Android 筑基 - Linux 系统进程间通信

2,389 阅读17分钟

┗|`O′|┛ 嗷~~ 奥利给 ~~ 加其油来 ~~ 你还看的动!!!

进程间通讯算是一个老大难问题,哪哪都有它的身影,面试、开源库、业务组件、功能模块,今天给小白们科普一下

看了这篇内容,再看 Binder 原理就好理解了

学习资料

进程之间,内核空间是共享的

视频:

教学指导:

  1. 先看麦子学院,麦子学院老师会带你把除mmap之外所有 IPC 学一遍,简单易懂
  2. 传智黑马这里重点就是说 mmap 函数的,里面视频名字是错的,大家按顺序看就行

Linux 文件系统

在说其他之前,必须先看 Linux 的 File 文件系统,简单说一下:

Linux 系统中,只有内核才有影响的机器指令权限直接操作硬件,所以所有想要操作硬件的操作都会被中转到系统内核中执行,但后再返回结果给用户进程

具体到 File 也是一样,进程A想要打开一个 File文件 open("./myFile.txt"),那么该操作会通过系统服务API被转移到内核中执行,内核会在用户空间产生一个文件结构体,文件结构体内部有个缓冲区,缓冲区在内核空间,用来存放文件内容,你读写文件都是读写这个内核中的缓冲区

Linux 7种文件类型:

文件类型符号对应方法
普通文件_open()
目录文件dmkdir()
链接文件lln-s
管道文件pmkfifo()/pipe()
套接字文件s
字符设备文件c
块设备文件

其中:有名管道、字符设备、块设备、套接字都只有文件节点,不占磁盘空间

进程间为什么不能直接通信

因为每一个进程都有自己的、单独的、虚拟内存地址,即便进程A、B中相同的虚拟内存地址:0110,其纸箱的也是不同的物理地址,实际上数据是不同的

物理地址通过虚拟内存地址被认为分割,相互之间不能访问,数据在进程间不可见,为的就是安全,保护有些进程干坏事,偷窃、篡改、别的进程数据

进程间如何通信

这就需要借助上面说的 File 文件了,大家考虑这种情况:2个进程打开同一个文件

问: 2个进程同时打开同一个文件,获得的文件描述符是不一样的?

答: 2个进程打开同一个 File 文件,每个进程获得的文件描述符是不同的,文件描述符是每个进程独有的。但是同一个 File 文件映射到内核中的缓冲区有切只有一份

这样就留出了可操作的余地缓冲区成了一个桥梁,A进程写数据到缓冲区,B进程就可以从缓冲区把这个数据读取出来,进程间通信就是利用的 File 的机制来实现的,这个内核中的 File 对象类型不同,就是不同的进程间通信方式

所以有好多人说 Linux 进程通信都是基于文件 IO 的思路,内核空间是所有进程共享的,这是所有进程通信技术的大前提,总得有一个大家都能访问的中介,要不相互隔离的对象之间是真的无法实现通信的

linux 进程通信方式有如下几种:

  • 管道
    • 无名管道
    • 有名管道
  • 文件
  • 信号
  • 共享内存
  • 消息队列
  • Socket 套接字
  • mmap

注意 Socket,前面说的都是2个进程在同一个内核中的通信,而 Socket 指的是2个进程在通过2个内核通信,Socket 的2端,客户端、服务端各自都有格子的内核,IPC 实在2个内核之间实现的,本质还是操作内核中的文件结构体

来看看其他的解释:

我们 open() 函数打开或者创建、打开一个文件,内核空间就会开辟一块缓存 buffer,有了这个缓存,用户空间通过 write()、read() 函数就可以往缓存里读写数据,close() 函数就是释放这个缓存。Linux 中所有的 IPC 手段都是基于 IO 文件的,就是 open 函数的形式不一样

进程通信和线程通信区别:

  • 进程间通信: 在用户空间内是不可能实现的,只能通过 Linux 内核通信
  • 线程间通信: 在用户空间就可以实现,可以通过全局变量实现

现在最常用的几种 IPC 通讯:

  • 管道 -> 这种操作最简单,不用自己再做数据同步了
  • 信号 -> 系统开销最小,但是只能发系统制定的消息,一般是和别的 IPC 方式配套使用,尤其是共享内存
  • 共享内存 -> 真正可以在进程间传递各种数据的,需要信号辅助
  • Socket -> 最安全,要是保证数据完整性就得用这个

特征区别:

  • 管道、队列无需自己做同步
  • 文件、共享内存、mmap 需要自己做同步

管道

管道 是个特殊的文件,本质是1个队列,FIFE 先入先出算法,队列的左边是 write() 函数写入数据,队列的右边是 read() 函数读取数据,没有数据时 read() 读取的进程会被阻塞,进入浅睡眠状态

管道对象是存在在内核空间中的,以队列储存数据,以用户空间中文件结构体为锚点,改造文件在内核空间中的缓存为管道,这样实现2个进程之间的通信,一个进程写,一个进程读,

队列内部储存的是每一次写入的数据,这个模型看着就是 Linux 同步机制中介绍的:生产者、消费者模型,队列2端分别有2个信号量控制写入和读取,达到写入和读取的排他操作

特点:

  • 进程结束,空间释放,管道就不存在了
  • 管道中的东西,读完管道就删除了
  • 管道中没有东西可读,则会阻塞
  • 管道中没有空间可以写了,写操作也会阻塞,进程也会 S 浅睡眠状态

数据的流动是:进程A用户空间 -> 内核空间 -> 进程B用户空间,数据在内存中经历2次拷贝

1. 无名管道

使用:pipe() 函数创建无名管道

  • 参数: 需要传一个2个大小的 int 数组,里面是2文件描述父,一个用来写,一个用来读
  • 返回值: 0 成功,-1 失败
int file[2];
char value[] = "hello world!";
char readBuffer[128] = {0};

// 创建管道
int buffer = pipe(file);
if( buffer < 0 ){
	// 创建管道失败
    return -1;
}
// 往管道里写入数据
int result = write(file[0],value,sizeof(value));
if( result==-1 ){
	// 写入失败
}

// 创建子进程
int pid = fork();

if( pid==0 ){
	// 这里是子进程,读取管道中数据
	int result = read(file[1],readBuffer,sizeof(readBuffer));
    if( result==-1 ){
		// 读取失败
	}
}

pipe 的大小是 64K,可以通过命令查看:ulimit -a,但是也有说是 4K,反正必须是一页 4K 的倍数

缺点:

  • 只能实现父子进程之间的通信,因为只有父子进程之间才是内存一样,代码连续的,大家看无名管道这里,file[0],file[1] 这要是2个不相关的进程,去哪找 file[0],file[1] 这个对象去

2. 有名管道

上面介绍的无名管道无法实现无关系进程间通讯,自然还有可以实现无关系进程间通讯的方法,这就是有名管道了

使用:mkfifo() 函数创建有名管道

  • 参数: 文件名和文件访问权限
  • 返回值: 0 成功,-1 失败
char value[] = "hello world!";
char readBuffer[128] = {0};

// 创建有名管道
int buffer = mkfifo("./myfifo");
if( buffer<0 ){
	// 创建失败
    return -1;
}

// 打开文件
int file = open("./myfifo",O_WRONLY);
if( file<0 ){
	// 打开文件失败
    return -1;
}

// 写入数据
write(file,value,sizeof(value));

// 读取数据
read(file,readBuffer,sizeof(readBuffer))

特点:

  • 不占磁盘空间
  • mkfifo() 函数只是创建了文件节点,没有在内核中出创建管道
  • 只有 open() 函数打开这个文件,才会在内核总创建管道

有名管道可以实际的看到这个文件节点,因为不占磁盘空间,所以文件大小是0

信号

信号这东西挺好玩的,进程可以发送 Linux 规定好的信号类型到内核,内核在接到信号后会转发给制定的进程,进程会根据默认的 Liunx 定义好的行为行为 hanler 处理该信号,也可以自定义处理该信号,信号的执行是立马的,会打算正在执行的任务

看过上篇操作系统原理的同学,应该知道:中断是进程从用户内存切换到内核进程的唯一手段,针对不同的中断类型,系统封装好了很多系统调用 API 给用户进程使用。软件也可以发送软中断,被成为80中断

信号利用的就是软中断,Linux 系统定义好了64种信号: 我们可以通过发送给制定进程信号的方式实现进程间通信,手段很巧妙,信号整体看很像 Android handle 发送系统制定任务

发送信号

  • kill() 函数

    • 参数:
      • PID 接收信号的进程ID
      • SIG 信号ID
    • 返回值:0 成功,-1 失败
  • raise() 函数 -> 只能发给自己的进程

    • 参数:
      • SIG 信号ID
    • 返回值:0 成功,-1 失败
  • alarm() 函数 -> 只能发给自己,并且只能发送定时消息

    • 参数:
      • second 延迟时间
    • 返回值:0 成功,-1 失败

kill() 函数可以这个真正的跨进程通信,raise()、alarm() 都是只能自己接收,raise() 函数相当于 raise(getpid(),SIG)

有一点要注意,进程结束之后是接收不到信号的,想接收信号我们必须保证进程还活着,一般可以使用 pause(),sleep() 函数

kill、raise、waitpid 一般都是结合使用,实现父子进程之间相互通信、控制的,代码很简单我就不写了

键盘就是通过硬件中断,调起驱动,然后驱动发送一个 SIGINT 信号

自定义接收信号

  • signal() 函数 -> 在接收到信号后,可以不执行默认操作,可以执行自己想要的操作

    • 参数:
      • SIG 信号ID
      • handler 自定义处理函数:SIG_IGN 忽略信号、SIG_DFL 执行默认处理
    • 返回值:0 成功,-1 失败

这个写段代码大家体会下:


void handler(int sig){
	// 自定义处理
}

// 注册自定义信号处理方法
signal(9,handler);

// 创建子进程
int pid = fork();
if( pid<0 ){
    // 这里是子进程,向父进程发信号
	kill(getppid(),9);
}

大概其就是这个意思,linux 的信号使用很简单,看着就像是 Java Thread 控制方法一样,通过信号可以实现进程间的消息传递,告诉对面要干活啦

共享内存

在内核空间中创建一个缓存,然后在用户空间中也对应一块缓存,然后实现2者之间的内存地址映射,也就是那内核缓存分配到的物理内存地址交给了用户程序。2个进程之间再都使用同一块共享内存对象,这样2个进程中都有一块地址实际指向相同的物理地址,任何一方的内存操作都会影响另一个进程

注意共享内存没有内存拷贝的问题,也是大家总说的:0拷贝。整个过程,所有的操作都是在一块物理内存上完成的,内核起到了一个衔接作用

特征:

  • 即可以实现父子进程之间通信,也可以实现无关联进程之间通信
  • 共享内存需要用户自己处理同步问题,不像管道已经帮你处理好了,这里要自己做,所以实现逻辑复杂,同步容易写错、出问题
  • 共享内存对象创建之后,对象一直都在内核中,直到被删除
  • 共享内存对象和管道不一样,读取后,数据还在共享内存中

使用步奏: ->

  • 父子进程通信:
    • 1. shmget() 在内核空间中创建共享内存对象
    • 2. shmat() 在用户空间中开辟一块空间,实现和内核空间共享内存对象的内存地址映射
  • 无关联进程通信:
    • 1. key=ftok(filename) 生成一个key,实际是在创建一个文件,使用文件在内核中的缓存作为共享内存对象
    • 2 shmget(key)共享内存对象个文件关联
    • 3 shmat() 在用户空间中开辟一块空间,实现和内核空间共享内存对象的内存地址映射

命令:

  • ipcs -m: 查看系统中所有共享内存对象
  • ipcrm -m id: 删除制定共享内存对象

shmget() 函数 ->

* 作用:在内核中创建共享内存对象
* 参数:
    * key 
    	* IPC_PRIVATE:父子进程通信模式
        * key=ftok(filename):无关系进程通信模式
    * size
    * flag 操作权限
* 返回值:ID,-1 失败

ftok() 函数 ->

* 作用:生成一个文件,返回一个 key,方便后面使用文件作为共享内存对象
* 参数:
    * filepath:文件历经
    * key:随便写
* 返回值:key,-1 失败

shmat() 函数 ->

* 作用:实现共享内存地址和用户空间地址映射 
* 参数:
    * shmid:共享内存ID
    * addr:用户空间起始地址,NULL 系统自动分配用户空间地址
    * shmflag:操作权限,0 可读写,SHM_RDONLY 只读
* 返回值:NULL 失败

shmdt() 函数 ->

* 作用:删除用于映射的用户空间块
* 参数:
    * addr:用户空间地址
* 返回值:-1 失败

shmctl() 函数 ->

* 作用:删除用于映射的内核空间块
* 参数:
	* shmid:共享内存ID
    * cmd:标记 
    	* IPC_STAT 获取属性
        * IPC_SET 设置属性
        * IPC_RMID 删除对象
    * shm_dt:结构体,1、2选项得到的数据写到这个参数里
* 返回值:-1 失败

fgets() 函数 ->

* 作用:从键盘写入数据
* 参数:
    * shmid:共享内存ID
    * size:文件大小
    * stdin:来自硬盘 
* 返回值:-1 失败

memcpy() 函数 ->

* 作用:写入数据
* 参数:
    * shmid:共享内存ID
    * value:数据
    * size:大小
* 返回值:-1 失败

printf() 函数 ->

* 作用:读取数据
* 参数:
    * shmid:共享内存ID
* 返回值:

下面的例子不是同步控制不是特别严谨,是按照发送端先运行,接收端再运行的顺序执行的

父子进程通信

父进程不停的从键盘写入数据,子进程读取数据,中间2个者用 kill() 信号做通知,同步处理的很随意,实际要严谨更多,这里看个意义

父子间进程通信,一般用一个共享内存对象做单向数据传递,双向数据传递用2个共享内存对象

// 自定义信号处理,唤醒自己后什么也不做,让进程继续执行就可以了
void hand(int sig){
	return 1;
}

char *buffer;
int shmid = shmget(IPC_PRIVATE,128,ipc_create | 0777);
if(shmid<0){
	// 创建失败
    return -1;
}

int cid = fork();
if(cid>0){
	// 父进程执行
    // 映射共享内存
    buffer = (char *)shmat(shmid,NULL,0);
    // 注册信号处理
    signal(2,hand);
	while(1){
    	// 循环键盘写,然后通知接收端读取
    	fgets(buffer,128,stdin);
        // 发送信号通知接收端我写完了,你可以读了
        kill(cid,1);
        // 完事暂停自己
        pause();
    }
}

if(cid==0){
	// 子进程执行
    // 映射共享内存
    buffer = (char *)shmat(shmid,NULL,0);
    // 注册信号处理
    signal(1,hand);
	while(1){
    	pause();
    	// 循环读
    	printf(buffer);
        // 发送信号通知发送端我读完了,你可以写了
        kill(getppid(),2);
    }
}

// 释放用户空间
shmdt(buffer);
// 释放内核空间
shmctl(shmid,IPC_RMID,null);

无关系进程通信

看了上面的父子之间进程通信的例子,大家这里会想到也是用信号来做进程控制吧,那 pid 怎么办,相互不知道啊,思路是这样的 ->

  1. 发送端一上来先把自己的PID写到共享内存中,然后等待
  2. 接收端一启动就去共享内存中把pid拿出,然后把自己的pid写进去,用拿到的pid通知对面
  3. 发送端被唤醒,从共享内存中拿到对面进程的pid,这下就能实现信号通信了

发送端

struct bufffer{
	int pid;
    char buffer[124];
}

int oid;

// 自定义信号处理,唤醒自己后什么也不做,让进程继续执行就可以了
void hand(int sig){
	return 1;
}

int key = ftok("./a.c","a");
if(key<0){
	// 创建失败
    return -1;
}

int shmid = shmget(key,128,ipc_create | 0777);
if(shmid<0){
	// 创建失败
    return -1;
}

// 映射共享内存
struct buffer = (struct *)shmat(shmid,NULL,0);
if(buffer == null){
	// 映射失败
    return -1;
}

// 注册信号处理
signal(2,hand);

// 先把自己的pid写到共享内存中
buffer->buffer = getpid();
// 然后等待
pasue();
// 唤醒之后拿到另一端的pid,这样就可以用pid实现控制了
oid = buffer->pid;

while(1){
	// 循环键盘写,然后通知接收端读取
	fgets(buffer->buffer,128,stdin);
	// 发送信号通知接收端我写完了,你可以读了
	kill(oid,1);
 	// 完事暂停自己
	pause();
}

// 释放用户空间
shmdt(buffer);
// 释放内核空间
shmctl(shmid,IPC_RMID,null);

接收端

struct bufffer{
	int pid;
    char buffer[124];
}

int oid;

// 自定义信号处理,唤醒自己后什么也不做,让进程继续执行就可以了
void hand(int sig){
	return 1;
}

int key = ftok("./a.c","a");
if(key<0){
	// 创建失败
    return -1;
}

int shmid = shmget(key,128,ipc_create | 0777);
if(shmid<0){
	// 创建失败
    return -1;
}

// 映射共享内存
struct buffer = (struct *)shmat(shmid,NULL,0);
if(buffer == null){
	// 映射失败
    return -1;
}

// 注册信号处理
signal(1,hand);

// 先把共享中另一个进程的pid拿出来
oid = buffer->buffer;
// 再把自己的pid写进去
buffer->buffer = getpid();
// 发送通知,告知对面拿pid
kill(oid,2);

while(1){
    pause();
    // 循环读
    printf(buffer->buffer);
    // 发送信号通知发送端我读完了,你可以写了
    kill(oid,2);
}

// 释放用户空间
shmdt(buffer);
// 释放内核空间
shmctl(shmid,IPC_RMID,null);

大家看到了吧,共享内存虽然 0拷贝 性能好,但是数据控制需要自己做,同步逻辑复杂,很容易就写错了

队列

队列看名字就知道是在内核中创建一个队列,可以实现父子进程、无关进程之间的通信,API 使用和共享内存差不多,区别是函数名不同

特点:

  • 可以实现父子进程、无关进程之间的通信
  • 队列中可以存储多个类型的消息,消息的格式是 struct 结构体
  • 队列已经实现了同步,读阻塞和写阻塞

上面说到管道也是队列,但是管道的队列有限制,必须是 FIFO 的,并且队列中的数据只能存一头写,另一头存,但是队列可以2头都读写

实现队列实现双向通信的话,一般是采用对线程,进程内一个线程读,一个线程写,这样才能2不耽误

命令:

  • ipcm -q: 查所有队列对象

msgget() 函数 ->

* 作用:在内核中创建队列对象
* 参数:
    * key 
    	* IPC_PRIVATE:父子进程通信模式
        * key=ftok(filename):无关系进程通信模式
    * flag 操作权限
* 返回值:ID,-1 失败

msgctl() 函数 ->

* 作用:删除队列
* 参数:
	* msgid:队列ID
    * cmd:标记 
    	* IPC_STAT 获取属性
        * IPC_SET 设置属性
        * IPC_RMID 删除对象
    * shm_dt:结构体,1、2选项得到的数据写到这个参数里
* 返回值:-1 失败

magsnd() 函数 ->

* 作用:发送数据
* 参数:
    * msgid:队列ID
    * value:数据结构体
    * size:结构体正文字节数
    * flag:
    	* IPC_NOWAIT:非阻塞方式,消息没发完函数也会返回
        * 0:阻塞方式,消息发完之后函数才会返回
* 返回值:-1 失败

队列消息结构体 ->

* 必须要有 type 这个参数
struct msgbuf{
    long type:消息类型
    char buf[xxx]; 消息正文
}

magrcv() 函数 ->

* 作用:读取数据
* 参数:
    * msgid:队列ID
    * msgbuf:接收数据缓冲区
    * size: 消息正文大小
    * msgtype: 消息类型
    	* 0:队列中第一个数据
        * >0:队列中第一个匹配类型的数据
        * <0:
    * flag:
    	* IPC_NOWAIT:非阻塞方式,消息没发完函数也会返回
        * 0:阻塞方式,消息发完之后函数才会返回
* 返回值:-1 失败

无关队列通信

  • 发送端:
struct msgbuf{
	long type;
	char data[128];
}

struct buffer;
// 设置消息类型
buffer.type = 100;

int key = ftok( "./a.c","a" );
int msgid = msgget(key,IPC_CREAT | 0777);

while(1){
	// 每次先把本地缓存清理掉
	memset(buffer.data,0,128);
    // 循环从键盘读取数据
	fget(buffer.data,128,stdin);
    // 发送数据到消息队列
	msgsnd(msgid,(void *)buffer,strlen(buffer.data));    
}

msgctl(msgid,IPC_RMID,0);
  • 接收端:
struct msgbuf{
	long type;
	char data[128];
}

struct buffer;

int key = ftok( "./a.c","a" );
int msgid = msgget(key,IPC_CREAT | 0777);

while(1){
	// 每次都先把本地缓存清理掉
	memset(buffer.data,0,128);
    // 从队列读取数据
	msgrcv(msgid,(void *)buffer,100,0);    
}

msgctl(msgid,IPC_RMID,0);

队列比共享内存好用多了,不用复杂的同步控制,但是性能没有优势,内存也是要拷贝2次

信号灯

信号灯是信号量的集合,实际使用也是操作信号等中某个信号量

命令:

  • ipcs -s: 查看所有信号灯

代码实在懒得写了,和上面差不多,麻烦的是方法里面有2个结构体,每次操作信号灯中的信号量都要把信号量包装成结构体,改好数据再写入信号灯中

MMAP 共享存储

利用 mmap() 函数、内存映射机制,一样也能实现进程通信

共享存储 ->

这个就是大家总说也老也玩不转,但是哪哪都还有的 mmap() 函数,swap 这个东西了。共享存储本身就是指文件,打开一个文件,系统会在内核空间中创建一个缓冲区用户缓存文件读写的数据,所有对于文件的读写都是于内核空间中这个缓存区的交互。共享储存进一步实现了用户进程与这个文件内核空间中缓冲区的内存地址映射,在用户空间开辟一块内存空间,该内存空间指向内核缓冲区所在的物理内存,这样任何对于该用户空间区域的操作都是对内核缓冲区的操作,无形中这样可以较少1次数据从用户空间到内核空间的拷贝

如果多个进程之间都实现了对与同一个文件的共享存储,进程A用户空间缓冲、进程B用户空间缓冲,内核文件缓冲区就都指向了同一块物理内存,那么进程A在自己用户空间中对该缓冲的写入就是对进程B该缓冲的写入,不用再向管道那样,数据要经历:A用户空间 -> 内核 -> B用户空间,2次内存拷贝的操作了。和共享内存实际如出一辙,0拷贝

2次内存拷贝

结合管道、共享内存大家理解下,mmap 赋予我们操作内存地址一样,操作文件数据,文件只能使用 write、read,但是内存地址操作就有太多的函数可以使了

文件描述符和mmap映射区都保存在用户空间高位内核态PCB中

特征:

  • 需要自己同步数据

mmap() 函数 ->

* 作用:实现用户空间地址和文件的映射,任何对内存的修改都会立马同步到文件
* 参数:
    * add:映射区内存首地址,默认 Linux 内核指定,传 NULL
    * leng:映射区大小
    * port: 文件操作权限
    	* PROT_READ
        * PROT_WRITE
        * PROT_READ|PROT_WRITE
        * PROT_EXEC:可执行,一般这是库函数用的
        * PROT_NONE:不允许访问,这是驱动用的
    * flag: 映射区操作权限
    	* MAP_SHARED:映射区的修改会反应到磁盘
        * MAP_PRIVATE:映射区的修改不会反应到磁盘,该模式不能实现进程间通信
        * MAP_NONYMOUS:建立匿名映射区
    * fd:文件描述符
    * offset:映射文件偏移量,4K整数倍,不一定非淂把整歌文件映射进去,可以只映射一部分,文件从 offset 开始,映射 leng 这么长。offset=4096,size=len-4096  	
* 返回值:映射区首地址,-1 失败

munmap() 函数 ->

* 作用:删除映射区
* 参数:
    * add:映射区内存首地址,默认传 NULL
    * leng:映射区大小 	
* 返回值:-1 失败

父子进程通信

因为父子进程之间内存的关联,只要在 fork() 之前 mmap(),那父子进程共享映射区,因为内存都是一模一样的嘛~

// 创建文件
int file = open( "./testmap", O_REWR|O_CREAT,0777);
// 设置文件大小
ftruncate(file,128);
// 计算文件大小
int len  = lseek(file,0,);

// 创建 mmap 映射
char *buffer = mmap(NULL,len,PROC_WRITE|PROC_READ,MAP_SHARED,file,0);
if(buffer==MAP_FAILED){
	// 映射失败
    return -1;
}

int pid = fork();
if(pid>0){
	// 父进程操作
    // 指针写数据
    strcpy(buffer,"aaa");
}

if(pid==0){
	// 子进程操作
    // 读数据
    printf(buffer);
}

// 释放映射区
munmap(buffer,len);
// 关闭文件
close(file);

匿名映射

Linux 提供一歌宏:MAP_NONYMOUS,mmap() 函数就不再需要自己 open() file 了

// 创建 mmap 映射
char *buffer = mmap(NULL,128,PROC_WRITE|PROC_READ,MAP_SHARED|MAP_NONYMOUS,-1,0);
if(buffer==MAP_FAILED){
	// 映射失败
    return -1;
}

int pid = fork();
if(pid>0){
	// 父进程操作
    // 指针写数据
    strcpy(buffer,"aaa");
}

if(pid==0){
	// 子进程操作
    // 读数据
    printf(buffer);
}

// 释放映射区
munmap(buffer,128);

无关进程通信

  • 发送端
// 创建文件
int file = open( "./testmap", O_REWR|O_CREAT,0777);
// 设置文件大小
ftruncate(file,128);
// 计算文件大小
int len  = lseek(file,0,);

// 创建 mmap 映射
char *buffer = mmap(NULL,len,PROC_WRITE|PROC_READ,MAP_SHARED,file,0);
if(buffer==MAP_FAILED){
	// 映射失败
    return -1;
}

memcpy(buffer,"aaa",6);

// 释放映射区
munmap(buffer,len);
// 关闭文件
close(file);
  • 接收端
// 创建文件
int file = open( "./testmap", O_REWR|O_CREAT,0777);
// 设置文件大小
ftruncate(file,128);
// 计算文件大小
int len  = lseek(file,0,);

// 创建 mmap 映射
char *buffer = mmap(NULL,len,PROC_WRITE|PROC_READ,MAP_SHARED,file,0);
if(buffer==MAP_FAILED){
	// 映射失败
    return -1;
}

printf(buffer);
// 释放映射区
munmap(buffer,len);
// 关闭文件
close(file);