在Linux 下User Space(用戶空間)與Kernel Space(內核空間) 溝通與資料交換的方式 有9種

引用: 燚楊 ( yang.y.yi@gmail.com ),計算機科學碩士 2006年2月16日Part1, Part2

第1 部分: 內核啟動參數、模塊參數與sysfs、sysctl、系統調用和netlink
本系列文章包括兩部分,它們文詳細地介紹了Linux系統下用戶空間與內核空間數據交換的九種方式,包括內核啟動參數、模塊參數與sysfs、sysctl、系統調用、netlink、procfs、seq_file、debugfs和relayfs,並給出具體的例子幫助讀者掌握這些技術的使用。它介紹了內核啟動參數、模塊參數與sysfs、sysctl、系統調用和netlink,並結合給出的例子程序詳細地說明了它們如何使用。

引言

一般地,在使用虛擬內存技術的多任務系統上,內核和應用有不同的地址空間,因此,在內核和應用之間以及在應用與應用之間進行數據交換需要專門的機制來實現,眾所周知,進程間通信(IPC)機制就是為實現應用與應用之間的數據交換而專門實現的,大部分讀者可能對進程間通信比較了解,但對應用與內核之間的數據交換機制可能了解甚少,本文將詳細介紹Linux 系統下內核與應用進行數據交換的各種方式,包括內核啟動參數、模塊參數與sysfs、sysctl、系統調用、netlink、procfs、seq_file、debugfs 和relayfs。

1、內核啟動參數

Linux 提供了一種通過bootloader 向其傳輸啟動參數的功能,內核開發者可以通過這種方式來向內核傳輸數據,從而控制內核啟動行為。
通常的使用​​方式是,定義一個分析參數的函數,而後使用內核提供的宏__setup把它註冊到內核中,該宏定義在 linux/init.h 中,因此要使用它必須包含該頭文件:

para_name 為參數名,parse_func 為分析參數值的函數,它負責把該參數的值轉換成相應的內核變量的值並設置那個內核變量。
內核為整數參數值的分析提供了函數get_option 和get_options,前者用於分析參數值為一個整數的情況,而後者用於分析參數值為逗號分割的一系列整數的情況,對於參數值為字符串的情況,需要開發者自定義相應的分析函數。
在源代碼包中的內核程序kern-boot-params.c 說明了三種情況的使用。
該程序列舉了參數為一個整數、逗號分割的整數串以及字符串三種情況,讀者要想測試該程序,需要把該程序拷貝到要使用的內核的源碼目錄樹的一個目錄下,為了避免與內核其他部分混淆,作者建議在內核源碼樹的根目錄下創建一個新目錄,如examples,然後把該程序拷貝到examples 目錄下並重新命名為setup_example.c,並且為該目錄創建一個Makefile 文件:

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

修改為

注意:如果讀者創建的新目錄和重新命名的文件名與上面不同,需要修改上面所說Makefile 文件相應的位置。做完以上工作就可以按照內核構建步驟去構建新的內核,在構建好內核並設置好lilo或grub為該內核的啟動條目後,就可以啟動該內核,然後使用lilo或grub的編輯功能為該內核的啟動參數行增加如下參數串:

當然,該參數串也可以直接寫入到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

讀者可以使用

來查看該程序的輸出。

2、模塊參數與sysfs

內核子系統或設備驅動可以直接編譯到內核,也可以編譯成模塊,如果編譯到內核,可以使用前一節介紹的方法通過內核啟動參數來向它們傳遞參數,如果編譯成模塊,則可以通過命令行在插入模塊時傳遞參數,或者在運行時,通過sysfs來設置或讀取模塊數據。

Sysfs是一個基於內存的文件系統,實際上它基於ramfs,sysfs提供了一種把內核數據結構,它們的屬性以及屬性與數據結構的聯繫開放給用戶態的方式,它與kobject子系統緊密地結合在一起,因此內核開發者不需要直接使用它,而是內核的各個子系統使用它。用戶要想使用sysfs 讀取和設置內核參數,僅需裝載sysfs 就可以通過文件操作應用來讀取和設置內核通過sysfs 開放給用戶的各個參數:

注意,不要把sysfs 和sysctl 混淆,sysctl 是內核的一些控制參數,其目的是方便用戶對內核的行為進行控制,而sysfs 僅僅是把內核的kobject 對象的層次關係與屬性開放給用戶查看,因此sysfs的絕大部分是只讀的,模塊作為一個kobject 也被出口到sysfs,模塊參數則是作為模塊屬性出口的,內核實現者為模塊的使用提供了更靈活的方式,允許用戶設置模塊參數在sysfs的可見性並允許用戶在編寫模塊時設置這些參數在sysfs 下的訪問權限,然後用戶就可以通過sysfs 來查看和設置模塊參數,從而使得用戶能在模塊運行時控制模塊行為。

對於模塊(module)而言,聲明為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 的組合,它們分別對應於用戶讀,用戶寫,用戶組讀,用戶組寫,其他用戶讀和其他用戶寫,因此用文件的訪問權限設置是一致的。

在源代碼包中的Kernel module(內核模塊) module-param-exam.c 是一個利用模塊參數和 sysfs 來進行用戶態與內核態數據交互的例子。
該模塊有三個參數可以通過命令行設置,下面是作者係統上的運行結果示例:

3、sysctl

Sysctl是一種用戶應用來設置和獲得運行時內核的配置參數的一種有效方式,通過這種方式,用戶應用可以在內核運行的任何時刻來改變內核的配置參數,也可以在任何時候獲得內核的配置參數,通常,內核的這些配置參數也出現在proc文件系統的/proc/sys目錄下,用戶應用可以直接通過這個目錄下的文件來實現內核配置的讀寫操作,例如,用戶可以通過

來得知內核IP層是否允許轉發IP包,用戶可以通過

把內核IP 層設置為允許轉發IP 包,即把該機器配置成一個路由器或網關。
一般地,所有的Linux 發布也提供了一個系統工具sysctl,它可以設置和讀取內核的配置參數,但是該工具依賴於proc 文件系統,為了使用該工具,內核必須支持proc 文件系統。

下面是使用sysctl 工具來獲取和設置內核配置參數的例子:

注意,參數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,即

註冊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條目)。

下面是作者運行該模塊與應用的輸出結果示例:

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。

這些函數的原型示例如下:

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

2. 註冊定義的偽字符設備並把它和上面的struct file_operations 關聯起來:

注意,函數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 後創建該實現的偽字符設備,用戶可以使用下面命令來正確創建設備:

然後用戶可以通過cat 來讀寫 /dev/mychrdev,應用程序 syscall-exam-user 則使用 mmap 來讀數據並使用ioctl 來得到該字符設備的信息以及裁減數據內容,它只是示例如何使用現有的系統調用來實現用戶需要的數據交互操作。
下面是作者運行該模塊的結果示例:

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則需要專門的培訓才能使用。

1) 用戶態使用netlink

用戶態應用使用標準的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和close() 就能很容易地使用netlink socket,查詢手冊頁可以了解這些函數的使用細節,本文只是講解使用netlink 的用戶應該如何使用這些函數。注意,使用netlink 的應用必須包含頭文件linux/netlink.h。當然socket 需要的頭文件也必不可少,sys/socket.h。

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

第一個參數必須是AF_NETLINK 或PF_NETLINK,在Linux 中,它們倆實際為一個東西,它表示要使用netlink,第二個參數必須是SOCK_RAW或SOCK_DGRAM, 第三個參數指定netlink協議類型,如前面講的用戶自定義協議類型NETLINK_MYTEST, NETLINK_GENERIC是一個通用的協議類型,它是專門為用戶使用的,因此,用戶可以直接使用它,而不必再添加新的協議類型。

內核預定義的協議類型有:

對於每一個netlink協議類型,可以有多達32多播組,每一個多播組用一個位表示,netlink 的多播特性使得發送消息給同一個組僅需要一次系統調用,因而對於需要多撥消息的應用而言,大大地降低了系統調用的次數。函數bind() 用於把一個打開的netlink socket 與netlink 源socket 地址綁定在一起。

netlink socket 的地址結構如下:

字段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 則可以設置為其它的值,如:

因此字段nl_pid 實際上未必是進程ID,它只是用於區分不同的接收者或發送者的一個標識,用戶可以根據自己需要設置該字段。函數bind 的調用方式如下:

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 需如下設置:

其中nladdr 為消息接收者的netlink 地址。
struct nlmsghdr 為netlink socket 自己的消息頭,這用於多路復用和多路分解netlink 定義的所有協議類型以及其它一些控制,netlink 的內核實現將利用這個消息頭來多路復用和多路分解已經其它的一些控制,因此它也被稱為netlink 控制塊。因此,應用在發送netlink 消息時必須提供該消息頭。

字段nlmsg_len 指定消息的總長度,包括緊跟該結構的數據部分長度以及該結構的大小,字段nlmsg_type 用於應用內部定義消息的類型,它對netlink 內核實現是透明的,因此大部分情況下設置為0,字段nlmsg_flags 用於設置消息標誌,可用的標誌包括:

標誌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。下面是一個示例:

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

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

應用接收消息時需要首先分配一個足夠大的緩存來保存消息頭以及消息的數據部分,然後填充消息頭,添完後就可以直接調用函數recvmsg() 來接收。

注意:fd為socket調用打開的netlink socket描述符。
在消息接收後,nlhdr指向接收到的消息的消息頭,nladdr保存了接收到的消息的目標地址,宏NLMSG_DATA(nlhdr)返回指向消息的數據部分的指針。
在linux/netlink.h中定義了一些方便對消息進行處理的宏,這些宏包括:

宏NLMSG_ALIGN(len)用於得到不小於len且字節對齊的最小數值。

宏NLMSG_LENGTH(len)用於計算數據部分長度為len時實際的消息長度。它一般用於分配消息緩存。

宏NLMSG_SPACE(len)返回不小於NLMSG_LENGTH(len)且字節對齊的最小數值,它也用於分配消息緩存。

宏NLMSG_DATA(nlh)用於取得消息的數據部分的首地址,設置和讀取消息數據部分時需要使用該宏。

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

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

宏NLMSG_PAYLOAD(nlh,len)用於返回payload的長度。
函數close用於關閉打開的netlink socket。

2) 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 就可以:

只要增加這個定義之後,用戶就可以在內核的任何地方引用該協議。
在內核中,為了創建一個netlink socket用戶需要調用如下函數:

參數unit表示netlink協議類型,如NETLINK_MYTEST,參數input則為內核模塊定義的netlink消息處理函數,當有消息到達這個netlink socket時,該input函數指針就會被引用。函數指針input的參數sk實際上就是函數netlink_kernel_create返回的struct sock指針,sock實際是socket的一個內核表示數據結構,用戶態應用創建的socket在內核中也會有一個struct sock結構來表示。下面是一個input函數的示例:

函數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就是這種使用的示例:

當內核中發送netlink消息時,也需要設置目​​標地址與源地址,而且內核中消息是通過struct sk_buff來管理的, linux/netlink.h中定義了一個宏:

來方便消息的地址設置。下面是一個消息地址設置的例子:

字段pid表示消息發送者進程ID,也即源地址,對於內核,它為0, dst_pid 表示消息接收者進程ID,也即目標地址,如果目標為組或內核,它設置為0,否則dst_group 表示目標組地址,如果它目標為某一進程或內核,dst_group 應當設置為0。

在內核中,模塊調用函數netlink_unicast 來發送單播消息:

參數sk為函數netlink_kernel_create()返回的socket,參數skb存放消息,它的data字段指向要發送的netlink消息結構,而skb的控制塊保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用於方便設置該控制塊, 參數pid為接收消息進程的pid,參數nonblock表示該函數是否為非阻塞,如果為1,該函數將在沒有接收緩存可利用時立即返回,而如果為0,該函數在沒有接收緩存可利用時睡眠。

內核模塊或子系統也可以使用函數netlink_broadcast來發送廣播消息:

前面的三個參數與netlink_unicast相同,參數group為接收消息的多播組,該參數的每一個代表一個多播組,因此如果發送給多個多播組,就把該參數設置為多個多播組組ID的位或。參數allocation為內核內存分配類型,一般地為GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用於原子的上下文(即不可以睡眠),而GFP_KERNEL用於非原子上下文。

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

注意函數netlink_kernel_create()返回的類型為struct sock,因此函數sock_release應該這種調用:

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看到全部的內容,同時內核也把該消息發送給用戶態接收程序,用戶態接收程序將把接收到的內容輸出到屏幕上。

第1部分小結

本文是系列文章的第一部分,它詳細介紹了五種用戶空間與內核空間的數據交換方式,並通過實際例子程序向讀者講解瞭如何在內核開發中使用這些技術,其中內核啟動參數方式是單向的,即只能向內核傳遞,而不能從內核獲取,其餘的均可以進行雙向數據交換,即既可以從用戶應用傳遞給內核,有可以從內核傳遞給應用態應用。netlink 是一種雙向的數據交換方式,它使用起來非常簡單高效,特別是它的廣播特性在一些應用中非常方便。作者認為,它是所有這些用戶態與內核態數據交換方式中最有效的最強大的方式。
該系列文章的第二部分將詳細地講解另外三種用戶態與內核態的數據交換方式,包括procfs、seq_file、debugfs 和relayfs,有興趣的讀者請參看該系列文章第二部分。


6、procfs

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

Procfs提供瞭如下API:

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

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

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

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

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

該函數用於創建一個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。

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

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

該函數用於刪除前面兩個函數在/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目錄條目,讀者在插入該模塊後應當看到如下結構:

讀者可以通過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結構):

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

Seq_file也定義了一些輔助函數用於格式化輸出:

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

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

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

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

函數seq_path則用於輸出文件名,字符串參數提供需要轉義的文件名字符,它主要供文件系統使用。

在定義了結構struct seq_operations之後,用戶還需要把打開seq_file文件的open函數,以便該結構與對應於seq_file文件的struct file結構關聯起來,例如,struct seq_operations定義為:

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

注意,函數seq_open是seq_file提供的函數,它用於把struct seq_operations結構與seq_file文件關聯起來。

最後,用戶需要如下設置struct file_operations結構:

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

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

2. 定義open函數

注意要使用single_open而不是seq_open。

3. 定義struct file_operations結構

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

8、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文件系統下創建一個目錄:

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

下面函數用於在debugfs文件系統中創建一個文件:

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

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

參數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

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

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

relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks)
Channel管理函數包括:
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()應該如下實現:

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

在覆蓋寫模式下, subbuf_start()的實現與非覆蓋模式類似:

只是不做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用例頁面

第2部分小結

本文是該系列文章最後一篇,它詳細地講解了其餘四種用戶空間與內核空間的數據交換方式,並通過實際例子程序向讀者講解瞭如何在內核開發中使用這些技術,其中seq_file是單向的,即只能向內核傳遞,而不能從內核獲取,而另外三種方式均可以進行雙向數據交換,即既可以從用戶應用傳遞給內核,又可以從內核傳遞給應用態應用。procfs一般用於向用戶出口少量的數據信息,或用戶通過它設置內核變量從而控制內核行為。seq_file實際上依賴於procfs,因此為了使用seq_file,必須使內核支持procfs。debugfs用於內核開發者調試使用,它比其他集中方式都方便,但是僅用於簡單類型的變量處理。relayfs是一種非常複雜的數據交換方式,要想準確使用並不容易,但是如果使用得當,它遠比procfs和seq_file功能強大。

發表迴響