相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
// main.cc
#include "utils.h"
#include <boost/asio.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/beast.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/file_body.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/parser.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp>
#include <cstdint>
#include <cstring>
#include <exception>
#include <filesystem>
#include <fstream>
#include <ios>
#include <iostream>
#include <limits>
#include <nlohmann/json.hpp>
#include <optional>
#include <ostream>
#include <string>
#include <string_view>
#include <taglib/fileref.h>
#include <taglib/flacfile.h>
#include <taglib/tag.h>
#include <taglib/tbytevector.h>

// file format:
// +----------------------------------+
// | magic header | 8 bytes
// +----------------------------------+
// | gap 1 | 2 bytes
// +----------------------------------+
// | encrypted-aes-key length | 4 bytes
// +----------------------------------+
// | encrypted-aes-key |
// +----------------------------------+
// | encrypted-meta-data length | 4 bytes
// +----------------------------------+
// | encrypted-meta-data |
// +----------------------------------+
// | gap 2 | 5 bytes
// +----------------------------------+
// | cover data length | 4 bytes
// +----------------------------------+
// | cover crc code | 4 bytes
// +----------------------------------+
// | cover data |
// +----------------------------------+
// | encrypted-audio-data |
// +----------------------------------+

// which stands for the length of 'neteasecloudmusic'.
constexpr size_t kLenNcmStr = 17;
// which stands for the length of '163 Key(Don't modify):'
constexpr size_t kLenMetaPrefix = 22;
// which stands for the length of 'music:'
constexpr size_t kLenJsonPrefix = 6;
constexpr size_t kLenMetaLenBuf = 4;
constexpr size_t kLenImageLenBuf = 4;
constexpr size_t kLenAudioChunk = 8 * 1024;
constexpr size_t kRc4SBoxSize = 256;
constexpr size_t kLenCoverCrc = 4;
constexpr size_t kLenEncAesKeyLen = 4;
constexpr size_t kLenMagicHeader = 8;
constexpr size_t kLenGap1 = 2;
constexpr size_t kLenGap2 = 5;
// field name in Json.
constexpr const char *kJsonFieldMusicName = "musicName";
constexpr const char *kJsonFieldArtist = "artist";
constexpr const char *kJsonFieldFormat = "format";
constexpr const char *kJsonFieldFormatFlac = "flac";
constexpr const char *kJsonFieldFormatMp3 = "mp3";
constexpr const char *kJsonFieldAlbum = "album";
constexpr const char *kJsonFieldAlbumPic = "albumPic";
constexpr const char *kJsonFieldAlias = "alias";
// aes key.
constexpr unsigned char kCoreAesKey[128] = {0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D,
0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E,
0x62, 0x61, 0x78, 0x57};
constexpr unsigned char kMetaAesKey[128] = {0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B,
0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30,
0x55, 0x3C, 0x27, 0x28};
// taglib constants.
// ref: https://taglib.org/api/.
constexpr const char *kTagLibPicture = "PICTURE";
constexpr const char *kTagLibData = "data";
constexpr const char *kTagLibPictureType = "pictureType";
constexpr const char *kTagLibPictureTypeFrontCover = "Front Cover";
constexpr const char *kTagLibMimeType = "mimeType";
constexpr const char *kTagLibMimeTypeJpeg = "image/jpeg";
// http.
constexpr const char *kDftCoverServerPort = "80";
constexpr int kDftHttpVersion = 11;
constexpr const char *kUserAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like "
"Gecko) Chrome/114.0.5735.199 Safari/537.36";
// misc.
constexpr const char *kUsage =
"usage: crack <audio_file_path>/<audio_files_dir> [--online]\n";
constexpr const char *kNcmSuffix = ".ncm";
constexpr const char *kOptionOnline = "--online";
constexpr const char *kOutputDir = "./ncmcrack_output";

struct AudioFileInfo {
std::string full_path;
std::string cover_url;
std::string album_name;
};

std::optional<AudioFileInfo> Crack(const std::string &path_input_file,
const std::string &path_output_folder) {
std::ifstream encrypted_file(path_input_file, std::ios::binary);
if (!encrypted_file) {
std::cerr << "failed to open file: " << path_input_file << "\n";
return std::nullopt;
}

encrypted_file.ignore(kLenMagicHeader + kLenGap1);

// get encrypted aes key length.
unsigned char key_len_buf[kLenEncAesKeyLen];
encrypted_file.read(reinterpret_cast<char *>(key_len_buf), kLenEncAesKeyLen);
int len_audio_key_aes_encrypted = static_cast<int>(*key_len_buf);

// get encrypted audio aes key.
std::unique_ptr<unsigned char[]> audio_key_aes_encrypted(
new unsigned char[len_audio_key_aes_encrypted]);
encrypted_file.read(reinterpret_cast<char *>(audio_key_aes_encrypted.get()),
len_audio_key_aes_encrypted);
for (size_t i = 0; i < len_audio_key_aes_encrypted; ++i) {
audio_key_aes_encrypted.get()[i] ^= 0x64;
}

// get the length of the meta, extract these 4 bytes in little endian.
unsigned char meta_len_buf[kLenMetaLenBuf];
encrypted_file.read(reinterpret_cast<char *>(meta_len_buf), kLenMetaLenBuf);
int len_meta = *reinterpret_cast<int *>(meta_len_buf);

// read and crack meta.
std::unique_ptr<unsigned char[]> meta(new unsigned char[len_meta]);
encrypted_file.read(reinterpret_cast<char *>(meta.get()), len_meta);
for (size_t i = 0; i < len_meta; ++i) {
meta.get()[i] ^= 0x63;
}

// base 64.
len_meta -= kLenMetaPrefix;
int max_len_meta_b64_decoded = len_meta * 3 / 4;
std::unique_ptr<unsigned char[]> meta_b64_decoded(
new unsigned char[max_len_meta_b64_decoded]);
int len_b64_bytes_decoded = 0;
if (!Base64Decode(meta.get() + kLenMetaPrefix, len_meta,
meta_b64_decoded.get(), max_len_meta_b64_decoded,
&len_b64_bytes_decoded)) {
std::cerr << "failed to decode b64 meta\n";
return std::nullopt;
}

// aes-ecb-128.
int len_meta_bytes_decrypted = 0;
std::unique_ptr<unsigned char[]> meta_aes_decrypted(
new unsigned char[len_b64_bytes_decoded]);
if (!AesEcb128Decrypt(meta_b64_decoded.get(), len_b64_bytes_decoded,
kMetaAesKey, meta_aes_decrypted.get(),
&len_meta_bytes_decrypted)) {
std::cerr << "failed to decrypt aes meta\n";
return std::nullopt;
}

encrypted_file.ignore(kLenGap2);

// get the image size.
std::unique_ptr<unsigned char[]> image_len_buf(
new unsigned char[kLenImageLenBuf]);
encrypted_file.read(reinterpret_cast<char *>(image_len_buf.get()),
kLenImageLenBuf);

// maybe image crc code.
encrypted_file.ignore(kLenCoverCrc);

encrypted_file.ignore(*reinterpret_cast<int *>(image_len_buf.get()));

// decode audio data with rc4.
// decode the encrypted aes key.
std::unique_ptr<unsigned char[]> audio_key_buf(
new unsigned char[len_audio_key_aes_encrypted]);
int len_audio_key_bytes_decrypted = 0;
unsigned char *audio_key = audio_key_buf.get();
if (!AesEcb128Decrypt(audio_key_aes_encrypted.get(),
len_audio_key_aes_encrypted, kCoreAesKey, audio_key,
&len_audio_key_bytes_decrypted)) {
std::cerr << "failed to decrypt audio key\n";
return std::nullopt;
}
audio_key = audio_key + kLenNcmStr;

// init rc4 sbox.
unsigned char s_box[kRc4SBoxSize];
Rc4KeySchedule(audio_key, len_audio_key_bytes_decrypted - kLenNcmStr, s_box);

// generate file name.
auto meta_json = nlohmann::json::parse(std::string_view(
reinterpret_cast<char *>(meta_aes_decrypted.get()) + kLenJsonPrefix,
len_meta_bytes_decrypted - kLenJsonPrefix));
auto music_format = meta_json[kJsonFieldFormat].get<std::string>();
auto artists = meta_json[kJsonFieldArtist];
std::string str_artists;
for (size_t i = 0; i < artists.size(); ++i) {
if (i) {
str_artists.append(",");
}
str_artists.append(artists[i][0].get<std::string>());
}
auto path_audio_file =
std::filesystem::path(path_output_folder) /
Utf8ToSysEncoding(str_artists + " - " +
meta_json[kJsonFieldMusicName].get<std::string>() +
"." + music_format);

// write audio data to local file.
std::ofstream audio_file(path_audio_file,
std::ios::out | std::ios::trunc | std::ios::binary);
if (!audio_file) {
std::cerr << "failed to create file: "
<< "\n";
return std::nullopt;
}
std::unique_ptr<unsigned char[]> audio_buf(new unsigned char[kLenAudioChunk]);
while (encrypted_file) {
encrypted_file.read(reinterpret_cast<char *>(audio_buf.get()),
kLenAudioChunk);
long bytes_read = encrypted_file.gcount();
if (bytes_read <= 0) {
std::cerr << "failed to read audio chunks\n";
return std::nullopt;
}

// rc4 decrypt.
Rc4CustomizedDecrypt(audio_buf.get(), bytes_read, s_box);

if (!audio_file.write(reinterpret_cast<char *>(audio_buf.get()),
bytes_read)) {
std::cerr << "failed to write audio data to file\n";
return std::nullopt;
}
}

#if defined(_WIN32) || defined(_WIN64)
AudioFileInfo audio_info{path_audio_file.string(),
meta_json[kJsonFieldAlbumPic].get<std::string>(),
meta_json[kJsonFieldAlbum].get<std::string>()};
return std::optional<AudioFileInfo>(audio_info);
#else
return std::optional<AudioFileInfo>(
AudioFileInfo{path_audio_file.string(),
meta_json[kJsonFieldAlbumPic].get<std::string>(),
meta_json[kJsonFieldAlbum].get<std::string>()});
#endif
}

bool CrackAndDownload(const std::string &path_encrypted_file,
const std::string &output_folder, bool download_cover) {
auto audio_file_info_opt = Crack(path_encrypted_file, output_folder);
if (!audio_file_info_opt) {
std::cerr << "failed to crack " << path_encrypted_file << "\n";
return false;
}
if (!download_cover) {
return true;
}
std::string host;
std::string target;
SplitUrl(audio_file_info_opt->cover_url, host, target);
// download。
using namespace boost;
beast::http::response<beast::http::string_body> res;
try {
beast::flat_buffer buffer;
// init connection.
asio::io_context ioc;
asio::ip::tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);
stream.connect(resolver.resolve(host, kDftCoverServerPort));
// assemble and send request.
beast::http::request<beast::http::string_body> req{beast::http::verb::get,
target, kDftHttpVersion};
req.set(beast::http::field::host, host);
req.set(beast::http::field::user_agent, kUserAgent);
beast::http::write(stream, req);
// read from peer.
beast::http::response_parser<beast::http::string_body> parser;
parser.body_limit(std::numeric_limits<std::uint64_t>::max());
beast::http::read(stream, buffer, parser);
res = std::move(parser.get());
if (res.result() != beast::http::status::ok) {
std::cerr << "http status error " << res.result() << "\n";
return false;
}
} catch (std::exception &e) {
std::cerr << "failed to download image from "
<< audio_file_info_opt->cover_url << "\n";
std::cerr << e.what() << "\n";
return false;
}
auto body = res.body();
// attach cover to audio file.
TagLib::FileRef file_ref(audio_file_info_opt->full_path.c_str());
if (file_ref.isNull() || !file_ref.tag()) {
std::cerr << "failed to open file: " << audio_file_info_opt->full_path
<< "\n";
return false;
}
file_ref.tag()->setAlbum(Utf8ToLatin1(audio_file_info_opt->album_name));
auto byte_vec_image = TagLib::ByteVector().setData(body.data(), body.size());
file_ref.setComplexProperties(
kTagLibPicture, {{{kTagLibData, byte_vec_image},
{kTagLibPictureType, kTagLibPictureTypeFrontCover},
{kTagLibMimeType, kTagLibMimeTypeJpeg}}});
if (!file_ref.save()) {
std::cerr << "failed to save file " << audio_file_info_opt->full_path
<< "\n";
return false;
}
return true;
};

// TODO: assume the system is little-endian for now.
int main(const int argc, char *argv[]) {
if (argc > 3 || argc < 2) {
std::cerr << kUsage;
return 1;
}
bool download_cover = false;
if (argc == 3) {
if (!strcmp(argv[2], kOptionOnline)) {
download_cover = true;
} else {
std::cerr << kUsage;
return 1;
}
}

if (!std::filesystem::exists(kOutputDir)) {
std::filesystem::create_directory(kOutputDir);
}

if (std::filesystem::is_directory(argv[1])) {
bool error = false;
for (const auto &entry : std::filesystem::directory_iterator(argv[1])) {
if (entry.is_regular_file()) {
const auto &fs_path_file = entry.path();
if (auto filename = fs_path_file.filename().string();
filename.size() > 4 &&
filename.substr(filename.size() - 4) == kNcmSuffix) {
error = CrackAndDownload(fs_path_file.string(), kOutputDir,
download_cover);
};
}
}
return error;
}

return CrackAndDownload(argv[1], kOutputDir, download_cover);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// utils.cc
#include <boost/locale.hpp>
#include <boost/locale/encoding.hpp>
#include <iostream>
#include <openssl/evp.h>

// return true if successful.
bool AesEcb128Decrypt(const unsigned char *input, const int len_input,
const unsigned char *key, unsigned char *output,
int *len_bytes_decrypted) {
// Use a unique_ptr with a custom deleter to manage EVP_CIPHER_CTX.
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx(
EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);
if (!ctx) {
std::cerr << "failed to create EVP_CIPHER_CTX.\n";
return false;
}
if (EVP_DecryptInit(ctx.get(), EVP_aes_128_ecb(), key, nullptr) != 1) {
std::cerr << "failed to initialize decryption context.\n";
return false;
}
// decrypt most of the blocks.
if (EVP_DecryptUpdate(ctx.get(), output, len_bytes_decrypted, input,
len_input) != 1) {
std::cerr << "failed to update decryption.\n";
return false;
}
// decrypt the last block.
int final_len = 0;
if (EVP_DecryptFinal_ex(ctx.get(), output + *len_bytes_decrypted,
&final_len) != 1) {
std::cerr << "failed to finalize decryption. Check padding or key.\n";
return false;
}
*len_bytes_decrypted += final_len;
return true;
}

// return true if successful.
bool Base64Decode(const unsigned char *input, const int input_len,
unsigned char *output, const int max_len_output,
int *len_bytes_decoded) {
BIO *bio = BIO_new_mem_buf(input, input_len);
BIO *base64 = BIO_new(BIO_f_base64());
bio = BIO_push(base64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
*len_bytes_decoded = BIO_read(bio, output, max_len_output);
BIO_free_all(bio);
return *len_bytes_decoded >= 0;
}

void Rc4KeySchedule(const unsigned char *key, const size_t key_len,
unsigned char *s_box) {
for (int i = 0; i < 256; ++i) {
s_box[i] = static_cast<unsigned char>(i);
}
int j = 0;
for (int i = 0; i < 256; ++i) {
j = (j + s_box[i] + key[i % key_len]) % 256;
unsigned char temp = s_box[i];
s_box[i] = s_box[j];
s_box[j] = temp;
}
}

// It should be noted that this is not a standard rc4 process.
void Rc4CustomizedDecrypt(unsigned char *data, const long data_len,
const unsigned char *key_box) {
for (int k = 1; k <= data_len; ++k) {
unsigned char j = k & 0xff;
data[k - 1] ^=
key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff];
}
}

#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#endif
std::string Utf8ToSysEncoding(std::string utf8_bytes) {
#if defined(_WIN32) || defined(_WIN64)
LCID lcid = GetSystemDefaultLCID();
switch (lcid) {
case 0x0804:
return boost::locale::conv::from_utf<char>(utf8_bytes, "GBK");
default:
break;
}
#endif
// do nothing in both apple and linux.
return utf8_bytes;
}

std::string Utf8ToLatin1(const std::string &gbk_bytes) {
return boost::locale::conv::from_utf<char>(gbk_bytes, "ISO-8859-1");
}

void SplitUrl(const std::string &url, std::string &host, std::string &target) {
size_t protocol_pos = url.find("://");
if (protocol_pos != std::string::npos) {
protocol_pos += 3;
} else {
protocol_pos = 0;
}
size_t path_pos = url.find('/', protocol_pos);

if (path_pos != std::string::npos) {
host = url.substr(protocol_pos, path_pos - protocol_pos);
target = url.substr(path_pos);
} else {
host = url.substr(protocol_pos);
target = "/";
}
}

摘要

  • CUDA (Compute Unified Device Architecture) 是 N 卡上的计算框架,在这个框架上可以装载 cuDNN 或者 TensorRT 进行深度学习模型的加速。前者主要用于模型训练的加速,而后者适用于部署时模型推理的加速。

  • 这篇文章主要记录 LinuxCUDAcuDNN 以及 TensorRT 的安装;

CUDA

  • 先去 CUDA 的页面 进行安装,另外一提 /etc/issue 中记录了当前 Linux 的发行版本,可以对照选择下载连接。因为是在 docker 中运行这个 CUDA 环境,所以不需要安装驱动,而是依靠物理主机 ( 我的 Windows ) 上的驱动 。在 **x86 > Debian 12 > deb (local) ** 的选择下有了这些指令

    1
    2
    3
    4
    5
    6
    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
    sudo dpkg -i cuda-repo-debian12-12-3-local_12.3.1-545.23.08-1_amd64.deb
    sudo cp /var/cuda-repo-debian12-12-3-local/cuda-*-keyring.gpg /usr/share/keyrings/
    sudo add-apt-repository contrib # 这个指令可能需要先 apt install software-properties-common
    sudo apt-get update
    sudo apt-get -y install cuda-toolkit-12-3

cuDNN

  • 官方给出了 完整说明,在 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
    2
    3
    sudo cp cudnn-linux-x86_64-8.9.7.29_cuda12-archive/include/cudnn*.h /usr/local/cuda/include
    sudo cp -P cudnn-linux-x86_64-8.9.7.29_cuda12-archive/lib/libcudnn* /usr/local/cuda/lib64
    sudo chmod a+r /usr/local/cuda/include/cudnn*.h /usr/local/cuda/lib64/libcudnn*

摘要

这篇文章继续记录我与 C++ 生态打架的精彩记录,主要记录一些配置文件的编写,便于日后参考第三方库的安装。

安装 vcpkg

与终端交互中完成如下配置(刚装的系统会有一些基础依赖需要按照报错提示自行安装)

1
2
3
4
5
6
7
# 下载并初始化
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg && ./bootstrap-vcpkg.sh
# 设置环境变量并刷新
export VCPKG_ROOT=/path/to/vcpkg # 这里要自己替换路径, 个人喜欢放到 ~/.bashrc 中永久保存
export PATH=$VCPKG_ROOT:$PATH
source ~/.bashrc

开始使用 vcpkg

  1. 这里参考官方的例子先给出基本的 CMakeLists.txt 文件和 main.cc 文件

    CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    cmake_minimum_required(VERSION 3.10)

    project(HelloWorld)

    find_package(fmt CONFIG REQUIRED)

    add_executable(Main main.cc)

    target_link_libraries(HelloWorld PRIVATE fmt::fmt)

    main.cc

    1
    2
    3
    4
    5
    6
    #include <fmt/core.h>

    int main() {
    fmt::print("Hello World!\n");
    return 0;
    }
  2. 由于网络时不时会遭受神秘力量的攻击, 这里可以通过设置环境变量更换镜像源

1
2
export X_VCPKG_ASSET_SOURCES=x-azurl,http://106.15.181.5/
source ~/.bashrc
  1. 为了能让 CMake 工具链加入 vcpkg,需要设置 CMKAE_TOOLCHAIN_FILE 这个值,官网给出了三种方法
  • 在 CMakePresets.json 中进行设置
1
2
3
4
5
6
7
8
9
10
11
12
{
"version": 3,
"configurePresets": [
{
"name": "default",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
}
}
]
}
  • 在 CMakeLists.txt 中进行设置,不过要写在 project() 这一行之前
1
set(CMAKE_TOOLCHAIN_FILE "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
  • 在使用cmake指令进行工程构建的时候加入 -D 选项
1
cmake -DCMAKE_TOOLCHAIN_FILE="$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
  1. 在希望使用 vcpkg 的项目中使用如下指令初始化 vcpkg 管理
1
vcpkg new --application
  1. 之后可以使用如下命令进行依赖的添加,比如我这里需要添加 fmt 库,回车之后会发现 vcpkg.json 中多了这一项依赖
1
vcpkg add port fmt 
  1. 之后就可以使用 cmake 指令进行构建编译了,加上 --preset 选项设置配置文件为上述 CMakePresets.json 中的 default 配置
1
2
3
mkdir build && cd build
cmake --preset=default ..
cmake --build .
  1. 构建成功即可运行(实际上大概率遇到一堆小问题,这里就即兴发挥了 :happy:
1
2
./Main
# Hello World!

  • 实际上 vcpkg 安装包有两种模式:classicmanifest (上文使用的就是这种方式,也是官方推荐的 )

    • manifest: 相当于每次安装库的位置都是项目 build 文件夹内

    • classic: 相当于全局安装,库安装在在 VCPKG_ROOT的文件夹中;在设置了 vcpkg 工具链的项目中, 每次安装都会从这个全局目录中寻找缓存,大大加快安装速度;该模式安装使用另一条指令

      1
      vcpkg install <lib> 
  • 另外贴上 vcpkg 页面 https://learn.microsoft.com/en-us/vcpkg/

前言

偶然发现了 TechEmpower 这个后端框架评分网站,发现了一位重量级选手,2020年综合评分的冠军 Drogon,而这两年 Just.jsRust 的两个框架霸榜,某 ++ 语言只能靠边观摩。今年更是因为测试不规范,被抬出了party。今天发现之前装框架的时候忘记装数据库接口了,这次干脆给自己写个 Dockerfile 打造一个专用的开发环境,也方便之后参考。

镜像源

我把我的镜像上传到了 Docker Hub,有缘人可以拉下来玩。当然我一般还会自己配置 sshVSCode 开发,这里为了整洁就不将配置写进去了。

1
docker pull azusaing/drogon:basic # 解压后 1.79 GiB

菜谱

花了我 3 分钟 build 的菜谱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# +----------------------------------+
# | @author: azusaings@gmail.com |
# | @date: 2023-11-25 |
# | @version: 0.0.1 |
# +----------------------------------+

FROM debian:latest
WORKDIR /root
RUN apt update && \
# basic tools
apt install -y \
git \
gcc \
g++ \
cmake \
valgrind \
ssh \
vim \
wget && \
# drogon dependencies
apt install -y \
libjsoncpp-dev \
uuid-dev \
zlib1g-dev \
openssl \
libssl-dev \
postgresql-server-dev-all \
default-libmysqlclient-dev \
libsqlite3-dev \
libhiredis-dev && \
# drogon
git clone https://github.com/drogonframework/drogon && \
cd drogon && \
git submodule update --init && \
mkdir build && \
cd build && \
cmake .. && \
make -j $(nproc) && \
make install && \
cd ../.. && \
rm -rf drogon/ && \
# curl
wget https://github.com/curl/curl/releases/download/curl-8_4_0/curl-8.4.0.tar.gz && \
tar -xzvf curl-8.4.0.tar.gz && \
cd curl-8.4.0 && \
./configure --with-openssl --prefix=/usr/ && \
make -j $(nproc) && \
make install && \
cd .. && \
rm -rf curl-8.4.0/ curl-8.4.0.tar.gz && \
apt remove -y wget && \
apt autoclean

前言

最近在学 CMakeUNIX ,不幸被老师抓去做 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 中愉快地写代码,需要在扩展安装 CMakeCMakeToolsC\C++ 这三个插件,以便识别 CMake 工程和代码提示;喜欢终端可以直接跳过这一项

  • 附加地,如果需要显卡计算,电脑上一定要先安装好 CUDAcuDNN 库,并且在下文的下载步骤中选择好对应 CUDA 版本的库下载

Linux 环境

  1. 在官网 https://pytorch.org/get-started/locally/ 根据交互界面获取分发地址,下载并解压

    1
    2
    3
    # 我的 CUDA 版本是 11.8, pre 代表预览版本
    curl -O https://download.pytorch.org/libtorch/cu118/libtorch-shared-with-deps-2.1.0%2Bcu118.zip
    unzip libtorch-shared-with-deps-2.1.0%2Bcu118.zip
  2. 编写 CMakeLists.txt 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    cmake_minimum_required(VERSION 3.10)
    project(example-app)

    find_package(Torch REQUIRED)
    set(CMAKE_CXX_FLAGS, "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}")

    add_executable(example-app example-app.cpp)

    target_link_libraries(example-app "${TORCH_LIBRARIES}")
    set_property(TARGET example-app PROPERTY CXX_STANDARD 17)

    # target_include_directories(example-app PUBLIC "${TORCH_INCLUDE_DIRS}")

    # The following code block is suggested to be used on Windows.
    # According to https://github.com/pytorch/pytorch/issues/25457,
    # the DLLs need to be copied to avoid memory errors.
    if (MSVC)
    file(GLOB TORCH_DLLS "${TORCH_INSTALL_PREFIX}/lib/*.dll")
    add_custom_command(TARGET example-app
    POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
    ${TORCH_DLLS}
    $<TARGET_FILE_DIR:example-app>)
    endif (MSVC)
  3. 编写 example-app.cpp 检查环境是否配置正确,我在官方给出代码的基础上多加了一句对 CUDA 的检查

    1
    2
    3
    4
    5
    6
    7
    8
    #include <torch/torch.h>

    int main() {
    torch::Tensor tensor = torch::rand({2, 3});
    std::cout << tensor << std::endl;
    std::cout << std::boolalpha << torch::cuda::is_available() << std::endl;
    return 0;
    }
  4. 编译

  • 官方给出的方法

    1
    2
    # 设置下载的 libtorch 文件夹绝对路径
    cmake .. -DCMAKE_PREFIX_PATH="absolute/path/to/libtorch"
  • 如果嫌麻烦也可以直接把路径写在 CMakeLists.txt 里,或者在 VSCodeCMake 插件中设置附加生成选项。比如前者可以这么写:

    1
    2
    3
    set(CMAKE_PREFIX_PATH "absolute/path/to/libtorch" )
    # 或者把路径加入环境变量(我取名为 LIBTORCH),用如下方式调用
    set(CMAKE_PREFIX_PATH "$ENV{LIBTORCH}")
  • 接着就可以直接使用下面指令生成了,或者可以通过 CMake 插件的按钮生成或者运行

    1
    cmake --build .

Windows 环境

  1. 在官网 https://pytorch.org/get-started/locally/ 根据交互界面获取分发地址,直接点击下载;

    1
    2
    # 安装 CPU 的 release 版本; Debug 版本会带有调试信息,但编译会稍慢。
    https://download.pytorch.org/libtorch/cpu/libtorch-win-shared-with-deps-2.1.0%2Bcpu.zip
  2. 接着我们可以使用和 Linux 环境下一样的源文件和 CMakeLists.txt

  3. 编译方法与 Linux 环境下一样

VSCode 代码提示

  • 按照以上的步骤安装完之后会发现 VSCode 无法给出代码提示,于是我们就需要配置 .vscode/c_cpp_properties.json 文件, 在 "includePath" 中添加头文件的位置,VSCode 会检查这个文件并进行代码提示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "configurations": [{
    "name": "Linux",
    "includePath": [
    "${workspaceFolder}/**",
    "/absolute/path/to/libtorch/**"
    ]
    }],
    "version": 4
    }
  • 如果在 Windows 环境下这里可以加入环境变量简化路径,在上文 Windows 环境下我配置了 LIBTORCH 这个环境变量,于是可以把头文件位置写成:

    1
    "${env:LIBTORCH}/**"  

  • 如果用 CMake 插件的按钮进行生成出错,可以用在 CMakeLists.txt 中使用 message(${CMAKE_PREFIX_PATH}) 检查环境变量是否正确导入。如果没有大概是因为 VSCode 保存着先前的环境变量。可以重启 VSCode,删除 build 文件夹重新生成 。(可见命令行的重要性 :XD)

1. 调制

1.1 文件描述符限制调整

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 使设置生效

1.2 内核参数设置

内核的参数大部分都可以在 /proc/sys 中查看,使用 sysctl -a 可以看到所有的内核参数。接下来我们关注几个重要的参数

  • /proc/sys/fs :其下保存着文件系统有关的信息

    • **/file-max **:系统级文件描述符限制
    • /epoll/max_user_watches :所有打开的 epoll 内核事件最大值
  • /proc/sys/net

    • /core/somaxconn :设置 Established 状态的 socket 的最大值
    • /ipv4/tcp_max_syn_backlog : 与上者类似,但是包括了 SYN_RCVD 状态的 socket
    • /ipv4/tcp_wmen:指定一个 sockettcp 写缓冲区的最小值,默认值与最大值
    • /ipv4/tcp_rmen:指定一个 sockettcp 读缓冲区的最小值,默认值与最大值
    • /ipv4/tcp_syncookies:代表通过对 syn 的缓存,防止同一地址的重复同步报文,导致 listen 监听队列溢出

2. gdb调试

2.1 多进程调试的方法

  1. 指定进程调试,在 gdb 中指定 [PID] 后打断点进行调试

    1
    (gdb) attach [PID] 
  2. 设置在 fork 调用之后是否跟随子进程(设置 [parent] 或者 [child]

    1
    (gdb) set follow-fork-mode [parent/child] 

2.2 多线程调试

  • 有以下指令可以帮助调试线程

    1
    2
    3
    (gdb) info threads
    (gdb) thread [ID] // 调试 [ID] 线程
    (gdb) set scheduler-locking [off/on/step]

    注意第三个指令 off 为默认值,on 表示只运行当前线程,step 表示单步调试的时候只有当前线程能执行

3. 压力测试

如何进行压力测试?

  • 压力测试程序有多种实现方法,其中等待服务器响应时用 I/O 可以让测试服务器施加尽可能多的压力,而不是浪费过多 CPU 资源在处理响应连接上
  • 另外可以通过一些常见的测试程序进行测试
    • Apache JMeter
    • LoadRunner
    • BlazeMeter
    • Gatling

4. 系统监测工具

  • tcpdump

    • 可以指定类型,方向或协议进行过滤

      • 类型(包括 nethostport 等)

        1
        tcpdump net 192.168.31.1/24
      • 方向 ( srcdest

        1
        tcpdump dest port 8080
      • 协议( 比如 icmp

        1
        tcpdump icmp
    • 表达式之间可以进行逻辑运算(andornot),复杂的表达式用单引号包裹,内部可以使用括号分组

      1
      tcpdump 'src 192.168.31.217 and (dest port 8080 or 22)'
    • 同时支持对包内部的信息进行过滤,比如过滤得到 syn 报文。因为报文的第 14 个字节的第 2 位正是 syn 标志

      1
      2
      # 格式 -> tcpdump 'tcp[BYTES] & [NUMBER] != 0' 
      tcpdump 'tcp[13] & 2 != 0'
    • 同时支持许多选项

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      -n # 使用IP地址表示主机名
      -i # 指定要监听的网口, 可以使用 -i any
      -v # 显示更详细的信息, 比如显示 tcp 中的 TTL 和 TOS 信息
      -t # 不打印时间戳
      -e # 显示以太网帧头部
      -X # 十六进制显示数据以及对应的 ASCII 字符
      -s # 设置抓包截取长度
      -S # 以绝对值显示 tcp 报文段序号
      -w # 将数据重定向某个文件
      -r # 从文件读取数据
  • lsof

    list open file

    • -i 选项,查看套接字

      1
      2
      3
      # lsof -i [IP_PROTO] [TRANS_PROTO][@HOSTNAME/IP_ADDR]:[SERVICE/PORT]
      # 示例
      lsof -i@192.169.31.217:80
    • -u 显示打开的所有套接字

      1
      lsof -u
    • -c 显示指定程序打开的套接字

      1
      lsof -c ssh
    • -p 显示指定进程打开的套接字

      1
      lsof -p [PID] 
    • -t 显示打开了套接字的进程

  • nc

    netcatdebian 中安装可以使用

    1
    apt install ncat

    之后我们就可以开始使用这个指令,有如下选项

    1
    2
    3
    4
    nc 	[HOSTNAME] [PORT] 		# 以客户端模式启动
    nc -l [PORT] # 以服务端启动, 并监听指定端口 [PORT]
    nc -lk [PORT] # 在 -l 基础上加上 -k 选项可以重复接受连接
    nc -C # 使用 CRLF 作为行结束符
  • strace

    跟踪系统调用以及发送的信号

    1
    2
    3
    4
    -c 	# 统计系统调用的执行时间、次数和出错次数
    -f # 跟踪由 fork 产生的子进程
    -t # 附加时间信息
    -e # 指定表达式, 详细见下文

    -e 选项中被指定的表达式形式如下

    1
    [QUALIFIER]=[!][VALUE1], [!][VALUE2], ... # `!` 代表取反
    • [qualifier] 有如下几种常见值
      • trace
      • signal
      • readwrite
    • [VALUE] 有如下一些取值
      • file
      • process
      • network
      • ipc
      • signal (或者直接指定特定信号值,比如 SIGINT )

    接下来是一些使用 -e 选项的示例

    1
    2
    3
    4
    5
    6
    strace -e trace=set 		# set代表了 open, write, read, close 四种简单调用
    strace -e trace=file
    strace -e trace=network
    strace -e trace=signal
    strace -e read=3, 5 # 输出从文件描述符读到的数据
    strace -e signal=SIGINT
  • netstat

    网络统计工具,可以统计网卡上的全部连接(相当于 lsof)以及路由表和网卡等信息。

    1
    2
    3
    4
    5
    6
    -n 	# 使用 IP 号表示主机
    -a # 统计的结果中包含监听套接字
    -r # 显示路由信息
    -i # 显示网卡接口的数据流量
    -c # 每隔一秒输出一次
    -p # 显示 socket 所属的进程的 PID
  • vmstat

    显示系统资源的指令

    1
    2
    3
    4
    5
    6
    7
    -f 		 # 显示自启动以来系统 fork 的次数
    -s # 显示内存相关信息以及系统活动信息
    -d # 显示磁盘相关信息
    -p # 统计分区信息
    -S # 指定单位显示
    [delay] # 设置采样间隔秒数, 直接替换 [delay]
    [count] # 设置统计次数, 直接替换 [count]
  • ifstat

    检测流量速度

    1
    2
    3
    4
    -a # 检测系统上所有网卡接口
    -i # 指定网卡接口
    -t # 加上时间戳
    -b # 以 bit/s 作为单位
  • mpstat

    mpmulti-processor , 用于监控 CPU 状况;下载 sysstat 包含这个指令

    1
    # 格式 -> mpstat -[CPU_ID | ALL] [INTERVAL] [COUNT]

    比如我们可以这样使用

    1
    mpstat -ALL 1 10 # 1s 输出一次, 总共十次

0. 前言

1. 安装

  • github 拉取,并安装前置 openssl

    1
    2
    git clone https://github.com/libevent/libevent.git 
    sudo apt-get install libssl-dev libmbedtls-dev # libevent 依赖于 openssl
  • CMake + gcc 12.2.0 的环境下进行编译

    1
    2
    3
    4
    5
    cd libevent
    mkdir build && cd build
    cmake -G "Unix Makefiles" .. # 生成 makefile
    make
    make verify # 可以跳过的测试步骤
  • 接着将当前文件夹下的 lib 中的库移动到 /usr/lib 下,include 中的文件移动到 /usr/include;源代码根目录下的 include 移动到 /usr/include 中。

  • 接着可以在 CMakeLists.txt 中引用,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cmake_minimum_required(VERSION 3.0.0)
    project(Test VERSION 0.1.0 LANGUAGES C CXX)

    include_directories(/usr/include/libevent) # 我把libevent文件放在了单独的文件夹中
    link_directories(/usr/lib/libevent)

    add_executable(Main main.cc)

    target_link_libraries(Main
    event
    )
  • 我们可以使用以下函数检查当前环境 Libevent 支持的后端方法以及版本

    1
    2
    const char** event_get_supported_methods(void);
    const char** event_get_version();

2. 基本方法

2.1 初始化

  • libevent 中通过下列方法初始化和清理一个 reactor 模型

    1
    2
    struct event_base* event_base_new();
    void event_base_free(struct event_base *base);
  • 如果需要更多客制化,也可以通过 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
        2
        3
        4
        // 初始化
        struct event_config *event_config_new(void);
        // 已经传入 event_base 实例的 event_config 实例可以直接释放
        void event_config_free(struct event_config *cfg);
      • 设置不需要的方法,比如可以传入 "epoll"

        1
        2
        // 1. 设置不需要的方法, 比如可以传入"epoll"
        int event_config_avoid_method(struct event_config *cfg, const char *method);
      • 让 event 保证支持下列参数

        1
        2
        3
        4
        5
        6
        7
        8
        // 2. 让 libevent 保证支持以下参数的一种或多种 (可以进行‘或’运算进行组合)
        enum event_method_feature {
        EV_FEATURE_ET = 0x01,
        EV_FEATURE_O1 = 0x02, // 保证增删事件复杂度 O(1)
        EV_FEATURE_FDS = 0x04, // 支持除了 socket 以外的普通文件描述符
        };
        int event_config_require_features(struct event_config *cfg,
        enum event_method_feature feature);
      • 更多运行时参数

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        // 3. 更多运行时参数
        enum event_base_config_flag {
        EVENT_BASE_FLAG_NOLOCK = 0x01,
        EVENT_BASE_FLAG_IGNORE_ENV = 0x02,
        EVENT_BASE_FLAG_STARTUP_IOCP = 0x04,
        EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
        EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10,
        EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
        };
        int event_config_set_flag(struct event_config *cfg,
        enum event_base_config_flag flag);
      • 控制线程使用的 CPU 数,但是只有 WindowsIOCP 支持

        1
        int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus);
      • *预防 Priority Inversion

        1
        2
        3
        int event_config_set_max_dispatch_interval(struct event_config *cfg,
        const struct timeval *max_interval, int max_callbacks,
        int min_priority);
  • 对于初始化的模型,可以设置 event_base 的优先级并检查

    1
    2
    int event_base_priority_init(struct event_base *base, int n_priorities);
    int event_base_get_npriorities(struct event_base *base);
  • 检查某个初始化的 event_base 结构体使用的方法

    1
    2
    const char *event_base_get_method(const struct event_base *base);
    enum event_method_feature event_base_get_features(const struct event_base *base);
  • 对于 fork 产生的子进程中的 event_base,需要使用以下函数进行重新初始化

    1
    int event_reinit(struct event_base *base);

2.2 事件注册

2.2.1 一般的注册方式
  • 对于一个事件有多个状态,根据官方文档

    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
      2
      3
      4
      5
      6
      #define EV_TIMEOUT      0x01   // 经过一定时间之后事件变为 Activated 状态
      #define EV_READ 0x02
      #define EV_WRITE 0x04
      #define EV_SIGNAL 0x08
      #define EV_PERSIST 0x10 // 被激活后仍然保持 Pending 状态
      #define EV_ET 0x20
    • cbarg,回调函数以及其传入参数

    1
    2
    3
    4
    5
    6
    7
    typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

    struct event *event_new(struct event_base *base, evutil_socket_t fd,
    short what, event_callback_fn cb,
    void *arg);

    void event_free(struct event *event);
  • 根据文档,创建的事件将会处于 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);
2.2.2 便捷的函数

为了方便使用,针对特定事件类型,Libevent 设置特殊的宏定义或者函数便于使用。如下是两个特化的事件注册函数

  • 定时事件

    1
    2
    #define evtimer_new(base, callback, arg) \
    event_new((base), -1, 0, (callback), (arg))
  • 信号事件,注意一个进程只能有一个信号事件(详细请查阅操作系统原理)

    1
    2
    #define evsignal_new(base, signum, cb, arg) \
    event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)

2.3 设置事件监听

当我们注册好要监听的事件以及属性之后,我们便可以开始监听事件。前者类似于在内核事件表中注册事件,后者则更像是调用 epoll_wait 进行监听

2.3.1 开启监听
  • 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);
2.3.2 关闭监听

一般的事件监听 (没有设置 EVLOOP_NO_EXIT_ON_EMPTY 的前提下) 都会在就绪事件队列为空的时候主动退出,但假如我们想在队列中还有事件的时候就强行退出,可以使用以下两个函数;与上述开启监听的两个函数类似,这两个函数只是调用接口略有不同

  • event_base_loopexit :设置一段时间之后退出

    1
    2
    int event_base_loopexit(struct event_base *base,
    const struct timeval *tv);
  • event_base_loopbreak :相当于 event_base_loopexit(struct event_base *base, NULL) ,立即退出

    1
    int event_base_loopbreak(struct event_base *base);	
  • 通过下列两个函数检查事件监听是否正常退出

    1
    2
    int event_base_got_exit(struct event_base *base);
    int event_base_got_break(struct event_base *base);

3. 进阶方法

1. 概述

  • I/O 复用可以使一个进程监听多个 I/O 事件 (包括基本的读写事件) 的发生,而不是每个事件都对应一个阻塞线程,以提升程序的 I/O 效率。可以设置:当可读事件发生时,函数返回;

    • 可读事件包含以下几种:

      • 内核缓冲区字节数大小大于或等于其低水位标记 SO_RCVLOWAT (前文提到默认为 1 byte

      • socket 连接被关闭

      • 监听 socket 上有新的连接请求

    • 可写事件包含以下几种:

      • 发送缓冲区字节大于或等于 SO_SNDLOWAT
      • socket 上的写操作被关闭
      • socket 使用非阻塞的 connect 连接成功或超时、
      • socket 上有未处理的错误。可以使用 getsockopt 读取和清除该错误

2. Linux 库中的 I/O 复用

2.1 select

  • 前三项表示对 异常 事件的监听,fd 按照 fd_set 结构传入,监听的事件被触发时,函数返回并将传入的 fd 修改后从内核复制到用户区。函数成功调用返回 就绪的文件描述符数量 ,若超时则返回 0 ,如果在等待期间接收到信号则返回 -1 并设置 errnoEINTR

    1
    2
    #include <sys/select>
    int select(int nfds, fd_set* rdfds, fd_set* wrfds, fd_set* excpfds, struct timeval* timeout);
  • fd_set 的结构在此不进行深究,其内部使用 bitmap 。可以用以下几组函数设置

    1
    2
    3
    4
    5
    #include <sys/select.h>
    void FD_ZERO(fd_set* fdset); // 清除所有fd位
    void FD_CLR(int fd, fd_set* fdset); // 清除指定fd位
    void FD_SET(int fd, fd_set* fdset);
    int FD_ISSET(int fd, fd_set* fdset); // 判断指定fd位是否设置
  • 使用 select 的时候可以把监听 fd 放入 fd 集合中一起处理,之后借助上述几个函数判断是什么事件使 select 返回,再逐一处理。注意内核会修改传入的 fd 集合,每次使用 select 的时候都应该清除触发的 fd 位,不然会导致同一个事件重复触发。

2.2 poll

  • 与上述 select 函数略有不同,这次 fd 的触发状态不再通过 fd_set 结构进行传递,而是通过 pollfd 类型的数组进行传递。而 n_fd_t 其实是 unsigned long 的封装,代表 fd 的数目。同时注意 timeout 参数类型不同。不过返回值逻辑与 select 相同

    1
    2
    3
    4
    5
    6
    7
    8
    #include <poll.h>
    int poll(struct pollfd* fds, n_fds_t nfds, int timeout);

    struct pollfd {
    int fd;
    short events;
    short revents; // 这里会被内核修改
    }

2.3 epoll

  • epoll 的实现和用法与上述两个函数十分不同,它无需全量传入监听事件给内核,而是将事件放在一个额外的内核事件表中,所以需要多一个文件描述符表示内核事件表

    1
    2
    #include <sys/epoll.h>
    int epoll_create(int size); // size 只是给内核的一个提示
  • 之后就可以向内核事件表中注册事件

    1
    2
    #include <sys/epoll.h>
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
    • 其中 op 指定操作类型,代表对于内核事件表的操作

      • EPOLL_CTL_ADD
      • EPOLL_CTL_MOD
      • EPOLL_CTL_DEL
    • event 用于指定事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      struct epoll_event {
      __uint32_t events; // 事件类型, EPOLLIN, EPOLLET, EPOLLONESHOT 等
      epoll_data_t data; // 存储返回数据的联合体, 一般内部的 fd 用的最多
      };

      typedef struct union epoll_data {
      void* ptr;
      int fd;
      uint32_t u32;
      uint64_t u64;
      } epoll_data_t;
  • 设置完事件之后就可以像前两种 I/O 复用函数一样调用,返回值逻辑同于上两种 I/O 复用函数。注意这里的 events 需要提供一个缓冲区供内核存放事件。

    1
    int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • LTET 的区别在于后者只会通知一次。另外注册了 EPOLLONESHOT 的文件描述符,只会被触发一次 (不管是什么事件),事件处理完后要重新注册以便于收到下一次通知。

2.4 三个 I/O 复用函数的比较

  • selectpoll 都需要不断地将监听事件传入内核再等待其写回用户空间, epoll 只需要注册一次,但是需要额外打开一张内核事件表。
  • epoll 内核中监听事件靠的是回调函数实现,同时只向用户空间传回被触发的事件,时间复杂度 O(1)
  • select 监听 fd 最大值的典型值为 1024,后二者一般为能打开的最大文件描述符数。
  • 前二者只有 LT 模式,而 epoll 还可以支持 ET

3. I/O 复用使用场景

  • 对于非阻塞的 connect, 连接失败会产生可写事件。这时就可以用上述 I/O 复用函数进行监听,接着用 getsockopt 读取错误码并清除错误。

1. 系统日志

1
2
3
4
5
6
7
8
#include <syslog.h>
void syslog (int priority, const char* message);

void openlog(const char* ident, int logopt, int facility); // 比起 syslog() 提供更详细的参数选择

int setlogmask

void closelog();

2. 查看与设置系统资源限制

  • struct rlimt 结构体存储着资源限制值,超过其值时内核分别对应发送 SIGXFSZSIGXCPU 信号

    1
    2
    3
    4
    struct rlimit {
    rlim_t rlim_cur; // 建议值
    rlim_t rlim_max; // 硬性要求最大值
    }
  • 通过以下函数查看与设置:

    1
    2
    3
    #include <sys/resource.h>
    int getrlimit(int resource, struct rlimit* rlim);
    int setrlimit(int resource, struct rlimit* rlim);
  • 另外可以通过 shell 命令 ulimit 设置当前shell 环境下资源限制,对当前 shell 启动的所有后续进程有效

3. 改变当前进程的工作目录与根目录

  • 查看与修改工作目录

    1
    2
    3
    #include <unistd.h>
    char* getcwd(char* buf, size_t size);
    int chdir(const char* path);
  • 修改根目录

    1
    2
    #include <unistd.h>
    int chroot(const char* path);

4. 服务器程序后台化

  • 直接使用 Linux 提供的库函数,设置 nochdirfalse 会改变当前 工作目录noclose 代表是否关闭原来的标准输入输出以及错误流

    1
    2
    #include <unistd.h>
    int daemon(int nochdir, int noclose);
  • 手动实现,从父进程 fork 出子进程之后将父进程退出,将输入输出以及错误流都重定向到 /dev/null

1. 常用的文件操作符

包括 open, read, write, lseek, close

  • 标准输入输出和错误流有 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO 等宏定义可以使用

  • 一个进程的文件描述符最大值小于 OPEN_MAX ( 见 POSIX 限制)

  • 使用 open 函数获取文件的描述符,成功则返回 fd , 否则返回 -1oflag 可以使用宏定义指定选项,例如 O_RDWR | O_CREAT

    1
    2
    3
    4
    #include <fcntl.h>
    int open(const char* path, int oflag, ...);

    int openat(int fd, const char* path, int oflag, ...);
  • 使用 create 函数创建文件

    1
    2
    int creat(const char* path, mode_t mode);
    // 实际上这个函数等价于 open(path, O_RDWR|O_CREAT|O_TRUNC, 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
    • 另外不可打开管道或者套接字,否则返回 -1
    • 偏移量可以为负值,所以判断 lseek 是否执行成功只能判断是否为 -1
    • 偏移量大于文件长度则会形成空洞,但空洞的数据不分配内存
0%