epoll监听文件_epoll学习笔记

750ad6a340a5e9d23eabba5edbbd4bb5.png

一. 阻塞式I/O 与非阻塞式I/O

阻塞式I/O和非阻塞式I/O:当进程调用一个进行I/O操作的API 时(例如read), 阻塞式I/O 就会卡在那里。 而非阻塞式I/O 会立即返回。

非阻塞式I/O 的好处是,在I/O 数据到来之前, 进程可以去做其他的事情。

但是此时进程有可能没有拿到想要的数据,怎么办呢? 两种方式:

  • 同步:I/O 进程等待I/O操作返回之后再处理接下来的事情。
  • 异步:在I/O操作返回结果之前去干其他的事情,等I/O 返回之后再进行相应的处理。

如果是非阻塞型I/O,因为进程没办法知道数据什么时候才真正读取完毕了,需要等待所以需要每隔一段时间就去轮询一下(就是重新调用 read,看是不是数据真的已经读取完毕了)。

早期的异步实现方式是内核给进程发信号(SIGIO 或者 SIGPOLL)。数据读写完毕后,内核发信号给进程,然后进程内的信号处理函数再调用 read 读取数据(这时可以确保数据真的已经读取完毕了)。但这种方式有一个小小的瑕疵,就是在进程进行多个 fd 读写的时候,信号来的时候没办法分清到底是哪个 fd 上的数据已经真正准备好了。所以进程还是要对所有持有的 fd 进行 read 调用。

看一段socket 编程的代码:

//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);   
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)

recv默认是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。

如果要把这段代码recv 改成非阻塞的, 就需要写个while 循环去循。

过程可以总结为:

进程等待数据 -> 接收网络数据 -> 进程中断 -> 切换到内核态 -> 数据写入socket -> 进程唤醒 -> 数据处理

看起来非常的简单, 然而这是监听当个socket 的情况,那如何监听多个socket 呢?

二. I/O 多路复用模型

I/O 多路复用的诞生就是为了解决同时监听多个socket 的场景。

最开始的时候, 人们想出的方案是, 每来一个连接, 就fork 出一个进程/线程去/接收处理它。 但是,当连接数超过CPU 数目的时候, 这个模型就会越来越慢。。

后来,select诞生, 一个进程和一个线程就可以搞定多个I/O连接。

对应的思想就是:每当发生I/O事件, 就遍历监听的所有的文件描述符,找到那个I/O 事件的文件描述符。

这样存在的问题就是当监听的文件描述符过多, 遍历一遍也会产生巨大的时间消耗。

后来在2002年的时候,发明了epoll , epoll 的本质,就是有一个list 记录下准备就绪的文件描述符。

三. select 模型

接下来让我们来看下select 模型的代码:

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...)
listen(s, ...)

int fds[] =  存放需要监听的socket

while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }
}

先准备一个数组(下面代码中的fds),让fds存放着所有需要监视的socket。然后调用select,如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。用户可以遍历fds,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

在上面的过程中, 进程会阻塞在select 的地方。

这样存在的问题在于select 每次唤醒进程之后进程还是不知道哪个socket有数据,要遍历所有监听的socket, 会产生很大开销。 于是就有了epoll。

四. epoll 模型

epoll 针对上面说的那种情况采取的优化策略: epoll 会维护一个就绪队列rdlist 来引用收到数据的socket ,当进程被唤醒, 就可以直接处理不用再去遍历了。

我们先来看一段epoll 伪代码:

1 fd = socket_connect()  #建立一个网络连接
2 efd = epoll_create(0)   #创建一个epoll
3 epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event)  #将网络连接 fd 添加到efd中
4 n = epoll_wait(efd, events, MAXEVENTS, -1) #从 list 中获取已经就绪的 fd 的数量
5 for i in range(n):
6     ev = events[i]   # 从events 内存中获取已经就绪的 fd,执行相关操作
7     doing(ev)

epoll_crteate() : 文件描述符fd可以理解为文件的引用,epoll_create() 的作用为创建一个类型为struct eventpoll的对象,用这个对象来管理epoll的所有事件。 并获得一个空闲的文件描述符fd。 将fd , file 和 eventpoll 进行关联,返回给用户态, 方便用户态就行操作。(通过fd 就可以获得eventpoll)。

在上面提到过epoll 通过就绪列表来解决遍历耗时的问题,eventpoll 作为一个管理结构,应该维持一个就绪队列rdllist, 让我们先来看下这个就绪队列应该具有什么特性:

rdllist 引用着就绪的socket,所以它应能够快速的插入数据。程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。同时,为了避免重复添加,还应当便于搜索

eventpoll 的结构如图所示:

bfe6fd02032d17591ec8ad360a451c48.png

rdllist 用于收集已经就绪了的item对象,rdlist并非直接引用socket,而是通过epitem间接引用,红黑树的节点也是epitem对象。

看到这张图是不是有种似曾相识的感觉?(如果熟悉索引结构或者看过本专栏的前几篇文章)

没错,又是熟悉的配方: epoll 要求socket 频繁的加入就绪队列和移出就绪队列怎么办? 拿对象包一层串成链表。 需要快速查找怎么办? 加个红黑树做索引。

epoll_ctl() : 提供给用户态应用程序向epoll中添加、删除和修改感兴趣的事件,其中epfd就是通过epoll_create()系统调用获取的epoll对象文件描述符,op用于指明epoll如何操作event事件。

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

epoll_wait() : 系统调用主要是用于收集在epoll中监控的就绪事件。epoll_wait()函数返回值表示的是获取到的就绪事件个数,epfd表示的epoll对象fd,第二个参数则是已经分配好内存的epoll_event结构体数组,用于给内核存放就绪事件的;

五. epoll 的边缘触发(ET)和水平触发(LT)

  • 边缘触发:当一个新的事件到来的时候,应用程序可以通过epoll_wait()系统调用获取到这个就绪事件,但是如果用户态应用程序没有一次性处理完这个事件对应的套接字缓冲区的话,那么在这个套接字没有新事件到来之前,epoll_wait()都不会在返回这个事件了。
  • 水平触发 : 只要某个事件对应的套接字缓冲区中还有数据没有处理完,那么在调用epoll_wait()的时候总能获取到这个就绪事件。
已标记关键词 清除标记
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页