python使用mmap模块实现进程间通信与文件映射

2022-09-16 10:36:50

1. 共享内存完成IPC(进程间通信)

1.1 操作mmap object实例

import mmap# 创建一个文件with open('hello.txt','wb')as f:
    f.write(b"Hello Python!\n")
with open('hello.txt','r+b')as f:# mmap基本上接收两个参数,(文件描述符,读取长度),size 为0表示读取整个文件
    mm = mmap.mmap(f.fileno(),0)# 标准读取方式
    print(mm.readline())# prints "Hello Python!"# 切片读取方式
    print(mm[:5])# prints "Hello"# 切片方式改变文件内容;# 注意size长度匹配
    mm[6:] =b" world!\n"# 使用seek定位光标到数据头,当前光标已到数据末(mm.tell()可获取当前光标)
    mm.seek(0)# 再次标准读取
    print(mm.readline())# prints "Hello  world!"# 像处理文件一样关闭mmap映射
    mm.close()

输出结果

b’Hello Python!\n’
b’Hello’
b’Hello world!\n’

从help(mmap.mmap)的注释文档来看, 得益于python的”鸭子类型”. 既可以像str类型一样操作mmap object, 也可以像读入的file一样使用read和write.

1.2 使用mmap完成共享内存方式的进程间通信

import mmapimport osimport time

mm = mmap.mmap(-1,27)#传入文件描述符-1,使用匿名映射
mm.write(b"Original msg")# 涉及mm的读写都要记得字符串的二进制
print('write successfully')

pid = os.fork()if pid ==0:# 子进程中
    mm.seek(0)
    print('Read from the mmap:')
    print(mm.readline())#以切片访问时,严格按照字符串长度
    mm[12:] =b' sth from child'
    mm.close()else:
    time.sleep(2)# 用sleep使子进程先执行
    mm.seek(0)
    print('Read from the child:')
    print(mm.readline())
    mm.close()

输出

write successfully
Read from the mmap:
b’Original msg\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00’
Read from the child:
b’Original msg sth from child’

当年理解fork花了不少时间,与其说父子进程,不如说兄弟进程.
从英文版入手,fork本身就是分叉的意思.运行到fork便分叉运行.这样理解自然更容易入手.
共享内存主要基于两点:
1) 子进程继承父进程的变量的副本;
2) mmap返回指向文件的指针,本来在子进程中操作的变量是不会影响父进程的.但是因为mmap代表的是一种映射关系. 犹如从fork切换到子进程时,给子进程复制了一把指向与父进程同一块内存的钥匙,虽然两把钥匙是独立的,但是凭借两把钥匙,两个进程可以在同一个内存区域上进行修改,从而完成进程间通信.

mmap共享内存两种方式:
1) 匿名映射用于有亲缘关系的进程间通信;
2) 文件映射使不同进程修改同一块内存区域,实现映射.

2. 内存映射读取文件

上面的例子使用的是mmap的匿名映射功能, mmap总共有两个用途:
1) 匿名映射于不同进程间共享进程内变量;
2) 传入文件描述符, 加速文件读取. 使用read系统调用,需要操作系统从磁盘文件复制到内核缓冲区,再从内核缓冲区复制到用户缓冲区.而mmap则采用预先在内存中分配空间的方式, 利用系统的缺页中断加载文件, 不需要经过内和缓冲区,理论上耗时是read()的一半. 与read系统调用的区别可以参考mmap内存映射

实际上功能1) 也是包括在功能2) 中,传入的文件完成映射之后也可以在进程间共享.

实践代码:
Tips: jupyter notebook当中, 魔术方法“%%time”可以统计cell块的运行时间,该好好利用
实验文件: 另外一次数据实验当中以二进制形式pickle在硬盘的文件,总大小400MB.

import mmapimport pickle

常规方法, 由pickle模块对传入的file-like object执行read()

%%timewith open("_local/history","rb")as f:
    history = pickle.load(f)with open("_local/w_users","rb")as f:
    w_users = pickle.load(f)with open("_local/w_users","rb")as f:
    w_items = pickle.load(f)
Walltime:2.68 s

删除变量 防止系统快表读取

del historydel w_usersdel w_items

使用mmap映射

%%time#使用"rb"方式会触发"WIN error 5"拒绝访问错误,应使用"r+b"with open("_local/history","r+b")as f:
    mapped_f = mmap.mmap(f.fileno(),0)
    history = pickle.load(mapped_f)
    mapped_f.close()with open("_local/w_users","r+b")as f:
    mapped_f = mmap.mmap(f.fileno(),0)
    w_users  = pickle.load(mapped_f)
    mapped_f.close()with open("_local/w_users","r+b")as f:
    mapped_f = mmap.mmap(f.fileno(),0)
    w_items = pickle.load(mapped_f)
    mapped_f.close()
Walltime:1.37 s

后记: 首次读取, mmap效率比read要高得多. 如果重复多次读取文件, 即使执行了del删除用户缓冲区内的变量, read方法可以利用内核缓冲区内的系统缓存, 直接从内核缓冲区复制到用户缓冲区(这也是系统调用的read/write操作需要经过内核缓冲区的原因——维护文件缓存,提高系统效率), 因为是完全内存中的操作, 在机器上测试平均在900ms完成上述读取, mmap始终要进行磁盘寻址, 重复执行,平均时间维持在1.4s不变.

随手记: 凡是计算机的概念理解都不能脱离代码,即使API封装程度已经很高,实现一遍也会有收货.从前只是通过概念学习进程间通信,如今通过代码可以加深理解.

  • 作者:chrispink_yang
  • 原文链接:https://blog.csdn.net/m0_37422289/article/details/79895526
    更新时间:2022-09-16 10:36:50