Linux进程控制

1. fork

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

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

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

因为fork之前只有父进程,fork之后有两个进程!

那么fork之后是否只有fork之后的代码是被父子进程共享的??

我们之前认识到进程具有独立性:代码和数据必须是独立的!我们针对数据给了写时拷贝的方案,代码是只读的,所以只能共享。

一般情况,fork之后父子共享所有的代码!!

子进程执行的后续代码 != 共享的所有代码,只不过子进程只能从这里开始执行!!

为什么?

CPU有一个寄存器叫eip:程序计数器,保存当前正在执行指令的下一条指令!在某些教材也叫pc指针。eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码出开始执行啦!!

image-20220828215616873

但是不代表子进程不能从头开始,子进程可以修改eip的值从main函数开始,就能从头开始执行代码!

image-20220828213738168

2.fork之后OS做了什么?

进程 = 内核的进程数据结构 + 进程的代码和数据

通过创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进程共享或者独立!!

3.写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图

父进程创建子进程之后,父子都有虚拟地址空间和页表,只不过子进程的各种隐射关系全部继承自父进程,所以父子的代码是共享的!

但是页表里面还会包含读写属性的设置,写时拷贝的底层实现其实就是在创建子进程之后,把父子进程的页表隐射关系全部设置为只读的。

所以读取的时候就支持读,当你写入时(当然也会判断写入的内存区域是代码还是数据)。比如说子进程想写入一份数据,父进程没有写入,所以父进程不管,依旧指向自己的代码和数据。而子进程代码是共享的,所以和父进程指向的代码区域是一样的!因为子进程想写入数据,所以操作系统会把数据拷贝到另一个区域,在此区域修改!有一个细节页表数据的只读属性被去掉了变成了正常属性,最后操作系统再重新修复父子进程页表的映射关系!

image-20220829091359681

写时拷贝本身就是由OS的内存管理模块完成的!所以我们平常感知不到!

4. 操作系统为什么要写时拷贝?

我们知道进程总要保持它的独立性,那么创建子进程的时候就把数据分开不行吗?

  1. 父进程的数据,子进程不一定会用,即便使用,也不一定全部写入。—会有浪费空间的嫌疑
  2. 最理想的情况,只有会被父子修改的数据,在子进程创建的时候就进行分离拷贝,不需要修改的共享即可 — 但是从技术角度实现复杂,即使是const也只能检查直接被修改的数据,有可能存在间接被修改的数据。fork的成本就太高了!
  3. 如果fork的时候,就无脑拷贝数据给子进程,会增加fork的成本(内存和时间上)。

所以最终采用写时拷贝:只会拷贝父子修改的,变相的,就是拷贝数据的最小成本,但是拷贝的成本依旧存在。但是写时拷贝是一种延迟拷贝策略!只有真正使用的时候才给你!你想要,但是不立马使用的空间,先不给你,那么也就意味着可以先给别人!

5.fork常规用法

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

6. fork调用失败的原因

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

7.进程终止

7.1 关于终止的正确认识

C/C++的时候,main是程序的入口函数,且通常最后我们都要写一句return 0.

  1. return 0,给谁return?
  2. 为什么是0?其他值可以吗?

常见进程退出:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序异常了

所以进程代码跑完,结果是否正确:

0:表示success,非0:表示失败。最想知道的是失败的原因!所以:非0标识不同的原因!return x其实是进程退出码!表征进程退出的信息,让父进程读取的,以便对处于僵尸态的子进程进行处理!

证明:

echo $?

这条命令表示在bash中最近一次执行完毕时对应进程的退出码!

假设我们自己的程序里返回123,第一次就是123,第二次是0的原因是echo本身这个程序执行成功了!

image-20220829102622275

一般而言,失败的非零值我该如何设置呢?以及默认表达的含义?

非零值可以自定义,错误码/退出码可以对应不同的错误原因,方便定位问题!

7.2 关于终止的常见做法

  1. 在main函数中return,为什么其他函数不行呢?
  2. 在自己的任意代码任意地点中,调用exit(),括号里的就是退出码。
  3. _exit(),它和exit()很像,其实就是exit()调用了_exit()。

exit和_exit的区别:
exit:一秒之后数据就被刷新出来了,因为我们打印的时候没有加\n,数据还在缓冲区。程序退出后数据才被刷新出来。

_exit:我们唯一做的就是把exit换成_exit.我们发现程序退出数据并没有被

刷新出来。

image-20220829104754884

结论:

exit终止进程,刷新缓冲区

_exit直接终止进程,不会有任何刷新操作

7.3 关于终止,内核做了什么

进程 = 内核结构 + 进程代码 和 数据

当一个进程退出时会先进入僵尸态,然后父进程会去等待它,回收子进程信息,说白了就是读取子进程的退出码!然后再将子进程设置为x状态,就可以是否子进程的内核结构,释放加载到内存的进程代码和数据。

代码和数据必定会释放掉,内核结构(task_struct和mm_struct)操作系统可能不会释放。

我们知道创建对象要先开辟空间,再初始化。废弃的内核结构的数据已经被释放了,但是内核结构开辟的空间还在,当你再次创建进程时,会把相应的task_struct和mm_struct重新初始化,就节省了重新开辟空间的消耗!

内核的数据结构缓冲池,slab分派器。

8. 进程等待

8.1 为什么要等待?

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

8.2 如何等待

父进程调用wait就可以了,一个简单的样例:

我们在40s内杀掉子进程,此时子进程就处于z状态,40s之后我们一旦等待成功了,子进程的z状态就没了。

下面我们再写一段监控命令行脚本:

while : ; do ps ajx | head -1 && ps ajx |grep mypro |grep -v 'grep\|worker\|master\|cache'; sleep 1; echo "################################################################"; done

image-20220829113842685

运行结果:

image-20220829120900639

现在我们知道了,我们可以通过wait()的方案解决回收子进程z状态,让子进程进入x

因为x太快了所以我们什么也看不到!

8.3 waitpid

#include <stdio.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int optinos);    

pid_t:

>0:等待子进程成功,返回值就是子进程的pid

<0:等待失败

pid:

>0:是几就等待几号子进程,指定等待

-1:等待任意进程

options:

0:阻塞等待

*int status是一个输出型参数,wait/waitpid()是系统调用!!!通过调用该函数从函数内部拿出特定的数据,这里的内部是指从task_struct中拿出子进程退出的退出码!!

image-20220829123321789

8.4 int *status

int *status这个整数其实是被当作位图在用,我们只关心它的低16位。我们通过这个整数的次低8位就能拿到子进程的退出码。1-7比特位可以拿到程序异常退出的终止信号。

status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

image-20220829123511463

image-20220829125035116

所以查看退出码还可以这样写:

if(WIFEXITEL(status))
{
    printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
}

8.5 问题1

为什么不能定义一个全局变量code,子进程退出的时候就把code设置好特定的值,然后父进程回收的时候直接拿code的数据呢?

绝对不可以,因为会发生写时拷贝。

8.6 问题2

一个子进程既有退出码:0,又有退出信号:11.

那我先看谁?

常见进程退出:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序异常了

退出码对应的是前两种情况,退出信号是第3种情况!

程序正常跑完,只关心退出码。一旦进程出现异常,只关心退出信号,退出码没有任何意义!

9. 阻塞等待和非阻塞等待

如果子进程就是不退出(如死循环),怎么办呢?我的父进程只能阻塞等待。

当我调用某些函数的时候,因为条件不就行,需要我们阻塞等待,本质:就是当前进程自己变成阻塞状态,等条件就绪的时候再被唤醒!

我们今天等待的资源就不是硬件了,而是软件。一个进程在等另一个进程!

  1. 代码没跑完,程序异常了

退出码对应的是前两种情况,退出信号是第3种情况!

程序正常跑完,只关心退出码。一旦进程出现异常,只关心退出信号,退出码没有任何意义!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yuucho

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值