文章目录引用:之前的文章介绍了 Linux 下进程相关的基础概念和一些常见的 shell 命令,本文则延续之前的思路——继续 Linux 下进程知识的学习与总结,介绍 Linux 下进程管理相关的 API,包括进程创建、进程信息查询、进程退出和进程回收。
- 一、进程信息查询
- 进程常用信息介绍
- 进程信息查询 API
- 二、进程创建
- 进程创建 API
- 父子进程关系
- 区分父子进程
- 三、进程退出
- 四、进程回收
- 进程回收概述
- 进程回收 API
- 总结
每个进程都由一个进程号来标识,其类型为 pid_t,进程号的类型—— pid_t 其实为一个短整形,所以pid_t能表示的范围是:0~32767。进程号总是唯一的,但进程号可以重用:同一时刻,只能有一个进程使用一个进程号,当一个进程终止后,该进程号就可以再次被其他进程使用。
接下来,再给大家介绍三个不同的进程号。
- 进程号(PID):标识进程的一个非负整型数。
- 父进程号(PPID):任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。例如A 进程创建了 B 进程,A 的进程号就是 B 进程的父进程号。
- 进程组号(PGID):进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。这个过程有点类似于 QQ 群,组相当于 QQ 群,各个进程相当于各个好友,把各个好友都拉入这个 QQ 群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和 QQ 群号是有点区别的,默认的情况下,当前的进程号会当做当前的进程组号。
(1)getpid 函数
#include#include pid_t getpid(void); 功能: 获取本进程号(PID) 参数: 无 返回值: 本进程号(注意:该函数总是执行成功,所以返回值不需要进行错误检测)
(2)getppid函数
#include#include pid_t getppid(void); 功能: 获取调用此函数的进程的父进程号(PPID) 参数: 无 返回值: 调用此函数的进程的父进程号(PPID)
(3)getpgid函数
#include#include pid_t getpgid(pid_t pid); 功能: 获取进程组号(PGID) 参数: pid:进程号 返回值: 参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
示例程序:
// test.c #include#include #include // 获取进程号、父进程号、进程组号 int main() { pid_t pid, ppid, pgid; pid = getpid(); printf("pid = %dn", pid); ppid = getppid(); printf("ppid = %dn", ppid); pgid = getpgid(pid); printf("pgid = %dn", pgid); return 0; }
yxm@192:~$ gcc test.c -o test yxm@192:~$ ./test pid = 27095 ppid = 26906 pgid = 27095二、进程创建
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
进程创建 API#include#include pid_t fork(void); 功能: 用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。 参数: 无 返回值: 成功:本函数返回值会返回两次,一次是子进程中返回 0,一次是父进程中返回子进程 ID。 失败:父进程中返回-1。失败的两个主要原因是: 1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。 2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
示例代码:
#include#include #include int main() { fork(); printf("id ==== %dn", getpid()); // 获取进程号 return 0; }
运行结果如下:
yxm@192:~$ gcc test.c -o test yxm@192:~$ ./test id ==== 27332 # 父进程的进程号 id ==== 27333 # 子进程的进程号
从运行结果,我们可以看出,fork() 之后的打印函数打印了两次,而且打印了两个进程号,这说明,fork() 之后确实创建了一个新的进程,新进程为子进程,原来的进程为父进程。
父子进程关系上一小节中,我们使用 fork() 函数得到的子进程实际上是父进程的一个复制品,它从父进程处继承了整个进程的地址空间:包括进程上下文(进程执行活动全过程的静态描述)、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。
子进程所独有的只有它的进程号,计时器等(只有小量信息)。因此,使用 fork() 函数的代价是很大的。
-
简单来说, 一个进程调用 fork() 函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
-
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 技术来实现克隆自己的。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核一开始并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享——写时拷贝,读时共享。
-
【补充】fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件文件偏移指针。
子进程是父进程的一个复制品,可以简单认为父子进程的代码一样的。
那大家想过没有,这样的话,父进程做了什么事情,子进程也做什么事情(如上面的例子),是不是不能实现满足我们实现多任务的要求呀(多任务一般是父进程做一件事,子进程做另外一件事,从而实现并发)?
实际上可以通过 fork() 的返回值区别父子进程:fork() 函数被调用一次,但返回两次。两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新子进程的进程 ID。
测试程序如下:
#include#include #include int main() { pid_t pid; pid = fork(); if (pid < 0) { // 没有创建成功 perror("fork"); return 0; } if (0 == pid) { // 子进程 while (1) { printf("I am sonn"); sleep(1); } } else if (pid > 0) { // 父进程 while (1) { printf("I am fathern"); sleep(1); } } return 0; }
#include#include #include int main() { pid_t pid; pid = fork(); if (pid < 0) { // 没有创建成功 perror("fork"); return 0; } if (0 == pid) { // 子进程 while (1) { printf("I am sonn"); sleep(1); } } else if (pid > 0) { // 父进程 while (1) { printf("I am fathern"); sleep(1); } } return 0; }
yxm@192:~$ gcc test.c -o test yxm@192:~$ ./test I am father I am son I am father I am son ... ^C yxm@192:~$
运行结果如下:通过运行结果,可以看到,父子进程各做一件事(各自打印一句话)。这里,我们只是看到只有一份代码,实际上,fork() 以后,有两个地址空间在独立运行着,有点类似于有两个独立的程序(父子进程)在运行。
-
一般来说,在 fork() 之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。
-
需要注意的是,在子进程的地址空间里,子进程是从 fork() 这个函数后才开始执行代码。
-
父子进程地址空间:父子进程各自的地址空间是独立的,也包括堆区、栈区和全局数据区。只不过在运行的时候,会根据 fork() 函数的返回值不一样而调用不同的代码段。
-
【注意】子进程还可以创建新的子进程,形成进程树结构模型:因为子进程是从 fork() 这个函数后才开始执行代码,所以在连续创建子进程或者循环创建子循环时候需要小心,否则创建的子进程数量可能远远超过预想的子进程数量,如下所示:
#include
#include // 想要连续创建两个进程 int main() { pid_t pid; // 连续创建两个子进程 fork(); fork(); return 0; } 上例中,想要连续创建两个子进程,但实际上创建了三个子进程,因为第一个 fork() 之后创建了一个子进程,而该子进程从第一个 fork() 函数后才开始执行代码,所以会执行第二个 fork() 创建了一个孙进程,再加上父进程执行第二个 fork() 函数又会创建新的子进程,一共创建了三个进程
既然可以创建进程,那如何结束一个进程呢?可以使用一下 API 函数:
#includevoid exit(int status); #include void _exit(int status); 功能: 结束调用此函数的进程。 参数: status:进程退出时的一个状态信息,父进程回收子进程资源的时候可以获取到(在父子进程中,如果子进程退出 了,exit 就能得到子进程退出的状态)。 返回值: 无
exit() 和 exit() 函数功能和用法是一样的,但还是有两点区别:
- 使用时,所包含的头文件不一样;
- exit() 属于标准库函数(标准 c 库中的函数),_exit() 属于系统调用函数(linux 系统中的函数)。由于 exit() 底层会调用 _exit() 函数,其在调用_exit() 函数之前,会做一些安全处理,所以使用 exit() 相对于直接使用 _exit() 更加安全。
exit()示例:
#include#include int main() { printf("hellon"); printf("world"); exit(0); // 等价于return 0; return 0; }
yxm@192:~$ gcc test.c -o test yxm@192:~$ ./test hello worldyxm@192:~$
_exit()示例:
#include#include int main() { printf("hellon"); printf("world"); _exit(0); return 0; }
worldyxm@192:~$ gcc test.c -o test yxm@192:~$ ./test hello yxm@192:~$
exit() 与 _exit() 的运行结果如下:
- 第一个 printf() 中有 ‘n’ 换行,带上换行之后,printf() 内部会自动实现刷新缓冲区的功能,所以 exit()示例与 _exit()示例中的 ‘hello’ 都被打印出来了;
- 第二个 printf() 中没有 ‘n’ 换行,且 _exit() 的内部没有刷新缓冲区,所以 _exit()示例中的 ‘world’ 遗留在缓冲区,在还没来得及输出到标准输出文件(stdout)的情况系,程序就已经结束, ‘world’ 没有来得及打印。
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要是进程控制块 PCB 的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用 wait() 或 waitpid() 等到它的子进程退出状态同时彻底清除掉这个进程。所以子进程运行结束或者进程退出时,父进程有义务回收子进程的资源。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束,下文会详细介绍。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
进程回收 API(1)wait函数
#include#include pid_t wait(int *status); 功能: 等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。 参数: status : 进程退出时的状态信息,本参数是传出参数。 返回值: 成功:已经结束子进程的进程号 失败: 返回 -1,失败的原因: 1、没有任何有; 2、所有的子进程都已结束; 3、函数调用函数失败。
-
调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。
-
若调用进程没有子进程,函数立刻返回,返回-1;若它的子进程已经结束,也会立即返回,返回-1。
-
如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。退出信息相关宏函数可分为如下三组:
-
WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态
-
WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
-
WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 如上宏为真 → 进程暂停后已经继续运行
-
示例:
#include#include #include #include #include #include int main() { int status = 0; int i = 0; int ret = -1; pid_t pid = -1; // 创建子进程 pid = fork(); if (pid < 0) // 没有创建成功 { perror("fork"); return -1; } if (0 == pid) // 子进程 { for (i =0; i < 5; i++) { printf("child process do thing %dn", i + 1); sleep(1); } exit(10); //子进程终止 } // 父进程执行 printf("父进程等待子进程退出,回收其资源n"); ret = wait(&status); // 父进程在此处会阻塞,等待子进程退出,返回值为exit函数的参数 if (-1 == ret) { perror("wait"); return -1; } printf("父进程回收了子进程资源n"); if (WIFEXITED(status)) //属于正常退出 { printf("子进程退出状态码:%dn", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) //属于异常终止退出 { printf("子进程被信号%d杀死了...n", WTERMSIG(status)); } else if (WIFSTOPPED (status)) //属于进程暂停 { printf("子进程被信号%d暂停...n", WSTOPSIG(status ) ); } return 0; }
yxm@192:~$ gcc test.c -o test yxm@192:~$ ./test 父进程等待子进程退出,回收其资源 child process do thing 1 child process do thing 2 child process do thing 3 child process do thing 4 child process do thing 5 父进程回收了子进程资源 子进程退出状态码:10
(2)waitpid函数
#include#include pid_t waitpid(pid_t pid, int *status, int options); 功能: 等待指定进程号终止,如果子进程终止了,此函数会回收子进程的资源,可以设置是否阻塞 参数: pid : 参数 pid 的值有以下几种类型: pid > 0 某个子进程的进程号,相当于等待并回收指定子进程。 pid = 0 等待并回收同一个进程组中的任何子进程,如果子进程已加入了别的进程组,waitpid 不会等待它。 pid = -1 等待并回收任一子进程,此时 waitpid 和 wait 作用一样(最常用)。 pid < -1 等待并回收指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。 status : 进程退出时的状态信息。和 wait() 用法一样。 options : options 提供了一些额外的选项来控制 waitpid()。 0: 同 wait() 一样,阻塞父进程,等待子进程退出。 WNOHANG:非阻塞。 WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。 (由于涉及到一些跟踪调试方面的知识,加之极少用到) 返回值: waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况: 1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号,则返回 > 0; 2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0; 3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在, 如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程, waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;
示例:
#include#include #include #include #include int main() { // 有一个父进程,创建5个子进程(兄弟) pid_t pid; // 创建5个子进程 for(int i = 0; i < 5; i++) { pid = fork(); if(pid == 0) // 子进程还会创建新的子进程,所以需要判断语句以保证只会创建 5 个子进程 { break; } } if(pid > 0) { // 父进程 while(1) { printf("parent, pid = %dn", getpid()); sleep(1); int st; // int ret = waitpid(-1, &st, 0); int ret = waitpid(-1, &st, WNOHANG); if(ret == -1) { break; } else if(ret == 0) { // 说明还有子进程存在 continue; } else if(ret > 0) { if(WIFEXITED(st)) { // 是不是正常退出 printf("退出的状态码:%dn", WEXITSTATUS(st)); } if(WIFSIGNALED(st)) { // 是不是异常终止 printf("被哪个信号干掉了:%dn", WTERMSIG(st)); } printf("child die, pid = %dn", ret); } } } else if (pid == 0) { // 子进程 while(1) { printf("child, pid = %dn",getpid()); sleep(1); } exit(0); } return 0; }
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程
本文介绍一些进程管理方面的知识点,包括进程信息的查询、进程的创建、进程的退出和进程资源回收的回收,同时还补充了一些相关的实例,篇幅不长,但实际上内容更多是 API 的使用总结,可以起到查缺补漏和知识字典的作用。