poll 回写模式
- 采用select机制的问题
- 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
- 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。
# -*- coding:utf-8 -*-
"""
File Name: poll_svr
Author: 82405
Data: 2022/6/2 15:43
-----------------------
Info:
基于poll实现的回写服务
-----------------------
Change Activity:
2022/6/2: create
"""
import socket
import select
import queue
def poll_svr():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 1234))
sock.listen(100)
sock.setblocking(False) # 非阻塞模式
sock_que = {} # 登记新的socket对象的消息队列
sk_fd_map = {sock.fileno(): sock}
p = select.poll()
p.register(sock, select.POLLIN | select.POLLERR)
while True:
events = p.poll()
for sk_fd, event in events:
if sk_fd == sock.fileno():
conn, addr = sk_fd_map[sk_fd].accept() # 生成新的读写套接字对象和 客户端地址
conn.setblocking(False) # 非阻塞
p.register(conn, select.POLLIN | select.POLLOUT | select.POLLERR)
sk_fd_map[conn.fileno()] = conn
sock_que[conn.fileno()] = queue.Queue()
else:
if event & select.POLLIN:
data = sk_fd_map[sk_fd].recv(1024)
if data:
print(f'{sk_fd}接收消息:{data}')
sock_que[sk_fd].put(data)
p.modify(sk_fd_map[sk_fd], select.POLLOUT | select.POLLERR)
else:
print(f'{sk_fd}关闭连接')
p.unregister(sk_fd)
sk_fd_map[sk_fd].close()
del sk_fd_map[sk_fd]
del sock_que[sk_fd]
continue
if event & select.POLLOUT:
try:
next_msg = sock_que[sk_fd].get_nowait()
print(f'{sk_fd}回写消息: {next_msg}')
sk_fd_map[sk_fd].send(next_msg)
p.modify(sk_fd_map[sk_fd], select.POLLIN | select.POLLERR)
except queue.Empty:
print(f'************{sk_fd} 回写消息为空******************')
if event & select.POLLERR:
p.unregister(sk_fd)
sk_fd_map[sk_fd].close()
del sock_que[sk_fd]
del sk_fd_map[sk_fd]
poll_svr()
epoll 会写模式
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
- epoll 中水平触发和边缘触发
1.水平触发level trigger LT(状态达到)
当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,如果用户一次读写没取完数据,他会一直通知用户,如果这个描述符是用户不关心的,它每次都返回通知用户,则会导致用户对于关心的描述符的处理效率降低。
复用型IO中的select和poll都是使用的水平触发方式。
2.边缘触发edge trigger ET(状态改变)
当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,它只会通知用户进程一次,这需要用户一次把内容读取玩,相对于水平触发,效率更高。如果用户一次没有读完数据,再次请求时,不会立即返回,需要等待下一次的新的数据到来时才会返回,这次返回的内容包括上次未取完的数据。
信号驱动型IO使用的是边缘触发方式。
epoll既支持水平触发也支持边缘触发,默认是水平触发。
3.比较
水平触发是状态达到后,可以多次取数据。这种模式下要注意多次读写的情况下,效率和资源利用率情况。
边缘触发是状态改变一次,取一次数据。这种模式下读写数据要注意一次是否能读写完成。
# -*- coding:utf-8 -*-
"""
*filename: epoll_svr
*author: wabi
*datetime: 2022/6/222:10
*info:
基于epoll实现的回写服务
"""
import socket
import select
import queue
def epoll_svr():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 1234))
sock.listen(100)
sock.setblocking(False) # 非阻塞模式
ep = select.epoll()
ep.register(sock.fileno(), select.EPOLLIN | select.EPOLLERR)
sk_fd_map = {sock.fileno(): sock}
sock_que = {} # 登记新的socket对象的消息队列
while True:
events = ep.poll(10)
if not events:
print('超时,重新轮询')
continue
for sk_fd, event in events:
if sk_fd is sock.fileno():
conn, addr = sk_fd_map[sk_fd].accept() # 生成新的读写套接字对象和 客户端地址
conn.setblocking(False) # 非阻塞
ep.register(conn, select.EPOLLIN | select.EPOLLERR)
sk_fd_map[conn.fileno()] = conn
sock_que[conn.fileno()] = queue.Queue()
else:
if event & select.EPOLLIN:
data = sk_fd_map[sk_fd].recv(1024)
if data:
print(f'{sk_fd}接收消息:{data}')
sock_que[sk_fd].put(data)
ep.modify(sk_fd_map[sk_fd], select.EPOLLOUT | select.EPOLLERR)
if event & select.EPOLLOUT:
try:
next_msg = sock_que[sk_fd].get_nowait()
print(f'{sk_fd}回写消息: {next_msg}')
sk_fd_map[sk_fd].send(next_msg)
ep.modify(sk_fd_map[sk_fd], select.EPOLLIN | select.EPOLLERR)
except queue.Empty:
print(f'************{sk_fd} 回写消息为空******************')
if event & select.EPOLLHUP:
print('conn close')
ep.unregister(sk_fd)
sk_fd_map[sk_fd].close()
del sk_fd_map[sk_fd]
del sock_que[sk_fd]
epoll_svr()
- 参考
- https://www.cnblogs.com/Zhangyq-yard/p/10165092.html
- https://www.itqiankun.com/article/select-poll-epoll