x86 汇编并没有多线程之类的并行指令,那操作系统的多线程是如何实现的?

您所在的位置:网站首页 mov的文件是一个 x86 汇编并没有多线程之类的并行指令,那操作系统的多线程是如何实现的?

x86 汇编并没有多线程之类的并行指令,那操作系统的多线程是如何实现的?

#x86 汇编并没有多线程之类的并行指令,那操作系统的多线程是如何实现的?| 来源: 网络整理| 查看: 265

题主的问题:

机器指令没有多线程相关指令,也没法规定某个指令在某个核上运行,那操作系统的多线程,并且能保证不同线程在不同的核心上运行,这是怎么实现的?

关于操作系统的进程上下文切换和进程调度方面,其他答主已经讲的比较详细了,那么我就来具体说一下“如何规定某个指令在某个核上运行”这件事吧。

首先需要指出的是,为了操作 CPU 的各种功能,通常我们只需要通过读写各种各样的寄存器来完成,并不需要对每个功能都准备特殊的指令。比如说对于 x86,你可以:

通过读写特定的内存地址来读写以内存映射方式存在的寄存器,例如我们下面要用到的 Local APIC 的寄存器;通过 rdmsr / wrmsr 指令操作 MSRModel Specific Register,模型特定寄存器),例如 x2APIC 的寄存器;通过 in / out 指令,例如经典的通过 0x92 端口开启 A20 Gate。x86 多处理器系统的启动流程

回到正题。简单来说,对于 x86 来说,多处理器[1]的初始化过程大致上是这样的:

系统启动时,CPU 会选择一个处理器作为 BSPBootstrap Processor,引导处理器),其他处理器作为 APApplication Processor,应用处理器)。BSP 负责执行系统的初始化和引导等操作,而 AP 们则会停机,默默等待被 BSP 唤醒。在引导完成,控制权交给操作系统时,依然是只在 BSP 正在运行,AP 们等待唤醒的状态。操作系统在初始化时会在 BSP 上通过 Local APIC 向其他 AP 发出信号,唤醒其他 AP。

这个时候你可能想问了,这个 APIC 是个什么东西?到底要怎么向其他 AP 发送信号?AP 被唤醒之后要怎么知道从哪里执行代码?下面就来详细介绍一下这个部分。

用 APIC 唤醒其他处理器

翻开 Intel 的官方手册 Intel® 64 and IA-32 Architectures Software Developer Manuals,在 卷3 11.1 Local and I/O APIC Overview 的开头我们可以看到这样一张图:

简单来说,APIC 分为 Local APIC 和 I/O APIC,它们是负责处理各种中断的控制器,包括外设到处理器的中断以及处理器之间的中断。BSP 唤醒其他 AP 的信号就是“处理器间的中断”,所以这里我们只需要关心 Local APIC 的用法。

想要使用 Local APIC,首先我们需要启用它。启用 Local APIC 的开关在一个名为 Spurious Interrupt Vector Register 的寄存器里:

默认情况下这个寄存器位于物理内存地址的 0xFEE000F0[2] 处,我们只需要通过读写此处的内存就可以访问这个寄存器。我们主要关心的是第 8 位 APIC Software Enable/Disable,将其设为 1 即可启用 Local APIC。写成汇编就是这样:

APIC_SVR equ 0xFEE000F0 ; Spurious Interrupt Vector Register ; 启用 APIC mov eax, [APIC_SVR] ; 读取内存地址 0xFEE000F0 处的32位值 or eax, 0x00000100 ; 将第8位置1 mov [APIC_SVR], eax ; 写回内存地址 0xFEE000F0 处

如果看不懂汇编的话,用 C 语言写就是这样:

#define APIC_SVR (*(volatile uint32_t*)0xFEE000F0) APIC_SVR |= 0x00000100;

启用 Local APIC 之后,我们就可以通过它向其他 AP 发送 IPIInterprocessor Interrupt,处理器间中断)了。根据手册 卷3 9.4 Multiple-Processor (MP) Initialization 的内容,为了唤醒其他 AP,我们需要让 BSP 按顺序发送一个 INIT-SIPI-SIPI 序列:

首先发送 INIT (初始化处理器并进入 wait-for-SIPI 状态),并等待 10 毫秒;发送 SIPIStartup IPI,退出 wait-for-SIPI 状态开始执行代码),并等待 200 微秒;如果 AP 没有被唤醒的话,再次发送 SIPI。

实际操作系统中的初始化过程可能会和这个步骤有所区别,比如 Linux 会在 INIT 之后再 De-assert INIT(出于兼容性考虑,只有较旧的处理器需要这一步),并且只会发送一次 SIPI(实际上一次 SIPI 已经足以唤醒 AP 了),并且有不少额外的处理各种兼容性问题的代码,此处可以参考 Linux 的源码(/arch/x86/kernel/smpboot.c)。

发送 IPI 是通过写入另一个位于 0xFEE00300 处,名为 Interrupt Command Register 的 Local APIC 寄存器来完成的,它的结构如图所示:

看着有点复杂,一个个来解释吧。

Destination Field 和 Destination Shorthand:指定发送的目标。Local APIC 可以向 Destination Field 指定的单个处理器发送 IPI,也可以直接通过 Destination Shorthand 向所有处理器(包括 / 不包括自己)发送 IPI。由于我们想给所有其他处理器发送 IPI,所以可以直接将 Destination Shorthand 设为 11(All Excluding Self),而不需要一个个给其他处理器发 IPI 了。Trigger Mode 和 Level:只有在 INIT De-assert 时有用,这里我们直接填 0 和 1 即可。Delivery Status:读取这一位可以检查 IPI 是否发送完成,写入时填 0 即可。Destination Mode:当 Destination Shorthand 为 00 时可以选择 Destination Field 是 Physical 还是 Logical 的 ID,这里填 0 即可。Delivery Mode:要发送的 IPI 类型,INIT 和 SIPI 依次为 101 和 110。Vector:8位的中断向量号,对于不同 IPI 有不同含义:发送 INIT 时,填 0 即可;发送 SIPI 时,通过该向量号可以指定一个 1MiB 范围以内的 4KiB 对齐内存地址,作为 AP 被唤醒时程序开始执行的物理内存地址,例如发送一个 Vector 为 0x12 的 SIPI 可以让 AP 从 0x00012000 内存地址处开始运行。BSP 只需要提前将代码写入该地址处并发送 SIPI,即可让 AP 被唤醒时开始执行相应的代码。

将上面这些合起来,我们可以得到:发送 INIT 时需要向 ICR 写入 0x000C4500,发送 SIPI 时需要向 ICR 写入 0x000C46XXXX 为指定的 AP 开始运行代码的地址。写成汇编就是这样:

APIC_ICR_LOW equ 0xFEE00300 ; Interrupt Command Register AP_START_ADDR equ 0x00008000 ; 该地址需要为 0x000XX000 的形式 ; 向所有其他 AP 发送 INIT mov eax, 0x000C4500 mov [APIC_ICR_LOW], eax ; 延时 10ms ; 向所有其他 AP 发送 SIPI mov eax, 0x000C4600 | (AP_START_ADDR >> 12) mov [APIC_ICR_LOW], eax

AP 被唤醒后就会开始执行操作系统放在指定位置的代码,进行各自的初始化操作。当各个处理器完成初始化工作之后,就会开始不断从进程队列中获取下一个要执行的进程,进行进程调度。此时如果想在某个处理器上执行指定的指令,只需要让对应的处理器调度该指令所在的的进程,就可以做到在规定的处理器上执行指定指令的效果了。关于操作系统进程调度方面的过程,已经有其他答主进行回答了,这里也就不再赘述。

顺带一提,在 AP 被唤醒之后,它们要怎么知道自己是谁呢?这时候只需要读取位于 0xFEE00020 处的 Local APIC ID 寄存器,获取自己的 ID 就可以知道自己是几号处理器了。一般情况下,BSP 的 Local APIC ID 为 0。

APIC_ID equ 0xFEE00020 ; Local APIC ID Register ; 获取 Local APIC ID mov eax, [APIC_ID] shr eax, 24动手实践

说了这么多,是时候动手实践一下了。为了展示 x86 多处理器的初始化流程,我编写了一段可以在 qemu 中唤醒多处理器并执行的 NASM 汇编,每个 AP 在被激活之后会在屏幕上显示一个字符。读懂这段代码需要一些汇编知识以及实模式 / 保护模式的知识,可以参考《Orange'S:一个操作系统的实现》等书。代码如下:

APIC_ID equ 0xFEE00020 ; Local APIC ID Register APIC_SVR equ 0xFEE000F0 ; Spurious Interrupt Vector Register APIC_ICR_LOW equ 0xFEE00300 ; Interrupt Command Register AP_START_ADDR equ 0x00008000 ; 该地址需要为 0x000XX000 的形式 org 0x7C00 ; 引导扇区会被加载到内存 0x7C00 处 [bits 16] ; BSP 引导时处于实模式 cli jmp 0x0000:bspStart bspStart: ; 进入保护模式,不然我们访问不到 0xFEE00XXX 处的 Local APIC 寄存器 mov ax, 0x0000 mov ds, ax lgdt [GDT_DESC] mov eax, cr0 or eax, 0x00000001 mov cr0, eax jmp dword GDT_CODE - GDT:bspProtect [bits 32] bspProtect: mov ax, GDT_DATA - GDT mov ds, ax mov es, ax ; 将 apStart 部分代码复制到 AP_START_ADDR mov esi, apStart mov edi, AP_START_ADDR mov ecx, apStartEnd - apStart cld rep movsb ; 启用 APIC mov eax, [APIC_SVR] or eax, 0x00000100 ; APIC Software Enable/Disable = 1 mov [APIC_SVR], eax ; 向所有其他 AP 发送 INIT mov eax, 0x000C4500 mov [APIC_ICR_LOW], eax ; 延时,简单起见直接使用了一个空循环,下同 mov ecx, 100000000 loop $ ; 向所有其他 AP 发送 SIPI mov eax, 0x000C4600 | (AP_START_ADDR >> 12) mov [APIC_ICR_LOW], eax ; 延时 mov ecx, 100000000 loop $ ; 获取 Local APIC ID mov ebx, [APIC_ID] shr ebx, 24 ; 在屏幕上绘制绿底白字的字符 mov edi, 0x000B8A00 mov eax, ebx mov cl, 10 div cl add ah, '0' mov [edi + 2 * ebx], ah ; 显示字符为 Local APIC ID % 10 mov byte [edi + 2 * ebx + 1], 0x2F ; 停机 hlt jmp $ [bits 16] ; AP 唤醒时处于实模式 apStart: jmp 0x0000:apMain apStartEnd: apMain: ; 进入保护模式 mov ax, 0x0000 mov ds, ax lgdt [GDT_DESC] mov eax, cr0 or eax, 0x00000001 mov cr0, eax jmp dword GDT_CODE - GDT:apProtect [bits 32] apProtect: mov ax, GDT_DATA - GDT mov ds, ax ; 获取 Local APIC ID mov ebx, [APIC_ID] shr ebx, 24 ; 在屏幕上绘制蓝底白字的字符 mov edi, 0x000B8A00 mov eax, ebx mov cl, 10 div cl add ah, '0' mov [edi + 2 * ebx], ah ; 显示字符为 Local APIC ID % 10 mov byte [edi + 2 * ebx + 1], 0x1F ; 停机 hlt jmp $ GDT: dq 0x0000000000000000 GDT_CODE: ; 平坦代码段 dq 0x00CF9A000000FFFF GDT_DATA: ; 平坦数据段 dq 0x00CF92000000FFFF GDT_END: GDT_DESC: dw GDT_END - GDT - 1 dd GDT times 510 - ($ - $$) db 0x00 db 0x55, 0xAA ; 可引导标识

生成二进制文件:

nasm Test.asm -o Test.bin

在 qemu 中运行:

qemu-system-x86_64 -smp 4 -m 1M -drive file=Test.bin,format=raw

绿色背景为 BSP(0号) 的输出,蓝色背景为 AP(1~3号)的输出。

甚至可以一次用 -smp 255 让 qemu 开 255 个核[3]玩!(可能会很卡哦 :))

数框框!参考链接Intel® 64 and IA-32 Architectures Software Developer Manuals兰新宇:x86-64的多核初始化老狼:兄弟阋墙,CPU内核们是如何争当老大的?Hakutaku: SMP on x86-64 | Codetector :: Yaotian FengOSDev.org • View topic - INIt-SIPI-SIPI参考^本文中的处理器指的都是逻辑处理器,比如说一个 X 核 Y 线程的 CPU,逻辑处理器的数量就是 Y。^Local APIC 寄存器的基址实际上并不固定,可以通过 IA32_APIC_BASE 这个 MSR 进行更改。默认情况下是 0xFEE00000。^qemu 支持的最大值。而且处理器更多的话普通的 APIC 就识别不了了,得用 x2APIC。


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3