CPU核心或什么是SMP,它吃什么

引言


美好的一天,今天,我想谈谈一个普通程序员几乎不知道的相当简单的话题,但是你们每个人都最有可能使用它。
它将涉及对称多处理(通常称为SMP)-在所有多任务操作系统中都可以找到的体系结构,当然,它是它们不可或缺的一部分。 众所周知,处理器拥有的内核越多,处理器的功能就越强大,是的,但是一个操作系统如何同时使用多个内核? 有些程序员并没有达到抽象的程度,他们只是不需要抽象,但是我认为每个人都会对SMP的工作方式感兴趣。

多任务及其实现


那些曾经研究过计算机体系结构的人都知道处理器本身不能一次执行多个任务,多任务处理仅给我们提供了操作系统,可以切换这些任务。 有多种类型的多任务处理,但是最合适,最方便和广泛使用的是挤出多任务处理(您可以在Wikipedia上阅读其主要方面)。 它基于以下事实:每个进程(任务)都有自己的优先级,这会影响为其分配多少处理器时间。 给每个任务一个时间片,在该时间片中该进程执行某项操作;时间片到期后,操作系统将控制权转移给另一个任务。 出现了一个问题-如何分配计算机资源,例如内存,设备等。 进程之间? 一切都很简单:Windows自行完成,Linux使用信号量系统。 但是,一个核心并不严重,我们继续前进。

中断和PIC


也许对于某些人而言这将成为新闻,对于某些人而言则不是,但是i386架构(我将谈论x86架构,ARM不计其数,因为我没有研究过该架构,而且我从没碰过它(甚至在编写服务或驻留程序的级别))使用中断(我们将仅谈论硬件中断IRQ)以便将事件通知OS或程序。 例如,存在一个中断0x8(对于保护模式和长模式,例如0x20,具体取决于如何配置PIC,后面将进一步介绍),该中断由PIT调用,例如,它可以生成具有任何必要频率的中断。 然后,用于分配时间片的OS的工作量减少为0,当调用中断时,程序停止运行,并且将控制权交给了内核,例如,内核又保存了当前程序数据(寄存器,标志等),并控制了下一个进程。

如您所知,中断是在某个时间点由设备或程序本身调用的函数(或过程)。 处理器总共在两个PIC上支持16个中断。 处理器具有标志,其中之一是“ I”标志-中断控制。 通过将此标志设置为0,处理器将不会导致任何硬件中断。 但我也要注意,存在所谓的NMI-不可屏蔽中断-即使将位I设置为0,仍将调用中断数据。使用PIC编程,可以禁用中断数据,但是在从任何中断返回后, IRET-他们不会再次被禁止。 我注意到,在常规程序下,您无法跟踪中断调用-您的程序停止并且仅在一段时间后恢复,您的程序甚至都没有注意到(是的,您可以检查该中断已被调用-但是为什么?

PIC-可编程中断控制器

从Wiki:
通常,它是一种电子设备,有时是处理器本身或其框架的复杂芯片的一部分,其输入电连接到各种设备的相应输出。 中断控制器的输入编号由“ IRQ”指示。 该编号应与中断优先级以及中断向量表(INT)中的条目编号区分开。 因此,例如,在处理器处于实际操作模式(MS-DOS在此模式下工作)的IBM PC中,从标准键盘中断使用IRQ 1和INT 9。

原始的IBM PC平台使用非常简单的中断方案。 中断控制器是一个简单的计数器,它可以顺序迭代不同设备的信号,或者在发现新的中断时重置为开始。 在第一种情况下,设备具有相同的优先级;在第二种情况下,序列号较低(或计数较高)的设备具有较高的优先级。

如您所知,这是一条电子线路,允许设备发送中断请求,通常恰好有2个。

现在,让我们继续本文的主题。

SMP


为了实施该标准,主板上开始采用了新的方案:APIC和ACPI。 让我们先谈谈。

APIC-高级可编程中断控制器,PIC的改进版本。 它用于多处理器系统,并且是所有最新Intel处理器(和兼容处理器)的组成部分。 APIC用于复杂的中断转发以及在处理器之间发送中断。 使用较早的PIC规范无法实现这些操作。

本地APIC和IO APIC


在基于APIC的系统中,每个处理器都包含一个“核心”和一个“本地APIC”。 本地APIC负责处理特定于处理器的中断配置。 除其他外,它包含一个本地向量表(LVT),该向量表将事件(例如“内部时钟”和其他“本地”中断源)转换为中断向量(例如,联系人LocalINT1可以引发NMI异常,保留“ 2”到相应的LVT输入)。

有关本地APIC的更多信息,请参见现代英特尔处理器的“系统编程指南”。

此外,还有一个APIC IO(例如intel 82093AA),它是芯片组的一部分,并提供多处理器中断控制,包括所有处理器的静态和动态对称中断分配。 在具有多个I / O子系统的系统上,每个子系统可以具有自己的一组中断。

每个中断引脚都被单独编程为边沿触发或电平触发。 可以为每个中断指定中断向量和中断控制信息。 间接寄存器访问方案可优化访问内部APIC I / O寄存器所需的存储空间。 为了增加分配内存空间时的系统灵活性,可以重定位两个APIC I / O寄存器,但默认值为0xFEC00000。

初始化“本地” APIC


本地APIC在引导时被激活,可以通过将位11 IA32_APIC_BASE(MSR)复位来禁用(这仅适用于家族> 5的处理器,因为Pentium没有这样的MSR),然后处理器直接从兼容的8259 PIC接收其中断。 。 但是,英特尔的软件开发指南指出,通过IA32_APIC_BASE禁用本地APIC后,您将无法将其打开,直到将其完全重置。 还可以将APO IO配置为在传统模式下运行,以便模拟8259设备。

本地APIC映射到物理页FEE00xxx(请参阅表8-1 Intel P4 SPG)。 该地址对于配置中存在的每个本地APIC都是相同的,这意味着您可以直接访问当前在其中运行代码的本地APIC内核的寄存器。 请注意,有一个MSR定义了实际的APIC基础(仅适用于系列> 5的处理器)。 MADT包含本地APIC基础,在64位系统上,MADT可能还包含一个字段,该字段指定对基地址的64位重新定义,而应改用该字段。 您可以仅将本地APIC库保留在找到它的地方,也可以将其移动到任何需要的地方。 注意:我认为您不能将其移至第4 GB以上的RAM。

为了使本地APIC能够接收中断,您必须配置杂散中断向量寄存器。 该字段的正确值是您要映射到具有低8位的错误中断的IRQ号,并且将第8位设置为1以实际启用APIC(有关更多详细信息,请参见规范)。 您必须选择设置低4位的中断号。 最简单的方法是使用0xFF。 这对于某些较旧的处理器很重要,因为对于这些值,低4位必须设置为1。

正确禁用8259 PIC。 这几乎与配置APIC一样重要。 您分两个步骤执行此操作:屏蔽所有中断并重新分配IRQ。 掩盖所有中断会在PIC中将其禁用。 重映射中断是您使用PIC时可能已经执行的操作:您希望中断请求从32开始而不是从0开始,以避免与异常冲突(在受保护和长(长)处理器模式下,因为前32个中断是例外)。 然后,应避免将这些中断向量用于其他目的。 这是必要的,因为尽管您屏蔽了所有PIC中断,但它仍可能引发错误的中断,然后这些错误的中断将被错误地处理为内核中的异常。
让我们继续进行SMP。

对称多任务:初始化


对于不同的CPU,启动顺序是不同的。 英特尔程序员指南(第7.5.4节)包含针对英特尔至强处理器的初始化协议,并且不涵盖较早的处理器。 有关常规的“所有处理器类型”算法,请参阅英特尔多处理器规范。

对于80486(带有外部APIC 8249DX),必须使用IPIT INIT,然后使用IPI“ INIT级别取消声明”,而无需任何SIPI。 这意味着您无法告诉他们从哪里开始执行您的代码(SIPI的向量部分),并且他们总是开始执行BIOS代码。 在这种情况下,您将CMOS BIOS重置值设置为“以远跳开始热启动”(即,将CMOS 0x0F设置为10),以便BIOS执行jmp far〜[0:0x0469],然后设置段和偏移量AP入口点为0x0469。

新处理器(奔腾4和英特尔至强)不支持“ INIT级别无效” IPI,而在这些处理器上AFAIK被完全忽略。

对于较新的处理器(P6,Pentium 4),一个SIPI就足够了,但是我不确定较旧的Intel处理器(奔腾)或其他制造商的处理器是否需要第二个SIPI。 如果第一个SIPI的交付失败(总线噪声等),则第二个SIPI也可能存在。

通常,我发送第一个SIPI,然后等待以查看AP是否增加了正在运行的处理器的数量。 如果它没有在几毫秒内增加此计数器,我将发送第二个SIPI。 这与常规的Intel算法(SIPI之间的延迟为200微秒)不同,但是要设法找到一个可以在早期引导期间准确测量200微秒的延迟的时间源并不是那么简单。 我还发现,在实际的硬件上,如果SIPI之间的延迟太长(并且您没有使用我的方法),则主AP可以为操作系统运行两次早期的AP启动代码(在我的情况下,这会使操作系统认为我们拥有的处理器数量实际上是我们的两倍)。

您可以在总线上广播这些信号以启动存在的每个设备。 但是,您也可以打开特别禁用的处理器(因为它们“有缺陷”)。

使用MT表查找信息


一些用于多处理的信息(在较新的计算机上可能不可用)。 首先,您需要找到MP浮动指针结构。 它在16字节边界上对齐,并在“ _MP_”或0x5F504D5F的开头包含一个签名。 操作系统应查看EBDA,BIOS ROM空间以及“基本内存”的最后一个千字节。 基本内存的大小以2字节值0x413(以千字节为单位,减去1 KB)指定。 结构如下所示:

struct mp_floating_pointer_structure { char signature[4]; uint32_t configuration_table; uint8_t length; // In 16 bytes (eg 1 = 16 bytes, 2 = 32 bytes) uint8_t mp_specification_revision; uint8_t checksum; // This value should make all bytes in the table equal 0 when added together uint8_t default_configuration; // If this is not zero then configuration_table should be // ignored and a default configuration should be loaded instead uint32_t features; // If bit 7 is then the IMCR is present and PIC mode is being used, otherwise // virtual wire mode is; all other bits are reserved } 

指针的浮动结构指向的配置表如下所示:

 struct mp_configuration_table { char signature[4]; // "PCMP" uint16_t length; uint8_t mp_specification_revision; uint8_t checksum; // Again, the byte should be all bytes in the table add up to 0 char oem_id[8]; char product_id[12]; uint32_t oem_table; uint16_t oem_table_size; uint16_t entry_count; // This value represents how many entries are following this table uint32_t lapic_address; // This is the memory mapped address of the local APICs uint16_t extended_table_length; uint8_t extended_table_checksum; uint8_t reserved; } 

配置表之后是entry_count条目,其中包含有关系统的更多信息,后跟扩展表。 条目要么是代表处理器的20个字节,要么是代表其他内容的8个字节。 这是APIC处理器和I / O记录的样子。

 struct entry_processor { uint8_t type; // Always 0 uint8_t local_apic_id; uint8_t local_apic_version; uint8_t flags; // If bit 0 is clear then the processor must be ignored // If bit 1 is set then the processor is the bootstrap processor uint32_t signature; uint32_t feature_flags; uint64_t reserved; } 

这是IO APIC条目。

 struct entry_io_apic { uint8_t type; // Always 2 uint8_t id; uint8_t version; uint8_t flags; // If bit 0 is set then the entry should be ignored uint32_t address; // The memory mapped address of the IO APIC is memory } 

使用API​​C查找信息


您可以在ACPI中找到MADT表(APIC)。 该表列出了本地APIC,其数量应与处理器上的内核数量相对应。 该表的详细信息不在此处,但是您可以在Internet上找到它们。

启动AP


收集信息后,需要禁用PIC并准备APIC I / O。 您还需要配置本地APIC的BSP。 然后使用SIPI启动AP。

启动内核的代码:

我注意到,您在启动时指定的向量表示起始地址:向量0x8-地址0x8000,向量0x9-地址0x9000,依此类推。

 // ------------------------------------------------------------------------------------------------ static u32 LocalApicIn(uint reg) { return MmioRead32(*g_localApicAddr + reg); } // ------------------------------------------------------------------------------------------------ static void LocalApicOut(uint reg, u32 data) { MmioWrite32(*g_localApicAddr + reg, data); } // ------------------------------------------------------------------------------------------------ void LocalApicInit() { // Clear task priority to enable all interrupts LocalApicOut(LAPIC_TPR, 0); // Logical Destination Mode LocalApicOut(LAPIC_DFR, 0xffffffff); // Flat mode LocalApicOut(LAPIC_LDR, 0x01000000); // All cpus use logical id 1 // Configure Spurious Interrupt Vector Register LocalApicOut(LAPIC_SVR, 0x100 | 0xff); } // ------------------------------------------------------------------------------------------------ uint LocalApicGetId() { return LocalApicIn(LAPIC_ID) >> 24; } // ------------------------------------------------------------------------------------------------ void LocalApicSendInit(uint apic_id) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, ICR_INIT | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } // ------------------------------------------------------------------------------------------------ void LocalApicSendStartup(uint apic_id, uint vector) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, vector | ICR_STARTUP | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } void SmpInit() { kprintf("Waking up all CPUs\n"); *g_activeCpuCount = 1; uint localId = LocalApicGetId(); // Send Init to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) { LocalApicSendInit(apicId); } } // wait PitWait(200); // Send Startup to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) LocalApicSendStartup(apicId, 0x8); } // Wait for all cpus to be active PitWait(10); while (*g_activeCpuCount != g_acpiCpuCount) { kprintf("Waiting... %d\n", *g_activeCpuCount); PitWait(10); } kprintf("All CPUs activated\n"); } 

 [org 0x8000] AP: jmp short bsp ;     -   BSP xor ax,ax mov ss,ax mov sp, 0x7c00 xor ax,ax mov ds,ax ; Mark CPU as active lock inc byte [ds:g_activeCpuCount] ;   ,   jmp zop bsp: xor ax,ax mov ds,ax mov dword[ds:g_activeCpuCount],0 mov dword[ds:g_activeCpuCount],0 mov word [ds:0x8000], 0x9090 ;  JMP   2 NOP' ;   ,   

现在,正如您所了解的那样,为了使OS使用多个内核,您需要为每个内核,每个内核,其中断等配置堆栈,但是最重要的是,在使用对称多处理时,所有内核都具有相同的资源:一个内存,一个PCI等,而操作系统只能在内核之间并行执行任务。

我希望这篇文章不足够无聊,并且能提供很多信息。 我想下一次,我们可以讨论它们以前如何在屏幕上绘制(现在又绘制),而无需使用着色器和炫酷的视频卡。

祝你好运

Source: https://habr.com/ru/post/zh-CN426497/


All Articles