I/O多路复用-EPOLL探索

什么是I/O多路复用

I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个IO能够读写,通知程序进行相应的读写操作。

I/O多路复用的场合

1、当客户处理多个描述字时(通常是交互式输入和网络套接字),必须使用I/O复用

2、如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到I/O复用

3、如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用

Linux 下I/O多路复用的方式

SELECT、POLL、EPOLL

对比SELECT、POLL、EPOLL

SELECT缺点

每次调用select,都需要把fd集合从用户态拷贝到内核态,开销和fd的数量成正比

每次调用select都需要在内核遍历传递进来的所有fd,开销和fd的数量成正比

select支持的文件描述符数量太少,32位1024个,64位2048个

POLL缺点

poll和select本质相同,使用链表存储文件描述符,没有最大连接数的限制

下图是select、poll、kqueue(FreeBSD平台)、epoll四种方式连接数和时间关系图

上图可以看出,随着fd数量的增大,select、poll的时间消耗非常大,kqueue和epoll基本上没有变化

EPOLL优点

1、它所支持的FD上限是最大可以打开文件的数目,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2、IO 效率不随FD数目增加而线性下降

3、epoll不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd,而不是全部遍历。

 

下面是一个场景,一个服务端Server.py,多个客户端订阅,服务端不断的生成消息,客户端订阅后服务端会不断的把消息发送给客户端:

Server:

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 
 4 __author__ = 'Andy'
 5 import socket, select, traceback, time
 6 import threading
 7 import Queue
 8 
 9 gen = Queue.Queue()
10 connections = {}
11 requests = {}
12 responses = {}
13 
14 
15 def run():
16     serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17     serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
18     serversocket.bind(('127.0.0.1', 8080))
19     serversocket.listen(5)
20     serversocket.setblocking(0)
21     epoll = select.epoll()  # 创建一个epoll对象
22     epoll.register(serversocket.fileno(), select.EPOLLIN)  # 给新建的serversocket.fileno注册一个读event
23 
24     try:
25         count = 0
26         while True:
27             events = epoll.poll()  # 激活的fileno举手
28             count += 1
29             for fileno, event in events:
30                 if fileno == serversocket.fileno():  # 当激活的fileno是新建的,给该fileno注册一个读event
31                     connection, address = serversocket.accept()
32                     connection.setblocking(0)
33                     epoll.register(connection.fileno(), select.EPOLLIN)
34                     connections[connection.fileno()] = connection
35                     requests[connection.fileno()] = b''
36                     responses[connection.fileno()] = b""
37                     print "new conn.fileno is %s" % connection.fileno()
38                 elif event & select.EPOLLIN:  # 如果fileno是读event,接收发送来消息,并修改该fileno为写event,下次循环时写数据
39                     print "read event is happing"
40                     requests[fileno] += connections[fileno].recv(1024)
41                     epoll.modify(fileno, select.EPOLLOUT)
42                     print('-' * 40 + '
' + requests[fileno].decode()[:-2])
43                 elif event & select.EPOLLOUT:  # 如果fileno是写事件,写完后正常的为挂起
44                     if responses[fileno]:
45                         byteswritten = connections[fileno].send(responses[fileno])
46                         responses[fileno] = responses[fileno][byteswritten:]
47                         if len(responses[fileno]) == 0:
48                             epoll.modify(fileno, select.EPOLLOUT)  # 需要向订阅者一直发消息,这里发完后仍为写event
49                             print "change event to write"
50                 elif event & select.EPOLLHUP:
51                     epoll.unregister(fileno)
52                     connections[fileno].close()
53                     del connections[fileno]
54                     print "event is HUP ===%s" % fileno
55         pass
56     except Exception, err:
57         print traceback.print_exc()
58     finally:
59         epoll.unregister(serversocket.fileno())
60         epoll.close()
61         serversocket.close()
62         print "finally"
63 
64 
65 def create_data():
66     count = 1
67     while True:
68         count += 1
69         res = "Message-%s" % count
70         gen.put_nowait(res)  # 把消息放入队列
71         time.sleep(0.5)
72 
73 
74 def update_message():
75     while True:
76         message = gen.get()
77         for res in responses:  # 遍历所有的活跃用户,更新消息
78             responses[res] = message
79         time.sleep(0.05)
80 
81 
82 if __name__ == "__main__":
83     p = threading.Thread(target=create_data)  # 开个线程向队列里放数据
84     p1 = threading.Thread(target=update_message)  # 从队列中取出数据
85     p.start()
86     p1.start()
87     run()
88     p.join()
89     p1.join()
View Code

Client:

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 
 4 __author__ = 'Andy'
 5 import socket,time
 6 def sim_client(name,i):
 7     connFd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
 8     connFd.connect(("127.0.0.1", 8080))
 9     connFd.send("Process-%s start subscibe

" % name)
10     while True:
11         try:
12             readData = connFd.recv(1024)
13             if readData:
14                 print "*"*40 + "
" + readData.decode()
15         except:
16             time.sleep(0.2)
17 
18 
19 if __name__=="__main__":
20     sim_client("progressage",1)
View Code

来看一下输出结果

服务端接受订阅的消息:

客户端订阅服务端的消息

 

才疏学浅,目前只研究了EPOLL的水平触发,还有边缘触发需要去探索


作者:Andy
出处:http://www.cnblogs.com/onepiece-andy/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/onepiece-andy/p/epoll_level-triggered.html