一、前言
在 嵌入式linux 中,应用程序常常需要和内核做通信,其中我们熟悉的方法有系统调用,异步IO等,但这些只能用于单工通信,即应用程序主动跟内核通信或者内核发送信号给应用程序,在某些场合中并不使用。而本文将介绍一种双工通信方法——netlink,它即可以在内核中主动传输数据,有可以在应用程序中发送数据,如同我们在做网编编程一样。本文就讲述 netlink 在嵌入式中常用的 2 种使用方法:数据传输、获取uevent事件信息
二、netlink
2.1 通信方式总结
应用程序和内核通信的常用方式如下:
- 系统调用:常见的有 write、read、ioctl 等等,它需要应用程序主动向内核写入或读取数据,是一种同步的单工数据传输方式
- /proc文件系统:同 系统调用 类似
- 异步IO:可以通过编写驱动代码,使得内核在某些时刻主动向应用程序发送信号,但无法传输大量数据。
- netlink:是一种同步或者异步的数据传输方式,使用 netlink 在应用程序和内核建立起连接后即可进行双工数据发送
2.2 netlink 优点
- 支持全双工、异步通信
- 用户空间使用标准的socket接口即可进行通信
- 支持多播
- 在内核端可用于进程上下文与中断上下文
2.3 netlink 常见应用
目前 linux内核 已经使用 netlink 实现了多种功能应用,关于功能使用后面会有部分讲解。下面罗列出几个常用的功能,如下:
- 获取或修改路由信息
- 监听TCP协议数据报文
- 防火墙
- netfilter子系统
- 内核事件向用户态通知
2.4 netlink 协议
为什么把 netlink 称之为协议呢?
按照笔者的理解,netlink 很想网络编程,因为它有一定的格式要求,是通过发送 消息 来完成数据传输的。那么它就需要 消息格式 和 协议流程。说得简单一些,就是 netlink 是一种通信方式,是内核提供的一种功能。按照内核的只提供机制不提供策略的理念,netlink 本身是不具备 协议流程 这个概念的。但我们要使用好 netlink,往往需要我们去指定数据的传输流程,所以就需要有一定的 数据格式 和 传输流程。其中 数据格式 可以看成就是 消息,是内核已经做好的固定格式,而 传输流程 则是 协议流程。当然了,另一方面则是因为 netlink 用就是 socket套接字 那一套编程接口,所以十分像是网络协议。
2.4.1 消息
消息 是 netlink 的主要发送单元,也就是 netlink 发送的数据只能是以 消息 为单位。 消息 的组成是 netlink消息头 加 有效载荷(数据)。从内存的角度来讲,就是 有效载荷 是直接放在 netlink消息头 后面的,也就是前面 16个字节 是 netlink消息头,后面的全部都是 有效载荷。
netlink消息头 组成如下:
- 总长度:包括 netlink消息头 在内的总字节数,长度为 32bit
- 消息类型:表示 消息 的类型,往往用于协议流程。内核定义了多个标准的消息类型。
内核已经使用 netlink 实现了多种协议,每个 协议都有特定的功能,所以每个 协议 都可能定义了额外的消息类型。长度为 16bit - 消息标志:可以用来更改 消息类型 的行为,某些 协议 有可能会用到该字段。长度为 16bit
- 序列号:该项是 可选项,类似 TCP协议 中的报文号。长度为 32bit
- 端口号:表示 消息 发往的进程。如果 消息 没有指定端口号,那么会被发送给 同一个协议中第一个匹配的内核端套接字。可以理解为网络编程中用于 寻址 的数据,端口号 一般是当前进程的 进程号。如果 端口号 为 0 ,则是表明消息需要发往 内核
关于 消息类型 和 消息标志 的信息可以参考附录中的《libnl库应用详解(一)》
2.4.2 协议
这里简单的说一下内核支持的 netlink 协议,在 include/uapi/linux/netlink.h 中已经定义了一些 协议类型 的宏,每个宏都代表一种协议,内核最多支持 32 种协议。如下所示:
#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
以 NETLINK_ROUTE 协议为例子,它可以获取和修改设备的路由信息。下面是它支持的一些 消息类型:
RTM_NEWLINK 创建获取网络设备的信息
RTM_DELLINK 删除网络设备的信息
RTM_GETLINK 获取网络设备的信息
RTM_NEWADDR 创建网络设备的IP信息
RTM_DELADDR 删除网络设备的IP信息
RTM_GETADDR 获取网络设备的IP信息
RTM_NEWROUTE 创建网络设备的路由信息
RTM_DELROUTE 删除网络设备的路由信息
RTM_GETROUTE 获取网络设备的路由信息
RTM_NEWNEIGH 创建网络设备的相邻信息
RTM_DELNEIGH 删除网络设备的相邻信息
RTM_GETNEIGH 获取网络设备的相邻信息
RTM_NEWRULE 创建路由规则信息
RTM_DELRULE 删除路由规则信息
RTM_GETRULE 获取路由规则信息
RTM_NEWQDISC 创建队列的原则
RTM_DELQDISC 删除队列的原则
RTM_GETQDISC 获取队列的原则
RTM_NEWTCLASS 创建流量的类别
RTM_DELTCLASS 删除流量的类别
RTM_GETTCLASS 获取流量的类别
RTM_NEWTFILTER 创建流量的过虑
RTM_DELTFILTER 删除流量的过虑
RTM_GETTFILTER 获取流量的过虑
除了 消息类型 之外,有效载荷 也是使用一些指定的数据结构,比如 ifinfomsg 、rtattr。因为笔者对此并不熟悉,所以不做过多描述。只是为了举例让读者门理解 netlink协议 的概念,有兴趣的读者可以从其他途径查找资料。
2.5 接口及数据结构
在前面讲了关于 netlink 的基本要点中,我们知道 netlink消息头 是数据传输的过程中需要的基本格式,内核也已经帮我们实现了一些数据结构及接口来帮助我们实现相关功能。
netlink 的接口分为 应用层接口 和 内核接口,我们分别需要在 应用层实现策略,然后在 内核实现机制。所以一个基本的自定义 netlink协议 需要分别在 应用层 和 内核 中有代码实现
2.5.2 应用层接口
前面说了 netlink 可以直接使用 socket套接字 的标准接口直接进行代码编写,也就是说 socket 、 bind 等接口都可以直接使用。
而按照笔者理解,netlink 的编程与 UDP 编程类似,但在网络编程中我们是使用 IP地址 + 端口号 进行寻址的,而 netlink 则是通过 协议类型+进程ID 进行寻址的,其中 协议类型 会在调用 socket接口 的时候指定。
netlink 基本的编程步骤如下:
- 使用 socket 声明套接字
- 使用 bind 绑定 本地地址 到套接字
- 构造 消息
- 发送或接收 消息,而在 netlink 中有 2 套发送及接收数据的接口,分别是 sendto和recvfrom、sendmsg和recvmsg
2.5.2.1 socket
socket 在网络编程中也需要用到,用于声明一个套接字、但在 netlink 中,它的参数有所不同。socket接口 的原型如下:
int socket(int domain, int type, int protocol);
它 netlink 中的参数如下:
- 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.5.2.2 bind
bind 接口用于绑定套接字和地址,原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
netlink 使用的 地址数据结构 与网络编程不同,如下所示:
struct sockaddr_nl {
__kernel_sa_family_t nl_family; //一般为AF_NETLINK
unsigned short nl_pad; //无需填充
__u32 nl_pid; //与内核通信的进程的进程ID,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;
}
2.5.2.1 sendto及recvfrom
上面分别完成了 声明套接字 和 本地地址绑定套接字 的 2 个前置步骤,完成之后我们就可以 构造和发送消息。netlink 有 2 套接收和发送消息的接口,本文主要讲述 sendto和recvfrom,对于 sendmsg和recvmsg 有兴趣的朋友可以参考附录中的《内核与用户层通信之netlink》
netlink 的基本数据单元是 消息,那么 sendto和recvfrom 在 netlink中 的基本接收单元也就是 消息。
消息 = netlink消息头 + 有效载荷,netlink消息头 的数据结构如下,与 2.4.1节 中的图片一一对应:
struct nlmsghdr {
__u32 nlmsg_len; //包括netlink消息头在内,整个消息的程度
__u16 nlmsg_type; //消息类型
__u16 nlmsg_flags; //消息标志
__u32 nlmsg_seq; //消息报文的序列号
__u32 nlmsg_pid; //发送进程的进程ID
};
构造消息 免不了需要进行一些 长度 和 地址 的计算,内核已经提供了一些宏帮助我们进行此类操作。如下所示:
#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的真实消息长度(消息体 + netlink消息头)*/
#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)))
而我们常用的一般有下面几个:
- NLMSG_SPACE:参数是 有效载荷的长度,其计算结果是包含 netlink消息头 在内的 消息长度
- NLMSG_DATA:参数是 netlink消息头地址,计算结果是 有效载荷的首地址
sento 和 recvfrom 的原型如下:
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);
从原型中我们可有看到,都分别需要传入 地址 来指定 发送目的地 和 接收源地址。
实例如下:
#define PAYLOAD_LEN 128
/* 自定义消息类型 */
typedef enum{
GPIO_NLMSG_REQUEST = 1,
GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;
/* 使用结构体将netlink消息头和有效载荷捆绑为一个结构体,编程时直接对结构体操作即可 */
typedef struct _user_msg_t
{
struct nlmsghdr hdr;//netlink消息头
char paylaod[PAYLOAD_LEN];//有效载荷
}user_msg_t;
int main()
{
struct sockaddr_nl nlsrc_addr = {0};
struct sockaddr_nl nldst_addr = {0};
user_msg_t user_msg_send = {0};
user_msg_t user_msg_recv = {0};
......
/* 设置目的socket地址 */
nldst_addr.nl_family = AF_NETLINK;
nldst_addr.nl_pid = 0;//0表示内核netlink地址
nldst_addr.nl_groups = 0;
/* 构造发送消息 */
user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE会自动加上消息头部去计算, 请求消息不需要有数据
user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
user_msg_send.hdr.nlmsg_type = GPIO_NLMSG_REQUEST;
user_msg_send.hdr.nlmsg_flags = 0;
user_msg_send.hdr.nlmsg_seq = 0;
/* 发送消息 */
send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);
/* 等待并接收消息 */
recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
}
2.5.3 内核接口
netlink的内核实现步骤与应用层相似,但相对来说比应用层灵活一些。其步骤大致如下:
- 创建socket套接字
- 构造消息
- 发送消息
netlink 内核接口成分比较复杂,涉及到个不同的头文件,下面简单的说明一下各个内核接口所在的头文件。
2.5.3.1 netlink模块接口
该模块的接口一般是实现 netlink 的功能,比如创建socket套接字、释放socket套接字、单播 、多播 等,其头文件是 include/linux/netlink.h。下面罗列几个常用的接口及数据结构
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);
};
static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
void netlink_kernel_release(struct sock *sk);
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);
-
netlink_kernel_create 用于创建套接字,其参数如下:
- net: 一般为 &init_net
- unit:指定协议类型,与用户空间的 socket接口 对应
- cfg :该参数是 struct netlink_kernel_cfg结构体指针,其常用成员是 input 和 groups。 input 是接收到消息时的 回调函数,我们一般在这个 回调函数 内完成数据接收。groups 用于指定 套接字的多播组
-
netlink_kernel_release 用于释放套接字,参数如下:
- sk:是由 netlink_kernel_create 创建的套接字。
-
netlink_unicast 用于单播传输数据,参数如下:
- ssk:由 netlink_kernel_create 创建的套接字
- skb:struct sk_buff 结构体,会将需要传输的 消息 放在其中。
- portid:指定发送目的进程的PID
- nonblock:指定阻塞标志,默认情况下为阻塞,可以使用 MSG_DONTWAIT 指定为非阻塞
2.5.3.2 skb模块接口
struct sk_buff 结构体是内核实现的应用于网络系统的数据结构体,而 netlink 的部分接口也需要使用到。该结构体的实现及其接口比较复杂,其头文件在 include/linux/skbuff.h。笔者对这一部分不甚了解,所以这里简单的罗列几个常用到的接口并简单讲述一下其使用场景,原型如下:
static inline struct sk_buff *skb_get(struct sk_buff *skb)
void kfree_skb(struct sk_buff *skb);
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
-
alloc_skb:用于开辟缓存块,一般是在 发送 时使用
- size:指定缓存块大小,在 netlink 中可以使用宏 NLMSG_SPACE 来计算其大小
- priority:一般指定为 GFP_KERNEL
skb_get:用于增加变量 skb 的计数,一般是在 接收回调 中对回调的参数使用。
kfree_skb:用于减少变量 skb 的计数,当计数为 0 时则释放缓存块。
2.5.3.3 消息模块接口
在实现 netlink 是需要对消息进行构造或者计算等操作,内核为我们提供一系列的方法,其头文件为inlcude/net/netlink.h
nlmsg_put 可以用于直接构造 消息,一般是在 发送 消息时使用,其原型如下:
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int payload, int flags)
- skb:指定作为 有效载荷 的 **struct sk_buff ** 结构体,一般是使用 alloc_skb 开辟
- portid:与 netlink消息头 中的 nlmsg_pid 对应。
- seq:与 netlink消息头 中的 nlmsg_seq 对应。
- type:与 netlink消息头 中的 nlmsg_type 对应。
- payload:与 netlink消息头 中的 nlmsg_len 对应。
- flags:与 netlink消息头 中的 nlmsg_flags 对应。
2.5.3.4 内核netlink步骤
看完以上的接口,我们就可以大致总结一下内核 netlink 实现的基本步骤:
- 实现数据的接收回调
- 注册接收回调并创建socket
- 实现发送函数
- 根据协议需要编写流程代码
2.6 代码例程
下面我们看看示例代码,示例代码中实现了一个简单的协议,协议流程如下:
- 应用程序请求驱动,请求消息不需要设置有效载荷
- 驱动对请求进行应答,成功返回 0,不成功返回 1
- 成功后应用程序可以一直接收来自驱动的外部数据
- 成功请求的程序退出后需要向驱动释放请求,以便其他程序可以继续获取驱动的外部数据
可以看出,其功能就是:外部程序通过对驱动写入数据,这些数据会通过驱动发送请求成功的应用程序。
PS:驱动层代码因为涉及到一些其他代码,所以笔者对netlink意外的代码做了删减,但整体上不影响阅读
应用层代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NETLINK_TEST 23//自定义协议
#define PAYLOAD_LEN 128
/* 自定义消息类型 */
typedef enum{
GPIO_NLMSG_REQUEST = 1,
GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;
/* 使用结构体将netlink消息头和有效载荷捆绑为一个结构体,编程时直接对结构体操作即可 */
typedef struct _user_msg_t
{
struct nlmsghdr hdr;
char paylaod[PAYLOAD_LEN];
}user_msg_t;
int main()
{
struct nlmsghdr nlmsg_hdr = {0};
struct sockaddr_nl nlsrc_addr = {0};
struct sockaddr_nl nldst_addr = {0};
user_msg_t user_msg_send = {0};
user_msg_t user_msg_recv = {0};
int skfd = 0;
int addr_len = 0;
int recv_len = 0;
int send_len = 0;
char ack = 0;
addr_len = sizeof(struct sockaddr_nl);
/* 创建套接字 */
skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(skfd < 0)
{
printf("can not create a netlink socket\n");
return 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;
}
/* 设置目的socket地址 */
nldst_addr.nl_family = AF_NETLINK;
nldst_addr.nl_pid = 0;//0表示内核netlink地址
nldst_addr.nl_groups = 0;
/* 设置请求消息体, 向驱动发送请求,请求消息不需要设置有效载荷 */
user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE会自动加上消息头部去计算, 请求消息不需要有数据
user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
user_msg_send.hdr.nlmsg_type = GPIO_NLMSG_REQUEST;
user_msg_send.hdr.nlmsg_flags = 0;
user_msg_send.hdr.nlmsg_seq = 0;
/* 发送请求 */
send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);
/* 接收来自驱动的应答 */
recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
/* 获取应答数据并根据应答来判断 */
ack = user_msg_recv.paylaod[0];
if(-1 == ack)
{
printf("can't get request\n");
return -1;
}
while(1)
{
/* 接收内核空间返回的数据, recvfrom默认阻塞 */
recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
printf("recv data = %s\n", user_msg_recv.paylaod);
/* 如果外部数据外exit字符串,则退出程序 */
if(!strncmp(user_msg_recv.paylaod, "exit", 4))
break;
}
/* 设置反请求消息体, 让驱动释放清奇 */
user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE会自动加上消息头部去计算, 请求消息不需要有数据
user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
user_msg_send.hdr.nlmsg_type = GPIO_NLMSG_UNREQUEST;
user_msg_send.hdr.nlmsg_flags = 0;
user_msg_send.hdr.nlmsg_seq = 0;
/* 发送反请求消息体 */
send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);
return 0;
}
驱动代码 如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NETLINK_TEST 23
#define PAYLOAD_LEN 128
typedef enum{
GPIO_NLMSG_REQUEST = 1,
GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;
typedef struct _gpio_nl_info_t
{
pid_t user_pid;
int request_flag;
struct semaphore flag_lock;
}gpio_nl_info_t;
struct gpio_device
{
......
struct sock* gpio_sock;
gpio_nl_info_t gpio_nl_info;
};
struct gpio_device* g_gpio_device = NULL;
int gpio_netlink_send(struct sock* nl_sock, int dst_pid, char* data, int len)
{
if(NULL == data)
{
printk(KERN_INFO"data is NULL\n");
return -1;
}
if(len > PAYLOAD_LEN)
{
printk(KERN_INFO"length(%d) is out of range\n", len);
return -1;
}
struct nlmsghdr *nl_msghdr = NULL;
struct sk_buff *skb_send = NULL;
int data_len = 0;
/* 使用宏NLMSG_SPACE计算包括消息头在内的消息长度 */
data_len = NLMSG_SPACE(PAYLOAD_LEN);
/* 开辟skb缓存块 */
skb_send = alloc_skb(data_len, GFP_KERNEL);
if(NULL == skb_send)
{
printk(KERN_INFO"alloc skb error\n");
return -1;
}
/* 使用nlmsg_put构造消息结构体 */
nl_msghdr = nlmsg_put(skb_send, 0, 0, 0, data_len - NLMSG_SPACE(0), 0);
/* 将要发送的数据赋值到消息上 */
memcpy(NLMSG_DATA(nl_msghdr), data, len);
/* 发送消息 */
netlink_unicast(nl_sock, skb_send, dst_pid, 0);
return 0;
}
void gpio_netlink_input(struct sk_buff *__skb)
{
if(__skb == NULL)
{
printk(KERN_INFO"sk_buff is NULL\n");
return;
}
struct sk_buff *skb = NULL;
struct nlmsghdr *nl_msghdr = NULL;
char* data = NULL;
char ack = 0;
/* 获取__skb的使用权 */
skb = skb_get(__skb);
/* 1. 获取消息头 */
nl_msghdr = nlmsg_hdr(skb);
if(skb->len < NLMSG_SPACE(0))
{
printk(KERN_INFO"message length error, len = %d\n", skb->len);
goto INPUT_EXIT;
}
/* 2. 根据消息类型进行相应处理 */
switch(nl_msghdr->nlmsg_type)
{
/* 2.1 请求占用 */
case GPIO_NLMSG_REQUEST:
/* 获取锁以改变pid占用标志 */
down(&g_gpio_device->gpio_nl_info.flag_lock);
/* 如果当前netlink的pid已经被占用 */
if(1 == g_gpio_device->gpio_nl_info.request_flag)
{
/* 如果当前进程再一次请求则提示不需要再次请求 */
if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
printk(KERN_INFO"can not get request again\n");
else/* 如果其他进程请求则提示当前有其他用户在占用 */
printk(KERN_INFO"other user get request\n");
/* 请求失败返回 -1 给用户空间以做其他处理 */
ack = -1;
gpio_netlink_send(g_gpio_device->gpio_sock, nl_msghdr->nlmsg_pid, &ack, 1);
}
else
{
/* 如果当前netlink的pid没被占用, 则设置相关数据 */
g_gpio_device->gpio_nl_info.request_flag = 1;
g_gpio_device->gpio_nl_info.user_pid = nl_msghdr->nlmsg_pid;
/* 返回 0 给用户空间表示成功 */
ack = 0;
gpio_netlink_send(g_gpio_device->gpio_sock, nl_msghdr->nlmsg_pid, &ack, 1);
}
/* 释放锁 */
up(&g_gpio_device->gpio_nl_info.flag_lock);
break;
/* 2.1 释放占用 */
case GPIO_NLMSG_UNREQUEST:
down(&g_gpio_device->gpio_nl_info.flag_lock);
/* 只有占用标志为 1 的情况下才会执行操作 */
if(1 == g_gpio_device->gpio_nl_info.request_flag)
{
/* 只有占用的进程才能释放请求 */
if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
{
/* 设置占用标志和pid以释放请求 */
g_gpio_device->gpio_nl_info.request_flag = 0;
g_gpio_device->gpio_nl_info.user_pid = 0;
}
else/* 其他进程无权释放请求 */
printk(KERN_INFO"you have no right to release the request\n");
}
up(&g_gpio_device->gpio_nl_info.flag_lock);
break;
/* 2.1 默认情况则打印数据用于调试 */
default:
data = NLMSG_DATA(nl_msghdr);
printk(KERN_INFO"netlink get data_len = %d, pid = %d, data = %s\n", (nl_msghdr->nlmsg_len - NLMSG_SPACE(0)), nl_msghdr->nlmsg_pid, data);
break;
}
INPUT_EXIT:
/* 释放__skb的使用权, 如果此时skb的计数为 0 则会释放出内存 */
kfree_skb(skb);
return;
}
static int gpio_open(struct inode* inode, struct file* filp)
{
printk(KERN_INFO"device open, filp = %#x, f_owner.pid = %#p\n", filp, filp->f_owner.pid);
filp->private_data = (void*)container_of(inode->i_cdev, struct gpio_device, cdev);
return 0;
}
static ssize_t gpio_write(struct file* filp, const char __user * buf, size_t size, loff_t* ppos)
{
struct gpio_device* gpio_device = (struct gpio_device*)filp->private_data;
char gpio_value = 0;
char* gpio_buf = kzalloc(size, GFP_KERNEL);
copy_from_user(gpio_buf, buf, size);
/* 将数据发往应用层 */
gpio_netlink_send(gpio_device->gpio_sock, gpio_device->gpio_nl_info.user_pid, gpio_buf, size);
kfree(gpio_buf);
return size;
}
static struct file_operations gpio_fops =
{
.open = gpio_open,
.write = gpio_write,
};
static int gpio_probe(struct platform_device *pdev)
{
int ret = 0;
/* 为设备数据分配空间 */
struct gpio_device* gpio_device = devm_kzalloc(&pdev->dev, sizeof(struct gpio_device), GFP_KERNEL);
struct device_node *np = pdev->dev.of_node;
if(!gpio_device)
{
printk(KERN_INFO"can't create gpio_device\n", gpio_device->gpio_num);
goto ALLOC_FAIL;
}
......
/* 创建netlink的socket */
struct netlink_kernel_cfg nl_cfg = {
.input = gpio_netlink_input,
};
gpio_device->gpio_sock = netlink_kernel_create(&init_net, NETLINK_TEST, &nl_cfg);
if(NULL == gpio_device->gpio_sock)
{
printk(KERN_INFO"create socket error\n");
goto DEVICE_FAILE;
}
/* 初始化信号量 */
sema_init(&gpio_device->gpio_nl_info.flag_lock, 1);
g_gpio_device = gpio_device;
return 0;
......
}
static int gpio_remove(struct platform_device *pdev)
{
struct gpio_device* gpio_device = (struct gpio_device*)platform_get_drvdata(pdev);
int n_num = 1;
netlink_kernel_release(gpio_device->gpio_sock);
...
return 0;
}
static const struct of_device_id of_gpio_match[] = {
{ .compatible = "gpio_node", .data = NULL},
{},
};
static struct platform_driver gpio_driver = {
.probe = gpio_probe,
.remove = gpio_remove,
.driver = {
.name = "gpio_driver",
.of_match_table = of_gpio_match,
},
};
module_platform_driver(gpio_driver);
MODULE_LICENSE("GPL");
2.7 获取uevent事件信息
我们前面已经讲了 netlink 用于通信的使用方法,那么本节说说如何使用 netlink 捕捉 uevent事件。
2.7.1 捕获 uevent事件 的作用
- 在 嵌入式Linux 中,我们有时需要检测设备是否在进行 热插播,而内核或者某些驱动实现了 热插拔 上报 uevent事件信息 的机制。这样我们就可以在应用层监听设备的 热插拔状态 ,并根据状态执行不同操作
- 当我们使用 insmod 命令加载内核时,常常需要手动使用 mknod 命令手动添加驱动节点。内核其实是提供了一种机制,该机制可以在加载内核时,通过上报 uevent事件信息,将 主次设备号 上报给应用空间,拿到设备号号后我们就可以自动设备节点,而这也就是 udev 实现的基本原理。
2.7.2 如何捕获uevent事件
实现捕获 uevent事件信息 比较简单,代码编写同前面所讲类似,其应用层步骤如下:
- 创建 socket,其协议类型为 NETLINK_KOBJECT_UEVENT。
- 使用 bind 绑定 socket。其中地址结构体 struct sockaddr_nl 的 group成员 要指定为 NETLINK_KOBJECT_UEVENT
- 直接接收来自内核的 uevent事件信息
除了 应用层 之外,内核 也需要做一定的工作,就是在设备加载或者初始化的时候使用接口 device_create,其原型如下:
device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
第一个参数指定所要创建的设备所从属的类,第二个参数是这个设备的父设备,如果没有就指定为NULL,第三个参数是设备号,第四个参数是设备名称,第五个参数是从设备号。
device_create 的参数如下:
- class:指定所要创建的设备所从属的类
- parent:指定设备的父设备,如果没有就指定为NULL
- devt:指定设备号
- drvdata:设备的私有数据
- fmt:指定 设备名称 (按照笔者的理解)
2.7.3 代码示例
看了步骤之后,其实会发现非常简单,应用层代码 如下:(PS:别忘了在驱动中使用device_create)
#define PAYLOAD_LEN 4096
typedef struct _uevent_msg_t
{
struct nlmsghdr hdr;
char paylaod[PAYLOAD_LEN];
}uevent_msg_t;
int main(int argc,char **argv)
{
uevent_msg_t uevent_msg = {0};
struct sockaddr_nl nl_sockaddr;
int sockfd = 0;
int recv_len = 0;
int i = 0;
int addr_len = sizeof(struct sockaddr_nl);
memset(&nl_sockaddr, 0, sizeof(nl_sockaddr));
nl_sockaddr.nl_family = AF_NETLINK;
nl_sockaddr.nl_groups = NETLINK_KOBJECT_UEVENT;//捕获uevnet时间只能将组设为NETLINK_KOBJECT_UEVENT
nl_sockaddr.nl_pid = getpid();
sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
if(sockfd == -1)
printf("socket creating failed:%s\n", strerror(errno));
if(bind(sockfd, (struct sockaddr *)&nl_sockaddr, sizeof(nl_sockaddr)) == -1)
printf("bind error:%s\n", strerror(errno));
while(1)
{
memset(&uevent_msg, 0, sizeof(uevent_msg));
recv_len = recvfrom(sockfd, &uevent_msg, sizeof(uevent_msg_t), 0, (struct sockaddr*)&nl_sockaddr, &addr_len);
if(recv_len < 0)
printf("receive error\n");
else if(recv_len < 32 || recv_len > sizeof(uevent_msg.paylaod))
printf("invalid message");
for(i = 0; i < recv_len; i++)
{
if(uevent_msg.paylaod[i] == '\0')
uevent_msg.paylaod[i] = '\n';
}
printf("received %d bytes\n%s\n", addr_len, uevent_msg.paylaod);
}
return 0;
}
驱动层 代码做了一些删减,主要展示初始化函数中的代码实现,如下:
static int gpio_probe(struct platform_device *pdev)
{
......
/* 创建设备,发出uevent事件 ,在/sys/class/目录下创建设备类别目录gpio_class */
gpio_device->gpio_class = class_create(THIS_MODULE, "gpio_class");
if(IS_ERR(gpio_device->gpio_class))
{
printk(KERN_INFO"create a class error\n");
goto CDEV_FAILE;
}
/*在/dev/目录和/sys/class/gpio_class目录下分别创建设备文件gpio_dev*/
gpio_device->gpio_dev = device_create(gpio_device->gpio_class, NULL, gpio_device->n_dev, NULL, "gpio_dev");
if(IS_ERR(gpio_device->gpio_dev))
{
printk(KERN_INFO"create a device error\n");
goto CLASS_FAILE;
}
......
}
那么,以上就是 捕获uevent事件 的基本原理和简单实现。
三、结语
本文主要讲述了 netlink 的 2 种使用方法:应用与内核的双工通信、捕获uevent事件。但其实这只是 netlink 的冰山一角,netlink 牵涉到的模块代码之多,之复杂在笔者学习的过程中深有体会,还有许多知识笔者还没接触到。希望通过本文可以让读者们对 netlink 的使用有一些基本的了解,能够帮助各位读者在工作生活中解决一些东西。
四、附录参考链接
libnl库应用详解(一):https://www.jianshu.com/p/ab2cd37a9b76
netlink编程介绍:https://blog.csdn.net/aabb3575007/article/details/17959199
Netlink编程-数据结构:http://edsionte.com/techblog/archives/4131
内核与用户层通信之netlink:https://blog.csdn.net/stone8761/article/details/72780863
netlink 学习笔记 3.8.13内核:https://www.cnblogs.com/D3Hunter/archive/2013/07/22/3207670.html
linux内核编程之netlink:https://blog.csdn.net/shallnet/article/details/17734643
linux协议栈skb操作函数:https://www.cnblogs.com/x_wukong/p/5924484.html
linux内核skb操作:https://blog.csdn.net/weixin_30315435/article/details/98735670
sk_buff 结构体以及完全解释:https://blog.csdn.net/shanshanpt/article/details/21024465
Netlink实现热拔插监控:http://blog.chinaunix.net/uid-24943863-id-3223000.html
Netlink的简介及使用方法:https://blog.csdn.net/ganshuyu/article/details/30241313/
内核kobject上报uevent过滤规则:https://blog.csdn.net/tankai19880619/article/details/11776589