c语言 select poll epoll 区别 总结

IO复用

​ 为了解决大量客户端访问的问题,引入IO技术:一个进程可以同事对多个客户请求进行服务,复用一个进程对多个IO进行服务。IO读写的数据多数情况下没准备好,需要通过一个函数监听这些数据状态,一旦有数据可以读写就服务。

​ select,poll,epoll都是IO多路复用的机制,监视多个描述符,一旦某个描述符就绪,通知程序进行操作。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

select

头文件

/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

函数

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

描述

​ select 允许程序监视多个文件描述符,直到一个或多个文件描述符对于某种I / O操作。如果可以执行文件描述符,则认为文件描述符已准备就绪相应的I / O操作。

​ select()只能监视小于FD_SETSIZE的文件描述符号。使用的超时时间是结构体时间,以秒和微秒为单位。可以更新超时参数以指示还剩多少时间。没有sigmask参数。

​ 在readfds中列出,监视三个独立的文件描述符集:

​ 观察是否可以读取字符、查看是否有可用空间来写入、监视exceptfds中的异常。如果没有文件描述符,则三个文件描述符集的每一个都可以指定为NULL。

​ FD_CLR()分别从集合中添加和删除给定的文件描述符。

​ FD_ISSET() 查看文件描述符是否是集合的一部分;

​ nfds是三组中编号最高的文件描述符,加1。

​ timeout参数指定select()应该等待文件描述符准备就绪的间隔,直到发生以下任何一种情况:文件描述符准备就绪、呼叫被信号处理程序中断、超时到期。超时间隔将四舍五入为系统时钟的粒度,并且内核调度延迟意味着阻塞间隔可能会少量溢出。如果timeval结构的两个字段均为零,则select()立即返回。

​ <sys/time.h> 中时间结构体定义如下:

struct timeval {
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* microseconds */
};	

​ 一些代码调用select(),将所有三个集合都设为空,nfds为零,并且将非NULL超时作为相当便携式的亚秒级精度睡眠方式。

返回值

​ 成功后,select()返回包含在文件中的文件描述符的数量。返回的三个描述符集(即readfds中设置的总位数,writefds,exceptfds),如果超时在事情之前到期,则可能为零发生。

​ 如果出错,则返回-1,并且将errno设置为指示错误;

​ 否则为0。 文件描述符集未修改,并且超时变未定义。

错误

EBADF	在一组中给出了无效的文件描述符。(也许是一个文件描述-已经关闭的Tor,或发生错误的Tor。)
EINTR	捕获到一个信号; 参见signal(7)。
EINVAL	nfds为负或超过RLIMIT_NOFILE资源限制(getrlimit(2))。
EINVAL	超时中包含的值无效。
ENOMEM	无法为内部表分配内存。

例子

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
    fd_set rfds;
    struct timeval tv;
    int retval;

    /* Watch stdin (fd 0) to see when it has input. */

    FD_ZERO(&rfds);
    FD_SET(0, &rfds);

    /* Wait up to five seconds. */

    tv.tv_sec = 5;
    tv.tv_usec = 0;

    retval = select(1, &rfds, NULL, NULL, &tv);
    /* Don't rely on the value of tv now! */

    if (retval == -1)
        perror("select()");
    else if (retval) {
        char buf[512] = {0};
        printf("Data is available now.\n");
        scanf("%[^\n]s", buf);
        /* FD_ISSET(0, &rfds) will be true. */
    }
    else
        printf("No data within five seconds.\n");

    exit(EXIT_SUCCESS);
}

poll

头文件

#include <poll.h>

函数

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

描述

​ poll()执行与select(2)类似的任务:它等待一组文件描述符中的准备执行I / O。

struct pollfd {
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};

​ 调用者应在nfds中指定fds数组中的项目数。

​ 字段fd包含打开文件的文件描述符。如果该字段为负,则相应的events字段将被忽略,而revents字段将返回零。

​ 字段events是输入参数,位掩码指定应用程序的事件对文件描述符fd。该字段可以指定为零,在这种情况下,只能在清除中返回的事件是POLLHUP,POLLERR和POLLNVAL

​ 字段revents是一个输出参数,由内核填充执行该事件的事件。revents中返回的位可以包括事件中指定的任何位,或值POLLERR,POLLHUP或POLLNVAL之一。 (这三位在事件字段,并且只要相应的条件是正确的。)

​ 如果任何文件描述都未发生请求的事件(也没有错误),poll()阻塞,直到其中一个事件发生:文件描述符准备就绪、呼叫被信号处理程序中断、超时到期。

​ <poll.h>中定义了可以在事件和清除中设置/返回的位:

​ POLLIN 有要读取的数据。

​ POLLPRI 有紧急数据要读取(例如,TCP套接字上的带外数据

​ POLLOUT现在可以进行写操作,尽管写操作要比套接字或管道仍将阻塞(除非设置了O_NONBLOCK)。

​ POLLRDHUP流套接字对等体关闭连接,或关闭写入一半连接。必须定义_GNU_SOURCE功能测试宏

​ POLLERR 错误条件仅在revent中返回;在events中忽略)。

​ POLLHUP

​ POLLNVAL 无效的请求:fd未打开

返回值

​ 成功时,将返回正数;非零的revent字段的结构的数量

​ 值为0表示调用超时并且没有文件描述符准备好。

​ 发生错误时,返回-1,并正确设置errno。

错误

​ EFAULT作为参数给出的数组未包含在调用程序的地址中空间。

    EINTR在任何请求的事件之前发生信号;参见signal(7)。

    EINVAL nfds值超过RLIMIT_NOFILE值。

    ENOMEM没有空间分配文件描述符表。
#include "../common/color.h"
#include "../common/common.h"
#include "../common/tcp_server.h"
#include "../common/head.h"

#define POLLSIZE 100
#define BUFSIZE 512

char ch_char(char c) {
    if (c >= 'a' && c <= 'z')
        return c - 32;
    return  c;
}

int main(int argc, char **argv){
    if (argc != 2) {
        fprintf(stderr, "Usage: %s port!\n", argv[0]);
        exit(1);
    }
    int server_listen, fd;

    if ((server_listen = socket_create(atoi(argv[1]))) < 0) {
        perror("socket_create");
        exit(1);
    }
    
    struct pollfd event_set[POLLSIZE];
    
    for (int i = 0; i < POLLSIZE; i++) {
        event_set[i].fd = -1;
    }
    
    event_set[0].fd = server_listen;
    event_set[0].events = POLLIN;

    while(1) {
        int retval;
        if ((retval = poll(event_set, POLLSIZE, -1)) < 0) {
            perror("poll");
            return 1;
        }
        if (event_set[0].revents & POLLIN) {
            if ((fd = accept(server_listen, NULL, NULL)) < 0) {
                perror("accept");
                continue;
            }
            retval--;
            int i;
            for (i = 1; i < POLLSIZE; i++) {
                if (event_set[i].fd < 0) {
                    event_set[i].fd = fd;
                    event_set[i].events = POLLIN;
                    break;
                }
            }

            if (i == POLLSIZE) {
                printf("Too many clients!\n");
            }
        }
        
        for (int i = 0; i < POLLSIZE; i++) {
            if (event_set[i].fd < 0) continue;
            
            if (event_set[i].revents & (POLLIN | POLLHUP | POLLERR)) {
                retval--;
                char buff[BUFSIZE] = {0};
                if (recv(event_set[i].fd, buff, BUFSIZE, 0) > 0) {
                    printf("Recv : %s \n", buff);
                    for (int i = 0; i < strlen(buff); i++) {
                        buff[i] = ch_char(buff[i]);
                    }
                    send(event_set[i].fd, buff, strlen(buff), 0);
                }
                else {
                    close(event_set[i].fd);
                    event_set[i].fd = -1;
                }
            }

            if (retval <= 0) {
                break;
            }
        }
    }
    return 0;
}

epoll

头文件

#include <sys/epoll.h>

描述

​ epoll API执行与poll(2)类似的任务:监视多个文件描述符以看看是否可以在其中任何一个上进行I / O。 epoll API可以用作边缘触发或级别触发的界面,可以很好地扩展到大量受监视的文件描述符。 提供了以下系统调用来创建和管理epoll实例

​ epoll_create(2)创建一个epoll实例并返回一个引用的文件描述符

   通过epoll_ctl(2)注册对特定文件描述符的兴趣。

​ epoll_wait(2)等待I / O事件,如果没有事件发生则阻塞调用线程

区别

select:**

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

​ 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

​ 当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll:

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

总结:

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章