该文章参考宋宝华老师的进程视频课程,详细可以去听阅码场宋老师的课程。
我们知道 进程是资源的单位,线程是调度的单位。也就是说每个进程都有自己独立的资源,独立的 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都有可能是空闲的。