摘要
帮你速读文章内容
Netlink是Linux内核提供的机制,用于内核与用户空间双向通信,支持多种网络操作,如路由、设备管理等,具有无需轮询、使用简单、支持单播组播等优势。
摘要由平台通过智能技术生成
有用

点击上方“Linux随笔录”,选择“置顶/星标公众号”

福利干货,第一时间送达


  • 前言

  • 本篇环境

  • netlink的介绍

  • netlink的数据结构

  • netlink常用宏

  • netlink常用的API

    • 创建netlink消息

    • 释放netlink消息

    • 创建skb

    • 释放skb

    • netlink消息加入到skb

    • 获取struct nlmsghdr数据结构

    • 获取对应的payload

    • 发送单播信息

    • 发送多播信息

  • 用户空间socket操作API

    • socket

    • bind

    • sendto

    • recvfrom

  • netlink的消息格式

  • netlink的使用

    • 内核空间netlink的使用

    • 用户空间netlink的使用

  • 总结


前言

用户空间访问内核空间通常由很多方式如open,write,read,ioctl,mmap,socket,poll等。今天讲的netlink也是一种用户空间访问内核空间的方式,它是在内核与用户态之间进行双向通信,在有线无线网络中用的比较多,所以本篇探讨下netlink的学习和使用

本篇环境

硬件平台:飞凌OK3588开发板

内核源码:5.10.66-rt53

编译环境:Ubuntu 20.04 LTS

编译工具链:aarch64-linux-gnu-

netlink的介绍

Netlink 是 Linux 内核提供的一种机制,用于在内核空间和用户空间之间进行通信。Netlink 套接字允许内核模块和用户空间应用程序进行高效的双向通信,Netlink 套接字可以用于各种网络操作,支持多个协议族,如NETLINK_ROUTENETLINK_NETFILTER 等。常用于网络配置、路由、设备管理等场景。

netlink和其他方法实现用户与内核通信的方法有何优势

  • 任何一方都不需要轮询;如果通过文件通信,用户态应用需要不断检查是否有新消息到达;
  • netlink使用简单,它是基于socket的,可以使用socket api;
  • 只需要在netlink协议族中新增加一个协议;使用netlink的内核部分可以采用模块的方式实现,之后使用socket api进行通信;
  • 内核可以直接向用户层发送信息,而无需用户层事先请求;
  • netlink支持单播、组播;内核模块可以把消息发送到一个多播组;

netlink的数据结构

struct sockaddr_nl: 表示netlink通信地址;

file: include/uapi/linux/netlink.h
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID */
__u32 nl_groups; /* multicast groups mask */
};

参数说明:

  • nl_famile : nl_family,固定为AF_NETLINK,表示netlink协议族
  • nl_pad :nl_pad通常设置为0
  • nl_pid :socket的唯一标识符;发送内核空间来说该字段是0,用户空间的通常使用其线程组PID;netlink并没有要求该字段是进程ID,它可以是任何值,只需要保证其唯一性;使用线程组ID不过是方便而已;nl_pid是一个单播地址
  • nl_groups :多播组掩码,每个bit表示一个多播组;每个netlink协议族最多支持32个多播组

其中netlink协议簇包含多个协议,内核中规定最大值是32。理论上是32以内没有被占用的协议号用户都可以用于自定义netlink。下面是内核中规定netlink协议簇的内容

file: include/uapi/linux/netlink.h
#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_SMC 22 /* SMC monitoring */

#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG

#define MAX_LINKS 32

struct nlmsghdr:描述netlink message的消息头部的结构体

file: include/uapi/linux/netlink.h
struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process port ID */
};

参数说明:

  • nlmsg_len: 整个netlink消息的长度,包含消息头
  • nlmsg_type: 消息的类型。内核在include/uapi/linux/netlink.h中定义以下4种通用的消息类型。
#define NLMSG_NOOP  0x1 /* Nothing.  */
#define NLMSG_ERROR 0x2 /* Error */
#define NLMSG_DONE 0x3 /* End of a dump */
#define NLMSG_OVERRUN 0x4 /* Data lost */

#define NLMSG_MIN_TYPE 0x10 /* < 0x10: reserved control messages */
  • nlmsg_flags: 消息的标志位,可以设置一些控制选项,比如NLM_F_REQUEST
/* Flags values */

#define NLM_F_REQUEST 0x01 /* It is request message. */
#define NLM_F_MULTI 0x02 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 0x04 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 0x08 /* Echo this request */
#define NLM_F_DUMP_INTR 0x10 /* Dump was inconsistent due to sequence change */
#define NLM_F_DUMP_FILTERED 0x20 /* Dump was filtered as requested */

/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)

/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */

/* Flags for ACK message */
#define NLM_F_CAPPED 0x100 /* request was capped */
#define NLM_F_ACK_TLVS 0x200 /* extended ACK TVLs were included */

  • nlmsg_seq:消息序列号,以将消息排队有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用。
  • nlmsg_pid: 消息发送者的port id,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号。

netlink_kernel_cfg: 描述netlink配置选项参数的结构体

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);
};

常用的参数说明

  • groups: 这个成员指定了该 Netlink 套接字可以接收的多播组的掩码。多播组用于向多个进程发送消息。

  • input: 这是一个函数指针,用于指定一个回调函数,当有消息到达该Netlink套接字时,将调用此函数。

  • bind: 这是一个可选的函数指针,允许用户指定自定义的绑定行为。

  • unbind: 这是一个可选的函数指针,允许用户指定自定义的解绑行为。

nlattr:描述消息体

struct nlattr {
__u16 nla_len;
__u16 nla_type;
};

netlink常用宏

#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

用于得到不小于len且字节对齐的最小数值

#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr))) 

用于得到netlink头部长度

#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN) 

用于得到计算消息数据len的真实消息长度,消息体+消息头

#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len)) 

返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值

#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0))) 

用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏

#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))

判断消息是否>len

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

用于返回payload的长度

netlink常用的API

下面介绍下本实验会用到的一些API函数,这些API函数定义在include/linux/netlink.h

创建netlink消息

netlink_kernel_create:创建netlink内核的消息

static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}

参数:

  • net: 表示net指向所在的网络命名空间,一般默认传入的是&init_net,不需要定义;定义在net_namespace.c中。
  • uint:表示netlink子协议族,如NETLINK_GENERIC。填的是你用户空间传进来的netlink协议族
  • cfg: 表示netlink内核配置参数struct netlink_kernel_cfg数据结构。 其中,input是该内核netlink模块收到消息后的处理回调函数

返回值:netlink消息句柄

释放netlink消息

netlink_kernel_release:释放netlink内核的消息

void netlink_kernel_release(struct sock *sk)

参数:

  • sk: 释放netlink_kernel_create()创建的sock。

创建skb

nlmsg_new:创建一个新的skb

struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)

参数:

  • payload: 消息中payload的大小
  • flag : 分配的内存的类型

返回值:分配得到socket 缓冲区的指针。

释放skb

nlmsg_free:释放nlmsg_new()创建的skb

static inline void nlmsg_free(struct sk_buff *skb)
{
kfree_skb(skb);
}

参数:

  • skb:socket 缓冲区的指针

netlink消息加入到skb

nlmsg_put:将一个新的netlink消息加入到skb中。如果skb无法存放消息则返回NULL

static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
int type, int payload, int flags)

参数:

  • skb: socket 缓冲区的指针
  • portid: 指定发送消息的进程的 PID(进程ID)。通常你可以使用0 表示内核发送消息给用户空间,或者使用目标用户空间进程的 PID。
  • seq: 消息的序列号,用于标识消息
  • type: 消息的类型,表示消息的内容类型。这个参数通常是一个定义在相关头文件中的常量,例如RTM_NEWLINKRTM_DELLINK 等,具体类型取决于应用场景。
  • payload: 负载的大小,以字节为单位。这个值指示消息中数据部分的大小,通常是你想要发送的数据的大小。
  • flags: 消息的标志位,用于指定消息的特性。这些标志可以用来设置消息的优先级、确保可靠性等。常见的标志包括NLM_F_REQUEST(表示这是一个请求消息)和NLM_F_ACK(表示需要确认消息)。

获取struct nlmsghdr数据结构

nlmsg_hdr:从sk_buff->data获取struct nlmsghdr数据结构

static inline struct nlmsghdr *nlmsg_hdr(const struct sk_buff *skb)
{
return (struct nlmsghdr *)skb->data;
}

参数:

  • skb:socket 缓冲区的指针

获取对应的payload

nlmsg_data:根据nlmsghdr指针获取对应的payload

static inline void *nlmsg_data(const struct nlmsghdr *nlh)
{
return (unsigned char *) nlh + NLMSG_HDRLEN;
}

参数:

  • nlh:netlink消息头部结构体指针

发送单播信息

netlink_unicast:用来发送单播信息

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

参数:

  • ssk:netlink socket
  • skb:skb buff指针
  • portid:指定发送消息的进程的 PID(进程ID)。通常你可以使用0 表示内核发送消息给用户空间,或者使用目标用户空间进程的 PID。
  • nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;如果为0,该函数在没有接收缓存可利用时定是睡眠。

发送多播信息

netlink_broadcast:用来发送多播信息

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

参数:

  • ssk:netlink_kernel_create()返回值
  • skb:socket 缓冲区的指针
  • portid:指定发送消息的进程的 PID(进程ID)。通常你可以使用0 表示内核发送消息给用户空间,或者使用目标用户空间进程的 PID。
  • group:是所有目标多播组对应掩码的OR操作的合值
  • allocation:指定内核内存分配方式,通常GFP_ATOMIC用于中断上下文,而GFP_KERNEL用于其他场合。这个参数的存在是因为该API可能需要分配一个或多个缓冲区来对多播消息进行clone

用户空间socket操作API

socket

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

该函数用来创建一个套接字,并返回一个描述符,该描述符可以用来访问该套接字。protocol参数设置为0表示使用默认协议。

bind

int bind( int socket, const struct sockaddr *address, size_t address_len);

把通过socket()创建的套接字命名,从而让它可以被其他进程使用

sendto

int sendto(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);

把缓冲区buffer中的信息送给制定的IP端口程序,buffer存放将要发送的数据,len是buffer长度,to是要发送数据到的程序IP端口,tolen是to参数长度。

recvfrom

int recvfrom(int sockfd, void *buffer, size_t len,int flags, struct sockaddr *src_from, socklen_t *src_len);

把发送给程序的信息存储在缓冲区buffer中,并记录数据来源的程序IP端口。buffer存放接收的数据,len是buffer长度,src_from是数据来源程序IP端口,src_len是src_from长度。

netlink的消息格式

netlink消息由两部分组成:消息头和消息体;消息头固定位16字节,消息体长度可变,最后要补齐NLMSG_ALLGNTO;

netlink的使用

netlink的测试分为两部分,一部分是内核提供接收用户空间消息,并响应发送功能;另一部分是用户空间发送netlink到内核,并等待回复。

内核空间netlink的使用

file: netlink_test.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <net/sock.h>
#include <linux/netlink.h>

#define NETLINK_TEST 30
#define MSG_LEN 128
#define USER_PORT 100

struct sock *nl_sk = NULL;

int send_usrmsg(char *pbuf, uint16_t len)
{
struct sk_buff *nl_skb;
struct nlmsghdr *nlh;

int ret;

/* 创建sk_buff 空间 */
nl_skb = nlmsg_new(len, GFP_KERNEL);
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;
}

/* 拷贝pbuf的缓冲区到netlink消息数据 */
memcpy(nlmsg_data(nlh), pbuf, len);
/* 发送单播数据 */
ret = netlink_unicast(nl_sk, nl_skb, USER_PORT, MSG_DONTWAIT);

return ret;
}

static void netlink_rcv_msg(struct sk_buff *skb)
{
struct nlmsghdr *nlh = NULL;
char *umsg = NULL;
char *kmsg = "hello users!!!";
//如果skb数据长度大于等于netlink头部大小
if(skb->len >= nlmsg_total_size(0))
{
//获得skb的数据部分
nlh = nlmsg_hdr(skb);
//取出数据体对齐部分
umsg = NLMSG_DATA(nlh);
if(umsg)
{
printk("kernel recv from user: %s\n", umsg);
send_usrmsg(kmsg, strlen(kmsg));
}
}
}

struct netlink_kernel_cfg cfg = {
.input = netlink_rcv_msg, /* set recv callback */
};

static int __init netlink_init(void)
{
/* create netlink socket */
nl_sk = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if(nl_sk == NULL)
{
printk("netlink_kernel_create error !\n");
return -1;
}
printk(KERN_INFO "Netlink socket created.\n");

return 0;
}

static void __exit netlink_exit(void)
{
if (nl_sk){
netlink_kernel_release(nl_sk); /* release ..*/
nl_sk = NULL;
}

printk(KERN_INFO "Netlink socket released.\n");
}

module_init(netlink_init);
module_exit(netlink_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Pan Shuai");
MODULE_DESCRIPTION("netlink example");

用户空间netlink的使用

file:netlink.c
#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 128
#define MAX_PLOAD 128

typedef struct _user_msg_info
{

struct nlmsghdr hdr;
char msg[MSG_LEN];
} user_msg_info;

int main(int argc, char **argv)
{
int sock_fd;
int ret;
user_msg_info u_info;
socklen_t len;
struct nlmsghdr *nlh = NULL;
struct sockaddr_nl src_addr, dest_addr;
char *umsg = "hello netlink!!";

/* 创建NETLINK socket */
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1)
{
perror("create socket error\n");
return -1;
}

// 填充源地址
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK; //AF_NETLINK
src_addr.nl_pid = 100; //端口号(port ID)
src_addr.nl_groups = 0;

// 绑定套接字
if(bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) != 0)
{
perror("bind() error\n");
close(sock_fd);
return -1;
}

memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; // to kernel
dest_addr.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 = src_addr.nl_pid; //self port

// 填充消息数据
memcpy(NLMSG_DATA(nlh), umsg, strlen(umsg));

// 发送消息
ret = sendto(sock_fd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr_nl));
if(!ret)
{
perror("sendto error\n");
close(sock_fd);
exit(-1);
}
printf("userspace send kernel:%s\n", umsg);

// 接收消息
memset(&u_info, 0, sizeof(u_info));
len = sizeof(struct sockaddr_nl);
ret = recvfrom(sock_fd, &u_info, sizeof(user_msg_info), 0, (struct sockaddr *)&dest_addr, &len);
if(!ret)
{
perror("recv form kernel error\n");
close(sock_fd);
exit(-1);
}

printf("userspace recv from kernel:%s\n", u_info.msg);

// 关闭套接字
close(sock_fd);

free((void *)nlh);
return 0;
}

注意:代码中的NETLINK_TEST是我自定义的NETLINK消息类型

放在板子上进行编译测试:

aarch64-linux-gnu-gcc netlink.c -o netlink

执行

从测试结果来看,编译测试看到打印先从用户空间发字符串"hello netlink"到内核空间,然后在收到内核空间发给用户空间字符串"hello users"。由此可见,netlink是可以在用户和内核空间进行双向通信的机制

总结

本篇讲了下netlink的使用方法,学废的话一键三连支持下小编,欢迎关注公众号[Linux随笔录],继续输出Linux随笔知识

参考文章:

[1] https://blog.csdn.net/congchp/article/details/

[2] https://www.cnblogs.com/arnoldlu/p/9532254.html

end


往期推荐



学习嵌入式推荐的必读书籍我的Linux驱动学习路线1V1付费咨询单片机、嵌入式大神平时浏览哪些网站推荐应届生写进简历的十个项目



举报/反馈

Linux随笔录

63获赞 31粉丝
垂直打造嵌入式领域的技术文章
关注
0
0
收藏
分享