fork
我们可以通过调用fork创建一个新进程。这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。fork和exec函数结合在一起使用就是创建新进程所需要的一切了。
fork之后,子进程是父进程的副本。子进程将获得父进程的数据空间、堆和栈的副本。父子进程不共享这些存储空间部分。父子进程共享正文段。
#include <unistd.h>
pid_t fork(void);
在父进程中的fork调用返回的是新的子进程的进程号PID(这个PID并不是父进程的PID,而是新建的子进程的PID)。新进程将继续执行,就像原进程一样,不同之处在于,子进程中的fork调用返回的是0。父子进程可以通过这一点来判断究竟谁是父进程,谁是子进程。如果fork失败,它将返回-1。失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时errno将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEM。
也就是说如果fork返回-1,表示进程创建失败;返回0,表示当前进程为子进程;返回其他值表示当前进程为父进程,并且这个值是子进程的PID。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
write(STDOUT_FILENO, "write\n", 6);
printf("before fork\n");
pid_t pid = fork();
if (pid == -1)
{
perror("fork error");
exit(1);
}
else if (pid == 0)
{
//printf("This is child process, pid is %d\n", getpid());
}
else
{
//printf("This is parent process, pid is %d, child pid is %d\n", getpid(), pid);
wait(NULL);
}
printf("atfer fork\n");
return 0;
}
./a.out输出
write
before fork
atfer fork
atfer fork
./a.out > tmp.out;cat tmp.out输出
write
before fork
atfer fork
before fork(多了一句)
atfer fork
因为write系统调用是不带缓冲的,而标准IO库是带缓冲的。
如果标准输出连接到终端设备,则它是行缓冲,否则就是全缓冲。
首先调用write,数据直接写到标准输出。
./a.out的方式,此时标准输出为终端,那么就是行缓冲,遇到换行符输出并冲洗缓冲区,然后fork,此时缓冲区已经没数据了,所以输出一次before fork。
./a.out >tmp.out,此时标准输出为文件,是全缓冲,当缓冲区满了才会冲洗缓冲区,fork的时候缓冲区是有内容的,当进程终止时,会冲洗缓冲区。
wait
当用fork启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。我们可以通过在父进程中调用wait函数让父进程等待子进程的结束。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
wait系统调用将暂停父进程直到它的子进程结束为止。调用成功返回wait子进程的PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。如果status不是空指针,状态信息将被写入它所指向的位置。
如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作。
WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)(或者return 5或者_exit(5))退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
WIFSIGNALED(status) 如果子进程是因为一个未捕获的信号而终止,则此宏值为非零值。
WTERMSIG(status) 如果WIFSIGNALED非零,返回子进程因信号而中止的信号代码。
WIFSTOPPED(status) 如果子进程意外终止,它就取一个非零值。
WSTOPSIG(status) 如果WIFSTOPPED非零,返回一个信号代码。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if (pid == -1)
{
perror("fork error");
exit(1);
}
else if (pid == 0)
{
printf("This is child process, pid is %d\n", getpid());
sleep(3);
exit(5); // or return 5 or _exit(5);
}
else
{
printf("This is parent process, pid is %d, child pid is %d\n", getpid(), pid);
int status;
printf("wait child pid:%d\n", wait(&status));
if (WIFEXITED(status))
{
printf("successful, exit code is %d\n", WEXITSTATUS(status));
}
}
return 0;
}
This is parent process, pid is 733, child pid is 734
This is child process, pid is 734
// sleep了3秒
wait child pid:734
successful, exit code is 5
僵尸进程
用fork来创建进程确实很有用,但你必须清楚子进程的运行情况。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才告结束。因此,进程表中代表子进程的表项不会立刻释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时它将成为一个死(defunct)进程或僵尸(zombie)进程。(子进程早于父进程结束,并且父进程没有调用wait来回收子进程就会造成僵尸进程)
waitpid
waitpid函数可以用来等待某个特定进程的结束。
#include<sys/types.h>#include<sys/wait.h>
pid_t waitpid(pid_t pid,int * status,int options);
pid参数指定需要等待的子进程的PID。如果它的值为-1,waitpid将返回任一子进程的信息。与wait一样,如果status不是空指针,waitpid将把状态信息写到它所指向的位置。option参数可用来改变waitpid的行为,其中最有用的一个选项是WNOHANG,它的作用是防止waitpid调用将调用者的执行挂起。你可以用这个选项来查找是否有子进程已经结束,如果没有,程序将继续执行。其他的选项和wait调用的选项相同。 因此,如果想让父进程周期性地检查某个特定的子进程是否已终止,就可以使用如下的调用方式:
waitpid(child_pid, (int *)0, WNOHANG);
如果子进程没有结束或意外终止,它就返回0,否则返回child_pid。如果waitpid失败,它将返回-1并设置errno。失败的情况包括:没有子进程(errno设置为ECHILD)、调用被某个信号中断(EINTR)或选项参数无效(EINVAL)