一、进程是什么?
进程是运行中的程序,是操作系统分配 CPU、内存、文件等资源的基本单位。它不仅包含程序的代码和数据,还包括:
- 进程 ID(PID):唯一标识进程,可通过
/proc目录查看进程详情(如ls /proc能看到所有运行进程的 PID 目录)。 - CPU 时间片:进程轮流占用 CPU 的“时间片段”,由调度算法决定。
- 内存空间:包括代码段、数据段、堆、栈等,采用“虚拟内存”技术隔离不同进程。
- 资源集合:打开的文件描述符、信号量、管道等。
二、如何创建进程?—— fork 与 exec 的“黄金组合”
Linux 中创建新进程的核心方式是 fork + exec,二者分工明确:
1. fork:“复制”出子进程
fork() 函数会创建一个与父进程几乎完全相同的子进程,核心特性是:
- 父子进程的关系:父进程中
fork()返回子进程的 PID,子进程中fork()返回0,据此可区分父子逻辑。 - 写时拷贝(Copy-On-Write, COW):为了高效,
fork并不立即复制父进程的全部内存,而是让父子进程共享只读的内存页;只有当任一进程修改内存数据时,才会为该部分内存创建副本。这样既保证了父子进程地址空间的独立性,又减少了不必要的内存复制开销。
代码示例:区分父子进程
#include <unistd.h>
#include <stdio.h>
int main() {
printf("父进程 PID: %d\n", getpid());
pid_t pid = fork();
if (pid == 0) {
// 子进程逻辑:fork 返回 0
printf("子进程:PID = %d,父进程 PID = %d\n", getpid(), getppid());
} else {
// 父进程逻辑:fork 返回子进程 PID
printf("父进程:子进程 PID = %d\n", pid);
}
return 0;
}
2. exec:让子进程“改头换面”
fork 出的子进程与父进程代码完全相同,若要让子进程执行新程序,需调用 exec 系列函数(如 execl、execv、execvp 等)。
exec 的作用是用新程序的代码、数据替换当前进程的地址空间,但进程 PID 保持不变(子进程“变身”,但仍是同一个进程实体)。
典型场景:Shell 执行命令
当你在终端输入 ls 时,Shell 的工作流程是:
- Shell(父进程)调用
fork创建子进程; - 子进程调用
exec加载/bin/ls程序,替换自身为ls进程; ls执行完毕后退出,Shell 继续等待下一条命令。
三、进程的状态与“僵尸进程”问题
进程在生命周期中有多种状态(如 R 运行、S 睡眠、Z 僵尸等),其中僵尸进程是需要重点关注的“异常状态”。
1. 僵尸进程的本质
子进程先于父进程退出时,会向父进程发送 SIGCHLD 信号,并进入僵尸状态(Z):此时子进程已无运行代码,但仍保留“进程退出状态”等信息,需父进程回收。
若父进程未及时回收子进程(如没有调用 wait/waitpid),僵尸进程会持续占用 PID 等系统资源,严重时导致系统无法创建新进程。
2. 如何避免僵尸进程?
父进程需主动调用 wait() 或 waitpid() 等待子进程退出,回收其资源:
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:执行一些逻辑后退出
printf("子进程运行中...\n");
sleep(2);
printf("子进程退出\n");
return 0;
} else {
// 父进程:等待子进程退出并回收资源
printf("父进程等待子进程...\n");
wait(NULL); // 阻塞直到子进程退出
printf("父进程:子进程已回收\n");
}
return 0;
}
四、进程间通信(IPC):打破隔离的“桥梁”
进程是独立的资源单元(内存空间隔离),但实际场景中常需协作(如一个进程生成数据,另一个进程处理数据)。进程间通信(IPC) 就是实现进程协作的技术,Linux 提供了多种 IPC 机制,适用于不同场景:
1. 管道(Pipe):父子进程的“单向通道”
- 定义:管道是内核中的一段缓冲区,用于有亲缘关系的进程(如父子、兄弟进程)间通信,数据单向流动(半双工)。
- 特点:
- 数据随读随删,不可重复读取;
- 无亲缘关系的进程无法使用(因管道无标识,仅能通过继承文件描述符访问);
2. 命名管道(FIFO):无亲缘关系进程的“双向通道”
- 定义:命名管道是有文件名标识的管道,存在于文件系统中(如
/tmp/myfifo),可用于无亲缘关系的进程通信,支持双向通信。 - 特点:
- 通过文件名访问,突破“亲缘关系”限制;
- 数据仍随读随删,本质是内核缓冲区(文件系统中仅为标识,不存储数据);
- 适用于不同程序间的简单通信(如客户端与服务器)。
- 使用流程:
- 用
mkfifo("/tmp/myfifo", 0666)创建命名管道; - 进程 A 以写方式打开
open("/tmp/myfifo", O_WRONLY); - 进程 B 以读方式打开
open("/tmp/myfifo", O_RDONLY); - 双方通过
read/write通信。
- 用
3. 消息队列:带“类型标签”的消息容器
- 定义:消息队列是内核中的有序消息链表,每条消息带“类型标签”,进程可按类型读取消息(无需按顺序)。
- 特点:
- 数据可持久化(进程退出后消息不丢失,直到被读取或删除);
- 支持按类型筛选消息(如进程 A 只接收类型为 1 的消息);
- 适用于“一对多”或“多对多”通信(如日志系统,多个进程写日志,一个进程按类型分类处理)。
4. 共享内存:最快的 IPC 方式
- 定义:共享内存是多个进程共享的同一块物理内存,进程直接读写内存,无需数据拷贝。
- 特点:
- 速度最快(无内核中转,直接访问内存);
- 需配合同步机制(如信号量)防止“竞态条件”(多个进程同时写导致数据混乱);
- 适用于高频、大数据量通信(如视频处理,一个进程采集帧,另一个进程编码)。
- 核心步骤:
- 用
shmget()创建共享内存; - 用
shmat()将共享内存映射到进程地址空间; - 进程直接读写映射后的内存地址;
- 通信结束后用
shmdt()解除映射,shmctl()删除共享内存。
- 用
5. 信号量(Semaphore):共享资源的“红绿灯”
- 定义:信号量是内核中的计数器,用于控制多个进程对共享资源的访问(如限制同时写共享内存的进程数)。
- 核心操作:
P 操作:计数器减 1,若结果 < 0,进程阻塞(等待资源);V 操作:计数器加 1,若结果 ≤ 0,唤醒一个阻塞进程;
- 适用场景:解决“临界资源竞争”问题(如多个进程写同一文件时,用信号量保证同一时间只有一个进程写入)。
6. 信号(Signal):事件通知的“紧急电报”
- 定义:信号是内核向进程发送的事件通知(如
Ctrl+C触发SIGINT信号,通知进程终止)。 - 特点:
- 异步性(信号可随时到达,进程需注册“信号处理函数”响应);
- 携带信息少(仅一个信号编号),适用于简单通知(如“退出”“暂停”);
- 常见信号:
SIGKILL(强制终止,不可捕获)、SIGTERM(正常终止,可捕获)、SIGCHLD(子进程退出通知)。
7. 套接字(Socket):跨网络的“万能通信器”
- 定义:套接字是支持跨主机进程通信的机制,基于 TCP/UDP 协议,可在同一主机或不同主机的进程间通信。
- 特点:
- 灵活性高,支持本地(
AF_UNIX域)和网络(AF_INET域)通信; - 是网络编程的核心(如客户端与服务器通信、分布式系统协作);
- 灵活性高,支持本地(
- 典型场景:Web 浏览器(客户端)与 Web 服务器(
80端口)通过 TCP 套接字通信。
8. 普通文件:基于磁盘的“持久化通信”
-
定义:通过读写同一个磁盘文件实现进程间通信,进程 A 将数据写入文件,进程 B 从文件读取数据,依赖文件系统持久化数据。
-
特点:
- 持久化:数据保存在磁盘,进程退出后不丢失,支持跨重启通信;
- 无亲缘限制:任意进程只要有文件权限即可通信;
- 性能较低:依赖磁盘 I/O,不适合高频通信;
- 需手动同步:需通过文件锁或标记文件避免读写冲突。
-
代码示例(两个独立进程):
进程A(写文件):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// 打开文件:写模式,不存在则创建,存在则覆盖
int fd = open("/tmp/share_file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) { perror("open failed"); return 1; }
const char* data = "用户订单:ID=10086,金额=99元";
write(fd, data, strlen(data));
printf("进程A已写入数据到文件\n");
close(fd);
return 0;
}
进程B(读文件):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// 打开文件:读模式
int fd = open("/tmp/share_file.txt", O_RDONLY);
if (fd == -1) { perror("open failed"); return 1; }
char buf[200];
int len = read(fd, buf, sizeof(buf)-1);
buf[len] = '\0'; // 确保字符串结束
printf("进程B从文件读取到:%s\n", buf);
close(fd);
return 0;
}
9. 文件映射虚拟内存(mmap):基于内存的“高性能通信”
-
定义:通过
mmap()将同一个文件映射到多个进程的虚拟内存,进程直接读写内存实现通信,数据自动同步到文件(可选持久化)。 -
特点:
- 高性能:数据在内存中交换,仅首次映射可能触发磁盘 I/O;
- 灵活持久化:映射文件时数据同步到磁盘,映射匿名内存(
MAP_ANONYMOUS)时仅内存共享; - 需同步机制:多进程写时需加锁(如信号量)避免数据冲突;
- 适用于大数据量:支持GB级数据共享,无需频繁读写磁盘。
-
代码示例(两个独立进程):
进程A(写mmap内存):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#define FILE_SIZE 1024
int main() {
// 创建并打开共享文件
int fd = open("/tmp/mmap_shared.dat", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd == -1) { perror("open failed"); return 1; }
// 扩展文件大小(mmap需要非空文件)
lseek(fd, FILE_SIZE-1, SEEK_SET);
write(fd, "", 1);
// 映射文件到内存(MAP_SHARED表示修改对其他进程可见)
char* addr = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap failed"); return 1; }
// 写入数据
strcpy(addr, "mmap共享数据:实时传感器数值=25.5℃");
printf("进程A已写入mmap内存\n");
// 等待进程B读取(实际场景需同步)
sleep(10);
// 清理资源
munmap(addr, FILE_SIZE);
close(fd);
return 0;
}
进程B(读mmap内存):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#define FILE_SIZE 1024
int main() {
// 打开共享文件
int fd = open("/tmp/mmap_shared.dat", O_RDWR);
if (fd == -1) { perror("open failed"); return 1; }
// 映射文件到内存
char* addr = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap failed"); return 1; }
// 读取数据
printf("进程B从mmap读取到:%s\n", addr);
// 清理资源
munmap(addr, FILE_SIZE);
close(fd);
return 0;
}
五、进程间通信方式对比与适用场景
| 通信方式 | 核心载体 | 性能 | 持久化 | 跨主机 | 适用场景典型案例 |
|---|---|---|---|---|---|
| 管道(Pipe) | 内核缓冲区 | 中 | 否 | 否 | 父子进程日志传递、命令行管道(` |
| 命名管道(FIFO) | 内核缓冲区+文件名 | 中 | 否 | 否 | 无亲缘关系的服务与客户端通信 |
| 消息队列 | 内核消息链表 | 中 | 否 | 否 | 多进程日志分类处理、事件通知 |
| 共享内存 | 物理内存 | 极高 | 否 | 否 | 高频视频帧传输、大缓存共享 |
| 信号量 | 内核计数器 | 高 | 否 | 否 | 共享资源互斥(如控制文件并发写入) |
| 信号 | 内核信号编号 | 高(通知) | 否 | 否 | 进程异常终止、子进程退出通知 |
| 套接字(Socket) | 网络协议/TCP/UDP | 中-低 | 否 | 是 | Web服务通信、分布式系统跨主机协作 |
| 普通文件 | 磁盘文件 | 低 | 是 | 否 | 配置文件共享、低频大数据持久化交换 |
| mmap(文件映射) | 内存+文件 | 极高 | 可选 | 否 | 实时数据采集(如传感器数据共享)、大文件内存映射 |
选择原则总结:
- 高频通信选内存级:共享内存、mmap(无磁盘IO开销);
- 跨主机必选套接字:TCP/UDP 是唯一支持网络通信的机制;
- 持久化用文件类:普通文件(低频)或 mmap(高频+需持久化);
- 简单通信选管道/FIFO:无需复杂API,适合父子或简单跨进程场景;
- 同步控制用信号量:配合共享内存/文件使用,解决资源竞争问题。
这些机制共同构成了 Linux 进程协作的基础,从简单的命令行工具到复杂的分布式系统,都依赖这些技术实现数据交互与协同工作。


895

被折叠的 条评论
为什么被折叠?



