按照以下思路大概總結下對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