zoukankan      html  css  js  c++  java
  • IO多路复用(一)-- Select、Poll、Epoll

    在上一篇博文中提到了五种IO模型,关于这五种IO模型可以参考博文IO模型浅析-阻塞、非阻塞、IO复用、信号驱动、异步IO、同步IO,本篇主要介绍IO多路复用的使用和编程。

    IO多路复用的概念

    多路复用是一种机制,可以用来监听多种描述符,如果其中任意一个描述符处于就绪的状态,就会返回消息给对应的进程通知其采取下一步的操作。

    IO多路复用的优势

    当进程需要等待多个描述符的时候,通常情况下进程会开启多个线程,每个线程等待一个描述符就绪,但是多路复用可以同时监听多个描述符,进程中无需开启线程,减少系统开销,在这种情况下多路复用的性能要比使用多线程的性能要好很多。

    相关API介绍

    在linux中,关于多路复用的使用,有三种不同的API,select、poll和epoll

    Select介绍

    select的使用需要引入sys/select.h头文件,API函数比较简单,函数原型如下:

    int select (int __nfds, fd_set *__restrict __readfds,
    		   fd_set *__restrict __writefds,
    		   fd_set *__restrict __exceptfds,
    		   struct timeval *__restrict __timeout);
    

    fd_set

    其中有一个很重要的结构体fd_set,该结构体可以看作是一个描述符的集合,可以将fa_set看作是一个位图,类似于操作系统中的位图,其中每个整数的每一bit代表一个描述符,。

    举个简单的例子,fd_set中元素的个数为2,初始化都为0,则fd_set中含有两个整数0,假设一个整数的长度8位(为了好举例子),则展开fd_set的结构就是 00000000 0000000,如果这个时候添加一个描述符为3,则对应fd_set编程 00000000 00001000,可以看到在这种情况下,第一个整数标记描述符07,第二个整数标记815,依次类推。

    fd_set有四个关联的api

    void FD_ZERO(fd_set *fdset) //清空fdset,将所有bit置为0
    void FD_SET(int fd, fd_set *fdset) //将fd对应的bit置为1
    void FD_CLR(int fd, fd_set *fdset) //将fd对应的bit置为0
    void FD_ISSET(int fd, fd_set *fdset) //判断fd对应的bit是否为1,也就是fd是否就绪
    

    select函数中存在三个fd_set集合,分别代表三种事件,__readfds表示读描述符集合,__writefds表示读描述符集合,__exceptfds表示读描述符集合,当对应的fd_set = NULL时,表示不监听该类描述符。

    __nfds

    __nfds是fd_set中最大的描述符+1,当调用select的时候,内核态会判断fd_set中描述符是否就绪,__nfds告诉内核最多判断到哪一个描述符。

    timeval

    struct timeval {
        long tv_sec;	//秒
        long tv_usec;	//微秒
    }
    

    参数__timeout指定select的工作方式:

    • __timeout= NULL,表示select永远等待下去,直到其中至少存在一个描述符就绪
    • __timeout结构体中秒或者微妙是一个大于0的整数,表示select等待一段固定的事件,若该短时间内未有描述符就绪则返回
    • __timeout= 0,表示不等待,直接返回

    函数返回

    select函数返回产生事件的描述符的数量,如果为-1表示产生错误

    值得注意的是,比如用户态要监听描述符1和3的读事件,则将readset对应bit置为1,当调用select函数之后,若只有1描述符就绪,则readset对应bit为1,但是描述符3对应的位置为0,这就需要注意,每次调用select的时候,都需要重新初始化并赋值readset结构体,将需要监听的描述符对应的bit置为1,而不能直接使用readset,因为这个时候readset已经被内核改变了。

    Poll介绍

    select中,每个fd_set结构体最多只能标识1024个描述符,在poll中去掉了这种限制,使用poll需要引入头文件sys/poll.h,poll调用的API如下:

    int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
    

    pollfd

    struct pollfd {
        int fd;					// poll的文件描述符
        short int events;		// poll关心的事件类型
        short int revents;		// 发生的事件类型
      };
    

    Poll使用结构体pollfd来指定一个需要监听的描述符,结构体中fd为需要监听的文件描述符,events为需要监听的事件类型,而revents为经过poll调用之后返回的事件类型,在调用poll的时候,一般会传入一个pollfd的结构体数组,数组的元素个数表示监控的描述符个数,所以pollfd相对于select,没有最大1024个描述符的限制。

    事件类型有多种,在bits/poll.h中定义了多种事件类型,主要如下:

    #define POLLIN		0x001		// 有数据可读
    #define POLLPRI		0x002		// 有紧迫数据可读
    #define POLLOUT		0x004		// 现在写数据不会导致阻塞
    
    # define POLLRDNORM	0x040		// 有普通数据可读
    # define POLLRDBAND	0x080		// 有优先数据可读
    # define POLLWRNORM	0x100		// 写普通数据不会导致阻塞
    # define POLLWRBAND	0x200		// 写优先数据不会导致阻塞
    
    #define POLLERR		0x008		// 发生错误
    #define POLLHUP		0x010		// 挂起
    #define POLLNVAL	0x020		// 无效文件描述符
    

    当一个文件描述符要同时监听读写事件时,可以写成 events = POLLIN | 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对应的时候,如果超过该时间没有事件产生则返回

    函数返回

    poll函数返回产生事件的描述符的数量,如果返回0表示超时,如果为-1表示产生错误

    Epoll介绍

    epoll中,使用一个描述符来管理多个文件描述符,使用epoll需要引入头文件sys/epoll.h,epoll相关的api函数如下:

    int epoll_create (int __size);
    int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);
    int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
    

    epoll_event

    typedef union epoll_data {
      void *ptr; 	// 可以用改指针指向自定义的参数
      int fd;	 	// 可以用改成员指向epoll所监控的文件描述符
      uint32_t u32;
      uint64_t u64;
    } epoll_data_t;
    
    struct epoll_event {
      uint32_t events;		// epoll事件
      epoll_data_t data;	// 用户数据
    } __EPOLL_PACKED;
    

    epoll_event结构体中,首先是一个events的整型变量,类似于pollfd->events,表示要监控的事件,events支持的事件类型在sys/epoll.h的头文件中,跟pollfd中的事件类型基本移植,如下,这里只写出一部分:

    enum EPOLL_EVENTS {
        EPOLLIN = 0x001,
    #define EPOLLIN EPOLLIN 	// 有数据可读
        EPOLLPRI = 0x002,
    #define EPOLLPRI EPOLLPRI 	// 有紧迫数据可读
        EPOLLOUT = 0x004,
    #define EPOLLOUT EPOLLOUT 	// 现在写数据不会导致阻塞
        EPOLLRDNORM = 0x040,
    #define EPOLLRDNORM EPOLLRDNORM		// 有普通数据可读
        EPOLLRDBAND = 0x080,
    #define EPOLLRDBAND EPOLLRDBAND		// 有优先数据可读
        EPOLLWRNORM = 0x100,
    #define EPOLLWRNORM EPOLLWRNORM		// 写普通数据不会导致阻塞
        EPOLLWRBAND = 0x200,
    #define EPOLLWRBAND EPOLLWRBAND		// 写优先数据不会导致阻塞
    	...
        EPOLLERR = 0x008,
    #define EPOLLERR EPOLLERR	// 发生错误
        EPOLLHUP = 0x010,
    #define EPOLLHUP EPOLLHUP	// 挂起
        EPOLLRDHUP = 0x2000,
       	...
      };
    

    epoll_event中的data指向一个共用体结构,可以用该共用体保存自定义的参数,或者指向被监控的文件描述符。

    epoll_create

    int epoll_create (int __size);
    

    epoll_create函数创建一个epoll实例并返回,该实例可以用于监控__size个文件描述符

    epoll_ctl

    int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);
    

    该函数用来向epoll中注册事件函数,其中__epfd为epoll_create返回的epoll实例,__op表示要进行的操作,__fd为要进行监控的文件描述符,__event要监控的事件。

    __op可用的类型定义在sys/epoll.h头文件中,如下:

    #define EPOLL_CTL_ADD 1		// 添加文件描述符
    #define EPOLL_CTL_DEL 2		// 删除文件描述符
    #define EPOLL_CTL_MOD 3		//	修改文件描述符(指的是epoll_ctl中传入的__event)
    

    该函数如果调用成功返回0,否则返回-1。

    epoll_wait

    int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
    

    epoll_wait类似与select中的select函数、poll中的poll函数,等待内核返回监听描述符的事件产生,其中__epfd是epoll_create创建的epoll实例,__events数组为epoll_wait要返回的已经产生的事件集合,其中第i个元素成员的__events[i]->data->fd表示产生该事件的描述符,__maxevents为希望返回的最大的事件数量(通常为__events的大小),__timeout和poll中的__timeout相同。该函数返回已经就绪的事件的数量,如果为-1表示出错。

    select、poll、epoll比较

    select和poll的机制基本相同,只不过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中吃饭的例子:

    在这个例子中,服务员和餐厅代表内核,客户“你”就是用户态进程,可能觉得这个例子写的不好,在这里写下加深记忆。

    select和poll:你去餐厅请客吃饭,你是个豪爽的人,点了很多菜,你告诉服务员对应种类的菜有多少上多少,服务员将菜名一一写在纸上。然后你开始问服务员饭菜有好了么,服务员看着你的菜单一大串,头皮发麻,于是按着菜单的顺序去厨房查看饭菜有没有好,如果菜没有好就划掉菜单中对应的菜,终于找出了所有已经烧好的饭菜,服务员把饭菜端给了你。可是这个时候菜单上只能看到已经准备好的菜了,没准备好的菜看不清了,你觉得这个服务员做事很傻逼,没办法将就点,谁让你性格好呢,于是你重新写了一份菜单(可能这个过程中你又想点一些新的菜或者删除一些菜)。接下来你又去问饭菜好没好,服务员又开始按照菜单的顺序去厨房查看饭菜有没有好。。。(select和poll的主要区别就在于,select中的菜单是有限的,而poll中的菜单是无限的,你可以点任意种类的菜)

    epoll:你去餐厅请客吃饭,你是个豪爽的人,点了很多菜,你告诉服务员对应种类的菜有多少上多少,服务员将菜名一一录入到餐厅后台的菜单管理软件中,厨房的师傅烧好一道菜在管理软件中标记完成一下,然后在烧好的菜上挂上对应的桌号放在取菜区,这个时候你来问服务员饭菜有准备好的么,服务员于是查一下管理软件,有标记欸,于是从取菜区取出对应桌号的饭菜送给你,清空标记。过了段时间,你又想点一道新的菜,于是叫来服务员,服务员在菜单软件中添加一栏。接下来你又去问饭菜好没好,服务员又开始看菜单软件中是否有标记完成的信息。。。

    另外关于epoll的高效还有很多细节,例如使用mmap将用户空间和内核空间的地址映射到同一块物理内存地址,使用红黑树存储要监听的事件等等,具体的细节可以参考博文select、poll、epoll之间的区别总结整理高并发网络编程之epoll详解Linux下的I/O复用与epoll详解彻底学会使用epoll(一)——ET模式实现分析等几篇文章。

    接下来使用select、pol、epoll实现一个tcp反射程序

    参考资料

    UNIX网络变成卷1:套接字联网API

    select、poll、epoll之间的区别总结整理

    高并发网络编程之epoll详解

    Linux下的I/O复用与epoll详解

    作者:yearsj

    转载请注明出处:https://www.cnblogs.com/yearsj/p/9647135.html
    segmentfault对应博文:https://segmentfault.com/a/1190000016400053

  • 相关阅读:
    Linux:grep 命令
    Linux:sort
    Java中路径相关的获取方式
    Spring boot 配置 Tomcat 临时文件缓存目录
    zabbix :web 界面显示的监控项值为0或者空
    Maven:禁止编码指定类型的资源文件
    Maven:element '******' cannot have character [children]
    MySQL:数据库名或者数据表名包含-
    Linux:sed
    Lucene 6.0下使用IK分词器
  • 原文地址:https://www.cnblogs.com/yearsj/p/9647135.html
Copyright © 2011-2022 走看看