音频转换工具
相关代码
1 | // main.cc |
1 | // utils.cc |
1 | // main.cc |
1 | // utils.cc |
CUDA (Compute Unified Device Architecture) 是 N 卡上的计算框架,在这个框架上可以装载 cuDNN 或者 TensorRT 进行深度学习模型的加速。前者主要用于模型训练的加速,而后者适用于部署时模型推理的加速。
这篇文章主要记录 Linux 下 CUDA,cuDNN 以及 TensorRT 的安装;
先去 CUDA 的页面 进行安装,另外一提 /etc/issue 中记录了当前 Linux 的发行版本,可以对照选择下载连接。因为是在 docker 中运行这个 CUDA 环境,所以不需要安装驱动,而是依靠物理主机 ( 我的 Windows ) 上的驱动 。在 **x86 > Debian 12 > deb (local) ** 的选择下有了这些指令
1 | wget https://developer.download.nvidia.com/compute/cuda/12.3.1/local_installers/cuda-repo-debian12-12-3-local_12.3.1-545.23.08-1_amd64.deb |
官方给出了 完整说明,在 CUDA 的基础上我们需要先安装 zlib 依赖
1 | apt install zlib1g |
接下来要去 cuDNN 主页 手动下载安装包,其中 tar 安装包适用于所有 Linux 分发版本,所以我选择了 tar 包
解压下载的 tar 包
1 | tar -xvf cudnn-linux-x86_64-8.9.7.29_cuda12-archive.tar.xz |
将文件放到 CUDA 目录中并设置文件权限
1 | sudo cp cudnn-linux-x86_64-8.9.7.29_cuda12-archive/include/cudnn*.h /usr/local/cuda/include |
这篇文章继续记录我与 C++ 生态打架的精彩记录,主要记录一些配置文件的编写,便于日后参考第三方库的安装。
与终端交互中完成如下配置(刚装的系统会有一些基础依赖需要按照报错提示自行安装)
1 | # 下载并初始化 |
这里参考官方的例子先给出基本的 CMakeLists.txt 文件和 main.cc 文件
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.10) |
main.cc
1 | #include <fmt/core.h> |
由于网络时不时会遭受神秘力量的攻击, 这里可以通过设置环境变量更换镜像源
1 | export X_VCPKG_ASSET_SOURCES=x-azurl,http://106.15.181.5/ |
CMKAE_TOOLCHAIN_FILE
这个值,官网给出了三种方法1 | { |
project()
这一行之前1 | set(CMAKE_TOOLCHAIN_FILE "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake") |
-D
选项1 | cmake -DCMAKE_TOOLCHAIN_FILE="$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" |
1 | vcpkg new --application |
1 | vcpkg add port fmt |
--preset
选项设置配置文件为上述 CMakePresets.json 中的 default 配置1 | mkdir build && cd build |
1 | ./Main |
实际上 vcpkg 安装包有两种模式:classic 和 manifest (上文使用的就是这种方式,也是官方推荐的 )
manifest: 相当于每次安装库的位置都是项目 build 文件夹内
classic: 相当于全局安装,库安装在在 VCPKG_ROOT
的文件夹中;在设置了 vcpkg 工具链的项目中, 每次安装都会从这个全局目录中寻找缓存,大大加快安装速度;该模式安装使用另一条指令
1 | vcpkg install <lib> |
另外贴上 vcpkg 页面 https://learn.microsoft.com/en-us/vcpkg/
偶然发现了 TechEmpower 这个后端框架评分网站,发现了一位重量级选手,2020年综合评分的冠军 Drogon,而这两年 Just.js 和 Rust 的两个框架霸榜,某 ++ 语言只能靠边观摩。今年更是因为测试不规范,被抬出了party。今天发现之前装框架的时候忘记装数据库接口了,这次干脆给自己写个 Dockerfile 打造一个专用的开发环境,也方便之后参考。
我把我的镜像上传到了 Docker Hub,有缘人可以拉下来玩。当然我一般还会自己配置 ssh 和 VSCode 开发,这里为了整洁就不将配置写进去了。
1 | docker pull azusaing/drogon:basic # 解压后 1.79 GiB |
花了我 3 分钟 build 的菜谱
1 | # +----------------------------------+ |
最近在学 CMake 和 UNIX ,不幸被老师抓去做 CV 和深度学习,(摸鱼的时候) 正巧发现 Pytorch 有开放的 C++ API 可以使用,便想着装来玩玩(一装装一下午)
LibTorch 是深度学习框架 PyTorch 开放的 C++ API ,本文将使用 CMake,针对 Linux 环境以及 Windows 环境对 LibTorch 库进行安装。目的是记录一些踩过的坑,并对 LibTorch 简洁的 README做一点补充解释 (他们甚至让我用 cmake-gui )
原文档 -> https://pytorch.org/cppdocs/installing.html
首先电脑上一定要装有 CMake 以及并配置好某个版本的 C++ 编译器路径
附加地,如果想要在 VSCode 中愉快地写代码,需要在扩展安装 CMake 、 CMakeTools 、 C\C++ 这三个插件,以便识别 CMake 工程和代码提示;喜欢终端可以直接跳过这一项
附加地,如果需要显卡计算,电脑上一定要先安装好 CUDA 和 cuDNN 库,并且在下文的下载步骤中选择好对应 CUDA 版本的库下载
在官网 https://pytorch.org/get-started/locally/ 根据交互界面获取分发地址,下载并解压
1 | # 我的 CUDA 版本是 11.8, pre 代表预览版本 |
编写 CMakeLists.txt 文件
1 | cmake_minimum_required(VERSION 3.10) |
编写 example-app.cpp 检查环境是否配置正确,我在官方给出代码的基础上多加了一句对 CUDA 的检查
1 | #include <torch/torch.h> |
编译
官方给出的方法
1 | # 设置下载的 libtorch 文件夹绝对路径 |
如果嫌麻烦也可以直接把路径写在 CMakeLists.txt 里,或者在 VSCode 的 CMake 插件中设置附加生成选项。比如前者可以这么写:
1 | set(CMAKE_PREFIX_PATH "absolute/path/to/libtorch" ) |
接着就可以直接使用下面指令生成了,或者可以通过 CMake 插件的按钮生成或者运行
1 | cmake --build . |
在官网 https://pytorch.org/get-started/locally/ 根据交互界面获取分发地址,直接点击下载;
1 | # 安装 CPU 的 release 版本; Debug 版本会带有调试信息,但编译会稍慢。 |
接着我们可以使用和 Linux 环境下一样的源文件和 CMakeLists.txt。
编译方法与 Linux 环境下一样
按照以上的步骤安装完之后会发现 VSCode 无法给出代码提示,于是我们就需要配置 .vscode/c_cpp_properties.json 文件, 在 "includePath"
中添加头文件的位置,VSCode 会检查这个文件并进行代码提示。
1 | { |
如果在 Windows 环境下这里可以加入环境变量简化路径,在上文 Windows 环境下我配置了 LIBTORCH
这个环境变量,于是可以把头文件位置写成:
1 | "${env:LIBTORCH}/**" |
message(${CMAKE_PREFIX_PATH})
检查环境变量是否正确导入。如果没有大概是因为 VSCode 保存着先前的环境变量。可以重启 VSCode,删除 build 文件夹重新生成 。(可见命令行的重要性 :XD)Linux 的一个优秀特性就是内核微调,包括了最大文件描述符个数的设置。最大文件描述符个数,分为用户的限制和系统级限制。
如果是用户级别的限制
可以用以下指令进行查看
1 | ulimit -n |
通过 ulimit
设置为最大限制,但是只针对当前会话有效
ulmit -SHn max-file-number
如果要永久有效,应该进入 **/etc/security/limits.conf ** 手动设置
1
2
hard nofile max-file-header # 硬限制
soft nofile max-file-header
系统级别的限制
临时设置
1 | sysctl -w fs.file-max=max-file-header |
永久设置 (修改 /etc/sysctl.conf)
1 | fs.file-max=max-file-header # 之后需要使用 sysctl -p 使设置生效 |
内核的参数大部分都可以在 /proc/sys 中查看,使用 sysctl -a
可以看到所有的内核参数。接下来我们关注几个重要的参数
/proc/sys/fs :其下保存着文件系统有关的信息
/proc/sys/net
listen
监听队列溢出指定进程调试,在 gdb 中指定 [PID] 后打断点进行调试
1 | (gdb) attach [PID] |
设置在 fork
调用之后是否跟随子进程(设置 [parent] 或者 [child])
1 | (gdb) set follow-fork-mode [parent/child] |
有以下指令可以帮助调试线程
1 | (gdb) info threads |
注意第三个指令 off
为默认值,on
表示只运行当前线程,step
表示单步调试的时候只有当前线程能执行
CPU
资源在处理响应连接上tcpdump
可以指定类型,方向或协议进行过滤
类型(包括 net
,host
,port
等)
1 | tcpdump net 192.168.31.1/24 |
方向 ( src
和 dest
)
1 | tcpdump dest port 8080 |
协议( 比如 icmp
)
1 | tcpdump icmp |
表达式之间可以进行逻辑运算(and
,or
,not
),复杂的表达式用单引号包裹,内部可以使用括号分组
1 | tcpdump 'src 192.168.31.217 and (dest port 8080 or 22)' |
同时支持对包内部的信息进行过滤,比如过滤得到 syn 报文。因为报文的第 14 个字节的第 2 位正是 syn 标志
1 | # 格式 -> tcpdump 'tcp[BYTES] & [NUMBER] != 0' |
同时支持许多选项
1 | -n # 使用IP地址表示主机名 |
lsof
即 list open file
-i
选项,查看套接字
1 | # lsof -i [IP_PROTO] [TRANS_PROTO][@HOSTNAME/IP_ADDR]:[SERVICE/PORT] |
-u
显示打开的所有套接字
1 | lsof -u |
-c
显示指定程序打开的套接字
1 | lsof -c ssh |
-p
显示指定进程打开的套接字
1 | lsof -p [PID] |
-t
显示打开了套接字的进程
nc
即 netcat ,debian 中安装可以使用
1 | apt install ncat |
之后我们就可以开始使用这个指令,有如下选项
1 | nc [HOSTNAME] [PORT] # 以客户端模式启动 |
strace
跟踪系统调用以及发送的信号
1 | -c # 统计系统调用的执行时间、次数和出错次数 |
-e
选项中被指定的表达式形式如下
1 | [QUALIFIER]=[!][VALUE1], [!][VALUE2], ... # `!` 代表取反 |
接下来是一些使用 -e
选项的示例
1 | strace -e trace=set # set代表了 open, write, read, close 四种简单调用 |
netstat
网络统计工具,可以统计网卡上的全部连接(相当于 lsof
)以及路由表和网卡等信息。
1 | -n # 使用 IP 号表示主机 |
vmstat
显示系统资源的指令
1 | -f # 显示自启动以来系统 fork 的次数 |
ifstat
检测流量速度
1 | -a # 检测系统上所有网卡接口 |
mpstat
mp 指 multi-processor , 用于监控 CPU 状况;下载 sysstat 包含这个指令
1 | # 格式 -> mpstat -[CPU_ID | ALL] [INTERVAL] [COUNT] |
比如我们可以这样使用
1 | mpstat -ALL 1 10 # 1s 输出一次, 总共十次 |
初稿 2023-9-17,Libevent 的版本 2.2.1-alpha-dev,在 Debian 12 环境上进行测试
官方网站有详细的文档可以参考 libevent.org
索引
从 github 拉取,并安装前置 openssl 库
1 | git clone https://github.com/libevent/libevent.git |
在 CMake + gcc 12.2.0 的环境下进行编译
1 | cd libevent |
接着将当前文件夹下的 lib 中的库移动到 /usr/lib 下,include 中的文件移动到 /usr/include;源代码根目录下的 include 移动到 /usr/include 中。
接着可以在 CMakeLists.txt 中引用,
1 | cmake_minimum_required(VERSION 3.0.0) |
我们可以使用以下函数检查当前环境 Libevent 支持的后端方法以及版本
1 | const char** event_get_supported_methods(void); |
libevent 中通过下列方法初始化和清理一个 reactor 模型
1 | struct event_base* event_base_new(); |
如果需要更多客制化,也可以通过 event_base_new_with_config()
进行初始化,通过一个 event_config
结构体设置更多初始化参数。
函数声明
1 | struct event_base* event_base_new_with_config(struct event_config* cfg); |
通过专门的函数设置 event_config
结构体。以下方法成功返回 0 , 否则返回 -1
初始化与释放
1 | // 初始化 |
设置不需要的方法,比如可以传入 "epoll"
1 | // 1. 设置不需要的方法, 比如可以传入"epoll" |
让 event 保证支持下列参数
1 | // 2. 让 libevent 保证支持以下参数的一种或多种 (可以进行‘或’运算进行组合) |
更多运行时参数
1 | // 3. 更多运行时参数 |
控制线程使用的 CPU 数,但是只有 Windows 的IOCP 支持
1 | int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus); |
*预防 Priority Inversion
1 | int event_config_set_max_dispatch_interval(struct event_config *cfg, |
对于初始化的模型,可以设置 event_base
的优先级并检查
1 | int event_base_priority_init(struct event_base *base, int n_priorities); |
检查某个初始化的 event_base
结构体使用的方法
1 | const char *event_base_get_method(const struct event_base *base); |
对于 fork
产生的子进程中的 event_base,需要使用以下函数进行重新初始化
1 | int event_reinit(struct event_base *base); |
对于一个事件有多个状态,根据官方文档
Events have similar lifecycles. Once you call a Libevent function to set up an event and associate it with an event base, it becomes initialized. At this point, you can add, which makes it pending in the base. When the event is pending, if the conditions that would trigger an event occur (e.g., its file descriptor changes state or its timeout expires), the event becomes active, and its (user-provided) callback function is run. If the event is configured persistent, it remains pending. If it is not persistent, it stops being pending when its callback runs. You can make a pending event non-pending by deleting it, and you can add a non-pending event to make it pending again.
使用 event_new()
函数创建事件,event_free()
释放事件;其中创建事件需要的参数
base
:指向创建的 event_base
实例
fd
:监听的事件描述符
what
: 指定事件类型(什么情况下被触发)以及附加选项,包含以下几种宏定义
1 | #define EV_TIMEOUT 0x01 // 经过一定时间之后事件变为 Activated 状态 |
cb
和 arg
,回调函数以及其传入参数
1 | typedef void (*event_callback_fn)(evutil_socket_t, short, void *); |
根据文档,创建的事件将会处于 Initialized 状态,但是不会被触发,除非我们将它设置为 Pending 状态。同时已经触发回调函数的事件将需要重新设置为 Pending 状态,除非它被设置了 EV_PERSIST
1 | int event_add(struct event *ev, const struct timeval *tv); |
删除事件将会使它们变为 Non-Pending 状态
1 | int event_del(struct event *ev); |
同时可以设置事件的优先级
1 | int event_priority_set(struct event *event, int priority); |
为了方便使用,针对特定事件类型,Libevent 设置特殊的宏定义或者函数便于使用。如下是两个特化的事件注册函数
定时事件
1 | #define evtimer_new(base, callback, arg) \ |
信号事件,注意一个进程只能有一个信号事件(详细请查阅操作系统原理)
1 | #define evsignal_new(base, signum, cb, arg) \ |
当我们注册好要监听的事件以及属性之后,我们便可以开始监听事件。前者类似于在内核事件表中注册事件,后者则更像是调用 epoll_wait
进行监听
event_base_loop()
,其中的 flags
可以设置为下列三个值:
EVLOOP_ONCE
:运行到事件完成之后便返回EVLOOP_NONBLOCK
: 只是检查事件是否有事件就绪,直接返回EVLOOP_NO_EXIT_ON_EMPTY
: 就算就绪队列为空也不会返回1 | int event_base_loop(struct event_base *base, int flags); |
event_base_dispatch()
可以看作是 event_base_loop
的简化版本,相当于设置了EVLOOP_ONCE
1 | int event_base_dispatch(struct event_base *base); |
一般的事件监听 (没有设置 EVLOOP_NO_EXIT_ON_EMPTY
的前提下) 都会在就绪事件队列为空的时候主动退出,但假如我们想在队列中还有事件的时候就强行退出,可以使用以下两个函数;与上述开启监听的两个函数类似,这两个函数只是调用接口略有不同
event_base_loopexit
:设置一段时间之后退出
1 | int event_base_loopexit(struct event_base *base, |
event_base_loopbreak
:相当于 event_base_loopexit(struct event_base *base, NULL)
,立即退出
1 | int event_base_loopbreak(struct event_base *base); |
通过下列两个函数检查事件监听是否正常退出
1 | int event_base_got_exit(struct event_base *base); |
I/O 复用可以使一个进程监听多个 I/O 事件 (包括基本的读写事件) 的发生,而不是每个事件都对应一个阻塞线程,以提升程序的 I/O 效率。可以设置:当可读事件发生时,函数返回;
可读事件包含以下几种:
内核缓冲区字节数大小大于或等于其低水位标记 SO_RCVLOWAT
(前文提到默认为 1 byte)
socket 连接被关闭
监听 socket 上有新的连接请求
可写事件包含以下几种:
SO_SNDLOWAT
connect
连接成功或超时、getsockopt
读取和清除该错误前三项表示对 读、写 和 异常 事件的监听,fd 按照 fd_set
结构传入,监听的事件被触发时,函数返回并将传入的 fd 修改后从内核复制到用户区。函数成功调用返回 就绪的文件描述符数量 ,若超时则返回 0 ,如果在等待期间接收到信号则返回 -1 并设置 errno 为 EINTR
1 | #include <sys/select> |
fd_set
的结构在此不进行深究,其内部使用 bitmap 。可以用以下几组函数设置
1 | #include <sys/select.h> |
使用 select
的时候可以把监听 fd 放入 fd 集合中一起处理,之后借助上述几个函数判断是什么事件使 select
返回,再逐一处理。注意内核会修改传入的 fd 集合,每次使用 select
的时候都应该清除触发的 fd 位,不然会导致同一个事件重复触发。
与上述 select 函数略有不同,这次 fd 的触发状态不再通过 fd_set
结构进行传递,而是通过 pollfd
类型的数组进行传递。而 n_fd_t
其实是 unsigned long
的封装,代表 fd 的数目。同时注意 timeout
参数类型不同。不过返回值逻辑与 select
相同
1 | #include <poll.h> |
epoll
的实现和用法与上述两个函数十分不同,它无需全量传入监听事件给内核,而是将事件放在一个额外的内核事件表中,所以需要多一个文件描述符表示内核事件表
1 | #include <sys/epoll.h> |
之后就可以向内核事件表中注册事件
1 | #include <sys/epoll.h> |
其中 op
指定操作类型,代表对于内核事件表的操作
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL
event
用于指定事件
1 | struct epoll_event { |
设置完事件之后就可以像前两种 I/O 复用函数一样调用,返回值逻辑同于上两种 I/O 复用函数。注意这里的 events 需要提供一个缓冲区供内核存放事件。
1 | int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); |
LT 与 ET 的区别在于后者只会通知一次。另外注册了 EPOLLONESHOT 的文件描述符,只会被触发一次 (不管是什么事件),事件处理完后要重新注册以便于收到下一次通知。
connect
, 连接失败会产生可写事件。这时就可以用上述 I/O 复用函数进行监听,接着用 getsockopt
读取错误码并清除错误。1 | #include <syslog.h> |
struct rlimt
结构体存储着资源限制值,超过其值时内核分别对应发送 SIGXFSZ
和 SIGXCPU
信号
1 | struct rlimit { |
通过以下函数查看与设置:
1 | #include <sys/resource.h> |
另外可以通过 shell 命令 ulimit 设置当前shell 环境下资源限制,对当前 shell 启动的所有后续进程有效
查看与修改工作目录
1 | #include <unistd.h> |
修改根目录
1 | #include <unistd.h> |
直接使用 Linux 提供的库函数,设置 nochdir
为 false
会改变当前 工作目录 。noclose
代表是否关闭原来的标准输入输出以及错误流
1 | #include <unistd.h> |
手动实现,从父进程 fork
出子进程之后将父进程退出,将输入输出以及错误流都重定向到 /dev/null 中
包括 open
, read
, write
, lseek
, close
标准输入输出和错误流有 STDIN_FILENO
、STDOUT_FILENO
和 STDERR_FILENO
等宏定义可以使用
一个进程的文件描述符最大值小于 OPEN_MAX
( 见 POSIX 限制)
使用 open
函数获取文件的描述符,成功则返回 fd , 否则返回 -1 。oflag
可以使用宏定义指定选项,例如 O_RDWR | O_CREAT
1 | #include <fcntl.h> |
使用 create
函数创建文件
1 | int creat(const char* path, mode_t mode); |
close
用于关闭文件,实际上进程终止时内核会自动释放所有打开的文件,并释放文件上的所有记录锁。
1 | int close(int fd); |
使用 lseek
为文件设置偏移量,下次读取文件会从这个位置开始读取数据;参数 where
有三个宏定义选项:
SEEK_SET
,将文件偏移量设置为 offset
SEEK_CUR
,将文件偏移量设置为 当前值加上 offset
SEEK_END
,将文件偏移量设置为 文件长度加上 offset
1 | off_t lseek(int fd, off_t offset, int where); |
有几点需要注意:
O_APPEND
选项的 open
打开的文件之外,偏移量都是 0 lseek
是否执行成功只能判断是否为 -1