Linux内核之进程2:进程和线程的本质

1.进程拥有资源mm,fs,files,signal…

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rt7jUg5M-1598432906561)(media/6db29a04353748366bd6d504f2999af2.png)]

fork创建一个新进程,也需要创建task_struct所有资源;实际上创建一个新进程之初,子进程完全拷贝父进程资源,如下图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-781aTvbm-1598432906567)(media/65bb20d444194988e69be17afe5bb542.png)]

比如fs结构体:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GzT7JsXi-1598432906569)(media/75921fc3caccdd4ac558cb155a8737e9.png)]

子进程会拷贝一份fs_struct,

*p2_fs = *p1_fs;

pwd路径和root路径与父进程相同,子进程修改当前路径,就会修改其p2_fs->pwd值;父进程修改当前路径,修改p1_fs->pwd;

其他资源大体与fs类似,最复杂的是mm拷贝,需借助MMU来完成拷贝;即写时拷贝技术:

2.写时拷贝技术:

#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int data = 10;

int child_process()
{
	printf(“Child process %d, data %d\n”,getpid(),data);
	data = 20;
	printf(“Child process %d, data %d\n”,getpid(),data);
	_exit(0);
}

int main(int argc, char* argv[])
{
	int pid;
	pid = fork();
	if(pid==0) {
		child_process();
	}
	else{
		sleep(1);
		printf(“Parent process %d, data %d\n”,getpid(), data);
		exit(0);
	}
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLIkS0rH-1598432906571)(media/b76e5602695e08b472a79ba4cf7f4d24.png)]

第一阶段:只有一个进程P1,数据段可读可写:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fBCT8dU5-1598432906573)(media/8cf4434a9219b60475a68cd9151a7227.png)]

第二阶段,调用fork之后创建子进程P2,P2完全拷贝一份P1的mm_struct,其指针指向相同地址,即P1/P2虚拟地址,物理地址完全相同,但该内存的页表地址变为只读;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qSVOgHXt-1598432906574)(media/38ede31c7552f6515a972b3fd05d49c2.png)]

第三阶段:当P2改写data时,子进程改写只读内存,会引起内存缺页中断,在ISR中申请一片新内存,通常是4K,把P1进程的data拷贝到这4K新内存。再修改页表,改变虚实地址转换关系,使物理地址指向新申请的4K,这样子进程P2就得到新的4K内存,并修改权限为可读写,然后从中断返回到P2进程写data才会成功。整个过程虚拟地址不变,对应用程序员来说,感觉不到地址变化。

谁先写,谁申请新物理内存;

Data=20;这句代码经过了赋值无写权限,引起缺页中断,申请内存,修改页表,拷贝数据…回到data=20再次赋值,所以整个执行时间会很长。

这就是linux中的写时拷贝技术(copy on write), 谁先写谁申请新内存,没有优先顺序;

cow依赖硬件MMU实现,没有MMU的系统就没法实现cow,也就不支持fork函数,只有vfork;

3. vfork的 mm指针直接指向父进程mm;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FAnCYyS8-1598432906576)(media/296245a1dd8d8455850d34c15abf1302.png)]

除了mm共享,其他资源全都拷贝一份,而fork是所有资源都对拷一份,对比如下图

在这里插入图片描述

不同点:vfork会阻塞:

vfork后,父进程会阻塞,直到子进程调用exit()或exec,否则父进程一直阻塞不执行;

上面代码改用vfork,打印输出10,20,20

4.线程

clone函数创建一个新进程,不执行任何拷贝,所有资源都等同vfork中的mm共享,task_struct里只有指针指向父进程task_struct;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-El08MZPr-1598432906577)(media/a84dd4fddbd28d7b7a50259f8333ca64.png)]

也就是子进程与父进程完全共享资源,但是又可以被独立调度,实际上这就是linux中的线程本质;

pthread_create()函数就是调用clone()函数(带有clone_flags)创建新task_struct,其内部mm,fs等指针全都指向父进程task_struct;

Linux中创建进程(fork,vfork)和线程(pthread_create),在内核都是调用do_fork()–>clone(),参数clone_flags标记表明哪些资源是需要克隆的,创建线程时,所有资源都克隆;

从调度的角度理解线程,从资源角度来理解进程,内核里只要是task_struct,就可以被调度;linux中的线程又叫轻量级进程lwp;

ret = pthread_create(&tid1, NULL, thread_fun, NULL);

if (ret == -1) {

perror(“cannot create new thread”);

return -1;

}

strace ./a.out

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cVHxPRhe-1598432906578)(media/0a1bcc889ade18054f6583309c5aaab8.png)]

5.人妖

如上述,资源全部共享是线程,不共享是进程;那假如修改clone函数中的clone_flags,使共享其中部分资源,如下图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHe3yTZP-1598432906579)(media/e22d8125e88fa382ff4ca2c5ae47ce07.png)]

这时候创建的既不是进程也不是线程,妖有了仁慈的心,就不再是妖,是人妖;

Linux是可以调用clone创建人妖的,不过没实际必要~

6.PID

Linux 的每个线程都会创建task_struct,会有个独立的PID;

POSIX标准规定,在多线程中调用getpid()应该获得相同的PID;

为兼容POSIX标准,linux增加了一层TGID,
调用getpid()实际上是去TGID层获取PID,TGID中PID均相同,保留了线程在内核中不同的PID,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7SHAC8Q-1598432906580)(media/093b63773b41e7ac8007aa9f52927df7.png)]

top命令看到的是进程TGID,所有线程相同;

top –H命令是从线程视角,此时的PID是task_struct中实际的PID;

7.进程死亡:

7.1子进程先死亡,父进程去清理,所谓白发人送黑发人,不清理则变成僵尸进程;

7.2
若父进程先死,子进程变成孤儿,一般托付给init,新版linux3.4引入subreaper,可以托付给中间进程subreaper。

父进程先死亡后,子进程沿tree向上找最近的subreaper挂靠,找不到subreaper,就挂在init。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ObOVANRt-1598432906581)(media/70800e4b2da69ad1fdfe17aeaa8ba16c.png)]

/* Become reaper of our children */

if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {

log_warning(“Failed to make us a subreaper: %m”);

if (errno == EINVAL)

log_info(“Perhaps the kernel version is too old (< 3.4?)”);

}

PR_SET_CHILD_SUBREAPER设置为非零值,当前进程就会变成subreaper,会像1号进程那样收养孤儿进程;

8.睡眠

当进程需要等待硬件I/O资源的时候,可以设置为睡眠状态,一般驱动做成浅度睡眠,硬盘等资源会置入深度睡眠(不会被信号唤醒);

睡眠是把task_struct挂在wait
queue上,比如多个进程都在等待串口,当串口可用时,唤醒等待队列上所有进程;

以下为《linux设备驱动开发详解》中案例注释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-maNdp7il-1598432906581)(media/8a98b726539726d0714f09028e82dbb4.png)]
注:上图有个错误,while循环中,应该为
“若非阻塞,直接退出”;
进程阻塞,将进程设置睡眠状态

当读取fifo为空即dev->current_len==0时,将进程加入等待队列睡眠,schedule()让出CPU,
fifo中写入数据时将等待队列唤醒,此函数中schedule()处继续执行;唤醒动作在write函数中执行;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1mqglwYf-1598432906582)(media/212994743340998fd93f1a8fbca39ece.png)]

唤醒后检查唤醒原因,若为IO唤醒,正常读取数据;若为信号唤醒,直接退出;

9. 0进程

0进程是唯一没通过fork()创建的进程,是系统中所有其它用户进程的祖先进程,其创建1号进程(init进程后),退化为idle进程,也叫swapper进程;

top命令中的id时间即为idle进程运行时间;

idle进程:优先级是最低的,当系统中没有任何进程运行时,即执行idle进程,idle将CPU置入低功耗模式,有任何其他进程被唤醒,idle即让出CPU;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4oerFL65-1598432906583)(media/cee63e8c6e288f0bd7eeb11bd55517cd.png)]

idle进程的设计,实际上是将“跑”与“不跑”的问题,统一为“跑”的问题。极巧妙的简化了系统设计,降低进程之间的耦合度。(将检查系统是否空闲,设置CPU低功耗模式的功能放在idle实现,其他进程都不用关心CPU工作模式)

ARM版本实现如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jefa6Qsl-1598432906584)(media/8d25a08468a3d9f43989071072f9a8d1.png)]

Wfi ==> wait for interrupt

对于用户空间来说,进程的鼻祖是init进程,所有用户空间的进程都由init进程创建和派生,只有init进程才会设置SIGNAL_UNKILLABLE标志位。

如果init进程或者容器init进程要使用CLONE_PARENT创建兄弟进程,那么该进程无法由init回收,父进程idle进程也无能为力,因此会变成僵尸进程(zombie)。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值