Linux内核在启动的时候需要一些参数,以获得当前硬件的信息或者启动所需资源在内存中的位置等等。这些信息可以通过bootloader传递给内核,比较常见的就是 cmdline 。以前我在启动内核的时候习惯性的通过 uboot 传递一个 cmdline 给内核,没有具体的分析这个过程。最近在分析内核启动过程的时候,重新看了一下内核启动参数的传递过程,彻底解决一下在这方面的疑惑。

一、bootloader与内核的通讯协议

内核的启动参数其实不仅仅包含在了cmdline中,cmdline不过是bootloader传递给内核的信息中的一部分。bootloader和内核的通信方式根据构架的不同而异。对于ARM构架来说,启动相关的信息可以通过内核文档(Documentation/arm/Booting)获得。其中介绍了bootloader与内核的通信协议,我简单总结如下:
(1)数据格式:可以是 标签列表(tagged list)设备树(device tree)
(2)存放地址:r2寄存器中存放的数据所指向的内存地址

在我所做过的开发中,都是使用 标签列表(tagged list) 的,所以下面以标签列表为例来介绍信息从bootloader(U-boot)到内核(Linux-3.0)的传递过程。

内核文档(Documentation/arm/Booting)对此处的说明,翻译摘要如下:

4a. 设置内核标签列表
bootloader必须创建和初始化内核的标签列表(tagged list)。
一个有效的标签列表(tagged list)以 ATAG_CORE标签 开始,且以 ATAG_NONE标签 结束。
ATAG_CORE标签 可以是空的,也可以是非空,一个空ATAG_CORE标签 其 size 域设置为 '2' (0x00000002)。
ATAG_NONE标签 的 size 域必须设置为 '0'。
在列表中可以保存任意数量的标签。
对于一个重复的标签是追加到之前标签所携带的信息之后,还是覆盖原来整个信息,是未定义的。某些标签的行为是前者,其他是后者。
bootloader必须传递一个系统内存的位置、最小值以及根文件系统位置。因此,最小的标签列表(tagged list)如下所示:
基地址 -> +-----------+
          | ATAG_CORE | |
         +-----------+  |
          | ATAG_MEM |  | 地址增长方向
         +-----------+  |
          | ATAG_NONE | |
         +-----------+  v
标签列表(tagged list)应该保存在系统的RAM中。
标签列表(tagged list)必须置于内核自解压和initrd'bootp'程序都不会覆盖的内存区。建议放在RAM的头16KB中。

(内核中关于ARM启动的标准文档为:Documentation/arm/Booting ,我翻译的版本:《Linux内核文档翻译:Documentation/arm/Booting》)
关于标签列表(tagged list)的数据结构和定义在内核与uboot中都存在,连路径都相同:arch/arm/include/asm/setup.h。uboot的定义是从内核中拷贝过来的,要和内核一致的,以内核为主。要了解标签列表(tagged list)的具体结构认真阅读这个头文件是必须的。
一个独立的标签的结构大致如下:

struct tag
+------------------------+
| struct tag_header hdr; | |
|      标签头信息         | |
+------------------------+ |
|union {                 | |
| struct tag_core core;  | |
| struct tag_mem32 mem;  | |
| ......                 | |
| } u;                   | |
|       标签具体内容      | |
|       此为联合体        | | 地址增长方向
|     根据标签类型确定    | |
+------------------------+ v
struct tag_header {
  __u32 size; //标签总大小(包括tag_header)
  __u32 tag;  //标签标识
};

比如一个 ATAG_CORE 在内存中的数据为:

+----------+
| 00000005 | |
| 54410001 | |
+----------+ |
| 00000000 | |
| 00000000 | |地址增长方向
| 00000000 | |
+----------+ v
当前在内核中接受的标签有:
ATAG_CORE : 标签列表开始标志
ATAG_NONE : 标签列表结束标志
ATAG_MEM  : 内存信息标签(可以有多个标签,以标识多个内存区块)
ATAG_VIDEOTEXT:VGA文本显示参数标签
ATAG_RAMDISK  :ramdisk参数标签(位置、大小等)
ATAG_INITRD   :压缩的ramdisk参数标签(位置为虚拟地址)
ATAG_INITRD2  :压缩的ramdisk参数标签(位置为物理地址)
ATAG_SERIAL :板子串号标签
ATAG_REVISION :板子版本号标签
ATAG_VIDEOLFB :帧缓冲初始化参数标签
ATAG_CMDLINE  :command line字符串标签(我们平时设置的启动参数cmdline字符串就放在这个标签中)

特定芯片使用的标签:
ATAG_MEMCLK :给footbridge使用的内存时钟标签
ATAG_ACORN :acorn RiscPC 特定信息

二、参数从u-boot到特定内存地址

使用uboot来启动一个Linux内核,通常情况下我们会按照如下步骤执行:

  1. 设置内核启动的command line,也就是设置uboot的环境变量“bootargs”(非必须,如果你要传递给内核cmdline才要设置)
  2. 加载内核映像文到内存指定位置(从SD卡、u盘、网络或flash)
  3. 使用“bootm (内核映像基址)”命令来启动内核

而这个uboot将参数按照协议处理好,并放入指定内存地址的过程,就发生在“bootm”命令中,下面我们仔细分析下bootm命令的执行。

1、bootm 命令主体流程

bootm命令的源码位于 common/cmd_bootm.c ,其中的 do_bootm 函数就是 bootm 命令的实现代码。


/*******************************************************************/
/* 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;

    /* 重载启动函数表 */
    if (!relocated) {
        int i;
        for (i = 0; i < ARRAY_SIZE(boot_os); 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_start() 这句非常重要,实际上这个就是 bootm 主要功能的开始。bootm_start() 主要目的是从 bootm 命令指定的内存地址中获取内核 uImage 的文件头(也就是在用 uboot 的 mkiamge 工具处理内核 zImage 时,添加的那64B的数据),核对并显示出其中包含的信息,并填充一个静态全局变量 static bootm_headers_t image, 该 image 变量的结构体类型为:

typedef struct bootm_headers {
    image_header_t    *legacy_hdr_os;        /* image header pointer */
    image_header_t    legacy_hdr_os_copy;    /* header copy */
    ulong        legacy_hdr_valid;
    int        verify;                       /* getenv("verify")[0] != 'n' */
    struct lmb    *lmb;                      /* for memory mgmt */
} bootm_headers_t;

image_info_t 结构体类型为:

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

该信息就是这个函数 do_bootm() 所调用的 boot_get_kernel() 及其子函数根据 uImage 的文件头打印出来的。

    /*
     * 我们已经到达了不可返回的位置: 我们正要覆盖所有的异常向量代码。
     * 所以我们再也无法简单地从任何失败中恢复
     * (突然让我想到张信哲的歌词:我们再也回不去了,对不对?)...
     */
    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

    /*
    bootm_load_os() 是第二个重要的启动过程节点,这个函数的作用是通过获取的文件头信息,将文件头后面所跟的内核映像放置到文件头信息规定的地址(如果是压缩内核,还在此函数中解压。但这个和zImage压缩内核不是一个概念,不要混淆)。

    平常我们在用 bootm 驱动内核的时候所看到的如下信息:
    XIP Kernel Image ... OK
    OK

    就是这个 bootm_load_os() 函数打印出来的。
    */
    ret = bootm_load_os(images.os, &load;_end, 1);
    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\n");
            } else {
                puts ("ERROR: new format image overwritten - "
                    "must RESET the board to recover\n");
                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

    /*
    这个语句是一个比较重要的节点,其功能是根据全局 static bootm_headers_t images 结构体的 image_info_t os 域中记录的 os 类型来将一个特定OS的内核引导函数入口赋给 boot_fn 变量。
    比如 我引导的是Linux内核,那么boot_fn就是 do_bootm_linux 。
    */
    boot_fn = boot_os[images.os.os];
    if (boot_fn == NULL) {
        if (iflag)
            enable_interrupts();
        printf ("ERROR: booting os '%s' (%d) is not supported\n",
                genimg_get_os_name(images.os.os), images.os.os);
        show_boot_progress (-8);
        return 1;
    }

    arch_preboot_os();

    /*
    如果不出错的话,这个函数应该是不会再返回了,因为在这个函数中会将控制权交由 OS 的内核。
    对于引导 Linux 内核来说,这里其实就是调用do_bootm_linux。
    */
    boot_fn(0, argc, argv, &images;);

    show_boot_progress (-9);
#ifdef DEBUG
    puts ("\n## Control returned to monitor - resetting...\n");
#endif
    do_reset (cmdtp, flag, argc, argv);

    return 1;
}

2、分析 do_bootm_linux

对我们来说非常重要的 do_bootm_linux() 位于:arch/arm/lib/bootm.c

//Bootm.c (arch\arm\lib):

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
    //这里获取了生成 cmdline标签 所需要的字符串:
    char *commandline = getenv ("bootargs");
#endif
    if ((flag != 0) && (flag != BOOTM_STATE_OS_GO))
        return 1;

    //注意:这里设备ID号可以从环境变量中获得!如果环境变量中有,就会覆盖之前赋值过的设备ID(最终通过 r1 传递给内核)。
    s = getenv ("machid");
    if (s) {
        machid = simple_strtoul (s, NULL, 16);
        printf ("Using machid 0x%x from environment\n", machid);
    }

    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;

    debug ("## Transferring control to Linux (at address %08lx) ...\n",
             (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 (&params);  //设置ATAG_SERIAL,依赖板级是否实现了get_board_serial函数
#endif
#ifdef CONFIG_REVISION_TAG
    setup_revision_tag (&params);//设置ATAG_REVISION,依赖板级是否实现了get_board_rev函数
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
    setup_memory_tags (bd);//设置ATAG_MEM,依赖于uboot的全局变量bd->bi_dram[i]中的数据
#endif
#ifdef CONFIG_CMDLINE_TAG
    setup_commandline_tag (bd, commandline);//设置ATAG_CMDLINE,依赖上面的字符串commandline中的数据
#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();

    //在进入内核前配置好芯片状态,以符合内核启动要求。
    //主要是关闭和清理缓存。
    //
    //不会再返回了。
    //跳入内核入口地址:r1=0、r1=machid、r2=启动参数指针
    kernel_entry(0, machid, bd->bi_boot_params);

    return 1;
}
static void announce_and_cleanup(void)
{
    printf("\nStarting kernel ...\n\n");

#ifdef CONFIG_USB_DEVICE
    {
        extern void udc_disconnect(void);
        udc_disconnect();
    }
#endif
    cleanup_before_linux();
}

Cpu.c (arch\arm\cpu\armv7) L1958 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;

对于上面启动环境的设定,可参考 Documentation/arm/Booting ,节选Booting中文翻译:

  1. 调用内核映像
    现有的引导加载程序: 强制
    新开发的引导加载程序: 强制
    调用内核映像 zImage 有两个选择。如果zImge是保存在flash中的,且其为了在flash中直接运行而被正确链接。这样引导加载程序就可以在flash中直接调用zImage。
    zImage也可以被放在系统RAM(任意位置)中被调用。注意:内核使用映像基地址的前16KB RAM空间来保存页表。建议将映像置于RAM的32KB处。
    对于以上任意一种情况,都必须符合以下启动状态:
  • 停止所有DMA设备,这样内存数据就不会因为虚假网络包或磁盘数据而被破坏。这可能可以节省你许多的调试时间。
  • CPU 寄存器配置
    r0 = 0,
    r1 = (在上面 (3) 中获取的)机器类型码.
    r2 = 标签列表在系统RAM中的物理地址,或
    设备树块(dtb)在系统RAM中的物理地址
  • CPU 模式
    所有形式的中断必须被禁止 (IRQs 和 FIQs)
    CPU 必须处于 SVC 模式。 (对于 Angel 调试有特例存在)
  • 缓存, MMUs
    MMU 必须关闭。
    指令缓存开启或关闭都可以。
    数据缓存必须关闭。
  • 引导加载程序应该通过直接跳转到内核映像的第一条指令来调用内核映像。

3、标签生成的函数举例分析:

所有标签生成函数都在Linux源码: arch/arm/lib/bootm.c 文件中,其实原理很简单,就是直接往指定的内存地址中写入标签信息

以下以 setup_start_tag() 和 setup_memory_tags() 为例分析:


static void setup_start_tag (bd_t *bd)
{
    params = (struct tag *) bd->bi_boot_params;   //params指向内存中标签列表中的基地址

    //直接往内存中按照内核定义的标签结构写入信息
    params->hdr.tag = ATAG_CORE;
    params->hdr.size = tag_size (tag_core);
    params->u.core.flags = 0;
    params->u.core.pagesize = 0;
    params->u.core.rootdev = 0;

    //根据本标签的大小数据,params跳到下一标签的起始地址
    params = tag_next (params);
}

static void setup_memory_tags (bd_t *bd)
{
    int i;

    //上一个标签已经将params指向了下一标签的基地址,所以这里可以直接使用
    for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
        params->hdr.tag = ATAG_MEM;
        params->hdr.size = tag_size (tag_mem32);
        //根据配置信息和uboot全局变量中的信息创建标签数据
        params->u.mem.start = bd->bi_dram[i].start;
        params->u.mem.size = bd->bi_dram[i].size;

        //根据本标签的大小数据,params跳到下一标签的起始地址
        params = tag_next (params);
    }
}

bootloader 完成了引导 Linux 内核所需要的准备之后将通过直接跳转,将控制权交由内核 zImage 。

三、内核从特定内存获取参数

在内核 zImage 开始运行后,首先是进行内核自解压,其过程在之前的博客中有详细介绍:《Linux内核源码分析–内核启动之(1)zImage自解压过程(Linux-3.0 ARMv7)》。其中对于内核标签列表的没有处理。在完成内核自解压之后,系统又恢复了bootloader设定的启动状态,将控制权交由解压后的内核。也就是说解压前后,系统启动环境不变。

解压后的内核开始运行后,首先是构架相关的汇编代码,其过程在之前的博客中有详细介绍:《Linux内核源码分析–内核启动之(2)Image内核启动(汇编部分)(Linux-3.0 ARMv7)》。其中对于内核标签列表的处理就是判断r2(内核启动参数)指针的有效性:验证指针指向的数据是否是有效的tagged list或者device tree,如果不是r2清零。

在运行完汇编代码后,就跳入了构架无关的C语言启动代码:init/main.c 中的 start_kernel(),在这个函数中开始了对内核启动参数的真正处理。

首先内核必须先要解析 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)
{
    //先让tags指针指向内核默认tagged list(init_tags)
    struct tag *tags = (struct tag *)&init_tags;

init_tags 结构体变量如下:

//此处在定义了 init_tags 结构体变量时,并初始化了其默认值。
//这个默认的 tagged list 实质上只定义了内存的参数。
/*
* 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 }
};
    struct machine_desc *mdesc = NULL, *p;
    //注意这个from的赋值,指向 default_command_line ,它是默认的内核cmdline,在内核配置的时候可指定。
    char *from = default_command_line;

对于 Linux 内核默认的 tagged list 中的内存起始地址进行初始化。

Boot options  --->
()  Default kernel command string
    //个人感觉这句有点奇怪,这个赋值为什么不直接放在变量定义的地方一起初始化呢?
    init_tags.mem.start = PHYS_OFFSET;

    /*
     * 在支持的设备列表中找到当前的设备。
     */
    for_each_machine_desc(p)
        if (nr == p->nr) {
            printk("Machine: %s\n", p->name);
            mdesc = p;
            break;
        }

    //内核编译的时候可能编译进了多个设备的支持,所以可能存在多个设备的描述结构体。这个通过bootloader传递进来的设备ID来匹配一个设备描述结构体。
    //如果上面没有找到匹配的设备描述结构体,则打印出错信息,并死循环。
    if (!mdesc) {
        early_print("\nError: unrecognized/unsupported machine ID"
            " (r1 = 0x%08x).\n\n", nr);
        dump_machine_table(); /* does not return */
    }


    if (__atags_pointer)
        tags = phys_to_virt(__atags_pointer);
    else if (mdesc->boot_params) {
#ifdef CONFIG_MMU
        /*
         * 我们依然运行在最小的MMU映射上,
         * 这假设设备默认将标签列表放在头1MB的RAM中。
         * 任何其他的位置将可能失败,
         * 并在此处静静地挂起内核。
         */
        if (mdesc->boot_params < PHYS_OFFSET ||
         mdesc->boot_params >= PHYS_OFFSET + SZ_1M) {
            printk(KERN_WARNING
             "Default boot params at physical 0x%08lx out of reach\n",
             mdesc->boot_params);
        } else
#endif
        {
            tags = phys_to_virt(mdesc->boot_params);
        }
    }

如果 bootloader 传递过来的 tagged list 有效,则将地址转换成虚拟地址,赋给tags。否则使用设备描述结构体中的数据。
例如:从这里也可以知道,设备描述结构体中的.boot_params数据是可选的,如果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

//如果tagged list的第一个tag不是 ATAG_CORE,说明tagged list 不存在或者有问题,打印错误信息并使用默认tagged list。
#if defined(CONFIG_DEPRECATED_PARAM_STRUCT)
    /*
     *  如果传递进来的是一个旧格式的参数, 将他们转换为
     *  一个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 , 那么就假假设一个合理的
         * 现代系统应该传入一个启动参数
         */
        early_print("Warning: Neither atags nor dtb found\n");
#endif
        tags = (struct tag *)&init_tags;
    }


    //如果此设备描述结构体中定义了fixup函数,就执行。从这里看出似乎这个函数是用于处理tagged list、cmdline和meminfo数据的。
    if (mdesc->fixup)
        mdesc->fixup(mdesc, tags, &from, &meminfo);

    //如果meminfo(其中保存了内存的bank信息)中已经初始化过了,就清除tagged list中mem_tags的信息(可导致跟在mem_tags之后的信息也一并失效)
    if (tags->hdr.tag == ATAG_CORE) {
        if (meminfo.nr_banks != 0)
            squash_mem_tags(tags);

    //备份tagged list信息到全局atags_copy。
    save_atags(tags);

    //逐个解析tag,主要功能是将每个tag的信息保存到内核全局变量中
    parse_tags(tags);

每个tag有对应的内核结构体:

struct tagtable {
    __u32 tag;                            //tag标识编号
    int (*parse)(const struct tag *);    //tag信息处理函数(一般是将其中的信息保存到内核全局变量中)
};

内核一般通过以下宏来定义一个 tagtable 结构体:

//也就是将所有定义好的tagtable结构体放入一个独立的".taglist.init"段中,使用时用一个for循环就可以遍历了。
#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn) \
        static struct tagtable __tagtable_##fn __tag = { tag, fn }

如果 tagged list 中的ATAG_CORE验证通过,就保存并解析 tag。


    //将form指向的字符串拷贝到boot_command_line字符数组中。
    /* parse_early_param 函数需要 boot_command_line */
    strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);

    //返回匹配的设备描述结构体指针。
    return mdesc;

}    

tag分析函数重点举例

对内存信息的处理

//将tag中的信息添加到全局的meminfo中去:arch/arm/include/asm/setup.h
static int __init parse_tag_mem32(const struct tag *tag)
{
    return arm_add_memory(tag->u.mem.start, tag->u.mem.size);

这些信息在内存子系统初始化的时候是会用到的,比如确定高低端内存的分界线。

/*
 * 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;
__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字符数组中。

#elif defined(CONFIG_CMDLINE_FORCE)
    pr_warning("Ignoring tag cmdline (using the default kernel command line)\n");

如果定义了“CONFIG_CMDLINE_FORCE”(强制使用配置内核时定义的cmdline),内核会忽略tag中cmdline

#else
    strlcpy(default_command_line, tag->u.cmdline.cmdline,
        COMMAND_LINE_SIZE);

如果以上两个配置都没有定义,则使用tag中cmdline覆盖到default_command_line字符数组中。

#endif
    return 0;
}

__tagtable(ATAG_CMDLINE, parse_tag_cmdline);

其他相关信息
其他所有的tag解析函数都是大同小异,都是将tag中的信息保存到各内核全局变量结构体中,以备后用。

四、内核处理cmdline

对于所有的tag中,我们最常用的就是cmdine,所以这里详细解析一下。

从上面的setup_machine_tags函数中我们知道,对于从tag传递到default_command_line中的cmdline字符串,内核又将其复制了一份到boot_command_line中。

在回到了setup_arch函数中之后,内核又把boot_command_line复制了一份到cmd_line字符数组中,并用cmdline_p指针指向这个cmd_line字符数组。

在完成了上面的工作后,cmdline已经从tag中到了多个全局字符数组中,也就是在内存中了,可以开始处理了。

这个cmdline的处理和tag的处理方法是一样的,每个cmdline中的参数都有对应的内核结构体:include/linux/init.h


struct obs_kernel_param {
    const char *str;             //参数标识字符串指针
    int (*setup_func)(char *);   //解析函数
    int early;                   //早期解析标志
};

/*
 * 仅用于真正的核心代码. 正常情况下详见 moduleparam.h.
 *
 * 强制对齐,使得编译器不会将obs_kernel_param "数组"中的元素放置在离
 * .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的, 不是
 * 当返回非零的时候发出警告!*/
#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);
    所有需要解析的参数都是通过__setup(str, fn)和early_param(str, fn)宏定义的,他们的差别仅在于是否为early参数。
    解析函数的作用是根据cmdline中的参数值设置全局变量。例如对“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 < MAX_INIT_ARGS; i++)
        argv_init[i] = NULL;
    return 1;
}
__setup("init=", init_setup);
其这样目的是为了将已经解析出的“init=”后的字符串指针赋给全局变量execute_command。而这个execute_command就是内核初始化到最后执行的用户空间初始化程序。

内核对于cmdline的处理分为两个步骤:早期处理和后期处理。

1、cmdline的早期处理
对于ARM构架,cmdline的早期处理是在setup_arch函数中的 parse_early_param();,但这个函数定义在init/main.c:


/* 检查早期参数. */
static int __init do_early_param(char *param, char *val)
{
    const struct obs_kernel_param *p;

    for (p = __setup_start; p < __setup_end; 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'\n", param);
        }
    }
    /* 这个阶段我们接受任何异常. */
    return 0;
}

此函数通过解析好的参数名及参数值,在上面介绍的“.init.setup”段中搜索匹配的“struct obs_kernel_param”结构体(必须标志为early,也就是用early_param(str, fn)宏定义的结构体),并调用参数处理函数。

void __init parse_early_options(char *cmdline)
{
    parse_args("early options", cmdline, NULL, 0, do_early_param);

这里通过统一的parse_args函数处理,此函数原型如下:


int parse_args(const char *name,
               char *args,
               const struct kernel_param *params,
               unsigned num,
               int (*unknown)(char *param, char *val))

这个函数的处理方法主要是分离出每个类似“foo=bar,bar2”的形式,再给 next_arg分离出参数名和参数值,并通过参数名在“ const struct kernel_param *params”指向的地址中搜索对应的数据结构,并调用其参数处理函数。如果没有找到就调用最后一个参数“unknown”传递进来的未知参数处理函数。

由于此处params为NULL,必然找不到对应的数据结构,所有分离好的参数及参数名都由最后一个函数指针参数指定的函数 do_early_param来处理。也就是上面那个函数。

}

/*  构架相关代码在早期调用这个函数, 如果没有, 会在解析其他参数前再次调用这个函数。 */
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复制到一个临时变量,并在下面的函数中使用

    parse_early_options(tmp_cmdline);
    done = 1;

对这个静态变量置1,标志着这个函数已经执行过。不需要再次执行。

}
一个典型的早期参数就是“mem=”,之所以会放在前期处理,是因为内存参数对于系统初始化很重要,在这里处理完后,下面马上就要用到这些数据了。
处理函数如下:


/*
 * 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);

这个函数上面介绍过了,就是把获取的内存大小和基地址添加到全局的meminfo结构体中。


    return 0;
}
early_param("mem", early_mem);

2、cmdline的后期分类处理

在上面的早期处理完成之后,系统就继续初始化。在从setup_arch(&command;_line);返回不久就将cmdline又进行了一次备份,使用的是bootmem内存分配系统:

setup_command_line(command_line);

对cmdline进行备份和保存:

/* 为处理的command line备份 (例如eg. 用于 /proc) */
char *saved_command_line;
/* 用于参数处理的command line */
static char *static_command_line;

之后就打印出内核cmdline并解析后期参数和模块参数。源码如下:

printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);

打印出完整的内核cmdline

    parse_early_param();

解析内核早期参数,但是对于ARM构架来说,在setup_arch函数中已经调用过了。所以这里什么都不做。

    parse_args("Booting kernel", static_command_line, __start___param,
         __stop___param - __start___param,
         &unknown_bootoption);

这里调用的parse_args就比较复杂了,我这里简单地分析一下:
在这个函数主要是一个循环,逐一分析完整的cmdline中的每个参数:
使用next_arg函数解析出类似“foo=bar,bar2”的形式中的参数名(foo)和参数值(bar和bar2)
使用parse_one根据参数名在内核内建模块的参数处理段(param)中搜索每一个“struct kernel_param”,是否为某个内核内建模块的参数:
如果是,则使用搜索到的那个“struct kernel_param”结构体中的参数设置函数“.ops->set”来设置模块的参数
如果不是,就使用unknown_bootoption函数处理,就是到内核的“.init.setup”段搜索,看是不是“非早期”内核启动参数(使用
setup(str, fn)宏定义的参数)。如果是的话,就用相应的函数来处理,这个和“早期”参数处理是一样的。如果不是,可能会打印错误信息。

到了这里,内核的cmdline处理就到此结束了。只有内置模块才会获取到cmdline中的参数,因为内建模块无法通过其他形式获取参数,不像.ok模块可以在挂载的时候从命令行获取参数。

如果你自己的外置模块(.ok)中需要参数,就算是你在内核启动cmdline中加了参数,模块挂载的时候也是没法自动获取。你必须在使用insmod挂载模块的时候,在最后加上你要的设置的参数信息。或者通过/proc/cmdline获取启动参数,然后用shell命令过滤出需要的参数字符串,并加到insmod命令的最后。


作者:tekkamanninja

http://blog.chinaunix.net/uid-20543672-id-3151113.html