文章目录
- [Linux 系统编程 进程篇 (六)](#Linux 系统编程 进程篇 (六))
-
- [1 进程控制](#1 进程控制)
-
- [1.1 进程创建](#1.1 进程创建)
- [1.2 进程终止](#1.2 进程终止)
-
- [1.2.1 进程退出](#1.2.1 进程退出)
- [1.2.2 常见进程退出的方法](#1.2.2 常见进程退出的方法)
- [1.3 进程等待](#1.3 进程等待)
-
- [1.3.1 进程等待的必要性](#1.3.1 进程等待的必要性)
- [1.3.2 进程等待是什么,怎么做](#1.3.2 进程等待是什么,怎么做)
-
- [wait 方法](#wait 方法)
- [waitpid 方法](#waitpid 方法)
Linux 系统编程 进程篇 (六)
1 进程控制
上一篇里面,我们写到了进程地址空间,进程地址空间。接下来我们来谈谈这个进程控制
1.1 进程创建
进程创建我们在初识进程的时候已经见过了,就是使用 fork() 函数来创建子进程。
这里就不过多地赘述了。 当我们使用 fork() 以后,内核会把新的内存块和内核数据结构给给子进程,然后拷贝父进程的程序地址空间,页表什么的,然后添加子进程到调度队列里面,最后CPU调度。这些以前都提到过。
frok() 函数的返回值,给子进程返回 0 ,给父进程返回子进程pid。这里注意, fork之后,子进程和父进程谁先执行完,全由调度器决定。
子进程要修改父进程中同名变量的时候,会发生写实拷贝。原理和为什么要写实拷贝都提到过了。
fork的常规用法,之前我们用过的一个是根据返回值来让父子进程执行不同的 if-else 语句。 还有一种用大就是让子进程来执行不同的可执行程序,通过调用 exec 系列函数。
思考一下,如果让子进程调用的是另一个操作系统呢?父子进程互相不影响,具有独立性,这样两个不同的操作系统就跑起来了。这就是内核级虚拟机的原理。
fork失败的原因,fork也是可能失败的,一个原因是这个操作系统里面的进程太多了,操作系统对这个是有这个进程上限的,这个很好理解。 第二个原因就是实际用户的进程数超过限制,操作系统是公平的,对每个用户拥有的进程也是有限的。
1.2 进程终止
1.2.1 进程退出
刚在的进程创建执行完了以后,我们来聊聊进程终止。
思考一下,进程退出,有几种情况?
- 代码运行完毕,结果正确。
- 代码运行完毕,结果错误。
- 代码异常终止
我们现在先主要聊聊前两个,进程异常终止呢,这个有些不一样,后面慢慢对比引入。
首先,我们怎么判定进程运行完毕,结果是正确还是错误呢? 根据 main 函数的返回值。 换句话说, main 函数的返回值就代表了我们程序的运行情况。
有的时候 main 函数可以不写返回值,也不写return语句 默认就是 int ,返回 0 。
我们通常写小程序的时候,会直接就是返回 0 , 0 就是程序正常退出的意思。 除此之外, 返回值还可以是正整数,代表程序异常退出。 main 函数的返回值呢,也是 这个程序或者说进程的退出码。
为什么要分为 0 和 非零正整数呢? 因为错误原因可能有很多个,正确的话,说实话就不需要原因了。
那么问题又来了?我们如果通过这个退出码,来确定这个错误的原因是什么呢,毕竟退出码都是数字。
还记得我们在学习C语言里面的字符串函数的时候,有过这样一个函数 strerror :

我们传入一个错误码,这个函数就会返回一个这个错误原因的这个字符串。
说到这个错误码,大家有没有想起以前学过的 errno ,这个是程序的错误码,当程序出现错误的时候,这个 errno 的值就会被自动处理,同样是使用 strerror 打印出来。 要使用这个 errno ,要包含头文件 <errno.h>
同样,我们把这个结果错误退出的退出码填进去,我们看看效果:

可以看到,这个退出码都对应着不同的错误,当退出码是 0 的时候,是成功。 在linux里面,有意义的退出码一共是134个。
剩下的打印多少都是unknown。

至于代码异常终止,相信大家也都可能见过,比如说这个访问到空指针了之类的。程序发生异常终止的话,一般是进程收到信号了,此时的退出码无意义。后续详细说,这里先抛出来。
我们可以通过指令 echo ? 来查询上一个进程的退出码。注意,是上一个。 这里的 ?里面就存着退出码, 通过这个 来查看。

思考一下,为为什么要有进程退出码。子进程是不是进程?肯定是进程。是父进程创建的。父进程创建子进程一定是想让子进程完成一些事情,所以,子进程不管怎么退出的,还是异常终止了,要把这个执行的结果传回来。
那么这个进程的退出码存在哪里呢? 写在自己的 task_struct 内部的。 而我们的进程父进程都是bash,所以通过 echo 就可以拿到 bash 子进程的退出码。
1.2.2 常见进程退出的方法
首先第一个就是我们这个从 main 函数返回。除此之外, 我们还和有使用这个 exit 接口:

这歌exit使用的时候,传参的时候传入的是退出码,自己随便想怎么退出怎么退出,毕竟是自己的程序肯定能认出来。
那么,这个函数的和这个 return 的区别是什么呢?
首先,如果在 main 函数里面,这两个东西效果是一模一样的。 但是, exit 是个函数, 可以在任意函数里面调用,如果不在 main 函数里面的话,使用return是退不出来的,只能使用 exit。
还有一个是 _exit

看起来这个 _exit 和 exit 使用上没什么区别。实际上是有的。 _exit 是系统调用, exit 就是封装了这个 _exit 才实现退出的,除此之外还做了一些其它的事情, 比如说,这个执行用户通过通过这个 atexit 或者 on_exit 定义的清理函数,关闭所有打开的流,刷新缓冲区,最后再调用 _exit。 _exit 就是直接就退出了。
所以,exit 和 _exit 就是库函数和系统调用之间的关系。还有一个现象是什么呢? exit() 退出的时候,会进行行刷新缓冲区,但是 _exit 退出就不会刷新行缓冲区。
这里,我们就可以引出一个结论,为后面讲缓冲区的时候做铺垫, 这个被刷新的缓冲区一定不是操作系统的缓冲区,因为调递推调用没有刷新。这个缓冲区一定是C语言库提供的缓冲区。
话说回来,如果是程序异常终止的话,可以直接 kill -9, 或者是 ctrl + c;
1.3 进程等待
1.3.1 进程等待的必要性
什么是进程等待,通俗一说就是父进程继续执行的时候要等待子进程结束。为什么要进行进程等待?
之前我们讲过,如果父进程在子进程退出的时候不管不顾,就会造成僵尸进程,从而内存泄漏。我们之前也见过,进程进入僵尸以后是杀不掉的,kill -9 也不行,操作系统就处理不了了。因为谁也没办法杀掉一个死去的进程,只能由父进程来处理回收。
最后,子进程执行的结果,父进程肯定要知道,从而进行进一步的操作。所以,进程等待是非常有必要的。
注意,父进程通过进程等待的方式一定要去回收掉子进程,但是不一定要去获取子进程的退出信息,这个是可选的,因为父进程和子进程之间的操作当然是可以没关系的。
1.3.2 进程等待是什么,怎么做
进程等阿迪是通过两个系统调用来进程操作的

一个是 wait 一个是 waitpid。
wait 方法
wait 这个进程调用呢,就是可以来回收子进程的。他的返回值是这个返回被等待成功的进程的 pid_t ,如果失败返回 -1.
注意啊,这里的等待的意思是等待并回收,回收也是他做的。
他的参数呢,是一个输出型参数,什么是输出型参数。思考一下,我们期望的不仅是拿到这个进程被等待的结果,我们还想拿到这个进程执行完之后的退出信息,但是函数只能返回 1 个值,所以我们想让他返回多个值的话,可以直接在外面定义一个参数,指针传进去,然后直接修改,得到我们期望的第二个值。所以,这就是输出型参数。
但是呢?我们发现这个 wait 并没有让我们指定进程,使用 wait的时候,谁先结束回收谁,如果想要这个精细化控制的话,可以使用 waitpid,后面会讲。
细心的同学发现了,我们想要拿到的,是这个进程退出信息,不就是我们之前提到的退出码吗?可以看到这个参数的名字都差不多,都是类似 status 。但真的是这样吗?

可以看到, 9 9我们这里让自己的退出码是 1 ,

可以看到,我们拿到的 status 并不是 1 ,而是 256 。这是为什么呢?
我们还可以得到一个结论,等待子进程的时候,父进程会阻塞在 wait ,等待子进程结束类似 scanf 的输入。
其实,这里的status和我们之前的有所不同。我们之前拿到的退出码是在这个 status 的基础上拿到的。
那么这个 status 是什么设计的呢?还记得之前的 O(1)调度算法中的bitmap位图吗?这里的status同样是用到了位运算。
首先呢,这个 status 是 32 个比特位,一个整形嘛。我们这里只用到了这个status的低16位。在这个底16位里面,前八位,也就是 15 ~ 8 位,是我们进程退出的退出码, 后 8 个位里面,首先,第八个位,也就是 7 位,是一个 core dump标志,这个我们先不用管,而后的七个位,是这个终止信号,也就是程序异常终止时,进程退出的信号。
如果进程没有异常的话,默认后八位都是 0 。
说到这里呢,就要来谈谈这个信号了。记得我们之前用过的信号 kill -9 或者说 -18 -19,那么信号是什么呢?信号其实也是一个宏,就是一个整数,来看:

可以看到 9 号信号,就是我们之前提到过的杀进程的信号。

可以看到,我们对应的信号就是个宏。可以看看 user/include/asm/signal.h 打开看看,就可以看到信号。
当这个程序异常终止的时候,比如说访问空指针,或者,除以零了,或者说被 kill -9 杀掉了还是怎么了,异常终止了,后七位就会被改成相应的信号的值。这时,我们会发现,为什么没有零信号?
因为没有异常退出的时候,后 7 位就是 0 , core dump 默认也是 0 。我们用到的是退出码。
而程序异常终止,我们修改的是后 7 个位,前面的退出码部分是不用的,所以,这也是我们之前说的,进程异常终止的时候,退出码是没有意义的。
所以,为什么我们设置的退出码是 1 ,而 status 是这个 256,我们也就知道了 2^8 嘛,想要拿到退出码的话,只需要这个status右移 8 位就 ok 。
当然,一般情况下呢,这个status不用我们手动去像什么右移,按位与来操作,有几个宏可以帮助我们直接就拿到想要的结果,这里介绍两个,
第一个就是 WIFEXITED(status) : 若正常终止子进程的返回的状态则为真, 用来查看进程是否正常退出。
第二个就是这个 WEXITSTATUS(status) 这个就是,若WIFEXITED非零, 提取子进程退出码, 查看进程退出码用的。
当然,如果不想要这个status的话,直接传一个 NULL 就好了。
那么这个是怎么做到的呢?当子进程退出的时候,进入僵尸状态,他的代码和数据可以释放掉不管他,但是这个PCB一定要留下,供父进程查看退出信息。而这个 子进程的僵尸的 task_struct 里面,一定放着这个 exit_code 和 exit_signal
当父进程调用这个 waitpid的时候,调用了系统调用,OS去这个子进程的task_struct里面去拿到这两个变量。然后返回到这个 status 里面。

其实,我们之前用到的 getpid , getppid 也是差不多的原理,都是去这个task_struct里面拿到对应的信息给用户。

至于,怎么把这个错误码在main函数结束以后,放到这个 task_struct 里面,就需要这个操作系统和编译器给咱们的程序加点东西了。
bash 想要拿到对应的进程退出码,也是一个道理。操作系统可以很简单拿到对应的退出信息,也可以很简单的处理这些退出信息。
waitpid 方法

可以看到 waitpid 和刚才的 pid 比就多了两个参数而已。 之前提到过 , wait 随便等待,谁先遇到他他就等待谁。
这个 waitpid 呢,就是等待特定的进程,传入 pid 就好。 如果第一个 pid 参数传 -1 的话,效果就和 wait 就一样了。
status 和之前的也是一样的,不过多赘述了。
来看第三个参数,是一个选项?这是什么呢?这个 options 默认我们在使用的时候传入的是 0。

可以看到这个参数就是有四个返回值,前面 <= -1 的,我们先不管。然后呢,当等待成功,返回 pid ,当等待失败返回0。
这和 wait 一样。
但是,这里的选项如果传入这个WNOHOANG, 这就不一样了,当等待成功返回 pid ,检查时子进程没有退出,返回 0, 当出错后,则返回 -1 , errno 被设置。
这个 W NO HANG是什么意思呢? 我们的电脑或者程序卡死了,其实这也叫 夯 住了,这个 WNOHANG 的意思 不要夯住。
我们之前说,在子进程结束之前,父进程会阻塞在 wait 的地方等待,这里我WNOHANG就是让父进程不阻塞等待。
怎么办到的呢?我们举个例子。
马上就要期末考试了,张三有些焦虑,害怕挂科。李四呢,天天记笔记,比较认真,张三就想请李四吃个饭,然后让他帮忙辅导辅导。等张三到了李四楼下,给李四打电话,李四说还没复习完,让张三等等。张三挂了电话就在楼下一直等着,啥也干不了就先等李四下来。
后来期这一课考完了,张三爽完一整晚。结果快睡觉了,舍友一说明天考操作系统的事,张三懵大杯了,赶紧又跑到李四楼下,给李四打电话再请他吃个夜宵。这次,李四刚要挂电话,张三就说,咱先别挂了,你什么时候好了和我说一声,李四答应了。然后张三在楼下一边看操作系统的课本,一边一会儿问一次李四好没好,一会儿问一次李四好没好。
第一个例子呢,张三就是父进程在阻塞调用。而第二个明显不一样,张三等待期间还可以干点自己的事,这个叫非阻塞调用。换句话说,非阻塞调用就是可以让父进程在等待的时候,还可以干点自己的事。而,这个张三在非阻塞调用的时候一遍一遍地问李四好没好,这个就叫非阻塞轮询,一直到李四好了。通过一个循环来实现就好。
所以,这个WNOHANG的选项就是让父进程在等待的过程中可以做自己的事,然后通过这个非阻塞轮询来等待这个子进程。这里用一个循环来实现就好。

这样,父进程再等待的过程中就可以做自己的事情。
还有一件事请,这个非阻塞调用的效率不用我说大家也知道更高,但是,这个效率高是张三效率高因为可以做自己的事,而不是李四效率高。
再补一句奥,非阻塞英文可以说None Block。
一直到李四好了。通过一个循环来实现就好。
所以,这个WNOHANG的选项就是让父进程在等待的过程中可以做自己的事,然后通过这个非阻塞轮询来等待这个子进程。这里用一个循环来实现就好。
外链图片转存中...(img-1diicJjB-1777348880280)
这样,父进程再等待的过程中就可以做自己的事情。
还有一件事请,这个非阻塞调用的效率不用我说大家也知道更高,但是,这个效率高是张三效率高因为可以做自己的事,而不是李四效率高。
再补一句奥,非阻塞英文可以说None Block。
所以,我们命令行里面sleep的时候呢,为什么输入命令的时候没反应,就是因为这个bash在阻塞等待捏。