3. 路径名解析:路径空间前缀树、匹配规则、挂载点管理

好,咱们进入资源管理器最核心的机制之一——路径名解析。说实话,这部分内容我当年啃了很久才真正吃透。你想想看,一个简单的 open("/dev/ser1", ...),背后到底发生了什么?系统怎么知道这个路径对应哪个驱动?这就是路径名解析要回答的问题。

3.1 路径空间前缀树

QNX 的路径空间,本质上是一棵前缀树(Trie)。我习惯叫它“路径树”。每个节点代表路径中的一个分量,根节点就是 /

举个例子,假设系统里有这些路径:

/dev/ser1
/dev/ser2
/dev/i2c0
/net/nfs_mount

它们在内存中的结构是这样的:

/
├── dev/
│   ├── ser1
│   ├── ser2
│   └── i2c0
└── net/
    └── nfs_mount

每个叶子节点(比如 ser1)都绑定了一个 resmgr_attach() 时返回的 iofunc_attr_t 结构体。这个结构体里存着挂载点的关键信息,包括函数分发表。

关键点:前缀树不是二叉树,每个节点可以有任意多个子节点。查找时按路径分量逐级匹配,复杂度是 O(path_length)。

我在项目中遇到过一个问题:有人把路径挂到了 /dev/ser1,然后又挂了一个 /dev/ser1/sub。结果访问 /dev/ser1 时,系统返回了 sub 节点的数据。为什么?因为匹配规则不是“精确匹配”,而是“最长前缀匹配”。

3.2 匹配规则

路径名解析的匹配规则,说白了就两条:

  1. 最长前缀匹配:从根开始,逐层匹配路径分量,取匹配最长的那个节点。
  2. 精确匹配优先:如果有完全匹配的节点,优先使用它。

嗯,这里要注意:resmgr_attach() 时有一个 flag 参数 _RESMGR_FLAG_DIR。如果你挂的是目录,系统会允许子路径继续匹配。否则,只匹配精确路径。

我曾经踩过一个坑:挂载 /dev/mychar 时忘了加 _RESMGR_FLAG_DIR,结果别人用 open("/dev/mychar/extra", ...) 时,系统直接返回了 ENOTDIR。排查了半天才发现是 flag 没设对。

避坑指南:如果你希望路径支持子路径(比如 /dev/mychar/0/dev/mychar/1),一定要在 resmgr_attach() 时设置 _RESMGR_FLAG_DIR。否则系统会把它当成普通文件节点处理。

匹配过程大致如下:

// 伪代码,展示匹配逻辑
path_match(const char *path) {
    node = root;
    best_match = NULL;
    
    for each component in path {
        if (node has child matching component) {
            node = child;
            if (node is mounted) {
                best_match = node;
            }
        } else {
            break;
        }
    }
    
    // 如果 best_match 是目录,允许继续向下匹配
    if (best_match && best_match->flags & _RESMGR_FLAG_DIR) {
        return best_match;
    }
    
    // 否则只返回精确匹配
    return (node == path_end) ? node : NULL;
}

你想想看,这个设计有什么好处?它允许一个资源管理器“接管”整个子树。比如 /dev 可以是一个虚拟目录,下面所有设备都由一个管理器统一处理。这在嵌入式系统中特别有用。

3.3 挂载点管理

挂载点,就是路径树上的“锚点”。每个挂载点对应一个 resmgr_attach() 调用。系统内部维护了一个挂载点链表,但查找时用的是前缀树,不是链表。

挂载点的数据结构大致如下:

struct mount_point {
    char *path;                 // 挂载路径,如 "/dev/ser1"
    resmgr_attr_t attr;         // 属性,包括 flag、权限等
    io_funcs_t *funcs;          // 函数分发表
    iofunc_attr_t *io_attr;     // IO 属性
    mount_point_t *next;        // 链表指针
};

管理挂载点有几个关键操作:

操作 函数 说明
挂载 resmgr_attach() 在路径树上创建节点,绑定函数表
卸载 resmgr_detach() 从路径树上移除节点,释放资源
查找 内部路径解析 按最长前缀匹配找到对应挂载点
枚举 resmgr_find() 遍历所有挂载点,用于调试

小技巧:调试路径问题时,可以用 mount 命令查看当前系统的挂载点列表。或者用 ls -la /proc/mounts 查看内核视角的挂载信息。我经常用这两个命令来确认路径是否挂载成功。

挂载点的生命周期是这样的:

  1. 资源管理器启动,调用 resmgr_attach() 注册路径。
  2. 系统在路径树上创建节点,分配 mount_point_t 结构体。
  3. 客户端发起 open() 等 IO 操作。
  4. 路径解析引擎按前缀树匹配,找到对应的挂载点。
  5. 调用挂载点绑定的函数分发表中的对应函数。
  6. 资源管理器退出时,调用 resmgr_detach() 清理。

我记得有一次,一个同事在热插拔设备时没有正确卸载挂载点,导致路径树里留下了“幽灵节点”。新插入的设备挂载到同一个路径时,系统报错说路径已存在。排查了半天,才发现是旧挂载点没清理干净。

注意事项:挂载点是有引用计数的。如果有客户端正在使用某个挂载点(比如文件还没关闭),resmgr_detach() 不会立即销毁它,而是标记为“待删除”。等所有引用都释放后,才会真正清理。这个机制避免了经典的 use-after-free 问题。

最后,聊一下挂载点的权限管理。每个挂载点可以设置自己的权限掩码,比如只允许特定用户访问。这个权限检查发生在路径解析阶段,而不是在具体的 IO 函数里。也就是说,如果路径解析就失败了,根本不会调用你的 read()write() 函数。

嗯,路径名解析这部分内容,说白了就是 QNX 资源管理器的“交通枢纽”。理解了它,你就理解了资源管理器的一半。下一章我们会深入函数分发表,看看那些 io_openio_read 到底是怎么被调用的。