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]
在Linux2.4版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用netlink套接字实现的,例如iprote2网络管 理工具,它与内核的交互就全部使用了netlink,著名的内核包过滤框架Netfilter在与用户空间的通读,也在最新版本中改变为netlink, 无疑,它将是Linux用户态与内核态交流的主要方法之一。它的通信依据是一个对应于进程的标识,一般定为该进程的ID。当通信的一端处于中断过程时, 该标识为0。当使用netlink套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则 不同。netlink套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调 用用户事先指定的接收函数。工作原理如图:
如图所示,这里使用了软中断而不是内核线程来接收数据,这样就可以保证数据接收的实时性。
当netlink套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同,下图是netlink套接字实现此类通信时创建的过程:
用户空间
用户态应用使用标准的socket与内核通讯,标准的socketAPI的函数,socket(),bind(),sendmsg(),recvmsg()和close()很容易地应用到netlinksocket。
为了创建一个netlinksocket,用户需要使用如下参数调用socket():
socket(AF_NETLINK,SOCK_RAW,netlink_type)
netlink对应的协议簇是AF_NETLINK,第二个参数必须是SOCK_RAW或SOCK_DGRAM,第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型:
#defineNETLINK_GENERIC16
#defineNETLINK_ROUTE0/*Routing/devicehook*/
#defineNETLINK_W11/*1-wiresubsystem*/
#defineNETLINK_USERSOCK2/*Reservedforusermodesocketprotocols*/
#defineNETLINK_FIREWALL3/*Firewallinghook*/
#defineNETLINK_INET_DIAG4/*INETsocketmonitoring*/
#defineNETLINK_NFLOG5/*netfilter/iptablesULOG*/
#defineNETLINK_XFRM6/*ipsec*/
#defineNETLINK_SELINUX7/*SELinuxeventnotifications*/
#defineNETLINK_ISCSI8/*Open-iSCSI*/
#defineNETLINK_AUDIT9/*auditing*/
#defineNETLINK_FIB_LOOKUP10
#defineNETLINK_CONNECTOR11
#defineNETLINK_NETFILTER12/*netfiltersubsystem*/
#defineNETLINK_IP6_FW13
#defineNETLINK_DNRTMSG14/*DECnetroutingmessages*/
#defineNETLINK_KOBJECT_UEVENT15/*Kernelmessagestouserspace*/
同样地,socket函数返回的套接字,可以交给bing等函数调用:
staticintskfd;
skfd=socket(PF_NETLINK,SOCK_RAW,NL_IMP2);
if(skfd<0)
{
printf("cannotcreateanetlinksocket/n");
exit(0);
}
bind函数需要绑定协议地址,netlink的socket地址使用structsockaddr_nl结构描述:
structsockaddr_nl
{
sa_family_tnl_family;
unsignedshortnl_pad;
__u32nl_pid;
__u32nl_groups;
};
成员nl_family为协议簇AF_NETLINK,成员nl_pad当前没有使用,因此要总是设置为0,成员nl_pid为接 收或发送消息的进程的ID,如果希望内核处理消息或多播消息,就把该字段设置为0,否则设置为处理消息的进程ID。成员nl_groups用于 指定多播组,bind函数用于把调用进程加入到该字段指定的多播组,如果设置为0,表示调用者不加入任何多播组:
structsockaddr_nllocal;
memset(&local,0,sizeof(local));
local.nl_family=AF_NETLINK;
local.nl_pid=getpid(); /*设置pid为自己的pid值*/
local.nl_groups=0;
/*绑定套接字*/
if(bind(skfd,(structsockaddr*)&local,sizeof(local))!=0)
{
printf("bind()error/n");
return-1;
}
用户空间可以调用send函数簇向内核发送消息,如sendto、sendmsg等,同样地,也可以使用structsockaddr_nl来描述一个对端地址,以待send函数来调用,与本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0:
structsockaddr_nlkpeer;
memset(&kpeer,0,sizeof(kpeer));
kpeer.nl_family=AF_NETLINK;
kpeer.nl_pid=0;
kpeer.nl_groups=0;
另一个问题就是发内核发送的消息的组成,使用我们发送一个IP网络数据包的话,则数据包结构为“IP包头+IP数据”,同样地,netlink的消息结构是“netlink消息头部+数据”。Netlink消息头部使用structnlmsghdr结构来描述:
structnlmsghdr
{
__u32nlmsg_len;/*Lengthofmessage*/
__u16nlmsg_type;/*Messagetype*/
__u16nlmsg_flags;/*Additionalflags*/
__u32nlmsg_seq;/*Sequencenumber*/
__u32nlmsg_pid;/*SendingprocessPID*/
};
字段nlmsg_len指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用netlink提供的宏NLMSG_LENGTH来计算这个长度,仅需向NLMSG_LENGTH宏提供要发送的数据的长度,它会自动计算对齐后的总长度:
/*计算包含报头的数据报长度*/
#defineNLMSG_LENGTH(len)((len)+NLMSG_ALIGN(sizeof(structnlmsghdr)))
/*字节对齐*/
#defineNLMSG_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。
structmsg_to_kernel /*自定义消息首部,它仅包含了netlink的消息首部*/
{
structnlmsghdrhdr;
};
structmsg_to_kernelmessage;
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,
(structsockaddr*)&kpeer,sizeof(kpeer));
当发送完请求后,就可以调用recv函数簇从内核接收数据了,接收到的数据包含了netlink消息首部和要传输的数据:
/*接收的数据包含了netlink消息首部和自定义数据结构*/
structu_packet_info
{
structnlmsghdrhdr;
structpacket_infoicmp_info;
};
structu_packet_infoinfo;
while(1)
{
kpeerlen=sizeof(structsockaddr_nl);
/*接收内核空间返回的数据*/
rcvlen=recvfrom(skfd,&info,sizeof(structu_packet_info),
0,(structsockaddr*)&kpeer,&kpeerlen);
/*处理接收到的数据*/
……
}
同样地,函数close用于关闭打开的netlinksocket。程序中,因为程序一直循环接收处理内核的消息,需要收到用户的关闭信号才会退出,所以关闭套接字的工作放在了自定义的信号函数sig_int中处理:
/*这个信号函数,处理一些程序退出时的动作*/
staticvoidsig_int(intsigno)
{
structsockaddr_nlkpeer;
structmsg_to_kernelmessage;
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,(structsockaddr*)(&kpeer),sizeof(kpeer));
close(skfd);
exit(0);
}
这个结束函数中,向内核发送一个“我已经退出了”的消息,然后调用close函数关闭netlink套接字,退出程序。
[size=3]内核空间[/size]
与应用程序内核,内核空间也主要完成三件工作:
n 创建netlink套接字
n 接收处理用户空间发送的数据
n 发送数据至用户空间
API函数netlink_kernel_create用于创建一个netlinksocket,同时,注册一个回调函数,用于接收处理用户空间的消息:
structsock*
netlink_kernel_create(intunit,void(*input)(structsock*sk,intlen));
参数unit表示netlink协议类型,如NL_IMP2,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个 netlinksocket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数 netlink_kernel_create返回的structsock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的 socket在内核中也会有一个structsock结构来表示。
staticint__initinit(void)
{
rwlock_init(&user_proc.lock); /*初始化读写锁*/
/*创建一个netlinksocket,协议类型是自定义的ML_IMP2,kernel_reveive为接受处理函数*/
nlfd=netlink_kernel_create(NL_IMP2,kernel_receive);
if(!nlfd) /*创建失败*/
{
printk("cannotcreateanetlinksocket/n");
return-1;
}
/*注册一个Netfilter钩子*/
returnnf_register_hook(&imp2_ops);
}
module_init(init);
用户空间向内核发送了两种自定义消息类型:IMP2_U_PID和IMP2_CLOSE,分别是请求和关闭。kernel_receive函数分别处理这两种消息:
DECLARE_MUTEX(receive_sem); /*初始化信号量*/
staticvoidkernel_receive(structsock*sk,intlen)
{
do
{
structsk_buff*skb;
if(down_trylock(&receive_sem)) /*获取信号量*/
return;
/*从接收队列中取得skb,然后进行一些基本的长度的合法性校验*/
while((skb=skb_dequeue(&sk->receive_queue))!=NULL)
{
{
structnlmsghdr*nlh=NULL;
if(skb->len>=sizeof(structnlmsghdr))
{
/*获取数据中的nlmsghdr结构的报头*/
nlh=(structnlmsghdr*)skb->data;
if((nlh->nlmsg_len>=sizeof(structnlmsghdr))
&&(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);
}
elseif(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)用于取得socketsk的接收队列上的消息,返回为一个structsk_buff的结 构,skb->data指向实际的netlink消息。
程序中注册了一个Netfilter钩子,钩子函数是get_icmp,它截获ICMP数据包,然后调用send_to_user函数将数据发送 给应用空间进程。发送的数据是info结构变量,它是structpacket_info结构,这个结构包含了来源/目的地址两个成员。 NetfilterHook不是本文描述的重点,略过。
send_to_user用于将数据发送给用户空间进程,发送调用的是API函数netlink_unicast完成的:
intnetlink_unicast(structsock*sk,structsk_buff*skb,u32pid,intnonblock);
参数sk为函数netlink_kernel_create()返回的套接字,参数skb存放待发送的消息,它的data字段指向要发送的 netlink消息结构,而skb的控制块保存了消息的地址信息,参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如 果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。
向用户空间进程发送的消息包含三个部份:netlink消息头部、数据部份和控制字段,控制字段包含了内核发送netlink消息时,需要设置 的目标地址与源地址,内核中消息是通过sk_buff来管理的,linux/netlink.h中定义了NETLINK_CB宏来方便消息的地址设置:
#defineNETLINK_CB(skb)(*(structnetlink_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。
staticintsend_to_user(structpacket_info*info)
{
intret;
intsize;
unsignedchar*old_tail;
structsk_buff*skb;
structnlmsghdr*nlh;
structpacket_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(structpacket_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消息首部:
#defineNLMSG_PUT(skb,pid,seq,type,len)/
({if(skb_tailroom(skb)<(int)NLMSG_SPACE(len))gotonlmsg_failure;/
__nlmsg_put(skb,pid,seq,type,len);})
static__inline__structnlmsghdr*
__nlmsg_put(structsk_buff*skb,u32pid,u32seq,inttype,intlen)
{
structnlmsghdr*nlh;
intsize=NLMSG_LENGTH(len);
nlh=(structnlmsghdr*)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;
returnnlh;
}
这个宏一个需要注意的地方是调用了nlmsg_failure标签,所以在程序中应该定义这个标签。
在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlinksocket:
voidsock_release(structsocket*sock);
程序在退出模块中释放netlinksockets和netfilterhook:
staticvoid__exitfini(void)
{
if(nlfd)
{
sock_release(nlfd->socket); /*释放netlinksocket*/
}
nf_unregister_hook(&imp2_ops); /*撤锁netfilter钩子*/
}