一丁点儿的网络日志 - Linux内核
type: channel
link: https://www.dingmos.com/index.php/tag/Linux%E5%86%85%E6%A0%B8/
items: Linux | CentOS 7 多队列盘分配 CPU 不均问题分析
items: Linux 内核 | CPU 热插拔（Hotplug）
items: Linux 内核 | CPU 状态管理
items: Linux 内核 | 内核启动 cmdline, early_param 和 initcall
items: Linux 内核 | 内存管理——Slab 分配器
items: Linux 内核| 内核通知链机制
items: Linux 内核 | 内核镜像分析方法
items: Linux 内核 | Per CPU 变量

Linux | CentOS 7 多队列盘分配 CPU 不均问题分析
type: item
date: 2025-04-30T17:59:00+08:00
description: 背景XX 团队发现，当多个虚拟机（子机）部署在同一台 S6 标卡母机上时，即使 IO 压力未达到物理机瓶颈，多子机同时下发 IO 会导致性能下降。SPDK 侧定位发现，4 个核（线程）中仅有 2 个核处理大量 IO 请求，而另外 2 个核几乎空闲。问题的直接原因在 SPDK 单核性能达到瓶颈，最终导致子机 IO 性能劣化。问题现象和初步分析XX 团队使用的子机挂载的数据盘为 ​​2 队列盘​​，初步怀疑子机仅通过盘的第一个队列下发 IO，导致多个盘的 IO 集中到 SPDK 的单个核上。简化模型如下：​​SPDK 使用 2 个核​​（CPU 0 和 CPU 1）。​​所有盘的第一个队列（mq0）均映射到 SPDK 的 CPU 0 ​​，第二个队列（mq1）映射到 CPU 1。​​多个子机的 IO 请求均通过 mq0 下发​​，导致 CPU 0 过载，而 CPU 1 空闲。Linux 多队列（Multi-Queue）机制​Linux Kernel 会为硬盘的每个队列（mq）各自分配一个 cpu_list，如下所示： mq0 绑定到 CPU 0-7，mq1 绑定到 CPU 8-15 。运行于指定 CPU 的进程提交 IO 请求就会发送到 CPU 关联的 mq 上。ubuntu@VM-16-202-ubuntu:~$ cat /sys/block/vda/mq/0/cpu_list 0, 1, 2, 3, 4, 5, 6, 7 ubuntu@VM-16-202-ubuntu:~$ cat /sys/block/vda/mq/1/cpu_list 8, 9, 10, 11, 12, 13, 14, 15因此，首先解决 IO 请求不均问题的方案是尝试将 XX 团队的子机中的一半业务进程/线程绑定到 mq1 对应的 CPU 上。然而，在 XX 团队的子机上查看 cpu_list 却得到了以下结果：只有 CPU 1 分配给了 mq1 。这明显不符合预期，并且在这种情况下也不可能通过将多个线程绑定到单个 CPU1 上来解决问题。[root@VM-0-3-centos ~]# cat /sys/block/vda/mq/0/cpu_list 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 [root@VM-0-3-centos ~]# cat /sys/block/vda/mq/1/cpu_list 1问题深入定位​不同母机机型的影响很自然的，我们首先怀疑是 Centos 子机内核有 bug，但中间有一个小插曲：XX 团队的同学发现在 S5 母机上的子机，mq 对应的 cpu_list 却又是均匀的，如下所示。（这里有一个隐藏点，CentOS 上的 mq 对应的 cpu_list 和我前面贴的 ubuntu 子机的 cpu_list 不一样， CentOS 的某个 mq 对应的 cpu 是间隔的，ubuntu 的某个 mq 对应的 cpu 是连续的。）[root@VM-0-19-centos ~]# cat /sys/block/vda/mq/0/cpu_list 0, 2, 4, 6, 8, 10, 12, 14 [root@VM-0-19-centos ~]# cat /sys/block/vda/mq/1/cpu_list 1, 3, 5, 7, 9, 11, 13, 15不同的母机机型会导致相同的子机内核产生不同的 cpu_list 分配逻辑，这是一个很有意思的点，难道和虚拟化还相关？从这一点出发，对比两种机型上的子机的 xml 配置，发现了一个可疑的点：两者对子机 CPU 拓扑结构配置不同，同样 16 核的子机，S6 机型配置了超线程。进一步，通过在 S6 机型上将配置从 8 核 2 超线程模式修改成 16 独立核模式，重启子机后发现盘队列的 cpu_list 也就变均匀了。# S6 机型 <topology sockets='1' cores='8' threads='2'/> # S5 机型 <topology sockets='1' cores='16' threads='1'/>这表明 ​​问题与超线程（Hyper-Threading）相关​​，而非虚拟化（QEMU）本身的问题。进一步将 bug 范围缩小到子机内核为硬盘队列映射 cpu 的处理函数中。Linux Kernel 多队列映射逻辑分析​CentOS 7 的长期支持（LTS）内核基于 Linux 3.10，而 Multi-Queue（blk-mq）特性在 3.13 版本引入。对比 ​​Linux Upstream 3.13​​ 和 ​​CentOS 定制内核​​ 为盘队列映射 cpu 的处理函数实现，发现了关键差异。…

Linux 内核 | CPU 热插拔（Hotplug）
type: item
date: 2022-12-03T20:52:00+08:00
description: 引言CPU 热插拔[63] 是 Linux 中一种广泛使用于支持处理器集缩放的机制。它们被设计用于两种用途：移除出现故障的 CPU 或动态增加 CPU 计算资源和动态更改虚拟机配置的 CPU 数量。现代先进的系统架构已经引进了具有先进的错误报告和纠错能力的处理器。有一些原始设备生产商（Original Equipment Manufacturer，OEM）也支持可热插拔的NUMA硬件，其中物理节点的插入与移除要求 CPU 热插拔的支持。这样的升级需要删除 kernel 当前可用的 CPU，要么是出于配置（Provisioning）的原因，要么是出于RAS（Reliability、Availability、Serviceability）的目的，以使有问题的 CPU 远离系统执行路径。因此，Linux内核需要CPU热插拔支持。CPU热插拔支持在系统启动后，关闭任意一个secondary cpu（在 ARM64 架构下，CPU0 为boot cpu，不能被关闭），并在需要时重新启动它。例如，在内核选项CONFIG_HOTPLUG_CPU使能的前提下，通过如下命令可以将 CPUx 移除（offline）或加入（online）到当前内核的管理下。echo 0 > /sys/devices/system/cpu/cpu1/online echo 1 > /sys/devices/system/cpu/cpu1/online毕业设计涉及到 Linux 内核 CPU 热插拔的实现修改，所以当时在 CPU 热插拔花了许多时间，现将基本的知识总结一下。CPU 热插拔的基本功能具体来说，CPU 热插拔需要完成以下基本功能。 状态迁移。当 CPU 被移除时，与该 CPU 相关的重要软件状态必须迁移到另一个活跃的 CPU 上。软件状态通常包括软中断以及 CPU 运行队列中的线程。系统中的软中断和当前 CPU 上运行的线程都保存在 per-CPU 队列中， 软中断需要被移动到其他活跃的 CPU 以进行处理，运行队列中的线程也会同步 迁移以避免运行程序的性能下降。但在 per-CPU 的实现下，每个 CPU 都存在一份副本，当发生 CPU 热插拔时就需要为目标 CPU 重新分配或释放 per-CPU 结构 实例，这一定程度上增大了 CPU 热插拔的时间开销。硬件管理与中断迁移。在 CPU 热插拔事件期间，需要禁用或启用某些硬件 相关功能。例如，CPU 进入离线状态前必须禁用机器检查（例如，停止看门狗线程的执行），因为离线 CPU 中的任何故障都不应影响其余系统。同时，当 CPU 被移除时，关联到当前 CPU 的中断需要被移动到其他活跃的 CPU 上进行处理以 保证系统的正常运行。 系统状态更新。关键全局变量的更新。例如，Linux 维护着一些重要的 CPU 状态全局位掩码，它们在系统中被频繁访问。这些掩码应在缩放事件期间 立即更新，以确保系统正常运行。系统文件系统（sysfs）的更新。sysfs 和 procfs 等易失性文件系统通过 Linux 中的虚拟文件系统接口提供对内核配置和获 取内核的状态。sysfs 中包含的基于 CPU 核的文件或目录节点在 CPU 集缩放期间 需要被删除或创建。内存管理。多数内核子系统在收到缩放事件通知后会释 放或分配内存结构，例如 per-CPU 变量或缓冲区队列。线程停放（Parking）。 新版本的 Linux 使用线程停放来无限期地挂起线程。在 CPU 缩放期间，系统线 程（例如看门狗线程）会被置为停放状态。CPU 的移除过程CPU 热插拔子系统为其他内核子系统提供接口来订阅处理器集更改的通知。当 CPU 热插拔事件发生时，其他子系统注册的通知函数将被调用从而进行相应的处理以保证系统在 CPU 移除或加入时系统能感知并维持正常的运行。通过读取 proc 文件/sys/devices/system/cpu/hotplug/states的内容可查看系统内所有注册的回调函数。tail /sys/devices/system/cpu/hotplug/states一次 CPU 移除过程需要经过 CPU_DOWN_PREPARE 、CPU_DYING 、CPU_DEAD 和 CPU_POST_DEAD 总共四个阶段。具体来说，CPU 热插拔过程由各个内核子系统注册的回调函数组成。当 CPU 移除事件发生时，各子系统注册的回调函数被调用来为系统 CPU 集的变化做相 应的操作。CPU 热插拔每个阶段对应多个步骤，每个步骤主要由子系统注册回 调函数构成。不同平台的硬件差异性和不同 Linux 内核版本可能导致系统内各子 系统注册的回调函数存在一些区别。在本文的实验环境下（硬件平台树莓派 4B 和 Linux 内核 5.10 版本），…

Linux 内核 | CPU 状态管理
type: item
date: 2021-11-16T16:20:00+08:00
description: 纪念自己第一次成为一个大开源项目的 contributor，尽管只是一个很小的PR。什么是CPU mask内核使用CPU mask 来记录 CPU 的状态，cpumasks 提供了代表系统中 CPU 集合的位图，一个 bit 的0/1代表了一个 CPU在每种cpu mask 时的状态。实现（Linux kernel 5.10.20）主要包含在以下文件中：include/linux/cpumask.hkernel/cpu.cCPU 的状态基本可分为以下四种：cpu_possible ：是一个在系统启动时任意时刻都可插入的 cpu ID 集合。它将等于通过CONFIG_NR_CPUS内核配置选项静态设置的NR_CPUS的值；cpu_present ：表示当前哪个CPU是插入的；cpu_online ： 是 cpu_present 的一个子集，表示哪些CPU 是调度器可访问的；cpu_active ： 该掩码的某些 bit 告诉Linux内核，一个任务可能已移至某个处理器。内核声明了结构体cpumask ，并实例化 4 个cpumask 分别用来记录 CPU 的各种状态。以__cpu_possible_mask为例，其定义如下代码所示(定义在文件 kernel/cpu.c 中)。其中__read_mostly原语将定义的变量存放在 .data.read_mostly 段中，其定义在/arch/arm64/include/asm/cache.h文件中，原型为#define __read_mostly __section(.data..read_mostly)，将经常需要被读取的数据定义为__read_mostly类型， 这样Linux内核被加载时,该数据将自动被存放到Cache中,以提高整个系统的执行效率。struct cpumask __cpu_possible_mask __read_mostly; EXPORT_SYMBOL(__cpu_possible_mask);cpumask的定义如下，DECLARE_BITMAP宏用来计算 cpumask所需的大小。NR_CPUS为内核支持的最大 CPU 数，在内核编译时 make menuconfig 指定。typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;DECLARE_BITMAP宏定义在include/linux/types.h文件中，其接收两个参数：name，位图名；bits，位图所使用的bit数量。其展开也就是定义了一个以 long为元素的数组。#define DECLARE_BITMAP(name,bits) \ unsigned long name[BITS_TO_LONGS(bits)]最后看一下BITS_TO_LONGS是如何根据 bits 计算出占用多少个 long 的。此宏定义在include/linux/bitops.h文件中，如下：#define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, BITS_PER_TYPE(long))DIV_ROUND_UP宏展开如下：// include/linux/kernel.h #define DIV_ROUND_UP __KERNEL_DIV_ROUND_UP // include/uapi/linux/const.h #define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))假设NR_CPUS为8，long 占 8 个字节，cpumask结构体展开如下。也就是说，内核为每个 CPU 分配 long 变量中的一个 bit 来保存各个 CPU 的状态。 BITS_TO_LONGS(8) -> DIV_ROUND_UP(8, BITS_PER_TYPE(long)) -> DIV_ROUND_UP(8, 64) -> (((8) + (64) - 1) / (64)) -> 1 typedef struct cpumask { unsigned long bits[1] } cpumask_t; [注意！] 参考文档1按以下计算方式存在问题。若此时 CPU 数量是 9 个，计算得出需要 2 个 long 变量来保存CPU状态，这样就会很容易理解为一个 CPU的状态需要 1 个 btye 而不是 1 个 bit。 我向源Github仓库提交了 PR，不知道是否能合并进主线已经合并进了主线——首次成为一个大项目的 Contributor。(((8) + (8) - 1) / (8)) = 1 cpumask API对 CPU mask 的访问通过函数set_c…

Linux 内核 | 内核启动 cmdline, early_param 和 initcall
type: item
date: 2021-11-13T10:18:00+08:00
description: 本文主要记录 Linux kernel 获取 cmdline 、Linux early_param 启动参数解析以及 init_call 过程。基于ARM64架构，Linux kernel 5.10.20。cmdline 的初始化cmdline的初始化：start_kernel() -> setup_arch() 将 commad_line 指向 boot_command_line。很明显，这个变量将记录 cmdline。*cmdline_p = boot_command_line;但是内核从哪里获取 bootloader 传递的 cmdline 以及在哪里对boot_command_line初始化呢？boot_command_line 定义在 init/main.c 文件中 char __initdata boot_command_line[COMMAND_LINE_SIZE];。ARM64 架构下，设备树被来描述系统中所有的硬件信息，并在启动内核时向内核传递设备树文件（bootloader将设备树文件所在的地址保存在X0寄存器），内核解析设备树来对系统中存在的硬件进行初始化等操作。与 X86 架构传递 cmdline的方式不同（X86方式可参考链接[4]），ARM64架构下使用设备树传递 cmdline。设备树文件中的 chosen 节点通常包含许多信息，启动参数 cmdline 就包含在内，一个简单的例子如下：chosen { bootargs = "console=ttyS0,115200 loglevel=8"; initrd-start = <0xc8000000>; initrd-end = <0xc8200000>; };BTW：但是在使用树莓派时，我们可以通过修改/boot目录下的 cmdline.txt 文件直接修改树莓派4b的启动boot comand line。我估计是在树莓派的启动流程[2]中读取了cmdline.txt文件并修改了设备树的chosen节点，替换了启动参数。设备树解析在内核的启动流程中，early_init_dt_scan_nodes()函数解析设备树文件，提取chosen节点中bootargs保存到 boot_command_line 中。void __init early_init_dt_scan_nodes(void) { /* Retrieve various information from the /chosen node */ rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); ... } 函数调用关系总结函数调用关系如下：setup_arch() // arch/arm64/kernel/setup.c *cmdline_p = boot_command_line; ->setup_machine_fdt(__fdt_pointer) ->early_init_dt_scan() // driver/of/fdt.c ->early_init_dt_scan_nodes() -> early_init_dt_scan_chosen()Cmdline 参数解析与 early_param获取到 cmdline 后，Linux kernel 在 start_kernel() 函数中调用parse_early_param()对系统启动早期所需的配置参数进行解析并调用相应的回调。例如，系统启动时可能需要开启一个 earlycon 打印 log 或从设备树文件中获取指定 mem 大小的配置。parse_early_options()函数最终会调用do_early_param()函数。为了能在do_early_param()函数中统一处理所有需 early 配置的选项，Linux kernel 设计了类似注册回调的机制。我们可以看到，do_early_param()函数使用一个循环判断 cmdline 中是否存在 early option，若存在则调用其注册的回调函数p->setup_func(val)。void __init parse_early_options(char *cmdline) { parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, do_early_param); } static int __init do_early_param(char *param, char *val, const char *unused, void *arg) { for (p = __setup_start; p < __setup_e…

Linux 内核 | 内存管理——Slab 分配器
type: item
date: 2021-11-06T11:39:00+08:00
description: 简介在Linux中，伙伴分配器（buddy allocator）是以页为单位管理和分配内存。但在内核中的需求却以字节为单位（在内核中面临频繁的结构体内存分配问题）。假如我们需要动态申请一个内核结构体（占 20 字节），若仍然分配一页内存，这将严重浪费内存。那么该如何分配呢？slab 分配器专为小内存分配而生，由Sun公司的一个雇员Jeff Bonwick在Solaris 2.4中设计并实现。slab分配器分配内存以字节为单位，基于伙伴分配器的大内存进一步细分成小内存分配。换句话说，slab 分配器仍然从 Buddy 分配器中申请内存，之后自己对申请来的内存细分管理。除了提供小内存外，slab 分配器的第二个任务是维护常用对象的缓存。对于内核中使用的许多结构，初始化对象所需的时间可等于或超过为其分配空间的成本。当创建一个新的slab 时，许多对象将被打包到其中并使用构造函数（如果有）进行初始化。释放对象后，它会保持其初始化状态，这样可以快速分配对象。举例来说, 为管理与进程关联的文件系统数据, 内核必须经常生成struct fs_struct的新实例. 此类型实例占据的内存块同样需要经常回收(在进程结束时). 换句话说, 内核趋向于非常有规律地分配并释放大小为sizeof(fs_struct)的内存块. slab分配器将释放的内存块保存在一个内部列表中. 并不马上返回给伙伴系统. 在请求为该类对象分配一个新实例时, 会使用最近释放的内存块。S这有两个优点. 首先, 由于内核不必使用伙伴系统算法, 处理时间会变短. 其次, 由于该内存块仍然是”新”的，因此其仍然驻留在CPU硬件缓存的概率较高.[3]SLAB分配器的最后一项任务是提高CPU硬件缓存的利用率。 如果将对象包装到SLAB中后仍有剩余空间，则将剩余空间用于为SLAB着色。 SLAB着色是一种尝试使不同SLAB中的对象使用CPU硬件缓存中不同行的方案。 通过将对象放置在SLAB中的不同起始偏移处，对象可能会在CPU缓存中使用不同的行，从而有助于确保来自同一SLAB缓存的对象不太可能相互刷新。 通过这种方案，原本被浪费掉的空间可以实现一项新功能。通过命令sudo cat /proc/slabinfo可查看系统当前 slab 使用情况。以vm_area_struct结构体为例，当前系统已分配了 13014 个vm_area_struct缓存，每个大小为 216 字节，其中 active 的有 12392 个。[root@VM-8-9-centos]# cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> ... vm_area_struct 12392 13014 216 18 1 : tunables 0 0 0 : slabdata 723 723 0 mm_struct 184 200 1600 20 8 : tunables 0 0 0 : slabdata 10 10 0 shared_policy_node 5015 5015 48 85 1 : tunables 0 0 0 : slabdata 59 59 0 numa_policy 15 15 264 15 1 : tunables 0 0 0 : slabdata 1 1 0 radix_tree_node 15392 17234 584 14 2 : tunables 0 0 0 : slabdata 1231 1231 0 idr_layer_cache 240 240 2112 15 8 : tunables 0 0 0 : slabdata 16 16 0 kmalloc-8192 39 44 8192 4 8 : tunables 0 0 0 : slabdata 11 11 0 kmalloc-4096 117 144 4096 8 8 : tunables 0 0 0 : slabdata 18 18 0 kmalloc-2048 458 528 2048 16 8 : tunables 0 0 0 : slabdata 33 33 0 kmalloc-1024 1399 1424 1024 16 4 : tunables 0 0 0 : slabdata 89 89 0 kmalloc-512 763 800 512 16 2 :…

Linux 内核| 内核通知链机制
type: item
date: 2021-11-05T20:36:00+08:00
description: 简介Linux内核中存在不同的子系统，例如内存子系统、I/O子系统。每个子系统需要可能需要获取其他子系统的某些事件以进行相应的处理。通知链（notification chains）机制允许子系统订阅某个事件，当该事件发生时就通知该子系统。这类似于MQTT[2]订阅/发布的方式。通知机制Linux源码实现主要在include/linux/notifier.h和kernel/notifier.c文件中。相关结构体当一个事件发生时，该事件对应的一连串回调函数将被调用，从而实现事件的通知。在Linux中，回调函数的定义如下：typedef int (*notifier_fn_t)(struct notifier_block *nb, unsigned long action, void *data);其接收 3 个参数：nb：回调函数组成的函数指针链表，下面会详细解析该结构体action: 事件的类型。通知链可能支持多个事件，因此我们需要此参数来将一个事件与其他事件区分开data：保存事件的私有信息，用来提供有关事件的其他数据信息。返回值标识通知执行的结果，分为#define NOTIFY_DONE 0x0000 /* 订阅者对该事件不感兴趣 */ #define NOTIFY_OK 0x0001 /* 通知正常完成 */ #define NOTIFY_STOP_MASK 0x8000 /* 通知已完成，但不应为此事件调用进一步的回调。 */ #define NOTIFY_BAD (NOTIFY_STOP_MASK|0x0002) /*通知失败*/希望在某个事件上得到通知的Linux内核子系统都应提供自己的notifier_fn_t回调函数。 通知链机制的主要作用是在发生异步事件时调用某些回调。struct notifier_block { notifier_fn_t notifier_call; // 回调函数 struct notifier_block __rcu *next; // 下一个回调块 int priority; // 优先级 };Linux通知链机制提供了 4 种通知类型，包括Blocking notifier chains、SRCU notifier chainsAtomic notifier chains和Raw notifier chains。每种类型的通知链都类似，主要区别在于通知链执行过程的保护机制不同，暂不展开。以阻塞通知链为例，阻塞通知链会在进程上下文中调用/执行回调函数， 这意味着通知链中的调用过程可能会被阻塞。struct blocking_notifier_head { struct rw_semaphore rwsem; struct notifier_block __rcu *head; };实现前面提到，通知机制类似一种订阅/发布的关系。订阅者向信息代理者注册自己的信息（例如，我们在报亭写下自己的地址），当信息代理者接收到信息发布者的信息后（如人民日报生成报纸并寄送给报亭），将就信息传递给该信息的所有订阅者（报亭将报纸分发给我们）。“代理者”通过以上例子，要实现事件的订阅与发布，首先需要建立一个代理者用来记录订阅者的“地址信息”。如下在文件mm/oom_kill.c中建立一个 oom_notify_list的链表，为需要关注内存不足的子系统提供一个记录点。static BLOCKING_NOTIFIER_HEAD(oom_notify_list);BLOCKING_NOTIFIER_HEAD宏定义在文件include/linux/notifier.h中，展开为定义一个blocking_notifier_head结构体，并进行简单初始化。#define BLOCKING_NOTIFIER_HEAD(name) \ struct blocking_notifier_head name = \ BLOCKING_NOTIFIER_INIT(name) 为方便订阅者注册自身对OOM时间发生时的处理，代理者还提供相应API。register_xxx和unregister_xxx分别将notifier_block *nb 插入到 list 中和从 list 中移除，从而建立/解除订阅者关系。当然订阅者也可以直接调用include/linux/notifier.h文件中提供的blocking_notifier_chain_register()函数也可。int register_oom_notifier(struct notifier_block *nb) { return blocking_notifier_chain_register(&oom_notify_list, nb); } EXPORT_SY…

Linux 内核 | 内核镜像分析方法
type: item
date: 2021-11-05T20:34:00+08:00
description: 简介Linux内核编译完成后生成的通常是被压缩过的内核文件，例如在 Centos 和Ubuntu的 /boot目录下就保存了 vmlinuz 文件。vmlinuz是可引导的、压缩的内核，“vm”代表“Virtual Memory”。# ls -l /boot -rwxr-xr-x 1 root root 6765160 Aug 26 2020 vmlinuz-3.10.0-1127.19.1.el7.x86_64 -rwxr-xr-x 1 root root 6769256 Feb 3 23:10 vmlinuz-3.10.0-1160.15.2.el7.x86_64vmlinuz由ELF文件vmlinux 经过OBJCOPY，并压缩后的文件。Linux下大量工具都是对 ELF 文件进行解析，因此，若我们想要逆向对 Linux 内核镜像进行二进制的分析，就需要先将 vmlinuz 文件还原成 vmlinux 文件。Linux内核镜像分析工具准备Linux内核提供了脚本来实现vmlinuz 到 vmlinux 的转换——下载地址 extract-vmlinux。但在 Centos 和 Ubuntu 下可以通过包管理器直接安装，如下。# in centos yum install kernel-devel # in ubuntu apt-get install linux-headers-$(uname -r)安装完成后，extract-linux脚本将保存在Centos: /usr/src/kernels/$(uname -r)/scripts/extract-vmlinuxUbuntu: /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux文件转换/usr/src/kernels/$(uname -r)/scripts/extract-vmlinux vmlinuz-$(uname -r) > vmlinuxELF 文件解析Linux 下 常用的 ELF 文件解析工具是 readelf和objdump，以下记录一些常用的例子：(1) readelf 查看 ELF 文件的信息：- -S --section-headers 查看段头 - -h --file-header 查看文件头 - -r --relocs 查看重定位相关信息 - -x --hex-dump 查看16进制 (2) objdump读取 ELF文件中的内容- -d 查看反编译结果 - -j 指定查看的段 - -r 查看重定位相关信息 - 例子： `objdump -d -j .text hello.o` 符号名通过 objdump 命令则可反汇编 vmlinux 文件。 objdump -D vmlinux | less但该文件中没有保存符号名（例如函数名），这样分析不太方便。符号名相关的内容保存在/boot/System.map文件中。例如查找函数tcp_v4_do_rcv的地址，可执行以下命令获取到函数tcp_v4_do_rcv的地址为ffffffff816c62e0，再结合反汇编结果进行分析。grep "tcp_v4_do_rcv" /boot/System.map-3.10.0-1160.15.2.el7.x86_64 ffffffff816c62e0 T tcp_v4_do_rcv参考链接# Extracting the Linux kernel image (vmlinuz)

Linux 内核 | Per CPU 变量
type: item
date: 2021-11-05T20:33:00+08:00
description: # 简介Per CPU 变量，顾名思义指每个CPU核都有对某一个变量的一份拷贝。Per CPU 变量的应用之一是保存每个 CPU 的 ID，这是因为内核有时需要判断自己当前是运行在哪个CPU核上，因此需要获取当前执行代码的 CPU ID。实现为每个CPU定义一个变量的拷贝的宏定义在文件include/linux/percpu-defs.h中，如下：#define DEFINE_PER_CPU(type, name) \ DEFINE_PER_CPU_SECTION(type, name, "")若我们使用DEFINE_PER_CPU(int, per_cpu_n)为每个CPU定义变量，其展开宏如下。__attribute__((section(".data..percpu"))) int per_cpu_n #define DEFINE_PER_CPU_SECTION(type, name, sec) \ __PCPU_ATTRS(sec) __typeof__(type) name #define __PCPU_ATTRS(sec) \ __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) #define PER_CPU_BASE_SECTION ".data..percpu"在链接过程中，所有通过DEFINE_PER_CPU宏定义的变量都将链接到一起。在操作系统启动时，Linux 将为该段分配一段内存。 查看编译出的内核镜像可找到.data..percpu段（Linux镜像分析方法请先参考此链接）。# readelf -S vmlinux [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [21] .data..percpu PROGBITS 0000000000000000 01000000 000000000001d000 0000000000000000 WA 0 0 4096per CPU 变量的访问通过宏get_cpu_var完成 。Linux内核是可抢占的，并且在访问访问 per cpu变量时我们需要知道当前代码运行在哪个CPU核上。 因此，在访问每个cpu变量时，应当不允许抢占当前代码并将其移至另一个CPU。例如，若在获取到 CPU id 为 1 后，该任务被抢占而移动到了 CPU 2上继续运行，这时访问的将仍然是 CPU 1的per cpu 变量。因此，在 get_cpu_var 宏中，首先要调用preempt_disable()函数禁止任务抢占。// in include/linux/percpu-defs.h #define get_cpu_var(var) \ (*({ \ preempt_disable(); \ this_cpu_ptr(&var); \ }))我比较好奇的是 this_cpu_ptr是如何实现的。内核如何将该变量对应到属于该CPU的 per CPU 变量内存呢？ 在初始化时，内核会使用一个数组__per_cpu_offset[cpu]记录每个CPU静态per cpu 变量的偏移地址。在ARM64架构下， OS 启动时将 per cpu 偏移地址写入到 TPDIR_EL1 和 TPDIR_EL2 寄存器中。void __init setup_per_cpu_areas(void) { unsigned long delta; unsigned int cpu; ... delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start; for_each_possible_cpu(cpu) __per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu]; } /* arch/arm64/include/asm/percpu.h */ static inline void set_my_cpu_offset(unsigned long off) { asm volatile(ALTERNATIVE("msr tpidr_el1, %0", "msr tpidr_el2, %0", ARM64_HAS_VIRT_HOST_EXTN) :: "r" (off) : "memory"); }this_cpu_ptr的宏展开如下：即相当于 percpu 变量指针 ptr 加上__my_cpu_offset。#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_of…