├── Booting ├── README.md ├── linux-bootstrap-1.md ├── linux-bootstrap-2.md ├── linux-bootstrap-3.md ├── linux-bootstrap-4.md ├── linux-bootstrap-5.md └── linux-bootstrap-6.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Cgroups ├── README.md └── linux-cgroups-1.md ├── Concepts ├── README.md ├── linux-cpu-1.md ├── linux-cpu-2.md ├── linux-cpu-3.md └── linux-cpu-4.md ├── DataStructures ├── README.md ├── dlist.md ├── linux-datastructures-1.md ├── linux-datastructures-2.md ├── linux-datastructures-3.md └── radix-tree.md ├── Initialization ├── README.md ├── linux-initialization-1.md ├── linux-initialization-10.md ├── linux-initialization-2.md ├── linux-initialization-3.md ├── linux-initialization-4.md ├── linux-initialization-5.md ├── linux-initialization-6.md ├── linux-initialization-7.md ├── linux-initialization-8.md └── linux-initialization-9.md ├── Interrupts ├── README.md ├── linux-interrupts-1.md ├── linux-interrupts-10.md ├── linux-interrupts-2.md ├── linux-interrupts-3.md ├── linux-interrupts-4.md ├── linux-interrupts-5.md ├── linux-interrupts-6.md ├── linux-interrupts-7.md ├── linux-interrupts-8.md └── linux-interrupts-9.md ├── KernelStructures ├── README.md └── linux-kernelstructure-1.md ├── LINKS.md ├── MM ├── README.md ├── linux-mm-1.md ├── linux-mm-2.md └── linux-mm-3.md ├── Misc ├── README.md ├── how_linux_compile.md ├── linux-misc-1.md ├── linux-misc-2.md ├── linux-misc-3.md └── linux-misc-4.md ├── README.md ├── SUMMARY.md ├── SyncPrim ├── README.md ├── linux-sync-1.md ├── linux-sync-2.md ├── linux-sync-3.md ├── linux-sync-4.md ├── linux-sync-5.md └── linux-sync-6.md ├── SysCall ├── README.md ├── linux-syscall-1.md ├── linux-syscall-2.md ├── linux-syscall-3.md ├── linux-syscall-4.md ├── linux-syscall-5.md ├── linux-syscall-6.md ├── syscall-1.md ├── syscall-2.md ├── syscall-3.md └── syscall-4.md ├── TRANSLATION_NOTES.md ├── Theory ├── ELF.md ├── Paging.md ├── README.md ├── linux-theory-1.md ├── linux-theory-2.md └── linux-theory-3.md ├── Timers ├── README.md ├── linux-timers-1.md ├── linux-timers-2.md ├── linux-timers-3.md ├── linux-timers-4.md ├── linux-timers-5.md ├── linux-timers-6.md └── linux-timers-7.md └── cover.jpg /Booting/README.md: -------------------------------------------------------------------------------- 1 | # 内核引导过程 2 | 3 | 本章介绍了Linux内核引导过程。此处你将在这看到一些描述内核加载过程的整个周期的文章: 4 | 5 | * [从引导程序到内核](linux-bootstrap-1.md) - 介绍了从启动计算机到内核执行第一条指令之前的所有阶段; 6 | * [在内核设置代码的第一步](linux-bootstrap-2.md) - 介绍了在内核设置代码的第一个步骤。你会看到堆的初始化,查询不同的参数,如 EDD,IST 等... 7 | * [视频模式初始化和保护模式切换](linux-bootstrap-3.md) - 介绍了内核设置代码中的视频模式初始化,并切换到保护模式。 8 | * [切换 64 位模式](linux-bootstrap-4.md) - 介绍切换到 64 位模式的准备工作以及切换的细节。 9 | * [内核解压缩](linux-bootstrap-5.md) - 介绍了内核解压缩之前的准备工作以及直接解压缩的细节。 10 | * [内核地址随机化](linux-bootstrap-6.md) - 介绍了 Linux 内核加载地址随机化的细节。 11 | -------------------------------------------------------------------------------- /Booting/linux-bootstrap-5.md: -------------------------------------------------------------------------------- 1 | 内核引导过程. Part 5. 2 | ================================================================================ 3 | 4 | 内核解压 5 | -------------------------------------------------------------------------------- 6 | 7 | 这是`内核引导过程`系列文章的第五部分。在[前一部分](linux-bootstrap-4.md#transition-to-the-long-mode)我们看到了切换到64位模式的过程,在这一部分我们会从这里继续。我们会看到跳进内核代码的最后步骤:内核解压前的准备、重定位和直接内核解压。所以...让我们再次深入内核源码。 8 | 9 | 内核解压前的准备 10 | -------------------------------------------------------------------------------- 11 | 12 | 我们停在了跳转到`64位`入口点——`startup_64`的跳转之前,它在源文件 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S) 里面。在之前的部分,我们已经在`startup_32`里面看到了到`startup_64`的跳转: 13 | 14 | ```assembly 15 | pushl $__KERNEL_CS 16 | leal startup_64(%ebp), %eax 17 | ... 18 | ... 19 | ... 20 | pushl %eax 21 | ... 22 | ... 23 | ... 24 | lret 25 | ``` 26 | 27 | 由于我们加载了新的`全局描述符表`并且在其他模式有CPU的模式转换(在我们这里是`64位`模式),我们可以在`startup_64`的开头看到数据段的建立: 28 | 29 | ```assembly 30 | .code64 31 | .org 0x200 32 | ENTRY(startup_64) 33 | xorl %eax, %eax 34 | movl %eax, %ds 35 | movl %eax, %es 36 | movl %eax, %ss 37 | movl %eax, %fs 38 | movl %eax, %gs 39 | ``` 40 | 41 | 除`cs`之外的段寄存器在我们进入`长模式`时已经重置。 42 | 43 | 下一步是计算内核编译时的位置和它被加载的位置的差: 44 | 45 | ```assembly 46 | #ifdef CONFIG_RELOCATABLE 47 | leaq startup_32(%rip), %rbp 48 | movl BP_kernel_alignment(%rsi), %eax 49 | decl %eax 50 | addq %rax, %rbp 51 | notq %rax 52 | andq %rax, %rbp 53 | cmpq $LOAD_PHYSICAL_ADDR, %rbp 54 | jge 1f 55 | #endif 56 | movq $LOAD_PHYSICAL_ADDR, %rbp 57 | 1: 58 | movl BP_init_size(%rsi), %ebx 59 | subl $_end, %ebx 60 | addq %rbp, %rbx 61 | ``` 62 | 63 | `rbp`包含了解压后内核的起始地址,在这段代码执行之后`rbx`会包含用于解压的重定位内核代码的地址。我们已经在`startup_32`看到类似的代码(你可以看之前的部分[计算重定位地址](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-4.md#calculate-relocation-address)),但是我们需要再做这个计算,因为引导加载器可以用64位引导协议,而`startup_32`在这种情况下不会执行。 64 | 65 | 下一步,我们可以看到栈指针的设置和标志寄存器的重置: 66 | 67 | ```assembly 68 | leaq boot_stack_end(%rbx), %rsp 69 | 70 | pushq $0 71 | popfq 72 | ``` 73 | 74 | 如上所述,`rbx`寄存器包含了内核解压代码的起始地址,我们把这个地址的`boot_stack_entry`偏移地址相加放到表示栈顶指针的`rsp`寄存器。在这一步之后,栈就是正确的。你可以在汇编源码文件 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S) 的末尾找到`boot_stack_end`的定义: 75 | 76 | ```assembly 77 | .bss 78 | .balign 4 79 | boot_heap: 80 | .fill BOOT_HEAP_SIZE, 1, 0 81 | boot_stack: 82 | .fill BOOT_STACK_SIZE, 1, 0 83 | boot_stack_end: 84 | ``` 85 | 86 | 它在`.bss`节的末尾,就在`.pgtable`前面。如果你查看 [arch/x86/boot/compressed/vmlinux.lds.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/vmlinux.lds.S) 链接脚本,你会找到`.bss`和`.pgtable`的定义。 87 | 88 | 由于我们设置了栈,在我们计算了解压了的内核的重定位地址后,我们可以复制压缩了的内核到以上地址。在查看细节之前,我们先看这段汇编代码: 89 | 90 | ```assembly 91 | pushq %rsi 92 | leaq (_bss-8)(%rip), %rsi 93 | leaq (_bss-8)(%rbx), %rdi 94 | movq $_bss, %rcx 95 | shrq $3, %rcx 96 | std 97 | rep movsq 98 | cld 99 | popq %rsi 100 | ``` 101 | 102 | 首先我们把`rsi`压进栈。我们需要保存`rsi`的值,因为这个寄存器现在存放指向`boot_params`的指针,这是包含引导相关数据的实模式结构体(你一定记得这个结构体,我们在开始设置内核的时候就填充了它)。在代码的结尾,我们会重新恢复指向`boot_params`的指针到`rsi`. 103 | 104 | 接下来两个`leaq`指令用`_bss - 8`偏移和`rip`和`rbx`计算有效地址并存放到`rsi`和`rdi`. 我们为什么要计算这些地址?实际上,压缩了的代码镜像存放在这份复制了的代码(从`startup_32`到当前的代码)和解压了的代码之间。你可以通过查看链接脚本 [arch/x86/boot/compressed/vmlinux.lds.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/vmlinux.lds.S) 验证: 105 | 106 | ``` 107 | . = 0; 108 | .head.text : { 109 | _head = . ; 110 | HEAD_TEXT 111 | _ehead = . ; 112 | } 113 | .rodata..compressed : { 114 | *(.rodata..compressed) 115 | } 116 | .text : { 117 | _text = .; /* Text */ 118 | *(.text) 119 | *(.text.*) 120 | _etext = . ; 121 | } 122 | ``` 123 | 124 | 注意`.head.text`节包含了`startup_32`. 你可以从之前的部分回忆起它: 125 | 126 | ```assembly 127 | __HEAD 128 | .code32 129 | ENTRY(startup_32) 130 | ... 131 | ... 132 | ... 133 | ``` 134 | 135 | `.text`节包含解压代码: 136 | 137 | ```assembly 138 | .text 139 | relocated: 140 | ... 141 | ... 142 | ... 143 | /* 144 | * Do the decompression, and jump to the new kernel.. 145 | */ 146 | ... 147 | ``` 148 | 149 | `.rodata..compressed`包含了压缩了的内核镜像。所以`rsi`包含`_bss - 8`的绝对地址,`rdi`包含`_bss - 8`的重定位的相对地址。在我们把这些地址放入寄存器时,我们把`_bss`的地址放到了`rcx`寄存器。正如你在`vmlinux.lds.S`链接脚本中看到了一样,它和设置/内核代码一起在所有节的末尾。现在我们可以开始用`movsq`指令每次8字节地从`rsi`到`rdi`复制代码。 150 | 151 | 注意在数据复制前有`std`指令:它设置`DF`标志,意味着`rsi`和`rdi`会递减。换句话说,我们会从后往前复制这些字节。最后,我们用`cld`指令清除`DF`标志,并恢复`boot_params`到`rsi`. 152 | 153 | 现在我们有`.text`节的重定位后的地址,我们可以跳到那里: 154 | 155 | ```assembly 156 | leaq relocated(%rbx), %rax 157 | jmp *%rax 158 | ``` 159 | 160 | 在内核解压前的最后准备 161 | -------------------------------------------------------------------------------- 162 | 163 | 在上一段我们看到了`.text`节从`relocated`标签开始。它做的第一件事是清空`.bss`节: 164 | 165 | ```assembly 166 | xorl %eax, %eax 167 | leaq _bss(%rip), %rdi 168 | leaq _ebss(%rip), %rcx 169 | subq %rdi, %rcx 170 | shrq $3, %rcx 171 | rep stosq 172 | ``` 173 | 174 | 我们要初始化`.bss`节,因为我们很快要跳转到[C](https://en.wikipedia.org/wiki/C_%28programming_language%29)代码。这里我们就清空`eax`,把`_bss`的地址放到`rdi`,把`_ebss`放到`rcx`,然后用`rep stosq`填零。 175 | 176 | 最后,我们可以调用`extract_kernel`函数: 177 | 178 | ```assembly 179 | pushq %rsi 180 | movq %rsi, %rdi 181 | leaq boot_heap(%rip), %rsi 182 | leaq input_data(%rip), %rdx 183 | movl $z_input_len, %ecx 184 | movq %rbp, %r8 185 | movq $z_output_len, %r9 186 | call extract_kernel 187 | popq %rsi 188 | ``` 189 | 190 | 我们再一次设置`rdi`为指向`boot_params`结构体的指针并把它保存到栈中。同时我们设置`rsi`指向用于内核解压的区域。最后一步是准备`extract_kernel`的参数并调用这个解压内核的函数。`extract_kernel`函数在 [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c) 源文件定义并有六个参数: 191 | 192 | * `rmode` - 指向 [boot_params](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973//arch/x86/include/uapi/asm/bootparam.h#L114) 结构体的指针,`boot_params`被引导加载器填充或在早期内核初始化时填充 193 | * `heap` - 指向早期启动堆的起始地址 `boot_heap` 的指针 194 | * `input_data` - 指向压缩的内核,即 `arch/x86/boot/compressed/vmlinux.bin.bz2` 的指针 195 | * `input_len` - 压缩的内核的大小 196 | * `output` - 解压后内核的起始地址 197 | * `output_len` - 解压后内核的大小 198 | 199 | 所有参数根据 [System V Application Binary Interface](http://www.x86-64.org/documentation/abi.pdf) 通过寄存器传递。我们已经完成了所有的准备工作,现在我们可以看内核解压的过程。 200 | 201 | 内核解压 202 | -------------------------------------------------------------------------------- 203 | 204 | 就像我们在之前的段落中看到了那样,`extract_kernel`函数在源文件 [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c) 定义并有六个参数。正如我们在之前的部分看到的,这个函数从图形/控制台初始化开始。我们要再次做这件事,因为我们不知道我们是不是从[实模式](https://en.wikipedia.org/wiki/Real_mode)开始,或者是使用了引导加载器,或者引导加载器用了32位还是64位启动协议。 205 | 206 | 在最早的初始化步骤后,我们保存空闲内存的起始和末尾地址。 207 | 208 | ```C 209 | free_mem_ptr = heap; 210 | free_mem_end_ptr = heap + BOOT_HEAP_SIZE; 211 | ``` 212 | 213 | 在这里 `heap` 是我们在 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S) 得到的 `extract_kernel` 函数的第二个参数: 214 | 215 | ```assembly 216 | leaq boot_heap(%rip), %rsi 217 | ``` 218 | 219 | 如上所述,`boot_heap`定义为: 220 | 221 | ```assembly 222 | boot_heap: 223 | .fill BOOT_HEAP_SIZE, 1, 0 224 | ``` 225 | 226 | 在这里`BOOT_HEAP_SIZE`是一个展开为`0x10000`(对`bzip2`内核是`0x400000`)的宏,代表堆的大小。 227 | 228 | 在堆指针初始化后,下一步是从 [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/kaslr.c#L425) 调用`choose_random_location`函数。我们可以从函数名猜到,它选择内核镜像解压到的内存地址。看起来很奇怪,我们要寻找甚至是`选择`内核解压的地址,但是Linux内核支持[kASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization),为了安全,它允许解压内核到随机的地址。 229 | 230 | 在这一部分,我们不会考虑Linux内核的加载地址的随机化,我们会在下一部分讨论。 231 | 232 | 现在我们回头看 [misc.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/misc.c#L404). 在获得内核镜像的地址后,需要有一些检查以确保获得的随机地址是正确对齐的,并且地址没有错误: 233 | 234 | ```C 235 | if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1)) 236 | error("Destination physical address inappropriately aligned"); 237 | 238 | if (virt_addr & (MIN_KERNEL_ALIGN - 1)) 239 | error("Destination virtual address inappropriately aligned"); 240 | 241 | if (heap > 0x3fffffffffffUL) 242 | error("Destination address too large"); 243 | 244 | if (virt_addr + max(output_len, kernel_total_size) > KERNEL_IMAGE_SIZE) 245 | error("Destination virtual address is beyond the kernel mapping area"); 246 | 247 | if ((unsigned long)output != LOAD_PHYSICAL_ADDR) 248 | error("Destination address does not match LOAD_PHYSICAL_ADDR"); 249 | 250 | if (virt_addr != LOAD_PHYSICAL_ADDR) 251 | error("Destination virtual address changed when not relocatable"); 252 | ``` 253 | 254 | 在所有这些检查后,我们可以看到熟悉的消息: 255 | 256 | ``` 257 | Decompressing Linux... 258 | ``` 259 | 260 | 然后调用解压内核的`__decompress`函数: 261 | 262 | ```C 263 | __decompress(input_data, input_len, NULL, NULL, output, output_len, NULL, error); 264 | ``` 265 | 266 | `__decompress`函数的实现取决于在内核编译期间选择什么压缩算法: 267 | 268 | ```C 269 | #ifdef CONFIG_KERNEL_GZIP 270 | #include "../../../../lib/decompress_inflate.c" 271 | #endif 272 | 273 | #ifdef CONFIG_KERNEL_BZIP2 274 | #include "../../../../lib/decompress_bunzip2.c" 275 | #endif 276 | 277 | #ifdef CONFIG_KERNEL_LZMA 278 | #include "../../../../lib/decompress_unlzma.c" 279 | #endif 280 | 281 | #ifdef CONFIG_KERNEL_XZ 282 | #include "../../../../lib/decompress_unxz.c" 283 | #endif 284 | 285 | #ifdef CONFIG_KERNEL_LZO 286 | #include "../../../../lib/decompress_unlzo.c" 287 | #endif 288 | 289 | #ifdef CONFIG_KERNEL_LZ4 290 | #include "../../../../lib/decompress_unlz4.c" 291 | #endif 292 | ``` 293 | 294 | 在内核解压之后,最后两个函数是`parse_elf`和`handle_relocations`.这些函数的主要用途是把解压后的内核移动到正确的位置。事实上,解压过程会[原地](https://en.wikipedia.org/wiki/In-place_algorithm)解压,我们还是要把内核移动到正确的地址。我们已经知道,内核镜像是一个[ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format)可执行文件,所以`parse_elf`的主要目标是移动可加载的段到正确的地址。我们可以在`readelf`的输出看到可加载的段: 295 | 296 | ``` 297 | readelf -l vmlinux 298 | 299 | Elf file type is EXEC (Executable file) 300 | Entry point 0x1000000 301 | There are 5 program headers, starting at offset 64 302 | 303 | Program Headers: 304 | Type Offset VirtAddr PhysAddr 305 | FileSiz MemSiz Flags Align 306 | LOAD 0x0000000000200000 0xffffffff81000000 0x0000000001000000 307 | 0x0000000000893000 0x0000000000893000 R E 200000 308 | LOAD 0x0000000000a93000 0xffffffff81893000 0x0000000001893000 309 | 0x000000000016d000 0x000000000016d000 RW 200000 310 | LOAD 0x0000000000c00000 0x0000000000000000 0x0000000001a00000 311 | 0x00000000000152d8 0x00000000000152d8 RW 200000 312 | LOAD 0x0000000000c16000 0xffffffff81a16000 0x0000000001a16000 313 | 0x0000000000138000 0x000000000029b000 RWE 200000 314 | ``` 315 | 316 | `parse_elf`函数的目标是加载这些段到从`choose_random_location`函数得到的`output`地址。这个函数从检查ELF签名标志开始: 317 | 318 | ```C 319 | Elf64_Ehdr ehdr; 320 | Elf64_Phdr *phdrs, *phdr; 321 | 322 | memcpy(&ehdr, output, sizeof(ehdr)); 323 | 324 | if (ehdr.e_ident[EI_MAG0] != ELFMAG0 || 325 | ehdr.e_ident[EI_MAG1] != ELFMAG1 || 326 | ehdr.e_ident[EI_MAG2] != ELFMAG2 || 327 | ehdr.e_ident[EI_MAG3] != ELFMAG3) { 328 | error("Kernel is not a valid ELF file"); 329 | return; 330 | } 331 | ``` 332 | 333 | 如果是无效的,它会打印一条错误消息并停机。如果我们得到一个有效的`ELF`文件,我们从给定的`ELF`文件遍历所有程序头,并用正确的地址复制所有可加载的段到输出缓冲区: 334 | 335 | ```C 336 | for (i = 0; i < ehdr.e_phnum; i++) { 337 | phdr = &phdrs[i]; 338 | 339 | switch (phdr->p_type) { 340 | case PT_LOAD: 341 | #ifdef CONFIG_RELOCATABLE 342 | dest = output; 343 | dest += (phdr->p_paddr - LOAD_PHYSICAL_ADDR); 344 | #else 345 | dest = (void *)(phdr->p_paddr); 346 | #endif 347 | memmove(dest, output + phdr->p_offset, phdr->p_filesz); 348 | break; 349 | default: 350 | break; 351 | } 352 | } 353 | ``` 354 | 355 | 这就是全部的工作。 356 | 357 | 从现在开始,所有可加载的段都在正确的位置。 358 | 359 | 在`parse_elf`函数之后是调用`handle_relocations`函数。这个函数的实现依赖于`CONFIG_X86_NEED_RELOCS`内核配置选项,如果它被启用,这个函数调整内核镜像的地址,只有在内核配置时启用了`CONFIG_RANDOMIZE_BASE`配置选项才会调用。`handle_relocations`函数的实现足够简单。这个函数从基准内核加载地址的值减掉`LOAD_PHYSICAL_ADDR`的值,从而我们获得内核链接后要加载的地址和实际加载地址的差值。在这之后我们可以进行内核重定位,因为我们知道内核加载的实际地址、它被链接的运行的地址和内核镜像末尾的重定位表。 360 | 361 | 在内核重定位后,我们从`extract_kernel`回来,到 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/boot/compressed/head_64.S). 362 | 363 | 内核的地址在`rax`寄存器,我们跳到那里: 364 | 365 | ```assembly 366 | jmp *%rax 367 | ``` 368 | 369 | 就是这样。现在我们就在内核里! 370 | 371 | 结论 372 | -------------------------------------------------------------------------------- 373 | 374 | 这是关于内核引导过程的第五部分的结尾。我们不会再看到关于内核引导的文章(可能有这篇和前面的文章的更新),但是会有关于其他内核内部细节的很多文章。 375 | 376 | 下一章会描述更高级的关于内核引导过程的细节,如加载地址随机化等等。 377 | 378 | 如果你有什么问题或建议,写个评论或在 [twitter](https://twitter.com/0xAX) 找我。 379 | 380 | **如果你发现文中描述有任何问题,请提交一个 PR 到 [linux-insides-zh](https://github.com/MintCN/linux-insides-zh) 。** 381 | 382 | 链接 383 | -------------------------------------------------------------------------------- 384 | 385 | * [address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) 386 | * [initrd](https://en.wikipedia.org/wiki/Initrd) 387 | * [long mode](https://en.wikipedia.org/wiki/Long_mode) 388 | * [bzip2](http://www.bzip.org/) 389 | * [RDRand instruction](https://en.wikipedia.org/wiki/RdRand) 390 | * [Time Stamp Counter](https://en.wikipedia.org/wiki/Time_Stamp_Counter) 391 | * [Programmable Interval Timers](https://en.wikipedia.org/wiki/Intel_8253) 392 | * [Previous part](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-4.md) 393 | -------------------------------------------------------------------------------- /Booting/linux-bootstrap-6.md: -------------------------------------------------------------------------------- 1 | 内核引导过程. Part 6. 2 | ================================================================================ 3 | 4 | 简介 5 | -------------------------------------------------------------------------------- 6 | 7 | 这是`内核引导过程`系列文章的第六部分。在[前一部分](linux-bootstrap-5.md),我们已经看到了内核引导过程的结尾,但是我们跳过了一些高级部分。 8 | 9 | 你可能还记得,Linux内核的入口点是 [main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 的`start_kernel`函数,它在`LOAD_PHYSICAL_ADDR`地址开始执行。这个地址依赖于`CONFIG_PHYSICAL_START`内核配置选项,默认为`0x1000000`: 10 | 11 | ``` 12 | config PHYSICAL_START 13 | hex "Physical address where the kernel is loaded" if (EXPERT || CRASH_DUMP) 14 | default "0x1000000" 15 | ---help--- 16 | This gives the physical address where the kernel is loaded. 17 | ... 18 | ... 19 | ... 20 | ``` 21 | 22 | 这个选项在内核配置时可以修改,但是加载地址可以选择为一个随机值。为此,`CONFIG_RANDOMIZE_BASE`内核配置选项在内核配置时应该启用。 23 | 24 | 在这种情况下,Linux内核镜像解压和加载的物理地址会被随机化。我们在这一部分考虑这个选项被启用,并且为了[安全原因](https://en.wikipedia.org/wiki/Address_space_layout_randomization),内核镜像的加载地址被随机化的情况。 25 | 26 | 页表的初始化 27 | -------------------------------------------------------------------------------- 28 | 29 | 在内核解压器要开始找随机的内核解压和加载地址之前,应该初始化恒等映射(identity mapped,虚拟地址和物理地址相同)页表。如果[引导加载器](https://en.wikipedia.org/wiki/Booting)使用[16位或32位引导协议](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt),那么我们已经有了页表。但在任何情况下,如果内核解压器选择它们之外的内存区域,我们需要新的页。这就是为什么我们需要建立新的恒等映射页表。 30 | 31 | 是的,建立恒等映射页表是随机化加载地址的最早的步骤之一。但是在此之前,让我们回忆一下我们是怎么来到这里的。 32 | 33 | 在[前一部分](linux-bootstrap-5.md),我们看到了到[长模式](https://en.wikipedia.org/wiki/Long_mode)的转换,并跳转到了内核解压器的入口点——`extract_kernel`函数。随机化从调用这个函数开始: 34 | 35 | ```C 36 | void choose_random_location(unsigned long input, 37 | unsigned long input_size, 38 | unsigned long *output, 39 | unsigned long output_size, 40 | unsigned long *virt_addr) 41 | {} 42 | ``` 43 | 44 | 你可以看到,这个函数有五个参数: 45 | 46 | * `input`; 47 | * `input_size`; 48 | * `output`; 49 | * `output_isze`; 50 | * `virt_addr`. 51 | 52 | 让我们试着理解一下这些参数是什么。第一个`input`参数来自源文件 [arch/x86/boot/compressed/misc.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/misc.c) 里的`extract_kernel`函数: 53 | 54 | ```C 55 | asmlinkage __visible void *extract_kernel(void *rmode, memptr heap, 56 | unsigned char *input_data, 57 | unsigned long input_len, 58 | unsigned char *output, 59 | unsigned long output_len) 60 | { 61 | ... 62 | ... 63 | ... 64 | choose_random_location((unsigned long)input_data, input_len, 65 | (unsigned long *)&output, 66 | max(output_len, kernel_total_size), 67 | &virt_addr); 68 | ... 69 | ... 70 | ... 71 | } 72 | ``` 73 | 74 | 这个参数由 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 的汇编代码传递: 75 | 76 | ```C 77 | leaq input_data(%rip), %rdx 78 | ``` 79 | 80 | `input_data`由 [mkpiggy](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/mkpiggy.c) 程序生成。如果你亲手编译过Linux内核源码,你会找到这个程序生成的文件,它应该位于 `linux/arch/x86/boot/compressed/piggy.S`. 在我这里,这个文件是这样的: 81 | 82 | ```assembly 83 | .section ".rodata..compressed","a",@progbits 84 | .globl z_input_len 85 | z_input_len = 6988196 86 | .globl z_output_len 87 | z_output_len = 29207032 88 | .globl input_data, input_data_end 89 | input_data: 90 | .incbin "arch/x86/boot/compressed/vmlinux.bin.gz" 91 | input_data_end: 92 | ``` 93 | 94 | 你能看到它有四个全局符号。前两个`z_input_len`和`z_output_len`是压缩的和解压后的`vmlinux.bin.gz`的大小。第三个是我们的`input_data`,你可以看到,它指向二进制格式(去掉所有调试符号、注释和重定位信息)的Linux内核镜像。最后的`input_data_end`指向压缩的Linux镜像的末尾。 95 | 96 | 所以我们`choose_random_location`函数的第一个参数是指向嵌入在`piggy.o`目标文件的压缩的内核镜像的指针。 97 | 98 | `choose_random_location`函数的第二个参数是我们刚刚看到的`z_input_len`. 99 | 100 | `choose_random_location`函数的第三和第四个参数分别是解压后的内核镜像的位置和长度。放置解压后内核的地址来自 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S),并且它是`startup_32`对齐到 2MB 边界的地址。解压后的内核的大小来自同样的`piggy.S`,并且它是`z_output_len`. 101 | 102 | `choose_random_location`函数的最后一个参数是内核加载地址的虚拟地址。我们可以看到,它和默认的物理加载地址相同: 103 | 104 | ```C 105 | unsigned long virt_addr = LOAD_PHYSICAL_ADDR; 106 | ``` 107 | 108 | 它依赖于内核配置: 109 | 110 | ```C 111 | #define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \ 112 | + (CONFIG_PHYSICAL_ALIGN - 1)) \ 113 | & ~(CONFIG_PHYSICAL_ALIGN - 1)) 114 | ``` 115 | 116 | 现在,由于我们考虑`choose_random_location`函数的参数,让我们看看它的实现。这个函数从检查内核命令行的`nokaslr`选项开始: 117 | 118 | ```C 119 | if (cmdline_find_option_bool("nokaslr")) { 120 | warn("KASLR disabled: 'nokaslr' on cmdline."); 121 | return; 122 | } 123 | ``` 124 | 125 | 如果有这个选项,那么我们就退出`choose_random_location`函数,并且内核的加载地址不会随机化。相关的命令行选项可以在[内核文档](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/kernel-parameters.txt)找到: 126 | 127 | ``` 128 | kaslr/nokaslr [X86] 129 | 130 | Enable/disable kernel and module base offset ASLR 131 | (Address Space Layout Randomization) if built into 132 | the kernel. When CONFIG_HIBERNATION is selected, 133 | kASLR is disabled by default. When kASLR is enabled, 134 | hibernation will be disabled. 135 | ``` 136 | 137 | 假设我们没有把`nokaslr`传到内核命令行,并且`CONFIG_RANDOMIZE_BASE`启用了内核配置选项。 138 | 139 | 下一步是以下函数的调用: 140 | 141 | ```C 142 | initialize_identity_maps(); 143 | ``` 144 | 145 | 它在 [arch/x86/boot/compressed/pagetable.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/pagetable.c) 源码文件定义。这个函数从初始化`mapping_info`,`x86_mapping_info`结构体的一个实例开始。 146 | 147 | ```C 148 | mapping_info.alloc_pgt_page = alloc_pgt_page; 149 | mapping_info.context = &pgt_data; 150 | mapping_info.page_flag = __PAGE_KERNEL_LARGE_EXEC | sev_me_mask; 151 | mapping_info.kernpg_flag = _KERNPG_TABLE | sev_me_mask; 152 | ``` 153 | 154 | `x86_mapping_info`结构体在 [arch/x86/include/asm/init.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/init.h) 头文件定义: 155 | 156 | ```C 157 | struct x86_mapping_info { 158 | void *(*alloc_pgt_page)(void *); 159 | void *context; 160 | unsigned long page_flag; 161 | unsigned long offset; 162 | bool direct_gbpages; 163 | unsigned long kernpg_flag; 164 | }; 165 | ``` 166 | 167 | 这个结构体提供了关于内存映射的信息。你可能还记得,在前面的部分,我们已经建立了初始的从0到`4G`的页表。现在我们可能需要访问`4G`以上的内存来在随机的位置加载内核。所以,`initialize_identity_maps`函数初始化一个内存区域,它用于可能需要的新页表。首先,让我们尝试查看`x86_mapping_info`结构体的定义。 168 | 169 | `alloc_pgt_page`是一个会在为一个页表项分配空间时调用的回调函数。`context`域是一个用于跟踪已分配页表的`alloc_pgt_data`结构体的实例。`page_flag`和`kernpg_flag`是页标志。第一个代表`PMD`或`PUD`表项的标志。第二个`kernpg_flag`域代表会在之后被覆盖的内核页的标志。`direct_gbpages`域代表对大页的支持。最后的`offset`域代表内核虚拟地址到`PMD`级物理地址的偏移。 170 | 171 | `alloc_pgt_page`回调函数检查有一个新页的空间,从缓冲区分配新页并返回新页的地址: 172 | 173 | 174 | ```C 175 | entry = pages->pgt_buf + pages->pgt_buf_offset; 176 | pages->pgt_buf_offset += PAGE_SIZE; 177 | ``` 178 | 179 | 缓冲区在此结构体中: 180 | 181 | ```C 182 | struct alloc_pgt_data { 183 | unsigned char *pgt_buf; 184 | unsigned long pgt_buf_size; 185 | unsigned long pgt_buf_offset; 186 | }; 187 | ``` 188 | 189 | `initialize_identity_maps`函数最后的目标是初始化`pgdt_buf_size`和`pgt_buf_offset`. 由于我们只是在初始化阶段,`initialize_identity_maps`函数设置`pgt_buf_offset`为0: 190 | 191 | ```C 192 | pgt_data.pgt_buf_offset = 0; 193 | ``` 194 | 195 | 而`pgt_data.pgt_buf_size`会根据引导加载器所用的引导协议(64位或32位)被设置为`77824`或`69632`. `pgt_data.pgt_buf`也是一样。如果引导加载器在`startup_32`引导内核,`pgdt_data.pgdt_buf`会指向已经在 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 初始化的页表的末尾: 196 | 197 | ```C 198 | pgt_data.pgt_buf = _pgtable + BOOT_INIT_PGT_SIZE; 199 | ``` 200 | 201 | 其中`_pgtable`指向这个页表 [_pgtable](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/vmlinux.lds.S) 的开头。另一方面,如果引导加载器用64位引导协议并在`startup_64`加载内核,早期页表应该由引导加载器建立,并且`_pgtable`会被重写: 202 | 203 | ```C 204 | pgt_data.pgt_buf = _pgtable 205 | ``` 206 | 207 | 在新页表的缓冲区被初始化之下,我们回到`choose_random_location`函数。 208 | 209 | 避开保留的内存范围 210 | -------------------------------------------------------------------------------- 211 | 212 | 在恒等映射页表相关的数据被初始化之后,我们可以开始选择放置解压后内核的随机位置。但是正如你猜的那样,我们不能选择任意地址。在内存的范围中,有一些保留的地址。这些地址被重要的东西占用,如[initrd](https://en.wikipedia.org/wiki/Initial_ramdisk), 内核命令行等等。这个函数: 213 | 214 | ```C 215 | mem_avoid_init(input, input_size, *output); 216 | ``` 217 | 218 | 会帮我们做这件事。所有不安全的内存区域会收集到: 219 | 220 | ```C 221 | struct mem_vector { 222 | unsigned long long start; 223 | unsigned long long size; 224 | }; 225 | 226 | static struct mem_vector mem_avoid[MEM_AVOID_MAX]; 227 | ``` 228 | 229 | 数组。其中`MEM_AVOID_MAX`来自[枚举类型](https://en.wikipedia.org/wiki/Enumerated_type#C)`mem_avoid_index`, 它代表不同类型的保留内存区域: 230 | 231 | ```C 232 | enum mem_avoid_index { 233 | MEM_AVOID_ZO_RANGE = 0, 234 | MEM_AVOID_INITRD, 235 | MEM_AVOID_CMDLINE, 236 | MEM_AVOID_BOOTPARAMS, 237 | MEM_AVOID_MEMMAP_BEGIN, 238 | MEM_AVOID_MEMMAP_END = MEM_AVOID_MEMMAP_BEGIN + MAX_MEMMAP_REGIONS - 1, 239 | MEM_AVOID_MAX, 240 | }; 241 | ``` 242 | 243 | 它们都定义在源文件 [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c) 中。 244 | 245 | 让我们看看`mem_avoid_init`函数的实现。这个函数的主要目标是在`mem_avoid`数组存放关于被`mem_avoid_index`枚举类型描述的保留内存区域的信息,并且在我们新的恒等映射缓冲区为这样的区域创建新页。`mem_avoid_index`函数的几个部分很相似,但是先看看其中一个: 246 | 247 | ```C 248 | mem_avoid[MEM_AVOID_ZO_RANGE].start = input; 249 | mem_avoid[MEM_AVOID_ZO_RANGE].size = (output + init_size) - input; 250 | add_identity_map(mem_avoid[MEM_AVOID_ZO_RANGE].start, 251 | mem_avoid[MEM_AVOID_ZO_RANGE].size); 252 | ``` 253 | 254 | `mem_avoid_init`函数的开头尝试避免用于当前内核解压的内存区域。我们用这个区域的起始地址和大小填写`mem_avoid`数组的一项,并调用`add_identity_map`函数,它会为这个区域建立恒等映射页。`add_identity_map`函数在源文件 [arch/x86/boot/compressed/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c) 定义: 255 | 256 | ```C 257 | void add_identity_map(unsigned long start, unsigned long size) 258 | { 259 | unsigned long end = start + size; 260 | 261 | start = round_down(start, PMD_SIZE); 262 | end = round_up(end, PMD_SIZE); 263 | if (start >= end) 264 | return; 265 | 266 | kernel_ident_mapping_init(&mapping_info, (pgd_t *)top_level_pgt, 267 | start, end); 268 | } 269 | ``` 270 | 271 | 你可以看到,它对齐内存到 2MB 边界并检查给定的起始地址和终止地址。 272 | 273 | 最后它调用`kernel_ident_mapping_init`函数,它在源文件 [arch/x86/mm/ident_map.c](https://github.com/torvalds/linux/blob/master/arch/x86/mm/ident_map.c) 中,并传入以上初始化好的`mapping_info`实例、顶层页表的地址和建立新的恒等映射的内存区域的地址。 274 | 275 | `kernel_ident_mapping_init`函数为新页设置默认的标志,如果它们没有被给出: 276 | 277 | ```C 278 | if (!info->kernpg_flag) 279 | info->kernpg_flag = _KERNPG_TABLE; 280 | ``` 281 | 282 | 并且开始建立新的2MB (因为`mapping_info.page_flag`中的`PSE`位) 给定地址相关的页表项([五级页表](https://lwn.net/Articles/717293/)中的`PGD -> P4D -> PUD -> PMD`或者[四级页表](https://lwn.net/Articles/117749/)中的`PGD -> PUD -> PMD`)。 283 | 284 | ```C 285 | for (; addr < end; addr = next) { 286 | p4d_t *p4d; 287 | 288 | next = (addr & PGDIR_MASK) + PGDIR_SIZE; 289 | if (next > end) 290 | next = end; 291 | 292 | p4d = (p4d_t *)info->alloc_pgt_page(info->context); 293 | result = ident_p4d_init(info, p4d, addr, next); 294 | 295 | return result; 296 | } 297 | ``` 298 | 299 | 首先我们找给定地址在 `页全局目录` 的下一项,如果它大于给定的内存区域的末地址`end`,我们把它设为`end`.之后,我们用之前看过的`x86_mapping_info`回调函数分配一个新页,然后调用`ident_p4d_init`函数。`ident_p4d_init`函数做同样的事情,但是用于低层的页目录 (`p4d` -> `pud` -> `pmd`). 300 | 301 | 就是这样。 302 | 303 | 和保留地址相关的新页表项已经在我们的页表中。这不是`mem_avoid_init`函数的末尾,但是其他部分类似。它建立用于 [initrd](https://en.wikipedia.org/wiki/Initial_ramdisk)、内核命令行等数据的页。 304 | 305 | 现在我们可以回到`choose_random_location`函数。 306 | 307 | 物理地址随机化 308 | -------------------------------------------------------------------------------- 309 | 310 | 在保留内存区域存储在`mem_avoid`数组并且为它们建立了恒等映射页之后,我们选择最小可用的地址作为解压内核的随机内存区域: 311 | 312 | ```C 313 | min_addr = min(*output, 512UL << 20); 314 | ``` 315 | 316 | 你可以看到,它应该小于512MB. 选择这个512MB的值只是避免低内存区域中未知的东西。 317 | 318 | 下一步是选择随机的物理和虚拟地址来加载内核。首先是物理地址: 319 | 320 | ```C 321 | random_addr = find_random_phys_addr(min_addr, output_size); 322 | ``` 323 | 324 | `find_random_phys_addr`函数在[同一个](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/kaslr.c)源文件中定义: 325 | 326 | ``` 327 | static unsigned long find_random_phys_addr(unsigned long minimum, 328 | unsigned long image_size) 329 | { 330 | minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN); 331 | 332 | if (process_efi_entries(minimum, image_size)) 333 | return slots_fetch_random(); 334 | 335 | process_e820_entries(minimum, image_size); 336 | return slots_fetch_random(); 337 | } 338 | ``` 339 | 340 | `process_efi_entries`函数的主要目标是在整个可用的内存找到所有的合适的内存区域来加载内核。如果内核没有在支持[EFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface)的系统中编译和运行,我们继续在[e820](https://en.wikipedia.org/wiki/E820)区域中找这样的内存区域。所有找到的内存区域会存储在 341 | 342 | ```C 343 | struct slot_area { 344 | unsigned long addr; 345 | int num; 346 | }; 347 | 348 | #define MAX_SLOT_AREA 100 349 | 350 | static struct slot_area slot_areas[MAX_SLOT_AREA]; 351 | ``` 352 | 353 | 数组中。内核解压器应该选择这个数组随机的索引,并且它会是内核解压的随机位置。这个选择会被`slots_fetch_random`函数执行。`slots_fetch_random`函数的主要目标是通过`kaslr_get_random_long`函数从`slot_areas`数组选择随机的内存范围: 354 | 355 | ```C 356 | slot = kaslr_get_random_long("Physical") % slot_max; 357 | ``` 358 | 359 | `kaslr_get_random_long`函数在源文件 [arch/x86/lib/kaslr.c](https://github.com/torvalds/linux/blob/master/arch/x86/lib/kaslr.c) 中定义,它返回一个随机数。注意这个随机数会通过不同的方式得到,取决于内核配置、系统机会(基于[时间戳计数器](https://en.wikipedia.org/wiki/Time_Stamp_Counter)的随机数、[rdrand](https://en.wikipedia.org/wiki/RdRand)等等)。 360 | 361 | 这就是随机内存范围的选择方法。 362 | 363 | 虚拟地址随机化 364 | -------------------------------------------------------------------------------- 365 | 366 | 在内核解压器选择了随机内存区域后,新的恒等映射页会为这个区域按需建立: 367 | 368 | ```C 369 | random_addr = find_random_phys_addr(min_addr, output_size); 370 | 371 | if (*output != random_addr) { 372 | add_identity_map(random_addr, output_size); 373 | *output = random_addr; 374 | } 375 | ``` 376 | 377 | 这时,`output`会存放内核将会解压的一个内存区域的基地址。但是现在,正如你还记得的那样,我们只是随机化了物理地址。在[x86_64](https://en.wikipedia.org/wiki/X86-64)架构,虚拟地址也应该被随机化: 378 | 379 | ```C 380 | if (IS_ENABLED(CONFIG_X86_64)) 381 | random_addr = find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size); 382 | 383 | *virt_addr = random_addr; 384 | ``` 385 | 386 | 正如你所看到的,对于非`x86_64`架构,随机化的虚拟地址和随机化的物理地址相同。`find_random_virt_addr`函数计算可以保存内存镜像的虚拟内存范围的数量并且调用我们在尝试找到随机的`物理`地址的时候,之前已经看到的`kaslr_get_random_long`函数。 387 | 388 | 这时,我们同时有了用于解压内核的随机化的物理(`*output`)和虚拟(`*virt_addr`)基地址。 389 | 390 | 就是这样。 391 | 392 | 结论 393 | -------------------------------------------------------------------------------- 394 | 395 | 这是关于Linux内核引导过程的第六,并且是最后一部分的结尾。我们不再会看到关于内核引导的帖子(可能有对这篇和之前文章的更新),但是会有很多关于其他内核内部细节的文章。 396 | 397 | 下一章是关于内核初始化的,我们会看到Linux内核初始化代码的早期步骤。 398 | 399 | 如果你有什么问题或建议,写个评论或在 [twitter](https://twitter.com/0xAX) 找我。 400 | 401 | **如果你发现文中描述有任何问题,请提交一个 PR 到 [linux-insides-zh](https://github.com/MintCN/linux-insides-zh) 。** 402 | 403 | Links 404 | -------------------------------------------------------------------------------- 405 | 406 | * [Address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) 407 | * [Linux kernel boot protocol](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt) 408 | * [long mode](https://en.wikipedia.org/wiki/Long_mode) 409 | * [initrd](https://en.wikipedia.org/wiki/Initial_ramdisk) 410 | * [Enumerated type](https://en.wikipedia.org/wiki/Enumerated_type#C) 411 | * [four-level page tables](https://lwn.net/Articles/117749/) 412 | * [five-level page tables](https://lwn.net/Articles/717293/) 413 | * [EFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface) 414 | * [e820](https://en.wikipedia.org/wiki/E820) 415 | * [time stamp counter](https://en.wikipedia.org/wiki/Time_Stamp_Counter) 416 | * [rdrand](https://en.wikipedia.org/wiki/RdRand) 417 | * [x86_64](https://en.wikipedia.org/wiki/X86-64) 418 | * [Previous part](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-5.md) 419 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 贡献 2 | ================================================================================ 3 | 4 | 如果你想要给 [linux-insides-zh](https://github.com/MintCN/linux-insides-zh) 做贡献,请遵照如下的规则: 5 | 6 | 1. 点击 `fork` 按钮: 7 | 8 | ![fork](http://oi58.tinypic.com/jj2trm.jpg) 9 | 10 | 2. 通过如下命令从你的 Github 帐号 `clone` 库: 11 | 12 | ``` 13 | git clone git@github.com:your_github_username/linux-insides-zh.git 14 | ``` 15 | 16 | 3. 通过如下命令创建分支 (`branch`) : 17 | 18 | ``` 19 | git checkout -b "linux-insides-zh-fix" 20 | # linux-insides-zh-fix is just an example 21 | ``` 22 | 23 | 4. 对本地库进行修改。 24 | 25 | 5. 提交自己的修改,然后推送 (`push`) 到远端。 26 | 27 | ``` 28 | git add your_changed_files 29 | git commit -m "your comment" 30 | git push --set-upstream origin linux-insides-zh-fix 31 | ``` 32 | 33 | 6. 点击 `New pull request` 按钮,将 your_github_username 的 `linux-insides-zh` 库的 linux-insides-zh-fix 分支的修改提交到 MintCN 的 `linux-insides-zh` 库中。 34 | 35 | 十分感谢! 36 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | ## 翻译人员 (排名不分先后) 2 | 3 | [@xinqiu](https://github.com/xinqiu) 4 | 5 | [@lijiangsheng1](https://github.com/lijiangsheng1) 6 | 7 | [@littleneko](https://github.com/littleneko) 8 | 9 | [@qianmoke](https://github.com/qianmoke) 10 | 11 | [@icecoobe](https://github.com/icecoobe) 12 | 13 | [@choleraehyq](http://github.com/choleraehyq) 14 | 15 | [@mudongliang](https://github.com/mudongliang) 16 | 17 | [@oska874](https://github.com/oska874) 18 | 19 | [@cloudusers](https://github.com/cloudusers) 20 | 21 | [@hailincai](https://github.com/hailincai) 22 | 23 | [@zmj1316](https://github.com/zmj1316) 24 | 25 | [@zhangyangjing](https://github.com/zhangyangjing) 26 | 27 | [@huxq](https://github.com/huxq) 28 | 29 | [@worldwar](https://github.com/worldwar) 30 | 31 | [@keltoy](https://github.com/keltoy) 32 | 33 | [@a1ickgu0](https://github.com/a1ickgu0) 34 | 35 | [@hao-lee](https://github.com/hao-lee) 36 | 37 | [@woodpenker](http://github.com/woodpenker) 38 | 39 | [@tjm-1990](http://github.com/tjm-1990) 40 | 41 | [@up2wing](https://github.com/up2wing) 42 | 43 | [@NeoCui](https://github.com/NeoCui) 44 | -------------------------------------------------------------------------------- /Cgroups/README.md: -------------------------------------------------------------------------------- 1 | # 控制组 2 | 3 | 这个章节描述了 Linux 内核中的控制组机制。 4 | 5 | * [简介](linux-cgroups-1.md) 6 | -------------------------------------------------------------------------------- /Concepts/README.md: -------------------------------------------------------------------------------- 1 | # Linux 内核概念 2 | 3 | 本章描述内核中使用到的各种各样的概念。 4 | 5 | * [每个 CPU 的变量](linux-cpu-1.md) 6 | * [CPU 掩码](linux-cpu-2.md) 7 | * [initcall 机制](linux-cpu-3.md) 8 | * [Linux 内核的通知链](linux-cpu-4.md) 9 | -------------------------------------------------------------------------------- /Concepts/linux-cpu-1.md: -------------------------------------------------------------------------------- 1 | Per-cpu 变量 2 | ================================================================================ 3 | 4 | Per-cpu 变量是一项内核特性。从它的名字你就可以理解这项特性的意义了。我们可以创建一个变量,然后每个 CPU 上都会有一个此变量的拷贝。本节我们来看下这个特性,并试着去理解它是如何实现以及工作的。 5 | 6 | 内核提供了一个创建 per-cpu 变量的 API - `DEFINE_PER_CPU` 宏: 7 | 8 | ```C 9 | #define DEFINE_PER_CPU(type, name) \ 10 | DEFINE_PER_CPU_SECTION(type, name, "") 11 | ``` 12 | 13 | 正如其它许多处理 per-cpu 变量的宏一样,这个宏定义在 [include/linux/percpu-defs.h](https://github.com/torvalds/linux/blob/master/include/linux/percpu-defs.h) 中。现在我们来看下这个特性是如何实现的。 14 | 15 | 看下 `DECLARE_PER_CPU` 的定义,可以看到它使用了 2 个参数:`type` 和 `name`,因此我们可以这样创建 per-cpu 变量: 16 | 17 | ```C 18 | DEFINE_PER_CPU(int, per_cpu_n) 19 | ``` 20 | 21 | 我们传入要创建变量的类型和名字,`DEFINE_PER_CPU` 调用 `DEFINE_PER_CPU_SECTION`,将两个参数和空字符串传递给后者。让我们来看下 `DEFINE_PER_CPU_SECTION` 的定义: 22 | 23 | ```C 24 | #define DEFINE_PER_CPU_SECTION(type, name, sec) \ 25 | __PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \ 26 | __typeof__(type) name 27 | ``` 28 | 29 | ```C 30 | #define __PCPU_ATTRS(sec) \ 31 | __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \ 32 | PER_CPU_ATTRIBUTES 33 | ``` 34 | 35 | 其中 `section` 是: 36 | 37 | ```C 38 | #define PER_CPU_BASE_SECTION ".data..percpu" 39 | ``` 40 | 41 | 当所有的宏展开之后,我们得到一个全局的 per-cpu 变量: 42 | 43 | ```C 44 | __attribute__((section(".data..percpu"))) int per_cpu_n 45 | ``` 46 | 47 | 这意味着我们在 `.data..percpu` 段有了一个 `per_cpu_n` 变量,可以在 `vmlinux` 中找到它: 48 | 49 | ``` 50 | .data..percpu 00013a58 0000000000000000 0000000001a5c000 00e00000 2**12 51 | CONTENTS, ALLOC, LOAD, DATA 52 | ``` 53 | 54 | 好,现在我们知道了,当我们使用 `DEFINE_PER_CPU` 宏时,一个在 `.data..percpu` 段中的 per-cpu 变量就被创建了。内核初始化时,调用 `setup_per_cpu_areas` 函数多次加载 `.data..percpu` 段,每个 CPU 一次。 55 | 56 | 让我们来看下 per-cpu 区域初始化流程。它从 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 中调用 `setup_per_cpu_areas` 函数开始,这个函数定义在 [arch/x86/kernel/setup_percpu.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup_percpu.c) 中。 57 | 58 | ```C 59 | pr_info("NR_CPUS:%d nr_cpumask_bits:%d nr_cpu_ids:%d nr_node_ids:%d\n", 60 | NR_CPUS, nr_cpumask_bits, nr_cpu_ids, nr_node_ids); 61 | ``` 62 | 63 | `setup_per_cpu_areas` 开始输出在内核配置中以 `CONFIG_NR_CPUS` 配置项设置的最大 CPUs 数,实际的 CPU 个数,`nr_cpumask_bits`(对于新的 `cpumask` 操作来说和 `NR_CPUS` 是一样的),还有 `NUMA` 节点个数。 64 | 65 | 我们可以在 `dmesg` 中看到这些输出: 66 | 67 | ``` 68 | $ dmesg | grep percpu 69 | [ 0.000000] setup_percpu: NR_CPUS:8 nr_cpumask_bits:8 nr_cpu_ids:8 nr_node_ids:1 70 | ``` 71 | 72 | 然后我们检查 `per-cpu` 第一个块分配器。所有的 per-cpu 区域都是以块进行分配的。第一个块用于静态 per-cpu 变量。Linux 内核提供了决定第一个块分配器类型的命令行:`percpu_alloc` 。我们可以在内核文档中读到它的说明。 73 | 74 | ``` 75 | percpu_alloc= 选择要使用哪个 per-cpu 第一个块分配器。 76 | 当前支持的类型是 "embed" 和 "page"。 77 | 不同架构支持这些类型的子集或不支持。 78 | 更多分配器的细节参考 mm/percpu.c 中的注释。 79 | 这个参数主要是为了调试和性能比较的。 80 | ``` 81 | 82 | [mm/percpu.c](https://github.com/torvalds/linux/blob/master/mm/percpu.c) 包含了这个命令行选项的处理函数: 83 | 84 | ```C 85 | early_param("percpu_alloc", percpu_alloc_setup); 86 | ``` 87 | 88 | 其中 `percpu_alloc_setup` 函数根据 `percpu_alloc` 参数值设置 `pcpu_chosen_fc` 变量。默认第一个块分配器是 `auto`: 89 | 90 | ```C 91 | enum pcpu_fc pcpu_chosen_fc __initdata = PCPU_FC_AUTO; 92 | ``` 93 | 94 | 如果内核命令行中没有设置 `percpu_alloc` 参数,就会使用 `embed` 分配器,将第一个 per-cpu 块嵌入进带 [memblock](http://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-1.html) 的 bootmem。最后一个分配器和第一个块 `page` 分配器一样,只是将第一个块使用 `PAGE_SIZE` 页进行了映射。 95 | 96 | 如我上面所写,首先我们在 `setup_per_cpu_areas` 中对第一个块分配器检查,检查到第一个块分配器不是 page 分配器: 97 | 98 | ```C 99 | if (pcpu_chosen_fc != PCPU_FC_PAGE) { 100 | ... 101 | ... 102 | ... 103 | } 104 | ``` 105 | 106 | 如果不是 `PCPU_FC_PAGE`,我们就使用 `embed` 分配器并使用 `pcpu_embed_first_chunk` 函数分配第一块空间。 107 | 108 | ```C 109 | rc = pcpu_embed_first_chunk(PERCPU_FIRST_CHUNK_RESERVE, 110 | dyn_size, atom_size, 111 | pcpu_cpu_distance, 112 | pcpu_fc_alloc, pcpu_fc_free); 113 | ``` 114 | 115 | 如前所述,函数 `pcpu_embed_first_chunk` 将第一个 per-cpu 块嵌入 bootmen,因此我们传递一些参数给 `pcpu_embed_first_chunk`。参数如下: 116 | 117 | * `PERCPU_FIRST_CHUNK_RESERVE` - 为静态变量 `per-cpu` 保留空间的大小; 118 | * `dyn_size` - 动态分配的最少空闲字节; 119 | * `atom_size` - 所有的分配都是这个的整数倍,并以此对齐; 120 | * `pcpu_cpu_distance` - 决定 cpus 距离的回调函数; 121 | * `pcpu_fc_alloc` - 分配 `percpu` 页的函数; 122 | * `pcpu_fc_free` - 释放 `percpu` 页的函数。 123 | 124 | 在调用 `pcpu_embed_first_chunk` 前我们计算好所有的参数: 125 | 126 | ```C 127 | const size_t dyn_size = PERCPU_MODULE_RESERVE + PERCPU_DYNAMIC_RESERVE - PERCPU_FIRST_CHUNK_RESERVE; 128 | size_t atom_size; 129 | #ifdef CONFIG_X86_64 130 | atom_size = PMD_SIZE; 131 | #else 132 | atom_size = PAGE_SIZE; 133 | #endif 134 | ``` 135 | 136 | 如果第一个块分配器是 `PCPU_FC_PAGE`,我们用 `pcpu_page_first_chunk` 而不是 `pcpu_embed_first_chunk`。 `per-cpu` 区域准备好以后,我们用 `setup_percpu_segment` 函数设置 `per-cpu` 的偏移和段(只针对 `x86` 系统),并将前面的数据从数组移到 `per-cpu` 变量(`x86_cpu_to_apicid`, `irq_stack_ptr` 等等)。当内核完成初始化进程后,我们就有了N个 `.data..percpu` 段,其中 N 是 CPU 个数,bootstrap 进程使用的段将会包含用 `DEFINE_PER_CPU` 宏创建的未初始化的变量。 137 | 138 | 内核提供了操作 per-cpu 变量的API: 139 | 140 | * get_cpu_var(var) 141 | * put_cpu_var(var) 142 | 143 | 让我们来看看 `get_cpu_var` 的实现: 144 | 145 | ```C 146 | #define get_cpu_var(var) \ 147 | (*({ \ 148 | preempt_disable(); \ 149 | this_cpu_ptr(&var); \ 150 | })) 151 | ``` 152 | 153 | Linux 内核是抢占式的,获取 per-cpu 变量需要我们知道内核运行在哪个处理器上。因此访问 per-cpu 变量时,当前代码不能被抢占,不能移到其它的 CPU。如我们所见,这就是为什么首先调用 `preempt_disable` 函数然后调用 `this_cpu_ptr` 宏,像这样: 154 | 155 | ```C 156 | #define this_cpu_ptr(ptr) raw_cpu_ptr(ptr) 157 | ``` 158 | 159 | 以及 160 | 161 | ```C 162 | #define raw_cpu_ptr(ptr) per_cpu_ptr(ptr, 0) 163 | ``` 164 | 165 | `per_cpu_ptr` 返回一个指向给定 CPU(第 2 个参数) per-cpu 变量的指针。当我们创建了一个 per-cpu 变量并对其进行了修改时,我们必须调用 `put_cpu_var` 宏通过函数 `preempt_enable` 使能抢占。因此典型的 per-cpu 变量的使用如下: 166 | 167 | ```C 168 | get_cpu_var(var); 169 | ... 170 | //用这个 'var' 做些啥 171 | ... 172 | put_cpu_var(var); 173 | ``` 174 | 175 | 让我们来看下这个 `per_cpu_ptr` 宏: 176 | 177 | ```C 178 | #define per_cpu_ptr(ptr, cpu) \ 179 | ({ \ 180 | __verify_pcpu_ptr(ptr); \ 181 | SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \ 182 | }) 183 | ``` 184 | 185 | 就像我们上面写的,这个宏返回了一个给定 cpu 的 per-cpu 变量。首先它调用了 `__verify_pcpu_ptr`: 186 | 187 | ```C 188 | #define __verify_pcpu_ptr(ptr) 189 | do { 190 | const void __percpu *__vpp_verify = (typeof((ptr) + 0))NULL; 191 | (void)__vpp_verify; 192 | } while (0) 193 | ``` 194 | 195 | 该宏声明了 `ptr` 类型的 `const void __percpu *`。 196 | 197 | 之后,我们可以看到带两个参数的 `SHIFT_PERCPU_PTR` 宏的调用。第一个参数是我们的指针,第二个参数是传给 `per_cpu_offset` 宏的CPU数: 198 | 199 | ```C 200 | #define per_cpu_offset(x) (__per_cpu_offset[x]) 201 | ``` 202 | 203 | 该宏将 `x` 扩展为 `__per_cpu_offset` 数组: 204 | 205 | ```C 206 | extern unsigned long __per_cpu_offset[NR_CPUS]; 207 | ``` 208 | 209 | 其中 `NR_CPUS` 是 CPU 的数目。`__per_cpu_offset` 数组以 CPU 变量拷贝之间的距离填充。例如,所有 per-cpu 变量是 `X` 字节大小,所以我们通过 `__per_cpu_offset[Y]` 就可以访问 `X*Y`。让我们来看下 `SHIFT_PERCPU_PTR` 的实现: 210 | 211 | ```C 212 | #define SHIFT_PERCPU_PTR(__p, __offset) \ 213 | RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)) 214 | ``` 215 | 216 | `RELOC_HIDE` 只是取得偏移量 `(typeof(ptr)) (__ptr + (off))`,并返回一个指向该变量的指针。 217 | 218 | 就这些了!当然这不是全部的 API,只是一个大概。开头是比较艰难,但是理解 per-cpu 变量你只需理解 [include/linux/percpu-defs.h](https://github.com/torvalds/linux/blob/master/include/linux/percpu-defs.h) 的奥秘。 219 | 220 | 让我们再看下获得 per-cpu 变量指针的算法: 221 | 222 | * 内核在初始化流程中创建多个 `.data..percpu` 段(一个 per-cpu 变量一个); 223 | * 所有 `DEFINE_PER_CPU` 宏创建的变量都将重新分配到首个扇区或者 CPU0; 224 | * `__per_cpu_offset` 数组以 (`BOOT_PERCPU_OFFSET`) 和 `.data..percpu` 扇区之间的距离填充; 225 | * 当 `per_cpu_ptr` 被调用时,例如取一个 per-cpu 变量的第三个 CPU 的指针,将访问 `__per_cpu_offset` 数组,该数组的索引指向了所需 CPU。 226 | 227 | 就这么多了。 228 | -------------------------------------------------------------------------------- /Concepts/linux-cpu-2.md: -------------------------------------------------------------------------------- 1 | CPU masks 2 | ================================================================================ 3 | 4 | 介绍 5 | -------------------------------------------------------------------------------- 6 | 7 | `Cpumasks` 是Linux内核提供的保存系统CPU信息的特殊方法。包含 `Cpumasks` 操作 API 相关的源码和头文件: 8 | 9 | * [include/linux/cpumask.h](https://github.com/torvalds/linux/blob/master/include/linux/cpumask.h) 10 | * [lib/cpumask.c](https://github.com/torvalds/linux/blob/master/lib/cpumask.c) 11 | * [kernel/cpu.c](https://github.com/torvalds/linux/blob/master/kernel/cpu.c) 12 | 13 | 正如 [include/linux/cpumask.h](https://github.com/torvalds/linux/blob/master/include/linux/cpumask.h) 注释:Cpumasks 提供了代表系统中 CPU 集合的位图,一位放置一个 CPU 序号。我们已经在 [Kernel entry point](http://0xax.gitbooks.io/linux-insides/content/Initialization/linux-initialization-4.html) 部分,函数 `boot_cpu_init` 中看到了一点 cpumask。这个函数将第一个启动的 cpu 上线、激活等等…… 14 | 15 | ```C 16 | set_cpu_online(cpu, true); 17 | set_cpu_active(cpu, true); 18 | set_cpu_present(cpu, true); 19 | set_cpu_possible(cpu, true); 20 | ``` 21 | 22 | `set_cpu_possible` 是一个在系统启动时任意时刻都可插入的 cpu ID 集合。`cpu_present` 代表了当前插入的 CPUs。`cpu_online` 是 `cpu_present` 的子集,表示可调度的 CPUs。这些掩码依赖于 `CONFIG_HOTPLUG_CPU` 配置选项,以及 `possible == present` 和 `active == online` 选项是否被禁用。这些函数的实现很相似,检测第二个参数,如果为 `true`,就调用 `cpumask_set_cpu` ,否则调用 `cpumask_clear_cpu`。 23 | 24 | 有两种方法创建 `cpumask`。第一种是用 `cpumask_t`。定义如下: 25 | 26 | ```C 27 | typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t; 28 | ``` 29 | 30 | 它封装了 `cpumask` 结构,其包含了一个位掩码 `bits` 字段。`DECLARE_BITMAP` 宏有两个参数: 31 | 32 | * bitmap name; 33 | * number of bits. 34 | 35 | 并以给定名称创建了一个 `unsigned long` 数组。它的实现非常简单: 36 | 37 | ```C 38 | #define DECLARE_BITMAP(name,bits) \ 39 | unsigned long name[BITS_TO_LONGS(bits)] 40 | ``` 41 | 42 | 其中 `BITS_TO_LONGS`: 43 | 44 | ```C 45 | #define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long)) 46 | #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d)) 47 | ``` 48 | 49 | 因为我们专注于 `x86_64` 架构,`unsigned long` 是8字节大小,因此我们的数组仅包含一个元素: 50 | 51 | ``` 52 | (((8) + (8) - 1) / (8)) = 1 53 | ``` 54 | 55 | `NR_CPUS` 宏表示的是系统中 CPU 的数目,且依赖于在 [include/linux/threads.h](https://github.com/torvalds/linux/blob/master/include/linux/threads.h) 中定义的 `CONFIG_NR_CPUS` 宏,看起来像这样: 56 | 57 | ```C 58 | #ifndef CONFIG_NR_CPUS 59 | #define CONFIG_NR_CPUS 1 60 | #endif 61 | 62 | #define NR_CPUS CONFIG_NR_CPUS 63 | ``` 64 | 65 | 第二种定义 cpumask 的方法是直接使用宏 `DECLARE_BITMAP` 和 `to_cpumask` 宏,后者将给定的位图转化为 `struct cpumask *`: 66 | 67 | ```C 68 | #define to_cpumask(bitmap) \ 69 | ((struct cpumask *)(1 ? (bitmap) \ 70 | : (void *)sizeof(__check_is_bitmap(bitmap)))) 71 | ``` 72 | 73 | 可以看到这里的三目运算符每次总是 `true`。`__check_is_bitmap` 内联函数定义为: 74 | 75 | ```C 76 | static inline int __check_is_bitmap(const unsigned long *bitmap) 77 | { 78 | return 1; 79 | } 80 | ``` 81 | 82 | 每次都是返回 `1`。我们需要它只是因为:编译时检测一个给定的 `bitmap` 是一个位图,换句话说,它检测一个 `bitmap` 是否有 `unsigned long *` 类型。因此我们传递 `cpu_possible_bits` 给宏 `to_cpumask` ,将 `unsigned long` 数组转换为 `struct cpumask *`。 83 | 84 | cpumask API 85 | -------------------------------------------------------------------------------- 86 | 87 | 因为我们可以用其中一个方法来定义 cpumask,Linux 内核提供了 API 来处理 cpumask。我们来研究下其中一个函数,例如 `set_cpu_online`,这个函数有两个参数: 88 | 89 | * CPU 数目; 90 | * CPU 状态; 91 | 92 | 这个函数的实现如下所示: 93 | 94 | ```C 95 | void set_cpu_online(unsigned int cpu, bool online) 96 | { 97 | if (online) { 98 | cpumask_set_cpu(cpu, to_cpumask(cpu_online_bits)); 99 | cpumask_set_cpu(cpu, to_cpumask(cpu_active_bits)); 100 | } else { 101 | cpumask_clear_cpu(cpu, to_cpumask(cpu_online_bits)); 102 | } 103 | } 104 | ``` 105 | 106 | 该函数首先检测第二个 `state` 参数并调用依赖它的 `cpumask_set_cpu` 或 `cpumask_clear_cpu`。这里我们可以看到在中 `cpumask_set_cpu` 的第二个参数转换为 `struct cpumask *`。在我们的例子中是位图 `cpu_online_bits`,定义如下: 107 | 108 | ```C 109 | static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly; 110 | ``` 111 | 112 | 函数 `cpumask_set_cpu` 仅调用了一次 `set_bit` 函数: 113 | 114 | ```C 115 | static inline void cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp) 116 | { 117 | set_bit(cpumask_check(cpu), cpumask_bits(dstp)); 118 | } 119 | ``` 120 | 121 | `set_bit` 函数也有两个参数,设置了一个给定位(第一个参数)的内存(第二个参数或 `cpu_online_bits` 位图)。这儿我们可以看到在调用 `set_bit` 之前,它的两个参数会传递给 122 | 123 | * cpumask_check; 124 | * cpumask_bits. 125 | 126 | 让我们细看下这两个宏。第一个 `cpumask_check` 在我们的例子里没做任何事,只是返回了给的参数。第二个 `cpumask_bits` 只是返回了传入 `struct cpumask *` 结构的 `bits` 域。 127 | 128 | ```C 129 | #define cpumask_bits(maskp) ((maskp)->bits) 130 | ``` 131 | 132 | 现在让我们看下 `set_bit` 的实现: 133 | 134 | ```C 135 | static __always_inline void 136 | set_bit(long nr, volatile unsigned long *addr) 137 | { 138 | if (IS_IMMEDIATE(nr)) { 139 | asm volatile(LOCK_PREFIX "orb %1,%0" 140 | : CONST_MASK_ADDR(nr, addr) 141 | : "iq" ((u8)CONST_MASK(nr)) 142 | : "memory"); 143 | } else { 144 | asm volatile(LOCK_PREFIX "bts %1,%0" 145 | : BITOP_ADDR(addr) : "Ir" (nr) : "memory"); 146 | } 147 | } 148 | ``` 149 | 150 | 这个函数看着吓人,但它没有看起来那么难。首先传参 `nr` 或者说位数给 `IS_IMMEDIATE` 宏,该宏调用了 GCC 内联函数 `__builtin_constant_p`: 151 | 152 | ```C 153 | #define IS_IMMEDIATE(nr) (__builtin_constant_p(nr)) 154 | ``` 155 | 156 | `__builtin_constant_p` 检查给定参数是否编译时恒定变量。因为我们的 `cpu` 不是编译时恒定变量,将会执行 `else` 分支: 157 | 158 | ```C 159 | asm volatile(LOCK_PREFIX "bts %1,%0" : BITOP_ADDR(addr) : "Ir" (nr) : "memory"); 160 | ``` 161 | 162 | 让我们试着一步一步来理解它如何工作的: 163 | 164 | `LOCK_PREFIX` 是个 x86 `lock` 指令。这个指令告诉 CPU 当指令执行时占据系统总线。这允许 CPU 同步内存访问,防止多核(或多设备 - 比如 DMA 控制器)并发访问同一个内存cell。 165 | 166 | `BITOP_ADDR` 转换给定参数至 `(*(volatile long *)` 并且加了 `+m` 约束。`+` 意味着这个操作数对于指令是可读写的。`m` 显示这是一个内存操作数。`BITOP_ADDR` 定义如下: 167 | 168 | ```C 169 | #define BITOP_ADDR(x) "+m" (*(volatile long *) (x)) 170 | ``` 171 | 172 | 接下来是 `memory`。它告诉编译器汇编代码执行内存读或写到某些项,而不是那些输入或输出操作数(例如,访问指向输出参数的内存)。 173 | 174 | `Ir` - 寄存器操作数。 175 | 176 | `bts` 指令设置一个位字符串的给定位,存储给定位的值到 `CF` 标志位。所以我们传递 cpu 号,我们的例子中为 0,给 `set_bit` 并且执行后,其设置了在 `cpu_online_bits` cpumask 中的 0 位。这意味着第一个 cpu 此时上线了。 177 | 178 | 当然,除了 `set_cpu_*` API 外,cpumask 提供了其它 cpumasks 操作的 API。让我们简短看下。 179 | 180 | 附加的 cpumask API 181 | -------------------------------------------------------------------------------- 182 | 183 | cpumaks 提供了一系列宏来得到不同状态 CPUs 序号。例如: 184 | 185 | ```C 186 | #define num_online_cpus() cpumask_weight(cpu_online_mask) 187 | ``` 188 | 189 | 这个宏返回了 `online` CPUs 数量。它读取 `cpu_online_mask` 位图并调用了 `cpumask_weight` 函数。`cpumask_weight` 函数使用两个参数调用了一次 `bitmap_weight` 函数: 190 | 191 | * cpumask bitmap; 192 | * `nr_cpumask_bits` - 在我们的例子中就是 `NR_CPUS`。 193 | 194 | ```C 195 | static inline unsigned int cpumask_weight(const struct cpumask *srcp) 196 | { 197 | return bitmap_weight(cpumask_bits(srcp), nr_cpumask_bits); 198 | } 199 | ``` 200 | 201 | 并计算给定位图的位数。除了 `num_online_cpus`,cpumask还提供了所有 CPU 状态的宏: 202 | 203 | * num_possible_cpus; 204 | * num_active_cpus; 205 | * cpu_online; 206 | * cpu_possible. 207 | 208 | 等等。 209 | 210 | 除了 Linux 内核提供的下述操作 `cpumask` 的 API: 211 | 212 | * `for_each_cpu` - 遍历一个mask的所有 cpu; 213 | * `for_each_cpu_not` - 遍历所有补集的 cpu; 214 | * `cpumask_clear_cpu` - 清除一个 cpumask 的 cpu; 215 | * `cpumask_test_cpu` - 测试一个 mask 中的 cpu; 216 | * `cpumask_setall` - 设置 mask 的所有 cpu; 217 | * `cpumask_size` - 返回分配 'struct cpumask' 字节数大小; 218 | 219 | 还有很多。 220 | 221 | 链接 222 | -------------------------------------------------------------------------------- 223 | 224 | * [cpumask documentation](https://www.kernel.org/doc/Documentation/cpu-hotplug.txt) 225 | -------------------------------------------------------------------------------- /Concepts/linux-cpu-3.md: -------------------------------------------------------------------------------- 1 | initcall 机制 2 | ================================================================================ 3 | 4 | 介绍 5 | -------------------------------------------------------------------------------- 6 | 7 | 8 | 就像你从标题所理解的,这部分将涉及 Linux 内核中有趣且重要的概念,称之为 `initcall`。在 Linux 内核中,我们可以看到类似这样的定义: 9 | 10 | ```C 11 | early_param("debug", debug_kernel); 12 | ``` 13 | 14 | 或者 15 | 16 | ```C 17 | arch_initcall(init_pit_clocksource); 18 | ``` 19 | 20 | 在我们分析这个机制在内核中是如何实现的之前,我们必须了解这个机制是什么,以及在 Linux 内核中是如何使用它的。像这样的定义表示一个 [回调函数](https://en.wikipedia.org/wiki/Callback_%28computer_programming%29) ,它们会在 Linux 内核启动中或启动后调用。实际上 `initcall` 机制的要点是确定内置模块和子系统初始化的正确顺序。举个例子,我们来看看下面的函数: 21 | 22 | ```C 23 | static int __init nmi_warning_debugfs(void) 24 | { 25 | debugfs_create_u64("nmi_longest_ns", 0644, 26 | arch_debugfs_dir, &nmi_longest_ns); 27 | return 0; 28 | } 29 | ``` 30 | 31 | 这个函数出自源码文件 [arch/x86/kernel/nmi.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/nmi.c)。我们可以看到,这个函数只是在 `arch_debugfs_dir` 目录中创建 `nmi_longest_ns` [debugfs](https://en.wikipedia.org/wiki/Debugfs) 文件。实际上,只有在 `arch_debugfs_dir` 创建后,才会创建这个 `debugfs` 文件。这个目录是在 Linux 内核特定架构的初始化期间创建的。实际上,该目录将在源码文件 [arch/x86/kernel/kdebugfs.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/kdebugfs.c) 的 `arch_kdebugfs_init` 函数中创建。注意 `arch_kdebugfs_init` 函数也被标记为 `initcall`。 32 | 33 | ```C 34 | arch_initcall(arch_kdebugfs_init); 35 | ``` 36 | 37 | Linux 内核在调用 `fs` 相关的 `initcalls` 之前调用所有特定架构的 `initcalls`。因此,只有在 `arch_kdebugfs_dir` 目录创建以后才会创建我们的 `nmi_longest_ns`。实际上,Linux 内核提供了八个级别的主 `initcalls`: 38 | 39 | * `early`; 40 | * `core`; 41 | * `postcore`; 42 | * `arch`; 43 | * `susys`; 44 | * `fs`; 45 | * `device`; 46 | * `late`. 47 | 48 | 它们的所有名称是由数组 `initcall_level_names` 来描述的,该数组定义在源码文件 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 中: 49 | 50 | ```C 51 | static char *initcall_level_names[] __initdata = { 52 | "early", 53 | "core", 54 | "postcore", 55 | "arch", 56 | "subsys", 57 | "fs", 58 | "device", 59 | "late", 60 | }; 61 | ``` 62 | 63 | 所有用这些标识符标记为 `initcall` 的函数将会以相同的顺序被调用,或者说,`early initcalls` 会首先被调用,其次是 `core initcalls`,以此类推。现在,我们对 `initcall` 机制了解点了,所以我们可以开始潜入 Linux 内核源码,来看看这个机制是如何实现的。 64 | 65 | initcall 机制在 Linux 内核中的实现 66 | -------------------------------------------------------------------------------- 67 | 68 | Linux 内核提供了一组来自头文件 [include/linux/init.h](https://github.com/torvalds/linux/blob/master/include/linux/init.h) 的宏,来标记给定的函数为 `initcall`。所有这些宏都相当简单: 69 | 70 | ```C 71 | #define early_initcall(fn) __define_initcall(fn, early) 72 | #define core_initcall(fn) __define_initcall(fn, 1) 73 | #define postcore_initcall(fn) __define_initcall(fn, 2) 74 | #define arch_initcall(fn) __define_initcall(fn, 3) 75 | #define subsys_initcall(fn) __define_initcall(fn, 4) 76 | #define fs_initcall(fn) __define_initcall(fn, 5) 77 | #define device_initcall(fn) __define_initcall(fn, 6) 78 | #define late_initcall(fn) __define_initcall(fn, 7) 79 | ``` 80 | 81 | 我们可以看到,这些宏只是从同一个头文件的 `__define_initcall` 宏的调用扩展而来。此外,`__define_initcall` 宏有两个参数: 82 | 83 | * `fn` - 在调用某个级别 `initcalls` 时调用的回调函数; 84 | * `id` - 识别 `initcall` 的标识符,用来防止两个相同的 `initcalls` 指向同一个处理函数时出现错误。 85 | 86 | `__define_initcall` 宏的实现如下所示: 87 | 88 | ```C 89 | #define __define_initcall(fn, id) \ 90 | static initcall_t __initcall_##fn##id __used \ 91 | __attribute__((__section__(".initcall" #id ".init"))) = fn; \ 92 | LTO_REFERENCE_INITCALL(__initcall_##fn##id) 93 | ``` 94 | 95 | 要了解 `__define_initcall` 宏,首先让我们来看下 `initcall_t` 类型。这个类型定义在同一个 [头文件]() 中,它表示一个返回 [整形](https://en.wikipedia.org/wiki/Integer)指针的函数指针,这将是 `initcall` 的结果: 96 | 97 | ```C 98 | typedef int (*initcall_t)(void); 99 | ``` 100 | 101 | 现在让我们回到 `_-define_initcall` 宏。[##](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html) 提供了连接两个符号的能力。在我们的例子中,`__define_initcall` 宏的第一行产生了 `.initcall id .init` [ELF 部分](http://www.skyfree.org/linux/references/ELF_Format.pdf) 给定函数的定义,并标记以下 [gcc](https://en.wikipedia.org/wiki/GNU_Compiler_Collection) 属性: `__initcall_function_name_id` 和 `__used`。如果我们查看表示内核链接脚本数据的 [include/asm-generic/vmlinux.lds.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/vmlinux.lds.h) 头文件,我们会看到所有的 `initcalls` 部分都将放在 `.data` 段: 102 | 103 | ```C 104 | #define INIT_CALLS \ 105 | VMLINUX_SYMBOL(__initcall_start) = .; \ 106 | *(.initcallearly.init) \ 107 | INIT_CALLS_LEVEL(0) \ 108 | INIT_CALLS_LEVEL(1) \ 109 | INIT_CALLS_LEVEL(2) \ 110 | INIT_CALLS_LEVEL(3) \ 111 | INIT_CALLS_LEVEL(4) \ 112 | INIT_CALLS_LEVEL(5) \ 113 | INIT_CALLS_LEVEL(rootfs) \ 114 | INIT_CALLS_LEVEL(6) \ 115 | INIT_CALLS_LEVEL(7) \ 116 | VMLINUX_SYMBOL(__initcall_end) = .; 117 | 118 | #define INIT_DATA_SECTION(initsetup_align) \ 119 | .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \ 120 | ... \ 121 | INIT_CALLS \ 122 | ... \ 123 | } 124 | 125 | ``` 126 | 127 | 第二个属性 - `__used`,定义在 [include/linux/compiler-gcc.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler-gcc.h) 头文件中,它扩展了以下 `gcc` 定义: 128 | 129 | ```C 130 | #define __used __attribute__((__used__)) 131 | ``` 132 | 133 | 它防止 `定义了变量但未使用` 的告警。宏 `__define_initcall` 最后一行是: 134 | 135 | ```C 136 | LTO_REFERENCE_INITCALL(__initcall_##fn##id) 137 | ``` 138 | 139 | 这取决于 `CONFIG_LTO` 内核配置选项,只为编译器提供[链接时间优化](https://gcc.gnu.org/wiki/LinkTimeOptimization)存根: 140 | 141 | ``` 142 | #ifdef CONFIG_LTO 143 | #define LTO_REFERENCE_INITCALL(x) \ 144 | static __used __exit void *reference_##x(void) \ 145 | { \ 146 | return &x; \ 147 | } 148 | #else 149 | #define LTO_REFERENCE_INITCALL(x) 150 | #endif 151 | ``` 152 | 153 | 为了防止当模块中的变量没有引用时而产生的任何问题,它被移到了程序末尾。这就是关于 `__define_initcall` 宏的全部了。所以,所有的 `*_initcall` 宏将会在Linux内核编译时扩展,所有的 `initcalls` 会放置在它们的段内,并可以通过 `.data` 段来获取,Linux 内核在初始化过程中就知道在哪儿去找到 `initcall` 并调用它。 154 | 155 | 既然 Linux 内核可以调用 `initcalls`,我们就来看下 Linux 内核是如何做的。这个过程从 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 头文件的 `do_basic_setup` 函数开始: 156 | 157 | ```C 158 | static void __init do_basic_setup(void) 159 | { 160 | ... 161 | ... 162 | ... 163 | do_initcalls(); 164 | ... 165 | ... 166 | ... 167 | } 168 | ``` 169 | 170 | 该函数在 Linux 内核初始化过程中调用,调用时机是主要的初始化步骤,比如内存管理器相关的初始化、`CPU` 子系统等完成之后。`do_initcalls` 函数只是遍历 `initcall` 级别数组,并调用每个级别的 `do_initcall_level` 函数: 171 | 172 | ```C 173 | static void __init do_initcalls(void) 174 | { 175 | int level; 176 | 177 | for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) 178 | do_initcall_level(level); 179 | } 180 | ``` 181 | 182 | `initcall_levels` 数组在同一个源码[文件](https://github.com/torvalds/linux/blob/master/init/main.c)中定义,包含了定义在 `__define_initcall` 宏中的那些段的指针: 183 | 184 | ```C 185 | static initcall_t *initcall_levels[] __initdata = { 186 | __initcall0_start, 187 | __initcall1_start, 188 | __initcall2_start, 189 | __initcall3_start, 190 | __initcall4_start, 191 | __initcall5_start, 192 | __initcall6_start, 193 | __initcall7_start, 194 | __initcall_end, 195 | }; 196 | ``` 197 | 198 | 如果你有兴趣,你可以在 Linux 内核编译后生成的链接器脚本 `arch/x86/kernel/vmlinux.lds` 中找到这些段: 199 | 200 | ``` 201 | .init.data : AT(ADDR(.init.data) - 0xffffffff80000000) { 202 | ... 203 | ... 204 | ... 205 | ... 206 | __initcall_start = .; 207 | *(.initcallearly.init) 208 | __initcall0_start = .; 209 | *(.initcall0.init) 210 | *(.initcall0s.init) 211 | __initcall1_start = .; 212 | ... 213 | ... 214 | } 215 | ``` 216 | 217 | 如果你对这些不熟,可以在本书的某些[部分](https://0xax.gitbooks.io/linux-insides/content/Misc/linkers.html)了解更多关于[链接器](https://en.wikipedia.org/wiki/Linker_%28computing%29)的信息。 218 | 219 | 正如我们刚看到的,`do_initcall_level` 函数有一个参数 - `initcall` 的级别,做了以下两件事:首先这个函数拷贝了 `initcall_command_line`,这是通常内核包含了各个模块参数的[命令行](https://www.kernel.org/doc/Documentation/kernel-parameters.txt)的副本,并用 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c)源码文件的 `parse_args` 函数解析它,然后调用各个级别的 `do_on_initcall` 函数: 220 | 221 | ```C 222 | for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) 223 | do_one_initcall(*fn); 224 | ``` 225 | 226 | `do_on_initcall` 为我们做了主要的工作。我们可以看到,这个函数有一个参数表示 `initcall` 回调函数,并调用给定的回调函数: 227 | 228 | ```C 229 | int __init_or_module do_one_initcall(initcall_t fn) 230 | { 231 | int count = preempt_count(); 232 | int ret; 233 | char msgbuf[64]; 234 | 235 | if (initcall_blacklisted(fn)) 236 | return -EPERM; 237 | 238 | if (initcall_debug) 239 | ret = do_one_initcall_debug(fn); 240 | else 241 | ret = fn(); 242 | 243 | msgbuf[0] = 0; 244 | 245 | if (preempt_count() != count) { 246 | sprintf(msgbuf, "preemption imbalance "); 247 | preempt_count_set(count); 248 | } 249 | if (irqs_disabled()) { 250 | strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf)); 251 | local_irq_enable(); 252 | } 253 | WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf); 254 | 255 | return ret; 256 | } 257 | ``` 258 | 259 | 让我们来试着理解 `do_on_initcall` 函数做了什么。首先我们增加 [preemption](https://en.wikipedia.org/wiki/Preemption_%28computing%29) 计数,以便我们稍后进行检查,确保它不是不平衡的。这步以后,我们可以看到 `initcall_backlist` 函数的调用,这个函数遍历包含了 `initcalls` 黑名单的 `blacklisted_initcalls` 链表,如果 `initcall` 在黑名单里就释放它: 260 | 261 | ```C 262 | list_for_each_entry(entry, &blacklisted_initcalls, next) { 263 | if (!strcmp(fn_name, entry->buf)) { 264 | pr_debug("initcall %s blacklisted\n", fn_name); 265 | kfree(fn_name); 266 | return true; 267 | } 268 | } 269 | ``` 270 | 271 | 黑名单的 `initcalls` 保存在 `blacklisted_initcalls` 链表中,这个链表是在早期 Linux 内核初始化时由 Linux 内核命令行来填充的。 272 | 273 | 处理完进入黑名单的 `initcalls`,接下来的代码直接调用 `initcall`: 274 | 275 | ```C 276 | if (initcall_debug) 277 | ret = do_one_initcall_debug(fn); 278 | else 279 | ret = fn(); 280 | ``` 281 | 282 | 取决于 `initcall_debug` 变量的值,`do_one_initcall_debug` 函数将调用 `initcall`,或直接调用 `fn()`。`initcall_debug` 变量定义在[同一个源码文件](https://github.com/torvalds/linux/blob/master/init/main.c): 283 | 284 | ```C 285 | bool initcall_debug; 286 | ``` 287 | 288 | 该变量提供了向内核[日志缓冲区](https://en.wikipedia.org/wiki/Dmesg)打印一些信息的能力。可以通过 `initcall_debug` 参数从内核命令行中设置这个变量的值。从Linux内核命令行[文档](https://www.kernel.org/doc/Documentation/kernel-parameters.txt)可以看到: 289 | 290 | ``` 291 | initcall_debug [KNL] Trace initcalls as they are executed. Useful 292 | for working out where the kernel is dying during 293 | startup. 294 | ``` 295 | 296 | 确实如此。如果我们看下 `do_one_initcall_debug` 函数的实现,我们会看到它与 `do_one_initcall` 函数做了一样的事,也就是说,`do_one_initcall_debug` 函数调用了给定的 `initcall`,并打印了一些和 `initcall` 相关的信息(比如当前任务的 [pid](https://en.wikipedia.org/wiki/Process_identifier)、`initcall` 的持续时间等): 297 | 298 | ```C 299 | static int __init_or_module do_one_initcall_debug(initcall_t fn) 300 | { 301 | ktime_t calltime, delta, rettime; 302 | unsigned long long duration; 303 | int ret; 304 | 305 | printk(KERN_DEBUG "calling %pF @ %i\n", fn, task_pid_nr(current)); 306 | calltime = ktime_get(); 307 | ret = fn(); 308 | rettime = ktime_get(); 309 | delta = ktime_sub(rettime, calltime); 310 | duration = (unsigned long long) ktime_to_ns(delta) >> 10; 311 | printk(KERN_DEBUG "initcall %pF returned %d after %lld usecs\n", 312 | fn, ret, duration); 313 | 314 | return ret; 315 | } 316 | ``` 317 | 318 | 由于 `initcall` 被 `do_one_initcall` 或 `do_one_initcall_debug` 调用,我们可以看到在 `do_one_initcall` 函数末尾做了两次检查。第一个检查在initcall执行内部 `__preempt_count_add` 和 `__preempt_count_sub` 可能的执行次数,如果这个值和之前的可抢占计数不相等,我们就把 `preemption imbalance` 字符串添加到消息缓冲区,并设置正确的可抢占计数: 319 | 320 | ```C 321 | if (preempt_count() != count) { 322 | sprintf(msgbuf, "preemption imbalance "); 323 | preempt_count_set(count); 324 | } 325 | ``` 326 | 327 | 稍后这个错误字符串就会被打印出来。最后检查本地 [IRQs](https://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29) 的状态,如果它们被禁用了,我们就将 `disabled interrupts` 字符串添加到我们的消息缓冲区,并为当前处理器使能 `IRQs`,以防出现 `IRQs` 被 `initcall` 禁用了但不再使能的情况出现: 328 | 329 | ```C 330 | if (irqs_disabled()) { 331 | strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf)); 332 | local_irq_enable(); 333 | } 334 | ``` 335 | 336 | 这就是全部了。通过这种方式,Linux 内核以正确的顺序完成了很多子系统的初始化。现在我们知道 Linux 内核的 `initcall` 机制是怎么回事了。在这部分中,我们介绍了 `initcall` 机制的主要部分,但遗留了一些重要的概念。让我们来简单看下这些概念。 337 | 338 | 首先,我们错过了一个级别的 `initcalls`,就是 `rootfs initcalls`。和我们在本部分看到的很多宏类似,你可以在 [include/linux/init.h](https://github.com/torvalds/linux/blob/master/include/linux/init.h) 头文件中找到 `rootfs_initcall` 的定义: 339 | 340 | ```C 341 | #define rootfs_initcall(fn) __define_initcall(fn, rootfs) 342 | ``` 343 | 344 | 从这个宏的名字我们可以理解到,它的主要目的是保存和 [rootfs](https://en.wikipedia.org/wiki/Initramfs) 相关的回调。除此之外,只有在与设备相关的东西没被初始化时,在文件系统级别初始化以后再初始化一些其它东西时才有用。例如,发生在源码文件 [init/initramfs.c](https://github.com/torvalds/linux/blob/master/init/initramfs.c) 中 `populate_rootfs` 函数里的解压 [initramfs](https://en.wikipedia.org/wiki/Initramfs): 345 | 346 | ```C 347 | rootfs_initcall(populate_rootfs); 348 | ``` 349 | 350 | 在这里,我们可以看到熟悉的输出: 351 | 352 | ``` 353 | [ 0.199960] Unpacking initramfs... 354 | ``` 355 | 356 | 除了 `rootfs_initcall` 级别,还有其它的 `console_initcall`、 `security_initcall` 和其他辅助的 `initcall` 级别。我们遗漏的最后一件事,是 `*_initcall_sync` 级别的集合。在这部分我们看到的几乎每个 `*_initcall` 宏,都有 `_sync` 前缀的宏伴随: 357 | 358 | ```C 359 | #define core_initcall_sync(fn) __define_initcall(fn, 1s) 360 | #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) 361 | #define arch_initcall_sync(fn) __define_initcall(fn, 3s) 362 | #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) 363 | #define fs_initcall_sync(fn) __define_initcall(fn, 5s) 364 | #define device_initcall_sync(fn) __define_initcall(fn, 6s) 365 | #define late_initcall_sync(fn) __define_initcall(fn, 7s) 366 | ``` 367 | 368 | 这些附加级别的主要目的是,等待所有某个级别的与模块相关的初始化例程完成。 369 | 370 | 这就是全部了。 371 | 372 | 结论 373 | -------------------------------------------------------------------------------- 374 | 375 | 在这部分中,我们看到了 Linux 内核的一项重要机制,即在初始化期间允许调用依赖于 Linux 内核当前状态的函数。 376 | 377 | 如果你有问题或建议,可随时在 twitter [0xAX](https://twitter.com/0xAX) 上联系我,给我发 [email](anotherworldofworld@gmail.com),或者创建 [issue](https://github.com/0xAX/linux-insides/issues/new)。 378 | 379 | **请注意英语不是我的母语,对此带来的不便,我很抱歉。如果你发现了任何错误,都可以给我发 PR 到[linux-insides](https://github.com/0xAX/linux-insides)。**. 380 | 381 | 链接 382 | -------------------------------------------------------------------------------- 383 | 384 | * [callback](https://en.wikipedia.org/wiki/Callback_%28computer_programming%29) 385 | * [debugfs](https://en.wikipedia.org/wiki/Debugfs) 386 | * [integer type](https://en.wikipedia.org/wiki/Integer) 387 | * [symbols concatenation](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html) 388 | * [GCC](https://en.wikipedia.org/wiki/GNU_Compiler_Collection) 389 | * [Link time optimization](https://gcc.gnu.org/wiki/LinkTimeOptimization) 390 | * [Introduction to linkers](https://0xax.gitbooks.io/linux-insides/content/Misc/linkers.html) 391 | * [Linux kernel command line](https://www.kernel.org/doc/Documentation/kernel-parameters.txt) 392 | * [Process identifier](https://en.wikipedia.org/wiki/Process_identifier) 393 | * [IRQs](https://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29) 394 | * [rootfs](https://en.wikipedia.org/wiki/Initramfs) 395 | * [previous part](https://0xax.gitbooks.io/linux-insides/content/Concepts/cpumask.html) 396 | -------------------------------------------------------------------------------- /DataStructures/README.md: -------------------------------------------------------------------------------- 1 | Linux内核中的数据结构 2 | ======================================================================== 3 | 4 | Linux内核对很多数据结构提供不同的实现方法,比如,双向链表,B+树,具有优先级的堆等等。 5 | 6 | 这部分考虑这些数据结构和算法。 7 | 8 | * [双向链表](linux-datastructures-1.md) 9 | * [基数树](linux-datastructures-2.md) 10 | * [位数组](linux-datastructures-3.md) 11 | -------------------------------------------------------------------------------- /DataStructures/dlist.md: -------------------------------------------------------------------------------- 1 | Linux内核中的数据结构 2 | ================================================================================ 3 | 4 | 双向链表 5 | -------------------------------------------------------------------------------- 6 | 7 | Linux kernel provides its own doubly linked list implementation which you can find in the [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h). We will start `Data Structures in the Linux kernel` from the doubly linked list data structure. Why? Because it is very popular in the kernel, just try to [search](http://lxr.free-electrons.com/ident?i=list_head) 8 | 9 | First of all let's look on the main structure: 10 | 11 | ```C 12 | struct list_head { 13 | struct list_head *next, *prev; 14 | }; 15 | ``` 16 | 17 | You can note that it is different from many lists implementations which you have seen. For example this doubly linked list structure from the [glib](http://www.gnu.org/software/libc/): 18 | 19 | ```C 20 | struct GList { 21 | gpointer data; 22 | GList *next; 23 | GList *prev; 24 | }; 25 | ``` 26 | 27 | Usually a linked list structure contains a pointer to the item. Linux kernel implementation of the list does not. So the main question is - `where does the list store the data?`. The actual implementation of lists in the kernel is - `Intrusive list`. An intrusive linked list does not contain data in its nodes - A node just contains pointers to the next and previous node and list nodes part of the data that are added to the list. This makes the data structure generic, so it does not care about entry data type anymore. 28 | 29 | For example: 30 | 31 | ```C 32 | struct nmi_desc { 33 | spinlock_t lock; 34 | struct list_head head; 35 | }; 36 | ``` 37 | 38 | Let's look at some examples to understand how `list_head` is used in the kernel. As I already wrote about, there are many, really many different places where lists are used in the kernel. Let's look for example in miscellaneous character drivers. Misc character drivers API from the [drivers/char/misc.c](https://github.com/torvalds/linux/blob/master/drivers/char/misc.c) is used for writing small drivers for handling simple hardware or virtual devices. This drivers share major number: 39 | 40 | ```C 41 | #define MISC_MAJOR 10 42 | ``` 43 | 44 | but have their own minor number. For example you can see it with: 45 | 46 | ``` 47 | ls -l /dev | grep 10 48 | crw------- 1 root root 10, 235 Mar 21 12:01 autofs 49 | drwxr-xr-x 10 root root 200 Mar 21 12:01 cpu 50 | crw------- 1 root root 10, 62 Mar 21 12:01 cpu_dma_latency 51 | crw------- 1 root root 10, 203 Mar 21 12:01 cuse 52 | drwxr-xr-x 2 root root 100 Mar 21 12:01 dri 53 | crw-rw-rw- 1 root root 10, 229 Mar 21 12:01 fuse 54 | crw------- 1 root root 10, 228 Mar 21 12:01 hpet 55 | crw------- 1 root root 10, 183 Mar 21 12:01 hwrng 56 | crw-rw----+ 1 root kvm 10, 232 Mar 21 12:01 kvm 57 | crw-rw---- 1 root disk 10, 237 Mar 21 12:01 loop-control 58 | crw------- 1 root root 10, 227 Mar 21 12:01 mcelog 59 | crw------- 1 root root 10, 59 Mar 21 12:01 memory_bandwidth 60 | crw------- 1 root root 10, 61 Mar 21 12:01 network_latency 61 | crw------- 1 root root 10, 60 Mar 21 12:01 network_throughput 62 | crw-r----- 1 root kmem 10, 144 Mar 21 12:01 nvram 63 | brw-rw---- 1 root disk 1, 10 Mar 21 12:01 ram10 64 | crw--w---- 1 root tty 4, 10 Mar 21 12:01 tty10 65 | crw-rw---- 1 root dialout 4, 74 Mar 21 12:01 ttyS10 66 | crw------- 1 root root 10, 63 Mar 21 12:01 vga_arbiter 67 | crw------- 1 root root 10, 137 Mar 21 12:01 vhci 68 | ``` 69 | 70 | Now let's have a close look at how lists are used in the misc device drivers. First of all let's look on `miscdevice` structure: 71 | 72 | ```C 73 | struct miscdevice 74 | { 75 | int minor; 76 | const char *name; 77 | const struct file_operations *fops; 78 | struct list_head list; 79 | struct device *parent; 80 | struct device *this_device; 81 | const char *nodename; 82 | mode_t mode; 83 | }; 84 | ``` 85 | 86 | We can see the fourth field in the `miscdevice` structure - `list` which is a list of registered devices. In the beginning of the source code file we can see the definition of misc_list: 87 | 88 | ```C 89 | static LIST_HEAD(misc_list); 90 | ``` 91 | 92 | which expands to definition of the variables with `list_head` type: 93 | 94 | ```C 95 | #define LIST_HEAD(name) \ 96 | struct list_head name = LIST_HEAD_INIT(name) 97 | ``` 98 | 99 | and initializes it with the `LIST_HEAD_INIT` macro which set previous and next entries: 100 | 101 | ```C 102 | #define LIST_HEAD_INIT(name) { &(name), &(name) } 103 | ``` 104 | 105 | Now let's look on the `misc_register` function which registers a miscellaneous device. At the start it initializes `miscdevice->list` with the `INIT_LIST_HEAD` function: 106 | 107 | ```C 108 | INIT_LIST_HEAD(&misc->list); 109 | ``` 110 | 111 | which does the same as the `LIST_HEAD_INIT` macro: 112 | 113 | ```C 114 | static inline void INIT_LIST_HEAD(struct list_head *list) 115 | { 116 | list->next = list; 117 | list->prev = list; 118 | } 119 | ``` 120 | 121 | In the next step after device created with the `device_create` function we add it to the miscellaneous devices list with: 122 | 123 | ``` 124 | list_add(&misc->list, &misc_list); 125 | ``` 126 | 127 | Kernel `list.h` provides this API for the addition of new entry to the list. Let's look on it's implementation: 128 | 129 | ```C 130 | static inline void list_add(struct list_head *new, struct list_head *head) 131 | { 132 | __list_add(new, head, head->next); 133 | } 134 | ``` 135 | 136 | It just calls internal function `__list_add` with the 3 given parameters: 137 | 138 | * new - new entry; 139 | * head - list head after which the new item will be inserted 140 | * head->next - next item after list head. 141 | 142 | Implementation of the `__list_add` is pretty simple: 143 | 144 | ```C 145 | static inline void __list_add(struct list_head *new, 146 | struct list_head *prev, 147 | struct list_head *next) 148 | { 149 | next->prev = new; 150 | new->next = next; 151 | new->prev = prev; 152 | prev->next = new; 153 | } 154 | ``` 155 | 156 | Here we set new item between `prev` and `next`. So `misc` list which we defined at the start with the `LIST_HEAD_INIT` macro will contain previous and next pointers to the `miscdevice->list`. 157 | 158 | There is still one question: how to get list's entry. There is a special macro: 159 | 160 | ```C 161 | #define list_entry(ptr, type, member) \ 162 | container_of(ptr, type, member) 163 | ``` 164 | 165 | which gets three parameters: 166 | 167 | * ptr - the structure list_head pointer; 168 | * type - structure type; 169 | * member - the name of the list_head within the structure; 170 | 171 | For example: 172 | 173 | ```C 174 | const struct miscdevice *p = list_entry(v, struct miscdevice, list) 175 | ``` 176 | 177 | After this we can access to any `miscdevice` field with `p->minor` or `p->name` and etc... Let's look on the `list_entry` implementation: 178 | 179 | ```C 180 | #define list_entry(ptr, type, member) \ 181 | container_of(ptr, type, member) 182 | ``` 183 | 184 | As we can see it just calls `container_of` macro with the same arguments. At first sight, the `container_of` looks strange: 185 | 186 | ```C 187 | #define container_of(ptr, type, member) ({ \ 188 | const typeof( ((type *)0)->member ) *__mptr = (ptr); \ 189 | (type *)( (char *)__mptr - offsetof(type,member) );}) 190 | ``` 191 | 192 | First of all you can note that it consists of two expressions in curly brackets. Compiler will evaluate the whole block in the curly braces and use the value of the last expression. 193 | 194 | For example: 195 | 196 | ``` 197 | #include 198 | 199 | int main() { 200 | int i = 0; 201 | printf("i = %d\n", ({++i; ++i;})); 202 | return 0; 203 | } 204 | ``` 205 | 206 | will print `2`. 207 | 208 | The next point is `typeof`, it's simple. As you can understand from its name, it just returns the type of the given variable. When I first saw the implementation of the `container_of` macro, the strangest thing for me was the zero in the `((type *)0)` expression. Actually this pointer magic calculates the offset of the given field from the address of the structure, but as we have `0` here, it will be just a zero offset alongwith the field width. Let's look at a simple example: 209 | 210 | ```C 211 | #include 212 | 213 | struct s { 214 | int field1; 215 | char field2; 216 | char field3; 217 | }; 218 | 219 | int main() { 220 | printf("%p\n", &((struct s*)0)->field3); 221 | return 0; 222 | } 223 | ``` 224 | 225 | will print `0x5`. 226 | 227 | The next offsetof macro calculates offset from the beginning of the structure to the given structure's field. Its implementation is very similar to the previous code: 228 | 229 | ```C 230 | #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 231 | ``` 232 | 233 | Let's summarize all about `container_of` macro. `container_of` macro returns address of the structure by the given address of the structure's field with `list_head` type, the name of the structure field with `list_head` type and type of the container structure. At the first line this macro declares the `__mptr` pointer which points to the field of the structure that `ptr` points to and assigns `ptr` to it. Now `ptr` and `__mptr` point to the same address. Technically we don't need this line but its useful for type checking. First line ensures that that given structure (`type` parameter) has a member called `member`. In the second line it calculates offset of the field from the structure with the `offsetof` macro and subtracts it from the structure address. That's all. 234 | 235 | Of course `list_add` and `list_entry` is not the only functions which `` provides. Implementation of the doubly linked list provides the following API: 236 | 237 | * list_add 238 | * list_add_tail 239 | * list_del 240 | * list_replace 241 | * list_move 242 | * list_is_last 243 | * list_empty 244 | * list_cut_position 245 | * list_splice 246 | 247 | and many more. 248 | -------------------------------------------------------------------------------- /DataStructures/linux-datastructures-1.md: -------------------------------------------------------------------------------- 1 | Linux 内核里的数据结构——双向链表 2 | ================================================================================ 3 | 4 | 双向链表 5 | -------------------------------------------------------------------------------- 6 | 7 | Linux 内核自己实现了双向链表,可以在 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 找到定义。我们将会从双向链表数据结构开始`内核的数据结构`。为什么?因为它在内核里使用的很广泛,你只需要在 [free-electrons.com](http://lxr.free-electrons.com/ident?i=list_head) 检索一下就知道了。 8 | 9 | 首先让我们看一下在 [include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 里的主结构体: 10 | 11 | ```C 12 | struct list_head { 13 | struct list_head *next, *prev; 14 | }; 15 | ``` 16 | 17 | 你可能注意到这和你以前见过的双向链表的实现方法是不同的。举个例子来说,在 [glib](http://www.gnu.org/software/libc/) 库里是这样实现的: 18 | 19 | ```C 20 | struct GList { 21 | gpointer data; 22 | GList *next; 23 | GList *prev; 24 | }; 25 | ``` 26 | 27 | 通常来说一个链表会包含一个指向某个项目的指针。但是内核的实现并没有这样做。所以问题来了:`链表在哪里保存数据呢?`。实际上内核里实现的链表实际上是`侵入式链表`。侵入式链表并不在节点内保存数据-节点仅仅包含指向前后节点的指针,然后把数据是附加到链表的。这就使得这个数据结构是通用的,使用起来就不需要考虑节点数据的类型了。 28 | 29 | 比如: 30 | 31 | ```C 32 | struct nmi_desc { 33 | spinlock_t lock; 34 | struct list_head head; 35 | }; 36 | ``` 37 | 38 | 让我们看几个例子来理解一下在内核里是如何使用 `list_head` 的。如上所述,在内核里有实在很多不同的地方用到了链表。我们以杂项字符驱动为例来说明双向链表的使用。在 [drivers/char/misc.c](https://github.com/torvalds/linux/blob/master/drivers/char/misc.c) 的杂项字符驱动API 被用来编写处理小型硬件和虚拟设备的小驱动。这些驱动共享相同的主设备号: 39 | 40 | ```C 41 | #define MISC_MAJOR 10 42 | ``` 43 | 44 | 但是都有各自不同的次设备号。比如: 45 | 46 | ``` 47 | ls -l /dev | grep 10 48 | crw------- 1 root root 10, 235 Mar 21 12:01 autofs 49 | drwxr-xr-x 10 root root 200 Mar 21 12:01 cpu 50 | crw------- 1 root root 10, 62 Mar 21 12:01 cpu_dma_latency 51 | crw------- 1 root root 10, 203 Mar 21 12:01 cuse 52 | drwxr-xr-x 2 root root 100 Mar 21 12:01 dri 53 | crw-rw-rw- 1 root root 10, 229 Mar 21 12:01 fuse 54 | crw------- 1 root root 10, 228 Mar 21 12:01 hpet 55 | crw------- 1 root root 10, 183 Mar 21 12:01 hwrng 56 | crw-rw----+ 1 root kvm 10, 232 Mar 21 12:01 kvm 57 | crw-rw---- 1 root disk 10, 237 Mar 21 12:01 loop-control 58 | crw------- 1 root root 10, 227 Mar 21 12:01 mcelog 59 | crw------- 1 root root 10, 59 Mar 21 12:01 memory_bandwidth 60 | crw------- 1 root root 10, 61 Mar 21 12:01 network_latency 61 | crw------- 1 root root 10, 60 Mar 21 12:01 network_throughput 62 | crw-r----- 1 root kmem 10, 144 Mar 21 12:01 nvram 63 | brw-rw---- 1 root disk 1, 10 Mar 21 12:01 ram10 64 | crw--w---- 1 root tty 4, 10 Mar 21 12:01 tty10 65 | crw-rw---- 1 root dialout 4, 74 Mar 21 12:01 ttyS10 66 | crw------- 1 root root 10, 63 Mar 21 12:01 vga_arbiter 67 | crw------- 1 root root 10, 137 Mar 21 12:01 vhci 68 | ``` 69 | 70 | 现在让我们看看它是如何使用链表的。首先看一下结构体 `miscdevice` : 71 | 72 | ```C 73 | struct miscdevice 74 | { 75 | int minor; 76 | const char *name; 77 | const struct file_operations *fops; 78 | struct list_head list; 79 | struct device *parent; 80 | struct device *this_device; 81 | const char *nodename; 82 | mode_t mode; 83 | }; 84 | ``` 85 | 86 | 我们可以看到结构体的第四个变量 `list` 是所有注册过的设备的链表。在源代码文件的开始可以看到这个链表的定义: 87 | 88 | ```C 89 | static LIST_HEAD(misc_list); 90 | ``` 91 | 92 | 它扩展开来实际上就是定义了一个 `list_head` 类型的变量: 93 | 94 | ```C 95 | #define LIST_HEAD(name) \ 96 | struct list_head name = LIST_HEAD_INIT(name) 97 | ``` 98 | 99 | 然后使用宏 `LIST_HEAD_INIT` 进行初始化,这会使用变量 `name` 的地址来填充 `prev` 和 `next` 结构体的两个变量。 100 | 101 | ```C 102 | #define LIST_HEAD_INIT(name) { &(name), &(name) } 103 | ``` 104 | 105 | 现在来看看注册杂项设备的函数 `misc_register` 。它在开始就用 `INIT_LIST_HEAD` 初始化了`miscdevice->list`。 106 | 107 | ```C 108 | INIT_LIST_HEAD(&misc->list); 109 | ``` 110 | 111 | 作用和宏 `LIST_HEAD_INIT`一样。 112 | 113 | ```C 114 | static inline void INIT_LIST_HEAD(struct list_head *list) 115 | { 116 | list->next = list; 117 | list->prev = list; 118 | } 119 | ``` 120 | 121 | 下一步在函数 `device_create` 创建了设备后我们就用下面的语句将设备添加到设备链表: 122 | 123 | ``` 124 | list_add(&misc->list, &misc_list); 125 | ``` 126 | 127 | 内核文件 `list.h` 提供了向链表添加新项的接口函数。我们来看看它的实现: 128 | 129 | 130 | ```C 131 | static inline void list_add(struct list_head *new, struct list_head *head) 132 | { 133 | __list_add(new, head, head->next); 134 | } 135 | ``` 136 | 137 | 实际上就是使用3个指定的参数来调用了内部函数 `__list_add`: 138 | 139 | * new - 新项。 140 | * head - 新项将会被添加到`head` 之后. 141 | * head->next - `head` 之后的项。 142 | 143 | `__list_add`的实现非常简单: 144 | 145 | ```C 146 | static inline void __list_add(struct list_head *new, 147 | struct list_head *prev, 148 | struct list_head *next) 149 | { 150 | next->prev = new; 151 | new->next = next; 152 | new->prev = prev; 153 | prev->next = new; 154 | } 155 | ``` 156 | 157 | 我们会在 `prev` 和 `next` 之间添加一个新项。所以我们用宏 `LIST_HEAD_INIT` 定义的 `misc` 链表会包含指向 `miscdevice->list` 的向前指针和向后指针。 158 | 159 | 这里仍有一个问题:如何得到列表的内容呢?这里有一个特殊的宏: 160 | 161 | ```C 162 | #define list_entry(ptr, type, member) \ 163 | container_of(ptr, type, member) 164 | ``` 165 | 166 | 使用了三个参数: 167 | 168 | * ptr - 指向链表头的指针; 169 | * type - 结构体类型; 170 | * member - 在结构体内类型为 `list_head` 的变量的名字; 171 | 172 | 比如说: 173 | 174 | ```C 175 | const struct miscdevice *p = list_entry(v, struct miscdevice, list) 176 | ``` 177 | 178 | 然后我们就可以使用 `p->minor` 或者 `p->name`来访问 `miscdevice`。让我们来看看 `list_entry` 的实现: 179 | 180 | ```C 181 | #define list_entry(ptr, type, member) \ 182 | container_of(ptr, type, member) 183 | ``` 184 | 185 | 如我们所见,它仅仅使用相同的参数调用了宏 `container_of`。初看这个宏挺奇怪的: 186 | 187 | ```C 188 | #define container_of(ptr, type, member) ({ \ 189 | const typeof( ((type *)0)->member ) *__mptr = (ptr); \ 190 | (type *)( (char *)__mptr - offsetof(type,member) );}) 191 | ``` 192 | 193 | 首先你可以注意到花括号内包含两个表达式。编译器会执行花括号内的全部语句,然后返回最后的表达式的值。 194 | 195 | 举个例子来说: 196 | 197 | ``` 198 | #include 199 | 200 | int main() { 201 | int i = 0; 202 | printf("i = %d\n", ({++i; ++i;})); 203 | return 0; 204 | } 205 | ``` 206 | 207 | 最终会打印 `2` 208 | 209 | 下一点就是 `typeof`,它也很简单。就如你从名字所理解的,它仅仅返回了给定变量的类型。当我第一次看到宏 `container_of` 的实现时,让我觉得最奇怪的就是 `container_of` 中的 0 。实际上这个指针巧妙的计算了从结构体特定变量的偏移,这里的 `0` 刚好就是位宽里的零偏移。让我们看一个简单的例子: 210 | 211 | ```C 212 | #include 213 | 214 | struct s { 215 | int field1; 216 | char field2; 217 | char field3; 218 | }; 219 | 220 | int main() { 221 | printf("%p\n", &((struct s*)0)->field3); 222 | return 0; 223 | } 224 | ``` 225 | 226 | 结果显示 `0x5`。 227 | 228 | 下一个宏 `offsetof` 会计算从结构体的某个变量的相对于结构体起始地址的偏移。它的实现和上面类似: 229 | 230 | ```C 231 | #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 232 | ``` 233 | 234 | 现在我们来总结一下宏 `container_of`。只需要知道结构体里面类型为 `list_head` 的变量的名字和结构体容器的类型,它可以通过结构体的变量 `list_head` 获得结构体的起始地址。在宏定义的第一行,声明了一个指向结构体成员变量 `ptr` 的指针 `__mptr` ,并且把 `ptr` 的地址赋给它。现在 `ptr` 和 `__mptr` 指向了同一个地址。从技术上讲我们并不需要这一行,但是它可以方便的进行类型检查。第一行保证了特定的结构体(参数 `type`)包含成员变量 `member`。第二行代码会用宏 `offsetof` 计算成员变量相对于结构体起始地址的偏移,然后从结构体的地址减去这个偏移,最后就得到了结构体的起始地址。 235 | 236 | 当然了 `list_add` 和 `list_entry` 不是 `` 提供的唯一函数。双向链表的实现还提供了如下API: 237 | 238 | * list_add 239 | * list_add_tail 240 | * list_del 241 | * list_replace 242 | * list_move 243 | * list_is_last 244 | * list_empty 245 | * list_cut_position 246 | * list_splice 247 | * list_for_each 248 | * list_for_each_entry 249 | 250 | 等等很多其它 API。 251 | -------------------------------------------------------------------------------- /DataStructures/linux-datastructures-2.md: -------------------------------------------------------------------------------- 1 | Linux内核中的数据结构 2 | ================================================================================ 3 | 4 | 基数树 5 | -------------------------------------------------------------------------------- 6 | 正如你所知道的 Linux 内核通过许多不同库以及函数提供各种数据结构以及算法实现。 7 | 这个部分我们将介绍其中一个数据结构 [Radix tree](http://en.wikipedia.org/wiki/Radix_tree)。Linux 内核中有两个文件与 `radix tree` 的实现和API相关: 8 | 9 | * [include/linux/radix-tree.h](https://github.com/torvalds/linux/blob/master/include/linux/radix-tree.h) 10 | * [lib/radix-tree.c](https://github.com/torvalds/linux/blob/master/lib/radix-tree.c) 11 | 12 | 首先说明一下什么是 `radix tree` 。Radix tree 是一种 `压缩 trie`,其中 [trie](http://en.wikipedia.org/wiki/Trie) 是一种通过保存关联数组(associative array)来提供 `关键字-值(key-value)` 存储与查找的数据结构。通常关键字是字符串,不过也可以是其他数据类型。 13 | 14 | trie 结构的节点与 `n-tree` 不同,其节点中并不存储关键字,取而代之的是存储单个字符标签。关键字查找时,通过从树的根开始遍历关键字相关的所有字符标签节点,直至到达最终的叶子节点。下面是个例子: 15 | 16 | 17 | ``` 18 |                +-----------+ 19 |                |           | 20 |                |    " "    | 21 | | | 22 |         +------+-----------+------+ 23 |         |                         | 24 |         |                         | 25 |    +----v------+            +-----v-----+ 26 |    |           |            |           | 27 |    |    g      |            |     c     | 28 | | | | | 29 |    +-----------+            +-----------+ 30 |         |                         | 31 |         |                         | 32 |    +----v------+            +-----v-----+ 33 |    |           |            |           | 34 |    |    o      |            |     a     | 35 | | | | | 36 |    +-----------+            +-----------+ 37 |                                   | 38 |                                   | 39 |                             +-----v-----+ 40 |                             |           | 41 |                             |     t     | 42 | | | 43 |                             +-----------+ 44 | ``` 45 | 46 | 这个例子中,我们可以看到 `trie` 所存储的关键字信息 `go` 与 `cat`,压缩 trie 或 `radix tree` 与 `trie` 所不同的是,所有只存在单个孩子的中间节点将被压缩。 47 | 48 | Linux 内核中的 Radix 树将值映射为整型关键字,Radix 的数据结构定义在 [include/linux/radix-tree.h](https://github.com/torvalds/linux/blob/master/include/linux/radix-tree.h) 文件中 : 49 | 50 | ```C 51 | struct radix_tree_root { 52 | unsigned int height; 53 | gfp_t gfp_mask; 54 | struct radix_tree_node __rcu *rnode; 55 | }; 56 | ``` 57 | 58 | 上面这个是 radix 树的 root 节点的结构体,它包括三个成员: 59 | 60 | * `height` - 从叶节点向上计算出的树高度。 61 | * `gfp_mask` - 内存分配标识。 62 | * `rnode` - 子节点指针。 63 | 64 | 这里我们先讨论的结构体成员是 `gfp_mask` : 65 | 66 | Linux 底层的内存申请接口需要提供一类标识(flag) - `gfp_mask` ,用于描述内存申请的行为。这个以 `GFP_` 前缀开头的内存申请控制标识主要包括,`GFP_NOIO` 禁止所有IO操作但允许睡眠等待内存,`__GFP_HIGHMEM` 允许申请内核的高端内存,`GFP_ATOMIC` 高优先级申请内存且操作不允许被睡眠。 67 | 68 | 69 | 接下来说的结构体成员是`rnode`: 70 | 71 | ```C 72 | struct radix_tree_node { 73 | unsigned int path; 74 | unsigned int count; 75 | union { 76 | struct { 77 | struct radix_tree_node *parent; 78 | void *private_data; 79 | }; 80 | struct rcu_head rcu_head; 81 | }; 82 | /* For tree user */ 83 | struct list_head private_list; 84 | void __rcu *slots[RADIX_TREE_MAP_SIZE]; 85 | unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; 86 | }; 87 | ``` 88 | 89 | 这个结构体中包括这几个内容,节点与父节点的偏移以及到树底端的高度,子节点的个数,节点的存储数据域,具体描述如下: 90 | 91 | * `path` - 从叶节点 92 | * `count` - 子节点的个数。 93 | * `parent` - 父节点的指针。 94 | * `private_data` - 存储数据内容缓冲区。 95 | * `rcu_head` - 用于节点释放的RCU链表。 96 | * `private_list` - 存储数据。 97 | 98 | 结构体 `radix_tree_node` 的最后两个成员 `tags` 与 `slots` 是非常重要且需要特别注意的。每个 Radix 树节点都可以包括一个指向存储数据指针的 slots 集合,空闲 slots 的指针指向 NULL。 Linux 内核的 Radix 树结构体中还包含用于记录节点存储状态的标签 `tags` 成员,标签通过位设置指示 Radix 树的数据存储状态。 99 | 100 | 至此,我们了解到 radix 树的结构,接下来看一下 radix 树所提供的 API。 101 | 102 | 103 | Linux 内核基数树 API 104 | --------------------------------------------------------------------------------- 105 | 106 | 我们从数据结构的初始化开始看,radix 树支持两种方式初始化。 107 | 108 | 第一个是使用宏 `RADIX_TREE` : 109 | 110 | ```C 111 | RADIX_TREE(name, gfp_mask); 112 | ```` 113 | 114 | 正如你看到,只需要提供 `name` 参数,就能够使用 `RADIX_TREE` 宏完成 radix 的定义以及初始化,`RADIX_TREE` 宏的实现非常简单: 115 | 116 | ```C 117 | #define RADIX_TREE(name, mask) \ 118 | struct radix_tree_root name = RADIX_TREE_INIT(mask) 119 | 120 | #define RADIX_TREE_INIT(mask) { \ 121 | .height = 0, \ 122 | .gfp_mask = (mask), \ 123 | .rnode = NULL, \ 124 | } 125 | ``` 126 | 127 | `RADIX_TREE` 宏首先使用 `name` 定义了一个 `radix_tree_root` 实例并用 `RADIX_TREE_INIT` 宏带参数 `mask` 进行初始化。宏 `RADIX_TREE_INIT` 将 `radix_tree_root` 初始化为默认属性并将 gfp_mask 初始化为入参 `mask` 。 128 | 第二种方式是手工定义 `radix_tree_root` 变量,之后再使用 `mask` 调用 `INIT_RADIX_TREE` 宏对变量进行初始化。 129 | ```C 130 | struct radix_tree_root my_radix_tree; 131 | INIT_RADIX_TREE(my_tree, gfp_mask_for_my_radix_tree); 132 | ``` 133 | 134 | `INIT_RADIX_TREE` 宏定义: 135 | 136 | ```C 137 | #define INIT_RADIX_TREE(root, mask) \ 138 | do { \ 139 | (root)->height = 0; \ 140 | (root)->gfp_mask = (mask); \ 141 | (root)->rnode = NULL; \ 142 | } while (0) 143 | ``` 144 | 宏 `INIT_RADIX_TREE` 所初始化的属性与 `RADIX_TREE_INIT` 一致 145 | 146 | 147 | 接下来是 radix 树的节点插入以及删除,这两个函数: 148 | 149 | * `radix_tree_insert`; 150 | * `radix_tree_delete`. 151 | 152 | 第一个函数 `radix_tree_insert` 需要三个入参: 153 | 154 | * radix 树 root 节点结构 155 | * 索引关键字 156 | * 需要插入存储的数据 157 | 158 | 第二个函数 `radix_tree_delete` 除了不需要存储数据参数外,其他与 `radix_tree_insert` 一致。 159 | 160 | radix 树的查找实现有以下几个函数:The search in a radix tree implemented in two ways: 161 | 162 | * `radix_tree_lookup`; 163 | * `radix_tree_gang_lookup`; 164 | * `radix_tree_lookup_slot`. 165 | 166 | 第一个函数 `radix_tree_lookup` 需要两个参数: 167 | 168 | * radix 树 root 节点结构 169 | * 索引关键字 170 | 171 | 这个函数通过给定的关键字查找 radix 树,并返关键字所对应的结点。 172 | 173 | 第二个函数 `radix_tree_gang_lookup` 具有以下特征: 174 | 175 | ```C 176 | unsigned int radix_tree_gang_lookup(struct radix_tree_root *root, 177 | void **results, 178 | unsigned long first_index, 179 | unsigned int max_items); 180 | ``` 181 | 182 | 函数返回查找到记录的条目数,并根据关键字进行排序,返回的总结点数不超过入参 `max_items` 的大小。 183 | 184 | 最后一个函数 `radix_tree_lookup_slot` 返回结点 slot 中所存储的数据。 185 | 186 | 187 | 链接 188 | --------------------------------------------------------------------------------- 189 | 190 | * [Radix tree](http://en.wikipedia.org/wiki/Radix_tree) 191 | * [Trie](http://en.wikipedia.org/wiki/Trie) 192 | 193 | 194 | -------------------------------------------------------------------------------- /DataStructures/radix-tree.md: -------------------------------------------------------------------------------- 1 | Linux内核中的数据结构 2 | ================================================================================ 3 | 4 | 基数树 5 | -------------------------------------------------------------------------------- 6 | 正如你所知道的 Linux 内核通过许多不同库以及函数提供各种数据结构以及算法实现。 7 | 这个部分我们将介绍其中一个数据结构 [Radix tree](http://en.wikipedia.org/wiki/Radix_tree)。Linux 内核中有两个文件与 `radix tree` 的实现和API相关: 8 | 9 | * [include/linux/radix-tree.h](https://github.com/torvalds/linux/blob/master/include/linux/radix-tree.h) 10 | * [lib/radix-tree.c](https://github.com/torvalds/linux/blob/master/lib/radix-tree.c) 11 | 12 | 首先说明一下什么是 `radix tree` 。Radix tree 是一种 `压缩 trie`,其中 [trie](http://en.wikipedia.org/wiki/Trie) 是一种通过保存关联数组(associative array)来提供 `关键字-值(key-value)` 存储与查找的数据结构。通常关键字是字符串,不过也可以是其他数据类型。 13 | 14 | trie 结构的节点与 `n-tree` 不同,其节点中并不存储关键字,取而代之的是存储单个字符标签。关键字查找时,通过从树的根开始遍历关键字相关的所有字符标签节点,直至到达最终的叶子节点。下面是个例子: 15 | 16 | 17 | ``` 18 |                +-----------+ 19 |                |           | 20 |                |    " "    | 21 | | | 22 |         +------+-----------+------+ 23 |         |                         | 24 |         |                         | 25 |    +----v------+            +-----v-----+ 26 |    |           |            |           | 27 |    |    g      |            |     c     | 28 | | | | | 29 |    +-----------+            +-----------+ 30 |         |                         | 31 |         |                         | 32 |    +----v------+            +-----v-----+ 33 |    |           |            |           | 34 |    |    o      |            |     a     | 35 | | | | | 36 |    +-----------+            +-----------+ 37 |                                   | 38 |                                   | 39 |                             +-----v-----+ 40 |                             |           | 41 |                             |     t     | 42 | | | 43 |                             +-----------+ 44 | ``` 45 | 46 | 这个例子中,我们可以看到 `trie` 所存储的关键字信息 `go` 与 `cat`,压缩 trie 或 `radix tree` 与 `trie` 所不同的是,所有只存在单个孩子的中间节点将被压缩。 47 | 48 | Linux 内核中的 Radix 树将值映射为整型关键字,Radix 的数据结构定义在 [include/linux/radix-tree.h](https://github.com/torvalds/linux/blob/master/include/linux/radix-tree.h) 文件中 : 49 | 50 | ```C 51 | struct radix_tree_root { 52 | unsigned int height; 53 | gfp_t gfp_mask; 54 | struct radix_tree_node __rcu *rnode; 55 | }; 56 | ``` 57 | 58 | 上面这个是 radix 树的 root 节点的结构体,它包括三个成员: 59 | 60 | * `height` - 从叶节点向上计算出的树高度。 61 | * `gfp_mask` - 内存分配标识。 62 | * `rnode` - 子节点指针。 63 | 64 | 这里我们先讨论的结构体成员是 `gfp_mask` : 65 | 66 | Linux 底层的内存申请接口需要提供一类标识(flag) - `gfp_mask` ,用于描述内存申请的行为。这个以 `GFP_` 前缀开头的内存申请控制标识主要包括,`GFP_NOIO` 禁止所有IO操作但允许睡眠等待内存,`__GFP_HIGHMEM` 允许申请内核的高端内存,`GFP_ATOMIC` 高优先级申请内存且操作不允许被睡眠。 67 | 68 | 69 | 接下来说的结构体成员是`rnode`: 70 | 71 | ```C 72 | struct radix_tree_node { 73 | unsigned int path; 74 | unsigned int count; 75 | union { 76 | struct { 77 | struct radix_tree_node *parent; 78 | void *private_data; 79 | }; 80 | struct rcu_head rcu_head; 81 | }; 82 | /* For tree user */ 83 | struct list_head private_list; 84 | void __rcu *slots[RADIX_TREE_MAP_SIZE]; 85 | unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; 86 | }; 87 | ``` 88 | 89 | 这个结构体中包括这几个内容,节点与父节点的偏移以及到树底端的高度,子节点的个数,节点的存储数据域,具体描述如下: 90 | 91 | * `path` - 从叶节点 92 | * `count` - 子节点的个数。 93 | * `parent` - 父节点的指针。 94 | * `private_data` - 存储数据内容缓冲区。 95 | * `rcu_head` - 用于节点释放的RCU链表。 96 | * `private_list` - 存储数据。 97 | 98 | 结构体 `radix_tree_node` 的最后两个成员 `tags` 与 `slots` 是非常重要且需要特别注意的。每个 Radix 树节点都可以包括一个指向存储数据指针的 slots 集合,空闲 slots 的指针指向 NULL。 Linux 内核的 Radix 树结构体中还包含用于记录节点存储状态的标签 `tags` 成员,标签通过位设置指示 Radix 树的数据存储状态。 99 | 100 | 至此,我们了解到 radix 树的结构,接下来看一下 radix 树所提供的 API。 101 | 102 | 103 | Linux 内核基数树 API 104 | --------------------------------------------------------------------------------- 105 | 106 | 我们从数据结构的初始化开始看,radix 树支持两种方式初始化。 107 | 108 | 第一个是使用宏 `RADIX_TREE` : 109 | 110 | ```C 111 | RADIX_TREE(name, gfp_mask); 112 | ```` 113 | 114 | 正如你看到,只需要提供 `name` 参数,就能够使用 `RADIX_TREE` 宏完成 radix 的定义以及初始化,`RADIX_TREE` 宏的实现非常简单: 115 | 116 | ```C 117 | #define RADIX_TREE(name, mask) \ 118 | struct radix_tree_root name = RADIX_TREE_INIT(mask) 119 | 120 | #define RADIX_TREE_INIT(mask) { \ 121 | .height = 0, \ 122 | .gfp_mask = (mask), \ 123 | .rnode = NULL, \ 124 | } 125 | ``` 126 | 127 | `RADIX_TREE` 宏首先使用 `name` 定义了一个 `radix_tree_root` 实例并用 `RADIX_TREE_INIT` 宏带参数 `mask` 进行初始化。宏 `RADIX_TREE_INIT` 将 `radix_tree_root` 初始化为默认属性并将 gfp_mask 初始化为入参 `mask` 。 128 | 第二种方式是手工定义 `radix_tree_root` 变量,之后再使用 `mask` 调用 `INIT_RADIX_TREE` 宏对变量进行初始化。 129 | ```C 130 | struct radix_tree_root my_radix_tree; 131 | INIT_RADIX_TREE(my_tree, gfp_mask_for_my_radix_tree); 132 | ``` 133 | 134 | `INIT_RADIX_TREE` 宏定义: 135 | 136 | ```C 137 | #define INIT_RADIX_TREE(root, mask) \ 138 | do { \ 139 | (root)->height = 0; \ 140 | (root)->gfp_mask = (mask); \ 141 | (root)->rnode = NULL; \ 142 | } while (0) 143 | ``` 144 | 宏 `INIT_RADIX_TREE` 所初始化的属性与 `RADIX_TREE_INIT` 一致 145 | 146 | 147 | 接下来是 radix 树的节点插入以及删除,这两个函数: 148 | 149 | * `radix_tree_insert`; 150 | * `radix_tree_delete`. 151 | 152 | 第一个函数 `radix_tree_insert` 需要三个入参: 153 | 154 | * radix 树 root 节点结构 155 | * 索引关键字 156 | * 需要插入存储的数据 157 | 158 | 第二个函数 `radix_tree_delete` 除了不需要存储数据参数外,其他与 `radix_tree_insert` 一致。 159 | 160 | radix 树的查找实现有以下几个函数:The search in a radix tree implemented in two ways: 161 | 162 | * `radix_tree_lookup`; 163 | * `radix_tree_gang_lookup`; 164 | * `radix_tree_lookup_slot`. 165 | 166 | 第一个函数 `radix_tree_lookup` 需要两个参数: 167 | 168 | * radix 树 root 节点结构 169 | * 索引关键字 170 | 171 | 这个函数通过给定的关键字查找 radix 树,并返关键字所对应的结点。 172 | 173 | 第二个函数 `radix_tree_gang_lookup` 具有以下特征: 174 | 175 | ```C 176 | unsigned int radix_tree_gang_lookup(struct radix_tree_root *root, 177 | void **results, 178 | unsigned long first_index, 179 | unsigned int max_items); 180 | ``` 181 | 182 | 函数返回查找到记录的条目数,并根据关键字进行排序,返回的总结点数不超过入参 `max_items` 的大小。 183 | 184 | 最后一个函数 `radix_tree_lookup_slot` 返回结点 slot 中所存储的数据。 185 | 186 | 187 | 链接 188 | --------------------------------------------------------------------------------- 189 | 190 | * [Radix tree](http://en.wikipedia.org/wiki/Radix_tree) 191 | * [Trie](http://en.wikipedia.org/wiki/Trie) 192 | 193 | 194 | -------------------------------------------------------------------------------- /Initialization/README.md: -------------------------------------------------------------------------------- 1 | #内核初始化流程 2 | 3 | 读者在这章可以了解到整个内核初始化的完整周期,从内核解压之后的第一步到内核自身运行的第一个进程。 4 | 5 | *注意* 这里不是所有内核初始化步骤的介绍。这里只有通用的内核内容,不会涉及到中断控制、 ACPI 、以及其它部分。此处没有详述的部分,会在其它章节中描述。 6 | 7 | * [内核解压之后的首要步骤](linux-initialization-1.md) - 描述内核中的首要步骤。 8 | * [早期的中断和异常控制](linux-initialization-2.md) - 描述了早期的中断初始化和早期的缺页处理函数。 9 | * [在到达内核入口之前最后的准备](linux-initialization-3.md) - 描述了在调用 start_kernel 之前最后的准备工作。 10 | * [内核入口 - start_kernel](linux-initialization-4.md) - 描述了内核通用代码中初始化的第一步。 11 | * [体系架构初始化](linux-initialization-5.md) - 描述了特定架构的初始化。 12 | * [进一步初始化指定体系架构](linux-initialization-6.md) - 描述了再一次的指定架构初始化流程。 13 | * [最后对指定体系架构初始化](linux-initialization-7.md) - 描述了指定架构初始化流程的结尾。 14 | * [调度器初始化](linux-initialization-8.md) - 描述了调度初始化之前的准备工作,以及调度初始化。 15 | * [RCU 初始化](linux-initialization-9.md) - 描述了 RCU 的初始化。 16 | * [初始化结束](linux-initialization-10.md) - Linux内核初始化的最后部分。 17 | -------------------------------------------------------------------------------- /Initialization/linux-initialization-3.md: -------------------------------------------------------------------------------- 1 | 内核初始化 第三部分 2 | ================================================================================ 3 | 4 | 进入内核入口点之前最后的准备工作 5 | -------------------------------------------------------------------------------- 6 | 7 | 8 | 这是 Linux 内核初始化过程的第三部分。在[上一个部分](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-2.md) 中我们接触到了初期中断和异常处理,而在这个部分中我们要继续看一看 Linux 内核的初始化过程。在之后的章节我们将会关注“内核入口点”—— [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 文件中的`start_kernel` 函数。没错,从技术上说这并不是内核的入口点,只是不依赖于特定架构的通用内核代码的开始。不过,在我们调用 `start_kernel` 之前,有些准备必须要做。下面我们就来看一看。 9 | 10 | boot_params again 11 | -------------------------------------------------------------------------------- 12 | 13 | 在上一个部分中我们讲到了设置中断描述符表,并将其加载进 `IDTR` 寄存器。下一步是调用 `copy_bootdata` 函数: 14 | 15 | ```C 16 | copy_bootdata(__va(real_mode_data)); 17 | ``` 18 | 19 | 这个函数接受一个参数—— `read_mode_data` 的虚拟地址。`boot_params` 结构体是在 [arch/x86/include/uapi/asm/bootparam.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/bootparam.h#L114) 作为第一个参数传递到 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S) 中的 `x86_64_start_kernel` 函数的: 20 | 21 | ``` 22 | /* rsi is pointer to real mode structure with interesting info. 23 | pass it to C */ 24 | movq %rsi, %rdi 25 | ``` 26 | 27 | 下面我们来看一看 `__va` 宏。 这个宏定义在 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c): 28 | 29 | ```C 30 | #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) 31 | ``` 32 | 33 | 其中 `PAGE_OFFSET` 就是 `__PAGE_OFFSET`(即 `0xffff880000000000`),也是所有对物理地址进行直接映射后的虚拟基地址。因此我们就得到了 `boot_params` 结构体的虚拟地址,并把他传入 `copy_bootdata` 函数中。在这个函数里我们把 `real_mod_data` (定义在 [arch/x86/kernel/setup.h](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.h)) 拷贝进 `boot_params`: 34 | 35 | ```C 36 | extern struct boot_params boot_params; 37 | ``` 38 | 39 | `copy_boot_data` 的实现如下: 40 | 41 | ```C 42 | static void __init copy_bootdata(char *real_mode_data) 43 | { 44 | char * command_line; 45 | unsigned long cmd_line_ptr; 46 | 47 | memcpy(&boot_params, real_mode_data, sizeof boot_params); 48 | sanitize_boot_params(&boot_params); 49 | cmd_line_ptr = get_cmd_line_ptr(); 50 | if (cmd_line_ptr) { 51 | command_line = __va(cmd_line_ptr); 52 | memcpy(boot_command_line, command_line, COMMAND_LINE_SIZE); 53 | } 54 | } 55 | ``` 56 | 57 | 首先,这个函数的声明中有一个 `__init` 前缀,这表示这个函数只在初始化阶段使用,并且它所使用的内存将会被释放。 58 | 59 | 在这个函数中首先声明了两个用于解析内核命令行的变量,然后使用`memcpy` 函数将 `real_mode_data` 拷贝进 `boot_params`。如果系统引导工具(bootloader)没能正确初始化 `boot_params` 中的某些成员的话,那么在接下来调用的 `sanitize_boot_params` 函数中将会对这些成员进行清零,比如 `ext_ramdisk_image` 等。此后我们通过调用 `get_cmd_line_ptr` 函数来得到命令行的地址: 60 | 61 | ```C 62 | unsigned long cmd_line_ptr = boot_params.hdr.cmd_line_ptr; 63 | cmd_line_ptr |= (u64)boot_params.ext_cmd_line_ptr << 32; 64 | return cmd_line_ptr; 65 | ``` 66 | 67 | `get_cmd_line_ptr` 函数将会从 `boot_params` 中获得命令行的64位地址并返回。最后,我们检查一下是否正确获得了 `cmd_line_ptr`,并把它的虚拟地址拷贝到一个字节数组 `boot_command_line` 中: 68 | 69 | ```C 70 | extern char __initdata boot_command_line[]; 71 | ``` 72 | 73 | 这一步完成之后,我们就得到了内核命令行和 `boot_params` 结构体。之后,内核通过调用 `load_ucode_bsp` 函数来加载处理器微代码(microcode),不过我们目前先暂时忽略这一步。 74 | 75 | 微代码加载之后,内核会对 `console_loglevel` 进行检查,同时通过 `early_printk` 函数来打印出字符串 `Kernel Alive`。不过这个输出不会真的被显示出来,因为这个时候 `early_printk` 还没有被初始化。这是目前内核中的一个小bug,作者已经提交了补丁 [commit](http://git.kernel.org/cgit/linux/kernel/git/tip/tip.git/commit/?id=91d8f0416f3989e248d3a3d3efb821eda10a85d2),补丁很快就能应用在主分支中了。所以你可以先跳过这段代码。 76 | 77 | 初始化内存页 78 | -------------------------------------------------------------------------------- 79 | 80 | 至此,我们已经拷贝了 `boot_params` 结构体,接下来将对初期页表进行一些设置以便在初始化内核的过程中使用。我们之前已经对初始化了初期页表,以便支持换页,这在之前的[部分](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-1.html)中已经讨论过。现在则通过调用 `reset_early_page_tables` 函数将初期页表中大部分项清零(在之前的部分也有介绍),只保留内核高地址的映射。然后我们调用: 81 | 82 | ```C 83 | clear_page(init_level4_pgt); 84 | ``` 85 | 86 | `init_level4_pgt` 同样定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S): 87 | 88 | ```assembly 89 | NEXT_PAGE(init_level4_pgt) 90 | .quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE 91 | .org init_level4_pgt + L4_PAGE_OFFSET*8, 0 92 | .quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE 93 | .org init_level4_pgt + L4_START_KERNEL*8, 0 94 | .quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE 95 | ``` 96 | 97 | 这段代码为内核的代码段、数据段和 bss 段映射了前 2.5G 个字节。`clear_page` 函数定义在 [arch/x86/lib/clear_page_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/lib/clear_page_64.S): 98 | 99 | ```assembly 100 | ENTRY(clear_page) 101 | CFI_STARTPROC 102 | xorl %eax,%eax 103 | movl $4096/64,%ecx 104 | .p2align 4 105 | .Lloop: 106 | decl %ecx 107 | #define PUT(x) movq %rax,x*8(%rdi) 108 | movq %rax,(%rdi) 109 | PUT(1) 110 | PUT(2) 111 | PUT(3) 112 | PUT(4) 113 | PUT(5) 114 | PUT(6) 115 | PUT(7) 116 | leaq 64(%rdi),%rdi 117 | jnz .Lloop 118 | nop 119 | ret 120 | CFI_ENDPROC 121 | .Lclear_page_end: 122 | ENDPROC(clear_page) 123 | ``` 124 | 125 | 顾名思义,这个函数会将页表清零。这个函数的开始和结束部分有两个宏 `CFI_STARTPROC` 和 `CFI_ENDPROC`,他们会展开成 GNU 汇编指令,用于调试: 126 | 127 | ```C 128 | #define CFI_STARTPROC .cfi_startproc 129 | #define CFI_ENDPROC .cfi_endproc 130 | ``` 131 | 132 | 在 `CFI_STARTPROC` 之后我们将 `eax` 寄存器清零,并将 `ecx` 赋值为 64(用作计数器)。接下来从 `.Lloop` 标签开始循环,首先就是将 `ecx` 减一。然后将 `rax` 中的值(目前为0)写入 `rdi` 指向的地址,`rdi` 中保存的是 `init_level4_pgt` 的基地址。接下来重复7次这个步骤,但是每次都相对 `rdi` 多偏移8个字节。之后 `init_level4_pgt` 的前64个字节就都被填充为0了。接下来我们将 `rdi` 中的值加上64,重复这个步骤,直到 `ecx` 减至0。最后就完成了将 `init_level4_pgt` 填零。 133 | 134 | 在将 `init_level4_pgt` 填0之后,再把它的最后一项设置为内核高地址的映射: 135 | 136 | ```C 137 | init_level4_pgt[511] = early_level4_pgt[511]; 138 | ``` 139 | 140 | 在前面我们已经使用 `reset_early_page_table` 函数清除 `early_level4_pgt` 中的大部分项,而只保留内核高地址的映射。 141 | 142 | `x86_64_start_kernel` 函数的最后一步是调用: 143 | 144 | ```C 145 | x86_64_start_reservations(real_mode_data); 146 | ``` 147 | 148 | 并传入 `real_mode_data` 参数。 `x86_64_start_reservations` 函数与 `x86_64_start_kernel` 函数定义在同一个文件中: 149 | 150 | ```C 151 | void __init x86_64_start_reservations(char *real_mode_data) 152 | { 153 | if (!boot_params.hdr.version) 154 | copy_bootdata(__va(real_mode_data)); 155 | 156 | reserve_ebda_region(); 157 | 158 | start_kernel(); 159 | } 160 | ``` 161 | 162 | 这就是进入内核入口点之前的最后一个函数了。下面我们就来介绍一下这个函数。 163 | 164 | 内核入口点前的最后一步 165 | -------------------------------------------------------------------------------- 166 | 167 | 在 `x86_64_start_reservations` 函数中首先检查了 `boot_params.hdr.version`: 168 | 169 | ```C 170 | if (!boot_params.hdr.version) 171 | copy_bootdata(__va(real_mode_data)); 172 | ``` 173 | 174 | 如果它为0,则再次调用 `copy_bootdata`,并传入 `real_mode_data` 的虚拟地址。 175 | 176 | 接下来则调用了 `reserve_ebda_region` 函数,它定义在 [arch/x86/kernel/head.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head.c)。这个函数为 `EBDA`(即Extended BIOS Data Area,扩展BIOS数据区域)预留空间。扩展BIOS预留区域位于常规内存顶部(译注:常规内存(Conventiional Memory)是指前640K字节内存),包含了端口、磁盘参数等数据。 177 | 178 | 接下来我们来看一下 `reserve_ebda_region` 函数。它首先会检查是否启用了半虚拟化: 179 | 180 | ```C 181 | if (paravirt_enabled()) 182 | return; 183 | ``` 184 | 185 | 如果开启了半虚拟化,那么就退出 `reserve_ebda_region` 函数,因为此时没有扩展BIOS数据区域。下面我们首先得到低地址内存的末尾地址: 186 | 187 | ```C 188 | lowmem = *(unsigned short *)__va(BIOS_LOWMEM_KILOBYTES); 189 | lowmem <<= 10; 190 | ``` 191 | 192 | 首先我们得到了BIOS地地址内存的虚拟地址,以KB为单位,然后将其左移10位(即乘以1024)转换为以字节为单位。然后我们需要获得扩展BIOS数据区域的地址: 193 | 194 | ```C 195 | ebda_addr = get_bios_ebda(); 196 | ``` 197 | 198 | 其中, `get_bios_ebda` 函数定义在 [arch/x86/include/asm/bios_ebda.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/bios_ebda.h): 199 | 200 | ```C 201 | static inline unsigned int get_bios_ebda(void) 202 | { 203 | unsigned int address = *(unsigned short *)phys_to_virt(0x40E); 204 | address <<= 4; 205 | return address; 206 | } 207 | ``` 208 | 209 | 下面我们来尝试理解一下这段代码。这段代码中,首先我们将物理地址 `0x40E` 转换为虚拟地址,`0x0040:0x000e` 就是包含有扩展BIOS数据区域基地址的代码段。这里我们使用了 `phys_to_virt` 函数进行地址转换,而不是之前使用的 `__va` 宏。不过,事实上他们两个基本上是一样的: 210 | 211 | ```C 212 | static inline void *phys_to_virt(phys_addr_t address) 213 | { 214 | return __va(address); 215 | } 216 | ``` 217 | 218 | 而不同之处在于,`phys_to_virt` 函数的参数类型 `phys_addr_t` 的定义依赖于 `CONFIG_PHYS_ADDR_T_64BIT`: 219 | 220 | ```C 221 | #ifdef CONFIG_PHYS_ADDR_T_64BIT 222 | typedef u64 phys_addr_t; 223 | #else 224 | typedef u32 phys_addr_t; 225 | #endif 226 | ``` 227 | 228 | 具体的类型是由 `CONFIG_PHYS_ADDR_T_64BIT` 设置选项控制的。此后我们得到了包含扩展BIOS数据区域虚拟基地址的段,把它左移4位后返回。这样,`ebda_addr` 变量就包含了扩展BIOS数据区域的基地址。 229 | 230 | 下一步我们来检查扩展BIOS数据区域与低地址内存的地址,看一看它们是否小于 `INSANE_CUTOFF` 宏: 231 | 232 | ```C 233 | if (ebda_addr < INSANE_CUTOFF) 234 | ebda_addr = LOWMEM_CAP; 235 | 236 | if (lowmem < INSANE_CUTOFF) 237 | lowmem = LOWMEM_CAP; 238 | ``` 239 | 240 | `INSANE_CUTOFF` 为: 241 | 242 | ```C 243 | #define INSANE_CUTOFF 0x20000U 244 | ``` 245 | 246 | 即 128 KB. 上一步我们得到了低地址内存中的低地址部分以及扩展BIOS数据区域,然后调用 `memblock_reserve` 函数来在低内存地址与1MB之间为扩展BIOS数据预留内存区域。 247 | 248 | ```C 249 | lowmem = min(lowmem, ebda_addr); 250 | lowmem = min(lowmem, LOWMEM_CAP); 251 | memblock_reserve(lowmem, 0x100000 - lowmem); 252 | ``` 253 | 254 | `memblock_reserve` 函数定义在 [mm/block.c](https://github.com/torvalds/linux/blob/master/mm/block.c),它接受两个参数: 255 | 256 | * 基物理地址 257 | * 区域大小 258 | 259 | 然后在给定的基地址处预留指定大小的内存。`memblock_reserve` 是在这本书中我们接触到的第一个Linux内核内存管理框架中的函数。我们很快会详细地介绍内存管理,不过现在还是先来看一看这个函数的实现。 260 | 261 | Linux内核管理框架初探 262 | -------------------------------------------------------------------------------- 263 | 264 | 在上一段中我们遇到了对 `memblock_reserve` 函数的调用。现在我们来尝试理解一下这个函数是如何工作的。 `memblock_reserve` 函数只是调用了: 265 | 266 | ```C 267 | memblock_reserve_region(base, size, MAX_NUMNODES, 0); 268 | ``` 269 | 270 | `memblock_reserve_region` 接受四个参数: 271 | 272 | * 内存区域的物理基地址 273 | * 内存区域的大小 274 | * 最大 NUMA 节点数 275 | * 标志参数 flags 276 | 277 | 在 `memblock_reserve_region` 函数一开始,就是一个 `memblock_type` 结构体类型的变量: 278 | 279 | ```C 280 | struct memblock_type *_rgn = &memblock.reserved; 281 | ``` 282 | 283 | `memblock_type` 类型代表了一块内存,定义如下: 284 | 285 | ```C 286 | struct memblock_type { 287 | unsigned long cnt; 288 | unsigned long max; 289 | phys_addr_t total_size; 290 | struct memblock_region *regions; 291 | }; 292 | ``` 293 | 294 | 因为我们要为扩展BIOS数据区域预留内存块,所以当前内存区域的类型就是预留。`memblock` 结构体的定义为: 295 | 296 | ```C 297 | struct memblock { 298 | bool bottom_up; 299 | phys_addr_t current_limit; 300 | struct memblock_type memory; 301 | struct memblock_type reserved; 302 | #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP 303 | struct memblock_type physmem; 304 | #endif 305 | }; 306 | ``` 307 | 308 | 它描述了一块通用的数据块。我们用 `memblock.reserved` 的值来初始化 `_rgn`。`memblock` 全局变量定义如下: 309 | 310 | ```C 311 | struct memblock memblock __initdata_memblock = { 312 | .memory.regions = memblock_memory_init_regions, 313 | .memory.cnt = 1, 314 | .memory.max = INIT_MEMBLOCK_REGIONS, 315 | .reserved.regions = memblock_reserved_init_regions, 316 | .reserved.cnt = 1, 317 | .reserved.max = INIT_MEMBLOCK_REGIONS, 318 | #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP 319 | .physmem.regions = memblock_physmem_init_regions, 320 | .physmem.cnt = 1, 321 | .physmem.max = INIT_PHYSMEM_REGIONS, 322 | #endif 323 | .bottom_up = false, 324 | .current_limit = MEMBLOCK_ALLOC_ANYWHERE, 325 | }; 326 | ``` 327 | 328 | 我们现在不会继续深究这个变量,但在内存管理部分的中我们会详细地对它进行介绍。需要注意的是,这个变量的声明中使用了 `__initdata_memblock`: 329 | 330 | ```C 331 | #define __initdata_memblock __meminitdata 332 | ``` 333 | 334 | 而 `__meminit_data` 为: 335 | 336 | ```C 337 | #define __meminitdata __section(.meminit.data) 338 | ``` 339 | 340 | 自此我们得出这样的结论:所有的内存块都将定义在 `.meminit.data` 区段中。在我们定义了 `_rgn` 之后,使用了 `memblock_dbg` 宏来输出相关的信息。你可以在从内核命令行传入参数 `memblock=debug` 来开启这些输出。 341 | 342 | 在输出了这些调试信息后,是对下面这个函数的调用: 343 | 344 | ```C 345 | memblock_add_range(_rgn, base, size, nid, flags); 346 | ``` 347 | 348 | 它向 `.meminit.data` 区段添加了一个新的内存块区域。由于 `_rgn` 的值是 `&memblock.reserved`,下面的代码就直接将扩展BIOS数据区域的基地址、大小和标志填入 `_rgn` 中: 349 | 350 | ```C 351 | if (type->regions[0].size == 0) { 352 | WARN_ON(type->cnt != 1 || type->total_size); 353 | type->regions[0].base = base; 354 | type->regions[0].size = size; 355 | type->regions[0].flags = flags; 356 | memblock_set_region_node(&type->regions[0], nid); 357 | type->total_size = size; 358 | return 0; 359 | } 360 | ``` 361 | 362 | 在填充好了区域后,接着是对 `memblock_set_region_node` 函数的调用。它接受两个参数: 363 | 364 | * 填充好的内存区域的地址 365 | * NUMA节点ID 366 | 367 | 其中我们的区域由 `memblock_region` 结构体来表示: 368 | 369 | ```C 370 | struct memblock_region { 371 | phys_addr_t base; 372 | phys_addr_t size; 373 | unsigned long flags; 374 | #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP 375 | int nid; 376 | #endif 377 | }; 378 | ``` 379 | 380 | NUMA节点ID依赖于 `MAX_NUMNODES` 宏,定义在 [include/linux/numa.h](https://github.com/torvalds/linux/blob/master/include/linux/numa.h) 381 | 382 | ```C 383 | #define MAX_NUMNODES (1 << NODES_SHIFT) 384 | ``` 385 | 386 | 其中 `NODES_SHIFT` 依赖于 `CONFIG_NODES_SHIFT` 配置参数,定义如下: 387 | 388 | ```C 389 | #ifdef CONFIG_NODES_SHIFT 390 | #define NODES_SHIFT CONFIG_NODES_SHIFT 391 | #else 392 | #define NODES_SHIFT 0 393 | #endif 394 | ``` 395 | 396 | `memblick_set_region_node` 函数只是填充了 `memblock_region` 中的 `nid` 成员: 397 | 398 | ```C 399 | static inline void memblock_set_region_node(struct memblock_region *r, int nid) 400 | { 401 | r->nid = nid; 402 | } 403 | ``` 404 | 405 | 在这之后我们就在 `.meminit.data` 区段拥有了为扩展BIOS数据区域预留的第一个 `memblock`。`reserve_ebda_region` 已经完成了它该做的任务,我们回到 [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head64.c) 继续。 406 | 407 | 至此我们已经结束了进入内核之前所有的准备工作。`x86_64_start_reservations` 的最后一步是调用 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 中的: 408 | 409 | ```C 410 | start_kernel() 411 | ``` 412 | 413 | 这一部分到此结束。 414 | 415 | 小结 416 | -------------------------------------------------------------------------------- 417 | 418 | 本书的第三部分到这里就结束了。在下一部分中,我们将会见到内核入口点处的初始化工作 —— 位于 `start_kernel` 函数中。这些工作是在启动第一个进程 `init` 之前首先要完成的工作。 419 | 420 | 如果你有任何问题或建议,请在twitter上联系我 [0xAX](https://twitter.com/0xAX),或者通过[邮件](anotherworldofworld@gmail.com)与我沟通,还可以新开[issue](https://github.com/MintCN/linux-insides-zh/issues/new)。 421 | 422 | 相关链接 423 | -------------------------------------------------------------------------------- 424 | 425 | * [BIOS data area](http://stanislavs.org/helppc/bios_data_area.html) 426 | * [What is in the extended BIOS data area on a PC?](http://www.kryslix.com/nsfaq/Q.6.html) 427 | * [Previous part](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-2.md) 428 | -------------------------------------------------------------------------------- /Interrupts/README.md: -------------------------------------------------------------------------------- 1 | # 中断和中断处理 2 | 3 | 在 linux 内核中你会发现很多关于中断和异常处理的话题 4 | 5 | * [中断和中断处理第一部分](linux-interrupts-1.md) - 描述中断处理主题 6 | * [深入 Linux 内核中的中断](linux-interrupts-2.md) - 这部分开始描述和初步步骤相关的中断和异常处理。 7 | * [初步中断处理](linux-interrupts-3.md) - 描述初步中断处理。 8 | * [中断处理](linux-interrupts-4.md) - fourth part describes first non-early interrupt handlers. 9 | * [异常处理的实现](linux-interrupts-5.md) - 一些异常处理的实现,比如双重错误、除零等等。 10 | * [处理不可屏蔽中断](linux-interrupts-6.md) - 描述了如何处理不可屏蔽的中断和剩下的一些与特定架构相关的中断。 11 | * [深入外部硬件中断](linux-interrupts-7.md) - 这部分讲述了关于处理外部硬件中断的一些早期初始化代码。 12 | * [IRQs的非早期初始化](linux-interrupts-8.md) - 这部分讲述了处理外部硬件中断的非早期初始化代码。 13 | * [Softirq, Tasklets and Workqueues](linux-interrupts-9.md) - 这部分讲述了softirqs、tasklets 和 workqueues 的内容。 14 | * [最后一部分](linux-interrupts-10.md) - 这是中断和中断处理的最后一部分,并且我们将会看到一个真实的硬件驱动和中断。 15 | -------------------------------------------------------------------------------- /KernelStructures/README.md: -------------------------------------------------------------------------------- 1 | # Linux 内核内部`系统`数据结构 2 | 3 | 这不是 `linux-insides-zh` 中的一般章节。正如你从题目中理解到的,它主要描述 Linux 内核中的内部`系统`数据结构。比如说,中断描述符表 (`Interrupt Descriptor Table`), 全局描述符表 (`Global Descriptor Table`) 。 4 | 5 | 大部分信息来自于 [Intel](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html) 和 [AMD](http://developer.amd.com/resources/developer-guides-manuals/) 官方手册。 6 | -------------------------------------------------------------------------------- /KernelStructures/linux-kernelstructure-1.md: -------------------------------------------------------------------------------- 1 | 中断描述符 (IDT) 2 | ================================================================================ 3 | 4 | 三个常见的中断和异常来源: 5 | 6 | * 异常 - sync; 7 | * 软中断 - sync; 8 | * 外部中断 - async。 9 | 10 | 异常的类型: 11 | 12 | * 故障 - 在指令导致异常`之前`会被准确地报告。`%rip`保存的指针指向故障的指令; 13 | * 陷阱 - 在指令导致异常`之后`会被准确地报告。`%rip`保存的指针同样指向故障的指令; 14 | * 终止 - 是不明确的异常。 因为它们不能被明确,中止通常不允许程序可靠地再次启动。 15 | 16 | 只有当RFLAGS.IF = 1时,`可屏蔽`中断触发才中断处理程序。 除非RFLAGS.IF位清零,否则它们将持续处于等待处理状态。 17 | 18 | `不可屏蔽`中断(NMI)不受rFLAGS.IF位的影响。 无论怎样一个NMI的发生都会进一步屏蔽之后的其他NMI,直到执行IRET(中断返回)指令。 19 | 20 | 具体的异常和中断来源被分配了固定的向量标识号(也称“中断向量”或简称“向量”)。中断处理程序使用中断向量来定位异常或中断,从而分配相应的系统软件服务处理程序。有至多256个特殊的中断向量可用。前32个是保留的,用于预定义的异常和中断条件。请参考[arch / x86 / include / asm / traps.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/traps.h#L121)头文件中对他们的定义: 21 | 22 | 23 | ``` 24 | /* 中断/异常 */ 25 | enum { 26 | X86_TRAP_DE = 0, /* 0, 除零错误 */ 27 | X86_TRAP_DB, /* 1, 调试 */ 28 | X86_TRAP_NMI, /* 2, 不可屏蔽中断 */ 29 | X86_TRAP_BP, /* 3, 断点 */ 30 | X86_TRAP_OF, /* 4, 溢出 */ 31 | X86_TRAP_BR, /* 5, 超出范围 */ 32 | X86_TRAP_UD, /* 6, 操作码无效 */ 33 | X86_TRAP_NM, /* 7, 设备不可用 */ 34 | X86_TRAP_DF, /* 8, 双精度浮点错误 */ 35 | X86_TRAP_OLD_MF, /* 9, 协处理器段溢出 */ 36 | X86_TRAP_TS, /* 10, 无效的 TSS */ 37 | X86_TRAP_NP, /* 11, 段不存在 */ 38 | X86_TRAP_SS, /* 12, 堆栈段故障 */ 39 | X86_TRAP_GP, /* 13, 一般保护故障 */ 40 | X86_TRAP_PF, /* 14, 页错误 */ 41 | X86_TRAP_SPURIOUS, /* 15, 伪中断 */ 42 | X86_TRAP_MF, /* 16, x87 浮点异常 */ 43 | X86_TRAP_AC, /* 17, 对齐检查 */ 44 | X86_TRAP_MC, /* 18, 机器检测 */ 45 | X86_TRAP_XF, /* 19, SIMD (单指令多数据结构浮点)异常 */ 46 | X86_TRAP_IRET = 32, /* 32, IRET (中断返回)异常 */ 47 | }; 48 | ``` 49 | 50 | 错误代码(Error code) 51 | -------------------------------------------------------------------------------- 52 | 53 | 处理器异常处理程序使用错误代码报告某些异常的错误和状态信息。在控制权交给异常处理程序期间,异常处理装置将错误代码推送到堆栈中。错误代码有两种格式: 54 | 55 | * 多数异常错误报告格式; 56 | * 页错误格式。 57 | 58 | 选择子错误代码的格式如下: 59 | 60 | ``` 61 | 31 16 15 3 2 1 0 62 | +-------------------------------------------------------------------------------+ 63 | | | | T | I | E | 64 | | Reserved | Selector Index | - | D | X | 65 | | | | I | T | T | 66 | +-------------------------------------------------------------------------------+ 67 | ``` 68 | 69 | 说明如下: 70 | 71 | * `EXT` - 如果该位设置为1,则异常源在处理器外部。 如果设置为0,则异常源位于处理器的内部; 72 | * `IDT` - 如果该位设置为1,则错误代码选择子索引字段引用位于“中断描述符表”中的门描述符。 如果设置为0,则选择子索引字段引用“全局描述符表”或本地描述符表“LDT”中的描述符,由“TI”位所指示; 73 | * `TI` - 如果该位设置为1,则错误代码选择子索引字段引用“LDT”中的描述符。 如果清除为0,则选择子索引字段引用“GDT”中的描述符; 74 | * `Selector Index` - 选择子索引字段指定索引为“GDT‘,“LDT”或“IDT”,它是由“IDT”和“TI”位指定的。 75 | 76 | 页错误代码格式如下: 77 | 78 | ``` 79 | 31 4 3 2 1 0 80 | +-------------------------------------------------------------------------------+ 81 | | | | R | U | R | - | 82 | | Reserved | I/D | S | - | - | P | 83 | | | | V | S | W | - | 84 | +-------------------------------------------------------------------------------+ 85 | ``` 86 | 87 | 说明如下: 88 | 89 | * `I/D` - 如果该位设置为1,表示造成页错误的访问是取指; 90 | * `RSV` - 如果该位设置为1,则页错误是处理器从保留给分页表的区域中读取1的结果; 91 | * `U/S` - 如果该位被设置为0,则是管理员模式(`CPL = 0,1或2`)进行访问导致了页错误。 如果该位设置为1,则是用户模式(CPL = 3)进行访问导致了页错误; 92 | * `R/W` - 如果该位被设置为0,导致页错误的是内存读取。 如果该位设置为1,则导致页错误的是内存写入; 93 | * `P` - 如果该位被设置为0,则页错误是由不存在的页面引起的。 如果该位设置为1,页错误是由于违反页保护引起的。 94 | 95 | 中断控制传输(Interrupt Control Transfers) 96 | -------------------------------------------------------------------------------- 97 | 98 | IDT可以包含三种门描述符中的任何一种: 99 | 100 | * `Task Gate(任务门)` - 包含用于异常与或中断处理程序任务的TSS的段选择子; 101 | * `Interrupt Gate(中断门)` - 包含处理器用于将程序从执行转移到中断处理程序的段选择子和偏移量; 102 | * `Trap Gate(陷阱门)` - 包含处理器用于将程序从执行转移到异常处理程序的段选择子和偏移量。 103 | 104 | 门的一般格式是: 105 | 106 | ``` 107 | 127 96 108 | +-------------------------------------------------------------------------------+ 109 | | | 110 | | Reserved | 111 | | | 112 | +-------------------------------------------------------------------------------- 113 | 95 64 114 | +-------------------------------------------------------------------------------+ 115 | | | 116 | | Offset 63..32 | 117 | | | 118 | +-------------------------------------------------------------------------------+ 119 | 63 48 47 46 44 42 39 34 32 120 | +-------------------------------------------------------------------------------+ 121 | | | | D | | | | | | | 122 | | Offset 31..16 | P | P | 0 |Type |0 0 0 | 0 | 0 | IST | 123 | | | | L | | | | | | | 124 | -------------------------------------------------------------------------------+ 125 | 31 16 15 0 126 | +-------------------------------------------------------------------------------+ 127 | | | | 128 | | Segment Selector | Offset 15..0 | 129 | | | | 130 | +-------------------------------------------------------------------------------+ 131 | ``` 132 | 133 | 说明如下: 134 | 135 | * `Selector` - 目标代码段的段选择子; 136 | * `Offset` - 处理程序入口点的偏移量; 137 | * `DPL` - 描述符权限级别; 138 | * `P` - 当前段标志; 139 | * `IST` - 中断堆栈表; 140 | * `TYPE` - 本地描述符表(LDT)段描述符,任务状态段(TSS)描述符,调用门描述符,中断门描述符,陷阱门描述符或任务门描述符之一。 141 | 142 | `IDT` 描述符在Linux内核中由以下结构表示(仅适用于`x86_64`): 143 | 144 | ```C 145 | struct gate_struct64 { 146 | u16 offset_low; 147 | u16 segment; 148 | unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1; 149 | u16 offset_middle; 150 | u32 offset_high; 151 | u32 zero1; 152 | } __attribute__((packed)); 153 | ``` 154 | 155 | 它定义在 [arch/x86/include/asm/desc_defs.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/desc_defs.h#L51) 头文件中。 156 | 157 | 任务门描述符不包含`IST`字段,并且其格式与中断/陷阱门不同: 158 | 159 | ```C 160 | struct ldttss_desc64 { 161 | u16 limit0; 162 | u16 base0; 163 | unsigned base1 : 8, type : 5, dpl : 2, p : 1; 164 | unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8; 165 | u32 base3; 166 | u32 zero1; 167 | } __attribute__((packed)); 168 | ``` 169 | 170 | 任务切换期间的异常(Exceptions During a Task Switch) 171 | -------------------------------------------------------------------------------- 172 | 173 | 任务切换在加载段选择子期间可能会发生异常。页错误也可能会在访问TSS时出现。在这些情况下,由硬件任务切换机构完成从TSS加载新的任务状态,然后触发适当的异常处理。 174 | 175 | **在长模式下,由于硬件任务切换机构被禁用,因而在任务切换期间不会发生异常。** 176 | 177 | 不可屏蔽中断(Nonmaskable interrupt) 178 | -------------------------------------------------------------------------------- 179 | 180 | **未完待续** 181 | 182 | API 183 | -------------------------------------------------------------------------------- 184 | 185 | **未完待续** 186 | 187 | 中断堆栈表(Interrupt Stack Table) 188 | -------------------------------------------------------------------------------- 189 | 190 | **未完待续** 191 | -------------------------------------------------------------------------------- /LINKS.md: -------------------------------------------------------------------------------- 1 | 有帮助的链接 2 | ======================== 3 | 4 | Linux 启动 5 | ------------------------ 6 | 7 | * [Linux/x86 boot protocol](https://www.kernel.org/doc/Documentation/x86/boot.txt) 8 | * [Linux kernel parameters](https://github.com/torvalds/linux/blob/master/Documentation/kernel-parameters.txt) 9 | 10 | 保护模式 11 | ------------------------ 12 | 13 | * [64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html) 14 | 15 | 串口编程 16 | ------------------------ 17 | 18 | * [8250 UART Programming](http://en.wikibooks.org/wiki/Serial_Programming/8250_UART_Programming#UART_Registers) 19 | * [Serial ports on OSDEV](http://wiki.osdev.org/Serial_Ports) 20 | 21 | VGA 22 | ------------------------ 23 | 24 | * [Video Graphics Array (VGA)](http://en.wikipedia.org/wiki/Video_Graphics_Array) 25 | 26 | IO 27 | ------------------------ 28 | 29 | * [IO port programming](http://www.tldp.org/HOWTO/text/IO-Port-Programming) 30 | 31 | GCC and GAS 32 | ------------------------ 33 | 34 | * [GCC type attributes](https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html) 35 | * [Assembler Directives](http://www.chemie.fu-berlin.de/chemnet/use/info/gas/gas_toc.html#TOC65) 36 | 37 | 重要的数据结构 38 | -------------------------- 39 | 40 | * [task_struct definition](http://lxr.free-electrons.com/source/include/linux/sched.h#L1274) 41 | 42 | 其他框架 43 | ------------------------ 44 | 45 | * [PowerPC and Linux Kernel Inside](http://www.systemcomputing.org/ppc/) 46 | 47 | 有帮助的链接 48 | ------------------------ 49 | 50 | * [Linux x86 Program Start Up](http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html) 51 | * [Memory Layout in Program Execution (32 bits)](http://fgiasson.com/articles/memorylayout.txt) 52 | -------------------------------------------------------------------------------- /MM/README.md: -------------------------------------------------------------------------------- 1 | # Linux 内核内存管理 2 | 3 | 本章描述 Linux 内核中的内存管理。在本章中你会看到一系列描述 Linux 内核内存管理框架的不同部分的帖子。 4 | 5 | * [内存块](linux-mm-1.md) - 描述早期的 `memblock` 分配器。 6 | * [固定映射地址和 ioremap](linux-mm-2.md) - 描述固定映射的地址和早期的 `ioremap` 。 7 | * [kmemcheck](linux-mm-3.md) - 第三部分描述 `kmemcheck` 工具。 8 | -------------------------------------------------------------------------------- /MM/linux-mm-1.md: -------------------------------------------------------------------------------- 1 | 内核内存管理. 第一部分. 2 | ================================================================================ 3 | 4 | 简介 5 | -------------------------------------------------------------------------------- 6 | 7 | 内存管理是操作系统内核中最复杂的部分之一(我认为没有之一)。在[讲解内核进入点之前的准备工作](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-3.html)时,我们在调用 `start_kernel` 函数前停止了讲解。`start_kernel` 函数在内核启动第一个 `init` 进程前初始化了所有的内核特性(包括那些依赖于架构的特性)。你也许还记得在引导时建立了初期页表、识别页表和固定映射页表,但是复杂的内存管理部分还没有开始工作。当 `start_kernel` 函数被调用时,我们会看到从初期内存管理到更复杂的内存管理数据结构和技术的转变。为了更好地理解内核的初始化过程,我们需要对这些技术有更清晰的理解。本章节是内存管理框架和 API 的不同部分的概述,从 `memblock` 开始。 8 | 9 | 内存块 10 | -------------------------------------------------------------------------------- 11 | 12 | 内存块是在引导初期,泛用内核内存分配器还没有开始工作时对内存区域进行管理的方法之一。以前它被称为 `逻辑内存块`,但是内核接纳了 [Yinghai Lu 提供的补丁](https://lkml.org/lkml/2010/7/13/68)后改名为 `memblock` 。`x86_64` 架构上的内核会使用这个方法。我们已经在[讲解内核进入点之前的准备工作](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-3.html)时遇到过了它。现在是时候对它更加熟悉了。我们会看到它是被怎样实现的。 13 | 14 | 我们首先会学习 `memblock` 的数据结构。以下所有的数据结构都在 [include/linux/memblock.h](https://github.com/torvalds/linux/blob/master/include/linux/memblock.h) 头文件中定义。 15 | 16 | 第一个结构体的名字就叫做 `memblock`。它的定义如下: 17 | 18 | ```C 19 | struct memblock { 20 | bool bottom_up; 21 | phys_addr_t current_limit; 22 | struct memblock_type memory; --> array of memblock_region 23 | struct memblock_type reserved; --> array of memblock_region 24 | #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP 25 | struct memblock_type physmem; 26 | #endif 27 | }; 28 | ``` 29 | 30 | 这个结构体包含五个域。第一个 `bottom_up` 域置为 `true` 时允许内存以自底向上模式进行分配。下一个域是 `current_limit`。 这个域描述了内存块的尺寸限制。接下来的三个域描述了内存块的类型。内存块的类型可以是:被保留,内存和物理内存(如果 `CONFIG_HAVE_MEMBLOCK_PHYS_MAP` 编译配置选项被开启)。接下来我们来看看下一个数据结构- `memblock_type` 。让我们来看看它的定义: 31 | 32 | ```C 33 | struct memblock_type { 34 | unsigned long cnt; 35 | unsigned long max; 36 | phys_addr_t total_size; 37 | struct memblock_region *regions; 38 | }; 39 | ``` 40 | 41 | 这个结构体提供了关于内存类型的信息。它包含了描述当前内存块中内存区域的数量、所有内存区域的大小、内存区域的已分配数组的尺寸和指向 `memblock_region` 结构体数据的指针的域。`memblock_region` 结构体描述了一个内存区域,定义如下: 42 | 43 | ```C 44 | struct memblock_region { 45 | phys_addr_t base; 46 | phys_addr_t size; 47 | unsigned long flags; 48 | #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP 49 | int nid; 50 | #endif 51 | }; 52 | ``` 53 | 54 | `memblock_region` 提供了内存区域的基址和大小,`flags` 域可以是: 55 | 56 | ```C 57 | #define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) 58 | #define MEMBLOCK_ALLOC_ACCESSIBLE 0 59 | #define MEMBLOCK_HOTPLUG 0x1 60 | ``` 61 | 62 | 同时,如果 `CONFIG_HAVE_MEMBLOCK_NODE_MAP` 编译配置选项被开启, `memblock_region` 结构体也提供了整数域 - [numa](http://en.wikipedia.org/wiki/Non-uniform_memory_access) 节点选择器。 63 | 64 | 我们将以上部分想象为如下示意图: 65 | 66 | ``` 67 | +---------------------------+ +---------------------------+ 68 | | memblock | | | 69 | | _______________________ | | | 70 | | | memory | | | Array of the | 71 | | | memblock_type |-|-->| membock_region | 72 | | |_______________________| | | | 73 | | | +---------------------------+ 74 | | _______________________ | +---------------------------+ 75 | | | reserved | | | | 76 | | | memblock_type |-|-->| Array of the | 77 | | |_______________________| | | memblock_region | 78 | | | | | 79 | +---------------------------+ +---------------------------+ 80 | ``` 81 | 82 | 这三个结构体: `memblock`, `memblock_type` 和 `memblock_region` 是 `Memblock` 的主要组成部分。现在我们可以进一步了解 `Memblock` 和 它的初始化过程了。 83 | 84 | 内存块初始化 85 | -------------------------------------------------------------------------------- 86 | 87 | 所有 `memblock` 的 API 都在 [include/linux/memblock.h](https://github.com/torvalds/linux/blob/master/include/linux/memblock.h) 头文件中描述, 所有函数的实现都在 [mm/memblock.c](https://github.com/torvalds/linux/blob/master/mm/memblock.c) 源码中。首先我们来看一下源码的开头部分和 `memblock` 结构体的初始化吧。 88 | 89 | ```C 90 | struct memblock memblock __initdata_memblock = { 91 | .memory.regions = memblock_memory_init_regions, 92 | .memory.cnt = 1, 93 | .memory.max = INIT_MEMBLOCK_REGIONS, 94 | 95 | .reserved.regions = memblock_reserved_init_regions, 96 | .reserved.cnt = 1, 97 | .reserved.max = INIT_MEMBLOCK_REGIONS, 98 | 99 | #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP 100 | .physmem.regions = memblock_physmem_init_regions, 101 | .physmem.cnt = 1, 102 | .physmem.max = INIT_PHYSMEM_REGIONS, 103 | #endif 104 | .bottom_up = false, 105 | .current_limit = MEMBLOCK_ALLOC_ANYWHERE, 106 | }; 107 | ``` 108 | 109 | 在这里我们可以看到 `memblock` 结构体的同名变量的初始化。首先请注意 `__initdata_memblock` 。这个宏的定义就像这样: 110 | 111 | ```C 112 | #ifdef CONFIG_ARCH_DISCARD_MEMBLOCK 113 | #define __init_memblock __meminit 114 | #define __initdata_memblock __meminitdata 115 | #else 116 | #define __init_memblock 117 | #define __initdata_memblock 118 | #endif 119 | ``` 120 | 121 | 你会发现这个宏依赖于 `CONFIG_ARCH_DISCARD_MEMBLOCK` 。如果这个编译配置选项开启,内存块的代码会被放置在 `.init` 段,这样它就会在内核引导完毕后被释放掉。 122 | 123 | 接下来我们可以看看 `memblock_type memory` , `memblock_type reserved` 和 `memblock_type physmem` 域的初始化。在这里我们只对 `memblock_type.regions` 的初始化过程感兴趣,请注意每一个 `memblock_type` 域都是 `memblock_region` 的数组初始化的: 124 | 125 | ```C 126 | static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; 127 | static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; 128 | #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP 129 | static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock; 130 | #endif 131 | ``` 132 | 133 | 每个数组包含了 128 个内存区域。我们可以在 `INIT_MEMBLOCK_REGIONS` 宏定义中看到它: 134 | 135 | ```C 136 | #define INIT_MEMBLOCK_REGIONS 128 137 | ``` 138 | 139 | 请注意所有的数组定义中也用到了在 `memblock` 中使用过的 `__initdata_memblock` 宏(如果忘掉了就翻到上面重温一下)。 140 | 141 | 最后两个域描述了 `bottom_up` 分配是否被开启以及当前内存块的限制: 142 | 143 | ```C 144 | #define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) 145 | ``` 146 | 147 | 这个限制是 `0xffffffffffffffff`. 148 | 149 | On this step the initialization of the `memblock` structure has been finished and we can look on the Memblock API. 150 | 到此为止 `memblock` 结构体的初始化就结束了,我们可以开始看内存块相关 API 了。 151 | 152 | 内存块应用程序接口 153 | -------------------------------------------------------------------------------- 154 | 155 | 我们已经结束了 `memblock` 结构体的初始化讲解,现在我们要开始看内存块 API 和它的实现了。就像我上面说过的,所有 `memblock` 的实现都在 [mm/memblock.c](https://github.com/torvalds/linux/blob/master/mm/memblock.c) 中。为了理解 `memblock` 是怎样被实现和工作的,让我们先看看它的用法。内核中有[很多地方](http://lxr.free-electrons.com/ident?i=memblock)用到了内存块。举个例子,我们来看看 [arch/x86/kernel/e820.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/e820.c#L1061) 中的 `memblock_x86_fill` 函数。这个函数使用了 [e820](http://en.wikipedia.org/wiki/E820) 提供的内存映射并使用 `memblock_add` 函数在 `memblock` 中添加了内核保留的内存区域。既然我们首先遇到了 `memblock_add` 函数,让我们从它开始讲解吧。 156 | 157 | 这个函数获取了物理基址和内存区域的大小并把它们加到了 `memblock` 中。`memblock_add` 函数本身没有做任何特殊的事情,它只是调用了 158 | 159 | ```C 160 | memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); 161 | ``` 162 | 163 | 函数。我们将内存块类型 - `memory`,内存基址和内存区域大小,节点的最大数目和标志传进去。如果 `CONFIG_NODES_SHIFT` 没有被设置,最大节点数目就是 1,否则是 `1 << CONFIG_NODES_SHIFT`。`memblock_add_range` 函数将新的内存区域加到了内存块中,它首先检查传入内存区域的大小,如果是 0 就直接返回。然后,这个函数会用 `memblock_type` 来检查 `memblock` 中的内存区域是否存在。如果不存在,我们就简单地用给定的值填充一个新的 `memory_region` 然后返回(我们已经在[对内核内存管理框架的初览](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-3.html)中看到了它的实现)。如果 `memblock_type` 不为空,我们就会使用提供的 `memblock_type` 将新的内存区域加到 `memblock` 中。 164 | 165 | 首先,我们获取了内存区域的结束点: 166 | 167 | ```C 168 | phys_addr_t end = base + memblock_cap_size(base, &size); 169 | ``` 170 | 171 | `memblock_cap_size` 调整了 `size` 使 `base + size` 不会溢出。它的实现非常简单: 172 | 173 | ```C 174 | static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size) 175 | { 176 | return *size = min(*size, (phys_addr_t)ULLONG_MAX - base); 177 | } 178 | ``` 179 | 180 | `memblock_cap_size` 返回了提供的值与 `ULLONG_MAX - base` 中的较小值作为新的尺寸。 181 | 182 | 之后,我们获得了新的内存区域的结束地址,`memblock_add_range` 会检查与已加入内存区域是否重叠以及能否合并。将新的内存区域插入 `memblock` 包含两步: 183 | 184 | * 将新内存区域的不重叠部分作为单独的区域加入; 185 | * 合并所有相接的区域。 186 | 187 | 我们会迭代所有的已存储内存区域来检查是否与新区域重叠: 188 | 189 | ```C 190 | for (i = 0; i < type->cnt; i++) { 191 | struct memblock_region *rgn = &type->regions[i]; 192 | phys_addr_t rbase = rgn->base; 193 | phys_addr_t rend = rbase + rgn->size; 194 | 195 | if (rbase >= end) 196 | break; 197 | if (rend <= base) 198 | continue; 199 | ... 200 | ... 201 | ... 202 | } 203 | ``` 204 | 205 | 如果新的内存区域不与已有区域重叠,直接插入。否则我们会检查这个新内存区域是否合适并调用 `memblock_double_array` 函数: 206 | 207 | ```C 208 | while (type->cnt + nr_new > type->max) 209 | if (memblock_double_array(type, obase, size) < 0) 210 | return -ENOMEM; 211 | insert = true; 212 | goto repeat; 213 | ``` 214 | 215 | `memblock_double_array` 会将提供的区域数组长度加倍。然后我们会将 `insert` 置为 `true`,接着跳转到 `repeat` 标签。第二步,我们会从 `repeat` 标签开始,迭代同样的循环然后使用 `memblock_insert_region` 函数将当前内存区域插入内存块: 216 | 217 | ```C 218 | if (base < end) { 219 | nr_new++; 220 | if (insert) 221 | memblock_insert_region(type, i, base, end - base, 222 | nid, flags); 223 | } 224 | ``` 225 | 226 | 我们在第一步将 `insert` 置为 `true`,现在 `memblock_insert_region` 会检查这个标志。`memblock_insert_region` 的实现与我们将新区域插入空 `memblock_type` 的实现(看上面)几乎相同。这个函数会获取最后一个内存区域: 227 | 228 | ```C 229 | struct memblock_region *rgn = &type->regions[idx]; 230 | ``` 231 | 232 | 然后用 `memmove` 拷贝这部分内存: 233 | 234 | ```C 235 | memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); 236 | ``` 237 | 238 | 之后我们会填充 `memblock_region` 域,然后增长 `memblock_type` 的尺寸。在函数执行的结束,`memblock_add_range` 会调用 `memblock_merge_regions` 来在第二步合并相邻可合并的内存区域。 239 | 240 | 还有第二种情况,新的内存区域与已储存区域完全重叠。比如 `memblock` 中已经有了 `region1` : 241 | 242 | ``` 243 | 0 0x1000 244 | +-----------------------+ 245 | | | 246 | | | 247 | | region1 | 248 | | | 249 | | | 250 | +-----------------------+ 251 | ``` 252 | 253 | 现在我们想在 `memblock` 中添加 `region2` ,它的基址和尺寸如下: 254 | 255 | ``` 256 | 0x100 0x2000 257 | +-----------------------+ 258 | | | 259 | | | 260 | | region2 | 261 | | | 262 | | | 263 | +-----------------------+ 264 | ``` 265 | 266 | 在这种情况下,新内存区域的基址会被像下面这样设置: 267 | 268 | ```C 269 | base = min(rend, end); 270 | ``` 271 | 272 | 所以在我们设置的这种场景中,它会被设置为 `0x1000` 。然后我们会在第二步中将这个区域插入: 273 | 274 | ``` 275 | if (base < end) { 276 | nr_new++; 277 | if (insert) 278 | memblock_insert_region(type, i, base, end - base, nid, flags); 279 | } 280 | ``` 281 | 282 | 在这种情况下我们会插入 `overlapping portion` (我们之插入地址高的部分,因为低地址部分已经被包含在重叠区域里了),然后会使用 `memblock_merge_regions` 合并剩余部分区域。就像我上文中所说的那样,这个函数会合并相邻的可合并区域。它会从给定的 `memblock_type` 遍历所有的内存区域,取出两个相邻区域 - `type->regions[i]` 和 `type->regions[i + 1]`,并检查他们是否拥有同样的标志,是否属于同一个节点,第一个区域的末尾地址是否与第二个区域的基地址相同。 283 | 284 | ```C 285 | while (i < type->cnt - 1) { 286 | struct memblock_region *this = &type->regions[i]; 287 | struct memblock_region *next = &type->regions[i + 1]; 288 | if (this->base + this->size != next->base || 289 | memblock_get_region_node(this) != 290 | memblock_get_region_node(next) || 291 | this->flags != next->flags) { 292 | BUG_ON(this->base + this->size > next->base); 293 | i++; 294 | continue; 295 | } 296 | ``` 297 | 298 | 如果上面所说的这些条件全部符合,我们就会更新第一个区域的长度,将第二个区域的长度加上去。 299 | 300 | ```C 301 | this->size += next->size; 302 | ``` 303 | 304 | 我们在更新第一个区域的长度同时,会使用 `memmove` 将后面的所有区域向前移动一个下标。 305 | 306 | ```C 307 | memmove(next, next + 1, (type->cnt - (i + 2)) * sizeof(*next)); 308 | ``` 309 | 310 | 然后将 `memblock_type` 中内存区域的数量减一: 311 | 312 | ```C 313 | type->cnt--; 314 | ``` 315 | 316 | 经过这些操作后我们就成功地将两个内存区域合并了: 317 | 318 | ``` 319 | 0 0x2000 320 | +------------------------------------------------+ 321 | | | 322 | | | 323 | | region1 | 324 | | | 325 | | | 326 | +------------------------------------------------+ 327 | ``` 328 | 329 | 这就是 `memblock_add_range` 函数的工作原理和执行过程。 330 | 331 | 同样还有一个 `memblock_reserve` 函数与 `memblock_add` 几乎完成同样的工作,只有一点不同: `memblock_reserve` 将 `memblock_type.reserved` 而不是 `memblock_type.memory` 储存到内存块中。 332 | 333 | 当然这不是全部的 API。内存块不仅提供了添加 `memory` 和 `reserved` 内存区域,还提供了: 334 | 335 | * memblock_remove - 从内存块中移除内存区域; 336 | * memblock_find_in_range - 寻找给定范围内的未使用区域; 337 | * memblock_free - 释放内存块中的内存区域; 338 | * for_each_mem_range - 迭代遍历内存块区域。 339 | 340 | 等等...... 341 | 342 | 获取内存区域的相关信息 343 | -------------------------------------------------------------------------------- 344 | 345 | 内存块还提供了获取 `memblock` 中已分配内存区域信息的 API。包括两部分: 346 | 347 | * get_allocated_memblock_memory_regions_info - 获取有关内存区域的信息; 348 | * get_allocated_memblock_reserved_regions_info - 获取有关保留区域的信息。 349 | 350 | 这些函数的实现都很简单。以 `get_allocated_memblock_reserved_regions_info` 为例: 351 | 352 | ```C 353 | phys_addr_t __init_memblock get_allocated_memblock_reserved_regions_info( 354 | phys_addr_t *addr) 355 | { 356 | if (memblock.reserved.regions == memblock_reserved_init_regions) 357 | return 0; 358 | 359 | *addr = __pa(memblock.reserved.regions); 360 | 361 | return PAGE_ALIGN(sizeof(struct memblock_region) * 362 | memblock.reserved.max); 363 | } 364 | ``` 365 | 366 | 这个函数首先会检查 `memblock` 是否包含保留内存区域。如果否,就直接返回 0 。否则函数将保留内存区域的物理地址写到传入的数组中,然后返回已分配数组的对齐后尺寸。注意函数使用 `PAGE_ALIGN` 这个宏实现对齐。实际上这个宏依赖于页的尺寸: 367 | 368 | ```C 369 | #define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE) 370 | ``` 371 | 372 | `get_allocated_memblock_memory_regions_info` 函数的实现是基本一样的。只有一处不同,`get_allocated_memblock_memory_regions_info` 使用 `memblock_type.memory` 而不是 `memblock_type.reserved` 。 373 | 374 | 内存块的相关除错技术 375 | -------------------------------------------------------------------------------- 376 | 377 | 在内存块的实现中有许多对 `memblock_dbg` 的调用。如果在内核命令行中传入 `memblock=debug` 选项,这个函数就会被调用。实际上 `memblock_dbg` 是 `printk` 的一个拓展宏: 378 | 379 | ```C 380 | #define memblock_dbg(fmt, ...) \ 381 | if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) 382 | ``` 383 | 384 | 比如你可以在 `memblock_reserve` 函数中看到对这个宏的调用: 385 | 386 | ```C 387 | memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n", 388 | (unsigned long long)base, 389 | (unsigned long long)base + size - 1, 390 | flags, (void *)_RET_IP_); 391 | ``` 392 | 393 | 然后你将看到类似下图的画面: 394 | 395 | ![Memblock](http://oi57.tinypic.com/1zoj589.jpg) 396 | 397 | 内存块技术也支持 [debugfs](http://en.wikipedia.org/wiki/Debugfs) 。如果你不是在 `X86` 架构下运行内核,你可以访问: 398 | 399 | * /sys/kernel/debug/memblock/memory 400 | * /sys/kernel/debug/memblock/reserved 401 | * /sys/kernel/debug/memblock/physmem 402 | 403 | 来获取 `memblock` 内容的核心转储信息。 404 | 405 | 结束语 406 | -------------------------------------------------------------------------------- 407 | 408 | 讲解内核内存管理的第一部分到此结束,如果你有任何的问题或者建议,你可以直接发消息给我[twitter](https://twitter.com/0xAX),也可以给我发[邮件](anotherworldofworld@gmail.com)或是直接创建一个 [issue](https://github.com/MintCN/linux-insides-zh/issues/new)。 409 | 410 | **英文不是我的母语。如果你发现我的英文描述有任何问题,请提交一个PR到[linux-insides](https://github.com/MintCN/linux-insides-zh).** 411 | 412 | 相关连接: 413 | -------------------------------------------------------------------------------- 414 | 415 | * [e820](http://en.wikipedia.org/wiki/E820) 416 | * [numa](http://en.wikipedia.org/wiki/Non-uniform_memory_access) 417 | * [debugfs](http://en.wikipedia.org/wiki/Debugfs) 418 | * [对内核内存管理框架的初览](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-3.html) 419 | -------------------------------------------------------------------------------- /MM/linux-mm-3.md: -------------------------------------------------------------------------------- 1 | Linux内核内存管理 第三节 2 | ================================================================================ 3 | 4 | 内核中 kmemcheck 介绍 5 | -------------------------------------------------------------------------------- 6 | 7 | Linux内存管理[章节](https://xinqiu.gitbooks.io/linux-insides-cn/content/MM/)描述了Linux内核中[内存管理](https://en.wikipedia.org/wiki/Memory_management);本小节是第三部分。 在本章[第二节](https://xinqiu.gitbooks.io/linux-insides-cn/content/MM/linux-mm-2.html)中我们遇到了两个与内存管理相关的概念: 8 | 9 | * `固定映射地址`; 10 | * `输入输出重映射`. 11 | 12 | 固定映射地址代表[虚拟内存](https://en.wikipedia.org/wiki/Virtual_memory)中的一类特殊区域, 这类地址的物理映射地址是在[编译](https://en.wikipedia.org/wiki/Compile_time)期间计算出来的。输入输出重映射表示把输入/输出相关的内存映射到虚拟内存。 13 | 14 | 例如,查看`/proc/iomem`命令: 15 | 16 | ``` 17 | $ sudo cat /proc/iomem 18 | 19 | 00000000-00000fff : reserved 20 | 00001000-0009d7ff : System RAM 21 | 0009d800-0009ffff : reserved 22 | 000a0000-000bffff : PCI Bus 0000:00 23 | 000c0000-000cffff : Video ROM 24 | 000d0000-000d3fff : PCI Bus 0000:00 25 | 000d4000-000d7fff : PCI Bus 0000:00 26 | 000d8000-000dbfff : PCI Bus 0000:00 27 | 000dc000-000dffff : PCI Bus 0000:00 28 | 000e0000-000fffff : reserved 29 | ... 30 | ... 31 | ... 32 | ``` 33 | 34 | `iomem` 命令的输出显示了系统中每个物理设备所映射的内存区域。第一列为物理设备分配的内存区域,第二列为对应的各种不同类型的物理设备。再例如: 35 | 36 | 37 | ``` 38 | $ sudo cat /proc/ioports 39 | 40 | 0000-0cf7 : PCI Bus 0000:00 41 | 0000-001f : dma1 42 | 0020-0021 : pic1 43 | 0040-0043 : timer0 44 | 0050-0053 : timer1 45 | 0060-0060 : keyboard 46 | 0064-0064 : keyboard 47 | 0070-0077 : rtc0 48 | 0080-008f : dma page reg 49 | 00a0-00a1 : pic2 50 | 00c0-00df : dma2 51 | 00f0-00ff : fpu 52 | 00f0-00f0 : PNP0C04:00 53 | 03c0-03df : vga+ 54 | 03f8-03ff : serial 55 | 04d0-04d1 : pnp 00:06 56 | 0800-087f : pnp 00:01 57 | 0a00-0a0f : pnp 00:04 58 | 0a20-0a2f : pnp 00:04 59 | 0a30-0a3f : pnp 00:04 60 | ... 61 | ... 62 | ... 63 | ``` 64 | 65 | `ioports` 的输出列出了系统中物理设备所注册的各种类型的I/O端口。内核不能直接访问设备的输入/输出地址。在内核能够使用这些内存之前,必须将这些地址映射到虚拟地址空间,这就是`io remap`机制的主要目的。在前面[第二节](https://xinqiu.gitbooks.io/linux-insides-cn/content/MM/linux-mm-2.html)中只介绍了早期的 `io remap` 。很快我们就要来看一看常规的 `io remap` 实现机制。但在此之前,我们需要学习一些其他的知识,例如不同类型的内存分配器等,不然的话我们很难理解该机制。 66 | 67 | 在进入Linux内核常规期的[内存管理](https://en.wikipedia.org/wiki/Memory_management)之前,我们要看一些特殊的内存机制,例如[调试](https://en.wikipedia.org/wiki/Debugging),检查[内存泄漏](https://en.wikipedia.org/wiki/Memory_leak),内存控制等等。学习这些内容有助于我们理解Linux内核的内存管理。 68 | 69 | 从本节的标题中,你可能已经看出来,我们会从[kmemcheck](https://www.kernel.org/doc/Documentation/kmemcheck.txt)开始了解内存机制。和前面的[章节](https://xinqiu.gitbooks.io/linux-insides-cn/content/)一样,我们首先从理论上学习什么是 `kmemcheck` ,然后再来看Linux内核中是怎么实现这一机制的。 70 | 71 | 让我们开始吧。Linux内核中的 `kmemcheck` 到底是什么呢?从该机制的名称上你可能已经猜到, `kmemcheck` 是检查内存的。你猜的很对。`kmemcheck` 的主要目的就是用来检查是否有内核代码访问 `未初始化的内存` 。让我们看一个简单的 [C](https://en.wikipedia.org/wiki/C_%28programming_language%29) 程序: 72 | 73 | ```C 74 | #include 75 | #include 76 | 77 | struct A { 78 | int a; 79 | }; 80 | 81 | int main(int argc, char **argv) { 82 | struct A *a = malloc(sizeof(struct A)); 83 | printf("a->a = %d\n", a->a); 84 | return 0; 85 | } 86 | ``` 87 | 88 | 89 | 在上面的程序中我们给结构体`A`分配了内存,然后我们尝试打印它的成员`a`。如果我们不使用其他选项来编译该程序: 90 | 91 | ``` 92 | gcc test.c -o test 93 | ``` 94 | 95 | [编译器](https://en.wikipedia.org/wiki/GNU_Compiler_Collection)不会显示成员 `a` 未初始化的提示信息。但是如果使用工具[valgrind](https://en.wikipedia.org/wiki/Valgrind)来运行该程序,我们会看到如下输出: 96 | 97 | ``` 98 | ~$ valgrind --leak-check=yes ./test 99 | ==28469== Memcheck, a memory error detector 100 | ==28469== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. 101 | ==28469== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info 102 | ==28469== Command: ./test 103 | ==28469== 104 | ==28469== Conditional jump or move depends on uninitialised value(s) 105 | ==28469== at 0x4E820EA: vfprintf (in /usr/lib64/libc-2.22.so) 106 | ==28469== by 0x4E88D48: printf (in /usr/lib64/libc-2.22.so) 107 | ==28469== by 0x4005B9: main (in /home/alex/test) 108 | ==28469== 109 | ==28469== Use of uninitialised value of size 8 110 | ==28469== at 0x4E7E0BB: _itoa_word (in /usr/lib64/libc-2.22.so) 111 | ==28469== by 0x4E8262F: vfprintf (in /usr/lib64/libc-2.22.so) 112 | ==28469== by 0x4E88D48: printf (in /usr/lib64/libc-2.22.so) 113 | ==28469== by 0x4005B9: main (in /home/alex/test) 114 | ... 115 | ... 116 | ... 117 | ``` 118 | 119 | 实际上 `kmemcheck` 在内核空间做的事情,和 `valgrind` 在用户空间做的事情是一样的,都是用来检测未初始化的内存。 120 | 121 | 要想在内核中启用该机制,需要在配置内核时开启 `CONFIG_KMEMCHECK` 选项: 122 | 123 | ``` 124 | Kernel hacking 125 | -> Memory Debugging 126 | ``` 127 | 128 | ![kernel configuration menu](http://oi63.tinypic.com/2pzbog7.jpg) 129 | 130 | `kmemcheck` 机制还提供了一些内核配置参数,我们可以在下一个段落中看到所有的可选参数。最后一个需要注意的是,`kmemcheck` 仅在 [x86_64](https://en.wikipedia.org/wiki/X86-64) 体系中实现了。为了确信这一点,我们可以查看 `x86` 的内核配置文件 [arch/x86/Kconfig](https://github.com/torvalds/linux/blob/master/arch/x86/Kconfig): 131 | 132 | ``` 133 | config X86 134 | ... 135 | ... 136 | ... 137 | select HAVE_ARCH_KMEMCHECK 138 | ... 139 | ... 140 | ... 141 | ``` 142 | 143 | 因此,对于其他的体系结构来说是没有 `kmemcheck` 功能的。 144 | 145 | 现在我们知道了 `kmemcheck` 可以检测内核中`未初始化内存`的使用情况,也知道了如何开启这个功能。那么 `kmemcheck` 是怎么做检测的呢?当内核尝试分配内存时,例如如下一段代码: 146 | 147 | ``` 148 | struct my_struct *my_struct = kmalloc(sizeof(struct my_struct), GFP_KERNEL); 149 | ``` 150 | 151 | 或者换句话说,在内核访问 [page](https://en.wikipedia.org/wiki/Page_%28computer_memory%29) 时会发生[缺页中断](https://en.wikipedia.org/wiki/Page_fault)。这是由于 `kmemcheck` 将内存页标记为`不存在`(关于Linux内存分页的相关信息,你可以参考[分页](https://0xax.gitbooks.io/linux-insides/content/Theory/linux-theory-1.html))。如果一个`缺页中断`异常发生了,异常处理程序会来处理这个异常,如果异常处理程序检测到内核使能了 `kmemcheck`,那么就会将控制权提交给 `kmemcheck` 来处理;`kmemcheck` 检查完之后,该内存页会被标记为 `present`,然后被中断的程序得以继续执行下去。 这里的处理方式比较巧妙,被中断程序的第一条指令执行时,`kmemcheck` 又会标记内存页为 `not present`,按照这种方式,下一个对内存页的访问也会被捕获。 152 | 153 | 目前我们只是从理论层面考察了 `kmemcheck`,接下来我们看一下Linux内核是怎么来实现该机制的。 154 | 155 | `kmemcheck` 机制在Linux内核中的实现 156 | -------------------------------------------------------------------------------- 157 | 158 | 我们应该已经了解 `kmemcheck` 是做什么的以及它在Linux内核中的功能,现在是时候看一下它在Linux内核中的实现。 `kmemcheck` 在内核的实现分为两部分。第一部分是架构无关的部分,位于源码 [mm/kmemcheck.c](https://github.com/torvalds/linux/blob/master/mm/kmemcheck.c);第二部分 [x86_64](https://en.wikipedia.org/wiki/X86-64)架构相关的部分位于目录[arch/x86/mm/kmemcheck](https://github.com/torvalds/linux/tree/master/arch/x86/mm/kmemcheck)中。 159 | 160 | 我们先分析该机制的初始化过程。我们已经知道要在内核中使能 `kmemcheck` 机制,需要开启内核的`CONFIG_KMEMCHECK` 配置项。除了这个选项,我们还需要给内核command line传递一个 `kmemcheck` 参数: 161 | 162 | * kmemcheck=0 (disabled) 163 | * kmemcheck=1 (enabled) 164 | * kmemcheck=2 (one-shot mode) 165 | 166 | 前面两个值得含义很明确,但是最后一个需要解释。这个选项会使 `kmemcheck` 进入一种特殊的模式:在第一次检测到未初始化内存的使用之后,就会关闭 `kmemcheck` 。实际上该模式是内核的默认选项: 167 | 168 | ![kernel configuration menu](http://oi66.tinypic.com/y2eeh.jpg) 169 | 170 | 从Linux初始化过程章节的第七节 [part](https://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-7.html) 中,我们知道在内核初始化过程中,会在 `do_initcall_level` , `do_early_param` 等函数中解析内核 command line。前面也提到过 `kmemcheck` 子系统由两部分组成,第一部分启动比较早。在源码 [mm/kmemcheck.c](https://github.com/torvalds/linux/blob/master/mm/kmemcheck.c) 中有一个函数 `param_kmemcheck` ,该函数在command line解析时就会用到: 171 | 172 | ```C 173 | static int __init param_kmemcheck(char *str) 174 | { 175 | int val; 176 | int ret; 177 | 178 | if (!str) 179 | return -EINVAL; 180 | 181 | ret = kstrtoint(str, 0, &val); 182 | if (ret) 183 | return ret; 184 | kmemcheck_enabled = val; 185 | return 0; 186 | } 187 | 188 | early_param("kmemcheck", param_kmemcheck); 189 | ``` 190 | 191 | 从前面的介绍我们知道 `param_kmemcheck` 可能存在三种情况:`0` (使能), `1` (禁止) or `2` (一次性)。 `param_kmemcheck` 的实现很简单:将command line传递的 `kmemcheck` 参数的值由字符串转换为整数,然后赋值给变量 `kmemcheck_enabled` 。 192 | 193 | 第二阶段在内核初始化阶段执行,而不是在早期初始化过程 [initcalls](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-3.html) 。第二阶断的过程体现在 `kmemcheck_init` : 194 | 195 | ```C 196 | int __init kmemcheck_init(void) 197 | { 198 | ... 199 | ... 200 | ... 201 | } 202 | 203 | early_initcall(kmemcheck_init); 204 | ``` 205 | 206 | `kmemcheck_init` 的主要目的就是调用 `kmemcheck_selftest` 函数,并检查它的返回值: 207 | 208 | ```C 209 | if (!kmemcheck_selftest()) { 210 | printk(KERN_INFO "kmemcheck: self-tests failed; disabling\n"); 211 | kmemcheck_enabled = 0; 212 | return -EINVAL; 213 | } 214 | 215 | printk(KERN_INFO "kmemcheck: Initialized\n"); 216 | ``` 217 | 218 | 如果 `kmemcheck_init` 检测失败,就返回 `EINVAL` 。 `kmemcheck_selftest` 函数会检测内存访问相关的[操作码](https://en.wikipedia.org/wiki/Opcode)(例如 `rep movsb`, `movzwq`)的大小。如果检测到的大小的实际大小是一致的,`kmemcheck_selftest` 返回 `true`,否则返回 `false`。 219 | 220 | 如果如下代码被调用: 221 | 222 | ```C 223 | struct my_struct *my_struct = kmalloc(sizeof(struct my_struct), GFP_KERNEL); 224 | ``` 225 | 226 | 经过一系列的函数调用,`kmem_getpages` 函数会被调用到,该函数的定义在源码 [mm/slab.c](https://github.com/torvalds/linux/blob/master/mm/slab.c) 中,该函数的主要功能就是尝试按照指定的参数需求分配[内存页](https://en.wikipedia.org/wiki/Paging)。在该函数的结尾处有如下代码: 227 | 228 | ```C 229 | if (kmemcheck_enabled && !(cachep->flags & SLAB_NOTRACK)) { 230 | kmemcheck_alloc_shadow(page, cachep->gfporder, flags, nodeid); 231 | 232 | if (cachep->ctor) 233 | kmemcheck_mark_uninitialized_pages(page, nr_pages); 234 | else 235 | kmemcheck_mark_unallocated_pages(page, nr_pages); 236 | } 237 | ``` 238 | 239 | 这段代码判断如果 `kmemcheck` 使能,并且参数中未设置 `SLAB_NOTRACK` ,那么就给分配的内存页设置 `non-present` 标记。`SLAB_NOTRACK` 标记的含义是不跟踪未初始化的内存。另外,如果缓存对象有构造函数(细节在下面描述),所分配的内存页标记为未初始化,否则标记为未分配。`kmemcheck_alloc_shadow` 函数在源码 [mm/kmemcheck.c](https://github.com/torvalds/linux/blob/master/mm/kmemcheck.c) 中,其基本内容如下: 240 | 241 | ```C 242 | void kmemcheck_alloc_shadow(struct page *page, int order, gfp_t flags, int node) 243 | { 244 | struct page *shadow; 245 | 246 | shadow = alloc_pages_node(node, flags | __GFP_NOTRACK, order); 247 | 248 | for(i = 0; i < pages; ++i) 249 | page[i].shadow = page_address(&shadow[i]); 250 | 251 | kmemcheck_hide_pages(page, pages); 252 | } 253 | ``` 254 | 255 | 首先为 shadow bits 分配内存,并为内存页设置 shadow 位。如果内存页设置了该标记,就意味着 `kmemcheck` 会跟踪这个内存页。最后调用 `kmemcheck_hide_pages` 函数。 `kmemcheck_hide_pages` 是体系结构相关的函数,其代码在 [arch/x86/mm/kmemcheck/kmemcheck.c](https://github.com/torvalds/linux/tree/master/arch/x86/mm/kmemcheck/kmemcheck.c) 源码中。该函数的功能是为指定的内存页设置 `non-present` 标记。该函数实现如下: 256 | 257 | ```C 258 | void kmemcheck_hide_pages(struct page *p, unsigned int n) 259 | { 260 | unsigned int i; 261 | 262 | for (i = 0; i < n; ++i) { 263 | unsigned long address; 264 | pte_t *pte; 265 | unsigned int level; 266 | 267 | address = (unsigned long) page_address(&p[i]); 268 | pte = lookup_address(address, &level); 269 | BUG_ON(!pte); 270 | BUG_ON(level != PG_LEVEL_4K); 271 | 272 | set_pte(pte, __pte(pte_val(*pte) & ~_PAGE_PRESENT)); 273 | set_pte(pte, __pte(pte_val(*pte) | _PAGE_HIDDEN)); 274 | __flush_tlb_one(address); 275 | } 276 | } 277 | ``` 278 | 279 | 该函数遍历参数代表的所有内存页,并尝试获取每个内存页的 `页表项` 。如果获取成功,清理页表项的present 标记,设置页表项的 hidden 标记。在最后还需要刷新 [TLB](https://en.wikipedia.org/wiki/Translation_lookaside_buffer) ,因为有一些内存页已经发生了改变。从这个地方开始,内存页就进入 `kmemcheck` 的跟踪系统。由于内存页的 `present` 标记被清除了,一旦 `kmalloc` 返回了内存地址,并且有代码访问这个地址,就会触发[缺页中断](https://en.wikipedia.org/wiki/Page_fault)。 280 | 281 | 在Linux内核初始化的[第二节](https://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-2.html)介绍过,`缺页中断`处理程序是 [arch/x86/mm/fault.c](https://github.com/torvalds/linux/blob/master/arch/x86/mm/fault.c) 的 `do_page_fault` 函数。该函数开始部分如下: 282 | 283 | ```C 284 | static noinline void 285 | __do_page_fault(struct pt_regs *regs, unsigned long error_code, 286 | unsigned long address) 287 | { 288 | ... 289 | ... 290 | ... 291 | if (kmemcheck_active(regs)) 292 | kmemcheck_hide(regs); 293 | ... 294 | ... 295 | ... 296 | } 297 | ``` 298 | 299 | `kmemcheck_active` 函数获取 `kmemcheck_context` [per-cpu](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-1.html) 结构体,并返回该结构体成员 `balance` 和0的比较结果: 300 | 301 | ``` 302 | bool kmemcheck_active(struct pt_regs *regs) 303 | { 304 | struct kmemcheck_context *data = this_cpu_ptr(&kmemcheck_context); 305 | 306 | return data->balance > 0; 307 | } 308 | ``` 309 | 310 | `kmemcheck_context` 结构体代表 `kmemcheck` 机制的当前状态。其内部保存了未初始化的地址,地址的数量等信息。其成员 `balance` 代表了 `kmemcheck` 的当前状态,换句话说,`balance` 表示 `kmemcheck` 是否已经隐藏了内存页。如果 `data->balance` 大于0, `kmemcheck_hide` 函数会被调用。这意味着 `kmemecheck` 已经设置了内存页的 `present` 标记,但是我们需要再次隐藏内存页以便触发下一次的缺页中断。 `kmemcheck_hide` 函数会清理内存页的 `present` 标记,这表示一次 `kmemcheck` 会话已经完成,新的缺页中断会再次被触发。在第一步,由于 `data->balance` 值为0,所以 `kmemcheck_active` 会返回false,所以 `kmemcheck_hide` 也不会被调用。接下来,我们看 `do_page_fault` 的下一行代码: 311 | 312 | ```C 313 | if (kmemcheck_fault(regs, address, error_code)) 314 | return; 315 | ``` 316 | 317 | 首先 `kmemcheck_fault` 函数检查引起错误的真实原因。第一步先检查[标记寄存器](https://en.wikipedia.org/wiki/FLAGS_register)以确认进程是否处于正常的内核态: 318 | 319 | ```C 320 | if (regs->flags & X86_VM_MASK) 321 | return false; 322 | if (regs->cs != __KERNEL_CS) 323 | return false; 324 | ``` 325 | 326 | 如果检测失败,表明这不是 `kmemcheck` 相关的缺页中断,`kmemcheck_fault` 会返回false。如果检测成功,接下来查找发生异常的地址的`页表项`,如果找不到页表项,函数返回false: 327 | 328 | ```C 329 | pte = kmemcheck_pte_lookup(address); 330 | if (!pte) 331 | return false; 332 | ``` 333 | 334 | `kmemcheck_fault` 最后一步是调用 `kmemcheck_access` 函数,该函数检查对指定内存页的访问,并设置该内存页的present标记。 `kmemcheck_access` 函数做了大部分工作,它检查引起缺页异常的当前指令,如果检查到了错误,那么会把该错误的上下文保存到环形队列中: 335 | 336 | ```C 337 | static struct kmemcheck_error error_fifo[CONFIG_KMEMCHECK_QUEUE_SIZE]; 338 | ``` 339 | 340 | `kmemcheck` 声明了一个特殊的 [tasklet](https://xinqiu.gitbooks.io/linux-insides-cn/content/Interrupts/linux-interrupts-9.html) : 341 | 342 | ```C 343 | static DECLARE_TASKLET(kmemcheck_tasklet, &do_wakeup, 0); 344 | ``` 345 | 346 | 该tasklet被调度执行时,会调用 `do_wakeup` 函数,该函数位于 [arch/x86/mm/kmemcheck/error.c](https://github.com/torvalds/linux/blob/master/arch/x86/mm/kmemcheck/error.c) 文件中。 347 | 348 | `do_wakeup` 函数调用 `kmemcheck_error_recall` 函数以便将 `kmemcheck` 检测到的错误信息输出。 349 | 350 | ```C 351 | kmemcheck_show(regs); 352 | ``` 353 | 354 | `kmemcheck_fault` 函数结束时会调用 `kmemcheck_show` 函数,该函数会再次设置内存页的present标记。 355 | 356 | ```C 357 | if (unlikely(data->balance != 0)) { 358 | kmemcheck_show_all(); 359 | kmemcheck_error_save_bug(regs); 360 | data->balance = 0; 361 | return; 362 | } 363 | ``` 364 | 365 | `kmemcheck_show_all` 函数会针对每个地址调用 `kmemcheck_show_addr` : 366 | 367 | ```C 368 | static unsigned int kmemcheck_show_all(void) 369 | { 370 | struct kmemcheck_context *data = this_cpu_ptr(&kmemcheck_context); 371 | unsigned int i; 372 | unsigned int n; 373 | 374 | n = 0; 375 | for (i = 0; i < data->n_addrs; ++i) 376 | n += kmemcheck_show_addr(data->addr[i]); 377 | 378 | return n; 379 | } 380 | ``` 381 | 382 | `kmemcheck_show_addr` 函数内容如下: 383 | 384 | ```C 385 | int kmemcheck_show_addr(unsigned long address) 386 | { 387 | pte_t *pte; 388 | 389 | pte = kmemcheck_pte_lookup(address); 390 | if (!pte) 391 | return 0; 392 | 393 | set_pte(pte, __pte(pte_val(*pte) | _PAGE_PRESENT)); 394 | __flush_tlb_one(address); 395 | return 1; 396 | } 397 | ``` 398 | 399 | 在函数 `kmemcheck_show` 的结尾处会设置 [TF](https://en.wikipedia.org/wiki/Trap_flag) 标记: 400 | 401 | ```C 402 | if (!(regs->flags & X86_EFLAGS_TF)) 403 | data->flags = regs->flags; 404 | ``` 405 | 406 | 我们之所以这么处理,是因为我们在内存页的缺页中断处理完后需要再次隐藏内存页。当 `TF` 标记被设置后,处理器在执行被中断程序的第一条指令时会进入单步模式,这会触发 `debug` 异常。从这个地方开始,内存页会被隐藏起来,执行流程继续。由于内存页不可见,那么访问内存页的时候又会触发缺页中断,然后`kmemcheck` 就有机会继续检测/收集并显示内存错误信息。 407 | 408 | 到这里 `kmemcheck` 的工作机制就介绍完毕了。 409 | 410 | 结束语 411 | -------------------------------------------------------------------------------- 412 | 413 | Linux内核[内存管理](https://en.wikipedia.org/wiki/Memory_management)第三节介绍到此为止。如果你有任何疑问或者建议,你可以直接给我[0xAX](https://twitter.com/0xAX)发消息, 发[邮件](anotherworldofworld@gmail.com),或者创建一个 [issue](https://github.com/0xAX/linux-insides/issues/new) 。 在接下来的小节中,我们来看一下另一个内存调试工具 - `kmemleak` 。 414 | 415 | **英文不是我的母语。如果你发现我的英文描述有任何问题,请提交一个PR到 [linux-insides](https://github.com/0xAX/linux-insides).** 416 | 417 | Links 418 | -------------------------------------------------------------------------------- 419 | 420 | * [memory management](https://en.wikipedia.org/wiki/Memory_management) 421 | * [debugging](https://en.wikipedia.org/wiki/Debugging) 422 | * [memory leaks](https://en.wikipedia.org/wiki/Memory_leak) 423 | * [kmemcheck documentation](https://www.kernel.org/doc/Documentation/kmemcheck.txt) 424 | * [valgrind](https://en.wikipedia.org/wiki/Valgrind) 425 | * [page fault](https://en.wikipedia.org/wiki/Page_fault) 426 | * [initcalls](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-3.html) 427 | * [opcode](https://en.wikipedia.org/wiki/Opcode) 428 | * [translation lookaside buffer](https://en.wikipedia.org/wiki/Translation_lookaside_buffer) 429 | * [per-cpu variables](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-1.html) 430 | * [flags register](https://en.wikipedia.org/wiki/FLAGS_register) 431 | * [tasklet](https://xinqiu.gitbooks.io/linux-insides-cn/content/Interrupts/linux-interrupts-9.html) 432 | * [Paging](https://xinqiu.gitbooks.io/linux-insides-cn/content/Theory/linux-theory-1.html) 433 | * [Previous part](https://xinqiu.gitbooks.io/linux-insides-cn/content/MM/linux-mm-2.html) 434 | -------------------------------------------------------------------------------- /Misc/README.md: -------------------------------------------------------------------------------- 1 | # 杂项 2 | 3 | 这个章节包含不直接涉及到内核源码的部分以及各个子系统的实现。 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linux 内核揭密 2 | 3 | [![Join the chat at https://gitter.im/MintCN/linux-insides-zh](https://badges.gitter.im/MintCN/linux-insides-zh.svg)](https://gitter.im/MintCN/linux-insides-zh?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | 一系列关于 Linux 内核和其内在机理的帖子。 6 | 7 | **目的很简单** - 分享我对 Linux 内核内在机理的一点知识,帮助对 Linux 内核内在机理感兴趣的人,和其他低级话题。 8 | 9 | **问题/建议**: 若有相关问题,请提交 issue。英文原文问题,找上游 repo - [linux-insides](https://github.com/0xAX/linux-insides) 提交 issue;翻译问题,在下游 repo - [linux-insides-zh](https://github.com/MintCN/linux-insides-zh) 中提交 issue。 10 | 11 | ## 贡献条目 12 | 13 | 对于 linux-insides-zh 项目,您可以通过以下方法贡献自己的力量: 14 | 15 | - 英文翻译,目前只提供简体中文的译文; 16 | - 更新未被翻译的英文原本,其实就是将上游英文的更新纳入到当前项目中; 17 | - 更新已经翻译的中文译文,其实就是查看上游英文的更新,检查是否需要对中文译文进行更新; 18 | - 校对当前已经翻译过的中文译文,包括修改 typo,润色等工作; 19 | 20 | ## 翻译进度 21 | 22 | | 章节|译者|翻译进度| 23 | | ------------- |:-------------:| -----:| 24 | | 1. [Booting](https://github.com/MintCN/linux-insides-zh/tree/master/Booting)||正在进行| 25 | |├ [1.0](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/README.md)|[@xinqiu](https://github.com/xinqiu)|更新至[527b2b8](https://github.com/0xAX/linux-insides/commit/527b2b8921c3d9c043bd914c5990d6a991e3035b)| 26 | |├ [1.1](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-1.md)|[@hailincai](https://github.com/hailincai)|已完成| 27 | |├ [1.2](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-2.md)|[@hailincai](https://github.com/hailincai)|已完成| 28 | |├ [1.3](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-3.md)|[@hailincai](https://github.com/hailincai)|已完成| 29 | |├ [1.4](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-4.md)|[@zmj1316](https://github.com/zmj1316)|已完成| 30 | |├ [1.5](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-5.md)|[@mytbk](https://github.com/mytbk)|更新至[31998d14](https://github.com/0xAX/linux-insides/commit/31998d14320f25399d67d4fff446a65178931e90)| 31 | |└ [1.6](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-6.md)|[@mytbk](https://github.com/mytbk)|更新至[31998d14](https://github.com/0xAX/linux-insides/commit/31998d14320f25399d67d4fff446a65178931e90)| 32 | | 2. [Initialization](https://github.com/MintCN/linux-insides-zh/tree/master/Initialization)||正在进行| 33 | |├ [2.0](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[44017507](https://github.com/0xAX/linux-insides/commit/4401750766f7150dcd16f579026f5554541a6ab9)| 34 | |├ [2.1](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-1.md)|[@dontpanic92](https://github.com/dontpanic92)|更新至[44017507](https://github.com/0xAX/linux-insides/commit/4401750766f7150dcd16f579026f5554541a6ab9)| 35 | |├ [2.2](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-2.md)|[@dontpanic92](https://github.com/dontpanic92)|更新至[44017507](https://github.com/0xAX/linux-insides/commit/4401750766f7150dcd16f579026f5554541a6ab9)| 36 | |├ [2.3](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-3.md)|[@dontpanic92](https://github.com/dontpanic92)|更新至[44017507](https://github.com/0xAX/linux-insides/commit/4401750766f7150dcd16f579026f5554541a6ab9)| 37 | |├ [2.4](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-4.md)|[@bjwrkj](https://github.com/bjwrkj)|已完成| 38 | |├ [2.5](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-5.md)|[@NeoCui](https://github.com/NeoCui)|更新至[cf32dc6c](https://github.com/0xAX/linux-insides/commit/cf32dc6c81abce567af330c480afc3d58678443d)| 39 | |├ [2.6](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-6.md)|[@kele1997](https://github.com/kele1997)|更新至[e896e56](https://github.com/0xAX/linux-insides/commit/e896e56c867876397ef78da58d5e2a31b2e690b6)| 40 | |├ [2.7](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-7.md)||未开始| 41 | |├ [2.8](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-8.md)||未开始| 42 | |├ [2.9](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-9.md)||未开始| 43 | |└ [2.10](https://github.com/MintCN/linux-insides-zh/blob/master/Initialization/linux-initialization-10.md)||未开始| 44 | | 3. [Interrupts](https://github.com/MintCN/linux-insides-zh/tree/master/Interrupts)||正在进行| 45 | |├ [3.0](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/README.md)|[@littleneko](https://github.com/littleneko)|更新至[57279321](https://github.com/0xAX/linux-insides/commit/5727932167a2ff6a1e647081c85d081d4ed8b508)| 46 | |├ [3.1](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-1.md)||未开始| 47 | |├ [3.2](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-2.md)|[@narcijie](https://github.com/narcijie)|正在进行| 48 | |├ [3.3](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-3.md)||未开始| 49 | |├ [3.4](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-4.md)||未开始| 50 | |├ [3.5](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-5.md)||未开始| 51 | |├ [3.6](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-6.md)||未开始| 52 | |├ [3.7](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-7.md)||未开始| 53 | |├ [3.8](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-8.md)||未开始| 54 | |├ [3.9](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-9.md)|[@zhangyangjing](https://github.com/zhangyangjing)|已完成| 55 | |└ [3.10](https://github.com/MintCN/linux-insides-zh/blob/master/Interrupts/linux-interrupts-10.md)|[@worldwar](https://github.com/worldwar)|已完成| 56 | | 4. [System calls](https://github.com/MintCN/linux-insides-zh/tree/master/SysCall)||正在进行| 57 | |├ [4.0](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[194d0c83](https://github.com/0xAX/linux-insides/commit/194d0c83e3273c6167830c29d9ba13ec57bfbcb6)| 58 | |├ [4.1](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/linux-syscall-1.md)|[@qianmoke](https://github.com/qianmoke)|已完成| 59 | |├ [4.2](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/linux-syscall-2.md)|[@qianmoke](https://github.com/qianmoke)|已完成| 60 | |├ [4.3](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/linux-syscall-3.md)|[@Newester](https://github.com/Newester)|正在进行| 61 | |├ [4.4](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/linux-syscall-4.md)|[@Newester](https://github.com/Newester)|正在进行| 62 | |├ [4.5](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/linux-syscall-5.md)||未开始| 63 | |└ [4.6](https://github.com/MintCN/linux-insides-zh/blob/master/SysCall/linux-syscall-6.md)||未开始| 64 | | 5. [Timers and time management](https://github.com/MintCN/linux-insides-zh/tree/master/Timers)||正在进行| 65 | |├ [5.0](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[2a742fd4](https://github.com/0xAX/linux-insides/commit/2a742fd485df0260efce2078e7162c0de668e98b)| 66 | |├ [5.1](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-1.md)||未开始| 67 | |├ [5.2](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-2.md)||未开始| 68 | |├ [5.3](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-3.md)||未开始| 69 | |├ [5.4](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-4.md)||未开始| 70 | |├ [5.5](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-5.md)||未开始| 71 | |├ [5.6](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-6.md)||未开始| 72 | |└ [5.7](https://github.com/MintCN/linux-insides-zh/blob/master/Timers/linux-timers-7.md)||未开始| 73 | | 6. [Synchronization primitives](https://github.com/MintCN/linux-insides-zh/tree/master/SyncPrim)||正在进行| 74 | |├ [6.0](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[6f85b63e](https://github.com/0xAX/linux-insides/commit/6f85b63e347b636e08e965e9dc22c177e972afe2)| 75 | |├ [6.1](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/linux-sync-1.md)|[@keltoy](https://github.com/keltoy)|已完成| 76 | |├ [6.2](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/linux-sync-2.md)|[@keltoy](https://github.com/keltoy)|已完成| 77 | |├ [6.3](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/linux-sync-3.md)|[@huxq](https://github.com/huxq)|已完成| 78 | |├ [6.4](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/linux-sync-4.md)||未开始| 79 | |├ [6.5](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/linux-sync-5.md)||未开始| 80 | |└ [6.6](https://github.com/MintCN/linux-insides-zh/blob/master/SyncPrim/linux-sync-6.md)||未开始| 81 | | 7. [Memory management](https://github.com/MintCN/linux-insides-zh/tree/master/MM)||未开始| 82 | |├ [7.0](https://github.com/MintCN/linux-insides-zh/blob/master/MM/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[f83c8ee2](https://github.com/0xAX/linux-insides/commit/f83c8ee29e2051a8f4c08d6a0fa8247d934e14d9)| 83 | |├ [7.1](https://github.com/MintCN/linux-insides-zh/blob/master/MM/linux-mm-1.md)|[@choleraehyq](https://github.com/choleraehyq)|已完成| 84 | |├ [7.2](https://github.com/MintCN/linux-insides-zh/blob/master/MM/linux-mm-2.md)|[@choleraehyq](https://github.com/choleraehyq)|已完成| 85 | |└ [7.3](https://github.com/MintCN/linux-insides-zh/blob/master/MM/linux-mm-3.md)||未开始| 86 | | 8. SMP||上游未开始| 87 | | 9. [Concepts](https://github.com/MintCN/linux-insides-zh/tree/master/Concepts)||正在进行| 88 | |├ [9.0](https://github.com/MintCN/linux-insides-zh/blob/master/Concepts/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[44017507](https://github.com/0xAX/linux-insides/commit/4401750766f7150dcd16f579026f5554541a6ab9)| 89 | |├ [9.1](https://github.com/MintCN/linux-insides-zh/blob/master/Concepts/linux-cpu-1.md)|[@up2wing](https://github.com/up2wing)|更新至[28a39fe6](https://github.com/0xAX/linux-insides/commit/28a39fe6653e780641e80ab6e37c79ffafca07b0#diff-0460583622f03a52d7693094d6fa2452)| 90 | |├ [9.2](https://github.com/MintCN/linux-insides-zh/blob/master/Concepts/linux-cpu-2.md)|[@up2wing](https://github.com/up2wing)|更新至[28a39fe6](https://github.com/0xAX/linux-insides/commit/28a39fe6653e780641e80ab6e37c79ffafca07b0#diff-0460583622f03a52d7693094d6fa2452)| 91 | |├ [9.3](https://github.com/MintCN/linux-insides-zh/blob/master/Concepts/linux-cpu-3.md)|[@up2wing](https://github.com/up2wing)|更新至[28a39fe6](https://github.com/0xAX/linux-insides/commit/28a39fe6653e780641e80ab6e37c79ffafca07b0#diff-0460583622f03a52d7693094d6fa2452)| 92 | |└ [9.4](https://github.com/MintCN/linux-insides-zh/blob/master/Concepts/linux-cpu-3.md)||未开始| 93 | | 10. [DataStructures](https://github.com/MintCN/linux-insides-zh/tree/master/DataStructures)||已完成| 94 | |├ [10.0](https://github.com/MintCN/linux-insides-zh/blob/master/DataStructures/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[99138e09](https://github.com/0xAX/linux-insides/commit/99138e0932dc25bf6c90dd102a70a6d15589e9ab)| 95 | |├ [10.1](https://github.com/MintCN/linux-insides-zh/blob/master/DataStructures/linux-datastructures-1.md)|[@oska874](http://github.com/oska874) [@mudongliang](https://github.com/mudongliang)|已完成| 96 | |├ [10.2](https://github.com/MintCN/linux-insides-zh/blob/master/DataStructures/linux-datastructures-2.md)|[@a1ickgu0](https://github.com/a1ickgu0)|已完成| 97 | |└ [10.3](https://github.com/MintCN/linux-insides-zh/blob/master/DataStructures/linux-datastructures-3.md)|[@cposture](https://github.com/cposture)|已完成| 98 | | 11. [Theory](https://github.com/MintCN/linux-insides-zh/tree/master/Theory)||正在进行| 99 | |├ [11.0](https://github.com/MintCN/linux-insides-zh/blob/master/Theory/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[99ad0799](https://github.com/0xAX/linux-insides/commit/99ad07999636b76985218e02e5a52140050cbbde)| 100 | |├ [11.1](https://github.com/MintCN/linux-insides-zh/blob/master/Theory/linux-theory-1.md)|[@mudongliang](https://github.com/mudongliang)|已完成| 101 | |├ [11.2](https://github.com/MintCN/linux-insides-zh/blob/master/Theory/linux-theory-2.md)|[@mudongliang](https://github.com/mudongliang)|已完成| 102 | |└ [11.3](https://github.com/MintCN/linux-insides-zh/blob/master/Theory/linux-theory-3.md)||未开始| 103 | | 12. Initial ram disk||上游未开始| 104 | | 13. [Misc](https://github.com/MintCN/linux-insides-zh/tree/master/Misc)||已完成| 105 | |├ [13.0](https://github.com/MintCN/linux-insides-zh/blob/master/Misc/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[ddf0793f](https://github.com/0xAX/linux-insides/commit/ddf0793f6b74b6c541c4979b4deaf093b2b87c9b)| 106 | |├ [13.1](https://github.com/MintCN/linux-insides-zh/blob/master/Misc/linux-misc-1.md)|[@hao-lee](https://github.com/hao-lee)|更新至[3ed52146](https://github.com/0xAX/linux-insides/commit/3ed521464e99a8ff2f8d438592a605a716a268e2)| 107 | |├ [13.2](https://github.com/MintCN/linux-insides-zh/blob/master/Misc/linux-misc-2.md)|[@oska874](https://github.com/oska874)|已完成| 108 | |├ [13.3](https://github.com/MintCN/linux-insides-zh/blob/master/Misc/linux-misc-3.md)|[@zmj1316](https://github.com/zmj1316)|已完成| 109 | |└ [13.4](https://github.com/MintCN/linux-insides-zh/blob/master/Misc/linux-misc-4.md)|[@mudongliang](https://github.com/mudongliang)|已完成| 110 | | 14. [KernelStructures](https://github.com/MintCN/linux-insides-zh/tree/master/KernelStructures)||已完成| 111 | |├ [14.0](https://github.com/MintCN/linux-insides-zh/tree/master/KernelStructures/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[3cb550c0](https://github.com/0xAX/linux-insides/commit/3cb550c089c8fc609f667290434e9e98e93fa279)| 112 | |└ [14.1](https://github.com/MintCN/linux-insides-zh/tree/master/KernelStructures/linux-kernelstructure-1.md)|[@woodpenker](https://github.com/woodpenker)|更新至[4521637d](https://github.com/0xAX/linux-insides/commit/4521637d9cb76e5d4e4dc951758b264a68504927)| 113 | | 15. [Cgroups](https://github.com/MintCN/linux-insides-zh/tree/master/Cgroups)||正在进行| 114 | |├ [15.0](https://github.com/MintCN/linux-insides-zh/tree/master/Cgroups/README.md)|[@mudongliang](https://github.com/mudongliang)|更新至[e811ca4f](https://github.com/0xAX/linux-insides/commit/90f50c2ac5a197da044e5091c631dd43e811ca4f)| 115 | |└ [15.1](https://github.com/MintCN/linux-insides-zh/tree/master/Cgroups/linux-cgroups-1.md)|[@tjm-1990](https://github.com/tjm-1990)|更新至[b420e581](https://github.com/0xAX/linux-insides/commit/b420e581fe3cfee64d9c65103740d4fd98127b6f)| 116 | 117 | ## 翻译认领规则 118 | 119 | 为了避免多个译者同时翻译相同章节的情况出现,请按照以下规则认领自己要翻译的章节: 120 | 121 | * 在 [README.md](https://github.com/MintCN/linux-insides-zh/blob/master/README.md) 中查看你想翻译的章节的状态; 122 | * 在确认想翻译的章节没有被翻译之后,开一个 issue ,告诉大家你想翻译哪一章节,同时提交申请翻译的 PR ,将 [README.md](https://github.com/MintCN/linux-insides-zh/blob/master/README.md) 中的翻译状态修改为“正在进行”; 123 | * 首先,从上游的[英文库](https://github.com/0xAX/linux-insides)中得到该章节的最新版本,将修改提交到我们的中文库中; 124 | * 然后翻译你认领的章节; 125 | * 完成翻译之后,提交翻译内容的 PR (**注:大家最好以一个文件为基本单位来提交翻译 PR,方便我们进行 review,否则可能会因为 comments 导致展示 PR 的网页变得过于冗长,不方便 review 修改的内容**)。待 PR 被合并之后,请关闭 issue; 126 | * 最后,将 [README.md](https://github.com/MintCN/linux-insides-zh/blob/master/README.md) 中的翻译状态修改为“更新至上游 commit id”(**注:8 位 commit id**),同时不要忘记把自己添加到 [CONTRIBUTORS.md](https://github.com/MintCN/linux-insides-zh/blob/master/CONTRIBUTORS.md) 中。 127 | 128 | 翻译前建议看 [TRANSLATION_NOTES.md](https://github.com/MintCN/linux-insides-zh/blob/master/TRANSLATION_NOTES.md) 。关于翻译约定,大家有任何问题或建议也请开 issue 讨论。 129 | 130 | ## 翻译申请回收原则 131 | 132 | 为了避免翻译申请过长时间没有任何更新,所以暂设置时间为三个月,如果三个月之内,issue 并没有任何更新,翻译申请就会被回收。 133 | 134 | ## 作者 135 | 136 | [@0xAX](https://twitter.com/0xAX) 137 | 138 | ## 中文维护者 139 | 140 | [@xinqiu](https://github.com/xinqiu) 141 | 142 | [@mudongliang](https://github.com/mudongliang) 143 | 144 | ## 中文贡献者 145 | 146 | 详见 [CONTRIBUTORS.md](https://github.com/MintCN/linux-insides-zh/blob/master/CONTRIBUTORS.md) 147 | 148 | ## LICENSE 149 | 150 | Licensed [BY-NC-SA Creative Commons](http://creativecommons.org/licenses/by-nc-sa/4.0/). 151 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [引导](Booting/README.md) 5 | * [从引导加载程序内核](Booting/linux-bootstrap-1.md) 6 | * [在内核安装代码的第一步](Booting/linux-bootstrap-2.md) 7 | * [视频模式初始化和转换到保护模式](Booting/linux-bootstrap-3.md) 8 | * [过渡到 64 位模式](Booting/linux-bootstrap-4.md) 9 | * [内核解压缩](Booting/linux-bootstrap-5.md) 10 | * [初始化](Initialization/README.md) 11 | * [内核解压之后的首要步骤](Initialization/linux-initialization-1.md) 12 | * [早期的中断和异常控制](Initialization/linux-initialization-2.md) 13 | * [在到达内核入口之前最后的准备](Initialization/linux-initialization-3.md) 14 | * [内核入口 - start_kernel](Initialization/linux-initialization-4.md) 15 | * [体系架构初始化](Initialization/linux-initialization-5.md) 16 | * [进一步初始化指定体系架构](Initialization/linux-initialization-6.md) 17 | * [最后对指定体系架构初始化](Initialization/linux-initialization-7.md) 18 | * [调度器初始化](Initialization/linux-initialization-8.md) 19 | * [RCU 初始化](Initialization/linux-initialization-9.md) 20 | * [初始化结束](Initialization/linux-initialization-10.md) 21 | * [中断](Interrupts/README.md) 22 | * [中断和中断处理第一部分](Interrupts/linux-interrupts-1.md) 23 | * [深入 Linux 内核中的中断](Interrupts/linux-interrupts-2.md) 24 | * [初步中断处理](Interrupts/linux-interrupts-3.md) 25 | * [中断处理](Interrupts/linux-interrupts-4.md) 26 | * [异常处理的实现](Interrupts/linux-interrupts-5.md) 27 | * [处理不可屏蔽中断](Interrupts/linux-interrupts-6.md) 28 | * [深入外部硬件中断](Interrupts/linux-interrupts-7.md) 29 | * [IRQs的非早期初始化](Interrupts/linux-interrupts-8.md) 30 | * [Softirq, Tasklets and Workqueues](Interrupts/linux-interrupts-9.md) 31 | * [最后一部分](Interrupts/linux-interrupts-10.md) 32 | * [系统调用](SysCall/README.md) 33 | * [系统调用概念简介](SysCall/linux-syscall-1.md) 34 | * [Linux 内核如何处理系统调用](SysCall/linux-syscall-2.md) 35 | * [vsyscall and vDSO](SysCall/linux-syscall-3.md) 36 | * [Linux 内核如何运行程序](SysCall/linux-syscall-4.md) 37 | * [open 系统调用的实现](SysCall/linux-syscall-5.md) 38 | * [Linux 资源限制](SysCall/linux-syscall-6.html) 39 | * [定时器和时钟管理](Timers/README.md) 40 | * [简介](Timers/linux-timers-1.md) 41 | * [时钟源框架简介](Timers/linux-timers-2.md) 42 | * [The tick broadcast framework and dyntick](Timers/linux-timers-3.md) 43 | * [定时器介绍](Timers/linux-timers-4.md) 44 | * [Clockevents 框架简介](Timers/linux-timers-5.md) 45 | * [x86 相关的时钟源](Timers/linux-timers-6.md) 46 | * [Linux 内核中与时钟相关的系统调用](Timers/linux-timers-7.md) 47 | * [同步原语](SyncPrim/README.md) 48 | * [自旋锁简介](SyncPrim/linux-sync-1.md) 49 | * [队列自旋锁](SyncPrim/linux-sync-2.md) 50 | * [信号量](SyncPrim/linux-sync-3.md) 51 | * [互斥锁](SyncPrim/linux-sync-4.md) 52 | * [读者/写者信号量](SyncPrim/linux-sync-5.md) 53 | * [顺序锁](SyncPrim/linux-sync-6.md) 54 | * [RCU]() 55 | * [Lockdep]() 56 | * [内存管理](MM/README.md) 57 | * [内存块](MM/linux-mm-1.md) 58 | * [固定映射地址和 ioremap](MM/linux-mm-2.md) 59 | * [kmemcheck](MM/linux-mm-3.md) 60 | * [控制组](Cgroups/README.md) 61 | * [控制组简介](Cgroups/linux-cgroups-1.md) 62 | * [SMP]() 63 | * [概念](Concepts/README.md) 64 | * [每个 CPU 的变量](Concepts/linux-cpu-1.md) 65 | * [CPU 掩码](Concepts/linux-cpu-2.md) 66 | * [initcall 机制](Concepts/linux-cpu-3.md) 67 | * [Linux 内核的通知链](Concepts/linux-cpu-4.md) 68 | * [Linux 内核中的数据结构](DataStructures/README.md) 69 | * [双向链表](DataStructures/linux-datastructures-1.md) 70 | * [基数树](DataStructures/linux-datastructures-2.md) 71 | * [位数组](DataStructures/linux-datastructures-3.md) 72 | * [理论](Theory/README.md) 73 | * [分页](Theory/linux-theory-1.md) 74 | * [ELF 文件格式](Theory/linux-theory-2.md) 75 | * [內联汇编](Theory/linux-theory-3.md) 76 | * [CPUID]() 77 | * [MSR]() 78 | * [Initial ram disk]() 79 | * [initrd]() 80 | * [杂项](Misc/README.md) 81 | * [Linux 内核开发](Misc/linux-misc-1.md) 82 | * [内核编译方法](Misc/linux-misc-2.md) 83 | * [链接器](Misc/linux-misc-3.md) 84 | * [用户空间的程序启动过程](Misc/linux-misc-4.md) 85 | * [书写并提交你第一个内核补丁]() 86 | * [内核数据结构](KernelStructures/README.md) 87 | * [中断描述符表](KernelStructures/linux-kernelstructure-1.md) 88 | * [有帮助的链接](LINKS.md) 89 | * [贡献者](CONTRIBUTORS.md) 90 | -------------------------------------------------------------------------------- /SyncPrim/README.md: -------------------------------------------------------------------------------- 1 | # Linux 内核中的同步原语 2 | 3 | 这个章节描述内核中所有的同步原语。 4 | 5 | * [自旋锁简介](linux-sync-1.md) - 这个章节的第一部分描述 Linux 内核中自旋锁机制的实现; 6 | * [队列自旋锁](linux-sync-2.md) - 第二部分描述自旋锁的另一种类型 - 队列自旋锁; 7 | * [信号量](linux-sync-3.md) - this part describes impmentation of `semaphore` synchronization primitive in the Linux kernel. 这个部分描述 Linux 内核中的同步原语 `semaphore` 的实现; 8 | * [互斥锁](linux-sync-4.md) - 这个部分描述 Linux 内核中的 `mutex` ; 9 | * [读者/写者信号量](linux-sync-5.md) - 这个部分描述特殊类型的信号量 - `reader/writer` 信号量; 10 | * [顺序锁](linux-sync-6.md) - 这个部分描述 Linux 内核中的顺序锁. 11 | -------------------------------------------------------------------------------- /SyncPrim/linux-sync-3.md: -------------------------------------------------------------------------------- 1 | 2 | 内核同步原语. 第三部分. 3 | ================================================================================ 4 | 5 | 信号量 6 | -------------------------------------------------------------------------------- 7 | 8 | 这是本章的第三部分 [chapter](https://xinqiu.gitbooks.io/linux-insides-cn/content/SyncPrim/index.html),本章描述了内核中的同步原语,在之前的部分我们见到了特殊的 [自旋锁](https://en.wikipedia.org/wiki/Spinlock) - `排队自旋锁`。 在更前的 [部分](https://xinqiu.gitbooks.io/linux-insides-cn/content/SyncPrim/linux-sync-2.html) 是和 `自旋锁` 相关的描述。我们将描述更多同步原语。 9 | 10 | 在 `自旋锁` 之后的下一个我们将要讲到的 [内核同步原语](https://en.wikipedia.org/wiki/Synchronization_%28computer_science%29)是 [信号量](https://en.wikipedia.org/wiki/Semaphore_%28programming%29)。我们会从理论角度开始学习什么是 `信号量`, 然后我们会像前几章一样讲到Linux内核是如何实现信号量的。 11 | 12 | 好吧,现在我们开始。 13 | 14 | 介绍Linux内核中的信号量 15 | -------------------------------------------------------------------------------- 16 | 17 | 那么究竟什么是 `信号量` ?就像你可以猜到那样 - `信号量` 是另外一种支持线程或者进程的同步机制。Linux内核已经提供了一种同步机制 - `自旋锁`, 为什么我们还需要另外一种呢?为了回答这个问题,我们需要理解这两种机制。我们已经熟悉了 `自旋锁` ,因此我们从 `信号量` 机制开始。 18 | 19 | `自旋锁` 的设计理念是它仅会被持有非常短的时间。 但持有自旋锁的时候我们不可以进入睡眠模式因为其他的进程在等待我们。为了防止 [死锁](https://en.wikipedia.org/wiki/Deadlock) [上下文交换](https://en.wikipedia.org/wiki/Context_switch) 也是不允许的。 20 | 21 | 当需要长时间持有一个锁的时候 [信号量](https://en.wikipedia.org/wiki/Semaphore_%28programming%29) 就是一个很好的解决方案。从另一个方面看,这个机制对于需要短期持有锁的应用并不是最优。为了理解这个问题,我们需要知道什么是 `信号量`。 22 | 23 | 就像一般的同步原语,`信号量` 是基于变量的。这个变量可以变大或者减少,并且这个变量的状态代表了获取锁的能力。注意这个变量的值并不限于 `0` 和 `1`。有两种类型的 `信号量`: 24 | 25 | * `二值信号量`; 26 | * `普通信号量`. 27 | 28 | 第一种 `信号量` 的值可以为 `1` 或者 `0`。第二种 `信号量` 的值可以为任何非负数。如果 `信号量` 的值大于 `1` 那么它被叫做 `计数信号量`,并且它允许多于 `1` 个进程获取它。这种机制允许我们记录现有的资源,而 `自旋锁` 只允许我们为一个任务上锁。除了所有这些之外,另外一个重要的点是 `信号量` 允许进入睡眠状态。 另外当某进程在等待一个被其他进程获取的锁时, [调度器](https://en.wikipedia.org/wiki/Scheduling_%28computing%29) 也许会切换别的进程。 29 | 30 | 信号量 API 31 | -------------------------------------------------------------------------------- 32 | 33 | 因此,我们从理论方面了解一些 `信号量`的知识,我们来看看它在Linux内核中是如何实现的。所有 `信号量` 相关的 [API](https://en.wikipedia.org/wiki/Application_programming_interface) 都在名为 [include/linux/semaphore.h](https://github.com/torvalds/linux/blob/master/include/linux/semaphore.h) 的头文件中 34 | 35 | 我们看到 `信号量` 机制是有以下的结构体表示的: 36 | 37 | ```C 38 | struct semaphore { 39 | raw_spinlock_t lock; 40 | unsigned int count; 41 | struct list_head wait_list; 42 | }; 43 | ``` 44 | 45 | 在内核中, `信号量` 结构体由三部分组成: 46 | 47 | * `lock` - 保护 `信号量` 的 `自旋锁`; 48 | * `count` - 现有资源的数量; 49 | * `wait_list` - 等待获取此锁的进程序列. 50 | 51 | 在我们考虑Linux内核的的 `信号量` [API](https://en.wikipedia.org/wiki/Application_programming_interface) 之前,我们需要知道如何初始化一个 `信号量`。事实上, Linux内核提供了两个 `信号量` 的初始函数。这些函数允许初始化一个 `信号量` 为: 52 | 53 | * `静态`; 54 | * `动态`. 55 | 56 | 我们来看看第一个种初始化静态 `信号量`。我们可以使用 `DEFINE_SEMAPHORE` 宏将 `信号量` 静态初始化。 57 | 58 | ```C 59 | #define DEFINE_SEMAPHORE(name) \ 60 | struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1) 61 | ``` 62 | 63 | 就像我们看到这样,`DEFINE_SEMAPHORE` 宏只提供了初始化 `二值` 信号量。 `DEFINE_SEMAPHORE` 宏展开到 `信号量` 结构体的定义。结构体通过 `__SEMAPHORE_INITIALIZER` 宏初始化。我们来看看这个宏的实现 64 | ```C 65 | #define __SEMAPHORE_INITIALIZER(name, n) \ 66 | { \ 67 | .lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \ 68 | .count = n, \ 69 | .wait_list = LIST_HEAD_INIT((name).wait_list), \ 70 | } 71 | ``` 72 | 73 | `__SEMAPHORE_INITIALIZER` 宏传入了 `信号量` 结构体的名字并且初始化这个结构体的各个域。首先我们使用 `__RAW_SPIN_LOCK_UNLOCKED` 宏对给予的 `信号量` 初始化一个 `自旋锁`。就像你从 [之前](https://xinqiu.gitbooks.io/linux-insides-cn/content/SyncPrim/linux-sync-1.html) 的部分看到那样,`__RAW_SPIN_LOCK_UNLOCKED` 宏是在 [include/linux/spinlock_types.h](https://github.com/torvalds/linux/blob/master/include/linux/spinlock_types.h) 头文件中定义,它展开到 `__ARCH_SPIN_LOCK_UNLOCKED` 宏,而 `__ARCH_SPIN_LOCK_UNLOCKED` 宏又展开到零或者无锁状态 74 | 75 | ```C 76 | #define __ARCH_SPIN_LOCK_UNLOCKED { { 0 } } 77 | ``` 78 | 79 | `信号量` 的最后两个域 `count` 和 `wait_list` 是通过现有资源的数量和空 [链表](https://xinqiu.gitbooks.io/linux-insides-cn/content/DataStructures/linux-datastructures-1.html)来初始化。 80 | 第二种初始化 `信号量` 的方式是将 `信号量` 和现有资源数目传送给 `sema_init` 函数。 这个函数是在 [include/linux/semaphore.h](https://github.com/torvalds/linux/blob/master/include/linux/semaphore.h) 头文件中定义的。 81 | 82 | ```C 83 | static inline void sema_init(struct semaphore *sem, int val) 84 | { 85 | static struct lock_class_key __key; 86 | *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val); 87 | lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0); 88 | } 89 | ``` 90 | 91 | 我们来看看这个函数是如何实现的。它看起来很简单。函数使用我们刚看到的 `__SEMAPHORE_INITIALIZER` 宏对传入的 `信号量` 进行初始化。就像我们在之前 [部分](https://xinqiu.gitbooks.io/linux-insides-cn/content/SyncPrim/index.html) 写的那样,我们将会跳过Linux内核关于 [锁验证](https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt) 的部分。 92 | 从现在开始我们知道如何初始化一个 `信号量`,我们看看如何上锁和解锁。Linux内核提供了如下操作 `信号量` 的 [API](https://en.wikipedia.org/wiki/Application_programming_interface) 93 | 94 | ``` 95 | void down(struct semaphore *sem); 96 | void up(struct semaphore *sem); 97 | int down_interruptible(struct semaphore *sem); 98 | int down_killable(struct semaphore *sem); 99 | int down_trylock(struct semaphore *sem); 100 | int down_timeout(struct semaphore *sem, long jiffies); 101 | ``` 102 | 103 | 前两个函数: `down` 和 `up` 是用来获取或释放 `信号量`。 `down_interruptible`函数试图去获取一个 `信号量`。如果被成功获取,`信号量` 的计数就会被减少并且锁也会被获取。同时当前任务也会被调度到受阻状态,也就是说 `TASK_INTERRUPTIBLE` 标志将会被至位。`TASK_INTERRUPTIBLE` 表示这个进程也许可以通过 [信号](https://en.wikipedia.org/wiki/Unix_signal) 退回到销毁状态。 104 | 105 | `down_killable` 函数和 `down_interruptible` 函数提供类似的功能,但是它还将当前进程的 `TASK_KILLABLE` 标志置位。这表示等待的进程可以被杀死信号中断。 106 | 107 | `down_trylock` 函数和 `spin_trylock` 函数相似。这个函数试图去获取一个锁并且退出如果这个操作是失败的。在这个例子中,想获取锁的进程不会等待。最后的 `down_timeout`函数试图去获取一个锁。当前进程将会被中断进入到等待状态当超过传入的可等待时间。除此之外你也许注意到,这个等待的时间是以 [jiffies](https://xinqiu.gitbooks.io/linux-insides-cn/content/Timers/linux-timers-1.html)计数。 108 | 109 | 我们刚刚看了 `信号量` [API](https://en.wikipedia.org/wiki/Application_programming_interface)的定义。我们从 `down` 函数开始看。这个函数是在 [kernel/locking/semaphore.c](https://github.com/torvalds/linux/blob/master/kernel/locking/semaphore.c) 源代码定义的。我们来看看函数实现: 110 | 111 | ```C 112 | void down(struct semaphore *sem) 113 | { 114 | unsigned long flags; 115 | 116 | raw_spin_lock_irqsave(&sem->lock, flags); 117 | if (likely(sem->count > 0)) 118 | sem->count--; 119 | else 120 | __down(sem); 121 | raw_spin_unlock_irqrestore(&sem->lock, flags); 122 | } 123 | EXPORT_SYMBOL(down); 124 | ``` 125 | 126 | 我们先看在 `down` 函数起始处定义的 `flags` 变量。这个变量将会传入到 `raw_spin_lock_irqsave` 和 `raw_spin_lock_irqrestore` 宏定义。这些宏是在 [include/linux/spinlock.h](https://github.com/torvalds/linux/blob/master/include/linux/spinlock.h)头文件定义的。这些宏用来保护当前 `信号量` 的计数器。事实上这两个宏的作用和 `spin_lock` 和 `spin_unlock` 宏相似。只不过这组宏会存储/重置当前中断标志并且禁止 [中断](https://en.wikipedia.org/wiki/Interrupt)。 127 | 128 | 就像你猜到那样, `down` 函数的主要就是通过 `raw_spin_lock_irqsave` 和 `raw_spin_unlock_irqrestore` 宏来实现的。我们通过将 `信号量` 的计数器和零对比,如果计数器大于零,我们可以减少这个计数器。这表示我们已经获取了这个锁。否则如果计数器是零,这表示所以的现有资源都已经被占用,我们需要等待以获取这个锁。就像我们看到那样, `__down` 函数将会被调用。 129 | `__down` 函数是在 [相同](https://github.com/torvalds/linux/blob/master/kernel/locking/semaphore.c))的源代码定义的,它的实现看起来如下: 130 | ```C 131 | static noinline void __sched __down(struct semaphore *sem) 132 | { 133 | __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); 134 | } 135 | ``` 136 | 137 | `__down` 函数仅仅调用了 `__down_common` 函数,并且传入了三个参数 138 | 139 | * `semaphore`; 140 | * `flag` - 对当前任务; 141 | * `timeout` - 最长等待 `信号量` 的时间. 142 | 143 | 在我们看 `__down_common` 函数之前,注意 `down_trylock`, `down_timeout` 和 `down_killable` 的实现也都是基于 `__down_common` 函数。 144 | 145 | ```C 146 | static noinline int __sched __down_interruptible(struct semaphore *sem) 147 | { 148 | return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); 149 | } 150 | ``` 151 | 152 | `__down_killable` 函数: 153 | 154 | ```C 155 | static noinline int __sched __down_killable(struct semaphore *sem) 156 | { 157 | return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT); 158 | } 159 | ``` 160 | 161 | `__down_timeout` 函数: 162 | 163 | ```C 164 | static noinline int __sched __down_timeout(struct semaphore *sem, long timeout) 165 | { 166 | return __down_common(sem, TASK_UNINTERRUPTIBLE, timeout); 167 | } 168 | ``` 169 | 170 | 现在我们来看看 `__down_common` 函数的实现。这个函数是在 [kernel/locking/semaphore.c](https://github.com/torvalds/linux/blob/master/kernel/locking/semaphore.c)源文件中定义的。这个函数的定义从以下两个本地变量开始。 171 | 172 | ```C 173 | struct task_struct *task = current; 174 | struct semaphore_waiter waiter; 175 | ``` 176 | 177 | 第一个变量表示当前想获取本地处理器锁的任务。 `current` 宏是在 [arch/x86/include/asm/current.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/current.h) 头文件中定义的。 178 | 179 | ```C 180 | #define current get_current() 181 | ``` 182 | 183 | `get_current` 函数返回 `current_task` [per-cpu](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-1.html) 变量的值。 184 | 185 | 186 | ```C 187 | DECLARE_PER_CPU(struct task_struct *, current_task); 188 | 189 | static __always_inline struct task_struct *get_current(void) 190 | { 191 | return this_cpu_read_stable(current_task); 192 | } 193 | ``` 194 | 195 | 第二个变量是 `waiter` 表示了一个 `semaphore.wait_list` 列表的入口: 196 | 197 | ```C 198 | struct semaphore_waiter { 199 | struct list_head list; 200 | struct task_struct *task; 201 | bool up; 202 | }; 203 | ``` 204 | 205 | 下一步我们将当前进程加入到 `wait_list` 并且在定义如下变量后填充 `waiter` 域 206 | 207 | ```C 208 | list_add_tail(&waiter.list, &sem->wait_list); 209 | waiter.task = task; 210 | waiter.up = false; 211 | ``` 212 | 213 | 下一步我们进入到如下的无限循环: 214 | 215 | ```C 216 | for (;;) { 217 | if (signal_pending_state(state, task)) 218 | goto interrupted; 219 | 220 | if (unlikely(timeout <= 0)) 221 | goto timed_out; 222 | 223 | __set_task_state(task, state); 224 | 225 | raw_spin_unlock_irq(&sem->lock); 226 | timeout = schedule_timeout(timeout); 227 | raw_spin_lock_irq(&sem->lock); 228 | 229 | if (waiter.up) 230 | return 0; 231 | } 232 | ``` 233 | 234 | 在之前的代码中我们将 `waiter.up` 设置为 `false`。所以当 `up` 没有设置为 `true` 任务将会在这个无限循环中循环。这个循环从检查当前的任务是否处于 `pending` 状态开始,也就是说此任务的标志包含 `TASK_INTERRUPTIBLE` 或者 `TASK_WAKEKILL` 标志。我之前写到当一个任务在等待获取一个信号的时候任务也许可以被 [信号](https://en.wikipedia.org/wiki/Unix_signal) 中断。`signal_pending_state` 函数是在 [include/linux/sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h)原文件中定义的,它看起来如下: 235 | 236 | ```C 237 | static inline int signal_pending_state(long state, struct task_struct *p) 238 | { 239 | if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL))) 240 | return 0; 241 | if (!signal_pending(p)) 242 | return 0; 243 | 244 | return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p); 245 | } 246 | ``` 247 | 我们先会检测 `state` [位掩码](https://en.wikipedia.org/wiki/Mask_%28computing%29) 包含 `TASK_INTERRUPTIBLE` 或者 `TASK_WAKEKILL` 位,如果不包含这两个位,函数退出。下一步我们检测当前任务是否有一个挂起信号,如果没有挂起信号函数退出。最后我们就检测 `state` 位掩码的 `TASK_INTERRUPTIBLE` 位。如果,我们任务包含一个挂起信号,我们将会跳转到 `interrupted` 标签: 248 | 249 | ```C 250 | interrupted: 251 | list_del(&waiter.list); 252 | return -EINTR; 253 | ``` 254 | 255 | 在这个标签中,我们会删除等待锁的列表,然后返回 `-EINTR` [错误码](https://en.wikipedia.org/wiki/Errno.h)。 如果一个任务没有挂起信号,我们检测超时是否小于等于零。 256 | 257 | ```C 258 | if (unlikely(timeout <= 0)) 259 | goto timed_out; 260 | ``` 261 | 262 | 我们跳转到 `timed_out` 标签: 263 | 264 | ```C 265 | timed_out: 266 | list_del(&waiter.list); 267 | return -ETIME; 268 | ``` 269 | 270 | 在这个标签里,我们继续做和 `interrupted` 一样的事情。我们将任务从锁等待者中删除,但是返回 `-ETIME` 错误码。如果一个任务没有挂起信号而且给予的超时也没有过期,当前的任务将会被设置为传入的 `state`: 271 | 272 | ```C 273 | __set_task_state(task, state); 274 | ``` 275 | 276 | 然后调用 `schedule_timeout` 函数: 277 | 278 | ```C 279 | raw_spin_unlock_irq(&sem->lock); 280 | timeout = schedule_timeout(timeout); 281 | raw_spin_lock_irq(&sem->lock); 282 | ``` 283 | 284 | 这个函数是在 [kernel/time/timer.c](https://github.com/torvalds/linux/blob/master/kernel/time/timer.c) 代码中定义的。`schedule_timeout` 函数将当前的任务置为休眠到设置的超时为止。 285 | 286 | 这就是所有关于 `__down_common` 函数。如果一个函数想要获取一个已经被其它任务获取的锁,它将会转入到无限循环。并且它不能被信号中断,当前设置的超时不会过期或者当前持有锁的任务不释放它。现在我们来看看 `up` 函数的实现。 287 | 288 | `up` 函数和 `down` 函数定义在[同一个](https://github.com/torvalds/linux/blob/master/kernel/locking/semaphore.c) 原文件。这个函数的主要功能是释放锁,这个函数看起来: 289 | 290 | ```C 291 | void up(struct semaphore *sem) 292 | { 293 | unsigned long flags; 294 | 295 | raw_spin_lock_irqsave(&sem->lock, flags); 296 | if (likely(list_empty(&sem->wait_list))) 297 | sem->count++; 298 | else 299 | __up(sem); 300 | raw_spin_unlock_irqrestore(&sem->lock, flags); 301 | } 302 | EXPORT_SYMBOL(up); 303 | ``` 304 | 305 | 它看起来和 `down` 函数相似。这里有两个不同点。首先我们增加 `semaphore` 的计数。如果等待列表是空的,我们调用在当前原文件中定义的 `__up` 函数。如果等待列表不是空的,我们需要允许列表中的第一个任务去获取一个锁: 306 | 307 | ```C 308 | static noinline void __sched __up(struct semaphore *sem) 309 | { 310 | struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, 311 | struct semaphore_waiter, list); 312 | list_del(&waiter->list); 313 | waiter->up = true; 314 | wake_up_process(waiter->task); 315 | } 316 | ``` 317 | 318 | 在此我们获取待序列中的第一个任务,将它从列表中删除,将它的 `waiter-up` 设置为真。从此刻起 `__down_common` 函数中的无限循环将会被停止。 `wake_up_process` 函数将会在 `__up` 函数的结尾调用。我们从 `__down_common` 函数调用的 `schedule_timeout` 函数调用了 `schedule_timeout` 函数。`schedule_timeout` 函数将当前任务置于睡眠状态直到超时等待。现在我们进程也许会睡眠,我们需要唤醒。这就是为什么我们需要从 [kernel/sched/core.c](https://github.com/torvalds/linux/blob/master/kernel/sched/core.c) 源代码中调用 `wake_up_process` 函数 319 | 320 | 这就是所有的信息了。 321 | 322 | 小结 323 | -------------------------------------------------------------------------------- 324 | 325 | 这就是Linux内核中关于 [同步原语](https://en.wikipedia.org/wiki/Synchronization_%28computer_science%29) 的第三部分的终结。在之前的两个部分,我们已经见到了第一个Linux内核的同步原语 `自旋锁`,它是使用 `ticket spinlock` 实现并且用于很短时间的锁。在这个部分我们见到了另外一种同步原语 - [信号量](https://en.wikipedia.org/wiki/Semaphore_%28programming%29),信号量用于长时间的锁,因为它会导致 [上下文切换](https://en.wikipedia.org/wiki/Context_switch)。 在下一部分,我们将会继续深入Linux内核的同步原语并且讨论另一个同步原语 - [互斥量](https://en.wikipedia.org/wiki/Mutual_exclusion)。 326 | 327 | 如果你有问题或者建议,请在twitter [0xAX](https://twitter.com/0xAX)上联系我,通过 [email](anotherworldofworld@gmail.com)联系我,或者创建一个 [issue](https://github.com/MintCN/linux-insides-zh/issues/new) 328 | 329 | 330 | 331 | 332 | 链接 333 | -------------------------------------------------------------------------------- 334 | 335 | * [spinlocks](https://en.wikipedia.org/wiki/Spinlock) 336 | * [synchronization primitive](https://en.wikipedia.org/wiki/Synchronization_%28computer_science%29) 337 | * [semaphore](https://en.wikipedia.org/wiki/Semaphore_%28programming%29) 338 | * [context switch](https://en.wikipedia.org/wiki/Context_switch) 339 | * [preemption](https://en.wikipedia.org/wiki/Preemption_%28computing%29) 340 | * [deadlocks](https://en.wikipedia.org/wiki/Deadlock) 341 | * [scheduler](https://en.wikipedia.org/wiki/Scheduling_%28computing%29) 342 | * [Doubly linked list in the Linux kernel](https://xinqiu.gitbooks.io/linux-insides-cn/content/DataStructures/linux-datastructures-1.html) 343 | * [jiffies](https://xinqiu.gitbooks.io/linux-insides-cn/content/Timers/linux-timers-1.html) 344 | * [interrupts](https://en.wikipedia.org/wiki/Interrupt) 345 | * [per-cpu](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-1.html) 346 | * [bitmask](https://en.wikipedia.org/wiki/Mask_%28computing%29) 347 | * [SIGKILL](https://en.wikipedia.org/wiki/Unix_signal#SIGKILL) 348 | * [errno](https://en.wikipedia.org/wiki/Errno.h) 349 | * [API](https://en.wikipedia.org/wiki/Application_programming_interface) 350 | * [mutex](https://en.wikipedia.org/wiki/Mutual_exclusion) 351 | * [Previous part](https://xinqiu.gitbooks.io/linux-insides-cn/content/SyncPrim/linux-sync-2.html) 352 | 353 | -------------------------------------------------------------------------------- /SysCall/README.md: -------------------------------------------------------------------------------- 1 | # 系统调用 2 | 3 | 本章描述 Linux 内核中的系统调用概念。 4 | 5 | * [系统调用概念简介](linux-syscall-1.md) - 介绍 Linux 内核中的系统调用概念 6 | * [Linux 内核如何处理系统调用](linux-syscall-2.md) - 介绍 Linux 内核如何处理来自于用户空间应用的系统调用。 7 | * [vsyscall and vDSO](linux-syscall-3.md) - 介绍 `vsyscall` 和 `vDSO` 概念。 8 | * [Linux 内核如何运行程序](linux-syscall-4.md) - 介绍一个程序的启动过程。 9 | * [open 系统调用的实现](linux-syscall-5.md) - 介绍 open 系统调用的实现。 10 | * [Linux 资源限制](linux-syscall-6.md) - 介绍 `getrlimit/setrlimit` 的实现。 11 | -------------------------------------------------------------------------------- /SysCall/linux-syscall-6.md: -------------------------------------------------------------------------------- 1 | Limits on resources in Linux 2 | ================================================================================ 3 | 4 | Each process in the system uses certain amount of different resources like files, CPU time, memory and so on. 5 | 6 | Such resources are not infinite and each process and we should have an instrument to manage it. Sometimes it is useful to know current limits for a certain resource or to change it's value. In this post we will consider such instruments that allow us to get information about limits for a process and increase or decrease such limits. 7 | 8 | We will start from userspace view and then we will look how it is implemented in the Linux kernel. 9 | 10 | There are three main fundamental [system calls](https://en.wikipedia.org/wiki/System_call) to manage resource limit for a process: 11 | 12 | * `getrlimit` 13 | * `setrlimit` 14 | * `prlimit` 15 | 16 | The first two allows a process to read and set limits on a system resource. The last one is extension for previous functions. The `prlimit` allows to set and read the resource limits of a process specified by [PID](https://en.wikipedia.org/wiki/Process_identifier). Definitions of these functions looks: 17 | 18 | The `getrlimit` is: 19 | 20 | ```C 21 | int getrlimit(int resource, struct rlimit *rlim); 22 | ``` 23 | 24 | The `setrlimit` is: 25 | 26 | ```C 27 | int setrlimit(int resource, const struct rlimit *rlim); 28 | ``` 29 | 30 | And the definition of the `prlimit` is: 31 | 32 | ```C 33 | int prlimit(pid_t pid, int resource, const struct rlimit *new_limit, 34 | struct rlimit *old_limit); 35 | ``` 36 | 37 | In the first two cases, functions takes two parameters: 38 | 39 | * `resource` - represents resource type (we will see available types later); 40 | * `rlim` - combination of `soft` and `hard` limits. 41 | 42 | There are two types of limits: 43 | 44 | * `soft` 45 | * `hard` 46 | 47 | The first provides actual limit for a resource of a process. The second is a ceiling value of a `soft` limit and can be set only by superuser. So, `soft` limit can never exceed related `hard` limit. 48 | 49 | Both these values are combined in the `rlimit` structure: 50 | 51 | ```C 52 | struct rlimit { 53 | rlim_t rlim_cur; 54 | rlim_t rlim_max; 55 | }; 56 | ``` 57 | 58 | The last one function looks a little bit complex and takes `4` arguments. Besides `resource` argument, it takes: 59 | 60 | * `pid` - specifies an ID of a process on which the `prlimit` should be executed; 61 | * `new_limit` - provides new limits values if it is not `NULL`; 62 | * `old_limit` - current `soft` and `hard` limits will be placed here if it is not `NULL`. 63 | 64 | Exactly `prlimit` function is used by [ulimit](https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-ulimit) util. We can verify this with the help of [strace](https://linux.die.net/man/1/strace) util. 65 | 66 | For example: 67 | 68 | ``` 69 | ~$ strace ulimit -s 2>&1 | grep rl 70 | 71 | prlimit64(0, RLIMIT_NPROC, NULL, {rlim_cur=63727, rlim_max=63727}) = 0 72 | prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024, rlim_max=4*1024}) = 0 73 | prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0 74 | ``` 75 | 76 | Here we can see `prlimit64`, but not the `prlimit`. The fact is that we see underlying system call here instead of library call. 77 | 78 | Now let's look at list of available resources: 79 | 80 | | Resource | Description 81 | |-------------------|------------------------------------------------------------------------------------------| 82 | | RLIMIT_CPU | CPU time limit given in seconds | 83 | | RLIMIT_FSIZE | the maximum size of files that a process may create | 84 | | RLIMIT_DATA | the maximum size of the process's data segment | 85 | | RLIMIT_STACK | the maximum size of the process stack in bytes | 86 | | RLIMIT_CORE | the maximum size of a [core](http://man7.org/linux/man-pages/man5/core.5.html) file. | 87 | | RLIMIT_RSS | the number of bytes that can be allocated for a process in RAM | 88 | | RLIMIT_NPROC | the maximum number of processes that can be created by a user | 89 | | RLIMIT_NOFILE | the maximum number of a file descriptor that can be opened by a process | 90 | | RLIMIT_MEMLOCK | the maximum number of bytes of memory that may be locked into RAM by [mlock](http://man7.org/linux/man-pages/man2/mlock.2.html).| 91 | | RLIMIT_AS | the maximum size of virtual memory in bytes. | 92 | | RLIMIT_LOCKS | the maximum number [flock](https://linux.die.net/man/1/flock) and locking related [fcntl](http://man7.org/linux/man-pages/man2/fcntl.2.html) calls| 93 | | RLIMIT_SIGPENDING | maximum number of [signals](http://man7.org/linux/man-pages/man7/signal.7.html) that may be queued for a user of the calling process| 94 | | RLIMIT_MSGQUEUE | the number of bytes that can be allocated for [POSIX message queues](http://man7.org/linux/man-pages/man7/mq_overview.7.html) | 95 | | RLIMIT_NICE | the maximum [nice](https://linux.die.net/man/1/nice) value that can be set by a process | 96 | | RLIMIT_RTPRIO | maximum real-time priority value | 97 | | RLIMIT_RTTIME | maximum number of microseconds that a process may be scheduled under real-time scheduling policy without making blocking system call| 98 | 99 | If you're looking into source code of open source projects, you will note that reading or updating of a resource limit is quite widely used operation. 100 | 101 | For example: [systemd](https://github.com/systemd/systemd/blob/01a45898fce8def67d51332bccc410eb1e8710e7/src/core/main.c) 102 | 103 | ```C 104 | /* Don't limit the coredump size */ 105 | (void) setrlimit(RLIMIT_CORE, &RLIMIT_MAKE_CONST(RLIM_INFINITY)); 106 | ``` 107 | 108 | Or [haproxy](https://github.com/haproxy/haproxy/blob/25f067ccec52f53b0248a05caceb7841a3cb99df/src/haproxy.c): 109 | 110 | ```C 111 | getrlimit(RLIMIT_NOFILE, &limit); 112 | if (limit.rlim_cur < global.maxsock) { 113 | Warning("[%s.main()] FD limit (%d) too low for maxconn=%d/maxsock=%d. Please raise 'ulimit-n' to %d or more to avoid any trouble.\n", 114 | argv[0], (int)limit.rlim_cur, global.maxconn, global.maxsock, global.maxsock); 115 | } 116 | ``` 117 | 118 | We've just saw a little bit about resources limits related stuff in the userspace, now let's look at the same system calls in the Linux kernel. 119 | 120 | Limits on resource in the Linux kernel 121 | -------------------------------------------------------------------------------- 122 | 123 | Both implementation of `getrlimit` system call and `setrlimit` looks similar. Both they execute `do_prlimit` function that is core implementation of the `prlimit` system call and copy from/to given `rlimit` from/to userspace: 124 | 125 | The `getrlimit`: 126 | 127 | ```C 128 | SYSCALL_DEFINE2(getrlimit, unsigned int, resource, struct rlimit __user *, rlim) 129 | { 130 | struct rlimit value; 131 | int ret; 132 | 133 | ret = do_prlimit(current, resource, NULL, &value); 134 | if (!ret) 135 | ret = copy_to_user(rlim, &value, sizeof(*rlim)) ? -EFAULT : 0; 136 | 137 | return ret; 138 | } 139 | ``` 140 | 141 | and `setrlimit`: 142 | 143 | ```C 144 | SYSCALL_DEFINE2(setrlimit, unsigned int, resource, struct rlimit __user *, rlim) 145 | { 146 | struct rlimit new_rlim; 147 | 148 | if (copy_from_user(&new_rlim, rlim, sizeof(*rlim))) 149 | return -EFAULT; 150 | return do_prlimit(current, resource, &new_rlim, NULL); 151 | } 152 | ``` 153 | 154 | Implementations of these system calls are defined in the [kernel/sys.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/kernel/sys.c) kernel source code file. 155 | 156 | First of all the `do_prlimit` function executes a check that the given resource is valid: 157 | 158 | ```C 159 | if (resource >= RLIM_NLIMITS) 160 | return -EINVAL; 161 | ``` 162 | 163 | and in a failure case returns `-EINVAL` error. After this check will pass successfully and new limits was passed as non `NULL` value, two following checks: 164 | 165 | ```C 166 | if (new_rlim) { 167 | if (new_rlim->rlim_cur > new_rlim->rlim_max) 168 | return -EINVAL; 169 | if (resource == RLIMIT_NOFILE && 170 | new_rlim->rlim_max > sysctl_nr_open) 171 | return -EPERM; 172 | } 173 | ``` 174 | 175 | check that the given `soft` limit does not exceed `hard` limit and in a case when the given resource is the maximum number of a file descriptors that hard limit is not greater than `sysctl_nr_open` value. The value of the `sysctl_nr_open` can be found via [procfs](https://en.wikipedia.org/wiki/Procfs): 176 | 177 | ``` 178 | ~$ cat /proc/sys/fs/nr_open 179 | 1048576 180 | ``` 181 | 182 | After all of these checks we lock `tasklist` to be sure that [signal]() handlers related things will not be destroyed while we updating limits for a given resource: 183 | 184 | ```C 185 | read_lock(&tasklist_lock); 186 | ... 187 | ... 188 | ... 189 | read_unlock(&tasklist_lock); 190 | ``` 191 | 192 | We need to do this because `prlimit` system call allows us to update limits of another task by the given pid. As task list is locked, we take the `rlimit` instance that is responsible for the given resource limit of the given process: 193 | 194 | ```C 195 | rlim = tsk->signal->rlim + resource; 196 | ``` 197 | 198 | where the `tsk->signal->rlim` is just array of `struct rlimit` that represents certain resources. And if the `new_rlim` is not `NULL` we just update its value. If `old_rlim` is not `NULL` we fill it: 199 | 200 | ```C 201 | if (old_rlim) 202 | *old_rlim = *rlim; 203 | ``` 204 | 205 | That's all. 206 | 207 | Conclusion 208 | -------------------------------------------------------------------------------- 209 | 210 | This is the end of the second part that describes implementation of the system calls in the Linux kernel. If you have questions or suggestions, ping me on Twitter [0xAX](https://twitter.com/0xAX), drop me an [email](anotherworldofworld@gmail.com), or just create an [issue](https://github.com/0xAX/linux-internals/issues/new). 211 | 212 | **Please note that English is not my first language and I am really sorry for any inconvenience. If you find any mistakes please send me PR to [linux-insides](https://github.com/0xAX/linux-internals).** 213 | 214 | Links 215 | -------------------------------------------------------------------------------- 216 | 217 | * [system calls](https://en.wikipedia.org/wiki/System_call) 218 | * [PID](https://en.wikipedia.org/wiki/Process_identifier) 219 | * [ulimit](https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-ulimit) 220 | * [strace](https://linux.die.net/man/1/strace) 221 | * [POSIX message queues](http://man7.org/linux/man-pages/man7/mq_overview.7.html) 222 | -------------------------------------------------------------------------------- /TRANSLATION_NOTES.md: -------------------------------------------------------------------------------- 1 | # 翻译约定 2 | 3 | ## 基本要求 4 | 5 | * 准确表述原文的意思; 6 | * 中文应该意思清晰且符合中文表达习惯; 7 | * 原文如果表达不清晰,中文应该意译,并且应根据上下文和注释进行推断并填补相应的信息; 8 | * 情况 `3` 不能太多; 9 | * 对同样短语的翻译,前后必须一致; 10 | * 在没有其它影响的情况下,英文“you”统一翻译为“你”。如果原文中“you”的出现过于频繁,可以在译文中适当减少“你”,或将部分“你”用“我们”替代,但应考虑如下原则: 11 | 12 | * 保证句子主语明确、主谓一致。 13 | * “你”与“我们”的表述不必严格统一,优先考虑行文的流畅。 14 | * 某些可以根据上下文确定主语的场景,可以省略“你”或“我们”,但不宜滥用。 15 | 16 | * 不要使用机器翻译的成果来提交,也就是说您可以使用 `Google Translate` 来帮助您理解内容,但是不能不经考虑就把其自动翻译的结果放在翻译里; 17 | 18 | * 关于原文中作者声明英语不是他母语的一句话可以统一不作翻译。 19 | 20 | ## 格式要求 21 | 22 | ### 标点的使用 23 | 24 | 一般的原则是:除了小括号、省略号和破折号保留不变以外,都应该使用中文(全角)标点符号。英文标点符号后方常常跟随有一个半角空格,请在翻译成中文标点符号时将其去除。另外,标点两侧不应有空格,输出、引用除外。 25 | 26 | ### 关于空格 27 | 28 | 为了美观,通常建议在中文与英文、中文与阿拉伯数字、英文与阿拉伯数字之间加入一个半角空格。如果英文、数字的前后是标点,则不需再添加空格。例如: 29 | 30 | 英文:Installing driver for %1 31 | 中文:正在安装 %1 的驱动程序 32 | 33 | 英文: 34 | Parameter start_num specifies the character at which to start the search. 35 | The first character is character number 1. If start_num is omitted, it is 36 | assumed to be 1. 37 | 中文: 38 | 参数 start_num 指定开始搜索的字符位置。第一个字符序号为 1。如果省略 " 39 | "start_num,默认它为 1。" 40 | 41 | 不过,有时候有例如:“2013年6月5日”的视觉效果就比“2013 年 6 月 5 日”好。 42 | 43 | ### 其他 44 | 45 | * 所有的英文人名统一不译,保留原文。 46 | * 章节标题只能出现中文字样;如有必要说明术语原文,在正文中第一次出现处注明,不应重复。 47 | 48 | # 参见 49 | - [Ubuntu 简体中文小组工作指南](http://wiki.ubuntu.org.cn/Ubuntu_%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87%E5%B0%8F%E7%BB%84%E5%B7%A5%E4%BD%9C%E6%8C%87%E5%8D%97) 50 | - [Markdown基础](https://help.github.com/articles/markdown-basics/) 51 | - [Markdown 语法说明 (简体中文版)](http://wowubuntu.com/markdown/index.html) 52 | -------------------------------------------------------------------------------- /Theory/ELF.md: -------------------------------------------------------------------------------- 1 | ELF文件格式 2 | ================================================================================ 3 | 4 | ELF (Executable and Linkable Format)是一种为可执行文件,目标文件,共享链接库和内核转储(core dumps)准备的标准文件格式。 Linux和很多类Unix操作系统都使用这个格式。 让我们来看一下64位ELF文件格式的结构以及内核源码中有关于它的一些定义。 5 | 6 | 一个ELF文件由以下三部分组成: 7 | 8 | * ELF头(ELF header) - 描述文件的主要特性:类型,CPU架构,入口地址,现有部分的大小和偏移等等; 9 | 10 | * 程序头表(Program header table) - 列举了所有有效的段(segments)和他们的属性。 程序头表需要加载器将文件中的节加载到虚拟内存段中; 11 | 12 | * 节头表(Section header table) - 包含对节(sections)的描述。 13 | 14 | 现在让我们对这些部分有一些更深的了解。 15 | 16 | **ELF头(ELF header)** 17 | 18 | ELF头(ELF header)位于文件的开始位置。 它的主要目的是定位文件的其他部分。 文件头主要包含以下字段: 19 | 20 | * ELF文件鉴定 - 一个字节数组用来确认文件是否是一个ELF文件,并且提供普通文件特征的信息; 21 | * 文件类型 - 确定文件类型。 这个字段描述文件是一个重定位文件,或可执行文件,或...; 22 | * 目标结构; 23 | * ELF文件格式的版本; 24 | * 程序入口地址; 25 | * 程序头表的文件偏移; 26 | * 节头表的文件偏移; 27 | * ELF头(ELF header)的大小; 28 | * 程序头表的表项大小; 29 | * 其他字段... 30 | 31 | 你可以在内核源码种找到表示ELF64 header的结构体 `elf64_hdr`: 32 | 33 | ```C 34 | typedef struct elf64_hdr { 35 | unsigned char e_ident[EI_NIDENT]; 36 | Elf64_Half e_type; 37 | Elf64_Half e_machine; 38 | Elf64_Word e_version; 39 | Elf64_Addr e_entry; 40 | Elf64_Off e_phoff; 41 | Elf64_Off e_shoff; 42 | Elf64_Word e_flags; 43 | Elf64_Half e_ehsize; 44 | Elf64_Half e_phentsize; 45 | Elf64_Half e_phnum; 46 | Elf64_Half e_shentsize; 47 | Elf64_Half e_shnum; 48 | Elf64_Half e_shstrndx; 49 | } Elf64_Ehdr; 50 | ``` 51 | 52 | 这个结构体定义在 [elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#L220) 53 | 54 | **节(sections)** 55 | 56 | 所有的数据都存储在ELF文件的节(sections)中。 我们通过节头表中的索引(index)来确认节(sections)。 节头表表项包含以下字段: 57 | 58 | * 节的名字; 59 | * 节的类型; 60 | * 节的属性; 61 | * 内存地址; 62 | * 文件中的偏移; 63 | * 节的大小; 64 | * 到其他节的链接; 65 | * 各种各样的信息; 66 | * 地址对齐; 67 | * 这个表项的大小,如果有的话; 68 | 69 | 而且,在linux内核中结构体 `elf64_shdr` 如下所示: 70 | 71 | ```C 72 | typedef struct elf64_shdr { 73 | Elf64_Word sh_name; 74 | Elf64_Word sh_type; 75 | Elf64_Xword sh_flags; 76 | Elf64_Addr sh_addr; 77 | Elf64_Off sh_offset; 78 | Elf64_Xword sh_size; 79 | Elf64_Word sh_link; 80 | Elf64_Word sh_info; 81 | Elf64_Xword sh_addralign; 82 | Elf64_Xword sh_entsize; 83 | } Elf64_Shdr; 84 | ``` 85 | 86 | [elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#L312) 87 | 88 | **程序头表(Program header table)** 89 | 90 | 在可执行文件或者共享链接库中所有的节(sections)都被分为多个段(segments)。 程序头是一个结构的数组,每一个结构都表示一个段(segments)。 它的结构就像这样: 91 | 92 | ```C 93 | typedef struct elf64_phdr { 94 | Elf64_Word p_type; 95 | Elf64_Word p_flags; 96 | Elf64_Off p_offset; 97 | Elf64_Addr p_vaddr; 98 | Elf64_Addr p_paddr; 99 | Elf64_Xword p_filesz; 100 | Elf64_Xword p_memsz; 101 | Elf64_Xword p_align; 102 | } Elf64_Phdr; 103 | ``` 104 | 105 | 在内核源码中。 106 | 107 | `elf64_phdr` 定义在相同的 [elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#L254) 文件中. 108 | 109 | EFL文件也包含其他的字段或结构。 你可以在 [Documentation](http://www.uclibc.org/docs/elf-64-gen.pdf) 中查看。 现在我们来查看一下 `vmlinux` 这个ELF文件。 110 | 111 | vmlinux 112 | -------------------------------------------------------------------------------- 113 | 114 | `vmlinux` 也是一个可重定位的ELF文件。 我们可以使用 `readelf` 工具来查看它。 首先,让我们看一下它的头部: 115 | 116 | ``` 117 | $ readelf -h vmlinux 118 | ELF Header: 119 | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 120 | Class: ELF64 121 | Data: 2's complement, little endian 122 | Version: 1 (current) 123 | OS/ABI: UNIX - System V 124 | ABI Version: 0 125 | Type: EXEC (Executable file) 126 | Machine: Advanced Micro Devices X86-64 127 | Version: 0x1 128 | Entry point address: 0x1000000 129 | Start of program headers: 64 (bytes into file) 130 | Start of section headers: 381608416 (bytes into file) 131 | Flags: 0x0 132 | Size of this header: 64 (bytes) 133 | Size of program headers: 56 (bytes) 134 | Number of program headers: 5 135 | Size of section headers: 64 (bytes) 136 | Number of section headers: 73 137 | Section header string table index: 70 138 | ``` 139 | 140 | 我们可以看出 `vmlinux` 是一个64位可执行文件。 141 | 我们可以从 [Documentation/x86/x86_64/mm.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt#L19) 读到相关信息: 142 | 143 | ``` 144 | ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0 145 | ``` 146 | 147 | 之后我们可以在 `vmlinux` ELF文件中查看这个地址: 148 | 149 | ``` 150 | $ readelf -s vmlinux | grep ffffffff81000000 151 | 1: ffffffff81000000 0 SECTION LOCAL DEFAULT 1 152 | 65099: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 _text 153 | 90766: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 startup_64 154 | ``` 155 | 156 | 值得注意的是,`startup_64` 例程的地址不是 `ffffffff80000000`, 而是 `ffffffff81000000`。 现在我们来解释一下。 157 | 158 | 我们可以在 [arch/x86/kernel/vmlinux.lds.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/vmlinux.lds.S) 看见如下的定义 : 159 | 160 | ``` 161 | . = __START_KERNEL; 162 | ... 163 | ... 164 | .. 165 | /* Text and read-only data */ 166 | .text : AT(ADDR(.text) - LOAD_OFFSET) { 167 | _text = .; 168 | ... 169 | ... 170 | ... 171 | } 172 | ``` 173 | 174 | 其中,`__START_KERNEL` 定义如下: 175 | 176 | ``` 177 | #define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START) 178 | ``` 179 | 180 | 从这个文档中看出,`__START_KERNEL_map` 的值是 `ffffffff80000000` 以及 `__PHYSICAL_START` 的值是 `0x1000000`。 这就是 `startup_64`的地址是 `ffffffff81000000`的原因了。 181 | 182 | 最后我们通过以下命令来得到程序头表的内容: 183 | 184 | ``` 185 | readelf -l vmlinux 186 | 187 | Elf file type is EXEC (Executable file) 188 | Entry point 0x1000000 189 | There are 5 program headers, starting at offset 64 190 | 191 | Program Headers: 192 | Type Offset VirtAddr PhysAddr 193 | FileSiz MemSiz Flags Align 194 | LOAD 0x0000000000200000 0xffffffff81000000 0x0000000001000000 195 | 0x0000000000cfd000 0x0000000000cfd000 R E 200000 196 | LOAD 0x0000000001000000 0xffffffff81e00000 0x0000000001e00000 197 | 0x0000000000100000 0x0000000000100000 RW 200000 198 | LOAD 0x0000000001200000 0x0000000000000000 0x0000000001f00000 199 | 0x0000000000014d98 0x0000000000014d98 RW 200000 200 | LOAD 0x0000000001315000 0xffffffff81f15000 0x0000000001f15000 201 | 0x000000000011d000 0x0000000000279000 RWE 200000 202 | NOTE 0x0000000000b17284 0xffffffff81917284 0x0000000001917284 203 | 0x0000000000000024 0x0000000000000024 4 204 | 205 | Section to Segment mapping: 206 | Segment Sections... 207 | 00 .text .notes __ex_table .rodata __bug_table .pci_fixup .builtin_fw 208 | .tracedata __ksymtab __ksymtab_gpl __kcrctab __kcrctab_gpl 209 | __ksymtab_strings __param __modver 210 | 01 .data .vvar 211 | 02 .data..percpu 212 | 03 .init.text .init.data .x86_cpu_dev.init .altinstructions 213 | .altinstr_replacement .iommu_table .apicdrivers .exit.text 214 | .smp_locks .data_nosave .bss .brk 215 | ``` 216 | 217 | 这里我们可以看出五个包含节(sections)列表的段(segments)。 你可以在生成的链接器脚本 - `arch/x86/kernel/vmlinux.lds` 中找到所有的节(sections)。 218 | 219 | 就这样吧。 当然,它不是ELF(Executable and Linkable Format)的完整描述,但是如果你想要知道更多,可以参考这个文档 - [这里](http://www.uclibc.org/docs/elf-64-gen.pdf) 220 | -------------------------------------------------------------------------------- /Theory/README.md: -------------------------------------------------------------------------------- 1 | # 理论 2 | 3 | 这一章描述各种理论性概念和那些不直接涉及实践,但是知道了会很有用的概念。 4 | 5 | * [分页](linux-theory-1.md) 6 | * [Elf64 格式](linux-theory-2.md) 7 | * [內联汇编](linux-theory-3.md) 8 | -------------------------------------------------------------------------------- /Theory/linux-theory-1.md: -------------------------------------------------------------------------------- 1 | 分页 2 | ================================================================================ 3 | 4 | 简介 5 | -------------------------------------------------------------------------------- 6 | 7 | 在 Linux 内核启动过程中的[第五部分](https://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-5.html),我们学到了内核在启动的最早阶段都做了哪些工作。接下来,在我们明白内核如何运行第一个 init 进程之前,内核初始化其他部分,比如加载 `initrd` ,初始化 lockdep ,以及许多许多其他的工作。 8 | 9 | 是的,那将有很多不同的事,但是还有更多更多更多关于**内存**的工作。 10 | 11 | 在我看来,一般而言,内存管理是 Linux 内核和系统编程最复杂的部分之一。这就是为什么在我们学习内核初始化过程之前,需要了解分页。 12 | 13 | 分页是将线性地址转换为物理地址的机制。如果我们已经读过了这本书之前的部分,你可能记得我们在实模式下有分段机制,当时物理地址是由左移四位段寄存器加上偏移算出来的。我们也看了保护模式下的分段机制,其中我们使用描述符表得到描述符,进而得到基地址,然后加上偏移地址就获得了实际物理地址。由于我们在 64 位模式,我们将看分页机制。 14 | 15 | 正如 Intel 手册中说的: 16 | 17 | > 分页机制提供一种机制,为了实现常见的按需分页,比如虚拟内存系统就是将一个程序执行环境中的段按照需求被映射到物理地址。 18 | 19 | 所以... 在这个帖子中我将尝试解释分页背后的理论。当然它将与64位版本的 Linux 内核关系密切,但是我们将不会深入太多细节(至少在这个帖子里面)。 20 | 21 | 开启分页 22 | -------------------------------------------------------------------------------- 23 | 24 | 有三种分页模式: 25 | 26 | * 32 位分页模式; 27 | * PAE 分页; 28 | * IA-32e 分页。 29 | 30 | 我们这里将只解释最后一种模式。为了开启 `IA-32e 分页模式`,我们需要做如下事情: 31 | 32 | * 设置 `CR0.PG` 位; 33 | * 设置 `CR4.PAE` 位; 34 | * 设置 `IA32_EFER.LME` 位。 35 | 36 | 我们已经在 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 中看见了这些位被设置了: 37 | 38 | ```assembly 39 | movl $(X86_CR0_PG | X86_CR0_PE), %eax 40 | movl %eax, %cr0 41 | ``` 42 | 43 | and 44 | 45 | ```assembly 46 | movl $MSR_EFER, %ecx 47 | rdmsr 48 | btsl $_EFER_LME, %eax 49 | wrmsr 50 | ``` 51 | 52 | 分页数据结构 53 | -------------------------------------------------------------------------------- 54 | 55 | 分页将线性地址分为固定尺寸的页。页会被映射进入物理地址空间或外部存储设备。这个固定尺寸在 `x86_64` 内核中是 `4096` 字节。为了将线性地址转换位物理地址,需要使用到一些特殊的数据结构。每个结构都是 `4096` 字节并包含 `512` 项(这只为 `PAE` 和 `IA32_EFER.LME` 模式)。分页结构是层次级的, Linux 内核在 `x86_64` 框架中使用4层的分层机制。CPU使用一部分线性地址去确定另一个分页结构中的项,这个分页结构可能在最低层,物理内存区域(页框),在这个区域的物理地址(页偏移)。最高层的分页结构的地址存储在 `cr3` 寄存器中。我们已经从 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 这个文件中已经看到了。 56 | 57 | ```assembly 58 | leal pgtable(%ebx), %eax 59 | movl %eax, %cr3 60 | ``` 61 | 62 | 我们构建页表结构并且将这个最高层结构的地址存放在 `cr3` 寄存器中。这里 `cr3` 用于存储最高层结构的地址,在 Linux 内核中被称为 `PML4` 或 `Page Global Directory` 。 `cr3` 是一个64位的寄存器,并且有着如下的结构: 63 | 64 | ``` 65 | 63 52 51 32 66 | -------------------------------------------------------------------------------- 67 | | | | 68 | | Reserved MBZ | Address of the top level structure | 69 | | | | 70 | -------------------------------------------------------------------------------- 71 | 31 12 11 5 4 3 2 0 72 | -------------------------------------------------------------------------------- 73 | | | | P | P | | 74 | | Address of the top level structure | Reserved | C | W | Reserved | 75 | | | | D | T | | 76 | -------------------------------------------------------------------------------- 77 | ``` 78 | 79 | 这些字段有着如下的意义: 80 | 81 | * 第 0 到第 2 位 - 忽略; 82 | * 第 12 位到第 51 位 - 存储最高层分页结构的地址; 83 | * 第 3 位 到第 4 位 - PWT 或 Page-Level Writethrough 和 PCD 或 Page-level Cache Disable 显示。这些位控制页或者页表被硬件缓存处理的方式; 84 | * 保留位 - 保留,但必须为 0 ; 85 | * 第 52 到第 63 位 - 保留,但必须为 0 ; 86 | 87 | 线性地址转换过程如下所示: 88 | 89 | * 一个给定的线性地址传递给 [MMU](http://en.wikipedia.org/wiki/Memory_management_unit) 而不是存储器总线; 90 | * 64位线性地址分为很多部分。只有低 48 位是有意义的,它意味着 `2^48` 或 256TB 的线性地址空间在任意给定时间内都可以被访问; 91 | * `cr3` 寄存器存储这个最高层分页数据结构的地址; 92 | * 给定的线性地址中的第 39 位到第 47 位存储一个第 4 级分页结构的索引,第 30 位到第 38 位存储一个第3级分页结构的索引,第 29 位到第 21 位存储一个第 2 级分页结构的索引,第 12 位到第 20 位存储一个第 1 级分页结构的索引,第 0 位到第 11 位提供物理页的字节偏移; 93 | 94 | 按照图示,我们可以这样想象它: 95 | 96 | ![四层分页](http://oi58.tinypic.com/207mb0x.jpg) 97 | 98 | 每一个对线性地址的访问不是一个管态访问就是用户态访问。这个访问是被 `CPL (Current Privilege Level)` 所决定。如果 `CPL < 3` ,那么它是管态访问级,否则,它就是用户态访问级。比如,最高级页表项包含访问位和如下的结构: 99 | 100 | ``` 101 | 63 62 52 51 32 102 | -------------------------------------------------------------------------------- 103 | | N | | | 104 | | | Available | Address of the paging structure on lower level | 105 | | X | | | 106 | -------------------------------------------------------------------------------- 107 | 31 12 11 9 8 7 6 5 4 3 2 1 0 108 | -------------------------------------------------------------------------------- 109 | | | | M |I| | P | P |U|W| | 110 | | Address of the paging structure on lower level | AVL | B |G|A| C | W | | | P | 111 | | | | Z |N| | D | T |S|R| | 112 | -------------------------------------------------------------------------------- 113 | ``` 114 | 115 | 其中: 116 | 117 | * 第 63 位 - N/X 位(不可执行位)显示被这个页表项映射的所有物理页执行代码的能力; 118 | * 第 52 位到第 62 位 - 被CPU忽略,被系统软件使用; 119 | * 第 12 位到第 51 位 - 存储低级分页结构的物理地址; 120 | * 第 9 位到第 11 位 - 被 CPU 忽略; 121 | * MBZ - 必须为 0 ; 122 | * 忽略位; 123 | * A - 访问位暗示物理页或者页结构被访问; 124 | * PWT 和 PCD 用于缓存; 125 | * U/S - 用户/管理位控制对被这个页表项映射的所有物理页用户访问; 126 | * R/W - 读写位控制着被这个页表项映射的所有物理页的读写权限 127 | * P - 存在位。当前位表示页表或物理页是否被加载进内存; 128 | 129 | 好的,我们知道了分页结构和它们的表项。现在我们来看一下 Linux 内核中的 4 级分页机制的一些细节。 130 | 131 | Linux 内核中的分页结构 132 | -------------------------------------------------------------------------------- 133 | 134 | 就如我们已经看到的那样, `x86_64`Linux 内核使用4级页表。它们的名字是: 135 | 136 | * 全局页目录 137 | * 上层页目录 138 | * 中间页目录 139 | * 页表项 140 | 141 | 在你已经编译和安装 Linux 内核之后,你可以看到保存了内核函数的虚拟地址的文件 `System.map`。例如: 142 | 143 | ``` 144 | $ grep "start_kernel" System.map 145 | ffffffff81efe497 T x86_64_start_kernel 146 | ffffffff81efeaa2 T start_kernel 147 | ``` 148 | 149 | 这里我们可以看见 `0xffffffff81efe497` 。我怀疑你是否真的有安装这么多内存。但是无论如何, `start_kernel` 和 `x86_64_start_kernel` 将会被执行。在 `x86_64` 中,地址空间的大小是 `2^64` ,但是它太大了,这就是为什么我们使用一个较小的地址空间,只是 48 位的宽度。所以一个情况出现,虽然物理地址空间限制到 48 位,但是寻址仍然使用 64 位指针。 这个问题是如何解决的?看下面的这个表。 150 | 151 | ``` 152 | 0xffffffffffffffff +-----------+ 153 | | | 154 | | | Kernelspace 155 | | | 156 | 0xffff800000000000 +-----------+ 157 | | | 158 | | | 159 | | hole | 160 | | | 161 | | | 162 | 0x00007fffffffffff +-----------+ 163 | | | 164 | | | Userspace 165 | | | 166 | 0x0000000000000000  +-----------+ 167 | ``` 168 | 169 | 这个解决方案是 `sign extension` 。这里我们可以看到一个虚拟地址的低 48 位可以被用于寻址。第 48 位到第 63 位全是 0 或 1 。注意这个虚拟地址空间被分为两部分: 170 | 171 | * 内核空间 172 | * 用户空间 173 | 174 | 用户空间占用虚拟地址空间的低部分,从 `0x000000000000000` 到 `0x00007fffffffffff` ,而内核空间占据从 `0xffff8000000000` 到 `0xffffffffffffffff` 的高部分。注意,第 48 位到第 63 位是对于用户空间是 0 ,对于内核空间是 1 。内核空间和用户空间中的所有地址是标准地址,而在这些内存区域中间有非标准区域。这两块内存区域(内核空间和用户空间)合起来是 48 位宽度。我们可以在 [Documentation/x86/x86_64/mm.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt) 找到 4 级页表下的虚拟内存映射: 175 | 176 | ``` 177 | 0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm 178 | hole caused by [48:63] sign extension 179 | ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor 180 | ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory 181 | ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole 182 | ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space 183 | ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole 184 | ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB) 185 | ... unused hole ... 186 | ffffec0000000000 - fffffc0000000000 (=44 bits) kasan shadow memory (16TB) 187 | ... unused hole ... 188 | ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks 189 | ... unused hole ... 190 | ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0 191 | ffffffffa0000000 - ffffffffff5fffff (=1525 MB) module mapping space 192 | ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls 193 | ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole 194 | ``` 195 | 196 | 这里我们可以看到用户空间,内核空间和非标准空间的内存映射。用户空间的内存映射很简单。让我们来更近地查看内核空间。我们可以看到它始于为管理程序 (hypervisor) 保留的防御空洞 (guard hole) 。我们可以在 [arch/x86/include/asm/page_64_types.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/page_64_types.h) 这个文件中看到防御空洞的概念! 197 | 198 | ```C 199 | #define __PAGE_OFFSET _AC(0xffff880000000000, UL) 200 | ``` 201 | 202 | 以前防御空洞和 `__PAGE_OFFSET` 是从 `0xffff800000000000` 到 `0xffff80ffffffffff` ,用来防止对非标准区域的访问,但是后来为了管理程序扩展了 3 位。 203 | 204 | 紧接着是内核空间中最低的可用空间 - `ffff880000000000` 。这个虚拟地址空间是为了所有的物理内存的直接映射。在这块空间之后,还是防御空洞。它位于所有物理内存的直接映射地址和被 vmalloc 分配的地址之间。在第一个 1TB 的虚拟内存映射和无用的空洞之后,我们可以看到 `ksan` 影子内存 (shadow memory) 。它是通过 [commit](https://github.com/torvalds/linux/commit/ef7f0d6a6ca8c9e4b27d78895af86c2fbfaeedb2) 提交到内核中,并且保持内核空间无害。在紧接着的无用空洞之后,我们可以看到 `esp` 固定栈(我们会在本书其他部分讨论它)。内核代码段的开始从物理地址 - `0` 映射。我们可以在相同的文件中找到将这个地址定义为 `__PAGE_OFFSET` 。 205 | 206 | ```C 207 | #define __START_KERNEL_map _AC(0xffffffff80000000, UL) 208 | ``` 209 | 210 | 通常内核的 `.text` 段开始于 `CONFIG_PHYSICAL_START` 偏移。我们已经在 [ELF64](https://github.com/MintCN/linux-insides-zh/blob/master/Theory/ELF.md) 相关帖子中看见。 211 | 212 | ``` 213 | readelf -s vmlinux | grep ffffffff81000000 214 | 1: ffffffff81000000 0 SECTION LOCAL DEFAULT 1 215 | 65099: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 _text 216 | 90766: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 startup_64 217 | ``` 218 | 219 | 这里我将 `CONFIG_PHYSICAL_START` 设置为 `0x1000000` 来检查 `vmlinux` 。所以我们有内核代码段的起始点 - `0xffffffff80000000` 和 偏移 - `0x1000000` ,计算出来的虚拟地址将会是 `0xffffffff80000000 + 1000000 = 0xffffffff81000000` 。 220 | 221 | 在内核代码段之后有一个为内核模块 `vsyscalls` 准备的虚拟内存区域和 2M 无用的空洞。 222 | 223 | 我们已经看见内核虚拟内存映射是如何布局的以及虚拟地址是如何转换位物理地址。让我们以下面的地址为例: 224 | 225 | ``` 226 | 0xffffffff81000000 227 | ``` 228 | 229 | 在二进制内它将是: 230 | 231 | ``` 232 | 1111111111111111 111111111 111111110 000001000 000000000 000000000000 233 | 63:48 47:39 38:30 29:21 20:12 11:0 234 | ``` 235 | 236 | 这个虚拟地址将被分为如下描述的几部分: 237 | 238 | * `48-63` - 不使用的位; 239 | * `37-49` - 给定线性地址的这些位描述一个 4 级分页结构的索引; 240 | * `30-38` - 这些位存储一个 3 级分页结构的索引; 241 | * `21-29` - 这些位存储一个 2 级分页结构的索引; 242 | * `12-20` - 这些位存储一个 1 级分页结构的索引; 243 | * `0-11` - 这些位提供物理页的偏移; 244 | 245 | 246 | 就这样了。现在你知道了一些关于分页理论,而且我们可以在内核源码上更近一步,查看那些最先的初始化步骤。 247 | 248 | 总结 249 | -------------------------------------------------------------------------------- 250 | 251 | 这简短的关于分页理论的部分至此已经结束了。当然,这个帖子不可能包含分页的所有细节,但是我们很快会看到在实践中 Linux 内核如何构建分页结构以及使用它们工作。 252 | 253 | 链接 254 | -------------------------------------------------------------------------------- 255 | 256 | * [Paging on Wikipedia](http://en.wikipedia.org/wiki/Paging) 257 | * [Intel 64 and IA-32 architectures software developer's manual volume 3A](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html) 258 | * [MMU](http://en.wikipedia.org/wiki/Memory_management_unit) 259 | * [ELF64](https://github.com/0xAX/linux-insides/blob/master/Theory/ELF.md) 260 | * [Documentation/x86/x86_64/mm.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt) 261 | * [Last part - Kernel booting process](http://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-5.html) 262 | -------------------------------------------------------------------------------- /Theory/linux-theory-2.md: -------------------------------------------------------------------------------- 1 | ELF文件格式 2 | ================================================================================ 3 | 4 | ELF (Executable and Linkable Format)是一种为可执行文件,目标文件,共享链接库和内核转储(core dumps)准备的标准文件格式。 Linux和很多类Unix操作系统都使用这个格式。 让我们来看一下64位ELF文件格式的结构以及内核源码中有关于它的一些定义。 5 | 6 | 一个ELF文件由以下三部分组成: 7 | 8 | * ELF头(ELF header) - 描述文件的主要特性:类型,CPU架构,入口地址,现有部分的大小和偏移等等; 9 | 10 | * 程序头表(Program header table) - 列举了所有有效的段(segments)和他们的属性。 程序头表需要加载器将文件中的节加载到虚拟内存段中; 11 | 12 | * 节头表(Section header table) - 包含对节(sections)的描述。 13 | 14 | 现在让我们对这些部分有一些更深的了解。 15 | 16 | **ELF头(ELF header)** 17 | 18 | ELF头(ELF header)位于文件的开始位置。 它的主要目的是定位文件的其他部分。 文件头主要包含以下字段: 19 | 20 | * ELF文件鉴定 - 一个字节数组用来确认文件是否是一个ELF文件,并且提供普通文件特征的信息; 21 | * 文件类型 - 确定文件类型。 这个字段描述文件是一个重定位文件,或可执行文件,或...; 22 | * 目标结构; 23 | * ELF文件格式的版本; 24 | * 程序入口地址; 25 | * 程序头表的文件偏移; 26 | * 节头表的文件偏移; 27 | * ELF头(ELF header)的大小; 28 | * 程序头表的表项大小; 29 | * 其他字段... 30 | 31 | 你可以在内核源码种找到表示ELF64 header的结构体 `elf64_hdr`: 32 | 33 | ```C 34 | typedef struct elf64_hdr { 35 | unsigned char e_ident[EI_NIDENT]; 36 | Elf64_Half e_type; 37 | Elf64_Half e_machine; 38 | Elf64_Word e_version; 39 | Elf64_Addr e_entry; 40 | Elf64_Off e_phoff; 41 | Elf64_Off e_shoff; 42 | Elf64_Word e_flags; 43 | Elf64_Half e_ehsize; 44 | Elf64_Half e_phentsize; 45 | Elf64_Half e_phnum; 46 | Elf64_Half e_shentsize; 47 | Elf64_Half e_shnum; 48 | Elf64_Half e_shstrndx; 49 | } Elf64_Ehdr; 50 | ``` 51 | 52 | 这个结构体定义在 [elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#L220) 53 | 54 | **节(sections)** 55 | 56 | 所有的数据都存储在ELF文件的节(sections)中。 我们通过节头表中的索引(index)来确认节(sections)。 节头表表项包含以下字段: 57 | 58 | * 节的名字; 59 | * 节的类型; 60 | * 节的属性; 61 | * 内存地址; 62 | * 文件中的偏移; 63 | * 节的大小; 64 | * 到其他节的链接; 65 | * 各种各样的信息; 66 | * 地址对齐; 67 | * 这个表项的大小,如果有的话; 68 | 69 | 而且,在linux内核中结构体 `elf64_shdr` 如下所示: 70 | 71 | ```C 72 | typedef struct elf64_shdr { 73 | Elf64_Word sh_name; 74 | Elf64_Word sh_type; 75 | Elf64_Xword sh_flags; 76 | Elf64_Addr sh_addr; 77 | Elf64_Off sh_offset; 78 | Elf64_Xword sh_size; 79 | Elf64_Word sh_link; 80 | Elf64_Word sh_info; 81 | Elf64_Xword sh_addralign; 82 | Elf64_Xword sh_entsize; 83 | } Elf64_Shdr; 84 | ``` 85 | 86 | [elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#L312) 87 | 88 | **程序头表(Program header table)** 89 | 90 | 在可执行文件或者共享链接库中所有的节(sections)都被分为多个段(segments)。 程序头是一个结构的数组,每一个结构都表示一个段(segments)。 它的结构就像这样: 91 | 92 | ```C 93 | typedef struct elf64_phdr { 94 | Elf64_Word p_type; 95 | Elf64_Word p_flags; 96 | Elf64_Off p_offset; 97 | Elf64_Addr p_vaddr; 98 | Elf64_Addr p_paddr; 99 | Elf64_Xword p_filesz; 100 | Elf64_Xword p_memsz; 101 | Elf64_Xword p_align; 102 | } Elf64_Phdr; 103 | ``` 104 | 105 | 在内核源码中。 106 | 107 | `elf64_phdr` 定义在相同的 [elf.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h#L254) 文件中. 108 | 109 | EFL文件也包含其他的字段或结构。 你可以在 [Documentation](http://www.uclibc.org/docs/elf-64-gen.pdf) 中查看。 现在我们来查看一下 `vmlinux` 这个ELF文件。 110 | 111 | vmlinux 112 | -------------------------------------------------------------------------------- 113 | 114 | `vmlinux` 也是一个可重定位的ELF文件。 我们可以使用 `readelf` 工具来查看它。 首先,让我们看一下它的头部: 115 | 116 | ``` 117 | $ readelf -h vmlinux 118 | ELF Header: 119 | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 120 | Class: ELF64 121 | Data: 2's complement, little endian 122 | Version: 1 (current) 123 | OS/ABI: UNIX - System V 124 | ABI Version: 0 125 | Type: EXEC (Executable file) 126 | Machine: Advanced Micro Devices X86-64 127 | Version: 0x1 128 | Entry point address: 0x1000000 129 | Start of program headers: 64 (bytes into file) 130 | Start of section headers: 381608416 (bytes into file) 131 | Flags: 0x0 132 | Size of this header: 64 (bytes) 133 | Size of program headers: 56 (bytes) 134 | Number of program headers: 5 135 | Size of section headers: 64 (bytes) 136 | Number of section headers: 73 137 | Section header string table index: 70 138 | ``` 139 | 140 | 我们可以看出 `vmlinux` 是一个64位可执行文件。 141 | 我们可以从 [Documentation/x86/x86_64/mm.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt#L19) 读到相关信息: 142 | 143 | ``` 144 | ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0 145 | ``` 146 | 147 | 之后我们可以在 `vmlinux` ELF文件中查看这个地址: 148 | 149 | ``` 150 | $ readelf -s vmlinux | grep ffffffff81000000 151 | 1: ffffffff81000000 0 SECTION LOCAL DEFAULT 1 152 | 65099: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 _text 153 | 90766: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 startup_64 154 | ``` 155 | 156 | 值得注意的是,`startup_64` 例程的地址不是 `ffffffff80000000`, 而是 `ffffffff81000000`。 现在我们来解释一下。 157 | 158 | 我们可以在 [arch/x86/kernel/vmlinux.lds.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/vmlinux.lds.S) 看见如下的定义 : 159 | 160 | ``` 161 | . = __START_KERNEL; 162 | ... 163 | ... 164 | .. 165 | /* Text and read-only data */ 166 | .text : AT(ADDR(.text) - LOAD_OFFSET) { 167 | _text = .; 168 | ... 169 | ... 170 | ... 171 | } 172 | ``` 173 | 174 | 其中,`__START_KERNEL` 定义如下: 175 | 176 | ``` 177 | #define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START) 178 | ``` 179 | 180 | 从这个文档中看出,`__START_KERNEL_map` 的值是 `ffffffff80000000` 以及 `__PHYSICAL_START` 的值是 `0x1000000`。 这就是 `startup_64`的地址是 `ffffffff81000000`的原因了。 181 | 182 | 最后我们通过以下命令来得到程序头表的内容: 183 | 184 | ``` 185 | readelf -l vmlinux 186 | 187 | Elf file type is EXEC (Executable file) 188 | Entry point 0x1000000 189 | There are 5 program headers, starting at offset 64 190 | 191 | Program Headers: 192 | Type Offset VirtAddr PhysAddr 193 | FileSiz MemSiz Flags Align 194 | LOAD 0x0000000000200000 0xffffffff81000000 0x0000000001000000 195 | 0x0000000000cfd000 0x0000000000cfd000 R E 200000 196 | LOAD 0x0000000001000000 0xffffffff81e00000 0x0000000001e00000 197 | 0x0000000000100000 0x0000000000100000 RW 200000 198 | LOAD 0x0000000001200000 0x0000000000000000 0x0000000001f00000 199 | 0x0000000000014d98 0x0000000000014d98 RW 200000 200 | LOAD 0x0000000001315000 0xffffffff81f15000 0x0000000001f15000 201 | 0x000000000011d000 0x0000000000279000 RWE 200000 202 | NOTE 0x0000000000b17284 0xffffffff81917284 0x0000000001917284 203 | 0x0000000000000024 0x0000000000000024 4 204 | 205 | Section to Segment mapping: 206 | Segment Sections... 207 | 00 .text .notes __ex_table .rodata __bug_table .pci_fixup .builtin_fw 208 | .tracedata __ksymtab __ksymtab_gpl __kcrctab __kcrctab_gpl 209 | __ksymtab_strings __param __modver 210 | 01 .data .vvar 211 | 02 .data..percpu 212 | 03 .init.text .init.data .x86_cpu_dev.init .altinstructions 213 | .altinstr_replacement .iommu_table .apicdrivers .exit.text 214 | .smp_locks .data_nosave .bss .brk 215 | ``` 216 | 217 | 这里我们可以看出五个包含节(sections)列表的段(segments)。 你可以在生成的链接器脚本 - `arch/x86/kernel/vmlinux.lds` 中找到所有的节(sections)。 218 | 219 | 就这样吧。 当然,它不是ELF(Executable and Linkable Format)的完整描述,但是如果你想要知道更多,可以参考这个文档 - [这里](http://www.uclibc.org/docs/elf-64-gen.pdf) 220 | -------------------------------------------------------------------------------- /Timers/README.md: -------------------------------------------------------------------------------- 1 | # 定时器和时钟管理 2 | 3 | 本章介绍 Linux 内核中定时器和时钟管理相关的观念。 4 | 5 | * [简介](linux-timers-1.md) - 简单介绍 Linux 内核中的定时器。 6 | * [时钟源框架简介](linux-timers-2.md) - this part describes `clocksource` framework in the Linux kernel. 7 | * [The tick broadcast framework and dyntick](linux-timers-3.md) - 介绍 tick broadcast framework and dyntick 概念。 8 | * [定时器介绍](linux-timers-4.md) - 介绍 Linux 内核中的定时器。 9 | * [Clockevents 框架简介](linux-timers-5.md) - 介绍另外一个时钟管理相关的框架 : `clockevents`. 10 | * [x86 相关的时钟源](linux-timers-6.md) - 介绍 `x86_64` 相关的时钟源。 11 | * [Linux 内核中与时钟相关的系统调用](linux-timers-7.md) - 介绍时钟相关的系统调用。 12 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinqiu/linux-insides/a11b7251ac937af4a8934c2d36288c9859ba9f4e/cover.jpg --------------------------------------------------------------------------------