Linux操作系统 --进程控制

进程创建

fork函数

在这里插入图片描述

在这里插入图片描述

有关fork的基础细节,可移步至https://blog.youkuaiyun.com/Iiverson2000/article/details/119394135进行学习


我们在此问一个问题,为什么要给子进程返回0,给父进程返回子进程的PID呢?它是如何做到同时返回两个返回值呢?

我们首先需要了解:父子关系永远是 1:n
也就是说 在父子进程立场,父进程只有一个,不需要进行标识,所以返回0. 而 子进程很多,需要进行标识,所以给父进程返回子进程PID。可以理解为:fork 给父亲返回子的标识 ,给子返回父的标识,同时子进程需要被父进程控制进行任务的执行,所以一定也需要其PID进行标识。

两个返回值的问题:
在这里插入图片描述
在这里插入图片描述
理解:fork为函数,其内部执行的过程大致为,拷贝父进程,形成子进程,添加到runqueue中。重点来了:此时fork函数并没有结束,但是此时进程已经创建完成。也就是说当前有两个进程,分别执行拷贝下来的剩余的代码,也就是return pid 。也就意味着,此时两个return在虚拟地址层面并不是同一个return ,他们有各自的虚拟地址。显式的表现为同一个return执行了两次。同时:我们在外界用变量接收返回值,发生了对变量的写入,此时会发生写时拷贝

写时拷贝

通常,父子的代码和数据都是共享的,也就是同一份。但在写入时,所写空间便会拷贝原来的数据到新的空间,之后再在新的空间进行写入。这个任务是OS做的,就叫做写时拷贝。

**共享:**页表所指向的物理空间地址时相同的

在这里插入图片描述
那么为何要有写时拷贝呢?

  • 确保进程的独立性,相互不要干扰
  • 为何不在创建进程时就分开呢?很明显是因为大部分情况都是代码数据共享,不需要进行写入的。同时,新进程创建后不会立刻被调度,那么就暂时不需要对其分配空间 。所以这样做最大程度 高效利用了内存,简单来说就是 a.按需分配 b.延时分配

90%情况下,代码区不会发生写时拷贝,因为我们不会在程序运行时去改写代码逻辑,代码的属性是只读的。
10%的情况是指:进程替换


进程终止

进程退出的场景:
在这里插入图片描述
进程退出的方法:
在这里插入图片描述
我们在了解进程退出之前,先掌握一个概念:进程退出码

进程退出码

为什么main 函数 每次都要return 0 呢?

main函数 是一个函数 ,并被其他函数调用,这里的其他函数是指系统级别的函数,所以会有返回值

main函数的返回值的意义:
运行程序的本质,就是加载形成进程,其目的是为了完成某项工作。工作一定是由人发出的,所以我们需要关心工作完成的情况,成功了?失败了? 这里的return 就是 我们所讲的退出码


此时我们再来谈谈上文说到的进程退出的三种场景:
我们经常通过返回值 来判断此时进程的状态,也就是进程退出码
代码跑完了 且 结果正确 ,我们返回 0 ,否则就返回非 0 .
此时就会有一个现象,0只有一个 ,非零有很多很多。同样的意思就是,正确情况只有一种,错误情况有很多。
我们通常使用非0值来表示各种各样不同的错误。

每种退出码都有不同含义,我们可以简单参考strerror等函数来查看具体的错误信息,帮助程序员来确定错误原因.
同时也可以通过 echo $? 来显式最近一次进程退出的退出码


再来看看常见的进程退出方法
正常终止也就是main返回和 exit ,他们两个只有唯一的区别 就是:main 函数return 只能在main函数中 终止进程,而exit 可以在任意函数中终止进程。一般我们的子进程的终止都是通过这样的方式完成的。

exit 与 _exit 的区别:
我们可以通过一个简单的小程序来看看他们的区别:
在这里插入图片描述
现象:此时进程运行起来,3s内没有任何反应,不打印任何东西。3s后执行到exit后,helloworld字符串被打印出来,之后的内容不会被打印。

解释:我们printf打印的字符串 由于没有 加 \n 等刷新符,导致其内容全部在输出缓冲区中,所以不会打印,直到缓冲区满或是遇到刷新符才会刷新显式出来。

结论:exit 在终止进程时,会将缓冲区的内容同时刷新出来 –释放该进程曾经占用的资源。

拓展:_exit: 直接结束进程,不会进行刷新缓冲区的工作。缓冲区的内容直接被可覆盖。

在这里插入图片描述

在这里插入图片描述

除了上述的正常退出,还有进程异常退出的情况

  • ctrl + c
  • 野指针导致的退出
    等等,这些都属于异常退出。
    既然是属于异常退出了,那其进程退出码就没有意义了,因为程序根本还没有执行到返回退出码的地方就被迫终止了。

此时我们可以如此理解:进程终止后,OS释放曾经申请的各种数据结构 、内存等资源,并且将其从各个队列数据结构中移除。

进程等待

进程等待通常是由父进程完成,我们为什么要有进程等待呢?

  • 子进程退出,父进程如果不管不顾,就会导致僵尸进程的问题,造成资源浪费、内存泄漏等问题。
  • 僵尸进程 只有父进程可以处理 ,kill -9 这样的命令 对其无效。
  • 父进程派遣子进程完成任务,父进程需要接收子进程的处理结果,也就是上文提到的退出码的概念。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息。

两种进程无法被kill 掉 1、 僵尸进程 2、 D进程

进程等待的方法

wait 和 waitpid

在这里插入图片描述

在这里插入图片描述
我们对以上概念进行解释:
子进程运行期间,父进程进行wait等待,此时的父进程什么都没有做,就是单纯的等待 ----阻塞等待
有了wait ,我们就可以保证子进程先于父进程退出,确保父进程回收子进程后,副进程再退出。
等待成功并不意味着子进程运行结果成功,子进程运行的结果如何,我们需要通过输出型参数 status 来进行提取其退出码

其中waitpid 和wait 都只能一次等待一个进程,而 waitpid可以指定所等待的进程,只需要传入进程id就可以了。wait 等待最先结束的进程。

简单看看效果吧!
在这里插入图片描述
在这里插入图片描述
我们通过结果看到不错的效果 ,父进程等待子进程exit后在进行输出ret以及返回码的返回。但我们子进程的返回码并不是我们能所设定的exit(0)这里的码。此处就涉及到了status这个输出型参数。


status详解

在这里插入图片描述

首先我们先要了解 status 的数据类型,我们传入一个int * 类型的指针。也就是说其在32位下有四个字节,我们传入一个空status,进程运行后,操作系统帮助我们对内填充信息。
我们不需要关注整个4字节,我们只需要关注最低字节和次低字节就可以了。
正常退出的情况:
我们只需要关注次8位,其内存储的为退出的状态。

而在异常退出情况:
低8位 中 最高位存储的是 core dump 标志 (日后再谈)
剩下的低7位就是我们的终止信号

所以我们就可以很好的理解为什么上述代码中的status值和我们的退出码不同了,我们的退出码只是次低八位的值,所以我们可以通过位运算对其进行解析:
(status >> 8) & 0x FF
这样我们就可以得到正常退出下的正确的返回码
得到异常终止信号也是同理,对低7位进行解析:
(status >> 7) & 0x 7F

异常进程的本质就是进程由于某种错误导致其收到某种异常信号,也就是低7位被写入数据 — 终止信号!

我们可以通过 kill -l 查看信号操作
其中 1- 31 为普通信号 ,32-64 为实时信号

当然在具体的应用中,我们并不会使用位操作这样的操作进行判断退出码和终止信号。而是通过系统定义的宏来完成。
也就是WIFEXITED(status) 和 WEXITSTATUS (status);
其中:含有if的这个宏用来判断 该 进程 是否为正常结束,若为正常结束 则返回值为真。之后我们可以用另一个status 宏来提取其退出码。如果进程异常终止,则不会对其进行退出码提取,而给一个错误信息。

我们给一个正确的模板看看:
在这里插入图片描述

至此,我们只剩waitpid函数最后一个参数没有讲到。
options这个参数,我们有两种选择,一种为默认的 0 ,意味着我们和wait一样进行阻塞等待,也就是等待的时候不进行任何操作。
还可以将其改为 WNOHANG --wait no hang
若pid指定的子进程没有结束,则waitpid 对其不停的检测,并返回0,并不会干等,过一段时间检测一下,检测完若依旧没有完成则返回0,并进行其他任务。直到检测到子进程正常结束,返回子进程pid。

我们暂时先简单了解一下

基于非阻塞接口的轮巡检查方案
在这里插入图片描述


进程程序替换

替换原理

在这里插入图片描述

总的来说,替换函数的本质,就是进程 把一个磁盘 中的程序加载起来。
进程程序替换 并没有生成新的进程 ,只是将新的程序的代码和数据 加载 到 调用该函数的进程 的 物理内存中,实现代码和数据的替换。之所以说没有任何新进程的创建 是意味着:我们没有新的pcb 页表 虚拟地址等 结构 ,被链接到进程队列中。是在原进程的最底层进行的替换。

我们可以简单的看一下execl这个函数:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们可以看到这么几个现象:
该进程在执行替换函数后,该程序后续的代码是不会被执行的,因为它已经被覆盖掉了,不可返回的过程
替换函数如果替换失败,则会运行原代码。
这里我们可以总结出一个结论:
当exec系列函数不需要返回值,执行成功则不返回,执行失败才会返回。

在这里插入图片描述
在这里插入图片描述

我们可以通过上述代码再去理解我们的替换函数返回值的问题,
当我们的子进程成功的替换,则不会返回1,而是返回函数的返回值 0.
只有当替换失败后,才会返回我们设置的1.

替换函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
仔细看看上述例子,聪明的你一下就能明白这几个函数的内在联系。
事实上,我们为什么要将execve单独拿出来呢,其实其它五个函数都是由这个execve这个系统调用接口改造而来的。所以最底层系统给我我们的开的接口就是这个execve。
此处我们还需要注意一个细节,当我们使用自己的环境变量,此时就无法在使用系统的环境变量。如果需要,我们要把系统的环境变量和自己的环境变量组合在一起。粘合怪。
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值