彻底搞懂 - 一文理清各种IO模型

一.背景知识:
1.文件描述符

众所周知,在Linux中,一切都可以看作是文件,进程对于一个打开的文件,如何对应上并找到它呢?
文件描述符应运而生:File descriptor,简称fd。操作系统为每个进程维护了一个文件描述符表,又维护了一个系统级的打开文件表,当然还有inode表。进程根据fd -> 文件指针 -> 文件偏移量 -> inode指针,就能指明对应的文件资源。
请添加图片描述

在操作系统中,用户进程是不能与IO设备直接交流的,需要通过内核来完成这个任务,内核将IO设备的数据copy到内核缓冲区,再由用户进程将其复制到内存的缓冲区(用户内存),才能读到数据。

2.同步/异步/阻塞/非阻塞

阻塞和非阻塞,指的是在数据就绪之前(内核还没准备好数据之前),用户进程是立刻返回还是等待。等待就是阻塞,反之为非阻塞。
同步与异步的区别主要在于内核数据准备好后,从内核缓冲区→用户内存这个过程需不需要用户进程等待。需要等待就是同步,反之为异步。

二.五种IO模型:
1.同步阻塞模型

网络编程中,用户进程读取客户端的数据需要调用内核提供的recvfrom函数。在默认情况下,这个调用会一直阻塞直到数据接收完毕,就是一个同步阻塞的IO方式。这也是最简单的IO模型,在通常fd较少、就绪很快的情况下使用是没有问题的,但是当有大量请求的时候,我们得创建大量的进程,进程是要占用内存的,在海量请求下,资源很快就会被打爆。

2.同步非阻塞模型

用户进程不断的轮询内核,问数据准备好没,没准备好,就直接返回,准备好了,就阻塞把数据copy到用户内存。比较耗CPU,因为在同步阻塞模型中,如果用户线程因等待IO阻塞了,CPU会把它挂起,去处理别的事,现在它一直轮询,就会一直占用CPU。
疑问:这个轮询间隙,用户进程可以做别的事情吗?

3.IO多路复用模型(改进了的同步非阻塞,也属于事件驱动)

本质上还是一种同步非阻塞模型。随着操作系统内核升级,提供了select/poll/epoll函数(这些函数的介绍参考文末扩展),可以让你调用select的时候,传递多个fd,内核会遍历他们,如果有就绪的,就返回。需要一个监听线程线程不断调用select函数轮询,select返回后再交给处理线程处理数据。看起来比普通同步阻塞模型复杂了,但优点是可以“同时”处理多个链接了,在某些场景下,效率会有提高。

4.信号驱动IO模型(改进了的同步非阻塞,也属于事件驱动)

IO多路复用解决了用一个进程监听多个链接的问题,但select函数还是在做很多无意义的轮询,所以我们希望内核可以在数据就绪后,给我们发一个信号通知一下,这样就不用无意义轮询了:
信号驱动需要开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求会即刻返回,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用线程来读取数据。

5.异步非阻塞模型(也直接叫异步IO)

对比信号驱动IO模型,异步非阻塞模型最大的变化是,用户进程只需要向内核发起一个read请求,就不需要管后边的事了,不需要用户进程亲自去把数据从内核缓冲区copy到用户内存,而是由内核自己完成后,再通知用户进程,此时数据已经在用户内存了,所以就是异步非阻塞了!


三.五种基础IO模型衍生出的JavaIO/Reactor模型/Proactor模型
Reactor模型(基于IO多路复用)
单线程
多线程

Java-BIO (基于同步阻塞)
早期的tomcat用了这种实现:
• 主线程accept请求阻塞
• 请求到达,创建新的线程来处理这个套接字,完成对客户端的响应。
• 主线程继续accept下一个请求
Java-NIO(基于IO多路复用,同步非阻塞)
• 创建ServerSocketChannel监听客户端连接并绑定监听端口,设置为非阻塞模式。(实际上还有各种其它的channel,他们感兴趣的事件不同)
• 创建Reactor线程,创建多路复用器(Selector)并启动线程。
• 将ServerSocketChannel注册到Reactor线程的Selector上。监听accept事件。
• Selector在线程run方法中无线循环轮询准备就绪的Key。
• Selector监听到新的客户端接入,处理新的请求,完成tcp三次握手,建立物理连接。
• 将新的客户端连接注册到Selector上,监听读操作。读取客户端发送的网络消息。
• 客户端发送的数据就绪则读取客户端请求,进行处理。
Java-AIO(基于异步IO,异步非阻塞)
• 创建AsynchronousServerSocketChannel,绑定监听端口
• 调用AsynchronousServerSocketChannel的accpet方法,传入自己实现的CompletionHandler。包括上一步,都是非阻塞的
• 连接传入,回调CompletionHandler的completed方法,在里面,调用AsynchronousSocketChannel的read方法,传入负责处理数据的CompletionHandler。
• 数据就绪,触发负责处理数据的CompletionHandler的completed方法。继续做下一步处理即可。
• 写入操作类似,也需要传入CompletionHandler。

扩展
关于select/poll/epoll,这些函数都是由内核提供的
select:
调用过程
(1)使用copy_from_user从用户空间拷贝fd_set到内核空间(fd_set就是上文提到的多个fd集合)
(2)注册回调函数__pollwait
(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
(8)把fd_set从内核空间拷贝回用户空间。
缺点:
1.单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
2.内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
3.select返回的是含有整个句w柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
4.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
• 水平触发:当就绪的fd未被用户进程处理后,下一次查询依旧会返回,这是select和poll的触发方式。
• 边缘触发:无论就绪的fd是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发。
poll:
与select基本一致,变化是保存fd集合使用了链表,因此从函数角度没有fd数量限制。其他select存在的缺点,poll依然存在。
epoll:
epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。
把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象,可以理解成是个就绪链表(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加fd
3)调用epoll_wait收集发生的事件的连接
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
虽然select/poll和epoll都是醒一会睡一会,醒着的时候去看看有没有就绪的fd,但是select/poll醒着的时候,需要遍历所有fd,而epoll只需要判断一下自己的就绪队列是否为空就行了,而且省去了把fd从内核与用户空间中来回拷贝的消耗,所以性能提升比较大。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值