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指針類型。 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章