IO读写基本原理

时间:2023-5-1 13:53     作者:林立     分类: 作品


用户态与内核态

  为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为两部分:一部分是内核空间(Kernel-Space),另一部分是用户空间(User-Space)。在Linux系统中,内核模块运行在内核空间,对应的进程处于内核态;用户程序运行在用户空间,对应的进程处于用户态。
  操作系统的核心是内核程序,它独立于普通的应用程序,既有权限访问受保护的内核空间,也有权限访问硬件设备,而普通的应用程序并没有这样的权限。内核空间总是驻留在内存中,是为操作系统的内核保留的。应用程序不允许直接在内核空间区域进行读写,也不允许直接调用内核代码定义的函数。每个应用程序进程都有一个单独的用户空间,对应的进程处于用户态,用户态进程不能访问内核空间中的数据,也不能直接调用内核函数,因此需要将进程切换到内核态才能进行系统调用。
  内核态进程可以执行任意命令,调用系统的一切资源,而用户态进程只能执行简单的运算,不能直接调用系统资源,那么问题来了:用户态进程如何执行系统调用呢?答案是:用户态进程必须通过系统调用(System Call)向内核发出指令,完成调用系统资源之类的操作。
  用户程序进行IO的读写依赖于底层的IO读写,基本上会用到底层的read和write两大系统调用。虽然在不同的操作系统中read和write两大系统调用的名称和形式可能不完全一样,但是它们的基本功能是一样的。
  操作系统层面的read系统调用并不是直接从物理设备把数据读取到应用的内存中,write系统调用也不是直接把数据写入物理设备。上层应用无论是调用操作系统的read还是调用操作系统的write,都会涉及缓冲区。具体来说,上层应用通过操作系统的read系统调用把数据从内核缓冲区复制到应用程序的进程缓冲区,通过操作系统的write系统调用把数据从应用程序的进程缓冲区复制到操作系统的内核缓冲区。
  简单来说,应用程序的IO操作实际上不是物理设备级别的读写,而是缓存的复制。read和write两大系统调用都不负责数据在内核缓冲区和物理设备(如磁盘、网卡等)之间的交换。这个底层的读写交换操作是由操作系统内核(Kernel)来完成的。所以,在应用程序中,无论是对socket的IO操作还是对文件的IO操作,都属于上层应用的开发,它们在输入(Input)和输出(Output)维度上的执行流程是类似的,都是在内核缓冲区和进程缓冲区之间进行数据交换。

内核缓冲区与进程缓冲区

  缓冲区的目的是减少与设备之间的频繁物理交换。计算机的外部物理设备与内存和CPU相比,有着非常大的差距,外部设备的直接读写涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,结束中断之后,还需要恢复之前的进程数据和状态等信息。为了减少底层系统的频繁中断所导致的时间损耗、性能损耗,出现了内核缓冲区。
  操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,通过这种机制来提升系统的性能。至于具体什么时候执行系统中断(包括读中断、写中断)则由操作系统的内核来决定,应用程序不需要关心。
  上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到应用的缓冲区(进程缓冲区);上层应用使用write系统调用时,仅仅把数据从应用的缓冲区复制到内核缓冲区。
  内核缓冲区与应用缓冲区在数量上也不同。在Linux系统中,操作系统内核只有一个内核缓冲区。每个用户程序(进程)都有自己独立的缓冲区,叫作用户缓冲区或者进程缓冲区。在大多数情况下,Linux系统中用户程序的IO读写程序并没有进行实际的IO操作,而是在用户缓冲区和内核缓冲区之间直接进行数据的交换。
  用户程序所使用的系统调用read和write并不是使数据在内核缓冲区和物理设备之间交换:read调用把数据从内核缓冲区复制到应用的用户缓冲区,write调用把数据从应用的用户缓冲区复制到内核缓冲区。

  以read系统调用为例,看一下一个完整输入流程的两个阶段:1.应用程序等待数据准备好。2.从内核缓冲区向用户缓冲区复制数据。
  如果是读取一个socket(套接字),那么以上两个阶段的具体处理流程:1.第一个阶段,应用程序等待数据通过网络到达网卡,当所等待的分组到达时,数据被操作系统复制到内核缓冲区中。这个工作由操作系统自动完成,用户程序无感知。2.第二个阶段,内核将数据从内核缓冲区复制到应用的用户缓冲区。
  再具体一点,如果是在Java客户端和服务端之间完成一次socket请求和响应(包括read和write)的数据交换,其完整的流程如下:
  1.客户端发送请求:Java客户端程序通过write系统调用将数据复制到内核缓冲区,Linux将内核缓冲区的请求数据通过客户端机器的网卡发送出去。在服务端,这份请求数据会从接收网卡中读取到服务端机器的内核缓冲区。
  2.服务端获取请求:Java服务端程序通过read系统调用从Linux内核缓冲区读取数据,再送入Java进程缓冲区。
  3.服务端业务处理:Java服务器在自己的用户空间中完成客户端的请求所对应的业务处理。
  4.服务端返回数据:Java服务器完成处理后,构建好的响应数据将从用户缓冲区写入内核缓冲区,这里用到的是write系统调用,操作系统会负责将内核缓冲区的数据发送出去。
  5.发送给客户端:服务端Linux系统将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议将数据发送给目标客户端。