這是一篇學習筆記,主要是對《Linux?系統內核空間與用戶空間通信的實現與分析》中的源碼imp2的分析。其中的源碼,可以到以下URL下載:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz
在?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?鉤子*/
}
評論
查看更多