歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

在 Linux 下用戶空間與內核空間數據交換的方式,第 2 部分: procfs、seq_file、debugfs和relayfs


一、procfs

procfs是比較老的一種用戶態與內核態的數據交換方式,內核的很多數據都是通過這種方式出口給用戶的,內核的很多參數也是通過這種方式來讓用戶方便設置的。除了sysctl出口到/proc下的參數,procfs提供的大部分內核參數是只讀的。實際上,很多應用嚴重地依賴於procfs,因此它幾乎是必不可少的組件。前面部分的幾個例子實際上已經使用它來出口內核數據,但是並沒有講解如何使用,本節將講解如何使用procfs。
Procfs提供了如下API:

struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
                                          struct proc_dir_entry *parent)

該函數用於創建一個正常的proc條目,參數name給出要建立的proc條目的名稱,參數mode給出了建立的該proc條目的訪問權限,參數parent指定建立的proc條目所在的目錄。如果要在/proc下建立proc條目,parent應當為NULL。否則它應當為proc_mkdir返回的struct proc_dir_entry結構的指針。


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

該函數用於刪除上面函數創建的proc條目,參數name給出要刪除的proc條目的名稱,參數parent指定建立的proc條目所在的目錄。


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

該函數用於創建一個proc目錄,參數name指定要創建的proc目錄的名稱,參數parent為該proc目錄所在的目錄。


extern struct proc_dir_entry *proc_mkdir_mode(const char *name, mode_t mode,
                        struct proc_dir_entry *parent);
struct proc_dir_entry *proc_symlink(const char * name,
                struct proc_dir_entry * parent, const char * dest)

該函數用於建立一個proc條目的符號鏈接,參數name給出要建立的符號鏈接proc條目的名稱,參數parent指定符號連接所在的目錄,參數dest指定鏈接到的proc條目名稱。


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)

該函數用於建立一個規則的只讀proc條目,參數name給出要建立的proc條目的名稱,參數mode給出了建立的該proc條目的訪問權限,參數base指定建立的proc條目所在的目錄,參數read_proc給出讀去該proc條目的操作函數,參數data為該proc條目的專用數據,它將保存在該proc條目對應的struct file結構的private_data字段中。


struct proc_dir_entry *create_proc_info_entry(const char *name,
        mode_t mode, struct proc_dir_entry *base, get_info_t *get_info)

該函數用於創建一個info型的proc條目,參數name給出要建立的proc條目的名稱,參數mode給出了建立的該proc條目的訪問權限,參數base指定建立的proc條目所在的目錄,參數get_info指定該proc條目的get_info操作函數。實際上get_info等同於read_proc,如果proc條目沒有定義個read_proc,對該proc條目的read操作將使用get_info取代,因此它在功能上非常類似於函數create_proc_read_entry。


struct proc_dir_entry *proc_net_create(const char *name,
        mode_t mode, get_info_t *get_info)

該函數用於在/proc/net目錄下創建一個proc條目,參數name給出要建立的proc條目的名稱,參數mode給出了建立的該proc條目的訪問權限,參數get_info指定該proc條目的get_info操作函數。


struct proc_dir_entry *proc_net_fops_create(const char *name,
        mode_t mode, struct file_operations *fops)

該函數也用於在/proc/net下創建proc條目,但是它也同時指定了對該proc條目的文件操作函數。


void proc_net_remove(const char *name)

該函數用於刪除前面兩個函數在/proc/net目錄下創建的proc條目。參數name指定要刪除的proc名稱。
除了這些函數,值得一提的是結構struct proc_dir_entry,為了創建一了可寫的proc條目並指定該proc條目的寫操作函數,必須設置上面的這些創建proc條目的函數返回的指針指向的struct proc_dir_entry結構的write_proc字段,並指定該proc條目的訪問權限有寫權限。
為了使用這些接口函數以及結構struct proc_dir_entry,用戶必須在模塊中包含頭文件linux/proc_fs.h。
在源代碼包中給出了procfs示例程序procfs_exam.c,它定義了三個proc文件條目和一個proc目錄條目,讀者在插入該模塊後應當看到如下結構:


$ ls /proc/myproctest
aint		astring		bigprocfile
$

讀者可以通過cat和echo等文件操作函數來查看和設置這些proc文件。特別需要指出,bigprocfile是一個大文件(超過一個內存頁),對於這種大文件,procfs有一些限制,因為它提供的緩存,只有一個頁,因此必須特別小心,並對超過頁的部分做特別的考慮,處理起來比較復雜並且很容易出錯,所有procfs並不適合於大數據量的輸入輸出,後面一節seq_file就是因為這一缺陷而設計的,當然seq_file依賴於procfs的一些基礎功能。
回頁首


二、seq_file

一般地,內核通過在procfs文件系統下建立文件來向用戶空間提供輸出信息,用戶空間可以通過任何文本閱讀應用查看該文件信息,但是procfs有一個缺陷,如果輸出內容大於1個內存頁,需要多次讀,因此處理起來很難,另外,如果輸出太大,速度比較慢,有時會出現一些意想不到的情況,Alexander Viro實現了一套新的功能,使得內核輸出大文件信息更容易,該功能出現在2.4.15(包括2.4.15)以後的所有2.4內核以及2.6內核中,尤其是在2.6內核中,已經大量地使用了該功能。
要想使用seq_file功能,開發者需要包含頭文件linux/seq_file.h,並定義與設置一個seq_operations結構(類似於file_operations結構):

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

start函數用於指定seq_file文件的讀開始位置,返回實際讀開始位置,如果指定的位置超過文件末尾,應當返回NULL,start函數可以有一個特殊的返回SEQ_START_TOKEN,它用於讓show函數輸出文件頭,但這只能在pos為0時使用,next函數用於把seq_file文件的當前讀位置移動到下一個讀位置,返回實際的下一個讀位置,如果已經到達文件末尾,返回NULL,stop函數用於在讀完seq_file文件後調用,它類似於文件操作close,用於做一些必要的清理,如釋放內存等,show函數用於格式化輸出,如果成功返回0,否則返回出錯碼。
Seq_file也定義了一些輔助函數用於格式化輸出:


int seq_putc(struct seq_file *m, char c);

函數seq_putc用於把一個字符輸出到seq_file文件。


int seq_puts(struct seq_file *m, const char *s);

函數seq_puts則用於把一個字符串輸出到seq_file文件。


int seq_escape(struct seq_file *, const char *, const char *);

函數seq_escape類似於seq_puts,只是,它將把第一個字符串參數中出現的包含在第二個字符串參數中的字符按照八進制形式輸出,也即對這些字符進行轉義處理。


int seq_printf(struct seq_file *, const char *, ...)
        __attribute__ ((format (printf,2,3)));

函數seq_printf是最常用的輸出函數,它用於把給定參數按照給定的格式輸出到seq_file文件。


int seq_path(struct seq_file *, struct vfsmount *, struct dentry *, char *);

函數seq_path則用於輸出文件名,字符串參數提供需要轉義的文件名字符,它主要供文件系統使用。
在定義了結構struct seq_operations之後,用戶還需要把打開seq_file文件的open函數,以便該結構與對應於seq_file文件的struct file結構關聯起來,例如,struct seq_operations定義為:


struct seq_operations exam_seq_ops = {
	.start = exam_seq_start,
   .stop = exam_seq_stop,
   .next = exam_seq_next,
   .show = exam_seq_show
};

那麼,open函數應該如下定義:


static int exam_seq_open(struct inode *inode, struct file *file)
{
        return seq_open(file, &exam_seq_ops);
};

注意,函數seq_open是seq_file提供的函數,它用於把struct seq_operations結構與seq_file文件關聯起來。 最後,用戶需要如下設置struct file_operations結構:


struct file_operations exam_seq_file_ops = {
        .owner   = THIS_MODULE,
        .open    = exm_seq_open,
        .read    = seq_read,
        .llseek  = seq_lseek,
        .release = seq_release
};

注意,用戶僅需要設置open函數,其它的都是seq_file提供的函數。
然後,用戶創建一個/proc文件並把它的文件操作設置為exam_seq_file_ops即可:


struct proc_dir_entry *entry;
entry = create_proc_entry("exam_seq_file", 0, NULL);
if (entry)
entry->proc_fops = &exam_seq_file_ops;

對於簡單的輸出,seq_file用戶並不需要定義和設置這麼多函數與結構,它僅需定義一個show函數,然後使用single_open來定義open函數就可以,以下是使用這種簡單形式的一般步驟:
1.定義一個show函數


int exam_show(struct seq_file *p, void *v)
{
…
}

2. 定義open函數


int exam_single_open(struct inode *inode, struct file *file)
{
        return(single_open(file, exam_show, NULL));
}

注意要使用single_open而不是seq_open。
3. 定義struct file_operations結構


struct file_operations exam_single_seq_file_operations = {
        .open           = exam_single_open,
        .read           = seq_read,
        .llseek         = seq_lseek,
        .release        = single_release,
};

注意,如果open函數使用了single_open,release函數必須為single_release,而不是seq_release。 在源代碼包中給出了一個使用seq_file的具體例子seqfile_exam.c,它使用seq_file提供了一個查看當前系統運行的所有進程的/proc接口,在編譯並插入該模塊後,用戶通過命令"cat /proc/ exam_esq_file"可以查看系統的所有進程。
回頁首


三、debugfs

內核開發者經常需要向用戶空間應用輸出一些調試信息,在穩定的系統中可能根本不需要這些調試信息,但是在開發過程中,為了搞清楚內核的行為,調試信息非常必要,printk可能是用的最多的,但它並不是最好的,調試信息只是在開發中用於調試,而printk將一直輸出,因此開發完畢後需要清除不必要的printk語句,另外如果開發者希望用戶空間應用能夠改變內核行為時,printk就無法實現。因此,需要一種新的機制,那只有在需要的時候使用,它在需要時通過在一個虛擬文件系統中創建一個或多個文件來向用戶空間應用提供調試信息。
有幾種方式可以實現上述要求:
使用procfs,在/proc創建文件輸出調試信息,但是procfs對於大於一個內存頁(對於x86是4K)的輸出比較麻煩,而且速度慢,有時回出現一些意想不到的問題。
使用sysfs(2.6內核引入的新的虛擬文件系統),在很多情況下,調試信息可以存放在那裡,但是sysfs主要用於系統管理,它希望每一個文件對應內核的一個變量,如果使用它輸出復雜的數據結構或調試信息是非常困難的。
使用libfs創建一個新的文件系統,該方法極其靈活,開發者可以為新文件系統設置一些規則,使用libfs使得創建新文件系統更加簡單,但是仍然超出了一個開發者的想象。
為了使得開發者更加容易使用這樣的機制,Greg Kroah-Hartman開發了debugfs(在2.6.11中第一次引入),它是一個虛擬文件系統,專門用於輸出調試信息,該文件系統非常小,很容易使用,可以在配置內核時選擇是否構件到內核中,在不選擇它的情況下,使用它提供的API的內核部分不需要做任何改動。
使用debugfs的開發者首先需要在文件系統中創建一個目錄,下面函數用於在debugfs文件系統下創建一個目錄:

struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);

參數name是要創建的目錄名,參數parent指定創建目錄的父目錄的dentry,如果為NULL,目錄將創建在debugfs文件系統的根目錄下。如果返回為-ENODEV,表示內核沒有把debugfs編譯到其中,如果返回為NULL,表示其他類型的創建失敗,如果創建目錄成功,返回指向該目錄對應的dentry條目的指針。
下面函數用於在debugfs文件系統中創建一個文件:


struct dentry *debugfs_create_file(const char *name, mode_t mode,
                                       struct dentry *parent, void *data,
                                       struct file_operations *fops);

參數name指定要創建的文件名,參數mode指定該文件的訪問許可,參數parent指向該文件所在目錄,參數data為該文件特定的一些數據,參數fops為實現在該文件上進行文件操作的fiel_operations結構指針,在很多情況下,由seq_file(前面章節已經講過)提供的文件操作實現就足夠了,因此使用debugfs很容易,當然,在一些情況下,開發者可能僅需要使用用戶應用可以控制的變量來調試,debugfs也提供了4個這樣的API方便開發者使用:


struct dentry *debugfs_create_u8(const char *name, mode_t mode, 
                                     struct dentry *parent, u8 *value);
    struct dentry *debugfs_create_u16(const char *name, mode_t mode, 
                                      struct dentry *parent, u16 *value);
    struct dentry *debugfs_create_u32(const char *name, mode_t mode, 
                                      struct dentry *parent, u32 *value);
    struct dentry *debugfs_create_bool(const char *name, mode_t mode, 
struct dentry *parent, u32 *value);

參數name和mode指定文件名和訪問許可,參數value為需要讓用戶應用控制的內核變量指針。
當內核模塊卸載時,Debugfs並不會自動清除該模塊創建的目錄或文件,因此對於創建的每一個文件或目錄,開發者必須調用下面函數清除:


void debugfs_remove(struct dentry *dentry);

參數dentry為上面創建文件和目錄的函數返回的dentry指針。
在源代碼包中給出了一個使用debufs的示例模塊debugfs_exam.c,為了保證該模塊正確運行,必須讓內核支持debugfs,debugfs是一個調試功能,因此它位於主菜單Kernel hacking,並且必須選擇Kernel debugging選項才能選擇,它的選項名稱為Debug Filesystem。為了在用戶態使用debugfs,用戶必須mount它,下面是在作者系統上的使用輸出:


$ mkdir -p /debugfs
$ mount -t debugfs debugfs /debugfs
$ insmod ./debugfs_exam.ko
$ ls /debugfs
debugfs-exam
$ ls /debugfs/debugfs-exam
u8_var		u16_var		u32_var		bool_var
$ cd /debugfs/debugfs-exam
$ cat u8_var
0
$ echo 200 > u8_var
$ cat u8_var
200
$ cat bool_var
N
$ echo 1 > bool_var
$ cat bool_var
Y

回頁首


四、relayfs

relayfs是一個快速的轉發(relay)數據的文件系統,它以其功能而得名。它為那些需要從內核空間轉發大量數據到用戶空間的工具和應用提供了快速有效的轉發機制。
Channel是relayfs文件系統定義的一個主要概念,每一個channel由一組內核緩存組成,每一個CPU有一個對應於該channel的內核緩存,每一個內核緩存用一個在relayfs文件系統中的文件文件表示,內核使用relayfs提供的寫函數把需要轉發給用戶空間的數據快速地寫入當前CPU上的channel內核緩存,用戶空間應用通過標准的文件I/O函數在對應的channel文件中可以快速地取得這些被轉發出的數據mmap來。寫入到channel中的數據的格式完全取決於內核中創建channel的模塊或子系統。
relayfs的用戶空間API:
relayfs實現了四個標准的文件I/O函數,open、mmap、poll和close
open(),打開一個channel在某一個CPU上的緩存對應的文件。
mmap(),把打開的channel緩存映射到調用者進程的內存空間。
read(),讀取channel緩存,隨後的讀操作將看不到被該函數消耗的字節,如果channel的操作模式為非覆蓋寫,那麼用戶空間應用在有內核模塊寫時仍可以讀取,但是如果channel的操作模式為覆蓋式,那麼在讀操作期間如果有內核模塊進行寫,結果將無法預知,因此對於覆蓋式寫的channel,用戶應當在確認在channel的寫完全結束後再進行讀。
poll(),用於通知用戶空間應用轉發數據跨越了子緩存的邊界,支持的輪詢標志有POLLIN、POLLRDNORM和POLLERR。
close(),關閉open函數返回的文件描述符,如果沒有進程或內核模塊打開該channel緩存,close函數將釋放該channel緩存。
注意:用戶態應用在使用上述API時必須保證已經掛載了relayfs文件系統,但內核在創建和使用channel時不需要relayfs已經掛載。下面命令將把relayfs文件系統掛載到/mnt/relay。

mount -t relayfs relayfs /mnt/relay

relayfs內核API:
relayfs提供給內核的API包括四類:channel管理、寫函數、回調函數和輔助函數。
Channel管理函數包括:
relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks)
relay_close(chan)
relay_flush(chan)
relay_reset(chan)
relayfs_create_dir(name, parent)
relayfs_remove_dir(dentry)
relay_commit(buf, reserved, count)
relay_subbufs_consumed(chan, cpu, subbufs_consumed)
寫函數包括:
relay_write(chan, data, length)
__relay_write(chan, data, length)
relay_reserve(chan, length)
回調函數包括:
subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf)
buf_mapped(buf, filp)
buf_unmapped(buf, filp)
輔助函數包括:
relay_buf_full(buf)
subbuf_start_reserve(buf, length)
前面已經講過,每一個channel由一組channel緩存組成,每個CPU對應一個該channel的緩存,每一個緩存又由一個或多個子緩存組成,每一個緩存是子緩存組成的一個環型緩存。
函數relay_open用於創建一個channel並分配對應於每一個CPU的緩存,用戶空間應用通過在relayfs文件系統中對應的文件可以訪問channel緩存,參數base_filename用於指定channel的文件名,relay_open函數將在relayfs文件系統中創建base_filename0..base_filenameN-1,即每一個CPU對應一個channel文件,其中N為CPU數,缺省情況下,這些文件將建立在relayfs文件系統的根目錄下,但如果參數parent非空,該函數將把channel文件創建於parent目錄下,parent目錄使用函數relay_create_dir創建,函數relay_remove_dir用於刪除由函數relay_create_dir創建的目錄,誰創建的目錄,誰就負責在不用時負責刪除。參數subbuf_size用於指定channel緩存中每一個子緩存的大小,參數n_subbufs用於指定channel緩存包含的子緩存數,因此實際的channel緩存大小為(subbuf_size
x n_subbufs),參數overwrite用於指定該channel的操作模式,relayfs提供了兩種寫模式,一種是覆蓋式寫,另一種是非覆蓋式寫。使用哪一種模式完全取決於函數subbuf_start的實現,覆蓋寫將在緩存已滿的情況下無條件地繼續從緩存的開始寫數據,而不管這些數據是否已經被用戶應用讀取,因此寫操作決不失敗。在非覆蓋寫模式下,如果緩存滿了,寫將失敗,但內核將在用戶空間應用讀取緩存數據時通過函數relay_subbufs_consumed()通知relayfs。如果用戶空間應用沒來得及消耗緩存中的數據或緩存已滿,兩種模式都將導致數據丟失,唯一的區別是,前者丟失數據在緩存開頭,而後者丟失數據在緩存末尾。一旦內核再次調用函數relay_subbufs_consumed(),已滿的緩存將不再滿,因而可以繼續寫該緩存。當緩存滿了以後,relayfs將調用回調函數buf_full()來通知內核模塊或子系統。當新的數據太大無法寫入當前子緩存剩余的空間時,relayfs將調用回調函數subbuf_start()來通知內核模塊或子系統將需要使用新的子緩存。內核模塊需要在該回調函數中實現下述功能:
初始化新的子緩存;
如果1正確,完成當前子緩存;
如果2正確,返回是否正確完成子緩存切換;
在非覆蓋寫模式下,回調函數subbuf_start()應該如下實現:


static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
			void *prev_subbuf,
			unsigned int prev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;
	if (relay_buf_full(buf))
		return 0;
	subbuf_start_reserve(buf, sizeof(unsigned int));
	return 1;
}

如果當前緩存滿,即所有的子緩存都沒讀取,該函數返回0,指示子緩存切換沒有成功。當子緩存通過函數relay_subbufs_consumed()被讀取後,讀取者將負責通知relayfs,函數relay_buf_full()在已經有讀者讀取子緩存數據後返回0,在這種情況下,子緩存切換成功進行。
在覆蓋寫模式下, subbuf_start()的實現與非覆蓋模式類似:


static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
			void *prev_subbuf,
			unsigned int prev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;
	subbuf_start_reserve(buf, sizeof(unsigned int));
	return 1;
}

只是不做relay_buf_full()檢查,因為此模式下,緩存是環行的,可以無條件地寫。因此在此模式下,子緩存切換必定成功,函數relay_subbufs_consumed() 也無須調用。如果channel寫者沒有定義subbuf_start(),缺省的實現將被使用。 可以通過在回調函數subbuf_start()中調用輔助函數subbuf_start_reserve()在子緩存中預留頭空間,預留空間可以保存任何需要的信息,如上面例子中,預留空間用於保存子緩存填充字節數,在subbuf_start()實現中,前一個子緩存的填充值被設置。前一個子緩存的填充值和指向前一個子緩存的指針一道作為subbuf_start()的參數傳遞給subbuf_start(),只有在子緩存完成後,才能知道填充值。subbuf_start()也被在channel創建時分配每一個channel緩存的第一個子緩存時調用,以便預留頭空間,但在這種情況下,前一個子緩存指針為NULL。
內核模塊使用函數relay_write()或__relay_write()往channel緩存中寫需要轉發的數據,它們的區別是前者失效了本地中斷,而後者只搶占失效,因此前者可以在任何內核上下文安全使用,而後者應當在沒有任何中斷上下文將寫channel緩存的情況下使用。這兩個函數沒有返回值,因此用戶不能直接確定寫操作是否失敗,在緩存滿且寫模式為非覆蓋模式時,relayfs將通過回調函數buf_full來通知內核模塊。
函數relay_reserve()用於在channel緩存中預留一段空間以便以後寫入,在那些沒有臨時緩存而直接寫入channel緩存的內核模塊可能需要該函數,使用該函數的內核模塊在實際寫這段預留的空間時可以通過調用relay_commit()來通知relayfs。當所有預留的空間全部寫完並通過relay_commit通知relayfs後,relayfs將調用回調函數deliver()通知內核模塊一個完整的子緩存已經填滿。由於預留空間的操作並不在寫channel的內核模塊完全控制之下,因此relay_reserve()不能很好地保護緩存,因此當內核模塊調用relay_reserve()時必須采取恰當的同步機制。
當內核模塊結束對channel的使用後需要調用relay_close() 來關閉channel,如果沒有任何用戶在引用該channel,它將和對應的緩存全部被釋放。
函數relay_flush()強制在所有的channel緩存上做一個子緩存切換,它在channel被關閉前使用來終止和處理最後的子緩存。
函數relay_reset()用於將一個channel恢復到初始狀態,因而不必釋放現存的內存映射並重新分配新的channel緩存就可以使用channel,但是該調用只有在該channel沒有任何用戶在寫的情況下才可以安全使用。
回調函數buf_mapped() 在channel緩存被映射到用戶空間時被調用。
回調函數buf_unmapped()在釋放該映射時被調用。內核模塊可以通過它們觸發一些內核操作,如開始或結束channel寫操作。
在源代碼包中給出了一個使用relayfs的示例程序relayfs_exam.c,它只包含一個內核模塊,對於復雜的使用,需要應用程序配合。該模塊實現了類似於文章中seq_file示例實現的功能。
當然為了使用relayfs,用戶必須讓內核支持relayfs,並且要mount它,下面是作者系統上的使用該模塊的輸出信息:


$ mkdir -p /relayfs
$ insmod ./relayfs-exam.ko
$ mount -t relayfs relayfs /relayfs
$ cat /relayfs/example0
…
$

relayfs是一種比較復雜的內核態與用戶態的數據交換方式,本例子程序只提供了一個較簡單的使用方式,對於復雜的使用,請參考relayfs用例頁面http://relayfs.sourceforge.net/examples.html。
Copyright © Linux教程網 All Rights Reserved