diff --git a/study/kernel/02-memory/03-initialize/README.md b/study/kernel/02-memory/03-initialize/README.md index 71eea9d..d94634a 100644 --- a/study/kernel/02-memory/03-initialize/README.md +++ b/study/kernel/02-memory/03-initialize/README.md @@ -1,21 +1,388 @@ -初始化内存管理 -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器. - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - - -#1 建立数据结构 -------- - -对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化. - +初始化内存管理 +======= + + + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + + + + + + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器. + + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + + + +对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化. + + +#1 物理内存初始化 +------- + +从硬件角度来看内存, 随机存储器 (`Random Access Memory`, `RAM`) 是与 `CPU` 直连交换数据的内部存储器. 现在大部分计算机都使用 `DDR`(`Dual Data Rate SDRAM`) 的存储设备, `DDR` 包括 `DDR3L`, `DDR4L`, `LPDDR3/4` 等. `DDR` 的初始化一般是在 `BIOS` 或者 `bootloader` 中, `BIOS` 或 `bootloader` 把 `DDR` 的大小传递给 `Linux` 内核. 因此从 `Linux` 内核的角度来看 `DDR` 其实就是一段物理内存空间. + + +##1.1 内存管理概述 +------- + +内存管理是一个很复杂的系统, 涉及的内容很多. 如果用分层来描述, 内存管理可以分成 `3` 个层次, 分别时用户空间层, 内核空间层和硬件层. + + +| 层次 | 描述 | +|:---:|:----:| +| **用户空间层** |可以理解为 `Linux` 内核内存管理为用户空间暴露的系统调用接口. 例如 `brk( )`, `mmap( )` 等系统调用. 通常 `libc` 库会将系统调用封装成大家常见的 `C` 库函数, 比如 `malloc( )`, `mmap( )` 等. | +| **内核空间层** | 包含的模块相当丰富, 用户空间和内核空间的接口时系统调用, 因此内核空间层首先需要处理这些内存管理相关的系统调用, 例如 `sys_brk`, `sys_mmap`, `sys_madvise` 等. 接下来就包括 `VMA` 管理, 缺页中断管理, 匿名页面, `page cache`, 页面回收, 反向映射, `slab` 分配器, 页面管理等模块. | +| **硬件层** | 包含处理器的 `MMU`, `TLB` 和 `cache` 部件, 以及板载的物理内存, 例如 `LPDDR` 或者 `DDR` | + + +##1.2 内存大小 +------- + + +###1.2.1 `DTS` 上报 +------- + + +在 `ARM Linux` 中, 各种设备的相关属性描述都采用 `DTS` 方式来呈现. `DTS` 是 `device tree source` 的简称. 最早是由 `PowerPC` 等其他体系结构使用的 `FDT(Flattened Device Tree)` 转变过来的. `ARM Linux` 社区自 `2011` 年被 `Linus Torvalds` 公开批评之后, 开始全面支持 `DTS`, 并且删除了大量的冗余代码. + + +在 `ARM Vexpress` 平台中, 内存的定义在 `vexpress-v2p-ca9.dts` 文件中, 该 `DTS` 文件定义了内存的起始地址为 `0x60000000`, 大小为 `0x40000000`, 即 `1GB` 大小内存空间. + + +```cpp +// http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/boot/dts/vexpress-v2p-ca9.dts#L65 +memory@60000000 { + device_type = "memory"; + reg = <0x60000000 0x40000000>; +}; +``` + +同样 `ARM64` 平台类似, 起始地址为 `0x80000000`, 大小 `0x80000000(2 GB)`. + +```cpp +// http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm64/boot/dts/arm/vexpress-v2f-1xv7-ca53x2.dts#L61 +memory@80000000 { + device_type = "memory"; + reg = <0 0x80000000 0 0x80000000>; /* 2GB @ 2GB */ +}; +``` + +内存在启动的过程中, 需要解析这些 `DTS` 文件, 实际代码在 [`early_init_dt_scan_memory( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L1058) 函数中, 代码调用关系如下 : + + +[`start_kernel( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/init/main.c#L536) + -=> [`setup_arch( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm64/kernel/setup.c#L260) + -=> [`setup_machine_fdt( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm64/kernel/setup.c#L185) + -=> [`early_init_dt_scan( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L1288) + -=> [`early_init_dt_scan_nodes( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L1268) + -=> [`of_scan_flat_dt( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L728) + + +```cpp +// http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L1268 +void __init early_init_dt_scan_nodes(void) +{ + /* Retrieve various information from the /chosen node */ + of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); + + /* Initialize {size,address}-cells info */ + of_scan_flat_dt(early_init_dt_scan_root, NULL); + + /* Setup memory, calling early_init_dt_add_memory_arch */ + of_scan_flat_dt(early_init_dt_scan_memory, NULL); +} +``` + +最终 `early_init_dt_scan_nodes( )` 调用了 `early_init_dt_scan_memory` 函数读取 `DTS` 的信息并初始化内存信息. + +```cpp +// http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L1055 +/** + * early_init_dt_scan_memory - Look for and parse memory nodes + */ +int __init early_init_dt_scan_memory(unsigned long node, const char *uname, + int depth, void *data) +{ + const char *type = of_get_flat_dt_prop(node, "device_type", NULL); + const __be32 *reg, *endp; + int l; + bool hotpluggable; + + /* We are scanning "memory" nodes only */ + if (type == NULL) { + /* + * The longtrail doesn't have a device_type on the + * /memory node, so look for the node called /memory@0. + */ + if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0) + return 0; + } else if (strcmp(type, "memory") != 0) + return 0; + + reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l); + if (reg == NULL) + reg = of_get_flat_dt_prop(node, "reg", &l); + if (reg == NULL) + return 0; + + endp = reg + (l / sizeof(__be32)); + hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL); + + pr_debug("memory scan node %s, reg size %d,\n", uname, l); + + while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) { + u64 base, size; + + base = dt_mem_next_cell(dt_root_addr_cells, ®); + size = dt_mem_next_cell(dt_root_size_cells, ®); + + if (size == 0) + continue; + pr_debug(" - %llx , %llx\n", (unsigned long long)base, + (unsigned long long)size); + + early_init_dt_add_memory_arch(base, size); + + if (!hotpluggable) + continue; + + if (early_init_dt_mark_hotplug_memory_arch(base, size)) + pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n", + base, base + size); + } + + return 0; +} +``` + +`early_init_dt_scan_memory` 函数解析 `memory` 描述的信息从而得到内存的 `base_address` 和 `size` 信息, 最后内存块信息通过 [`early_init_dt_add_memory_arch( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/drivers/of/fdt.c#L1163)->[`memblock_add( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/mm/memblock.c#L596) + + +###1.2.2 `ACPI` 上报 +------- + +待补充 + + +##1.3 物理内存映射 +------- + + +在内核使用使用内存之前, 需要初始化内核的页表, 初始化页表主要在 [`map_lowmem( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1429) 函数中. + +在映射页表之前, 需要把页表的页表项清 `0`. 这部分工作主要在 [`prepare_page_table( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1246) 函数中实现. + + +```cpp +start_kernel( ) + -=> setup_arch( ) + -=> paging_init( ) + -=> prepare_page_table( ) +static inline void prepare_page_table(void) +{ + unsigned long addr; + phys_addr_t end; + + /* + * Clear out all the mappings below the kernel image. + */ + for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE) + pmd_clear(pmd_off_k(addr)); + +#ifdef CONFIG_XIP_KERNEL + /* The XIP kernel is mapped in the module area -- skip over it */ + addr = ((unsigned long)_exiprom + PMD_SIZE - 1) & PMD_MASK; +#endif + for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE) + pmd_clear(pmd_off_k(addr)); + + /* + * Find the end of the first block of lowmem. + */ + end = memblock.memory.regions[0].base + memblock.memory.regions[0].size; + if (end >= arm_lowmem_limit) + end = arm_lowmem_limit; + + /* + * Clear out all the kernel space mappings, except for the first + * memory bank, up to the vmalloc region. + */ + for (addr = __phys_to_virt(end); + addr < VMALLOC_START; addr += PMD_SIZE) + pmd_clear(pmd_off_k(addr)); +} +``` + +这里对如下 `3` 个地址调用 `pmd_clear( )` 函数来清除一级页表项的内容. + +* 0 ~ `MODULES_VADDR` + +* `MODULES_VADDR` ~ `PMD_SIZE` + +* `arm_lowmem_limit` ~ `VMALLOC_START` + +真正创建页表的函数时在 [`map_lowmem( )`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1429) 函数中. 定义在 [`arch/arm/mm/mmu.c, line 1429`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1429). 该函数会在内存开始的地方覆盖到 [`arm_lowmem_limit`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1160) 处. + + +```cpp +// http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1429 +static void __init map_lowmem(void) +{ + struct memblock_region *reg; + phys_addr_t kernel_x_start = round_down(__pa(KERNEL_START), SECTION_SIZE); + phys_addr_t kernel_x_end = round_up(__pa(__init_end), SECTION_SIZE); + + /* Map all the lowmem memory banks. */ + for_each_memblock(memory, reg) { + phys_addr_t start = reg->base; + phys_addr_t end = start + reg->size; + struct map_desc map; + + if (memblock_is_nomap(reg)) + continue; + + if (end > arm_lowmem_limit) + end = arm_lowmem_limit; + if (start >= end) + break; + + if (end < kernel_x_start) { + map.pfn = __phys_to_pfn(start); + map.virtual = __phys_to_virt(start); + map.length = end - start; + map.type = MT_MEMORY_RWX; + + create_mapping(&map); + } else if (start >= kernel_x_end) { + map.pfn = __phys_to_pfn(start); + map.virtual = __phys_to_virt(start); + map.length = end - start; + map.type = MT_MEMORY_RW; + + create_mapping(&map); + } else { + /* This better cover the entire kernel */ + if (start < kernel_x_start) { + map.pfn = __phys_to_pfn(start); + map.virtual = __phys_to_virt(start); + map.length = kernel_x_start - start; + map.type = MT_MEMORY_RW; + + create_mapping(&map); + } + + map.pfn = __phys_to_pfn(kernel_x_start); + map.virtual = __phys_to_virt(kernel_x_start); + map.length = kernel_x_end - kernel_x_start; + map.type = MT_MEMORY_RWX; + + create_mapping(&map); + + if (kernel_x_end < end) { + map.pfn = __phys_to_pfn(kernel_x_end); + map.virtual = __phys_to_virt(kernel_x_end); + map.length = end - kernel_x_end; + map.type = MT_MEMORY_RW; + + create_mapping(&map); + } + } + } +} +``` + +这里需要考虑 `kernel` 代码段的问题, `kernel` 的代码段从 `_stext` 开始, 到 `_init_end` 结束. 以 `ARM Vexpress` 平台为例. + +* 内存起始地址 `0x60000000` + +* `_stext` : `0x60000000` + +* `_init_end` : `0x60800000` + +* `arm_lowmem_limit` : 0x8f800000 + +其中, `arm_lowmem_limit` 地址考虑高端内存的情况, 该值的计算是在 `sanity_check_meminfo` 函数中. 在 `ARM Vexpress` 平台中 `arm_lowmem_limit` 等于 [`vmalloc_min`](http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1131) 其定义如下所示 : + +```cpp +// http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/mm/mmu.c#L1131 +static void * __initdata vmalloc_min = + (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET); +``` + + +`map_lowmem( )` 会对两个内存区域创建映射关系. + +* 区域 `1` + 物理地址 : `0x60000000` ~ `0x608000000` + 虚拟地址 : `0xc0000000` ~ `0xc08000000` + 属性 : 可读, 可写并且可执行(`MT_MEMORY_RWX`). + +* 区域 `2` + 物理地址 : `0x60800000` ~ `0x8f8000000` + 虚拟地址 : `0xc0800000` ~ `0xcf8000000` + 属性 : 可读, 可写(`MT_MEMORY_RW`) + + +`MT_MEMORY_RWX` 和 `MT_MEMORY_RW` 的区别在于 `ARM` 页表项有一个 `XN` 比特位. `XN` 比特位置 `1` 表示这段内存区域不允许执行. + +内存映射的函数为 `create_mapping( )`,这里创建的映射就是物理内存直接映射, 即线性映射. + + +##1.4 `zone` 初始化 +------- + +对页表的初始化完成之后, 内核旧可以对内存进行管理了, 但是内核并不是统一对待这些页面. 而是对内存进行了层次化管理. 参见 [`深入理解 `Linux` 内存管理-之-目录导航--`1` 内存描述`](http://blog.csdn.net/gatieme/article/details/52384965), 或者 [`Linux` 内存描述之概述--`Linux` 内存管理(一)--第 `2.2` 节](http://blog.csdn.net/gatieme/article/details/52384058) + + +>Linux把物理内存划分为三个层次来管理 +> +>| 层次 | 描述 | +|:---:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | +> +>为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 +> +>首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为pg_data_t的实例. 系统中每个节点被链接到一个以NULL结尾的pgdat_list链表中<而其中的每个节点利用pg_data_tnode_next字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. +> +>接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. +> +>最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. +> +>在一个单独的节点内,任一给定CPU访问页面所需的时间都是相同的。然而,对不同的CPU,这个时间可能就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少这就要小心地选择CPU最常引用的内核数据结构的存放位置. + + +其中 `zone` 结构的定义用 `struct zone` 描述在之前 [`Linux` 内存描述之内存区域 `zone–Linux` 内存管理(三)](http://blog.csdn.net/gatieme/article/details/52384529) 已经详细解释过. 由于这个数据结构经常被访问到, 银子这个数据结构需求以 `L1 Cache` 对齐. + +通常情况下, 内核的 `zone` 分为 `ZONE_DMA`, `ZONE_DMA32`, `ZONE_NORMAL` 和 `ZONE_HIGHMEM`. 在 `ARM Vexpress` 平台中, 没有定义 `CONFIG_ZONE_DMA` 和 `CONFIG_ZONE_DMA32`. 所以只有 `ZONE_NORMAL` 和 `ZONE_HIGHMEM` 两种. zone 类型的定义在 [`include/linux/mmzone.h`](http://elixir.free-electrons.com/linux/v4.13.11/source/include/linux/mmzone.h#L293) 文件中. + +`zone` 的初始化函数集中在 `bootmem_init( )` 中完成, 所以需要确定每个 `zone` 的范围. 在 `find_limits( )` 函数中会计算出 `min_low_pfn`, `max_low_pfn` 和 `max_pfn` 这 `3` 个值. 其中, `min_low_pfn` 是内存块开始地址的页帧号 (`0x60000`), `max_low_pfn(0x8f800)` 表示 `normal` 区域的结束页帧号, 它由 `arm_lowmem_limit` 这个变量得来, `max_pfn`(`0xa0000`) 是内存块结束地址的页帧号. + + +```cpp +Memory: 1026280K/1048576K available (6144K kernel code, 402K rwdata, 1400K rodata, 4096K init, 170K bss, 22296K reserved, 0K cma-reserved) +Virtual kernel memory layout: + vector : 0xffff0000 - 0xffff1000 ( 4 kB) + fixmap : 0xffc00000 - 0xfff00000 (3072 kB) + vmalloc : 0xc0800000 - 0xff800000 (1008 MB) + lowmem : 0x80000000 - 0xc0000000 (1024 MB) + modules : 0x7f000000 - 0x80000000 ( 16 MB) + .text : 0x80008000 - 0x80700000 (7136 kB) + .init : 0x80900000 - 0x80d00000 (4096 kB) + .data : 0x80d00000 - 0x80d64b58 ( 403 kB) + .bss : 0x80d6bc1c - 0x80d964f4 ( 171 kB) +SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=4, Nodes=1 +``` diff --git a/study/problem/build/local_version/README.md b/study/problem/build/local_version/README.md new file mode 100644 index 0000000..b1c85f3 --- /dev/null +++ b/study/problem/build/local_version/README.md @@ -0,0 +1,596 @@ +Linux 内核编译 LOCALVERSION 配置(分析内核版本号自动添加的"+"号) +======= + +| CSDN | GitHub | +|:----:|:------:| +| [Linux 内核编译 LOCALVERSION 配置(分析内核版本号自动添加的"+"号)](http://blog.csdn.net/gatieme/78510497) | [`AderXCoding/system/tools`](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/problem/build/local_version) | + +
+ +知识共享许可协议 + +本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作 + +因本人技术水平和知识面有限, 内容如有纰漏或者需要修正的地方, 欢迎大家指正, 也欢迎大家提供一些其他好的调试工具以供收录, 鄙人在此谢谢啦 + +
+ +#1 问题发现 +------- + + +编译主线 `kernel` 版本的时候发现, 的内核版本编译成功后生成的版本号变成了 `"x.y.z+"`, 为什么后面会多一个加号呢? + +刚开始考虑是不是 `CONFIG_LOCALVERSION` 的问题, 配置了 `CONFIG_LOCALVERSION`, 还是会在内核版本的最后加上一个 `"+"` 后, 安装完成之后, 每次 `uname -a` 都会出现 `+` 后真的感觉很郁闷, 强迫症的我真的受不了. + + + + +#2 原因分析 +------- + +问题必然出现在 `linux` 构建过程中的版本控制这一块, 既然是在构建的过程中添加的, 那我们就可以从 `Makefile` 中发现一些端倪. + + +##2.1 `Makefile` 中 `LOCALVERSION` 信息 +------- + +```cpp +VERSION = 2 +PATCHLEVEL = 6 +SUBLEVEL = 35 +EXTRAVERSION = .7 +NAME = Yokohama +``` + +这些是我们内核版本的版本号, 生成出来的版本号理论上不应带 `+` 号, 但为什么带 `+` 号呢. + + +内核中有两个配置宏 `CONFIG_LOCALVERSION` 和 `CONFIG_LOCALVERSION_AUTO` 配置了系统内核版本号和后缀的信息. + + + + +#2.2 `Makefile` 中读取和设置版本号 +------- + + +我们检索与这两个宏相关的信息, 检查 `LOCALVERSION` 宏排除 `arch/*/configs` 和 `Documentation` 等目录. + +```cpp +grep -r LOCALVERSION * grep --exclude-dir={arch,Documentation,Kconfig} +``` + +![检查 `LOCALVERSION` 相关的信息](grep_localversion.png) + +可以看到 `scripts/setlocalversion` 脚本中读取了相关的信息. + +不着急, 我们慢慢分析, 看看 `Makefile` 是怎么读取和设置这些信息. 继续从 `Makefile` 中分析 `LOVALVERSION` 的信息. + +```cpp +define filechk_kernel.release + echo "$(KERNELVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))" +endef + +# Store (new) KERNELRELEASE string in include/config/kernel.release +include/config/kernel.release: include/config/auto.conf FORCE + $(call filechk,kernel.release) +``` + + +`Makefile` 使用 `scripts/setlocalversion` 工具来生成 `include/config/kernel.release`. "+" 号就是在调用这个脚本时添加的. + +那么可以**通过执行如下命令生成版本文件** + +```cpp +make include/config/kernel.release +OR +make include/generated/utsrelease.h +``` + +查看这两个文件的信息就可以看到版本号信息 + +![`make include/config/kernel.release`](make_releasefile.png) + +另外 `Makefile` 还有如下定义 : +```cpp +kernelrelease: + @echo "$(KERNELVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))" +``` + + + +也可以直接**使用如下命令来显示版本号信息** + +```cpp +make kernelrelease +``` + +![make release]( make_release.png) + + +##2.3 `setlocalversion` 函数设置版本号信息 +------- + +阅读 `scripts/setlocalversion` 文件, 并查阅资料, 做如下笔记 : + + +```cpp +# 如果当前内核使用SVN托管, 则只从.scmversion中读取版本号信息 +if $scm_only; then + if test ! -e .scmversion; then + res=$(scm_version) + echo "$res" >.scmversion + fi + exit +fi + +# 确认 auto.conf 文件是否存在 +if test -e include/config/auto.conf; then + . include/config/auto.conf +else + echo "Error: kernelrelease not valid - run 'make prepare' to update it" >&2 + exit 1 +fi + +# 调用 localversion 从源码根目录下的localversion文件中读取信息 +# localversion* files in the build and source directory +res="$(collect_files localversion*)" +if test ! "$srctree" -ef .; then + res="$res$(collect_files "$srctree"/localversion*)" +fi + +# 设置 LOCALVERSION 信息 +# CONFIG_LOCALVERSION and LOCALVERSION (if set) +res="${res}${CONFIG_LOCALVERSION}${LOCALVERSION}" + +# 调用 scm_version 函数读取后缀信息 +# scm version string if not at a tagged commit +if test "$CONFIG_LOCALVERSION_AUTO" = "y"; then + # full scm version string + res="$res$(scm_version)" +else + # append a plus sign if the repository is not in a clean + # annotated or signed tagged state (as git describe only + # looks at signed or annotated tags - git tag -a/-s) and + # LOCALVERSION= is not specified + if test "${LOCALVERSION+set}" != "set"; then + scm=$(scm_version --short) + res="$res${scm:++}" + fi +fi +``` + + + +###2.3.1 LOCALVERSION 的设置 +------- + + +在 `scripts/setlocalversion` 文件中还有有这么一段 : + +```cpp +# CONFIG_LOCALVERSION and LOCALVERSION (if set) +res="${res}${CONFIG_LOCALVERSION}${LOCALVERSION}" +``` + +可以发现如果配置了 `CONFIG_LOCALVERSION` 和 `LOCALVERSION` 则会在版本号后面意思添加此后缀. + +而 `res` 就是获取到的本地版本号信息, 比如 `4.14-rc8` + + +###2.3.2 SCM_VERSION 后缀信息的添加 +------- + +最后根据是否配置了 `CONFIG_LOCALVERSION_AUTO` 和 `CONFIG_LOCALVERSION` 宏, 添加版本后缀信息 + +如果定义了 + +```cpp +CONFIG_LOCALVERSION_AUTO=y +``` + +此时会执行执行 + +```cpp +res="$res$(scm_version)" +``` + +其中 `res` 就是我们的版本号信息, 而 `scm_version` 函数获取了版本号后缀. + +否则如果没有设置 `CONFIG_LOCALVERSION_AUTO`, 则执行如下片段. + +```cpp +# 调用 scm_version 函数读取后缀信息 +# scm version string if not at a tagged commit +if test "$CONFIG_LOCALVERSION_AUTO" = "y"; then + # full scm version string + res="$res$(scm_version)" +else + # append a plus sign if the repository is not in a clean + # annotated or signed tagged state (as git describe only + # looks at signed or annotated tags - git tag -a/-s) and + # LOCALVERSION= is not specified + if test "${LOCALVERSION+set}" != "set"; then + scm=$(scm_version --short) + res="$res${scm:++}" + fi +fi +``` + +对于 `setlocalversion` 中的一个语法解释一下 : + +| 语法 | 描述 | +|:---:|:----:| +| `${var:-value1}` | 在变量 `var` 不为空时, 保持 `var` 原有的值不变; 如果 `var` 变量未设置或者为空, 这表达式结果为 `value1`, 但是变量 `var` 的值并不改变(未设置或为空) | +| `${var:+value1}` | 在变量 `var` 不为空时, 表达式结果为 `value1`; 如果 `var` 变量未设置或者为空, 这表达式结果为空. `${var+value1}` 的效果一样 | +| `${var:=value1}` | 在变量 `var` 不为空时, 保持 `var` 原有的值不变; 如果 `var` 变量未设置或者为空, 这表达式结果为`value1`, 变量 `var` 也被赋值为 `value1` | +| `${var:?value1}` | 在变量 `var` 未设置或为空时, 脚本会退出并抛出一个错误信息(包含`value1`) | + + +那么上面的 `shell` 语句 + +1. 如果 `CONFIG_LOCALVERSION_AUTO = y` 这段程序会通过 `scm_version` 函数(不加参数)配置本地版本号. + +2. 如果 `CONFIG_LOCALVERSION_AUTO` 未被设置, 而 `LOVALVERSION` 为空, 则 `"${LOCALVERSION+set}" != "set"`, 那么调用 `scm_version --short` 会在最后添加一个 `+` 号. + + +原来如此, 加号是这样加上去的. 那么加号具体怎么添加上去的, 然后, `scm_version` 具体做了什么工作, 这些配置宏是如何影响版本号和后缀信息的, 那只有研究 `scm_version` 函数了. + + +###2.3.3 版本后缀信息获取 +------- + + +```cpp +scm_version() +{ + local short + short=false + + cd "$srctree" + + # 如果存在 .scmversion 文件则直接获取该文件的后缀信息 + if test -e .scmversion; then + cat .scmversion + return + fi + + # --short 参数的设置 + if test "$1" = "--short"; then + short=true + fi + + # Check for git and a git repo. + # 读取 git 仓库的版本信息 + # 如果 --short 被设置则直接打印 + 号 + # 否则git读取版本号信息, + # 如果git tag号存在git describe | awk -F- '{printf("-%05d-%s", $(NF-1),$(NF))}' + # 否则直接打印commit号信息 + if test -z "$(git rev-parse --show-cdup 2>/dev/null)" && + head=`git rev-parse --verify --short HEAD 2>/dev/null`; then + + # If we are at a tagged commit (like "v2.6.30-rc6"), we ignore + # it, because this version is defined in the top level Makefile. + if [ -z "`git describe --exact-match 2>/dev/null`" ]; then + + # If only the short version is requested, don't bother + # running further git commands + if $short; then + echo "+" + return + fi + # If we are past a tagged commit (like + # "v2.6.30-rc5-302-g72357d5"), we pretty print it. + if atag="`git describe 2>/dev/null`"; then + echo "$atag" | awk -F- '{printf("-%05d-%s", $(NF-1),$(NF))}' + + # If we don't have a tag at all we print -g{commitish}. + else + printf '%s%s' -g $head + fi + fi + + # Is this git on svn? + if git config --get svn-remote.svn.url >/dev/null; then + printf -- '-svn%s' "`git svn find-rev $head`" + fi + + # Check for uncommitted changes + # 如果有未提交的文件则会添加-dirty后缀 + if git diff-index --name-only HEAD | grep -qv "^scripts/package"; then + printf '%s' -dirty + fi + + # All done with git + return + fi + + # Check for mercurial and a mercurial repo. + if test -d .hg && hgid=`hg id 2>/dev/null`; then + # Do we have an tagged version? If so, latesttagdistance == 1 + if [ "`hg log -r . --template '{latesttagdistance}'`" == "1" ]; then + id=`hg log -r . --template '{latesttag}'` + printf '%s%s' -hg "$id" + else + tag=`printf '%s' "$hgid" | cut -d' ' -f2` + if [ -z "$tag" -o "$tag" = tip ]; then + id=`printf '%s' "$hgid" | sed 's/[+ ].*//'` + printf '%s%s' -hg "$id" + fi + fi + + # Are there uncommitted changes? + # These are represented by + after the changeset id. + case "$hgid" in + *+|*+\ *) printf '%s' -dirty ;; + esac + + # All done with mercurial + return + fi + + # Check for svn and a svn repo. + # 获取 svn 仓库的版本号后缀信息 + if rev=`LANG= LC_ALL= LC_MESSAGES=C svn info 2>/dev/null | grep '^Last Changed Rev'`; then + rev=`echo $rev | awk '{print $NF}'` + printf -- '-svn%s' "$rev" + + # All done with svn + return + fi +} +``` + + + +用 `bash` 判断语句来判断 + +```cpp +git rev-parse --verify --short +``` + +来判断当前是否是 `git` 版本库管理, 接着输出一个短的版本库`HEAD revision` 的短编码. + +```cpp +git rev-parse --verify --short HEAD 2>/dev/null +``` + +关键在下面这条语句的执行结果 + +```cpp +git describe --exact-match +``` + +这一句是描述出当前的 `tag` 标识. 如果没有 `tag` 就为空, 那么整个 `if` 语句就为真, 就会执行下去, 下面的 `echo "+"`, 这就会在版本号中输出一个 `+` 号. + +如果我们在版本库中 + +```cpp +git tag -a -m "v0.1" v0.1 +``` + +后, 我们在执行 `git describe --exact-match` 这一句, 发现输出的是我们的 `tag` 标识. 那 `if` 语句就不成里了, 就不会 `echo "+"` 了. + +继续看上面的代码, 如果有未提交的代码, `printf -dirty` 的地方进行了 `git diff` 的检查, 也就是说我有修改过的, 没有上传的文件. 到此基本上原因全部查明, 我把文件进行上传后, 重新 `make prepare` 后, 生成的 `kernel.release` 果然正确. + +结论, `linux` 对版本的管理相当严格,这也就让我们在进行代码管理中必须严格要求自己,比如发版本前,先检查是否还有修改为上传的文件,然后要在git版本库中打一个tag。 + + +如果代码属于 `git` 管理 + +* 打了 `tag`, 则会添加tag相关字符 + + * 如果 `tag` 只是简单的标记, 比如 `4.14-rc8` 则跳过, 因为这些信息已经从前面 `makefile` 中获取到了 + + * 如果 `tag` 还有其他后缀标记, 比如 `v2.6.30-rc5-302-g72357d5`, 则将这些打印出来 + +* 没有打 `tag`, 则会添加 `log` 字符 + 例如最新的 `commit` 是 + ```cpp + commit cdebe039ded3e7fcd00c6e5603a878b14d7e564e + ``` + 则编译之后文件 `include/config/kernel.release` 的内容为 `4.14.0-rc8-gcdebe03` + +按照从之前传递的参数过来 + +* 如果没有定义了 `CONFIG_LOCALVERSION_AUTO` 和 `LOCALVERSION`, scm_version 函数会传递过去 `--short` 参数版本号后面会添加 `"+"` 号. + + ```cpp + if $short; then + echo "+" + return + fi + ``` + + +#2.4 总结 +------- + + +##2.4.1 版本号的设置 +------- + +脚本 `script/setlocalversion` 中读取了版本号的信息 + + +```cpp +# localversion* files in the build and source directory +res="$(collect_files localversion*)" +if test ! "$srctree" -ef .; then + res="$res$(collect_files "$srctree"/localversion*)" +fi + +# CONFIG_LOCALVERSION and LOCALVERSION (if set) +res="${res}${CONFIG_LOCALVERSION}${LOCALVERSION}" +``` + +由此可看出, 如果想往版本号里添加字符, 有几种方式 : + +* 使用 `LOCALVERSION` 变量(或者在命令行, 或者添加为环境变量) + +* 在内核源代码根目录下添加文件 `localversion` 文件内容会自动添加到版本号里去. 在本地创建 文件中添加 + +* 定义 `CONFIG_LOCALVERSION` 变量 + +* 往版本号里添加字符的方式 + + +`LOCALVERSION` 变量可在命令行定义 : + +```CPP +make LOCALVERSION=.44 include/config/kernel.release +``` + +或者添加为环境变量 + +```cpp +export LOCALVERSION=.44 +make include/config/kernel.release +``` + + + + +当前内核版本为 `4.14.0-rc8`, 如果源码根目录下有文件 `localversion`(其内容为 `.33`), 也使用了 `LOCALVERSION`变量(`make` 时指定), 也定义了`CONFIG_LOCALVERSION=".XYZ"`. + +```cpp +make LOCALVERSION=.44 include/config/kernel.release +``` + +此时对 `4.14-rc8` 的内核, `include/config/kernel.release` 的内容为 `4.14-rc8.33.XYZ.55`. + + +可看到添加的三种字符的顺序 + +文件 `localversion` 内容在前, 然后是 `CONFIG_LOCALVERSION` 的值, 最后是 `LOCALVERSION` 的值 + +即 + +| 版本号 | 标识 | 内容 | +|:-----:|:----:|:---:| +| 主版本号 | VERSION | 4 | +| 发布版本 | PATCHLEVEL | 14 | +| 次版本号 | SUBLEVEL | 0 +| 扩展版本号 | EXTRAVERSION | -rc8 | +| 文件 | localversion | 33| +| 配置宏 | CONFIG_LOCALVERSION | XYZ | +| 本地宏 | LOCALVERSION | 55 | + +###2.4.2 后缀信息的获取 +------- + + +1. 如果 `CONFIG_LOCALVERSION_AUTO = y` 这段程序会通过 `scm_version` 函数(不加参数)配置本地版本号后缀信息. 后缀信息一般都是托管仓库的版本号, 比如 `git tag/commit` 等 + +2. 如果 `CONFIG_LOCALVERSION_AUTO` 未被设置, 而 `LOVALVERSION` 为空, 则 `"${LOCALVERSION+set}" != "set"`, 那么调用 `scm_version --short` 会在最后添加一个 `+` 号. + + + + +>另外, 关于 `scripts/setlocalversion` 文件. +> +>在 `scripts/setlocalversion文件中,可用echo "aaa" >&2来输出显示相关信息,例如: +>echo "LOCALVERSION=${LOCALVERSION}" >&2 + + +>需要仔细注意 +> +>使用 `modinfo` 可查看编译出来的 `ko` 文件对应的内核版本号 +使用 `uname` 或者 `cat /proc/version` 可在目标系统上查看内核版本号. +> +>可查看 `kernel` 编译过程生成的文件 `include/config/kernel.release` 或者 `include/generated/utsrelease.h`, 确定编译出来的内核的版本号. + + +###2.4.3 验证 +------- + +* `LOCALVERSION` 可以在版本号之后追加后缀信息, 如果再定义 `CONFIG_LOCALVERSION_AUTO`, 将在最后进一步追加 `git` 版本号为后缀信息 + +| 宏 | 定义 | +|:--:|:---:| +| CONFIG_LOCALVERSION | "" | +| CONFIG_LOCALVERSION_AUTO | y | +| LOCALVERSION | not set or set | + +![验证](test3.png) + + +* 不定义`CONFIG_LOCALVERSION_AUTO` 将不显示 `git` 仓库信息, 如果此时 `LOCALVERSION` 变量定义也未定义, 将追加 "+". + +| 宏 | 定义 | +|:--:|:---:| +| CONFIG_LOCALVERSION | "" | +| CONFIG_LOCALVERSION_AUTO | not set | +| LOCALVERSION | not set | + + +此时 scm_version --short 添加了一个 "+" 号 + + +```cpp +cat .config | grep -E "CONFIG_LOCALVERSION" +make kernelrelease +``` + +![验证](test1.png) + + +* 只要定义了 LOCALVERSION 即使定义为 `NULL`, 也不会追加 "+" + + +| 宏 | 定义 | +|:--:|:---:| +| CONFIG_LOCALVERSION | "" | +| CONFIG_LOCALVERSION_AUTO | not set | +| LOCALVERSION | 设置为空 | + + + +```cpp +make LOCALVERSION= kernelrelease + +make LOCALVERSION="" kernelrelease + +``` + +此时将不会添加 "+" 号 + +![验证](test2.png) + + +#3 解决 +------- + +* `LOCALVERSION` 可以在版本号之后追加后缀信息, 如果再定义 `CONFIG_LOCALVERSION_AUTO`, 将在最后进一步追加 `git` 版本号为后缀信息. + +* 不定义`CONFIG_LOCALVERSION_AUTO` 将不显示 `git` 仓库信息, 如果此时 `LOCALVERSION` 变量定义也未定义, 将追加 "+". + +* 如果既不想添加后缀, 又不想有 `"+"` 号 : 不定义`CONFIG_LOCALVERSION_AUTO`, 将 `LOCALVERSION` 变量定义为空 : `LOCALVERSION=`. + +* 只要定义了 `LOCALVERSION`, 则就不会追加 "+" 号了 + + + + +#4 参考资料 +------- + +[向 `linux` 内核版本号添加字符/为何有时会自动添加 `+` 号](http://blog.csdn.net/xiaofeng_yan/article/details/48376353) + + +[向linux内核版本号添加字符/为何有时会自动添加"+"号或者"xxx-dirty"](http://blog.csdn.net/davion_zhang/article/details/53781269) + +[向linux内核版本号添加字符/为何有时会自动添加“+”号](http://blog.csdn.net/adaptiver/article/details/7225980) + +[去掉linux内核版本号自动添加的“+”号](http://blog.csdn.net/l461269717/article/details/44080463) + +[LINUX KERNEL编译生成的版本多一个加号"+"](http://smilejay.com/2012/07/kernel-version-plus-sign/) + +
+ +* 本作品/博文 ( [AderStep-紫夜阑珊-青伶巷草 Copyright ©2013-2017](http://blog.csdn.net/gatieme) ), 由 [成坚(gatieme)](http://blog.csdn.net/gatieme) 创作, + +* 采用知识共享许可协议知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可. 欢迎转载、使用、重新发布, 但务必保留文章署名[成坚gatieme](http://blog.csdn.net/gatieme) ( 包含链接: http://blog.csdn.net/gatieme ), 不得用于商业目的. + +* 基于本文修改后的作品务必以相同的许可发布. 如有任何疑问,请与我联系. \ No newline at end of file diff --git a/study/problem/build/local_version/grep_localversion.png b/study/problem/build/local_version/grep_localversion.png new file mode 100644 index 0000000..0436103 Binary files /dev/null and b/study/problem/build/local_version/grep_localversion.png differ diff --git a/study/problem/build/local_version/make_release.png b/study/problem/build/local_version/make_release.png new file mode 100644 index 0000000..bbac341 Binary files /dev/null and b/study/problem/build/local_version/make_release.png differ diff --git a/study/problem/build/local_version/make_releasefile.png b/study/problem/build/local_version/make_releasefile.png new file mode 100644 index 0000000..1c3fbb0 Binary files /dev/null and b/study/problem/build/local_version/make_releasefile.png differ diff --git a/study/problem/build/local_version/test1.png b/study/problem/build/local_version/test1.png new file mode 100644 index 0000000..efb7f75 Binary files /dev/null and b/study/problem/build/local_version/test1.png differ diff --git a/study/problem/build/local_version/test2.png b/study/problem/build/local_version/test2.png new file mode 100644 index 0000000..5149530 Binary files /dev/null and b/study/problem/build/local_version/test2.png differ diff --git a/study/problem/build/local_version/test3.png b/study/problem/build/local_version/test3.png new file mode 100644 index 0000000..9897998 Binary files /dev/null and b/study/problem/build/local_version/test3.png differ