linux内核启动2_setup_arch函数

执行setup_arch()函数
回到start_kernel当中,569行,调用setup_arch函数,传给他的参数是那个未被初始化的内部变量command_line。这个setup_arch()函数是start_kernel阶段最重要的一个函数,每个体系都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个体系的setup_arch()函数,由顶层Makefile中的ARCH变量决定:
它首先通过检测出来的处理器类型进行处理器内核的初始化,然后通过 bootmem_init()函数根据系统定义的 meminfo 结构进行内存结构的初始化,最后调用paging_init()开启 MMU,创建内核页表,映射所有的物理内存和 IO空间。
start_kernel ()
–> setup_arch ()
–> paging_init ()
–> bootmem_init ()
–> alloc_bootmem_low_pages ()
第569行setup_arch(&command_line)在arch/arm/kernel/setup.c定义如下:
767 void __init setup_arch(char **cmdline_p)
768 {
769 struct tag tags = (struct tag )&init_tags;
770 struct machine_desc *mdesc;
771 char *from = default_command_line;
772
773 unwind_init();
774
775 setup_processor();
776 mdesc = setup_machine(machine_arch_type);
777 machine_name = mdesc->name; //machine_name在126行定义static const char *machine_name;
778
779 if (mdesc->soft_reboot) //这个变量初始值为”h”,如果这里设置成softboot,它会将这个初始值变为”s”
780 reboot_setup(“s”);
781
782 if (__atags_pointer) //检查BootLoader是否传入参数
783 tags = phys_to_virt(__atags_pointer);
784 else if (mdesc->boot_params)//machine descriptor中设置的启动参数地址(arch/arm/mach-s3c2410/mach-smdk2410.c)
785 tags = phys_to_virt(mdesc->boot_params);
786
787 #if defined(CONFIG_DEPRECATED_PARAM_STRUCT)
788 /*
789 * If we have the old style parameters, convert them to
790 * a tag list.
791 */
792 if (tags->hdr.tag != ATAG_CORE)//内核参数列表第一项必须是ATAG_CORE类型,如果不是,则需要转换成新的内核参数类型,新的内核参数类型用下面 struct tag结构表示,由bootloader[u-boot-1.1.5]传递到物理地址0x30000100处的参数类型是tag list结构,在u-boot-1.1.6后是采用了新的内核参数类型struct tag结构
793 convert_to_tag_list(tags);//此函数完成新旧参数结构转换,将参数结构转换为tag list结构
794 #endif
795 if (tags->hdr.tag != ATAG_CORE)//如果没有内核参数
796 tags = (struct tag *)&init_tags;//则选用默认的内核参数,init_tags文件中有定义。
797
798 if (mdesc->fixup) //用内核参数列表填充meminfo,fixup函数出现在注册machine_desc中,即MACHINE_START、MACHINE_END定义中,这个函数,有些板子有,但在2410中没有定义这个函数。
799 mdesc->fixup(mdesc, tags, &from, &meminfo);
800
801 if (tags->hdr.tag == ATAG_CORE) {
802 if (meminfo.nr_banks != 0) //说明内存被初始化过
803 squash_mem_tags(tags);//如果是tag list,那么如果系统已经创建了默认的meminfo.nr_banks,清除tags中关于MEM的参数,以免再次被初始化
804 save_atags(tags);
805 parse_tags(tags);//做出一些针对各个tags的处理
806 }
807 //下面是记录内核代码的起始,结束虚拟地址
808 init_mm.start_code = (unsigned long) _text;
809 init_mm.end_code = (unsigned long) _etext;
810 init_mm.end_data = (unsigned long) _edata;
811 init_mm.brk = (unsigned long) _end;
812
813 /* parse_early_param needs a boot_command_line */
814 strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
815
816 /* populate cmd_line too for later use, preserving boot_command_line */
817 strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
818 *cmdline_p = cmd_line;
819
820 parse_early_param(); //解释命令行参数,见后
821
822 arm_memblock_init(&meminfo, mdesc);
823
824 paging_init(mdesc);
825 request_standard_resources(&meminfo, mdesc);//将设备实体登记注册到总线空间链表
826
827 #ifdef CONFIG_SMP
828 smp_init_cpus();
829 #endif
830 reserve_crashkernel(); //要配置CONFIG_KEXEC,否则为空函数,2410中没有配置
831
832 cpu_init(); //对用到的IRQ, ABT, UND模式的SP都作了初始化
833 tcm_init(); //在ARM中是个空函数
834
835 /* 根据arch/arm/mach-s3c2410/mach-smdk2410.c中machine_desc结构,
设置各种特定于体系结构的指针
836 * Set up various architecture-specific pointers
837 */
838 arch_nr_irqs = mdesc->nr_irqs; //在arch/arm/kernel/irq.c第171行,由nr_irqs = arch_nr_irqs ? arch_nr_irqs : NR_IRQS;引用,而nr_irqs在init_IRQ、asm_do_IRQ、set_irq_flags中都有引用

839 init_arch_irq = mdesc->init_irq; //在init_IRQ函数第165行调用
840 system_timer = mdesc->timer;
841 init_machine = mdesc->init_machine;
842
843 #ifdef CONFIG_VT
844 #if defined(CONFIG_VGA_CONSOLE)
845 conswitchp = &vga_con;
846 #elif defined(CONFIG_DUMMY_CONSOLE)
847 conswitchp = &dummy_con;
848 #endif
849 #endif
850 early_trap_init(); //见后
851 }
852

769行tag数据结构在arch/arm/include/asm/setup.h中定义如下:
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;

    /*
     * Acorn specific
     */
    struct tag_acorn    acorn;

    /*
     * DC21285 specific
     */
    struct tag_memclk    memclk;
} u;

};
其中init_tags在arch/arm/kernel/setup.c文件下定义如下
662 static struct init_tags {
663 struct tag_header hdr1;
664 struct tag_core core;
665 struct tag_header hdr2;
666 struct tag_mem32 mem;
667 struct tag_header hdr3;
668 } init_tags __initdata = {
669 { tag_size(tag_core), ATAG_CORE },
670 { 1, PAGE_SIZE, 0xff },
671 { tag_size(tag_mem32), ATAG_MEM },
672 { MEM_SIZE, PHYS_OFFSET },
673 { 0, ATAG_NONE }
674 };
675
676 static void (*init_machine)(void) __initdata;
770行完整的machine_desc结构描述如下:arch/arm/include/asm/mach/arch.h
13 struct tag;
14 struct meminfo;
15 struct sys_timer;
16
17 struct machine_desc {
18 /*
19 * Note! The first four elements are used
20 * by assembler code in head.S, head-common.S
21 */
22 unsigned int nr; /* 开发板的机器类型ID */
23 unsigned int nr_irqs; /* number of IRQs */
24 unsigned int phys_io; /* 起始IO物理地址 */
25 unsigned int io_pg_offst; /* byte offset for io
26 * page tabe entry */
27
28 const char name; / 开发板名称 */
29 unsigned long boot_params; /* tagged list内核启动参数的地址*/
30
31 unsigned int video_start; /* start of video RAM */
32 unsigned int video_end; /* end of video RAM */
33
34 unsigned int reserve_lp0 :1; /* never has lp0 */
35 unsigned int reserve_lp1 :1; /* never has lp1 */
36 unsigned int reserve_lp2 :1; /* never has lp2 */
37 unsigned int soft_reboot :1; /* soft reboot */
38 void (fixup)(struct machine_desc ,
39 struct tag , char *,
40 struct meminfo *);
41 void (reserve)(void);/ reserve mem blocks */
42 void (map_io)(void);/*IO映射函数(在这里修改时钟频率)/
43 void (init_irq)(void); /中断初始化函数*/
44 struct sys_timer timer; / system tick timer */
45 void (*init_machine)(void);
46 };

771行default_command_line在setup.c文件129行中定义如下:
static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
其中CONFIG_CMDLINE在“.config”配置文件中定义的。
773行arch/arm/kernel/unwind.c
439 int __init unwind_init(void)
440 {
441 struct unwind_idx *idx;
442
443 /* Convert the symbol addresses to absolute values */
444 for (idx = __start_unwind_idx; idx < __stop_unwind_idx; idx++)
445 idx->addr = prel31_to_addr(&idx->addr);
446
447 pr_debug(“unwind: ARM stack unwinding initialised\n”);
448
449 return 0;
450 }
struct unwind_idx {
unsigned long addr;
unsigned long insn;
};
776行,machine_arch_type在arch/arm/tools/gen-mach-types中第69行定义,
printf(“#define machine_arch_type\t__machine_arch_type\n”)
它最后会被转换成一个头文件include/generated/mach-types.h,包含在arch/arm/include/asm/mach- types.h文件中,__machine_arch_type在arch/arm/kernel/head-common.S定义
27 .long __machine_arch_type @ r5
并通过arch/arm/kernel/head-common.S第60行命令
60 str r1, [r5] @ Save machine type
将bootloader通过r1传递过来的机器码保存到__machine_arch_type。
下面我们来看setup_machine函数,在在arch/arm/kernel/setup.c文件下定义如下
391 static struct machine_desc * __init setup_machine(unsigned int nr)
392 {
393 struct machine_desc *list;
394
395 /*
396 * locate machine in the list of supported machines.
397 */
398 list = lookup_machine_type(nr);
399 if (!list) {
400 printk(“Machine configuration botched (nr %d), unable ”
401 “to continue.\n”, nr);
402 while (1);
403 }
404
405 printk(“Machine: %s\n”, list->name);
406
407 return list;
408 }
在这个函数中就是查找你是什么版本的处理器架构,最后就是调用了lookup_processor_type这个函数,它在汇编部分也提到过,在arch/arm/kernel/head-common.S定义
230 ENTRY(lookup_machine_type)
231 stmfd sp!, {r4 - r6, lr}
232 mov r1, r0
233 bl __lookup_machine_type
234 mov r0, r5
235 ldmfd sp!, {r4 - r6, pc}
236 ENDPROC(lookup_machine_type)
可见最后调用的是__lookup_machine_type,这个函数在汇编中我们已经分析过了。这里再来分析一下:
内核中对于每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义开发板相关的一些属性及函数,比如机器类型ID、起始I/O物理地址、Bootloader传入的参数的地址、中断初始化函数、I/O映射函数等,比如对于 SMDK2410开发板,在arch/arm/mach-s3c2410/mach-smdk2410.c中定义如下:
MACHINE_START(SMDK2410, “SMDK2410”) /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
而宏MACHINE_START、MACHINE_END在arch/arm/include/asm/mach/arch.h中定义如下:
52 #define MACHINE_START(_type,_name) \
53 static const struct machine_desc _mach_desc##_type \
54 __used \
55 attribute((section(“.arch.info.init”))) = { \
56 .nr = MACH_TYPE_##_type, \
57 .name = _name,
58
59 #define MACHINE_END \
60 };
所以以下展开后如下:
static const struct machine_desc __mach_desc_SMDK2410
__used
attribute((section(“.arch.info.init”))) = {
.nr = MACH_TYPE_SMDK2410,
.name = SMDK2410,
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
};
对照前面完整的machine_desc结构描述就能找到系统定义开发板相关的一些属性及函数
所有machine_desc结构体初始化后被编译连接到“.arch.info.init”段中,在连接内核时,它们被组织在一起,开始地址为 __arch_info_begin,结束地址为__arch_info_end,从连接脚本“arch/arm/kernel /vmlinux.lds.S”中可以看出:
37 __arch_info_begin = .; //machine_desc结构体的开始地址
38 *(.arch.info.init) //所有machine_desc都在这里面
39 __arch_info_end = .; //machine_desc结构体的开始地址
不同的machine_desc结构用于不同的开发板,U-BOOT调用内核时,会在r1寄存器中给出开发板的标记(机器类型ID);对于S3C2410、 S3C2440开发板,U-Boot传入的机器类型ID为MACH_TYPE_SMDK2410、MACH_TYPE_S3C2440,它们对应的 machine_desc结构分别在arch/arm/mach-s3c2410/mach-smdk2410.c和
arch/arm/mach-s3c2440/mach-smdk2440.c中定义,现在再来看看__lookup_machine_type函数。它在arch/arm/kernel/head-common.S中定义如下:
196 4: .long . //当前行编译链接后的虚拟地址
197 .long __arch_info_begin //machine_desc结构体的开始地址(虚拟地址)
198 .long __arch_info_end //machine_desc结构体的结束地址(虚拟地址)

211 __lookup_machine_type:
212 adr r3, 4b //r3 = 196行的物理地址
213 ldmia r3, {r4, r5, r6} /*[r3]->r4,[r3+4]->r5,[r3+8]->r6,
r4 = 196行的虚拟地址,
r5 = __arch_info_begin (VA)
r6 = __arch_info_end (VA)*/
214 sub r3, r3, r4 @r3=(196行的物理地址)-(196行的虚拟地址),即物理地址与虚拟地址的差值
215 add r5, r5, r3 @ r5= __arch_info_begin(物理地址),虚拟地址转物理地址
216 add r6, r6, r3 @ r6= __arch_info_end(物理地址)
217 1: ldr r3, [r5, #MACHINFO_TYPE] @ r5是machine_desc结构体地址
218 teq r3, r1 @ matches loader number? r1是Bootloader调用内核时传入的机器类型ID
219 beq 2f @ found 相等则跳转到224行
220 add r5, r5, #SIZEOF_MACHINE_DESC @ 否则跳转到下一个machine_desc结构体
其中MACHINFO_TYPE,SIZEOF_MACHINE_DESC 在arch/arm/kernel/asm-offsets.c中定义:
DEFINE(MACHINFO_TYPE, offsetof(struct machine_desc, nr));
DEFINE(SIZEOF_MACHINE_DESC, sizeof(struct machine_desc));
offsetof在include/linux/stddef.h中定义:

define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

可以看出nr是函数setup_machine传递过来的参数,这里得到的是machine_desc结构体地址。
221 cmp r5, r6 是否已经比较完所有的machine_desc结构体
222 blo 1b 没有则继续比较(lo:无符号小于)
223 mov r5, #0 @ 比较完毕,但没有匹配的machine_desc结构体,r5=0
224 2: mov pc, lr 返回
225 ENDPROC(__lookup_machine_type)
上面代码功能说明:
__lookup_machine_type 函数将这个r1寄存器中的机器类型ID与“.arch.info.init”段中machine_desc结构中的nr成员比较,如果相等则表示找到了匹配的machine_desc结构,于是返回它的地址(存于r5中),如果__arch_machine_begin和 __arch_machine_end间所有machine_desc结构的nr成员都不等于r1寄存器中的值,则返回0(r5等于0)

在配置菜单时,选中两个开发板即可:
System Type—>
S3C2410 Machines—>
[*] SMDK2410/A9M2410
S3C2440 Machines—>
[*] SMDK2440
第780行reboot_setup函数在arch/arm/kernel/process.c定义如下:
198 static char reboot_mode = ‘h’;
199
200 int __init reboot_setup(char *str)
201 {
202 reboot_mode = str[0];
203 return 1;
204 }
205
206 __setup(“reboot=”, reboot_setup);
reboot这个参数在引导参数中设置。
第782行__atags_pointer是bootloader传递参数的物理地址,见第一阶段启动源码arch/arm/kernel/head-common.S第61行(str r2, [r6]),此处r6就是__atags_pointer的地址,__atags_pointer在28处有定义,它将bootloader通过r2传递过来的地址参数存放到 __atags_pointer。
第783、784行,由于MMU单元已打开,此处__atags_pointer是物理地址,需要转换成虚拟地址才能访问,因为此时CPU访问的都是虚拟地址,在这里如果bootloader没有传递地址参数则就使用arch/arm/mach-s3c2410 /mach-smdk2410.c文件中MACHINE_START、MACHINE_END定义的boot_params参数,对2410定义如下:
.boot_params = S3C2410_SDRAM_PA + 0x100,
S3C2410_SDRAM_PA在arch/arm/mach-s3c2410/include/mach/map.h中定义如下

define S3C2410_CS6 (0x30000000)

define S3C2410_CS7 (0x38000000)

define S3C2410_SDRAM_PA (S3C2410_CS6)

所以.boot_params参数地址是0x30000100位置
801 if (tags->hdr.tag == ATAG_CORE) {
802 if (meminfo.nr_banks != 0) //说明内存被初始化过
803 squash_mem_tags(tags);//如果是tag list,那么如果系统已经创建了默认的meminfo.nr_banks,清除tags中关于MEM的参数,以免再次被初始化
804 save_atags(tags);
805 parse_tags(tags);//做出一些针对各个tags的处理
806 }
803行squash_mem_tags定义如下(arch/arm/kernel/setup.c)
static void __init squash_mem_tags(struct tag *tag)
{
for (; tag->hdr.size; tag = tag_next(tag))
if (tag->hdr.tag == ATAG_MEM)
tag->hdr.tag = ATAG_NONE;
}
这个函数功能是清除tags中关于MEM的参数,以免再次被初始化
804行save_atags定义如下(arch/arm/kernel/atags.c)

define BOOT_PARAMS_SIZE 1536

static char __initdata atags_copy[BOOT_PARAMS_SIZE];

void __init save_atags(const struct tag *tags)
{
memcpy(atags_copy, tags, sizeof(atags_copy));
}
805行parse_tags定义如下(arch/arm/kernel/setup.c)
static void __init parse_tags(const struct tag *t)
{
for (; t->hdr.size; t = tag_next(t))
if (!parse_tag(t)) //针对每个tag 调用parse_tag 函数
printk(KERN_WARNING
“Ignoring unrecognised tag 0x%08x\n”,
t->hdr.tag);
}
这个函数解析内核参数列表,然后调用内核参数列表的处理函数对这些参数进行处理。比如,如果列表为命令行,则最终会用parse_tag_cmdlin函数进行解析,这个函数用_tagtable编译连接到了内核里,其中如果U-boot传入ATAG_CMDLINE,则使用U-boot传入的 bootargs覆盖default_command_line
static int __init parse_tag(const struct tag *tag)
{
extern struct tagtable __tagtable_begin, __tagtable_end;
struct tagtable *t;

for (t = &__tagtable_begin; t < &__tagtable_end; t++) //遍历tagtable列表,并调用处理函数,
    if (tag->hdr.tag == t->tag) {
        t->parse(tag);
        break;
    }

return t < &__tagtable_end;

}
arch/arm/include/asm/setup.h
struct tagtable {
__u32 tag;
int (parse)(const struct tag );
};

define tag_next(t) ((struct tag )((__u32 )(t) + (t)->hdr.size))

define tag_size(type) ((sizeof(struct tag_header) + sizeof(struct type)) >> 2)

define for_each_tag(t,base) \

for (t = base; t->hdr.size; t = tag_next(t))

ifdef KERNEL

define tag __used __attribute((section(“.taglist.init”)))

define __tagtable(tag, fn) \

static struct tagtable _tagtable##fn __tag = { tag, fn }
这个tagtable 列表 是怎么形成的?
如arch/arm/kernel/setup.c
556 static int __init parse_tag_mem32(const struct tag *tag)
557 {
558 return arm_add_memory(tag->u.mem.start, tag->u.mem.size);
559 }
560
561 __tagtable(ATAG_MEM, parse_tag_mem32);

607 __tagtable(ATAG_SERIAL, parse_tag_serialnr);
608
609 static int __init parse_tag_revision(const struct tag *tag)
610 {
611 system_rev = tag->u.revision.rev;
612 return 0;
613 }
614
615 __tagtable(ATAG_REVISION, parse_tag_revision);

618 static int __init parse_tag_cmdline(const struct tag *tag)
619 {
620 strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
621 return 0;
622 }
623
624 __tagtable(ATAG_CMDLINE, parse_tag_cmdline);
根据前面相关宏定义,__tagtable(ATAG_CMDLINE, parse_tag_cmdline)展开后为
static struct tagtable tagtable_parse_tag_cmdline __used __attribute((section(“.taglist.init”))) = { ATAG_CMDLINE, parse_tag_cmdline }
再参看arch/arm/kernel/vmlinux.lds.S文件
34 __proc_info_begin = .;
35 *(.proc.info.init)
36 __proc_info_end = .;
37 __arch_info_begin = .;
38 *(.arch.info.init)
39 __arch_info_end = .;
40 __tagtable_begin = .;
41 *(.taglist.init)
42 __tagtable_end = .;
tagtable 列表编译连接后被存放在.taglist.init中。

从808行到811行是init_mm的初始化,记录内核代码的起始,结束虚拟地址
808 init_mm.start_code = (unsigned long) _text; 内核代码段开始
809 init_mm.end_code = (unsigned long) _etext; 内核代码段结束
810 init_mm.end_data = (unsigned long) _edata; 内核数据段开始
811 init_mm.brk = (unsigned long) _end; 内核数据段结束
_text,_etext,_edata,_end参见arch/arm/kernel/vmlinux.lds.S 链接脚本,init_mm定义在mm/init-mm.c
12 #ifndef INIT_MM_CONTEXT
13 #define INIT_MM_CONTEXT(name)
14 #endif
15
16 struct mm_struct init_mm = {
17 .mm_rb = RB_ROOT,
18 .pgd = swapper_pg_dir,
19 .mm_users = ATOMIC_INIT(2),
20 .mm_count = ATOMIC_INIT(1),
21 .mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
22 .page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
23 .mmlist = LIST_HEAD_INIT(init_mm.mmlist),
24 .cpu_vm_mask = CPU_MASK_ALL,
25 INIT_MM_CONTEXT(init_mm)
26 };

每一个任务都有一个mm_struct结构管理任务内存空间,init_mm是内核的mm_struct,设置* pgd=swapper_pg_dir,swapper_pg_dir是内核的页目录,在arm体系结构有16k,所以init_mm定义了整个 kernel的内存空间,下面我们会碰到内核线程,所有的内核线程都使用内核空间,拥有和内核同样的访问权限。对于内存管理后面再做详细分析。
mm_struct结构定义在include/linux/mm_types.h中,
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */

ifdef CONFIG_MMU

unsigned long (*get_unmapped_area) (struct file *filp,
            unsigned long addr, unsigned long len,
            unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);

endif

unsigned long mmap_base;        /* base of mmap area */
unsigned long task_size;        /* size of task vm space */
unsigned long cached_hole_size;     /* if non-zero, the largest hole below free_area_cache */
unsigned long free_area_cache;        /* first hole of size cached_hole_size or larger */
pgd_t * pgd;
atomic_t mm_users;            /* How many users with user space? */
atomic_t mm_count;            /* How many references to "struct mm_struct" (users count as 1) */
int map_count;                /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock;        /* Protects page tables and some counters */

struct list_head mmlist;        /* List of maybe swapped mm's.    These are globally strung
                     * together off init_mm.mmlist, and are protected
                     * by mmlist_lock
                     */


unsigned long hiwater_rss;    /* High-watermark of RSS usage */
unsigned long hiwater_vm;    /* High-water virtual memory usage */

unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;

unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

/*
 * Special counters, in some configurations protected by the
 * page_table_lock, in other configurations by being atomic.
 */
struct mm_rss_stat rss_stat;

struct linux_binfmt *binfmt;

cpumask_t cpu_vm_mask;

/* Architecture-specific MM context */
mm_context_t context;

/* Swap token stuff */
/*
 * Last value of global fault stamp as seen by this process.
 * In other words, this value gives an indication of how long
 * it has been since this task got the token.
 * Look at mm/thrash.c
 */
unsigned int faultstamp;
unsigned int token_priority;
unsigned int last_interval;

unsigned long flags; /* Must use atomic bitops to access the bits */

struct core_state *core_state; /* coredumping support */

ifdef CONFIG_AIO

spinlock_t        ioctx_lock;
struct hlist_head    ioctx_list;

endif

ifdef CONFIG_MM_OWNER

/*
 * "owner" points to a task that is regarded as the canonical
 * user/owner of this mm. All of the following must be true in
 * order for it to be changed:
 *
 * current == mm->owner
 * current->mm != mm
 * new_owner->mm == mm
 * new_owner->alloc_lock is held
 */
struct task_struct *owner;

endif

ifdef CONFIG_PROC_FS

/* store ref to file /proc//exe symlink points to */
struct file *exe_file;
unsigned long num_exe_file_vmas;

endif

ifdef CONFIG_MMU_NOTIFIER

struct mmu_notifier_mm *mmu_notifier_mm;

endif

};

第814行,下面是对命令行的处理,刚才在参数列表处理parse_tag_cmdline函数已把命令行拷贝到了from空间(from前面定义成了default_command_line),这里814行是把命令行拷贝到boot_command_line,boot_command_line在后面的parse_early_param为用到,boot_command_line在init/main.c中定义
char __initdata boot_command_line[COMMAND_LINE_SIZE];指定变量存放在__initdata区
第817行,填充cmd_line,以备以后使用,维护boot_command_line,cmd_line在127行定义
static char __initdata cmd_line[COMMAND_LINE_SIZE];指定变量存放在__initdata区
__initdata在前面的分析中出现了好几次,在这里我们来对它详细分析
Linux在arch/arm/kernel/vmlinux.lds.S中定义了.init段。__init和__initdata属性的数据都在这个段中,当内核启动完毕后,这个段中的内存会被释放掉供其他使用。
__init和__initdata宏定义如下(include/linux/init.h):

define __init __section(.init.text) __cold notrace

define __initdata __section(.init.data)

vmlinux.lds.S有如下内容:
/* arch/arm/kernel/vmlinux.lds.S*/
20 SECTIONS
21 {
22 #ifdef CONFIG_XIP_KERNEL
23 . = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
24 #else
25 . = PAGE_OFFSET + TEXT_OFFSET;
26 #endif
27
28 .init : { /* Init code and data */
29 _stext = .;
30 _sinittext = .;
31 HEAD_TEXT
32 INIT_TEXT
33 _einittext = .;
34 __proc_info_begin = .;
35 *(.proc.info.init)
36 __proc_info_end = .;
37 __arch_info_begin = .;
38 *(.arch.info.init)
39 __arch_info_end = .;
40 __tagtable_begin = .;
41 *(.taglist.init)
42 __tagtable_end = .;
43
44 INIT_SETUP(16)
45
46 INIT_CALLS
47 CON_INITCALL
48 SECURITY_INITCALL
49 INIT_RAM_FS
50
51 #ifndef CONFIG_XIP_KERNEL
52 __init_begin = _stext;
53 INIT_DATA
54 #endif
55 }

第32的INIT_TEXT和第53行INIT_DATA在include/asm-generic/vmlinux.lds.h定义如下
457 /* init and exit section handling */
458 #define INIT_DATA \
459 *(.init.data) \
460 DEV_DISCARD(init.data) \
461 CPU_DISCARD(init.data) \
462 MEM_DISCARD(init.data) \
463 KERNEL_CTORS() \
464 *(.init.rodata) \
465 MCOUNT_REC() \
466 DEV_DISCARD(init.rodata) \
467 CPU_DISCARD(init.rodata) \
468 MEM_DISCARD(init.rodata)
469
470 #define INIT_TEXT \
471 *(.init.text) \
472 DEV_DISCARD(init.text) \
473 CPU_DISCARD(init.text) \
474 MEM_DISCARD(init.text)
475
可以发现__init对应的section(.init.text)和__initdata对应的section(.init.data)都在.init 段中。同样,这里定义的其他一些section也都会在使用完后被释放,如.init.setup,.initcall1.init等。
释放memory的大小会在系统启动过程中打印出来:
eth0: link up
IP-Config: Complete:
device=eth0, addr=192.168.167.15, mask=255.255.255.0, gw=192.168.1.1,
host=192.168.167.15, domain=, nis-domain=(none),
bootserver=192.168.1.8, rootserver=192.168.1.54, rootpath=
Looking up port of RPC 100003/2 on 192.168.167.170
Looking up port of RPC 100005/1 on 192.168.167.170
VFS: Mounted root (nfs filesystem).
Freeing init memory: 128K

在分析820行的parse_early_param函数前,先说一下系统启动时对bootloader传递参数的初始化,即linux启动参数的实现,启动参数的实现,我们知道boot传递给内核的参数都是 “name_varibale=value”这种形式的,如下:
CONFIG_CMDLINE=”root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M”
那么内核如何知道传递进来的参数该怎么去处理呢?
内核是通过__setup宏或者early_param宏来将参数与参数所处理的函数相关联起来的。我们接下来就来看这个宏以及相关的结构的定义:
include/linux/init.h
230 #define __setup_param(str, unique_id, fn, early) \
231 static const char _setup_str##unique_id[] __initconst \
232 __aligned(1) = str; \
233 static struct obs_kernel_param _setup##unique_id \
234 __used __section(.init.setup) \
235 attribute((aligned((sizeof(long))))) \
236 = { _setup_str##unique_id, fn, early }
237
238 #define __setup(str, fn) \
239 __setup_param(str, fn, fn, 0)
240
241 /* NOTE: fn is as per module_param, not __setup! Emits warning if fn
242 * returns non-zero. */
243 #define early_param(str, fn) \
244 __setup_param(str, fn, fn, 1)
看起来很复杂。 首先setup宏第一个参数是一个key。比如”netdev=”这样的,而第二个参数是这个key对应的处理函数。这里要注意相同的handler能联系到不同的key。__setup与early_param不同的是,early_param宏注册的内核选项必须要在其他内核选项之前被处理。在函数start_kernel中,parse_early_param处理early_param定义的参数,parse_args处理__setup定义的参数。
early_param和setup唯一不同的就是传递给__setup_param的最后一个参数,这个参数下面会说明,而接下来_setup_param定义了一个struct obs_kernel_param类型的结构,然后通过_section宏,使这个变量在链接的时候能够放置在段.init.setup(后面会详细介绍内核中的这些初始化段).

接下来来看struct obs_kernel_param结构:
include/linux/init.h
218 struct obs_kernel_param {
219 const char *str;
220 int (setup_func)(char );
221 int early;
222 };

前两个参数很简单,一个是key,一个是handler。最后一个参数其实也就是类似于优先级的一个flag,因为传递给内核的参数中,有一些需要比另外的一些更早的解析。(这也就是为什么early_param和setup传递的最后一个参数的不同的原因了。
启动参数的实现
1,所有的系统启动参数都是由形如
static int __init init_setup(char *str)
的函数来支持的
init/main.c
static int __init init_setup(char *str)
{
unsigned int i;

execute_command = str;
for (i = 1; i < MAX_INIT_ARGS; i++)
    argv_init[i] = NULL;
return 1;

}
__setup(“init=”, init_setup);
注:(include/linux/init.h):

define __init __section(.init.text) __cold notrace申明所有的启动参数支持函数都放入.init.text段

2.1,用__setup宏来导出参数的支持函数
__setup(“init=”, init_setup);
展开后就是如下的形式

static const char __setup_str_init_setup[] __initdata = “init=”;
static struct obs_kernel_param __setup_init_setup
used __section(“.init.setup”)
attribute((aligned((sizeof(long)))))
= { __setup_str_init_setup, init_setup, 0 };//”init=”,init_setup,0
也就是说,启动参数(函数指针)被封装到obs_kernel_param结构中,
所有的内核启动参数形成内核映像.init.setup段中的一个
obs_kernel_param数组

2.2用early_param宏来申明需要’早期’处理的启动参数,例如在
arch/arm/kernel/setup.c就有如下的申明:
468 early_param(“mem”, early_mem);
展开后和__setup是一样的只是early参数不一样,因此会在do_early_param
中被处理
443 static int __init early_mem(char *p)
444 {
445 static int usermem __initdata = 0;
446 unsigned long size, start;
447 char *endp;
448
449 /*
453 */
454 if (usermem == 0) {
455 usermem = 1;
456 meminfo.nr_banks = 0;
457 }
458
459 start = PHYS_OFFSET;
460 size = memparse(p, &endp);
461 if (*endp == ‘@’)
462 start = memparse(endp + 1, NULL);
463
464 arm_add_memory(start, size);
465
466 return 0;
467 }

3,内核对启动参数的解析:下面函数历遍obs_kernel_param数组,调用
支持函数
static int __init do_early_param(char *param, char *val)
这个函数在parse_early_param中被调用,而parse_early_param在start_kernel
中被调用,parse_early_param之后的parse_args会调用下面函数
static int __init obsolete_checksetup(char *line)
这两个函数后面会讲到。
init/main.c中启动参数申明列表:
early_param(“nosmp”, nosmp);
early_param(“nr_cpus”, nrcpus);
early_param(“maxcpus”, maxcpus);
__setup(“reset_devices”, set_reset_devices);
early_param(“debug”, debug_kernel);
early_param(“quiet”, quiet_kernel);
early_param(“loglevel”, loglevel);
__setup(“init=”, init_setup);
__setup(“rdinit=”, rdinit_setup);
arch/arm/kernel/setup.c中启动参数申明列表:
__setup(“fpe=”, fpe_setup);
early_param(“mem”, early_mem);
early_param(“elfcorehdr”, setup_elfcorehdr);

接下来我们来看内核解析bootloader传递给内核的参数的步骤:
先看下面的图:

可以看到内核首先通过parse_early_param来解析优先级更高的,也就是需要被更早解析的命令行参数,然后通过parse_ares来解析一般的命令行参数.
现在再来看看start_arch函数中第820行的parse_early_param函数
init/main.c
void __init parse_early_options(char *cmdline)
{
parse_args(“early options”, cmdline, NULL, 0, do_early_param);
}

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
static __initdata int done = 0;
static __initdata char tmp_cmdline[COMMAND_LINE_SIZE];

if (done)
    return;

/* All fall through to do_early_param. */
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline);
done = 1;

}
在上面我们可以看到最终调用的是 parse_args(“early options”, cmdline, NULL, 0, do_early_param);它调用do_early_param函数处理early_param定义的参数函数
这个函数还另一种调用形式
parse_args(“Booting kernel”, static_command_line, __start___param,_stop___param - __start___param,&unknown_bootoption); 它调用unknown_bootoption函数处理__setup定义的参数,unknown_bootoption函数在init/main.c中定义如下:
static int __init unknown_bootoption(char *param, char *val)
{
/* Change NUL term back to “=”, to make “param” the whole string. */
if (val) {
/* param=val or param=”val”? */
if (val == param+strlen(param)+1)
val[-1] = ‘=’;
else if (val == param+strlen(param)+2) {
val[-2] = ‘=’;
memmove(val-1, val, strlen(val)+1);
val–;
} else
BUG();
}

/* Handle obsolete-style parameters */
if (obsolete_checksetup(param))
    return 0;

/* Unused module parameter. */
if (strchr(param, '.') && (!val || strchr(param, '.') < val))
    return 0;

if (panic_later)
    return 0;

if (val) {
    /* Environment option */
    unsigned int i;
    for (i = 0; envp_init[i]; i++) {
        if (i == MAX_INIT_ENVS) {
            panic_later = "Too many boot env vars at `%s'";
            panic_param = param;
        }
        if (!strncmp(param, envp_init[i], val - param))
            break;
    }
    envp_init[i] = param;
} else {
    /* Command line option */
    unsigned int i;
    for (i = 0; argv_init[i]; i++) {
        if (i == MAX_INIT_ARGS) {
            panic_later = "Too many boot init vars at `%s'";
            panic_param = param;
        }
    }
    argv_init[i] = param;
}
return 0;

}
在这个函数中它调用了obsolete_checksetup函数,该函数也定义在init/main.c中
static int __init obsolete_checksetup(char *line)
{
const struct obs_kernel_param *p;
int had_early_param = 0;

p = __setup_start;
do {
    int n = strlen(p->str);
    if (!strncmp(line, p->str, n)) {
        if (p->early) {
            /* Already done in parse_early_param?
             * (Needs exact match on param part).
             * Keep iterating, as we can have early
             * params and __setups of same names 8( */
            if (line[n] == '\0' || line[n] == '=')
                had_early_param = 1;
        } else if (!p->setup_func) {
            printk(KERN_WARNING "Parameter %s is obsolete,"
                   " ignored\n", p->str);
            return 1;
        } else if (p->setup_func(line + n)) //调用支持函数
            return 1;
    }
    p++;
} while (p < __setup_end);

return had_early_param;

}
我们再来看一下do_early_param函数,它在init/main.c中
static int __init do_early_param(char *param, char *val)
{
const struct obs_kernel_param *p;
这里的_setup_start和-setup_end分别是.init.setup段的起始和结束的地址
for (p = __setup_start; p < __setup_end; p++) { 如果没有early标志则跳过
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);
}
}
/* We accept everything at this stage. */
return 0;
}
parse_args在kernel/params.c中定义,注意它与前面讲到的parse_args区别。
/* Args looks like “foo=bar,bar2 baz=fuz wiz”. */
int parse_args(const char *name,
char *args,
const struct kernel_param *params,
unsigned num,
int (*unknown)(char *param, char *val))
{
char *param, *val;

DEBUGP("Parsing ARGS: %s\n", args);

/* Chew leading spaces */
args = skip_spaces(args);

while (*args) {
    int ret;
    int irq_was_disabled;

    args = next_arg(args, ¶m, &val);
    irq_was_disabled = irqs_disabled();
    ret = parse_one(param, val, params, num, unknown);
    if (irq_was_disabled && !irqs_disabled()) {
        printk(KERN_WARNING "parse_args(): option '%s' enabled "
                "irq's!\n", param);
    }
    switch (ret) {
    case -ENOENT:
        printk(KERN_ERR "%s: Unknown parameter `%s'\n",
               name, param);
        return ret;
    case -ENOSPC:
        printk(KERN_ERR
               "%s: `%s' too large for parameter `%s'\n",
               name, val ?: "", param);
        return ret;
    case 0:
        break;
    default:
        printk(KERN_ERR
               "%s: `%s' invalid for parameter `%s'\n",
               name, val ?: "", param);
        return ret;
    }
}

/* All parsed OK. */
return 0;

}

接下来看下,一些内置模块的参数处理的问题。传递给模块的参数都是通过module_param来实现的:
include/linux/moduleparam.h
99 #define module_param(name, type, perm) \
100 module_param_named(name, name, type, perm)

113 #define module_param_named(name, value, type, perm) \
114 param_check_##type(name, &(value)); \
115 module_param_cb(name, ¶m_ops_##type, &value, perm); \
116 __MODULE_PARM_TYPE(name, #type)

126 #define module_param_cb(name, ops, arg, perm) \
127 __module_param_call(MODULE_PARAM_PREFIX, \
128 name, ops, arg, __same_type((arg), bool *), perm )

142 #define __module_param_call(prefix, name, ops, arg, isbool, perm) \
143 /* Default value instead of permissions? */ \
144 static int param_perm_check_##name __attribute((unused)) = \
145 BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2)) \
146 + BUILD_BUG_ON_ZERO(sizeof(“”prefix) > MAX_PARAM_PREFIX_LEN); \
147 static const char _param_str##name[] = prefix #name; \
148 static struct kernel_param _moduleparam_const __param##name \
149 __used \
150 attribute ((unused,section (“__param”),aligned(sizeof(void *)))) \
151 = { _param_str##name, ops, perm, isbool ? KPARAM_ISBOOL : 0, \
152 { arg } }
这里也就是填充了 struct kernel_param的结构体,并将这个变量标记为__param段,以便于链接器将此变量装载到指定的段
接下来我们来看struct kernel_param这个结构:
include/linux/moduleparam.h
struct kernel_param;

struct kernel_param_ops {
/* Returns 0, or -errno. arg is in kp->arg. */
int (*set)(const char *val, const struct kernel_param *kp);设置参数的函数
/* Returns length written or -errno. Buffer is 4k (ie. be short!) */
int (*get)(char *buffer, const struct kernel_param *kp);读取参数的函数
/* Optional function to free kp->arg when module unloaded. */
void (*free)(void *arg);
};

/* Flag bits for kernel_param.flags */

define KPARAM_ISBOOL 2

struct kernel_param {
const char *name;
const struct kernel_param_ops *ops;
u16 perm;
u16 flags;
union { 传递给上面kernel_param_ops中两个函数的参数
void *arg;
const struct kparam_string *str;
const struct kparam_array *arr;
};
};

/* Special one for strings we want to copy into */
struct kparam_string {
unsigned int maxlen;
char *string;
};

/* Special one for arrays */
struct kparam_array
{
unsigned int max;
unsigned int *num;
const struct kernel_param_ops *ops;
unsigned int elemsize;
void *elem;
};

接下来来看parse_one函数,其中early和一般的pase都是通过这个函数来解析:
kernel/params.c
static inline char dash2underscore(char c)
{
if (c == ‘-‘)
return ‘_’;
return c;
}

static inline int parameq(const char *input, const char *paramname)
{
unsigned int i;
for (i = 0; dash2underscore(input[i]) == paramname[i]; i++)
if (input[i] == ‘\0’)
return 1;
return 0;
}

static int parse_one(char *param,
char *val,
const struct kernel_param *params,
unsigned num_params,
int (*handle_unknown)(char *param, char *val))
{
unsigned int i;
int err;
如果是early_param则直接跳过这步,而非early的,则要通过这步来设置一些内置模块的参数。
/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) {
/* Noone handled NULL, so do it here. */
if (!val && params[i].ops->set != param_set_bool) 调用参数设置函数来设置对应的参数。
return -EINVAL;
DEBUGP(“They are equal! Calling %p\n”,
params[i].ops->set);
mutex_lock(¶m_lock);
err = params[i].ops->set(val, ¶ms[i]);
mutex_unlock(¶m_lock);
return err;
}
}

if (handle_unknown) {
    DEBUGP("Unknown argument: calling %p\n", handle_unknown);
    return handle_unknown(param, val);
}

DEBUGP("Unknown argument `%s'\n", param);
return -ENOENT;

}
这里之所以把所有的内核初始化参数都放在一个段里,主要是为了初始化成功后,释放这些空间。
我们下来看一下内核编译完毕后的一些段的位置:

右边就是每个段定义的相关的宏。在__init_start和__init_end之间的段严格按照顺序进行初始化,比如 core_initcall宏修饰的函数就要比arch_initcall宏定义的函数优先级高,所以就更早的调用。这个特性就可以使我们将一些优先级比较高的初始化函数放入相应比较高的段。。
这里要注意,每个init宏,基本都会有个对应的exit宏,比如__initial和__exit等等。。
内核中修饰的宏很多,我们下面只介绍一些常用的:
__devinit 用来标记一个设备的初始化函数,比如pci设备的probe函数,就是用这个宏来修饰。
__initcall这个已被废弃,现在实现为__devinit的别名。
剩下的宏需要去看内核的相关文档。。

最后还要注意几点:
1 当模块被静态的加入到内核中时,module_init标记的函数只会被执行一次,因此当初始化成功后,内核会释放掉这块的内存。
2 当模块静态假如内核,module_exit在链接时就会被删除掉。
这里的优化还有很多,比如热拔插如果不支持的话,很多设备的初始化函数都会在执行完一遍后,被丢弃掉。
这里可以看到,如果模块被静态的编译进内核时,内核所做的内存优化会更多,虽然损失了更多的灵活性

回到start_arch中第822行,来看arm_memblock_init(&meminfo, mdesc);函数
arch/arm/mm/init.c
270 void __init arm_memblock_init(struct meminfo *mi, struct machine_desc *mdesc)
271 {
272 int i;
273
274 memblock_init();
275 for (i = 0; i < mi->nr_banks; i++)
276 memblock_add(mi->bank[i].start, mi->bank[i].size);
277
278 /* Register the kernel text, kernel data and initrd with memblock. */
279 #ifdef CONFIG_XIP_KERNEL
280 memblock_reserve(__pa(_data), _end - _data);
281 #else
282 memblock_reserve(__pa(_stext), _end - _stext);
283 #endif
284 #ifdef CONFIG_BLK_DEV_INITRD
285 if (phys_initrd_size) {
286 memblock_reserve(phys_initrd_start, phys_initrd_size);
287
288 /* Now convert initrd to virtual addresses */
289 initrd_start = __phys_to_virt(phys_initrd_start);
290 initrd_end = initrd_start + phys_initrd_size;
291 }
292 #endif
293
294 arm_mm_memblock_reserve();
295
296 /* reserve any platform specific memblock areas */
297 if (mdesc->reserve)
298 mdesc->reserve();
299
300 memblock_analyze();
301 memblock_dump_all();
302 }
这个函数保留了内存, 包括linux内核占用的代码数据段空间, initrd占用的空间 以及一些平台相关的内存
第274行,memblock_init()在mm/memblock.c里面被定义。
void __init memblock_init(void)
{
/* Create a dummy zero size MEMBLOCK which will get coalesced away later.
* This simplifies the memblock_add() code below…
*/
memblock.memory.region[0].base = 0;
memblock.memory.region[0].size = 0;
memblock.memory.cnt = 1;

/* Ditto. */
memblock.reserved.region[0].base = 0;
memblock.reserved.region[0].size = 0;
memblock.reserved.cnt = 1;

}
其作用就是初始化memblock这个结构。memblock包含两个重要的成员,分别是memblock.memory和memblock.reserved.其分别代表系统中可用的内存和已经被保留的内存。
memblock.memory和memblock.reserved被定义为以下结构:include/linux/memblock.h

define MAX_MEMBLOCK_REGIONS 128

struct memblock_property {
u64 base;
u64 size;
};

struct memblock_region {
unsigned long cnt;
u64 size;
struct memblock_property region[MAX_MEMBLOCK_REGIONS+1];
};

struct memblock {
unsigned long debug;
u64 rmo_size;
struct memblock_region memory;
struct memblock_region reserved;
};
276行,内存原始数据由u-boot传入,并在start_arch中调用parse_tags对它初始化(其详细过程请参看我后面的u-boot与linux内核间的参数传递过程分析),在初始化完memblock_init后,memblock_add调用memblock_add_region加入原始内存数据.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值