注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

内核启动之initrd  

2012-01-08 01:24:52|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、内核和文件系统
内核作为一个工作在核心态的C语言程序,它本身作为一个服务机构存在(例如民主国家的政府机构),服务的目的就是让human being用户对系统的使用更加的方便,让系统中各个进程相处更加和谐,可以让不同的用户安居乐业,公平分享系统资源。
内核在完成初始化之后,可以认为kernel已经接管(不是城管)了整个系统,它已经探测并了解了系统的配置,对各种资源进行必要的初始化,构建提供服务需要的基础设施(例如调度器、协议栈等)。掌控了系统之后,内核需要还政于民,而不应该霸占着系统资源自嗨。这里就有一个方式的问题,内核应该何时通过什么方法把控制权放给用户。
二、根文件系统
根文件系统始终是一个内存文件系统,这个根文件系统不依赖于任何外部设备(除了内存本身),它是一个动态创建的文件系统,只存在于内存中,可以认为,内核刚开始的时候只信赖自己一手创建的根文件系统。这个系统非常简单,和mmap把一个文件转换为可以进行内存操作相反,这个根文件系统是把内存转换为可以进行文件操作。该文件系统中所有的文件创建、目录创建都是通过内存中的一些数据结构进行关联。这个文件系统相对于具有日志系统的ext4文件系统来说,可以认为是文件系统中处于单细胞阶段的文件系统,它甚至没有自己的确定的存储位置。比方说,我在跟文件系统中创建一个文件,这个文件的内容都是临时从内存中分配的页面,本来这些页面是为了避免多次操作慢速设备(例如磁盘)而进行的缓冲,而此时窘迫的rootfs却只能依靠这些不确定的缓冲页面来作为自己的真正存储内容。就好像刘备同志借荆州一样,并且被rootfs借了之后的缓冲页面同样不会在还给系统(当然,文件删除的时候一定会的)。
rootfs是如此的重要,但是却如此的低调,它和ramfs共享一个不到300的.c文件(在2.6.21内核中),代码位于linux-2.6.21\fs\ramfs\inode.c,其中对于该文件系统的定义为
static struct file_system_type rootfs_fs_type = {
    .name        = "rootfs",
    .get_sb        = rootfs_get_sb,
    .kill_sb    = kill_litter_super,
};
这个文件的确非常简单,它和ramfs_get_sb的唯一区别在于get_sb_nodev的flags重多了一个MS_NOUSER标志位。
1、ramfs中文件存储设备的分配
我本来是想看一下这个文件系统在initrd中的使用,但是突然间又好奇起来了其中的文件系统的实现,因为它所有的内容都是存在于内存中,这些文件内容使用的内存是在什么时候分配的呢?
在该文件的开始有一段注释
/*
 * NOTE! This filesystem is probably most useful
 * not as a real filesystem, but as an example of这个文件系统的最大用处或许不在于作为一个真正的文件系统,而在于它作为一个展示如果实现一个
 * how virtual filesystems can be written.           虚拟文件系统的例子
所以通过这个文件系统可以看一下一个真正的文件系统如何实现。看一下最为原始的问题,就是这个文件系统中真正用来存储文件内容的内存是在哪里分配的。我直观的找了一下这个分配动作,很失望,没有找到。由于我没有放弃,所以最后还是找到了,事实上它根本没有主动申请,而是直接使用VFS的缓冲机制,当用户使用write向一个文件中写数据的时候,VFS为了提供效率,会在内存中分配一个新的页面(“新”相对于用户通过write传入的buf使用的页面),将用户数据存储在这个缓冲页中,这样在用户下次读取或者修改文件中该部分的内容的时候就可以避免再次启动设备操作。再具体的说,这个操作是在
const struct file_operations ramfs_file_operations = {
    .read        = do_sync_read,
    .aio_read    = generic_file_aio_read,
    .write        = do_sync_write,
    .aio_write    = generic_file_aio_write,,从这个地方继续,可以追踪到分配的地点
generic_file_aio_write--->>>__generic_file_aio_write_nolock--->>>generic_file_buffered_write--->>>__grab_cache_page
……
    page = find_lock_page(mapping, index);
    if (!page) {
        if (!*cached_page) {
            *cached_page = page_cache_alloc(mapping);就是在这里分配的,也就是框架会为文件系统分配一个缓冲页面,这个页面是设备内容在内存中的一个映像备份,让用户对设备的操作先在这个页面上完成,这也操作系统可以对该页面进行缓存(例如再次读取、或者改写这片数据的时候不用启动设备操作)。
            if (!*cached_page)
                return NULL;
        }
而ramfs就比较流氓了,它直接霸占了框架为它分配的缓冲页面,并把自己的文件内容永久性的据为己有,相当于寄生在了这个缓冲页面。但是这个寄生是如何避免自己被消灭的呢?因为系统中文件的缓冲页面不可能永远存在,被写的脏页面终究是要写回到存储设备的,而这些缓冲页面也应该被漂白洗涤,此时ramfs就需要有一种方式避免自己经受这样的命运,因为如果缓冲页面消失了,那么相当于ramfs中文件内容丢失了。大致看了一下,感觉是通过
static struct backing_dev_info ramfs_backing_dev_info = {
    .ra_pages    = 0,    /* No readahead */
    .capabilities    = BDI_CAP_NO_ACCT_DIRTY | BDI_CAP_NO_WRITEBACK |这个标志表示不要将缓冲页面写回到设备中
              BDI_CAP_MAP_DIRECT | BDI_CAP_MAP_COPY |
              BDI_CAP_READ_MAP | BDI_CAP_WRITE_MAP | BDI_CAP_EXEC_MAP,
};
然后在回收过程中,执行的操作为
#define bdi_cap_writeback_dirty(bdi) \
    (!((bdi)->capabilities & BDI_CAP_NO_WRITEBACK))
sync_sb_inodes(struct super_block *sb, struct writeback_control *wbc)
{
……
        if (!bdi_cap_writeback_dirty(bdi)) {
            list_move(&inode->i_list, &sb->s_dirty);
            if (sb_is_blkdev_sb(sb)) {
                /*
                 * Dirty memory-backed blockdev: the ramdisk
                 * driver does this.  Skip just this inode
                 */
                continue;
            }
            /*
             * Dirty memory-backed inode against a filesystem other
             * than the kernel-internal bdev filesystem.  Skip the
             * entire superblock.
             */
            break;我想应该是在这里让ramfs的页面逃过一劫
        }
2、根文件系统的生成
init_mount_tree
    mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);这里非常给力,直接要求使用rootfs这种设备,所以就找到了原来注册的rootfs文件系统。
……
list_add(&mnt->mnt_list, &ns->list);
    ns->root = mnt;
    mnt->mnt_ns = ns;

    init_task.nsproxy->mnt_ns = ns;这里的init_task就是系统的0号进程,也就是一生二、二生三、三生万物的“一”。
    set_fs_pwd(current->fs, ns->root, ns->root->mnt_root);
    set_fs_root(current->fs, ns->root, ns->root->mnt_root);这里设置了当前任务(init_task)的文件系统,这样,所有的文件操作例如sys_open都将处于可用状态

这样,系统中的第一个根文件系统,也就是整个系统唯一的根文件系统就算产生了,之后的所有文件系统都将会这棵树上嫁接、生长、生生不息。这里也是为根文件系统找到了一个确定的位置,就是放在init_task中,这个位置非常醒目,之后查找也比较方便。
但是这个根文件系统中其实是一个空的容器,里面什么内容都没有,或者说是一片不毛之地。所以需要有文件或者目录在这里创建和开拓,否则一个空的文件系统没有任何意义。
三、根文件系统的开拓
static int __init populate_rootfs(void)
函数将会完成对根文件系统的第一次填充,从这个名字就可以看出,这是一个向根文件系统“移民”的过程,用中国汉代的做法就叫做“实边”。但是这个根文件系统的来源又有不同的位置。这里有三种方案,均体现在static int __init populate_rootfs(void)中
1、从内核映像自带的文件结构填充
这个内容赫然位于populate_rootfs函数的开始,其内容为
    char *err = unpack_to_rootfs(__initramfs_start,
             __initramfs_end - __initramfs_start, 0);
这片内存是始终存在的一片内存,具体生成使用的文件为linux-2.6.21\usr文件夹。这种文件的特点就是文件系统和内核映像存放在同一个文件(bzimage,vmlinux)中,整个文件系统被作为一个数据段存放在内核映像的一个数据段中,当然,这离不开链接脚本的帮助。依然用我自己喜闻乐见的386做例子linux-2.6.21\arch\i386\kernel\vmlinux.lds.S
#if defined(CONFIG_BLK_DEV_INITRD)
  . = ALIGN(4096);
  .init.ramfs : AT(ADDR(.init.ramfs) - LOAD_OFFSET) {
    __initramfs_start = .;
    *(.init.ramfs)
    __initramfs_end = .;
  }
#endif
然后就是其中的.init.ramfs数据内容的来源了。正如刚才所说,它来自usr文件,并且使用了linux-2.6.21\scripts\gen_initramfs_list.sh文件
initramfs   := $(CONFIG_SHELL) $(srctree)/scripts/gen_initramfs_list.sh
ramfs-input := $(if $(filter-out "",$(CONFIG_INITRAMFS_SOURCE)), \
            $(shell echo $(CONFIG_INITRAMFS_SOURCE)),-d)
在构建内核的时候,可以在menuconfig中配置使用host的哪个目录作为这个ramfs的根目录,如果没有指定,则使用默认目录,其结构在linux-2.6.21\scripts\gen_initramfs_list.sh文件中定义为
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
}
这个根文件系统的配置位置为
内核启动ramdisk文件系统 - Tsecer - Tsecer的回音岛
 
2、boot传递过来的内存initrd结构
这个内容是bootloader传递过来的一个位于内存中的初始文件系统,它和第一种情况的最大不同就在于它不是编译内核的时候就已经确定的结构,而是可以在boot的阶段确定的一个文件结构。明显地,这种方式更加灵活一些,适用于相对通用一些的系统,例如Fedora Core的发行版。而这些bootloader包括了lilo,grub,kuboot之类的loader。对于这种形式启动,它对应的代码为
        err = unpack_to_rootfs((char *)initrd_start,
            initrd_end - initrd_start, 1);这里最后一个参数1表示只是检测initrd的内容是否合法,这个是一个提前亮。
        if (!err) {
            printk(" it is\n");
            unpack_to_rootfs((char *)initrd_start,
                initrd_end - initrd_start, 0);如果检测通过,则直接解压缩到rootfs文件系统中,并直接返回。
            free_initrd();
            return 0;
        }
这里的initrd使用的是cpio格式,而所谓的cpio格式和tar的功能相似,它们都是把一个目录的属性结构拍扁了之后放在一个文件中,从而便于处理。而这里的unpack_to_rootfs则负责进行相反的动作,就是把这个文件中保存的目录结构再次展开还原到rootfs中。由于一般的cpio之后是一个文件,所以可以使用各种压缩方式对单个文件进行压缩,这里的压缩算法一直使用的是gzip,但是后来有添加了新的压缩算法,例如效率更高的lzma算法,但是这个和cpio是独立的。
那这样说来,是不是也可以使用tar来压缩一个文件,然后把这个文件作为一个根设备呢?我想是可以的,但是当前的内核只支持cpio格式的压缩,可能是这种压缩的方式更加的高效和便于处理吧。
其中cpio的还原代码同样位于initramfs.c\initramfs.c文件中,从还原过程可以看到,它保存的是文件名和文件内容,目录结构,符号链接等,主要还原操作位于

static __initdata int (*actions[])(void) = {
    [Start]        = do_start,
    [Collect]    = do_collect,
    [GotHeader]    = do_header,
    [SkipIt]    = do_skip,
    [GotName]    = do_name,
    [CopyFile]    = do_copy,
    [GotSymlink]    = do_symlink,
    [Reset]        = do_reset,
};
这里的特点就是它是具体文件系统无关的,例如,它要还原etc/init.d/rcS文件,它只是依次调用目标文件系统的mkdir操作,然后open创建一个rcS文件,然后把cpio中保存的文件内容写入,这里不会依赖目标文件的文件系统类型,例如不管是ext2文件系统还是proc文件系统,或者是ramfs文件系统,只要有标准文件操作接口就可以了。这一点将会和接下来说的image方式形成对比。
3、initrd.image方式
populate_rootfs的第三种方式就是使用image方式,相对于cpio,这种方式比较臃肿,但是也比较完备,它把整个文件系统中所有的内容都包含进一个文件,一般是通过dd 工具实现。例如,它会把整个hda的数据放到一个文件中,这就包含了文件系统 的所有内容,例如这个文件系统的超级块内容,inode区内容,文件区内容,各个扇区的使用情况等一股脑的放在了一个文件中。
这种方式对于加载的影响就是:相对于之前需要的只会一个目录,这个文件同样必须一次性的写入一个设备,然后把整个设备作为一个新的文件系统,具体这个文件系统是什么,就有image的原始内容决定了。
最为简单的办法就是
dd
if=/dev/hda of=/mnt/nebula/hda_dd.image

这样就制作了一个以当前/dev/hda为模板的initrd了,它包含了一个文件系统所有的元数据和文件数据。
正如之前所说,这个释放必须有一个设备,从而把这个文件中的所有内容一股脑的dump到设备中,所以此时还需要一个内存设备文件,注意,这是一个具体的设备,而不是一个目录。所谓设备,它最为基本的特征就是支持mount操作,并且内存设备的一个特点就是它的具体的block大小可以通过ioctl设置为和原始的image相同。
int __init initrd_load(void)
{
    if (mount_initrd) {
        create_dev("/dev/ram", Root_RAM0);
        /*
         * Load the initrd data into /dev/ram0. Execute it as initrd
         * unless /dev/ram0 is supposed to be our actual root device,
         * in that case the ram disk is just set up here, and gets
         * mounted in the normal path.
         */
        if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
            sys_unlink("/initrd.image");
            handle_initrd();这里将会把Root_RAM0设备挂载到/root文件夹下,然后切换根目录到/root,然后执行其中可能存在的linuxrc文件……。
            return 1;
        }
4、sum up
如果有人坚持看到这里的话,还有一个点需要说明,之前的三种方式都是对initrd来说的,它们都是完成系统的一些预初始化工作,这些还都没有考虑到系统真正的ROOTDEV的使用。这个预初始化可以完成不同版本的内核定制,例如一些动态ko文件的注入工作都可以在这个阶段完成,从而对于不同的发型版本,只用修改此处的linuxrc文件就可以,这样就避免了每次内核的变化都必须修改boot或者内核的代码。
  评论这张
 
阅读(1638)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017