• 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模式仅在第一次返回。

  • 相关阅读:
    Codeforces 1265A Beautiful String
    1039 Course List for Student (25)
    1038 Recover the Smallest Number (30)
    1037 Magic Coupon (25)
    1024 Palindromic Number (25)
    1051 Pop Sequence (25)
    1019 General Palindromic Number (20)
    1031 Hello World for U (20)
    1012 The Best Rank (25)
    1011 World Cup Betting (20)
  • 原文地址:https://www.cnblogs.com/kenai/p/14267557.html
Copyright © 2020-2023  润新知