1 copy_to_user与copy_from_user
2 netlink
2.1 netlink简介
- netlink socket是一种linux特有的socket,用与实现用户进程与内核进程之间通信的一种特殊的进程之间通信方式(IPC),也是网络应用程序与内核通信的最常用的接口
- netlink是一种在内核和用户应用空间之间进行双向数据传输的好方式,用户态只需要使用标准的socket API接口就能够使用Netlink所提供的功能,当然,内核态还需要使用专门的内核API来使用Netlink
2.2 netlink API
- netlink报文格式:netlink的报文通常由消息头与消息体构成,其中消息头使用
struct nlmsghdr结构体保存,如下:
/* netlink消息报文消息头 */
/*
@nlmsg_len:整个消息长度,包含netlink消息头本身,单位:字节
@nlmsg_type:消息类型,即是数据消息还是控制消息,其中有以下4种控制消息
NLMSG_NOOP:空消息,啥也不干
NLMSG_ERROR:指明该消息中含有一个错误
NLMSG_DONE:如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有 消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效
NLMSG_OVERRUN:暂时用不到
@nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI,一般很少用
@nlmsg_seq:序列号,很少用
@nlmsg_pid:消息发送方的PID进程号,如果发送方是内核,那么为0
*/
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 PID */
};
/* 得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGNTO 4
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* 计算数据部分长度为len时实际的消息长度,它一般用于分配消息缓存 */
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* 得到下一个消息的首地址,同时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))
/* 返回payload的长度 */
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
/* step1:建立socket套接字 */
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:创建套接字
参数:
@domain:地址族(Address Family),即IP地址类型,常用有:
AF_INET(IPV4类型),AF_INET6(IPV6类型)
AF_UNIX, AF_LOCAL(本地进程通信)
AF_NETLINK(内核与用户空间的通信,用于设备驱动)
AF_PACKET(原始套接字)
@type:套接字类型,常用有:
SOCK_STREAM(流式套接字,对应TCP)
SOCK_DGRAM(数据报套接字,对应UDP)
SOCK_RAW(原始套接字)
@protocol:TCP与UDP编程时候填0,原始套接字时候需填充,为netlink时候需填入netlink协议类型
返回值:成功返回一个文件描述符,失败返回-1
*/
int socket(int domain, int type, int protocol);
/* 一共支持32种消息类型,下述是linux系统所定义好的几种消息类型,剩下的可自己定义 */
#define NETLINK_ROUTE 0 /* Routing/device hook(路由) */
#define NETLINK_W1 1 /* 1-wire subsystem */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook(防火墙) */
#define NETLINK_INET_DIAG 4 /* INET 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
/***************************************************************************************************/
/* step2:绑定端口 */
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:将一本地地址与一套接口捆绑
参数:
@sockfd:通过socket函数获得的文件描述符
@addr:结构体地址,基于Internet通信时候填sockaddr_in类型结构体,需要强制转换为sockaddr类型
@addrlen:结构体长度
返回值:成功返回0,失败返回-1
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 注意:在绑定的时候bind()函数的参数2是sockaddr类型参数,实际在编程中使用的却不是该格式的数据类型,所以在填充的时候需要强制转换为struct sockaddr类型。在网络编程中使用的是struct sockaddr_in类似的数据,而在与内核通信时候需要填充为struct sockaddr_nl类型,该类型详细介绍如下:*/
struct sockaddr_nl
{
sa_family_t nl_family; /*该字段总是为AF_NETLINK */
unsigned short nl_pad; /* 目前未用到,填充为0*/
__u32 nl_pid; /* 绑定者进程的进程号,通常设置为当前进程的进程号 */
__u32 nl_groups; /* 绑定者希望加入的多播组 */
};
/***************************************************************************************************/
/* setp3:发送消息 */
int sendmsg(struct socket *sock, struct msghdr *m, size_t total_len);
/* msghdr参数格式如下:
@msg_name:指向sockaddr的指针,在netlink中指向sockaddr_nl类型
@msg_namelen:msg_name的长度
@msg_iov:数据
*/
struct msghdr {
void * msg_name; /* Socket name */
int msg_namelen; /* Length of name */
struct iovec * msg_iov; /* Data blocks */
__kernel_size_t msg_iovlen; /* Number of blocks */
void * msg_control; /* Per protocol magic (eg BSD file descriptor passing) */
__kernel_size_t msg_controllen; /* Length of cmsg list */
unsigned msg_flags;
};
/* iovc结构体组成如下:
@iov_base:数据的基地址
@iov_len:数据的长度
*/
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) */
};
/************************************内核API*********************************************************/
/*
功能:创建netlink socket(老版本内核)
参数:
@net:一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等,默 认情况下都是使用 init_net 这个全局变量
@unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX
@groups:多播地址
@input:为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引 用,且只有此函数返回时,调用者的sendmsg才能返回
@cb_mutex:为访问数据时的互斥信号量
@module:一般为THIS_MODULE
*/
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);
/*
功能:创建netlink socket(新版本内核)
参数:
@net:一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等,默 认情况下都是使用 init_net这个全局变量
@unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX
@cfg:配置参数结构体
*/
struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg);
/* netlink_kernel_cfg结构体 */
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);
}
/*
功能:发送单播消息
参数:
@ssk:函数netlink_kernel_create()返回的socket
@skb:存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,宏
NETLINK_CB(skb)就用于方便设置该控制块
@pid:即接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为0
@nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有 接收缓存可利用定时睡眠。
*/
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock);
/*
功能:发送广播消息
参数:
@ssk:函数netlink_kernel_create()返回的socket
@skb:存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,宏
NETLINK_CB(skb)就用于方便设置该控制块
@pid:即接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为0
@group:接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播 组组ID的位相或,所有目标多播组对应掩码的OR操作的合值
@allocation:内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡 眠),而GFP_KERNEL用于非原子上下文。
*/
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation);
/*
功能:释放 netlink socket
*/
void netlink_kernel_release(struct sock *sk);
/**************************************工具函数******************************************************/
/* 从sk_buff->data获取struct nlmsghdr(消息头)数据 */
static inline struct nlmsghdr *nlmsg_hdr(const struct sk_buff *skb)
{
return (struct nlmsghdr *)skb->data;
}
/* 创建len大小的struct sk_buff */
static inline struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)
{
return alloc_skb(nlmsg_total_size(payload), flags);
}
/* 将一个新的netlink消息加入到skb中。如果skb无法存放消息则返回NULL */
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
int type, int payload, int flags);
/* 释放nlmsg_new()创建的skb */
static inline void nlmsg_free(struct sk_buff *skb)--------------------------------释放nlmsg_new()创建的skb。
{
kfree_skb(skb);
}
/* 根据nlmsghdr指针获取对应的payload */
static inline void *nlmsg_data(const struct nlmsghdr *nlh)
{
return (unsigned char *) nlh + NLMSG_HDRLEN;
}
/* 获取消息流中下一个netlink消息 */
static inline struct nlmsghdr *nlmsg_next(const struct nlmsghdr *nlh, int *remaining);
/* */
static inline void nlmsg_end(struct sk_buff *skb, struct nlmsghdr *nlh);
2.3 netlink使用流程分析
2.3.1 用户层分析
- 首先使用netlink与用户交互数据需要遵守内核所规定的格式
- 跟UDP网络编程一样,我们在发送消息的时候需要先建立socket连接,在建立socket连接的时候我们需要指定地址族为
AF_NETLINK类型,专门用于用户与内核空间之间进行数据交互;之后需要将套接字类型设置为原始套接字SOCK_RAW其专门用于进程间通信;最后的消息类型直接填写我们自己定义的或者系统定义好的消息类型即可,消息种类可参考2.2
- 在建立好套接字过后我们需要绑定端口,其中参数二的类型为
sockaddr类型,但是我们实际的类型却是sockaddr_nl类型,在此结构体中我们需要指明我们的地址族,仍然是AF_NETLINK类型;之后便是设置端口号,注意,虽然代码里面写的是pid,但此pid并非指的是进程号,相比于进程号,它更像是一个地址,在bind函数中此处nl_pid就是数据源地址,即消息的发送者
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_nl
{
sa_family_t nl_family; /*该字段总是为AF_NETLINK */
unsigned short nl_pad; /* 目前未用到,填充为0*/
__u32 nl_pid; /* 消息地址,通常设置为当前进程的进程号,0则表示是内核 */
__u32 nl_groups; /* 绑定者希望加入的多播组 */
};
- 绑定完成之后就需要我们去填写消息接收方的信息,同样使用
sockaddr_nl结构体去保存消息接收者的信息,示例中的消息接收方是内核,所以nl_pid应该写为0
- 收发双方信息确定之后就需要我们去发送消息了,netlink整个数据包由消息头与消息体所组成,消息头使用
nlmsghdr封装起来,在此处我们需要使用NLMSG_ALIGN宏去计算我们所申请的用来保存消息头的空间大小
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 PID */
};
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
- 有了保存消息头的空间之后就需要我们去完善消息头内容了,首先我们需要在
nlmsg_len处指明消息的大小,此处还是使用NLMSG_ALIGN宏来计算;之后在nlmsg_pid处填上发送方的地址即可,这里就有人会决定奇怪了,之前不是在sockaddr_nl处指明了消息发送方的地址了吗,为啥还要指定?因为此处不像网络编程,在网络编程里面系统会把发送方的信息给直接写入报文里面去,而此处需要我们手动写入发信方的地址,这样在接收方接收到这条消息之后才可以从消息中解析出发信人的地址。就好比我们去邮局寄信,虽然邮局(操作系统)知道你邮寄了一封信件,但是你还是得在信上写下你的姓名,为的就是告诉收信人这是你发的消息。之后再使用NLMSG_DATA来找到我们写入消息的位置进而去向这个位置写入消息
/* 取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
- 至此,消息准备完毕,之后就需要寻求载体将准备好的消息发送出去,这里我们先使用
sendmsg函数去发送消息,此处需要将上面准备的接收方信息和消息头数据填入msghdr结构体中
int sendmsg(struct socket *sock, struct msghdr *m, size_t total_len);
/* msghdr参数格式如下:
@msg_name:指向sockaddr的指针,在netlink中指向sockaddr_nl类型
@msg_namelen:msg_name的长度
@msg_iov:数据
*/
struct msghdr {
/* 指向socket地址结构 */
void * msg_name; /* Socket name */
/* 地址结构长度 */
int msg_namelen; /* Length of name */
/* 数据 */
struct iovec * msg_iov; /* Data blocks */
__kernel_size_t msg_iovlen; /* Number of blocks */
void * msg_control; /* Per protocol magic (eg BSD file descriptor passing) */
__kernel_size_t msg_controllen; /* Length of cmsg list */
unsigned msg_flags;
};
/* iovc结构体组成如下:
@iov_base:数据的基地址
@iov_len:数据的长度
*/
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) */
};
2.3.2 内核层分析
- 内核端不用多说,首先就是进入模块初始化函数里面,在里面创建一个内核版本的netlink socket,其中我们需要在参数二处指定我们所要收发的消息类型,此处应该与应用层的类型保持一致;而且我们需要在参数三中配置相关参数,其中最重要的就是消息回调函数,当内核收到消息后会进入消息回调函数处理消息,只有当这个函数处理完消息之后,应用层的
sendmsg函数才会返回
struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg);
void (*input)(struct sk_buff *skb);
- 其中应用层的消息(消息头)被保存在了
sk_buff->data结构体中,我们可通过函数nlmsg_hdr将消息头取出
- 取出消息头之后可以使用
NLMSG_DATA宏将用户实际发送的负载信息取出来
- 至此,用户数据已经被内核拿到,此刻如何给用户层一个反馈呢?
- 其实又需要反过来,首先准备我们的负载信息(内核-->用户空间),之后使用
nlmsg_new函数去申请一片sk_buff类型的数据区,
2.4 netlink使用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024
#define NETLINK_TEST 25
int main(int argc, char *argv[])
{
int sock_fd;
int ret;
struct nlmsghdr *nlh;
struct sockaddr_nl send_addr, recv_addr;
struct iovec iov;
struct msghdr msg;
/* step1:创建一个socket套接字 */
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if (sock_fd == -1) {
printf("fail to create socket\n");
return -1;
}
/* 填充绑定步骤所需要的数据,地址族,消息源地址,多播组等 */
memset(&send_addr, 0x00, sizeof(send_addr));
send_addr.nl_family = AF_NETLINK;
send_addr.nl_pid = 100;
send_addr.nl_groups = 0;
/* step2:绑定,与创建的套接字绑定,发信人信息准备完毕 */
ret = bind(sock_fd, (struct sockaddr*)&send_addr, sizeof(send_addr));
if (ret < 0) {
printf("fail to bind\n");
close(sock_fd);
return -1;
}
/* 填写消息接收者的信息,收信人信息准备完毕 */
memset(&recv_addr, 0x00, sizeof(recv_addr));
recv_addr.nl_family = AF_NETLINK;
recv_addr.nl_pid = 0;
recv_addr.nl_groups = 0;
/* 申请一个空间来存储消息头的信息,申请了一张信纸 */
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if (!nlh) {
printf("failed to malloc space\n");
close(sock_fd);
return -1;
}
/* 填写消息头信息,在信纸上写信 */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = 100; //填写发送方的地址
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh), "hello kernel\n"); //填写发送的内容
/* 将消息头数据再次封装 */
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
/* 填写接收方信息 */
memset(&msg, 0x00, sizeof(msg));
msg.msg_name = (void *)&recv_addr;
msg.msg_namelen = sizeof(recv_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
/* 发送消息 */
ret = sendmsg(sock_fd, &msg, 0);
if (ret == -1) {
printf("failed to sendmsg\n");
}
/* 清空消息头 */
memset(nlh, 0x00, NLMSG_SPACE(MAX_PAYLOAD));
while (1) {
/* 接收消息 */
ret = recvmsg(sock_fd, &msg, 0);
if (ret < 0) {
printf("failed to recvmsg\n");
}
/* 取出收到的消息 */
printf("APP recv msg--->%s\n", (char *)NLMSG_DATA(nlh));
}
close(sock_fd);
free(nlh);
nlh = NULL;
return 0;
}
#include <linux/module.h>
#include <linux/init.h>
#include <net/netlink.h>
#include <net/sock.h>
#define NETLINK_TEST 25
#define USER_PORT 100
int netlink_count = 0;
/* 发送给用户层的消息 */
char netlink_kmsg[30] = {0};
struct sock *nl_sock = NULL;
int send_to_user(char *buff, int len)
{
int ret;
/* 保存消息头 */
struct nlmsghdr *nlh = NULL;
/* 保存向用户发送的消息 */
struct sk_buff *nl_skb;
/* 申请一片sk_buff类型的数据区 */
nl_skb = nlmsg_new(len, GFP_ATOMIC);
if (!nl_skb) {
printk("sk_buff malloc failed\n");
return -1;
}
/* 将消息头数据作为负载加入到sk_buff中 */
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
if (!nlh) {
printk("failed to nlmsg_put\n");
nlmsg_free(nl_skb);
return -1;
}
/* 在消息头中找到负载区,将负载消息加入到负载区 */
memcpy(nlmsg_data(nlh), buff, len);
/* 发送单播消息给用户层 */
ret = netlink_unicast(nl_sock, nl_skb, USER_PORT, MSG_DONTWAIT);
return ret;
}
static void netlink_rcv_msg(struct sk_buff *skb)
{
char *recv_msg = NULL;
/* 保存消息头 */
struct nlmsghdr *nlh = NULL;
if (skb->len >= NLMSG_SPACE(0)) {
netlink_count++;
/* 封装给用户层发送的消息 */
snprintf(netlink_kmsg, sizeof(netlink_kmsg), "recv %d messages from user\n", netlink_count);
/* 取出消息头 */
nlh = nlmsg_hdr(skb);
/* 取出消息头中的负载信息(真实信息) */
recv_msg = NLMSG_DATA(nlh);
if (recv_msg) {
printk(KERN_INFO "recv from user:%s\n", recv_msg);
/* 发送反馈信息给用户层 */
send_to_user(netlink_kmsg, strlen(netlink_kmsg));
}
}
}
/* 填写netlink socket的配置参数,此处指定了对应的消息处理函数 */
struct netlink_kernel_cfg cfg = {
.input = netlink_rcv_msg,
};
static int __init netlink_init(void)
{
/* 创建内核netlink socket */
nl_sock = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if (nl_sock == NULL) {
printk(KERN_ERR "fail to create kernel netlink\n");
return -1;
}
printk("create kernel netlink success\n");
return 0;
}
static void __exit netlink_exit(void)
{
if (nl_sock != NULL) {
netlink_kernel_release(nl_sock);
nl_sock = NULL;
}
printk("kernel netlink release success\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_AUTHOR("hq");
MODULE_LICENSE("GPL");