由于開發(fā)和維護內核的復雜性,只用最為關鍵同時對性能要求最高的代碼才會放在內核中。其他的諸如GUI,管理和控制代碼,通常放在用戶空間運行。這種將實現(xiàn)分離在內核和用戶空間的思想在Linux中非常常見。現(xiàn)在的問題是內核代碼和用戶代碼如果彼此通信。
答案是內核空間和用戶空間存在的各種IPC方法,例如系統(tǒng)調用,ioctl,proc文件系統(tǒng)和netlink socket。這篇文章討論netlink socket和討論其作為一種網(wǎng)絡特征IPC的優(yōu)勢。
簡介
Netlink socket是用于內核和用戶空間之間交換信息的特殊的IPC機制。它提供了一種全復用的通信鏈路。和TCP/IP使用的地址族AF_INET相 比,Netlink socket使用地址族AF_NETLINK,每個的netlink socket特征定義協(xié)議類型在內核頭文件中include/linux/netlink.h
以下是netlink socket當前支持的特征和他們的協(xié)議類型的子集:
- NETLINK_ROUTE:用戶空間路由damon,如BGP,OSPF,RIP和內核包轉發(fā)模塊的通信信道。用戶空間路由damon通過此種netlink協(xié)議類型更新內核路由表
- NETLINK_FIREWALL:接收IPv4防火墻代碼發(fā)送的包
- NETLINK_NFLOG:用戶空間iptable管理工具和內核空間Netfilter模塊的通信信道
- NETLINK_ARPD:用戶空間管理arp表
為 什么以上的特征使用netlink而不是系統(tǒng)調用,ioctl或者proc文件系統(tǒng)來完成通信?為新特性添加系統(tǒng)調用,ioctl和proc文件系統(tǒng)相對 而言是一項比較復雜的工作,我們冒著污染內核和損害系統(tǒng)穩(wěn)定性的風險。netlink socket相對簡單:只有一個常量,協(xié)議類型,需要加入到netlink.h中。然后,內核模塊和用戶程序可以通過socket類型的API進行通信。
和 其他socket API一樣,Netlink是異步的,它提供了一個socket隊列來平滑突發(fā)的信息。發(fā)送一個netlink消息的系統(tǒng)調用將消息排列到接受者的 netlink隊列中然后調用接收者的接收處理函數(shù)。接收者,在接收處理函數(shù)的上下文中,可以決定是否立即處理該消息還是等待在另一個上下文中處理。不想 netlink,系統(tǒng)調用需要同步處理。因此,如果我們使用了一個系統(tǒng)來傳遞一條消息到內核,如果需要處理該條信息的時間很長,那么內核調度粒度可以會受 影響。
在內核中實現(xiàn)的系統(tǒng)調用代碼在編譯時被靜態(tài)的鏈接到內核中,因此在一個可以動態(tài)加載的模塊中包括系統(tǒng)調用代碼是不合適的。在netlink socket中,內核中的netlink核心和在一個可加載的模塊中沒有編譯時的相互依賴。
netlink socket支持多播,這也是其與其他交互手段相比較的優(yōu)勢之一。一個進程可以將一條消息廣播到一個netlink組地址。任意多的進程可以監(jiān)聽那個組地址。這提供了一種從內核到用戶空間進行事件分發(fā)接近完美的機制。
從 會話只能由用戶空間應用發(fā)起的角度來看,系統(tǒng)調用和ioctl是單一的IPC。但是,如果一個內核模塊有一個用戶空間應用的緊急消息,沒有一種直接的方法 來實現(xiàn)這些功能。通常,應用需要階段性的輪詢內核來獲取狀態(tài)變化,盡管密集的輪詢會有很大的開銷。netlink通過允許內核初始化一個對話來優(yōu)雅的解決 這個問題。我們稱之為netlink的復用特性。
最后,netlink提供了bsd socket風格的API,而這些API是被軟件開發(fā)社區(qū)所熟知。因此,培訓費用相較較小。
和BSD路由socket的關系
在BSC TCP/IP的棧實現(xiàn)中,有一種叫做路由套接字的特殊的socket。它有AF_ROUTE地址族,PF_ROUTE協(xié)議族和SOCK_RAWsocket類型。在bsd中,路由套接字用于在內核路由表中添加和刪除路由。
在Linux中,路由套接字的實現(xiàn)通過netlink套接字的NETLINK_ROUTE協(xié)議類型來支持。netlink套接字提供了bsd路由套接字的功能的超集。
Netlink套接字API
標 準的套接字API,socket(),sendmsg(),recvmsg()和close(),可以被用戶態(tài)程序使用。可以通過查詢man手冊頁來看這 些函數(shù)的具體定義。這兒,我們討論在netlink上下文中為這些API選擇參數(shù)。對于寫過TCP/IP套接字程序的人對這些API都應該非常熟悉。
創(chuàng)建一個套接字,
int socket(int domain,int type, int protocol)
domain指代地址族,AF_NETLINK,套接字類型不是SOCK_RAW就是SOCK_DGRAM,因為netlink是一個面向數(shù)據(jù)報的服務。
protocol選擇該套接字使用那種netlink特征。以下是幾種預定義的協(xié)議類型:NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_APRD,NETLINK_ROUTE6_FW。你也可以非常容易的添加自己的netlink協(xié)議。
為 每一個協(xié)議類型最多可以定義32個多播組。每一個多播組用一個bit mask來表示,1<<i(0<= i<= 31),這在一組進程和內核進程協(xié)同完成一項任務時非常有用。發(fā)送多播netlink消息可以減少系統(tǒng)調用的數(shù)量,同時減少用來維護多播組成員信息的負 擔。
bind()
和TCP/IP套接字一樣,netlink bind()API用來將一個本地socket地址和一個打開的socket關聯(lián)。一個netlink地址結構如下所示:
1: struct sockaddr_nl
2: {
3: sa_family_t nl_family; /* AF_NETLINK */
4: unsigned short nl_pad; /* zero */
5: __u32 nl_pid; /* process pid */
6: __u32 nl_groups; /* mcast groups mask */
7: } nladdr;
當使用bind()調用的時候,nl_pid域可以被賦值為調用進程的pid。nl_pid在這兒被當做該netlink套接字的本地地址。程序負責找一個獨一無二的32位整數(shù)在填充該域。一種常見的做法是:
1: NL_PID Formula 1: nl_pid = getpid();
公式一使用進程ID號作為nl_pid的值,如果說該進程只需要一個netlink套接字,這是一個自然的選擇。當一個進程中的不同線程需要同一個netlink協(xié)議多個netlink套接字。公式而可以用來產(chǎn)生nl_pid號:
1: NL_PID Formula 2: pthread_self() << 16 | getpid();
通過這種方法,同一個進程中的不同線程可以有同一種netlink協(xié)議類型的netlink套接字。事實上,即使在同一個線程中,在可能使用同一種協(xié)議類型的多個套接字。開發(fā)者必須要更有創(chuàng)造力來產(chǎn)生一個唯一的nl_pid。
如果應用想接受發(fā)送給特定多播組的netlink消息,所有感興趣的多播組bit應該or在一起,并填充到nl_groups域。否則, nl_groups應該被顯式至零,說明該應用只接受到該應用的消息,填充完上述域, 使用如下方式進行綁定:
1: bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));
發(fā)送一條netlink消息
為了發(fā)送一條netlink消息到內核或者其他的用戶空間進程,另外一個struct sockaddr_nl nladdr需要作為目的地址,這和使用sendmsg()發(fā)送一個UDP包是一樣的。如果該消息是發(fā)送至內核的,那么nl_pid和nl_groups 都置為0.
如果說消息時發(fā)送給另一個進程的單播消息,nl_pid是另外一個進程的pid值而nl_groups為零。
如果消息是發(fā)送給一個或多個多播組的多播消息,所有的目的多播組必須bitmask必須or起來從而形成nl_groups域。當我們填充struct msghdr結構用于sendmsg時,使用如下:
1: struct msghdr msg;
2: msg.msg_name = (void *)&(nladdr);
3: msg.msg_namelen = sizeof(nladdr);
netlink套接字也需要它自己本身的消息頭,這是為了給所有協(xié)議類型的netlink消息提供一個統(tǒng)一的平臺。
因為Linux內核netlink核心假設每個netlink消息中存在著以下的頭,所有應用也必須在其發(fā)送的消息中提供這些頭信息:
1: struct nlmsghdr
2: {
3: __u32 nlmsg_len; /* Length of message */
4: __u16 nlmsg_type; /* Message type*/
5: __u16 nlmsg_flags; /* Additional flags */
6: __u32 nlmsg_seq; /* Sequence number */
7: __u32 nlmsg_pid; /* Sending process PID */
8: };
nlmsg_len指整個netlink消息的長度,包括頭信息,這也是netlink核心所必須的。nlmsg_type用于應用但是對于 netlink核心而言其是透明的。nlmsg_flags用于給定附加的控制信息,其被netlink核心讀取和更新。nlmsg_seq和 mlmsg_pid,應用用來跟蹤消息,這些對于netlink核心也是透明的。
所以一個netlink消息由消息頭和消息負載組成。一旦一個消息被加入,它就加入到一個通過nlh指針指向的緩沖區(qū)。我們也可以將消息發(fā)送到struct msghdr msg:
1: struct iovec iov;
2:
3: iov.iov_base = (void *)nlh;
4: iov.iov_len = nlh->nlmsg_len;
5:
6: msg.msg_iov = &iov;
7: msg.msg_iovlen = 1;
經(jīng)過以上步驟,調用sendmsg()函數(shù)來發(fā)送netlink消息:
1: sendmsg(fd, &msg, 0);
一個接收程序必須分配一個足夠大的內存用于保存netlink消息頭和消息負載。然后其填充struct msghdr msg,然后使用標準的recvmsg()函數(shù)來接收netlink消息,假設緩存通過nlh指針指向:
1: struct sockaddr_nl nladdr;
2: struct msghdr msg;
3: struct iovec iov;
4:
5: iov.iov_base = (void *)nlh;
6: iov.iov_len = MAX_NL_MSG_LEN;
7: msg.msg_name = (void *)&(nladdr);
8: msg.msg_namelen = sizeof(nladdr);
9:
10: msg.msg_iov = &iov;
11: msg.msg_iovlen = 1;
12: recvmsg(fd, &msg, 0);
當消息被正確的接收之后,nlh應該指向剛剛接收到的netlink消息的頭。nladdr應該包含接收消息的目的地址,其中包括了消息發(fā)送者的 pid和多播組。同時,宏NLMSG_DATA(nlh),定義在netlink.h中,返回一個指向netlink消息的負載的指針。 close(fd)調用關閉fd描述符所標識的socket。
內核空間netlink API
內核空間的netlinkAPI在內核中被netlink核心支持,即net/core/af_netlink.c。從內核角度看,這些API不同 于用戶空間的API。這些API可以被內核模塊使用從而存取netlink套接字與用戶空間程序通信。除非你使用現(xiàn)存的netlink套接字協(xié)議類型,否 則你必須通過在netlink.h中定義一個常量來添加你自己的協(xié)議類型。例如,我們需要添加一個netlink協(xié)議類型用于測試,則在 netlink.h中加入下面的語句:
1: #define NETLINK_TEST 17
之后,亦可以在linux內核中的任何地方引用添加的協(xié)議類型。
在用戶空間,我們使用socket()來創(chuàng)建一個netlink套接字,但是在內核空間,我們使用如下的API:
1: truct sock *
2: netlink_kernel_create(int unit,
3: void (*input)(struct sock *sk, int len));
參數(shù)unit,即為netlink協(xié)議類型,如NETLINK_TEST,回調函數(shù)會在消息到達netlink套接字時調用。當用戶態(tài)程序發(fā)送一個NETLINK_TEST協(xié)議類型的消息給內核時,input()函數(shù)被調用。下面是一個實現(xiàn)回調函數(shù)的例子:
1: void input (struct sock *sk, int len)
2: {
3: struct sk_buff *skb;
4: struct nlmsghdr *nlh = NULL;
5: u8 *payload = NULL;
6:
7: while ((skb = skb_dequeue(&sk->receive_queue))
8: != NULL) {
9: /* process netlink message pointed by skb->data */
10: nlh = (struct nlmsghdr *)skb->data;
11: payload = NLMSG_DATA(nlh);
12: /* process netlink message with header pointed by
13: * nlh and payload pointed by payload
14: */
15: }
16: }
input()函數(shù)在發(fā)送進程的sendmsg()系統(tǒng)調用上下文執(zhí)行。如果在input中處理netlink消息非常快,那是沒有問題的。如果處 理netlink消息需要很長的時間,我們希望在input()外面處理消息來避免阻塞其他系統(tǒng)調用進入內核。事實上,我們可以使用一個指定的內核線程來 來不斷執(zhí)行以下的步驟。使用skb=skb_recv_datagram(nl_sk),其中nl_sk是 netlink_kernel_create()返回的netlink套接字。然后,處理由skb->data指向的netlink消息。
以下的內核線程在沒有netlink消息在nl_sk中時睡眠,在回調函數(shù)input中,我們只要喚醒睡眠的內核線程,如下所示:
1: void input (struct sock *sk, int len)
2: {
3: wake_up_interruptible(sk->sleep);
4: }
這是一個更具有擴展性的用戶和內核通信的模型。其也提高了上下文交換的粒度。
在內核中發(fā)送netlink消息
真如在用戶空間中一樣,在發(fā)送一個netlink消息時需要設置源和目的netlink消息地址。假設socket緩存中包含了將要發(fā)送的netlink消息,本地地址可以通過以下方式設置:
1: NETLINK_CB(skb).groups = local_groups;
2: NETLINK_CB(skb).pid = 0; /* from kernel */
目的地址可以如下設置:
1: NETLINK_CB(skb).dst_groups = dst_groups;
2: NETLINK_CB(skb).dst_pid = dst_pid;
這些信息不是存儲在skb->data,而是存儲在skb中的netlink控制塊中。發(fā)送一個消息,使用:
1: int
2: netlink_unicast(struct sock *ssk, struct sk_buff
3: *skb, u32 pid, int nonblock);
其中ssk是netlink_kernel_create返回的netlink套接字,skb->data指向netlink將要發(fā)送的消息而pid是接受該消息的用戶程序id。nonblock用于標識在接收緩存不可用時,API是阻塞還是立即返回失敗。
你也可以發(fā)送一個多播消息。以下的API用于將消息傳送到指定的進程,同時多播至指定的多播組。
1: void
2: netlink_broadcast(struct sock *ssk, struct sk_buff
3: *skb, u32 pid, u32 group, int allocation);
group是所有接收多播組的bitmask。allocation是內核內存分配的類型。通常,GFP_ATOMIC用于中斷上下文而在其他情況下是用GFP_KERNEL.只是因為API可能需要分配一個或者多個套接字緩存來克隆多播消息。
在內核中關閉一個netlink套接字
給定了netlink_kernel_create()函數(shù)返回的struct sock *nl_sk,我們可以通過調用以下的API關閉netlink套接字。
1: sock_release(nl_sk->socket);
在內核和用戶態(tài)使用單播通信
1: #include <sys/socket.h>
2: #include <linux/netlink.h>
3:
4: #define MAX_PAYLOAD 1024 /* maximum payload size*/
5: struct sockaddr_nl src_addr, dest_addr;
6: struct nlmsghdr *nlh = NULL;
7: struct iovec iov;
8: int sock_fd;
9:
10: void main() {
11: sock_fd = socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST);
12:
13: memset(&src_addr, 0, sizeof(src_addr));
14: src__addr.nl_family = AF_NETLINK;
15: src_addr.nl_pid = getpid(); /* self pid */
16: src_addr.nl_groups = 0; /* not in mcast groups */
17: bind(sock_fd, (struct sockaddr*)&src_addr,
18: sizeof(src_addr));
19:
20: memset(&dest_addr, 0, sizeof(dest_addr));
21: dest_addr.nl_family = AF_NETLINK;
22: dest_addr.nl_pid = 0; /* For Linux Kernel */
23: dest_addr.nl_groups = 0; /* unicast */
24:
25: nlh=(struct nlmsghdr *)malloc(
26: NLMSG_SPACE(MAX_PAYLOAD));
27: /* Fill the netlink message header */
28: nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
29: nlh->nlmsg_pid = getpid(); /* self pid */
30: nlh->nlmsg_flags = 0;
31: /* Fill in the netlink message payload */
32: strcpy(NLMSG_DATA(nlh), "Hello you!");
33:
34: iov.iov_base = (void *)nlh;
35: iov.iov_len = nlh->nlmsg_len;
36: msg.msg_name = (void *)&dest_addr;
37: msg.msg_namelen = sizeof(dest_addr);
38: msg.msg_iov = &iov;
39: msg.msg_iovlen = 1;
40:
41: sendmsg(fd, &msg, 0);
42:
43: /* Read message from kernel */
44: memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
45: recvmsg(fd, &msg, 0);
46: printf(" Received message payload: %s\n",
47: NLMSG_DATA(nlh));
48:
49: /* Close Netlink Socket */
50: close(sock_fd);
51: }
1: struct sock *nl_sk = NULL;
2:
3: void nl_data_ready (struct sock *sk, int len)
4: {
5: wake_up_interruptible(sk->sleep);
6: }
7:
8: void netlink_test() {
9: struct sk_buff *skb = NULL;
10: struct nlmsghdr *nlh = NULL;
11: int err;
12: u32 pid;
13:
14: nl_sk = netlink_kernel_create(NETLINK_TEST,
15: nl_data_ready);
16: /* wait for message coming down from user-space */
17: skb = skb_recv_datagram(nl_sk, 0, 0, &err);
18:
19: nlh = (struct nlmsghdr *)skb->data;
20: printk("%s: received netlink message payload:%s\n",
21: __FUNCTION__, NLMSG_DATA(nlh));
22:
23: pid = nlh->nlmsg_pid; /*pid of sending process */
24: NETLINK_CB(skb).groups = 0; /* not in mcast group */
25: NETLINK_CB(skb).pid = 0; /* from kernel */
26: NETLINK_CB(skb).dst_pid = pid;
27: NETLINK_CB(skb).dst_groups = 0; /* unicast */
28: netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
29: sock_release(nl_sk->socket);
30: }
在內核和用戶態(tài)使用單播通信
1: #include <sys/socket.h>
2: #include <linux/netlink.h>
3:
4: #define MAX_PAYLOAD 1024 /* maximum payload size*/
5: struct sockaddr_nl src_addr, dest_addr;
6: struct nlmsghdr *nlh = NULL;
7: struct iovec iov;
8: int sock_fd;
9:
10: void main() {
11: sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
12:
13: memset(&src_addr, 0, sizeof(local_addr));
14: src_addr.nl_family = AF_NETLINK;
15: src_addr.nl_pid = getpid(); /* self pid */
16: /* interested in group 1<<0 */
17: src_addr.nl_groups = 1;
18: bind(sock_fd, (struct sockaddr*)&src_addr,
19: sizeof(src_addr));
20:
21: memset(&dest_addr, 0, sizeof(dest_addr));
22:
23: nlh = (struct nlmsghdr *)malloc(
24: NLMSG_SPACE(MAX_PAYLOAD));
25: memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
26:
27: iov.iov_base = (void *)nlh;
28: iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
29: msg.msg_name = (void *)&dest_addr;
30: msg.msg_namelen = sizeof(dest_addr);
31: msg.msg_iov = &iov;
32: msg.msg_iovlen = 1;
33:
34: printf("Waiting for message from kernel\n");
35:
36: /* Read message from kernel */
37: recvmsg(fd, &msg, 0);
38: printf(" Received message payload: %s\n",
39: NLMSG_DATA(nlh));
40: close(sock_fd);
41: }
1: #define MAX_PAYLOAD 1024
2: struct sock *nl_sk = NULL;
3:
4: void netlink_test() {
5: sturct sk_buff *skb = NULL;
6: struct nlmsghdr *nlh;
7: int err;
8:
9: nl_sk = netlink_kernel_create(NETLINK_TEST,
10: nl_data_ready);
11: skb=alloc_skb(NLMSG_SPACE(MAX_PAYLOAD),GFP_KERNEL);
12: nlh = (struct nlmsghdr *)skb->data;
13: nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
14: nlh->nlmsg_pid = 0; /* from kernel */
15: nlh->nlmsg_flags = 0;
16: strcpy(NLMSG_DATA(nlh), "Greeting from kernel!");
17: /* sender is in group 1<<0 */
18: NETLINK_CB(skb).groups = 1;
19: NETLINK_CB(skb).pid = 0; /* from kernel */
20: NETLINK_CB(skb).dst_pid = 0; /* multicast */
21: /* to mcast group 1<<0 */
22: NETLINK_CB(skb).dst_groups = 1;
23:
24: /*multicast the message to all listening processes*/
25: netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
26: sock_release(nl_sk->socket);
27: }
Netlink可靠性機制
在基于netlink的通信中,有兩種可能的情形會導致消息丟失:
- 內存耗盡,沒有足夠多的內存分配給消息
- 緩存復寫,接收隊列中沒有空間存儲消息,這在內核空間和用戶空間之間通信時可能會發(fā)生
緩存復寫在以下情況很可能會發(fā)生:
- 內核子系統(tǒng)以一個恒定的速度發(fā)送netlink消息,但是用戶態(tài)監(jiān)聽者處理過慢
- 用戶存儲消息的空間過小
如果netlink傳送消息失敗,那么recvmsg()函數(shù)會返回No buffer space available(ENOBUFS)錯誤。那么,用戶空間進程知道它丟失了信息,如果內核子系統(tǒng)支持dump操作,它可以重新同步來獲取最新的消息。在 dump操作中,netlink通過在每次調用recvmsg()函數(shù)時傳輸一個包的流控機制來防止接收隊列的復寫。改包消耗一個內存頁,其包含了幾個多 部分netlink消息。圖6中的序列圖顯示了在一個重新同步的過程中所使用的dump操作。
另一方面,緩存復寫不會發(fā)生在用戶和內核空間的通信中,因為sendmsg()同步的將netlink消息發(fā)送到內核子系統(tǒng)。如果使用的是阻塞套接字,那么netlink在從用戶空間到內核空間的通信時完全可靠的,因為內存分配可以等待,所以沒有內存耗盡的可能。
netlink也可以提供應答機制。所以如果用戶空間進程發(fā)送了一個設置了NLM_F_ACK標志的請求,netlink會在netlink錯誤消息中報告給用戶空間剛才請求操作的結果。
從用戶空間的角度來看,Netlink套接字在通用的BSD套接字接口之上實現(xiàn)。因此,netlink套接字編程與通用的TCP/IP編程類似。但是,我們也應該考慮幾個與netlink相關的特殊問題:
- netlink套接字沒有像其他協(xié)議一樣對用戶空間隱藏協(xié)議細節(jié)。事實上,netlink傳遞的是整個消息,包括netlink頭和其他信 息。因此,這就導致了數(shù)據(jù)處理函數(shù)與通用的TCP/IP套接字不同,所以用戶態(tài)程序必須根據(jù)其格式解析和構建netlink信息。然而,沒有標準的工具來 完成這些工作,所以你必須實現(xiàn)自己的函數(shù)或者使用一些現(xiàn)成的庫。
- 來自netlink和內核子系統(tǒng)的錯誤不是通過recvmsg()函數(shù)返回的整數(shù)值來表現(xiàn)的。事實上,錯誤信息時被包裝在netlink錯誤 消息中的。唯一的例外是(ENOBUFS)錯誤,該錯誤不是包裝在netlink消息中,因為報告該錯誤的原因就是我們沒有足夠的空間來緩存新的 netlink消息。標準的通用套接字錯誤,如(EAGAIN),通常和其他輪詢原語,例如poll()和select(),也是通過recvmsg() 返回整數(shù)值。