1. rootfs 到底是什么

1.1 定义

rootfs(根文件系统)是运行中的操作系统用来提供根目录 / 的文件系统实例。所有绝对路径都从 / 开始解析;其他文件系统可以挂载到它下面的目录,最终形成进程可见的完整文件系统目录树。

  这里的 “文件系统实例” 是指一个已经被操作系统创建或挂载、能够实际接受文件操作的文件系统对象,而不是 FAT、ROMFS、ext4 这类文件系统格式名称。rootfs 可以采用不同的文件系统实现;它之所以称为“根文件系统”,关键在于它提供了根目录 /

1.2 容易混淆的概念

概念含义
rootfs运行时提供根目录 / 的文件系统实例。
rootfs 镜像或目录制作固件时准备的一组根目录内容,是 rootfs 的数据来源或构建产物,不等同于运行中的文件系统实例。
mount point当前目录树中的接入位置,其他文件系统通过它进入完整目录树。

如何理解 rootfs 镜像? rootfs 镜像是根目录内容在构建和部署阶段的保存形式,例如 SquashFS、ext4 镜像或 cpio 归档。镜像内容需要通过挂载、解包或路径映射等方式接入运行时目录树;某些镜像也可以直接提供 /,从而成为运行中的 rootfs。镜像是部署载体,rootfs 是运行时角色,动态创建的内存 rootfs 可以没有对应镜像。

1.3 为什么系统需要 rootfs

  没有根目录,绝对路径就没有解析起点,其他文件系统也没有可接入的路径位置。系统必须先建立可用的 rootfs,才能继续组织目录、注册设备、接入其他文件系统,并为应用提供统一的路径访问方式。

2. rootfs 在文件访问中的位置

2.1 绝对路径从 rootfs 开始

  以 open("/etc/config", O_RDONLY) 为例,路径以 / 开头,因此系统首先从根目录开始查找 etc,再从 etc 中查找 config。rootfs 提供了这个查找过程的起点。

  一次文件访问通常可以抽象为:

应用调用 open/read/stat
          |
          v
     规范化路径
          |
          v
从根目录 / 开始逐级查找路径分量
          |
          v
判断是否进入其他文件系统或设备节点
          |
          v
调用最终对象的文件操作函数

需要注意:路径从 rootfs 开始,不等于最终操作一定由 rootfs 完成。 rootfs 提供路径起点;路径命中其他文件系统或设备后,后续操作会转交给对应实现。

2.2 rootfs、VFS 和具体文件系统

  很多操作系统会设置 VFS 或功能相近的 I/O 抽象层,将应用使用的 open()read()stat() 等统一接口分派给不同对象:

层次主要职责
rootfs提供根目录 /,作为绝对路径解析的起点。
VFS 或 I/O 抽象层组织路径查找,识别文件系统或设备,并选择操作函数。
具体文件系统管理自己的节点、元数据和内容,实现最终的 open/read/write 等操作。

  rootfs 自身也需要一种具体实现,但“rootfs”描述的是它在目录树中的位置和职责,不是某一种固定的磁盘格式。

2.3 进入挂载点后,路径归属发生变化

  假设系统先建立内存 rootfs,再把 SD 卡上的 FAT 文件系统接入 /media/sdcard0。应用调用:

open("/media/sdcard0/etc/config", O_RDONLY);

  路径处理过程可以表示为:

/                         由 rootfs 提供路径起点
└── media                 rootfs 中的目录
    └── sdcard0           SD 卡 FAT 文件系统的接入点
        └── etc           由 FAT 文件系统管理
            └── config    由 FAT 文件系统查找和读取

  系统先从 / 查找 media;到达 /media/sdcard0 后,剩余的 etc/config 由 FAT 文件系统处理。随后对该文件的 read()write() 等操作也由 FAT 文件系统完成,rootfs 不保存它的文件内容。

3. rootfs 与完整目录树的形成

3.1 先建立根,再接入其他文件系统

  这里讨论的不是所有驱动的严格初始化顺序,而是文件系统对象进入运行时目录树时的逻辑依赖:

初始化 I/O 或 VFS 基础设施
            |
            v
创建或挂载 rootfs,使 / 可用
            |
            v
建立 /dev、/proc、/media 等接入位置
            |
            v
接入设备文件系统、procfs 和磁盘文件系统
            |
            v
形成应用可以通过绝对路径访问的完整目录树

  文件系统驱动可以提前完成注册,但只有在根目录、接入路径以及相应的挂载或设备登记关系建立后,应用才能通过绝对路径访问它。

3.2 根目录树可以由多个文件系统共同组成

  rootfs 首先提供 / 和最初的目录结构,其他文件系统再接入其中。进程最终看到的是一棵统一目录树:

/                         rootfs 提供根和路径起点
├── etc                   可能由 rootfs 自身提供
├── dev                   接入设备文件系统或设备节点
├── proc                  接入 procfs
├── sys                   接入 sysfs
└── media
    └── sdcard0           接入 SD 卡上的 FAT 文件系统

  从路径关系看,/dev/proc/sys/media/sdcard0 都位于 / 下面;但这种“包含”是目录树中的包含,不是数据和实现上的包含。procfs 生成自己的节点,设备驱动处理设备操作,FAT 文件系统读写 SD 卡数据。rootfs 提供根和接入基础,各个对象共同组成进程最终看到的目录树。

3.3 常见目录只是约定

  嵌入式系统的根目录树中经常出现以下目录:

目录常见用途
/dev设备访问入口
/proc/sys内核和系统状态的虚拟接口
/etc系统配置
/bin/sbin/lib程序和运行库
/tmp/var临时数据和运行时可变数据
/mnt/media其他文件系统的接入位置

  这些目录是否存在、由哪个文件系统提供,取决于具体系统。rootfs 的必要条件不是拥有固定的目录清单,而是提供根目录 /,使路径解析和后续接入成为可能。

4. 阅读一个操作系统的 rootfs 实现时看什么

  分析一个操作系统的 rootfs 实现,可以依次回答四个问题:

  1. 谁创建根目录 /,它对应哪个运行时对象?
  2. 路径通过什么数据结构逐级查找?
  3. 目录、设备节点和符号链接分别如何创建?
  4. 路径命中其他文件系统或设备后,操作如何完成分派?

  可以用三层模型组织源码:

路径命名层:目录、设备名、符号链接
      |
路径分派层:确定由 rootfs、文件系统还是设备驱动处理
      |
对象操作层:具体实现 open/read/write/stat 等操作

  下面分析 SylixOS 时也沿用这条主线:先明确 rootFs 的职责,再看节点如何进入树中,最后跟踪路径如何转交给具体实现。

5. SylixOS rootFs:内存节点树与根设备

5.1 rootFs 的职责边界

  SylixOS 的 rootFs 是一棵驻留在内存中的节点树,同时也是 I/O 层进行路径匹配和设备分派的基础。它主要保存:

  • 根目录下的目录结构;
  • 设备在目录树中的入口;
  • 符号链接及其目标路径;
  • socket、IPC key 等少量本地节点的元数据。

  它不是一个用于保存普通文件内容的完整磁盘文件系统。

  SylixOS 使用两个对象共同表示根:

对象作用
_G_rfsrRootrootFs 节点树的根容器,保存第一层子节点、内存用量和节点数量。
_G_devhdrRoot注册到 I/O 系统中的根设备头,对应路径 / 和 rootFs 操作表。

  因此,/ 本身不是一个普通的 LW_ROOTFS_NODE。第一层目录和设备节点直接挂在 _G_rfsrRoot.RFSR_plineSon 下;I/O 层则通过 _G_devhdrRoot 调用 rootFs 的文件操作。

5.2 核心数据结构

fs/rootFs/rootFsLib.h:30-68 定义了 rootfs 的主要结构:

/*********************************************************************************************************
  rootfs 节点内容
*********************************************************************************************************/

typedef union lw_rootfs_node_value {
    PCHAR                        RFSNV_pcName;                          /*  节点名字                    */
    PLW_DEV_HDR                  RFSNV_pdevhdr;                         /*  设备指针                    */
} LW_ROOTFS_NODE_VALUE;
typedef LW_ROOTFS_NODE_VALUE    *PLW_ROOTFS_NODE_VALUE;

/*********************************************************************************************************
  rootfs 节点
*********************************************************************************************************/
typedef struct lw_rootfs_node {
    LW_LIST_LINE                 RFSN_lineBrother;                      /*  兄弟节点                    */
    struct lw_rootfs_node       *RFSN_prfsnFather;                      /*  父系节点                    */
    PLW_LIST_LINE                RFSN_plineSon;                         /*  儿子节点                    */
    
    INT                          RFSN_iOpenNum;                         /*  打开次数                    */
    size_t                       RFSN_stAllocSize;                      /*  此节点占用内存大小          */
    mode_t                       RFSN_mode;                             /*  模式                        */
    time_t                       RFSN_time;                             /*  创建时间                    */
    INT                          RFSN_iNodeType;                        /*  节点类型                    */
    
    uid_t                        RFSN_uid;
    gid_t                        RFSN_gid;
    
    LW_ROOTFS_NODE_VALUE         RFSN_rfsnv;                            /*  节点的内容                  */
    PCHAR                        RFSN_pcLink;                           /*  链接目标 (不是链接文件为 0) */
} LW_ROOTFS_NODE;
typedef LW_ROOTFS_NODE          *PLW_ROOTFS_NODE;

/*********************************************************************************************************
  rootfs 根
*********************************************************************************************************/

typedef struct lw_rootfs_root {
    PLW_LIST_LINE                RFSR_plineSon;                         /*  指向第一个儿子              */
    size_t                       RFSR_stMemUsed;                        /*  内存消耗量                  */
    ULONG                        RFSR_ulFiles;                          /*  文件总数                    */
    time_t                       RFSR_time;                             /*  创建时间                    */
} LW_ROOTFS_ROOT;
typedef LW_ROOTFS_ROOT          *PLW_ROOTFS_ROOT;

  RFSN_prfsnFatherRFSN_plineSonRFSN_lineBrother 共同组成父子树和兄弟链表。目录、链接等节点直接保存自身名称;DEV 节点保存 PLW_DEV_HDR,设备名称则可以从设备头取得。

在这里插入图片描述

5.3 节点类型

  fs/rootFs/rootFs.h:46-50 给出了节点类型:

常量作用
LW_ROOTFS_NODE_TYPE_DIRrootFs 本地目录。
LW_ROOTFS_NODE_TYPE_DEV设备或文件系统入口,值为 PLW_DEV_HDR
LW_ROOTFS_NODE_TYPE_LNK符号链接,RFSN_pcLink 保存目标路径。
LW_ROOTFS_NODE_TYPE_SOCKAF_UNIX socket 文件。
LW_ROOTFS_NODE_TYPE_REG普通节点,主要用于 IPC key 等场景,不提供普通文件内容读写。

  理解 SylixOS rootFs 的关键不是把它分成多套根文件系统,而是看同一棵节点树中的不同节点最终由谁处理:

DIR / REG / SOCK  -> rootFs 自身处理节点操作
DEV               -> 转交节点保存的设备或文件系统
LNK               -> 改写路径后重新匹配

6. rootFs 节点树如何建立

6.1 _IosInit() 建立最初的根目录环境

system/ioLib/ioLib.c:523-545 在 I/O 子系统初始化阶段建立最初的 rootFs 环境:

INT  _IosInit (VOID)
{
	......
 	rootFsDrv();                                                        /*  安装 rootFs 驱动            */
    rootFsDevCreate();                                                  /*  创建根设备                  */
    
    API_RootFsMakeNode("/dev", LW_ROOTFS_NODE_TYPE_DIR, LW_ROOTFS_NODE_OPT_ROOTFS_TIME, 
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  设备目录                    */
    API_RootFsMakeNode("/dev/pty", LW_ROOTFS_NODE_TYPE_DIR, LW_ROOTFS_NODE_OPT_ROOTFS_TIME,
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  虚拟终端设备                */
    API_RootFsMakeNode("/dev/pipe", LW_ROOTFS_NODE_TYPE_DIR, LW_ROOTFS_NODE_OPT_ROOTFS_TIME,
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  管道设备                    */
    API_RootFsMakeNode("/dev/input", LW_ROOTFS_NODE_TYPE_DIR, LW_ROOTFS_NODE_OPT_ROOTFS_TIME,
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  输入设备                    */
    API_RootFsMakeNode("/dev/blk", LW_ROOTFS_NODE_TYPE_DIR, LW_ROOTFS_NODE_OPT_ROOTFS_TIME,
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  blk raw 设备                */
    API_RootFsMakeNode("/mnt", LW_ROOTFS_NODE_TYPE_DIR, LW_ROOTFS_NODE_OPT_ROOTFS_TIME, 
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  文件系统挂载目录            */
    API_RootFsMakeNode("/media", LW_ROOTFS_NODE_TYPE_DIR,
                       LW_ROOTFS_NODE_OPT_ROOTFS_TIME | LW_ROOTFS_NODE_OPT_ROOTFS_EXTDIR,
                       DEFAULT_DIR_PERM, LW_NULL);                      /*  热插拔存储器挂载目录        */
                                                                        /*  unix 兼容 null 设备路径     */
    (VOID)API_IosDevAddEx(&_G_devhdrNull.NZ_devHdr, "/dev/null", iNullDrv, DT_CHR);
    (VOID)API_IosDevAddEx(&_G_devhdrZero.NZ_devHdr, "/dev/zero", iZeroDrv, DT_CHR);
	......
}

这几步的职责不同:

调用作用
rootFsDrv()安装 rootFs 的 file_operations,取得驱动号。
rootFsDevCreate()_G_devhdrRoot 以名称 / 注册为根设备,并初始化 _G_rfsrRoot
API_RootFsMakeNode()创建 /dev/mnt/media 等本地目录节点。
API_IosDevAddEx()登记 /dev/null/dev/zero 的设备头,并创建相应 DEV 节点。

所以 SylixOS 的根、基础目录和第一批设备入口在 _IosInit() 中就已经建立。

6.2 API_RootFsMakeNode() 的核心流程

API_RootFsMakeNode() 位于 fs/rootFs/rootFsLib.c:352 起,是构造 rootFs 节点树的统一底层入口。其主要流程为:

  1. 检查绝对路径、节点类型以及 pvValue 是否有效。
  2. 根据类型分配节点内存,并填写名称、设备头或链接目标。
  3. 在锁保护下调用 __rootFsFindNode(),检查重名、父节点和中间目录。
  4. 将新节点加入父目录的子链表;第一层节点直接加入 _G_rfsrRoot.RFSR_plineSon
  5. 更新内存用量和节点计数。

节点插入逻辑可以概括为:

if (prfsnFather) {
    prfsnNew->RFSN_prfsnFather = prfsnFather;
    _List_Line_Add_Ahead(&prfsnNew->RFSN_lineBrother,
                         &prfsnFather->RFSN_plineSon);
} else {
    prfsnNew->RFSN_prfsnFather = LW_NULL;
    _List_Line_Add_Ahead(&prfsnNew->RFSN_lineBrother,
                         &_G_rfsrRoot.RFSR_plineSon);
}

  父节点必须是 DIR,且中间目录必须已经存在。例如创建 /dev/ttyS0 前,/dev 必须先建立。函数不会自动补齐缺失的中间目录。

6.3 设备创建同时完成两种登记

  设备创建函数通常调用 API_IosDevAddEx()。该接口不仅登记设备对象,还让设备名称进入 rootFs:

设备创建函数,例如 ttyDevCreate("/dev/ttyS0", ...)
                    |
                    v
API_IosDevAddEx(pdevhdr, "/dev/ttyS0", iDrvNum, DT_CHR)
                    |
                    +-- 将设备头加入 I/O 设备表
                    |
                    v
rootFsMakeDev("/dev/ttyS0", pdevhdr)
                    |
                    v
API_RootFsMakeNode(..., LW_ROOTFS_NODE_TYPE_DEV, ..., pdevhdr)
                    |
                    v
rootFs 节点树中出现 /dev/ttyS0

rootFsMakeDev()fs/rootFs/rootFs.h 中的宏:

#define rootFsMakeDev(pcPath, pdevhdr) \
    API_RootFsMakeNode(pcPath,
                       LW_ROOTFS_NODE_TYPE_DEV,
                       LW_ROOTFS_NODE_OPT_NONE,
                       0,
                       (PVOID)(pdevhdr))

因此,创建设备实际建立了两种关系:

登记位置保存的信息用途
I/O 设备表设备头、驱动号、设备号等确定设备对象及其操作表。
rootFs 节点树设备路径到设备头的关联使设备能够通过路径被匹配。

  如果 rootFs 节点创建失败,API_IosDevAddEx() 会从 I/O 设备表中移除刚加入的设备头,避免留下“设备已经登记,但路径不可访问”的半完成状态。相关代码位于 system/ioLib/ioSys.c:544-637

  可以用一句话区分驱动安装与设备创建:

DrvInstall = 注册“如何操作”
DevCreate  = 创建“哪个对象”,并把它接入 rootFs 路径树

6.4 链接和其他本地节点

  符号链接的创建同样遵循 I/O 层的路径分派规则。symlink(pcLinkDst, pcSymPath) 根据第二个参数 pcSymPath(新链接的路径)确定负责创建链接的设备,再调用该设备驱动的符号链接操作;第一个参数 pcLinkDst 仅表示链接目标。相关代码位于 system/ioLib/ioSymlink.c:45-130

symlink(pcLinkDst, pcSymPath)
              |
              v
ioFullFileNameGet(pcSymPath, &pdevhdr, ...)
              |
              +-- 检查新链接路径是否已存在
              +-- 解析新链接路径中已有的中间链接
              |
              v
iosSymlink(pdevhdr, pcName, pcLinkDst)
              |
              v
API_IosSymlink() 调用该设备驱动的 fo_symlink
              |
              +-- rootFs 设备 -> __rootFsSymlink()
              +-- ramfs 设备  -> __ramFsSymlink()
              +-- 其他设备    -> 对应 fo_symlink,未实现则报错

  有当 pcSymPath_G_devhdrRoot 管理时,I/O 层才会调用 rootFs 操作表中的 __rootFsSymlink()。该函数随后创建 LNK 节点:

static INT  __rootFsSymlink (PLW_DEV_HDR     pdevhdr,
                             PCHAR           pcName,
                             CPCHAR          pcLinkDst)
{
	......
	API_RootFsMakeNode(pcName,
	                   LW_ROOTFS_NODE_TYPE_LNK,
	                   LW_ROOTFS_NODE_OPT_NONE,
	                   DEFAULT_SYMLINK_PERM,
	                   (PVOID)pcLinkDst);
	......
}

  例如,若 /mnt/ram 是 ramfs 卷,则 symlink("target", "/mnt/ram/link") 会进入 __ramFsSymlink();而 symlink("/media/sdcard1/apps", "/apps") 的新链接路径 /apps 属于 rootFs,因此会进入 __rootFsSymlink()。链接目标可以位于另一个文件系统中,但它不决定本次 symlink() 由哪个驱动执行。

  __rootFsOpen() 在处理带 O_CREAT 的请求时,还可以通过 rootFsMakeDir()rootFsMakeSock()rootFsMakeReg() 创建目录、socket 或普通元数据节点。其中,rootFs 的 REG 节点不提供普通文件内容读写;持久文件应由 FAT、TPSFS、YAFFS2 等具体文件系统管理。

7. 其他文件系统如何接入 rootFs

7.1 procfs:动态信息树

  API_ProcFsDevCreate()fs/procFs/procFs.c:172-190 中调用:

INT  API_ProcFsDevCreate (VOID)
{
	......
	iosDevAddEx(&_G_devhdrProc, "/proc", _G_iProcDrvNum, DT_DIR);
	......
}

  该调用在 rootFs 中创建 /procDEV 节点,节点保存 _G_devhdrProc。之后,各模块通过 API_ProcFsMakeNode() 在 procfs 自己的节点树中创建 kernel/version 等节点。

  读取 proc 文件时,__procFsRead() 不从磁盘获取内容,而是调用节点绑定的读取回调动态生成数据。因此:

rootFs 负责 /proc 入口和设备分派
procfs 负责 /proc 内部节点和内容生成

7.2 ramfs:真正保存文件内容的内存文件系统

  API_RamFsDevCreate() 创建 ramfs 卷、初始化锁和容量信息,随后调用:

INT  API_RamFsDevCreate (PCHAR   pcName, PLW_BLK_DEV  pblkd)
{
	......
	iosDevAddEx(&pramfs->RAMFS_devhdrHdr,
	            pcName,
	            _G_iRamfsDrvNum,
	            DT_DIR);
	......
}

  假设 pcName/mnt/ram,rootFs 中会出现一个指向 ramfs 卷设备头的 DEV 节点。/mnt/ram 以下的目录、普通文件和文件内容均由 ramfs 自己管理,而不是由 rootFs 保存。

7.3 存储设备上的文件系统

  对于 eMMC、SD 或 SATA,应该区分三个阶段:

存储驱动发现介质和分区
          |
          v
mount 根据文件系统类型取得卷创建函数
          |
          v
FAT/TPSFS 等文件系统创建运行时卷
          |
          v
卷通过 API_IosDevAddEx() 以 DEV 节点接入 rootFs

  fs/mount/mount.c 中的 __mount() 会规范化目标路径,根据文件系统名取得创建函数,并为多数磁盘文件系统准备 block-raw 设备,最后调用:

pfuncFsCreate(pcVolName, &pmnDev->MN_blkd);

  如果文件系统类型是 vfat,这里最终进入 API_FatFsDevCreate()。该函数完成 FAT 卷初始化后调用:

INT  API_FatFsDevCreate (PCHAR   pcName, PLW_BLK_DEV  pblkd)
{
	......
	iosDevAddEx(&pfatvol->FATVOL_devhdrHdr,
	            pcName,
	            _G_iFatDrvNum,
	            DT_DIR);
	......
}

  于是目标路径(例如 /media/sdcard0)成为 rootFs 中的 DEV 节点,节点以下的路径交给 FAT 文件系统处理。

  分区由存储和块设备层提供;rootFs 只记录“/media/sdcard0 对应哪个文件系统卷设备”。这也不同于典型 Linux VFS 中“把文件系统覆盖到一个已经存在的目录项”这一具体实现:SylixOS 通过在目标路径注册文件系统设备来完成接入。

8. 路径如何分派到最终设备

8.1 __rootFsFindNode() 逐级查找

  __rootFsFindNode() 位于 fs/rootFs/rootFsLib.c:65-211。它从 _G_rfsrRoot 的第一层子节点开始,将绝对路径按 / 分成多个分量,在当前层的兄弟链表中查找,再进入命中节点的子链表。

函数还会返回:

  • 已匹配节点;
  • 最接近的父节点;
  • 尚未处理的路径尾部;
  • 是否正在处理根目录或最后一个路径分量。

  这些信息既用于创建节点,也用于判断一条路径应该交给哪个设备。

8.2 __rootFsDevMatch() 决定由谁处理

__rootFsDevMatch() 位于 rootFsLib.c:221-275,其核心规则是:

  1. 路径为 / 时,返回根设备 _G_devhdrRoot
  2. 精确命中 DEV 节点时,返回节点保存的设备头。
  3. 没有精确命中,但最接近的父节点是 DEV 时,也返回该父设备,让它处理剩余路径。
  4. 命中 rootFs 本地节点,或只找到本地目录父节点时,返回 _G_devhdrRoot

应用调用 open() 后,整体分派过程为:

open(path)
  |
  v
_IoOpen()
  |
  +-- ioFullFileNameGet()
  |       |
  |       +-- __rootFsDevMatch(path)
  |               |
  |               +-- _G_devhdrRoot
  |               |       -> __rootFsOpen()
  |               |
  |               +-- RFSNV_pdevhdr
  |                       -> 对应设备或文件系统的 open()
  |
  +-- iosOpen()

几个典型结果如下:

路径匹配结果最终 open 回调
/_G_devhdrRoot__rootFsOpen()
/devrootFs 本地 DIR__rootFsOpen()
/dev/nullnull 设备的 DEV 节点null 驱动的 open()
/proc/kernel/version/procDEV 节点__procFsOpen()
/media/sdcard0/a.txtFAT 卷的 DEV 节点FAT 文件系统的 open()

  即使最终没有调用 __rootFsOpen(),rootFs 仍参与路径匹配和设备选择。完整过程是 rootFs 先找到设备头,再由 I/O 层调用该设备的操作表。

8.3 符号链接会触发重新匹配

  假设 rootFs 中存在:

/etc -> /media/sdcard0/etc

  应用调用 open("/etc/config", O_RDONLY) 时:

第一次匹配 /etc/config
  -> /etc 是 LNK 节点
  -> __rootFsDevMatch() 返回 _G_devhdrRoot
  -> __rootFsOpen() 将路径改写为 /media/sdcard0/etc/config
  -> 返回 FOLLOW_LINK_TAIL

_IoOpen() 使用新路径再次匹配
  -> 命中 /media/sdcard0 的 DEV 节点
  -> 调用 FAT 文件系统的 open()

  对应源码位于 fs/rootFs/rootFs.c:344-376system/ioLib/ioInterface.c:178-214。系统不是重新调用一次应用层公开的 open(),而是在同一次 _IoOpen() 中改写路径并循环匹配。

9. rootFsMap:把标准目录映射到文件系统卷

9.1 配置映射关系

  fs/rootFs/rootFsMap.c:41-55 的默认配置以 /media/hdd1 作为多数标准目录的目标前缀:

typedef struct {
    PCHAR   RFSMN_pcDir;
    CHAR    RFSMN_cMapDir[LW_ROOTFS_MAP_LEN];
} LW_ROOTFS_MAP_NODE;

static LW_ROOTFS_MAP_NODE   _G_rfsmapRoot   = { "/" , "/media/hdd1" };
static LW_ROOTFS_MAP_NODE   _G_rfsmapSubp[] = {
    { "/var" , "" },
    { "/usr" , "" },
    { "/tmp" , "" },
    { "/sbin", "" },
    { "/root", "" },
    { "/qt"  , "" },
    { "/lib" , "" },
    { "/home", "" },
    { "/etc" , "" },
    { "/boot", "/media/hdd0" },
    { "/bin" , "" },
    { "/apps", "" }
};

  API_RootFsMapInit() 只解析并保存配置,不创建目录或链接。函数入参 pcMap 为映射关系字符串。例如:

pcMap = rfsmap=/boot:/media/sdcard0,/:/media/sdcard1
INT  API_RootFsMapInit (CPCHAR  pcMap)
{
	......
	pcDir  = cMap;
    pcNext = cMap;
    pcMdir = LW_NULL;
    
    while (*pcNext) {
        if (*pcNext == ',') {
            *pcNext =  PX_EOS;
            pcNext++;
__mapsave:
            if (pcMdir) {
                if (lib_strcmp(pcDir, _G_rfsmapRoot.RFSMN_pcDir) == 0) {
                    lib_strlcpy(_G_rfsmapRoot.RFSMN_cMapDir, pcMdir, LW_ROOTFS_MAP_LEN);
                    
                } else {
                    for (i = 0; i < LW_ROOTFS_ARRAY_SIZE(_G_rfsmapSubp); i++) {
                        if (lib_strcmp(pcDir, _G_rfsmapSubp[i].RFSMN_pcDir) == 0) {
                            lib_strlcpy(_G_rfsmapSubp[i].RFSMN_cMapDir, pcMdir, LW_ROOTFS_MAP_LEN);
                        }
                    }
                }
                pcMdir = LW_NULL;
                pcDir  = pcNext;
            }
            
        } else if (*pcNext == ':') {
            *pcNext =  PX_EOS;
            pcNext++;
            pcMdir = pcNext;
        
        } else {
            pcNext++;
            if (*pcNext == PX_EOS) {
                goto    __mapsave;
            }
        }
    }
	......
}

  解析后,/boot 使用 /media/sdcard0/ 使用 /media/sdcard1’'。

static LW_ROOTFS_MAP_NODE   _G_rfsmapRoot   = { "/" , "/media/sdcard1" };
static LW_ROOTFS_MAP_NODE   _G_rfsmapSubp[] = {
    { "/var" , "" },
    { "/usr" , "" },
    { "/tmp" , "" },
    { "/sbin", "" },
    { "/root", "" },
    { "/qt"  , "" },
    { "/lib" , "" },
    { "/home", "" },
    { "/etc" , "" },
    { "/boot", "/media/sdcard0" },
    { "/bin" , "" },
    { "/apps", "" }
};

9.2 API_RootFsMap() 创建实际链接

  API_RootFsMap() 才会检查目标目录并调用 symlink()

INT  API_RootFsMap (ULONG  ulFlags)
{
	......
    if (lib_strcmp(_G_rfsmapRoot.RFSMN_cMapDir, "/dev/ram") == 0) {
        lib_bzero(&blkdevRam, sizeof(LW_BLK_DEV));
        blkdevRam.BLKD_pcName = "0";
        ramFsDevCreate("/dev/ram", &blkdevRam);
    }
    
    for (i = 0; i < LW_ROOTFS_ARRAY_SIZE(_G_rfsmapSubp); i++) {
        if (_G_rfsmapSubp[i].RFSMN_cMapDir[0]) {
            if (access(_G_rfsmapSubp[i].RFSMN_cMapDir, R_OK) < 0) {
                mkdir(_G_rfsmapSubp[i].RFSMN_cMapDir, DEFAULT_DIR_PERM);
            }
            symlink(_G_rfsmapSubp[i].RFSMN_cMapDir, _G_rfsmapSubp[i].RFSMN_pcDir);
        
        } else {
        	/* 这里注意,对于 _G_rfsmapSubp 中暂未映射的,默认都映射到 _G_rfsmapRoot.RFSMN_cMapDir 目录下 */
            snprintf(cMap, MAX_FILENAME_LENGTH, "%s%s", 
                     _G_rfsmapRoot.RFSMN_cMapDir, _G_rfsmapSubp[i].RFSMN_pcDir);
            if (access(cMap, R_OK) < 0) {
                mkdir(cMap, DEFAULT_DIR_PERM);
            }
            /* 这里会去创建名为 _G_rfsmapSubp[i].RFSMN_pcDir 的 rootfs node 节点 */
            symlink(cMap, _G_rfsmapSubp[i].RFSMN_pcDir);
        }
    }
	......
}

  symlink 的第二个参数是 /apps/etc 等 rootFs 路径,因此 I/O 层匹配到 _G_devhdrRoot,最终由 __rootFsSymlink() 创建 rootFs LNK 节点;第一个参数 cMap 只是链接目标,不参与本次驱动选择。

  按照上面的配置,结果类似:

/boot -> /media/sdcard0
/apps -> /media/sdcard1/apps
/etc  -> /media/sdcard1/etc
/lib  -> /media/sdcard1/lib

  因此,rootFsMap 不是把整个 rootFs 换成磁盘文件系统,也不是执行 Linux 意义上的根挂载切换。SylixOS 的根设备和内存节点树仍然存在;/apps/etcLNK 节点只是把访问引导到已经接入的持久化文件系统卷。

  如果根映射目标配置为 /dev/ramAPI_RootFsMap() 会先创建 ramfs 卷,再建立标准目录链接。这再次说明:映射目标既可以是持久存储文件系统,也可以是运行时内存文件系统。

10. 两个完整的访问案例

10.1 打开 /proc/kernel/version

open("/proc/kernel/version", O_RDONLY)
  |
  +-- _IoOpen() 请求匹配设备
  |
  +-- rootFs 节点树命中 /proc 的 DEV 节点
  |
  +-- 取得 _G_devhdrProc
  |
  +-- 调用 __procFsOpen()
  |
  +-- procfs 在自己的节点树中查找 kernel/version
  |
  +-- read() 进入 __procFsRead()
        |
        +-- 调用节点绑定的 PFSNO_pfuncRead 回调
        +-- 动态生成文本并返回

  这个过程没有读取磁盘上的普通文件。rootFs 负责找到 /proc 入口并完成设备分派,procfs 负责内部节点和内容生成。

10.2 打开映射后的 /apps/a.txt

假设:

/media/sdcard1             已接入 FAT 或 TPSFS 文件系统卷
/apps -> /media/sdcard1/apps

应用只需要调用一次:

open("/apps/a.txt", O_RDONLY);

内部处理过程为:

第一次路径匹配
  /apps/a.txt
      |
      +-- rootFs 找到 /apps 的 LNK 节点
      +-- __rootFsOpen() 改写为 /media/sdcard1/apps/a.txt
      +-- 返回 FOLLOW_LINK_TAIL

第二次路径匹配
  /media/sdcard1/apps/a.txt
      |
      +-- rootFs 找到 /media/sdcard1 的 DEV 节点
      +-- I/O 层取得 FAT/TPSFS 卷设备头
      +-- 文件系统自己的 open() 查找 apps/a.txt

后续 read()
      |
      +-- 由 FAT/TPSFS 文件系统读取实际存储数据

这个案例把三种职责连接起来:

层次职责
rootFs 的 LNK 节点将稳定的逻辑路径 /apps 转换为实际存储路径。
rootFs 的 DEV 节点/media/sdcard1 关联到文件系统卷设备头。
FAT/TPSFS 文件系统查找 apps/a.txt 并读写实际数据。

应用不需要知道 /apps 最终位于 SD、eMMC 还是 ramfs,只要系统启动时建立正确的卷接入和映射关系即可。

11. 总结

理解 SylixOS rootFs,可以抓住以下几点:

  1. rootfs 是运行时提供根目录 / 的角色;SylixOS rootFs 是这一角色的具体实现。
  2. SylixOS rootFs 是一棵内存节点树,主要保存目录、设备入口、链接和少量元数据,不负责普通文件内容读写。
  3. API_RootFsMakeNode() 是节点创建的底层入口;API_IosDevAddEx() 同时登记设备头并创建 rootFs DEV 节点。
  4. procfs、ramfs 和磁盘文件系统各自管理内部节点与数据,只通过 DEV 节点接入以 / 为起点的统一目录树。
  5. __rootFsDevMatch() 根据路径选择根设备或具体设备;LNK 节点由 __rootFsOpen() 改写路径后触发重新匹配。
  6. rootFsMap 批量创建符号链接,将 /apps/etc 等逻辑目录映射到实际文件系统卷,但不会替换 rootFs 本身。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐