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