1. 程式人生 > >[保留] Linux 使用者態與核心態的互動——netlink 篇

[保留] Linux 使用者態與核心態的互動——netlink 篇


[size=4]Linux 使用者態與核心態的互動
——netlink 篇[/size]

作者:Kendo
2006-9-3

這是一篇學習筆記,主要是對《Linux 系統核心空間與使用者空間通訊的實現與分析》中的原始碼imp2的分析。其中的原始碼,可以到以下URL下載:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz

[size=3]參考文件[/size]
《Linux 系統核心空間與使用者空間通訊的實現與分析》 陳鑫
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux
《在 Linux 下使用者空間與核心空間資料交換的方式》 楊燚
http://www-128.ibm.com/developerworks/cn/linux/l-kerns-usrs/

[size=3]理論篇[/size]
在 Linux 2.4 版以後版本的核心中,幾乎全部的中斷過程與使用者態程序的通訊都是使用 netlink 套接字實現的,例如iprote2網路管理工具,它與核心的互動就全部使用了netlink,著名的核心包過濾框架Netfilter在與使用者空間的通讀,也在最新版本中改變為netlink,無疑,它將是Linux使用者態與核心態交流的主要方法之一。它的通訊依據是一個對應於程序的標識,一般定為該程序的 ID。當通訊的一端處於中斷過程時,該標識為 0。當使用 netlink 套接字進行通訊,通訊的雙方都是使用者態程序,則使用方法類似於訊息佇列。但通訊雙方有一端是中斷過程,使用方法則不同。netlink 套接字的最大特點是對中斷過程的支援,它在核心空間接收使用者空間資料時不再需要使用者自行啟動一個核心執行緒,而是通過另一個軟中斷呼叫使用者事先指定的接收函式。工作原理如圖:


 
如圖所示,這裡使用了軟中斷而不是核心執行緒來接收資料,這樣就可以保證資料接收的實時性。
當 netlink 套接字用於核心空間與使用者空間的通訊時,在使用者空間的建立方法和一般套接字使用類似,但核心空間的建立方法則不同,下圖是 netlink 套接字實現此類通訊時建立的過程:
 


使用者空間

使用者態應用使用標準的socket與核心通訊,標準的socket API 的函式, socket(), bind(), sendmsg(), recvmsg() 和 close()很容易地應用到 netlink socket。
為了建立一個 netlink socket,使用者需要使用如下引數呼叫 socket():

socket(AF_NETLINK, SOCK_RAW, netlink_type)


netlink對應的協議簇是 AF_NETLINK,第二個引數必須是SOCK_RAW或SOCK_DGRAM, 第三個引數指定netlink協議型別,它可以是一個自定義的型別,也可以使用核心預定義的型別:

#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

同樣地,socket函式返回的套接字,可以交給bing等函式呼叫:
static int skfd;

skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
if(skfd < 0)
{
      printf("can not create a netlink socket/n");
      exit(0);
}


bind函式需要繫結協議地址,netlink的socket地址使用struct sockaddr_nl結構描述:
struct sockaddr_nl

{
  sa_family_t    nl_family;
  unsigned short nl_pad;
  __u32          nl_pid;
  __u32          nl_groups;
};


成員 nl_family為協議簇 AF_NETLINK,成員 nl_pad 當前沒有使用,因此要總是設定為 0,成員 nl_pid 為接收或傳送訊息的程序的 ID,如果希望核心處理訊息或多播訊息,就把該欄位設定為 0,否則設定為處理訊息的程序 ID。成員 nl_groups 用於指定多播組,bind 函式用於把呼叫程序加入到該欄位指定的多播組,如果設定為 0,表示呼叫者不加入任何多播組:
struct sockaddr_nl local;


memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid(); /*設定pid為自己的pid值*/
local.nl_groups = 0;
/*繫結套接字*/
if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)
{
printf("bind() error/n");
     return -1;
}


使用者空間可以呼叫send函式簇向核心傳送訊息,如sendto、sendmsg等,同樣地,也可以使用struct sockaddr_nl來描述一個對端地址,以待send函式來呼叫,與本地地址稍不同的是,因為對端為核心,所以nl_pid成員需要設定為0:

struct sockaddr_nl kpeer;

memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;


另一個問題就是發核心傳送的訊息的組成,使用我們傳送一個IP網路資料包的話,則資料包結構為“IP包頭+IP資料”,同樣地,netlink的訊息結構是“netlink訊息頭部+資料”。Netlink訊息頭部使用struct nlmsghdr結構來描述:
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提供的巨集NLMSG_LENGTH來計算這個長度,僅需向NLMSG_LENGTH巨集提供要傳送的資料的長度,它會自動計算對齊後的總長度:
/*計算包含報頭的資料報長度*/

#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*位元組對齊*/
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )


後面還可以看到很多netlink提供的巨集,這些巨集可以為我們編寫netlink巨集提供很大的方便。

欄位 nlmsg_type 用於應用內部定義訊息的型別,它對 netlink 核心實現是透明的,因此大部分情況下設定為 0,欄位 nlmsg_flags 用於設定訊息標誌,對於一般的使用,使用者把它設定為 0 就可以,只是一些高階應用(如 netfilter 和路由 daemon 需要它進行一些複雜的操作),欄位 nlmsg_seq 和 nlmsg_pid 用於應用追蹤訊息,前者表示順序號,後者為訊息來源程序 ID。

struct msg_to_kernel		/*自定義訊息首部,它僅包含了netlink的訊息首部*/

{
  struct nlmsghdr hdr;
};

struct msg_to_kernel message;
memset(&message, 0, sizeof(message));
message.hdr.nlmsg_len = NLMSG_LENGTH(0); /*計算訊息,因為這裡只是傳送一個請求訊息,沒有多餘的資料,所以,資料長度為0*/
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_U_PID; /*設定自定義訊息型別*/
message.hdr.nlmsg_pid = local.nl_pid; /*設定傳送者的PID*/

這樣,有了本地地址、對端地址和傳送的資料,就可以呼叫傳送函式將訊息傳送給核心了:
  /*傳送一個請求*/
  sendto(skfd, &message, message.hdr.nlmsg_len, 0,
 (struct sockaddr*)&kpeer, sizeof(kpeer));


當傳送完請求後,就可以呼叫recv函式簇從核心接收資料了,接收到的資料包含了netlink訊息首部和要傳輸的資料:
/*接收的資料包含了netlink訊息首部和自定義資料結構*/

struct u_packet_info
{
  struct nlmsghdr hdr;
  struct packet_info icmp_info;
};
struct u_packet_info info;
while(1)
{
    kpeerlen = sizeof(struct sockaddr_nl);
      /*接收核心空間返回的資料*/
      rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
0, (struct sockaddr*)&kpeer, &kpeerlen);
  
       /*處理接收到的資料*/
……
}


同樣地,函式close用於關閉開啟的netlink socket。程式中,因為程式一直迴圈接收處理核心的訊息,需要收到使用者的關閉訊號才會退出,所以關閉套接字的工作放在了自定義的訊號函式sig_int中處理:
/*這個訊號函式,處理一些程式退出時的動作*/

static void sig_int(int signo)
{
  struct sockaddr_nl kpeer;
  struct msg_to_kernel message;

  memset(&kpeer, 0, sizeof(kpeer));
  kpeer.nl_family = AF_NETLINK;
  kpeer.nl_pid    = 0;
  kpeer.nl_groups = 0;

  memset(&message, 0, sizeof(message));
  message.hdr.nlmsg_len = NLMSG_LENGTH(0);
  message.hdr.nlmsg_flags = 0;
  message.hdr.nlmsg_type = IMP2_CLOSE;
  message.hdr.nlmsg_pid = getpid();

  /*向核心傳送一個訊息,由nlmsg_type表明,應用程式將關閉*/
  sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr *)(&kpeer),         sizeof(kpeer));

  close(skfd);
  exit(0);
}


這個結束函式中,向核心傳送一個“我已經退出了”的訊息,然後呼叫close函式關閉netlink套接字,退出程式。

[size=3]核心空間[/size]

與應用程式核心,核心空間也主要完成三件工作:
n 建立netlink套接字
n 接收處理使用者空間傳送的資料
n 傳送資料至使用者空間

API函式netlink_kernel_create用於建立一個netlink socket,同時,註冊一個回撥函式,用於接收處理使用者空間的訊息:

struct sock *

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


引數unit表示netlink協議型別,如NL_IMP2,引數input則為核心模組定義的netlink訊息處理函式,當有訊息到達這個netlink socket時,該input函式指標就會被引用。函式指標input的引數sk實際上就是函式netlink_kernel_create返回的struct sock指標,sock實際是socket的一個核心表示資料結構,使用者態應用建立的socket在核心中也會有一個struct sock結構來表示。
static int __init init(void)

{
  rwlock_init(&user_proc.lock); /*初始化讀寫鎖*/

  /*建立一個netlink socket,協議型別是自定義的ML_IMP2,kernel_reveive為接受處理函式*/
  nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
  if(!nlfd) /*建立失敗*/
  {
      printk("can not create a netlink socket/n");
      return -1;
  }

  /*註冊一個Netfilter 鉤子*/
  return nf_register_hook(&imp2_ops);
}


module_init(init);


使用者空間向核心傳送了兩種自定義訊息型別:IMP2_U_PID和IMP2_CLOSE,分別是請求和關閉。kernel_receive 函式分別處理這兩種訊息:

DECLARE_MUTEX(receive_sem); /*初始化訊號量*/
static void kernel_receive(struct sock *sk, int len)
{
do
    {
struct sk_buff *skb;
if(down_trylock(&receive_sem)) /*獲取訊號量*/
return;
/*從接收佇列中取得skb,然後進行一些基本的長度的合法性校驗*/
while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
        {
{
struct nlmsghdr *nlh = NULL;

if(skb->len >= sizeof(struct nlmsghdr))
{
/*獲取資料中的nlmsghdr 結構的報頭*/
nlh = (struct nlmsghdr *)skb->data;
if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))
&& (skb->len >= nlh->nlmsg_len))
{
/*長度的全法性校驗完成後,處理應用程式自定義訊息型別,主要是對使用者PID的儲存,即為核心儲存“把訊息傳送給誰”*/
if(nlh->nlmsg_type == IMP2_U_PID) /*請求*/
{
write_lock_bh(&user_proc.pid);
user_proc.pid = nlh->nlmsg_pid;
write_unlock_bh(&user_proc.pid);
}
else if(nlh->nlmsg_type == IMP2_CLOSE) /*應用程式關閉*/
{
write_lock_bh(&user_proc.pid);
if(nlh->nlmsg_pid == user_proc.pid)
user_proc.pid = 0;
write_unlock_bh(&user_proc.pid);
}
}
}
}
kfree_skb(skb);
        }
up(&receive_sem); /*返回訊號量*/
    }while(nlfd && nlfd->receive_queue.qlen);
}


因為核心模組可能同時被多個程序同時呼叫,所以函式中使用了訊號量和鎖來進行互斥。skb = skb_dequeue(&sk->receive_queue)用於取得socket sk的接收佇列上的訊息,返回為一個struct sk_buff的結構,skb->data指向實際的netlink訊息。

程式中註冊了一個Netfilter鉤子,鉤子函式是get_icmp,它截獲ICMP資料包,然後呼叫send_to_user函式將資料傳送給應用空間程序。傳送的資料是info結構變數,它是struct packet_info結構,這個結構包含了來源/目的地址兩個成員。Netfilter Hook不是本文描述的重點,略過。
send_to_user 用於將資料傳送給使用者空間程序,傳送呼叫的是API函式netlink_unicast 完成的:
int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);


引數sk為函式netlink_kernel_create()返回的套接字,引數skb存放待發送的訊息,它的data欄位指向要傳送的netlink訊息結構,而skb的控制塊儲存了訊息的地址資訊, 引數pid為接收訊息程序的pid,引數nonblock表示該函式是否為非阻塞,如果為1,該函式將在沒有接收快取可利用時立即返回,而如果為0,該函式在沒有接收快取可利用時睡眠。
向用戶空間程序傳送的訊息包含三個部份:netlink 訊息頭部、資料部份和控制欄位,控制欄位包含了核心傳送netlink訊息時,需要設定的目標地址與源地址,核心中訊息是通過sk_buff來管理的, linux/netlink.h中定義了NETLINK_CB巨集來方便訊息的地址設定:

#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。

static int send_to_user(struct packet_info *info)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;

/*計算訊息總長:訊息首部加上資料加度*/
size = NLMSG_SPACE(sizeof(*info));

/*分配一個新的套接字快取*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;

/*初始化一個netlink訊息首部*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
/*跳過訊息首部,指向資料區*/
packet = NLMSG_DATA(nlh);
/*初始化資料區*/
memset(packet, 0, sizeof(struct packet_info));
/*填充待發送的資料*/
packet->src = info->src;
packet->dest = info->dest;

/*計算skb兩次長度之差,即netlink的長度總和*/
nlh->nlmsg_len = skb->tail - old_tail;
/*設定控制欄位*/
NETLINK_CB(skb).dst_groups = 0;

/*傳送資料*/
read_lock_bh(&user_proc.lock);
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
read_unlock_bh(&user_proc.lock);


}


函式初始化netlink 訊息首部,填充資料區,然後設定控制欄位,這三部份都包含在skb_buff中,最後呼叫netlink_unicast函式把資料傳送出去。
函式中呼叫了netlink的一個重要的巨集NLMSG_PUT,它用於初始化netlink 訊息首部:
#define NLMSG_PUT(skb, pid, seq, type, len) /

({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; /
   __nlmsg_put(skb, pid, seq, type, len); })
static __inline__ struct nlmsghdr *
__nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
{
struct nlmsghdr *nlh;
int size = NLMSG_LENGTH(len);

nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
nlh->nlmsg_type = type;
nlh->nlmsg_len = size;
nlh->nlmsg_flags = 0;
nlh->nlmsg_pid = pid;
nlh->nlmsg_seq = seq;
return nlh;
}


這個巨集一個需要注意的地方是呼叫了nlmsg_failure標籤,所以在程式中應該定義這個標籤。

在核心中使用函式sock_release來釋放函式netlink_kernel_create()建立的netlink socket:
void sock_release(struct socket * sock);


程式在退出模組中釋放netlink sockets和netfilter hook:
static void __exit fini(void)

{
  if(nlfd)
    {
      sock_release(nlfd->socket); /*釋放netlink socket*/
    }
  nf_unregister_hook(&imp2_ops); /*撤鎖netfilter 鉤子*/
}