一个进程可以通过 fork()或 vfork()等系统调用创建一个子进程,一个新的进程就此诞生!事实上,Linux系统下的所有进程都是由其父进程创建而来,譬如在 shell 终端通过命令的方式执行一个程序./app,那么 app进程就是由 shell 终端进程创建出来的,shell 终端就是该进程的父进程。
既然所有进程都是由其父进程创建出来的,那么总有一个最原始的父进程吧,否则其它进程是怎么创建出来的呢?确实如此,在 Ubuntu 系统下使用"ps -aux"命令可以查看到系统下所有进程信息,如下:
上图中进程号为 1 的进程便是所有进程的父进程,通常称为 init 进程,它是 Linux 系统启动之后运行的第一个进程,它管理着系统上所有其它进程,init 进程是由内核启动,因此理论上说它没有父进程。init 进程的 PID 总是为 1,它是所有子进程的父进程,一切从 1 开始、一切从 init 进程开始!
一个进程的生命周期便是从创建开始直至其终止。
通常,进程有两种终止方式:异常终止和正常终止。
正常终止 | main 函数中使用 return 返回、调用 exit()函数结束进程、调用_exit()或_Exit()函数结束进程等 |
---|---|
异常终止 | 在程序当中调用 abort()函数异常终止进程、当进程接收到某些信号导致异常终止等 |
_exit()函数和 exit()函数的 status 参数定义了进程的终止状态(termination status),父进程可以调用 wait()函数以获取该状态。虽然参数 status 定义为 int 类型,但仅有低 8 位表示它的终止状态,一般来说,终止状态为 0 表示进程成功终止,而非 0 值则表示进程在执行过程中出现了一些错误而终止,譬如文件打开失败、读写失败等等,对非 0 返回值的解析并无定例。
在我们的程序当中,一般使用 exit()库函数而非_exit()系统调用,原因在于 exit()最终也会通过_exit()终止进程,但在此之前,它将会完成一些其它的工作,exit()函数会执行的动作如下:
所以,由此可知,exit()函数会比_exit()会多做一些事情,包括执行终止处理函数、刷新 stdio 流缓冲以及调用_exit(),在前面曾提到过,在我们的程序当中,父、子进程不应都使用 exit()终止,只能有一个进程使用 exit()、而另一个则使用_exit()退出,当然一般推荐的是子进程使用_exit()退出、而父进程则使用 exit()退出。其原因就在于调用 exit()函数终止进程时会刷新进程的 stdio 缓冲区。接下来我们便通过一个示例代码进行说明:
#include
#include
#include
int main(void)
{printf("Hello World!\n");switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */exit(0);default:/* 父进程 */exit(0);}}
打印结果确实如我们所料,接下来将代码进行简单地修改,把 printf()打印的字符串最后面的换行符\n去掉,如下所示:
#include
#include
#include
int main(void)
{printf("Hello World!");switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */exit(0);default:/* 父进程 */exit(0);}
}
从打印结果可知,"Hello World!"被打印了两次,这是怎么回事呢?在程序当中明明只使用了 printf 打印了一次字符串。要解释这个问题,首先要知道,进程的用户空间内存中维护了 stdio 缓冲区,通过 fork()创建子进程时会复制这些缓冲区。标准输出设备默认使用的是行缓冲,当检测到换行符\n 时会立即显示函数 printf()输出的字符串,在第一次实例代码中 printf 输出的字符串中包含了换行符,所以会立即读走缓冲区中的数据并显示,读走之后此时缓冲区就空了,子进程虽然拷贝了父进程的缓冲区,但是空的,虽然父、子进程使用 exit()退出时会刷新各自的缓冲区,但对于空缓冲区自然无数据可读。
但在第二次代码中,printf()并没有添加换行符\n,当调用 printf()时并不会立即读取缓冲区中的数据进行显示,由此 fork()之后创建的子进程也自然拷贝了缓冲区的数据,当它们调用 exit()函数时,都会刷新各自的缓冲区、显示字符串,所以就会看到打印出了两次相同的字符串。
tips:这个结果,可以当一个结论吧,这也许可能会影响到后续编程时,出现的一些看不出问题的因素。
所以,可以采用以下任一方法来避免重复的输出结果:
在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视。
所以本小结,多来探讨一下通过前面提到的wait()【监视子进程状态的函数】,来观察进程的变化。
对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:
#include
#include pid_t wait(int *status);
函数参数和返回值含义如下:
status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。系统调用 wait()将执行如下动作:
参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status信号参数:
#include
#include
#include
#include
#include
#include
int main(void)
{int status;int ret;int i;/* 循环创建 3 个子进程 */for (i = 1; i <= 3; i++) {switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */printf("子进程<%d>被创建\n", getpid());sleep(i);_exit(i);default:/* 父进程 */break;}}sleep(1);printf("~~~~~~~~~~~~~~\n");for (i = 1; i <= 3; i++) {ret = wait(&status);if (-1 == ret) {if (ECHILD == errno) {printf("没有需要等待回收的子进程\n");exit(0);}else {perror("wait error");exit(-1);}}printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));}exit(0);
}
示例代码中,通过 for 循环创建了 3 个子进程,父进程中循环调用 wait()函数等待回收子进程,并将本次回收的子进程进程号以及终止状态打印出来,编译测试结果如下:
使用 wait()系统调用存在着一些限制,这些限制包括如下:
为了突破这些限制,在linux中,设计了waitpid()函数;
#include
#include pid_t waitpid(pid_t pid, int *status, int options);
函数参数和返回值含义如下:
pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:
status:与 wait()函数的 status 参数意义相同。
options:下面介绍。
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍。
options:一个位掩码,可以包括 0 个或多个如下标志:
将上述wait()的代码改成waitpid()的代码:
#include
#include
#include
#include
#include
#include
int main(void)
{int status;int ret;int i;/* 循环创建 3 个子进程 */for (i = 1; i <= 3; i++) {switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */printf("子进程<%d>被创建\n", getpid());sleep(i);_exit(i);default:/* 父进程 */break;}}sleep(1);printf("~~~~~~~~~~~~~~\n");for (i = 1; i <= 3; i++) {ret = waitpid(-1, &status, 0);if (-1 == ret) {if (ECHILD == errno) {printf("没有需要等待回收的子进程\n");exit(0);}else {perror("wait error");exit(-1);}}printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));}exit(0);
}
将 wait(&status)替换成了 waitpid(-1, &status, 0),通过上面的介绍可知,waitpid()函数的这种参数配置情况与 wait()函数是完全等价的。