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封装程度已经很高,实现一遍也会有收货.从前只是通过概念学习进程间通信,如今通过代码可以加深理解.