进程线程(二) fork、vfork、clone,写时拷贝(cow技术)

该文章参考宋宝华老师的进程视频课程,详细可以去听阅码场宋老师的课程。

我们知道 进程是资源的单位,线程是调度的单位。也就是说每个进程都有自己独立的资源,独立的 task_struct , 那我们如何去理解 fork 的时候,资源是怎么分割出一个新的资源呢?

linux 中著名的COW 写实拷贝 例子

子进程修改全局变量,修改完后,父进程再打印该全局变量,会变化吗,答案是不变的。

 这就是著名的Copy on write 技术,是依赖硬件MMU的,没有MMU,就不支持 fork,只支持vfork。

 当fork的时候,所有的资源都好分裂,除了 mm内存资源,

 

 这个写拷贝技术,是依赖硬件中的MMU (memoy ,management unit ),可以帮你完成虚拟地址到物理地址的转换,可以帮你控制 内存的权限 RWX,  当CPU 去访问一个虚拟地址,如果MMU没有映射它,就会产生一个page_fault,当你访问一个虚拟地址的时候,权限不对,也会产生一个page_fault,cpu 会跳到缺页中断的服务程序去执行。这个 mm 资源的分裂,一定是依赖MMU的。

 data=10 这个是数据页( R+W),当fork之后,就变成两个进程共享的页了,权限被改成了RD-ONLY,当有一方,不管是父进程,还是子进程先写,都会拿到一个新拷贝的页(谁先写,谁拿到新的copy),原来的页就留给没写的那个进程独享了,之后两个页的权限都改成了 R+W .  这里有点像 malloc 写时分配。

对于一个没有MMU的linux 而言,只有 vfork 

vfork 和 fork 的区别,就是 内存资源不再分裂,内存资源是共享的。在有MMU的系统中,也可以调用vfork, 只是 内存资源是共享,不再分裂了。

 

上面的例子,把fork 改成 vfork ,结果就变啦 

 

线程的实现 :创建一个新的 task_struct ,但是所有的资源是和原来的资源是共享的。

 

 进程是 所有的资源都不共享,线程是 所有的资源都共享。没有必要去区分进程和线程的概念,

最本质上就看两个东西,资源和调度。调度单元是线程,资源单位是进程。

 

fork的时候  clone_flags = 0

                 (p1->p2,最开始的时候,只读的方式共享资源,后面谁先改,就资源分裂)

vfork的时候  clone_flags = CLONE_VM (除了内存资源,其他资源改的时候,分裂)

pthread_create的时候  clone_flags = CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGNAL ..........  (所有的资源都有共享)

理解本质以后,全部共享资源是线程,都不共享是进程,共享一半,就是人妖。不过在工程中很少见这种代码,主要是理解进程和线程的区别。

 

 POSIX 对线程有更好的要求。POSIX 标准,要求一个进程,如果有多个线程的话,要有一个整体的进程视角来看,比如 top命令看的时候,4个线程是按照一个进程的资源来列出来的, ps 查看的时候,是把线程按照一个整体,而不是一个部分来看的。 

top -H 是按照线程的视角来看的, htop 默认就是线程视角。所以出现了一个tgid 的概念。

ps axH 加上H 就是按照线程的视角来看的。

下面的例子,p1主线程,创建了 p2 p3 p4 三个线程。则在内核里有 4个task_struct ,有4个 pid,

但是,按照进程视角,所有的线程的tgid 都是 主线程的pid 保持一致,都是 主线程的pid

 

托孤

 如果父进程先死了,子进程就会变成孤儿,等孤儿变成僵尸进程,谁会帮它清理僵尸呢?

 在3.4之前的 kernel ,只能托孤给1号进程init,在 3.4之后的kernel 多了一个 subreaper (子收割机)的概念,都是往上找最近的 subreaper ,然后托孤给它,如果找不到就托孤给1号进程。

 

 比如 ubuntu 系统,使用 pstree 查看,

 

pstree 查看(终端是通过ssh连接进来的) 

 

 此时杀掉父进程,可以看到子进程被托孤给 systemd

睡眠分为 深度睡眠 和 浅度睡眠 ,浅度睡眠可以被 [信号][ 资源] 打断,深度睡眠只能被[资源]打断。在linux 当中,一般都是用浅度睡眠,比如串口,网口数据的读取,深度睡眠一般只用在硬盘IO当中,有时也称为D状态。

 

在 linux 当中 睡眠是如何实现的呢? 睡眠只可能在内核态实现。

等资源的时候,把自己挂到等待队列上,当你去读串口,或者fifo 的时候,如果现在没有数据,不能在那儿死等啊,这是占用CPU资源的,这时候只能是 睡眠,在 kernel 中有一个数据结构就是 等待队列 wait_queue , 你要把自己的任务挂到这个等待队列上面,当有数据的时候,再把任务唤醒。比如下面的例子

对一个线程睡眠来讲,表面看起来就是调用了一个 schedule() 函数,睡过去了,醒来的时候 从 schedule()函数返回,但是已经进行了上下文切换了。

 那到底什么时候深度睡眠,什么时候浅度睡眠呢?

在linux 当中,一般都是用浅度睡眠,比如串口,网口数据的读取,深度睡眠一般只用在硬盘IO当中,有时也称为D状态。

 top命令中,idle 就是真正的空闲状态, wa就是在等硬盘的一种idle, 深度睡眠其实是一种负载,读不到硬盘,程序就走不下去。会增加top命令下的 load average

 init 也是有父进程的

 0进程就是idle进程,优先级最低的进程,当所有的进程都不跑的时候,才会跑0进程,

也就是 WFI(wait for interrupt)空闲状态,CPU低功耗状态。

这样设计的好处就是。

 大道至简,把复杂的事情简单化。

如果CPU现在跑的有10000个进程,每个进程睡眠的时候,都要判断一下自己是不是最后一个睡眠的进程,那么WFI就要和每个进程是耦合的,是极其复杂的。

 如果有一个0号进程,针对 WFI 状态,当所有的进程都睡眠的时候,就跑0号进程,0号进程就永远是最后一个进程,可以进入WFI 状态,这就解耦了

 top命令,看到 id 和 wa 为0的时候,其实就是 0号进程在跑,不过在其它列表是看不出来的

对于多核的CPU来说,每个CPU都有一个0号进程,因为每个CPU都有可能是空闲的。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值