【进程控制】fork 函数和 vfork 函数

【进程控制】fork 函数和 vfork 函数

1. 函数 fork

一个现有进程可以调用 fork 函数来创建一个新进程:

#include <unistd.h> // fork函数定义在该头文件

pid_t fork(void);
// fork函数被调用1次,返回2次
// 子进程 : 返回0
// 父进程 : 返回子进程ID
//   出错 : 返回-1

​ fork 函数有如下特点:

(1)fork 函数被调用1次,返回2次,2次返回的区别是:子进程返回0,父进程返回子进程的ID;

(2)子进程和父进程继续执行fork调用后的指令。子进程是父进程的副本,它获得了父进程 数据空间、堆和栈的副本。注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分,它们共享的是正文段

(3)由于 fork 后经常跟随着 exec,所以很多实现并不执行一个父进程数据段、栈和堆的完全副本。作为替代,使用 写时复制 技术。这些区域由父进程和子进程 共享,而且内核将它们的访问权限设为 只读。如果父进程或子进程中的一个试图修改这些区域,则内核 只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的“一页”。

fork函数使用的一个例子:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>


int var1 = 10;
char buf[] = "I am writting ! \n";

int main() {
	pid_t pid;

	int var2 = 20;

    // write函数是不带缓冲的
    // sizeof(buf)-1 是为了防止将终止符写入
	if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) {
		fprintf(stderr, "write error !\n");
		exit(-1);
	}
	printf("before fork\n");

	if((pid = fork()) < 0) {
		perror("fork error");
	} else if(pid == 0) {
		var1++;
		var2++;
	} else {
		sleep(2);
	}

	printf("pid = %ld, var1 = %d, var2 = %d\n", long(pid), var1, var2);

	exit(0);
}

运行结果:

$ ./a.out
I am writting !
before fork
pid = 0, var1 = 11, var2 = 21
pid = 136, var1 = 10, var2 = 20  // 父进程的变量没有被改动

$ ./a.out > b.out
$ cat b.out
I am writting !
before fork
pid = 0, var1 = 11, var2 = 21
before fork
pid = 138, var1 = 10, var2 = 20  // 父进程的变量没有被改动

【代码分析】

(1) write 函数

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);
// 从buf中读取 nbytes 个字节到文件 fd 中
// 若成功,返回已写的字节数
// 若失败,返回-1

write 函数是不带缓冲的,上例在fork之前调用了该函数,因此其数据写道标准输出一次;

(2)文件描述符

#include <unistd.h>  // 文件描述符定义在该头文件中

幻数0 : STDIN_FILENO    // 标准输入
幻数1 : STDOUT_FILENO	//  标准输出
幻数2 : STDERR_FILENO   //  标准出错

(3)printf 是标准 I/O 库,标准 I/O 库是带缓冲的,并且:

​ A. 如果标准输出连接着 终端设备,则它是 行缓冲 的;
​ B. 其它情况下,它是 全缓冲 的。

​ 当以 交互方式 运行该程序时(即第1次运行),标准输出缓冲区被换行符冲洗,因此只得到 printf 输出的行一次;

​ 当 将标准输出重定向到一个文件 时(即第2次运行),在fork之前调用了一次 printf,但当调用 fork 时,由于此时是全缓冲,因此这些数据仍在缓冲区内,然后在将父进程数据空间复制到子进程中时,该缓冲区的数据也被复制到了子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。则 exit 之前的第二个 printf 会 将其数据追加到已有缓冲区中。当进程终止时,其缓冲区中的内容被写到相应的文件中。

(4)父进程和子进程每个相同的打开描述符共享一个文件表项,因此,在重定向了父进程的标准输出时,子进程的标准输出也被重定向。

2. 函数 vfork

vfork 函数的 调用序列和返回值 与fork相同,但 二者的语义不同

(1)vfork 函数用于创建一个新进程,该新进程的目的是 exec 一个新程序

​ vfork 和 fork 一样都创建一个子进程,但是它 并不将父进程的地址空间完全复制到子进程中,因为子进程会 **立即调用 exec (或 exit)**而不会引用该地址空间。要注意的是,子进程在调用 exec 或 exit 之前,它在父进程的空间运行,因此,如果子进程 修改数据(除了用于存放 vfork 返回值的变量)、进行函数调用、或者没有调用 exit 或 exec 就返回,则都可能带来未知的后果

(2)vfork 保证子进程先行

​ vfork保证 子进程先行。在子进程调用 exec 或 exit 之后,父进程才可能被调度运行;当子进程调用这两个函数中的一个时,父进程会恢复运行(如果在调用这两个函数前,子进程依赖于父进程的进一步动作,则会导致 死锁)。

函数 vfork 的使用例子:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int var1 = 10;

int main() {
	pid_t pid;

	int var2 = 20;

	printf("before fork\n");

	if((pid = vfork()) < 0) {
		perror("fork error");
	} else if(pid == 0) {
		var1++;
		var2++;
		_exit(0);  // _exit不执行冲洗操作
	} 

	printf("pid = %ld, var1 = %d, var2 = %d\n", long(pid), var1, var2);

	exit(0);  // exit执行冲洗操作
}

执行结果:

$ ./a.out
before fork
pid = 148, var1 = 11, var2 = 21  // 父进程的变量被改了

【代码分析】

(1)因为子进程在父进程的地址空间中运行,因此子进程对变量的操作,会改变父进程中的变量值;

(2)子进程调用的是 _exit 而不是 exit。exit 会实现冲洗标准 I/O 流,因此如果子进程调用的是 exit,那么会出 现两种情况:

​ A. 如果 函数库采取的唯一操作就是冲洗 I/O 流,那么此时得到的输出结果与子进程调用 _exit 时得到的输出结果是一样的;

​ B. 如果 该实现也关闭了 I/O 流,那么表示标准输出 FILE 对象的相关存储区将被清0.因为子进程借用了父进程的地址空间,因此当父进程恢复运行并调用 printf 的时候,printf 会返回-1,即不会产生任何输出结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值