Operating-System-Chap.5


操作系统 Chap.5 进程间通信

1. 进程间通信(IPC)

一个系统中的多个进程是可以单独运行的, 但同时也存在相互影响的情况(比如通过进程间的数据交换 / 共享来协同工作).

为啥要有合作进程? 这其实跟我们此前提到的多线程(Multithread)出现的理由差不多:

  • 信息共享
  • 提高效率
  • 功能模块化
  • 便携性

当然, 合作进程存在的前提在于进程间能够真正做到共享信息, 这就引出了本文的主题: IPC(Interprocess communication / 进程间通信).

1.1 同步机制(Synchronization)

通信就通信吧, 干嘛同步啊?

我们需要明确, 进程间通信的根本需求是要共同完成某个任务, 因此进程之间的信息需要实时更新且保持一致, 否则就得出乱子.

现在很多进程都能访问同一块数据, 我们要确保它们看到的数据都是一致的, 这就需要我们对 不同进程处理数据的方法与顺序 进行详尽的设计.

2. 管道(Pipe)

我们接下来进入进程间通信的第一种方式, 管道通信.

2.1 定义

顾名思义, 所谓管道就是一个很长的字节流, 一端写入, 一端读出. 该消息传递方式没有 消息边界 这一概念.

所谓 消息边界 , 个人的理解是, 比如写入端是一行一行写入的, 这一点无法直接提现到读出端上, 因为读出端只能通过一个字节流来读取另一端传递过来的信息. 因此如果需要分割消息, 这个方法可能需要自行定义.

Pipe: Understand it as a circular ring buffer

2.2 具体实现

管道是通过数组来进行实现的:

int fd[2];
//fd[0]表示读端
//fd[1]表示写端

//写入端
close(fd[0]); //关闭读端
write(fd[1], string.c_str(), string.size()); //写入指定字符串

//读出端
close(fd[1]); //关闭写端
read(fd[0], buffer, sizeof(buffer)-1); //buffer: 字符数组(其实就是字节数组)

Pipe: Read port & Write port

2.3 具体机制

在内核内存系统中, 有一个专门用于管道的文件系统: pipefs

这玩意会在系统初始化阶段就进行安装, 当进程尝试构建一个管道时, 就相当于在这个 pipefs 中创建一个文件. 对读端 / 写端分别用文件描述符 O_RDONLY / O_WRONLY 来进行控制.

那如果我硬在关闭的读端读 / 在关闭的写端写呢?
前者, 系统会直接返回EOF(即 End of file ). 后者, 系统会进入异常处理, 给出 SIGPIPE 信号, 并报写入错误.

如果读时, 管道里没东西? / 写时, 东西太多了?
前者, 系统会阻塞读端进程的IO操作, 直到管道里有东西可读为止. 后者, 系统会将过长的消息自动分割为固定字节大小的块(在Linux中, 这个大小为4KB). 再一块一块传输.

2.4 FIFO / 命名管道

不同于此前我们提及的管道, FIFO不仅仅局限于父进程与子进程之间的通信, 而是可以在两个完全无关的进程之间进行类似管道的通信操作.

其使用方式如下:

//Read Port
#define FIFO_FILE "MYFIFO”

FILE *fp;
char readbuf[80];

/* Create the FIFO if it does not exist */
mkfifo(myfifo, 0666);

fp = fopen(FIFO_FILE, "r");
fgets(readbuf, 80, fp);
printf("Received string: %s\n", readbuf);
fclose(fp); // fclose() will not delete fifo

//Write Port
#define FIFO_FILE "MYFIFO”

fp = fopen(FIFO_FILE, "w"))
fputs(“Hello!, fp);

fclose(fp);
return(0);

从上面的代码我们能看出来: FIFO不同于管道, 它真正的在计算机的硬盘中创建了一个文件(虽然其IO仍然是通过内核内存实现的). 相应的, 这个文件也不会在管道使用完毕后自动删除.

相对于管道而言, 它的好处在于两点:
(1) 不仅仅局限于父子进程间
(2) 可以有多个写进程与一个读进程

3. Unix Domain Socket

提及Socket, 读者如果有 计算机网络原理 的相关基础, 大概会想到其中的 套接字 .

Unix Domain Socket就是一种专用于本机进程间通信的套接字, 其使用的信息传递方式与计网中的套接字尤为类似.

为啥要引入它呢:

  • 这玩意不仅支持字节流, 同时支持数据报传输(可以理解为一个封装好的消息包)
  • 不同于管道, 它是双向的 , 这省去了很多麻烦.

3.1 使用方式

Unix Domain Socket是通过文件路径进行标识的, 这意味着要使用它, 也需要在硬盘内某个位置创建一个文件.

对于服务端(或者被称作发起连接端), 其连接流程如下:

  • bind(): 绑定指定文件作为套接字
  • listen(): 监听, 等待连接请求
  • accept(): 收到连接请求后接收该请求

对于客户端, 其流程要简单一些:

  • connect(): 向监听套接字发送连接请求

一个比较清晰的Unix Domain Socket的使用说明:
https://systemprogrammingatntu.github.io/mp2/unix_socket.html

4. 共享内存(Shared Memory)

上述两种方式已经能够应对绝大部分的进程间通信需求了, 但我们可以发现, 上述两种方法都需要进行较为繁杂的初始化操作.

有一种更加高效的数据传输方式, 即通过将 同一块物理内存映射到不同的进程中 , 这意味着这两个进程均具有其完全读写权限.

shmid = shmget(key, sizeof(sharedMemoryBlock), 0666 | IPC_CREAT); //创建共享内存

int pid = fork();
if(pid == 0){
    sharedMemoryBlock *sharedData = (sharedMemoryBlock *)shmat(shmid, NULL, 0);
    //func to sharedData...
    shmdt(sharedData); //解绑共享内存
}else if(pid > 0){
    sharedMemoryBlock *sharedData = (sharedMemoryBlock *)shmat(shmid, NULL, 0);
    //func to sharedData...
    shmdt(sharedData);
    shmctl(shmid, IPC_RMID, NULL);
}

这种进行直接映射, 直接对内存进行操作的方式, 相比于其他方法效率较高.


Comparision of diffierent IPC methods


本文梳理了当前较为通用的进程间通信的方式.

这篇博文就到这里~


文章作者: MUG-chen
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MUG-chen !
  目录
加载中...