• select/poll/epoll 学习


    select

    select通过设置或检查存放fd标志位的数据结构进行下一步处理.
    对socket是线性扫描,即轮询,效率较低: 仅知道有I/O事件发生,却不知哪几个流,只会无差异轮询所有流,找出能读/写数据的流进行操作。同时处理的流越多,无差别轮询时间越长 - O(n)。

    fd_set的实现就是采用位图bitmap
    使用位图bitmap,往集合了添加n时只需将第n个bit位置1,移除n时只需将第n个比特置为0,移除所有数据时,只需将所有bit置为0。

    fd_set *readset
    fd_set *writeset
    fd_set *exceptset
    
    • 1
    • 2
    • 3

    readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字
    在这里插入图片描述
    这也是为什么fdset不可重用的原因
    bitmap在用户态只能用四个宏来操作,没办法正确将对应输出置0

    /*申请了连续的内存后,平分到6个映射*/
    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size;
    /* 我们需要使用6倍于最大描述符的描述符个数,
    * 分别是in/out/exception(参见fd_set_bits结构体),
     * 并且每份有一个输入和一个输出(用于结果返回) */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    具体流程如下

    select(fdr+1,&rdset,NULL,NULL,NULL);
    
    • 1

    fdr+1 是为了减少查找范围,要检测描述符8, 9, 10, 把select的第一个参数定为11, 实际上会检测0-10,如果你把select的第一个参数定为8, 实际上只检测0到7, 所以select不会感知到8, 9, 10描述符的变化。
    (1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
    (2)若fd=5,执行FD_SET(fd,&set);后set将第5位置为1)
    (3)若再加入fd=2,fd=1,则set再将1、2位置为1
    (4)执行select(6,&set,0,0,0)阻塞等待
    (5)若fd=1,fd=2上都发生可读事件,则select返回。
    在这里插入图片描述
    select的优点:
    可以等待多个套接字
    select的几大缺点:
    (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大(fdset不可重用,用户态<->内核态开销大)
    (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大(时间复杂度O(n))
    (3)select支持的文件描述符数量太小了,默认是1024

    poll

    struct pollfd {
    int fd;        /* 文件描述符 */
    short events; /* 等待的事件 */
    short revents; /* 实际发生了的事件 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    fd:特定的文件描述符,若设置为负值则忽略events字段并且revents字段返回0。
    events:需要监视该文件描述符上的哪些事件。
    revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪。

    扩展
    poll没有限制是因为在内核基于链表实现的,注意是内核
    在用户态他还是数组
    在内核态 struct poll_list类型的链表walk啊,这是个在内核空间开辟出来的链表,而我们传入的从用户空间复制来的pollfd则通过copy_from_user方法拷贝给了walk链表

    int poll (struct pollfd *fds, size_t nfds , int timeout);
    
    • 1

    参数说明:

    fds:是一个struct pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符;每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便;特别是对于 socket连接比较多的情况下,在一定程度上可以提高处理的效率;这一点与select()函数不同,调用select()函数之后,select() 函数会清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中;因此,select()函数适合于只检测一个socket描述符的情况,而poll()函数适合于大量socket描述符的情况;

    nfds:nfds_t类型的参数,用于标记数组fds中的结构体元素的总数量;

    timeout:是poll函数调用阻塞的时间,单位:毫秒;

    事件的添加

    poll的升级在它可以将事件进行具体的划分,即增加多个事件种类。注意,这里的events是short类型,但是我们也可以将它看成一个位图结构,方便我们进行事件的添加。添加事件的本质就在于按位与或上一个表示某一个事件的宏。

    在poll函数返回后,可以通过 与 运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。

    解决了什么问题

    fdset是不可重用问题
    select 支持的文件描述符数量1024

    Epoll

    epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait

    调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个 红黑树(rbr) 用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表(rdlist),用于存储准备就绪的事件.
    epoll_ctl 对红黑树操作,添加所有socket节点。
    Epoll将所有你关注的事件存放在统一的事件集合里面。
    当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已.

    level-triggered 和 edge-triggered

    读缓冲区刚开始是空的
    读缓冲区写入2KB数据
    水平触发和边缘触发模式此时都会发出可读信号
    收到信号通知后,读取了1kb的数据,读缓冲区还剩余1KB数据
    水平触发会再次进行通知,而边缘触发不会再进行通知
    所以,边缘触发需要一次性的把缓冲区的数据读完为止,也就是一直读,直到读到EGAIN为止,EGAIN说明缓冲区已经空了,因为这一点,边缘触发需要设置文件句柄为非阻塞.

    Epoll

  • 相关阅读:
    反制小技巧-利用访客记录获取攻击者社交ID
    Redis高级特性和应用:慢查询、Pipeline、事务、Lua
    基于Web的美食分享平台的设计与实现——HTML+CSS+JavaScript水果介绍网页设计(橙子之家)
    windows下Redis-cluster集群搭建
    如何3分钟,快速开发一个新功能
    二十三种设计模式全面解析-从线程安全到创新应用:探索享元模式的进阶之路
    组织赋能,统一企业门户实现高效化、移动化协作
    Apollo 添加自己的地图并显示到DreamView
    Tilemap瓦片资源
    企业如何通过会员积分营销留住客户?
  • 原文地址:https://blog.csdn.net/moernagedian/article/details/127648486