【Linux】进程控制

目录

一、进程创建

1、fork

1.1、fork常规用法

1.2、fork调用失败的原因

2、写时拷贝

二、进程终止

1、进程退出码 

2、进程退出方式

三、等待进程

1、进程等待必要性

2、进程等待的方法

2.1、wait

2.2、waitpid

3、获取子进程退出信息

四、进程程序替换

1、替换原理

1.1、进程的角度

1.2、程序的角度

2、替换函数

2.1、execl

2.2、execv

2.3、execlp

2.4、execvp

2.5、execle

2.6、总结


一、进程创建

1、fork

 在linux中 fork 函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

 进程调用 fork ,当控制转移到内核中的 fork 代码后,内核会做如下工作:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

【Linux】进程控制_第1张图片

 当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。即 fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork 之后,谁先执行完全由调度器决定

1.1、fork常规用法

  •  一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.2、fork调用失败的原因

  • 系统中有太多的进程,内存空间不足
  • 实际用户的进程数超过了限制

2、写时拷贝

 通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

【Linux】进程控制_第2张图片

二、进程终止

进程退出共有以下三种场景:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止,因为某些原因,导致进程收到了来自操作系统的信号。

1、进程退出码 

 当进程正常执行完后,会返回退出码。一般而言,当结果正确,退出码为 0 ,当结果不正确,退出码为 非 0 值,比如 1、2、3、4... ,分别对应不同的错误原因,供用户进行进程退出健康状态的判断。

使用如下代码进程举例说明:

  1 #include 
  2 #include 
  3 #include 
  4 
  5 
  6 int add_to_top(int top)
  7 {
  8   int sum = 0;
  9   for(int i = 0; i < top; ++i)
 10   {
 11     sum += i;
 12   }
 13   return sum;
 14 }
 15 
 16 int main()
 17 {
 18   int result = add_to_top(100);
 19   if(result == 5050) return 0;//结果正确
 20   else return 1;//结果不正确
 21 }

【Linux】进程控制_第3张图片

计算从 1 到 100 的累加,如果结果等于 5050 ,则说明结果正确,正常返回 0,否则说明结果不正确,返回 1 。

查看进程返回结果的指令:

echo $?

 根据我们自己所写的代码,返回值为 1 ,说明结果错误。


补充说明

之后再输入 echo $? 后,显示的结果就都是 0 了:

【Linux】进程控制_第4张图片

 这时因为 $? 只会保留最近一次执行的进程的退出码。


 关于C语言提供的进程退出码所代表的含义我们可以通过函数 strerror 来获取:

【Linux】进程控制_第5张图片

 其中 errnum 为退出码。

我们编写如下代码来查看这些退出码的含义:

int main()
{
  for(int i = 0; i < 140; ++i)
  {
    printf("%d : %s\n", i, strerror(i));
  }

  return 0;
}

由于输出结果较长,这里就不再全部放出,我截取了其中一部分:

【Linux】进程控制_第6张图片

观察到:如果返回值为 0,说明进程成功,如果返回值为 2, 说明没有这个文件或目录。

但是并不是所有指令的退出码都是根据C语言提供的进程退出码为基准的,比如:

【Linux】进程控制_第7张图片

 我们使用 kill -9 指令来杀死一个不存在的进程时所报的错误如果按照C语言的标准,退出码应该为3,但实际上退出码是 1 。

我们也可以自己来定义进程退出码的含义:

【Linux】进程控制_第8张图片

2、进程退出方式

当一个进程退出时,OS中就少了一个进程,就要释放该进程对应的内核数据结构 + 代码和数据。

进程正常退出有三种方式:

  • 从main函数return
  • 调用exit
  • _exit

 众所周知,只有 main 函数 return 才标志进程退出,其他函数 return 仅仅代表函数返回,这说明进程执行的本质是 main 执行流执行

 前面的内容已经介绍过 return 退出的方式,接下来讲解 exit 函数退出的方式:

【Linux】进程控制_第9张图片

编写如下代码:

【Linux】进程控制_第10张图片

 使用指令 echo $? 查看进程退出码:

【Linux】进程控制_第11张图片

 看到退出码为我们自己写入的 123 。由此我们得知函数 exit(int code) 中的参数 code 代表的就是进程退出码。在代码的任意地方调用 exit 函数都表示进程退出。


 函数 exit 为C标准库函数,除此之外还有一个 _exit 函数,该函数为系统调用

【Linux】进程控制_第12张图片

 其用法与 exit 相同。 

 exit _exit 的区别在于,exit 中封装了 _exit, exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

【Linux】进程控制_第13张图片

三、等待进程

1、进程等待必要性

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

2、进程等待的方法

 进程等待就是通过系统调用,获取子进程退出码或者退出信号的方式,顺便释放内存。

等待进程有两种方式,分别为 wait waitpid

2.1、wait

【Linux】进程控制_第14张图片

返回值:成功返回被等待进程pid,失败返回-1。

参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

 编写如下代码:

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 #include 
  7 
  8 int main()
  9 {
 10   pid_t id = fork();
 11   if(id == 0)
 12   {
 13     //子进程
 14     int cnt = 5;
 15     while(cnt)
 16     {
 17       printf("子进程,剩余%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
 18       sleep(1);
 19     }
 20     exit(0);
 21   }
 22 
 23   //父进程
 24   sleep(10);
 25   pid_t ret_id = wait(NULL);
 26   printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d\n", getpid(), getppid(), ret_id);
 27   sleep(5);                                                                                                              
 28 
 29   return 0;
 30 }
      

【Linux】进程控制_第15张图片

 在命令行输入

while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; sleep 1; echo "--------------"; done

运行观察结果:

【Linux】进程控制_第16张图片

 可以观察到 mytest 进程的三个阶段,第一阶段,父子进程都在运行。第二阶段,子进程变为僵尸进程,父进程继续运行。第三阶段,经过等待,僵尸进程被回收,父进程继续运行。

2.2、waitpid

【Linux】进程控制_第17张图片

 函数 pid_ t waitpid(pid_t pid, int *status, int options);
 1、返回值

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

2、参数

  • pid:
    Pid = -1,等待任一个子进程。与wait等效。
    Pid > 0,等待其进程ID与pid相等的子进程。
  • status:
    WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  • options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

 其中,waitpid函数的参数 status 是一个输出型参数,用于获取子进程的状态,即子进程的信号 + 退出码。我们可以把 status 看作位图:

【Linux】进程控制_第18张图片

 整数 status 32 个比特位,我们只使用其中低 16 个比特位。

 低16位中的次低 8 位代表退出状态,也成为退出码,低 7 位代表进程退出时收到的信号,如果为 0 ,就说明没有收到退出信号,为正常退出,如果信号不为 0 ,就说明进程是异常退出。只有在正常退出时,我们才会关注退出码。至于 core dump 以后再讲。

编写如下代码进行说明:

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 #include                                   
  7                                                         
  8 int main()                                              
  9 {                                                       
 10   pid_t id = fork();                                    
 11   if(id == 0)                                           
 12   {                                                     
 13     //子进程                                            
 14     int cnt = 5;                                        
 15     while(cnt)                                                                                                
 16     {                                                                                                         
 17       printf("子进程,剩余%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());                             
 18       sleep(1);                                                                                               
 19     }                                                                                                         
 20     exit(123);                                                                                                
 21   }                                                                                                           
 22                                                                                                               
 23   //父进程                                                                                                    
 24   int status = 0;                                                                                             
 25   pid_t ret_id = waitpid(id, &status, 0);                                                                     
 26   printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);                                                          
 28 
 29   return 0;
 30 }

【Linux】进程控制_第19张图片

运行结果如下:

【Linux】进程控制_第20张图片

 子进程的信号为 0 ,退出码为 123 。符合我们的预期。

对于status,除了我们自己按位操作以外,也可以使用库提供的宏来替换:

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

【Linux】进程控制_第21张图片

3、获取子进程退出信息

 我们知道子进程拥有自己的PCB结构 task_struct ,在task_struct中存在两个变量,分别为 int exit_code int exit_signal 。当子进程退出时,OS会把退出码填写到 exit_code 中,把退出信号填写到 exit_signal 中,并维护子进程的 task_struct ,此时子进程的状态就是僵尸状态。通过 wait 或者 waitpid 系统调用可以访问到该内核数据结构,并把退出信息以上面所讲过的格式存放在 status 中,顺便释放该数据结构占用的内存空间。

 了解了以上知识后,我们应该有一个疑问,父进程在等待子进程退出,并回收子进程。那么如果子进程一直都没有退出,父进程又在做什么呢?

 默认情况下,在子进程没有退出的时候,父进程只能一直在调用 wait waitpid 进行等待,我们称之为阻塞等待。关于阻塞的内容可以参考文章《进程概念》。

【Linux】进程控制_第22张图片

 当子进程退出时,通过 parent 指针找到父进程,并把父进程放到运行队列中,继续执行 wait waitpid 指令。

如果不想让父进程阻塞等待,则可以通过设置 waitpid 系统调用的参数 options WNOHANG 来实现非阻塞轮询。

 非阻塞轮询有三种结果:

  1. waitpid > 0:好了
  2. waitpid == 0:没好,再等等
  3. waitpid < 0:出错
  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 #include 
  7 
  8 int main()
  9 {
 10   pid_t id = fork();
 11   if(id == 0)
 12   {
 13     //子进程
 14     while(1)
 15     {
 16       printf("子进程, pid: %d, ppid: %d\n", getpid(), getppid());
 17       sleep(1);
 18     }
 19     exit(0);
 20   }
 21   //父进程
 22   while(1)
 23   {
 24 
 25     int status = 0;                                                                                                                 
 26     pid_t ret_id = waitpid(id, &status, WNOHANG);
 27     if(ret_id < 0)
 28     {
 29       printf("waitpid error!\n");
 30       exit(1);
 31     }
 32     else if(ret_id == 0)
 33     {
 34       printf("子进程没退出,处理其他事情。。\n");
 35       sleep(1);
 36       continue;
 37     }
 38     else
 39     {
 40       printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
 41               getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
 42       break;
 43     }
 44 
 45   }
 46   return 0;
 47 }

【Linux】进程控制_第23张图片

运行观察结果:

【Linux】进程控制_第24张图片

这就叫做父进程的非阻塞状态。

下面我们来写一个完整的父进程非阻塞代码:

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 #include 
  7 
  8 #define TASK_NUM 10
  9 
 10 //预设任务
 11 void sync_dick()
 12 {
 13   printf("刷新数据\n");
 14 }
 15 
 16 void sync_log()
 17 {
 18   printf("同步日志\n");
 19 }
 20 
 21 void net_send()
 22 {
 23   printf("网络发送\n");
 24 }
 25 
 26 //要保存的任务
 27 typedef void (*func_t)();                                                                                                           
 28 func_t other_task[TASK_NUM] = {NULL};
 29 
 30 
 31 int LoadTask(func_t func)
 32 {
 33   int i = 0;
 34   for(; i < TASK_NUM; ++i)
 35   {
 36     if(other_task[i] == NULL) break;                                                                                                
 37   }
 38   if(i == TASK_NUM) return -1;
 39   else other_task[i] = func;
 40 
 41   return 0;
 42 }
 43 
 44 
 45 void InitTask()
 46 {
 47   for(int i = 0; i < TASK_NUM; ++i) other_task[i] = NULL;
 48   LoadTask(sync_dick);
 49   LoadTask(sync_log);
 50   LoadTask(net_send);
 51 }
 52 
 53 void RunTask()
 54 {
 55   for(int i = 0; i > 8) & 0xFF ,  status & 0x7F);
106       break;
107     }
108 
109   }
110   return 0;
111 }

四、进程程序替换

创建子进程无非就两种目的:

  1. 让子进程执行父进程的一部分代码
  2. 让子进程执行全新的程序代码

为了让子进程执行全新的程序代码,就需要进行程序替换。

1、替换原理

1.1、进程的角度

 用 fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
【Linux】进程控制_第25张图片

1.2、程序的角度

 程序原本存放在磁盘中,当调用 exec 函数时,被加载到了内存中。所以程序替换就相当于程序加载器,我们平常说程序被加载到内存中,其实就是调用了 exec 。在创建进程的时候,是先创建的进程数据结构PCB,再把代码和数据加载到内存的。

2、替换函数

 程序替换的接口函数:

【Linux】进程控制_第26张图片

2.1、execl

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

 函数参数列表中的 "..." 为可变参数。可以让我们给C函数传递任意个数个参数。 path 程序路径 arg 命令 + 命令参数,最后一定要以 NULL 为结尾。

编写如下代码进行解释说明:

  1 #include 
  2 #include 
  3 #include 
  4 
  5 int main()
  6 {
  7   printf("hello world\n");
  8   printf("hello world\n");
  9   printf("hello world\n");
 10   printf("hello world\n");
 11 
 12   execl("/bin/ls", "ls", "-a", "-l", NULL);                                                                                          
 13 
 14   printf("can you see me\n");
 15   printf("can you see me\n");
 16   printf("can you see me\n");
 17   printf("can you see me\n");
 18   return 0;
 19 }

【Linux】进程控制_第27张图片

 需要注意的是,命令参数一定要以 NULL 为结尾。

运行程序观察结果:

【Linux】进程控制_第28张图片

 可以看到 execl 函数后,执行程序替换,新的代码和数据就被加载进内存了,后续的代码属于老代码,直接被替换掉,没机会再执行了。程序替换是整体替换,不能局部替换

 进程替换只会影响调用 execl 的进程,不会影响其他进程,包括父进程,因为进程具有独立性。换句话说,子进程加载新程序的时候,是需要进行程序替换的,发生代码的写时拷贝


补充内容

 我们知道 execl 是一个函数,也有可能调用失败。如果程序替换失败,进程会继续执行老代码,并且 execl 一定会有返回值。反之,如果程序替换成功,则 execl 一定没有返回值。只要 execl 有返回值,则程序替换一定失败了。

 如果程序替换成功,新程序的退出码会返回给子进程,同样可以被父进程拿到:

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 
  6 int main()
  7 {
  8   pid_t id = fork();
  9   if(id == 0)
 10   {
 11     printf("子进程, pid: %d\n", getpid());
 12     execl("/bin/ls", "ls", "not found", NULL);
 13     exit(1);
 14   }
 15 
 16   sleep(5); 
 17 
 18   int status = 0;
 19   printf("父进程, pid: %d\n", getpid());
 20   waitpid(id, &status, 0);
 21   printf("child exit code: %d\n", WEXITSTATUS(status));                                                                                 
 22                                                                                                    
 23   return 0;                                                                                        
 24 }                                 

 因为 ls 指令没有 "not found" 命令选项,所以新程序一定会返回对应的退出码给子进程,并最终被父进程获取:

【Linux】进程控制_第29张图片

【Linux】进程控制_第30张图片


2.2、execv

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

函数参数列表中, path 程序路径 argv 数组内存放 命令 + 命令参数 execl execv 只在传参形式上有所不同。

【Linux】进程控制_第31张图片

 观察结果:

【Linux】进程控制_第32张图片

2.3、execlp

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

 函数参数列表中, file 程序名 arg 命令 + 命令参数, "..." 可变参数。除了 file 外,其他用法与 execl 相同。

 当我们执行指定程序的时候,只需要指定程序名即可,系统会自动在环境变量 PATH 中进行查找。

【Linux】进程控制_第33张图片

查看运行结果:

【Linux】进程控制_第34张图片

2.4、execvp

int execvp(const char *file, char *const argv[]);

函数参数列表中, file 程序名 argv 数组内存放命令 + 命令参数

【Linux】进程控制_第35张图片

2.5、execle

int execle(const char *path, const char *arg,
                  ..., char * const envp[]);

 execle 的函数参数列表中,比 execl 多了一个 envp envp 为自定义环境变量。

 我们在当前目录的子目录 exec 里再编写一个可执行文件 otherproc :

【Linux】进程控制_第36张图片

观察运行结果:

【Linux】进程控制_第37张图片

 可以发现该子进程没有环境变量 MYENV

现在我们接着编写之前的 myproc 程序:

【Linux】进程控制_第38张图片

 在 myproc 中使用 execle 函数调用 otherproc 程序,并给该程序传递环境变量 MYENV

运行并观察结果:

【Linux】进程控制_第39张图片

 发现 otherproc 进程中已经有了环境变量 MYENV ,但是 PATH 却没有了。这是因为函数 execle 传递环境变量表是覆盖式传递的,老的环境变量表被全部清空了,只保留我们传递的自定义环境变量。

如果我们想在原有环境变量的基础上给进程添加环境变量,则可以使用函数 putenv

【Linux】进程控制_第40张图片

 运行观察结果:

【Linux】进程控制_第41张图片

 此时就可以得到预期的结果了。

 因为所有的进程都是 bash 的子进程,而 bash 执行所有的指令都可以直接通过 exec 来执行,如果需要把环境变量交给子进程,只需要调用 execle 就可以了。因此,我们成环境变量具有全局属性。

2.6、总结

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

【Linux】进程控制_第42张图片

 事实上,只有 execve 是真正的系统调用,其它函数都是 execve 的封装,最终都要调用 execve


关于进程控制的相关内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!

你可能感兴趣的:(Linux,linux,服务器,运维)