一起分析Linux系统设计思想——04文件系统(二)

it2025-06-25  4

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

文章目录

3 剖析第一个应用程序(接上篇)3.1 配置文件的设计3.2 配置文件的解析3.2.1 字符串操作

3 剖析第一个应用程序(接上篇)

Tips:arm构建根文件系统一般使用busybox,我们这里使用的源码是busybox-1.7.0。

上篇中我们提到init进程启动的第一个应用程序有好多“备胎”,那我们从哪一个开始分析呢。我们在设备上查看所有这些备份,发现只有三个是有效的,而且都链接到了 /bin/busybox 文件。

Tips:这种用法其实就是编译了一个busybox程序/工具,该工具可以有好多入参,而且入参就是支持的指令名称,将这些入参的名称做成指向busybox的链接文件就达到了“我们安装了好多指令”的效果。这个技巧值得我们学习,背后的指导思想是 数据驱动 ,只不过是在这个基础上恰到好处地使用了“链接文件”和“程序名会被作为第一个参数传入(通过这个特点可以做到让ls 等价于 busybox ls)”的技术特点而已。

下面这三个应用程序的关系如下:linuxrc约等价于/sbin/init;/bin/sh是前两者的子集。

所以,接下来我们以/sbin/init程序为例进行分析。

# 在嵌入式设备上输入下述指令的结果 # ls -al sbin/init lrwxrwxrwx 1 0 0 14 Dec 24 2010 sbin/init -> ../bin/busybox # ls -al bin/init ls: bin/init: No such file or directory # ls -al bin/sh lrwxrwxrwx 1 0 0 7 Dec 24 2010 bin/sh -> busybox # ls -al etc/init ls: etc/init: No such file or directory # ls -al linuxrc lrwxrwxrwx 1 0 0 11 Dec 24 2010 linuxrc -> bin/busybox

3.1 配置文件的设计

有了前面的基础,我们这里不再啰里啰唆地讲拿到一个工程如何确定程序的入口并顺藤摸瓜地找到我们感兴趣的接口了。直接分析/sbin/init的入口函数 init_main()。

/* busybox源码树/init/init.c */ int init_main(int argc, char **argv) { struct init_action *a; pid_t wpid; ... /* Hello world */ /* 在控制台中可以看到下述打印: init started: BusyBox v1.7.0 (2008-01-22 10:04:09 EST) */ message(MAYBE_CONSOLE | L_LOG, "init started: %s", bb_banner); ... /* Check if we are supposed to be in single user mode */ if (argc > 1 && (!strcmp(argv[1], "single") || !strcmp(argv[1], "-s") || LONE_CHAR(argv[1], '1')) ) { /*不会走该分支*/ /* Start a shell on console */ new_init_action(RESPAWN, bb_default_login_shell, ""); } else { parse_inittab(); /*会执行解析初始化表的函数*/ } ... }

接下我们探究一下 parse_inittab() 函数,以及其中的配置文件是如何设计的。

/* busybox源码树/init/init.c */ static void parse_inittab(void) { FILE *file; char buf[INIT_BUFFS_SIZE], lineAsRead[INIT_BUFFS_SIZE]; char tmpConsole[CONSOLE_NAME_SIZE]; char *id, *runlev, *action, *command, *eol; const struct init_action_type *a = actions; /* 打开初始化配置文件 #define INITTAB "/etc/inittab" */ file = fopen(INITTAB, "r"); /* 以下是默认配置,下面注释每行转换成配置文件后内容 */ if (file == NULL) { /* No inittab file -- set up some default behavior */ /* Reboot on Ctrl-Alt-Del */ new_init_action(CTRLALTDEL, "reboot", ""); /*::ctrlaltdel:reboot*/ /* Umount all filesystems on halt/reboot */ new_init_action(SHUTDOWN, "umount -a -r", ""); /*::shutdown:umount -a -r*/ ... /* Prepare to restart init when a HUP is received */ new_init_action(RESTART, "init", ""); /*::restart:init*/ /* Askfirst shell on tty1-4 */ new_init_action(ASKFIRST, bb_default_login_shell, ""); /*::askfirst:-/bin/sh*/ new_init_action(ASKFIRST, bb_default_login_shell, VC_2); /*/dev/tty2::askfirst:-/bin/sh*/ new_init_action(ASKFIRST, bb_default_login_shell, VC_3); /*/dev/tty3::askfirst:-/bin/sh*/ new_init_action(ASKFIRST, bb_default_login_shell, VC_4); /*/dev/tty4::askfirst:-/bin/sh*/ /* sysinit */ new_init_action(SYSINIT, INIT_SCRIPT, ""); /*::sysinit:/etc/init.d/rcS*/ return; } ... }

我们发现该函数的设计还是非常富有弹性的,如果可以打开用户设置的初始化配置文件则使用文件中的初始化列表,如果打开失败则使用一套自己写死的默认初始化配置表。下面我们分别将用户配置的文件和默认的配置(转化成配置文件后)分别列举出来。我们发现用户配置的文件和默认的差别并不是很大,而且我尝试了删掉用户配置的文件直接使用默认配置,系统也是可以正常启动和运行的。

# /etc/inittab 用户配置文件内容 ::sysinit:/etc/init.d/rcS # 当设备启动时,初始化shell脚本 s3c2410_serial0::askfirst:-/bin/sh # 当用户响应时,开启shell终端 ::ctrlaltdel:/sbin/reboot # 当用户按下ctrl+alt+del时,重启系统 ::shutdown:/bin/umount -a -r # 当设备关机时,umount 所有文件系统 # 由默认配置转换成的配置文件内容 ::ctrlaltdel:reboot ::shutdown:umount -a -r ::restart:init ::askfirst:-/bin/sh /dev/tty2::askfirst:-/bin/sh /dev/tty3::askfirst:-/bin/sh /dev/tty4::askfirst:-/bin/sh ::sysinit:/etc/init.d/rcS

我们解析一下配置文件格式。

每一行都遵循下述格式:<id>:<runlevels>:<action>:<process>。id为终端,可以忽略;runlevels一般用不到,直接忽略。接下来重点来了~敲黑板!!!action:process 动作:处理 才是配置文件设计的核心。背后依赖的是 事件驱动思想 ,用大白话说就是 在什么时候干什么事 。

3.2 配置文件的解析

首先要问一个问题:配置文件为什么要解析?

因为配置文件是文本文件,也就是人类更容易看懂的格式。但干活儿的是计算机,所以需要将字符串转换成二进制(数字)以便给计算机看。这一步工作就叫做 解析 。

解析的细节操作就是将文件中记录的字符串按照配置文件定义的格式等价地转换为结构体。每一行对应一个结构体,多行就对应多个结构体,多个结构体再按照链表结构进行组织。

所以,字符串操作和数据结构确实是很重要也很实用的知识。但本篇阐述的重点不在此,仅做简单分析,有心的TX自行修炼吧~~

3.2.1 字符串操作

直接上代码,详见注释。代码中有好多字符串的处理步骤和技巧是我们可以在自己的项目中借鉴和使用的。

static void parse_inittab(void) { FILE *file; char buf[INIT_BUFFS_SIZE], lineAsRead[INIT_BUFFS_SIZE]; char tmpConsole[CONSOLE_NAME_SIZE]; char *id, *runlev, *action, *command, *eol; const struct init_action_type *a = actions; file = fopen(INITTAB, "r"); while (fgets(buf, INIT_BUFFS_SIZE, file) != NULL) { /* Skip leading spaces,只要解析字符串这一点基本都要考虑到, 从另一个角度讲就是——把这行代码理解后记下来吧。 */ for (id = buf; *id == ' ' || *id == '\t'; id++); /* Skip the line if it's a comment, 这个操作也不可或缺,方便调试和修改*/ if (*id == '#' || *id == '\n') /*'#'代表注释,'\n'是空行*/ continue; /*从这里可以看出fgets每次只取文件的一行进行解析*/ /* Trim the trailing \n ,干掉\n,因为\n只是给人看的。这个也是常规操作 */ /* char *strrchr(const char *str, int c)的功能: 在参数 str 所指向的字符串中从右开始搜索第一次出现字符 c */ eol = strrchr(id, '\n'); if (eol != NULL) *eol = '\0'; /* Keep a copy around for posterity's sake (and error msgs) */ /* buf指针位置并没有改变,这行代码出现在这里而不是出现在刚刚读取完 文件后的原因是接下来马上要使用lineAsRead;遵循的原则是:即插即用; 好处是:逻辑紧凑。*/ strcpy(lineAsRead, buf); /* Separate the ID field from the runlevels */ /* 注意这里的技巧(划重点):如果将id定义为指向字符串的第1层指针(大窗口); 那runlev就是第2层指针(小窗口)*/ runlev = strchr(id, ':'); if (runlev == NULL || *(runlev + 1) == '\0') { /*没有:和只有:的情况*/ message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead); continue; } else { /* 将分割符直接替换成字符串结束符。这种操作就像是将一条大蛇切成几个小段, 而且是只切不动(刀口还对着,这是一把宝刀,哈哈~) */ *runlev = '\0'; ++runlev; /*runlev指向runlevels这一段的开头*/ } /* Separate the runlevels from the action */ action = strchr(runlev, ':'); if (action == NULL || *(action + 1) == '\0') { message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead); continue; } else { *action = '\0'; ++action; /*action指向action这一段的开头*/ } /* Separate the action from the command */ command = strchr(action, ':'); if (command == NULL || *(command + 1) == '\0') { message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead); continue; } else { *command = '\0'; ++command; /*command指向process这一段的开头*/ } /* Ok, now process it */ for (a = actions; a->name != 0; a++) { /*以actions的name进行遍历*/ if (strcmp(a->name, action) == 0) {/*配置文件中的和预定义的相同*/ if (*id != '\0') { /*这里是冗余判断,其实*id如果是0代码是执行不到这里的*/ if (strncmp(id, "/dev/", 5) == 0) id += 5; /* 如果id没有/dev/,则给它加一个 */ strcpy(tmpConsole, "/dev/"); safe_strncpy(tmpConsole + 5, id, sizeof(tmpConsole) - 5); id = tmpConsole; } new_init_action(a->action, command, id); break; } } ... } fclose(file); }

注:防止文章过长影响阅读效果,链表结构的分析放到下一篇,欢迎小伙伴们围观。


恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

最新回复(0)