当前位置: 首页 > Linux

IO多路复用(一)——Select、Poll、Epoll

时间:2023-04-06 06:42:06 Linux

在上一篇博文中提到了五种IO模型。这五种IO模型可以参考博文IO模型解析——阻塞、非阻塞、IO多路复用、信号驱动、异步IO、同步IO,本文主要介绍IO多路复用的使用和编程。IO多路复用的概念多路复用是一种可以用来监视多个描述符的机制。如果任何一个描述符处于就绪状态,就会向相应的进程返回一条消息,通知它进行下一步。操作。IO多路复用的优点当一个进程需要等待多个描述符时,通常进程会开启多个线程,每个线程等待一个描述符就绪,但是多路复用可以同时监听多个描述符。进程中无需开启线程,减少系统开销。在这种情况下,多路复用的性能要比使用多线程好很多。相关API介绍在linux中,select、poll和epollSelect的使用有3种不同的API介绍select的使用需要导入sys/select.h头文件,API函数比较简单,函数原型如下:intselect(int__nfds,fd_set*__restrict__readfds,fd_set*__restrict__writefds,fd_set*__restrict__exceptfds,structtimeval*__restrict__timeout);fd_set有一个很重要的结构体fd_set,可以看成是一个描述符的集合,fa_set可以看成是一个位图,类似于操作系统中的位图,其中每个整数的每一位代表一个描述符。举个简单的例子,fd_set中的元素个数为2,初始化为0,那么fd_set中包含两个整数0,假设一个整数的长度为8位(为了举例),展开后的结构为fd_set为000000000000000,如果此时加入一个描述符3,对应的fd_set编程为0000000000001000,可以看到本例中,第一个整数标记描述符0~7,第二个整数标记8~15,等等。fd_set有四个关联的apivoidFD_ZERO(fd_set*fdset)//清除fdset,将所有位设置为0voidFD_SET(intfd,fd_set*fdset)//将fd对应的位设置为1voidFD_CLR(intfd,fd_set*fdset)//将fd对应的bit设置为0voidFD_ISSET(intfd,fd_set*fdset)//判断fd对应的bit是否为1,即fd是否就绪select函数中有3个fd_set集合,分别代表三种类型分别是Event,__readfds表示读描述符集合,__writefds表示读描述符集合,__exceptfds表示读描述符集合。当对应的fd_set=NULL时,表示不监听该类型的描述符。__nfds__nfds是fd_set+1中最大的描述符,调用select时,内核态会判断fd_set中的描述符是否就绪。__nfds告诉内核最多判断哪个描述符。timevalstructtimeval{longtv_sec;//秒长tv_usec;//microseconds}参数__timeout指定了select的工作方式:__timeout=NULL,表示select一直等待,直到__timeout结构中至少有一个描述符就绪,或者subtle是一个大于0的整数,表示select等待对于固定时间段的事件。如果在这短时间内没有描述符就绪,则返回__timeout=0,表示不等待,直接返回函数,返回select函数返回产生事件的描述符。如果为-1,则表示发生了错误。值得注意的是,例如用户态要监听描述符1和3的读事件,将readset对应的位设置为1。调用select函数后,如果只有1个描述符就绪,则对应readset的bit为1,但是descriptor3对应的位置为0。需要注意的是,每次调用select时,都需要重新初始化readset结构体并赋值,需要监听的descriptor对应的bit为setto是1,不能直接使用readset,因为此时readset已经被内核改掉了。poll引入select。每个fd_set结构最多只能识别1024个描述符。民意调查中删除了此限制。要使用poll,需要导入头文件sys/poll.h。poll调用的API如下:intpoll(structpollfd*__fds,nfds_t__nfds,int__timeout);pollfdstructpollfd{intfd;//pollshortint事件的文件描述符;//事件类型poll关心shortintrevents;//发生的事件类型};poll使用结构体pollfd来指定一个需要监听的描述符。结构体中,fd是需要监听的文件描述符,events是需要监听的事件类型,revents是调用poll后返回的事件类型。在调用poll的时候,一般会传入一个pollfd结构体数组,数组中元素的个数表示监听的描述符个数,所以pollfd相比select没有1024个描述符的最大限制。事件类型有很多种,定义在bits/poll.h中,主要如下:#definePOLLIN0x001//readablewithdata#definePOLLPRI0x002//readablewithurgentdata#definePOLLOUT0x004//现在写数据会不会造成阻塞#definePOLLRDNORM0x040//可以读取普通数据#definePOLLRDBAND0x080//可以读取优先级数据#definePOLLWRNORM0x100//写入普通数据不会造成阻塞#definePOLLWRBAND0x200//写入优先级数据不会造成阻塞blocking#definePOLLERR0x008//发生错误#definePOLLHUP0x010//hang#definePOLLNVAL0x020//invalidfiledescriptor当一个文件描述符想要同时监听读写事件时,可以写成events=花粉|从POLLOUT可以看出,在poll中使用了一个结构体来存储文件描述符关心的事件,而在select中统一使用了fd_set。一个fd_set可以包含所有需要监听读事件的文件描述符,或者所有需要写文件描述符的事件。相比之下,poll比select更灵活。调用poll后,不需要像select一样重新初始化文件描述符,因为poll返回的事件是写在pollfd->revents成员中的。__fds__fds与select中的__nfds作用相同,表示pollfd数组中的最大下标索引__timeout__timeout=-1:poll阻塞直到事件发生__timeout=-0:poll立即返回__timeout!=-1&&__timeout!=0:当poll被__timeout阻塞,如果过了这个时间没有事件发生,则return函数会返回poll函数返回产生事件的描述符个数。如果返回0,则表示超时。如果为-1,则表示发生错误。epoll介绍epoll,使用一个描述符来管理多个文件描述符。使用epoll需要引入头文件sys/epoll.h。epoll相关的api函数如下:intepoll_create(int__size);intepoll_ctl(int__epfd,int__op,int__fd,structepoll_event*__event);intepoll_wait(int__epfd,structepoll_event*__events,int__maxevents,int__timeout);epoll_eventtypedefunionepoll_data{void*ptr;//你可以使用指针指向自定义参数intfd;//可以使用Changemember指向epoll监听的文件描述符uint32_tu32;uint64_tu64;}epoll_data_t;structepoll_event{uint32_t事件;//epoll事件epoll_data_t数据;//用户数据}__EPOLL_PACKED;类型变量,类似于pollfd->events,表示要监听的事件。events支持的事件类型在sys/epoll.h的头文件中,基本上是和pollfd中的事件类型一起移植的。如下,这里只写了一部分:enumEPOLL_EVENTS{EPOLLIN=0x001,#defineEPOLLINEPOLLIN//numberEPOLLPRI=0x002,#defineEPOLLPRIEPOLLPRI//可以读取紧急数据EPOLLOUT=0x004,#defineEPOLLOUTEPOLLOUT//现在写数据不会阻塞EPOLLRDNORM=0x040,#defineEPOLLRDNORMEPOLLRDNORM//可以读取普通数据EPOLLRDBAND=0x080,#defineEPOLLRDBANDEPOLLRDBAND//可以读取优先数据EPOLLWRNORM=0x100,#defineEPOLLWRNORMEPOLLWRNORM//写普通数据不会阻塞EPOLLWRBAND=0x200,#defineEPOLLWRBANDEPOLLWRBAND//写优先数据不会阻塞。..EPOLLERR=0x008,#defineEPOLLERREPOLLERR//发生错误EPOLLHUP=0x010,#defineEPOLLHUPEPOLLHUP//暂停EPOLLRDHUP=0x2000,...};epoll_event中的数据指向一个联合结构,可以用来保存自定义参数,或者指向监听的文件描述符epoll_createintepoll_create(int__size);epoll_create函数创建一个epoll实例并返回,可用于监控__size文件描述符该函数用于向epoll注册事件函数,其中__epfd为epoll_create返回的epoll实例,__op代表要执行的操作,__fd为要监听的文件描述符,__event需要监听事件。__op的可用类型定义在sys/epoll.h头文件中,如下:#defineEPOLL_CTL_ADD1//添加文件描述符#defineEPOLL_CTL_DEL2//删除文件描述符#defineEPOLL_CTL_MOD3//修改文件描述符(指就是epoll_ctl中传入的__event)如果调用成功,函数返回0,否则返回-1。epoll_waitintepoll_wait(int__epfd,structepoll_event*__events,int__maxevents,int__timeout);epoll_wait类似于select中的select函数和poll中的poll函数。它等待内核返回监听描述符事件,其中__epfd是epoll_createepoll实例创建的,__events数组是epoll_wait返回的已经生成的事件集合,其中__events[i]->data->第i个元素成员的fd表示产生事件的描述符,__maxevents是你要返回的最大事件数(通常是__events的大小),__timeout和poll中的__timeout一样。该函数返回就绪事件的个数,如果为-1,则表示出错。select、poll、epoll的机制基本相同,只是poll没有select的最大文件描述符的限制。在实际使用时,它有以下缺点:每次调用select或poll时,都需要设置被监听的fd_set或将pollfd发送到内核态。如果需要监控大量的文件描述符,效率很低。在内核模式下,需要每次轮询传入的文件描述符,检查是否有对应的事件。epoll的效率就在于将这些分开。首先,epoll并不是在每次调用epoll_wait时都将描述符传递给内核,而是在调用epoll_ctl时将描述符传递给内核。调用epoll_wait时,不需要每次都接收。与select和poll一样,使用了单独的API函数。在epoll中,使用epoll_create创建一个epoll实例,然后在调用epoll_ctl时添加新的监听描述符,此时将用户态描述符发送到内核态,因为epoll_wait的调用频率必须高于频率epoll_create的,所以epoll_wait时不需要传递任何描述符到用户态;关于第二点,在内核态,使用一个描述符就绪链表,当描述符就绪时,内核态会使用回调函数,将相应的描述符添加到就绪链表,那么当epoll_wait是调用时,不需要遍历所有的描述符来检查是否有就绪事件,而是直接检查链表是否为空。总结大家可以用生活中的一个场景来总结一下三者的区别,按照作者之前的博文IO模型分析——阻塞、非阻塞、IO多路复用、信号驱动、异步IO、同步IO来吃例子:在这个例子中,服务员和餐厅代表内核,顾客“你”是用户态进程。可能大家觉得这个例子写得不好,记下来加深记忆。selectandpoll:你去餐厅请客人吃饭。你是个大方的人,点了很多菜。你告诉服务员相应种类的菜品要上多少,服务员就把菜名一一写在纸上。然后你开始问服务员食物准备好了没有。服务员看着你的菜单清单,头皮发麻,按着菜单的顺序去厨房查看菜品做好了没有。如果食物不好吃,请划掉菜单中相应的菜,终于找到所有的熟食,服务员给你端来了。但此时菜单上只能看到做好的菜品,没做好的菜品是看不清的。你觉得这个服务员很傻,不会随便点菜。谁让你人品好,让你重新来过。写一个菜单(也许你想点一些新菜或者在这个过程中删除一些菜)。然后你去问菜好不好,服务员开始按照菜单的顺序去厨房检查菜好不好。..(select和poll的主要区别在于select中的菜单是有限的,而poll中的菜单是无限的,你可以点任何菜)epoll:你去餐厅请客吃饭,你是一个大方的人person,order点了很多菜后,你告诉服务员要上多少个对应种类的菜,服务员把菜名一一输入到餐厅后台的菜单管理软件中。将相应的桌号放在好菜上,放在取菜区。这时候你来问服务员菜好了没有。服务员接着检查管理软件。如果有标记,他就从取餐区取出对应的桌号。食物在这里给你,清除标记。过了一会儿,你想点一道新菜,于是你打电话给服务员,服务员在菜单软件上加了一栏。接下来,你去询问食物准备好了吗,服务员开始检查菜单软件中是否有标记完成的信息。..另外关于epoll的效率还有很多细节,比如使用mmap将用户空间和内核空间的地址映射到同一个物理内存地址,使用红黑树来存储需要监控的事件等。具体来说详细可以参考博文select和poll,epoll区别总结,epoll高并发网络编程详解,Linux下I/O多路复用和epoll详解,彻底学会使用epoll(一)——ET模式实现分析等文章。接下来使用select、poll、epoll实现一个TCP反射程序。参考资料UNIX网络成卷1:Socket网络APIselect、poll、epoll的区别高并发网络编程之epoll总结与整理Reuse与epoll详解作者:yearsj转载请注明出处:https://segmentfault.com/a/11...