你好,我是安然无虞。 |
文章目录
- 自学网站
- 进程创建
- fork函数
- 写时拷贝
- 进程终止
- 进程退出场景
- 练习题
自学网站
进程创建
fork函数
fork函数在Linux中是一个很重要的函数, 它是从已经存在的进程中创建一个新的进程. 新进程为子进程, 而原进程为父进程. 所以, fork函数是用来创建子进程的
.
返回值: fork 函数比较特殊, 它有两个返回值, 给子进程返回0, 给父进程返回子进程的pid
, 出错返回-1
调用 fork 函数, 当控制转移到内核中的 fork 代码后, 内核做了几件事:
-
分配新的内存块和内核数据结构给子进程(task_struct, mm_struct等)
; -
将父进程部分数据结构内容拷贝至子进程
; -
添加子进程到系统进程列表当中
; -
fork 返回, 开始调度器调度
.
当一个进程调用 fork 函数之后, 就会有两个二进制相同的代码的进程, 而且它们都运行到相同的地方, 但是每个进程都可以执行自己的代码.
做个小实验:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
printf("Before: pid is %d\n", getpid());
pid_t id = fork();
if(id == -1)
{
perror("fork()");
exit(1);
}
printf("After: pid is %d, fork return %d\n", getpid(), id);
sleep(1);
return 0;
}
执行程序:
我们看到的现象是:
打印了三行内容, 一行before, 两行after, 注意哦, 进程28187没有打印before, 这是为什么呢?
fork之前只有父进程, 父进程独立执行, fork 之后, 有两个进程, 分子两个执行流分别执行. 注意哦, fork 之后, 谁先执行并没有硬性规定, 完全由调度器决定
.
那么 fork 之后, 是否只有 fork 之后的代码是被父子进程共享的? 不是, 一般情况下, fork 之后, 父子进程共享所有的代码, 子进程执行的后续代码 != 共享的所有代码, 只不过子进程只能从这里开始执行
, 如上图所示.
因为我们知道, CPU的内部是有一些寄存器的, 其中有一个eip寄存器, 叫程序计数器, 也叫PC指针, 它是保存当前正在执行指令的下一条指令. 所以eip程序计数器会拷贝给子进程, 子进程便从eip所指向的代码处开始执行.
写时拷贝
我们知道进程是具有独立性的, 通常情况下, 父子进程代码共享, 如若父子不再写入时, 数据也是共享的, 但是当任意一方试图写入, 数据便以写时拷贝的方式各自独立
.
所以 fork 之后, OS做了什么
?
进程 = 进程的内核数据结构 + 进程的代码和数据. 所以 fork 之后创建子进程的内核数据结构(task_struct + mm_struct + 页表等), 代码继承父进程, 数据以写时拷贝的方式, 来共享或独立
.
为什么要写时拷贝?
我们说了这么多次写时拷贝, 为什么要发生写时拷贝呢, 创建子进程的时候, 就把数据分开不行吗?
当然不行, 原因有三:
- 父进程的数据, 子进程不一定会用, 即便使用, 也不一定全部写入, 所以会有浪费空间的嫌疑;
- 最理想的情况, 只有会被父子修改的数据, 进行分离拷贝, 不需要修改的共享即可, 但是这样的话, 技术角度实现复杂;
- 如果fork的时候就无脑拷贝数据给子进程, 会增加 fork 的成本, 比如内存和时间等.
所以采用写时拷贝的方式, 只会拷贝父子修改的数据, 是拷贝数据的最小成本, 但是拷贝的成本依然是存在的, 这也可以认为是一种延时拷贝策略 (只有你真正使用的时候才给你, 你想要, 但是不立马使用的空间, 先不给你, 这也就意味着可以先给别人) 变相的提高内存的使用率
.
写时拷贝本身就是由OS的内存管理模块完成的.
fork 函数的常规用法:
- 一个父进程希望复制自己, 使父子进程同时执行不同的代码段;
- 一个进程要执行不同的程序(后面程序替换时详细讲解)
fork 函数调用失败的原因:
-
系统之中有太多的进程;
- 实际用户的进程数超过了限制.
进程终止
进程退出场景
进程退出总共有三种场景:
-
代码跑完, 结果正确
; -
代码跑完, 结果不正确
; -
代码没跑完, 程序异常
.
1,2两种情况关注的是退出码, 第3种情况关注的是退出信号, 这个进程等待的时候详细介绍.
关于进程终止的正确认识:
我们之前在编写 C/C++ 代码的时候, main函数是入口函数, 进程return 0. 下面有几个问题, 看看你知道吗:
- return 0, 给谁return?
- 为何是0? 其他值可以吗?
进程代码执行完毕, 我们想要知道结果是否正确, 常用0表征成功, 用非零表征失败.
为什么用非零表征失败呢? 因为当结果错误, 我们最想知道的是失败的原因, 所以用非零标识不同的原因, main函数中, return x 代表的是进程退出码, 用来表征进程退出的信息, 让父进程读取的. 所以return 0, 是给父进程return 的
.
一般而言, 失败的非零值我该如何设置呢, 以及默认表达的含义?
错误码, 退出码可以对应不同的错误原因, 方便我们定位问题, 这个放在后面讲解.
关于进程终止的常见做法:
1.在main函数中return
, 代表进程退出, 为什么其他函数中不行呢?
其他函数中return , 代表的是函数调用结束, 函数返回.
2.在自己的代码任意地点中, 调用exit()
其中exit() 用的多.
exit() 终止进程, 刷新缓冲区;
_exit() 终止进程, 不会刷新缓冲区.
exit 函数最后也会调用_exit 函数, 但在此之前, 还做了其他动作:
关于进程终止, 内核做了什么?
进程 = 进程的内核数据结构 + 进程的代码和数据
进程终止后, 代码和数据会被释放, 当时进程的内核数据结构, 如task_struct, mm_struct等, 操作系统可能并不会释放该进程的内核数据结构, 因为用task_struct, mm_struct 这些内核数据结构创建对象的时候, 要开辟空间和初始化啥的, 这些都是需要花时间的.
了解部分:
OS将不同的数据结构全部维护到一个链表中, 空间并没有释放, 只是设置为无效, 当再次创建进程时, OS会直接从这里拿出来相关的task_struct 和 mm_struct这些内核数据结构, 由此省去了开辟空间所花费的时间, 这样一来, 只要处理新进程的代码和数据的初始化工作即可.这里会提到一个概念, 叫做内核的数据结构缓冲池(slab分派器)
练习题
1.如何使一个进程退出,以下错误的是
A.在程序的任意位置调用return
B.在main函数中调用return
C.在程序的任意位置调用exit接口
D.在程序的任意位置调用_exit接口
解析:
退出进程的方式咱们讲到了三种:
-
在main函数中return
-
主任意位置调用库函数 exit
-
在任意位置调用系统调用 _exit
2.关于进程退出返回值的说法中,正确的有
A.进程退出的返回值可以随便设置
B.进程的退出返回值可以在父进程中通过wait/waitpid接口获取
C.程序异常退出时,进程返回值为-1
D.进程的退出返回值可以在任意进程中通过wait/waitpid接口获取
解析:
进程的退出返回值也不能随意设置,因为进程的退出返回值实际上只用了一个字节进行存储,因此随意设置可能会导致实际保存的数据与设置的数据不同的情况,因为过大会导致数据截断存储;
waitpid(int pid, int *status, int options); 函数中 status参数 用于父进程获取退出子进程的返回值;
程序异常退出时,意味着程序并没有运行到return/exit去设置返回值,则返回值不做评判标准,因为返回值的存储位置的数据是一个未知随机值。