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