用戶空間與內核通信-netlink

http://www.ibm.com/developerworks/cn/linux/l-kerns-usrs/
http://www.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux

 

在 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函數返回的套接字,可以交給bind等函數調用:

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套接字,退出程序。

內核空間


與應用程序內核,內核空間也主要完成三件工作:
1 創建netlink套接字
2 接收處理用戶空間發送的數據
3 發送數據至用戶空間

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 struct sock *nlfd;
struct
{
__u32 pid;
rwlock_t lock;
}user_proc;
/*掛接在 netfilter 框架的 NF_IP_PRE_ROUTING 點上的函數爲 get_icmp()*/

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-& gt;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 鉤子*/

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