当前位置: 首页 > ai >正文

【系统全面】Linux内核原理——基础知识介绍

理解内核:内核原理

计算机系统的软件分层

不同于单片机中使用代码直接与硬件交互,对于这种方式的缺点深有:

(1)复杂度高,调用难度高,需要深入理解硬件的工作原理和细节。

(2)细节繁琐,特别是在处理底层寄存器和端口配置时,极易出错。

(3)出错时调试难度也很高,因为涉及到底层硬件的交互,定位问题相对困难。

(4)更换芯片时,往往需要重写驱动,导致代码的可移植性很差,增加了开发和维护 的成本。

​ **为了解决这些问题,操作系统应运而生。**后者在硬件之上做了一层抽象,提供了统一 的接口和服务,隐藏了硬件的复杂性和差异性,从而简化了程序对硬件资源的访问和管理, 使得开发者能够更加专注于应用逻辑的实现,而不必深入到复杂的硬件操作细节中。因此, 它为上层应用程序提供了一个统一、稳定、高效的运行环境,极大地提升了软件开发的效 率和可靠性。

在这里插入图片描述
内核 内核是操作系统的核心部分,负责管理计算机的硬件资源,包括处理器、内存、存储 设备和其他外围设备。内核提供系统服务的基础,如进程管理、内存管理、设备驱动、文 件系统和网络通信等。 内核作为硬件和应用程序之间的中介,提供一个抽象层,使得应用程序不需要直接与 硬件交互。

应用程序 应用程序是运行在操作系统之上的软件,用于执行特定的任务,如文本编辑、图像处 理、网络通信等。 应用程序提供用户所需的功能,依赖于操作系统提供的接口与硬件资源进行交互。

进程和程序

程序是存储在硬盘等存储介质的代码,是一串二进制机器码,是静态的。

进程是正在运行的程序及相关资源的总称,是一种抽象,是动态的。

在这里插入图片描述

进程控制块PCB

在Linux 内核中,进程控制块的实现是struct task_struct,下述信息都存储在这 个结构体中。

内核会保存每个进程的一些信息,称为进程控制块(Process Control Block, PCB),方便管理进程,内容如下:

(1)进程编号(PID),每个进程对应的唯一编号,一般为正整数形式。

(2)进程状态信息。

(3)进程切换时需要保存和恢复的一些 CPU 寄存器,其中关键的有程序计数器 (Program Counter)的值,用于记录进程恢复时应执行的指令地址。

(4)内存管理信息,如页表、内存限制、段表等。

(5)当前工作目录(Current Working Directory)。

(6)进程调度信息,包括进程优先级、调度队列指针等。

(7)I/O 状态信息,包括分配给进程的 I/O 设备列表,打开的文件描述符表等,后者 包含很多指向file结构体的指针。

(8)同步和通信信息,包括信号量、信号、等用于进程同步和通信机制的信息。

(9)用户id和组id。

进程的内存模型

在这里插入图片描述

在这里插入图片描述

为什么可以同时运行的程 序数量远大于CPU的核心数?——————CPU虚拟化和进程的调度

​ 理论上,同一时刻单核CPU只能运行一个进程,但很多时候,我们可以同时运行的程 序数量远大于CPU的核心数。这是因为,操作系统的CPU调度单元对CPU的资源做了时间 分片,即在时间尺度上对CPU做了划分,如15:15到15:16执行进程A,15:16到15:17 执行进程B。实际上进程间的切换是非常迅速的,在用户的角度,就好像多个进程在同一 时间运行。因此,看起来好像计算机可以同时运行的进程数远大于CPU核心数。

进程的切换是由操作系统的CPU调度器完成的。 进程执行到某一时刻,被内核中断,然后内核可以重新开始之前被 中断的进程,这种决策就是调度(Scheduling),是由内核中称为调度器(Scheduler) 的代码处理的。当内核选择一个新的进程时,我们说内核调度了这个进程。 

在这里插入图片描述

抽象的进程状态模型和统一

在这里插入图片描述
初始态非常短暂,通常是看不到的。终止态和僵尸态实际上都是进程执行完毕之后的 状态,严格意义上讲,并不属于进程运行时状态。因此,部分操作系统资料在介绍进程状 态时不会在进程转换状态机中包含。下图是《操作系统导论》中的进程转换状态机。

在这里插入图片描述

虚拟内存映射

(1)虚拟内存 虚拟内存是计算机系统内存管理的一种技术,它为每个进程提供了一种“虚拟”的地 址空间,这个地址空间对于每个程序来说看起来都是连续的,但实际上可能被分散地存储 在物理内存和磁盘上(如交换空间或页面文件)。虚拟内存允许系统超额分配内存,即分 配的内存总量可以超过物理内存的实际容量。虚拟内存简化了内存的管理,使得应用程序 不需要关心物理内存的实际情况。

(2)物理内存 物理内存指的是计算机中安装的实际RAM(随机访问存储器)模块。它是系统用来存 储正在运行的程序和数据的硬件资源。 物理内存直接影响到计算机能够同时处理的信息量。更多的物理内存意味着可以同时 运行更多的程序,或者处理更大的数据集。

(3)MMU 进程可以直接操作的只有虚拟内存,那么,虚拟内存毕竟是“虚拟”的,进程的代码 段、数据、栈等最终一定要存储到真正的物理内存,那么,我们就要建立虚拟内存和物理 内存之间的映射关系。在操作系统中,这件事是由MMU来完成的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前面的共享内存与虚拟内存映射的关系

在这里插入图片描述

异常和中断

在这里插入图片描述

(1)中断 中断(Interrupt)通常是I/O设备或时钟触发的,信号来自处理器外部,不是由任 何一条指令造成的,从这个角度讲,它是异步的。中断处理完毕后总是执行下一条指令。

(2)异常 异常是(Exception)CPU 执行指令时检测到特定条件触发的。x86-64架构定义了三 种异常:陷入(Trap)、故障(Fault)和终止(Abort)。

① 陷入 是由进程执行陷入指令(可以切换到内核态的指令)主动触发的,是同步的,执行完 毕后总是执行下一条指令。

② 故障 故障由错误情况引起,可能被故障处理程序修正,如上文提到的缺页故障就是故障的 一种。故障发生时,处理器将控制权交由故障处理程序,如果故障被修复,则返回引起故 障的指令,并重新执行。否则,处理程序返回到内核中的abort例程,后者终止引起故障 的进程。

③ 终止 终止是不可恢复的致命错误造成的结果。如底层硬件错误,或者进程产生的算数异常, 无法被修复。终止发生时,CPU将控制权交由终止处理程序,这个程序不会将CPU的控制 权返还给应用程序,而是返回到内核中的abort例程,后者终止进程。

中断和异常的区别

在x86-64 架构中,中断和异常区别在于:中断处理例程被调用时,CPU会清除 EFLAGS 寄存器中的IF(Interrupt Enable)位,避免其它中断干扰当前中断处理例程 的执行。而异常处理例程被调用时IF不会被清除。

中断和异常处理

在这里插入图片描述
在这里插入图片描述

中断上文 中断上文是指在异常发生之前,处理器的状态。即中断处理时最先压入内核栈的数据,
包括段选择器的值、栈指针、程序计数器、状态相关寄存器等的值。

中断下文 中断下文是指涉及中断或异常处理时内核所处的环境和状态,包括内核栈中保存的用
户线程状态、状态寄存器、段选择器和内核栈指针等寄存器、内核态下执行的中断或异常 处理函数等内容。

中断和异常处理的案例——缺页故障 原理

在这里插入图片描述
在这里插入图片描述

进程创建过程

​ 进程是操作系统中的一个基本概念,代表了操作系统中正在运行的一个程序的实例。 进程不仅包括代码本身,还包括运行该程序所需的各种资源,如虚拟内存空间、文件描述 符、环境变量等。每个进程都有一个独立的虚拟地址空间,这意味着一个进程无法直接访 问另一个进程的内存。

​ 当一个新进程被创建时,操作系统会为其分配一个唯一的进程标识符(PID)和一个 新的虚拟地址空间。随后,操作系统会加载程序代码到虚拟内存中,并设置初始的堆栈和寄存器,包括程序计数器(PC)和栈指针(SP)。通过fork创建的子进程将继承父进程 的资源副本,包括打开的文件描述符和环境变量。

​ 当我们通过ps -ef看到的信息可以分为两类:进程信息和内核线 程的信息,进程工作在用户态,执行用户程序,内核线程工作在内核态,执行内核任务。 内核启动时会创建1号进程和2号内核线程,通常1号进程名为systemd,2号内核线程 名为kthreadd,其它进程全部由systemd及其子进程通过fork()+execve()的方式创建, 其它内核线程都是由kthreadd及其创建的内核线程通过kthread_create()这样的方式 创建的。fork()和kthread_create()是通过创建系统调用clone()实现的。那么他们对应关系是怎样的?

1.进程PCB的实现是struct task_struct类型的实 例

2.fork()

fork()主要完成了以下工作:

(1)为子进程创建内核栈、thread_info实例。

(2)复制父进程的 task_struct,后者包含了内核栈、虚拟内存管理信息、打开的文件 描述符表等的指针,此时子进程只是复制了这些资源的引用。

(3)清除子进程的统计信息,更新子进程task_struct的标志位。

(4)为子进程分配新的PID,将子进程的PPID设置为调用fork()的进程。

(5)清除与fork()返回值相关的寄存器,使得子进程中fork()返回的是0。

(6)复制打开的文件描述符表,这一过程底层被指向的 struct file 实例中引用计数加 一。复制文件系统信息、复制地址空间(页表相关信息),复制信号处理信息。

(7)最后,如果子进程成功创建则被唤醒,处于就绪态。

在这里插入图片描述
COW

写时复制机制(Copy on Write COW)可以提高进程创建效率。子进程完整地复制了 父进程的地址空间,此时父子进程的虚拟内存空间映射到相同的物理内存空间。只有当二 者之一执行了写入操作才会复制写入区域的内容,为父子进程维护不同的物理页帧。
在这里插入图片描述

COW就是以下程序现象的原因:

#include <stdio.h> 
#include <unistd.h> 
int main() { 
int val = 123; // 定义变量接收子进程PID __pid_t pid; // 创建一个子进程 if ((pid = fork()) > 0 ){ sleep(1); printf("父进程中val 的内容是: %d\nval 所在的地址是: %p\n", val, 
&val); } else if (pid == 0) { val = 321; printf("子进程中val 的内容是: %d\nval 所在的地址是: %p\n", val, 
&val); } else { printf("子进程创建失败\n"); } return 0; 
} 

在这里插入图片描述

父子进程中各自打印了val变量的值和内存地址。子进程将val变量更改为321,我 们特意让父进程休眠了一秒,确保子进程的更改先于父进程的输出。我们发现子进程修改 了val变量的值,而父进程的val未受影响,说明两个进程的val变量是完全独立的。但 是,二者的内存地址却完全相同。既然内存地址是相同的,父子进程中的val变量不就应 该是同一个吗?但子进程修改了自己的val,父进程的val却未受影响,从这个角度看, 二者又是完全独立的变量?这不是矛盾了吗?
答:没有 原因是COW

3.execve

(1)参数和环境准备 内核检查传递给execve()的参数,包括可执行文件的路径、环境变量和命令行参数, 以确保它们的有效性和安全性。这个阶段内核会在内核空间中准备一份新程序需要的命令 行参数和环境变量的备份。

(2)打开和验证可执行文件 打开指定的二进制文件,验证其格式是否支持(例如,ELF格式),并检查执行权限。 如果这一步找不到可执行文件的路径,就会直接终止。

(3)创建新的内存映射 清除进程当前的内存映射,包括用户空间中的代码、数据、堆和栈。 根据新的程序建立新的代码段、数据段、堆和栈等。需要注意的是,内存映射不包含内核空间,内核空间的映射是由操作系统内核管理的, 对所有进程是共享的。execve切换的只是用户空间。

(4)复制参数和环境变量 在新的地址空间中为命令行参数和环境变量分配空间,并将内核中它们的备份复制到 新的位置。

(5)初始化进程上下文 设置新的程序计数器、栈指针等,以便新程序可以正确执行。 清理和重设进程的各种内核资源,如文件描述符表。根据文件描述符的 close-on exec 标志(FD_CLOEXEC)进行处理,如果有该标志,则文件描述符被关闭。

(6)更新 task_struct 和其他内核结构 更新 task_struct 中关于进程地址空间、堆栈、命令行参数、环境变量的指针。 重置信号控制信息到默认状态。 清理进程的各种内核状态,如未处理的信号、定时器等。

(7)执行新程序 跳转到新加载程序的入口点开始执行。度看, 二者又是完全独立的变量?这不是矛盾了吗? 在这里插入图片描述

进程组

进程组ID(Process Group ID,简称PGID)在UNIX和类UNIX系统(如Linux) 中用来标识一个或多个进程的集合。进程组用于信号传递和终端控制(如作业控制)。在 很多方面,进程组的概念是为了更好地支持在终端中运行的交互式作业。

进程切换过程

1)进程切换的场景 如果进程的运行不会被打断,那么操作系统内核想要回CPU的控制权就只能寄希望于 进程主动归还,或者强制重启计算机。现代计算机提供了中断和异常机制,二者都可以打 断正在执行的进程,将CPU的控制权交还给内核。进程的切换需要内核介入,必然要通过 中断或异常来实现。进程切换主要在以下几种情况下发生。

(1)时钟中断触发,被中断的进程获得的 CPU 时间片耗尽,操作系统决定切换进程。

(2)当前进程发生故障,内核夺回 CPU 控制权,如果故障无法被修复,则内核终止 该进程,切换至其它进程。 (3)时钟中断触发,当前进程在等待IO操作,为避免资源浪费,切换至其他进程。

(4)时钟中断触发,高优先级进程处于就绪状态,内核将 CPU 使用权由当前进程转 交给高优先级进程。

2)进程切换过程 进程的切换需要借助中断或异常,流程如下。假设正在运行的进程A要被切换到进程 B。

(1)CPU暂存栈指针、程序计数器、段选择器和状态寄存器的值。

(2)栈指针由进程A的用户栈切换至它的内核栈,操作系统切换至内核态。

(3)CPU将第一步暂存的寄存器值压入内核栈。

(4)将错误码压入内核栈。

(5)程序计数器指向中断或异常处理程序。

(6)操作系统执行中断或异常处理程序。

7)在中断或异常处理程序中,调度器会判断是否满足进程切换条件,如果满足则执 行以下操作:

① 将进程A所有相关寄存器的值保存至进程A的PCB(Linux底层实现为struct task_struct)。这会包含它的页表基址。

② 有些架构会清除TLB。

③ 将进程B的PCB中记录的页表基址、栈指针等寄存器信息加载(恢复)到对应寄 存器。此时栈指针指向进程B的内核栈。进程A回到调度队列。如果进程A是因为CPU时 间片耗尽,则处于就绪状态,回到就绪队列。

要注意,打开的文件描述符表等相关资源的切换不需要通过寄存器实现,这些资源存 储在struct task_struct 结构体中,调度器可以从调度队列获得task_struct,完成 资源切换。

(8)执行权限检查,判断当前是否处于内核态。

(9)从进程B的内核栈恢复CS和程序计数器,后者指向B的用户进程代码。

(10)恢复进程B的状态寄存器RFLAGS。

(11)从进程 B的内核栈恢复 SS和栈指针,后者指向进程 B的用户栈,此时切换到 用户态。

(12)在用户态下继续进程B的执行,进程切换完成。

在这里插入图片描述

进程上下文

1)进程上文 进程上文是指进程被挂起时其执行状态的集合,这使得进程能够在未来某个时间点继 续执行。它包括进程的程序计数器、栈指针等寄存器状态,进程的内核栈和用户栈的信息、 内存映射信息(如页表条目),打开的文件描述符表等。

2)进程下文 进程下文是指将要被加载和执行的挂起进程的执行状态集合。包括将要被执行的进程 的程序计数器、栈指针等寄存器状态,进程的内核栈和用户栈的信息、内存映射信息(如 页表条目),打开的文件描述符表等。

系统调用和库函数

在这里插入图片描述

线程真的存在吗?

线程是进程中的执行单元,它共享进程的资源和地址空间,但拥有自己的执行堆栈、 程序计数器和一组寄存器。由于线程共享相同进程内的资源,它们之间的通信和数据共享 相对容易。在很多类Unix系统中,线程被称为轻量级的进程,创建和上下文切换的开销 小于进程。

线程实际上在底层是不存在的,它只是人为规定的一个概念

线程和进程的区别与联系

在Linux 中,线程等同于轻量级进程,二者都有独立的task_struct结构体实例。 线程创建和进程创建在技术上是完全等同的,fork()和进程创建函数pthread_create() 底层都调用了系统调用clone()。

1)创建进程

当我们调用fork()时,等同于调用clone(SIGCHLD, 0),SIGCHLD标志的作用是告 诉操作系统:当子进程终止时,父进程应当接收到SIGCHLD 信号。这个信号是默认的方式, 用于通知父进程其子进程已经结束。这样一来,父进程就可以在子进程退出后执行清理操 作。

2)创建线程

创建线程时,底层会调用clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0),这些 Flag 的含义如下。

➢ CLONE_VM:共享地址空间。从技术上将,被创建的线程和创建者的 struct task_struct 实例中 struct mm_struct 指针类型的字段 mm和 active_mm指向相同的实例。

➢ CLONE_FS:共享文件系统信息。从技术上将,被创建的线程和创建者的 struct task_struct 实例中 struct fs_struct 指针类型的字段 fs指向相同的实例。

➢ CLONE_FILES:共享打开的文件描述符表,从技术上讲,被创建的线程和创建者 的struct task_struct 实例中 struct files_struct 指针类型的字段 files指向相同的实例。

➢ CLONE_SIGHAND:共享信号处理函数表。从技术上讲,被创建的线程和创建者 的struct task_struct 实例中 struct signal_struct 指针类型的字段和 struct sighand_struct指针类 型的字段指向相同的实例。

3)进程创建和线程创建的区别

从clone()系统调用的角度,我们可以得出结论:如果多个进程共享了地址空间、文 件系统信息、打开的文件信息、信号处理信息,那么他们就是同属于一个进程的线程。

task_struct 结构体中的pid实际上表示的是进程或线程ID,tgid字段表示的是线 程组ID,等同于传统意义上的线程ID。对于单线程的进程,pid字段和tgid字段相同。 对于多线程进程,主线程的pid等于tgid,pid此时可以理解为主线程的线程ID或者进 程ID,普通线程的tgid等于主线程的pid和tgid,即当前线程所属的线程组ID和进程 ID,而pid字段此时相当于线程ID。

在这里插入图片描述
在这里插入图片描述

线程的特点

1)资源共享 thread_info 实例 mm_struct 实例 files_struct 实例 signal_struct 实例 线程之间共享进程资源,包括地址空间、文件系统信息、打开的文件描述符和信号处 理函数,而不同进程之间的资源是隔离的。

2)通信 线程间的通信通常比进程间的通信(例如,通过管道、共享内存)更为高效。因为地 址空间是共享的,线程间可以直接通过如全局变量这样的方式通信。

3)创建和管理开销 线程的创建和上下文切换通常比进程更轻量级,因此在需要频繁创建和销毁执行单元 的场景中,线程可能是更合适的选择。

内核线程

Linux的内核线程是在内核空间中运行的轻量级 进程,它们没有独立的地址空间和大部分用户空间资源。内核线程是操作系统内核功能的 一部分,主要用于管理和执行内核级任务,如硬件中断处理、系统调用服务、内存管理等。

1)内核栈 内核线程拥有自己的内核栈。

2)控制内核线程的数据结构 内核线程的信息也存储在task_struct结构体中。

3)地址空间 不同于普通进程,struct mm_struct类型的字段mm及active_mm取值为NULL, 它们通常共享内核的全局地址空间。

4)标记字段 内核线程的task_struct 中flags字段被标记为PF_KTHREAD,用于表示这是一个 内核线程。

5)内核线程的ID 内核线程工作在内核态,相互之间地位是等同的,因此,任意内核线程的 TID、TGID、 PID 都是相同的。并且,所有内核线程的PGID都是0。这是因为内核线程不与任何特定的 终端相关联,也不参与普通的作业控制和信号处理,这些通常是用户空间进程的特性。 PGID 设置为0是设计上的选择,用于确保内核线程在操作系统中的特殊性和隔离性。

6)打开的文件描述 此外,内核进程不需要执行I/O操作或者直接操作文件描述符,因此,不需要文件描 述符表,它的task_struct中,指向struct files_struct实例的字段为NULL。

7)文件系统信息 同样地,指向struct fs_struct实例的字段也通常是NULL。fs_struct用于管理 文件系统相关的信息。内核线程不与特定的文件系统路径交互,所以不需要此信息。

8)信号处理信息 struct signal_struct和struct sighand_struct实例主要用于处理用户级信号, 因此,内核线程的task_struct实例中,指向它们的指针也通常为NULL。

在这里插入图片描述

http://www.xdnf.cn/news/15867.html

相关文章:

  • Python-数据库概念-pymysql-元编程-SQLAlchemy-学习笔记
  • 【ASP.NET Core】ASP.NET Core中Redis分布式缓存的应用
  • Python day20 - 特征降维之奇异值分解
  • 隧道代理的动态IP切换机制与实现原理
  • 农村供水智慧化管理系统:从精准监测到智能调度,破解农村用水安全与效率难题
  • 康复器材动静态性能测试台:精准检测,为康复器械安全保驾护航
  • Gradio项目部署到魔搭创空间
  • 开发避坑短篇(3):解决@vitejs plugin-vue@5.0.5对Vite^5.0.0的依赖冲突
  • [特殊字符] Java反射从入门到飞升:手撕类结构,动态解析一切![特殊字符]
  • Dockerfile 完全指南:从入门到精通
  • Three.js 全景图(Equirectangular Texture)教程:从加载到球面映射
  • AR技术:石化行业培训的“游戏规则改变者”
  • 【C语言】字符串与字符函数详解(下)
  • 【UE5医学影像可视化】读取dicom数据生成2D纹理并显示
  • Python趣味算法:借书方案知多少 | 排列组合穷举法详解
  • 均值漂移累积监测算法(MDAM):原理、命名、用途及实现
  • 分治算法---归并
  • 【java】消息推送
  • 编程语言Java入门——核心技术篇(一)封装、继承和多态
  • 响应式编程入门教程第七节:响应式架构与 MVVM 模式在 Unity 中的应用
  • 【Python练习】053. 编写一个函数,实现简单的文件加密和解密功能
  • Filter快速入门 Java web
  • SaTokenException: 未能获取对应StpLogic 问题解决
  • c#:TCP服务端管理类
  • Spark专栏开篇:它从何而来,为何而生,凭何而强?
  • EPLAN 电气制图(十): 继电器控制回路绘制(下)放料、放灰
  • 机器学习基础:从数据到智能的入门指南
  • 第三章自定义检视面板_创建自定义编辑器类_编辑器操作的撤销与恢复(本章进度3/9)
  • MySQL锁(一) 概述与分类
  • 算法讲解--复写零