zoukankan      html  css  js  c++  java
  • 进程控制

    1. 程序和进程

    什么是程序?什么是进程?

    • 程序是计算机存储系统中的数据文件,如源代码程序和可执行程序
    • 进程是程序关于某个数据集合的一次运行活动,是程序执行后得到的一个实体
    • 在当代操作系统中,进程是资源分配的基本单位

    程序和进程有什么联系?

    • 没有程序就没有进程;但有了程序,未必就会有进程,如程序不运行、程序本身是动态库等
    • 一个程序可能对应多个进程,如记事本程序多次运行,产生多个记事本进程
    • 一个进程可能包含多个程序,如一个程序依赖多个动态库,每个动态库都是一个程序

    2. 进程状态

    进程三态模型:就绪、阻塞、运行。

    • 就绪:进程已经做好了一切准备,一旦得到CPU,就会开始运行
    • 阻塞:进程正在等待某一事件发生(如共享资源被释放、IO完成)而停止运行,在事件发生前,即使得到CPU也无法运行
    • 运行:进程拥有CPU控制权,并正在运行

    进程五态模型:与三态模型相比,多了新建、终止两种状态。

    • 新建:进程还未创建完毕,不能被系统调度
    • 终止:进程已结束运行,正在回收系统资源

    3. 进程标识

    每个进程都有一个非负整数的进程ID(pid),作为识别不同进程的唯一标识。
    此外,每个进程还有一些其他标识符,包括父进程ID(ppid)、实际用户ID(uid)、有效用户ID(euid)、实际组ID(gid)、有效组ID(egid)。

    #include <unistd>
    
    pid_t getpid();   //返回值:调用进程的进程ID
    pid_t getppid();  //返回值:调用进程的父进程ID
    uid_t getuid();   //返回值:调用进程的实际用户ID
    uid_t geteuid();  //返回值:调用进程的有效用户ID
    uid_t getgid();   //返回值:调用进程的实际组ID
    uid_t getegid();  //返回值:调用进程的有效组ID
    

    4. 进程创建

    一个现有的进程可以调用fork函数创建一个新进程,这个新进程叫做子进程,调用fork的进程叫做父进程。

    • 子进程获得父进程数据空间、堆、栈的副本
    • 父进程和子进程共享代码段、文件描述符和文件偏移量
    #include <unistd.h>
    
    pid_t fork();  //若成功:子进程返回0,父进程返回子进程ID;若出错,返回-1
    

    fork的特点为:一次调用,两次返回。

    • 父进程返回子进程ID的原因:父进程可以有多个子进程,但父进程不能通过函数获得其所有子进程的ID,因为没有这样的函数
    • 子进程返回0的原因:一个进程只会有一个父进程,并且子进程还可以调用getppid获得其父进程ID

    fork有以下两种用法:

    • 父进程和子进程同时执行不同的代码段
    • 子进程从fork返回后立即调用exec,执行另一个不同的程序

    fork成功返回后,父进程和子进程继续执行后面的代码,但父子进程谁先执行,这点是不确定的。

    #include <stdio.h>
    #include <unistd.h>
    
    int globvar = 10;
    
    int main()
    {
        int var = 5;
    
        pid_t pid = fork();
    
        if (pid > 0)
        {
            sleep(2); //父进程休眠2秒,让子进程先运行
        }
        else if (pid == 0)
        {
            globvar++;
            var++;
        }
    
        printf("pid = %d, globvar = %d, var = %d
    ", getpid(), globvar, var);
    
        return 0;
    }
    

    5. 进程终止

    正常终止方式:

    • 从main函数return
    • 调用exit()、_Exit()、_exit()(对于linux,后两个函数是同义的)
    • 进程的最后一个线程在其启动例程中调用return或pthread_exit()

    异常终止方式:

    • 调用abort()以产生SIGABRT信号
    • 进程接收到某些信号
    • 进程的最后一个线程对pthread_cancel()请求做出响应

    6. 避免僵尸进程

    僵尸进程的产生与危害

    • 一个已经终止、但是其父进程尚未对其进行善后处理的进程,称为僵尸进程
    • 子进程退出时,内核会释放它占用的内存等资源,但是仍然保留了一些信息,如进程ID
    • 内核为终止子进程保留的信息直到父进程调用wait或waitpid时才会释放
    • 如果父进程没有调用wait或waitpid,那么已经终止的子进程就会变成僵尸进程,其占用的进程ID会无法释放
    • 大量的僵尸进程可能会使系统没有可用的进程ID,从而导致系统无法创建新进程

    wait函数

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t wait(int *status); //成功返回终止子进程ID,失败返回-1
    

    当在父进程中调用了wait时:

    • 如果所有子进程都还在运行,则父进程阻塞
    • 如果有任意子进程终止,则取得其终止状态并立即返回
    • 如果父进程没有子进程,则立即出错返回

    如果wait的参数status不为NULL,那么子进程的终止状态就存放在它指向的内存中,如果不关心终止状态,可以将status指定为NULL。

    waitpid函数

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t waitpid(pid_t pid, int *status, int options); //成功返回终止子进程ID,失败返回-1或0
    

    waitpid的第二个参数status用法和wait一样,但waitpid相比于wait的不同之处在于:

    • pid > 0时,waitpid可以等待由pid指定的特定子进程
    • options == WNOHANG时,若pid指定的子进程尚未终止,waitpid不会阻塞,而是立即返回0
    • pid == -1 && options == 0时,waitpid等价于wait

    虽然waitpid可以实现非阻塞版本的wait,但也存在一个缺陷:如果子进程在父进程waitpid(pid, NULL, WNOHANGE)之后才终止,那么即使父进程尚未结束,也不会给子进程收尸,也就是说,终止的子进程会一直处于僵尸进程状态,直到父进程退出。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {    
        pid_t pid = fork();
        
        if (pid == 0)
        {
            sleep(2); //确保父进程执行waitpid时子进程还在休眠
            printf("child %d exit
    ", getpid());  
            exit(0);      
        }
            
        if (waitpid(pid, NULL, WNOHANG) == 0)
        {
            printf("waitpid return before child exit
    ");
        }
        
        sleep(300);   //虽然waitpid不阻塞,但在父进程终止前,子进程pid会一直是僵尸进程
            
        return 0;
    }
    
    

    如果既要保证父进程不阻塞等待子进程终止,也不希望子进程处于僵尸状态直到父进程终止,可以采用调用两次fork的诀窍,其核心思路为:

    • 进程A调用fork产生子进程B,然后立即调用waitpid(pid, NULL, 0),等待进程B终止
    • 进程B再次调用fork产生子进程C,然后立即调用exit(0)终止(必须确保进程B在进程C之前终止)
    • 进程A随即解除waitpid阻塞,对进程B收尸处理
    • 由于父进程提前终止,进程C由init收养,其终止时也会由init收尸
    • 此时,进程A和进程C就成为相互独立、互不干扰的两个进程,两者各司其职,分别执行不同的处理
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <stdio.h>
    #include <stdlib.h>
     
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {
            if ((pid = fork()) > 0)
            {
                exit(0); //子进程B再次调用fork后立即终止,只留下孙子进程C
            }
            
            /*孙子进程C由init进程收养*/
            sleep(2);
            printf("I'm second child %d, my parent bacomes %d
    ", getpid(), getppid());
            
            exit(0);
        }
        
        printf("I'm parent %d
    ", getpid());
        
        if (waitpid(pid, NULL, 0) == pid) //父进程A立即调用waitpid等待子进程B终止
        {
            printf("first child %d exit
    ", pid);
        }
        
        /*此时,进程A和进程C之间就没有了继承关系,两者相互独立,互不干扰,各司其职*/
        
        sleep(300);
        
        return 0;
    }
    

    从上图执行结果可以看出:

    • 进程B(pid=9293)已经在进程列表中找不到了,说明已经被收尸处理了,没有产生僵尸进程
    • 进程C(pid=9294)也不在进程列表中了,说明其终止后由init收尸处理了,也没有产生僵尸进程

    7. exec函数族

    exec函数及使用规则

    上面提到过fork的两种用法,其中一种是“子进程从fork返回后立即调用exec,执行另一个不同的程序”。

    • 当进程调用exec函数时,其执行的程序将完全替换为exec指定的新程序,而新程序则从其main()开始执行。
    • 因为exec并不创建新进程,所以替换前后的进程ID不会改变,exec只是用磁盘上的一个新程序替换了调用进程的代码段、数据段和堆栈。

    有7个不同的exec函数可供使用,它们统称为exec函数族,可以根据需要调用这7个函数中的任意一个。

    #include <unistd.h>
    
    /*7个函数返回值均为:若成功,不返回;若失败,返回-1*/
    
    //以路径名为参数
    int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
    int execv(const char *path, char *const argv[]);
    int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/);
    int execve(const char *path, char *const argv[], char *const envp[]);
    
    //以文件名为参数
    int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
    int execvp(const char *file, char *const argv[]);
    
    //以文件描述符为参数
    int fexecve(int fd, char *const argv[], char *const envp[]);
    

    这些函数的命名是有规律的:exec[必选:l or v][可选:p or e](fexecve作为特例单独拎出来)
    这些函数的参数用于指定新程序的相关信息,分为3部分:可执行程序、命令行参数、环境变量,具体使用规则为:
    【必选项l or v】

    • 带l,各个命令行参数必须以','间隔,最后一个命令行参数必须是NULL
    • 带v,需要将l后续的各个命令行参数(包括最后的NULL)构造成一个指针数组,然后以该指针数组作为参数

    【可选项p】

    • 不带p,以可执行程序的路径名path作为参数
    • 带p,以可执行程序的文件名file作为参数,如果file中包含/,则视作路径名,否则从PATH环境变量指定的目录中搜索可执行程序

    【可选项e】

    • 不带e,复制调用进程的环境变量给新程序使用
    • 带e,需要传递环境变量给新程序使用

    【fexecve特例】

    • 后缀ve含义和上面一样
    • 前缀f代表新程序由文件描述符fd指定

    exec函数使用示例

    /* filename - execl.c */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {
            execl("/bin/echo", "echo", "executed by execl", NULL);
        }
        
        waitpid(pid, NULL, 0);
        
        return 0;
    }
    
    /* filename - execv.c */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    char *execv_argv[] = 
    {
        "echo",
        "executed by execv",
        NULL
    };
    
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {      
            execv("/bin/echo", execv_argv);
        }
        
        waitpid(pid, NULL, 0);
        
        return 0;
    }
    
    /* filename - execlp.c */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {       
            execlp("echo", "echo", "executed by execlp", NULL);
        }
        
        waitpid(pid, NULL, 0);
        
        return 0;
    }
    
    /* filename - execvp.c */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    char *execvp_argv[] = 
    {
        "echo",
        "executed by execvp",
        NULL
    };
    
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {
            execvp("echo", execvp_argv);
        }
        
        waitpid(pid, NULL, 0);
        
        return 0;
    }
    
    /* filename - execle.c */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    char *env[] = 
    {
        "PATH=/home/delphi",
        "USER=execle",
        NULL
    };
    
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {
            execle("/usr/bin/env", "env", NULL, env);
        }
        
        waitpid(pid, NULL, 0);
        
        return 0;
    }
    
    /* filename - execve.c */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    char *execve_argv[] = 
    {
        "env",
        NULL
    };
    
    char *env[] = 
    {
        "PATH=/home/delphi",
        "USER=execve",
        NULL
    };
    
    int main()
    {
        pid_t pid = fork();
        
        if (pid == 0)
        {
            execve("/usr/bin/env", execve_argv, env);
        }
        
        waitpid(pid, NULL, 0);
        
        return 0;
    }
    

    8. system函数

    #include <stdlib.h>
    
    int system(const char *command);
    

    system返回值

    在Unix系统中,system在其内部实现调用了fork、exec和waitpid,因此有3种返回值。

    • 如果fork失败,或者waitpid返回除EINTR之外的错误,则system返回-1,并且设置errno以指示错误类型
    • 如果exec失败,比如被信号中断,或者command命令不存在,system返回127
    • 如果fork、exec和waitpid都成功,system的返回值是shell的终止状态,即command通过exit或return返回的值

    下面通过一个system的简易实现,来帮助理解该函数的返回值。

    int system(const char * cmdstring)
    {
        pid_t pid;
        int status;
    
        if (cmdstring == NULL)
        {
            return (1); //如果cmdstring为空,返回非零值,一般为1
        }
    
        if ((pid = fork()) < 0)
        {
            status = -1; //fork失败,返回-1
        }
        else if (pid == 0)
        {
            execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
            _exit(127); // exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的进程就不存在啦~~
        }
        else //父进程
        {
            while (waitpid(pid, &status, 0) < 0)
            {
                if (errno != EINTR)
                {
                    status = -1; //如果waitpid被信号中断,则返回-1
                    break;
                }
            }
        }
    
        return status; //如果waitpid成功,则返回子进程的返回状态
    }
    

    仔细看完这个system函数的简单实现,该函数的返回值就清晰了吧,那么什么时候system()返回0呢?答案是只在command命令返回0时。

    system使用示例

    #include <stdlib.h>
    
    int main()
    {
        system("ls -l");
        system("cat func.c");    
        system("gcc -o func.out func.c");
        system("ls -l");
        system("echo main.out begin system func.out");
        system("./func.out");
        
        return 0;
    }
    

  • 相关阅读:
    有个表叫杨表(上)
    Codeforces Round #698 (Div. 2) 题解 全部6题
    Leetcode 821. 字符的最短距离
    gitbook mermaid不能渲染问题
    adb命令启动app及查找系统版本号
    git库使用
    excle转html方法
    gitbook插入视频
    xcode使用技巧
    在 Mac 上的“自动操作”工作流程中使用 Shell 脚本操作
  • 原文地址:https://www.cnblogs.com/songhe364826110/p/11432253.html
Copyright © 2011-2022 走看看