select,poll,epoll深入理解!

写在前面

select,poll,epoll总结

主要内容

io多路复用模型 IO multiplexing

目的:因为阻塞模型在没有收到数据的时候就会阻塞卡住,如果一次需要接受多个socket fd的时候,就会导致必须处理完前面的fd,才能处理后面的fd,即使可能后面的fd比前面的fd还要先准备好,所以这样就会造成客户端的严重延迟。
为了处理多个请求,我们自然先想到用多线程来处理多个socketfd,但是这样又会启动大量的线程,造成资源的浪费,所以这个时候就出现了io多路复用技术。就是用一个进程来处理多个fd的请求

个人一点理解:当用户数量很大的时候不断的开启新的线程去服务不同的用户不再是一件令人愉悦的事情了,因为此时不断的创建线程和维护线程以及线程间切换的开销不再可以忽略不计,这些开销将是阻碍服务器性能更大一步提升的瓶颈。此时IO多路复用技术就可以在没有创建任何线程的情况下仅凭借单进程就能够很好的处理大量用户的情况。

select,poll,epoll 就是Linux上IO多路复用的三种方式

细谈 io 多路复用技术 select 和poll

select

单个进程就可以同时处理多个网络连接的io请求(同时阻塞多个io操作)。基本原理就是程序呼叫select,然后整个程序就阻塞了,这时候,kernel就会轮询检查所有select负责的fd,当找到一个client中的数据准备好了,select就会返回,这个时候程序就会系统调用,将数据从kernel复制到进程缓冲区。

  • 缺点

因为函数的返回只能告诉调用者有描述符fd准备好了但是具体是哪个描述符好了呢?还得用户自己去检查每一个监听的描述符看哪一个好,这样需要线性的时间。
同时每次调用select函数都需要将监听的描述符集合传入函数(将用户态的描述符信息拷贝到内核态)需要线性的时间。
内核需要轮询每个描述符需要线性的时间。
单个进程可以监听的描述符的个数有限 通常是1024

当监听的描述符非常大的时候描述符拷贝和寻找那个描述符准备好的开销将很大,随着用户的数量增大效率将显著下降。
我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。进程间上下文切换的时间消耗非常的大。

poll

扫描二维码关注公众号,回复: 3037220 查看本文章

描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制。
(也就是采用链表的结构维护监听的描述符集合所以没有长度限制只要内存够放的下就还可以放)。
poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。
其他的缺点和select一样。

对于select和poll的上述缺点,就引进了一种新的技术,epoll技术

eopll

对于select和poll的上述缺点,就引进了一种新的技术,epoll技术

int epoll_create(int size);  
建立一個 epoll 对象,并传回它的id
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 
等待注册的事件被触发或者timeout发生 
  • 使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
  • 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中拿到了要监控的句柄列表。

个人理解:
前面的select,poll设计的比较傻,维护的监听描述符的数据结构都是在用户态维护的,所以内核态是不去保存这些东西的。只有当你进行系统调用的时候,将这些描述符交给操作系统,操作系统才知道需要帮你监听哪些东西好没好。这种问题是函数的设计问题带来的。所以epoll很好的解决这个问题,通过在内核态创建并维护一个epoll对象范湖一个其id让用户去使用,同时当需要监听新的描述符的时候就通过epoll_ctl函数进行动态的添加和删除,这些描述符文在创建epoll对象的时候就在内核态保留了一份此时就不会在每次的调用时进行大量的拷贝操作。

在内核里,一切皆文件。所以,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时立刻返回准备就绪链表里的数据即可。
红黑树的数据结构加速了动态添加和删除描述符的速度因为查找的速度非常的快不再是线性查找而是o(nlogn)

而且不再是主动的轮询去遍历每一个描述符看是不是准备好了,而是采用回调函数的方式接收其通知,这样节省了大量的遍历时间效率得到进一步的提高。

猜你喜欢

转载自blog.csdn.net/zhc_24/article/details/82154966