问题聚焦:
    前篇提到了I/O处理单元的四种I/O模型。
    本篇详细介绍实现这些I/O模型所用到的相关技术。
    核心思想:I/O复用

使用情景:
  • 客户端程序要同时处理多个socket
  • 客户端程序要同时处理用户输入和网络连接
  • TCP服务器要同时处理监听socket和连接socket,这是使用最多的场合
  • 服务器要同时可处理TCP请求和UDP请求
  • 服务器要同时监听多个端口
主要技术:
  • select
  • poll
  • epoll

select系统调用
作用:
    在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。
    
select API
原型:
#include <sys/select.h>
int select ( int nfds, fd_set* readfds, fde_set* writefds, fd_set* exceptfds, struct timeval* timeout );
函数说明:
nfds:指定被监听的文件描述符的总数,通常为所有文件描述符中的最大值+1
readfds, writefds, exceptfds:可读、可写和异常等事件对应的文件描述符集合。
fd_set结构:仅包含一个整型数组,该数组的每个元素的每一位标记了一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
fd_set相关的位操作:
#include <sys/select.h>
FD_ZERO( fd_set *fdset );
FD_SET( int fd, fd_set *fdset );
FD_CLR( int fd, fd_set *fdset );
int FD_ISSET( int fd, fd_set *fdset );
timeout:设置select函数的超时时间
返回状态:
select成功时返回就绪(可读、可写和异常)文件描述符的总数。
如果在超时时间内没有任何文件描述符就绪,select将返回0.
select失败时返回-1并设置errno
如果select 等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
文件描述符就绪条件
可读:
  • socket内核接收缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地对该socket,并且读操作返回的字节数大于0
  • socket通信的对方关闭连接,此时读操作返回0
  • 监听socket上有新的连接请求
  • socekt上有未处理的错误,此时我们可以使用getsockopt来读取和清除该错误。

可写:

  • socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0
  • socket的写操作被关闭。对写操作被关闭的socket执行写操作将出发一个SIGPIPE信号
  • socket使用非阻塞connect连接成功或者失败之后
  • socket上有未处理的错误,此时我们可以使用getsockopt来读取和清除该错误。

异常:

  • socket上接收到带外数据

处理带外数据

socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态:前者处于可读状态,后者处于异常状态。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h> int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
printf( "ip is %s and port is %d\n", ip, port ); int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port ); int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 ); ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 ); ret = listen( listenfd, 5 );
assert( ret != -1 ); struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
close( listenfd );
} char remote_addr[INET_ADDRSTRLEN];
printf( "connected with ip: %s and port: %d\n",
inet_ntop( AF_INET, &client_address.sin_addr, remote_addr, INET_ADDRSTRLEN ),
ntohs( client_address.sin_port ) ); char buf[1024];
fd_set read_fds;
fd_set exception_fds; FD_ZERO( &read_fds );
FD_ZERO( &exception_fds ); int nReuseAddr = 1;
setsockopt( connfd, SOL_SOCKET, SO_OOBINLINE, &nReuseAddr, sizeof( nReuseAddr ) );
while( 1 )
{
memset( buf, '\0', sizeof( buf ) );
/* 每次调用select前都要重新在readfds和exception_fds中设置文件描述符connfd,因为事件发生之后,文件描述符集合将被内核修改 */
FD_SET( connfd, &read_fds );
FD_SET( connfd, &exception_fds ); ret = select( connfd + 1, &read_fds, NULL, &exception_fds, NULL );
printf( "select one\n" );
if ( ret < 0 )
{
printf( "selection failure\n" );
break;
} /* 对于可读事件,采用普通的recv函数读取数据 */
if ( FD_ISSET( connfd, &read_fds ) )
{
ret = recv( connfd, buf, sizeof( buf )-1, 0 );
if( ret <= 0 )
{
break;
}
printf( "get %d bytes of normal data: %s\n", ret, buf );
}
/* 对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据 */
else if( FD_ISSET( connfd, &exception_fds ) )
{
ret = recv( connfd, buf, sizeof( buf )-1, MSG_OOB );
if( ret <= 0 )
{
break;
}
printf( "get %d bytes of oob data: %s\n", ret, buf );
} } close( connfd );
close( listenfd );
return 0;
}

poll系统调用
作用:和select类型,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
原型:
#include <poll.h>
int poll ( struct pollfd* fds, nfds_t nfds, int timeout );
函数说明:
fds:指定我们所感兴趣的文件描述符上发生的可读,可写和异常事件。
pllfd结构:
struct pollfd
{
int fd; /* 文件描述符 */
short events; /* 注册的事件 */
short revents; /* 实际发生的事件,由内核填充 */
}
支持的事件类型:(自行百度)
返回值:通常,应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。poll系统调用的返回值的含义与select相同。
nfds:指定被监听事件集合fds的大小。
类型nfds_t定义:
typedef unsigned long int nfds_t;
timeout:指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生。当timeout为0时,poll调用将立即返回。

epoll系列系统调用
特点:
    一组函数完成任务
    epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。
    需要一个额外的文件描述来唯一标识内核中的这个事件表。
文件描述符的创建:
#include <sys/epoll.h>
int epoll_create ( int size );
该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
操作内核事件表:
#include <sys/epoll.h>
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
函数说明:
fd:要操作的文件描述符
op:指定操作类型
操作类型:
    EPOLL_CTL_ADD:往事件表中注册fd上的事件
    EPOLL_CTL_MOD:修改fd上的注册事件
    EPOLL_CTL_DEL:删除fd上的注册事件
event:指定事件,它是epoll_event结构指针类型
epoll_event定义:
struct epoll_event
{
__unit32_t events; /* epoll事件 */
epoll_data_t data; /* 用户数据 */
};
结构体说明:
events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)
data成员:存储用户数据

typedef union epoll_data
{
void* ptr; /* 指定与fd相关的用户数据 */
int fd; /* 指定事件所从属的目标文件描述符 */
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_wait函数
主要接口
作用:在一段时间内,等待一组文件描述符上的事件
原型:
#include <sys/epoll.h>
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
函数说明:
返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
timeout:与poll相同
maxevents:指定最多监听多少个事件
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。与poll的区别(见下面的demo)
poll和epoll在使用上的差别:
/* 索引poll返回的就绪文件描述符 */
/* 方式:遍历,检查标志位 */
int ret = poll ( fds, MAX_EVENT_NUMBER, -1 );
for ( int i = 0; i < MAX_EVENT_NUMBER; ++i )
{
if ( fds[i].revents & POLLIN )
{
int sockfd = fds[i].fd;
/* 处理sockfd */
}
} /* 索引epoll返回的就绪文件描述符 */
/* 方式:遍历 */
int ret = epoll_wait ( epollfd, events, MAX_EVENT_NUMBER, -1 );
for ( int i = 0; i < ret; i++ )
{
int sockfd = events[i].data.fd;
/* sockfd必然就绪,直接处理 */
}
LT和ET模式
对文件操作符的操作模式:
  • LT:电平触发,默认的工作模式,检测到有事件发生可以不立即处理。
  • ET:边沿触发,搞笑工作模式,文件描述符注册为EPOLLET事件,检测到有事件发生立即处理,后续epoll_wait不再向应用程序通知这一事件。
区别:ET模式在很大程度上降低了同一个epoll事件被重复出发的参数,因此效率要比LT模式高。
EPOLLONESHOT事件
使用场合:
    一个线程在读取完某个socket上的数据后开始处理这些数据,而数据的处理过程中该socket又有新数据可读,此时另外一个线程被唤醒来读取这些新的数据。
    于是,就出现了两个线程同时操作一个socket的局面。
    可以使用epoll的EPOLLONESHOT事件实现一个socket连接在任一时刻都被一个线程处理。
作用:
    对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多出发其上注册的一个可读,可写或异常事件,且只能触发一次。
使用:
    注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个sockt。
效果:
尽管一个socket在不同事件可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

小结:三组I/O复用函数的比较
系统调用
select
poll
epoll
事件集合
用哦过户通过3个参数分别传入感兴趣的可读,可写及异常等事件
内核通过对这些参数的在线修改来反馈其中的就绪事件
这使得用户每次调用select都要重置这3个参数
统一处理所有事件类型,因此只需要一个事件集参数。
用户通过pollfd.events传入感兴趣的事件,内核通过
修改pollfd.revents反馈其中就绪的事件
内核通过一个事件表直接管理用户感兴趣的所有事件。
因此每次调用epoll_wait时,无需反复传入用户感兴趣
的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件
应用程序索引就绪文件
描述符的时间复杂度
O(n)
O(n)
O(1)
最大支持文件描述符数
一般有最大值限制
65535
65535
工作模式
LT
LT
支持ET高效模式
内核实现和工作效率
采用轮许方式检测就绪事件
时间复杂度:O(n)
采用轮许方式检测就绪事件
时间复杂度:O(n)
采用回调方式检测就绪事件
事件复杂度:O(1)
需要说明的是:
epoll的效率未必一定比select和poll高。
当活动连接比较多的时候,回调函数被触发得过于频繁,而降低效率。
所以,epoll_wait适用于连接数量多,但活动连接较少的情况。

参考资料:
《Linux高性能服务器编程》

服务器编程入门(7)I/O复用的更多相关文章

  1. 服务器编程入门(11)TCP并发回射服务器实现 - 单线程select实现

    问题聚焦: 当客户端阻塞于从标准输入接收数据时,将读取不到别的途径发过来的必要信息,如TCP发过来的FIN标志. 因此,进程需要内核一旦发现进程指定的一个或多个IO条件就绪(即输入已准备好被读取,或者 ...

  2. 服务器编程入门(10)TCP回射服务器实现 - 并发

    问题聚焦:     在前面我们大概浏览了一下服务器编程需要掌握的一些知识和技术,以及架构思想.        实践,才是检验真理的唯一标准..从这节起我们将在这些技术的基础上,一步步实现以及完善一个服 ...

  3. 服务器编程入门(5)Linux服务器程序规范

    问题聚焦:     除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范.     工欲善其事,必先利其器,这篇主要来探 ...

  4. 服务器编程入门(4)Linux网络编程基础API

      问题聚焦:     这节介绍的不仅是网络编程的几个API     更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系.     这节主要介绍三个方面的内容:套接字( ...

  5. 服务器编程入门(3)TCP协议详解

    问题聚焦:     本节从如下四个方面讨论TCP协议:     TCP头部信息:指定通信的源端端口号.目的端端口号.管理TCP连接,控制两个方向的数据流     TCP状态转移过程:TCP连接的任意一 ...

  6. 服务器编程入门(2)IP协议详解

    问题聚焦:     IP协议是TCP/IP协议族的核心协议,也是socket网络编程的基础之一.这里从两个方面较为深入地探讨IP协议:     1,IP头部信息(指定IP通信的源端IP地址,目的端IP ...

  7. 服务器编程入门(1)TCP/IP协议族

    问题聚焦: 简单地梳理一下TCP/IP各层的功能和常用协议 详细了解ARP(数据链路层)和DNS(应用层)协议的工作原理 1 TCP/IP协议族体系结构 数据链路层:     职责:实现网卡接口的网络 ...

  8. 服务器编程入门(13) Linux套接字设置超时的三种方法

    摘要:     本文介绍在套接字的I/O操作上设置超时的三种方法. 图片可能有点宽,看不到的童鞋可以点击图片查看完整图片.. 1 调用alarm 使用SIGALRM为connect设置超时 设置方法: ...

  9. COM编程入门第二部分——深入COM服务器

    本文为刚刚接触COM的程序员提供编程指南,解释COM服务器内幕以及如何用C++编写自己的接口.继上一篇COM编程入门之后,本文将讨论有关 COM服务器的内容,解释编写自己的COM接口和COM服务器所需 ...

随机推荐

  1. Delphi中运行时改变panel的位置及大小(通过wm_SysCommand来实现)

    procedure TForm1.pnl1MouseDown(Sender: TObject; Button: TMouseButton;  Shift: TShiftState; X, Y: Int ...

  2. C++经典书目索引及资源下载

    C++经典书目索引: 严重申明 : 本博文未经原作者(jerryjiang)同意,不论什么人不得转载和抄袭 ! Essential C++ 中文版 层次:0基础 导读:<Essential C+ ...

  3. (Relax 数论1.6)POJ 1061 青蛙的约会(扩展的欧几里得公式)

    /* * POJ_1061.cpp * * Created on: 2013年11月19日 * Author: Administrator */ #include <iostream> # ...

  4. Ubuntu 环境安装整理

    Ubuntu11.04下Java开发环境搭建和配置 转自:http://guoyunsky.iteye.com/blog/1175861 类似的搭建,网上一搜一大把,但每次去搜索比较麻烦.我这里就整理 ...

  5. js监听滚动条 回到顶端

    效果:当出现滚动条,且滚动条出现移动时,把回到顶端按钮 显示出来:当滚动条回到顶部时,将回到顶端按钮隐藏. <script type="text/javascript"> ...

  6. 自绘XP风格菜单

    这是以前写的代码,自绘XP风格的菜单,硬盘坏了后以为没了,最后写的一个软件要自定义风格,“翻箱倒柜”的终于在我可爱的古董机^_^上找到了一个应用的例子.还是把它放到Blog上来,即可共享又可作为备用 ...

  7. javascript (十四) dom

    通过 HTML DOM,可访问 JavaScript HTML 文档的所有元素. HTML DOM (文档对象模型) 当网页被加载时,浏览器会创建页面的文档对象模型(Document Object M ...

  8. MFC 总体理解

    在MFC程序中,我们并不经常直接调用Windows API,而是从MFC类创建对象并调用属于这些对象的成员函数.也就是说MFC封装了Windows API 你说你喜欢C++而MFC换一种说法就是一个用 ...

  9. vc 按钮自绘

    按钮自绘,将按钮区域分成三部分,左边.右边.中间都由贴图绘制,可用于手动进度条按钮,或者左右选择项按钮 cpp代码部分: // LRSkinButton.cpp : implementation fi ...

  10. 14.5.7 Storing InnoDB Undo Logs in Separate Tablespaces 存储InnoDB Undo logs 到单独的表空间

    14.5.7 Storing InnoDB Undo Logs in Separate Tablespaces 存储InnoDB Undo logs 到单独的表空间 在MySQL 5.6.3,你可以存 ...