【IO多路复用】select、epoll

linux文件描述符(file discription)

1、文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件或socket

2、每一个进程都有自己的文件描述符集合。

3、当创建进程时,通常默认会有3个文件描述符(0,1,2),0代表标准输入,1代表标准输出,2代表标准错误,它们统称为标准IO,所以如果进程通过open打开一个文件的时候,文件描述符会从3开始,fd的值其实就是进程中打开文件列表的文件描述符集合的下标索引。

4、由于文件描述符在一个进程中是特有的,因此不能在多个进程中间实现共享,而唯一的例外是在父/子进程之间,当一个进程调用fork时,调用fork时打开的所有文件在子进程和父进程中仍然是打开的,而且子进程写入文件描述符会影响到父进程的同一文件描述符,反之亦然

5、Unix操作系统通常给每个进程能打开的文件数量强加一个限制(1024),ulimit -n查看系统默认的文件描述符

6、基于文件描述符的输入输出函数:

  open:打开一个文件,并指定访问该文件的方式,调用成功后返回一个文件描述符。
  creat:打开一个文件,如果该文件不存在,则创建它,调用成功后返回一个文件描述符。
  close:关闭文件,进程对文件所加的锁全都被释放。
  read:从文件描述符对应的文件中读取数据,调用成功后返回读出的字节数。
  write:向文件描述符对应的文件中写入数据,调用成功后返回写入的字节数。
  ftruncate:把文件描述符对应的文件缩短到指定的长度,调用成功后返回0。
  lseek:在文件描述符对应的文件里把文件指针设定到指定的位置,调用成功后返回新指针的位置。
  fsync:将所有已写入文件中的数据真正写到磁盘或其他下层设备上,调用成功后返回0。
  fstat:返回文件描述符对应的文件的相关信息,把结果保存在struct stat中,调用成功后返回0。
  fchown:改变与打开文件相关联的所有者和所有组,调用成功后返回0。
  fchmod:把文件描述符对应的文件的权限位改为指定的八进制模式,调用成功后返回0。
  flock:用于向文件描述符对应的文件施加建议性锁,调用成功后返回0。
  fcntl:既能施加建议性锁也能施加强制性锁,能建立记录锁、读取锁和写入锁,调用成功后返回0。
  dup:复制文件描述符,返回没使用的文件描述符中最小的编号。
  dup2:由用户指定返回的文件描述符的值,用来重新打开或重定向一个文件描述符。
  select:同时从多个文件描述符读取数据或向多个文件描述符写入数据
7、简单归纳:fd只是一个整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。

select、poll、epoll之间的区别(搜狗面试) - aspirant - 博客园

epoll底层结构 - unique_ptr - 博客园

select

【硬核教程】IO多路复用底层原理全解,select,poll,epoll,socket,系统中断,进程调度,系统调用_哔哩哔哩_bilibili

  • 监听方式:它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
  • 文件描述符的限制:select用一个bit序列去记录监听的文件描述符(长度固定为1024的bit序列),监听一个socket,就将序列上某一位置为1,所以select最多可以监听1024个socket连接。
  • 事件read:select要让用户来判断哪些socket产生了事件,因此每次都要将binmap复制到用户空间,用户处理完后将binmap归位后又赋值到内核空间,产生了大量的拷贝
  • 参数:下面的select函数是用户自己去调用的。maxfdp1是指定OS将文件描述符都设置到fd_set的哪些范围,这样maxfdp1后面的位就不用检查了;fd_set就是binmap,用户要传3个fd_set,去接收OS监听到的socket事件,readset接收read事件,writeset接收write事件,exceptset接收异常事件。

for (int=0;i<5;i++){
    // 伪代码,socket server接收5个client连接.fds里面存的是建立连接的socket文件描述符
    fds[i] = accept(serverSocketFd);
    // max为最大的文件描述符
    int max;
    if (fds[i] > max) {
        max = fds[i];
    }
}
// read binmap
int rset[1024];
while (1){
    // 把rset的所有位置0
    FD_ZERO(&rset);
    // 把socket位置1
    for (i=0;i<5;i++){
        FD_SET(fds[i], &rset);
    }
    print("round again");
    select(max+1, &rset, NULL, NULL, NULL);

    for (i=0;i<5;i++){
        // 判断这几个socket的fd有没有被置位,有说明监听到了对应socket的read事件。
        if (FD_ISSET(fds[i], &rset)) {
            memset(buffer, 0, MAXBUF);
            // 从就绪的socket中read数据
            read(fds[i], buffer, MAXBUF);
            print(buffer);
        }
    }
}

poll

与select一样,只是没有了文件描述符1024的限制,使用链表来记录文件描述符。

epoll

视频:仅有30%的人了解的Linux网络高并发技术之epoll_哔哩哔哩_bilibili

  • 支持的文件描述符上限是整个系统最大可以打开的文件数目。例如,在1GB内存的机器上,这个限制大概为10万左右。
  • epoll不是轮询,而是采用通知机制。每个socket都有一个callback函数,只有活跃的socket才会主动去调用callback函数,当有事件(如socket读缓冲区被写入了数据)产生时,会调用回调函数去通知epoll。也就是说,epoll只管你“活跃”的连接,而跟连接总数无关。
  • 事件read:epoll来监听socket,当事件发生后将对应的事件信息直接复制到公共内存中,用户直接取就可以,避免了用户来判断和频繁的数据拷贝。通过内核与用户空间共享(mmap())同一块内存来避免拷贝。

mmap(一种内存映射文件的方法)_百度百科 (baidu.com)

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

当事件触发时,epoll会将数据复制到events(struct epoll_events)中,然后epoll_wait()会返回事件数量,程序直接从event中读取数据即可。

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

struct eventpoll{
    ....
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
    ....
};

每一个epoll对象都有一个独立的eventpoll结构体,epoll中内置了红黑树和双向链表,红黑树用于存放通过epoll_ctl方法向epoll对象中添加进来的事件,双向链表用来存放满足条件的事件

而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

struct epoll_event{
  uint32_t events;   /* 具体监听的事件如:EPOLLIN*/
  epoll_data_t data;    /* User data variable,可以存放监听的socket的fd */
} 

typedef union epoll_data{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

epoll.jpg

为什么使用红黑树?

因为epoll_ctl方法可以向epoll中添加、修改、删除事件,在添加的过程中也要判断是否有重复的事件(等于要遍历一遍),因此事件的操作涉及到增删改查,红黑树作为弱平衡的排序二叉树,查询效率为lgn,同时因为只追求若平衡,插入和删除的效率也非常高,平均调整的次数在三次左右。

为什么使用双向链表?

因为双向链表插入和删除非常的方便,当事件发生时将事件信息拷贝到用户态,然后将事件从链表删除;事件发生时又要插入。

epoll用法:

第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。参数size是你在epoll上能够监听的socket的数量。

第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。参数fd表示监听的socket fd,参数event表示监听的socket的什么事件(EPOLLIN,EPOLLOUT等)。

第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

仔细体会 epoll中的et lt模式 - 克莱尔孙 - 博客园

  • 边沿触发(edge triggered),就是在事务的两个状态交替的边沿触发,对socket来讲就是的新的数据来的时候触发,如果是上一次的数据,你没有收完 则不会再次提醒。除非有新的数据到来。
  • 电平触发(levle triggered),所谓电平触发只要是事务的某一状态出现就触发,对sockets来讲就是只有内核缓冲区中有数据,就会触发。而不管这数据是你上次没收完,还是新来数据。
// 指定水平触发

 38 /* 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) */
 39 void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type){
 41     struct epoll_event ep_event;
 42     ep_event.data.fd = fd;
 43     ep_event.events = EPOLLIN;
 44 
 45     /* 如果是ET模式,设置EPOLLET,使用“|”就可以监听多个事件 */
 46     if (epoll_type == EPOLL_ET)
 47         ep_event.events |= EPOLLET;
 48 
 49     /* 设置是否阻塞 */
 50     if (block_type == FD_NONBLOCK)
 51         set_nonblock(fd);
 52 
 53     epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event);
    }

一:代码场景:

  •    1:ET模式,得到可读事件,但是不去read数据,看epoll_wait以后是否有事件提醒。

    2:LT模式,得到可读事件,但是不去read数据,看epoll_wait以后是否有事件提醒。

  •  1:ET模式,没有及时读取数据,再次有新数据到来,能不能继续读取上次的数据。

  验证结果:ET模式中,接收到新数据,给数据提醒,无论读不读数据,读取多少,都只提醒一次。如果没有及时读取,则下次有新数据到来,则读取上次数据。

         LT模式中,接收到新数据,给数据提醒,只要不读完数据,一直提醒。

二:如何处理ET模式场景

  解决此问题有两种方法:
  1.采用非阻塞函数,将数据收完后,再次调用epoll_wait()。

为什么要采用非阻塞的方式读取数据?

  • ET模式下每次write或read需要循环write或read直到errno=EAGAIN(再试一次。常见于在非阻塞模式下读取文件,当没有数据可读时,errno=EAGAIN)错误。以读操作为例,这是因为ET模式只在socket描述符状态发生变化时才触发事件,如果不一次把socket内核缓冲区的数据读完,会导致socket内核缓冲区中即使还有一部分数据,该socket的可读事件也不会被触发。
  • 根据上面的讨论,若ET模式下使用阻塞IO(最后read不到数据会阻塞而不是返回EAGAIN),则程序一定会阻塞在最后一次write或read操作,因此说ET模式下一定要使用非阻塞IO。

errno:errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。

  2.在接收数据后,不管有没有收完,都调用 epoll_ctl() with EPOLL_CTL_MOD,这样相当于重新设置事件,就可以收到数据。

int epfd = epoll_create(10);

...
struct epoll_event envent[5];
for (int i=0;i<5;i++){
    static struct epoll_event ev;
    // socket server接收连接(简化了)。封装epoll_event,监听的时间(EPOLLIN),监听的socketfd
    en.data.fd = accept(socketServerFd);
    ev.event = EPOLLIN;
    // 将epoll_event添加到epoll中
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}

while (1){
    prints("round again");
    // 等待read事件就绪,最大接收5个读就绪socket的数据,非阻塞调用。返回就绪到的socket个数
    nfds = epoll_wait(epfd, events, 5, 10000);

    for (i=0;i<nfds;i++){
        memset(buffer, 0, MAXBUF);
        // 从就绪的socket缓冲区中读数据
        read(events[i].data.fd, buffer, MAXBUF);
        prints(buffer);
    }
}

select、epoll的区别

  1. select、poll能监听的socket进程的个数是单个进程能监听的上限1024(可以修改);poll才采用链表来维护文件描述符列表,监听的数量只跟内存有关,也是系统的上限;epoll能监听的个数是整个系统的上限,1G内存大概是10万个。
  2. 当IO中断产生时,select、poll在内核中采用主动轮训所有socket的方式来判断哪些socket事件就绪;epoll采用异步回调的方式,就绪socket进程会主动通知epoll,epoll只用关心活跃的socket。
  3. 当有就绪事件产生时,select、poll是通过内存拷贝的方式将就绪列表从内核空间拷贝到用户空间;epoll是通过mmap开辟一块共享空间(用户程序创建的epoll_event数组),把就绪事件写到共享空间中,用户进程直接读取即可。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值