根文件系统分析之旅——简单分析(一)

时间:2022-11-09 20:07:20
首先我们还是要先把框架在啰嗦一下:u-boot启动内核、内核启动应用程序、应用程序挂载在根文件系统上面。这个框架要 时刻记牢!
1、首先我们从上一讲的最后一个步骤:启动应用程序开始!其函数是: init_post()
我们贴出其代码:
static int noinline init_post(void)
{
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();

if ( sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) /*打开设备文件/dev/console,它对应着终端,即标准输入、标准输出、标准错误*/
printk(KERN_WARNING "Warning: unable to open an initial console.\n");

(void) sys_dup(0); //复制/dev/console
(void) sys_dup(0); //复制/dev/console

if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
if (execute_command)  //见注释1
{
run_init_process(execute_command);// 启动命令行指定的第一个应用程序
printk(KERN_WARNING "Failed to execute %s.  Attempting "
"defaults...\n", 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.");
}
注释1: execute_command在哪里定义的呢?从下面的代码中可以看出来:
static int __init init_setup(char *str)
{
unsigned int i;

execute_command = str;

for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);

根据上一讲的内容,当我们在命令行参数里设置了init为某个值,那么就会调用init_setup()函数,在这个函数里,将init的值给了execute_command 
2、接下来我们分析busybox:下载地址 http://www.busybox.net/downloads/
busybox其实就是诸如ls、cd、cat之类命令的组合。我们编译busybox会得到一个应用程序busybox,当在命令行输入ls时,就会连接到busybox里面的ls命令,我们运行ls的结构和busybox ls的结果一致就说明这点。而且 /sbin/init也要连接到busybox,所以busybox是个非常重要的东西,为了进一步了解它,阅读源码是必要的。
我们知道启动的第一个应用程序是 /sbin/init, 而这个程序是对应着busybox里的init文件的,所以我们从init这个文件来分析, 分析之前我们首先要明确我们的目的是启动应用程序,init进程只是一个桥梁,因此可以大致知道init进程要做的工作:
(1)读取配置文件
(2)解析配置文件
(3)执行用户程序
接下来我们选出一部分码来分析:
busybox->init_main
signal(SIGINT, ctrlaltdel_signal); //定义键盘上的组合键会产生的信号,当按下 ctrlaltdel组合键是,就会产生SIGINT信号,并执行相应处理函数
ctrlaltdel_signal;
run_actions(CTRLALTDEL);
waitfor(a, 0);
  run(a);
waitpid(runpid, &status, 0);
 delete_init_action(a);
console_init(); //初始化控制台,我们在init_post函数里不是打开并复制了控制台文件嘛,现在就是来初始化它了
parse_inittab(); //解析inittab
file = fopen(INITTAB, "r"); //打开配置文件,如果没有配置文件将会采用默认配置,具体参见注释1
new_init_action(ASKFIRST, bb_default_login_shell, VC_2); //构建配置文件,无inittab时才用到,我们分析一下,见注释2
run_actions(SYSINIT);
waitfor(a, 0); //执行应用程序等待执行完毕
run(a); //创建process子进程
waitpid(runpid, &status, 0); //等待它结束
delete_init_action(a); //在init_action_list链表里删除
run_actions(WAIT);
waitfor(a, 0); //执行应用程序等待执行完毕
run(a); //创建process子进程
waitpid(runpid, &status, 0); //等待它结束
delete_init_action(a); //在init_action_list链表里删除
run_actions(ONCE);
run(a); //创建process子进程
delete_init_action(a); //在init_action_list链表里删除
while(1)
{
run_actions(RESPAWN);
if (a->pid == 0) //在run之前pid为0,这在一个死循环中,一旦某个子进程退出,pid变为0,它会再次运行
{
  a->pid = run(a);
  }
run_actions(ASKFIRST);
if (a->pid == 0)
{
 a->pid = run(a);
打印: Please press Enter to activate this console.;
等待回车
创建进程
 }
wpid = wait(NULL); //等待子进程退出
while (wpid > 0)
{
a->pid = 0; //退出后设置pid为0
}
}

注释1:init.c里有定义:#define INITTAB      "/etc/inittab",就是说配置文件是/etc/inittab,我们需要查看一下这个文件,它在/examples文件夹里面, 定义/etc/inittab的语法格式:
# Format for each entry: <id>:<runlevels>:<action>:<process>
id:id=>/dev/id,用作终端
runlevels:可完全忽略
action:执行时机, <action>: Valid actions include: sysinit, respawn, askfirst, wait, once,restart, ctrlaltdel, and shutdown.
process:应用程序或脚本

如果没有配置文件将会运行下面一段程序:
if (file == NULL) {

new_init_action(CTRLALTDEL, "reboot", ""); //   ::ctrlaltdel:reboot
new_init_action(SHUTDOWN, "umount -a -r", ""); //  ::shutdown:umount -a -r
if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", "");
new_init_action(RESTART, "init", ""); //   ::restart:init
new_init_action(ASKFIRST, bb_default_login_shell, ""); //  ::askfirst:-/bin/sh
new_init_action(ASKFIRST, bb_default_login_shell, VC_2); //   tty2 ::askfirst: -/bin/ sh
new_init_action(ASKFIRST, bb_default_login_shell, VC_3); //   tty3 ::askfirst: -/bin/sh
new_init_action(ASKFIRST, bb_default_login_shell, VC_4); //   tty4 ::askfirst: -/bin/sh
/* sysinit */
new_init_action(SYSINIT, INIT_SCRIPT, ""); //   ::sysinit:/etc/init.d/rcS

return;
}
我们来分析一下这段代码,可以反推出默认配置文件时怎样的:
::ctrlaltdel:reboot
::shutdown:umount -a -r
::restart:init
::askfirst:-/bin/sh
tty2::askfirst:-/bin/sh
tty3::askfirst:-/bin/sh
tty4::askfirst:-/bin/sh
::sysinit:/etc/init.d/rc

注释2: new_init_action(ASKFIRST, bb_default_login_shell, VC_2)
我们先搞清楚参数是什么东东:
(1)ASKFIRST: #define ASKFIRST    0x004, 对应配置文件的 action
(2)bb_default_login_shell:
const char bb_default_login_shell[] ALIGN1 = LIBBB_DEFAULT_LOGIN_SHELL;
#define LIBBB_DEFAULT_LOGIN_SHELL "-/bin/sh"
所以 bb_default_login_shell= "-/bin/sh", 对应配置文件的 process
(3)VC:# define VC_2 "/dev/tty2", 对应配置文件的id
因此 new_init_action(ASKFIRST, bb_default_login_shell, VC_2) 实际上是 new_init_action(0x004,  "-/bin/sh"  , "/dev/tty2"  )
接下来我们可是具体分析这个函数:
(1)struct init_action *new_action, *a, *last;:创建struct init_action结构体,这个结构体定义是:
struct init_action {
struct init_action *next;
int action;
pid_t pid;
char command[INIT_BUFFS_SIZE];
char terminal[CONSOLE_NAME_SIZE];
};
(2)strcmp(a->command, command)
 strcmp(a->terminal, cons)
 a->action = action
对上面定义的结构体赋初始值
(3) for (a = last = init_action_list; a; a = a->next) : 把这个结构体放入链表
总结一下就是当解析一个inittab时,会创建许多 struct init_action 结构体,对这些结构体赋值并加入连表里

我们总结一下,一个根文件系统需要什么:
(1)/dev/console,这个在内核启动时打开: sys_open((const char __user *) "/dev/console", O_RDWR, 0) 
(2)/dev/null,当不设置标准输入输出标准出错时,就会定位到/dev/null
(3)配置文件里指定的应用程序
(4)init本事,就busybox


嵌入式根文件系统的移植和制作详解

理论上说一个嵌入式设备如果内核能够运行起来,且不需要运行用户进程的话,是不需要文件系统的,文件系统简单的说就是一种目录结构,由于 linux操作系统的设备在系统中是以文件的形式存在,将这些文件进行分类管理以及提供和内核交互的接口,就形成一定的目录结构也就是文件系统,文件系统是为用户反映系统的一种形式,为用户提供一个检测控制系统的接口。

  根文件系统,我认为根文件系统就是一种特殊的文件系统,那么根文件系统和普通的文件系统有什么区别呢?由于根文件系统是内核启动时挂在的第一个文件系统,那么根文件系统就要包括Linux启动时所必须的目录和关键性的文件,例如Linux启动时都需要有init目录下的相关文件,在 Linux挂载分区时Linux一定会找/etc/fstab这个挂载文件等,根文件系统中还包括了许多的应用程序bin目录等,任何包括这些Linux 系统启动所必须的文件都可以成为根文件系统。

  Linux支持多种文件系统,包括ext2、ext3、vfat、ntfs、iso9660、jffs、romfs和nfs等,为了对各类文件系统进行统一管理,Linux引入了虚拟文件系统VFS(Virtual File System),为各类文件系统提供一个统一的操作界面和应用编程接口。

根文件系统分析之旅——简单分析(一)

 

  Linux启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。之后可以自动或手动挂载其他的文件系统。因此,一个系统中可以同时存在不同的文件系统。

  不同的文件系统类型有不同的特点,因而根据存储设备的硬件特性、系统需求等有不同的应用场合。在嵌入式Linux应用中,主要的存储设备为 RAM(DRAM, SDRAM)和ROM(常采用FLASH存储器),常用的基于存储设备的文件系统类型包括:jffs2, yaffs, cramfs, romfs, ramdisk, ramfs/tmpfs等。

  1. 基于FLASH的文件系统

  Flash(闪存)作为嵌入式系统的主要存储媒介,有其自身的特性。Flash的写入操作只能把对应位置的1修改为0,而不能把0修改为1(擦除 Flash就是把对应存储块的内容恢复为1),因此,一般情况下,向Flash写入内容时,需要先擦除对应的存储区间,这种擦除是以块(block)为单位进行的。

  闪存主要有NOR和NAND两种技术。Flash存储器的擦写次数是有限的,NAND闪存还有特殊的硬件接口和读写时序。因此,必须针对Flash的硬件特性设计符合应用要求的文件系统;传统的文件系统如ext2等,用作Flash的文件系统会有诸多弊端。

  一块Flash芯片可以被划分为多个分区,各分区可以采用不同的文件系统;两块Flash芯片也可以合并为一个分区使用,采用一个文件系统。即文件系统是针对于存储器分区而言的,而非存储芯片。

  在嵌入式Linux下,MTD(Memory Technology Device,存储技术设备)为底层硬件(闪存)和上层(文件系统)之间提供一个统一的抽象接口,即Flash的文件系统都是基于MTD驱动层的(参见上面的Linux下的文件系统结构图)。使用MTD驱动程序的主要优点在于,它是专门针对各种非易失性存储器(以闪存为主)而设计的,因而它对Flash有更好的支持、管理和基于扇区的擦除、读/写操作接口。

  (1) jffs2

  JFFS文件系统最早是由瑞典Axis Communications公司基于Linux2.0的内核为嵌入式系统开发的文件系统。JFFS2是RedHat公司基于JFFS开发的闪存文件系统,最初是针对RedHat公司的嵌入式产品eCos开发的嵌入式文件系统,所以JFFS2也可以用在Linux, uCLinux中。

  Jffs2: 日志闪存文件系统版本2 (Journalling Flash FileSystem v2)

  主要用于NOR型闪存,基于MTD驱动层,特点是:可读写的、支持数据压缩的、基于哈希表的日志型文件系统,并提供了崩溃/掉电安全保护,提供“写平衡”支持等。缺点主要是当文件系统已满或接近满时,因为垃圾收集的关系而使jffs2的运行速度大大放慢。

  jffs不适合用于NAND闪存主要是因为NAND闪存的容量一般较大,这样导致jffs为维护日志节点所占用的内存空间迅速增大,另外,jffs文件系统在挂载时需要扫描整个FLASH的内容,以找出所有的日志节点,建立文件结构,对于大容量的NAND闪存会耗费大量时间。

  (2) yaffs:Yet Another Flash File System

  yaffs/yaffs2是专为嵌入式系统使用NAND型闪存而设计的一种日志型文件系统。与jffs2相比,它减少了一些功能(例如不支持数据压缩),所以速度更快,挂载时间很短,对内存的占用较小。另外,它还是跨平台的文件系统,除了Linux和eCos,还支持WinCE, pSOS和ThreadX等。

  yaffs/yaffs2自带NAND芯片的驱动,并且为嵌入式系统提供了直接访问文件系统的API,用户可以不使用Linux中的MTD与VFS,直接对文件系统操作。当然,yaffs也可与MTD驱动程序配合使用。

  yaffs与yaffs2的主要区别在于,前者仅支持小页(512 Bytes) NAND闪存,后者则可支持大页(2KB) NAND闪存。同时,yaffs2在内存空间占用、垃圾回收速度、读/写速度等方面均有大幅提升。

  (3) Cramfs:Compressed ROM File System

  Cramfs是Linux的创始人 Linus Torvalds参与开发的一种只读的压缩文件系统。它也基于MTD驱动程序。

  在cramfs文件系统中,每一页(4KB)被单独压缩,可以随机页访问,其压缩比高达2:1,为嵌入式系统节省大量的Flash存储空间,使系统可通过更低容量的FLASH存储相同的文件,从而降低系统成本。

  Cramfs文件系统以压缩方式存储,在运行时解压缩,所以不支持应用程序以XIP方式运行,所有的应用程序要求被拷到RAM里去运行,但这并不代表比 Ramfs需求的RAM空间要大一点,因为Cramfs是采用分页压缩的方式存放档案,在读取档案时,不会一下子就耗用过多的内存空间,只针对目前实际读取的部分分配内存,尚没有读取的部分不分配内存空间,当我们读取的档案不在内存时,Cramfs文件系统自动计算压缩后的资料所存的位置,再即时解压缩到 RAM中。

  另外,它的速度快,效率高,其只读的特点有利于保护文件系统免受破坏,提高了系统的可靠性。

  由于以上特性,Cramfs在嵌入式系统中应用广泛。

  但是它的只读属性同时又是它的一大缺陷,使得用户无法对其内容对进扩充。

  Cramfs映像通常是放在Flash中,但是也能放在别的文件系统里,使用loopback 设备可以把它安装别的文件系统里。

  (4) Romfs

  传统型的Romfs文件系统是一种简单的、紧凑的、只读的文件系统,不支持动态擦写保存,按顺序存放数据,因而支持应用程序以XIP(eXecute In Place,片内运行)方式运行,在系统运行时,节省RAM空间。uClinux系统通常采用Romfs文件系统。

  其他文件系统:fat/fat32也可用于实际嵌入式系统的扩展存储器(例如PDA, Smartphone, 数码相机等的SD卡),这主要是为了更好的与最流行的Windows桌面操作系统相兼容。ext2也可以作为嵌入式Linux的文件系统,不过将它用于 FLASH闪存会有诸多弊端。

  2. 基于RAM的文件系统

  (1) Ramdisk

  Ramdisk是将一部分固定大小的内存当作分区来使用。它并非一个实际的文件系统,而是一种将实际的文件系统装入内存的机制,并且可以作为根文件系统。将一些经常被访问而又不会更改的文件(如只读的根文件系统)通过Ramdisk放在内存中,可以明显地提高系统的性能。

  在Linux的启动阶段,initrd提供了一套机制,可以将内核映像和根文件系统一起载入内存。

  (2)ramfs/tmpfs

  Ramfs是Linus Torvalds开发的一种基于内存的文件系统,工作于虚拟文件系统(VFS)层,不能格式化,可以创建多个,在创建时可以指定其最大能使用的内存大小。(实际上,VFS本质上可看成一种内存文件系统,它统一了文件在内核中的表示方式,并对磁盘文件系统进行缓冲。)

  Ramfs/tmpfs文件系统把所有的文件都放在RAM中,所以读/写操作发生在RAM中,可以用ramfs/tmpfs来存储一些临时性或经常要修改的数据,例如/tmp和/var目录,这样既避免了对Flash存储器的读写损耗,也提高了数据读写速度。

  Ramfs/tmpfs相对于传统的Ramdisk的不同之处主要在于:不能格式化,文件系统大小可随所含文件内容大小变化。

  Tmpfs的一个缺点是当系统重新引导时会丢失所有数据。

  3. 网络文件系统NFS (Network File System)

  NFS是由Sun开发并发展起来的一项在不同机器、不同操作系统之间通过网络共享文件的技术。在嵌入式Linux系统的开发调试阶段,可以利用该技术在主机上建立基于NFS的根文件系统,挂载到嵌入式设备,可以很方便地修改根文件系统的内容。

  以上讨论的都是基于存储设备的文件系统(memory-based file system),它们都可用作Linux的根文件系统。实际上,Linux还支持逻辑的或伪文件系统(logical or pseudo file system),例如procfs(proc文件系统),用于获取系统信息,以及devfs(设备文件系统)和sysfs,用于维护设备文件。

  我们要移植的开发板的存储设备为Nandflash,我们可以用应用比较广泛的cramfs文件系统。

  二.移植准备

  1.目标板

  我们还是使用之前移植过程一直使用的开发板参数。

  2.软件准备

  (1)Busybox

  Busybox被形象的称为嵌入式linux系统中的瑞士军刀,可以从这个称呼中看到busybox是一个集多种功能于一身的东西,它将许多常用的UNIX命令和工具结合到了一个单独的可执行程序中。虽然与相应的GNU工具比较起来,busybox所提供的功能和参数略少,但在比较小的系统(例如启动盘)或者嵌入式系统中,已经足够了。

  Busybox在设计上就充分考虑了硬件资源受限的特殊工作环境。它采用一种很巧妙的办法减少自己的体积:所有的命令都通过“插件”的方式集中到一个可执行文件中,在实际应用过程中通过不同的符号链接来确定到底要执行哪个操作。例如最终生成的可执行文件为busybox,当为它建立一个符号链接ls的时候,就可以通过执行这个新命令实现列目录的功能。采用单一执行文件的方式最大限度地共享了程序代码,甚至连文件头、内存中的程序控制块等其他操作系统资源都共享了,对于资源比较紧张的系统来说,真是最合适不过了。

  在busybox的编译过程中,可以非常方便地加减它的“插件”,最后的符号链接也可以由编译系统自动生成。

  编译busybox

  Busybox的编译过程与内核的编译过程很接近都是先make menuconfig进行配置,然后在make进行编译。

  【1】从http://www.busybox.net/downloads/下载busybox工具。这里我们选择busybox-1.13.4.tar.bz2

  【2】解压busybox-1.13.4.tar.bz2使用命令

  tar jxvf busybox-1.13.4.tar.bz2

  【3】进入busybox目录,修改Makefile  在164行 CROSS_COMPILE=arm-linux-

根文件系统分析之旅——简单分析(一)

 

  【4】Make menuconfig进行配置,可以选择静态编译,如果是动态编译的话要拷贝相应的库文件,默认配置保存即可。

根文件系统分析之旅——简单分析(一)

 

  【5】make all install

  这是会在busybox目录下生成_install文件夹。

  (2)文件系统打包工具

  【1】从http://prdownloads.sourceforge.net/cramfs/下载cramfs工具。

  【2】解压cramfs-1.1.tar.gz使用命令:tar zxvf cramfs-1.1.tar.gz

  【3】进入cramfs工具的根目录执行make。

  【4】Make后在cramfs工具的根目录中就会生成一个mkcramfs文件,这个就是我们需要的工具。

  三.制作过程

  1.建立根文件系统目录

  就是之前busybox生成的_install目录 cd …/_install

  2.创建各种必要的系统文件目录。

  mkdir dev lib tmp proc

  3.创建设备文件。

  cd dev

  mknod fb0 c 29 0     建立framebuffer设备文件

  mknod ts0 c 13 128     建立触摸屏设备文件

  mknod console c 5 1

  mknod tty0 c 4 0

  mknod tty1 c 4 1

  mknod tty2 c 4 2

  mknod tty3 c 4 3

  mknod tty4 c 4 4

  4.添加必要的库文件,由于之前没有选择静态编译busybox,这里要拷贝相应的库文件

  cd lib

  cp –arf …/arm-linux/lib/* .

  5.根据自己需要添加应用程序

  这里我们编写一个简单的应用程序打印一句问候语,程序代码如下:

  #include <stdio.h>

  void main()

  {

  printf(“Hello World\n”);

  }

  注意编译时要使用arm-linux-gcc,由于之前我们把编译器的库文件全部进行拷贝,可以直接动态编译。生成的可执行文件hello放入tmp文件夹。使用的命令:

  rm-linux-gcc hello.c –o hello

  cp –arf …/_install/tmp/

  6.打包

  mkcramfs _install rootfs

  四.烧写过程

  我们采用的烧写方法和烧写内核的方法一样内核的烧,我们采用tftp方式,用网线将目标板和pc机连接起来,配置好目标板的网络参数,主要是serverip、ipaddr。

  首先将rootfs下载到内存中:#tftp 30008000 rootfs

  按照之前内核的nandflash分区进行烧写,将内存中的文件系统烧写到flash中:

  #nand erase 500000 3b00000

  #nand write 30008000 500000 3b00000

  重启uboot使其加载文件系统。

  可以看到内核启动,不在出现panic,这时会提示回车,回车后进入命令行,我们可以使用一些linux的常用命令,如:ls、cd、vi等。

  如下图所示:

根文件系统分析之旅——简单分析(一)

 

  注意:我们这里使用的是PUTTY串口显示软件,如果用之前的DNW查看串口信息,会出现下面的错误:

根文件系统分析之旅——简单分析(一)

 

  可以看到第一个目录 [1;34mbin [0m

  会发现出现的信息除了bin目录外还有其他的内容,这并不是文件系统的问题只是DNW这款软件并不支持这些表示色彩的附加信息,换成PUTTY可以正常显示了。

  我们可以运行一下我们自己的应用程序hello:./hello

  可以看到打印信息Hello World如下图所示:

根文件系统分析之旅——简单分析(一)


经常看到坛子里有人对内核启动进行分析,但是大部分集中在各个模式跳转,内存系统建立,中断系统建立等流程的分析上。这里想根据自己之前学习,看书的理解对内核从磁盘加载开始到根文件系统建立写一个粗线条的理解,有兴趣的朋友欢迎指正。

一.上电之初,grub经过各个阶段(stage 1 stage1.5 stage 2)终于找到kernerl image和ramfs(忘了正名了,随便起了个)。随后将其加载入内存。此时文件系统尚未建立,但是由于grub自己是懂“文件系统的”,或者说自己有些简陋的驱动可以访问硬盘而已
二.内核自解压,执行各个子系统的初始化
三."ramfs"自解压
四.内核开始挂载内存中的一个简陋的根文件系统。(这之前要挂载rootfs,一个空的内存文件系统,目的在于建立根文件系统挂载点)然后挂载“ramfs”
五."ramfs"中有预先放置好的一些dirver,如磁盘driver。和一些已经创建好的设备文件(?/dev /dev/fs)。根据这些driver和设备文件。内核最终挂载真正的根设备。
六.至此,挂载完毕。可以执行一些各个厂商自制的配置文件进行个性化设置,不过这些已经是用户态程序了~~

有个小疑问:“ramfs”中预创建的一些设备文件是谁创建的。有没有什么原则?不会是漫天突发奇想随手创建的吧?