在上一篇博文中提到了五种IO模型,关于这五种IO模型能够参考博文IO模型浅析-阻塞、非阻塞、IO复用、信号驱动、异步IO、同步IO,本篇主要介绍IO多路复用的使用和编程。html
多路复用是一种机制,能够用来监听多种描述符,若是其中任意一个描述符处于就绪的状态,就会返回消息给对应的进程通知其采起下一步的操做。linux
当进程须要等待多个描述符的时候,一般状况下进程会开启多个线程,每一个线程等待一个描述符就绪,可是多路复用能够同时监听多个描述符,进程中无需开启线程,减小系统开销,在这种状况下多路复用的性能要比使用多线程的性能要好不少。编程
在linux中,关于多路复用的使用,有三种不一样的API,select、poll和epollsegmentfault
select的使用须要引入sys/select.h头文件,API函数比较简单,函数原型以下:api
int select (int __nfds, fd_set *__restrict __readfds, fd_set *__restrict __writefds, fd_set *__restrict __exceptfds, struct timeval *__restrict __timeout);
其中有一个很重要的结构体fd_set,该结构体能够看做是一个描述符的集合,能够将fa_set看做是一个位图,相似于操做系统中的位图,其中每一个整数的每一bit表明一个描述符,。数组
举个简单的例子,fd_set中元素的个数为2,初始化都为0,则fd_set中含有两个整数0,假设一个整数的长度8位(为了好举例子),则展开fd_set的结构就是 00000000 0000000,若是这个时候添加一个描述符为3,则对应fd_set编程 00000000 00001000,能够看到在这种状况下,第一个整数标记描述符0~7,第二个整数标记8~15,依次类推。
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是fd_set中最大的描述符+1,当调用select的时候,内核态会判断fd_set中描述符是否就绪,__nfds告诉内核最多判断到哪个描述符。并发
struct timeval { long tv_sec; //秒 long tv_usec; //微秒 }
参数__timeout指定select的工做方式:异步
select函数返回产生事件的描述符的数量,若是为-1表示产生错误
值得注意的是,好比用户态要监听描述符1和3的读事件,则将readset对应bit置为1,当调用select函数以后,若只有1描述符就绪,则readset对应bit为1,可是描述符3对应的位置为0,这就须要注意,每次调用select的时候,都须要从新初始化并赋值readset结构体,将须要监听的描述符对应的bit置为1,而不能直接使用readset,由于这个时候readset已经被内核改变了。
select中,每一个fd_set结构体最多只能标识1024个描述符,在poll中去掉了这种限制,使用poll须要引入头文件sys/poll.h,poll调用的API以下:
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
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的做用同select中的__nfds,表示pollfd数组中最大的下标索引
poll函数返回产生事件的描述符的数量,若是返回0表示超时,若是为-1表示产生错误
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);
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指向一个共用体结构,能够用该共用体保存自定义的参数,或者指向被监控的文件描述符。
int epoll_create (int __size);
epoll_create函数建立一个epoll实例并返回,该实例能够用于监控__size个文件描述符
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。
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的机制基本相同,只不过poll没有select最大文件描述符的限制,在具体使用的时候,有以下缺点:
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、poll、epoll实现一个TCP反射程序
UNIX网络变成卷1:套接字联网API
做者: yearsj转载请注明出处:https://segmentfault.com/a/11...