在安裝新硬體到 Linux 系統之前,你會想要知道當前系統的資源配置狀況。 Linux 將這類資訊全集中在 /proc 檔案系統下。/proc 目錄下的檔案都是 Linux 核心虛擬出來的,當你讀取它們是,核心會實時提供檔案內容。藉由/proc,我們可得知系統的運作狀態。例如,從/proc/interrupt、/proc/dma、/proc/ioports這幾個檔案,可分別查出系統的中斷請求(IRQ)、DMA通道、I/O埠使用狀況的狀態。

介紹

/proc 檔案系統是一個虛擬檔案系統(沒有任何一部分與 磁碟相關,只存在記憶體中,不佔用外存空間),包含了一 些目錄和虛擬檔案

通過它可以在Linux核心空間和使用者空間之間進行通訊: 可以向用戶呈現核心中的一些資訊(用cat、more等命令 檢視/proc檔案中的資訊),也可以用作一種從使用者空間向核心傳送資訊的手段

LKM是用來展示 /proc 檔案系統的一種簡單方法,因為它可以動態地向 Linux 核心新增或刪除程式碼

直觀認識

這裡寫圖片描述
1
/proc 檔案系統可以用於獲取執行中的程序的資訊。在 /proc 中有一些編號的子目錄。每個編號的目錄對應一個程序 id (PID)。這樣,每一個執行中的程序 /proc 中都有一個用它的 PID 命名的目錄。這些子目錄中包含可以提供有關程序的狀態和環境的重要細節資訊的檔案,它們是讀取程序資訊的介面。

2
還有一些檔案的含義,如

apm # 高階電源管理資訊 
bus # 匯流排配置資訊(USB的配置也記錄在此) 
cmdline # 核心命令列 
Cpuinfo # 關於Cpu資訊 
Devices # 可以用到的裝置(塊裝置/字元裝置) 
Dma # 使用的DMA通道 
Filesystems # 支援的檔案系統 
Interrupts # 中斷的使用 
Ioports # I/O埠的使用 
Kcore # 核心核心印象 
Kmsg # 核心訊息 
Ksyms # 核心符號表 
Loadavg # 負載均衡 
Locks # 核心鎖 
Meminfo # 記憶體資訊 
Misc # 雜項 
Modules # 載入模組列表(可以想成是驅動程式) 
Mounts # 載入的檔案系統 
Partitions # 系統識別的分割槽表 
PCI # 在PCI總線上,每臺裝置的詳細情況(可以使用lspci來檢視) 
Rtc # 實時時鐘 
Slabinfo Slab # 池資訊 
stat # 全面統計狀態表
Swaps # 對換空間的利用情況 
Version # 核心版本 
Uptime # 系統正常執行時間 

3
在/proc下還有三個很重要的目錄:net,scsisys。sys目錄是可寫的,可以通過它來訪問或修改核心的引數,而net和scsi則依賴於核心配置。例如,如果系統不支援scsi,則scsi 目錄不存在。

練習

核心中分別找出一處proc和seqfile的完整使用過程並記錄下來

proc

proc_create函式開始,看看其中的實現

static inline struct proc_dir_entry *proc_create(
    const char *name, umode_t mode, struct proc_dir_entry *parent,
    const struct file_operations *proc_fops)
{
    return proc_create_data(name, mode, parent, proc_fops, NULL);
}

這裡寫圖片描述

函式返回一個proc_dir_entry。可以看到proc_create中直接呼叫了proc_create_data,而該函式主要完成2個功能

  1. 呼叫__proc_create完成具體proc_dir_entry的建立。
  2. 呼叫proc_register把entry註冊進系統。
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
                    struct proc_dir_entry *parent,
                    const struct file_operations *proc_fops,
                    void *data)
{
    struct proc_dir_entry *pde;
    if ((mode & S_IFMT) == 0)
        mode |= S_IFREG;

    if (!S_ISREG(mode)) {
        WARN_ON(1);    /* use proc_mkdir() */
        return NULL;
    }

    if ((mode & S_IALLUGO) == 0)
        mode |= S_IRUGO;
    pde = __proc_create(&parent, name, mode, 1);
    if (!pde)
        goto out;
    pde->proc_fops = proc_fops;
    pde->data = data;
    if (proc_register(parent, pde) < 0)
        goto out_free;
    return pde;
out_free:
    kfree(pde);
out:
    return NULL;
}

先看proc_dir_entry的建立,這裡通過__proc_create函式,其實該函式內部也很簡單,就是為entry分配了空間,並對相關欄位進行設定,主要包含name,namelen,mod,nlink等。建立好後,就設定操作函式proc_fops和data。然後就呼叫proc_register進行註冊,

static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp)
{
    struct proc_dir_entry *tmp;
    int ret;

    ret = proc_alloc_inum(&dp->low_ino);
    if (ret)
        return ret;
     /*如果是 目錄*/
    if (S_ISDIR(dp->mode)) {
        dp->proc_fops = &proc_dir_operations;
        dp->proc_iops = &proc_dir_inode_operations;
        dir->nlink++;
        /*如果是連結*/
    } else if (S_ISLNK(dp->mode)) {
        dp->proc_iops = &proc_link_inode_operations;
        /*如果是檔案*/
    } else if (S_ISREG(dp->mode)) {
        BUG_ON(dp->proc_fops == NULL);
        dp->proc_iops = &proc_file_inode_operations;
    } else {
        WARN_ON(1);
        return -EINVAL;
    }

    spin_lock(&proc_subdir_lock);

    for (tmp = dir->subdir; tmp; tmp = tmp->next)
        if (strcmp(tmp->name, dp->name) == 0) {
            WARN(1, "proc_dir_entry '%s/%s' already registered\n",
                dir->name, dp->name);
            break;
        }
    /*子dir連結成連結串列,且子dir中含有父dir的指標*/
    dp->next = dir->subdir;
    dp->parent = dir;
    dir->subdir = dp;
    spin_unlock(&proc_subdir_lock);

    return 0;
}

函式首先分配一個inode number,然後根據entry的型別對其進行操作函式賦值,主要分為目錄、連結、檔案。這裡我們只關注檔案,檔案的操作函式一般由使用者自己定義,即上面我們設定的ops,這裡僅僅是設定inode操作函式表,設定成了全域性的proc_file_inode_operations,然後插入到父目錄的子檔案連結串列中,注意是頭插法。基本結構如下,其中每個子節點都有指向父節點的指標。
(轉自 忘存地址了)

seqfile

UNIX的世界裡,檔案是最普通的概念,所以用檔案來作為核心和使用者空間傳遞資料的介面也是再普通不過的事情,並且這樣的介面對於shell也是相當友好的,方便管理員通過shell直接管理系統。由於偽檔案系統proc檔案系統在處理大資料結構(大於一頁的資料)方面有比較大的侷限性,使得在那種情況下進行程式設計特別彆扭,很容易導致bug,所以序列檔案介面被髮明出來,它提供了更加友好的介面,以方便程式設計師。
這裡寫圖片描述

struct seq_file {
    char *buf;          //seq_file介面使用的快取頁指標
    size_t size;        //seq_file介面使用的快取頁大小
    size_t from;        //從seq_file中向用戶態緩衝區拷貝時相對於buf的偏移地址
    size_t count;       //buf中可以拷貝到使用者態的字元數目 
    loff_t index;       //start、next的處理的下標pos數值
    loff_t read_pos;    //當前已拷貝到使用者態的資料量大小
    u64 version;
    struct mutex lock;      //針對此seq_file操作的互斥鎖,所有seq_*的訪問都會上鎖
    const struct seq_operations *op; //操作實際底層資料的函式
    void *private;
};

在這個結構體中,幾乎所有的成員都是由seq_file內部實現來處理的,程式設計師不用去關心,除非你要去研究seq_file的內部原理。對於這個結構體,程式設計師唯一要做的就是實現其中的const struct seq_operations *op。為使用 seq_file介面對於不同資料結構體進行訪問,你必須建立一組簡單的物件迭代操作函式。

seq_file內部機制使用這些介面函式訪問底層的實際資料結構體,並不斷沿資料序列向前,同時逐個輸出序列裡的資料到seq_file自建的快取(大小為一頁)中。也就是說seq_file內部機制幫你實現了對序列資料的讀取和放入快取的機制,你只需要實現底層的迭代函式介面就好了,因為這些是和你要訪問的底層資料相關的,而seq_file屬於上層抽象。這可能看起來有點複雜,大家看了下面的圖就好理解了:

struct seq_operations {
    void * (*start) (struct seq_file *m, loff_t *pos);
    void (*stop) (struct seq_file *m, void *v);
    void * (*next) (struct seq_file *m, void *v, loff_t *pos);
    int (*show) (struct seq_file *m, void *v);
};
void * (*start) (struct seq_file *m, loff_t *pos);

start 方法會首先被呼叫,它的作用是在設定訪問的起始點。
m:指向的是本seq_file的結構體,在正常情況下無需處理。
pos:是一個整型位置值,指示開始讀取的位置。對於這個位置的意義完全取決於底層實現,不一定是位元組為單位的位置,可能是一個元素的序列號。
返回值如果非NULL,則是一個指向迭代器實現的私有資料結構體指標。如果訪問出錯則返回NULL。

設定好了訪問起始點,seq_file內部機制可能會使用show方法獲取start返回值指向的結構體中的資料到內部快取,並適時送往使用者空間。

int (*show) (struct seq_file *m, void *v);

所以show方法就是負責將v指向元素中的資料輸出到seq_file的內部快取,但是其中必須藉助seq_file提供的一些類似printf的介面函式:

int seq_printf(struct seq_file *sfile, const char *fmt, ...);
//專為 seq_file 實現的類似 printf 的函式;用於將資料常用的格式串和附加值引數.
//你必須將給 show 函式的 set_file 結構指標傳遞給它。如果seq_printf 返回-1,意味著快取區已滿,部分輸出被丟棄。但是大部分時候都忽略了其返回值。

int seq_putc(struct seq_file *sfile, char c);
int seq_puts(struct seq_file *sfile, const char *s);
//類似 putc 和 puts 函式的功能,sfile引數和返回值與 seq_printf相同。
int seq_escape(struct seq_file *m, const char *s, const char *esc);
//這個函式類似 seq_puts ,但是它會將 s 中所有在 esc 中出現的字元以八進位制格式輸出到快取。
//esc 的常用值是"\t\n\\", 它使內嵌的空格不會搞亂輸出或迷惑 shell 指令碼.

int seq_write(struct seq_file *seq, const void *data, size_t len) 
//直接將data指向的資料寫入seq_file快取,資料長度為len。用於非字串資料。

int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);
//這個函式能夠用來輸出給定目錄項關聯的檔名,驅動極少使用。  

在show函式返回之後,seq_file機制可能需要移動到下一個資料元素,那就必須使用next方法

void * (*next) (struct seq_file *m, void *v, loff_t *pos);

在next實現中應當遞增pos指向的值,但是具體遞增的數量和迭代器的實現有關,不一定是1。而next的返回值如果非NULL,則是下一個需要輸出到快取的元素指標,否則表明已經輸出結束,將會呼叫stop方法做清理。

void (*stop) (struct seq_file *m, void *v);

在stop實現中,引數m指向本seq_file的結構體,在正常情況下無需處理。而v是指向上一個next或start返回的元素指標。在需要做退出處理的時候才需要實現具體的功能。但是許多情況下可以直接返回。

在next和start的實現中可能需要對一個序列的函式進行遍歷,而在核心中,對於一個序列資料結構體的實現一般是使用雙向連結串列或者雜湊連結串列,所有seq_file同時提供了一些對於核心雙向連結串列和雜湊連結串列的封裝介面函式,方便程式設計師實現對於通過連結串列連結的結構體序列的操作。這些函式名一般是seq_list_*或者seq_hlist_*,這些函式的實現都在fs/seq_file.c中,有興趣的朋友可以看看。我在後面的實驗中依然使用核心通用的雙向連結串列API。

在使用者空間進行“讀”、“寫”

“讀”

使用cat,讀程序2的狀態資訊,比如PID 、PPID、State等
這裡寫圖片描述

“寫”

使用echo,修改hostname
這裡寫圖片描述