【深入理解Linux内核笔记】第三章 进程

it2026-02-13  5

进程通常定义为程序执行的一个实例。

一、进程、线程与轻量级进程

轻量级进程

Linux中常常将进程称为任务或者线程,因此在Linux中其实是没有线程这一概念的,所谓线程其实由进程来模拟实现,即我们说的轻量级进程。(但在Windows中有线程)

进程

目的:从内核的观点来看,进程的目的是担当分配系统资源(CPU时间、内存)的实体。 创建:每一个进程都只有一个父进程,它在创建时几乎与父进程相同,它接收父进程地址空间的一个逻辑拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。父子进程各自有独立的数据拷贝(堆与栈),因此父子进程对一个内存单元的修改对彼此都是不可见的。

线程

意义:每一个线程都代表进程的一个执行流。

进程是分配资源的最小单位,而线程是调度资源的最小单位。

二、进程描述符

为了管理进程,内核要对每一个进程做的事情有清楚的描述。 进程描述符中包含了许多进程属性的字段,一些字段还包括了指向其他数据结构的指针。

进程状态

进程状态分为以下几种: 可运行状态:进程正在CPU上执行或者准备执行。 可中断的等待状态:进程被挂起,直到某个条件变为真。 不可中断的等待状态:与上一个状态类似,不过有一些情况是不能被打断的,比如正在打开某一个设备文件。 暂停状态:进程的执行被暂停 跟踪状态:进程的执行被debugger 僵死状态:进程的执行被终止,但是父进程还没有返回有关死亡进程的信息,此时父进程可能还需要它。 僵死撤销状态:进程由系统删除,即上一步中父进程已经返回了死亡进程的信息。

如果父进程在子进程结束之前就结束的话,显然会出现很多永久占用RAM的情况,因此init进程被强制指定为这些进程的父进程。

进程标识符processID

Linux通过将不同的PID与系统中每个进程或轻量级进程相关联来唯一地识别系统中的每个执行上下文。 线程组:线程组中所有线程使用和该线程组的领头线程相同的PID,即组中第一个轻量级进程的PID,存入进程描述符的tgid字段中。

进程描述符内存管理

进程是一个动态实体,因此进程描述符也应该放在动态内存而不是永久分配给内核的内存区。 Linux中有一个单独为进程分配的存储区域,区域中一种数据结构叫做线程描述符,另一种是内核态的进程堆栈。一般来说这块区域的大小是两个页框(8KB),内核考虑到效率的因素,会让这块空间占据连续的两个页框。80x86体系中也可以在编译时进行设置让它可以跨越一个页框(内存碎片的影响)。 其中,esp寄存器指向这块内存栈的顶端,在数据项写入栈时,esp的值递减。由此内核也很容易从esp寄存器中获取到当前在CPU中正在运行进程的thread_info结构的地址。即便是多核处理器,也一样可以这样获取进程信息。

三、进程链表

进程链表将所有的进程描述符链接起来,它的头节点就是所谓的0进程(在完成系统加载后转而进行进程的调度、交换工作),头节点中指向下一位的指针指向最后插入的进程描述符(头插法)。

题外话:0号进程,1号进程,2号进程 Linux下有3个特殊的进程,idle进程(PID = 0), init进程(PID = 1)和kthreadd(PID = 2)

idle进程由系统自动创建, 运行在内核态

idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换

init进程由idle通过kernel_thread创建,在内核空间完成初始化后, 加载init程序, 并最终用户空间

由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程 Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。

kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有内核线程的调度和管理

它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程

可运行状态的进程链表

Linux为了提高调度程序运行速度,建立了多个可运行的程序链表,每一种进程的优先度都对应了不同的链表,优先度k取值在0到139之间,因此建立了140个表示不同优先度的链表。

散列表

内核初始化期间动态地为4个散列表分配了空间,这四个散列表分别是用来存放进程PID,线程组领头进程PID,进程组领头进程PID和会话领头进程PID的。之所以采用这样的方式,是因为顺序遍历链表来检查进程描述符的pid字段是很低效的,如果直接从hash表中查询无疑可以大大提高查询效率。 Linux对于散列冲突的解决方式是使用链表来解决,解决方式参考Java的hashmap。 形式如图:

等待队列

等待队列表示一组睡眠的进程,当某一个条件变为真时,由内核唤醒它们。 等待队列由双向链表实现,会有中断处理程序和主要内核函数对其进行修改,因此必须对双向链表进行保护以避免同时访问,保护的方式是通过等待队列头中的自旋锁完成的。 队列中有两种睡眠进程:互斥进程与非互斥进程。 互斥进程由内核有选择性地唤醒,而非互斥进程会在事件发生时全部唤醒。 比如我们在访问某一资源时,不能让所有进程一起对它进行访问的话,就需要的是互斥进程,而如果我们需要在磁盘传输完成过后唤醒进程的话,就可以全部唤醒,这些就是非互斥进程。

资源限制

每个进程都是有一组资源限制的,限制了进程能够使用的系统资源数量,也避免了用户过多使用系统资源。

四、进程切换

又叫任务切换,上下文切换。 内核为了控制进程的执行,需要有能力去挂起正在CPU上运行的进程,并且恢复以前挂起的某个进程的执行。

硬件上下文

每个进程有属于自己的地址空间,但是所有的进程是共享CPU寄存器的。在恢复一个进程之前,内核要保证每一个寄存器都装入了挂起进程时的值。这一组数据就被成为硬件上下文。 Linux中,硬件上下文一部分放在TSS段,一部分放在内核态的堆栈中。 进程切换只发生在内核态,进程切换之前用户态进程使用的所有寄存器内容都已经保存在了内核态堆栈上。

任务状态段(TSS)

当80x86的一个CPU从用户态切换到内核态时,就从TSS中获取内核态堆栈地址。 TSS中同样有I/O许可权位图,可检查进程是否有访问端口的权利。 内核会在每一次进程切换的时候更新TSS中的字段从而让CPU可以检索到它的信息,因此没有在运行的进程是不需要保存它的TSS的。

最新回复(0)