I/O模型
一个输入操作通常包括两个阶段:
1. 等待数据准备好
2. 从内核向进程复制数据
unix下有5种I/O模型
- 阻塞IO
- 非阻塞IO
- IO复用
- 信号驱动式IO
- 异步IO
阻塞I/O
应用进程被阻塞,直到数据复制到应用进程缓冲区才返回。
非阻塞式I/O
应用进程执行系统调用时,内核返回一个错误码。应用进程可以继续执行,但需要不断的执行系统调用来获知IO是否完成,这种方式称为轮询。
I/O复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。它可以让单个进程具有处理多个 I/O 事件的能力。
信号驱动I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
异步I/O
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
同步I/O与异步I/O
- 同步 I/O:应用进程在调用 recvfrom 操作时会阻塞。
- 异步 I/O:不会阻塞
I/O复用
select
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。fd_set使用数组实现,数组大小使用FD_SETSIZE定义。
timeout为超时函数,调用select会一直阻塞直到有描述符的时间到达或者等待堵塞时间超过timeout。
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。
fd_set fd_in, fd_out;
struct timeval tv;
FD_ZERO(&fd_in);
FD_ZERO(&fd_out);
FD_SET(sock1,&fd1);
FD_SET(sock,&fd2);
int largest_sock = sock1 > sock2 ? sock1:sock2;
tv.tv_sec = 10;
tv.tv_usec = 0;
int ret = select(largest_sock + 1,&fd_in,&fd_out,NULL,&tv);
if(ret == -1)
// report error and abort
else if(ret == 0)
// timeout; no event detected
else
{
if(FD_ISSET(sock1,&fd_in);
// input event on sock1
if(FD_ISSET(sock2,&fd_out))
// output event on sock2
}
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
pollfd使用链表实现。
strcut pollfd fds[2];
fd[0].fd = sock1;
fd[0].events = POLLIN;
fd[1].fd = sock2;
fd[1].events = POLLOUT;
int ret = poll(&fds, 2, 10000);
if(ret == -1)
// Check if poll actually succeed
else if(ret == 0)
// timeout; no event detected
else
{
if(pfd[0].revents & POLLIN)
{
pfd[0].revents = 0;
// input event on sock1
}
if(pfd[1].revents & POLLOUT)
{
pfd[1].revents = 0;
// output events on sock2
}
}
select 和 poll 的比较:
1. select会修改文件描述符,而poll不会
2. select的文件描述符使用数组实现,FD_SETSIZE 大小默认为 1024, 。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符的数量的限制;
3. poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
4. 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定
5. select和poll,每次调用都需要将全部描述符从应用缓冲区复制到内核缓冲区
6. select和poll的返回结果中没有声明哪些文件描述符已经准备好,如果返回值大于0时,应用进程都需使用轮询方式来找到IO完成的描述符。
7. 几乎所有系统都支持select,但只有比较新的系统支持poll。
epoll
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_ctl()向内核注册新的文件描述符或改变某个描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上。通过回调函数内核将IO准备好的描述符加入到一个链表中管理,进程调用epoll_wait()便可以得到事件完成的描述符。
epoll只需要将文件描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。
epoll仅使用于Linux,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。
int epfd = epoll_create( 0xCAFE ); // size
if ( pollingfd < 0 )
// report error
struct epoll_event ev = { 0 };
ev.data.ptr = pConnection1;
ev.events = EPOLLIN || EPOLLONESHOT;
if(epoll_ctl(epfd,EPOLL_CTL_ADD, pConnection1->getSock(), &ev) != 0)
// report error
struct epoll_event pevents[ 20 ];
int ready = epoll_wait(epfd, pevents, 20, 10000 );
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
// Check if any events detected
for ( int i = 0; i < ret; i++ )
{
if ( pevents[i].events & EPOLLIN )
{
// Get back our connection pointer
Connection * c = (Connection*) pevents[i].data.ptr;
c->handleReadEvent();
}
}
epoll的描述符有两种工作模式:LT 和 ET
1. LT,当epoll_wait()检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用epoll_wait()会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
2. ET,和LT模式不同的是,通知之后进程必须立即处理事件,下次再调用epoll_wait()时不会再得到事件的通知。很大程度减少了epoll事件被重复触发的次数,效率要比LT高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
应用场景
- select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求更高的场景,比如核反应堆的控制。select 可移植性更好,几乎被所有主流平台所支持。
- poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且epoll 的描述符存储在内核,不容易调试。
- epoll只需要运行在 Linux 平台上,并且有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。