当前位置:首页 > 公众号精选 > 码农的荒岛求生

作为1970年代贝尔实验室的系统工程师,你亲历了操作系统的早期发展。当时,你刚刚实现了进程隔离机制,这是一项重大突破 —— 每个进程都拥有自己独立的地址空间,彼此之间不会相互干扰。

进程真的是一个很棒的概念,快速在程序员之间流行起来,随着进程的大量使用,你很快发现了一个新问题:进程之间无法方便地交换数据。

这难不倒你们团队中的大聪明,小意思,可以直接借助磁带实现多个进程协同工作,假设有3个进程协同工作:

1. 第一个进程从磁带读入原始数据进行格式转换写入磁带
2. 第二个进程从磁带读入数据并执行复杂的数据分析
3. 第三个进程从磁带读取第二个结果简单处理后写入报表

尽管这个方法可行但效率极低:

  1. 每次进程通信都需要完整的磁带I/O操作
  2. 进程之间无法实时通信,必须等待前一个进程运行完成

显然需要一种比大聪明的方法更聪明的进程间通信机制。

管道:第一个突破性的IPC机制

你知道大聪明的方法尽管不怎么聪明但思路是对的,不过为什么要借助磁带这种慢速设备而不是更快的内存呢?你突然想到:"如果能创建一个内存中的先进先出队列,让一个进程写入数据,另一个进程读取数据,不就解决问题了吗?这很像一个管道pipe"

你很快实现了第一个原型:

// 管道数据结构
struct pipe {
    char buffer[PIPE_SIZE];  // 环形缓冲区
    int read_pos;            // 读取位置
    int write_pos;           // 写入位置
    struct proc *reader;     // 读取进程
    struct proc *writer;     // 写入进程
};

这个简单的设计解决了几个关键问题:

  1. 数据直接在内存中传递,避免了低俗磁带,速度杠杠的
  2. 为了实现以及使用起来更简单,你基于文件描述符进行管道读写,因为父子进程可以共享文件描述符,但这也限制了管道只能在父子进程之间使用

管道这个概念真的是太好用了,它快速流行起来,你的大名开始突破贝尔实验室的小圈子,管道极大提升了进程间通信的效率,并成为操作系统中最基础、最常用的IPC(进程间通信)机制之一。

随着管道的大量使用,管道的局限性很快就显现了:

  1. 管道只能用于有亲缘关系的进程(如父子进程)之间通信,这是你当初偷懒的结果
  2. 管道是单向的,双向通信需要创建两个管道
  3. 管道中的数据是无结构的字节流,缺乏消息边界

如何让没有亲缘关系的进程之间通信?这些限制促使你继续探索更强大的IPC机制。

命名管道:突破亲缘关系限制

这时大聪明的思路又一次在你的脑海中浮现了出来,磁带数据这种东西天然可以突破进程亲缘关系,因为文件并不限于某个进程的读写,如果能给管道一个名字,并将其注册到文件系统中,那么任何进程都可以通过这个名字来访问管道。

啊哈,你真的是比大聪明聪明大了。

既然管道有名字了,那么这个新管道就叫命名管道(Named Pipe)。

命名管道的实现非常成功,它让任意两个进程都能通过文件系统路径建立通信。这种方式有几个显著优点:

  1. 突破了传统管道只能用于亲缘进程的限制
  2. 复用了文件系统的权限管理机制
  3. 使用文件I/O接口,对程序员友好

你的名声又一次在计算机科学届出圈,实现了二次出道,属于你的时代又一次到来了,命名管道开始大量使用起来。

消息队列:结构化的通信方式

然而,随着应用场景的复杂化,命名管道也开始显露出一些不足:

  1. 仍然是单向通信,双向通信需要创建两个FIFO
  2. 缺乏消息边界,处理变长消息比较麻烦
  3. 没有内置的多客户端支持机制

这促使你设计了一种新的IPC机制:消息队列。与管道不同,消息队列:

  1. 支持结构化的消息,每条消息有明确的边界
  2. 可以为消息分配优先级
  3. 支持按类型接收消息,而不仅仅是FIFO顺序

你的实现大致如下:

struct msg_msg {
    struct list_head m_list;   // 消息链表指针
    long m_type;               // 消息类型(用于分类)
    size_t m_ts;               // 消息总大小(含数据和元数据)
    char m_data[0];            // 实际数据存储在此
};

struct msg_queue {
    struct list_head q_messages; // 消息链表头
    unsigned long q_cbytes;      // 当前队列总字节数
    unsigned long q_qnum;        // 当前队列消息数
};

消息队列的引入极大地简化了复杂系统中的进程通信。特别是在处理多种类型消息的场景下,它的优势非常明显。

然而,随着这种进程间通信机制的使用问题又一次暴露了出来。

  • 你发明的这些通信机制,如管道、消息队列等需要将数据从用户空间拷贝到内核缓冲区,再拷贝到接收进程的用户空间。两次数据拷贝(用户态到内核态再到用户)增加了CPU和内存的开销。
  • 图像处理、科学计算、数据库系统等应用需要进程间频繁交换大量数据。传统IPC的延迟和吞吐量难以满足这些场景的实时性要求。
  • 这些通信机制要求数据以固定格式序列化传输,复杂数据结构(如链表、树)的传递需要额外序列化/反序列化步骤,增加开发复杂性和性能损耗。

为什么进程之间通信会有这些问题呢?你不禁陷入思考,这让你不得不追溯到进程发明的起点,未完待续。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭