在内核态与用户态的交互中,字符设备/sys节点等通信方式上只能单向通信,并且应用层需要读取相应数据时只能循环遍历,当数据比较大时会影响程序运行上下文时间。当前存在的需求不仅仅是内核将事件上报,需要在内核中实现类型select超时接收用户态信息。
本文主要记录学习过程中遇到的几个问题:
针对以上的情况,主要还是如何在内核中实现select形式发送信息之后超时等待应用层回复的内容。
kernel\linux-4.9.y\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
netlink最大支持协议数为32,可自行增加协议类型方便开发。
#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的真实消息长度(消息体 + 消息头)*/
#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)))
/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb); /* input 回调函数 */
struct mutex *cb_mutex;
void (*bind)(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)
/*
net: 所在的网络命名空间, 一般默认为 init_net 是内核定义的变量;
init_net 来自 net_namespace.c(extern struct net init_net)
貌似是不建议使用init_net的,但对于测试足够了
unit:netlink协议类型
cfg: netlink内核配置参数
*/
/* 发送单播消息 */
extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
/*
ssk: netlink socket
skb: skb buff 指针
portid: 通信的端口号
nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用定时睡眠
*/
/* 发送多播消息 */
extern int netlink_broadcast(struct sock *ssk,
struct sk_buff *skb,
__u32 portid,
__u32 group,
gfp_t allocation);
/*
ssk: 同netlink_unicast(对应netlink_kernel_create 返回值)、
skb: 内核skb buff
portid: 端口id
group: 是所有目标多播组对应掩码的"OR"操作的合值。
allocation: 指定内核内存分配方式,通常GFP_ATOMIC用于中断上下文,而GFP_KERNEL用于其他场合。这个参数的存在是因为该API可能需要分配一个或多个缓冲区来对多播消息进行clone
*/
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK (跟AF_INET对应)*/
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID (通信端口号)*/
__u32 nl_groups; /* multicast groups mask */
};
nl_pid就是一个约定的通信端口,用户态使用的时候需要用一个非0的数字,一般来 说可以直接采用上层应用的进程ID(不用进程ID号码也没事,只要系统中不冲突的一个数字即可使用)。对于内核的地址,该值必须用0,也就是说,目标地址是内核的情况下,nl_pid必须设置为0。
值得注意的是,使用组播组进行数据监听的时候nl_groups必须与你要发送的组播掩码地址一致,当前只需要记得使用组播组的时候使用如下操作,详情下文补充。
#define NL_GROUP_MASK(group) (group ? 1 << (group - 1) : 0)
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); /* self pid */
/* interested in group 1<<0 */
src_addr.nl_groups = NL_GROUP_MASK(2);
if (bind(netlink_fd, (struct sockaddr*)&src_addr, sizeof(src_addr))) {
fprintf(stderr,"event listener bind failed\n");
goto done;
}
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
/* iov_base: iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff) */
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
对于该结构,我们更需要关注的是前三个变量参数,对于netlink数据包来说其中msg_name指向的就是目的sockaddr_nl地址结构实例的首地址,iov_base指向的就是消息实体中的nlmsghdr消息头的地址,而iov_len赋值为nlmsghdr中的nlmsg_len即可(消息头+实际数据)
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种通用的消息类型。
nlmsg_flags:消息标记,它们用以表示消息的类型,同样定义在include/uapi/linux/netlink.h中
#define NLM_F_REQUEST 1 /* It is request message. */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 8 /* Echo this request */
#define NLM_F_DUMP_INTR 16 /* Dump was inconsistent due to sequence change */
/* 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 */
nlmsg_seq:消息序列号,用以将消息排队,有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用
nlmsg_pid:发送端口的ID号,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号
linux应用程序–netlink的部分使用方法
Linux中内核与应用程序的交互方式–netlink
用户空间和内核空间通讯–netlink
内核与用户层通信之netlink
浅析内核与用户层通信的四种方法