fork()是什么?
fork()的原理其实非常简单:当你调用fork()时,会产生一个子进程,这个子进程会拥有父进程所有的东西(记住,是所有)更通俗一点来讲,在函数代码段中,调用fork后,调用一次,返回两次,如果返回0,则代表当前的进程是子进程,不是的话则是父进程。接下来我们看段代码深入理解一下:
int main(int argc, char *argv[])
{
pid_t pid;
int x = 1;
pid = Fork(); //line:ecf:forkreturn
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x);
fflush(stdout);
return 0;
}
/* Parent */
printf("parent: x=%d\n", --x);
fflush(stdout);
return 0;
}
代码中先定义了x=1,之后调用了fork并获取返回值,它判断了pid的值,如果等于0,代表是子进程,则输出child:x= ,如果是父进程则输出parent:x= ,在这里要注意两个进程是相互独立互不干扰的,所以每一次的输出结果可能都会不一样,我本次输出的是:
- child:x=2
- parent: x=0
如答案所示:判断pid等于0,则是子进程输出x加一之后的值,之后则退出,return 0直接为退出程序,而到了父进程的时候,x仍为1,则父进程输出x减一之后的值。怎么样,看到现在是不是有点明白原理了呢?接下来一大波实验即将来袭!
实验一
void fork1()
{
if (fork() == 0) {
printf("Hello from child\n");
}
else {
printf("Hello from parent\n");
}
}
int main(){
fork1();
}
这个是很简单的例子,如果是子进程则输出Hello from child,父进程则输出Hello from parent。
则输出结果可能是以下可能:
-
Hello from child
Hello from parent
-
Hello from parent
Hello from child
还有一点不知道有没有细心的小伙伴注意到,上面实验中有一个fflush(stdout),而这个实验中没有,而是换行符。
首先呢,对于一个shell中运行的程序,默认的标准输出是控制台,而控制台是行缓冲的,也就是系统在缓冲区看到换行符,就帮你冲洗缓冲区。如果调用fflush,也可以触发输出,这就是强制冲洗。因此,如果只写输出语句不加换行,屏幕上是不会有语句的哦~不仅不会,有的时候还会产生令你满头问号的结果,因此,养成一个良好的习惯,从现在做起!
实验二
void fork2()
{
int x = 1;
pid_t pid = fork();
if (pid == 0) {
printf("Child has x = %d\n", ++x);
}
else {
printf("Parent has x = %d\n", --x);
}
printf("Bye from process %d with x = %d\n", getpid(), x);
}
int main(){
fork2();
}
可以看到运行结果,调用fork后,子进程拥有父进程所有代码,所以printf也要执行,即如上图所示,也可以观察到子进程和父进程的进程号。
实验三
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
int main(){
fork3();
}
好的,我估计到这里就有人开始头冒问号了,这里我用最通俗的语言讲一下,不然再看下去的小伙伴就会秃头了!
大家可以在本子上画一个进程图,就会很清晰
我们可以从图中看出大体结构,打印完L0只后,调用了fork返回两个进程父进程继续往下走,打印L1,调用fork又返回两个进程,父进程继续往下走,打印bye,而第一个fork的子进程只会继承fork之后的代码,因此打印L1后继续调用fork,再产生两个子进程,只后如上,但因为所有的父进程和子进程都是相互独立的(就是当前干嘛完全是计算机想干啥干啥,反复横跳,但要记住,要有bye必须得先有L1,要想有L1,必须得先有L0)所以输出可能性就有很多种,有啥可能性呢?如下(这里就不换行了 麻烦):
- L0 L1 Bye L1 Bye Bye Bye
- L0 L1 L1 Bye Bye Bye Bye
- L0 L1 Bye Bye L1 Bye Bye
好了,你有没有列出来呢,接下来难度升级哦,准备好!
实验四
void fork4()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
int main(){
fork4();
}
好的,我们可以数一数一共有一个L0,两个L1,四个L2,8个Bye,但因为进程独立,所以输出方式又有很多种啦,上面只是其中之一,列举如下的可能性(太多啦,只列举一些,大家只要记住上面的规则就可以都推出来的,完全是数学的排列组合问题嘛,o(╥﹏╥)o):
- L0 L1 L2 Bye Bye L1 L2 Bye Bye L2 Bye Bye L2 Bye Bye
- L0 L1 L2 Bye L1 Bye L2 L2 L2 Bye Bye Bye Bye Bye Bye
- L0 L1 L2 Bye L1 Bye L2 Bye Bye L2 Bye Bye L2 Bye Bye
- L0 L1 L2 Bye L1 Bye L2 Bye Bye L2 L2 Bye Bye Bye Bye
- L0 L1 L2 L2 Bye Bye Bye Bye L1 L2 L2 Bye Bye Bye Bye
实验五
void fork5()
{
printf("L0\n");
if (fork() != 0) {
printf("L1\n");
if (fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
int main(){
fork5();
}
梳理一下,结果是1个L0 ,1个L1, 1个L2, 3个Bye,因为是if的嵌套,所以我们来走一遍,先输出L0,if中调用fork产生两个进程,父进程继续往下走,输出L1,调用fork,输出L2,输出Bye,第一个子进程在第一个if中条件不符合,直接输出Bye,第二个子进程也不满足第二个if,直接输出Bye,因此答案就出来啦~
实验六
void fork6()
{
printf("L0\n");
if (fork() == 0) {
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
int main(){
fork6();
}
这个跟上面是类似的,就不再多做赘述了喔。
实验七
void cleanup(void) {
printf("Cleaning up\n");
}
void fork7()
{
atexit(cleanup);
fork();
exit(0);
}
int main(){
fork7();
}
首先先来解释一下atexit函数:atexit函数是一个特殊的函数,它是在正常程序退出时调用的函数,被称为登记函数,因此,在上文中,cleanup就是一个登记函数,登记函数被退出函数exit自动调用,因此调用fork后的子进程因为继承了exit函数,所以也会输出cleaning up,了解了重要函数这个就很简单啦~
实验八
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
int main(){
fork8();
}
很简单的判断父进程和子进程的例子,但因为其中加了一个死循环,即while(1),则当前会一直在运行,强制退出即可。
实验九
void fork10()
{
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}
int main(){
fork10();
}
在这个代码中又出现了新的函数:wait(),我们来看看它的具体作用:进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1
因此,在上面,父进程输出语句后调用了wait,则子进程运行结束后,输出子进程结束了,输出Bye。
实验十
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#define N 5
void fork11()
{
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
int main(){
fork11();
}
因为一个for循环,循环了五次,每次的fork返回一个子进程一个父进程,子进程调用exit直接退出,父进程每次调用wait返回子进程的状态,当结束时调用输出语句,输出多少进程号的子进程结束,并伴有状态码。
实验十一
void fork12()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
int main(){
fork12();
}
与上个不同的是,父进程的循环是反着的,因此输出子进程的进程号也是从大到小输出的。
实验十二
#define N 5
void fork13()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
int main(){
fork13();
}
这里的不同则是,子进程中有一个死循环,即一直运行,父进程中杀死了子进程,输出的时候wait获得子进程的状态,因为不是exit正常退出而是被杀死的,因此输出子进程不正常的结束。
实验十三
#define N 5
void int_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
exit(0);
}
void fork14()
{
pid_t pid[N];
int i;
int child_status;
signal(SIGINT, int_handler);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
int main(){
fork14();
}
又出现了新的函数signal:signal(参数1,参数2);
参数1:我们要进行处理的信号。
参数2:我们处理的方式,上文中则是输出语句
而信号则是进程与进程之间通信的方式。
上面进程收到了信号2,则是进程中断,父进程输出语句时,wait得到的状态改变了。
因为进程独立,因此我们在运行一次,可能会出现如下结果
都是正常的哦~
具体信号如下:
-
SIGABRT 进程停止运行 6
SIGINT 终端中断 2
SIGKILL 停止进程(此信号不能被忽略或捕获)
SIGQUIT 终端退出 3
实验十四
#define N 5
int ccount = 0;
void child_handler(int sig)
{
int child_status;
pid_t pid = wait(&child_status);
ccount--;
printf("Received SIGCHLD signal %d for process %d\n", sig, pid); /* Unsafe */
fflush(stdout); /* Unsafe */
}
/*
* fork14 - Signal funkiness: Pending signals are not queued
*/
void fork15()
{
pid_t pid[N];
int i;
ccount = N;
signal(SIGCHLD, child_handler);
for (i = 0; i < N; i++) {
if ((pid[i] = fork()) == 0) {
sleep(1);
exit(0); /* Child: Exit */
}
}
while (ccount > 0)
;
}
int main(){
fork15();
}
这个实验的光标一直在闪,说明没有结束进程,具体我们和下个实验结合一起来一起看
实验十五
#define N 5
void child_handler2(int sig)
{
int child_status;
pid_t pid;
while ((pid = wait(&child_status)) > 0) {
ccount--;
printf("Received signal %d from process %d\n", sig, pid); /* Unsafe */
fflush(stdout); /* Unsafe */
}
}
/*
* fork15 - Using a handler that reaps multiple children
*/
void fork16()
{
pid_t pid[N];
int i;
ccount = N;
signal(SIGCHLD, child_handler2);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
sleep(1);
exit(0); /* Child: Exit */
}
while (ccount > 0) {
pause();
}
}
int main(){
fork16();
}
看到这里迷惑了,为什么只加了一个函数结果完全 不一样了?我们先来看看函数吧
pause() :它会令目前的进程挂起, 直到信号被传送,进程要么终止要么触发调用信号处理函数。
返回值:仅当捕获信号并且信号捕获函数返回时才返回。这种情况下,仅返回-1,并且errno设置为EINTR
sleep() :sleep()会令目前的进程暂停参数seconds 所指定的时间, 或是暂停中途被信号所中断.
返回值:若进程暂停到参数seconds 所指定的时间则返回0, 若有信号中断则返回剩余秒数.
看到这两个函数,会发现相似点,都是会被信号中断的函数,而实验十五中,在死循环中使用pause函数让父进程被挂起,只有捕获到信号的时候 触发信号处理程序,这个时候子进程运行完,被挂起的父进程收到了子进程返回的状态信号,因此,触发了输出语句。
实验十六
void fork17()
{
if (fork() == 0) {
printf("Child1: pid=%d pgrp=%d\n",
getpid(), getpgrp());
if (fork() == 0)
printf("Child2: pid=%d pgrp=%d\n",
getpid(), getpgrp());
while(1);
}
}
int main(){
fork17();
}
这里看一下新的函数:
getpgrp():用来取得目前进程所属的组识别码. 此函数相当于调用getpgid(0),嵌套if中,只有子进程进入,之后子进程中又调用了fork,同样只有子进程输出语句。
书中家庭作业有关fork实验
8.14
void doit(){
if(fork()==0){
fork();
printf("hello\n");
exit(0);
}
return;
}
int main(){
doit();
printf("hello\n");
exit(0);
}
这个的进程图如下图所示:
因为doit函数中,子程序最后调用exit直接退出,则不会继续执行输出语句,而父进程不会进if,则执行return返回到main函数,执行之后的输出语句,后exit结束进程。
8.15
void doit(){
if(fork()==0){
fork();
printf("hello\n");
return;
}
return;
}
int main(){
doit();
printf("hello\n");
exit(0);
}
进程运行结果和上面不同,是五个hello,这就对比出了exit和return的区别,下图是进程图
exit函数是系统级调用,来终结程序用的,使用后程序自动结束,跳回操作系统。而return是函数级调用,是返回调用的地方,因此子进程执行完调回main函数,不会终结,所以会有五个输出。
8.18
void end(void){
printf("2");
fflush(stdout);
}
int main(){
if(fork()==0)
atexit(end);
if(fork()==0){
printf("0");
fflush(stdout);
}
else{
printf("1");
fflush(stdout);
}
exit(0);
}
前面讲过atexit函数,叫做登记函数,在调用exit的时候,自动调用end函数,因为第一个fork的时候只有子进程调用了登记函数,因此只有上面的才会在调用exit时候同时输出语句,而下方父进程则没有。因此我们看看这道题的选项:
A.112002 B.211020 C.102120 D.122001 E.100212
正确答案是ACE,因为就像上面讲过的原理,虽然各进程是独立的,但绝不会“无中生有”,像进程图中画的很明白:第一个子进程的两条进程中,没有输出0,1,就不可能输出2
所以我们一起来看看,如果我们从上到下将进程标为①②③④的话,讲起来会简单一点:
A:正确。其中的一种可能是 ② :1 → ④:1 → ②:2 →①:0→ ③:0 → ①:2
B:错误。正如上面所说,没有0,1,不可能出2 ,所以2不可能在前。
C: 正确。 可以是 ②:1 →①:0 → ①:2 → ④:1 → ②:2 →③:0
D: 错误。因为只输出一个1,不可能产生两个2.
E:正确。可以是②:1 →①:0 → ③:0 → ②:2 → ④:1 → ①:2
所以正确答案是ACE,只要理解独立但不能“无中生有”的原则,这道题就很好做啦~