一个新进程的内核之旅

一、背景

我们常在Linux平台bash环境下执行一条cmd,如看下当前文件有哪些"ls -l"。这条cmd会fork一个新的进程,然后完成ls可执行程序的加载和执行。对于用户而言,看上去仿佛就应该这样,简单快速。而对于这个新进程,在内核中经历了什么,内核表示自己默默扛下所有。本文尝试探索内核所做的工作,开始之前提出几个问题:1 fork新进程的返回值为何是0?2 fork父进程和新进程如何执行同一份代码?3 fork新进程何时被加入到调度器队列?接下来,将开始整个旅行啦!以下研究基于开源代码linux kernel-5.10/Android kernel-5.10,主要以x86架构为目标。源码可从以下链接获取:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/?h=linux-5.10.yhttps://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/stable-review/https://android.googlesource.com

二、第1站:用户态触发

在用户态调用fork,生成一个新进程(子进程),如图1。成功后,子进程的pid = 0,子进程就可以为所欲为了,当然对于bash而言,主要还是执行cmd,如ls -l。


一个新进程的内核之旅

图1.fork程序

下图就比较清晰的说明fork前父进程和子感觉像是运行一样的进程,判断pid之后,父子开始分道扬镳,至于为何pid不同,需要从内核层面找到原因。

从用户态的汇编代码可以知道:父、子进程在fork后会判断寄存器eax (x86架构)的返回值,即pid。


一个新进程的内核之旅

图2.fork子进程

三、第2站:内核fork

从用户空间调用的fork通过中断调用到内核sys_fork,继续调用到kernel_clone,该函数注意看返回的是pid_t,即进程pid。

一个新进程的内核之旅

图3.内核fork调用

截取kernel_clone关键操作,copy_process,wake_up_new_task,那就进入下一站分析。


一个新进程的内核之旅

一个新进程的内核之旅

一个新进程的内核之旅

一个新进程的内核之旅

图4.内核kernel_clone调用

更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

一个新进程的内核之旅

一个新进程的内核之旅


Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

四、第3站:copy_process进程信息拷贝操作

这一站就是fork的核心操作了,一步步探索其中的奥秘。从图5中的注释看到,copy_process函数仅做进程所拥有的各种资源复制,复制完成后,新进程并没有启动。


一个新进程的内核之旅

图5.内核copy_process函数

首先开始一系列的flag检查,其中可以关注下这个flag:SIGNAL_UNKILLABLE,在Android中init进程设置该flag,即新进程不可以作为init进程的兄弟进程,只能作为init进程的子进程,你想到了什么呢?

一个新进程的内核之旅

图6.SIGNAL_UNKILLABLE标志

1. 复制task_struct关键信息

copy_process开始复制第一个重要的资源:task_struct。


一个新进程的内核之旅


一个新进程的内核之旅

图7.内核dup_task_struct

这个结构体表示什么?还得从操作系统说起。

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。

这个结构体主要是用于内核管理任务,包括进程状态、进程标识符、进程亲属关系、ptrace系统调用、进程高度、进程地址空间、进程返回码判断标志、时间统计、信号处理、内存、文件描述符等。

以下列举几个说明:

【注】在Linux系统中,一个线程组(thread group)中的所有线程使用相同的PID,主线程PID存放在tgid成员中。getpid系统调用返回的是当前进程的tgid值而不是pid值。

继续回到dup_task_struct,主要工作有:

1分配task_struct内存2分配thread_info内存3复制父进程的task_struct信息4设置线程堆栈关于thread_info,每当进程从用户态进入内核态后都要使用栈,这个栈叫做进程的内核栈。当进程已进入内核栈,CPU就自动设置该进程的内核栈,这个栈位于内核的数据段上。为了节省空间,linux把内核栈和一个紧挨近PCB的小数据结构thread_info放在一起,占用8KB的内存空间。

在dup_task_struct中会调用alloc_thread_info分配两个页(8KB)的空闲内存(其内部调用的是一个__get_free_psages函数)。

有了task_struct结构体,后边copy_process继续复制其他资源到该结构体中。

2. 调度相关初始化

task_struct结构初始化之后,开始进行调度相关的初始化,这里仅设置任务状态和优先级,如图8。


一个新进程的内核之旅

一个新进程的内核之旅

一个新进程的内核之旅

图8.内核sched_fork

state = TASK_NEW这点表示,在调度器与进程创建的第二个逻辑交互时机,内核会调用调度器类的task_new函数,将新进程加入到相应的类的就绪队列。该函数主要工作:1初始化跟调度相关的值,比如调度实体,运行时间等2根据父进程的运行优先级设置设置进程的优先级3设置调度类rt_sched_class(实时进程)或者fair_sched_class(普通进程)

3. 复制进程主要数据

接着继续复制files、fs、signal、mm、io等数据,需要关注的有:

copy_mm:如果是进程,则将父进程的mm_struct结构复制到子进程中,然后修改当中属于子进程有别于父进程的信息(如页目录)。如果是线程,则将子线程的mm指针和active_mm指针都指向父进程的mm指针所指结构。

一个新进程的内核之旅

图9.内核复制进程数据

4. 复制线程copy_thread

copy_thread函数将子进程的ip寄存器值设置为ret_from_fork()的地址,childregsax,即新进程的返回值设置为0,即当子进程首次被调用就立即执行系统调用clone返回0。

当子进程被调度到时,子进程处于内核态。经过switch_to函数进程切换,ip设置为thread.ip,sp设置为thread.sp。ip是ret_from_fork,sp是childregs。

对于子进程而言:好像是内核态正要执行到ret_from_fork时,由于中断等原因被抢占,所以在fork执行完毕后,在随后进程调度中,新进程可能被调度到。执行完ret_from_fork中的一些代码,返回到用户态后,开始执行fork之后的代码。


一个新进程的内核之旅

一个新进程的内核之旅

一个新进程的内核之旅

图10.内核copy_thread函数

这里的设置非常巧妙,对子进程的运行现场进行伪装,形成父进程fork后,子进程执行的分叉点。

ret_from_fork函数主要完成新进程寄存器的恢复,包括SS、CS段寄存器等,然后返回用户模式。


一个新进程的内核之旅

图11.x86函数ret_from_fork函数

5. 设置全局PID和收尾工作

将子进程的PID设置为在全局namespace中分配的PID,在不同namespace中进程的PID不同,而p->pid保存的是全局的namespace中所分配的PID。

接着进行TGID、PGID、SID的设置,并加入到链表中。

一个新进程的内核之旅

一个新进程的内核之旅

一个新进程的内核之旅

图12.设置全局pid

然后进行proc文件系统、cgroup等的关联,最后返回task_struct结构体指针。


一个新进程的内核之旅

图13.关联proc文件系统

五、第4站:新进程调度运行

经过漫长的复制操作,最后回到第2站内核的fork过程,在kernel_clone中,copy_process结束后,调用wake_up_new_task函数。

这表明新进程调度相关的初始化已经完成,但是还没有被调度器加入到队列中,是不能被调度执行的。因此,该函数主要是将新进程加入到队列中。

一个新进程的内核之旅

图14.内核调用wake_up_new_task

在wake_up_new_task函数中,将进程加入到运行队列的函数为activate_task,而activate_task函数最后会调用到新进程调度类中的enqueue_task指针所指函数。

该函数会调用CFS调度类的enqueue_task_fair(普通进程),接着会调用enqueue_entity函数,完成调度中运行时间、权重等设置,最后加入CFS红黑树进程运行队列中,等待被调度执行。

一个新进程的内核之旅


一个新进程的内核之旅


一个新进程的内核之旅

图15.内核wake_up_new_task函数

六、结束语

到此,fork一个新进程内核之旅第1部分结束了,其中分析了主要的流程,实际上还有大量的细节没有分析,有兴趣的读者可以进一步深入研究。

再回顾下整个过程,如下图part1部分,最后到新进程开始运行。同时预告下part2部分内容,将分析load程序实体的ELF和so,尤其是so的加载非常的有趣和精彩。Linux内核博大精深,需要不断去探索,可谓是“路曼曼其修远,吾将上下而求索”。


一个新进程的内核之旅

图1.6.进程之旅系列


一个新进程的内核之旅

展开阅读全文

页面更新:2024-04-28

标签:内核   进程   寄存器   队列   初始化   线程   指针   函数   结构   用户

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top