目录

Linux进程

进程时程序执行的一个实例,可以把它看作充分描述程序已经执行到何种程度的数据结构的汇集。在 Linux 源代码中,常把进程称为任务(task)或线程(thread)。

Linux 使用轻量级进程(lightweight process)对多线程应用提供更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开的文件等等。

进程描述符

https://raw.githubusercontent.com/xingyys/myblog/main/posts/images/20211025101011.png

进程状态

进程状态符中的 state 字段描述了进程所处的状态:

  • 可运行状态 (TASK_RUNNING) : 进程要么在 CPU 上执行,要么准备执行。
  • 可中断的等待状态 (TASK_INTERRUPTIBLE) : 进程被挂起 (睡眠),直到某个条件表为真。产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件(把进程的状态放回到 TASK_RUNNING)。
  • 不可中断的等待状态 (TASK_UNINTERRUPTIBLE) : 与可中断的等待状态类似,但有一个例外,把信号传递到睡眠不能改变它的状态。这种状态很少用到,但在一些特定的情况下(进程必须等待,直到一个不能被中断的事件发生),这种状态是很有用的。例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时会用到这种状态。探测完成以前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。
  • 暂定状态 (TASK_STOPPED) : 进程的执行被暂停。当进程接收到 SIGSTOP、SIGTSTP、SIGTTIN 或 STGTTOU 信号,进程暂停状态。
  • 跟踪状态 (TASK_TRACED) : 进程的执行已由 debugger 程序暂停。当一个进程被另一个进程监控时(例如 debugger 执行 ptrace() 系统调用监控一个测试程序),任何信号都可以把这个进程置于 TASK_TRACED 状态。

还有两个进程状态是既可以存放在进程描述符的 state 字段中,也可以存放在 exit_state 字段中。从这两个字段的名称可以看出,只有当进程的执行被终止时,进程的状态才会变为这两种状态中的一种:

  • 僵死状态 (EXIT_ZOMBIE) : 进程的执行被终止,但是,父进程还没有发布 wait4()waitpid() 系统调用来返回有关死亡进程的信息。发布 wait() 类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它。
  • 僵死撤销状态 (EXIT_DEAD) : 最终状态:由于父进程刚发出 wait4()waitpid() 系统调用,因而进程由系统删除。为了防止其他执行线程在同一个进程上也执行 wait() 类系统调用,而把进程的状态由 (EXIT_ZOMBIE) 状态改为僵死撤销状态 (EXIT_DEAD)。

标识一个进程

标识一个进程有两种方式:

  • 进程描述符指针: 进程和进程描述符之间有非常严格的一一对应关系,这使得用32位进程描述符地址标识进程成为一种方便的方式。
  • PID: PID 存放在进程描述符的 pid 字段中。

PID 被顺序编号,新创建进程的 PID 通常是前一个进程的 PID 加1。PID 存在上限,当内核使用的 PID 达到这个上限值的时候就必须开始循环使用已闲置的 PID 号。

PID 的默认最大值是32767(PID_MAX_DEFAULT-1)。可以通过修改 /proc/sys/kernel/pid_max 文件改变 PID 上限值。

内核通过管理一个 pidmap-array 位图来表示当前已分配的 PID 号和闲置的 PID 号。

进程的切换

为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行。这个中行为被称为进程切换 (process switch)、任务切换 (task switch) 或上下文切换 (context switch)。

所有进程共享 CPU 寄存器,所以在恢复一个进程的执行之前,必须确保每个寄存器装入了挂起进程时的值。

硬件上下文

进程恢复执行前必须装入寄存器的一组数据称为硬件上下文 (hardware context)。在 Linux 中,进程硬件上下文的一部分存放在 TSS 段,而剩余部分存放在内核态堆栈中。

进程切换只发生在内核态,在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上,这也包括 ss 和 esp 这对寄存器的内容 (存储用户态堆栈指针的地址)。

Linux 使用软件切换上下文

任务状态段

80x86 体系结构包括了一个特殊的但类型,叫任务状态段 (Task State Segment, TSS) 来存放硬件上下文。Linux 不使用硬件上下文切换,但是强制为每个不同CPU创建一个TSS。

执行进程切换

进程切换的核心点在于 scheduler() 函数。

从本质上说,每个进程切换由两步组成:

  1. 切换页全局目录以安装一个新的地址空间。
  2. 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含 CPU 寄存器 (重点在 switch_to() 函数)。

创建进程

Linux 中创建一个进程的方式: fork() -> sys_fork() -> do_fork() -> copy_process()

内核进程

传统的 Unix 系统把一些重要的任务委托给周期性执行的进程,这些进程只运行在内核态,称作内核线程(kernel thread),内核线程不受不必要的用户态上下文的拖累。内核线程和普通进程的区别:

  • 内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
  • 因为内核线程只运行在内核态,它们只使用大于 PAGE_OFFSET 的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用 4GB 的线程地址空间。

进程 0

所有进程的祖先叫做进程0,idle 进程或 swapper 进程,它是在 Linux 的初始化阶段从无到有创建的一个内核线程。start_kernel() 函数初始化内核需要的所有数据结构,激活中断,创建另一个叫进程1的内核线程(init进程)。

新创建的内核线程的 PID 为1,并与进程0共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init 进程开始执行 init() 函数。

创建init进程后,进程0执行 cpu_idle() 函数,该函数本质上是在开中断的情况下重复执行 hlt 汇编语言指令。只有当没有进程处于 TASK_RUNNING 状态是,调度程序才选择进程0。

在多处理器系统中,每个 CPU 都有一个进程 0。只要打开机器电源,计算机的 BIOS 就启动某一个 CPU,同时禁用其他 CPU。运行在 CPU 0 上的 swapper 进程初始化内核数据结构,然后激活其他的CPU,并通过 copy_process() 函数创建另外的 swapper 进程,把 0 传递给新创建的 swapper 进程作为它们的新 PID。此外,内核把适当的 CPU 索引赋给内核所创建的每个进程的 thread_info 描述符的 cpu 字段。

进程 1

由进程0创建的内核线程执行 init() 函数,init() 依次完成内核初始化。init() 调用 execve() 系统调用装入可执行程序 init。结果,init 内核线程变为一个普通进程,且拥有自己的每进程(per-process)内核数据结构。在系统关闭之前,init 进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。

其他内核线程

  • keventd(也被称为事件): 执行 keventd_wq 工作队列中的函数。
  • kapmd: 处理与高级电源管理(APM)相关的事件。
  • kswapd: 执行内存回收。
  • pdflush: 刷新 “脏” 缓冲区中的内容到磁盘回收内存。
  • blockd: 执行 kblockd_workqueue 工作队列中的函数。
  • ksoftirqd: 运行 tasklet; 系统中每个 CPU 都有这样一个内核线程。

撤销进程

进程终止的一般方式是调用 exit() 库函数。

进程终止

终止用户态应用的系统调用:

  • exit_group() 系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit() 是实现这个系统调用的主要内核函数。
  • exit() 系统调用,它终止某一个线程,而不管该线程所属线程组中的所有其他进程。do_exit() 是实现这个系统调用的主要内核函数。

进程删除

进程通过调用 wait() 类函数来检查子进程是否终止,在子进程已终止,但是父进程还未接收到 wait() 类函数的通知之前,子进程处于僵死状态。这时系统资源已经释放,但还占用进程描述符。

如果父进程在接收到子进程前就终止,子进程就会被init进程接管。