前言
进程(process)是系统进行资源分配和调度的基本单位,每个进程都有自己的地址(内存)空间(由CPU分配),处于安全的考虑,不同进程之间的内存空间是相互隔离的,也就是说 进程A 是不能直接访问 进程B 的内存空间。但某些场景下,不同进程间需要相互通信,该怎么办呢?即然进程间不能直接通信,那就借助第三方来完成通信,这个第三方就是系统的内核。
内核提供了多种方式来完成进程间的通信,比如管道、消息队列、共享内存、信号量等。本文主要介绍 管道(pipe) 的原理及os.pipe()实现。
ps:本文中的代码需要在linux系统中运行。
1. 模拟管道通信
管道本质上就是内核中的一个缓存,当进程创建一个管道后,系统会返回两个文件描述符,可以通过这两个描述符往管道写入数据或者读取数据。管道是一种单向通信,即管道的一端只用于读数据,另一端只用于写数据,只有写完数据之后另一个进程才能去读。
模拟一下管道的读数据和写数据:
import os
input_data = 'hello world!'.encode('utf-8')
if __name__ == '__main__':
r, w = os.pipe() # 创建管道
os.write(w, input_data) # 向管道写入数据
# os.close(w)
# 如果没有数据会进入等待状态(阻塞)
output_data = os.read(r, len(input_data)) # 从管道中读数据
print(output_data.decode('utf-8'))
# os.close(r)
# hello world!
os.pipe() 创建一个管道,返回一对分别用于读取和写入的文件描述符(r, w),新的文件描述符是 不可继承 的。
os.read(fd, n) 从文件描述符 fd 中读取至多 n 个字节,返回所读取字节的字节串(bytestring),如果到达了 fd 指向的文件末尾,则返回空字节对象。
os.write(fd, str) 将 str 中的字节串(bytestring)写入文件描述符 fd,返回实际写入的字节数。
os.read(fd, n)和os.write(fd, str)适用于低级I/O操作,必须用于os.open()或pipe()返回的文件描述符。
除了上述对管道进行读写外,os库还提供了fdopen()函数来创建文件对象,再使用对象来进行相关操作。
import os
import struct
input_data = 'hello world!'.encode('utf-8')
if __name__ == '__main__':
r, w = os.pipe()
writer = os.fdopen(w, 'wb')
writer.write(input_data) # 写入数据
writer.flush() # 刷新(写入数据之后必须手动刷新)
# writer.close()
reader = os.fdopen(r, 'rb')
output_data = reader.read(len(input_data)) # 读取数据
print(output_data.decode('utf-8'))
# reader.close()
# hello world!
os.fdopen(fd, *args, **kwargs) 返回打开文件描述符 fd 对应文件的对象,类似 open() 函数,二者接受同样的参数,不同之处在于 fdopen() 第一个参数是整数(文件描述符是整数类型的)。
2. 实现进程间的单向通信
进程间的管道通信过程大致如下:
(1) 父进程使用 pipe函数 通过系统调用创建一个管道;
(2) 父进程使用 fork 函数 通过系统调用创建两个子进程;
(3) 两个子进程可以通过管道进行通信。

这里简化生产者与消费者的例子来模拟进程间的单向通信,main 代码实现如下:
# main.py
import os
import sys
import subprocess
if __name__ == '__main__':
r, w = os.pipe()
cmd1 = [sys.executable, "-m", "consumer", str(r)]
cmd2 = [sys.executable, "-m", "producer", str(w)]
proc1 = subprocess.Popen(cmd1, pass_fds=(r, )) # 在一个新的进程中执行子程序
proc2 = subprocess.Popen(cmd2, pass_fds=(w, ))
print('parent process pid: ', os.getpid())
print('child process pid(proc1): ', proc1.pid)
print('child process pid(proc2): ', proc2.pid)
proc1.wait() # 等待子进程被终止
proc2.wait()
producer 代码实现如下:
"""负责写数据"""
import os
import sys
import struct
writer = os.fdopen(int(sys.argv[1]), "wb")
input_data = 'hello world!'.encode('utf-8')
writer.write(struct.pack("<i", len(input_data))) # 小端模式, 低地址存储 b'\x05\x00\x00\x00'
writer.write(input_data)
writer.flush() # 刷新(写入数据之后必须手动刷新)
print('input data: ', input_data.decode('utf-8'))
consumer 代码实现如下:
"""负责读数据"""
import os
import sys
import struct
reader = os.fdopen(int(sys.argv[1]), "rb")
len_data = reader.read(4) # int占用4个字节
recv_bytes = struct.unpack("<i", len_data)[0]
output_data = reader.read(recv_bytes)
print('output data: ', output_data.decode('utf-8'))
运行结果如下:

3. 实现进程间的双向通信
如果使用os.pipe()函数实现双向通信,则需要创建两个管道。

main 代码实现如下:
# main2.py
import os
import sys
import subprocess
if __name__ == '__main__':
# connect subprocess with a pair of pipes
worker1_read, worker2_write = os.pipe()
worker2_read, worker1_write = os.pipe()
cmd1 = [sys.executable, "-m", "client", str(worker1_read), str(worker1_write)]
cmd2 = [sys.executable, "-m", "server", str(worker2_read), str(worker2_write)]
proc1 = subprocess.Popen(cmd1, pass_fds=(worker1_read, worker1_write)) # 在一个新的进程中执行子程序
proc2 = subprocess.Popen(cmd2, pass_fds=(worker2_read, worker2_write))
print('parent process pid: ', os.getpid())
print('child process pid(proc1): ', proc1.pid)
print('child process pid(proc2): ', proc2.pid)
proc1.wait() # 等待子进程被终止
proc2.wait()
client 代码实现如下:
import os
import sys
import struct
reader = os.fdopen(int(sys.argv[1]), "rb")
writer = os.fdopen(int(sys.argv[2]), "wb")
pid = os.getpid()
send_info = '[{}]hello server!'.format(pid).encode('utf-8')
print('pid[{}] send info: {}'.format(pid, send_info.decode()))
writer.write(struct.pack("<i", len(send_info))) # 小端模式, 低地址存储 b'\x05\x00\x00\x00'
writer.write(send_info)
writer.flush() # 刷新(写入数据之后必须手动刷新)
len_data = reader.read(4) # int占用4个字节
recv_bytes = struct.unpack("<i", len_data)[0]
recv_data = reader.read(recv_bytes)
print('pid[{}] recv info: {}'.format(pid, recv_data.decode()))
server 代码实现如下:
import os
import sys
import struct
reader = os.fdopen(int(sys.argv[1]), "rb")
writer = os.fdopen(int(sys.argv[2]), "wb")
pid = os.getpid()
send_info = '[{}]hello client!'.format(pid).encode('utf-8')
len_data = reader.read(4) # int占用4个字节
recv_bytes = struct.unpack("<i", len_data)[0]
recv_data = reader.read(recv_bytes)
print('pid[{}] recv info: {}'.format(pid, recv_data.decode()))
print('pid[{}] send info: {}'.format(pid, send_info.decode()))
writer.write(struct.pack("<i", len(send_info))) # 小端模式, 低地址存储 b'\x05\x00\x00\x00'
writer.write(send_info)
writer.flush() # 刷新(写入数据之后必须手动刷新)
运行结果如下:

结束语
os.open()函数与open()函数的区别:
os.open()是通过os库调用操作系统来打开文件,而open()函数是python自带的函数,它是通过python程序来打开文件的,可以理解为是对低阶os.open()的封装。
本文详细介绍Linux环境下管道(pipe)通信的原理及实现方法,包括单向和双向进程间通信的示例代码。
2509

被折叠的 条评论
为什么被折叠?



