• TCP IP 网络编程(七) 理解select和epoll的使用


    理解select函数

    select函数的功能和调用顺序

    使用select函数可以将多个文件描述符集中到一起统一监视

    • 是否存在套接字接收数据
    • 无需阻塞传输数据的套接字有哪些
    • 哪些套接字发生了异常

    select函数的调用方法和顺序

    • 设置文件描述符
    • 指定监视范围
    • 设置超时

    ​ ↓

    • 调用select函数

      ​ ↓

    • 查看调用结果

    设置文件描述符

    利用select函数可以同时监视多个文件描述符,监视文件门描述符也可以视为监视套接字,首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分

    • FD_ZERO(fd_set * fdset) 将fd_set变量都初始化为0
    • FD_SET(int fd,fd_set *fdset) 在参数fdset指向的变量注册文件描述符fd的信息
    • FD_CLR(int fd, fd_set * fdset) 从参数fdset指向的变量中清楚文件描述符fd的信息
    • FD_ISSET(int fd , fd_set * fdset)若参数fdset指向的变量中包含文件描述符fd的信息,则返回 真

    int main(void)
    {
        fd_set set;
        
        FD_ZERO(&set);			0 0 0 0 ....
        
        FD_SET(1,&set);			0 1 0 0 ....
        
        FD_SET(2,&set);			0 1 1 0 ....
        
        FD_CLR(2,&set);			0 1 0 0 ....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    设置监视范围及超时

    #include 
    #include 
    
    int select(int maxfd, fd_set * readset,fd_set *writeset,fd_set exceptset,const struct timeval * timeout);
    
    	成功返回大于 0 的值,失败返回 - 1
        
        maxfd		监视文件描述符的数量
        readset		将所有关注是否存在待读取数据的文件描述符注册到fd_set型变量,并传递到其地址值
       	writeset 	将所有关注是否可传无阻塞数据的文件描述符注册到fd_set型变量,并传递到其地址值
        exceptset   将所有关注是否发生异常的文件描述符注册到fd_set型变量,并传递其地址值
        timeout 	调用select函数后,为防止陷入无限阻塞的状态,传递超时time - out消息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    文件描述符的监视范围与select函数的第一个参数有关,select要求通过第一个参数传递监视对象文件描述符的数量

    select函数的超时时间与select函数的最后一个参数有关,其中timeval结构体定义为:

    struct timeval
    {
        long tv_sec;    	//秒
        long tv_usec;       //微秒
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    select函数只有在监视的文件描述符发生变化时才返回,如果未发生变化,就会进到阻塞状态。指定超时时间就是为了这种情况的发生,通过上述结构体变量,将秒数填入tv_sec成员,微秒数填入tv_usec成员,将结构体的地址值传递到select函数的最后一个参数,不想设置超时时间,直接传递NULL。

    select函数调用示例

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define BUF_SIZE 100
    void error_handling(char *buf);
    
    int main(int argc, char *argv[])
    {
    	int serv_sock, clnt_sock;
    	struct sockaddr_in serv_adr, clnt_adr;
    	struct timeval timeout;
    	fd_set reads, cpy_reads;
    
    	socklen_t adr_sz;
    	int fd_max, str_len, fd_num, i;
    	char buf[BUF_SIZE];
    	if(argc!=2) {
    		printf("Usage : %s \n", argv[0]);
    		exit(1);
    	}
    
    	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    	memset(&serv_adr, 0, sizeof(serv_adr));
    	serv_adr.sin_family=AF_INET;
    	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    	serv_adr.sin_port=htons(atoi(argv[1]));
    	
    	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
    		error_handling("bind() error");
    	if(listen(serv_sock, 5)==-1)
    		error_handling("listen() error");
    
    	FD_ZERO(&reads);
    	FD_SET(serv_sock, &reads);
    	fd_max=serv_sock;
    
    	while(1)
    	{
    		cpy_reads=reads;
    		timeout.tv_sec=5;
    		timeout.tv_usec=5000;
    
    		if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
    			break;
    		
    		if(fd_num==0)
    			continue;
    
    		for(i=0; i
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    优于select的epoll

    epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。

    epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。

    基于select的I/O复用速度慢

    • 调用select函数后常见的针对所有文件描述符的循环语句
    • 每次调用select时都需要向该函数传递监视对象信息

    相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。

    因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。

    实现epoll时必要的函数和结构体

    • epoll_create: 创建保存epoll文件描述符的空间
    • epoll_ctl: 向空间注册并注销文件描述符
    • epoll_wait: 等待文件描述符发生变化

    为添加和删除监视对象文件描述符,select方式中需要FD_SET、FD_CLR函数,但是在epoll中都是通过epoll_ctl函数请求操作系统完成

    select方式中调用select等待文件描述符的变化,而epoll调用epoll_wait函数。

    select方式中通过fd_set变量查看监视对象的状态变化,而epoll_wait方式通过结构体epoll_event将发生变化的文件描述符集中一起

        struct epoll_event {
          __uint32_t events; 
         epoll_data_t data; 
    	};
    
    
         typedef union epoll_data {
          void *ptr;
          int fd;
          __uint32_t u32;
          __uint64_t u64;
      } epoll_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    • EPOLLOUT:表示对应的文件描述符可以写;
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    • EPOLLERR:表示对应的文件描述符发生错误;
    • EPOLLHUP:表示对应的文件描述符被挂断;
    • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还监听这个socket的话,再次把这个socket加入到EPOLL队列

    epoll_create

    #include 
    
    int epoll_create(int size);
    
    	成功返回epoll文件描述符,失败返回 - 1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    创建一个epoll的描述符,size用来告诉内核这个监听数目一共多大,此参数不同于select()中的第一个参数,给出最大监听的fd+1的值

    当创建好epoll描述符后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

    epoll_ctl

    #include 
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
    	
    	成功返回0,失败时返回-1
            epfd 	用于注册监视对象的epoll例程的文件描述符
            op		用于指定监视对象的添加、删除、更改操作 
            			↓
            	EPOLL_CTL_ADD:注册新的fd到epfd中;
    			EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    			EPOLL_CTL_DEL:从epfd中删除一个fd;
            fd		需要注册的监视对象文件描述符
            event 	监视对象的事件类型
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    epoll_wait

    #include 
    
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    	成功返回时间的文件描述符,失败返回-1
            epfd		时间发生监视范围的epoll例程的文件描述符
            events		保存时间的文件描述符集合的结构体地址值 (缓冲需要动态分配)
            maxevents	第二个参数可以保存的最大事件数
            timeout		以毫秒为单位,传递-1,一直等待发送事件。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    基于epoll的服务器端

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define BUF_SIZE 100
    #define EPOLL_SIZE 50
    void error_handling(char *buf);
    
    int main(int argc, char *argv[])
    {
    	int serv_sock, clnt_sock;
    	struct sockaddr_in serv_adr, clnt_adr;
    	socklen_t adr_sz;
    	int str_len, i;
    	char buf[BUF_SIZE];
    
    	struct epoll_event *ep_events;
    	struct epoll_event event;
    	int epfd, event_cnt;
    
    	if(argc!=2) {
    		printf("Usage : %s \n", argv[0]);
    		exit(1);
    	}
    
    	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    	memset(&serv_adr, 0, sizeof(serv_adr));
    	serv_adr.sin_family=AF_INET;
    	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    	serv_adr.sin_port=htons(atoi(argv[1]));
    	
    	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
    		error_handling("bind() error");
    	if(listen(serv_sock, 5)==-1)
    		error_handling("listen() error");
    
    	epfd=epoll_create(EPOLL_SIZE);
    	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    
    	event.events=EPOLLIN;
    	event.data.fd=serv_sock;	
    	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
    	while(1)
    	{
    		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    		if(event_cnt==-1)
    		{
    			puts("epoll_wait() error");
    			break;
    		}
    
    		for(i=0; i
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    边缘触发和水平触发

    epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和 水平触发(level-triggered,LT)

    这两个术语还挺抽象的,其实它们的区别还是很好理解的。

    • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
    • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

    举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

    这就是两者的区别,水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

    如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。

    如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 readwrite)返回错误,错误类型为 EAGAINEWOULDBLOCK

    一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

    select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。

    参考资料:

    https://xiaolincoding.com/


    更多资料尽在 GitHub 欢迎各位读者去Star

    ⭐学术交流群Q 754410389 持续更新中~~~

  • 相关阅读:
    使用vue-pdf预览pdf文件
    [附源码]计算机毕业设计基于springboot血库管理系统
    Response设置响应数据,重定向,目录问题,字节流,字符流
    2、ARM处理器概论
    管道和重定向分号-连接符
    300美元就能“盗走”一辆车,这些车主要小心了
    92.(cesium之家)cesium楼栋分层
    [apue] linux 文件系统那些事儿
    codeforces每日5题(均1500)-第二十四天
    SSL加密
  • 原文地址:https://blog.csdn.net/m0_63743577/article/details/134224645