Linux:学会如何创建进程(fork、vfork、写时拷贝)


fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程,复制父进程中的信息。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void); 
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

在这里插入图片描述

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid;
	printf("Before: pid is %d\n", getpid());
	if ( (pid=fork()) == -1 )
		perror("fork()"), _exit(1);

	printf("After:pid is %d, fork return %d\n", getpid(), pid);
	sleep(1);
	return 0; 
}

运行结果:
apple@AppledeMacBook-Pro Linux % ./test
Before: pid is 26524
After:pid is 26524, fork return 26525
After:pid is 26525, fork return 0

这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after 消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示

在这里插入图片描述

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

fork函数返回值:

  • 子进程返回0
  • 父进程返回的是子进程的pid

fork常规用法:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

vfork函数

fork 创建的子进程,父子进程是可以同时运行的

pid_t vfork() - - 创建一个子进程,并且阻塞父进程,直到子进程exit退出或者程序替换之后,父进程才会运行。

vfork创建子进程效率比较高,因为vfork创建子进程之后父子进程共用同一个虚拟空间

在这里插入图片描述

早期使用vfork是因为vfork创建子进程效率高,但是fork实现了写实拷贝之后,创建效率也变高了,并且使用起来更加灵活,因此vfork现在已经很少使用了

为什么要创建一个子进程?

创建子进程大多数情况下并不是为了让子进程干跟自己一样的活,而是让子进程区调度另一个程序的运行

(有一个任务,父进程可以做,但是怕有风险,万一数据处理出错崩溃了,因此创建一个子进程,让子进程去做)

写时拷贝(COW)

写时拷贝技术实际上是一种拖延战术,是为了提高效率而产生的技术,这怎么提高效率呢?实际上就是在需要开辟空间时,假装开了空间,实际上用的还是原来的空间,减少开辟空间的时间,等到真正要使用新空间的时候才去真正开辟空间。

在这里插入图片描述

写时拷贝技术原理:

写时拷贝技术实际上是运用了一个 “引用计数” 的概念来实现的。在开辟的空间中多维护四个字节来存储引用计数。

有两种方法:
①:多开辟四个字节(pCount)的空间,用来记录有多少个指针指向这片空间。
②:在开辟空间的头部预留四个字节的空间来记录有多少个指针指向这片空间。
  当我们多开辟一份空间时,让引用计数+1,如果有释放空间,那就让计数-1,但是此时不是真正的释放,是假释放,等到引用计数变为 0 时,才会真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。

多进程同时操作文件

在操作文件之前,很有必要了解一下内核中文件的存储和访问方式:

在这里插入图片描述

这张图摘自《APUE》

从图中能够看出每个进程都有自己独立的一个进程表项,由文件指针指向文件表项;在文件表项中两个很重要的东西:状态标志和当前文件偏移量,为什么说它很重要,因为在多进程写文件出错时,一般都是由文件偏移量引起的;然后由V节点指针指向一个V节点表

第一种情况:父子进程同时写一个文件

在这里插入图片描述

父进程用fork函数创建子进程的时候,会把自己的上下文环境拷贝一份复制到子进程的内存空间中,这里当然包括进程表。所以子进程的进程表和父进程的是一模一样的,它们指向的是同一个文件表,上面讲到过,当前偏移量会引起文件操作错误。

对于这个文件偏移量,有几点需要搞清楚:在用open函数打开文件时如果没有加上O_APPEND标志,那么这个文件表的文件偏移量为0;加上的话,它会把V节点中的当前文件长度赋给文件偏移量;写完文件之后(没有关闭文件描述符),文件长度会变化,相应的当前偏移量也随着文件长度的变化而变化。这里需要注意,是写完一个文件之后(也就是write函数执行完之后),偏移量才会改变。

到这里的话,基本上就清楚了:如果写操作是一个原子操作的话(可以用pwrite实现),那么父子进程写同一个文件不会出现任何问题;如果不是原子操作的话,有可能在父进程的write函数没有返回之前又执行了子进程的write函数,由于当前文件偏移量没有改变,所以会覆盖掉原先内容。

提问:

  1. 在多进程的环境下,父子进程同时去写一个文件,例如父进程每次写入aaaaa,子进程每次写入bbbbb,问题是会不会出现写操作被打断的现象,比如出现aabbbaaabb这样交替的情况?

结论:

  • 使用write系统调用的情况下,不会出现内容交叉的情况。
  • 使用fwrite ANSIC标准C语言函数,会出现内容交叉的情况。

为什么write不会出现问题但是fwrite却出现了问题?

答:write是Linux操作系统的系统调用,fwrite是ANSIC标准的C语言库函数,fwrite在用户态是有缓冲区的。因此需要锁机制来保证并发环境下的安全访问。

如果两个进程同时write一个socket会怎样?

答:就像队列一样,一个进程写完另一个进程才能写,数据上不会有问题。

  1. 如果父子进程同时操作一个文件,这个内存数据会不会被重新拷贝?

不会的,每个打开文件的操作句柄都有自己的缓冲区,各操作各的。

知识点习题

int main{
	fork() || fork()
} 

共创建了()个进程

A. 3
B. 2
C. 1
D. 4

正确答案: A

答案解析:

  • fork()给子进程返回一个零值,而给父进程返回一个非零值;
  • 在main这个主进程中,首先执行 fork() || fork(), 左边的fork()返回一个非零值,根据||的短路原则,前面的表达式为真时,后面的表达式不执行,故包含main的这个主进程创建了一个子进程,
  • 由于子进程会复制父进程,而且子进程会根据其返回值继续执行,就是说,在子进程中, fork() ||fork()这条语句左边表达式的返回值是0, 所以||右边的表达式要执行,这时在子进程中又创建了一个进程,

main进程->子进程->子进程,一共创建了3个进程。

  1. 下列程序代码在Linux系统执行后"*"会被输出多少次()
void main() {
	int i; 
	for(i=0;i<3;i++) {
		fork();
		printf("*\n"); 
	}
	return; 
}

A. 14
B. 16
C. 30
D. 32

正确答案: A

答案解析

注意这个题目中输出的时候有\n,刷新了缓冲区,所以只能是14个。
画一个二叉树可以快速得出

1                   。第一个不算
2            。            。(23        。      。     。     。(44      。  。  。   。 。  。 。   。(8
  1. 下列关于 clone 和 fork 的区别描述正确的有?

A. clone和fork最大不同在于fork不再复制父进程的栈空间,而是自己创建一个新的。
B. clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。
C. clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程 变成父进程的兄弟进程等等
D. fork是clone的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程 变成父进程的兄弟进程等等

正确答案: C

答案解析:

fork() 函数复制时将父进程的所以资源都通过复制数据结构进行了复制,然后传递给子进程,所以 fork() 函数不带参数; clone() 函数则是将部分父进程的资源的数据结构进行复制,复制哪些资源是可选择的,这个可以通过参数设定,所以 clone() 函数带参数,没有复制的资源可以通过指针共享给子进程

  1. 下面关于系统调用的描述中,错误的是()

A. 系统调用把应用程序的请求传输给系统内核执行
B. 系统调用中被调用的过程运行在"用户态"中
C. 利用系统调用能够得到操作系统提供的多种服务
D. 是操作系统提供给编程人员的接口
E. 系统调用给用户屏蔽了设备访问的细节
F. 系统调用保护了一些只能在内核模式执行的操作指令

正确答案: B

答案解析:

调用程序是运行在用户态,而被调用的程序是运行在系统态


如有不同见解,欢迎留言讨论~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值