【Linux修炼】11.进程的创建、终止、等待、程序替换(二)

简介: 【Linux修炼】11.进程的创建、终止、等待、程序替换(二)

3.进程等待



3.1进程等待的原因


之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。


另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。


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


父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息


总结:进程为什么要等待?回收子进程资源,获取子进程退出信息,即通过进程等待的方式解决僵尸进程的问题。


3.2进程等待的方法


1. 回收子进程资源wait


我们需要了解wait这个函数,通过man 2 wait打开手册:

微信图片_20230224214231.png

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
    成功返回被等待进程pid,失败返回-1。
参数:
    输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

了解了关于wait的信息之后,就试着使用一下wait()


微信图片_20230224214327.png

这段代码的目的是想演示僵尸状态下的子进程被回收的结果:


即子进程先在循环中sleep10秒,而父进程sleep15秒,这样当子进程运行完毕exit时,父进程在子进程结束的5s内不会回收子进程,这就造成子进程变成Z(僵尸)状态,当5s之后,父进程就会通过wait回收子进程,ret的接收的值就是子进程的进程退出码。最后得sleep(5)是为了让父进程再破案一段时间从而更好的观察状态。


那么这段代码我们编辑完成之后赋值ssh渠道进行观察进程的状态:


image.gif


一开始右侧执行脚本,观察状态,同时左侧运行mytest,我们发现当子进程正在执行时,子进程和父进程都处于S+状态,当子进程执行完毕,没有被父进程回收时的那5秒,子进程就变成了Z+状态,当父进程执行时,通过调用wait将子进程回收,子进程就结束了,最后的5秒只剩下父进程处于S+状态。这就是父进程通过进程等待回收了僵尸进程(子进程)。


2. 获取子进程的退出信息waitpid


通过man 2 waitpid查询waitpid的信息


微信图片_20230222020230.png


pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
    pid:
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0.等待其进程ID与pid相等的子进程。
    status:
        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。


1. 获取子进程status


wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

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


而上面所说的实际上就是:对于这个拿到子进程的退出结果,实际上并不能直接反应出我们想要的结果,其结果是一个复合类型,我们需要将其进行拆分:


对于32个bit位在这里只有尾部16个bit位是有意义的,因此我们将这些拿出来,即0~7位返回0代表正常的终止信号(返回0证明没有出问题),8~15次低8位代表子进程对应的退出码。


3.若代码没跑完结果异常了:(在子进程中添加一个错误)

微信图片_20230222020410.png

微信图片_20230222020413.png


不同的错误通过信号的值可找到对应的错误。下面是struct task_struct的源码,我们发现对于进程退出码和终止信号都在这个PCB中。


上述的过程我们也再总结一下:


  1. 让OS释放子进程的僵尸状态

  2. 获取子进程的退出结果(如果子进程不结束,父进程就会一直处于阻塞等待,等待子进程退出)


2. WIFEXITED(status)和WEXITSTATUS(status):


微信图片_20230222020533.png

运行一下结果:

微信图片_20230222020538.png

成功接收到了子进程正常退出的退出码。那如果子进程不是正常退出呢?我们将cnt改成50,这样会有充足的时间杀掉子进程让其异常:

微信图片_20230222020635.png


3.3再谈进程退出


  1. 进程退出会变成僵尸,会把自己的退出结果写入到自己的task_struct中
  2. wait/waitpid 是一个系统调用,即以OS的身份进行,因此OS也有能力去读取子进程的status。

即前两条都意味着子进程的退出信号和退出结果都保留在子进程的PCB中。


3.4进程的阻塞和非阻塞等待


在此之前,我们先以一个例子解释阻塞和非阻塞:


在一所学校中有张三和李四这么两个人,张三经常逃课,因此什么也不会,李四认真听讲,学的非常好。考试周到了,张三约好李四让其辅导张三,并想着帮了这么大的忙,得请李四吃顿饭。于是张三给李四打电话:“李四,现在有时间吗?下楼请你吃个饭。”李四说:“等我20多分钟,我看完这本书就下去。”于是张三答应了下来,但这期间张三并没有挂电话,想着能够等待他看完的消息。(现实中并不会出现这样的情况,即便是舔狗也不会)就这样两头电话打着,双方却都很安静,过了20多分钟,李四看完了,就这样二人通过电话彼此收到了消息。


过了几天之后,张三考的还不错,为了感谢李四的帮助想再请李四吃个饭,这次李四仍然说:请等我一会,我处理完事情就下楼。而张三对与上次一直打电话但两头都沉默这种情况感觉很是尴尬,于是这次就先挂了电话。张三一会看看书,一会打打游戏,又时不时的给李四打电话了解处理事情的进度,就这样打了10几次电话后,李四说,我下楼了并且已经看到你了,张三很是高兴,便和李四出去吃饭了。


对于上面的这个例子,张三第一次打电话并没有挂断电话,就这样一直检测李四的状态,这种状态实际上就是阻塞状态。


而对于第二次打电话,并没有一直接通,打的每一次电话都是一种状态检测,如果李四没有就绪,那么就挂断,过一段时间再次检测,而这种每一次打电话实际上都是一个非阻塞状态——而这多次非阻塞就是一个轮询的过程。因此打电话就相当于系统调用wait/waitpid,张三就相当于父进程,李四就相当于子进程。


对于阻塞等待,我们上面已经演示过,那么下面就直接上非阻塞状态的过程:


微信图片_20230224214545.png

对于这段代码,设计理念是这样的:子进程在执行期间,父进程则会一直等待并通过while的方式去轮询非阻塞状态,直到子进程退出。


微信图片_20230224214619.png

如果子进程出异常了,那么父进程也能够抓到,为了演示这种情况我们在子进程中增加一个野指针的错误:


微信图片_20230224214638.png


微信图片_20230224214641.png


此时的退出码为0,代表的是子进程的退出码,而终止信号是11号错误,对于异常的进程退出,他的退出码是没有意义的,所以我们返回为0的退出码也不看。

那什么时候会等待失败呢?id错误的时候会等待失败。


微信图片_20230224214717.png


微信图片_20230224214720.png

 

4. 进程的程序替换


创建子进程的目的:


  1. 想让子进程执行父进程代码的一部分(执行父进程对应磁盘代码中的一部分)

  2. 想让子进程执行一个全新的程序(让子进程想办法加载磁盘是指定的程序,执行新程序的代码和数据,这就是进程的程序替换)


4.1见见猪跑


在这一小节中,包含6种函数,为了提前演示,就在这里拿出一个函数看看进程程序替换究竟是什么样子。


对于一个程序加载到内存去执行,首先是找到这个程序,然后通过不同的选项去以不同的方式去执行,这与环境变量是一样的。因此对于此execl函数来讲,第一个参数path就代表找到程序对应的路径,第二个就代表选项,选哪种方式运行程序的选项;而后面的...


我们为他引入一个新的名词:可变参数列表。顾名思义我们在C语言中的scanf以及printf类的函数,无论传入多少个参数都没有限制,实际上就是可变参数列表的作用,因此,excel里的可变参数列表的作用就是让我们能在传入选项参数时能够传入任意数量的选项。(如 cmd 选项1,选项2……)


知道了这个函数功能之后,开始操作:


一、构建环境


  • 首先新建一个目录exec,并将上一级的Makefile拷贝到当前目录下:cp ../Makefile .
  • 然后打开Makefile,将里面的文件名替换成我们想要创建的文件名: %s/mychild/myexec/g


微信图片_20230224214856.png

编写代码,函数execl的头文件是unistd.h

微信图片_20230224214924.png


二、编译执行

微信图片_20230224214951.png

我们发现其就有了ls指令的功能(ls也是一个程序)。


三、修改完善


当然,我们也可以将其增加选项命令执行对应的功能:


微信图片_20230224215026.png


执行之后对比正常的ls -a -l命令:


微信图片_20230224215054.png


发现二者无异。那么这就叫做进程的程序替换。

但是我们发现第一个printf打印出来了,但是execl后面的printf却没有打印出来,这是为什么呢?通过下面理解:


4.2 理解原理(是什么、为什么、怎么办)


微信图片_20230224215146.png

当我们执行代码时,就会创建进程地址空间与物理内存磁盘之间形成映射关系,当执行上面的代码时就是这样,执行第一个printf会照常打印,到了execl函数时,就会发生进程的程序替换,也就是说,我们所编写的代码会被我们调用的execl对应磁盘内部的代码覆盖,即将指定程序的代码和数据覆盖自己的代码和数据,执行这个新的代码和数据,所以我们明白了为什么execl后面的printf没有执行。


那在进程程序替换的时候,有没有创建新的进程呢?实际上是没有,我们一开始所创建的虚拟空间并不会变化。


execl函数的返回值问题

我们知道,只要是一个函数调用就有可能失败,就是没有替换成功,就是没有替换,而对于这exec系列的函数,失败了返回-1,程序不被替换,因此execl下面的代码也会继续执行。下面就演示一下:(随便打一个不存在的路径或者程序)


//

微信图片_20230224215209.png


微信图片_20230224215213.png

execl下面的代码也就正常执行了。而exec系列的函数调用成功是没有返回值的,也不需要返回值,因为进程被替换之候原本的代码就没有意义了,即便返回了一个值,也不会有什么作用,还会有额外的开销。

  • 多进程的问题

这次我们通过fork创建子进程,并在子进程执行对应的execl函数:


微信图片_20230224215256.png

微信图片_20230224215259.png


如果我们仍随便打一个不存在的位置或者程序,那么code的值就会变成-1。那这个时候,子进程调用的execl会影响父进程吗?答案当然是否定的,进位进程具有独立性,下面就来理解一下具体是什么原因:


微信图片_20230224215337.png

当只存在一个父进程时,就会创建出上面这样的映射关系,当fork函数开始执行,子进程生成,就会创建出子进程的PCB,以及对应的虚拟内存、页表,与父进程共享对应的物理内存:

微信图片_20230224215401.png而当子进程调用execl时,由于子进程发生改变,本着进程直之间具有独立性的原则,子进程就会发生写时拷贝,将共享的数据段和代码段在物理内存的另一个位置进行写时拷贝,并与新的位置形成映射,这样便不会影响到父进程。此外我们也可以看出,数据和代码都可以发生写时拷贝。

微信图片_20230224215425.png


总结:虚拟地址空间+页表保证进程独立性,一旦有执行流想替换代码或者数据,就会发生写时拷贝


4.3 一个一个调用对应的方式


除了execl,还有其他类似的接口,六种以exec开头的函数,统称exec函数,我们通过man execl查看:


微信图片_20230224215510.png

主要:

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);//l(list) : 表示参数采用列表
int execlp(const char *file, const char *arg, ...);//p(path) : 有p自动搜索环境变量PATH
int execle(const char *path, const char *arg, ...,char *const envp[]);//e(env) : 表示自己维护环境变量
int execv(const char *path, char *const argv[]);//v(vector) : 参数用数组
int execvp(const char *file, char *const argv[]);//vp就是v和p的结合

一、函数解释


这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值。

二、函数的具体原理及演示


下面就来演示其他几个例子:


execlp(const char *file, const char *arg, …)

p:path,不用告诉我程序的路径,只有告诉这个函数传入的名字,就会自动的在环境变量PATH中进行可执行程序的查找。


4.4 应用场景:模拟shell命令行解释器


我们将子进程的代码中的替换注释掉,在添加成这样:


微信图片_20230224215653.png

不传入argv[0]的原因是argv[0]代表我的程序:myexec,这样的话就会出现死循环的情况,因为会一直调用,所以为了跳过,我们从第二个元素argv[1]的地址开始。

微信图片_20230224215658.png

那如果我们将第一个./myexec去掉,发现不就是相当于自己写了一个shell吗?因此下面我们来编写shell命令行解释器

新建目录myshell,touch一个myshell.c ,并编辑Makefile


微信图片_20230224215749.png

下面就来编写myshell.c:

微信图片_20230224215752.png

编译运行

微信图片_20230224215831.png

这样就可以很好的模拟出shell命令行解释器了,但还有一个问题:就是返回上一级路径时,对于我们这个代码是这样的情况:


微信图片_20230224215901.png

但是按照正常的命令行来说应该是变化的,因此下面就来尝试解决这个问题:

  • 首先我们要知道什么是当前路径

因此在这里touch一个新的myproc.c来解释:


微信图片_20230224220001.png

复制ssh渠道并观察执行:

微信图片_20230224220007.png


当前进程的工作目录,就是当前路径。 因此,若是想实现路径的改变,就需要实现进程工作目录的改变,说到这里,大家也应该明白,这个当前进程的工作目录也是可以修改的。

  • 改变当前路径:chdir函数


微信图片_20230224220114.png

下面不废话,直接演示其是如何改变当前路径的:


微信图片_20230224220140.png

编译运行:

微信图片_20230224220210.png


我们发现,这样就将这个进程的路径改变了,也就是说如果我们再通过这个进程创建文件,就会创建到此时这个/home/cfy的这个路径中。


那回到一开始,为什么我们自己写的shell,cd 的时候路径没有变化呢?


在上面实现的shell模拟代码中,我们fork出了子进程,子进程有自己的工作目录,因此cd更改的是子进程的工作目录,子进程执行完毕,继续用的是父进程,就是我们的shell,因此在这个过程中父进程也就是shell的工作目录并没有发生变化。


  • 将编写的模拟shell进行修改——修改当前路径


微信图片_20230224220301.png


微信图片_20230224220305.png

这样就补充了之前的不足。像cd这种不需要让我们的子进程来执行,而是让shell自己执行的命令,被称为内建/内置命令。 接下来还没完,实现最后一个问题:echo内建命令。对于echo我们知道,通过echo $? 能够活获得最近一次进程的退出码和终止信号。最终代码:


微信图片_20230224220339.png微信图片_20230224220343.png


完结!

相关文章
|
16天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
48 4
linux进程管理万字详解!!!
|
7天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
43 8
|
15天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
49 4
|
16天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
18天前
|
消息中间件 存储 Linux
|
5月前
|
监控 Linux 应用服务中间件
探索Linux中的`ps`命令:进程监控与分析的利器
探索Linux中的`ps`命令:进程监控与分析的利器
126 13
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
167 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
3月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。
|
4月前
|
存储 缓存 安全
【Linux】冯诺依曼体系结构与操作系统及其进程
【Linux】冯诺依曼体系结构与操作系统及其进程
171 1