其实简单来说,进程无非就是处于运行期的程序及其相关资源的总和。这里读者应该注意“相关资源”一词,Linux 在内核中是如何记录进程的资源的呢?
2 o$ g9 N M$ q' \) m$ Y
; j+ D* x: m) {首先应该明白,Linux 内核大都是采用C语言编写的,因此要弄清楚内核如何记录进程资源,只需要查看相关的C语言代码就可以了。事实上,Linux 内核是使用 task_struct 结构体描述进程的资源的,它的C语言部分代码如下,请看:- N6 Q+ C) T U3 x5 t
- q) ^6 M8 c" t9 P
task_struct 结构体很长task_struct 结构体很长,在我手中的 Linux 内核C语言源代码中,它占用了280行。当然了,这其中包含很多条件编译部分,在 32 位机器上,task_struct 大约要占用 1.7 KB 的内存空间,不过考虑到它可以管理完整的进程,1.7kB 其实并不算大了。
1 {" Z) i+ g% h6 {. u+ X2 \
+ q" b7 \2 F; [( K鉴于 task_struct 结构体过长,这里不可能将其成员一一介绍清楚。如果读者和我一样好奇,粗略的浏览 task_struct 结构体,应该能够发现一些比较令人熟悉的成员,例如:0 p# Y/ I: X) J; ^3 k* X% P; ?
3 m4 |8 Q0 ?6 d- {$ w% P
task_struct 结构体令人熟悉的成员
% r* @. [- ?. ?- T. ~9 X
4 {% E# X6 T h3 r5 d# @' q通过C语言注释以及成员的变量名,能够看到 task_struct 结构体包含了文件系统,线程结构体,以及进程打开的文件等信息,这就与上一节文章的内容对应上了。其他成员在我之后的文章中会涉及到,这里暂不赘述。+ }5 M: }: k; }6 y" @
- a- C1 {" I& Y1 G
在创建进程时,Linux 通过 slab 分配器分配 task_struct 结构,这样可以避免动态分配和释放带来的开销,提高内存的使用效率。& X* Y6 O* p4 a5 Y) _# \+ C7 L: r
' P$ w# m7 \0 a: S( o9 D9 ^
那么创建 task_struct 结构后,内核如何访问它呢?& d4 d. L& ~! r
根据我手上的内核C语言源代码,Linux 中还有一个结构体 thread_info,它的其中一个成员 task 指针正好适合用于索引 task_struct 结构体,在X86_64平台上,thread_info 的相关C语言代码如下,请看:
* G7 s' Z; Z& Y5 t) a, f
2 @* T4 V2 x; w# Z1 htask 指针$ c& ]- |9 V% P; V C9 a5 e, S0 X
! j( z* g7 [! Z4 B7 k( Z0 C
Linux 通常会在内核栈底或者栈顶保留 thread_info 结构,而内核栈通常大小都是可知的,因此每个进程都能方便的从自己的栈中找到 thread_info 结构,进而找到 task_struct 结构。; y4 _# a/ {0 \; E+ i* A
3 h3 N5 a9 q: e7 k: z查找当前进程的 thread_info 结构,可以调用 current_thread_info() 函数,它的C语言代码如下,请看:
0 M: w& b' |5 P/ W% G/ z0 \- ?, S U- static inline struct thread_info *current_thread_info(void)) f* D* _# { K e
- {5 b$ y1 T& m# \8 H' M( c
- register unsigned long sp asm ("sp");
- i% r, i3 r4 Z9 P - return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
" y/ J n/ {+ D. |' S% A - }
复制代码 + F7 J% s9 t# G s* y8 E2 Z
8 P2 u) {$ r- [ current_thread_info() 函数
2 v, d0 O! D( s+ G5 z. w; d
1 i9 `" \5 N7 O9 ~9 _2 a8 @! [0 O! A可见,current_thread_info() 函数其实就是通过进程栈计算的,因此它的实现与平台架构有关,上述C语言代码其实只是 arm 平台的实现方法,其他平台的实现方法,读者可自行查阅。+ ]8 m0 R9 O# F) t
9 T" c# R5 ]# [) }* L
此时,要获取当前进程的资源,可以通过 current_thread_info()->task 索引。
% p" v1 j) I) ^- w
. Q3 W) V5 x1 `: q( X/ e5 R进程 PID
) Q# e' e! S; S" L5 c r Linux 内核为每一个进程分配独一无二的进程标识(process identification,PID)用于区分不同的进程。PID 是一个整数,在内核的C语言源码中表示为 pid_t 类型(其实就是 int 类型)。在Linux命令行输入 ps 命令,即可查看进程的 PID,例如:
% m+ Z+ u3 R* G x% e& F; g: [) S- V/ J' T( P* Y
查看进程的 PID
. P- B! }, `4 ?: \( i% R- m5 T `2 q
task_struct 结构体使用成员 pid 记录进程的 PID 值,相关的C语言代码如下,请看:
: E# D0 f- ~5 l6 ^
+ y. p ?) |, `! Y: e1 Ztask_struct 结构体使用成员 pid 记录进程的 PID 值
2 F4 j) M8 x4 v' O; u% ~$ G& L; L, o! G" Z8 N3 j2 J% t
在Linux系统中,PID 的最大值是可以调整的,早期为了兼容老版本的 Unix 和 Linux,默认最大值为 32768(short int 类型能够表示的最大值),这个值可以通过 cat 命令查看:- <font color="#001000"><font style="background-color:rgb(255, 255, 255)">: ?" l3 V z b J2 Z9 S( F* y
- # cat /proc/sys/kernel/pid_max32768</font></font><font color="#003000"><font face="Tahoma">* Y9 w3 M( H2 L1 M
- </font></font>
复制代码 PID 的最大值对于Linux系统的运行是有影响的,因为 PID 值是独一无二的,所以它的最大值实际上就表示系统可以同时运行的最多进程数目。对于普通的个人用户来说,32768 足够多,但是对于大型服务器来说,32768 可能就远远不够了,这时可以修改 pid_max 解决这一问题。
( _; r1 K, E! v1 f+ a7 j2 n( K! d* Q+ x% v1 a
进程的状态
4 m2 V* d* f8 H- d& j& i0 s4 b 现在知道了Linux内核是如何描述和记录进程资源,以及如何区分不同进程的了。那么进程有哪些状态呢?读者应该注意到 task_struct 结构体的第一个成员 state 了,它就是用于记录进程状态的。进程的状态在C语言源代码中是使用几个宏定义的:
5 Q4 r! h/ [. A9 E5 J
# f3 @0 m( o+ [! o" s! G9 |) m进程的状态在C语言源代码中是使用几个宏定义的
+ u1 p A c( ]. `5 o& Y3 s' D3 q' R0 s6 X# q2 \
Linux 系统中的进程必定处于这 5 种状态之一。从上到下,分别表示进程处于:$ R/ A- A; r. ]3 a& S; l
' {% b* v7 K* E1 R- Y. s. {. I9 b
! O- x( z1 Q7 V" C ^4 @- 正在运行或者准备运行
- 正在睡眠,但是可中断,接收到信号会被提前唤醒
- 正在睡眠,并且不可中断,也即即使接收到信号也不会被唤醒
- 被其他进程跟踪中
- 停止运行7 R6 K- {# c5 X, r/ j# G! G* P1 K/ [
. G% ]; j+ H6 }" c S7 D S+ q+ s" Q现在就明白有时无法通过 kill 命令杀死 D 状态的进程了,这是因为这些进程处于不响应信号的状态,kill 命令本质上是发送 SIGKILL 信号,自然无法杀死该进程。
& e" d5 @; N! M+ ?* v% C2 b5 A" L y1 F$ K
父进程和子进程
& }- k) e* J. K X4 A 进程的父进程和子进程也属于进程的资源,因此也被记录在 task_struct 结构体中,请看相关C语言代码:
3 X0 i }/ W$ i! U
2 x& {# u' g& M4 x% e& ?& I7 D
访问当前进程的父进程和子进程是方便的9 T1 C ?$ a- N% g8 A$ l
. E- D0 w# g9 X7 d0 X
所以要访问当前进程的父进程和子进程是方便的,例如:1 D/ E4 I. X! l4 c% T
- & I. T2 v1 Y$ B3 E" @. P( c
- struct task_struct *p = current->parent;
' ?9 f. @( d1 N6 B. G& b8 ` - struct task_stuck *c = current->children;
复制代码 3 |. y; k& L x% G( f+ E& Z; ]* K
稍稍思考一下,应该能够发现进程结构体 task_struct 中的 parent 指针和 children 指针其实构成了一条链表,通过这样的链表,我们能够轻易的访问进程的父进程,祖父进程…, 以及子进程,孙进程… 等。不过应该明白,对于拥有大量进程的系统来说,重复遍历所有进程的开销是很大的。
, B) a9 l; a* Q5 q" z8 A2 s2 [6 B
H6 |8 ~4 p7 e/ D! [小结1 B, F" @! Y9 a7 _* R/ r
本节先讨论了Linux内核如何记录和描述进程资源,可以看出,内核管理进程其实就是管理 task_struct 结构体。接着,通过C语言源代码查看了内核如何访问 task_struct 结构体,以及如何区分进程,最后我们一起还讨论了进程的状态和家族树,可见,Linux内核源代码也并不是神秘到不可理解。! s( t8 _: U7 D* N: g
|