前言
在 Linux 操作系统的世界里,进程是程序运行的动态实体,它们如同一个个忙碌的工作者,承载着系统中各种任务的执行。无论是系统服务的稳定运行,还是用户程序的交互响应,都离不开进程的支持。深入理解进程的生命周期,包括创建、终止、等待以及程序替换等关键环节,对于掌握 Linux 系统编程和开发高性能应用程序至关重要。
一、重谈进程创建
1.1 fork 函数
在 Linux 系统中,
fork
函数可从已有进程创建新进程。新进程是子进程,原进程为父进程。其函数声明为pid_t fork(void)
,返回值规则为:子进程中返回 0,父进程中返回子进程的pid
,出错则返回 -1。#include <unistd.h> pid_t fork(void);
当进程调用
fork
函数,控制转移到内核代码后,内核会进行以下操作:- 为子进程分配新的内存块和内核数据结构;
- 将父进程部分数据结构内容复制到子进程;
- 把子进程添加到系统进程列表;
fork
返回并开始调度。
实际上,完成前两步,子进程就已创建。
通过代码演示,能看到 fork
前父进程独自执行,fork
后父子进程分别执行,且谁先执行由调度器决定。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("befor pid:%d\n", getpid());
fork();
printf("after pid:%d\n", getpid());
return 0;
}
1.2 写时拷贝
通常,父子进程的代码是共享的,在不写入数据时,数据也共享。一旦任意一方尝试写入,就会以写时拷贝的方式再生成一份数据。
操作系统通过在创建子进程时,将父子进程页表中的数据项权限设为只读来实现写时拷贝。当父子进程写入数据触发权限问题时,操作系统会识别并拷贝数据,重新映射页表项,再将权限恢复为可读可写。
采用写时拷贝的原因在于避免不必要的数据拷贝。若父进程有大量数据,而子进程仅需修改少量数据,全部拷贝会浪费时间和物理内存,所以操作系统采用此策略。
1.3 fork 的常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端的请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从
fork
返回后,调用exec
函数。
1.4 fork 调用失败的原因
系统中有太多的进程。
实际用户的进程数超过了限制。
1.5 创建一批进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 5
void func()
{
int cnt = 10;
while(cnt)
{
printf("I am chid, pid:%d, ppid:%d\n", getpid(), getppid());
cnt--;
sleep(1);
}
return;
}
int main()
{
int i = 0;
for(i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)// 只有子进程会进去
{
func();
exit(0);// 子进程走到这里就退出了
}
}
sleep(1000);
return 0;
}
父进程执行的速度是很快的,由于父进程的 for 循环里没有 sleep 函数,所以五个子进程几乎是在同一时间被创建出来,创建出来的每一个子进程会去调用 func 函数,每一个子进程执行完 func 函数后会执行 exit 函数退出。父子进程谁先执行完全是由调度器来决定的。
二、进程终止
2.1 进程退出场景
进程终止意味着其生命周期结束,通常包括以下场景:
- 正常退出:代码运行完毕,结果正确。
- 异常退出:代码运行完毕但结果不正确,或因错误终止(如未捕获的异常)。
- 外部干预:被用户或其他进程终止(如
kill
命令)。
main 函数里常写的 return 0
的作用,就是每个进程终止时都会返回的一个退出码(Exit Code),用于标识运行结果:
- 0:表示成功(Success)。
- 非 0:表示失败或异常,不同值表征不同错误。
提示:父进程最关心子进程的运行情况,main
函数返回的退出码会被父进程(如 bash
)获取并保留,可通过 echo $?
查看最近进程的退出码。
int main()
{
printf("模拟一段逻辑!\n");
return 0;
}
2.2 strerror 函数的作用
strerror
函数是一个标准 C 库函数,用于将错误码转换为可读的错误信息字符串。它非常适合程序员在调试和错误处理时,将数字形式的错误码(如 errno
)转化为更直观的文本信息,方便理解和输出。
下面的代码展示了如何遍历并打印当前系统支持的所有错误码及其对应的描述信息:
#include <stdio.h>
#include <string.h>
int main() {
for (int i = 0; i < 200; i++) {
printf("%d: %s\n", i, strerror(i)); // 输出错误码及其描述
}
return 0;
}
说明:
strerror
仅对系统支持的错误码返回有效的错误描述。- 如果传入的错误码无效或系统未定义的错误,返回
"Unknown error"
。
2.3 errno
全局变量
errno
是 C 语言提供的全局变量,记录最近一次函数调用失败时的错误码。当调用 C 标准库函数失败,errno
会被赋值为特定数值,结合 strerror
函数可获取错误详细信息。例如 malloc
内存分配失败时,通过 errno
和 strerror
输出错误码及描述。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int ret = 0;
char* str = (char*)malloc(1000*1000*1000*4);
if(str == NULL)
{
printf("malloc error:%d, %s\n", errno, strerror(errno));
ret = errno;
}
else
{
printf("malloc success!\n");
}
return ret;
}
2.4 程序异常机制
程序异常(如除零操作、空指针解引用)会导致进程未执行到 return
语句即终止,此时退出码无参考价值。程序异常本质是进程接收到操作系统发送的信号,如空指针解引用会触发段错误信号,导致进程异常结束。Linux 系统的所有信号如下图所示:
2.5 进程退出方式
进程退出分为正常终止和异常终止两类:
- 正常终止:代码完整执行完毕,包括
main
函数return
语句返回、调用库函数exit
、调用系统函数_exit
。 - 异常终止:通过
Ctrl + C
中断,或因接收到特定信号导致进程强制结束。
三、进程等待
3.1 进程等待必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
- 此外,进程一旦变成僵尸状态,那就刀枪不入,就算是"杀人不眨眼"的
kill -9
也无能为力,因为谁也没有办法杀死一个死去的进程。 - 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
3.2 进程等待的方法
进程等待就是在父进程的代码中,通过系统调用 wait/waitpid
,来进行对子进程进行状态检测与回收的功能。
1. wait
方法
在 Linux 系统中,wait
方法是用于等待子进程结束的系统调用或库函数。其功能是让父进程挂起,直到一个子进程终止,并返回子进程的终止状态。
1. 功能描述
wait()
方法会暂停父进程的执行,直到任意一个子进程结束。- 当子进程终止时,父进程会从子进程处获取其退出状态。
- 如果没有子进程,
wait()
会立即返回-1
,并设置错误信息为ECHILD
。
2. 原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
3. 参数解释
- status:
- 一个指针,指向存储子进程退出状态的整数。
- 父进程可以通过宏(如
WIFEXITED
、WEXITSTATUS
等)解析该状态。
4. 返回值
- 返回终止的子进程的 PID(进程 ID)。
- 如果出错,返回
-1
,并设置errno
。
5. 父进程只等待一个进程(阻塞式等待)
以下是一个使用 wait()
的简单示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0);
}
else
{
int cnt = 10;
// parent
while(cnt)
{
printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
int ret = wait(NULL);
if(ret == id)
{
printf("wait success!\n, ret:%d\n", ret);
}
sleep(5);
}
return 0;
}
注意:前五秒父子进程同时运行,紧接着子进程退出变成僵尸状态,五秒钟后父进程对子进程进行了等待,成功将子进程释放掉,最后再五秒钟后父进程也退出,整个程序执行结束。
6. 父进程等待多个子进程(阻塞式等待)
一个 wait
只能等待任意一个子进程,因此父进程如果要等待多个子进程可以通过循环来多次调用 wait
实现等待多个子进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 5
// 父进程等待多个子进程
void RunChild()
{
int cnt = 5;
while(cnt--)
{
printf("I am child, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
return;
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();// 创建一批子进程
if(id == 0)
{
// 子进程
RunChild();
exit(0);
}
// 父进程
printf("Creat process sucess:%d\n", id);
}
sleep(10);
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("Wait process:%d, success!\n", id);
}
}
sleep(5);
return 0;
}
2. waitpid
方法
waitpid
是 wait
的增强版本,它可以指定等待特定的子进程终止,同时支持非阻塞等待。相比 wait
,waitpid
提供了更大的灵活性,适用于复杂的进程管理场景。
1. 函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
2. 参数说明
pid
:- 用于指定需要等待的子进程。
- 各种取值的含义:
pid > 0
:等待进程 ID 为pid
的子进程。pid == 0
:等待与调用进程属于同一进程组的任何子进程。pid < -1
:等待进程组 ID 为|pid|
(绝对值)的所有子进程。pid == -1
:等待任意子进程(等效于wait()
)。
status
:- 指向一个整数,用于存储子进程的退出状态。
- 可以通过一系列宏来解析
status
的值(见 状态解析宏 部分)。
options
:- 控制
waitpid
的行为。 - 常见取值:
WNOHANG
:非阻塞等待。如果没有子进程终止,立即返回0
。WUNTRACED
:如果子进程由于信号暂停(但未终止),则返回。WCONTINUED
:如果子进程接收到SIGCONT
信号恢复运行,则返回。
- 控制
3. 返回值
- 成功:
- 返回终止的子进程的 PID。
- 如果使用
WNOHANG
且没有子进程终止,则返回0
。
- 失败:
- 返回 -1,并设置 errno。常见错误包括:
ECHILD
:没有符合条件的子进程。EINTR
:调用被信号中断。
- 返回 -1,并设置 errno。常见错误包括:
4. 状态解析宏
子进程退出状态存储在 status
参数中,可以通过以下宏解析:
WIFEXITED(status)
:- 返回非零值,表示子进程正常终止。
WEXITSTATUS(status)
:- 返回子进程的退出码(仅当
WIFEXITED
为真时)。
- 返回子进程的退出码(仅当
5. 获取子进程的退出信息(阻塞式等待)
进程有三种退出场景,父进程等待希望获得子进程退出的以下信息:子进程代码是否异常?没有异常,结果对嘛?不对是因为什么呢? 子进程这些所有的退出信息都被保存在 status
参数里面。
wait
和waitpid
都有一个status
参数,该参数是一个输出型参数,由操作系统填充。如果传递
NULL
,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status
不能简单的当做整型来看待,例如 int 型总共占 32 位, 则具体细节需要关注位,如下图:
注意:操作系统没有0号信号,因此,如果低七位是0说明子进程没有收到任何信号。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5, a = 10;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
a /= 0; // 故意制造一个异常
}
exit(11); // 将退出码故意设置成11
}
else
{
// parent
int cnt = 10;
while(cnt)
{
printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
// 目前为止,进程等待是必须的!
//int ret = wait(NULL);
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
// 获取子进程退出状态信息的关键代码
// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00
printf("wait success! exit signal:%d, exit code:%d!\n", status&0X7F, (status >> 8)&0XFF);
}
sleep(5);
}
return 0;
}
在代码运行结果中,我们可以看到以下几点:
- 子进程在运行过程中,执行了
a /= 0;
的除零操作,触发了SIGFPE
信号(信号编号为 8),导致子进程异常终止。 - 子进程未能正常执行完
exit(11)
,因此我们设置的退出码11
并没有生效。 - 子进程的退出码显示为
0
,这是因为进程收到信号并被异常终止时,退出码是不可信的。
6. 一般的进程等待代码
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00
//printf("wait success! exit signal:%d, exit code:%d!\n", status&0X7F, (status >> 8)&0XFF);
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码是:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程被异常终止!\n");
}
}
3. wait
和 waitpid
的本质
wait
和 waitpid
的核心工作是:
- 检查子进程状态:
- 操作系统会检查子进程是否已经处于 僵尸状态(
Z
状态)。
- 操作系统会检查子进程是否已经处于 僵尸状态(
- 读取子进程的退出状态:
- 如果子进程已经终止,操作系统会从其 PCB 中读取退出状态(包括信号和退出码)。
- 释放子进程的 PCB:
- 回收子进程的 PCB 资源,将其从僵尸状态变为完全销毁的状态。
- 将状态信息返回给父进程:
- 父进程通过
status
获取子进程的退出状态信息。
- 父进程通过
3.3 非阻塞轮询等待
非阻塞轮询等待的核心思想是,父进程在等待子进程的同时,继续执行其他任务,而不是一直阻塞在等待操作上。通过 waitpid
的 WNOHANG
选项,可以实现非阻塞的轮询等待。
1. 使用场景
- 父进程需要同时执行其他任务,并且不希望因为等待子进程而被阻塞。
- 多任务处理时,比如监听其他事件或继续其他逻辑的执行。
2. 关键点
使用
waitpid
的WNOHANG
选项:如果有子进程已退出,则返回该子进程的 PID。
如果没有子进程退出,则立即返回
0
。如果发生错误,则返回
-1
并设置errno
。
父进程通过轮询检查子进程的状态。
在轮询中加入适当的延迟(如
sleep
),以避免占用过多 CPU。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
// 父进程只等待一个子进程(非阻塞轮询等待)
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(11);
}
else
{
// parent
// 目前为止,进程等待是必须的!
while(1)
{
int status = 0;
int ret = waitpid(id, &status, WNOHANG);
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码是:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程被异常终止!\n");
}
break;
}
else if(ret == 0)
{
// 父进程的任务可以写在这里
printf("child process is running...\n");
}
else
{
printf("等待出错!\n");
}
sleep(1);
}
sleep(2);
}
return 0;
}
注意:进程等待在一定程度上确保了父进程一定是最后一个退出的,这样可以避免子进程变为僵尸进程,进而导致内存泄露的问题。
四、程序替换
4.1 单进程版程序替换
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("befor: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
// exec类函数的标准写法
// execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execl("/usr/bin/top", "top", NULL);
printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
return 0;
}
输出示例:
4.2 程序替换的基本原理
函数原型:
int execl(const char *path, const char *arg0, ..., NULL);
execl
是一个可变参数函数,用于执行程序替换操作。- 它的第一个参数是可执行程序的路径(
path
)。 - 后续参数是传递给该程序的命令行参数(
argv
),并以NULL
结尾。
各个参数意义:
/bin/ls
:可执行程序的绝对路径,这是需要加载的新程序。"ls"
:新程序的第一个命令行参数(argv[0]
),通常用作程序名(与 shell 中运行ls
类似)。NULL
:表示参数列表结束,这是可变参数函数的结束标志。
程序替换行为:
- 当前进程的代码段、数据段等被新程序(
ls
)替换。 - 当前进程的 PID 保持不变,但其执行的内容变为
/bin/ls
。 - 此处,
ls
是 Linux 中的一个命令行工具,用于列出目录中的文件和目录。
注意点:
- 如果
execl
成功执行,新程序的代码替换当前程序,后续代码(如printf
)不会执行。 - 如果
execl
执行失败(例如路径不存在或权限不足),execl
会返回-1
,并设置errno
。通常在失败时用perror
或strerror(errno)
打印错误原因。
4.3 程序替换七大接口
在 Linux 中,程序替换操作(Program Replacement)是通过 exec
系列接口 实现的。这些接口将当前进程的执行内容替换为新程序,并保留原有进程的 PID。以下是 exec
系列的七个接口及其用法。
1. execl
(上文已经解释,这里不再过多赘述。)
2. execlp
int execlp(const char *file, const char *arg0, ..., NULL);
描述:与
execl
类似,但会在环境变量PATH
中搜索file
,而无需指定绝对路径。参数:
file
: 可执行程序的名称(会从PATH
中查找)。- 其他参数与
execl
一致。
示例:
execlp("ls", "ls", "-l", "-a", NULL);
- 替换当前进程为
ls
程序,execlp
会自动在/bin
等目录中查找ls
。
- 替换当前进程为
3. execle
int execle(const char *path, const char *arg0, ..., NULL, char *const envp[]);
描述:与
execl
类似,但允许显式指定新的环境变量envp
。参数:
path
: 新程序的文件路径。arg0
: 通常为程序名(argv[0]
)。..., NULL
: 命令行参数。envp[]
: 指定的环境变量数组(如{"KEY=VALUE", NULL}
)。
示例:
char *envp[] = {"MY_ENV=HelloWorld", NULL}; execle("/usr/bin/env", "env", NULL, envp);
- 替换当前进程为
env
程序,并设置环境变量MY_ENV=HelloWorld
。
- 替换当前进程为
4. execv
int execv(const char *path, char *const argv[]);
描述:与
execl
类似,但通过数组传递命令行参数。参数:
path
: 新程序的文件路径。argv[]
: 参数数组,argv[0]
通常是程序名,最后一项必须为NULL
。
示例:
char *argv[] = {"ls", "-l", "-a", NULL}; execv("/bin/ls", argv);
- 替换当前进程为
/bin/ls
程序,使用参数数组。
- 替换当前进程为
5. execvp
int execvp(const char *file, char *const argv[]);
描述:与
execv
类似,但会在环境变量PATH
中搜索程序。参数:
file
: 程序名(会从PATH
环境变量中查找)。argv[]
: 参数数组。
示例:
char *argv[] = {"ls", "-l", "-a", NULL}; execvp("ls", argv);
- 替换当前进程为
ls
程序,execvp
会从PATH
中查找ls
。
- 替换当前进程为
6. execve
int execve(const char *path, char *const argv[], char *const envp[]);
描述:是所有
exec
系列函数的底层系统调用,直接调用内核执行程序替换。参数:
path
: 新程序的文件路径。argv[]
: 参数数组。envp[]
: 环境变量数组。
示例:
char *argv[] = {"ls", "-l", "-a", NULL}; char *envp[] = {"MY_ENV=HelloWorld", NULL}; execve("/bin/ls", argv, envp);
- 替换当前进程为
/bin/ls
程序,并显式传递参数和环境变量。
- 替换当前进程为
7. execvpe
int execvpe(const char *file, char *const argv[], char *const envp[]);
描述:扩展接口,结合了
execvp
和显式的环境变量指定功能。参数:
file
: 程序名(会从PATH
环境变量中查找)。argv[]
: 参数数组。envp[]
: 环境变量数组。
注意:此函数并非 POSIX 标准,某些系统可能不支持。
示例:
char *argv[] = {"ls", "-l", "-a", NULL}; char *envp[] = {"MY_ENV=HelloWorld", NULL}; execvpe("ls", argv, envp);
总结:七大接口对比
接口 | 参数传递方式 | 是否使用 PATH |
是否支持显式 envp |
---|---|---|---|
execl |
可变参数列表 | 否 | 否 |
execlp |
可变参数列表 | 是 | 否 |
execle |
可变参数列表 + envp |
否 | 是 |
execv |
参数数组 | 否 | 否 |
execvp |
参数数组 | 是 | 否 |
execve |
参数数组 + envp |
否 | 是 |
execvpe |
参数数组 + envp |
是 | 是 |
注意点:
替换后的进程依然会继承原父进程的环境变量。
在使用第三个参数
envp
的时候,要注意此时新替换的进程将会覆盖原来父进程的环境变量。七大接口均是
exec*
的形式,后缀中l(list)
表示列表,v(vector)
表示数组,p
表示是否使用环境变量PATH
,e
表示是否支持显式envp
。
4.4 练习:自定义命令行参数和环境变量进行程序替换
1. mycommand.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[], char* env[])
{
pid_t id = fork();
if(id == 0)
{
// child
printf("before: I am process, pid: %d, ppid: %d\n", getpid(), getppid());
// 自定义命令行参数
char *const myargv[] = {"otherExe", "-a", "-b", "-c", NULL};
// 自定义环境变量
char *const myenv[] = {
"MY_YAL=123456",
"MY_NAME=lesson17",
NULL
};
execve("./otherExe", myargv, myenv);
printf("after: I am process, pid: %d, ppid: %d\n", getpid(), getppid());
exit(1);
}
// father
pid_t ret = waitpid(id, NULL, 0);
if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
return 0;
}
2. otherExe.cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
int main(int argc, char* argv[], char* env[])
{
cout << "这是命令行参数" << endl;
for(int i = 0; argv[i]; i++)
{
cout << i << " : " << argv[i] << endl;
}
cout << "这是环境变量" << endl;
for(int j = 0; env[j]; j++)
{
cout << j << " : " << env[j] << endl;
}
return 0;
}
3. makefile
.PHONY: all
all: otherExe mycommand
mycommand: mycommand.c
gcc -o $@ $^ -std=c99
otherExe: otherExe.cpp
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f mycommand otherExe
不难看出,环境变量已经被覆盖了。
结语
至此,我们完成了对 Linux 进程从创建到替换全流程的深入探讨。从fork函数的神奇复制,到进程终止时的各种场景与退出方式;从进程等待对资源回收和状态获取的重要性,到程序替换实现进程功能蜕变的原理与多样接口,每一个环节都展现了 Linux 进程管理的精妙之处。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!