进程控制

1 #include <unistd.h>
2 pid_t getpid(void);    //return value:  调用进程的进程ID
3 pid_t getppid(void);   //return value:  调用进程的父进程ID
1 //返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1
2 pid_t fork(void);

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。

  例如,子进程获得父进程数据空间、堆和栈的副本

  注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段。

  • 正文段,这是由CPU执行的机器指令部分。
  • 数据段(初始化数据段),包含程序中需明确赋初的变量。例如C程序中出现在任何函数之外的声明(全局变量?)
    • int maxcount = 99;
  • 栈,自动变量(局部变量?)以及每次函数调用时所需保存的信息。
  • 堆,动态存储分配。

程序演示了 fork 函数,从中可以看到子进程对变量所做的改变并不影响父进程中该变量的值。

 1 #include "apue.h"
 2 
 3 int        globvar = 6;        /* external variable in initialized data */
 4 char    buf[] = "a write to stdout\n";
 5 
 6 int
 7 main(void)
 8 {
 9     int        var;        /* automatic variable on the stack */
10     pid_t    pid;
11 
12     var = 88;
13     if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
14         err_sys("write error");
15     printf("before fork\n");    /* we don't flush stdout */
16 
17     if ((pid = fork()) < 0) {
18         err_sys("fork error");
19     } else if (pid == 0) {        /* child */
20         globvar++;                /* modify variables */
21         var++;
22     } else {
23         sleep(2);                /* parent */
24     }
25 
26     printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
27       var);
28     exit(0);
29 }

一般来说,fork之后父、子进程的执行顺序是不确定的,这取决于内核所使用的调度算法。

待文件IO章节学习完毕后回看184-186页

fork有以下两种用法。

(1)一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。

(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

 

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以发生在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理函数)。对于这种信号的系统默认动作是忽略它。

调用 wait 和 waitpid 函数的进程可能会发生什么?

  • 如果其所有子进程都还在运行,则阻塞。
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
  • 如果它没有任何子进程,则立即出错返回。

如果进程由于接收到SIGCHLD信号而调用wait , 我们期望wait 会立即返回。但是如果在随机时间点调用wait,则进程可能会阻塞。

如果一个进程 fork 一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死进程直到父进程终止,实现这一要求的诀窍是调用 fork 两次。

 https://blog.csdn.net/keyeagle/article/details/6679934

首先,要了解什么叫僵尸进程,什么叫孤儿进程,以及服务器进程运行所需要的一些条件。两次fork()就是为了解决这些相关的问题而出现的一种编程方法。

  • 孤儿进程

       孤儿进程是指父进程在子进程结束之前死亡(return 或exit)。如下图1所示:

    

                图1  孤儿进程

 

  但是孤儿进程并不会像上面画的那样持续很长时间,当系统发现孤儿进程时,init进程就收养孤儿进程,成为它的父亲,child进程exit后的资源回收就都由init进程来完成。

  • 僵尸进程

       僵尸进程是指子进程在父进程之前结束了,但是父进程没有用wait或waitpid回收子进程。如下图所示:

     

    

                图2   僵尸进程

         父进程没有用wait回收子进程并不说明它不会回收子进程。子进程在结束的时候会给其父进程发送一个SIGCHILD信号,父进程默认是忽略SIGCHILD信号的,如果父进程通过signal()函数设置了SIGCHILD的信号处理函数,则在信号处理函数中可以回收子进程的资源。

      事实上,即便是父进程没有设置SIGCHILD的信号处理函数,也没有关系,因为在父进程结束之前,子进程可以一直保持僵尸状态,当父进程结束后,init进程就会负责回收僵尸子进程。

      但是,如果父进程是一个服务器进程,一直循环着不退出,那子进程就会一直保持着僵尸状态。虽然僵尸进程不会占用任何内存资源,但是过多的僵尸进程总还是会影响系统性能的。黔驴技穷的情况下,该怎么办呢?

         这个时候就需要一个英雄来拯救整个世界,它就是两次fork()技法。

  • 两次fork()技法

         两次fork()的流程如下所示:

    

                图3    两次fork的控制流

       如上图3所示,为了避免子进程child成为僵尸进程,我们可以人为地创建一个子进程child1,再让child1成为工作子进程child2的父进程,child2出生后child1退出,这个时候child2相当于是child1产生的孤儿进程,这个孤儿进程由系统进程init回收。这样,当child2退出的时候,init就会回收child2的资源。

 1 int main(void)
 2 {
 3     pid_t        pid;
 4  
 5     if ( (pid = fork()) < 0)
 6           err_sys("fork error");
 7     else if (pid == 0) 
 8         {                /* first child */
 9            if ( (pid = fork()) < 0)
10                         err_sys("fork error");
11            else if (pid > 0)
12                  exit(0);        /* parent from second fork == first child */
13  
14                 /* We're the second child; our parent becomes init as soon
15                    as our real parent calls exit() in the statement above.
16                    Here's where we'd continue executing, knowing that when
17                    we're done, init will reap our status. */
18  
19             sleep(2);
20             printf("second child, parent pid = %d\n", getppid());
21             exit(0);
22         }
23  
24     if (waitpid(pid, NULL, 0) != pid)        /* wait for first child */
25             err_sys("waitpid error");
26  
27         /* We're the parent (the original process); we continue executing,
28            knowing that we're not the parent of the second child. */
29  
30     exit(0);
31 }

 

理所当然,第二个子进程的父进程是进程号为1的init进程。

 

总之,两次fork()是人为地创建一个工作子进程的父进程,然后让这个人为父进程退出,之后工作子进程就由init回收,避免了工作子进程成为僵尸进程。

 

如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。

下方程序中,当第二个子进程打印其父进程ID时,我们看到了一个潜在的竞争条件。如果第二个子进程在第一个子进程之前运行,则其父进程会是第一个子进程。

但是,如果第一个子进程先运行,并有足够的时间到达并执行exit,则第二个子进程的父进程就是init。即使在程序中调用sleep,也不能保证什么。如果系统负载很重,那么在sleep返回之后、第一个子进程得到机会之前,第二个子进程可能恢复运行。这种形式的问题很难调试,因为在大部分时间,这种问题并不会出现。

 1 #include "apue.h"
 2 #include <sys/wait.h>
 3 
 4 int
 5 main(void)
 6 {
 7     pid_t    pid;
 8 
 9     if ((pid = fork()) < 0) {
10         err_sys("fork error");
11     } else if (pid == 0) {        /* first child */
12         if ((pid = fork()) < 0)
13             err_sys("fork error");
14         else if (pid > 0)
15             exit(0);    /* parent from second fork == first child */
16 
17         /*
18          * We're the second child; our parent becomes init as soon
19          * as our real parent calls exit() in the statement above.
20          * Here's where we'd continue executing, knowing that when
21          * we're done, init will reap our status.
22          */
23         sleep(2);
24         printf("second child, parent pid = %ld\n", (long)getppid());
25         exit(0);
26     }
27 
28     if (waitpid(pid, NULL, 0) != pid)    /* wait for first child */
29         err_sys("waitpid error");
30 
31     /*
32      * We're the parent (the original process); we continue executing,
33      * knowing that we're not the parent of the second child.
34      */
35     exit(0);
36 }

如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个。

如果一个进程要等待其父进程终止(例如上述程序),则可使用下列形式的循环:

1 whie(getppid()!=12     sleep(1);

这种形式的循环称为轮训(polling),它的问题是浪费了CPU时间,因为调用者每隔1s都被唤醒,然后进行条件测试。

为了避免竞争条件和轮训,在多个进程之间需要有某种形式的信号发送和接收方法,在unix中可以使用信号机制。各种形式的进程间通信(IPC)也可使用。

 

posted @ 2018-09-17 17:03  宇尉  阅读(166)  评论(0编辑  收藏  举报