进程是一个程序执行的过程,会去分配内存(mem),固态硬盘空间(ssd)和CPU等等;用来实现同一时刻多任务并发
进程控制块PCB
这是一个结构体,是操作系统中最重要的数据结构之一。PCB是进程存在的唯一标识,操作系统通过管理PCB来管理进程。
进程ID:唯一的身份标识。
进程状态:运行、就绪、阻塞等。
程序计数器:指向下一条要执行的指令的地址。
CPU寄存器:当进程被切换时,需要保存当前的寄存器状态,以便下次执行时能恢复。
内存管理信息:如基地址、界限地址等。
记账信息:使用的CPU时间、时间戳等。
I/O状态信息:分配给该进程的I/O设备、打开的文件列表等。
进程的内存分布(32位架构最大约3GB)
代码段/文本段(code)
存放程序本身的机器指令(即编译后的可执行代码),是从可执行文件中直接加载进来的。是只读且可共享的,同一程序的多个进程可以共享同一份代码段,节省内存。
数据段(data)
包含了全局变量和静态变量(包括全局静态变量和局部静态变量),在程序开始时分配,并一直存在直到程序结束。
内存映射段
进程虚拟地址空间中的一个区域,操作系统通过 mmap()
系统调用,将一个文件或者一段匿名内存直接映射到这个区域。
映射成功后,进程就可以像访问普通内存一样,使用指针来读写文件内容,而无需调用传统的 read()
和 write()
等I/O函数。操作系统会在幕后负责将内存中的修改写回磁盘文件。
堆(heap)
内存占用小于3G,用与在程序运行时动态申请内存(malloc),由程序员手动控制申请和释放,如果忘记释放,就会造成“内存泄漏”。
增长方向:向高地址增长。堆的当前边界由一个称为“程序中断点”的指针来标识。
栈(stack)
linux中默认内存占用8M(无限递归或定义非常大的局部变量会导致“栈溢出”),用于储存函数调用时的局部变量、参数和返回地址。每次函数调用都会在栈上创建一个新的“栈帧”
由编译器自动管理。函数调用时分配栈帧,函数返回时自动销毁该栈帧。
内核空间
在地址空间的最高部分,存放操作系统内核的代码和数据。这部分内存受保护,用户进程无法直接访问。当进程通过系统调用陷入内核态时,才会访问这部分空间。
进程的状态
创建
进程正在被创建。操作系统正在为其分配PCB(进程控制块)、建立地址空间、加载程序代码等。这是一个初始化的中间状态,尚未准备就绪。
当操作系统完成创建工作并有足够资源运行它时,状态变为就绪。
就绪
进程已获得了除CPU以外的所有必需资源。它已经加载到内存中,随时可以执行,只是在等待操作系统的调度器选中它。在新进程创建完成,运行中的进程时间片用完,阻塞的进程等待的事件发生了(如I/O操作完成)等情况下,进程就会处于就绪态。
当调度器选中它时,状态变为运行。
运行
进程正在CPU上执行其指令。
在单核CPU系统中,任何时候最多只有一个进程处于运行状态。
如果下一步进程执行完毕,变为终止;如果进程需要等待某个事件(如用户输入,读取文件),它会主动让出CPU,变为阻塞;如果操作系统音某些原因停止进程占用CPU(通常因为用完了分配的时间片),变为就绪。
阻塞(等待)
进程因等待某个外部事件而主动暂停执行。即使CPU空闲,它也无法运行。如等待用户输入、等待磁盘读写、等待网络传输、等待其他进程发送信号。
当它所等待的事件发生后,状态变为就绪。
终止(退出)
进程已经结束执行(无论是正常退出还是被强制杀死)。操作系统会开始回收其占用的资源(内存、文件句柄、PCB等)。如进程执行完 exit()
系统调用、被其他进程发送信号强制终止或出现严重错误和异常。
PCB中可能仍保留一些退出状态码供父进程查询。这是一个临时的最终状态。
重要状态转换
调度:就绪 -> 运行
这是操作系统的调度器负责的工作,它根据特定的算法(如时间片轮转、优先级)从就绪队列中选择一个进程来执行。
时间片用完:运行 -> 就绪
这是抢占式多任务系统的核心机制。每个进程被分配一个很短的时间片(如几毫秒),用完后就被剥夺CPU,放回就绪队列,以保证所有进程都能公平地得到执行。
等待事件:运行 -> 阻塞
这是进程主动的行为,例如调用 sleep()
, read()
, wait()
等函数。
事件发生:阻塞 -> 就绪
当外部资源就绪(如I/O完成),操作系统会得到通知,随后将对应的进程从阻塞队列移动到就绪队列,等待再次被调度。
进程相关的命令
查看进程状态
ps
最基础的进程查看命令,参数风格多样
ps aux:查看所有用户的所有进程详细信息(USER, PID, %CPU, %MEM, COMMAND 等)
ps -ef --forest:以层级结构显示进程,可以看出父子关系。
ps -u username:查看特定用户的进程。
top
实时动态显示系统进程信息。显示信息包括系统负载、CPU/内存使用率、每个进程的资源占用等。按q退出,按k可以杀死进程。
控制进程(发送信号)
kill (-n)
向指定 PID 的进程发送信号,请求进程终止,默认信号是15(正常终止)。
常用信号:
-1:挂起(SIGHUP),让进程重新读取配置文件;
-2:中断(SIGINT),相当于在终端按ctrl+c;
-9:强制终止(SIGKILL),进程无法捕获或忽略,直接由系统终止,可能导致数据丢失;
-15:正常终止(SIGTERM),默认信号;
-18:继续运行(SIGCONT),与SIGSTOP对应;
-19:暂停运行(SIGSTOP),相当于在终端按ctrl+z;
示例:
kill 1234:请求 PID 为 1234 的进程终止。
kill -9 1234:强制杀死 PID 为 1234 的进程。
kill -HUP 4567:让 PID 为 4567 的进程(如 Nginx)重新加载配置。
pkill
根据进程名或其他属性来发送信号
示例:
pkill firefox:杀死所有名为 firefox
的进程。
pkill -u username:杀死某用户的所有进程。
killall
与pkill类似,也是根据进程名来操作,如killall chrome
僵尸进程和孤儿进程
当一个进程终止时,它并不会立刻从系统中完全消失。内核会保留一些信息(主要是进程描述符PCB),直到其父进程读取它的退出状态。
特性 |
僵尸进程 |
孤儿进程 |
---|---|---|
本质 |
已终止但未被回收的进程 |
仍在运行但父进程已死的进程 |
产生原因 |
父进程未调用 |
父进程先于子进程退出 |
对系统的影响 |
占用进程ID,过多会导致问题 |
无害,是正常运行的进程 |
系统处理 |
无自动处理,依赖父进程 |
自动被 init 进程收养 |
如何清除 |
让父进程调用 |
无需干预,会正常结束并被init清理 |
在PS中的状态 |
|
|
可否用 |
否(已是死的) |
可以(是活的进程) |
Init是最终的守护者:它确保了所有进程最终都能被妥善清理,是防止系统进程资源泄漏的最后一道防线。
僵尸进程: 执行完毕或终止,但其退出状态没有被父进程读取的进程
在Linux中,进程终止时,内核会释放其大部分资源(内存、文件描述符等),但会保留一个最小数据集(包括进程ID、退出状态、CPU时间等)保存在进程表项中。这样,父进程以后可以通过 wait()或 waitpid()系统调用来获取这些信息。如果父进程一直没有调用wait(),这个已死的子进程就会一直占据着一个进程表项,成为一个僵尸进程。
僵尸进程已经是“死”的,它不占用CPU和内存,但占用着一个进程ID。无法用 kill -9 杀死一个已经死亡的进程。如果系统中存在大量僵尸进程,可能会耗尽可用的进程ID,导致无法创建新进程。在ps或top命令中,其状态显示为z(或者z+表示是前台进程组的一部分),“defunct”也是其标志。
僵尸进程是bug:它意味着程序逻辑有缺陷(父进程没有履行等待子进程的责任),需要避免。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process (PID: %d) is running.\n", getpid());
printf("Child process is exiting now.\n");
exit(0); // 子进程正常退出,成为僵尸
} else {
// 父进程
printf("Parent process (PID: %d) created a child (PID: %d).\n", getpid(), pid);
printf("Parent is going to sleep for 30 seconds and will NOT wait for the child.\n");
sleep(30); // 在这30秒内,子进程是僵尸状态
printf("Parent waking up. The program ends, and the zombie is finally reaped.\n");
// 父进程退出后,init会接管并清理僵尸子进程
}
return 0;
}
解决方法:
编写代码时:父进程必须调用 wait()或waitpid()来等待子进程结束并回收其资源。
已存在僵尸进程:如果父进程还活着,发送SIGCHLD信号给父进程,使其调用 wait()。如果父进程已经异常,杀死父进程。父进程死后,它的所有僵尸子进程会被 init进程(PID 1) 收养,init会定期调用 wait(),从而彻底清理这些僵尸进程。
孤儿进程:父进程已经终止或退出,但自身仍在运行的子进程
父进程先于子进程结束(如父进程崩溃或显式退出没有等待子进程)
当一个进程成为孤儿时,内核会立即将它过继给 init 进程(PID 1),成为 init 进程的子进程。Init 进程是系统所有进程的祖先。它会定期调用 wait()系统调用,来清理其下任何终止的子进程(包括这些被过继来的孤儿进程)。因此,孤儿进程本身不会变成僵尸进程,因为 init 会确保回收它们。所以孤儿进程只是正常运行的进程,不会对系统造成任何危害,最终会正常结束并被 init 清理。
孤儿进程是特性:它是系统设计的自然结果,并且系统有完善的机制(init收养)来自动处理,不会造成问题。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process (PID: %d) started. My parent is (PID: %d).\n", getpid(), getppid());
printf("Child is going to sleep for 5 seconds...\n");
sleep(5); // 在此期间,父进程会先退出
printf("Child woke up. Now my parent is (PID: %d).\n", getppid()); // 此时父进程已是 init (1)
printf("Child exiting.\n");
} else {
// 父进程
printf("Parent process (PID: %d) created a child (PID: %d).\n", getpid(), pid);
printf("Parent is exiting NOW, making the child an orphan.\n");
exit(0); // 父进程立即退出
}
return 0;
}
系统调用函数
创建新进程:fork()
pid_t fork(void);
用于创建一个新的进程。这个新进程是调用进程(父进程)的一个几乎完全相同的副本。子进程会从父进程那里继承许多属性,包括代码段、数据段、堆、栈的副本;环境变量;已打开的文件描述符(这意味着父子进程可以操作同一个打开的文件);信号处理设置;进程组ID和会话ID。
写时复制:
在调用fork()的瞬间,子进程并不会立即复制父进程的全部物理内存,相反,父子进程共享相同的物理内存页。只有当其中任何一个进程尝试修改某块内存时,操作系统才会为子进程复制那块特定的内存页。这极大地提高了效率,避免了不必要的内存拷贝。
返回值:调用一次,返回两次
在父进程中,fork()返回新创建子进程的 进程ID。在子进程中,fork()返回 0。如果创建失败(例如系统资源不足),则在父进程中返回 -1,并设置相应的error。
不同之处
子进程和父进程的ID不同,fork()返回值不同,子进程的挂起信号集被清零,子进程的累计CPU时间被重置为0。
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
//声明一个变量用于存储fork的返回值,pid_t是一个专门用于表示PID的数据类型,本质通常是一个整数。
pid_t pid;
//打印当前进程PID(父进程)
printf("Before fork,PID:%d\n",getpid());
//创建子进程
pid=fork();
//pid<0,创建失败
if(pid<0)
{
perror("error");
return 1;
}
//pid=0,创建成功,此分支只有子进程会进入,打印子进程和父进程的pid
else if (0==pid)
{
printf("Child process PID:%d,Father process PID:%d\n",getpid(),getppid());
}
//pid>0此分支只有父进程会进入,打印父进程和子进程的pid
else
{
printf("Father process PID:%d,Child process PID:%d\n",getppid(),pid);
}
//公用代码区域,此处代码父子进程都会执行,分别打印自己的pid
printf("This line is printed by both processes (PID: %d)\n", getpid());
return 0;
}
获取进程标识:getpid()/getppid()
pid_t getpid(void);
返回当前调用进程的进程ID,每个进程都有一个唯一的正整数作为其PID。
pid_t getppid(void);
返回当前调用进程的父进程的进程ID,如果父进程终止了,子进程会被 init 进程(PID = 1)收养,此时 getppid()将返回 1。
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 子进程:执行一个耗时计算或外部程序
printf("Child (PID: %d) is starting work...\n", getpid());
sleep(2); // 模拟工作耗时
printf("Child (PID: %d) work done!\n", getpid());
_exit(0); // 子进程退出
} else {
// 父进程:等待子进程结束
printf("Parent (PID: %d) is waiting for child (PID: %d)...\n", getpid(), pid);
wait(NULL); // 阻塞,直到一个子进程结束
printf("Parent: My child has finished.\n");
}
return 0;
}
终止调用:exit()与_exit()
void exit(int status);
用于正常终止调用它的进程,status是返回给父进程的退出状态码;exit(0)
表示正常退出,exit(1)
(或任何非零值)表示异常退出
void _exit(int status);
立即终止当前进程,同样0代表成功非0代表失败
特性 |
|
|
---|---|---|
本质 |
C标准库函数 |
Linux/Unix 系统调用 |
头文件 |
|
|
清理动作 |
执行所有用户层清理 |
立即终止,不进行任何用户层清理 |
缓冲区 |
刷新所有标准I/O缓冲区 |
不刷新缓冲区,可能导致数据丢失 |
主要用途 |
在大多数情况下正常终止程序 |
1. 子进程中终止 |
核心区别:exit()
会刷新缓冲区,_exit()
不会。
在 main()
函数中,使用 return
或 exit();
在任何其他函数中需要终止整个程序时,使用 exit();
在 fork()
创建的子孙进程中需要终止时,总是使用 _exit()
。
用法1:在子进程中终止,避免感染父进程的I/O缓冲区
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
printf("This message will be lost"); // 注意:末尾没有换号\n,数据在缓冲区
pid_t pid = fork();
if (pid == 0) {
// 子进程
// exit(0); // 如果使用这个,输出会被刷新,消息能打印出来
_exit(0); // 使用这个,不刷新缓冲区,消息丢失
} else {
// 父进程
wait(NULL);
exit(0); // 父进程退出时会刷新缓冲区
}
}
如果子进程使用 _exit(0),那么printf的消息因为还在缓冲区里没有被刷新到屏幕,就会随着进程的终止而丢失。这就是为什么在子进程中通常推荐使用 _exit()——为了避免重复刷新父进程已经持有的缓冲区。
用法2:处理严重错误后紧急退出
当程序检测到一种严重的、不可恢复的内部状态错误(比如内存结构被破坏)时,可能需要立即终止以避免执行更多的代码,从而造成更大的破坏(如写入错误数据到重要文件)。在这种情况下,跳过所有清理过程是更安全的选择。
清理回调函数:atexit()
int atexit(void (*function)(void));
注册一个函数,当程序通过exit()正常终止时,该函数会被自动调用,主要用于程序结束前的资源释放和状态保存等清理工作。
function是一个函数指针,指向一个没有参数、没有返回值的函数。
返回值:注册成功返回 0;失败返回非零值(例如注册的函数数量已达到系统限制)。
当程序调用exit或者由main函数执行return时,所有用atexit注册的退出函数,将会由注册时顺序倒序被调用;
#include <stdio.h>
#include <stdlib.h>
void cleanup1() {
printf("Performing cleanup 1...\n");
}
void cleanup2() {
printf("Performing cleanup 2...\n");
}
void cleanup3() {
printf("Performing cleanup 3...\n");
}
int main() {
// 注册退出处理函数
atexit(cleanup1);
atexit(cleanup2);
atexit(cleanup3); // 最后注册,最先执行
printf("Main function is starting...\n");
printf("Main function is ending...\n");
return 0; // return 触发 exit,从而执行 atexit 函数
}
//输出结果:
// Main function is starting...
// Main function is ending...
// Performing cleanup 3...
// Performing cleanup 2...
// Performing cleanup 1...
获取子进程状态和回收资源:wait()/waitpid()
特性 |
|
|
---|---|---|
等待目标 |
任意子进程 |
可指定特定子进程或进程组 |
阻塞行为 |
总是阻塞 |
可设置为非阻塞 ( |
灵活性 |
简单但功能有限 |
更精细的控制(如只等待暂停的子进程) |
常见用途 |
简单场景,只需等待一个子进程 |
需要选择性等待或非阻塞轮询 |
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
阻塞调用进程,直到它的任意一个子进程终止或收到信号。如果已经有子进程终止(成为僵尸进程),则wait()会立即返回。
@status:一个指向整数的指针,用于存储子进程的退出状态信息。如果不需要这些信息,可以传入NULL。status是一个位掩码,需要用特定的宏来提取信息:
宏 |
作用 |
示例 |
---|---|---|
|
子进程是否正常退出(通过 |
|
|
获取子进程的退出状态码( |
|
|
子进程是否因未捕获的信号而终止 |
|
|
获取导致子进程终止的信号编号 |
|
|
子进程是否被暂停(如 |
(较少使用) |
|
获取导致子进程暂停的信号编号 |
(较少使用) |
返回值:成功返回被终止子进程的PID;失败返回-1。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
printf("Child (PID: %d) is running.\n", getpid());
sleep(2);
printf("Child is exiting with code 42.\n");
exit(42); // 子进程退出,状态码为 42
} else {
// 父进程
printf("Parent (PID: %d) is waiting...\n", getpid());
int status;
pid_t child_pid = wait(&status); // 阻塞等待
if (child_pid == -1) {
perror("Wait failed");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("Child %d exited normally with code: %d\n",
child_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child %d was killed by signal: %d\n",
child_pid, WTERMSIG(status));
}
}
return 0;
}
输出:
Parent (PID: 1234) is waiting...
Child (PID: 1235) is running.
Child is exiting with code 42.
Child 1235 exited normally with code: 42
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
等待指定的子进程结束,或符合特定条件的子进程。
@pid
>0:等待进程ID等于 pid
的特定子进程。
= -1
:等待任意子进程,等同于wait(&status)。
= 0
:等待与调用进程同进程组的任何子进程。
< -1:等待进程组ID等于 |pid|
的任何子进程
@status:同wait();
@options:控制行为的选项
0:阻塞等待,直到目标子进程终止
WNOHANG:非阻塞模式。如果没有子进程退出,立即返回 0
。
WUNTRACED:额外返回已停止的子进程状态(如被SIGSTOP暂停)。
WCONTINUED:额外返回已继续的子进程状态(如被SIGCONT恢复)。
返回值:成功返回状态已改变的子进程PID(如果设置了 WNOHANG
且没有子进程退出,返回 0);
失败返回 -1
(如无子进程)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t child1 = fork();
if (child1 == 0) {
// 子进程1
sleep(2);
exit(10);
}
pid_t child2 = fork();
if (child2 == 0) {
// 子进程2
sleep(4);
exit(20);
}
// 父进程:非阻塞地轮询子进程状态
int status;
while (1) {
pid_t pid = waitpid(-1, &status, WNOHANG); // 不阻塞
if (pid == -1) {
printf("No more children.\n");
break;
} else if (pid == 0) {
printf("No child exited yet. Parent is working...\n");
sleep(1);
} else {
if (WIFEXITED(status)) {
printf("Child %d exited with code: %d\n",
pid, WEXITSTATUS(status));
}
}
}
return 0;
}
exec族
exec
是一组用于替换当前进程映像的系统调用,它们将当前进程的代码段、数据段、堆和栈替换为一个新的程序。exec
不会创建新进程(PID 不变),而是让当前进程“变身”为另一个程序。
不创建新进程:fork()
创建子进程,而 exec
替换当前进程的代码和数据。
不改变 PID:进程的 PID、父进程 PID、文件描述符等保持不变。
不返回:如果 exec
成功,原程序的代码被覆盖,exec
之后的代码不会执行。只有失败时才会返回 -1
。
函数 |
参数传递方式 |
是否搜索 |
是否自定义环境变量 |
示例 |
---|---|---|---|---|
|
列表(可变参数) |
❌ 需完整路径 |
❌ 使用当前环境 |
|
|
数组( |
❌ 需完整路径 |
❌ 使用当前环境 |
|
|
列表 |
✅ 自动搜索 |
❌ 使用当前环境 |
|
|
数组 |
✅ 自动搜索 |
❌ 使用当前环境 |
|
|
列表 |
❌ 需完整路径 |
✅ 自定义环境变量 |
|
|
数组 |
✅ 自动搜索 |
✅ 自定义环境变量 |
|
*execvpe
不是标准 POSIX 函数,但 Linux 支持。
execl(参数列表 + 完整路径)
int execl(const char *path, const char *arg, .../* (char *) NULL */);
参数以列表形式传递,最后一个参数必须是 NULL
,需指定完整路径,不会自动搜索 PATH
。
例:把ls指令替换为'ls -l'指令
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Before exec\n");
execl("/bin/ls", "ls", "-l", NULL); // 替换为 `ls -l`
perror("exec failed"); // 只有失败才会执行
return 1;
}
execv(参数数组 + 完整路径)
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
参数以数组形式传递(需要提前定义一个char *const argv[ ]),数组最后一个元素必须是 NULL,
需指定完整路径,不会自动搜索 PATH
。
例:把ls指令替换为'ls -l'指令
#include <unistd.h>
#include <stdio.h>
int main() {
char *args[] = {"ls", "-l", NULL};
printf("Before exec\n");
execv("/bin/ls", args); // 替换为 `ls -l`
perror("exec failed");
return 1;
}
execlp(参数列表+搜索PATH)
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
参数以列表形式传递,自动搜索 PATH
环境变量,只需提供程序名(如 ls
)。
例:把ls指令替换为'ls -l'指令
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Before exec\n");
execlp("ls", "ls", "-l", NULL); // 自动在 PATH 中找 `ls`
perror("exec failed");
return 1;
}