Linux:深入理解文件系统及其实现

Linux:Proc文件系统和实现

Abstract

“一切都是文件”是unix/linux中广为人知的哲学,更详细的解释是:一切设备,套接字,管道,进程,都以文件的形式描述,支持open,close,read,write等操作。1

不同的数据储存形式不同,由不同的文件系统管理,但它们需要提供相同的接口,使得这一点得以成立的是VFS,这是一个建立在所有文件系统之上的文件系统。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A4cdG7tt-1584504612798)(F:\Users\iSIka\AppData\Roaming\Typora\typora-user-images\image-20200309095138400.png)]

例如,cat a2,无论a是什么类型的文件,内核都必须保证cat a能够执行某种功能,而不至于崩溃。

通过学习设备驱动,我们理解了这样一个道理:所有的设备都是文件,要定义某种设备,只需要定义该设备对应的文件操作函数。然而,设备毕竟是真实可见的东西,换句话说,当你cat device的时候,你知道自己在处理什么。

在所有的文件系统中,Proc文件系统时比较特殊的一个,它管理的文件是正在运行的内核的信息3,换句话说,Proc文件系统提供了用户和内核之间交互的接口4,经典的ps,top等都是通过读取Proc文件系统实现的。

根据VFS的要求,和管理设备的文件系统一样,Proc文件系统也应该提供将内核信息视为文件的方法,在Linux2.6之前,内核中有成千上万种实现,如果需要展示的内核信息很简短,这些实现都能工作,然而,当信息过长时,便需要一种统一的机制处理,于是出现了seq_file和single系接口5

我将从以下三个方法考虑Proc文件系统的实现,希望能增进对文件系统本质的理解。

  1. file_operations
  2. seq_file
  3. single_open

file_operations

我们将从最简单的”只使用file_operations"出发,这种方法的思路也很简单:”为了由内核向用户发送信息,内核在/Proc下创建一个文件,为该文件赋予一个file_operations结构体,其中指定open函数。,当用户访问时,调用指定的函数“

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/mm.h>

MODULE_LICENSE("GPL");
static int test_proc_open(struct inode* inode, struct file* file){
	printk("hello\n");
}
static const struct file_operations test_proc_ops={
	.open    = test_proc_open,
};
static int __init test_module_init(void){
	proc_create("proc_test1",0644,NULL,&test_proc_ops);
	printk("init success\n");
	return 0;
}
static int __exit test_module_exit(void){
	printk("exit success\n");
}
module_init(test_module_init);
module_exit(test_module_exit);

如果有linux模块的基础知识,那么只有一个函数是我们陌生的:

proc_create6

说明:创建proc虚拟文件系统文件

函数原型:

struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops)

参数:

  • name
    你要创建的文件名。
  • mode
    为创建的文件指定权限
  • parent
    为你要在哪个文件夹下建立名字为name的文件,如:init_net.proc_net是要在/proc/net/下建立文件。
  • proc_fops
    为struct file_operations

简而言之,当模块被加载时,在Proc目录下创建名为“proc_test1"的文件,当它被访问时,调用test_proc_open函数,打印hello。

在这里插入图片描述

之所以出现4个hello,是因为我之前已经尝试过三种其他方式。

尽管这是一个简单的示例程序,但我认为它体现了”一切皆文件“这一哲学和文件系统的本质:对为不同的文件安排不同函数,以符合统一的接口。后面的seq_file和single只是在进一步包装了它,提出了更好用的规范而已7

从这里我们也可以看出Linux的面向对象特质:一切皆文件,也就是一切都需要实现file_operations接口,一切都继承于文件,另外,Linux中大多数操作都是由结构体(对象)完成。如果能用C++重写Linux…。

seq_file

正如名字所示,seq_file擅长处理序列化的信息,因此我们使用链表举例。

#include <linux/init.h>

#include <linux/module.h>

#include <linux/seq_file.h>

#include <linux/debugfs.h>

#include <linux/fs.h>

#include <linux/list.h>

#include <linux/slab.h>

#include <linux/proc_fs.h>

static LIST_HEAD(seq_demo_list);

static DEFINE_MUTEX(seq_demo_lock);

struct seq_demo_node {

    char name[10];

    struct list_head list;

};
static void *seq_demo_start(struct seq_file *s, loff_t *pos)

{

    mutex_lock(&seq_demo_lock);



    return seq_list_start(&seq_demo_list, *pos);

}

static void *seq_demo_next(struct seq_file *s, void *v, loff_t *pos)

{

    return seq_list_next(v, &seq_demo_list, pos);

}



static void seq_demo_stop(struct seq_file *s, void *v)

{

    mutex_unlock(&seq_demo_lock);

}

static int seq_demo_show(struct seq_file *s, void *v)

{

    struct seq_demo_node *node = list_entry(v, struct seq_demo_node, list);



    seq_printf(s, "name: %s, addr: 0x%p\n", node->name, node);



    return 0;

}

static const struct seq_operations seq_demo_ops = {

    .start = seq_demo_start,

    .next = seq_demo_next,

    .stop = seq_demo_stop,

    .show = seq_demo_show,

};


static int seq_demo_open(struct inode *inode, struct file *file)

{

    return seq_open(file, &seq_demo_ops);

}


static const struct file_operations seq_demo_fops = {

    .owner = THIS_MODULE,

    .open = seq_demo_open,

    .read = seq_read,

    .llseek = seq_lseek,

    .release = seq_release,

};



static int __init seq_demo_init(void)

{

    struct seq_demo_node *node;

    for (int i = 0; i < 7; i++) {

        node = kzalloc(sizeof(struct seq_demo_node), GFP_KERNEL);

        sprintf(node->name, "node%d", i);

        INIT_LIST_HEAD(&node->list);

        list_add_tail(&node->list, &seq_demo_list);

    }



    proc_create("seq_demo", 0444, NULL, &seq_demo_fops);

    return 0;

}



static void __exit seq_demo_exit(void)

{

    struct seq_demo_node *node_pos, *node_n;

	
    if (seq_demo_dir) {
        list_for_each_entry_safe(node_pos, node_n, &seq_demo_list, list)

            if (node_pos) {

                printk("%s: release %s\n", __func__, node_pos->name);

                kfree(node_pos);

            }

    }

}
module_init(seq_demo_init);

module_exit(seq_demo_exit);

MODULE_LICENSE("GPL");

实验代码分为三层,一是信息的定义,这里我们使用Linux链表;二是seq_file的定义,它描述如何读取这个信息;三是文件的定义,它实现标准的文件接口。
在这里插入图片描述

Linux 链表

Linux内置了一套优美的链表实现,它的核心是:LIST_HEAD

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Ga5SUXN-1584504612806)(F:\Users\iSIka\AppData\Roaming\Typora\typora-user-images\image-20200309210938835.png)]

可以看到,这是一个双向链表,结构体内部只有前后邻居,而没有数据,链表本身和链表所储存的数据分离。

在下面的实验中,我们会用到一个自定义结构体seq_demo_node:

struct seq_demo_node {

    char name[10];

    struct list_head list;

};

这就是list_head的用法:置身于自定义结构体内部。我们随时可以通过list_entry (listpoint,seq_demo_node,list)宏8得到包含它的结构体的指针,进而访问结构体私有数据。

对这样一个结构体的建立一般以这样的流程进行:

  1. 申请结构体需要的内存空间
  2. 写入数据
  3. 初始化链表指针,为了避免野指针。这个宏所作的工作就是list->prev=list,list->next=list。
  4. 将链表指针连接到链表头上。

这里的链表头是一开始用LIST_HEAD宏创建和初始化的seq_demo_list。

static LIST_HEAD(seq_demo_list);
struct seq_demo_node *node;

for (int i = 0; i < 7; i++) {

    node = kzalloc(sizeof(struct seq_demo_node), GFP_KERNEL);

    sprintf(node->name, "node%d", i);

    INIT_LIST_HEAD(&node->list);

    list_add_tail(&node->list, &seq_demo_list);

}

如果你的目的只是学习文件系统,而不关心链表怎么遍历和删除的话,可以暂且略过下一段,直接看实验代码

对于链表的遍历,有list_for_each_entry和list_for_each_entry_safe两种,后者用于边遍历边删除的场景。

在这里插入图片描述

可以看到,safe比普通方法多了next参数,在遍历之前就会将next赋值给一个临时变量,否则删除掉当前节点之后会无法找到next。

在我们的实验中,会使用safe在遍历的过程中释放节点。

struct seq_demo_node *node_pos, *node_n;
list_for_each_entry_safe(node_pos, node_n, &seq_demo_list, list)

            if (node_pos) {

                printk("%s: release %s\n", __func__, node_pos->name);

                kfree(node_pos);

            }

seq_file

seq_file的所有基本操作都是围绕着链表进行的,这从它要求的四个函数:start,next,stop和show可以看出来。即使你的信息本身并不是链表,以这四个函数处理也会比较好,毕竟内核信息大多是时间轴形式的,天生具有序列的性质。

start:根据索引编号pos找到对应的node,并返回该node的地址,也就是show和next方法里的v

next:根据当前node的地址和索引编号计算下一个node的地址和索引编号pos,返回值就是下一个节点的地址

show:输出传入的node的信息

stop:如果在start里有加锁,那么在这里需要释放锁

内核为链表形式的数据提供了标准的seq_list_start和seq_list_next,言简意赅的利用了链表的基本操作,而stop只需要管理一个mutex,实际上需要自己编写的只有show函数。

static int seq_demo_show(struct seq_file *s, void *v)

{

    struct seq_demo_node *node = list_entry(v, struct seq_demo_node, list);



    seq_printf(s, "name: %s, addr: 0x%p\n", node->name, node);



    return 0;

}

最后剩下的文件定义就很简单了,这里和实验1一样,只有open是自定义的,其他都使用seq_file通用接口。

open函数也仅仅是将seq_file结构体和传入seq_open。

static int seq_demo_open(struct inode *inode, struct file *file)

{

    return seq_open(file, &seq_demo_ops);

}

将模块编译装载后,可以在/Proc下访问链表文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eta2q942-1584504612808)(F:\Users\iSIka\AppData\Roaming\Typora\typora-user-images\image-20200309235514475.png)]

single

如果你的信息简单到不需要视为序列,可以考虑single系方法。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/mm.h>

MODULE_LICENSE("GPL");
int data=1;
static struct proc_dir_entry* procdir;
static int test_proc_show(struct seq_file *m, void *v){
	int* dp = (int*)m->private;
	seq_printf(m,"%d",*dp);
	return 0;
}
static int test_proc_open(struct inode* inode, struct file* file){
	return single_open(file,test_proc_show,PDE(inode)->datai);
}
static const struct file_operations test_proc_ops={
	.owner   = THIS_MODULE,
	.open    = test_proc_open,
	.read    = seq_read,
	.llseek  = seq_lseek,
	.release = single_release,
};

static int __init test_module_init(void){
	proc_create_data("proc_test1",0644,NULL,&test_proc_ops,&data);
	printk("init success\n");
	return 0;
}
static int __exit test_module_exit(void){
	printk("exit success\n");
}
module_init(test_module_init);
module_exit(test_module_exit);

前两种方法分别展示了文件不附带私有信息和附带链表信息,第三种方法我们让文件附带一个整数信息。

int data=1

由于附带信息,我们使用proc_create_data在/Proc创建文件,这个函数和proc_create唯一的区别就是它多了一个参数data,实际上proc_create就是通过将这个参数置为NULL实现的。

使用single方法,只需要将文件的open接口绑定为single_open并将自定义函数传入即可。

static int test_proc_show(struct seq_file *m, void *v){
	int* dp = (int*)m->private;
	seq_printf(m,"%d",*dp);
	return 0;
}
static int test_proc_open(struct inode* inode, struct file* file){
	return single_open(file,test_proc_show,PDE(inode)->datai);//通过PDE宏得到inode的私有数据
}

Conclusion

本文通过介绍三种实现/proc文件系统的方法,希望能增进对文件,文件系统的理解。

本文是作者<嵌入式操作系统设计>的课程报告,同步发表于作者博客,创作和传播过程均遵循CC4.0协议。


  1. 用Linus本人的话说:The whole point with "everything is a file" is not that you have some random filename (indeed, sockets and pipes show that "file" and "filename" have nothing to do with each other), but the fact that you can use common tools to operate on different things.(https://yarchive.net/comp/linux/everything_is_file.html) ↩︎

  2. 将文件a内容打印到终端 ↩︎

  3. 这也是它被称为Process文件系统的原因 ↩︎

  4. 在Linux2.6之后,原/Proc中和设备相关的部分被分离为sysfs,这是一个更加结构化的文件系统,通过kset,kobject等结构体以一种面向对象的方式组织设备,但我们今天不讨论它。 ↩︎

  5. https://www.kernel.org/doc/Documentation/filesystems/seq_file.txt ↩︎

  6. 选自:http://www.embeddedlinux.org.cn/emb-linux/file-system/201703/27-6340.html ↩︎

  7. 这是我不成熟的理解。 ↩︎

  8. **#define list_entry(ptr, struct, member)\ ((struct *)((char *)(ptr) – (unsigned long)(&((struct *)0)->member)))。**简单来说,(&((struct *)0)->member)计算的是member在struct中的偏移:首先将地址0转换为结构体指针,然后使用->member取得数据元素,再使用&取地址,得到的就是member相对0地址的偏移,也就是它的相对结构体的偏移,然后用ptr的值减去这个偏移,转换为struct指针类型。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值