本文作者:源理君

Linux 是一种动态系统,能够适应不断变化的计算需求。Linux 计算需求的表现是以进程 的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,进程的管理就非常重要了。

在用户空间,进程是PID表示的。从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程。一个 PID 在进程的整个生命期间不会更改,但 PID 可以在进程销毁后被重新使用,所以对它们进行缓存并不见得总是理想的。

在用户空间,创建进程可以采用几种方式。可以执行一个程序(这会导致新进程的创建),也可以在程序内,调用一个 fork或 exec 系统调用。fork 调用会导致创建一个子进程,而 exec 调用则会用新程序代替当前进程上下文。接下来,我将对这几种方法进行讨论以便您能很好地理解它们的工作原理。

在本文中,我将按照下面的顺序展开对进程的介绍,首先展示进程的内核表示以及它们是如何在内核内被管理的,然后来看看进程创建和调度的各种方式(在一个或多个处理器上),最后介绍进程的销毁。

Linux进程表示

在 Linux 内核内,进程是由相当大的一个称为 task_struct 的结构表示的。此结构包含所有表示此进程所必需的数据,此外,还包含了大量的其他数据用来统计(accounting)和维护与其他进程的关系(父和子)。对 task_struct 的完整介绍超出了本文的范围,下面给出了 task_struct 的一小部分。这些代码包含了本文所要探索的这些特定元素。task_struct 位于 ./linux/include/linux/sched.h。

struct task_struct {
 volatile long state;
 void *stack;
 unsigned int flags;
 int prio, static_prio;
 struct list_head tasks;
 struct mm_struct *mm, *active_mm;
 pid_t pid;
 pid_t tgid;
 struct task_struct *real_parent;
 char comm[TASK_COMM_LEN];
 struct thread_struct thread;
 struct files_struct *files;
 ...
};

可以看到几个预料之中的项,比如执行的状态、堆栈、一组标志、父进程、执行的线程(可以有很多)以及开放文件。我稍后会对其进行详细说明,这里只简单加以介绍。state 变量是一些表明任务状态的比特位。最常见的状态有:

  1. TASK_RUNNING 表示进程正在运行,或是排在运行队列中正要运行。
  2. TASK_INTERRUPTIBLE 表示进程可中断休眠,也就是正在休眠,可以被叫醒;
  3. TASK_UNINTERRUPTIBLE 表示进程不可中断休眠,也就是正在休眠但不能叫醒;
  4. TASK_STOPPED 表示进程已经停止。

这些标志的完整列表可以在 ./linux/include/linux/sched.h 内找到。

flags 定义了很多指示符,表明进程是否正在被创建(PF_STARTING)或退出(PF_EXITING),或是进程当前是否在分配内存(PF_MEMALLOC)。可执行程序的名称(不包含路径)占用 comm(命令)字段。

每个进程都会被赋予优先级(称为 static_prio),但进程的实际优先级是基于加载以及其他几个因素动态决定的。优先级值越低,实际的优先级越高。

tasks 字段提供了链接列表的能力。它包含一个 prev 指针(指向前一个任务)和一个 next 指针(指向下一个任务)。

进程的地址空间由 mm 和 active_mm 字段表示。mm 代表的是进程的内存描述符,而 active_mm 则是前一个进程的内存描述符(为改进上下文切换时间的一种优化)。

thread_struct 则用来标识进程的存储状态。此元素依赖于 Linux 在其上运行的特定架构,在 ./linux/include/asm-i386/processor.h 内有这样的一个例子。在此结构内,可以找到该进程自执行上下文切换后的存储(硬件注册表、程序计数器等)。

进程创建

我们来看下如何从用户空间创建一个进程,用户态和内核态的任务机制是一样的,都是通过do_fork的函数来创建的。在本人的《fork为什么会返回“两次”》的文章中有详细的解释。




centos 如何查看PID centos根据pid查进程_linux实验五进程管理命令


从图中可以看到 do_fork 是进程创建的基础。可以在 ./linux/kernel/fork.c 内找到 do_fork 函数(以及合作函数copy_process)。

do_fork 函数首先调用 alloc_pidmap,该调用会分配一个新的 PID。接下来,do_fork 检查调试器是否在跟踪父进程。如果是,在 clone_flags 内设置 CLONE_PTRACE 标志以做好执行 fork 操作的准备。之后 do_fork 函数还会调用 copy_process,向其传递这些标志、堆栈、注册表、父进程以及最新分配的 PID。具体的实现细节就不费口舌了。

进程调度

存在于 Linux 的进程也可通过 Linux 调度程序被调度。虽然调度程序超出了本文的讨论范围,但 Linux 调度程序维护了针对每个优先级别的一组列表,其中保存了 task_struct 引用。任务通过 schedule 函数(在 ./linux/kernel/sched.c 内)调用,它根据加载及进程执行历史决定最佳进程。

进程销毁

进程销毁可以通过几个事件驱动 — 通过正常的进程结束、通过信号或是通过对 exit 函数的调用。不管进程如何退出,进程的结束都要借助对内核函数 do_exit(在 ./linux/kernel/exit.c 内)的调用。此过程如下图所示。


centos 如何查看PID centos根据pid查进程_centos 如何查看PID_02


do_exit 的目的是将所有对当前进程的引用从操作系统删除(针对所有没有共享的资源)。销毁的过程先要通过设置 PF_EXITING 标志来表明进程正在退出。内核的其他方面会利用它来避免在进程被删除时还试图处理此进程。将进程从它在其生命期间获得的各种资源分离开来是通过一系列调用实现的,比如 exit_mm(删除内存页)和 exit_keys(释放线程会话和进程安全键)。

do_exit 函数执行释放进程所需的各种统计,这之后,通过调用 exit_notify 执行一系列通知(比如,告知父进程其子进程正在退出)。

最后,进程状态被更改为 PF_DEAD,并且还会调用 schedule 函数来选择一个将要执行的新进程。请注意,如果对父进程的通知是必需的(或进程正在被跟踪),那么任务将不会彻底消失。如果无需任何通知,就可以调用 release_task 来实际收回由进程使用的那部分内存。

总结

本文篇幅的原因,并没有特别详细的讲解进程调度的内容。Linux进程调度的一直随着版本的增加,而演进。所以还需要不停的学习。