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

原文地址:https://www.cnblogs.com/ydqblogs/p/13613324.html