进程是linux操作系统的环境的基础,它控制着系统上几乎所有的活动。关于进程编程内容如下:
1).复制进程映像的fork系统调用和替换进程映像的exec系列系统调用。
2).僵尸进程以及如何避免僵尸进程。
3).进程间通信。
4).三种system v进程通信方式:信号量、消息队列和共享内存。
5).在进程间传递文件描述符的通用办法:通过unix本地域socket传递特殊的辅助数据。
这里,我们先讲如何用一个进程创建出另一个进程。
1.fork系统调用
pid_t fork(void);
该函数返回两次,父进程中返回子进程PID,子进程中返回0,所以返回值是后续代码判断当前进程是父进程还是子进程的依据。fork调用失败返回-1,并设置errno。fork函数复制当前进程,在内核进程表中创建一个新的进程表象。新的进程表象有很多属性和原进程相同,比对堆指针、栈指针和表示寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置为原进程的PID,信号位被清除(原进程设置的信号处理不再对新进程起作用)。子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是所谓的写时复制(copy on writte),即只有在任一进程(父进程或者子进程)对数据执行了写操作时,复制才会发生(先缺页也中断,然后操作系统给子进程分配内存并复制赋值父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应当十分谨慎,尽量避免没必要的内存分配和数据复制。创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1,不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
1 #include <sys/types.h> 2 #include <sys/types.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <errno.h> 8 9 10 int main(void) 11 { 12 pid_t pid = fork(); 13 if(pid > 0) 14 { 15 printf("I am parents process, pid %d, my child is %d ", getpid(), pid); 16 } 17 else if(pid == 0) 18 { 19 printf("I am child process, pid %d, my parent is %d ", getpid(), getppid()); 20 } 21 else 22 { 23 printf("fork failed, errno %d ", errno); 24 } 25 }
Linux环境下编译并执行
ydq@docsis4 chapter13 $ gcc fork.c -o fork
ydq@docsis4 chapter13 $ ./fork
I am parents process, pid 26293, my child is 26294
I am child process, pid 26294, my parent is 26293
例子上我们还调用了getpid()和getppid()来分别获取自己的进程号和父进程的进程号来验证fork的返回值情况,确认得到pid>0时,对应的代码是父进程跑的代码;pid=0时,对应的代码是子进程跑的代码。
2.exec系列系统调用
有时我们需要在子进程中执行其他可执行程序,即替换当前进程映像,这就需要使用如下exec系列函数之一。
#include <unistd.h> extern char **environ; int execl(const char *pathname, const char *arg, .../* (char *) NULL */); int execlp(const char *file, const char *arg, .../* (char *) NULL */); int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char * const envp[] */); int execv(const char *pathname, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
path参数指定可执行文件的完整路径,file参数可以接受文件名,该文件的具体则在环境变量PATH中搜寻。arg接受可变参数,argv则接受参数数组,它们都会被传递给新程序(path或file指定的程序)的main函数。envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量。
一般情况下,exec函数是不返回的,除非出错。出错返回-1,并设置errno。如果没出错,则原程序中exec调用之后的代码都不会被执行,因为此时原程序已经被exec的参数指定的程序完全替换(包括代码和数据)。
exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC属性。
我们拿第一个函数execl来写一段测试代码。
ydq@docsis4 chapter13 $ vi execl.c
1 #include <sys/types.h> 2 #include <sys/types.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <errno.h> 8 9 10 int main(void) 11 { 12 pid_t pid = fork(); 13 if(pid > 0) 14 { 15 printf("I am parents process, pid %d, my child is %d ", getpid(), pid); 16 usleep(5000);//确保父进程比子进程晚退出,这样子进程才不会被init进程接手 17 } 18 else if(pid == 0) 19 { 20 printf("I am child process, pid %d, my parent is %d ", getpid(), getppid()); 21 if(execl("/home/ydq/code/high_performance/chapter13/child", "child", (char * )0) == -1) 22 { 23 printf("execl failed, errno %s ", strerror(errno)); 24 return -1; 25 } 26 printf("hello world! ");//在execl调用后才执行的打印语句,不会被调用到. 27 } 28 else 29 { 30 printf("fork failed, errno %d ", errno); 31 } 32 33 }
ydq@docsis4 chapter13 $ vi child.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 5 int main(int argc, char *argv[]) 6 { 7 printf("[%s]:I am child process, pid %d, my parent is %d ", argv[0],getpid(), getppid()); 8 return 0; 9 }
ydq@docsis4 chapter13 $ gcc execl.c -o execl ; gcc child.c -o child
ydq@docsis4 chapter13 $ ./execl
I am parents process, pid 12958, my child is 12959
I am child process, pid 12959, my parent is 12958
[child]:I am child process, pid 12959, my parent is 12958
ydq@docsis4 chapter13 $
验证成功,说明上述的说法是正确的。
3.处理僵尸进程
对于多进程程序,父进程一般需要跟踪子进程的退出转态。因此,当子进程结束运行时,内核不会立即释放改进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(父进程必须还在运行)。在子进程结束运行之后,父进程读取器退出状态之前,我们成该子进程为僵尸态。另外一种是父进程结束或者异常终止,而子进程继续运行,此时子进程被init进程接管,并等待它退出。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
下面,我们编写一个例子来测试子进程进入僵尸态的情况。
ydq@docsis4 chapter13 $ vi zombie_child.c
1 #include <sys/types.h> 2 #include <sys/types.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <errno.h> 8 9 10 int main(void) 11 { 12 pid_t pid = fork(); 13 if(pid > 0) 14 { 15 printf("I am parents process, pid %d, my child is %d ", getpid(), pid); 16 sleep(30);//为了让父进程比子进程晚退出和有足够时间去查看进程状态,睡眠30s 17 } 18 else if(pid == 0) 19 { 20 printf("I am child process, pid %d, my parent is %d ", getpid(), getppid()); 21 printf("child process is finished "); 22 } 23 else 24 { 25 printf("fork failed, errno %d ", errno); 26 } 27 28 }
ydq@docsis4 chapter13 $ gcc zombie_child.c -o zombie_child
ydq@docsis4 chapter13 $ ./zombie_child & -- 执行时加&,让进程在后台执行
[1] 30061
ydq@docsis4 chapter13 $ I am parents process, pid 30061, my child is 30062
I am child process, pid 30062, my parent is 30061
child process is finished
ydq@docsis4 chapter13 $ ps -as | grep zom
1002 30061 0000000000000000 0000000000000000 0000000000000000 0000000000000000 S pts/7 0:00 ./zombie_child
1002 30062 0000000000000000 0000000000000000 0000000000000000 0000000000000000 Z pts/7 0:00 [zombie_child] <defunct>
1002 30157 0000000000000000 0000000000000000 0000000000000000 0000000180000000 S+ pts/7 0:00 grep --color=auto zom
我们通过ps命令的输出,可以获知子进程已经进入Z状态(僵尸态)。
父进程没有正确地处理子进程的返回信息,子进程都将停留在僵尸态,并占据着内核资源,这是必须避免的,我们可以在父进程中调用下面这对函数,等待子进程结束并获取子进程返回信息,从而避免僵尸进程的产生,或者使子进程的僵尸态立即结束。
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus); pid_t waitpid(pid_t pid, int *wstatus, int options);
wait函数将阻塞进程,知道该进程的某个子进程结束运行位置。返回值为该结束的子进程PID,并将子进程的退出转态信息存储于wstatus指向的内存中。我们可以通过sys/wait.h定义的宏来帮助解释子进程的退出状态,如下所示:
宏 含义
WIFEXITED(wstatus) 如果子进程正常结束,它就返回一个非0值
WEXITSTATUS(wstatus) 如果WIFEXITED非0,它返回子进程的退出码
WIFSIGNALED(wstatus) 如果子进程是因为一个未捕获的信号而终止,它就返回一个非0值
WTERMSIG(wstatus) 如果WIFSIGNALED非0,它返回终止子进程的信号
WIFSTOPPED(wstatus) 如果子进程是因为一个未捕获的信号而暂停,它就返回一个非0值
WSTOPSIG(wstatus) 如果WIFSTOPPED非0,它就返回一个信号值
wait函数的阻塞特性显然不是服务器程序期望的,因为服务器一般都是异步的(关于异步,请大家自行学习),而waitpid函数提供了非阻塞特性。
waitpid参数和返回值表示意义如下
参数pid:pid参数有四种取值情况,如下:
<-1 代表等待进程组ID等于pid绝对值的任何子进程。
-1 代表等待所有子进程
0 代表等待进程组ID等于调用的进程的进程组ID的任何子进程。
>0 代表等待进程ID等于pid值的子进程。
参数wstatus:存放子进程退出状态信息的内存地址。
参数options:控制waitpid的行为,有以下选项
WNOHANG 如果没有子进程退出,立即返回
WUNTRACED 如果子线程已经停止也返回。即使未指定此选项,也会提供已停止跟踪的子进程的状态。
WCONTINUED 如果停止的子进程被信号SIGCONT.恢复了,也返回 (since Linux 2.6.10)。
下面我们用代码测试一下。
ydq@docsis4 chapter13 $ vi handler_child.c
1 #include <sys/types.h> 2 #include <signal.h> 3 #include <sys/types.h> 4 #include <unistd.h> 5 #include <string.h> 6 #include <sys/wait.h> 7 #include <stdio.h> 8 #include <assert.h> 9 #include <stdlib.h> 10 11 12 typedef struct signal_info_s 13 { 14 int sig; //要捕获的信号 15 void (*sig_handler)(int); //捕获信号后的执行函数 16 }signal_info_t; 17 18 19 //捕获SIGCHLD信号后执行的函数,用来调用waitpid处理子进程退出状态信息 20 static void handler_child(int sig) 21 { 22 pid_t pid; 23 int stat; 24 while((pid = waitpid(-1, &stat, WNOHANG|WUNTRACED )) > 0) 25 { 26 if(WIFEXITED(stat)) 27 printf("child(pid %d) process exits normally, exit value %d ", pid, WEXITSTATUS(stat)); 28 else if(WIFSIGNALED(stat)) 29 printf("child(pid %d) process is terminated by a signal %d ", pid, WTERMSIG(stat)); 30 else if(WIFSTOPPED(stat)) 31 printf("child(pid %d) process is stopped by a signal %d ", pid, WSTOPSIG(stat)); 32 else 33 printf("others "); 34 } 35 } 36 37 38 //初始化信号捕获函数 39 void child_signal_init(signal_info_t sig_info) 40 { 41 struct sigaction sa; 42 memset(&sa, 0, sizeof(sa)); 43 sa.sa_handler = sig_info.sig_handler; 44 sa.sa_flags |= SA_RESTART; 45 sigfillset(&sa.sa_mask); 46 assert(sigaction(sig_info.sig, &sa, NULL) != -1); 47 } 48 49 50 int main(void) 51 { 52 pid_t pid = fork(); 53 if(pid > 0) 54 { 55 printf("I am parents process, pid %d ", getpid()); 56 signal_info_t sig_info; 57 sig_info.sig = SIGCHLD; 58 sig_info.sig_handler = handler_child; 59 child_signal_init(sig_info); 60 while(1) //父进程死循环,方便看测试结果 61 { 62 } 63 } 64 else 65 { 66 printf("I am child process, pid %d ", getpid()); 67 #ifdef D_SIGTERM //测试被信号SIGTERM终止的情况,因为我们没有去捕获信号SIGTERM,所以当我们去访问非法地址时,子进程会崩溃。 68 int *pointer = 0; 69 *pointer = 100; 70 #elif D_SIGSTOP //测试子进程被停止的情况,我们可以通过在终端给子进程发送SIGSTOP信号 71 while(1) 72 { 73 74 } 75 #elif D_EXIT //测试正常退出的情况 76 exit(100); 77 #endif 78 } 79 }
ydq@docsis4 chapter13 $ gcc handler_child.c -o handler_child -DD_SIGSTOP ,编译加-DXXX,表示编译后生成的可执行程序带有XXX的宏定义。
ydq@docsis4 chapter13 $ ./handler_child
I am parents process, pid 17214
I am child process, pid 17215
我们执行后,发现父子进程各打印了一条log信息,我们再开启一个终端来查看一下它们的进程状态。
ydq@docsis4 bin $ ps -as | grep handler
1002 17214 0000000000000000 0000000000000000 0000000000000000 0000000000010000 R+ pts/7 4:26 ./handler_child
1002 17215 0000000000000000 0000000000000000 0000000000000000 0000000000000000 R+ pts/7 4:26 ./handler_child
1002 17494 0000000000000000 0000000000000000 0000000000000000 0000000180000000 S+ pts/3 0:00 grep --color=auto handler
我们发现父子进程的状态都是R+(R (TASK_RUNNING),可执行状态。),这时候我们向子进程(pid17215)发送信号SIGSTOP(信号19,可通过kill -l命令查询得到),再来查看其状态,执行如下命令。
ydq@docsis4 bin $ kill -19 17215
ydq@docsis4 bin $ ps -as | grep handler
1002 17214 0000000000000000 0000000000000000 0000000000000000 0000000000010000 R+ pts/7 9:39 ./handler_child
1002 17215 0000000000000000 0000000000000000 0000000000000000 0000000000000000 T+ pts/7 9:35 ./handler_child
1002 17794 0000000000000000 0000000000000000 0000000000000000 0000000180000000 S+ pts/3 0:00 grep --color=auto handler
我们发现子进程接收到信号SIGSTOP后,由于没有注册捕获行为,所以系统默认执行信号SIGSTOP的缺省行为--暂停子进程,所以我们在ps输出可以看到子进程已经进入T+状态(T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态。),由于子进程由转态执行状态转化为停止转态,所以父进程接收到信号SIGCHLD,并且由于我们注册了信号SIGCHLD的捕获函数,所以我们可以原来执行handler_child的终端看到如下打印语句。
child(pid 17215) process is stopped by a signal 19
至此我们已经把测试子进程状态变换为停止的情况,从结果看出waitpid的行为,至于其他的子进程退出状态的测试,读着可以自行编译并运行,再测试,再三强调,最后的例子一定要熟悉,这是多进程网络服务器处理子进程退出状态的基础,希望能够理解。
现在,我们已经把进程如何创建,如何替换可执行文件,如何避免进入僵尸态都讲解完毕,这些内容使我们多进程编程的基础,是我们以后设计多进程程序的重中之重,望熟悉。
总结不易,转载请标明原链接和作者。
博客园 - ydqun