IO模型
单进程:阻塞
多进程:每个进程响应一个请求,缺陷:进程切换次数多,切换本身消耗大量的资源。每个进程地址空间独立,很多资源被加载到用户内存空间会有重复,内存利用效率底。
线程:每个线程响应一个请求。线程也需要切换,切换量级比较轻。线程可以共享同一个进程的资源(提高内存使用效率)。但是会有资源争用问题。
线程实质上是进程内部的子运行单位
线程在linux上被称为轻量级进程(lwp),linux不支持源生态的线程。
问题:linux支持多少种类型的线程库,如何切换线程库。
IO发生时涉及的对象和步骤。对于一个网络请求,请求命令到达服务器后,会涉及到两个系统对象,一个是调用服务器用户空间的进程或线程,另一个就是进程或者线程调用系统内核。
在进程(线程)调用系统内核去访问IO设备的过程是一个两段式结构,即:
等待数据准备:内核从IO设备将数据准备至内核空间的buffer中
内核将数据从内核的buffer拷贝到进程空间中
通俗的说,就是进程将请求交给cpu,等待cpu返回数据,而进程本身可以是异步、同步、阻塞、非阻塞状态;而内核对磁盘IO的调用,包括从内核内存空间复制到用户内存空间,这些操作都占据了大量的时间。
进程无法直接操作IO设备,必须通过系统调用请求内核来协助IO操作;内核在IO操作时,会为每一个IO设备维护一个buffer,等待数据输入到buffer、从buffer复制到进程内存空间,都需要时间。
根据等待的模式不同,IO动作可以分为五种:
blocking IO:阻塞式IO
nonblocking IO:非阻塞式IO
IO multiplexing:IO复用,一个进程处理两个IO,就得复用
signal driven IO:信号驱动IO
asynchronous IO:
阻塞式IO
阻塞式IO可以以同步方式,也可以以异步方式。这里说的阻塞式IO指同步阻塞IO(blocking IO)。blocking IO的特点就是在IO执行的两个阶段都被阻塞了。
网络模型
网络模型:同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在linux中,默认情况下所有的socket都是阻塞状态。它符合人们最常见的思考逻辑。阻塞就是进程 "被" 休息, CPU处理其它进程去了。
在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程,大致如下图:
流程描述
流程描述:
当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。
第二个阶段:当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后聂赫返回结果,用户进程才解除阻塞的状态,重新运行起来。
Blocking IO的优缺点
优点:
能够及时返回数据,无延迟;
对内核开发者来说简单;
缺点:
对用户来说处于等待就要付出性能的代价了;
非阻塞式IO
非阻塞IO可以以同步方式,也可以以异步方式。这里指的时同步非阻塞式IO。
网络模型
同步非阻塞就是进程一直处于询问状态,非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地分到CPU事件片去查看内核数据准备是否完成。也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
在linux下,可以通过设置socket使其变为non-blocking。流程如图所示:
流程描述
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦聂赫中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有
nonblocking IO的优缺点
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
IO复用
异步阻塞IO称之为IO复用。
网络模型
同步非阻塞方式进程需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。
UNIX/Linux 下的 select、poll、epoll 可以帮助完成。
IO多路复用有几个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于:前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。
select或poll调用之后,会阻塞进程,与blocking IO阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这几个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。
对于多路复用,也就是轮询多个socket。多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了,当然也可以针对不同的编号。具体流程,如下图所示:
流程描述
它的基本原理进程处理网络请求,在第一阶段(数据准备),select,poll,epoll这些函数会不断的轮询所负责的所有socket,当某个socket有数据被内核加载到内核的buffer,就通知用户进程。这个过程进程阻塞。进程得到信号后,重新发送系统调用(recvfrom),通知内核复制数据到进程空间,内核复制数据到进程空间。整个过程中,数据准备阶段和复制阶段进程都是阻塞的,但是这两步进程会发送不同的系统调用,两步之间进程时非阻塞的,也就是这个间隙,进程可以去处理别的网络请求。
多路复用的特点是通过一种机制一个进程能同时等待多个IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。
整个过程,进程会发起两次系统调用:
数据准备阶段:select
复制阶段:recvfrom
在IO多路复用中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,整个用户的process其实是一直被阻塞的。只不过是被select这个函数阻塞,而不是被socket IO给阻塞。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
IO复用的优缺点
优点:
一个进程可以同时处理多个connection
信号驱动IO
异步进行
进程发起请求,等待数据阶段不阻塞,进程也不需要轮询数据是否准备完成,数据准备完成后,内核通知进程。进程再次发起系统调用,在将数据复制到进程空间过程,仍然是阻塞的。
第一阶段完成后,内核通知是水平触发,即隔一段时间去通知一次。
异步IO
AIO:异步非阻塞IO。在数据准备和复制到进程空间都不阻塞。
网络模型
用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。
IO两个阶段,进程都是非阻塞的。
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:
流程描述
用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而内核接受到进程的asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞,然后内核会等待数据准备完成,然后将数据复制到进程空间。当这一切都完成之后,内核会给用户进程发送一个信号或执行一个基于线程的回调函数来完成这次 IO 处理过程。
在 Linux 中,通知的方式是 “信号”:
如果这个进程正在用户态忙着做别的事,那就强行中断,调用事先注册的信号处理函数。
如果这个进程正在内核态忙着做别的事`,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
如果这个进程现在被挂起了,那就把这个进程唤醒。等待CPU分配时间片,触发信号通知。
总结
IO模型 | 使用模式 | 调用函数 | 第一段 | 第二段 |
---|---|---|---|---|
阻塞式IO | 同步阻塞 | recvform | 阻塞 | 阻塞 |
非阻塞式IO | 同步非阻塞 | recvform | 非阻塞 | 阻塞 |
IO复用 | 异步阻塞 | select,recvform | 阻塞 | 阻塞 |
信号驱动IO | 异步 | signal,recvform | 非阻塞 | 阻塞 |
异步IO | 异步非阻塞 | epoll,recvform | 非阻塞 | 非阻塞 |