鸿蒙内核源码分析(文件句柄篇) | 深挖应用操作文件的细节 | 百篇博客分析OpenHarmony源码 | v69.02

754 阅读10分钟

子曰:“诵诗三百,授之以政,不达;使于四方,不能专对;虽多,亦奚以为?” 《论语》:子路篇

在这里插入图片描述

百篇博客系列篇.本篇为:

v69.xx 鸿蒙内核源码分析(文件句柄篇) | 你为什么叫句柄 ?

文件系统相关篇为:

句柄 | handle

int open(const char* pathname,int flags);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
int close(int fd);

只要写过应用程序代码操作过文件不会陌生这几个函数,文件操作的几个关键步骤嘛,跟把大象装冰箱分几步一样.先得把冰箱门打开,再把大象放进去,再关上冰箱门.其中最重要的一个参数就是fd,应用程序所有对文件的操作都基于它.fd可称为文件描述符,或者叫文件句柄(handle),个人更愿意称后者. 因为更形象,handle英文有手柄的意思,跟开门一样,握住手柄才能开门,手柄是进门关门的抓手.映射到文件系统,fd是应用层出入内核层的抓手.句柄是一个数字编号, open | creat去申请这个编号,内核会创建文件相关的一系列对象,返回编号,后续通过编号就可以操作这些对象.原理就是这么的简单,本篇将从fd入手,跟踪文件操作的整个过程.

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

  • 系统文件句柄(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进程)
    

进程文件句柄

在鸿蒙一个进程默认最多可以有256fd,即最多可打开256个文件.文件也是资源的一种,系列篇多次说过进程是管理资源的,所以在进程控制块中能看到文件的影子files_struct. files_struct可理解为进程的文件管理器,里面只放和本进程相关的文件,线程则共享这些文件.另外子进程也会拷贝一份父进程的files_struct到自己的files_struct上,在父子进程篇中也讲过fork的本质就是拷贝资源,其中就包括了文件内容.

//进程控制块
typedef struct ProcessCB {
    //..
    #ifdef LOSCFG_FS_VFS
        struct files_struct *files;        /**< Files held by the process */ //进程所持有的所有文件,注者称之为进程的文件管理器
    #endif	//每个进程都有属于自己的文件管理器,记录对文件的操作. 注意:一个文件可以被多个进程操作
} LosProcessCB;
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
};

fd_table_sfiles_struct的成员,负责记录所有进程文件句柄的信息,个人觉得鸿蒙这块的实现有点乱,没有封装好.

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 */ //管理对文件表的访问的信号量
};

file_table_s 记录进程fd和系统fd之间的绑定或者说映射关系

struct file_table_s {//进程fd <--> 系统fd绑定
    intptr_t sysFd; /* system fd associate with the tg_filelist index */
};

fd_set实现了进程fd按位图管理,系列操作为 FD_SET,FD_ISSET,FD_CLR,FD_ZERO 除以8是因为 char类型占8bit位.请尝试去理解下按位操作的具体实现.

typedef struct fd_set
{
  unsigned char fd_bits [(FD_SETSIZE+7)/8];
} fd_set;
#define FD_SET(n, p)  FDSETSAFESET(n, (p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] = (u8_t)((p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] |  (1 << (((n)-LWIP_SOCKET_OFFSET) & 7))))
#define FD_CLR(n, p)  FDSETSAFESET(n, (p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] = (u8_t)((p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] & ~(1 << (((n)-LWIP_SOCKET_OFFSET) & 7))))
#define FD_ISSET(n,p) FDSETSAFEGET(n, (p)->fd_bits[((n)-LWIP_SOCKET_OFFSET)/8] &   (1 << (((n)-LWIP_SOCKET_OFFSET) & 7)))
#define FD_ZERO(p)    memset((void*)(p), 0, sizeof(*(p)))

vfs_procfd.c 为进程文件句柄实现文件,每个进程的 0,1,2fd是由系统占用并不参与分配,即为大家熟知的:

  • STDIN_FILENO(fd = 0) 标准输入 接收键盘的输入
  • STDOUT_FILENO(fd = 1) 标准输出 向屏幕输出
  • STDERR_FILENO(fd = 2) 标准错误 向屏幕输出
/* minFd should be a positive number,and 0,1,2 had be distributed to stdin,stdout,stderr */
    if (minFd < MIN_START_FD) {
        minFd = MIN_START_FD;
    }
//分配进程文件句柄
static int AssignProcessFd(const struct fd_table_s *fdt, int minFd)
{
    if (fdt == NULL) {
        return VFS_ERROR;
    }
    if (minFd >= fdt->max_fds) {
        set_errno(EINVAL);
        return VFS_ERROR;
    }
	//从表中搜索未使用的 fd
    /* search unused fd from table */
    for (int i = minFd; i < fdt->max_fds; i++) {
        if (!FD_ISSET(i, fdt->proc_fds)) {
            return i;
        }
    }
    set_errno(EMFILE);
    return VFS_ERROR;
}
//释放进程文件句柄
void FreeProcessFd(int procFd)
{
    struct fd_table_s *fdt = GetFdTable();

    if (!IsValidProcessFd(fdt, procFd)) {
        return;
    }
    FileTableLock(fdt);
    FD_CLR(procFd, fdt->proc_fds);	//相应位清0
    FD_CLR(procFd, fdt->cloexec_fds);
    fdt->ft_fds[procFd].sysFd = -1;	//解绑系统文件描述符
    FileTableUnLock(fdt);
}
  • 分配和释放的算法很简单,由位图的相关操作完成.
  • fdt->ft_fds[i].sysFd中的i代表进程的fd,-1代表没有和系统文件句柄绑定.
  • 进程文件句柄和系统文件句柄的意义和关系在 (VFS篇)中已有说明,此处不再赘述,请自行前往翻看.

系统文件句柄

系统文件句柄的实现类似,但它并不在鸿蒙内核项目中,而是在NuttX项目的 fs_files.c 中, 因鸿蒙内核项目中使用了其他第三方的项目,所以需要加进来一起研究才能看明白鸿蒙整个内核的完整实现.具体涉及的子系统仓库如下:

  • 子系统注解仓库

    在给鸿蒙内核源码加注过程中发现仅仅注解内核仓库还不够,因为它关联了其他子系统,若对这些子系统不了解是很难完整的注解鸿蒙内核,所以也对这些关联仓库进行了部分注解,这些仓库包括:

  • 同样由位图来管理系统文件句柄,具体相关操作如下

//用 bitmap 数组来记录文件描述符的分配情况,一位代表一个SYS FD
static unsigned int bitmap[CONFIG_NFILE_DESCRIPTORS / 32 + 1] = {0};
//设置指定位值为 1
static void set_bit(int i, void *addr)
{
  unsigned int tem = (unsigned int)i >> 5; /* Get the bitmap subscript */
  unsigned int *addri = (unsigned int *)addr + tem;
  unsigned int old = *addri;
  old = old | (1UL << ((unsigned int)i & 0x1f)); /* set the new map bit */
  *addri = old;
}
//获取指定位,看是否已经被分配
bool get_bit(int i)
{
  unsigned int *p = NULL;
  unsigned int mask;

  p = ((unsigned int *)bitmap) + (i >> 5); /* Gets the location in the bitmap */
  mask = 1 << (i & 0x1f); /* Gets the mask for the current bit int bitmap */
  if (!(~(*p) & mask)){
    return true;
  }
  return false;
}
  • tg_filelist是全局系统文件列表,统一管理系统fd,其中的关键结构体是 file,这才是内核对文件对象描述的实体,是本篇最重要的内容.
    #if CONFIG_NFILE_DESCRIPTORS > 0
    struct filelist tg_filelist; //全局统一管理系统文件句柄
    #endif
    struct filelist
    {
      sem_t   fl_sem;               /* Manage access to the file list */
      struct file fl_files[CONFIG_NFILE_DESCRIPTORS];
    };
    struct file
    {
      unsigned int         f_magicnum;  /* file magic number */
      int                  f_oflags;    /* Open mode flags */
      struct Vnode         *f_vnode;    /* Driver interface */
      loff_t               f_pos;       /* File position */
      unsigned long        f_refcount;  /* reference count */
      char                 *f_path;     /* File fullpath */
      void                 *f_priv;     /* Per file driver private data */
      const char           *f_relpath;  /* realpath */
      struct page_mapping  *f_mapping;  /* mapping file to memory */
      void                 *f_dir;      /* DIR struct for iterate the directory if open a directory */
      const struct file_operations_vfs *ops;
      int fd;
    };
    
    • f_magicnum魔法数字,每种文件格式不同魔法数字不同,gif47 49 46 38,png89 50 4e 47
    • f_oflags 操作文件的权限模式,读/写/执行
    • f_vnode 对应的vnode
    • f_pos 记录操作文件的当前位置
    • f_refcount 文件被引用的次数,即文件被所有进程打开的次数.
    • f_priv 文件的私有数据
    • f_relpath 记录文件的真实路径
    • f_mapping 记录文件和内存的映射关系,这个在文件映射篇中有详细介绍.
    • ops 对文件内容的操作函数
    • fd 文件句柄编号,系统文件句柄是唯一的,一直到申请完为止,当f_refcount为0时,内核将回收fd.

open | creat | 申请文件句柄

通过文件路径名pathname获取文件句柄,鸿蒙实现过程如下

SysOpen //系统调用
    AllocProcessFd  //分配进程文件句柄
    do_open //向底层打开文件
        fp_open //vnode 层操作
            files_allocate
            filep->ops->open(filep) //调用各文件系统的函数指针
    AssociateSystemFd //绑定系统文件句柄

建一个file对象,i即为分配到的系统文件句柄.

//创建系统文件对象及分配句柄
int files_allocate(struct Vnode *vnode_ptr, int oflags, off_t pos, void *priv, int minfd)
  //...
  while (i < CONFIG_NFILE_DESCRIPTORS)//系统描述符
    {
      p = ((unsigned int *)bitmap) + (i >> 5); /* Gets the location in the bitmap */
      mask = 1 << (i & 0x1f); /* Gets the mask for the current bit int bitmap */
      if ((~(*p) & mask))//该位可用于分配
        {
          set_bit(i, bitmap);//占用该位
          list->fl_files[i].f_oflags   = oflags;
          list->fl_files[i].f_pos      = pos;//偏移位
          list->fl_files[i].f_vnode    = vnode_ptr;//vnode
          list->fl_files[i].f_priv     = priv;//私有数据
          list->fl_files[i].f_refcount = 1;	//引用数默认为1
          list->fl_files[i].f_mapping  = NULL;//暂无映射
          list->fl_files[i].f_dir      = NULL;//暂无目录
          list->fl_files[i].f_magicnum = files_magic_generate();//魔法数字
          process_files = OsCurrProcessGet()->files;//获取当前进程文件管理器
          return (int)i;
        }
      i++;
    }
    // ...
}

read | write

SysRead   //系统调用|读文件:从文件中读取nbytes长度的内容到buf中(用户空间)
  fd = GetAssociatedSystemFd(fd); //通过进程fd获取系统fd
  read(fd, buf, nbytes);  //调用系统fd层的读函数
    fs_getfilep(fd, &filep);  //通过系统fd获取file对象
    file_read(filep, buf, nbytes) //调用file层的读文件
      ret = (int)filep->ops->read(filep, (char *)buf, (size_t)nbytes);//调用具体文件系统的读操作
SysWrite   //系统调用|写文件:将buf中(用户空间)nbytes长度的内容写到文件中
  fd = GetAssociatedSystemFd(fd); //通过进程fd获取系统fd
  write(sysfd, buf, nbytes);  //调用系统fd层的写函数
    fs_getfilep(fd, &filep);  //通过系统fd获取file对象
    file_seek64
    file_write(filep, buf, nbytes);//调用file层的写文件
      ret = filep->ops->write(filep, (const char *)buf, nbytes);//调用具体文件系统的写操作

此处仅给出 file_write 的实现

ssize_t file_write(struct file *filep, const void *buf, size_t nbytes)
{
  int ret;
  int err;

  if (buf == NULL)
    {
      err = EFAULT;
      goto errout;
    }

  /* Was this file opened for write access? */

  if ((((unsigned int)(filep->f_oflags)) & O_ACCMODE) == O_RDONLY)
    {
      err = EACCES;
      goto errout;
    }

  /* Is a driver registered? Does it support the write method? */

  if (!filep->ops || !filep->ops->write)
    {
      err = EBADF;
      goto errout;
    }

  /* Yes, then let the driver perform the write */

  ret = filep->ops->write(filep, (const char *)buf, nbytes);
  if (ret < 0)
    {
      err = -ret;
      goto errout;
    }

  return ret;

errout:
  set_errno(err);
  return VFS_ERROR;
}      

close

//关闭文件句柄
int SysClose(int fd)
{
    int ret;

    /* Process fd convert to system global fd */
    int sysfd = DisassociateProcessFd(fd);//先解除关联

    ret = close(sysfd);//关闭文件,个人认为应该先 close - > DisassociateProcessFd 
    if (ret < 0) {//关闭失败时
        AssociateSystemFd(fd, sysfd);//继续关联
        return -get_errno();
    }
    FreeProcessFd(fd);//释放进程fd
    return ret;
}
  • 解除进程fd和系统fd的绑定关系
  • close时会有个判断,这个文件的引用数是否为0,只有为0才会真正的执行_files_close
    int files_close_internal(int fd, LosProcessCB *processCB)
    {
      //...
      list->fl_files[fd].f_refcount--;
      if (list->fl_files[fd].f_refcount == 0)
        {
    #ifdef LOSCFG_KERNEL_VM
          dec_mapping_nolock(filep->f_mapping);
    #endif
          ret = _files_close(&list->fl_files[fd]);
          if (ret == OK)
            {
              clear_bit(fd, bitmap);
            }
        }
      // ... 
    }
    static int _files_close(struct file *filep)
    {
      struct Vnode *vnode = filep->f_vnode;
      int ret = OK;
    
      /* Check if the struct file is open (i.e., assigned an vnode) */
      if (filep->f_oflags & O_DIRECTORY)
        {
          ret = closedir(filep->f_dir);
          if (ret != OK)
            {
              return ret;
            }
        }
      else
        {
          /* Close the file, driver, or mountpoint. */
          if (filep->ops && filep->ops->close)
            {
              /* Perform the close operation */
    
              ret = filep->ops->close(filep);
              if (ret != OK)
                {
                  return ret;
                }
            }
          VnodeHold();
          vnode->useCount--;
          /* Block char device is removed when close */
          if (vnode->type == VNODE_TYPE_BCHR)
            {
              ret = VnodeFree(vnode);
              if (ret < 0)
                {
                  PRINTK("Removing bchar device %s failed\n", filep->f_path);
                }
            }
          VnodeDrop();
        }
    
      /* Release the path of file */
    
      free(filep->f_path);
    
      /* Release the file descriptor */
    
      filep->f_magicnum = 0;
      filep->f_oflags   = 0;
      filep->f_pos      = 0;
      filep->f_path     = NULL;
      filep->f_priv     = NULL;
      filep->f_vnode    = NULL;
      filep->f_refcount = 0;
      filep->f_mapping  = NULL;
      filep->f_dir      = NULL;
    
      return ret;
    }    
    
  • 最后FreeProcessFd负责释放该文件在进程层面占用的资源

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

四大码仓同步注解内核源码, >> 查看 gitee 仓库

百篇博客分析.深挖内核地基

给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 :P
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,.xx 代表修改的次数,精雕细琢,言简意赅,力求打造精品内容。

编译构建基础工具加载运行进程管理
编译环境
编译过程
环境脚本
构建工具
gn应用
忍者ninja
双向链表
位图管理
用栈方式
定时器
原子操作
时间管理
ELF格式
ELF解析
静态链接
重定位
进程映像
进程管理
进程概念
Fork
特殊进程
进程回收
信号生产
信号消费
Shell编辑
Shell解析
进程通讯内存管理前因后果任务管理
自旋锁
互斥锁
进程通讯
信号量
事件控制
消息队列
内存分配
内存管理
内存汇编
内存映射
内存规则
物理内存
总目录
调度故事
内存主奴
源码注释
源码结构
静态站点
时钟任务
任务调度
任务管理
调度队列
调度机制
线程概念
并发并行
系统调用
任务切换
文件系统硬件架构
文件概念
文件系统
索引节点
挂载目录
根文件系统
字符设备
VFS
文件句柄
管道文件
汇编基础
汇编传参
工作模式
寄存器
异常接管
汇编汇总
中断切换
中断概念
中断管理

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。