因为之前对内核的代码接触的不是很多,这篇只是记录自己学习内核通信的内容,肯定存在诸多缺陷,请大佬们批评指正。
github地址:https://github.com/yisenFangW/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
实现功能的demo已上传到github:https://github.com/yisenFangW/Netlink
demo实现的功能即从用户态向内核态发送消息,然后内核获取消息之后直接打印(可以实现其他功能),用法为在用户态传入参数:
usage:./user add|del name age weight
通过dmesg看内核打印到的接受内容;
具体实现的demo很简单,内核态未做任何处理,直接打印,结果如图所示:
用户态演示:
内核态演示:
用户端实现是通过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中;
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;
};
本次测试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;
};
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部分的长度。
*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文件,目标文件为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