Linux进程详解二:进程控制
Posted _light_house_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程详解二:进程控制相关的知识,希望对你有一定的参考价值。
【Linux】进程详解二:进程控制
前言
前面一章「【Linux】进程详解一:进程概念」 中我们已经了解过了进程的基本概念,这一章我们要进一步的学习进程,即**「进程的控制」**。
一、进程创建
1.fork()函数的认识
在已经认识了进程的基本概念之后,我们首先需要来自己创建一个进程。
创建进程有两种创建方式:
1.使用./
运行某一个可执行程序,这种是最常见的方式
2.使用系统调用接口创建进程,即使用fork()
,fork()
函数可以帮助我们从原来的进程中创建一个新的子进程,而原来的进程就被叫做父进程。
1.1.利用系统调用fork()创建进程
fork()
函数的语法规则:
#include <unistd.h>
pid_t fork(); // 返回值有两个:子进程返回0,父进程返回子进程的PID,如果子进程创建失败返回-1
注意:返回值有两个:子进程返回0,父进程返回子进程的PID,如果子进程创建失败返回-1(后面会解释为什么fork()
函数会有两个返回值,现在只要了解fork()
函数之后有父子进程都会有返回值即可)
废话不多说,我们先来看看如何使用fork()
,之后我们再来观察fork()
中干了什么,以及父子进程的关系。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
int main()
{
printf("Before:PID is %d\\n", getpid()); // 在fork之前打印一句话
pid_t pid = fork();
if (pid == -1) {
printf("fork error!\\n");
exit(1);
}
printf("After:PID is %d, return is %d\\n", getpid(), pid); // 在fork之后打印一句话
sleep(1);
return 0;
}
运行结果:
案例解释:运行的结构为fork()
之前只有一个进程,所以只打印出了一句话。而fork()
之后,多出了一个新的进程,所以会打印出两句话,并且两个进程返回值不一样,其中返回值为0的进程为子进程,返回值非0的进程为父进程。
注意:在fork()
之后,父子进程是相互独立的两个进程,所以两个进程的执行顺序是不能确定的。完全取决于调度器的调度。
1.2.fork()在内核中都干了什么?
在一个进程中调用fork()的时候,内核中做了哪些工作?
1.给子进程分配内存块和task_struct
和mm_struct
等数据结构。
2.将父进程中部分数据结构的内容拷贝一份到子进程中。
3.添加子进程中系统进程列表中
4.fork()
返回相应的返回值,调度器开始调度。
其实知道了fork
函数内部的执行的过程,就知道了在fork
内部的时候,就已经创建了一个新的进程,所以新的进程会有一个返回值,而父进程也会有一个返回值,所以fork
创建一个进程会有两个返回值。
1.3.父子进程的关系
新创建的子进程机会和父进程一模一样,但是还是不完全一样。
- 子进程得到与父进程在用户级别虚拟地址空间相同的一份拷贝,包括代码和数据段,堆,共享库以及用户栈。而他们之间最大的区别就在于两个进程的PID不同。
- 父进程和子进程是并发执行的独立进程。
- 父进程和子进程有相同但是独立的地址空间,后面会讲到其实父进程和子进程在虚拟地址层面上地址空间是一样的,但是它们都有自己独立的物理地址空间。
- 子进程继承了父进程中所有的打开文件,所以父子进程共享所有的文件。
2.fork()函数的返回值
上面说过了,fork()
函数会有两个返回值,子进程返回0,父进程返回子进程的PID,下面就需要解决两个问题。第一:fork()
为什么会有两个返回值。第二:为什么要返回值是返回0和PID。
1.fork()
函数为什么会有两个返回值?
根据fork()
函数在内核中的操作就包含了子进程的数据结构的创建,所以**在fork()
返回之前,子进程就已经被创建出来了。而一旦被创建出来一个独立的进程就会有返回值,所以调用这个fork()
函数的父进程有一个返回值,而创建出的子进程也会有一个返回值。**因为这两个过程是在fork()
函数内部就已经完成了,因此我们在fork()
函数外面看到的现象就是一个函数出现了两个返回值。
2.为什么fork()
函数中,子进程要返回0,而父进程要返回子进程的PID?
一个父进程可以创建很多的子进程,而每一个子进程都只能有一个父进程。**而父进程创建子进程是为了让子进程完成任务的,所以父进程需要标志每一个子进程,所以父进程通过返回子进程的PID来标识每一个子进程。**而子进程只有唯一的父进程,所以不需要标识父进程,因此返回一个0就可以了。
3.写时拷贝
一开始创建子进程的时候,子进程和父进程的代码和数据共享,即相同的虚拟地址会映射到相同的物理地址空间。 当在子进程要修改父进程中的数据的时候,父进程中的数据会重新的拷贝一份,然后子进程再对数据进行修改。这样父子进程中的数据就独立了。
对于写时拷贝,有三个问题要注意:
1.为什么要进行写时拷贝?
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
2.为什么不在创建子进程的时候就直接在子进程中拷贝一份父进程中的data和code?
子进程不一定会修改父进程中的code或者data,只有当需要修改的时候,拷贝父进程中的数据才会有意义,这种按需分配的方式,也是一种延时分配,可以高效的时候使用内存空间和运行的效率。
3.父进程的代码段会不会进行拷贝?
一般情况下,子进程只会修改父进程副本的数据,不会对父进程的代码进行什么操作。但是当在进程替换的时候,子进程会拷贝一份父进程的代码段。
4.fork()函数的使用场景
1.一个进程希望有多个进程执行一段代码的不同部分。
2.可以在一个进程中调用另一个进程,可以通过进程替换exec
系列函数实现。
5.fork()调用失败的原因
一般情况下fork()
函数不会调用失败,但是有两个情况下会使得fork()
创建子进程失败:
1.系统中已经存在了很多的进程,内存空间不足以再创建进程了
2.实际用户的进程超过了限制
二、进程终止
了解进程创建之后,我们就要来了解一个进程的终止。
本节会介绍进程终止退出的场景,然后再分别介绍进程两种进程退出的方法。
1.进程终止的使用场景
进程需要终止退出的情况有三种:
1.代码运行完毕,并且运行结果正确。(进程正常终止)
2.代码运行完毕,并且运行结果不正确。(进程正常终止)
3.进程崩溃(进程异常终止)。
2.进程退出码
2.1.进程退出码概念
得到进程退出码有不止一种方式,但是这里介绍一种大家最熟悉的得到进程退出码的方式。
我们平时如果想要写一个C/C++
程序的代码,写的第一个函数一定是main()
,而main()
是由返回值的。而**所谓的进程退出码就是以main()
函数的返回值的形式返回的。退出码为0表示代码执行成功,退出码为非0表示代码执行失败。**所以一般情况下,main()
函数返回0,以表示代码执行成功。
下面两个问题可以帮助你更好地理解进程退出码的意义。
1.
main()
的返回值给了谁?
main()
函数也是一个函数,既然函数有返回值,那么该函数返回给了谁呢?要想搞清楚这个问题,就需要搞清楚到底是谁调用了main()
函数。不同的平台下调用main()
函数的函数不同,但是最终main()
函数是由系统间接调用的,所以其实main()
的返回值返回给了操作系统。
2.为什么
main()
函数要有返回值或者进程要有退出码?
一个程序被加载到内存上,形成进程,是用来完成某项任务的。当进程完成任务后,我们需要知道进程完成任务的情况,因此需要通过退出码这种形式来得知进程执行任务的情况。
3.为什么退出码为0表示执行成功,非0表示执行错误?
前面说了进程需要通过进程退出码的性质告诉外界自己完成任务的情况。
如果进程成功的执行完任务正常退出,这种情况很好。而且这种情况值唯一的,所以用0就可以表示了。
但是如果进程非正常退出,那么我们就需要知道进程为什么不正常退出,这时情况就比较复杂了,不正常退出的情况有很多, 例如内存空间不足、非法访问以及栈溢出等等。所以非正常退出的原因需要很多的数字来表示,因此就使用了非0来表示。
2.2.查看进程退出码
进程退出码有很多,每一个退出码都有对应的字符串含义,帮助用户确认执行失败的原因。
我们可以使用$?
来查看最近一个进程的退出码。
echo $? # 打印出最近一个进程的退出码
例如:
如果想要知道每一种的进程退出码的含义, C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:
#include <cstdio>
#include <cstring>
int main()
{
for (int i = 0; i < 100; i ++) {
printf("第%d中进程退出码的含义: %s\\n", i, strerror(i));
}
return 0;
}
运行结果:
注意:这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。
3.进程终止的方法
正常终止进程一般由3种方法。
3.1.return退出
最常用的退出方式就是在main()
最后通过return
的方式退出进程。
例如:
#include <cstdio>
int main()
{
printf("hello world\\n");
return 0;
}
查看进程退出码:
3.2.exit()系统接口退出
exit()
函数和return
的功能差不多,但是exit()
在任何的地方只要被调用,就会立即的退出进程。
注意和return
的区别:只有在main()
函数中return
才会退出进程,而exit()
在任意一个函数中都可以退出进程。
例如:
#include <cstdio>
#include <cstdlib>
void func()
{
printf("I am exiting!"); // 这里没有使用\\n来刷新缓冲区
exit(1);
}
int main()
{
func();
}
运行结果并且查看进程退出码:
由上面这一段可以看出:
1.exit()
可以像return
返回进程退出码。
2.原本在缓冲区中的内容,在exit()
之后被刷新出来了,所以exit()
其实在退出之前,刷新了缓冲区。
3.3._exit()系统接口退出
_exit()
其实是封装在exit()
函数中的,和exit()
的区别在于_exit()
在退出之前不会刷新缓冲区,而是直接退出。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
void func()
{
printf("I am exiting!"); // 这里没有使用\\n来刷新缓冲区
_exit(1); // 和exit()那一段代码相比,只有这一部分不同
}
int main()
{
func();
}
运行结果并且查看退出码:
通过运行结果可知:
_exit()
函数不会在退出进程前刷新缓冲区。
总结:return
,exit()
和_exit()
相似点:
通过return
,exit()
和_exit()
都可以得到退出码。
不同点:
return
:return
只能在main()
函数中返回才可以退出进程。
exit()
:exit()
可以在任何的地方随时的退出进程,并且在退出进程前会刷新缓冲区。
_exit()
:_exit()
可以在任何的地方随时的退出进程,但是在退出系统的时候,不会刷新缓冲区。
联系:
1.在main()
函数中的return
等价于exit()
2.在exit()
中封装了_exit()
函数。
4.进程的异常终止
进程也会异常退出:
情况1:向进程发起信号或者ctrl + c
直接终止进程。
使用kill -9 PID
或者ctrl + c
可以直接使得进程异常终止。
情况2:代码出现段错误导致运行异常退出。
例如:代码中有num/0
的情况或者出现了野指针的问题。
注意:当进程异常终止的时候,退出码就没有意义了。因为退出码是进程正常退出的返回的信息,对于异常终止的进程,退出码不能反映出进程的执行的情况。
三、进程等待
1.进程等待的意义
「进程等待」的工作就是让父进程回收子进程的资源,获取子进程的退出信息。
因为如果子进程退出,父进程不读取子进程的退出信息回收子进程的资源的话,子进程就会变成僵尸进程,进而造成内存泄漏。而一个进程变成僵尸进程的时候,就算是使用kill -9
发送信号的方式也是不能回收该进程的资源的。
所以一定需要通过父进程通过进程等待的方式,来回收子进程的资源,同时为了搞清楚子进程完成任务的情况,也需要通过通过进程等待的方式获取子进程的退出信息。
2.子进程的status
我们需要通过pid_t wait(int* status);
函数和pid_t waitpid(pid_t pid, int* status, int options);
函数来做进程等待。
而在学习这两个函数之前,我们发现这两个函数中都有一个参数status
,而这个参数比较复杂且重要,所以我们先来学习一下status
这个参数。
2.1.status
的作用
上文说过,进程等待不仅是回收子进程的资源也需要获取子进程的退出信息,所以 status
的作用就是获取退出的信息 。status
是一个输出型参数,即在wait()
函数外面的变量,传入wait()
函数,然后在wait()
内部对status
进行操作,从而改变status
。
注意:如果不想要获取进程的退出信息的话,就可以用NULL
替代status
。
2.2.了解status
的组成
在status
的后16个比特位上,高8位表示进程退出的状态,即进程退出码。而后7位为进程终止的信号。第8个比特位是一个标志。
注意:当进程正常退出的时候,不用查看退出信号。而如果一个进程异常退出,即被信号杀死的话,不用看退出码。
3.3.从status
中获取退出码和退出信号
有两种方法我们可以获取status
中的退出信息。
方法一:位运算
既然我们已经知道了status
中的比特位组成部分,我们就可以通过位运算的操作直接获取退出信息。
int exit_code = (status >> 8) & 0xff; // 获取退出码
int exit_signal = status >> 0x7f; // 获取退出信号
方法二:使用宏
在系统中,提供了两个宏来获取提出码和退出信号。
WIFEXITED(status); // 用于查看进程是否正常退出,其实就是查看是否有退出信号
WEXITSTATUS(status); // 用于获取进程的退出码
再次提醒:如果一个进程被信号杀死,则退出码没有意义。
举例子:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\\n", getpid(), getppid());
sleep(1);
}
exit(10);
} else {
// father
int status = 0;
pid_t res = wait(&status);
if (res > 0) {
printf("wait child process success!\\n");
printf("exit code1:%d, exit signal1:%d, exit code2:%d,exit signal2:%d\\n", (status >> 8) & 0xff, (status) & 0x7f, WEXITSTATUS(status), WIFEXITED(status));
}
}
return 0;
}
运行结果:
3.进程等待的方法
下面来学习进程等待的两种方法:分别是调用wait()
函数和waitpid()
函数。
3.1.wait()
方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
wait()
函数的作用是:等待任意的一个子进程
- 返回值:如果等待成功返回等待进程的PID,等待失败则返回-1
- 参数:status为输出型参数,通过传入一个参数来获取被等待的子进程的退出状态。如果不想获取可以设置为NULL
例如:
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\\n", getpid(), getppid());
sleep(1);
}
exit(10);
} else {
// father
int status = 0;
pid_t res = wait(&status); // 如果wait成功,返回值为子进程的PID
sleep(10); // 这里休眠了10秒,可以看到子进程已经退出了并且没有被wait,这是进程变成僵尸进程
if (res > 0) {
printf("wait child process success!\\n");
if (WIFEXITED(status)) {
// 正常退出
printf("exit code: %d\\n", WEXITSTATUS(status));
} else {
// 异常退出
printf("exit signal:%d\\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
这里是监控级进程的一个脚本:
while :; do ps -axj | head -1 && ps -axj | grep test | grep -v grep; sleep 1; done;
我们可以发现,在子进程运行完毕之后,因为父进程还在sleep()
,所以此时子进程变成了僵尸进程,而在父进程sleep()
完了之后,子进程的资源被wait()
完回收掉了,所以僵尸进程消失了。
3.2.waitpid()
方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
waitpid()
函数的作用是:等待指定的一个子进程或者任意一个进程。(这个可以有options
参数控制)
-
函数的参数:
pid
:指定等待的子进程的PID
,如果PID
设置为-1
的话,则等待任意一个子进程。status
:输出型参数,获取子进程的退出信息,如果不需要进程退出的退出信息,可设置为NULL
。options
:当options
设置为0的时候,叫做阻塞等待。当options
设置为WNOWAIT
的时候,叫做非阻塞等待。(后面会有阻塞等待和非阻塞等待的例子)
-
函数的返回值:
- 等待成功返回被等待子进程的
PID
。 - 如果
options
被设置为WNOWAIT
,但是并没有出现等待的子进程时,返回0。 - 调用
wait
失败,返回-1
。
- 等待成功返回被等待子进程的
下面分别对阻塞等待和非阻塞等待举出一个例子:
在子进程运行的时候,父进程在干什么呢?如果父进程就在那里等待子进程完成任务,接收子进程的退出信息的话,这种方式就是阻塞等待。就好像父进程被阻塞住不能前进一样。。如果父进程在子进程运行的时候,自己可以感自己的事情,这种方式就叫做非阻塞等待。
所以想要判断是否为阻塞或者非阻塞等待,就只要判断父进程在子进程运行的时候,可不可以自己运行自己的代码即可。
下面两个案例,为了使我们看清楚,所以在子进程运行的时候sleep(1)
,这样就可以看清楚是否父进程在运行代码。
3.2.1.waitpid()
阻塞等待案例
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\\n", getpid(), getppid());
sleep(1);
}
exit(10);
} else {
// father
int status = 0;
pid_t res = waitpid(pid, &status, 0); // 阻塞等待指定的子进程
if (res > 0) {
printf("wait child process success!\\n");
if (WIFEXITED(status)) {
printf("exit code: %d\\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
情况一:
情况二:
3.2.2.waitpid()
非阻塞等待案例
当options
设置为WNOHANG
的时候,进程等待为非阻塞等待,此时父进程不会一个阻塞在一个地方等待子进程的退出,而是每隔一段时间检查是否子进程已经退出,如果子进程没有退出的话,waitpid
返回0
,此时父进程可以独立的执行自己的代码。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\\n", [OS-Linux]详解Linux进程控制
linux 进程间通信 dbus-glib实例详解二(上) 消息和消息总线(附代码)
linux 进程间通信 dbus-glib实例详解二(下) 消息和消息总线(ListActivatableNames和服务器的自动启动)(附代码)