linux下的epoll有什么作用?

linux下的epoll有什么作用?,第1张

你好,希望我的回答对你有帮助 1. Epoll是何方神圣? Epoll可是当前在Linux下开发大规模并发网络程序的热门人选,Epoll 在Linux2.6内核中正式引入,和select相似,其实都I/O多路复用技术而已,并没有什么神秘的。 其实在Linux下设计并发网络程序,向来不缺少方法,比如典型的Apache模型(Process Per Connection,简称PPC),TPC(Thread PerConnection)模型,以及select模型和poll模型,那为何还要再引入Epoll这个东东呢?那还是有得说说的… 2. 常用模型的缺点 如果不摆出来其他模型的缺点,怎么能对比出Epoll的优点呢。 2.1 PPC/TPC模型 这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是PPC是为它开了一个进程,而TPC开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程/线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。 2.2 select模型 1. 最大并发数限制,因为一个进程所打开的FD(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此Select模型的最大并发数就被相应限制了。自己改改这个FD_SETSIZE?想法虽好,可是先看看下面吧… 2. 效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是,大家都慢慢来,什么?都超时了??!! 3. 内核/用户空间内存拷贝问题,如何让内核把FD消息通知给用户空间呢?在这个问题上select采取了内存拷贝方法。 2.3 poll模型 基本上效率和select是相同的,select缺点的2和3它都没有改掉。 3. Epoll的提升 把其他模型逐个批判了一下,再来看看Epoll的改进之处吧,其实把select的缺点反过来那就是Epoll的优点了。 3.1. Epoll没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。 3.2. 效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 3.3. 内存拷贝,Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了。 4. Epoll为什么高效 Epoll的高效和其数据结构的设计是密不可分的,这个下面就会提到。 首先回忆一下select模型,当有I/O事件到来时,select通知应用程序有事件到了快去处理,而应用程序必须轮询所有的FD集合,测试每个FD是否有事件发生,并处理事件;代码像下面这样: int res = select(maxfd+1, &readfds, NULL, NULL, 120)if(res >0) { for (int i = 0i <MAX_CONNECTIONi++) { if (FD_ISSET(allConnection[i], &readfds)) { handleEvent(allConnection[i])} } } // if(res == 0) handle timeout, res <0 handle error Epoll不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD 集合。 intres = epoll_wait(epfd, events, 20, 120)for(int i = 0i <resi++) { handleEvent(events[n])} 5. Epoll关键数据结构 前面提到Epoll速度快和其数据结构密不可分,其关键数据结构就是: structepoll_event { __uint32_t events// Epoll events epoll_data_t data// User data variable }typedefunion epoll_data { void *ptrint fd__uint32_t u32__uint64_t u64} epoll_data_t可见epoll_data是一个union结构体,借助于它应用程序可以保存很多类型的信息:fd、指针等等。有了它,应用程序就可以直接定位目标了。 6. 使用Epoll 既然Epoll相比select这么好,那么用起来如何呢?会不会很繁琐啊…先看看下面的三个函数吧,就知道Epoll的易用了。 int epoll_create(int size)生成一个Epoll专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个Epoll fd上能关注的最大socket fd数,大小自定,只要内存足够。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event )控制某个Epoll文件描述符上的事件:注册、修改、删除。其中参数epfd是epoll_create()创建Epoll专用的文件描述符。相对于select模型中的FD_SET和FD_CLR宏。 int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)等待I/O事件的发生;参数说明: epfd:由epoll_create() 生成的Epoll专用的文件描述符; epoll_event:用于回传代处理事件的数组; maxevents:每次能处理的事件数; timeout:等待I/O事件发生的超时值; 返回发生事件数。 相对于select模型中的select函数。 7. 例子程序 下面是一个简单Echo Server的例子程序,麻雀虽小,五脏俱全,还包含了一个简单的超时检查机制,简洁起见没有做错误处理。 参考地址:http://blog.csdn.net/sparkliang/article/details/4770655

select这个系统调用的原型如下

第一个参数nfds用来告诉内核 要扫描的socket fd的数量+1 ,select系统调用最大接收的数量是1024,但是如果每次都去扫描1024,实际上的数量并不多,则效率太低,这里可以指定需要扫描的数量。 最大数量为1024,如果需要修改这个数量,则需要重新编译Linux内核源码。

第2、3、4个参数分别是readfds、writefds、exceptfds,传递的参数应该是fd_set 类型的引用,内核会检测每个socket的fd, 如果没有读事件,就将对应的fd从第二个参数传入的fd_set中移除,如果没有写事件,就将对应的fd从第二个参数的fd_set中移除,如果没有异常事件,就将对应的fd从第三个参数的fd_set中移除 。这里我们应该 要将实际的readfds、writefds、exceptfds拷贝一份副本传进去,而不是传入原引用,因为如果传递的是原引用,某些socket可能就已经丢失

最后一个参数是等待时间, 传入0表示非阻塞,传入>0表示等待一定时间,传入NULL表示阻塞,直到等到某个socket就绪

FD_ZERO()这个函数将fd_set中的所有bit清0,一般用来进行初始化等。

FD_CLR()这个函数用来将bitmap(fd_set )中的某个bit清0,在客户端异常退出时就会用到这个函数,将fd从fd_set中删除。

FD_ISSET()用来判断某个bit是否被置1了,也就是判断某个fd是否在fd_set中。

FD_SET()这个函数用来将某个fd加入fd_set中,当客户端新加入连接时就会使用到这个函数。

epoll_create系统调用用来创建epfd,会在开辟一块内存空间(epoll的结构空间)。size为epoll上能关注的最大描述符数,不够会进行扩展,size只要>0就行,早期的设计size是固定大小,但是现在size参数没什么用,会自动扩展。

返回值是epfd,如果为-1则说明创建epoll对象失败

第一个参数epfd传入的就是epoll_create返回的epfd。

第二个参数传入对应 *** 作的宏,包括 增删改(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD)

第三个参数传入的是 需要增删改的socket的fd

第四个参数传入的是 需要 *** 作的fd的哪些事件 ,具体的事件可以看后续。

返回值是一个int类型,如果为-1则说明 *** 作失败

第一个参数是epfd,也就是epoll_create的返回值。

第二个参数是一个epoll_event类型的指针,也就是传入的是一个数组指针。 内核会将就绪的socket的事件拷贝到这个数组中,用户可以根据这个数组拿到事件和消息等

第三个参数是maxevents,传入的是 第二个参数的数组的容量

第四个参数是timeout, 如果设为-1一直阻塞直到有就绪数据为止,如果设为0立即返回,如果>0那么阻塞一段时间

返回值是一个int类型,也就是就绪的socket的事件的数量(内核拷贝给用户的events的元素的数量),通过这个数量可以进行遍历处理每个事件

一般需要传入 ev.data.fd 和 ev.events ,也就是fd和需要监控的fd的事件。事件如果需要传入多个,可以通过按位与来连接,比如需要监控读写事件,只需要像如下这样 *** 作即可: ev.events=EPOLLIN | EPOLLOUT 。

LT(水平触发), 默认 的工作模式, 事件就绪后用户可以选择处理和不处理,如果用户不处理,内核会对这部分数据进行维护,那么下次调用epoll_wait()时仍旧会打包出来

ET(边缘触发),事件就绪之后, 用户必须进行处理 ,因为内核把事件打包出来之后就把对应的就绪事件给清掉了, 如果不处理那么就绪事件就没了 。ET可以减少epoll事件被重复触发的次数,效率比LT高。

如果需要设置为边缘触发只需要设置事件为类似 ev.events=EPOLLIN | EPOLLET 即可

select/poll/epoll是nio多路复用技术, 传统的bio无法实现C10K/C100K ,也就是无法满足1w/10w的并发量,在这么高的并发量下,在进行上下文切换就很容易将服务器的负载拉飞。

1.将fd_set从用户态拷贝到内核态

2.根据fd_set扫描内存中的socket的fd的状态,时间复杂度为O(n)

3.检查fd_set,如果有已经就绪的socket,就给对应的socket的fd打标记,那么就return 就绪socket的数量并唤醒当前线程,如果没有就绪的socket就继续阻塞当前线程直到有socket就绪才将当前线程唤醒。

4.如果想要获取当前已经就绪的socket列表,则还需要进行一次系统调用,使用O(n)的时间去扫描socket的fd列表,将已经打上标记的socket的fd返回。

CPU在同一个时刻只能执行一个程序,通过RR时间片轮转去切换执行各个程序。没有被挂起的进程(线程)则在工作队列中排队等待CPU的执行,将进程(线程)从工作队列中移除就是挂起,反映到Java层面的就是线程的阻塞。

什么是中断?当我们使用键盘、鼠标等IO设备的时候,会给主板一个电流信号,这个电流信号就给CPU一个中断信号,CPU执行完当前的指令便会保存现场,然后执行键盘/鼠标等设备的中断程序,让中断程序获取CPU的使用权,在中断程序后又将现场恢复,继续执行之前的进程。

如果第一次没检测到就绪的socket,就要将其进程(线程)从工作队列中移除,并加入到socket的等待队列中。

socket包含读缓冲区+写缓冲区+等待队列(放线程或eventpoll对象)

当从客户端往服务器端发送数据时,使用TCP/IP协议将通过物理链路、网线发给服务器的网卡设备,网卡的DMA设备将接收到的的数据写入到内存中的一块区域(网卡缓冲区),然后会给CPU发出一个中断信号,CPU执行完当前指令则会保存现场,然后网卡的中断程序就获得了CPU的使用权,然后CPU便开始执行网卡的中断程序,将内存中的缓存区中的数据包拿出,判断端口号便可以判断它是哪个socket的数据,将数据包写入对应的socket的读(输入)缓冲区,去检查对应的socket的等待队列有没有等待着的进程(线程),如果有就将该线程(进程)从socket的等待队列中移除,将其加入工作队列,这时候该进程(线程)就再次拥有了CPU的使用权限,到这里中断程序就结束了。

之后这个进程(线程)就执行select函数再次去检查fd_set就能发现有socket缓冲区中有数据了,就将该socket的fd打标记,这个时候select函数就执行完了,这时候就会给上层返回一个int类型的数值,表示已经就绪的socket的数量或者是发生了错误。这个时候就再进行内核态到用户态的切换,对已经打标记的socket的fd进行处理。

将原本1024bit长度的bitmap(fd_set)换成了数组的方式传入 ,可以 解决原本1024个不够用的情况 ,因为传入的是数组,长度可以不止是1024了,因此socket数量可以更多,在Kernel底层会将数组转换成链表。

在十多年前,linux2.6之前,不支持epoll,当时可能会选择用Windows/Unix用作服务器,而不会去选择Linux,因为select/poll会随着并发量的上升,性能变得越来越低,每次都得检查所有的Socket列表。

1.select/poll每次调用都必须根据提供所有的socket集合,然后就 会涉及到将这个集合从用户空间拷贝到内核空间,在这个过程中很耗费性能 。但是 其实每次的socket集合的变化也许并不大,也许就1-2个socket ,但是它会全部进行拷贝,全部进行遍历一一判断是否就绪。

2.select/poll的返回类型是int,只能代表当前的就绪的socket的数量/发生了错误, 如果还需要知道是哪些socket就绪了,则还需要再次使用系统调用去检查哪些socket是就绪的,又是一次O(n)的 *** 作,很耗费性能

1.epoll在Kernel内核中存储了对应的数据结构(eventpoll)。我们可以 使用epoll_create()这个系统调用去创建一个eventpoll对象 ,并返回eventpoll的对象id(epfd),eventpoll对象主要包括三个部分:需要处理的正在监听的socket_fd列表(红黑树结构)、socket就绪列表以及等待队列(线程)。

2.我们可以使用epoll_ctl()这个系统调用对socket_fd列表进行CRUD *** 作,因为可能频繁地进行CRUD,因此 socket_fd使用的是红黑树的结构 ,让其效率能更高。epoll_ctl()传递的参数主要是epfd(eventpoll对象id)。

3.epoll_wait()这个系统调用默认会 将当前进程(线程)阻塞,加入到eventpoll对象的等待队列中,直到socket就绪列表中有socket,才会将该进程(线程)重新加入工作队列 ,并返回就绪队列中的socket的数量。

socket包含读缓冲区、写缓冲区和等待队列。当使用epoll_ctl()系统调用将socket新加入socket_fd列表时,就会将eventpoll对象引用加到socket的等待队列中, 当网卡的中断程序发现socket的等待队列中不是一个进程(线程),而是一个eventpoll对象的引用,就将socket引用追加到eventpoll对象的就绪列表的尾部 。而eventpoll对象中的等待队列存放的就是调用了epoll_wait()的进程(线程),网卡的中断程序执行会将等待队列中的进程(线程)重新加入工作队列,让其拥有占用CPU执行的资格。epoll_wait()的返回值是int类型,返回的是就绪的socket的数量/发生错误,-1表示发生错误。

epoll的参数有传入一个epoll_event的数组指针(作为输出参数),在调用epoll_wait()返回的同时,Kernel内核还会将就绪的socket列表添加到epoll_event类型的数组当中。

epoll是linux IO多路复用的管理机制,现在是linux平台高性能网络io必要的组件。

理解内核epoll的运行原理,需要从四方面来理解:

1.epoll的数据结构。2.epoll的线程安全。

3.epoll的内核回调。4.epoll的LT与ET。

主要两个结构体 eventpoll 与 epitem。

eventpoll是每一个epoll所对应的,epitem是每一个IO所对应的事件。

数据结构图下图所示

list用来存储准备就绪的IO,内核IO准备就绪的时候,会执行epoll_event_callback的回调函数,将epitem添加到list中;当epoll_wait激活重新运行的时候,将list的epitem逐一copy到events参数中。

rbtree用来存储所有的io数据,方便快速通过io_fd查找;epoll_ctl执行EPOLL_CTL_ADD *** 作时,将epitem添加到rbtree中;epoll_ctl执行EPOLL_CTL_DEL *** 作时,将epitem从retree中删除。

     以下几个包括list *** 作,rbtree *** 作,epoll_wait的等待需要加锁。

    list使用最小粒度的spinlock锁,避免多核竞争。

    rbtree的添加使用互斥锁,

    epoll_wait采用pthread_cond_wait

1.tcp三次握手,对端反馈ack,socket进入rcvd状态,需要将监听的socket的event置为EPOLLIN,此时标识可以进入到accept读取socket数据。

2.established状态时,收到数据,将socket的event置为EPOLLIN状态。

3.established状态时 收到fin,socket进入close_Wait,需要将socket的event设置为EPOLLIN,读取断开信息

4 .   检测到socket的send状态,cwnd >0可以发送的数据,需要将socket置为EPOLLOUT。

LT(水平触发):socket接收缓冲区不为空 有数据可读,读事件一直触发;socket发送缓冲区不满,可以继续写入数据,写事件一直触发。

ET(边缘触发):socket接收缓冲区变化时触发读事件,空的接收缓冲区刚接收到数据时触发读事件;socket发送缓冲区状态发生变化时触发写事件,即满的缓冲区刚空出空间时触发读事件。

LT的处理过程:

    accept一个连接,添加到epoll中监听EPOLLIN事件。

    当EPOLLIN事件到达时,read fd中的数据并处理,

    当需要写出数据时,把数据write到fd中;如果数据较大,无法一次性写出,那么在epoll中监听EPOLLOUT事件。

    当EPOLLOUT事件到达时,继续把数据write到fd中 ;如果数据写出完毕,那么在epoll中关闭EPOLLOUT事件。

ET的处理过程:

    accept一个连接,添加到epoll中监听EPOLLIN|EPOLLOUT事件

    当EPOLLIN事件到达时,read fd中数据并处理,read需要一直读,直到返回EAGAIN为止

    当需要写出数据时,把数据write到fd中,直到数据全部写完或者write返回EAGAIN

    当EPOLLOUT事件到达时,继续把数据write到fd中,直到数据全部写完,或者write返回EAGAIN

accept要考虑两个问题:

阻塞模式accept存在的问题:TCP连接被客户端夭折,即服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就一直阻塞到accept调用上,直到其他某个客户建立一个新的连接为止。在此期间,服务器 单纯阻塞在accept调用上,就绪队列上其他描述符都得不到处理。解决办法是把监听的套接口设置成非阻塞的,客户端在在服务器端调用accept之前中止某个连接时,accept调用可以立即返回-1。

ET模式accept存在的问题:

    多个连接同时到达,,服务器TCP就行连接瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理,解决办法是,while循环 中accpet调用,处理完accept就绪队列中所有连接后再退出循环。如何知道是否处理完所有连接,accept返回-1并且error设置为errno设置为EAGAIN便是所有连接都处理完。

LT 只要event为EPOLLIN时就能不断调用回调函数

ET 如果从EPOLLOUT变化为EPOLLIN时候,就会触发。    


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/7287562.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-03
下一篇 2023-04-03

发表评论

登录后才能评论

评论列表(0条)

保存