Linux Process
- linux-exec
- linux-fork
- linux-exit
- linux-init
- linux-kthread
- linux-pid
- linux-task-structs
- linux-thread
- linux-idle
- linux-threadinfo
进程是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。Linux 内核通过一个被称为进程描述符的task_struct 结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sched.h文件中。
进程状态
|
|
state成员的可能取值如下
参见http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L207
|
|
5 个互斥状态
state 域能够取 5 个互为排斥的值(通俗一点就是这五个值任意两个不能一起使用,只能单独使用)。系统中的每个进程都必然处于以上所列进程状态中的一种。
| 状态 | 描述 |
|---|---|
| TASK_RUNNING | 表示进程要么正在执行,要么正要准备执行(已经就绪),正在等待 cpu 时间片的调度 |
| TASK_INTERRUPTIBLE | 进程因为等待一些条件而被挂起(阻塞)而所处的状态。这些条件主要包括:硬中断、资源、一些信号……,一旦等待的条件成立,进程就会从该状态(阻塞)迅速转化成为就绪状态 TASK_RUNNING |
| TASK_UNINTERRUPTIBLE | 意义与 TASK_INTERRUPTIBLE 类似,除了不能通过接受一个信号来唤醒以外,对于处于 TASK_UNINTERRUPIBLE 状态的进程,哪怕我们传递一个信号或者有一个外部中断都不能唤醒他们。只有它所等待的资源可用的时候,他才会被唤醒。这个标志很少用,但是并不代表没有任何用处,其实他的作用非常大,特别是对于驱动刺探相关的硬件过程很重要,这个刺探过程不能被一些其他的东西给中断,否则就会让进城进入不可预测的状态 |
| TASK_STOPPED | 进程被停止执行,当进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后就会进入该状态 |
| TASK_TRACED | 表示进程被 debugger 等进程监视,进程执行被调试程序所停止,当一个进程被另外的进程所监视,每一个信号都会让进城进入该状态 |
2 个终止状态
其实还有两个附加的进程状态既可以被添加到 state 域中,又可以被添加到exit_state域中。只有当进程终止的时候,才会达到这两种状态.
|
|
| 状态 | 描述 |
|---|---|
| EXIT_ZOMBIE | 进程的执行被终止,但是其父进程还没有使用 wait()等系统调用来获知它的终止信息,此时进程成为僵尸进程 |
| EXIT_DEAD | 进程的最终状态 |
而 int exit_code, exit_signal;我们会在后面进程介绍
新增睡眠状态
如前所述,进程状态 TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE 都是睡眠状态。现在,我们来看看内核如何将进程置为睡眠状态。
内核如何将进程置为睡眠状态。
Linux 内核提供了两种方法将进程置为睡眠状态。
将进程置为睡眠状态的普通方法是将进程状态设置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 并调用调度程序的 schedule() 函数。这样会将进程从 CPU 运行队列中移除。
- 如果进程处于可中断模式的睡眠状态(通过将其状态设置为 TASK_INTERRUPTIBLE),那么可以通过显式的唤醒呼叫(wakeup_process())或需要处理的信号来唤醒它。
- 但是,如果进程处于非可中断模式的睡眠状态(通过将其状态设置为 TASK_UNINTERRUPTIBLE),那么只能通过显式的唤醒呼叫将其唤醒。除非万不得已,否则我们建议您将进程置为可中断睡眠模式,而不是不可中断睡眠模式(比如说在设备 I/O 期间,处理信号非常困难时)。
当处于可中断睡眠模式的任务接收到信号时,它需要处理该信号(除非它已被屏弊),离开之前正在处理的任务(此处需要清除代码),并将 -EINTR 返回给用户空间。再一次,检查这些返回代码和采取适当操作的工作将由程序员完成。
因此,懒惰的程序员可能比较喜欢将进程置为不可中断模式的睡眠状态,因为信号不会唤醒这类任务。
但需要注意的一种情况是,对不可中断睡眠模式的进程的唤醒呼叫可能会由于某些原因不会发生,这会使进程无法被终止,从而最终引发问题,因为惟一的解决方法就是重启系统。一方面,您需要考虑一些细节,因为不这样做会在内核端和用户端引入 bug。另一方面,您可能会生成永远不会停止的进程(被阻塞且无法终止的进程)。
现在,我们在内核中实现了一种新的睡眠方法
Linux Kernel 2.6.25 引入了一种新的进程睡眠状态,
| 状态 | 描述 |
|---|---|
| TASK_KILLABLE | 当进程处于这种可以终止的新睡眠状态中,它的运行原理类似于 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号 |
它定义如下:
|
|
换句话说,TASK_UNINTERRUPTIBLE + TASK_WAKEKILL = TASK_KILLABLE。
而 TASK_WAKEKILL 用于在接收到致命信号时唤醒进程
新的睡眠状态允许 TASK_UNINTERRUPTIBLE 响应致命信号
进程状态的切换过程和原因大致如下图
进程标识符(PID)
|
|
Unix 系统通过 pid 来标识进程,linux 把不同的 pid 与系统中每个进程或轻量级线程关联,而 unix 程序员希望同一组线程具有共同的 pid,遵照这个标准 linux 引入线程组的概念。一个线程组所有线程与领头线程具有相同的 pid,存入 tgid 字段,getpid()返回当前进程的 tgid 值而不是 pid 的值。
在 CONFIG_BASE_SMALL 配置为 0 的情况下,PID 的取值范围是 0 到 32767,即系统中的进程数最大为 32768 个。
|
|
参见 http://lxr.free-electrons.com/source/include/linux/threads.h#L27
在 Linux 系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的 PID,并被存放在 tgid 成员中。只有线程组的领头线程的 pid 成员才会被设置为与 tgid 相同的值。注意,getpid()系统调用返回的是当前进程的 tgid 值而不是 pid 值。
进程内核栈
|
|
内核栈与线程描述符
对每个进程,Linux 内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中
- 一个是内核态的进程堆栈,
- 另一个是紧挨着进程描述符的小数据结构 thread_info,叫做线程描述符。
Linux 把 thread_info(线程描述符)和内核态的线程堆栈存放在一起,这块区域通常是 8192 Bytes(占两个页框),其实地址必须是 8192 的整数倍。
在 linux/arch/x86/include/asm/page_32_types.h 中,
|
|
出于效率考虑,内核让这 8K 空间占据连续的两个页框并让第一个页框的起始地址是 213 的倍数。
内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。
用户态进程所用的栈,是在进程线性地址空间中;
而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。
需要注意的是,内核态堆栈仅用于内核例程,Linux 内核另外为中断提供了单独的硬中断栈和软中断栈
下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自 ULK3,进程内核栈与进程描述符的关系如下图:
但是较新的内核代码中,进程描述符 task_struct 结构中没有直接指向 thread_info 结构的指针,而是用一个 void 指针类型的成员表示,然后通过类型转换来访问 thread_info 结构。
相关代码在include/linux/sched.h中
|
|
在这个图中,esp 寄存器是 CPU 栈指针,用来存放栈顶单元的地址。在 80x86 系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp 寄存器指向这个栈的顶端。一旦数据写入堆栈,esp 的值就递减。
内核栈数据结构描述 thread_info 和 thread_union
thread_info 是体系结构相关的,结构的定义在thread_info.h中
| 架构 | 定义链接 |
|---|---|
| x86 | linux-4.5/arch/x86/include/asm/thread_info.h, line 55 |
| arm | linux-4.5arch/arm/include/asm/thread_info.h, line 49 |
| arm64 | linux/4.5/arch/arm64/include/asm/thread_info.h, line 47 |
Linux 内核中使用一个联合体来表示一个进程的线程描述符和内核栈:
|
|
获取当前在 CPU 上正在运行进程的 thread_info
下面来说说如何通过 esp 栈指针来获取当前在 CPU 上正在运行进程的 thread_info 结构。
实际上,上面提到,thread_info 结构和内核态堆栈是紧密结合在一起的,占据两个页框的物理内存空间。而且,这两个页框的起始起始地址是 213 对齐的。
早期的版本中,不需要对 64 位处理器的支持,所以,内核通过简单的屏蔽掉 esp 的低 13 位有效位就可以获得 thread_info 结构的基地址了。
我们在下面对比了,获取正在运行的进程的 thread_info 的实现方式
| 架构 | 版本 | 定义链接 | 实现方式 | 思路解析 |
|---|---|---|---|---|
| x86 | 3.14 | current_thread_info(void) | return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); | 屏蔽了 esp 的低十三位,最终得到的是 thread_info 的地址 |
| x86 | 3.15 | current_thread_info(void) | ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE); | |
| x86 | 4.1 | current_thread_info(void) | (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); |
早期版本
当前的栈指针(current_stack_pointer == sp)就是 esp,
THREAD_SIZE 为 8K,二进制的表示为 0000 0000 0000 0000 0010 0000 0000 0000。
~(THREAD_SIZE-1)的结果刚好为 1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全为零,也就是刚好屏蔽了 esp 的低十三位,最终得到的是 thread_info 的地址。
进程最常用的是进程描述符结构 task_struct 而不是 thread_info 结构的地址。为了获取当前 CPU 上运行进程的 task_struct 结构,内核提供了 current 宏,由于 task_struct *task 在 thread_info 的起始位置,该宏本质上等价于 current_thread_info()->task,在include/asm-generic/current.h中定义:
|
|
这个定义是体系结构无关的,当然 linux 也为各个体系结构定义了更加方便或者快速的 current
请参见 :http://lxr.free-electrons.com/ident?v=4.5;i=current
分配和销毁 thread_info
进程通过alloc_thread_info_node函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。
|
|
其中,THREAD_SIZE_ORDER宏的定义请查看
| 架构 | 版本 | 定义链接 | 实现方式 | 思路解析 |
|---|---|---|---|---|
| x86 | 4.5 | arch/x86/include/asm/page_32_types.h, line 20 | define THREAD_SIZE_ORDER 1 | __get_free_pages 函数分配 2 个页的内存(它的首地址是 8192 字节对齐的) |
| x86_64 | 4.5 | arch/x86/include/asm/page_64_types.h, line 10 | define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER) |
进程标记
|
|
反应进程状态的信息,但不是运行状态,用于内核识别进程当前的状态,以备下一步操作
flags 成员的可能取值如下,这些宏以 PF(ProcessFlag)开头
参见
http://lxr.free-electrons.com/source/include/linux/sched.h?v4.5#L2083
例如 PF_FORKNOEXEC 进程刚创建,但还没执行。 PF_SUPERPRIV 超级用户特权。 PF_DUMPCORE dumped core。 PF_SIGNALED 进程被信号(signal)杀出。 PF_EXITING 进程开始关闭。
|
|
表示进程亲属关系的成员
|
|
在 Linux 系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。
| 字段 | 描述 |
|---|---|
| real_parent | 指向其父进程,如果创建它的父进程不再存在,则指向 PID 为 1 的 init 进程 |
| parent | 指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与 real_parent 相同 |
| children | 表示链表的头部,链表中的所有元素都是它的子进程 |
| sibling | 用于把当前进程插入到兄弟链表中 |
| group_leader | 指向其所在进程组的领头进程 |
ptrace 系统调用
Ptrace 提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心 image。
它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。
|
|
成员 ptrace 被设置为 0 时表示不需要被跟踪,它的可能取值如下:
参见
http://lxr.free-electrons.com/source/include/linux/ptrace.h?v=4.5#L20
|
|
Performance Event
Performance Event 是一款随 Linux 内核代码一同发布和维护的性能诊断工具。这些成员用于帮助 PerformanceEvent 分析进程的性能问题。
|
|
关于 Performance Event 工具的介绍可参考文章http://www.ibm.com/developerworks/cn/linux/l-cn-perf1/index.html?ca=drs-#major1和http://www.ibm.com/developerworks/cn/linux/l-cn-perf2/index.html?ca=drs-#major1。
进程调度
优先级
|
|
| 字段 | 描述 |
|---|---|
| static_prio | 用于保存静态优先级,可以通过 nice 系统调用来进行修改 |
| rt_priority | 用于保存实时优先级 |
| normal_prio | 的值取决于静态优先级和调度策略 |
| prio | 用于保存动态优先级 |
实时优先级范围是 0 到 MAX_RT_PRIO-1(即 99),而普通进程的静态优先级范围是从 MAX_RT_PRIO 到 MAX_PRIO-1(即 100 到 139)。值越大静态优先级越低。
|
|
调度策略相关字段
|
|
| 字段 | 描述 |
|---|---|
| policy | 调度策略 |
| sched_class | 调度类 |
| se | 普通进程的调用实体,每个进程都有其中之一的实体 |
| rt | 实时进程的调用实体,每个进程都有其中之一的实体 |
| cpus_allowed | 用于控制进程可以在哪里处理器上运行 |
调度策略
policy 表示进程的调度策略,目前主要有以下五种:
参见
http://lxr.free-electrons.com/source/include/uapi/linux/sched.h?v=4.5#L36
|
|
| 字段 | 描述 | 所在调度器类 |
|---|---|---|
| SCHED_NORMAL | (也叫 SCHED_OTHER)用于普通进程,通过 CFS 调度器实现。SCHED_BATCH 用于非交互的处理器消耗型进程。SCHED_IDLE 是在系统负载很低时使用 | CFS |
| SCHED_BATCH | SCHED_NORMAL 普通进程策略的分化版本。采用分时策略,根据动态优先级(可用 nice()API 设置),分配 CPU 运算资源。注意:这类进程比上述两类实时进程优先级低,换言之,在有实时进程存在时,实时进程优先调度。但针对吞吐量优化 | CFS |
| SCHED_IDLE | 优先级最低,在系统空闲时才跑这类进程(如利用闲散计算机资源跑地外文明搜索,蛋白质结构分析等任务,是此调度策略的适用者) | CFS |
| SCHED_FIFO | 先入先出调度算法(实时调度策略),相同优先级的任务先到先服务,高优先级的任务可以抢占低优先级的任务 | RT |
| SCHED_RR | 轮流调度算法(实时调度策略),后 者提供 Roound-Robin 语义,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,同样,高优先级的任务可以抢占低优先级的任务。不同要求的实时任务可以根据需要用 sched_setscheduler()API 设置策略 | RT |
| SCHED_DEADLINE | 新支持的实时进程调度策略,针对突发型计算,且对延迟和完成时间高度敏感的任务适用。基于 Earliest Deadline First (EDF) 调度算法 |
调度类
sched_class 结构体表示调度类,目前内核中有实现以下四种:
|
|
| 调度器类 | 描述 |
|---|---|
| idle_sched_class | 每个 cpu 的第一个 pid=0 线程:swapper,是一个静态线程。调度类属于:idel_sched_class,所以在 ps 里面是看不到的。一般运行在开机过程和 cpu 异常的时候做 dump |
| stop_sched_class | 优先级最高的线程,会中断所有其他线程,且不会被其他任务打断。作用:1.发生在 cpu_stop_cpu_callback 进行 cpu 之间任务 migration;2.HOTPLUG_CPU 的情况下关闭任务。 |
| rt_sched_class | RT,作用:实时线程 |
| fair_sched_class | CFS(公平),作用:一般常规线程 |
目前系統中,Scheduling Class 的优先级顺序为 StopTask > RealTime > Fair > IdleTask
开发者可以根据己的设计需求,來把所属的 Task 配置到不同的 Scheduling Class 中.
进程地址空间
|
|
| 字段 | 描述 |
|---|---|
| mm | 进程所拥有的用户空间内存描述符,内核线程无的 mm 为 NULL |
| active_mm | active_mm 指向进程运行时所使用的内存描述符, 对于普通进程而言,这两个指针变量的值相同。但是内核线程 kernel thread 是没有进程地址空间的,所以内核线程的 tsk->mm 域是空(NULL)。但是内核必须知道用户空间包含了什么,因此它的 active_mm 成员被初始化为前一个运行进程的 active_mm 值。 |
| brk_randomized | 用来确定对随机堆内存的探测。参见LKML上的介绍 |
| rss_stat | 用来记录缓冲信息 |
因此如果当前内核线程被调度之前运行的也是另外一个内核线程时候,那么其 mm 和 avtive_mm 都是 NULL
判断标志
|
|
| 字段 | 描述 |
|---|---|
| exit_code | 用于设置进程的终止代号,这个值要么是_exit()或 exit_group()系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。 |
| exit_signal | 被置为-1 时表示是某个线程组中的一员。只有当线程组的最后一个成员终止时,才会产生一个信号,以通知线程组的领头进程的父进程。 |
| pdeath_signal | 用于判断父进程终止时发送信号。 |
| personality | 用于处理不同的 ABI,参见Linux-Man |
| in_execve | 用于通知 LSM 是否被 do_execve()函数所调用。详见补丁说明,参见LKML |
| in_iowait | 用于判断是否进行 iowait 计数 |
| sched_reset_on_fork | 用于判断是否恢复默认的优先级或调度策略 |
时间
|
|
| 字段 | 描述 |
|---|---|
| utime/stime | 用于记录进程在用户态/内核态下所经过的节拍数(定时器) |
| prev_utime/prev_stime | 先前的运行时间,请参考LKML的补丁说明 |
| utimescaled/stimescaled | 用于记录进程在用户态/内核态的运行时间,但它们以处理器的频率为刻度 |
| gtime | 以节拍计数的虚拟机运行时间(guest time) |
| nvcsw/nivcsw | 是自愿(voluntary)/非自愿(involuntary)上下文切换计数 |
| last_switch_count | nvcsw 和 nivcsw 的总和 |
| start_time/real_start_time | 进程创建时间,real_start_time 还包含了进程睡眠时间,常用于/proc/pid/stat,补丁说明请参考LKML |
| cputime_expires | 用来统计进程或进程组被跟踪的处理器时间,其中的三个成员对应着 cpu_timers[3]的三个链表 |
信号处理
|
|
| 字段 | 描述 |
|---|---|
| signal | 指向进程的信号描述符 |
| sighand | 指向进程的信号处理程序描述符 |
| blocked | 表示被阻塞信号的掩码,real_blocked 表示临时掩码 |
| pending | 存放私有挂起信号的数据结构 |
| sas_ss_sp | 是信号处理程序备用堆栈的地址,sas_ss_size 表示堆栈的大小 |
其他
用于保护资源分配或释放的自旋锁
|
|
进程描述符使用计数
被置为 2 时,表示进程描述符正在被使用而且其相应的进程处于活动状态
|
|
用于表示获取大内核锁的次数
如果进程未获得过锁,则置为-1。
|
|
在 SMP 上帮助实现无加锁的进程切换(unlocked context switches)
|
|
preempt_notifier 结构体链表
|
|
FPU 使用计数
|
|
blktrace
blktrace 是一个针对 Linux 内核中块设备 I/O 层的跟踪工具。
|
|
RCU 同步原语
|
|
用于调度器统计进程的运行信息
|
|
用于构建进程链表
|
|
to limit pushing to one attempt
|
|
补丁说明请参考:http://lkml.indiana.edu/hypermail/linux/kernel/0808.3/0503.html
防止内核堆栈溢出
|
|
在 GCC 编译内核时,需要加上-fstack-protector 选项。
PID 散列表和链表
|
|
do_fork 函数
|
|
在执行 do_fork()时,如果给定特别标志,则 vfork_done 会指向一个特殊地址。
如果 copy_process 函数的 clone_flags 参数的值被置为 CLONE_CHILD_SETTID 或 CLONE_CHILD_CLEARTID,则会把 child_tidptr 参数的值分别复制到 set_child_tid 和 clear_child_tid 成员。这些标志说明必须改变子进程用户态地址空间的 child_tidptr 所指向的变量的值。
缺页统计
|
|
进程权能
|
|
相应的程序名
|
|
文件
|
|
fs 用来表示进程与文件系统的联系,包括当前目录和根目录。
files 表示进程当前打开的文件。
进程通信(SYSVIPC)
|
|
处理器特有数据
|
|
命名空间
|
|
进程审计
|
|
secure computing
|
|
用于 copy_process 函数使用 CLONE_PARENT 标记时
|
|
中断
|
|
task_rq_lock 函数所使用的锁
|
|
基于 PI 协议的等待互斥锁,其中 PI 指的是 priority inheritance(优先级继承)
|
|
死锁检测
|
|
lockdep
参见内核说明文档 linux-2.6.38.8/Documentation/lockdep-design.txt
|
|
JFS 文件系统
|
|
块设备链表
|
|
内存回收
|
|
存放块设备 I/O 数据流量信息
|
|
I/O 调度器所使用的信息
|
|
记录进程的 I/O 计数
|
|
在 Ubuntu 11.04 上,执行 cat 获得进程 1 的 I/O 计数如下:
输出的数据项刚好是 task_io_accounting 结构体的所有成员。
CPUSET 功能
|
|
Control Groups
|
|
futex 同步机制
|
|
非一致内存访问(NUMA Non-Uniform Memory Access)
|
|
文件系统互斥资源
|
|
RCU 链表
|
|
管道
|
|
延迟计数
|
|
fault injection
参考内核说明文件 linux-2.6.38.8/Documentation/fault-injection/fault-injection.txt
|
|
FLoating proportions
|
|
Infrastructure for displayinglatency
|
|
time slack values
常用于 poll 和 select 函数
|
|
socket 控制消息(control message)
|
|
ftrace 跟踪器
|
|
-
No backlinks found.