?Linux內核在啟動的時候需要一些參數(shù),以獲得當前硬件的信息或者啟動所需資源在內存中的位置等等。這些信息可以通過bootloader傳遞給內核,比較常見的就是cmdline。以前我在啟動內核的時候習慣性的通過uboot傳遞一個cmdline給內核,沒有具體的分析這個過程。最近在分析內核啟動過程的時候,重新看了一下內核啟動參數(shù)的傳遞過程,徹底解決一下在這方面的疑惑。
內核的啟動參數(shù)其實不僅僅包含在了cmdline中,cmdline不過是bootloader傳遞給內核的信息中的一部分。bootloader和內核的通信方式根據(jù)構架的不同而異。對于ARM構架來說,啟動相關的信息可以通過內核文檔(Documentation/arm/Booting)獲得。其中介紹了bootloader與內核的通信協(xié)議,我簡單總結如下:
(1)數(shù)據(jù)格式:可以是標簽列表(tagged list)或設備樹(device tree)。
(2)存放地址:r2寄存器中存放的數(shù)據(jù)所指向的內存地址。
在我所做過的開發(fā)中,都是使用tagged list的,所以下面以標簽列表為例來介紹信息從bootloader(U-boot)到內核(Linux-3.0)的傳遞過程。
內核文檔對此的說明,翻譯摘要如下:
4a. 設置內核標簽列表
--------------------------------
bootloader必須創(chuàng)建和初始化內核標簽列表。一個有效的標簽列表以ATAG_CORE標簽開始,且以ATAG_NONE標簽結束。ATAG_CORE標簽可以是空的,也可以是非空。一個空ATAG_CORE標簽其 size 域設置為 '2' (0x00000002)。ATAG_NONE標簽的 size 域必須設置為 '0'。
在列表中可以保存任意數(shù)量的標簽。對于一個重復的標簽是追加到之前標簽所攜帶的信息之后,還是覆蓋原來整個信息,是未定義的。某些標簽的行為是前者,其他是后者。
bootloader必須傳遞一個系統(tǒng)內存的位置和最小值,以及根文件系統(tǒng)位置。因此,最小的標簽列表如下所示:
基地址 ->?+-----------+
| ATAG_CORE | |
+-----------+ ?|
| ATAG_MEM | ?| 地址增長方向
+-----------+ ?|
| ATAG_NONE | |
+-----------+ ?v
標簽列表應該保存在系統(tǒng)的RAM中。
標簽列表必須置于內核自解壓和initrd'bootp'程序都不會覆蓋的內存區(qū)。建議放在RAM的頭16KiB中。
(內核中關于ARM啟動的標準文檔為:Documentation/arm/Booting ,我翻譯的版本:《Linux內核文檔翻譯:Documentation/arm/Booting》)
關于tagged list的數(shù)據(jù)結構和定義在內核與uboot中都存在,連路徑都相同:arch/arm/include/asm/setup.h。uboot的定義是從內核中拷貝過來的,要和內核一致的,以內核為主。要了解標簽列表的具體結構認真閱讀這個頭文件是必須的。
一個獨立的標簽的結構大致如下:
struct tag
+------------------------+
| struct tag_header hdr; | |
| ? ? ?標簽頭信息 ? ? ? ?| |
+------------------------+ |
|union { ? ? ? ? ? ? ? ? | |
| struct tag_core core; ?| |
| struct tag_mem32 mem; ?| |
| ...... ? ? ? ? ? ? ? ? | |
| } u; ? ? ? ? ? ? ? ? ? | |
|?? ? ??標簽具體內容 ? ? | |
|?? ? ??此為聯(lián)合體 ? ? ? | | 地址增長方向
|???根據(jù)標簽類型確定 ? ? | |
+------------------------+ v
點擊(此處)折疊或打開
struct tag_header?{
__u32 size;?//標簽總大?。ò╰ag_header)
__u32 tag;?//標簽標識
};
比如一個ATAG_CORE在內存中的數(shù)據(jù)為:
+----------+
| 00000005 | |
| 54410001 | |
+----------+ |
| 00000000 | |
| 00000000 | |地址增長方向
| 00000000 | |
+----------+ v
當前在內核中接受的標簽有:
ATAG_CORE : 標簽列表開始標志
ATAG_NONE : 標簽列表結束標志
ATAG_MEM : 內存信息標簽(可以有多個標簽,以標識多個內存區(qū)塊)
ATAG_VIDEOTEXT:VGA文本顯示參數(shù)標簽
ATAG_RAMDISK :ramdisk參數(shù)標簽(位置、大小等)
ATAG_INITRD :壓縮的ramdisk參數(shù)標簽(位置為虛擬地址)
ATAG_INITRD2 :壓縮的ramdisk參數(shù)標簽(位置為物理地址)
ATAG_SERIAL :板子串號標簽
ATAG_REVISION :板子版本號標簽
ATAG_VIDEOLFB :幀緩沖初始化參數(shù)標簽
ATAG_CMDLINE :command line字符串標簽(我們平時設置的啟動參數(shù)cmdline字符串就放在這個標簽中)
特定芯片使用的標簽:
ATAG_MEMCLK :給footbridge使用的內存時鐘標簽
二、參數(shù)從u-boot到特定內存地址
使用uboot來啟動一個Linux內核,通常情況下我們會按照如下步驟執(zhí)行:
設置內核啟動的command line,也就是設置uboot的環(huán)境變量“bootargs”(非必須,如果你要傳遞給內核cmdline才要設置)
加載內核映像文到內存指定位置(從SD卡、u盤、網(wǎng)絡或flash)
使用“bootm (內核映像基址)”命令來啟動內核
而這個uboot將參數(shù)按照協(xié)議處理好并放入指定內存地址的過程就發(fā)生在“bootm”命令中,下面我們仔細分析下bootm命令的執(zhí)行。
1、bootm 命令主體流程
bootm命令的源碼位于common/cmd_bootm.c,其中的do_bootm函數(shù)就是bootm命令的實現(xiàn)代碼。
點擊(此處)折疊或打開
/*******************************************************************/
/*?bootm?-?boot application image from image?in?memory?*/
/*******************************************************************/
int?do_bootm?(cmd_tbl_t?*cmdtp,?int?flag,?int?argc,?char?*?const?argv[])
{
ulong????????iflag;
ulong????????load_end?=?0;
int????????ret;
boot_os_fn????*boot_fn;
#ifdef CONFIG_NEEDS_MANUAL_RELOC
static?int?relocated?=?0;
/* 重載啟動函數(shù)表 */
if?(!relocated)?{
int?i;
for?(i?=?0;?i?
if?(boot_os[i]?!=?NULL)
boot_os[i]?+=?gd->reloc_off;
relocated?=?1;
}
#endif
/* 確定我們是否有子命令 */
/* bootm其實是有子命令的,可以自己將bootm的功能手動分步進行,來引導內核 */
if?(argc?>?1)?{
char?*endp;
simple_strtoul(argv[1],?&endp,?16);
/*?endp pointing?to?NULL?means that argv[1]?was just a
*?valid number,?pass it along?to?the normal bootm processing
*
*?If?endp?is?':'?or?'#'?assume a FIT identifier so pass
*?along?for?normal processing.
*
*?Right?now?we assume the first arg should never be?'-'
*/
if?((*endp?!=?0)?&&?(*endp?!=?':')?&&?(*endp?!=?'#'))
return do_bootm_subcommand(cmdtp,?flag,?argc,?argv);
}
if (bootm_start(cmdtp, flag, argc, argv))
return 1;
點擊(此處)折疊或打開
這句非常重要,使其這個就是bootm主要功能的開始。
其主要的目的是從bootm命令指定的內存地址中獲取內核uImage的文件頭(也就是在用uboot的mkiamge工具處理內核zImage時添加的那64B的數(shù)據(jù))。核對并顯示出其中包含的信息,并填充一個全局的static bootm_headers_t images結構體的image_info_t os域:
點擊(此處)折疊或打開
typedef struct image_info?{
ulong start,?end;?/*?start/end?of blob?*/?
ulong image_start,?image_len;?/*?start of image within blob,?len?of image?*/
ulong load;?/*?load addr?for?the image?*/
uint8_t comp,?type,?os;?/*?compression,?type of image,?os type?*/
}?image_info_t;
/*這個域保存OS映像的信息,包括uImage的起止地址、所包含內核映像(可能被壓縮過)的起始地址和大小、(解壓后)內核映像的加載位置以及壓縮方式、映像類型和OS類型。*/
平常我們在用bootm驅動內核的時候所看到的如下信息:
## Booting kernel from Legacy Image at 50008000 ...
Image Name: Linux-2.6.37.1
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 3800644 Bytes = 3.6 MiB
Load Address: 50008000
Entry Point: 50008040
Verifying Checksum ... OK
就是這個函數(shù)所調用的?boot_get_kernel函數(shù)及其子函數(shù)根據(jù)uImage的文件頭打印出來的。
/*
*?我們已經(jīng)到達了不可返回的位置: 我們正要覆蓋所有的異常向量代碼。
*?所以我們再也無法簡單地從任何失敗中恢復
*?(突然讓我想到張信哲的歌詞:我們再也回不去了,對不對?)...
*/
iflag?=?disable_interrupts();
#if?defined(CONFIG_CMD_USB)
/*
*?turn off USB?to?prevent the host controller from writing?to?the
*?SDRAM?while?Linux?is?booting.?This could happen?(at least?for?OHCI
*?controller),?because the HCCA?(Host Controller Communication Area)
*?lies within the SDRAM?and?the host controller writes continously?to
*?this area?(as The HccaFrameNumber?is?for?example
*?updated every 1 ms within the HCCA structure?in?For?more
*?details see the OpenHCI specification.
*/
usb_stop();
#endif
ret = bootm_load_os(images.os, &load_end, 1);
點擊(此處)折疊或打開
這里是第二個重要的啟動過程節(jié)點,這個函數(shù)的作用是通過獲取的文件頭信息,將文件頭后面所跟的內核映像放置到文件頭信息規(guī)定的地址(如果是壓縮內核,還在此函數(shù)中解壓。但這個和zImage壓縮內核不是一個概念,不要混淆)。
平常我們在用bootm驅動內核的時候所看到的如下信息:
XIP Kernel Image ... OK
OK
就是這個函數(shù)打印出來的。
if?(ret?0)?{
if?(ret?==?BOOTM_ERR_RESET)
do_reset?(cmdtp,?flag,?argc,?argv);
if?(ret?==?BOOTM_ERR_OVERLAP)?{
if?(images.legacy_hdr_valid)?{
if?(image_get_type?(&images.legacy_hdr_os_copy)?==?IH_TYPE_MULTI)
puts?("WARNING: legacy format multi component "
"image overwritten ");
}?else?{
puts?("ERROR: new format image overwritten - "
"must RESET the board to recover ");
show_boot_progress?(-113);
do_reset?(cmdtp,?flag,?argc,?argv);
}
}
if?(ret?==?BOOTM_ERR_UNIMPLEMENTED)?{
if?(iflag)
enable_interrupts();
show_boot_progress?(-7);
return 1;
}
}
lmb_reserve(&images.lmb,?images.os.load,?(load_end?-?images.os.load));
if?(images.os.type?==?IH_TYPE_STANDALONE)?{
if?(iflag)
enable_interrupts();
/*?This may return when?'autostart'?is?'no'?*/
bootm_start_standalone(iflag,?argc,?argv);
return 0;
}
show_boot_progress?(8);
#ifdef CONFIG_SILENT_CONSOLE
if?(images.os.os?==?IH_OS_LINUX)
fixup_silent_linux();
#endif
boot_fn = boot_os[images.os.os];
點擊(此處)折疊或打開
這個語句是有是一個比較重要的節(jié)點,其功能是根據(jù)全局static bootm_headers_t images結構體的image_info_t os域中記錄的os類型來將一個特定OS的內核引導函數(shù)入口賦給boot_fn變量。比如我引導的是Linux內核,那么boot_fn就是do_bootm_linux。
if?(boot_fn?==?NULL)?{
if?(iflag)
enable_interrupts();
printf?("ERROR: booting os '%s' (%d) is not supported ",
genimg_get_os_name(images.os.os),?images.os.os);
show_boot_progress?(-8);
return 1;
}
arch_preboot_os();
boot_fn(0, argc, argv, &images);
點擊(此處)折疊或打開
如果不出錯的話,這個函數(shù)應該是不會在返回了,因為在這個函數(shù)中會將控制權交由OS的內核。對于引導Linux內核來說,這里其實就是調用do_bootm_linux。
show_boot_progress?(-9);
#ifdef DEBUG
puts?(" ## Control returned to monitor - resetting... ");
#endif
do_reset?(cmdtp,?flag,?argc,?argv);
return 1;
}
2、分析do_bootm_linux
對我們來說非常重要的do_bootm_linux函數(shù)位于:arch/arm/lib/bootm.c
Bootm.c (archarmlib):
點擊(此處)折疊或打開
int?do_bootm_linux(int?flag,?int?argc,?char?*argv[],?bootm_headers_t?*images)
{
bd_t????*bd?=?gd->bd;
char????*s;
int????machid?=?bd->bi_arch_number;
void????(*kernel_entry)(int?zero,?int?arch,?uint params);
#ifdef CONFIG_CMDLINE_TAG
char *commandline = getenv ("bootargs");
#endif
這里獲取了生成cmdline標簽所需要的字符串
if?((flag?!=?0)?&&?(flag?!=?BOOTM_STATE_OS_GO))
return 1;
s = getenv ("machid");
if?(s)?{
machid?=?simple_strtoul?(s,?NULL,?16);
printf?("Using machid 0x%x from environment ",?machid);
}
注意:這里設備ID號可以從環(huán)境變量中獲得!如果環(huán)境變量中有,就會覆蓋之前賦值過的設備ID(最終通過r1傳遞給內核)。
show_boot_progress?(15);
#ifdef CONFIG_OF_LIBFDT
if?(images->ft_len)
return bootm_linux_fdt(machid,?images);
#endif
kernel_entry = (void (*)(int, int, uint))images->ep;
這里讓函數(shù)指針指向內核映像的入口地址
debug?("## Transferring control to Linux (at address %08lx) ... ",
(ulong)?kernel_entry);
以下就是我們一直在找的內核標簽列表生成代碼,從這里看出:U-boot原生只支持部分標簽。當然,如果要加的話也很簡單。
#if?defined?(CONFIG_SETUP_MEMORY_TAGS)?||?
defined?(CONFIG_CMDLINE_TAG)?||?
defined?(CONFIG_INITRD_TAG)?||?
defined?(CONFIG_SERIAL_TAG)?||?
defined?(CONFIG_REVISION_TAG)
setup_start_tag (bd); ?//設置ATAG_CORE
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag?(¶ms); ?//設置ATAG_SERIAL,依賴板級是否實現(xiàn)了get_board_serial函數(shù)
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag?(¶ms);//設置ATAG_REVISION,依賴板級是否實現(xiàn)了get_board_rev函數(shù)
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags?(bd);//設置ATAG_MEM,依賴于uboot的全局變量bd->bi_dram[i]中的數(shù)據(jù)
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag?(bd,?commandline);//設置ATAG_CMDLINE,依賴上面的字符串commandline中的數(shù)據(jù)
#endif
#ifdef CONFIG_INITRD_TAG
if?(images->rd_start?&&?images->rd_end)
setup_initrd_tag?(bd,?images->rd_start,?images->rd_end);//設置ATAG_INITRD
#endif
setup_end_tag(bd);//設置ATAG_NONE
#endif
announce_and_cleanup();
在進入內核前配置好芯片狀態(tài),以符合內核啟動要求。
主要是關閉和清理緩存
kernel_entry(0, machid, bd->bi_boot_params);
/*?不會再返回了?*/
跳入內核入口地址:r1=0、r1=machid、r2=啟動參數(shù)指針
return 1;
}
點擊(此處)折疊或打開
static void announce_and_cleanup(void)
{
printf(" Starting kernel ... ");
#ifdef CONFIG_USB_DEVICE
{
extern void udc_disconnect(void);
udc_disconnect();
}
#endif
cleanup_before_linux();
}
Cpu.c (archarmcpuarmv7) 1958 2011-4-1
點擊(此處)折疊或打開
int?cleanup_before_linux(void)
{
unsigned?int?i;
/*
*?this?function?is?called just before we?call?linux
*?it prepares the processor?for?linux
*
*?we turn off caches etc?...
*/
disable_interrupts();
/*?turn off I/D-cache?*/
icache_disable();
dcache_disable();
/*?invalidate I-cache?*/
cache_flush();
#ifndef CONFIG_L2_OFF
/*?turn off L2 cache?*/
l2_cache_disable();
/*?invalidate L2 cache also?*/
invalidate_dcache(get_device_type());
#endif
i?=?0;
/*?mem barrier?to?sync up things?*/
asm("mcr p15, 0, %0, c7, c10, 4":?:"r"(i));
#ifndef CONFIG_L2_OFF
l2_cache_enable();
#endif
return 0;
對于上面啟動環(huán)境的設定,可參考Documentation/arm/Booting。節(jié)選Booting中文翻譯:
5. 調用內核映像
---------------------------
現(xiàn)有的引導加載程序: 強制
新開發(fā)的引導加載程序: 強制
調用內核映像zImage有兩個選擇。如果zImge是保存在flash中的,且其為了在flash中直接運行而被正確鏈接。這樣引導加載程序就可以在flash中直接調用zImage。
zImage也可以被放在系統(tǒng)RAM(任意位置)中被調用。注意:內核使用映像基地址的前16KB RAM空間來保存頁表。建議將映像置于RAM的32KB處。
對于以上任意一種情況,都必須符合以下啟動狀態(tài):
- 停止所有DMA設備,這樣內存數(shù)據(jù)就不會因為虛假網(wǎng)絡包或磁盤數(shù)據(jù)而被破壞。這可能可以節(jié)省你許多的調試時間。
- CPU 寄存器配置
r0 = 0,
r1 = (在上面 (3) 中獲取的)機器類型碼.
r2 = 標簽列表在系統(tǒng)RAM中的物理地址,或
設備樹塊(dtb)在系統(tǒng)RAM中的物理地址
- CPU 模式
所有形式的中斷必須被禁止 (IRQs 和 FIQs)
CPU 必須處于 SVC 模式。 (對于 Angel 調試有特例存在)
- 緩存, MMUs
MMU 必須關閉。
指令緩存開啟或關閉都可以。
數(shù)據(jù)緩存必須關閉。
- 引導加載程序應該通過直接跳轉到內核映像的第一條指令來調用內核映像。
3、標簽生成的函數(shù)舉例分析:
所有標簽生成函數(shù)都在arch/arm/lib/bootm.c文件中,其實原理很簡單,就是直接往指定的內存地址中寫入標簽信息。以下以setup_start_tag和setup_memory_tags為例分析:
點擊(此處)折疊或打開
static void setup_start_tag?(bd_t?*bd)
130?{
131 ? ? params?=?(struct tag?*)?bd->bi_boot_params; ??//params指向內存中標簽列表中的基地址
132 ? ??//直接往內存中按照內核定義的標簽結構寫入信息
133 ? ? params->hdr.tag?=?ATAG_CORE;
134 ? ? params->hdr.size?=?tag_size?(tag_core);
135?
136 ? ? params->u.core.flags?=?0;
137 ? ? params->u.core.pagesize?=?0;
138 ? ? params->u.core.rootdev?=?0;
139 ? ??//根據(jù)本標簽的大小數(shù)據(jù),params跳到下一標簽的起始地址
140 ? ? params?=?tag_next?(params);
141?}
點擊(此處)折疊或打開
static void setup_memory_tags?(bd_t?*bd)
146?{
147 ? ??int?i;
148 ? ??//上一個標簽已經(jīng)將params指向了下一標簽的基地址,所以這里可以直接使用
149 ? ??for?(i?=?0;?i?
150 ? ? ? ? ?params->hdr.tag?=?ATAG_MEM;
151 ? ? ? ? ?params->hdr.size?=?tag_size?(tag_mem32);
152 ? ? ? ? ?//根據(jù)配置信息和uboot全局變量中的信息創(chuàng)建標簽數(shù)據(jù)
153 ? ? ? ? ?params->u.mem.start?=?bd->bi_dram[i].start;
154 ? ? ? ? ?params->u.mem.size?=?bd->bi_dram[i].size;
155?
156 ? ? ? ? ?params?=?tag_next?(params);//根據(jù)本標簽的大小數(shù)據(jù),params跳到下一標簽的起始地址
157 ? ? ?}
158?}
bootloader完成了引導Linux內核所需要的準備之后將通過直接跳轉,將控制權交由內核zImage。
三、內核從特定內存獲取參數(shù)
在內核zImage開始運行后,首先是進行內核自解壓,其過程在之前的博客中有詳細介紹:《Linux內核源碼分析--內核啟動之(1)zImage自解壓過程(Linux-3.0 ARMv7)》。其中對于內核標簽列表的沒有處理。在完成內核自解壓之后,系統(tǒng)又恢復了bootloader設定的啟動狀態(tài),將控制權交由解壓后的內核。也就是說解壓前后,系統(tǒng)啟動環(huán)境不變。
解壓后的內核開始運行后,首先是構架相關的匯編代碼,其過程在之前的博客中有詳細介紹:《Linux內核源碼分析--內核啟動之(2)Image內核啟動(匯編部分)(Linux-3.0 ARMv7)》。其中對于內核標簽列表的處理就是判斷r2(內核啟動參數(shù))指針的有效性:驗證指針指向的數(shù)據(jù)是否是有效的tagged list或者device tree,如果不是r2清零。
在運行完匯編代碼后,就跳入了構架無關的C語言啟動代碼:init/main.c中的start_kernel函數(shù)。在這個函數(shù)中開始了對內核啟動參數(shù)的真正處理。
首先內核必須先要解析tagged list,而它的處理位于:
start_kernel-->setup_arch(&command_line);-->mdesc = setup_machine_tags(machine_arch_type);
點擊(此處)折疊或打開
static struct machine_desc?*?__init setup_machine_tags(unsigned?int?nr)
{
struct tag?*tags?=?(struct tag?*)&init_tags;
點擊(此處)折疊或打開
先讓tags指針指向內核默認tagged list(init_tags)
點擊(此處)折疊或打開
/*
*?This holds our defaults.
*/
static struct init_tags?{
struct tag_header hdr1;
struct tag_core core;
struct tag_header hdr2;
struct tag_mem32 mem;
struct tag_header hdr3;
}?init_tags __initdata?=?{
{?tag_size(tag_core),?ATAG_CORE?},
{?1,?PAGE_SIZE,?0xff?},
{?tag_size(tag_mem32),?ATAG_MEM?},
{?MEM_SIZE?},
{?0,?ATAG_NONE?}
};
這個默認的tagged list實質上只定義了內存的參數(shù)
struct machine_desc?*mdesc?=?NULL,?*p;
char?*from?=?default_command_line;
點擊(此處)折疊或打開
注意這個from的賦值,指向default_command_line,它是默認的內核cmdline,在內核配置的時候可指定。
Boot options ?--->
() ?Default kernel command string
init_tags.mem.start?=?PHYS_OFFSET;
點擊(此處)折疊或打開
對上面的內核默認的tagged list中的內存起始地址進行初始化。
個人感覺這句有點奇怪,這個賦值為什么不直接放在變量定義的地方一起初始化呢?
/*
*?在支持的設備列表中找到當前的設備。
*/
for_each_machine_desc(p)
if?(nr?==?p->nr)?{
printk("Machine: %s ",?p->name);
mdesc?=?p;
break;
}
點擊(此處)折疊或打開
內核編譯的時候可能編譯進了多個設備的支持,所以可能存在多個設備的描述結構體。這個通過bootloader傳遞進來的設備ID來匹配一個設備描述結構體。
if?(!mdesc)?{
early_print(" Error: unrecognized/unsupported machine ID"
" (r1 = 0x%08x). ",?nr);
dump_machine_table();?/*?does?not?return?*/
}
點擊(此處)折疊或打開
如果上面沒有找到匹配的設備描述結構體,則打印出錯信息,并死循環(huán)。
if?(__atags_pointer)
tags?=?phys_to_virt(__atags_pointer);
else?if?(mdesc->boot_params)?{
#ifdef CONFIG_MMU
/*
*?我們依然運行在最小的MMU映射上,
*?這假設設備默認將標簽列表放在頭1MB的RAM中。
*?任何其他的位置將可能失敗,
*?并在此處靜靜地掛起內核。
*/
if?(mdesc->boot_params?
mdesc->boot_params?>=?PHYS_OFFSET?+?SZ_1M)?{
printk(KERN_WARNING
"Default boot params at physical 0x%08lx out of reach ",
mdesc->boot_params);
}?else
#endif
{
tags?=?phys_to_virt(mdesc->boot_params);
}
}
點擊(此處)折疊或打開
如果bootloader傳遞過來的tagged list有效,則將地址轉換成虛擬地址,賦給tags。
否則使用設備描述結構體中的數(shù)據(jù)。例如:
從這里也可以知道,設備描述結構體中的.boot_params數(shù)據(jù)是可選的,如果bootloader傳入的地址沒有問題,這里就不會用到。(其他地方是否用的,有待確定)
點擊(此處)折疊或打開
MACHINE_START(MINI6410, "MINI6410")
/* Maintainer: Darius Augulis */
.boot_params = S3C64XX_PA_SDRAM + 0x100,
.init_irq = s3c6410_init_irq,
.map_io = mini6410_map_io,
.init_machine = mini6410_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END
#if?defined(CONFIG_DEPRECATED_PARAM_STRUCT)
/*
*??如果傳遞進來的是一個舊格式的參數(shù), 將他們轉換為
*? 一個tag list.
*/
if?(tags->hdr.tag?!=?ATAG_CORE)
convert_to_tag_list(tags);
#endif
if?(tags->hdr.tag?!=?ATAG_CORE)?{
#if?defined(CONFIG_OF)
/*
*?如果定義了 CONFIG_OF , 那么就假假設一個合理的
*?現(xiàn)代系統(tǒng)應該傳入一個啟動參數(shù)
*/
early_print("Warning: Neither atags nor dtb found ");
#endif
tags?=?(struct tag?*)&init_tags;
}
點擊(此處)折疊或打開
如果tagged list的第一個tag不是 ATAG_CORE,說明tagged list 不存在或者有問題,打印錯誤信息并使用默認tagged list。
if?(mdesc->fixup)
mdesc->fixup(mdesc,?tags,?&from,?&meminfo);
點擊(此處)折疊或打開
如果此設備描述結構體中定義了fixup函數(shù),就執(zhí)行。從這里看出似乎這個函數(shù)是用于處理tagged list、cmdline和meminfo數(shù)據(jù)的。
if?(tags->hdr.tag?==?ATAG_CORE)?{
if?(meminfo.nr_banks?!=?0)
squash_mem_tags(tags);
點擊(此處)折疊或打開
如果meminfo(其中保存了內存的bank信息)中已經(jīng)初始化過了,就清除tagged list中mem_tags的信息(可導致跟在mem_tags之后的信息也一并失效)
save_atags(tags);
點擊(此處)折疊或打開
備份tagged list信息到全局atags_copy。
parse_tags(tags);
點擊(此處)折疊或打開
逐個解析tag,主要功能是將每個tag的信息保存到內核全局變量中
每個tag有對應的內核結構體:
點擊(此處)折疊或打開
struct tagtable {
__u32 tag; //tag標識編號
int (*parse)(const struct tag *); //tag信息處理函數(shù)(一般是將其中的信息保存到內核全局變量中)
};
內核一般通過以下宏來定義一個tagtable結構體:
點擊(此處)折疊或打開
#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn)
static struct tagtable __tagtable_##fn __tag = { tag, fn }
也就是將所有定義好的tagtable結構體放入一個獨立的".taglist.init"段中,使用時用一個for循環(huán)就可以遍歷了。
}
點擊(此處)折疊或打開
如果tagged list中的ATAG_CORE驗證通過,就保存并解析tag。
/*?parse_early_param 函數(shù)需要 boot_command_line?*/
strlcpy(boot_command_line,?from,?COMMAND_LINE_SIZE);
點擊(此處)折疊或打開
將form指向的字符串拷貝到boot_command_line字符數(shù)組中。
return mdesc;
點擊(此處)折疊或打開
返回匹配的設備描述結構體指針。
}
tag分析函數(shù)重點舉例
對內存信息的處理
點擊(此處)折疊或打開
static?int?__init parse_tag_mem32(const?struct tag?*tag)
{
return arm_add_memory(tag->u.mem.start,?tag->u.mem.size);
點擊(此處)折疊或打開
將tag中的信息添加到全局的meminfo中去:arch/arm/include/asm/setup.h
點擊(此處)折疊或打開
/*
*?Memory map description
*/
#define NR_BANKS 8
struct membank?{
phys_addr_t start;
unsigned long size;
unsigned?int?highmem;
};
struct meminfo?{
int?nr_banks;
struct membank bank[NR_BANKS];
};
extern struct meminfo meminfo;
這些信息在內存子系統(tǒng)初始化的時候是會用到的,比如確定高低端內存的分界線。
}
__tagtable(ATAG_MEM,?parse_tag_mem32);
對cmdline的保存
點擊(此處)折疊或打開
static?int?__init parse_tag_cmdline(const?struct tag?*tag)
{
#if?defined(CONFIG_CMDLINE_EXTEND)
strlcat(default_command_line,?" ",?COMMAND_LINE_SIZE);
strlcat(default_command_line,?tag->u.cmdline.cmdline,
COMMAND_LINE_SIZE);
點擊(此處)折疊或打開
如果定義了“CONFIG_CMDLINE_EXTEND”(cmdline擴展),內核會將tag中cmdline和配置內核時定義的cmdline合并到default_command_line字符數(shù)組中。
#elif defined(CONFIG_CMDLINE_FORCE)
pr_warning("Ignoring tag cmdline (using the default kernel command line) ");
點擊(此處)折疊或打開
如果定義了“CONFIG_CMDLINE_FORCE”(強制使用配置內核時定義的cmdline),內核會忽略tag中cmdline
#else
strlcpy(default_command_line,?tag->u.cmdline.cmdline,
COMMAND_LINE_SIZE);
點擊(此處)折疊或打開
如果以上兩個配置都沒有定義,則使用tag中cmdline覆蓋到default_command_line字符數(shù)組中。
#endif
return 0;
}
__tagtable(ATAG_CMDLINE,?parse_tag_cmdline);
其他相關信息
其他所有的tag解析函數(shù)都是大同小異,都是將tag中的信息保存到各內核全局變量結構體中,以備后用。
四、內核處理cmdline
對于所有的tag中,我們最常用的就是cmdine,所以這里詳細解析一下。
從上面的setup_machine_tags函數(shù)中我們知道,對于從tag傳遞到default_command_line中的cmdline字符串,內核又將其復制了一份到boot_command_line中。
在回到了setup_arch函數(shù)中之后,內核又把boot_command_line復制了一份到cmd_line字符數(shù)組中,并用cmdline_p指針指向這個cmd_line字符數(shù)組。
在完成了上面的工作后,cmdline已經(jīng)從tag中到了多個全局字符數(shù)組中,也就是在內存中了,可以開始處理了。
這個cmdline的處理和tag的處理方法是一樣的,每個cmdline中的參數(shù)都有對應的內核結構體:include/linux/init.h
點擊(此處)折疊或打開
struct obs_kernel_param?{
const?char?*str; ? ? ? ? ? ??//參數(shù)標識字符串指針
int?(*setup_func)(char?*); ? //解析函數(shù)
int early;?? ? ? ? ? ? ? ? ? //早期解析標志
};
/*
*?僅用于真正的核心代碼.?正常情況下詳見 moduleparam.h.
*
*?強制對齊,使得編譯器不會將obs_kernel_param?"數(shù)組"中的元素放置在離
*?.init.setup較遠的地方.
*/
#define __setup_param(str,?unique_id,?fn,?early)????????????
static?const?char __setup_str_##unique_id[]?__initconst????
__aligned(1)?=?str;?
static struct obs_kernel_param __setup_##unique_id????
__used __section(.init.setup)????????????
__attribute__((aligned((sizeof(long)))))????
=?{?__setup_str_##unique_id,?fn,?early?}
#define __setup(str,?fn)????????????????????
__setup_param(str,?fn,?fn,?0)
/*?注意:?fn 是作為 module_param的,?不是?
*?當返回非零的時候發(fā)出警告!*/
#define early_param(str,?fn)????????????????????
__setup_param(str,?fn,?fn,?1)
/*?依賴 boot_command_line 被設置?*/
void __init parse_early_param(void);
void __init parse_early_options(char?*cmdline);
所有需要解析的參數(shù)都是通過__setup(str, fn)和early_param(str, fn)宏定義的,他們的差別僅在于是否為early參數(shù)。
解析函數(shù)的作用是根據(jù)cmdline中的參數(shù)值設置全局變量。例如對“init=”的定義如下:
init/main.c
點擊(此處)折疊或打開
static?int?__init init_setup(char?*str)
{
unsigned?int?i;
execute_command?=?str;
/*
*?In?case?LILO?is?going?to?boot us with default command line,
*?it prepends?"auto"?before the whole cmdline which makes
*?the shell think it should execute a script with such name.
*?So we ignore all arguments entered _before_ init=...?[MJ]
*/
for?(i?=?1;?i?
argv_init[i]?=?NULL;
return 1;
}
__setup("init=",?init_setup);
其這樣目的是為了將已經(jīng)解析出的“init=”后的字符串指針賦給全局變量execute_command。而這個execute_command就是內核初始化到最后執(zhí)行的用戶空間初始化程序。
內核對于cmdline的處理分為兩個步驟:早期處理和后期處理。
1、cmdline的早期處理
對于ARM構架,cmdline的早期處理是在setup_arch函數(shù)中的?parse_early_param();,但這個函數(shù)定義在init/main.c:
點擊(此處)折疊或打開
/*?檢查早期參數(shù).?*/
static?int?__init do_early_param(char?*param,?char?*val)
{
const?struct obs_kernel_param?*p;
for?(p?=?__setup_start;?p?
if?((p->early?&&?strcmp(param,?p->str)?==?0)?||
(strcmp(param,?"console")?==?0?&&
strcmp(p->str,?"earlycon")?==?0)
)?{
if?(p->setup_func(val)?!=?0)
printk(KERN_WARNING
"Malformed early option '%s' ",?param);
}
}
/*?這個階段我們接受任何異常.?*/
return 0;
}
點擊(此處)折疊或打開
此函數(shù)通過解析好的參數(shù)名及參數(shù)值,在上面介紹的“.init.setup”段中搜索匹配的“struct obs_kernel_param”結構體(必須標志為early,也就是用early_param(str, fn)宏定義的結構體),并調用參數(shù)處理函數(shù)。
void __init parse_early_options(char?*cmdline)
{
parse_args("early options", cmdline, NULL, 0, do_early_param);
點擊(此處)折疊或打開
這里通過統(tǒng)一的parse_args函數(shù)處理,此函數(shù)原型如下:
點擊(此處)折疊或打開
int?parse_args(const?char?*name,
char?*args,
const?struct kernel_param?*params,
unsigned num,?
int?(*unknown)(char?*param,?char?*val))
這個函數(shù)的處理方法主要是分離出每個類似“foo=bar,bar2”的形式,再給?next_arg分離出參數(shù)名和參數(shù)值,并通過參數(shù)名在“?const struct kernel_param *params”指向的地址中搜索對應的數(shù)據(jù)結構,并調用其參數(shù)處理函數(shù)。如果沒有找到就調用最后一個參數(shù)“unknown”傳遞進來的未知參數(shù)處理函數(shù)。
由于此處params為NULL,必然找不到對應的數(shù)據(jù)結構,所有分離好的參數(shù)及參數(shù)名都由最后一個函數(shù)指針參數(shù)指定的函數(shù)?do_early_param來處理。也就是上面那個函數(shù)。
}
/*??構架相關代碼在早期調用這個函數(shù), 如果沒有, 會在解析其他參數(shù)前再次調用這個函數(shù)。?*/
void __init parse_early_param(void)
{
static __initdata int done = 0;
static __initdata char tmp_cmdline[COMMAND_LINE_SIZE];
if (done)
return;
/*??最終調用?do_early_param.?*/
strlcpy(tmp_cmdline,?boot_command_line,?COMMAND_LINE_SIZE);
點擊(此處)折疊或打開
再次將boot_command_line復制到一個臨時變量,并在下面的函數(shù)中使用
parse_early_options(tmp_cmdline);
done = 1;
點擊(此處)折疊或打開
對這個靜態(tài)變量置1,標志著這個函數(shù)已經(jīng)執(zhí)行過。不需要再次執(zhí)行。
}
一個典型的早期參數(shù)就是“mem=”,之所以會放在前期處理,是因為內存參數(shù)對于系統(tǒng)初始化很重要,在這里處理完后,下面馬上就要用到這些數(shù)據(jù)了。
處理函數(shù)如下:
點擊(此處)折疊或打開
/*
*?Pick out the memory size.?We look?for?mem=size@start,
*?where start?and?size are?"size[KkMm]"
*/
static?int?__init early_mem(char?*p)
{
static?int?usermem __initdata?=?0;
unsigned long size;
phys_addr_t start;
char?*endp;
/*
*?如果此處指定內存大小,
*?我們會丟棄任何自動生成的大小
*
*/
if?(usermem?==?0)?{
usermem?=?1;
meminfo.nr_banks?=?0;
}
點擊(此處)折疊或打開
這里自動情況原有的內存配置信息,如果tagged list中有設置,這里就會清除并覆蓋原來的信息。
start?=?PHYS_OFFSET;
size?=?memparse(p,?&endp);
if?(*endp?==?'@')
start?=?memparse(endp?+?1,?NULL);
arm_add_memory(start, size);
點擊(此處)折疊或打開
這個函數(shù)上面介紹過了,就是把獲取的內存大小和基地址添加到全局的meminfo結構體中。
return 0;
}
early_param("mem",?early_mem);
2、cmdline的后期分類處理
在上面的早期處理完成之后,系統(tǒng)就繼續(xù)初始化。在從setup_arch(&command_line);返回不久就將cmdline又進行了一次備份,使用的是bootmem內存分配系統(tǒng):
點擊(此處)折疊或打開
setup_command_line(command_line);
點擊(此處)折疊或打開
對cmdline進行備份和保存:
/* 為處理的command line備份 (例如eg. 用于 /proc) */
char *saved_command_line;
/* 用于參數(shù)處理的command line */
static char *static_command_line;
之后就打印出內核cmdline并解析后期參數(shù)和模塊參數(shù)。源碼如下:
點擊(此處)折疊或打開
printk(KERN_NOTICE?"Kernel command line: %s ",?boot_command_line);
點擊(此處)折疊或打開
打印出完整的內核cmdline
parse_early_param();
點擊(此處)折疊或打開
解析內核早期參數(shù),但是對于ARM構架來說,在setup_arch函數(shù)中已經(jīng)調用過了。所以這里什么都不做。
parse_args("Booting kernel",?static_command_line,?__start___param,
__stop___param?-?__start___param,
&unknown_bootoption);
點擊(此處)折疊或打開
這里調用的parse_args就比較復雜了,我這里簡單地分析一下:
在這個函數(shù)主要是一個循環(huán),逐一分析完整的cmdline中的每個參數(shù):
使用next_arg函數(shù)解析出類似“foo=bar,bar2”的形式中的參數(shù)名(foo)和參數(shù)值(bar和bar2)
使用parse_one根據(jù)參數(shù)名在內核內建模塊的參數(shù)處理段(__param)中搜索每一個“struct kernel_param”,是否為某個內核內建模塊的參數(shù):
如果是,則使用搜索到的那個“struct kernel_param”結構體中的參數(shù)設置函數(shù)“.ops->set”來設置模塊的參數(shù)
如果不是,就使用unknown_bootoption函數(shù)處理,就是到內核的“.init.setup”段搜索,看是不是“非早期”內核啟動參數(shù)(使用__setup(str, fn)宏定義的參數(shù))。如果是的話,就用相應的函數(shù)來處理,這個和“早期”參數(shù)處理是一樣的。如果不是,可能會打印錯誤信息。
到了這里,內核的cmdline處理就到此結束了。只有內置模塊才會獲取到cmdline中的參數(shù),因為內建模塊無法通過其他形式獲取參數(shù),不像.ok模塊可以在掛載的時候從命令行獲取參數(shù)。
如果你自己的外置模塊(.ok)中需要參數(shù),就算是你在內核啟動cmdline中加了參數(shù),模塊掛載的時候也是沒法自動獲取。你必須在使用insmod掛載模塊的時候,在最后加上你要的設置的參數(shù)信息。或者通過/proc/cmdline獲取啟動參數(shù),然后用shell命令過濾出需要的參數(shù)字符串,并加到insmod命令的最后。
?
評論
查看更多