第四章:喂,听得见吗? - IPC

在上一章中,我们深入理解了进程管理——操作系统如何像一个指挥家,协调多个进程共享 CPU 资源。但现在有一个新的挑战:这些被严格隔离的进程如何相互通信?就像被困在不同房间的人,他们如何传递消息?

这就是 IPC(Inter-Process Communication,进程间通信)要解决的核心问题。

4.1 为什么需要 IPC?

在微内核的世界里,进程之间是老死不相往来的。

  • 进程 A 看不到进程 B 的内存。
  • 进程 B 改不了进程 A 的变量。

这种隔离保证了安全,但也带来了麻烦。 比如,Shell 想要读取文件,它不能直接去读硬盘,因为它没有权限。它必须找“文件系统服务”帮忙。 但是,它怎么告诉文件系统“我要读这个文件”呢?

这就需要 IPC (Inter-Process Communication,进程间通信)

4.2 电话 vs 邮件:同步与异步

IPC 主要有两种模式:

  1. 异步 (Asynchronous) - 寄信

    • 我把信扔进邮筒,然后该干嘛干嘛。
    • 对方什么时候收到,我不知道。
    • 对方回信了,我再去信箱拿。
    • 优点:我不被阻塞,效率高。
    • 缺点:逻辑复杂,不知道信丢没丢。
  2. 同步 (Synchronous) - 打电话

    • 我拨通电话,在那儿等着。
    • 对方接了,我们聊完,我挂电话。
    • 在通话期间,我什么也干不了,只能专心打电话。
    • 优点:简单直接,打完就知道结果。
    • 缺点:我会被阻塞 (Block)。

EwokOS 主要使用同步 IPC。 为什么?因为它简单、可靠。对于微内核来说,简单就是美。

IPC 的历史:从管道到消息队列

IPC 的历史和操作系统一样古老。让我们看看它是如何演化的:

1. 管道 (Pipes) - 1970 年代

  • Unix 最早的 IPC 机制,由 Doug McIlroy 提出。
  • 经典的命令 ls | grep txt 就是用管道连接两个进程。
  • 问题:只能单向通信,且只能在父子进程间使用。

2. 消息队列 (Message Queues) - 1980 年代

  • System V Unix 引入的机制。
  • 进程可以向队列发送消息,其他进程从队列读取。
  • 问题:需要复制数据,效率不高。

3. 共享内存 (Shared Memory) - 1980 年代

  • 两个进程直接访问同一块物理内存。
  • 零拷贝,效率极高。
  • 问题:需要手动同步,容易出错。

4. RPC (Remote Procedure Call) - 1984 年

  • Sun Microsystems 开发的机制。
  • 让跨网络的进程通信看起来像本地函数调用。
  • 微内核的 IPC 本质上就是本地的 RPC。

Mach 微内核的贡献: CMU 的 Mach 微内核(1985 年)首次将 IPC 作为操作系统的核心抽象。它提出的端口 (Port)消息 (Message) 模型深刻影响了后续所有微内核的设计,包括 EwokOS。

4.3 EwokOS 的 IPC 流程

让我们看看一次典型的 IPC 通话(比如 Shell 呼叫 VFS)发生了什么:

  1. 拨号 (Client Call)

    • Shell (Client) 调用 ipc_call(vfs_pid, CMD_READ, ...)
    • 内核收到请求,把 Shell 冻结 (BLOCK),让它去睡觉。
    • 内核生成一个任务单 ipc_task_t,塞进 VFS (Server) 的信箱里。
  2. 接听 (Server Fetch)

    • VFS (Server) 平时就在循环里等着,调用 ipc_fetch() 检查信箱。
    • 发现有新任务!VFS 拿到任务单,开始干活(读文件)。
  3. 回复 (Server Return)

    • VFS 干完活了,把结果写好,调用 ipc_end()
    • 内核收到通知,把结果搬运给 Shell。
    • 内核唤醒 (WAKEUP) Shell。
  4. 挂断 (Client Resume)

    • Shell 醒来,发现 ipc_call 返回了,手里拿着 VFS 给的数据。
    • Shell 继续干活。

4.4 核心代码赏析

IPC 的核心代码在 kernel/kernel/src/ipc.c

// 客户端发起呼叫
int32_t ipc_call(int32_t pid, int32_t call_id, proto_t* data) {
    // 1. 准备参数
    // 2. 陷入内核 (System Call)
    // 3. 内核将当前进程状态设为 BLOCK
    // 4. 切换到其他进程
}

// 服务端处理
void ipc_handle_loop() {
    while(true) {
        // 1. 检查有没有任务
        int id = ipc_fetch(); 
        if (id > 0) {
            // 2. 处理任务
            do_work();
            // 3. 告诉内核完事了
            ipc_end();
        }
    }
}

4.5 消息数据结构:proto_t

IPC 传递的数据被封装在 proto_t 结构中,它是一个灵活的数据包格式:

typedef struct {
    void*    data;      // 数据缓冲区指针
    uint32_t size;      // 数据大小
    uint32_t offset;    // 读写偏移量
    uint32_t capacity;  // 缓冲区容量
} proto_t;

使用方式

  • 写入数据:使用 proto_add_int(), proto_add_str() 等函数追加数据。
  • 读取数据:使用 proto_read_int(), proto_read_str() 等函数按顺序读取。
  • 序列化:数据在发送前会被序列化成字节流,接收后再反序列化。

示例

// 客户端构造请求
proto_t* req = proto_new(NULL, 0);
proto_add_int(req, CMD_READ);
proto_add_str(req, "/dev/timer");
proto_add_int(req, 1024);  // 读取长度

// 发送 IPC 请求
proto_t* resp = ipc_call(server_pid, req);

// 解析响应
int status = proto_read_int(resp);
char* data = proto_read_str(resp, NULL);
proto_free(resp);

4.6 共享内存:更快的数据传递

对于大量数据的传递,每次通过 IPC 复制会很慢。EwokOS 提供了共享内存 (Shared Memory) 机制:

  1. 创建共享内存:进程 A 创建一块共享内存区域。
  2. 映射:进程 A 和进程 B 都将这块物理内存映射到各自的虚拟地址空间。
  3. 直接访问:两个进程都可以直接读写这块内存,无需通过内核复制。
  4. 同步:使用锁或信号量保证并发访问的安全性。

优点

  • 零拷贝 (Zero-Copy),效率极高。
  • 适合视频流、大文件等场景。

通过这种机制,EwokOS 将一个个独立的孤岛(进程)连接成了一个繁忙的群岛网络。

4.7 本章小结与展望

我们刚刚探索了微内核的"神经系统"——IPC(进程间通信)。让我们回顾一下关键点:

  • IPC 的本质:在保持进程隔离的同时,提供安全、高效的通信机制
  • 同步 vs 异步:打电话 vs 发邮件,EwokOS 选择了简单可靠的同步模式
  • 历史演进:从 Unix 的管道到 Mach 的端口模型,IPC 一直在进化
  • 共享内存:当需要传输大量数据时,零拷贝是最高效的方案

IPC 解决了"进程如何通信"的问题,但还有一个更基础的问题:这些进程的数据存放在哪里?内存是如何分配和保护的?

想象一下,如果没有内存管理,进程 A 可能会不小心改写进程 B 的数据,导致灾难性后果。更糟糕的是,如果有恶意程序故意读取其他进程的密码或私钥,后果不堪设想。

这就是下一章的主题——内存管理。我们将看到:

  • 虚拟内存如何给每个进程创造"独占整个世界"的幻觉
  • MMU(内存管理单元)如何像一个隐形的翻译官,偷偷替换地址
  • 写时复制(COW)如何让 fork() 变得飞快
  • 缺页异常如何让操作系统耍"空头支票"的把戏

进程管理是系统的"心脏",IPC 是"神经系统",而内存管理就是"土地规划局"。让我们继续深入。

results matching ""

    No results matching ""