- 論壇徽章:
- 0
|
Linux 用戶態(tài)與內(nèi)核態(tài)的交互
——netlink 篇
作者:Kendo
2006-9-3
這是一篇學(xué)習(xí)筆記,主要是對《Linux 系統(tǒng)內(nèi)核空間與用戶空間通信的實(shí)現(xiàn)與分析》中的源碼imp2的分析。其中的源碼,可以到以下URL下載:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz
參考文檔
《Linux 系統(tǒng)內(nèi)核空間與用戶空間通信的實(shí)現(xiàn)與分析》 陳鑫
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux
《在 Linux 下用戶空間與內(nèi)核空間數(shù)據(jù)交換的方式》 楊燚
http://www-128.ibm.com/developerworks/cn/linux/l-kerns-usrs/
理論篇
在 Linux 2.4 版以后版本的內(nèi)核中,幾乎全部的中斷過程與用戶態(tài)進(jìn)程的通信都是使用 netlink 套接字實(shí)現(xiàn)的,例如iprote2網(wǎng)絡(luò)管理工具,它與內(nèi)核的交互就全部使用了netlink,著名的內(nèi)核包過濾框架Netfilter在與用戶空間的通讀,也在最新版本中改變?yōu)閚etlink,無疑,它將是Linux用戶態(tài)與內(nèi)核態(tài)交流的主要方法之一。它的通信依據(jù)是一個(gè)對應(yīng)于進(jìn)程的標(biāo)識,一般定為該進(jìn)程的 ID。當(dāng)通信的一端處于中斷過程時(shí),該標(biāo)識為 0。當(dāng)使用 netlink 套接字進(jìn)行通信,通信的雙方都是用戶態(tài)進(jìn)程,則使用方法類似于消息隊(duì)列。但通信雙方有一端是中斷過程,使用方法則不同。netlink 套接字的最大特點(diǎn)是對中斷過程的支持,它在內(nèi)核空間接收用戶空間數(shù)據(jù)時(shí)不再需要用戶自行啟動一個(gè)內(nèi)核線程,而是通過另一個(gè)軟中斷調(diào)用用戶事先指定的接收函數(shù)。工作原理如圖:
![]()
如圖所示,這里使用了軟中斷而不是內(nèi)核線程來接收數(shù)據(jù),這樣就可以保證數(shù)據(jù)接收的實(shí)時(shí)性。
當(dāng) netlink 套接字用于內(nèi)核空間與用戶空間的通信時(shí),在用戶空間的創(chuàng)建方法和一般套接字使用類似,但內(nèi)核空間的創(chuàng)建方法則不同,下圖是 netlink 套接字實(shí)現(xiàn)此類通信時(shí)創(chuàng)建的過程:
![]()
用戶空間
用戶態(tài)應(yīng)用使用標(biāo)準(zhǔn)的socket與內(nèi)核通訊,標(biāo)準(zhǔn)的socket API 的函數(shù), socket(), bind(), sendmsg(), recvmsg() 和 close()很容易地應(yīng)用到 netlink socket。
為了創(chuàng)建一個(gè) netlink socket,用戶需要使用如下參數(shù)調(diào)用 socket():
- socket(AF_NETLINK, SOCK_RAW, netlink_type)
復(fù)制代碼
netlink對應(yīng)的協(xié)議簇是 AF_NETLINK,第二個(gè)參數(shù)必須是SOCK_RAW或SOCK_DGRAM, 第三個(gè)參數(shù)指定netlink協(xié)議類型,它可以是一個(gè)自定義的類型,也可以使用內(nèi)核預(yù)定義的類型:
- #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 */
復(fù)制代碼 #define NETLINK_GENERIC 16
同樣地,socket函數(shù)返回的套接字,可以交給bing等函數(shù)調(diào)用:
- static int skfd;
- skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
- if(skfd < 0)
- {
- printf("can not create a netlink socket\n");
- exit(0);
- }
復(fù)制代碼
bind函數(shù)需要綁定協(xié)議地址,netlink的socket地址使用struct sockaddr_nl結(jié)構(gòu)描述:
- struct sockaddr_nl
- {
- sa_family_t nl_family;
- unsigned short nl_pad;
- __u32 nl_pid;
- __u32 nl_groups;
- };
復(fù)制代碼
成員 nl_family為協(xié)議簇 AF_NETLINK,成員 nl_pad 當(dāng)前沒有使用,因此要總是設(shè)置為 0,成員 nl_pid 為接收或發(fā)送消息的進(jìn)程的 ID,如果希望內(nèi)核處理消息或多播消息,就把該字段設(shè)置為 0,否則設(shè)置為處理消息的進(jìn)程 ID。成員 nl_groups 用于指定多播組,bind 函數(shù)用于把調(diào)用進(jìn)程加入到該字段指定的多播組,如果設(shè)置為 0,表示調(diào)用者不加入任何多播組:
- struct sockaddr_nl local;
- memset(&local, 0, sizeof(local));
- local.nl_family = AF_NETLINK;
- local.nl_pid = getpid(); /*設(shè)置pid為自己的pid值*/
- local.nl_groups = 0;
- /*綁定套接字*/
- if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)
- {
- printf("bind() error\n");
- return -1;
- }
復(fù)制代碼
用戶空間可以調(diào)用send函數(shù)簇向內(nèi)核發(fā)送消息,如sendto、sendmsg等,同樣地,也可以使用struct sockaddr_nl來描述一個(gè)對端地址,以待send函數(shù)來調(diào)用,與本地地址稍不同的是,因?yàn)閷Χ藶閮?nèi)核,所以nl_pid成員需要設(shè)置為0:
- struct sockaddr_nl kpeer;
- memset(&kpeer, 0, sizeof(kpeer));
- kpeer.nl_family = AF_NETLINK;
- kpeer.nl_pid = 0;
- kpeer.nl_groups = 0;
復(fù)制代碼
另一個(gè)問題就是發(fā)內(nèi)核發(fā)送的消息的組成,使用我們發(fā)送一個(gè)IP網(wǎng)絡(luò)數(shù)據(jù)包的話,則數(shù)據(jù)包結(jié)構(gòu)為“IP包頭+IP數(shù)據(jù)”,同樣地,netlink的消息結(jié)構(gòu)是“netlink消息頭部+數(shù)據(jù)”。Netlink消息頭部使用struct nlmsghdr結(jié)構(gòu)來描述:
- 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 */
- };
復(fù)制代碼
字段 nlmsg_len 指定消息的總長度,包括緊跟該結(jié)構(gòu)的數(shù)據(jù)部分長度以及該結(jié)構(gòu)的大小,一般地,我們使用netlink提供的宏NLMSG_LENGTH來計(jì)算這個(gè)長度,僅需向NLMSG_LENGTH宏提供要發(fā)送的數(shù)據(jù)的長度,它會自動計(jì)算對齊后的總長度:
- /*計(jì)算包含報(bào)頭的數(shù)據(jù)報(bào)長度*/
- #define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
- /*字節(jié)對齊*/
- #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
復(fù)制代碼
后面還可以看到很多netlink提供的宏,這些宏可以為我們編寫netlink宏提供很大的方便。
字段 nlmsg_type 用于應(yīng)用內(nèi)部定義消息的類型,它對 netlink 內(nèi)核實(shí)現(xiàn)是透明的,因此大部分情況下設(shè)置為 0,字段 nlmsg_flags 用于設(shè)置消息標(biāo)志,對于一般的使用,用戶把它設(shè)置為 0 就可以,只是一些高級應(yīng)用(如 netfilter 和路由 daemon 需要它進(jìn)行一些復(fù)雜的操作),字段 nlmsg_seq 和 nlmsg_pid 用于應(yīng)用追蹤消息,前者表示順序號,后者為消息來源進(jìn)程 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); /*計(jì)算消息,因?yàn)檫@里只是發(fā)送一個(gè)請求消息,沒有多余的數(shù)據(jù),所以,數(shù)據(jù)長度為0*/
- message.hdr.nlmsg_flags = 0;
- message.hdr.nlmsg_type = IMP2_U_PID; /*設(shè)置自定義消息類型*/
- message.hdr.nlmsg_pid = local.nl_pid; /*設(shè)置發(fā)送者的PID*/
- 這樣,有了本地地址、對端地址和發(fā)送的數(shù)據(jù),就可以調(diào)用發(fā)送函數(shù)將消息發(fā)送給內(nèi)核了:
- /*發(fā)送一個(gè)請求*/
- sendto(skfd, &message, message.hdr.nlmsg_len, 0,
- (struct sockaddr*)&kpeer, sizeof(kpeer));
復(fù)制代碼
當(dāng)發(fā)送完請求后,就可以調(diào)用recv函數(shù)簇從內(nèi)核接收數(shù)據(jù)了,接收到的數(shù)據(jù)包含了netlink消息首部和要傳輸?shù)臄?shù)據(jù):
- /*接收的數(shù)據(jù)包含了netlink消息首部和自定義數(shù)據(jù)結(jié)構(gòu)*/
- struct u_packet_info
- {
- struct nlmsghdr hdr;
- struct packet_info icmp_info;
- };
- struct u_packet_info info;
- while(1)
- {
- kpeerlen = sizeof(struct sockaddr_nl);
- /*接收內(nèi)核空間返回的數(shù)據(jù)*/
- rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
- 0, (struct sockaddr*)&kpeer, &kpeerlen);
-
- /*處理接收到的數(shù)據(jù)*/
- ……
- }
復(fù)制代碼
同樣地,函數(shù)close用于關(guān)閉打開的netlink socket。程序中,因?yàn)槌绦蛞恢毖h(huán)接收處理內(nèi)核的消息,需要收到用戶的關(guān)閉信號才會退出,所以關(guān)閉套接字的工作放在了自定義的信號函數(shù)sig_int中處理:
- /*這個(gè)信號函數(shù),處理一些程序退出時(shí)的動作*/
- 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();
- /*向內(nèi)核發(fā)送一個(gè)消息,由nlmsg_type表明,應(yīng)用程序?qū)㈥P(guān)閉*/
- sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr *)(&kpeer), sizeof(kpeer));
- close(skfd);
- exit(0);
- }
復(fù)制代碼
這個(gè)結(jié)束函數(shù)中,向內(nèi)核發(fā)送一個(gè)“我已經(jīng)退出了”的消息,然后調(diào)用close函數(shù)關(guān)閉netlink套接字,退出程序。
內(nèi)核空間
與應(yīng)用程序內(nèi)核,內(nèi)核空間也主要完成三件工作:
n 創(chuàng)建netlink套接字
n 接收處理用戶空間發(fā)送的數(shù)據(jù)
n 發(fā)送數(shù)據(jù)至用戶空間
API函數(shù)netlink_kernel_create用于創(chuàng)建一個(gè)netlink socket,同時(shí),注冊一個(gè)回調(diào)函數(shù),用于接收處理用戶空間的消息:
- struct sock *
- netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
復(fù)制代碼
參數(shù)unit表示netlink協(xié)議類型,如NL_IMP2,參數(shù)input則為內(nèi)核模塊定義的netlink消息處理函數(shù),當(dāng)有消息到達(dá)這個(gè)netlink socket時(shí),該input函數(shù)指針就會被引用。函數(shù)指針input的參數(shù)sk實(shí)際上就是函數(shù)netlink_kernel_create返回的struct sock指針,sock實(shí)際是socket的一個(gè)內(nèi)核表示數(shù)據(jù)結(jié)構(gòu),用戶態(tài)應(yīng)用創(chuàng)建的socket在內(nèi)核中也會有一個(gè)struct sock結(jié)構(gòu)來表示。
- static int __init init(void)
- {
- rwlock_init(&user_proc.lock); /*初始化讀寫鎖*/
- /*創(chuàng)建一個(gè)netlink socket,協(xié)議類型是自定義的ML_IMP2,kernel_reveive為接受處理函數(shù)*/
- nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
- if(!nlfd) /*創(chuàng)建失敗*/
- {
- printk("can not create a netlink socket\n");
- return -1;
- }
- /*注冊一個(gè)Netfilter 鉤子*/
- return nf_register_hook(&imp2_ops);
- }
- module_init(init);
復(fù)制代碼
用戶空間向內(nèi)核發(fā)送了兩種自定義消息類型:IMP2_U_PID和IMP2_CLOSE,分別是請求和關(guān)閉。kernel_receive 函數(shù)分別處理這兩種消息:
- DECLARE_MUTEX(receive_sem); /*初始化信號量*/
- static void kernel_receive(struct sock *sk, int len)
- {
- do
- {
- struct sk_buff *skb;
- if(down_trylock(&receive_sem)) /*獲取信號量*/
- return;
- /*從接收隊(duì)列中取得skb,然后進(jìn)行一些基本的長度的合法性校驗(yàn)*/
- while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
- {
- {
- struct nlmsghdr *nlh = NULL;
-
- if(skb->len >= sizeof(struct nlmsghdr))
- {
- /*獲取數(shù)據(jù)中的nlmsghdr 結(jié)構(gòu)的報(bào)頭*/
- nlh = (struct nlmsghdr *)skb->data;
- if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))
- && (skb->len >= nlh->nlmsg_len))
- {
- /*長度的全法性校驗(yàn)完成后,處理應(yīng)用程序自定義消息類型,主要是對用戶PID的保存,即為內(nèi)核保存“把消息發(fā)送給誰”*/
- 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) /*應(yīng)用程序關(guān)閉*/
- {
- 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);
- }
復(fù)制代碼
因?yàn)閮?nèi)核模塊可能同時(shí)被多個(gè)進(jìn)程同時(shí)調(diào)用,所以函數(shù)中使用了信號量和鎖來進(jìn)行互斥。skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收隊(duì)列上的消息,返回為一個(gè)struct sk_buff的結(jié)構(gòu),skb->data指向?qū)嶋H的netlink消息。
程序中注冊了一個(gè)Netfilter鉤子,鉤子函數(shù)是get_icmp,它截獲ICMP數(shù)據(jù)包,然后調(diào)用send_to_user函數(shù)將數(shù)據(jù)發(fā)送給應(yīng)用空間進(jìn)程。發(fā)送的數(shù)據(jù)是info結(jié)構(gòu)變量,它是struct packet_info結(jié)構(gòu),這個(gè)結(jié)構(gòu)包含了來源/目的地址兩個(gè)成員。Netfilter Hook不是本文描述的重點(diǎn),略過。
send_to_user 用于將數(shù)據(jù)發(fā)送給用戶空間進(jìn)程,發(fā)送調(diào)用的是API函數(shù)netlink_unicast 完成的:
- int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
復(fù)制代碼
參數(shù)sk為函數(shù)netlink_kernel_create()返回的套接字,參數(shù)skb存放待發(fā)送的消息,它的data字段指向要發(fā)送的netlink消息結(jié)構(gòu),而skb的控制塊保存了消息的地址信息, 參數(shù)pid為接收消息進(jìn)程的pid,參數(shù)nonblock表示該函數(shù)是否為非阻塞,如果為1,該函數(shù)將在沒有接收緩存可利用時(shí)立即返回,而如果為0,該函數(shù)在沒有接收緩存可利用時(shí)睡眠。
向用戶空間進(jìn)程發(fā)送的消息包含三個(gè)部份:netlink 消息頭部、數(shù)據(jù)部份和控制字段,控制字段包含了內(nèi)核發(fā)送netlink消息時(shí),需要設(shè)置的目標(biāo)地址與源地址,內(nèi)核中消息是通過sk_buff來管理的, linux/netlink.h中定義了NETLINK_CB宏來方便消息的地址設(shè)置:
- #define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))
復(fù)制代碼
例如:
- NETLINK_CB(skb).pid = 0;
- NETLINK_CB(skb).dst_pid = 0;
- NETLINK_CB(skb).dst_group = 1;
復(fù)制代碼
字段pid表示消息發(fā)送者進(jìn)程ID,也即源地址,對于內(nèi)核,它為 0, dst_pid 表示消息接收者進(jìn)程 ID,也即目標(biāo)地址,如果目標(biāo)為組或內(nèi)核,它設(shè)置為 0,否則 dst_group 表示目標(biāo)組地址,如果它目標(biāo)為某一進(jìn)程或內(nèi)核,dst_group 應(yīng)當(dāng)設(shè)置為 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;
- /*計(jì)算消息總長:消息首部加上數(shù)據(jù)加度*/
- size = NLMSG_SPACE(sizeof(*info));
- /*分配一個(gè)新的套接字緩存*/
- skb = alloc_skb(size, GFP_ATOMIC);
- old_tail = skb->tail;
- /*初始化一個(gè)netlink消息首部*/
- nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
- /*跳過消息首部,指向數(shù)據(jù)區(qū)*/
- packet = NLMSG_DATA(nlh);
- /*初始化數(shù)據(jù)區(qū)*/
- memset(packet, 0, sizeof(struct packet_info));
- /*填充待發(fā)送的數(shù)據(jù)*/
- packet->src = info->src;
- packet->dest = info->dest;
- /*計(jì)算skb兩次長度之差,即netlink的長度總和*/
- nlh->nlmsg_len = skb->tail - old_tail;
- /*設(shè)置控制字段*/
- NETLINK_CB(skb).dst_groups = 0;
- /*發(fā)送數(shù)據(jù)*/
- read_lock_bh(&user_proc.lock);
- ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
- read_unlock_bh(&user_proc.lock);
- }
復(fù)制代碼
函數(shù)初始化netlink 消息首部,填充數(shù)據(jù)區(qū),然后設(shè)置控制字段,這三部份都包含在skb_buff中,最后調(diào)用netlink_unicast函數(shù)把數(shù)據(jù)發(fā)送出去。
函數(shù)中調(diào)用了netlink的一個(gè)重要的宏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;
- }
復(fù)制代碼
這個(gè)宏一個(gè)需要注意的地方是調(diào)用了nlmsg_failure標(biāo)簽,所以在程序中應(yīng)該定義這個(gè)標(biāo)簽。
在內(nèi)核中使用函數(shù)sock_release來釋放函數(shù)netlink_kernel_create()創(chuàng)建的netlink socket:
- void sock_release(struct socket * sock);
復(fù)制代碼
程序在退出模塊中釋放netlink sockets和netfilter hook:
- static void __exit fini(void)
- {
- if(nlfd)
- {
- sock_release(nlfd->socket); /*釋放netlink socket*/
- }
- nf_unregister_hook(&imp2_ops); /*撤鎖netfilter 鉤子*/
- }
復(fù)制代碼 |
|