创建子进程
众所周知,要实现多进程,就是用fork函数来创建子进程。下面就从fork函数说起。
先从官网里找到fork函数资料,可以参考:fork()函数官方链接,里面讲得非常详细了,这里提取一些要点出来。
头文件
#include <sys/types.h>
#include <unistd.h>
函数原型
pid_t fork(void);
参数和返回值
- 当函数执行成功
父进程中,返回值是子进程的pid;
子进程中,返回值是0。 - 当函数执行失败
父进程中,返回值是-1,可以通过errno获得错误信息;
没有创建出子进程。
fork继承了什么?又没继承什么?
继承:uid,gid,euid,egid,sgid,进程组id,会话id,当前工作目录和根目录,文件描述符fd,信号屏蔽和处理,存储映射,资源限制 等。
没继承:pid,进程时间,文件锁,未处理信号 等。
僵尸进程
什么是僵尸进程?
A child that terminates, but has not been waited for becomes a “zombie”. The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child. As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes.
上面是官方解释,如果一个子进程结束,但是它的父进程没有调用wait,那么这个子进程就会变成僵尸进程。
- 如果子进程先退出,父进程还未退出,此时子进程必须等到父进程捕获到子进程的退出状态后,子进程才能真正结束,否则,此时的子进程就会变成僵尸进程。
- 如果父进程先退出,子进程还未退出,此时子进程的父进程会变为init(pid=1)进程,init进程在子进程结束时候,会默认执行wait操作,所以该子进程一定不会变成将僵尸进程。(备注:任何一个进程都必须有父进程)
如何避免僵尸进程?
第一种方法:就是在父进程用wait或者waitpid来回收子进程资源,即可避免僵尸进程。
第二种方法:子进程退出会发送SIGCHLD信号给父进程,在父进程上,可以选择忽略或者使用信号处理函数接收处理它,就可以避免僵尸进程。
为何第二种方法这样可以?
官方链接在这,截取出来如下:
POSIX.1-2001 specifies that if the disposition of SIGCHLD is set to SIG_IGN or the SA_NOCLDWAIT flag is set for SIGCHLD, then children that terminate do not become zombies and a call to wait() or waitpid() will block until all children have terminated, and then fail with errno set to ECHILD.
更具体地,linux内核版本2.6以上是如此,因为linux2.6符合POSIX标准,但是linux2.4或者之前就不是这样了,这个扯得就多了,但是我们现在开发正常内核版本都在2.6以上,所以问题也不大,这里就不详细讨论,有兴趣自己看下官方。
如下图,一般父进程在fork之前就先定义SIGCHLD的处理动作。
wait和waitpid
wait和waitpid的作用是为了获得子进程状态改变的信息,特别地,更为重要地,是为了回收子进程资源,避免僵尸进程。
子进程状态改变包含:
子进程终止(the child terminated)
子进程由于信号停止(the child was stopped by a signal)
子进程由于信号继续(the child was resumed by a signal)
当子进程结束时,父进程调用wait或者waitpid,会回收子进程的相关资源(pid,子进程结束状态status,资源使用信息)。如果没有回收,子进程就变成僵尸子进程会占用系统进程表的一个槽位,如果该表满了,就无法继续创建新的进程了。
wait函数表现:
1、当子进程还未结束,那么父进程调用wait会阻塞,直到子进程结束 或 信号处理函数中断。
2、当子进程已经结束,那么父进程调用wait会立刻返回。
头文件
#include <sys/types.h>
#include <sys/wait.h>
函数原型
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数和返回值
The value of pid can be:
- < -1 :meaning wait for any child process whose process group ID is equal to the absolute value of pid.
- -1 :meaning wait for any child process.
- 0:meaning wait for any child process whose process group ID is equal to that of the calling process at the time of the call to waitpid().
- >0:meaning wait for the child whose process ID is equal to the value of pid.
The value of options is an OR of zero or more of the following constants:
- WNOHANG:return immediately if no child has exited.
- WUNTRACED:also return if a child has stopped (but not traced via ptrace(2)).
- WCONTINUED (since Linux 2.6.10):also return if a stopped child has been resumed by delivery of SIGCONT.
备注:本来是只有子进程终止时候,waitpid才返回,上面also的意思,就是现在当子进程stop(WUNTRACED)或者resume(WCONTINUED)时,waitpid也会返回,而不是一直阻塞在那里。
wstatus 是一个入参返回值,可以指定为NULL(如果你不关心返回状态信息),也可以指定为一个整形数的地址,然后这个值可以用一些宏来读取,如:WIFEXITED(wstatus),WEXITSTATUS(wstatus)等等,具体参考:点我。
返回值
wait函数,当执行成功, 返回子进程的pid;当执行失败,返回-1。
waitpid函数,当执行成功,返回改变进程状态的子进程pid,但是当指定了WHOHANG,子进程还没退出,则返回0。当执行失败,返回-1。
错误值
特别地,当父/子进程指定了SIGCHLD信号处理动作为SIG_IGN,那么父进程上调用wait()或者waitpid()函数会在所有子进程退出后返回错误,设置errno为ECHILD。
wait和waitpid区别
waitpid比wait功能强大很多,wait只是waitpid的一个特例,如下:
wait(&wstatus); 等价于 waitpid(-1, &wstatus, 0);
看上面waitpid的pid参数就知道,waitpid不但可以回收子进程(pid=-1)资源,也可以回收指定子进程pid的进程资源(pid>0,比如fork很多次,就有很多子进程,这时候只回收其中一个子进程的情况)。
示例一
官方示例
The following program demonstrates the use of fork(2) and waitpid(). The program creates a child process. If no command-line argument is supplied to the program, then the child suspends its execution using pause(2), to allow the user to send signals to the child. Otherwise, if a command-line argument is supplied, then the child exits immediately, using the integer supplied on the command line as the exit status. The parent process executes a loop that monitors the child using waitpid(), and uses the W*() macros described above to analyze the wait status value.
// gcc -g -O0 test_fork.c -o test_fork
#include <sys/wait.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int
main(int argc, char *argv[])
{
pid_t cpid, w;
int wstatus;
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* Code executed by child */
printf("Child PID is %jd\n", (intmax_t) getpid());
if (argc == 1)
pause(); /* Wait for signals */
_exit(atoi(argv[1]));
} else { /* Code executed by parent */
do {
w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED);
if (w == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(wstatus)) {
printf("exited, status=%d\n", WEXITSTATUS(wstatus));
} else if (WIFSIGNALED(wstatus)) {
printf("killed by signal %d\n", WTERMSIG(wstatus));
} else if (WIFSTOPPED(wstatus)) {
printf("stopped by signal %d\n", WSTOPSIG(wstatus));
} else if (WIFCONTINUED(wstatus)) {
printf("continued\n");
}
} while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus));
exit(EXIT_SUCCESS);
}
}
示例二
使用WNOHANG
// gcc -g -O0 test_fork.c -o test_fork
#include <sys/wait.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int
main(int argc, char *argv[])
{
pid_t cpid, w;
int wstatus;
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* Code executed by child */
printf("Child PID is %jd\n", (intmax_t) getpid());
if (argc == 1)
pause(); /* Wait for signals */
_exit(atoi(argv[1]));
} else { /* Code executed by parent */
do {
w = waitpid(cpid, &wstatus, WNOHANG);
if (w == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
} while (w == 0);
printf("break waitpid, parent process exit..\n");
exit(EXIT_SUCCESS);
}
}