一、IO基本概念
什么是IO?
I/O(input/output)即输入和输出,在冯诺依曼体系结构中,将数据从输入设备拷贝到内存就即输入,将数据从内存拷贝到输出设备就即输出
对文件进行的读写操作本质就是一种IO,文件IO对应的外设是磁盘
对网络进行的读写操作本质也是一种IO,网络IO对应的外设是网卡
OS如何得知外设中有数据可读取?
输入就是操作系统将数据从外设拷贝到内存的过程,操作系统一定要通过某种方法得知特定外设上是否有数据就绪
并不是操作系统想要从外设读取数据时外设上就一定有数据。如用户正在访问某台服务器,当用户的请求报文发出后就需等待从网卡中读取服务器发来的响应数据,但此时服务器可能还没有收到请求报文,或是正在对请求报文进行数据分析,也可能服务器发来的响应数据还在网络中路由
但操作系统不会主动去检测外设上是否有数据就绪,这种做法会降低操作系统的工作效率,因为大部分情况下外设中都是没有数据的,因此操作系统所做的大部分检测工作其实都是徒劳的
操作系统实际采用的是中断的方式来得知外设上是否有数据就绪的,当某个外设上面有数据就绪时,该外设就会向CPU中的中断控制器发送中断信号,中断控制器再根据产生的中断信号的优先级按顺序发送给CPU
每一个中断信号都有一个对应的中断处理程序,存储中断信号和中断处理程序映射关系的表被称为中断向量表,当CPU收到某个中断信号时就会自动停止正在运行的程序,然后根据该中断向量表执行该中断信号对应的中断处理程序,处理完毕后再返回原被暂停的程序继续运行
注意:CPU不直接和外设交互指的是在数据层面上,而外设其实是可以直接将某些控制信号发送给CPU中的某些控制器的。
OS如何处理从网卡中读取到的数据包?
操作系统任何时刻都可能会收到大量的数据包,因此操作系统须将这些数据包管理起来。所谓的管理即"先描述,再组织",在内核中有一个结构sk_buff,该结构就是用来管理和控制接收或发送数据包的信息
简化版的sk_buff结构:
当操作系统从网卡中读取到一个数据包后,会将该数据依次交给链路层、网络层、传输层、应用层进行解包和分用,最终将数据包中的数据交给了上层用户,那对应到这个sk_buff结构来说具体是如何进行数据包的解包和分用的呢?
当操作系统从网卡中读取到一个数据包后,就会定义一个sk_buff结构,然后用sk_buff结构中的data指针指向这个读取到的数据包,并将这个sk_buff结构与其他sk_buff结构以双链表的形式组织起来,此时操作系统对各个数据包的管理就变成了对双链表的增删查改等操作
接下来需要将读取上来的数据包交给最底层的链路层处理,进行链路层的解包和分用,让sk_buff结构中的mac_header指针指向最初的数据包,然后向后读取链路层的报头,剩下的就是要交给网络层处理的有效载荷了,此时便完成了链路层的解包
这时链路层就需要将有效载荷向上交付给网络层进行解包和分用了,所谓的向上交付只是形象的说法,实际向上交付并不是要将数据从链路层的缓冲区拷贝到网络层的缓冲区,只需让sk_buff结构中的network_header指针,指向数据包中链路层报头后的数据即可,然后继续向后读取网络层的报头,便完成了网络层的解包
紧接着就是传输层对数据进行处理,同样的道理,让sk_buff结构中的transport_header指针,指向数据包中网络层报头后的数据,然后继续向后读取传输层的报头,便完成了传输层的解包
传输层解包后就可以根据具体使用的传输层协议,对应将剩下的数据拷贝到TCP或UDP的接收缓冲区供用户读取即可
发送数据时对数据进行封装也是同样的道理,即依次在数据前面拷贝上对应的报头。应用层以下,数据包在进行封装和解包的过程中,本质数据的存储位置是没有发生变化的,实际只是在用不同的指针对数据进行操作
但内核中的sk_buff并不像上面那样简单:
一方面,为了保证高效的网络报文处理效率,要求sk_buff的结构必须是高效的
另一方面,sk_buff结构需要被内核协议中的各个协议共同使用,因此sk_buff必须能够兼容所有网络协议
什么是高效的IO?
IO主要分为两步:
第一步是等,即等待IO条件就绪
第二步是拷贝,即当IO条件就绪后将数据拷贝到内存或外设
任何IO的过程,都包含"等"和"拷贝"这两个步骤,但在实际的应用场景中"等"消耗的时间往往比"拷贝"消耗的时间多,因此要让IO变得高效,最核心的办法就是尽量减少"等"的时间
二、钓鱼故事理解IO
IO的过程其实和钓鱼是非常类似的
钓鱼的过程同样分为"等"和"拷贝"两个步骤,只不过这里的"等"指的是等鱼上钩,"拷贝"指的是当鱼上钩后将鱼从河里"拷贝"到鱼桶中
IO时"等"消耗的时间往往比"拷贝"消耗的时间多,钓鱼也符合这个特点,钓鱼时大部分时间都在等鱼上钩,而当鱼上钩后只需要一瞬间就能将鱼"拷贝"上来
下面给出五个人的钓鱼方式:
张三:拿1个鱼竿,将鱼钩抛入水中后就死死的盯着浮漂,什么也不做,当有鱼上钩后就挥动鱼竿将鱼钓上来
李四:拿1个鱼竿,将鱼钩抛入水中后就去玩手机了,然后定期观察浮漂,若有鱼上钩则挥动鱼竿将鱼钓上来,否则继续去做其他事情
王五:拿1个鱼竿,将鱼钩抛入水中后在鱼竿顶部绑一个铃铛,然后就去玩手机了,若铃铛响了就挥动鱼竿将鱼钓上来,否则就不管鱼竿
赵六:拿100个鱼竿,将100个鱼竿抛入水中后就定期观察这100个鱼竿的浮漂,如果某个鱼竿有鱼上钩则挥动对应的鱼竿将鱼钓上来。
田七:田七是一个有钱的老板,他给了自己的司机一个桶、一个电话、一个鱼竿,让司机去钓鱼,当鱼桶装满的时候再打电话告诉田七来拿鱼,而田七自己则开车去做其他事情去了
张三、李四、王五的钓鱼效率是否一样?为什么?
张三、李四、王五的钓鱼效率本质上是一样的
其钓鱼方式都是一样的,都是先等鱼上钩,然后再将鱼钓上来
其次,因为他们每个人都是拿的一根鱼竿,当河里有鱼来咬鱼钩时,这条鱼咬哪一个鱼钩的概率都是相等的
因此张三、李四、王五三个人的钓鱼的效率是一样的,只是等鱼上钩的方式不同而已,张三是死等,李四是定期检测浮漂,而王五是通过铃铛来判断是否有鱼上钩
问题是钓鱼效率是否是一样的,而不是问整体谁做的事最多,若说整体做事情的量的话,那一定是王五做得最多,李四次之,张三最少
张三、李四、王五它们三个人分别和赵六比较,谁的钓鱼效率更高?
赵六是四个人中钓鱼效率最高的,因为赵六同时在等多个鱼竿上有鱼上钩,因此在单位时间内,赵六的鱼竿有鱼上钩的概率是最大的
为了方便计算,假设赵六拿了97个鱼竿,加上张三、李四、王五的鱼竿一共就有100个鱼竿
当河里有鱼来咬鱼钩时,这条鱼咬张三、李四、王五的鱼钩的概率都是百分之一,而咬赵六的鱼钩的概率就是百分之九十七
因此在单位时间内,赵六的鱼竿上有鱼的概率是张三、李四、王五的97倍
而高效的钓鱼就是要减少单位时间内"等"的时间,增加"拷贝"的时间,所以说赵六的钓鱼效率是这四个人中最高的
赵六的钓鱼效率之所以高,是因为赵六一次等待多个鱼竿上的鱼上钩,可以将"等"的时间进行重叠
如何看待田七的这种钓鱼方式?
田七让司机帮自己钓鱼,自己开车去做其他事情去了,此时这个司机具体怎么钓鱼已经不重要了,他可以模仿张三、李四、王五、赵六任何一个人的钓鱼方式进行钓鱼
最重要的是田七本人并没有参与整个钓鱼的过程,只是发起了钓鱼的任务,而真正钓鱼的是司机,田七在司机钓鱼期间可能在做任何其他事情,若将钓鱼看作是一种IO的话,那田七的这种钓鱼方式即异步IO
而对于张三、李四、王五、赵六而言,都需要自己等鱼上钩,当鱼上钩后又需要自己把鱼从河里钓上来,对应到IO中就是需要自行进行数据的拷贝,因此他们四个人的钓鱼方式即同步IO
五种IO模型
实际这五个人的钓鱼方式分别对应的就是五种IO模型。
张三这种死等的钓鱼方式对应即阻塞式IO
李四这种定时检测是否有鱼上钩的方式即非阻塞IO
王五这种通过设置铃铛得知事件是否就绪的方式即信号驱动IO
王五这种一次等待多个鱼竿上有鱼的钓鱼方式即IO多路转接
田七这种让别人帮自己钓鱼的钓鱼方式即异步IO
通过这里的钓鱼例子可以看到发现,阻塞IO、非阻塞IO和信号驱动IO本质上是不能提高IO的效率的,但非阻塞IO和信号驱动IO能提高整体做事的效率
其中,这个钓鱼场景中的各个事物都能与IO中的相关概念对应起来,比如这里钓鱼的河对应就是内核,这里每一个人都是进程或线程,鱼竿对应的就是文件描述符或套接字,装鱼的桶对应的就是用户缓冲区