目录
fork
介绍
- fork()函数是一个系统调用,用于创建一个新的进程
- 它会复制当前进程的内容,并在新进程中运行,当前进程是新进程的父进程,从而使父进程和子进程可以并行执行
- 各自有自己的独立地址空间,但它们可以共享一些资源,如文件描述符
- 头文件:<unistd.h>
fork的返回值问题
介绍
- 当我们运行起来这个程序时,我们会发现,我们已经成功创建出了子进程(因为有两个pid被打印出来,并且其中一个是另一个的父亲)
- 但是,我们仔细观察观察,似乎ret这个变量似乎有不同的值
- 因为我们看到它可以同时执行if 和 else的内容,也就是说,在两个进程中,实际上ret的值是不同的
- 这是为什么呢?
fork()时,系统要做什么
- 首先明确: 进程 = 内核数据结构(os来分配) + 进程代码和数据(一般从磁盘中来)
- 执行fork,也就是创建了一个新的进程,只不过这个新进程有点特殊,数据几乎全部都继承自创建自己的父进程
- 控制权转移到内核中后(os会为子进程分配空间,提供初始值)
- 从fork()函数的返回值开始,就开始并行执行两个进程啦
数据是否要独立
- 因为进程具有独立性,所以子进程也得有自己的代码和数据
- 但是我们没有加载内存的过程,它也就没有自己的代码和数据,就可以和父进程共享使用
- 代码共享没有什么问题,因为不可更改,都是只读
- 但是!!数据可不能共享,它是可写的!
如果共享的话,就会出现问题!
- 一般创建子进程就是为了和父进程执行不同的操作
- 如果其中一个变量一修改,另一个进程也会受到影响的话,未免也太挫了,也不满足独立性
- 所以!数据必须分离!!
写时拷贝
引入
虽然数据要分离,但是子进程用不到的那些数据还有必要再拷贝一份吗?
- 显然是没有的
- 这样的情况下,共享也是没有问题的
- (这样的例子可以在c语言中看到,比如两个指针指向同一个常量字符串,编译器也不会傻到开辟两份空间存储内容一样的字符串)
- 因此,只有将来会被父/子进程写入的数据,才值得被拷贝
介绍
- 但是,其实os也无法知道哪些数据会被写入,提前拷贝了也不会立马使用(相当于占着茅坑不拉屎)
- 所以,os选择了写时拷贝技术来将数据分离
举例(fork返回值)
- 所以前面的fork返回值问题,实际上返回值接收的这一过程就是写入
- 所以发生了写时拷贝
- 父子进程这一变量存在两份,但虚拟地址并没有改变,也就导致了我们看到的结果
- 但是可以通过不同的页表映射,从而找到实际的物理地址
fork返回的值是什么
虽然我们已经知道了两个进程中的ret不一样,但为什么会不一样呢?
- 是因为fork()在不同进程中返回了不同的值
- fork()在父进程中返回子进程的进程pid(该值一般大于0),而在子进程中返回0
- fork的返回值由os控制
- 它将子进程的pid返回给父进程,以便父进程可以管理子进程,然后继续下面的代码
- 子进程返回0,用于区分子进程
- 如果父进程收到-1,则代表进程创建失败
创建失败的原因
- 创建内存是需要使用内存资源的
- 如果当前系统有太多进程,内存资源不足时,os就可能不让你继续创建了
- 而且作为一个用户,可拥有的进程数是一定的
子进程执行位置从哪里开始
引入
我们其实还会发现,在fork之前的语句,子进程并没有执行:
我们会发现,在fork之前的那句打印只出现了一次,说明子进程是没有执行那个语句的
可以说明 -- 子进程并不是从头开始执行程序的
那子进程从哪里开始执行呢?
进程切换
- cpu使用进程切换的策略执行语句
- 并且要保证每个进程下次来的时候,必须从之前的位置继续运行(而不是重新来过,不然前面就白执行了)
- 所以cpu必须要有对应的寄存器数据(EIP , 程序计数器(pc指针)),来记录当前运行进程的执行位置
- cpu执行指令靠的就是EIP中的地址 (地址+指令长度=下一条指令地址)
- 每个进程走的时候,带走这份数据,下次来的时候再给寄存器,就可以实现想要的效果啦
- (寄存器数据也就是进程的上下文数据)
子进程执行的位置
- 而子进程成功创建的时候,恰好为fork该执行return指令的时候
- 因为当执行到return时,就说明它的主要功能已经实现,也就是子进程已经创建出来了
- 它里面的pc也就继承自父进程的进度(return的那里),他就认为自己的EIP起始值就是return指令的地址
- 所以子进程就从return开始执行
- 也可以通过这个理论,修改子进程的pc值
子进程的用途
就 -- 要么子承父业,要么自己独自发展