多路IO复用
1.DMA
Direct Memory Access/直接存储器访问
允许不同速度的硬件来沟通。DMA传输将数据从一个地址复制到另一个地址空间。CPU需要初始化这个操作,然后操作是由DMA完成的。例如:移动一个外部内存的区块到芯片内部更快的内存区。这种操作让CPU去处理其他操作,提供了效率。
总线控制权转移:
- 传输前:CPU要把总线控制权交给DMA控制器
- 传输后:把总线控制权再交还给CPU
DMA传输步骤:
- 请求
- 响应
- 传输
- 结束
1.请求
CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。
2.响应
DMA获得总线控制权的过程。(DMA控制器对DMA请求判别优先级及屏蔽,向总线裁决逻辑提出总线请求。当CPU执行完当前总线周期即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经响应,通过DMA控制器通知I/O接口开始DMA传输)
3.传输
DMA控制器获得总线控制权后,由DMA控制器输出读写命令,直接控制RAM与I/O接口进行DMA传输。在传送过程中不需要CPU的参与。开始时需提供要传送的数据的起始位置和数据长度。
4.结束
DMA控制器即释放总线控制权,并向I/O接口发出结束信号。当I/O接口收到结束信号后,一方面停 止I/O设备的工作,另一方面向CPU提出中断请求。
2. SELECT
准备文件描述符的数组
文件描述符的最大值存入一个变量——MAX
select函数,需要传入的参数:select(MAX+1, &rset, NULL, NULL, NULL);
- 最大的文件描述符加上一:
MAX+1 - 读文件描述符集合 (bitmap,需要监听的置为1?)
- 写文件描述符集合
- 异常的文件描述符集合
- 超时时间
select函数是一个阻塞函数,没有收到数据就不返回。
select将 rset 从用户态拷贝到内核态
当有数据到来时,内核会做两件事:
- 将 rset 中对应的有数据的文件描述符置位
- selcet函数返回,不再阻塞
select函数返回后,遍历,看看是哪一些文件描述符有数据来了,并进行相应的处理
select函数的缺点:
- rset的默认大小是1024,虽然可以提升,但仍有上限
- FDset被内核置为后,不可重用。每次while循环都要重新置位
- 将rset拷贝到内核态,开销较大
- select函数返回后,需要遍历,复杂度为O(n)
3. POLL
struct pollFD {
int fd;
short events;//需要干什么事情: 读(pollIN), 写(pollOUT) 都在意(与|)
short revent;
};
poll函数的传入参数 poll(pollFDs, 5, 5000);
- pollFD数据
- 传入数组中元素的数量
- 超时时间
poll是一个阻塞函数,水平出发
收到事件后(读写),内核要
pollFD.revent置位- poll函数返回
poll函数返回后,遍历,看哪些fd有事件被触发,并将pollFD.revent 置为0;
相比于select函数的优点:
- 没有最大连接数的限制(基于链表), 突破了bitmap的上限
- 每次只需回复
pollFD.revent
poll缺点:
- 将
pollFD拷贝到内核态,开销较大 - poll函数返回后,需要遍历,复杂度为O(n)
4. epoll
event poll,是事件驱动的
nfds = epoll_wait(eqfd, events, 5, 5000);
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll通过内核和用户空间共享一块内存来实现的。
两种触发模式:
epoll有两种触发的方式即LT和ET。在前者,只要存在着事件就会不断的触发,直到处理完成,而后者只触发一次相同事件或者说只在从非触发到触发两个状态转换的时候儿才触发。
- LT:水平触发
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
- ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。
下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数.
因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
多线程中epoll_oneshot
借鉴了一篇博客,来源点这里
在多线程处理中,有一个socket事件发生,数据开始解析。正在这个时候,同一个socket 又有新的事件发生,而数据还没有解析完成,那么程序就会自动调度另一个线程或者进程处理这个事件。这样的话,就会带来很大的问题——不同的线程处理来源于同一个 socket 事件,这会使程序的健壮性降低且编程的复杂度增加。(ET模式下也有可能出现这种情况)
解决方案:
- 在单独的线程或进程里解析数据——接收数据的线程在接收到数据后立刻将数据转移至另外的线程
- epoll_oneshot:可以在epoll上注册这个事件,注册这个事件后,如果在处理写成当前的SOCKET后不再重新注册相关事件,那么这个事件就不再响应了或者说触发了。要想重新注册事件则需要调用epoll_ctl重置文件描述符上的事件,这样前面的socket就不会出现竞态这样就可以通过手动的方式来保证同一SOCKET只能被一个线程处理,不会跨越多个线程。
epoll是阻塞的,有数据到来时,内核会:
- 将有数据的 fd 放到最前面的位置
- 返回触发事件的 fd 的数量
epoll 的优点:
- 没有将 fd 从用户态拷贝到内核态的开销
- 函数返回后,遍历复杂度降低
详情点这里


