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