如何在Linux内核中添加一个新的Netlink指令(命令)
Linux内核中Netlink有两种常见方式添加新指令:
- Generic Netlink(genl,推荐方式) 现代、灵活、动态分配family ID、无需修改内核头文件,广泛用于nl80211、ethtool、taskstats等子系统。
- 原生Netlink(legacy/raw Netlink,不推荐新开发) 旧式方式,需要固定协议号(NETLINK_XXX),命令通过nlmsg_type字段区分。早期子系统(如NETLINK_ROUTE、NETLINK_USERSOCK)使用此方式。
下面分别详细说明两种方式的完整实现步骤,并以相同功能为例:实现一个命令MY_CMD_ECHO,用户空间发送一个字符串,内核回显该字符串。
一、Generic Netlink(推荐)
1. 定义命令、属性和family名称
#include <net/genetlink.h>
/* 命令枚举 */
enum my_commands {
MY_CMD_UNSPEC, /* 必须有 */
MY_CMD_ECHO,
__MY_CMD_MAX,
};
#define MY_CMD_MAX (__MY_CMD_MAX - 1)
/* 属性枚举 */
enum my_attrs {
MY_ATTR_UNSPEC,
MY_ATTR_MSG, /* 携带的字符串 */
__MY_ATTR_MAX,
};
#define MY_ATTR_MAX (__MY_ATTR_MAX - 1)
/* family 定义 */
#define MY_FAMILY_NAME "MYFAMILY"
#define MY_FAMILY_VERSION 1
2. 属性验证策略(推荐)
static const struct nla_policy my_genl_policy[MY_ATTR_MAX + 1] = {
[MY_ATTR_MSG] = { .type = NLA_STRING, .len = 256 }, /* 最大256字节字符串 */
};
3. 命令处理函数
static int my_echo_doit(struct sk_buff *skb, struct genl_info *info)
{
struct sk_buff *rep;
void *reply_hdr;
char *msg;
if (!info->attrs[MY_ATTR_MSG])
return -EINVAL;
msg = nla_data(info->attrs[MY_ATTR_MSG]);
/* 分配回复skb */
rep = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
if (!rep)
return -ENOMEM;
/* 开始构造回复消息 */
reply_hdr = genlmsg_put_reply(rep, info, info->snd_portid, info->snd_seq,
info->nlhdr, 0, MY_CMD_ECHO);
if (!reply_hdr)
goto nla_put_failure;
/* 把原消息回显回去 */
if (nla_put_string(rep, MY_ATTR_MSG, msg))
goto nla_put_failure;
genlmsg_end(rep, reply_hdr);
/* 单播发送回复 */
return genlmsg_reply(rep, info);
nla_put_failure:
genlmsg_cancel(rep, reply_hdr);
kfree_skb(rep);
return -EMSGSIZE;
}
4. 操作(ops)定义
static const struct genl_ops my_genl_ops[] = {
{
.cmd = MY_CMD_ECHO,
.doit = my_echo_doit,
.policy = my_genl_policy,
.flags = GENL_CMD_CAP_DO, /* 可选:需要CAP_NET_ADMIN用 GENL_ADMIN_PERM */
},
};
5. family注册
static struct genl_family my_genl_family = {
.name = MY_FAMILY_NAME,
.version = MY_FAMILY_VERSION,
.maxattr = MY_ATTR_MAX,
.ops = my_genl_ops,
.n_ops = ARRAY_SIZE(my_genl_ops),
.module = THIS_MODULE,
/* .mcgrps = ... 如果需要多播组 */
};
static int __init my_genl_init(void)
{
int ret = genl_register_family(&my_genl_family);
if (ret) {
pr_err("genl_register_family failed: %d\n", ret);
return ret;
}
pr_info("MYFAMILY registered, family id: %d\n", my_genl_family.id);
return 0;
}
static void __exit my_genl_exit(void)
{
genl_unregister_family(&my_genl_family);
}
module_init(my_genl_init);
module_exit(my_genl_exit);
6. 用户空间调用方式(使用libnl)
int family = genl_ctrl_resolve(sock, "MYFAMILY"); // 动态获取family id
struct nl_msg *msg = nlmsg_alloc();
genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, family, 0, 0, MY_CMD_ECHO, MY_FAMILY_VERSION);
nla_put_string(msg, MY_ATTR_MSG, "Hello from userspace!");
nl_send_auto(sock, msg);
优点:无需修改uapi头文件、动态ID、支持多命令、多属性、权限控制、多播等现代特性。
二、原生Netlink(Legacy方式,不推荐新项目)
1. 定义协议号(需修改内核头文件)
在 include/uapi/linux/netlink.h 添加:
#define NETLINK_MYPROTO 31 /* 找一个未使用的数字(最大31) */
注意:协议号0-31固定,超过需要修改MAX_LINKS。新内核不鼓励添加新协议。
2. 定义命令(通过nlmsg_type区分)
enum my_netlink_commands {
MY_NL_CMD_UNSPEC,
MY_NL_CMD_ECHO,
__MY_NL_CMD_MAX,
};
3. 消息接收处理函数
static int my_netlink_rcv(struct sk_buff *skb, struct nlmsghdr *nlh, struct netlink_ext_ack *extack)
{
char *msg;
int len;
struct sk_buff *rep;
struct nlmsghdr *rep_nlh;
if (nlh->nlmsg_type != MY_NL_CMD_ECHO)
return -EINVAL;
/* 解析payload(简单假设是null-terminated string) */
msg = nlmsg_data(nlh);
len = nlmsg_len(nlh);
/* 分配回复 */
rep = nlmsg_new(len, GFP_KERNEL);
if (!rep)
return -ENOMEM;
rep_nlh = nlmsg_put(rep, NETLINK_CB(skb).portid, nlh->nlmsg_seq,
MY_NL_CMD_ECHO, len, 0);
if (!rep_nlh) {
kfree_skb(rep);
return -EMSGSIZE;
}
memcpy(nlmsg_data(rep_nlh), msg, len);
/* 发送单播回复 */
netlink_unicast(nl_sk, rep, NETLINK_CB(skb).portid, 0);
return 0;
}
static void my_netlink_rcv_skb(struct sk_buff *skb)
{
netlink_rcv_skb(skb, &my_netlink_rcv);
}
4. 创建Netlink socket并注册
static struct netlink_kernel_cfg my_nl_cfg = {
.input = my_netlink_rcv_skb,
/* .flags = NL_CFG_F_NONROOT_RECV 可选权限控制 */
};
static struct sock *nl_sk;
static int __init my_raw_init(void)
{
nl_sk = netlink_kernel_create(&init_net, NETLINK_MYPROTO, &my_nl_cfg);
if (!nl_sk) {
pr_err("netlink_kernel_create failed\n");
return -ENOMEM;
}
pr_info("Raw netlink MYPROTO created\n");
return 0;
}
static void __exit my_raw_exit(void)
{
netlink_kernel_release(nl_sk);
}
module_init(my_raw_init);
module_exit(my_raw_exit);
5. 用户空间调用(原始socket)
int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_MYPROTO);
struct nlmsghdr *nlh = ...;
nlh->nlmsg_type = MY_NL_CMD_ECHO;
strcpy(NLMSG_DATA(nlh), "Hello");
send(sock, nlh, nlh->nlmsg_len, 0);
缺点:
- 需要修改uapi头文件(难以进入主线)
- 协议号固定且稀缺
- 没有内置属性解析(需手动解析TLV)
- 权限、多播等功能支持较弱
- 已被Generic Netlink取代
总结对比
| 特性 | Generic Netlink (推荐) | 原生Netlink (Legacy) |
|---|---|---|
| 是否需要修改uapi头 | 否 | 是(添加NETLINK_XXX) |
| family ID分配 | 动态分配 | 固定协议号 |
| 属性解析 | 内置nla_* API + policy验证 | 手动解析 |
| 多命令支持 | 自然支持(genl_ops数组) | 通过nlmsg_type手动区分 |
| 权限控制 | 支持GENL_ADMIN_PERM等 | 有限 |
| 多播组 | 支持 | 支持但较复杂 |
| 现代子系统使用情况 | 绝大多数(如nl80211、mac80211) | 极少数旧协议(如route) |
| 推荐程度 | 强烈推荐新开发 | 不推荐,仅维护旧代码 |