fork()
一个现有的进程可以调用fork()函数创建一个新的进程,调用fork()函数的进程成为父进程,由fork()函数创建出来的进程被称为子进程。
一次fork调用会产生两次返回值
怎么理解这句话?
- fork调用会创建一个新的进程,这个新的进程就是子进程。也就是说,fork调用之后会存在两个进程:一个子进程,一个父进程。
- 所以会有两个返回值,子进程返回一次,父进程返回一次。
- 父进程返回的是子进程的pid,子进程返回的是0。
用代码测验 一下,加深理解。
#include <stdio.h>
#include <unistd.h>
int main(void){
int ret;
ret = fork();
if(-1 == ret){
perror("fork error");
return 1;
}
else if(0 == ret){
printf("I am child, PID = %d, PPID = %d\n",getpid(),getppid());
}
else {
printf("I am parent, PID = %d, PPID = %d, CPID = %d\n", getpid(), getppid(), ret);
}
return 0;
}
然后编译运行。
可以看到,返回了一个大于0的数和0。
大于0时,返回的是创建的子进程的pid,也就是4697,此时的父进程是bash,pid是4640,bash的子进程为4696。
等于0时,此时父进程pid为4696,子进程为4697,也就是fork创建的子进程。
OK,这样应该就理解了fork的两次返回了。
fork创建了一个与原来进程几乎完全相同的进程
- 子进程看作是父进程的一个副本,fork是以复制的形式创建子进程,子进程几乎完全复制了父进程
- 父进程与子进程不共享这些存储空间,比如拷贝父进程的数据段、堆、栈、文件描述符等。
- 子进程与父进程各自在自己的进程空间运行,相互独立
可以用代码来测试一下。
#include <stdio.h>
#include <unistd.h>
int main(void){
int ret;
int a = 10;
ret = fork();
if(-1 == ret){
perror("fork error");
return 1;
}
else if(0 == ret)//子进程
{
printf("I am child\n");
printf("a = %d\n",a);
a = 100;
printf("a= %d\n",a);
}
else//父进程
{
sleep(1);//先让子进程去修改a的值
printf("I am parent\n");
printf("a = %d\n",a);
}
return 0;
}
编译运行。
可以看到,即便是子进程里修改了a的值,但是父进程打印出来的还是原来的值。
子进程从fork调用返回后开始运行
- 虽然子进程和父进程运行在不同的进程空间中,但是它们执行的却是同一个程序。
- 子进程执行的是fork之后的代码,不会从头开始运行
代码测试一下。
#include <stdio.h>
#include <unistd.h>
int main(void){
int ret;
int a = 10;
printf("hello world!\n");//写在fork之前
/**************************************************/
ret = fork();
if(-1 == ret){
perror("fork error");
return 1;
}
else if(0 == ret)//子进程
{
printf("I am child\n");
printf("a = %d\n",a);
a = 100;
printf("a= %d\n",a);
}
else//父进程
{
sleep(1);//先让子进程去修改a的值
printf("I am parent\n");
printf("a = %d\n",a);
}
printf("end\n");
return 0;
}
编译运行一下。
可以发现,确实是如此。
父、子进程间的文件共享
子进程复制了父进程的文件描述符,那如果父、子进程都对同一个文件进行读写,那是会覆盖还是接续写呢?
理论上是接续写。
但我们可以用一个程序来进行验证一下。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void){
int fd;
fd = open("./test.txt",O_WRONLY | O_TRUNC);
if(-1 == fd){
perror("open error");
return 1;
}
switch (fork())
{
case -1:
perror("fork error");
close(fd);
return 1;
//子进程
case 0:
printf("I am child process\n");
write(fd,"hello world",11);
close(fd);
return 0;
default:
printf("I am parent process\n");
write(fd,"123456", 6);
close(fd);
return 0;
}
return 0;
}
编译运行一下。
可以看出,的确是接续写的。
父、子进程间的竞争关系
fork之后父进程、子进程谁先运行?
不确定,绝大多数情况下是父进程先运行。
监视子进程
父进程监视子进程,父进程需要知道子进程的状态改变,子进程什么时候发生状态改变。
那么子进程状态改变包括哪些呢?
- 子进程终止
- 子进程因为收到停止信号而停止运行,SIGSTOP、SIGTSTP
- 子进程在停止状态下因为收到恢复信号而恢复运行,SIGCONT
wait()函数
- 只能监视子进程什么时候终止,获取子进程终止时的状态信息
- 回收子进程的资源
编写程序验证一下。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void){
switch (fork())
{
case -1:
perror("fork error");
return 1;
case 0:
printf("I am child,PID = %d\n",getpid());
return 0;
default:{
int ret;
int status;
printf("I am parent\n");
ret = wait(&status);
if(-1 == ret){
perror("wait error");
return 1;
}
printf("%d, %d\n",ret,WIFEXITED(status));
return 0;
}
}
return 0;
}
特别注意的是,一开始default:之后我是没有加大括号的,然后出现了报错。
然后我去查了一下,发现其实是因为如果不加大括号,那么这个变量的声明是在整个switch作用域的,不够严谨,所以要加大括号。
具体参考这篇文章:http://t.csdn.cn/aMH62
编译运行一下。
成功。
参数status不为NULL的情况下,则wait()会将子进程的终止时的状态信息存储在它指向的int变量中,可以通过以下的宏来进行检查。
- WIFEXITED(status):如果子进程正常终止,则返回 true
- WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit() 时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过 WEXITSTATUS 宏转换
- WIFSIGNALED(status):如果子进程被信号终止,则返回 true
- WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号
- WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true
waitpid()函数
使用wait()函数存在一些限制:
- 父进程创建多个子进程,无法等待某个特定的子进程完成,只能按照顺序
- 如果子进程没有终止,wait()总是保持阻塞,有时我们希望非阻塞等待
- 使用wait()只能发现那些被终止的子进程
waitpid()原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options)
重点看options这个参数,它可以包括0个或多个如下标志:
- WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
- WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
- WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
由此可见,它的功能要强于wait()函数,弥补了子进程状态改变的后两个情况。
异步方式监视子进程
SIGCHLD信号
我们可以为SIGCHLD信号绑定一个信号处理函数,然后在信号处理函数中调用wait/waitpid函数回收子进程。
只有子进程终止后才需要回收,如果是其他两种状态改变,则父进程可根据设计需求在信号处理函数中做出相应的处理。
使用SIGCHLD 信号回收子进程需要注意一个问题:
当调用信号处理函数的时候,会暂时将当前正要处理的信号添加到进程的信号掩码中,这样一来,当SIGCHLD信号处理函数正在为某一个已经终止的子进程收尸时,如果此时相继有两个子进程终止了,也就是会产生两次SIGCHLD信号,但是会有一次SIGCHLD信号会被丢失,也就是说父进程最终也会只能接收一次SIGCHLD,那么就会导致漏掉一个,导致有一个子进程没有被回收。
解决此问题,就是在信号处理函数中循环以非阻塞方式来调用waitpid()。
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
使用代码测试一下。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
static void wait_child(int sig)
{
/*替子进程收尸*/
printf("父进程回收子进程\n");
while (waitpid(-1,NULL,WNOHANG)>0)//循环非阻塞的方式
{
continue;
}
}
int main(void){
/*为信号绑定SIGCHLD函数*/
struct sigaction sig = {0};
sigemptyset(&sig.sa_mask);
sig.sa_handler = wait_child;
sig.sa_flags = 0;
if(-1 == sigaction(SIGCHLD,&sig,NULL)){
perror("sigaction error");
exit(-1);
}
/*创建子进程*/
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0://子进程
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default://父进程
break;
}
while(1){
sleep(1);
}
return 0;
}
编译运行一下。
实现了异步监视。
僵尸进程与孤儿进程
- 父进程先于子进程结束,成为孤儿进程,父进程变为init进程
- 如果子进程先于父进程结束,此时父进程还未来得及“收尸”,成为僵尸进程
- 僵尸进程无法通过信号将其删除,僵尸进程本来就是已经终止的进程,只不过还未被回收,只要它的父进程一直不去回收它,这个僵尸进程就会一直存在系统中。
参考正点原子linux应用开发教程