I/O(Input/Output)即数据的输入/输出,为什么大家很关心I/O的性能呢?因为I/O存在的范围很广,在高并发的场景下,这部分性能会被无限放大。而且与业务无关,是可以有统一解决方案的。
所有的系统I/O都分为两个阶段:等待就绪和数据操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写:
-
等待就绪 :等待数据就绪,一般是将数据加载到 内核缓存区 。无论是从磁盘、网络读取数据,程序能处理的都是进入内核态之后的数据,在这之前,cpu会阻塞住,等待数据进入内核态。
-
数据操作 :数据就绪后,一般是将内核缓存中的数据加载到 用户缓存区 。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。这就出现一个奇怪的现象 – 不使用CPU的“等待就绪”,却比实际使用CPU的“数据操作”,占用CPU时间更多 。
传统阻塞I/O模型,即在读写数据过程中会发生阻塞现象。当用户线程发出I/O请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才会解除block状态。
明确的是,让当前工作线程阻塞,等待数据就绪,是很浪费线程资源的事情,上述三种I/O都有一定的优化方案:
-
磁盘I/O :现代电脑中都有一个DMA(Direct Memory Access 直接内存访问) 的外设组件,可以将I/O数据直接传送到主存储器中并且传输不需要CPU的参与,以此将CPU解放出来去完成其他的事情。
-
网络I/O :NIO、AIO等I/O模型,通过向事件选择器注册I/O事件,基于就绪的事情来驱动执行I/O操作,避免的等待过程。
-
内存I/O :内存部分没涉及到太多阻塞,优化点在于减少用户态和内核态之间的数据拷贝。nio中的零拷贝就有mmap和sendfile等实现方案。
===============================================================================
这里仔细的讲讲网络I/O模型中的阻塞,即socket的阻塞。在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式,是在tcp/ip协议上,抽象出来的一层网络通讯协议。
同上面I/O的过程一样,网络I/O也同样分成两个部分:
-
等待网络数据到达网卡,读取到内核缓冲区。
-
从内核缓冲区复制数据到用户态空间。
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区:
-
输入缓冲区 :当使用 read()/recv() 读取数据时,(1)首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。(2)如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。(3)直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
-
输出缓冲区 :当使用 write()/send() 发送数据时,(1)首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间