原理
Netlink是一种在内核态和用户态可以进行双向数据传输的通信机制,也就是说,用户进程既可以作为服务器端又可以作为客户端,内核也是如此。用户进程和内核谁是服务器端谁是客户端,这个问题与谁先主动发起数据交互会话有关。
用户进程主动向内核发起会话在Linux内核中很常见,比如系统调用、对/proc的操作等。本文通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。
用户态使用标准的socket API就可以使用netlink提供的强大功能,内核态需要使用专门的内核API来使用netlink。
Netlink相对于系统调用,ioctl以及/proc文件系统而言具有以下优点:
1,为了使用 netlink,用户仅需要在include/linux/netlink.h 中增加一个新类型的 netlink 协议定义即可, 如 #define NETLINK_MYTEST 17 然后,内核和用户态应用就可以立即通过socket API 使用该netlink 协议类型进行数据交换。但系统调用需要增加新的系统调用,ioctl则需要增加设备或文件, 那需要不少代码,proc文件系统则需要在/proc 下添加新的文件或目录,那将使本来就混乱的/proc 更加混乱。
2. netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接 收队列,而不需要等待接收者收到消息,但系统调用与ioctl 则是同步通信机制,如果传递的数据太长,将影响调度粒度。
3.使用netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。
4.netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性,任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件。
5.内核可以使用netlink 首先发起会话,但系统调用和 ioctl 只能由用户应用发起调用。
用户空间
用户态应用使用标准的socket与内核通讯,标准的socket API 的函数,socket(), bind(), sendmsg(), recvmsg() 和 close()很容易地应用到netlink socket。
为了创建一个netlink socket,用户需要使用如下参数调用 socket():
socket(AF_NETLINK,SOCK_RAW, netlink_type)
同样地,socket函数返回的套接字,可以交给bing等函数调用:
staticint 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地址使用structsockaddr_nl结构描述:
structsockaddr_nl
{
sa_family_t nl_family;
unsignedshort nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
成员nl_family为协议簇AF_NETLINK,成员nl_pad 当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。成员nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组:
structsockaddr_nl local;
memset(&local,0, sizeof(local));
local.nl_family= AF_NETLINK;
local.nl_pid= getpid(); /*设置pid为自己的pid值*/
local.nl_groups= 0;
用户空间可以调用send函数簇向内核发送消息,如sendto、sendmsg等,同样地,也可以使用structsockaddr_nl来描述一个对端地址,以待send函数来调用,与本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0:
structsockaddr_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消息头部使用structnlmsghdr结构来描述:
structnlmsghdr
{
__u32nlmsg_len; /* Length of message */
__u16nlmsg_type; /* Message type*/
__u16nlmsg_flags; /* Additional flags */
__u32nlmsg_seq; /* Sequence number */
__u32nlmsg_pid; /* Sending process PID */
};
字段nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用netlink提供的宏NLMSG_LENGTH来计算这个长度,仅需向NLMSG_LENGTH宏提供要发送的数据的长度,它会自动计算对齐后的总长度:
/*计算包含报头的数据报长度*/
#defineNLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字节对齐*/
#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。
内核空间
与应用程序内核,内核空间也主要完成三件工作:
1.创建netlink套接字
2.接收处理用户空间发送的数据
3. 发送数据至用户空间
API函数netlink_kernel_create用于创建一个netlinksocket,同时,注册一个回调函数,用于接收处理用户空间的消息:
static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
static struct netlink_kernel_cfg cfg = {
.input = kernel_receive,
};
参数unit表示netlink协议类型;sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个structsock结构来表示。
向用户空间进程发送的消息包含三个部份: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。
send_to_user用于将数据发送给用户空间进程,发送调用的是API函数netlink_unicast完成的:
intnetlink_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,该函 数在没有接收缓存可利用时睡眠。
staticint send_to_user(struct packet_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(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;
/*发送数据*/
ret =netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
}
函数初始化netlink消息首部,填充数据区,然后设置控制字段,这三部份都包含在skb_buff中,最后调用netlink_unicast函数把数据发送出去。
用户空间示例:
内核空间示例:
运行结果:
可以看到用户空间接收到内核发送的信息!