一,需求背景
相信大家都知道,嵌入式设备通常会具备一个基本的功能,那就是借助可移动块设备(U盘等)实现升级、日志等数据导出的功能,原因大概有这么几点:
1,嵌入式设备通常是没有界面操作的,无法在设备本体上人工触发升级或者将日志转移到其他地方
2,可以通过服务器远程升级或者将日志上传到服务器,但这终归是没有块设备升级或者导出来得方便,考虑网络不理想、升级程序包或者日志文件通常比较大,上传和下载都比较耗时。
基于以上两点,相信可以理解为什么嵌入式设备需要支持“块设备”升级和导出数据的功能了,那么要实现此功能,关键点有两个:
a, 当块设备插入时,我们得知道块设备接入了,并且将其挂载到一个可访问的目录下,
b, 当块设备拔出时,我们得知道块设备移除了,并且卸载之前所挂载的目录
换句话说,我们得检测块设备的热插拔事件,并进行相应的挂载和卸载操作,当然实现方式比较多,这里介绍一种比较优雅的方式,不仅仅是针对块设备,只要是外部硬件设备的插拔,比如USB、SCSI、串口等接口方式接入的设备,都可以检测其热插拔。
二、方案设计
linux下有一句话叫“一切皆文件”,对于硬件设备也不例外,外部硬件设备接入后,会在系统/dev目录下产生相应类型的设备文件,当外部设备移除后,该设备文件会被删除。比如块设备在接入后,在系统/dev目录下产生的设备文件是虚拟的“块设备文件”,不能直接进行读写访问,需要挂载到嵌入式设备本身的文件系统才能进行读写访问,如果是手柄、触摸屏幕、键盘、鼠标等输入设备接入,在系统中产生的是字符设备文件,那么是直接可以进行读写的,比如Android手机触摸屏在系统中就是一个“字符设备文件”,当手指触摸屏幕,产生硬件中断事件,内核接收并打包该事件,将其写入对应的字符设备文件,应用层通过读取该字符设备文件来获取各种触摸输入事件。
前面我们说了,硬件设备接入后,会在系统/dev目录下产生对应的设备文件,移除时,设备文件会被删除,那么这个文件是由谁创建和删除的呢?其实这个就是实现热插拔检测的关键,实际上硬件热插拔事件在linux系统中有一套上报机制,称为uevent机制,其大致流程如下:
现在我们来回答外部硬件设备在/dev目录下对应的设备文件是由谁创建的,实际上在linux系统启动时,会创建一个守护进程udevd,该进程运行在用户空间,通过监听内核发送的uevent来执行相应的热插拔动作响应,这个动作响应就是在/dev目录下创建或删除响应的设备文件,udevd守护进程还有一个功能,就是根据其在/etc目录下的uevent规则文件(比如/etc/udev/rules.d/50-udev.rules)进行匹配,比如规则文件内容有这么一行
ACTION=="add", SUBSYSTEM=="?*", ENV{MODALIAS}=="?*", RUN+="/sbin/modprobe $env{MODALIAS}"
那么当收到uevent的add(即硬件接入)事件后,shell就会自动加载运行在MODALIAS中定义的模块,那么我们可以在模块中实现对硬件设备的访问,但这种方式只适用于系统启动前,硬件设备已经接入了,不适合热插拔处理,而且不太优雅,需要更改配置文件,在Android系统下,是ueventd这个守护进程在接收uevent事件,源码位于system/core/init/ueventd.c,他读取的配置文件是ueventd.rc。
可能有人会问,内核如何将uevent发送到用户空间,其实这仅仅是个linux操作系统内部的数据传输的问题,这里采用了一种特殊的socket,linux下专有名词叫Netlink,Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。前面说的用户空间的守护进程udev内部实际上就是利用Netlink来实现uevent事件的接收的。
有了Netlink以及udev创建或者删除设备文件节点的支持,那么相信实现设备热插拔检测以及块设备的挂载和卸载也不是什么难事,基本算法分两步:
1,利用Netlink接收uevent事件
2,当接收到硬件插入事件时,将udev在/dev目录下创建的块设备文件挂载到嵌入式设备的某个目录下
当接收到硬件移除事件时,将之前设备挂载的目录卸载掉
三、编码设计
借鉴了linux内核以及Andorid部分源码,linux内核对uevent事件类型的定义在include/linux/kobject.h中,定义如下:
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
对应的事件定义如下:
static const char *kobject_actions[] = {
[KOBJ_ADD] = "add",
[KOBJ_REMOVE] = "remove",
[KOBJ_CHANGE] = "change",
[KOBJ_MOVE] = "move",
[KOBJ_ONLINE] = "online",
[KOBJ_OFFLINE] = "offline",
};
Android系统ueventd守护进程利用Netlink接收uevent事件的源码在system/core/init/devices.c中,如下:
#include
#include
#include
#include
#include
#include
#include
#define UEVENT_MSG_LEN 4096
struct luther_gliethttp {
const char *action;
const char *path;
const char *subsystem;
const char *firmware;
int major;
int minor;
};
static int open_luther_gliethttp_socket(void);
static void parse_event(const char *msg, struct luther_gliethttp *luther_gliethttp);
int main(int argc, char* argv[])
{
int device_fd = -1;
char msg[UEVENT_MSG_LEN+2];
int n;
device_fd = open_luther_gliethttp_socket();
printf("device_fd = %d\n", device_fd);
do {
while((n = recv(device_fd, msg, UEVENT_MSG_LEN, 0)) > 0) {
struct luther_gliethttp luther_gliethttp;
if(n == UEVENT_MSG_LEN)
continue;
msg[n] = '\0';
msg[n+1] = '\0';
parse_event(msg, &luther_gliethttp);
}
} while(1);
}
static int open_luther_gliethttp_socket(void)
{
struct sockaddr_nl addr;
int sz = 64*1024;
int s;
memset(&addr, 0, sizeof(addr));
addr.nl_family = AF_NETLINK;
addr.nl_pid = getpid();
addr.nl_groups = 0xffffffff;
s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if (s < 0)
return -1;
setsockopt(s, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz));
if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
close(s);
return -1;
}
return s;
}
static void parse_event(const char *msg, struct luther_gliethttp *luther_gliethttp)
{
luther_gliethttp->action = "";
luther_gliethttp->path = "";
luther_gliethttp->subsystem = "";
luther_gliethttp->firmware = "";
luther_gliethttp->major = -1;
luther_gliethttp->minor = -1;
printf("========================================================\n");
while (*msg) {
printf("%s\n", msg);
if (!strncmp(msg, "ACTION=", 7)) {
msg += 7;
luther_gliethttp->action = msg;
} else if (!strncmp(msg, "DEVPATH=", 8)) {
msg += 8;
luther_gliethttp->path = msg;
} else if (!strncmp(msg, "SUBSYSTEM=", 10)) {
msg += 10;
luther_gliethttp->subsystem = msg;
} else if (!strncmp(msg, "FIRMWARE=", 9)) {
msg += 9;
luther_gliethttp->firmware = msg;
} else if (!strncmp(msg, "MAJOR=", 6)) {
msg += 6;
luther_gliethttp->major = atoi(msg);
} else if (!strncmp(msg, "MINOR=", 6)) {
msg += 6;
luther_gliethttp->minor = atoi(msg);
}
while(*msg++)
;
}
printf("event { '%s', '%s', '%s', '%s', %d, %d }\n",
luther_gliethttp->action, luther_gliethttp->path, luther_gliethttp->subsystem,
luther_gliethttp->firmware, luther_gliethttp->major, luther_gliethttp->minor);
}
下面来实现我们自己的编码,首先要明确的是,我们只需要关心add和remove两种事件即可,对应硬件的插和拔,而且只需要检测块设备即可,这里由于项目需要,增加了对tty串口设备的检测,不需要可以去掉,总之,你想检测什么类型的设备,只需要增加响应的设备协议解析逻辑即可。
HotPlugObserver.h中定义对外使用的接口和数据接口,如下
/*************************************************
Author:lijuncheng
Emial:[email protected]
Date:2020-01-11
Description: this file mainly defines some interfaces about observing hardware hot plug and relative data structures
**************************************************/
#ifndef HOT_PLUG_OBSERVER_H
#define HOT_PLUG_OBSERVER_H
enum ObserveDeviceType
{
ObserveDeviceType_Block, ///< observe block devices , such as U disk , mobile disk etc
ObserveDeviceType_Tty, ///< observe tty devices , such as serial or fake terminal device etc
ObserveDeviceType_All ///< observe all devices above
};
enum DevType
{
DevType_Block, ///< block devices , such as U disk , mobile disk etc
DevType_Tty ///< tty devices , such as serial or fake terminal device etc
};
enum DevAction
{
DevAction_Add, ///< add device action , such as plugin a U disk
DevAction_Remove ///< remove device action, such as remove a U disk
};
/*****************************************************************************
function name : ObserveCallback
used for :callback the observed device information
input param :devType see enum DevType, devAction see enum DevAction ,devPath is device filse system access path , if device is block device , then devPath is the mounted path
ouput param :no
return value :no
******************************************************************************/
typedef void (*ObserveCallback)(const DevType devType,const DevAction devAction,const char * devPath);
#ifdef __cplusplus
extern "C" {
#endif
/*****************************************************************************
function name : initHotPlugObserver
used for :initialize hot plug observer to lisen devices' change
input param :no
ouput param :no
return value :no
******************************************************************************/
void initHotPlugObserver();
/*****************************************************************************
function name : unInitHotPlugObserver
used for :uninitialize hot plug observer to release some resources
input param :no
ouput param :no
return value :no
******************************************************************************/
void unInitHotPlugObserver();
/*****************************************************************************
function name : registerObserveCallback
used for :resister a callback function to observe device's change
input param :observeDeviceType see enum ObserveDeviceType, observeCallback is the callback function
ouput param :no
return value :no
******************************************************************************/
void registerObserveCallback(const ObserveDeviceType observeDeviceType,const ObserveCallback observeCallback);
/*****************************************************************************
function name : unregisterObserveCallback
used for :unresister a callback function ,then you will not receive device's change
input param :observeDeviceType see enum ObserveDeviceType, observeCallback is the callback function
ouput param :no
return value :no
******************************************************************************/
void unregisterObserveCallback(const ObserveDeviceType observeDeviceType, const ObserveCallback observeCallback);
#ifdef __cplusplus
}
#endif
#endif //HOT_PLUG_OBSERVER_H
HotPlugObserver.cpp 定义了相关实现,如下所示:
#include "HotPlugObserver.h"
#include
测试demo.cpp如下:
#include "HotPlugObserver.h"
#include
#include
void observeBlockDeviceHotPlugEventCallback(const DevType devType,const DevAction devAction,const char * devPath);
int main(int argc , char * argv[])
{
///< init hot plug observer
initHotPlugObserver();
///< register hot plug evet callback
registerObserveCallback(ObserveDeviceType_Block,observeBlockDeviceHotPlugEventCallback);
#if 0
///< unInit hot plug observer
unInitHotPlugObserver();
///< unregister hot plug evet callback
unregisterObserveCallback(ObserveDeviceType_Block,observeBlockDeviceHotPlugEventCallback);
#endif
///< suspend here to avoid main exit
pause();
return 0;
}
void observeBlockDeviceHotPlugEventCallback(const DevType devType,const DevAction devAction,const char * devPath)
{
printf(" observeBlockDeviceHotPlugEventCallback devType=%d devAction=%d devPath=%s ",devType,devAction,devPath);
if(devType == DevType_Block)
{
if(devAction == DevAction_Add)
{
///< here we can use devPath to do our business , such as export some data from device , upgrade our device etc;
}
}
}
四,调试运行
编译过程如下:
运行如下,为了确保挂载和目录创建成功,请使用有权限的用户来执行demo:
当我们插入U盘设备时,会打印很多信息,U盘检测和挂载过程如下
此时我们便可以通过访问/mnt/sdb1/来访问U盘了,比如读取U盘中的升级程序包进行升级,导出日志等文件,甚至执行U盘中携带的程序来处理我们的业务。
当U盘拔出时,会执行卸载过程如下:
五、总结
至此,linux下硬件设备的热插拔以及块设备的挂载卸载功能我们已经完成,在此基础上,我们可以拓展完成更多的硬件设备热插拔检测,进而完成更多的业务功能,希望可以帮到您!写得有问题的地方,还请路过的大神多多指点。