【unix高级编程系列】进程控制

avatar
作者
筋斗云
阅读量:0

在这里插入图片描述

引言

本文主要介绍unix系统的进程控制,其中包括创建进程、执行程序和进程终止。

进程标识

每一个进程都有一个非负整型表示的唯一进程ID,这也就是我们常说的进程ID。

注:虽然进程ID是唯一的,但是它也是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。但是大多数unix系统使用延迟复用算法

系统中也会有一些专用进程,比如:

  • 进程ID 0 通常是调度进程
  • 进程ID 1 通常是init 进程。它负责在自举内核后启动一个UNIX系统,init进程绝不会终止。它是一个普通进程,但是以超级用户特权运行,还是所有孤儿进程的父进程
  • 进程ID 2 通常是页守护进程,此进程负责支持虚拟存储器系统的分页操作。

可通过下面接口获取进程ID:

#include <unistd.h> //返回调用进程的ID pid_t getpid(void);  //返回调用进程的父进程ID pid_t getppid(void); 

创建进程

一个现有的进程可以调用fork函数创建一个新的进程。

#include<unistd.h> pid_t fork(void);     // 返回值:子进程返回0,父进程返回子进程ID;若出错,返回-1 

分析:为什么fork对父进程和子进程分别返回子进程ID和0?

  1. 对于父进程而言,它可以由多个子进程,并且没有相关接口获取指定进程的ID,因此需要在创建时刻获取子进程ID。
  2. 在第一点的基础上,若子进程返回父进程的ID,那么对于开发者而言,就区分不了哪一个是父进程,哪一个是子进程。所以用特殊值0表示子进程。若子进程需要获取父进程ID,可通过getppid接口。

子进程是父进程的副本:其中包括父进程数据空间、堆、栈的副本,并且父进程和子进程共享正文段

示例如下:

#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdint.h>  int32_t g_i32Var = 6; int main() {     int32_t i32Var;     pid_t pid;      i32Var = 88;     write(STDOUT_FILENO,"test fork\n",strlen("test fork\n"));     printf("before fork\n");      if((pid = fork()) < 0)     {         printf("fork failed\n");     }     else if(pid == 0)     {         g_i32Var++;         i32Var++;     }else{         sleep(2);     }      printf("pid = %d , globvar = %d, var = %d\n",getpid(),g_i32Var,i32Var);      return 0; } 

编译输出如下:

// 标准输出到终端 xieyihua@xieyihua:~/test$ gcc fork.c  xieyihua@xieyihua:~/test$ ./a.out  test fork before fork pid = 3635 , globvar = 7, var = 89 pid = 3634 , globvar = 6, var = 88 xieyihua@xieyihua:~/test$   // 标准输出重定向到文件 xieyihua@xieyihua:~/test$ ./a.out > 1 xieyihua@xieyihua:~/test$ cat 1  test fork before fork pid = 3749 , globvar = 7, var = 89 before fork pid = 3748 , globvar = 6, var = 88 xieyihua@xieyihua:~/test$  

分析:

  • 标准输出到终端
  1. globvarvar的值输出可知,子进程会复制父进程的栈和数据段,并且修改并不会影响父进程;
  2. 在前面章节中,我们知道write是不带缓冲的,直接输出到标准输出;
  3. printf是带缓冲的标准I/O,因为标准输出为终端,因此为行缓冲,直接输出;
  • 标准输出重定向到文件

由于标准输出重定向到文件,那么printf就是全缓冲;因此内部流程如下:

  1. 父进程第一次输出before fork\n时,并没输出到终端,而是缓存在数据区
  2. 父进程fork之后,子进程的数据区也copy了一份befor fork\n
  3. 当子进程结束时,开始清空缓存区,会将所有的缓存进行输出。
  4. 当父进程结束时,开始清空缓存区,会将所有的缓存进行输出。

因此,该输出中,第一个字符串before fork实际是子进程输出的。这个你get到了吗?

思考:从上述现象中,我们可知:fork函数,会将父进程的所有打开文件描述符都被复制到子进程中。在【unix高级编程系列】文件I/O了解到linux 对共享文件的处理方式。而父进程和子进程的关系如下:

这就引发了一个新的问题:父进程和子进程共享一个文件表项,若不进行同步,则会将两者的输出混合(描述符在fork之前打开)

fork函数失败的两个主要原因有:

  1. 系统中已经有太多的进程。(常见场景:系统中存在大量的僵尸进程)
  2. 该实际用户ID的进程数超过了系统限制。可通过下述命令查看:
xieyihua@xieyihua:~$ cat /proc/sys/kernel/pid_max 4194304 xieyihua@xieyihua:~$ sysctl kernel.pid_max kernel.pid_max = 4194304 xieyihua@xieyihua:~$ 

拓展: 现在很多的实现并不执行一个父进程数据段,栈和堆的完全副本。作为替代,使用了写时复制技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改为只读。如果父进程和子进程中的人一个试图修改这些区域,则内核只为修改区域的那块内存只做一个副本

进程退出

在上章节【unix高级编程系列】进程环境中,介绍了进程退出的8种方式(5种正常终止、3种异常终止方式):

  1. main函数内执行return语句。
  2. 调用exit函数。
  3. 调用_exit_Exit函数。
  4. 进程的最后一个线程在其启动列成中执行return语句。
  5. 进程的最后一个线程调用pthread_exit函数。
  6. 调用abort
  7. 进程收到某些信号时。
  8. 最后一个线程对取消请求做出响应。

不管进程如何终止,最后都期望执行内核中的同一段代码。这段代码的功能为相应进程关闭所有打开描述符,释放它所使用的存储器资源(PCB):进程ID、终止状态、以及该进程使用的CPU时间总量。

正常情况下,我们是期望父进程去执行上述操作,对子进程进行善后处理。流程:父进程调用waitwaitpid等待子进程结束,会释放上述资源。否则子进程的退出状态没有被获取,相关资源会一直保存在内核中,成为僵尸进程。

思考:由上可知,进程退出,其资源的回收时依赖父进程,否则会成为僵尸进程。那么若父进程在子进程之前终止,情况又是如何呢

答:unix 系统中,当一个进程终止时,内核会逐个检查所有活动进程,以判断它是否为即将终止进程的子进程。如果是,则将该进程的父进程ID更改为1(init 进程ID)。而init的实现逻辑:只要有一个子进程终止,init就会调用一个wait函数取得其终止状态,并进行资源回收。

总结:僵尸进程的产生原因:子进程退出,且父进程依旧运行,没有调用waitwaitpid系统调用来回收子进程的终止状态。

获取子进程终止状态

在上章节我们了解到父进程可以通过waitwaitpid获取子进程的终止状态,本章介绍如何使用,以及介绍如何获取子进程的相关资源信息。

#include<sys/wait.h>  pid_t wait(int* statloc);  pid_t waitpid(pid_t pid,int* statloc,int option);                 //两个函数返回值:若成功,返回进程ID;若出错,返回0。 

区别如下:

  • 没有任何一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞。
  • waitpid可通过入参,控制调用进程。比如:
pid == -1 : 等待任一子进程退出,此时与wait函数等效 pid > 0 等待进程ID与pid相等的子进程。即使不是父子进程关系 pid == 0 等待组ID等于调用进程组ID的任一子进程 pid < -1 等待组ID等于oid绝对值的任一子进程 

获取进程终止状态的常见用法如下:

  • 场景一:
    使用wait()系统调用: wait()系统调用会使父进程阻塞,直到任一子进程终止。当子进程终止时,wait()会回收子进程,并返回终止子进程的进程ID。
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h>  int main() {     pid_t pid = fork();      if (pid > 0) {         // 父进程         int status;         pid_t child_pid = wait(&status); // 等待子进程退出         if (child_pid > 0) {             printf("子进程 %d 已退出,状态: %d\n", child_pid, status);         }     } else if (pid == 0) {         // 子进程         printf("子进程开始执行...\n");         sleep(1); // 模拟子进程工作         printf("子进程结束。\n");         exit(0); // 子进程退出     } else {         // fork失败         perror("fork");         exit(1);     }      return 0; } 
  • 场景二:使用waitpid()系统调用: waitpid()系统调用类似于wait(),但它允许父进程指定等待哪个子进程,以及是否阻塞等待。
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h>  int main() {     pid_t pid = fork();      if (pid > 0) {         // 父进程         int status;         // 等待特定的子进程退出,这里使用WUNTRACED | WCONTINUED以捕获停止或继续的子进程         pid_t child_pid = waitpid(pid, &status, 0);         if (child_pid > 0) {             printf("子进程 %d 已退出,状态: %d\n", child_pid, status);         }     } else if (pid == 0) {         // 子进程         printf("子进程开始执行...\n");         sleep(1); // 模拟子进程工作         printf("子进程结束。\n");         exit(0); // 子进程退出     } else {         // fork失败         perror("fork");         exit(1);     }      return 0; } 

对于场景一,在子进程未终止前,父进程会一直阻塞,导致无法执行后续业务。若应用场景,父进程并不关注子进程的终止状态,仅为了避免产生僵尸进程,可通过信号处理:父进程可以设置一个信号处理函数来处理SIGCHLD信号,该信号在子进程退出时由内核发送给父进程。

#include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdio.h> #include <stdlib.h>  void sigchld_handler(int sig) {     int status;     pid_t child_pid = wait(&status);     if (child_pid > 0) {         printf("子进程 %d 已退出,状态: %d\n", child_pid, status);     } }  int main() {     signal(SIGCHLD, sigchld_handler); // 设置SIGCHLD的处理函数      pid_t pid = fork();      if (pid > 0) {         // 父进程         // ... 父进程可以继续其他工作     } else if (pid == 0) {         // 子进程         printf("子进程开始执行...\n");         sleep(1); // 模拟子进程工作         printf("子进程结束。\n");         exit(0); // 子进程退出     } else {         // fork失败         perror("fork");         exit(1);     }      return 0; } 

无论是wait还是waitpid只能获取进程的终止状态。实际上内核为终止进程还保存了其它信息,比如:系统资源信息,用户CPU时间总量、系统CPU时间总量、缺页次数、收到信号的次数等。我们可以通过wait4函数获取。

#include <sys/types.h> #include <sys/wait.h> #include <sys/time.h> #include <sys/resource.h> pid_t wait4(pid_t pid, int* statloc,int options,struct rusage* rusage); 

使用示例:

#include <sys/types.h> #include <sys/wait.h> #include <sys/resource.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>  int main() {     pid_t pid = fork();      if (pid == -1) {         // fork失败         perror("fork");         exit(EXIT_FAILURE);     }      if (pid > 0) {         // 父进程         int status;         struct rusage usage;         pid_t child_pid;          // 等待子进程退出,并获取资源使用情况         child_pid = wait4(pid, &status, 0, &usage);         if (child_pid == -1) {             perror("wait4");             exit(EXIT_FAILURE);         }          // 检查子进程是否正常退出         if (WIFEXITED(status)) {             printf("子进程 %d 已退出,退出状态: %d\n", child_pid, WEXITSTATUS(status));         }          // 打印资源消耗         printf("用户态CPU时间: %ld.%06ld秒\n", usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);         printf("核心态CPU时间: %ld.%06ld秒\n", usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);         printf("最大驻留集大小: %ld 千字节\n", usage.ru_maxrss);         printf("页面错误次数: %ld\n", usage.ru_majflt);         printf("自愿上下文切换次数: %ld\n", usage.ru_nvcsw);         printf("非自愿上下文切换次数: %ld\n", usage.ru_nivcsw);     } else {         // 子进程         printf("子进程开始执行...\n");         // 子进程做一些工作,这里只是简单地睡眠一段时间         sleep(2);         printf("子进程结束。\n");         exit(EXIT_SUCCESS); // 子进程正常退出     }      return 0; } 

输出如下:

xieyihua@xieyihua:~/test$ gcc 3.c -o 3 xieyihua@xieyihua:~/test$ ./3  子进程开始执行... 子进程结束。 子进程 5472 已退出,退出状态: 0 用户态CPU时间: 0.001042秒 核心态CPU时间: 0.000000秒 最大驻留集大小: 1028 千字节 页面错误次数: 0 自愿上下文切换次数: 2 非自愿上下文切换次数: 0 xieyihua@xieyihua:~/test$  

总结

文章主要介绍了Unix系统中进程控制的相关知识,包括进程标识、创建进程、进程退出以及获取子进程终止状态的方法。

  1. 进程标识。
    每个进程都有一个唯一的进程ID(PID),用于标识系统中的进程。PID是可复用的,当一个进程终止后,其PID就成为复用的候选者。
  2. 创建进程。fork函数是用来创建新进程的。父进程通过fork创建子进程,子进程是父进程的副本,包括数据空间、堆、栈的副本,并且父进程和子进程共享正文段。
  3. 进程退出。进程有多种方式退出,包括正常退出(如return语句、exit函数)和异常退出(如信号处理)。正常情况下,父进程应该调用wait或waitpid来回收子进程的终止状态,否则子进程的退出状态没有被获取,相关资源会一直保存在内核中,成为僵尸进程。
  4. 获取子进程终止状态。父进程可以通过wait、waitpid和wait4系统调用获取子进程的终止状态。wait和waitpid可以获取进程的终止状态,而wait4还可以获取进程的资源使用情况。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
在这里插入图片描述

在这里插入图片描述

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!