IO模型
原文链接https://www.jianshu.com/p/486b0965c296
写在前面:缓存IO又称为标准IO,大多数文件系统的默认IO操作都是缓存IO。在LINUX的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先拷贝到操作系统内核的缓冲区中,然后才会从操作系统的内核缓冲区拷贝到应用程序的地址空间。
网络IO的本质是socket的读取,socket在Linux系统中被抽象为流,IO可以理解成对流的操作。对于一次IO访问,数据先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统的内核缓冲区拷贝到应用程序的地址空间。所以分为两步:
1.第一阶段 等待数据准备
2.第二阶段 将数据从内核拷贝到进程中
对于socket流而言:
第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区
第二步:把数据从内核缓冲区复制到应用程序缓冲区
网络IO模型分为:
同步模型:1.阻塞IO 2.非阻塞IO 3. 多路复用IO 4.信号驱动式IO
异步IO
信号驱动IO在实际中是不常用的。
以一个生动形象的例子来说明这四个概念。周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案。
2.1 阻塞IO
同步阻塞IO流程描述
kernel就开始了IO的第一个阶段:准备数据
(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。在用户进程这边,整个进程会被阻塞。第二阶段:当内核一直等待到数据准备好了,它就会将数据从内核拷贝到用户地址空间,然后内核返回结果,用户进程解除阻塞状态。这就是非阻塞
。需要不断的询问,是否准备好了。网络模型:
同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式
。在这种模型中,设备是以非阻塞的形式打开的
。这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。再linux下,通过设置socket使其变为non-blocking。
这就是典型的IO多路复用
。I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作
。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。对于多路复用,也就是轮询多个socket。多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了
,当然也可以针对不同的编号。具体流程,如下图所示:
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking
,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block
。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理
。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求
。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小
,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下:
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
服务器需要同时处理多种网络协议的套接字。
了解了前面三种IO模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不一样,直接等待,轮询,select或poll轮询
,两个阶段过程:
第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
第二个阶段都是阻塞的。
从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。【此句很重要!!!】
高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式
。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度
。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求)
,而并行数是可以同时工作的物理资源数量(如 CPU 核数)
。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求
。
用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情
。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知
。IO两个阶段,进程都是非阻塞的
。Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:
首先它会立刻返回,所以不会对用户进程产生任何block
。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程
,告诉它read操作完成了。五种IO模型总结#
3.1 blocking和non-blocking区别##
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
3.2 synchronous IO和asynchronous IO区别##
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞
。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作
,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了
,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成
。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check
,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知
。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据
。