epoll与select,poll的区别

select,poll都可以来实现并发

(select限制)

1,一个进程所能打开的最大文件描述符的个数是有限的

2,select中集合的限制(fd_set)FD_SETSIZE

3,select每一次跟客户端连接的过程就会陷入内核,    并且是以轮寻的方式查找的

poll:

只有最大文件描述符的个数限制,而没有FD_SETSIZE限制


而我们所能打开的最大文件描述符的个数,我们可以通过一个命令来修改:ulimit -n number

但是这个number不能无限大,它也是有限制的:(系统所能打开的最大文件描述符的个数的限制,而这个限制是跟内存的

大小来确定的)

这个数:我们可以通过查看一个文件来看到命令:

[cpp]  view plain  copy
  1. cat  /proc/sys/fs/file-max  


共同点(select,poll)

内核要遍历所有的文件描述符,直到找到发生事件的文件描述符。

所以当我们的并发数大大增加的时候,内核所要遍历的也就越多,这样效率就会降低


为了解决这个问题,我们引入了epoll

1,epoll是从Linux的2.5.44版内核(操作系统的核心模块)开始引入的,所以使用epoll前面需要验证Linux内核版本

     当然,我们可以通过命令来查看:    

[cpp]  view plain  copy
  1. cat /proc/sys/kernel/osrelease  
    
2,关于epoll的几个函数
[cpp]  view plain  copy
  1. #include <sys/epoll.h>  
  2.   
  3. int  epoll_create(int size);       //创建一个epoll实例,成功时返回epoll文件描述符,失败时返回-1  
  4. //调用epoll_create函数时创建的文件描述符保存空间称为:“epoll例程”  
  5. //size并非用来决定epoll例程的大小,而仅供操作系统参考。Linux2.6.8之后将完全忽略传入epoll_create函数的size参  
  6. //数,因为内核会根据情况调整epoll例程的大小。。。  
  7.   
  8. int  epoll_create1(int flags);  
  9.   
  10. int  epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
  11. //成功返回0,失败返回-1,是可以将套接字/IO文件描述符添加到epoll来管理  
  12.   
  13. int  epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);  
  14. //成功时返回发生事件的文件描述符数, 失败时返回-1。  
  15.   
  16. //第二个参数所指缓冲需要动态分配  
  17. int  event_cnt;  
  18. struct epoll_event *ep_events;  
  19.   
  20. ep_events = malloc(sizeof(struct epoll_event) *EPOLL_SIZE);    //EPOLL_SIZE是宏常量  


关于epoll_create,我们再来看看,,,,:

我们的程序中用到了epoll_create1:

[cpp]  view plain  copy
  1. std::vector<int> clients;  
  2. int epollfd;  
  3. epollfd = epoll_create1(EPOLL_CLOEXEC);  
那么我们为什么会用到epoll_create1呢???

1,我们man一下这个函数:

[cpp]  view plain  copy
  1. #include <sys/epoll.h>  
  2.   
  3. int epoll_create(int size);  
  4. int epoll_create1(int flags);  
      第一个函数说明epoll所能支持的最大并发数,,,如上所说:也并不表示并发数,实际上,会创建epoll的一个实

      例,内部会创建一个哈希表(而这里的size仅仅只是代表哈希表的容量)。而到了现在,我们已经不使用哈希表

了,我们使用红黑树来实现,那么这个时候就不需要给定容量了,所以说,对于Linux内核版本比较高的话,引入了

第二个函数。

[cpp]  view plain  copy
  1. EPOLL_CLOEXEC  
  2.               Set the close-on-exec (FD_CLOEXEC) flag on the new file descrip‐  
  3.               tor.  See the description of the O_CLOEXEC flag in  open(2)  for  
  4.               reasons why this may be useful.  


[cpp]  view plain  copy
  1. 表示epoll_create1(int flags)当参数是EPOLL_CLOEXEC时,(在系统编程方面,类似与O_CLOEXEC),表示进程  
  2. 被替换的时候,文件描述符会被关闭。。。。。  


2,那么我们接下来要将感兴趣的文件描述符加入epoll来进行管理。(epoll_ctl)

[cpp]  view plain  copy
  1.  struct epoll_event event;         //定义一个事件  
  2.  event.data.fd = listenfd;          //感兴趣的文件描述符是监听套接口  
  3.  event.events = EPOLLIN | EPOLLET;//是不是有数据到啦,//边缘的方式触发  
  4. //如果没有EPOLLET,那么默认的方式是电平触发  
  5.  epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);  
     接下来:man epoll_ctl

[cpp]  view plain  copy
  1.        #include <sys/epoll.h>  
  2.   
  3.        int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
  4.   
  5. //epfd:是我们刚才所创建的epoll实例  
  6. //op:操作方式,增加,删除,更改,如上程序,我们用的是增加  
  7. //fd :文件描述符,我们需要将文件描述符加入到epoll来进行管理  
  8. //event:对文件描述符感兴趣的事件  
  9.   
  10. struct epoll_event {  
  11.                uint32_t     events;      /* Epoll events   感兴趣的事件是可读事件,还是可写事件 */  
  12.                epoll_data_t data;        /* User data variable    是一个数据,目的是使得epoll更加高效*/  
  13.            };  
  14.   
  15.   
  16.  typedef union epoll_data {   //就是说:这个数据类型是一个共用体,可以是下面的类型  
  17.                void        *ptr;  
  18.                int          fd;  
  19.                uint32_t     u32;  
  20.                uint64_t     u64;  
  21.            } epoll_data_t;             //所以当前共用体的大小就是8个字节  

 
 
 
 

3,接下来,我们就需要去检测所返回的事件,那些I/O产生了事件:epoll_wait

[cpp]  view plain  copy
  1. typedef std::vector<struct epoll_event> EventList;  


[cpp]  view plain  copy
  1. EventList events(16);  
  2.   
  3.   
  4. nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);    
  5.   
  6. //其中events.begin()其实是一个迭代器,我们可以将其看成是一个指针,取*那么就是数组里的第一个元素,&就是元素  
  7. 的首地址,而为什么我们如此复杂的将其如此转化,仅仅是因为,如果直接这样的话,会产生编译出错,迭代器不等于  
  8. 指针  
  9. //<static_cast><int>C++中的类型强制转化。。。。  

man epoll_wait:

[cpp]  view plain  copy
  1.        #include <sys/epoll.h>  
  2.   
  3.        int epoll_wait(int epfd, struct epoll_event *events,  
  4.                       int maxevents, int timeout);  
  5. //epfd:还是上文的实例句柄  
  6. //events:返回值,到底是那些事件产生了可读事件或者是感兴趣的事件  
  7. //maxevents:事件的最大容量  
  8. //timeout超时时间,类似于select和poll,传递-1时,一直等待直到发生事件,  
  9. //select和poll采用的是轮寻的机制,而epoll不是的  
  10.   
  11.  int epoll_pwait(int epfd, struct epoll_event *events,  
  12.                       int maxevents, int timeout,  
  13.                       const sigset_t *sigmask);  


对于文件描述符而言:

0,1,2已经被占用,3是监听的,4是epoll的句柄


并且我们可以通过运行结果发现:epoll的运行效率最高


epoll和select,poll

1,epoll最大的好处是:相比于select和poll,不会随着监听fd的数目增长而降低效率

2,内核中select和poll是通过轮寻的方式来进行处理的,而轮寻的fd越多,自然耗时越多

3,epoll的实现是基于回调的,如果fd有期望的事件发生,那么就通过回调函数将其加入epoll的就绪队列中。也就是说:

     epoll只关心活跃的fd,与fd的数目并没有关系。

4,如何让内核把fd消息通知给用户空间呢?????

     select和poll采取的是:内存拷贝方法,将文件描述符和消息拷贝过去(内核)

     epoll采取的是:共享内存的方法,不需要进行拷贝,效率高

5,epoll不仅会告诉应用程序有i/o事件的到来,还会告诉应用程序相关的信息,而这些信息是由应用程序来填充的,所以应用程序能直接定位到事件,而不用再次的遍历i/o



接下来就是epoll的两种模式:

Level Trigger:条件触发(电平触发)                  边缘触发:(Edge Trigger)

EPOLLLT                                                              EPOLLET


理论上:边缘触发的效率高


条件触发:方式中,只要输入缓冲中有数据就会一直通知该事件。

如:服务端输入缓冲收到50字节的数据时,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但

是服务器端读取20字节的数据后还剩30字节的情况下,仍然会注册事件。也就是说:调节触发方式中,只要输入缓

冲中还剩有数据,就将以事件方式再次注册。


边缘触发:输入缓冲收到数据时仅注册1次该事件。即使输入缓冲中留有数据,也不会再进行注册。




电平触发:完全靠内核中的kernel epoll驱动,应用程序通过epoll_wait返回的fds,也就是处于就绪状态的文件描述、

                    符。因为内核中检测到文件描述符产生事件的时候,就将这些事件添加到啦就绪队列。那么对于这些就

                    就绪的套接字或I/O,我们的应用程序就可以处理这些就绪的文件描述符。

而对于ET模式:模式下,系统仅仅通知应用程序那些fds变成了就绪状态,一旦fds变成了就绪状态,epoll将不再关

                    注fd的任何状态信息,从epoll队列中移除,直到应用程序通过读写操作触发EAGIN状态,epoll认为这个

                    fd又变成了空闲状态,那么epoll又重新关注这个fd的状态,重新加入epoll队列。

                    也就是说:关注的都是从空闲状态到就绪状态的文件描述符。

                    

                    总的来说,随着epoll_wait的返回,队列中的fds是在减少的,所以在大并发的系统中,ET更有优势。

但是,也不见得ET就一定比LT更具有优势,因为,加入2k的数据,而我们一次的读写并没有读完数据,只读写1k'数

据,还剩余1k的数据,可是这时ET模式就该移除当前的套接字了,那么里面还有我们的一部分数据啊,那么就需要

我们的应用层来维护部分数据,如果我们维护不得当的话,那么也会大大的影响我们的效率,



我们也可以将2k的数据一次读完,或者说我们触发EGAIN,表示数据全部都读完了,当然前提是:我们必须把这个

套接字设置为非阻塞模式,然后epoll认为这个fd又变成了空闲状态,那么epoll又重新关注这个状态(加入队列),

那么我们调用epoll_wait的话,如果又新的事件到来,那么它将不会阻塞。。。。。。。

所以说:这个ET模式挺麻烦的,如果数据没有处理完全,就调用epoll_wait,那么这个时候就会一直阻塞,



所以,我们也就知道了,select模型是以调节触发的方式工作的,输入缓冲中如果还有数据,肯定会注册事件(再次

         调用)


简单总结:

1,Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生

      错误的原因。因此,为了在发生错误时提供额外的信息,Linux下声明了如下全局变量:

      int errno;

      为了访问该变量,需要引入error.h头文件。另外,每种函数发生错误时,保存到errno变量中的值都不同

      read函数发现输入缓冲中没有数据可读时返回-1,同时在errno中保存EAGAIN常量。


2,套接字改为非套接字方式的方法。

[cpp]  view plain  copy
  1. #include <fcntl.h>  
  2. int  fcntl(int filedes, int cmd, .....);  
  3. //成功时返回cmd参数相关值,失败时返回-1  
fileds:属性更改目标的文件描述符

cmd: 表示函数调用的目的

从上述声明中,fcntl具有可变参数。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性

(int型),反之,如果传递F_SETFL,可以更改文件描述符属性,若希望将文件(套接字)改为非阻塞模式。

[cpp]  view plain  copy
  1. int flag = fcntl(fd, F_GETFL, 0);  
  2. fcntl(fd, F_SETFL, flag | O_NONBLOCK);  

第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。

调用read&write函数时,无论是不是存在数据,都会形成非阻塞文件(套接字)


3,边缘触发方式下:以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿。因此,边缘触发方式中

      一定要采用非阻塞read&write函数

猜你喜欢

转载自blog.csdn.net/qq_22313585/article/details/79228080