一文帶你玩轉 proc
PART 1
前言
最近在做測試,怎麼實現的呢?通過實現了一個 KO,然後 KO 加載後就會註冊一個 proc 節點。然後通過 echo 給這個節點進行輸入指令,調用對應的文件節點,最後實現測試的函數調用。
這裏對 proc 這個東西一直感覺很神奇,很多 cat、cpu 等信息都可以從裏面獲取到。於是這裏來看看這個文件系統。
1、什麼是 proc 文件系統
-
(1) proc 是虛擬文件系統,虛擬的意思就是 proc 文件系統裏的文件不對應硬盤上任何文件,我們用去查看 proc 目錄下的文件大小都是零;
-
(2) proc 文件系統是開放給上層瞭解內核運行狀態的窗口,通過讀取 proc 系統裏的文件,可以知道內核中一些重要數據結構的數值,從而知道內核的運行情況,也可以方便調試內核和應用程序;
-
(3) proc 文件系統的思路:在內核中構建一個虛擬文件系統 / proc,內核運行時將內核中一些關鍵的數據結構以文件的方式呈現在 / proc 目錄中的一些特定文件中,這樣相當於將不可見的內核中的數據結構以可視化的方式呈現給內核的開發者。
-
(4) proc 文件系統是一種無存儲的文件系統,當讀其中的文件時,其內容動態生成,當寫文件時,文件所關聯的寫函數被調用。每個 proc 文件都關聯的字節特定的讀寫函數,因而它提供了另外的一種和內核通信的機制:內核部件可以通過該文件系統向用戶空間提供接口來提供查詢信息、修改軟件行爲,因而它是一種比較重要的特殊文件系統。
2、常見的 proc 文件介紹
3、和 sys 文件系統的比較
-
(1)proc 文件系統主要是用來調試內核,在內核運行時可以知道內核中一些重要的數據結構的值,一般都是讀很少寫;
-
(2)proc 文件系統出現的比 sys 文件系統早,proc 文件系統的目錄結構比較亂,在 proc 文件系統下面有很多文件夾,比如一個進程就有一個文件夾,現在內核越來越複雜,支持的設備類型也越來越多,顯得很混亂;於是又開發出了 sys 系統,sys 系統可以說是 proc 的升級,將來用 sys 系統會是主流;
-
(3)proc 文件系統和 sys 文件系統都是虛擬系統,並且有對應關係,比如 "/proc/misc" 對應於 "sys/class/misc" 下面的設備,都是描述 misc 類設備的;
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);
}
-
name:表示你要創建的設備節點的名稱,可隨意命名即可;
-
mode:表示節點的權限,一般賦值 0644;
-
parent:表示父節點,如果直接在 proc 目錄創建節點,直接賦值 NULL 即可;
-
proc_fops:表示與節點相關聯的 file_operations;
如下代碼是我實現的一個 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);
}
簡要介紹下參數:
-
name: 名字
-
mod: 模式
-
parent: 父 entry,爲 NULL 的話,默認父 entry 是 / proc
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_fops: 操作函數表
函數返回一個 proc_dir_entry。可以看到 proc_create 中直接調用了 proc_create_data,而該函數主要完成 2 個功能
-
1、調用__proc_create 完成具體 proc_dir_entry 的創建。
-
2、調用 proc_register 把 entry 註冊進系統。
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 命令的結果。
-
1.meminfo 文件
[root@localhost proc]# cat meminfo MemTotal: 29516 kB MemFree: 1472 kB Buffers: 4096 kB Cached: 12648 kB SwapCached: 0 kB Active: 14208 kB Inactive: 8844 kB HighTotal: 0 kB HighFree: 0 kB LowTotal: 29516 kB LowFree: 1472 kB SwapTotal: 265064 kB SwapFree: 265064 kB Dirty: 20 kB Writeback: 0 kB Mapped: 10052 kB Slab: 3864 kB CommitLimit: 279820 kB Committed_AS: 13760 kB PageTables: 444 kB VmallocTotal: 999416 kB VmallocUsed: 560 kB VmallocChunk: 998580 kB
-
2.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
0
$ echo 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);
參考資料
感謝下面前輩的優秀文章與書籍
-
https://zhuanlan.zhihu.com/p/584749553
-
https://www.elecfans.com/emb/202210101902598.html
-
https://zhuanlan.zhihu.com/p/584749553
-
https://zhuanlan.zhihu.com/p/557870063
-
https://www.cnblogs.com/ck1020/p/7475729.html
-
《Linux 設備驅動開發詳解》
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xmzddGhXCNb3uyhIN-waAA