您的位置:首页 > 编程语言 > PHP开发

epoll与selectpoll的区别!!!

2016-07-29 12:36 274 查看
select,poll都可以来实现并发

(select限制)

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

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

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

poll:

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

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

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

大小来确定的)

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

cat  /proc/sys/fs/file-max




共同点(select,poll)

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

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

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

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

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

cat /proc/sys/kernel/osrelease
    


2,关于epoll的几个函数
#include <sys/epoll.h>

int  epoll_create(int size);       //创建一个epoll实例,成功时返回epoll文件描述符,失败时返回-1
//调用epoll_create函数时创建的文件描述符保存空间称为:“epoll例程”
//size并非用来决定epoll例程的大小,而仅供操作系统参考。Linux2.6.8之后将完全忽略传入epoll_create函数的size参
//数,因为内核会根据情况调整epoll例程的大小。。。

int  epoll_create1(int flags);

int  epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//成功返回0,失败返回-1,是可以将套接字/IO文件描述符添加到epoll来管理

int  epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//成功时返回发生事件的文件描述符数, 失败时返回-1。

//第二个参数所指缓冲需要动态分配
int  event_cnt;
struct epoll_event *ep_events;

ep_events = malloc(sizeof(struct epoll_event) *EPOLL_SIZE);    //EPOLL_SIZE是宏常量


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

我们的程序中用到了epoll_create1:

std::vector<int> clients;
int epollfd;
epollfd = epoll_create1(EPOLL_CLOEXEC);
那么我们为什么会用到epoll_create1呢???

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

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);
      第一个函数说明epoll所能支持的最大并发数,,,如上所说:也并不表示并发数,实际上,会创建epoll的一个实

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

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

第二个函数。

EPOLL_CLOEXEC
Set the close-on-exec (FD_CLOEXEC) flag on the new file descrip‐
tor.  See the description of the O_CLOEXEC flag in  open(2)  for
reasons why this may be useful.


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


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

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

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

//epfd:是我们刚才所创建的epoll实例
//op:操作方式,增加,删除,更改,如上程序,我们用的是增加
//fd :文件描述符,我们需要将文件描述符加入到epoll来进行管理
//event:对文件描述符感兴趣的事件

struct epoll_event {
uint32_t     events;      /* Epoll events   感兴趣的事件是可读事件,还是可写事件 */
epoll_data_t data;        /* User data variable    是一个数据,目的是使得epoll更加高效*/
};

typedef union epoll_data {   //就是说:这个数据类型是一个共用体,可以是下面的类型
void        *ptr;
int          fd;
uint32_t     u32;
uint64_t     u64;
} epoll_data_t;             //所以当前共用体的大小就是8个字节





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

typedef std::vector<struct epoll_event> EventList;


EventList events(16);

nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);

//其中events.begin()其实是一个迭代器,我们可以将其看成是一个指针,取*那么就是数组里的第一个元素,&就是元素
的首地址,而为什么我们如此复杂的将其如此转化,仅仅是因为,如果直接这样的话,会产生编译出错,迭代器不等于
指针
//<static_cast><int>C++中的类型强制转化。。。。


man epoll_wait:

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
//epfd:还是上文的实例句柄
//events:返回值,到底是那些事件产生了可读事件或者是感兴趣的事件
//maxevents:事件的最大容量
//timeout超时时间,类似于select和poll,传递-1时,一直等待直到发生事件,
//select和poll采用的是轮寻的机制,而epoll不是的

 int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
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,套接字改为非套接字方式的方法。

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

cmd: 表示函数调用的目的

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

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

int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);


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

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

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

      一定要采用非阻塞read&write函数
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: