1. 程式人生 > >Linux中Netlink實現熱插拔監控——核心與使用者空間通訊

Linux中Netlink實現熱插拔監控——核心與使用者空間通訊

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
[email protected]/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
[email protected]/devices/virtual/bdi/179:0
ACTION=add
DEVPATH=/devices/virtual/bdi/179:0
SUBSYSTEM=bdi
SEQNUM=522

count = 3 
received 244 bytes
[email protected]/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
[email protected]/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 !");
		}
	}
 }