Linux进程控制
原文地址:http://www.cnblogs.com/feisky/archive/2009/10/25/1589613.html
进程是程序的一次执行, 是运行在自己的虚拟地址空间的一个具有独立功能的程序. 进程是分配和释放资源的基本单位, 当程序执行时, 系统创建进程, 分配内存和 CPU 等资源; 进程结束时, 系统回收这些资源。 进程由PCB(进程控制块)来描述:
-
进程id。系统中每个进程有唯一的id,在C语言中用
pid_t
类型表示,其实就是一个非负整数。 -
进程的状态,有运行、挂起、停止、僵尸等状态。
-
进程切换时需要保存和恢复的一些CPU寄存器。
-
描述虚拟地址空间的信息。
-
描述控制终端的信息。
-
umask
掩码。 -
文件描述符表,包含很多指向
file
结构体的指针。 -
和信号相关的信息。
-
用户id和组id。
-
控制终端、Session和进程组。
线程与进程
- 线程又名轻负荷进程, 它是在进程基础上程序的一次执行, 一个进程可以拥有多个线程.
- 线程没有独立的资源, 它共享进程的 ID, 共享进程的资源.
- 线程是 UNIX 中最小的调度单位, 目前有系统级调度和进程级调度两种线程调度实行方式: 系统级调度的操作系统以线程为单位进行调度; 进程级调度的操作系统仍以进程为单位进行调度, 进程再为其上运行的线程提供调度控制.
守护进程:常驻后台执行的特殊进程,如sysproc init
读取PID号:getpid getpgrp getppid <unistd.h> <sys/types.h>
读取用户标识号:getuid geteuid getgid getegid
例子:
#include<unistd.h>
void main()
{
printf("pid=[%d], gid=[%d], ppid=[%d]\n", getpid(), getpgrp(), getppid());
printf("uid=[%d], euid=[%d], gid=[%d], egid=[%d]\n", getuid(), geteuid(), getgid(), getegid());
}
# ./id1
pid=[3311], gid=[3311], ppid=[2925]
uid=[0], euid=[0], gid=[0], egid=[0]
环境变量
UNIX 中, 存储了一系列的变量, 在 shell 下执行'env'命令, 就可以得到环境变量列表.
环境变量分为系统环境变量和用户环境变量两种. 系统环境变量在注册时自动设置, 大部分具有特定
的含义; 用户环境变量在 Shell 中使用赋值命令和 export 命令设置. 如下例先设置了变量 XYZ, 再将其转化
为用户环境变量:
[bill@billstone Unix_study]$ XYZ=/home/bill
[bill@billstone Unix_study]$ env | grep XYZ
[bill@billstone Unix_study]$ export XYZ
[bill@billstone Unix_study]$ env | grep XYZ
XYZ=/home/bill
[bill@billstone Unix_study]$
UNIX 下 C 程序中有两种获取环境变量值的方法: 全局变量法和函数调用法
(a) 全局变量法
UNIX 系统中采用一个指针数组来存储全部环境值:
Extern char **environ;
该法常用于将 environ 作为参数传递的语句中, 比如后面提到的 execve 函数等.
1: #include <stdio.h>2:
3: extern char **environ;4:
5: int main()6:
7: {
8:
9: char **p = environ;10:
11: while(*p){12:
13: fprintf(stderr, "%s\n", *p);14:
15: p++;
16:
17: }
18:
19: return 0;20:
21: }
(b) 函数调用法
UNIX 环境下操作环境变量的函数如下:
#include <stdlib.h> char *getenv(const char *name); int setenv(const char *name, const char *value, int rewrite); void unsetenv(const char *name);
函数 getenv 以字符串形式返回环境变量 name 的取值, 因此每次只能获取一个环境变量的值; 而且要使用该函数, 必须知道要获取环境变量的名字.
在进程中执行新程序的三种方法
进程和人类一样, 都有创建, 发展, 休眠和死亡等各种生命形态.
- 函数 fork 创建新进程,
- 函数exec 执行新程序,
- 函数 sleep 休眠进程,
- 函数 wait 同步进程和函数
- exit 结束进程.
创建子进程的两个用途: 1.复制代码 2.执行新程序
(1) fork-exec
调用 fork 创建的子进程, 将共享父进程的代码空间, 复制父进程数据空间, 如堆栈等. 调用 exec 族函数将使用新程序的代码覆盖进程中原来的程序代码, 并使进程使用函数提供的命令行参数和环境变量去执行
新的程序.
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
fork
函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。一开始是一个控制流程,调用fork
之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork
的返回值是0,而父进程中fork
的返回值则是子进程的id(从根本上说fork
是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork
函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。fork
的返回值这样规定是有道理的。fork
在子进程中返回0,子进程仍可以调用getpid
函数得到自己的进程id,也可以调用getppid
函数得到父进程的id。在父进程中用getpid
可以得到自己的进程id,然而要想得到子进程的id,只有将fork
的返回值记录下来,别无它法。
fork
的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file
结构体,也就是说,file
结构体的引用计数要增加。
exec 函数族有六个函数如下:
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., (char *)0);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
int execlp(const char *file, const char *arg0, ..., (char *)0);
int execv(const char *path, const char *argv[]);
int execve(const char *path, const char *argv[], const char *envp[]);
int execvp(const char *file, const char *argv[]);
extern char **environ;
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec
函数只有出错的返回值而没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。不带字母p(表示path)的exec
函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"
或"./a.out"
,而不能是"ls"
或"a.out"
。对于带字母p的函数:
-
如果参数中包含/,则将其视为路径名。
-
否则视为不带路径的程序名,在
PATH
环境变量的目录列表中搜索这个程序。
带有字母l(表示list)的exec
函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有...
,...
中的最后一个可变参数应该是NULL
,起sentinel的作用。对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL
,就像main
函数的argv
参数或者环境变量表一样。
对于以e(表示environment)结尾的exec
函数,可以把一份新的环境变量表传给它,其他exec
函数仍使用当前的环境变量表执行新程序。
exec
调用举例如下:
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL}; char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execv("/bin/ps", ps_argv); execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp); execve("/bin/ps", ps_argv, ps_envp); execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execvp("ps", ps_argv);
(2) vfork-exec
vfork 比起 fork 函数更快, 二者的区别如下:
- a) vfork 创建的子进程并不复制父进程的数据, 在随后的 exec 调用中系统会复制新程序的数据到内存, 继而避免了一次数据复制过程
- b) 父进程以 vfork 方式创建子进程后将被阻塞, 知道子进程退出或执行 exec 调用后才能继续运行. 当子进程只用来执行新程序时, vfork-exec 模型比 fork-exec 模型具有更高的效率, 这种方法也是 Shell创建新进程的方式.
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid; if((pid = vfork()) == 0){ fprintf(stderr, "---- begin ----\n"); sleep(3); execl("/bin/uname", "uname", "-a", 0); fprintf(stderr, "---- end ----\n"); } else if(pid > 0) fprintf(stderr, "fork child pid = [%d]\n", pid); else fprintf(stderr, "Fork failed.\n"); return 0; } [bill@billstone Unix_study]$ make exec2 make: `exec2' is up to date. [bill@billstone Unix_study]$ ./exec2 ---- begin ---- fork child pid = [13293] [bill@billstone Unix_study]$ Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux
(3) system
在 UNIX 中, 我们也可以使用 system 函数完成新程序的执行.
函数 system 会阻塞调用它的进程, 并执行字符串 string 中的 shell 命令.
[bill@billstone Unix_study]$ cat exec3.c
#include <unistd.h>
#include <stdio.h>
int main()
{
char cmd[] = {"/bin/uname -a"};
system(cmd);
return 0;
}
[bill@billstone Unix_study]$ make exec3
cc exec3.c -o exec3
[bill@billstone Unix_study]$ ./exec3
Linux billstone 2.4.20-8 #1 Thu Mar 13 17:18:24 EST 2003 i686 athlon i386 GNU/Linux
进程休眠:sleep
进程终止:exit abort
进程同步(等待):wait
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait
或waitpid
获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?
查看,因为Shell是它的父进程,当它终止时Shell调用wait
或waitpid
得到它的退出状态同时彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用wait
或waitpid
对它进行清理,这时的进程状态称为僵尸(Zombie)进程。
ps -ef | grep 13707
bill 13707 1441 0 04:17 pts/0 00:00:00 ./szomb1
bill 13708 13707 0 04:17 pts/0 00:00:00 [szomb1 <defunct>] // 僵死进程
bill 13710 1441 0 04:17 pts/0 00:00:00 grep 13707
[bill@billstone Unix_study]$
其中, 'defunct'代表僵死进程. 对于僵死进程, 不能奢望通过 kill 命令杀死之, 因为它已经'死'了, 不再接收任何系统信号.
当子进程终止时, 它释放资源, 并且发送 SIGCHLD 信号通知父进程. 父进程接收 SIGCHLD 信号,调用wait 返回子进程的状态, 并且释放系统进程表资源. 故如果子进程先于父进程终止, 而父进程没有调用 wait接收子进程信息,则子进程将转化为僵死进程, 直到其父进程结束.
一旦知道了僵死进程的成因, 我们可以采用如下方法预防僵死进程:
(1) wait 法
父进程主动调用 wait 接收子进程的死亡报告, 释放子进程占用的系统进程表资源.
(2) 托管法
如果父进程先于子进程而死亡, 则它的所有子进程转由进程 init 领养, 即它所有子进程的父进程 ID 号变为 1. 当子进程结束时 init 为其释放进程表资源.
托管法技巧:两次fork,子进程退出,则子子进程的父进程变为init。
(3) 忽略 SIGC(H)LD 信号
当父进程忽略 SIGC(H)LD 信号后, 即使不执行 wait, 子进程结束时也不会产生僵死进程.
(4) 捕获 SIGC(H)LD 信号
当父进程捕获 SIGC(H)LD 信号, 并在捕获函数代码中等待(wait)子进程
wait和waitpid函数的原型是:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会:
阻塞(如果它的所有子进程都还在运行)。
带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。如果参数status不是
空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数指定为NULL。
例 30.6. waitpid
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
if (pid == 0) {
int i;
for (i = 3; i > 0; i--) {
printf("This is the child\n");
sleep(1);
}
exit(3);
} else {
int stat_val;
waitpid(pid, &stat_val, 0);
if (WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else if (WIFSIGNALED(stat_val))
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
return 0;
}
子进程的终止信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段:如果子进程是正常终止的,WIFEXITED取出的字段值非零,
WEXITSTATUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WIFSIGNALED取出的字段值非零,WTERMSIG取出的
字段值就是信号的编号。作为练习,请读者从头文件里查一下这些宏做了什么运算,是如何取出字段值的。
守护进程
所谓守护进程是一个在后台长期运行的进程, 它们独立于控制终端, 周期性地执行某项任务, 或者阻塞直到事件发生, 默默地守护着计算机系
统的正常运行. 在 UNIX 应用中, 大部分 socket 通信服务程序都是以守护进程方式执行.
完成一个守护进程的编写至少包括以下几项:
(1) 后台执行
后台运行的最大特点是不再接收终端输入, 托管法可以实现这一点
pid_t pid;
pid = fork();
if(pid > 0) exit(0); // 父进程退出
/* 子进程继续运行 */
父进程结束, shell 重新接管终端控制权, 子进程移交 init 托管
(2) 独立于控制终端
在后台进程的基础上, 脱离原来 shell 的进程组和 session 组, 自立门户为新进程组的会话组长进程, 与原终端脱离关系
#include <unistd.h>
pid_t setsid();
函数 setsid 创建一个新的 session 和进程组.
(3) 清除文件创建掩码
进程清除文件创建掩码,代码如下:
umask(0);
(4) 处理信号
为了预防父进程不等待子进程结束而导致子进程僵死, 必须忽略或者处理 SIGCHLD 信号, 其中忽略该信号的方法为:
signal(SIGCHLD, SIG_IGN);
守护进程独立于控制终端, 它们一般以文件日志的方式进行信息输出. Syslog 是 Linux 中的系统日志管理服务,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”。该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可记录到一个文件中。 该机制提供了 3 个 syslog 函数,分别为 openlog、syslog 和 closelog。
下面是一个简单的守护进程实例 InitServer
[bill@billstone Unix_study]$ cat initServer.c
1: #include <assert.h>
2:
3: #include <signal.h>
4:
5: #include <sys/wait.h>
6:
7: #include <sys/types.h>
8:
9: void ClearChild(int nSignal){
10:
11: pid_t pid;
12:
13: int nState;
14:
15: // WNOHANG 非阻塞调用 waitpid, 防止子进程成为僵死进程
16:
17: while((pid = waitpid(-1, &nState, WNOHANG)) > 0);
18:
19: signal(SIGCLD, ClearChild); // 重新绑定 SIGCLD 信号
20:
21: }
22:
23: int InitServer(){
24:
25: pid_t pid;
26:
27: assert((pid = fork()) >= 0); // 创建子进程
28:
29: if(pid != 0){ // 父进程退出, 子进程被 init 托管
30:
31: sleep(1);
32:
33: exit(0);
34:
35: }
36:
37: assert(setsid() >= 0); // 子进程脱离终端
38:
39: umask(0); // 清除文件创建掩码
40:
41: signal(SIGINT, SIG_IGN); // 忽略 SIGINT 信号
42:
43: signal(SIGCLD, ClearChild); // 处理 SIGCLD 信号,预防子进程僵死
44:
45: return 0;
46:
47: }
48:
49: int main()
50:
51: {
52:
53: InitServer();
54:
55: sleep(100);
56:
57: return 0;
58:
59: }
[bill@billstone Unix_study]$ make initServer
cc initServer.c -o initServer
[bill@billstone Unix_study]$ ./initServer
[bill@billstone Unix_study]$ ps -ef | grep initServer
bill 13721 1 0 04:40 ? 00:00:00 ./initServer // '?'代表 initServer 独立于终端
bill 13725 1441 0 04:41 pts/0 00:00:00 grep initServer
程序在接收到 SIGCLD 信号后立即执行函数 ClearChild, 并调用非阻塞的 waitpid 函数结束子进程结束
信息, 如果结束到子进程结束信息则释放该子进程占用的进程表资源, 否则函数立刻返回. 这样既保证了不增加守护进程负担, 又成功地预防了僵死进程的产生.
自己编写的一个程序:
# cat test.c
1: #include <unistd.h>
2: #include <stdio.h>
3: #include <sys/types.h>
4:
5: int cal ()
6: {
7:
8: int i = 0, sum = 0;
9:
10: for (i = 0; i <= 100; i++)
11:
12: {
13:
14: sum += i;
15:
16: }
17:
18: return sum;
19:
20: }
21:
22: int
23:
24: main ()
25:
26: {
27:
28: int num=1, status;
29:
30: int *s=#
31:
32: pid_t pid;
33:
34: if ((pid = fork ()) == 0)
35:
36: {
37:
38: *s = cal ();
39:
40: printf ("1+..+100=%d\n", *s);
41:
42: exit (0);
43:
44: }
45:
46: else if (pid < 0)
47:
48: {
49:
50: exit (0);
51:
52: }
53:
54: //pid = wait (&status);
55:
56: //if (status == 0)
57:
58: // {
59:
60: wait ();
61:
62: printf ("1+2+...+100=%d\n", *s);
63:
64: // }
65:
66: //else
67:
68: // {
69:
70: // printf ("error!\n");
71:
72: // }
73:
74: }
75:
76: [root@localhost chapter9]# ./test
77:
78: 1+..+100=5050
完