Linux进程控制_#include

进程是程序的一次执行,  是运行在自己的虚拟地址空间的一个具有独立功能的程序.  进程是分配和释放资源的基本单位,  当程序执行时,  系统创建进程,  分配内存和 CPU 等资源;  进程结束时,  系统回收这些资源。 进程由PCB(进程控制块)来描述:

  • 进程id。系统中每个进程有唯一的id,在C语言中用​​pid_t​​类型表示,其实就是一个非负整数。
  • 进程的状态,有运行、挂起、停止、僵尸等状态。
  • 进程切换时需要保存和恢复的一些CPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前工作目录(Current Working Directory)。
  • ​umask​​掩码。
  • 文件描述符表,包含很多指向​​file​​结构体的指针。
  • 和信号相关的信息。
  • 用户id和组id。
  • 控制终端、Session和进程组。
  • 进程可以使用的资源上限(Resource Limit)。

    线程与进程
  •     线程又名轻负荷进程,  它是在进程基础上程序的一次执行,  一个进程可以拥有多个线程.
  •     线程没有独立的资源,  它共享进程的 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 的取值,  因此每次只能获取一个环境变量的值;  而且要使用该函数,  必须知道要获取环境变量的名字. 

  在进程中执行新程序的三种方法

    进程和人类一样,  都有创建,  发展,  休眠和死亡等各种生命形态.  

  1. 函数 fork 创建新进程,
  2. 函数exec 执行新程序, 
  3. 函数 sleep 休眠进程, 
  4. 函数 wait 同步进程和函数
  5. 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=&num;
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