在实际开发中难免会处理进程间的关系,最常见的是父子进程的相互监督。父进程等待子进程,或者自进程知道父进程运行是否结束,以方便释放资源。
一、关于进程
进程是操作系统进行资源分配和调度的基本单位。linux系统使用fork创建进程,进程pid 0是swapper进程,进程pid 1是init进程,init进程是所有普通用户进程的父进程。
fork在 文件中定义如下:
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
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
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) { // child
printf("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 { // parent
printf("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只是其中一种简单的实现方式。这里只是使用简单的例子说明,在实际应用中处理的情况会比较复杂。