一、引言
一般地,在使用虛擬內存技術的多任務系統上,內核和應用有不同的地址空間,因此,在內核和應用之間以及在應用與應用之間進行數據交換需要專門的機制來實現,眾所周知,進程間通信(IPC)機制就是為實現應用與應用之間的數據交換而專門實現的,大部分讀者可能對進程間通信比較了解,但對應用與內核之間的數據交換機制可能了解甚少,本文將詳細介紹 Linux 系統下內核與應用進行數據交換的各種方式,包括內核啟動參數、模塊參數與 sysfs、sysctl、系統調用、netlink、procfs、seq_file、debugfs 和 relayfs。
回頁首
二、內核啟動參數
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
來查看該程序的輸出。
回頁首
三、模塊參數與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'
回頁首
四、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
0
$ 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
$
回頁首
五、系統調用
系統調用是內核提供給應用程序的接口,應用對底層硬件的操作大部分都是通過調用系統調用來完成的,例如得到和設置系統時間,就需要分別調用 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
$
回頁首
六、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 /* Routing/device hook */
#define NETLINK_W1 1 /* 1-wire subsystem */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#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() << 16 | getpid();
因此字段 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; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
字段 nlmsg_len 指定消息的總長度,包括緊跟該結構的數據部分長度以及該結構的大小,字段 nlmsg_type 用於應用內部定義消息的類型,它對 netlink 內核實現是透明的,因此大部分情況下設置為 0,字段 nlmsg_flags 用於設置消息標志,可用的標志包括:
/* Flags values */
#define NLM_F_REQUEST 1 /* It is request message. */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 8 /* Echo this request */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
標志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(); /* self pid */
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 <= (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) {
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
data = NLMSG_DATA(nlh);
/* process netlink message with header pointed by
* nlh and data pointed by data
*/
}
}
函數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
看到全部的內容,同時內核也把該消息發送給用戶態接收程序,用戶態接收程序將把接收到的內容輸出到屏幕上。