LiteOS-A核心中的procfs檔案系統分析

OpenHarmony開發者社群發表於2022-12-08

一、 procfs介紹

procfs是類UNIX作業系統中程式檔案系統(process file system)的縮寫,主要用於透過核心訪問程式資訊和系統資訊,以及可以修改核心引數改變系統行為。需要注意的是,procfs檔案系統是一個虛擬檔案系統,不存在硬碟當中,而是系統啟動時動態生成的檔案系統,儲存在記憶體中。procfs檔案系統通常掛載在/proc目錄下。

LiteOS-A是OpenAtom OpenHarmony(以下簡稱“OpenHarmony”)系統中使用的輕量系統核心,實現了procfs檔案系統。本文主要對LiteOS-A核心中的procfs檔案系統的設計、實現和使用進行介紹和分析。

procfs檔案系統是LiteOS-A核心檔案系統的一個案例,透過了解procfs檔案系統,能夠熟悉LiteOS-A的檔案系統框架,並很好地將核心資訊透過檔案系統反饋給使用者。

1. Linux系統中的procfs檔案系統包含的內容

Ubuntu 20.04中的/proc檔案資訊如下:

undefined

圖1:Ubuntu proc目錄資訊

2. OS-A系統的命令以及procfs檔案系統的內容

LiteOS-A的命令集:

undefined

LiteOS-A的proc目錄資訊如下:

undefined

圖2:liteOS-A proc目錄資訊

二、 procfs檔案系統的設計

LiteOS-A中使用VFS作為各個檔案系統的粘合層,而VFS在OpenHarmony核心中採用樹結構實現,樹中的每一個節點都是Vnode結構體。VFS提供統一的抽象介面用於遮蔽檔案系統之間的差異,其提供三大操作介面用於統一不同檔案系統呼叫不同介面的現狀。

VFS提供的三大操作介面:

• VnodeOps

• MountOps

• file_operations_vfs

VnodeOps用於控制Vnode節點,MountOps控制掛載點,file_operations_vfs提供常用的檔案介面。

檔案系統各自需要實現VFS提供的這三大介面,即實現系統本身需要的介面方法,讓VFS能夠呼叫這些介面即可。procfs檔案系統雖作為一個偽檔案系統pseudo-file system,但其仍舊需要實現上述介面。

1. VFS提供的重要介面

(1) Vnode 結構體:

struct Vnode {
    enum VnodeType type;                /* Vnode節點型別 */
    int useCount;                       /* 節點連結數 */
    uint32_t hash;                      /* 雜湊值 */
    uint uid;                           /* 檔案擁有者的user id */
    uint gid;                           /* 檔案群組id */
    mode_t mode;                        /* 檔案讀寫執行許可權 */
    LIST_HEAD parentPathCaches;         /* 指向父節點的路徑快取 */
    LIST_HEAD childPathCaches;          /* 指向兒子節點的路徑快取 */
    struct Vnode *parent;               /* vnode父節點 */
    struct VnodeOps *vop;               /* vnode操作介面 */
    struct file_operations_vfs *fop;    /* 檔案操作介面,即指定檔案系統 */
    void *data;                         /* 資料,指向內部資料的指標 */
    uint32_t flag;                      /* 節點標籤 */
    LIST_ENTRY hashEntry;               /* 掛入v_vnodeHashEntry[i]中 */
    LIST_ENTRY actFreeEntry;            /* 透過本節點掛載到空閒和使用連結串列中 */
    struct Mount *originMount;          /* 所在檔案系統掛載資訊 */
    struct Mount *newMount;             /* 其他掛載在這個節點中的檔案系統資訊 */
    char *filePath;                     /* Vnode的路徑資訊 */
    struct page_mapping mapping;        /* page mapping of the vnode */
};

undefined

圖3:Vnode structure

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);// 查詢節點
    int (*Open)(struct Vnode *vnode, int fd, int mode, int flags);// 開啟節點
    ssize_t (*ReadPage)(struct Vnode *vnode, char *buffer, off_t pos);
    ssize_t (*WritePage)(struct Vnode *vnode, char *buffer, off_t pos, size_t buflen);
    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);// 改變節點屬性
    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);
};

Vnode根節點的初始化操作:

將全域性Vnode表進行初始化,開始節點指向根目錄/,全域性節點g_rootVnode。

int VnodesInit(void)
{
    int retval = LOS_MuxInit(&g_vnodeMux, NULL);
    if (retval != LOS_OK) {
        PRINT_ERR("Create mutex for vnode fail, status: %d", retval);
        return retval;
    }
    LOS_ListInit(&g_vnodeFreeList);
    LOS_ListInit(&g_vnodeVirtualList);
    LOS_ListInit(&g_vnodeActiveList);
    retval = VnodeAlloc(NULL, &g_rootVnode);
    if (retval != LOS_OK) {
        PRINT_ERR("VnodeInit failed error %d\n", retval);
        return retval;
    }
    g_rootVnode->mode = S_IRWXU | S_IRWXG | S_IRWXO | S_IFDIR;
    g_rootVnode->type = VNODE_TYPE_DIR;
    g_rootVnode->filePath = "/";
    return LOS_OK;
}

(2) Mount結構體:

struct Mount {
    LIST_ENTRY mountList;              /* 全域性Mount連結串列 */
    const struct MountOps *ops;        /* Mount的功能函式 */
    struct Vnode *vnodeBeCovered;      /* 要被掛載的節點 */
    struct Vnode *vnodeCovered;        /* 要掛載的節點 */
    struct Vnode *vnodeDev;            /* 裝置vnode */
    LIST_HEAD vnodeList;               /* Vnode表的表頭 */
    int vnodeSize;                     /* Vnode表的節點數量 */
    LIST_HEAD activeVnodeList;         /* 啟用的節點連結串列 */
    int activeVnodeSize;               /* 啟用的節點數量 */
    void *data;                        /* 資料,指向內部資料的指標 */
    uint32_t hashseed;                 /* Random seed for vfshash */
    unsigned long mountFlags;          /* 掛載標籤 */
    char pathName[PATH_MAX];           /* 掛載點路徑 */
    char devName[PATH_MAX];            /* 裝置名稱 /dev/sdb-1 */
};

undefined

圖4:Mount structure

掛載點的介面定義:

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);//統計檔案系統的資訊,型別、大小等
    int (*Sync)(struct Mount *mount);
};

(3)檔案結構定義:

struct file
{
  unsigned int         f_magicnum;  /* file magic number. -- to be deleted */
  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.  -- to be deleted */
  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;
};

檔案介面功能定義:

struct file_operations_vfs
{
  /* The device driver open method differs from the mountpoint open method */
  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 */
  int     (*poll)(struct file *filep, poll_table *fds);
  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);
};

2.檔案系統的重要介面設計

procfs檔案系統中每個目錄或檔案都是一個Vnode,也可以理解為一個entry。ProcDirEntry中的subdir指向的目錄中的一個子項,其本質是一個單向連結串列的形式,並且採用頭插法的形式進行節點的插入。

undefined

圖5:DirEntry

undefined

圖6:ProcFile

undefined

圖7:ProcData

undefined

圖8: ProcFileOperations

三、 procfs檔案系統的實現

1. Procfs的註冊過程

(1)向系統註冊檔案系統入口函式:

LOS_MODULE_INIT(ProcFsInit, LOS_INIT_LEVEL_KMOD_EXTENDED);

(2)向VFS檔案系統表註冊系統名以及實現的介面等:

const struct MountOps procfs_operations = {
    .Mount = VfsProcfsMount,
    .Unmount = NULL,
    .Statfs = VfsProcfsStatfs,
};
static struct VnodeOps g_procfsVops = {
    .Lookup = VfsProcfsLookup,
    .Getattr = VfsProcfsStat,
    .Readdir = VfsProcfsReaddir,
    .Opendir = VfsProcfsOpendir,
    .Closedir = VfsProcfsClosedir,
    .Truncate = VfsProcfsTruncate
};
static struct file_operations_vfs g_procfsFops = {
    .read = VfsProcfsRead,
    .write = VfsProcfsWrite,
    .open = VfsProcfsOpen,
    .close = VfsProcfsClose
};
// 註冊檔案系統名字以及實現的介面方法等
FSMAP_ENTRY(procfs_fsmap, "procfs", procfs_operations, FALSE, FALSE);

2. Procfs的初始化初始化

需要做的工作主要包括向OS註冊procfs檔案系統,生成procfs檔案目錄中的檔案初始項,在liteOS-A具體包含目錄power、mounts等。

procfs檔案系統的初始化流程大致如下:

// 系統的入口函式
main(VOID)
|-> OsMain() // ./liteos/kernel/liteos_a/kernel/common/main.c
    | // 進行系統的相關初始化工作
    | -> EarliestInit()
    | -> ...
    |
    | -> KModInit()
         |-> ...
         |
         |-> OsInitCall(LOS_INIT_LEVEL_KMOD_EXTENDED) // 生成procfs檔案系統並掛載到/proc目錄
          |-> InitLevelCall(level)//根據不同的級別進行相關初始化工作,procfs的級別是8,其級別是檔案系統向OS註冊的
           | // ./liteos/kernel/liteos_a/fs/proc/os_adapt/proc_init.c
           |
           |-> ProcFsInit() // 進行procfs檔案系統的具體初始化工作
           | |-> mkdir(PROCFS_MOUNT_POINT, PROCFS_DEFAULT_MODE) // 先生成/proc目錄,之後需要將procfs檔案系統掛載到該目錄下
           | |-> mount(NULL, PROCFS_MOUNT_POINT, "procfs", 0, NULL)
           | | // 生成mount檔案,包括分配Vnode和掛載Vnode
           | |
           | |-> ProcMountsInit()
           | | | // procfs具體項的初始化都寫在一個獨立的檔案中,例如mounts在./liteos/kernel/liteos_a/fs/proc/os_adapt/mounts_proc.c
           | | |
           | | |-> ProcMountsInit(void)
           | | | // 建立mounts節點並掛載到該目錄下,NULL位parent為父節點,若parent為NULL,則預設父節點為/proc
           | | |
           | | |-> CreateProcEntry("mounts", 0, NULL)
           | |  | // 先判斷節點是檔案屬性還是目錄屬性,後選擇具體的節點建立函式,在這選擇File節點
           | |  |
           | |  |-> ProcCreateFile(parent, name, NULL, mode)
           | |   |-> struct ProcDirEntry *pn = NULL
           | |   |-> ProcAllocNode(&parent, name, S_IFREG | mode) // 具體的分配節點
           | |    |-> struct ProcDirEntry *pn = NULL;
           | |    | // 首先對節點的合法性進行相關檢查,例如parent是否NULL,name是否NULL等
           | |    | 
           | |    |-> pn = (struct ProcDirEntry *)malloc(sizeof(struct ProcDirEntry));//分配一個struct ProcDirEntry記憶體地址
           | |    | // 對生成的節點賦予一些屬性,例如節點名字長度,許可權,名字等,每個ProcDirEntry都需要指定一個ProcFile成員,裡面含有具體資訊
           | |    |
           | |    |-> pn->nameLen = strlen(lastName);
           | |    |-> pn->mode = mode;
             | |    |-> ret = memcpy_s(pn->name, sizeof(pn->name), lastName, strlen(lastName) + 1);
             | |    |-> pn->pf = (struct ProcFile *)malloc(sizeof(struct ProcFile));
             | |    |-> pn->pf->pPDE = pn;// ProcFile的parent是生成的pn節點
             | |   | // 生成對應的節點,對節點指定相應的函式介面後,需要掛載的父節點中
             | |   |
             | |   |-> ProcAddNode(parent, pn)
             | |    | // 先判斷parent是否為NULL以及pn是否已經有parent,即判斷是否已掛載
             | |    |
             | |    | // 在這裡可知一個目錄下的子目錄以及檔案都是以一個單連結串列的形式儲存的,且採用的是頭插法,即最先生成的在最後面
             | |    |-> pn->parent = parent;
             | |    |-> pn->next = parent->subdir;
             | |    |-> parent->subdir = pn;
             | |->...
             | |
             | |->ProcPmInit() // 目錄初始化工作
             | | | // power目錄下含有子目錄,但是目錄生成的過程都一樣,在這以power資料夾為例
             | | |-> struct ProcDirEntry *power = CreateProcEntry("power", S_IFDIR | S_IRWXU | S_IRWXG | S_IROTH, NULL);
             | | | |-> CreateProcEntry("power", S_IFDIR | S_IRWXU | S_IRWXG | S_IROTH, NULL)
             | | | | |-> // 先判斷節點是檔案屬性還是目錄屬性,後選擇具體的節點建立函式,在這選擇目錄節點
             | | | | |
             | | | | |-> ProcCreateDir(parent, name, NULL, mode)
             | | | | | | // 這裡節點建立和掛載和上述檔案節點建立一樣,不再贅述
             | | | | | |
             | | | | | |-> ProcAllocNode(&parent, name, S_IFREG | mode) // 具體的分配節點
             | | | | | |-> ProcAddNode(parent, pn)
             | | | | |
             | | | |
             | | |-> ...
             | |
             |...

四、procfs業務分析

1. procfs掛載過程分析

在procfs檔案系統的掛載過程中,若使用qemu進行除錯,則掛載的命令大致如下: mount -R -t procfs [Dir_Path]  

mount的系統呼叫間接呼叫procfs的mount介面。

使用者輸入掛載命令後,引發系統呼叫SysMount開始逐層呼叫:

-> ... 
-> SysMount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags,const void *data) 
|--|-> 將路徑,檔案系統等轉化之後呼叫mount 
|  |-> mount(sourceRet, targetRet, (filesystemtype ? fstypeRet : NULL), mountflags, dataRet) 
|  |   |-> //找到指定的檔案系統 
|  |   |-> fsmap = mount_findfs(filesystemtype) 
|  |   |-> mops = fsmap->fs_mops // 為mount節點指定mount的介面函式 
|  |   |-> //找到掛載目錄對應的Vnode並且設定檔案系統相關資訊 
|  |   |-> VnodeLookup(target, &mountpt_vnode, 0) 
|  |   |   |->VnodeLookupAt(path, vnode, flags, NULL) 
|  |   |   |  |-> //對目錄變成絕對路徑並且從全域性Vnode連結串列中開始找 
|  |   |   |  |-> PreProcess(path, &startVnode, &normalizedPath) 
|  |   |   |  |  |-> vfs_normalize_path(NULL, originPath, &absolutePath) 
|  |   |-> mnt = MountAlloc(mountpt_vnode, (struct MountOps*)mops) 
|  |   |-> mops->Mount(mnt, device, data)//進入具體的procfs檔案系統的mount函式 
|  |   |  |-> VfsProcfsMount(struct Mount *mnt, struct Vnode *device, const void *data) 
|  |   |  |   |-> VnodeAlloc(&g_procfsVops, &vp);//生成一個Vnode用於掛載mount節點和procfs檔案系統的root節點 
|  |   |  |   |-> root = GetProcRootEntry(); //獲取procfs檔案系統的根節點 
|  |   |  |   |-> vp->data = root; //  
|  |   |  |   |-> vp->originMount = mnt;// 將vp掛載在掛載目錄所對應的mount節點上 
|  |   |  |   |-> mnt->data = NULL; 
|  |   |  |   |-> mnt->vnodeCovered = vp;// mount節點掛載的Vnode是該檔案系統,方便後續在mount連結串列中找掛載點 
|  |   |  |   |-> vp->type = root->type; 
|  |   |...

2. 節點的增加過程分析

關鍵程式碼如下:

temp = ProcFindNode(parent, pn->name);
    if (temp != NULL) {
        PRINT_ERR("Error!ProcDirEntry '%s/%s' already registered\n", parent->name, pn->name);
        spin_unlock(&procfsLock);
        return -EEXIST;
    }
    pn->parent = parent;
    pn->next = parent->subdir;
    parent->subdir = pn;

為了更好地說明,假設目前已經在系統中生成了proc/和mounts節點,proc/節點就是該檔案系統的根節點,此時兩者的關係可以用下圖表示:

undefined

圖9:層級目錄的關係此時若需要在兩者在插入一個power節點,則首先需要先生成一個power節點如下,再改變相應的指向即可,具體可以參考圖10,給出三者之間的關係,最終的節點效果如圖11。

undefined

圖10:生成一個新節點

undefined

圖11:重新組合

3、writeproc shell命令的建立

liteOS-A中含有一個叫writeproc的shell命令,使用格式如下:

writeproc value >> path

shell命令的建立方式主要有兩種,分靜態註冊和動態註冊,writeproc命令使用靜態註冊方式進行註冊,在本文中也主要介紹靜態註冊。

shell開發的流程如下:

① 定義一個新增命令所要呼叫的執行函式xxx;

② 使用SHELLCMD_ENTRY函式新增新增命令項;

③ 在連結選項liteos_tables_ldflags.mk中新增連結該新增命令項引數;

④ 重新編譯程式碼後執行。

writeproc的註冊如下:

// 定義一個具體的執行函式 
int OsShellCmdWriteProc(int argc, char **argv); 
// 新增命令項 
SHELLCMD_ENTRY(writeproc_shellcmd, CMD_TYPE_EX, "writeproc", XARGS, (CmdCallBackFunc)OsShellCmdWriteProc);

writeproc的具體流程分析:

①首先由使用者按照命令格式進行輸入;

②OsShellCmdWriteProc函式對輸入的命令進行分析,並採取相關的動作。

-> ... 
 -> // 使用shell命令喚起writeproc註冊函式 
 -> writeproc value >> path 
  |-> // 進行初始化工作,主要用於判斷輸入路徑是否合法,節點是否存在 
  |-> struct ProcDirEntry *handle = NULL; 
  |-> const char *rootProcDir = "/proc/"; 
  |-> handle = OpenProcFile(realPath, O_TRUNC) // 若路徑合法則找到對應的Vnode 
  |   |-> pn = ProcFindEntry(fileName) 
  |   |   |-> int leveltotal = 0;// leveltotal用於判定檔案所對應的層級,一個/表示一層 
  |   |   | // 遍歷Vnode找到對應的Vnode並返回 
  |   |-> pn->flags = (unsigned int)(pn->flags) | (unsigned int)flags// 設定節點相應的許可權 
  |   |-> ... 
  |   WriteProcFile(handle, value, len) // 找到檔案控制程式碼之後開始寫入資料 
  |   | // 使用Vnode的檔案介面對ProcFile資料成員進行寫入 
  |   |-> result = pde->procFileOps->write(pde->pf, (const char *)buf, len, &(pde->pf->fPos)) 
  |...

根據檔名查詢Vnode的關鍵程式碼:

pn = &g_procRootDirEntry; 
while ((pn != NULL) && (levelcount < leveltotal)) { 
 levelcount++; 
 isfoundsub = 0; 
 while (pn != NULL) { 
      next = strchr(path, '/'); 
      if (next == NULL) { 
           while (pn != NULL) { 
               if (strcmp(path, pn->name) == 0) { 
                    spin_unlock(&procfsLock); 
                    return pn; 
               } 
               pn = pn->next; 
           } 
           pn = NULL; 
           spin_unlock(&procfsLock); 
           return pn; 
      } 
 
      len = next - path; 
      if (pn == &g_procRootDirEntry) { 
           if (levelcount == leveltotal) { 
               spin_unlock(&procfsLock); 
               return pn; 
           } 
           len = g_procRootDirEntry.nameLen; 
      } 
      if (ProcMatch(len, path, pn)) { 
           isfoundsub = 1; 
           path += len + 1; 
           break; 
      } 
 
      pn = pn->next; 
 } 
}

五、總結

本文介紹了LiteOS-A核心下proc相關目錄資訊,並且對LiteOS-A核心中procfs檔案系統的原理和實現,結合原始碼進行了分析。同時,透過writeproc shell命令介紹了procfs的使用。希望讀者可以掌握LiteOS-A檔案系統的基本知識,更好地運用於基於LiteOS-A核心的系統移植工作。

關於OpenHarmony核心的內容,之前我還介紹了LiteOS-A核心之基礎硬體——中斷控制器、GIC400核心物件佇列的演算法、OpenHarmony LiteOS-M核心事件的運作機制,以及核心IPC機制資料結構、OpenHarmony Liteos-A核心之iperf3移植方法,感興趣的讀者可以點選閱讀:

淺談OpenHarmony LiteOS-A核心之基礎硬體——中斷控制器GIC400 》、

OpenHarmony——核心物件佇列之演算法詳解(上) 》、

OpenHarmony——核心物件佇列之演算法詳解(下) 》、

OpenHarmony——核心物件事件之原始碼詳解 》、

OpenHarmony——核心IPC機制資料結構解析 》、

OpenHarmony Liteos_A核心之iperf3移植心得 》。

undefined


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70011554/viewspace-2927285/,如需轉載,請註明出處,否則將追究法律責任。

相關文章