├── README.md ├── SUMMARY.md ├── alipay.gif ├── bootup ├── 00_index.md ├── 01_init_call.md └── 02_command_line_parse.md ├── brief_tutorial_on_kbuild ├── 00_index.md ├── 01_build_your_first_kernel.md ├── 02_common_targets_in_kernel.md ├── 03_first_target_help.md ├── 04_one_example_of_kbuild_function_cscope.md ├── 05_rules_for_single_object.md ├── 06_building_vmlinux_under_root.md ├── 07_rules_for_bzImage.md ├── 08_rule_for_setupbin.md ├── 09_rule_for_vmlinux_bin.md ├── 10_those_buddy_in_kbuild.md ├── 11_location_of_common_targets.md ├── 13_root_makefile.md ├── 14_bzImage_whole_picture.md ├── menuconfig.png └── rules_for_bzImage_build_detail.md ├── bus_driver_device ├── 00-device_model.md ├── 01-bus.md ├── 02-driver.md ├── 03-device.md └── 04-bind.md ├── cgroup ├── 00-index.md ├── 01-control_cpu_mem_by_cgroup.md ├── 02-cgroup_fs.md ├── 03-hierarchy.md ├── 04-cgroup_and_process.md ├── 05-statistics.md ├── cgroup_files.png └── cgroup_fs_context.png ├── data_struct ├── 00-index.md ├── 01-list.md ├── 02-hlist.md ├── 03-plist.md ├── 04-xarray.md ├── 05-btree.md ├── 06-maple_tree.md └── 07-interval_tree.md ├── interrupt_exception ├── 00-start_from_hardware.md ├── 01-idt.md ├── 02-difference.md ├── 03-syscall.md ├── 04-exception_vector_setup.md ├── 05-interrupt_handler.md ├── 06-apic.md ├── 07-timer_interrupt.md ├── 08-softirq.md ├── 09-irq_softirq_preempt_and_smp.md ├── apic.png └── system_level_registers.png ├── iommu └── intel_iommu.md ├── kernel_pagetable ├── 00-evolution_of_kernel_pagetable.md ├── 01-pagetable_before_decompressed.md ├── 02-pagetable_compiled_in.md ├── 03-pagetable_after_cleanup_highmap.md ├── 04-map_whole_memory.md ├── 05-switch_to_init_level4_pgt.md ├── intel_virtual_phys_add.png ├── map_whole_memory.png ├── pagetable_after_cleanup_highmap.png ├── pagetable_before_decompression.png ├── pagetable_compiled.png └── switch_to_init_level4_pgt.png ├── kvm ├── 00-kvm.md ├── 01-memory_virtualization.md ├── 01_1-qemu_memory_model.md ├── 01_2-kvm_memory_manage.md └── ept.png ├── load_kernel ├── 00_index.md ├── 02_how_bzImage_loaded.md ├── 03_analysis_protected_kernel.md ├── 04_compress_decompress_kernel.md └── 05_phases_in_loading.md ├── memcg ├── 00-index.md ├── 01-init_overview.md ├── 02-set_memcg_limit.md └── 03-charge_memcg.md ├── mm ├── 00-memory_a_bottom_up_view.md ├── 01-e820_retrieve_memory_from_HW.md ├── 02-memblock.md ├── 03-sparsemem.md ├── 05-Node_Zone_Page.md ├── 06-page_alloc.md ├── 07-per_cpu_pageset.md ├── 08-slub_general.md ├── 09-slub_in_graph.md ├── 10-page_struct.md ├── 11-users_of_buddy.md ├── 12-gfp_usage.md ├── 13-physical-layer-partition.md ├── 14-folio.md ├── 50-challenge_evolution.md ├── 51-scalability_design_implementation.md ├── 52-where_is_page_struct.md ├── 53-memory_hotplug.md ├── 54-defer_init.md ├── 55-cma.md ├── common │ ├── 00_global_variable.md │ └── 01_important_transform.md ├── page_allocator │ ├── 00_page_allocator.md │ └── 01-compound_page.md ├── slub_allocator │ └── 00_slub.md ├── static_pcpu.md └── tests │ ├── 01_functional_test.md │ └── 02_performance_test.md ├── mm_reclaim ├── 00-index.md ├── 01-swapfile.md ├── 02-watermark.md ├── 03-big_picture.md ├── 04-pfra.md ├── 05-do_reclaim.md ├── vm_watermark.jpg └── vmscan.png ├── nvdimm ├── 00-brief_navigation.md ├── 00-brief_user_guide.md ├── 01-a_big_picture.md ├── 02-nvdimm_bus.md ├── 03-nvdimm.md ├── 04-nd_region.md ├── 05-namespace.md ├── 06-btt.md ├── 07-dax.md ├── 08-pfn.md └── 09-dev_dax.md ├── paypal.gif ├── reference ├── 00-reference.md ├── 01-mm.md ├── 02-mail.md └── 03-kernel_doc.md ├── support.md ├── synchronization ├── 00-index.md ├── 01-rcu.md └── 02-memory_barrier.md ├── tools ├── 01-patch.md ├── 02-check_file_change.md ├── 03-selftest.md ├── 03_01-build.md ├── 03_02-write_test.md └── handy_tools.md ├── tracing ├── 00-index.md ├── 01-ebpf.md ├── 02-trace_event.md ├── 03-ftrace_usage.md ├── 04-ftrace_internal.md ├── 05-kernel_live_patch.md ├── 06-drgn.md └── ftrace_framework.png ├── virtual_mm ├── 00-index.md ├── 01-anon_rmap_history.md ├── 02-thp_mapcount.md ├── 03-page_table_fault.md ├── 04-thp.md ├── 05-vma.md ├── 06-anon_rmap_usage.md ├── 07-mempolicy.md ├── 08-numa_balance.md ├── 09-mapcount.md ├── deprecate-vma.md └── will-it-scale.png └── wechat.gif /README.md: -------------------------------------------------------------------------------- 1 | # kernel_exploring 2 | 3 | My exploring in linux kernel, where I will record the interesting topic in 4 | linux kernel area. 5 | 6 | ## Two Purpose 7 | 8 | * summarise what I have learnt in linux kernel 9 | * share these to who is willing to study it 10 | 11 | ## About Me 12 | 13 | I am a kernel contributor since 2011. 14 | 15 | My major contributions go into: 16 | 17 | * PCI 18 | * Device model 19 | * MM 20 | * x86 misc 21 | 22 | No.4 active contributor in mm for [v5.11][5] 23 | 24 | Here are the patches merged in kernel: 25 | 26 | [2011-2016][1] 27 | 28 | [2016-2019][2] 29 | 30 | [2019-][4] 31 | 32 | ## Support 33 | 34 | If you like it, think it is useful, you can support me in this link. 35 | 36 | [Support][3] 37 | 38 | Welcome comments and discussion :-) 39 | 40 | [1]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/log/?qt=author&q=weiyang%40linux.vnet.ibm.com 41 | [2]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/log/?qt=author&q=richard.weiyang%40gmail.com 42 | [3]: support.md 43 | [4]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/log/?qt=author&q=richardw.yang%40linux.intel.com 44 | [5]: https://lwn.net/Articles/845831/ -------------------------------------------------------------------------------- /alipay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/alipay.gif -------------------------------------------------------------------------------- /bootup/00_index.md: -------------------------------------------------------------------------------- 1 | 内核启动的过程不是一蹴而就的,而且其中还包含了不少让人不太好找的小秘密。 2 | 3 | 每次我看到相关的代码,都要从头再找一遍,感觉非常费事。这次干脆就记录下来,以备后用。 4 | 5 | # INIT_CALL 6 | 7 | 在内核代码中经常会看到core_initcall(), subsys_initcall()这样xxx_initcall()的函数。 8 | 9 | 这些函数可以理解为c++中的构造函数,只是内核对这些函数做了分类,并且在特定的地方调用他们。 10 | 11 | [INIT_CALLS的秘密][1] 12 | 13 | # 内核参数 14 | 15 | 启动时我们可以通过[内核参数][2]来调整系统行为。 16 | 17 | [1]: /bootup/01_init_call.md 18 | [2]: /bootup/02_command_line_parse.md 19 | -------------------------------------------------------------------------------- /bootup/01_init_call.md: -------------------------------------------------------------------------------- 1 | 2 | 在内核代码中经常会看到core_initcall(), subsys_initcall()这样xxx_initcall()的函数。 3 | 4 | 这些函数可以理解为c++中的构造函数,只是内核对这些函数做了分类,并且在特定的地方调用他们。 5 | 6 | 今天我们就来学习一下。 7 | 8 | # 函数定义 9 | 10 | 先来看看都有哪些类似的函数: 11 | 12 | ``` 13 | #define early_initcall(fn) __define_initcall(fn, early) 14 | 15 | /* 16 | * A "pure" initcall has no dependencies on anything else, and purely 17 | * initializes variables that couldn't be statically initialized. 18 | * 19 | * This only exists for built-in code, not for modules. 20 | * Keep main.c:initcall_level_names[] in sync. 21 | */ 22 | #define pure_initcall(fn) __define_initcall(fn, 0) 23 | 24 | #define core_initcall(fn) __define_initcall(fn, 1) 25 | #define core_initcall_sync(fn) __define_initcall(fn, 1s) 26 | #define postcore_initcall(fn) __define_initcall(fn, 2) 27 | #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) 28 | #define arch_initcall(fn) __define_initcall(fn, 3) 29 | #define arch_initcall_sync(fn) __define_initcall(fn, 3s) 30 | #define subsys_initcall(fn) __define_initcall(fn, 4) 31 | #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) 32 | #define fs_initcall(fn) __define_initcall(fn, 5) 33 | #define fs_initcall_sync(fn) __define_initcall(fn, 5s) 34 | #define rootfs_initcall(fn) __define_initcall(fn, rootfs) 35 | #define device_initcall(fn) __define_initcall(fn, 6) 36 | #define device_initcall_sync(fn) __define_initcall(fn, 6s) 37 | #define late_initcall(fn) __define_initcall(fn, 7) 38 | #define late_initcall_sync(fn) __define_initcall(fn, 7s) 39 | ``` 40 | 41 | 这些函数不仅有相似的名字,还有这相似的定义: 42 | 43 | ``` 44 | #define ___define_initcall(fn, id, __sec) \ 45 | static initcall_t __initcall_##fn##id __used \ 46 | __attribute__((__section__(#__sec ".init"))) = fn; 47 | 48 | #define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id) 49 | ``` 50 | 51 | 可以看到这些函数地址都保存在类型为initcall_t的变量中。而这个变量的定义上又加了对应的section属性。 52 | 53 | 那这个section的名字展开是什么呢?以early_initcall()为例,这个section展开后是 .initcallearly.init 54 | 55 | # 函数链接 56 | 57 | 既然看到了section定义,那这个东西就要和链接脚本联系起来。 58 | 59 | 在x86上使用的脚本是arch/x86/kernel/vmlinux.lds.S,其中简化后长这样: 60 | 61 | ``` 62 | SECTIONS 63 | { 64 | ... 65 | INIT_DATA_SECTION(16) 66 | ... 67 | } 68 | ``` 69 | 70 | 而INIT_DATA_SECTION的定义在文件include/asm-generic/vmlinux.lds.h中: 71 | 72 | ``` 73 | #define INIT_CALLS_LEVEL(level) \ 74 | __initcall##level##_start = .; \ 75 | KEEP(*(.initcall##level##.init)) \ 76 | KEEP(*(.initcall##level##s.init)) \ 77 | 78 | #define INIT_CALLS \ 79 | __initcall_start = .; \ 80 | KEEP(*(.initcallearly.init)) \ 81 | INIT_CALLS_LEVEL(0) \ 82 | INIT_CALLS_LEVEL(1) \ 83 | INIT_CALLS_LEVEL(2) \ 84 | INIT_CALLS_LEVEL(3) \ 85 | INIT_CALLS_LEVEL(4) \ 86 | INIT_CALLS_LEVEL(5) \ 87 | INIT_CALLS_LEVEL(rootfs) \ 88 | INIT_CALLS_LEVEL(6) \ 89 | INIT_CALLS_LEVEL(7) \ 90 | __initcall_end = .; 91 | 92 | #define INIT_DATA_SECTION(initsetup_align) \ 93 | .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \ 94 | INIT_DATA \ 95 | INIT_SETUP(initsetup_align) \ 96 | INIT_CALLS \ 97 | CON_INITCALL \ 98 | INIT_RAM_FS \ 99 | } 100 | ``` 101 | 102 | 所以源代码中定义的所有函数都有各自对应的一个section,而且这么多函数都放在__initcall_start和__initcall_end指定的一段空间。 103 | 104 | # 函数调用 105 | 106 | 好了,找了这么半天都还没有看到这些init函数究竟是在哪里被调用的。这又是一堆很狗血的东西,要不然为啥每次找都要花上一段时间呢。 107 | 108 | 还是直接写出调用的点吧,好像也没有什么可以多说的。 109 | 110 | ``` 111 | start_kernel() 112 | ... 113 | rest_init() <--- almost last step in start_kernel() 114 | kernel_init() 115 | ... 116 | kernel_init_freeable() 117 | do_pre_smp_initcalls() 118 | for (fn = __initcall_start; fn < __initcall0_start; fn++) 119 | do_one_initcall(initcall_from_entry(fn)); 120 | ... 121 | do_basic_setup() 122 | do_initcalls() 123 | for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) 124 | do_initcall_level(level, command_line) 125 | ``` 126 | 127 | 节奏上一共分成两个部分,第一部分只调用了__initcall_start到__initcall0_start之间的初始化函数。 128 | 129 | 而第二部分则是调用剩下的,要证实这点还要看initcall_levels的定义。 130 | 131 | ``` 132 | static initcall_entry_t *initcall_levels[] __initdata = { 133 | __initcall0_start, 134 | __initcall1_start, 135 | __initcall2_start, 136 | __initcall3_start, 137 | __initcall4_start, 138 | __initcall5_start, 139 | __initcall6_start, 140 | __initcall7_start, 141 | __initcall_end, 142 | }; 143 | ``` 144 | 145 | 因为每一个level中包含多个定义好的函数,所以在do_initcall_level中还有一个循环调用每一个具体的函数。 146 | -------------------------------------------------------------------------------- /bootup/02_command_line_parse.md: -------------------------------------------------------------------------------- 1 | 内核除了在编译时的配置选项,还可以在启动时根据不同的命令行参数调整运行时的行为。 2 | 3 | 比如加上memblock=debug可以打开memblock的调试功能,启动过程中可以看到各区域变化情况。 4 | 5 | 本节我们来看一下这部分在启动时是怎么处理的。 6 | 7 | # 全局流程 8 | 9 | ```c 10 | start_kernel() 11 | parse_early_param(); 12 | parse_early_options() 13 | parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, 14 | do_early_param); 15 | after_dashes = parse_args("Booting kernel", 16 | static_command_line, __start___param, 17 | __stop___param - __start___param, 18 | -1, -1, NULL, &unknown_bootoption); 19 | print_unknown_bootoptions(); 20 | if (!IS_ERR_OR_NULL(after_dashes)) 21 | parse_args("Setting init args", after_dashes, NULL, 0, -1, -1, 22 | NULL, set_init_arg); 23 | if (extra_init_args) 24 | parse_args("Setting extra init args", extra_init_args, 25 | NULL, 0, -1, -1, NULL, set_init_arg); 26 | ``` 27 | 28 | 从上面的流程可以看出,核心的函数就是parse_args()。 29 | 30 | 而根据不同的参数,形成了内核参数不同层级的解析过程。 31 | 32 | # early param 33 | 34 | 内核首先解析的是early_param,使用的函数是do_early_param。函数补偿,直接拿来放这里。 35 | 36 | 37 | ```c 38 | const struct obs_kernel_param *p; 39 | for (p = __setup_start; p < __setup_end; p++) { 40 | if ((p->early && parameq(param, p->str)) || 41 | (strcmp(param, "console") == 0 && 42 | strcmp(p->str, "earlycon") == 0) 43 | ) { 44 | if (p->setup_func(val) != 0) 45 | pr_warn("Malformed early option '%s'\n", param); 46 | } 47 | } 48 | ``` 49 | 50 | 可以看到,这个是遍历__setup_start到__setup_end,根据情况调用setup_func。 51 | 52 | 举个例子,比如,memblock中的参数。 53 | 54 | ```c 55 | static int __init early_memblock(char *p) 56 | { 57 | if (p && strstr(p, "debug")) 58 | memblock_debug = 1; 59 | return 0; 60 | } 61 | early_param("memblock", early_memblock); 62 | ``` 63 | 64 | 这两个怎么关联上呢?对了,就是看是不是定义在指定的一个section里。 65 | 66 | 首先我们看__setup_start/__setup_end在哪里。这个东西有点隐藏,直接搜搜不到。打开看了一下才发现这个是由INIT_SETUP定义的。 67 | 68 | 最后展开长这个样子。 69 | 70 | ``` 71 | . = ALIGN(16); __setup_start = .; KEEP(*(.init.setup)) __setup_end = .; 72 | ``` 73 | 74 | 这里就看到这个循环找的是.init.setup这个section里面的东西。 75 | 76 | 然后再看early_param的定义,摘出其中重要的部分 77 | 78 | ```c 79 | static struct obs_kernel_param __setup_##unique_id \ 80 | __used __section(".init.setup") \ 81 | __aligned(__alignof__(struct obs_kernel_param)) \ 82 | = { __setup_str_##unique_id, fn, early } 83 | ``` 84 | 85 | 也就是定义了一个obs_kernel_param结构,而且还是放在.init.setup这个section的。 86 | 87 | 好了,这样就联系起来了。do_early_param会遍历.init.setup这个section中的数组,判断符合条件,就会运行对应的setup_func。对memblock来说,也就是early_memblock了。 88 | 89 | # 非early param 90 | 91 | 在do_early_param中,我们看到只有p->early的内核参数才会在这里执行。但是用__setup()定义的结构early并没有置1,那问题来了,这部分的参数是怎么被解析的呢? 92 | 93 | 这个查找着实花了点时间,主要是因为当前内核认为这种参数是要淘汰的定义方式了。所以藏在了函数unknown_bootoption中的obsolete_checksetup()中。 94 | 95 | 其过程和之前一样,也是遍历__setup_start/__setup_end,但只运行early=0的结构。 96 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/00_index.md: -------------------------------------------------------------------------------- 1 | 内核编译是一个非常生僻的领域,哪怕是做内核的童鞋也不一定关注过整个内核编译的流程,更不用说内核的门外汉了。 2 | 3 | 然而了解学习内核编译系统,可能会有你意想不到的收获。我在这个过程中从以下几个方面获得收益。 4 | 5 | * 深入的理解makefile如何管理大型项目 6 | * 编译出错了也不会那么慌张 7 | * 多年后才明白究竟是哪个文件才是放在/boot/目录下的那个启动文件以及它和另一个很像的文件之间的差别 8 | * 节省了编译时间,不会每次都用make all这种粗暴的方式了 9 | * 传说的make -j8中,这8个进程是在什么条件下才能开启的 10 | * 进一步的了解了整个内核的构造,而不是仅仅知道自己做的那一块了 11 | * 了解了某些编译链接细节,帮助看懂内核启动时的一些操作 12 | 13 | 但是很多人,包括我自己在很长的一段时间内都没有相对仔细阅读和研究内核编译的过程。在此,我希望通过分享自己一点点的经验,帮助大家破除对内核编译系统的抵触,快速掌握内核编译系统的整体架构,为后续的深入研究开一个好头。 14 | 15 | 鉴于本人经验尚浅,或有不透彻甚至是错误的地方,还望大家包涵。如能告知,万分感谢。 16 | 17 | # 先用起来 18 | 19 | 大多数人都是从最简单的开始的,我也不例外。首先你得有这么一个环境,能够编译内核,接下来你才有机会去修改去实验去探索内核编译的过程。 20 | 21 | 第一步先 22 | 23 | [编译出你的第一个内核][9] 24 | 25 | 由此小白们可以体会一下编译的过程,和使用自己编译的内核的快乐。也为后续我们做深入的实验做好准备。 26 | 27 | 在了解了基本的编译过程和步骤之后,我们再来看看那些 28 | 29 | [内核编译中的小目标][10]。 30 | 31 | 这些小目标可以帮助我们 32 | 33 | * 生成辅助资料 34 | * 生成单个目标文件 35 | * 节约时间 36 | * 偶尔可以帮助一下调试 37 | 38 | 或许你会发现某个对你有用的~ 39 | 40 | # 跟着我走 41 | 42 | 现在我们已经会使用基本的命令制作出内核和一些小目标了。说实话,整个kbuild系统还是有点复杂的。不过不用担心,我们先来三个简单的小目标,了解一下基本的运作方式。或许你会觉得并没有想象中的那么高不可攀,或许可以让自己觉得还是有那么点机会可以看懂的~ 43 | 44 | 先来一个最简单最直接的 45 | 46 | [可能是kbuild中最直接的小目标 -- help][11] 47 | 48 | 看了这个例子,你或许会觉得不过瘾。什么嘛,这个help的目标就和平时自己写的makefile的套路是一样的。没有什么花头,也不是什么高级货。好了,这次我们来一个稍微复杂一些的。上面的例子太直接了只有一个层次的结构,那这次来一个有两层的看看。 49 | 50 | [使用了一个kbuild函数的目标 -- cscope][13] 51 | 52 | 有了上述这两个不是编译目标的目标,我们已经储备了相当的make和kbuild知识,了解了一定的kbuild系统结构,接下来来看一个稍微复杂一点但相对还是比较直接的目标。这次可是货真价实的编译目标了哦~ 53 | 54 | [内核中单个.o文件的编译过程][12] 55 | 56 | # 真刀真枪 57 | 58 | 能走到这,而且看懂上面三个小目标的基本上已经储备了足够多的基础知识。正所谓养兵千日,用兵一时,接下来我们就该动真格的了~ 59 | 60 | 在根目录下最明显的内核目标就数vmlinux了。不过编译内核这么多年,安装内核无数次,却从来都没有好好研究过他老人家出生的过程。那我们就先来看看 61 | 62 | [根目录vmlinux的编译过程][1] 63 | 64 | 研究完了根目录的vmlinux,我突然发现还有一个叫bzImage的目标。也是啊,内核不是说要压缩的么?vmlinux是ELF格式,那就能被直接加载到内存了?带着这些疑惑,让我们来探索一下 65 | 66 | [启动镜像bzImage的前世今生][2] 67 | 68 | 探索的过程中发现bzImage是由setup.bin和vmlinux.bin两个目标粘合而成。看来马上就要弄明白整个bzImage编译过程了。咱逐个探索~ 69 | 70 | [setup.bin的诞生记][14] 71 | [真假vmlinux--由vmlinux.bin揭开的秘密][3] 72 | 73 | 弄明白了这两个组成部分后,在来看看[bzImage的全貌][16] 74 | 75 | 终于,经历了九九八十一难之后,可以说彻底的理解了内核编译的整个过程,也对kbuild系统架构有了基本的认识。可以学成下山了。 76 | 77 | # 基本概念 78 | 79 | 温故而知新,在探索过程中我们是见招拆招,可能用到了不少概念,但没有系统梳理。现把相关知识点整理如下,以便有更好的理解。 80 | 81 | [kbuild系统浅析][15] 82 | 83 | 84 | # 写在最后 85 | 86 | 整个内核的编译系统依然是十分庞大复杂的。其中还有不少细枝末节在本系列中没有深入仔细地去分析,讲解和探索。经过了这么一段时间的磨练,相信大家已经掌握了基本的知识,对kbuild系统运作原理有了深入了了解,若能为大家进一步的探索打下了基础也算是没有白费功夫。 87 | 88 | 本系列文章或许还会再修正更新增加,本次更新就先到这里。愿大家能够在内核探索的道路上勇猛精进。 89 | 90 | 送君千里,终须一别,我们来日江湖再见~ 91 | 92 | [1]: /brief_tutorial_on_kbuild/06_building_vmlinux_under_root.md 93 | [2]: /brief_tutorial_on_kbuild/07_rules_for_bzImage.md 94 | [3]: /brief_tutorial_on_kbuild/09_rule_for_vmlinux_bin.md 95 | [4]: http://blog.csdn.net/richardysteven/article/details/52551443 96 | [8]: https://www.gnu.org/software/make/manual/make.html#Rule-Introduction 97 | [9]: /brief_tutorial_on_kbuild/01_build_your_first_kernel.md 98 | [10]: /brief_tutorial_on_kbuild/02_common_targets_in_kernel.md 99 | [11]: /brief_tutorial_on_kbuild/03_first_target_help.md 100 | [12]: /brief_tutorial_on_kbuild/05_rules_for_single_object.md 101 | [13]: /brief_tutorial_on_kbuild/04_one_example_of_kbuild_function_cscope.md 102 | [14]: /brief_tutorial_on_kbuild/08_rule_for_setupbin.md 103 | [15]: /brief_tutorial_on_kbuild/13_root_makefile.md 104 | [16]: /brief_tutorial_on_kbuild/14_bzImage_whole_picture.md -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/01_build_your_first_kernel.md: -------------------------------------------------------------------------------- 1 | 第一次总是让人激动的,但是也常常伴随着恐惧和害怕。我相信有不少小白希 2 | 望学习内核,但是常常连第一步都没有开始,就直接栽倒在了出发的地方。 3 | 4 | 这篇文章献给那些向往成为内核大牛的小白们,由我来帮你们破除进入内核世界的第一道障碍。 5 | 6 | # 准备环境 7 | 8 | 编译内核之前有一些基本的条件 9 | 10 | * 有一台可以联网的机器(或者虚拟机) 11 | * 安装了linux系统 12 | * 怎么着也得会一点基本的命令操作 13 | 14 | 除此之外对linux系统还要求一些软件包的安装(可能不全,在编译过程中遇到提示可以使用google搜索是缺了哪个包) 15 | 16 | * git 一个软件版本管理工具,我们用它来获得内核源码 17 | * gcc 编译器 18 | * make 编译工具 19 | * libncurse-dev 一个图形库 20 | * openssl-dev 加密库 21 | 22 | 嗯,差不多了,开始动手吧。 23 | 24 | # 获取内核 25 | 26 | 感谢Linus,感谢git,自从有了git,获取内核代码变得异常的方便,而且时刻都可以是最新的。 27 | 28 | 在终端输入以下命令即可: 29 | 30 | > git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git 31 | 32 | 没有安装git软件的请重复上一个步骤 33 | 34 | # 基本配置 35 | 36 | 和大部分开源软件类似,linux kernel也是要配置之后才能够编译的。配置方法有好几种,我比较偏爱的是 make menuconfig。 37 | 38 | ```bash 39 | cd linux 40 | make menuconfig 41 | ``` 42 | 43 | 执行完,你可以看到如下的配置界面。 44 | 45 | ![这里写图片描述](/brief_tutorial_on_kbuild/menuconfig.png) 46 | 47 | 知道为啥我喜欢用这个了吧,因为这有个图形界面。虽然有好多看不懂,但是毕竟有些你是能猜出个大概意思的。 48 | 49 | 这次只是用来编译第一个内核,所以不需要什么配置,直接按右箭头,走到Exit退出保存即可。 50 | 51 | ## 可能会遇到的问题 52 | 53 | 在执行make menuconfig的时候,可能会遇到一些提示说某些包没有安装导致执行失败。大家不要慌,究其原因是因为配置的过程实际上是内核先编译了一个用户态的配置工具,这个过程就需要依赖的软件包有: make, gcc, ld 和图形库libncurse-dev。不用紧张,按照提示,缺什么软件就安装什么软件就好了。 54 | 55 | # 开始编译 56 | 57 | 配置完了之后就可以编译了。 58 | 59 | 很简单,运行如下命令 60 | 61 | ``` 62 | make -j8 63 | ``` 64 | 65 | 如果编译成功,你就会看到目录下有一个文件叫vmlinux。 66 | 恭喜~ 67 | 68 | # 安装内核 69 | 70 | 也是so easy 71 | 72 | ``` 73 | make modules_install 74 | make install 75 | ``` 76 | 77 | 注意: 这两步需要有管理员权限。 78 | 79 | 另外需要注意的是,安装后有些版本可能要调整引导程序的配置。比如在ubuntu上,配置文件在/etc/default/grub, /boot/grub/grub.cfg。否则有时候下一次重启还是使用旧的内核。 80 | 81 | ## 启动时显示grub界面 82 | 83 | ubuntu发行版默认grub配置在启动时是不显示grub界面的。虽然在make install后,大概率重启后能进入新编译/安装的内核。但是这么对内核开发人员来说还是不太友好。需要修改/etc/default/grub文件让grub进入的界面在启动时显示出来。 84 | 85 | ``` 86 | # GRUB_TIMEOUT_STYLE=hidden 87 | GRUB_TIMEOUT=10 88 | ``` 89 | 90 | 需要修改的是上面两条。先把隐藏的方式去掉,再设置一个超时时间。 91 | 92 | # 重启机器 93 | 94 | 执行如下命令 95 | 96 | ``` 97 | reboot 98 | ``` 99 | 100 | 好了,等下次机器起来,选择你刚编译安装的内核,那就是一个崭新的世界了。 101 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/02_common_targets_in_kernel.md: -------------------------------------------------------------------------------- 1 | 王首富定过一个亿的小目标,虽然我们内核中没有一个亿,却还是有不少有意思的小目标的。 2 | 3 | 这些小目标可以帮助我们 4 | 5 | * 生成辅助资料 6 | * 生成单个目标文件 7 | * 节约时间 8 | * 偶尔可以帮助一下调试 9 | 10 | 那我们就一个个讲吧~ 11 | 12 | # all 或者 空 13 | 14 | 当你在内核代码目录下执行make的时候,编译的目标默认就是这个all。 15 | 16 | 代码中的注释很好的解释了这个目标的意义。 17 | 18 | ``` 19 | # The all: target is the default when no target is given on the 20 | # command line. 21 | # This allow a user to issue only 'make' to build a kernel including modules 22 | ``` 23 | 24 | 这个目标在x86平台 = 25 | 26 | > vmlinux + modules + bzImage 27 | 28 | # vmlinux 29 | 30 | 嗯,这个就是你编译完成后在linux源代码目录下的那个vmlinux了。 31 | 32 | 注意了,这个是一个ELF的文件哦。用处嘛,以后你会慢慢知道的。 33 | 34 | # modules 35 | 36 | modules就是编译内核模块的。 37 | 38 | 所以整个内核,你可以理解为就是 vmlinux + modules组成的。 39 | 40 | # bzImage 41 | 42 | 那既然vmlinux + modules组成了整个内核,多出来了一个bzImage来插什么腿? 43 | 44 | 这个东西可以看作是vmlinux的衍生。你看vmlinux是ELF格式的,首先在引导程序要加载的时候你得有人看得懂这个格式,另外这个文件比较大。现在4.9的内核编译下来vmlinux就有422M这么大,直接放到启动分区实在太占地方。当然我这个没有裁剪,裁剪后应该可以相应减少,但是恐怕也不会小太多。 45 | 46 | 所以bzImage可以粗暴地理解为 47 | 48 | > ELF加载器 + 压缩了的vmlinux。 49 | 50 | 整个压缩完之后bzImage大小只有7M不到了,这压缩能力杠杠的。 51 | 52 | # M=drivers/xxx 53 | 54 | 这个可以看作是modules目标中的更小的一个目标了。 55 | 56 | 举一个栗子大家看了或许更明白。 57 | 58 | ``` 59 | make M=drivers/net/ethernet/intel/ixgbe/ 60 | ``` 61 | 62 | 这条命令只会去编译ixgbe这个驱动,而不需要去运行对其他驱动代码的检测。对开发或者后期维护,都节省了相应的编译时间。 63 | 64 | # dir/file.o 65 | 66 | 这是单独编译某个文件的,当你修改某个文件做开发的时候,你可以先单独编译你修改的文件确保没有语法错误,然后再编译内核或者是模块。 67 | 68 | 比如在ixgbe模块中,更改了ixgbe_ethtool.o文件,那么你可以先运行 69 | 70 | ``` 71 | make drivers/net/ethernet/intel/ixgbe/ixgbe_ethtool.o 72 | ``` 73 | 74 | 来确保单个文件的语法没有错误,然后再去编译整个模块。 75 | 76 | 同样这也是可以节省编译时间的,尤其是当遇到某个文件编译有错误的时候。 77 | 78 | # dir/file.i 79 | 80 | 这个目标是针对某个文件只做到预处理,也就是把所有的头文件和宏定义都展开了。 81 | 82 | 用法很简单 83 | 84 | ``` 85 | make mm/memblock.i 86 | ``` 87 | 88 | 就生成了mm/memblock.i文件,其结果是已经预处理完的,也就是个种宏定义展开后的。 89 | 90 | 感觉用处不是很多,能想到的就是在阅读内核源代码的时候可以确定到底这个宏定义的是啥。因为内核需要在不同的架构体系下运行,而且同一架构体系下又有不同的配置。所以同样一个函数或者宏会在不同情况下定义成不同的样子,有时候直接肉眼去看代码不一定能看得准。 91 | 92 | 那怎么办呢? 93 | 94 | 这个时候就可以执行这个命令,直接看预处理后的文件,或许可以有助于你理解代码那么一点点。 95 | 96 | # dir/file.s 97 | 98 | 这个作用是生成了指定文件的汇编代码。 99 | 100 | 用法类似 101 | 102 | ``` 103 | make mm/memblock.s 104 | ``` 105 | 106 | 这估计是要做汇编级的调试了,俺还从来没有整过。 107 | 108 | # cscope 109 | 110 | 这个目标特殊,已经不是代码了,是生成cscope的文件。嗯,这个文件是用来辅助阅读代码的。 111 | 112 | 懂得人秒懂,不懂的估计得重新开一个话题了。 113 | 114 | # isoimage 115 | 116 | 这个还是Andi Kleen告诉我的。这是一个包含内核的可以启动的iso文件。 117 | 用来调试用的,用得真的不多。 118 | 119 | 有兴趣的朋友或许可以在我其他的文章中见到它的身影。 120 | 121 | # help 122 | 123 | 最后的最后,kbuild中提供了一个help的目标。当你不确定如何使用或者想知道还有哪些目标可以用那就执行 124 | 125 | ``` 126 | make help 127 | ``` 128 | 129 | 你就可以知道都还有哪些用法了。 130 | 131 | 好了,小目标们讲完了。现在是不是对内核编译又有了点了解?强烈建议没有编译过这些小目标的筒子手动运行一下,看一看执行的结果加深内核编译过程的印象。 132 | 133 | 还是那句话 134 | 135 | > 纸上得来终觉浅,绝知此事须躬行 136 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/03_first_target_help.md: -------------------------------------------------------------------------------- 1 | help目标可以说是在kbuild中最直接的小目标了,虽然它和我们的代码基本没有什么关系,而是用来生成kbuild的简短使用说明,但是用它来作为走近kbuild系统的敲门砖,或许是比较合适的。 2 | 3 | # 如何用? 4 | 5 | 用法还是很简单的 6 | 7 | ``` 8 | make help 9 | ``` 10 | 11 | 就可以显示出当前kbuild所支持的小目标们。比如: 12 | 13 | * vmlinux 14 | * modules 15 | * clean 16 | * dir/file.o 17 | 18 | 具体的大家可以动手操作,体会一下这个过程。 19 | 20 | # 在哪里? 21 | 22 | 万事开头难,既然是你遇到的头一个目标,可能你会丈二和尚摸不着头脑。不知道会是在哪里。 23 | 24 | > 肿么办? 25 | 26 | 如果是你,你会想怎么做呢?请在这里停留一分钟,自己思考一下再往下看我提供的做法。 27 | 28 | **这里是分割线** 29 | --- 30 | 31 | 你看,我们平时自己使用make的时候是怎么用的呢? 要写makefile是吧,在makefile里面加上目标和规则是吧。那好了,kbuild也是基于make这个基本结构运作的。那就是找到别人写的那个makefile呗。 32 | 33 | 先别着急找,我们先来看一下make的手册是怎么讲的。 34 | 35 | > Once a suitable makefile exists, each time you 36 | > change some source files, this simple shell 37 | > command: 38 | > 39 | > make 40 | > 41 | > suffices to perform all necessary recompilations. 42 | > The make program uses the makefile description 43 | > and the last-modification times of 44 | > the files to decide which of the files need 45 | > to be updated. For each of those files, it 46 | > issues the commands recorded in the makefile. 47 | > 48 | > make executes commands in the makefile to 49 | > update one or more target names, where name 50 | > is typically a program. If no -f option 51 | > is present, make will look for the makefiles 52 | > GNUmakefile, makefile, and Makefile, in that 53 | > order. 54 | 55 | 总结一下: 56 | 57 | * 运行make后,会去寻找makefile,根据其中的规则做更新 58 | * 可以使用选项-f指定要寻找那个makefile,如果没有指定则按照上述顺序去寻找 59 | 60 | 还稍微需要解释一下下,这里的makefile这个词有两种不同的意义,头一次看的估计会晕,说实话我也有点晕。 61 | * 代词,代表的是make使用的规则文件,并不是具体的哪个文件。 62 | * 名字,是指make运行时,如果没有传入-f选项,那么会按照GNUmakefile, makefile, Makefile这个顺序去搜索规则文件 63 | 64 | 从上面的手册中,我们可以看到,运行make其实是又其自身的要求的。也就是需要有个规则文件。那我们再来做个实验。 65 | 66 | 随便新建一个目录,cd进去,运行make,看一下结果。 67 | 68 | ``` 69 | $ mkdir test 70 | $ cd test/ 71 | $ make 72 | make: *** No targets specified and no makefile found. Stop. 73 | ``` 74 | 75 | 你看是不是啥都干不了? 76 | 77 | 整理了一下make的基本知识,再回过来看我们执行的命令。 78 | 79 | ``` 80 | make help 81 | ``` 82 | 83 | 这次我们的make命令并没有带选项-f,所以按照手册所说,应该是在本地按照顺序寻找了规则文件再执行的。 那我们来看一下,内核源码根目录下都有谁呗。 84 | 85 | ``` 86 | ls 87 | OPYING REPORTING-BUGS include scripts 88 | CREDITS arch init security 89 | Documentation block ipc sound 90 | Kbuild certs kernel tools 91 | Kconfig crypto lib usr 92 | MAINTAINERS drivers mm virt 93 | Makefile firmware net 94 | README fs samples 95 | ``` 96 | 97 | 我相信你已经看到了点什么。 正所谓, 98 | 99 | > 众里寻她千百度,蓦然回首,那人却在,灯火阑珊处。 100 | 101 | # 什么样? 102 | 103 | 已经找到了规则文件Makefile, 那我们就打开看一下,找找我们的help小目标呗。 104 | 105 | 相信你已经看到了~ 它就长这个样子: 106 | 107 | ``` 108 | help: 109 | @echo 'Cleaning targets:' 110 | @echo ' clean - Remove most generated files but keep the config and' 111 | @echo ' enough build support to build external modules' 112 | @echo ' mrproper - Remove all generated files + config + various backup files' 113 | @echo ' distclean - mrproper + remove editor backup and patch files' 114 | @echo '' 115 | @echo 'Configuration targets:' 116 | @$(MAKE) -f $(srctree)/scripts/kconfig/Makefile help 117 | @echo '' 118 | @echo 'Other generic targets:' 119 | @echo ' all - Build all targets marked with [*]' 120 | @echo '* vmlinux - Build the bare kernel' 121 | @echo '* modules - Build all modules' 122 | @echo ' modules_install - Install all modules to INSTALL_MOD_PATH (default: /)' 123 | ... 124 | ``` 125 | 126 | 怎么样,确实够直接吧,在根目录的Makefile中就找到了目标。看来我们今天的运气还不错~ 127 | 128 | # 恭喜你 129 | 130 | 恭喜,你已经知道了一个kbuild的小目标是如何运作起来的了。你看是不是和我们平时见到的最简单的makefile结构差不多呢? 131 | 132 | 一切事物皆有源头,哪怕是再复杂的结构都可以将其拆分成简单的组成部分,而去逐个了解和研究。我们的kbuild更是如此。相信你可以通过不断探索,掌握这看似庞大的kbuild系统~ 133 | 134 | 祝好,加油~ 135 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/04_one_example_of_kbuild_function_cscope.md: -------------------------------------------------------------------------------- 1 | 方才我们学习了史上最简单[可能是kbuild中最直接的小目标 – help][1]。这次我们来看看稍微高级那么一点点的目标 -- cscope。 2 | 3 | 客官可能要着急了,这个help和cscope都还不能算是什么真正的编译目标,讲这个我不爱听啊。嗯,出门右转,点击下一个链接就是你真的爱听的了。不过呢,东西有点多,恐怕你一下子接受不了。反正我第一次写的时候都有一种写到要吐的感觉。 4 | 5 | 你想我们平时要是很久没有运动,突然让你冲刺一百米是不是会头晕眼花腿抽经?如果在奔跑前能让你做个热身,充分让身体舒展开,你的感觉会不会好很多?所以我特意增加了这篇小进阶,希望能帮助你在进入高难度之前,给你做个脑力上的热身。 6 | 7 | # 找到cscope目标 8 | 9 | 打开根目录下的Makefile文件,搜索cscope关键字。你找到了么? 10 | 11 | ``` 12 | tags TAGS cscope gtags: FORCE 13 | $(call cmd,tags) 14 | ``` 15 | 16 | 看来kbuild把相应的这几个tag类目标都放在了一起。 17 | 18 | 不过后面这个$(call cmd,tags)是什么鬼?原来这是makefile中定义函数的一种方式。我们来看一下手册中是怎么讲的, [GNU make: call function][2] 19 | 20 | ``` 21 | The call function is unique in that it can be used to create new parameterized functions. You can write a complex expression as the value of a variable, then use call to expand it with different values. 22 | 23 | The syntax of the call function is: 24 | 25 | $(call variable,param,param,…) 26 | 27 | When make expands this function, it assigns each param to temporary variables $(1), $(2), etc. 28 | ``` 29 | 30 | 知道了这个定义,再对照刚才的命令 31 | 32 | > $(call cmd,tags) 33 | 34 | 意思就是有一个变量叫做cmd,需要在这里展开,而$(1)会被替换成tags。嗯,有点像宏定义,对不。 35 | 36 | 那我们现在要去找一个名字为cmd的变量咯~ 37 | 38 | # 初次遇见kbuild函数 39 | 40 | c语言代码都是有一定的层次结构的,变量定义,函数声明都有各自的地方存放。比如定义要在源文件,而声明要在头文件。那宏定义呢? 是不是也在头文件中定义的? 41 | 42 | makefile中也有类似的用法 -- include。 43 | 44 | 在根Makefile中搜索include关键字,没几下就找到了这么一行 45 | 46 | ``` 47 | include scripts/Kbuild.include 48 | ``` 49 | 50 | 是不是看着眼熟? 51 | 52 | 而在这个文件中就会发现那个叫cmd的变量了。 53 | 54 | ``` 55 | cmd = @$(echo-cmd) $(cmd_$(1)) 56 | ``` 57 | 58 | 我们把变量tags代入,就得到了 59 | 60 | ``` 61 | @$(echo-cmd) $(cmd_tags) 62 | ``` 63 | 64 | 先不管echo-cmd,先来看cmd_tags长什么样子。 65 | 66 | 细心的童鞋可能一开始就看到了,其实它就在刚才cscope规则的上方。 67 | 68 | ``` 69 | # Generate tags for editors 70 | # ---------------------------------- 71 | quiet_cmd_tags = GEN $@ 72 | cmd_tags = $(CONFIG_SHELL) $(srctree)/scripts/tags.sh $@ 73 | ``` 74 | 75 | 这下明白了,原来cmd就好像一个函数,而这个函数的参数就是一个回调函数~ 76 | 77 | # tags.sh脚本文件 78 | 79 | 嗯,这个文件咱就不细讲了,毕竟和编译和kbuild的关系不大。总体来说就是传入什么参数,就执行相应的动作来生成需要的辅助文件。 80 | 81 | ``` 82 | case "$1" in 83 | "cscope") 84 | docscope 85 | ;; 86 | 87 | "gtags") 88 | dogtags 89 | ;; 90 | 91 | "tags") 92 | rm -f tags 93 | xtags ctags 94 | remove_structs=y 95 | ;; 96 | 97 | "TAGS") 98 | rm -f TAGS 99 | xtags etags 100 | remove_structs=y 101 | ;; 102 | esac 103 | ``` 104 | 105 | 对cscope目标来讲,就是执行docscope这个动作。 106 | 107 | # cscope目标的层次结构 108 | 109 | 经过了一点小小挣扎,我们弄明白了cscope目标是如何通过kbuild系统生成的。怎么样,是不是和你想象的步骤略有不同? 是不是有学到一些些kbuild的基本结构? 110 | 111 | 这里我们来回顾一下整个cscope目标生成的步骤,我把它叫做层级结构。 112 | 113 | ``` 114 | Makefile <--- scripts/Kbuild.include 115 | --------------- 116 | cscope: FORCE 117 | $(call cmd,tags) 118 | 119 | Makefile 120 | --------------- 121 | cmd_tags 122 | scripts/tags.sh $@ 123 | ``` 124 | 125 | 在Kbuild.include中定义了一些辅助函数,而整个kbuild系统都构建在这些辅助函数的基础上。这次我们看到的例子着实简单,看上去把规格在本地展开要更加清晰。不过随着内核复杂度增加,每次都本地展开会显得代码冗长且不易维护。 126 | 127 | 虽然这么些在理解上增加了一些难度,不过也经过了一些些努力就能水落石出。若能真的理解,就已经做好了kbuild系统探索的基本准备了。 128 | 129 | # 一个小tip 130 | 131 | 阅读代码的时候我喜欢用cscope生成代码之间的索引,而我常用的方法就是make cscope。 132 | 133 | 但是通常重新生成一遍需要比较长的时间,后来我发现了一个加快生成速度的方法。 134 | 135 | > 那就是跳过一些我并不想看的目录。 136 | 137 | 具体怎么做呢?好了,直接上代码: 138 | 139 | ``` 140 | diff --git a/scripts/tags.sh b/scripts/tags.sh 141 | index 4fa070f9231a..5ac0873cfe4d 100755 142 | --- a/scripts/tags.sh 143 | +++ b/scripts/tags.sh 144 | @@ -27,6 +27,7 @@ fi 145 | 146 | # ignore userspace tools 147 | ignore="$ignore ( -path ${tree}tools ) -prune -o" 148 | +ignore="$ignore ( -path ${tree}drivers/gpu ) -prune -o" 149 | +ignore="$ignore ( -path ${tree}drivers/net ) -prune -o" 150 | +ignore="$ignore ( -path ${tree}drivers/media ) -prune -o" 151 | +ignore="$ignore ( -path ${tree}drivers/scsi ) -prune -o" 152 | +ignore="$ignore ( -path ${tree}drivers/staging ) -prune -o" 153 | +ignore="$ignore ( -path ${tree}drivers/usb ) -prune -o" 154 | +ignore="$ignore ( -path ${tree}drivers/infiniband ) -prune -o" 155 | 156 | # Detect if ALLSOURCE_ARCHS is set. If not, we assume SRCARCH 157 | if [ "${ALLSOURCE_ARCHS}" = "" ]; then 158 | ``` 159 | 160 | 主要是去掉了几个大体积驱动的索引。整个制作索引的时间从117s下降到了57s,超过了50%。 161 | 162 | 希望能帮到你。 163 | 164 | ## v6.7 版本更新 165 | 166 | 最新6.7的版本又看了下,现在忽略生成cscope的方式改变了。现在是通过环境变量IGNORE_DIRS来指定需要忽略的路径。 167 | 168 | 比如 169 | ``` 170 | make IGNORE_DIRS="drivers tools" cscope 171 | ``` 172 | 173 | 或者把变量定义到.bashrc中 174 | ``` 175 | export IGNORE_DIRS="drivers tools" 176 | ``` 177 | 178 | 这样优化后,生成cscope文件的时间大大缩短。文件大小也从1.8M减小到436K。 179 | 180 | [1]: /brief_tutorial_on_kbuild/03_first_target_help.md 181 | [2]: https://www.gnu.org/software/make/manual/html_node/Call-Function.html 182 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/07_rules_for_bzImage.md: -------------------------------------------------------------------------------- 1 | # 原来安装的是它 2 | 3 | 编译完内核,接下来要做的就是安装新的内核来使用了。那安装的是哪个文件呢? 4 | 5 | 关于这个问题,昨天我还真好好看了一下,发现原来安装的不是根目录下的vmlinux,而是bzImage。玩了内核这么多年,一直都以为是根目录下的那个vmlinux是安装时拷贝到/boot/目录下的文件,结果原来不是。真是惭愧惭愧。 6 | 7 | 我是从make install这个规则开始找下去的。 8 | 9 | ``` 10 | boot := arch/x86/boot 11 | install: 12 | $(Q)$(MAKE) $(build)=$(boot) $@ 13 | ``` 14 | 15 | 这个规则在arch/x86/Makefile中,好奇为什么没有在根Makefile里找到。 16 | 17 | 那实际上真正执行的是在arch/x86/boot/Makefile中这个规则 18 | 19 | ``` 20 | install: 21 | sh $(srctree)/$(src)/install.sh $(KERNELRELEASE) $(obj)/bzImage \ 22 | System.map "$(INSTALL_PATH)" 23 | ``` 24 | 25 | 对应x86架构,在这个install.sh就是arch/x86/boot/install.sh。虽然脚本中有几种安装内核的方式,不过我们只看其中一种也就能确认安装在/boot/目录下的是bzImage而不是vmlinux了。 26 | 27 | ``` 28 | cat $2 > $4/vmlinuz 29 | ``` 30 | 31 | 所以说不看不知道,一看吓一跳。以后不敢说自己懂内核了。 32 | 33 | #目标在哪里? 34 | 35 | 我们在[内核编译的小目标][1]一文中也提到过,bzImage是x86平台下默认的目标之一。但是并没有在根目录的Makefile中发现bzImage目标。 而在根目录的Makefile中的前面部分有 36 | 37 | ``` 38 | include $(srctree)/arch/$(SRCARCH)/Makefile 39 | ``` 40 | 41 | 这个是不同的arch会include不同的文件,比如是x86的架构就会include arch/x86/Makefile 42 | 43 | 打开一看,果不其然 44 | 45 | ``` 46 | # Default kernel to build 47 | all: bzImage 48 | ``` 49 | 50 | 好了,我们终于找到这个bzImage的target了,在x86平台它也是all的一部分。 51 | 52 | 来看看具体是什么 53 | 54 | 55 | # 生成规则 56 | 57 | 在arch/x86/Makefile中,bzImage具体定义是这么个样子的。 58 | 59 | ``` 60 | # KBUILD_IMAGE specify target image being built 61 | KBUILD_IMAGE := $(boot)/bzImage 62 | 63 | bzImage: vmlinux 64 | ifeq ($(CONFIG_X86_DECODER_SELFTEST),y) 65 | $(Q)$(MAKE) $(build)=arch/x86/tools posttest 66 | endif 67 | $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE) 68 | $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot 69 | $(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@ 70 | ``` 71 | 72 | 又是一坨这么长的,整的好生心烦。幸好,看到了一个眼熟的\$(MAKE) \$(build)=\$(boot) \$(KBUILD_IMAGE)。这个不是我们编译具体目标的时候经常看到的么?咱来展开看一眼: 73 | 74 | > make -f scripts/Makefile.build obj=arch/x86/boot arch/x86/boot/bzImage 75 | 76 | 是不是觉得好像亲切了一些? 77 | 78 | 对scripts/Makefile.build文件再补充一点,在该文件的开头处出了包含了scripts/Kbuild.include文件,还包含了一个$(build-file)文件。 79 | 80 | 先来看一下这个变量的定义: 81 | 82 | ``` 83 | #The filename Kbuild has precedence over Makefile 84 | kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src)) 85 | kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile) 86 | include $(kbuild-file) 87 | ``` 88 | 89 | 你猜到了什么不? 没猜到? 再看一眼上面的注释? 90 | 91 | 对了,这个就是单个目录下符合kbuild系统的规则文件。 92 | 93 | 你还记得编译一个内核模块时候那个Makefile中定义的obj-y, obj-m么?为什么我们在内核模块中的规则文件只需要定义这几个变量,就可以编译出目标文件和模块呢?原因就是在scripts/Makefile.build中包含了目标目录下的规则文件。 94 | 95 | 不懂也没关系,看多了日后自然知晓。 96 | 97 | 先看一下当前包含的文件,这次包含的是arch/x86/boot/Makefile。里面有这么一句。 98 | 99 | ``` 100 | quiet_cmd_image = BUILD $@ 101 | cmd_image = $(obj)/tools/build $(obj)/setup.bin $(obj)/vmlinux.bin \ 102 | $(obj)/zoffset.h $@ 103 | 104 | $(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE 105 | $(call if_changed,image) 106 | @echo 'Kernel: $@ is ready' ' (#'`cat .version`')' 107 | ``` 108 | 109 | 简单明了,会心一笑。 110 | 111 | 所以最后的最后是通过tools/build这个用户态工具生成bzImage文件,而依赖的文件是setup.bin和vmlinux.bin。 112 | 113 | 当然加上绝对路径后,这几个文件分别是, 114 | 115 | ``` 116 | arch/x86/boot/tools/build 117 | arch/x86/boot/setup.bin 118 | arch/x86/boot/vmlinux.bin 119 | arch/x86/boot/zoffset.h arch/x86/boot/bzImage 120 | ``` 121 | 122 | # 看看这个build都做了什么~ 123 | 124 | 这是一个用户态的程序,文件在arch/x86/boot/tools/build.c。虽然说main函数的主体结构相对简洁清晰,但实际上包含了不少文件格式相关的检查和动态填充。不少细节我也不是很懂,而且本主题关注的还是bzImage编译过程,所以细节部分就暂且略过。 125 | 126 | 来看我们能看得明白的,按照相关线索抽取了代码 127 | 128 | 回忆一下最后build的命令: 129 | 130 | ``` 131 | $(obj)/tools/build $(obj)/setup.bin $(obj)/vmlinux.bin \ 132 | $(obj)/zoffset.h $@ 133 | ``` 134 | 135 | ## 拷贝setup.bin 136 | 137 | ``` 138 | dest = fopen(argv[4], "w"); 139 | 140 | file = fopen(argv[1], "r"); 141 | c = fread(buf, 1, sizeof(buf), file); 142 | 143 | if (fwrite(buf, 1, i, dest) != i) 144 | die("Writing setup failed"); 145 | ``` 146 | 147 | * 先打开了bzImage,叫dest。 148 | * 然后读取setup.bin到buf 149 | * 最后写入bzImage 150 | 151 | ## 拷贝vmlinux.bin 152 | 153 | ``` 154 | fd = open(argv[2], O_RDONLY); 155 | 156 | kernel = mmap(NULL, sz, PROT_READ, MAP_SHARED, fd, 0); 157 | 158 | /* Copy the kernel code */ 159 | crc = partial_crc32(kernel, sz, crc); 160 | if (fwrite(kernel, 1, sz, dest) != sz) 161 | die("Writing kernel failed"); 162 | ``` 163 | 164 | * 先打开了vmlinux.bin 165 | * 做了一下map 166 | * 拷贝到了dest指向的bzImage 167 | 168 | 是不是也挺简单的呢。 169 | 170 | ## 一张图显示依赖关系 171 | 172 | ``` 173 | arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin 174 | \ / 175 | \ / 176 | bzImage 177 | ``` 178 | 179 | 丑是丑了点,毕竟清晰了些。 180 | 181 | # 心中的疑惑 182 | 183 | bzImage的编译过程就告一段落,然而心中的疑惑更多了。作为保存在磁盘上的一个文件,grub又是如何加载?加载到内存的哪个位置?加载时首先执行的是哪条指令? 184 | 185 | bzImage的两个组成部分vmlinux.bin和setup.bin又是怎么出现的呢? 186 | 187 | 路漫漫其修远兮,吾将上下而求索 188 | 189 | 190 | [1]: /brief_tutorial_on_kbuild/02_common_targets_in_kernel.md 191 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/08_rule_for_setupbin.md: -------------------------------------------------------------------------------- 1 | 书接上回,bzImage由setup.bin和vmlinux.bin两个文件粘合而成。这次我们来看看setup.bin的诞生记。 2 | 3 | # 寻找目标 4 | 5 | 老套路,第一步就是找一下setup.bin这个目标的规则。还记得之前我们走到哪里了么?对,arch/x86/boot/Makefile。 6 | 7 | ``` 8 | $(obj)/setup.bin: $(obj)/setup.elf FORCE 9 | $(call if_changed,objcopy) 10 | ``` 11 | 12 | 下一步呢? 对了,找cmd_objcopy。这次定义的地方稍有不同,不在scripts/Makefile.build,而是在scripts/Makefile.lib。 13 | 14 | ``` 15 | # Objcopy 16 | # ------------------------------------- 17 | 18 | quiet_cmd_objcopy = OBJCOPY $@ 19 | cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) $(OBJCOPYFLAGS_$(@F)) $< $@ 20 | ``` 21 | 22 | 原来setup.bin是由setup.elf经过objcopy而来。那看来想要弄清楚就要看看setup.elf的来历。走,既然已经到这里了,那咱就再入一层~ 23 | 24 | # 深入虎穴 25 | 26 | ``` 27 | $(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE 28 | $(call if_changed,ld) 29 | ``` 30 | 31 | 怎么样,现在是不是驾轻就熟了。看到这个也基本能够猜出个八九不离十。setup.elf是由$(SETUP_OBJS)链接而成的。嗯,没想到这么简单,白白浪费了我这么气势磅礴的一个标题了。 32 | 33 | 为了弥补点什么,咱把SETUP_OBJS的内容也给大家展开了。 34 | 35 | ``` 36 | SETUP_OBJS = $(addprefix $(obj)/,$(setup-y)) 37 | ``` 38 | 39 | 原来还套了那么一层: 40 | 41 | ``` 42 | setup-y += a20.o bioscall.o cmdline.o copy.o cpu.o cpuflags.o cpucheck.o 43 | setup-y += early_serial_console.o edd.o header.o main.o memory.o 44 | setup-y += pm.o pmjump.o printf.o regs.o string.o tty.o video.o 45 | setup-y += video-mode.o version.o 46 | setup-$(CONFIG_X86_APM_BOOT) += apm.o 47 | 48 | # The link order of the video-*.o modules can matter. In particular, 49 | # video-vga.o *must* be listed first, followed by video-vesa.o. 50 | # Hardware-specific drivers should follow in the order they should be 51 | # probed, and video-bios.o should typically be last. 52 | setup-y += video-vga.o 53 | setup-y += video-vesa.o 54 | setup-y += video-bios.o 55 | ``` 56 | 57 | 嗯,够多,终于能勉强配得上咱这个霸气的标题了~ 58 | 59 | # 图文并茂 60 | 61 | setup.bin的编译过程确实简单,来一张图略微总结那么一下子。 62 | 63 | ``` 64 | a20.o bioscall.o cmdline.o copy.o 65 | cpu.o cpuflags.o cpucheck.o 66 | edd.o header.o main.o memory.o 67 | tty.o pmjump.o printf.o regs.o 68 | pm.o string.o video.o 69 | video-mode.o version.o 70 | early_serial_console.o 71 | 72 | video-vga.o 73 | video-vesa.o 74 | video-bios.o 75 | 76 | || 77 | || <--- arch/x86/boot/setup.ld 78 | || 79 | \/ 80 | 81 | setup.elf 82 | 83 | || 84 | \/ 85 | 86 | setup.bin 87 | ``` 88 | 89 | 这么一看,东西还挺多的啊。 好了,这个我们也看完啦,是不是感觉so easy~ 90 | 91 | # 未完待续 92 | 93 | bzImage的组成部分除了setup.bin还有另一半—vmlinux.bin。是不是它也这么简单明了呢?是不是它能解除我们心中的一些疑惑呢? 94 | 95 | 不要走开,即将呈现~ 96 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/09_rule_for_vmlinux_bin.md: -------------------------------------------------------------------------------- 1 | 真没有想到,在编译过内核的源码目录下,你可以找到两个同叫vmlinux的文件。 2 | 3 | ``` 4 | $find . -name vmlinux 5 | ./vmlinux 6 | ./arch/x86/boot/compressed/vmlinux 7 | ``` 8 | 9 | 怎么样,你之前有发现过么?这还是在探索vmlinux.bin的过程中发现的秘密。 10 | 11 | 他们究竟都是干什么用的?有什么联系?和bzImage之间有关联么?让我们来揭开这神秘的面纱。 12 | 13 | 14 | # 隐藏的vmlinux 15 | 16 | 老规矩,先来看看vmlinux.bin的规则。 17 | 18 | ``` 19 | $(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE 20 | $(call if_changed,objcopy) 21 | ``` 22 | 23 | 嗯,看到刚才find中发现的vmlinux了不?原来vmlinux.bin是这个vmlinux通过objcopy而来。那这个都包含了谁? 又和根目录下的vmlinux有什么关系呢?你们俩真是太像了。 24 | 25 | ``` 26 | $(obj)/compressed/vmlinux: FORCE 27 | $(Q)$(MAKE) $(build)=$(obj)/compressed $@ 28 | ``` 29 | 30 | 原来老人家还有一个单独的目录,再次调用了make命令。 31 | 32 | # 揭开面纱 33 | 34 | 检验基本功的时候又来了,还记得这个命令究竟是做了什么么?还记得这个时候,规则文件是要去哪里找么?如果想不起来可以去回顾前面几篇入门文章。 35 | 36 | 在arch/x86/boot/compressd/Makefile中,找到了规则: 37 | 38 | ``` 39 | $(obj)/vmlinux: $(vmlinux-objs-y) FORCE 40 | $(call if_changed,check_data_rel) 41 | $(call if_changed,ld) 42 | ``` 43 | 44 | 偷个懒,猜一下,这个vmlinux就是把变量vmlinux-objs-y中的所有目标链接而成的。是不是觉得易如反掌了? 45 | 46 | 好,那来看看这个变量里都有谁。 47 | 48 | ``` 49 | vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \ 50 | $(obj)/string.o $(obj)/cmdline.o $(obj)/error.o \ 51 | $(obj)/piggy.o $(obj)/cpuflags.o 52 | ``` 53 | 54 | 好少啊,感觉比setup.bin还少。貌似挺简单的啊,这么着就算是把整个bzImage的编译流程都走完了~ 55 | 56 | 真的完了么? 你有没有发现什么不对劲? 57 | 58 | 对了,我们的根目录的vmlinux呢?难道放到启动目录下的内核里面没有根目录的vmlinux?不对啊,根目录的vmlinux可是包含了所有内核真正的代码的啊。 59 | 60 | 是的,你发现的没错,我们还没有真正走到内核编译的最深处。 61 | 62 | # 石破惊天 63 | 64 | 也不知道是什么机缘巧合,竟然发现在arch/x86/boot/compress/目录下面的piggy.S这个文件是编译时生成的。 65 | 66 | 在我的环境上,这个文件看上去像是这样: 67 | 68 | ``` 69 | .section ".rodata..compressed","a",@progbits 70 | .globl z_input_len 71 | z_input_len = 6957106 72 | .globl z_output_len 73 | z_output_len = 22677744 74 | .globl input_data, input_data_end 75 | input_data: 76 | .incbin "arch/x86/boot/compressed/vmlinux.bin.gz" 77 | input_data_end: 78 | ``` 79 | 80 | 怎么样,你有没有觉得大跌眼镜?简直就是FXXK。 81 | 82 | 在汇编代码中直接包含了一个文件。让我们看看这个incbin语句是什么含义吧。 83 | 84 | ``` 85 | You can use INCBIN to include executable files, literals, or any arbitrary data. The contents of the file are added to the current ELF section, byte for byte, without being interpreted in any way. 86 | ``` 87 | 88 | 哥们直接把整个内核给包进来了,小生佩服。 89 | 90 | # 再烧一次脑 91 | 92 | 为了确认,我们再来看看这个vmlinux.bin.gz是不是真的是内核的代码。 93 | 94 | 注意,下面的关系还真是有点烧脑。 95 | 96 | 先来看vmlinux.bin.gz: 97 | 98 | ``` 99 | vmlinux.bin.all-y := $(obj)/vmlinux.bin 100 | 101 | $(obj)/vmlinux.bin.gz: $(vmlinux.bin.all-y) FORCE 102 | $(call if_changed,gzip) 103 | ``` 104 | 105 | 那再来看$(obj)/vmlinux.bin。注意哦,这个已经是arch/x86/boot/compressed/vmlinux.bin,而不是arxh/x86/boot/vmlinux.bin了。怎么样,是不是有点烧脑? 106 | 107 | ``` 108 | $(obj)/vmlinux.bin: vmlinux FORCE 109 | $(call if_changed,objcopy) 110 | ``` 111 | 112 | 而这个时候的依赖,vmlinux,就是根目录下的vmlinux了。 113 | 114 | At last。 115 | 116 | 终于,我们看清楚了内核代码是怎么样打包到bzImage中了。 117 | 118 | # 一张图解说 119 | 120 | 我知道,你肯定已经晕的不能再晕了。我们还是用一张图来解说一下。 121 | 122 | ``` 123 | vmlinux 124 | 125 | || objcopy 126 | \/ 127 | 128 | arch/x86/boot/compressed/vmlinux.bin 129 | 130 | 131 | || gzip 132 | \/ 133 | 134 | arch/x86/boot/compressed/vmlinux.bin.gz 135 | 136 | || include 137 | \/ 138 | 139 | piggy.o head_$(BITS).o 140 | misc.o 141 | cmdline.o 142 | error.o 143 | cpuflsgs.o 144 | 145 | || ld <--- arch/x86/boot/compressed/vmlinux.lds 146 | \/ 147 | 148 | arch/x86/boot/compressed/vmlinux 149 | 150 | || objcopy 151 | \/ 152 | 153 | arch/x86/boot/vmlinux.bin 154 | ``` 155 | 156 | 是不是稍微的清晰了那么一些些? 157 | 158 | # 真相大白 159 | 160 | 终于知道了内核还会被压缩,还会被放在一个叫piggy.o的文件中,然后再被打包成一个bzImage放到启动目录中被加载。 161 | 162 | 没想到内核的大侠们也挺逗的,起个名字尽然还有点冷幽默。 163 | 164 | 整个内核的编译过程已经基本走完了,你可能会问知道这个编译的过程除了好玩还能有什么用呢? 165 | 166 | 我想除了能用来参加面试之外,可能还有一个原因,那就是你有机会明白内核启动时的页表是长什么样的。嗯?你不知道?不着急,你的内核之旅可能才刚刚开始。 167 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/11_location_of_common_targets.md: -------------------------------------------------------------------------------- 1 | # bzImage isoimage 2 | 3 | ``` 4 | Makefile -- include arch/x86/Makefile 5 | arch/x86/Makefile 6 | BOOT_TARGETS = bzlilo bzdisk fdimage fdimage144 fdimage288 isoimage 7 | bzImage: 8 | $(BOOT_TARGETS) 9 | ``` 10 | 11 | #make kernel/fork.o 12 | 13 | 原来在根目录的Makefile中,单独为.o目标做了这么一个规则。 14 | 15 | ``` 16 | "Makefile" 17 | 18 | %.o: %.c prepare scripts FORCE 19 | $(Q)$(MAKE) $(build)=$(build-dir) $(target-dir)$(notdir $@) 20 | ``` 21 | 22 | #make drivers/net/ethernet/mellanox/mlx4/mlx4_en.ko 23 | 24 | 原来这个也是在根Makefile目录下有单独的一个目标 25 | 26 | ``` 27 | "Makefile" 28 | 29 | %.ko: prepare scripts FORCE 30 | @echo target is $@ 31 | $(cmd_crmodverdir) 32 | $(Q)$(MAKE) KBUILD_MODULES=$(if $(CONFIG_MODULES),1) \ 33 | $(build)=$(build-dir) $(@:.ko=.o) 34 | $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost 35 | ``` 36 | 37 | #make M=drivers/net/ethernet/mellanox/mlx4/ 38 | 39 | 这个也是在根目录的Makefile中定义 40 | 41 | ``` 42 | "Makefile" 43 | 44 | ifeq ("$(origin M)", "command line") 45 | KBUILD_EXTMOD := $(M) 46 | endif 47 | 48 | ifeq ($(KBUILD_EXTMOD),) 49 | _all: all 50 | else 51 | _all: modules 52 | endif 53 | 54 | module-dirs := $(addprefix _module_,$(KBUILD_EXTMOD)) 55 | PHONY += $(module-dirs) modules 56 | $(module-dirs): crmodverdir $(objtree)/Module.symvers 57 | $(Q)$(MAKE) $(build)=$(patsubst _module_%,%,$@) 58 | 59 | modules: $(module-dirs) 60 | @$(kecho) ' Building modules, stage 2.'; 61 | $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost 62 | ``` 63 | 64 | 首先会解析是不是有M变量在命令行定义。如果有,则添加目标modules。 65 | 仔细看通常内核模块编译的两个阶段,在上面这段代码中,就很明显了。 66 | 67 | # buit-in.o 68 | 69 | 在scripts/Makefile.build中定义 70 | 71 | ``` 72 | builtin-target := $(obj)/built-in.o 73 | 74 | # If the list of objects to link is empty, just create an empty built-in.o 75 | cmd_link_o_target = $(if $(strip $(obj-y)),\ 76 | $(cmd_make_builtin) $@ $(filter $(obj-y), $^) \ 77 | $(cmd_secanalysis),\ 78 | $(cmd_make_empty_builtin) $@) 79 | 80 | $(builtin-target): $(obj-y) FORCE 81 | $(call if_changed,link_o_target) 82 | 83 | ``` -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/13_root_makefile.md: -------------------------------------------------------------------------------- 1 | 编译内核的这一套叫做kbuild,是在makefile的基础上,为了方便和统一内核编译搭建的一套自成体系的系统。我们现在从整体结构上来学习以下kbuild系统。 2 | 3 | # 根Makefile的结构 4 | 5 | 首先我们从根Makefile开始,因为最基本的一切都是从根Makefile开始的。 6 | 7 | 8 | ``` 9 | !sub_make_done 10 | 设置了一些参数,如: 11 | KBUILD_EXTMOD := $(M) 真正干活的时候,会以这个作区分,执行的操作不一样 12 | sub_make_done := 1 13 | end 14 | 15 | need_sub_make 16 | 某些情况下会重新执行一下make 17 | make -C dir -f Makefile $(MAKECMDGOALS) 18 | end 19 | 20 | 设置下面几个参数,决定这次构建的方式 21 | config-build := 22 | single-build := 23 | mixed-build := 24 | 25 | mixed-build 26 | 对$(MAKECMDGOALS)中的目标,依次执行make -f Makefile $$i 27 | else 28 | 到这里才开始真正干活 29 | 30 | kbuild核心文件 31 | include scripts/Kbuild.include 32 | 33 | config-build 34 | 构建配置,如make menuconfig 35 | else 36 | 读取配置,看上去和.config一样 37 | include include/config/auto.conf 38 | 39 | !KBUILD_EXTMOD 40 | build-dir := . 41 | else 42 | build-dir := $(KBUILD_EXTMOD) 43 | end 44 | 45 | PHONY += $(build-dir) 46 | $(build-dir): prepare 47 | $(Q)$(MAKE) $(build)=$@ need-builtin=1 need-modorder=1 $(single-goals) 48 | 49 | end 50 | end 51 | ``` 52 | 53 | 我把根Makefile的骨架子,通过注释的方式列了出来。感觉终于对根Makefile有了点了解。 54 | 55 | 首先根Makefile会判断以下是否需要重新执行一下make -f Makefile。接下来会根据编译目标,MAKECMDGOALS来区分,包括 56 | 57 | * config-build 58 | * single-build 59 | * mixed-build 60 | 61 | 其中config-build就是生成.config配置的。mixed-build是有混合目标时触发的,如同时有config/clean/真实目标,kbuild会把他们分开,依次执行make。single-build比较特殊,单独划出了一类目标处理。这么一看感觉两千多行的Makefile,也不是那么晦涩了。 62 | 63 | ## 小tip 64 | 65 | kbuild涉及的文件较多,还有条件判断,这样导致目标的依赖不一定很清楚。而make -p可以把整个编译的目标、依赖和命令都输出出来。可以帮助我们理解构建时的具体内容。 66 | 67 | 比如 68 | 69 | >> make -p modules 70 | 71 | 可以打印出目标是modules时所有的变量和规则定义。 72 | 73 | 不过这个输出还是有点大的。。。 74 | 75 | # kbuild 76 | 77 | * Makefile.build文件 78 | * 常用函数 79 | 80 | ## 从Makefile.build开始 81 | 82 | 在上面我摘出来的根Makefile文件末尾,我保留了一段具体的目标规则的代码。这部分,就是内核编译过程中干活的核心。 83 | 84 | 在内核编译文件内,我们可以看到很多类似下面的代码: 85 | 86 | >> $(Q)$(MAKE) $(build)=dir 87 | 88 | 其中build是在Kbuild.include定义的变量。 89 | 90 | >> build := -f $(srctree)/scripts/Makefile.build obj 91 | 92 | 所以在调用时,真正执行的是 93 | 94 | >> make -f scripts/Makefile.build obj=dir 95 | 96 | 所以内核编译时,在继承了根Makefile导出的变量设置后,具体的工作交给了每个单独的Makefile.build来完成。 97 | 98 | 了解了这个调用方式后,就来看看这个Makefile.build的庐山真面目。 99 | 100 | ``` 101 | src := $(obj) 102 | 103 | 默认目标 104 | $(obj)/: 105 | 106 | include include/config/auto.conf 107 | 108 | include scripts/Kbuild.include 109 | 设置kbuild-file变量,接下来将被引用。 srctree在根Makefile中定义 110 | kbuild-dir = $(if $(filter /%,$(src)),$(src),$(srctree)/$(src)) 111 | kbuild-file = $(or $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Makefile) 112 | include $(kbuild-file) 113 | 定义了obj-y,obj-m等变量 114 | include scripts/Makefile.lib 115 | 根据kbuild-file设置了subdir-ym 116 | 117 | 设置targets-for-modules,targets-for-builtin变量 118 | 119 | 默认目标依赖了subdir-ym 120 | $(obj)/: $(if $(KBUILD_BUILTIN), $(targets-for-builtin)) \ 121 | $(if $(KBUILD_MODULES), $(targets-for-modules)) \ 122 | $(subdir-ym) $(always-y) 123 | @: 124 | 125 | 对每个subdir-ym,再执行make完成递归 126 | PHONY += $(subdir-ym) 127 | $(subdir-ym): 128 | $(Q)$(MAKE) $(build)=$@ \ 129 | need-builtin=$(if $(filter $@/built-in.a, $(subdir-builtin)),1) \ 130 | need-modorder=$(if $(filter $@/modules.order, $(subdir-modorder)),1) \ 131 | $(filter $@/%, $(single-subdir-goals)) 132 | ``` 133 | 134 | 从上面的片段看,Makefile.build不是一个人在战斗。还有他的同伴Kbuild.include,Makefile.lib在一起协作。 135 | 136 | 每次调用Makefile.build时,就会去obj指定的目录下找到Kbuild或者Makefile。而这两个文件中就是按照kbuild定义的目标obj-y/obj-m。如果是目录,就会递归得进入下层目录继续编译。并且因为subdir-ym是默认目标的依赖,所以保证了下层目标会先生成。 137 | 138 | ## 层次结构 139 | 140 | 整个内核代码从kbuild的角度来看,可能是这样的。 141 | 142 | ``` 143 | +-- Kbuild 144 | | 145 | +--+ init 146 | | | 147 | | +-- Makefile 148 | | 149 | +--+ fs 150 | | | 151 | | +-- Makefile 152 | | | 153 | | +-+ ext4 154 | | | 155 | | +-- Makefile 156 | | 157 | +--+ kernel 158 | | | 159 | | +-- Makefile 160 | | | 161 | | +-+ sched 162 | | | 163 | | +-- Makefile 164 | | 165 | +-- 166 | ``` 167 | 168 | 每个层级的Kbuild/Makefile定义了obj-y/obj-m,如果需要则先进入下层目录,形成递归。 169 | 170 | ## 常用函数 171 | 172 | scripts/Kbuild.include 173 | 174 | ### if_changed 175 | 176 | 内核Makefile中常见的构建方法就是 177 | 178 | >> $(call if_changed,xxx) 179 | 180 | 这个函数的作用是判断目标的依赖是否有变化,如果有变化,则调用xxx函数进行构建。 181 | 182 | 来看一下定义 183 | 184 | ``` 185 | # Execute command if command has changed or prerequisite(s) are updated. 186 | if_changed = $(if $(if-changed-cond),$(cmd_and_savecmd),@:) 187 | 188 | cmd_and_savecmd = \ 189 | $(cmd); \ 190 | printf '%s\n' 'savedcmd_$@ := $(make-cmd)' > $(dot-target).cmd 191 | 192 | ``` 193 | 194 | 就是当if-changed-cond不是空,就执行cmd_and_savecmd。也就是调用了cmd后,再把构建当前目标的命令保存到一个临时文件里。比如查看.vmlinux.o.cmd,就可以知道在链接vmlinux.o时的具体命令。 195 | 196 | 接下来就是看这个if-changed-cond是如何判断命令行和依赖是否有更新。 197 | 198 | ``` 199 | if-changed-cond = $(newer-prereqs)$(cmd-check)$(check-FORCE) 200 | ``` 201 | 202 | 这里判断了依赖和命令行有没有变化。最后这个FORCE的作用还不是很清楚。 203 | 204 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/14_bzImage_whole_picture.md: -------------------------------------------------------------------------------- 1 | 在[启动镜像bzImage的前世今生][1]我们看到bzImage由setup.bin和vmlinux.bin两部分组成。 2 | 3 | 经过对这两部分的探索后,将整体展开在一起。希望有一个比较清晰的全景。 4 | 5 | 6 | ``` 7 | * 8 | | <-- vmlinux.lds.S 9 | | 10 | vmlinux 11 | | 12 | | <-- objdump 13 | | 14 | arch/x86/boot/compressed/vmlinux.bin 15 | | 16 | | <-- compress 17 | | 18 | arch/x86/boot/compressed/vmlinux.bin.zst 19 | | 20 | | <-- mkpiggy 21 | | 22 | arch/x86/boot/compressed/piggy.S 23 | | 24 | | 25 | | arch/x86/boot/compressed/* 26 | arch/x86/boot/* \ / 27 | | \/ 28 | | <-- setup.ld | <-- vmlinux.lds 29 | | | 30 | | v 31 | | arch/x86/boot/compressed/vmlinux 32 | | | 33 | | | <-- objcopy 34 | | | 35 | v v 36 | arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin 37 | \ / 38 | \ / 39 | arch/x86/boot/bzImage 40 | ``` 41 | 42 | 其中几个链接脚本具体是: 43 | 44 | * vmlinux.lds.S -> arch/x86/kernel/vmlinux.lds.S 45 | * setup.ld -> arch/x86/boot/setup.ld 46 | * vmlinux.lds -> arch/x86/boot/compressed/vmlinux.lds 47 | 48 | [1]: /brief_tutorial_on_kbuild/07_rules_for_bzImage.md 49 | -------------------------------------------------------------------------------- /brief_tutorial_on_kbuild/menuconfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/brief_tutorial_on_kbuild/menuconfig.png -------------------------------------------------------------------------------- /bus_driver_device/00-device_model.md: -------------------------------------------------------------------------------- 1 | 内核除了管理内存,其很大一部分工作就是管理设备了。如果大家看一眼代码就可以发现drivers目录驱动的代码量估计占一半以上。 2 | 3 | 现在我们就来看看内核是如何管理这么庞大数量的设备的,其中有三个比较重要的概念: 4 | 5 | * 总线 6 | * 驱动 7 | * 设备 8 | 9 | # 总线 10 | 11 | 第一个我想说的是总线bus,因为接着我们就可以看到总线连接着驱动和设备,是整个设备模型的纽带。 12 | 13 | [总线][1] 14 | 15 | # 驱动 16 | 17 | 驱动和设备部分前后,先聊聊驱动。 18 | 19 | [驱动][2] 20 | 21 | # 设备 22 | 23 | 终于要看到设备了。 24 | 25 | [设备][3] 26 | 27 | # 如何关联设备和驱动 28 | 29 | 在上面小节的描述中,我们跳过了一个非常总要的环节,那就是驱动和设备是如何关联到一起的。 30 | 31 | [绑定][4] 32 | 33 | [1]: /bus_driver_device/01-bus.md 34 | [2]: /bus_driver_device/02-driver.md 35 | [3]: /bus_driver_device/03-device.md 36 | [4]: /bus_driver_device/04-bind.md 37 | -------------------------------------------------------------------------------- /bus_driver_device/01-bus.md: -------------------------------------------------------------------------------- 1 | 可能是之前有做过pci的关系,对总线不感到陌生。但是如果你头一次接触,可能不太理解总线的定义。 2 | 3 | 总的来说,总线是一个硬件架构的概念。举一个不恰当的类比,就好像我们马路上的车道:机动车,非机动车和人行道。不同的车、人需要在不同的地方行驶。同理,计算机设备也有这样的要求。不同的设备需要在指定的总线上才能正常运行。所以我们先把路认清,这样有了车就知道在哪里跑了。 4 | 5 | # 数据结构 6 | 7 | 先从数据结构入手,看看总线对应的数据类型。在linux内核中总线用数据结构bus_type表示。 8 | 9 | ``` 10 | bus_type 11 | +------------------------------------+ 12 | |name | 13 | |dev_name | 14 | | (char *) | 15 | +------------------------------------+ 16 | |bus_groups | 17 | |dev_groups | 18 | |drv_groups | 19 | | (struct attribute_group**) | 20 | +------------------------------------+ 21 | |match | 22 | |uevent | 23 | |probe | 24 | |remove | 25 | |shutdown | 26 | | | 27 | +------------------------------------+ 28 | |p | 29 | | (struct subsys_private*) | 30 | | +--------------------------------+ 31 | | |bus | 32 | | | (struct bus_type*) | 33 | | |subsys | 34 | | | +----------------------------+ 35 | | | |kobj.name | = bus->name 36 | | | |kobj.kset | = bus_kset 37 | | | |kobj.ktype | = bus_ktype 38 | | | +----------------------------+ 39 | | |devices_kset | 40 | | |drivers_kset | 41 | | | (struct kset) | 42 | | |klist_devices | 43 | | |klist_drivers | 44 | | | (struct klist) | 45 | | |class | 46 | | | (struct class*) | 47 | +---+--------------------------------+ 48 | ``` 49 | 50 | 简单解释一下部分成员的含义: 51 | 52 | * name/dev_name: 总线自己和下属设备的名字 53 | * bus/dev/drv_groups: 相关sysfs的文件 54 | * match/probe...: 匹配设备,操作设备的接口 55 | * p.subsys.kobj: kobj/sysfs 树的节点 56 | * p.subsys.devices_kset:下属设备的kobj/sysfs节点 57 | * p.subsys.drivers_kset:下属驱动的kobj/sysfs节点 58 | 59 | 估计第一次看很多都不明白,不过不要紧因为重点内容基本都在这里了。 60 | 61 | # 总线树 62 | 63 | 通过kobj的连接,总线在内核中形成一棵树。(其实严格来说,这棵树就一层。当然这是我的理解。) 64 | 65 | 这颗树可以通过ls /sys/bus查看。在这里画一个简图突出一下。 66 | 67 | ``` 68 | bus 69 | | 70 | | 71 | --+------------------+------------------+----- 72 | | | 73 | | | 74 | pci nd 75 | | | 76 | | | 77 | +- drivers +- drivers 78 | | | 79 | | | 80 | +- devices +- devices 81 | ``` 82 | 83 | 这张图中我只列出了总线类型中的两种:pci, nd。所以各个总线之前是平级的。 84 | 85 | 而接下来可以看到,每个总线下面各自带了drivers, devices两目录。其中将会列出该总线上所属的驱动和设备。 86 | 87 | 所以从这点也可以看出总线是整个设备模型中的纽带了。 88 | -------------------------------------------------------------------------------- /bus_driver_device/02-driver.md: -------------------------------------------------------------------------------- 1 | 接下来讲讲设备模型中的驱动,和之前套路一样,先看看驱动的数据结构。 2 | 3 | # 数据结构 4 | 5 | 内核中用结构体device_driver来表示一个驱动。不过大家通常在驱动程序中看到是另外的结构体,比如pci驱动用的是pci_driver。但是我们打开这个pci_driver就可以看到其中包含了device_driver。所以驱动的核心数据结构还是device_driver。 6 | 7 | ``` 8 | device_driver 9 | +----------------------------------+ 10 | |name | 11 | | (char *) | 12 | +----------------------------------+ 13 | |bus | 14 | | (struct bus_type*) | 15 | +----------------------------------+ 16 | |owner | 17 | | (struct module*) | 18 | +----------------------------------+ 19 | |probe | 20 | |remove | 21 | |shutdown | 22 | |suspend | 23 | |resume | 24 | | | 25 | +----------------------------------+ 26 | |p | 27 | | (struct driver_private*) | 28 | | +-----------------------------+ 29 | | |driver | 30 | | | (struct device_driver*) | 31 | | |kobj | 32 | | | +------------------------+ 33 | | | |name | = device_driver->name 34 | | | |kset | = device_driver->bus->p->drivers_kset 35 | | | |ktype | = driver_ktype 36 | | | +------------------------+ 37 | | |klist_devices | 38 | | | | 39 | +----+-----------------------------+ 40 | ``` 41 | 42 | 简单解释一下部分成员的含义: 43 | 44 | * name: 驱动名 45 | * bus: 对应的总线 46 | * probe/remove...: 驱动调用接口 47 | * p->kobj.name: kobj对应的名字 48 | * p->kobj.kset: kobj的父节点 49 | 50 | 暂时没有更多可以解释的,想强调的一点是p->kobj.kset设置成了对应总线的drivers_kset。这点表明了驱动在sysfs中的根是在对应的总线上。当我们看设备的时候,会发现两者之间有所不同。 51 | 52 | 针对驱动的图解将在下一节设备中给出。 53 | -------------------------------------------------------------------------------- /bus_driver_device/03-device.md: -------------------------------------------------------------------------------- 1 | 最后来看看设备。 2 | 3 | # 数据结构 4 | 5 | 内核中用结构体device来表示一个设备。不过大家通常在驱动程序中看到是另外的结构体,比如pci设备用的是pci_dev。但是我们打开这个pci_dev就可以看到其中包含了device。所以设备的核心数据结构还是device。 6 | 7 | ``` 8 | device 9 | +----------------------------------------+ 10 | |init_name | 11 | | (char *) | 12 | |devt | 13 | | (dev_t) | 14 | |id | 15 | | (u32) | 16 | +----------------------------------------+ 17 | |kobj | 18 | | (struct kobject) | 19 | | +-----------------------------------+ 20 | | |name | = device->init_name or 21 | | | | device->bus->dev_name + id 22 | | |kset | = devices_kset 23 | | |ktype | = device_ktype 24 | +----+-----------------------------------+ 25 | |type | 26 | | (struct device_type*) | 27 | +----------------------------------------+ 28 | |bus | = [pci_bus_type|nvdimm_bus_type] 29 | | (struct bus_type) | 30 | +----------------------------------------+ 31 | |driver | 32 | | (struct device_driver*) | 33 | +----------------------------------------+ 34 | |p | 35 | | (struct device_private *) | 36 | +----------------------------------------+ 37 | ``` 38 | 39 | 简单解释一下部分成员的含义: 40 | 41 | * init_name: 设备名(如果有的话) 42 | * id: 设备号 43 | * bus: 对应的总线 44 | * driver: 对应的驱动 45 | * kobj.name: kobj对应的名字,真正的设备名 46 | * kobj.kset: kobj的父节点 47 | 48 | 这里着重强调一点kobj.kset,这个值和驱动的父节点有所不同。驱动的父节点指向了总线,而设备的父节点是另一个根节点。这个时候我们可以来看看有了驱动和设备后,kobj树形结构的样子了。 49 | 50 | # 总线,驱动和设备的树 51 | 52 | ``` 53 | bus(bus_kset) <-------------+ devices(devices_kset) 54 | | | | 55 | --+-----------+------+----- | ----+----+------------- 56 | | | | | 57 | +- drivers +- devices | | 58 | | | | | 59 | +- drvA <- - - +- devA - * - * | > +- devA 60 | | | | | | 61 | | | +- subsystem --+ | 62 | | | | | 63 | +- drvB <- - - +- devB - * - * | > +- devB 64 | | | | | | 65 | | | +- subsystem --+ | 66 | | | | | 67 | +- drvC <- - - +- devC - * - * | > +- devC 68 | | | | | | 69 | | | +- subsystem --+ | 70 | | | | 71 | +- drvD <- - - +- devD - * - * - > +- devD 72 | ``` 73 | 74 | 从这张图我们可以看到 75 | 76 | * 总线下有驱动和设备 77 | * 设备和驱动存在着关联 78 | * 总线下的设备实际是一个软链接 79 | * 设备真正的根在devices_kset 80 | -------------------------------------------------------------------------------- /bus_driver_device/04-bind.md: -------------------------------------------------------------------------------- 1 | 在内核中驱动和设备发生关联称之为绑定bind。 2 | 3 | 而且有意思的是绑定的过程可以在两个地方发生。 4 | 5 | * 驱动加载 6 | * 设备发现 7 | 8 | 不过这两个地方最后都会通过函数__driver_attach()来处理。而这个过程又可以分成两步: 9 | 10 | * 匹配 11 | * 探测 12 | 13 | 接着我们就按照这两个步骤展开来看。 14 | 15 | # 匹配 16 | 17 | 匹配的过程由总线的match函数来处理。 18 | 19 | ``` 20 | static inline int driver_match_device(struct device_driver *drv, 21 | struct device *dev) 22 | { 23 | return drv->bus->match ? drv->bus->match(dev, drv) : 1; 24 | } 25 | ``` 26 | 27 | 那具体的操作就由总线来决定。比如pci总线上的match函数就是pci_match_device()。它的过程就是匹配pci的厂商号和设备号。 28 | 29 | # 探测 30 | 31 | 找到了匹配的设备和驱动后,就可以执行探测了。这个过程相对比较复杂,在当前的内核中,大多最后由总线的probe函数执行。 32 | 33 | 比如pci总线上的probe函数就是pci_device_probe()。它的作用就是去调用pci_driver中的probe函数。 34 | 35 | # 流程图 36 | 37 | 如果我们简单画出绑定过程的流程,大致如下: 38 | 39 | ``` 40 | __driver_attach 41 | driver_match_device() 42 | bus->match() 43 | driver_probe_device() 44 | bus->probe() 45 | ``` 46 | 47 | 从这点可以看出,总线的重要性。 48 | -------------------------------------------------------------------------------- /cgroup/00-index.md: -------------------------------------------------------------------------------- 1 | cgroup是计算机资源管理常用的管理设施,尤其是在云计算环境使用广泛。这里我们从cgroup的使用和架构上进行一次学习。 2 | 3 | # 基本使用 4 | 5 | 对大多数人来说,接触cgroup还是使用cgroup来控制应用程序的资源。所以这一节,我们先来看看 6 | 7 | [使用cgroup控制进程cpu和内存][1] 8 | 9 | # cgroup文件系统 10 | 11 | 在上一节基本使用中,我们通过创建目录,向指定文件写入内容来使用cgroup。我想你也一定好奇,为什么这么操作就能达到这样的效果? 12 | 13 | 这一切都要归功于[cgroup文件系统][2] 14 | 15 | # cgroup树形结构 16 | 17 | 从用户使用的角度看过了cgroup后,终于是时候揭开cgroup真实的面纱了。 18 | 19 | 在开始之前,我们先来好好看看cgroup的定义。来个借花献佛,我搬运一下[内核文档][3] 20 | 21 | ``` 22 | Control Groups provide a mechanism for aggregating/partitioning sets of 23 | tasks, and all their future children, into hierarchical groups with 24 | specialized behaviour. 25 | 26 | Definitions: 27 | 28 | A *cgroup* associates a set of tasks with a set of parameters for one 29 | or more subsystems. 30 | 31 | A *subsystem* is a module that makes use of the task grouping 32 | facilities provided by cgroups to treat groups of tasks in 33 | particular ways. A subsystem is typically a "resource controller" that 34 | schedules a resource or applies per-cgroup limits, but it may be 35 | anything that wants to act on a group of processes, e.g. a 36 | virtualization subsystem. 37 | 38 | A *hierarchy* is a set of cgroups arranged in a tree, such that 39 | every task in the system is in exactly one of the cgroups in the 40 | hierarchy, and a set of subsystems; each subsystem has system-specific 41 | state attached to each cgroup in the hierarchy. Each hierarchy has 42 | an instance of the cgroup virtual filesystem associated with it. 43 | ``` 44 | 45 | 从上面的描述中可以看到cgroup有两个重要的概念: 46 | 47 | * 层次结构 48 | * 子系统 49 | 50 | 层次结构是对系统中进程的切分,而子系统则赋予了cgroup在不同层面对进程的切变控制的方法。 51 | 52 | 每个子系统对系统的控制有着不同的方式,所以在本章我们主要讲述[cgroup的树形结构][4]。 53 | 54 | 虽然会涉及到一些子系统的内容,但其其中具体的精妙细节将留给各个子系统描述。 55 | 56 | # cgroup和进程的关联 57 | 58 | 接下来我们需要了解的是cgroup和进程的关系。 59 | 60 | 从上面的定义中,我们看到cgroup联结了计算机系统中的两部分内容: 61 | 62 | * 进程 63 | * 系统资源 64 | 65 | 在上一节中,我们看到了cgroup的subsystem将系统的资源按照层次结构做了切分。(当然具体怎么切的需要看对应的subsystem,cgroup本身只提供了一种层次化结构框架。)那接下来就要看看内核是如何把进程和cgroup联系起来的。 66 | 67 | [cgroup和进程的关联][5] 68 | 69 | # cgroup数据统计 70 | 71 | 最后一个有意思的部分就是cgroup上如果做数据同步。当整个cgroup树变得庞大后,如何有效将子cgroup的信息同步收集到父cgroup,来保证cgroup能有效控制资源,这是一个非常有意思的问题。 72 | 73 | [cgroup数据统计][6] 74 | 75 | [1]: /cgroup/01-control_cpu_mem_by_cgroup.md 76 | [2]: /cgroup/02-cgroup_fs.md 77 | [3]: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/cgroups.html 78 | [4]: /cgroup/03-hierarchy.md 79 | [5]: /cgroup/04-cgroup_and_process.md 80 | [6]: /cgroup/05-statistics.md 81 | -------------------------------------------------------------------------------- /cgroup/01-control_cpu_mem_by_cgroup.md: -------------------------------------------------------------------------------- 1 | 刚开始接触cgroup时,第一个任务就是用cgroup限制cpu/mem的使用。可能这也是大多数人接触到cgroup时,需要做的任务。 2 | 3 | # 使用cgroup限制进程能运行的cpu 4 | 5 | 在这个例子中,我们探寻一下如何用cgroup限制进程能够在哪些cpu上调度。在云计算场景中,我们通常会用这个功能来隔离不同的用户/业务,保证对应资源的使用情况。 6 | 7 | 这个过程分为两步: 8 | 9 | * 创建一个限制了cpu调度的cgroup 10 | * 将指定进程加入到该cgroup中 11 | 12 | ## 新建限制cpu的cgroup 13 | 14 | ``` 15 | mkdir /sys/fs/cgroup/cpuset/test 16 | echo "0-1" > /sys/fs/cgroup/cpuset/test/cpuset.cpus 17 | ``` 18 | 19 | 这样就创建了一个cgroup,并且限制了该cgroup中的进程只能运行在0-1号cpu上。 20 | 21 | ## 将指定进程加入cgoup 22 | 23 | ``` 24 | echo $pid > /sys/fs/cgroup/cpuset/test/cgroup.procs 25 | ``` 26 | 27 | 这么看坑你还是有点空,我们来看一个实际的例子 28 | 29 | ## 完整例子 30 | 31 | ``` 32 | mkdir /sys/fs/cgroup/cpuset/test 33 | 34 | echo "0-1" > /sys/fs/cgroup/cpuset/test/cpuset.cpus 35 | echo 0 > /sys/fs/cgroup/cpuset/test/cpuset.mems 36 | 37 | # echo current shell process number to cgroup.procs 38 | # so all child will inherit this attribute 39 | echo $$ > /sys/fs/cgroup/cpuset/test/cgroup.procs 40 | 41 | # stress on 4 cpus 42 | $ stress -c 4 43 | 44 | # while only 2 cpu is working 45 | $ top 46 | ``` 47 | 48 | 正常情况下,stress -c 4会使用4块cpu。但是因为将测试进程放进了cgroup,所以只有两个cpu在实际使用。 49 | 50 | # 使用cgroup限制内存使用 51 | 52 | 在这个例子中,我们探寻一下如何用cgroup限制进程能够使用的内存。在云计算场景中,我们通常会用这个功能来限制用户/业务的内存,保证对应资源的使用情况,避免有恶意用户/业务抢占资源。 53 | 54 | 这个过程分为两步: 55 | 56 | * 创建一个限制内存使用的cgroup 57 | * 将指定进程加入到该cgroup中 58 | 59 | ## 创建限制内存使用的cgroup 60 | 61 | ``` 62 | mkdir /sys/fs/cgroup/memory/test 63 | 64 | # limit 4M 65 | echo 4M > /sys/fs/cgroup/memory/test/memory.limit_in_bytes 66 | ``` 67 | 68 | ## 将指定进程加入cgoup 69 | 70 | ``` 71 | echo $pid > /sys/fs/cgroup/cpuset/test/cgroup.procs 72 | ``` 73 | 74 | 这一步完全一样。。。 75 | 76 | ## 完整例子 77 | 78 | 以例服人 79 | 80 | ``` 81 | # create a cgroup with memory limitation 82 | mkdir /sys/fs/cgroup/memory/test 83 | # limit 4M 84 | echo 4M > /sys/fs/cgroup/memory/test/memory.limit_in_bytes 85 | # disable swap, otherwise it will use swap to get more "memory" 86 | echo 0 > /sys/fs/cgroup/cpuset/rabbit/memory.swappiness 87 | 88 | # put ourself into this cgroup 89 | echo $$ > /sys/fs/cgroup/cpuset/rabbit/cgroup.procs 90 | 91 | # prepare a program eats memory 92 | cat eat_mem.c 93 | #include 94 | #include 95 | #include 96 | #include 97 | 98 | #define MB (1024 * 1024) 99 | 100 | int main(int argc, char *argv[]) 101 | { 102 | char *p; 103 | int i = 0; 104 | while(1) { 105 | p = (char *)malloc(MB); 106 | memset(p, 0, MB); 107 | printf("%dM memory allocated\n", ++i); 108 | sleep(1); 109 | } 110 | 111 | return 0; 112 | } 113 | 114 | # run to trigger oom 115 | ./eat_mem 116 | ``` 117 | 118 | 上面的例子中,有一点值得注意的是这个cgroup的swap被关掉了。否则的话,当进程内存不够,内核会将进程的内存交换到磁盘上。虽然进程的内存实际上是被限制了,但是无法触发oom观察到实验结果。 119 | 120 | 好了,希望上面两个例子能够让你亲眼看到cgroup的操作和效果。接下来我们就开始探索其中的奥秘了~ 121 | -------------------------------------------------------------------------------- /cgroup/cgroup_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/cgroup/cgroup_files.png -------------------------------------------------------------------------------- /cgroup/cgroup_fs_context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/cgroup/cgroup_fs_context.png -------------------------------------------------------------------------------- /data_struct/00-index.md: -------------------------------------------------------------------------------- 1 | 想要理解软件,编写出优美高效的代码离不开对数据结构的理解。虽然内核中基本沿用了通用的数据结构,但是有些要么增加了自己的使用方法,要么会为了符合内核需求提出自己的实现。 2 | 3 | 在这一章节,本人打算总结一下代码中遇到过的那些有意思的数据结构们。 4 | 5 | # 链表 6 | 7 | 链表是软件中常用的数据结构了,虽然没有什么新花头,但是架不住用的多。而且内核中为了自身需求还演化出了新花样。 8 | 9 | [双链表][1] 10 | 11 | [优先级队列][2] 12 | 13 | [哈希表][3] 14 | 15 | [Xarray][4] 16 | 17 | [B树][5] 18 | 19 | [Maple Tree][6] 20 | 21 | [Interval Tree][7] 22 | 23 | [1]: /data_struct/01-list.md 24 | [2]: /data_struct/03-plist.md 25 | [3]: /data_struct/02-hlist.md 26 | [4]: /data_struct/04-xarray.md 27 | [5]: /data_struct/05-btree.md 28 | [6]: /data_struct/06-maple_tree.md 29 | [7]: /data_struct/07-interval_tree.md 30 | -------------------------------------------------------------------------------- /data_struct/01-list.md: -------------------------------------------------------------------------------- 1 | 双链表大概是软件中最为常见也是最简单的数据结构之一了。所以在内核中的定义和使用也不难,只是用法上可能和其他地方的实现略有不同。 2 | 3 | # 单个的样子 4 | 5 | 这个样子很普通,一共两个成员,各自指向前后就行了。 6 | 7 | ``` 8 | list_head 9 | +-----------------------+ 10 | |prev | 11 | |next | 12 | | (struct list_head*)| 13 | +-----------------------+ 14 | ``` 15 | 16 | # 组合时的样子 17 | 18 | 组合起来样子也相对比较简单。 19 | 20 | ``` 21 | list_head 22 | +-----------+ 23 | |prev next|<-+ 24 | +-----------+ | 25 | ^ | 26 | | | 27 | | | Parent Data Parent Data 28 | | | +-----------------+ +-----------------+ 29 | | +----->|list_head |<---->|list_head |<-----+ 30 | | +-----------------+ +-----------------+ | 31 | | | 32 | +--------------------------------------------------------------------+ 33 | ``` 34 | 35 | 这里想要强调的一点是,内核中通常把list_head结构嵌入到某个真实的数据结构中,然后由一个list_head结构作为链表头。 36 | 37 | # 常用的API 38 | 39 | 常用的增删类API有: 40 | 41 | * list_add() 42 | * list_add_tail() 43 | * list_del() 44 | * list_move() 45 | * list_move_tail() 46 | 47 | 常用的遍历类API有: 48 | 49 | * list_for_each() 50 | * list_for_each_prev() 51 | * list_for_each_entry() 52 | * list_for_each_entry_reverse() 53 | -------------------------------------------------------------------------------- /data_struct/02-hlist.md: -------------------------------------------------------------------------------- 1 | 内核中的哈希表采用的是链式冲突解决方法。 2 | 3 | # 整体的样子 4 | 5 | ``` 6 | hlist_head 7 | +--------+ 8 | | | -> hlist_node -> hlist_node -> hlist_node 9 | +--------+ 10 | | | -> hlist_node -> hlist_node -> hlist_node 11 | +--------+ 12 | | | -> hlist_node -> hlist_node -> hlist_node 13 | +--------+ 14 | ``` 15 | 16 | 一个hash table是由多个hlist_head组成的,根据hash算法,会计算出对应的key到哪个bucket。 17 | 18 | 而每个bucket是一个以hlist_head为首,hlist_node为元素的链表。 19 | 20 | # hlist_head链表 21 | 22 | ``` 23 | 24 | hlist_head hlist_node hlist_node 25 | +--------+ +----------+ +----------+ 26 | | | +--|--pprev | +--|--pprev | 27 | | | | | | | | | 28 | | +-----|-+ | +-------|-+ | | 29 | | | | | | | | | 30 | | v | | v | | | 31 | |first --|--> | next --|--> | next | 32 | +--------+ +----------+ +----------+ 33 | 34 | ``` 35 | 36 | 仔细一看也算是个双向链表,不过pprev是指针的指针。 37 | 38 | 39 | # 常用API 40 | -------------------------------------------------------------------------------- /data_struct/03-plist.md: -------------------------------------------------------------------------------- 1 | 优先级列表,就是在插入链表时,要考虑链表元素的某个特征值。在内核中优先级列表由两个双链表组合实现。 2 | 3 | # 单个的样子 4 | 5 | ``` 6 | plist_node 7 | +-----------------------+ 8 | |prio | 9 | | (int) | 10 | |prio_list | 11 | |node_list | 12 | | (struct list_head*)| 13 | +-----------------------+ 14 | ``` 15 | 16 | 其中 17 | 18 | * prio表示元素的优先级 19 | * prio_list是优先级链表元素 20 | * node_list是整个链表元素 21 | 22 | 乍一看觉得为啥要两个链表,从下面整体的图上会看得比较清楚。 23 | 24 | # 整体的样子 25 | 26 | 内核开发者给优先级列表画了图: 27 | 28 | ``` 29 | pl:prio_list (only for plist_node) 30 | nl:node_list 31 | HEAD| NODE(S) 32 | | 33 | ||------------------------------------| 34 | ||->|pl|<->|pl|<--------------->|pl|<-| 35 | | |10| |21| |21| |21| |40| (prio) 36 | | | | | | | | | | | | 37 | | | | | | | | | | | | 38 | |->|nl|<->|nl|<->|nl|<->|nl|<->|nl|<->|nl|<-| 39 | |-------------------------------------------| 40 | ``` 41 | 42 | 意思是 43 | 44 | * prio_list是链接没有优先级中第一个元素的链表 45 | * node_list则是所有的元素都会链接的链表 46 | 47 | 不知道是不是讲清楚了,不过实现上就是这么有意思。 48 | 49 | # 常用API 50 | 51 | * plist_add() 52 | * plist_del() 53 | 54 | 主要就是这两个,没啥花花肠子。 55 | -------------------------------------------------------------------------------- /data_struct/05-btree.md: -------------------------------------------------------------------------------- 1 | maple tree是B树的优化形式,为了更好的理解maple tree,这里先对B树做一个铺垫学习。 2 | 3 | # B树的性质 4 | 5 | B树是平衡搜索树的一种,对于一个m阶B树它的性质有: 6 | 7 | * 平衡:所有的叶子节点都在一层 8 | * 有序:节点内有序,左子树都小于它,右子树都大于它 9 | * 多路:**最多**m-1个元素,根节点**最少**1个元素,其余节点**最少**(m/2上取整 - 1)个元素 10 | 11 | 而这些性质都是在树的操作上保证的。下面就看看B树的插入和删除操作 12 | 13 | # 插入操作 14 | 15 | 先找到需要插入的node,此时一定是叶子节点。 16 | 插入后,如果节点没有上溢出,则结束。如果上溢出,则进行拆分动作,将多处的元素插入父节点。 17 | 再判断父节点。 18 | 19 | 下面用一张图来观察拆分动作。 20 | 21 | 假设执行插入后,节点有5个元素,已经上溢出。这时候就要执行拆分,以中间元素B为分割点拆分。 22 | 23 | 24 | ``` 25 | idx 26 | +---+ +---+ 27 | | A | | C | 28 | +---+ +---+ 29 | | | | 30 | c0 c1 c2 31 | / 32 | / 33 | / 34 | / 35 | +---+ +---+ +---+ +---+ +---+ 36 | | X | | Y | | B | | J | | K | 37 | +---+ +---+ +---+ +---+ +---+ 38 | | | | | | | 39 | c10 c11 c12 c20 c21 c22 40 | ``` 41 | 42 | 拆分后,B元素上移到父节点,而右半部分作为子树也插入父节点。 43 | 44 | ``` 45 | idx 46 | +---+ +---+ +---+ 47 | | A | | B | | C | 48 | +---+ +---+ +---+ 49 | | | | | 50 | c0 c1 c' c2 51 | / \ 52 | / \ 53 | / \ 54 | / \ 55 | +---+ +---+ +---+ +---+ 56 | | X | | Y | | J | | K | 57 | +---+ +---+ +---+ +---+ 58 | | | | | | | 59 | c10 c11 c12 c20 c21 c22 60 | ``` 61 | 62 | # 删除操作 63 | 64 | 先找到需要删除的元素,如果该元素不在叶子节点,则找到其后继,并替换。总之确保是在叶子节点上做删除操作。 65 | 如果删除后,节点没有下溢出,那么删除操作完成。 66 | 此时再判断节点的兄弟,如果兄弟节点元素超过一半,那么就借一个过来。如果兄弟元素也不过,则合并。 67 | 如果是合并的,那么就往上走一层,看父节点是否下溢出。 68 | 69 | ## 借元素 70 | 71 | 我们先看看从兄弟节点上借元素的情况。 72 | 73 | 此时左子树的元素下溢出,我们要做的是把父节点上的元素拿来放到最后,然后把右子树的第一个孩子也拿过来。 74 | 然后右子树的第一个元素上移到父节点上。 75 | 76 | ``` 77 | idx 78 | +---+ +---+ +---+ 79 | | A | | B | | C | 80 | +---+ +---+ +---+ 81 | | | | | 82 | c0 c1 c2 c3 83 | / \ 84 | / \ 85 | / \ 86 | / \ 87 | +---+ +---+ +---+ +---+ +---+ +---+ 88 | | X | | Y | | J | | K | | L | | M | 89 | +---+ +---+ +---+ +---+ +---+ +---+ 90 | | | | | | | | | 91 | c10 c11 c12 c20 c21 c22 c23 c24 92 | ``` 93 | 94 | 最后的结果是这样。 95 | 96 | ``` 97 | idx 98 | +---+ +---+ +---+ 99 | | A | | J | | C | 100 | +---+ +---+ +---+ 101 | | | | | 102 | c0 c1 c2 c3 103 | / \ 104 | / \ 105 | / \ 106 | / \ 107 | +---+ +---+ +---+ +---+ +---+ +---+ 108 | | X | | Y | | B | | K | | L | | M | 109 | +---+ +---+ +---+ +---+ +---+ +---+ 110 | | | | | | | | | 111 | c10 c11 c12 c20 c21 c22 c23 c24 112 | ``` 113 | 114 | ## 合并兄弟 115 | 116 | 合并兄弟的操作实际是拆分操作的反向操作。 117 | 118 | 此时,左子树已经下溢出,其兄弟也没有足够的元素。 119 | 就需要把父节点的元素拉下来放到最后,然后把兄弟也加到后面。 120 | 121 | 122 | ``` 123 | idx 124 | +---+ +---+ +---+ 125 | | A | | B | | C | 126 | +---+ +---+ +---+ 127 | | | | | 128 | c0 c1 c2 c3 129 | / \ 130 | / \ 131 | / \ 132 | / \ 133 | +---+ +---+ +---+ 134 | | X | | J | | K | 135 | +---+ +---+ +---+ 136 | | | | | | 137 | c10 c11 c20 c21 c22 138 | ``` 139 | 140 | 最后的结果就像这样。 141 | 142 | ``` 143 | idx 144 | +---+ +---+ 145 | | A | | C | 146 | +---+ +---+ 147 | | | | 148 | c0 c1 c3 149 | / 150 | / 151 | / 152 | / 153 | +---+ +---+ +---+ +---+ 154 | | X | | B | | J | | K | 155 | +---+ +---+ +---+ +---+ 156 | | | | | | 157 | c10 c11 c20 c21 c22 158 | ``` 159 | -------------------------------------------------------------------------------- /data_struct/07-interval_tree.md: -------------------------------------------------------------------------------- 1 | Interval Tree(区段树)在内存管理中有用到,比如rmap和vma中。 2 | 3 | 一直觉得有点神秘,今天来看一下。 4 | 5 | # 是附带属性的红黑树 6 | 7 | 本质上区段树是一个红黑树,在红黑树的基础上增加了两个信息: 8 | 9 | * leftmost: 保存了整个树中最左叶子 10 | * subtree: 每个节点增加当前子树内区间最大值 11 | 12 | # 优势 13 | 14 | 和红黑树相比,区段树的优势在与查找某区间内有较差的节点时,可以借助新增的这两个信息加速查找。 15 | 16 | 具体参考区段树提供的api: 17 | 18 | * interval_tree_iter_first() 19 | * interval_tree_iter_next() 20 | 21 | # 测试代码 22 | 23 | 内核中有相关的测试代码,lib/interval_tree_test.c。这样可以看出简单的使用方法。 24 | 25 | -------------------------------------------------------------------------------- /interrupt_exception/00-start_from_hardware.md: -------------------------------------------------------------------------------- 1 | 学习、适用内核,中断和异常是不能绕过的一大知识点。只可惜出道这么多年,一直都没有认真地潜心研究这部分的内容。那这次就从硬件开始。 2 | 3 | # 从硬件开始 -- IDT 4 | 5 | 中断和异常的处理需要硬件的支持,知道了硬件可以说就是了解了一大半。而这部分的硬件在x86平台上就是熟知的IDT了。 6 | 7 | [从IDT开始][1] 8 | 9 | # 中断和异常的区别 10 | 11 | 中断和异常这两个词我想大家经常听到,但是这两者之间有什么区别呢?你能想到啥?那就听我慢慢道来吧~ 12 | 13 | [中断?异常?有什么区别][2] 14 | 15 | # 插播--系统调用的实现 16 | 17 | 学习过程中突然想到以前学习的时候说系统调用是通过**int 0x80**来实现的,那就干脆找找系统调用是怎么处理的呗。 18 | 19 | 这不看不打紧,一看发现现在的系统调用实现采用了新的方式。所以在这里插播一下~ 20 | 21 | [系统调用的实现][3] 22 | 23 | # 异常处理 24 | 25 | 书归正传,看过了系统调用,现在继续回来看异常处理。 26 | 27 | 在这里我们只看看异常向量表是如何初始化,并关联到异常处理函数的。 28 | 29 | [异常向量表的设置][4] 30 | 31 | # 中断函数 32 | 33 | 接着我们来看看中断。当然我们同样暂时不看很多具体的细节,只是把中断向量表和中断函数串起来。能够对系统架构有一个大致的理解。 34 | 35 | [中断向量和中断函数][5] 36 | 37 | # APIC 38 | 39 | 随着硬件的进步,中断控制器也从原始的8259A进化到了APCI。在这里我们也粗略的了解一下。 40 | 41 | [APIC][6] 42 | 43 | # 重要的中断处理 44 | 45 | 系统中有些中断处理比较特殊,我们也尝试了解一下。 46 | 47 | ## 时钟中断 48 | 49 | 时钟中断对整个系统有特殊的意义。比如调度和RCU,都和时钟中断有关联。 50 | 51 | [时钟中断][7] 52 | 53 | ## 软中断 54 | 55 | 本质来讲这部分已经不是中断了,而且和硬件没有耦合。之所以放在这里,是因为一来暂时没有别的地方放。二来在某些方面和中断还有关系,比如中断上下文。 56 | 57 | 嗯,那就先放在这里吧。如果那一天觉得不合适了,再移动。 58 | 59 | [软中断][8] 60 | 61 | # 中断、软中断、抢占和多处理器 62 | 63 | 这也是一个无处安放的章节,因为这个topic其实有点大。Paul老人家还专门写过一本这方面的书--《并行编程》。 64 | 65 | 暂时我只看了点皮毛,不过有些概念觉得实在是太重要了。必须找个地方记录一下,否则下次来看又忘记了。 66 | 67 | [中断、软中断、抢占和多处理器][9] 68 | 69 | [1]: /interrupt_exception/01-idt.md 70 | [2]: /interrupt_exception/02-difference.md 71 | [3]: /interrupt_exception/03-syscall.md 72 | [4]: /interrupt_exception/04-exception_vector_setup.md 73 | [5]: /interrupt_exception/05-interrupt_handler.md 74 | [6]: /interrupt_exception/06-apic.md 75 | [7]: /interrupt_exception/07-timer_interrupt.md 76 | [8]: /interrupt_exception/08-softirq.md 77 | [9]: /interrupt_exception/09-irq_softirq_preempt_and_smp.md 78 | -------------------------------------------------------------------------------- /interrupt_exception/01-idt.md: -------------------------------------------------------------------------------- 1 | 在x86平台上,专门有一张表来保存中断和异常处理对应的函数。这张表就叫做IDT(Interrupt Descriptor Table)。 2 | 3 | # IDT的长相 4 | 5 | 先来看手册上的一张图,这张图是在SDM Volume 3中的Figure 2-1. IA-32 System-Level Registers and Data Structures。 6 | 7 | ![IA-32 System-Level Registers and Data Structures](/interrupt_exception/system_level_registers.png) 8 | 9 | 在这张图中, IDT位于最左边靠近下方的部分。和GDT/LDT类似,IDT的位置由IDTR寄存器保存。所以在代码中加载IDT的代码如下: 10 | 11 | ``` 12 | static inline void native_load_idt(const struct desc_ptr *dtr) 13 | { 14 | asm volatile("lidt %0"::"m" (*dtr)); 15 | } 16 | ``` 17 | 18 | 而其中struct desc_ptr的一个实现则长成这个样子: 19 | 20 | ``` 21 | /* Must be page-aligned because the real IDT is used in a fixmap. */ 22 | gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss; 23 | 24 | struct desc_ptr idt_descr __ro_after_init = { 25 | .size = (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1, 26 | .address = (unsigned long) idt_table, 27 | }; 28 | ``` 29 | 30 | 嗯,这下算是把代码和手册在这个点上结合起来了。 31 | 32 | # IDT的基本工作过程 33 | 34 | 具体的过程是有点复杂的,那在这里对我而言只要知道一下这点暂时就足够用了。 35 | 36 | > CPU在执行下一条指令之前,将会检查此时是否有中断或者异常发生。如果有,则会判断中断或异常的vector number。在做完一系列检查、保护工作之后,会执行**IDT中对应vector number**的处理函数。 37 | 38 | 具体这个vector number是如何得到的,又做了哪些检查和保护,不在本次学习的重点中。关键是我们知道了硬件的IDT和软件代码之间的关系。 39 | 40 | # 内核中IDT的布局 41 | 42 | 说的这么眼花缭乱,那我们来看看内核中IDT是个什么样子吧。这个呢就在内核代码arch/x86/include/asm/irq_vectors.h中了。 43 | 44 | ``` 45 | /* 46 | * Linux IRQ vector layout. 47 | * 48 | * There are 256 IDT entries (per CPU - each entry is 8 bytes) which can 49 | * be defined by Linux. They are used as a jump table by the CPU when a 50 | * given vector is triggered - by a CPU-external, CPU-internal or 51 | * software-triggered event. 52 | * 53 | * Linux sets the kernel code address each entry jumps to early during 54 | * bootup, and never changes them. This is the general layout of the 55 | * IDT entries: 56 | * 57 | * Vectors 0 ... 31 : system traps and exceptions - hardcoded events 58 | * Vectors 32 ... 127 : device interrupts 59 | * Vector 128 : legacy int80 syscall interface 60 | * Vectors 129 ... INVALIDATE_TLB_VECTOR_START-1 except 204 : device interrupts 61 | * Vectors INVALIDATE_TLB_VECTOR_START ... 255 : special interrupts 62 | * 63 | * 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table. 64 | * 65 | * This file enumerates the exact layout of them: 66 | */ 67 | ``` 68 | 69 | # 心中的疑惑 70 | 71 | 到这里,虽然已经对IDT有了大致的了解,但是我想你一定也和我一样还有很多疑惑。 72 | 73 | * 中断和异常有区别么? 74 | * 中断和异常里究竟有什么? 75 | 76 | 不着急,让我们一点点揭开这神秘的面纱。 77 | -------------------------------------------------------------------------------- /interrupt_exception/02-difference.md: -------------------------------------------------------------------------------- 1 | 在学习的初期,我一直有这么个疑惑: 2 | 3 | > 不都是在IDT中么?不都是通过vector number索引么?中断和异常到底有啥区别呢? 4 | 5 | 这次我们就来尝试揭开这个谜团。 6 | 7 | # 雷锋和雷峰塔 8 | 9 | > 在想究竟用哪个标题来描述这两者之间的区别,突然想到之前看到过这个就用上了。 10 | 11 | 当然,实际上这两者的差别并没有这么大。我能想到的两点区别是: 12 | 13 | * 来源不同 14 | * 处理不同 15 | * 位置不同 16 | 17 | 来源不同是指中断是外界引入的。比如键盘,硬盘,网络的数据相应是中断,而异常则是软件执行时自己触发的,比如除0,溢出。 18 | 在处理上,异常通常是发送一个信号给到执行的进程,由进程自行决定该如何处理。而中断的处理则由中断函数在处理。 19 | 位置不同写的有点模糊,意思是在IDT中,异常和nmi中断的中断向量是固定的,而可屏蔽中断的中断向量是写死的。 20 | 21 | 来源的不同就不用我来讲了,这是个事实。我就简单说一下后两者。 22 | 23 | # 处理不同 24 | 25 | 总的来说,中断和异常的处理在内核中是分成两套机制完成的。 26 | 27 | * 异常的处理最后落到给进程发送信号上 28 | * 中断的处理最后落到do_IRQ函数上 29 | 30 | 具体的细节将在后续的小节中展开。 31 | 32 | # 对应的中断向量 33 | 34 | 除此之外,中断和异常在IDT中的位置也是有讲究的。 35 | 36 | * 异常和nmi的中断向量是固定的 37 | * 可屏蔽中断的中断向量则是可配的 38 | 39 | 在上一节中,我们看到了中断向量的整个布局。现在我们进一步仔细看看其中哪些是中断哪些是异常。 40 | 下面的描述在文件arch/x86/include/asm/irq_vectors.h中。 41 | 42 | ``` 43 | * Vectors 0 ... 31 : system traps and exceptions - hardcoded events 44 | * Vectors 32 ... 127 : device interrupts 45 | * Vector 128 : legacy int80 syscall interface 46 | * Vectors 129 ... INVALIDATE_TLB_VECTOR_START-1 except 204 : device interrupts 47 | * Vectors INVALIDATE_TLB_VECTOR_START ... 255 : special interrupts 48 | ``` 49 | 50 | 可以看到其中大部分是中断,而在0-31中保存的是异常的中断向量。那进一步再来看异常向量的布局。 51 | 52 | ``` 53 | /* Interrupts/Exceptions */ 54 | enum { 55 | X86_TRAP_DE = 0, /* 0, Divide-by-zero */ 56 | X86_TRAP_DB, /* 1, Debug */ 57 | X86_TRAP_NMI, /* 2, Non-maskable Interrupt */ 58 | X86_TRAP_BP, /* 3, Breakpoint */ 59 | X86_TRAP_OF, /* 4, Overflow */ 60 | X86_TRAP_BR, /* 5, Bound Range Exceeded */ 61 | X86_TRAP_UD, /* 6, Invalid Opcode */ 62 | X86_TRAP_NM, /* 7, Device Not Available */ 63 | X86_TRAP_DF, /* 8, Double Fault */ 64 | X86_TRAP_OLD_MF, /* 9, Coprocessor Segment Overrun */ 65 | X86_TRAP_TS, /* 10, Invalid TSS */ 66 | X86_TRAP_NP, /* 11, Segment Not Present */ 67 | X86_TRAP_SS, /* 12, Stack Segment Fault */ 68 | X86_TRAP_GP, /* 13, General Protection Fault */ 69 | X86_TRAP_PF, /* 14, Page Fault */ 70 | X86_TRAP_SPURIOUS, /* 15, Spurious Interrupt */ 71 | X86_TRAP_MF, /* 16, x87 Floating-Point Exception */ 72 | X86_TRAP_AC, /* 17, Alignment Check */ 73 | X86_TRAP_MC, /* 18, Machine Check */ 74 | X86_TRAP_XF, /* 19, SIMD Floating-Point Exception */ 75 | X86_TRAP_IRET = 32, /* 32, IRET Exception */ 76 | }; 77 | ``` 78 | 79 | 这个表大家也可以对应SDM Volume 3中Table 6-1 Protected-Mode Exceptions and Interrupts一起看。这样也找到了手册和代码之间的一个对应关系。 80 | 81 | 好了,了解了这两者的区别后,我们就可以来看看内核中是如何处理他们的了。 82 | -------------------------------------------------------------------------------- /interrupt_exception/04-exception_vector_setup.md: -------------------------------------------------------------------------------- 1 | 知道了IDT中前32个中断向量用来处理异常后,我就很想知道这些异常向量对应的IDT项是如何初始化的,如何对应到异常处理函数的。 2 | 3 | 这一小节我们就来解开这部分的谜团。 4 | 5 | # 全局观 6 | 7 | 先来看看内核启动时,是在哪里初始化的异常向量表。 8 | 9 | ``` 10 | start_kernel() 11 | trap_init() 12 | idt_setup_traps() 13 | idt_setup_from_table(idt_table, def_idts) 14 | ``` 15 | 16 | 上面的流程中,基本看出了异常向量表初始化的位置。进一步从代码中可以看出,实际的工作就是把def_idts中的内容写到idt_table对应的异常向量中。 17 | 18 | # 从def_idts开始 19 | 20 | 既然是将def_idts写到idt_table,那就来看看这个表的内容。 21 | 22 | ``` 23 | /* Interrupt gate */ 24 | #define INTG(_vector, _addr) \ 25 | G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS) 26 | 27 | static const __initconst struct idt_data def_idts[] = { 28 | INTG(X86_TRAP_DE, divide_error), 29 | ... 30 | }; 31 | ``` 32 | 33 | 可以看到,这张表中的一项对应了一个异常处理。其中_addr就是异常处理函数了。 34 | 35 | # idtentry和异常处理函数 36 | 37 | 接着我们就要找到这个divide_error异常处理函数的定义了。开始我怎么也找不到,后来才发现这个divide_error的异常处理函数是在汇编代码中用idtentry来实现的。 38 | 39 | ``` 40 | idtentry divide_error do_divide_error has_error_code=0 41 | 42 | .macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1 43 | ENTRY(\sym) 44 | UNWIND_HINT_IRET_REGS offset=\has_error_code*8 45 | 46 | ... 47 | 48 | call \do_sym 49 | 50 | ... 51 | 52 | END(\sym) 53 | .endm 54 | ``` 55 | 56 | 省略众多细节,突出大致结构。idtentry为每一个异常处理做了基本统一的处理,然后对应不同的异常再调用do_sym函数处理。对应divide_error,这个函数就是do_divide_error。 57 | 58 | # DO_ERROR和信号 59 | 60 | 内核为了代码简洁和统一,也用了一个宏DO_ERROR来定义统一的异常处理方式。 61 | 62 | ``` 63 | #define DO_ERROR(trapnr, signr, sicode, addr, str, name) \ 64 | dotraplinkage void do_##name(struct pt_regs *regs, long error_code) \ 65 | { \ 66 | do_error_trap(regs, error_code, str, trapnr, signr, sicode, addr); \ 67 | } 68 | 69 | DO_ERROR(X86_TRAP_DE, SIGFPE, FPE_INTDIV, IP, "divide error", divide_error) 70 | ``` 71 | 72 | 从上面的代码片段可以看出,大家殊途同归,异常处理最后都走到了do_error_trap()函数,而这个函数最后又调用了do_trap()。 73 | 74 | ``` 75 | static void 76 | do_trap(int trapnr, int signr, char *str, struct pt_regs *regs, 77 | long error_code, int sicode, void __user *addr) 78 | { 79 | struct task_struct *tsk = current; 80 | 81 | 82 | if (!do_trap_no_signal(tsk, trapnr, str, regs, error_code)) 83 | return; 84 | 85 | show_signal(tsk, signr, "trap ", str, regs, error_code); 86 | 87 | if (!sicode) 88 | force_sig(signr, tsk); 89 | else 90 | force_sig_fault(signr, sicode, addr, tsk); 91 | } 92 | ``` 93 | 94 | > **所以异常处理的最后就是通过内核向该进程发送一个信号,由进程捕获该信号来处理。** 95 | 96 | 从代码可以看到这个信号的值是signr,一路追踪divide_error对应的信号在DO_ERROR宏中定义为SIGFPE。到这里我们基本理清了异常向量初始化的内容,以及异常处理函数采用信号和进程通信。至于内核如何产生和发送信号,进程如何处理信号,那又是一个值得探索的话题了。 97 | -------------------------------------------------------------------------------- /interrupt_exception/06-apic.md: -------------------------------------------------------------------------------- 1 | 虽然是一名软件工程师,不过有时候还是要对硬件有一些了解。 2 | 3 | # 硬件规范 4 | 5 | 要了解硬件,第一件事当然是查看手册。还好,APIC的信息在SDM vol3中有比较详细的介绍。这里我做一个个人理解的总结。 6 | 7 | ![APIC Structure](/interrupt_exception/apic.png) 8 | 9 | 第一眼看确实有点懵逼,多看几眼就好了。 10 | 11 | 为了方便理解,我们把这些寄存器按照功能分类: 12 | 13 | ``` 14 | Timer related: 15 | 16 | CCR: Current Count Register 17 | ICR: Initial Count Register 18 | DCR: Divide Configuration Register 19 | Timer: in LVT 20 | 21 | LVT (Local Vector Table): 22 | 23 | Timer 24 | Local Interrupt 25 | Performance Monitor Counters 26 | Thermal Sensor 27 | Error 28 | 29 | IPI: 30 | 31 | ICR: Interrupt Command Register 32 | LDR: Logical Destination Register 33 | DFR: Destination Format Register 34 | 35 | Interrupt State: 36 | 37 | ISR: In-Service Register 38 | IRR: Interrupt Request Register 39 | TMR: Trigger Mode Register 40 | ``` 41 | 42 | 这样或许有助于理解和记忆。 43 | 44 | # 初始化 45 | 46 | 初始化还真有点麻烦,说实话我没有验证过,只是扫了一眼代码。 47 | 48 | 大致上分成两部分: 49 | 50 | * 找到驱动 51 | * 初始化 52 | 53 | 因为APIC出现了一段时间了,也有了不同的版本。所以系统在做真正的初始化之前将要先探测出究竟是哪种中断控制器。 54 | 55 | 系统中定义apic驱动的方式还和常见的驱动不一样。系统使用apic_driver/apic_drivers来定义驱动。而且其实就是一个数据结构。在这里借用驱动这个词,只是为了方便描述。 56 | 57 | 这个过程分散在多个地方,如apic_boot_init, apic_intr_mode_init。具体之间的关系和顺序本人没有深究。 58 | 59 | 选好了驱动,就可以做真正的初始化了。初始化的函数还是很清晰的。 60 | 61 | > setup_local_APIC 62 | 63 | 当你打开这个函数之后,你就会发现主要的工作就是配置APIC中的各个寄存器。比如设置LVT0/1。 64 | 65 | 好了,其实这步还挺简单的。 66 | 67 | # 对应的中断向量 68 | 69 | 相对于初始化,中断向量就要好找很多。 70 | 71 | ``` 72 | start_kernel() 73 | init_IRQ() 74 | native_init_IRQ() 75 | idt_setup_apic_and_irq_gates() 76 | idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true); 77 | ``` 78 | 79 | 我相信,有了之前的经验,这行代码就是小菜了。 80 | 81 | 等等,这样就结束了么?是不是还差了点什么? 82 | 83 | > IDT中的某一项是如何同apic中的寄存器关联的呢? 84 | 85 | 我们就以Thermal Sensor这个中断为例来看看之间的关联。 86 | 87 | 先看apic_idts中的定义,找到这个中断处理的定义处: 88 | 89 | ``` 90 | INTG(THERMAL_APIC_VECTOR, thermal_interrupt), 91 | ``` 92 | 93 | 由此可知,这个中断占用的向量号是**THERMAL_APIC_VECTOR**。 94 | 95 | 既然如此,按照规范就需要在Thermal Sensor Register中注册这个向量号,这样在中断发生时才能正确找到并触发中断。 96 | 97 | ``` 98 | /* We'll mask the thermal vector in the lapic till we're ready: */ 99 | h = THERMAL_APIC_VECTOR | APIC_DM_FIXED | APIC_LVT_MASKED; 100 | apic_write(APIC_LVTTHMR, h); 101 | ``` 102 | 103 | 在代码中我们找到这么两行,其中就有我们要找的THERMAL_APIC_VECTOR。而接下来的动作就是写到APIC_LVTTHMR指定的寄存器中。 104 | 105 | 那这个寄存器地址是什么呢? 106 | 107 | ``` 108 | #define APIC_LVTTHMR 0x330 109 | ``` 110 | 111 | 到这里,我想你也已经清楚了~ 112 | 113 | # x2APIC 114 | 115 | x2APIC是xAPIC对应的扩展,理论上这两者之间应该有不少区别。在这里先记录一点:两者的访问方式不同。 116 | 117 | * xAPIC通过MMIO访问APCI的寄存器 118 | * x2APIC则是通过MSR来访问的 119 | -------------------------------------------------------------------------------- /interrupt_exception/07-timer_interrupt.md: -------------------------------------------------------------------------------- 1 | 时钟中断牵扯到系统的其他子系统,如调度,RCU。本着最小可用原则,我们只探究一下时钟中断的初始化过程以及最后都做了哪些操作。 2 | 3 | # 相似的初始化 4 | 5 | 时钟中断的IDT初始化也普通中断初始化的隔壁。 6 | 7 | ``` 8 | start_kernel() 9 | init_IRQ() 10 | native_init_IRQ() 11 | idt_setup_apic_and_irq_gates() 12 | idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true); 13 | INTG(LOCAL_TIMER_VECTOR, apic_timer_interrupt) 14 | ``` 15 | 16 | 所以当个时钟中断来了后,就会调用到apic_timer_interrupt(smp_apic_timer_interrupt)。 17 | 18 | 貌似也不难嘛。 19 | 20 | # 未知的event_handler 21 | 22 | 我们打开这个中断处理函数 smp_apic_timer_interrupt 23 | 24 | ``` 25 | smp_apic_timer_interrupt(regs) 26 | local_apic_timer_interrupt() 27 | evt = this_cpu_ptr(&lapic_events); 28 | evt->event_handler(evt) 29 | ``` 30 | 31 | 此处出现了一个新成员, lapic_events。并且调用了它的回调函数event_handler。这个东西是个啥? 32 | 33 | # 掘地三尺 34 | 35 | 这个event_handler的设置还真实隐藏得很深啊。废了九牛二虎之力才找到了它。 36 | 37 | ``` 38 | setup_boot_APIC_clock() 39 | calibrate_APIC_clock() 40 | setup_APIC_timer() 41 | evt = this_cpu_ptr(&lapic_events); 42 | memcpy(levt, &lapic_clockevent, sizeof(*levt)); 43 | clockevents_register_device(evt) 44 | tick_check_new_device(dev); 45 | tick_setup_device(td, newdev, cpu, cpumask_of(cpu)); 46 | tick_setup_periodic(newdev, 0); 47 | tick_set_periodic_handler(dev, broadcast); 48 | dev->event_handler = tick_handle_periodic; 49 | ``` 50 | 51 | 反正我是眼花了,还好终于找到了设置的地方。也找到了真正在时钟中断时调用的函数**tick_handle_periodic**。 52 | 53 | # 主角登场 54 | 55 | ``` 56 | tick_periodic(cpu) 57 | update_process_times(user_mode(get_irq_regs())) 58 | account_process_tick(p, user_tick) 59 | run_local_timers() 60 | raise_softirq(TIMER_SOFTIRQ), if required 61 | rcu_sched_clock_irq(user_tick) 62 | scheduler_tick() 63 | profile_tick() 64 | ``` 65 | 66 | 终于看到了时钟中断的庐山真面目。 67 | 68 | * 更新进程的时间 69 | * profile 70 | 71 | 其中除了会调用一个timer的软中断,别的都是为了调度和rcu服务的了。 72 | 73 | 好了,本次的使命到此结束。 74 | -------------------------------------------------------------------------------- /interrupt_exception/08-softirq.md: -------------------------------------------------------------------------------- 1 | 软中断,softirq,经常听说,但是究竟是什么,怎么用其实我并不清楚。这不,最近看到有代码使用了软中断,为了能够进一步了解只好硬着头皮来看看源代码。 2 | 3 | > 软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。 4 | 5 | 那究竟是怎么做的呢?让我们一探究竟。 6 | 7 | # 初始化 8 | 9 | 一切的一切总有个起头的,既然是抓瞎的状态,那就先抓住开始的部分。 10 | 11 | ``` 12 | start_kernel() 13 | softirq_init(void) 14 | open_softirq(TASKLET_SOFTIRQ, tasklet_action); 15 | open_softirq(HI_SOFTIRQ, tasklet_hi_action); 16 | softirq_vec[nr].action = action; 17 | ``` 18 | 19 | 瞧,这其实啥都没干,就填写了一个数组的成员。那这个数组长什么样子呢? 20 | 21 | ``` 22 | softirq_vec[NR_SOFTIRQS] 23 | +------------------------------------+ 24 | |action | 25 | | void (*)(struct softirq_action *) | 26 | +------------------------------------+ 27 | |action | 28 | | void (*)(struct softirq_action *) | 29 | +------------------------------------+ 30 | |action | 31 | | void (*)(struct softirq_action *) | 32 | +------------------------------------+ 33 | ``` 34 | 35 | 好吧,就这么一个光杆司令,每个数组就是一个毁掉函数的成员。真的是不知所云。 36 | 37 | # 硬塞的__preemt_count 38 | 39 | 按照正常逻辑,现在我应该来讲什么时候softirq被调用的。但是呢,我发先有一个非常重要的概念(变量)对理解何时调用很关键,所以就硬插进来,模拟一个“软中断”。 40 | 41 | 这个变量叫__preemt_count。名字很简单,但是定义却藏着玄机。 42 | 43 | ``` 44 | DEFINE_PER_CPU(int, __preempt_count) = INIT_PREEMPT_COUNT; 45 | ``` 46 | 47 | 乍一看就是一个32位的整型,但是你再往里看其实这个整型被切几个小块,没块有自己的含义。 48 | 49 | ``` 50 | PREEMPT_MASK: 0x000000ff 51 | SOFTIRQ_MASK: 0x0000ff00 52 | HARDIRQ_MASK: 0x000f0000 53 | NMI_MASK: 0x00100000 54 | PREEMPT_NEED_RESCHED: 0x80000000 55 | ``` 56 | 57 | 然后我用一张简易图来展示一下上面的定义: 58 | 59 | ``` 60 | |< 8bits >|< 8bits >|< 8bits >|< 8bits >| 61 | +-+--------------+-----+-+--------+--------------+-+----------------+ 62 | | | | | |hard irq| softirq cnt | | preempt cnt | 63 | +-+--------------+-----+-+--------+--------------+-+----------------+ 64 | ^ ^ ^ 65 | | | | 66 | | | | 67 | | | | 68 | | | +--- Bit8: in_serving_softirq() 69 | | +----------------------------- Bit20: NMI_MASK 70 | +---------------------------------------------------- Bit31: PREEMPT_NEED_RESCHED 71 | ``` 72 | 73 | 与此同时,就要引出几个和当前cpu运行状态相关的函数了。 74 | 75 | * in_irq() - We're in (hard) IRQ context 76 | * in_softirq() - We have BH disabled, or are processing softirqs 77 | * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled 78 | * in_serving_softirq() - We're in softirq context 79 | * in_nmi() - We're in NMI context 80 | * in_task() - We're in task context 81 | 82 | 原来我们通常判断cpu状态的函数,就是根据cpu上变量__preempt_count来确定的。顿时有种找到根的感觉。 83 | 84 | 那为什么会插播这么一个变量呢?是因为在softirq中,以及其他很多地方都会判断这个值来确认当前cpu运行状态,以此来判断是否应该执行什么操作。 85 | 86 | 好了,这个“软中断”结束了,让我们回到上文。 87 | 88 | # 何时调用软中断? 89 | 90 | 对软中断的调用,还得分成两步: 91 | 92 | * 标记有软中断的请求 93 | * 在适当的时机执行 94 | 95 | 毕竟软中断不像硬中断,可以来了就执行。软中断没有硬件的这种权利打断别人的运行,只好先标记好自己的到来,等待时机的出现。 96 | 97 | ## 标记软中断请求 98 | 99 | 标记请求由函数raise_softirq(nr)来完成。 100 | 101 | ``` 102 | raise_softirq(nr), explicit raise 103 | local_irq_save(flags); 104 | raise_softirq_irqoff(nr); 105 | __raise_softirq_irqoff(nr); 106 | or_softirq_pending(1UL << nr) 107 | __this_cpu_or(local_softirq_pending_ref, (x)) 108 | wakeup_softirqd(), if !in_interrupt() 109 | tsk = __this_cpu_read(ksoftirqd); 110 | wake_up_process(tsk); wakeup ksoftirqd 111 | local_irq_restore(flags); 112 | ``` 113 | 114 | 实际上做了什么呢?打开看一看。 115 | 116 | * 关中断 117 | * 在变量local_softirq_pending_ref上,标记nr 118 | * 如果不在in_interrupt(),唤醒softirq线程 119 | 120 | 在这里我们着重要讲的是第二部,标记变量local_softirq_pending_ref。 121 | 122 | ## 在适当的时机执行软中断 123 | 124 | 之前我们也提到,软中断不像硬中断能够要求硬件及时响应。所以只好等到某个时间,再由cpu来处理。那么都有哪些时机,cpu回来处理软中断呢? 125 | 126 | * irq_exit(): 中断处理函数返回时 127 | * local_bh_enable(): 允许软中断时 128 | * raise_softirq(): 标记软中断时(通过唤醒线程) 129 | 130 | 在没有线程化irq时,前两者是立即执行的,只有第三者是通过wakeup来唤醒软中断线程。 131 | 132 | 这里我们结合一下上一小节硬插进来的概念,看看raise_softirq()中的处理。 133 | 134 | ``` 135 | if(!in_interrupt()) 136 | wakeup_softirq() 137 | ``` 138 | 139 | 而这个in_interrupt()的含义是: We're in NMI,IRQ,SoftIRQ context or have BH disabled。这就是当我们cpu运行在这几个状态下时,我们就不去唤醒软中断了。 140 | 为什么呢?因为当我们从中断/软中断返回时,我们会去处理新的这个软中断的。 141 | 142 | 这就是上一小节引入变量的作用。 143 | 144 | # 响应软中断 145 | 146 | 终于是时候来看软中断的响应流程了。在上面三个响应软中的地方看下来,最终都会走到函数__do_softirq()。 147 | 148 | 这个函数就是根据变量local_softirq_pending_ref上标记的软中断号,来依次处理事先注册好的软中断函数。 149 | 150 | 当然里面有几个点值得关注: 151 | 152 | * 函数__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET)和__local_bh_enable(SOFTIRQ_OFFSET) 来表示in_serving_softirq()。 153 | * 保存好现场后才开中断 154 | 155 | 好了,大致框架梳理完了。有机会再来细扣其中的细节。 156 | -------------------------------------------------------------------------------- /interrupt_exception/09-irq_softirq_preempt_and_smp.md: -------------------------------------------------------------------------------- 1 | 内核代码中大家经常会看到这么些函数: 2 | 3 | * spin_lock_irq() 4 | * disable_ 5 | 6 | 说起来大家好像都知道,这些函数的作用就是关中断,关抢占,拿锁。那请问,为什么要真么做?如果不这么做会发生什么呢? 7 | 8 | 可能你会觉得这个问题很low,以前我也觉得这不是明摆着的么?但是仔细一想这背后的原理,感觉自己还真说不清楚。 9 | 10 | 今天我终于有点get到了。看了别人的很多文章,想用自己的语言组织一下。 11 | 12 | # 产生“并行”的两大原因 13 | 14 | 总的来说上述的这些函数的目标都是为了使程序在当前计算机系统上能够**并行**执行。那么产生这些并行执行的原因都有哪些? 15 | 16 | 我总结下来有两点: 17 | 18 | * 广义抢占 19 | * 多处理器 20 | 21 | 广义抢占不仅仅是内核中说的抢占,而是指任何能够打断当前处理器执行顺序的。现有的中断、软中断和抢占都是一种抢占的机制。 22 | 23 | 在这些机制下,处理器的执行顺序会被打断。比如当中断到来,当前处理器就要收拾好现场,去处理中断。而进程调度也是一种“抢占”,因为进程运行的环境会被切换,当下次再执行时别人已经执行过了。只不过这个行为主要影响的是进程内部。如果是一个单线程进程,那么即使被打断,我们也知道没有别人会动它的环境。而对多线程进程来说,我们就要用锁等机制来保护共享资源了。 24 | 25 | 这样也就引出了第二个抢占的来源--多处理器。和多线程原理一样,如果系统中只有一个处理器,那么即使CPU运行环境被打断,我们也能保证共享资源不会有别人改动,那就不需要采用什么保护机制。但是如果有多处理器,那就可能有多处理器共同访问共享资源的情况发生。此时我们就需要用锁等机制来保护共享资源。 26 | 27 | 好了,叨叨了半天,也不知道有没有把这部分梳理清楚。 28 | 29 | 所以总的来说这么多函数的目的其实只有一个: 30 | 31 | > 在这么一个纷繁复杂,弱肉强食的计算机系统中,如何采用正确的互斥原语来保护一个共享变量不会被同时修改。 32 | 33 | 不采取任何措施,程序一定会崩。如果都放大招,那程序的性能就一定会有影响。 34 | 35 | # 各种抢占之间的不同之处 36 | 37 | 刚才我们看到了广义抢占的种类,那各种抢占之间有没有什么差别呢? 有的,暂时想到的是有两个方面的差异: 38 | 39 | * 优先级 40 | * 是否能睡眠 41 | 42 | 先来看看优先级,优先级从高到底,意味着高优先级可以抢占低优先级。 43 | 44 | > 中断 -> 软中断 -> 抢占 45 | 46 | 也就是软中断在执行时可以被中断打断。 47 | 48 | 当然我觉得这个优先级有些是硬件定义的,比如中断优先级最高。而有些则是软件定义的。比如中断过程中不能执行软中断,这是在代码中有保护的。 49 | 50 | 而第二点不同,是否能够睡眠则影响到了在保护共享资源时,可以采用什么锁。 51 | 52 | 所以总结来看,这两点的差异本别给出编程上的指导是: 53 | 54 | * 优先级: 决定如何正确关中断,关软中断,关抢占 55 | * 是否能睡眠: 决定采用什么锁 56 | 57 | 了解了各种抢占及他们之间的区别后,我们来看看针对各自的情况应该如何做到保护。 58 | 59 | # 多处理器之间的保护 60 | 61 | 多处理器之间的保护可以参考多线程之间的保护,主要就是采用各种锁来保护共享资源。 62 | 63 | 但是需要注意一点,在不同抢占形式中可以使用的锁是有限制的。比如在中断、软中断中不能用会休眠的锁。 64 | 65 | # 抢占之间的保护 66 | 67 | 各抢占之间的保护主要是避免自己被更高优先级的抢占而抢占。 68 | 69 | 比如软中断的优先级没有中断高,那么在需要处理某些共享资源时,需要关中断不让中断来打断自己的工作,否则将会产生死锁。 70 | -------------------------------------------------------------------------------- /interrupt_exception/apic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/interrupt_exception/apic.png -------------------------------------------------------------------------------- /interrupt_exception/system_level_registers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/interrupt_exception/system_level_registers.png -------------------------------------------------------------------------------- /kernel_pagetable/00-evolution_of_kernel_pagetable.md: -------------------------------------------------------------------------------- 1 | 内存管理重要的组成部分是虚拟地址和物理地址之间的映射关系。从物理设备上看这部分的功能由页表(page table)实现。 2 | 3 | 页表本身是存储在物理内存中的一块内存空间,操作系统按照硬件规范填写相应地址形成树状结构。而硬件则根据这个树状结构实现虚拟地址到物理地址的映射。 4 | 5 | Intel的手册上写的非常详细,我就截取一张图做示例。 6 | 7 | ![这里写图片描述](/kernel_pagetable/intel_virtual_phys_add.png) 8 | 9 | 每个进程都有自己的页表,虽然用户空间的映射依赖进程自身,但所有进程都共享同样的内核页表空间。所以探究内核页表结构是加深内核页表运行机制的方式之一。 10 | 11 | 然而你知道么,内核页表并非一蹴而就,而是经过了几个步骤才最终成为我们想要的样子。就好像你得先飞升上仙,才能够飞升上神。这是一个道理。 12 | 13 | # 不断成长 14 | 15 | 人是不断成长的,内核页表也一样。下面我们来看看它成长过程中经历的几个过程。 16 | 17 | 内核刚加载时,还是压缩后的状态。这时候内核就有一张非常简陋的页表。直接映射了4G空间。 18 | 19 | [未解压时的内核页表][1] 20 | 21 | ![这里写图片描述](/kernel_pagetable/pagetable_before_decompression.png) 22 | 23 | 内核解压缩完之后,就换掉了那张简陋的页表。这张页表在编译时就已经有了大概的雏形。不过只映射了内核空间。这时候虚拟机地址已经和物理地址不一样了。 24 | 25 | [内核早期的页表][2] 26 | 27 | ![这里写图片描述](/kernel_pagetable/pagetable_compiled.png) 28 | 29 | 而出于某些原因,内核只留下了_text到_brk_end这段空间的映射。其余的映射都清零了。 30 | 31 | [cleanup_highmap之后的页表][3] 32 | 33 | ![这里写图片描述](/kernel_pagetable/pagetable_after_cleanup_highmap.png) 34 | 35 | 只映射了内核的空间肯定是不够的,否则内核怎么访问其他的系统内存呢?所以接着内核就映射了系统上的所有物理地址。 36 | 37 | [映射完整物理地址][4] 38 | 39 | ![这里写图片描述](/kernel_pagetable/map_whole_memory.png) 40 | 41 | 所有的准备工作做完,最后又切换了一次。从early_level4_pgt切换到了init_level4_pgt。对了,这个就是那个init进程的空间了。 42 | 43 | [启用init_level4_pgt][5] 44 | 45 | 这个图和上面的是一样的,没有变化。关键变化在cr3的内容,而不是页表本身。 46 | 47 | # 页表成长大汇总 48 | 49 | ``` 50 | /* use pgtable 51 | * arch/x86/boot/compressed/head_64.S 52 | */ 53 | leal pgtable(%ebx), %eax 54 | movl %eax, %cr3 55 | 56 | /* use early_level4_pgt 57 | * arch/x86/kernel/head_64.S 58 | */ 59 | movq $(early_level4_pgt - __START_KERNEL_map), %rax 60 | addq phys_base(%rip), %rax 61 | movq %rax, %cr3 62 | 63 | /* set init_level4_pgt kernel high mapping */ 64 | x86_64_start_kernel() 65 | init_level4_pgt[511] = early_level4_pgt[511]; 66 | 67 | start_kernel() 68 | setup_arch() 69 | /* cleanup highmap */ 70 | cleanup_highmap() 71 | init_mem_mapping() 72 | /* map whole memory space */ 73 | memory_map_top_down() 74 | /* switch to init_level4_pgt */ 75 | load_cr3(swapper_pg_dir); 76 | ``` 77 | 78 | 基本这是我已知的页表变化的内容,整理成这样的调用顺序,或许会帮助你更好的理解。 79 | 80 | 整个内核页表中还有其他映射的部分,比如page的映射和pcpu变量的映射,但因为很难用图描述,就没有放在本系列当中。当然肯定还有我并没有研究透彻的部分,不过我相信现有的内容也足够大家对内核页表有个较为感性的认识了。 81 | 82 | 好了,先到这里,休息休息~ 83 | 84 | [1]: /kernel_pagetable/01-pagetable_before_decompressed.md 85 | [2]: /kernel_pagetable/02-pagetable_compiled_in.md 86 | [3]: /kernel_pagetable/03-pagetable_after_cleanup_highmap.md 87 | [4]: /kernel_pagetable/04-map_whole_memory.md 88 | [5]: /kernel_pagetable/05-switch_to_init_level4_pgt.md 89 | -------------------------------------------------------------------------------- /kernel_pagetable/01-pagetable_before_decompressed.md: -------------------------------------------------------------------------------- 1 | 这或许是x86平台启动过程中第一张页表了。 2 | 3 | 之前我们也学习了内核启动镜像bzImage由两部分组成setup.bin和vmlinux.bin。而这张第一章页表就在vmlinux.bin的head.S中。 4 | 5 | 如果对上述两个文件编译过程不熟悉的话可以参考下面的链接: 6 | 7 | * [启动镜像bzImage的前世今生][1] 8 | * [真假vmlinux--vmlinux.bin揭开的秘密][2] 9 | 10 | # 先来看个代码 11 | 12 | 这第一张页表初始化的代码就在arch/x86/boot/compressed/head_64.S中。 13 | 14 | ``` 15 | /* 16 | * Build early 4G boot pagetable 17 | */ 18 | /* Initialize Page tables to 0 */ 19 | leal pgtable(%ebx), %edi 20 | xorl %eax, %eax 21 | movl $(BOOT_INIT_PGT_SIZE/4), %ecx 22 | rep stosl 23 | 24 | /* Build Level 4 */ 25 | leal pgtable + 0(%ebx), %edi 26 | leal 0x1007 (%edi), %eax 27 | movl %eax, 0(%edi) 28 | 29 | /* Build Level 3 */ 30 | leal pgtable + 0x1000(%ebx), %edi 31 | leal 0x1007(%edi), %eax 32 | movl $4, %ecx 33 | 1: movl %eax, 0x00(%edi) 34 | addl $0x00001000, %eax 35 | addl $8, %edi 36 | decl %ecx 37 | jnz 1b 38 | 39 | /* Build Level 2 */ 40 | leal pgtable + 0x2000(%ebx), %edi 41 | movl $0x00000183, %eax 42 | movl $2048, %ecx 43 | 1: movl %eax, 0(%edi) 44 | addl $0x00200000, %eax 45 | addl $8, %edi 46 | decl %ecx 47 | jnz 1b 48 | 49 | /* Enable the boot page tables */ 50 | leal pgtable(%ebx), %eax 51 | movl %eax, %cr3 52 | ``` 53 | 54 | 看到最后把地址保存到了cr3了么?Yep, you get it. 55 | 56 | 干这么看确实有点枯燥,不过简单来说就是分别填写了三层结构,构造出了一张覆盖4G大小的页表。 57 | 58 | PS: 此时页表并没有启用,而是要等到后面CR0中的PG被置位后。 59 | 60 | # 再来看一张图 61 | 62 | 看着图再去对照代码,我相信你就可以看懂了。 63 | 64 | ![这里写图片描述](/kernel_pagetable/pagetable_before_decompression.png) 65 | 66 | 这样是不是清晰了很多。 67 | 68 | 代码我就不多说了,多看几遍自然就懂了。正所谓 69 | 70 | > 代码虐我千百遍,我待代码如初恋 71 | 72 | # 重要提醒 73 | 74 | 大家看这张表,有没有意识到什么特别的地方?对了,虚拟地址和物理地址是一样的。 75 | 76 | 那内核中的虚拟地址又是如何访问的呢?别着急,这一切才刚刚开始~ 77 | 78 | [1]: /brief_tutorial_on_kbuild/07_rules_for_bzImage.md 79 | [2]: /brief_tutorial_on_kbuild/09_rule_for_vmlinux_bin.md 80 | -------------------------------------------------------------------------------- /kernel_pagetable/02-pagetable_compiled_in.md: -------------------------------------------------------------------------------- 1 | 刚才的那张页表实在是太粗糙了,到了真正的内核,怎么也得换个漂亮点的页表。 2 | 3 | 这次的页表就在arch/x86/kernel/head_64.S里面咯~ 4 | 5 | # 从cr3开始 6 | 7 | 在x86平台上保存页表起始地址的就是这个cr3寄存器了,那就先看看这个寄存器变成了谁呗。 8 | 9 | ``` 10 | movq $(early_level4_pgt - __START_KERNEL_map), %rax 11 | 12 | /* Setup early boot stage 4 level pagetables. */ 13 | addq phys_base(%rip), %rax 14 | movq %rax, %cr3 15 | ``` 16 | 17 | 嗯,这个东西稍微有点绕。phys_base是一个偏移,我们暂且认为就是0吧。所以这次加载到cr3中的地址是early_level4_pgt - __START_KERNEL_map。这里稍微解释一下下,如果实在理解不了,暂时先跳过。 18 | 19 | 加载到cr3的是一个物理地址,而我们编译出的内核vmlinux是一个ELF文件且最终会在虚拟地址空间运行,所以所有的符号都保存的是虚拟地址。上面这两个动作就是得到early_level4_pgt这个符号在运行时的物理地址。 20 | 21 | 不管怎么样知道加载到cr3的地址是指向early_level4_pgt的就好~ 22 | 23 | # early_level4_pgt的容貌 24 | 25 | 刚才最简单的页表也有三层了,内核中的页表也不例外。 26 | 27 | 28 | **early_level4_pgt:** 29 | 30 | ``` 31 | NEXT_PAGE(early_level4_pgt) 32 | .fill 511,8,0 33 | .quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE 34 | ``` 35 | 36 | **level3_kernel_pgt:** 37 | 38 | ``` 39 | NEXT_PAGE(level3_kernel_pgt) 40 | .fill L3_START_KERNEL,8,0 41 | /* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */ 42 | .quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE 43 | .quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE 44 | ``` 45 | 46 | **level2_kernel_pgt:** 47 | 48 | ``` 49 | NEXT_PAGE(level2_kernel_pgt) 50 | /* 51 | * 512 MB kernel mapping. We spend a full page on this pagetable 52 | * anyway. 53 | * 54 | * The kernel code+data+bss must not be bigger than that. 55 | * 56 | * (NOTE: at +512MB starts the module area, see MODULES_VADDR. 57 | * If you want to increase this then increase MODULES_VADDR 58 | * too.) 59 | */ 60 | PMDS(0, __PAGE_KERNEL_LARGE_EXEC, 61 | KERNEL_IMAGE_SIZE/PMD_SIZE) 62 | ``` 63 | 64 | # 看图说话 65 | 66 | 是不是又看得头晕了? 嗯,不着急,再来看一张图~ 67 | 68 | ![这里写图片描述](/kernel_pagetable/pagetable_compiled.png) 69 | 70 | 所以这其实是一张编译时就写好的页表。这样看,是不是简单了些? 71 | 72 | # 映射关系 73 | 74 | 细心的朋友可能发现了,这张页表和之前的页表最后一个层级差不多,就是少了点。而最大的差别是第一层上我们使用了最后一个表项,而前一个页表中的第一层使用的是最后一个表项。 75 | 76 | 对了,我想你猜到了。这就是虚拟地址和物理地址的映射了。 77 | 78 | 那我们来算一下这次映射关系究竟是什么样子的。 79 | 80 | ``` 81 | (511) << 39 | (510) << 30 82 | = FFFF 8000 0000 83 | ``` 84 | 85 | 再来看一下变量__START_KERNEL_map的定义 86 | 87 | ``` 88 | #define __START_KERNEL_map _AC(0xffffffff80000000, UL) 89 | ``` 90 | 91 | x86上只使用了48bit虚拟地址空间,而不是64bit。所以这两个地址就是等价的。经过计算证实了这次映射的就是内核代码空间的页表。You get it? 92 | 93 | # 启用虚拟地址 94 | 95 | 正如之前看到的,虽然使用了页表,但是页表中的物理地址和虚拟地址是一模一样的。经过了这次页表的改造,就有了真正的地址转换了。但是这个时候我们还是运行在一一对应的地址映射空间,还需要跳一次才能够进入真正的虚拟地址映射的空间。 96 | 97 | 代码很有意思 98 | 99 | ``` 100 | /* Ensure I am executing from virtual addresses */ 101 | movq $1f, %rax 102 | jmp *%rax 103 | 1: 104 | ``` 105 | 106 | 就是取到下一个lable的地址,然后跳过去~ 107 | 108 | 好了,内核就这么欢快的在它自己的**虚拟地址**上运行了~ 109 | 110 | # 后记 111 | 112 | 写完之后自己再看一遍,却担心在手机前的你并不能真的理解内核页表究竟是如何长成这样的。有些东西不通过自己亲身阅读,加上动手实验,别人再怎么讲也没有办法真的get到那个点。 113 | 114 | 才发现文字和图片是如此的单薄,我只能告诉你发现的最后结果,却无法帮助你体会探索过程中想通页表模样时那种被电击一般的体验。不知道你是否能够体会,如果此文能对你有一点点的帮助,我也感到非常开心。 115 | 116 | 加油~ 117 | 118 | # 更新:2024.03.15 119 | 120 | 看了下最新的内核,这部分的页表初始化有些许变化。 121 | 122 | 虽然也是在编译时候就基本写好了页表的内容,但是至少有两点变化: 123 | 124 | * 支持5级页表的改动 125 | * 支持内核随机加载 126 | 127 | 总体还好,只要理解了静态的问题不大。这部分代码在函数__startup_64中完成。 128 | 129 | 另外时隔多年后回过头来再看,这个页表的最大作用是切换到内核的虚拟地址。也算是兜兜转转又找了一圈。 -------------------------------------------------------------------------------- /kernel_pagetable/03-pagetable_after_cleanup_highmap.md: -------------------------------------------------------------------------------- 1 | 在x86平台的setup_arch中,会对内核的虚拟机地址空间做一个剪切。具体原因可以看代码的注释。 2 | 3 | ``` 4 | /* 5 | * The head.S code sets up the kernel high mapping: 6 | * 7 | * from __START_KERNEL_map to __START_KERNEL_map + size (== _end-_text) 8 | * 9 | * phys_base holds the negative offset to the kernel, which is added 10 | * to the compile time generated pmds. This results in invalid pmds up 11 | * to the point where we hit the physaddr 0 mapping. 12 | * 13 | * We limit the mappings to the region from _text to _brk_end. _brk_end 14 | * is rounded up to the 2MB boundary. This catches the invalid pmds as 15 | * well, as they are located before _text: 16 | */ 17 | ``` 18 | 19 | 说的有点多,最后的结果比较简单,就是只留下了_text到_brk_end之间的页表映射。 20 | 21 | 那咱们来打印一下,看看效果呗。 22 | 23 | # 调试补丁 24 | 25 | ``` 26 | diff --git a/arch/x86/mm/init_64.c b/arch/x86/mm/init_64.c 27 | index 14b9dd7..2ffb7f2 100644 28 | --- a/arch/x86/mm/init_64.c 29 | +++ b/arch/x86/mm/init_64.c 30 | @@ -310,6 +310,25 @@ void __init cleanup_highmap(void) 31 | unsigned long vaddr_end = __START_KERNEL_map + KERNEL_IMAGE_SIZE; 32 | unsigned long end = roundup((unsigned long)_brk_end, PMD_SIZE) - 1; 33 | pmd_t *pmd = level2_kernel_pgt; 34 | + int i = 0; 35 | + 36 | + pr_err(": phys_base %lx\n", phys_base ); 37 | + pr_err(": #level2_kernel_pgt %lu\n", KERNEL_IMAGE_SIZE/PMD_SIZE); 38 | + pr_err(": __START_KERNEL_map %lx\n", __START_KERNEL_map); 39 | + pr_err(": __START_KERNEL %lx\n", __START_KERNEL); 40 | + pr_err(": _text %lx\n", (unsigned long)_text); 41 | + pr_err(": _brk_end %lx\n", (unsigned long)_brk_end); 42 | + pr_err(": _end %lx\n", (unsigned long)_end); 43 | + pr_err(": __START_KERNEL_map + KI %lx\n", 44 | + __START_KERNEL_map + KERNEL_IMAGE_SIZE); 45 | + 46 | + for (i = 0; i < 512; i++) { 47 | + if (pmd_none(*(pmd + i))) 48 | + continue; 49 | + 50 | + pr_err(": level2_kernel_pgt[%d] = %lx\n", 51 | + i, pmd_val(*(pmd+i))); 52 | + } 53 | 54 | /* 55 | * Native path, max_pfn_mapped is not set yet. 56 | @@ -325,6 +344,17 @@ void __init cleanup_highmap(void) 57 | if (vaddr < (unsigned long) _text || vaddr > end) 58 | set_pmd(pmd, __pmd(0)); 59 | } 60 | + 61 | + pr_err(": 2nd round\n"); 62 | + pmd = level2_kernel_pgt; 63 | + for (i = 0; i < 512; i++) { 64 | + if (pmd_none(*(pmd + i))) 65 | + continue; 66 | + 67 | + pr_err(": level2_kernel_pgt[%d] = %lx\n", 68 | + i, pmd_val(*(pmd+i))); 69 | + } 70 | + 71 | } 72 | 73 | /* 74 | ``` 75 | 76 | 稍微有点长,但是功能却比较简单。 77 | 78 | * 打印了几个比较重要的变量 79 | * 打印了level2_kernel_pgt中的非空项 80 | 81 | # 几个重要的变量 82 | 83 | 下面打印了内核中几个比较重要的虚拟地址的值,按照大小排序: 84 | 85 | ``` 86 | [ 0.000000] : phys_base 0 87 | [ 0.000000] : #level2_kernel_pgt 256 88 | [ 0.000000] : __START_KERNEL_map ffffffff80000000 89 | [ 0.000000] : __START_KERNEL ffffffff81000000 90 | [ 0.000000] : _text ffffffff81000000 91 | [ 0.000000] : _brk_end ffffffff82236000 92 | [ 0.000000] : _end ffffffff82256000 93 | [ 0.000000] : __START_KERNEL_map + KI ffffffffa0000000 94 | ``` 95 | 96 | 注,这个内核我disable了RANDOMIZE_MEMORY。 97 | 98 | 在没有随机放置内核的情况下 phys_base为0,所以内核的起始地址并没有变化和编译时的一样。 99 | 100 | 这里要看的是那个KI 101 | 102 | ``` 103 | KI = 0xa0000000 - 0x80000000 104 | = 0x20000000 105 | = 512MB 106 | ``` 107 | 108 | 这个值就是内核镜像的大小,也就是我们需要做内核地址映射的大小。因为每个PMD映射空间是2MB,所以整个内核地址空间需要映射256个entry。 109 | 110 | # level2_kernel_pgt的变化 111 | 112 | 截取调试打印的部分 113 | 114 | ``` 115 | [ 0.000000] : level2_kernel_pgt[0] = 1e3 116 | [ 0.000000] : level2_kernel_pgt[1] = 2001e3 117 | [ 0.000000] : level2_kernel_pgt[2] = 4001e3 118 | [ 0.000000] : level2_kernel_pgt[3] = 6001e3 119 | [ 0.000000] : level2_kernel_pgt[4] = 8001e3 120 | [ 0.000000] : level2_kernel_pgt[5] = a001e3 121 | ... 122 | [ 0.000000] : level2_kernel_pgt[251] = 1f6001e3 123 | [ 0.000000] : level2_kernel_pgt[252] = 1f8001e3 124 | [ 0.000000] : level2_kernel_pgt[253] = 1fa001e3 125 | [ 0.000000] : level2_kernel_pgt[254] = 1fc001e3 126 | [ 0.000000] : level2_kernel_pgt[255] = 1fe001e3 127 | 128 | [ 0.000000] : 2nd round 129 | [ 0.000000] : level2_kernel_pgt[8] = 10001e3 130 | [ 0.000000] : level2_kernel_pgt[9] = 12001e3 131 | [ 0.000000] : level2_kernel_pgt[10] = 14001e3 132 | [ 0.000000] : level2_kernel_pgt[11] = 16001e3 133 | [ 0.000000] : level2_kernel_pgt[12] = 18001e3 134 | [ 0.000000] : level2_kernel_pgt[13] = 1a001e3 135 | [ 0.000000] : level2_kernel_pgt[14] = 1c001e3 136 | [ 0.000000] : level2_kernel_pgt[15] = 1e001e3 137 | [ 0.000000] : level2_kernel_pgt[16] = 20001e3 138 | [ 0.000000] : level2_kernel_pgt[17] = 22001e3 139 | ``` 140 | 141 | 在cleanup_highmap()之前,level2_kernel_pgt中一共有256项有效项。你看是不是和之前计算的能对应上了。而且每个映射是线性的。 142 | 143 | 经过cleanup_highmap()之后,level2_kernel_pgt中只有[8..17]项是有效映射了。然后再来仔细分析一下。[8] = 0x1000000 这个值是不是正好是_pa(_text)?而[17] = 0x2200000 又和_brk_end对应上? 144 | 145 | # 长这样 146 | 147 | 用图来看,应该能够更清楚一些 148 | 149 | ![这里写图片描述](/kernel_pagetable/pagetable_after_cleanup_highmap.png) 150 | 151 | 感觉离真相又近了一步~ 152 | -------------------------------------------------------------------------------- /kernel_pagetable/05-switch_to_init_level4_pgt.md: -------------------------------------------------------------------------------- 1 | 内核启动阶段,使用的页表是early_level4_pgt。(有可能不是,大部分情况下是的。) 2 | 3 | early_level4_pgt的设置可以看[head_64.S设置的段页][1] 4 | 5 | 看这个名字就能猜出来,这个页表仅仅是初始的时候使用的。那什么时候内核第一次更改的呢? 6 | 7 | # 成功上位 8 | 9 | ``` 10 | void __init init_mem_mapping(void) 11 | { 12 | 13 | load_cr3(swapper_pg_dir); 14 | __flush_tlb_all(); 15 | 16 | } 17 | ``` 18 | 19 | 然后呢? 咱来做个实验吧~ 20 | 21 | 给出补丁~ 4.7上的 22 | 23 | ``` 24 | diff --git a/arch/x86/include/asm/pgtable_64.h b/arch/x86/include/asm/pgtable_64.h 25 | index 2ee7811..f6dc1ed 100644 26 | --- a/arch/x86/include/asm/pgtable_64.h 27 | +++ b/arch/x86/include/asm/pgtable_64.h 28 | @@ -21,6 +21,7 @@ extern pmd_t level2_fixmap_pgt[512]; 29 | extern pmd_t level2_ident_pgt[512]; 30 | extern pte_t level1_fixmap_pgt[512]; 31 | extern pgd_t init_level4_pgt[]; 32 | +extern pgd_t early_level4_pgt[]; 33 | 34 | #define swapper_pg_dir init_level4_pgt 35 | 36 | diff --git a/arch/x86/mm/init.c b/arch/x86/mm/init.c 37 | index 372aad2..ba19480 100644 38 | --- a/arch/x86/mm/init.c 39 | +++ b/arch/x86/mm/init.c 40 | @@ -577,7 +577,7 @@ static void __init memory_map_bottom_up(unsigned long map_start, 41 | 42 | void __init init_mem_mapping(void) 43 | { 44 | - unsigned long end; 45 | + unsigned long end, pgd; 46 | 47 | probe_page_size_mask(); 48 | 49 | @@ -619,7 +619,13 @@ void __init init_mem_mapping(void) 50 | early_ioremap_page_table_range_init(); 51 | #endif 52 | 53 | + printk(KERN_ERR "%s: early_level4_pgt is %lx\n", __func__, __pa_symbol(early_level4_pgt )); 54 | + printk(KERN_ERR "%s: init_level4_pgt is %lx\n", __func__, __pa_symbol(init_level4_pgt )); 55 | + pgd = read_cr3(); 56 | + printk(KERN_ERR "%s: current cr3 is %lx\n", __func__, pgd); 57 | load_cr3(swapper_pg_dir); 58 | + pgd = read_cr3(); 59 | + printk(KERN_ERR "%s: cr3 is set to %lx\n", __func__, pgd); 60 | __flush_tlb_all(); 61 | 62 | early_memtest(0, max_pfn_mapped << PAGE_SHIFT); 63 | ``` 64 | 65 | 其实很简单,就是打印了一下early_level4_pgt, init_level4_pgt,用来对比一下cr3中,原有的值和更改后的值。 66 | 67 | 简单吧,然后看一下这次输出的结果,看看我们的猜想对不对~ 68 | 69 | ``` 70 | [ 0.000000] init_mem_mapping: early_level4_pgt is 1fca000 71 | [ 0.000000] init_mem_mapping: init_level4_pgt is 1e06000 72 | 73 | [ 0.000000] init_mem_mapping: current cr3 is 1fca000 74 | [ 0.000000] init_mem_mapping: cr3 is set to 1e06000 75 | ``` 76 | 77 | 好了,内核页表切换完成。进入到init_level4_pgt统治的时代了。 78 | 79 | # 补充细节 80 | 81 | 之前内核都是使用early_level4_pgt这个页表的,那和init_level4_pgt有什么关系呢? 82 | 83 | 来来看看这段代码。 84 | 85 | ``` 86 | asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data) 87 | { 88 | ... 89 | 90 | clear_page(init_level4_pgt); 91 | 92 | ... 93 | 94 | /* set init_level4_pgt kernel high mapping*/ 95 | init_level4_pgt[511] = early_level4_pgt[511]; 96 | 97 | ... 98 | } 99 | ``` 100 | 101 | 在这段代码中,init_level4_pgt[511]设置成了early_level4_pgt一样的值。 102 | 103 | 好了这下清楚了,从本质上讲,这两个页表没有什么区别,本次的页表和之前的页表样子没有变。 104 | 105 | [1]: http://blog.csdn.net/richardysteven/article/details/52629731#t16 106 | -------------------------------------------------------------------------------- /kernel_pagetable/intel_virtual_phys_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kernel_pagetable/intel_virtual_phys_add.png -------------------------------------------------------------------------------- /kernel_pagetable/map_whole_memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kernel_pagetable/map_whole_memory.png -------------------------------------------------------------------------------- /kernel_pagetable/pagetable_after_cleanup_highmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kernel_pagetable/pagetable_after_cleanup_highmap.png -------------------------------------------------------------------------------- /kernel_pagetable/pagetable_before_decompression.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kernel_pagetable/pagetable_before_decompression.png -------------------------------------------------------------------------------- /kernel_pagetable/pagetable_compiled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kernel_pagetable/pagetable_compiled.png -------------------------------------------------------------------------------- /kernel_pagetable/switch_to_init_level4_pgt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kernel_pagetable/switch_to_init_level4_pgt.png -------------------------------------------------------------------------------- /kvm/00-kvm.md: -------------------------------------------------------------------------------- 1 | KVM一直是想要好好学习的一个内容,终于有时间和需要进行记录了。 2 | -------------------------------------------------------------------------------- /kvm/01-memory_virtualization.md: -------------------------------------------------------------------------------- 1 | 到哪里内存都是系统中重要的资源,那在虚拟环境下 2 | 3 | * 如何管理内存宿主机 4 | * 如何获得虚拟机上的内存信息 5 | * 如何得到虚拟机中内存地址和宿主机上内存地址之间的对应? 6 | 7 | 这些都是在虚拟环境下遇到的问题和挑战。 8 | 9 | 就我当前粗陋的理解,下面会从两个方面尝试解读。 10 | 11 | * [首先是基于Qemu用户态程序中的内存管理模型][1] 12 | * [其次是KVM内核模块中内存管理的机制][2] 13 | 14 | 在查阅资料的过程中也发现了网上不错的内容,其中有更多的代码细节,有兴趣的读者可以进一步学习 15 | 16 | * [intel EPT 机制详解][3] 17 | * [QEMU学习笔记——内存][4] 18 | 19 | [1]: /kvm/01_1-qemu_memory_model.md 20 | [2]: /kvm/01_2-kvm_memory_manage.md 21 | [3]: http://www.cnblogs.com/ck1020/p/6043054.html 22 | [4]: https://www.binss.me/blog/qemu-note-of-memory/ 23 | -------------------------------------------------------------------------------- /kvm/01_2-kvm_memory_manage.md: -------------------------------------------------------------------------------- 1 | 书接上回,看过了Qemu中的内存模型,这下该来看看KVM中的工作了。 2 | 3 | # 从Qemu获得的信息 4 | 5 | 上文中我们停在了Qemu和KVM之间的握手上,那我们就先来看看这次交流之后KVM那头发生了什么。 6 | 7 | 说简单也简单,那就是把Qemu传递过来的信息记录了起来,保存在了一个叫kvm_memory_slot的结构体中。 8 | 9 | ``` 10 | struct kvm_memory_slot { 11 | gfn_t base_gfn; 12 | unsigned long npages; 13 | unsigned long *dirty_bitmap; 14 | struct kvm_arch_memory_slot arch; 15 | unsigned long userspace_addr; 16 | u32 flags; 17 | short id; 18 | }; 19 | ``` 20 | 21 | 起始也就是记录下来了虚拟机中对应GPA的HVA。那记录下来是要干啥呢? 22 | 23 | 对了,构造EPT表。 24 | 25 | # 逆向盗梦空间 26 | 27 | 先来看一张图 28 | 29 | ![ept](/kvm/ept.png) 30 | 31 | 这张图描述了EPT的作用,**GPA->HPA的转换**。 32 | 33 | 当虚拟机中的系统访问虚拟机内的物理地址GPA时,系统就可以通过EPT找到真实的内存地址HPA。 34 | 35 | 是不是有点像盗梦空间中从梦中回到现实的感觉? 36 | 37 | # EPT树 38 | 39 | 刚才看了EPT的原理图,接着我们看看在代码中是如何表示的。 40 | 41 | 在代码中,用kvm_mmu_page结构体来表示EPT结构中的一个节点。如果大家有过页表的概念,那么可以 42 | 将EPT想象为一个树形结构,而其中的每个节点就是用kvm_mmu_page结构来描述。 43 | 44 | 截取这棵树上的一个分叉,就像下图一样。 45 | 46 | ``` 47 | kvm_mmu_page <---------+ 48 | +--------------------------|---+ 49 | +-----------|parent_ptes | | 50 | | | | | 51 | | | | | 52 | | |spt--+ one page | | 53 | v | | page->private ---+ | 54 | +-----------|--> +-->+--------------------+ 55 | | | | | 56 | | | | | 57 | | | | | 58 | kvm_mmu_page <---------+ | | 512 | | 59 | +--------------------------|---+ | | entries | | 60 | | | | | | | | 61 | | | | | | | | 62 | | | | | | | | 63 | |spt--+ one page | | | +---------+--------------------+ 64 | | | page->private ---+ | | 65 | | +-->+--------------------+ | 66 | | | | | 67 | | | ---|----------+ 68 | | | | 69 | | 512 | | 70 | | entries | | 71 | | | | 72 | | | | 73 | | | ---|----------+ 74 | +---------+--------------------+ | 75 | | 76 | | 77 | | 78 | | kvm_mmu_page <---------+ 79 | | +--------------------------|---+ 80 | |<----------|parent_ptes | | 81 | | | | | 82 | | | | | 83 | | |spt--+ one page | | 84 | | | | page->private ---+ | 85 | +-----------|--> +-->+--------------------+ 86 | | | | 87 | | | | 88 | | | | 89 | | 512 | | 90 | | entries | | 91 | | | | 92 | | | | 93 | | | | 94 | +---------+--------------------+ 95 | ``` 96 | 97 | 好了,简单的看内存虚拟化在KVM中就是这样的。 98 | 99 | 而真正复杂的内容隐藏于细节之中,待我悟到之后再来详述。 100 | -------------------------------------------------------------------------------- /kvm/ept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/kvm/ept.png -------------------------------------------------------------------------------- /load_kernel/00_index.md: -------------------------------------------------------------------------------- 1 | # start_kernel之前 2 | 3 | 一般认为start_kernel是整个内核代码开始的地方。但你有没有好奇过在这个之前都发生了什么。 4 | 5 | 就好像应用程序main函数在执行之前,加载器会做准备工作一样。在内核真正运行前也要有很多准备工作。这里我们就详细探索一下内核加载的全流程。 6 | 7 | 首先我们用bochs探索[bootloader如何加载bzImage][2]。 8 | 9 | 然后我们看看[内核压缩与解压][4] 10 | 11 | 最后记录一些学习过程中的[保护模式内核代码赏析][3] 12 | 13 | [2]: /load_kernel/02_how_bzImage_loaded.md 14 | [3]: /load_kernel/03_analysis_protected_kernel.md 15 | [4]: /load_kernel/04_compress_decompress_kernel.md -------------------------------------------------------------------------------- /memcg/00-index.md: -------------------------------------------------------------------------------- 1 | memcg在当前云计算场景下被广泛应用,其目标是限定用户对主机内存的使用。本章我们将对memcg的工作原理做探究。 2 | 3 | 首先我们来简单看一下[memcg初始化][1]的过程。因为memcg是cgroup的子系统,所以更多的细节可以看[cgroup][2]。 4 | 5 | 既然memcg是用来限制用户对内存使用的,那么自然我们要了解如何[限制memcg大小][3]和[对memcg记账][4] 6 | 7 | [1]: /memcg/01-init_overview.md 8 | [2]: /cgroup/00-index.md 9 | [3]: /memcg/02-set_memcg_limit.md 10 | [4]: /memcg/03-charge_memcg.md 11 | -------------------------------------------------------------------------------- /memcg/01-init_overview.md: -------------------------------------------------------------------------------- 1 | 作为cgroup的一个子系统,自然在初始化时需要遵照cgroup的框架。所以我们先来回顾一下cgroup的框架。 2 | 3 | ``` 4 | cgroup_init_subsys(ss, early) 5 | ss->root = &cgrp_dfl_root 6 | css = ss->css_alloc() 7 | init_and_link_css(css, ss, &cgrp_dfl_root.cgrp) 8 | css->cgroup = cgrp 9 | css->ss = ss 10 | init_css_set.subsys[ss->id] = css 11 | online_css(css) 12 | ss->css_online(css) 13 | css->cgroup->subsys[ss->id] = css 14 | ``` 15 | 16 | 其中 ss 是 memory_cgrp_subsys,那么对应调用的函数就是 17 | * css_alloc = mem_cgroup_css_alloc 18 | * css_online = mem_cgroup_css_online 19 | 20 | 这两个函数除了mem_cgroup_css_online中有一个周期性刷新状态的“工作”,其余做的工作比较直白。 21 | 22 | 所以初始化的过程没有什么神秘的,但是我们借此机会看一眼memcg的数据结构。mem_cgroup_css_alloc分配的数据结构是mem_cgroup。 23 | 24 | 这个结构体很长,我把重要的部分分类出来。 25 | 26 | ``` 27 | mem_cgroup 28 | +-------------------------------------+ 29 | |memory/swap/memsw/kmem/tcpmem | 30 | | (struct page_counter) | 31 | | | 32 | |thresholds/memsw_thresholds | 33 | | (struct mem_cgroup_thresholds) | 34 | | | 35 | |vmstats | 36 | | (struct memcg_vmstats) | 37 | |vmstats_percpu | 38 | | (struct memcg_vmstats_percpu) | 39 | | | 40 | |[]nodeinfo | 41 | | (struct mem_cgroup_per_node*) | 42 | +-------------------------------------+ 43 | ``` 44 | 45 | 对其中的含义做个大致的解释: 46 | 47 | * memory/swap/memsw/kmem/tcpmem 保存了用户设置的限额和当前使用额度 48 | * thresholds/memsw_thresholds保存了eventfs相关的水线 49 | * vmstats/vmstats_percpu保存了页的统计数据 50 | * nodeinf主要和页回收相关 51 | 52 | 说到这里再提一点,page_counter里自己还保存了一个树形结构。这个结构和cgroup形成的树形结构是一致的。这或许是一个遗留问题。 53 | -------------------------------------------------------------------------------- /memcg/02-set_memcg_limit.md: -------------------------------------------------------------------------------- 1 | 使用memcg最常用的目的就是限制内存使用。这一节我们就来看看用户如何设置内存使用限制,下一节我们就来看看内核是如何做到对内存记账的。 2 | 3 | # memory.limit_in_bytes 4 | 5 | 在[使用cgroup控制进程cpu和内存][1]中我们已经看到,用户可以通过写memory.limit_in_bytes来限制memcg的大小。这里我们就来看看这个具体过程。 6 | 7 | ``` 8 | mem_cgroup_write() 9 | mem_cgroup_resize_max(memcg, nr_pages, false) 10 | counter = &memcg->memory 11 | page_counter_set_max(counter, max) 12 | try_to_free_mem_cgroup_pages(memcg, 1, GFP_KERNEL, !memsw) 13 | ``` 14 | 15 | 设置内存大小限制的逻辑还是比较简单的。用一句话来概括就是设置memcg->memory这个page_counter的max值。 16 | 17 | 如果设置最大值失败,也就是当前使用已经超过我们要设置的值,那么就会调用try_to_free_mem_cgroup_pages尝试直接回收一些内存,然后再试。 18 | 19 | 这部分确实挺简单的,恭喜你又掌握了一个知识点。 20 | 21 | # memory.soft_limit_in_bytes 22 | 23 | 和zone的水线对应,memcg也有一个“水线”的概念。但是相对来说简单许多,也就是只有一根线。当超过这条线内核就会尝试做内存回收。 24 | 25 | ``` 26 | mem_cgroup_write() 27 | memcg->soft_limit = nr_pages 28 | ``` 29 | 30 | 从代码上看,设置软限制也相对简单的多。 31 | 32 | 但是这个soft limit的作用点相对较少,而且社区渐渐想要废弃这块的作用。 33 | 34 | [1]: /cgroup/01-control_cpu_mem_by_cgroup.md 35 | -------------------------------------------------------------------------------- /memcg/03-charge_memcg.md: -------------------------------------------------------------------------------- 1 | 既然是要限制内存使用,自然需要一个办法来对内存使用记账。也就是将内存的消耗对应到memcg,这样才有可能知道内存是否超过限制并采取行动。 2 | 3 | # 两个入口 4 | 5 | 经过调研,当前内核5.17一共有两个对外的入口做memcg记账 6 | 7 | * mem_cgroup_charge 8 | * mem_cgroup_swapin_charge_page 9 | 10 | 前者是在做page fault时,而后者是在swapin时。别看就这两个入口,但在内核里要找全安插这两个入口的点可真是不容易。没有对内核全面的了解,真是做不到。所以在现在内核的框架下,能统一这个入口是不是也是一个改进方向呢? 11 | 12 | # 核心函数 charge_memcg 13 | 14 | 不论是从哪个入口对memcg做记账,都会调用到函数charge_memcg()。 15 | 16 | ``` 17 | charge_memcg(folio, memcg, gfp) 18 | try_charge(memcg, gfp, nr_pages) 19 | page_counter_try_charge(&memcg->memsw, batch, &counter) 20 | page_counter_try_charge(&memcg->memory, batch, &counter) 21 | mem_over_limit = mem_cgroup_from_counter(counter, memory); 22 | memcg_memory_event(mem_over_limit, MEMCG_MAX) 23 | try_to_free_mem_cgroup_pages(mem_over_limit, ) 24 | mem_cgroup_oom(mem_over_limit, gfp) 25 | css_get(memcg->css) 26 | commit_charge(folio, memcg) 27 | folio->memcg_data = memcg 28 | mem_cgroup_charge_statistics(memcg, nr_pages) 29 | memcg_check_events(memcg, folio_nid(folio)) 30 | ``` 31 | 32 | 简单来说就是尝试用page_counter_try_charge()做记账,如果没成功就用try_to_free_mem_cgroup_pages()或者mem_cgroup_oom()回收一些资源然后再尝试。 33 | -------------------------------------------------------------------------------- /mm/00-memory_a_bottom_up_view.md: -------------------------------------------------------------------------------- 1 | 内存模块是内核中一个非常重要的部分。我们现在的计算机都被称作为**存储程序计算机**,也就是所有待运行的程序和数据都需要加载在内存当中方能被执行。正因为如此,很多朋友都希望学习内存模块的工作机制,但碍于内核代码的庞大以及文档的缺失和稀少,总是感觉无从下手。 2 | 3 | 经过一段时间的摸索,我终于对内存模块有了一点点的了解。今天整理成文,希望能给想要探究内存模块的朋友一点点借鉴。 4 | 5 | # 内存模块的层次结构 6 | 7 | 首先内存模块具有一定层次结构的,从物理内存到软件控制的内存经过了几个层次的隔离。 8 | 9 | [e820从硬件获取内存分布][1] 10 | 11 | [原始内存分配器--memblock][2] 12 | 13 | [页分配器][22] 14 | 15 | [Slub分配器][23] 16 | 17 | 大致我们能看到这么四个层次的内存管理结构。前两者基本在内核启动时使用,而平时大多使用的是后两者。 18 | 19 | # 内存管理的不同粒度 20 | 21 | 越是学习,越发现内存管理是一个比较大的话题。这么一路走来,我们从最底层的物理信息一直走到了slub分配器,可以说对内存管理有了一定的认知。 22 | 23 | 此刻请允许我站的远一点来观察整个内存管理架构,也从另一个角度 -- **粒度** 来观察内存管理的奥妙。 24 | 25 | [内存管理的不同粒度][21] 26 | 27 | # 挑战和进化 28 | 29 | 内存管理的层次结构已经逐渐清晰,接着发现在内存子系统随着应用场景和硬件环境的变化也会遇到新的挑战,并对此做出自身的进化。 30 | 31 | 在此仅以一点点的记录来进一步窥探这个神秘的世界。 32 | 33 | [挑战和进化][11] 34 | 35 | # 参考文献 36 | 37 | [Understand Linux VM][18] 38 | 39 | [1]: /mm/01-e820_retrieve_memory_from_HW.md 40 | [2]: /mm/02-memblock.md 41 | [3]: /mm/03-sparsemem.md 42 | [5]: /mm/05-Node_Zone_Page.md 43 | [6]: /mm/06-page_alloc.md 44 | [7]: /mm/07-per_cpu_pageset.md 45 | [8]: /mm/08-slub_general.md 46 | [9]: /mm/09-slub_in_graph.md 47 | [10]: /mm/10-page_struct.md 48 | [11]: /mm/50-challenge_evolution.md 49 | [18]: https://www.kernel.org/doc/gorman/html/understand/index.html 50 | [19]: /mm/11-users_of_buddy.md 51 | [20]: /mm/12-gfp_usage.md 52 | [21]: /mm/13-physical-layer-partition.md 53 | [22]: /mm/page_allocator/00_page_allocator.md 54 | [23]: /mm/slub_allocator/00_slub.md -------------------------------------------------------------------------------- /mm/08-slub_general.md: -------------------------------------------------------------------------------- 1 | slub分配器对一个内核开发者来讲是既熟悉又陌生的。 2 | 3 | 熟悉是因为在开发的过程中大家总会使用到它,什么kmem_cache_alloc(), kmalloc()都是slub分配器的接口。而陌生是因为大部分开发者都不了解slub分配器的工作机制。像页分配器大家至少还听说过伙伴系统,而slub分配器好像真的一点绯闻都没有。 4 | 5 | 那今天我就尝试用我这粗陋的认知给大家揭开一点点盖头来。 6 | 7 | # 从设计理念开始 8 | 9 | > 假装自己懂得很多的样子,给大家讲讲我对内存模块的理解。 10 | 11 | 内存模块就是一个大管家,管理着系统中内存的使用。有人要用内存了,它分配一点。有人释放内存了,它小心翼翼收好。 12 | 13 | 在整个模块中,我们能看到其运用了三个设计思想。 14 | 15 | 第一个思想就是分层。通过之前的文章分析我们也能看到在x86架构上分成了:e820, memblock, page, slub这么几层。分层设计的思想在计算机业十分普遍,其优势也自然用不着我说。 16 | 17 | 第二个思想我认为是**分类管理**。 18 | 19 | 这个思想从页分配器这一层就开始了。为了更好的管理内存,在页分配器这一层,页就依照NUMA和ZONE的属性分类,在不同需求时分配不同的页。而且页还依照他连续空闲的大小分类,这样对不同大小的内存请求也可以快速找到空闲页。slub中也是这种思想, 20 | 21 | > 其做法就是事先分配好指定大小的**内存板**,当有内存请求时,直接在指定的内存板中分配和释放。 22 | 23 | 而这个思想在我们生活中也有用武之地,当遇到比较多的物品时,就可以分类存放方便使用。比如忘了在哪个节目中听说有女明星有上百双鞋放满了一个屋子,那这个时候就可以按照运动鞋,高跟鞋,休闲鞋等分类。否则找鞋所花费的检索时间还不如直接网购一双来的快。 24 | 25 | 第三个思想是预取,也就是事先准备好一些资源而不是每次等有请求时再去分配。 26 | 27 | 这个思想也很常见,比如说cpu上的cache。当每次读取一段数据的时候,把其后的一段内容也加载进cache。根据概率统计,在读取一部分内容后有很大概率会访问之后的部分内容,所以这样的设计可以提升性能。(PS:这也导致了各种性能优化的小窍门) 28 | 29 | slub分配器也使用了这样的思想。在每一个分类的内存板中,都预先保存了空闲的内存以便于快速响应。 30 | 31 | 好了,吹牛的部分讲完了,接下来就看看slub是究竟如何做到分类管理和预取的。 32 | 33 | # 分类管理 34 | 35 | 在slub分配器中,用来做分类管理的就是这个kmem_cache结构体了。 36 | 37 | ``` 38 | kmem_cache 39 | +------------------------------+ 40 | |name | 41 | | (char *) | 42 | +------------------------------+ 43 | |object_size | = original object size 44 | |inuse | = ALIGN(object_size, sizeof(void *)) 45 | |size | = ALIGN(inuse + padding + debug space, s->align) 46 | |align | 47 | |offset | 48 | | (int) | 49 | |reserved | 50 | +------------------------------+ 51 | |oo | 52 | |min | 53 | |max | 54 | | (kmem_cache_order_objects) | 55 | | +--------------------------+ 56 | | |order | 57 | | |order_objects | 58 | +---+--------------------------+ 59 | ``` 60 | 61 | 说到分类,我们总是按照某些属性分的,比如说鞋子的用途。那在slub中按照什么属性分类呢? 62 | 63 | 首先就是按照名字了。所以我们看到在调用kmem_cache_create()的第一个参数就是name,而这个名字就保存在了kmem_cache中的name字段。我们可以通过 64 | 65 | ``` 66 | cat /proc/slabinfo 67 | ``` 68 | 69 | 来查看系统中slub的分类。比如会有常见的task_struct, inode_cache等。 70 | 71 | 另一种是按照大小分类。我们可以在上面命令的输出中看到kmalloc-512, kmalloc-256等字样。这就是那些没有特定名字,按照大小来分配时选用的内存板。 72 | 73 | 其实按照名字分类时隐含了按大小分类的意思,这里单独列出是为了引出slub中对大小的一个计算,也就是按照什么样的标准进行预取。 74 | 75 | # 预取 76 | 77 | 预取的目的是为了能够提高系统的性能,那它是如何做到这一点的呢?我们来看看生活中的例子。 78 | 79 | 比如在超市中,我们看到货架上琳琅满目的商品,这都可以算是预取。超市按照估计预先把商品放在货架上,等待顾客的购买。好像没有见过哪个超市是每个商品只摆放一个的吧?如果每种商品货架上只有一个,那么如果有几个人要购买,还不要等很久? 80 | 81 | > 预取首先节省了顾客的平均购买时间。 82 | 83 | 接下来我们再来看看另一头,服务员的工作。顾客在购买的过程中,服务员也会不断地补充货架。但是从仓库取出商品,到摆放到货架需要一定的时间。如果每当一个商品被拿走就去仓库取出一个商品补充,那是不是太琐碎了呢?如果有哪个超市是这么做的,那么这个超市肯定要倒闭。所以超市中改进的方案是,当货架上的货物基本卖完了后,才去仓库取出**一批**货物补充货架。 84 | 85 | > 预取其次降低了服务员的工作负担。 86 | 87 | 道理都是懂的,但是落到操作上就有一个实际问题需要解决--一批是多少?拿少了不能最大化每次取货的劳动,拿多了货架上放不下。那如果是你,你会如何定义“一批”的数量呢? 88 | 89 | > 如果是我的话,我可能会选择补满一货架 90 | 91 | **生活中如此,内核中也是如此。** 92 | 93 | 内存管理模块是层次化的,slub分配器建立在页分配器上,所以可以牵强的理解为**页是slub的货架**。 94 | 95 | 超市中货物上架前需要做好两个计算: 96 | 97 | * 货架的大小 98 | * 货架上能放多少货物 99 | 100 | 在slub中同样要计算相应的两个值: 101 | 102 | * 用多大的页来作为货架 103 | * 每个页中可以放多少object 104 | 105 | 这两个数据都保存在上图kmem_cache结构体中的oo字段。整个计算过程在calculate_sizes()函数中,图表中其余字段在计算过程中各有用途。 106 | 107 | PS:上面说到的页不是单个的物理页,而是内核struct page对应页的概念。 108 | 109 | 来举两个例子说明一下问题: 110 | 111 | * 假如想要申请的结构体(货物)大小是512字节,那么页(货架)可以选择为4K字节大小,每个页(货架)上就可以存放8个结构体(货物)。 112 | * 假如想要申请的结构体(货物)大小是2050字节,那么页(货架)可以选择为8K字节大小,每个页(货架)上就可以存放3个结构体(货物)。 113 | 114 | 当然在实际计算的时候第二种情况的值可能不是这样,因为大家可以看到这么选择其实会有较大的浪费,内核很有可能选择更大的页来减少内存浪费。 115 | 116 | 好了,希望本文对大家理解slub有一点点的帮助。slub依然博大精深,还有很多非常巧(nue)妙(xin)的设计。有兴趣的童鞋做好心理准备~ 117 | -------------------------------------------------------------------------------- /mm/11-users_of_buddy.md: -------------------------------------------------------------------------------- 1 | 内存从页分配器(buddy系统)中分配出去后,就散落在了系统的各个角落。对于一个特定的pfn或者page,怎么知道这个页是分配给了谁,用在了哪里是内核对内存管理的一个任务。 2 | 3 | 比如函数memory_failure中就对不同用途的页做了不同的出错处理。下面我将列出我所知道的页分配器的“知名”用户们。 4 | 5 | 对于页用途的标示,在内核中又分成两类途径: 6 | 7 | * page->flags 8 | * page->page_type 9 | 10 | 不知道为什么要分成两个字段来做区分,可能是用满了吧。 11 | 12 | 鉴于用page_type来区分的类型较少,我们就先介绍page_type中的类型。 13 | 14 | # 以page_type分类的类型 15 | 16 | page_type支持的类型一共就五种 17 | 18 | ``` 19 | #define PG_buddy 0x00000080 20 | #define PG_offline 0x00000100 21 | #define PG_kmemcg 0x00000200 22 | #define PG_table 0x00000400 23 | #define PG_guard 0x00000800 24 | ``` 25 | 26 | 对应的操作通过PAGE_TYPE_OPS来定义,在使用中也是用__SetPageXXX和__ClearPageXXX来判断。 27 | 28 | ## PageTable 29 | 30 | Marks pages in use as page tables. 也就是这个页我们是用来做页表的。 31 | 32 | 设置这个类型的地方只有天字唯一一个地方: 33 | 34 | ``` 35 | __pte_alloc_one 36 | pgtable_pte_page_ctor 37 | __SetPageTable(page) 38 | ``` 39 | 40 | 因为页表,至少在x86上占一个页面,所以没有compound page的可能。 41 | 42 | ## PageBuddy 43 | 44 | 这个用来判断一个页是是否在buddy系统中,如果在的话,说明还没有分配出去。 45 | 46 | 但是有意思的是,并不是所有在buddy系统中的空闲页都会设置这个类型。而是只有在整个页的page[0]对应的page_type才会被设置。所以函数is_free_buddy_page()就这样诞生了。 47 | 48 | 设置这个类型的地方也是只有天字唯一一个地方: 49 | 50 | ``` 51 | __free_one_page() 52 | set_page_order() 53 | set_page_private(page, order); 54 | __SetPageBuddy(page); 55 | ``` 56 | 57 | 因为只有对头部的page设置这个值,所以这个判断的作用需要进一步考量。 58 | 59 | ## PageGuard 60 | 61 | 这是一个在debug情况下才用到的判断。 62 | 63 | 调用的地方也只有一个: 64 | 65 | ``` 66 | expand() 67 | set_page_guard() 68 | __SetPageGuard() 69 | ``` 70 | 71 | 这样被分片的页就不能再被分配了。也不知道这是什么调试技巧。 72 | 73 | # 以flags分类的类型 74 | 75 | ## PageHuge 76 | 77 | 这个判断用来确认对应的页是不是hugetlbfs中分配的。其中有个重要的依据就是compound_dtor是不是HUGETLB_PAGE_DTOR。 78 | 79 | 这个设置的工作在prep_new_huge_page()函数中完成。 80 | 81 | 从PageHuge的代码中可以看到,被判断的为真的页必须是compound page。这个设置的过程有点复杂。 82 | 83 | 比如在alloc_fresh_huge_page()函数中,分成两种情况。 84 | 85 | * alloc_gigantic_page 86 | * alloc_buddy_huge_page 87 | 88 | 后者alloc_buddy_huge_page函数中,明确设置了__GFP_COMP。 89 | 但是在alloc_gigantic_page中,并没有用通用的方式设置compound page。而是在prep_compound_gigantic_page中设置了对应的Head/Tail。这一点颇为有趣。 90 | 91 | ## PageLRU 92 | 93 | 这个判断用来标示指定的页是不是在lru链表上。在pagevec cache上的页,也不会设置这个标示。 94 | 95 | 咨询了一下huangying,一共有四种情况会把page放到lru链表上。 96 | 97 | * anonymous page 98 | * file backend pages 99 | * page cache(read/write through syscall) 100 | * shmem 101 | 102 | 按照我的理解,添加到lru链表的一个重要来源如下: 103 | 104 | ``` 105 | __lru_cache_add() 106 | __pagevec_lru_add() 107 | __pagevec_lru_add_fn() 108 | ``` 109 | 110 | 而__lru_cache_add()函数的调用者有三种情况: 111 | 112 | * lru_cache_add_anon 113 | * lru_cache_add_file 114 | * lru_cache_add 115 | 116 | 调用的地方较多,其中有一个地方的流程是: 117 | 118 | ``` 119 | handle_pte_fault 120 | do_anonymous_page 121 | page = alloc_zeroed_user_highpage_movable() 122 | lru_cache_add_active_or_unevictable() 123 | lru_cache_add() 124 | ``` 125 | 126 | 从这一点上至少可以证明普通进程中的映射的页会添加到lru链表中。同样看了一眼hugepage的流程,hugepage的页面也会添加到lru链表中。 127 | 128 | ## PageSlab() 129 | 130 | 对于给slab分配器使用的内存,只要这个用来判断就知道了。 131 | 132 | 设置这个bit的位置只有一个,在函数allocate_slab()中。从页分配器中获得了page后,则会做上相应的标记。 133 | 134 | 另外需要说明的是,slab中分配到的页只要是高阶的那就是compound page。因为在函数calculate_size中会做如下操作。 135 | 136 | ``` 137 | if (order) 138 | s->allocflags |= __GFP_COMP; 139 | ``` 140 | 141 | ## PageTransHuge 142 | 143 | 这个函数说是用来判断Transparent Huge Page的,但说实话我没有看懂它是怎么做到的。因为它其实只是判断了是不是PageHead。 144 | 145 | 那我们还是来看看Transparent Huge Page是怎么个分配的吧。 146 | 147 | Transparent Huge Page的分配发生在缺页中断,其中有两个地方可能分配create_huge_pud和create_huge_pmd。 148 | 149 | 我们以匿名页表的pmd创建为例。 150 | 151 | ``` 152 | do_huge_pmd_anonymous_page 153 | gfp = alloc_hugepage_direct_gfpmask(vma); 154 | return GFP_TRANSHUGE or GFP_TRANSHUGE_LIGHT 155 | page = alloc_hugepage_vma(gfp, vma, haddr, HPAGE_PMD_ORDER); 156 | prep_transhuge_page(page); 157 | set_compound_page_dtor(page, TRANSHUGE_PAGE_DTOR) 158 | ``` 159 | 160 | 其中值得注意的是gfp的计算和prep_transhuge_page。 161 | 162 | alloc_hugepage_direct_gfpmask返回的gfp都包含了__GFP_COMP。这一点保证了得到的页一定是compound page。 163 | prep_transhuge_page中设置了compound_dtor。个人感觉用这个来做判断更加准确。 164 | 165 | # PageSwapCache 166 | 167 | 有几个地方设置,其中之一在函数add_to_swap_cache()中。 168 | 169 | ``` 170 | shrink_page_list 171 | add_to_swap(page) 172 | add_to_swap_cache(page, entry, gfp) 173 | SetPageSwapCache(page) 174 | ``` 175 | 176 | 感觉这条线还是从kswapd下来的。 177 | 178 | # PageSwapBacked 179 | 180 | 这个也有几个地方可以设置,其中一个在缺页中断的do_swap_page()。 181 | 182 | ``` 183 | handle_pte_fault 184 | if (!pte_present(vmf->orig_pte)) 185 | return do_swap_page(vmf); 186 | __SetPageSwapBacked(page) 187 | ``` 188 | 189 | 感觉意思是发生缺页中断,如果这个页有在swap中了,那就标记这个位。 190 | -------------------------------------------------------------------------------- /mm/14-folio.md: -------------------------------------------------------------------------------- 1 | Folio 是2020年Matthew引入的,用来代表物理内存的新结构。 2 | 3 | 4 | ``` 5 | commit 7b230db3b8d373219f88a3d25c8fbbf12cc7f233 6 | Author: Matthew Wilcox (Oracle) 7 | Date: Sun Dec 6 22:22:48 2020 -0500 8 | 9 | mm: Introduce struct folio 10 | 11 | A struct folio is a new abstraction to replace the venerable struct page. 12 | A function which takes a struct folio argument declares that it will 13 | operate on the entire (possibly compound) page, not just PAGE_SIZE bytes. 14 | In return, the caller guarantees that the pointer it is passing does 15 | not point to a tail page. No change to generated code. 16 | ``` 17 | 18 | 相比与page/compound_page,folio的作用是把逻辑上相关的几个page struct用一个folio结构来呈现。 19 | 20 | 而且Matthew的目标是替换调page struct。 21 | 22 | 其实folio的样子说起来很简单,就是几个page struct的组合,但是具体那个成员应该放哪里需要多年内核的功力。 23 | 24 | 我们来看一下结构体,有一个直观的印象: 25 | 26 | 27 | ``` 28 | struct folio { 29 | /* private: don't document the anon union */ 30 | union { 31 | struct { 32 | /* public: */ 33 | unsigned long flags; 34 | ... 35 | }; 36 | struct page page; 37 | }; 38 | union { 39 | struct { 40 | unsigned long _flags_1; 41 | unsigned long _head_1; 42 | ... 43 | }; 44 | struct page __page_1; 45 | }; 46 | union { 47 | struct { 48 | unsigned long _flags_2; 49 | unsigned long _head_2; 50 | ... 51 | }; 52 | struct page __page_2; 53 | }; 54 | union { 55 | struct { 56 | unsigned long _flags_3; 57 | ... 58 | }; 59 | struct page __page_3; 60 | }; 61 | }; 62 | ``` 63 | 64 | 我们只看每个union中第二个成员,就可以看出folio可以理解为一个page[4]的数组。 65 | -------------------------------------------------------------------------------- /mm/50-challenge_evolution.md: -------------------------------------------------------------------------------- 1 | 在没有深入研究内存子系统之前总是觉得这个子系统应该已经经过了千锤百炼,固若金汤了。 2 | 3 | 但是当深入研究之后就会发现,随着使用场景、硬件更新,原有内核的实现就暴露出了不同的问题。 4 | 5 | 在这里我尝试做一点总结。 6 | 7 | # 扩展性 8 | 9 | 内存管理作为系统中最为重要的模块,其处理性能直接影响到整个系统的表现。经过这么多年社区各路大神的打磨,其中有不少值得我们借鉴的地方。 10 | 11 | 扩展性是一个非常有意思的话题,还希望各路大侠多多指教。 12 | 13 | 先来看看有那些设计层次的方法值得借鉴。 14 | 15 | [扩展性的设计和实现][1] 16 | 17 | 再来看一个实际的例子。为了加速页的分配和回收,减少多cpu之间对伙伴系统的竞争,内核给每个zone增加了per_cpu_pageset。 18 | 19 | [per_cpu_pageset][7] 20 | 21 | # 海量内存 22 | 23 | 仔细想象海量内存是对扩展性挑战的一个原因,正因为内存容量的增大导致了扩展性的瓶颈。 24 | 25 | 这一小节和上一小节观察的重点不同。而是主要观察内存控制结构struct page的空间分配和初始化。 26 | 27 | 首先我们来看空间分配上为了应对海量内存做的一步步演进。 28 | 29 | [海量内存][2] 30 | 31 | 其次随着内存容量的增加,需要初始化的page struct也增加。为此内核引入了延迟初始化。 32 | 33 | [延迟初始化][4] 34 | 35 | # 内存热插拔 36 | 37 | 最近有review了相关的patch,发现自己之前竟然没有总结这一块。看来看去不知道放在哪里,就先放在这里吧~ 38 | 39 | [内存热插拔][3] 40 | 41 | # 连续内存分配器 42 | 43 | 不少设备需要很大,如10M,的连续内存。现有的page allocator无法满足这样的需求。 44 | 45 | 所以内核中增加了[连续内存分配器][5]。 46 | 47 | [1]: /mm/51-scalability_design_implementation.md 48 | [2]: /mm/52-where_is_page_struct.md 49 | [3]: /mm/53-memory_hotplug.md 50 | [4]: /mm/54-defer_init.md 51 | [5]: /mm/55-cma.md 52 | -------------------------------------------------------------------------------- /mm/53-memory_hotplug.md: -------------------------------------------------------------------------------- 1 | 本小节我们将来看看内存热插拔的一些流程。 2 | 3 | # 热插的总体流程 4 | 5 | 我们先来看一下热插拔的总体流程: 6 | 7 | ``` 8 | add_memory_resource() 9 | check_hotplug_memory_range() --- (1) 10 | 11 | mem_hotplug_begin() 12 | memblock_add_node(start, size, nid) --- (2) 13 | __try_online_node(nid, start, false) 14 | arch_add_memory() 15 | init_memory_mapping() --- (3) 16 | add_pages() -> __add_pages() --- (4) 17 | create_memory_block_devices() --- (5) 18 | __register_one_node() 19 | register_mem_sect_under_node() 20 | mem_hotplug_done() 21 | 22 | online_memory_block() --- (6) 23 | ``` 24 | 25 | 其中有几个比较关键的地方解释一下: 26 | 27 | * (1) 保证热插拔的内存空间是section对齐 28 | * (2) 把相关的内存信息填写到memblock中 29 | * (3) 填写内核的内存页表(direct mapping) 30 | * (4) 又称为hot-add,后面详细说明 31 | * (5) 创建memory_block设备,用户在sysfs上可以看到 32 | * (6) 又称为hot-online,后面详细说明 33 | 34 | 从上面的流程中可以看到,热插拔的过程包含不少细节。 35 | 但是通常来说,主要分成两个部分: hot-add 和 hot-online。 36 | 37 | 而且这么划分有实际意义。在某些情况下,nvdimm,可以只通过hot-add将内存告知系统,但不添加到buddy system。 38 | 39 | 另外提一点add_memory_resource()的使用者。比如在x86上内存热插拔是通过acpi信号告知系统的,所以acpi驱动是add_memory_resource()的用户之一。 40 | 41 | ## hot-add 42 | 43 | hot-add的意义在于给对应的内存分配page struct,也就是有了内存管理的元数据。这样就可以管理内存了。 44 | 45 | 但此时内存并没有添加到buddy system,也没有初始化page struct。 46 | 47 | 好了,还是先来看一下相关的流程: 48 | 49 | ``` 50 | __add_pages(nid, pfn, nr_pages) 51 | sparse_add_section(nid, pfn, pfns, altmap) 52 | sparse_index_init(section_nr, nid) 53 | memmap = section_activate(nid, pfn, nr_pages, altmap) 54 | memmap = populate_section_memmap(pfn, nr_pages, nid, altmap) 55 | page_init_poison() 56 | set_section_nid(section_nr, nid) 57 | section_mark_present(ms) 58 | sparse_init_one_section(ms, section_nr, memmap, ms->usage, 0) 59 | ms->section_mem_map |= sparse_encode_mem_map(memmap, pnum) 60 | ``` 61 | 62 | 总的来说中规中矩,就是添加了对应的mem_section和memmap,也就是page struct。 63 | 64 | ## hot-online 65 | 66 | hot-online就是要把内存放到buddy system,让内核管理起这部分内存。 67 | 68 | 前面看到的online_memory_block()实际上会走到memory_block_online()。 69 | 70 | 71 | ``` 72 | memory_block_online() 73 | zone = zone_for_pfn_range(online_type, nid, pfn, nr_pages) 74 | online_pages(pfn, nr_pages, online_type) 75 | move_pfn_range_to_zone(zone, pfn, nr_pages, NULL) 76 | init_currently_empty_zone() 77 | resize_zone_range(zone, start_pfn, nr_pages) 78 | resize_pgdat_range(pgdat, start_pfn, nr_pages) 79 | memmap_init_zone() 80 | __init_single_page() --- (1) 81 | set_pageblock_migratetype() 82 | online_pages_range() 83 | generic_online_page() 84 | __free_pages_core() --- (2) 85 | online_mem_sections() 86 | build_all_zonelists(NULL) --- (3) 87 | shuffle_zone(zone) 88 | init_per_zone_wmark_min() 89 | kswapd_run(nid) 90 | kcompactd_run(nid) 91 | writeback_set_ratelimit() 92 | ``` 93 | 94 | 几个重要的步骤: 95 | 96 | * (1) 初始化page struct 97 | * (2) 将page加入到buddy system 98 | * (3) 重新构造所有NODE_DATA上的zonelist 99 | 100 | 这么看好像也不是很难。当然魔鬼在细节,仔细看还是有不少内容的。 101 | 102 | 另外,generic_online_page()有可能会被设置成其他的online函数,比如balloon和virtiomem。 103 | 104 | # 利用qemu测试内存热插 105 | 106 | 这功能还好有虚拟机,否则真是不好测试。 107 | 108 | ## 内核配置 109 | 110 | 有几个内存配置需要加上 111 | 112 | ``` 113 | ACPI_HOTPLUG_MEMORY 114 | MEMORY_HOTPLUG 115 | ARCH_ENABLE_MEMORY_HOTPLUG 116 | MEMORY_HOTPLUG_DEFAULT_ONLINE 117 | 118 | MEMORY_HOTREMOVE 119 | ARCH_ENABLE_MEMORY_HOTREMOVE 120 | ``` 121 | 122 | ## 启动虚拟机 123 | 124 | 启动虚拟机时,有几个参数需要注意。 125 | 126 | 这里给出一个可以用的 127 | 128 | ``` 129 | qemu-system-x86_64 -m 6G,slots=32,maxmem=32G -smp 8 --enable-kvm -nographic \ 130 | -drive file=fedora.img,format=raw -drive file=project.img,format=qcow2 \ 131 | -object memory-backend-ram,id=mem1,size=3G -object memory-backend-ram,id=mem2,size=3G \ 132 | -numa node,nodeid=0,memdev=mem1 -numa node,nodeid=1,memdev=mem2 133 | ``` 134 | 135 | 初始内存是6G,最多可以扩展到32G。配置了两个node,每个node上3G内存。 136 | 137 | 可以通过numactl -H和free -h来确认当前内存。 138 | 139 | ## 热插内存 140 | 141 | 启动完后,需要使用qemu monitor来插入内存。 142 | 143 | 通过Ctrl + a + c,调出qemu monitor。 144 | 145 | 146 | ``` 147 | object_add memory-backend-ram,id=ram0,size=1G 148 | device_add pc-dimm,id=dimm0,memdev=ram0,node=1 149 | ``` 150 | 151 | 增加1G内存到node1。 152 | 153 | 因为我们选择了 MEMORY_HOTPLUG_DEFAULT_ONLINE,所以新插的内存直接添加到了系统,可以用free -h查看到。 154 | 155 | ## 手动上线内存 156 | 157 | 前面我们也看到热插的时候有hot-add和hot-online两个步骤。如果没有选择MEMORY_HOTPLUG_DEFAULT_ONLINE,在执行完qemu的命令后需要手动上线内存。 158 | 159 | ``` 160 | cd /sys/devices/system/memory/memory56 161 | sudo echo online_movable > state 162 | ``` 163 | 164 | 事先看好哪个memory设备是新增的,然后进入对应设备的目录执行。 165 | 166 | 还有一些细节在[内核文档][1]里有详细描述。 167 | 168 | [1]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/mm/memory-hotplug.rst 169 | -------------------------------------------------------------------------------- /mm/54-defer_init.md: -------------------------------------------------------------------------------- 1 | defer_init,内核中的延迟满足 2 | 3 | 在上一节的内容中[page结构体,我要如何安放你的灵魂][1]中我们看到了为了应对海量内存,内核是如何安放page结构体的。然而到了这里,事情还没有结束。紧接着应对海量内存,内核遇到了另一个棘手的问题―初始化。 4 | 5 | Page结构体需要初始化后才能加入到buddy分配器供内核中各个模块使用,可想而知,随着内存容量的增加,需要初始化的page结构体也会增加。如果没有记错的话,当内存达到T级别,page结构体初始化的时间将达到分钟级。 6 | 7 | 那有什么办法解决这个问题呢?有的,那就是 8 | 9 | > 延迟满足 10 | 11 | 其实思想很简单,在系统启动时只初始化部分的page结构体,然后使用内核线程来初始化剩下的page结构体。从而达到尽量减少page结构体初始化对系统启动的影响。 12 | 13 | 多说无益,我们还是直接来看代码吧。 14 | 15 | # 正常情况 16 | 17 | 为了有一个对比,我们先来看看没有defer_init的情况。 18 | 19 | ```c 20 | start_kernel() 21 | setup_arch() 22 | x86_init.paging.pagetable_init() -> paging_init() 23 | sparse_init() 24 | sparse_init_nid() 25 | map = __populate_section_memmap() (1) 26 | sparse_init_one_section(sec, map) 27 | zone_sizes_init() 28 | free_area_init() -> free_area_init_node() 29 | free_area_init_core() 30 | memmap_init() -> memmap_init_zone() 31 | __init_single_page() (2) 32 | mm_core_init() -> mem_init() 33 | memblock_free_all() 34 | free_low_memory_core_early() 35 | __free_memory_core() -> __free_pages_memory() 36 | memblock_free_pages() 37 | __free_pages_core() (3) 38 | ``` 39 | 40 | Page结构体要经历三个过程,被分配,被初始化,添加到buddy。 41 | 42 | 在上述代码片段中,分别标出了这三个步骤在正常情况下发生的时机。而defer_init要解决的就是2,3在初始化时占用的时间过多,导致系统启动时间过长。 43 | 44 | 既然是在2,3的地方占用了太多时间,那么就把这部分的工作延后执行把。 45 | 46 | # 延迟满足 47 | 48 | 现在我们来看看内核代码是如何把这部分的工作延后执行的。 49 | 50 | 51 | ```c 52 | start_kernel() 53 | setup_arch() 54 | x86_init.paging.pagetable_init() -> paging_init() 55 | sparse_init() 56 | sparse_init_nid() 57 | map = __populate_section_memmap() 58 | sparse_init_one_section(sec, map) 59 | zone_sizes_init() 60 | free_area_init() -> free_area_init_node() 61 | free_area_init_core() 62 | memmap_init() -> memmap_init_zone() 63 | defer_init() (1) 64 | __init_single_page() 65 | mm_core_init() -> mem_init() 66 | memblock_free_all() 67 | free_low_memory_core_early() 68 | __free_memory_core() -> __free_pages_memory() 69 | memblock_free_pages() 70 | early_page_uninitialised() (2) 71 | __free_pages_core() 72 | rest_init() 73 | pid = kernel_thread(kernel_init, NULL, CLONE_FS); 74 | kernel_init_freeable() -> page_alloc_init_late() 75 | deferred_init_memmap() (3) 76 | ``` 77 | 78 | 在上面的代码片段中可以看到,在1,2的地方分别增加了判断,是否要跳过这部分的初始化。 79 | 80 | 接着在3的位置启用了一个内核线程来初始化剩下的page结构体,并将page释放到buddy system。 81 | 82 | 好了,这件事情就是这么简单~ 83 | 84 | [1]: /mm/52-where_is_page_struct.md 85 | -------------------------------------------------------------------------------- /mm/55-cma.md: -------------------------------------------------------------------------------- 1 | # 使能 2 | 3 | cma是一个可配置功能,可以检查CMA这个内核配置有没有打开。 4 | 5 | 另外还有一个CMA_AREAS的配置参数,默认是20。 6 | 7 | # 数据结构 8 | 9 | ``` 10 | struct cma cma_areas[MAX_CMA_AREAS]; 11 | 12 | cma_init_reserved_mem(base, size, order_per_bit, name, res_cma) 13 | 14 | cma 15 | +---------------------------+ 16 | |count | number of pages 17 | |available_count | = size >> PAGE_SHIFT 18 | |order_per_bit | order_per_bit 19 | | | 20 | |nr_ranges | = 1 21 | |ranges[CMA_MAX_RANGES] | 22 | | (struct cma_memrange) | 23 | | +----------------------+ 24 | | |base_pfn | = PFN_DOWN(base) 25 | | |early_pfn | = PFN_DOWN(base) 26 | | |count | = cma->count 27 | | | | 28 | | |bitmap | 29 | | +----------------------+ 30 | | | | 31 | | | | 32 | +----+----------------------+ 33 | ``` 34 | 35 | # 初始化及过程 36 | 37 | cma是个有意思的东西。他先要通过memblock将需要的内存区域reserve,这样让别人用不到。然后再把这部分内存释放到buddy系统,好让系统使用。 38 | 39 | 在使用方需要的时候,再把这部分内存,如果已经被人用的情况下,迁移到空闲内存后,再使用。 40 | 41 | ## 创建预留 42 | 43 | 创建预留可以分成三步 44 | 45 | * 比如在__cma_declare_contiguous_nid()中先通过memblock预留下需要的空间 46 | * 再通过cma_init_reserved_mem()分到一个cma区域,分到时如上图中所示。 47 | * 然后在cma_init_reserved_areas()初始化,并释放到buddy 48 | 49 | 其中cma_init_reserved_areas()会调用init_cma_reserved_pageblock()将page释放到buddy。并且标注为MIGRATE_CMA类型。 50 | 51 | ## 分配 52 | 53 | cma_alloc() 54 | 55 | ## 释放 56 | 57 | cma_release() 58 | 59 | # 使用的例子 60 | 61 | 目前默认使用cma的用户是dma,在内核启动过程中会调用dma_contiguous_reserve()分配连续内存。 62 | 63 | 这里值的注意的是调用的时机,在x86上,我们可以看到调用的时机在刚确认memblock numa后,在分配memmap前。 64 | 65 | 具体可以参考[memblock][1]中的全局流程。 66 | 67 | 68 | [1]: /mm/02-memblock.md 69 | -------------------------------------------------------------------------------- /mm/common/00_global_variable.md: -------------------------------------------------------------------------------- 1 | # max_pfn 2 | 3 | 最大page frame number. 4 | 5 | ``` 6 | max_pfn = e820__end_of_ram_pfn(); 7 | ``` 8 | 9 | # MAX_PHYSMEM_BITS 10 | 11 | 最大支持物理内存 12 | 13 | ``` 14 | # define MAX_PHYSMEM_BITS (pgtable_l5_enabled() ? 52 : 46) 15 | ``` 16 | 17 | 也就是说,没有5级页表的情况下,物理内存最多是64T。 18 | 19 | # phys_base 20 | 21 | 定义在head_64.S 22 | 23 | ``` 24 | SYM_DATA(phys_base, .quad 0x0) 25 | EXPORT_SYMBOL(phys_base) 26 | ``` 27 | 28 | 赋值在__startup_64() 29 | 30 | ``` 31 | /* 32 | * Compute the delta between the address I am compiled to run at 33 | * and the address I am actually running at. 34 | */ 35 | load_delta = physaddr - (unsigned long)(_text - __START_KERNEL_map); 36 | RIP_REL_REF(phys_base) = load_delta; 37 | ``` 38 | 39 | 在配置了kaslr时,内核加载地址会和编译时的地址有个偏移。phys_base就记录了这个偏移。(所以看上去这个变量名字好像不是很准确。) 40 | 41 | 在后续fixup页表,以及计算符号物理地址时(__pa_symbol)会用到。 -------------------------------------------------------------------------------- /mm/common/01_important_transform.md: -------------------------------------------------------------------------------- 1 | # 虚拟地址和物理地址 2 | 3 | ## __pa_symbol() 4 | 5 | 获取内核程序中符号的物理地址,也就是nm vmlinux命令能显示出的内容。 6 | 7 | 比如 nm vmlinux | grep -w _text,其结果是“ffffffff81000000 T _text”。 8 | 9 | ``` 10 | #define __START_KERNEL_map _AC(0xffffffff80000000, UL) 11 | 12 | 13 | #define __pa_symbol(x) \ 14 | __phys_addr_symbol(__phys_reloc_hide((unsigned long)(x))) 15 | 16 | 17 | #define __phys_addr_symbol(x) \ 18 | ((unsigned long)(x) - __START_KERNEL_map + phys_base) 19 | ``` 20 | 21 | 这里我们只看x86_64的定义。从定义中我们可以看到,符号的物理地址是虚拟地址 减去 __START_KERNEL_map 再加上 phys_base。 22 | 23 | 当配置了kaslr时,内核加载的物理地址和编译时的加载地址会有个偏移,这个偏移记录在phys_base。具体解释可见[常用全局变量][1] 24 | 25 | 正因为phys_base的偏移是基于__START_KERNEL_map计算出来的,所以如此计算后就得到了内核代码中符号的物理地址。 26 | 27 | ## __pa() 28 | 29 | 计算出虚拟地址对应的物理地址。 30 | 31 | ``` 32 | static __always_inline unsigned long __phys_addr_nodebug(unsigned long x) 33 | { 34 | unsigned long y = x - __START_KERNEL_map; 35 | 36 | /* use the carry flag to determine if x was < __START_KERNEL_map */ 37 | x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET)); 38 | 39 | return x; 40 | } 41 | 42 | #define __phys_addr(x) __phys_addr_nodebug(x) 43 | 44 | #define __pa(x) __phys_addr((unsigned long)(x)) 45 | ``` 46 | 47 | 其实这里计算的时候分了两种情况。 48 | 49 | * 虚拟地址 > __START_KERNEL_map 50 | * 其他虚拟地址 51 | 52 | 第一种情况,说明虚拟地址在内核代码空间,所以实际上就退化成和__pa_symbol()一样。 53 | 第二种情况,转换过程和__va相反。这说明传入的虚拟地址需要是在内核页表上映射的空间内。 54 | 55 | ## __va() / phys_to_virt() 56 | 57 | 只能对有内核映射的地址调用该函数,来获得对应地址的虚拟地址。 58 | 59 | ``` 60 | static inline void *phys_to_virt(phys_addr_t address) 61 | { 62 | return __va(address); 63 | } 64 | 65 | #define __PAGE_OFFSET_BASE_L4 _AC(0xffff888000000000, UL) 66 | unsigned long page_offset_base __ro_after_init = __PAGE_OFFSET_BASE_L4; 67 | 68 | #define __PAGE_OFFSET page_offset_base 69 | #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) 70 | 71 | #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) 72 | ``` 73 | 74 | 再x86_64架构下,大概率是这么个定义。 75 | 76 | 也就是一个物理地址(非内核代码范围内的)的虚拟地址 = 虚拟地址 + 0xffff888000000000。 77 | 78 | 这是因为在映射整个内存空间时,是这么操作的。具体参见[映射完整物理地址][2] 79 | 80 | # pfn和struct page 81 | 82 | 这里先列出只有sparsemem的情况 83 | 84 | ## __pfn_to_page(pfn) 85 | 86 | ``` 87 | static inline struct page *__section_mem_map_addr(struct mem_section *section) 88 | { 89 | unsigned long map = section->section_mem_map; 90 | map &= SECTION_MAP_MASK; 91 | return (struct page *)map; 92 | } 93 | 94 | #define __pfn_to_page(pfn) \ 95 | ({ unsigned long __pfn = (pfn); \ 96 | struct mem_section *__sec = __pfn_to_section(__pfn); \ 97 | __section_mem_map_addr(__sec) + __pfn; \ 98 | }) 99 | ``` 100 | 101 | 先根据pfn找到对应的section,然后对section->section_mem_map做一个偏移运算得到struct page的地址。 102 | 103 | ## __page_to_pfn(pg) 104 | 105 | ``` 106 | static inline unsigned long page_to_section(const struct page *page) 107 | { 108 | return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK; 109 | } 110 | 111 | #define __page_to_pfn(pg) \ 112 | ({ const struct page *__pg = (pg); \ 113 | int __sec = page_to_section(__pg); \ 114 | (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \ 115 | }) 116 | ``` 117 | 118 | 先根据page找到对应的section,注意这里找是因为把sectino number写在了page->flags里。 119 | 然后再对section->section_mem_map做一个偏移得到pfn。 120 | 121 | # 虚拟地址和struct page 122 | 123 | 有了上面两个转换,自然可以推导出这个转换。也就是形成了地址和struct page之间的关系。 124 | 125 | ## virt_to_page() 126 | 127 | ``` 128 | #define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT) 129 | ``` 130 | 131 | 这个转换没有什么太多可以解释的,不过这里有另一个检查的函数值得一看。 132 | 133 | ``` 134 | bool __virt_addr_valid(unsigned long x) 135 | { 136 | unsigned long y = x - __START_KERNEL_map; 137 | 138 | /* use the carry flag to determine if x was < __START_KERNEL_map */ 139 | if (unlikely(x > y)) { 140 | x = y + phys_base; 141 | 142 | if (y >= KERNEL_IMAGE_SIZE) 143 | return false; 144 | } else { 145 | x = y + (__START_KERNEL_map - PAGE_OFFSET); 146 | 147 | /* carry flag will be set if starting x was >= PAGE_OFFSET */ 148 | if ((x > y) || !phys_addr_valid(x)) 149 | return false; 150 | } 151 | 152 | return pfn_valid(x >> PAGE_SHIFT); 153 | } 154 | ``` 155 | 156 | 就是这个用来判断虚拟地址是否有效的函数。可以看出在内核中认为有效的虚拟地址空间有两个: 157 | 158 | * __START_KERNEL_map以上的内核代码空间 159 | * PAGE_OFFSET以上的direct mapping空间 160 | 161 | 其余部分都是无效空间。 162 | 163 | ## page_to_virt() 164 | 165 | ``` 166 | #define page_to_virt(x) __va(PFN_PHYS(page_to_pfn(x))) 167 | ``` 168 | 169 | [1]: /mm/common/00_global_variable.md 170 | [2]: /kernel_pagetable/04-map_whole_memory.md -------------------------------------------------------------------------------- /mm/page_allocator/00_page_allocator.md: -------------------------------------------------------------------------------- 1 | # 页分配器的探究 2 | 3 | 页分配器是一个相对牵着概念较多的层次,也可以说正是这个层次把物理上的内存差异屏蔽,从而向用户呈现了一致的使用接口。 4 | 5 | 第一个让我好奇的是页结构体究竟是存放在哪里的? 6 | 7 | 最原始的版本中页结构体是作为一个大的静态数组存放在内存中的,而随着内存变大,空洞变多,静态数组显然不符合设计理念。之后则提出 8 | SPARSEMEM的概念,按实际情况分配页结构体。 9 | 10 | [寻找页结构体的位置][3] 11 | 12 | 知道页结构体在那里,顺便来瞥一眼结构体的样子。为啥说是瞥一眼呢?因为这个结构体实在是太大(乱)了。为了满足各种需求,这个结构 13 | 中进行了多重复用。先放在这里,作为一个参考文档把。 14 | 15 | [眼花的页结构体][10] 16 | 17 | 所谓的内存物理差异无非就两点: 18 | 19 | * 硬件是否能访问 20 | * 访问速度的差异 21 | 22 | 而这两点对应到软件上的概念是: 23 | 24 | * ZONE 25 | * NUMA NODE 26 | 27 | 那内核中是如何把这两个信息保存起来,并用来指导内存非配的呢? 28 | 29 | 这就需要大名鼎鼎的pg_data_t结构体出场了。所有的页分配工作都是基于这个数据结构的信息所作出的。 30 | 31 | [Node-Zone-Page][5] 32 | 33 | 有了这样的概况之后,我们就可以来看看页是如何初始化和被分配的了。 34 | 35 | [传说的伙伴系统][6] 36 | 37 | 将内存划分为node/zone之后,分配内存时是不是有办法去控制从哪个node哪个zone上去分配呢?答案是有的。 38 | 39 | [GFP的功效][20] 40 | 41 | 为了更好管理内存,内核中会给分配出去的内存做一些标记,这样方便在回收,出错等时候判断内存的用途。 42 | 43 | 为了更好的理解内存管理中的代码流程,我们需要了解[页分配器的用户们][19] 44 | 45 | [1]: /mm/01-e820_retrieve_memory_from_HW.md 46 | [2]: /mm/02-memblock.md 47 | [3]: /mm/03-sparsemem.md 48 | [5]: /mm/05-Node_Zone_Page.md 49 | [6]: /mm/06-page_alloc.md 50 | [7]: /mm/07-per_cpu_pageset.md 51 | [8]: /mm/08-slub_general.md 52 | [9]: /mm/09-slub_in_graph.md 53 | [10]: /mm/10-page_struct.md 54 | [11]: /mm/50-challenge_evolution.md 55 | [18]: https://www.kernel.org/doc/gorman/html/understand/index.html 56 | [19]: /mm/11-users_of_buddy.md 57 | [20]: /mm/12-gfp_usage.md 58 | [21]: /mm/13-physical-layer-partition.md 59 | -------------------------------------------------------------------------------- /mm/page_allocator/01-compound_page.md: -------------------------------------------------------------------------------- 1 | 真实的页分配不仅仅是从free list上取下来就完了,复杂点不在于内存大小的分配,而是对页的分类、统计和管理。 2 | 3 | 在这里我们先看一个概念 compound page。也就是对一个跨越多个页的“组合页”。 4 | 内核对超过一个页面大小的页提供了一个方法PageCompound()来判断一个page是不是组合页的一部分。以下是内核中的注释。 5 | 6 | ```c 7 | /* 8 | * Higher-order pages are called "compound pages". They are structured thusly: 9 | * 10 | * The first PAGE_SIZE page is called the "head page" and have PG_head set. 11 | * 12 | * The remaining PAGE_SIZE pages are called "tail pages". PageTail() is encoded 13 | * in bit 0 of page->compound_head. The rest of bits is pointer to head page. 14 | * 15 | * The first tail page's ->compound_order holds the order of allocation. 16 | * This usage means that zero-order pages may not be compound. 17 | */ 18 | ``` 19 | 20 | 简单得用一个图来描述这么一个概念。 21 | 22 | 23 | ``` 24 | page[0] page[1] page[2] page[3] 25 | +----------------+ +----------------+ +----------------+ +----------------+ 26 | |PG_HEAD | |_flags_1(order) | | | | | 27 | | | | | | | | | 28 | | | | | | | | | 29 | | | |compound_head + | |compound_head + | |compound_head + | 30 | +----------------+ +--------------|-+ +--------------|-+ +--------------|-+ 31 | ^ | | | 32 | | | | | 33 | +------------------------------------+---------------------+---------------------+ 34 | ``` 35 | 36 | # 组合页的判断 37 | 38 | ## 头页面 39 | 40 | PageHead 41 | 42 | ```c 43 | static __always_inline int PageHead(const struct page *page) 44 | { 45 | PF_POISONED_CHECK(page); 46 | return test_bit(PG_head, &page->flags) && !page_is_fake_head(page); 47 | } 48 | ``` 49 | 50 | ## 尾页面 51 | 52 | PageTail 53 | 54 | ```c 55 | static __always_inline int PageTail(const struct page *page) 56 | { 57 | return READ_ONCE(page->compound_head) & 1 || page_is_fake_head(page); 58 | } 59 | ``` 60 | 61 | 虽然叫尾页,但实际上compound page里,除了头页其余的都是尾页,不仅仅是最后一个。 62 | 63 | ## 是组合页 64 | 65 | PageCompound 66 | 67 | ``` 68 | static __always_inline int PageCompound(const struct page *page) 69 | { 70 | return test_bit(PG_head, &page->flags) || 71 | READ_ONCE(page->compound_head) & 1; 72 | } 73 | ``` 74 | 75 | 这是上面两者的结合,是头页或者是尾页就会返回真。 76 | 77 | # 获取头页面 78 | 79 | 因为重要的信息大多存储在头页面,或者是第二个页面。所以一个组合页找到它的头页面是经常需要的工作。 80 | 81 | ``` 82 | static __always_inline unsigned long _compound_head(const struct page *page) 83 | { 84 | unsigned long head = READ_ONCE(page->compound_head); 85 | 86 | if (unlikely(head & 1)) 87 | return head - 1; 88 | return (unsigned long)page_fixed_fake_head(page); 89 | } 90 | 91 | #define compound_head(page) ((typeof(page))_compound_head(page)) 92 | ``` 93 | 94 | 其实就是从page->compound_head中取出来就好了。为什么要这么做,参考set_compound_head()。 95 | 96 | 97 | # 组合页的分配和释放 98 | 99 | 设置组合页和清除组合页的方法为 100 | 101 | * prep_compound_page 102 | * free_tail_pages_check 103 | 104 | ## 分配流程 105 | 106 | 当我们从free list上把page拿下来,交出去前,对compound page有额外的处理。这里我们直接从get_page_from_freelist看一下这个流程。 107 | 108 | ```c 109 | get_page_from_freelist() 110 | page = rmqueue() // 这里从free list上取下了page 111 | prep_new_page(page, order, ...) // 开始准备page 112 | post_alloc_hook(page, order, gfp_flags); 113 | prep_compound_page(page, order); 114 | __SetPageHead(page); // 设置PG_head, page[0] 115 | prep_compound_tail(page, i); // 设置compound_head, 所有tail pages 116 | p->mapping = TAIL_MAPPING 117 | set_compound_head(p, head); 118 | set_page_private(p, 0); 119 | prep_compound_head(page, order) 120 | folio = (struct folio*)page; 121 | folio_set_order(folio, order); 122 | atomic_set(&folio->_large_mapcount, -1); 123 | atomic_set(&folio->_entire_mapcount, -1); 124 | atomic_set(&folio->_nr_pages_mapped, 0); 125 | atomic_set(&folio->_pincount, 0); 126 | INIT_LIST_HEAD(&folio->_deferred_list); 127 | ``` 128 | 129 | 但是出乎我意料的是,只有在分配页面时使用__GFP_COMP时才会设置compound_head。那么这么说有些“组合页”也不能用PageCompound检测出来了。 130 | 131 | ## 释放流程 132 | 133 | 释放过程中对compound page的处理在函数free_pages_prepare(). 134 | 135 | ```c 136 | free_pages_prepare(page, order) 137 | free_tail_page_prepare(page, page + i) 138 | page->mapping = NULL; 139 | clear_compound_head(page) // 清除compound_head 140 | free_page_is_bad(page + i) // 检验page是否有问题 141 | (page + i)->flags &= ~PAGE_FLAGS_CHECK_AT_PREP; 142 | ``` 143 | -------------------------------------------------------------------------------- /mm/slub_allocator/00_slub.md: -------------------------------------------------------------------------------- 1 | 页分配以一个页为最小单位,而在系统运行过程中往往不需要这么大的空间。对于这些内存请求,则有slub完成。 2 | 3 | [slub的理念][8] 4 | 5 | 为了理解,小编还针对各种情况用图来解说,希望能更直观和形象。 6 | 7 | [图解slub][9] -------------------------------------------------------------------------------- /mm/static_pcpu.md: -------------------------------------------------------------------------------- 1 | 内核中通常会定义很多的pcpu变量,这样有几个好处 2 | 3 | * 增加数据访问的并发量 4 | * 减少数据访问的时延 5 | 6 | 从定义上就可以看出pcpu变量就是每个cpu都有某个变量的副本,各自访问各自的。那在实现上是怎么做的呢?我们今天就来看一下。 7 | 8 | # 如何定义 9 | 10 | 我们先来看静态pcpu变量是如何定义的。 11 | 12 | 通常我们定义一个pcpu变量使用这样的语句。 13 | 14 | ``` 15 | DEFINE_PER_CPU(int, numa_node); 16 | ``` 17 | 18 | 这样就定义了一个int类型,名字为numa_node的变量。接下来就深入研究一下。 19 | 20 | ## DEFINE_PER_CPU 21 | 22 | ``` 23 | #define DEFINE_PER_CPU(type, name) \ 24 | DEFINE_PER_CPU_SECTION(type, name, "") 25 | 26 | 27 | #define DEFINE_PER_CPU_SECTION(type, name, sec) \ 28 | __PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \ 29 | __typeof__(type) name 30 | #endif 31 | ``` 32 | 33 | 看最后一行,最终也就是定义了一个type类型,名字是name的变量。感觉和普通的变量没有什么区别。那区别在哪里呢?对了,就在上面那个宏里面。 34 | 35 | ## __PCPU_ATTRS 36 | 37 | ``` 38 | #define __PCPU_ATTRS(sec) \ 39 | __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \ 40 | PER_CPU_ATTRIBUTES 41 | 42 | #define PER_CPU_BASE_SECTION ".data..percpu" 43 | ``` 44 | 45 | 好了,这个比较明确了,就是给定义的变量添加了一个section的修饰符。section是什么概念呢?你可以理解为同一个section的变量会放在同一块存储区。一会儿我们再来看这个概念。 46 | 47 | ## 展开后 48 | 49 | ``` 50 | DEFINE_PER_CPU(int, numa_node); 51 | 52 | = 53 | 54 | __attribute__((section(".data..percpu"))) \ 55 | __typeof__(int) numa_node; 56 | ``` 57 | 58 | 这样看或许能够清楚一些。 59 | 60 | pcpu变量和普通变量定义时的差别在于pcpu变量被安排在了一个指定的section中。 61 | 62 | ## 放在哪 63 | 64 | 已经看到变量定义在某一个section了,但是还是不死心,想要看看究竟是怎么放的。 65 | 66 | 好吧,我就带你来看看。 67 | 68 | 首先在[vmlinux.lds.h][1]中定义了PERCPU_INPUT。 69 | 70 | ``` 71 | #define PERCPU_INPUT(cacheline) \ 72 | VMLINUX_SYMBOL(__per_cpu_start) = .; \ 73 | *(.data..percpu..first) \ 74 | . = ALIGN(PAGE_SIZE); \ 75 | *(.data..percpu..page_aligned) \ 76 | . = ALIGN(cacheline); \ 77 | *(.data..percpu..read_mostly) \ 78 | . = ALIGN(cacheline); \ 79 | *(.data..percpu) \ 80 | *(.data..percpu..shared_aligned) \ 81 | VMLINUX_SYMBOL(__per_cpu_end) = .; 82 | ``` 83 | 84 | 你看,凡是.data..percpu开头的都包含在这个定义内了。 85 | 86 | 在同一个文件中又定义了一个包含这个定义的定义PERCPU_VARRD。 87 | 88 | ``` 89 | #define PERCPU_VADDR(cacheline, vaddr, phdr) \ 90 | VMLINUX_SYMBOL(__per_cpu_load) = .; \ 91 | .data..percpu vaddr : AT(VMLINUX_SYMBOL(__per_cpu_load) \ 92 | - LOAD_OFFSET) { \ 93 | PERCPU_INPUT(cacheline) \ 94 | } phdr \ 95 | . = VMLINUX_SYMBOL(__per_cpu_load) + SIZEOF(.data..percpu); 96 | ``` 97 | 98 | 而这个PERCPU_VADDR定义最终包含在文件[arch/x86/kernel/vmlinux.lds.S][2]中。 99 | 100 | 这就是我们最后链接vmlinux时使用的脚本。 101 | 102 | 再详细就请大家自行看代码~ 103 | 104 | # 如何访问 105 | 106 | 定义看完了,来看看要怎么访问。 107 | 108 | 还是看刚才的numa_node变量,我们要访问某cpu上的变量时通过如下的语句。 109 | 110 | ``` 111 | per_cpu(numa_node, cpu); 112 | ``` 113 | 114 | 好了,那来看看都是些什么吧 115 | 116 | ## per_cpu() 117 | 118 | ``` 119 | #define per_cpu(var, cpu) (*per_cpu_ptr(&(var), cpu)) 120 | ``` 121 | 122 | ``` 123 | #define per_cpu_ptr(ptr, cpu) \ 124 | ({ \ 125 | __verify_pcpu_ptr(ptr); \ 126 | SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \ 127 | }) 128 | ``` 129 | 130 | 卧槽,这么长。不着急,仔细看其实有的不用。比如这个__verif_pcpu_ptr(),一看就是用来检测了,就先跳过吧。关键是这个SHIFT_PERCPU_PTR。 131 | 132 | ## SHIFT_PERCPU_PTR 133 | 134 | ``` 135 | #define SHIFT_PERCPU_PTR(__p, __offset) \ 136 | RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)) 137 | ``` 138 | 139 | 嗯,这玩意也是写了老长了,不过你仔细一看其实就是带有两个参数的一个宏,RELOC_HIDE。分别传入了变量的一个指针,和一个offset。 140 | 141 | 好啦,那就再来看看这个宏。 142 | 143 | ## RELOC_HIDE 144 | 145 | ``` 146 | #define RELOC_HIDE(ptr, off) \ 147 | ({ unsigned long __ptr; \ 148 | __ptr = (unsigned long) (ptr); \ 149 | (typeof(ptr)) (__ptr + (off)); }) 150 | ``` 151 | 152 | 又是一个很长,但是其实很简单的定义。是什么呢? 你看最后一行,其实就是一个指针加上了offset。 153 | 154 | ## 展开后 155 | 156 | ``` 157 | per_cpu(numa_node, cpu) 158 | 159 | = 160 | 161 | *(&numa_node + per_cpu_offset(cpu)) 162 | ``` 163 | 164 | 好了,这样看可能可以清楚一些。 165 | 166 | 但是呢,还是有点不清楚,让我再来把实现的细节讲一下。这样,我估计你就可以彻底理解了。 167 | 168 | # 背后的实现 169 | 170 | 先透露一下,其实pcpu变量的实现就是给每个cpu都分配一块内存,并且记录下每块区域和原生区域之间的offset。所以访问的时候就是直接通过原生变量的地址加上目标cpu变量区域的offset就可以了。 171 | 172 | 貌似有点饶,来看一张图吧。 173 | 174 | ``` 175 | __per_cpu_start +-------------------+ -+- -+- 176 | | | | | 177 | | | | | 178 | | | | | 179 | __per_cpu_end +-------------------+ | | 180 | | | 181 | | | 182 | | | 183 | | 184 | Group0 | 185 | pcpu_base_addr +-------------------+ per_cpu_offset(2) 186 | |cpu0 | | 187 | | | | | 188 | | | | | 189 | +-------------------+ | | 190 | |cpu1 | | | 191 | | | | 192 | | | | per_cpu_offset(3) 193 | +-------------------+ -+- 194 | |cpu2 | | 195 | | | | 196 | | | | 197 | +-------------------+ | 198 | | 199 | | 200 | | 201 | | 202 | Group1 | 203 | +-------------------+ -+- 204 | |cpu3 | 205 | | | 206 | | | 207 | +-------------------+ 208 | |cpu4 | 209 | | | 210 | | | 211 | +-------------------+ -+- 212 | |cpu5 | | 213 | | | pcpu_unit_size 214 | | | | 215 | +-------------------+ -+- 216 | ``` 217 | 218 | 其中__per_cpu_start和__per_cpu_end就是刚才我们看到的那个section的定义。所有pcpu变量都会被保存在这个地址空间内。 219 | 220 | 在系统启动的时候,会给每一个cpu分配各自的pcpu变量内存空间。并计算出每个区域相对于__per_cpu_start的offset。 221 | 222 | 具体的代码在setup_per_cpu_areas()函数中,有兴趣的可以再深入研究。具体实现就不在这里展开了。 223 | -------------------------------------------------------------------------------- /mm/tests/01_functional_test.md: -------------------------------------------------------------------------------- 1 | 在内核中为了保证代码质量,有很多功能测试的测试用例。在改完代码后运行一下,可以初步检查出问题。 2 | 3 | 这里列举几个和mm相关的测试。一般都在tools/testing目录下。 4 | 5 | # 基础数据结构 6 | 7 | 内核中有很多基础数据结构如radix_tree/maple_tree/rbtree等。这些关键的数据结构也有测试用例。 8 | 9 | 他们的测试目录分别是 10 | 11 | * tools/testing/radix-tree 12 | * tools/testing/radix-tree 13 | * tools/testing/rbtree 14 | 15 | 使用方法同上。 16 | 17 | # memblock 18 | 19 | 测试代码在tools/testing/memblock目录下。 20 | 21 | 进入该目录后直接make,生成测试程序,然后直接运行./main。 22 | 23 | 也可以 make NUMA=1,测试有numa的情况。 24 | 25 | # vma 26 | 27 | 测试代码在tools/testing/vma目录下。 28 | 29 | 使用方法同上。 30 | 31 | # selftests 32 | 33 | 测试代码在tools/testing/selftests/mm目录下。 34 | 35 | 进入该目录后直接make,生成测试程序。 36 | 37 | 该目录下有个执行脚本run_vmtests.h,通过-h可以查看如何运行测试程序。 38 | 39 | ``` 40 | $./run_vmtests.sh -h 41 | ``` 42 | 43 | 默认情况下,什么都不加会把所有测试都跑一遍。一般会通过-t指定类型,如mmap就测试和mmap相关的。 44 | 45 | 具体在脚本中是通过"CATEGORY="hugetlb" run_test ./map_hugetlb"这样的命令来最后运行实际的测试程序。 46 | 47 | 在run_test函数中,判断CATEGORY变量里指定的类型是否是-t选项指定的,如果是则真正运行后面的测试程序。真正运行测试程序的地方是 48 | 49 | ``` 50 | if [ "${skip}" != "1" ]; then 51 | ("$@" 2>&1) | tap_prefix 52 | local ret=${PIPESTATUS[0]} 53 | else 54 | local ret=$ksft_skip 55 | fi 56 | ``` 57 | 58 | 所以如果你知道具体是哪个测试用例,直接运行对应的可执行文件就好。如: ./map_fixed_noreplace。 59 | 60 | PS: 如果想增加一个测试用例可以参考文档Documentation/dev-tools/kselftest.rst,以及现有的测试用例migration.c。 61 | -------------------------------------------------------------------------------- /mm/tests/02_performance_test.md: -------------------------------------------------------------------------------- 1 | 社区中有很多改动是和性能相关的,社区中也已经有很多工人的性能测试用例。这里汇总一下。 2 | 3 | # will-it-scale 4 | 5 | 好像目前最火的就是[will-it-scale][1]。 6 | 7 | 我在[vma][2]这一节中简单描述了如何使用它测试mmap的性能差异,后续我再继续完善这个测试用例的使用方法。 8 | 9 | 10 | [1]: https://github.com/antonblanchard/will-it-scale 11 | [2]: /virtual_mm/05-vma.md 12 | -------------------------------------------------------------------------------- /mm_reclaim/00-index.md: -------------------------------------------------------------------------------- 1 | 本来以为对内核中内存的理解划分成两部分就算完结了 2 | 3 | * [自底而上话内存][1] 4 | * [虚拟内存空间][2] 5 | 6 | 随着学习的深入发现还要再划出一个分类 -- 回收再利用。毕竟系统中的内存是有限的,如何能够在有限的资源下服务好更多的进程就需要有**螺蛳壳里做道场**的精神。 7 | 8 | linux内核的设计理念是尽量多使用内存,这样可以最大化系统性能。那什么时候通过什么手段开始回收内存的操作呢? 9 | 10 | 其判断标准就是[水线][4]也就是当空闲内存少于某个范围,那就开始回收部分内存,直到系统觉得内存足够应对用户的需求。 11 | 12 | 内核有两种触发内存回收的路径: 13 | 14 | * 主动:同步 15 | * 被动:异步 16 | 17 | 相当于不仅有人定期巡视,也有热线电话随叫随到。这部分牵扯到的调用路径比较多,我们可以用一张[Big Picture][8]来帮助我们理解之间的关系。并且为了更直观理解内存回收的过程,我们做个实验[手动触发回收][10] 18 | 19 | 真正到做回收的时候,我们就要考虑回收谁,留下谁。如何选取是一门很大的学问,目的是为了能够尽量减少回收对系统性能的影响。这里用到的就是[Page Frame Reclaim Algorithm][9]。 20 | 21 | 刚才我们看到,回收时对内存分成了**匿名页**和**文件页**。对于文件读取的页面,回收时可以放回文件中。而匿名页要释放再利用,就要靠[swapfile原理使用和演进][3] 22 | 23 | 内存回收这部分还是相对比较复杂,也有很多细节我不一定全部覆盖。下面是我之前搜到的一些参考文章,可以交叉学习。 24 | 25 | [Linux中的内存回收1][5] 26 | 27 | [Linux中的内存回收2][6] 28 | 29 | [Linux中的内存调节之watermark][7] 30 | 31 | [1]: /mm/00-memory_a_bottom_up_view.md 32 | [2]: /virtual_mm/00-index.md 33 | [3]: /mm_reclaim/01-swapfile.md 34 | [4]: /mm_reclaim/02-watermark.md 35 | [5]: https://zhuanlan.zhihu.com/p/70964195 36 | [6]: https://zhuanlan.zhihu.com/p/72998605 37 | [7]: https://zhuanlan.zhihu.com/p/73539328 38 | [8]: /mm_reclaim/03-big_picture.md 39 | [9]: /mm_reclaim/04-pfra.md 40 | [10]: /mm_reclaim/05-do_reclaim.md 41 | -------------------------------------------------------------------------------- /mm_reclaim/03-big_picture.md: -------------------------------------------------------------------------------- 1 | 2 | 内存回收有那么点儿复杂,也涉及到了系统中很多地方。为了对这些做一个梳理,我整理了回收的大图。这样可以对全局有个了解。 3 | 4 | 下图中左边部分就是所谓的直接回收(direct reclaim), 右边部分就是kswapd了。 5 | 6 | ![vmscan](/mm_reclaim/vmscan.png) 7 | -------------------------------------------------------------------------------- /mm_reclaim/04-pfra.md: -------------------------------------------------------------------------------- 1 | 内存回收的算法,在Gorman的巨著[Understanding the Linux Virtual Memory Manager][1]中有详细的介绍。 2 | 3 | [Page Frame Reclaim Algorithm][2] 4 | 5 | 虽然这部分已经是古董级的材料了,但是作为原理还是很值得研究的。 6 | 7 | # LRU定义 8 | 9 | 回收策略通常被称为Least Recently Used (LRU)。在内核中,对应的数据结构是lruvec。 10 | 11 | ``` 12 | lruvec 13 | +-------------------------------+ 14 | |lists[NR_LRU_LISTS] | 15 | | (struct list_head) | 16 | |lru_lock | 17 | | (spinlock_t) | 18 | |anon_cost | 19 | |file_cost | 20 | | (unsigned long) | 21 | |nonresident_age | 22 | | (atomic_long_t) | 23 | |flags | 24 | | (unsigned long) | 25 | |refaults[ANON_AND_FILE] | 26 | | (unsigned long) | 27 | |pgdat | 28 | | (struct pglist_data*) | 29 | +-------------------------------+ 30 | ``` 31 | 32 | 其中lists代表的就是大名鼎鼎的lru lists。这个上面一共有五个链表: 33 | 34 | * LRU_INACTIVE_ANON 35 | * LRU_ACTIVE_ANON 36 | * LRU_INACTIVE_FILE 37 | * LRU_ACTIVE_FILE 38 | * LRU_UNEVICTABLE, 39 | 40 | 简单来说,回收的过程就是从lru lists上找到合适的page做回收。 41 | 42 | #把页放到lru上 43 | 44 | lru是这样一个数据结构,就好像一个收纳箱。我们把使用的页放在里面,当这个箱子塞满的时候,我们就要清理这个箱子。为了能够更好的清理,我们按照了一定算法在这个箱子里摆放页。这个工作在内核中就是PFRA算法了。 45 | 46 | 为了更好的理解这个算法,我们可以将这个过程进一步拆解为: 47 | 48 | * 将页存放进箱子和箱子内腾挪的步骤 49 | * 腾挪操作的算法原理 50 | 51 | 第一步完全是为了更好理解内核代码做的工程化拆解,也是本小节的主要内容。 52 | 53 | ## pagevec 54 | 55 | 半路杀出个程咬金,lruvec的怎么又出来了个pagevec?怎么讲呢,内核为了减少锁竞争,在把页放入lruvec前,先放到percpu的pagevec上。相当于做了一个软cache。 56 | 57 | 我们先来看看内核中有多少pagevec。 58 | 59 | ``` 60 | lru_pvecs lru_rotate 61 | +-------------------------------+ +-------------------------------+ 62 | |lock | |lock | 63 | | (local_lock_t) | | (local_lock_t) | 64 | |lru_add | |pvec | 65 | |lru_deactivate_file | | (struct pagevec) | 66 | |lru_deactivate | +-------------------------------+ 67 | |lru_lazyfree | 68 | |activate_page | 69 | | (struct pagevec) | 70 | | +--------------------------+ mlock_pvec(struct pagevec) 71 | | |pages[PAGEVEC_SIZE] | +-------------------------------+ 72 | | | (struct page*) | | | 73 | | |nr | +-------------------------------+ 74 | | | (unsigned char) | 75 | | |percpu_pvec_drained | 76 | | | (bool) | 77 | | | | 78 | +----+--------------------------+ 79 | ``` 80 | 81 | 考虑到内核中还有别的子系统使用pagevec,这里只列出和lru相关的。所以这么数来,一共有七个相关的pagevec。而对于每一个pagevec,内核中都有对应的函数处理。咱们先把相关的函数展示出来。 82 | 83 | ``` 84 | folio_rotate_reclaimable 85 | lru_rotate.pvec 86 | | 87 | 88 | | folio_activate deactivate_page 89 | lru_pvecs.activate_page lru_pvecs.lru_deactivate 90 | | / / 91 | / / 92 | folio_add_lru | deactivate_file_folio mark_page_lazyfree 93 | lru_pvecs.lru_add lru_pvecs.lru_deactivate_file lru_pvecs.lru_lazyfree 94 | \ | / / / 95 | \ | / / / 96 | v v v v v 97 | pagevec_add_and_need_flush 98 | / \ 99 | / \ 100 | __pagevec_lru_add pagevec_lru_move_fn 101 | 102 | 103 | 104 | mlock_page_drain mlock_folio mlock_new_page munlock_page 105 | \ \ / / 106 | \ \ / / 107 | \ \ / / 108 | mlock_pagevec 109 | / \ 110 | / \ 111 | __mlock_page __mlock_new_page __munlock_page 112 | ``` 113 | 114 | 本来我想把这两个合一块的,社区没同意。也好,那就分开看看。 115 | 116 | 先解释一下上面的图: 117 | 118 | * mlock_pvec 比较独立。添加到mlock_pvec后,由mlock_pagevec加到lru上 119 | * 其余的pagevec都通过pagevec_add_and_need_flush检查后,做相应的操作 120 | * folio_add_lru/mlock_new_page 是两个加入到pagevec的入口函数 121 | 122 | 123 | [1]: https://www.kernel.org/doc/gorman/html/understand/ 124 | [2]: https://www.kernel.org/doc/gorman/html/understand/understand013.html 125 | -------------------------------------------------------------------------------- /mm_reclaim/05-do_reclaim.md: -------------------------------------------------------------------------------- 1 | 纸上得来终觉浅,绝知此事要躬行 2 | 3 | 看了这么多文章,哪怕是直接看了代码,我以为我懂了,但是真动起手来还是发现有很多不知道,或者是一知半解。 4 | 5 | 好了,那咱就想办法给系统点压力,让他做个内存回收瞧瞧。 6 | 7 | # 触发回收 8 | 9 | 既然大家都说内存回收在低水线触发,在高水线停止,那么我们想办法让系统可用内存达到低水位就可以了。为了更快达到低水位,我们可以抬高水位,让这个过程更容易达到。 10 | 11 | 首先我们看一下当前的水线情况。 12 | 13 | ``` 14 | $ free -h 15 | total used free shared buff/cache available 16 | Mem: 3.8Gi 116Mi 3.5Gi 0.0Ki 244Mi 3.7Gi 17 | $ cat /proc/sys/vm/min_free_kbytes 18 | 8000 19 | $ grep -E "zone |min|low |high |managed|pages free" /proc/zoninfo 20 | Node 0, zone DMA 21 | pages free 3840 22 | min 7 23 | low 10 24 | high 13 25 | managed 3840 26 | Node 0, zone DMA32 27 | pages free 758226 28 | min 1505 29 | low 2257 30 | high 3009 31 | managed 758592 32 | Node 0, zone Normal 33 | pages free 145291 34 | min 487 35 | low 730 36 | high 973 37 | managed 243597 38 | Node 0, zone Movable 39 | pages free 0 40 | min 0 41 | low 0 42 | high 0 43 | managed 0 44 | ``` 45 | 46 | 约4G的内存,留了8k作为低水位。每个zone可用内存距离低水位还都有些距离。那怎么抬高水位呢?还记得水线计算的公式么? 47 | 48 | ``` 49 | zone的低水线 = (当前zone的内存 * pages_min) / total 50 | ``` 51 | 52 | 其中zone的内存和总内存没法改变,所以只能将pages_min提高才能抬高水位。看着当前还有3.5G的空闲内存,那咱就把这最少空闲内存提高到3G吧。 53 | 54 | ``` 55 | echo 3000000 > /proc/sys/vm/min_free_kbytes 56 | $ grep -E "zone |min|low |high |managed|pages free" /proc/zoneinfo 57 | Node 0, zone DMA 58 | pages free 3840 59 | min 2862 60 | low 3577 61 | high 4292 62 | managed 3840 63 | Node 0, zone DMA32 64 | pages free 757824 65 | min 565534 66 | low 706917 67 | high 848300 68 | managed 758592 69 | Node 0, zone Normal 70 | pages free 145221 71 | min 181602 72 | low 227002 73 | high 272402 74 | managed 243597 75 | Node 0, zone Movable 76 | pages free 0 77 | min 0 78 | low 0 79 | high 0 80 | managed 0 81 | ``` 82 | 83 | 怎么样,一下子水位就提高了吧。空闲内存和低水位之间的距离明显缩小。 84 | 而且有意思的是Normal Zone的空闲内存已经低于最低水位了,但是这个时候却没有发生回收。你说为啥呢?这是因为在判断节点内存是否需要回收,是按照整个节点纬度判断的。可以看到DMA的zone仍然有750M空闲内存。 85 | 86 | 另外发现DMA这个Zone的内存实在太小了,从回收的角度很容易达到低水位导致认为整个节点内存已经平衡。所以建议没有特殊需要编译时把ZONE_DMA这个选贤关掉。 87 | 88 | ``` 89 | # memhog 10M 90 | . 91 | # grep pageout /proc/vmstat 92 | pageoutrun 0 93 | ``` 94 | 95 | 96 | ``` 97 | memhog 300M 98 | ``` 99 | 100 | # 谁唤醒了回收 101 | 102 | kswapd作为一个内核线程,没事儿的时候他老人家是在那里睡大觉的。只有在需要的时候,才会被唤醒起来干活。 103 | 那究竟是谁在什么情况下会集齐龙珠,召唤神龙呢?我们可以看到有一个函数叫wakeup_kswapd,这个是专门用来唤醒kswapd的。 104 | 但是这个函数有两个地方被调用,为了确认唤醒kswapd的源头,小编在函数里加了dump_stack()。看看究竟是什么情况会唤醒kswapd老人家。 105 | 106 | ``` 107 | [ 27.185421] Call Trace: 108 | [ 27.185750] 109 | [ 27.186010] dump_stack_lvl+0x33/0x42 110 | [ 27.186453] wakeup_kswapd.cold.87+0x5/0x35 111 | [ 27.186952] wake_all_kswapds+0x53/0xb0 112 | [ 27.187413] __alloc_pages_slowpath.constprop.136+0xa3f/0xc40 113 | [ 27.188120] ? get_page_from_freelist+0xe3/0xc60 114 | [ 27.188673] __alloc_pages+0x30c/0x320 115 | [ 27.189124] alloc_pages_vma+0x71/0x180 116 | [ 27.189610] __handle_mm_fault+0x315/0xb60 117 | [ 27.190103] handle_mm_fault+0xc0/0x290 118 | [ 27.190700] do_user_addr_fault+0x1d7/0x650 119 | [ 27.191375] exc_page_fault+0x4b/0x110 120 | [ 27.191886] ? asm_exc_page_fault+0x8/0x30 121 | [ 27.192447] asm_exc_page_fault+0x1e/0x30 122 | ``` 123 | 124 | 从上面的Trace中可以看到,这是一路从缺页中断过来的。因为内存分配失败,然后唤醒了kswapd。 125 | 126 | ## 龙珠 127 | 128 | 刚才说了,要召唤神龙需要集齐龙珠。在召唤kswapd的过程中也需要符合多个条件。其中一条是ALLOC_KSWAPD。 129 | 130 | ``` 131 | if (alloc_flags & ALLOC_KSWAPD) 132 | wake_all_kswapds(order, gfp_mask, ac); 133 | ``` 134 | 135 | 讲真,这个ALLOC_KSWAPD的标志真的让我一顿好找。总结下来,确认分配时需不需要这个标志由以下一个原因判断: 136 | 137 | * alloc_flags从gfp_flags决定,gfp_to_alloc_flags()函数负责这个翻译 138 | * __GFP_KSWAPD_RECLAIM和ALLOC_KSWAPD的定义是一样的 139 | * gfp_flags定义中更有多个flag带了__GFP_KSWAPD_RECLAIM,其中基本的两个是__GFP_KSWAPD_RECLAIM和__GFP_RECLAIM 140 | * 对于匿名页,__alloc_pages的gfp是GFP_HIGHUSER_MOVABLE, 这个定义的展开包含了__GFP_RECLAIM 141 | 142 | 所以对于普通的用户内存申请都符合这个条件。 143 | 144 | # 直接回收在哪? 145 | 146 | 在[Big Picture][1]中我们看到,回收分成两种:直接回收和间接回收。上面我们看到的是间接回收,那直接回收在什么时候会发生呢? 147 | 148 | 仔细看下来,又一个重要的GFP标志:__GFP_DIRECT_RECLAIM。只有当设置了这个标志时,才会执行直接回收。 149 | 150 | 对这个标志的检测发生在两个地方: 151 | 152 | * __alloc_pages_slowpath -> can_direct_reclaim 153 | * node_reclaim() -> gfpflags_allow_blocking() 154 | 155 | [8]: /mm_reclaim/03-big_picture.md 156 | -------------------------------------------------------------------------------- /mm_reclaim/vm_watermark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/mm_reclaim/vm_watermark.jpg -------------------------------------------------------------------------------- /mm_reclaim/vmscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/mm_reclaim/vmscan.png -------------------------------------------------------------------------------- /nvdimm/00-brief_navigation.md: -------------------------------------------------------------------------------- 1 | 最近研究nvdimm,发现这玩意还真有点复杂。 2 | 3 | 简单记录之。 4 | 5 | # 使用手册 6 | 7 | 写了一半发现得先写个使用手册,这样一来自己做个记录,二来也清楚正常使用流程,三来部分内容可以帮助解释代码。 8 | 9 | 所以加一个[使用手册][5] 10 | 11 | # 上帝视角 12 | 13 | 经过了设备模型的洗礼,那就先从总线,驱动和设备角度看看都有些什么。 14 | 15 | [上帝视角][1] 16 | 17 | # nvdimm_bus 18 | 19 | 首先创建的是nvdimm_bus设备,而且从树形结构中可以看到它是nvdimm设备树的根。 20 | 21 | [nvdimm_bus][2] 22 | 23 | # nvdimm 24 | 25 | 在整个设备树中,有一个孤零零的存在:nvdimm。这就是是用来表示物理dimm设备的。 26 | 27 | [nvdimm][3] 28 | 29 | # nd_region 30 | 31 | 接着我们就来看nvdimm_bus下,另一个子树。而这颗子树的根就是nd_region了。 32 | 33 | [nd_region][4] 34 | 35 | 而在nd_region下,有四个并列的设备: 36 | 37 | * [nd_namespace_X][6] 38 | * [nd_btt][7] 39 | * [nd_pfn][9] 40 | * [nd_dax][8] 41 | 42 | # dev_dax 43 | 44 | 在着重描述了namespace和nd_dax后,终于要到整个驱动的最后也就是[dev_dax][10]。 45 | 46 | 这是用户使用nd_dax设备的接口。 47 | 48 | [1]: /nvdimm/01-a_big_picture.md 49 | [2]: /nvdimm/02-nvdimm_bus.md 50 | [3]: /nvdimm/03-nvdimm.md 51 | [4]: /nvdimm/04-nd_region.md 52 | [5]: /nvdimm/00-brief_user_guide.md 53 | [6]: /nvdimm/05-namespace.md 54 | [7]: /nvdimm/06-btt.md 55 | [8]: /nvdimm/07-dax.md 56 | [9]: /nvdimm/08-pfn.md 57 | [10]: /nvdimm/09-dev_dax.md 58 | -------------------------------------------------------------------------------- /nvdimm/00-brief_user_guide.md: -------------------------------------------------------------------------------- 1 | 简单描述一下用户使用nvdimm设备前需要做的配置,主要分成两个方面 2 | 3 | * Dimm 4 | * Region 5 | * Namespace 6 | 7 | 这其实也就是对应了nvdimm上的三个概念,让我们一个个来熟悉。 8 | 9 | # Dimm 10 | 11 | Dimm就是真实的硬件,长得和内存差不多。这个的配置不由软件决定,而是由硬件系统插线决定的。 12 | 13 | 不过我们可以通过软件查看硬件的情况。 14 | 15 | ``` 16 | # ixpdimm-cli show -topology 17 | MemoryType Capacity DimmID PhysicalID DeviceLocator 18 | ... 19 | AEP DIMM 125.6 GiB 0x0000 26 CPU1_DIMM_A1 20 | AEP DIMM 125.6 GiB 0x0100 37 CPU1_DIMM_D1 21 | ... 22 | ``` 23 | 24 | 这显示了现在有两个dimm是nvdimm,以及容量和设备号等情况。 25 | 26 | 嗯,暂时就知道这些,等学习了再回来补充。 27 | 28 | # Region 29 | 30 | 在使用设备前,我们需要先创建Region。这玩意有点高级。 31 | 32 | ## 查看Region 33 | 34 | ``` 35 | # ndctl list -R 36 | { 37 | "dev":"region0", 38 | "size":268435456000, 39 | "available_size":0, 40 | "type":"pmem", 41 | "numa_node":0, 42 | "iset_id":-600138270574898608, 43 | "persistence_domain":"unknown" 44 | } 45 | ``` 46 | 47 | 这里显示当前只有一个Region。 48 | 49 | ## 创建Region 50 | 51 | 创建有点讨厌,用命令行也行,但是好像会卡住。暂时只好在EFI中操作。不过命令行格式差不多。 52 | 53 | 在EFI的shell中输入,就可以创建一个只有PersistentMemoryType的Region了。 54 | 55 | ``` 56 | Shell> ApachePassCli.efi create –goal PersistentMemoryType=AppDirect 57 | ``` 58 | 59 | 这个命令的完整格式是: 60 | 61 | ``` 62 | [MemoryMode = (0|%)] [PersistentMemoryType = (AppDirect|AppDirectNotInterleaved)] [Reserved = (0|%)] [Config = (MM|AD|MM+AD)] 63 | ``` 64 | 65 | 从这个命令中可以看出,Region可以有两种模式: Memory 和 PersistentMemory。 66 | 67 | 补充一下在linux系统中划分region的命令: 68 | 69 | ``` 70 | ixpdimm-cli create -goal PersistentMemoryType=AppDirect 71 | ``` 72 | 73 | # Namespace 74 | 75 | 有了Region后,可以在Region中再建立Namespace。建立了Namespace之后,就可以使用nvdimm了。 76 | 77 | ## 查看Namespace 78 | 79 | ``` 80 | # ndctl list 81 | { 82 | "dev":"namespace0.0", 83 | "mode":"fsdax", 84 | "size":264239054848, 85 | "uuid":"4c268f38-ee5d-4a3b-af4c-8285e08a4547", 86 | "raw_uuid":"e7d5888c-c1d2-405b-b304-6ad9d7b6d18a", 87 | "sector_size":512, 88 | "blockdev":"pmem0", 89 | "numa_node":0 90 | } 91 | ``` 92 | 93 | 94 | ## 创建Namespace 95 | 96 | ``` 97 | # ndctl create-namespace -f -e namespace0.0 -m devdax 98 | { 99 | "dev":"namespace0.0", 100 | "mode":"devdax", 101 | "size":"246.09 GiB (264.24 GB)", 102 | "uuid":"7a928d1b-c74e-4028-987f-f7ef3eb1a9df", 103 | "raw_uuid":"aeeb2f3e-3a28-4b8f-bc4b-0106fe9f770d", 104 | "daxregion":{ 105 | "id":0, 106 | "size":"246.09 GiB (264.24 GB)", 107 | "align":2097152, 108 | "devices":[ 109 | { 110 | "chardev":"dax0.0", 111 | "size":"246.09 GiB (264.24 GB)" 112 | } 113 | ] 114 | }, 115 | "numa_node":0 116 | } 117 | ``` 118 | 119 | ## 销毁namespace 120 | 121 | ``` 122 | ndctl delete-namespace -f namespace0.0 123 | ``` 124 | -------------------------------------------------------------------------------- /nvdimm/02-nvdimm_bus.md: -------------------------------------------------------------------------------- 1 | 说实话,对于nvdimm_bus中的成员含义还知之甚少。不过暂时还不影响对这个结构体生成过程的理解。 2 | 3 | # 函数原型揭示的秘密 4 | 5 | 首先我们看一下创建nvdimm_bus这个结构调用的函数,看看它能带给我们什么信息: 6 | 7 | ``` 8 | struct nvdimm_bus *nvdimm_bus_register(struct device *parent, 9 | struct nvdimm_bus_descriptor *nd_desc) 10 | ``` 11 | 12 | 从总可以看到,生成nvdimm_bus结构体需要两个成员: 13 | 14 | * parent: 父节点 15 | * nd_desc:描述信息 16 | 17 | 父节点含义很明确,那这个描述信息是什么,在哪里呢? 18 | 19 | # 数据结构的关联 20 | 21 | 从数据结构的角度上看,刚才的问题就可以从acpi_nfit_desc结构体中找到: 22 | 23 | ``` 24 | acpi_nfit_desc 25 | +-----------------------------------------------+ 26 | |dev | = point to an acpi_device 27 | | (struct device*) | 28 | +-----------------------------------------------+ 29 | |acpi_header | 30 | | (struct acpi_table_header) | 31 | +-----------------------------------------------+ 32 | |nd_desc | * <-----------------------------------+ 33 | | (struct nvdimm_bus_descriptor) | | 34 | | +------------------------------------------+ | 35 | | |attr_groups | = acpi_nfit_attribute_groups | 36 | | | (struct attribute_group**) | | 37 | | |provider_name | = "ACPI.NFIT" | 38 | | | (char *) | | 39 | | | | | 40 | | +------------------------------------------+ | 41 | | |ndctl | = acpi_nfit_ctl | 42 | | | (ndctl_fn) | | 43 | | |flush_probe | = acpi_nfit_flush_probe | 44 | | |clear_to_send | = acpi_nfit_clear_to_send | 45 | | | | | 46 | | +------------------------------------------+ | 47 | |nvdimm_bus | * | 48 | | (struct nvdimm_bus*) | | 49 | | +------------------------------------------+ | 50 | | |nd_desc | point to the above nd_desc -----------+ 51 | | | (struct nvdimm_bus_descriptor*) | 52 | | |dev | 53 | | | (struct device) | 54 | | | +-----------------------------------+ 55 | | | |name | "ndbus%d" 56 | | | |groups | = nd_desc->attr_groups 57 | | | |release | = nvdimm_bus_release 58 | | | | | 59 | | | +-----------------------------------+ 60 | | |list | 61 | | |mapping_list | 62 | | | (struct list_head) | 63 | | | | 64 | +----+------------------------------------------+ 65 | ``` 66 | 67 | 可以看出,用来创建nvdimm_bus的描述信息nd_desc是在acpi_nfit_desc中的。这么看是不是感觉清楚了一点点? 68 | 69 | # nd_bus_driver -> /dev/ndctl0 70 | 71 | nvdimm_bus设备生成之后接下去的事情就交给了对应的驱动 nd_bus_driver。这个驱动超级简单,但是作用倒是有点。 72 | 73 | ``` 74 | int nvdimm_bus_create_ndctl(struct nvdimm_bus *nvdimm_bus) 75 | { 76 | dev_t devt = MKDEV(nvdimm_bus_major, nvdimm_bus->id); 77 | struct device *dev; 78 | 79 | dev = device_create(nd_class, &nvdimm_bus->dev, devt, nvdimm_bus, 80 | "ndctl%d", nvdimm_bus->id); 81 | 82 | if (IS_ERR(dev)) 83 | dev_dbg(&nvdimm_bus->dev, "failed to register ndctl%d: %ld\n", 84 | nvdimm_bus->id, PTR_ERR(dev)); 85 | return PTR_ERR_OR_ZERO(dev); 86 | } 87 | ``` 88 | 89 | 可以看到主要的工作就是创建了/dev/ndctl%d设备。这下终于知道这个设备文件是如何来的了。 90 | -------------------------------------------------------------------------------- /nvdimm/03-nvdimm.md: -------------------------------------------------------------------------------- 1 | 创建了nvdimm_bus,读取了nfit中的信息后,接下来驱动就根据这些信息创建nvdimm这个数据结构了。 2 | 3 | 这个数据结构就对应着一个物理的dimm,这可能也是为什么使用memmap=内核参数时没有这个设备的原因。 4 | 5 | # 构造nvdimm的函数 6 | 7 | 这个过程由acpi_nfit_register_dimms()函数完成,大致的流程如下: 8 | 9 | ``` 10 | list_for_each_entry(nfit_mem, &acpi_desc->dimms, list) { 11 | __nvdimm_create(); 12 | } 13 | ``` 14 | 15 | 从这个片段能看出nvdimm结构体是从acpi_desc->dimms的信息构建的。 16 | 17 | # 相应的数据结构 18 | 19 | 看了函数调用,现在来看看对应的数据结构的样子。虽然看了有时候也还是不懂。 20 | 21 | 22 | ``` 23 | acpi_nfit_desc 24 | +-----------------------------------------------+ 25 | |dimms | a list of nfit_mem 26 | | (struct list_head) | parsed from above spa & memdevs 27 | | +------------------------------------------+ 28 | | |nvdimm | * 29 | | | (struct nvdimm*) | 30 | | | +------------------------------------+ 31 | | | |dev | 32 | | | | (struct device) | 33 | | | | +--------------------------------+ 34 | | | | |name | = "nmem%d" 35 | | | | |groups | = acpi_nfit_dimm_attribute_groups 36 | | | | | | 37 | | | | |driver_data | = struct nvdimm_drvdata 38 | | | | | (void*) | 39 | | | | | +-----------------------------+ 40 | | | | | |dev | 41 | | | | | | (struct device*) | 42 | | | | | |ns_current, ns_next | 43 | | | | | |nslabel_size | = 128 44 | | | | | | (int) | 45 | | | | | |nsarea | 46 | | | | | | (nd_cmd_get_config_size)| 47 | | | | | |data | config data 48 | | | | | | (void*) | namespace index + namespace label 49 | | | | | |dpa | created from namespace label 50 | | | | | | (struct resource) | from nvdimm_drvdata->data 51 | | | | | +-----------------------------+ 52 | | | | | | 53 | | | | +--------------------------------+ 54 | | | |provider_data | = nfit_mem 55 | | | | (void*) | 56 | | | |flush_wpq | 57 | | | | (struct resource*) | 58 | | | |dwork | 59 | | | | (struct delayed_work) | 60 | | | |sec.ops | 61 | | | | | 62 | | | | | 63 | | +-----+------------------------------------+ 64 | | |memdev_dcr | 65 | | |memdev_pmem | 66 | | |memdev_bdw | 67 | | | (struct acpi_nfit_memory_map) | 68 | | | | 69 | | +------------------------------------------+ 70 | | | 71 | +-----------------------------------------------+ 72 | ``` 73 | 74 | 在这个数据结构的部分截取中可以看到,acpi_nfit_desc中包含了一个类型为nfit_mem的链表。而函数acpi_nfit_register_dimms就是根据这个链表来构造硬件相对无关的nvdimm数据。 75 | 76 | # nvdimm_driver 77 | 78 | nvdimm对应的是一个物理的dimm,这个设备生成之后就需要去检查并初始化。这个工作由它的驱动nvdimm_driver来完成。 79 | 80 | 驱动做的关键工作就是根据硬件信息设置这个数据结构中的dev->driver_data。 81 | 82 | 这个结构是一个nvdimm_drvdata类型,其具体的成员也已经在图中显示。主要解释其中两个内容: 83 | 84 | * data: 驱动将硬件上的namespace index和namespace label_size读取到这段内存 85 | * dpa: 驱动根据上面的data信息,将dimm对应的dpa信息转换成res树 86 | 87 | 我猜这些信息会留着后续使用,那就等着看吧。 88 | -------------------------------------------------------------------------------- /nvdimm/06-btt.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/nvdimm/06-btt.md -------------------------------------------------------------------------------- /nvdimm/07-dax.md: -------------------------------------------------------------------------------- 1 | nd_dax设备可以在两个地方创建: 2 | 3 | * nd_region_probe 4 | * nd_pmem_probe 5 | 6 | 经过测试,在nd_pmem_probe中通过nd_dax_probe()创建的设备才是有真实信息的。而具maintainer说,nd_region_probe中生成的设备是一个用来配置的接口。这个暂时还没有去研究,这里我们只看nd_pmem_probe中通过nd_dax_probe创建的流程。 7 | 8 | # 构造nd_dax的函数 9 | 10 | 这里我们只看通过nd_dax_probe函数创建的流程。 11 | 12 | ``` 13 | nd_dax_probe() 14 | nd_dax_alloc() 15 | nd_pfn_devinit() 16 | nd_pfn_validate() 17 | __nd_device_register() 18 | ``` 19 | 20 | 主要就是做了点分配、初始化、检测的任务。 21 | 22 | 其中想要着重突出的是,如果nd_dax_probe()返回0,也就是一切正常的话。那么它的父函数就会报错,而导致驱动对namespace的probe失败。 23 | 24 | 这也是为什么我们在sysfs上看不到在这种情况下namespace的驱动的原因。 25 | 26 | # nd_dax 27 | 28 | ``` 29 | nd_dax 30 | +-------------------------------------------------+ 31 | |nd_pfn | 32 | | (struct nd_pfn) | 33 | | +-------------------------------------------+ 34 | | |ndns | points to the ns 35 | | | (struct nd_namespace_common*) | 36 | | |dev | 37 | | | (struct device) | 38 | | | +--------------------------------------+ 39 | | | |name | "dax0.0" 40 | | | |groups | nd_dax_attribute_groups 41 | | | |type | nd_dax_device_type 42 | | | | | 43 | | | |driver_data | = dax_region 44 | | | | | 45 | | | +--------------------------------------+ 46 | | |id | 47 | | | (int) | 48 | | |uuid | 49 | | | (u8*) | 50 | | |mode | PFN_MODE_NONE 51 | | | (enum nd_pfn_mode) | 52 | | |align | HPAGE_PMD_SIZE / PAGE_SIZE 53 | | |npfns | tricky? 54 | | |pfn_sb | setup in nd_pfn_init 55 | | | (struct nd_pfn_sb*) | 56 | | | +--------------------------------------+ 57 | | | |signature[PFN_SIG_LEN] | 58 | | | |uuid[16] | = nd_pfn->uuid 59 | | | |parent_uuid[16] | 60 | | | | (u8) | 61 | | | |mode | = nd_pfn->mode 62 | | | |align | = nd_pfn->align 63 | | | | (__le32) | 64 | | | | | 65 | | | |start_pad | trim_pfn_device() 66 | | | |end_trunc | 67 | | | | (__le32) | 68 | | | |dataoff | meta data 69 | | | |npfns | number of pfn 70 | | | | (__le64) | 71 | | | | | 72 | +-----+----+--------------------------------------+ 73 | ``` 74 | 75 | 东西有点长,重要的成员加了备注解释其作用。然后再着重解释两个东西: 76 | 77 | * pfn_sb: pfn superblock 78 | * pfn_sb: start_pad, end_trunc, dataoff 79 | 80 | pfn_sb这个结构的内容将要写回到硬件,作为硬件的配置。这样就说通了作为硬件是如何知道某个内存地址的访问是在硬件的什么位置。 81 | 82 | 然后还有这么几个长得很奇怪的大兄弟是干什么的呢?且看下面这张图。 83 | 84 | ``` 85 | |< start_pad >|< dataoff >| |< end_trunc >| 86 | v v 8k |page |dax_res v v v 87 | |--------------------------------------------------------------| 88 | ^ ^ 89 | | | 90 | nsio->res.start nsio->res.end 91 | ``` 92 | 93 | 这个nsio就是nd_pfn->ndns指向的nd_namespace_io。而这个的res也就是当前namespace所映射的空间范围。 94 | 95 | * start_pad, end_trunc就是为了对其128M,并不和邻居冲突做的调整 96 | * dataoff是一段用来存放page结构体和其他buffer的空间 97 | 98 | 好了,这样是不是觉得清楚点了? 99 | 100 | # dax_pmem_driver 101 | 102 | 有了新的设备nd_dax,那么就有对应的驱动来干活。而它的驱动就是dax_pmem_driver。 103 | 104 | 主要做了这么几件事情: 105 | 106 | * 创建并设置了pfn_sb,并写入硬件 107 | * 将设备中的空间hotplug到系统内存中 108 | * 创建了一个dax字符设备,其ops为dax_fops 109 | 110 | 其中第二件事已经超出了本章节的描述范围,如果有机会将会在内存相关的章节中描述。 111 | 112 | 而第三件事虽然也可以单独开章节讨论,不过鉴于和本章内容强相关,所以将在下一篇中详细讨论。 113 | -------------------------------------------------------------------------------- /nvdimm/08-pfn.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/nvdimm/08-pfn.md -------------------------------------------------------------------------------- /paypal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/paypal.gif -------------------------------------------------------------------------------- /reference/00-reference.md: -------------------------------------------------------------------------------- 1 | 有很多值得一看的材料,对理解内核和社区给了我很大的帮助。 2 | 3 | 其中非常重要的一个来源就是[lwn][1] 4 | 5 | 在这里我列一些,一方面是可以以后查找,另一方面是后续研究学习的方向。 6 | 7 | * [设计理念][2] 8 | * [内核开发者书单][6] 9 | * [git][3] 10 | * [调试][4] 11 | 12 | 还有些可以整理成专题的: 13 | 14 | [内存相关][5] 15 | 16 | PS: 这个lwn kernel index页面做的不太友好,找专题不一定能看清楚。写了个破脚本把主题先搂出来一遍。不是很准,凑合可以看。 17 | 18 | ``` 19 | wget --no-check-certificate https://lwn.net/Kernel/Index/ 20 | grep "href=\"#" index.html | grep "name=" | grep -Po 'href="\K[^"]*' > kernel_index 21 | awk -F "-" '{print $1}' kernel_index | uniq > kernel_index.first 22 | ``` 23 | 24 | 这样可以筛出来一级标题,虽然也挺多的。。。 25 | 26 | 忘了还有一个重量级的宝库[内核自带文档][7]。 27 | 28 | [1]: https://lwn.net/Kernel/Index/ 29 | [2]: https://lwn.net/Kernel/Index/#Development_model-Patterns 30 | [3]: https://lwn.net/Kernel/Index/#Development_tools-Git 31 | [4]: https://lwn.net/Kernel/Index/#Development_tools-Kernel_debugging 32 | [5]: /reference/01-mm.md 33 | [6]: https://lwn.net/Kernel/Index/#Kernel_Hackers_Bookshelf 34 | [7]: /reference/03-kernel_doc.md 35 | -------------------------------------------------------------------------------- /reference/01-mm.md: -------------------------------------------------------------------------------- 1 | lwn上内存相关的主题有点儿分散,在这里集中一下: 2 | 3 | * [MMTests][1] 4 | * [anonmm][2] 5 | * [anon_vma][3] 6 | * [Contiguous_memory_allocator][4] 7 | * [DMA][5] 8 | * [GFP][6] 9 | * [Memory Management][7] 10 | * [HugePage][8] 11 | * [Hugetlbfs][9] 12 | 13 | 另外Gorman的巨著 14 | 15 | [Understanding the Linux Virtual Memory Manager][10] 16 | 17 | [1]: https://lwn.net/Kernel/Index/#Development_tools-MMTests 18 | [2]: https://lwn.net/Kernel/Index/#anonmm 19 | [3]: https://lwn.net/Kernel/Index/#anon_vma 20 | [4]: https://lwn.net/Kernel/Index/#Contiguous_memory_allocator 21 | [5]: https://lwn.net/Kernel/Index/#Direct_memory_access 22 | [6]: https://lwn.net/Kernel/Index/#gfp_t 23 | [7]: https://lwn.net/Kernel/Index/#Memory_management 24 | [8]: https://lwn.net/Kernel/Index/#Huge_pages 25 | [9]: https://lwn.net/Kernel/Index/#hugetlbfs 26 | [10]: https://www.kernel.org/doc/gorman/html/understand/ 27 | -------------------------------------------------------------------------------- /reference/02-mail.md: -------------------------------------------------------------------------------- 1 | 虽然gmail够大,但架不住社区邮件太多。突然发现两年前自己写过相关的一组patch,但是没有被接受。 2 | 也不知道发生了什么。。。 3 | 4 | 自己的邮箱里已经删了,但是web页面查邮件还是不太友好,想要下载到本地用mutt来看。搜了一下果然可以。 5 | 6 | # 找到Message-ID 7 | 8 | 通过google找到邮件,可以用title。找网址是https://lore.kernel.org的。 9 | 点进去,里面有个raw选项。再点进去 10 | 11 | # 找到Message-ID的邮件 12 | 13 | lkml提供了根据Message-ID[列出邮件的功能][1]。 14 | 15 | 用刚才找到的Message-ID拼接出url 16 | 17 | http://lore.kernel.org/lkml// 18 | 19 | # 下载整个会话 20 | 21 | 找到 Thread overview: 这一行,有一个mbox.gz按钮。 22 | ok,这样就可以将整个会话下载到本地了~ 23 | 24 | [1]: https://lore.kernel.org/lkml/_/text/help/ 25 | -------------------------------------------------------------------------------- /reference/03-kernel_doc.md: -------------------------------------------------------------------------------- 1 | 内核自带文档在源代码的Documents目录里。 2 | 3 | 这里面可以说是浩如烟海了。。。但是说实话程序员一般不喜欢写文档,很多内容估计没有及时维护已经过时了。不过就算是过时也是很好的学习资料。 4 | 5 | # 文档制作 6 | 7 | 另外这里要说一下文档的制作。因为现在文档用了rst格式,所以直接用编辑器看不是很友好。最好制作一下文档。 8 | 9 | 这里说一下如何制作html格式的文档。 10 | 11 | ``` 12 | # make htmldocs 13 | ``` 14 | 15 | 完成后文档就在Documentations/output目录下了。 16 | 17 | 不过这么制作的文档内容太多了,我们可以指定一下文档目录,限制一下制作范围。 18 | 19 | 比如只制作mm相关的文档,可以执行: 20 | 21 | ``` 22 | # make SPHINXDIRS=mm htmldocs 23 | ``` 24 | 25 | 这样会快很多。 26 | -------------------------------------------------------------------------------- /support.md: -------------------------------------------------------------------------------- 1 | Thanks for your support. 2 | 3 | Through Wechat 4 | 5 | ![wechat](/wechat.gif) 6 | 7 | 8 | Through Alipay 9 | 10 | ![Alipay](/alipay.gif) 11 | 12 | Through PayPal 13 | 14 | ![Alipay](/paypal.gif) 15 | 16 | -------------------------------------------------------------------------------- /synchronization/00-index.md: -------------------------------------------------------------------------------- 1 | 这里我们看看内核中常用的同步机制。 2 | 3 | 虽然 [Memory Barrier][2] 不算是同步机制,却是一个非常重要的基本概念。我也就一起放这里了。 4 | 5 | 6 | [RCU][1] 7 | 8 | 9 | [1]: /synchronization/01-rcu.md 10 | [2]: /synchronization/02-memory_barrier.md 11 | -------------------------------------------------------------------------------- /synchronization/01-rcu.md: -------------------------------------------------------------------------------- 1 | 2 | # RCU做到了什么 3 | 4 | > RCU supports concurrency between a single updater and multiple readers. 5 | 6 | 在RCU之前,我们只能做到的是: 7 | 8 | * 通过普通锁,同一时间只有一个人能访问,包括读和写 9 | * 通过读写锁,同一时间可以有多个人读,但是此时不能有写操作 10 | 11 | 而RCU的出现,能够做到在有一个人写的同时,可以有多个人读。同时这也意味着,写操作之间还需要其他锁机制做保护,RCU通常不会被单独使用。 12 | 13 | 当然,RCU对访问的数据也有一个适用范围。也就是对数据的实时性不敏感。 14 | 15 | # RCU底层的三个机制 16 | 17 | 在[What is RCU][2]中,描述了RCU底层的三个机制: 18 | 19 | * Publish-Subscribe 20 | * Wait For Pre-Existing RCU Readers to Complete 21 | * Maintain Multiple Versions of Recent Updated Objects 22 | 23 | ## Publish-Subscribe 24 | 25 | 这个机制的底层原因是**编译器和cpu的乱序执行**。 26 | 27 | 为了让我们最终访问的数据如我们期待,而不会被改变顺序。内核提供了写/读两方面的API,分别称为Publish/Subscribe。 28 | 29 | 下面这个表格举例了内核中针对不同数据结构给出的相应API 30 | 31 | |Category|Publish |Retract |Subscribe | 32 | |--------|-----------------------------------------------------------|--------------------------|--------------------------| 33 | |Pointer |rcu_assign_pointer() |rcu_assign_pointer(, NULL)|rcu_dereference() | 34 | |List |list_add_rcu()
list_add_tail_rcu()
list_replace_rcu()|list_del_rcu() |list_for_each_entry_rcu() | 35 | |Hlist |hlist_add_after_rcu()
hlist_replace_rcu() |hlist_del_rcu() |hlist_for_each_entry_rcu()| 36 | 37 | ## Wait For Pre-Existing RCU Readers to Complete 38 | 39 | 本质上,RCU是一种等的功夫。但是厉害的是,**RCU并不记录具体跟踪的对象**。 40 | 41 | > The great advantage of RCU is that it can wait for each of (say) 20,000 different things without having to explicitly track each and every one of them, and without having to worry about the performance degradation, scalability limitations, complex deadlock scenarios, and memory-leak hazards that are inherent in schemes using explicit tracking. 42 | 43 | 用rcu_read_lock()/rcu_read_unlock()原语界定的区域是RCU read-side critical section。这段区域可以包含任何代码,**除了 阻塞和睡眠(block or sleep)**。 44 | 45 | 文档[2]中描述了等待机制synchronize_rcu()的原理,就是让自己在所有cpu上都调度一遍。原因是: 46 | 47 | > RCU Classic read-side critical sections delimited by rcu_read_lock() and rcu_read_unlock() are not permitted to block or sleep. Therefore, when a given CPU executes a context switch, we are guaranteed that any prior RCU read-side critical sections will have completed. 48 | 49 | 这里的前提是RCU critical section中间的代码不能阻塞和睡眠,一旦调度发生,表明RCU read-side critical section一定执行完了。 50 | 51 | 所以当synchronize_rcu()返回,就可以清理相应的数据了。 52 | 53 | ## Maintain Multiple Versions of Recent Updated Objects 54 | 55 | 我觉得实际上就是通过上面的延迟销毁来做到的。 56 | 57 | # 一个例子 58 | 59 | 在参考[3]中,有这么一个使用rcu的例子。 60 | 61 | ``` 62 | 1 struct el { 63 | 2 struct list_head list; 64 | 3 long key; 65 | 4 spinlock_t mutex; 66 | 5 int data; 67 | 6 /* Other data fields */ 68 | 7 }; 69 | 8 spinlock_t listmutex; 70 | 9 struct el head; 71 | 72 | 1 int search(long key, int *result) 73 | 2 { 74 | 3 struct list_head *lp; 75 | 4 struct el *p; 76 | 5 77 | 6 rcu_read_lock(); 78 | 7 list_for_each_entry_rcu(p, head, lp) { 79 | 8 if (p->key == key) { 80 | 9 *result = p->data; 81 | 10 rcu_read_unlock(); 82 | 11 return 1; 83 | 12 } 84 | 13 } 85 | 14 rcu_read_unlock(); 86 | 15 return 0; 87 | 16 } 88 | 89 | 1 int delete(long key) 90 | 2 { 91 | 3 struct el *p; 92 | 4 93 | 5 spin_lock(&listmutex); 94 | 6 list_for_each_entry(p, head, lp) { 95 | 7 if (p->key == key) { 96 | 8 list_del_rcu(&p->list); 97 | 9 spin_unlock(&listmutex); 98 | 10 synchronize_rcu(); 99 | 11 kfree(p); 100 | 12 return 1; 101 | 13 } 102 | 14 } 103 | 15 spin_unlock(&listmutex); 104 | 16 return 0; 105 | 17 } 106 | ``` 107 | 108 | ## 为什么用list_for_each_entry()? 109 | 110 | 其中在delete函数中有一个我开始不太理解的用法。 111 | 112 | > 为什么这里遍历的时,用的是list_for_each_entry(),而不是list_for_each_entry_rcu() 113 | 114 | 我现在的理解是,因为update操作用spin_lock保证了此时没有别人变更链表,且锁本身保证了内存一致性。 115 | 所以现在用list_for_each_entry()看到的就是最新的版本,不会有老的版本。 116 | 117 | ## 为什么用list_del_rcu()? 118 | 119 | 但是此时删除为什么要用list_del_rcu()?后续的spin_unlock()难道不能保证内存被正确同步吗? 120 | 答:是我从函数名字上去理解了。list_del_rcu()的定义是: 121 | 122 | ``` 123 | static inline void list_del_rcu(struct list_head *entry) 124 | { 125 | __list_del_entry(entry); 126 | entry->prev = LIST_POISON2; 127 | } 128 | ``` 129 | 130 | 也就是执行完后保留了entry->next,是为了list_for_each_entry()能够继续往后面去找。 131 | 而普通的list,在删除的时候是通过list_for_each_entry_safe()事先得到next。所以list_del()的时候能够直接清除next。 132 | 133 | ## 不能list_move()? 134 | 135 | 在[protect list and objects][5] 中,说到不能用 hlist_for_each_entry_rcu()。而是要中间加一个smp_rmb()。 136 | 137 | 否则该节点已经从原链表移动到了新的链表,这样->next就被该改变,而无法继续遍历原来的链表。 138 | 139 | 从这点我想到,如果对一个链表执行list_move()应该也是不行的。 140 | 141 | # 参考资料 142 | 143 | [内核文档--RCU概念][1] 144 | [RCU dereference][2] 145 | 146 | [1]: https://docs.kernel.org/RCU/index.html 147 | [2]: https://lwn.net/Articles/262464/ 148 | [3]: https://lwn.net/Articles/263130/ 149 | [4]: https://docs.kernel.org/RCU/rcu_dereference.html 150 | [5]: https://docs.kernel.org/RCU/rculist_nulls.html 151 | -------------------------------------------------------------------------------- /tools/01-patch.md: -------------------------------------------------------------------------------- 1 | 发patch是和社区交流的重要过程,这个过程也有相关的规范要遵守。比如检查代码格式和确认需要发给谁。 2 | 3 | # 检查patch 4 | 5 | 内核代码有着自己的规范,在发patch前最好用脚本checkpatch.pl检查一下。 6 | 7 | 检查某个commit 8 | 9 | ``` 10 | ./scripts/checkpatch.pl -g HEAD 11 | ``` 12 | 13 | 检查某个patch文件 14 | 15 | ``` 16 | ./scripts/checkpatch.pl /patch/to/patch/file 17 | ``` 18 | 19 | # 查要发给谁 20 | 21 | 写完patch后,我们需要发到社区。具体发到哪里,需要发给谁也是有讲究的。可以用get_maintainer.pl来查看。 22 | 23 | ``` 24 | ./scripts/get_maintainer.pl /patch/to/patch/file 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /tools/02-check_file_change.md: -------------------------------------------------------------------------------- 1 | # obj文件变化 2 | 3 | 有时候我们该了代码,比如清理后,代码的大小会发生变化。这时想要展示这部分变化可以用bloat-o-meter。 4 | 5 | ``` 6 | ./scripts/bloat-o-meter file1.o file2.o 7 | ``` 8 | 9 | 通过这个,可以看到不同symbole在代码变化前后有没有区别,区别是多少。 10 | -------------------------------------------------------------------------------- /tools/03-selftest.md: -------------------------------------------------------------------------------- 1 | 内核中专门做功能测试的单元叫selftest。在内核目录的tools/testing/selftests目录下。 2 | 3 | 在这一部分我们花点时间研究一下。 4 | 5 | [测试用例的构造过程][1] 6 | 7 | [编写测试][2] 8 | 9 | [1]: /tools/03_01-build.md 10 | [2]: /tools/03_02-write_test.md 11 | -------------------------------------------------------------------------------- /tools/handy_tools.md: -------------------------------------------------------------------------------- 1 | 内核中有些好用的工具,在发patch、检查变化的时候可以用到。在这里做一个汇总。 2 | 3 | [检查补丁][1] 4 | 5 | [检查文件变化][2] 6 | 7 | [selftest][3] 8 | 9 | [1]: /tools/01-patch.md 10 | [2]: /tools/02-check_file_change.md 11 | [3]: /tools/03-selftest.md 12 | -------------------------------------------------------------------------------- /tracing/00-index.md: -------------------------------------------------------------------------------- 1 | 随着对内核理解的深入,是时候了解一下对内核调试、跟踪、画像的技术了。 2 | 3 | 不仅可以帮助自己了解内核运行,更能在出现性能问题时定位瓶颈。 4 | 5 | 在内核中能帮助我们的工具有很多,让我一一学来。 6 | 7 | 经过一段时间学习,发现内核中很多探测方法都基于了ftrace提供的框架。比如在[ftrace 中 eventtracing 的实现原理][3]中提到的trace原理,以及其他探测机制是如何使用ftrace框架的。在这里也把文中的图放在这里,做一个直观的了解。 8 | 9 | ![ftrace framework](/tracing/ftrace_framework.png) 10 | 11 | 既然如此,那我们就先来看看ftrace的使用以及原理。 12 | 13 | [ftrace的使用][4] 14 | 15 | [探秘ftrace][5] 16 | 17 | 了解了原理后,我们来看看都有哪些工具会依赖ftrace。 18 | 19 | 比如 [内核热补丁的黑科技][6]。这个看上去不像是trace的工具,却依赖ftrace框架来实现的。 20 | 21 | 还有一个借用了部分ftrace框架,但是实际上是另一个机制的[Trace Event][2] 22 | 23 | 接着来看的是传说为瑞士军刀的[eBPF初探][1]. 24 | 严重怀疑后续可能要完整的一章来讲述清楚。 25 | 26 | 对eBPF的探索中,发现他和内核系统中其他的探测机制有着非常深厚的关联。 27 | 28 | 比如: 29 | 30 | [TraceEvent][2] 31 | 32 | [1]: /tracing/01-ebpf.md 33 | [2]: /tracing/02-trace_event.md 34 | [3]: https://www.ibm.com/developerworks/cn/linux/1609_houp_ftrace/index.html 35 | [4]: /tracing/03-ftrace_usage.md 36 | [5]: /tracing/04-ftrace_internal.md 37 | [6]: /tracing/05-kernel_live_patch.md 38 | -------------------------------------------------------------------------------- /tracing/03-ftrace_usage.md: -------------------------------------------------------------------------------- 1 | 这章我们先来看看如何使用ftrace。 2 | 3 | 说来惭愧,多少年前同事告诉我这种高级方法的时候,我还觉得麻烦,依然在使用printk。现在回想起来真实井底之蛙啊。 4 | 5 | 有几个非常不错的参考资料: 6 | 7 | * 内核源码中的[使用手册][1] 8 | * [LWN 1][2] 9 | * [LWN 2][3] 10 | * [Secrets of the Ftrace function tracer][6] 11 | * [LWN Artical List][4] 12 | * [Ftrace Kernel Hooks: More than just tracing][5] 13 | 14 | 接下来就从来看几个例子: 15 | 16 | # Tracing测试文件目录 17 | 18 | 要和trace打交道都是要通过它的debugfs接口,默认情况下这个文件目录在 /sys/kernel/debug/tracing. 19 | 20 | 你也可以手动挂载到想要的位置: 21 | 22 | ``` 23 | mount -t tracefs nodev /sys/kernel/tracing 24 | ``` 25 | 26 | 在这个目录下有很多重要的文件,具体每个文件的作用可以在[使用手册][1]中找到。这里解说几个比较重要的 27 | 28 | 设置当前使用的tracer 29 | 30 | * current_tracer 31 | * available_tracer 32 | 33 | 是否过滤函数 34 | 35 | * available_filter_functions 36 | * set_ftrace_filter 37 | * set_graph_function 38 | 39 | 得到trace输出 40 | 41 | * trace 42 | * per_cpu/cpu0/trace 43 | * trace_pipe 44 | 45 | 控制是否将跟踪输出写入 46 | 47 | * tracing_on 48 | 49 | 像我这样的初级小白,了解这么几个文件就够用了。 50 | 51 | # 使用function tracer 52 | 53 | 先来看一个完整的使用方法。 54 | 55 | ``` 56 | echo 0 > tracing_on 57 | echo function > current_tracer 58 | echo 1 > tracing_on 59 | # wait a while 60 | echo 0 > tracing_on 61 | cat trace 62 | cat per_cpu/cpu0/trace 63 | ``` 64 | 65 | tracing_on用来控制是否实际输出到trace文件,所以我们在开始和结束时,我们都关掉输出,以免有太多的输出内容。 66 | 第二个命令用来指定这次使用的是function trace。因为没有设置函数过滤,所以应该是所有的内核函数都会被记录。 67 | 打开输出,停一会儿再关掉后,就可以通过trace文件查看这段时间内的内核运行情况了。 68 | 69 | 另外还可以通过查看per_cpu/cpu0/trace文件来只观察在cpu0上发生的内核函数调用。这是一个非常不错去除多个cpu之间干扰的好方法。 70 | 71 | # 使用function_graph tracer 72 | 73 | 这个例子和之前的区别在于选择了不同的tracer,function_graph的话可以打印出函数调用的关系,更加方便理解。 74 | 75 | ``` 76 | echo 0 > tracing_on 77 | echo function_graph > current_tracer 78 | echo 1 > tracing_on 79 | # wait a while 80 | echo 0 > tracing_on 81 | cat trace 82 | cat per_cpu/cpu0/trace 83 | ``` 84 | 85 | 也就不做过多的解释。 86 | 87 | # 使用trace_printk() 88 | 89 | 这个函数的用法和printk()一样,只是输出不在console,而是在ftrace的ring buffer。 90 | 91 | 而且在nop tracer的情况下也能在trace中看到输出。这样就不会被函数调用信息干扰到了。 92 | 93 | 汗颜,现在才知道这个。 94 | 95 | # 使用function command 96 | 97 | 这个用法来自作者的第三篇文章[Secrets of the Ftrace function tracer][6]。因为trace的内容太多,这个方法能够在某个函数被调用时停止trace,方便调试时观察到trace信息。 98 | 99 | 方法如下 100 | 101 | ``` 102 | # echo '__bad_area_nosemaphore:traceoff' > set_ftrace_filter 103 | # cat set_ftrace_filter 104 | #### all functions enabled #### 105 | __bad_area_nosemaphore:traceoff:unlimited 106 | ``` 107 | 108 | 这样就可以在函数__bad_area_nosemaphore触发时,停止trace,留住最后的案发现场。 109 | 110 | # 现实谁调用了某个函数 111 | 112 | 在学习内核的过程中,我们有时候会想了解一下一个函数都会被谁调用,整个调用栈是什么样子的。ftrace也提供了这个功能。 113 | 114 | ``` 115 | [tracing]# echo kfree > set_ftrace_filter 116 | [tracing]# cat set_ftrace_filter 117 | kfree 118 | [tracing]# echo function > current_tracer 119 | [tracing]# echo 1 > options/func_stack_trace 120 | [tracing]# cat trace | tail -8 121 | => sys32_execve 122 | => ia32_ptregs_common 123 | cat-6829 [000] 1867248.965100: kfree <-free_bprm 124 | cat-6829 [000] 1867248.965100: 125 | 126 | => free_bprm 127 | => compat_do_execve 128 | => sys32_execve 129 | => ia32_ptregs_common 130 | [tracing]# echo 0 > options/func_stack_trace 131 | [tracing]# echo > set_ftrace_filter 132 | ``` 133 | 134 | # Profiling 135 | 136 | Ftrace也能做profile。 137 | 138 | ``` 139 | [tracing]# echo nop > current_tracer 140 | [tracing]# echo 1 > function_profile_enabled 141 | [tracing]# cat trace_stat/function0 |head 142 | Function Hit Time Avg 143 | -------- --- ---- --- 144 | schedule 22943 1994458706 us 86931.03 us 145 | poll_schedule_timeout 8683 1429165515 us 164593.5 us 146 | schedule_hrtimeout_range 8638 1429155793 us 165449.8 us 147 | sys_poll 12366 875206110 us 70775.19 us 148 | do_sys_poll 12367 875136511 us 70763.84 us 149 | compat_sys_select 3395 527531945 us 155384.9 us 150 | compat_core_sys_select 3395 527503300 us 155376.5 us 151 | do_select 3395 527477553 us 155368.9 us 152 | ``` 153 | 154 | 没实验成功,以后再看看。 155 | 156 | [1]: https://github.com/torvalds/linux/blob/master/Documentation/trace/ftrace.rst 157 | [2]: https://lwn.net/Articles/365835/ 158 | [3]: https://lwn.net/Articles/366796/ 159 | [4]: https://lwn.net/Kernel/Index/#Tracing 160 | [5]: https://blog.linuxplumbersconf.org/2014/ocw/system/presentations/1773/original/ftrace-kernel-hooks-2014.pdf 161 | [6]: https://lwn.net/Articles/370423/ 162 | -------------------------------------------------------------------------------- /tracing/06-drgn.md: -------------------------------------------------------------------------------- 1 | [DRGN][1]是一个相对较新的查看内核状态的工具。它是Meta开发的,作为crash的一个替代工具。 2 | 3 | # 安装 4 | 5 | 参考最新的[安装文档][2]。 6 | 7 | 有几种安装的方式,感觉通过pip来安装,方式比较统一。 8 | 9 | ``` 10 | sudo pip3 install drgn 11 | ``` 12 | 13 | 如果能运行下面的命令,看上去应该是安装好了。 14 | 15 | ``` 16 | python3 -m drgn --help 17 | ``` 18 | 19 | # 20 | 21 | [1]: https://drgn.readthedocs.io/en/latest/ 22 | [2]: https://drgn.readthedocs.io/en/latest/installation.html 23 | -------------------------------------------------------------------------------- /tracing/ftrace_framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/tracing/ftrace_framework.png -------------------------------------------------------------------------------- /virtual_mm/00-index.md: -------------------------------------------------------------------------------- 1 | 终于又要开新的一章了,这次我们来聊聊内存相关但又相对独立的一块 -- 虚拟内存空间。 2 | 3 | 从我现在的角度去看,内核中的内存管理可以分成两个重要的部分: 4 | 5 | * 物理内存分配回收 6 | * 虚拟内存空间管理 7 | 8 | 其中第一部分在[自底而上话内存][1]一章中做了描述,第二部分将在这章展开。 9 | 10 | 其实这两部分并不是完全分割的,而是你中有我,我中有你。只是在一定层次上看,第一部分的内容已经相对完整可以自成一个体系了。 11 | 12 | 虽然在本章中将紧密得引用第一部分的内容,尤其是struct page,但是我们还是可以尽量将本章分成一下几个部分来描述。 13 | 14 | * 页表 15 | * vma 16 | * 反向映射 17 | * mapcount 18 | * THP 19 | 20 | # 页表 21 | 22 | 可以说虚拟内存空间的物理根本就是页表了,所以开篇的头一个小节我们来看看页表的样子,以及页表构造和释放的过程。 23 | 24 | [页表和缺页中断][4] 25 | 26 | # vma 27 | 28 | 除了有硬件上的页表做虚拟空间的映射,在软件上也有对应的数据结构来区分虚拟地址空间的属性,而这个数据结构就是: 29 | 30 | [虚拟地址空间的管家--vma][6] 31 | 32 | PS: 在引入maple tree之前的[版本][10] 33 | 34 | # 反向映射 35 | 36 | 反向映射是用来搜索对于某一个内存页,都有哪些进程在使用。其中分成了匿名页和文件页。 37 | 38 | 反向映射的架构也是经历了一番周折才定型为现在的样子。首先我们来说说[匿名反向映射的前世今生][2] 39 | 40 | 了解了历史后,我们深入学习一下现状 [图解匿名反向映射][7] 41 | 42 | # mapcount 43 | 44 | 看了上文后,大家一定对反向映射有了概念性的认识。但是你一定想不到在学习反向映射的过程中有一个非常繁琐的内容 -- mapcount。 45 | 46 | 从概念上看这个值的含义是表示当前页映射到页表的个数,案例是比较好理解的。但是当这个数遇到了透明大页(THP)后就产生了各种纠缠不清的恩恩怨怨。在[THP和mapcount之间的恩恩怨怨][3]文章中,我将给大家尝试理清楚这中间的是是非非。 47 | 48 | 最后我们尝试全面了解一下page结构中,跟踪作为进程内存被页表映射的各种情况[page mapcount][11]。 49 | 50 | # THP 51 | 52 | 透明大页(THP)是一种能够加速页表查询的方法。很有意思,在这里简要说一下我的理解。 53 | 54 | [透明大页的玄机][5] 55 | 56 | # NUMA策略 57 | 58 | 当系统中存在多个numa节点时,内存从哪个numa节点上分配就变得有些重要了。因为这会影响到系统运行的性能。 59 | 60 | 内核中为了应对这个问题,提出了[NUMA策略][8] 61 | 62 | 同时为了保持进程迁移过程中内存策略的一致,内核还提供了[numa balance][9]。 63 | 64 | [1]: /mm/00-memory_a_bottom_up_view.md 65 | [2]: /virtual_mm/01-anon_rmap_history.md 66 | [3]: /virtual_mm/02-thp_mapcount.md 67 | [4]: /virtual_mm/03-page_table_fault.md 68 | [5]: /virtual_mm/04-thp.md 69 | [6]: /virtual_mm/05-vma.md 70 | [7]: /virtual_mm/06-anon_rmap_usage.md 71 | [8]: /virtual_mm/07-mempolicy.md 72 | [9]: /virtual_mm/08-numa_balance.md 73 | [10]: /virtual_mm/deprecate-vma.md 74 | [11]: /virtual_mm/09-mapcount.md 75 | -------------------------------------------------------------------------------- /virtual_mm/04-thp.md: -------------------------------------------------------------------------------- 1 | 虚拟内存管理中有个常用,非常有意思,但是知道名字但是不知道是怎么运作的机制--透明大页(Transparent Huge Page)。 2 | 3 | 之前我对这个东西也不是很理解,而且还经常和另一个概念混淆--hugetlb。这两个都是利用了页表中“大页”表项,减少tlb的失误进而提高内存访问速度。而两者不同的是,相对hugetlb, THP更加灵活,当然也更加复杂。 4 | 5 | # 相同之处 6 | 7 | THP和hugetlb相同之处,或者说和其他虚拟内存的相同之处在于在缺页中断中的处理流程几乎是一样的。在__handle_mm_fault函数中会逐级查询,并按照不同情况进行处理。比如如果pmd为空,且支持THP,那么就会调用create_huge_pmd()来创建。或者是在COW的情况下,就会调用wp_huge_pmd()进行处理。 8 | 9 | ``` 10 | __handle_mm_fault 11 | create_huge_pmd() 12 | do_huge_pmd_numa_page 13 | wp_huge_pmd 14 | ``` 15 | 16 | # 不同之处 17 | 18 | 相同之处平平无奇,关键还是在于不同之处。所谓透明,其实就是比hugetlb多了一些灵活性。在必要的时候可以拆分页表或者拆分大页本身。 19 | 20 | 就当前的代码阅读理解来看,有这么几个不同之处。 21 | 22 | * 预留页表 23 | * 拆分页表 24 | * 拆分大页 25 | 26 | ## 预留页表 27 | 28 | 在创建大页表项的时候,与其他人不同,还预留了一张页表。分别对应两个函数。 29 | 30 | * pgtable_trans_huge_deposit() 31 | * pgtable_trans_huge_withdraw() 32 | 33 | 我们先来看第一个。这个函数的动作是预留了一个页表。那什么时候用呢?就是在第二个函数发生的时候。那为什么要这么做呢?这个就牵扯到下一个不同点--拆分页表。 34 | 35 | 我们知道对于大页,页表一共有三级。而正常的4k页的页表是四级。所以当我们要拆分大页页表时,就需要补上这么一级。为了保证拆分时,不会因为内存不够导致不能展开到四级页表,所以在分配时就多预留了一个页表。 36 | 37 | 不得不说这是一个有意思的设计。 38 | 39 | ## 拆分页表 40 | 41 | 现在的透明大页还支持拆分,这样可以在必要的时候退回到四级页表。 42 | 43 | 这个工作由函数__split_huge_pmd_locked完成。 44 | 45 | ``` 46 | __split_huge_pmd_locked 47 | pgtable = pgtable_trans_huge_withdraw(mm, pmd); 48 | pmd_populate(mm, &_pmd, pgtable); 49 | for (i = 0; i < HPAGE_PMD_NR; i++) { 50 | set_pte_at(mm, addr, pte, entry); 51 | } 52 | pmd_populate(mm, pmd, pgtable); 53 | ``` 54 | 55 | 其实说白了很简单,就是把预留了页表拿出来,每一项都填上大页对应的地址。最后把相应的pmd改好。是不是有点酷。 56 | 57 | 当然我这个流程中只显示了匿名页的拆分过程,对于文件页还需要继续学习。 58 | 59 | ## 拆分大页 60 | 61 | 拆分页表最终的目的也是为了拆分大页,这样在系统内存不够时可以回收大页中没有使用的部分。 62 | 63 | 下图是一个非常简化版本的大致流程。 64 | 65 | ``` 66 | split_huge_page_to_list 67 | unmap_page 68 | try_to_unmap_one 69 | ... 70 | __split_huge_pmd_locked 71 | __split_huge_page 72 | remap_page 73 | ``` 74 | 75 | 可以看到,拆分大页的过程中,也有可能会拆分页表。此时我们称之为PMD-mapped THP。如果这个大页的页表已经被拆分过了,就不需要也不能再次拆分,我们就叫它PTE-mapped THP。 76 | 77 | 所以对于匿名大页,整个拆分的过程就分成两种情况 78 | 79 | * PMD-mapped THP 80 | * PTE-mapped THP 81 | 82 | 我们分别来看看这两种情况下拆分过程中细微的差别。 83 | 84 | 对于PMD-mapped THP, __split_huge_pmd_locked函数会执行到。该函数会将PTE entry改写成migration entry。然后在函数remap_page中再将migration entry 恢复到页表中。怎么样,是不是很有意思。 85 | 86 | 对于PTE-mapped THP, __split_huge_pmd_locked函数不会被执行,因为页表早已拆分。此时try_to_unmap_one函数就担负起将PTE entry设置成migration entry的重任。接下来就和之前一样,由remap_page将migration entry恢复到页表中。 87 | -------------------------------------------------------------------------------- /virtual_mm/07-mempolicy.md: -------------------------------------------------------------------------------- 1 | 随着系统中的CPU和内存增加,物理上将内存分成了numa节点。而运行时系统从哪个节点上分配内存就会影响到系统运行的性能。 2 | 3 | 内核当然可以提供一种默认的分配策略,而同时用户或许也会希望能够设置自己的分配行为。 4 | 5 | 今天我们就来看看内核提供了哪些分配策略,又是如何实现的。 6 | 7 | # 最关键的数据结构 -- mempolicy 8 | 9 | 对我来说,每次要学习一个新的概念,最关键的就是学习这个概念背后的数据结构,而算法则是对数据结构的一个操作。 10 | 11 | 在numa策略中,内核最关键的数据结构是mempolicy。 12 | 13 | ``` 14 | mempolicy 15 | +------------------------------+ 16 | |refcnt | 17 | | (atomic_t) | 18 | |mode | 19 | |flags | 20 | | (unsigned short) | 21 | |v | 22 | | +--------------------------+ 23 | | |preferred_node | 24 | | | (short) | 25 | | |nodes | 26 | | | (nodemask_t) | 27 | | +--------------------------+ 28 | |w | 29 | | +--------------------------+ 30 | | |cpuset_mems_allowed | 31 | | |user_nodemask | 32 | | | (nodemask_t) | 33 | +---+--------------------------+ 34 | ``` 35 | 36 | 这个结构相对来说是简单的。其中最重要的就是mode成员: 37 | 38 | * mode 指定了策略的模式 39 | 40 | 知道了这个策略的模式,你就大致能猜到策略的运作和其他成员的含义。那我们就来看看内核当前都有哪些模式: 41 | 42 | ``` 43 | enum { 44 | MPOL_DEFAULT, 45 | MPOL_PREFERRED, 46 | MPOL_BIND, 47 | MPOL_INTERLEAVE, 48 | MPOL_LOCAL, 49 | MPOL_MAX, /* always last member of enum */ 50 | }; 51 | ``` 52 | 53 | 具体每个策略的含义暂且不表,因为这个可以在网上直接搜到。而我们想要说的是,这个数据结构是如何影响到一个进程的内存分配的。 54 | 55 | # mempolicy和进程的关联 56 | 57 | 既然是要控制进程的内存分配策略,那么必然这个数据结构就要和进程发生关系,否则怎么能够控制到进程呢? 58 | 59 | 这时候,还是数据结构能帮上忙。 60 | 61 | ``` 62 | task_struct 63 | +----------------------+ 64 | |mempolicy | 65 | | (struct mempolicy*)| 66 | |mm | 67 | | (struct mm_struct*)| 68 | +----------------------+ 69 | / \ 70 | / \ 71 | / \ 72 | / \ 73 | vma vma 74 | +------------------------+ +------------------------+ 75 | |vm_ops | |vm_ops | 76 | | get_policy | | get_policy | 77 | | | | | 78 | |vm_policy | |vm_policy | 79 | | (struct mempolicy*) | | (struct mempolicy*) | 80 | +------------------------+ +------------------------+ 81 | ``` 82 | 83 | 从这个结构中我们可以看到,在进程和vma级别都有各自的mempolicy指针。当我们在分配内存时,就会根据对应的mempolicy来分配。 84 | 85 | 那接下来我们就要回答两个问题: 86 | 87 | * 如何设置mempolicy 88 | * mempolicy如何影响分配 89 | 90 | # 设置mempolicy的用户态接口 91 | 92 | 首先我们来回答第一个问题--如何设置mempolicy。因为mempolicy分别在进程和vma两个层次,所以内核提供了对应的两个接口来设置。 93 | 94 | * set_mempolicy 95 | * mbind 96 | 97 | ## 设置进程级numa策略 -- set_mempolicy 98 | 99 | 首先是进程级别的numa策略设置,由函数set_mempolicy来负责。 100 | 101 | 具体函数的含义大家查找man就可以了,我们这里来看看这个函数干的活。 102 | 103 | ``` 104 | set_mempolicy() -> kernel_set_mempolicy() 105 | get_nodes(&nodes, nmask, maxnode) 106 | do_set_mempolicy(mode, flags, &nodes) 107 | new = mpol_new(mode, flags, nodes) 108 | task_lock(current) 109 | mpol_set_nodemask(new, nodes, scratch) 110 | old = current->mempolicy 111 | current->mempolicy = new 112 | task_unlock(current) 113 | mpol_put(old) 114 | ``` 115 | 116 | 打开一看其实很简单,就是给task_struct上新安装一个mempolicy。得嘞。 117 | 118 | ## 设置区域级numa策略 -- mbind 119 | 120 | 除了进程级别的numa策略,内核还提供了vma级别细粒度的策略设置,由函数mbind来负责。 121 | 122 | mbind函数除了设置vma级别的numa策略,还会做内存迁移。所以这个函数实际上是做了两件事情。 123 | 124 | ``` 125 | mbind(start, len, mode, nmask, maxnode, flags) -> kernel_mbind() 126 | get_nodes(&nodes, nmask, maxnode) 127 | do_mbind(start, len, mode, mode_flags, &nodes, flags) 128 | new = mpol_new(mode, mode_flags, nmask) 129 | migrate_prep() 130 | lru_add_drain_all() 131 | queue_pages_range(mm, start, end, nmask, flags | MPOL_MF_INVERT, &pagelist) 132 | mbind_range(mm, start, end, new) 133 | migrage_pages(&pagelist, new_page, NULL, start, ) 134 | ``` 135 | 136 | 所以整个函数只有在mbind_range上才是去更新vma对应的策略,其他的步骤都是为了做内存迁移准备的。 137 | 138 | # numa策略的作用 139 | 140 | 了解了策略的数据结构,了解了这个结构和进程之间的关系,也了解了如何设置策略,那么接下来就是要看看这个设置好的策略究竟如何起作用了。 141 | 142 | 大家猜猜,内核中会在哪些地方起作用呢?嗯,对了,至少有两个地方会发挥numa策略的作用。 143 | 144 | * page fault 145 | * numa balance 146 | 147 | 首先是在缺页处理时当然会按照当前的策略去分配内存,其次就是会动态的检测进程的内存是否符合当前的策略并进行相应的调整。 148 | 149 | ## page fault 150 | 151 | 发生缺页异常时,内核会去分配内存并将页表填好。此时就会考虑到从哪里去分配内存,在这个时候就会去参考之前设置的numa策略。 152 | 153 | ``` 154 | do_anonymous_page() 155 | alloc_zeroed_user_highpage_movable() -> __alloc_zeroed_user_highpage() 156 | alloc_pages_vma(), alloc with NUMA policy(understand how policy works) 157 | pol = get_vma_policy(vma, addr), pol from vma or task policy 158 | alloc_page_interleave(), if pol->mode == MPOL_INTERLEAVE 159 | nmask = policy_nodemask() 160 | ``` 161 | 162 | 比如说在我们熟悉的do_anonymous_page()过程中需要去分配页,就会调用alloc_pages_vma()根据对应的mempolicy来分配。 163 | 164 | ## numa balance 165 | 166 | 而另一个重要的场景是numa balance,这个部分因为确实有点长,我们需要另开一节来详细描述。 167 | -------------------------------------------------------------------------------- /virtual_mm/08-numa_balance.md: -------------------------------------------------------------------------------- 1 | 在上一节中我们对内存分配策略做了一定的了解,并且留了一个话题 -- numa balance。也就是内核为了在进程迁移过程中保持内存分配策略做的工作。 2 | 3 | 进程在运行过程中有可能会迁移到不同的cpu上执行,如果被迁移到的cpu和内存之前不满足mempolicy,那么numa balance就会尝试将用符合策略的内存来替换。所以整个过程分为两个步骤: 4 | 5 | * 扫描内存是否符合策略,并将不符合的pte设置为pronnone 6 | * 通过numa hinting page fault替换页 7 | 8 | # 扫描进程空间 9 | 10 | 说到扫描就有一个扫描时机,当使用cfs调度时,这个时机由task_tick_fair时刻触发。经过一系列的检查后,会插入curr->numa_work等待执行。 11 | 12 | 这个numa_work的回调函数是task_numa_work,而这个函数的核心就是扫描符合条件的vma,并通过change_prot_numa把对应的pte属性添加上PAGE_NONE。 13 | 14 | # numa hinting page fault 15 | -------------------------------------------------------------------------------- /virtual_mm/09-mapcount.md: -------------------------------------------------------------------------------- 1 | 当分配内存给进程使用,被映射到进程页表中时,内核会在page中记录page被映射的次数。也是因为有大页的存在,这个计数的过程变得有点复杂。 2 | 3 | # 都在哪里计数 4 | 5 | 既然是要计数,那就一定有记录的位置。本质上计数还是放在struct 6 | page中,在这里我们把相关的内容抽出来看,更清楚理解其中的使用方式。毕竟page太[让人眼花缭乱了][1]。 7 | 8 | 现在有两种表达page的结构,struct page和struct folio。为了清楚,下面分别展示一下计数的位置,其中每一行代表一个word。 9 | 10 | ``` 11 | page 12 | +----------------------------------+ 13 | |flags | 14 | | compound_head | 15 | | | 16 | |mapping | 17 | |share | 18 | |private | 19 | |_mapcount/page_type | 20 | |_refcount | 21 | |memcg_data / _unused_slab_obj_exts| 22 | |virtual | 23 | |_last_cpupid | 24 | +----------------------------------+ 25 | ``` 26 | 27 | ``` 28 | folio 29 | page page_1 30 | +-----------------------------+-------------------------------+ 31 | |flags |_flags_1(order) | 32 | | |_head_1(compound_head) | 33 | | |large_mapcount/_nr_pages_mapped| 34 | |mapping |_entire_mapcount/_pin_count | 35 | |share |_mm_id_mapcount[2] | 36 | |private |_mm_ids | 37 | |_mapcount |_mapcount_1 | 38 | |_refcount |_refcount_1 | 39 | | |_nr_pages | 40 | +-----------------------------+-------------------------------+ 41 | ``` 42 | 43 | [1]: /mm/10-page_struct.md 44 | -------------------------------------------------------------------------------- /virtual_mm/will-it-scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/virtual_mm/will-it-scale.png -------------------------------------------------------------------------------- /wechat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RichardWeiYang/kernel_exploring/ab2a7a8090c994f0d5926a5bb3a168b5e22151c7/wechat.gif --------------------------------------------------------------------------------