‘壹’ linux中异步IO模型有哪些
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
其中前4种都是同步,最后一种才是异步。
‘贰’ 面试必问的epoll技术,从内核源码出发彻底搞懂epoll
epoll是linux中IO多路复用的一种机制,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。当然linux中IO多路复用不仅仅是epoll,其他多路复用机制还有select、poll,但是接下来介绍epoll的内核实现。
events可以是以下几个宏的集合:
epoll相比select/poll的优势 :
epoll相关的内核代码在fs/eventpoll.c文件中,下面分别分析epoll_create、epoll_ctl和epoll_wait三个函数在内核中的实现,分析所用linux内核源码为4.1.2版本。
epoll_create用于创建一个epoll的句柄,其在内核的系统实现如下:
sys_epoll_create:
可见,我们在调用epoll_create时,传入的size参数,仅仅是用来判断是否小于等于0,之后再也没有其他用处。
整个函数就3行代码,真正的工作还是放在sys_epoll_create1函数中。
sys_epoll_create -> sys_epoll_create1:
sys_epoll_create1 函数流程如下:
sys_epoll_create -> sys_epoll_create1 -> ep_alloc:
sys_epoll_create -> sys_epoll_create1 -> ep_alloc -> get_unused_fd_flags:
linux内核中,current是个宏,返回的是一个task_struct结构(我们称之为进程描述符)的变量,表示的是当前进程,进程打开的文件资源保存在进程描述符的files成员里面,所以current->files返回的当前进程打开的文件资源。rlimit(RLIMIT_NOFILE) 函数获取的是当前进程可以打开的最大文件描述符数,这个值可以设置,默认是1024。
相关视频推荐:
支撑亿级io的底层基石 epoll实战揭秘
网络原理tcp/udp,网络编程epoll/reactor,面试中正经“八股文”
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要更多C/C++ Linux服务器架构师学习资料加群 812855908 获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
__alloc_fd的工作是为进程在[start,end)之间(备注:这里start为0, end为进程可以打开的最大文件描述符数)分配一个可用的文件描述符,这里就不继续深入下去了,代码如下:
sys_epoll_create -> sys_epoll_create1 -> ep_alloc -> get_unused_fd_flags -> __alloc_fd:
然后,epoll_create1会调用anon_inode_getfile,创建一个file结构,如下:
sys_epoll_create -> sys_epoll_create1 -> anon_inode_getfile:
anon_inode_getfile函数中首先会alloc一个file结构和一个dentry结构,然后将该file结构与一个匿名inode节点anon_inode_inode挂钩在一起,这里要注意的是,在调用anon_inode_getfile函数申请file结构时,传入了前面申请的eventpoll结构的ep变量,申请的file->private_data会指向这个ep变量,同时,在anon_inode_getfile函数返回来后,ep->file会指向该函数申请的file结构变量。
简要说一下file/dentry/inode,当进程打开一个文件时,内核就会为该进程分配一个file结构,表示打开的文件在进程的上下文,然后应用程序会通过一个int类型的文件描述符来访问这个结构,实际上内核的进程里面维护一个file结构的数组,而文件描述符就是相应的file结构在数组中的下标。
dentry结构(称之为“目录项”)记录着文件的各种属性,比如文件名、访问权限等,每个文件都只有一个dentry结构,然后一个进程可以多次打开一个文件,多个进程也可以打开同一个文件,这些情况,内核都会申请多个file结构,建立多个文件上下文。但是,对同一个文件来说,无论打开多少次,内核只会为该文件分配一个dentry。所以,file结构与dentry结构的关系是多对一的。
同时,每个文件除了有一个dentry目录项结构外,还有一个索引节点inode结构,里面记录文件在存储介质上的位置和分布等信息,每个文件在内核中只分配一个inode。 dentry与inode描述的目标是不同的,一个文件可能会有好几个文件名(比如链接文件),通过不同文件名访问同一个文件的权限也可能不同。dentry文件所代表的是逻辑意义上的文件,记录的是其逻辑上的属性,而inode结构所代表的是其物理意义上的文件,记录的是其物理上的属性。dentry与inode结构的关系是多对一的关系。
sys_epoll_create -> sys_epoll_create1 -> fd_install:
总结epoll_create函数所做的事:调用epoll_create后,在内核中分配一个eventpoll结构和代表epoll文件的file结构,并且将这两个结构关联在一块,同时,返回一个也与file结构相关联的epoll文件描述符fd。当应用程序操作epoll时,需要传入一个epoll文件描述符fd,内核根据这个fd,找到epoll的file结构,然后通过file,获取之前epoll_create申请eventpoll结构变量,epoll相关的重要信息都存储在这个结构里面。接下来,所有epoll接口函数的操作,都是在eventpoll结构变量上进行的。
所以,epoll_create的作用就是为进程在内核中建立一个从epoll文件描述符到eventpoll结构变量的通道。
epoll_ctl接口的作用是添加/修改/删除文件的监听事件,内核代码如下:
sys_epoll_ctl:
根据前面对epoll_ctl接口的介绍,op是对epoll操作的动作(添加/修改/删除事件),ep_op_has_event(op)判断是否不是删除操作,如果op != EPOLL_CTL_DEL为true,则需要调用_from_user函数将用户空间传过来的event事件拷贝到内核的epds变量中。因为,只有删除操作,内核不需要使用进程传入的event事件。
接着连续调用两次fdget分别获取epoll文件和被监听文件(以下称为目标文件)的file结构变量(备注:该函数返回fd结构变量,fd结构包含file结构)。
接下来就是对参数的一些检查,出现如下情况,就可以认为传入的参数有问题,直接返回出错:
当然下面还有一些关于操作动作如果是添加操作的判断,这里不做解释,比较简单,自行阅读。
在ep里面,维护着一个红黑树,每次添加注册事件时,都会申请一个epitem结构的变量表示事件的监听项,然后插入ep的红黑树里面。在epoll_ctl里面,会调用ep_find函数从ep的红黑树里面查找目标文件表示的监听项,返回的监听项可能为空。
接下来switch这块区域的代码就是整个epoll_ctl函数的核心,对op进行switch出来的有添加(EPOLL_CTL_ADD)、删除(EPOLL_CTL_DEL)和修改(EPOLL_CTL_MOD)三种情况,这里我以添加为例讲解,其他两种情况类似,知道了如何添加监听事件,其他删除和修改监听事件都可以举一反三。
为目标文件添加监控事件时,首先要保证当前ep里面还没有对该目标文件进行监听,如果存在(epi不为空),就返回-EEXIST错误。否则说明参数正常,然后先默认设置对目标文件的POLLERR和POLLHUP监听事件,然后调用ep_insert函数,将对目标文件的监听事件插入到ep维护的红黑树里面:
sys_epoll_ctl -> ep_insert:
前面说过,对目标文件的监听是由一个epitem结构的监听项变量维护的,所以在ep_insert函数里面,首先调用kmem_cache_alloc函数,从slab分配器里面分配一个epitem结构监听项,然后对该结构进行初始化,这里也没有什么好说的。我们接下来看ep_item_poll这个函数调用:
sys_epoll_ctl -> ep_insert -> ep_item_poll:
ep_item_poll函数里面,调用目标文件的poll函数,这个函数针对不同的目标文件而指向不同的函数,如果目标文件为套接字的话,这个poll就指向sock_poll,而如果目标文件为tcp套接字来说,这个poll就是tcp_poll函数。虽然poll指向的函数可能会不同,但是其作用都是一样的,就是获取目标文件当前产生的事件位,并且将监听项绑定到目标文件的poll钩子里面(最重要的是注册ep_ptable_queue_proc这个poll callback回调函数),这步操作完成后,以后目标文件产生事件就会调用ep_ptable_queue_proc回调函数。
接下来,调用list_add_tail_rcu将当前监听项添加到目标文件的f_ep_links链表里面,该链表是目标文件的epoll钩子链表,所有对该目标文件进行监听的监听项都会加入到该链表里面。
然后就是调用ep_rbtree_insert,将epi监听项添加到ep维护的红黑树里面,这里不做解释,代码如下:
sys_epoll_ctl -> ep_insert -> ep_rbtree_insert:
前面提到,ep_insert有调用ep_item_poll去获取目标文件产生的事件位,在调用epoll_ctl前这段时间,可能会产生相关进程需要监听的事件,如果有监听的事件产生,(revents & event->events 为 true),并且目标文件相关的监听项没有链接到ep的准备链表rdlist里面的话,就将该监听项添加到ep的rdlist准备链表里面,rdlist链接的是该epoll描述符监听的所有已经就绪的目标文件的监听项。并且,如果有任务在等待产生事件时,就调用wake_up_locked函数唤醒所有正在等待的任务,处理相应的事件。当进程调用epoll_wait时,该进程就出现在ep的wq等待队列里面。接下来讲解epoll_wait函数。
总结epoll_ctl函数:该函数根据监听的事件,为目标文件申请一个监听项,并将该监听项挂人到eventpoll结构的红黑树里面。
epoll_wait等待事件的产生,内核代码如下:
sys_epoll_wait:
首先是对进程传进来的一些参数的检查:
参数全部检查合格后,接下来就调用ep_poll函数进行真正的处理:
sys_epoll_wait -> ep_poll:
ep_poll中首先是对等待时间的处理,timeout超时时间以ms为单位,timeout大于0,说明等待timeout时间后超时,如果timeout等于0,函数不阻塞,直接返回,小于0的情况,是永久阻塞,直到有事件产生才返回。
当没有事件产生时((!ep_events_available(ep))为true),调用__add_wait_queue_exclusive函数将当前进程加入到ep->wq等待队列里面,然后在一个无限for循环里面,首先调用set_current_state(TASK_INTERRUPTIBLE),将当前进程设置为可中断的睡眠状态,然后当前进程就让出cpu,进入睡眠,直到有其他进程调用wake_up或者有中断信号进来唤醒本进程,它才会去执行接下来的代码。
如果进程被唤醒后,首先检查是否有事件产生,或者是否出现超时还是被其他信号唤醒的。如果出现这些情况,就跳出循环,将当前进程从ep->wp的等待队列里面移除,并且将当前进程设置为TASK_RUNNING就绪状态。
如果真的有事件产生,就调用ep_send_events函数,将events事件转移到用户空间里面。
sys_epoll_wait -> ep_poll -> ep_send_events:
ep_send_events没有什么工作,真正的工作是在ep_scan_ready_list函数里面:
sys_epoll_wait -> ep_poll -> ep_send_events -> ep_scan_ready_list:
ep_scan_ready_list首先将ep就绪链表里面的数据链接到一个全局的txlist里面,然后清空ep的就绪链表,同时还将ep的ovflist链表设置为NULL,ovflist是用单链表,是一个接受就绪事件的备份链表,当内核进程将事件从内核拷贝到用户空间时,这段时间目标文件可能会产生新的事件,这个时候,就需要将新的时间链入到ovlist里面。
仅接着,调用sproc回调函数(这里将调用ep_send_events_proc函数)将事件数据从内核拷贝到用户空间。
sys_epoll_wait -> ep_poll -> ep_send_events -> ep_scan_ready_list -> ep_send_events_proc:
ep_send_events_proc回调函数循环获取监听项的事件数据,对每个监听项,调用ep_item_poll获取监听到的目标文件的事件,如果获取到事件,就调用__put_user函数将数据拷贝到用户空间。
回到ep_scan_ready_list函数,上面说到,在sproc回调函数执行期间,目标文件可能会产生新的事件链入ovlist链表里面,所以,在回调结束后,需要重新将ovlist链表里面的事件添加到rdllist就绪事件链表里面。
同时在最后,如果rdlist不为空(表示是否有就绪事件),并且由进程等待该事件,就调用wake_up_locked再一次唤醒内核进程处理事件的到达(流程跟前面一样,也就是将事件拷贝到用户空间)。
到这,epoll_wait的流程是结束了,但是有一个问题,就是前面提到的进程调用epoll_wait后会睡眠,但是这个进程什么时候被唤醒呢?在调用epoll_ctl为目标文件注册监听项时,对目标文件的监听项注册一个ep_ptable_queue_proc回调函数,ep_ptable_queue_proc回调函数将进程添加到目标文件的wakeup链表里面,并且注册ep_poll_callbak回调,当目标文件产生事件时,ep_poll_callbak回调就去唤醒等待队列里面的进程。
总结一下epoll该函数: epoll_wait函数会使调用它的进程进入睡眠(timeout为0时除外),如果有监听的事件产生,该进程就被唤醒,同时将事件从内核里面拷贝到用户空间返回给该进程。
‘叁’ 什么是i/o复用
当你编写的程序需要同时处理多个描数字(socket或file或device),你又不知道什么时候应该(比方说有数据可以读了)去操作(读/写)哪个描数字。这时候I/O复用就需要登场了。
I/O复用是一种让进程预先“警告”内核能力,使得内核一旦发现进程预先告知时指定的一个或多个I/O条件(就是描述符)就绪(可以读/写了),内核就通知进程。linux有4个调用可实现I/O复用:select、poll继承自Unix系统。pselect是select到Posix版。epoll是linux2.6内核特有的。
‘肆’ Linux系统I/O模型及select、poll、epoll原理和应用
理解Linux的IO模型之前,首先要了解一些基本概念,才能理解这些IO模型设计的依据
操作系统使用虚拟内存来映射物理内存,对于32位的操作系统来说,虚拟地址空间为4G(2^32)。操作系统的核心是内核,为了保护用户进程不能直接操作内核,保证内核安全,操作系统将虚拟地址空间划分为内核空间和用户空间。内核可以访问全部的地址空间,拥有访问底层硬件设备的权限,普通的应用程序需要访问硬件设备必须通过 系统调用 来实现。
对于Linux系统来说,将虚拟内存的最高1G字节的空间作为内核空间仅供内核使用,低3G字节的空间供用户进程使用,称为用户空间。
又被称为标准I/O,大多数文件系统的默认I/O都是缓存I/O。在Linux系统的缓存I/O机制中,操作系统会将I/O的数据缓存在页缓存(内存)中,也就是数据先被拷贝到内核的缓冲区(内核地址空间),然后才会从内核缓冲区拷贝到应用程序的缓冲区(用户地址空间)。
这种方式很明显的缺点就是数据传输过程中需要再应用程序地址空间和内核空间进行多次数据拷贝操作,这些操作带来的CPU以及内存的开销是非常大的。
由于Linux系统采用的缓存I/O模式,对于一次I/O访问,以读操作举例,数据先会被拷贝到内核缓冲区,然后才会从内核缓冲区拷贝到应用程序的缓存区,当一个read系统调用发生的时候,会经历两个阶段:
正是因为这两个状态,Linux系统才产生了多种不同的网络I/O模式的方案
Linux系统默认情况下所有socke都是blocking的,一个读操作流程如下:
以UDP socket为例,当用户进程调用了recvfrom系统调用,如果数据还没准备好,应用进程被阻塞,内核直到数据到来且将数据从内核缓冲区拷贝到了应用进程缓冲区,然后向用户进程返回结果,用户进程才解除block状态,重新运行起来。
阻塞模行下只是阻塞了当前的应用进程,其他进程还可以执行,不消耗CPU时间,CPU的利用率较高。
Linux可以设置socket为非阻塞的,非阻塞模式下执行一个读操作流程如下:
当用户进程发出recvfrom系统调用时,如果kernel中的数据还没准备好,recvfrom会立即返回一个error结果,不会阻塞用户进程,用户进程收到error时知道数据还没准备好,过一会再调用recvfrom,直到kernel中的数据准备好了,内核就立即将数据拷贝到用户内存然后返回ok,这个过程需要用户进程去轮询内核数据是否准备好。
非阻塞模型下由于要处理更多的系统调用,因此CPU利用率比较低。
应用进程使用sigaction系统调用,内核立即返回,等到kernel数据准备好时会给用户进程发送一个信号,告诉用户进程可以进行IO操作了,然后用户进程再调用IO系统调用如recvfrom,将数据从内核缓冲区拷贝到应用进程。流程如下:
相比于轮询的方式,不需要多次系统调用轮询,信号驱动IO的CPU利用率更高。
异步IO模型与其他模型最大的区别是,异步IO在系统调用返回的时候所有操作都已经完成,应用进程既不需要等待数据准备,也不需要在数据到来后等待数据从内核缓冲区拷贝到用户缓冲区,流程如下:
在数据拷贝完成后,kernel会给用户进程发送一个信号告诉其read操作完成了。
是用select、poll等待数据,可以等待多个socket中的任一个变为可读,这一过程会被阻塞,当某个套接字数据到来时返回,之后再用recvfrom系统调用把数据从内核缓存区复制到用户进程,流程如下:
流程类似阻塞IO,甚至比阻塞IO更差,多使用了一个系统调用,但是IO多路复用最大的特点是让单个进程能同时处理多个IO事件的能力,又被称为事件驱动IO,相比于多线程模型,IO复用模型不需要线程的创建、切换、销毁,系统开销更小,适合高并发的场景。
select是IO多路复用模型的一种实现,当select函数返回后可以通过轮询fdset来找到就绪的socket。
优点是几乎所有平台都支持,缺点在于能够监听的fd数量有限,Linux系统上一般为1024,是写死在宏定义中的,要修改需要重新编译内核。而且每次都要把所有的fd在用户空间和内核空间拷贝,这个操作是比较耗时的。
poll和select基本相同,不同的是poll没有最大fd数量限制(实际也会受到物理资源的限制,因为系统的fd数量是有限的),而且提供了更多的时间类型。
总结:select和poll都需要在返回后通过轮询的方式检查就绪的socket,事实上同时连的大量socket在一个时刻只有很少的处于就绪状态,因此随着监视的描述符数量的变多,其性能也会逐渐下降。
epoll是select和poll的改进版本,更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的只需一次。
epoll_create()用来创建一个epoll句柄。
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个就绪链表中管理。
epoll_wait() 可以从就绪链表中得到事件完成的描述符,因此进程不需要通过轮询来获得事件完成的描述符。
当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序可以不立即处理该事件,下次调用epoll_wait还会再次通知该事件,支持block和nonblocking socket。
当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序需要立即处理该事件,如果不立即处理,下次调用epoll_wait不会再次通知该事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用nonblocking socket,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
【segmentfault】 Linux IO模式及 select、poll、epoll详解
【GitHub】 CyC2018/CS-Notes
‘伍’ IO多路复用的三种机制Select,Poll,Epoll
select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
相信大家都了解了Unix五种IO模型,不了解的可以 => 查看这里
[1] blocking IO - 阻塞IO
[2] nonblocking IO - 非阻塞IO
[3] IO multiplexing - IO多路复用
[4] signal driven IO - 信号驱动IO
[5] asynchronous IO - 异步IO
其中前面4种IO都可以归类为synchronous IO - 同步IO,而select、poll、epoll本质上也都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
在介绍select、poll、epoll之前,首先介绍一下Linux操作系统中 基础的概念 :
我们先分析一下select函数
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
【参数说明】
int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1。
fd_set *readset , fd_set *writeset , fd_set *exceptset
fd_set 可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。
const struct timeval *timeout timeout 告知内核等待所指定文件描述符集合中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
【返回值】
int 若有就绪描述符返回其数目,若超时则为0,若出错则为-1
select()的机制中提供一种 fd_set 的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。
下面是pll的函数原型:
poll改变了文件描述符集合的描述方式,使用了 pollfd 结构而不是select的 fd_set 结构,使得poll支持的文件描述符集合限制远大于select的1024
【参数说明】
struct pollfd *fds fds 是一个 struct pollfd 类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后 fds 数组不会被清空;一个 pollfd 结构体表示一个被监视的文件描述符,通过传递 fds 指示 poll() 监视多个文件描述符。其中,结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的 revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
nfds_t nfds 记录数组 fds 中描述符的总数量
【返回值】
int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的只需一次。
Linux中提供的epoll相关函数如下:
1. epoll_create 函数创建一个epoll句柄,参数 size 表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。
2. epoll_ctl 函数注册要监听的事件类型。四个参数解释如下:
epoll_event 结构体定义如下:
3. epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显着提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。比如:0->1 就是Edge,1->1 就是Level。
ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
一张图总结一下select,poll,epoll的区别:
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
既然select,poll,epoll都是I/O多路复用的具体的实现,之所以现在同时存在,其实他们也是不同历史时期的产物
‘陆’ 干货 五种IO模型的特点以及比较
为了保证操作系统的安全,将内存划分为内核空间和用户空间。内核空间的进程,可以访问硬件执行IO等操作,用户空间的进程只能通过系统调用来访问IO等系统资源。
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后再拷贝到应用程序的地址空间。
所以,当一个read操作发生时,它会经历两个阶段
正是因为这两个阶段,linux系统产生了下面五种网络模型:
在linux下,默认情况下所有的socket都是blocking的,流程如下图所示:
进程调用recvfrom系统调用来读取数据,这是如果还没有到达,进程就进入阻塞状态。等数据到达后完成到内核去的拷贝,再从内核拷贝到用户空间,用户进程才解除阻塞状态。
特点:在IO执行的两个阶段进程都会都阻塞
执行非阻塞io系统调用时,如果内核中的数据还没有准备好,会直接返回,不会阻塞。通过进程不断查询,直到数据在内核中就绪,便开始拷贝到用户空间。拷贝的过程中,进程还是被阻塞了,所有非阻塞IO也是同步IO。
特点:需要进程不断地主动询问kernel数据是否准备好了
单个进程处理多个网络连接IO,使用select\poll\epoll三种系统调用,不断轮询所有的连接,如果有数据到达内核则通知进程,进行数据拷贝到用户内存。
当调用select时,进程会进入阻塞状态,直到有数据到达。
这个图看起来和阻塞IO区别不大,甚至还多使用了一个系统调用
但它的优势在于可以同时监控多个IO连接。
所以,如果连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web servet性能更好。多路IO复用的优势并不是对于单个连接能处理的更快,而在于能处理更多的连接。
linux下的异步io使用的很少,其流程如下:
发起read操作后进程立马返回,整个Io过程不会产生任何block。kernel会等等数据准备完成,然后将数据拷贝到用户内存。当这一切都完成后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
调用blocking io会一直block进程直到操作完成
no-blocking io在kernel准备数据的阶段是会立刻返回的
只有异步IO是异步IO,
其他3种:阻塞IO、非阻塞IO、多路复用IO都是同步的。
这是因为其他三种IO在执行真实IO操作的过程中都有进程阻塞的阶段,而异步IO在整个过程中进程都没有被阻塞。非阻塞IO在内核数据就绪,拷贝到用户空间的阶段也是阻塞的,因此也是同步IO。
Linux IO模式及select、poll、epoll详解
‘柒’ linux中block IO,no-block IO,异步IO,IO多路复用笔记
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间 。针对linux操作系统而言, 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF) ,供内核使用,称为内核空间, 而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述 指向文件的引用的抽象化概念 。文件描述符在形式上是一个非负整数。 实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表 。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
1、等待数据准备 (Waiting for the data to be ready)
2、将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面 五种网络模式 的方案。
阻塞 I/O(blocking IO)
非阻塞 I/O(nonblocking IO)
I/O 多路复用( IO multiplexing)
异步 I/O(asynchronous IO)
信号驱动 I/O( signal driven IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。
阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了(内核阻塞读取数据,内核将数据复制到应用户态)。
非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要 不断的主动询问 kernel数据好了没有( 内核读取数据时,用户态不需要阻塞,内核将数据复制到用户态时,需要阻塞 )。
I/O 多路复用( IO multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是 select,poll,epoll这个function会不断的轮询所负责的所有socket ,当某个socket有数据到达了,就通知用户进程。
当用户 进程调用了select , 那么整个进程会被block ,而同时,kernel会“监视”所有 select负责的socket(一个管理多个socket连接),当任何一个socket中的数据准备好了,select就会返回 。这个时候用户进程再调用read操作, 将数据从kernel拷贝到用户进程 。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。 因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom) 。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的 连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大 。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
总结:IO多路复用其实也是阻塞的,阻塞的地方在用当有socket连接有数据以后, 会阻塞知道数据从内核复制到用户态(第二步阻塞)。
异步 I/O(asynchronous IO)
inux下的asynchronous IO其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
总结:两个阶段都不需要用户进程干涉,内核将数据准备好以后通知用户态去读取
总结
blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的 blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO 。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是, 当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
‘捌’ 同步与异步,阻塞与非阻塞的区别,以及select,poll和epoll
异步的概念和同步相对。
(1)当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;
(2)当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过 状态、通知和回调 来通知调用者。
这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。使用哪一种通知机制,依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。
(A)阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务
(B)非阻塞调用是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回
场景比喻:
举个例子,比如我去银行办理业务,可能会有两种方式:
在上面的场景中,如果:
a)如果选择排队(同步),且排队的时候什么都不干(线程被挂起,什么都干不了),是同步阻塞模型;
b)如果选择排队(同步),但是排队的同时做与办银行业务无关的事情,比如抽烟,(线程没有被挂起,还可以干一些其他的事),是同步非阻塞模型;
c)如果选择拿个小票,做在位置上等着叫号(通知),但是坐在位置上什么都不干(线程被挂起,什么都干不了),这是异步阻塞模型;
d)如果选择那个小票,坐在位置上等着叫号(通知),但是坐着的同时还打电话谈生意(线程没有被挂起,还可以干其他事情),这是异步非阻塞模型。
对这四种模型做一个总结:
1:同步阻塞模型,效率最低,即你专心排队,什么都不干。
2:异步阻塞,效率也非常低,即你拿着号等着被叫(通知),但是坐那什么都不干
3:同步非阻塞,效率其实也不高,因为涉及到线程的来回切换。即你在排队的同时打电话或者抽烟,但是你必须时不时得在队伍中挪动。程序需要在排队和打电话这两种动作之间来回切换,系统开销可想而知。
4:异步非阻塞,效率很高,你拿着小票在那坐着等叫号(通知)的同时,打电话谈你的生意。
linux下几个基本概念
1:用户控件和内核空间。 现代操作系统都是采用虚拟存储器,在32位操作系统下,它的寻址空间(虚拟存储空间)为4G(2的32次方)。为了保证用户进程补鞥呢直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。对linux操作系统而言,将最高的1G字节空间分给了内核使用,称为内核空间,将较低的3G字节的空间划分为用户空间。
2:进程切换很耗资源 ,为了控制进程的执行,内核必须有能力挂起正在cpu上运行的进程,并恢复以前挂起的某个进程的执行,这种行为叫进程的切换。每次切换,要保存上一个的上下文环境等等,总之记住进程切换很耗资源。
3:文件描述符 :文件描述符在形式上是一个非负整数。实际上,他是一个索引,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个文件时,内核就会向进程返回一个非负整数的文件描述符。但是文件描述符一般在unix,linux系统中才讲。
缓存IO ,大多数系统的默认IO操作都是缓存IO,在linux的缓存IO机制中,操作系统会将IO的数据缓存在系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。 缓存IO的缺点: 数据在传输过程中需要在应用程序和地址空间和内核进行多次数据拷贝操作,这种数据拷贝操作锁带来的cpu以及内存消耗是很大的。
LINUX的IO模型
网络IO的本质是socket的读取。socket在linux系统被抽象为流,故对网络IO的操作可以理解为对流的操作。
对于一次IO访问,比如以read操作为例, 数据会先被拷贝到操作系统内核的缓冲区,然后才会从内核缓冲区拷贝到进程的用户层,即应用程序的地址空间 。故当一个read操作发生时,其实是经历了两个阶段:
1:内核缓冲区的数据就位
2:数据从内核缓冲区拷贝到用户程序地址空间
那么具体到socket io的一次read操来说,这两步分别是:
1:等待网络上的数据分组到达,然后复制到内核缓冲区中
2:数据从内核缓冲区拷贝到用户程序的地址空间(缓冲区)
所以说 网络应用要处理的无非就两个问题:网络IO和数据计算 ,一般来说网络io带来的延迟影响比较大。
网络IO的模型大致有如下几种:
熟悉不? 我们常说的select,poll和epoll就是属于同步模型中多路复用IO的不同实现方法罢了。 下面分别对同步阻塞,同步不阻塞,同步io复用进行说明。
一:同步阻塞
它是最简单也最常用的网络IO模型。linux下默认的socket都是blocking的。
从图中可以看到,用户进程调用recvfrom这个系统调用后,就处于阻塞状态。然后kernel就开始了IO的第一个阶段:数据准备。等第一个阶段准备完成之后,kernel开始第二阶段,将数据从内核缓冲区拷贝到用户程序缓冲区(需要花费一定时间)。然后kernel返回结果(确切的说是recvfrom这个系统调用函数返回结果),用户进程才结束blocking,重新运行起来。
总结 : 同步阻塞模型下,用户程序在kernel执行io的两个阶段都被blocking住了 。但是优点也是因为这个,无延迟能及时返回数据,且程序模型简单。
二:同步非阻塞
同步非阻塞就是隔一会瞄一下的轮询方式。同步非阻塞模式其实是可以看做一小段一小段的同步阻塞模式。
三:IO多路复用
由于同步非阻塞方式需要不断的轮询,光轮询就占据了很大一部分过程,且消耗cpu资源。而这个用户进程可能不止对这个socket的read,可能还有对其他socket的read或者write操作,那人们就想到了一次轮询的时候,不光只查询询一个socket fd,而是在一次轮询下,查询多个任务的socket fd的完成状态,只要有任何一个任务完成,就去处理它。而且,轮询人不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 IO多路复用 。总所周知的linux下的select,poll和epoll就是这么干的。。。
selelct调用是内核级别的,selelct轮询相比较同步非阻塞模式下的轮询的区别为: 前者可以等待多个socket,能实现同时对多个IO端口的监听 ,当其中任何一个socket数据准备好了,就返回可读。 select或poll调用之后,会阻塞进程 ,与blocking IO 阻塞不用在于,此时的select不是等到所有socket数据达到再处理,而是某个socket数据就会返回给用户进程来处理。
其实select这种相比较同步non-blocking的效果在单个任务的情况下可能还更差一些 ,因为这里调用了select和recvfrom两个system call,而non-blocking只调用了一个recvfrom,但是 用select的优势在于它可以同时处理多个socket fd 。
在io复用模型下,对于每一个socket,一般都设置成non-blocking,但是其实 整个用户进程是一直被block的 ,只不过用户process不是被socket IO给block住,而是被select这个函数block住的。
与多进程多线程技术相比,IO多路复用的最大优势是系统开销小。
一:select
select函数监视多个socket fs,直到有描述符就绪或者超时,函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。select的基本流程为:
二:poll
poll本质上跟select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd的状态,如果某个fd的状态为就绪,则将此fd加入到等待队列中并继续遍历。如果遍历完所有的fd后发现没有就绪的,则挂起当前进程,直到设备就绪或者主动超时。被唤醒后它又要再次遍历fd。
特点:
1:poll没有最大连接数限制,因为它是用基于链表来存储的,跟selelct直接监听fd不一样。
2:同样的大量的fd的数组被整体复制与用户态和内核地址空间之间。
3:poll还有一个特点是水平触发:如果报告了fd后没有被处理,则下次poll时还会再次报告该fd。
4:跟select一样,在poll返回后,还是需要通过遍历fdset来获取已经就绪的socket。当fd很多时,效率会线性下降。
三:epoll
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
聊聊同步、异步、阻塞与非阻塞
聊聊Linux 五种IO模型
聊聊IO多路复用之select、poll、epoll详解
‘玖’ Linux的五种IO模型
在linux中,对于一次读取IO请求(不仅仅是磁盘,还有网络)的操作,数据并不会直接拷贝到用户程序的用户空间缓冲区。它首先会被拷贝到操作系统的内核空间,然后才会从操作系统内核的缓冲区拷贝到用户空间的缓冲区。
大概是这个样子。
从图中可以看见,这是分四步进行的,而这四步里面有些细节,就有了这5种IO模型
前四种为同步IO,后一种为异步IO,什么是同步异步可以看看我之前写的 同步与异步,阻塞与非阻塞 。
应用进程发起系统调用后就阻塞了,直到内核buffer拷贝到用户buffer,发出成功提示后才继续执行。
适用场景:并发量小的要及时响应的网络应用开发,JavaBIO。
优点:易于开发,不消耗CPU资源(线程阻塞),及时响应。
缺点:不适用与并发量大的网络应用开发,一个请求一个线程,系统开销大。
应用进程发起系统调用,内核立马返回一个自己当前的缓冲区的状态(错误或者说成功),假如
为错误则隔段时间再系统调用(轮询),直到返回成功为止。另外再说一点,有人说轮询之间可以设置一个时间,例如每几秒执行一次,然后在这段期间程序可以干自己的事情。(这个我不清楚是不是,虽然理论上可以实现,但是我觉得第一种与第二种的区别应该强调的是是否放弃CPU,第二种有点CAS+轮询这种轻量级锁的感觉,第一种就是那种重量级锁的感觉)。
适用场景:并发量小且不用技术响应的网络应用开发
优点:易于开发,可以在轮询的间断期间继续执行程序。
缺点:不适用与并发量大的网络应用开发,一个请求一个线程,系统开销大。消耗CPU资源(轮询),不及时响应。
将多个IO注册到一个复用器上(select,poll,epoll),然后一个进程监视所有注册进来的IO。
进程阻塞在select上,而不是真正阻塞在IO系统调用上。当其中任意一个注册的IO的内核缓冲区有了数据,select就会返回(告诉程序内核态缓存有数据了),然后用户进程再发起调用,数据就从内核态buffer转到用态buffer(这段期间也是要阻塞的)。
适用场景:并发量大且对响应要求较为高的网络应用开发,JavaNIO
优点:将阻塞从多个进程转移到了一个select调用身上,假如并发量大的话select调用是不易被阻塞的,或者说阻塞时间短的。
缺点:不易开发,实现难度大,当并发量小的时候还不如同步阻塞模型。
应用程序向内核注册一个信号处理程序,然后立即返回,当数据准备好了以后(数据到了内核buffer),内核个应用进程一个信号,然后应用进程通过信号处理程序发起系统调用,然后阻塞直达数据从内核buffer复制到用户buffer。
优点:将阻塞从多个进程转移到了一个select调用身上,假如并发量大的话select调用是不易被阻塞的,或者说阻塞时间短的。
缺点:不易开发,实现难度大。
以上四个IO模型都可以看出来,到最后用户进程都要在数据从内核buffer复制到用户buffer时阻塞,直到内核告诉进程准备成功。这就是同步进程,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作
这个就是直到数据完成到用户buffer才通知。
应用场景:Java AIO,适合高性能高并发应用。
优点:不阻塞,减少了线程切换,
缺点:难以实现,要操作系统支持。
‘拾’ Handler消息机制(一):Linux的epoll机制
在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在linux新的内核中,有了一种替换它的机制,就是epoll。
相比select模型, poll使用链表保存文件描述符,因此没有了监视文件数量的限制 ,但其他三个缺点依然存在。
假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。
由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。
设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接
如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有 添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法 。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
epoll结构示意图
通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
events可以是以下几个宏的集合:
EPOLLIN:触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
EPOLLOUT:触发该事件,表示对应的文件描述符上可以写数据;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(EdgeTriggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
示例:
ET(EdgeTriggered) :高速工作模式,只支持no_block(非阻塞模式)。在此模式下,当描述符从未就绪变为就绪时,内核通过epoll告知。然后它会假设用户知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到某些操作导致那个文件描述符不再为就绪状态了。(触发模式只在数据就绪时通知一次,若数据没有读完,下一次不会通知,直到有新的就绪数据)
LT(LevelTriggered) :缺省工作方式,支持blocksocket和no_blocksocket。在LT模式下内核会告知一个文件描述符是否就绪了,然后可以对这个就绪的fd进行IO操作。如果不作任何操作,内核还是会继续通知!若数据没有读完,内核也会继续通知,直至设备数据为空为止!
1.我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)……
ET工作模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,在第2步执行了一个写操作,第三步epoll_wait会返回同时通知的事件会销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
只有当read(2)或者write(2)返回EAGAIN时(认为读完)才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时(即小于sizeof(buf)),就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
LT工作模式:
LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。
当调用 epoll_wait检查是否有发生事件的连接时,只是检查 eventpoll对象中的 rdllist双向链表是否有 epitem元素而已,如果 rdllist链表不为空,则把这里的事件复制到用户态内存中,同时将事件数量返回给用户。因此,epoll_wait的效率非常高。epoll_ctl在向 epoll对象中添加、修改、删除事件时,从 rbr红黑树中查找事件也非常快,也就是说,epoll是非常高效的,它可以轻易地处理百万级别的并发连接。
1.减少用户态和内核态之间的文件句柄拷贝;
2.减少对可读可写文件句柄的遍历。
https://cloud.tencent.com/developer/information/linux%20epoll%E6%9C%BA%E5%88%B6
https://blog.csdn.net/u010657219/article/details/44061629
https://jiahao..com/s?id=1609322251459722004&wfr=spider&for=pc