用户与内核交互--netlink

常见的进程见通信方式
常见的进程间通信方式主要有:pipe、有名管道、信号量、消息队列、共享内存、信号量以及套接字。但是上面的通信方式都不能实现用户与内核之间的交互。其原因见表1:
表1 常见通信方式

通信方式 无法实现内核与用户之间通信原因
管道(不包括命名管道) 局限于父子进程间的通信。
消息队列 在硬、软中断中无法无阻塞地接收数据。
信号量 无法介于内核态和用户态使用。
共享内存 需要信号量辅助,而信号量又无法使用。
套接字 在硬、软中断中无法无阻塞地接收数据。

那么系统中有哪些方式可以实现用户与内核之间的通信?主要通信方式有内核启动参数、模块参数、sysfs、procfs、sysctl、netlink、seq_file、系统调用、debug、relayfs等等。
内核启动参数:简单的说linux使用bootloader向开发者提供接口向内核启动传输一个参数从而控制内核启动行为。
模块参数:写过内核参数的对这个很了解,使用module_param可以在加载内核模块时输入用户参数。
sysfs和procfs都是获取内核行为的方式。主要区别是sysfs除了用于访问内核状态、计算机属性、进程状态信息之外还会对设备进行管理。
sysctl:用户可以通过sysctl配置内核参数。
seq_file: seq_file是在procfs的基础上发展起来的,主要弥补了profs中内核向用户输出大量数据时的多次读写以及速度慢的问题。
系统调用:是实现用户与内核通信的一种软中断方式。
bebug:是一个虚拟文件系统,主要用于内核开发者调试信息时向用户空间输出调试信息。
relayfs:relayfs是一个快速的转发(relay)数据的文件系统,它为那些需要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制。
上面说了这么多通信方式,那么他们之间有何区别。见表2:
表2 用户与内核通信方式对比

通信方式 通信方式类别
procfs、debug、sysfs、relayfs、 基于文件系统的通信方式
系统调用、sysctl 用户空间发起
内核启动参数、模块参数 用户设置内核参数
seq_file 内核向用户输出信息

从表2可以看出,用户态与内核态之间的通信要么基于文件系统,要么是用户与内核单向通信,有没有用户与内核实现双向通信的方式?Netlink
netlink基于sock,实现了用户与内核之间小量数据之间的交互。
那么如何建立netlink会话?见图1
用户与内核交互--netlink_第1张图片
在讲内核与用户通信之前先说下里面涉及的数据结构以及相关宏定义。
struct msghdr:该数据结构包含了数据发送时的一些参数。通过msg_name指明套接字名字,而msg_namelen则指明该地址结构的长度。msg_iov指定数据缓冲区数组,而msg_iovlen指明了该数组的元素个数。

struct iovec:该结构用于将消息用于一次系统调用发送,为消息缓冲区。其中iov_base用于指向要发送的数据,而iov_len用于发送数据的长度。
用户与内核交互--netlink_第2张图片
struct nlmsghdr:描述了socket消息头。
用户与内核交互--netlink_第3张图片
struct sockaddr_nl:该结构体为socket的地址结构。字段nl_family为netlink套接字地址类型,一般nl_family默认为AF_NETLINK。nl_pad为填充字段,一般填充0.字段nl_pid为端口号。nl_groups为多播地址掩码。
用户与内核交互--netlink_第4张图片
还有几个常用的宏MLMSG_DATA、NLMSG_LENGTH、NLMSG_SPACE.
MLMSG_DATA宏用于取得消息数据部分的首地址。
NLMSG_LENGTH用于获得消息长度。
NLMSG_SPACE该宏用于返回不小于MAX_PAYLOAD且4字节对齐的最小长度值,一般用于向内存系统申请空间是指定所申请的内存字节数,和NLMSG_LENGTH(len)所不同的是,前者所申请的空间里不包含Netlink消息头部所占的字节数,后者是消息负载和消息头加起来的总长度。
既然用户与内核通信都需要编写代码,那么就分别从用户态与内核对流程进行解释。
首先从用户态来说。
1)创建套接字
创建套接字的方法是使用函数socket实现,其原型为int socket(int domain, int type, int protocol);其中字段domain用于指定创建套接字锁使用的协议簇,netlink使用的协议簇主要定义在linux/socket.h中。参数type为套接字类型;protocol为用户自定义的协议,也可以使用原有的协议。
2)将本地套接字与源地址绑定。
主要使用bind实现,其原型为 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 其中sockfd为创建套接字时的返回值;addr指定了sockfd将绑定到本地地址;addrlen为地址长度。
3)初始化msghdr
这里涉及初始化源地址、目的地址、msghdr。
a)源地址目的地址初始化:
struct sockaddr_nl src_addr, dest_addr;
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
memset(&dest_addr,0,sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;
b)在初始化msghdr之前需要将消息头nlmsghdr先进行初始化操作
struct nlmsghdr *nlh = NULL;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = 0;
c)将缓冲区向量iovec与消息进行绑定,指向消息头。
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
d)将要发送的消息与目的套接字绑定
struct msghdr msg;
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
4)调用sendmsg向内核发送消息,recvmsg从内核获取消息。
内核态代码流程:
内核态比较简单,主要在加载模块时调用netlink_kernel_create完成创建内核套接字服务。该函数原型如下:

struct sock * netlink_kernel_create(struct net *net, int unit, unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module)
主要使用了5个参数。
参数net默认为init_net,参数unit表示netlink协议类型,如NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引用,需要用户自己实现。参数cb_mutex一般设置为NULL,module设置为THIS_MODULE.
在卸载模块时,调用sock_release释放 netlink_kernel_create创建的套接字。
至此,一个完整的用户与内核通信就结束了。学要具体实现代码可以百度一堆。

你可能感兴趣的:(netlink)