美文网首页
5.进程控制

5.进程控制

作者: 大雄good | 来源:发表于2016-12-14 23:16 被阅读0次

    进程控制

    这一节主要介绍Unix系统的进程控制,包括创建新进程、执行进程和进程终止。由于前面Linux学习部分有了一定的进程控制的理论知识,本节更多是结合实践代码介绍。

    1.fork

    一个现有进程通过调用fork函数创建一个新进程。

    #include<unistd.h>
    pid_t fork(void)
    /* 返回值:子进程返回0,父进程返回子进程ID;若出错返回-1 */
    

    子进程ID为0原因是因为系统中ID为0的进程总是由内核交换程序使用,所以子进程ID不可能为0。执行fork之后,子进程是父进程的副本,获得父进程的数据空间、堆和栈的副本。下面通过一个实例演示fork函数:

    #include "apue.h"
    
    int     globvar = 6;        /* external variable in initialized data */
    char    buf[] = "a write to stdout\n";
    
    int
    main(void)
    {
        int     var;        /* automatic variable on the stack */
        pid_t   pid;
    
        var = 88;
        if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
            err_sys("write error");
        printf("before fork\n");    /* we don't flush stdout */
    
        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0) {      /* child */
            globvar++;              /* modify variables */
            var++;
        } else {
            sleep(2);               /* parent */
        }
    
        printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
          var);
        exit(0);
    }
    

    运行程序得到:

    fork

    上述结果中,先运行了子进程,再运行了父进程(一般希望子进程先运行,但是不一定),子进程拷贝了父进程的数据空间,并对globvarvar执行加1操作,此时子进程和父进程都有各自的数据空间。

    父进程和子进程的文件共享如下图所示:

    子进程和父进程文件共享

    fork主要有两种用法:

    • 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。在网络服务进程中是常见的——父进程等待客户端的服务请求,请求到达后,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求
    • 一个进程执行一个不同的程序。对shell常见,子进程从fork返回后立即调用exec

    2.vfork

    由Linux内核设计的理论知道,vfork的原理是,父进程产生一个子进程,并阻塞父进程,子进程使用父进程的地址空间,直到子进程执行完毕,父进程继续执行。通过下面代码分析:

    #include "apue.h"
    
    int     globvar = 6;        /* external variable in initialized data */
    
    int
    main(void)
    {
        int     var;        /* automatic variable on the stack */
        pid_t   pid;
    
        var = 88;
        printf("before vfork\n");   /* we don't flush stdio */
        if ((pid = vfork()) < 0) {
            err_sys("vfork error");
        } else if (pid == 0) {      /* child */
            globvar++;              /* modify parent's variables */
            var++;
            _exit(0);               /* child terminates */
        }
    
        /* parent continues here */
        printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
          var);
        exit(0);
    }
    

    在子进程中对globvarvar执行了加1操作,因为使用的父进程地址空间,因此相对于修改了父进程数据,运行该程序可以得到结果:

    vfork

    3.进程终止

    进程终止时,内核逐个检查所有活动进程,判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就改为1,保证每个进程都有父进程。

    如果子进程在父进程之前终止,内核为每个终止子进程保存一定量的信息,所以当终止进程的父进程调用waitwaitpid时,可以得到这些信息(包括进程ID、进程终止状态以及使用的CPU时间总量)。内核可以释放终止进程所使用的所有存储区,关闭其所有文件夹。一个已经终止,但是父进程未对其进行善后的进程称为僵尸进程

    4.wait和waitpid

    当一个进程终止时,内核向其父进程发送SIGCHLD信号,因为子进程终止是一个异步时间,所以这个信号也是内核向父进程发的异步通知,父进程可以忽略该信号或者调用waitwaitpid。若是调用waitwaitpid,可能发生下面情况:

    • 如果其所有子进程都在运行,则阻塞
    • 如果一个子进程已经终止,正在等待父进程获取其终止状态,则取得该子进程的终止状态立即返回
    • 如果它没有任何子进程,则立即出错返回
    #include <sys/wait.h>
    pid_t wait(int *statloc);
    pid_t waitpid(pid_t pid, int *statloc, int options);
    /* 返回值:成功返回进程ID,出错返回0或-1 */
    

    这两个函数区别如下:

    • 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞
    • waitpid并不等待在其调用之后的第一个终止子进程,可以控制他所等待的进程

    如果一个子进程已经停止,且是一个僵尸进程,则wait立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止,返回终止子进程的ID。

    5.竞争条件

    当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件。例如在fork后无法预料那个进程先运行。

    这里给出一种解决方法:

    #include "apue.h"
    
    static void charatatime(char *);
    
    int
    main(void)
    {
        pid_t   pid;
    
        TELL_WAIT();
    
        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0) {
            WAIT_PARENT();      /* parent goes first */
            charatatime("output from child\n");
        } else {
            charatatime("output from parent\n");
            TELL_CHILD(pid);
        }
        exit(0);
    }
    
    static void
    charatatime(char *str)
    {
        char    *ptr;
        int     c;
    
        setbuf(stdout, NULL);           /* set unbuffered */
        for (ptr = str; (c = *ptr++) != 0; )
            putc(c, stdout);
    }
    

    上述代码即在子进程执行时,先等待父进程执行,而父进程内容中执行完毕时通知子进程当前进程执行完毕。

    6.exec

    fork创建新进程之后,往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

    有7种不同的exec可供使用,它们常称为exec函数:

    #include <unistd.h>
    int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
    int execv(const char *pathname, char *const argv[]);
    int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
    int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ ); int execvp(const char *filename, char *const argv[]);
    int fexecve(int fd, char *const argv[], char *const envp[]);
    /* 返回值:出错返回-1,成功不返回*/
    

    它们之间的关系可以由下图得到:

    exec

    相关文章

      网友评论

          本文标题:5.进程控制

          本文链接:https://www.haomeiwen.com/subject/blswmttx.html