您的位置:首页 > 理论基础 > 计算机网络

网络编程_非阻塞与io复用

2017-10-08 00:06 309 查看
                                           网络编程_非阻塞与io复用

一:阻塞与非阻塞

    默认创建的socket是阻塞式的,即在创建好的socket上调用读写函数时如果没有相应的数据可读或写缓冲区没有足够的空间、调用connect函数时如果三次握手还没有完成、调用accept函数时如果已完成连接队列为空进程将被阻塞,直到操作完或者超时。可以用下图来展示阻塞式socket的流程。


  

    图中显示要么收到数据返回,要么阻塞在函数调用一直等待。

 

    非阻塞式socket在执行读写、connnet、accept操作时总是立即返回,不管事件是否已经发生。如果操作完事件已经发生,则返回正确的结果,否则返回-1,错误码保存在errno中。对于accept、读写而言,如果事件没发生errno被设置为EAGAIN或者EWOULDBLOCK,对于connect而言,errno则被设置成EINPROGRESS。所以在操作非阻塞函数是在判定返回值的同时也需要对判定errno的值,防止误伤。可以用下图来展示非阻塞式socket的流程。

                                  


 

    图中显示即使没有收到数据也返回,直到有数据时才返回我们需要的数据,并且返回成功。观察图中可以发现,既然进程没有阻塞在读调用上,那么就可以在函数返回失败时去做点其他事情,等进程闲的时候再去读,这样进程就不会在等待读的过程中浪费怎么多时间了。

    我们可以通过在调用socket函数创建socket的时在第二个参数传递

SOCK_NONBLOCK标志位,或者在创建好的socket上执行fcntl系统调用的F_SETFL命令,设置O_NONBLOCK标志,即可让socket变为非阻塞的。不过accept上来的socket的非阻塞模式可以从监听socket继承而来。

 

二:io复用

    上面讲到了阻塞和非阻塞的socket,要么一直阻塞在系统调用上,要么需要不断的轮询,这个两种效率都太低了,我们需要一种机制可以在数据准备好了通知我们,然后我们去读就好了。这就是我们这里要介绍的io复用机制。我们先用通过一张图来了解一下io复用的的大体流程。

                                  


    从图中可以看到,现在不是阻塞在读写函数了,而是阻塞在io复用的函数(select)调用上。当有数据可读或可写入的时该调用将返回可读或可写的信息。当前了如果select函数只作用在单个的socket函数上时,程序的性能不会有任何的提高,反而性能会更差,但是io复用函数可以同时等待多个socket的就绪状态(包括可读,可写、连接完成、连接断开等状态),再加上程序可以在处理完其他的任务时再去等待socket的就绪状态,并且在一定时间内如果没有就绪状态的socket就超时返回继续处理其他任务,这样程序的性能就会大大的提高。

    下面章节将介绍一下几个和io复用相关的系统调用

 

三:select系统调用

     select系统调用的会将用户感兴趣的socket列表及感兴趣的事件位告知系统,系统依次轮询该列表,直到有用户感兴趣的事件发生,则返回,并将用户发生的事件设置到对应的队列中返回。

    #include<sys/select.h>  

int select(int maxfdp1,fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数说明:

        nfds: 待监听的最大socket值+1,轮询的时候用到

        readfds: 待监听的可读文件socket 集合,返回时为发生可读事件的socket集合,           返回可读并不意味着真的可读,在出错或连接断开也会设置为可读可写,使用

            的时候需要做判定,特别是对与等待connect完成的socket,不能返回可读

            或可写就认为连接成功,其实也可能是失败。这里要注意,每次调用select              函数之前都要设置一遍readfds,其他几个fd_set也一样。

        writefds: 待监听的可写文件socket集合,返回是为发生可写事件的socket集合,

            同样需要注意返回并不代码是成功,也可能出错或者连接断开。

        exceptfds: 待监听的异常文件fd集合,这个一般不会用到

        timeout: 超时设置,如果为空则一直等待直到有就绪的事件,如果timeout非空             且值为0则会立马返回不管是否有就绪的事件,其他表将会在等待指定时间后

            返回超时。

        返回值:返回满足条件的fd数量和,如果出错返回-1,如果是超时返回0

 

其中fd_set的定义大致如下:

        #define__NFDBITS   (8 * sizeof(unsigned long))

       #define__FD_SETSIZE    1024

       #define__FDSET_LONGS   (__FD_SETSIZE/__NFDBITS)

 

       typedefstruct {

         unsigned long fds_bits [__FDSET_LONGS];

       }__kernel_fd_se

由上可见,fd_set包含一个整型数组,该数组中每一位代表一个文件描述符,fd_set可以容纳的文件描述符个数为__FD_SETSIZE(1024),这就限制了select能同时处理文件描述符的总量。
4000

为了方便操作fd_set结构,系统还提供了如下宏定义

    void FD_SET(int fd, fd_set *set); //设置fd_set的fd位

    void FD_ZERO(fd_set *set);        //清除fd_set的所有位

    void FD_CLR(int fd, fd_set *set); //清除fd_set的fd位

    int  FD_ISSET(int fd,fd_set *set);//测试fd_set的fd位是否被设置,这个可以                                        //用来检测返回值中是否包含指定的fd

 

四:epoll系统调用

    每个epoll实例在内核空间中都有独立的内存空间,用于存储和管理用户感兴趣的socket及事件。对于用户添加进来的感兴趣事件会存储在一个红黑树中,对于已经发生的用户感兴趣事件则存储在一个双向链表中。对于用户每次设置的事件,都会和设备驱动程序建立回调关系,当事件发生时会会调用这里的回调函数,将事件设置到双向链表中,epoll只需要检测双向链表是否为空即可知道是否有事件发生。下面介绍一下相关的函数。

    int epoll_create(intsize);

    用于创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大,epoll句柄占用一个文件描述符,所以在使用完epoll时需要调用close关闭,否则将造成文件描述符被耗尽的可能。

 

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event);

    epoll的事件注册函数,第一个参数是 epoll_create() 的返回值,第二个参数表示动作,使用如下三个宏来表示:

    EPOLL_CTL_ADD    //注册新的fd到epfd中;

    EPOLL_CTL_MOD    //修改已经注册的fd的监听事件;

    EPOLL_CTL_DEL    //从epfd中删除一个fd;

 

    第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event 结构如下:

    typedef union epoll_data

    {

            void        *ptr;

        int         fd;

         __uint32_t  u32;

        __uint64_t   u64;

    } epoll_data_t;

 

    struct epoll_event {

           __uint32_t events; /*Epoll events */

           epoll_data_t data; /*User data variable */

    };

    events 可以是以下几个宏的集合:

    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, intmaxevents, int timeout);

    参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

 

    EPOLL事件有两种模型 Level Triggered (LT) 和 Edge Triggered (ET):

    LT(level triggered,水平触发模式)是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。

 

    ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件

 

 

参考资料:

    《union网络编程卷1》

    《linux高性能服务器编程》

    《Nginx模块开发与架构解析》

    http://www.cnblogs.com/haippy/archive/2012/01/09/2317269.html
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: