目录
创建进程
写时拷贝
fork函数
进程终止
进程终止时,操作系统都做了什么?
进程终止的常见方式有哪些?
如何使用代码终止掉一个进程?
创建进程
写时拷贝
在了解下面的内容之前,我们需要先聊一聊写时拷贝这一概念。
什么是写时拷贝呢?
通常,父进程与子进程的代码是共享的,父进程与子进程不再进行写入时,数据也是共享的,当任意一方试图写入的时候,便会以写时拷贝的方式各自生成一份副本,这项技术被称为写时拷贝。
示例:
为什么会存在写实拷贝这一项技术呢?
一般而言,创建子进程,子进程是必须要独立出去的,因为进程是具有独立性的,理论上,子进程也必须要有自己的代码以及数据。
可是,fork创建子进程,我们并没有加载的过程,这也就意味着,子进程并没有自己的代码与数据! 所以,子进程必须 “使用” 父进程的代码与数据。
代码:都是不可以被写的,进程只有读取的权限,所以说父子进程共享代码,没有问题。但是
数据:是可以被父进程或者子进程更改的,所以从这一方面来说,必须分离。
对于数据来说:
创建进程的时候,就直接进行拷贝分离,吗???可是拷贝子进程根本不会用到的空间,或者拷贝到子进程只会读取但是不会写入的空间,无疑增大了内存的负担。那么什么样的数据子进程会进行写入呢???系统根本无法提前预知哪些数据会被写入。
所以,系统选择了写实拷贝的技术,来实现父进程与子进程的数据分离,完成了进程独立性的技术保障。子进程或者父进程需要进行写入的时候,系统再进行分配空间,必要的时候拷贝原数据。这是高效使用内存的一种表现。
也可以参考程序地址空间这一概念
fork函数
#include <unistd.h> pid_t fork(void); 返回值:自进程中返回0,父进程返回子进程id,出错返回-1
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
注:fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
示例1:
示例2:
fork函数用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。
- 一个进程要执行一个不同的程序
fork函数调用失败原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
fork的两个返回值是什么?
给父进程返回子进程的pid,给子进程返回0
fork为什么会有两个返回值?
因为在实现fork这个函数的时候,在这个函数内部,return返回之前,fork函数就已经开始生效了,这也就意味着return会被执行两次,一次是原本的父进程,一次是fork在函数创建子进程的返回值。
示例:在实现fork函数的内部,就已经是两个执行流了
一个变量怎么可能同时保存不同的值?
pid_t id = fork();
因为return会被执行两次。
return的本质,不就是对id进行写入吗!
此时发生了写时拷贝!,所以父子进程各自其实在物理内存中,有属于自己的变量空间!只不过在用户层用同一个变量(虚拟地址!)来标识了。也可以参考程序地址空间这一概念
问题:fork之后,是调用fork函数这一行之后的代码开始与父进程共享?还是父进程所有的代码都与子进程共享??
注:pc:程序计数器(当前正在执行代码的下一行代码的地址)
示例:
- 1.我们的代码汇编之后,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址
- 2.因为进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行(不是最开始哦!),就要要求CPU必须随时记录下,当前进程执行的位置,所以,CPU内有对应的寄存器数据(这就是进程的上下文数据),用来记录当前进程的执行位置!
- 3.寄存器在CPU内,只有一份,寄存器内的数据,是可以有多份的!
- 创建的时候,要不要给子进程 --- 进程的上下文数据? 答案是需要的。
所以,虽然父子进程各自调度,各自会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值,就是fork之后的代码!!
进程终止
进程终止时,操作系统都做了什么?
谈到进程终止,首先我们需要明白,创建进程时,操作系统都做了些什么?
创建进程 ---> 操作系统要管理进程 ---> 创建内核数据结构test_struct ---> 创建对应的地址空间mm_struct ---> 创建页表,构建映射关系 ---> 在一定程度上,将该进程对应的代码和数据加载至内存
所以,进程终止,就是操作系统释放进程申请的相关数据结构和对应的数据与代码(本质上就是释放内存资源,也可能是CPU或者是磁盘等等,但是只会占很少的一部分,更多的还是内存)。
进程终止的常见方式有哪些?
情况示例:
a.代码跑完,结果正确
b.代码跑完,结果不正确
c.代码没有跑完,程序崩溃
老样子,谈上述前两种情况之前,先来聊一聊main函数的返回值。
好像从我们开始学习写代码的时候,老师就告诉我们,main函数的最后一行要写 return 0;可是为什么?mian函数返回的意义是什么?return 0的含义是什么? return 其他的值可不可以?
答案是这样的,main函数返回的意义是返回给上一级进程,用来评判该进程的执行结果;return 0代表的是进程的退出码;可以return其他值,并不总是0;这里0标识 sucesss,非0标识运行结果不正确。
非0值有很多,不同的非0值就可以标识不同的错误原因,这样在程序结束之后,当结果不正确时,方便我们快速定位原因。
示例:Linux下可以通过strerror函数来获取系统的错误信息。
strerror #include<string> char* strerror(int errnum);
示例1:
Linux下的运行结果:
另外,Linux下,我们也可以通过指令,来获取进程的退出码
echo $? : 获取最近一个进程执行完毕的退出码
示例2:
#include<stdio.h> #include<string.h> #include<stdlib.h> int sum(int top) { int s = 0; int i = 0; for(i = 1; i <= top; i++) { s += i; } return s; } int main() { int ret = 0; int res = sum(100); if(res != 5050) { //代码运行结果不正确 ret = 1; } return ret; }
运行结果:
当我们故意调整一下逻辑:
int sum(int top) { int s = 0; int i = 0; for(i = 1; i < top; i++) { s += i; } return s; }
运行结果:
因为函数是我们自己实现的,所以我们可以对返回值做判断,来确定程序运行的结果是正确的还是不正确的,进而设置main函数的返回值
示例3:
示例4:这里使用kill指令发送9号信号杀掉11111进程,但是系统此时是没有11111进程的
按道理来说这里的错误码应该是3啊。怎么会是1呢??? --- 因为这里的退出码叫自定义退出码,是可以自定义设置的
所以:我们自己可以使用这些退出码和含义,但是,如果你想自己定义,也可以自己设计一套退出方案!
关于情况c.代码没有跑完,程序崩溃
示例:这是经典的野指针错误
int main()
{
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
int *p = NULL;
*p = 12345; // 野指针
printf("hello world\n");
printf("hello world\n");
return 0;
}
输出结果:段错误
程序崩溃的时候,退出码无意义。一般而言退出码对应的return语句,没有被执行,即使被执行,我们也不会关心,因为我们更想知道,程序为什么会崩溃?
如何使用代码终止掉一个进程?
- 1. return语句,就是终止进程的 !
- 2. exit函数在代码的任何地方调用,都表示直接终止进程
注意: main函数内的return是终止进程,main函数调用其他函数,其他函数内部的return不是终止进程,而是return返回
main函数内的 exit函数 是终止进程,main函数调用其他函数,其他函数内部的 exit函数 依旧会直接终止进程
示例1:
int main() { printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); return 1; printf("hello world\n"); printf("hello world\n"); return 0; }
输出:
示例2:
int main()
{
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
exit(111);
printf("hello world\n");
printf("hello world\n");
return 0;
}
输出:
示例3:
#include<stdio.h> #include<string.h> #include<stdlib.h> int sum(int top) { int s = 0; int i = 0; for(i = 1; i < top; i++) { s += i; } exit(222); return s; } int main() { printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); exit(111); printf("hello world\n"); printf("hello world\n"); return 0; }
输出:
深入了解
_exit函数 :Linux下的系统接口
#include <unistd.h> void _exit(int status); 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
exit函数 :c语言库函数
NAME exit - cause normal process termination SYNOPSIS #include <stdlib.h> void exit(int status);
- 1. 执行用户通过 atexit或on_exit定义的清理函数。
- 2. 关闭所有打开的流,所有的缓存数据均被写入
- 3. 调用_exit
二者区别:
c语言库–会把缓冲区的内容打印在显示器
Linux系统接口–不会把缓冲区内容打印在显示器