拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 select,poll,epoll的区别以及使用方法

select,poll,epoll的区别以及使用方法

白鹭 - 2022-01-26 1966 0 0

I/O多路复用是指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,

原生socket客户端在与服务端建立连接时,即服务端呼叫accept方法时是阻塞的,同时服务端和客户端在收发资料(呼叫recv、send、sendall)时也是阻塞的,原生socket服务端在同一时刻只能处理一个客户端请求,即服务端不能同时与多个客户端进行通信,实作并发,导致服务端资源闲置(此时服务端只占据 I/O,CPU空闲),

如果我们的需求是要让多个客户端连接至服务器端,而且服务器端需要处理来自多个客户端请求,很明显,原生socket实作不了这种需求,此时我们使用I/O多路复用机制就可以实作这种需求,可以同时监听多个档案描述符,一旦描述符就绪,能够通知程序进行相应的读写操作,

linux中的IO多路复用

(1)select

select最早于1983年出现在4.2BSD中,它通过一个select()系统呼叫来监视多个档案描述符的阵列,当select()回传后,该阵列中就绪的档案描述符便会被内核修改标志位,使得行程可以获得这些档案描述符从而进行后续的读写操作,

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一,

select的一个缺点在于单个行程能够监视的档案描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制,

另外,select()所维护的存盘大量档案描述符的资料结构,随着档案描述符数量的增大,其复制的开销也线性增长,同时,由于网络回应时间的延迟使得大 量TCP连接处于非活跃状态,但呼叫select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销,

(2)poll

poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大档案描述符数量的限制,

poll和select同样存在一个缺点就是,包含大量档案描述符的阵列被整体复制于用户态和内核的地址空间之间,而不论这些档案描述符是否就绪,它的开销随着档案描述符数量的增加而线性增大,

另外,select()和poll()将就绪的档案描述符告诉行程后,如果行程没有对其进行IO操作,那么下次呼叫select()和poll()的时候 将 再次报告这些档案描述符,所以它们一般不会丢失就绪的讯息,这种方式称为水平触发(Level Triggered),

(3)epoll

直到Linux2.6才出现了由内核直接支持的实作方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法,

epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉行程哪些档案描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实作相当复杂,

epoll同样只告知那些就绪的档案描述符,而且当我们呼叫epoll_wait()获得就绪档案描述符时,回传的不是实际的描述符,而是一个代表就绪描 述符数量的 值,你只需要去epoll指定的一个阵列中依次取得相应数量的档案描述符即可,这里也使用了存储器映射(mmap)技术,这样便彻底省掉了这些档案描述符在 系统呼叫时复制的开销,

另一个本质的改进在于epoll采用基于事件的就绪通知方式,在select/poll 中,行程只有在呼叫一定的方法后,内核才对所有监视的档案描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个档案描述符,一旦基于某 个档案描述符就绪时,内核会采用类似callback的回呼机制,迅速激活这个档案描述符,当行程呼叫epoll_wait()时便得到通知,

总结:

select

select的几大缺点:

(1)每次呼叫select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次呼叫select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的档案描述符数量太小了,默认是1024

poll

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大档案描述符数量的限制,poll和select同样存在一个缺点就是,包含大量档案描述符的阵列被整体复制于用户态和内核的地址空间之间,而不论这些档案描述符是否就绪,它的开销随着档案描述符数量的增加而线性增大,

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本,相对于select和poll来说,epoll更加灵活,没有描述符限制,epoll使用一个档案描述符管理多个描述符,将用户关系的档案描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次,

最终呼叫epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);函式等待事件到来,回传值是需要处理的事件数目,events表示要处理的事件集合,

一句话总结

(1)select,poll实作需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替,而epoll其实也需要呼叫epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,呼叫回呼函式,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的行程,虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间,这就是回呼机制带来的性能提升,

(2)select,poll每次呼叫都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列),这也能节省不少的开销,

epoll的使用方法

epoll的界面非常简单,一共就三个函式,

1,epoll_create

/*
size:在 Linux最新的一些内核版本的实作中,这个 size自变量没有任何意义,
回传值:回传值为一个档案描述符,作为后面两个函式的自变量
*/
int epoll_create(int size)

此函式可以在内核中创建一个内核事件表,通过回传的内核事件表来管理

2,epoll_ctl

/*
epfd:操作内核时间表的档案描述符,即epoll_create函式的回传值
op:操作内核时间表的方式
	EPOLL_CTL_ADD(向内核时间表添加档案描述符,即注册);
	EPOLL_CTL_MOD(修改内核事件表事件);
	EPOLL_CTL_DEL (洗掉内核事件表中的事件);
fd:操作的档案描述符
event:指向struct epoll_event的指标
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

poll的事件注册函式,epoll_ctl向 epoll物件中添加、修改或者洗掉感兴趣的事件,回传0表示成功,否则回传–1,此时需要根据errno错误码判断错误型别,

event结构

struct epoll_event
{
    /*
    储存用户感兴趣的事情和就绪事件,
    events可以是以下几个宏的集合:
    EPOLLIN :表示对应的档案描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的档案描述符可以写;
    EPOLLPRI:表示对应的档案描述符有紧急的资料可读(这里应该表示有带外资料到来);
    EPOLLERR:表示对应的档案描述符发生错误;
    EPOLLHUP:表示对应的档案描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的,
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
    */
    uint32_t events; 
    epoll_data_t data; //联合体最重要的就是fd,即要操作的档案描述符
};
 
typedef union epoll_data
{
    void *ptr;
    int fd;
    _uint32_t u32;
    _uint64_t u64;
}epoll_data_t;

3,epoll_wait

/*
epfd:同上面函式
events:用于接收内核回传的就绪事件的阵列
maxevents:用户最多能处理的事件个数
等待I/O的超时值(后面的编程设为-1,表示永不超时),单位为ms
回传值,指的是就绪事件的个数
*/
int epoll_wait(int epfd, struct epoll_event events, int maxevents, int timeout)

等待事件的产生,类似于select()呼叫,自变量events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,自变量timeout是超时时间(毫秒,0会立即回传,-1将不确定,也有说法说是永久阻塞),该函式回传需要处理的事件数目,如回传0表示已超时,如果回传–1,则表示出现错误,需要检查 errno错误码判断错误型别,

下面通过一个echo回射服务器的客户端和服务端案例介绍epoll的使用方法

服务端事件poll

    int epollFd;
    struct epoll_event events[MAX_EVENTS];
    int ret;
    char buf[MAXSIZE];
    memset(buf,0,MAXSIZE);
    //创建一个epoll描述符,通过这个描述管理多个描述符
    epollFd = epoll_create(FDSIZE);
    //添加监听描述符事件
    add_event(epollFd,listenFd,EPOLLIN);
    while(1){
        //获取已经准备好的描述符事件,阻塞
        ret = epoll_wait(epollFd, events, MAX_EVENTS,-1);
        //处理事件,ret是发生的事件个数
        handle_events(epollFd,events,ret,listenFd,buf);
    }
    close(epollFd);

客户端事件poll

    int                 sockfd;
    struct sockaddr_in  servaddr;
    sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = inet_addr(IPADDRESS);
    printf("start\n");
    if(connect(sockfd,(struct sockaddr*)&servaddr, sizeof(sockaddr_in)) < 0){
        perror("connect err: ");
        return 0;
    }
    else{
        printf("connect succ\n");
    }
    //处理连接
    handle_connection(sockfd);
    close(sockfd);
    return 0;

程序运行结果

客户端

./cli
start
connect succ
cli hello
epollfd 4, rdfd 0, sockfd 3, read 10
epollfd 4, wrfd 3, sockfd 3, write 10
epollfd 4, rdfd 3, sockfd 3, read 10
cli hello
epollfd 4, wrfd 1, sockfd 3, write 10
cli over
epollfd 4, rdfd 0, sockfd 3, read 9
epollfd 4, wrfd 3, sockfd 3, write 9
epollfd 4, rdfd 3, sockfd 3, read 9
cli over
epollfd 4, wrfd 1, sockfd 3, write 9
^C

服务端

./srv accept a new client: 127.0.0.1:37098, fd = 5read fd=5, num read=10read message is : cli hellowrite fd=5, num write=10read fd=5, num read=9read message is : cli overwrite fd=5, num write=9read fd=5, num read=0client close.^C

本文简单总结了select,poll,epoll的使用方法以及各自的优劣势,以及写了一个epoll的demo供参考,详细的运行机制参考文章,

程序源代码详见公众号 xutopia77 的文章 《select,poll,epoll的区别以及使用方法》

标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *