Linux内核netlink机制 - 用户空间和内核空间数据传输

目录

简介:

1、netlink 优点

2、netlink常用场景

一、netlink常用数据结构及函数

1、netlink应用层数据结构及函数

1.1 消息结构

消息头 nlmsghdr 结构体:

1.2 netlink通信地址 struct sockaddr_nl

2、netlink内核数据结构及函数

2.1 常用宏

2.2 常用函数

二、代码实例

1、用户态netlink程序

1.1 netlink应用层基本步骤

(1) 使用 socket 声明套接字

(2) 使用 bind 绑定 本地地址 到套接字

(3) 构造消息

(4) 接收/发送消息

1.2 netlink用户空间代码

2、内核空间代码

2.1 netlink内核态编程步骤

(1) 创建socket并注册回调

(2) 构造消息 

(3) 接收/发送消息

2.2 netlink内核态模块代码

3、测试结果


简介:


        Netlink socket 是一种Linux特有的socket,用于实现用户空间内核空间通信的一种特殊的进程间通信方式(IPC) ,也是网络应用程序与内核通信的最常用的接口。
        Netlink 是一种在内核和用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就能使用 Netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 Netlink。Netlink 的接口分为应用层接口和内核接口,我们需要在应用程序实现策略,然后在内核实现机制

用户空间内核空间的常用通信方式有三种:proc、ioctl、Netlink

(1) /proc-单向:虚拟文件系统,用户获取内核信息,输出数据

(2) ioctl-单向:不能发送异步信息,用户向内核传送命令

(3)netlink-双向:kernel可以启动传输,而不是仅限于响应用户空间请求而返回信息

1、netlink 优点


  •  支持全双工、异步通信
  • 用户空间使用标准的socket接口即可进行通信
  • 支持多播
  • 在内核端可用于进程上下文与中断上下文

2、netlink常用场景


  • 获取或修改路由信息
  • 监听TCP协议数据报文
  • 防火墙
  • netfilter子系统
  • 内核事件向用户态通知

一、netlink常用数据结构及函数


1、netlink应用层数据结构及函数


网络编程对比netlink:网络编程中我们是使用 IP地址 + 端口号 进行寻址的,而 netlink 则是通过 协议类型+进程ID 进行寻址的,其中 协议类型 会在调用 socket接口 的时候指定。

1.1 消息结构


netlink中的message包含消息头和消息体,消息头占16字节,后面跟的是消息体(数据)。

  • Message Length:总长度,包括netlink消息头在内的总字节数,占4字节;
  • type:消息类型,往往用于协议流程。内核定义了多个标准的消息类型。内核已经使用 netlink实现了多种协议,每个协议都有特定的类型和功能,占2字节;
  • Flags:消息标志,占2字节;
  • Sequence number:序列号,该项是可选项,类似TCP协议中的报文号。占4字节;
  • PID(port ID):端口号,发送端的端口ID号,占4字节;
消息头 nlmsghdr 结构体:
struct nlmsghdr {
	__u32		nlmsg_len;	    /* 包括netlink消息头在内,整个消息长度 */
	__u16		nlmsg_type;	    /* 消息类型 */
	__u16		nlmsg_flags;	/* 消息标志 */
	__u32		nlmsg_seq;	    /* 消息报文的序列号 */
	__u32		nlmsg_pid;	    /* 发送端口的ID号,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号 */
};

1.2 netlink通信地址 struct sockaddr_nl


struct sockaddr_nl 是netlink通信地址跟普通socket struct sockaddr_in类似

struct sockaddr_nl {
    __kernel_sa_family_t    nl_family;  /* AF_NETLINK */
    unsigned short          nl_pad;     /* 目前未用到,填充为0 */
    __u32                   nl_pid;     /* port ID  (通信端口号),0表示发送给kernel*/
    __u32                   nl_groups;  /* 多播组掩码 */
};

nl_groups是多播组的地址掩码,注意是掩码不是多播组的组号。可以看下 net/netlink/af_netlink.c 中函数:

static u32 netlink_group_mask(u32 group)
{
    return group ? 1 << (group - 1) : 0;
}

也就是说,在用户空间的代码里,如果我们要加入到多播组1,需要设置nl_groups设置为1;多播组2的掩码为2;多播组3的掩码为4,依次类推。为0表示我们不希望加入任何多播组。

nl_groups多播组掩码是32位,每个bit位表示1个多播组,因此每种Netlink协议最多支持32个多播组。用户空间的进程如果对某个多播组感兴趣,那么它就加入到该组中,当内核空间的进程往该组发送多播消息时,所有已经加入到该多播组的用户进程都会收到该消息。

2、netlink内核数据结构及函数


2.1 常用宏


netlink消息类型及常用宏

/******************** netlink消息类型 ********************/
#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   /* Unused number, formerly ip_queue     */
#define NETLINK_SOCK_DIAG   4   /* 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 NETLINK_CRYPTO      21  /* Crypto layer */

#define NETLINK_INET_DIAG   NETLINK_SOCK_DIAG

#define MAX_LINKS 32


/******************** netlink常用宏 ********************/
#define NLMSG_ALIGNTO   4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

/* Netlink 头部长度 */
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))

/* 计算消息数据len的真实消息长度(消息体 + 消息头)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)

/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度 */
#define NLMSG_NEXT(nlh,len)  ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
                  (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

/* 判断消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
               (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
               (nlh)->nlmsg_len <= (len))

/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

2.2 常用函数


static inline struct sock *

netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)

功能:创建内核 socket,以提供和用户态通信

参数:

  • net: net指向所在的网络命名空间namespace, 默认情况传入&init_net(不需要定义)这个全局变量;
  • unit:netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX等;
  • cfg: cfg存放的是netlink内核配置参数(如下)
struct netlink_kernel_cfg {
	unsigned int	groups;    //该协议类型支持的最大多播组数量,如果该值小于32,则默认按32处理。一般置为0即可
	unsigned int	flags;
	void		(*input)(struct sk_buff *skb);    //input 回调函数
	struct mutex	*cb_mutex;
	int		(*bind)(struct net *net, int group);
	void		(*unbind)(struct net *net, int group);
	bool		(*compare)(struct net *net, struct sock *sk);
};

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock)

功能:发送单播消息

参数:

  • ssk: netlink socket(netlink_kernel_create 的返回值);
  • skb: skb buff 指针;
  • portid: 通信的端口号,接收消息的进程pid;
  • nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用 定时睡眠;

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
                 __u32 group, gfp_t allocation)

功能:发送多播消息

参数:

  • ssk: netlink socket(netlink_kernel_create 的返回值);
  • skb: 内核skb buff;
  • portid: 端口id;
  • group: 所有目标多播组掩码的"位或"操作的值;
  • allocation: 指定内核内存分配方式,通常GFP_ATOMIC用于中断上下文,而GFP_KERNEL用于其他场合。这个参数的存在是因为该API可能需要分配一个或多个缓冲区来对多播消息进行clone;

二、代码实例


代码示例分为用户态内核态netlink实现。

1、用户态netlink程序


1.1 netlink应用层基本步骤


(1) 使用 socket 声明套接字

int socket(int domain, int type, int protocol)

  • domain:表示协议簇,在 netlink 中一般为 AF_NETLINK。
  • type:表示套接字类型,在 netlink 中一般为 SOCK_RAW。
  • protocol:表示协议类型,在 netlink 中可以是内核支持的协议,也可以是自定义协议

自定义协议例如:

    #define NETLINK_TEST 23    //自定义协议
    ......
    int skfd = 0;
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
(2) 使用 bind 绑定 本地地址 到套接字

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)        //绑定
 
/* sockaddr_nl 是 netlink 使用的地址数据结构,与网络编程不同 */
struct sockaddr_nl {
     __kernel_sa_family_t      nl_family;  //一般为AF_NETLINK
     unsigned short                nl_pad;     //无需填充
     __u32                              nl_pid;     //与内核通信的进程的PID,0 则代表地址为内核
     __u32                              nl_groups;  //多播组的地址掩码,netlink支持多播
};

例如:

    struct sockaddr_nl nlsrc_addr = {0};
    /* 设置本地socket地址 */
    nlsrc_addr.nl_family = AF_NETLINK;
    nlsrc_addr.nl_pid = getpid();
    nlsrc_addr.nl_groups = 0;

    /* 绑定套接字 */
    if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
    {
        printf("bind addr error\n");   
        return -1;
    }
(3) 构造消息
(4) 接收/发送消息

netlink 有 2 套收发消息的接口,分别是 sendto/recvfromsendmsg/recvmsg。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

1.2 netlink用户空间代码


#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>

#define NETLINK_TEST	17
#define RX_BUF_SIZE		100		//接收buf size
#define MAX_PLOAD		100		//发送message内存size

typedef struct
{
    struct nlmsghdr hdr;
    char msg[RX_BUF_SIZE];
}RX_KERNEL_MSG;

int main(int argc, char* argv[])
{
    char *data = "This message is from user's space";
    //初始化
    struct sockaddr_nl src_addr, dest_addr;    //sockaddr_nl 是 netlink 使用的地址数据结构
    int skfd, ret, rxlen = sizeof(struct sockaddr_nl);
    struct nlmsghdr *message;
    RX_KERNEL_MSG info;
    char *retval;

/* 1.创建NETLINK socket */
    skfd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(skfd < 0){
        printf("can not create a netlink socket\n");
        return -1;
    }

    //初始化 netlink 源地址
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid();
    src_addr.nl_groups = 0;
    if(bind(skfd, (struct sockaddr *)&src_addr, sizeof(src_addr)) != 0){
        printf("bind() error\n");
        return -1;
    }
    
    //初始化 netlink 目标地址
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;            //0表示发送给kernel
    dest_addr.nl_groups = 0;         //多播组的地址掩码

/* 2.设置消息 */
    message = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
    memset(message, '\0', sizeof(struct nlmsghdr));
    message->nlmsg_len = NLMSG_SPACE(strlen(data)); //包括netlink消息头在内,整个消息长度 
    message->nlmsg_flags = 0;            //消息标志
    message->nlmsg_type = 0;             //消息类型
    message->nlmsg_seq = 0;              //消息报文的序列号
    message->nlmsg_pid = src_addr.nl_pid; //消息中加入本应用端口pid,内核应答时往此pid写入
    retval = memcpy(NLMSG_DATA(message), data, strlen(data));    //数据

//    printf("User Send: %s, len:%d\r\n", (char *)NLMSG_DATA(message), message->nlmsg_len);

/* 3.发送消息 */
    ret = sendto(skfd, message, message->nlmsg_len, 0,(struct sockaddr *)&dest_addr, sizeof(dest_addr));	//将message消息发送给内核
    if(!ret){
        perror("send pid:");
        exit(-1);
    }

/* 4.接收内核发来的信息 */
    ret = recvfrom(skfd, &info, sizeof(RX_KERNEL_MSG), 0, (struct sockaddr*)&dest_addr, &rxlen);
    if(!ret){
        perror("recv form kerner:");
        exit(-1);
    }

    printf("User Receive ACK from kernel:%s\r\n",(char *)info.msg);
    //内核和用户进行通信

    close(skfd);
    free((void *)message);
    return 0;
}

2、内核空间代码


2.1 netlink内核态编程步骤


内核接口与应用层接口相似,其过程和使用的接口如下:

(1) 创建socket并注册回调
struct netlink_kernel_cfg {
    unsigned int    groups;
    unsigned int    flags;
    void            (*input)(struct sk_buff *skb);    /* 接收回调函数 */
    struct mutex    *cb_mutex;
    int             (*bind)(struct net *net, int group);
    void            (*unbind)(struct net *net, int group);
    bool            (*compare)(struct net *net, struct sock *sk);
};
 
/* 创建接口,其中,参数net 一般为 &init_net */
static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
 
/* 释放接口 */
void netlink_kernel_release(struct sock *sk);
(2) 构造消息 
//sk_buff 为内核中 Netlink 使用的数据结构体,下面分别是分配、使用、释放的接口
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
static inline struct sk_buff *skb_get(struct sk_buff *skb)
void kfree_skb(struct sk_buff *skb);
 
//下面为构建消息内容的接口
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int payload, int flags)

大概的调用方法为: 

//NLMSG_SPACE计算包括消息头在内的消息长度
size_t size = max(NLMSG_SPACE(message_size), (size_t)NLMSG_GOODSIZE);
 
//分配sk_buff内存
sk_buff * log_skb = alloc_skb(size, GFP_ATOMIC);
 
//构造消息结构体
nlmsghdr *nlh = nlmsg_put(log_skb, /*pid*/0, /*seq*/0, type,
		message_size, 0);
 
//将要发送的数据复制到消息结构体上
if(payload != NULL) {
	memcpy(nlmsg_data(nlh),  payload, size);
}
(3) 接收/发送消息

发送消息有单播、广播两个接口,可以根据实际业务选择使用。

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid, __u32 group, gfp_t allocation);

2.2 netlink内核态模块代码


#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <linux/netlink.h>

#define NETLINK_TEST    17

struct {
    __u32 pid;
}user_process;

static struct sock *netlinkfd = NULL;

/* 发送到用户空间 */
int send_to_user(int _pid, char *pbuf, uint16_t len)
{
    struct sk_buff *nl_skb;
    struct nlmsghdr *nlh;

    int ret;

    /* 创建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(netlinkfd, nl_skb, _pid, MSG_DONTWAIT);	//发送数据

    return ret;
}

/* 接收回调函数 */
static void netlink_rcv_msg(struct sk_buff *skb)
{
    struct nlmsghdr *nlh = NULL;
    char *data = NULL;
    char *kmsg = "hello users!!!";

    nlh = nlmsg_hdr(skb);

    if(skb->len >= NLMSG_SPACE(0))
    {
        data = NLMSG_DATA(nlh);    //数据
        if (data)
        {
            user_process.pid = nlh->nlmsg_pid;    //数据从用户端口ID获取
            printk("kernel recv from user pid %d: %s\n", user_process.pid, data);
            send_to_user(user_process.pid, kmsg, strlen(kmsg));    //发送数据给用户pid
        }
    } else {
        printk("%s: error skb, length:%d\n", __func__, skb->len);
    }
}

struct netlink_kernel_cfg cfg = { 
    .input  = netlink_rcv_msg, /* 设置接收回调函数 */
    .groups = 0,    //内核进程最多能处理多播组的个数,如果该值小于32,则默认按32处理。一般置为0即可
    .flags = 0,
    .cb_mutex = NULL,
    .bind = NULL,
    .compare = NULL,
};

int __init test_netlink_init(void)
{
    /* 创建netlink socket */
    netlinkfd = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
    if(netlinkfd == NULL){
        printk(KERN_ERR "can not create a netlink socket\n");
        return -1;
    }
    return 0;
}

void __exit test_netlink_exit(void)
{
    if (netlinkfd){
        netlink_kernel_release(netlinkfd); /* release ..*/
        netlinkfd = NULL;
    }
    printk(KERN_DEBUG "test_netlink_exit!!\n");
}

module_init(test_netlink_init);
module_exit(test_netlink_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("donga");

3、测试结果


测试代码下载链接:https://download.csdn.net/download/hinewcc/89590914

将内核态代码编译成ko模块,用户态代码编译成app。insmod加载ko模块,并运行app代码:

$ insmod netlink_test.ko        #加载驱动

$ ./netlink_app                       #运行APP

内核态收到用户态发送的数据,并发送"hello users!!!"给用户空间。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值