|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
% }8 g2 \! V/ U. k
进程是程序的一次执行, 是运行在自己的虚拟地址空间的一个具有独立功能的程序. 进程是分配和释放资源的基本单位, 当程序执行时, 系统创建进程, 分配内存和 CPU 等资源; 进程结束时, 系统回收这些资源。 进程由PCB(进程控制块)来描述:
! S! N% b' ]; k* F/ H- Y
; c7 o: q& g( Q+ ?$ p- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
4 }4 S5 F: P, w3 }0 Q ~; s 8 b$ K) g* z3 f9 a
- 进程的状态,有运行、挂起、停止、僵尸等状态。- L/ g; i" R, m( l9 G# ]+ U+ [
0 T. Y; |* x( M' r" W& |" U- 进程切换时需要保存和恢复的一些CPU寄存器。3 {$ s6 \) \1 \0 N4 P2 c
. B- D6 D2 V( e' V' {! v; T" m- 描述虚拟地址空间的信息。
' r# g- A( o) @; `2 t
8 v: p M2 C& v2 Z- 描述控制终端的信息。5 x$ c; V% O: E' S5 \2 @5 d
! l/ P" ]0 g# r2 |0 x0 \
- 当前工作目录(Current Working Directory)。
' e: Z9 N e! y% L$ g
2 b% `% i N1 R; q- a2 s- umask掩码。. P. o! Z" T# y$ }6 j
0 h% c. Y u1 q, `& y9 ]; o- 文件描述符表,包含很多指向file结构体的指针。
4 {2 B( \# j# B5 g# l2 J8 P4 w , _5 w& m4 n. X. J- v; W* r8 T
- 和信号相关的信息。
) y% h r) s' a" U2 B# Y: W 7 U9 b$ i/ e7 k3 _
- 用户id和组id。
% W3 p: P% W. h / l( a# f4 e& D, g+ U
- 控制终端、Session和进程组。/ u- X* P7 i+ K' b5 V1 k1 v# ^" P
4 @ y* x' F2 P h' V- 进程可以使用的资源上限(Resource Limit)。5 E( \8 B, ?& M5 b5 \; l
5 n7 }! g7 F* J s2 ~" x' S3 z1 |' I5 B; D2 T0 r& e7 M1 i
线程与进程, N( U) g1 F! X: s7 Y5 y
- 线程又名轻负荷进程, 它是在进程基础上程序的一次执行, 一个进程可以拥有多个线程.
- 线程没有独立的资源, 它共享进程的 ID, 共享进程的资源.
- 线程是 UNIX 中最小的调度单位, 目前有系统级调度和进程级调度两种线程调度实行方式: 系统级调度的操作系统以线程为单位进行调度; 进程级调度的操作系统仍以进程为单位进行调度, 进程再为其上运行的线程提供调度控制.
* K2 r; [* {7 x6 @/ Z H 1 r6 T) M7 F, n& U: H& @) k
守护进程:常驻后台执行的特殊进程,如sysproc init
0 T9 Z+ y. T# ~5 n4 r/ V7 r, u3 ^- O3 m
读取PID号:getpid getpgrp getppid <unistd.h> <sys/types.h>
5 N& S+ p$ k+ C! `
' q- I% l- J" \) {8 @读取用户标识号:getuid geteuid getgid getegid! h) t1 Q( I x. w8 I/ ?
' j/ R0 \' I: t" L+ i) o- e1 A' h例子:1 r3 u, c* V7 O: y$ t1 @
+ z& ]9 A$ P& O( R" P#include<unistd.h>
3 |8 ]4 s6 B0 q2 A6 D
# Y$ p7 j- c; e1 Kvoid main()
+ m, g4 p) R: K
# d$ t" G4 T# b2 }7 `8 f' w) `{
* \5 H" V& ~8 ^3 q0 B6 ~) F3 D
7 a5 R6 k" l; T' I& J/ h) g* u printf("pid=[%d], gid=[%d], ppid=[%d]\n", getpid(), getpgrp(), getppid());
, g! W8 x! v0 Y: X9 V( C a ]" R- v* q/ b6 Q
printf("uid=[%d], euid=[%d], gid=[%d], egid=[%d]\n", getuid(), geteuid(), getgid(), getegid());
2 [7 V0 P: Y% K% l
0 J) j/ V% M1 J( I& _}
0 S Y4 r/ L$ f5 v4 d3 [5 l7 u( G0 ?- E5 w
# ./id14 O# ]3 h: Q0 U, \5 k
/ r/ B2 M4 M$ t" c" f+ a- Rpid=[3311], gid=[3311], ppid=[2925]
- } X" n4 y/ i3 L$ X; E) u3 l3 I, m
uid=[0], euid=[0], gid=[0], egid=[0]+ E6 T+ k% J3 h2 i' q% i- k3 ]/ e
$ a( }+ @+ r8 u) z; c4 C* r! _ 环境变量& b" _5 R) `+ ?4 M
UNIX 中, 存储了一系列的变量, 在 shell 下执行'env'命令, 就可以得到环境变量列表.
; x0 e; c1 y2 _0 h5 s) U# d* _7 a* p" ?/ K# d+ {
环境变量分为系统环境变量和用户环境变量两种. 系统环境变量在注册时自动设置, 大部分具有特定
0 d3 H/ U3 _+ f9 @, c5 L
0 \# `" T' u/ w) |' @的含义; 用户环境变量在 Shell 中使用赋值命令和 export 命令设置. 如下例先设置了变量 XYZ, 再将其转化6 m: Q8 V' J7 u3 Q7 H1 P
N. l3 ~3 }! E, x' v# ], i
为用户环境变量: 8 @, ^% a* L" D
: C7 i$ W& |2 q; T0 u- M[bill@billstone Unix_study]$ XYZ=/home/bill
9 j1 Y* N+ h, w; E: j/ {* p4 w
9 g0 A% e; _5 J+ x* U7 p[bill@billstone Unix_study]$ env | grep XYZ $ ~/ X* C: @5 H( c7 V2 _; G( u
# Q5 L- S. V8 ~$ T# P[bill@billstone Unix_study]$ export XYZ 1 [8 R! C \+ [* ~0 M4 m
" A4 Y) `( V4 R! u( H! j
[bill@billstone Unix_study]$ env | grep XYZ
" k! E8 ^4 L5 A$ O7 u3 W: p0 c- W0 I% q O4 u
XYZ=/home/bill
/ Q7 l. W2 m8 B/ h# z
4 I; e. ?3 N$ E' U: W[bill@billstone Unix_study]$ + Q/ U1 M8 D# I# \4 o" j( W! ?' f
/ T6 S( r* c7 ^) C
UNIX 下 C 程序中有两种获取环境变量值的方法: 全局变量法和函数调用法
7 g+ k Q3 u5 i+ N4 K, a+ u" q, k& M, c5 T7 z
(a) 全局变量法7 o7 V3 E' W: j0 u2 [/ p0 B. S. H: F3 F. b
UNIX 系统中采用一个指针数组来存储全部环境值: & a% i4 a) }2 |$ ~8 x
- B; N: z; y2 I/ p! |% o) AExtern char **environ; 3 J5 a' Z, a" Q% @5 _) f* ^
. M% J( \6 K. h" P) m( p0 T8 C 该法常用于将 environ 作为参数传递的语句中, 比如后面提到的 execve 函数等.
6 I0 d- z/ ]( v: g6 \- U6 F9 J: f0 B- i7 \9 Q5 J/ F) X
1: #include <stdio.h> 3 a) p' y4 `1 c4 R1 }. c9 G8 w
2: 6 v2 N% b; U' Q% M) k7 Q
3: extern char **environ; ( B2 v: X$ \/ p( f5 `
4: 4 T7 v0 u! N6 u8 A v9 S) R# H* n3 a
5: int main()
3 f! H9 ]! q0 J& ^$ K! @1 U 6: + d, X2 ^) _+ N `1 I- X$ _: F$ b
7: { ; P2 s. m& |5 c5 e) H
8: " ~% Q& r+ N) E$ ]- \& v
9: char **p = environ;
( y! W- U2 N/ ] 10: . O$ \6 s4 k$ j3 b# s) N
11: while(*p){ ; ^; E% k9 n4 F6 C# {
12:
6 s# j( y7 ]/ Q: u4 V 13: fprintf(stderr, "%s\n", *p); P$ v. u9 G$ |
14: 0 |# n3 b0 X* X2 B2 p& y. ^
15: p++; 4 m9 h$ N: S) K
16:
$ e! [' x( T( T" z5 L* _( Q5 U$ x 17: }
+ ]: y% o! v. N2 }3 m- g 18:
: [/ q& s4 W" r, O S+ w. G 19: return 0;
% X2 U5 {8 C& T& ?6 A/ T$ i 20:
5 W! ^* U0 d+ B# I, y! p p4 G* y7 Z 21: } , U" j3 X2 y/ u1 m; P5 d; H. ?
(b) 函数调用法
- w) a8 E' [6 ]/ f% j UNIX 环境下操作环境变量的函数如下:+ b% v+ j5 b6 P, N" g# I
' @. L( d! a0 T* n9 b" b
#include <stdlib.h>2 _0 ]) C; Z; E0 M0 H0 b
char *getenv(const char *name);8 B5 u& Q2 D4 M i5 x, F
int setenv(const char *name, const char *value, int rewrite);
9 c6 T6 `# h, o! Y1 ^1 Q; }void unsetenv(const char *name);
) H9 J" s) f% D6 |. ?( q 函数 getenv 以字符串形式返回环境变量 name 的取值, 因此每次只能获取一个环境变量的值; 而且要使用该函数, 必须知道要获取环境变量的名字.
2 k* b4 T* H% i0 z$ |- d+ k( n
0 R2 _$ {$ C, f8 O1 G. V
9 g7 d5 I5 t& H9 k/ b* @在进程中执行新程序的三种方法6 B8 B2 j+ o+ k3 W; B
进程和人类一样, 都有创建, 发展, 休眠和死亡等各种生命形态.
8 n- q6 S8 \8 M6 m* W5 D2 _
. y& [8 r! \( { l+ z- `, E4 c函数 fork 创建新进程,
! h% l( D% G( E6 m) D函数exec 执行新程序,
' \( _: G" l& u- A6 i函数 sleep 休眠进程, 0 {$ q5 m$ O. k& m7 T4 u# G
函数 wait 同步进程和函数3 ?3 d! f1 u; d7 V4 ^
exit 结束进程.
0 i! {" @4 x9 x' a$ r" M创建子进程的两个用途: 1.复制代码 2.执行新程序
& N5 {* L! d1 w/ ~# x$ J1 H6 ?4 i6 P* a5 e5 O, k% B+ I( f8 o
(1) fork-exec/ a9 c0 A, s8 t- w# Y# h s* x
调用 fork 创建的子进程, 将共享父进程的代码空间, 复制父进程数据空间, 如堆栈等. 调用 exec 族函数将使用新程序的代码覆盖进程中原来的程序代码, 并使进程使用函数提供的命令行参数和环境变量去执行
, ?) t4 ?8 N! t @* D: `* ~$ I
! l" Q, p6 s) e# {6 M) P新的程序.! [* a* l. Q5 v5 {
( |" m" b6 `& W% S5 g#include <sys/types.h>
( v/ o' I# j+ C; M#include <unistd.h>
. a7 N# h& Z+ N) H% spid_t fork(void);
% K$ L2 g! j7 z1 g
" _( \- J6 R, g) |fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。fork的返回值这样规定是有道理的。fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。9 }( \# V: R) G/ e" }6 X
% r1 _( a, m5 |fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。
" [, s$ }: P# `; u! s
/ {6 d* @# P* }' S I- @7 M3 F exec 函数族有六个函数如下:1 ] Y& `. h7 X, B+ S5 I
0 m1 j9 r/ O& [ ~#include <unistd.h>* f8 g$ S. X( ^2 f6 P
Z4 O7 M1 @( @
int execl(const char *path, const char *arg0, ..., (char *)0);& H; k' k0 m; n+ P- o
( E0 {& f6 m+ p
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
# n. ]# W. c! Z* W; N/ X
$ R2 ~) z4 `* t* ~& nint execlp(const char *file, const char *arg0, ..., (char *)0);
( M& r. T2 I% ]2 a) ]( A$ L
( M1 v {. x) ~: x& jint execv(const char *path, const char *argv[]);
q8 W' D5 h. N& |6 d9 F3 o' V% [: Z! F# H! L
int execve(const char *path, const char *argv[], const char *envp[]);
3 D1 n% ~0 K( V) G( i* t( l* G: K. C$ F# ^) v g; b" @
int execvp(const char *file, const char *argv[]);) }4 }" E) w0 }, A4 s8 ^
: p4 V8 ^% l. j R3 K# Q' F
extern char **environ;) b; R6 h( N k% O9 k- C- R% M4 R
( ^. P( T. q/ b, N/ A这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
' A9 w) R) C" e% ~ H' a i* E ?! x8 \/ X1 \: U( J% @ B! F
这些函数原型看起来很容易混,但只要掌握了规律就很好记。不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out",而不能是"ls"或"a.out"。对于带字母p的函数:' a) j* @$ b+ A$ [1 X6 j
- c! x, u0 K( [2 J, Q: k% {- [5 P如果参数中包含/,则将其视为路径名。
) z2 n, m* l! \/ D' }" e5 [) \
# S( n$ T- p. u1 A1 l否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。+ y# q+ C& _ b. b2 z& r. Y% o( y
: j6 z' R ~) n! g5 z/ A1 Y带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有...,...中的最后一个可变参数应该是NULL,起sentinel的作用。对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。
$ _; H, B. Y, G4 h8 M" d* W; b$ f3 f6 B' M; { p. r
对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。! n& H! \( u& F2 x' m# b' [
5 T2 o; \/ w: vexec调用举例如下:
+ ^# w7 F" j2 r+ p: M6 ^7 H4 Z) v7 u& ]$ H* H0 f0 }8 s0 [4 H
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};4 m( @7 y( c; K" Q
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
: \8 l& W: c* Q7 Mexecl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
, p9 B2 P3 J I. {execv("/bin/ps", ps_argv);
4 [2 x" s# `4 l3 n l" Z9 pexecle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);% { y+ @3 L' V6 J7 j' N1 H1 n
execve("/bin/ps", ps_argv, ps_envp);; o8 Q9 Z7 v2 V4 N" W
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);* G# I& U4 P, r4 O4 K
execvp("ps", ps_argv);
% f7 A/ ]" R* ]' }: U5 }(2) vfork-exec
( O, K* [& @" l. S2 cvfork 比起 fork 函数更快, 二者的区别如下:
: B% N# x( L3 b. ?# Q* F
2 x7 Y% c: N- p; m6 ~$ W0 K a) vfork 创建的子进程并不复制父进程的数据, 在随后的 exec 调用中系统会复制新程序的数据到内存, 继而避免了一次数据复制过程# Q1 R! }/ }; i# M, J7 F6 b$ i' m
b) 父进程以 vfork 方式创建子进程后将被阻塞, 知道子进程退出或执行 exec 调用后才能继续运行. 当子进程只用来执行新程序时, vfork-exec 模型比 fork-exec 模型具有更高的效率, 这种方法也是 Shell创建新进程的方式.
* X8 q. j- D; }; p- I#include <sys/types.h> * d' M3 _9 H! K1 m. P0 H
8 c; @+ S. O4 Z g4 }" ^! l6 G6 _
#include <unistd.h>
( h( t$ B# z0 E) t' L1 h5 n: X5 j! N
$ V) @4 @ M3 v0 H. [$ {#include <stdio.h> 8 e( O* B) g! c2 W- A/ y r0 w) p
6 p) e2 B' J& L# N8 ], o/ a
int main() 6 b Q' J5 r: P% z: ^/ u
+ C% y+ J* j/ y( `8 t+ R
{
- l% `4 e& D. G4 i+ Q- {8 g ^6 p: Q" A& G t3 d# u7 R
pid_t pid; + z8 ~2 G: m, e$ X. E0 b
8 F7 e1 H7 v; g; F" s% H
if((pid = vfork()) == 0){ " m n( R7 s7 C8 M$ R
# Y/ u, m1 H- x( s% l3 u; t; S# C
fprintf(stderr, "---- begin ----\n");
4 `$ t2 D( M( ]' p" p- a( N
/ G3 H$ P" d: x sleep(3); 8 `( u' r. A0 Z+ h2 p9 o8 R
& ?7 d' h& G, E" E& n( P' r execl("/bin/uname", "uname", "-a", 0); 6 ]5 t: G( T% R. ]" U
5 x8 d1 m4 `6 W! Y" A [
fprintf(stderr, "---- end ----\n");
' X* M$ E+ M4 P, ^' u, z
1 h$ d' |2 E8 d3 P$ a% { } 4 q* p. O; ]9 E. l) |
5 h# t# @8 z8 z: ~& i else if(pid > 0) * x _5 v& M6 u# }3 z& w9 p
8 X3 x" h% g8 i+ ?6 ^3 x# T) E fprintf(stderr, "fork child pid = [%d]\n", pid); ( Q/ ~2 ^, H3 P5 y8 `
q7 f6 q6 R4 m4 ^4 c. j else " l) [+ V8 v% O- \* I) f% s
. i/ m! u& H, y2 R fprintf(stderr, "Fork failed.\n");
+ Q4 G: P& l. f F: r; ^7 b2 f9 u4 L, Y! P, g6 N* s9 `! }
return 0;
; W3 ^& I1 o% w6 [1 q# T7 B2 f; E& J4 |, B" h6 @$ G& U8 S
}
& L0 D1 i0 t8 K+ U
. j3 I* p9 \+ B% Z( Q3 E[bill@billstone Unix_study]$ make exec2 ( y1 I% T* g! z& i8 z
n" S0 E& Z% j E8 L7 w
make: `exec2' is up to date. ( E/ F) N: X" r/ m
; y3 O4 r G; E8 f. `
[bill@billstone Unix_study]$ ./exec2
: N8 B0 t5 ?6 e/ D( n H/ p6 P5 y* n8 p: d) g; h, s
---- begin ---- ) N( A* D' K( a& _7 a
$ N) F+ p6 N8 m$ J1 sfork child pid = [13293] $ S$ s3 O; L9 f7 K( H$ F
% H7 Q7 z- s. T
[bill@billstone Unix_study]$ Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux
" Z) \6 i- U0 S (3) system, L/ B$ S8 l8 V" K+ i- J% H/ g) Q/ D, W. v
在 UNIX 中, 我们也可以使用 system 函数完成新程序的执行.
- V |( r% v# E( a6 h, E' E9 P+ p. U( W3 r* e: k4 ~
函数 system 会阻塞调用它的进程, 并执行字符串 string 中的 shell 命令. & X7 O4 z) \0 X6 J* u3 y7 O: f# r f9 i
E3 L$ ]! f9 n; c1 U5 C. B1 K7 ]
[bill@billstone Unix_study]$ cat exec3.c + Z% S, h4 `: c9 ]! p3 |1 u g
) }# W, u5 h7 f/ ]: T4 V2 ^1 @" _3 _2 q
#include <unistd.h> * B% f' J a" A, ^0 @5 Q8 T8 B' W( @7 h
4 g1 }. L: e. W. B4 w
#include <stdio.h> ( }& l* T4 f, i
5 p! I: U0 W1 s8 {7 O
int main() ' f7 |) e) U) E7 u# N7 U; e& Y- M
6 o! i1 b$ D. E3 Q0 ^3 e0 k* }4 e0 r
{
$ Z& v* i7 e, ^: ?1 ~3 |/ J7 Z! f) z- p1 p
char cmd[] = {"/bin/uname -a"};
* W$ ^) W3 J9 R' I& ] ^" x
' d( K& Y: B6 u8 [, D; p# W! w system(cmd); " P! o# k, f# |0 {3 V' E9 m7 G
5 b: h( i' q+ s6 a9 d- R" ?; u( l return 0; / b: Y' D: a: E+ O. r
# W8 S, y, g" B2 Q' o; f* D}
6 K. r# J- W! {" H2 J% y/ r3 p, O1 \$ C. C, K3 }. T
[bill@billstone Unix_study]$ make exec3
! m. M0 Y4 g: Z5 X$ G# E7 k) @& ~% M4 M, ] `
cc exec3.c -o exec3 4 Z' y% Q! O9 a( Q5 R
2 J2 Q5 ], Q+ w; h[bill@billstone Unix_study]$ ./exec3
& z% e5 P) C Y8 C2 ]) ?" Q9 n# x. |' Y% J+ M' [! w
Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux 2 V3 d& {3 H& X$ o+ i6 G) K! p" `
* s$ C" w. I* B6 h
# t$ @7 D- A4 r. |
( F! H7 o3 @" v
进程休眠:sleep: K9 k$ S3 B3 w' n* g
进程终止:exit abort. M, ?% ^' h& m: K& K
进程同步(等待):wait
N& T$ r, N: |3 [2 }& R; n一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
- w1 ?( l& X: A6 c0 a9 `9 h. ~+ K/ _# E8 |
如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。
* Q6 N0 F: F1 k: i
# W5 q$ T3 R) |' B K$ cps -ef | grep 13707 ' p, q1 O `, H' p8 I. R
' }0 y" n# E1 a. Y: {* q* L
bill 13707 1441 0 04:17 pts/0 00:00:00 ./szomb1 % Y4 X; w9 g6 B& l
& O5 `( n+ {3 B1 h& Y; obill 13708 13707 0 04:17 pts/0 00:00:00 [szomb1 <defunct>] // 僵死进程 1 b/ B: o6 t% P- L* Z. _6 I; h8 }2 j
# ]' G. U. ]% b, tbill 13710 1441 0 04:17 pts/0 00:00:00 grep 13707
3 H5 I- |5 ]" |. c4 i2 A$ {! j' Q& w4 {& x
[bill@billstone Unix_study]$
( \- K6 Y+ f7 F8 b7 H4 a4 `7 S/ U Y% m: l t
其中, 'defunct'代表僵死进程. 对于僵死进程, 不能奢望通过 kill 命令杀死之, 因为它已经'死'了, 不再接收任何系统信号.
& V0 F% }8 B9 p2 D% V
. F( L. R8 |, [8 r) P4 h# a3 f 当子进程终止时, 它释放资源, 并且发送 SIGCHLD 信号通知父进程. 父进程接收 SIGCHLD 信号,调用wait 返回子进程的状态, 并且释放系统进程表资源. 故如果子进程先于父进程终止, 而父进程没有调用 wait接收子进程信息,则子进程将转化为僵死进程, 直到其父进程结束. 5 p G1 Y$ e. K; f* i
" Z* A6 [2 \8 d% F, g: X# i
一旦知道了僵死进程的成因, 我们可以采用如下方法预防僵死进程: & N* L6 F/ P8 H- U
( l* Q! a- L5 a, d+ V
(1) wait 法 : g5 C) i2 x5 q$ O( f
% J" Q( Q1 L% c0 O# ]9 k
父进程主动调用 wait 接收子进程的死亡报告, 释放子进程占用的系统进程表资源. ! e/ }; h, D* ?6 N* e1 X4 r0 J* @
/ j9 s4 h, F. v/ ^+ O
(2) 托管法 7 U! ^) C5 Q0 @9 H
+ s: j0 `# p& \5 Y+ l 如果父进程先于子进程而死亡, 则它的所有子进程转由进程 init 领养, 即它所有子进程的父进程 ID 号变为 1. 当子进程结束时 init 为其释放进程表资源. 5 | Y& K2 |% s5 _ |$ ?
; y. y( E2 r0 F3 L9 d$ ~0 }
托管法技巧:两次fork,子进程退出,则子子进程的父进程变为init。' R( v( d' i) Q% K8 W7 x
9 T0 F8 u9 K/ ]2 e9 N (3) 忽略 SIGC(H)LD 信号
0 t7 \; v% c; ]* |* i% |# N9 ]) z0 k3 \0 p
当父进程忽略 SIGC(H)LD 信号后, 即使不执行 wait, 子进程结束时也不会产生僵死进程. . H9 h0 u6 T3 c% U
5 ]$ G" ?7 e4 Y/ F% b) x& J( _( U (4) 捕获 SIGC(H)LD 信号
7 {5 p) m2 w/ w& r" p
4 D1 o9 U' V* @7 L7 U 当父进程捕获 SIGC(H)LD 信号, 并在捕获函数代码中等待(wait)子进程
# g. E1 Y; m$ z% h4 R5 }$ _, h4 I6 y( C& |' @) q6 O$ E
wait和waitpid函数的原型是:
7 e' ?( E2 J* l# f
_/ I& d/ N) S7 _% `# i#include <sys/types.h>5 }' b+ p% h z
#include <sys/wait.h>
* x q# z, @" w ^
0 m3 ] g" ~# T+ a5 p0 jpid_t wait(int *status);+ T) u F9 B! L6 I
pid_t waitpid(pid_t pid, int *status, int options);
/ ?% P' t/ P! D7 x若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会:, C' B) r' t/ g1 J$ e! ]/ B) r) X
% ]# k7 P# K- c. V% j1 N阻塞(如果它的所有子进程都还在运行)。
* H: r/ W7 J ~3 M, a
3 H3 T" x5 ^ e1 D4 Z2 f9 ~带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
) y0 U, N7 b; d+ w 1 `) w, @* W: O5 _/ l+ Y9 `$ S
出错立即返回(如果它没有任何子进程)。6 G3 ^5 [: [) {5 ]" P- }
7 \. L! d# i, K这两个函数的区别是:2 P4 n3 i7 R. t T; C8 t" W
/ ~0 H& u* L& _4 I9 s如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
! }0 L+ o/ j# Z$ h5 Q' E5 t9 \7 U , }) F$ b2 t9 i+ ~8 g, D
wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
3 r) O% c: C7 @! R
$ J6 o" ]" ~2 ^5 M+ j% ^可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。如果参数status不是
7 g( I! }0 c }$ Z+ U6 n4 ^; Z3 B
% M0 A) }$ ^" o& s% g空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数指定为NULL。
% O- b' {1 f9 z, l
; C% E# j/ h/ C( ^8 L1 H2 { 0 m: X5 B; w0 U4 @, A9 }
例 30.6. waitpid
1 |& C+ `3 w/ ?; h- k/ X
( o; J) m3 U# N8 z! k#include <sys/types.h>2 R3 W( t, ^2 n( S% e# V! g! ^# W
#include <sys/wait.h>' |! f- L' p3 l
#include <unistd.h>
' }6 E6 @- c c+ E#include <stdio.h>! m" a' [/ @6 |' g1 s# W3 C( K8 r* n( j
#include <stdlib.h>+ A6 G$ X1 s6 b* h2 I! M6 W
2 l ?* ?- B. F+ H% @ r
int main(void)
* O2 j [/ N, I6 b2 @/ S/ k3 u& t{/ B( k+ @( ?. j: X, W. l
pid_t pid;, j; R6 y p8 R; j; Q" b
pid = fork();; y. K/ T( l( y4 [0 ^7 Z8 j
if (pid < 0) {
g% L' E6 ~: x) L7 W) p perror("fork failed");. A9 D) i; _$ E' E* ~- o0 d
exit(1);' z8 Q _. I1 d; x- y% U) A9 I. A
}7 m6 h4 |. z c# g y* q
if (pid == 0) {
* h: {: Y1 e. Y, G% b int i;% f; V! k2 v1 H2 M( ]% A
for (i = 3; i > 0; i--) {
1 V# \1 r+ O5 {: n3 Y printf("This is the child\n");, G+ s b `( D
sleep(1);" L, c& O* X# W# c: B$ ?, I
}
4 Z$ Y9 D1 _- @8 F" g# a' r; c exit(3);9 j: T1 ]7 g- w# _7 T$ f2 N
} else {3 |& @+ C+ K% E2 ?% I
int stat_val;, m( ?6 K+ o8 p- f$ ]- b& _1 }
waitpid(pid, &stat_val, 0);
: F; B+ `. J0 G, M, ?! f! k5 t# n if (WIFEXITED(stat_val))( U8 j% K, r, e3 e O' u
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));1 N0 g% R }4 w1 R4 D9 k
else if (WIFSIGNALED(stat_val))3 l% g: i8 M& ^8 N* Q+ ~8 w
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
) t# Z9 G0 [" b4 d+ b }% w- A1 _& a* Z! d9 R
return 0; j. \; n5 ^" G: p
}
8 m6 n3 p' m4 f3 `8 p/ ] % b) L1 f" p8 w$ [ W2 n& U8 @# T
子进程的终止信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段:如果子进程是正常终止的,WIFEXITED取出的字段值非零," N! Y8 x0 Z- ^: g* n: k; S
, j6 Y2 c1 X0 u1 Y
WEXITSTATUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WIFSIGNALED取出的字段值非零,WTERMSIG取出的5 a/ K: C4 ^* b9 k
0 s- U/ z+ b" v. [, d字段值就是信号的编号。作为练习,请读者从头文件里查一下这些宏做了什么运算,是如何取出字段值的。* `7 D d1 i" u
; [( v, k+ E3 H% K# Z 守护进程9 t9 P" R3 s) w& t8 z
所谓守护进程是一个在后台长期运行的进程, 它们独立于控制终端, 周期性地执行某项任务, 或者阻塞直到事件发生, 默默地守护着计算机系% [0 }& ^. L% x( ]5 U) W9 K
1 O* [# e3 ]+ k& t7 O
统的正常运行. 在 UNIX 应用中, 大部分 socket 通信服务程序都是以守护进程方式执行.
9 u( P- N- ^$ A
o5 k. ~. \# v1 m3 i 完成一个守护进程的编写至少包括以下几项:
4 {4 g5 r& I" w3 g, D3 | N" ?5 [: V! N+ M8 b1 N8 D6 T
(1) 后台执行
+ W4 C+ j+ Z u8 ]9 o& \& k: [: o2 t0 r" |
后台运行的最大特点是不再接收终端输入, 托管法可以实现这一点
3 F' Y' R" [1 h. d$ C1 `& V1 W! x" B% J S& T. L) w
pid_t pid; - v1 r2 s: B- k5 E* e+ }$ J. p9 _
5 @5 t; v2 G# j; a2 i
pid = fork();
( w& r0 U* E* D6 C
% h* m- d1 ]% {" T; S8 ]' G1 i: pif(pid > 0) exit(0); // 父进程退出 . O& |8 J& s! ]0 W! Z
; B3 h6 Z O; g% F h# ]+ W# M
/* 子进程继续运行 */ ' _# ~$ e7 `' i1 F) Q- S
$ A) B2 n, o7 p9 g; I2 T
父进程结束, shell 重新接管终端控制权, 子进程移交 init 托管 " m: Z- e8 x! R, b. g
; o( L: X8 Z; \& u( @( `* k (2) 独立于控制终端
) i. W7 |1 I) u# Q
1 [, [# S, j+ w( \3 M! c 在后台进程的基础上, 脱离原来 shell 的进程组和 session 组, 自立门户为新进程组的会话组长进程, 与原终端脱离关系
; Y$ X( A+ |: R' I
" t* i; o4 A& u+ \7 A#include <unistd.h>
3 a/ _. {4 ^! {) R
( P' j: m& b0 M" i" h! {pid_t setsid(); 0 L) L- i% K$ ]( P: I: f1 D
$ M5 y3 R- I# N. o n7 \* T) u
函数 setsid 创建一个新的 session 和进程组.
! O* ^% R' X, Q6 V' q+ G6 x
# ?3 H7 R+ z' L1 [/ S. G8 F (3) 清除文件创建掩码 7 v5 @- z. ]& A1 t
* C0 f/ v; w7 B; F 进程清除文件创建掩码,代码如下: ( K: i+ T/ w5 m) @! P; ^0 V$ l
, F T1 }& q$ I. ~umask(0); : f) l# v' ]/ \( \
" Z) B+ l _- t! A0 X4 T; R$ |8 \
(4) 处理信号 5 F8 k3 Y& }9 R) u+ K
& ] B5 L* _8 s) A+ x. w$ m 为了预防父进程不等待子进程结束而导致子进程僵死, 必须忽略或者处理 SIGCHLD 信号, 其中忽略该信号的方法为: " S: x$ M+ V$ ^8 \8 \
1 [. V2 }, J V+ ]) csignal(SIGCHLD, SIG_IGN); . ~- m1 U5 k; [% r
1 m" S8 m/ L$ J& j7 A4 X" b
守护进程独立于控制终端, 它们一般以文件日志的方式进行信息输出. Syslog 是 Linux 中的系统日志管理服务,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”。该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可记录到一个文件中。 该机制提供了 3 个 syslog 函数,分别为 openlog、syslog 和 closelog。& X$ d" e. A6 j8 C7 X* c4 V
! D: z* T, g7 g2 t1 w% g
下面是一个简单的守护进程实例 InitServer / g1 r$ m) |6 _5 v! f# r' t2 i
, O/ }+ y7 ^! G2 a
[bill@billstone Unix_study]$ cat initServer.c ; c8 o% n% H3 h& `. N7 K; l9 o
9 V4 C8 v9 `$ U/ E2 x( f( m: Y 1: #include <assert.h> . u' j7 V; U* j1 I; \: f8 c
2:
+ }2 j8 T, b9 t" o% V 3: #include <signal.h>
- e2 ]! q- @+ z; Z } 4: 9 _5 V# M3 I! S: V" u& L5 K
5: #include <sys/wait.h>
P2 g* C' s: q9 Y3 d' c, A 6: % S# H% w2 i5 { q8 J0 d) }0 }; x: n
7: #include <sys/types.h>
: v4 s1 v2 G( C) M 8: * N2 e- L: @0 [ |$ H: F
9: void ClearChild(int nSignal){ 8 P9 S7 g( I1 [% m
10:
5 H& }% x+ H6 t7 ?; ?$ j 11: pid_t pid;
3 M8 i! r8 S+ y4 u 12: 1 `& t3 c% J3 x$ o1 O. H- S9 r- _8 P
13: int nState;
5 y0 `% f7 ^$ ]) _' E8 n. f 14:
8 n+ L3 g% {$ i e, A- | 15: // WNOHANG 非阻塞调用 waitpid, 防止子进程成为僵死进程 3 D) G2 B6 N3 G+ {
16: - C7 b9 S2 x$ S( N' [0 Q: f5 Q
17: while((pid = waitpid(-1, &nState, WNOHANG)) > 0); 6 T6 `6 Z; W9 p1 E5 z, t
18:
3 h1 e' k, a% ] Z) e7 K# s3 N 19: signal(SIGCLD, ClearChild); // 重新绑定 SIGCLD 信号
0 J" Z8 K* o9 F- M' z) l 20: 3 q5 r& x0 ], A* h, g) X
21: }
4 ~, d/ C+ c# X* v: l( \. @ 22: 0 J: e8 v6 b% B2 [
23: int InitServer(){ ; a8 L8 L2 I7 S- i- y* I
24: 9 U4 E- w; f! w- m' q. \6 J* Q
25: pid_t pid; 3 H# }7 E; n7 S6 h( {% {
26: # T/ B, i' B' |' u6 s+ B
27: assert((pid = fork()) >= 0); // 创建子进程
/ M' g7 n- D, F' o 28: 6 D& U, @1 D2 \' r( U7 E
29: if(pid != 0){ // 父进程退出, 子进程被 init 托管 / q: J9 e q' r/ n8 j8 K8 T2 A
30: 5 _- Y- @3 j- C. U7 \
31: sleep(1); 2 x% r" \5 O* k6 P( g
32:
3 C' R6 j/ ~3 T8 @0 P 33: exit(0); ; ^2 _# J5 U) A* H+ h
34:
: G6 t7 [! K3 i* R! _4 e' | 35: } 9 G+ L6 Q' K8 m! `% j0 ?
36:
' R/ x, a$ {7 c0 m3 R" V 37: assert(setsid() >= 0); // 子进程脱离终端 1 `7 q( t; l8 Z
38:
; h! t9 M0 X! \1 T( p1 @! t$ V; V 39: umask(0); // 清除文件创建掩码
1 d7 z7 S& g: ~3 t$ \ 40: # D* q0 A7 I# b7 |
41: signal(SIGINT, SIG_IGN); // 忽略 SIGINT 信号
' E, g$ s6 ^7 U4 z* g4 o# p 42: , k6 G9 z6 H/ H# d1 I
43: signal(SIGCLD, ClearChild); // 处理 SIGCLD 信号,预防子进程僵死 ) J& n" s8 h/ l5 V; X( N
44:
: Y* F' V( Z/ l3 {1 b" n( B0 ?& e: o 45: return 0; B( I" Q( a* ^9 M
46:
; `) j3 U5 @; M0 Q 47: } 0 K- j' O7 L) u8 J; y8 S; b( b
48:
7 \% T2 m" r A& H' F, W' ^- G 49: int main()
3 h* D0 p( w$ _1 @ Q- G2 Y 50: 7 n" @* G0 }% A9 P/ Y8 G U
51: { 3 l1 e) W0 v5 |- D+ D1 P8 d8 j
52: 3 i1 C; X" l; ]; H) g; ~: {
53: InitServer(); 1 p5 v' c" u: X# |( P* T+ F
54:
+ I+ b {' }# W% L. q 55: sleep(100); 3 u" m( d6 y B3 w; f
56: 3 r7 b. o7 z/ b. R" Z; r6 R
57: return 0; 1 T) R$ \9 Y. t
58:
l& d+ W3 c: A9 C 59: } 1 |: ]7 v+ P1 C! W F$ N# Z6 L6 T
[bill@billstone Unix_study]$ make initServer - m6 o6 @% g) z4 u8 \5 ^
) r- Z9 X$ Y# R' G" _" S
cc initServer.c -o initServer + }0 `+ V- _2 w# T' g
; ~# v9 F- K- K4 D) f7 b4 U; u% |# m
[bill@billstone Unix_study]$ ./initServer
% M9 y. U ^- F7 Q8 d; o. W
2 k7 Y% O% B. K9 M[bill@billstone Unix_study]$ ps -ef | grep initServer / W% ^5 s6 A) f- ?7 H
. E! Z* u* m' a4 L8 Y! O/ r" a) ?bill 13721 1 0 04:40 ? 00:00:00 ./initServer // '?'代表 initServer 独立于终端 ) p( e* e7 g2 d+ s6 L6 t
( u* T! S, y+ ~$ W5 r: g- wbill 13725 1441 0 04:41 pts/0 00:00:00 grep initServer
: j/ ]* B* I% b) y. l3 a4 E$ l' y
程序在接收到 SIGCLD 信号后立即执行函数 ClearChild, 并调用非阻塞的 waitpid 函数结束子进程结束
( u B4 h6 [% y, X" n3 `% g4 E% k
信息, 如果结束到子进程结束信息则释放该子进程占用的进程表资源, 否则函数立刻返回. 这样既保证了不增加守护进程负担, 又成功地预防了僵死进程的产生. 0 b( `4 ? w) ^# j4 B0 f
l: w( I0 t6 k& ^: j E 2 n* v* P8 S" i' G$ P0 z
* N( r* ^) q4 I9 F自己编写的一个程序:/ t3 W& A" _; _0 R+ O
6 Q' C" @8 H5 P; P$ K1 g# cat test.c
1 v1 H [; i/ g* y* y' B
* ?& u2 K" X$ J0 T6 F R/ ` 1: #include <unistd.h>7 E& {. X, s7 _% |
2: #include <stdio.h>
9 |4 o' Y2 n5 I1 _ 3: #include <sys/types.h>
' |7 j: N; M9 C& c$ P: ]# D 4: + V7 B3 F! [9 B3 U, a9 [
5: int cal ()
+ q. c8 I$ k2 I/ W2 r 6: {* c; U m/ h: ~6 a5 Q$ {6 L
7:
- d1 X5 N# U7 L5 ~# W2 ?% F 8: int i = 0, sum = 0;
, C2 O& R7 t E. J& _# w# Z6 V) P 9: / r8 i( B# Q6 j* e
10: for (i = 0; i <= 100; i++)
. ^8 ?8 ^2 D2 c 11: ! \6 C0 r9 O3 z# {- ^
12: {
8 M$ {0 }9 u1 {' _! R) z# q 13: . }: B1 s( o2 f8 Y% W! l
14: sum += i;
7 d2 R' B, Y# Z( T# P 15:
3 D) D7 X4 p- i2 C# N" |: |3 i 16: }
- m9 c( V; u7 k3 s& W. x% ` 17:
& _- [$ b! r& {# |7 A 18: return sum;
, O& v- m1 m$ p 19:
% V9 j# \8 z5 P0 L% Z 20: }
$ r) I. b9 r/ k G7 g 21: $ T* A6 n) X1 m& }% I0 q
22: int
; `3 |+ a3 x9 N* ?8 a! C. q- m 23:
- }) V2 A$ Y. j( o8 H: X" v* y! Z# Q 24: main ()
6 b1 U! q9 V9 E0 g5 S 25:
- \5 H* f) {3 o9 } 26: {
2 j8 v8 N T2 e3 y6 p 27: % |# {4 O/ [* g0 f" c2 R
28: int num=1, status;/ A5 S8 X# }5 O; I, ^/ b. U
29:
) W9 O c; c6 G0 ]) m" ~+ u" F 30: int *s=#
4 ?! x/ r0 N$ F! C7 v$ f. E. U 31: , e4 j. i! K* j/ M
32: pid_t pid;, a9 a) h) E1 ?7 E6 R, K
33:
8 v3 u! r4 q% x1 g) l1 m 34: if ((pid = fork ()) == 0)6 F/ ?2 m ]4 X6 h1 u$ t
35: 7 Q! O/ k' w: @3 T4 S. `6 Q P
36: {
2 L7 f) I; c) j0 b+ }/ ^ 37:
+ u; A# N# M! h: H4 u1 S* @5 p" f# A, P 38: *s = cal ();& F: V: D( t7 p% x7 N3 M
39: % O9 k7 H2 w/ b; v* i+ x
40: printf ("1+..+100=%d\n", *s);% B, b% l1 l# Q' _
41: : [2 ]6 F2 }+ m; A7 p' M* i4 B
42: exit (0);
$ E+ T# ` `0 k5 L6 } 43:
: n* K- [, u5 Q0 w 44: }
% T) E7 F1 X E! c Q 45: ( V7 Y. p$ l2 u6 h
46: else if (pid < 0)6 c1 ~ H/ Q$ k2 v
47:
: w) b* w2 C. C( X% y, P 48: {
6 R# G0 O0 A$ }. m0 q1 [' o+ n 49: - ~3 `, U8 m2 A& g
50: exit (0);4 p' _. S& D) h
51:
$ M) W8 h: Q: Q 52: }
s5 Y' _" |! T& x) k 53: - o" U! D: V5 A% b# ~5 J5 J
54: //pid = wait (&status);3 c% C% S/ R) s
55: {: x( N+ W) J$ R2 O0 {
56: //if (status == 0)0 E& T, {: ~2 `, j9 p! S
57: ' w; Q4 i$ s8 f* P6 {7 x1 v
58: // {1 c- x: b$ s+ u- A8 U: r
59:
/ s5 n2 E' m1 A( _0 |" | 60: wait (); N# P, ?- @) F/ \7 r; O6 Q
61:
& s% a' l3 r0 L6 a 62: printf ("1+2+...+100=%d\n", *s);
9 }9 o& X* P* @( X 63:
1 m8 Y/ u2 [3 Y1 z7 z 64: // }
i# s; V, [0 a( Z 65: 4 T: o( `7 U2 E
66: //else3 i2 X5 l/ @* q0 v$ ^
67:
( r) T. p% \. @9 ?6 ~" U) Q, W4 } 68: // {7 Y- J1 r- |3 k/ Q6 {
69:
4 Y' E3 j h% {$ L$ W 70: // printf ("error!\n");
# f# D# ~1 p( `/ k( X; c# a$ p7 ? 71: ( E; ^ G" U2 \ y7 d W* h3 n
72: // }, Y4 Y# V% T4 U6 A4 F
73: " w$ J: H: q% }% I3 }8 v8 k
74: }
" X% a3 ?, b/ R 75:
% D) M9 o' G7 R; |/ } 76: [root@localhost chapter9]# ./test) t2 E" r( t) ?) L/ f( A( C7 \
77:
N& }0 m) S7 q" g$ R. q/ L* f 78: 1+..+100=5050
) @, D# `7 z/ c- R 79: ) E1 A; a3 B5 W ]1 A! P
80: 1+2+...+100=1
0 o! |2 |/ r, X( f9 @, ~ 81:
+ V; O0 O& S; t! D程序的本意是用子进程来执行函数,而fork子进程完全复制父进程数据空间,这样子进程会获得父进程的所有变量的拷贝,尽管父子进程变量名相同,但却存在了不同的地方,因此不能通过内存变量完成父子进程之间的信息传递。
r0 r& ] j+ U( V: \ |
|