|
本篇算是进程管理的的揭幕篇,简单介绍一个进程在内核眼里的来龙去脉,为接下来的进程创建,进程调度,进程管理等篇章做好学习准备。 1 ]' A; I( ^3 p
从程序到进程再到内核 啥是程序,啥是进程,一张图可以给我们解释: Y/ E- a; h( `) u# o% g
% ]! U( G7 P. [: r
我们转换镜头,站在内核OS的视角看什么是程序,什么是进程。ELF可执行文件送给内核后,OS是如何看待它的呢?换句话讲,内核OS眼里只有进程:7 s7 y3 D, `1 ~/ i9 r+ e
* l. N% F5 }% H- F6 a: K % \* H) ]8 c1 g! v0 t% R
6 P7 G, k8 l0 {: L4 P
通过 top 命令我们可以看到 linux 的各种进程(即上右图)。
# U/ p: B$ w6 [5 \3 b1 _
' f! z3 q8 t# f9 a7 ^8 d5 f5 O
内核通过 task_struct 描述进程 用命令 pstree 可以让内核以树形的结构把进程之间的关系列出来,如下图:& }8 }( W9 w0 m' F6 Y* O6 A( v
6 u* ?6 n% E, [5 l
* s% \9 f6 z/ E
2 v) u) q7 o! j5 F. @ O4 y
$ I* P( `9 ^# I/ c1 Y 这是进程在内核中的结构形式,那么内核是如何来以树形结构管理描述这些进程的呢?用来描述进程的数据结构,可以理解为进程的属性。比如进程的状态、进程的标识(PID)等,都被封装在了进程描述符这个数据结构中,一起来看下今天的主角—— task_struct 结构体。- : @+ N3 M! ^0 |
- struct task_struct {
0 L1 W/ z0 E8 _$ {% ]& C - volatile long state; //说明了该进程是否可以执行,还是可中断等信息 -1 unrunnable, 0 runnable, >0 stopped
0 E' R0 }* m2 [3 }' j. O - unsigned long flags; //Flage 是进程号,在调用fork()时给出( g* h, a6 q4 A7 \1 R- d( H& L4 D# S
- int sigpending; //进程上是否有待处理的信号2 ]% p3 F* o3 {2 N
- mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
. a( d j! C3 x' c; `; r - //0-0xBFFFFFFF for user-thead
; i9 \$ A; n, T8 {, m A k - //0-0xFFFFFFFF for kernel-thread
; \% H9 Z; h8 g6 N L# c - //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度: b/ v8 d, w3 {" O* [
- volatile long need_resched;4 ]! U* Y) y; F0 q7 ]! H
- int lock_depth; //锁深度7 K( a+ L* b$ x9 s
- long nice; //进程的基本时间片
1 V" ? V/ d) q/ a$ D' P9 g- u - //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER7 }7 Q! m0 a! j+ Q, v$ U$ b+ m
- unsigned long policy;2 ?; v \+ Z' q& g% ?' b! @
- struct mm_struct *mm; //进程内存管理信息7 g/ d. n% C2 K- n4 b, r
- int processor;
# v5 \3 Q, g/ F* w+ f( j8 l9 m6 R, O/ E - //若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新, p- ^ H! f8 \ \
- unsigned long cpus_runnable, cpus_allowed;/ x$ ^# F( r" ]
- struct list_head run_list; //指向运行队列的指针
% n' @: a& |2 W5 ~2 G0 x. n - unsigned long sleep_time; //进程的睡眠时间/ F. S J8 [3 M* c: m2 s
- //用于将系统中所有的进程连成一个双向循环链表, 其根是init_task3 }5 ^( x9 ^! l: }. ?. \
- struct task_struct *next_task, *prev_task;
2 w5 e2 {) Y8 v) |( n$ p8 c - struct mm_struct *active_mm;; N7 N4 v; B! v' [9 v
- struct list_head local_pages; //指向本地页面 9 }+ L6 @% l9 M
- unsigned int allocation_order, nr_local_pages;
% q! F! z& p( G; W - struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式+ {8 l7 t* w7 F: C
- int exit_code, exit_signal;! B- t1 V5 H6 h: p4 E% X
- int pdeath_signal; //父进程终止时向子进程发送的信号- B) D4 q" P7 ]$ {/ h
- unsigned long personality;
; ~7 W7 D. c, T/ F- P - //Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
+ K8 o2 I1 ?$ d9 Q5 b& h! J3 x - int did_exec:1;
: R+ S7 C! Q( j% A& q7 c - pid_t pid; //进程标识符,用来代表一个进程 K( w5 \6 V" [- e4 ?
- pid_t pgrp; //进程组标识,表示进程所属的进程组
+ p6 }4 j# Q# u - pid_t tty_old_pgrp; //进程控制终端所在的组标识
6 ]% W# r6 |( _ K0 K2 F( P - pid_t session; //进程的会话标识$ S! z+ v' U5 q# c$ h0 I1 c4 |5 u
- pid_t tgid;
. |' ^1 T+ |8 i ~& L - int leader; //表示进程是否为会话主管' l V5 Z7 k2 J3 `1 A
- struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
) g6 ]- t* A9 U# ]3 n0 S5 |' S - struct list_head thread_group; //线程链表
5 V# v8 n4 j9 t3 ]8 m - struct task_struct *pidhash_next; //用于将进程链入HASH表
; e/ `* h+ l, Z4 x5 M! L - struct task_struct **pidhash_pprev;
. J9 ?. P! B6 n& }% w% t - wait_queue_head_t wait_chldexit; //供wait4()使用 I3 \5 _: K6 a1 ]. A/ \$ j' ?
- struct completion *vfork_done; //供vfork() 使用/ y- X; h( d! h7 D* u& m
- unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
8 N2 C; V* W2 ?3 M ~+ J) p - struct timer_list real_timer; //指向实时定时器的指针5 r- ~! N3 J/ R* \
- struct tms times; //记录进程消耗的时间
9 Y5 O9 ~, a$ M - unsigned long start_time; //进程创建的时间
7 R O! ~; g# ~$ @% X8 |; I( t - //记录进程在每个CPU上所消耗的用户态时间和核心态时间( Y' I7 R- h6 X5 o1 n+ G
- long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; # Z0 W4 L1 w) K1 e% Y" |! _4 k0 \
- int swappable:1; //表示进程的虚拟地址空间是否允许换出/ m+ X# p. b9 ]- H9 g: C
- int ngroups; //记录进程在多少个用户组中
% y5 U3 A/ @* N" w, m1 L) L/ x - gid_t groups[NGROUPS]; //记录进程所在的组: K9 r- j1 y9 P s9 d# h, N
- //进程的权能,分别是有效位集合,继承位集合,允许位集合$ b8 D* x+ u( Y# {- }- g/ R
- kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
3 U# V: \2 @* e( T+ ?9 c - int keep_capabilities:1;, `9 K/ K2 C, |2 g- h9 Z
- struct user_struct *user;* z* @5 d& ?) t( q- }. T3 g, j/ {
- struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
0 ]; M7 H( q- G& ? - unsigned short used_math; //是否使用FPU
) n a5 Q O1 m4 `& d - char comm[16]; //进程正在运行的可执行文件名! q9 G3 l5 B+ y* I& f
- //文件系统信息
: K" ~8 S; ~* c% x - int link_count, total_link_count;
( j+ [3 o0 Q X9 ?8 L - //NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空3 j3 z( X; T5 m8 F* e5 @9 ~( Q
- struct tty_struct *tty;
4 J1 p h9 ^$ W/ a$ Y$ | - unsigned int locks;
% B1 p1 a8 ]+ s6 w$ \1 D5 z - //进程间通信信息
. [% G/ }/ x( a% l% b/ g ] - struct sem_undo *semundo; //进程在信号灯上的所有undo操作
! ^9 g8 \/ O9 H - struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
# T8 n0 T: X2 _# O. m- F& v - //进程的CPU状态,切换时,要保存到停止进程的task_struct中
| r0 U$ Y0 Z/ k- ^- J - struct thread_struct thread;9 P" h* U N& y7 Q9 k$ S \
- //文件系统信息
. F5 _8 t" C" i( {0 x% U - struct fs_struct *fs;
' z0 ?9 l6 s" I+ b8 Q6 U* G/ F - //打开文件信息" [7 b! l9 { A4 U" a3 g, r
- struct files_struct *files;
( n/ q8 g& }9 J - //信号处理函数" z9 N( Z, C. c6 B7 A, O
- spinlock_t sigmask_lock;
, S& }. V4 ^' w - struct signal_struct *sig; //信号处理函数
7 g* R3 A) n1 w1 e8 A) Z K - sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
8 W* V g; J2 S9 G2 P; z# C4 P - struct sigpending pending; //进程上是否有待处理的信号
8 E0 x& |, }5 H" W; a8 V - ......
2 ?. D. W5 f& `: A - };
复制代码 内核就是通过list_head链表把各个进程关系以树形结构管理起来的。& P$ O, R. _! `4 |. j
task_struct 结构体内容太多,这里只列出部分成员变量,感兴趣的读者可以去源码 include/linux/sched.h头文件查看。task_struct 中的主要信息分类:6 `2 h8 W s6 w7 V
1. 标示符:描述本进程的唯一标识符,用来区别其他进程。 2. 状态:任务状态,退出代码,退出信号等 3. 优先级:相对于其他进程的优先级 4. 程序计数器:程序中即将被执行的下一条指令的地址 5. 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针 6. 上下文数据:进程执行时处理器的寄存器中的数据 7. I/O状态信息:包括显示的I/O请求,分配的进程I/O设备和进程使用的文件列表 8. 记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记帐号等
3 e5 J1 x" l. w! b$ ^7 f' a
这些信息每类都可以单独开个章节去讲解,这里先简单描述下任务状态的转换,以后篇章再深入介绍各个分类。
) j5 b% q* Q' B" K$ T
任务状态转换 上面可以看到变量定义后面的注释,它说明变量内容<0是不运行的,=0是运行状态,>0是停止状态。下面我们介绍几个常用的取值:
y9 e0 r" a$ @$ d
6 Q6 ^ P* i* N, L$ B
# ~, M' i9 B6 d. H4 _+ g 5 u% T0 s6 J/ a- T+ w8 E4 U6 {0 R! G
任务状态在不同情况下的状态转换如下:4 r5 [+ Z$ ?: E
% }) \& y0 {+ d7 O# @5 N
N8 O. Y7 v" m+ V; j: I
9 u) V0 D6 s; `. x9 _; b
4 i/ c3 Y2 ~& ]/ S
内核如何存放 task_struct 我们知道一个进程所占的栈空间有用户栈和内核栈,用户栈的分布方式见之前的文章《C语言在ARM中函数调用时,栈是如何变化的?》。那么内核栈是如何存放进程描述符的呢?6 g1 f8 ]" u" d0 i
内核栈对于应用程序是不可见的,因为它位于内核空间中。在应用程序执行过程中,如果发生异常、中断或系统调用的话,应用程序会被暂停,系统进入内核态,转去执行异常响应等代码,这个时候所使用的栈就是内核栈。2 H% r+ t+ j- Q7 }4 y
% U8 i. u; o5 \- d5 D$ B0 x
. ]$ D/ t u& w8 ?
' l& X" k1 w6 ?. S
为了节省空间,linux把内核栈和紧挨着task_struct的thread_info放在一起,如上所示,thread_info中存放了进程/线程(内核不大区分进程与线程)的一些数据,其中包括指向task_struct结构的指针。数组stack即内核栈,stack占据8K/4K(依配置不同)空间。8 r9 e& W9 P; k9 j
- , x$ y3 `' O/ M0 |9 s' G7 S
- union thread_union {1 b+ C( M6 k2 g2 N7 }/ n0 s
- #ifndef CONFIG_THREAD_INFO_IN_TASK
1 y7 Z' {4 |( p+ `8 ~! c: ]! d - struct thread_info thread_info;0 ?( x; S7 c7 j' `
- #endif
( v3 x Z* D- U! J, `+ g - unsigned long stack[THREAD_SIZE/sizeof(long)];
, V8 ~- N1 f# W* ]0 X1 ~6 K q7 ? - };
复制代码
5 y$ p% |% ^9 X1 K. o
5 H! I. J: h* {
最后 到这里应该已经了解了一个程序如何转换为进程,内核如何描述进程,又如何存储进程,当然还有很多关于进程的描述没有介绍,比如进程的调度,优先级,内存管理等等,这些会在以后的文章里单独分开详细介绍。但这些所有的信息都存储在今天的主角里——task_struct。, I/ l# s8 a) I: H* S
& W. Q% y- V% Y% f |