如何使用没有initrd的内核来启动U盘系统的几种方法

时间:2021-09-23 03:53:55

具体症状

    目前,在Linux系统中,由于内核中U盘驱动usb-storage模块初始化比较慢,导致在U盘启动时,会出现U盘还没有启动完成,就开始挂载Linux系统分区的问题,因此导致在挂载系统时,出现找不到系统分区的情况,导致内核无法正常运行。

解决思路

    针对这个问题,目前可以着手进行的方法有两个:

   思路一:加快usb-stroage模块的初始化速度,使其能够在挂载文件系统前完成。

   思路二:在进行文件系统挂载前等待一点时间,让usb-stroage模块能够正常的初始化完成,再进行挂载。

思路分析

思路一分析

    加快usb-stroage模块的初始化速度,使其能够在挂载文件系统前完成。这个想法的优点在于可以从根本上解决启动速度慢的问题,但是根据对USB存储介质的初始化过程了解以后,发现由于U盘介质本身的访问和读写速度比较慢,导致在驱动层面上,要在很多地方不停的去等待,去探测。由于考虑到U盘介质不同,访问和读写速度也不一样的问题,所以在驱动中的好多地方都使用了等待一秒或者几毫秒后就去重新访问U盘,初始化U盘的方法,其实从内核代码层面上来看,已经没有什么可以值得优化的了,之所以初始化U盘比较慢,主要是慢在了U盘介质上。或许等USB 3.0规范发布以后,这种局面会有所好转。

目前,经过计时发现一个8GU盘的访问速度竟然达到了5秒钟的时间。可想而知U盘介质的访问和读写速度的确不太适合用作存储操作系统的介质。

思路二分析

    等待usb-stroage模块初始化完成后,再进行挂载文件系统的操作。这种思路的启动速度虽然不如第一种思路的启动速度那样快,但是这种思路却能够切实的解决现有的问题。

初步想法是,在内核初始化文件系统代码前加上一段等待的代码,使其能够等待usb-stroage模块初始化后再进行启动。

内核启动代码深度分析

    在内核源码中,具体的启动都是从各种架构下的汇编预言文件开始初始化的。在这里具体的内存分配,启动指令导入都规定好了。然而,查看这些汇编语言会发现,具体调用到内核中设备初始化和内存分配,cpu初始化以及分配情况的函数确是start_kernel。这个函数位于init目录,这就是内核初始化代码的开始。内核的启动过程都是从这里开始的。在这个目录下面,有一个叫做main的文件,在其中定义了几种内核参数加入时的处理函数。例如内核信息的打印模式,例如debug模式和quiet模式各有所不同,同事还指定了CPU最大数maxcpus,是否是多核处理器的参数nosmp参数加入时的各自不同的处理方式。

start_kernel函数中根据是否配置了CONFIG_BLK_DEV_INITRD进行了不同的处理,其处理过程大致是,根据输入的initrd的情况,指定initrd_start的情况。

start_kernel函数最后调用了rest_init。在这个函数中,单独分配了一个进程,给kernel_init函数。这才开始了真正的设备初始化,内存,CPU初始化,驱动加载,以及最终的如果没有initrd的情况下该如何处理的问题。

     kernel_init函数首先是锁定到内核模式下运行。然后再对内存,CPU进行初始化。此时机器基本可以运行起来了。接着就是执行do_basic_setup函数来初始化机器中的所有设备。在这个过程中有一个函数叫做driver_init的函数。该函数将是内核中各个驱动模块初始化的入口点。其他函数将分别设置内存,CPU的值。这主要是根据内核启动时加入的参数决定的。

    等do_basic_setup函数完成之后,接下来就是选择启动脚本了。具体的代码如下:

    在这里,程序调用prepare_namespace来开始挂载文件系统,启动init脚本等。经过对prepare_namespace的研究发现,在该函数中,有一个root_delay的过程,这个过程就是用来根据参数rootdelay的值,延迟root文件系统挂载的时间。也就是等待rootdelay参数规定的时间后,再开始挂载root文件系统分区,启动init脚本。而在这个时间段中,设备的初始化工作却没有间断。因为设备的初始化工作是在另外一个进程中进行的。所以我们可以在启动系统时,如果没有initrd的话,加入rootdelay参数来进行。此种做法可以作为方案一。

    随着代码的深入,我们了解到,如果有initrd镜像,则根据initrd_load的结果,调整到out过程,开始执行切换主文件系统,并执行init脚本中的内容,最终启动整个操作系统。在没有initrd镜像的情况下,则继续向下执行设备初始化的工作。在这里将根据root_wait的值来看是否等待root文件系统的初始化过程。具体的代码如下:

    在这里等待root文件系统初始化时,并不需要指定时间,而是每个100毫秒就去初始化一次ROOT_DEV 。如果初始化成功,则就不用再去等待。如果不成功,则需要继续等待,直到彻底初始化成功。

    当然root_wait并不是刚开始就有的,也需要内核中加入参数,根据该参数来判断是否要等待。具体的参数就是rootwait。这个参数不需要赋值,只需要在启动时加入rootwait字样,内核就可以根据该字样来进行相关处理的。具体抓取该参数的函数如下:

      rootwait可以做为我们解决内核启动的第二套方案实施。只需要加入该参数,让内核等待计算机上的所有设备都被初始化完成后,再去执行root文件系统的挂载工作。

    其实在root文件系统初始化的过程,实际就是ROOT_DEV=name_to_dev_t(saved_root_name)的过程。在这个过程中,根据内核启动时指定的root=/dev/sda*来随着saved_root_name的存在,而产生/dev/root。具体的saved_root_nameroot=/dev/sda*挂钩的代码如下:

     等有了/dev/root设备后,就可以进行mount_root函数了。在该函数中,就是切切实实的root文件系统的挂载过程了。而该函数的具体执行过程,实际就是随着config中指定的文件系统的不同格式,来进行不同的挂载形式。具体有NFSFD和块设备的挂载过程。由于U盘属于块设备,所以我们只需要研究块设备的挂载过程即可。块设备的具体挂载过程就是调用mount_block_root函数来进行。

    经过我们对mount_block_root函数的分析和调试,发现在该函数中,当没有初始化出块设备时,挂载返回错误代号是-ENXIO。而在程序中,没有针对这个过程的处理方式。所以我们需要针对抓住-ENXIO代号的错误挂载后,进行一定的处理。在对遇到错误代码-ENXIO的处理过程,我们的基本思路是,使其等待一定的时间后,返回到retry段代码,再次开始挂载root分区的工作。如果说初始化U盘成功了,那么就不会再返回-ENXIO错误,而是返回0,挂载成功了。如果还是没有初始化U盘成功,则继续会返回-ENXIO错误代码,继续进行等待。知道初始化成功。具体的改进代码如下:

    当然,这里面有很多是调试代码,还没有来得及进行优化。如果使用时,建议先优化后,在进行使用。这可以作为解决这个问题的第三种方案

解决方案

    随着对内核启动代码的深度分析,可以得到三种具体的解决方案。详细分析如下:

方案一

    在内核启动时,加入rootdelay=10的参数,来使内核在挂载文件系统前,先等待十秒钟,然后再开始挂载文件系统。这样做虽然能够解决系统启动时,无法挂载root文件系统的问题,但是要等待10秒中的时间。这与initrd的初始化时间相当,达不到加快系统启动速度的目标。另外,不同的U盘,存储介质不一样,初始化,读写的访问速度也不一样。这样一来,如果rootdelay给定的时间过短,会导致U盘还没有初始化完成,就挂载的情况,仍然达不到预期解决问题的目标,但是如果等待时间过长,会导致启动速度变慢。

   优点:可以解决U盘文件系统的启动失败的问题;

   缺点:如果输入值过小,则无法解决U盘文件系统启动失败的问题,如果输入值过大,会导致整个系统的启动速度变慢。

   注意:一定要随着U盘介质的不同而选择合适的值来给rootdelay,否则会出现上述问题。

方案二

    在内核启动时,加入参数rootwait参数,使内核在挂载文件系统前,等待root文件设备的初始化工作完成。此方案不仅可以解决U盘文件系统的启动失败的问题,而且也能很自动化的等待U盘的初始化,不需要浪费时间在等待U盘的启动上,同时也不会造成U盘还没有初始化完成就开始挂载的问题。

    优点:1. 可以解决U盘文件系统的启动失败的问题;

                2. u盘介质无关,不需要指定具体的初始化等待时间;

                3. 很自动的调整,不会影响到系统的启动时间

    缺点:

方案三

    加入补丁来解决U盘文件系统的启动失败的问题。具体的补丁程序见上面对内核启动过程的深度分析。这种做法实际就是将rootwait的过程给代码化,程序化了而已。与rootwait没有什么区别。