Linux建立Nor Flash分区 韩大卫@吉林师范大学 接上文章<<linux系统Nor Flash芯片初始化及驱动>>, 当Nor Flash 芯片在flash芯片驱动器里链表chip_drvs_list中找到并调用名为”cfi_probe”的驱动后, 完成芯片初始化阶段, 接着进入linux对Flash建立分区阶段. 在 arch/mips/cavium-octeon/flash_setup.c 中 static struct map_info flash_map; static int __init flash_init(void) { union cvmx_mio_boot_reg_cfgx region_cfg; //从bootbus总线上获取flash的基地址. region_cfg.u64 = cvmx_read_csr(CVMX_MIO_BOOT_REG_CFGX(0)); if (region_cfg.s.en) { //将全局数据结构struct map_info flash_map命名为octeon_nor0 flash_map.name = "octeon_nor0"; //物理地址和大小 flash_map.phys = region_cfg.s.base << 16; flash_map.size = 0x1fc00000 - flash_map.phys; flash_map.bankwidth = 1; //使用ioremap()将32M 大小的Flash的物理地址映射到虚拟地址上. flash_map.virt = ioremap(flash_map.phys, flash_map.size); pr_notice("Bootbus flash: Setting flash for %luMB flash at " "0x%08llx\n", flash_map.size >> 20, flash_map.phys); simple_map_init(&flash_map); /* 调用do_map_probe()进入Nor Flash芯片初始化阶段,该函数会在Flash芯片驱动器列表中找到名为cfi_probe的驱动器, 并调用其probe()函数, 准备好read/wirte/ioctl等函数的实现方法. */ mymtd = do_map_probe("cfi_probe", &flash_map); if (mymtd) { mymtd->owner = THIS_MODULE; #ifdef CONFIG_MTD_PARTITIONS /* 对FLash芯片成功探测(调用过probe)后, linux进入处理Flash分区阶段 */ nr_parts = parse_mtd_partitions(mymtd, part_probe_types, &parts, 0); if (nr_parts > 0) /* nr_parts>0 ,说明解析到存在多个分区, 那么添加各个分区 */ add_mtd_partitions(mymtd, parts, nr_parts); else add_mtd_device(mymtd); #else //由于定义了CONFIG_MTD_PARTITIONS 宏, 不执行该函数 add_mtd_device(mymtd); #endif } else { pr_err("Failed to register MTD device for flash\n"); } } return 0; } late_initcall(flash_init); 调用do_map_probe()后, 成功的话返回一个重要的数据结构struct mtd_info. Struct mtd_info是linux描述MTD类型设备的数据结构, 里面有mtd设备等待初始化的信息(变量)和一些设备操作方法(函数指针). 经过do_map_probe()的初始化, 其中有一些成员已经得到赋值, 请参考drivers/mtd/chips/cfi_probe.c 中的cfi_probe()函数. parse_mtd_partitions(mymtd, part_probe_types,&parts, 0); 解析器类型: 有cmdlinepart和 RedBoot两中, 如没有定义CONFIG_MTD_REDBOOT_PARTS, 那只将cmdlinepart编译在内, 最后只连接cmdlinepart.o. static const char *part_probe_types[] = { "cmdlinepart", #ifdef CONFIG_MTD_REDBOOT_PARTS "RedBoot", #endif NULL }; parse_mtd_partitions()定义在drivers/mtd/mtdpart.c 中: int parse_mtd_partitions(struct mtd_info *master, const char **types, struct mtd_partition **pparts, unsigned long origin) { struct mtd_part_parser *parser; int ret = 0; //循环次数: 解析器的个数, 这里只有一个解析器cmdlinepart, 故只循环一次 for ( ; ret <= 0 && *types; types++) { //获取cmdlinepart解析器 parser = get_partition_parser(*types); if (!parser && !request_module("%s", *types)) parser = get_partition_parser(*types); if (!parser) { printk(KERN_NOTICE "%s partition parsing not available\n", *types); continue; } /* 获取成功的话, 调用其解析函数 parse_fn(), 类似于do_map_probe() 后者是获取驱动器cfi_probe, 获取成功的话, 调用其探测函数Probe() */ ret = (*parser->parse_fn)(master, pparts, origin); /* parse_fn()返回解析到的分区个数.在此打印出相关信息,此信息可在dmesg中看到. parser->name 为cmdlinepart, master->name为octeon_nor0 */ if (ret > 0) { printk(KERN_NOTICE "%d %s partitions found on MTD device %s\n", ret, parser->name, master->name); } /* 减少调用get_partition_parser ()时增加模块使用计数器个数, 释放解析器模块使用权 #define put_partition_parser(p) do { module_put((p)->owner); } while(0) */ put_partition_parser(parser); } return ret; } get_partition_parser() 定义如下: static struct mtd_part_parser *get_partition_parser(const char *name) { struct mtd_part_parser *p, *ret = NULL; spin_lock(&part_parser_lock); //遍历分区解析器链表part_parsers list_for_each_entry(p, &part_parsers, list) //在链表中获取解析器名为name的节点, 并增加其模块使用计数, 取得解析器模块使用权 if (!strcmp(p->name, name) && try_module_get(p->owner)) { ret = p; break; } spin_unlock(&part_parser_lock); return ret; } 总的来说, parse_mtd_partitions()函数类似do_map_probe(), 都是传入一个指定参数, 之后的工作都交给linux的mtd层去实现. 另一方面, 在late_initcall(flash_init)调用之前, linux已经向内核注册了名为”cmdlinepart”的解析器, 等待被调用, 完成使命, 体现出自身价值. 在drivers/mtd/cmdlinepart.c中: module_init(cmdline_parser_init); 注: module_init()优先级为6, 高于优先级为7的late_initcall(). 数值越低优先级越高. static int __init cmdline_parser_init(void) { //向分区解析器链表中添加名为”cmdlinepart” 的解析器 return register_mtd_parser(&cmdline_parser); } /* 注: 这是driver/mtd/mtdpart.c提供的API: int register_mtd_parser(struct mtd_part_parser *p) { spin_lock(&part_parser_lock); //向分区解析器链表中添加成员 list_add(&p->list, &part_parsers); spin_unlock(&part_parser_lock); return 0; } */ cmdline_parser 定义如下: static int mtdpart_setup(char *s) { cmdline = s; return 1; } __setup("mtdparts=", mtdpart_setup); static struct mtd_part_parser cmdline_parser = { .owner = THIS_MODULE, // cmdlinepart的 解析函数为: parse_cmdline_partitions .parse_fn = parse_cmdline_partitions, .name = "cmdlinepart", }; 关于__setup("mtdparts=", mtdpart_setup)解释一下: __setup()宏 定义在include/linux/init.h 中: #define __setup(str, fn) \ __setup_param(str, fn, fn, 0) #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 } __setup宏是linux用来注册关键字及提供处理函数. Str即为关键字, fn为对应的处理函数. 经过上面的展开后, __setup()宏会将传进来的的函数指针放到.init.setup段.而.init.setup是在.init.text下的. /* 注: mips架构下: 在arch/mips/kernel/vmlinux.lds.S __init_begin = . INIT_TEXT_SECTION(PAGE_SIZE) INIT_DATA_SECTION() #define INIT_DATA_SECTION(initsetup_align) \ .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \ INIT_DATA \ INIT_SETUP(initsetup_align) \ INIT_CALLS \ CON_INITCALL \ SECURITY_INITCALL \ INIT_RAM_FS \ } 在INIT_SETUP() 宏中, #define INIT_SETUP(initsetup_align) \ . = ALIGN(initsetup_align); \ VMLINUX_SYMBOL(__setup_start) = .; \ *(.init.setup) \ VMLINUX_SYMBOL(__setup_end) = .; 可以看到: 经过层层包装,.init.setup字段放在了.init.text字段中. 同样, 在arm架构中:arch/arm/kernel/vmlinux.lds.S .init : { /* Init code and data */ INIT_TEXT ... __setup_start = .; *(.init.setup) __setup_end = .; //直接就可以看到__setup的位置. 可以看到, __setup()宏将传进来的函数指针放到.init.setup字段中. linux定义了parse_args()函数处理由__setup()传进来参数, 当pa rse_args()解析uboot传给内核的启动参数, 匹配__setup()传进来的str, 匹配成功后调用其fn函数. 在init/main.c 的 __init start_kernel(void) 中: parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption); 当parse_args()解析到配置命令字符串中有"mtdparts=" 时,mtdpart_setup()函数便会自动得到调用:将 boot_command_line 字符串保存到本地变量static char *cmdline中. 另外, start_kernel()中有这样的语句: printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line); 通过这个打印可以看到Uboot传给内核的配置命令字符串: 在我的系统中的打印信息是: Kernel command line: bootoctlinux 0x1dd00000 coremask=0x1 mtdparts=octeon_nor0:1m(BOOT),7m(LINUX),21m(CONFIG),3m(MD) console=ttyS0,115200 其中”bootoctlinux 0x1dd00000 coremask=0x1 mtdparts=octeon_nor0:1m(BOOT),7m(LINUX),21m(CONFIG),3m(MD) console=ttyS0,115200” 就是 boot_command_line配置命令字符串. 在此可能有些疑问, 这样的配置命令字符串是怎样产生的,以及内核在哪里得到这个配置命令字符串static_command_line的? 第一个问题跟uboot的配置有关, 需要一大堆文字来解释说明, 请参考作者其他博文, 这里只简单说个大概: uboot启动时, 将Uboot环境变量(如linux_args, loadaddr, bootcmd,numcores等, 在uboot环境里使用printenv命令可以看到)作为编译参数, 传给Uboot的启动linux时的调用参数, 在该调用函数中, 会获取并处理这些环境变量, 得到我们见到的配置命令字符串, 最后调用一段用汇编语言写的代码, 将其存入特定寄存器中. 在linux内核启动时, 在init/main.c的 start_kernel()中: setup_arch(&command_line)这里获取到了Uboot传下来的配置命令字符串, 并调用setup_command_line(command_line)将command_line 复制static_command_line, 之后调用parse_args()对其进行处理. 总的来说, 可以简单理解为: cmdlinepart通过mtdpart_setup()函数保存了内核启动时由uboot传进来的含有”mtdparts=”的配置命令字符串, 并该字符串传给本地指针static char *cmdline, 在接下来的parse_cmdline_partitions()中, 根据此配置命令字符串作出解析.最后实现该配置. 在parse_mtd_partitions() 中使用的(*parser->parse_fn)(master, pparts, origin)的实现函数就是drivers/mtd/cmdlinepart.c 中的 parse_cmdline_partitions, 定义如下: static int parse_cmdline_partitions(struct mtd_info *master, struct mtd_partition **pparts, unsigned long origin) { unsigned long offset; int i; struct cmdline_mtd_partition *part; const char *mtd_id = master->name; /* parse command line */ if (!cmdline_parsed) /* 在此函数中解析了linux启动时的mtdparts配置命令字符串.即”bootoctlinux 0x1dd00000 coremask=0x1 mtdparts=octeon_nor0:1m(BOOT),7m(LINUX),21m(CONFIG),3m(MD) console=ttyS0,115200”, 初始化并返回一个全局的分区描述数据结构static struct cmdline_mtd_partition *partitions struct cmdline_mtd_partition { struct cmdline_mtd_partition *next; //下个分区的指针 char *mtd_id; //mtd_id int num_parts; //分区编号 struct mtd_partition *parts; //包含了分区名,大小, 偏移等信息 }; */ mtdpart_setup_real(cmdline); /* 在获得了分区描述信息partitions后, 将每一个分区的partitions传给参数 struct mtd_partition **pparts, 完成解析函数真正的作用. 等解析函数退出后, flash_init()便可以调用add_mtd_partitions()将每个 struct mtd_partition 添加到linux的mtd核心中.后面会看到这样的操作. */ for(part = partitions; part; part = part->next) { if ((!mtd_id) || (!strcmp(part->mtd_id, mtd_id))) { for(i = 0, offset = 0; i < part->num_parts; i++) { if (part->parts[i].offset == OFFSET_CONTINUOUS) part->parts[i].offset = offset; else offset = part->parts[i].offset; if (part->parts[i].size == SIZE_REMAINING) part->parts[i].size = master->size - offset; if (offset + part->parts[i].size > master->size) { printk(KERN_WARNING ERRP "%s: partitioning exceeds flash size, truncating\n", part->mtd_id); part->parts[i].size = master->size - offset; part->num_parts = i; } offset += part->parts[i].size; } //将每个分区的struct cmdline_mtd_partition 传给函数参数pparts. *pparts = kmemdup(part->parts, sizeof(*part->parts) * part->num_parts, GFP_KERNEL); if (!*pparts) return -ENOMEM; //在最后一次循环时,返回分区号, 即分区个数 return part->num_parts; } } return 0; } parse_cmdline_partitions()调用结束后, 返回系统存在多少个分区, parse_mtd_partitions()函数也随之调用完成. flash_init() 走到add_mtd_partitions(mymtd, parts, nr_parts); 在drivers/mtd/mtdpart.c 中: int add_mtd_partitions(struct mtd_info *master, const struct mtd_partition *parts, int nbparts) { struct mtd_part *slave; uint64_t cur_offset = 0; int i; printk(KERN_NOTICE "Creating %d MTD partitions on \"%s\":\n", nbparts, master->name); //nbparts 为前面函数解析到的分区个数 for (i = 0; i < nbparts; i++) { /* parts为每个分区的数据结构数组指针 add_one_partition()初始化并填充一个linux描述mtd分区的数据结构:struct mtd_part, 主要是填充struct mtd_part 内的 struct mtd_info, 为mtd_info内的函数指针填充实现函数等. 比如给每个分区的mtd_info 填充好read, write, lock/unlock, erase等函数. 最后把指针交给slave, slave可利用地址偏移offset进行下个分区处理. 在add_one_partition() 内, 会打印出该分区的地址偏移值, 大小, 和slave分区的名字. 在dmesg里可以看到这样的信息: [ 33.891465] 0x000000000000-0x000000100000 : "BOOT" [ 33.896708] 0x000000100000-0x000000800000 : "LINUX" [ 33.901975] 0x000000800000-0x000001d00000 : "CONFIG" [ 33.907329] 0x000001d00000-0x000002000000 : "MD" */ slave = add_one_partition(master, parts + i, i, cur_offset); if (!slave) return -ENOMEM; //当前偏移值 cur_offset = slave->offset + slave->mtd.size; } return 0; } EXPORT_SYMBOL(add_mtd_partitions); 总的来说, 就是经过了以上步骤, 实现了uboot传给内核的配置命令字符串所代表的配置. 根据配置命令字符串建立了相应的分区.