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);
1,我们man一下这个函数:
- #include <sys/epoll.h>
- int epoll_create(int size);
- int epoll_create1(int flags);
例,内部会创建一个哈希表(而这里的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);
- #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
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函数