Eli's Blog

1. select

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

1
2
3
4
5
6
7
while true {
select(streams[])
for i in streams[] {
if i has data
read until unavailable
}
}

select的优点是支持目前几乎所有的平台,缺点主要有如下2个:
 1)单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024(32位,64位默认2048,proc/sys/fs/file-max),不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
 2)select 所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

2. poll

poll则在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

3. epoll

 epoll是Linux 2.6 开始出现的为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
 在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。  

1
2
3
4
5
6
while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till
}
}

1.3 epoll

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

1.4 select、poll、epoll 区别总结

1、支持一个进程所能打开的最大连接数

类型 特点
select 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

类型 特点
select 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll 同上
epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式
| 类型 | 特点 |
| —— | ———————————————————— |
| select | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
| poll | 同上 |
| epoll | epoll通过内核和用户空间共享一块内存来实现的。 |

总结:

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

2. 异步与非阻塞的区别

非阻塞是对于socket而言;异步是相对于应用程序而言,是一种编程模型

Epoll是非阻塞的,但不是异步。实现阻塞和简单,socket.setblocking(False)即可;实现异步很复杂。

Linux没有实现异步IO(效率并不高),Epoll是一种I/O多路复用技术,用户程序需要主动去询问内核是否有事件发生,而不是事件发生时内核主动去调用回调函数,所以不是异步的。

Tornado框架之所以是异步的,它在epoll的基础上进行了一层封装,由框架去取事件,然后由框架去调用用户的回调函数,所以对于基于该框架的用户程序来说,是异步的。

Tornado使用Epoll实现了异步编程模型,使用异步的前提是socket是非阻塞的。

3. Python select

Python的select()方法直接调用操作系统的IO接口,它监控sockets,open files, and pipes(所有带fileno()方法的文件句柄)何时变成readable 和writeable, 或者通信错误,select()使得同时监控多个连接变的简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过Python的解释器。

select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息,接下来我们需要创建2个列表来包含输入和输出信息来传给select().

select_echo_server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import select
import socket
import sys
from queue import Queue, Empty

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)

server_address = ('localhost', 5000)
print('starting up on %s port %s' % server_address, file=sys.stderr)
server.bind(server_address)
server.listen(5)

# reading sockets
inputs = [server, ]

# writing sockets
outputs = []

message_queues = {}
while inputs:
print('\nwaiting for the next event.')
readable, writable, exceptional = select.select(inputs, outputs, inputs)

for s in readable:
if s is server:
# A readable server socket is ready to accept a connection
connection, client_address = s.accept()
print('new connection from', client_address)
connection.setblocking(False)
inputs.append(connection)

# Give the connection a queue for data we want to send
message_queues[connection] = Queue()
else:
data = s.recv(1024)
if data:
# A readable client socket has data
print('received "%s" from %s' % (data, s.getpeername()))
message_queues[s].put(data)

# Add output channel for response
if s not in outputs:
outputs.append(s)
else:
# Interpret empty result as closed connection
print('closing', client_address, 'after reading not data.')

# Stop listening for input on the connection
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()

# Remove message queue
del message_queues[s]

for s in writable:
try:
next_msg = message_queues[s].get_nowait()
except Empty:
# No message waiting so stop checking for writability
print('output queue for', s.getpeername(), 'is empty')
outputs.remove(s)
else:
print('sending "%s" to %s' % (next_msg, s.getpeername()))
reply = 'replied: ' + next_msg.decode('utf-8')
s.send(reply.encode('utf-8'))

for s in exceptional:
print('handling exceptional condition for', s.getpeername())

inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()

del message_queues[s]

select_echo_multiclient.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import socket
import sys

server_address = ('localhost', 5000)

messages = ['message %s' % i for i in range(10)]
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(3)]

print('connecting to %s port %s' % server_address, file=sys.stderr)
for s in socks:
s.connect(server_address)

for message in messages:
# send messages on both sockets
for s in socks:
print('%s: sending "%s"' % (s.getsockname(), message), file=sys.stderr)
s.send(message.encode('utf-8'))

# read response on both sockets
for s in socks:
data = s.recv(1024)
print('%s: received "%s"' % (s.getsockname(), data), file=sys.stderr)
if not data:
print('closing socket', s.getsockname(), file=sys.stderr)
s.close()

 上一页