Linux进程控制

原文地址:http://www.cnblogs.com/feisky/archive/2009/10/25/1589613.html

   

进程是程序的一次执行,  是运行在自己的虚拟地址空间的一个具有独立功能的程序.  进程是分配和释放资源的基本单位,  当程序执行时,  系统创建进程,  分配内存和 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还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用waitwaitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用waitwaitpid得到它的退出状态同时彻底清除掉这个进程。

如果一个进程已经终止,但是它的父进程尚未调用waitwaitpid对它进行清理,这时的进程状态称为僵尸(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

posted @ 2011-07-23 10:50  wangkangluo1  阅读(2097)  评论(0编辑  收藏  举报