操作系统 Chap.5 进程间通信
1. 进程间通信(IPC)
一个系统中的多个进程是可以单独运行的, 但同时也存在相互影响的情况(比如通过进程间的数据交换 / 共享来协同工作).
为啥要有合作进程? 这其实跟我们此前提到的多线程(Multithread)出现的理由差不多:
- 信息共享
- 提高效率
- 功能模块化
- 便携性
当然, 合作进程存在的前提在于进程间能够真正做到共享信息, 这就引出了本文的主题: IPC(Interprocess communication / 进程间通信).
1.1 同步机制(Synchronization)
通信就通信吧, 干嘛同步啊?
我们需要明确, 进程间通信的根本需求是要共同完成某个任务, 因此进程之间的信息需要实时更新且保持一致, 否则就得出乱子.
现在很多进程都能访问同一块数据, 我们要确保它们看到的数据都是一致的, 这就需要我们对 不同进程处理数据的方法与顺序 进行详尽的设计.
2. 管道(Pipe)
我们接下来进入进程间通信的第一种方式, 管道通信.
2.1 定义
顾名思义, 所谓管道就是一个很长的字节流, 一端写入, 一端读出. 该消息传递方式没有 消息边界 这一概念.
所谓 消息边界 , 个人的理解是, 比如写入端是一行一行写入的, 这一点无法直接提现到读出端上, 因为读出端只能通过一个字节流来读取另一端传递过来的信息. 因此如果需要分割消息, 这个方法可能需要自行定义.
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: 字符数组(其实就是字节数组)
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);
}
这种进行直接映射, 直接对内存进行操作的方式, 相比于其他方法效率较高.
本文梳理了当前较为通用的进程间通信的方式.
这篇博文就到这里~