• Select和epoll


    Select和epoll

    一、作用

    这三个其实是三个函数,这三个函数是用来干什么的呐,我们举个例子吧,假设你的电脑是个高性能服务器,服务就会接收和处理很多消息,一个服务器肯定不止一个链接,很定有很多链接,但是不是任何时候链接都有内容需要我么去接收,我们这时候就需要判断出有内容的链接,并处理他。

    我们自己来考虑的话,可能会想到多线程来处理,但是多线程来处理是不是会有问题呐,对,会带来相当多的上下文切换,上下文切换是很浪费性能的。

    文件句柄:通俗来说,当我们使用一个文件,会首先返回一个文件句柄,通过句柄来处理文件

    FD用于描述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

    二、Select

    sockfd = socket(AF_INET,sOCK_STREAM,0);
    memset(&addr,0,sizeof (addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(2000);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
    listen(sockfd,5);
    for(i=0; i<5;i++)
    {
    	memset(&client,0,sizeof (client));
    	addrlen = sizeof(client);
    	fds[i] = accept(sockfd,(struct sockaddr*)&client&addrlen);
    	if(fds[i] > max)
    		max = fds[i];
    }
    while(1){
    	FD_ZERO(&rset); //全部置位0
    	for (i = 0; i< 5; i++ ) {
    		FD_SET(fds[i],&rset); //将文件描述在rset让的位置置为1
    	}
    	
    	puts( " round again");
        // var1 被监听的文件描述符总数+1, var2 可读事件, var3 可写事件
        // var3 异常事件 ,var 4超时时间
    	select(max+1,&rset,NULL,NULL,NULL);
    	
    	for(i=0;i<5;i++) {
    		if (FD_ISSET(fds[i],&rset))  //测试某个位置是否被置位
    		{
                //处理数据逻辑
                memset(buffer,0,MAXBUF);
                read(fds[i],buffer,MAXBUF);
                puts(buffer);
    		}
    
    	}
    }
    
    

    while(1)上部分的代码是监听端口获取文件描述符(连接)。把这些文件描述符放到一个数组中,这些文件描述符是随机无序的,我们需要找出一个在里面找出一个最大的来。

    我们再来看一下select函数 ,我们关注读文件描述符,其他的可以不传,超时时间有默认值。

    读文件描述符 rset是什么呐,是一个bitmap 比如说 ,文件描述符为10,我们就把第10位置为1。bitmap默认1024位。

    上面对reset的解释在应用上符合逻辑也比较好理解,但是reset在内核中的真实样貌是:

    #undef __NFDBITS
    #define __NFDBITS   (8 * sizeof(unsigned long))  计算一个long类型有几个bit
    
    #undef __FD_SETSIZE
    #define __FD_SETSIZE    1024
    
    #undef __FDSET_LONGS
    #define __FDSET_LONGS   (__FD_SETSIZE/__NFDBITS)
    
    typedef struct {
        unsigned long fds_bits [__FDSET_LONGS];
    } __kernel_fd_set;
    

    reset里面创建了一个只能表示1024bit的long数组。1024是在代码里写死的,也可以改。

    我们的select函数运行过程中会直接把rset拷贝到内核态来运行判断的。

    ​ 没数据的时候:内核会一直遍历判断

    ​ 有数据的时候,会将我们rset对应的位置置位,select不再阻塞会返回。

    然后会执行for循环,判断哪个文件标识符(对应bitmap里面的位置,不是真正的文件描述符)被置位了,给读出来

    这个max+1是什么意思那,是卡一下最少遍历长度,提高效率。

    大小限制:

    可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

    缺点

    1、bitmap 默认1024,有上限

    2、bitmap会置位,不可重用,每次执行需要恢复bitmap初始化

    3、rset整体切换还是涉及到用户态内核态切换,交给内核态去修改。

    4、有数据返回的时候还是会O(n)的复杂度去遍历

    三、POll

    // 我们发现poll的变化 是用结构体存储数据了,而不是使用bitmap了
    struct pollfd {
        int fd;  //文件描述符
        short events; //注册的事件
        short revents; //实际发生的事件,由内核填充
    }
    
    
    for (i=0; i<5;i++)
    {
        memset(&client,0,sizeof (client));
        addrlen = sizeof(client);
        pollfds[i].fd = 
            accept(sockfd,(struct sockaddr*)&client,&addrlen);
        pollfds[i].events = POLLIN;  //表示我们要监听读就绪事件
    }
    
    sleep(1);
    
    while(1){
       puts("round again");
       // pollfds数组第零个元素的指针,pollfds结构体数组大小,超时事件单位ms
       // 返回值小于0,表示出错,等于0,表示poll函数等待超时,大于0,表示poll由于监听的文件描述符就绪返回,返回就绪的个数。
       poll(pollfds ,5,50000);
       for(i=0; i<5;i++) {
           if (pollfds[i].revents & POLLIN){
               //处理逻辑
               pollfds[i].revents = 0; 
               memset(buffer,0,MAXBUF);
               read(pollfds[i].fd,buffer,MAXBUF);
               puts(buffer);
           }
       }
    }
    
    
    

    上面是准备文件描述符,下边是操作,这次poll不是使用的bitmap了,而是做了一个结构体。

    也是把这个结构体拷贝到内核态去操作数据

    有事件的话会置位revents这个字段为发生的事件。

    poll函数返回

    找到变化之后,会首先初始化revents再读数据。

    改进:

    他是用数组来管理的没有最大连接数的限制。

    缺点:

    1. poll采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;

    2. 在轮询期间,需要复制大量的句柄数据结构到内核空间,产生巨大的开销;

    3. 还是需要遍历整个数组才能发现哪些句柄发生了事件

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

    select和poll的区别

    1、select采用bitmap表示文件描述符,bitmap不可重用每次需要重新置位。poll函数传入pollfd结构体数组,内部将pollfd结构体数组转换为pollfd链表进行操作。

    2、select需要将rset拷贝到内核态,poll需要复制大量的句柄数据结构到内核空间。

    3、select大小受限于最大文件描述符,poll不受限制。

    4、调用返回之后,两者都需要轮询判断

    5、poll是水平触发

    四、epoll

    struct epoll_event events[5];
    int epfd = epoll_create(10);
    
    for (i=0;i<5;i+t){
       static struct epoll_event ev; 
       memset(&client,0,sizeof (client);
       addrlen = sigeof(client);
       ev.data.fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
       ev.events = EPOLLIN;
       epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
              
    }
     while(1){
         puts( "round again");
         nfds = epoll_wait(epfd,events,5,10000);
         for(i=0; i<nfds ;i++) {
             memset(buffer,o,MAXBUF);
             read(events[i].data.fd,buffer,MAXBUF);
             puts(buffer);
         }
     }
    

    第一步:使用epoll_create(10) ,我们会创建一个eventpoll的结构体,这个参数是需要监听的文件描述符大致的个数,意义不大。

    struct eventpoll{
        ....
        /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
        struct rb_root  rbr;
        /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
        struct list_head rdlist; 
        .... 
    };
    

    首先明确一点epfd在epoll中是内核态和用户态共享的。比poll还要复杂,复杂带来的好处就是方便了我们快速定位有事件的文件描述符。

    第二步:使用epoll_ctl方法在eventpoll结构上添加数据。

    // var1 我们的eventpoll结构, var2 我们要执行的操作, var3 文件描述符, var4 与pollfd类似的结构体
    epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
    

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些新添加进来的事件会被封装为下面的结构体

    struct epitem{
        struct rb_node  rbn;//红黑树节点
        struct list_head    rdllink;//双向链表节点 
        struct epoll_filefd ffd; //事件句柄信息 
        struct eventpoll *ep; //指向其所属的eventpoll对象 
        struct epoll_event event; //期待发生的事件类型 
    }
    

    这些事件结构体会挂载到红黑树上,重复事件依据红黑树的特性很快就会识别出来。所有添加在epoll中的事件都会与设备驱动程序建立回调关系,就是,当相应的文件描述符发生事件是,会调用这个回调方法,将我们的事件添加到eventpoll中的rdlist双链表中,这个地方就是为什么epoll不需要去遍历的原因了。

    第三步:我们来看一下epoll_wait。

    // var1 我们的eventpoll结构,var2 事件,var3文件描述符数量,var4超时事件
    epoll_wait(epfd,events,5,10000);
    

    成功时:返回为请求的I / O准备就绪的文件描述符的数目;

    超时时:返回零。

    错误时:返回-1。

    epoll支持百万级别句柄的监听

    小结

    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

    极其高效的原因:

    这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

    这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket封装为一个结构放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄(这个socket文件描述符)的中断到了(有事件了),就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。(注:好好理解这句话!)

    执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

    最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

  • 相关阅读:
    gotour源码阅读
    CPU知识
    GCC知识
    go/src/make.bash阅读
    Go的pprof使用
    CGI的一些知识点
    STM32——C语言数据类型
    css 学习资料
    项目管理实践教程
    js 格式验证总结
  • 原文地址:https://www.cnblogs.com/kenai/p/14267557.html
Copyright © 2020-2023  润新知