笔者写着篇文章是为了之后学习RTOS、linux内核裁剪、ROS等内容,喜欢可以点个关注,关注后续更新
这篇文章部分截图节选自B站王道计算机教育,仅供学习使用。

文章目录

0、操作系统概述

四大特征

并发—信号量
共享
虚拟—内存管理
异步

内核

系统调用

https://www.bilibili.com/video/BV1YE411D7nH?t=354.8&p=7

操作系统开机引导

WIN启动流程

https://www.bilibili.com/video/BV1YE411D7nH?t=381.2&p=10

#预启动 - 硬件初始化
按下电源按钮,电源开始稳定地向主板和其他组件供电。主板上的芯片组会向CPU发送一个复位信号。
CPU脱离复位状态,进入一个已知的初始状态。
强制CPU从内存地址 0xFFFF0 开始执行指令。这个地址是硬编码在CPU内部的,它指向了BIOS芯片(ROM)的入口点。
BIOS开始执行POST程序。
检查CPU、内存、显卡等是否正常工作。
检测和初始化键盘、鼠标、硬盘、光驱等。
BIOS读取用户设置(如启动顺序),并最终生成一份系统中所有硬件的清单。

#启动引导设备 - 寻找引导扇区
BIOS按照预设的启动顺序,逐个尝试设备。
BIOS找到硬盘后,它会读取硬盘的第一个扇区(0号扇区)。这个扇区的大小是 512字节,被称为主引导记录(MBR)。
BIOS将这512字节的数据加载到内存的特定位置(通常是 0x7C00)。
BIOS检查MBR的最后两个字节是否为魔数 0x55AA。如果不是,则尝试列表中的下一个启动设备。

#MBR与分区表
主引导记录的前440字节是第一阶段引导加载程序的代码。控制权从BIOS移交到这里。
主引导记录的中间部分是分区表,它描述了硬盘上的分区情况。
引导加载程序代码扫描分区表,寻找活动分区(又称主分区,即安装了操作系统的分区)。
找到活动分区后,引导加载程序会去读取这个活动分区的第一个扇区。这个扇区被称为分区引导记录(PBR)或引导扇区。

#第二阶段引导加载程序
分区引导记录包含了第二阶段引导加载程序的代码,以及该分区的文件系统信息。控制权移交到分区引导记录代码。
分区引导记录的512字节空间极其有限,它的主要任务是去文件系统中查找并加载一个更强大的启动管理器文件。

#操作系统选择与内核加载
启动管理器被加载到内存并执行。
启动管理器读取启动配置数据BCD (Boot Configuration Data)文件。BCD是一个数据库,包含了所有启动配置信息,比如安装了哪些操作系统、它们的内核在哪里。
如果只有一个操作系统,它会直接加载。如果有多个(如Windows和Linux双系统),启动管理器会显示一个启动菜单供用户选择。
启动管理器会找到并加载 winload.exe。
winload.exe 是操作系统的正式加载程序。此时控制权移交此处。
winload.exe 加载Windows内核 ntoskrnl.exe。
加载硬件抽象层 hal.dll。
加载系统注册表配置单元和其他必要的启动驱动程序。
CPU从16位的实模式切换到32位/64位的保护模式,从而可以访问所有内存并执行现代操作系统的代码。

#操作系统初始化
ntoskrnl.exe 开始执行,此时控制权移交至此。
初始化核心系统组件、内存管理、进程调度等。
内核启动 smss.exe。这是第一个用户模式进程,它负责:设置系统环境。启动子系统(如Win32子系统)。启动 csrss.exe 和winlogon.exe。
winlogon.exe 启动,显示登录界面。此时控制权移交至此。
用户输入凭据后,lsass.exe 进行身份验证。
验证成功后,winlogon.exe 启动 explorer.exe,也就是我们看到的桌面外壳。
此时,操作系统启动完毕,等待用户交互。
同时,系统还会在后台启动一系列服务和启动项。

linux启动流程

按下电源按钮,电源开始稳定地向主板和其他组件供电。主板上的芯片组会向CPU发送一个复位信号。
CPU脱离复位状态,进入一个已知的初始状态。
强制CPU从内存地址 0xFFFF0 开始执行指令。这个地址是硬编码在CPU内部的,它指向了BIOS/UEFI 芯片(ROM)的入口点。
BIOS/UEFI 开始执行POST程序。
检查CPU、内存、显卡等是否正常工作。
检测和初始化键盘、鼠标、硬盘、光驱等。
BIOS/UEFI 读取用户设置(如启动顺序)。

BIOS方式
BIOS:读取硬盘的 MBR。
BIOS 读取 MBR(前 512 字节)。MBR 中包含 GRUB2 的 第一阶段代码 boot.img。
boot.img 的空间极小,它的唯一任务就是加载位于 MBR 之后、磁盘第一个扇区与第一个分区之间的 core.img。
core.img 包含了基本的文件系统驱动,使得 GRUB2 可以读取 /boot 分区中的文件。

UEFI方式
UEFI:从 EFI 系统分区 查找 .efi 引导程序。
EFI 固件直接从 ESP 分区加载 GRUB2 的 EFI 应用程序,通常是 /EFI/ubuntu/grubx64.efi。这跳过了 MBR 和 core.img 的加载过程。

core.img 或 grubx64.efi 会加载 /boot/grub 目录下的模块(如文件系统模块、界面模块等)。
然后读取 /boot/grub/grub.cfg 配置文件。这个文件定义了启动菜单的样式、超时时间,以及最重要的——可启动的操作系统列表。
GRUB2 将 grub.cfg 中定义的菜单显示给用户。用户可以选择启动不同的 Linux 内核版本、进入恢复模式,或者 chainload 其他系统(如 Windows)。

当用户选择了一个菜单项后,GRUB2 会从 /boot 目录加载对应的 Linux 内核镜像文件(通常是 vmlinuz-<版本号>)到内存中。
同时,它也会加载 初始 RAM 磁盘 文件(通常是 initrd.img-<版本号> 或 initramfs-<版本号>)。
控制权从 GRUB2 移交到 Linux 内核。
内核在内存中解压自己,然后开始初始化:设置内存管理、检测 CPU。初始化硬件子系统。
内核需要挂载真正的根文件系统,才能找到并运行第一个用户空间进程。
内核会执行 initramfs 中的 /init 脚本。这个脚本的任务是:
加载必要的硬件驱动模块。
如果根文件系统被加密,会提示输入密码。
激活 LVM/RAID 卷。
最终,找到并挂载真正的根文件系统。

initramfs 的 /init 脚本在成功挂载真正的根文件系统后,会进行 “pivot root” 操作,即从内存中的 initramfs 切换到硬盘上的根文件系统。
然后,它执行根文件系统中的 /sbin/init 程序。这是 第一个用户空间进程,其进程号永远是 1。
/sbin/init 进程负责启动系统的其余部分。
systemd 的二进制文件就是 /sbin/init(一个符号链接)。
它并行启动服务,大大加快启动速度。
它管理着整个系统的服务、挂载点、套接字等(称为 units)。
systemd 启动的目标是达到 “default.target”,这通常是一个符号链接,指向 multi-user.target(多用户命令行界面)或 graphical.target(图形界面)。
在启动过程中,它会解决各个单元之间的依赖关系,并行启动所有必要的服务(如网络、日志、显示管理器等)。
如果目标是 graphical.target(图形界面),systemd 会启动显示管理器,如 GDM、SDDM 或 LightDM。
显示管理器提供图形化登录界面。
用户输入凭据登录后,显示管理器启动桌面环境,如 GNOME、KDE Plasma 或 XFCE。
最终,用户看到完整的桌面环境,启动完成。

嵌入式Linux启动流程

使用bootloader的方式进行启动,以RK35XX举例
详细见:SoC详细启动流程0724

单片机启动流程

使用bootloader的方式进行启动,以STM32举例
详细见:单片机启动流程以STM32举例https://blog.csdn.net/wxr2268715051/article/details/160373020?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

1、进程调度—处理机管理

进程与线程

ARM架构下进程切换和线程切换的核心区别
https://blog.csdn.net/wxr2268715051/article/details/163538216?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

进程的状态

自己画的

进程间通信

调度

饥饿

同步与互斥

互斥和同步都是为了解决并发环境下,访问共享资源时可能出现的问题

死锁

https://www.bilibili.com/video/BV1YE411D7nH?t=999.1&p=39

信号量(semaphore)

PV操作
P—wait(S)—申请/占用资源 V—signal(S)—释放资源

记录型信号量的数据结构

/*记录型信号量的定义*/
typedef struct
	int value;				//剩余资源数
	struct process*L;	//等待队列
}semaphore;

  1. 互斥资源的访问—互斥信号量(互斥量)

  2. 异步并发进程的同步—同步信号量
    每一对前后执行的关系都需要一个同步信号量
    先释放V,再占用P

生产者与消费者

使用三个信号量

  1. 互斥信号量—实现对buffer的互斥访问
  2. 同步信号量—表示buffer当中空闲位置的数量
  3. 同步信号量—表示buffer当中产品的数量
// 全局变量和信号量
BUFFER_SIZE = 10
buffer[BUFFER_SIZE]          // 共享缓冲区
mutex = semaphore(1)         // 互斥信号量,初值1
empty = semaphore(BUFFER_SIZE)  // 空槽位数,初值N
full = semaphore(0)          // 已填充槽位数,初值0

// 生产者进程
PRODUCER(){
    while(1){
        item = produce_item()    // 生产一个数据项
        
        wait(empty)              // 等待空槽位
        wait(mutex)              // 进入临界区
        
        buffer[in] = item        // 放入数据
        
        signal(mutex)            // 离开临界区
        signal(full)             // 增加已填充槽位
    	}
}

// 消费者进程
CONSUMER(){
    while(1){
        wait(full)               // 等待有数据可消费
        wait(mutex)              // 进入临界区
        
        item = buffer[out]       // 取出数据
        
        signal(mutex)            // 离开临界区
        signal(empty)            // 增加空槽位
        
        consume_item(item)       // 消费数据
     }
}

互斥占用一定要在同步占用之后

多生产者与多消费者
单生产者AB消费者
// 缓冲区(生产者写入,两个消费者读取)
BUFFER_SIZE = 10
buffer[BUFFER_SIZE]

// 同步变量
mutex = semaphore(1)           // 保护缓冲区的互斥锁
data_ready = semaphore(0)      // 有新数据可供 A 消费
a_done = semaphore(0)          // A 已读完,B 可以读
b_done = semaphore(0)          // B 已读完,槽位可以回收
slots_free = semaphore(BUFFER_SIZE)  // 可用于生产者的空槽位


// 生产者
PRODUCER(){
    while (1){
        item = produce_item()
        
        wait(slots_free)            // 等空槽位
        wait(mutex)
        
        buffer[in] = item           // 放入数据
        
        signal(mutex)
        signal(data_ready)          // 通知 A 有新数据
   }
}

// 消费者 A(必须先读)
CONSUMER_A():
    while true:
        wait(data_ready)            // 等待新数据
        wait(mutex)
        
        item = buffer[out_a]        // A 从自己的指针读取
        
        signal(mutex)
        signal(a_done)              // 通知 B:A 已读完这条
        
        consume_A(item)             // A 处理数据


// 消费者 B(必须在 A 之后读)
CONSUMER_B(){
    while(1){
        wait(a_done)                // 等待 A 先读完
        wait(mutex)
        
        item = buffer[out_b]        // B 从自己的指针读取
        
        signal(mutex)
        signal(b_done)              // 通知:这条数据两个消费者都读完了
        signal(slots_free)          // 槽位可以回收(给生产者用)
        
        consume_B(item)             // B 处理数据
    }
}

读者与写者

本质:

  1. 读共享,写互斥
  2. 读者对资源不消费
单写者 多读者 写优先

使用三个信号量,一个计数值

  1. 互斥信号量rw:读的时候不能写,写的时候不能写
  2. 读任务计数count:计算当前有多少读者正在读
  3. 互斥信号量mutex_count:实现对count的互斥
  4. 互斥信号量w:实现写优先
// 信号量
rw = semaphore(1)          // 资源锁:读者和写者都用它竞争资源
mutex_count = semaphore(1) // 保护 count 的互斥锁
w = semaphore(1)           // 写者优先锁:写者在等待时,阻止新读者进入

// 计数值
count = 0                  // 当前正在读的读者数量



WRITER(){
    while(1){
        wait(w)               //写优先     
        wait(rw)              //--------------------------------------------------------
        
        write_resource(new_data)
        
        signal(rw)            //--------------------------------------------------------
        signal(w)             //写优先
   }
}

READER(){
    while(1){
        wait(w)               //写优先
        wait(mutex_count)     //------------------------------
        
        count++
        if count == 1:        // 第一个读者负责抢占资源
            wait(rw)          //---------------------------------------------------------

        signal(mutex_count)		//-----------------------------
        signal(w)             //写优先
        

        data = read_resource()
        
        wait(mutex_count)     //------------------------------

        count--
        if count == 0:        // 最后一个读者负责释放资源
            signal(rw)        //----------------------------------------------------------

        signal(mutex_count)//------------------------------
        

        process_data(data)
     }
}
多写者 多读者 写优先
// 信号量
rw = semaphore(1)           // 资源锁:读者和写者都用它竞争资源
mutex_count = semaphore(1)  // 保护 count 的互斥锁
w = semaphore(1)            // 写者优先锁:写者在等待时,阻止新读者进入
ww = semaphore(1)           // ★新增★ 写者间互斥锁

// 计数值
count = 0                   // 当前正在读的读者数量



WRITER(){
    while(1){
        wait(w)               // 写优先:抢占闸门     
        wait(ww)              // ★新增★ 写者之间互斥
        wait(rw)              // 等待资源空闲
        
        write_resource(new_data)
        
        signal(rw)            // 释放资源
        signal(ww)            // ★新增★ 释放写者互斥锁
        signal(w)             // 写优先:释放闸门
   }
}

READER(){
    while(1){
        wait(w)               // 写优先:通过闸门
        wait(mutex_count)     // 互斥修改 count

        count++
        if count == 1:        // ★修改★ 先加再判断
            wait(rw)          // 第一个读者锁资源
        signal(mutex_count)
        signal(w)             // 写优先:释放闸门
        

        data = read_resource()
        
        wait(mutex_count)     // 互斥修改 count

        count--
        if count == 0:        // 最后一个读者释放资源
            signal(rw)
        signal(mutex_count)
        

        process_data(data)
     }
}

线程同步与互斥方式:管程—封装好的类

2、内存管理—存储器管理

进程的内存映像
https://www.bilibili.com/video/BV1YE411D7nH?t=523.5&p=45
https://www.bilibili.com/video/BV1YE411D7nH?t=1.2&p=45

分配与回收

连续分配内存管理

进程的内外
内部碎片,分配给某进程的内存区域中,如果有些部分没有用上。
外部碎片,是指内存中的某些空闲分区由于太小而难以利用。

单一连续分配
固定分区分配
动态分区分配

https://www.bilibili.com/video/BV1YE411D7nH?t=1.1&p=48

非连续分配内存管理

体现出操作系统的虚拟特性

分页内存管理

页是信息的物理单位。
分页的主要目的是为了实现离散分配,提高内存利用率。
分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。

页的大小固定且由系统决定。
分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。

优点:
内存空间利用率高,不会产生外部碎片,只会有少量的页内碎片。
缺点:
不方便按照逻辑模块实现信息的共享和保护

快表(Translation Lookaside Buffer,TLB)
内容:一部分的页表
特性:访问速度比内存快很多(cache);TLB不是内存
作用:存放最近访问的页表项,加速地址变换
联想:进程线程的切换开销资源的不同,最根本就是来源于此,进程切换快表页面清除

分段内存管理

段是信息的逻辑单位。
分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。
分段对用户是可见的,用户编程时需要显式地给出段名。

段的长度却不固定,决定于用户编写的程序。
分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。

分段比分页更容易实现信息的共享和保护。不能被修改的代码称为纯代码或可重入代码(不属于临
界资源),这样的代码是可以共享的。可修改的代码是不能共享的

优点:
很方便按照逻辑模块实现信息的共享和保护。
缺点:
如果段长过大,为其分配很大的连续空间会很不方便。另外,段式管理会产生外部碎片

段页式内存管理

先分段,后分页

内存扩充

覆盖

需要程序员显式声明,现已不使用
一个进程当中发生
对内存分为
固定区:程序运行不会调入调出
覆盖区:程序运行会根据需要调入调出

交换

内存调度
不同进程之间发生
对外存(硬盘)分为
文件区:不连续的
对换区:连续的,换出的进程(挂起态)存储在对换区

虚拟内存

请求调页:访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存
页面置换:内存空间不够时,将内存中暂时用不到的信息换出到外存

请求分页内存管理

新增页表项:
状态位:表示页面是否已在内存中
访问字段:记录最近被访问过几次,或记录上次访问的时间,供置换算法选择换出页面时参考
修改位:表示页面调入内存后是否被修改过,只有修改过的页面才需在置换时写回外存
外存地址:页面在外存中存放的位置

页面置换算法

https://www.bilibili.com/video/BV1YE411D7nH?t=2.8&p=57

页面分配策略

驻留集
工作集

分配置换策略
固定分配局部置换:进程运行前就分配一定数量物理块,缺页时只能换出进程自己的某一页
可变分配全局置换:只要缺页就分配新物理块,可能来自空闲物理块,也可能需换出别的进程页面
可变分配局部置换:频繁缺页的进程,多分配一些物理块;缺页率很低的进程,回收一些物理块。直到缺页率合适

请求分段内存管理
请求页段内存管理

地址转换

逻辑地址(相对)->物理地址(绝对)

逻辑地址:是在进程视角下看到的地址。每个进程都拥有自己独立的、从0开始编址的、连续的地址空间。逻辑地址通常在编译和链接期间由编译器生成。编译器并不知道程序未来会被加载到物理内存的哪个位置。

物理地址:指的是在物理内存条(RAM)上真实存在的、每一个内存单元的绝对地址。CPU的地址总线最终就是通过这个地址来访问内存数据的。

内存保护

保证各进程在自己的内存空间内运行,不会越界访问

3、文件—文件管理

块->页->扇区
块:操作系统按块建造索引表进行查找,通常与内存页的大小相同,同时也是最小的擦除单位
页:最小的写入单位
扇区:机械硬盘时代的最小可寻址单元,固定为 512字节。

文件系统层次结构

https://www.bilibili.com/video/BV1YE411D7nH?t=329.1&p=70

文件系统布局

硬盘中的布局

物理格式化

https://www.bilibili.com/video/BV1YE411D7nH?t=50.1&p=71
注意:坏扇区操作系统不可见

逻辑格式化

https://www.bilibili.com/video/BV1YE411D7nH?t=114.7&p=71

内存中的布局

https://www.bilibili.com/video/BV1YE411D7nH?t=599.0&p=71

文件调用过程

https://www.bilibili.com/video/BV1YE411D7nH?t=633.7&p=71

文件逻辑结构—用户

文件物理结构—OS

顺序分配

链接分配

索引分配

文件共享

硬链接

末影箱
inode
文件系统中的链接指针,指向同一存储数据
只有当某个文件的所有硬链接(包括原始文件)都被删除时,磁盘上的数据才会被真正释放
硬链接仅支持同一分区(如 C 盘内)

软连接

快捷方式

虚拟文件系统

https://www.bilibili.com/video/BV1YE411D7nH?t=564.8&p=72

文件挂载

4、IO设备—设备管理

IO软件的层次

设备—特殊的文件

设备独立性软件=设备无关性软件=设备接口=系统调用

设备的分配与回收

一个通道控制多个控制器,一个控制器控制多个设备
https://www.bilibili.com/video/BV1YE411D7nH?t=393.4&p=80

逻辑设备表LUT(Logical Unit Table)

一个用户一张LUT
存在于进程级别/用户级别,负责将进程使用的逻辑设备名映射到系统的物理设备。

系统设备表SDT(System Device Table)

整个系统级别的全局表,记录了系统中所有已被识别和配置的设备。无论设备是否正在被使用,只要它存在于系统中,就会在SDT中有一个条目。
https://www.bilibili.com/video/BV1YE411D7nH?t=748.4&p=80

设备控制表DCT(Device Control Table)

一个设备一张DCT
https://www.bilibili.com/video/BV1YE411D7nH?t=534.8&p=80
设备队列的队首指针:被阻塞的进程的PCB所放到的阻塞队列

控制器控制表COCT(Controller Control Table)

一个控制器一张COCT
https://www.bilibili.com/video/BV1YE411D7nH?t=621.8&p=80

通道控制表CHCT(Channel Control Table)

一个通道一张CHCT
https://www.bilibili.com/video/BV1YE411D7nH?t=689.2&p=80

1、进程发出 系统调用,CPU从用户态陷入内核态,操作系统查询该进程的LUT,使用逻辑设备名作为索引,找到对应的条目。该条目包含了指向系统级物理设备标识符的指针。

2、操作系统使用从LUT中获取的物理设备标识符,去查询全局的系统设备表(SDT)。在SDT中匹配到条目,并从该条目中获取指向对应的设备控制表(DCT) 的指针。

3、操作系统访问 DCT,检查设备状态字段。
如果设备状态为“忙”,则将当前进程加入到DCT的等待队列中,进程阻塞,调度其他进程运行。
如果设备状态为“闲”,操作系统将状态标记为“忙”,以防止其他进程占用。

4、操作系统根据DCT中的指针,找到管理该设备的控制器控制表(COCT)。检查COCT的状态。
如果控制器“忙”,则进程在COCT的等待队列中阻塞。
控制器就绪后,操作系统再根据COCT中的指针,找到连接该控制器的通道控制表(CHCT)。
同样,检查CHCT的状态。
如果通道“忙”,则进程在CHCT的等待队列中阻塞。

5、当设备、控制器、通道三者皆就绪后,操作系统构建一个通道程序,并将其地址填入CHCT。操作系统向通道发出启动I/O指令。

6、通道接管工作:它读取通道程序,通过控制器向设备发出具体的控制命令。此时,CPU被解放出来,可以执行其他任务。

假脱机技术—SPOOLing

Simultaneous Peripheral Operations On-Line
外围设备联机并行操作
作用:将独占设备改造成共享设备,从而极大地提升CPU和I/O设备的利用率和系统效率。
https://www.bilibili.com/video/BV1YE411D7nH?t=584.2&p=79

缓冲区—buffer—水库

缓冲区是一块临时的数据存储区,通常位于内存中,用于在两个速度不匹配或时序不同的实体之间平滑数据流。
https://www.bilibili.com/video/BV1YE411D7nH?t=376.0&p=81
缓冲区是SPOOLing的组成部分。

缓冲池

https://www.bilibili.com/video/BV1YE411D7nH?t=1296.1&p=81

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐