原文:http://bbs.chinaunix.net/thread-2162796-1-1.html
Linux 使用者态與核心态的互動
——netlink 篇
作者:Kendo
2006-9-3
這是一篇學習筆記,主要是對《Linux 系統核心空間與使用者空間通信的實作與分析》中的源碼imp2的分析。其中的源碼,可以到以下URL下載下傳:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz
參考文檔
《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/
理論篇
在 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 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套接字,退出程式。
核心空間
與應用程式核心,核心空間也主要完成三件工作:
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) /*應用程式關閉*/
- if(nlh->nlmsg_pid == user_proc.pid)
- user_proc.pid = 0;
- }
- }
- }
- 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-> 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*/