使用 /proc 檔案系統來訪問 Linux 核心的內容
最初開發 /proc 檔案系統是為了提供有關係統中程序的資訊。但是由於這個檔案系統非常有用,因此核心中的很多元素也開始使用它來報告資訊,或啟用動態執行時配置。
/proc 檔案系統包含了一些目錄(用作組織資訊的方式)和虛擬檔案。虛擬檔案可以向用戶呈現核心中的一些資訊,也可以用作一種從使用者空間向核心傳送資訊的手段。實際上我們並不會同時需要實現這兩點,但是本文將向您展示如何配置這個檔案系統進行輸入和輸出。
儘管像本文這樣短小的一篇文章無法詳細介紹 /proc 的所有用法,但是它依然對這兩種用法進行了展示,從而可以讓我們體會一下 /proc 是多麼強大。清單 1 是對 /proc 中部分元素進行一次互動查詢的結果。它顯示的是 /proc 檔案系統的根目錄中的內容。注意,在左邊是一系列數字編號的檔案。每個實際上都是一個目錄,表示系統中的一個程序。由於在 GNU/Linux 中建立的第一個程序是 init
process-id
為 1。然後對這個目錄執行一個 ls
命令,這會顯示很多檔案。每個檔案都提供了有關這個特殊程序的詳細資訊。例如,要檢視init
的
command-line 項的內容,只需對 cmdline
檔案執行 cat
命令。
/proc 中另外一些有趣的檔案有:cpuinfo
,它標識了處理器的型別和速度;pci
,顯示在
PCI 總線上找到的裝置;modules
,標識了當前載入到核心中的模組。
清單 1. 對 /proc 的互動過程
[[email protected]]# ls /proc 1 2040 2347 2874 474 fb mdstat sys 104 2061 2356 2930 9 filesystems meminfo sysrq-trigger 113 2073 2375 2933 acpi fs misc sysvipc 1375 21 2409 2934 buddyinfo ide modules tty 1395 2189 2445 2935 bus interrupts mounts uptime 1706 2201 2514 2938 cmdline iomem mtrr version 179 2211 2515 2947 cpuinfo ioports net vmstat 180 2223 2607 3 crypto irq partitions 181 2278 2608 3004 devices kallsyms pci 182 2291 2609 3008 diskstats kcore self 2 2301 263 3056 dma kmsg slabinfo 2015 2311 2805 394 driver loadavg stat 2019 2337 2821 4 execdomains locks swaps [[email protected] 1]# ls /proc/1 auxv cwd exe loginuid mem oom_adj root statm task cmdline environ fd maps mounts oom_score stat status wchan [[email protected]]# cat /proc/1/cmdline init [5] [[email protected]]#
清單 2 展示了對 /proc 中的一個虛擬檔案進行讀寫的過程。這個例子首先檢查核心的 TCP/IP 棧中的 IP 轉發的目前設定,然後再啟用這種功能。
清單 2. 對 /proc 進行讀寫(配置核心)
[[email protected]]# cat /proc/sys/net/ipv4/ip_forward 0 [[email protected]]# echo "1" > /proc/sys/net/ipv4/ip_forward [[email protected]]# cat /proc/sys/net/ipv4/ip_forward 1 [[email protected]]#
另外,我們還可以使用 sysctl
來配置這些核心條目。有關這個問題的更多資訊,請參閱 參考資料 一節的內容。
順便說一下,/proc 檔案系統並不是 GNU/Linux 系統中的惟一一個虛擬檔案系統。在這種系統上,sysfs 是一個與 /proc 類似的檔案系統,但是它的組織更好(從 /proc 中學習了很多教訓)。不過 /proc 已經確立了自己的地位,因此即使 sysfs 與 /proc 相比有一些優點,/proc 也依然會存在。還有一個 debugfs 檔案系統,不過(顧名思義)它提供的更多是除錯介面。debugfs 的一個優點是它將一個值匯出給使用者空間非常簡單(實際上這不過是一個呼叫而已)。
核心模組簡介
可載入核心模組(LKM)是用來展示 /proc 檔案系統的一種簡單方法,這是因為這是一種用來動態地向 Linux 核心新增或刪除程式碼的新方法。LKM 也是 Linux 核心中為裝置驅動程式和檔案系統使用的一種流行機制。
如果您曾經重新編譯過 Linux 核心,就可能會發現在核心的配置過程中,有很多裝置驅動程式和其他核心元素都被編譯成了模組。如果一個驅動程式被直接編譯到了核心中,那麼即使這個驅動程式沒有執行,它的程式碼和靜態資料也會佔據一部分空間。但是如果這個驅動程式被編譯成一個模組,就只有在需要記憶體並將其載入到核心時才會真正佔用記憶體空間。有趣的是,對於 LKM 來說,我們不會注意到有什麼效能方面的差異,因此這對於建立一個適應於自己環境的核心來說是一種功能強大的手段,這樣可以根據可用硬體和連線的裝置來載入對應的模組。
下面是一個簡單的 LKM,可以幫助您理解它與在 Linux 核心中看到的標準(非動態可載入的)程式碼之間的區別。清單 3 給出了一個最簡單的 LKM。(可以從本文的 下載 一節中下載這個程式碼)。
清單 3 包括了必須的模組頭(它定義了模組的 API、型別和巨集)。然後使用 MODULE_LICENSE
定義了這個模組使用的許可證。此處,我們定義的是 GPL,從而防止會汙染到核心。
清單 3 然後又定義了這個模組的 init
和 cleanup
函式。my_module_init
函式是在載入這個模組時被呼叫的,它用來進行一些初始化方面的工作。my_module_cleanup
函式是在解除安裝這個模組時被呼叫的,它用來釋放記憶體並清除這個模組的蹤跡。注意此處 printk
的用法:這是核心的 printf
函式。KERN_INFO
符號是一個字串,可以用來對進入核心迴環緩衝區的資訊進行過濾(非常類似於 syslog
)。
最後,清單 3 使用 module_init
和 module_exit
巨集聲明瞭入口函式和出口函式。這樣我們就可以按照自己的意願來對這個模組的 init
和cleanup
函式進行命名了,不過我們最終要告訴核心維護函式就是這些函式。
清單 3. 一個簡單的但可以正常工作的 LKM(simple-lkm.c)
#include <linux/module.h> /* Defines the license for this LKM */ MODULE_LICENSE("GPL"); /* Init function called on module entry */ int my_module_init( void ) { printk(KERN_INFO "my_module_init called. Module is now loaded.\n"); return 0; } /* Cleanup function called on module exit */ void my_module_cleanup( void ) { printk(KERN_INFO "my_module_cleanup called. Module is now unloaded.\n"); return; } /* Declare entry and exit functions */ module_init( my_module_init ); module_exit( my_module_cleanup );
清單 3 儘管非常簡單,但它卻是一個真正的 LKM。現在讓我們對其進行編譯並在一個 2.6 版本的核心上進行測試。2.6 版本的核心為核心模組的編譯引入了一種新方法,我發現這種方法比原來的方法簡單了很多。對於檔案 simple-lkm.c
,我們可以建立一個
makefile,其惟一內容如下:
obj-m += simple-lkm.o
要編譯 LKM,請使用 make
命令,如清單
4 所示。
清單 4. 編譯 LKM
[[email protected]]# make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules make: Entering directory `/usr/src/linux-2.6.11' CC [M] /root/projects/misc/module2.6/simple/simple-lkm.o Building modules, stage 2. MODPOST CC /root/projects/misc/module2.6/simple/simple-lkm.mod.o LD [M] /root/projects/misc/module2.6/simple/simple-lkm.ko make: Leaving directory `/usr/src/linux-2.6.11' [[email protected]]#
結果會生成一個 simple-lkm.ko
檔案。這個新的命名約定可以幫助將這些核心物件(LKM)與標準物件區分開來。現在可以載入或解除安裝這個模組了,然後可以檢視它的輸出。要載入這個模組,請使用 insmod
命令;反之,要解除安裝這個模組,請使用 rmmod
命令。lsmod
可以顯示當前載入的
LKM(參見清單 5)。
清單 5. 插入、檢查和刪除 LKM
[[email protected]]# insmod simple-lkm.ko [[email protected]]# lsmod Module Size Used by simple_lkm 1536 0 autofs4 26244 0 video 13956 0 button 5264 0 battery 7684 0 ac 3716 0 yenta_socket 18952 3 rsrc_nonstatic 9472 1 yenta_socket uhci_hcd 32144 0 i2c_piix4 7824 0 dm_mod 56468 3 [[email protected]]# rmmod simple-lkm [[email protected]]#
注意,核心的輸出進到了核心迴環緩衝區中,而不是列印到 stdout
上,這是因為 stdout
是程序特有的環境。要檢視核心迴環緩衝區中的訊息,可以使用 dmesg
工具(或者通過
/proc 本身使用 cat
/proc/kmsg
命令)。清單 6 給出了 dmesg
顯示的最後幾條訊息。
清單 6. 檢視來自 LKM 的核心輸出
[[email protected]]# dmesg | tail -5 cs: IO port probe 0xa00-0xaff: clean. eth0: Link is down eth0: Link is up, running at 100Mbit half-duplex my_module_init called. Module is now loaded. my_module_cleanup called. Module is now unloaded. [[email protected]]#
可以在核心輸出中看到這個模組的訊息。現在讓我們暫時離開這個簡單的例子,來看幾個可以用來開發有用 LKM 的核心 API。
整合到 /proc 檔案系統中
核心程式設計師可以使用的標準 API,LKM 程式設計師也可以使用。LKM 甚至可以匯出核心使用的新變數和函式。有關 API 的完整介紹已經超出了本文的範圍,因此我們在這裡只是簡單地介紹後面在展示一個更有用的 LKM 時所使用的幾個元素。
建立並刪除 /proc 項
要在 /proc 檔案系統中建立一個虛擬檔案,請使用 create_proc_entry
函式。這個函式可以接收一個檔名、一組許可權和這個檔案在
/proc 檔案系統中出現的位置。create_proc_entry
的返回值是一個 proc_dir_entry
指標(或者為 NULL,說明在 create
時發生了錯誤)。然後就可以使用這個返回的指標來配置這個虛擬檔案的其他引數,例如在對該檔案執行讀操作時應該呼叫的函式。create_proc_entry
的原型和 proc_dir_entry
結構中的一部分如清單
7 所示。
清單 7. 用來管理 /proc 檔案系統項的元素
struct proc_dir_entry *create_proc_entry( const char *name, mode_t mode, struct proc_dir_entry *parent ); struct proc_dir_entry { const char *name; // virtual file name mode_t mode; // mode permissions uid_t uid; // File's user id gid_t gid; // File's group id struct inode_operations *proc_iops; // Inode operations functions struct file_operations *proc_fops; // File operations functions struct proc_dir_entry *parent; // Parent directory ... read_proc_t *read_proc; // /proc read function write_proc_t *write_proc; // /proc write function void *data; // Pointer to private data atomic_t count; // use count ... }; void remove_proc_entry( const char *name, struct proc_dir_entry *parent );
稍後我們就可以看到如何使用 read_proc
和 write_proc
命令來插入對這個虛擬檔案進行讀寫的函式。
要從 /proc 中刪除一個檔案,可以使用 remove_proc_entry
函式。要使用這個函式,我們需要提供檔名字串,以及這個檔案在
/proc 檔案系統中的位置(parent)。這個函式原型如清單 7 所示。
parent 引數可以為 NULL(表示 /proc 根目錄),也可以是很多其他值,這取決於我們希望將這個檔案放到什麼地方。表 1 列出了可以使用的其他一些父 proc_dir_entry
,以及它們在這個檔案系統中的位置。
表 1. proc_dir_entry 快捷變數
proc_dir_entry | 在檔案系統中的位置 |
---|---|
proc_root_fs |
/proc |
proc_net |
/proc/net |
proc_bus |
/proc/bus |
proc_root_driver |
/proc/driver |
回撥函式
我們可以使用 write_proc
函式向
/proc 中寫入一項。這個函式的原型如下:
int mod_write( struct file *filp, const char __user *buff, unsigned long len, void *data );
filp
引數實際上是一個開啟檔案結構(我們可以忽略這個引數)。buff
引數是傳遞給您的字串資料。緩衝區地址實際上是一個使用者空間的緩衝區,因此我們不能直接讀取它。len
引數定義了在 buff
中有多少資料要被寫入。data
引數是一個指向私有資料的指標(參見
清單 7)。在這個模組中,我們聲明瞭一個這種型別的函式來處理到達的資料。
Linux 提供了一組 API 來在使用者空間和核心空間之間移動資料。對於 write_proc
的情況來說,我們使用了 copy_from_user
函式來維護使用者空間的資料。
讀回撥函式
我們可以使用 read_proc
函式從一個
/proc 項中讀取資料(從核心空間到使用者空間)。這個函式的原型如下:
int mod_read( char *page, char **start, off_t off, int count, int *eof, void *data );
page
引數是這些資料寫入到的位置,其中 count
定義了可以寫入的最大字元數。在返回多頁資料(通常一頁是
4KB)時,我們需要使用start
和 off
引數。當所有資料全部寫入之後,就需要設定 eof
(檔案結束引數)。與 write
類似,data
表示的也是私有資料。此處提供的 page
緩衝區在核心空間中。因此,我們可以直接寫入,而不用呼叫 copy_to_user
。
其他有用的函式
我們還可以使用 proc_mkdir
、symlinks
以及 proc_symlink
在
/proc 檔案系統中建立目錄。對於只需要一個 read
函式的簡單
/proc 項來說,可以使用 create_proc_read_entry
,這會建立一個
/proc 項,並在一個呼叫中對 read_proc
函式進行初始化。這些函式的原型如清單
8 所示。
清單 8. 其他有用的 /proc 函式
/* Create a directory in the proc filesystem */ struct proc_dir_entry *proc_mkdir( const char *name, struct proc_dir_entry *parent ); /* Create a symlink in the proc filesystem */ struct proc_dir_entry *proc_symlink( const char *name, struct proc_dir_entry *parent, const char *dest ); /* Create a proc_dir_entry with a read_proc_t in one call */ 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 ); /* Copy buffer to user-space from kernel-space */ unsigned long copy_to_user( void __user *to, const void *from, unsigned long n ); /* Copy buffer to kernel-space from user-space */ unsigned long copy_from_user( void *to, const void __user *from, unsigned long n ); /* Allocate a 'virtually' contiguous block of memory */ void *vmalloc( unsigned long size ); /* Free a vmalloc'd block of memory */ void vfree( void *addr ); /* Export a symbol to the kernel (make it visible to the kernel) */ EXPORT_SYMBOL( symbol ); /* Export all symbols in a file to the kernel (declare before module.h) */ EXPORT_SYMTAB
通過 /proc 檔案系統實現財富分發
下面是一個可以支援讀寫的 LKM。這個簡單的程式提供了一個財富甜點分發。在載入這個模組之後,使用者就可以使用 echo
命令向其中匯入文字財富,然後再使用 cat
命令逐一讀出。
清單 9 給出了基本的模組函式和變數。init
函式(init_fortune_module
)負責使用 vmalloc
來為這個點心罐分配空間,然後使用memset
將其全部清零。使用所分配並已經清空的 cookie_pot
記憶體,我們在
/proc 中建立了一個 proc_dir_entry
項,並將其稱為fortune。當 proc_entry
成功建立之後,對自己的本地變數和 proc_entry
結構進行了初始化。我們載入了
/proc read
和 write
函式(如清單
9 和清單 10 所示),並確定這個模組的所有者。cleanup
函式簡單地從
/proc 檔案系統中刪除這一項,然後釋放 cookie_pot
所佔據的記憶體。
cookie_pot
是一個固定大小(4KB)的頁,它使用兩個索引進行管理。第一個是 cookie_index
,標識了要將下一個
cookie 寫到哪裡去。變數 next_fortune
標識了下一個
cookie 應該從哪裡讀取以便進行輸出。在所有的 fortune 項都讀取之後,我們簡單地回到了next_fortune
。
清單 9. 模組的 init/cleanup 和變數
#include <linux/module.h> #include <linux/kernel.h> #include <linux/proc_fs.h> #include <linux/string.h> #include <linux/vmalloc.h> #include <asm/uaccess.h> MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Fortune Cookie Kernel Module"); MODULE_AUTHOR("M. Tim Jones"); #define MAX_COOKIE_LENGTH PAGE_SIZE static struct proc_dir_entry *proc_entry; static char *cookie_pot; // Space for fortune strings static int cookie_index; // Index to write next fortune static int next_fortune; // Index to read next fortune int init_fortune_module( void ) { int ret = 0; cookie_pot = (char *)vmalloc( MAX_COOKIE_LENGTH ); if (!cookie_pot) { ret = -ENOMEM; } else { memset( cookie_pot, 0, MAX_COOKIE_LENGTH ); proc_entry = create_proc_entry( "fortune", 0644, NULL ); if (proc_entry == NULL) { ret = -ENOMEM; vfree(cookie_pot); printk(KERN_INFO "fortune: Couldn't create proc entry\n"); } else { cookie_index = 0; next_fortune = 0; proc_entry->read_proc = fortune_read; proc_entry->write_proc = fortune_write; proc_entry->owner = THIS_MODULE; printk(KERN_INFO "fortune: Module loaded.\n"); } } return ret; } void cleanup_fortune_module( void ) { remove_proc_entry("fortune", &proc_root); vfree(cookie_pot); printk(KERN_INFO "fortune: Module unloaded.\n"); } module_init( init_fortune_module ); module_exit( cleanup_fortune_module );
向這個罐中新寫入一個 cookie 非常簡單(如清單 10 所示)。使用這個寫入 cookie 的長度,我們可以檢查是否有這麼多空間可用。如果沒有,就返回 -ENOSPC
,它會返回給使用者空間。否則,就說明空間存在,我們使用 copy_from_user
將使用者緩衝區中的資料直接拷貝到cookie_pot
中。然後增大 cookie_index
(基於使用者緩衝區的長度)並使用
NULL 來結束這個字串。最後,返回實際寫入 cookie_pot
的字元的個數,它會返回到使用者程序。
清單 10. 對 fortune 進行寫入操作所使用的函式
ssize_t fortune_write( struct file *filp, const char __user *buff,
unsigned long len, void *data )
{
int space_available = (MAX_COOKIE_LENGTH-cookie_index)+1;
if (len > space_available) {
printk(KERN_INFO "fortune: cookie pot is full!\n");
return -ENOSPC;
}
if (copy_from_user( &cookie_pot[cookie_index], buff, len )) {
return -EFAULT;
}
cookie_index += len;
cookie_pot[cookie_index-1] = 0;
return len;
}
對 fortune 進行讀取也非常簡單,如清單 11 所示。由於我們剛才寫入資料的緩衝區(page
)已經在核心空間中了,因此可以直接對其進行操作,並使用 sprintf
來寫入下一個
fortune。如果 next_fortune
索引大於 cookie_index
(要寫入的下一個位置),那麼我們就將next_fortune
返回為
0,這是第一個 fortune 的索引。在將這個 fortune 寫入使用者緩衝區之後,在 next_fortune
索引上增加剛才寫入的
fortune 的長度。這樣就變成了下一個可用 fortune 的索引。這個 fortune 的長度會被返回並傳