固态存储设备固件升级方案
1. 前言
随着数字化时代的发展,数字数据的量越来越大,相应的数据存储的需求也越来越大,存储设备产业也是蓬勃发展。存储设备产业中,发展最为迅猛的则是固态存储(Solid State Storage,SSS)。数字化时代,海量的数据,需要海量的存储设备。可以说,固态存储设备是数字化时代最重要的基础设施。
为了解决发现的Bug,安全漏洞,或者为了提升性能,固态存储设备也有升级其固件的需求。固态存储设备种类繁多,有可以随身携带的U盘、TF卡,也有手机中的eMMC,UFS,还有电脑中的固态硬盘(SSD),更有各种云背后的分布式存储系统中大量使用的各种固态存储设备。不同应用场景,其使用方式也不同,那么相应的升级方式也有可能不同。本文尝试全面介绍一下不同应用场景下的固态存储设备的固件升级方案。
2. 固态存储设备
固态存储设备的主要构成为存储介质,控制芯片和一些外置电子元器件。固态存储设备的主要存储介质为非易失性存储器,其主要为NAND Flash。控制芯片的主要作用是连接NAND Flash和主机存储接口,并管理存储在NAND Flash上的数据。
主机存储接口不同,应用的场景也不同。其主要分为:
● USB接口,主要用于移动存储;
● SD接口,主要用于小型电子设备,如相机,监控设备等;
● eMMC接口,主要用于手机、平板和一些嵌入式设备,如智能电视,车机等;
● UFS接口,主要用于手机;
● SATA接口,主要用于个人电脑;
● PCIe接口,主要用于对性能有更高要求的企业级存储,如分布式存储,云存储等。
3. 升级方案
存储设备是否支持固件升级,需要两个方面的支持,存储设备固件算法支持升级,并且有升级工具。
3.1. 固件算法
存储设备升级成功,或者万一在升级过程中断电,都不能影响存储设备上之前存放的用户数据。升级算法设计要点:
● 用户数据的正确性,即L2P表(存储设备逻辑到NAND Flash物理地址映射表)不能发生改变。
○ FTL算法设计的时候,需要将L2P表等影响用户数据的算法数据结构和其他算法数据结构分块存储。因为升级固件,必须重新写入算法二进制文件和重新配置Boot信息,因为NAND Flash的块必须擦除了才能写,所以固件二进制文件和Boot信息必须和L2P表分块存储。
● 异常行为的安全性,即升级过程中,发生异常,重新上电时,能够恢复原有的状态。
○ 升级的操作过程是日志型的,每一步操作都有记录,只有最后升级成功,并且有CheckSum校验机制来保证操作的完整性。
○ 升级算法的设计,新旧算法都保留,每次升级成功之后,将新算法头中索引加1,这样每次启动后,检测到两个算法,并通过CheckSum校验算法的完整性,最后比较算法头中的索引值,启动索引较大的算法。
3.2. 升级流程
3.2.1. 移动存储设备
移动存储设备U盘/TF卡,基本都可以通过USB接口接入电脑,电脑的操作系统主要包括Windows和Linux。所以针对移动存储设备,主要考虑制作系统软件来应对此类升级。为了减少软件开发工作,建议使用跨平台方案,保证最大限度的复用代码。为了更好地操作底层API,并且有效率的开发软件,采用C++作为软件开发语言,选用Qt作为跨平台的开发框架,并且都采用g++编译器。无论是在Linux平台,还是在Windows平台,升级的基本流程基本是相同的。如下图:
抽象出Linux和Windows下不同点,统一抽象的接口,然后复用宏WIN32区分不同系统,主要包括:
- 设备的标识,Windows下以盘符(E:\,F:\等)作为标识,Linux下则以设备路径(/dev/sdb, /dev/sdc等)作为标识。
- 文件路径,Windows下以反斜杠\作为分隔符,Linux下则以斜杠/作为分隔符。
- 和设备通信,和USB设备通信,数据层是采用SCSI协议,传输层采用的USB协议。应用软件直接采用SCSI协议与设备进行通信即可。
a. Windows层的通信主要代码:
// Open device
char szLetter[] = "\\.\G:;
HANDLE hDev = CreateFile((LPCSTR)_devFile, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);// Transport CMD to device
if (!DeviceIoControl(hDev,IOCTL_SCSI_PASS_THROUGH,pt,sizeof(buf),buf,sizeof(buf),&bytes,NULL)) {printf("IOCTL failed %d\n", GetLastError());}// Close device
CloseHandle(hDev);
b. Linux下通信的主要代码:
// Open device
int fd = open("/dev/sde", O_RDWR);// Transport CMD
unsigned char buff[1024] = {0};
unsigned char inq_cmd[] = {WRITE_10, 00, 0, 0, 0, 0, 0, 0, 0x2, 0};
unsigned char sense[32] = {0};
struct sg_io_hdr io_hdr = {};
io_hdr.interface_id = 'S';
io_hdr.cmdp = inq_cmd;
io_hdr.cmd_len = sizeof(inq_cmd);
io_hdr.dxferp = buff;
io_hdr.dxfer_len = 32;
io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
io_hdr.sbp = sense;
io_hdr.mx_sb_len = sizeof(sense);
io_hdr.timeout = 5000;
ioctl(fd, SG_IO, &io_hdr);// Close device
close(fd);
3.2.2. eMMC/UFS
eMMC/UFS主要用于手机、平板和一些嵌入式设备,手机和平板基本是Andriod系统,嵌入式也多是Andriod和Linux。而Andriod的内核也基本上就是Linux内核了。Andriod因为安全机制,不允许应用程序直接与存储储备通信,所有的数据传输都是加密的,所以Andriod也不方便通过应用程序来进行升级固件。
无论是Andriod还是嵌入式Linux,都是通过u-boot来启动系统的。u-boot中都已经携带有eMMC/UFS的驱动代码,并且u-boot中都会初始化eMMC/UFS,然后读取存放在eMMC/UFS上的系统进行引导启动。所以在u-boot中进行固件升级,不仅可以绕开Andriod的安全机制,升级方式也可以与Linux统一。
eMMC自从协议4.0版之后,协议提出统一的固件更新规则FFU(Field Firmware Update,现场固件更新)。其基本流程为Host发送命令进入FFU模式,通过写命令将固件bin文件写入设备存储在NAND Flash相应位置,然后Host发送激活命令、或硬件复位或者断上电操作,就可以完成固件更新。
其协议流程为:
u-boot流程,修改完u-boot之后,编译成二进制文件,然后与Andriod文件一起推送到手机终端,等待用户升级系统过程中,先完成对eMMC/UFS的升级。
// 在mmc_inti之后执行如下流程
mmc_set_clock(mmc, mmc->tran_speed,MMC_CLK_ENABLE);
mmc_switch(mmc, FFU_MODE);
mmc_write(mmc, addr, buff, size);
mmc_switch(mmc, NORMAL_MODE);
mmc_power_cycle(mmc);
UFS协议继承了eMMC的FFU,其流程基本一样,只不过在激活新固件时,简化了eMMC原有的方式,只支持HW Reset或Power Cycle。
3.2.3. SATA SSD
SATA接口的SSD主要用于个人电脑。如果SSD作为电脑的从盘(非操作系统盘),则可以直接使用系统软件。如果SSD作为电脑的主盘(操作系统盘),由于操作系统的限制,无法直接与主盘通信。操作系统有MAC、Windows、Linux等,而且cpu内核也有x86/x86-64/Arm等,如果要编译系统软件,会有很多版本。有没有一种通用的方法兼容所有情况呢?
为了解决主盘无法直接升级,需要另外接入系统,让主盘作为从盘。
3.2.3.1. UEFI应用
当前电脑系统的启动都是通过UEFI来引导启动操作系统的,可以考虑编写UEFI应用程序,通过UEFI程序来完成对SSD固件的更新。
3.2.3.2. WinPE应用
现在安装系统,都是使用U盘来完成系统,先在U盘中安装一个Win PE的启动系统来引导安装操作系统。Win PE是一个简化版的Windows系统,可以运行基本的Windows应用程序。这种方式可以不用管电脑原来是什么操作系统,只需要针对不同CPU制作不同的Win PE启动盘即可。
● 升级工具:
SATA接口的SSD使用ATA协议来通信,并且兼容SCSI的通用命令。
可以通过SCSI的3种操作码,来配置3种ATA通信协议。
主要通信代码:
SCSI_PASS_THROUGH spt = {0};
spt.Length = sizeof(SCSI_PASS_THROUGH);
spt.TimeOutValue = 2;
spt.CdbLength = 12; // 16,32
spt.Cdb[0] = 0xA1; // 0x85,0x7F
spt.Cdb[1] = 3 << 1;
memcpy(&spt.Cdb[3], aptex.CurrentTaskFile, 8);
ret = DeviceIoControl(handle, IOCTL_SCSI_PASS_THROUGH,&spt, sizeof(SCSI_PASS_THROUGH), NULL, 0, &nRet, NULL);
也可以直接通过ATA协议来与设备通信,其主要代码:
ATA_PASS_THROUGH_EX aptex = {0};
aptex.Length = sizeof(ATA_PASS_THROUGH_EX);
aptex.TimeOutValue = 2;
aptex.CurrentTaskFile[6] = 0xEF;
aptex.CurrentTaskFile[0] = 0x05;
aptex.CurrentTaskFile[1] = 0x80;
ret = DeviceIoControl(handle, IOCTL_ATA_PASS_THROUGH, &aptex, sizeof(ATA_PASS_THROUGH_EX), NULL, 0, &nRet, NULL);
● WinPE系统盘
- 制作WinPE需要安装ADK,不同版本WinPE对应不同的ADK,下载合适的ADK版本安装。
- 以管理员身份启动“部署和映像工具环境” 。
- 运行“copype”以创建 Windows PE 文件的工作副本。
// 提取64位的WinPE资源文件
copype amd64 C:\WinPE_amd64
// 提取32位的WinPE资源文件
copype x86 C:\WinPE_x86 - 提取镜像中文件
- 在WinPE中添加固件更新工具。
把fw_update_tool.exe放在mout\Program Files目录。 - 启动WinPE之后自动执行升级工具。
wpeinit
cd …
cd “Program Files\FWupdateTool”
FWupdateTool.exe - 提交修改
将相关的修改提交到新的winpe.wim中,并卸载所有提取的文件。
Dism /unmount-Wim /MountDir:C:\winpe_x86\mount /Commit - 拷贝Winpe.wim至IOS目录
copy winpe.wim C:\winpe_x86\ISO\sources\boot.wim /y - 生成镜像文件
oscdimg -n -bC:\winpe_x86\etfsboot.com C:\winpe_x86\iso C:\winpe.iso - 利用Ultraiso写入镜像
利用Ultraiso打开Winpe.iso,然后菜单选择启动->写入硬盘映像,选择指定U盘,点击确认,等待完成,winpe启动盘即制作完成。
也可以使用命令行制作WinPE系统:
set PEPath=C:\win10PE_x86
call "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
if exist %PEPath% rmdir /s /q %PEPath%
call CopyPE.cmd x86 %PEPath%
Dism /Mount-Image /ImageFile:%PEPath%\media\sources\boot.wim /index:1 /MountDir:%PEPath%\mount
pause
Dism /Unmount-Image /MountDir:%PEPath%\mount /commit
rem MakeWinPEMedia /ISO %PEPath% %PEPath%\Win10PE_x86.ISO
MakeWinPEMedia /ufd %PEPath% g:
3.2.4. PCIe SSD
PCIe接口的SSD主要用高性能场景,如高价个人电脑,还有云存储、企业分布式存储等。PCIe接口的SSD采用NVMe通信协议,协议中也有规定固件升级流程,和FFU流程大同小异,下载固件,激活固件。
● Windows下的实现流程:
BOOL DeviceStorageFirmwareUpgrade(int _nPhyNo, BYTE _slotID)
{QString strDeviceName = QString("%1%2").arg("\\\\.\\Physicaldrive").arg(_nPhyNo);HANDLE deviceHandle = CreateFile(strDeviceName.toStdWString().data(),GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,FILE_FLAG_NO_BUFFERING,nullptr);if (deviceHandle == INVALID_HANDLE_VALUE){return FALSE;}// Setup header of firmware download data structure.const DWORD dwBuffSize = FIELD_OFFSET(STORAGE_HW_FIRMWARE_DOWNLOAD, ImageBuffer) + 4*1024;QScopedPointer<char> scopeBuff(new char[dwBuffSize]());PSTORAGE_HW_FIRMWARE_DOWNLOAD firmwareDownload = reinterpret_cast<PSTORAGE_HW_FIRMWARE_DOWNLOAD>(scopeBuff.get());firmwareDownload->Version = sizeof(STORAGE_HW_FIRMWARE_DOWNLOAD);firmwareDownload->Size = dwBuffSize;firmwareDownload->Flags = STORAGE_HW_FIRMWARE_REQUEST_FLAG_CONTROLLER;firmwareDownload->Slot = 0x11;// Open image file and download it to controller.ULONGLONG imageBufferLength = dwBuffSize - FIELD_OFFSET(STORAGE_HW_FIRMWARE_DOWNLOAD, ImageBuffer);QString strFilePath = ":/libra_cpu01_sysfw.bin";QFile file(strFilePath);if (!file.open(QIODevice::ReadOnly)){qDebug()<<"Open "<<strFilePath<<" failed.";return FALSE;}ULONG imageOffset = 0;BOOL moreToDownload = TRUE;while (moreToDownload){RtlZeroMemory(firmwareDownload->ImageBuffer, imageBufferLength);qint64 readLength = file.read(reinterpret_cast<char*>(firmwareDownload->ImageBuffer), static_cast<qint64>(imageBufferLength));if (readLength == 0){file.close();break;}firmwareDownload->Offset = imageOffset;firmwareDownload->BufferSize = min(imageBufferLength, static_cast<ULONG>(readLength));ULONG returnedLength = 0;BOOL result = DeviceIoControl(deviceHandle,IOCTL_STORAGE_FIRMWARE_DOWNLOAD,scopeBuff.get(),dwBuffSize,scopeBuff.get(),dwBuffSize,&returnedLength,nullptr);if (!result){return FALSE;}imageOffset += static_cast<ULONG>(firmwareDownload->BufferSize);}// Activate the newly downloaded image.RtlZeroMemory(scopeBuff.get(), dwBuffSize);PSTORAGE_HW_FIRMWARE_ACTIVATE firmwareActivate = reinterpret_cast<PSTORAGE_HW_FIRMWARE_ACTIVATE>(scopeBuff.get());firmwareActivate->Version = sizeof(STORAGE_HW_FIRMWARE_ACTIVATE);firmwareActivate->Size = sizeof(STORAGE_HW_FIRMWARE_ACTIVATE);firmwareActivate->Slot = _slotID;firmwareActivate->Flags = STORAGE_HW_FIRMWARE_REQUEST_FLAG_CONTROLLER;// activate firmwareULONG returnedLength = 0;BOOL result = DeviceIoControl(deviceHandle,IOCTL_STORAGE_FIRMWARE_ACTIVATE,scopeBuff.get(),dwBuffSize,scopeBuff.get(),dwBuffSize,&returnedLength,nullptr);return result;
}
● Linux下有开源工具NVME-cli,可以直接使用:
> nvme fw-download /dev/nvme0 -f allBinary.bin nvme fw-commit /dev/nvme0
> -s 2 -a 1
> # 参数-s代表slot。标准定义SSD支持7个slot,slot 1 是只读权限,用于存放出厂固件,slot 2和3 可用于固件下载。
> # 参数-a代表不同的升级方法,常用的有两个。001b(向指定slot下载固件,需要reset后完成激活操作);
> # 011b(向指定slot下载固件,激活立即生效,固件升级完成)
4. 参考资料
- NVM Express TM Revision 1.4
- Information technology -SCSI / ATA Translation - 5 (SAT-5)
- ATA Command Pass-Through
- Universal Serial Bus Mass Storage Class UFI Command Specification
- Universal Flash Storage (UFS)Version 3.1
- Embedded Multi-Media Card (e•MMC) Electrical Standard (5.1)
- https://github.com/linux-nvme/nvme-cli/
- https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-ioctl_storage_firmware_activate
- https://learn.microsoft.com/zh-cn/windows-hardware/manufacture/desktop/winpe-intro?view=windows-11
- https://github.com/u-boot/u-boot
相关文章:

固态存储设备固件升级方案
1. 前言 随着数字化时代的发展,数字数据的量越来越大,相应的数据存储的需求也越来越大,存储设备产业也是蓬勃发展。存储设备产业中,发展最为迅猛的则是固态存储(Solid State Storage,SSS)。数字化时代,海量…...

Python交通标志识别基于卷积神经网络的保姆级教程(Tensorflow)
项目介绍 TensorFlow2.X 搭建卷积神经网络(CNN),实现交通标志识别。搭建的卷积神经网络是类似VGG的结构(卷积层与池化层反复堆叠,然后经过全连接层,最后用softmax映射为每个类别的概率,概率最大的即为识别…...

基于Selenium+Python的web自动化测试框架(附框架源码+项目实战)
目录 一、什么是Selenium? 二、自动化测试框架 三、自动化框架的设计和实现 四、需要改进的模块 五、总结 总结感谢每一个认真阅读我文章的人!!! 重点:配套学习资料和视频教学 一、什么是Selenium? …...

Python进阶-----高阶函数zip() 函数
目录 前言: zip() 函数简介 运作过程: 应用实例 1.有序序列结合 2.无序序列结合 3.长度不统一的情况 前言: 家人们,看到标题应该都不陌生了吧,我们都知道压缩包文件的后缀就是zip的,当然还有r…...

win10打印机拒绝访问解决方法
一直以来,在安装使用共享打印机打印一些文件的时候,会遇到错误提示:“无法访问.你可能没有权限使用网络资源。请与这台服务器的管理员联系”的问题,那为什么共享打印机拒绝访问呢?别着急,下面为大家带来相关的解决方法…...

深度学习训练营之数据增强
深度学习训练营学习内容原文链接环境介绍前置工作设置GPU加载数据创建测试集数据类型查看以及数据归一化数据增强操作使用嵌入model的方法进行数据增强模型训练结果可视化自定义数据增强查看数据增强后的图片学习内容 在深度学习当中,由于准备数据集本身是一件十分复杂的过程,…...

Tomcat安装及启动
日升时奋斗,日落时自省 目录 1、Tomcat下载 2、JDK安装及配置环境 3、Tomcat配置环境 4、启动Tomcat 5、部署演示 1、Tomcat下载 直接入主题,下载Tomcat 首先就是别下错了,直接找官方如何看是不是广告,或者造假 搜索Tomc…...

【专项训练】排序算法
排序算法 非比较类的排序,基本上就是放在一个数组里面,统计每个数出现的次序 最重要的排序是比较类排序! O(nlogn)的3个排序,必须要会!即:堆排序、快速排序、归并排序! 快速排序:分治 经典快排 def quickSort1(arr...
Android压测测试事件行为参数对照表
执行参数参数说明颗粒度指标基础参数--throttle <ms> 用于指定用户操作间的时延。 -s 随机数种子,用于指定伪随机数生成器的seed值,如果seed值相同,则产生的时间序列也相同。多用于重测、复现问题。 -v 指定输出日志的级别,…...

【观察】亚信科技:“飞轮效应”背后的数智化创新“延长线”
著名管理学家吉姆柯林斯在《从优秀到卓越》一书中提出“飞轮效应”,它指的是为了使静止的飞轮转动起来,一开始必须使很大的力气,每转一圈都很费力,但达到某一临界点后,飞轮的重力和冲力就会成为推动力的一部分…...
QT编程从入门到精通之十四:“第五章:Qt GUI应用程序设计”之“5.1 UI文件设计与运行机制”之“5.1.1 项目文件组成”
目录 第五章:Qt GUI应用程序设计 5.1 UI文件设计与运行机制 5.1.1 项目文件组成 第五章:Qt GUI应用程序设计...
(二分)730. 机器人跳跃问题
目录 题目链接 一些话 切入点 流程 套路 ac代码 题目链接 AcWing 730. 机器人跳跃问题 - AcWing 一些话 // 向上取整 mid的表示要写成l r 1 >> 1即可,向下取整 mid l r >> 1 // 这里我用了浮点二分,mid (l r) / 2,最…...

vue3使用nextTick
发现nextTick必须放在修改一个响应式数据之后,才会在onUpdated之后被调用,如果nextTick是放在所有对响应式数据修改之前,则nextTick里面的回调函数会在onBeforeUpdate方法执行前就被调用了。可是nextTick必须等到onUpdated执行完成之后执行&a…...

传统图像处理之颜色特征
博主简介 博主是一名大二学生,主攻人工智能研究。感谢让我们在CSDN相遇,博主致力于在这里分享关于人工智能,c,Python,爬虫等方面知识的分享。 如果有需要的小伙伴可以关注博主,博主会继续更新的,…...
GPS问题调试—MobileLog中有关GPS关键LOG的释义
GPS问题调试—MobileLog中有关GPS关键LOG的释义 [DESCRIPTION] 在mobile log中,有很多GPS相关的log出现在main log和kernel log、properties文件中,他们的意思是什么,通过这篇文档进行总结,以便在处理GPS 问题时,能够根据这些log快速的收敛问题。 [SOLUTION] 特别先提醒…...
【企业管理】你真的理解向下管理吗?
导读:拜读陈老师一篇文章《不会向下负责,你凭什么做管理者?》,引发不少共鸣,“很多管理者有一种错误的观念,认为管理是向下管理,向上负责。其实应该反过来,是向上管理,向…...
Centos7 硬盘挂载流程
1、添加硬盘到Linux,添加后重启系统2、查看添加的硬盘,lsblksdb 8:16020G 0disk3、分区fdisk /dev/sdbmnw其余默认,直接回车再次查看分区情况,lsblksdb1 8:17 0 20G 0 part4、格式化mkfs -t ext4 /dev/sdb15、挂载mkdir /home/new…...

认识vite_vue3 初始化项目到打包
从0到1创建vite_vue3的项目背景效果vite介绍(对比和vuecli的区别)使用npm创建vitevitevuie3创建安装antdesignvite自动按需引入(vite亮点)请求代理proxy打包背景 vue2在使用过程中对象的响应式不好用新增属性的使用$set才能实现效…...
【Go】cron时间格式
【Go】cron时间格式 Minutes:分钟,取值范围[0-59],支持特殊字符* / , -;Hours:小时,取值范围[0-23],支持特殊字符* / , -;Day of month:每月的第几天,取值范…...
leetcode 55. 跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标。 示例 1: 输入:nums [2,3,1,1,4] 输出:true 解释:可以先跳 1 …...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...

MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
JS手写代码篇----使用Promise封装AJAX请求
15、使用Promise封装AJAX请求 promise就有reject和resolve了,就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...

android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...