天天看點

在 Linux 下使用者空間和核心空間資料交換的方式

在 Linux 下使用者空間和核心空間資料交換的方式

本系列文章包括兩篇,他們文周詳地地介紹了Linux系統下使用者空間和核心空間資料交換的九種方式,包括核心啟動參數、子產品參數和sysfs、

sysctl、系統調用、netlink、procfs、seq_file、debugfs和relayfs,并給出具體的例子幫助讀者掌控這些技術的使

用。

本文是該系列文章的第二篇,他介紹了procfs、seq_file、debugfs和relayfs,并結合給出的例子程式周詳地說明了他們怎麼使用。

1、核心啟動參數

Linux 提供了一種通過 bootloader 向其傳輸啟動參數的功能,核心研發者能通過這種方式來向核心傳輸資料,進而控制核心啟動行為。

通常的使用方式是,定義一個分析參數的函數,而後使用核心提供的宏 __setup把他注冊到核心中,該宏定義在 linux/init.h 中,是以要使用他必須包含該頭檔案:

__setup("para_name=", parse_func)

para_name 為參數名,parse_func

為分析參數值的函數,他負責把該參數的值轉換成相應的核心變量的值并設定那個核心變量。核心為整數參數值的分析提供了函數 get_option 和

get_options,前者用于分析參數值為一個整數的情況,而後者用于分析參數值為逗号分割的一系列整數的情況,對于參數值為字元串的情況,需要研發

者自定義相應的分析函數。在原始碼包中的核心程式kern-boot-params.c

說明了三種情況的使用。該程式列舉了參數為一個整數、逗号分割的整數串及字元串三種情況,讀者要想測試該程式,需要把該程式拷貝到要使用的核心的源碼目

錄樹的一個目錄下,為了避免和核心其他部分混淆,作者建議在核心源碼樹的根目錄下建立一個新目錄,如 examples,然後把該程式拷貝到

examples 目錄下并重新命名為 setup_example.c,并且為該目錄建立一個 Makefile 檔案:

obj-y = setup_example.o

Makefile 僅許這一行就足夠了,然後需要修改源碼樹的根目錄下的 Makefile檔案的一行,把下面行

core-y          := usr/

修改為

core-y          := usr/ examples/

注意:如果讀者建立的新目錄和重新命名的檔案名和上面不同,需要修改上面所說 Makefile 檔案相應的位置。

做完以上工作就能按照核心建構步驟去建構新的核心,在建構好核心并設定好lilo或grub為該核心的啟動條目後,就能啟動該核心,然後使用lilo或grub的編輯功能為該核心的啟動參數行增加如下參數串:

setup_example_int=1234 setup_example_int_array=100,200,300,400 setup_example_string=Thisisatest

當然,該參數串也能直接寫入到lilo或grub的設定檔案中對應于該新核心的核心指令行參數串中。讀者能使用其他參數值來測試該功能。

下面是作者系統上使用上面參數行的輸出:

setup_example_int=1234

setup_example_int_array=100,200,300,400

setup_example_int_array includes 4 intergers

setup_example_string=Thisisatest

讀者能使用

dmesg | grep setup

來檢視該程式的輸出。

2、子產品參數和sysfs

核心子系統或裝置驅動能直接編譯到核心,也能編譯成子產品,如果編譯到核心,能使用前一節介紹的方法通過核心啟動參數來向他們傳遞參數,如果編譯成子產品,則能通過指令行在插入子產品時傳遞參數,或在運作時,通過sysfs來設定或讀取子產品資料。

Sysfs是個基于記憶體的檔案系統,實際上他基于ramfs,sysfs提供了一種把核心資料結構,他們的屬性及屬性和資料結構的聯系開放給用

戶态的方式,他和kobject子系統緊密地結合在一起,是以核心研發者不必直接使用他,而是核心的各個子系統使用他。使用者要想使用 sysfs

讀取和設定核心參數,僅需裝載 sysfs 就能通過檔案操作應用來讀取和設定核心通過 sysfs 開放給使用者的各個參數:

$ mkdir -p /sysfs

$ mount -t sysfs sysfs /sysfs

注意,不要把 sysfs 和 sysctl 混淆,sysctl 是核心的一些控制參數,其目的是友善使用者對核心的行為進行控制,而

sysfs 僅僅是把核心的 kobject 對象的層次關系和屬性開放給使用者檢視,是以 sysfs 的絕大部分是隻讀的,子產品作為一個

kobject 也被出口到 sysfs,子產品參數則是作為子產品屬性出口的,核心實作者為子產品的使用提供了更靈活的方式,允許使用者設定子產品參數在

sysfs 的可見性并允許使用者在編寫子產品時設定這些參數在 sysfs 下的通路權限,然後使用者就能通過sysfs

來檢視和設定子產品參數,進而使得使用者能在子產品運作時控制子產品行為。

對于子產品而言,聲明為 static 的變量都能通過指令行來設定,但要想在 sysfs下可見,必須通過宏 module_param

來顯式聲明,該宏有三個參數,第一個為參數名,即已定義的變量名,第二個參數則為變量類型,可用的類型有 byte, short, ushort,

int, uint, long, ulong, charp 和 bool 或 invbool,分别對應于 c 類型 char, short,

unsigned short, int, unsigned int, long, unsigned long, char * 和

int,使用者也能自定義類型 XXX(如果使用者自己定義了 param_get_XXX,param_set_XXX 和

param_check_XXX)。該宏的第三個參數用于指定通路權限,如果為 0,該參數将不出目前 sysfs 檔案系統中,允許的通路權限為

S_IRUSR, S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH 和 S_IWOTH

的組合,他們分别對應于使用者讀,使用者寫,使用者組讀,使用者組寫,其他使用者讀和其他使用者寫,是以用檔案的通路權限設定是一緻的。

原始碼包

中的核心子產品 module-param-exam.c 是個利用子產品參數和sysfs來進行使用者态和核心态資料互動的例子。該子產品有三個參數能通過指令行設定,下面是作者系統上的運作結果示例:

$ insmod ./module-param-exam.ko my_invisible_int=10 my_visible_int=20 mystring="Hello,World"

my_invisible_int = 10

my_visible_int = 20

mystring = ’Hello,World’

$ ls /sys/module/module_param_exam/parameters/

mystring  my_visible_int

$ cat /sys/module/module_param_exam/parameters/mystring

Hello,World

$ cat /sys/module/module_param_exam/parameters/my_visible_int

20

$ echo 2000 > /sys/module/module_param_exam/parameters/my_visible_int

$ cat /sys/module/module_param_exam/parameters/my_visible_int

2000

$ echo "abc" > /sys/module/module_param_exam/parameters/mystring

$ cat /sys/module/module_param_exam/parameters/mystring

abc

$ rmmod module_param_exam

my_invisible_int = 10

my_visible_int = 2000

mystring = ’abc’

3、sysctl

Sysctl是一種使用者應用來設定和獲得運作時核心的設定參數的一種有效方式,通過這種方式,使用者應用能在核心運作的所有時刻來改動核心的設定參

數,也能在所有時候獲得核心的設定參數,通常,核心的這些設定參數也出目前proc檔案系統的/proc/sys目錄下,使用者應用能直接通過這個目錄

下的檔案來實作核心設定的讀寫操作,例如,使用者能通過

Cat /proc/sys/net/ipv4/ip_forward

來得知核心IP層是否允許轉發IP包,使用者能通過

echo 1 > /proc/sys/net/ipv4/ip_forward

把核心 IP 層設定為允許轉發 IP 包,即把該機器設定成一個路由器或網關。

一般地,所有的 Linux 釋出也提供了一個系統工具 sysctl,他能設定和讀取核心的設定參數,不過該工具依賴于 proc 檔案系統,為了使用該工具,核心必須支援 proc 檔案系統。下面是使用 sysctl 工具來擷取和設定核心設定參數的例子:

$ sysctl net.ipv4.ip_forward

net.ipv4.ip_forward = 0

$ sysctl -w net.ipv4.ip_forward=1

net.ipv4.ip_forward = 1

$ sysctl net.ipv4.ip_forward

net.ipv4.ip_forward = 1

注意,參數 net.ipv4.ip_forward 實際被轉換到對應的 proc

檔案/proc/sys/net/ipv4/ip_forward,選項 -w 表示設定該核心設定參數,沒有選項表示讀核心設定參數,使用者能使用

sysctl -a 來讀取所有的核心設定參數,對應更多的 sysctl 工具的資訊,請參考手冊頁 sysctl(8)。

不過 proc 檔案系統對 sysctl 不是必須的,在沒有 proc 檔案系統的情況下,仍然能,這時需要使用核心提供的系統調用 sysctl 來實作對核心設定參數的設定和讀取。

原始碼包

給出了一個實際例子程式,他說明了怎麼在核心和使用者态使用sysctl。頭檔案 sysctl-exam.h 定義了 sysctl 條目

ID,使用者态應用和核心子產品需要這些 ID 來操作和注冊 sysctl 條目。核心子產品在檔案 sysctl-exam-kern.c

中實作,在該核心子產品中,每一個 sysctl 條目對應一個 struct ctl_table 結構,該結構定義了要注冊的 sysctl 條目的

ID(字段 ctl_name),在 proc

下的名稱(字段procname),對應的核心變量(字段data,注意該該字段的指派必須是指針),條目允許的最大長度(字段maxlen,他主要用于

字元串核心變量,以便在對該條目設定時,對超過該最大長度的字元串截掉後面超長的部分),條目在proc檔案系統下的通路權限(字段mode),在通過

proc設定時的處理函數(字段proc_handler,對于整型核心變量,應當設定為&proc_dointvec,而對于字元串核心變量,

則設定為 &proc_dostring),字元串處理政策(字段strategy,一般這是為&sysctl_string)。

Sysctl 條目能是目錄,此時 mode 字段應當設定為 0555,否則通過 sysctl 系統調用将無法通路他下面的 sysctl

條目,child 則指向該目錄條目下面的所有條目,對于在同一目錄下的多個條目,不必一一注冊,使用者能把他們組織成一個 struct

ctl_table 類型的數組,然後一次注冊就能,但此時必須把數組的最後一個結構設定為NULL,即

{

        .ctl_name = 0

}

注冊sysctl條目使用函數register_sysctl_table(struct ctl_table *,

int),第一個參數為定義的struct

ctl_table結構的sysctl條目或條目數組指針,第二個參數為插入到sysctl條目表中的位置,如果插入到末尾,應當為0,如果插入到開頭,

則為非0。核心把所有的sysctl條目都組織成sysctl表。

當子產品解除安裝時,需要使用函數unregister_sysctl_table(struct ctl_table_header

*)解注冊通過函數register_sysctl_table注冊的sysctl條目,函數register_sysctl_table在調用成功時返

回結構struct ctl_table_header,他就是sysctl表的表頭,解注冊函數使用他來解除安裝相應的sysctl條目。

使用者态應用sysctl-exam-user.c通過sysctl系統調用來檢視和設定前面核心子產品注冊的sysctl條目(當然如果使用者的系統核心已

支援proc檔案系統,能直接使用檔案操作應用如cat, echo等直接檢視和設定這些sysctl條目)。

下面是作者運作該子產品和應用的輸出結果示例:

$ insmod ./sysctl-exam-kern.ko

$ cat /proc/sys/mysysctl/myint

$ cat /proc/sys/mysysctl/mystring

$ ./sysctl-exam-user

mysysctl.myint = 0

mysysctl.mystring = ""

$ ./sysctl-exam-user 100 "Hello, World"

old value: mysysctl.myint = 0

new value: mysysctl.myint = 100

old vale: mysysctl.mystring = ""

new value: mysysctl.mystring = "Hello, World"

$ cat /proc/sys/mysysctl/myint

100

$ cat /proc/sys/mysysctl/mystring

Hello, World

$

4、系統調用

系統調用是核心提供給應用程式的接口,應用對底層硬體的操作大部分都是通過調用系統調用來完成的,例如得到和設定系統時間,就需要分别調用

gettimeofday 和 settimeofday 來實作。事實上,所有的系統調用都涉及到核心和應用之間的資料交換,如檔案系統操作函數

read 和 write,設定和讀取網絡協定棧的 setsockopt 和

getsockopt。本節并不是講解怎麼增加新的系統調用,而是講解怎麼利用現有系統調用來實作使用者的資料傳輸需求。

一般地,使用者能建立一個僞裝置來作為應用和核心之間進行資料交換的管道,最通常的做法是使用僞字元裝置,具體實作方法是:

1.定義對字元裝置進行操作的必要函數并設定結構 struct file_operations

結構 struct file_operations 非常大,對于一般的資料交換需求,隻定義 open, read, write,

ioctl, mmap 和 release 函數就足夠了,他們實際上對應于使用者态的檔案系統操作函數 open, read, write,

ioctl, mmap 和 close。這些函數的原型示例如下:

ssize_t exam_read (struct file * file, char __user * buf, size_t count, loff_t * ppos)

{

}

ssize_t exam_write(struct file * file, const char __user * buf, size_t count, loff_t * ppos)

{

}

int exam_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long argv)

{

}

int exam_mmap(struct file *, struct vm_area_struct *)

{

}

int exam_open(struct inode * inode, struct file * file)

{

}

int exam_release(struct inode * inode, struct file * file)

{

}

在定義了這些操作函數後需要定義并設定結構struct file_operations

struct file_operations exam_file_ops = {

        .owner = THIS_MODULE,

        .read = exam_read,

        .write = exam_write,

        .ioctl = exam_ioctl,

        .mmap = exam_mmap,

        .open = exam_open,

        .release = exam_release,

};

2. 注冊定義的僞字元裝置并把他和上面的 struct file_operations 關聯起來:

int exam_char_dev_major;

exam_char_dev_major = register_chrdev(0, "exam_char_dev", &exam_file_ops);

注意,函數 register_chrdev 的第一個參數如果為

0,表示由核心來确定該注冊僞字元裝置的主裝置号,這是該函數的傳回為實際配置設定的主裝置号,如果傳回小于

0,表示注冊失敗。是以,使用者在使用該函數時必須判斷傳回值以便處理失敗情況。為了使用該函數必須包含頭檔案 linux/fs.h。

在原始碼包中給出了一個使用這種方式實作使用者态和核心态資料交換的典型例子,他包含了三個檔案:

頭檔案 syscall-exam.h 定義了 ioctl 指令,.c 檔案

syscall-exam-user.c為使用者态應用,他通過檔案系統操作函數 mmap 和 ioctl 來和核心态子產品交換資料,.c 檔案

syscall-exam-kern.c 為核心子產品,他實作了一個僞字元裝置,以便和使用者态應用進行資料交換。為了正确運作應用程式

syscall-exam-user,需要在插入子產品 syscall-exam-kern

後建立該實作的僞字元裝置,使用者能使用下面指令來正确建立裝置:

$ mknod /dev/mychrdev c `dmesg | grep "char device mychrdev" | sed ’s/.*major is //g’` 0

然後使用者能通過 cat 來讀寫 /dev/mychrdev,應用程式 syscall-exam-user則使用 mmap 來讀資料并使用 ioctl 來得到該字元裝置的資訊及裁減資料内容,他隻是示例怎麼使用現有的系統調用來實作使用者需要的資料互動操作。

下面是作者運作該子產品的結果示例:

$ insmod ./syscall-exam-kern.ko

char device mychrdev is registered, major is 254

$ mknod /dev/mychrdev c `dmesg | grep "char device mychrdev" | sed ’s/.*major is //g’` 0

$ cat /dev/mychrdev

$ echo "abcdefghijklmnopqrstuvwxyz" > /dev/mychrdev

$ cat /dev/mychrdev

abcdefghijklmnopqrstuvwxyz

$ ./syscall-exam-user

User process: syscall-exam-us(1433)

Available space: 65509 bytes

Data len: 27 bytes

Offset in physical: cc0 bytes

mychrdev content by mmap:

abcdefghijklmnopqrstuvwxyz

$ cat /dev/mychrdev

abcde

$

5、netlink

Netlink 是一種特别的 socket,他是 Linux 所特有的,類似于 BSD 中的AF_ROUTE

但又遠比他的功能強大,目前在最新的 Linux 核心(2.6.14)中使用netlink 進行應用和核心通信的應用非常多,包括:路由

daemon(NETLINK_ROUTE),1-wire 子系統(NETLINK_W1),使用者态 socket

協定(NETLINK_USERSOCK),防火牆(NETLINK_FIREWALL),socket

監視(NETLINK_INET_DIAG),netfilter 日志(NETLINK_NFLOG),ipsec

安全政策(NETLINK_XFRM),SELinux 事件通知(NETLINK_SELINUX),iSCSI

子系統(NETLINK_ISCSI),程序審計(NETLINK_AUDIT),轉發資訊表查詢(NETLINK_FIB_LOOKUP),

netlink connector(NETLINK_CONNECTOR),netfilter

子系統(NETLINK_NETFILTER),IPv6 防火牆(NETLINK_IP6_FW),DECnet

路由資訊(NETLINK_DNRTMSG),核心事件向使用者态通知(NETLINK_KOBJECT_UEVENT),通用

netlink(NETLINK_GENERIC)。

Netlink 是一種在核心和使用者應用間進行雙向資料傳輸的非常好的方式,使用者态應用使用标準的 socket API 就能使用 netlink 提供的強大功能,核心态需要使用專門的核心 API 來使用 netlink。

Netlink 相對于系統調用,ioctl 及 /proc 檔案系統而言具有以下好處:

1,為了使用 netlink,使用者僅需要在 include/linux/netlink.h 中增加一個新類型的 netlink

協定定義即可, 如

#define NETLINK_MYTEST 17

然後,核心和使用者态應用就能即時通過 socket API 使用該 netlink

協定類型進行資料交換。但系統調用需要增加新的系統調用,ioctl 則需要增加裝置或檔案, 那需要不少代碼,proc 檔案系統則需要在

/proc 下添加新的檔案或目錄,那将使本來就混亂的 /proc 更加混亂。

2.

netlink是一種異步通信機制,在核心和使用者态應用之間傳遞的消息儲存在socket緩存隊列中,發送消息隻是把消息儲存在接收者的socket的接

收隊列,而不必等待接收者收到消息,但系統調用和 ioctl 則是同步通信機制,如果傳遞的資料太長,将影響排程粒度。

3.使用 netlink 的核心部分能采用子產品的方式實作,使用 netlink 的應用部分和核心部分沒有編譯時依賴,但系統調用就有依賴,而且新的系統調用的實作必須靜态地連接配接到核心中,他無法在子產品中實作,使用新系統調用的應用在編譯時需要依賴核心。

4.netlink 支援多點傳播,核心子產品或應用能把消息多點傳播給一個netlink組,屬于該neilink

組的所有核心子產品或應用都能接收到該消息,核心事件向使用者态的通知機制就使用了這一特性,所有對核心事件感興趣的應用都能收到該子系統發送的核心事件,在

後面的文章中将介紹這一機制的使用。

5.核心能使用 netlink 首先發起會話,但系統調用和 ioctl 隻能由使用者應用發起調用。

6.netlink 使用标準的 socket API,是以非常容易使用,但系統調用和 ioctl則需要專門的教育訓練才能使用。

使用者态使用 netlink

使用者态應用使用标準的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和

close() 就能非常容易地使用 netlink socket,查詢手冊頁能了解這些函數的使用細節,本文隻是講解使用 netlink

的使用者應該怎麼使用這些函數。注意,使用 netlink 的應用必須包含頭檔案 linux/netlink.h。當然 socket

需要的頭檔案也必不可少,sys/socket.h。

為了建立一個 netlink socket,使用者需要使用如下參數調用 socket():

socket(AF_NETLINK, SOCK_RAW, netlink_type)

第一個參數必須是 AF_NETLINK 或 PF_NETLINK,在 Linux

中,他們倆實際為一個東西,他表示要使用netlink,第二個參數必須是SOCK_RAW或SOCK_DGRAM,

第三個參數指定netlink協定類型,如前面講的使用者自定義協定類型NETLINK_MYTEST,

NETLINK_GENERIC是個通用的協定類型,他是專門為使用者使用的,是以,使用者能直接使用他,而不必再添加新的協定類型。核心預定義的協定類

型有:

#define NETLINK_ROUTE           0      

#define NETLINK_W1              1      

#define NETLINK_USERSOCK        2      

#define NETLINK_FIREWALL        3      

#define NETLINK_INET_DIAG       4      

#define NETLINK_NFLOG           5      

#define NETLINK_XFRM            6      

#define NETLINK_SELINUX         7      

#define NETLINK_ISCSI           8      

#define NETLINK_AUDIT           9      

#define NETLINK_FIB_LOOKUP      10

#define NETLINK_CONNECTOR       11

#define NETLINK_NETFILTER       12      

#define NETLINK_IP6_FW          13

#define NETLINK_DNRTMSG         14      

#define NETLINK_KOBJECT_UEVENT  15      

#define NETLINK_GENERIC         16

對于每一個netlink協定類型,能有多達 32多點傳播組,每一個多點傳播組用一個位表示,netlink 的多點傳播特性使得發送消息給同一個組僅需要一次系統調用,因而對于需要多撥消息的應用而言,大大地降低了系統調用的次數。

函數 bind() 用于把一個打開的 netlink socket 和 netlink 源 socket 位址綁定在一起。netlink socket 的位址結構如下:

struct sockaddr_nl

{

  sa_family_t    nl_family;

  unsigned short nl_pad;

  __u32          nl_pid;

  __u32          nl_groups;

};

字段 nl_family 必須設定為 AF_NETLINK 或着 PF_NETLINK,字段 nl_pad

目前沒有使用,是以要總是設定為 0,字段 nl_pid 為接收或發送消息的程序的 ID,如果希望核心處理消息或多點傳播消息,就把該字段設定為

0,否則設定為處理消息的程序 ID。字段 nl_groups 用于指定多點傳播組,bind 函數用于把調用程序加入到該字段指定的多點傳播組,如果設定為

0,表示調用者不加入所有多點傳播組。

傳遞給 bind 函數的位址的 nl_pid 字段應當設定為本程序的程序 ID,這相當于 netlink socket 的本地位址。不過,對于一個程序的多個線程使用 netlink socket 的情況,字段 nl_pid 則能設定為其他的值,如:

pthread_self() 

是以字段 nl_pid 實際上未必是程序 ID,他隻是用于區分不同的接收者或發送者的一個辨別,使用者能根據自己需要設定該字段。函數  bind 的調用方式如下:

bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));

fd為前面的 socket 調用傳回的檔案描述符,參數 nladdr 為 struct sockaddr_nl 類型的位址。

為了發送一個 netlink 消息給核心或其他使用者态應用,需要填充目标 netlink socket 位址

,此時,字段 nl_pid 和 nl_groups 分别表示接收消息者的程序 ID 和多點傳播組。如果字段 nl_pid 設定為 0,表示消息接收者為核心或多點傳播組,如果 nl_groups為 0,表示該消息為單點傳播消息,否則表示多點傳播消息。

使用函數 sendmsg 發送 netlink 消息時還需要引用結構 struct msghdr、struct nlmsghdr 和 struct iovec,結構 struct msghdr 需如下設定:

struct msghdr msg;

memset(&msg, 0, sizeof(msg));

msg.msg_name = (void *)&(nladdr);

msg.msg_namelen = sizeof(nladdr);

其中 nladdr 為消息接收者的 netlink 位址。

struct nlmsghdr 為 netlink socket 自己的消息頭,這用于多路複用和多路分解 netlink

定義的所有協定類型及其他一些控制,netlink

的核心實作将利用這個消息頭來多路複用和多路分解已其他的一些控制,是以他也被稱為netlink 控制塊。是以,應用在發送 netlink

消息時必須提供該消息頭。

struct nlmsghdr

{

  __u32 nlmsg_len;   

  __u16 nlmsg_type;  

  __u16 nlmsg_flags;

  __u32 nlmsg_seq;   

  __u32 nlmsg_pid;   

};

字段 nlmsg_len 指定消息的總長度,包括緊跟該結構的資料部分長度及該結構的大小,字段 nlmsg_type

用于應用内部定義消息的類型,他對 netlink 核心實作是透明的,是以大部分情況下設定為 0,字段 nlmsg_flags

用于設定消息标志,可用的标志包括:

#define NLM_F_REQUEST           1      

#define NLM_F_MULTI             2      

#define NLM_F_ACK               4      

#define NLM_F_ECHO              8      

#define NLM_F_ROOT      0x100   

#define NLM_F_MATCH     0x200   

#define NLM_F_ATOMIC    0x400   

#define NLM_F_DUMP      (NLM_F_ROOT|NLM_F_MATCH)

#define NLM_F_REPLACE   0x100   

#define NLM_F_EXCL      0x200   

#define NLM_F_CREATE    0x400   

#define NLM_F_APPEND    0x800   

标志NLM_F_REQUEST用于表示消息是個請求,所有應用首先發起的消息都應設定該标志。

标志NLM_F_MULTI 用于訓示該消息是個多部分消息的一部分,後續的消息能通過宏NLMSG_NEXT來獲得。

宏NLM_F_ACK表示該消息是前一個請求消息的響應,順序号和程序ID能把請求和響應關聯起來。

标志NLM_F_ECHO表示該消息是相關的一個包的回傳。

标志NLM_F_ROOT 被許多 netlink

協定的各種資料擷取操作使用,該标志訓示被請求的資料表應當整體傳回使用者應用,而不是個條目一個條目地傳回。有該标志的請求通常導緻響應消息設定

NLM_F_MULTI标志。注意,當設定了該标志時,請求是協定特定的,是以,需要在字段 nlmsg_type 中指定協定類型。

标志 NLM_F_MATCH 表示該協定特定的請求隻需要一個資料子集,資料子集由指定的協定特定的過濾器來比對。

标志 NLM_F_ATOMIC 訓示請求傳回的資料應當原子地收集,這預防資料在擷取期間被修改。

标志 NLM_F_DUMP 未實作。

标志 NLM_F_REPLACE 用于取代在資料表中的現有條目。

标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果條目已存在,将失敗。

标志 NLM_F_CREATE 訓示應當在指定的表中建立一個條目。

标志 NLM_F_APPEND 訓示在表末尾添加新的條目。

核心需要讀取和修改這些标志,對于一般的使用,使用者把他設定為 0 就能,隻是一些進階應用(如 netfilter 和路由 daemon

需要他進行一些複雜的操作),字段 nlmsg_seq 和 nlmsg_pid 用于應用追蹤消息,前者表示順序号,後者為消息來源程序

ID。下面是個示例:

#define MAX_MSGSIZE 1024

char buffer[] = "An example message";

struct nlmsghdr nlhdr;

nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));

strcpy(NLMSG_DATA(nlhdr),buffer);

nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));

nlhdr->nlmsg_pid = getpid();  

nlhdr->nlmsg_flags = 0;

結構 struct iovec 用于把多個消息通過一次系統調用來發送,下面是該結構使用示例:

struct iovec iov;

iov.iov_base = (void *)nlhdr;

iov.iov_len = nlh->nlmsg_len;

msg.msg_iov = &iov;

msg.msg_iovlen = 1;

在完成以上步驟後,消息就能通過下面語句直接發送:

sendmsg(fd, &msg, 0);

應用接收消息時需要首先配置設定一個足夠大的緩存來儲存消息頭及消息的資料部分,然後填充消息頭,添完後就能直接調用函數 recvmsg() 來接收。

#define MAX_NL_MSG_LEN 1024

struct sockaddr_nl nladdr;

struct msghdr msg;

struct iovec iov;

struct nlmsghdr * nlhdr;

nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);

iov.iov_base = (void *)nlhdr;

iov.iov_len = MAX_NL_MSG_LEN;

msg.msg_name = (void *)&(nladdr);

msg.msg_namelen = sizeof(nladdr);

msg.msg_iov = &iov;

msg.msg_iovlen = 1;

recvmsg(fd, &msg, 0); 

注意:fd為socket調用打開的netlink socket描述符。

在消息接收後,nlhdr指向接收到的消息的消息頭,nladdr儲存了接收到的消息的目标位址,宏NLMSG_DATA(nlhdr)傳回指向消息的資料部分的指針。

在linux/netlink.h中定義了一些友善對消息進行處理的宏,這些宏包括:

#define NLMSG_ALIGNTO   4

#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

宏NLMSG_ALIGN(len)用于得到不小于len且位元組對齊的最小數值。

#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))

宏NLMSG_LENGTH(len)用于計算資料部分長度為len時實際的消息長度。他一般用于配置設定消息緩存。

#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

宏NLMSG_SPACE(len)傳回不小于NLMSG_LENGTH(len)且位元組對齊的最小數值,他也用于配置設定消息緩存。

#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

宏NLMSG_DATA(nlh)用于取得消息的資料部分的首位址,設定和讀取消息資料部分時需要使用該宏。

#define NLMSG_NEXT(nlh,len)      ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \

                      (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

宏NLMSG_NEXT(nlh,len)用于得到下一個消息的首位址,同時len也減少為剩餘消息的總長度,該宏一般在一個消息被分成幾個部分發送或接收時使用。

#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \

                           (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \

                           (nlh)->nlmsg_len 

宏NLMSG_OK(nlh,len)用于判斷消息是否有len這麼長。

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

宏NLMSG_PAYLOAD(nlh,len)用于傳回payload的長度。

函數close用于關閉打開的netlink socket。

netlink核心API

netlink的核心實目前.c檔案net/core/af_netlink.c中,核心子產品要想使用netlink,也必須包含頭檔案

linux/netlink.h。核心使用netlink需要專門的API,這完全不同于使用者态應用對netlink的使用。如果使用者需要增加新的

netlink協定類型,必須通過修改linux/netlink.h來實作,當然,目前的netlink實作已包含了一個通用的協定類型

NETLINK_GENERIC以友善使用者使用,使用者能直接使用他而不必增加新的協定類型。前面講到,為了增加新的netlink協定類型,使用者僅需增

加如下定義到linux/netlink.h就能: 

#define NETLINK_MYTEST  17

隻要增加這個定義之後,使用者就能在核心的所有地方引用該協定。

在核心中,為了建立一個netlink socket使用者需要調用如下函數:

struct sock *

netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));

參數unit表示netlink協定類型,如NETLINK_MYTEST,參數input則為核心子產品定義的netlink消息處理函數,當有消

息到達這個netlink

socket時,該input函數指針就會被引用。函數指針input的參數sk實際上就是函數netlink_kernel_create傳回的

struct sock指針,sock實際是socket的一個核心表示資料結構,使用者态應用建立的socket在核心中也會有一個struct

sock結構來表示。下面是個input函數的示例:

void input (struct sock *sk, int len)

{

struct sk_buff *skb;

struct nlmsghdr *nlh = NULL;

u8 *data = NULL;

while ((skb = skb_dequeue(&sk->receive_queue)) 

       != NULL) {

nlh = (struct nlmsghdr *)skb->data;

data = NLMSG_DATA(nlh);

}   

}

函數input()會在發送程序執行sendmsg()時被調用,這樣處理消息比較及時,不過,如果消息特别長時,這樣處理将增加系統調用

sendmsg()的執行時間,對于這種情況,能定義一個核心線程專門負責消息接收,而函數input的工作隻是喚醒該核心線程,這樣sendmsg将

非常快傳回。

函數skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收隊列上的消息,傳回為一個struct sk_buff的結構,skb->data指向實際的netlink消息。

函數skb_recv_datagram(nl_sk)也用于在netlink socket

nl_sk上接收消息,和skb_dequeue的不同指出是,如果socket的接收隊列上沒有消息,他将導緻調用程序睡眠在等待隊列nl_sk-

>sk_sleep,是以他必須在程序上下文使用,剛才講的核心線程就能采用這種方式來接收消息。

下面的函數input就是這種使用的示例:

void input (struct sock *sk, int len)

{

  wake_up_interruptible(sk->sk_sleep);

}

當核心中發送netlink消息時,也需要設定目标位址和源位址,而且核心中消息是通過struct sk_buff來管理的,

linux/netlink.h中定義了一個宏:

#define NETLINK_CB(skb)         (*(struct netlink_skb_parms*)&((skb)->cb))

來友善消息的位址設定。下面是個消息位址設定的例子:

NETLINK_CB(skb).pid = 0;

NETLINK_CB(skb).dst_pid = 0;

NETLINK_CB(skb).dst_group = 1;

字段pid表示消息發送者程序ID,也即源位址,對于核心,他為 0, dst_pid 表示消息接收者程序

ID,也即目标位址,如果目标為組或核心,他設定為 0,否則 dst_group 表示目标組位址,如果他目标為某一程序或核心,dst_group

應當設定為 0。

在核心中,子產品調用函數 netlink_unicast 來發送單點傳播消息:

int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);

參數sk為函數netlink_kernel_create()傳回的socket,參數skb存放消息,他的data字段指向要發送的

netlink消息結構,而skb的控制塊儲存了消息的位址資訊,前面的宏NETLINK_CB(skb)就用于友善設定該控制塊,

參數pid為接收消息程序的pid,參數nonblock表示該函數是否為非阻塞,如果為1,該函數将在沒有接收緩存可利用時即時傳回,而如果為0,該函

數在沒有接收緩存可利用時睡眠。

核心子產品或子系統也能使用函數netlink_broadcast來發送廣播消息:

void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation);

前面的三個參數和netlink_unicast相同,參數group為接收消息的多點傳播組,該參數的每一個代表一個多點傳播組,是以如果發送給多個多點傳播

組,就把該參數設定為多個多點傳播組組ID的位或。參數allocation為核心記憶體配置設定類型,一般地為GFP_ATOMIC或GFP_KERNEL,

GFP_ATOMIC用于原子的上下文(即不能睡眠),而GFP_KERNEL用于非原子上下文。

在核心中使用函數sock_release來釋放函數netlink_kernel_create()建立的netlink socket:

void sock_release(struct socket * sock);

注意函數netlink_kernel_create()傳回的類型為struct sock,是以函數sock_release應該這種調用:

sock_release(sk->sk_socket);

sk為函數netlink_kernel_create()的傳回值。

原始碼包

給出了一個使用 netlink 的示例,他包括一個核心子產品 netlink-exam-kern.c 和兩個應用程式

netlink-exam-user-recv.c,

netlink-exam-user-send.c。核心子產品必須先插入到核心,然後在一個終端上運作使用者态接收程式,在另一個終端上運作使用者态發送程

序,發送程式讀取參數指定的文本檔案并把他作為 netlink

消息的内容發送給核心子產品,核心子產品接受該消息儲存到核心緩存中,他也通過proc接口出口到 procfs,是以使用者也能夠通過

/proc/netlink_exam_buffer

看到全部的内容,同時核心也把該消息發送給使用者态接收程式,使用者态接收程式将把接收到的内容輸出到螢幕上。

6、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的一些基礎功能。

7、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

$ echo 200 > u8_var

$ cat u8_var

200

$ cat bool_var

N

$ echo 1 > bool_var

$ cat bool_var

Y

8、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

o open(),o 打開一個channel在某一個CPU上的緩存對應的檔案。 

o mmap(),o 把打開的channel緩存映射到調用者程序的記憶體空間。 

o read

(),o 讀取channel緩存,o 随後的讀操作将看不o 到被該函數消耗的位元組,o 如果channel的操作模式為非覆寫寫,o 那麼使用者空間應用在有核心子產品寫時仍

能讀取,o 不o 過如果channel的操作模式為覆寫式,o 那麼在讀操作期間如果有核心子產品進行寫,o 結果将無法預知,o 是以對于覆寫式寫的channel,o 使用者

應當在确認在channel的寫完全結束後再進行讀。 

o poll(),o 用于通知使用者空間應用轉發資料跨越了子緩存的邊界,o 支援的輪詢标o 志有POLLIN、POLLRDNORM和POLLERR。 

o close(),o 關閉open函數傳回的檔案描述符,o 如果沒有程序或核心子產品打開該channel緩存,o close函數将釋放該channel緩存。

注意:使用者态應用在使用上述API時必須確定已挂載了relayfs檔案系統,但核心在建立和使用channel時不必relayfs已挂載。下面指令将把relayfs檔案系統挂載到/mnt/relay。

                        mount -t relayfs relayfs /mnt/relay

relayfs核心API:

relayfs提供給核心的API包括四類:channel管理、寫函數、回調函數和輔助函數。

Channel管理函數包括:

o relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks) 

o relay_close(chan) 

o relay_flush(chan) 

o relay_reset(chan) 

o relayfs_create_dir(name, parent) 

o relayfs_remove_dir(dentry) 

o relay_commit(buf, reserved, count) 

o relay_subbufs_consumed(chan, cpu, subbufs_consumed)

寫函數包括:

o relay_write(chan, data, length) 

o __relay_write(chan, data, length) 

o relay_reserve(chan, length)

回調函數包括:

o subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf) 

o buf_mapped(buf, filp) 

o buf_unmapped(buf, filp)

輔助函數包括:

o relay_buf_full(buf) 

o 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

小結

本文是該系列文章最後一篇,他周詳地講解了其餘四種使用者空間和核心空間的資料交換方式,并通過實際例子程式向讀者講解了怎麼在核心研發中使用這些技

術,其中seq_file是單向的,即隻能向核心傳遞,而不能從核心擷取,而另外三種方式均能進行雙向資料交換,即既能從使用者應用傳遞給核心,又能

從核心傳遞給應用态應用。procfs一般用于向使用者出口少量的資料資訊,或使用者通過他設定核心變量進而控制核心行為。seq_file實際上依賴于

procfs,是以為了使用seq_file,必須使核心支援procfs。debugfs用于核心研發者調試使用,他比其他集中方式都友善,不過僅用于

簡單類型的變量處理。relayfs是一種非常複雜的資料交換方式,要想準确使用并不容易,不過如果使用得當,他遠比procfs和seq_file功能

強大。

繼續閱讀