[翻译]python调用linux epoll编程指南

原文:http://scotdoyle.com/python-epoll-howto.html

翻译前的话

开发高性能web服务器,要求在单位时间内能够尽可能多的响应客户端的请求,就必然要能支持大并发。关于支持并发的操作,有多进程模型(比如Apache),有多线程模型,这些一般使用的是I/O多路复用技术,比较粗浅的有select,poll,他们使用都有自己的局限性,并且性能也不高。从linux 2.6 开始,引入了epoll I/O多路复用的模型, 它避免了select和poll的局限,性能有了很大的提升,同时编程接口也很简洁,我最近使用python较多,就想寻找这方面的内容来学习一下,正好遇到下面这篇文章,感觉写的很好,故翻译出来,一是锻炼自己的英语能力,二是加深自己对epoll的理解,如果还能帮到读者,那就是再好不过的了。限于英语水平,汉语表达能力,文章中肯定有错漏,如果有读者能够指出,万分感谢!

下面是原文的译文。

内容

引言阻塞socket 编程例子异步sockets的好处与linux epoll使用epoll的异步socket编程实例性能思考源代码

引言

从2.6开始,Python 包含了一个访问linux epoll库的api 接口。本文使用Python 3 来简洁的展示这个API。

阻塞socket 编程例子

例子1 是一个监听在8080端口上等待http请求信息的简单Python 版服务器,打印请求信息岛终端上,并且给客户端发送一个HTTP响应信息。

第9行, 创建服务器socket第10行,让第11行的bind调用能够绑定到指定的端口,即使最近有程序监听到同一个端口。否则这个程序会等待一到两分钟直到使用那个端口的程序结束后再运行第11行,绑定本机所有可用的IPv4地址到8080端口,第12行,告诉服务器开始接受请求连接的客户端第14行,程序会在此处停止直到收到一个连接。当次发生后,服务器socket会在这个机器上创建一个新的socket给客户端提供服务。这个新的socket使用从accept()调用返回的clientconnection对象来表示。address 对象表示IP地址,port 存储的是客户端的发过来请求时的端口第15-17行,汇集客户端完成一个完整的http请求传递的数据。HTTP 协议的更多的内容可以参考 HTTP Made Easy。第18行,打印请求信息到终端上,目的是确认正确的操作第19行,向客户端发送回应第20-22行,关闭和客户端之间的连接,关闭监听的socket server

官方的 HOWTO 有关于在Python中进行socket编程的更多的细节描述。

Example 1 (All examples use Python 3) 1  import socket 2 3  EOL1 = b'\n\n' 4  EOL2 = b'\n\r\n' 5  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 6  response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 7  response += b'Hello, world!' 8 9  serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)10  serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)11  serversocket.bind(('0.0.0.0', 8080))12  serversocket.listen(1)1314  connectiontoclient, address = serversocket.accept()15  request = b''16  while EOL1 not in request and EOL2 not in request:17     request += connectiontoclient.recv(1024)18  print(request.decode())19  connectiontoclient.send(response)20  connectiontoclient.close()2122  serversocket.close()

例子2中在15行加入了一个循环,用来重复的处理客户端的请求,直到遇到用户的中断(如键盘中断)。这个例子阐述的更加清晰,服务器socket永远不会和客户端交换数据。而是,接受客户端的请求,在服务器上创建一个新的socket来和客户端通信。23-24行的 finally 声明块保证了监听服务器socket永远能被关闭,即使发生了异常。

Example 2 1  import socket 2 3  EOL1 = b'\n\n' 4  EOL2 = b'\n\r\n' 5  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 6  response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 7  response += b'Hello, world!' 8 9  serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)10  serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)11  serversocket.bind(('0.0.0.0', 8080))12  serversocket.listen(1)1314  try:15     while True:16        connectiontoclient, address = serversocket.accept()17        request = b''18        while EOL1 not in request and EOL2 not in request:19            request += connectiontoclient.recv(1024)20        print('-'*40 + '\n' + request.decode()[:-2])21        connectiontoclient.send(response)22        connectiontoclient.close()23  finally:24     serversocket.close()

异步sockets的好处与linux epoll

例子2中的sockts被称为阻塞sockets,因为Python程序停止运行直到某个事件发生。16行的accpet()调用会一直阻塞直到收到一个客户端的请求。19行的recv()会一直阻塞直到从客户端读取到数据(或没有更多的数据读取了)。21行的send()调用会一直阻塞直到所有的要被发送给客户端的数据全部被linux 加入队列准备传输。

当一个程序使用阻塞sockets时,通常使用一个线程(或不被推荐的进程)来接管和每一个sockets的通信。程序主线程会维护接受客户端连接的监听socket。它会一次接受一个连接,把这个新创建的socket分给一个新的线程,让这个新的线程来和客户端交互。因为所有的这些线程中的每一个只和一个对应的客户端交互,任何一个的阻塞不会阻止其他线程执行他们对应的任务。

使用多线程的阻塞sockets可以产生很直观的代码,但是却遇到了一系列的缺点。当需要共享资源时很难确认线程间的合作是恰当的。并且这种方式的编程在只有一个CPU的机器上会相对低效。

C10K问题 讨论了一些处理大量并发的sockets的可选方案,比如使用异步sockets。这些socktes除非遇到事件发生否则不会阻塞。相反,程序在异步sockets上执行一个操作并且立刻得到一个回应指示那个操作是成功或者失败。这个信息提供程序给来决定如何处理。因为异步socktets是无阻塞的,所以就没有必要运行多线程了。所有的工作都在一个线程中完成。单线程的方法也遇到了它自身带来的挑战,但却是很多程序的好的选择。它也可以喝多线程方法结合起来。异步sockets使用单线程可以用作网络服务器的一部分,线程可以用来访问其他的阻塞资源,比如数据库。

Linux上有很多的机制来管理异步sockets,其中 select,poll,epoll被Python 的API 暴露出来。epoll和poll比select好,因为Python程序无需检查每一个它关注的事件。相反它可以基于操作系统,让其来告诉Python程序哪些sockets可能有Python程序关注的事件发生。epoll 比 poll 更好,因为在python程序的每一次查询中,它无需要求操作系统检查所有的sockets来获取python程序关心的事件。而是linux追踪这些事件的发生,当python程序查询时返回一个list。这些图表显示出当有成千上万的并发sockets连接时epoll的优越性能。

使用epoll的异步socket编程实例

使用epoll来编程的程序通常执行下面的顺序:

    创建一个epoll对象告诉epoll对象在指定的sockets上监听指定的事件询问epoll从上次查询以来哪些sockets可能有指定的事件在这些sockets上执行一些操作告诉epoll对象修改sockets 及(或)事件列表以便监听重复3到5直到结束释放epoll对象

例子3复制了例子2中使用异步sockets的代码。这个例子更加的复杂,因为一个单线程交叉的与多个客户端进行通信。

第1行,导入select模块,其中包含epoll函数。第13行,因为默认情况下sockets是阻塞的,这里需要对socket参数进行设置以便使用非阻塞(异步)模式。第15行,创建一个epoll对象。第16行,在server socket上注册read事件。read事件会在这个server socket接受一个socket请求时的任何时刻发生第19行,连接字典映射文件描述符到他们对应的网络连接对象上。第21行,查询epoll对象,寻找是否有关注的事件发生。参数“1”表示我们愿意等待一秒钟以便前面查询的事件发生。如果任何关注的事件在本次查询的时间段内发生,那么这个查询会立刻返回,并且返回值是一个包含那些发生了的事件的列表。第22行,事件作为一个元组序列(fileno, event code)返回。fileno是文件描述符的同义词,并总是一个整数。第23行,如果socket server上发生了读事件,一个新的socket连接会被创建。第25行,设置新的socket属性为无阻塞模式。第26行,在新的socket上注册读(EPOLLIN)事件。第31行,如果读事件发生了,那么从这个客户端上读取新数据。第33行,一旦接收到完整的请求,就注销读事件并注册写(EPOLLOUT)事件。当能够给客户端回应数据时写事件就会发生。第34行,打印出来完整的请求,展示出虽然与客户端的通信是交叉的,但是数据却可以被组合并且作为一个完整的信息被处理。第35行,如果客户端socket上发生了写事件,那么服务端能够接收数据来发送给客户端。第36-38行,一次发送一点儿数据直到完整的回应已经传递给操作系统去传输。第39行,一旦完整的回应被发送,屏蔽在写或读事件上的关注。第40行,如果一个连接明确的被关闭那么socket shutdown是一个可选项。这个程序调用它是为了引起客户端先关闭连接。shutdown 调用通知客户端没有更多的数据会被发送或者接收了,并且是一个好习惯来引起客户端从它那一端来关闭数据连接。第41行,HUP(hang-up)事件标识客户端socket已经断开连接(比如closed),所以本端也同样关闭。没有必要对HUP事件注册关注。他们总是随epoll对象被注册用来通知sockets。第42行,注销对这个socket连接的关注。第43行,关闭socket连接。第18-45行,try-catch代码块放置在这里,因为程序可能会被中断,比如键盘输入异常。第46-48行,打开的socket连接不需要被关闭,因为当程序结束时python会主动关闭。这里包含在代码中是一个好的形式。

Example 3 1  import socket, select 2 3  EOL1 = b'\n\n' 4  EOL2 = b'\n\r\n' 5  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 6  response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 7  response += b'Hello, world!' 8 9  serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)10  serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)11  serversocket.bind(('0.0.0.0', 8080))12  serversocket.listen(1)13  serversocket.setblocking(0)1415  epoll = select.epoll()16  epoll.register(serversocket.fileno(), select.EPOLLIN)1718  try:19     connections = {}; requests = {}; responses = {}20     while True:21        events = epoll.poll(1)22        for fileno, event in events:23           if fileno == serversocket.fileno():24              connection, address = serversocket.accept()25              connection.setblocking(0)26              epoll.register(connection.fileno(), select.EPOLLIN)27              connections[connection.fileno()] = connection28              requests[connection.fileno()] = b''29              responses[connection.fileno()] = response30           elif event & select.EPOLLIN:31              requests[fileno] += connections[fileno].recv(1024)32              if EOL1 in requests[fileno] or EOL2 in requests[fileno]:33                 epoll.modify(fileno, select.EPOLLOUT)34                 print('-'*40 + '\n' + requests[fileno].decode()[:-2])35           elif event & select.EPOLLOUT:36              byteswritten = connections[fileno].send(responses[fileno])37              responses[fileno] = responses[fileno][byteswritten:]38              if len(responses[fileno]) == 0:39                 epoll.modify(fileno, 0)40                 connections[fileno].shutdown(socket.SHUT_RDWR)41           elif event & select.EPOLLHUP:42              epoll.unregister(fileno)43              connections[fileno].close()44              del connections[fileno]45  finally:46     epoll.unregister(serversocket.fileno())47     epoll.close()48     serversocket.close()

epoll有两种操作模式,称为 edge-triggered 和 level-triggered。在edge-triggered操作模式中,调用 epoll.poll()只有在read或者write事件在socket发生了才会返回那个socket的事件。调用程序必须处理跟那个事件相关的所有数据,在接下来的调用 epoll.poll()中不会再得到通知了。当从某一个特定的事件中的数据被耗尽后,进一步的在socket上的尝试会导致一个异常。相反,在 level-triggered 操作模式中,重复的调用epoll.poll()会收到重复的关注的某个事件的提醒,直到所有的数据被处理了。在level-triggered模式中,通常不会产生异常。

比如,假设一个server socket 被注册了一个epoll对象的读事件。在 edge-triggered 模式中,程序需要accept()新的socket通信直到一个socket.error异常发生。然后在level-triggered模式中,单独的accept()被调用,然后epoll对象能够被重复查询,用来对server socket上发生新的事件,标识出额外的accept()应该被执行。

例子3使用了 level-triggered模式,这是默认的操作模式。例子4展示了如何使用edge-triggered模式。在例子4中,第25、36、45行引入了循环的执行直到遇到异常发生(或所有的数据被直到全部处理了)。第32、38、48行捕获socket 异常,最后在16、28、41和51行,增加了一个EPOLLET掩码用来设置edge-triggered模式。

Example 4 1  import socket, select 2 3  EOL1 = b'\n\n' 4  EOL2 = b'\n\r\n' 5  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 6  response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 7  response += b'Hello, world!' 8 9  serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)10  serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)11  serversocket.bind(('0.0.0.0', 8080))12  serversocket.listen(1)13  serversocket.setblocking(0)1415  epoll = select.epoll()16  epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)1718  try:19     connections = {}; requests = {}; responses = {}20     while True:21        events = epoll.poll(1)22        for fileno, event in events:23           if fileno == serversocket.fileno():24              try:25                 while True:26                    connection, address = serversocket.accept()27                    connection.setblocking(0)28                    epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)29                    connections[connection.fileno()] = connection30                    requests[connection.fileno()] = b''31                    responses[connection.fileno()] = response32              except socket.error:33                 pass34           elif event & select.EPOLLIN:35              try:36                 while True:37                    requests[fileno] += connections[fileno].recv(1024)38              except socket.error:39                 pass40              if EOL1 in requests[fileno] or EOL2 in requests[fileno]:41                 epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)42                 print('-'*40 + '\n' + requests[fileno].decode()[:-2])43           elif event & select.EPOLLOUT:44              try:45                 while len(responses[fileno]) > 0:46                    byteswritten = connections[fileno].send(responses[fileno])47                    responses[fileno] = responses[fileno][byteswritten:]48              except socket.error:49                 pass50              if len(responses[fileno]) == 0:51                 epoll.modify(fileno, select.EPOLLET)52                 connections[fileno].shutdown(socket.SHUT_RDWR)53           elif event & select.EPOLLHUP:54              epoll.unregister(fileno)55              connections[fileno].close()56              del connections[fileno]57  finally:58     epoll.unregister(serversocket.fileno())59     epoll.close()60     serversocket.close()

因为它们很相近,level-triggered模式经常被用来作为移植那些使用select 或者poll的程序,而edge-triggered模式或许用在当程序员不需要或者不想要操作系统更多的帮助来管理事件状态。

除了这两种模式外,sockets或许还会被在epoll对象上注册EPOLLONSHOT事件掩码。当这个选项被使用,注册的事件只有一次调用epoll.poll()是合法的,除此后它会自动的从sockets监视的事件链表上被移除。

性能思考LISTEN Backlog 队列大小

在例子1-4中,第12行展示了一个serversocket.listen()方法。这个方法的属性石listen backlog的队列大小。它告诉操作系统多少个TCP/IP连接在被python程序accpet前,能够被accpet并放在backlog队列中。每一次server端的socket 程序调用accept(),一个连接从这个队列中被移除,那个空出来的位置可以被其他即将进来的连接使用。如果队列满了,新进来的连接被静默的忽略掉,引起客户端的网络连接的无必要的延迟。一个生产上的服务器通常处理成十上百的并行连接,所以值1通常是不恰当的。比如,当使用ab发起100个HTTP 1.0 客户端来执行性能测试,测试这些例子程序的性能时,低于50的backlog值通常会引性能的下降。

TCP 选项

TCP_CORK选项能被用隐藏信息直到它们准备好被读取。这个选项,在例子5的34和40行被展示,或许对于使用HTTP/1.1 管道的HTTP服务器来说是一个好的选项。

Example 5 1  import socket, select 2 3  EOL1 = b'\n\n' 4  EOL2 = b'\n\r\n' 5  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 6  response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 7  response += b'Hello, world!' 8 9  serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)10  serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)11  serversocket.bind(('0.0.0.0', 8080))12  serversocket.listen(1)13  serversocket.setblocking(0)1415  epoll = select.epoll()16  epoll.register(serversocket.fileno(), select.EPOLLIN)1718  try:19     connections = {}; requests = {}; responses = {}20     while True:21        events = epoll.poll(1)22        for fileno, event in events:23           if fileno == serversocket.fileno():24              connection, address = serversocket.accept()25              connection.setblocking(0)26              epoll.register(connection.fileno(), select.EPOLLIN)27              connections[connection.fileno()] = connection28              requests[connection.fileno()] = b''29              responses[connection.fileno()] = response30           elif event & select.EPOLLIN:31              requests[fileno] += connections[fileno].recv(1024)32              if EOL1 in requests[fileno] or EOL2 in requests[fileno]:33                 epoll.modify(fileno, select.EPOLLOUT)34                 connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)35                 print('-'*40 + '\n' + requests[fileno].decode()[:-2])36           elif event & select.EPOLLOUT:37              byteswritten = connections[fileno].send(responses[fileno])38              responses[fileno] = responses[fileno][byteswritten:]39              if len(responses[fileno]) == 0:40                 connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 0)41                 epoll.modify(fileno, 0)42                 connections[fileno].shutdown(socket.SHUT_RDWR)43           elif event & select.EPOLLHUP:44              epoll.unregister(fileno)45              connections[fileno].close()46              del connections[fileno]47  finally:48     epoll.unregister(serversocket.fileno())49     epoll.close()50     serversocket.close()

另一方面,TCP_NODELAY 选项告诉操作系统,任何通过socket.send()发送的数据应该立刻被发送到客户端而不要放在系统缓存中。这个选项,在例子6的第14行被展示了。对于SSH 客户端或其他 “实时”应用来说,是一个好的选项。

Example 6 1  import socket, select 2 3  EOL1 = b'\n\n' 4  EOL2 = b'\n\r\n' 5  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' 6  response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' 7  response += b'Hello, world!' 8 9  serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)10  serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)11  serversocket.bind(('0.0.0.0', 8080))12  serversocket.listen(1)13  serversocket.setblocking(0)14  serversocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)1516  epoll = select.epoll()17  epoll.register(serversocket.fileno(), select.EPOLLIN)1819  try:20     connections = {}; requests = {}; responses = {}21     while True:22        events = epoll.poll(1)23        for fileno, event in events:24           if fileno == serversocket.fileno():25              connection, address = serversocket.accept()26              connection.setblocking(0)27              epoll.register(connection.fileno(), select.EPOLLIN)28              connections[connection.fileno()] = connection29              requests[connection.fileno()] = b''30              responses[connection.fileno()] = response31           elif event & select.EPOLLIN:32              requests[fileno] += connections[fileno].recv(1024)33              if EOL1 in requests[fileno] or EOL2 in requests[fileno]:34                 epoll.modify(fileno, select.EPOLLOUT)35                 print('-'*40 + '\n' + requests[fileno].decode()[:-2])36           elif event & select.EPOLLOUT:37              byteswritten = connections[fileno].send(responses[fileno])38              responses[fileno] = responses[fileno][byteswritten:]39              if len(responses[fileno]) == 0:40                 epoll.modify(fileno, 0)41                 connections[fileno].shutdown(socket.SHUT_RDWR)42           elif event & select.EPOLLHUP:43              epoll.unregister(fileno)44              connections[fileno].close()45              del connections[fileno]46  finally:47     epoll.unregister(serversocket.fileno())48     epoll.close()49     serversocket.close()

源代码

本页中的例子放在了公共空间中,可以从这里下载。

翻译后记

tornado 就是使用epoll来提高性能的典型应用,最近的出墙应用shadowsocks中也使用epoll来提高性能,对于编写无阻塞的高性能服务器来说,epoll是一个非常好的选择,熟练掌握它的调用及使用方法,对于提高自身的编程素养,编写性能更优的程序,或者说在使用别人的框架时,也能更好的理解别人的优势是什么,以及对其的性能评估。

下一步的计划是阅读tornado相关的源代码,敬请期待。

[翻译]python调用linux epoll编程指南

相关文章:

你感兴趣的文章:

标签云: