Linux 系统编程 进程篇 (六)

文章目录

  • [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 进程退出

刚在的进程创建执行完了以后,我们来聊聊进程终止。

思考一下,进程退出,有几种情况?

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果错误。
  3. 代码异常终止

我们现在先主要聊聊前两个,进程异常终止呢,这个有些不一样,后面慢慢对比引入。

首先,我们怎么判定进程运行完毕,结果是正确还是错误呢? 根据 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在阻塞等待捏。

相关推荐
爱笑的Sunday1 小时前
Linux Java前后端项目 企业级0-1完整部署手册
java·linux·运维·服务器
小年糕是糕手1 小时前
【C/C++刷题集】顺序表、vector、链表、list核心精讲
c语言·开发语言·数据结构·c++·算法·leetcode·蓝桥杯
xyx-3v1 小时前
FreeRTOS队列通信
java·服务器·网络
会编程的土豆1 小时前
从 C/C++ 视角快速上手 Go 语言:核心差异与避坑指南
c语言·开发语言·c++·后端·golang
小白学大数据1 小时前
Python 3.7 高并发爬虫:接口请求与页面解析并发处理
开发语言·爬虫·python
我命由我123451 小时前
Kotlin 开发 - 双冒号操作符(引用顶层函数、引用成员函数、引用构造函数、引用属性、引用类)
android·java·开发语言·kotlin·android studio·android jetpack·android-studio
Jacky-0081 小时前
Python pywin32 outlook邮箱
开发语言·python·outlook
minji...1 小时前
Linux 线程同步与互斥(六) 线程安全与重入问题,死锁,线程done
linux·运维·开发语言·数据库·c++·算法·安全
佳xuan1 小时前
QA与RAG检索
java·服务器·前端