1、什么是NetLink?
它是一种特殊的 socket,它是Linux所特有的,由于传送的消息是暂存在socket接收缓存中,并不被接收者立即处理,所以netlink是一种异步通信机制。 系统调用和 ioctl 则是同步通信机制。Netlink是面向数据包的服务,为内核与用户层搭建了一个高速通道。
用户空间进程可以通过标准socket API来实现消息的发送、接收。进程间通信的方式有:管道(Pipe)及命名管道(Named Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared Memory),信号量(Semaphore),套接字(Socket)。
为了完成内核空间与用户空间通信,Linux提供了基于socket的Netlink通信机制,可以实现内核与用户空间数据的及时交换。
2、在Linux3.0的内核版本中定义了下面的21个用于Netlink通信的宏
在include/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 /* 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
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_RDMA 20
#define MAX_LINKS 32
3、建立Netlink会话过程如下:
(1)首先通过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);
其中net参数是网络设备命名空间指针,input函数是netlink socket在接受到消息时调用的回调函数指针,module默认为THIS_MODULE.
(2)用户空间进程使用标准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。
(3)接着使用bind函数绑定。Netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联。完成绑定,内核空间接收到用户进程ID之后便可以进行通讯。
(4)用户空间进程发送数据使用标准socket API中sendmsg()函数完成,使用时需添加struct msghdr消息和nlmsghdr消息头。一个netlink消息体由nlmsghdr和消息的payload部分组成,输入消息后,内核会进入nlmsghdr指向的缓冲区。
4、实例:热插拔监听
内核中使用uevent事件通知用户空间,uevent首先在内核中调用netlink_kernel_create()函数创建一个socket套接字,该函数原型在netlink.h有定义,其类型是表示往用户空间发送消息的NETLINK_KOBJECT_UEVENT,groups=1,由于uevent只往用户空间发送消息而不接受,因此其输入回调函数input和cb_mutex都设置为NULL。
当有事件发生的时候,调用 kobject_uevent()函数,实际上最终是调用
netlink_broadcast_filtered(uevent_sock, skb , 0, 1, GFP_KERNEL , kobj_bcast_filter, kobj);完成广播任务。
用户空间程序只需要创建一个socket描述符,将描述符绑定到接收地址,就可以实现热拔插事件的监听了。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <asm/types.h>
//该头文件需要放在netlink.h前面防止编译出现__kernel_sa_family未定义
#include <sys/socket.h>
#include <linux/netlink.h>
void MonitorNetlinkUevent()
{
int sockfd;
struct sockaddr_nl sa;
int len;
char buf[4096];
struct iovec iov;
struct msghdr msg;
int i;
memset(&sa,0,sizeof(sa));
sa.nl_family=AF_NETLINK;
sa.nl_groups=NETLINK_KOBJECT_UEVENT;
sa.nl_pid = 0;//getpid(); both is ok
memset(&msg,0,sizeof(msg));
iov.iov_base=(void *)buf;
iov.iov_len=sizeof(buf);
msg.msg_name=(void *)&sa;
msg.msg_namelen=sizeof(sa);
msg.msg_iov=&iov;
msg.msg_iovlen=1;
sockfd=socket(AF_NETLINK,SOCK_RAW,NETLINK_KOBJECT_UEVENT);
if(sockfd==-1)
printf("socket creating failed:%s\n",strerror(errno));
if(bind(sockfd,(struct sockaddr *)&sa,sizeof(sa))==-1)
printf("bind error:%s\n",strerror(errno));
len=recvmsg(sockfd,&msg,0);
if(len<0)
printf("receive error\n");
else if(len<32||len>sizeof(buf))
printf("invalid message");
for(i=0;i<len;i++)
if(*(buf+i)=='\0')
buf[i]='\n';
printf("received %d bytes\n%s\n",len,buf);
close(sockfd);
}
int main(int argc,char **argv)
{
printf("***********************start***********************\n");
MonitorNetlinkUevent();
printf("***********************ends************************\n");
return 0;
}
我们Cmake编译好程序,在设备上执行:开始从设备上拔掉SD卡,之后运行程序,再插入SD卡,就有拔插事件。
创建socket描述符的时候指定协议族为AF_NETLINK或者PF_NETLINK,套接字type选择SOCK_RAW或者SOCK_DGRAM,Netlink协议并不区分这两种类型,第三个参数协议填充NETLINK_KOBJECT_UEVENT表示接收内核uevent信息。
接着就绑定该文件描述符到sockadd_nl,注意该结构体nl_groups是接收掩码,取~0是将接收所有来自内核的消息,我们接收热拔插只需要填NETLINK_KOBJECT_UEVENT即可。接下来调用recvmsg开始接收内核消息,recvmsg函数需要我们填充message报头,包括指定接收缓存等工作。该函数会阻塞直到有热拔插事件产生。因此根据实际的运用来实现自己的代码。
Notice:补充内容
当初的demo只是验证这种实现机制是正确的,但是在具体的实际应用中,我们的程序从一开始启动创建一个线程,去接收每一次监控的结果,我发现每一次插拔,会有很多种消息,比如SD卡插入,还有块信息(一次操作一共4个消息),SD卡拔出,同样的情况;
//日志信息
count = 1
received 204 bytes
add@/devices/platform/jzmmc_v1.2.0/mmc_host/mmc0/mmc0:0001
ACTION=add
DEVPATH=/devices/platform/jzmmc_v1.2.0/mmc_host/mmc0/mmc0:0001
SUBSYSTEM=mmc //SD卡
MMC_TYPE=SD
MMC_NAME=00000
MODALIAS=mmc:block
SEQNUM=521
count = 2
received 102 bytes
add@/devices/virtual/bdi/179:0
ACTION=add
DEVPATH=/devices/virtual/bdi/179:0
SUBSYSTEM=bdi
SEQNUM=522
count = 3
received 244 bytes
add@/devices/platform/jzmmc_v1.2.0/mmc_host/mmc0/mmc0:0001/block/mmcblk0
ACTION=add
DEVPATH=/devices/platform/jzmmc_v1.2.0/mmc_host/mmc0/mmc0:0001/block/mmcblk0
SUBSYSTEM=block //块
MAJOR=179
MINOR=0
DEVNAME=mmcblk0
DEVTYPE=disk
NPARTS=1
SEQNUM=523
count = 4
received 270 bytes
add@/devices/platform/jzmmc_v1.2.0/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1
ACTION=add
DEVPATH=/devices/platform/jzmmc_v1.2.0/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1
SUBSYSTEM=block
MAJOR=179
MINOR=1
DEVNAME=mmcblk0p1
DEVTYPE=partition
PARTN=1
SEQNUM=524
因此我们需要详细解析接收到的信息!!!,recvmsg函数会阻塞,因此代码要注意!!!!
/*
线程里处理的事情,创建socket,绑定,销毁socket等都在创建线程、关闭线程时实现,
*/
while ((kSocketfd >= 0 )&&(kThreadisRunning >=0)) //
{
MessageLength = recvmsg(kSocketfd, &message, 0);
if (MessageLength > 0 )
{
/*
4 : pesae message
*/
for(int i=0;i<MessageLength;i++)
{
if(MeaasgeBuffer[i]=='\0') MeaasgeBuffer[i]='\n';
}
MeaasgeBuffer[MessageLength]='\0';
ParsingMessages(MeaasgeBuffer, MessageLength); //解析消息
}
else if (MessageLength < 0)
{
printf("receive error!\n");
}
else if (MessageLength<32 || MessageLength>sizeof(MeaasgeBuffer))
{
printf("invalid message !");
}
}
}