11.11 进程间传递Socket文件描述符
最后更新于:2022-04-01 15:38:55
## 问题
You have multiple Python interpreter processes running and you want to pass an openfile descriptor from one interpreter to the other. For instance, perhaps there is a serverprocess that is responsible for receiving connections, but the actual servicing of clientsis to be handled by a different interpreter.
## 解决方案
To pass a file descriptor between processes, you first need to connect the processestogether. On Unix machines, you might use a Unix domain socket, whereas on Win‐dows, you could use a named pipe. However, rather than deal with such low-levelmechanics, it is often easier to use the multiprocessing module to set up such aconnection.
Once a connection is established, you can use the send_handle() and recv_handle()functions in multiprocessing.reduction to send file descriptors between processes.The following example illustrates the basics:
import multiprocessingfrom multiprocessing.reduction import recv_handle, send_handleimport socket
def worker(in_p, out_p):
out_p.close()while True:
> > fd = recv_handle(in_p)print(‘CHILD: GOT FD', fd)with socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd) as s:
> > while True:> > msg = s.recv(1024)if not msg:
> > > break
> > print(‘CHILD: RECV {!r}'.format(msg))s.send(msg)
def server(address, in_p, out_p, worker_pid):
in_p.close()s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)s.bind(address)s.listen(1)while True:
> client, addr = s.accept()print(‘SERVER: Got connection from', addr)send_handle(out_p, client.fileno(), worker_pid)client.close()
if __name__ == ‘__main__':
c1, c2 = multiprocessing.Pipe()worker_p = multiprocessing.Process(target=worker, args=(c1,c2))worker_p.start()
server_p = multiprocessing.Process(target=server,args=((‘', 15000), c1, c2, worker_p.pid))
server_p.start()
c1.close()c2.close()
In this example, two processes are spawned and connected by a multiprocessing Pipeobject. The server process opens a socket and waits for client connections. The workerprocess merely waits to receive a file descriptor on the pipe using recv_handle(). Whenthe server receives a connection, it sends the resulting socket file descriptor to the worker
using send_handle(). The worker takes over the socket and echoes data back to theclient until the connection is closed.If you connect to the running server using Telnet or a similar tool, here is an exampleof what you might see:
> bash % python3 passfd.pySERVER: Got connection from (‘127.0.0.1', 55543)CHILD: GOT FD 7CHILD: RECV b'Hellorn'CHILD: RECV b'Worldrn'
The most important part of this example is the fact that the client socket accepted in theserver is actually serviced by a completely different process. The server merely hands itoff, closes it, and waits for the next connection.
## 讨论
Passing file descriptors between processes is something that many programmers don’teven realize is possible. However, it can sometimes be a useful tool in building scalablesystems. For example, on a multicore machine, you could have multiple instances of thePython interpreter and use file descriptor passing to more evenly balance the numberof clients being handled by each interpreter.The send_handle() and recv_handle() functions shown in the solution really onlywork with multiprocessing connections. Instead of using a pipe, you can connect in‐terpreters as shown in Recipe 11.7, and it will work as long as you use UNIX domainsockets or Windows pipes. For example, you could implement the server and workeras completely separate programs to be started separately. Here is the implementation ofthe server:
# servermp.pyfrom multiprocessing.connection import Listenerfrom multiprocessing.reduction import send_handleimport socket
def server(work_address, port):
# Wait for the worker to connectwork_serv = Listener(work_address, authkey=b'peekaboo')worker = work_serv.accept()worker_pid = worker.recv()
# Now run a TCP/IP server and send clients to workers = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)s.bind((‘', port))s.listen(1)while True:
> > client, addr = s.accept()print(‘SERVER: Got connection from', addr)
> send_handle(worker, client.fileno(), worker_pid)client.close()
if __name__ == ‘__main__':
import sysif len(sys.argv) != 3:
> print(‘Usage: server.py server_address port', file=sys.stderr)raise SystemExit(1)
server(sys.argv[1], int(sys.argv[2]))
To run this server, you would run a command such as python3 servermp.py /tmp/servconn 15000. Here is the corresponding client code:
# workermp.py
from multiprocessing.connection import Clientfrom multiprocessing.reduction import recv_handleimport osfrom socket import socket, AF_INET, SOCK_STREAM
def worker(server_address):
serv = Client(server_address, authkey=b'peekaboo')serv.send(os.getpid())while True:
> > fd = recv_handle(serv)print(‘WORKER: GOT FD', fd)with socket(AF_INET, SOCK_STREAM, fileno=fd) as client:
> > while True:> > msg = client.recv(1024)if not msg:
> > > break
> > print(‘WORKER: RECV {!r}'.format(msg))client.send(msg)
if __name__ == ‘__main__':
import sysif len(sys.argv) != 2:
> print(‘Usage: worker.py server_address', file=sys.stderr)raise SystemExit(1)
worker(sys.argv[1])
To run the worker, you would type python3 workermp.py /tmp/servconn. The result‐ing operation should be exactly the same as the example that used Pipe().Under the covers, file descriptor passing involves creating a UNIX domain socket andthe sendmsg() method of sockets. Since this technique is not widely known, here is adifferent implementation of the server that shows how to pass descriptors using sockets:
# server.pyimport socket
import struct
def send_fd(sock, fd):
‘''Send a single file descriptor.‘''sock.sendmsg([b'x'],
> [(socket.SOL_SOCKET, socket.SCM_RIGHTS, struct.pack(‘i', fd))])
ack = sock.recv(2)assert ack == b'OK'
def server(work_address, port):
# Wait for the worker to connectwork_serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)work_serv.bind(work_address)work_serv.listen(1)worker, addr = work_serv.accept()
# Now run a TCP/IP server and send clients to workers = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)s.bind((‘',port))s.listen(1)while True:
> client, addr = s.accept()print(‘SERVER: Got connection from', addr)send_fd(worker, client.fileno())client.close()
if __name__ == ‘__main__':
import sysif len(sys.argv) != 3:
> print(‘Usage: server.py server_address port', file=sys.stderr)raise SystemExit(1)
server(sys.argv[1], int(sys.argv[2]))
Here is an implementation of the worker using sockets:
# worker.pyimport socketimport struct
def recv_fd(sock):
‘''Receive a single file descriptor‘''msg, ancdata, flags, addr = sock.recvmsg(1,
> socket.CMSG_LEN(struct.calcsize(‘i')))
cmsg_level, cmsg_type, cmsg_data = ancdata[0]assert cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTSsock.sendall(b'OK')
return struct.unpack(‘i', cmsg_data)[0]
def worker(server_address):
serv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)serv.connect(server_address)while True:
> > fd = recv_fd(serv)print(‘WORKER: GOT FD', fd)with socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd) as client:
> > while True:> > msg = client.recv(1024)if not msg:
> > > break
> > print(‘WORKER: RECV {!r}'.format(msg))client.send(msg)
if __name__ == ‘__main__':
import sysif len(sys.argv) != 2:
> print(‘Usage: worker.py server_address', file=sys.stderr)raise SystemExit(1)
worker(sys.argv[1])
If you are going to use file-descriptor passing in your program, it is advisable to readmore about it in an advanced text, such as Unix Network Programming by W. RichardStevens (Prentice Hall, 1990). Passing file descriptors on Windows uses a differenttechnique than Unix (not shown). For that platform, it is advisable to study the sourcecode to multiprocessing.reduction in close detail to see how it works.