内核Generic Netlink通信

内核Generic Netlink通信编程

因为之前对内核的代码接触的不是很多,这篇只是记录自己学习内核通信的内容,肯定存在诸多缺陷,请大佬们批评指正。
github地址:https://github.com/yisenFangW/Netlink

netlink通信介绍

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,
内核态需要使用专门的内核 API 来使用 netlink。
Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言具有以下优点:
1,netlink使用简单,只需要在include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST 20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换);
2. netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息;
3.使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖;
4.netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性;
5.内核可以使用 netlink 首先发起会话;
这一段废话抄自:https://www.cnblogs.com/wenqiang/p/6306727.html

netlink通信demo

demo功能

实现功能的demo已上传到github:https://github.com/yisenFangW/Netlink
demo实现的功能即从用户态向内核态发送消息,然后内核获取消息之后直接打印(可以实现其他功能),用法为在用户态传入参数:

usage:./user  add|del name age weight

通过dmesg看内核打印到的接受内容;
具体实现的demo很简单,内核态未做任何处理,直接打印,结果如图所示:
用户态演示:
在这里插入图片描述
内核态演示:
内核Generic Netlink通信_第1张图片

用户态实现

用户端实现是通过libnl3的库实现的,我理解的libnl库是对netlink进行上一层封装,让接口调用更加简洁。libnl3库查看链接.
在用户态传入的参数中,首先第一个参数作为命令参数,定义一个命令参数的枚举类型,后面传入的name,age,weight作为一个命令参数的枚举类型;

enum {
     CTRL_ATTR_UNDO,
     CTRL_ATTR_NAME,
     CTRL_ATTR_AGE,
     CTRL_ATTR_WEIGHT,
    __CTRL_ATTR_MAX_TEST,
};
#define CTRL_ATTR_MAX_TEST (__CTRL_ATTR_MAX_TEST - 1)

enum {
     CTRL_CMD_UNDO,
     CTRL_CMD_ADD,
     CTRL_CMD_DEL,
     __CTRL_CMD_MAX_TEST,
};
#define CTRL_CMD_MAX_TEST (__CTRL_CMD_MAX_TEST -1)

进入到main函数中,因为设计到通信,首先看是如何建立连接的,nl_sock应该是libnl在socket编程的基础上封装的套接字结构体。
对于nl_sock的操作,首先是初始化nl_sock,用到的函数是struct nl_sock* nl_socket_alloc ( void )

// nl_socket_alloc内部主要是调用__alloc_socket实现的,cb应该是回调函数,先不管
 struct nl_sock *nl_socket_alloc(void)
{
	struct nl_cb *cb;
	struct nl_sock *sk;
	cb = nl_cb_alloc(default_cb);
	if (!cb)
		return NULL;
  	/* will increment cb reference count on success */
	sk = __alloc_socket(cb);
	nl_cb_put(cb);

	return sk;
}

// 再看下__alloc_socket的实现,可以看到初始化nl_sock的哪些内容:
 static struct nl_sock *__alloc_socket(struct nl_cb *cb)
 {
	struct nl_sock *sk;
	// 申请的内存空间,要记得释放
	sk = calloc(1, sizeof(*sk));
 	if (!sk)
		return NULL;
	sk->s_fd = -1;
	sk->s_cb = nl_cb_get(cb);
	// 就是对netlink通信的一个封装
	sk->s_local.nl_family = AF_NETLINK;
	sk->s_peer.nl_family = AF_NETLINK;
	sk->s_seq_expect = sk->s_seq_next = time(0);
	sk->s_local.nl_pid = generate_local_port();
	if (sk->s_local.nl_pid == UINT_MAX) {
		nl_socket_free(sk);
	return NULL;
	}
	return sk;
}

调用完nl_sock_alloc()执行完成之后,相应的应该调用void nl_socket_free(struct nl_sock * sk),释放掉nl_sock申请的内存空间;
通过genl_connect()连接的内核,genl netlink与libnl混用了…

// 通过genl_connect连接一个netlink的socket
/* func:connect a generic netlink socket
 *returns : 0 on success or a negative error code.
 */
int genl_connect(struct nl_sock* sk);

内核中有那么多socket,如何准确的与内核中想要连接的socket建立连接呢,通过自定义一个与内核中同名的TEST_GENL_NAME作为标识(名字阔以自己起)

#define TEST_GENL_NAME "TEST"
// func:resolve generic netlink family name to numeric identifier
// name参数即传入之前定义的参数名
int genl_ctrl_resolve(struct nl_sock* sk, const char* name);

这里还要说明一个结构体nl_msg用于承载消息内容,中间会涉及到nl_msg的初始化与nl_msg的数据填充,nl_msg的结构很复杂,在网上找的一张结构图,demo中传输全部都放到attr中;
内核Generic Netlink通信_第2张图片
user实现的具体代码:

//
// Created by 方伟 on 2019-10-11.
//
// user端编译方式 gcc test4_user.c -o user $(pkg-config --cflags --libs libnl-genl-3.0)

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define HELP_STRING "usage:%s\n \
		add|del name age weight \n"

#define TEST_GENL_NAME "TEST"
#define TEST_GENL_VERSION    0x1
#define NAMELEN  20

enum {
    CTRL_ATTR_UNDO = 0,
    CTRL_ATTR_NAME,
    CTRL_ATTR_AGE,
    CTRL_ATTR_WEIGHT,
    __CTRL_ATTR_MAX_TEST,
};
#define CTRL_ATTR_MAX_TEST (__CTRL_ATTR_MAX_TEST - 1)

enum {
    CTRL_CMD_UNDO = 0,
    CTRL_CMD_ADD,
    CTRL_CMD_DEL,
    __CTRL_CMD_MAX_TEST,
};
#define CTRL_CMD_MAX_TEST (__CTRL_CMD_MAX_TEST -1)

struct user_key{
    char name[NAMELEN];
    unsigned int age;
    unsigned int weight;
};

void print_help(int argc, char **argv) {
    fprintf(stderr, HELP_STRING, argv[0]);
    exit(1);
}

static int noop_parse_cb(struct nl_msg *msg, void *args) {
    return NL_OK;
}

static inline int parse_options(struct nl_msg *msg, struct user_key* user) {
    struct nlattr* attr;
    attr = nla_nest_start(msg, 1);
    NLA_PUT_STRING(msg, CTRL_ATTR_NAME, user->name);
    NLA_PUT_U32(msg, CTRL_ATTR_AGE, user->age);
    NLA_PUT_U32(msg, CTRL_ATTR_WEIGHT, user->weight);
    nla_nest_end(msg, attr);
    return 0;
nla_put_failure:
    nlmsg_free(msg);
    return 1;
}

int main(int argc, char **argv) {
    int err = EINVAL;
    int cmd;
    struct nl_sock *sock = NULL;
    int family = -1;
    struct nl_msg *msg = NULL;

    if (argc != 5)
        print_help(argc, argv);

    if (!strncmp(argv[1], "add", 3)) {
        cmd = CTRL_CMD_ADD;
    } else if (!strncmp(argv[1], "del", 3)) {
        cmd = CTRL_CMD_DEL;
    } else
        print_help(argc, argv);

    struct user_key *user;
    user = malloc(sizeof(struct user_key));
    if(!user){
        fprintf(stderr, "create memory failed.\n");
        return -1;
    }
    strncpy(user->name, argv[2], NAMELEN);
    user->age = atoi(argv[3]);
    user->weight = atoi(argv[4]);

    if (NULL == sock) {
        if (NULL == (sock = nl_socket_alloc())) {
            fprintf(stderr, "create handler error");
            return -1;
        }
        if (genl_connect(sock) < 0) {
            fprintf(stderr, "genl_connect failed.\n");
            goto fail_genl;
        }
    }

    if (family == -1 && (family = genl_ctrl_resolve(sock, TEST_GENL_NAME)) < 0) {
        fprintf(stderr, "resolve NL_NAME failed.\n");
        goto fail_genl;
    }

    if (NULL == (msg = nlmsg_alloc())) {
        fprintf(stderr, "Alloc nl_msg error.\n");
        goto fail_genl;
    }

    if (parse_options(msg, user)) {
        return -1;
    }

    genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, family, 0, 0, cmd, TEST_GENL_VERSION);


    if (nl_socket_modify_cb(sock, NL_CB_VALID, NL_CB_CUSTOM, noop_parse_cb, NULL) != 0)
        goto fail_genl;

    if (nl_send_auto_complete(sock, msg) < 0) {
        fprintf(stderr, "send msg error .\n");
        goto fail_genl;
    } else {
        prink("print success!\n");
    }

    nlmsg_free(msg);
    nl_socket_free(sock);
    return 0;

    fail_genl:
    nl_socket_free(sock);
    sock = NULL;
    return -1;
}

其中有个之前编译困扰了我的一个点,就是NLA_PUT_STRING宏定义之后,一定要做一个nla_put_failure的判断,开始没写一直编译出问题,然后看了一下NLA_PUT_STRING的实现:

// NLA_PUT_STRING是对NLA_PUT的调用
#define NLA_PUT_STRING(msg, attrtype,value )
	NLA_PUT(msg, attrtype, strlen(value) + 1, value)

// 这里调用了一个goto nla_put_failure的设计,若底下不实现,就会一直编译出问题;
#define NLA_PUT	(msg, attrtype, attrlen, data)		
do {
	if (nla_put(msg, attrtype, attrlen, data) < 0)
		goto nla_put_failure;
} while(0)

内核态的实现

内核态首先声明与用户态相同的数据结构,用于接受参数并打印。CTRL_CMD_ADD是添加命令,CTRL_CMD_DEL是删除命令,其中attr参数包括name,age,weight。

enum {
	CTRL_ATTR_UNDO = 0,
	CTRL_ATTR_NAME,
	CTRL_ATTR_AGE,
	CTRL_ATTR_WEIGHT,
	 __CTRL_ATTR_MAX_TEST,
};
#define CTRL_ATTR_MAX_TEST (__CTRL_ATTR_MAX_TEST - 1)

enum {
	CTRL_CMD_UNDOi = 0,
	CTRL_CMD_ADD,
	CTRL_CMD_DEL,
	__CTRL_CMD_MAX_TEST,
};
#define CTRL_CMD_MAX_TEST (__CTRL_CMD_MAX_TEST -1)

然后很关键的步骤,是定义一个genl_family。内核态注册genl_family,用户态通过已注册的信息与内核态通信。通用的genl_family的结构及各参数含义如下:

//genl family的结构体如下:
struct genl_family
{
      unsigned int            id;
      unsigned int            hdrsize;
      char                    name[GENL_NAMSIZ];
      unsigned int            version;
      unsigned int            maxattr;
      struct nlattr **        attrbuf;
      struct list_head        ops_list;
      struct list_head        family_list;
};
  • id: family id。当新注册一个family的时候,应该用GENL_ID_GENERATE宏(0x0),表示请控制器自动为family分配的一个id。0x10保留供genl控制器使用。
  • hdrsize: 用户自定议头部长度,如果没有用户自定义头部,这个值被赋为0。
  • version: 版本号,可自由定义。
  • name: family名,要求不同的family使用不同的名字。以便控制器进行正确的查找。
  • maxattr:genl使用netlink标准的attr来传输数据。此字段定义了最大attr类型数。(注意:不是一次传输多少个attr,而是一共有多少种attr,因此,这个值可以被设为0,为0代表不区分所收到的数据的attr type)。在接收数据时,可以根据attr type,获得指定的attr type的数据在整体数据中的位置。
  • struct nlattr **attrbuf
  • struct list_head ops_list
  • struct list_head family_list
    以上的三个字段为私有字段,由系统自动配置,开发者不需要做配置。

本次测试demo的genl_family的定义

struct genl_family test_ctl = {
	.id = GENL_ID_GENERATE,    //GENL_ID_GENERATE为由内核随机产生,也可自定义,但是会有与线有的内核id冲突的可能性;
	.name = TEST_GENL_NAME,
	.version = TEST_GENL_VERSION,
	.maxattr = CTRL_ATTR_MAX_TEST,
};

定义genl_family后,需要为family定义ops用于操作上述add,del的操作,genl_ops的数据结构及定义如下:

struct genl_ops
{
      u8                      cmd;
      unsigned int            flags;
      struct nla_policy       *policy;
      int                     (*doit)(struct sk_buff *skb,
                                      struct genl_info *info);
      int                     (*dumpit)(struct sk_buff *skb,
                                          struct netlink_callback *cb);
      struct list_head        ops_list;
};
  • cmd: 命令名。用于识别各genl_ops
  • flag: 各种设置属性,以“或”连接。在需要admin特权级别时,使用GENL_ADMIN_PERM
  • policy:定义了attr规则。如果此指针非空,genl在触发事件处理程序之前,会使用这个字段来对帧中的attr做校验(见nlmsg_parse函数)。该字段可以为空,表示在触发事件处理程序之前,不做校验。
    policy是一个struct nla_policy的数组。struct nla_policy结构体表示如下:
struct nla_policy
{
    u16	    type;
   u16      len;
};

其中,type字段表示attr中的数据类型,可被配置为:
NLA_UNSPEC–未定义
NLA_U8, NLA_U16, NLA_U32, NLA_U64为8bits, 16bits, 32bits, 64bits的无符号整型
NLA_STRING–字符串
NLA_NUL_STRING–空终止符字符串
NLA_NESTED–attr流
len字段的意思是:如果在type字段配置的是字符串有关的值,要把len设置为字符串的最大长度(不包含结尾的’\0’)。如果type字段未设置或被设置为NLA_UNSPEC,那么这里要设置为attr的payload部分的长度。

  • doit:这是一个回调函数。在generic netlink收到数据时触发,运行在进程上下文。
    doit传入两个参数,skb为触发此回调函数的socket buffer。第二个参数是一个genl_info结构体。
  • dumpit:这是一个回调函数,当genl_ops的flag标志被添加了NLM_F_DUMP以后,每次收到genl消息即会回触发这个函数。 dumpit与doit的区别是:dumpit的第一个参数skb不会携带从客户端发来的数据。相反地,开发者应该在skb中填入需要传给客户端的数据, 然后,并skb的数据长度(可以用skb->len)return。skb中携带的数据会被自动送到客户端。只要dumpit的返回值大于 0,dumpit函数就会再次被调用,并被要求在skb中填入数据。当服务端没有数据要传给客户端时,dumpit要返回0。如果函数中出错,要求返回一 个负值。关于doit和dumpit的触发过程,可以查看源码中的genl_rcv_msg函数。

*ops_list为私有字段,由系统自动配置,开发者不需要做配置。

本demo定义的genl_ops的结构如下,只定义了add的操作,del可仿照add的代码实现:

static struct genl_ops test_ctl_ops = {
	.cmd = CTRL_CMD_ADD,
	.doit = test_ctl_func,
	.policy = test_ctl_policy,
};

本demo的参数有name,age,weight,定义的参数类型如下:

static const struct nla_policy test_ctl_policy[CTRL_ATTR_MAX_TEST + 1] = {
	[CTRL_ATTR_NAME] = {.type = NLA_STRING},
	[CTRL_ATTR_AGE] = {.type = NLA_U16},
	[CTRL_ATTR_WEIGHT] = {.type = NLA_U16},
};

内核态的代码以驱动的形式的运行,最终实现的全部代码如下:

#include 
#include 
#include 
#include 

#define TEST_GENL_NAME "TEST"
#define TEST_GENL_VERSION    0x1
#define NAMELEN 20

enum {
    CTRL_ATTR_UNDO,
    CTRL_ATTR_NAME,
    CTRL_ATTR_AGE,
    CTRL_ATTR_WEIGHT,
    __CTRL_ATTR_MAX_TEST,
};
#define CTRL_ATTR_MAX_TEST (__CTRL_ATTR_MAX_TEST - 1)

enum {
    CTRL_CMD_UNDO,
    CTRL_CMD_ADD,
    CTRL_CMD_DEL,
    __CTRL_CMD_MAX_TEST,
};
#define CTRL_CMD_MAX_TEST (__CTRL_CMD_MAX_TEST -1)

struct genl_family test_ctl = {
        .id = GENL_ID_GENERATE,
        .name = TEST_GENL_NAME,
        .version = TEST_GENL_VERSION,
        .maxattr = CTRL_ATTR_MAX_TEST,
        .netnsok = true,
};

static const struct nla_policy test_ctl_policy[CTRL_ATTR_MAX_TEST + 1] = {
        [CTRL_ATTR_NAME] = {.type = NLA_STRING},
        [CTRL_ATTR_AGE] = {.type = NLA_U16},
        [CTRL_ATTR_WEIGHT] = {.type = NLA_U16},
};

int test_ctl_func(struct sk_buff *skb, struct genl_info *info) {
    struct sk_buff *skbuff = NULL;
    char *name;
    int age, weight;
    void *hdr;
    int msg_size;

    printk("get process pid=%d  cmd=%d pid=%d type=%d len=%d\n",info->snd_pid,info->genlhdr->cmd,
            info->nlhdr->nlmsg_pid,info->nlhdr->nlmsg_type,info->nlhdr->nlmsg_len);

    int cmd = info->genlhdr->cmd;
    if(cmd == CTRL_CMD_ADD)
        printk("cmd is add!\n");
    else
        printk("cmd is del!\n");

    if(info->attrs[CTRL_ATTR_NAME]){
        name = nla_data(info->attrs[CTRL_ATTR_NAME]);
        printk("recv message from usr name is %s\n", name);
    }
    if(info->attrs[CTRL_ATTR_AGE]){
        age = nla_get_u16(info->attrs[CTRL_ATTR_AGE]);
        printk("recv message from usr age is %d\n", age);
    }
    if(info->attrs[CTRL_ATTR_WEIGHT]){
        weight = nla_get_u16(info->attrs[CTRL_ATTR_WEIGHT]);
        printk("recv message from usr weight is %d\n", weight);
    }
    return 0;
}

static struct genl_ops test_ctl_ops = {
        .cmd = CTRL_CMD_ADD,
        .doit = test_ctl_func,
        .policy = test_ctl_policy,
};

static int __init testnlk_init(void){
    if(genl_register_family(&test_ctl) != 0)
    {
        printk("register faimly error\n");
        return -1;
    }

    if(genl_register_ops(&test_ctl,&test_ctl_ops) !=  0)
    {
        printk("Register ops error\n");
        goto out;
    }

    return 0;
    out:
    genl_unregister_family(&test_ctl);
    return 0;
}

static void __exit testnlk_exit(void)
{
    genl_unregister_ops(&test_ctl,&test_ctl_ops);
    genl_unregister_family(&test_ctl);

}

module_init(testnlk_init);
module_exit(testnlk_exit);

MODULE_LICENSE("GPL");

内核态代码Makefile

最简单的驱动的Makefile文件,目标文件为test_kernel.o,编译地址我的编译机器为KDIR地址,可根据实际情况进行改变。make -C ( K D I R ) M = (KDIR) M= (KDIR)M=(PWD) modules 编译模块首先改变目录到-C选项指定的位置(即内核源代码目录,其中保存有内核的顶层makefile;M=选项让该makefile在构造modules目标之前返回到模块源代码目录;然后,modules目标指向obj-m变量中设定的模块。

ifneq  ($(KERNELRELEASE),)
obj-m:=test_kernel.o
else
KDIR := /usr/src/kernels/2.6.32-754.23.1.el6.x86_64/
PWD:=$(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules
clean:
    rm -f *.ko *.o *.symvers *.cmd *.cmd.o
endif

你可能感兴趣的:(linux,C++,计算机网络)