进程管理是操作系统最核心的功能之一,理解起来不难,但很多新手看完教材还是一知半解。这篇文章把核心概念、系统调用、实战案例串起来,看完就能上手。
一、进程管理
1.1 什么是进程?
进程就是正在运行的程序的实例。
进程就像厨师在厨房里做菜,每个厨师有自己独立的灶台(CPU)、食材(内存)、工具(文件描述符),互不干扰。
在 Linux 里,每个进程拥有:
它是操作系统进行资源分配和调度的基本单位。
1.2 进程的生命周期
一个进程从生到死,一共经历五个阶段:
1.3 核心系统调用
fork() - 创建进程
Linux 里最有意思的函数:调用一次,返回两次。
#include<unistd.h>pid_tfork(void);
返回值规则:
一次调用分出两个进程,父进程需要知道子进程是谁,子进程只需要知道自己是新的。
就像复制一份文件,原件得到副本的名字(PID),副本知道自己是个新的副本。
execvp() - 替换进程映像
intexecvp(constchar *file, char *const argv[]);
用新程序替换当前进程的代码段、数据段和堆栈。
调用成功不会返回——原来的代码已经被替换了。只有失败才返回 -1。
厨师突然换了个菜谱,继续做菜,但做的是完全不同的菜。
wait() / waitpid() - 等待子进程
父进程必须调用这个函数回收子进程资源,不然会产生僵尸进程。
#include<sys/wait.h>pid_twait(int *wstatus);pid_twaitpid(pid_t pid, int *wstatus, int options);
waitpid():等待特定子进程,可用 WNOHANG 非阻塞轮询
wstatus 存储子进程退出状态,可以用宏解析:
WIFEXITED(status):检查是否正常退出WEXITSTATUS(status):获取退出码
类比:家长等孩子放学回家,还得问孩子今天怎么样(退出状态)。
1.4 特殊情况:僵尸进程与孤儿进程
这两个概念经常搞混。
僵尸进程
子进程终止了,父进程还没调用 wait() 读取退出状态。
子进程不运行了,但 PCB 还占着内核内存。如果僵尸进程太多,系统可能无法创建新进程。
类比:孩子不做饭了但还没收拾灶台,灶台一直占着,别人没法用。
解决方法:父进程调用 wait() 或 waitpid() 回收子进程。
孤儿进程
父进程比子进程先退出,子进程变成孤儿。
在 Linux 中,孤儿进程会被 init 进程(PID = 1)收养。init 会自动调用 wait() 回收,孤儿进程一般没危害。
家长下班走了,孩子被食堂经理(init)收养,经理会帮孩子收拾灶台。
1.5 实战:实现简易 Shell
写个极简 Shell 把知识点串起来。
流程:父进程提示输入 → fork() 创建子进程 → 子进程 execvp() 执行命令 → 父进程 waitpid() 等待结束。
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/wait.h>#define MAX_CMD_LEN 1024intmain(){char input[MAX_CMD_LEN];char *args[64];pid_t pid;int status;printf("=== 简易 Shell 启动 ===\n");printf("输入 'exit' 退出\n\n");while (1) {printf("myshell> ");if (fgets(input, MAX_CMD_LEN, stdin) == NULL) {break; } input[strcspn(input, "\n")] = 0;if (strcmp(input, "exit") == 0) {break; }int i = 0; args[i] = strtok(input, " ");while (args[i] != NULL) { i++; args[i] = strtok(NULL, " "); } args[i] = NULL;if (args[0] == NULL) continue; pid = fork();if (pid < 0) { perror("fork failed");continue; }elseif (pid == 0) { execvp(args[0], args); perror("execvp failed");exit(1); }else { waitpid(pid, &status, 0);printf("\n命令退出,退出码:%d\n\n", WEXITSTATUS(status)); } }printf("Bye!\n");return0;}
编译运行
gcc shell.c -o myshell./myshell
二、进程间通信
每个进程有独立地址空间,无法直接访问其他进程内存。操作系统提供 IPC 机制让进程交换数据。
好比两家独立的小餐馆,不能直接用对方的食材,需要专门的传递方式。
常用方式:
2.1 管道
匿名管道
最简单的 IPC 方式,Shell 里的 cmd1 | cmd2 就是管道。
就像两家餐馆之间有个传菜窗口,只够一个人用,而且有方向性。
特点:
创建接口:
#include<unistd.h>intpipe(int pipefd[2]);
父进程写、子进程读
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pipefd[2];pid_t pid;char buf[1024];constchar *msg = "Hello from parent!";// 1. 创建管道,传菜窗口建好了if (pipe(pipefd) < 0) { perror("pipe failed");exit(1); }// 2. 创建子进程 pid = fork();if (pid < 0) { perror("fork failed");exit(1); }if (pid > 0) {// 父进程:写数据 close(pipefd[0]); // 关闭不用的读端,像不用传菜窗口接收 write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); // 写完关闭,传完菜关窗户 wait(NULL); // 等子进程读完 } else {// 子进程:读数据 close(pipefd[1]); // 关闭不用的写端ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);if (n > 0) { buf[n] = '\0';printf("Child: %s\n", buf); } close(pipefd[0]);exit(0); }return0;}
关键点:
有名管道(FIFO)
用于非亲缘进程通信,是文件系统中的特殊文件。
就像在公共区域设个公用信箱,谁都可以往里面塞纸条。
创建:
#include<sys/stat.h>intmkfifo(constchar *pathname, mode_t mode);
命令行创建:
mkfifo myfifo
写进程:
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>intmain(){constchar *fifo_path = "/tmp/myfifo";// 创建信箱 mkfifo(fifo_path, 0666);// 打开信箱写(会阻塞直到有人来读)int fd = open(fifo_path, O_WRONLY);if (fd < 0) { perror("open failed");exit(1); }char *messages[] = {"Hello", "Second", "Bye"};for (int i = 0; i < 3; i++) { write(fd, messages[i], strlen(messages[i])); sleep(1); } close(fd); unlink(fifo_path); // 删除信箱return0;}
读进程:
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>intmain(){constchar *fifo_path = "/tmp/myfifo";// 打开信箱读(会阻塞直到有人来写)int fd = open(fifo_path, O_RDONLY);if (fd < 0) { perror("open failed");exit(1); }char buf[1024];ssize_t n;while ((n = read(fd, buf, sizeof(buf)-1)) > 0) { buf[n] = '\0';printf("Reader: %s\n", buf); } close(fd);return0;}
匿名管道 vs 有名管道
2.2 信号处理
常见信号
信号处理接口
#include<signal.h>sighandler_tsignal(int signum, sighandler_t handler);
handler 可选值:
捕获 SIGINT 信号
#include<stdio.h>#include<stdlib.h>#include<signal.h>#include<unistd.h>voidhandler(int sig){printf("\n收到 SIGINT(%d),不退出\n", sig);printf("用 SIGKILL 才能杀掉我\n");}intmain(){// 注册 SIGINT 的处理函数 signal(SIGINT, handler);printf("PID: %d\n", getpid());printf("试试 Ctrl+C\n");while (1) { sleep(1); }return0;}
推荐使用 sigaction
signal 是老接口,现在推荐更安全的 sigaction:
intsigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
示例:
structsigactionsa;sa.sa_handler = handler;sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sigaction(SIGINT, &sa, NULL);
2.3 消息队列
消息队列是内核维护的消息链表,每个消息都有明确的边界,不会像管道那样粘在一起。
就像两家餐馆之间有个带编号的小信箱,每封信都独立,不会混在一起。
POSIX 消息队列接口详解
1. mq_open - 打开或创建消息队列
mqd_tmq_open(constchar *name, int oflag, ...);
参数说明:
name:消息队列名称,必须以 / 开头,比如 /my_queuemode_t mode(可选):创建时的权限,如 0666struct mq_attr *attr(可选):创建时的队列属性
返回值: 成功返回消息队列描述符,失败返回 -1
示例:
// 创建或打开消息队列,只写模式mqd_t mq = mq_open("/my_queue", O_WRONLY | O_CREAT, 0666, NULL);
2. mq_send - 发送消息
intmq_send(mqd_t mqdes, constchar *msg_ptr,size_t msg_len, unsignedint msg_prio);
参数说明:
mqdes:消息队列描述符(mq_open 返回的)msg_prio:消息优先级(0 最低,数字越大优先级越高)
返回值: 成功返回 0,失败返回 -1
示例:
// 发送一条高优先级消息mq_send(mq, "Hello", 5, 10);
3. mq_receive - 接收消息
ssize_tmq_receive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsignedint *msg_prio);
参数说明:
msg_prio:传出参数,接收到的消息优先级(可传 NULL 忽略)
返回值: 成功返回接收到的消息字节数,失败返回 -1
示例:
char buf[1024];unsignedint prio;ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
4. mq_close - 关闭消息队列
intmq_close(mqd_t mqdes);
参数说明:
返回值: 成功返回 0,失败返回 -1
5. mq_unlink - 删除消息队列
intmq_unlink(constchar *name);
参数说明:
返回值: 成功返回 0,失败返回 -1
发送端完整代码
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<mqueue.h>#include<unistd.h>#define QUEUE_NAME "/my_queue"#define MAX_SIZE 1024intmain(){// 1. 创建或打开消息队列(只写模式)// O_CREAT:不存在则创建// O_WRONLY:只写// 0666:读写权限// NULL:使用默认队列属性mqd_t mq = mq_open(QUEUE_NAME, O_WRONLY | O_CREAT, 0666, NULL);if (mq == (mqd_t)-1) { perror("mq_open failed");exit(1); }char *messages[] = {"First", "Second", "Bye"};for (int i = 0; i < 3; i++) {// 发送消息:优先级 0// 注意:mq_send 会阻塞直到队列有空间if (mq_send(mq, messages[i], strlen(messages[i]), 0) < 0) { perror("mq_send failed"); } sleep(1); }// 关闭消息队列描述符 mq_close(mq);return0;}
注意:
接收端完整代码
#include<stdio.h>#include<stdlib.h>#include<mqueue.h>#define QUEUE_NAME "/my_queue"#define MAX_SIZE 1024intmain(){// 1. 创建或打开消息队列(只读模式)mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY | O_CREAT, 0666, NULL);if (mq == (mqd_t)-1) { perror("mq_open failed");exit(1); }char buf[MAX_SIZE];for (int i = 0; i < 3; i++) {// 接收消息// 注意:mq_receive 会阻塞直到有消息// NULL 表示不关心优先级ssize_t n = mq_receive(mq, buf, MAX_SIZE, NULL);if (n > 0) { buf[n] = '\0'; // 手动添加字符串结束符printf("Received: %s\n", buf); } } mq_close(mq);// 删除消息队列(最后一个使用者负责) mq_unlink(QUEUE_NAME);return0;}
避坑指南:
- 缓冲区大小:接收缓冲区必须大于等于消息最大长度,否则 mq_receive 返回 EMSGSIZE 错误
- 字符串结束符:mq_receive 返回的是字节数,不会自动加
\0,必须手动添加 - 删除时机:mq_unlink 应该由最后一个使用者调用,否则其他进程可能找不到队列
- 非阻塞模式:如果不想阻塞,需要在 mq_open 时设置 mq_attr 的 mq_flags 为 O_NONBLOCK
编译需要链接 pthread:
gcc sender.c -o sender -lpthreadgcc receiver.c -o receiver -lpthread
2.4 共享内存
共享内存让多个进程直接访问同一块物理内存,不需要任何数据拷贝。
就像两家餐馆共用一个大仓库,都直接去仓库取东西,不用传来传去,速度最快。
必须自己做同步,通常搭配信号量使用。
POSIX 共享内存接口详解
1. shm_open - 打开或创建共享内存对象
intshm_open(constchar *name, int oflag, mode_t mode);
参数说明:
name:共享内存对象名称,必须以 / 开头,比如 /my_shm
返回值: 成功返回文件描述符,失败返回 -1
示例:
int fd = shm_open("/my_shm", O_RDWR | O_CREAT, 0666);
2. ftruncate - 设置共享内存大小
intftruncate(int fd, off_t length);
参数说明:
返回值: 成功返回 0,失败返回 -1
示例:
ftruncate(fd, 4096); // 设置为 4KB
3. mmap - 映射共享内存到进程地址空间
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
参数说明:
addr:建议的映射地址,通常传 NULL 让系统选择MAP_SHARED:共享映射,写入会反映到其他进程MAP_PRIVATE:私有映射,写入不会影响其他进程
返回值: 成功返回映射地址,失败返回 MAP_FAILED
示例:
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
4. munmap - 取消映射
intmunmap(void *addr, size_t length);
参数说明:
返回值: 成功返回 0,失败返回 -1
5. close - 关闭共享内存描述符
intclose(int fd);
参数说明:
返回值: 成功返回 0,失败返回 -1
6. shm_unlink - 删除共享内存对象
intshm_unlink(constchar *name);
参数说明:
返回值: 成功返回 0,失败返回 -1
写端完整代码
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/mman.h>#include<fcntl.h>#include<unistd.h>#define SHM_NAME "/my_shm"#define SHM_SIZE 4096intmain(){// 1. 创建或打开共享内存对象// O_RDWR:需要读写权限// O_CREAT:不存在则创建// 0666:权限(可读可写)int fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);if (fd < 0) { perror("shm_open failed");exit(1); }// 2. 设置共享内存大小(必须做!)// 新创建的共享内存大小为 0,必须用 ftruncate 设置if (ftruncate(fd, SHM_SIZE) < 0) { perror("ftruncate failed"); close(fd);exit(1); }// 3. 映射共享内存到进程地址空间// PROT_READ | PROT_WRITE:可读可写// MAP_SHARED:共享映射,写入会反映到其他进程void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (ptr == MAP_FAILED) { perror("mmap failed"); close(fd);exit(1); }// 4. 写入数据constchar *msg = "Hello from shared memory writer!";// 安全拷贝,防止溢出strncpy((char *)ptr, msg, SHM_SIZE - 1); ((char *)ptr)[SHM_SIZE - 1] = '\0'; // 确保字符串结束printf("Written: %s\n", (char *)ptr);// 5. 取消映射(不关闭文件描述符,让读端能继续用) munmap(ptr, SHM_SIZE);// 6. 关闭文件描述符 close(fd);return0;}
注意:
- ftruncate 必须调用,否则共享内存大小为 0,无法写入
- 用 strncpy 而不是 strcpy,防止缓冲区溢出
读端完整代码
#include<stdio.h>#include<stdlib.h>#include<sys/mman.h>#include<fcntl.h>#include<unistd.h>#define SHM_NAME "/my_shm"#define SHM_SIZE 4096intmain(){// 1. 打开共享内存对象int fd = shm_open(SHM_NAME, O_RDWR, 0);if (fd < 0) { perror("shm_open failed");exit(1); }// 2. 映射共享内存到进程地址空间void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (ptr == MAP_FAILED) { perror("mmap failed"); close(fd);exit(1); }// 3. 读取数据printf("Read from shared memory: %s\n", (char *)ptr);// 4. 取消映射 munmap(ptr, SHM_SIZE);// 5. 关闭文件描述符 close(fd);// 6. 删除共享内存对象(最后一个使用者负责) shm_unlink(SHM_NAME);return0;}
避坑指南:
- ftruncate 必不可少:新创建的共享内存大小默认为 0,必须用 ftruncate 设置大小,否则 mmap 会失败
- 内存同步:虽然 MAP_SHARED 会自动同步,但在某些情况下可能需要调用 msync 手动同步
- 删除时机:shm_unlink 只删除名称,不会立即释放内存,只有所有描述符都关闭了才会真正释放
- 权限问题:shm_open 的 mode 参数决定了谁能打开共享内存,常用 0666 让所有用户可读写
- 映射大小:mmap 的 length 必须小于等于 ftruncate 设置的大小
为什么共享内存最快?
- 管道、消息队列需要两次拷贝:用户 → 内核 → 用户
注意事项
2.5 IPC 方式对比
三、补充:PCB(进程控制块)
PCB 是内核用于描述和管理进程的数据结构。
一句话:PCB 是进程存在的唯一标志。
操作系统通过 PCB 感知、管理和控制进程。可以把 PCB 想象成:
3.1 PCB 内容
进程标识信息
进程状态信息
资源管理信息
进程控制信息
3.2 PCB 和僵尸/孤儿进程
僵尸进程
子进程终止后,代码和数据空间释放,但 PCB 还留在系统里,等待父进程调用 wait() 读取退出状态。
如果父进程不回收,PCB 一直占用内核资源。
孤儿进程
父进程先退出,子进程被 init(PID=1)收养,更新 PCB 中的 PPID。等子进程结束,init 自动回收 PCB。
总结
Linux 进程管理核心内容:
- 进程管理:概念、生命周期、系统调用、僵尸/孤儿进程、Shell 实战
- 进程间通信:管道、信号处理、消息队列、共享内存,每种都带完整源码
所有源码可直接编译运行。
理解这些,Linux 应用层开发的进程部分就算入门了。
“💡 觉得有用欢迎点赞收藏,关注不迷路。