对select、poll、epoll的简介及作业中epoll方式的实现

来源:互联网 发布:在中国长大的老外知乎 编辑:程序博客网 时间:2024/04/29 18:03


在视频中,老师对于高级I/O复用进行了一些解释,并详细讲了select的用法,而对于epoll和poll只是简单的提一下。而现在用得最多的是epoll,所以自己查阅了一些资料,并把作业用epoll完成。
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

Python代码实现如下:
###############################################
client.py
###############################################
  1. import socket
  2. import time
  3. server = ('127.0.0.1',2007)
  4. msg = ['hello', 'welcome', 'xiaoming', 'zhangsan', 'lisi', 'liuliu']

  5. socks = []
  6. for i in range(10):
  7.     sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  8.     socks.append(sock)
  9. for s in socks:
  10.     s.connect(server)

  11. counter = 0

  12. for m in msg:
  13.     for s in socks:
  14.         s.send("%d send %s"%(counter,m))
  15.         counter +=1
  16.     for s in socks:
  17.         data = s.recv(1024)
  18.         print '%s echo %s'%(s.getpeername(),data)
  19.         if not data:
  20.             s.close()
  21.     #time.sleep(2)

  22. sock.close()
复制代码


*******************************************************
sever_select.py
*******************************************************
  1. import socket
  2. import select
  3. import Queue
  4. server = ('127.0.0.1',2007)
  5. sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  6. sock.setblocking(False)
  7. sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
  8. sock.bind(server)
  9. sock.listen(10)
  10. rlists=[sock]
  11. wlists=[]
  12. msg_que = {}
  13. timeout=20

  14. while rlists:
  15.     rs,ws,es = select.select(rlists,wlists,rlists,timeout)
  16.     if not (rs or ws or es):
  17.         print 'timeout...'
  18.         break
  19.     for s in rs:
  20.         if s is sock:
  21.             conn,addr = s.accept()
  22.             print 'connect by ',addr
  23.             conn.setblocking(False)
  24.             rlists.append(conn)
  25.             msg_que[conn]=Queue.Queue()
  26.         else:
  27.             data = s.recv(1024)
  28.             if data:
  29.                 print data
  30.                 msg_que.put(data)
  31.                 if s not in wlists:
  32.                     wlists.append(s)
  33.             else:
  34.                 if s in wlists:
  35.                     wlists.remove(s)
  36.                 rlists.remove(s)
  37.                 s.close()
  38.                 del msg_que

  39.     for s in ws:
  40.         try:
  41.             msg = msg_que.get_nowait()
  42.         except Queue.Empty:
  43.             print 'msg empty'
  44.             wlists.remove(s)
  45.         else:
  46.             s.send(msg)
  47.     for s in es:
  48.         print 'except',s.getpeername()
  49.         if s in rlists:         
  50.             rlists.remove(s)
  51.         if s in wlists:
  52.             wlists.remove(s)
  53.         s.close()
  54.         del msg_que
复制代码



以上的client.py和select_server.py都是视频中老师的代码,在这里就不再说了。主要还是看看epoll_server.py的代码

*******************************************************
sever_epoll.py
*******************************************************
  1. import socket
  2. import select
  3. import Queue

  4. server = ("127.0.0.1", 2007)
  5. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  6. sock.setblocking(False)
  7. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  8. sock.bind(server)
  9. sock.listen(10)
  10. msg_que = {}
  11. timeout = 20

  12. # 设置事件权限
  13. READ_ONLY = ( select.EPOLLIN | select.EPOLLPRI | select.EPOLLHUP | select.EPOLLERR)
  14. READ_WRITE = (READ_ONLY|select.EPOLLOUT)
  15. poller = select.epoll()
  16. #将sock和相应的权限注册到epoll中,使得epoll可以响应该sock
  17. poller.register(sock,READ_ONLY)
  18. fd_to_socket = {sock.fileno():sock,}
  19. while True:
  20.     events = poller.poll(timeout) #监控是否有事件发生,若有事件返回一个(fd,flag),fd:sock.fileno(),即该sock的文件描述符,flag:该sock的权限
  21.     if len(events) == 0:   #超时退出
  22.         print 'timeout...'
  23.         breeak

  24.     for fd ,flag in  events:
  25.         s = fd_to_socket[fd]
  26.         if flag & (select.EPOLLIN | select.EPOLLPRI) :#判断该sock的请求是否为读
  27.             if s is sock : #请求连接的sock
  28.                 conn , client_address = s.accept()
  29.                 print " connect by " , client_address
  30.                 conn.setblocking(False)               
  31.                 fd_to_socket[conn.fileno()] = conn
  32.                 poller.register(conn,READ_ONLY)               
  33.                 msg_que[conn]  = Queue.Queue()
  34.             else : #请求接收的sock
  35.                 data = s.recv(1024)
  36.                 if data:
  37.                     print data
  38.                     msg_que.put(data)
  39.                     poller.modify(s,READ_WRITE) #修改该sock的权限,使之可读可写
  40.                 else :
  41.                     poller.unregister(s) #收不到数据,取消在epoll中的注册,说明该连接以后不再用
  42.                     s.close()
  43.                     del fd_to_socket[fd]
  44.                     del msg_que
  45.         elif flag & select.EPOLLOUT :#判断该sock是否为写
  46.             try:
  47.                 msg = msg_que.get_nowait()
  48.             except Queue.Empty:
  49.                 print s.getpeername() , " que empty"
  50.             else :
  51.                 s.send(msg)

  52.         elif flag & select.EPOLLHUP :#判断该sock是否被挂起,将要被关闭
  53.             print " Closing ", s.getpeername() ,"(HUP)"
  54.             poller.unregister(s)
  55.             s.close()
  56.             del fd_to_socket[fd]

  57.         elif flag & select.EPOLLERR:#判断该sock是否出错
  58.             print "except" , s.getpeername()
  59.             poller.unregister(s)
  60.             s.close()
  61.             del fd_to_socket[fd]
  62.             del msg_que
复制代码


总的来说,epoll的python实现与select的不同在于将sock和其权限注册到实例化后的一个epoll中,使得该epoll检测已注册的sock是否有事件发生。
epoll的官方文档说明如下,都比较简单。

事件权限掩码表如下:

Constant

Meaning

EPOLLIN

Available for read

EPOLLOUT

Available for write

EPOLLPRI

Urgent data for read

EPOLLERR

Error condition happened on the assoc. fd

EPOLLHUP

Hang up happened on the assoc. fd

EPOLLET

Set Edge Trigger behavior, the default is Level Trigger behavior

EPOLLONESHOT

Set one-shot behavior. After one event is pulled out, the fd is internally disabled

EPOLLRDNORM

Equivalent to EPOLLIN

EPOLLRDBAND

Priority data band can be read.

EPOLLWRNORM

Equivalent to EPOLLOUT

EPOLLWRBAND

Priority data may be written.

EPOLLMSG

Ignored.


epoll的方法如下:
epoll.close()Close the control file descriptor of the epoll object.
epoll.fileno()Return the file descriptor number of the control fd.
epoll.fromfd(fd)Create an epoll object from a given file descriptor.
epoll.register(fd[, eventmask])Register a fd descriptor with the epoll object.

Note

Registering a file descriptor that’s already registered raises an IOError – contrary toPolling Objects‘s register.


epoll.modify(fd, eventmask)Modify a register file descriptor.
epoll.unregister(fd)Remove a registered file descriptor from the epoll object.
epoll.poll([timeout=-1[, maxevents=-1]])Wait for events. timeout in seconds (float)

由于poll的实现与epoll的实现基本一样,将代码中的epoll改为poll即可,但是其实现原理却差别很大。这里就不贴poll实现代码
自己也是第一次接触select,希望有高手能提出意见。