深入理解计算机系统———fork()函数的运用

fork()函数定义如下:

pid_t fork( void);
(pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
函数说明

一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。 子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
接下来我们看一些例子:

## fork0

void fork0() 
{
    if (fork() == 0) {
	printf("Hello from child\n");
    }
    else {
	printf("Hello from parent\n");
    }
}

在这里插入图片描述
从上述运行结果来看,有2次输出,第一次运行到if语句时,由于是父进程fork()!=0,所以执行else语句;
在进入子进程时(与父进程同理),由于fork()==0,所以执行if语句。
**

## fork1

void fork1()
{
    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);
}

在这里插入图片描述
这个程序与fork0原理相同,第一次执行父进程,pid!=0所以执行else语句,接下来在执行子进程时,由于子进程是父进程的独立的副本(详情见开头的fork函数说明),x=1,所以再执行if语句;同时也可以看出子进程与父进程的pid是连着的,子进程的pid是父进程的+1。
## fork2

void fork2()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("Bye\n");
}

在这里插入图片描述
上述程序:首先父进程进行输出L0,然后运行第一个fork(),产生一个子进程1,然后父进程输出L1,接着在第二个fork时又产生一个子进程2,同时在子进程1中也产生了一个子进程(1,2),然后父进程输出Bye,接着在父进程的子进程2中输出Bye,然后回到子进程1中,子进程1输出L1,接着跟父进程类似,输出Bye,Bye。
在这里插入图片描述
## fork3

void fork3()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("L2\n");    
    fork();
    printf("Bye\n");
}

在这里插入图片描述
在这里插入图片描述
## fork4

void fork4()
{
    printf("L0\n");
    if (fork() != 0) {
	printf("L1\n");    
	if (fork() != 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}

在这里插入图片描述## fork5

void fork5()
{
    printf("L0\n");
    if (fork() == 0) {
	printf("L1\n");    
	if (fork() == 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}

在这里插入图片描述
## fork6

void cleanup(void) {
    printf("Cleaning up\n");
}
void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}

在这里插入图片描述
这里注意一下:int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func。您可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。
父进程退出的时候执行一次Cleanup,子进程退出时也执行一次。
在这里插入图片描述
## fork7

void fork7()
{
    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 */
    }
}

在这里插入图片描述
这里,父进程并没有退出,所以导致程序一直在运行,成为僵尸(zombies.)进程,一直在后台运行,具体表现如下,无法在终端创建新程序,只有Ctrl+C或者Ctrl+Z退出此程序才可以进行新的操作。
在这里插入图片描述
## fork8

void fork8()
{
    if (fork() == 0) {
	/* Child */
	printf("Running Child, PID = %d\n",
	       getpid());
	while (1)
	    ; /* Infinite loop */
    } else {
	printf("Terminating Parent, PID = %d\n",
	       getpid());
	exit(0);
    }
}

和前面的一样,父进程返回非0,所以输出PPID;然后进入子进程,输出PID,但是之后进入死循环,子进程并没有退出,成为僵尸进程。
在这里插入图片描述
## fork9

void fork9()
{
    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");
}

这里注意一下:pid_t wait(int *status)

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

上述程序,父进程需在调用wait()时等待子进程结束,再执行下面的操作。
在这里插入图片描述
## fork10

void fork10()
{
    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);
    }
}

这个程序一直for循环,让每个子进程有了自己的状态值,相应的父进程一直在wait着子进程,当最深的子进程推出时,输出pid和状态值,之后再进行上一层的输出。
在这里插入图片描述
## fork11

void fork11()
{
    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);
    }
}

这里注意:pid_t waitpid(pid_t pid,int *status,int options);

返回:如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1

以上程序在父进程中等待子进程退出,并输出他们的pid。
在这里插入图片描述
总结
1.fork 函数被调用一次,但返回两次
2.将子进程 ID 返回给父进程的理由是:一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程 ID
3.fork 使子进程得到返回值 0 的理由是:一个进程只会有一个父进程,所以子进程可以通过调用 getppid 函数,来获得其父进程的进程 ID
4.进程 ID 0 总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为 0
5.fork 之后进程的动作:子进程和父进程继续执行 fork 调用之后的指令。
6.子进程是父进程的副本。fork 之后,子进程获得父进程数据空间、堆和栈的副本。注意:这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段
其实创建子进程,就是把父进程的 PCB 拷贝过来,稍加修改,例如,修改子进程的进程 ID(肯定不能和父进程一样), 就变成子进程的 PCB 了。
7.进程调度:父进程和子进程谁先被执行,完全由进程调度器决定,谁先调度,是不确定的。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值