网络编程教程(七)I/O复用

        I/O复用是一种I/O通知机制,使得程序同时监听多个文件描述符,从而提高程序的性能。

一、I/O复用的使用场景

        1.客户端程序要同时处理多个socket

        2.客户端程序同时处理用户输入和网络连接

        3.TCP服务器要同时处理监控socket和连接socket

        4.服务器要同时处理TCP请求和UDP请求

        5.服务器要同时监听多个端口,或者处理多种服务

二、select系统调用

1.用途

        在一段时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。

2.select API

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
           struct timeval *timeout);
函数功能:
    监听指定的文件描述符上是否有就绪的可读、可写或异常事件发生。通过readfds、writefds和exceptfds这三个参数传入用户感兴趣的
文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。

函数参数:
    nfds     :指定被监听的文件描述符总数
    readfds  :可读文件描述符集合
    writefds :可写文件描述符集合
    exceptfds:异常文件描述符集合
    timeout  :用于设置select函数的超时时间。因为select是同步i/o,所以会阻塞

返 回 值:
    select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0.
select失败时返回-1并设置errno.如果在select等待期间,程序收到信号,则select立即返回-1,并设置errno为EINTR。


补充:
#include <typesizes.h>
#define __FD_SETSIZE 1024

#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
#typedef long int __fd_mask;

#undef __NFDBITS
#define __NFDBITS ( 8 * (int)sizeof(__fd_mask) )

typedef struct 
{
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];  
#define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[ __FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits)
#endif
}fd_set;

        从上面的结构体中可以看到,fd_set结构体仅包含一个整型数组,该数组的每一位标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的数量。

3.select特点

        (1)select能描述的文件描述符有限,一般情况是1024个

        (2)select中用户感兴趣的文件描述符和就绪的文件描述符没有分开,都放在同一个数组中,这样就需要将文件描述符的数组先从用户空间拷贝到内核空间,在内核空间中获取哪些事件就绪以后,再从内核空间拷贝回用户空间通知应用程序。

        (3)获取就绪事件时需要逐个按位遍历readfds、writefds和exceptfds这三个数组与来进行查找

4.文件描述符就绪条件

(1)socket可读

        a).sockt内核接收缓冲区的字节数大于或等于其低水位标记SO_RECVLOWAT。此时可以无阻塞地读该socket,并且读操作返回的字节数大于0.

        b).socket通信的对方关闭连接。此时该socket读操作返回0

        c).监听socket上有新的连接请求

        d).socket上有未处理的错误

(2)socket可写

    a).socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时可以无阻塞地写该socket,并且写操作返回的字节数大于0.

    b).socket的写操作被关闭。对写操作被关闭的socket执行写操作将 触发一个SIGPIPE的信号

    c).socket使用非阻塞connect连接成功或失败之后

    d).socket上有未处理的错误

(3)socket异常

     a)接收到带外数据

 

三、poll系统调用

        poll系统调用和select类似,也是在指定时间内轮训一定数量的文件描述符,以测试其是否有就绪者。

1.poll API

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数功能:
    在一定时间内轮询一定数量的文件描述符,以测试其是否有就绪者。
函数参数:
    fds    :是一个结构体,fd成员指定文件描述符,events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位与;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。
    nfds   :指定被监听事件集合fds的大小
    timeout:指定poll的超时值
返 回 值:
    成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间没有任何文件描述符就绪,poll将返回0,poll失败时返回-1并设置errno,


struct pollfd
{
    int fd;         //文件描述符
    short events;   //注册的事件
    short revents;  //实际发生的事件,由内核填充
};

2.poll支持的事件类型

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLLPRI高优先级数据可读,比如TCP带外数据
POLLOUT数据(包括普通数据和优先数据)可写

是 

POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入
POLLERR错误
POLLHUP挂起。比如管道的写端被关闭后,该端描述符上将收到POLLHUB事件
POLLNVAL·文件描述符没有打开

 3.特点

        poll的实现和select很相似,唯一比select先进的一点在于它把用户感兴趣的事件和就绪的事件分开了,这样的话轮询的时候工作量就会小很多,不用再一个位一个位的进行与操作来查找就绪事件,其他的和select一样。

四、epoll系统调用

1.内核事件表

     epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须向select和poll那样每次调用都要重复传入文件描述符机或事件集。但epoll需要一个额外的文件描述符,来唯一标识内核中的这个事件表。

#include <sys/epoll.h>
int epoll_create(int size);  
函数功能:
    创建一个内核事件表

函数参数:
    size:这个参数目前并不起作用,只是给内核一个提示,告诉它事件表需要多大。

返回值:
    返回指示内核事件表的文件描述符


2.epoll_ctl API

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数功能:
    操作内核事件表,向内核事件表中注册/修改/删除fd上的事件

函数参数:
    epfd :内核事件表的文件描述符
    op   :操作类型
        EPOLL_CTL_ADD,往内核事件表中注册fd上的事件
        EPOLL_CTL_MOD,修改fd上的注册事件
        EPOLL_CTL_DEL,删除fd上的注册事件
    fd   :待操作的文件描述符
    event:指定事件

返 回 值:
    成功时返回0,失败则返回-1并设置errno.

struct epoll_event
{
    __uint32_t   events;  //epoll事件
    epoll_data_t data;    //用户数据
};

typedef union epoll_data
{
    void *ptr;      //指定与fd相关的用户数据
    int  fd;        //指定事件从属的目标文件描述符
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

3.epoll_wait API

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数功能:
    在一段超时时间内等待一组文件描述符上的事件就绪.epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到它
的第二个参数events指向的数组中。

函数参数:
    epfd     :内核事件表的文件描述符
    events   :用于存放就绪的事件
    maxevents:指定最多监听多少个事件,它必须大于0
    timeout  :设置超时时间

返 回 值:
    成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno.

4.特点

         epoll_wait当检测到就绪事件时将所有的就绪事件从内核事件表中复制到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。

五、三种I/O复用函数的使用

1.select 

ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
if(ret < 0)
{
     printf("selection failure.\n");
     break;
}

if( FD_ISSET(connfd, &read_fds) )  //发生可读事件
{
    ret = recv(connfd, buf, sizeof(buf) - 1, 0);
    if(ret <= 0)
    {
        break;
    }
    printf("got %d bytes of normal data:%s.\n", ret, buf);
}

2.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
    }
}

3.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肯定就绪,直接处理
}

六、两种模式

    epoll对文件描述符的操作有两种模式:LT(Level Trigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式。

1.LT模式

    对于采用LT模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。

2.ET模式

    对于采用ET模式的文件描述符,当epoll_wait检测到其上有时间发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

注:每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那么读或写操作因为没有后续的事件而一直处于阻塞状态。

3.EPOLLONESHOT事件

        在并发程序中,使用ET模式会触发一个问题,比如一个线程(或进程)在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。我们期望的是一个socket连接在任一时刻都只被一个线程处理。这个可以通过EPOLLONESHOT事件实现。

        对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其注册的一个可读、可写或异常事件,且只触发一次。除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样当一个线程在处理某个socket时,其他线程可能有机会处理该socket。反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN时间能被触发,进而让其他工作线程有机会继续处理这个socket.

七、三组I/O复用函数的比较

系统调用selectpollepoll
事件集合用户通过3个参数分别传入感兴趣的可读、可写以及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置3个参数统一处理所有的事件类型,因此只需一个事件集参数,用户通过pollfd.events传入感兴趣的事件,内核通过修改poll.revents反馈其中就绪的事件内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件
应用程序索引就绪文件描述符的事件复杂度O(n)O(n)O(1)
最大支持的文件描述符数一般有最大值限制,如10246553565535
工作模式LTLT支持ET高效模式
内核实现和工作效率采用轮询方式来检测就绪事件,算法事件复杂度为O(n)采用轮询方式来检测就绪事件,算法事件复杂度为O(n)采用回调方式来检测就绪事件,算法事件复杂度为O(1)

 

 

读者如果对源码有兴趣,这里推荐两个博客大家可以看一下。

https://blog.csdn.net/vividonly/article/details/7539342

http://zhangyafeikimi.iteye.com/blog/248815

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值