网络协议栈(9)netlink机制
一、为什么需要这种通讯机制
设想用户态和内核交互数据,主要是通过系统调用read,ioctl,或者是proc文件系统。但是read有一个明显的缺点,它是一个字符流机制,不能定制,也就是无论谁从里面读,读到的内容都是相同的。这种无格式的结构对于网络这种非常繁琐的交互方法来说是不够的。例如,要枚举系统中所有的网卡信息、路由信息、邻居信息等,这些结构非常繁琐,使用单单的字符流难以满足需求。更不要说添加或者删除这些操作了。
另一个就是proc文件系统,这个毕竟过于耗时,效率太低,同样不能处理修改动作(无论是添加还是删除)。但是这并不是不可能的,因为早期的很多工具就是这么实现的,例如route程序就是。
对于修改动作,只能通过ioctl这类接口来实现了。可见这种实现方式不统一,而且效率较低,在内核和用户态进行大量数据传送的时候这些问题更加突出。所以需要一些新的机制,这就是netlink机制的由来原因。
netlink底层使用的是socket原语,也就是作为新的地址簇来实现,这也就是说它有自己的帧格式和地址格式,这个格式就是nlmsghdr结构,所有的报文都必须有这个结构头,作为一个报文帧的开始。而一个netlink套接口的地址则通过sockaddr_nl表示。
socket的特点就是可以实现 Clinet/Server 机制,也就是交互性强。另外就是每次传递的数据量可以比较大,当然这是次要原因。
二、netlink套接口的管理
内核作为一个管理核心,它有义务来管理和维护系统中所有的netlink套接口,其中最为基本的功能就是查找/添加/删除之类的,而其中最为重要的就是查找操作了,这个接口实现为netlink_lookup接口。从接口的实现来看,系统中维护了一个具有#define MAX_LINKS 32项的数组,该数组的地址保存在nl_table变量中。这里至少做到了物以类聚,每种不同协议(netlink socket的第三个参数)之间互不干扰。其中的查找算法为
sk_for_each(sk, node, head) {
if (nlk_sk(sk)->pid == pid) {
sock_hold(sk);
goto found;
也就是说,pid是其中最为关键的键值,每个netlink 套接口的protocol+pid可以唯一的确定一个socket实例,这个也可以认为是一个地址。这样一来,套接口的范围被进一步缩小(相对于其他的系统级套接口,例如unix domain)到一个进程单独创建。
三、内核套接口的创建
netlink的原始目的可能就是为了让内核方便的给用户提供服务,所以内核会预先创建一些套接口,这些套接口实例可以作为内核的侦听套接口,接收用户态的套接字请求,例如枚举系统网卡、添加路由等操作。既然是netlink套接口,就需要有自己的地址,而这个地址就是pid+proctol类型。我们一直都知道,init是系统的1号任务,它不能被杀死,相当于一人之下万人之上,而它在一人之下的这一人就可以认为是内核本身。也就是系统的0号任务(也就是通过init_task.c中定义的init_task结构),它的特殊之处就在于它的task_struct结构不是动态分配的,而是在编译的时候就已经确定的一片地址。
当内核创建服务器套接口的时候,它通过extern struct sock *netlink_kernel_create(int unit, unsigned int groups, void (*input)(struct sock *sk, int len), struct module *module);接口来创建一个内核套接口,它和用户态套接口的最大不同就在于它可以指定自己的input函数。当一个套接口向另一个套接口发送报文的时候,内核会调用另一个套接口的data_ready接口,而netlink_kernle_create接口就会将参数中的input赋值给data_ready接口。
sk->sk_data_ready = netlink_data_ready;
if (input)
nlk_sk(sk)->data_ready = input;这里将参数中提供的input接口赋值给新创建的套接口的data_ready函数指针。
if (netlink_insert(sk, 0));这里的第二个参数是0,而这个参数就是进程号,所以0号任务代表的就是内核。
四、地址及报文格式
作为一个协议簇,它应该有自己的报文格式和地址格式。例如,对于以太网来说它的地址就是源和目的网卡的MAC地址,每个地址共6个字节。开始是目的地址,然后是源地址,然后是服务类型等,每个报文的格式都是确定的,这样才能提供通讯的一个基础设施。所以新的netlink尽然可能成为成为一个AF_NETLINK,它同样需要自己的格式,所谓我的协议我做主,指的就是这个意思。
在之前也说到过,netlink是一种进程敏感的套接口,所以它的地址就和系统内的进程号相关,skbuff的寻址同样是需要和地址相关。同时,它作为socket协议簇的一种,它同样遵守socket的编码规范,它可以通过connect来和目的套接口建立一个可靠连接,也可以通过bind来声明自己占用的地址。
但是这里的bind和TCP的bind不同,因为TCP可以绑定系统中所有尚未被占用的端口号,但是如果说一个套接口bind一个和调用者pid不同的地址就没有什么意义了,所以这个bind的地址必须是调用者的地址。这么说这个函数就是画蛇添足了?当然也不是,它的重要功能是为了让一个套接口可以加入多播域,当它加入该域之后,就可以接收到多播消息,而不仅仅是目的为自己pid的单播消息了,这个之后再详细说明一下。
还有一个问题就是地址的冲突问题。因为一个任务可能需要创建相同协议的netlink套接口,比如创建两个NETLINK_ROUTE类型的netlink套接口,这样系统中的pid只有一个,此时如何解决地址冲突问题?假设我们创建套接口之后没有执行bind,就开始发送报文,此时执行的函数为netlink_sendmsg
if (!nlk->pid) {
err = netlink_autobind(sock);
if (err)
goto out;
}
static int netlink_autobind(struct socket *sock)
static s32 rover = -4097;
retry:
cond_resched();
netlink_table_grab();
head = nl_pid_hashfn(hash, pid);
sk_for_each(osk, node, head) {
if (nlk_sk(osk)->pid == pid) {第二个或者之后创建的套接口都会满足这个条件
/* Bind collision, search negative pid values. */
pid = rover--;这里的rover是一个静态变量,所以会更具系统级冲突情况而不断变化,注意它是一个负值,并且比-4097要小,这是因为-4097到-1之间一般是用来做错误码的。
if (rover > -4097)
rover = -4097;
netlink_table_ungrab();
goto retry;
}
}
也就是说,之后创建的套接口的pid和进程 的pid不相等,而且是不确定的随机值(要根据系统中冲突情况来决定),所以我们不能依赖所有的套接口的pid都和创建者的pid相等这个假设。尽管很不情愿,但是新的套接口的pid将会一直保持一个随机的负值,这个将成为它与其它套接口交互的唯一表示ID,例如报文的发送,所以在发送报文的时候,在每个skbuff的CB中要记录发送者的pid,这个pid并不是套接口创建者的pid,而是通过bind/autobind分配的pid。
NETLINK_CB(skb).pid = nlk->pid;
NETLINK_CB(skb).dst_group = dst_group;
例如,在rtnl_getlink中回应消息的时候,就会通过
err = rtnl_fill_ifinfo(nskb, dev, iw, iw_buf_len, RTM_NEWLINK,
NETLINK_CB(skb).pid, nlh->nlmsg_seq, 0, 0);
来确定接收者的地址,所以只要发送者和接收者使用相同的规则和约定,那么就不会有问题。
五、用户消息发送及处理
可以看到,在最后将会调用目标套接口的sk_data_ready接口来处理这个信号,对于内核创建的套接口来说,不同的协议簇有自己不同的处理函数,例如对于ip层使用的相关协议簇,其值为rtnetlink_init中初始化的rtnetlink_rcv接口
netlink_sendmsg-->>netlink_unicast--->>>netlink_sendskb--->>>sk->sk_data_ready(sk, len);
之后的故事请看调用链,这个调用链是我在用户态执行ip address show 时的调用链
(gdb) bt
#0 rtnl_fill_ifinfo (skb=0x0, dev=0x0, iwbuf=0xcfc258b8, iwbuflen=0, type=16,
pid=932, seq=1325167038, change=0, flags=2) at net/core/rtnetlink.c:316
#1 0xc06f9675 in rtnl_dump_ifinfo (skb=0xcfee5360, cb=0xcfeca920)
at net/core/rtnetlink.c:400
#2 0xc070b4a2 in netlink_dump (sk=0xcfe3d000) at net/netlink/af_netlink.c:1357
#3 0xc070be24 in netlink_dump_start (ssk=0xc1270e00, skb=0xcfee5f40,
nlh=0xcfe3d200, dump=0xc06f95e8 <rtnl_dump_ifinfo>, done=0)
at net/netlink/af_netlink.c:1426
#4 0xc06fac1c in rtnetlink_rcv_msg (skb=0xcfee5f40, nlh=0xcfe3d200,
errp=0xcfc25bcc) at net/core/rtnetlink.c:760
#5 0xc070c3cd in netlink_rcv_skb (skb=0xcfee5f40,
cb=0xc06faa8b <rtnetlink_rcv_msg>) at net/netlink/af_netlink.c:1476
#6 0xc070c4ca in netlink_run_queue (sk=0xc1270e00, qlen=0xcfc25c08,
cb=0xc06faa8b <rtnetlink_rcv_msg>) at net/netlink/af_netlink.c:1518
#7 0xc06fae30 in rtnetlink_rcv (sk=0xc1270e00, len=20)
at net/core/rtnetlink.c:810
#8 0xc070b118 in netlink_data_ready (sk=0xc1270e00, len=20)
at net/netlink/af_netlink.c:1255
#9 0xc0709423 in netlink_sendskb (sk=0xc1270e00, skb=0xcfee5f40, protocol=0)
at net/netlink/af_netlink.c:770
#10 0xc07096b1 in netlink_unicast (ssk=0xcfe3d000, skb=0xcfee5f40, pid=0,
nonblock=0) at net/netlink/af_netlink.c:827
#11 0xc070ab37 in netlink_sendmsg (kiocb=0xcfc25de0, sock=0xcff6c980,
---Type <return> to continue, or q <return> to quit---
msg=0xcfc25eb8, len=20) at net/netlink/af_netlink.c:1182
#12 0xc06d4afc in __sock_sendmsg (size=20, msg=0xcfc25eb8, sock=0xcff6c980,
iocb=0xcfc25de0) at net/socket.c:553
#13 sock_sendmsg (size=20, msg=0xcfc25eb8, sock=0xcff6c980, iocb=0xcfc25de0)
at net/socket.c:564
#14 0xc06d6988 in sys_sendto (fd=3, buff=0xbff144ec, len=20, flags=0,
addr=0xbff14500, addr_len=12) at net/socket.c:1573
#15 0xc06d765c in sys_socketcall (call=11, args=0xbff144c8)
at net/socket.c:2022
六、netlink属性
在linux-2.6.21\net\netlink\文件夹下为数不多的三个文件中,还有一个attr.c文件,这个文件从名字上看就是处理一些属性的代码。比方说对于一个网卡,它有很多的属性,例如它的名字,它的MTU,端口编号等信息,并且名字是一个字符串、之后的可能是数值,可能有16bit,也可能32bit,所以这些琐碎的属性同样需要在用户态和内核之间进行交互,而这就是attr.c的作用。它同样提供了一个简单的框架,为每个属性定义了确定的格式。这些格式那TCP/IP类比,可以认为是链路层+IP层+TCP层+应用层(例如ftp的格式等),这个属性的格式由不同的应用自己定义,并不是netlink的范围,netlink只要保证每个报文开始的nlmsg还是格式就好了,就是nlmsghdr+nlmsgdata格式。
例如,对于网口报文,它的有效负载就是格式为
struct ifinfomsg
{
unsigned char ifi_family;
unsigned char __ifi_pad;
unsigned short ifi_type; /* ARPHRD_* */
int ifi_index; /* Link index */
unsigned ifi_flags; /* IFF_* flags */
unsigned ifi_change; /* IFF_* change mask */
};
的结构,并且这个结构之后还可以跟若干个不同结构的属性。所以netlink也给出了属性的格式,就是
/*
* <------- NLA_HDRLEN ------> <-- NLA_ALIGN(payload)-->
* +---------------------+- - -+- - - - - - - - - -+- - -+
* | Header | Pad | Payload | Pad |
* | (struct nlattr) | ing | | ing |
* +---------------------+- - -+- - - - - - - - - -+- - -+
* <-------------- nlattr->nla_len -------------->
*/
struct nlattr
{
__u16 nla_len;
__u16 nla_type;
};
结构,其中的nla_type是属性名,具体每个type对应什么结构则由两端解释。这一点,可以类比一下ELF文件格式中动态节中的各种属性。
七、多播
之前说过多播,netlink的每个协议簇(例如NETLINK_ROUTE、NETLINK_FIREWALL等)都可以定义32个多播域,而netlink套接口可以通过bind系统调用添加到这个多播域中。如果发送报文在自己的地址中指明了某个多播域的话,那么所有加入到这个广播域的套接口都将收到这个报文。
1、bind
netlink_bind
netlink_update_subscriptions(sk, nlk->subscriptions +
hweight32(nladdr->nl_groups) -
hweight32(nlk->groups[0]));这里通过sk_add_bind_node将调用者socket加入到netlink某个协议簇的mc_list链表中。在这个时候没有区分多播组的概念,所有的socket都将加入这个单向线性链表,然后我们将会看到如何区分每个套接口具体是如何只接收自己声明的多播组(而不是该协议簇的所有多播组)。
nlk->groups[0] = (nlk->groups[0] & ~0xffffffffUL) | nladdr->nl_groups;
netlink_update_listeners(sk);
tbl->listeners是一个bit标志位,如果某个bit为1,则表示这个多播组中有听众,应该和内核信号处理中的标志位意义相同。
2、send
netlink_sendmsg
NETLINK_CB(skb).dst_group = dst_group;
if (dst_group) {
atomic_inc(&skb->users);
netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);
}
netlink_broadcast()
sk_for_each_bound(sk, node, &nl_table[ssk->sk_protocol].mc_list)这里循环一个协议的mc_list,这个链表包含了所有多播组的所有套接口。
do_one_broadcast(sk, &info);
在do_one_broadcast函数中
if (nlk->pid == p->pid || p->group - 1 >= nlk->ngroups ||
!test_bit(p->group - 1, nlk->groups))这里进行了过滤,如果套接口加入的多播组nlk->groups不包含当前报文发送的多播组,则这里直接返回。
goto out;
八、杂项
用户态通过rcvmsg接收的数据中可能包含多个nlmsg结构,所以用户态要对这个报文进行遍历。其中系统调用的返回值表示了接收数据的总大小,而每个nlmsghdr中又包含了自己的大小,所以可以对这个流数据按照nlmsg结构进行分割。
如果没有通过netlink_kernel_create指明sk_data_ready实现,则使用默认的通过唤醒方式,也就是通过下面这条路径初始化的套接口唤醒接口。netlink_create-->>__netlink_create--->>sock_init_data------>>>ssk->sk_data_ready = sock_def_readable
通过套接口创建netlink设备的时候,第二个参数必须为SOCK_DGRAM或者SOCK_RAW,虽然不知道有什么区别,判断代码位于static int netlink_create(struct socket *sock, int protocol)函数
if (sock->type != SOCK_RAW && sock->type != SOCK_DGRAM)
return -ESOCKTNOSUPPORT;
大家可以参考linux journal中的一片关于netlink的文章
http://www.linuxjournal.com/article/7356?page=0,0