我是靠谱客的博主 甜甜黄蜂,最近开发中收集的这篇文章主要介绍I/O多路复用,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

近日涉及I/O多路复用的知识,在此做一个梳理和备忘。

转载地址:https://zhuanlan.zhihu.com/p/22834126  https://blog.csdn.net/davidsguo008/article/details/73556811

 

概念引入

 

I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

 

Linux中基于socket的通信本质也是一种I/O,使用socket()函数创建的套接字默认都是阻塞的,这意味着当sockets API的调用不能立即完成时,线程一直处于等待状态,直到操作完成获得结果或者超时出错。会引起阻塞的socket API分为以下四种:

•输入操作:recv()recvfrom()。以阻塞套接字为参数调用该函数接收数据时,如果套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。

•输出操作:send()sendto()。以阻塞套接字为参数调用该函数发送数据时,如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。

•接受连接:accept()。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。

•外出连接:connect()。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少服务器的一次往返时间。

 

使用阻塞模式的套接字编写网络程序比较简单,容易实现。但是在服务器端,通常要处理大量的套接字通信请求,如果线程阻塞于上述的某一个输入或输出调用时,将无法处理其他任何运算或响应其他网络请求,这么做无疑是十分低效的,当然可以采用多线程,但大量的线程占用很大的内存空间,并且线程切换会带来很大的开销。而I/O多路复用模型能处理多个connection的优点就使其能支持更多的并发连接请求。

 

Linux支持I/O多路复用的系统调用有selectpollepoll,这些调用都是内核级别的。但selectpollepoll本质上都是同步I/O,先是block住等待就绪的socket,再block住将数据从内核拷贝到用户内存空间。

 

 

 

2.1 select详解

 

Linux提供的select相关函数接口如下:

 

#include <sys/select.h>

#include <sys/time.h>

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

FD_ZERO(int fd, fd_set* fds)   //清空集合

FD_SET(int fd, fd_set* fds)    //将给定的描述符加入集合

FD_ISSET(int fd, fd_set* fds)  //判断指定描述符是否在集合中

FD_CLR(int fd, fd_set* fds)    //将给定的描述符从文件中删除  

 

1.select函数的返回值就绪描述符的数目,超时时返回0,出错返回-1

2.第一个参数max_fd指待测试的fd个数,它的值是待测试的最大文件描述符加1,文件描述符从0开始到max_fd-1都将被测试。

3.中间三个参数readsetwritesetexceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为NULL

 

select的缺点:

1.单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)

2.内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;

3.select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

4.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

 

 

 

2.2 poll详解

 

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。

 

Linux提供的poll函数接口如下:


#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

 

typedef struct pollfd {

        int fd;                         //需要被检测或选择的文件描述符

        short events;                   //对文件描述符fd上感兴趣的事件

        short revents;                  //文件描述符fd上当前实际发生的事件*/

} pollfd_t;

 

1.poll()函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;

2.fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;

3.nfds记录数组fds中描述符的总数量;

4.timeout是调用poll函数阻塞的超时时间,单位毫秒;

5.一个pollfd结构体表示一个被监视的文件描述符,通过传递fds[]指示poll()监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。

 

合法的事件如下:

POLLIN            有数据可读

POLLRDNORM        有普通数据可读

POLLRDBAND        有优先数据可读

POLLPRI           有紧迫数据可读

POLLOUT           写数据不会导致阻塞

POLLWRNORM        写普通数据不会导致阻塞       

POLLWRBAND              写优先数据不会导致阻塞    

POLLMSGSIGPOLL        消息可用

当需要监听多个事件时,使用POLLIN | POLLRDNORM设置events域;当poll调用之后检测某事件是否发生时,fds[i].revents & POLLIN进行判断。

 

相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

 

 

2.3 epoll详解

 

epollLinux2.6内核正式提出,是基于事件驱动的I/O方式,相对于selectpoll来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。优点如下:

 

1.没有最大并发连接的限制,能打开的fd上限远大于10241G的内存能监听约10万个端口)

2.采用回调的方式,效率提升。只有活跃可用的fd才会调用callback函数,也就是说epoll只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于selectpoll

3.内存拷贝。使用mmap()文件映射内存来加速与内核空间的消息传递,减少复制开销。

 

epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)ET(edge trigger,边缘触发)

水平触发:默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。

边缘触发:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次)。

ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。

 

Linux中提供的epoll相关函数接口如下:

 

#include <sys/epoll.h>

int epoll_create(int size);

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

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 

1.epoll_create函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1

2.epoll_ctl函数注册要监听的事件类型。四个参数解释如下:

  epfd表示epoll句柄;

  op表示fd操作类型:EPOLL_CTL_ADD(注册新的fdepfd中),EPOLL_CTL_MOD(修改已注册的fd的监听事件),EPOLL_CTL_DEL(从epfd中删除一个fd

 fd是要监听的描述符;

 event表示要监听的事件


epoll_event结构体定义如下:

struct epoll_event {

    __uint32_t events;  /* Epoll events */

    epoll_data_t data;  /* User data variable */

};

 

typedef union epoll_data {

    void *ptr;

    int fd;

    __uint32_t u32;

    __uint64_t u64;

} epoll_data_t;

 

3.epoll_wait函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回-1,等待超时返回0

 epfdepoll句柄

  events表示从内核得到的就绪事件集合

  maxevents告诉内核events的大小

  timeout表示等待的超时事件

 


最后,附上一个epoll编程实例。

#include <sys/socket.h>  
#include <sys/epoll.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <errno.h>  
#include <iostream>  
using namespace std;  
#define MAX_EVENTS 500  
struct myevent_s  
{  
    int fd;  
    void (*call_back)(int fd, int events, void *arg);  
    int events;  
    void *arg;  
    int status; // 1: in epoll wait list, 0 not in  
    char buff[128]; // recv data buffer  
    int len, s_offset;  
    long last_active; // last active time  
};  
// set event  
void EventSet(myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg)  
{  
    ev->fd = fd;  
    ev->call_back = call_back;  
    ev->events = 0;  
    ev->arg = arg;  
    ev->status = 0;
    bzero(ev->buff, sizeof(ev->buff));
    ev->s_offset = 0;  
    ev->len = 0;
    ev->last_active = time(NULL);  
}  
// add/mod an event to epoll  
void EventAdd(int epollFd, int events, myevent_s *ev)  
{  
    struct epoll_event epv = {0, {0}};  
    int op;  
    epv.data.ptr = ev;  
    epv.events = ev->events = events;  
    if(ev->status == 1){  
        op = EPOLL_CTL_MOD;  
    }  
    else{  
        op = EPOLL_CTL_ADD;  
        ev->status = 1;  
    }  
    if(epoll_ctl(epollFd, op, ev->fd, &epv) < 0)  
        printf("Event Add failed[fd=%d], evnets[%d]n", ev->fd, events);  
    else  
        printf("Event Add OK[fd=%d], op=%d, evnets[%0X]n", ev->fd, op, events);  
}  
// delete an event from epoll  
void EventDel(int epollFd, myevent_s *ev)  
{  
    struct epoll_event epv = {0, {0}};  
    if(ev->status != 1) return;  
    epv.data.ptr = ev;  
    ev->status = 0;
    epoll_ctl(epollFd, EPOLL_CTL_DEL, ev->fd, &epv);  
}  
int g_epollFd;  
myevent_s g_Events[MAX_EVENTS+1]; // g_Events[MAX_EVENTS] is used by listen fd  
void RecvData(int fd, int events, void *arg);  
void SendData(int fd, int events, void *arg);  
// accept new connections from clients  
void AcceptConn(int fd, int events, void *arg)  
{  
    struct sockaddr_in sin;  
    socklen_t len = sizeof(struct sockaddr_in);  
    int nfd, i;  
    // accept  
    if((nfd = accept(fd, (struct sockaddr*)&sin, &len)) == -1)  
    {  
        if(errno != EAGAIN && errno != EINTR)  
        {  
        }
        printf("%s: accept, %d", __func__, errno);  
        return;  
    }  
    do  
    {  
        for(i = 0; i < MAX_EVENTS; i++)  
        {  
            if(g_Events[i].status == 0)  
            {  
                break;  
            }  
        }  
        if(i == MAX_EVENTS)  
        {  
            printf("%s:max connection limit[%d].", __func__, MAX_EVENTS);  
            break;  
        }  
        // set nonblocking
        int iret = 0;
        if((iret = fcntl(nfd, F_SETFL, O_NONBLOCK)) < 0)
        {
            printf("%s: fcntl nonblocking failed:%d", __func__, iret);
            break;
        }
        // add a read event for receive data  
        EventSet(&g_Events[i], nfd, RecvData, &g_Events[i]);  
        EventAdd(g_epollFd, EPOLLIN, &g_Events[i]);  
    }while(0);  
    printf("new conn[%s:%d][time:%d], pos[%d]n", inet_ntoa(sin.sin_addr),
            ntohs(sin.sin_port), g_Events[i].last_active, i);  
}  
// receive data  
void RecvData(int fd, int events, void *arg)  
{  
    struct myevent_s *ev = (struct myevent_s*)arg;  
    int len;  
    // receive data
    len = recv(fd, ev->buff+ev->len, sizeof(ev->buff)-1-ev->len, 0);    
    EventDel(g_epollFd, ev);
    if(len > 0)
    {
        ev->len += len;
        ev->buff[len] = '';  
        printf("C[%d]:%sn", fd, ev->buff);  
        // change to send event  
        EventSet(ev, fd, SendData, ev);  
        EventAdd(g_epollFd, EPOLLOUT, ev);  
    }  
    else if(len == 0)  
    {  
        close(ev->fd);  
        printf("[fd=%d] pos[%d], closed gracefully.n", fd, ev-g_Events);  
    }  
    else  
    {  
        close(ev->fd);  
        printf("recv[fd=%d] error[%d]:%sn", fd, errno, strerror(errno));  
    }  
}  
// send data  
void SendData(int fd, int events, void *arg)  
{  
    struct myevent_s *ev = (struct myevent_s*)arg;  
    int len;  
    // send data  
    len = send(fd, ev->buff + ev->s_offset, ev->len - ev->s_offset, 0);
    if(len > 0)  
    {
        printf("send[fd=%d], [%d<->%d]%sn", fd, len, ev->len, ev->buff);
        ev->s_offset += len;
        if(ev->s_offset == ev->len)
        {
            // change to receive event
            EventDel(g_epollFd, ev);  
            EventSet(ev, fd, RecvData, ev);  
            EventAdd(g_epollFd, EPOLLIN, ev);  
        }
    }  
    else  
    {  
        close(ev->fd);  
        EventDel(g_epollFd, ev);  
        printf("send[fd=%d] error[%d]n", fd, errno);  
    }  
}  
void InitListenSocket(int epollFd, short port)  
{  
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);  
    fcntl(listenFd, F_SETFL, O_NONBLOCK); // set non-blocking  
    printf("server listen fd=%dn", listenFd);  
    EventSet(&g_Events[MAX_EVENTS], listenFd, AcceptConn, &g_Events[MAX_EVENTS]);  
    // add listen socket  
    EventAdd(epollFd, EPOLLIN, &g_Events[MAX_EVENTS]);  
    // bind & listen  
    sockaddr_in sin;  
    bzero(&sin, sizeof(sin));  
    sin.sin_family = AF_INET;  
    sin.sin_addr.s_addr = INADDR_ANY;  
    sin.sin_port = htons(port);  
    bind(listenFd, (const sockaddr*)&sin, sizeof(sin));  
    listen(listenFd, 5);  
}  
int main(int argc, char **argv)  
{  
    unsigned short port = 12345; // default port  
    if(argc == 2){  
        port = atoi(argv[1]);  
    }  
    // create epoll  
    g_epollFd = epoll_create(MAX_EVENTS);  
    if(g_epollFd <= 0) printf("create epoll failed.%dn", g_epollFd);  
    // create & bind listen socket, and add to epoll, set non-blocking  
    InitListenSocket(g_epollFd, port);  
    // event loop  
    struct epoll_event events[MAX_EVENTS];  
    printf("server running:port[%d]n", port);  
    int checkPos = 0;  
    while(1){  
        // a simple timeout check here, every time 100, better to use a mini-heap, and add timer event  
        long now = time(NULL);  
        for(int i = 0; i < 100; i++, checkPos++) // doesn't check listen fd  
        {  
            if(checkPos == MAX_EVENTS) checkPos = 0; // recycle  
            if(g_Events[checkPos].status != 1) continue;  
            long duration = now - g_Events[checkPos].last_active;  
            if(duration >= 60) // 60s timeout  
            {  
                close(g_Events[checkPos].fd);  
                printf("[fd=%d] timeout[%d--%d].n", g_Events[checkPos].fd, g_Events[checkPos].last_active, now);  
                EventDel(g_epollFd, &g_Events[checkPos]);  
            }  
        }  
        // wait for events to happen  
        int fds = epoll_wait(g_epollFd, events, MAX_EVENTS, 1000);  
        if(fds < 0){  
            printf("epoll_wait error, exitn");  
            break;  
        }  
        for(int i = 0; i < fds; i++){  
            myevent_s *ev = (struct myevent_s*)events[i].data.ptr;  
            if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN)) // read event  
            {  
                ev->call_back(ev->fd, events[i].events, ev->arg);  
            }  
            if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT)) // write event  
            {  
                ev->call_back(ev->fd, events[i].events, ev->arg);  
            }  
        }  
    }  
    // free resource  
    return 0;  
}   

 

完!



最后

以上就是甜甜黄蜂为你收集整理的I/O多路复用的全部内容,希望文章能够帮你解决I/O多路复用所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部