1.进程创建
进程创建部分由于就是fork函数,还有写时拷贝,在上一篇已经讲述过了,这里不在进行赘述,有疑问的读者可以前往上一篇博文《Linux系统--程序地址空间》中阅读!
这里在多说一嘴写时拷贝吧
我们可以对比一下写时拷贝修改内容前后内存的变化:
可以先看一下老师的图,并尝试分析一下问题1和2:
好,这里我来说明一下,
修改内容之前的内存状态
虚拟内存与物理内存页对应关系 :父进程和子进程各自的虚拟内存中的数据段和代码段,通过各自的页表项映射到相同的物理内存页上。例如,父进程页表项 50 和 100,以及子进程页表项 50 和 100,都指向同一块物理内存页,且都标记为 “只读”。
共享内存的优势 :这样做的好处是节省了内存资源。因为在子进程刚创建时,可能并不会对共享的内存页进行修改,如果一开始就复制一份副本,会浪费内存空间。
修改内容之后的内存状态
子进程尝试写入 :当子进程试图对共享的物理内存页进行写操作时,操作系统会检测到对该 “只读” 内存页的写入请求,从而触发一个异常或中断。
操作系统的处理 :操作系统会对此情况进行判断分类。如果是野指针等真正的错误,会终止进程;如果是合法的写操作,就会执行写时拷贝操作,即为子进程分配一块新的物理内存页,并将原来共享页面的内容复制到新的页面中,然后将子进程页表项对应页面的权限改为 “读写”,之后子进程就可以在这块新的物理内存页上进行写操作,而父进程仍然指向原来的物理内存页,保持其 “只读” 状态。
问题解答
问题 1 :如果在创建子进程之后直接将数据分开,即直接拷贝,会增加内存的占用。因为在很多情况下,子进程可能并不会对父进程的内存数据进行修改。而写时拷贝技术可以延迟拷贝操作,只有在子进程需要写入数据时才进行拷贝,这样可以有效节省内存资源,提高系统的性能和效率。
问题 2 :直接开辟对应的空间而不采用写时拷贝的话,会导致内存的浪费。因为每个子进程都需要为父进程的内存数据开辟一份完整的副本,即使这些数据在子进程中可能从未被修改过。而写时拷贝技术可以按需获取内存资源,只有在需要写入时才分配和拷贝必要的内存页面,实现了 “惰性” 申请内存,使内存的使用更加高效合理。如下述用例,当我们想对a++时,我们才会给他开辟新的内存空间,没有这条命令时,并不会给他开辟!
2.进程终止
2.1 进程终止的本质
2.2 进程退出场景
进程退出,无非就是三种情况:
1.代码跑完,结果对
2.代码跑完,结果不对
3.代码没跑完,进程异常了
以上3中情况中,情况12是由我们的退出码(稍后会做讲解)来决定的,即exit(code)
而情况三,我们要思考一个问题,那就是代码异常了,退出码本身,还有意义吗?
为了解决这个问题我们可以打个比方:
我们可以将上述过程类比为考试,代码跑没跑完,就好比你的考试过程顺不顺利,有没有中途产生意外,而结果就好比我们的考试分数,情况12就可以类比为我去考试了,也认真的考完了,只不过情况一拿了100分,而情况二我可能不及格,只拿了20分,好,相比大家应该都知道吧?我们考了一百分的时候很少有家长问原因,问为什么考这么高,但是,你要是考了20分,包先问原因的,而且搞不好还要请你吃一顿炒肉哈。那么,这就是我们退出码存在的意义了,倘若有一天,你和父母约法三章,说以后考试,我回来之后说0,就代表我考的不错,满分,但我要是考砸了,如果说1,代表我笔没油了导致考砸了,2代表考试时饿了导致考砸了,3代表考试时溜号想自己对象了导致考砸了等等等,这样一来,只要你考砸了,父母问你,你就回一个数字是不是就可以了?换句话说,这便是一种反馈,要让你父母知道你考砸的原因!(现实中不建议这样做哈,容易再吃一份炒肉)
这便是退出码存在的意义!!!
而我们再看情况三,就好比我在考试的时候作弊被抓了,取消考试资格了,那么,此时我考了多少分还重要吗?不重要,重要的是我是怎么被发现的,问题出现在哪里,以及回去怎么交代!同理我们回到计算机上,代码都错了都跑不了了,那我还回去关心你的退出码是几吗?我肯定当前最关注的是代码为什么会错!怎么去修改代码!所以上述问题也迎刃而解,即
代码异常了,退出码本身没有意义!
好,我们接下来再详细解释一下退出码相关的内容
2.3 退出码
大家还记得之前的C语言我们每次又要int main() return 0吗?那这里的0是什么?为啥不是return 1,return 2,return3呢?这里就要涉及退出码的概念了。
退出码的定义
退出码是一个整数值,通常用于指示程序的执行结果。一般情况下,退出码0表示程序成功执行,非零值则表示程序执行过程中遇到了某种问题或错误。
常见退出码例举
在这里给大家罗列了一些常用的退出码,如果有需要可以自己去百度上查一查
不愿意查的看这里,可以这样自己找
只要写一个这个文件:
将其编译运行就可以得到如下结果:
查看退出码的方法
在Linux中,可以通过特殊变量$?
查看上一个命令的退出状态码。
echo $?
例如,我们可以写如下文件,来查看退出码!
文件内容如下所示:
我们来编译运行,查一下退出码:
欸?我又敲了一遍echo $?,这回退出码咋变成0了?难道是我的文件改邪归正了吗?
实际上,并不是,我们的这个0是上一个进程echo $?的退出码,这个进程成功执行了,那当然退出码就是0了!
2.4 exit函数
exit函数 是C语言中的一个标准库函数,用于正常终止一个程序。它定义在stdlib.h
头文件中
void exit(int status);
status
:退出状态码,是一个整数值,用于向操作系统报告程序的执行状态。通常,0表示程序成功执行,非零值表示程序执行失败,不同的非零值可以表示不同的错误类型。
功能
执行清理操作:当调用exit函数时,它会先执行所有已注册的清理函数(通过atexit
函数注册的函数)。这些清理函数通常用于释放资源、关闭文件等操作。
终止程序:在完成所有清理操作后,exit函数会终止程序的执行,并将status
值作为退出状态码返回给操作系统。
代码测试:
我们写入如下内容:
编译运行:
证明了返回值确实是exit里面的状态值。
2.5 strerror
我们输入man 3 strerror查询一下向相关信息:
说明:
参数
errnum
:错误码,是一个整数值,通常由系统调用或库函数返回。
返回值
返回一个指向错误信息字符串的指针。该字符串描述了由errnum
指定的错误。
3.前景回顾
3.1 进程退出的方式
前文我们提及了几点进程的退出方式,还有三种退出方法,分别为
方法1:在main
函数中使用return n;
,其中n
是返回的退出码。
方法2:直接调用exit(n);
,其中n
是退出码。
方法3:直接调用_exit(n);
,其中n
是退出码。
那么,exit和_exit有什么区别呢?为什么要搞两个呢?
3.2 exit和_exit的区别
exit
和 _exit
都是用来终止进程的函数,但它们在行为上有所不同。exit
函数会调用所有注册的清理函数(通过 atexit
注册),刷新所有标准I/O缓冲区,然后终止进程。这意味着它会确保所有的输出都被写入到文件或终端,并且执行任何必要的清理工作。相反,_exit
函数直接终止进程,不刷新缓冲区,也不调用任何清理函数,因此它通常用于需要立即终止进程且不需要进行任何清理的情况。简而言之,exit
提供了一个更优雅的退出方式,而 _exit
提供了一个快速且简单的退出方式。
换句话说,exit是终止进程,会主动刷新缓冲区,而_exit会直接终止进程,不会刷新缓冲区,那么话说回来,什么是进程终止?进程终止就是绝对要调用系统调用,必须让操作系统完整真正的的进程删除退出!
3.3 return 和 exit的区别
return
和 exit
在C语言中都用于结束程序,但return
用于从函数返回一个值,可以是从main
函数返回一个整数值给操作系统表示程序退出状态,而exit
是一个库函数,用于立即终止整个程序,它接受一个整型参数作为退出状态码,并在终止前执行清理操作如刷新标准I/O缓冲区和调用已注册的atexit
函数。简而言之,return
通常用于函数内部,而exit
用于程序的全局退出。
4.进程等待
4.1 什么是进程等待?
进程等待是指在多进程环境中,一个进程(通常是父进程)暂停其执行,直到另一个进程(子进程)完成其任务并终止的行为,这个过程允许父进程同步子进程的结束,回收子进程占用的资源,并获取子进程的退出状态,以便于进行后续处理。
4.2 为什么要有进程等待?
防止僵尸进程:
当子进程结束时,如果父进程没有及时回收子进程的资源,子进程可能会变成僵尸进程。僵尸进程会占用系统资源,导致内存泄漏。
获取子进程退出信息:
父进程需要知道子进程是否成功完成任务,以及子进程的退出状态(退出码)。这有助于父进程根据子进程的执行结果进行相应的处理。
资源回收:
父进程通过等待子进程,可以回收子进程占用的资源,如内存和文件描述符等。
4.3 如何实现进程等待?
4.3.1 使用 wait
函数
wait
函数用于父进程等待任意一个子进程结束。它返回结束的子进程的进程ID(PID)。如果子进程没有结束,wait
函数会阻塞父进程,直到一个子进程结束。如果子进程已经结束,wait
函数会立即返回,父进程可以获取子进程的退出信息。
4.3.2 wait函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
说明:
其中pid_t wait(int *status);
:该函数使父进程等待其某个子进程结束,并返回子进程的进程ID。参数status
是一个指向整数的指针,用于存储子进程的终止状态;接收子进程的退出状态。成功时,wait
返回子进程的PID;出错时,返回-1。输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL。
在多进程编程中,父进程通常会fork多个子进程来执行并行任务。通过调用wait
函数,父进程可以确保所有子进程都已完成,然后再进行进一步的处理。wait
函数可以用于实现进程间的同步,确保父进程在子进程完成特定任务后再继续执行。而在多进程中,往往父进程最先创建,最后退出!
具体来说,父进程调用wait,表示父进程等待任意一个子进程:
1.如果子进程没有退出,父进程wait的时候,就会阻塞
2.如果子进程退出,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题,等待成功的时候,返回子进程的pid
4.3.3 waitpid函数
waitpid
函数是wait
函数的扩展,提供了更多的控制选项。它的原型如下:
pid_t waitpid(pid_t pid, int *status, int options);
说明:
pid
:指定要等待的子进程的PID。可以是特定的PID、0(等待任意子进程)、负值(等待属于特定进程组的子进程)。
status
:指向一个整数的指针,用于存储子进程的退出状态。如果不关心状态,可以设置为NULL。不仅仅是退出码,它包含了子进程的详细信息,包括退出码、终止信号、核心转储标志等。
options
:控制等待行为的选项。常用的选项有WNOHANG
(非阻塞等待)和WUNTRACED
(也获取停止状态的子进程)。
4.3.3.1 高16位和低16位
高16位用于存储核心转储标志和终止信号,低16位用于存储退出码。
核心转储标志和终止信号
核心转储标志:如果子进程因为信号而终止并且产生了核心转储,status
的第7位(从0开始计数)会被设置为1。
终止信号:子进程因为信号而终止时,信号编号会被存储在status
的高8位中。
退出码:如果子进程正常结束,退出码会被存储在status
的低8位中。
常见的信号编号及其含义:
SIGKILL
(9):立即终止进程,不能被捕获或忽略。
SIGTERM
(15):请求终止进程,可以被捕获或忽略。
SIGINT
(2):通常由Ctrl+C产生,请求终止进程。
SIGQUIT
(3):请求终止并产生核心转储。
SIGSTOP
(19):立即停止进程,不能被捕获或忽略。SIGCONT
(18):继续执行被停止的进程。
进程退出场景
图中解释了如何判断进程是正常结束还是异常结束:
正常结束:status
的低8位不为0,表示进程的退出码。
异常结束:status
的高16位不为0,表示进程因为信号而终止。
附上老师画的图:
一般而言,进程的退出码范围是【0,255】
4.3.4 阻塞等待
阻塞等待(Blocking Wait)是指在多进程或多线程编程中,一个进程或线程在等待某个条件或事件(如子进程结束、资源可用等)时,会暂停其执行,直到该条件或事件发生。这种行为称为阻塞等待,因为它会阻塞进程或线程的进一步执行,直到等待的条件得到满足。
本质就是检测子进程状态、退出,回收,没有退出立即返回
4.3.5 非阻塞等待
与阻塞等待相对的是非阻塞等待(Non-blocking Wait),在非阻塞等待中,进程或线程在等待条件满足时不会暂停执行,而是继续执行其他任务或定期检查等待条件是否满足。非阻塞等待通常需要更复杂的编程模型,如使用轮询、事件驱动或异步I/O等技术。
4.3.6 阻塞/非阻塞对比
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
这里我们说一下options选项:
这个选项是控制等待行为的选项,可以是0或以下选项的组合:
WNOHANG
:非阻塞等待。
WUNTRACED
:也获取停止状态的子进程。
WNOHANG
选项的作用
非阻塞等待:当 options
参数包含 WNOHANG
时,如果指定的子进程尚未结束,waitpid()
函数会立即返回,而不是阻塞调用进程。
返回值:如果子进程已经结束,waitpid()
返回子进程的PID;如果子进程尚未结束(并且 WNOHANG
被设置),则返回0。
错误处理:如果发生错误,waitpid()
返回-1,并设置 errno
以指示错误原因。
额外多说一句,那个HANG在程序员口中很常见,如果一个程序员说什么东西hang住了,就是说什么东西宕机了!
这里讲一个故事,来理解一下,
假设张三是一个学渣,他有一个学霸朋友,叫李四,李四住在10楼,由于张三是学渣,所以不关注老师发布的消息,但是一天他突然听到室友说明天要Linux考试了,他慌的一批,于是给李四打电话,事情到这里便有了两条主线:
主线一:李四接到张三的电话后,得知他是想要借我的学霸笔记,但是李四自己还没有用完,于是便跟张三说,不行啊,我没用完,现在还不能借你,说完之后,张三说了一声好的,便挂掉了电话,之后过了两三分钟,张三电话又打来了,问:四啊,你的笔记用完了吗?四说还没呢!张三又回了一句好的,便又挂了电话,此种循环持续了n次,直到最后一次李四说好了,我用完了,于是张三如愿以偿接到了笔记!
主线二:李四接到张三的电话后,得知他是想要借我的学霸笔记,但是李四自己还没有用完,于是便跟张三说,不行啊,我没用完,现在还不能借你,说完之后,张三说了一声好的,但是没有挂掉电话,之后过了两三分钟,张三又问了,四啊,你的笔记用完了吗?四说还没呢!张三又回了一句好的,但是没有挂电话,通话一直保持着,此种模式持续了n次,直到最后一次李四说好了,我用完了,于是张三如愿以偿接到了笔记!
再举一个例子感受一下:
假设你在一个餐厅排队等待点餐。在你前面的人点餐时,你无法进行点餐操作,必须等待他们完成。在这个过程中,你不能做其他事情(比如看手机、聊天等),直到轮到你点餐。这就像进程在阻塞状态时的行为。
---------------------------------------------------------------------------------------------------------------------------------
假设你在一个自助餐厅。你可以在等待食物准备好的同时,先去拿饮料、餐具等。你不需要一直站在食物窗口前等待,而是可以边做其他事情边等待。这就像进程在非阻塞状态时的行为。
4.4 代码实战
while(1) {pid_t rid = waitpid(id, NULL, WNOHANG);if(rid == id) {printf("wait child success\n");break;} else if(rid == 0) {printf("child not quit!\n");sleep(1);} else if(rid < 0) {perror("wait error!\n");break;}
}
代码解释:
-
非阻塞等待:
waitpid(id, NULL, WNOHANG)
调用waitpid
函数,其中id
是要等待的子进程的PID,NULL
表示不关心子进程的退出状态,WNOHANG
表示非阻塞等待。这意味着如果子进程尚未结束,waitpid
会立即返回而不是阻塞调用进程。 -
返回值检查:
-
if(rid == id)
:如果waitpid
返回的PID与要等待的子进程的PID相同,说明子进程已经结束,打印成功消息并跳出循环。 -
else if(rid == 0)
:如果waitpid
返回0,说明子进程尚未结束,打印一条消息表示子进程还在运行,并休眠1秒后继续检查。这是非阻塞等待的典型行为。 -
else if(rid < 0)
:如果waitpid
返回负值,说明发生了错误,打印错误消息并跳出循环。
-