StratoVirt 的 vCPU 拓撲(SMP)

JIAN2發表於2023-09-27

CPU 拓撲用來表示 CPU 在硬體層面的組合方式,本文主要講解 CPU 拓撲中的 SMP(Symmetric Multi-Processor,對稱多處理器系統)架構,CPU 拓撲還包括其他資訊,比如:cache 等,這些部分會在後面進行補充。CPU 拓撲除了描述 CPU 的組成關係外,還為核心的排程器提供服務,從而提供更好的效能。在 StratoVirt 中,支援 CPU 拓撲為後續的 CPU 熱插拔開發打下一個基礎。

常見的 CPU SMP 結構是:

Socket --> die --> cluster --> core --> thread
  • socket:對應主機板上的 CPU 插槽

  • die:處理器在生產過程中,從晶圓上切割下來的一個個小方塊,Die 之間的元件是透過片內匯流排互聯的。

  • cluster:簇,大核或者小核的一種組合

  • core:表示獨立的物理 CPU

  • thread:邏輯 CPU,英特爾超執行緒技術引入的新概念

CPU 拓撲的獲取原理

因為 x86 和 ARM 的拓撲獲取方式不同,下面將會分開進行介紹。

x86

在 x86 架構下面,作業系統會透過讀取 CPUID 來獲取 CPU 拓撲結構。在 x86 體系結構中,CPUID 指令(由 CPUID 操作碼標識)是處理器補充指令(其名稱源自 CPU 標識),允許軟體發現處理器的細節。程式可以使用 CPUID 來確定處理器型別。

CPUID 隱式使用 EAX 暫存器來確定返回的資訊的主要類別,這被稱為 CPUID 葉。跟 CPU 拓撲相關的 CPUID 葉分別是:0BH 和 1FH。1FH 是 0BH 的擴充套件,可以用來表示更多的層級。Intel 建議先檢查 1FH 是否存在,如果 1FH 存在會優先使用它。當 EAX 的值被初始化為 0BH 的時候,CPUID 會在 EAX,EBX,ECX 和 EDX 暫存器中返回 core/logical 處理器拓撲資訊。這個函式(EAX=0BH)要求 ECX 同時被初始化為一個 index,這個 index 表示的是在 core 層級還是 logical processor 層級。OS 呼叫這個函式是按 ECX=0,1,2..n 這個順序呼叫的。返回處理器拓撲級別的順序是特定的,因為每個級別報告一些累積資料,因此一些資訊依賴於從先前級別檢索到的資訊。在 0BH 下,ECX 可以表示的層級有:SMT 和 Core,在 1FH 下,可以表示的層級有:SMT,Core,Module,Tile 和 Die。

下表是一個更詳細的一個解釋:

Initial EAX Value Information Provided about the Processor
0BH EAX Bits 04 - 00: Number of bits to shift right on x2APIC ID to get a unique topology ID of the next level type*. All logical processors with the same next level ID share current level. Bits 31 - 05: Reserved. EBX Bits 15 - 00: Number of logical processors at this level type. The number reflects configuration as shipped by Intel. Bits 31- 16: Reserved. ECX Bits 07 - 00: Level number. Same value in ECX input. Bits 15 - 08: Level type. Bits 31 - 16: Reserved. EDX Bits 31- 00: x2APIC ID the current logical processor.
1FH EAX Bits 04 - 00: Number of bits to shift right on x2APIC ID to get a unique topology ID of the next level type*. All logical processors with the same next level ID share current level. Bits 31 - 05: Reserved. EBX Bits 15 - 00: Number of logical processors at this level type. The number reflects configuration as shipped by Intel. Bits 31- 16: Reserved. ECX Bits 07 - 00: Level number. Same value in ECX input. Bits 15 - 08: Level type. Bits 31 - 16: Reserved. EDX Bits 31- 00: x2APIC ID the current logical processor

來源: Intel 64 and IA-32 Architectures Software Developer's Manual

ARM

在 ARM 架構下,如果作業系統是依靠 Device Tree 啟動的,則會透過 Device Tree 去獲取 CPU 拓撲。如果是以 ACPI 的方式啟動的話,作業系統會透過解析 ACPI 的 PPTT 表去獲取 CPU 拓撲結構。

ACPI——PPTT

ACPI 是 Advanced Configuration and Power Interface (高 級配置和電源介面)的縮寫,ACPI 是一種與體系結構無關的電源管理和配置框架。這個框架建立了一個硬體暫存器集合來定義電源狀態。ACPI 是作業系統和韌體之間的一箇中間層,是他們兩者之間的一個介面。ACPI 定義了兩種資料結構:data tables 和 definition blocks。data tables 用於儲存給裝置驅動使用的 raw data。definition blocks 由一些位元組碼組成,這些碼可以被直譯器執行。

為了使硬體供應商在選擇其實施時具有靈活性,ACPI 使用表格來描述系統資訊、功能和控制這些功能的方法。這些表列出了系統主機板上的裝置或無法使用其他硬體標準檢測或電源管理的裝置,以及 ACPI 概念中所述的功能。它們還列出了系統功能,如支援的睡眠電源狀態、系統中可用的電源平面和時鐘源的說明、電池、系統指示燈等。這使 OSPM 能夠控制系統裝置,而不需要知道系統控制是如何實現的。

PPTT 表就是其中的一個表格,PPTT 表全稱是 Processor Properties Topology Table,處理器屬性拓撲表用於描述處理器的拓撲結構,該表還可以描述附加資訊,例如處理器拓撲中的哪些節點構成物理包。

下表是 PPTT 表的結構,包含一個表頭和主體,表頭和其他的 ACPI 表差別不大。其中  Signature 用於表示這是 PPTT 表, Length 是整張表的大小,其他的資訊可以檢視下面的這張表。表的主體是一系列處理器拓撲結構。

下面的表表示處理器層級節點結構,表示處理器結構的話  Type 要設定為 0, Length 表示這個節點的位元組數。 Flags 用來描述跟處理器相關的資訊,詳細的看後面關於  Flags 的詳細資訊。 Parent 用於指向這個節點的上一級節點,存放的是一個偏移量地址

下表是  Flags 的結構, Flags 佔據 4 個位元組的長度。 Physical package:如果處理器拓撲的此節點表示物理封裝的邊界,則設定  Physical package 為 1。如果處理器拓撲的此例項不表示物理軟體包的邊界,則設定為 0。 Processor is a Thread:對於葉條目:如果代表此處理器的處理元素與兄弟節點共享功能單元,則必須將其設定為 1。對於非葉條目:必須設定為 0。 Node is a Leaf:如果節點是處理器層次結構中的葉,則必須設定為 1。否則必須設定為 0。

參考:

Device Tree

Device Tree 是一種描述硬體的資料結構。核心的啟動程式會將裝置樹載入入記憶體中,然後透過解析 Device Tree 來獲取硬體細節。Device Tree 是樹形結構,由一系列被命名的節點和屬性組成,節點可以包含子節點,它們之間的關係構成一棵樹。屬性就是 name 和 value 的鍵值對。

一個典型的裝置樹如下圖:

ARM 的 CPU 拓撲是定義在 cpu-map 節點內,cpu-map 是 cpu 節點的子節點。在 cpu-map 節點裡可以包含三種子節點:cluster 節點,core 節點,thread 節點。整個 dts 的例子如下:

cpus {
 #size-cells = <0>;
 #address-cells = <2>;
 cpu-map {
  cluster0 {
   cluster0 {
    core0 {
     thread0 {
      cpu = <&CPU0>;
     };
     thread1 {
      cpu = <&CPU1>;
     };
    };
    core1 {
     thread0 {
      cpu = <&CPU2>;
     };
     thread1 {
      cpu = <&CPU3>;
     };
    };
   };
   cluster1 {
    core0 {
     thread0 {
      cpu = <&CPU4>;
     };
     thread1 {
      cpu = <&CPU5>;
     };
    };
    core1 {
     thread0 {
      cpu = <&CPU6>;
     };
     thread1 {
      cpu = <&CPU7>;
     };
    };
   };
  };
    };
    //...
};

參考:https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/topology.txt

圖來源:https://www.devicetree.org/specifications/

StratoVirt 具體實現

CPUID

首先我們需要計算每個拓撲結構唯 一的 topology ID,然後獲取或者自己建立相對應的 CPUID entry,當 entry 的 function 的值等於 0xB 和 0X1F 的時候,我們需要根據 CPUID 的規範去設定相對應的 EAX, EBX, ECX 的值。EAX 設定為拓撲 ID,EBX 用來表示那個層級的有幾個邏輯處理器,ECX 表示層級號。0xB 需要配置 index 等於 0,1 對應的值,0x1F 需要配置 index 等於 0,1,2 對應的值。下面是相對應的程式碼:

// cpu/src/x86_64/mod.rs
const ECX_INVALID: u32 = 0u32 << 8;
const ECX_THREAD: u32 = 1u32 << 8;
const ECX_CORE: u32 = 2u32 << 8;
const ECX_DIE: u32 = 5u32 << 8;
impl X86CPUState {
    fn setup_cpuid(&self, vcpu_fd: &Arc<VcpuFd>) -> Result<()> {
        // 計算 topology ID
        let core_offset = 32u32 - (self.nr_threads - 1).leading_zeros();
        let die_offset = (32u32 - (self.nr_cores - 1).leading_zeros()) + core_offset;
        let pkg_offset = (32u32 - (self.nr_dies - 1).leading_zeros()) + die_offset;
        // 獲取 KVM 的 fd 和 獲取它支援的 CPUID entries
        for entry in entries.iter_mut() {
            match entry.function {
                // ...
                0xb => {
                    // Extended Topology Enumeration Leaf
                    entry.edx = self.apic_id as u32;
                    entry.ecx = entry.index & 0xff;
                    match entry.index {
                        0 => {
                            entry.eax = core_offset;
                            entry.ebx = self.nr_threads;
                            entry.ecx |= ECX_THREAD;
                        }
                        1 => {
                            entry.eax = pkg_offset;
                            entry.ebx = self.nr_threads * self.nr_cores;
                            entry.ecx |= ECX_CORE;
                        }
                        _ => {
                            entry.eax = 0;
                            entry.ebx = 0;
                            entry.ecx |= ECX_INVALID;
                        }
                    }
                }
                // 0x1f 擴充套件,支援 die 層級
                0x1f => {
                    if self.nr_dies < 2 {
                        entry.eax = 0;
                        entry.ebx = 0;
                        entry.ecx = 0;
                        entry.edx = 0;
                        continue;
                    }
                    entry.edx = self.apic_id as u32;
                    entry.ecx = entry.index & 0xff;
                    match entry.index {
                        0 => {
                            entry.eax = core_offset;
                            entry.ebx = self.nr_threads;
                            entry.ecx |= ECX_THREAD;
                        }
                        1 => {
                            entry.eax = die_offset;
                            entry.ebx = self.nr_cores * self.nr_threads;
                            entry.ecx |= ECX_CORE;
                        }
                        2 => {
                            entry.eax = pkg_offset;
                            entry.ebx = self.nr_dies * self.nr_cores * self.nr_threads;
                            entry.ecx |= ECX_DIE;
                        }
                        _ => {
                            entry.eax = 0;
                            entry.ebx = 0;
                            entry.ecx |= ECX_INVALID;
                        }
                    }
                }
                // ...
            }
        }
}

PPTT

根據 ACPI PPTT 表的標準來構建,我們需要計算每個節點的偏移值用於其子節點指向它。我們還需要計算每個節點的 uid,uid 初始化為 0,每增加一個節點 uid 的值加一。還需要根據 PPTT 表的標準計算 Flags 的值。最後需要計算整張表的大小然後修改原來的長度的值。

// machine/src/standard_vm/aarch64/mod.rs
impl AcpiBuilder for StdMachine {
    fn build_pptt_table(
        &self,
        acpi_data: &Arc<Mutex<Vec<u8>>>,
        loader: &mut TableLoader,
    ) -> super::errors::Result<u64> {
        // ...
        // 配置 PPTT 表頭
        // 新增 socket 節點
        for socket in 0..self.cpu_topo.sockets {
            // 計算到起始地址的偏移量
            let socket_offset = pptt.table_len() - pptt_start;
            let socket_hierarchy_node = ProcessorHierarchyNode::new(0, 0x2, 0, socket as u32);
            // ...
            for cluster in 0..self.cpu_topo.clusters {
                let cluster_offset = pptt.table_len() - pptt_start;
                let cluster_hierarchy_node =
                    ProcessorHierarchyNode::new(0, 0x0, socket_offset as u32, cluster as u32);
                // ...
                for core in 0..self.cpu_topo.cores {
                    let core_offset = pptt.table_len() - pptt_start;
                    // 判斷是否需要新增 thread 節點
                    if self.cpu_topo.threads > 1 {
                        let core_hierarchy_node =
                            ProcessorHierarchyNode::new(0, 0x0, cluster_offset as u32, core as u32);
                        // ...
                        for _thread in 0..self.cpu_topo.threads {
                            let thread_hierarchy_node =
                                ProcessorHierarchyNode::new(0, 0xE, core_offset as u32, uid as u32);
                            // ...
                            uid += 1;
                        }
                    } else {
                        let thread_hierarchy_node =
                            ProcessorHierarchyNode::new(0, 0xA, cluster_offset as u32, uid as u32);
                        // ...
                        uid += 1;
                    }
                }
            }
        }
        // 將 PPTT 表新增到 loader 中
    }
}

Device Tree

StratoVirt 的 microvm 使用 device tree 啟動,所以我們需要配置 device tree 中的 cpus 節點下的 cpu-map 來使 microvm 支援解析 CPU 拓撲。在 StratoVirt 中,我們支援兩層 cluster。我們使用了多層迴圈來建立這個 tree,第一層是建立第一層 cluster,第二層對應建立第二層的 cluster,第三層建立 core,第四層建立 thread。

impl CompileFDTHelper for LightMachine {
    fn generate_cpu_nodes(&self, fdt: &mut FdtBuilder) -> util::errors::Result<()> {
        // 建立 cpus 節點
        // ...
        // Generate CPU topology
        // 建立 cpu-map 節點
        let cpu_map_node_dep = fdt.begin_node("cpu-map")?;
        // 建立第一層 cluster 節點
        for socket in 0..self.cpu_topo.sockets {
            let sock_name = format!("cluster{}", socket);
            let sock_node_dep = fdt.begin_node(&sock_name)?;
            // 建立第二層 cluster 節點
            for cluster in 0..self.cpu_topo.clusters {
                let clster = format!("cluster{}", cluster);
                let cluster_node_dep = fdt.begin_node(&clster)?;
                // 建立 core 節點
                for core in 0..self.cpu_topo.cores {
                    let core_name = format!("core{}", core);
                    let core_node_dep = fdt.begin_node(&core_name)?;
                    // 建立 thread 節點
                    for thread in 0..self.cpu_topo.threads {
                        let thread_name = format!("thread{}", thread);
                        let thread_node_dep = fdt.begin_node(&thread_name)?;
                        // 計算 cpu 的 id
                        // let vcpuid = ...
                        // 然後新增到節點中
                    }
                    fdt.end_node(core_node_dep)?;
                }
                fdt.end_node(cluster_node_dep)?;
            }
            fdt.end_node(sock_node_dep)?;
        }
        fdt.end_node(cpu_map_node_dep)?;
        Ok(())
    }
}

這個程式碼構建出來裝置樹的結構和前面原理中展示的結構基本一致

驗證方法

我們可以透過下面的命令啟動一個虛擬機器, smp 引數用來配置 vCPU 拓撲

sudo ./target/release/stratovirt \
    -machine virt \
    -kernel /home/hwy/std-vmlinux.bin.1 \
    -append console=ttyAMA0 root=/dev/vda rw reboot=k panic=1 \
    -drive file=/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw,if=pflash,unit=0,readonly=true \
    -drive file=/home/hwy/openEuler-22.03-LTS-stratovirt-aarch64.img,id=rootfs,readonly=false \
    -device virtio-blk-pci,drive=rootfs,bus=pcie.0,addr=0x1c.0x0,id=rootfs \
    -qmp unix:/var/tmp/hwy.socket,server,nowait \
    -serial stdio \
    -m 2048 \
    -smp 4,sockets=2,clusters=1,cores=2,threads=1

接著,我們可以透過觀察  /sys/devices/system/cpu/cpu0/topology 下面的檔案來檢視配置的 topology。

[root@StratoVirt topology] ll
total 0
-r--r--r-- 1 root root 64K Jul 18 09:04 cluster_cpus
-r--r--r-- 1 root root 64K Jul 18 09:04 cluster_cpus_list
-r--r--r-- 1 root root 64K Jul 18 09:04 cluster_id
-r--r--r-- 1 root root 64K Jul 18 09:04 core_cpus
-r--r--r-- 1 root root 64K Jul 18 09:04 core_cpus_list
-r--r--r-- 1 root root 64K Jul 18 09:01 core_id
-r--r--r-- 1 root root 64K Jul 18 09:01 core_siblings
-r--r--r-- 1 root root 64K Jul 18 09:04 core_siblings_list
-r--r--r-- 1 root root 64K Jul 18 09:04 die_cpus
-r--r--r-- 1 root root 64K Jul 18 09:04 die_cpus_list
-r--r--r-- 1 root root 64K Jul 18 09:04 die_id
-r--r--r-- 1 root root 64K Jul 18 09:04 package_cpus
-r--r--r-- 1 root root 64K Jul 18 09:04 package_cpus_list
-r--r--r-- 1 root root 64K Jul 18 09:01 physical_package_id
-r--r--r-- 1 root root 64K Jul 18 09:01 thread_siblings
-r--r--r-- 1 root root 64K Jul 18 09:04 thread_siblings_list

比如:

cat core_cpus_list

結果是

0

表示和 cpu0 同一個 core 的 cpu 只有 cpu0。

cat package_cpus_list

會顯示

0-1

表示和 cpu0 同一個 socket 的 cpu 有從 cpu0 到 cpu1。

下面這些工具也可以輔助進行驗證。

比如:lscpu

lscpu

透過執行  lscpu 命令會出現下面結果

Architecture:            aarch64
  CPU op-mode(s):        32-bit, 64-bit
  Byte Order:            Little Endian
CPU(s):                  64
  On-line CPU(s) list:   0-63
Vendor ID:               ARM
  Model name:            Cortex-A72
    Model:               2
    Thread(s) per core:  1
    Core(s) per cluster: 16
    Socket(s):           -
    Cluster(s):          4
    Stepping:            r0p2
    BogoMIPS:            100.00
    Flags:               fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
NUMA:
  NUMA node(s):          4
  NUMA node0 CPU(s):     0-15
  NUMA node1 CPU(s):     16-31
  NUMA node2 CPU(s):     32-47
  NUMA node3 CPU(s):     48-63
Vulnerabilities:
  Itlb multihit:         Not affected
  L1tf:                  Not affected
  Mds:                   Not affected
  Meltdown:              Not affected
  Spec store bypass:     Vulnerable
  Spectre v1:            Mitigation; __user pointer sanitization
  Spectre v2:            Vulnerable
  Srbds:                 Not affected
  Tsx async abort:       Not affected



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70006413/viewspace-2986263/,如需轉載,請註明出處,否則將追究法律責任。

相關文章