什么是IO
io是数据的接收和发送操作,linux进程无法直接操作io设备,需要通过系统调用请求内核来完成io操作,内核为每个设备维护一个缓冲区。用户进程发送操作的一个完整io包括两部分:用户空间将数据发送到内核,内核将数据发送到io设备。用户进程接收操作的一个完整io也是包括两部分:内核从io设备中接收数据到缓冲区,从内核缓冲区复制数据到进程空间
5种io模型
阻塞io:进程发起io操作后,进程被阻塞,转到内核空间处理,整个io处理完后返回进程。特点:需要为每一个io请求分配一个进程或线程来处理
非阻塞io:进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。需要进程主动去轮询。
io多路复用: linuxIO多路复用技术提供一个单进程、单线程监听多个IO读写时间的机制。其基本原理是各个IO将句柄设置为非阻塞IO,然后将各个IO句柄注册到linux提供的IO复用函数上(select,poll或者epoll),如果某个句柄的IO数据就绪,则函数返回通知io ready。调用者进行后续的read write操作。多路复用函数帮我们进行了多个非阻塞IO数据是否就绪的轮询操作,只不过IO多路复用函数的轮询更有效率,因为函数一次性传递文件描述符到内核态,在内核态中进行轮询(epoll则是进行等待边缘事件的触发),不必反复进行用户态和内核态的切换。io多路复用的特点:内核轮询多个io在内核缓冲区是否ready,适合高并发网络服务应用。高并发网络服务应用如果采用阻塞io怎么实现?需要为每个socket连接启动一个线程,频繁的线程切换会导致性能低下。如果采用非阻塞io怎么实现?需要进程轮询每个io,如果有一万个socket连接,确定哪个连接ready需要进行1万次从用户态到内核态的切换,性能低下。
信号驱动io:进程发起io操作,会向内核注册一个信号处理函数,然后进程返回不阻塞,当内核数据就绪时发送一个信号给进程,进程在信号处理函数中调用io函数处理。特点:回调机制,开发难度大
异步io:进程发起一个io操作后,进程返回。内核把整个io处理完后(包括将数据从内核缓冲区复制到用户态)通知进程,进程只需要在指定的数组中引用数据即可
同步io和异步io
同步IO:用户进程发出IO调用,去获取IO设备数据,双方的数据要经过内核缓冲区同步,完全准备好后,再复制返回到用户进程。而复制返回到用户进程会导致请求进程阻塞,直到I/O操作完成。
异步IO:用户进程发出IO调用,去获取IO设备数据,并不需要同步,内核直接复制到进程,整个过程不导致请求进程阻塞。
阻塞io、非阻塞io、io多路复用、信号驱动io都是同步io
同步IO最终需要应用程序调用系统调用从内核来读取数据、异步IO由系统来负责将数据从内核读取到应用程序,应用程序直接使用。
select
1 可以一次性从用户态向内核态传递多个fd
2 内核把当前进程挂到相应io设备的等待队列中
3 内核逐个io判断是否就绪,如果有就绪的就返回(select返回),如果全部未就绪,调用schedule_timeout将进程睡眠
4 当设备驱动发生自身资源可读写后,唤醒其队列上的睡眠进程,进程返回(select返回)
5 如果超过schedule_timeout还没人唤醒,进程唤醒重新遍历fd重复上边流程
优点:
1 一次性传递多个fd
2 在内核中遍历,不必反复进行用户态和内核态的切换
3 具有唤醒机制
缺点:
总结一句话就是:一堆fd从用户态拷贝到内核态,内核态遍历一堆fd,内核态返回后用户态需要遍历一堆fd,这一堆的个数还限制的比较小
1 每次调用select,都需要把fd集合从用户态拷贝到内核态返回时还要从内核态拷贝到用户态,这个开销在fd很多时会很大
2 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3 select返回后,用户不得不自己再遍历一遍fd集合,以找到哪些fd的IO操作可用
4 再次调用select时,fd数组需要重新被初始化
5 select支持的文件描述符数量太小了,内核中采用位图存储,默认是1024
poll
1 通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,其他的没有区别
epoll
epoll三大关键要素:mmap、红黑树、链表
mmap:epoll通过mmap将用户空间的一块地址和内核空间的一块地址映射到相同的一块物理内存地址,减少用户态和内核态之间的数据交换,内核可以直接看到监听的句柄
红黑树:epoll采用红黑树来存储监听的套接字。epoll_ctl添加或者删除一个套接字时,都在红黑树上处理,红黑树的插入和删除性能比较好
双向链表:epoll的rdllist采用的就是双向链表
epoll主要函数
1 int epoll_create (int size );
采用epoll_create()建立epoll描述符来记录需要监控的fd(select每次都把fd集合从用户空间赋值到内核空间)
在epoll早期的实现中,对于监控文件描述符的组织并不是使用红黑树,而是hash表。这里的size实际上已经没有意义。
2 int epoll_ctl (int epfd,int op,int fd,struct epoll_event *event);
epoll_ctrl用来添加或删除待监控的fd。当调用epoll_ctrl添加fd和事件时,该事件都会和相应的设备驱动程序建立回调关系,当相应的事件发生后,调用ep_poll_callback回调函数,这个函数把这个事件添加到rdllist双向链表中。
op可以指定操作类型:EPOLL_CTL_ADD(往事件表中注册fd上的事件)、EPOLL_CTL_MOD(修改fd上的注册事件)、EPOLL_CTL_DEL(删除fd上的注册事件)
event:指定fd关注的事件
3 int epoll_wait (int epfd,struct epoll_event* events,int maxevents,int timeout );
(1) epoll_wait调用ep_poll,当rdllist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
(2) 文件fd状态改变,导致相应fd上的回调函数ep_poll_callback()被调用。
(3) ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。
(4) ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。
(5) ep_send_events函数,它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。
ET模式和LT模式
epoll_wait返回的两个时机:
时机一:fd状态改变,ep_poll_callback被调用,加入rdllist。对于读操作:一是buffer由不可读状态变为可读的时候。二是有新数据到达,buffer中待读的内容变多的时候。对于写操作:一是buffer由不可写变为可写的时候。二是旧数据发送走,buffer中可写的空间变大的时候。
时机二:fd的events中有相应的事件位置1时。对于读操作:buffer中有数据可读,buffer不为空的时候fd的events的可读位就置1。对于写操作:buffer中有空间可写,buffer不满的时候fd的可写位就置1。
对于ET模式,只采用上述时机一。LT模式采用上述时机一和时机二。
ET模式注意事项
ET模式下,读操作如果一次没有读尽buffer中的数据,将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。因此ET模式下需要注意:1,采用非阻塞io 2,循环读写,保证读完、写满。
ET模式和LT模式如何保证数据可以被读完?ET模式由用户程序来循环,LT模式通过多次epoll_wait返回来循环。
建议采用ET模式+非阻塞io+循环读写,相比LT模式,省去了多次epoll_wait的调用。
https://blog.csdn.net/daaikuaichuan/article/details/88777274
高并发服务器模型epoll+线程池
这种架构特点如下:
1 基于I/O多路复用的思想,通过单线程I/O多路复用,可以达到高效并发,同时避免了多线程I/O来回切换的各种开销。
2 由于业务多跟数据库打交道会造成阻塞,基于线程池的多工作者线程,可以充分发挥和利用多线程的优势。
创建一个epoll实例;
while(server running)
{
epoll等待事件;
if(新连接到达且是有效连接)
{
accept此连接;
将此连接设置为non-blocking;
为此连接设置event(EPOLLIN | EPOLLET ...);
将此连接加入epoll监听队列;
从线程池取一个空闲工作者线程并处理此连接;
}
else if(读请求)
{
从线程池取一个空闲工作者线程并处理读请求;
}
else if(写请求)
{
从线程池取一个空闲工作者线程并处理写请求;
}
else
其他事件;
}
golang net库网络实现分析
golang中的网络io全部是非阻塞io
网络io的read操作如下:
1 golang协程通过系统调用来读取数据
2 如果数据没准备好,调用waitRead----->runtime_pollWait----->poll_runtime_pollWait------>netpollblock------>gopark------->park_m--------->schedule 进行协程的切换。这样就通过非阻塞io加协程切换模拟出了阻塞io,可以采用阻塞io的简单开发方式
3 golang中起一个线程定期执行sysmon函数,sysmon---->netpoll----->epollwait 返回ready的协程列表,sysmon----->injectglist------>casgstatus将ready的协程的状态设置为可运行
4 读io的协程继续运行,再次通过系统调用来读取数据,最终返回
底层本质是:非阻塞io+epoll+线程池+任务队列的模型,epoll返回哪个任务的io 已经ready,线程池中的线程取io ready的任务继续执行,goalng通过协程将这些全部封装好了,通过协程实现了非阻塞的io可以采用阻塞的方式编程