一文帶你玩轉 proc

PART 1

前言

最近在做測試,怎麼實現的呢?通過實現了一個 KO,然後 KO 加載後就會註冊一個 proc 節點。然後通過 echo 給這個節點進行輸入指令,調用對應的文件節點,最後實現測試的函數調用。

這裏對 proc 這個東西一直感覺很神奇,很多 cat、cpu 等信息都可以從裏面獲取到。於是這裏來看看這個文件系統。

1、什麼是 proc 文件系統

2、常見的 proc 文件介紹

3、和 sys 文件系統的比較

4、怎麼註冊一個 proc 文件系統的節點

Linux 系統上的 / proc 目錄是一種文件系統,即 proc 文件系統 (procfs), 它以文件系統的方式爲用戶提供訪問系統內核數據的操作接口。

proc 文件系統是一種內核和內核模塊用來向進程 (process) 發送信息的機制,因此被稱爲 proc。

與其它常見的文件系統不同的是,proc 是一種僞文件系統 (也即虛擬文件系統),它只存在於內存當中,因此它會在系統啓動時創建並掛載到 / proc 目錄,在系統關閉時卸載並釋放。

下面是設備上 / proc 的掛載信息。

$ mount | grep proc
proc on /proc type proc (rw,relatime)

proc 文件系統存儲的是當前內核運行狀態的一系列特殊文件,用戶可以通過這些文件查看系統和進程的信息,或者改變內核的運行狀態,因此可以把它視爲 Linux 內核開放給用戶的控制和信息中心。實際上,proc 文件系統是內核空間和用戶空間之間的一種通信媒介。

使用 proc_create 實例分析

proc 虛擬文件系統也可以創建虛擬文件節點,實現用戶空間與內核空間的交互。

在驅動中創建節點,可以實現對硬件的控制。proc_create 函數原型(在 kernel-3.10/include/linux/proc_fs.h 文件)如下所示:

1-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);
}

如下代碼是我實現的一個 test 程序,可供參考學習 proc_create 的使用:

2-test 實例

#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/string.h>
#include <linux/wait.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/pinctrl/consumer.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
#include <linux/types.h>
#include <linux/proc_fs.h>

#define BUFSIZE  1024 

static char *buf;
static unsigned int len;

/***********************
 * file_operations->open
 * 無操作
 ***********************/

static int test_proc_open(struct inode *inode, struct file *file)
{
    return 0;
}

/************************
 * file_operations->read
 * 可以在adb工具進入機器的pro目錄,執行adb shell && cd proc && cat tets_rw,
 * 即可讀出節點test_rw的內容是12345
 ************************/

static ssize_t test_proc_read(struct file *file,
                char __user *buffer,size_t count, loff_t *f_pos) 
{
    if(*f_pos > 0)
        return 0;

printk("---start read---\n");
    printk("the string is >>>>> %s\n", buf);

if(copy_to_user(buffer, buf, len))
        return -EFAULT;
    *f_pos = *f_pos + len;
    return len;
}

/************************
 * file_operations->write
 * 可以在adb工具進入機器的pro目錄,
 * 執行adb shell && cd proc && echo 12345 > tets_rw,即可把12345寫入節點test_rw
 ************************/

static ssize_t test_proc_write(struct file *file, const char __user *buffer,
                                        size_t count, loff_t *f_pos) 
{

if(count <= 0)
        return -EFAULT;
    printk("---start write---\n");

len = count > BUFSIZE ? BUFSIZE : count;

// kfree memory by kmalloc before
    if(buf != NULL)
        kfree(buf);
    buf = (char*)kmalloc(len+1, GFP_KERNEL);
    if(buf == NULL)
    {
        printk("test_proc_create kmalloc fail!\n");
        return -EFAULT;
    }

//memset(buf, 0, sizeof(buf));
    memset(buf, 0, len+1);

if(copy_from_user(buf, buffer, len))
        return -EFAULT;
    printk("test_proc_write writing :%s",buf);
    return len;
}

static struct file_operations test_fops = {
    .owner  = THIS_MODULE,
    .open   = test_proc_open,
//  .release = single_release,
    .read   = test_proc_read,
//  .llseek = seq_lseek,
    .write  = test_proc_write,
};

static int __init test_init(void)
{
    struct proc_dir_entry* file;

//創建proc文件並關聯file_operations
    file = proc_create("test_rw", 0644, NULL, &test_fops);
    if (!file)
        return -ENOMEM;
    printk("test_rw init success!\n");
    return 0;
}

static void __exit test_exit(void)
{
    remove_proc_entry("test_rw", NULL);
    printk("test_exit\n");

}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("caizd");
MODULE_DESCRIPTION("Proc_create Test Driver");
MODULE_LICENSE("GPL");

3 - 試一下

可以將上面的代碼編譯成一個 ko,然後編近 kernel,然後驗證。

root@inwatch_portal:/ # cd proc
cd proc
root@inwatch_portal:/proc # echo 12345 > test_rw
echo 12345 > test_rw
root@inwatch_portal:/proc # cat test_rw
cat test_rw
12345

PART 2

proc 源碼

下面來跟着前輩來學習一下更進一步的關於 proc 源碼的東西。

一直以爲 PROC 文件系統很是晦澀難懂,平時僅僅是使用它,不願意去觸碰內核中的具體實現。今天突發奇想,想看看裏面究竟是怎麼實現的,結果…… 真是大跌眼鏡,沒想到裏面並不複雜

關於 PROC 文件系統的功能以及在 Linux 中的地位就不多說了,在用戶空間和內核空間交互的界面也扮演者舉足輕重的地位。

*proc_create

我們今天就從 proc_create 函數開始,看看其中的實現。該函數會創建一個 PROC entry,用戶可以通過對文件系統中的該文件,和內核進行數據的交互。

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);
}

簡要介紹下參數:

struct proc_dir_entry proc_root = {
.low_ino = PROC_ROOT_INO,
.namelen = 5,
.mode = S_IFDIR | S_IRUGO | S_IXUGO,
.nlink = 2,
.count = ATOMIC_INIT(1),
.proc_iops = &proc_root_inode_operations,
.proc_fops = &proc_root_operations,
.parent = &proc_root,
.name = "/proc",
};

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

proc_create_data

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 進行註冊,

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,然後插入到父目錄的子文件鏈表中,注意是頭插法。基本結構如下,其中每個子節點都有指向父節點的指針。

小結

其實創建 entry 的過程就這麼簡單,由於 PROC 也是一種文件系統,所以可以和 ext2/ext3 等文件系統一樣,作爲一個實體文件系統,通過 VFS 給用戶提供統一的接口。

相對於實體的文件系統而言,PROC 文件系統要簡單的多。因爲其不需要管理具體磁盤上的文件,不需要和硬件打交道。

正常情況下用戶發起文件操作流程爲:用戶層序 -> 系統調用 ->VFS 層 -> 具體文件系統 -> 磁盤驅動程序。

而針對 PROC 文件系統而言,其不需要和磁盤驅動打交道,最低層的部分就是操作系統各個子模塊提供的操作函數表。

這個就需要根據不同的模塊進行不同的操作了,所以都是某個模塊自己通過 PROC 的接口,向 PROC 註冊內容針對我們普通用戶添加的 entry,最低層的操作自然是我們註冊的 ops 函數表了。

PART 3

Porc 怎麼用

前面我們看了這個怎麼玩,這裏我們來看看怎麼用?

使用 “/proc”

在 Linux 系統中,“/proc” 文件系統十分有用,它被內核用於向用戶導出信息。“/proc” 文件系統是一個虛擬文件系統,通過它可以在 Linux 內核空間和用戶空間之間進行通信。在 / proc 文件系統中,我們可以將對虛擬文件的讀寫作爲與內核中實體進行通信的一種手段,與普通文件不同的是,這些虛擬文件的內容都是動態創建的。

“/proc”下的絕大多數文件是隻讀的,以顯示內核信息爲主。但是 “/proc” 下的文件也並不是完全只讀的,若節點可寫,還可用於一定的控制或配置目的,例如前面介紹的寫 / proc/sys/kernel/printk 可以改變 printk()的打印級別。

Linux 系統的許多命令本身都是通過分析 “/proc” 下的文件來完成的,如 ps、top、uptime 和 free 等。例如,free 命令通過分析 / proc/meminfo 文件得到可用內存信息,下面顯示了對應的 meminfo 文件和 free 命令的結果。

[root@localhost proc]# free
           total       used      free     shared    buffers     cached
Mem:       29516       28104     1412     0         4100        12700
-/+ buffers/cache:     11304     18212
Swap:      265064      0         265064

在 Linux 3.9 以及之前的內核版本中,可用如下函數創建 “/proc” 節點:

struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
            struct proc_dir_entry *parent);
struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode,
            struct proc_dir_entry *base, read_proc_t *read_proc, void * data);

create_proc_entry()函數用於創建 “/proc” 節點,而 create_proc_read_entry()調用 create_proc_entry()創建只讀的 “/proc” 節點。參數 name 爲 “/proc” 節點的名稱,parent/base 爲父目錄的節點,如果爲 NULL,則指 “/proc” 目錄,read_proc 是 “/proc” 節點的讀函數指針。當 read()系統調用在 “/proc” 文件系統中執行時,它映像到一個數據產生函數,而不是一個數據獲取函數。

下列函數用於創建 “/proc” 目錄:

struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);

結合 create_proc_entry()和 proc_mkdir(),代碼清單 21.5 中的程序可用於先在 / proc 下創建一個目錄 procfs_example,而後在該目錄下創建一個文件 example_file。代碼清單 21.5 proc_mkdir()和 create_proc_entry()函數使用範例

/* 創建/proc下的目錄 */
example_dir = proc_mkdir("procfs_example", NULL);
if (example_dir == NULL) {
rv =  -ENOMEM;
goto out;
}

example_dir->owner = THIS_MODULE;

/* 創建一個/proc文件 */
example_file = create_proc_entry("example_file", 0666, example_dir);
if (example_file == NULL) {
rv =  -ENOMEM;
goto out;
}

example_file->owner = THIS_MODULE;
example_file->read_proc = example_file_read;
example_file->write_proc = example_file_write;

作爲上述函數返回值的 proc_dir_entry 結構體包含了 “/proc” 節點的讀函數指針(read_proc_tread_proc)、寫函數指針(write_proc_twrite_proc)以及父節點、子節點信息等。/proc 節點的讀寫函數的類型分別爲:

typedef int (read_proc_t)(char *page, char **start, off_t off,
                                  int count, int *eof, void *data);
typedef int (write_proc_t)(struct file *file, const char __user *buffer,
                                  unsigned long count, void *data);

讀函數中 page 指針指向用於寫入數據的緩衝區,start 用於返回實際的數據並寫到內存頁的位置,eof 是用於返回讀結束標誌,offset 是讀的偏移,count 是要讀的數據長度。start 參數比較複雜,對於 / proc 只包含簡單數據的情況,通常不需要在讀函數中設置 * start,這意味着內核將認爲數據保存在內存頁偏移 0 的地方。

寫函數與 file_operations 中的 write()成員函數類似,需要一次從用戶緩衝區到內存空間的複製過程。在 Linux 系統中可用如下函數刪除 / proc 節點:

void remove_proc_entry(const char *name, struct proc_dir_entry *parent);

在 Linux 系統中已經定義好的可使用的 / proc 節點宏包括:proc_root_fs(/proc)、proc_net(/proc/net)、proc_bus(/proc/bus)、proc_root_driver(/proc/driver)等,proc_root_fs 實際上就是 NULL。

代碼清單 21.6 所示爲一個簡單的 “/proc” 文件系統使用範例,這段代碼在模塊加載函數中創建 / proc/test_dir 目錄,並在該目錄中創建 / proc/test_dir/test_rw 文件節點,在模塊卸載函數中撤銷 “/proc” 節點,而 / proc/test_dir/test_rw 文件中只保存了一個 32 位的整數。

 #include <linux/module.h>
 #include <linux/kernel.h>
 #include <linux/init.h>
 #include <linux/proc_fs.h>

 static unsigned int variable;
 static struct proc_dir_entry *test_dir, *test_entry;

 static int test_proc_read(char *buf, char **start, off_t off, int count,
      int *eof, void *data)
 {
 unsigned int *ptr_var = data;
 return sprintf(buf, "%u\n", *ptr_var);
 }

 static int test_proc_write(struct file *file, const char *buffer,
      unsigned long count, void *data)
 {
 unsigned int *ptr_var = data;

 *ptr_var = simple_strtoul(buffer, NULL, 10);

 return count;
 }

 static __init int test_proc_init(void)
{
 test_dir = proc_mkdir("test_dir", NULL);
 if (test_dir) {
     test_entry = create_proc_entry("test_rw", 0666, test_dir);
     if (test_entry) {
         test_entry->nlink = 1;
         test_entry->data = &variable;
         test_entry->read_proc = test_proc_read;
         test_entry->write_proc = test_proc_write;
         return 0;
     }
 }

 return -ENOMEM;
 }
 module_init(test_proc_init);

 static __exit void test_proc_cleanup(void)
{
 remove_proc_entry("test_rw", test_dir);
 remove_proc_entry("test_dir", NULL);
 }
 module_exit(test_proc_cleanup);

 MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
 MODULE_DESCRIPTION("proc exmaple");
 MODULE_LICENSE("GPL v2");

上述代碼第 21 行調用的 simple_strtoul()用於將用戶輸入的字符串轉換爲無符號長整數,第 3 個參數 10 意味着轉化方式是十進制。

編譯上述簡單的 proc.c 爲 proc.ko,運行 insmod proc.ko 加載該模塊後,“/proc” 目錄下將多出一個目錄 test_dir,該目錄下包含一個 test_rw,ls–l 的結果如下:

$ ls -l /proc/test_dir/test_rw
-rw-rw-rw- 1 root root 0 Aug 16 20:45 /proc/test_dir/test_rw
$ cat /proc/test_dir/test_rw
0echo 111 > /proc/test_dir/test_rw
$ cat /proc/test_dir/test_rw

說明我們上一步執行的寫操作是正確的。在 Linux 3.10 及以後的版本中,“/proc” 的內核 API 和實現架構變更較大,create_proc_entry()、create_proc_read_entry()之類的 API 都被刪除了,取而代之的是直接使用 proc_create()、proc_create_data()API。同時,也不再存在 read_proc()、write_proc()之類的針對 proc_dir_entry 的成員函數了,而是直接把 file_operations 結構體的指針傳入 proc_create()或者 proc_create_data()函數中,其原型爲:

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);
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);

我們把代碼清單 21.6 的範例改造爲同時支持 Linux 3.10 以前的內核和 Linux3.10 以後的內核。改造結果如代碼清單 21.7 所示。#if LINUX_VERSION_CODE<KERNEL_VERSION(3,10,0)中的部分是舊版本的代碼,與 21.6 相同,所以省略了。代碼清單 21.7 支持 Linux 3.10 以後內核的 / proc 文件系統使用範例

 #include <linux/module.h>
 #include <linux/kernel.h>
 #include <linux/init.h>
 #include <linux/version.h>
 #include <linux/proc_fs.h>
 #include <linux/seq_file.h>

 static unsigned int variable;
 static struct proc_dir_entry *test_dir, *test_entry;

 #if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
 ...
 #else
 static int test_proc_show(struct seq_file *seq, void *v)
 {
 unsigned int *ptr_var = seq->private;
 seq_printf(seq, "%u\n", *ptr_var);
 return 0;
 }

 static ssize_t test_proc_write(struct file *file, const char __user *buffer,
      size_t count, loff_t *ppos)
 {
 struct seq_file *seq = file->private_data;
 unsigned int *ptr_var = seq->private;

 *ptr_var = simple_strtoul(buffer, NULL, 10);
 return count;
 }

 static int test_proc_open(struct inode *inode, struct file *file)
 {
 return single_open(file, test_proc_show, PDE_DATA(inode));
 }

 static const struct file_operations test_proc_fops =
 {
 .owner = THIS_MODULE,
 .open = test_proc_open,
 .read = seq_read,
 .write = test_proc_write,
 .llseek = seq_lseek,
 .release = single_release,
 };
 #endif

 static __init int test_proc_init(void)
 {
 test_dir = proc_mkdir("test_dir", NULL);
 if (test_dir) {
 #if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
      ...
 #else
 test_entry = proc_create_data("test_rw",0666, test_dir, &test_proc_fops, &variable);
 if (test_entry)
     return 0;
 #endif
 }

 return -ENOMEM;
}
 module_init(test_proc_init);

 static __exit void test_proc_cleanup(void)
{
 remove_proc_entry("test_rw", test_dir);
 remove_proc_entry("test_dir", NULL);
 }
 module_exit(test_proc_cleanup);

參考資料

感謝下面前輩的優秀文章與書籍

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xmzddGhXCNb3uyhIN-waAA