《探秘 Linux 进程控制:驾驭系统运行的核心之力》
在 Linux 的世界里,进程是系统运行的基本单元,如同一个个忙碌的 “工人”,执行着各式各样的任务。而进程控制,就是那只 “无形的手”,精准地调度、管理这些 “工人”,让系统高效且有序地运转。从进程的创建、终止,到进程间的等待与程序替换,再到理解 shell 这一与用户交互的桥梁如何运作,每一个环节都蕴含着 Linux 系统精妙的设计逻辑。接下来,我们就一同深入 Linux 进程控制的领域,揭开
前引:在 Linux 的世界里,进程是系统运行的基本单元,如同一个个忙碌的 “工人”,执行着各式各样的任务。而进程控制,就是那只 “无形的手”,精准地调度、管理这些 “工人”,让系统高效且有序地运转。从进程的创建、终止,到进程间的等待与程序替换,再到理解 shell 这一与用户交互的桥梁如何运作,每一个环节都蕴含着 Linux 系统精妙的设计逻辑。接下来,我们就一同深入 Linux 进程控制的领域,揭开它神秘的面纱!
目录
【一】进程退出码
进程的退出码可以理解为该进程的执行结果反馈:(子进程将“遗言”交给父进程)
为0表示信号正常没有,正确执行;非0表示出现了问题
而一个进程(比如执行程序)跑完之后,它可以分为三种状态:
正确执行,结果正确
正确执行,结果错误
执行完部分,报错
例如:我们C语言的 return 0 也是一种返回退出码。退出码的存在方便我们快速定位错误
(1)如何查看退出码
在 Shell 中,上一条命令的退出码存放在特殊变量 $?
中,我们可以通过下面的命令查看:
echo $?
例如:我现在执行命令 ls,然后查看退出码
(2)error介绍与使用
系统调用(system call) 或 C 标准库函数执行失败时,会返回一个特殊值(通常是
-1
或NULL
),并将一个错误编号存放在一个全局变量errno
中,它在程序不崩溃的情况下一般都是可以正常拿到错误编号的,如果我们要查看哪些可以正常拿到错误编号,可以通过安装 sudo yum install moreutils 执行 errno -l 来查看!
errno
是一个整数,每个值代表一种特定的错误类型- 头文件:
#include <errno.h>
- 值为 0 表示 “没有错误”
- 非 0 值表示 “发生了某种错误”
errno相当于单词系统/函数调用返回的错误编号,退出码是整个进程的退出结果反馈
它通常搭配strerror()
来进行使用,以errno作为该函数调用的参数,需要头文件<string.h>
例如:
注意:部分程序被发生信号会导致直接崩溃,不会执行到打印错误语句,例如:
- 段错误 (
SIGSEGV
,信号 11) → 访问非法内存地址(如空指针解引用、数组越界)- 总线错误 (
SIGBUS
,信号 7) → 访问未对齐的内存地址- 浮点异常 (
SIGFPE
,信号 8) → 除零等数学错误- 非法指令 (
SIGILL
,信号 4) → 执行了无效的机器码特点:
这些错误不会通过
errno
返回,而是由内核发送信号终止进程进程在收到信号后默认行为是终止(可能产生 core dump)
例如:
(3)exit
exit()
是 C/C++ 标准库中的一个函数,用来正常终止一个进程
功能:终止当前进程,并将退出状态码返回给父进程,我们可以设置参数为errno
(2)exit与_exit
特性 | exit(status) |
_exit(status) / _Exit(status) |
---|---|---|
头文件 | <stdlib.h> |
<unistd.h> 或 <stdlib.h> |
标准 | C/C++ 标准库函数 | POSIX 系统调用(更底层) |
清理操作 | 会执行退出处理函数(atexit ),刷新 I/O 缓冲区 |
不执行清理,不刷新缓冲区,直接终止 |
用途 | 正常程序结束(建议在应用层使用) | 子进程结束时避免重复清理(如 fork 后子进程用) |
(3)退出码与信号
首先我们知道退出码相当于该进程的“遗言”,为该进程正常结束还是被信号杀死返回给父进程的一个状态码信息,那么什么是信号?它跟退出码的关系是什么?
信号:Linux 内核发给进程的一种中断事件通知,用来告诉进程发生了某个事件,需要立即处理
我们可以通过下面的指令来查看信号类型和对应的编号,例如:
kill -l
如果程序正常退出(不管结果正确与否)它会通过退出码来交给父进程来反馈用户
如果程序出现严重的错误(被信号终止),它的退出码将由信号编号计算得出
退出码 = 128 + 信号编号
例如:退出码130=128+2
【二】进程等待
进程等待可以解决以下几个问题:
子进程没有父进程来回收处于的僵尸状态、获取子进程退出信息(执行结果、是否正常退出等)
(1)wait()
函数作用:
阻塞等待:如果没有子进程退出,父进程会一直卡在 wait()
这里,直到有子进程退出
等待任意一个子进程:不能指定等待某一个
参数:整型指针(int * status),也可以传 NULL,表示不关心子进程的退出状态
返回值:
返回类型为 pid_t ,等待成功返回子进程的PID,否则返回-1
例如:我们直接清除资源让子进程保持僵尸状态(除非子进程只能被父进程回收)
(2)waitpid()
功能:
wait()的升级版,也是回收子进程的,同时可以可以获得更多的选择(一次只能回收一个)
参数:
pid_t pid, int *status, int options
第一个参数:
pid 取值 |
含义 |
---|---|
-1 | 等待任意子进程(和 wait 一样) |
> 0 | 等待进程 ID 等于 pid 的子进程 |
0 | 等待和当前进程 同一个进程组 的子进程 |
< -1 | 等待进程组 ID 等于 pid 绝对值的子进程 |
第二个参数:和wait()一样,可以选择NULL
第三个参数:
选项 | 含义 |
---|---|
0 | 阻塞等待(默认行为) |
WNOHANG | 非阻塞:如果没有子进程退出,立即返回 0,不等待 |
WUNTRACED | 除了退出的子进程,还返回被暂停的子进程状态 |
WCONTINUED | 返回被 SIGCONT 唤醒的子进程状态 |
返回值:
- > 0:返回退出的子进程 PID
- = 0:
WNOHANG
模式下,表示没有子进程退出 - -1:出错(如没有子进程)
例如:我们先让子进程处于僵尸状态,然后执行waitpid()
(3)status参数
status
是一个 整型指针参数(int *
),用来接收子进程的退出状态
上面的wait()和waitpid()函数,它们的status参数我们都选择的是NULL不关心子进程退出状态
那么如果现在要去获取子进程退出状态该如何操作?
status是一个整型的,现在研究它的前16bit位:(信号+退出码)
我们只需要设置一个整型变量 int status 即可,操作系统会帮我们自行填充,例如:
我们填充了 status 表示我们关心子进程的退出状态,我们可通过下面Linux定义的宏来解析:
常用宏:
宏 作用 WIFEXITED(status)
如果子进程是 正常退出(调用 exit()
或return
),返回真WEXITSTATUS(status)
当 WIFEXITED
为真时,提取退出码(exit()
的参数)WIFSIGNALED(status)
如果子进程是 被信号杀死,返回真 WTERMSIG(status)
当 WIFSIGNALED
为真时,返回终止进程的信号编号WCOREDUMP(status)
如果子进程被信号杀死且产生了 core dump,返回真 WIFSTOPPED(status)
如果子进程被暂停(配合 WUNTRACED
),返回真WSTOPSIG(status)
当 WIFSTOPPED
为真时,返回暂停进程的信号编号WIFCONTINUED(status)
如果子进程被 SIGCONT
唤醒(配合WCONTINUED
),返回
(4)非阻塞等待
首先阻塞等待我们已经讲过了,就是父进程一直等待子进程回收,期间父进程只能无效等待!
而非阻塞等待就是在waitpid()第三个参数填WNOHANG,这样父进程如果没有等到子进程就直接返回0,不等待子进程了。但是子进程又要回收才行,所以我们可以在父进程非阻塞等待的这段时间让父进程去干自己的事情,例如:父进程隔一段时间就去看是否可以回收子进程,比较自由
那么如果有多个子进程呢,父进程如何保证回收完了子进程再退出?
第一种:每创建完一个子进程就回收一个子进程
第二种:一次性创建多个子进程,最后一起回收
(5)注意事项
(1)wait()和waitpid()每调用一次就会回收一个子进程
(2)必须注意父子进程代码可能有两份的问题,因为子进程如果写时拷贝会和父进程一样的代码
(3)回收成功返回子进程PID,但是子进程可能休眠,所以保险起见等到返回-1就结束回收
【三】进程替换
进程替换就加载新的代码和数据来替换当前进程的代码和数据,不会创建新的进程
那么可以理解为:用于在当前进程中替换掉当前运行的程序,加载并执行一个新的程序
特点:
(1)不会创建新的进程
(2)直接替换代码和数据(注意代码数据的对应范围可能变化),其余不变化
参数特点:第一个参数都是文件执行路径,第二个参数都是如何执行该文件
下面来看 exec 系列的函数,它们都是用一个新程序替换当前进程的代码段、数据段、堆和栈
l (list) :参数采⽤列表
v (vector) :参数⽤数组
p (path) :有 p ⾃动搜索环境变量 PATH
e (env) :表⽰⾃⼰维护环境变量
(1)execl()
函数原型:
int execl(const char *path, const char *arg, ...);
第一个参数:文件的绝对路径或相对路径
第二个参数:具体的执行方法(命令行怎么调用,这里就怎么写,空格代表逗号,字符串形式)
最后一个参数:必须是NULL
返回值:
调用成功不返回,失败返回-1
例如:
如果我们想调用其余的可执行程序也是可以的,例如:
(2)execlp()
函数原型:
int execlp(const char *file, const char *arg, ...);
第一个参数:文件的绝对路径或相对路径(但是这里可以只写文件名,系统会去PATH自动查找)
第二个参数:具体的执行方法(命令行怎么调用,这里就怎么写,空格代表逗号,字符串形式)
(注意:若想在PATH里面添加自定义路径,可执行export PATH=/你的自定义路径:$PATH)
最后一个参数:必须是NULL
例如:
(3)execv()
函数原型:
int execv(const char *path, char *const argv[]);
第一个参数:文件的绝对路径或相对路径
第二个参数:参数数组,argv [0] 一般是执行命令,argv 末尾必须是 NULL(不是字符串)
例如:
直接用字符串初始化也是可以的:
(4)execvp()
函数原型:
int execvp(const char *file, char *const argv[]);
第一个参数:文件的绝对路径或相对路径(但是这里可以只写文件名,系统会去PATH自动查找)
第二个参数:参数数组,argv [0] 一般是执行命令,argv 末尾必须是 NULL(不是字符串)
例如:
(5)execle()
函数原型:
int execle(const char *pathname, const char *arg0, ..., (char *) NULL, char *const envp[]);
第一个参数:必须是可执行文件的路径(必须能直接找到可执行文件)
第二个参数:新程序的命令行参数列表,最后一个参数必须是 (char*)NULL
第三个参数:新的环境变量数组,替换当前进程的环境变量
例如:
我们先写另外一个 .c 文件,然后gcc编译得到“测试”可执行程序:
现在我们去另外一个文件来调用这个可执行程序:
然后我们看看执行的结果:
为什么“text.c”文件调用外面的“测试”文件,打印出来的环境变量和“测试”单独打出来会不一样?
首先这个函数接口的作用是替换当前进程的代码数据,即创建出来的子进程
然后该函数会去调用“测试”这个可执行程序,调用同时用我们自定义的环境变量
而如何直接在终端用指令调用“测试”可执行程序,是由shell调用的,会继承当前shell本身环境变量
如何在替换当前进程的代码数据同时采用它原本的环境变量?
通过一个指向字符串指针数组的变量 environ 来替换原来的 argv 环境参数
更多推荐
所有评论(0)