按照以下思路大概总结下对linux内核4.14.2总体框架的认识
1、内核是由哪些文件组成的
2、内核的编译体系是怎么样的,是怎么编译链接起来的
3、内核的启动流程,在启动过程中大致做了哪些工作
4、通过对exynos4412开发板上移植linux内核4.14.2验证上述分析
5、编译出uImage后,是怎么被uboot加载运行起来的
一、linux内核4.14.2是由哪些文件组成的
1. arch目录
这个目录是体系结构相关的代码,里面每一个目录对应一种架构CPU,比如arm架构arch/arm和mips架构arch/mips等;
2. block目录
这个目录下存放关于块设备(比如SD卡、iNand、Nand、硬盘等块设备)管理的一些通用代码,是上层逻辑的代码和底层具体驱动代码无关,是体系架构无关的代码;
3. crypto目录
存放常用的加密、散列、压缩和CRC校验等算法;
4. drivers目录
这个目录里面存放了linux内核支持的所有设备驱动程序,里面每一个目录对应一类设备驱动,比如mmc驱动drivers/mmc,led驱动drivers/leds,pci驱动drivers/pci,NOR Flash和NAND Flash驱动drivers/mtd等;
5、fs目录
里面是linux内核支持的所有文件系统代码,比如fs/fat、fs/ext4等;
6、include目录
存放的是所有CPU架构通用的头文件,比如基本头文件include/linux,字符驱动通用的结构体头文件就放在这个目录下,如include/linux/cdev.h头文件,比如各种驱动通用的头文件,如网络驱include/net;每种CPU架构特有的头文件在目录arch/arm/include目录下;
7、init目录
这个目录存放linux内核启动时初始化内核的代码;
8、ipc目录
里面是linux内核支持的进程间通信机制的实现代码,比如消息队列、共享内存、信号量等;
9、kernel目录
这个目录是内核管理的核心代码,其中和CPU体系架构相关的代码在arch/arm/kernel目录下;
10、lib目录
里面是一些公用的库函数,比如string.c、crc32.c等,在内核中是不能使用C语言的库函数,这些库函数是内核编程中用来替代C库函数的;其中和CPU体系架构相关的库函数代码在arch/arm/lib目录,里面有比如延时的代码arch/arm/lib/delay.c;
11、mm目录
内存管理相关的代码,比如伙伴系统代码的实现;
12、net目录
网络相关的代码,比如mac80211协议,tcp/ip协议,netfilter等的实现代码;
13、scripts目录
里面存放的是用来辅助对linux内核进行配置编译生产的脚本文件;
14、security目录
安全相关的代码;
15、sound目录
音频设备驱动代码;
16、usr目录
目录下是initramfs相关的,和linux内核的启动有关;
17、virt目录
内核虚拟机相关的;virt/kvm,内核虚拟机;
二、内核的编译体系是怎么样的,是怎么编译链接起来的
具体参考:
1、内核文档Documentation/kbuild/makefiles.txt
2、韦东山《嵌入式linux应用开发》
内核提供图形化的配置方式,通过执行 make menuconfig读取各个目录下的配置文件Kconfig将配置选项通过图形界面的形式提供给我们使用,Kconfig文件中包含了所有可配置模块的信息,我们可以在图形化界面中配置模块是否被编译进内核以及模块的编译方式(比如编译成目标文件或者编译成模块)等,图形化配置完成后,生成.config文件供Makefile使用,Makefile通过.config就可以知道各个模块是否被编译以及编译的方式等。
内核编译体系由5个部分组成:
1、Makefile:顶层Makefile,总体上控制着内核的编译链接
2、.config:内核配置结果
3、arch/$(ARCH)/Makefile:对应体系结构的Makefile
4、scripts/Makefile.*:提供通用的编译规则给所有kbuild Makefiles
5、kbuild Makefiles:各个模块文件夹下的Makefile(有的目录下是kbuid,当两者同时存在时,优先使用kbuild),根据.config文件将本目录下的文件编译成目标文件或模块供主Makefile链接
内核整体的编译流程由主目录下的Makefile控制,主Makefile将子目录分成6类:init-y、drivers-y、net-y、libs-y、core-y、virt-y
arch目录通过arch/arm/Makefile被包含进内核,主Makefile通过如下包含这个Makefile
include arch/$(SRCARCH)/Makefile
arch/arm/Makefile控制arch中各模块的编译规则,同样的对arch目录下的各子目录进行和主Makefile同样的处理,如下,新定义了一个head-y目标(这个目标是内核启动时的入口,因此链接时要放在最前面),然后将各子目录添加到上述6类对应的类中
编译过程中,会分别进到head-y、init-y、drivers-y、net-y、libs-y、core-y、virt-y所列出的子目录下执行对应目录下的Makefile,最后每个子目录下都会生成一个built-in.o(生成built-in.o的过程有点复杂,还要调用scripts目录下的脚本)或者lib.a(libs-y所列的目录下会生成lib.a文件),最后Makefile会调用链接脚本将head-y、init-y、drivers-y、net-y、libs-y、core-y、virt-y按顺序链接成内核映像文件vmlinux。
内核的链接脚本:内核的链接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S(arch\arm\kernel\vmlinux.lds.S),然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds。
三、内核的启动流程,在启动过程中大致做了哪些工作
内核的启动大致可以分为两个阶段,第一阶段由汇编函数编写,第二阶段由C语言编写。
第一阶段大致完成的工作:
通过CPU的ID判断内核是否支持当前CPU、检查u-boot传过来的r2寄存器中的atags或者设备树参数是否合法、创建初始粗页表并使能MMU、最后调用start_kernel进入内核启动的C语言阶段。
第二阶段大致完成的工作:
第二阶段的工作由start_kernel函数完成,该阶段会对内核的各个子系统进行初始化,比如内存管理、调度管理、中断子系统等,加载各类驱动,创建内核线程,挂载根文件系统,通过加载根文件系统中的init进程进入用户态等。
第一阶段大致完成的工作:
1、由链接脚本arch/arm/kernel/vmlinux.lds确定内核入口点ENTRY(stext),
ENTRY(stext)在arch/arm/kernel/head.s文件中
2、设置SVC模式
3、检查内核是否支持当前的CPU
arch/arm/kernel/head-common.S |
/* 这时还没有使能MMU,不能使用虚拟地址,链接脚本决定的都是虚拟地址,需要转换 * 成物理地址使用 * 这里主要确定proc_info_list 结构体的物理地址,这些结构体表示内核支持的CPU架构 */ __lookup_processor_type: adr r3, __lookup_processor_type_data ldmia r3, {r4 - r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b mov r5, #0 @ unknown processor 2: ret lr ENDPROC(__lookup_processor_type) |
4、判断u-boot通过r2寄存器传过来的参数是否合法,r2有可能是atags参数或者设备树dtb的首地址,在__vet_atags函数判断r2是atags或者设备树参数,并检查合法性
arch/arm/kernel/head-common.S |
/* * Exception handling. Something went wrong and we can‘t proceed. We * ought to tell the user, but since we don‘t have any guarantee that * we‘re even running on the right architecture, we do virtually nothing. * * If CONFIG_DEBUG_LL is set we try to print out something about the error * and hope for the best (useful if bootloader fails to pass a proper * machine ID for example). */ __HEAD
/* Determine validity of the r2 atags pointer. The heuristic requires * that the pointer be aligned, in the first 16k of physical RAM and * that the ATAG_CORE marker is first and present. If CONFIG_OF_FLATTREE * is selected, then it will also accept a dtb pointer. Future revisions * of this function may be more lenient with the physical address and * may also be able to move the ATAGS block if necessary. * * Returns: * r2 either valid atags pointer, valid dtb pointer, or zero * r5, r6 corrupted */ __vet_atags: tst r2, #0x3 @ aligned? bne 1f
ldr r5, [r2, #0] #ifdef CONFIG_OF_FLATTREE ldr r6, =OF_DT_MAGIC @ is it a DTB? cmp r5, r6 beq 2f #endif cmp r5, #ATAG_CORE_SIZE @ is first tag ATAG_CORE? cmpne r5, #ATAG_CORE_SIZE_EMPTY bne 1f ldr r5, [r2, #4] ldr r6, =ATAG_CORE cmp r5, r6 bne 1f
2: ret lr @ atag/dtb pointer is ok
1: mov r2, #0 ret lr ENDPROC(__vet_atags) |
5、__create_page_tables创建初始粗页表,以使得内核可以使用虚拟地址,在内核启动后期还会创建一个更精细的页表
6、将__mmap_switched函数指针存入r13寄存器,这个函数中负责跳转到kernel启动的C语言阶段start_kernel
arch/arm/kernel/head-common.S |
/* * The following fragment of code is executed with the MMU on in MMU mode, * and uses absolute addresses; this is not position independent. * * r0 = cp#15 control register * r1 = machine ID * r2 = atags/dtb pointer * r9 = processor ID */ __INIT __mmap_switched: adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b
mov fp, #0 @ Clear BSS (and zero fp) 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp}) THUMB( ldmia r3, {r4, r5, r6, r7} ) THUMB( ldr sp, [r3, #16] ) str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type str r2, [r6] @ Save atags pointer cmp r7, #0 strne r0, [r7] @ Save control register values b start_kernel ENDPROC(__mmap_switched) |
7、进入__enable_mmu使能mmu并跳转到__turn_mmu_on
arch/arm/kernel/head,S |
__enable_mmu: #if defined(CONFIG_ALIGNMENT_TRAP) && __LINUX_ARM_ARCH__ < 6 orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE bic r0, r0, #CR_I #endif #ifdef CONFIG_ARM_LPAE mcrr p15, 0, r4, r5, c2 @ load TTBR0 #else mov r5, #DACR_INIT mcr p15, 0, r5, c3, c0, 0 @ load domain access register mcr p15, 0, r4, c2, c0, 0 @ load page table pointer #endif b __turn_mmu_on ENDPROC(__enable_mmu) |
8、进入__turn_mmu_on函数,并通过r13寄存器保存的__mmap_switched函数地址进入__mmap_switched函数,最终在__mmap_switched函数中跳转到内核启动的C语言阶段
arch/arm/kernel/head,S |
/* * Enable the MMU. This completely changes the structure of the visible * memory space. You will not be able to trace execution through this. * If you have an enquiry about this, *please* check the linux-arm-kernel * mailing list archives BEFORE sending another post to the list. * * r0 = cp#15 control register * r1 = machine ID * r2 = atags or dtb pointer * r9 = processor ID * r13 = *virtual* address to jump to upon completion * * other registers depend on the function called upon completion */ .align 5 .pushsection .idmap.text, "ax" ENTRY(__turn_mmu_on) mov r0, r0 instr_sync mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg instr_sync mov r3, r3 mov r3, r13 ret r3 __turn_mmu_on_end: ENDPROC(__turn_mmu_on) .popsection |
第二阶段大致完成的工作:
第二阶段的工作在start_kernel函数中完成,比较重要的几个工作如下
1、第二阶段的入口点是start_kernel函数,定义在init/main.c中
2、打印内核版本信息pr_notice("%s", linux_banner),这个打印信息只是放到缓存,需要控制台初始化完才会打印出来
其中linux_banner定义在init/version.c中如下:
3、setup_arch这是一个启动过程中和体系架构相关的函数,进行一些体系结构相关的创建过程;这个函数定义在arch/arm/kernel/setup.c文件中
setup_arch中调用setup_machine_fdt获取机器描述符machine_desc,
arch/arm/include/asm/mach/arch.h |
struct machine_desc { unsigned int nr; /* architecture number */ const char *name; /* architecture name */ unsigned long atag_offset; /* tagged list (relative) */ const char *const *dt_compat; /* array of device tree * ‘compatible‘ strings */
unsigned int nr_irqs; /* number of IRQs */
#ifdef CONFIG_ZONE_DMA phys_addr_t dma_zone_size; /* size of DMA-able area */ #endif
unsigned int video_start; /* start of video RAM */ unsigned int video_end; /* end of video RAM */
unsigned char reserve_lp0 :1; /* never has lp0 */ unsigned char reserve_lp1 :1; /* never has lp1 */ unsigned char reserve_lp2 :1; /* never has lp2 */ enum reboot_mode reboot_mode; /* default restart mode */ unsigned l2c_aux_val; /* L2 cache aux value */ unsigned l2c_aux_mask; /* L2 cache aux mask */ void (*l2c_write_sec)(unsigned long, unsigned); const struct smp_operations *smp; /* SMP operations */ bool (*smp_init)(void); void (*fixup)(struct tag *, char **); void (*dt_fixup)(void); long long (*pv_fixup)(void); void (*reserve)(void);/* reserve mem blocks */ void (*map_io)(void);/* IO mapping function */ void (*init_early)(void); void (*init_irq)(void); void (*init_time)(void); void (*init_machine)(void); void (*init_late)(void); #ifdef CONFIG_MULTI_IRQ_HANDLER void (*handle_irq)(struct pt_regs *); #endif void (*restart)(enum reboot_mode, const char *); }; |
一个machine_desc描述符用来描述一个机器平台的信息(这些信息包括机器码、机器相关的初始化函数init_machine(对于exynos开发板init_machine被赋值为exynos_dt_machine_init,然后通过machine_desc->init_machine()方式被调用)、设备树compatible属性值dt_compat等),内核通过设备树中提供的compatible属性查找内核中适配的machine_desc描述符;
setup_machine_fdt函数中还会获取启动参数:
setup_machine_fdt
early_init_dt_scan_nodes
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line)
early_init_dt_scan_chosen:该函数中会根据menuconfig的配置(优先使用bootloader传递的cmdline(即bootargs)或者优先使用.config中定义的CONFIG_CMDLINE)获取启动参数保存到boot_command_line
4、通过parse_early_param和parse_args将cmdline解析出来,分别放到对应变量中
比如cmdline: console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init/linuxrc rootfstype=ext3,则解析出的内容就是一个字符数组,数组中依次存放了一个个设置项:
console=ttySAC2,115200 一个
root=/dev/mmcblk0p2 rw 一个
init/linuxrc 一个
rootfstype=ext3 一个
5、接下来会进行一些架构无关的通用初始化,比如内存初始化mm_init、调度系统初始化sched_init、工作队列初始化workqueue_init_early、中断初始化init_IRQ、软中断初始化softirq_init、定时器初始化time_init、控制台初始化console_init(控制台初始化后,上面printk打印的内容才在控制台打印出来)等;
上述内容完成之后,内核的组装基本完成,最后调用rest_init函数完成剩余的初始化工作。
6、rest_init函数
该函数主要做了3件事情:
1)调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthread
kernel_thread(kernel_init, NULL, CLONE_FS)
kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)
kernel_init函数就是进程1,这个进程被称为init进程,是用户空间的第一个进程;kthread函数就是进程3,这个进程是linux内核的守护进程,这个进程是用来保证linux内核自己本身能正常工作的。
2)开启内核调度系统,从此linux系统开始转起来了
schedule_preempt_disabled
schedule
3)最后while(1)无限调用do_idle函数进行死循环,结束内核的启动初始化过程
cpu_startup_entry
while (1) {
do_idle();
}
7、init进程
init刚创建的时候是内核态线程,然后它会去挂载根文件系统,加载根文件系统中的init进程(这个init进程在cmdline中指定,比如init=/linuxrc),从而转变为用户态;init进程是其他用户进程的老祖宗。linux系统中一个进程的创建是通过其父进程创建出来的。
init进程首先获取三个标准文件(标准输入、标准输出、标准错误),这样以后所有进程创建后,都能继承这3个文件描述符
kernel_init
kernel_init_freeable
/* 标准输入 */
sys_open((const char __user *) "/dev/console", O_RDWR, 0)
/* 标准输出 */
(void) sys_dup(0);
/* 标准错误 */
(void) sys_dup(0);
然后挂载根文件系统,根文件系统的信息在cmdline中提供,比如root=/dev/mmcblk0p2 rw(根文件系统所在介质)、rootfstype=ext3根文件系统类型、init=/linuxrc根文件系统中init进程
kernel_init
kernel_init_freeable
prepare_namespace
mount_block_root
do_mount_root
printk(KERN_INFO
"VFS: Mounted root (%s filesystem)%s on device %u:%u.\n",
s->s_type->name,
sb_rdonly(s) ? " readonly" : "",
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
四、通过对exynos4412开发板上移植linux内核4.14.2验证上述分析
Linux官方内核版本对迅为电子 iTOP 系列开发平台 iTOP-4412 的 SCP 核心板有提供支持,对于linux4.14.2的官方版本内核,只要稍微修改下就可以在itop4412上运行了。下面记录下linux4.14.2官方版本移植到itop4412的步骤:
虚拟机版本:ubuntu14.04.6
交叉编译工具链:arm-linux-gnueabi-
linux4.14.2官方版本:https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.14.2.tar.gz
移植参考博客:
https://www.cnblogs.com/yueliang17/p/11776163.html
1、解压并清理下内核
tar -xvf linux-4.14.2.tar
cd linux-4.14.2
make distclean
2、修改主Makefile里面的ARCH和CROSS_COMPILE变量
3、执行make exynos_defconfig生成默认的.config文件,再用menuconfig稍微修改下就可以了
具体的内核中开发板相关的配置以及设备树参数的修改按照
https://www.cnblogs.com/yueliang17/p/11776163.html的步骤就可以了。
编译内核
make uImage LOADADDR=0X40007000 -j4
编译完成后,在arch/arm/boot文件夹下生成内核镜像
编译设备树
make dtbs
至此,内核可以启动,但是不支持用nfs挂载根文件系统(如下图所示);需要进一步使能USB网卡和配置NFS相关参数以支持nfs启动。
在menuconfig中配置内核以支持nfs方式挂载根文件系统
1)使能DM9621网卡的驱动(kernel中叫DM9601),menuconfig中按“/”搜索DM9601就可以看到配置位置;
配置网络文件系统支持如下
配置网络选项支持
完成上述配置后,内核启动时会优先使用uboot提供的启动参数bootargs,在uboot中配置nfs挂载根文件系统的启动方式
Set bootargs ‘root=/dev/nfs rw nfsroot=192.168.255.110:/xxx ip=192.168.255.8:192.168.255.110:192.168.255.110:255.255.255.0:itop:eth0:off rootfstype=ext4 init=/linuxrc console=ttySAC2,115200’
至此,kernel可以用nfs挂载根文件系统,启动后如下
本文仅是本人在熟悉linux内核的代码框架过程中的记录,分析总结出来以便自己更好的理解,大家勿喷哈。参考了网上的博客没有一一列出。如有侵权,请联系删除。
参考博客:
朱有鹏老师的linux内核移植视频:http://t.elecfans.com/topic/8.html