linux 内核与用户空间通信之netlink原理与应用

1. 概述

	本文是基于MTK6779 Android Q (kernel-4.9)内核版本来分析的。

Linux中的进程间通信机制源自于Unix平台上的进程通信机制。Linux实现了以下几种主要的IPC机制:

管道(Pipe)及命名管道(Named Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared Memory),

信号量(Semaphore),套接字(Socket)。

通过这些IPC机制,用户空间进程之间可以完成互相通信。为了完成内核空间与用户空间通信,

Linux提供了基于socket的Netlink通信机制,可以实现内核与用户空间数据的及时交换。

本文第2节概述相关研究工作,第3节与其他IPC机制对比,详细介绍Netlink机制及其关键技术

2. 背景与应用

到目前Linux提供了9种机制完成内核与用户空间的数据交换,

分别是内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,

其中模块参数与sysfs、procfs、debugfs、relayfs是基于文件系统的通信机制,

用于内核空间向用户控件输出信息;sysctl、系统调用是由用户空间发起的通信机制。

由此可见,以上均为单工通信机制,在内核空间与用户空间的双向互动数据交换上略显不足。

Netlink是基于socket的通信机制,由于socket本身的双共性、突发性、不阻塞特点,

因此能够很好的满足内核与用户空间小量数据的及时交互,因此在Linux 2.6内核中广泛使用,

例如SELinux,Linux系统的防火墙分为内核态的netfilter和用户态的iptables,

netfilter与iptables的数据交换就是通过Netlink机制完成。

3 Netlink机制及其关键技术

3.1 Netlink机制

Linux操作系统中当CPU处于内核状态时,可以分为有用户上下文的状态和执行硬件、软件中断两种。

其中当处于有用户上下文时,由于内核态和用户态的内存映射机制不同,不可直接将本地变量传给用户态的内存区;

处于硬件、软件中断时,无法直接向用户内存区传递数据,代码执行不可中断。针对传统的进程间通信机制,

他们均无法直接在内核态和用户态之间使用,原因如下表:

通信方法									无法介于内核态与用户态的原因

管道(不包括命名管道)						局限于父子进程间的通信。

消息队列									在硬、软中断中无法无阻塞地接收数据。

信号量										无法介于内核态和用户态使用。

内存共享									需要信号量辅助,而信号量又无法使用。

套接字										在硬、软中断中无法无阻塞地接收数据。

解决内核态和用户态通信机制可分为两类:

	A. 处于有用户上下文时:
	
		可以使用Linux提供的copy_from_user()和copy_to_user()函数完成,但由于这两个函数可能阻塞,
		
		因此不能在硬件、软件的中断过程中使用。
	
	B. 处于硬、软件中断时:
	
	B.1 可以通过Linux内核提供的spinlock自旋锁实现内核线程与中断过程的同步,
			
		由于内核线程运行在有上下文的进程中,因此可以在内核线程中使用套接字或消息队列来取得用户空间的数据,
		
		然后再将数据通过临界区传递给中断过程.
	
	B.2 通过Netlink机制实现。Netlink 套接字的通信依据是一个对应于进程的标识,
	
		一般定为该进程的 ID。Netlink通信最大的特点是对对中断过程的支持,
		
		它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,
		
		而是通过另一个软中断调用用户事先指定的接收函数。通过软中断而不是自行启动内核线程保证了数据传输的及时性。

3.2 Netlink优点

	Netlink相对于其他的通信机制具有以下优点:
	
	使用Netlink通过自定义一种新的协议并加入协议族即可通过socket API使用Netlink协议完成数据交换,
	
	而ioctl和proc文件系统均需要通过程序加入相应的设备或文件。
	
	Netlink使用socket缓存队列,是一种异步通信机制,而ioctl是同步通信机制,
	
	如果传输的数据量较大,会影响系统性能。
	
	Netlink支持多播,属于一个Netlink组的模块和进程都能获得该多播消息。
	
	Netlink允许内核发起会话,而ioctl和系统调用只能由用户空间进程发起。
	
	在内核源码有关Netlink协议的头文件中包含了内核预定义的协议类型,如下所示:	
	
	#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
	
上述这些协议已经为不同的系统应用所使用,每种不同的应用都有特有的传输数据的格式,

因此如果用户不使用这些协议,需要加入自己定义的协议号。对于每一个Netlink协议类型,

可以有多达 32多播组,每一个多播组用一个位表示,Netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,

因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。

建立Netlink会话过程如下:

在这里插入图片描述
内核使用与标准 socket API 类似的一套API完成通信过程。首先通过 netlink_kernel_create() 创建套接字,

该函数的原型如下:

static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)

net: 是网络设备命名空间指针,一般直接填&init_net

unit: 协议类型,可自定义,如 #define NETLINK_TEST 25

cfg:配置结构,类型如下:

/* optional Netlink kernel configuration parameters */
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:组编号;

input:是 netlink socket 在接受到消息时调用的回调函数指针,接收一个 sk_buff 结构,数据包含一个 nlmsghdr 协议头;

return:返回一个sock结构,返回NULL表示创建失败;

然后用户空间进程使用标准 Socket API 来创建套接字,将进程ID发送至内核空间,用户空间创建使用socket()创建套接字,该函数的原型如下:

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

domain: 值为 PF_NETLINK,即Netlink使用协议族。

protocol: 为Netlink提供的协议或者是用户自定义的协议,Netlink提供的协议包括 NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6和 NETLINK_IP6_FW。



接着使用bind函数绑定。Netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联。

完成绑定,内核空间接收到用户进程ID之后便可以进行通讯。

如: 
    struct sockaddr_nl addr;

    memset(&addr, 0, sizeof(struct sockaddr_nl));
    addr.nl_family = AF_NETLINK;
    addr.nl_pid = TEST_PID;
    addr.nl_groups = 0;

    return bind(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_nl));//sock_fd 为打开的socket

用户空间进程发送数据使用标准 socket API中 sendmsg() 函数完成,使用时需添加s truct msghdr 消息和 nlmsghdr 消息头。

一个netlink消息体由 nlmsghdr 和消息的 payload 部分组成,输入消息后,内核会进入 nlmsghdr 指向的缓冲区。

内核空间发送数据使用独立创建的 sk_buff 缓冲区,Linux定义了如下宏方便对于缓冲区地址的设置,如下所示:

#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))

在对缓冲区设置完成消息地址之后,可以使用 netlink_unicast() 来发布单播消息,netlink_unicast()原型如下:

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

ssk: 函数 netlink_kernel_create() 返回的socket,

skb: 存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,
	
	前面的宏 NETLINK_CB(skb) 就用于方便设置该控制块,
	
portid: 为接收消息进程的pid,

nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。

内核模块或子系统也可以使用函数 netlink_broadcast 来发送广播消息:

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

前面的三个参数与netlink_unicast相同,

group:为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。

allocation:为内核内存分配类型,一般地为 GFP_ATOMIC 或 GFP_KERNEL,GFP_ATOMIC 用于原子的上下文(即不可以睡眠),而 GFP_KERNEL 用于非原子上下文。

接收数据时程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。然后使用标准函数接口 recvmsg() 来接收netlink消息

4. 其他相关说明

Netlink 是一种特殊的 socket,它是 Linux 所特有的,类似于 BSD 中的AF_ROUTE 但又远比它的功能强大,

目前在使用的 Linux kernel-4.9 中使用 netlink 进行应用与内核通信的应用很多,包括:

路由 daemon(NETLINK_ROUTE),1-wire 子系统(NETLINK_W1),用户态 socket 协议(NETLINK_USERSOCK),

防火墙(NETLINK_FIREWALL),socket 监视(NETLINK_INET_DIAG),netfilter 日志(NETLINK_NFLOG),

ipsec 安全策略(NETLINK_XFRM),SELinux 事件通知(NETLINK_SELINUX),iSCSI 子系统(NETLINK_ISCSI),

进程审计(NETLINK_AUDIT),转发信息表查询(NETLINK_FIB_LOOKUP),netlink connector(NETLINK_CONNECTOR),

netfilter 子系统(NETLINK_NETFILTER),IPv6 防火墙(NETLINK_IP6_FW),DECnet 路由信息(NETLINK_DNRTMSG),

内核事件向用户态通知(NETLINK_KOBJECT_UEVENT),通用 netlink(NETLINK_GENERIC)。

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,

内核态需要使用专门的内核 API 来使用 netlink。

Netlink 相对于系统调用,ioctl 以及 /proc 文件系统而言具有以下优点:

1,为了使用 netlink,用户仅需要在 include/linux/netlink.h 中增加一个新类型的 netlink 协议定义即可, 

    如 #define NETLINK_MYTEST 17 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换。
	
	但系统调用需要增加新的系统调用,ioctl 则需要增加设备或文件, 那需要不少代码,proc 文件系统则需要在 /proc 下添加新的文件或目录,
	
	那将使本来就混乱的 /proc 更加混乱。

2. netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,

	发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息,但系统调用与 ioctl 则是同步通信机制,
	
	如果传递的数据太长,将影响调度粒度。

3.使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖,

	但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。

4.netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,

	内核事件向用户态的通知机制就使用了这一特性,任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件,在后面的文章中将介绍这一机制的使用。

5.内核可以使用 netlink 首先发起会话,但系统调用和 ioctl 只能由用户应用发起调用。

6.netlink 使用标准的 socket API,因此很容易使用,但系统调用和 ioctl则需要专门的培训才能使用。	

5. API

5.1 用户态API

用户态应用使用标准的 socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 就能很容易地使用 netlink socket,

查询手册页可以了解这些函数的使用细节,本文只是讲解使用 netlink 的用户应该如何使用这些函数。

注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h。
		A. socket()
	
		为了创建一个 netlink socket,用户需要使用如下参数调用 socket():
	
		int socket(int domain, int type, int protocol);
	
		domain: 值为 PF_NETLINK,即Netlink使用协议族。
		
		protocol: 为Netlink提供的协议或者是用户自定义的协议,Netlink提供的协议包括 NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6和 NETLINK_IP6_FW。
		
		第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,
		
		第二个参数必须是 SOCK_RAW 或 SOCK_DGRAM,
		
		第三个参数指定netlink协议类型,如前面讲的用户自定义协议类型 NETLINK_MYTEST, NETLINK_GENERIC 是一个通用的协议类型,
		
		它是专门为用户使用的,因此,用户可以直接使用它,而不必再添加新的协议类型。内核预定义的协议类型有:	
		
			#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	
	
		对于每一个netlink协议类型,可以有多达 32多播组,每一个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,
		
		因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。	
	B. bind()
	
		函数 bind() 用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一起。netlink socket 的地址结构如下:
		
		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_family 必须设置为 AF_NETLINK 或着 PF_NETLINK,
		
		字段 nl_pad 当前没有使用,因此要总是设置为 0,
		
		字段 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。
		
		字段 nl_groups 用于指定多播组,bind() 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组。
		
		传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这相当于 netlink socket 的本地地址。但是,对于一个进程的多个线程使用 netlink socket 的情况,
		
		字段 nl_pid 则可以设置为其它的值,如:
		
		pthread_self() << 16 | getpid();	
		
		
		因此字段 nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,用户可以根据自己需要设置该字段。函数 bind 的调用方式如下:

		bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));		
		
		fd:为前面的 socket 调用返回的文件描述符,
		
		nladdr:为 struct sockaddr_nl 类型的地址。
		
		为了发送一个 netlink 消息给内核或其他用户态应用,需要填充目标 netlink socket 地址,此时,字段 nl_pid 和 nl_groups 分别表示接收消息者的进程 ID 与多播组。
		
		如果字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,如果 nl_groups 为 0,表示该消息为单播消息,否则表示多播消息。
	C. sendmsg()
	
		sendmsg(fd, &msg, 0);
		
		使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec,struct sockaddr_nl 如下:	
		
		bionic/libc/include/sys/socket.h		
		bionic/libc/kernel/uapi/linux/netlink.h
		
		struct msghdr {
			void* msg_name;
			socklen_t msg_namelen;
			struct iovec* msg_iov;
			size_t msg_iovlen;
			void* msg_control;
			size_t msg_controllen;
			int msg_flags;
		};
		
		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 消息体的总长度来填充,包含头信息在内,这个是netlink核心需要的信息。
			
			nlmsg_type 可以被应用程序所用,它对于netlink核心来说是一个透明的值。因此大部分情况下设置为 0
			
			nlmsg_flags 用来该对消息体进行另外的控制,会被netlink核心代码读取并更新。 用于设置消息标志,如下所示,实际应用可设为0;
				#define NLM_F_REQUEST 0x01
				#define NLM_F_MULTI 0x02
				#define NLM_F_ACK 0x04
				#define NLM_F_ECHO 0x08
				#define NLM_F_DUMP_INTR 0x10
				#define NLM_F_DUMP_FILTERED 0x20
				#define NLM_F_ROOT 0x100
				#define NLM_F_MATCH 0x200
				#define NLM_F_ATOMIC 0x400
				#define NLM_F_DUMP (NLM_F_ROOT | NLM_F_MATCH)
				#define NLM_F_REPLACE 0x100
				#define NLM_F_EXCL 0x200
				#define NLM_F_CREATE 0x400
				#define NLM_F_APPEND 0x800		
			nlmsg_seq 和 nlmsg_pid 同样对于netlink核心部分来说是透明的,应用程序用它们来跟踪消息。字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。
				标志 NLM_F_REQUEST 用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。
				
				标志 NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。
					 
				宏NL NLM_F_ACK 表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。
					 
				标志 NLM_F_ECHO 表示该消息是相关的一个包的回传。
					 
				标志 NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。
					 
				标志  NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。
					 
				标志  NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。
					 
				标志  NLM_F_DUMP 未实现。
					 
				标志  NLM_F_REPLACE 用于取代在数据表中的现有条目。
					 
				标志  NLM_F_EXCL 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。
					 
				标志  NLM_F_CREATE 指示应当在指定的表中创建一个条目。
					 
				标志  NLM_F_APPEND 指示在表末尾添加新的条目。

				内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),				
		*/

		struct iovec
		{
			void __user *iov_base;	/* BSD uses caddr_t (1003.1g requires void *) */
			__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
		};
		/*
			结构 struct iovec 用于把多个消息通过一次系统调用来发送
		*/
		
		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 */
		};
		
		需要如下设置:

		struct nlmsghdr *nlh = NULL;
        nlh->nlmsg_len = NLMSG_SPACE(len);
        nlh->nlmsg_pid = TEST_PID;
        nlh->nlmsg_flags = 0;

		struct iovec iov;
        iov.iov_base = (void *)nlh;
        iov.iov_len = nlh->nlmsg_len;
		
		struct sockaddr_nl dest_addr;
        memset(&dest_addr, 0, sizeof(struct sockaddr_nl));
        dest_addr.nl_family = AF_NETLINK;
        dest_addr.nl_pid = pid;
        dest_addr.nl_groups = group;	
		
		struct msghdr msg;
		memset(&msg, 0, sizeof(struct msghdr));
        msg.msg_name = (void *)&dest_addr;
        msg.msg_namelen = sizeof(struct sockaddr_nl);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;	

		struct nlmsghdr 为 netlink socket 自己的消息头,这用于多路复用和多路分解 netlink 定义的所有协议类型以及其它一些控制,
		
		netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,因此它也被称为 netlink 控制块。因此,应用在发送 netlink 消息时必须提供该消息头。
				
		在完成以上步骤后,消息就可以通过下面语句直接发送:
		
		sendmsg(fd, &msg, 0);
		
	D. recvmsg()
	
		应用接收消息时需要首先分配一个足够大的缓存来保存消息头以及消息的数据部分,然后填充消息头,添完后就可以直接调用函数 recvmsg() 来接收

		struct sockaddr_nl source_addr;
		memset(&source_addr, 0, sizeof(struct sockaddr_nl));

		struct nlmsghdr *nlh = NULL;
		nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
		
		struct iovec iov;
        iov.iov_base = (void *)nlh;
        iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);	
		
		struct msghdr msg;
        memset(&msg, 0, sizeof(struct msghdr));
        msg.msg_name = (void *)&source_addr;
        msg.msg_namelen = sizeof(struct sockaddr_nl);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;		
		
		recvmsg(sock_fd, &msg, 0);
	
		注意:fd为socket调用打开的netlink socket描述符。
		
		在消息接收后,nlh 指向接收到的消息的消息头,source_addr 保存了接收到的消息的目标地址,宏 NLMSG_DATA(nlh) 返回指向消息的数据部分的指针。
		
		memcpy(message, (unsigned char *)NLMSG_DATA(nlh), *len); //这样就可以用printf把消息打印出来了
		
		bionic/libc/kernel/uapi/linux/netlink.h 中定义了一些方便对消息进行处理的宏,这些宏包括:
		
		#define NLMSG_ALIGNTO 4 
		#define NLMSG_ALIGN(len)    ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值。
		
		#define NLMSG_LENGTH(len)   ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存。
		
		#define NLMSG_SPACE(len)   NLMSG_ALIGN(NLMSG_LENGTH(len))NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存。
		
		#define NLMSG_DATA(nlh)   ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏。
		
		#define NLMSG_NEXT(nlh,len)  ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
		(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用。
		
		#define NLMSG_OK(nlh,len)   ((len) >= (int)sizeof(struct nlmsghdr) && \
		(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \ 
		(nlh)->nlmsg_len <= (len))NLMSG_OK(nlh,len)用于判断消息是否有len这么长。
		
		#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度。
		
	E. close()

		函数close用于关闭打开的netlink socket。	
		
		close(sock_fd);

5.2 内核态API

	netlink的内核实现在.c文件net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件net/netlink.h。
	
	内核使用netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协议类型,必须通过修改linux/netlink.h来实现,当然,
	
	目前的netlink实现已经包含了一个通用的协议类型 NETLINK_GENERIC 以方便用户使用,用户可以直接使用它而不必增加新的协议类型。
	
	前面讲到,为了增加新的netlink协议类型,用户仅需增加如下定义到net/netlink.h就可以:#include <net/netlink.h>  /*#include <net/sock.h>*/
	
	
	#define NETLINK_MYTEST 57 //Netlink协议类型只有32个,0~31为有效值,一般情况下0~21已经被系统使用,所以我们只能使用22~31的值,为了减少冲突,建议设置较大值。

	只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。

	在内核中,为了创建一个netlink socket用户需要调用如下函数:
	A. static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)	
		net: 是网络设备命名空间指针,一般直接填&init_net
		
		unit: 协议类型,可自定义,如 #define NETLINK_TEST 25
		
		cfg:配置结构,类型如下:
		
		/* optional Netlink kernel configuration parameters */
		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:组编号;
		
		input:是 netlink socket 在接受到消息时调用的回调函数指针,接收一个 sk_buff 结构,数据包含一个 nlmsghdr 协议头;
		
			函数input()会在发送进程执行sendmsg()时被调用,这样处理消息比较及时,但是,如果消息特别长时,
			
			这样处理将增加系统调用sendmsg()的执行时间,对于这种情况,可以定义一个内核线程专门负责消息接收,
			
			而函数input的工作只是唤醒该内核线程,这样sendmsg将很快返回。	
		
		return:返回一个sock结构,返回NULL表示创建失败;
		
		然后用户空间进程使用标准 Socket API 来创建套接字,将进程ID发送至内核空间,用户空间创建使用socket()创建套接字,该函数的原型如下:
	
		int socket(int domain, int type, int protocol);
	
		domain: 值为 PF_NETLINK,即Netlink使用协议族。
		
		protocol: 为Netlink提供的协议或者是用户自定义的协议,Netlink提供的协议包括 NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6和 NETLINK_IP6_FW。	
	
		当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过 struct sk_buff 来管理的, linux/netlink.h 中定义了一个宏:
	
		#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))
	
	
	
		来方便消息的地址设置。下面是一个消息地址设置的例子:
	
        NETLINK_CB(skb).portid = 0;
        NETLINK_CB(skb).dst_group = 0;
	
	
	
		portid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。
	
		在内核中,模块调用函数 netlink_unicast 来发送单播消息:
	
	B. int netlink_unicast(struct sock *ssk, struct sk_buff *skb,u32 portid, int nonblock)
	
		ssk: 函数 netlink_kernel_create() 返回的socket,
		
		skb: 存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,
			
			前面的宏 NETLINK_CB(skb) 就用于方便设置该控制块,
			
		portid: 为接收消息进程的pid,
		
		nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。
	
		内核模块或子系统也可以使用函数 netlink_broadcast 来发送广播消息:
	
	C. int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 portid,u32 group, gfp_t allocation)
	
		前面的三个参数与netlink_unicast相同,
		
		group:为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。
		
		allocation:为内核内存分配类型,一般地为 GFP_ATOMIC 或 GFP_KERNEL,GFP_ATOMIC 用于原子的上下文(即不可以睡眠),而 GFP_KERNEL 用于非原子上下文。
	
		接收数据时程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。然后使用标准函数接口 recvmsg() 来接收netlink消息	
	
	D. void	(*input)(struct sk_buff *__skb);//回调函数,即接收用户空间的信息。
	
		需要如下设置:
			
			struct sk_buff *skb;//这是保存netlink信息的结构体;
			skb = skb_get(__skb);
			
			struct nlmsghdr *nlh;
			nlh = nlmsg_hdr(skb);
			
			memcpy(str, NLMSG_DATA(nlh), sizeof(str));//这样就可以获取用户空间的数据了。
			
	
		在内核中使用函数 sock_release 来释放函数 netlink_kernel_create() 创建的netlink socket:
	
	E. void sock_release(struct socket * sock);
	
	
	
		注意函数 netlink_kernel_create() 返回的类型为struct sock,因此函数sock_releas e应该这种调用:
		
		sock_release(sk->sk_socket);
	
	
	
		sk为函数netlink_kernel_create()的返回值。

6. Netlink 实例应用

调试平台:MT6779

调试程序:分为内核模块和用户空间程序两部分,当内核模块被加载后,运行用户空间程序,由用户空间发起Netlink会话,和内核模块进行数据交换。

6.1 应用层代码

#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>
 
#define NETLINK_TEST    (25)
#define MAX_PAYLOAD     (1024)
#define TEST_PID        (100)
 
int netlink_create_socket(void)
{
        //create a socket
        return socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
}
 
int netlink_bind(int sock_fd)
{
        struct sockaddr_nl addr;
 
        memset(&addr, 0, sizeof(struct sockaddr_nl));
        addr.nl_family = AF_NETLINK;
        addr.nl_pid = TEST_PID;
        addr.nl_groups = 0;
 
        return bind(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_nl));
}
 
int
netlink_send_message(int sock_fd, const unsigned char *message, int len,
                                        unsigned int pid, unsigned int group)
{
        struct nlmsghdr *nlh = NULL;
        struct sockaddr_nl dest_addr;
        struct iovec iov;
        struct msghdr msg;
 
        if( !message ) {
                return -1;
        }
 
        //create message
        nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(len));
        if( !nlh ) {
                perror("malloc");
                return -2;
        }
        nlh->nlmsg_len = NLMSG_SPACE(len);
        nlh->nlmsg_pid = TEST_PID;
        nlh->nlmsg_flags = 0;
        memcpy(NLMSG_DATA(nlh), message, len);
 
        iov.iov_base = (void *)nlh;
        iov.iov_len = nlh->nlmsg_len;
        memset(&dest_addr, 0, sizeof(struct sockaddr_nl));
        dest_addr.nl_family = AF_NETLINK;
        dest_addr.nl_pid = pid;
        dest_addr.nl_groups = group;
 
        memset(&msg, 0, sizeof(struct msghdr));
        msg.msg_name = (void *)&dest_addr;
        msg.msg_namelen = sizeof(struct sockaddr_nl);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
 
        //send message
        if( sendmsg(sock_fd, &msg, 0) < 0 )
        {
                printf("send error!\n");
                free(nlh);
                return -3;
        }
 
        free(nlh);
        return 0;
}
 
int
netlink_recv_message(int sock_fd, unsigned char *message, int *len)
{
        struct nlmsghdr *nlh = NULL;
        struct sockaddr_nl source_addr;
        struct iovec iov;
        struct msghdr msg;
 
        if( !message || !len ) {
                return -1;
        }
 
        //create message
        nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
        if( !nlh ) {
                perror("malloc");
                return -2;
        }
        iov.iov_base = (void *)nlh;
        iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
        memset(&source_addr, 0, sizeof(struct sockaddr_nl));
        memset(&msg, 0, sizeof(struct msghdr));
        msg.msg_name = (void *)&source_addr;
        msg.msg_namelen = sizeof(struct sockaddr_nl);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
 
        if ( recvmsg(sock_fd, &msg, 0) < 0 ) {
                printf("recvmsg error!\n");
                return -3;
        }
        *len = nlh->nlmsg_len - NLMSG_SPACE(0);
        memcpy(message, (unsigned char *)NLMSG_DATA(nlh), *len);
 
        free(nlh);
        return 0;
}
NETLINK_ROUTE 
int
main(int argc, char **argv)
{
        int sock_fd;
        char buf[MAX_PAYLOAD];
        int len;
 
        if( argc < 2) {
                printf("enter message!\n");
                exit(EXIT_FAILURE);
        }
 
        sock_fd = netlink_create_socket();
        if(sock_fd == -1) {
                printf("socket error!\n");
                return -1;
        }
 
        if( netlink_bind(sock_fd) < 0 ) {
                perror("bind");
                close(sock_fd);
                exit(EXIT_FAILURE);
        }
 
        netlink_send_message(sock_fd, argv[1], strlen(argv[1]) + 1, 0, 0);
        if( netlink_recv_message(sock_fd, buf, &len) == 0 ) {
                printf("recv:%s len:%d\n", buf, len);
        }
 
        close(sock_fd);
        return 0;
}

6.2 内核代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/stat.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
 
#include <net/netlink.h>
#include <net/sock.h>
 
#define NETLINK_TEST (25)
 
static dev_t devId;
static struct class *cls = NULL;
struct sock *nl_sk = NULL;
 netlink_broadcast
static void
netlink_test_cleanup(void)
{
        netlink_kernel_release(nl_sk);
        device_destroy(cls, devId);
        class_destroy(cls);
        unregister_chrdev_region(devId, 1);
}
 silfp_netlink_send
static void
netlink_send(int pid, uint8_t *message, int len)
{
        struct sk_buff *skb_1;
        struct nlmsghdr *nlh;
 
        if(!message || !nl_sk) {
                return;
        }
 
        skb_1 = alloc_skb(NLMSG_SPACE(len), GFP_KERNEL);
        if( !skb_1 ) {
                printk(KERN_ERR "alloc_skb error!\n");
        }
 
        nlh = nlmsg_put(skb_1, 0, 0, 0, len, 0);
        NETLINK_CB(skb_1).portid = 0;
        NETLINK_CB(skb_1).dst_group = 0;
        memcpy(NLMSG_DATA(nlh), message, len);
        netlink_unicast(nl_sk, skb_1, pid, MSG_DONTWAIT);
}
 recvmsg
static void
netlink_input(struct sk_buff *__skb)
{
        struct sk_buff *skb;
        char str[100];
        struct nlmsghdr *nlh;
 
        if( !__skb ) {
                return;
        }
 
        skb = skb_get(__skb);
        if( skb->len < NLMSG_SPACE(0)) {
                return;
        }
 
        nlh = nlmsg_hdr(skb);
        memset(str, 0, sizeof(str));
        memcpy(str, NLMSG_DATA(nlh), sizeof(str));
        printk(KERN_INFO "receive message (pid:%d):%s\n", nlh->nlmsg_pid, str);
        printk(KERN_INFO "space:%d\n", NLMSG_SPACE(0));
        printk(KERN_INFO "size:%d\n", nlh->nlmsg_len);
        netlink_send(nlh->nlmsg_pid, NLMSG_DATA(nlh), nlh->nlmsg_len - NLMSG_SPACE(0));
 
        return;
}
 
static __init int netlink_test_kernel_init(void)
{
        int result;
        struct netlink_kernel_cfg nkc;
 
        printk(KERN_WARNING"netlink init start!\n");
 
        //动态注册设备号
        if(( result = alloc_chrdev_region(&devId, 0, 1, "netlink_test_dev") ) != 0) {
                printk(KERN_WARNING "register dev id error:%d\n", result);
                goto err;
        } else {
                printk(KERN_WARNING "register dev id success!\n");
        }
        //动态创建设备节点
        cls = class_create(THIS_MODULE, "netlink_test_class");
        if(IS_ERR(cls)) {
                printk(KERN_WARNING "create class error!\n");
                goto err;
        }
        if(device_create(cls, NULL, devId, "", "hello%d", 0) == NULL) {
                printk(KERN_WARNING "create device error!\n");
                goto err;
        }
 
        //初始化netlink
        nkc.groups = 0;
        nkc.flags = 0;
        nkc.input = netlink_input;
        nkc.cb_mutex = NULL;
        nkc.bind = NULL;
        nkc.unbind = NULL;
        nkc.compare = NULL;
        nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &nkc);
        if( !nl_sk ) {
                printk(KERN_ERR "[netlink] create netlink socket error!\n");
                goto err;
        }
 
        printk(KERN_ALERT "netlink init success!\n");
        return 0;
err:
        netlink_test_cleanup();
        return -1;
}
 
static __exit void netlink_test_kernel_exit(void)
{
        hello_cleanup();
        printk(KERN_WARNING "netlink exit!\n");
}
 
module_init(netlink_test_kernel_init);
module_exit(netlink_test_kernel_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhangtiangui");

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人在路上……

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值