IO多路复用之select、poll、epoll

参考文档:

https://www.cnblogs.com/aspirant/p/9166944.html

https://blog.csdn.net/qq_35976351/article/details/85228002

https://www.cnblogs.com/Anker/p/3265058.html

https://www.cnblogs.com/lixiaoliuer/p/6735821.html

 

从事服务端开发,少不了要接触网络编程。epoll作为linux下高性能网络服务器的必备技术至关重要,nginx、redis、skynet和大部分游戏服务器都使用到这一多路复用技术。学习多路复用技术对于我们更好理解高性能服务器有很大的帮助。

上篇博文介绍了unix网络编程的5种io模型,其中提到了IO多路复用模型,如下图所示:

上文提到IO多路复用,应用进程阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

比较I/O复用和阻塞式IO并不显得有什么优势,事实上,由于使用select需要两个系统调用,I/O还稍显劣势。不过使用select的优势在于我们可以等待多个描述符就绪。它指的是允许计算机执行或阻塞在一组数据流上,直到到达唤醒阻塞的进程。区别于阻塞式IO时调用的recv,此时的IO信道上不仅仅通过一个数据流,而是一组,所以是复用

 

下面我们就分别介绍下IO多路复用对应的三个函数select,poll,epoll、以及它们区别其他IO模型的优势,以及适用场景。

Select

首先看下unix 系统中的select 函数的参数原型:

int select(int maxfdpl, fd_set * readset, fd_set *writeset, fd_set *exceptset, const struct timeval * tiomeout)

参数说明:

  • maxfdpl:最大的文件描述符长度
  • fd_set * readset:监听的可读集合
  • fd_set *writeset:监听的可写集合
  • fd_set *exceptset:监听的异常集合
  •  timeval * tiomeout:时间限制

select是三者中最定层的,它的事件的轮询机制是基于比特位的。每次查询都要遍历整个事件列表。fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有

函数有三个类型的返回值:

  • 正数: 表示readfdswritefds就绪事件的总数
  • 0:超时返回0
  • -1:出现错误

select是select、poll、select三者中最底层的,它的事件的轮询机制是基于比特位的。每次查询都要遍历整个事件列表。fd_set可以简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有。

select函数的处理流程大致可以分为以下几步:

(1) 从用户可永久拷贝fd_set到内核空间。

(2)内核中遍历传递进来的所有fd_set,为每一个fd_set比特位赋值,描述其读写操作是否就位。

(3)把fd_set从内核空间拷贝到用户空间。

综上可分析:select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

①单个进程可监视的fd数量被限制,即能监听端口的大小有限。

②对socket进行扫描时采用的是线性扫描,即采用轮询的方式,效率偏低。

③需要维护一个用来存放大量fd的数据接口,这样会使得用户空间和内核空间传递该结构时复制开销大。

 

python有直接封装好的select:select.select模块  

readable, writable, exceptional = select.select(inputs, outputs, inputs)

如果监听的套接字满足了可读可写条件, 那么所返回的can,read 或是 can_write就会有值了, 然后我们就可以利用这些返回值进行随后的操作了。其第一个参数就是服务器端的socket, 第二个是我们在运行过程中需要进行回复操作的socket, 第三个存储错误信息。

下面为一段使用python中的select.select模块编写的Server端代码:

​
# coding: utf-8
import select
import socket
import Queue
from time import sleep
​
​
# Create a TCP/IP
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
​
# Bind the socket to the port
server_address = ('localhost', 8090)
print ('starting up on %s port %s' % server_address)
server.bind(server_address)
​
# Listen for incoming connections
server.listen(5)
​
# Sockets from which we expect to read
inputs = [server]
​
# Sockets to which we expect to write
# 处理要发送的消息
outputs = []
​
# Outgoing message queues (socket: Queue)
message_queues = {}
​
while inputs:
    # Wait for at least one of the sockets to be ready for processing
    print ('waiting for the next event')
    # 开始select 监听, 对input_list 中的服务器端server 进行监听
    # 一旦调用socket的send, recv函数,将会再次调用此模块
    readable, writable, exceptional = select.select(inputs, outputs, inputs)
​
    # Handle inputs
    # 循环判断是否有客户端连接进来, 当有客户端连接进来时select 将触发
    for s in readable:
        # 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时,说明有新客户端连接进来了
        # 表示有新用户来连接
        if s is server:
            # A "readable" socket is ready to accept a connection
            connection, client_address = s.accept()
            print ('connection from', client_address)
            # this is connection not server
            connection.setblocking(0)
            # 将客户端对象也加入到监听的列表中, 当客户端发送消息时 select 将触发
            inputs.append(connection)
​
            # Give the connection a queue for data we want to send
            # 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
            message_queues[connection] = Queue.Queue()
        else:
            # 有老用户发消息, 处理接受
            # 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list), 客户端发送消息将触发
            # 所以判断是否是客户端对象触发
            data = s.recv(1024)
            # 客户端未断开
            if data != '':
                # A readable client socket has data
                print ('received "%s" from %s' % (data, s.getpeername()))
                # 将收到的消息放入到相对应的socket客户端的消息队列中
                message_queues[s].put(data)
                # Add output channel for response
                # 将需要进行回复操作socket放到output 列表中, 让select监听
                if s not in outputs:
                    outputs.append(s)
            else:
                # 客户端断开了连接, 将客户端的监听从input列表中移除
                # Interpret empty result as closed connection
                print ('closing', client_address)
                # Stop listening for input on the connection
                if s in outputs:
                    outputs.remove(s)
                inputs.remove(s)
                s.close()
​
                # Remove message queue
                # 移除对应socket客户端对象的消息队列
                del message_queues[s]
​
    # Handle outputs
    # 如果现在没有客户端请求, 也没有客户端发送消息时, 开始对发送消息列表进行处理, 是否需要发送消息
    # 存储哪个客户端发送过消息
    for s in writable:
        try:
            # 如果消息队列中有消息,从消息队列中获取要发送的消息
            message_queue = message_queues.get(s)
            send_data = ''
            if message_queue is not None:
                send_data = message_queue.get_nowait()
            else:
                # 客户端连接断开了
                print "has closed "
        except Queue.Empty:
            # 客户端连接断开了
            print "%s" % (s.getpeername())
            outputs.remove(s)
        else:
            # print "sending %s to %s " % (send_data, s.getpeername)
            # print "send something"
            if message_queue is not None:
                s.send(send_data)
            else:
                print "has closed "
            # del message_queues[s]
            # writable.remove(s)
            # print "Client %s disconnected" % (client_address)
​
    # # Handle "exceptional conditions"
    # 处理异常的情况
    for s in exceptional:
        print ('exception condition on', s.getpeername())
        # Stop listening for input on the connection
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()
​
        # Remove message queue
        del message_queues[s]
​
    sleep(1)
​

Client端

​
# coding: utf-8
import socket
​
​
messages = ['This is the message ', 'It will be sent ', 'in parts ', ]
​
server_address = ('localhost', 8090)
​
# Create aTCP/IP socket
​
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM), socket.socket(socket.AF_INET,  socket.SOCK_STREAM), ]
​
# Connect thesocket to the port where the server is listening
​
print ('connecting to %s port %s' % server_address)
# 连接到服务器
for s in socks:
    s.connect(server_address)
​
for index, message in enumerate(messages):
    # Send messages on both sockets
    for s in socks:
        print ('%s: sending "%s"' % (s.getsockname(), message + str(index)))
        s.send(bytes(message + str(index)).decode('utf-8'))
    # Read responses on both sockets
​
for s in socks:
    data = s.recv(1024)
    print ('%s: received "%s"' % (s.getsockname(), data))
    if data != "":
        print ('closingsocket', s.getsockname())
        s.close()
​

poll

poll的参数原型如下,

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

参数说明:

  • fds:一个pollfd队列的队头指针,我们先把需要监视的文件描述符和他们上面的事件放到这个队列中
  • nfds:队列的长度
  • timeout:事件操作,设置指定正数的阻塞事件,0表示非阻塞模式,-1表示永久阻塞。
    时间的数据结构:

其中与select采用的fd_set不同的是,poll采用的是pollfd

struct pollfd {

        int fd;          // 需要监视的文件描述符

        short events;    // 需要内核监视的事件

        short revents;   // 实际发生的事件

};

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll

epoll是对select和poll的改进,epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

epoll的的三个函数:

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 

1. int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议

 

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数是对指定描述符fd执行op操作。

- epfd:是epoll_create()的返回值。

- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。

- fd:是需要监听的fd(文件描述符)

- epoll_event:是告诉内核需要监听什么事,

 

其中struct epoll_event结构如下:

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待epfd上的io事件,最多返回maxevents个事件。

参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

python代码示例:

#_*_coding:utf-8_*_
__author__ = 'Alex Li'

import socket, logging
import select, errno

logger = logging.getLogger("network-server")

def InitLog():
    logger.setLevel(logging.DEBUG)

    fh = logging.FileHandler("network-server.log")
    fh.setLevel(logging.DEBUG)
    ch = logging.StreamHandler()
    ch.setLevel(logging.ERROR)

    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)
    fh.setFormatter(formatter)

    logger.addHandler(fh)
    logger.addHandler(ch)


if __name__ == "__main__":
    InitLog()

    try:
        # 创建 TCP socket 作为监听 socket
        listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    except socket.error as  msg:
        logger.error("create socket failed")

    try:
        # 设置 SO_REUSEADDR 选项
        listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except socket.error as  msg:
        logger.error("setsocketopt SO_REUSEADDR failed")

    try:
        # 进行 bind -- 此处未指定 ip 地址,即 bind 了全部网卡 ip 上
        listen_fd.bind(('', 2003))
    except socket.error as  msg:
        logger.error("bind failed")

    try:
        # 设置 listen 的 backlog 数
        listen_fd.listen(10)
    except socket.error as  msg:
        logger.error(msg)

    try:
        # 创建 epoll 句柄
        epoll_fd = select.epoll()
        # 向 epoll 句柄中注册 监听 socket 的 可读 事件
        epoll_fd.register(listen_fd.fileno(), select.EPOLLIN)
    except select.error as  msg:
        logger.error(msg)

    connections = {}
    addresses = {}
    datalist = {}
    while True:
        # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
        epoll_list = epoll_fd.poll()

        for fd, events in epoll_list:
            # 若为监听 fd 被激活
            if fd == listen_fd.fileno():
                # 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄
                conn, addr = listen_fd.accept()
                logger.debug("accept connection from %s, %d, fd = %d" % (addr[0], addr[1], conn.fileno()))
                # 将连接 socket 设置为 非阻塞
                conn.setblocking(0)
                # 向 epoll 句柄中注册 连接 socket 的 可读 事件
                epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
                # 将 conn 和 addr 信息分别保存起来
                connections[conn.fileno()] = conn
                addresses[conn.fileno()] = addr
            elif select.EPOLLIN & events:
                # 有 可读 事件激活
                datas = ''
                while True:
                    try:
                        # 从激活 fd 上 recv 10 字节数据
                        data = connections[fd].recv(10)
                        # 若当前没有接收到数据,并且之前的累计数据也没有
                        if not data and not datas:
                            # 从 epoll 句柄中移除该 连接 fd
                            epoll_fd.unregister(fd)
                            # server 侧主动关闭该 连接 fd
                            connections[fd].close()
                            logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
                            break
                        else:
                            # 将接收到的数据拼接保存在 datas 中
                            datas += data
                    except socket.error as  msg:
                        # 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况
                        # 这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理
                        if msg.errno == errno.EAGAIN:
                            logger.debug("%s receive %s" % (fd, datas))
                            # 将已接收数据保存起来
                            datalist[fd] = datas
                            # 更新 epoll 句柄中连接d 注册事件为 可写
                            epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT)
                            break
                        else:
                            # 出错处理
                            epoll_fd.unregister(fd)
                            connections[fd].close()
                            logger.error(msg)
                            break
            elif select.EPOLLHUP & events:
                # 有 HUP 事件激活
                epoll_fd.unregister(fd)
                connections[fd].close()
                logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
            elif select.EPOLLOUT & events:
                # 有 可写 事件激活
                sendLen = 0
                # 通过 while 循环确保将 buf 中的数据全部发送出去
                while True:
                    # 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置
                    sendLen += connections[fd].send(datalist[fd][sendLen:])
                    # 在全部发送完毕后退出 while 循环
                    if sendLen == len(datalist[fd]):
                        break
                # 更新 epoll 句柄中连接 fd 注册事件为 可读
                epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET)
            else:
                # 其他 epoll 事件不进行处理
                continue

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值