Linux系统编程之进程控制(进程创建、终止、等待及替换)

进程创建

在上一节讲解进程概念时,我们提到fork函数是从已经存在的进程中创建一个新进程。那么,系统是如何创建一个新进程的呢?这就需要我们更深入的剖析fork函数。

1.1 fork函数的返回值

调用fork创建进程时,原进程为父进程,新进程为子进程。运行 man fork后,我们可以看到如下信息:

#include <unistd.h>
pid_t fork(void);
</unistd.h>

fork函数有两个返回值,子进程中返回0,父进程返回子进程pid,如果创建失败则返回-1。

实际上,当我们调用fork后,系统内核将会做:

  • 分配新的内存块和内核数据结构(如task_struct)给子进程
  • 将父进程的部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表中
  • fork返回,开始调度

Linux系统编程之进程控制(进程创建、终止、等待及替换)

1.2 写时拷贝

在创建进程的过程中,默认情况下,父子进程共享代码,但是数据是各自私有一份的。如果父子只需要对数据进行读取,那么大多数的数据是不需要私有的。这里有三点需要注意:

第一,为什么子进程也会从fork之后开始执行?

因为父子进程是共享代码的,在给子进程创建PCB时,子进程PCB中的大多数数据是父进程的拷贝,这里面就包括了程序计数器(PC)。由于PC中的数据是即将执行的下一条指令的地址,所以当fork返回之后,子进程会和父进程一样,都执行fork之后的代码。

第二,创建进程时,子进程需要拷贝父进程所有的数据吗?

父进程的数据有很多,但并不是所有的数据都要立马使用,因此并不是所有的数据都进行拷贝。一般情况下,只有当父进程或者子进程对某些数据进行写操作时,操作系统才会从内存中申请内存块,将新的数据拷写入申请的内存块中,并且更改页表对应的页表项,这就是 写时拷贝。原理如下图所示:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

第三,为什么数据要各自私有?

这是因为进程具有独立性,每个进程的运行不能干扰彼此。

1.3 fork函数的用法及其调用失败的原因

fork函数的用法:

  • 一个父进程希望复制自己,通过条件判断,使父子进程分流同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
  • 如子进程从fork返回后,调用进程替换的函数,如exec等(将会在本节4.程序替换中讲解)。

fork函数调用失败的原因:

  • 系统中进程太多
  • 实际用户的进程数超过了限制

2.进程终止

2.1 进程终止的原因

进程终止的原因有三种

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2 常见的进程退出方法

进程正常终止

1.从main函数return,这是最常见的进程退出方法。在函数设计中,0代表正确,非0代表错误。其中不同的非0的退出码对应了退出原因。

2.调用exit或者_exit

_exit函数是系统调用,执行 man _exit可以看到

#include <unistd.h>
void _exit(int status);
</unistd.h>

status 定义了进程的终止状态。父进程可以通过wait来获得子进程的status(会在3.进程等待中讲解)。

需要注意的是,

exit函数是库函数,虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值 是255。

#include <stdlib.h>
void exit(int status);
</stdlib.h>

从作用上来看,_exit和exit是相似的,exit是对_exit的封装,exit的执行实际上是通过调用_exit来实现的。

但是二者也有一些细微的差别,请看如下代码段:

代码1

int main()
{
 printf("Hello world");
 exit(0);
}

Linux系统编程之进程控制(进程创建、终止、等待及替换)

代码2

 #include<stdio.h>
 #include<unistd.h>
 int main()
 {
   printf("Hello world");
    _exit(0);                                                  }
</unistd.h></stdio.h>

Linux系统编程之进程控制(进程创建、终止、等待及替换)

相比于_exit函数,exit函数先要执行用户定义的清理函数,在 冲刷缓冲区,关闭所有打开的流,将所有的缓存数据写入文件后,再调用_exit。因此我们可以看到,执行exit输出了”hello World”,而执行_exit并没有输出。

那么,return和exit有什么区别呢?

在普通函数中,return是用来终止函数的,只有在main函数中才是终止进程,而exit无论在哪里,一旦调用,整个进程就会终止。

3.进程等待

3.1 为什么要有进程等待?

在讲进程概念时我们提到,当子进程退出,父进程如果不管不顾,子进程残留资源(PCB)存放于内核中,就可能会造成僵尸进程。如果该资源不能得到释放,就会导致内存泄漏。僵尸进程是不能使用 kill -9 命令清除掉的。因为 kill 命令只是用来终止进程的, 而僵尸进程已经终止。

同时,父进程派给子进程的任务完成的如何,我们是需要知道的。例如,子进程运行完成,结果对还是不对, 或者是否正常退出。

因此,就需要父进程通过进程等待的方式,回收子进程的资源。

3.2 进程等待的方法

一个进程在终止时会关闭所有文件,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是 正常终止则保存着退出状态,如果是 异常终止则保存着导致该进程终止的信号是哪个。当这个进程的父进程调用 wait 或 waitpid 获取这些信息后,才会将这个进程彻底清除掉。

一个进程的退出状态可以在 Shell 中通过运行 echo $?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出 状态同时彻底清除掉这个进程。

3.2.1 wait函数

#include<sys types.h>
#include<sys wait.h>
pid_t wait(int*status);
</sys></sys>
  • 返回值:成功返回被等待进程pid,失败返回-1。
  • status:是一个输出型参数,将wait函数内部计算的结果通过status返回给调用者,父进程从而获取子进程退出状态,如果不关心子进程的退出状态则可以将参数设置成为NULL。

这里提一下输入型参数和输出型参数的区别,输入型参数是调用者给函数传的参数,而输出型参数是是函数将内部计算结果返回给调用者,因此输出型参数往往用指针。

父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)。

当父进程调用wait得到传出参数status后,可以借助宏函数来进一步判断进程终止的具体原因:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。( 查看进程是否是正常退出

WEXITSTATUS(status): 若WIFEXITED非零,说明子进程正常终止,提取子进程退出码。( 查看进程的退出码(exit 的参数))

3.2.2 waitpid函数

作用同 wait,但 waitpid可指定 pid 进程清理,可以通过非阻塞方式等待子进程退出。

pid_ t waitpid(pid_t pid, int *status, int options);

pid:

  • pid = -1,等待任一子进程退出,此时与wait等效
  • pid > 0, 回收指定 ID 的子进程,pid为指定进程的进程号。 *如果不存在该子进程,则立即出错返回

status:

  • 同wait

option:

  • 0:阻塞模式,即父进程会阻塞在waitpid处,等到子进程退出后继续。
  • WNOHANG: 非阻塞模式,若pid指定的子进程没有结束,则waitpid函数返回0,不予以等待。若正常结束,则返回该子进程的ID。一般情况下,非阻塞模式需要搭配循环使用。

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

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

3.3.3 子进程的status

关于status的用法,我已经在wait函数处讲解,此处不再赘述。这里将从底层的角度剖析status的含义。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。

Linux系统编程之进程控制(进程创建、终止、等待及替换)

我们以下一段代码为例,来展示一下非阻塞等待方式

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys wait.h>
int main()
{
   pid_t pid;

    pid = fork();
    if(pid < 0){
       printf("%s fork error\n",__FUNCTION__);
        return 1;

    }else if( pid == 0  ){ //child
     printf("child is run, pid is : %d\n",getpid());
     sleep(5);
     exit(1);
    } else{
       int status = 0;
       pid_t ret = 0;
       do
       {
       ret = waitpid(-1, &status, WNOHANG);//&#x975E;&#x963B;&#x585E;&#x5F0F;&#x7B49;&#x5F85;
       if( ret == 0  ){
       printf("child is running\n");
       }
       sleep(1);
       }while(ret == 0);

       if( WIFEXITED(status) && ret == pid  ){
          printf("wait child 5s success, child return codeis:%d.\n",WEXITSTATUS(status));
          }else{
          printf("wait child failed, return.\n");
          return 1;
          }
    }
     return 0;
}

</sys></stdlib.h></unistd.h></stdio.h>

这段代码先创建子进程,让子进程等待5s再退出,父进程每1s检查一下,5s后子进程退出,ret将变成子进程的进程号,退出循环等待。最终的运行结果如下:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

4.进程替换

4.1进程替换的原理

在讲进程替换原理前,我们需要先知道什么是进程替换。在讲fork函数时我们提到,fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),如果此时我们用一个新的程序替 换掉子进程的地址空间、代码段和数据,子进程将会从新程序的启动例程开始执行,这就是进程替换。

进程替换并不是创建新的进程,因为替换前后该进程的PID并未改变。

4.2 环境变量

进程替换需要用到一种exec函数,在讲exec函数族之前,我们先介绍一下环境变量的概念。

4.2.1常见的环境变量

按照惯例,环境变量字符串都是name=value 这样的形式,大多数 name 由大写字母加下划线组成,一般把name 的部分叫做环境变量,value 的部分则是环境变量的值。

环境变量定义了进程的运行环境,具有全局属性,因此设置环境变量时要加export,一些比较重要的环境变量的含义如下:

PATH

可执行文件的搜索路径。ls 命令也是一个程序,执行它不需要提供完整的路径名/bin/ls, 然而通常我们执行当前目录下的程序 a.out 却需要提供完整的路径名./a.out,这是因为 PATH 环境变量的值里面包含了 ls 命令所在的目录/bin,却不包含 a.out 所在的目录。

PATH 环境变量的值可以包含多个目录,用:号隔开。在 Shell 中用 echo 命令可以查看这个环境变量的值: echo $PATH

Linux系统编程之进程控制(进程创建、终止、等待及替换)

SHELL

当前 Shell,它的值通常是/bin/bash。

Linux系统编程之进程控制(进程创建、终止、等待及替换)

TERM

当前终端类型

Linux系统编程之进程控制(进程创建、终止、等待及替换)

HOME

当前用户主目录的路径, 很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

Linux系统编程之进程控制(进程创建、终止、等待及替换)

4.2.2与环境变量相关的函数

getenv函数

获取环境变量值: char *getenv(const char *name);

成功:返回环境变量的值;失败:NULL (name 不存在)

setenv 函数

设置环境变量的值 :i nt setenv(const char *name, const char *value, int overwrite);

成功:返回0;失败: 返回-1

参数 overwrite 取值:

1:覆盖原环境变量

0:不覆盖。(该参数常用于设置新环境变量,如:HELLO = “hello”)

unsetenv 函数

删除环境变量 name 的定义: int unsetenv(const char *name);

成功:0;失败:-1

注意事项:name 不存在仍返回 0(成功)。

4.2.3 环境变量的组织形式

Linux系统编程之进程控制(进程创建、终止、等待及替换)

environ 变量是一个char * 类型,存储着系统的环境变量。*每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。

4.3 exec函数族

4.3.1 exec函数族的使用

知道了环境变量的概念后,再简要介绍一下命令行参数。当我们在某个目录下输入 ls -als -l时,会有如下显示:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

我们发现,同样的ls命令,由于后面所跟的字符串不同,显示了不同的结果。这里的”-a”,”-l”被称为参数。实际上,一个程序内可以通过加入参数,让相同的程序执行不同的功能。

接下来我们来介绍进程替换必不可少的函数族——exec函数族。

其实有六种以 exec 开头的函数,统称 exec 函数:

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>
int main()
{
 char *const argv[] = {"ps", "-ef", NULL};//argv[0]&#x59CB;&#x7EC8;&#x662F;&#x7A0B;&#x5E8F;&#x540D;
 char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
 //execl("/bin/ps", "ps", "-ef", NULL);

 // &#x5E26;p&#x7684;&#xFF0C;&#x53EF;&#x4EE5;&#x4F7F;&#x7528;&#x73AF;&#x5883;&#x53D8;&#x91CF;PATH&#xFF0C;&#x65E0;&#x9700;&#x5199;&#x5168;&#x8DEF;&#x5F84;
 //execlp("ps", "ps", "-ef", NULL);

 // &#x5E26;e&#x7684;&#xFF0C;&#x9700;&#x8981;&#x81EA;&#x5DF1;&#x7EC4;&#x88C5;&#x73AF;&#x5883;&#x53D8;&#x91CF;
 //execle("ps", "ps", "-ef", NULL, envp);

 //execv("/bin/ps", argv);

 // &#x5E26;p&#x7684;&#xFF0C;&#x53EF;&#x4EE5;&#x4F7F;&#x7528;&#x73AF;&#x5883;&#x53D8;&#x91CF;PATH&#xFF0C;&#x65E0;&#x9700;&#x5199;&#x5168;&#x8DEF;&#x5F84;
 //execvp("ps", argv);

 // &#x5E26;e&#x7684;&#xFF0C;&#x9700;&#x8981;&#x81EA;&#x5DF1;&#x7EC4;&#x88C5;&#x73AF;&#x5883;&#x53D8;&#x91CF;
 execve("/bin/ps", argv, envp);
 exit(0);
}
</unistd.h>

事实上, 只有execve是真正的系统调用,其它五个函数最终都调用 execve。

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表,如果采用列表形式,const char *arg中的第一个参数必须是可执行程序本身,如上例中的 “ps”。
  • v(vector) : 参数用数组 ,v和l只能二选一
  • e(env) : 表示自己维护环境变量,有e参数中就需要有char *const envp[]
  • p(path) : 有p自动搜索环境变量PATH,第一个参数直接输入程序名即可,且有p一定没有e,因为有表示已经自动添加了环境变量,如果没有p则需要输入对应程序的路径

4.3.2 进程替换的应用

我们平时使用的shell读取命令和分析命令就是一个很典型的例子,如下图所示:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

我们平时输入的如 ls -a等命令实际上是一个个可执行程序。当shell读取一行命令时,shell会对命令进行解析,并且shell创建一个子进程,再通过调用execve,用可执行程序替换掉子进程,当程序执行完毕并且退出后,shell读取子进程的退出信息。这样,即便会出现程序崩溃的情况,也不会影响到shell本身。

以上就是关于进程控制的内容,主要分为四个方面——进程创建,进程终止,进程等待以及进程替换。有了以上的知识,我们已经可以实现一个很简易的shell,如何实现,请读者自行思考!

Original: https://www.cnblogs.com/Grong/p/15516023.html
Author: 乌有先生ii
Title: Linux系统编程之进程控制(进程创建、终止、等待及替换)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/585761/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 闪存和SSD存储之间有什么区别?

    PC硬件术语经常被互换使用,其缩写被削减,或者对同一组件有多个词。以DRAM/RAM/memory为例。这三个词都是指同一个PC组件,它被安装在CPU插座旁边的瘦小插槽中&#821…

    Linux 2023年6月7日
    0166
  • gnutls_handshake() failed

    原文链接:https://www.zhoubotong.site/post/75.html使用git clone https://github.com/xxx.git的时候,出现错…

    Linux 2023年6月6日
    085
  • 2. 文件与I/O

    文件与I/o open 系统调用 close 系统调用 creat 系统调用 read 系统调用 write 系统调用 open&#x7CFB;&#x7EDF;&a…

    Linux 2023年6月6日
    085
  • typesafe_cb

    callback 回调函数 什么是callback function 如图(来自维基百科),回调函数提供了一种服务,可以由用户决定使用怎么样的服务(登记回调函数)。回调函数机制,提…

    Linux 2023年6月8日
    095
  • SSM中的拦截器

    SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。 过滤器与拦截器的区别…

    Linux 2023年6月14日
    079
  • Rook搭建Ceph

    Rook搭建Ceph 一、软件版本及先决条件 软件版本: 软件版本名称 软件版本号 Pacific 16.2.7 Rook V1.9 先决条件: K8s集群,1.16版本+ K8s…

    Linux 2023年6月13日
    074
  • Linux之Keepalived高可用

    一、高可用介绍 一般是指2台机器启动着完全相同的业务系统,当有一台机器down机了,另外一台服务器就能快速的接管,对于访问的用户是无感知的。 硬件通常使用:F5 软件通常使用:Ke…

    Linux 2023年6月14日
    0131
  • 前端之jQuery快速入门

    一、jQuery 一款轻量级的JS框架。jQuery的核心JS文件才几十kb,不会影响页面加载速度。 丰富的DOM选择器,jQuery的选择器用起来很方便,比如要找到某个DOM对象…

    Linux 2023年6月14日
    084
  • SQL52 获取employees中的first_name

    本题链接表结构如下所示(内容不完整):额外的要求是按照first_name最后两个字母升序进行输出。这里需要用到MySQL的字符串处理函数RIGHT。RIGHT函数的语法如下所示:…

    Linux 2023年6月13日
    0104
  • Grafana+Prometheus 搭建 JuiceFS 可视化监控系统

    作为承载海量数据存储的分布式文件系统,用户通常需要直观地了解整个系统的容量、文件数量、CPU 负载、磁盘 IO、缓存等指标的变化。 JuiceFS 没有重复造轮子,而是通过 Pro…

    Linux 2023年6月14日
    080
  • Docker搭建Redis Cluster集群及扩容和收容

    上一篇文章讲解了Redis集群原理及搭建,由于工作中使用docker较多,本文主要讲解使用docker搭建集群及对集群的扩展收容。环境:Centos7.6Docker:20.10….

    Linux 2023年6月13日
    069
  • FastDFS安装和简介详细总结

    1、fastDFS简介 1 FastDFS是用c语言编写的一款开源的分布式文件系统。 2 FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用…

    Linux 2023年6月7日
    0114
  • 回溯法:算法思路以及相关流程图的绘制

    参考建模原文2020国赛B题参考文章1 回溯法介绍 深度优先搜索(缩写DFS):对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标…

    Linux 2023年6月14日
    081
  • zookeeper 配置文件详情

    目录结构 目录名作用 bin 存放系统脚本 conf 存放配置文件 contrib zk附加功能支持 dist-maven maven仓库文件 docs zk文档 lib 依赖的第…

    Linux 2023年6月8日
    061
  • 爱前端公开课学习笔记——JS02 字符串类型,布尔类型

    字符串是用引号包裹的,表示语言文字。 用双引号包裹的都是字符串 console.log(typeof 5); // number console.log(typeof "…

    Linux 2023年6月14日
    061
  • 灵敏度分析简介

    参考文章1 😄参考文章2 😸参考文章3 😃 1. 灵敏度分析: 某一个假定的常量,在现实中不可能完全保持不变,可能发生一定范围的波动。灵敏度分析就是检验这部分波动对结果的影响。 灵…

    Linux 2023年6月14日
    088
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球