在上一篇博文中提到了五种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

最新文章

  1. 对tomcat中使用反射加载类的理解
  2. elasticsearch 集群搭建
  3. javascript 中break、 continue、函数不能重载
  4. VHD轻松实现双系统
  5. js变量作用域
  6. SQL 范式(转载)
  7. java可访问修饰符
  8. [HAOI2006]旅行
  9. salt基本使用之一(1)
  10. ThreadPoolExecutor运行机制
  11. Android缓存机制——LruCache
  12. 从零开始学习PYTHON3讲义(十三)记事本的升级版:网络记事本
  13. 第十三届东北师范大学程序设计竞赛热身赛 C(exgcd+欧拉函数)
  14. 9、js扩展
  15. Python学习之旅(十九)
  16. VMware 虚拟机磁盘
  17. ZOJ Monthly, March 2018 题解
  18. 动态SQL中 实现条件参数 varchar类型的参数名称 以及模糊查询实现
  19. Highcharts做柱状图怎样样每个柱子都是不同的颜色显示
  20. jzoj100031

热门文章

  1. Visual Studio 2017 安装过程问题解决
  2. 怎样卸载wineQQ?
  3. 开源Webshell利用工具——Altman
  4. AutoMapper 帮助类
  5. MVC 当中 [ValidateAntiForgeryToken] 的作用
  6. Win 10安装11g 客户端条件检查失败,INS-30131 无法从节点 "desktop-njm3a7m" 检索 exectask 的版本
  7. Oracle条件查询
  8. 自己动手写一个简易对象关系映射,ORM(单例版和数据库池版)
  9. linux静态IP最简配置
  10. 解决docker pull出现 error pulling image configuration: Get https://dseasb33srnrn.cloudfront.net······: net/http: TLS handshake timeout的问题