netlink socket編程實例解析

開發和維護內核是一件很繁雜的工作,因此,只有那些最重要或者與系統性能息息相關的代碼纔將其安排在內核中。其它程序,比如GUI,管理以及控制部分的代碼,一般都會作爲用戶態程序。在linux系統中,把系統的某個特性分割成在內核中和在用戶空間中分別實現一部分的做法是很常見的(比如linux系統的防火牆就分成了內核態的Netfilter和用戶態的iptables)。然而,內核程序與用戶態的程序又是怎樣行通訊的呢?
答案就是通過各種各樣的用戶態和內核態的IPC(interprocess   communication  )機制來實現。比如系統調用,ioctl接口,proc文件系統以及netlink socket,本文就是要討論netlink socekt並向讀者展示這種用網絡
通訊接口方式實現的IPC機制的優點。

介紹:
netlink socekt是一種用於在內核態和用戶態進程之間進行數據傳輸的特殊的IPC。它通過爲內核模塊提
供一組特殊的API,併爲用戶程序提供了一組標準的socket 接口的方式,實現了一種全雙工的通訊連接。類似於TCP/IP中使用AF_INET地址族一樣,netlink socket使用地址族AF_NETLINK。每一個netlink
socket在內核頭文件

include/linux/netlink.h

中定義自己的協議類型。
下面是netlink socket 目前的特性集合以及它支持的協議類型:

NETLINK_ROUTE 用戶空間的路由守護程序之間的通訊通道,比如BGP,OSPF,RIP以及內核數據轉發模塊。用戶態的路由守護程序通過此類型的協議來更新內核中的路由表。
NETLINK_FIREWALL:接收IPV4防火牆代碼發送的數據包。
NETLINK_NFLOG:用戶態的iptables管理工具和內核中的netfilter模塊之間通訊的通道。
NETLINK_ARPD:用來從用戶空間管理內核中的ARP表。

   爲什麼以上的功能在實現用戶程序和內核程序通訊時,都使用netlink方法而不是系統調用,ioctls
或者proc文件系統呢?原因在於:爲新的特性添加一個新的系統調用,ioctls或者一個proc文件的做法並不是很容易的一件事情,因爲我們要冒着污染內核代碼並且可能破壞系統穩定性的風險去完成這件事情。
然而,netlink socket卻是如此的簡單,你只需要在文件netlink.h中添加一個常量來標識你的協議類型,然後,內核模塊和用戶程序就可以立刻使用socket風格的API進行通訊了!
        Netlink提供了一種異步通訊方式,與其他socket API一樣,它提供了一個socket隊列來緩衝或者平滑
瞬時的消息高峯。發送netlink消息的系統調用在把消息加入到接收者的消息對列後,會觸發接收者的接收處理函數。接收者在接收處理函數上下文中,可以決定立即處理消息還是把消息放在隊列中,在以後其它上下文去處理它(因爲我們希望接收處理函數執行的儘可能快)。系統調用與netlink不同,它需要一個同步的處理,因此,當我們使用一個系統調用來從用戶態傳遞消息到內核時,如果處理這個消息的時間很長的話,內核調度的粒度就會受到影響。
        內核中實現系統調用的代碼都是在編譯時靜態鏈接到內核的,因此,在動態加載模塊中去包含一個系統調用的做法是不合適的,那是大多數設備驅動的做法。使用netlink socket時,動態加載模塊中的netlink程序不會和linux內核中的netlink部分產生任何編譯時依賴關係。
Netlink優於系統調用,ioctls和proc文件系統的另外一個特點就是它支持多點傳送。一個進程可以把消息傳輸給一個netlink組地址,然後任意多個進程都可以監聽那個組地址(並且接收消息)。這種機制爲內核到用戶態的事件分發提供了一種近乎完美的解決方案。
系統調用和ioctl都屬於單工方式的IPC,也就是說,這種IPC會話的發起者只能是用戶態程序。但是,如果內核有一個緊急的消息想要通知給用戶態程序時,該怎麼辦呢?如果直接使用這些IPC的話,是沒辦法做到這點的。通常情況下,應用程序會週期性的輪詢內核以獲取狀態的改變,然而,高頻度的輪詢勢必會增加系統的負載。Netlink 通過允許內核初始化會話的方式完美的解決了此問題,我們稱之爲netlink socket的雙工特性。
        最後,netlink socket提供了一組開發者熟悉的BSD風格的API函數,因此,相對於使用神祕的系統調用API或者ioctl而言,netlink開發培訓的費用會更低些。
        與BSD的Routing socket的關係
在BSD TCP/IP的協議棧實現中,有一種特殊的socket叫做Routing socket.它的地址族爲AF_ROUTE, 協議族爲PF_ROUTE, socket類型爲SOCK_RAW. 這種Routing socket是用戶態進程用來向內核中的路由表增加或者刪除路由信息用的。在Linux系統中,netlink socket通過協議類型NETLINK_ROUTE實現了與Routing socket相同的功能,可以說,netlink socket提供了BSD Routing socket功能的超集。
Netlink Socket 的API
     標準的socket API函數-

socket(), sendmsg(), recvmsg()和close()

- 都能夠被用戶態程序直接調用來訪問netlink socket.你可以訪問man手冊來獲取這些函數的詳細定義。在本文,我們只討論怎樣在netlink socket的上下文中爲這些函數選擇參數。這些API對於使用TCP/IP socket寫過一些簡單網絡程序的讀者來說應該很熟悉了。
使用socket()函數創建一個socket,輸入:

intsocket(int domain,int type, int protocol)


socket域(地址族)是AF_NETLINK,socket的類型是SOCK_RAW或者SOCK_DGRAM,因爲netlink是一種面向數據包的服務。
協議類型選擇netlink要使用的類型即可。下面是一些預定義的netlink協議類型:

NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6
和 NETLINK_IP6_FW.

你同樣可以很輕鬆的在netlink.h中添加自定義的協議類型。

每個netlink協議類型可以定義高達32個多點傳輸的組。每個組用一個比特位來表示,1<<i,0<=i<=31.
當一組用戶態進程和內核態進程協同實現一個相同的特性時,這個方法很有用,因爲發送多點傳輸的netlink消息可以減少系統調用的次數,並且減少了相關應用程序的個數,這些程序本來是要用來處理維護多點傳輸組之間關係而帶來的負載的。
bind()函數
跟TCP/IP中的socket一樣,netlink的bind()函數把一個本地socket地址(源socket地址)與一個打開的socket進行關聯,netlink地址結構體如下:
struct sockaddr_nl
{
  sa_family_t    nl_family;  /* AF_NETLINK   */
  unsigned short nl_pad;     /* zero         */
  __u32          nl_pid;     /* process pid */
  __u32          nl_groups;  /* mcast groups mask */
} nladdr;

當上面的結構體被bind()函數調用時,sockaddr_nl的nl_pid屬性的值可以設置爲訪問netlink socket的當前進程的PID,nl_pid作爲這個netlink socket的本地地址。應用程序應該選擇一個唯一的32位整數來填充nl_pid的值。

NL_PID 公式 1:  nl_pid = getpid();

公式一使用進程的PID作爲nl_pid的值,如果這個進程只需要一個該類型協議的netlink socket的話,選用進程pid作爲nl_pid是一個很自然的做法。
換一種情形,如果一個進程的多個線程想要創建屬於各個線程的相同協議類型的netlink socket的話,公式二可以用來爲每個線程的netlink socket產生nl_pid值。


NL_PID 公式 2: pthread_self() << 16 | getpid();

採用這種方法,同一進程的不同線程都能獲取屬於它們的相同協議類型的不同netlink socket。事實上,即便是在一個單獨的線程裏,也可能需要創建同一協議類型的多個netlink socket。所以開發人員需要更多聰明才智去創建不同的nl_pid值,然而本文中不會就如何創建多個不同的nl_pid的值進行過多的討論
如果應用程序想要接收特定協議類型的發往指定多播組的netlink消息的話,所有接收組的比特位應該進行與運算,形成sockaddr_nl的 nl_groups域的值。否則的話,nl_groups應該設置爲0,以便應用程序只能夠收到發送給它的netlink消息。在填充完結構體 nladdr後,作如下的綁定工作:

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


發送一個netlink 消息
爲了能夠把一個netlink消息發送給內核或者別的用戶進程,類似於UDP數據包發送的sendmsg()函數一樣,我們需要另外一個結構體 struct sockaddr_nl nladdr作爲目的地址。如果這個netlink消息是發往內核的話,nl_pid屬性和nl_groups屬性都應該設置爲0。
如果這個消息是發往另外一個進程的單點傳輸消息,nl_pid應該設置爲接收者進程的PID,nl_groups應該設置爲0,假設系統中使用了公式1。
如果消息是發往一個或者多個多播組的話,應該用所有目的多播組的比特位與運算形成nl_groups的值。然後我們就可以將netlink地址應用到結構體struct msghdr msg中,供函數sendmsg()來調用:
structmsghdr msg;
msg.msg_name =(void *)&(nladdr);
msg.msg_namelen =sizeof(nladdr);

netlink消息同樣也需要它自身的消息頭,這樣做是爲了給所有協議類型的netlink消息提供一個通用的背景。
由於linux內核的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 需要用netlink 消息體的總長度來填充,包含頭信息在內,這個是netlink核心需要的信息。mlmsg_type可以被應用程序所用,它對於netlink核心來說是一個透明的值。Nsmsg_flags 用來該對消息體進行另外的控制,會被netlink核心代碼讀取並更新。Nlmsg_seq和nlmsg_pid同樣對於netlink核心部分來說是透明的,應用程序用它們來跟蹤消息。
因此,一個netlink消息體由nlmsghdr和消息的payload部分組成。一旦輸入一個消息,它就會進入一個被nlh指針指向的緩衝區。我們同樣可以把消息發送個結構體struct msghdr msg:

struct iovec iov;
iov.iov_base =(void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov =&iov;
msg.msg_iovlen = 1;


在完成了以上步驟後,調用一次sendmsg()函數就能把netlink消息發送出去:
sendmsg(fd,&msg, 0);

接收netlink消息:
接收程序需要申請足夠大的空間來存儲netlink消息頭和消息的payload部分。它會用如下的方式填充結構體 struct msghdr msg,然後使用標準函數接口recvmsg()來接收netlink消息,假設nlh指向緩衝區:
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);


當消息正確接收後,nlh應該指向剛剛接收到的netlink消息的頭部分。Nladdr應該包含接收到消息體的目的地信息,這個目的地信息由pid和消息將要發往的多播組的值組成。Netlink.h中的宏定義NLMSG_DATA(nlh)返回指向netlink消息體的payload的指針。調用


close(fd)

就可以關閉掉fd描述符代表的netlink socket.
內核空間的netlink API接口
   內核空間的netlink API是由內核中的netlink核心代碼支持的,在net/core/af_netlink.c中實現。從內核的角度來說,API接口與用戶空間的 API是不一樣的。內核模塊通過這些API訪問netlink socket並且與用戶空間的程序進行通訊。如果你不想使用netlink預定義好的協議類型的話,可以在netlink.h中添加一個自定義的協議類型。例如,我們可以通過在netlink.h中插入下面的代碼行,添加一個測試用的協議類型:


#define NETLINK_TEST  17

然後,就可以在linux內核的任何部分訪問這個協議類型了。
在用戶空間,我們通過socket()調用來創建一個netlink socket,但是在內核空間,我們調用如下的API:

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

參數uint是netlink協議類型,例如NETLINK_TEST。函數指針,input,是netlink socket在收到消息時調用的處理消息的回調函數指針。
在內核創建了一個NETLINK_TEST類型的netlink socket後,無論什麼時候,只要用戶程序發送一個NETLINK_TEST類型的netlink消息到內核的話,通過 netlink_kernel_create()函數註冊的回調函數input()都會被調用。下面是一個實現了消息處理函數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()是在發送進程的系統調用sendmsg()的上下文被調用的。如果input函數中處理消息很快的話,一切都沒有問題。但是如果處理netlink消息花費很長時間的話,我們則希望把消息的處理部分放在input()函數的外面,因爲長時間的消息處理過程可能會阻止其它系統調用進入內核。取而代之,我們可以犧牲一個內核線程來完成後續的無限的的處理動作。
使用


skb = skb_recv_datagram(nl_sk)

來接收消息。nl_sk是netlink_kernel_create()函數返回的netlink socket,然後,只需要處理skb->data指針指向的netlink消息就可以了。
這個內核線程會在nl_sk中沒有消息的時候睡眠。因此,在回調函數input()中我們要做的事情就是喚醒睡眠的內核線程,像這樣的方式:

void input(struct sock*sk, int len)
{
  wake_up_interruptible(sk->sleep);
}


這就是一個升級版的內核與用戶空間的通訊模型,它提高了上下文切換的粒度。
從內核中發送netlink消息
就像從用戶空間發送消息一樣,內核在發送netlink消息時也需要設置源netlink地址和目的netlink地址。假設結構體struct sk_buff * skb指向存儲着要發送的netlink消息的緩衝區,源地址可以這樣設置:

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中,相反,它們存儲在socket緩衝區的netlink控制塊skb中.
發送一個單播消息,使用:
int  netlink_unicast(struct sock*ssk, struct sk_buff  *skb,  u32 pid,int nonblock);

ssk是by netlink_kernel_create()函數返回的netlink socket, skb->data指向需要發送的netlink消息體,如果使用公式一的話,pid是接收程序的pid,noblock表明當接收緩衝區不可用時是否應該阻塞還是立即返回一個失敗信息。
你同樣可以從內核發送一個多播消息。下面的函數同時把一個netlink消息發送給pid指定的進程和group標識的多個組。
void   netlink_broadcast(struct sock*ssk, struct sk_buff *skb,  u32 pid, u32 group,int allocation);

group的值是接收消息的各個組的比特位進行與運算的結果。Allocation是內核內存的申請類型。通常情況下在中斷上下文使用 GFP_ATOMIC,否則使用GFP_KERNEL。這是由於發送多播消息時,API可能需要申請一個或者多個socket緩衝區並進行拷貝所引起的。
從內核空間關閉netlink socket
netlink_kernel_create()函數返回的netlink socket爲struct sock *nl_sk,我們可以通過訪問下面的API來從內核空間關閉這個netlink socket:
sock_release(nl_sk->socket);
到目前爲止,我們已經演示了netlink編程概念的最小代碼框架。接着我們會使用NETLINK_TEST協議類型,並且假設它已經被添加到內核頭文件中了。這裏列舉的內核模塊代碼只是與netlink相關的,所以,你應該把它插入到一個完整的內核模塊代碼當中去,這樣的完整代碼在其它代碼中可以找到很多。

實例:
net_link.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h>

#define NETLINK_TEST 21

struct sock *nl_sk = NULL;
EXPORT_SYMBOL_GPL(nl_sk);

void nl_data_ready (struct sk_buff *__skb)
{
  struct sk_buff *skb;
  struct nlmsghdr *nlh;
  u32 pid;
  int rc;
  int len = NLMSG_SPACE(1200);
  char str[100];

  printk("net_link: data is ready to read.\n");
  skb = skb_get(__skb);

  if (skb->len >= NLMSG_SPACE(0)) {
    nlh = nlmsg_hdr(skb);
    printk("net_link: recv %s.\n", (char *)NLMSG_DATA(nlh));
    memcpy(str,NLMSG_DATA(nlh), sizeof(str)); 
    pid = nlh->nlmsg_pid; /*pid of sending process */
    printk("net_link: pid is %d\n", pid);
    kfree_skb(skb);

    skb = alloc_skb(len, GFP_ATOMIC);
    if (!skb){
      printk(KERN_ERR "net_link: allocate failed.\n");
      return;
    }
    nlh = nlmsg_put(skb,0,0,0,1200,0);
    NETLINK_CB(skb).pid = 0; /* from kernel */

    memcpy(NLMSG_DATA(nlh), str, sizeof(str));
    printk("net_link: going to send.\n");
    rc = netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
    if (rc < 0) {
      printk(KERN_ERR "net_link: can not unicast skb (%d)\n", rc);
    }
    printk("net_link: send is ok.\n");
  }
  return;
}

static int test_netlink(void) {
  nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);

  if (!nl_sk) {
    printk(KERN_ERR "net_link: Cannot create netlink socket.\n");
    return -EIO;
  }
  printk("net_link: create socket ok.\n");
  return 0;
}

int init_module()
{
  test_netlink();
  return 0;
}
void cleanup_module( )
{
  if (nl_sk != NULL){
    sock_release(nl_sk->sk_socket);
  }
  printk("net_link: remove ok.\n");
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kidoln");


sender.c
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.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;
struct msghdr msg;

int main(int argc, char* argv[])
{
        sock_fd = socket(PF_NETLINK, SOCK_RAW, 21);
        memset(&msg, 0, sizeof(msg));
        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;

        printf(" Sending message. ...\n");
        sendmsg(sock_fd, &msg, 0);

        /* Read message from kernel */
        memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
        printf(" Waiting message. ...\n");
        recvmsg(sock_fd, &msg, 0);
        printf(" Received message payload: %s\n",NLMSG_DATA(nlh));

         /* Close Netlink Socket */
        close(sock_fd);
}

Makefile
MODULE_NAME :=net_link
obj-m :=$(MODULE_NAME).o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD)
    gcc -o sender sender.c
clean:
    rm -fr *.ko *.o *.cmd sender $(MODULE_NAME).mod.c

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章