目录
进程创建
Linux 中我们可以说一个进程就是一个PCB, 即 一个task_struct, 那么创建进程也就是创建PCB, 即是创建task_struct
Linux 中说到进程创建, 就不得不提到 fork()函数. fork()在Lnux下是非常重要的一个函数 .
fork()函数
从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
fork()在函数内部会调用clone这个系统调用接口
扫描二维码关注公众号,回复: 9655650 查看本文章pid_t fork ()
头文件: unistd.h
fork返回值
fork函数返回值 : (返回值类型为pid_t, 实际等同于int)
- 子进程在执行fork()时返回 0
- 父进程在执行fork()时, fork()创建子进程, 返回子进程的PID (PID是一个大于0的整数)
- 父进程在用fork()创建子进程失败时返回 -1
因为fork运行有多种结果, 所以往往fork之后要根据fork的返回值进行分流(例如用 if 写多个分支), 来看个例子 .
testfork.c 如下
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
}
else if(pid == 0){
printf("子进程\n");
}
else{
printf("父进程\n");
}
return 0;
}
编译执行如下, 可以看到, 当父进程用fork() 创建子进程成功后, 返回了其子进程的pid, 然后继续执行, 直到执行打印语句后子进程才执行, 如下 :
但并不是父进程创建了子进程, 父进程就一定会先执行完,才执行子进程, 也可能是父进程执行到一半, 甚至刚调用fork()创建完子进程后, 就立即转而执行子进程. 这取决于CPU的调度. 比如说下面这段代码 .
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
}
else if(pid == 0){
printf("子进程执行\n子进程pid:%d\n", getpid());
}
else{
printf("父进程开始执行\n");
sleep(5);
printf("父进程执行\n父进程pid:%d\n", getpid());
printf("父进程运行结束\n");
}
return 0;
}
可以看到, 父进程执行到一半开始执行子进程了, 就此次运行结果分析, 由于父进程中sleep()函数, 致使父进程进入睡眠状态
(sleeping)(这种睡眠是可中断的, 当sleep()执行完, 就会中断睡眠, 进入就绪状态(或者说进入运行队列), 等待分配时间片), 子进程
当被创建后, 一直处于就绪状态(一直处于运行队列中), 等待分配时间片, 当父进程睡眠时, 子进程拿到了时间片, 子进程执行 . 当子
进程执行完, 父进程拿到时间片后, 父进程继续运行 .
所以就有, fork创建子进程之前, 父进程独立运行, 创建子进程之后, 谁先运行取决于调度器的调度
进程调用fork, 内核会做出以下操作
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程 ( 此时已经创建了子进程的PCB即Linux下的task_struct )
- 添加子进程到系统进程列表当中 ( 即添加子进程PCB)
- fork返回,调度器开始调度
fork写时拷贝
fork 创建子进程采取分时拷贝的策略 .
即, 父进程创建子进程时,只创建了子进程的task_struct(PCB), 并没有直接给子进程开辟内存来拷贝数据,而是跟父进程
一样映射到同一位置,但是如果父进程或子进程有一方想要修改内存中的数据时,那么对于改变的这块内存,需要重新给
子进程开辟内存,并且更新子进程页表信息. 这样做, 提高创建子进程的性能, 并且能节省内存 . 如下图 :
图1. 父子进程共享代码与数据 图2. 父子进程代码共享, 数据独立
图里涉及到了虚拟内存与分页式内存管理的内容, 简单说一下, 分页式内存管理可以将一段程序加载到不连续的物理空间上,但是
从虚拟地址空间来看依旧是连续的, 用以解决内存使用率低的问题 .
mm_struct结构体也叫内存描述符,其中记录虚拟内存各个段的起始地址, 结束地址, 通过这种方式描述了进程的虚拟地址空间,
每一个进程都会有唯一的mm_struct结构体, mm_struct记录在task_struct中.
页表: 页表中存储的是虚拟地址和物理地址的映射关系, 即页号到物理块的地址映射. 通过虚拟地址得到页号与页内地址(或者叫页内偏移), 在页表中通过页号找到物理块号. 然后, 物理地址 = 物理块号 x 页面大小 + 页内偏移 就得到了物理地址
来段代码感受一下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t pid = fork();
int data = 0;
if(pid == -1){
perror("fork erro");
}
else if(pid == 0){
printf("子进程执行\n");
data = 10;
printf("data = %d\n", data);
printf("data地址: %p\n", &data);
}
else{
sleep(2);
printf("父进程执行\n");
printf("data = %d\n", data);
printf("data地址: %p\n", &data);
}
return 0;
}
代码中, 先让父进程睡上2秒, 这时会执行子进程, 子进程修改了data的值为10, 但子进程结束后, 父进程继续执行打印出的data
还是0, 两个进程所打印的值不同, 但父子进程中data的地址都是一样的. 我们知道数据不同, 则数据一定存储在不同的物理地址
上, 打印的的变量地址依旧相同, 这是因为取地址&得到的并不是物理地址(在所有有关地址的操作中, 我们只能接触到虚拟地址),
而是虚拟地址, 虚拟地址虽然相同, 但是父子进程有着不同的mm_struct, 即有着不同的页表, 这父子进程的data相同的(虚拟)地址
通过不同的页表映射到不同的物理地址上.
运行结果如下图:
fork失败原因
- 系统进程数达到太多, 达到上限(系统会有一个进程数的限定, 可以修改)
- 内存不足 (fork创建子进程需要创建新的PCB, 写时拷贝可能还会分配新的数据空间)
fork用法
fork()函数当然不是为了创建子进程而创建子进程, 创建子进程目的有两种:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段.
例如: 父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序 . 例如: 子进程从fork返回后,调用exec函数(用来做进程替换的函数, 下面说)替换子进程. 比
如, 我们用的Shell就是一个程序, 一般为默认为bash, 我们执行一些非Shell内建命令时, 实际就是一个Shell创建子进程, 再进
行进程替换的过程