在实际开发中难免会处理进程间的关系,最常见的是父子进程的相互监督。父进程等待子进程,或者自进程知道父进程运行是否结束,以方便释放资源。
一、关于进程
进程是操作系统进行资源分配和调度的基本单位。linux系统使用fork创建进程,进程pid 0是swapper进程,进程pid 1是init进程,init进程是所有普通用户进程的父进程。 fork在 <unistd.h>文件中定义如下: pid_t fork(void); 当fork成功时会返回两次,一次返回给父进程,一次返回给子进程。 如果fork调用成功,返回pid > 0 给父进程,返回pid == 0给子进程。 如果fork调用失败,返回pid == -1给父进程,并且设置errno。二、父进程与子进程的关系
通过fork后,为什么父进程返回pid > 0,而子进程返回pid == 0呢?这是因为父进程无法知道自己有多少子进程,子进程创建后就独立于父进程了,无法知道子进程的id。子进程能够通过getppid获取父进程的pid。当父进程比子进程提前退出时,子进程会成为孤儿进程,系统会将重置孤儿进程的父进程到init进程ppid == 1。如果子进程提前退出并且父进程没有使用wait函数监听处理结束的子进程,那么该子进程会成为僵尸进程,即系统认为该进程存在,它占用的进程信息如进程pid等,会导致系统无法重复利用该pid号,可以这么理解僵尸进程就是名存实亡的进程,系统认为进程还存在,而子进程已经完成他的运行任务了。三、简单的进程例程
simple_process.cpp#include <cstring>#include <cstdio>#include <cstdlib>#include <unistd.h>#include <errno.h>#include <sys/types.h>#include <sys/wait.h>int main(int argc, char *argv[]) {printf("start program %s, pid: %ld, ppid: %ld \n", argv[0], getpid(), getppid());pid_t pid = fork();if (-1 == pid) {printf("fork process failed. errno: %u, error: %s\n", errno, strerror(errno));exit(-1);} if (pid > 0) { // parent printf("parent precess\n");sleep(1);/** 标记一、wait 等待子进程结束*///int status;//wait(&status);} else { // child printf("child process, pid: %ld, ppid: %ld\n", getpid(), getppid());for (int i = 0; i < 5; i++) {printf("child sleep %ds\n", i); sleep(1);} printf("##child process, pid: %ld, ppid: %ld\n", getpid(), getppid());} return 0;}
编译运行 $ g++ simple_process.cpp $ ./a.out start program ./a.out, pid: 17164, ppid: 8564 parent precess child process, pid: 17165, ppid: 17164 child sleep 0s [sunny@icentos process-wait]$ child sleep 1s child sleep 2s child sleep 3s child sleep 4s ##child process, pid: 17165, ppid: 1 可以看到,当父进程提前结束时,子进程的父进臣变化为init进程,ppid == 1。
四、实现父进程监听子进程运行结束
如果我们把“标志一”的wait代码删除注释, 即 使用下面代码/** 标记一、wait 等待子进程结束 */int status;wait(&status);
编译 $ g++ simple_process.cpp $ ./a.out start program ./a.out, pid: 28989, ppid: 8564 parent precess child process, pid: 28990, ppid: 28989 child sleep 0s child sleep 1s child sleep 2s child sleep 3s child sleep 4s ##child process, pid: 28990, ppid: 28989 因为wait函数的作用是等待子进程结束,并获取子进程结束的状态,所以父进程wait等待子进程结束后才继续运行退出,故自进程的ppid一直是原来fork父进程的pid。因为wait函数是阻塞的,所以在实际开发中,需要监听子进程退出,可以新建线程来监听,并通过回调函数处理。
五、实现子进程监听父进程运行结束
fork创建进程以后子进程就成为独立的运行单位(进程是操作系统资源分配和调度的基本单位),虽然子进程会继承父进程原来的变量,但是子进程对这些变量的操作不会影响相应父进程变量的值等(这里只是指简单的int,long变量,如果是socket,file等共享资源那么还可能会影响)。子进程已经无法通过普通方式知道父进程的运行状态(正在运行还是运行结束,进程是否存活)。 子进程要知道父进程的状态只能通过进程通讯方法解决,常用的进程间通讯:匿名管道,有名管道,信号量,消息队列,信号,共享内存,套接字。 本文将介绍使用套接字实现父进程结束自动结束子进程的方式,主要运用两个特性。 1)socketpair会产生一个双向的句柄,两个句柄相当于通讯两端,程序能够通过一端写入,然后再另一端句柄读取内容。 2)进程运行退出时系统会自动回收资源,关闭该进程所占有的文件句柄,套接字句柄也是一种句柄。 代码实现#include <cstdio>#include <cstdlib>#include <cstring>#include <errno.h>#include <pthread.h>#include <sys/socket.h>#include <sys/types.h>#include <unistd.h>#include <fcntl.h>void * thr_child(void *arg) {int sock = *((int*)arg);char buf[2] = {0};ssize_t len = 0;while (-1 == (len = read(sock, buf, sizeof(buf))) && EINTR == errno);printf("thr_child. pid: %ld, ppid:%ld\n", getpid(), getppid());exit(-1 == len ? -1 : 0);}int main(int argc, char *argv[]) {printf("start program: %s, pid: %ld, ppid: %ld\n", argv[0], getpid(), getppid());int fd[2] = {0};if (-1 == socketpair(PF_LOCAL, SOCK_STREAM, 0, fd)) {perror("socketpair failed\n");exit(-1);}pid_t pid = fork();if (-1 == pid) {perror("fork failed\n");exit(-1);}if (0 == pid) { // childprintf("child process, pid: %ld, ppid: %ld\n", getpid(), getppid());close(fd[0]);int sock = fd[1];pthread_t pid;if (pthread_create(&pid, NULL, thr_child, (void *)&sock)) {perror("child. create thread failed\n");exit(-1);}for (int i = 0; i < 20; i++) {sleep(1);printf("child sleep: %ds\n", i + 1);}} else { // parentprintf("parent process, pid: %ld, ppid: %ld\n", getpid(), getppid());close(fd[1]);int sock = fd[0];for (int i = 0; i < 5; i++) {sleep(1);printf("parent sleep: %ds\n", i + 1);}}printf("process %s finish\n", pid > 0 ? "parent" : "child");return 0;}
编译及运行 $ g++ -pthread notify_parent_quit.cpp $ ./a.out start program: ./a.out, pid: 31141, ppid: 8564 parent process, pid: 31141, ppid: 8564 child process, pid: 31142, ppid: 31141 parent sleep: 1s child sleep: 1s parent sleep: 2s child sleep: 2s parent sleep: 3s child sleep: 3s parent sleep: 4s child sleep: 4s parent sleep: 5s process parent finish child sleep: 5s thr_child. pid: 31142, ppid:1 可以看到,如果使用wait函数,那么父进程会等待子进程sleep 20秒再自行结束。但是因为父进程结束时会关闭套接字句柄sock导致子进程read返回0表示达到文件结尾,因为父进程没有主动close套接字句柄sock,所以只有父进程退出时由操作系统关闭该句柄。上面的notify程序就是运用这点,在父进程结束时通知子进程的。通过线程函数thr_child可以知道,在线程退出前打印ppid时,子进程的父进程ppid == 1,即由于父进程结束导致子进程成为孤儿进程。 总结、socketpair只是其中一种简单的实现方式。这里只是使用简单的例子说明,在实际应用中处理的情况会比较复杂。