引用:之前的文章介绍了 Linux 下进程相关的基础概念和一些常见的 shell 命令,本文则延续之前的思路——继续 Linux 下进程知识的学习与总结,介绍 Linux 下进程管理相关的 API,包括进程创建、进程信息查询、进程退出和进程回收。
每个进程都由一个进程号来标识,其类型为 pid_t,进程号的类型—— pid_t 其实为一个短整形,所以pid_t能表示的范围是:0~32767。进程号总是唯一的,但进程号可以重用:同一时刻,只能有一个进程使用一个进程号,当一个进程终止后,该进程号就可以再次被其他进程使用。

接下来,再给大家介绍三个不同的进程号。
(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 = %d\n", pid);
ppid = getppid();
printf("ppid = %d\n", ppid);
pgid = getpgid(pid);
printf("pgid = %d\n", pgid);
return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
pid = 27095
ppid = 26906
pgid = 27095
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
#include
#include
pid_t fork(void);
功能:
用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
无
返回值:
成功:本函数返回值会返回两次,一次是子进程中返回 0,一次是父进程中返回子进程 ID。
失败:父进程中返回-1。失败的两个主要原因是:
1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
示例代码:
#include
#include
#include
int main()
{
fork();
printf("id ==== %d\n", 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 son\n");
sleep(1);
}
}
else if (pid > 0)
{ // 父进程
while (1)
{
printf("I am father\n");
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 son\n");
sleep(1);
}
}
else if (pid > 0)
{ // 父进程
while (1)
{
printf("I am father\n");
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 函数:
#include
void 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("hello\n");
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("hello\n");
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调用只能清理一个子进程,清理多个子进程应使用循环。
(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 %d\n", 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("子进程退出状态码:%d\n", 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 = %d\n", 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("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st))
{
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
}
}
}
else if (pid == 0)
{
// 子进程
while(1)
{
printf("child, pid = %d\n",getpid());
sleep(1);
}
exit(0);
}
return 0;
}
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程
本文介绍一些进程管理方面的知识点,包括进程信息的查询、进程的创建、进程的退出和进程资源回收的回收,同时还补充了一些相关的实例,篇幅不长,但实际上内容更多是 API 的使用总结,可以起到查缺补漏和知识字典的作用。