前一段时间,在开发一个驱动程序的过程中,需要在驱动程序与应用程序之间进行通信。其中驱动程序在接收到一个硬件中断之后通知应用程序进行相应的处理。为 解决此类问题,驱动程序提供了几种机制:(1)使用copy_to_user/copy_from_user方法,缺点是通信响应时间过长(2)使用信 号,但是限于字符设备(3)使用netlink。
在linux2.4之后引入了netlink机制,它将是Linux用户态与内核态交流的主要方法之一。netlink 的特点是对中断过程的支持,也就是说,可以在中断程序中直接调用netlink相关函数。它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线 程,而是通过另一个软中断调用用户事先指定的接收函数。netlink的通信过程如下:
下面分用户空间与内核空间2个部分讲述netlink的基本使用方法
1. 用户空间的程序
用户的应用程序使用标准套接字socket与内核空间进行通讯,标准socket API的函数, socket()、 bind()、sendmsg()、recvmsg() 和 close()很容易地应用到 netlink socket。
程序代码:
#define <include/linux/netlink.h>
struct u_packet_info
{
struct nlmsghdr hdr;
struct packet_info p_info;
};
struct u_packet_info info;
/*自定义消息首部,它仅包含了netlink的消息首部*/
struct msg_to_kernel
{
struct nlmsghdr hdr;
};
struct msg_to_kernel message;
static int skfd, kpeerlen, rcvlen;
struct sockaddr_nl local;
struct sockaddr_nl kpeer;
struct msg_to_kernel message;
skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
if(skfd < 0)
{
printf("can not create a netlink socket/n");
exit(0);
}
memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid(); /*设置pid为自己的pid值*/
local.nl_groups = 0;
/*绑定套接字*/
if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)
{
printf("bind() error/n");
return -1;
}
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;
memset(&message, 0, sizeof(message));
/*计算消息,因为这里只是发送一个请求消息,没有多余的数据,所以,数据长度为0*/
message.hdr.nlmsg_len = NLMSG_LENGTH(0);
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = USER_TYPE; /*设置自定义消息类型*/
message.hdr.nlmsg_pid = local.nl_pid; /*设置发送者的PID*/
/*发送一个请求*/
sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr*)&kpeer, sizeof(kpeer));
while(1)
{
kpeerlen = sizeof(struct sockaddr_nl);
/*接收内核空间返回的数据*/
rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info), 0, (struct sockaddr*)&kpeer, &kpeerlen);
/*处理接收到的数据*/
……
}
可以看到,应用程序流程与一般socket程序类似。
1) 调用socket创建套接字。
netlink对应的协议簇是 AF_NETLINK,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型,内核预定义的类型在文件include/linux/netlink.h中。
#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
用户自定义的协议类型可以在此文件中加入。
2) 调用bind,绑定协议地址
bind函数需要绑定协议地址,netlink的socket地址使用struct sockaddr_nl结构描述:
struct sockaddr_nl
{
sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
成员 nl_family为协议簇 AF_NETLINK,成员 nl_pad 当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。成员 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组。
3) 调用sendto向内核发送消息
一个重要的问题就是发内核发送的消息的组成,使用我们发送一个IP网络数据包的话,则数据包结构为“IP包头+IP数据”,同样地,netlink的消息结构是“netlink消息头部+数据”。netlink消息头部使用struct nlmsghdr结构来描述:
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用netlink提供的宏NLMSG_LENGTH来计算这个长度,仅需向NLMSG_LENGTH宏提供要发送的数据的长度,它会自动计算对齐后的总长度:
/*计算包含报头的数据报长度*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字节对齐*/
#define NLMSG_ALIGN(len) (((len)+NLMSG_ALIGNTO-1)&~(NLMSG_ALIGNTO-1) )
可以看到netlink提供很多宏,这些宏可以为我们编写netlink宏提供很大的方便。
字段 nlmsg_type 用于应用内部定义消息的类型,它对netlink内核实现是透明的,因此大部分情况下设置为0,字段nlmsg_flags用于设置消息标志,对于一般的使用,用户把它设置为0就可以,只是一些高级应用(如netfilter和路由daemon需要它进行一些复杂的操作),字段nlmsg_seq和nlmsg_pid用于应用追踪消息,前者表示顺序号,后者为消息来源进程ID。
4) 调用recvfrom
当发送完请求后,就可以调用recv函数簇从内核接收数据了,接收到的数据包含了netlink消息首部和要传输的数据。程序中定义了如下的数据结构用来进行通信:
/*接收的数据包含了netlink消息首部和自定义数据结构*/
struct u_packet_info
{
struct nlmsghdr hdr;
struct packet_info p_info;
};
2. 内核空间的程序
与应用程序内核,内核空间也主要完成三件工作:
1) 创建netlink套接字
API函数netlink_kernel_create用于创建一个netlink socket,同时,注册一个回调函数,用于接收处理用户空间的消息。
struct sock * netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
参数unit表示netlink协议类型,如NETLINK_GENERIC,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个 netlink socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。
2) 接收处理用户空间发送的数据
前面通过netlink_kernel_create注册的input函数对来自用户空间的数据进行处理。函数input()会在发送进程执行sendmsg()时被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处 理将增加系统调用sendmsg()的执行时间,对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input的工作只是唤醒该内核线程,这样 sendmsg将很快返回。
3) 发送数据至用户空间
在内核中,模块调用函数 netlink_unicast 来发送单播消息:
int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
参数sk为函数netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。
内核模块或子系统也可以使用函数netlink_broadcast来发送广播消息:
void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation);
内核程序代码:
static int __init init(void)
{
/*创建一个netlink socket,协议类型是自定义的NETLINK_GENERIC,kernel_reveive为接受处理函数*/
nlfd = netlink_kernel_create(NETLINK_GENERIC, kernel_receive);
if(!nlfd) /*创建失败*/
{
printk("can not create a netlink socket/n");
return -1;
}
}
/*程序在退出模块中释放netlink sockets*/
static void __exit fini(void)
{
if(nlfd)
sock_release(nlfd->socket); /*释放netlink socket*/
}
DECLARE_MUTEX(receive_sem); /*初始化信号量*/
static void kernel_receive(struct sock *sk, int len)
{
do
{
struct sk_buff *skb;
if(down_trylock(&receive_sem)) /*获取信号量*/
return;
/*从接收队列中取得skb,然后进行一些基本的长度的合法性校验*/
while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
{
struct nlmsghdr *nlh = NULL;
if(skb->len >= sizeof(struct nlmsghdr))
{
/*获取数据中的nlmsghdr 结构的报头*/
nlh = (struct nlmsghdr *)skb->data;
if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))
&& (skb->len >= nlh->nlmsg_len))
{
/*长度的全法性校验完成后,处理应用程序自定义消息类型,主要是对用户PID的保存,即为内核保存“把消息发送给谁”*/
if(nlh->nlmsg_type == USER_TYPE) /*请求*/
{
write_lock_bh(&user_proc.pid);
user_proc.pid = nlh->nlmsg_pid;
write_unlock_bh(&user_proc.pid);
}
}
}
kfree_skb(skb);
}
up(&receive_sem); /*返回信号量*/
}while(nlfd && nlfd->receive_queue.qlen);
}
/*一个中断处理程序,向应用程序发送数据*/
static irqreturn_t irq_handler(int irq, void *dev_id, struct pt_regs *regs)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;
/*计算消息总长:消息首部加上数据加度*/
size = NLMSG_SPACE(sizeof(*info));
/*分配一个新的套接字缓存*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;
/*初始化一个netlink消息首部*/
nlh = NLMSG_PUT(skb, 0, 0, 0, size-sizeof(*nlh));
/*跳过消息首部,指向数据区*/
packet = NLMSG_DATA(nlh);
/*初始化数据区*/
memset(packet, 0, sizeof(struct packet_info));
/*填充待发送的数据*/
.......
/*计算skb两次长度之差,即netlink的长度总和*/
nlh->nlmsg_len = skb->tail - old_tail;
/*设置控制字段*/
NETLINK_CB(skb).dst_groups = 0;
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
return IRQ_HANDLED;
}