1. 进程间通信方式
- 管道(使用最简单)
- 信号(开销最小)
- 共享内存(无血缘关系进程通信)
- 本地套接字(最稳定)
2. 管道之pipe
2.1 管道的原理
- 管道的本质是一个伪文件(实际上是内核缓冲区)
- 它有两个文件描述符引用,一个表示读端,一个表示写端。规定数据从管道的写段流入管道,从读端流出。
- 管道实际上是内核使用环形队列机制(先进先出),借助内核缓冲区(默认4k,可以通过命令ulimit -a可查看)实现
补充:什么是伪文件
- - 文件
- d 目录
- l 符号连接
- s 套接字
- b 块设备
- c 字符设备
- p 管道
前三种占用磁盘存储,后四种是伪文件,伪文件不占用磁盘存储。
2.2 管道的局限性
- 管道的两端连接着两个进程,所以一个进程不能自己读管道还自己写管道
- 数据一旦被读走,便不在管道中存在,不可反复读取
- 由于管道采用半双工通信方式。因此数据只能在一个方向上流动
- 只能在有公共祖先的进程间使用管道
补充:通信方式:
- 单工通信:单向通信
- 半双工通信:双向通信,但是一方发送的时候另一方不能也同时发送
- 全双工通信:双向通信,接收发送可以同时进行
2.3 创建管道
int pipe(int pipefd[2]);
- 返回值:int类型,成功返回0,失败返回-1
- pipefd[2]: 传出参数,管道的双端的文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
int fd[2];
pid_t pid;
int i;
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error:");
exit(1);
}
for (i = 0; i < 2; i++){
pid = fork();
if (pid == -1) {
perror("pipe error:"); //ls | wc -l
exit(1);
}
if (pid == 0)
break;
}
if (i == 0) { //兄 ls
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
} else if (i == 1) { // 弟 wc -l
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
} else if (i == 2) { //父
close(fd[0]);
close(fd[1]);
for(i = 0; i < 2; i++)
wait(NULL);
}
return 0;
}
2.4 读管道与写管道
- 读管道
- 管道中有数据,read返回实际读到的字节数
- 管道中无数据:
- 如果管道写端全部关闭,read返回0(如同读到文件结尾)
- 如果管道写端没有全部关闭,read阻塞等待
- 写管道
- 管道读端全部关闭,进程会异常终止
- 管道读端没有全部关闭:
- 管道已满,write阻塞
- 管道未满,write将数据写入,并返回实际写入的字节数
2.5 管道的优劣
- 优点
- 缺点
- 只能单向通信,双向通信需要建立两个管道
- 只能用于父子进程,兄弟进程之间通信
3. 管道之FIFO
3.1 原理
- FIFO是linux基础文件类型的一种。但FIFO文件在磁盘上没有数据块,仅仅用来表示内存中一条通道。各进程可以打开这个文件进程read/write
- 实际上是在读写内核通道,实现了进程间通信
3.2 创建方式
- 方式一:通过命令 mkfifo 管道名
- 方式二:库函数
int mkfifo(const char *pathname,mode_t mode)
- 一旦创建了使用mkfifo创建了一个有名管道,就可以使用open打开它。
- 常见的文件I/O函数都可用于fifo。如close、read、write、unlink等
4. 共享存储映射(共享内存)
4.1 原理
- 使一个磁盘文件与内存空间中的一个缓冲区相映射。于是当从缓冲区中取数据,将相当于读文件中的相应字节。
- 与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可以在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
- 使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
4.2 mmap函数
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
- addr:建立映射区的首地址,由linux内核指定。使用时,直接传递null
- length:欲创建映射区的大小(其实是虚拟地址大小)
- prot:映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
- flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
- MAP_SHARED:会将映射区所做的操作反映到物理设备上
- MAP_PRIVATE:映射区所做的修改不会反映到物理设备。
- fd:用来建立映射区的文件描述符
- offset:映射文件的偏移(必须设置为4k的整数倍)
- 返回值:
- 成功:返回创建映射区的首地址
- 失败:MAP_FAILED宏
4.3 munmap函数
- 同malloc函数申请内存空间类似,mmap建立的映射区在使用结束后也应调用类似free的函数来释放内存
int munmap(void *addr,size_t length);
4.4 使用mmap要注意的事项
- 创建映射区的过程,隐含了一次对映射文件的读操作
- 当flags为MAP_SHARED时,要求:映射区的权限<=文件打开的权限(防止对文件的写入会报错)。
- 当flags为MAP_PRIVATE时则无所谓,因为mmap中的权限时对内存的限制。
- 当映射文件的大小为0时,不能创建映射区。所以用于映射的文件必须要有实际大小。
- munmap传入的地址一定要是mmap的返回地址。
- 文件偏移量必须为4k的整数倍(因为映射区是内核帮我们创建,有mmu完成地址转换,mmu映射的单位就是4k)
- mmap创建映射区出错概率非常高,一定要检查返回值。确保映射区建立成功再进行后续操作
- mmap映射成功后,即使关闭文件描述符,也不会对mmap映射有影响。因为内存与磁盘的映射已经建立。
4.5 当利用mmap进行父子进程通信时flags参数意义
- 当父子等有血缘关系的进程利用mmap建立映射区来完成数据通信时,flags标志位的意义就发生了变化
- MAP_PRIVATE:私有映射,父子进程各自独占映射区
- MAP_SHARED:共享映射,父子进程共享映射区。
- 如果希望父子进程利用mmap实现数据通信,可以将flags标志位设置为MAP_SHARED
4.6 匿名映射
- 前面利用mmap创建共享映射的过程中还是需要依赖一个文件,需要很多的文件操作,但实际使用的时候又用不到这个文件。所以如何解决这个问题?可以通过匿名映射来解决这个问题
- 匿名映射利用flags参数来实现的
int *p=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0)
- 在flags参数中
- 文件描述符填-1,因为匿名映射,不需要文件描述符了。
补充:类Unix系统如何实现匿名映
fd=open("/dev/zero",O_RDWR);
p=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
4.7 mmap实现无血缘关系进程间通信
- 将flags参数设置为MAP_SHARED。
- 在共享模式下,操作系统会将这个映射缓冲区的虚拟地址映射在3~4G的内核空间。
- 因为多个进程虽然每个都有自己的虚拟地址空间,但是3~4G为内核空间,所有线程共享。
|