由于官方SDK比较冗余且经常跨文档讲解且SDK整理的乱七八糟,对于新手来说全部看完上手成本较高,本文旨在以简短的方式介绍 CAFFE / ONNX 模型转 om 模型,并进行推理的全流程。希望能够帮助到第一次接触华为海思框架的道友们。大佬们就没必要看这种基础文章啦!
注:本文所有操作均在 WSL (Windows 虚拟子系统)上操作的,默认 root 环境,默认开发板系统为LINUX,所有环境变量均写入 bashrc ,非虚拟机用户谨慎写入。所有用到的工具以及示例代码,均在文末。
1. 简介
ATC 是海思芯片的模型转换工具,用于将开源框架网络模型(caffe,onnx,tensorflow)转化为 .om 格式并使用 AscendCL进行推理(因为笔者比较熟悉瑞芯微系列,分别类似于类似瑞芯微系列的RKNNToolkit工具, .rknn 模型,RKNNToolkitLite板端推理工具)。
首先在进行下面这篇文章之前,先来了解一下 SS928V100(Hi3403V100) 这款芯片,该芯片有两颗 NPU,分别是 NNN 和SVP_NNN,其中 NNN 支持4.8Tops算力,支持 INT4/INT8/FP16;SVP_NNN 支持5.6Tops算力,支持 INT8/INT16。
简单说明:
因此本文的安装内容也会分成两部分,分别是 NNN 和 SVP_NNN
1.1 通用交叉编译工具链安装
chmod +x Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run
# 校验安装包是否完整, 校验成功会显示 Verifying archive integrity… 100%
./Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run –check
# 安装,安装过程中有协议确认,按 Y 即可,安装成功会显示 <…> install success
# 安装路径:root用户:“/usr/local/Ascend”,非root用户:“${HOME}/Ascend”
./Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run –install
# 配置环境变量,以 root 用户为例(WSL默认sudo权限)
source /usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/x86_64-linux/bin/setenv.bash
vi ~/.bahsrc
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/x86_64-linux/devlib/:$LD_LIBRARY_PATH
source ~/.bashrc
1.2 安装openssh(有的话可跳过,没有也可以通过 NFS 共享目录解决文件传输问题)
说实话,挺烦这种精简 linux 系统的,要啥没啥,不知道精减的意义是啥,每次都要重复这种安装编译环境的过程。
参考,注意本文使用的交叉编译工具链的名字和文中的不一样:【MindStudio】【OpenSSH】海思SOC环境准备(linux + android)-云社区-华为云
真是令人抓狂,海思SS928这款开发板,连adb都莫得,交叉编译好的文件甚至都没办法传到开发板上,太恶心啦!!!
NFS 共享目录方法如下:
# 1. 以 root 用户登录 Linux 服务器(Ubuntu 操作系统),安装 NFS 服务并配置共享目录。
apt-get install nfs-kernel-server
# 2. 在“/etc/exports”文件中配置共享目录
vi /etc/exports
# 在末尾添加 <服务器共享目录绝对路径> *(rw,sync,root_squash), 如
/userdata/share *(rw,sync,root_squash)
# 3. 重启 nfs 服务
/etc/init.d/nfs-kernel-server restart
# 4. 开发板板端挂载,mount -t nfs -o nolock,tcp NFS <服务器IP地址:服务器绝对路径path>,如
mount -t nfs -o nolock,tcp NFS 192.168.1.2:/userdata/share
2. NNN 引擎下的模型转换与推理
2.1 安装CANN ToolKit(以python3.9为例)
- 编译python3.9
坑:各 python 必须严格按照下述版本,否则执行模型转换时会报错 :Python3.7.x(3.7.5~3.7.11)、Python3.8.x(3.8.0~3.8.11)、Python3.9.x(3.9.0~3.9.2)推荐使用Python3.9.2
# 安装python3.9
sudo apt update
sudo apt install -y build-essential libssl-dev zlib1g-dev \\
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \\
libncurses5-dev libncursesw5-dev xz-utils tk-dev \\
libffi-dev libxslt1-dev liblzma-dev python3-openssl cmake Pillow git
wget https://www.python.org/ftp/python/3.9.2/Python-3.9.2.tgz
tar -zxvf Python-3.9.2.tgz
cd Python-3.9.2
./configure –prefix=/usr/local/python3.9.2 –with-ssl-default-suites=openssl –enable-shared CFLAGS=-fPIC
make -j8
make install
vi ~/.bashrc
export LD_LIBRARY_PATH=/usr/local/python3.9.2/lib:$LD_LIBRARY_PATH
export PATH=/usr/local/python3.9.2/bin:$PATH
source ~/.bashrc
# 安装 cann 所需 python 环境
# 注意,WSL默认sudo权限,此处如果用sudo,后续执行 ./Ascend-cann-toolkit_8.1.RC1_linux-x86_64.run –install 也要加sudo
pip3 install attrs cython numpy==1.24.0 decorator sympy cffi pyyaml pathlib2 psutil protobuf==3.20 scipy requests absl-py -i https://pypi.tuna.tsinghua.edu.cn/simple
- 在昇腾官网下载 cann 安装包, 并传到WSL中:社区版资源下载-资源下载中心-昇腾社区
注:我用的是SDK中提供的
Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run
- 执行如下命令安装 chmod +x Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run
# 校验安装包是否完整, 校验成功会显示 Verifying archive integrity… 100%
./Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run –check# 安装,安装过程中有协议确认,按 Y 即可,安装成功会显示 <…> install success
# 安装路径:root用户:“/usr/local/Ascend”,非root用户:“${HOME}/Ascend”
./Ascend-cann-toolkit_5.20.t6.2.b060_linux-x86_64.run –install# 配置环境变量,以 root 用户为例(WSL默认sudo权限)
source /usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/x86_64-linux/bin/setenv.bash
vi ~/.bahsrc
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/x86_64-linux/devlib/:$LD_LIBRARY_PATH
source ~/.bashrc安装完成后,模型转换工具 atc 在 " ${HOME}Ascend/ascend-toolkit/latest/bin " 目录下。
2.2 caffe/onnx 转 om(以resnet50为例)
- 转换示例:
# 模型转换(类似RKNNToolKit),注意每次重启后都需要再次 source /usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/x86_64-linux/bin/setenv.bash
# caffe 转 om
atc –model=./caffe_model/resnet50.prototxt –weight=./caffe_model/resnet50.caffemodel –framework=0 –output=./model/caffe_resnet50 –soc_version=OPTG –input_fp16_nodes="data" –enable_single_stream=true
# onnx 转 om
atc –model=./caffe_model/resnet50_official.onnx –framework=5 –output=./model/resnet50_bs64 –input_format=NCHW –soc_version=OPTG –input_fp16_nodes="data" –enable_single_stream=true
# sample 编译
vi ~/.bashrc
export DDK_PATH=/usr/local/Ascend/ascend-toolkit/latest/arm64-lmixlinux200
export NPU_HOST_LIB=/usr/local/Ascend/ascend-toolkit/latest/arm64-lmixlinux200/aarch64-linux/devlib
source ~/.bashrc
mkdir -p build/intermediates/soc
cd build/intermediates/soc
cmake ../../../src -DCMAKE_CXX_COMPILER=aarch64-mix210-linux-g++ -DCMAKE_SKIP_RPATH=TRUE
make
- 参数讲解:
| 参数 | 说明 |
|
–weight |
仅caffe模型需要,指定 .caffemodel 路径 |
|
–framewowrk |
0:Caffe,1:MindSpore,2:TensorFlow ,5:ONNX (opset v11) |
| –model |
模型文件 |
|
–input_format |
输入数据格式 (Caffe、ONNX 默认为 NCHW;TensorFlow 默认为 NHWC) |
|
–enable_single_stream |
是否使能一个模型推理时只能使用一条 Stream (由于 SoC 的 Stream 资源有限,在 SoC 场景下需要用户打开此开关,SOC 广义上讲其实就是我们常用的开发板啦) |
| –input_fp16_nodes | 模型输入类型:
FP32、FP16通过指定该参数设置,UINT8 需要结合 AIPP 预处理文件设置 |
2.3 om 模型推理流程(类似RKNNToolKitLite)
- 基本概念:
Device 表示板端环境上的 NNN(Neural-Network Processing Unit),提供 NN 计算能力。
Context 作为一个容器,管理了所有对象(包括 Stream、Event、设备内存等)的生命周期。
Stream 用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序在 Device 上执行。
坑1:
如果开发板没有相关的 NPU 驱动 "dev/xiaoyaozi0", 切换到 " /smp/a55_linux/mpp/out/ko " 目录下执行 " ./load_ss928v100 -a "。等等,先不要惊讶,这个 NNN 的 NPU 驱动就叫 xiaoyaozi0,哈哈哈哈,看得出来,编这个驱动的程序猿很逍遥……
坑2:
板端执行需设置环境变量 , {PATH} 替换成实际目录地址:
export LD_LIBRARY_PATH={PATH}/smp/a55_linux/mpp/out/lib/npu:$LD_LIBRARY_PATH
坑3:
执行 NNN 相关代码时,必须手动拷贝 libcpu_kernels.so 和 libaicpu_kernels.so 到指定目录,否则将无法运行,软链接无效!!!
mkdir -p /usr/lib64/aicpu_kernels
mkdir -p /usr/lib64/aicpu_kernels/0/aicpu_kernels_device/
cp /usr/lib64/npu/libcpu_kernels.so /usr/lib64/aicpu_kernels
cp /usr/lib64/npu/libcpu_kernels.so /usr/lib64/aicpu_kernels/libaicpu_kernels.so.
cp /usr/lib64/npu/libaicpu_kernels.so /usr/lib64/aicpu_kernels/0/aicpu_kernels_device/
cp /usr/lib64/npu/libaicpu_kernels.so /usr/lib64/aicpu_kernels/0/aicpu_kernels_device/libaicpu_kernels.so.
你可能会有这样一个疑问:为啥要这么样做?好麻烦的说。
答:别问,问就是工业LJ!
坑4(不一定每家开发板都有):
板端环境要注意,/usr/lib64/npu/libascend_protobuf有两个
/usr/lib64/npu/libascend_protobuf.so
/usr/lib64/npu/libascend_protobuf.so.3.13.0.0
其中 /usr/lib64/npu/libascend_protobuf.so.3.13.0.0 用的是新版 C++ ABI,包含符号 cxx11,运行可执行程序时会出错,因为开发板自带的libstdc++ 不包含 cxx11
- 板端推理代码示例:
#include "acl/acl.h"
#include <iostream>
#include <fstream>
#include <cstring>
#include <map>
#include <math.h>
using namespace std;
int32_t deviceId = 0;
uint32_t modelId;
size_t pictureDataSize = 0;
void *pictureHostData;
void *pictureDeviceData;
aclmdlDataset *inputDataSet;
aclDataBuffer *inputDataBuffer;
aclmdlDataset *outputDataSet;
aclDataBuffer *outputDataBuffer;
aclmdlDesc *modelDesc;
size_t outputDataSize = 0;
void *outputDeviceData;
void *outputHostData;
void ReadPictureTotHost(const char *picturePath)
{
string fileName = picturePath;
ifstream binFile(fileName, ifstream::binary);
binFile.seekg(0, binFile.end);
pictureDataSize = binFile.tellg();
binFile.seekg(0, binFile.beg);
aclError ret = aclrtMallocHost(&pictureHostData, pictureDataSize);
binFile.read((char*)pictureHostData, pictureDataSize);
binFile.close();
}
int main()
{
printf("==== 这是测试代码123 ==== \\n");
// ===== step1 : 初始化 acl =====
aclError ret = aclInit(nullptr);
if (ret != ACL_SUCCESS) cerr << "aclInit failed with ret: " << ret << "\\n" << endl;
ret = aclrtSetDevice(deviceId); // 默认创建了 context 和 stream
if (ret != ACL_SUCCESS) cerr << "aclrtSetDevice failed with ret: " << ret << "\\n" << endl;
// ===== step2 : 加载模型 =====
const char *modelPath = "../model/resnet50.om";
ret = aclmdlLoadFromFile(modelPath, &modelId);
if (ret != ACL_SUCCESS) cerr << "aclmdlLoadFromFile failed with ret: " << ret << "\\n" << endl;
// ===== setp3 : 加载 bin 类型的图片 =====
const char *picturePath = "../data/dog1_1024_683.bin";
ReadPictureTotHost(picturePath);
ret = aclrtMalloc(&pictureDeviceData, pictureDataSize, ACL_MEM_MALLOC_HUGE_FIRST);
if (ret != ACL_SUCCESS) cerr << "aclrtMalloc failed with ret: " << ret << "\\n" << endl;
ret = aclrtMemcpy(pictureDeviceData, pictureDataSize, pictureHostData, pictureDataSize, ACL_MEMCPY_HOST_TO_DEVICE);
if (ret != ACL_SUCCESS) cerr << "aclrtInputMemcpy failed with ret: " << ret << "\\n" << endl;
// ===== step4 :执行推理 =====
// === step4.1 : 创建aclmdlDataset类型的数据,描述模型推理的输入 ===
inputDataSet = aclmdlCreateDataset();
inputDataBuffer = aclCreateDataBuffer(pictureDeviceData, pictureDataSize);
ret = aclmdlAddDatasetBuffer(inputDataSet, inputDataBuffer);
if (ret != ACL_SUCCESS) cerr << "aclmdlAddDatasetBuffer failed with ret: " << ret << "\\n" << endl;
// === step4.2 : 创建aclmdlDataset类型的数据,描述模型推理的输出 ===
// 创建模型描述信息
modelDesc = aclmdlCreateDesc();
ret = aclmdlGetDesc(modelDesc, modelId);
if (ret != ACL_SUCCESS) cerr << "aclmdlGetDesc failed with ret: " << ret << "\\n" << endl;
outputDataSet = aclmdlCreateDataset();
outputDataSize = aclmdlGetOutputSizeByIndex(modelDesc, 0);
// 申请输出内存
ret = aclrtMalloc(&outputDeviceData, outputDataSize, ACL_MEM_MALLOC_HUGE_FIRST);
if (ret != ACL_SUCCESS) cerr << "aclrtMalloc failed with ret: " << ret << "\\n" << endl;
outputDataBuffer = aclCreateDataBuffer(outputDeviceData, outputDataSize);
ret = aclmdlAddDatasetBuffer(outputDataSet, outputDataBuffer);
if (ret != ACL_SUCCESS) cerr << "aclmdlAddDatasetBuffer failed with ret: " << ret << "\\n" << endl;
// === step 4.3 : 推理 ===
ret = aclmdlExecute(modelId, inputDataSet, outputDataSet);
if (ret != ACL_SUCCESS) cerr << "aclmdlExecute failed with ret: " << ret << "\\n" << endl;
// ===== step5:获取推理结果数据 =====
// === step5.1 : copy 推理结果到 outputHostData
ret = aclrtMallocHost(&outputHostData, outputDataSize);
if (ret != ACL_SUCCESS) cerr << "aclrtMallocHost failed with ret: " << ret << "\\n" << endl;
ret = aclrtMemcpy(outputHostData, outputDataSize, outputDeviceData, outputDataSize, ACL_MEMCPY_DEVICE_TO_HOST);
if (ret != ACL_SUCCESS) cerr << "aclrtOutputMemcpy failed with ret: " << ret << "\\n" << endl;
// === step5.2 :后处理部分
float* outFloatData = reinterpret_cast<float *>(outputHostData);
// 屏显测试图片的top5置信度的类别编号
map<float, unsigned int, greater<float>> resultMap;
for (unsigned int j = 0; j < outputDataSize / sizeof(float); ++j)
{
resultMap[*outFloatData] = j;
outFloatData++;
}
double totalValue=0.0;
for (auto it = resultMap.begin(); it != resultMap.end(); ++it)
{
totalValue += exp(it->first);
}
int cnt = 0;
for (auto it = resultMap.begin();it != resultMap.end();++it)
{
if(++cnt > 5) break;
printf("top %d: index[%d] value[%lf] \\n", cnt, it->second, it->first);
}
// ===== step6 : 释放资源 =====
// === 6.1 释放模型描述信息并卸载模型 ===
aclmdlDestroyDesc(modelDesc);
aclmdlUnload(modelId);
// === 6.2 释放加载的测试图片 ===
ret = aclrtFreeHost(pictureHostData);
pictureHostData = nullptr;
ret = aclrtFree(pictureDeviceData);
pictureDeviceData = nullptr;
aclDestroyDataBuffer(inputDataBuffer);
inputDataBuffer = nullptr;
aclmdlDestroyDataset(inputDataSet);
inputDataSet = nullptr;
ret = aclrtFreeHost(outputHostData);
outputHostData = nullptr;
ret = aclrtFree(outputDeviceData);
outputDeviceData = nullptr;
aclDestroyDataBuffer(outputDataBuffer);
outputDataBuffer = nullptr;
aclmdlDestroyDataset(outputDataSet);
outputDataSet = nullptr;
// === 6.3 释放计算资源 ===
ret = aclrtResetDevice(deviceId);
aclFinalize();
}
3. SVP_NNN 引擎下的模型转换与推理
3.1 安装 CANN ToolKit(以python3.9为例)
注意,此处用的SDK是"Ascend-cann-toolkit_6.10.t01spc030b270_linux.x86_64.run",写到这里,不得不吐槽一下,我不知道是我拿到手的SDK有问题,还是整理这个SDK的人有问题,喵的,整个SDK没有一篇文档有说过 SVP_NNN 和 NNN 用的ATC工具不是同一个。也没有一篇文档讲了 SVP_NNN 的 ATC 工具进行转换时参数应该怎么写……
不过好在,安装过程和 NNN 的 ATC 工具差别不大,只需要注意环境变量即可
chmod +x Ascend-cann-toolkit_6.10.t01spc030b270_linux.x86_64.run
./Ascend-cann-toolkit_6.10.t01spc030b270_linux.x86_64.run –install
# install 成功后,终端会打印如下 export 环境变量信息
vi ~/.bashrc
export DDK_PATH=/usr/local/Ascend/ascend-toolkit/svp_latest/x86_64-linux
export NPU_HOST_LIB=/usr/local/Ascend/ascend-toolkit/svp_latest/x86_64-linux/acllib/lib64/stub
export PATH=/usr/local/Ascend/ascend-toolkit/svp_latest/toolkit/bin:$PATH
export PATH=/usr/local/Ascend/ascend-toolkit/svp_latest/atc/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/svp_latest/toolkit/tools/sim/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/svp_latest/atc/lib64:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/svp_latest/atc/third_party_lib:$LD_LIBRARY_PATH
source ~/.bashrc
3.2 caffe 转 om
我个人觉得 SVP_NNN 的 ATC 比较好用,可能因为 SVP_NNN 只支持 INT8/INT16 ,所以就量化而言,要比 NNN 下使用 AMCT 工具来的轻松友好的多,转换示例:
atc \\
–weight="./caffe_model/resnet50.caffemodel" \\
–model="./caffe_model/resnet50.prototxt" \\
–framework=0 \\
–log_level=0 \\
–dump_data=0 \\
–online_model_type=0 \\
–batch_num=256 \\
–input_shape="data:1,3,224,224" \\
–input_type="data:UINT8" \\
–input_format=NCHW \\
–output="./model/resnet50" \\
–soc_version=SS928V100 \\
–insert_op_conf=./insert_op.cfg \\
–compile_mode=0 \\
–save_original_model=false \\
–image_list="data:./data/image_ref_list.txt"
具体参数信息,安装完 ATC 环境后,可通过 atc –hlep 查看,以下仅对常用基础参数进行说明:
| 参数 | 说明 |
|
–batch_num |
批处理层的批次数,[0, 256],默认值为 256 |
|
–compile_mode |
量化数据类型,0:低精度(8位量化),1:高精度(16位量化) |
|
–framework |
输入模型的框架,0:caffe,5:onnx,默认 0 |
|
–image_list |
量化校准使用的图像列表文件 |
|
–input_shape |
输入数据的形状 |
|
–input_format |
输入数据的格式,{NCHW},默认:NCHW |
|
–input_type |
支持的输入数据类型:{FP16, FP32, INT16, INT8, S16, S8, U16, U8, UINT16, UINT8},默认 FP32 |
|
–insert_op_conf |
AIPP 配置文件路径 |
|
–model |
输入模型文件路径 |
|
–weight |
权重文件路径 |
|
–output |
输出 om 路径及文件名 |
|
–output_type |
支持的输出数据类型:{FP16, FP32, INT16, INT8, S16, S8, U16, U8, UINT16, UINT8},默认 FP32 |
|
–soc_version |
SoC 版本,{SS626V100, SS928V100},默认 SS626V100(注意,次数和 NNN 下的 ATC 有一定区别,不支持 OPTG) |
|
–save_original_model |
是否保存原始模型,默认 false |
3.3 om 推理流程
坑1:
如果开发板没有相关的 NPU 驱动 "dev/svp_npu",切换到 " /smp/a55_linux/mpp/out/ko " 目录下,修改 " ./load_ss928v100" 这个可执行程序,搜索加载 " pqp.ko "的代码,然后将其注释掉,然后关闭注释或添加如下内容:"insmod svp_npu/ot_svp_npu.ko svp_npu_save_power=1 svp_npu_max_task_node_num=512", 因为 "ot_svp_npu.ko" 和 "pqp.ko" 不能同时加载
坑2:
板端执行需设置环境变量,{PATH} 替换成实际目录地址 :
export LD_LIBRARY_PATH={PATH}/smp/a55_linux/mpp/out/lib/svp_npu:$LD_LIBRARY_PATH
SVP_NNN 下的推理流程和 NNN 类似,区别在于引用的头文件名称以及接口都添加了 svp 的前缀,且接口风格由驼峰变成了下划线分割,下列代码仅贴出和 NNN 不同的代码段以供理解,完整代码见文末:
#include "acl/svp_acl.h"
# 1. 初始化 svp_acl
svp_acl_error ret = svp_acl_init(aclConfigPath);
svp_acl_error ret = svp_acl_rt_set_device(deviceId_);
# 2. 加载模型
svp_acl_error ret = svp_acl_mdl_load_from_mem(static_cast<uint8_t* >(modelMemPtr_), modelMemSize_, &modelId_);
# 3. 创建模型描述信息
modelDesc_ = svp_acl_mdl_create_desc();
svp_acl_error ret = svp_acl_mdl_get_desc(modelDesc_, modelId_);
# 4. 创建输入
Result ret = GetInputStrideParam(0, devSize, stride, inputDims);
size_t dataSize = GetInputDataSize(0);
void *picDevBuffer = Utils::GetDeviceBufferOfFile(filePath, inputDims, stride, dataSize);
Result ret = InitInput();
Result ret = CreateInput(picDevBuffer, devSize, stride);
# 4. 创建输出
output_ = svp_acl_mdl_create_dataset();
size_t outputSize = svp_acl_mdl_get_num_outputs(modelDesc_);
# 5. 执行推理
svp_acl_error ret = svp_acl_mdl_execute(modelId_, input_, output_);
4. 其他
4.1 om 模型信息查看
通过 netron 可以直接查看NNN ATC 工具转换的模型信息(SVP_NNN ATC 工具转换的 om 模型无法直接使用 netron 查看),或者执行如下命令,将 .om 模型转化为 json 文件查看模型信息:
atc –mode=1 –om={oem_model_path.oem} –json={oem_json_path.json}
4.2 查看 NPU 利用率
看整体的NPU资源占用,分两个核看
# SVP_NNN, 执行命令后查看 hw_utilization
watch -n 1 cat /proc/umap/svp_npu
# NNN, 执行命令后查看 utilizaiton rate
watch -n 1 cat /proc/umap/tsfw的utilizaiton rate
4.3 源码传送门
SS928V100_NNN&SVP_NNN_TOOLS 提取码: 2jdc





