LINUX系统编程 - 进程和进程间通信

一、进程是什么?

进程是运行中的程序,是操作系统分配 CPU、内存、文件等资源的基本单位。它不仅包含程序的代码和数据,还包括:

  • 进程 ID(PID):唯一标识进程,可通过 /proc 目录查看进程详情(如 ls /proc 能看到所有运行进程的 PID 目录)。
  • CPU 时间片:进程轮流占用 CPU 的“时间片段”,由调度算法决定。
  • 内存空间:包括代码段、数据段、堆、栈等,采用“虚拟内存”技术隔离不同进程。
  • 资源集合:打开的文件描述符、信号量、管道等。

二、如何创建进程?—— forkexec 的“黄金组合”

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 系列函数(如 execlexecvexecvp 等)。
exec 的作用是用新程序的代码、数据替换当前进程的地址空间,但进程 PID 保持不变(子进程“变身”,但仍是同一个进程实体)。

典型场景:Shell 执行命令
当你在终端输入 ls 时,Shell 的工作流程是:

  1. Shell(父进程)调用 fork 创建子进程;
  2. 子进程调用 exec 加载 /bin/ls 程序,替换自身为 ls 进程;
  3. 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),可用于无亲缘关系的进程通信,支持双向通信。
  • 特点
    • 通过文件名访问,突破“亲缘关系”限制;
    • 数据仍随读随删,本质是内核缓冲区(文件系统中仅为标识,不存储数据);
    • 适用于不同程序间的简单通信(如客户端与服务器)。
  • 使用流程
    1. mkfifo("/tmp/myfifo", 0666) 创建命名管道;
    2. 进程 A 以写方式打开 open("/tmp/myfifo", O_WRONLY)
    3. 进程 B 以读方式打开 open("/tmp/myfifo", O_RDONLY)
    4. 双方通过 read/write 通信。

3. 消息队列:带“类型标签”的消息容器

  • 定义:消息队列是内核中的有序消息链表,每条消息带“类型标签”,进程可按类型读取消息(无需按顺序)。
  • 特点
    • 数据可持久化(进程退出后消息不丢失,直到被读取或删除);
    • 支持按类型筛选消息(如进程 A 只接收类型为 1 的消息);
    • 适用于“一对多”或“多对多”通信(如日志系统,多个进程写日志,一个进程按类型分类处理)。

4. 共享内存:最快的 IPC 方式

  • 定义:共享内存是多个进程共享的同一块物理内存,进程直接读写内存,无需数据拷贝。
  • 特点
    • 速度最快(无内核中转,直接访问内存);
    • 需配合同步机制(如信号量)防止“竞态条件”(多个进程同时写导致数据混乱);
    • 适用于高频、大数据量通信(如视频处理,一个进程采集帧,另一个进程编码)。
  • 核心步骤
    1. shmget() 创建共享内存;
    2. shmat() 将共享内存映射到进程地址空间;
    3. 进程直接读写映射后的内存地址;
    4. 通信结束后用 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(文件映射)内存+文件极高可选实时数据采集(如传感器数据共享)、大文件内存映射

选择原则总结:

  1. 高频通信选内存级:共享内存、mmap(无磁盘IO开销);
  2. 跨主机必选套接字:TCP/UDP 是唯一支持网络通信的机制;
  3. 持久化用文件类:普通文件(低频)或 mmap(高频+需持久化);
  4. 简单通信选管道/FIFO:无需复杂API,适合父子或简单跨进程场景;
  5. 同步控制用信号量:配合共享内存/文件使用,解决资源竞争问题。

这些机制共同构成了 Linux 进程协作的基础,从简单的命令行工具到复杂的分布式系统,都依赖这些技术实现数据交互与协同工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值