您的位置  > 互联网

Linux内核模块自动加载设备驱动程序的制作方法

您可以使用以下方法创建文件。

# dd if=/dev/zero of=initrd.img bs=4k count=1024
# mkfs.ext2 -F initrd.img
# mount -o loop initrd.img /mnt
# cp -r miniroot/* /mnt
# umount /mnt
# gzip -9 initrd.img

通过上面的命令,我们创建了一个4M的文件,这是一个根目录。 最后我们得到一个名为.img.gz的压缩文件。

利用内核可以在启动阶段顺利加载设备驱动程序。 但它有以下缺点:

尺寸是固定的。 例如上面压缩前的大小是4M(4k*1024)。 假设你的根目录(上例中的/)总大小只有1M,仍然占用4M的空间。 如果在dd阶段指定了1M的大小,后来发现不够,必须按照上面的步骤再试一次。

它是一个虚拟块设备。 在上面的示例中,您使用 fdisk 对此虚拟块设备进行分区。 在内核中,块设备的读写也需要经过缓冲区管理模块。 也就是说,当内核读取文件内容时,缓冲区管理层会认为下层块设备速度较慢,因此会启用缓冲区管理模块。 读取和缓存功能。 这个本身是在内存中的,块设备缓冲区管理层也会保存部分内容。 为了避免上面的缺点,看起来它的工作原理类似,你可以使用下面的方法来制作一个:

# find miniroot/ | cpio -c -o > initrd.img
# gzip initrd.img 

通过这种方式获得的.img的大小是可变的,它取决于你的小根目录/的总大小。 由于优先使用cpio来打包根目录,所以也称为cpio。 在系统启动阶段,除了从磁盘下载上述机制的内核镜像之外,还必须加载.img.gz,然后必须将.img.gz的起始地址传递给内核。 这两个文件可以合并为一个吗? 答案是肯定的。 在Linux 2.6内核中,可以将.img.gz链接到内核文件(ELF格式)中的特殊数据段。 该段的名称是.init.ramfs。 全局变量 和 分别指向该数据段的起始地址和结束地址。 内核启动时,会解压.init.ramfs段中的数据,然后将其作为临时根文件系统。 虽然这个过程比较复杂,但是只需要在make中配置以下选项即可:

General setup ---> 
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support (../miniroot/) Initramfs source file(s) 

其中..//就是我们的小根目录。 这样就只需要一个内核镜像文件。 在启动过程中,内核必须处理以下情况:

如果.init.ramfs数据段大小不为0(-!= 0),则表示已集成到内核数据段中。 这是cpio。

它被加载到内存中。 这时,起始地址和结束地址就会传递给内核。 内核中的全局地址和结束地址分别指向起始地址和结束地址。 现在内核仍然需要确定这是新的 cpio 格式还是旧的格式。

并处理内核中的临时根目录挂载

第一种选择是在内核启动过程中初始化文件系统。 tmpfs和tmpfs都是内存中的文件系统,其类型是ramfs。 然后rootf就会被挂载到根目录。 代码如下:

[() -> () -> ()]

void __init mnt_init(void)
{ ...... 
 init_rootfs(); 
 init_mount_tree();
}init_rootfs()注册rootfs文件系统,代码如下:
static struct file_system_type rootfs_fs_type = { 
 .name = "rootfs", 
 .get_sb = rootfs_get_sb, 
 .kill_sb = kill_litter_super,};
int __init init_rootfs(void){ 
 err = register_filesystem(&rootfs_fs_type); 
 ...... return err;}
init_mount_tree会把rootfs挂载到/目录,代码如下:
static void __init init_mount_tree(void)
{ 
 struct vfsmount *mnt; 
 struct mnt_namespace *ns; 
 mnt = do_kern_mount("rootfs", 0, "rootfs", NULL); ...... 
 set_fs_pwd(current->fs, ns->root, ns->root->mnt_root); 
 set_fs_root(current->fs, ns->root, ns->root->mnt_root);}
do_kern_mount()会调用前面注册的rootfs文件系统对象的rootfs_get_sb()函数,
[rootfs_get_sb() -> ramfs_fill_super() -> d_alloc_root()]struct dentry * d_alloc_root(struct inode * root_inode)
{ 
 struct dentry *res = NULL; 
 if (root_inode)
 { 
static const struct qstr name = { .name = "/", .len = 1 }; 
 res = d_alloc(NULL, &name); 
 if (res) { 
 res->d_sb = root_inode->i_sb; 
 res->d_parent = res; 
 d_instantiate(res, root_inode); 
 } 
 } 
return res;
}

从上面的代码可以看出,这个对象的名字是“/”,也就是根目录。

减压

在()的末尾,调用()。 ()会创建一个新的内核进程,并在这个内核进程中执行()函数。 ()会调用()来检测并解压文件。 该函数需要处理上述情况。

[() -> ()]

static int __init populate_rootfs(void){ 
/* 如果__initramfs_end - __initramfs_start不为0,就说明这是和内核文件集成在一起的cpio的intrd。*/ 
 char *err = unpack_to_rootfs(__initramfs_start, __initramfs_end - __initramfs_start, 0); 
if (err) 
panic(err);
#ifdef CONFIG_BLK_DEV_INITRD 
/* 如果initrd_start不为0,说明这是由bootloader加载的initrd, * 那么需要进一步判断是cpio格式的initrd,还是老式块设备的initrd。 */ 
 if (initrd_start) {#ifdef CONFIG_BLK_DEV_RAM 
 int fd; 
/* 首先判断是不是cpio格式的initrd,也就是这里说的initramfs。*/ 
printk(KERN_INFO "checking if image is initramfs..."); /* 这里unpack_to_rootfs()的最后一个参数为1,表示check only,不会执行解压缩。*/ 
err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 1); 
if (!err) { 
 /* 如果是cpio格式的initrd,把它解压到前面挂载的根文件系统上,然后释放initrd占用的内存。*/ 
printk(" it is/n"); unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0); 
 free_initrd(); 
 return 0; 
} 
 /* 如果执行到这里,说明这是旧的块设备格式的initrd。 
* 那么首先在前面挂载的根目录上创建一个initrd.image文件, 
* 再把initrd_start到initrd_end的内容写入到/initrd.image中, 
* 最后释放initrd占用的内存空间(它的副本已经保存到/initrd.image中了。)。 
*/ 
printk("it isn't (%s); looks like an initrd/n", err); 
fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700); 
 if (fd >= 0) { 
sys_write(fd, (char *)initrd_start, initrd_end - initrd_start); 
sys_close(fd); 
free_initrd(); 
} 
...... 
return 0;
}
rootfs_initcall(populate_rootfs);

经过()函数处理后,如果是cpio格式,那么()函数已经将该目录解压到上次挂载的根目录下。 但如果是旧的块设备,则通过()函数解压后得到的是块虚拟设备镜像文件/.image。 在这种情况下,需要进一步处理才能使用。 接下来,()将处理这种情况。

static int __init kernel_init(void * unused)
{ 
...... 
do_basic_setup(); 
 /* 内核启动时,可以通过启动参数 rdinit=xxx 来指定启动的最后阶段,需要运行initrd中的哪一个可执行文件, * 如果指定了这个参数,那么ramdisk_execute_command就会指向xxx这字符串,新cpio格式的initrd默认执行/init。 * 因此,如果如果ramdisk_execute_command为NULL, 就把它设置为/init。 */ 
if (!ramdisk_execute_command) 
ramdisk_execute_command = "/init"; 
/* 现在,尝试访问ramdisk_execute_command,默认为/init,如果访问失败,说明根目录上不存在这个文件。 
* 于是调用prepare_namespace(),进一步检查是不是旧的块设备的initrd 
* (在这种情况下,还是一个块设备镜像文件/initrd.image,所以访问/init文件失败。)。 */ 
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) { 
ramdisk_execute_command = NULL; 
prepare_namespace(); 
} 
init_post(); 
return 0;
}

老式加工

() 用于处理旧式。

[kernel_init() -> prepare_namespace() -> initrd_load()]
int __init initrd_load(void){ 
if (mount_initrd) { 
create_dev("/dev/ram", Root_RAM0); 
if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) { 
	sys_unlink("/initrd.image"); 
	handle_initrd(); 	
	return 1; 
} 
} 
sys_unlink("/initrd.image"); 
return 0;
}

()执行以下步骤:

调用()创建设备文件节点/dev/ram。 其实这也是一个ramfs文件系统。 调用()将/.image加载到/dev/ram中。 调用()将块设备文件/dev/ram挂载到/root。

()代码如下:

[() -> () -> () -> ()] void (void){ ...... = (); /* 创建 /dev/root.old 设备文件。 */ ("/dev/root.old", ); /* 挂载 /dev/root.old 到 /root 目录。 */ /* 挂载到 ' /root */ ("/dev/root.old", & ~); (“/旧”,0700); = (“/”, 0, 0); = ("/旧", 0, 0); /* 移至 root 中的 / 和 chdir/ */ ("/root"); (".", "/", NULL, , NULL); /* 进入/root目录,好了,/root目录现在变成了当前根目录。 */(“。”); /* * 如果磁盘中的 a 已被其或其之一取出,我们需要告诉它不要等待我们。 */ -> 标志 |= ; /* 创建一个线程,执行/,这是旧的默认执行文件。 */ pid = (, "/", ); ...} cpio格式的处理

新的cpio格式不需要额外的处理,所以()继续:

[() -> ()]

static int noinline init_post(void)
{ ...... 
/* 打开console,注意如果cpio格式的根目录中不存在/dev/console文件, * 在unpack_to_rootfs()函数也会建立这个设备文件。 */ 
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) 
	printk(KERN_WARNING "Warning: unable to open an initial console./n"); 
/* 现在,标准输入,标准输出和标准错误全部都是/dev/console。*/ 
(void) sys_dup(0); 
(void) sys_dup(0); 
/* 执行ramdisk_execute_command指定的命令,默认为/init.*/ 
if (ramdisk_execute_command) { 
	run_init_process(ramdisk_execute_command); 
	printk(KERN_WARNING "Failed to execute %s/n", 
	ramdisk_execute_command); 
} 
...... 
run_init_process("/sbin/init"); 
run_init_process("/etc/init"); 
run_init_process("/bin/init"); 
run_init_process("/bin/sh"); 
panic("No init found. Try passing init= option to kernel.");
}

调用()执行/init后,该函数不会返回。 在一般的Linux发行版中,/init中的脚本将启动udevd,加载必要的设备驱动程序,然后挂载真正的根文件系统。 最后执行真正的根文件系统,使启动过程顺利交接。

块设备不仅使用不方便,而且内核中的处理也比较复杂,所以cpio肯定会取代它,建议使用cpio格式。

案例分析

如果您使用的是 ,您可以执行以下命令来查看其内容。

# mkdir /tmp/initrd# cp /boot/initrd.img-xxx /tmp/initrd/initrd.img.gz
# cd /tmp/initrd# gunzip initrd.img.gz
# cat initrd.img | cpio -ivmd 

现在,您可以看看这个根目录中的init脚本做了什么。

# cat init
#!/bin/sh
# ubuntu用户一定很熟悉这个消息。
echo "Loading, please wait..."......exec run-initrootmntrootmnt{init} "@"<@"<{rootmnt}/dev/console >${rootmnt}/dev/console 2>&1

在此 init 脚本末尾执行的 run-init 切换到真正的根文件系统。

你可以修改这个脚本,添加相关的打印信息,然后用本文开头介绍的方法新建一个cpio,然后用这个来启动内核,看看实验结果。