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会话过程如下:

Linux中Netlink实现热插拔监控——内核与用户空间通信-LMLPHP

(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卡,如下结果是拔插事件。

Linux中Netlink实现热插拔监控——内核与用户空间通信-LMLPHP

创建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 !");
		}
	}
 }
12-06 13:42