• BSP Day50


    今天我们继续学习昨天没有学习完的进程的相关知识。

    进程共享

    父子进程之间在fork后。有哪些相同,哪些相异之处呢?

    父子相同处:

    全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…

    父子不同处:

    1. 进程ID

    2. fork返回值

    3. 父进程ID

    4. 进程运行时间

    5. 闹钟(定时器)

    6. 未决信号集

    似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。

    父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己

    的逻辑都能节省内存开销。

    接下来用一个程序来测试下,父子进程是否共享全局变量。

    1. #include
    2. #include
    3. #include
    4. int var = 100;
    5. int main()
    6. {
    7. printf("init var = %d\n",var);
    8. pid_t pid = fork();
    9. if(pid<0)
    10. {
    11. perror("pid error");
    12. exit(1);
    13. }
    14. else if(pid>0)
    15. {
    16. var = 120;
    17. printf("parent,var = %d\n",var);
    18. printf("I' am parent pid = %d,getppid = %d\n",getpid(),getppid());
    19. }
    20. else if(pid == 0)
    21. {
    22. var = 140;
    23. printf("child,var = %d\n",var);
    24. printf("I' am child pid = %d,getppid = %d\n",getpid(),getppid());
    25. }
    26. printf("------finish----------\n");
    27. return 0;
    28. }

    结论:

    【注】:父子进程不共享全局变量。

    【注】:父子进程共享。

    1. 文件描述符(打开文件的结构体)
    2. mmap建立的映射区

    特别的,fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法

    fork生成的子进程和父进程的功能一样,如果想让fork生成的子进程的功能不一样,即拥有与父进程不一样的代码段数据段以及堆栈段,应该怎么办呢?

    使用exec函数系列。

    fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。

    exec替换进程映像:

    在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离。这样的好处是有更多的余地对两种操作进行管理。

    当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用exec系列的函数来进行。当然,exec系列的函数也可以将当前进程替换掉。

    例如:在shell命令行执行ps命令,实际上是shell进程调用fork复制一个新的子进程,在利用exec系统调用将新产生的子进程完全替换成ps进程。

    exec系列函数(execl,execlp,execle,execv,execvp)

    包含头文件

    功能:

     用exec函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。exec名下是由多个关联函数组成的一个完整系列。

    头文件

    extern char **environ;

    原型:

    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[]);

    参数:

    path参数表示你要启动程序的名称包括路径名

    arg参数表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束

    返回值:成功返回0,失败返回-1

    注:上述exec系列函数底层都是通过execve系统调用实现:

           #include

           int execve(const char *filename, char *const argv[],char *const envp[]);

    DESCRIPTION:
           execve() executes the program pointed to by filename.  filename must be
           either a binary executable, or a script starting with  a  line  of  the form

    若参数file文件名:包含/视为路径名,否则按PATH环境变量去指定各目录搜寻文件
      函数名的解读:
        (1)l 表示参数以列表(list)方式提供。
        (2)v 表示参数以数组[向量(vector)]方式提供。
        (3)p 表示用户在PATH环境变量中寻找可执行文件。
    (只需简单提供文件名,主要用于shell,因为shell所指向进程通常会从shell继承环境变量)
        (4)e 表示会提供给新进程以新的环境变量。
      exec系列函数没有一个同时可搜索路径和使用新环境变量的函数。
      exec系列函数成功调用不仅改变地址空间与进程映像,
      还改变进程的一些属性:
        1.任何挂起的信号都会丢失。
        2.捕捉的任何信号会还原为缺省的处理方式,因为信号处理函数已经不存在于地址空间中了。
        3.任何内存的锁定会丢失。
        4.多数线程的属性会还原到缺省值。
        5.多数关于进程的统计信息会复位。
        6.与进程内存相关的任何数据都会丢失,包括映射的文件。
        7.包括c语言库的一些特性(例如aexit())等独立存在于用户空间的数据都会丢失。
      未改变的进程属性例如 pid、父进程的pid、优先级、所属的用户和组。

    实例:

    1. #include
    2. #include
    3. #include
    4. int main (int argc,char *argv[])
    5. {
    6. int ret = 0; //返回值
    7. printf("Executing ls\n");
    8. /**
    9. * 调用execl函数
    10. * 参数1:带路径文件名
    11. * 参数2:文件名
    12. * 最后参数:NULL
    13. */
    14. ret = execl("/bin/ls","ls","-l",NULL);
    15. /**
    16. * 若execl()函数有返回,说明调用失败
    17. */
    18. if(ret == -1){
    19. perror("execl failed to run ls");
    20. }
    21. exit(1); //退出
    22. }

    编译及运行结果:

    1.编译gcc execl.c -o execl
    2.运行:./execl
    3.结果: 

    -rwxrwxr-x 1 hhb hhb 16232 11月 7 14:47 fork

    -rw-rw-r-- 1 hhb hhb 563 11月 7 14:47 fork.c

    结论:

    execl()调用后紧跟着 perror()的无条件调用。这是因为若调用程序还存在且 execl()调用返回,那么肯定是 execl()调用出错了。
    只要 execl()其它exec 调用成功,就肯定清除了调用程序而代之以新的程序。 

    execv函数实例:

    1. #include
    2. #include
    3. #include
    4. int main (int argc,char *argv[])
    5. {
    6. char* av[] = {"ls","-1",NULL};
    7. execv("/bin/ls",av);
    8. perror("execl failed");
    9. exit(1);
    10. }

     编译及运行结果:

    1.编译gcc execv.c -o execv
    2.运行:./execv
    3.结果: 

    -rwxrwxr-x 1 hhb hhb 16232 11月 7 14:47 fork

    -rw-rw-r-- 1 hhb hhb 563 11月 7 14:47 fork.c

    execlp()和 execvp()(p表示用户在PATH环境变量中寻找可执行文件)
    execlp()和 execvp()分别类似于系统调用 execl()和 execv(),主要区别是:
    函数名后多了一个p,多用于shell,因为shell所执行进程通常会从shell继承环境变量,表示:
    第一个参数指向的是文件名(不包含路径)。
    可通过检索 shell 环境变量 PATH指出的目录,来得到该文件名的路径前缀部分。如可在 shell 中用下述命令序列来设置环境变量 PATH:

    $PATH=/bin;/usr/bin;/sbin

    $export PATH

    即: execlp() execvp()执行时:先在目录/bin,然后在目录/usr/bin,最后在目录/sbin 中搜索程序文件。另外, execlp() execvp()还可以用于运行 shell 程序,而不只是普通的程序。 

    execvp函数实例

    1. #include
    2. #include
    3. #include
    4. int main (int argc,char *argv[])
    5. {
    6. int ret = 0; //返回值
    7. /**
    8. * 调用execvp函数
    9. * 参数1:可执行文件名
    10. * 参数2:参数
    11. * 最后参数:NULL
    12. */
    13. char *const args[] = {"vi","./data.txt",NULL};
    14. ret = execvp("vi",args);
    15. /**
    16. * 若execvp()函数有返回,说明调用失败
    17. */
    18. if(ret == -1){
    19. perror("execvp");
    20. }
    21. exit(1); //退出
    22. }

    execle()和 execve()(e表示会提供给新进程以新的环境变量)

    execle()和 execve()分别类似于系统调用 execl()和 execv(),主要区别是:
    函数名后多了一个e。

    实例:

    1. #include
    2. #include
    3. #include
    4. int main (int argc,char *argv[])
    5. {
    6. char *args[] = {"/bin/ls",NULL};
    7. printf("PID = %d\n",getpid());
    8. if(execve("/bin/ls",args,NULL) < 0)
    9. {
    10. perror("execve");
    11. }
    12. exit(1);
    13. }

    错误的返回值:

    回收子进程

    孤儿进程
    孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

    僵尸进程
    僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

    【注意】:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。

    清除掉僵尸进程的方法:

    1. wait();
    2. waitpid();
    3. 杀死其父进程 kill -9 ppid,使其变成孤儿进程, init进程回收

    wait函数 

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

    父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
    ① 阻塞等待子进程退出
    ② 回收子进程残留资源
    ③ 获取子进程结束状态(退出原因)。

    pid_t wait(int *status);

    成功:清理掉的子进程`ID`;失败:`-1` (没有子进程)

    当进程终止时,操作系统的隐式回收机制会:
    1.关闭所有文件描述符
    2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
    可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

    1. WIFEXITED(status) 为非0    → 进程正常结束
        WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
    2. WIFSIGNALED(status) 为非0 → 进程异常终止
        WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
    3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态
        WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
        WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行

    waitpid函数

    作用同wait,但可指定pid进程清理,可以不阻塞。

    pid_t waitpid(pid_t pid, int *status, in options);    
    成功:返回清理掉的子进程`ID`;
    失败:-1(无子进程)
    特殊参数和返回情况:
    参数`pid`: 
    > 0 回收指定`ID`的子进程    
     -1 回收任意子进程(相当于`wait`)
      0 回收和当前调用`waitpid`一个组的所有子进程
    <-1 回收指定进程组内的任意子进程
    返回0:参3为`WNOHANG`,且子进程正在运行。

    注意:一次`wait`或`waitpid`调用只能清理一个子进程,清理多个子进程应使用循环。    

     今天就学到这儿了,明天接着分享。


     

  • 相关阅读:
    order by注入与limit注入
    30天拿下Rust之前世今生
    【Redis】List列表相关的命令
    微信小程序日期增加时间完成订单失效倒计时(有效果图)
    取消检验批过账(取消检验批UD判定到Rerel,再把非限性库存转到质检库存,然后就可以取101收货了)
    【图像去雾】基于matlab暗通道和非均值滤波图像去雾【含Matlab源码 2011期】
    【经典控制理论】| 自动控制原理知识点概要(上)
    【算法优选】 滑动窗口专题——壹
    LeetCode通关:连刷十四题,回溯算法完全攻略
    linux kernel内核调试方法(二)
  • 原文地址:https://blog.csdn.net/weixiaxiao/article/details/127436132