Skip to content

58_VFS篇

kuangyufei edited this page Sep 5, 2022 · 1 revision

本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

文件系统相关篇为:

基本概念 | 官方定义

VFS(Virtual File System)是文件系统的虚拟层,它不是一个实际的文件系统,而是一个异构文件系统之上的软件粘合层,为用户提供统一的类Unix文件操作接口。由于不同类型的文件系统接口不统一,若系统中有多个文件系统类型,访问不同的文件系统就需要使用不同的非标准接口。而通过在系统中添加VFS层,提供统一的抽象接口,屏蔽了底层异构类型的文件系统的差异,使得访问文件系统的系统调用不用关心底层的存储介质和文件系统类型,提高开发效率。

OpenHarmony内核中,VFS框架是通过在内存中的树结构来实现的,树的每个结点都是一个Vnode结构体,父子结点的关系以PathCache结构体保存。VFS最主要的两个功能是:

  • 查找节点。
  • 统一调用(标准)。

VFS层具体实现包括四个方面:

  • 通过三大函数指针操作接口,实现对不同文件系统类型调用不同接口实现标准接口功能;
  • 通过VnodePathCache机制,提升路径搜索以及文件访问的性能;
  • 通过挂载点管理进行分区管理;
  • 通过FD管理进行进程间FD隔离等。

三大操作接口

VFS层通过函数指针的形式,将统一调用按照不同的文件系统类型,分发到不同文件系统中进行底层操作。各文件系统的各自实现一套Vnode操作(VnodeOps)、挂载点操作(MountOps)以及文件操作接口(file_operations_vfs),并以函数指针结构体的形式存储于对应Vnode、挂载点、File结构体中,实现VFS层对下访问。这三个接口分别为:

VnodeOps | 操作 Vnode 节点

struct VnodeOps {
    int (*Create)(struct Vnode *parent, const char *name, int mode, struct Vnode **vnode);//创建节点
    int (*Lookup)(struct Vnode *parent, const char *name, int len, struct Vnode **vnode);//查询节点
    //Lookup向底层文件系统查找获取inode信息
    int (*Open)(struct Vnode *vnode, int fd, int mode, int flags);//打开节点
    int (*Close)(struct Vnode *vnode);//关闭节点
    int (*Reclaim)(struct Vnode *vnode);//回收节点
    int (*Unlink)(struct Vnode *parent, struct Vnode *vnode, const char *fileName);//取消硬链接
    int (*Rmdir)(struct Vnode *parent, struct Vnode *vnode, const char *dirName);//删除目录节点
    int (*Mkdir)(struct Vnode *parent, const char *dirName, mode_t mode, struct Vnode **vnode);//创建目录节点
    /*
    创建一个目录时,实际做了3件事:在其“父目录文件”中增加一个条目;分配一个inode;再分配一个存储块,
    用来保存当前被创建目录包含的文件与子目录。被创建的“目录文件”中自动生成两个子目录的条目,名称分别是:“.”和“..”。
    前者与该目录具有相同的inode号码,因此是该目录的一个“硬链接”。后者的inode号码就是该目录的父目录的inode号码。
    所以,任何一个目录的"硬链接"总数,总是等于它的子目录总数(含隐藏目录)加2。即每个“子目录文件”中的“..”条目,
    加上它自身的“目录文件”中的“.”条目,再加上“父目录文件”中的对应该目录的条目。
    */
    int (*Readdir)(struct Vnode *vnode, struct fs_dirent_s *dir);//读目录节点
    int (*Opendir)(struct Vnode *vnode, struct fs_dirent_s *dir);//打开目录节点
    int (*Rewinddir)(struct Vnode *vnode, struct fs_dirent_s *dir);//定位目录节点
    int (*Closedir)(struct Vnode *vnode, struct fs_dirent_s *dir);//关闭目录节点
    int (*Getattr)(struct Vnode *vnode, struct stat *st);//获取节点属性
    int (*Setattr)(struct Vnode *vnode, struct stat *st);//设置节点属性
    int (*Chattr)(struct Vnode *vnode, struct IATTR *attr);//改变节点属性(change attr)
    int (*Rename)(struct Vnode *src, struct Vnode *dstParent, const char *srcName, const char *dstName);//重命名
    int (*Truncate)(struct Vnode *vnode, off_t len);//缩减或扩展大小
    int (*Truncate64)(struct Vnode *vnode, off64_t len);//缩减或扩展大小
    int (*Fscheck)(struct Vnode *vnode, struct fs_dirent_s *dir);//检查功能
    int (*Link)(struct Vnode *src, struct Vnode *dstParent, struct Vnode **dst, const char *dstName);
    int (*Symlink)(struct Vnode *parentVnode, struct Vnode **newVnode, const char *path, const char *target);
    ssize_t (*Readlink)(struct Vnode *vnode, char *buffer, size_t bufLen);
};

MountOps | 挂载点操作

//挂载操作
struct MountOps {
    int (*Mount)(struct Mount *mount, struct Vnode *vnode, const void *data);//挂载
    int (*Unmount)(struct Mount *mount, struct Vnode **blkdriver);//卸载
    int (*Statfs)(struct Mount *mount, struct statfs *sbp);//统计文件系统的信息,如该文件系统类型、总大小、可用大小等信息
};

file_operations_vfs | 文件操作接口

struct file_operations_vfs 
{
  int     (*open)(struct file *filep);	//打开文件
  int     (*close)(struct file *filep);	//关闭文件
  ssize_t (*read)(struct file *filep, char *buffer, size_t buflen);	//读文件
  ssize_t (*write)(struct file *filep, const char *buffer, size_t buflen);//写文件
  off_t   (*seek)(struct file *filep, off_t offset, int whence);//寻找,检索 文件
  int     (*ioctl)(struct file *filep, int cmd, unsigned long arg);//对文件的控制命令
  int     (*mmap)(struct file* filep, struct VmMapRegion *region);//内存映射实现<文件/设备 - 线性区的映射>
  /* The two structures need not be common after this point */

#ifndef CONFIG_DISABLE_POLL
  int     (*poll)(struct file *filep, poll_table *fds);	//轮询接口
#endif
  int     (*stat)(struct file *filep, struct stat* st);	//统计接口
  int     (*fallocate)(struct file* filep, int mode, off_t offset, off_t len);
  int     (*fallocate64)(struct file *filep, int mode, off64_t offset, off64_t len);
  int     (*fsync)(struct file *filep);
  ssize_t (*readpage)(struct file *filep, char *buffer, size_t buflen);
  int     (*unlink)(struct Vnode *vnode);
};

PathCache | 路径缓存

PathCache是路径缓存,它通过哈希表存储,利用父节点Vnode的地址和子节点的文件名,可以从PathCache中快速查找到子节点对应的Vnode。当前PageCache仅支持缓存二进制文件,在初次访问文件时通过mmap映射到内存中,下次再访问时,直接从PageCache中读取,可以提升对同一个文件的读写速度。另外基于PageCache可实现以文件为基底的进程间通信。下图展示了文件/目录的查找流程。

LIST_HEAD g_pathCacheHashEntrys[LOSCFG_MAX_PATH_CACHE_SIZE];	//路径缓存哈希表项
struct PathCache {//路径缓存
    struct Vnode *parentVnode;    /* vnode points to the cache */	
    struct Vnode *childVnode;     /* vnode the cache points to */
    LIST_ENTRY parentEntry;       /* list entry for cache list in the parent vnode */
    LIST_ENTRY childEntry;        /* list entry for cache list in the child vnode */
    LIST_ENTRY hashEntry;         /* list entry for buckets in the hash table */
    uint8_t nameLen;              /* length of path component */
#ifdef LOSCFG_DEBUG_VERSION
    int hit;                      /* cache hit count*/
#endif
    char name[0];                 /* path component name */
};
//路径缓存初始化
int PathCacheInit(void)
{
    for (int i = 0; i < LOSCFG_MAX_PATH_CACHE_SIZE; i++) {
        LOS_ListInit(&g_pathCacheHashEntrys[i]);
    }
    return LOS_OK;
}

挂载点管理

当前OpenHarmony内核中,对系统中所有挂载点通过链表进行统一管理。挂载点结构体中,记录了该挂载分区内的所有Vnode。当分区卸载时,会释放分区内的所有Vnode。

static LIST_HEAD *g_mountList = NULL;//挂载链表,上面挂的是系统所有挂载点
struct Mount {
    LIST_ENTRY mountList;              /* mount list */			 //通过本节点将Mount挂到全局Mount链表上
    const struct MountOps *ops;        /* operations of mount */ //挂载操作函数	
    struct Vnode *vnodeBeCovered;      /* vnode we mounted on */ //要被挂载的节点 即 /bin1/vs/sd 对应的 vnode节点
    struct Vnode *vnodeCovered;        /* syncer vnode */		 //要挂载的节点	即/dev/mmcblk0p0 对应的 vnode节点
    struct Vnode *vnodeDev;            /* dev vnode */
    LIST_HEAD vnodeList;               /* list of vnodes */		//链表表头
    int vnodeSize;                     /* size of vnode list */	//节点数量
    LIST_HEAD activeVnodeList;         /* list of active vnodes */	//激活的节点链表
    int activeVnodeSize;               /* szie of active vnodes list *///激活的节点数量
    void *data;                        /* private data */	//私有数据,可使用这个成员作为一个指向它们自己内部数据的指针
    uint32_t hashseed;                 /* Random seed for vfs hash */ //vfs 哈希随机种子
    unsigned long mountFlags;          /* Flags for mount */	//挂载标签
    char pathName[PATH_MAX];           /* path name of mount point */	//挂载点路径名称  /bin1/vs/sd
    char devName[PATH_MAX];            /* path name of dev point */		//设备名称 /dev/mmcblk0p0
};
//分配一个挂载点
struct Mount* MountAlloc(struct Vnode* vnodeBeCovered, struct MountOps* fsop)
{
    struct Mount* mnt = (struct Mount*)zalloc(sizeof(struct Mount));//申请一个mount结构体内存,小内存分配用 zalloc
    if (mnt == NULL) {
        PRINT_ERR("MountAlloc failed no memory!\n");
        return NULL;
    }

    LOS_ListInit(&mnt->activeVnodeList);//初始化激活索引节点链表
    LOS_ListInit(&mnt->vnodeList);//初始化索引节点链表

    mnt->vnodeBeCovered = vnodeBeCovered;//设备将装载到vnodeBeCovered节点上
    vnodeBeCovered->newMount = mnt;//该节点不再是虚拟节点,而作为 设备结点
#ifdef LOSCFG_DRIVERS_RANDOM	//随机值	驱动模块
    HiRandomHwInit();//随机值初始化
    (VOID)HiRandomHwGetInteger(&mnt->hashseed);//用于生成哈希种子
    HiRandomHwDeinit();//随机值反初始化
#else
    mnt->hashseed = (uint32_t)random(); //随机生成哈子种子
#endif
    return mnt;
}

fd管理 | 两种描述符/句柄的关系

Fd(File Descriptor)是描述一个打开的文件/目录的描述符。当前OpenHarmony内核中,fd总规格为896,分为三种类型:

  • 普通文件描述符,系统总数量为512。

    #define CONFIG_NFILE_DESCRIPTORS    512	// 系统文件描述符数量
    
  • Socket描述符,系统总规格为128。

    #define LWIP_CONFIG_NUM_SOCKETS         128	//socket链接数量
    #define CONFIG_NSOCKET_DESCRIPTORS  LWIP_CONFIG_NUM_SOCKETS 
    
  • 消息队列描述符,系统总规格为256。

    #define CONFIG_NQUEUE_DESCRIPTORS    256
    

请记住,在OpenHarmony内核中,在不同的层面会有两种文件句柄::

  • 系统文件描述符(sysfd),由内核统一管理,和进程描述符形成映射关系,一个sysfd可以被多个profd映射,也就是说打开一个文件只会占用一个sysfd,但可以占用多个profd,即一个文件被多个进程打开。

  • 进程文件描述符(profd),由进程管理的叫进程文件描述符,内核对不同进程中的fd进行隔离,即进程只能访问本进程的fd。举例说明之间的关系:

    文件            sysfd     profd
    吃个桃桃.mp4        10    13(A进程)
    吃个桃桃.mp4        10    3(B进程)
    容嬷嬷被冤枉.txt    12    3(A进程)
    容嬷嬷被冤枉.txt    12    3(C进程)
    
  • 不同进程的相同fd往往指向不同的文件,但有三个fd例外

    • STDIN_FILENO(fd = 0) 标准输入 接收键盘的输入
    • STDOUT_FILENO(fd = 1) 标准输出 向屏幕输出
    • STDERR_FILENO(fd = 2) 标准错误 向屏幕输出 sysfd和所有的profd的(0,1,2)号都是它们。熟知的 printf 就是向 STDOUT_FILENO中写入数据。
  • 具体涉及结构体

    struct file_table_s {//进程fd <--> 系统FD绑定
        intptr_t sysFd; /* system fd associate with the tg_filelist index */
    };//sysFd的默认值是-1
    struct fd_table_s {//进程fd表结构体
        unsigned int max_fds;//进程的文件描述符最多有256个
        struct file_table_s *ft_fds; /* process fd array associate with system fd *///系统分配给进程的FD数组 ,fd 默认是 -1
        fd_set *proc_fds;	//进程fd管理位,用bitmap管理FD使用情况,默认打开了 0,1,2	       (stdin,stdout,stderr)
        fd_set *cloexec_fds;
        sem_t ft_sem; /* manage access to the file table */ //管理对文件表的访问的信号量
    };
    struct files_struct {//进程文件表结构体
        int count;				      //持有的文件数量
        struct fd_table_s *fdt; //持有的文件表
        unsigned int file_lock;	//文件互斥锁
        unsigned int next_fd;	  //下一个fd
    #ifdef VFS_USING_WORKDIR
        spinlock_t workdir_lock;	//工作区目录自旋锁
        char workdir[PATH_MAX];		//工作区路径,最大 256个字符
    #endif
    };
    typedef struct ProcessCB {
    #ifdef LOSCFG_FS_VFS
        struct files_struct *files;        /**< Files held by the process */ //进程所持有的所有文件,注者称之为进程的文件管理器
    #endif	//每个进程都有属于自己的文件管理器,记录对文件的操作。 注意:一个文件可以被多个进程操作
    }
    

    解读

    • 鸿蒙的每个进程ProcessCB都有属于自己的进程的文件描述符files_struct,该进程和文件系统有关的信息都由它表达。

    • 搞清楚 files_structfd_table_sfile_table_s三个结构体的关系就明白了进度描述符和系统描述符的关系。

    • fd_table_s是由alloc_fd_table分配的一个结构体数组,用于存放进程的文件描述符

      //分配进程文件表,初始化 fd_table_s 结构体中每个数据,包括系统FD(0,1,2)的绑定
      static struct fd_table_s * alloc_fd_table(unsigned int numbers)
      {
        struct fd_table_s *fdt;
        void *data;
        fdt = LOS_MemAlloc(m_aucSysMem0, sizeof(struct fd_table_s));//申请内存
        if (!fdt)
          {
            goto out;
          }
        fdt->max_fds = numbers;//最大数量
        if (!numbers)
          {
            fdt->ft_fds = NULL;
            fdt->proc_fds = NULL;
            return fdt;
          }
        data = LOS_MemAlloc(m_aucSysMem0, numbers * sizeof(struct file_table_s));//这是和系统描述符的绑定
        if (!data)
          {
            goto out_fdt;
          }
        fdt->ft_fds = data;//这其实是个 int[] 数组,
        for (int i = STDERR_FILENO + 1; i < numbers; i++)
          {
              fdt->ft_fds[i].sysFd = -1;//默认的系统描述符都为-1,即还没有和任何系统文件描述符绑定
          }
        data = LOS_MemAlloc(m_aucSysMem0, sizeof(fd_set));//管理FD的 bitmap 
        if (!data)
          {
            goto out_arr;
          }
        (VOID)memset_s(data, sizeof(fd_set), 0, sizeof(fd_set));
        fdt->proc_fds = data;
        alloc_std_fd(fdt);//分配标准的0,1,2系统文件描述符,这样做的结果是任务进程都可以写系统文件(0,1,2)
        (void)sem_init(&fdt->ft_sem, 0, 1);//互斥量初始化
        return fdt;
      out_arr:
        (VOID)LOS_MemFree(m_aucSysMem0, fdt->ft_fds);
      out_fdt:
        (VOID)LOS_MemFree(m_aucSysMem0, fdt);
      out:
        return NULL;
      }
      
    • file_table_s记录 sysfdprofd的绑定关系.fdt->ft_fds[i].sysFd中的i就是profd

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断
编译运行 调测工具
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。:)

Clone this wiki locally