linux进程控制

2025-12-23 12:24:05
文章摘要
定义:内核是操作系统的核心部分,负责管理系统资源,如 CPU、内存、设备和文件系统。它是操作系统与硬件之间的桥梁。 功能: 进程管理:创建、调度和终止进程。 内存管理:分配和回收内存,管理虚拟内存。 设备管理:与硬件设备进行通信,提供设备驱动程序。 系统调用:提供用户程序与内核之间的接口。 类型:内核可以是单内核(Monolithic Kernel)或微内核(Microkernel)。单内核将所有

@TOC

内核(Kernel)

定义:内核是操作系统的核心部分,负责管理系统资源,如 CPU、内存、设备和文件系统。它是操作系统与硬件之间的桥梁。 功能: 进程管理:创建、调度和终止进程。 内存管理:分配和回收内存,管理虚拟内存。 设备管理:与硬件设备进行通信,提供设备驱动程序。 系统调用:提供用户程序与内核之间的接口。 类型:内核可以是单内核(Monolithic Kernel)或微内核(Microkernel)。单内核将所有功能集成在一起,而微内核则将功能模块化。

写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。应用于物理内存。 如图: ![在这里插入图片描述]图片描述

  1. 未修改的时候,父进程和子进程的对应的内容时相同的,权限(读取)也是相同的。

  2. 当子进程进行修改,就会在页表进行映射,由于子进程修改的内容是正确的,页表对应的映射权限有错,操作系统就会介入,在物理内存进行写时拷贝,然后更新对应的映射关系,权限也会修改。

写时拷贝的好处: 1.提高内存利用率 2. 申请地址后拷贝数据过来,可以防止很大程度上出现问题。

进程终止

进程退出场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

进程退出方法

main函数的返回值(退出码)

main函数的返回值是0,表示进程正常结束,不为0,表示进程不是正常结束。 这个main函数的返回值就是进程的退出码, 是让bash看的,可以使用如下命令进行查看最近一次进程的退出码:

echo $?

![在这里插入图片描述]![图片描述]图片描述这些退出码是需要计算机获取的,但是我们是不知道是否执行成功,所以就会有了把退出码转换成对应的信息描述。 需要使用到strerron函数来进行查看对应的的信息

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
int main()
{
  for(int i = 0; i < 200; i++)
  {
    printf("%d: %s\n", i , strerror(i));
  }
  return 0;
}

如图: ![在这里插入图片描述]图片描述

虽然我们可以知道main函数的返回码,但是我们在写其他的函数的时候,也是希望在调用函数的的时候查看对应的情况, 调用函数我们想知道的:

  1. 函数的执行结果
  2. 函数的执行情况

前面的我们知道C语言的文件的fopen函数,如果打开失败,我们可以使用errno这个全局变量来获取对应的错误码信息。在C语言中,errno 是一个全局变量,用于指示最近一次系统调用或库函数调用的错误类型。它是一个整型变量,定义在 <errno.h> 头文件中。使用 errno 可以帮助程序员了解错误的具体原因,从而采取相应的处理措施。

_exit和exit(进程信号)

一般情况下,前面我们写代码的时候,有时候会因为访问野指针而崩溃,这是因为进程收到了异常信号。 我看可以使用kill -l查看对应的命令参数,如图: ![在这里插入图片描述]图片描述 我们可以写一段代码,然后运行起来,然后给这个进程发送一个信号,这里使用的kill -8

include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
int main()
{
  while(1)
  {
    printf("getpid: %d\n", getpid());
    sleep(1); 
}
return 0;
}

效果如下:

![在这里插入图片描述]图片描述 可以说明不同的信号代表不同的异常信息。 除了使用kill命令进行进程的终止,还可以exit函数终止进程

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
int main()
{
  while(1)
  {
    printf("getpid: %d\n", getpid());
    sleep(1); 
    exit(3);//exit终止进程的退出,退出码为3
  }
  return 0;
}

如图:

![在这里插入图片描述]![图片描述]进程运行一下就终止了,退出码为3

exit和_exit的区别:

至于_exit这个函数的使用,和exit的使用的是一模一样的,但是有一点不一样的就是,exit函数比_exit函数多了一步,刷新缓冲区.缓冲区是不在操作系统里的。

由此我们可以总结出任意进程的执行情况由两个数字进行表示,一个是进程的信号值,一个是退出码

![在这里插入图片描述]图片描述

进程的终止是操作系统操作,进程的创建和销毁都是操作系统进行操作的,进程的创建首先创建对应的PCB和 进程地址空间,页表,然后操作系统把数据加载到内容上,然后进行映射,进程终止,创建的地址全部释放掉,保留PCB,等待父进程进行释放

进程等待

进程等待必要性 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

一个进程的结束,会保留对应进程的PCB,然后等待父进程进行释放, 进程等待的原因:

  1. 父进程通过wait方式,回收子进程的资源
  2. 通过wait方式,获取到子进程的退出信息

进程等待的方式

wait

使用这个函数,需要导入

#include <sys/types.h>
#include <sys/wait.h>

status: 一个指向整数的指针,用于存储子进程的退出状态。如果不需要状态信息,可以传入NULL,我们只需传入一个整数地址,然后就会在父进程进行把子进程的退出码返回到这个整数上覆盖。

返回值 成功时返回终止的子进程的PID

失败时返回-1,并设置errno 下面写一段代码进行验证

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t id = fork();
  //子进程运行
  if(id == 0)
  {
    int cnt = 6;
    while(cnt)
    {
      printf("I is child pid: %d, ppid: %d\n",getpid(), getppid());
      sleep(1);
      cnt--;
    }
    exit(0);
}
//父进程等待10秒
sleep(10);
//父进程阻塞等待
pid_t rid = wait(NULL);
if(rid > 0)
{
printf("wait succes, rid:%d\n",rid);
}
sleep(3);
return 0;
}

当运行成功后,查看对应的进程运行情况,就会发现,父进程等待到子进程运行完成,进程运行到对应时间,就会把子进程的僵尸状态释放掉。 如图: ![在这里插入图片描述]图片描述

waitpid

pid_t waitpid(pid_t pid, int *status, int options);

参数说明: pid: 如果 pid 为 -1,表示等待任意子进程,等待任一个子进程。与wait等效。 如果 pid 为具体的进程 ID,表示等待该特定子进程。 如果 pid 为 0,表示等待与调用进程同一进程组中的任意子进程。 如果 pid 为大于 0 的值,表示等待该特定子进程。

status: 指向整数的指针,用于存储子进程的退出状态。如果不需要状态信息,可以传递NULL。

options: 可以是 0 或者其他选项,如 WNOHANG(非阻塞等待)等。WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。

返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

代码如下:

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t id = fork();//如果是父进程是具体数字,子进程就是0
  if(id == 0)
  {
    int i = 0;
    while(i < 5)
    {
    printf("child pid: %d, father pid: %d\n", getpid(), getppid());
    i++;
    sleep(1);
    }
    exit(1);//结束进程,退出码为1
    printf("chila process end\n");
  }
//父进程进行等待
int status = 0;//设置子进程的退出状态,这个不是退出码和信号
printf("当前的进程的pid:%d\n", id);
pid_t childId = waitpid(id, &status,0);
printf("父进程释放完毕,释放的子进程id:%d, 子进程的状态码: %d\n", childId, status);
sleep(10);
return 0;
}

效果如下: ![在这里插入图片描述]图片描述

子进程的status

当我们查看对应的子进程的退出码的时候,就会发现, 子进程的退出码为啥不是exit设置的1呢, 为啥是256, 原因如下: 一个整形的大小是32bit status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位): ![在这里插入图片描述图片描述因为,在Linux中,一个整形的大小是32位, ![在这里插入图片描述]图片描述我们代码中的exit设置为1,所以会在16位bit后面补上八位的二进制写法如下: ![在这里插入图片描述]图片描述 子进程没有收到信号就会补全后面的信号编码格式,组合起来就会变成一个整数,也就是status这个整数,如图: ![在这里插入图片描述]图片描述当我们把代码修改为如下:

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t id = fork();//如果是父进程是具体数字,子进程就是0
  if(id == 0)
  {
    int i = 0;
    while(i < 5)
    {
    printf("child pid: %d, father pid: %d\n", getpid(), getppid());
    i++;
    sleep(1);
    }
    exit(1);//结束进程,退出码为1
    printf("chila process end\n");
  }

我们发送向子进程发送一个8的信号,就会观看到对应的信号码: ![在这里插入图片描述]图片描述我们下个获取对应的信息除了可以手动计算,在C语言中也提供对应的宏,直接获取到对应的的退出码以及进程是否为正常终止:

  1. WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常 出)
  2. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

疑问1: 为啥我们不在父进程中使用全局变量来控制子进程的状态码和信号码,而是使用waitpid来获取对应的状态码? 因为虽然我们使用fork创建子进程,子进程会继承对应的父进程的全局变量,但是当父进程或者子进程对这些全局变量进行修改,修改的进程就会发生写时拷贝,进程之间互不干扰,对应的的进程地址空间,是通过页面进行映射到对应的物理内存,哪个进程修改全局变量,都不会影响其他进程,所以就会使用waitpid传入status这个参数,获取对应的子进程的状态码。

父进程的阻塞和非阻塞

阻塞 在使用waitpid的时候,会发现有一个参数就是options,当设置为0的时候,父进程会等待子进程执行结束,这一过程父进程会进行阻塞等待,只会调用调用一次系统调用。 非阻塞 如果设置为宏WNOHANG,如果子进程还没结束,就会让父进程获取子进程的错误信息(不管子进程是否运行正常,都会返回错误的信息),让父进程继续执行,不会进行阻塞等待,然后循环调用waitpid进行访问对应的的子进程的状态,直到子进程运行结束, 期间会进行多次的调用系统调用。父进程可以继续执行其他任务,同时定期检查子进程状态。 如图: ![在这里插入图片描述]图片描述 代码如下:

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
typedef void(*fun_t)();
#define  NUM 10
fun_t task[NUM];
void taskLog()
{
  printf("this is tasklog\n");
}
void taskNet()
{
  printf("this is taskNet\n");
}
void initTask()
{
  task[0] = taskLog;
  task[1] = taskNet;
  task[2] = NULL; 
}
void funtionPrint()
{
  initTask();
  printf("任务开始\n");
  for(int i = 0; task[i];i++)
  {
    printf("%p\n", task[i]);
    (*task[i])();
  }
}
int main()
{
  //创建子进程
  pid_t id = fork();
  if(id==0)
  {
    int elemet = 20;
    while(elemet > 6)
    {
      printf("child pid: %d father pid: %d\n", getpid(), getppid());
      sleep(1);
      elemet--;
    }
    printf("child ended\n");
    exit(1);
}
//父进程开始操作
//
int status = 0;
printf("father state\n");
while(1)
{
pid_t childId =  waitpid(id, &status, WNOHANG);
printf(&quot;child status: %d, child exit code %d, childe exit sigend: %d\n&quot;, status, WEXITSTATUS(status), (status&amp;0x7F));
printf(&quot;当前获取到子进程的id为%d, 需要等待到的子进程id:%d\n&quot;, childId, id);
sleep(1);
funtionPrint();
if(childId == id)
  break;

只需把任务写入循环中,父进程在访问后,不会进行阻塞,就是执行自己的任务。

进程程序替换

原理如下: ![在这里插入图片描述]图片描述 如上图前面我们知道,一个进程的基本工作原理,进程创建首先会创建对应的PCB,进程拥有对应的进程地址空间,通过页表映射到对应的物理内存,执行对应的内容,而进程程序替换就是在可执行程序加载到内存这一步进行相关的操作,把新程序的内容覆盖原理的物理空间的内容,这样就达到了一个程序可以运行不一样的程序的内容。 这个过程不会创建新的PCB,

父进程和子进程执行不同分支的原理

前面我们使用fork创建子进程,就会好奇,进程的工作原理就是需要通过页表映射到物理内存,父进程的页表和子进程的页表相同,可以找到对应的内容,但是如果有代码分支,比如只有子进程才能执行的代码,这块是怎么让父进程无法执行的呢,这是因为子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

替换函数

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

execl

int execl(const char *path, const char *arg, ...);

参数说明: path: 表示对应的文件路径 arg:表示运行的方式 前面我们知道,在Linux中的命令都是一个个对应的的程序,都是在/usr/bin/中,只不过是添加到对应的环境变量, 比如ls -a中ls、-a就相当于参数arg, /usr/bin/ls就是path。 返回参数: execl运行失败就会返回-1,成功不会返回 下面写一个ls的代码:

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  printf("start process\n");
  execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
  printf("process end\n");
  return 0;
}

需要注意的是,execl函数的最后一个参数一定是NULL(不是字符),

效果如下: ![在这里插入图片描述]图片描述 可以看出,运行成功后,执行的是ls执行的功能,但是当执行完execl函数后,后面的代码就不运行了。因为把ls程序的代码替换进去了,导致本该运行execl函数的后面的代码被ls的代码覆盖了。如果execl函数执行失败,就会执行后面的代码

进程替换工作本质就是程序加载, 创建一个进程,首先会先创建对应的的PCB,地址空间,页表,然后再把程序加载到内存,

多进程版本 上面的代码是在父进程进行,我们可以创建一个子进程,然后让子进程进行执行这些代码:

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    printf("start process\n");
    printf("child pid: %d , ppid:%d\n", getpid(), getppid());
    sleep(20);
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    printf("process end\n");
}
int status = 0;
pid_t childId = waitpid(id, &status, 0);
if(childId > 0)
printf("child exit status: %d, exit code:%d exit signal: %d\n", status, WEXITSTATUS(status), status&0x7f);
return 0;
}

这里通过子进程去进行程序替换,为啥子进程不会影响父进程呢, 因为为了保护进程的独立性,子进程在进行程序替换(就是把程序加载到内存,进行覆盖)会进行写时拷贝,进而不会影响父进程。

execlp

小知识: p:PATH,不用告诉程序在哪里,系统替换过程,会在PATH中去寻找

int execlp(const char *file, const char *arg, ...);

代码如下:

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t pid = fork();
  if(pid == 0)
  {
    printf("I am is child, pid:%d\n", getpid());
    execlp("ls", "ls", "-a","-l", NULL); 
    printf("child end\n");
  }
  int stat = 0;
  pid_t childPid =  waitpid(pid, &stat, 0);
  printf("child pid: %d\n", childPid);
  return 0;
}

效果: ![在这里插入图片描述]图片描述

execv

int execv(const char *path, char *const argv[]);

这个函数的传参不像前面的函数传参一样,这里第二个参数和我们前面的main函数的第二个参数是一样的 main(int argc, char* const argv[]); 需要我们自己构建对于的argv,重要的 一点就是NULL结尾(不是字符). 所以我们可以大致理解就是 l:就是列表传参 v:就是vector传参

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t pid = fork();
  if(pid == 0)
  {
    char* argv[] = {
      (char*)"ls",
      (char*)"-a",
      (char*)"-l",
      NULL
    };
    printf("child end , child pid: %d\n", getpid());
    execv("/usr/bin/ls", argv);
}
int stata = 0;
pid_t childPid = waitpid(pid, &stata, 0);
printf("child pid is %d", childPid);
printf("father end\n");
return 0;
}

效果如下: ![在这里插入图片描述]图片描述

含e的函数

int execve(const char *path, char *const argv[], char *const envp[]);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

e:跟环境变量有关 如图: ![在这里插入图片描述]图片描述这些就是环境变量的,PYTH就是其中的一个 下面我们以execle为例 我们自己写一个程序,然后通过进程程序替。 在Linux中,.cc 或者.cpp或者.cxx都是C++的后缀名 .cc

#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, char* argv[])
{
  for(int i = 0; i < argc; i++)
  {
    cout << argv[i] << endl;
  }
  std::cout << "我是程序"<< std::endl;
  return 0;
}

.cpp

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{
  pid_t pid= fork();
  if(pid == 0)
  {
    printf("我是子进程开始\n");
    execle("./mytest", "./mytest", "-a", "-d", "-c", NULL);
}
int stat = 0;
pid_t pidChlid = waitpid(pid, &stat, 0);
printf("父进程结束, 等待的子进程为:%d\n", pidChlid);
return 0;
}

结果如下: ![在这里插入图片描述]图片描述 在Linux下写的任何语言都可以进行相关的进程程序替换,因为这些语言运行起来就是一个个进程, 其他语言的调用比如shell脚本, ![在这里插入图片描述]图片描述 bash为命令行, mytest.sh为命令行参数,书写如下:

 execle("/usr/bin/bash", "bash", "mytest.sh", NULL);

没有传入env运行成功的 上面我们发现, execle函数,子进程是没有传入一个对应的的环境变量表的,为啥会运行成功呢? 这是因为在进程地址空间中有一段空间是存放命令行参数环境变量的,创建子进程,会复制父进程的进程地址空间, 而进程替换是不会替换环境变量的数据的,还要一个因素就是在mytest.cc中是不涉及环境变量的运行起来是成功的, 在此,如果我们需要子进程在继承父进程的环境变量的基础上增加新的环境,可以使用函数putenv来进行增加, 如果想要从0开始可以自己构建例如:

char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

.c

#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(int argc, char* argv[], char* env[])
{
  pid_t pid= fork();
  if(pid == 0)
  {
    printf("我是子进程开始\n");
		//for(int i = 0; env[i]; i++)
  	//{
		//	printf("%s\n", env[i]);
  	//}
    printf("开始程序替\n");
    execle("./mytest", "./mytest", "-a", "-d", "-c", NULL, env);
}
  int stat = 0;
  pid_t pidChlid = waitpid(pid, &stat, 0);
  printf("父进程结束, 等待的子进程为:%d\n", pidChlid);
return 0;
}

.cc

#include<iostream>
#include<stdio.h>
#include<unistd.h>
using namespace std;
int main(int argc, char* argv[])
{
  extern char ** environ;
  for(int i = 0; environ[i]; i++)
  {
    cout << environ[i] << endl;
  }
  std::cout << "我是程序"<< std::endl;
  return 0;
}
声明:该内容由作者自行发布,观点内容仅供参考,不代表平台立场;如有侵权,请联系平台删除。