Why and How to Use Netlink Socket
作者:Kevin He,2005-01-05
原文地址:http://www.linuxjournal.com/article/7356
譯者:Love. Katherine,2007-03-23
譯文地址:http://blog.csdn.net/lovekatherine/archive/2007/03/23/1539267.aspx
轉載時務必以超鏈接形式標明文章原始出處及作者、譯者信息。
一種用於在內核空間與用戶空間之間雙向傳遞數據的通用方法。
由於內核開發和維護的複雜性,內核中只保留最重要和對性能要求最嚴格的代碼。其它部分,例如GUI、管理和控制代碼等,通常以用戶空間應用程序的方式實現。在Linux系統中,這種將某些功能在內核和用戶空間中分開實現的做法是很常見的。
現在的問題是內核代碼和用戶空間代碼之間要如何相互通訊?
答案就是存在於內核和用戶空間之間的各種IPC方法,例如系統調用,ioctl, proc文件系統或者Netlink Socket. 本文對Netlink Socket進行討論,並展示它作爲網絡特性友好IPC方法的優點。
簡介
Netlink Socket是一種用於在內核和用戶空間進程之間傳遞信息的特殊IPC。對於用戶進程,Netlink Socket以標準socket API的形式爲內核與用戶之間提供了全雙工的通訊通道;而對於內核模塊,則提供了一類特殊的API。相對於TCP/IP socket使用AF_INET地址族,Netlink Socket使用地址族AF_NETLINK。每個Netlink Socket功能定在kernel 頭文件 include/linux/netlink.h中定義自己的protocol type。
以下是目前Netlink Socket所提供的功能和相應protocol type的一個子集
* NETLINK_ROUTE:用戶空間的路由守護進程,例如BGP,OSPF,RIP等,與內核數據包轉發模塊之間的通訊通道。用戶空間的路由守護進程通過該類型的Netlink Socket更新內核的路由表
* NETLINK_FIREWALL:接收由IPV4 防火牆所放過的數據包。
* NETLINK_NFLOG:用戶空間的iptable 管理工具與內核空間的Netfilter之間的通訊通道
* NETLINK_ARPD:用於用戶空間程序管理ARP table。
爲什麼上面的功能要使用Netlink而不是系統調用、ioctl或proc文件系統來實現用戶空間和內核世界的通訊?這是因爲增加系統調用、ioctl或proc文件並不是件簡單的事情——這樣會有污染現有內核並損害系統穩定性的危險。而Netlink Socket則很簡單,只有一個常量即協議類型,需要被添加至netlink.h頭文件。之後,內核模塊和應用程序可以立即使用socket風格的API進行通訊。
Netlink是異步通訊過程;和其他socket API一樣,它爲每個socket提供了緩衝隊列,以使突發性的消息發送平滑化。用於發送Netlink message的系統調用將消息放入接收者所申請的Netlink Socket對應的緩衝隊列中,之後觸發接收者的接收處理函數。在執行接收處理函數執行這樣的上下文環境下,接收者可以決定是立即處理收到的消息,還是將消息留在隊列中留到稍後在不同的上下文環境中處理。不同於Netlink,系統調用要求同步處理。因此,假設我們使用系統調用從用戶空間向內核傳遞一條消息,如果用於處理該消息的時間較長的話,可能會影響內核調度的粒度。
內核中用於實現系統調用的代碼在編譯是被靜態連接入內核;因此,在可動態加載的模塊中(大多數驅動程序都爲此類),包含系統調用代碼是不合適的使用方法。而對於Netlink Socket,在Linux kernel的Netlink模塊核心,與存在於可加載內核模塊中的Netlink 應用程序,這兩者之間不存在編譯時的依賴問題。
Netlink Socket 支持多播,這是與系統調用、ioctls和proc文件系統相比的又一優勢。一個進程可以以多播的形式,將一條消息發送給一個Netlink 組地址,同時任意數量的其他進程都可以監聽該組地址。這就爲從內核向用戶空間分發事件通知提供了一種近乎完美的解決機制。
系統調用和ioctl都是單工IPC,即只有用戶進程能使用這兩種IPC方法建立會話。然而,如果一個內核模塊有一個緊急消息要發送給用戶進程,該怎麼辦?用這兩種IPC,沒有直接的解決辦法。通常,應用程序週期性的對kernel進行輪詢以檢查狀態的變化,然而輪詢的代價是很高的(佔用大量CPU時間)。Netlink通過也允許內核發起會話的方式,優雅的解決了這一問題。這稱之爲Netlink的雙工特性。
最後,Netlink Socket 提供的BSD socket風格的API,很容易被軟件開發者所理解。
與BSD路由socket的關係
在BSD的TC/IP協議棧的實現中,包括一種被稱爲路由socket的特殊sccket。該類socket使用地址族AF_ROUTE ,socket類型爲原始套接字(SOCK_RAW),協議類型爲PF_ROUTE。在BSD中,進程通過路由socket來對內核路由表執行添加或刪除操作。
在Linux中,與BSD中的路由socket對等的功能是由協議類型爲NETLINK_ROUTE的Netlink Socket來提供的,而且Netlink Socket所提供的功能是BSD的路由socket的超集。
Netlink Socket APIS
標準socket API——socket(),sendmsg(), recvmsg() and close()——,都可以被用戶空間進程用於操作Netlink Socket。這些API的詳細說明請查閱相關的使用手冊(man pages)。本文只針對Netlink Socket來討論如何爲這些API選擇合適的參數。對於任何曾經使用TCP/IP socket編寫過普通網絡應用程序的用戶,這些API應該是非常熟悉的。
socket()
通過socket()函數創建一個socket,輸入:
int socket(int domain, int type, int protocol);
Netlink Socket所使用的域(地址族)爲AF_NETLINK,socket類型爲原始套接字(SOCK_RAW)或數據報套接字(SOCK_DGRAM),因爲Netlink提供的是面向消息的服務。
協議類型(protocol)決定了使用Netlink所提供的哪項功能。以下是一些預定義的Nettlinkx協議類型:
NETLINK_ROUTE,NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6 and NETLINK_IP6_FW。
用戶可以很容易的添加自己的Netlink 協議類型。
bind()
每個Netlink協議類型中可以最多定義32個多播組。每個多播組用相應的掩碼錶示 (1<<i, 其中 0<=i<=31)。當一組用戶進程和內核協調完成同一功能時,這是及其有用的。發送多播Netlink message能夠減少執行系統調用的次數,並且減輕了用戶進程需要維護多播組成員列表的負擔。
類似於TCP/IP socket,Netlink 的bind() API 將已打開的socket與某一本地socket地址結構關聯起來 。
Netlink Socket的地址結構如下
struct sockaddr_nl
{
sa_family_t nl_family; /* AF_NETLINK */ 地址族
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* process pid */ 進程ID
__u32 nl_groups; /* mcast groups mask */ 多播組掩碼
} nladdr;
調用bind()時,結構sockaddr_nl 的nl_pid字段應該填寫爲調用進程的pid。在這裏,nl_pid字段充當了Netlink Socket的本地地址的角色。應用程序需負責選擇一個唯一的32字節的整數填入該字段。
NL_PID Formula 1: nl_pid = getpid();
生成式1:選擇應用程序的pid作爲nl_pid的值。如果對於給定的Netlink 協議類型,進程只需要一個Netlink Socket的話,這是種很自然也很合理的選擇。
如果同一進程內的不同線程需要創建多個同一協議類型的Netlink Socket,可採用生成式2來生成合適的nl_pid.
NL_PID Formula 2: pthread_self() << 16 | getpid();
生成式2:這種方式下,同一進程內的不同線程都可以爲同一Netlink 協議類型申請自己特有的socket。實際上,即使在一個線程內,創建多個基於相同協議類型的Netlink Socket也是可能的。然而,開發者需要在如何生成唯一nl_pid上更具創造性。此處,我們不考慮這種非正常情形。
如果應用程序希望接收到某種協議類型發往某些多播組的Netlink message,那麼就應該將其所有感興趣的多播組的掩碼通過"OR"運算組合起來,並填入sockaddr_nl結構中的nl_groups字段。否則,nl_groups字段就應該被清零,這樣應用程序就只接收到發送至該進程的對應協議類型的單播Netlink message。將變量nladdr(類型爲struct sockaddr_nl)填寫好後,執行如下的bind()
bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));
發送Netlink message
爲了向內核和其他用戶空間進程發送消息,需要另外一個類型爲sockaddr_nl的對象提供目標地址,這點與通過sendmsg發送UDP包相同。
如果消息是發往內核的,nl_pid和nl_groups字段都應該置0。
如果是發往另一個進程的單播消息, nl_pid應該是目標進程的pid而nl_groups字段置0(假設系統採用生成式1計算nl_pid)。
如果是發往一個或多個多播組的消息,所有目標多播組對應的掩碼應該執行"OR"操作後填入nl_groups字段。
然後,按如下方式,向sendmsg()API 所需要的 msghdr結構提供目標Netlink 地址。
struct msghdr msg;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
Netlink Socket 還需要有自己的消息頭部。這是爲了爲所有Netlink協議類型提供一個公共基礎。
由於Linux內核中的Netlink核心假設如下頭部在每個Netlink message中的存在,用戶必須爲每個發送的Netlink message提供這個頭部。
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 */ //發送方的pid
};
nlmsg_len 表示整個Netlink message的長度(包括消息的頭部),並且是Netlink核心要求必須填寫的。
nlmsg_type 由用戶使用,對Netlink核心是一個不透明的值。
nlmsg_flags 用於對Netlink message提供額外的控制;該字段被Netlink 核心讀取並更新。
nlmsg_seq和nlmsg_pid由用戶進程用於跟蹤消息,對於Netlink 核心同樣是不透明的值。
因此,一條Netlink message由消息頭部(nlmsghdr結構)和消息負載組成。一旦一條消息被輸入,它被放入由nlh 指針所指向的緩衝區。
struct iovec iov;
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
完成上述步驟後,調用sendmsg(),將消息發送出去。
sendmsg(fd, &msg, 0);
接收Netlink message
接收進程需要分配足夠大的緩衝區來存放Netlink message(包括消息頭部消息負載)。然後需要填寫如下的struct msghdr,並調用標準的recvmsg()來接收Netlink message(此處假設nth指向緩衝區)
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
iov.iov_base = (void *)nlh;
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);
消息被正確接收後,nth應該指向剛接收的Netlink message的頭部,而nladdr則應該存放着接收到消息的目標地址,其中包含目標pid和多播組。定義於頭文件netlink.h中的宏NLMSG_DATA(nlh),返回指向Netlink message的負載的指針。
調用close(fd)則關閉由文件描述符fd所標識的Netlink Socket
內核空間使用的Netlink API
內核空間的Netlink API是由Netlink核心在net/core/af_netlink.c文件提供的。內核使用與用戶空間不同的API。內核模塊可以調用這些API來操縱Netlink Socket,並與用戶空間程序通訊。若不打算利用已有的Netlink協議類型,用戶必須通過在netlink.h中添加常量來添加自己的協議。
例如,我們可以通過在netllink.h頭文件中插入下面一行,來增加一種用於測試目的的協議類型。
#define NETLINK_TEST 17
之後,就可以在內核的任意地方引用所添加的協議類型
在用戶空間,用戶調用socket()函數來創建Netlink Socket;但是在內核空間,則需要調用下面的API:
struct sock *
netlink_kernel_create(int unit,
void (*input)(struct sock *sk, int len));
參數unit實際上是Netlink協議類型,例如NETLINK_TEST。函數指針input,指向一個回調函數,該函數在有消息到達Netink Socket時被調用。
在內核創建了一個類型爲NETLINK_TEST的Netlink Socket後,無論何時用戶空間向內核發送一條類型爲NETLINK_TEST的Netlink message時,之前調用netlink_kernel_create()時通過input參數註冊的回調函數被調用。下面是一個回調函數的示例代碼
void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *payload = NULL;
while ((skb = skb_dequeue(&sk->receive_queue))
!= NULL) {
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
payload = NLMSG_DATA(nlh);
/* process netlink message with header pointed by
* nlh and payload pointed by payload
*/
}
}
input()函數是在由發送進程所激發的sendmeg()系統調用的上下文環境中執行的。如果對該Netlink message的處理速度很快的話,在input()函數中執行對消息的處理是沒有問題的。但是如果對該Netlink message的處理是耗時操作,爲了避免阻止其他系統調用"陷入"內核,應該將處理操作移出input()函數。這種情況下可以使用一個內核線程來無限循環的完成下述操作。
使用 skb = skb_recv_datagram(nl_sk),其中nl_sk是 netlink_kernel_create()返回的Netlink Socket。然後,處理由skb->data所指向的netlink message。
內核線程在nl_sk中沒有Netlink message時睡眠。因此,在回調函數input()中,只需要喚醒睡眠的內核進程,如下:
void input (struct sock *sk, int len)
{
wake_up_interruptible(sk->sleep);
}
這種方式是一種用戶空間和內核間更具擴展性的通訊模型。此外,還改善了上下文切換的粒度。
從內核發送Netlink message
如同在用戶空間一樣,源Netlink 地址和目標Netlink 地址,這兩者需要在發送Netlink message時指定。
假設指針skb指向存放待發送netlink message的sk_buff 結構,源地址可以這樣設置:
NETLINK_CB(skb).groups = local_groups;
NETLINK_CB(skb).pid = 0; /* from kernel */
目標地址可這樣設置:
NETLINK_CB(skb).dst_groups = dst_groups;
NETLINK_CB(skb).dst_pid = dst_pid;
以上這些信息並不存放在skb->data指向的緩衝區中,而是存放在sk_buff的control block字段中。
要發送單播消息,使用:
int
netlink_unicast(struct sock *ssk, struct sk_buff
*skb, u32 pid, int nonblock);
其中參數ssk是由netlink_kernel_create()返回的Netlink Socket,skb->data指向要發送的Netlink message,而參數pid爲接收進程的pid(假設採用的是NLPID 計算方法一);參數nonblock指示API在接收緩衝區不可用時是阻塞(),還是立即返回一個錯誤。
內核同樣可以發送多播消息。下面的API不僅消息發送至由參數pid執行的進程,也發送至由參數group指定的多播組
void
netlink_broadcast(struct sock *ssk, struct sk_buff
*skb, u32 pid, u32 group, int allocation);
參數group是所有目標多播組對應掩碼的"OR"操作的合值。參數allocation指定內核內存分配方式,通常GFP_ATOMIC用於中斷上下文,而GFP_KERNEL用於其他場合。這個參數的存在是因爲該API可能需要分配一個或多個緩衝區來對多播消息進行clone。
在內核中關閉一個Netlink Socket
對於通過netlink_kernel_create()返回的 指向sock結構的指針nl_sk ,調用如下的API來關閉內核中的Netlink Socket
sock_release(nl_sk->socket);
目前爲止,只展示了描述Netlink 編程框架的最少代碼。現在我們要使用己定義的NETLINK_TEST 協議類型,並假設其已經被添加至內核頭文件中。這裏展示的內核模塊代碼只包含netlink 相關的部分,所以它應該被插入一個完整的內核模塊框架,而這樣的框架可以從很多地方找到。
In this example, a user-space process sends a netlink message to the kernel module, and the kernel module echoes the message back to the sending process. Here is the user-space code:
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_PAYLOAD 1024 /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
void main() {
sock_fd = socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST);
memset(&src_addr, 0, sizeof(src_addr));
src__addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); /* self pid */
src_addr.nl_groups = 0; /* not in mcast groups */
bind(sock_fd, (struct sockaddr*)&src_addr,
sizeof(src_addr));
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; /* For Linux Kernel */
dest_addr.nl_groups = 0; /* unicast */
nlh=(struct nlmsghdr *)malloc(
NLMSG_SPACE(MAX_PAYLOAD));
/* Fill the netlink message header */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid(); /* self pid */
nlh->nlmsg_flags = 0;
/* Fill in the netlink message payload */
strcpy(NLMSG_DATA(nlh), "Hello you!");
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(fd, &msg, 0);
/* Read message from kernel */
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
recvmsg(fd, &msg, 0);
printf(" Received message payload: %s/n",
NLMSG_DATA(nlh));
/* Close Netlink Socket */
close(sock_fd);
}
And, here is the kernel code:
struct sock *nl_sk = NULL;
void nl_data_ready (struct sock *sk, int len)
{
wake_up_interruptible(sk->sleep);
}
void netlink_test() {
struct sk_buff *skb = NULL;
struct nlmsghdr *nlh = NULL;
int err;
u32 pid;
nl_sk = netlink_kernel_create(NETLINK_TEST,
nl_data_ready);
/* wait for message coming down from user-space */
skb = skb_recv_datagram(nl_sk, 0, 0, &err);
nlh = (struct nlmsghdr *)skb->data;
printk("%s: received netlink message payload:%s/n",
__FUNCTION__, NLMSG_DATA(nlh));
pid = nlh->nlmsg_pid; /*pid of sending process */
NETLINK_CB(skb).groups = 0; /* not in mcast group */
NETLINK_CB(skb).pid = 0; /* from kernel */
NETLINK_CB(skb).dst_pid = pid;
NETLINK_CB(skb).dst_groups = 0; /* unicast */
netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
sock_release(nl_sk->socket);
}
After loading the kernel module that executes the kernel code above, when we run the user-space executable, we should see the following dumped from the user-space program:
Received message payload: Hello you!
And, the following message should appear in the output of dmesg:
netlink_test: received netlink message payload:
Hello you!
Multicast Communication between Kernel and Applications
In this example, two user-space applications are listening to the same netlink multicast group. The kernel module pops up a message through Netlink Socket to the multicast group, and all the applications receive it. Here is the user-space code:
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_PAYLOAD 1024 /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
void main() {
sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
memset(&src_addr, 0, sizeof(local_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); /* self pid */
/* interested in group 1<<0 */
src_addr.nl_groups = 1;
bind(sock_fd, (struct sockaddr*)&src_addr,
sizeof(src_addr));
memset(&dest_addr, 0, sizeof(dest_addr));
nlh = (struct nlmsghdr *)malloc(
NLMSG_SPACE(MAX_PAYLOAD));
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
printf("Waiting for message from kernel/n");
/* Read message from kernel */
recvmsg(fd, &msg, 0);
printf(" Received message payload: %s/n",
NLMSG_DATA(nlh));
close(sock_fd);
}
And, here is the kernel code:
#define MAX_PAYLOAD 1024
struct sock *nl_sk = NULL;
void netlink_test() {
sturct sk_buff *skb = NULL;
struct nlmsghdr *nlh;
int err;
nl_sk = netlink_kernel_create(NETLINK_TEST,
nl_data_ready);
skb=alloc_skb(NLMSG_SPACE(MAX_PAYLOAD),GFP_KERNEL);
nlh = (struct nlmsghdr *)skb->data;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = 0; /* from kernel */
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh), "Greeting from kernel!");
/* sender is in group 1<<0 */
NETLINK_CB(skb).groups = 1;
NETLINK_CB(skb).pid = 0; /* from kernel */
NETLINK_CB(skb).dst_pid = 0; /* multicast */
/* to mcast group 1<<0 */
NETLINK_CB(skb).dst_groups = 1;
/*multicast the message to all listening processes*/
netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
sock_release(nl_sk->socket);
}
Assuming the user-space code is compiled into the executable nl_recv, we can run two instances of nl_recv:
./nl_recv &
Waiting for message from kernel
./nl_recv &
Waiting for message from kernel
Then, after we load the kernel module that executes the kernel-space code, both instances of nl_recv should receive the following message:
Received message payload: Greeting from kernel!
Received message payload: Greeting from kernel!
總結
Netlink Socket 是一種用於用戶空間程序和內核之間通訊的靈活的藉口。它爲應用程序和內核提供一套易用的socket API還提供了其他高級通訊功能,例如全雙工,緩衝式I/O,多播,以及異步通訊,這些都是其他內核-用戶空間 IPC方法所缺少的。
Kevin Kaichuan He([email protected])是Solustek Corp.的首席軟件工程師。他目前的工作包括嵌入式系統、設備驅動以及網絡協議工程。他之前的工作經驗包括任Cisco Systems高級軟件工程師,Purdue University 計算機系助教。業餘時間,他喜歡數字攝影、PS2遊戲和文學。