1.IO读写基本原理与IO模型

2023年 7月 18日 47.6k 0

IO读写基本原理
什么是用户态和内核态

为了避免用户进程直接操作内核,保证内核安全,操作系统将内存寻址空间划分为两部分:

内核空间[Kernel-space] :供内核程序使用。

用户空间[User-space] :供用户进程使用。

为什么分用户态和内核态:安全

操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

为了保证内核的安全,操作系统一般都强制用户进程不能直接操作内核。内核空间和用户空间是隔离的,即使用户的程序崩溃了,内核也不受影响。

具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。

在linux系统中内核模块运行在内核空间,对应的进程处于内核态。用户模块运行在用户空间,对应的进程处于用户态。

类似于超级管理员权限和普通权限。如果把超级管理员的权限给了普通人。普通人执行了rm -f ,那不就完了吗?

用户态如何切换内核态:系统调用

内核态:cpu可以访问内存的所有数据,包括外围设备比如磁盘。

用户态:只能受限的访问内存,且不允许访问外围设备。

每个应用程序都有一个单独的用户空间,对应的进程处于用户态,用户态进程不能访问内核空间的数据,也不能调用内核函数。

应用程序不允许直接在内核空间区域进行读写也不允许直接调用内核代码定义的函数。

内核态进程可以执行任意命令,调用系统的一切资源,用户态进程只能执行简单的计算。不能调用系统资源。

因此需要将进程切换到内核态才可以进行系统调用。

那么问题来了,用户态如何执行系统调用的呢?

答案是用户态进程必须通过系统调用来向内核发出命令,完成调用系统资源之类的操作。

假设没有这种内核态和用户态之分,程序随随便便就能访问硬件资源,比如说分配内存,程序能随意的读写所有的内存空间,如果程序员一不小心将不适当的内容写到了不该写的地方,就很可能导致系统崩溃。

用户程序是不可信的,不管程序员是有意的还是无意的,都很容易将系统干到崩溃。

在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。

如果所有的程序都能使用这些指令,那么你的系统一天死机n回就不足为奇了。

所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。

Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。

linux的内核是一个有机的整体。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。

当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。

即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。

因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

什么是系统调用?

Linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。从某种角度来看,系统调用和普通的函数调用非常相似。区别仅仅在于,系统调用由操作系统核心提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态。

随Linux核心还提供了一些C语言函数库,这些库对系统调用进行了一些包装和扩展,因为这些库函数与系统调用的关系非常紧密,所以习惯上把这些函数也称为系统调用。

为什么要用系统调用?

实际上,很多已经被我们习以为常的C语言标准函数,在Linux平台上的实现都是靠系统调用完成的,所以如果想对系统底层的原理作深入的了解,掌握各种系统调用是初步的要求。进一步,若想成为一名Linux下编程高手,也就是我们常说的Hacker,其标志之一也是能对各种系统调用有透彻的了解。

即使除去上面的原因,在平常的编程中你也会发现,在很多情况下,系统调用是实现你的想法的简洁有效的途径,所以有可能的话应该尽量多掌握一些系统调用,这会对你的程序设计过程带来意想不到的帮助。

系统调用是怎么工作的?

一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作"保护模式")。系统调用是这些规则的一个例外。其原理是进程先用适当的值填充寄存器,然后调用一个特殊的指令,这个指令会跳到一个事先定义的内核中的一个位置(当然,这个位置是用户进程可读但是不可写的)。在Intel CPU中,这个由中断0x80实现。硬件知道一旦你跳到这个位置,你就不是在限制模式下运行的用户,而是作为操作系统的内核--所以你就可以为所欲为。

进程可以跳转到的内核位置叫做sysem_call。这个过程会检查系统调用号,这个号码告诉内核进程请求哪种服务。然后,它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。接着,就调用函数,等返回后,做一些系统检查,最后返回到进程(或到其他进程,如果这个进程时间用尽)。

Read&&Write

在计算机中无时无刻不存在着对数据的访问和读取(数据都存储在物理的媒介上,例如寄存器,高速缓存,内存,磁盘,网卡等等),这些操作被称为IO。

无论是Socket的读写还是文件的读写,在Java层面的应用开发或者是linux系统底层开发,都属于输入input和输出output的处理,简称为IO读写。

在原理上和处理流程上,都是一致的。区别在于参数的不同。

用户程序进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统,名称不完全一样,但是功能是一样的。

image.png

先强调一个基础知识:

read系统调用,并不是把数据直接从物理设备,读数据到内存。

write系统调用,也不是直接把数据,写入到物理设备。

read系统调用,是把数据从内核缓冲区复制到用户缓冲区;

write系统调用,是把数据从用户缓冲区复制到内核缓冲区。

这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成的。

说到 IO 模型的时候,我们先说说绕不开的内核缓冲区。

缓冲区的目的,是为了减少频繁地与设备之间的物理交换,我们知道,外部设备的直接读写,会涉及到操作系统的中断,发生和结束系统中断的时候,需要保存和恢复进程数据等信息,为了减少这种底层系统的时间损耗和性能损耗,于是出现了内存缓冲区。

在 linux 操作系统中,操作系统内核只有一个内核缓冲区,但是每个用户进程都有自己的缓冲区,叫做进程缓冲区。

有了这两个概念,再来说下 read 和 write 操作。

read 系统调用。并不是直接从物理设备把数据读取到内存中,是把数据从内核缓冲区复制到用户进程缓冲区。 write 系统调用。也不是直接把数据写入到物理设备,是把数据从进程缓冲区复制到内核缓冲区。

所以,上层程序的 IO 操作,实际上不是物理设备级别的读写,而是缓存的复制。数据在内核缓冲区和物理设备之间的交换,是由操作系统内核来完成的。

image.png

I/O 模型基本说明

什么是IO?

在计算机中无时无刻不存在着对数据的访问和读取(数据都存储在物理的媒介上,例如寄存器,高速缓存,内存,磁盘,网卡等等),这些操作被称为IO。

同步、异步:发起I/O请求后会不会阻塞用户进程

同步:是指在发起I/O请求后,应用程序等到I/O操作的结果才能继续执行下去。

异步:是指在发起I/O请求后,应用程序就可以去执行其他任务,不需要等待I/O操作的结果。

同步:指进程触发IO操作后会阻塞用户进程,用户等待请求数据到达的过程。

异步:指进程触发IO操作后不会阻塞用户进程。两者区别,进程触发IO操作是否会阻塞用户进程,用户的cup此时是否释放。

所以同步异步针对的是用户进程。

同步IO与异步IO的区别在于当内核准备好数据之后在真正读取数据的时候用户线程是否被阻塞。

非阻塞IO虽然在用户发起请求时会立即返回,但是当内核准备好数据之后,在步骤2非阻塞IO依然需要用户线程发起请求才会将数据从内核空间拷贝到用户空间,因此非阻塞IO属于同步IO。

阻塞、非阻塞

阻塞:是指在拿到I/O操作结果的过程中进程是处于挂起状态。

非阻塞:是指在拿到I/O操作结果的过程中进程是处于运行状态。

阻塞: 指用户进程触发IO操作后,触发read()等待内核将数据copy到用户空间的过程(select 也是一种阻塞IO)。

非阻塞: 指用户进程触发IO操作后,触发read(),系统内核会立刻返回成功,当read响应完成,系统内核会产生一个信号或者基于一个线程回调函数完成IO过程。

两者区别,触发read()操作后,系统内核是否会立刻返回状态还是等待。

所以阻塞非阻塞是针对操作系统内核。

阻塞IO与非阻塞IO的区别在于内核线程在执行IO操作时是否立即返回结果,若立即返回则为非阻塞IO,反之则为阻塞IO。

★阻塞IO:同步

指的是需要内核IO操作彻底完成后才能返回用户空间继续执行用户的操作,阻塞指的是用户空间程序的执行状态,用户空间程序需等到内核IO操作彻底完成。

  • 当用户线程发起IO请求后,会进行系统调用(system call)来让内核(Kernel)进行IO操作
  • 此时用户线程阻塞,等待内核将数据准备好
  • 内核将数据准备好后会将数据从内核空间拷贝到用户空间,并返回给用户线程结束阻塞。

image.png

进程调用recvfrom,从用户态转到内核态,直到数据准备好且拷贝到应用程序缓冲区或者出错(最常见的错误是信号中断)才会返回。

我们所说的进程阻塞的整段时间是从调用recvfrom开始到数据拷贝完成这段时间。

当进程返回成功时,应用程序就开始处理数据了。

★非阻塞IO:同步

指的是用户程序不需要等待内核IO操作完成后,内核立即返回给用户一个状态值,用户空间无需等到内核的IO操作彻底完成,可以立即返回用户空间,执行用户的操作,处于非阻塞的状态,非阻塞需要一直轮询。

  • 由用户线程发起IO请求, 进行系统调用来让内核进行IO操作
  • 此时如果内核没有准备好数据则会直接返回error,并不会阻塞用户线程,用户线程可以重复的发起IO请求
  • 当用户线程发起请求并且内核已经将数据准备好后,会将数据从内核空间拷贝到用户空间(这个过程是需要阻塞用户线程的),返回给用户

image.png

当一个应用程序重复对一个非阻塞描述符调用recvfrom时,我们称此过程为轮询。当系统调用没有期待的操作发生的时候,内核立即返回一个错误。应用程序不断地查询内核,看看某操作是否准备好,这样子对CPU时间是极大的浪费。

当操作准备好也就是数据报准备好的时候,将数据报拷贝到应用缓冲区这一段时间依旧是阻塞的。

★多路复用IO

  • 用户线程调用select后进行系统调用(内核会监视所有select负责的socket),此时用户线程被阻塞
  • 当内核将数据准备好后就会返回,并通知用户线程进行读取操作,此时内核将数据拷贝到用户空间并返回

image.png

应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。I/O复用本身阻塞的,他们能提高程序运行的效率的原因在于他们具有同时监听多个I/O事件的能力。也就是说如果只有一个I/O事件时,复用模型的效率跟阻塞模型基本一样。 常用的I/O复用函数是select,poll,调用select或poll,在这个两个系统调用中的某一个上阻塞,而不是而不阻塞于真正的系统调用。 以select为例,我们阻塞于select调用,等待数据报套接口可读。当select返回可读调用时,调用recvfrom将数据拷贝到应用程序缓冲区。

★异步IO

  • 用户线程进行aio_read,进行系统调用切换到内核
  • 内核立即返回,并不会阻塞用户线程
  • 内核准备好数据后会将数据从内核空间拷贝到用户空间并通知用户线程操作已完成

image.png

用户可以直接对I/O执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及I/O操作完成之后内核通知应用程序的方式。

异步I/O的读写操作总是立即返回,而不论I/O是否阻塞,因为真正的读写操作已由内核接管。

上面实例中,假设要求内核在完成操作后生成一个信号,此信号直到数据已拷贝到应用缓冲区才产生,通知用户已经处理完事件。

阻塞IO与非阻塞IO

阻塞IO:用户线程发起IO操作,紧接着由内核线程来执行IO操作,在阻塞IO中内核线程并不会立即返回而是等待数据拷贝到内存空间时才返回,在此期间用户线程处于阻塞状态。

非阻塞IO: 与阻塞IO不同,内核线程在执行IO操作后会立即返回,若结果为error则用户线程可以重新发起请求而不会被阻塞,一旦内核将数据准备好了且用户线程发起了IO请求那么将数据拷贝到用户空间。

IO操作大致分为两个部分:

1.用户线程发起IO请求时,内核未准备好数据

2.用户线程发起IO请求时,内核已准备好数据

阻塞IO与非阻塞IO步骤2是相同的,区别在于步骤1。

阻塞IO与非阻塞IO的区别在于内核线程在执行IO操作时是否立即返回结果,若立即返回则为非阻塞IO,反之则为阻塞IO。

同步IO与异步IO

同步异步是用户空间与内核空间的调用发起方式。

同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。

异步IO则反过来,是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。

异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间线程是变成被动接受的,内核空间是主动调用者,即回调。

异步IO: 一方面,用户线程发起IO操作后,可以立即去做其他事情,另一方面,对于内核线程当它收到异步读取之后会立即返回,不会对用户线程造成阻塞。当内核将数据准备好之后会将数据从内核空间拷贝到用户空间,内核会发送给用户一个信号通知用户IO操作已完成。

同步IO: 同步IO的关键在于在真正读取数据(也就是上面提到的步骤2)的时候用户线程是否被阻塞。非阻塞IO虽然在用户发起请求时会立即返回,但是当内核准备好数据之后,在步骤2非阻塞IO依然需要用户线程发起请求才会将数据从内核空间拷贝到用户空间,因此非阻塞IO属于同步IO。

阻塞I/O,非阻塞I/O,I/O复用,信号驱动I/O都属于同步I/O。

总结

用户进程发起请求从内核中获取数据,有两种情况:

  • 操作系统还没有准备后数据,那么这时候怎么办,有两种办法:

1.让用于进程等着,进程是处于挂起状态,这就是阻塞。

2.如果没有数据就返回一个ERROR,不需要用户进程干等,进程是处于运行状态,这就是非阻塞。

  • 过了一会儿操作系统准备好数据了,也有两种办法:

1.啥也不管,等着用户进程再次来请求才把数据给它,这就是同步

2.负责到底,数据准备好,给到用户进程并发出一个信号告诉用户进程数据已准备好,这就是异步

同步:是指在发起I/O请求后,应用程序等到I/O操作的结果才能继续执行下去。

异步:是指在发起I/O请求后,应用程序就可以去执行其他任务,不需要等待I/O操作的结果。

阻塞:是指在拿到I/O操作结果的过程中进程是处于挂起状态。

非阻塞:是指在拿到I/O操作结果的过程中进程是处于运行状态。

参考:blog.csdn.net/weixin_4430…

参考:blog.csdn.net/shanxiaoshu…

参考:blog.csdn.net/yxtxiaotian…

参考:blog.csdn.net/god8816/art…

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论