乱七八糟风暴收藏

I/O多路复用模型select,poll,epoll原理分析及对

2018-02-11  本文已影响12人  刘建会

一、什么是io多路复用
在bio模型中,一个io请求对应一个线程,造成线程极大浪费,且没有数据发送的情况下,线程也一直阻塞等待,资源利用率不高。
在nio模型中,多个io请求归一个线程管理,当io就绪的时候,线程通知应用程序进行数据读写,这样不用创建很多线程,线程也不用一直阻塞等待io就绪,这样的技术称为io多路复用。
io多路复用通过一种机制,可以监视多个文件描述符,一旦某个文件描述就绪(),能够通知应用程序进行相应的io读写操作。
二、为什么用io多路复用
io多路复用解决了多线程io阻塞问题,避免创建很多线程,及降低线程上下文切换的开销,提升资源利用率。
三、io多路复用模型原理分析
监视多个文件描述符的机制不同,io多路复用技术主要有select,poll,epoll,本质上都是阻塞io,因为他们都需要在io就绪后,自己负责进行数据在用户空间和内核空间之间copy。非阻塞io无需自己负责数据copy,操作系统会把数据从内核空间copy到用户空间。
1.select:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
监视多个文件描述符的属性变化(可读、可写、错误异常)。select监视的文件描述符有3类:writefds,readfds,execeptfds,调用select 线程会阻塞,知道有描述符就绪,才返回,之后通过遍历fdset来找到就绪的描述符。
nfds: 要监视的文件描述符的范围,在 Linux 上最大值一般为1024。
readfd: 监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这。
writefds: 监视的可写描述符集合,只要有文件描述符即将进行写操作,这个文件描述符就存储到这里。
exceptfds: 监视的错误异常描述符集合。
timeout: 超时时间,它告知内核等待任何一个文件描述符就绪可花多少时间。有3中设置:
1)永远等待,仅有一个描述符就绪后返回;
2)不等待,检查所有描述符看其是否就绪,是一种轮训方式,返回就绪的个数;
3)等待固定时间,返回就绪的个数。
优点:所有平台都支持,不存在兼容问题。
缺点:1)每次调用select,都要把fe集合从用户态copy到内核态,fd很多时,开销很大。同时在内核态要遍历fd,看其是否就绪,开销也很大。
2.poll:int poll(struct pollfd fds, nfds_t nfds, int timeout);
poll的本质和select一样,没有太大差别,管理多个描述符也是进行轮询,根据描述符状态进行处理,但poll没有最大描述符个数限制(select限制1024个),但是随着数量的增加,性能也有下降。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
fds:不同于 select() ,使用三个位置来表示三个 fd集合,poll() 使用一个 pollfd 的指针,指向一个 pollfd 结构体数组,结构体中包括了要监视的文件描述符和事件,感兴趣的事件由结构中 events 来确定,调用后实际发生的时间将被填写在结构体的 revents 域中,如下:
struct pollfd{
int fd; /
文件描述符 /
short events; /
感兴趣的事件 /
short revents; /
实际发生了的事件 */
};
nfds: 指定第一个参数数组元素个数,并不是限制监视的文件描述符个数,poll不限制文件描述符个数。
timeout: 指定等待的毫秒数,无论 I/O 是否就绪,poll() 都会返回。当等待时间为 0 时,poll() 函数立即返回,为 -1 则使 poll() 一直阻塞直到一个感兴趣的事件发生后返回。
poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
返回-1说明有错误发生,具体错误有以下集中:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
poll和select非常类似,只是fd集合的方式不同,poll使用结构体数组,select用set,其他都差不多。
3.epoll:
int epoll_create(int size);创建一个epoll文件描述符,管理其他多个文件描述符。
size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好 epoll 描述符后,它就是会占用一个 fd 值,在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);事件注册,它不同于 select() 是在遍历描述符时告诉内核要关注什么事件,而是在这里先注册要关注的事件。
epfd: epoll 专用的文件描述符,epoll_create()的返回值
op: 表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
fd: 需要监视的文件描述符
event: 告诉内核要监听什么事件,主要有以下事件:
EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读;
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET :将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里。
int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );注册在epoll中已经发生的事件,类似于 select() 调用,轮训事件表,看是否有事件发生。
epfd: epoll 专用的文件描述符,epoll_create()的返回值
events:epoll 将会把发生的事件添加到events 数组中,events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存。
maxevents: maxevents 告之内核这个 events 有多大。
timeout: 超时时间,为 -1 时,函数为阻塞,知道有事件发生。
返回就绪的文件描述符个数。
epoll 对文件描述符的操作有两种模式:LT(level trigger:水平触发模式,default)和 ET(edge trigger:边沿触发模式)。
LT模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序后,应用程序可以不处理该事件,下次调用 epoll_wait 时,会再次向应用程序通知此事件,直到事件被处理,可能会重复处理事件。
ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,如果不处理,下次调用 epoll_wait 时,不会再次向应用程序通知此事件,可能会丢失事件处理。
在 select/poll中,内核对所有监视的文件描述符进行扫描,返回就绪的个数;而 epoll() 事先通过 epoll_ctl() 来注册一个文件描述符及感兴趣的事件,一旦某个文件描述符上感兴趣的事件发生后,内核会采用类似 callback 的回调机制,将发生的事件填入epoll_wait()的结构中,当进程调用 epoll_wait() 时便得到就绪的个数。
优点:
1.监视的描述符数量不受限制。
2.效率不会随着监视 fd 的数量的增长而下降。select(),poll() 需要自己不断轮询所有 fd 集合来发现就绪设备,当fd集合很大时,开销很大。而epoll是通过事件回调机制发现就绪的设备,当设备就绪后,回进行回调,不需要轮训。
3.select(),poll() 每次调用都要把 fd 集合从用户态往内核态拷贝一次,而epoll通过内核与用户空间共享一块内存,避免了无畏的内存拷贝(mmap机制)。

上一篇下一篇

猜你喜欢

热点阅读