在前面linux块设备原理文章中,已经分析过linux如何加载mtd设备。那么linux是如何把真实的根文件系统加载到系统中的呢,这边以ubi文件系统为例,分析linux真实根文件系统的加载。
沿着函数调用顺序一步步来看相关代码。
1 ubi卷的加载
kernel_init
--------------->kernel_init_freeable
---------------------->do_basic_setup
------------------->do_initcalls
在do_initcalls中,有一个ubi相关的初始化函数,它是如下定义的:
static int __init ubi_init(void)
{
int err, i, k;
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
/* Create base sysfs directory and sysfs files */
ubi_class = class_create(THIS_MODULE, UBI_NAME_STR);
if (IS_ERR(ubi_class)) {
err = PTR_ERR(ubi_class);
ubi_err("cannot create UBI class");
goto out;
}
err = class_create_file(ubi_class, &ubi_version);
if (err) {
ubi_err("cannot create sysfs file");
goto out_class;
}
err = misc_register(&ubi_ctrl_cdev);
if (err) {
ubi_err("cannot register device");
goto out_version;
}
ubi_wl_entry_slab = kmem_cache_create("ubi_wl_entry_slab",
sizeof(struct ubi_wl_entry),
0, 0, NULL);
if (!ubi_wl_entry_slab)
goto out_dev_unreg;
err = ubi_debugfs_init();
if (err)
goto out_slab;
/* Attach MTD devices */
------------------------------------------------------------(1)
for (i = 0; i < mtd_devs; i++) {
struct mtd_dev_param *p = &mtd_dev_param[i];
struct mtd_info *mtd;
cond_resched();
-------------------------------------------------------------(2)
mtd = open_mtd_device(p->name);
if (IS_ERR(mtd)) {
err = PTR_ERR(mtd);
goto out_detach;
}
mutex_lock(&ubi_devices_mutex);
--------------------------------------------------------------(3)
err = ubi_attach_mtd_dev(mtd, UBI_DEV_NUM_AUTO,
p->vid_hdr_offs, p->max_beb_per1024);
mutex_unlock(&ubi_devices_mutex);
。。。。。。。。。。。。。。。。。。。。。。。。。。
}
return 0;
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
}
late_initcall(ubi_init);
该函数的主要作用是,根据前面已经注册的mtd块设备以及uboot的启动参数中的参数,找到对应的mtd设备,从mtd分区中读取ubi卷信息,在系统中创建相应的ubi设备,完成mtd驱动和ubi卷的结合。
(1)获取mtd设备的编号。mtd_dev_param是从哪里来的呢。具体的初始化是在ubi_mtd_param_parse函数,该函数用module_param_call宏进行修饰:
module_param_call(mtd, ubi_mtd_param_parse, NULL, NULL, 000);
用上述宏定义的结构,最终会被放入__param段中。在系统初始化,解析uboot参数的时候:
start_kernel
------------>parse_args
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
parse_args会依次取出uboot传过来的参数,和定义在__param段中的结构进行对比,如果名字上面匹配,则会调用__param中的函数进行解析。这边使用的uboot的启动参数为:
console=ttySAC0,115200 ubi.mtd=3 root=ubi0:rootfs rootfstype=ubifs
相应的,会匹配到这个字段ubi.mtd,所以ubi_mtd_param_parse函数会被调用,用来解析该字段后面的参数。uboot参数的本意是使用mtd3设备来加载ubi卷。看一下解析参数的这个过程:
static int __init ubi_mtd_param_parse(const char *val, struct kernel_param *kp)
{
int i, len;
struct mtd_dev_param *p;
char buf[MTD_PARAM_LEN_MAX];
char *pbuf = &buf[0];
char *tokens[MTD_PARAM_MAX_COUNT];
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
strcpy(buf, val); //这边传入的val,就是字符3
/* Get rid of the final newline */
if (buf[len - 1] == '\n')
buf[len - 1] = '\0';
for (i = 0; i < MTD_PARAM_MAX_COUNT; i++)
tokens[i] = strsep(&pbuf, ",");
if (pbuf) {
ubi_err("UBI error: too many arguments at \"%s\"\n", val);
return -EINVAL;
}
p = &mtd_dev_param[mtd_devs]; //mtd_devs为0
strcpy(&p->name[0], tokens[0]); //把参数3放入mtd_dev_param[0]的name中
。。。。。。。。。。。。。。。。。。。。
mtd_devs += 1;
return 0;
}
可以看到,最终会解析到mtd分区3,然后存在mtd_dev_param的name中。
(2)获取到了第一个mtd分区,利用该分区索引,能找到对应的分区描述符:
static struct mtd_info * __init open_mtd_device(const char *mtd_dev)
{
struct mtd_info *mtd;
int mtd_num;
char *endp;
mtd_num = simple_strtoul(mtd_dev, &endp, 0);//将字符转化为数字,以这边为例,mtd_num为3
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
mtd = get_mtd_device(NULL, mtd_num);//根据分区号获取分区描述符
return mtd;
}
进去看一下get_mtd_device函数:
struct mtd_info *get_mtd_device(struct mtd_info *mtd, int num)
{
struct mtd_info *ret = NULL, *other;
int err = -ENODEV;
mutex_lock(&mtd_table_mutex);
。。。。。。。。。。。。。。。。。
else if (num >= 0) {
ret = idr_find(&mtd_idr, num);//根据mtd分区号,获取mtd分区描述符
if (mtd && mtd != ret)
ret = NULL;
}
。。。。。。。。。。。。。。。。。
return ret;
}
在注册mtd设备的时候:
int add_mtd_device(struct mtd_info *mtd)
{
struct mtd_notifier *not;
int i, error;
。。。。。。。。。。。。。。。
i = idr_alloc(&mtd_idr, mtd, 0, 0, GFP_KERNEL);
。。。。。。。。。。。。。。。
}
会为要注册的mtd描述符分配一个索引号,所以上面通过mtd设备索引号,就能快速的找到mtd设备描述符
(3)利用获取到的mtd分区描述符,读取分区flash中信息,完成ubi卷的加载,具体加载过程这边不分析。加载完以后,ubi device已经就绪了,随时可以挂载ubi文件系统。
2 ubi根文件系统挂载
在分析具体的挂载代码之前,大概把该路径上的代码都撸一遍。做完do_initcalls以后,回到kernel_init_freeable函数中:
static noinline void __init kernel_init_freeable(void)
{
.。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
do_basic_setup();
/* Open the /dev/console on the rootfs, this should never fail */
----------------------------------------------------------------------(1)
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
pr_err("Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
------------------------------------------------------------------------(2)
if (!ramdisk_execute_command){
ramdisk_execute_command = "/init";
}
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
--------------------------------------------------------------------------(3)
prepare_namespace();
}
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
}
(1)打开/dev/console这个文件,如果打开失败,则系统引导失败。网上有很多文章,在制作根文件系统的时候,在根文件系统下面需要手动建立/dev/console这个文件,不然会引导失败(我也是这么做的),其实不然,这边真实的根文件系统都还没有挂载。linux默认都是会编译initramfs的,编译的时候,即使没有指定initramfs需要压缩的rootfs路径,initramfs自己也会生成一个很小的文件系统,可以参考gen_inittamfs_list.sh脚本,initramfs不指定rootfs路径的情况下,会默认创建这些文件:
default_initramfs() {
cat <<-EOF >> ${output}
# This is a very simple, default initramfs
dir /dev 0755 0 0
nod /dev/console 0600 0 0 c 5 1
dir /root 0700 0 0
# file /kinit usr/kinit/kinit 0755 0 0
# slink /init kinit 0755 0 0
EOF
}
所以这边的/dev/console来自initramfs,initramfs在之前已经解压过,可以参考这篇文章:
(2)ramdisk_execute_command默认为空,所以这边初始化为/init,从initramfs中启动,会需要该文件,所以制作initramfs启动方式的文件系统时,该文件系统中必须包含/init文件,不然会启动失败
(3)由于这里选择从ubi文件系统启动,所以initramfs解压出来不包含/init文件,ramdisk_execute_command重新置为null,prepare_namespace用来挂载真实的ubi文件系统。
void __init prepare_namespace(void)
{
int is_floppy;
。。。。。。。。。。。。。。。。。。。。。。。。。。
wait_for_device_probe();//等待driver的probe函数都执行完,执行probe函数之前有个统计量会加1,执行完则减1,所以该统计量为0的时候必然已经执行完
md_run_setup();
if (saved_root_name[0]) {
root_device_name = saved_root_name;
--------------------------------------------------------------------(3.1)
if (!strncmp(root_device_name, "mtd", 3) ||
!strncmp(root_device_name, "ubi", 3)) {
---------------------------------------------------------------------(3.2)
mount_block_root(root_device_name, root_mountflags);
goto out;
}
。。。。。。。。。。。。。。。。。。。。。。。。
}
。。。。。。。。。。。。。。。。。。。。。。。。。。
out:
------------------------------------------------------------------------(3.3)
devtmpfs_mount("dev");
--------------------------------------------------------------------------(3.4)
sys_mount(".", "/", NULL, MS_MOVE, NULL);//把当前真实的文件系统挂载到根目录下面
sys_chroot("."); //设置根目录路径为刚挂载的真实文件系统
}
(3.1)saved_root_name在哪里赋值呢,和前面解析mtd.ubi参数一样,parse_args中解析root=参数:
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup);
saved_root_name被设置为ubi0:rootfs,该函数为设备ubi0名叫rootfs的卷
(3.2)匹配ubi名字成功,调用mount_block_root函数:
void __init mount_block_root(char *name, int flags)
{
struct page *page = alloc_page(GFP_KERNEL |
__GFP_NOTRACK_FALSE_POSITIVE);
char *fs_names = page_address(page);
char *p;
#ifdef CONFIG_BLOCK
char b[BDEVNAME_SIZE];
#else
const char *b = name;
#endif
get_fs_names(fs_names);//这里获取rootfs的类型,又是通过解析uboot启动参数得到的,
//匹配rootfstype参数,设置root_fs_names变量为ubifs,fs_names变为ubifs
retry:
for (p = fs_names; *p; p += strlen(p)+1) {
//name为ubi0:rootfs,文件系统类型为ubifs,root_mount_data不设置默认就以只读方式挂载
int err = do_mount_root(name, p, flags, root_mount_data);
switch (err) {
case 0:
goto out;
case -EACCES:
flags |= MS_RDONLY;
goto retry;
case -EINVAL:
continue;
}
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
}
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
out:
put_page(page);
}
do_mount_root用于挂载真实的ubi文件系统:
static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
struct super_block *s;
int err = sys_mount(name, "/root", fs, flags, data);//把文件系统挂载在/root路径下
if (err)
return err;
sys_chdir("/root");//设置当前路径为/root,即默认的当前路径为新挂载的真实的文件系统
s = current->fs->pwd.dentry->d_sb;
ROOT_DEV = s->s_dev;
printk(KERN_INFO
"VFS: Mounted root (%s filesystem)%s on device %u:%u.\n",
s->s_type->name,
s->s_flags & MS_RDONLY ? " readonly" : "",
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
return 0;
}
/root目录在前面建立默认的initramfs中可以看到,由initramfs创建,该目录应该就是为了这边挂载真实的文件系统所用的。最终真实的文件系统被挂载在/root目录下面
(3.3)把devtmpfs文件系统挂载到/root/dev/目录下面
devtmpfs目录是个什么呢,前面驱动初始化的时候,注册了很多设备,由于真实的文件系统还没有挂载,也就不可能有udev和mdev程序来负责在/dev目录下面创建设备节点。linux采用了另一个方法,在系统初始化的时候,先利用devtmpfs文件系统,把设备节点都创建在该文件系统下面,最后把该文件系统挂载在/root/dev下面。
看一下devtmpfs创建设备节点的基本原理。
系统初始化的时候:
int __init devtmpfs_init(void)
{
int err = register_filesystem(&dev_fs_type);//注册devtmpfs文件系统
。。。。。。。。。。。。。。。。。。。。。。。
thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");//创建devtmpfsd线程,来处理创建设备节点请求
。。。。。。。。。。。。。。。。。。。。。。。
return 0;
}
看一下devtmpfsd线程:
static int devtmpfsd(void *p)
{
char options[] = "mode=0755";
int *err = p;
*err = sys_unshare(CLONE_NEWNS);
if (*err)
goto out;
//挂载devtmpfs文件系统到根目录
*err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options);
if (*err)
goto out;
sys_chdir("/.."); /* will traverse into overmounted root */
sys_chroot(".");//
complete(&setup_done);
while (1) {
spin_lock(&req_lock);
while (requests) {
struct req *req = requests;
requests = NULL;
spin_unlock(&req_lock);
while (req) {
struct req *next = req->next;
req->err = handle(req->name, req->mode,
req->uid, req->gid, req->dev);
complete(&req->done);
req = next;
}
spin_lock(&req_lock);
}
__set_current_state(TASK_INTERRUPTIBLE);
spin_unlock(&req_lock);
schedule();//没有任务执行则睡眠
}
return 0;
out:
complete(&setup_done);
return *err;
}
上面函数先把devtmpfs文件系统挂载到了根目录,当device注册的时候
device_add
-------------->devtmpfs_create_node
int devtmpfs_create_node(struct device *dev)
{
const char *tmp = NULL;
struct req req;
if (!thread)
return 0;
req.mode = 0;
req.uid = GLOBAL_ROOT_UID;
req.gid = GLOBAL_ROOT_GID;
//获取设备名
req.name = device_get_devnode(dev, &req.mode, &req.uid, &req.gid, &tmp);
if (!req.name)
return -ENOMEM;
if (req.mode == 0)
req.mode = 0600;
if (is_blockdev(dev))
req.mode |= S_IFBLK; //根据字符设备还是块设备,设置标记,最后创建的操作集函数会不一样
else
req.mode |= S_IFCHR;
req.dev = dev;
init_completion(&req.done);
spin_lock(&req_lock);
req.next = requests;
requests = &req;
spin_unlock(&req_lock);
//新建request,唤醒线程来处理新建设备节点的请求
wake_up_process(thread);
wait_for_completion(&req.done);
kfree(tmp);
return req.err;
}
创建设备节点函数为:
handle
------------>handle_create
static int handle_create(const char *nodename, umode_t mode, kuid_t uid,
kgid_t gid, struct device *dev)
{
struct dentry *dentry;
struct path path;
int err;
dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);//找到父目录的dentry
if (dentry == ERR_PTR(-ENOENT)) {
create_path(nodename);
dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);
}
if (IS_ERR(dentry))
return PTR_ERR(dentry);
//创建设备节点
err = vfs_mknod(path.dentry->d_inode, dentry, mode, dev->devt);
if (!err) {
struct iattr newattrs;
newattrs.ia_mode = mode;
newattrs.ia_uid = uid;
newattrs.ia_gid = gid;
newattrs.ia_valid = ATTR_MODE|ATTR_UID|ATTR_GID;
mutex_lock(&dentry->d_inode->i_mutex);
notify_change(dentry, &newattrs);
mutex_unlock(&dentry->d_inode->i_mutex);
/* mark as kernel-created inode */
dentry->d_inode->i_private = &thread;
}
done_path_create(&path, dentry);
return err;
}
最终devtmpfs_mount把devtmpfs文件系统又挂载到/root/dev目录下
(3.4)然后把当前目录,也就是root目录,重新挂载到根文件夹/下面,也就是说真实的文件系统被放到了根目录/下,然后设置绝对路径为/,至此程序的当前路径和绝对路径都被设置为/根目录,其下面挂载的就是真实的根文件系统了
最终真实的根文件系统挂载完成:
static int __ref kernel_init(void *unused)
{
。。。。。。。。。。。。。。。。。。。。。。。。。。。。
if (!run_init_process("/sbin/init") ||
!run_init_process("/etc/init") ||
!run_init_process("/bin/init") ||
!run_init_process("/bin/sh"))
return 0;
panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
最终会运行真实根文件系统的某个初始化程序,kernel_init线程不会再返回。系统启动完成