现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

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

2020-07-20 07:03 工业·编程 ⁄ 共 6482字 ⁄ 字号 暂无评论

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

wps1[4]

(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 !");

        }

    }

}

给我留言

留言无头像?