Linux Kernel 网络 之 Netlink

参考书籍

《精通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 宏)的消息,都会到这个内核模块中来。我们是可以根据报头,去获取到一些信息,针对不同的进程,我们去做不同的回复,这样可以实现内核转发的进程间通信。

内核的Makefile

结果

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值