Linux内核NETLINK驱动

1. Linux内核netlink

在内核态与用户态的交互中,字符设备/sys节点等通信方式上只能单向通信,并且应用层需要读取相应数据时只能循环遍历,当数据比较大时会影响程序运行上下文时间。当前存在的需求不仅仅是内核将事件上报,需要在内核中实现类型select超时接收用户态信息。

本文主要记录学习过程中遇到的几个问题:

  • 内核使用组播组发送消息到应用层;内核使用单播发送消息到应用层
  • 应用层使用组播组发送消息到内核;应用层使用单播发送消息到内核

针对以上的情况,主要还是如何在内核中实现select形式发送信息之后超时等待应用层回复的内容。

1.1 netlink协议类型

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,可自行增加协议类型方便开发。

1.2 netlink宏定义

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

2. 内核态netlink

2.1 数据结构

2.1.1 netlink_kernel_cfg

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

2.2 函数接口

2.2.1 netlink_kernel_create

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内核配置参数

*/

2.2.2 netlink_unicast

/* 发送单播消息 */
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,该函数在没有接收缓存可利用定时睡眠
*/

2.2.3 netlink_broadcast

/* 发送多播消息 */
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
*/

3. 用户态netlink

3.1 sockaddr_nl

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

3.2 msghdr

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 */
 };
  • msg_name:指向数据包的目的地址
  • msg_namelen:目的地址数据结构的长度
  • msg_iov:消息包的实际数据块
    • iov_base:消息包实际载荷的首地址
    • iov_len:消息实际载荷的长度
  • msg_control:消息的辅助数据
  • msg_controllen:消息辅助数据的大小
  • msg_flags:接收消息的标识

对于该结构,我们更需要关注的是前三个变量参数,对于netlink数据包来说其中msg_name指向的就是目的sockaddr_nl地址结构实例的首地址,iov_base指向的就是消息实体中的nlmsghdr消息头的地址,而iov_len赋值为nlmsghdr中的nlmsg_len即可(消息头+实际数据)

3.3 nlmsghdr

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_NOOP:不执行任何动作,必须将该消息丢弃
    • NLMSG_ERROR:消息发生错误
    • NLMSG_DONE:标识分组消息的末尾
    • NLMSG_OVERRUN:缓冲区溢出,表示某些消息已经丢失
  • 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
浅析内核与用户层通信的四种方法

你可能感兴趣的:(Linux内核编程,linux,网络,内核)