elect、poll和epoll的区别
在linux没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在大数据、高并发、集群等一些名词唱的火热之年代,select和poll的用武之地越来越有限了,风头已经被epoll占尽。
select()和poll() IO多路复用模型
select的缺点:
单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销
select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO,那么之后再次select调用还是会将这些文件描述符通知进程。
相比于select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。
拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在_FD_SETSIZE为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。
epoll IO多路复用模型实现机制
由于epoll的实现机制与select/poll机制完全不同,上面所说的select的缺点在epoll上不复存在。
设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll的设计和实现select完全不同。epoll通过在linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接
如此一来,要实现上面说的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
上面的3个部分非常清晰,首先要调用epoll_create创建一个epoll对象。然后使用epoll_ctl可以操作上面建立的epoll对象,例如,将刚建立的socket加入到epoll中让其监控,或者把epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!
那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。
这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。
其中涉及到的数据结构:
epoll用kmem_cache_create(slab分配器)分配内存用来存放structepitem和structeppoll_entry。
当向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据结构:
structepitem{
structrb_noderbn;//用于主结构管理的红黑树
structlist_headrdllink;//事件就绪队列
structepitem*next;//用于主结构体中的链表
structepoll_filefdffd;//这个结构体对应的被监听的文件描述符信息
intnwait;//poll操作中事件的个数
structlist_headpwqlist;//双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
structeventpoll*ep;//该项属于哪个主结构体(多个epitm从属于一个eventpoll)
structlist_headfllink;//双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点
structepoll_eventevent;//注册的感兴趣的事件,也就是用户空间的epoll_event
}
而每个epoll fd(epfd)对应的主要数据结构为:
structeventpoll {
spin_lock_tlock;//对本数据结构的访问
structmutex mtx;//防止使用时被删除
wait_queue_head_t wq;//sys_epoll_wait()使用的等待队列
wait_queue_head_tpoll_wait; //file->poll()使用的等待队列
structlist_head rdllist;//事件满足条件的链表 /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
structrb_rootrbr;//用于管理所有fd的红黑树(树根)/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
structepitem*ovflist;//将事件到达的fd进行链接起来发送至用户空间
}
structeventpoll在epoll_create时创建。
这样说来,内核中维护了一棵红黑树,大致的结构如下:
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
jQuery官网:https://jquery.com/
jQuery是一个高效、轻量并且功能丰富的js库。
核心在于查询query。
jQuery是一个优秀的js函数库,是React/Vue/Angular框架之外中大型项目的首选。
jQuery的主旨是write less, do more。
引入jQuery的方式有2种,一种是项目中直接引入jQuery的min.js文件,一种是使用服务器端jQuery文件(使用cdn)脚本标签方式引入。
在官网的:https://jquery.com/download/ 链接下可以下载到完整的代码,放到项目文件的js文件夹下。
<script src="static/js/jquery-3.7.1.min.js"></script>在网站:https://www.bootcdn.cn/ 可以获得稳定、快速、免费的cdn加速服务。
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js"></script>开发过程中一般使用非min.js文件方便调试,生产环境部署上线时才使用min.js这种压缩文件。
从源码中可以看出,jQuery的整体逻辑可以用以下简单的结构进行描述:
( function( global, factory ) {
// 判断有无window环境的一堆逻辑代码
})( typeof window !=="undefined" ? window : this, function( window, noGlobal ) {
// 构造jQuery的一些逻辑代码
return jQuery
});从源码中可以看出,jQuery被定义为一个函数,函数中返回了一个实例对象(看new关键字)。
继续跟踪源码 new jQuery.fn.init( selector, context),这个函数中调用了makeArray,当然在其他if判断语句中也有返回伪数组对象(比如,定义了length字段,还有[0]的操作),这里拿makeArray作为演示。
查看makeArray函数:
所以这个返回实例对象,是一个伪数组。
$('#menu-trigger') instanceof Array // false
$('#menu-trigger') instanceof Object // true从源码中可以看出,将jQuery函数和window.$ 以及window.jQuery绑定赋值,所以使用jQuery和$ 标识符就可以直接使用jQuery。通常在项目中直接使用$标识符,快捷简省。
所以在引入jQuery的项目中:
console.log(typeof $); // function
console.log($===jQuery); // true
console.log($() instanceof Object); // true通常形式为:$(param)
$(function() {
console.log("dom finished and execute this");
})
$('#btn').click(function () {
// 这里的this是id为#btn的dom元素
console.log(this.innerHTML)
console.log($(this).html())
})
$('<input type="number"></input>').appendTo('div')let list=[1, 2, 3]
$.each(list, function(i, ele) {
console.log(i, ele)
})
$.trim(' hello world ')// class中名为btn的dom元素有多少
$('.btn').length
$('.btn')[0]
$('.btn').get(0)
$('.btn').index()
// 设置名为btn的class对应的dom标签的文本内容
$('.btn').text('自定义文本内容')通过$(param)传入的是selector、element、标签情况下,返回的是包含1个或者多个dom元素对象的伪数组。
// 基础标签和class
// 选择了所有的div和span标签
$('div, span')
// 选择所有具有某个class的标签
$('div.container')
// 层次选择器
$('ul span') // ul标签下的所有span元素
$('ul>span') // ul标签下的所有子span元素
$('.container+li') // class为container的元素后的下一个li元素
$('ul .item~*') // class为item的元素后面所有兄弟元素
// 过滤选择器
$('div:first') // 选择第一个div
$('div:last') // 最后一个div
$('div:not(.container)') // class不为container的所有div
$('div:lt(3):gt(0)') // 所有div元素中的大于0小于3的div元素,表示1和2索引处的dom元素
$('div:containers("hello world")') // 内容为hellow world的div元素
$('div:hidden') // style中display: none的div元素
$('div[data]') // 有data属性的div元素, example: <div data=""></div>
$('div[data="123"]') // 有data属性且值为123的div元素, example: <div data="123"></div>
// 示例,使table表格的奇数行背景样式设置
$('table>tbody>tr:odd')
// form表单中
$(':text') // 所有单行输入框
$(':text:disabled') // 所有disabled的input输入框
$(':checkbox') // 所有checkbox
$(':checkbox:checked') // 所有选中的checkbox
$('select').val() // select标签选中的option的value值直接修改css属性(如果其dom标签存在这个css属性)
$('#container').css('background', 'red');
$('#container').css({ 'background' : 'red', 'color': 'blue' }) // 一组属性清空某标签下的所有dom:
$('.carousel-inner').empty();给某标签下添加dom标签:
$('.carousel-inner').append(domStr);移除、添加class:
$('.carousel-indicators li').removeClass('active');
$('.carousel-indicators li:first').addClass('active');获取dom标签上的属性:
$('.about-img-1>img').attr('src');设置标签的属性:
$('.about-img-1>img').attr('src', (data && data['image']) ? data['image'] : '');点击:
$('.category-product-page-ul>li').click(function(e) {
e.preventDefault();
console.log('this is:', this); // 打印对应的dom标签
});hover:
$('#container').hover(
function() {
// 当鼠标进入元素时执行的函数
},
function() {
// 当鼠标离开元素时执行的函数
}
);监听事件:
$('.bigImage').on("mousemove", function( e ) {
// do something
});const json='/static/js/data/xxx.json';
$.ajax({
url: json,
dataType: 'json',
success: function(data) {
// do something
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Fail to read json:', textStatus, errorThrown, json);
}
});post请求:
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
相信大家都了解了Unix五种IO模型,不了解的可以=> 查看这里
其中前面4种IO都可以归类为synchronous IO - 同步IO,而select、poll、epoll本质上也都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
在介绍select、poll、epoll之前,首先介绍一下Linux操作系统中基础的概念:
我们先分析一下select函数
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);【参数说明】int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1。fd_set *readset , fd_set *writeset , fd_set *exceptsetfd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。const struct timeval *timeout timeout告知内核等待所指定文件描述符集合中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
【返回值】int 若有就绪描述符返回其数目,若超时则为0,若出错则为-1
select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。
下面是pll的函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024
【参数说明】struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
nfds_t nfds 记录数组fds中描述符的总数量
【返回值】int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
Linux中提供的epoll相关函数如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。比如:0->1 就是Edge,1->1 就是Level。
ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
一张图总结一下select,poll,epoll的区别:
select | poll | epoll | |
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
*请认真填写需求信息,我们会在24小时内与您取得联系。