进程控制

原文链接:http://www.orlion.ga/1044/

一、fork函数

#include <sys/types.h>

#include <unistd.h>

 

pid_t fork(void);

    fork调用失败返回-1。下面通过一个例子来理解fork是怎样创建进程的。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
        pid_t pid;
        char *message;
        int n;
        pid = fork();
        if (pid < 0) {
                perror("fork failed");
                exit(1);
        }
        if (pid == 0) {
                message = "This is the child\n";
                n = 6;
        } else {
                message = "This is the parent\n";
                n = 3;
        }
        for(; n > 0; n--) {
                printf(message);
                sleep(1);
        }
        return 0;
}

    输出

   

    fork-www.orlion.ga

    程序运行过程:

   

    1.父进程初始化

    2.父进程调用fork,这是一个系统调用,因此进入内核

    3.内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态和数据也相同。因此子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核中返回

    4.现在有两个一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork值调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法

    5.如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执行下面的else分支,然后执行for循环,打印"This is the parent\n"三次之后终止

    6.如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的if (pid == 0)分支,然后执行for循环,打印"This is the child\n"六次之后终止。fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。

    7.父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里子进程有可能被调度到。同样的,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交替打印

    8.这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态,当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没有结束,所以子进程的消息打印到了Shell提示符的后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。

 

    fork函数的特点是"调用一次,返回两次",在父进程和子进程中各返回一次。fork的另一个特点是所有由父进程打开的描述符都复制到了子进程中。父、子进程中相同的编号的文件描述符在内核中指向同一个file结构体,也就是说file结构体的引用计数要增加。

    

2、exec函数

    用fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数以执行另一个程序。当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

    有六种exec开头的函数:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const 
envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const 
envp[]);

    这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功返回的值。

    例:

#include <unistd.h>
#include <stdlib.h>
int main(void)
{
        execlp("ps", "ps", "-o", 
"pid,ppid,pgrp,session,tpgid,comm", NULL);
        perror("exec ps");
        exit(1);
}

    结果:

   

    因为exec只有错误返回值,只要有返回值一定是出错了,所以不需要判断它的返回值,直接在后面调用perror即可。

    system是在单独的进程中执行命令完了还会回到自己的程序中,而exec函数是直接在自己的程序中执行新的程序,新的程序会把自己的程序覆盖,除非调用出错,否则再也回不到exec后面的代码,就是说自己的程序就变成了exec调用的那个程序了。  

 

 

3、wait和waitpid函数

    一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait和waitpid获取这些信息,然后彻底清除掉这个进程。进程的退出状态可以在Shell中用$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。

    如果一个进程终止了,但是父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,为了观察到僵尸进程,下面写一个不正常的程序,父进程fork出子进程,子进程终止,而父进程既不终止也不调用wait请求进程:

#include <unistd.h>
#include <stdlib.h>
int main(void)
{
        pid_t pid=fork();
        if(pid<0) {
                perror("fork");
                exit(1);
        }
        if(pid>0) { /* parent */
                while(1);
        }
        /* child */
        return 0;         
}

    在后台运行这个程序:

   

    在./zombie后边加&表示后台运行,Shell不等待这个进程终止就立刻打印提示符并等待用户输命令。现在Shell是位于前台的,用户在终端的输入会被Shell读取,后台进程是读不到终端输入的。第二条命令ps u是在前台运行的,在此期间Shell进程和zombie进程都在后台运行,等到ps u命令结束时Shell进程又重新回到前台。

    父进程的pid是5818,子进程是僵尸进程pid是5819,ps命令显示僵尸进程的状态为z,在命令行一栏还显示<defunct>.

    如果一个父进程终止而它的子进程还存在,则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。

    僵尸进程是不能用kill命令清除掉的,因为kill命令只是用来终止进程的,而僵尸进程已经终止了。

    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。

#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");
                }
                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取出的字段非零,WEXITSATTUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WISIGNLED取出的字段值非零,WTERMSIG取出的字段值就是信号的编号。

posted @ 2016-04-03 21:31  orlion  阅读(382)  评论(0编辑  收藏  举报