开源鸿蒙内核源码分析系列 | 索引节点 | 谁是文件系统最重要的概念(转载)

开源鸿蒙内核源码分析系列 | 索引节点 | 谁是文件系统最重要的概念(转载)

读懂开源鸿蒙内核的关键线索是LOS_DL_LIST(双向链表)。它是系列篇开篇双向链表 | 谁是内核最重要结构体的内容。而读懂文件系统的关键线索是vnode(索引节点),vnode在文件系统中起承上启下的关键点。vnode是 BSD的叫法。开源鸿蒙沿用了BSD的称呼。Linux的叫法是inode,关于vnode有翻译成虚拟节点,但系列篇还是统一翻译成索引节点。

什么是vnode?

先看大佬们对其的定义。

  • OpenBSD 定义
    • A vnode is an object in kernel memory that speaks the UNIX file interface (open, read, write, close, readdir, etc.). Vnodes can represent files, directories, FIFOs, domain sockets, block devices, character devices.
    • vnode 是内核内存中的一个对象,它使用 UNIX 文件接口(打开、读取、写入、关闭、readdir 等)。Vnodes 可以代表文件、目录、管道、套接字、块设备、字符设备。
  • freeBSD 定义
    • vnode — internal representation of a file or directory . The vnode is the focus of all file activity in UNIX. A vnode is described by struct vnode. There is a unique vnode allocated for each active file, each current directory, each mounted-on file, text file, and the root.
    • vnode — 文件或目录的内部表示. vnode 是 UNIX 中所有文件活动的焦点。vnode 由 struct vnode 描述。为每个活动文件、每个当前目录、每个挂载文件、文本文件和根分配了一个唯一的 vnode。
  • Linux 定义
    • The inode (index node) is a data structure in a Unix-style file system that describes a file-system object such as a file or a directory. Each inode stores the attributes and disk block locations of the object’s data.[1] File-system object attributes may include metadata (times of last change, access, modification), as well as owner and permission data.
    • inode(索引节点)是 Unix 风格的文件系统中的一种数据结构,用于描述文件系统对象,例如文件或目录。每个 inode 存储对象数据的属性和磁盘块位置。文件系统对象属性可能包括元数据(上次更改、访问、修改的时间),以及所有者和权限数据。

综合所述,发现木有,这说的可不就是文件故事 | 用图书管理说文件系统中的索引页吗? 没读过的建议先阅读后再继续。对于在硬盘中的vnode,在系统启动后vnode会被加载到内存统一管理。

Librarian  painting by Georg Reimer

vnode 长啥样?

本篇主要围绕 vnode结构体来说,说透说烂这个文件系统最关键的节点。


struct IATTR { //此结构用于记录 vnode 的属性
  /* This structure is used for record vnode attr. */
  unsigned int attr_chg_valid;//节点改变有效性 (CHG_MODE | CHG_UID | ... )
  unsigned int attr_chg_flags;//额外的系统与用户标志(flag),用来保护该文件
  unsigned attr_chg_mode;  //确定了文件的类型,以及它的所有者、它的group、其它用户访问此文件的权限 (S_IWUSR | ...)
  unsigned attr_chg_uid;  //用户ID
  unsigned attr_chg_gid;  //组ID
  unsigned attr_chg_size;  //节点大小
  unsigned attr_chg_atime;  //节点最近访问时间
  unsigned attr_chg_mtime;  //节点对应的文件内容被修改时间
  unsigned attr_chg_ctime;  //节点自身被修改时间
};
// 对IATTR的修改最终将落到 vnode->vop->Chattr(vnode, attr);
enum VnodeType {//节点类型
    VNODE_TYPE_UNKNOWN,       /* unknown type */  //未知类型
    VNODE_TYPE_REG,           /* regular file */  //vnode代表一个正则文件(普通文件)
    VNODE_TYPE_DIR,           /* directory */      //vnode代表一个目录
    VNODE_TYPE_BLK,           /* block device */  //vnode代表一个块设备
    VNODE_TYPE_CHR,           /* char device */    //vnode代表一个字符设备
    VNODE_TYPE_BCHR,          /* block char mix device *///块和字符设备混合
    VNODE_TYPE_FIFO,          /* pipe */      //vnode代表一个管道
    VNODE_TYPE_LNK,           /* link */      //vnode代表一个符号链接
};
struct Vnode {//vnode并不包含文件名,因为 vnode和文件名是 1:N 的关系
    enum VnodeType type;                /* vnode type */  //节点类型 (文件|目录|链接...)
    int useCount;                       /* ref count of users *///节点引用(链接)数,即有多少文件名指向这个vnode,即上层理解的硬链接数   
    uint32_t hash;                      /* vnode hash */  //节点哈希值
    uint uid;                           /* uid for dac */  //文件拥有者的User ID
    uint gid;                           /* gid for dac */  //文件的Group ID
    mode_t mode;                        /* mode for dac */  //chmod 文件的读、写、执行权限
    LIST_HEAD parentPathCaches;         /* pathCaches point to parents */  //指向父级路径缓存,上面的都是当了爸爸节点
    LIST_HEAD childPathCaches;          /* pathCaches point to children */  //指向子级路径缓存,上面都是当了别人儿子的节点
    struct Vnode *parent;               /* parent vnode */  //父节点
    struct VnodeOps *vop;               /* vnode operations */  //相当于指定操作Vnode方式 (接口实现|驱动程序)
    struct file_operations_vfs *fop;    /* file operations */  //相当于指定文件系统
    void *data;                         /* private data */    //文件数据block的位置,指向每种具体设备私有的成员,例如 ( drv_data | nfsnode | ....)
    uint32_t flag;                      /* vnode flag */    //节点标签
    LIST_ENTRY hashEntry;               /* list entry for bucket in hash table */ //通过它挂入哈希表 g_vnodeHashEntrys[i], i:[0,g_vnodeHashMask]
    LIST_ENTRY actFreeEntry;            /* vnode active/free list entry */  //通过本节点挂到空闲链表和使用链表上
    struct Mount *originMount;          /* fs info about this vnode */ //自己所在的文件系统挂载信息
    struct Mount *newMount;             /* fs info about who mount on this vnode */  //其他挂载在这个节点上文件系统信息
};

解读:

  • VnodeType即七种文件类型。开源鸿蒙增加了一种 VNODE_TYPE_BCHR,去掉了 socket类型,没搞懂为什么。
  • useCount代表硬链接数。任何目录下都会有 .,..两个文件。前者指向当前目录,后者指向父目录。这样做的好处是由索引页指向的数据块中(目录项)存有父目录和当前目录的索引号。有了索引号就能很快的找到对应的索引页。例如当外部使用 cd ../../../这样的命令时,只需在当前目录(inode)所指向的目录项中查找..的索引号。这样是非常的快捷和方便的。用自己勤劳的双手就能解决的困扰,何必去麻烦别人呢?因为被下级留有记录,所以硬链接数会增加。会增加多少呢?举例说明:stat命令用于查看索引节点信息。

turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/kernel/liteos_a$ stat kernel
File: kernel
Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 805h/2053d  Inode: 1099218     Links: 7
Access: (0755/drwxr-xr-x)  Uid: ( 1000/  turing)   Gid: ( 1000/  turing)

注意Inode: 1099218,而Links: 7代表kernel被七个地方所关联,除了自己应该还有六个,在哪呢?用 ll -a命令展开kernel看看:


turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/kernel/liteos_a/kernel$ ll -a
total 36
drwxr-xr-x  7 turing turing 4096 Jun 21 02:38 ./
drwxr-xr-x 21 turing turing 4096 Jul 23 19:45 ../
drwxr-xr-x 11 turing turing 4096 Jun 21 02:38 base/
-rwxr-xr-x  1 turing turing 2214 Jun 21 02:38 BUILD.gn*
drwxr-xr-x  3 turing turing 4096 Jun 21 02:38 common/
drwxr-xr-x  9 turing turing 4096 Jun 21 02:38 extended/
drwxr-xr-x  2 turing turing 4096 Jun 21 02:38 include/
-rwxr-xr-x  1 turing turing 2864 Jun 21 02:38 Kconfig*
drwxr-xr-x  4 turing turing 4096 Jun 21 02:38 user/

发现包括.,..在内有七个目录,d代表的是目录,但是注意其中的 ../并不指向kernel,而是指向它的父级liteos_a。其余的 ./,base/..,common/..六个刚好指向kernel,可以验证下它们的inode信息就知道了。


turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/kernel/liteos_a$ stat ./kernel/.
File: ./kernel/.
Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 805h/2053d  Inode: 1099218     Links: 7
Access: (0755/drwxr-xr-x)  Uid: ( 1000/  turing)   Gid: ( 1000/  turing)
turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/kernel/liteos_a$ stat ./kernel/base/..
  File: ./kernel/base/..
  Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 805h/2053d  Inode: 1099218     Links: 7
Access: (0755/drwxr-xr-x)  Uid: ( 1000/  turing)   Gid: ( 1000/  turing)
turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/kernel/liteos_a$ stat ./kernel/..
File: ./kernel/..
Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 805h/2053d  Inode: 1099213     Links: 21
Access: (0755/drwxr-xr-x)  Uid: ( 1000/  turing)   Gid: ( 1000/  turing)

会发现./kernel/.和./kernel/base/..的 Inode都是1099218,而./kernel/..的为1099213是不一样的。

  • 正常情况下一个目录的.,..是不一样的,但只有一个目录例外,就是 /

turing@ubuntu:/$ stat /.
File: /.
Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 805h/2053d  Inode: 2           Links: 20
turing@ubuntu:/$ stat /..
File: /..
Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 805h/2053d  Inode: 2           Links: 20
  • 为什么/的inode的编号一定是2 ?inode 为 0 和 1的节点又去哪了呢?
  • inode编号真的是唯一的吗?不同的文件系统可以有相同编号的inode吗?如果可以有,那上层又是如何确保全局唯一的呢?
  • uid,gid“mode代表文件所属用户/用户组和权限。discretionary access control (DAC) 自主访问控制。在计算机安全中,自主访问控制 (DAC) 是一种由可信计算机系统评估标准定义的访问控制。作为一种根据对象所属的主体和组的身份限制对对象的访问的手段,控制方式是自由的。因为具有特定访问权限的主体能够将该权限(可能是间接地)传递给任何其他主体(除非受到强制访问控制的约束)。与其对应的是 mandatory access control (MAC) 强制访问控制。
  • parentPathCaches“childPathCaches路径缓存链表,用户快速查找父子信息。
  • parent指向父节点,父节点不管是什么内容,一样都是文件,都用Vnode描述。
  • VnodeOps *vop这是对vnode的操作,vnode本身也是数据,存储在索引表中,记录了用户,用户组,权限,时间等信息,这部分信息是可以修改的,就需要接口来维护,便是VnodeOps。

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);//创建目录节点
  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);//重命名
  ....
}

看到没有里面的所有方法都是对索引节点(索引页)的增删改查操作,并不操作索引节点指向的数据块(图书区)内容。

  • 那么对数据块(图书区)的修改用什么方法呢? 答案是:file_operations_vfs *fop。

/* This structure is provided by devices when they are registered with the
* system.  It is used to call back to perform device specific operations.
*/
//该结构由设备在向系统注册时提供,它用于回调以执行特定于设备的操作。
struct file_operations_vfs
{
  /* The device driver open method differs from the mountpoint open method */
  int     (*open)(struct file *filep);

  /* The following methods must be identical in signature and position because
  * the struct file_operations and struct mountp_operations are treated like
  * unions.
  */

  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 */
  ....
};

file_operations_vfs看参数就知道,很是给vnode的上层的使用的。它是夹在应用层和vnode中间的一层,是 vnode起承上启下作用的上层。具体为什么要有file存在,后续会详细说明。总之通过file找到vnode,从而对vnode指向的内容区进行修改。我们在应用层比如修改一个ppt,创建一个word文档这些操作就是通过file_operations_vfs。一定要搞清楚VnodeOps 和 file_operations_vfs二者的区别。一个是对索引页的操作,一个是对索引页指向的内容的操作。

  • data使用了一个void类型。这是私有格式数据,说明运行时才知道是什么类型。就像一个没有任何提示信息的私人密码箱一样,是打不开的。不知道顺序乱开,只会毁掉数据。只有密码箱那边派人来了才能开,而这人就是各种不同的文件系统。每种文件系统如何读取数据的方式是不同的,差异化的就有接口内部来实现了。对外是相同的,无非都是读读写写。
  • hashEntry使用哈希算法来检索vnode。
  • actFreeEntry:这个就不用介绍了,双向链表是内核最重要的结构体。通过它挂到全局空闲链表和使用链表上。
  • originMount和newMount是挂载相关的,任何文件系统都需要先挂载到根文件系统下才能使用。关于挂载后续有详细介绍。

百文说内核 | 抓住主脉络

子曰:“诗三百,一言以蔽之,曰‘思无邪’。”——《论语》:为政篇。百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在开源鸿蒙内核源码加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。百篇博客系列思维导图结构如下:

根据上图的思维导图,我们未来将要和大家一一分享以上大部分关键技术点的博客文章。

百万汉字注解.精读内核源码

如果大家觉得看文章不过瘾,想直接撸代码的话,可以去下面四大码仓围观同步注释内核源码:

gitee仓

https://gitee.com/weharmony/kernel_liteos_a_note

github仓 :

https://github.com/kuangyufei/kernel_liteos_a_note

codechina仓

https://codechina.csdn.net/kuangyufei/kernel_liteos_a_note

coding仓

https://weharmony.coding.net/public/harmony/kernel_liteos_a_note/git/files

写在最后

我们最近正带着大家玩嗨OpenHarmony。如果你有用OpenHarmony开发的好玩的东东,或者有对OpenHarmony的深度技术剖析,想通过我们平台让更多的小伙伴知道和分享的,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:

合作邮箱:zzliang@atomsource.org