DLX Dev Everything Engineer

搞清楚I/O多路复用

2019-10-20

Linux上有哪些不同的I/O模型,如图所示:

基本 Linux I/O 模型的简单矩阵

总共有4种组合:

同步阻塞,同步非阻塞,异步阻塞,异步非阻塞(AIO)

什么是I/O多路复用?

I/O多路复用允许应用在多个文件描述符上同时阻塞,并在其中某个可以读写时收到通知。

Linux提供了三种I/O多路复用方案:select poll epoll。

显然,I/O多路复用的select和poll属于异步阻塞的IO模型。接下来介绍下select和poll。

select

#define TIMEOUT 5       /* select timeout in seconds */
#define BUF_LEN 1024    /* read buffer in bytes */
int main( void )
{
 struct timeval tv;
 fd_set readfds;
 int ret;

/* Wait on stdin for input. */
 FD_ZERO( &readfds );
 FD_SET( STDIN_FILENO, &readfds );
/* Wait up to five seconds. */
 tv.tv_sec = TIMEOUT;
 tv.tv_usec = 0;

/* All right, now block! */
 ret = select( STDIN_FILENO + 1, &readfds, NULL, NULL, &tv );

 if ( ret == -1 )
 {
 perror( "select "); return(1);
 }else if ( !ret )
 {
 printf( "%d seconds elapsed.\n", TIMEOUT ); return(0);
 }
/* 
* Is our file descriptor ready to read? 
* (It must be, as it was the only fd that 
* we provided and the call returned 
* nonzero, but we will humor ourselves.) 
*/
 if ( FD_ISSET( STDIN_FILENO, &readfds ) )
 {
 char buf[BUF_LEN + 1];
 int len;
/* guaranteed to not block */
 len = read( STDIN_FILENO, buf, BUF_LEN );
 if ( len == -1 )
 {
 perror( "read "); return(1);
 }
 if ( len )
 {
 buf[len] = '\0'; printf( "read: %s \n ", buf );
 }
 return(0);
 }
 fprintf( stderr, "This should not happen !\n ");
 return(1);
}

上面是select的demo,具体细节可查阅Linux select的API;

poll

#define TIMEOUT 5 /* poll timeout, in seconds */

int main( void )
{
 struct pollfd fds[2]; int ret;
/* watch stdin for input */ 
 fds[0].fd = STDIN_FILENO; 
 fds[0].events = POLLIN; 
/* watch stdout for ability to write (almost always true) */ 
 fds[1].fd = STDOUT_FILENO; 
 fds[1].events = POLLOUT;
/* All set, block! */ 
 ret = poll( fds, 2, TIMEOUT * 1000 );
 
 if ( ret == -1 )
 {
 perror( "poll" ); 
 return(1);
 }
 if ( !ret )
 {
 printf( "%d seconds elapsed.\n", TIMEOUT ); 
 return(0);
 }
 if ( fds[0].revents & POLLIN )
 printf( " stdin is readable \n");
 if ( fds[1].revents & POLLOUT )
 printf( " stdout is writable \n" );
 return(0);
}

上面是poll的demo,具体细节可查阅Linux poll的API。

epoll

对于poll和select,内核必须遍历所有被监视的文件描述符。当这个表变得很大时,每次调用的遍历就成为了明显的瓶颈。另一方面poll和selectIO复用受系统进程打开文件数量限制。而epoll改进了上述问题。

#define MAX_EVENTS 64
struct epoll_event *events;
int nr_events, i, epfd;
events = malloc( sizeof(struct epoll_event) * MAX_EVENTS );
if ( !events )
{
 perror(  malloc  ); return(1);
}
nr_events = epoll_wait( epfd, events, MAX_EVENTS, -1 ); 
if ( nr_events < 0 )
{
 perror(  epoll_wait  ); free( events ); return(1);
}
for ( i = 0; i < nr_events; i++ )
{
 printf(  event = % ld on fd = % d \ n , events[i].events, events[i].data.fd );                                                                                                                                                                                            /* * We now can, per events[i].events, operate on
                                                                                                                                                                                                                                                                              * events[i].data.fd without blocking. */
}
free( events );

我epoll性能高效的奥秘:

当我们执行epoll_ctl添加一个文件时,会把要监控的文件句柄保存在红黑树中(便于查找),还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪链表里。 执行epoll_wait反馈就绪事件时,仅仅返回就绪的文件句柄。

参考资料:

《linux系统编程》

使用异步IO大大提高应用程序的性能


Comments

Content