文章目录
- 用户态和内核态
- 操作系统角度的IO
- IO模型
- 阻塞IO
- 非阻塞IO(NIO)
- IO多路复用
- select 、poll和epoll
用户态和内核态
用户态也叫用户空间,内核态也叫内核空间。
操作系统为了保护系统的安全,都会划分出内核空间和用户空间。简单来说用户空间是指没有特权的应用程序,访问系统资源时受到极大的限制,比如对系统中的文件进行读写操作时,都会切换到内核态,例如我们编写程序、APP、客户端软件等都是用户态程序。而内核态就是权限较大的进程,可以真正去操作磁盘。
内核态运行操作系统程序,操作硬件,用户态运行用户程序;当程序运行在3级特权级上时,可以称之为运行在用户态,当程序运行在0级特权级上时,称之为运行在内核态。
操作系统角度的IO
我们将内存中的数据写入到磁盘的话,操作系统负责计算机的资源管理和进行的调度,我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊的操作,如磁盘文件读写,内存的读写等。
真正的IO是操作系统执行的,应用程序的IO分为两个动作:IO调用,IO执行.IO调用是由进行发起的,而IO执行是操作系统内核的工作。
IO模型
主要区分就在于内核态进程遇到数据没有准备好时,处理的方式。
等待——阻塞IO,立即返回——非阻塞IO,
阻塞IO
当一个用户态进程需要读取磁盘上的某个数据时,需要调用内核对外提供的指令或者是函数,调用之后,如果现在数据还没有准备好,内核就等待,直到数据准备好了并返回给内核空间之后再将数据返回给用户态进程。
谁阻塞?用户进程
非阻塞IO(NIO)
当内核进程访问数据没有准备好时,立即返回一个失败信息,告诉用户进程没有准备好,用户进程不断的重新调用数据,也就是不断的问内核准备好了没有,在这段时间内核在后台会持续尝试将硬件上的数据读取到缓冲区。在内核准备的这段时间用户进程不会阻塞,但是会不停的访问。直达某一刻,数据准备好了,也到达内核了,访问不在报错,尝试拷贝并返回数据。
非阻塞IO的第一阶段:等待数据,的时候用户进程不是阻塞状态;而第二阶段:从内核拷贝数据到用户空间时,用户进程是阻塞状态的。
IO多路复用
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,主要区别在于无数据时的处理方案:
如果恰好没有数据时,阻塞IO会使进程阻塞,非阻塞IO会使CPU空转,都不能充分发挥CPU的作用。
比如服务器端处理客户端Socket请求时,在单线程情况下,只能依次处理一个Socket,如果正在处理的Socket恰好未就绪(数据不可读或者写),线程就会阻塞,其他所有的客户端Socket都必须等待(即使其他的Socket已经准备就绪了),性能自然差。
与一个生活中例子十分相似:
去店里排队买饭的时候,正在买饭的那个人有选择困难症,半天不知道吃啥,后面的人即使已经想好了要吃啥也必须在那干等。
解决方法:
- 增加服务员(多线程)。缺点:开销大(CPU性能随着线程的增加而降低)。
- 不排队,谁想好了(数据就绪),服务员就给谁点餐(用户应用就去读取数据)。线程监听所有到达服务器的Socket任意一个Socket准备就绪就赶紧处理它。问题如何知道是否准备就绪?使用到了FD(文件描述符),是一个从0开始递增的无符号整数,用来关联一个文件。
多路复用就是利用单线程来同时监听多个FD,并在某个FD可读、可写时通知,从而避免无效等待,充分利用CPU资源。
select 、poll和epoll
select
先调用select函数,select内部可以接收多个fd,也就是可以吧每个客户端的Socket对应的FD都可以传给select函数,然后传入到内核,内核检查这些FD是否有准备就绪的,只要有一个准备就绪的,就调用recvfrom函数处理。如果都没有就绪,会等待。注意:select函数返回后,是通过遍历fdset,找到就绪的描述符FD,因为它并不知道是哪一个Socket准备好了。
recvfrom是只能监听一个FD,select一次可以监听多个FD。
poll
select监听的IO最大连接有限,在Linux系统上一般为1024。为了解决数量限制提出了poll。
epoll
poll和select只会通知用户进行有就绪FD,但是不确定具体哪个FD,需要进程逐个遍历来确认。
epoll会通知用户进程FD就绪时,把已经就绪的FD写入用户空间中,用户应用就可以直接处理准备就绪的Socket。