zoukankan      html  css  js  c++  java
  • Linux进程控制

       

    进程是程序的一次执行,  是运行在自己的虚拟地址空间的一个具有独立功能的程序.  进程是分配和释放资源的基本单位,  当程序执行时,  系统创建进程,  分配内存和 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
      79:  
      80: 1+2+...+100=1
      81:  

    程序的本意是用子进程来执行函数,而fork子进程完全复制父进程数据空间,这样子进程会获得父进程的所有变量的拷贝,尽管父子进程变量名相同,但却存在了不同的地方,因此不能通过内存变量完成父子进程之间的信息传递。

  • 相关阅读:
    Linux磁盘管理
    Linux系统中的计划任务与压缩归档简介------If you shed tears when you miss the sun, you also miss the stars.
    Linux系统之权限管理(所有者,所属组,其他;r,w,x;特殊权限)------If you shed tears when you miss the sun, you also miss the stars.
    linuxx系统中高级命令简介------If you shed tears when you miss the sun, you also miss the stars.
    IE11新特性 -- Internet Explorer 11:请不要再叫我IE
    SQL WHILE 循环中的游标 用例,SQL中实现循环操作
    group_concat函数使用
    Git 初始化配置
    WCF 、Web API 、 WCF REST 和 Web Service 的区别
    js 解析 json
  • 原文地址:https://www.cnblogs.com/feisky/p/1589613.html
Copyright © 2011-2022 走看看