一、 procfs介绍
procfs是类UNIX操作系统中进程文件系统(process file system)的缩写,主要用于通过内核访问进程信息和系统信息,以及可以修改内核参数改变系统行为。需要注意的是,procfs文件系统是一个虚拟文件系统,不存在硬盘当中,而是系统启动时动态生成的文件系统,储存在内存中。procfs文件系统通常挂载在/proc目录下。
LiteOS-A是OpenAtom OpenHarmony(以下简称“OpenHarmony”)系统中使用的轻量系统内核,实现了procfs文件系统。本文主要对LiteOS-A内核中的procfs文件系统的设计、实现和使用进行介绍和分析。
procfs文件系统是LiteOS-A内核文件系统的一个案例,通过了解procfs文件系统,能够熟悉LiteOS-A的文件系统框架,并很好地将内核信息通过文件系统反馈给使用者。
- Linux系统中的procfs文件系统包含的内容
Ubuntu 20.04中的/proc文件信息如下:
图1:Ubuntu proc目录信息
- OS-A系统的命令以及procfs文件系统的内容
LiteOS-A的命令集:
LiteOS-A的proc目录信息如下:
图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,但其仍旧需要实现上述接口。
- VFS提供的重要接口
(1) Vnode 结构体:
struct Vnode {
enum VnodeType type;
int useCount;
uint32_t hash;
uint uid;
uint gid;
mode_t mode;
LIST_HEAD parentPathCaches;
LIST_HEAD childPathCaches;
struct Vnode *parent;
struct VnodeOps *vop;
struct file_operations_vfs *fop;
void *data;
uint32_t flag;
LIST_ENTRY hashEntry;
LIST_ENTRY actFreeEntry;
struct Mount *originMount;
struct Mount *newMount;
char *filePath;
struct page_mapping mapping;
};
图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;
const struct MountOps *ops;
struct Vnode *vnodeBeCovered;
struct Vnode *vnodeCovered;
struct Vnode *vnodeDev;
LIST_HEAD vnodeList;
int vnodeSize;
LIST_HEAD activeVnodeList;
int activeVnodeSize;
void *data;
uint32_t hashseed;
unsigned long mountFlags;
char pathName[PATH_MAX];
char devName[PATH_MAX];
};
图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;
int f_oflags;
struct Vnode *f_vnode;
loff_t f_pos;
unsigned long f_refcount;
char *f_path;
void *f_priv;
const char *f_relpath;
struct page_mapping *f_mapping;
void *f_dir;
const struct file_operations_vfs *ops;
int fd;
};
文件接口功能定义:
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);
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指向的目录中的一个子项,其本质是一个单向链表的形式,并且采用头插法的形式进行节点的插入。
图5:DirEntry
图6:ProcFile
图7:ProcData
图8: ProcFileOperations
三、 procfs文件系统的实现
- 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);
- Procfs的初始化初始化
需要做的工作主要包括向OS注册procfs文件系统,生成procfs文件目录中的文件初始项,在liteOS-A具体包含目录power、mounts等。
procfs文件系统的初始化流程大致如下:
main(VOID)
|-> OsMain()
|
| -> EarliestInit()
| -> ...
|
| -> KModInit()
|-> ...
|
|-> OsInitCall(LOS_INIT_LEVEL_KMOD_EXTENDED)
|-> InitLevelCall(level)
|
|
|-> ProcFsInit()
| |-> mkdir(PROCFS_MOUNT_POINT, PROCFS_DEFAULT_MODE)
| |-> mount(NULL, PROCFS_MOUNT_POINT, "procfs", 0, NULL)
| |
| |
| |-> ProcMountsInit()
| | |
| | |
| | |-> ProcMountsInit(void)
| | |
| | |
| | |-> CreateProcEntry("mounts", 0, NULL)
| | |
| | |
| | |-> ProcCreateFile(parent, name, NULL, mode)
| | |-> struct ProcDirEntry *pn = NULL
| | |-> ProcAllocNode(&parent, name, S_IFREG | mode)
| | |-> struct ProcDirEntry *pn = NULL;
| | |
| | |
| | |-> pn = (struct ProcDirEntry *)malloc(sizeof(struct ProcDirEntry));
| | |
| | |
| | |-> 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;
| | |
| | |
| | |-> ProcAddNode(parent, pn)
| | |
| | |
| | |
| | |-> pn->parent = parent;
| | |-> pn->next = parent->subdir;
| | |-> parent->subdir = pn;
| |->...
| |
| |->ProcPmInit()
| | |
| | |-> 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业务分析
- 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(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;
| | |...
- 节点的增加过程分析
关键代码如下:
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/节点就是该文件系统的根节点,此时两者的关系可以用下图表示:
图9:层级目录的关系
此时若需要在两者在插入一个power节点,则首先需要先生成一个power节点如下,再改变相应的指向即可,具体可以参考图10,给出三者之间的关系,最终的节点效果如图11。图10:生成一个新节点
图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函数对输入的命令进行分析,并采取相关的动作。
-> ...
->
-> writeproc value >> path
|->
|-> struct ProcDirEntry *handle = NULL;
|-> const char *rootProcDir = "/proc/";
|-> handle = OpenProcFile(realPath, O_TRUNC)
| |-> pn = ProcFindEntry(fileName)
| | |-> int leveltotal = 0;
| | |
| |-> pn->flags = (unsigned int)(pn->flags) | (unsigned int)flags
| |-> ...
| WriteProcFile(handle, value, len)
| |
| |-> 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移植心得》。