Linux内核空间野生程序员的理解与理解(一)

什么是IO

在计算机操作系统中,所谓的I/O就是输入(Input)和输出(Output),也可以理解为读(Read)和写(Write)。针对不同的对象,I/O模式可以分为磁盘IO模型和网络IO模型。

IO操作会涉及到用户空间和内核空间的转换。首先,了解以下规则:

我们来看看所谓的读写操作:

用户空间和内核空间

野程序员可能对这个概念比较陌生,其实是Linux操作系统中的一个概念。虚拟内存(操作系统中的一个概念,对应物理内存)被操作系统分为两部分:User Space(用户空间和Kernel Space(内核空间),本质上是不划分计算机的物理内存分成这些,只是在操作系统启动后,地址和空间范围在逻辑上和虚拟上被划分了。

操作系统将为每个进程分配一个独立的、连续的虚拟内存地址空间(可能在物理上不连续)。以32位操作系统为例,大小一般为4G,即232。其中高地址值的内存空间分配给系统内核(网上资料:Linux下1G,Windows下2G) ,其余的内存地址空间分配给用户进程。

由于我们不是试图深入学习操作系统,这里有一个 32 位系统的示例来帮助您理解原理。32位LINux操作系统下,0~3G是用户空间,3~4G是内核空间:

那么为什么要这样划分空间呢?

这也很容易理解。毕竟操作系统有高贵的身份,玩用户应用太重要了。他们各自的数据必须分开存储,严格控制权限,不能越界。只有这样,才能保证操作系统的稳定运行。用户应用程序太不可控,可以由不同的公司或个人开发。万一误操作或恶意破坏系统数据,系统将崩溃并被淘汰。隔离后,如果应用挂了,可以挂掉,操作系统可以正常运行。

简单来说,内核空间是操作系统内核代码运行的地方,用户空间是用户程序代码运行的地方。当应用进程执行系统调用并陷入内核代码执行时,处于内核态,而当应用进程运行用户代码时,处于用户态。

同时内核空间可以执行任意命令,而用户空间只能进行简单的操作,不能直接调用系统资源和数据。操作系统必须提供一个接口来向系统内核发送指令。

一旦调用了系统接口,应用程序进程就会从用户空间切换到内核空间,因为内核代码开始运行。

只看几行代码,分析一下应用程序在用户空间和内核空间之间的切换过程:

str = "i am qige" // 用户空间
x = x + 2
file.write(str) // 切换到内核空间
y = x + 4 // 切换回用户空间

在上面的代码中,第一行和第二行是在用户空间执行的简单赋值操作。第三行需要写一个文件,所以需要切换到内核空间,因为用户不能直接写文件,必须由内核安排。第四行又是赋值操作,又切换回用户空间。

从用户模式切换到内核模式有 3 种方式:

在上述三种方法中,除了系统调用由进程发起外,异常和外围设备中断都是被动切换的。

要查看用户空间和内核空间之间的 CPU 时间分配情况,请使用 top 命令。它的第三行输出是 CPU 时间分配统计信息。

我们来看看图中圈出的三个CPU使用率指标:

其中,第一项 7.57% user 是 CPU 在 User Space 中消耗的时间百分比,第二项 7.0% sys 是在 Kernel Space 中花费的时间百分比。第三项 85.4% idle 是 CPU 在空闲进程中花费的时间百分比。值越低,CPU 越忙。

PIO&DMA

大家都知道,我们的数据一般是存储在磁盘上的,而应用程序想要读写这些数据就必须加载到内存中。接下来,我将介绍PIO和DMA、两个IO设备和内存之间的数据传输方法。

PIO 的工作原理

用户进程通过read等系统调用接口向操作系统(即CPU)发送IO请求,请求将数据读入自己的用户内存缓冲区,然后进程进入阻塞状态。操作系统收到用户进程的请求后,进一步向磁盘发送IO请求。磁盘驱动程序收到内核的IO请求后,将数据读入自己的缓冲区,此时不占用CPU。当磁盘缓冲区已满时,它会向内核发送一个中断信号,通知自己缓冲区已满。内核接收到来自磁盘的中断信号,并使用CPU将磁盘缓冲区中的数据复制到内核缓冲区中。如果内核缓冲区中的数据小于用户请求的数据,重复步骤2、3、 4 直到内核缓冲区中的数据满足用户的要求。内核缓冲区中的数据已经满足用户要求,CPU停止向磁盘请求IO。CPU 将数据从内核缓冲区复制到用户缓冲区并从系统调用返回。用户进程读取数据后,继续执行原来的任务。

PIO的缺点:每个IO请求都需要CPU多次参与,效率很低。

DMA 的工作原理

DMA(直接内存访问,直接内存访问)。

用户进程通过read等系统调用接口向操作系统(即CPU)发送IO请求,请求将数据读入自己的用户内存缓冲区,然后进程进入阻塞状态。操作系统收到用户进程的请求后,进一步向DMA发送IO请求,然后CPU就可以做其他事情了。DMA 将 IO 请求转发到磁盘。磁盘驱动程序收到内核的IO请求后,将数据读入自己的缓冲区。当磁盘缓冲区已满时,它会向 DMA 发送一个中断信号,通知自己缓冲区已满。DMA接收到来自磁盘驱动器的信号,将磁盘缓冲区中的数据复制到内核缓冲区,此时不占用CPU(PIO这里占用CPU)。如果内核缓冲区中的数据小于用户请求的数据,重复步骤3、4、5,直到内核缓冲区中的数据满足用户要求。内核缓冲区中的数据已经满足用户要求,DMA停止向磁盘发送IO请求。DMA 向 CPU 发送中断信号。CPU收到DMA信号,知道数据准备好了,于是将数据从内核空间复制到用户空间,系统调用返回。用户进程读取数据后,继续执行原来的任务。DMA 向 CPU 发送中断信号。CPU收到DMA信号,知道数据准备好了,于是将数据从内核空间复制到用户空间,系统调用返回。用户进程读取数据后,继续执行原来的任务。DMA 向 CPU 发送中断信号。CPU收到DMA信号,知道数据准备好了,于是将数据从内核空间复制到用户空间,系统调用返回。用户进程读取数据后,继续执行原来的任务。

与PIO模式相比,DMA是CPU的代理,负责部分拷贝工作,从而减轻CPU的负担。

需要注意的是,DMA所承担的工作是从磁盘缓冲区到内核缓冲区或网卡设备到内核的套接字缓冲区的复制工作,以及从内核缓冲区到磁盘缓冲区或内核的套接字缓冲区的复制工作到网卡设备。,而内核缓冲区和用户缓冲区之间的复制工作仍然是 CPU 的责任。

可以肯定的是,现在很少看到处于 PIO 模式的计算机。

缓冲 IO 和直接 IO

我们在了解用户空间和内核空间的时候,也说过用户空间不能直接访问内核空间的数据。如果我们需要访问它怎么办?很简单,你需要从内核空间的用户空间复制数据。

缓冲 IO

缓冲 IO 也称为标准 IO,大多数文件系统系统默认工作在缓冲 IO 中。在 Linux 的缓冲 I/O 机制中,数据首先从磁盘复制到内核空间缓冲区,然后再从内核空间缓冲区复制到应用程序的地址空间。

接下来我们看看buffered IO下的读写操作是如何进行的?

操作系统检查内核缓冲区是否有所需的数据。如果已经被缓冲,则直接从缓冲区返回;否则,它会从磁盘读取到内核缓冲区,然后复制到用户空间缓冲区。

将数据从用户空间复制到内核空间的缓冲区。此时,用户程序的写操作已经完成,何时写入磁盘由操作系统决定,除非显式调用sync同步命令。

缓冲 I/O 的优点:

在一定程度上将内核空间和用户空间分开,保护系统本身的运行安全;因为内核中有一个缓冲区,可以减少磁盘读取的次数,从而提高性能。

缓冲 I/O 的缺点:

在缓冲 I/O 机制中io口输入输出实验报告,DMA 方法可以直接从磁盘读取数据到内核空间页面缓冲区,也可以直接从内核空间页面缓冲区将数据写回磁盘,而不是直接在用户地址空间和磁盘。进行数据传输,使得数据在传输过程中需要在应用地址空间(用户空间)和内核缓冲区(内核空间)之间进行多次数据复制操作。这些数据复制操作带来的 CPU 和内存开销非常高。大的。

直接 IO

顾名思义,直接IO是指应用程序直接访问磁盘数据而不经过内核缓冲区,即绕过内核缓冲区,自行管理I/O缓冲区。这样做的目的是减少从内核缓冲区到用户程序缓冲区的时间。数据复制。

引入内核缓冲区的主要目的是提高从磁盘读写数据文件的性能,这也是很多系统优化中常用的方法。增加一层缓存可以有效减少很多磁盘IO操作;以及当用户程序需要写入磁盘文件时。输入数据时只需要写入内核缓冲区即可返回,而真实磁盘有一定的延迟策略,但这无疑提高了应用程序在写入文件时的响应速度。

在数据库管理系统等应用中,更倾向于选择自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存储的数据,而数据库管理系统可以提供更有效的缓存机制提高数据库中数据的访问性能。

直接 I/O 的优点:

应用程序直接访问磁盘数据,无需经过操作系统内核数据缓冲区。这样做最直观的目的是减少从内核缓冲区到用户程序缓冲区的数据复制。这种方法通常用在数据库和消息中间件中,应用程序实现数据缓存管理。

直接 I/O 的缺点:

如果访问的数据不在应用缓冲区中,每次直接从磁盘加载数据,这种直接加载会很慢。通常直接 I/O 与异步 I/O 结合以获得更好的性能。(异步IO:当访问数据的线程发出请求时,线程会继续处理其他事情,而不是阻塞等待)

IO访问方式

我们常说的IO操作不仅仅是磁盘IO,还有常见的网络数据传输,即网络IO。

磁盘 IO

读取操作:

当应用程序调用 read() 方法时,操作系统会检查内核高速缓冲区中是否存在所需的数据。如果存在,则直接将内核空间中的数据复制到用户空间,供用户的应用程序使用。如果内核缓冲区没有所需的数据,则通过DMA将数据从磁盘读取到内核缓冲区,然后控制CPU将内核空间中的数据复制到用户空间。

这个过程涉及到两个缓冲区拷贝,第一个是从磁盘到内核缓冲区,第二个是从内核缓冲区到用户缓冲区,第一个是DMA拷贝,第二个是CPU拷贝。

写操作:

当应用程序调用write()方法时,应用程序将数据从用户空间复制到内核空间的缓冲区中(如果用户空间没有对应的数据,则需要从磁盘->内核缓冲区->用户缓冲区),此时用户程序的写操作已经完成。至于何时将数据写入磁盘(内核缓冲区到磁盘的写操作也是由DMA控制的,不需要CPU参与),由操作系统决定。除非应用程序显式调用同步命令,否则数据会立即写入磁盘。

如果应用程序尚未准备好写入数据,则必须先从磁盘读取数据,然后才能执行写入操作。此时,将涉及到四个缓冲区副本。第一个是从磁盘缓冲区到内核缓冲区,第三个是复制缓冲区。第二个是从内核缓冲区到用户缓冲区,第三个是从用户缓冲区到内核缓冲区,第四个是从内核缓冲区写回磁盘。前两个用于阅读,后两个用于书写。有两个 CPU 副本和两个 DMA 副本。

磁盘 IO 延迟:

为了读取或写入,磁头必须能够移动到指定的磁道并等待指定扇区的开始在磁头下旋转,然后才能开始读取或写入数据。磁盘IO的延迟分为以下三个部分:

网络IO

读取操作:

网络 IO 既可以从物理磁盘读取数据,也可以从 Socket 读取数据(从网卡获取)。从物理磁盘读取数据时,过程与磁盘 IO 的读取操作相同。应用程序从 Socket 读取数据时,需要等待客户端发送数据。如果客户端还没有发送数据,则相应的应用程序会被阻塞,直到客户端发送数据后应用程序才会被唤醒。协议栈(网卡)将客户端发送的数据读取到内核空间(这个过程也是由DMA控制的),然后将内核空间中的数据复制到用户空间供应用程序使用。

写操作:

为了简化描述,我们假设网络IO的数据是从磁盘获取的,读写操作流程如下:

网络IO写操作也有四个缓冲区副本,第一个是从磁盘缓冲区到内核缓冲区(由DMA控制),第二个是从内核缓冲区到用户缓冲区(由CPU控制),第三个是是从用户缓冲区到内核缓冲区(由CPU控制)的Socket Bufferio口输入输出实验报告,第四是从内核缓冲区到网卡设备的Socket Buffer(由DMA控制)。四个缓冲区的复制工作由 CPU 控制两次,由 DMA 控制两次。

网络 IO 延迟:

网络IO的主要延迟由:服务器响应延迟+带宽限制+网络延迟+跳转路由延迟+本地接收延迟决定。一般为几十到几千毫秒,受环境影响较大。因此,一般来说,网络 IO 延迟大于磁盘 IO 延迟(与数据中心的交互除外,它会比磁盘 IO 更快)。

零拷贝 IO

在上面的IO中,一次读写操作要经过4次buffer副本,内核态和用户态之间的切换要经过4次。零拷贝 IO 技术减少了内核缓冲区和用户缓冲区之间不必要的拷贝,从而减少了 CPU 开销和状态切换开销,提高了性能。

下面我们对比分析一下上面不使用零拷贝时的网络IO传输过程:

与上图中普通的网络IO传输过程相比,零拷贝传输过程:硬盘->内核缓冲区(快速复制到内核套接字缓冲区)->套接字协议栈(在网卡设备中)。

这里只有三个缓冲区副本,第一个是从磁盘缓冲区到内核缓冲区,第二个是从内核缓冲区到内核套接字缓冲区,第三个是从内核套接字缓冲区到Socket协议栈(网络卡设备)。内核模式和用户模式之间只有两种切换。第一次是应用程序调用read方法,用户态切换到内核态执行read系统调用,第二次是从网络发送数据,系统调用返回。,从内核模式切换到用户模式。

零拷贝的应用:

注意:零拷贝要求输入fd必须是文件句柄,不能是socket,输出fd必须是socket,也就是说数据的来源必须来自本地磁盘,不能来自网络,如果数据来自套接字,则不能使用零拷贝功能。我们看一下sendfile接口就知道了:

#include 
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)

in_fd 必须指向真实文件,而不是套接字和管道;并且 out_fd 必须是一个套接字。可以看出,sendfile 几乎是专门为通过网络传输文件而设计的。

在Linxu系统中,一切都是文件,所以socket也是一个文件,也有一个文件句柄(或文件描述符)。

同步与异步、阻塞与非阻塞

这两套概念,自从我开始编程,就听人说服务器是同步非阻塞模型或者异步阻塞IO模型。同步和阻塞的概念很容易混淆。每个人都有不同的意见。我最近耐心地阅读了几篇文章。这一次,我觉得我有一个顿悟。下面我分享一下我的理解:

同步和异步是指应用程序向内核发起任务后的状态:如果在发起调用之后,在得到结果之前,当前调用不会返回,无法继续下一件事情。一直等待就是同步。异步是指调用后,虽然不能立即得到结果,但可以继续执行下面的事情。当调用结果出来时,会通过状态、通知、回调等方式通知调用者。

为了更好地理解:

在互联网普及之前,我们去医院看病都要排队等候。如果我们想看医生,我们不得不排队等到轮到你。在此之前,您必须一直排队等候。这是同步;拨打号码,我们预约挂号后就可以去休息室坐坐、玩游戏等,看病会有通知,这个是异步的;

阻塞阻塞和非阻塞非阻塞关注CPU在等待结果的过程中的状态。

阻塞调用是指当前线程在调用结果返回之前会被挂起,只有在得到结果后才会返回。您可以将阻塞调用等同于同步调用。事实上,它们是不同的。同步只是意味着在返回之前必须等待结果,但是在等待的过程中可以激活线程。阻塞意味着线程被挂起。

非阻塞和阻塞的概念是相互对应的,也就是说函数不会阻塞当前线程,直到不能立即得到结果,而是立即返回。

比如前面的例子,在排队的过程中,如果什么都做不了,就被阻塞,把CPU执行权交出来;一边排队一边看手机,是无阻塞的,CPU执行能力还是在自己手里,但是看病前还是在自己手里。它在队列中等待,所以它仍然是同步的。

总结

通过今天的学习,我们已经掌握了IO是什么,常见的IO操作类型和对应操作的原理,以及同步与异步、阻塞与非阻塞之间非常重要但容易混淆的区别,还是要说明的比较清楚。

这篇文章的内容比较简单,都是一些基础知识,但是如果想要深入学习网络编程,这些基础知识是无法回避的。只有了解操作系统对IO操作的优化,才能了解各种高性能网络服务器的原理。

原文链接:

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论