• Linux进程基础知识


    进程是linux操作系统的环境的基础,它控制着系统上几乎所有的活动。关于进程编程内容如下:

    1).复制进程映像的fork系统调用和替换进程映像的exec系列系统调用。

    2).僵尸进程以及如何避免僵尸进程。

    3).进程间通信。

    4).三种system v进程通信方式:信号量、消息队列和共享内存。

    5).在进程间传递文件描述符的通用办法:通过unix本地域socket传递特殊的辅助数据。

    这里,我们先讲如何用一个进程创建出另一个进程。

    1.fork系统调用

    pid_t fork(void);

           该函数返回两次,父进程中返回子进程PID,子进程中返回0,所以返回值是后续代码判断当前进程是父进程还是子进程的依据。fork调用失败返回-1,并设置errno。fork函数复制当前进程,在内核进程表中创建一个新的进程表象。新的进程表象有很多属性和原进程相同,比对堆指针、栈指针和表示寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置为原进程的PID,信号位被清除(原进程设置的信号处理不再对新进程起作用)。子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是所谓的写时复制(copy on writte),即只有在任一进程(父进程或者子进程)对数据执行了写操作时,复制才会发生(先缺页也中断,然后操作系统给子进程分配内存并复制赋值父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应当十分谨慎,尽量避免没必要的内存分配和数据复制。创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1,不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。 

     1 #include <sys/types.h>
     2 #include <sys/types.h>
     3 #include <unistd.h>
     4 #include <string.h>
     5 #include <stdio.h>
     6 #include <stdlib.h>
     7 #include <errno.h>
     8 
     9 
    10 int main(void)
    11 {
    12     pid_t pid = fork();
    13     if(pid > 0)
    14     {
    15         printf("I am parents process, pid %d, my child is %d
    ", getpid(), pid);
    16     }
    17     else if(pid == 0)
    18     {
    19         printf("I am child process, pid %d, my parent is %d
    ", getpid(), getppid());
    20     }
    21     else
    22     {
    23         printf("fork failed, errno %d
    ", errno);
    24     }
    25 }

    Linux环境下编译并执行

    ydq@docsis4 chapter13 $ gcc fork.c -o fork
    ydq@docsis4 chapter13 $ ./fork
    I am parents process, pid 26293, my child is 26294
    I am child process, pid 26294, my parent is 26293

    例子上我们还调用了getpid()和getppid()来分别获取自己的进程号和父进程的进程号来验证fork的返回值情况,确认得到pid>0时,对应的代码是父进程跑的代码;pid=0时,对应的代码是子进程跑的代码。

    2.exec系列系统调用

      有时我们需要在子进程中执行其他可执行程序,即替换当前进程映像,这就需要使用如下exec系列函数之一。

    #include <unistd.h>
    
    extern char **environ;
    
    int execl(const char *pathname, const char *arg, .../* (char *) NULL */);
    int execlp(const char *file, const char *arg, .../* (char *) NULL */);
    int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char * const envp[] */);
    int execv(const char *pathname, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[], char *const envp[]);

      path参数指定可执行文件的完整路径,file参数可以接受文件名,该文件的具体则在环境变量PATH中搜寻。arg接受可变参数,argv则接受参数数组,它们都会被传递给新程序(path或file指定的程序)的main函数。envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量。

      一般情况下,exec函数是不返回的,除非出错。出错返回-1,并设置errno。如果没出错,则原程序中exec调用之后的代码都不会被执行,因为此时原程序已经被exec的参数指定的程序完全替换(包括代码和数据)。

      exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC属性。

      我们拿第一个函数execl来写一段测试代码。

    ydq@docsis4 chapter13 $ vi execl.c

     1 #include <sys/types.h>
     2 #include <sys/types.h>
     3 #include <unistd.h>
     4 #include <string.h>
     5 #include <stdio.h>
     6 #include <stdlib.h>
     7 #include <errno.h>
     8 
     9 
    10 int main(void)
    11 {
    12     pid_t pid = fork();
    13     if(pid > 0)
    14     {
    15         printf("I am parents process, pid %d, my child is %d
    ", getpid(), pid);
    16         usleep(5000);//确保父进程比子进程晚退出,这样子进程才不会被init进程接手
    17     }
    18     else if(pid == 0)
    19     {
    20         printf("I am child process, pid %d, my parent is %d
    ", getpid(), getppid());
    21         if(execl("/home/ydq/code/high_performance/chapter13/child", "child", (char * )0) == -1)
    22         {
    23             printf("execl failed, errno %s
    ", strerror(errno));
    24             return -1;
    25         }
    26         printf("hello world!
    ");//在execl调用后才执行的打印语句,不会被调用到.
    27     }
    28     else
    29     {
    30         printf("fork failed, errno %d
    ", errno);
    31     }
    32 
    33 }

    ydq@docsis4 chapter13 $ vi child.c

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 
    4 
    5 int main(int argc, char *argv[])
    6 {
    7     printf("[%s]:I am child process, pid %d, my parent is %d
    ", argv[0],getpid(), getppid());
    8     return 0;
    9 }

    ydq@docsis4 chapter13 $ gcc execl.c -o execl ; gcc child.c -o child
    ydq@docsis4 chapter13 $ ./execl
    I am parents process, pid 12958, my child is 12959
    I am child process, pid 12959, my parent is 12958
    [child]:I am child process, pid 12959, my parent is 12958
    ydq@docsis4 chapter13 $

      验证成功,说明上述的说法是正确的。

    3.处理僵尸进程

      对于多进程程序,父进程一般需要跟踪子进程的退出转态。因此,当子进程结束运行时,内核不会立即释放改进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(父进程必须还在运行)。在子进程结束运行之后,父进程读取器退出状态之前,我们成该子进程为僵尸态。另外一种是父进程结束或者异常终止,而子进程继续运行,此时子进程被init进程接管,并等待它退出。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。

      下面,我们编写一个例子来测试子进程进入僵尸态的情况。

    ydq@docsis4 chapter13 $ vi zombie_child.c

     1 #include <sys/types.h>
     2 #include <sys/types.h>
     3 #include <unistd.h>
     4 #include <string.h>
     5 #include <stdio.h>
     6 #include <stdlib.h>
     7 #include <errno.h>
     8 
     9 
    10 int main(void)
    11 {
    12     pid_t pid = fork();
    13     if(pid > 0)
    14     {
    15         printf("I am parents process, pid %d, my child is %d
    ", getpid(), pid);
    16         sleep(30);//为了让父进程比子进程晚退出和有足够时间去查看进程状态,睡眠30s
    17     }
    18     else if(pid == 0)
    19     {
    20         printf("I am child process, pid %d, my parent is %d
    ", getpid(), getppid());
    21         printf("child process is finished
    ");
    22     }
    23     else
    24     {
    25         printf("fork failed, errno %d
    ", errno);
    26     }
    27 
    28 }

    ydq@docsis4 chapter13 $ gcc zombie_child.c -o zombie_child
    ydq@docsis4 chapter13 $ ./zombie_child &     -- 执行时加&,让进程在后台执行
    [1] 30061
    ydq@docsis4 chapter13 $ I am parents process, pid 30061, my child is 30062
    I am child process, pid 30062, my parent is 30061
    child process is finished

    ydq@docsis4 chapter13 $ ps -as | grep zom
    1002 30061 0000000000000000 0000000000000000 0000000000000000 0000000000000000 S pts/7 0:00 ./zombie_child
    1002 30062 0000000000000000 0000000000000000 0000000000000000 0000000000000000 Z pts/7 0:00 [zombie_child] <defunct>
    1002 30157 0000000000000000 0000000000000000 0000000000000000 0000000180000000 S+ pts/7 0:00 grep --color=auto zom

    我们通过ps命令的输出,可以获知子进程已经进入Z状态(僵尸态)。

      父进程没有正确地处理子进程的返回信息,子进程都将停留在僵尸态,并占据着内核资源,这是必须避免的,我们可以在父进程中调用下面这对函数,等待子进程结束并获取子进程返回信息,从而避免僵尸进程的产生,或者使子进程的僵尸态立即结束。

           #include <sys/types.h>
           #include <sys/wait.h>
    
           pid_t wait(int *wstatus);
    
           pid_t waitpid(pid_t pid, int *wstatus, int options);

           wait函数将阻塞进程,知道该进程的某个子进程结束运行位置。返回值为该结束的子进程PID,并将子进程的退出转态信息存储于wstatus指向的内存中。我们可以通过sys/wait.h定义的宏来帮助解释子进程的退出状态,如下所示:

    宏                                           含义

    WIFEXITED(wstatus)            如果子进程正常结束,它就返回一个非0值

    WEXITSTATUS(wstatus)           如果WIFEXITED非0,它返回子进程的退出码

    WIFSIGNALED(wstatus)        如果子进程是因为一个未捕获的信号而终止,它就返回一个非0值

    WTERMSIG(wstatus)             如果WIFSIGNALED非0,它返回终止子进程的信号

    WIFSTOPPED(wstatus)            如果子进程是因为一个未捕获的信号而暂停,它就返回一个非0值

    WSTOPSIG(wstatus)             如果WIFSTOPPED非0,它就返回一个信号值

    wait函数的阻塞特性显然不是服务器程序期望的,因为服务器一般都是异步的(关于异步,请大家自行学习),而waitpid函数提供了非阻塞特性。

           waitpid参数和返回值表示意义如下

      参数pid:pid参数有四种取值情况,如下:

        <-1     代表等待进程组ID等于pid绝对值的任何子进程。

        -1       代表等待所有子进程

        0        代表等待进程组ID等于调用的进程的进程组ID的任何子进程。

        >0   代表等待进程ID等于pid值的子进程。

      参数wstatus:存放子进程退出状态信息的内存地址。

      参数options:控制waitpid的行为,有以下选项

        WNOHANG 如果没有子进程退出,立即返回

        WUNTRACED  如果子线程已经停止也返回。即使未指定此选项,也会提供已停止跟踪的子进程的状态。  

             WCONTINUED 如果停止的子进程被信号SIGCONT.恢复了,也返回   (since Linux 2.6.10)。

    下面我们用代码测试一下。

    ydq@docsis4 chapter13 $ vi handler_child.c

     1 #include <sys/types.h>
     2 #include <signal.h>
     3 #include <sys/types.h>
     4 #include <unistd.h>
     5 #include <string.h>
     6 #include <sys/wait.h>
     7 #include <stdio.h>
     8 #include <assert.h>
     9 #include <stdlib.h>
    10 
    11 
    12 typedef struct signal_info_s
    13 {
    14     int sig;                  //要捕获的信号
    15     void (*sig_handler)(int); //捕获信号后的执行函数
    16 }signal_info_t;
    17 
    18 
    19 //捕获SIGCHLD信号后执行的函数,用来调用waitpid处理子进程退出状态信息
    20 static void handler_child(int sig)
    21 {
    22     pid_t pid;
    23     int stat;
    24     while((pid = waitpid(-1, &stat, WNOHANG|WUNTRACED )) > 0)
    25     {
    26         if(WIFEXITED(stat))
    27             printf("child(pid %d) process exits normally, exit value %d
    ", pid, WEXITSTATUS(stat));
    28         else if(WIFSIGNALED(stat))
    29             printf("child(pid %d) process is terminated by a signal %d
    ", pid, WTERMSIG(stat));
    30         else if(WIFSTOPPED(stat))
    31             printf("child(pid %d) process is stopped by a signal %d
    ", pid, WSTOPSIG(stat));
    32         else
    33             printf("others
    ");
    34     }
    35 }
    36 
    37 
    38 //初始化信号捕获函数
    39 void child_signal_init(signal_info_t sig_info)
    40 {
    41     struct sigaction sa;
    42     memset(&sa, 0, sizeof(sa));
    43     sa.sa_handler = sig_info.sig_handler;
    44     sa.sa_flags |= SA_RESTART;
    45     sigfillset(&sa.sa_mask);
    46     assert(sigaction(sig_info.sig, &sa, NULL) != -1);
    47 }
    48 
    49 
    50 int main(void)
    51 {
    52     pid_t pid = fork();
    53     if(pid > 0)
    54     {
    55         printf("I am parents process, pid %d
    ", getpid());
    56         signal_info_t sig_info;
    57         sig_info.sig = SIGCHLD;
    58         sig_info.sig_handler = handler_child;
    59         child_signal_init(sig_info);
    60         while(1) //父进程死循环,方便看测试结果
    61         {
    62         }
    63     }
    64     else
    65     {
    66         printf("I am child process, pid %d
    ", getpid());
    67 #ifdef D_SIGTERM   //测试被信号SIGTERM终止的情况,因为我们没有去捕获信号SIGTERM,所以当我们去访问非法地址时,子进程会崩溃。
    68         int *pointer = 0;
    69         *pointer = 100;
    70 #elif  D_SIGSTOP   //测试子进程被停止的情况,我们可以通过在终端给子进程发送SIGSTOP信号
    71         while(1)
    72         {
    73 
    74         }
    75 #elif D_EXIT      //测试正常退出的情况
    76         exit(100);
    77 #endif
    78     }
    79 }

    ydq@docsis4 chapter13 $ gcc handler_child.c -o handler_child -DD_SIGSTOP ,编译加-DXXX,表示编译后生成的可执行程序带有XXX的宏定义。
    ydq@docsis4 chapter13 $ ./handler_child
    I am parents process, pid 17214
    I am child process, pid 17215

    我们执行后,发现父子进程各打印了一条log信息,我们再开启一个终端来查看一下它们的进程状态。

    ydq@docsis4 bin $ ps -as | grep handler
    1002 17214 0000000000000000 0000000000000000 0000000000000000 0000000000010000 R+ pts/7 4:26 ./handler_child
    1002 17215 0000000000000000 0000000000000000 0000000000000000 0000000000000000 R+ pts/7 4:26 ./handler_child
    1002 17494 0000000000000000 0000000000000000 0000000000000000 0000000180000000 S+ pts/3 0:00 grep --color=auto handler

    我们发现父子进程的状态都是R+(R (TASK_RUNNING),可执行状态。),这时候我们向子进程(pid17215)发送信号SIGSTOP(信号19,可通过kill -l命令查询得到),再来查看其状态,执行如下命令。

    ydq@docsis4 bin $ kill -19 17215
    ydq@docsis4 bin $ ps -as | grep handler
    1002 17214 0000000000000000 0000000000000000 0000000000000000 0000000000010000 R+ pts/7 9:39 ./handler_child
    1002 17215 0000000000000000 0000000000000000 0000000000000000 0000000000000000 T+ pts/7 9:35 ./handler_child
    1002 17794 0000000000000000 0000000000000000 0000000000000000 0000000180000000 S+ pts/3 0:00 grep --color=auto handler

    我们发现子进程接收到信号SIGSTOP后,由于没有注册捕获行为,所以系统默认执行信号SIGSTOP的缺省行为--暂停子进程,所以我们在ps输出可以看到子进程已经进入T+状态(T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态。),由于子进程由转态执行状态转化为停止转态,所以父进程接收到信号SIGCHLD,并且由于我们注册了信号SIGCHLD的捕获函数,所以我们可以原来执行handler_child的终端看到如下打印语句。

    child(pid 17215) process is stopped by a signal 19

    至此我们已经把测试子进程状态变换为停止的情况,从结果看出waitpid的行为,至于其他的子进程退出状态的测试,读着可以自行编译并运行,再测试,再三强调,最后的例子一定要熟悉,这是多进程网络服务器处理子进程退出状态的基础,希望能够理解。

      现在,我们已经把进程如何创建,如何替换可执行文件,如何避免进入僵尸态都讲解完毕,这些内容使我们多进程编程的基础,是我们以后设计多进程程序的重中之重,望熟悉。

         总结不易,转载请标明原链接和作者。

      博客园 - ydqun

  • 相关阅读:
    前端网站汇总
    更换Sublime Text主题字体
    免费收录网站搜索引擎登录口
    IE6,7,8支持css圆角
    CSS继承—深入剖析
    JavaScript正则表达式大全
    CSS伪元素选择器
    line-height用法总结
    判断腾讯QQ是否在线
    text-overflow使用文字超多div的宽度或超过在table中<td>
  • 原文地址:https://www.cnblogs.com/ydqblogs/p/13613324.html
Copyright © 2020-2023  润新知