Linux进程管理
Linux进程管理
进程的定义与特征
进程的定义
程序:是静态的,就是个存放在磁盘里的可执行文件,就是一系列的指令集合。
进程:是动态的,是程序的一次执行过程。
进程的组成--PCB
- 一个程序文件
program
,只是一堆待执行的代码和部分待处理的数据,他们只有被加载到内存中,然后让CPU逐条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程process
,因此进程是一个动态变化的过程,而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本。
当进程被创建时,操作系统会为该进程分配一个** 唯一的 ** , ** 不重复 ** 的“身份证号”--PID。
- 进程标识符(process identifier)
用于区分系统中的其他进程,这个PID也是Linux内核提供给用户访问进程的一个接口,用户可以通过PID控制进程。
- PCB --
ProcessControl Block
ELF格式的程序被执行时,内核中实际上产生了一个叫
task_struct{}
的结构 体来表示这个进程。进程是一个“活动的实体”,这个活动的实体从一开始诞生就需要各种 各样的资源以便于生存下去,比如内存资源、CPU资源、文件、信号、各种锁资源等,所 有这些东西都是动态变化的,这些信息都被事无巨细地一一记录在结构体task_struct
之 中,所以这个结构体也常常被成为进程控制块(PCB,即Process Control Block
)。该结构体被定义在一个名称叫做sched.h
的头文件中。PCB是进程存在的唯一标志
-
进程是操作系统分配资源的基本单位!
-
操作系统是以进程为单位来分配系统资源的,比如内存空间、CPU使用权等。
-
线程是操作系统调度资源的最小单位!
-
进程包含线程!
- PCB是给操作系统用的。
- 程序段和数据段是给进程自己用的。
进程的特征
- 并发性:并发性指的是多个进程实体可以同时存在于内存中,并在一段时间内同时运行。这是进程的重要特征,也是操作系统的关键特性之一。例如,多个应用程序可以同时运行,共享计算机资源。
- 动态性:进程的实质是程序的一次执行过程。因此,进程是动态产生和动态消亡的。每个进程都有自己的生命周期,从创建到终止。
- 独立性:进程实体是一个独立运行、独立分配资源和独立接受调度的基本单位。每个进程都有自己的内存空间、寄存器和其他资源。这使得进程之间相互隔离,不会相互干扰。
- 异步性:进程按照各自独立的、不可预知的速度向前推进。这意味着不同进程之间的执行是异步的,它们不会严格按照某个固定的顺序运行。例如,一个进程可能在等待用户输入时暂停,而另一个进程继续执行
Linux 系统查看进程pid
的shell命令
ps -ef
ps -aux
查看所有用户的相关进程的所有消息
进程的状态与转换
- 新建态(New):进程刚刚被创建,但还没有被操作系统调度执行。在这个状态下,操作系统会为进程分配必要的资源,例如内存空间和标识符。
- 就绪态(Ready):进程已经准备好运行,等待被操作系统调度到CPU上执行。在这个状态下,进程已经被添加到可执行进程队列中,但还没有获得CPU的使用权。
- 运行态(Running):进程正在被CPU执行。它占有处理器,其程序正在运行。在单处理机系统中,只有一个进程处于执行状态;在多处理机系统中,可能有多个进程处于执行状态。
- 阻塞态(Blocked):进程因为某些原因(例如等待I/O操作完成、等待资源等)而暂时无法继续执行,被阻塞。在这个状态下,进程不会占用CPU资源,但会等待外部事件的发生。
- 可中断等待状态:进程可以被信号量或中断唤醒,一旦资源有效,进程会立即进入就绪状态。
- 不可中断等待状态:进程不能被信号量或中断唤醒,只有当它申请的资源有效时才能被唤醒。这种状态通常用于内核中某些场景,例如磁盘读写时的DMA操作。
- 终止态(Terminated):进程因某种原因而中止运行,占有的所有资源将被回收。
- 系统对其不再予以理睬,也称为“僵死状态”。进程则成为僵尸进程。
进程控制
进程控制是对系统中所有进程从创建、执行到撤销的全过程实行有效的管理和控制。
进程控制一般是由操作系统内核的相应程序(原语)来实现。通常,操作系统内核运行在系统态。
- 原语
原语是由若干条指令组成的,用于完成特定功能的,具有原子性(不可分割)的子程序。它与一般过程的区别:它们是原子操作(Action Operation)为保证操作的正确性,原语在执行期间是不可被中断的。因此,规定在执行原语操作时要屏蔽中断,以保证原语操作的不可分割性。
用于进程控制过程中的原语有:
- 创建原语(Create)、撤销原语(Termination)
- 阻塞原语(Block)、 唤醒原语(Wakeup)
- 挂起原语(Suspend)、 激活原语(Active)
进程的创建
fork
函数名称 | fork() |
---|---|
头文件 | #include <unistd.h> |
原型 | pid_t fork(void); |
返回值 | - 成功:在父进程中返回大于0的正整数,表示子进程的PID。 - 成功:在子进程中返回0。 - 失败:返回-1,表示创建子进程失败。 |
备注 | - fork() 执行成功后,会生成一个新的子进程。- 在新的子进程中, fork() 返回值为0。- 在原来的父进程中, fork() 返回值为大于0的正整数,即子进程的PID。 |
-
fork()
函数的作用:fork()
会使得进程本身被复制,类似细胞分裂。因此,被创建出来的子进程和父进程几乎是一模一样的。但需要注意的是,子进程并不是100%父进程的复印件,具体关系如下:- 父子进程的以下属性在创建之初完全一样,子进程相当于搞了一份复制品:
- 实际 UID 和 GID,以及有效 UID 和 GID。
- 所有环境变量。
- 进程组 ID 和会话 ID。
- 当前工作路径(除非使用
chdir()
进行修改)。 - 打开的文件。
- 信号响应函数。
- 整个内存空间,包括栈、堆、数据段、代码段、标准 I/O 的缓冲区等等。
- 而以下属性,父子进程是不一样的:
- 进程号 PID。PID 是身份证号码,哪怕亲如父子,也要区分开。
- 记录锁。如果父进程对某文件加了锁,子进程不会继承这把锁。
- 挂起的信号。这些信号是所谓的“悬而未决”的信号,等待着进程的响应,子进程也不会继承这些信号。
- 父子进程的以下属性在创建之初完全一样,子进程相当于搞了一份复制品:
-
子进程的执行:
- 子进程会从
fork()
返回值后的下一条逻辑语句开始运行。这样就避免了不断调用fork()
而产生无限子孙的悖论。
- 子进程会从
-
父子进程的关系:
- 父子进程是相互平等的:他们的执行次序是随机的,或者说他们是并发运行的,除非使用特殊机制来同步他们,否则你不能判断他们的运行究竟谁先谁后。
- 父子进程是相互独立的:由于子进程完整地复制了父进程的内存空间,因此从内存空间的角度看,他们是相互独立、互不影响的。
- 获取当前进程PID的函数接口
getpid()
,获取当前进程的父进程PID的函数接口是getppid()
示例:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(int argc, char const *argv[]) { int fd = 3; // 父进程的数据段 //创建子进程,调用fork函数的时候就已经创建子进程 pid_t child_pid = fork(); //通过fork函数的返回值分析父进程 or 子进程 if (child_pid > 0) { //说明是父进程的进程空间 printf("my is parent,my pid = %d,my child pid = %d\n",getpid(),child_pid); } else if( child_pid == 0) { //说明是子进程的进程空间 printf("my is child,my pid = %d,my parent pid = %d\n",getpid(),getppid()); } else { printf("child process fork error\n"); return -1; } return 0; }
由于父子进程的并发性,以上程序的执行效果是不一定的。
进程的撤销
wait
功能 | 等待子进程结束 |
---|---|
头文件 | #include <sys/wait.h> |
原型 | pid_t wait(int *stat_loc); pid_t waitpid(pid_t pid, int *stat_loc, int options); |
参数 | - pid :- 小于-1:等待组ID的绝对值为pid的进程组中的任一子进程。 - -1:等待任一子进程。 - 0:等待调用者所在进程组中的任一子进程。 - 大于0:等待进程组ID为pid的子进程。 - stat_loc :子进程退出状态。- options :- WCONTINUED :报告任一从暂停态出来且从未报告过的子进程的状态。- WNOHANG :非阻塞等待。- WUNTRACED :报告任一当前处于暂停态且从未报告过的子进程的状态。 |
返回值 | - wait() :- 成功:退出的子进程PID。 - 失败:-1。 - waitpid() :- 成功:状态发生改变的子进程PID(如果 WNOHANG 被设置,且由pid指定的进程存在但状态尚未发生改变,则返回0)。- 失败:-1。 |
备注 | 如果不需要获取子进程的退出状态,stat_loc 可以设置为NULL 。 |
所谓的退出状态不是退出值,退出状态包括了退出值。如果使用以上两个函数成功获取了子进程的退出状态,则可以使用以下宏来进一步解析
宏 | 含义 |
---|---|
WIFEXITED(status) |
如果子进程正常退出,则该宏为真。 |
WEXITSTATUS(status) |
如果子进程正常退出,则该宏将获取子进程的退出值。 |
WIFSIGNALED(status) |
如果子进程被信号杀死,则该宏为真。 |
WTERMSIG(status) |
如果子进程被信号杀死,则该宏将获取导致他死亡的信号值。 |
WCOREDUMP(status) |
如果子进程被信号杀死且生成核心转储文件(coredump ),则该宏为真。 |
WIFSTOPPED(status) |
如果子进程的被信号暂停,且option中WUNTRACED 已经被设置时,则该宏为真。 |
WSTOPSIG(status) |
如果WIFSTOPPED(status) 为真,则该宏将获取导致子进程暂停的信号值。 |
WIFCONTINUED(status) |
如果子进程被信号SIGCONT 重新置为就绪态,该宏为真。 |
status是一个出参,由操作系统为其赋值,用户可以传递NULL值表示不关心,而如果传入参数,操作系统就会根据该参数,将子进程的退出信息反馈给父进程,由status最终被赋予的值来体现。 |
可以看出,不论是正常退出还是异常退出,status的高8个比特位(只讨论低16个比特位)都表示子进程的退出码,而这个退出码一般是return的返回值或者exit的参数;正常退出时,status的低8个比特位为全0;而异常退出时,其第8个比特位则为core dump标志位,用来标志是否会有core dump文件产生,而低7个比特位则是退出信号。
可以通过位运算判断是否正常退出,是否产生core dump文件。
示例:
child_elf.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
printf("[%d]:yep,I am the child\n", (int)getpid());
exit(0);
}
wait.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/wait.h> // 包含 wait 函数的声明
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main(int argc, char** argv) {
pid_t x = fork();
if (x == 0) { // 子进程,执行指定程序 child_elf
execl("./child_elf", "child_elf", NULL);
}
if (x > 0) { // 父进程,使用 wait() 阻塞等待子进程的退出
int status;
wait(&status);
if (WIFEXITED(status)) { // 判断子进程是否正常退出
printf("child exit normally, exit value: %hhu\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) { // 判断子进程是否被信号杀死
printf("child killed by signal: %u\n", WTERMSIG(status));
}
}
return 0;
}
父进程使用 wait()
阻塞等待子进程的退出状态。如果子进程正常退出,我们获取其退出值;如果子进程被信号杀死,我们获取导致其死亡的信号值。
进程的执行
也可以叫程序的替换。
进程程序替换与fork不同,它并不会创建新的进程,而是该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。替换前后的进程号并未改变。
exec函数簇
功能 | 在进程中加载新的程序文件或者脚本,覆盖原有代码,重新运行 |
---|---|
头文件 | <unistd.h> |
原型 | int execl(const char *path, const char *arg, ...); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, ..., char *const envp[]); int execlp(const char *file, const char *arg, ...); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]); |
参数 | - path :即将被加载执行的ELF文件或脚本的路径- file :即将被加载执行的ELF文件或脚本的名字- arg :以列表方式罗列的ELF文件或脚本的参数- argv :以数组方式组织的ELF文件或脚本的参数- envp :用户自定义的环境变量数组 |
返回值 | 成功不返回 失败返回 -1 |
备注 | 1. 函数名带字母 l 意味着其参数以列表(list)的方式提供。2. 函数名带字母 v 意味着其参数以矢量(vector)数组的方式提供。3. 函数名带字母 p 意味着会利用环境变量 PATH 来找寻指定的执行文件。4. 函数名带字母 e 意味着用户提供自定义的环境变量。 |
system
功能 | 在子进程中执行一个 shell 命令,然后返回 |
---|---|
头文件 | <stdlib.h> |
原型 | int system(const char *command); |
参数 | - command :要执行的 shell 命令字符串。如果 command 为 NULL ,则返回 shell 是否存在的状态 |
返回值 | - 如果 command 为 NULL :- 返回 0:没有可用的 shell - 返回 非 0 值:有可用的 shell - 如果 command 不为 NULL :- 成功:返回 shell 的退出状态码 - 失败:返回 -1,并设置 errno |
备注 | 1. system 函数使用 /bin/sh 来执行命令。2. 在调用 system 之前,会忽略 SIGINT 和 SIGQUIT 信号,并且阻塞 SIGCHLD 信号。3. 如果在 system 调用期间捕获到信号,shell 的退出状态可能会受到影响。 |
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
// 创建子进程,调用fork函数的时候就已经创建子进程
pid_t child_pid = fork();
// 通过fork函数的返回值分析父进程或子进程
if (child_pid > 0) {
// 父进程的进程空间
printf("my is parent, my pid = %d, my child pid = %d\n", getpid(), child_pid);
wait(NULL); // 会阻塞,当第一个子进程状态改变时,该函数可以解除阻塞
} else if (child_pid == 0) {
// 子进程的进程空间
printf("my is child, my pid = %d, my parent pid = %d\n", getpid(), getppid());
// 打算让子进程加载新的代码段和数据段,也就是让子进程执行新的可执行文件
//execl("./demo","demo",NULL);
system("./demo"); // 使用system调用执行新的可执行文件 demo
} else {
// fork失败
printf("child process fork error\n");
return -1;
}
return 0;
}
进程的终止
_exit exit
功能 | 退出本进程 |
---|---|
头文件 | <unistd.h> <stdlib.h> |
原型 | void _exit(int status); void exit(int status); |
参数 | status :子进程的退出值- 如果子进程正常退出,则 status 一般为0。- 如果子进程异常退出,则 status 一般为非0。 |
返回值 | 不返回 |
备注 | - exit() 函数退出时会自动冲洗(flush)标准IO缓冲区的残留数据到内核,如果进程注册了退出处理函数,还会自动执行这些函数。- _exit() 函数直接退出,不执行任何清理操作。- 除了\n(换行)以及exit()函数会刷新缓冲区之外,也可以调用 fflush() 来强制刷新缓冲区 |
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int status;
printf("父进程开始执行。\n");
// 创建子进程
pid_t child_pid = fork();
if (child_pid == -1) {
// 创建子进程失败
perror("fork");
return 1;
} else if (child_pid == 0) {
// 子进程
printf("子进程正在执行,PID:%d\n", getpid());
// 模拟一个错误条件
int result = 5 / 0; // 这会导致除以零异常
// 检查除法是否成功
if (result != 0) {
// 如果发生错误,以状态码 1 退出
printf("子进程遇到错误。\n");
exit(1);
} else {
// 如果成功,以状态码 0 退出
printf("子进程成功执行。\n");
exit(0);
}
} else {
// 父进程
printf("父进程等待子进程结束,子进程PID:%d\n", child_pid);
wait(&status); // 等待子进程结束
if (WIFEXITED(status)) {
printf("子进程以状态码 %d 正常退出。\n", WEXITSTATUS(status));
} else {
printf("子进程未正常退出。\n");
}
printf("父进程结束。\n");
}
return 0;
}
错误处理 (补充)
fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
fprintf
: 这个函数用于向流中写入格式化的输出。在这个例子中,它将格式化的消息写入标准错误流(stderr
)。"open user.txt fail, error = %d, %s\n"
: 这是格式字符串。它包含了将要插入字符串的占位符。具体来说:%d
是一个整数值的占位符(在这个例子中,是errno
的值)。%s
是一个字符串值的占位符(在这个例子中,是strerror(errno)
的结果)。
errno
: 这个全局变量保存了最后一个失败的系统调用的错误代码。当发生错误时,系统调用和库函数会设置这个变量。strerror(errno)
: 这个函数接受一个错误号(如errno
),并返回一个人类可读的描述错误的字符串。它将错误代码转换为有意义的错误消息。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[]) {
// 1. 打开文件
int file_fd = open("./user.txt", O_RDWR | O_CREAT);
if (file_fd == -1) {
fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
return -1;
}
// 2. 对文件进行写入
write(file_fd, "hello world", 11);
lseek(file_fd,0,SEEK_SET); //将光标偏移到开头
// 3. 从文本中读取数据
char recv_buf[11] = {0};
read(file_fd, recv_buf, 11);
printf("read from user.txt data is [%s]\n", recv_buf);
// 4. 关闭文件
close(file_fd);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)