I/O复用

来源:互联网 发布:linux搭建测试环境步骤 编辑:程序博客网 时间:2024/05/22 12:52

I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。
网络程序在以下情况下需要使用I/O技术:
(1)客户端程序要同时处理多个socket
(2)客户端程序要同时处理用户输入和网络连接
(3)TCP服务器既要处理监听socket和连接socket
(4)服务器要同时处理TCP请求和UDP请求
(5)服务器要同时监听多个端口,或者处理多种服务

I/O复用的三个系统调用:select、poll、epoll

  • select
系统调用原型:#include<sys/select.h>int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval* timeout);//nfds为被监听文件描述符的总数//三个fd_set* 的参数分别指向可读、可写、异常事件的文件描述符集合//timeout设置select的等待时间返回值:成功时返回就绪文件描述符的总数

既然有三个参数都是fd_set的指针,就先来看看fd_set的真面目!

简化版#include<typesizes.h>#define _FD_SETSIZE 1024typedef long int _fd_mask;   //4#undef _NFDBITS#define _NFDBITS (8*(int)sizeof(_fd_mask))   //32typedef struct{    _fd_mask fds_bits[_FD_SETSIZE/_NFDBITS];   1024/32=32}fd_set;

由上可见,fd_set结构体仅包含一个整形数组,该数组的每一个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符有FD_SETSIZE决定,而FD_SETSIZE的大小与系统版本有关。

select的底层通过sys_select实现,它将用户的数据(fd_set)拷贝到内核态,select中的每一个描述符都对应一个位,通过给每个事件分配一个bitmap,来对用户的读写,异常事件进行监听,然后由do_select去完成链表的建立,回调函数的设置,最后将就绪事件返回给了sys_select,由sys_select把发生就绪事件的文件描述符返回给用户。

select的参数类型fd_set没有将文件描述符与事件绑定,它仅仅是一个文件描述符集合。因此select需要提供3个种类型的参数来分别传入和输出可读、可写、异常事件,所以select能处理的事件类型有限。另外由于内核对于fd_set是在线修改,应用程序下次调用select前需要重置fs_set集合,select采用的是轮询,每次调用都要扫描整个注册的文件描述符集合,并将其中就绪的文件描述返回给用户程序,时间复杂度为O(n).

  • poll
#include<poll.h>int poll(struct pollfd fds[],nfds_t nfds,int timeout);//fds为pollfd结构的数组,nfds指定被监听事件集合fds的大小,timeout指定poll的超时值
struct pollfd{    int fd;         //指定成员描述符    short events;   //告诉poll监听fd上的哪些事件,是一系列事件的按位或    short revents;   //由内核修改,通知应用程序fd上发生了哪些事件,如果当某个文件描述符有状态变化时,revents的值就不为空。}

poll的参数类型pollfd,将文件描述符与和事件都绑定在一起,内核每次只会修改结构体中revents的值,而event的值保持不变,因此下次调用poll时应用程序无须重置pollfd类型的事件集参数,支持的最大文件描述符数为65535个,和select一样,也是用的轮询,时间复杂度为O(n)。

poll的使用

int ret = poll(fds,MAX_EVENT_NUMBER,-1);for(int i = 0 ;i < MAX_EVENT_NUMBER;i++){    if(fds[i].revent & POLLIN)    {        int socket = fds[i].fd;    }}
  • epoll
    epoll使用一组函数来完成任务,而不是单个函数,epoll把用户关心的文件描述符上的事件都存放在内核的一个事件表中,而不是像select和poll那样,每次调用都重复传入文件描述符或事件集。但epoll需要一个额外的文件描述符来唯一标识这个事件表。epoll_create函数可以创建这个内核事件表的文件描述符。
#include<sys/epoll.h>int epoll_create(int size);

在这个函数中,size只起到了一个参数检查的作用,没有实际分配大小的作用,该函数返回的该事件表的文件描述符。

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)//fd是要操作的文件描述符,op指定操作类型,event指定事件op的类型有3种:EPOLL_CTL_ADD  往事件表中注册fd上的事件EPOLL_CTL_MOD  修改fd上的注册事件EPOLL_CTL_DEL  删除fd上的注册事件/*struct epoll_event{    _uint32_t events;   //epoll事件    epoll_data_t data;    //用户事件,一个联合体(用来存放内核拷贝的就绪事件)}typedef union epoll_data{    void* ptr;    int fd;    uint32_t u32;    uint64_t u64;}epoll_data_t;*/成功时返回0,失败返回-1
int epoll_wait(int epfd,struct epoll_event* event,int maxevents,int timeout)/*maxevent指定最多监听多少个事件,必须大于0  event用来存放内核拷贝的就绪事件,提高epoll的效率*/
int ret = epoll(epollfd,events,MAX_EVENT_NUMBER,-1);for(int i = 0;i<ret;i++){    int sockfd = event[i].data.fd;}

不得不说到epoll的两种模式ET和LT,LT是默认的工作模式,当往内核表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式工作
对于采用LT模式下的文件描述符,当epoll_wait检测到有事件发生时,会将事件通知应用程序,应用程序可以不立即处理该事件。当应用程序下一次在调用epoll_wait时,会再一次通知应用程序,直到事件被处理为止。but在ET模式下, 当检测到事件发生时通知应用程序,应用程序必须立即处理,因为后续的调用epoll_wait将不会在通知此事件。所以ET模式在很大程度上,减少了同一个事件被重复触发的次数,这就是LT模式比ET模式高效的原因。
epoll在内核中维护一张事件表,提供了一个独立的系统调用epoll_ctl来控制往其中添加、修改、删除事件,这样,每次epoll_wait调用都是从内核时间表中取得用户注册的事件,而无须每次都从用户空间读入这些事件。epoll_wait系统调用中的events参数用来返回就绪事件,这使得应用程序索引就绪文件的时间复杂度为O(1)。
epoll能监听的最大文件描述符数目能达到65535个。
select、poll都是采用轮询的方式,每次调用都要扫描整个文件描述符集合,并将其中就绪的文件描述符返回给用户程序,但是epoll采用的是回调,内核检测到有就绪的文件描述符时,将触发回调函数,回调函数会将文件描述符上对应的事件插入内核就绪事件队列上,内核在适当的时机会把就绪事件队列上的内容拷贝到用户空间。

原创粉丝点击