参考书籍
《精通Linux内核网络》 Rami Rosen著(罗伊森)
Netlink 也是一种套接字,就是socket,跟TCP/UDP的socket是类似的,但是,不同的是,TCP/UDP层的socket是 BSD socket。Netlink是独立的一套协议族。
作用
这是该参考书中的说明,这段说明,真是扯淡,对于没有从事过内核网络开发的人来说,这简直是天书。
说白了,这也是一种通信协议,不同的是,一般所说的通信协议,指的是不同主机,包括网络设备之间的通信,可以是有线,也可以是无线。
但是,通信,不仅仅是发生在不同的网络设备之间,也可能发生在一个机器内部,比如进程与进程之间,这就是所谓的进程间通信,手段有信号,信号量,消息队列,共享内存,管道,unix域套接字等等,Unix环境高级编程卷三,就是专门说的这个进程间通信。除了进程与进程之间,内核与进程之间,也是需要通信的。前面也有过一些例子,copy_to_user,copy_from_user之类的,通过ioctl 或者 write/read 进行从内核到用户空间数据的传输,这个走的是文件系统(我猜的)。而Netlink就是一种用户空间与内核之间的通信机制,可以实现双向的通信,并且,Netlink还可以进行内核不同模块之间的通信。当然,不同的进程与内核通信,就能通过Netlink进行进程间通信,但是,不推荐这么干。
Netlink协议,优势在于,第一,不需要轮询。第二,内核可以主动往应用层发送异步消息,而不需要用户空间触发,也就是不需要用户空间去调用什么ioctl。第三,Netlink套接字支持组播。
其实除了以上的优点,我在想,是否还有其他的优点,以下说的未必准确:
第一,Netlink封装了一层,有效的去抽象出了一个功能模块,协助内核内部其他模块的通信,架构层面,更加清晰了,编码更容易了。
第二,Linux内核的网络编程里面,网络数据的流转,都是通过 skb 去流转的,其实都是 skb 的指针,而netlink 虽然抽象出了这样一层通信工具层,但是,数据流转依旧是以skb指针去流转,在内核内部,依旧遵循了 零拷贝 策略,保证了性能。
第三,没有那些过程中依赖的文件里,比如之前博客中的字符设备,/dev 下会创建一个设备文件,netlink就不会创建了。
创建方式
对于用户空间,也就是进程代码中,就当成 socket 创建就可以。
family参数: AF_NETLINK , 与TCP/UDP的 AF_INET / AF_INET6 不同。
第二个参数:SOCK_RAW 或者 SOCK_DGRAM,当然这里面加了个 CLOEXEC 标志。这个fd的标志,不写,也是默认加的,所以可以不显式的去写。至于干嘛的,跟 dup,fork, vfork , 以及 exec 系列族有关。我们要关心的就是 fork的时候,fd会被dup的方式被继承到子进程,如果调用exec系列族函数,那么有这个标志,这个fd就不会被继承了,失效了,否则,这个fd依旧可以读写,前面有博客提到了fork 针对 fd的处理,dup或者fork继承了内核的同一张文件表项。
第三个参数:这是netlink众多协议族中的一种NETLINK_ROUTE。netlink是协议族,这是其中一个协议的规定,利用了netlink的框架,我们自己试验,可以自己定义一个协议,不跟其他的去混淆。
我们不按照这个书去走,这个书上说的,废话连篇,而且,看完之后,你也写不出一个实验来,很扯淡。
我们做个试验
场景:内核与进程通信
内核作为一个 server 端,也就说,不主动给进程空间发消息,而是被动的监听用户进程消息,收到了之后呢,回复一个消息给到用户层,然后就结束,类似回射模型,并且,用同步的方式去处理。
内核层内,我们用动态模块的方式去加载进内核。
用户空间,就写个linux的app即可。
内核代码
// server_netlink.c
#include <linux/init.h>
#include <linux/module.h>
#include <net/sock.h>
#include <net/netlink.h>
#include <linux/rwsem.h>
#include <linux/idr.h>
#include <linux/string.h>
#define NETLINK_TEST 30
#define USER_PORT 100
extern struct net init_net;
static struct sock *s_netlink;
static DECLARE_RWSEM(hello_lock);
static int hello_send_msg_2_user(char *pbuf, uint16_t len)
{
struct sk_buff *nl_skb;
struct nlmsghdr *nlh;
int ret = 0;
/* 创建sk_buff 空间 */
nl_skb = nlmsg_new(len, GFP_ATOMIC);
if(!nl_skb)
{
printk("netlink alloc failure !\n");
return -1;
}
/* 设置netlink消息头部 */
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
if(nlh == NULL)
{
printk("nlmsg_put failaure !\n");
nlmsg_free(nl_skb);
return -1;
}
/* 拷贝数据发送 */
memcpy(nlmsg_data(nlh), pbuf, len);
ret = netlink_unicast(s_netlink, nl_skb, USER_PORT, MSG_DONTWAIT);
printk("=[frocheng]=[%s]=[%s]=\n", __FILE__, __func__);
return ret;
}
static int hello_handle_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
struct nlmsghdr *nhdr;
char *umsg = NULL;
char *kmsg = "Hi, it's msg from kernel !";
printk("=[frocheng]=[%s]=[%s]=\n", __FILE__, __func__);
if(skb->len >= nlmsg_total_size(0))
{
nhdr = nlmsg_hdr(skb);
umsg = NLMSG_DATA(nlh);
if(umsg)
{
printk("msg recv from user = %s\n", umsg);
hello_send_msg_2_user(kmsg, strlen(kmsg));
}
else
{
printk("msg recv from is null\n");
}
}
return 0;
}
static void hello_rcv(struct sk_buff *skb)
{
printk("=[frocheng]=[%s]=[%s]=\n", __FILE__, __func__);
//down_read(&hello_lock);
//netlink_rcv_skb(skb, &hello_handle_msg);
//up_read(&hello_lock);
hello_handle_msg(skb, NULL, NULL);
}
static struct netlink_kernel_cfg cfg = {
.input = hello_rcv,
.flags = NL_CFG_F_NONROOT_RECV | NL_CFG_F_NONROOT_SEND,
//.bind = hello_bind,
//.unbind = hello_unbind,
};
static int __init hello_init(void)
{
printk("=[frocheng]=[%s]=[%s]=[Hello !]=\n", __FILE__, __func__);
s_netlink = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if(s_netlink == NULL)
{
printk("netlink_kernel_create error !\n");
return -1;
}
return 0;
}
static void __exit hello_exit(void)
{
netlink_kernel_release(s_netlink);
s_netlink = NULL;
printk("=[frocheng]=[%s]=[%s]=[Bye bye ...]=\n", __FILE__, __func__);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_DESCRIPTION("Frocheng: Netlink DEMO!");
MODULE_AUTHOR("Frodo Cheng");
MODULE_LICENSE("GPL");
MODULE_VERSION("V0.0.1");
用户空间代码(抄来的,版权不记得了,做个试验而已,请原作者勿怪)
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#define NETLINK_TEST 30
#define MSG_LEN 125
#define MAX_PLOAD 125
#define USER_PORT 100
typedef struct _user_msg_info
{
struct nlmsghdr hdr;
char msg[MSG_LEN];
} UserMsgInfo;
int main(int argc, char **argv)
{
int skfd;
int ret;
UserMsgInfo u_info;
socklen_t len;
struct nlmsghdr *nlh = NULL;
struct sockaddr_nl saddr, daddr;
char *umsg = "hello netlink!!";
/* 创建NETLINK socket */
skfd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_TEST);
if(skfd == -1)
{
perror("socket");
return 0;
}
memset(&saddr, 0, sizeof(saddr));
saddr.nl_family = AF_NETLINK; // AF_NETLINK
saddr.nl_pid = USER_PORT; // 端口号(port ID)
saddr.nl_groups = 0;
if(bind(skfd, (struct sockaddr *)&saddr, sizeof(saddr)) != 0)
{
perror("bind");
close(skfd);
return -1;
}
memset(&daddr, 0, sizeof(daddr));
daddr.nl_family = AF_NETLINK;
daddr.nl_pid = 0; // to kernel
daddr.nl_groups = 0;
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
memset(nlh, 0, sizeof(struct nlmsghdr));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
nlh->nlmsg_flags = 0;
nlh->nlmsg_type = 0;
nlh->nlmsg_seq = 0;
nlh->nlmsg_pid = saddr.nl_pid; // self port
memcpy(NLMSG_DATA(nlh), umsg, strlen(umsg));
ret = sendto(skfd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&daddr, sizeof(struct sockaddr_nl));
if(!ret)
{
perror("sendto");
close(skfd);
exit(-1);
}
printf("send kernel:%s\n", umsg);
memset(&u_info, 0, sizeof(u_info));
len = sizeof(struct sockaddr_nl);
ret = recvfrom(skfd, &u_info, sizeof(UserMsgInfo), 0, (struct sockaddr *)&daddr, &len);
if(!ret)
{
perror("recvfrom");
close(skfd);
return 0;
}
printf("from kernel:%s\n", u_info.msg);
close(skfd);
free((void *)nlh);
return 0;
}
说明一下
内核代码说明
init_net,是外部全局变量,我们引用过来,作为netlink_kernel_create 的第一个参数,这个Linux内核的网络命名空间,我们不是做网络虚拟化的,系统里面其他的netlink的地方基本也都用的这个,我们也用这个。
NETLINK_TEST,我们自定义了一个协议种类,不用系统已经用过的,表示,我们自己去解析我们自己的消息。
cfg:如下图
input域callback 很重要,就是内核模块接收到消息了,我们就调用这个callback进行处理。这个域少不得。
还有就是flags域:权限域,组播组相关的,我们加上,管他三七二十一,加上反正不会错,我们现在也没用到组播。
s_netlink 返回值。 是个对象,或者说句柄,其他的操作,要依赖这个句柄。
如下:我们在模块加载的入口函数中,完成这个netlink socket的注册操作。
释放
入口create,那么出口总要free 掉的。否则,内核handle泄露,关键是我们rmmod之后,我们重新insmod就会报错,影响我们重复试验,那就不好了。如下,release 函数,去release掉这个handle。
接收消息函数
为什么会写成这个样子呢?因为我从其他模块搜索,然后贴过来的代码,所以内部还有其他的调用。可以忽略。
hello_handle_msg 中,用一些函数,宏,去解析了skb的消息,获取到 netlink的报头,以及netlink消息的payload的。
netlink的报头定义如下
详细解释
对于我们的这个实验,这些全都用不着。。。。感兴趣的可以printk出来看一看。
剥离掉这个报头,其实对于netlink协议内部,还有一个属性头。
/* ========================================================================
* Netlink Messages and Attributes Interface (As Seen On TV)
* ------------------------------------------------------------------------
* Messages Interface
* ------------------------------------------------------------------------
*
* Message Format:
* <--- nlmsg_total_size(payload) --->
* <-- nlmsg_msg_size(payload) ->
* +----------+- - -+-------------+- - -+-------- - -
* | nlmsghdr | Pad | Payload | Pad | nlmsghdr
* +----------+- - -+-------------+- - -+-------- - -
* nlmsg_data(nlh)---^ ^
* nlmsg_next(nlh)-----------------------+
*
* Payload Format:
* <---------------------- nlmsg_len(nlh) --------------------->
* <------ hdrlen ------> <- nlmsg_attrlen(nlh, hdrlen) ->
* +----------------------+- - -+--------------------------------+
* | Family Header | Pad | Attributes |
* +----------------------+- - -+--------------------------------+
* nlmsg_attrdata(nlh, hdrlen)---^
*
* Data Structures:
* struct nlmsghdr netlink message header
*
* Message Construction:
* nlmsg_new() create a new netlink message
* nlmsg_put() add a netlink message to an skb
* nlmsg_put_answer() callback based nlmsg_put()
* nlmsg_end() finalize netlink message
* nlmsg_get_pos() return current position in message
* nlmsg_trim() trim part of message
* nlmsg_cancel() cancel message construction
* nlmsg_free() free a netlink message
*
* Message Sending:
* nlmsg_multicast() multicast message to several groups
* nlmsg_unicast() unicast a message to a single socket
* nlmsg_notify() send notification message
*
* Message Length Calculations:
* nlmsg_msg_size(payload) length of message w/o padding
* nlmsg_total_size(payload) length of message w/ padding
* nlmsg_padlen(payload) length of padding at tail
*
* Message Payload Access:
* nlmsg_data(nlh) head of message payload
* nlmsg_len(nlh) length of message payload
* nlmsg_attrdata(nlh, hdrlen) head of attributes data
* nlmsg_attrlen(nlh, hdrlen) length of attributes data
*
* Message Parsing:
* nlmsg_ok(nlh, remaining) does nlh fit into remaining bytes?
* nlmsg_next(nlh, remaining) get next netlink message
* nlmsg_parse() parse attributes of a message
* nlmsg_find_attr() find an attribute in a message
* nlmsg_for_each_msg() loop over all messages
* nlmsg_validate() validate netlink message incl. attrs
* nlmsg_for_each_attr() loop over all attributes
*
* Misc:
* nlmsg_report() report back to application?
*
* ------------------------------------------------------------------------
* Attributes Interface
* ------------------------------------------------------------------------
*
* Attribute Format:
* <------- nla_total_size(payload) ------->
* <---- nla_attr_size(payload) ----->
* +----------+- - -+- - - - - - - - - +- - -+-------- - -
* | Header | Pad | Payload | Pad | Header
* +----------+- - -+- - - - - - - - - +- - -+-------- - -
* <- nla_len(nla) -> ^
* nla_data(nla)----^ |
* nla_next(nla)-----------------------------'
*
* Data Structures:
* struct nlattr netlink attribute header
*
* Attribute Construction:
* nla_reserve(skb, type, len) reserve room for an attribute
* nla_reserve_nohdr(skb, len) reserve room for an attribute w/o hdr
* nla_put(skb, type, len, data) add attribute to skb
* nla_put_nohdr(skb, len, data) add attribute w/o hdr
* nla_append(skb, len, data) append data to skb
*
* Attribute Construction for Basic Types:
* nla_put_u8(skb, type, value) add u8 attribute to skb
* nla_put_u16(skb, type, value) add u16 attribute to skb
* nla_put_u32(skb, type, value) add u32 attribute to skb
* nla_put_u64_64bit(skb, type,
* value, padattr) add u64 attribute to skb
* nla_put_s8(skb, type, value) add s8 attribute to skb
* nla_put_s16(skb, type, value) add s16 attribute to skb
* nla_put_s32(skb, type, value) add s32 attribute to skb
* nla_put_s64(skb, type, value,
* padattr) add s64 attribute to skb
* nla_put_string(skb, type, str) add string attribute to skb
* nla_put_flag(skb, type) add flag attribute to skb
* nla_put_msecs(skb, type, jiffies,
* padattr) add msecs attribute to skb
* nla_put_in_addr(skb, type, addr) add IPv4 address attribute to skb
* nla_put_in6_addr(skb, type, addr) add IPv6 address attribute to skb
*
* Nested Attributes Construction:
* nla_nest_start(skb, type) start a nested attribute
* nla_nest_end(skb, nla) finalize a nested attribute
* nla_nest_cancel(skb, nla) cancel nested attribute construction
*
* Attribute Length Calculations:
* nla_attr_size(payload) length of attribute w/o padding
* nla_total_size(payload) length of attribute w/ padding
* nla_padlen(payload) length of padding
*
* Attribute Payload Access:
* nla_data(nla) head of attribute payload
* nla_len(nla) length of attribute payload
*
* Attribute Payload Access for Basic Types:
* nla_get_u8(nla) get payload for a u8 attribute
* nla_get_u16(nla) get payload for a u16 attribute
* nla_get_u32(nla) get payload for a u32 attribute
* nla_get_u64(nla) get payload for a u64 attribute
* nla_get_s8(nla) get payload for a s8 attribute
* nla_get_s16(nla) get payload for a s16 attribute
* nla_get_s32(nla) get payload for a s32 attribute
* nla_get_s64(nla) get payload for a s64 attribute
* nla_get_flag(nla) return 1 if flag is true
* nla_get_msecs(nla) get payload for a msecs attribute
*
* Attribute Misc:
* nla_memcpy(dest, nla, count) copy attribute into memory
* nla_memcmp(nla, data, size) compare attribute with memory area
* nla_strlcpy(dst, nla, size) copy attribute to a sized string
* nla_strcmp(nla, str) compare attribute with string
*
* Attribute Parsing:
* nla_ok(nla, remaining) does nla fit into remaining bytes?
* nla_next(nla, remaining) get next netlink attribute
* nla_validate() validate a stream of attributes
* nla_validate_nested() validate a stream of nested attributes
* nla_find() find attribute in stream of attributes
* nla_find_nested() find attribute in nested attributes
* nla_parse() parse and validate stream of attrs
* nla_parse_nested() parse nested attribuets
* nla_for_each_attr() loop over all attributes
* nla_for_each_nested() loop over the nested attributes
*=========================================================================
*/
不过,我们的这个示例中,我们没用,我们直接用的 c-style的字符串。
发送,回射回复
用户空间进程代码,client 端代码
首先回顾一下 UDP 的编程过程
1. socket
2. 准备地址
3. 绑定(UDP,server端需要绑定,以固定自己的端口,方便客户端去连接,而客户端是不需要绑定的,底层会随机分配一个port 给这个socket使用),但是此处,我们一定要绑定。因为内核回复消息的时候,可以直接定位到指定的port上,也就是指定的回复到,准确的说是发送消息到这个进程上。
4. 绑定完了之后,就sentto 或者 recvfrom即可。注意,要用 操作数据报 fd 的接口去操作。
这个示例过于简单
比如内核监听,没有指定端口,只是指定port去回复了,那么,任何我们创建的时候指定的那个类型(NETLINK_TEST 宏)的消息,都会到这个内核模块中来。我们是可以根据报头,去获取到一些信息,针对不同的进程,我们去做不同的回复,这样可以实现内核转发的进程间通信。