四种常用的IO模型
不管是做C端还是做B端,都要接触网络。文件操作,rpc,网上冲浪等,都与网络相关。网络又离不开IO。用的最多的IO操作就是读取和写入了。在Linux系统中,用read系统调用来发起读取操作,用write系统调用来发起写入操作。虽然在开发中,很少接触到底层的原理。但是学习后可以让我们写出更高质量的代码。
当发起read系统调用后,是否就可以读取数据了?在这过程中数据是怎么样传输的?这就不得不要了解操作系统的知识了。在现代的操作系统,操作系统主要是内核管理的,为了保护内核,将内存分为了内核空间和用户空间。内核运行在内核空间,用户程序运行在用户空间。如果用户程序想要读取或写入,只能通过系统调用,而不能直接访问硬件设备。那么发起系统调用后就直接访问硬件设备吗?假如程序要发送数据,但是要隔段时间发送,为了提高效率,引入了缓存。不管是在内核空间的程序,还是用户空间的程序。比如,调用write写入数据,并不会将数据直接通过网卡发送。而是先放在内核空间的缓存中,积累一定量的数据后在发送。read系统调用也是如此。read,write系统调用如下所示:
在客户端,通过write调用发送请求,在服务端,通过read调用获取请求并处理后在通过write调用发送响应。客户端在通过read调用读取响应。
当有少量请求时,IO对系统的吞吐量等影响较小,但是有大量请求时,比如几万,甚至10几万时对系统影响就很大了。这时主要关注哪些地方影响了系统的效率,以及怎么解决。这就不得不了解IO模型了。
IO模型
阻塞IO指的是需要等待内核IO操作完成后才返回用户空间执行指令.
非阻塞IO指的是用户进程发起IO后不需要等待内核完成即可返回用户空间执行指令.
同步IO:用户进程主动发起IO,内核被动接收.
异步IO:系统内核主动发起IO,用户进程被动接收.
同步阻塞IO
用户进程/线程主动发起IO后,需要等待内核IO完成操作才能返回用户空间执行指令.用户从发起调用开始,直到数据到达并复制到用户缓冲区为止,整个过程都是阻塞的,用户程序不能做其他的事。
特点:发起IO的用户线程被阻塞了。
优点:开发简单,用户线程被阻塞期间不占用CPU。
缺点:一个线程维护一个连接,并发量大时内存,线程开销巨大。
同步非阻塞IO
用户进程/线程主动发起IO后,不需要等待内核操作完成即可返回用户空间执行指令.由于非阻塞,第一次发起系统调用后,数据可能未到达,此时会立刻返回。所以需要用户程序不断发起系统调用询问内核数据是否已准备好。
特点:用户线程不断进行IO系统调用轮询数据是否准备好,没准备好就继续轮询,直到IO调用完成。
优点:进行IO调用时,如果内核没有数据则不会阻塞线程,时实性好。
缺点:需要不断进行轮询内核,占用大量CPU时间。
异步阻塞IO
异步阻塞IO经典的用例是IO多路复用.由内核监控socket的状态,一旦有socket就绪(可读/可写),内核将socket连接返回给用户,用户根据socket连接进行IO调用.首先需要将所有的Socket交由操作系统的select/epoll选择器中。在Java中对应的是Selector类。再轮询select/epoll选择器,获取已就绪的Socket。用户程序根据就绪的Socket列表发起系统调用,用户程序会被阻塞。直到内核将数据复制到用户缓冲区中为止才会解除阻塞。
特点:涉及两种系统调用,一种是查询socket状态的系统调用,比如select/epoll,另一种是IO调用。需要操作系统提供IO多路复用支持。
优点:一个线程管理成千上万个socket连接。
缺点:select/epoll调用时是阻塞的,IO调用时也是阻塞的。
异步非阻塞IO
用户将IO事件的IO操作完成后的回调函数注册到内核,由内核主动调用,并将数据复制到用户缓冲区,通知用户直接使用.内核在等待内核缓冲区中数据到达,和将数据从内核缓冲区中复制到用户缓冲区中都不会阻塞用户程序。
优点:内核准备数据和复制数据不是阻塞的
缺点:需要操作系统支持