前面有一篇文章其实已经介绍过Netlink方面的知识,还有一个内核和用户空间之间的一个交互例子,这篇文章主要是更细节和基础的知识介绍!
Netlink 是一种特殊的 socket,它是 Linux 所特有的,由于传送的消息是暂存在socket接收缓存中,并不被接收者立即处理,所以netlink是一种异步通信机制。 系统调用和 ioctl 则是同步通信机制。
用户空间进程可以通过标准socket API来实现消息的发送、接收,在Linux中,有很多用户空间和内核空间的交互都是通过Netlink机制完成的,在Linux3.0的内核版本中定义了下面的21个用于Netlink通信的宏,其中默认的最大值为32.我这里重点关注的是IPv6路由部分的通信过程。
在include/linux/`netlink.h文件中定义了下面的用于通信的宏!
- #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #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
- /* leave room for NETLINK_DM (DM Events) */
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
- #define NETLINK_ECRYPTFS 19
- #define NETLINK_RDMA 20
- #define MAX_LINKS 32
用户态可以使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 等函数就能很容易地使用netlink socket,我们在用户空间可以直接通过socket函数来使用Netlink通信,例如可以通过下面的方式:
- sock = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
说明:第一个参数必须是AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,第二个参数必须是SOCK_RAW或SOCK_DGRAM,第三个参数指定netlink协议类型,可以是自己在netlink.h中定义的,也可以是内核中已经定义好的。上面的例子使用主要是路由的Netlink协议。也可以是上面21中协议类型的其中之一。
NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必再添加新的协议类型。
对于每一个netlink协议类型,可以使用多播的概念,最多可以有 32个多播组,每一个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多播消息的应用而言,大大地降低了系统调用的次数。
下面介绍一下主要的数据结构:
- struct sockaddr_nl {
- sa_family_t nl_family; /* AF_NETLINK */
- unsigned short nl_pad; /* zero */
- __u32 nl_pid; /* port ID */
- __u32 nl_groups; /* multicast groups mask */
- };
说明:(1) sa_family_t nl_family;
一般为AF_NETLINK,
(2) unsigned short nl_pad;
字段 nl_pad 当前没有使用,因此要总是设置为 0,
(3)__u32 nl_pid;
绑定时用于指定绑定者的进程号,发送消息时用于指定接收进程号,如果希望内核处理多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这相当于 netlink socket 的本地地址。但是,对于一个netlink socket 的情况,字段 nl_pid 则可以设置为其它的值,如:
pthread_self()<< 16 | getpid();
因此字段 nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,用户可以根据自己需要设置该字段。
(4) __u32 nl_groups;
绑定时用于指定绑定者所要加入的多播组,这样绑定者就可以接收多播消息,发送 消息时可以用于指定多播组,这样就可以将消息发给多个接收者。这里nl_groups 为32位的无符号整形,所以可以指定32个多播组,每个进程可以加入多个多播组, 因为多播组是通过“或”操作,如果设置为 0,表示调用者不加入任何多播组。这里就是Netlink多播的概念!和通信中的多播概念有点类似。
Bind的调用方式如下!
- struct sockaddr_nl snl;
- memset (&snl, 0, sizeof snl);
- snl.nl_family = AF_NETLINK;
- snl.nl_groups = groups;
- ret = bind (sock, (struct sockaddr *) &snl, sizeof snl);
其中sock为前面的 socket 调用返回的文件描述符,参数snl为 structsockaddr_nl 类型的地址。为了发送一个 netlink 消息给内核或其他用户态应用,需要填充目标 netlink socket 地址,此时,字段 nl_pid 和 nl_groups 分别表示接收消息者的进程 ID 与多播组。如果字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,如果 nl_groups为 0,表示该消息为单播消息,否则表示多播消息。用户态使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec,结构 struct msghdr 的定义如下:
- struct msghdr {
- void * msg_name; /* Socket name */
- int msg_namelen; /* Length of name */
- struct iovec * msg_iov; /* Data blocks */
- __kernel_size_t msg_iovlen; /* Number of blocks */
- void * msg_control; /* Per protocol magic (eg BSD file descriptor passing) */
- __kernel_size_t msg_controllen; /* Length of cmsg list */
- unsigned msg_flags;
- };
使用方法如下:
- struct msghdr msg;
- memset(&msg, 0, sizeof(msg));
- msg.msg_name = (void *)&(snl);
- msg.msg_namelen = sizeof(snl);
struct nlmsghdr 为 netlink socket 自己的消息头,因此它也被称为netlink 控制块。应用层在向内核发送 netlink 消息时必须提供该控制头。
消息头。
- struct nlmsghdr
- {
- __u32 nlmsg_len; /* Length of message including header */
- __u16 nlmsg_type; /* Message content */
- __u16 nlmsg_flags; /* Additional flags */
- __u32 nlmsg_seq; /* Sequence number */
- __u32 nlmsg_pid; /* Sending process PID */
- };
字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段 nlmsg_type 用于应用内部定义消息的类型,它对netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。字段 nlmsg_flags 用于设置消息标志,可用的标志包括下面的宏定义:kernel/include/linux/netlink.c
- /* Flags values */
- #define NLM_F_REQUEST 1 /* It is request message. */
- #define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
- #define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
- #define NLM_F_ECHO 8 /* Echo this request */
- /* Modifiers to GET request */
- #define NLM_F_ROOT 0x100 /* specify tree root */
- #define NLM_F_MATCH 0x200 /* return all matching */
- #define NLM_F_ATOMIC 0x400 /* atomic GET */
- #define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
- /* Modifiers to NEW request */
- #define NLM_F_REPLACE 0x100 /* Override existing */
- #define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
- #define NLM_F_CREATE 0x400 /* Create, if it does not exist */
- #define NLM_F_APPEND 0x800 /* Add to end of list */
标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。
标志NLM_F_MULTI用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。
宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。
标志NLM_F_ECHO表示该消息是相关的一个包的回传。
标志NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条
地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段
nlmsg_type 中指定协议类型。
标志NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。
标志NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。
标志 NLM_F_DUMP未实现。
标志NLM_F_REPLACE 用于取代在数据表中的现有条目。
标志NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。
标志NLM_F_CREATE 指示应当在指定的表中创建一个条目。
标志NLM_F_APPEND 指示在表末尾添加新的条目。
内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些设置),
下面是在调用sendmsg函数之前的各个结构体的赋值操作:
- struct nlmsghdr *n
- struct iovec iov = { (void*) n, n->nlmsg_len };
- struct msghdr msg = {(void*) &snl, sizeof snl, &iov, 1, NULL, 0, 0};
其中snl为 struct sockaddr_nl snl;在结构体structmsghdr中包含有struct iovec结构,其实就是我们要传输的数据块,它为一个指针,定义了数据块的基址和长度。
- struct iovec
- {
- void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
- __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
- };
上面的数据结构全部初始化以后就可以调用sendmsg函数进行发送操作了。
- status = sendmsg (sock, &msg, 0);
其中sock就是我们创建的sock套接字,msg就是上面结构体structmsghdr的实例。如果我们需要返回一个ACK消息,可以对flags标志进行设置如下:
- /* Request an acknowledgement by setting NLM_F_ACK */
- n->nlmsg_flags |= NLM_F_ACK;
使用下面的函数进行接收处理时,status;为返回的状态,这里可能的结果为:
- #define NLMSG_NOOP 0x1 /* Nothing. */
- #define NLMSG_ERROR 0x2 /* Error */
- #define NLMSG_DONE 0x3 /* End of a dump */
- #define NLMSG_OVERRUN 0x4 /* Data lost
- int status;
- char buf[4096];
- struct iovec iov = { buf, sizeof buf };
- struct sockaddr_nl snl;
- struct msghdr msg = { (void*)&snl, sizeof snl, &iov, 1, NULL, 0, 0};
- struct nlmsghdr *h;
- status = recvmsg (sock, &msg, 0);
在linux/netlink.h中定义了一些方便对消息进行处理的宏,这些宏包括:
#define NLMSG_ALIGNTO 4
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) &~(NLMSG_ALIGNTO-1) )
/*宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值*/
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(NLMSG_HDRLEN))
/*宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存*/
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/*宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存*/
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/*宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏*/
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) +NLMSG_ALIGN((nlh)->nlmsg_len)))
/*宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用*/
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr)&& \
(nlh)->nlmsg_len >= sizeof(structnlmsghdr) && \
(nlh)->nlmsg_len <= (len))
/*宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len -NLMSG_SPACE((len)))
/*宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度*/
在/kernel/net/netlink/af_netlink.c文件中定义了netlink套接字的结构体,
- struct netlink_sock {
- /* struct sock has to be the first member of netlink_sock */
- struct sock sk;
- u32 pid; //内核自己的pid,=0
- u32 dst_pid;
- u32 dst_group;//目的组
- u32 flags;
- u32 subscriptions;
- u32 ngroups;// 组数量
- unsigned long *groups; //组号
- unsigned long state;
- wait_queue_head_t wait;// 进程在接收数据包时等待队列
- struct netlink_callback *cb;
- spinlock_t cb_lock;
- void (*data_ready)(struct sock *sk, int bytes); ///内核态接收到用户态信息后的处理函数
- struct module *module;
- };
在af_netlink.c文件中我们可以看到netlink协议的注册
- static struct proto netlink_proto = {
- .name = "NETLINK",
- .owner = THIS_MODULE,
- .obj_size = sizeof(struct netlink_sock),
- };
在static int__init netlink_proto_init(void)函数中会调用注册协议的函数,对netlink协议进行注册,其中,netlink_proto就是上面的struct proto netlink_proto协议。
- int err = proto_register(&netlink_proto, 0);
在内核中接收的数据和存储发送的数据都是放在了skb_buff的结构体中
- struct netlink_skb_parms
- {
- struct ucred creds; /* Skb credentials */
- __u32 pid;
- __u32 dst_pid;
- __u32 dst_group;
- kernel_cap_t eff_cap;
- __u32 loginuid; /* Login (audit) uid */
- };
使用下面的宏获取skb_bff中的数据部分
- #define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))