一、Linux内核目录与顶层Makefile详解
Linux内核源码目录简介:我们重点关心以下文件夹与文件 1、arch目录:这个目录是和架构有关的目录,比如 arm、arm64、avr32、x86 等等架构。每种架构都对应 一个目录,在这些目录中又有很多子目录,比如 boot、common、configs 等等。arch/arm 的一部分子目录,这些子目录用于控制系统引导、系统调用、动态调频、主频设置等。arch/arm/configs 目录是不同平台的默认配置文件:xxx_defconfig。arch/arm/boot/dts 目录里面是对应开发平台的设备树文件。arch/arm/boot 目录下会保存编译出来的 Image 和 zImage 镜像文件,而 zImage 就是我们要用的 linux 镜像文件。arch/arm/mach-xxx 目录分别为相应平台的驱动和初始化文件,比如 mach-imx 目录里面就是 I.MX 系列 CPU 的驱动和初始化文件。 /// 2、block目录:block 是 Linux 下块设备目录,像 SD 卡、EMMC、NAND、硬盘等存储设备就属于块设备,block 目录中存放着管理块设备的相关文件。 /// 3、crypto目录:crypto 目录里面存放着加密文件,比如常见的 crc、crc32、md4、md5、hash 等加密算法。 /// 4、Documentation目录:此目录里面存放着 Linux 相关的文档,如果要想了解 Linux 某个功能模块或驱动架构的功能,就可以在 Documentation 目录中查找有没有对应的文档。Documentation\devicetree\bindings这个目录有关设备树的绑定信息,很重要。 /// 5、drivers目录:驱动目录文件,此目录根据驱动类型的不同,分门别类进行整理,比如 drivers/i2c 就是 I2C相关驱动目录,drivers/gpio 就是 GPIO 相关的驱动目录。后面几乎所有的驱动设备都在里面实现。 /// 6、firmware目录:用于存放固件,也就是具体的外部设备驱动相关。 /// 7、fs目录:此目录存放文件系统,比如 fs/ext2、fs/ext4、fs/f2fs 等,分别是 ext2、ext4 和 f2fs 等文件系统。各种文件系统格式的实现源码 /// 8、include目录:Linux内核相关的头文件存放目录。 /// 9、init目录:此目录存放 Linux 内核启动的时候初始化代码。 /// 10、ipc目录:IPC 为进程间通信,ipc 目录是进程间通信的具体实现代码。 /// 11、kernel目录:Linux内核的源码,后面学习Linux底层实现的关键位置 /// 12、lib目录:lib 是库的意思,lib 目录都是一些公用的库函数。 /// 13、mm目录:此目录存放内存管理相关源码 /// 14、net目录:此目录存放网络相关的相关源码,包括IP协议。蓝牙软件协议的等软件相关实现。 /// 15、samples目录:此目录存放一些实例代码实现。 /// 16、scripts目录:脚本目录,Linux 编译的时候会用到很多脚本文件,这些脚本文件就保存在此目录中。 /// 17、security目录:此目录存放与安全相关的文件。 /// 18、sound目录:此目录存放音频相关驱动文件,音频驱动文件并没有存放到 drivers 目录中,而是单独的目 录。 /// 19、tools目录:此目录存放一些编译的时候使用到的工具。 /// 20、usr目录:此目录存放与 initramfs 有关的代码。文件系统初始化??? /// 21、virt目录:此目录存放虚拟机相关文件。在Linux上对对虚拟机的支持??? /// 22、.config文件:跟 uboot 一样,.config 保存着 Linux 最终的配置信息,编译 Linux 的时候会读取此文件中 的配置信息。最终根据配置信息来选择编译 Linux 哪些模块,哪些功能。 /// 23、Kbuild文件:有些Makefile会读取此文件 /// 24、Kconfig文件:图形化配置界面的配置文件。 /// 25、Makefile文件:分析源码编译的重点文件。 /// 26、README文件:此文件详细讲解了如何编译 Linux 源码,以及 Linux 源码的目录信息,建议仔细阅读一下此文件。 /// |
顶层Makefile编译流程: 因为Uboot的Makefile参考了Linux的架构,所以Makefile前602行几乎一模一样。都是根据make命令输入的一些命令行参数来生成编译所需的环境变量,包括C检查、M模块编译、O编译输出路径指定、V编译过程是否可见、以及ARCH和CROSS_COMPILE指定CPU架构和选择的交叉编译器等等。 1、make xxx_defconfig过程 第一步:指令make xxx_defconfig匹配到顶层Makefile文件第540行 %config: scripts_basic outputmakefile FORCE $(Q) $(MAKE) $(build)=scripts/kconfig $@ 第二步:接下来,找到scripts_basic的来源,第448行 scripts_basic: $(Q)$(MAKE) $(build)=scripts/basic $(Q)rm -f .tmp_quiet_recordmcount 第三步:总结出来的话,指令最终有意义的就以下这两行 $(Q) $(MAKE) $(build)=scripts/kconfig $@ $(Q)$(MAKE) $(build)=scripts/basic 第四步:这两句话,不约而同地使用了build这个变量,这个变量从下面应用的文件而来,第349行 scripts/Kbuild.include: ; include scripts/Kbuild.include 第五步:打开scripts/Kbuild.include这个文件,找到变量build,在第171行 build := -f $(srctree)/scripts/Makefile.build obj 第六步:结合上面的分析,将顶层Makefile的命令展开,就得到 make ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig make ./scripts/Makefile.build obj=scripts/basic 第七步:上面提炼出来的指令我们接下来就要进入到文件./scripts/Makefile.build里去看看,第42行 kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src)) kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile) include $(kbuild-file) 第八步:我们将kbuild-dir和kbuild-file展开,得到 kbuild-dir=./scripts/basic或kbuild-dir=./scripts/kconfig kbuild-file= ./scripts/basic/Makefile或kbuild-file= ./scripts/kconfig/Makefile 第九步:继续分析./scripts/Makefile.build,从make ./scripts/Makefile.build obj=scripts/basic入手,由于没有指定具体编译目标,所以跳转到默认目标__build,第94行 __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \ $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \ $(subdir-ym) $(always) @: 第十步:这个__build最终展开后,得到(如下);也就意味着,scripts_basic 目标的作用就是编译出 scripts/basic/fixdep 和 scripts/basic/bin2c 这两个软件。 __build: scripts/basic/fixdep scripts/basic/bin2c @: 第十一步:回过头来看看make ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig这个指令,依据上面的分析,会调用到./scripts/kconfig/的Makefile文件,其中第104行 %_defconfig: $(obj)/conf $(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig) 第十二步:将上面的命令展开后,得出结论,%_defconfig依赖scripts/kconfig/conf,所以会编译scripts/kconfig/conf.c生成conf这个软件。此软件就会将%_defconfig 中的配置输出到.config 文件中,最终生成 Linux kernel 根目录下的.config 文件。 %_defconfig: scripts/kconfig/conf @ scripts/kconfig/conf --defconfig=arch/arm/configs/%_defconfig Kconfig 这些流程和Uboot基本是一样的!!! /// 2、Makefile.build脚本分析 前面就已经分析了,后面还会用到,无需专门讲解。 /// 3、make的过程分析 第一步:输入指令make 或 make all进入到顶层Makefile,这里会调用默认的目标all,从125行到608行。 第二步:由第608行可知,all这个目标又依赖目标vmlinux,第920行 vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE ifdef CONFIG_HEADERS_CHECK $(Q)$(MAKE) -f $(srctree)/Makefile headers_check endif ifdef CONFIG_SAMPLES $(Q)$(MAKE) $(build)=samples endif ifdef CONFIG_BUILD_DOCSRC $(Q)$(MAKE) $(build)=Documentation endif ifdef CONFIG_GDB_SCRIPTS $(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py endif +$(call if_changed,link-vmlinux) 第三步:从vmlinux这个目标可以看到vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE,它也要依赖一些文件,这里找到被依赖项vmlinux-deps的具体描述,在912行 vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) 第四步:从上面来看,vmlinux-deps被赋值一些变量,在第905行 export KBUILD_VMLINUX_INIT := $(head-y) $(init-y) export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y) export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds 第五步:接下来,就需要具体去解析上面提及的各种“y”所对应的具体内容了。 第六步:看看head-y被定义在 arch/arm/Makefile 中,其内容如下;最终解释出来就是head-y = arch/arm/kernel/head.o head-y := arch/arm/kernel/head$(MMUEXT).o 第七步:接下来找找init-y 、drivers-y 和 和 net-y的定义,在558行和896行这里有定义;从这些定义可以得到结论 init-y := init/ drivers-y := drivers/ sound/ firmware/ net-y := net/ libs-y := lib/ core-y := usr/ init-y := $(patsubst %/, %/built-in.o, $(init-y)) core-y := $(patsubst %/, %/built-in.o, $(core-y)) drivers-y := $(patsubst %/, %/built-in.o, $(drivers-y)) net-y := $(patsubst %/, %/built-in.o, $(net-y)) 转换后得到 init-y = init/built-in.o drivers-y = drivers/built-in.o sound/built-in.o firmware/built-in.o net-y = net/built-in.o 第八步:我们再来看看libs-y,变量具体定义在顶层Makedfile的第561行和第900行;实际上,在第561行以后,lib-y这个变量在文件 arch/arm/Makefile里被添加了新的变量, libs-y := lib/ libs-y1 := $(patsubst %/, %/lib.a, $(libs-y)) libs-y2 := $(patsubst %/, %/built-in.o, $(libs-y)) libs-y := $(libs-y1) $(libs-y2) libs-y := arch/arm/lib/ $(libs-y) //在arch/arm/Makefile里 从这里得到结果 libs-y = arch/arm/lib/lib.a lib/lib.a arch/arm/lib/built-in.o lib/built-in.o 第九步:这里在看看最主要的变量core-y,在顶层Makefile第562行和887行还有第897行;在arch/arm/Makefile里面,也对这个变量添加了新的内容,第269行。 core-y := usr/ core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ 目录arch/arm/Makefile里的内容 core-$(CONFIG_FPE_NWFPE) += arch/arm/nwfpe/ core-$(CONFIG_FPE_FASTFPE) += $(FASTFPE_OBJ) core-$(CONFIG_VFP) += arch/arm/vfp/ core-$(CONFIG_XEN) += arch/arm/xen/ core-$(CONFIG_KVM_ARM_HOST) += arch/arm/kvm/ core-$(CONFIG_VDSO) += arch/arm/vdso/ core-y += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/ core-y += arch/arm/probes/ core-y += arch/arm/crypto/ core-y += arch/arm/firmware/ core-y += $(machdirs) $(platdirs) 由顶层的Makefile的命令core-y := $(patsubst %/, %/built-in.o, $(core-y))可知,最终也是生成一系列的built-in.o。 第十步:这些变量都是一些 built-in.o 或.a 等文件,这个和 uboot 一样,都是将相应目录中的源码文件进行编译,然后在各自目录下生成 built-in.o 文件,有些生成了.a 库文件。最终将这些 built-in.o 和.a 文件进行链接即可形成 ELF 格式的可执行文件,也就是 vmlinux!但是链接是需要链接脚本的,vmlinux 的依赖 arch/arm/kernel/vmlinux.lds 就是整个 Linux 的链接脚本。顶层Makefile第933行 +$(call if_changed,link-vmlinux) 第十一步:从上面第933行的命令含义来解析,$(call if_changed,link-vmlinux)是调用函数 if_changed,link-vmlinux 是函数 if_changed 的参数,函数 if_changed 定义在文件 scripts/Kbuild.include 中,第247行 if_changed = $(if $(strip $(any-prereq) $(arg-check)), \ @set -e; \ $(echo-cmd) $(cmd_$(1)); \ printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd) 第十二步:进一步分析上面的脚本函数,可以得出结论,会执行顶层Makefile的cmd_link-vmlinux这个目标,第915行 cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux) quiet_cmd_link-vmlinux = LINK $@ 最终解析出来就是 cmd_link-vmlinux = /bin/bash scripts/link-vmlinux.sh arm-linux-gnueabihf-ld -EL -p --no-undefined -X --pic-veneer --build-id 第十三步:从上面可以知道,需要调用到这个脚本scripts/link-vmlinux.sh来链接出vmlinux这个可执行文件,脚本文件内容如下,第51行 vmlinux_link() { local lds="${objtree}/${KBUILD_LDS}" if [ "${SRCARCH}" != "um" ]; then ${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} \ -T ${lds} ${KBUILD_VMLINUX_INIT} \ --start-group ${KBUILD_VMLINUX_MAIN} --end-group ${1} else ${CC} ${CFLAGS_vmlinux} -o ${2} \ -Wl,-T,${lds} ${KBUILD_VMLINUX_INIT} \ -Wl,--start-group \ ${KBUILD_VMLINUX_MAIN} \ -Wl,--end-group \ -lutil ${1} rm -f linux fi } ... info LD vmlinux vmlinux_link "${kallsymso}" vmlinux 第十四步,分析上面的脚本代码,可知,最后就lds链接各种built-in.o和.a文件,生成vmlinux这个ELF格式的可执行文件。 /// 4、built-in.o的生成过程 第一步:从make的过程分析我们可以知道,vmlinux可执行文件依赖变量vmlinux-deps来最终链接生成;如果要分析built--in.o从何而来,就从这个变量开始。 第二步:在顶层Makefile的第937行,有这么一条语句;sort 是排序函数,用于对 vmlinux-deps 的字符串列表进行排序,并且去掉重复的单词。 vmlinux-deps 依赖 vmlinux-dirs,vmlinux-dirs 也定义在顶层 Makefile 中 $(sort $(vmlinux-deps)): $(vmlinux-dirs) ; 第三步:变量vmlinux-dirs在顶层Makefile的第889行被定义;vmlinux-dirs 看名字就知道和目录有关,此变量保存着生成 vmlinux 所需源码文件的目录 vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \ $(core-y) $(core-m) $(drivers-y) $(drivers-m) \ $(net-y) $(net-m) $(libs-y) $(libs-m))) 将此值解析出来的结果: vmlinux-dirs = init usr arch/arm/vfp \ arch/arm/vdso arch/arm/kernel arch/arm/mm \ arch/arm/common arch/arm/probes arch/arm/net \ arch/arm/crypto arch/arm/firmware arch/arm/mach-imx\ kernel mm fs \ ipc security crypto \ block drivers sound \ firmware net arch/arm/lib \ lib 第四步:接下来看到顶层Makefile的第945行,可以看到变量vmlinux-dirs最终会执行一个语句 PHONY += $(vmlinux-dirs) $(vmlinux-dirs): prepare scripts $(Q)$(MAKE) $(build)=$@ 第五步:将上面的命令$(Q)$(MAKE) $(build)=$@解析出来,最终可以得到以下的一些语句;就是全部调用scripts/Makefile.build来生成build-in.o文件。 @ make -f ./scripts/Makefile.build obj=init @ make -f ./scripts/Makefile.build obj=usr @ make -f ./scripts/Makefile.build obj=arch/arm/vfp @ make -f ./scripts/Makefile.build obj=arch/arm/vdso @ make -f ./scripts/Makefile.build obj=arch/arm/kernel ...... @ make -f ./scripts/Makefile.build obj=sound @ make -f ./scripts/Makefile.build obj=firmware @ make -f ./scripts/Makefile.build obj=net @ make -f ./scripts/Makefile.build obj=arch/arm/lib @ make -f ./scripts/Makefile.build obj=lib 第六步:那么我们从上面的语句中随便提出一条来解析./scripts/Makefile.build编译为build-in.o的过程;文件./scripts/Makefile.build的默认目标是__build,第94行 __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \ $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \ $(subdir-ym) $(always) @: 如果只编译内核镜像的话,变量为KBUILD_MODULES空,那么语句将简化为 __build: $(builtin-target) $(lib-target) $(extra-y)) $(subdir-ym) $(always) @: 第七步:重点看一下变量builtin-target,也被定义在./scripts/Makefile.build的第86行;这个就解释了built-in.o的来源了。 ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),) builtin-target := $(obj)/built-in.o endif 第八步:在文件./scripts/Makefile.build里,变量builtin-target也作为目标,调用函数if_changed进一步生成最终目标cmd_link_o_target来链接生成built-in.o;第325行到340行 ifdef builtin-target quiet_cmd_link_o_target = LD $@ # If the list of objects to link is empty, just create an empty built-in.o cmd_link_o_target = $(if $(strip $(obj-y)),\ $(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \ $(cmd_secanalysis),\ rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@) $(builtin-target): $(obj-y) FORCE $(call if_changed,link_o_target) targets += $(builtin-target) endif # builtin-target /// 5、make zImage过程 第一步:在arch/arm/Makefile文件里,第310行这里; BOOT_TARGETS = zImage Image xipImage bootpImage uImage INSTALL_TARGETS = zinstall uinstall install PHONY += bzImage $(BOOT_TARGETS) $(INSTALL_TARGETS) $(BOOT_TARGETS): vmlinux $(Q)$(MAKE) $(build)=$(boot) MACHINE=$(MACHINE) $(boot)/$@ 第二步:上面的源命令,如果要编译zImage的话,命令$(Q)$(MAKE) $(build)=$(boot) MACHINE=$(MACHINE) $(boot)/$@展开的结果如下;所以这里又是使用./scripts/Makefile.build obj来完成vmlinux到zImage的转换。 @ make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE=arch/arm/boot/zImage /// |
/
二、Linux内核启动流程
链接脚本vmlinux.lds的解析: 这里不做详细的讲解,自己去看看 |
Linux启动流程简介:(后期具体使用Linux做开发的过程中,再去深入学习) 第一步:从./arch/arm/kernel/vmlinux.lds开始,这是一个源码链接文件,在里面找到Linux内核启动的总入口;第493行 ENTRY(stext) 第二步:stext是整个内核启动的入口地址,具体代码在./arch/arm/kernel/head.S中;第80行~第145行;接下来就来分析这些代码。 第三步:在文件./arch/arm/kernel/head.S里,stext这里开头处的注释可以知道,这里会先执行解压缩代码,将zImage解压为vmlinux后,再执行stext;且在执行之前,必须满足以下条件(这些条件由u-boot来提供) * This is normally called from the decompressor code. The requirements * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0, * r1 = machine nr, r2 = atags or dtb pointer. 第四步:具体来分析stext里面的内容。 ① 第92行,调用函数 safe_svcmode_maskall 确保 CPU 处于 SVC 模式,并且关闭了所有的中断。safe_svcmode_maskall 定义在文件 arch/arm/include/asm/assembler.h 中。(safe_svcmode_maskall r9) ② 第 94 行,读处理器 ID,ID 值保存在 r9 寄存器中。(mrc p15, 0, r9, c0, c0 @ get processor id) ③ 第95行,调用函数__lookup_processor_type 检查当前系统是否支持此 CPU,如果支持的就获取 procinfo 信息。procinfo 是 proc_info_list 类型的结构体, proc_info_list 在文件arch/arm/include/asm/procinfo.h 中(bl __lookup_processor_type @ r5=procinfo r9=cpuid);Linux 内核将每种处理器都抽象为一个 proc_info_list 结构体,每种处理器都对应一个procinfo。因此可以通过处理器 ID 来找到对应的 procinfo 结构,__lookup_processor_type 函数找到对应处理器的 procinfo 以后会将其保存到 r5 寄存器中。 ④ 第 121 行,调用函数__vet_atags 验证 atags 或设备树(dtb)的合法性。函数__vet_atags 定义在文件arch/arm/kernel/head-common.S 中。 ⑤ 第 128 行,调用函数__create_page_tables 创建页表。 ⑥ 第 137 行,将函数__mmap_switched 的地址保存到 r13 寄存器中。__mmap_switched 定义在文件 arch/arm/kernel/head-common.S,__mmap_switched 最终会调用 start_kernel 函数。 ⑦ 第 144 行 , 调 用 __enable_mmu 函 数 使 能 MMU , __enable_mmu 定 义 在 文 件arch/arm/kernel/head.S 中。__enable_mmu 最终会通过调用__turn_mmu_on 来打开 MMU,__turn_mmu_on 最后会执行 r13 里面保存的__mmap_switched 函数。 第五步:接下来,具体看看__mmap_switched函数,在arch/arm/kernel/head-common.S文件中;这个函数只要是做复制数据段、清除BSS段、设置堆栈指针、保存CPU和机器ID以及设备树地址,最后调用Linux内核C入口函数start_kernel进入内核启动的第二阶段。 第六步:打开start_kernel函数,在./init/main.c里面第492行;这个函数开始,是Linux启动的第二阶段,由于start_kernel 函数里面调用的子函数太多,而这些子函数又很复杂,因此我们简单的来看一下一些重要的子函数。 asmlinkage __visible void __init start_kernel(void) { char *command_line; char *after_dashes; lockdep_init(); /* lockdep 是死锁检测模块,此函数会初始化 * 两个 hash 表。此函数要求尽可能早的执行! */ set_task_stack_end_magic(&init_task);/* 设置任务栈结束魔术数, *用于栈溢出检测 */ smp_setup_processor_id(); /* 跟 SMP 有关(多核处理器),设置处理器 ID。 * 有很多资料说 ARM 架构下此函数为空函数,那是因 * 为他们用的老版本 Linux,而那时候 ARM 还没有多 * 核处理器。 */ debug_objects_early_init(); /* 做一些和 debug 有关的初始化 */ boot_init_stack_canary(); /* 栈溢出检测初始化 */ cgroup_init_early(); /* cgroup 初始化,cgroup 用于控制 Linux 系统资源*/ local_irq_disable(); /* 关闭当前 CPU 中断 */ early_boot_irqs_disabled = true; /* * 中断关闭期间做一些重要的操作,然后打开中断 */ boot_cpu_init(); /* 跟 CPU 有关的初始化 */ page_address_init(); /* 页地址相关的初始化 */ pr_notice("%s", linux_banner);/* 打印 Linux 版本号、编译时间等信息 */ setup_arch(&command_line); /* 架构相关的初始化,此函数会解析传递进来的 * ATAGS 或者设备树(DTB)文件。会根据设备树里面 * 的 model 和 compatible 这两个属性值来查找 * Linux 是否支持这个单板。此函数也会获取设备树 * 中 chosen 节点下的 bootargs 属性值来得到命令 * 行参数,也就是 uboot 中的 bootargs 环境变量的 * 值,获取到的命令行参数会保存到 *command_line 中。 */ mm_init_cpumask(&init_mm); /* 看名字,应该是和内存有关的初始化 */ setup_command_line(command_line); /* 好像是存储命令行参数 */ setup_nr_cpu_ids(); /* 如果只是 SMP(多核 CPU)的话,此函数用于获取 * CPU 核心数量,CPU 数量保存在变量 * nr_cpu_ids 中。 */ setup_per_cpu_areas(); /* 在 SMP 系统中有用,设置每个 CPU 的 per-cpu 数据 */ smp_prepare_boot_cpu(); build_all_zonelists(NULL, NULL); /* 建立系统内存页区(zone)链表 */ page_alloc_init(); /* 处理用于热插拔 CPU 的页 */ /* 打印命令行信息 */ pr_notice("Kernel command line: %s\n", boot_command_line); parse_early_param(); /* 解析命令行中的 console 参数 */ after_dashes = parse_args("Booting kernel",static_command_line, __start___param,__stop___param - __start___param,-1, -1, &unknown_bootoption); if (!IS_ERR_OR_NULL(after_dashes)) parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,set_init_arg); jump_label_init(); setup_log_buf(0); /* 设置 log 使用的缓冲区*/ pidhash_init(); /* 构建 PID 哈希表,Linux 中每个进程都有一个 ID, * 这个 ID 叫做 PID。通过构建哈希表可以快速搜索进程 * 信息结构体。 */ vfs_caches_init_early(); /* 预先初始化 vfs(虚拟文件系统)的目录项和 * 索引节点缓存 */ sort_main_extable(); /* 定义内核异常列表 */ trap_init(); /* 完成对系统保留中断向量的初始化 */ mm_init(); /* 内存管理初始化 */ sched_init(); /* 初始化调度器,主要是初始化一些结构体 */ preempt_disable(); /* 关闭优先级抢占 */ if (WARN(!irqs_disabled(), /* 检查中断是否关闭,如果没有的话就关闭中断 */ "Interrupts were enabled *very* early, fixing it\n")) local_irq_disable(); idr_init_cache(); /* IDR 初始化,IDR 是 Linux 内核的整数管理机 * 制,也就是将一个整数 ID 与一个指针关联起来。 */ rcu_init(); /* 初始化 RCU,RCU 全称为 Read Copy Update(读-拷贝修改) */ trace_init(); /* 跟踪调试相关初始化 */ context_tracking_init(); radix_tree_init(); /* 基数树相关数据结构初始化 */ early_irq_init(); /* 初始中断相关初始化,主要是注册 irq_desc 结构体变 * 量,因为 Linux 内核使用 irq_desc 来描述一个中断。 */ init_IRQ(); /* 中断初始化 */ tick_init(); /* tick 初始化 */ rcu_init_nohz(); init_timers(); /* 初始化定时器 */ hrtimers_init(); /* 初始化高精度定时器 */ softirq_init(); /* 软中断初始化 */ timekeeping_init(); time_init(); /* 初始化系统时间 */ sched_clock_postinit(); perf_event_init(); profile_init(); call_function_init(); WARN(!irqs_disabled(), "Interrupts were enabled early\n"); early_boot_irqs_disabled = false; local_irq_enable(); /* 使能中断 */ kmem_cache_init_late(); /* slab 初始化,slab 是 Linux 内存分配器 */ console_init(); /* 初始化控制台,之前 printk 打印的信息都存放 * 缓冲区中,并没有打印出来。只有调用此函数 * 初始化控制台以后才能在控制台上打印信息。 */ if (panic_later) panic("Too many boot %s vars at `%s'", panic_later, panic_param); lockdep_info(); /* 如果定义了宏 CONFIG_LOCKDEP,那么此函数打印一些信息。*/ locking_selftest() /* 锁自测 */ ...... page_ext_init(); debug_objects_mem_init(); kmemleak_init(); /* kmemleak 初始化,kmemleak 用于检查内存泄漏 */ setup_per_cpu_pageset(); numa_policy_init(); if (late_time_init) late_time_init(); sched_clock_init(); calibrate_delay(); /* 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能 * BogoMIPS 设置越大,说明 CPU 性能越好。 */ pidmap_init(); /* PID 位图初始化 */ anon_vma_init(); /* 生成 anon_vma slab 缓存 */ acpi_early_init(); ...... thread_info_cache_init(); cred_init(); /* 为对象的每个用于赋予资格(凭证) */ fork_init(); /* 初始化一些结构体以使用 fork 函数 */ proc_caches_init(); /* 给各种资源管理结构分配缓存 */ buffer_init(); /* 初始化缓冲缓存 */ key_init(); /* 初始化密钥 */ security_init(); /* 安全相关初始化 */ dbg_late_init(); vfs_caches_init(totalram_pages); /* 为 VFS 创建缓存 */ signals_init(); /* 初始化信号 */ page_writeback_init(); /* 页回写初始化 */ proc_root_init(); /* 注册并挂载 proc 文件系统 */ nsfs_init(); cpuset_init(); /* 初始化 cpuset,cpuset 是将 CPU 和内存资源以逻辑性 * 和层次性集成的一种机制,是 cgroup 使用的子系统之一 */ cgroup_init(); /* 初始化 cgroup */ taskstats_init_early(); /* 进程状态初始化 */ delayacct_init(); check_bugs(); /* 检查写缓冲一致性 */ acpi_subsystem_init(); sfi_init_late(); if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_late_init(); efi_free_boot_services(); } ftrace_init(); rest_init(); /* rest_init 函数 */ } 第七步:start_kernel 函数最后调用了 rest_init,接下来简单看一下 rest_init函数;在./init/main.c的第383行。 ① 第 387 行,调用函数 rcu_scheduler_starting,启动 RCU 锁调度器。 ② 第 394 行,调用函数 kernel_thread 创建 kernel_init 进程,也就是大名鼎鼎的 init 内核进程。init 进程的 PID 为 1。init 进程一开始是内核进程(也就是运行在内核态),后面 init 进程会在根文件系统中查找名为“init”这个程序,这个“init”程序处于用户态,通过运行这个“init”程序,init 进程就会实现从内核态到用户态的转变。 ③ 第 396 行,调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。kthreadd进程负责所有内核进程的调度和管理。 ④ 第 409 行,最后调用函数 cpu_startup_entry 来进入 idle 进程,cpu_startup_entry 会调用cpu_idle_loop,cpu_idle_loop 是个 while 循环,也就是 idle 进程代码。idle 进程的 PID 为 0,idle进程叫做空闲进程,如果学过 FreeRTOS 或者 UCOS 的话应该听说过空闲任务。idle 空闲进程就和空闲任务一样,当 CPU 没有事情做的时候就在 idle 空闲进程里面“瞎逛游”,反正就是给CPU 找点事做。当其他进程要工作的时候就会抢占 idle 进程,从而夺取 CPU 使用权。其实大家应该可以看到 idle 进程并没有使用 kernel_thread 或者 fork 函数来创建,因为它是有主进程演变而来的。 第八步:接下来重点关注一下PID=1的进程--kernel_init函数,被定义实现在./init/main.c的第928行。它是rest_init的主要工作内容。 ① 第 932 行,kernel_init_freeable 函数用于完成 init 进程的一些其他初始化工作,稍后再来具体看一下此函数。 ② 第 942 行,ramdisk_execute_command 是一个全局的 char 指针变量,此变量值为“/init”,也就是根目录下的 init 程序。ramdisk_execute_command 也可以通过 uboot 传递,在 bootargs 中使用“rdinit=xxx”即可,xxx 为具体的 init 程序名字。 ③ 第 943 行,如果存在“/init”程序的话就通过函数 run_init_process 来运行此程序。 ④ 第 956 行,如果 ramdisk_execute_command 为空的话就看 execute_command 是否为空,反正不管如何一定要在根文件系统中找到一个可运行的 init 程序。execute_command 的值是通过uboot 传递,在 bootargs 中使用“init=xxxx”就可以了,比如“init=/linuxrc”表示根文件系统中的 linuxrc 就是要执行的用户空间 init 程序。 ⑤ 第 963~966 行,如果 ramdisk_execute_command 和 execute_command 都为空,那么就依次查找“/sbin/init”、“/etc/init”、“/bin/init”和“/bin/sh”,这四个相当于备用 init 程序,如果这四个也不存在,那么 Linux 启动失败! ⑥ 第 969 行,如果以上步骤都没有找到用户空间的 init 程序,那么就提示错误发生! 第九步:具体来看看kernel_init_freeable这个函数,这个函数被kernel_init调用来初始化一些init进程的一些工作;在./init/main.c文件按第973行。 ① 第 1002 行,do_basic_setup 函数用于完成 Linux 下设备驱动初始化工作!非常重要。do_basic_setup 会调用 driver_init 函数完成 Linux 下驱动模型子系统的初始化。 ② 第 1005 行,打开设备“/dev/console”,在 Linux 中一切皆为文件!因此“/dev/console”也是一个文件,此文件为控制台设备。每个文件都有一个文件描述符,此处打开的“/dev/console”文件描述符为 0,作为标准输入(0)。 ③ 第 1008 和 1009 行,sys_dup 函数将标准输入(0)的文件描述符复制了 2 次,一个作为标准输出(1),一个作为标准错误(2)。这样标准输入、输出、错误都是/dev/console 了。console 通过uboot 的 bootargs 环境变量设置,“console=ttymxc0,115200”表示将/dev/ttymxc0 设置为 console,也就是 I.MX6U 的串口 1。当然,也可以设置其他的设备为 console,比如虚拟控制台 tty1,设置 tty1 为 console 就可以在 LCD 屏幕上看到系统的提示信息。 ④ 第 1020 行,调用函数 prepare_namespace 来挂载根文件系统。跟文件系统也是由命令行参数指定的,也就是 uboot 的 bootargs 环境变量。比如“root=/dev/mmcblk1p2 rootwait rw”就表示根文件系统在/dev/mmcblk1p2 中,也就是 EMMC 的分区 2 中。 / 借助韦中山老师的书籍来总结: |
/
三、Linux内核的移植与修改
Linux内核的简单移植: 基于NXP官方的Linux包移植到正点原子的开发板。 第一步:以半导体厂家提供的Linux内核作为蓝本,因为通常情况下,在设计开发板时,都会去参考官方的开发板。 第二步:在arm/arm/configs下添加自己的xxx_defconfig文件,可以直接复制半导体厂家提供的,后期再根据自己的外设驱动作修改。 第三步:在arch/arm/boot/dts/中添加开发板对应的设备树文件xxx.dts,同样可以直接复制半导体厂家提供的,后期再做添加或修改。然后修改arch/arm/boot/dts下的Makefile,添加新增的dts文件参与编译。 / 第四步:通过修改配置文件xxx_defconfig或make menuconfig菜单,选择CPU运行的频率;这里CPU的运行频率有几种模式。 Performance,最高性能,直接用最高频率,不考虑耗电。 Interactive,一开始直接用最高频率,然后根据 CPU 负载慢慢降低。 Powersave,省电模式,通常以最低频率运行,系统性能会受影响,一般不会用这个! Userspace,可以在用户空间手动调节频率。 Ondemand,定时检查负载,然后根据负载来调节频率。负载低的时候降低 CPU 频率,这样省电,负载高的时候提高 CPU 频率,增加性能。 第五步:针对有EMMC的开发板,将EMMC带宽从4线模式调整为8线模式;这里只需要修好设备树上相关的描述即可。 第六步:修改PHY网络网卡驱动,匹配开发板的网卡;修改设备树描述文件以外,对应的PHY的驱动代码也需要修改。 ① 打开设备树文件,根据具体的PHY引脚的定义;这里要删除SPI4对PHY复位引脚的占用。 ② 在节点iomuxc_snvs里(也就是GPIO基本配置描述节点)添加对引脚电气特性配置的描述。 ③ 在网卡fecx节点上,引用上面这两个配置,这样才可以实际上被初始化配置到。 ④ 根据具体的PHY卡,配置fecx节点,指定PHY的地址和使用的具体驱动代码等。 ⑤ 接下来就要去修改网卡的驱动代码了,一个是通用driver挂载时的,一个是device具体设备的驱动代码。 第七步:根据LCD屏幕的选型,修改匹配LCD屏幕的显示;这个只需要修改设备树对应内容即可。 / 个人规律总结: 1、设备树其实就是对个各种设备对应的GPIO引脚和参数信息的描述,会被Linux定义的驱动程序使用来驱动对应的外设。类似于数据库。 2、通过Kconfig可以找到对应的设备具体的驱动源码和路径。 bootcmd=mmc dev 1;fatload mmc 1:1 80800000 zimage;fatload mmc 1:1 83000000 imx6ull-14x14-emmc-4.3-800x480-c.dtb;bootz 80800000 - 83000000; bootargs=console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw |
/
四、根文件系统的构建
根文件系统的基本目录:(遵循FHS标准) 1、/bin目录:看到“bin”大家应该能想到 bin 文件,bin 文件就是可执行文件。所以此目录下存放着系统需要的可执行文件,一般都是一些命令,比如 ls、mv 等命令。此目录下的命令所有的客户都可以使用。 2、/dev目录:dev 是 device 的缩写,所以此目录下的文件都是和设备有关的,此目录下的文件都是设备文件。在 Linux 下一切皆文件,即使是硬件设备,也是以文件的形式存在的,比如/dev/ttymxc0(I.MX6ULL 根目录会有此文件)就表示 I.MX6ULL 的串口 0,我们要想通过串口 0发送或者接收数据就要操作文件/dev/ttymxc0,通过对文件/dev/ttymxc0 的读写操作来实现串口0 的数据收发。 3、/etc目录:此目录下存放着各种配置文件,大家可以进入 Ubuntu 的 etc 目录看一下,里面的配置文件非常多!但是在嵌入式 Linux 下此目录会很简洁。 4、/mnt目录:临时挂载目录,一般是空目录,可以在此目录下创建空的子目录,比如/mnt/sd、/mnt/usb,这样就可以将 SD 卡或者 U 盘挂载到/mnt/sd 或者/mnt/usb 目录中。 5、/lib目录:lib 是 library 的简称,也就是库的意思,因此此目录下存放着 Linux 所必须的库文件。这些库文件是共享库,命令和用户编写的应用程序要使用这些库文件。 6、/proc目录:此目录一般是空的,当 Linux 系统启动以后会将此目录作为 proc 文件系统的挂载点,proc是个虚拟文件系统,没有实际的存储设备。proc 里面的文件都是临时存在的,一般用来存储系统运行信息文件。 7、/usr目录:usr 不是 user 的缩写,而是 Unix Software Resource 的缩写,也就是 Unix 操作系统软件资源目录。这里有个小知识点,那就是 Linux 一般被称为类 Unix 操作系统,苹果的 MacOS也是类 Unix 操作系统。既然是软件资源目录,因此/usr 目录下也存放着很多软件,一般系统安装完成以后此目录占用的空间最多。 8、/var目录:此目录存放一些可以改变的数据。 9、/sbin目录:此目录页用户存放一些可执行文件,但是此目录下的文件或者说命令只有管理员才能使用,主要用户系统管理。 10、/sys目录:系统启动以后此目录作为 sysfs 文件系统的挂载点,sysfs 是一个类似于 proc 文件系统的特殊文件系统,sysfs 也是基于 ram 的文件系统,也就是说它也没有实际的存储设备。此目录是系统设备管理的重要目录,此目录通过一定的组织结构向用户提供详细的内核数据结构信息。 11、/opt目录:可选的文件、软件存放区,由用户选择将哪些文件或软件放到此目录中。 /// Linux的文件属性介绍: |
BusyBox编译安装流程 第一步:创建一个目录用于存放编译出来的系统文件目录; 第二步:下载BusyBox源码并在Ubuntu下解压,修改Makefile文件;在里面指定使用的交叉编译器和架构(第164行和第190行)。 第三步:通过修改源码,让BusyBox支持中文字符;打开源码文件 busybox-1.29.0/libbb/printable_string.c,找到函数 printable_string,屏蔽掉第33行和34行内容,修改第48行的内容;打开源码文件 busybox-1.29.0/libbb/unicode.c,找到第1022行和1033行做类似的修改。 第四步:指令make defconfig使用默认配置来配置BusyBox。 第五步:指令make menuconfig打开配置菜单,根据需要修改一些配置; ① 取消静态编译,因为BusyBox静态编译会很大而且存在一些奇怪的问题,配置路径如下: ② 选中打开vi格式的编辑器风格,配置选项如下: ③ 取消选中简化模块选项,配置路径如下: ④ 启动支持mdev工具,这样才可以支持dev目录和热拔插设备,也可以挂载网络文件系统,配置路径如下: ⑤ 选中支持ucode中文字符集,配置路径如下: 第六步:指令make编译BusyBox。 第七步:指令make install CONFIG_PREFIX=xxx将编译出来的文件系统安装到指定的路径xxx /// glibc库的移植和使用 Linux 中的应用程序一般都是需要动态库的,当然你也可以编译成静态的,但是静态的可执行文件会很大。如果编译为动态的话就需要动态库,所以我们需要向根文件系统中添加动态库。lib 库文件从交叉编译器中获取,库的位置在交叉编译器所在目录的./arm-linux-gnueabihf/libc/lib里面。 1、在目录里的文件可以分为8类型: ① 加载器ld-2.21.so* 和ld-linux-armhf.so.3*;动态程序启动前,他们被用来加载动态库。 ② 目标文件(.o);在编译应用程序时,这些.o文件会像一般文件一样被链接。 ③ 静态库文件(.a);在编译静态程序的时候需要连接他们。 ④ 动态库文件(.so/.so.[0-9]);编译出来的动态链接程序在运行时,就需要实时的链接他们。 ⑤ libtool库文件(.la);在链接库文件时,这些文件会被用到,可以用来指定程序所链接的库又需要链接哪些库,程序运行时不需要使用。 ⑥ gconv目录;里面是有头字符集的目录,比如ISO8859-1.so等。 ⑦ ldscripts目录;里面是各种的链接脚本,在编译应用程序时,他们被用于指定程序的运行地址、各段的位置等 ⑧ 其他的文件目录 2、移植glibc库到开发板: 开发板只需要用到加载器文件和动态库文件,我们先在安装了文件系统的目录下创建一个新目录lib,用于存放移植过来的lib动态库。 ① 命令 cd xxx/arm-linux-gnueabihf/libc/lib 进入交叉编译器的库所在路径; ② 命令 cp ./*so* ./*.a /xxx/rootfs/lib -d 将所有的依赖库全部拷贝到刚才创建好的lib目录下,-d代表将软连接提取为本尊。 ③ 在文件系统目录的./usr/下再创建一个lib目录; ④ 命令 cd xxx/arm-linux-gnueabihf/libc/usr/lib 进入到交叉编译器的另一个存放库的路径下; ⑤ 命令 cp ./*so* ./*.a /xxx/rootfs/usr/lib -d 将这些库拷贝到文件系统的usr/lib目录里; /// 完善根文件系统 1、创建其他文件夹:在安装好的BusyBox路径下,还需要进一步创建一些目录才可以形成一个基本的根文件系统;需要再创建 dev、proc、mnt、sys、tmp 和 root。 2、创建etc这个关键的目录:Linux内核启动后,会创建第一个内核进程ID=1,接下来会通过这个进程搜索根文件系统下的init应用程序并执行,从而实现内核态到用户态的切换;而init进程需要根据etc目录下的相关文件来运行一些其他的应用程序,包括基本的shell命令功能。 ① 创建/etc/init.d/rcS 文件;rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件 的脚本文件。内容如下: 解释: 第 1 行,表示这是一个 shell 脚本。 第 3 行,PATH 环境变量保存着可执行文件可能存在的目录,这样我们在执行一些命令或者可执行文件的时候就不会提示找不到文件这样的错误。 第 4 行,LD_LIBRARY_PATH 环境变量保存着库文件所在的目录 第 5 行,使用 export 来导出上面这些环境变量,相当于声明一些“全局变量” 第 7 行,使用 mount 命令来挂载所有的文件系统,这些文件系统由文件/etc/fstab 来指定,所以我们一会还要创建/etc/fstab 文件 第 8 和 9 行,创建目录/dev/pts,然后将 devpts 挂载到/dev/pts 目录中 第 11 和 12 行,使用 mdev 来管理热插拔设备,通过这两行,Linux 内核就可以在/dev 目录下自动创建设备节点。关于 mdev 的详细内容可以参考 busybox 中的 docs/mdev.txt 文档。 ② 创建/etc/fstab 文件;在 rootfs中创建/etc/fstab 文件,fstab在Linux开机以后自动配置哪些需要自动挂载的分区,格式如下: 解释: <file system>:要挂载的特殊的设备,也可以是块设备,比如/dev/sda 等等 <mount point>:挂载点 <type>:文件系统类型,比如 ext2、ext3、proc、romfs、tmpfs 等等 <options>:挂载选项,在 Ubuntu 中输入“man mount”命令可以查看具体的选项。一般使用 defaults,也就是默认选项,defaults 包含了 rw、suid、 dev、 exec、 auto、 nouser 和 async <dump>:为 1 的话表示允许备份,为 0 不备份,一般不备份,因此设置为 0 <pass>:磁盘检查设置,为 0 表示不检查。根目录‘/’设置为 1,其他的都不能设置为 1,其他的分区从 2 开始。一般不在 fstab 中挂载根目录,因此这里一般设置为 0 例子: ③ 创建/etc/inittab 文件;init进程会根据这个文件里面的条目信息,创建其他应用进程,inittab 由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的 4 个段组成,格式如下: 解释: <id>:每个指令的标识符,不能重复。但是对于 busybox 的 init 来说,<id>有着特殊意义。对于 busybox 而言<id>用来指定启动进程的控制 tty,一般我们将串口或者 LCD 屏幕设置为控制 tty。 <runlevels> :对 busybox 来说此项完全没用,所以空着。 <action>:动作,用于指定<process>可能用到的动作。Busybox 支持的动作如表: <process> :具体的动作,比如程序、脚本或命令等。 例子: /// |
实现软件的开机自启动 方案一:修改etc/init.d/rcS文件。在文件里添加启动软件的Shell命令。 配置开发板网络域名解析服务器 1、在/etc目录下创建文件resolv.conf; 2、在resolv.conf中写入配置项如下:
|
/
五、Linux设备驱动
Linux驱动开发的概述: 1、以驱动一个LED为例,一个软件系统分为4层:应用程序、库、操作系统(内核)、驱动程序;这4层的关系如下: ① 应用程序使用库提供的open函数打开代表LED的设备文件; ② 库根据“open”函数传入的参数执行“swi”指令,这条指令会引起CPU异常,进入内核(中断机制); ③ 内核的异常处理函数根据这些参数找到相应的驱动程序,返回一个文件句柄给库,进而返回给应用程序; ④ 应用程序得到文件句柄后,使用库提供的write或ioctl函数发出控制命令; ⑤ 库根据write或ioctl函数传入的参数再次执行“swi”指令,又一次引发CPU异常进入内核; ⑥ 内核的异常处理函数再次根据这些参数调用驱动程序的相关函数,点亮LED。 2、Linux的外设可以分为3类:字符设备、块设备和网络接口设备;Linux驱动程序的开发步骤: ① 查看原理图、数据手册,了解设备的操作方法; ② 在内核中找到相近的驱动程序,以它为模板进行开发,有时侯需要从零开始; ③ 实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序; ④ 设计所要实现的具体操作,比如open、close、read、write等函数; ⑤ 实现内核驱动中断服务函数(这个不是必须的); ⑥ 编译该驱动程序到内核中,或者用insmod命令加载; ⑦ 测试驱动程序。 /// Linux驱动模块的加载与卸载: 方式一:固定两个函数,init_module和cleanup_module; 方式二:使用Linux提供的宏定义,module_init(my_init)和module_exit(my_cleanup); 开发好的驱动模块,可以通过指令来动态挂载和卸载: ① inmod xxx.ko 和 rmmod xxx.ko 这种方式只能单独操作某个模块,不能智能识别模块间的依赖关系 ② modprobe xxx.ko 和 modprobe -r xxx.ko 这种指令会智能的检查挂载被依赖的其他模块且具备错误检查的功能 /// Linux内核打印调试技术 1、printk函数的打印等级划分: 2、内核里的四个默认级别属性: 这几个宏的含义和作用: ① 对于调用printk函数设置打印级别,只有设置级别小于console_loglevel,才可以在控制台打印出来 ② printk函数的默认打印等级为default_message_loglevel; ③ minimum_console_loglevel是系统当前允许设置的最高打印等级 ④ default_console_loglevel是系统默认的内核打印等级 修改和查看这四个值:通过命令cat /proc/sys/kernel/printk 可以查看当前这四个宏的值;命令 echo "1 2 3 4" > /proc/sys/kernel/printk 可以修改这四个宏的值。 3、开机之后,打印启动过程的信息: 命令 dmesg ;相当于将printk打印到缓冲区的信息通过控制台打印出来。 /// Linux内核源码级别的调试技术 1、内核在线调试工具KGDB: 2、Oops错误信息及回溯定位: /// |