文章目录
- 一. IO的概念和分类
- 1. IO操作的原理:
- 二. I/O多路复用使用场景和作用
- 1. 问题: 一台网络服务器需要接收100台客户端的连接和数据通信,应该如何设计和实现?
- 2. I/O多路复用机制:
- 三. Select poll epoll
- select
- poll
- epoll
- 比较
一. IO的概念和分类
1. IO操作的原理:
以一个进程的输入类型的IO调用为例,它将完成或引起如下工作内容:
1)用户进程向操作系统请求外部数据;
2)操作系统将外部数据加载到内核缓冲区
3)操作系统将数据从内核缓冲区拷贝到进程缓冲区
4)进程读取数据继续后面的工作
二. I/O多路复用使用场景和作用
1. 问题: 一台网络服务器需要接收100台客户端的连接和数据通信,应该如何设计和实现?
分析:
1).肯定不能一个进程或线程依次进行IO的处理,那么会出现阻塞等待某个IO的过程,此时就可能丢其他IO的数据;
2).可以使用多个线程,每个线程直跟踪处理一个IO通信,只有少量IO通信是可行的,如果有多个IO都是使用线程的话,由于线程的资源开销比较大,线程创建、释放和调度切换等操作都会大大的浪费CPU时间,降低系统资源和效率;
3).解决上面的问题的办法,内核提供了IO多路复用的机制,即下面3种:poll、select、epoll;
2. I/O多路复用机制:
- 通过一种事件机制,可以监视多个描述符,一旦某个描述符就绪(一般是可读或可写状态),能够通知应用程序进行相应的读写操作;
- 在多路复用IO模型中,会借助于内核的处理。一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。
- 在多路复用IO模型中,上层应用只需要使用一个线程就可以管理多个socket,不需要建立新的进程或者线程,也不必维护这些线程和进程,且只有真正有读写事件进行时才会使用IO资源,所以它大大减少来资源占用;
- 在多路复用IO模型中,应用层通常使用select/poll/epoll函数向系统内核发起调用,并阻塞在这几个系统函数的调用中,而不是阻塞于实际的IO操作;
- 在网络编程的过程中,以下几种情况需要使用I/O复用模型:
- 客户端要同时处理多路socket通信;
客户端程序要同时处理用户交互式输入和网络套接口;
TCP服务器要同时处理监听socket和连接socket,这种情况是I/O复用使用最多的一种情况。
服务器要同时处理TCP请求和UDP请求;
服务器要处理多个服务或多个协议,一般要使用I/O复用。
三. Select poll epoll
select
1.1 优点 select 其实就是把NIO中用户态要遍历的 fd 数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了
从代码中可以看出,select系统调用后,就会修改对应数据到达后的标记位,从而判断数据是否到达。之后把内核态的数据再返回到用户态。
1.2 存在的问题 1、bitmap最大1024位,一个进程最多只能处理1024个客户端 2、&rset不可重用,每次socket有数据就相应的位会被置位 3、文件描述符数组拷贝到了内核态,仍然有开销
4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历
poll
优点 1、poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client
2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回0,实现了pollfd数组的重用 缺点 poll
解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题
1、pollfds数组拷贝到了内核态,仍然有开销 2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历
epoll
一棵记录要监听的FD的红黑树和一个记录就绪FD的链表
1 事件通知机制 1、当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中
2、网卡向cpu发起中断,让cpu先处理网卡的事
3、中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中
2 详细过程
首先epoll_create创建epoll实例,它会创建所需要的红黑树,以及就绪链表,以及代表epoll实例的文件句柄,其实就是在内核开辟一块内存空间,所有与服务器连接的socket都会放到这块空间中,这些socket以红黑树的形式存在,同时还会有一块空间存放就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;
epoll_ctl添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有,
则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。
epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。
3 水平触发和边沿触发
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用
epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
4 优点 epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll
1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小
2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket
比较
- select:
优点:目前几乎在所有的平台上支持,其具有良好的跨平台支持;
缺点:
1)单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024, 可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。
2) 可读和可写的fdset分开管理和添加,比较麻烦。假设我们有一个文件描述符sockfd,如果我们要监听它的可读和可写,那么这个文件描述符就需要传入到两个fd_set中,再将两个fd_set传入到select中;
2.poll:
优点:
1)poll克服了文件描述符数量的限制,但仍然存在一定的效率问题;
2)解决了select中的第2个缺点;
3)select监测的事件只有3种,而poll将监测事件细化了。
缺点:select和poll采用的都是轮询检测的机制,即每次调用都要重复的将文件描述符传入到内核当中,这一点很大程度上降低了程序的运行效率; - epoll:
优点:
1)监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目;
2)效率高;尤其适用于高并发场景;不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的;
3)支持电平触发和边沿触发两种方式,理论上边缘触发的性能要更高一些,但是代码实现相当复杂;
4)mmap加速内核与用户空间的信息传递。epoll是通过内核于用户空间mmap同一块内存,避免了无畏的内存拷贝。
缺点:但仅能在Linux平台上可用,跨平台性差;