经常会看到IO模型,同步阻塞,同步非阻塞,异步阻塞,异步非阻塞等等,看起来很晕。
《UNIX网络编程:卷一》第六章 ,看看IO模型的本质。
- Socket与文件描述符的关系
- 进程的五状态模型
- 用户态与内核态的区别
Socket与文件描述符
-
socket:
socket是一种网络通信的抽象,它允许进程通过网络进行通信。socket提供了一种机制,使进程能够通过网络连接到其他进程,并在网络上进行数据传输。在编程中,socket通常通过操作系统提供的套接字API(比如POSIX套接字或windows套接字)来创建和使用。这些套接字API允许程序员创建套接字、绑定地址、监听连接请求,接收连接、发送和接收数据等。套接字通常用于网络通信,例如TCP和UDP连接,用于构建客户端和服务器应用程序。 -
文件描述符:
文件描述符是操作系统内核用来标识打开文件或其他 I/O 设备的整数。在 Unix-like 操作系统中,文件、套接字、管道等都被抽象为文件描述符。文件描述符的范围通常是 0 到 ulimit -n,其中 0 表示标准输入(stdin),1 表示标准输出(stdout),2 表示标准错误(stderr),而其他文件描述符用于表示打开的文件、套接字等。文件描述符在进程之间可以继承、共享和传递。这允许一个进程在创建子进程时,将已打开的文件或套接字传递给子进程。文件描述符在编程中用于进行 I/O 操作,如读取或写入文件、读取或写入套接字等。
两者之间的关系:
在 Unix-like 操作系统中,Socket 也可以被表示为文件描述符。这意味着你可以使用与文件 I/O 相同的系统调用来进行套接字 I/O。例如,你可以使用 read() 和 write() 等系统调用来读取和写入套接字,就像操作文件一样。通过文件描述符,你可以将套接字的输入和输出重定向到文件,也可以将文件的内容写入套接字。这种能力使得在网络编程中可以将套接字与文件操作结合起来,实现数据的读写和处理。
Socket 是网络通信的一种抽象,而文件描述符是一种通用的 I/O 抽象,它们可以用于操作文件、套接字和其他 I/O 设备,并且在某些情况下,它们可以交叉使用。
进程的五状态模型
进程的五状态模型是操作系统中描述进程状态的一种经典模型:
-
创建(New): 进程被创建时处于这个状态。在这个阶段,操作系统正在为进程分配资源,初始化进程的数据结构等。创建状态通常是非常短暂的,一旦进程准备好运行,它就会进入就绪状态。
-
就绪(Ready): 进程已经准备好运行,但由于操作系统调度算法或其他原因,暂时没有执行。在就绪状态下的进程通常等待着分配给它的 CPU 时间片。多个进程可能同时处于就绪状态,等待 CPU 时间。
-
运行(Running): 进程被分配到 CPU 并且正在执行。在运行状态下的进程正在消耗 CPU 时间,执行它的指令。操作系统会根据调度算法决定哪个进程应该在某一时刻运行。
-
阻塞(Blocked): 进程在某种条件下停止执行,通常是因为等待某个事件的发生,比如等待磁盘 I/O 完成、等待用户输入等。当事件发生时,进程将从阻塞状态转换为就绪状态,然后可以继续执行。
-
终止(Terminated): 进程已经执行完毕或被终止。在这个状态下,进程的资源被释放,包括内存、文件描述符等。终止状态通常是进程的最终状态。
这个五状态模型有助于操作系统在多任务环境中管理和调度进程。操作系统的调度器负责在这些状态之间转换进程,以便合理分配 CPU 时间,确保系统的高效性和公平性。此外,操作系统可以通过监控进程的状态来处理异常情况,例如进程崩溃或无响应。不同的操作系统可能会在模型中添加其他状态或细分这些状态以满足其需求,但通常都基于这个基本模型构建。
用户态与内核态
用户态(User Mode)和内核态(Kernel Mode)是操作系统中两种不同的运行级别或权限级别。这两种模式之间的主要区别在于进程对计算机硬件和操作系统资源的访问权限以及对指令的执行能力。
-
权限级别: 用户态:在用户态中,进程的权限受到限制,通常只能访问自己的内存空间和被允许的系统资源。用户态进程不能直接访问硬件设备或执行特权指令。
内核态:内核态具有更高的权限,允许操作系统内核访问系统的所有资源,包括硬件设备和系统内存。内核态可以执行特权指令,例如修改页表、控制中断等。 -
系统调用: 用户态:当用户态的进程需要执行特权操作(例如文件读写、网络通信、创建新进程等),它必须通过系统调用(System Call)请求内核来执行这些操作。系统调用是用户态和内核态之间的接口。
内核态:内核态中的操作系统内核可以直接执行这些特权操作,而无需系统调用。 -
中断和异常处理: 用户态:在用户态中,进程通常不能直接响应硬件中断或异常。当硬件发生中断或异常时,操作系统内核会接管控制并处理,然后可能唤醒相关的用户态进程。
内核态:内核态中的操作系统内核可以直接处理硬件中断和异常,包括时钟中断、硬件故障、设备IO完成等。内核可以执行必要的操作,然后选择继续执行当前进程或切换到其他进程。 -
性能开销: 用户态:用户态的进程之间的切换通常比较快速,但涉及到系统调用时,会有一定的性能开销。
内核态:内核态的进程之间的切换可能更为昂贵,因为涉及更高的权限级别和更多的上下文切换。
用户态和内核态之间的切换是操作系统的核心机制,有助于保护系统的稳定性和安全性,同时允许用户程序执行必要的系统操作。
IO的流程
在区分各种IO模型的区别之前,我们需要先了解一个IO,在操作系统的层面究竟发生了哪些事情。注意,我们说的IO模型对IO的优化,都是基于读IO。
假设我们需要等待Socket的数据,也就是说当前是一个读操作的IO,那么在操作系统层面需要分为两步:
- 等待数据被写入Socket的缓冲区中
- 将Socket缓冲区中的数据拷贝到应用程序中
我们可以很容易的发现,第一步是由对方决定的,对方决定了什么时候把数据发送过来。第二步是由操作系统决定的,操作系统需要陷入内核态拷贝数据。
而我们的同步异步、阻塞非阻塞也是从这里出发的。
这里先提一下,阻塞和非阻塞指的是第一个阶段,进程等待缓冲区被写入数据的方式;同步和异步指的是第二个阶段,进程需不需要等待操作系统把数据从缓冲区拷贝到应用中。
五种IO模型
阻塞
在同步阻塞的IO模型中,在第一阶段“等待数据被写入Socket的缓冲区中”,操作系统会把当前的进程设置为阻塞状态,直到缓冲区被写入数据这个进程才被唤醒。
这也就造成了一个问题,当操作系统把这个进程置为阻塞态的时候,这个进程就什么事都做不了了。
非阻塞
为了解决“同步阻塞”进程可能无期限阻塞的情况,于是产生了“同步非阻塞”。
在这种IO模型中,如果发现这个Socket里面没有准备好的数据就返回一个错误,而不是把这个进程设置为阻塞状态。
也就是说我们可以轮询这个Socket查看有无准备好的数据。
多路复用
假设此时我们的服务器需要管理很多的IO请求,如果给每一个IO都分配一个进程/线程,自旋的等待有无数据到来,无疑是很浪费资源的。
如果我们用一个进程,轮询所有的IO请求,又会使IO的响应变得很慢。
因此,产生了多路复用IO。
多路复用IO就是用一条线程,同时监听多个IO请求,并且在有IO请求产生的时候返回。
注意,虽然我们的IO多路复用也会阻塞,但是这里的阻塞是应用层面的,也就是说在多路复用的方法上进行阻塞,而不是在操作系统层面去阻塞。
信号驱动
在以上的IO模型中,都是需要应用自己去“询问”Socket是否已经准备好了数据,而信号驱动就是由Socket“主动”告诉你有没有准备好数据。
这么做的好处在于第一阶段程序不需要任何的等待与轮询,只需要在收到了信号通知之后去处理这个IO即可。
异步IO
在以上的四种IO操作中,无论第一个阶段是如何进行优化的,在第二阶段都需要陷入内核态来拷贝数据。
异步IO就是为了解决这个问题。
异步IO在解决了阻塞这个问题后,同样把陷入内核态来拷贝数据的时间也节省了。
这样,当程序需要进行IO的时候,只需要发出IO的请求,然后就可以继续执行后面的代码,直到IO完成操作系统会通知程序。
同样的,异步IO是非阻塞的。
总结
阻塞跟非阻塞,是在等待数据这个阶段的;同步跟非同步,指的是需不需要等待CPU进行数据的拷贝。