-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeed.json
More file actions
839 lines (839 loc) · 984 KB
/
feed.json
File metadata and controls
839 lines (839 loc) · 984 KB
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
{
"version": "https://jsonfeed.org/version/1",
"title": "Peihao Yang",
"home_page_url": "https://forsworns.github.io//",
"feed_url": "https://forsworns.github.io//feed.json",
"description": "Personal Blog",
"icon": "https://forsworns.github.io/assets/logo.png",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
},
"items": [
{
"content_html": "<p>[[toc]]</p>\n<p>第四范式开源的通用 GPU 虚拟化组件,支持多家 GPU 产品,目前只做了切分功能,已进 CNCF。比较像之前腾讯开源的 <a href=\"https://github.com/tkestack/gpu-manager\">https://github.com/tkestack/gpu-manager</a> 和对应的 <a href=\"https://github.com/tkestack/vcuda-controller%EF%BC%8C%E4%B9%8B%E5%89%8D%E4%B8%80%E7%9B%B4%E6%B2%A1%E4%BB%94%E7%BB%86%E8%AF%BB%EF%BC%8C%E5%88%9A%E5%A5%BD%E8%AF%BB%E4%B8%8B%E8%BF%99%E4%B8%AA%E8%BF%9B%E4%BA%86\">https://github.com/tkestack/vcuda-controller,之前一直没仔细读,刚好读下这个进了</a> CNCF 的吧。</p>\n<h1>HAMi-core</h1>\n<p><a href=\"https://github.com/Project-HAMi/HAMi-core\">https://github.com/Project-HAMi/HAMi-core</a><br>\n基于主线 6b2aed490910db1a33c6575ba81b1ecd96fce5f4</p>\n<h2>src/libvgpu.c</h2>\n<p>劫持 <a href=\"http://libcuda.so\">libcuda.so</a> 和 <a href=\"http://libnvidia-ml.so\">libnvidia-ml.so</a> 的方法是通过劫持 dlsym 函数,如果用户查询的某个符号是 HAMI-core 可以拦截的,就返回对应的拦截函数。dlsym 则是通过暴露一个同名符号,通过 LD_PRELOAD 等方式对 libdl 做覆盖拦截。</p>\n<h2>src/nvml</h2>\n<h3>hook.c</h3>\n<p>做了改动的一些 NVML API,查初始化的时候构造的真实函数指针表调用过去。最重要的就是 <code>_nvmlDeviceGetMemoryInfo</code> 这个实现。</p>\n<p><code>_nvmlDeviceGetMemoryInfo</code> 里面 <code>nvmlDeviceGetIndex</code> 获取到设备的 id,通过 <code>nvml_to_cuda_map()</code> 转换成 <code>shared_region_info_t</code> 中用于标识虚拟设备的 id。根据 id 查询 <code>shared_region_info_t</code> 类型的全局单例 <code>region_info</code>,获取当前虚拟设备的显存限制和使用情况。</p>\n<h3>nvml_entry.c</h3>\n<p>没有做改动直接调用下去的 NVML API。</p>\n<h2>src/multiprocess</h2>\n<h3>shrreg_tool.c</h3>\n<p>一个命令行小工具,支持几个选项:</p>\n<ul>\n<li>create_new:创建了一个文件 <code>/tmp/cudevshr.cache</code>,后面会被用来做跨进程的共享区域,它只是保证这个文件存在。</li>\n<li>Suspend/resume:对所有运行中的被监控到的进程执行 <code>SIGUSR1</code> 和 <code>SIGUSR2</code> 分别用于恢复和挂起这些任务。</li>\n</ul>\n<h3>multiprocess_memory_limit.c</h3>\n<p>这个文件里面比较杂,主要是 HAMI 的多进程资源使用情况的共享内存文件缓存、基于这个共享内存实现的显存管理、还有一些工具函数如host/container pid 转换、共享内存的加锁(lock_shrreg、unlock_shrreg<br>\n),虽然看文件名只是做显存限制的。vcuda-controller 里面实现的比较简单,就是在一个文件里面存了下每个进程的 pid,然后每个 API 调用都会调用 <code>nvmlDeviceGetComputeRunningProcesses</code> 去查然后去对 pid 做匹配,为了省时,搜索的时候对 pid 做了二分,总体上开销还是比较高的。HAMI 这里则是通过直接创建一个多进程共享的资源消耗统计文件,进行了缓存,减少 NVML API 调用次数。这个共享文件会被 mmap 到每个进程内,也就是 <code>shared_region_t</code> 类型的 <code>region_info.shared_region</code>。</p>\n<p>初始化过程中主要做了两件事 <code>try_create_shrreg()</code> 和 <code>init_proc_slot_withlock()</code>。<code>try_create_shrreg</code> 就是创建、初始化上面提到的共享文件的过程。<code>init_proc_slot_withlock</code> 则是对共享内存中当前进程的 slot 做了初始化。为 <code>SIGUSR1</code> 和 <code>SIGUSR1</code> 分别注册信号处理函数 <code>sig_restore_stub</code> 和 <code>sig_swap_stub</code>,用来恢复/暂停显存分配。</p>\n<p>那么有了上面的共享内存,HAMI 中就不是通过 NVML 去查询显存占用了,而是通过 <code>get_gpu_memory_usage</code> 直接查询共享内存缓存中的统计数据。那么它又是如何收集这个统计数据的呢?这就涉及到了另一个比较有趣的函数是 <code>add_gpu_device_memory_usage</code>,它在涉及到显存剧烈变化的 CUDA API 执行时被调用,用来修改共享内存中的显存消耗统计数据,同时它还支持对显存进行分类,区分了是 CUcontext 相关、CUmodule相关、数据相关三类。但是读完代码发现,其实只有通过 cuda API 分配的数据显存被准确统计到了,不知道是不是他们内部实现没有开源出来。CUmodule 实际上完全没统计,CUcontext 则是用了一下初始化后 primary context 的消耗去计算,不过除了 primary context,一般上层框架也基本没有自己去创建 CUcontext 的(其他厂商,AMD ROCm 甚至实际上没有区分 device 和 context)。</p>\n<h3>multiprocess_utilization_watcher.c</h3>\n<p>对 cuda core 进行分配,与 vcuda-controller 中类似。</p>\n<p><code>cuda_to_nvml_map</code> 变量定义在这里,在别的地方 extern 引用了。全局变量 <code>g_cycle</code> 为 10ms,<code>g_wait</code> 为 120ms。</p>\n<p>几个重要的函数,<code>setspec()</code> 用于计算 cuda core 的总数。这里的 FACTOR 是 32,没太搞懂为啥,A100 每个 SM 是 64 个 cuda core,L20、H100 GPU 每个 SM 上是 128 个 cuda core,当然也可能这里表示的是 CUDA 一个 SM 上的 wrap size 是 32。over-subscription 的话有允许调度多个线程,所以是乘起来。感觉只是一个软性的估计,下面的限流器实现其实也可以看出来是允许超过限制的。</p>\n<pre><code>int setspec() {\n CHECK_CU_RESULT(cuDeviceGetAttribute(&g_sm_num,CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,0));\n CHECK_CU_RESULT(cuDeviceGetAttribute(&g_max_thread_per_sm,CU_DEVICE_ATTRIBUTE_MAX_THREADS_PER_MULTIPROCESSOR,0));\n g_total_cuda_cores = g_max_thread_per_sm * g_sm_num * FACTOR;\n return 0;\n}\n</code></pre>\n<p><code>rate_limiter()</code> 是对 utilization 做限制的核心实现(注意 nvidia-smi 看到 utilization 是一个时分的统计数据,这里从 cuda core 的角度去限制实际上是空分的),在 <code>cuLaunchKernel</code> 被调用的时候先触发它,用来限流。加载核函数的 grid 参数被用来计算需要占用的 cuda core 数量。这里在限流的时候,留了一个优先级的接口,<code>region_info.shared_region->priority</code> 目前看没啥实际用途。(NSDI 23 的一个类似的工作 <a href=\"https://github.com/pkusys/TGS%EF%BC%8C%E5%81%9A%E4%BA%86%E4%BC%98%E5%85%88%E7%BA%A7%E8%B0%83%E5%BA%A6%EF%BC%8C%E7%AE%80%E5%8D%95%E7%9C%8B%E4%BA%86%E4%B8%8B%E4%BB%A3%E7%A0%81%E9%83%BD%E6%98%AF%E5%90%8C%E6%A0%B9%E7%94%9F~%EF%BC%89%E3%80%82%E7%B2%BE%E7%AE%80%E5%90%8E%E6%A0%B8%E5%BF%83%E5%A6%82%E4%B8%8B\">https://github.com/pkusys/TGS,做了优先级调度,简单看了下代码都是同根生~)。精简后核心如下</a></p>\n<pre><code class=\"language-cpp\">// 同样没有用 block,按 vcuda-controller 那边的解释,他们是认为 block 的影响没有 grid 大所以只用了 grid,所以只是留了一个参数给其他算法实现\nvoid rate_limiter(int grids, int blocks) {\n \tint before_cuda_cores = 0;\n \tint after_cuda_cores = 0;\n\tint kernel_size = grids;\n \tdo {\n \t\tbefore_cuda_cores = g_cur_cuda_cores;\n\t\t// cuda core 不够了,进入睡眠,每 10ms 检查一次 cuda core 资源,等待别的核函数跑完释放了 cuda core,再提交新的核函数。\n \t\tif (before_cuda_cores < 0) {\n \t\tnanosleep(&g_cycle, NULL);\n\t\t\tcontinue;\n \t\t}\n\t\t// 更新如果加载了核函数,会剩余的 cuda core 数量。按这种实现,用户可以使用远超过限制的 cuda core?\n \t\tafter_cuda_cores = before_cuda_cores - kernel_size;\n \t} while (!CAS(&g_cur_cuda_cores, before_cuda_cores, after_cuda_cores));\n}\n</code></pre>\n<p><code>utilization_watcher</code> 是一个进程内的守护线程,在 <code>src/libvgpu.c</code> 中通过 <code>init_utilization_watcher()</code> 在初始化完成后创建,以 <code>g_wait</code> 为周期重新分配 cuda core 计算资源,较 vcuda-controller 中的 cuda core 分配策略做了很大的删减,代码简化后如下,</p>\n<pre><code class=\"language-cpp\">void* utilization_watcher() {\n int userutil[CUDA_DEVICE_MAX_COUNT];\n int sysprocnum;\n int share = 0;\n int upper_limit = get_current_device_sm_limit(0);\n while (1){\n\t// 120ms 更新一次分配\n nanosleep(&g_wait, NULL);\n if (pidfound==0) {\n\t // 在 `region_info.shared_region->procs` 中注册自己,目前代码中写死了最多支持 1024 个进程。\n update_host_pid();\n }\n\t// 设置进程 sm 利用率为 0\n init_gpu_device_sm_utilization();\n\t// 这里实际上拿到了多卡的信息,而且和 vcuda-controller 不同的是它是会把查询结果写到一个共享文件里面做缓存。\n get_used_gpu_utilization(userutil,&sysprocnum);\n if ((share==g_total_cuda_cores) && (g_cur_cuda_cores<0)) {\n\t // 这里没看懂\n g_total_cuda_cores *= 2;\n share = g_total_cuda_cores;\n }\n\t// 但是按这里的写法,当前是只支持了单卡,没有利用到上一步取出的多卡信息,\n\t// 根据利用率限制、当前的利用率、上次的变化值,计算这次分配额度的变化\n share = delta(upper_limit, userutil[0], share);\n\t// 应用计算出的额度变化,重新配置 `g_cur_cuda_cores`\n change_token(share);\n }\n}\n</code></pre>\n<p>上面通过在 <code>get_used_gpu_utilization()</code> 内,调用 <code>nvmlDeviceGetComputeRunningProcesses</code> 获取所有正在使用某个 GPU 设备的进程,然后调用 <code>nvmlDeviceGetProcessUtilization</code> 获取每个进程的 GPU 利用率和显存占用,分别记录到了 <code>region_info.shared_region->procs[i].device_util[dev].sm_util</code> 和 <code>region_info.shared_region->procs[i].monitorused[dev]</code>。这个函数是考虑了多进程的。</p>\n<h2>src/allocator/allocator.c</h2>\n<p><code>allocated_list</code> 是一个存了 <code>allocated_device_memory_struct</code> 的双向链表,实例化了两个全局变量 <code>device_overallocated</code>、<code>array_list</code>。成员定义如下</p>\n<pre><code class=\"language-cpp\">struct allocated_device_memory_struct{\n CUdeviceptr address;\n size_t length;\n CUcontext ctx;\n CUmemGenericAllocationHandle *allocHandle;\n};\n</code></pre>\n<p><code>region_list</code> 是一个存了 <code>region_struct</code> 的双向链表,实例化成了一个全局变量 ``r_list。成员定义如下</p>\n<pre><code class=\"language-cpp\">struct region_struct{\n size_t start;\n size_t freemark;\n size_t freed_map;\n size_t length;\n CUcontext ctx;\n allocated_list *region_allocs;\n char *bitmap;\n CUmemGenericAllocationHandle *allocHandle;\n};\n</code></pre>\n<p>OVERSIZE 128M,IPCSIZE 2M,ALIGN 也是 2M。</p>\n<p><code>oom_check()</code> 就是查询了上面提到的记录着进程信息的共享内存区域,获取当前设备的显存用量,加上请求分配的显存值,和设定的限制值做比较。如果超过限制,就尝试清理下已经结束的进程的显存记录,然后重新计算一遍。</p>\n<p>剩下的接口就都是对 cuda 显存分配的一些抽象,把分配结果记录到上面创建的全局列表里面。他们底层又对应着 <code>add_chunk_async()</code></p>\n<pre><code class=\"language-cpp\">int allocate_raw(CUdeviceptr *dptr, size_t size);\nint free_raw(CUdeviceptr dptr);\nint add_chunk_only(CUdeviceptr address,size_t size);\nint allocate_async_raw(CUdeviceptr *dptr, size_t size, CUstream hStream); // 基于 add_chunk_async\nint free_raw_async(CUdeviceptr dptr, CUstream hStream);\n</code></pre>\n<p><code>check_memory_type()</code> 就是在 <code>device_overallocated</code> 里面检查有没有查询的指针,判断是设备地址还是 host 侧地址。值得注意的是按这个实现,<code>cuMemAllocManaged</code> 分配出来的地址算到了设备地址里面。</p>\n<h2>src/cuda/</h2>\n<p><a href=\"http://libcuda.so\">libcuda.so</a> 库劫持逻辑,好多 API 其实没实现劫持方案,只是打印了一下日志,感觉是之后打算做。劫持的时候注意一下 <code>cuGetProcAddress</code> 即可。</p>\n<h3>memory.c</h3>\n<p>劫持了显存分配 API,逻辑上就是先调用 <code>oom_check()</code> 检查一下,如果超过显存限制,就不分配了直接返回 OOM。</p>\n<p>比较特殊的是 <code>cuMemHostAlloc</code>、<code>cuMemAllocHost_v2</code>、<code>cuMemHostRegister_v2</code>,这三个 API,则是先真实触发进行分配,再调用 <code>oom_check()</code> 检查显存,如果超了,就释放掉回滚刚刚到真实分配。这个逻辑感觉很迷惑,这三个 API 的分配并没有涉及到对 <code>src/allocator/allocator.c</code> 中的全局链表的修改,也就是说分配前后实际上检查的效果是一样的,为什么要先分配再回滚呢?</p>\n<h2>src/utils.c</h2>\n<p>定义了一个跨进程的锁,<code>"/tmp/vgpulock/lock”</code>,<code>try_lock_unified_lock</code> 通过标记 <code>O_EXCL</code> 互斥地打开该文件作为锁。</p>\n<p><code>parse_cuda_visible_env</code> 查看环境变量中 <code>CUDA_VISIBLE_DEVICES</code>,尝试对卡的序号进行修正,存到 <code>cuda_to_nvml_map</code> 里面。那这里实际上只支持 nvidia-container-toolkit 对应的 runc 场景,其他厂商都还需要适配。比如昇腾,他们接入 HAMI,是也拿 <code>CUDA_VISIBLE_DEVICES</code> 去做自己的 runc 配置的环境变量了么。</p>\n<p><code>mergepid</code> 接收两个 <code>nvmlDeviceGetComputeRunningProcesses</code> 采集到的进程组,合并到一个里面。实际上不要这个函数也行,反正现在只支持单卡,这个函数只是为了对多卡的<code>nvmlDeviceGetComputeRunningProcesses</code> 返回结果做聚合。</p>\n<p><code>getextrapid</code> 比较两个<code>nvmlDeviceGetComputeRunningProcesses</code> 采集到的进程组,找到新增的那一个进程。</p>\n<p><code>set_task_pid</code> 是为容器内 pid 和 host 侧 pid 建立关联,因为 <code>nvmlDeviceGetComputeRunningProcesses</code> 可以获取到占用 GPU 设备的 host 侧进程号。先调用一次<code>mergepid</code> 将所有占用 GPU 的进程记录到 <code>pre_pids_on_device</code>。<code>cuDevicePrimaryCtxRetain</code> 之后(看上去没激活 ctx 的话不会被 nvml 检测到?),再调用一次 <code>mergepid</code>把所有占用 GPU 的进程记录到 <code>pids_on_device</code>,然后通过<code>getextrapid</code>过滤出来一个新增的进程,就是当前进程在 host 侧的进程号,然后它通过 <code>set_host_pid</code> 把这个 hostpid 写入到 region_info 的共享内存当前进程的分区中。如果有恰好在查找过程中退出的进程,没有影响,因为我们只关心新增的进程,而别的新增进程还卡在 <code>try_lock_unified_lock</code> 那里。</p>\n<h2>include</h2>\n<h3>libnvml_hook.h</h3>\n<p>定义了宏如 NVML_OVERRIDE_CALL,和用于标识 NVML API 的枚举 NVML_OVERRIDE_ENUM_t。实现上不够简洁很多地方可以 #include 同一个 API 列表去做替换。也没看到生成这些头文件的相关脚本,后面升级更新 API 列表很麻烦。</p>\n<h3>libcuda_hook.h</h3>\n<p>类似 libnvml_hook.h</p>\n<h1>HAMi</h1>\n<p><a href=\"https://github.com/Project-HAMi/HAMi\">https://github.com/Project-HAMi/HAMi</a></p>\n<p>update: 2025-01-05: cbccbd469c636870655dff5f6d2707e206d00a02,更新了 nvidia MIG 和 Metax GPU,加了一些 UT 和文档,特别是有个 <a href=\"https://github.com/Project-HAMi/HAMi/blob/master/docs/mind-map/HAMI-VGPU-mind-map-Chinese.png\">脑图</a> 画得很好。</p>\n<p>下面的代码基于 66cabbfac0aebd4ccf19a2d0850c1a2d682b3159</p>\n<p>HAMI-core 中的劫持库,会被编译成 <a href=\"http://libvgpu.so\">libvgpu.so</a>,通过挂载 ld.so.preload 文件的方式注入到容器里面做 cuda/nvml 劫持。</p>\n<h2>cmd/vGPUmonitor/</h2>\n<p>看代码像是内部删了一些东西才开源的,好多没用到的符号……main.go 中开了两个协程分别运行 <code>initMetrics</code> 和 <code>watchAndFeedback</code>。</p>\n<h3>metrics.go</h3>\n<p>将自定义的 vGPU 数据格式转换收集到 Promethus。</p>\n<p><code>initMetrics</code> 监听了 <code>9394</code> 端口,<code>/metrics</code> 会被路由到 Prometheus 服务。由 <code>ClusterManagerCollector</code> 实现 Prometheus 的 <code>Collector</code> 接口,负责采集 metrics 并注册到 Prometheus,注册时定义了 metric 的标签 zone为 <code>vGPU</code>。接口 <code>Collect()</code> 实现时候就是通过 nvml 获取了 host 侧 GPU 指标,通过 <code>ContainerLister.ListContainers()</code> 获取了每个容器的 vGPU 指标。</p>\n<h4>testcollector/main.go</h4>\n<p>验证 metrics.go 中的 Prometheus 数据采集。</p>\n<h3>validation.go</h3>\n<p><code>ValidateEnvVars</code> 检查了一下 <code>HOOK_PATH</code> 环境变量是否配置了。</p>\n<h3>feedback.go</h3>\n<p><code>watchAndFeedback</code> 中每五秒,通过 <code>pkg/monitor/nvidia.ContainerLister</code> 遍历一遍所有容器,记录容器配置的优先级,综合各个容器内的 <code>Priority</code>、<code>RecentKernel</code>、<code>UtilizationSwitch</code> 信息分别修改他们的配置。<br>\n这里没看懂修改这三个变量的逻辑,还得回到 HAMI-core 那边联合起来看下。看上去 <code>Priority</code> 是数值越小优先级越高。</p>\n<h3>noderpc/noderpc.proto</h3>\n<p>定义了一个 gRPC 服务用来获取各个 POD 中的 vGPU 使用情况,<code>rpc GetNodeVGPU (GetNodeVGPURequest) returns (GetNodeVGPUReply) {}</code>。响应中的 <code>sharedRegionT</code>,也就是 HAMI-core 中存放资源切分数据的共享内存中的数据。</p>\n<h2>pkg/monitor/</h2>\n<h3>nvidia/cudevshr.go</h3>\n<p>为 HAMI-core 中共享内存区域内的数据,定义了 v0 和 v1 两个版本的统计信息,通过统一接口 <code>UsageInfo</code>。单个容器的统计信息如下</p>\n<pre><code class=\"language-go\">type ContainerUsage struct {\n PodUID string\n ContainerName string\n data []byte\n Info UsageInfo\n}\n</code></pre>\n<p><code>ContainerUsage</code> 数据存储在 <code>ContainerLister</code> 类型中,<code>ContainerLister.ListContainers()</code>在上面看到过的 <code>vGPUmonitor</code> 中被用来获取容器内的统计信息。<br>\n<code>ContainerLister.Update()</code> 则是遍历各个,通过 <code>loadCache()</code> 函数获取容器的统计数据 <code>ContainerUsage</code>。如果容器内没有调用过 cuInit,那 <code>loadCache()</code> 不会统计到它。<br>\n函数<code>loadCache()</code> 实现的逻辑是,查询文件 <code>$HOOK_PATH/containers/$POD_NAME/.cache</code>(<a href=\"http://libvgpu.so\">libvgpu.so</a> 也在该目录内),然后直接 mmap 读取出来转换成符合 <code>UsageInfo</code> 接口的数据。<br>\n`</p>\n<h2>cmd/scheduler/</h2>\n<p>节点/GPU 调度器,实现在 <code>pkg/scheduler/scheduler.go</code>。</p>\n<h3>main.go</h3>\n<p>启动两个协程运行收集集群中的节点设备信息的 <code>Scheduler.RegisterFromNodeAnnotations()</code> 和采集上报 Prometheus metric 的 <code>initMetrics()</code>,然后默认监听 <code>8080</code> 端口,为 <code>pkg/scheduler/routes/route.go</code> 中定义的 http 服务提供扩展 k8s 调度器服务。</p>\n<h3>metrics.go</h3>\n<p>和 vGPUmonitor 中的 <code>cmd/vGPUmonitor/metrics.go</code> 一致,不同的地方是容器内信息来自 <code>pkg/scheduler/scheduler.go</code> 中的 <code>Scheduler</code> 类型,例如这里获取 host 侧指标是通过 <code>Scheduler.InspectAllNodesUsage()</code>,每个容器的信息是通过 <code>podManager.GetScheduledPods()</code></p>\n<h2>pkg/scheduler</h2>\n<p>通过 <code>k8s.io/kube-scheduler/extender/v1</code> API 拓展 k8s 的调度器。k8s 调度器负责将 Pod 分配到合适的节点,而扩展调度器可以让用户自定义调度逻辑,一般都会包含过滤、打分、绑定等机制。</p>\n<h3>routes/route.go</h3>\n<p><code>cmd/scheduler/main.go</code> 中定义的路由为</p>\n<ul>\n<li><code>/filter</code> 对应 <code>PredicateRoute</code>。调用 <code>Scheduler.Filter()</code>,处理 Pod 调度过滤逻辑。</li>\n<li><code>/bind</code> 对应 <code>Bind</code>,调用<code>Scheduler.Bind()</code>,处理 Pod 调度绑定逻辑。</li>\n<li><code>/webhook</code> 对应 <code>WebHookRoute</code>,调用 <code>Scheduler.NewWebHook()</code> 创建 webhook,在 webhook 上调用 <code>ServeHTTP()</code>。</li>\n<li><code>/healthz</code> 对应 <code>HealthzRoute</code>,心跳包。</li>\n</ul>\n<h3>scheduler.go</h3>\n<p>调度器类定义</p>\n<pre><code class=\"language-go\">type Scheduler struct {\n nodeManager\n podManager\n stopCh chan struct{}\n kubeClient kubernetes.Interface\n podLister listerscorev1.PodLister\n nodeLister listerscorev1.NodeLister\n //Node status returned by filter\n cachedstatus map[string]*NodeUsage\n nodeNotify chan struct{}\n //Node Overview\n overviewstatus map[string]*NodeUsage\n eventRecorder record.EventRecorder\n}\n</code></pre>\n<p>调度器类实现了接口 <code>onUpdateNode()</code>、<code>onDelNode()</code>、<code>onAddNode()</code>,当集群中的节点变更时,会通过回调写入事件到 <code>Scheduler.nodeNotify</code>。</p>\n<p>它也实现了<code>onAddPod()</code>、<code>onUpdatePod()</code>、<code>onDelPod()</code>,其中 <code>onUpdatePod()</code> 算是 <code>onAddPod()</code> 的一种特殊情况。这几个回调会直接调用 <code>Scheduler.addPod()</code> 和 <code>Scheduler.delPod()</code>。</p>\n<p><code>Scheduler.RegisterFromNodeAnnotations()</code> 会启动一个无限循环,每隔一段时间或收到节点变更通知时,执行节点注册逻辑,直到<code>Scheduler.stopCh</code> 收到信号终止。它通过 <code>Scheduler.nodeLister</code> 获取所有节点,做一次健康检测,然后把每个正常的节点信息建模成 <code>util.NodeInfo</code> 类型,通过 <code>Scheduler.addNode()</code> 函数在调度器中存储节点。注册完毕后会调用一次 <code>Scheduler.getNodesUsage()</code> 获取所有节点/Pod 的设备和显存信息,更新到 <code>Scheduler.cachedstatus</code> 中,不过这个成员看上去还没有怎么用到,可能是之后想做缓存。</p>\n<p><code>Scheduler.Filter()</code> 和 <code>Scheduler.Bind()</code> 就是之前 <code>routes/route.go</code> 部分提过的调度器做设备分配和过滤的 http 接口的实现。<code>Scheduler.Filter()</code> 首先尝试清理掉当前请求相关的 Pod 以避免对节点资源统计数据造成偏差,然后通过<code>Scheduler.getNodesUsage()</code>获取当前所有的节点及其资源信息,然后通过 <code>Scheduler.calcScore()</code> 计算一遍节点的分值和对应的节点信息、节点设备信息,按分值排序后获取最优的一个可分配节点,最后调整 Pod 的 annotation,调用 <code>Scheduler.addPod()</code>记录。<code>Scheduler.Bind()</code> 的实现比较简单,就是调用 k8s 的API 获取 node 和 pod,对 node 加锁(锁实现见 <code>pkg/util/nodelock/nodelock.go</code>),然后调用 k8s 的 Bind API 去把 pod 调度到请求的节点上。</p>\n<h3>events.go</h3>\n<p>基于 <code>k8s.io/client-go/tools/record</code> 中的 <code>EventRecorder</code>,将Pod调度过程中的绑定/过滤事件记录到 k8s事件系统中,便于后续的故障排查和状态监控。</p>\n<h3>nodes.go</h3>\n<p><code>nodeManager</code> 持有一个节点和设备的哈希表,可以通过 <code>nodeManager.addNode()</code> 和 <code>nodeManager.rmNodeDevice()</code> 添加、删除调度器自身维护的节点上的 GPU 设备信息。</p>\n<h3>pods.go</h3>\n<p><code>podManager</code> 持有一个已被调度的 Pod 的哈希表,可以通过 <code>podManager.addPod()</code> 和 <code>podManager.delPod()</code> 添加、删除调度器自身维护的 Pod 信息。</p>\n<h3>webhook.go</h3>\n<p>k8s 允许集群中存在多个调度器。默认情况下,Pod 使用的是 kube-scheduler 调度器。通过设置 SchedulerName 字段,我们可以指定哪个调度器来调度特定的 Pod。<br>\n这个 webhook 的 <code>Handle()</code> 就是用来为合法的 Pod 选择使用 HAMI 实现的调度器进行调度。它的实现逻辑是,先检查是否 Pod 内有容器,如果有,再去看是不是特权容器,如果不是特权容器,再调用 <code>pkg/device/devices.go</code> 中定义的 <code>Device</code> 公共接口,检查容器的资源限制、annotation 等是否符合对应设备的配置规范,如果合规就修改调度器。</p>\n<h3>score.go</h3>\n<p>实现 k8s 中调度算法的核心,打分机制。该文件内的函数之间的调用链为 <code>Scheduler.calcScore()->fitInDevices()->fitInCertainDevice()</code>,对每个节点、每个请求进行遍历,又会对请求中的每种设备需求进行检查,最后返回一个包含节点、设备(包含了针对请求的分配情况)、分值的 <code>policy.NodeScore</code> 的列表。最外层的 <code>Scheduler.calcScore()</code> 是被 <code>Scheduler.Filter()</code> 用在了调度器的节点过滤逻辑中,用来计算节点、设备的分数选择节点设备。</p>\n<h3>node_policy.go</h3>\n<p>节点调度策略,目前实现了两个,<code>binpack</code> 和 <code>spread</code>,默认为 <code>binpack</code>,优先占满节点,可以通过 POD 级别的 annotation <code>hami.io/node-scheduler-policy</code>进行修改。</p>\n<p>为 <code>NodeScoreList</code> 定义了 <code>Less</code> 接口,按节点的分数进行排序。根据节点上的设备使用占比、设备核心使用占比,显存使用占比,三者求和计算分数。</p>\n<h3>policy/gpu_policy.go</h3>\n<p>GPU 调度策略,目前实现了两个,<code>binpack</code> 和 <code>spread</code>,默认为 <code>spread</code>,优先均匀分布,可以通过 POD 级别的 annotation <code>hami.io/gpu-scheduler-policy</code> 进行修改。</p>\n<p>为 <code>DeviceUsageList</code> 定义了 <code>Less</code> 接口,按节点的分数进行排序。根据设备上的设备使用占比、设备核心使用占比,显存使用占比,三者求和计算分数,这里注意是要加上申请的额度的。</p>\n<h2>cmd/device-plugin/nvidia</h2>\n<p>为英伟达设备实现的 <a href=\"https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/\">k8s Device Plugin</a>,HAMI 一开始只支持 nvidia,放在这里也算是一个实现范例,其他厂商的 DP 在别的仓库。<br>\n也相当于是 nvidia 官方的 <a href=\"https://github.com/NVIDIA/k8s-device-plugin\">DP</a> 的一个拓展。</p>\n<h3>main.go</h3>\n<p>通过 <code>startPlugins()</code> 启动 DP 的服务,它的主要逻辑如下</p>\n<pre><code class=\"language-go\">func startPlugins(c *cli.Context, flags []cli.Flag, restarting bool) ([]plugin.Interface, bool, error) {\n config, err := loadConfig(c, flags)\n disableResourceRenamingInConfig(config)\n devConfig, err := generateDeviceConfigFromNvidia(config, c, flags)\n // Update the configuration file with default resources.\n err = rm.AddDefaultResourcesToConfig(&devConfig)\n // Get the set of plugins.\n pluginManager, err := NewPluginManager(&devConfig)\n plugins, err := pluginManager.GetPlugins()\n // Loop through all plugins, starting them if they have any devices to serve. \n for _, p := range plugins {\n if len(p.Devices()) == 0 {\n continue\n }\n if err := p.Start(); err != nil {\n return plugins, true, nil\n }\n }\n return plugins, false, nil\n}\n</code></pre>\n<p><code>disableResourceRenamingInConfig()</code> 中禁用了官方 DP 中对设备的重命名,之后会恢复回来,应该是受限于当前的 HAMI-core 的实现?</p>\n<h3>plugin-manager.go</h3>\n<p><code>NewPluginManager()</code> 根据配置生成一个 DP 的工厂用来构建各种依赖的 DP,后面就会看到不只有一个,例如 nvml 也需要单独的 DP 配置。</p>\n<h3>vgpucfg.go</h3>\n<p>实现了用来解析 HAMI 自定义的参数的工具函数 <code>generateDeviceConfigFromNvidia()</code>。</p>\n<h2>pkg/device-plugin/nvidiadevice/nvinternal</h2>\n<p>也是基于英伟达官方的 <a href=\"https://github.com/NVIDIA/k8s-device-plugin/blob/main/internal\">https://github.com/NVIDIA/k8s-device-plugin/blob/main/internal</a> 中的 DP 实现进行了拓展。</p>\n<h3>plugin</h3>\n<h4>api.go</h4>\n<p>定义接口</p>\n<pre><code class=\"language-go\">type Interface interface {\n Devices() rm.Devices\n Start() error\n Stop() error\n}\n</code></pre>\n<h4>server.go</h4>\n<p>定义了类型 <code>NvidiaDevicePlugin</code>,实现下面的 DP 的标准服务接口。</p>\n<pre><code>service DevicePlugin {\n // GetDevicePluginOptions returns options to be communicated with Device Manager.\n rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {}\n\n // ListAndWatch returns a stream of List of Devices\n // Whenever a Device state change or a Device disappears, ListAndWatch\n // returns the new list\n rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}\n\n // Allocate is called during container creation so that the Device\n // Plugin can run device specific operations and instruct Kubelet\n // of the steps to make the Device available in the container\n rpc Allocate(AllocateRequest) returns (AllocateResponse) {}\n\n // GetPreferredAllocation returns a preferred set of devices to allocate\n // from a list of available ones. The resulting preferred allocation is not\n // guaranteed to be the allocation ultimately performed by the\n // devicemanager. It is only designed to help the devicemanager make a more\n // informed allocation decision when possible.\n rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {}\n\n // PreStartContainer is called, if indicated by Device Plugin during registration phase,\n // before each container start. Device plugin can run device specific operations\n // such as resetting the device before making devices available to the container.\n rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {}\n}\n</code></pre>\n<p><code>cmd/device-plugin/nvidia/main.go</code> 通过它的 <code>NvidiaDevicePlugin.Start()</code> 函数拉起 DP 服务 <code>NvidiaDevicePlugin.Serve()</code>,通过 <code>NvidiaDevicePlugin.Register()</code> 注册自身到 kubelet,然后开启一个线程调用 <code>ResourceManager.CheckHealth()</code> 持续做健康检查,一个线程调用 <code>NvidiaDevicePlugin.WatchAndRegister()</code> 定期更新节点设备信息。</p>\n<p><code>NvidiaDevicePlugin.Serve()</code> 就是启动了一个 uds server,提供上面的 <code>DevicePlugin</code> service。</p>\n<p><code>DevicePlugin.PreStartContainer()</code> 和 <code>DevicePlugin.GetPreferredAllocation()</code> 都是空实现,<code>DevicePlugin.ListAndWatch()</code> 在设备有健康状态变化的时候,返回 <code>ResourceManager.Devices().GetPluginDevices()</code> 的结果。</p>\n<p><code>DevicePlugin.Allocate()</code> 先调用 <code>util.GetNextDeviceRequest()</code> 从 pending 状态的 Pod 中找到需要 GPU 设备的容器;调用 <code>NvidiaDevicePlugin.getAllocateResponse()</code> ,其中根据 <code>NvidiaDevicePlugin.deviceIDsFromAnnotatedDeviceIDs()</code> 根据请求信息中的设备获取符合 CDI 规范的设备 ID,然后在 <code>NvidiaDevicePlugin.getAllocateResponseForCDI()</code> 中进一步根据 CDI 规范,修改响应的格式以触发 CDI;然后修改容器的环境变量和 mount,在这里配置 vgpu/HAMi-core(这里一些操作不可以放到 <code>DevicePlugin.PreStartContainer()</code> 里面吗)。</p>\n<h4>register.go</h4>\n<p>DP 的 grpc 服务器启动后,会单独启动一个线程调用该文件内的 <code>WatchAndRegister()</code>,定期获取节点侧设备信息,更新到节点的 annotation 中。</p>\n<h4>manager</h4>\n<p>不同平台下的 DP manager。</p>\n<h5>api.go</h5>\n<p>定义接口</p>\n<pre><code class=\"language-go\">type Interface interface {\n GetPlugins() ([]plugin.Interface, error)\n CreateCDISpecFile() error\n}\n</code></pre>\n<h5>factory.go</h5>\n<p><code>manager</code> 类型用来初始化 nvml、cdi,解析环境是 nvml 类型还是 tegra 类型,与后面的 manager 实现、<code>rm</code> 模块有关。</p>\n<p><code>New()</code> 中 <code>manager</code> 被拓展成了 <code>nvmlmanager</code>(一般情况都是基于 nvml 来管理)、<code>tegramanager</code> (tegra 设备使用)和 <code>null</code>(错误情况下的 fallback) 三类 manager,他们均需要实现 <code>api.Interface</code>。</p>\n<h5>null.go</h5>\n<p><code>null</code> manager 实现。</p>\n<h5>nvml.go</h5>\n<p><code>nvmlmanager</code> 实现。<code>nvmlmanager.GetPlugins()</code> 接口,通过 <code>rm.NewNVMLResourceManagers()</code> 获取所有资源,对每个资源,通过 <code>plugin/server.go</code> 中 <code>plugin.NewNvidiaDevicePlugin()</code> 构建 DP。</p>\n<h5>tegra.go</h5>\n<p><code>tegramanager</code> 实现。</p>\n<h3>rm</h3>\n<p>分配、管理、监控每个资源对应的 GPU 设备。</p>\n<h4>rm.go</h4>\n<p>实现 <code>resourceManager</code>,负责管理 GPU 设备。定义接口 <code>ResourceManager</code>。</p>\n<pre><code class=\"language-go\">type ResourceManager interface {\n Resource() spec.ResourceName\n Devices() Devices\n GetDevicePaths([]string) []string\n GetPreferredAllocation(available, required []string, size int) ([]string, error)\n CheckHealth(stop <-chan interface{}, unhealthy chan<- *Device) error\n}\n</code></pre>\n<p><code>NewResourceManagers()</code> 为每个资源创建 <code>ResourceManager</code> 接口类型,一般来说使用的是 <code>NewNVMLResourceManagers()</code> (不需要考虑 tegra 设备)。</p>\n<h4>allocate.go</h4>\n<p>实现了两个 GPU 分配算法,用户可以使用 <code>resourceManager.getPreferredAllocation()</code> 获取分配出的 GPU 设备。</p>\n<p>其中一个集成了 <a href=\"https://github.com/NVIDIA/go-gpuallocator/\">https://github.com/NVIDIA/go-gpuallocator/</a> 中的 GPU 分配器,它会借助 nvml 识别拓扑关系,按预定的策略选择合适的 GPU 设备,<code>resourceManager.alignedAlloc()</code>。<br>\n另一个则是考虑了过往的分配情况,尽可能均匀地完成分配,<code>resourceManager.distributedAlloc()</code>。</p>\n<h4>devices.go</h4>\n<p><code>Device</code> 类包装 k8s 的 DP 中对设备的抽象,即 <a href=\"http://k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1\">k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1</a> 中的 <code>Device</code>。提供了一组公共的接口</p>\n<pre><code class=\"language-go\">type deviceInfo interface {\n GetUUID() (string, error)\n GetPaths() ([]string, error)\n GetNumaNode() (bool, int, error)\n}\n</code></pre>\n<p><code>Devices.GetPluginDevices()</code> 会被 DP 的 <code>ListAndWatch()</code> 接口使用。</p>\n<h4>device_map.go</h4>\n<p><code>DeviceMap</code> 基于给定的 libnvml、资源名、nvidia 官方 DP 的配置,构建资源名到 HAMI <code>resourceManager</code> 中的设备抽象的映射(device.go 中的 <code>Device</code> 类型)。</p>\n<h4>health.go</h4>\n<p>检查 GPU 设备的监控状态,允许通过环境变量 <code>DP_DISABLE_HEALTHCHECKS</code> 指定一些可忽略的 xid 错误。目前默认忽略下面的 xid,因为他们只表明用户应用出错了但是设备可能仍然可用。</p>\n<pre><code class=\"language-go\">// http://docs.nvidia.com/deploy/xid-errors/index.html#topic_4\n// Application errors: the GPU should still be healthy\napplicationErrorXids := []uint64{\n 13, // Graphics Engine Exception\n 31, // GPU memory page fault\n 43, // GPU stopped processing\n 45, // Preemptive cleanup, due to previous errors\n 68, // Video processor exception\n}\n</code></pre>\n<h4>nvml_devices.go</h4>\n<p><code>nvmlDevice</code> 和 <code>nvmlMigDevice</code> 类型包装了 <a href=\"http://github.com/NVIDIA/go-nvlib/pkg/nvml\">github.com/NVIDIA/go-nvlib/pkg/nvml</a> 的 <code>Device</code>。同样是实现了 <code>deviceInfo</code> 接口。</p>\n<h4>nvml_manager.go</h4>\n<p><code>nvmlResourceManager</code> 包装了 <code>resourceManager</code>,集成了 <a href=\"http://github.com/NVIDIA/go-nvlib/pkg/nvml\">github.com/NVIDIA/go-nvlib/pkg/nvml</a> 中的 nvml接口。</p>\n<h4>wsl.go</h4>\n<p><code>wslDevice</code> 包装了一层 <code>nvmlDevice</code>。= = 还支持 wsl 的……</p>\n<h4>tegra_devices.go、tegra_manager.go</h4>\n<p>Tegra 设备只支持 <code>resourceManager.distributedAlloc()</code> 分配策略。</p>\n<p><code>tegraResourceManager</code> 包装了 <code>resourceManager</code>。</p>\n<p>= = 还支持 tegra 的……</p>\n<h3>cdi</h3>\n<p>借助官方实现 <code>github.com/NVIDIA/nvidia-container-toolkit/pkg/nvcdi</code>,为 DP 使用的 nvidia 设备创建 CDI specs</p>\n<h4>api.go</h4>\n<p>定义实现 CDI 的接口</p>\n<pre><code class=\"language-go\">type Interface interface {\n CreateSpecFile() error\n QualifiedName(string, string) string\n}\n</code></pre>\n<h4>factory.go</h4>\n<p>CDI <code>Interface</code> 的工厂函数,如果没有检测到 nvidia 设备,就创建一个空实现,也就无法生成 CDI specs。</p>\n<h4>cdi.go</h4>\n<p>定义一个 <code>cdiHandler</code> 类型用于实现生成 CDI 的接口 <code>Interface</code></p>\n<pre><code class=\"language-go\">type cdiHandler struct {\n logger *logrus.Logger\n nvml nvml.Interface // github.com/NVIDIA/go-nvlib/pkg/nvml\n nvdevice nvdevice.Interface // github.com/NVIDIA/go-nvlib/pkg/nvlib/device\n driverRoot string\n targetDriverRoot string\n nvidiaCTKPath string\n cdiRoot string\n vendor string\n deviceIDStrategy string\n enabled bool\n gdsEnabled bool // GPUDirect Storage\n mofedEnabled bool // Mellanox OpenFabrics Enterprise Distribution\n cdilibs map[string]nvcdi.Interface // github.com/NVIDIA/nvidia-container-toolkit/pkg/nvcdi\n}\n</code></pre>\n<h3>mig/mig.go</h3>\n<p><code>GetMigCapabilityDevicePaths</code> 获取 nvidia MIG 切分模式下的设备文件。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20241020/",
"title": "HAMI 源码阅读",
"summary": "第四范式开源的通用 GPU 虚拟化组件",
"date_modified": "2024-10-20T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h2>pytorch2 中打开日志</h2>\n<p>import torch<br>\nimport logging<br>\ntorch._logging.set_logs(all=logging.DEBUG)</p>\n<h2>nvrtc</h2>\n<p>torch.compile 时大量的子进程占用 GPU 设备。已知单纯调用 libnvrtc 和 libnvJitLink 不会引用 GPU 设备。</p>\n<pre><code>import torch\nimport logging\n\ntorch._logging.set_logs(all=logging.DEBUG)\n\n# “reduce-overhead” 模式才会用到 cuda graph\n@torch.compile(mode="reduce-overhead")\ndef my_model(x):\n y = torch.matmul(x, x).cuda()\n # side effect breaks graph construction\n input("during capture")\n y = torch.matmul(y, x).cuda()\n return y\n\nx = torch.randn(10, 10).cuda()\n\nprint("graph exec 1", flush=True)\ny = my_model(x)\n\nprint("graph exec 2", flush=True)\ny = my_model(x)\n\nprint("y", y, flush=True)\n\n</code></pre>\n<p>Pytorch 中通过宏 AT_CUDA_NVRTC_CHECK 调用 nvrtc 的代码处理错误。<a href=\"http://libnvrtc.so\">libnvrtc.so</a> 库中的符号类似 <a href=\"http://libcuda.so\">libcuda.so</a> 等是通过下面的函数动态获取的</p>\n<pre><code class=\"language-c++\">const at::cuda::NVRTC& nvrtc() {\n return at::globalContext().getNVRTC();\n}\n</code></pre>\n<p>CUDAHook 和 nvrtc 的具体的实现是在<br>\n<code>aten/src/ATen/cuda/detail/CUDAHooks.cpp</code><br>\n和<br>\n<code>aten/src/ATen/cuda/detail/LazyNVRTC.cpp</code>。在 <code>aten/src/ATen/cuda/detail/LazyNVRTC.cpp</code> 中,<code>lazyNVRTC</code> 这个全局变量上除了 nvrtc 的 API,还会通过 <code>dlsym</code> 懒加载 <a href=\"http://libcuda.so\">libcuda.so</a> 中的部分 API。 可能就是这里 dlopen 的 <a href=\"http://libcuda.so\">libcuda.so</a> 仍占用设备。但是这里也说不通,cuInit 和 CUcontext 相关的函数不可能不先调用。</p>\n<p>除了下面 torch.compile 相关的会调用到 nvrtc 编译 cuda 算子,aten 库中例如 <code>aten/src/ATen/native/cuda/jit_utils.cpp</code> 的 <code>jit_pwise_function</code> 也调用了 nvrtc,但是是用来做一些循环展开等算子优化的。</p>\n<h2>torch.compile 过程中的 nvrtc 调用</h2>\n<p>被用于代码生成</p>\n<p>torch/_dynamo/trace_rules.py 中定义了一系列指导前端的 trace rule,其中的 “torch._C._te.construct_codegen” 是定义在 torch/csrc/jit/tensorexpr/tensorexpr_init.cpp 中的 <code>construct_codegen -> CudaCodeGen</code>,<code>CudaCodeGen::Initialize -> CudaCodeGen::CompileToNVRTC</code> 可以调用到 nvrtc 进行即时编译。</p>\n<p>将 CudaAnalysis 分析出的代树,通过 CudaPrinter、GPUMetaVarRewriter 辅助结构体交给其他辅助函数做重写,结果写入到一个 ostringstream,交给 nvrtc 编译。</p>\n<p>被用于算子融合</p>\n<p>prim::FusionGroup (torch/csrc/jit/runtime/register_prim_ops_fulljit.cpp) -> runFusion -> launchFusion(torch/csrc/jit/codegen/fuser/executor.cpp) -> launch_raw<br>\ncompileKernel (torch/csrc/jit/codegen/fuser/compiler.cpp) 调用了 getConstructor 拿到 registerFusionBackend 中注册到全局的 <code>createFusionKernel</code>(torch/csrc/jit/codegen/fuser/cuda/fused_kernel.cpp 或 <code>torch/csrc/jit/codegen/fuser/cpu/fused_kernel.cpp</code>) 。</p>\n<h2>torch.compile 其他 API</h2>\n<h3>前端 dynamo</h3>\n<p>torch/_dynamo/eval_frame.py 中的 <code>_optimize()</code> 前端 TorchDynamo 的入口函数。</p>\n<p>torch/_dynamo/convert_frame.py 定义了 <code>ConvertFrame</code>、 <code>ConvertFrameAssert</code> 等仿函数类,最终调用同文件内的 <code>_compile</code> 函数,将栈帧转换成 FX graph。</p>\n<h3>后端 inductor</h3>\n<p>torch/_inductor/compile_fx.py 中的 <code>compile_fx</code>,在 torch/_dynamo/backends/inductor.py 里面通过 <code>register_backend</code> 注册到全局的后端 fx graph 入口函数。</p>\n<p>torch/_inductor/async_compile.py 异步编译。维护了一个进程池,例如 <code>triton</code> 后端就通过下面这个函数,向进程池提交了一个编译任务然后返回了一个 future。</p>\n<pre><code class=\"language-python\">def triton(self, kernel_name: str, source_code: str, device_str: str = "cuda"):\n kernel = TritonCodeCache.load(kernel_name, source_code)\n return TritonFuture(\n kernel,\n self.process_pool().submit(\n _worker_compile_triton,\n kernel._reload_in_subproc,\n extra_env,\n ),\n )\n</code></pre>\n<p>所以能看出,多出来的那些进程,是通过这里生成的。可以通过下面的 <code>TORCHINDUCTOR_COMPILE_THREADS</code> 环境变量修改。<br>\n然后进程池是使用的 concurrent.futures.ProcessPoolExecutor,<strong>基于 fork,所以可能 cuda 没有用到,但是 /dev/nvidia0 等 fd 也被占用了</strong>。</p>\n<p>torch/_inductor/codecache.py 代码编译缓存,例如 CUDACodeCache 为 cuda 代码的编译缓存。如果需要,调用 <code>cuda_compile_command</code> 函数进行编译。</p>\n<h3>其他</h3>\n<p>torch/_inductor/config.py 中,通过 <code>decide_compile_threads</code> 获取了 cpu 核心数。通过环境变量 <code>TORCHINDUCTOR_COMPILE_THREADS</code> 可以修改。</p>\n<h2>cuda graph 代码</h2>\n<h3>前端 dynamo</h3>\n<p>torch/_dynamo/backends/cudagraphs.py 中的 <code>CudagraphsBackend</code>,通过 <code>register_backend</code> 注册到全局的 cuda graph 后端入口。</p>\n<h3>在后端 inductor</h3>\n<p>torch/_inductor/cudagraph_trees.py<br>\ntorch/_inductor/cudagraph_utils.py</p>\n<h2>torch 日志打印</h2>\n<p>pytorch2 中,<br>\n环境变量 TORCH_LOGS="+inductor,+dynamo"<br>\n或者也可以通过 API 直接设置 torch._logging.set_logs(all=logging.DEBUG)</p>\n<h2>c10/cuda/CUDACachingAllocator.cpp</h2>\n<p>torch 的两个显存分配器实现,PYTORCH_NO_CUDA_MEMORY_CACHING 控制版本。有一个torch 版本的新分配器依赖 NVML,平台不支持 NVML 的时候可以先用这个环境变量关了新分配器。</p>\n<h2>torch/nn/parallel/distributed.py</h2>\n<p>完全基于集合通信库的 DP 实现 <code>DistributedDataParallel</code>(DDP),各个 rank 独自持有模型。</p>\n<p>首先看 <code>DistributedDataParallel</code> 的构造函数。<code>self._module_parameters</code> 中存了所有未被参数过滤掉的模型参数。</p>\n<p>初始化阶段 <code>self._verify_param_shape_across_processes()</code>、<code>self._sync_module_states()</code> 用来在不同 rank 间检查、同步 module 的初始状态,这一步会用 broadcast 集合通信,buffer 大小在代码里写死了是 250 MB。最关键的是 <code>self._ddp_init_helper()</code> 去初始化 reducer,这一步完了基本初始化就结束了,剩下的就是一些混合精度 AMP 相关的代码等,该函数的注释如下,解释得很详细了</p>\n<pre><code class=\"language-python\">"""\nDDP init helper function to manage parameters, grad hooks, logging, and SyncBatchNorm.\nInitialization helper function that does the following:\n(1) bucketing the parameters for reductions\n(2) resetting the bucketing states\n(3) registering the grad hooks\n(4) Logging construction-time DDP logging data\n(5) passing a handle of DDP to SyncBatchNorm Layer\n"""\n</code></pre>\n<p>DDP 通过将参数划分到不同的桶里面,来实现梯度 reduction 和 反向传播的 overlap 优化,掩盖 reduction 耗时,桶的默认大小是 25MB,可以通过 <code>bucket_cap_mb</code> 参数控制,桶的初始化也在 <code>self._ddp_init_helper()</code> ,调用到的方法是 <code>torch/csrc/distributed/c10d/reducer.cpp</code> 里面定义的 <code>compute_bucket_assignment_by_size</code>,获取每个桶的 idx 和 size。划分好桶之后,再通过 <code>torch/csrc/distributed/c10d/reducer.cpp</code> 中实现的 <code>Reducer</code> 类型的构造函数,完成初始化。</p>\n<p><code>compute_bucket_assignment_by_size()</code>的逻辑是:先构造一个桶的哈希表,每个桶内可能有多个张量,哈希表的键是通过张量的数据类型和它所在的设备哈希出来的,张量数据的大小计算方式就是张量规模乘上它的数据类型的大小。当一个键对应的桶被塞满,就要将当前的桶添加到返回列表里面,然后为相应键重建一个桶。最后再把剩余的没满的桶填充到返回列表里面。这个函数的实现上有一个技巧,就是希望尽可能让返回的列表中桶的顺序按模型中张量出现的顺序排列。在没有 torch 上层代码提供提示的情况下,这个函数里面对返回列表中的每个桶里面最小的张量序号进行了排序,假设序号小的张量是优先出现在模型中的参数。回到<code>self._ddp_init_helper()</code>中,它又将 <code>compute_bucket_assignment_by_size()</code> 返回的列表翻转了一下,希望优先处理先被反向传播过程处理到的张量。</p>\n<p>前面提到的 <code>torch/csrc/distributed/c10d/reducer.cpp</code> 中的 <code>Reducer</code>,进行 all reduce 通信的方法实际上是 <code>Reducer::run_comm_hook()</code>。它的调用链路是 <code>Reducer::autograd_hook()-> Reducer::mark_variable_ready() -> Reducer::mark_bucket_ready() -> Reducer::all_reduce_bucket() -> Reducer::run_comm_hook()</code>。<code>Reducer::autograd_hook()</code> 被注册给了 <code>torch::autograd::impl::grad_accumulator::add_post_hook()</code>,会在梯度计算完毕累加到了梯度张量后执行,而且只会在 pytorch 的 autograd 线程上执行。</p>\n<blockquote>\n<p>torch 的 autograd 线程在 torch/csrc/autograd/engine.cpp 的 <code>Engine::start_device_threads()->Engine::thread_init()</code> 创建。</p>\n</blockquote>\n<pre><code class=\"language-cpp\">c10::intrusive_ptr<c10::ivalue::Future> Reducer::run_comm_hook(\n GradBucket& grad_bucket) {\n if (comm_hook_ == nullptr) {\n // `Reducer` 构造时的参数没有配置过的话,会从 `Reducer::process_group_` 构造一个 `_AllReduceBySumCommHook`,\n // 然后对每个 bucket 做 all reduce。\n return run_allreduce_hook(grad_bucket);\n } else {\n return comm_hook_->runHook(grad_bucket);\n }\n}\n</code></pre>\n<p>初始化函数里面还有一段有趣的代码是通过 <code>torch._dynamo.config._get_optimize_ddp_mode()</code> 获取了 torch 2 中编译器前端 torchdynamo 的设置,选择启用实验性的 python reducer,而不是用默认的 cpp 实现的 reducer。去读 <code>torch/_dynamo/config.py</code> 的话就会知道,<code>DistributedDataParallel</code> 默认会对 DDP 的通信和计算做 overlap 以掩盖通信开销。但是由于 torch 2 依赖于 PEP 523,所以纯 python 实现的 reducer 更有利于 torch 做自动分析和优化。比如通常路径上 all_reduce 操作调用的是 <code>torch.distributed.distributed_c10d.all_reduce()</code>,但是 python 版的是 <code>torch.distributed._functional_collectives.all_reduce()</code> 中的实现。通过 <code>DistributedDataParallel._get_active_ddp_module()</code> 类方法可以把 DDP 对象暴露给 torchdynamo,<code>DistributedDataParallel._inside_ddp_forward()</code> 这个 contextmanager 则是在调用 <code>DistributedDataParallel._run_ddp_forward()</code> 前就禁用了 torchdynamo。</p>\n<p>对于 <code>torch.distributed.distributed_c10d.all_reduce()</code>,它默认调用的是 <code>_get_default_group()</code> 获取的 <code>ProcessGroup</code> 上的 <code>all_reduce()</code> 接口,这实现在 <code>torch/csrc/distributed/c10d/ProcessGroupWrapper.cpp</code> 中的公共抽象 <code>ProcessGroupWrapper</code>,它继承于 <code>Backend</code> 类型。torch 支持了多种集合通信库,每个实现也都继承了 <code>Backend</code>,如 NCCL、GLOO、UCC 等。一般我们只关心 NCCL,它实现在 <code>torch/csrc/distributed/c10d/ProcessGroupNCCL.cpp</code>。</p>\n<p>通过 <code>PYTORCH_DDP_USE_SIDE_STREAM</code> 环境变量可以新开一个 cuda steram 做 H2D 的拷贝。</p>\n<h2>torch/utils/data/distributed.py</h2>\n<p><code>DistributedSampler</code> 配合 <code>DistributedDataParallel</code> 使用,对输入进行分片。<br>\n实现很简单,就是在 <code>self.__iter__</code> 函数中 <code>indices[self.rank : self.total_size : self.num_replicas]</code>,对整个 <code>self.total_size</code> 长的数据,间隔 <code>self.num_replicas</code> 按 <code>self.rank</code> 选一个。<code>self.rank</code> 可以通过 <code>dist.get_rank</code> 获取。</p>\n<h2>torch/distributed/</h2>\n<p><code>DTensor</code> 的各种 TP 方式的实现,和与其他并行方式的集成。DTensor 本身实现在 <code>torch/distributed/_tensor/api.py</code>。</p>\n<h3>tensor/parallel/ddp.py</h3>\n<p><code>DistributedDataParallel</code> 中调用的 <code>_pre_dp_module_transform()</code> 的实现,便于 DDP 和 TP 结合(torch 的 TP 依赖于 DTensor)。它注册了两个更新 DTensor 的钩子,一个用于在前向传播之前将本地张量转换回 DTensor,另一个用于在前向传播之后将 DTensor 转换回张量。避免 DDP 对 DTensor 参数的特殊处理,并使 DTensor 的梯度能够传递回 DDP 的梯度桶。</p>\n<pre><code class=\"language-python\">def _pre_dp_module_transform(module: nn.Module):\n _localize_dtensor(module, None, None)\n # Recontruct DTensor parameters from local tensors\n module.register_forward_pre_hook(_reconstruct_dtensor)\n # Convert DTensor parameters to local tensors\n module.register_forward_hook(_localize_dtensor)\n</code></pre>\n<h3>nn</h3>\n<p>定义了 torch 用户可以主动使用的集合通信接口 <code>torch.distributed.nn.functional</code>,主动创建位于远端进程的 module <code>torch.distributed.nn.RemoteModule</code>。</p>\n<h3>algorithms</h3>\n<p>一些分布式下的算法实现。</p>\n<p>如 <code>torch/distributed/algorithms/model_averaging/averagers.py</code> 定义了用户可以直接调用的对各个 rank 的参数做均值的 <code>PeriodicModelAverager</code>,可以用于主动同步模型参数、和 PostLocalSGDOptimizer 结合用于优化器等。</p>\n<p><code>torch/distributed/optim/</code> 实现分布式的优化器,例如 <code>PostLocalSGDOptimizer</code>、<code>ZeroRedundancyOptimizer</code>。</p>\n<p><code>torch/distributed/algorithms/_comm_hooks/default_hooks.py</code> 是分布式训练中默认的 hook,可以通过 <code>model.register_comm_hook</code> 注册别的 hook。如 <code>torch/distributed/algorithms/ddp_comm_hooks/</code> 下的 <code>allreduce_hook()</code>、<code>post_localSGD_hook()</code>。注册 hook 的函数是 <code>DistributedDataParallel.register_comm_hook()</code></p>\n<h2>torch/nn/parallel/data_parallel.py</h2>\n<p>实现了 <code>DataParallel</code>,仅持有一份模型,每次前向更新都会在不同设备间拷贝需要并行的参数,一般已不使用。</p>\n<p><code>torch.nn.parallel.replicate()</code> 用于在各个设备上复制模型,它主要调用了 <code>_broadcast_coalesced_reshape() -> comm._broadcast_coalesced()</code>。<code>broadcast_coalesced()</code> 是个 C 函数,实现在 <code>torch/csrc/cuda/comm.cpp</code>。具体实现在 <code>_broadcast_out_impl</code>,这里通过一个宏控制了是否使用 NCCL,否则就是直接 CUDA D2D 拷贝。</p>\n<pre><code class=\"language-python\">static inline std::vector<Tensor>& _broadcast_out_impl(\n const Tensor& tensor,\n std::vector<Tensor>& out_tensors) {\n#ifdef USE_NCCL\n std::vector<Tensor> nccl_list;\n nccl_list.reserve(out_tensors.size() + 1);\n nccl_list.emplace_back(tensor);\n for (auto& out_tensor : out_tensors) {\n nccl_list.emplace_back(out_tensor);\n }\n if (nccl::is_available(nccl_list)) {\n nccl::broadcast(nccl_list);\n } else {\n#else\n {\n#endif\n for (auto& out_tensor : out_tensors) {\n out_tensor.copy_(tensor, /*non_blocking=*/true);\n }\n }\n return out_tensors;\n}\n</code></pre>\n<p><code>torch.nn.parallel.parallel_apply()</code> 用于在不同设备上并行计算,走得就是在不同线程、不同 <code>torch.cuda.device()、torch.cuda.stream()</code> 下调用 module 的方式。</p>\n<h2>torch.randn() 的实现</h2>\n<p>randn的具体实现方式<br>\n/aten/src/ATen/native/TensorFactories.cpp: Tensor rand() -><br>\n/aten/src/ATen/native/Distributions.cpp: Tensor& uniform_() -><br>\n/aten/src/ATen/native/DistributionTemplates.h: at::Tensor& uniform_impl_() -><br>\n/aten/src/ATen/native/cuda/DistributionUniform.cu: void uniform_kernel() -><br>\n/aten/src/ATen/native/cuda/DistributionTemplates.h: void uniform_kernel(), void uniform_and_transform(),void distribution_nullary_kernel()<br>\nvoid uniform_and_transform() 里面根据数据类型,通过 distribution_nullary_kernel()加载了一个遍历 Tensor 的核函数,逐项调用 curand API curand_uniform4 或 curand_uniform2_double进行填充。<br>\n动态获取 cuda API 符号<br>\n<a href=\"https://github.com/pytorch/pytorch/blob/main/c10/cuda/driver_api.cpp#L40\">https://github.com/pytorch/pytorch/blob/main/c10/cuda/driver_api.cpp#L40</a><br>\n在单个单例中,搜索 cuda driver API 和 nvml API</p>\n<h2>/aten/src/ATen/native/TensorFactories.cpp</h2>\n<p>张量的工厂类</p>\n<h2>/aten/src/ATen/native/Convolution.cpp</h2>\n<p>卷积实现,<br>\n例如正向推理的 cudnn 实现<br>\ncudnn_convolution -> cudnn_convolution_forward -> raw_cudnn_convolution_forward_out -> raw_cudnn_convolution_forward_out_32bit -> cudnnConvolutionForward<br>\n<a href=\"https://github1s.com/pytorch/pytorch/blob/v2.1.0/aten/src/ATen/native/cudnn/ConvShared.cpp#L180\">https://github1s.com/pytorch/pytorch/blob/v2.1.0/aten/src/ATen/native/cudnn/ConvShared.cpp#L180</a><br>\n<a href=\"https://github1s.com/pytorch/pytorch/blob/v2.1.0/aten/src/ATen/native/cudnn/Conv_v7.cpp#L629\">https://github1s.com/pytorch/pytorch/blob/v2.1.0/aten/src/ATen/native/cudnn/Conv_v7.cpp#L629</a><br>\n例如反向传播的实现<br>\n<a href=\"https://github1s.com/pytorch/pytorch/blob/v2.1.0/aten/src/ATen/native/Convolution.cpp#L1974-L1978\">https://github1s.com/pytorch/pytorch/blob/v2.1.0/aten/src/ATen/native/Convolution.cpp#L1974-L1978</a><br>\n这里去根据后端选择对应实现,例如 cudnn_convolution_backward_stub,实现在 /aten/src/ATen/native/cudnn/ConvShared.cpp。</p>\n<h2>/aten/src/ATen/native/cuda/CUDALoops.cuh</h2>\n<p>借助 cuda 遍历 Tensor 中的每个元素,上述工厂类中会通过 Tensor::fill_()方法调用到它,对张量进行赋值。</p>\n<h2>/aten/src/ATen/native/native_functions.yaml</h2>\n<p>每个 native 算子有多个后端的 native 实现,该文件描述了这些变体。<br>\n例如 fft 变换,/aten/src/ATen/native/SpectralOps.cpp 中的 Tensor stft()调用了对应的 native 算子 _fft_r2c,这又对应了两类后端实现,_fft_r2c_cufft 和 _fft_r2c_mkl</p>\n<ul>\n<li>func: _fft_r2c(Tensor self, int[] dim, int normalization, bool onesided) -> Tensor<br>\nvariants: function<br>\ndispatch:<br>\nCPU: _fft_r2c_mkl<br>\nCUDA: _fft_r2c_cufft</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20241008/",
"title": "torch 源码阅读",
"summary": "Pytorch2 recap",
"date_modified": "2024-10-08T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>基于 <a href=\"https://github.com/kvcache-ai/ktransformers/tree/0f054fe4ff73133378a8de8ae17f47d5f3ec680a\">https://github.com/kvcache-ai/ktransformers/tree/0f054fe4ff73133378a8de8ae17f47d5f3ec680a</a></p>\n<p>kvcache-ai/ktransformers 是一个很有趣的推理服务器,只需要写一个 YAML 配置文件,就可以动态地注入借助 SIMD、<a href=\"https://github.com/IST-DASLab/marlin\">marlin</a> 等方法调优后的模块,替换原始模型中的结构。</p>\n<p>清华的章明星老师有一个讲座视频:<a href=\"https://www.bilibili.com/video/BV1CAW6eiENy/\">https://www.bilibili.com/video/BV1CAW6eiENy/</a></p>\n<p>先从服务端入口读起来</p>\n<h2>ktransformers/server/api/<strong>init</strong>.py</h2>\n<p>服务端的路由定义,支持多种风格:ollama、OpenAI、web</p>\n<h2>ktransformers/server/api/openai/endpoints/chat.py</h2>\n<p>OpenAI 接口实现在这里,使用的是 fastapi 构建的服务器,一些用户参数的解析可以在 ktransformers/server/schemas/endpoints/chat.py 里面看到,例如 <code>/chat/completions</code> 的参数 <code>ChatCompletionCreate</code> 现在其实只支持了 steram 类型响应,也忽略了 model 参数。</p>\n<h2>ktransformers/server/utils/create_interface.py</h2>\n<p>从上面 OpenAI 的 <code>/chat/completions</code> 实现可以继续找到后端推理服务的实现。该文件内实现了一个全局单例 GlobalInterface,server 在处理推理请求的时候都会去获取这个单例,调用 <code>inference</code> 接口执行推理,目前支持两种后端:transformers、ktransformers,都在 ktransformers/server/backend/interfaces 路径下,还有个 <code>exllamav2</code> 看上去还未实现。</p>\n<p>ktransformers 和 transformers 后端间是存在继承关系的:<code>KTransformersInterface -> TransformersInterface -> BackendInterfaceBase</code>。他们重要的成员都是 tokenizer、model、cache。只是 <code>KTransformersInterface</code> 里面的 model 是通过 <code>optimize_and_load_gguf()</code> 函数根据 YAML 配置优化过的模型。</p>\n<p><code>TransformersInterface</code> 做一次推理的完整的调用链是</p>\n<pre><code>inference()\n\t-> prefill()\n\t-> generate()\n\t\t-> decode_one_tokens()`。\n</code></pre>\n<p><code>KTransformersInterface</code> 只重载了 <code>TransformersInterface</code> 的 <code>decode_one_tokens()</code> 这个方法,也就是只有 decode 阶段是优化过的,prefill 阶段是默认的。</p>\n<h2>ktransformers/models/</h2>\n<p>这个目录下面放的都是模型的具体实现。<br>\n我们测试的 deepseek 就放在了这里,我们支持了 decode 阶段现的任意保存恢复启停,但是 prefill 阶段做保存恢复就 segmentfault 了。</p>\n<h2>ktransformers/operators/</h2>\n<p>Ktransformer 的调优实现,即开头提到的注入到模型中的模块。</p>\n<h2>ktransformers/util/cuda_graph_runner.py</h2>\n<p>一开始关注这个项目,就是听到前面的视频里面,章老师提到用到了他们用了 CUDA Graph,看了下这段用的是 torch.cuda 的接口,实现得很简洁清晰,刚好可以拿来做我们的测试用例。 直接去跑 Llama.cpp 我一直调用不到 CUDA Graph API = =</p>\n<p>这里实现的话就是借助了 CUDA API 的自动捕获能力。因为图的输入输出 buffer 都是 capture 得到的,所以需要注意保证在执行 CUDA Graph 的时候还是固定地址,因此可以看到 <code>CUDAGraphRunner::forward</code> 需要把数据拷贝到之前已经创建好的固定的 buffer 里面。</p>\n<pre><code class=\"language-python\">class CUDAGraphRunner:\ndef forward(\n self,\n cur_token,\n position_ids,\n cache_position,\n ) -> torch.Tensor:\n # Copy the input tensors to the input buffers.\n inputs_embeds = self.model.model.embed_tokens(cur_token.to("cpu"))\n self.input_buffers["inputs_embeds"].copy_(inputs_embeds)\n self.input_buffers["position_ids"].copy_(position_ids)\n self.input_buffers["cache_position"].copy_(cache_position)\n\n # Run the graph.\n self.graph.replay()\n torch.cuda.synchronize(self.main_device)\n # Return the output tensor.\n return self.output_buffers["logits"]\n\n</code></pre>\n<p><code>CUDAGraphRunner</code> 只用在了下面两个地方。</p>\n<h3>ktransformers/server/backend/interfaces/ktransformers.py</h3>\n<p>上文提到的 <code>KTransformersInterface::decode_one_tokens()</code> 实现,重载了 <code>TransformersInterface</code> 对应方法。<br>\ncuda graph 相关的实现也比较简单。如果是首次调用该函数会初始化 <code>CUDAGraphRunner</code>,然后 <code>CUDAGraphRunner::capture</code> 启动图捕获,借助 model 进行推理得到 logits 转换成 token。<br>\n然后再次调用时发现 <code>CUDAGraphRunner</code> 已经构造好了,就直接把参数传给 <code>CUDAGraphRunner::forward</code>。</p>\n<h3>ktransformers/util/utils.py</h3>\n<p><code>prefill_and_generate</code>,这个函数就是给 <code>ktransformers/local_chat.py</code> 用的,它是个用来调试的命令行工具。<br>\n值得注意的是整个项目目前支持的模型列表也是定义在 <code>ktransformers/local_chat.py</code> 这里的 = = 这个结构组织得有点乱,于是你可以看到在 KTransformersInterface 里面是 <code>from ktransformers.local_chat import custom_models, default_optimize_rules</code> 获取支持的模型列表和用于调优的 YAML 文件。</p>\n<pre><code class=\"language-python\">custom_models = {\n "DeepseekV2ForCausalLM": DeepseekV2ForCausalLM,\n "Qwen2MoeForCausalLM": Qwen2MoeForCausalLM,\n "MixtralForCausalLM": MixtralForCausalLM,\n}\n\nktransformer_rules_dir = os.path.dirname(os.path.abspath(__file__)) + "/optimize/optimize_rules/"\ndefault_optimize_rules ={\n "DeepseekV2ForCausalLM": ktransformer_rules_dir + "DeepSeek-V2-Chat.yaml",\n "Qwen2MoeForCausalLM": ktransformer_rules_dir + "Qwen2-57B-A14B-Instruct.yaml",\n "MixtralForCausalLM": ktransformer_rules_dir + "Mixtral.yaml",\n}\n</code></pre>\n",
"url": "https://forsworns.github.io///zh/blogs/20240924/",
"title": "kTransformers 源码阅读",
"summary": "可便捷调优的推理服务器",
"date_modified": "2024-09-24T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>基于 TensorRT-LLM-0.10.0,<a href=\"https://github.com/NVIDIA/TensorRT-LLM/releases/tag/v0.10.0\">https://github.com/NVIDIA/TensorRT-LLM/releases/tag/v0.10.0</a><br>\n最开始想看下 cuda graph 怎么开启的。<br>\ntrtllm-build 工具不像 trtexec,有 trtexec --useCudaGraph 这个选项</p>\n<h1>cpp/tensorrt_llm/runtime/gptSession.cpp</h1>\n<p>cpp runtime 中关于 cuda graph API,是 <code>GptSession::mCudaGraphMode</code> 这个变量控制的,它被设置成了外部的 <code>tr::GptSession::Config::cudaGraphMode</code> 的配置值。</p>\n<h1>cpp/tensorrt_llm/pybind/bindings.cpp</h1>\n<p>python binding 文件,可以看到例如 cpp 中的 <code>tr::GptSession::Config</code> 被映射成了 python 中的 <code>GptSessionConfig</code>。<br>\n它的成员被映射成了 <code>GptSessionConfig::cuda_graph_mode</code>。</p>\n<h1>tensorrt_llm/runtime/model_runner_cpp.py</h1>\n<p>包装了 cpp 目录下的具体实现,暴露成 python 接口。从 <code>ModelRunnerCppGptSession::from_dir()</code> 的实现里面实际上可以看到 <code>GptSessionConfig</code> 这些配置项都是怎么传递进去的。实际上没有传递 cuda graph 那个参数。</p>\n<h1>tensorrt_llm/runtime/generation.py</h1>\n<p>python runtime 中,同样是在 decode 阶段使用 cuda graph 加速。</p>\n<p>在调用 <code>tensorrt_llm.runtime.GenerationSession()</code> 的时候,配置一下 <code>cuda_graph_mode=True</code> 即可。也就是改一下例如 <code>/examples/llama/summarize_long.py</code> 这样的示例代码。<br>\ntensorrt-llm 自己的 benchmark 里面倒是加了对应的开关,只不过是示例代码里面省略了。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240916/",
"title": "TensorRT-LLM 源码阅读",
"summary": "再看看 cuda graph 咋开启的 ...",
"date_modified": "2024-09-16T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>基于 <a href=\"https://github.com/ollama/ollama/tree/123a722a6f541e300bc8e34297ac378ebe23f527\">https://github.com/ollama/ollama/tree/123a722a6f541e300bc8e34297ac378ebe23f527</a></p>\n<p>ollama 是一个通用的 llm 推理服务器,借助 llama.cpp 进行推理。<br>\nollama 0.1.44 镜像内,将 llama.cpp 编写的推理服务器放到了 /tmp/ollama1917690259/runners/下,以 cuda 后端为例,为 /tmp/ollama1917690259/runners/cuda_v11/ollama_llama_server。<br>\n当我们调用 ollama serve后,只会启动一个 go 编写的 server;当我们执行 ollama run qwen:7b它会拉起 llama.cpp server,然后作为反向代理转发我们的请求给 lamma.cpp server。</p>\n<h2>gpu/gpu.go</h2>\n<p>一开始看 ollama 的代码是碰到了问题,想看下 go server 侧为什么会调用到 cuda API。</p>\n<p>GetGPUInfo()->initCudaHandles()这里会打开 <a href=\"http://libnvidia-ml.so\">libnvidia-ml.so</a>、<a href=\"http://libcuda.so\">libcuda.so</a>、<a href=\"http://libcudart.so\">libcudart.so</a>。<br>\n会去找这些库中的一些符号,go server 运行期间通过 cgo 调用他们。</p>\n<h2>server/routes.go</h2>\n<p><code>func Serve(ln net.Listener)</code> 函数也就是调用 ollama serve时执行的函数,它在启动 go server 前会去调用上面提到的<code>GetGPUInfo()</code>获取 GPU 信息。</p>\n<p>看下这个文件的其他内容</p>\n<p><code>func (s *Server) GenerateRoutes()</code> 配置路由和对应的 handler。<br>\n以 <code>r.POST("/api/embeddings", s.EmbeddingsHandler)</code> 为例,<code>func (s *Server) EmbeddingsHandler(c *gin.Context)</code> 中解析请求参数,调用 <code>s.sched.GetRunner</code> 阻塞直到获取到 runner。<br>\n再调用 runner.llama.Embedding 获取 llama.cpp 中的模型服务,获取响应返回给用户。</p>\n<pre><code class=\"language-go\">func (s *Server) EmbeddingsHandler(c *gin.Context) {\n var req api.EmbeddingRequest\n err := c.ShouldBindJSON(&req)\n model, err := GetModel(req.Model)\n opts, err := modelOptions(model, req.Options)\n\n rCh, eCh := s.sched.GetRunner(c.Request.Context(), model, opts, req.KeepAlive.Duration)\n var runner *runnerRef\n select {\n case runner = <-rCh:\n case err = <-eCh:\n handleErrorResponse(c, err)\n return\n }\n\n embedding, err := runner.llama.Embedding(c.Request.Context(), req.Prompt)\n\n resp := api.EmbeddingResponse{\n Embedding: embedding,\n }\n c.JSON(http.StatusOK, resp)\n}\n</code></pre>\n<h2>server/sched.go</h2>\n<p>除了直接调用 <code>GetGPUInfo()</code>,该函数还可能通过该文件下的 <code>Scheduler.getGpuFn</code> 函数指针调用,包含下面两个调用处</p>\n<ul>\n<li>func (s *Scheduler) processPending(ctx context.Context)</li>\n<li>func (runner *runnerRef) waitForVRAMRecovery()</li>\n</ul>\n<p>看下这个文件的其他内容</p>\n<p><code>runnerRef</code> 是调度的实体,对应请求中的 <code>req.model.ModelPath</code>,为这个模型启动 llama.cpp 服务器。</p>\n<p><code>GetRunner</code>,把用户请求 req 写入了 <code>s.pendingReqCh</code>,如果失败了把错误写入到 <code>req.errCh</code>,没有失败的时候,会阻塞在 <code>req.successCh</code>。</p>\n<pre><code class=\"language-go\">func (s *Scheduler) GetRunner(c context.Context, model *Model, opts api.Options, sessionDuration time.Duration) (chan *runnerRef, chan error) {\n req := &LlmRequest{\n ctx: c,\n model: model,\n opts: opts,\n sessionDuration: sessionDuration,\n successCh: make(chan *runnerRef),\n errCh: make(chan error, 1),\n }\n select {\n case s.pendingReqCh <- req:\n default:\n req.errCh <- ErrMaxQueue\n }\n return req.successCh, req.errCh\n}\n</code></pre>\n<p><code>Run</code> 函数会创建两个 go routine 去分别处理等待队列和完成队列,刚刚的 <code>s.pendingReqCh </code> 中的请求就是在 <code>processPending</code> 中处理的。任务成功后写回到 <code>req.successCh</code>。</p>\n<pre><code class=\"language-go\">func (s *Scheduler) Run(ctx context.Context) {\n go func() {\n s.processPending(ctx)\n }()\n go func() {\n s.processCompleted(ctx)\n }()\n}\n</code></pre>\n<p><code>func (s *Scheduler) load(req *LlmRequest, ggml *llm.GGML, gpus gpu.GpuInfoList)</code> 调用 <code>Scheduler.newServerFn</code>,也就是 <code>llm/server.go</code> 中的 <code>NewLlamaServer</code> 创建 llama.cpp 服务;同时创建调度实体 <code>runnerRef</code>。</p>\n<p><code>func (s *Scheduler) findRunnerToUnload()</code> 用来寻找一个最合适被关闭的 runnerRef,会先去看 runner.refCount 这个引用计数,看是否有空闲的 runnerRef,如果有就把它关闭;否则就给所有 runnerRef 按 runnerRef.sessionDuration 排序,返回马上要执行完成的 runner。</p>\n<h2>initCudaHandles中寻找的符号</h2>\n<ul>\n<li>gpu/gpu_info_nvcuda.h:cuda_handle_t</li>\n<li>gpu/gpu_info_cudart.h:cudart_handle_t</li>\n<li>gpu/gpu_info_nvml.h:nvml_handle_t</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20240909/",
"title": "ollama 源码阅读",
"summary": "推理服务器",
"date_modified": "2024-09-09T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>之前看了 Andrej Karpathy 的 Tokenizer 视频,最近他又发了一个从零复现 GPT-2 的视频,学习一个。</p>\n<p>相关博客</p>\n<p>网上读到 Maeiee 记录的,Karpathy 之前另一期从零搭建 GPT 视频的学习笔记 <a href=\"https://garden.maxieewong.com/087.%E8%A7%86%E9%A2%91%E5%BA%93/YouTube/Andrej%20Karpathy/Let%27s%20build%20GPT%EF%BC%9Afrom%20scratch,%20in%20code,%20spelled%20out./#step6-bigram-language-model-v1\">Let's build GPT:from scratch, in code, spelled out.</a>。</p>\n<p>仓库地址</p>\n<ul>\n<li>视频配套的教学仓库 build-nanogpt: <a href=\"https://github.com/karpathy/build-nanogpt\">https://github.com/karpathy/build-nanogpt</a></li>\n<li>nanoGPT: <a href=\"https://github.com/karpathy/nanoGPT\">https://github.com/karpathy/nanoGPT</a></li>\n<li>llm.c: <a href=\"https://github.com/karpathy/llm.c\">https://github.com/karpathy/llm.c</a></li>\n</ul>\n<p><a href=\"https://www.youtube.com/watch?v=l8pRSuU81PU\">视频地址</a></p>\n<p>论文</p>\n<ul>\n<li>Transformer:<a href=\"https://arxiv.org/abs/1706.03762\">Attention is All You Need</a></li>\n<li>GPT-2:<a href=\"https://d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf\">Language Models are Unsupervised Multitask Learners</a>,论文中的参数配置信息不够具体,需要参考 GPT-3 的论文。</li>\n<li>GPT-3:<a href=\"https://arxiv.org/abs/2005.14165\">Language Models are Few-Shot Learners</a></li>\n</ul>\n<h2>引言</h2>\n<p>复现的是 124M 的模型,原始论文中的参数统计数据有误。该模型由 12个 768 channel、768 dimension 的 transformer 构成。</p>\n<p>借助 HuggingFace 的 transformers 库,打印了 GPT-2 中的张量信息。首先是输入层,tokenizer embedding 的规模是 50257,每个 token 被表示为 768 维的向量,所以 wte 是一个 50257x768 的矩阵;position embedding 上下文长度为 1024,每个位置由 768 维的向量编码,所以 wpe 是一个 1024x768 的矩阵。<br>\n原始 transformer 文章使用固定的正弦余弦波来做位置编码(考虑到正弦波的加法性质),GPT-2将位置编码也参数化了。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240626/",
"title": "Let's reproduce GPT-2 笔记",
"summary": "学习了一下 Andrej Karpathy 大神的 GPT-2 视频课程",
"date_modified": "2024-06-26T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h2>ollama</h2>\n<p>ollama基于 <a href=\"https://github.com/ollama/ollama/blob/ccef9431c8aae4ecfd0eec6e10377d09cb42f634\">https://github.com/ollama/ollama/blob/ccef9431c8aae4ecfd0eec6e10377d09cb42f634</a></p>\n<h3>llm/</h3>\n<h4>server.go</h4>\n<p>go 写的 server,主体为 <a href=\"https://github.com/ollama/ollama/blob/ccef9431c8aae4ecfd0eec6e10377d09cb42f634/llm/server.go#L80\"><code>NewLlamaServer</code></a></p>\n<p>它会拉起多个进程,分别执行下面的 ext_server/server.cpp 中,基于 llama.cpp 实现的真正做推理服务的 server。</p>\n<h4>ext_server/server.cpp</h4>\n<p>把 llama.cpp 导入成了一个 submodule,基于 llama.cpp 开发的一个推理服务器。</p>\n<h2>llama.cpp</h2>\n<p>llama.cpp 基于 <a href=\"https://github1s.com/ggerganov/llama.cpp/blob/45c0e2e4c1268c2d7c8c45536f15e3c9a731ecdc/llama.h\">https://github1s.com/ggerganov/llama.cpp/blob/45c0e2e4c1268c2d7c8c45536f15e3c9a731ecdc/llama.h</a></p>\n<p>看编译脚本上是默认开 cuda graph 优化,但是用 ollama 起的服务器跑的时候没有用到。</p>\n<p>CmakeLists.txt 里面声明了只要找到了 libcuda,就会定义 GGML_CUDA_USE_GRAPHS 开启 cuda graph 优化。Makefile 中同样,只要是声明了 <code>make LLAMA_CUDA=1</code> 就会定义 <code>GGML_CUDA_USE_GRAPHS</code> 开启 cuda graph 优化。</p>\n<h3>llama.cpp</h3>\n<p>对外的 llama.cpp 库 API 实现</p>\n<h3>ggml.c</h3>\n<p>被 llama.cpp 包了一层的内部 API</p>\n<h3>ggml-backend.c</h3>\n<p>不同的后端通过 <code>ggml_backend_register</code> 注册自身,<code>ggml_backend_registry_init</code> 运行时分别调用他们,这里利用了一个技巧避免引入头文件。</p>\n<pre><code class=\"language-cpp\">GGML_CALL static void ggml_backend_registry_init(void) {\n ggml_backend_register("CPU", ggml_backend_reg_cpu_init, ggml_backend_cpu_buffer_type(), NULL);\n\n // add forward decls here to avoid including the backend headers\n#ifdef GGML_USE_CUDA\n extern GGML_CALL void ggml_backend_cuda_reg_devices(void);\n ggml_backend_cuda_reg_devices();\n#endif\n // …\n}\n</code></pre>\n<p>实现 <code>static struct ggml_backend_i cpu_backend_i </code> 后端。</p>\n<h3>ggml-blas.cpp</h3>\n<p>实现 <code>static struct ggml_backend_i blas_backend_i</code> 后端。</p>\n<h3><a href=\"http://ggml-cuda.cu\">ggml-cuda.cu</a></h3>\n<p>实现 <code>static ggml_backend_i ggml_backend_cuda_interface</code> 后端。</p>\n<p>cuda graph 是由 <a href=\"https://github.com/ggerganov/llama.cpp/commit/bc4bba364fb96d908f2698e908648df5e6f55e02\">https://github.com/ggerganov/llama.cpp/commit/bc4bba364fb96d908f2698e908648df5e6f55e02</a> 这个 commit-bc4b 引入的。</p>\n<h3>ggml-cuda</h3>\n<h4>cpy.cuh</h4>\n<p>commit-bc4b 为 <code>struct ggml_backend_cuda_context</code> 新增了一个成员,<code>std::unique_ptr<ggml_cuda_graph> cuda_graph</code>。看上去一个 context 只会捕获出一个 cuda graph。</p>\n<p>结构体 <code>ggml_cuda_graph</code> 在析构的时候会自动调用 <code>cudaGraphExecDestroy</code> 和 <code>cudaGraphDestroy</code> 清理之前捕获到的 cuda graph。它的定义比较简单,如下</p>\n<pre><code class=\"language-cpp\">struct ggml_cuda_graph {\n cudaGraph_t graph = nullptr;\n cudaGraphExec_t instance = nullptr;\n size_t num_nodes = 0;\n std::vector<cudaGraphNode_t> nodes;\n std::vector<cudaKernelNodeParams> params;\n // 禁用该 feature 的几种可能的原因\n bool disable_due_to_gpu_arch = false;\n // 如果当前用例中,图节点更新得太快,那图需要一直重建,建图的开销可能会大于 cuda graph 节省的开销。\n bool disable_due_to_too_many_updates = false;\n bool disable_due_to_failed_graph_capture = false;\n int number_consecutive_updates = 0;\n std::vector<ggml_graph_node_properties> ggml_graph_properties;\n std::vector<char **> updated_kernel_arg;\n};\n\nstruct ggml_graph_node_properties {\n void * node_address;\n // 同 `ggml_tensor` 上的 `ggml_op`,例如 `GGML_OP_CPY`、`GGML_OP_VIEW`\n ggml_op node_op;\n // 同 `ggml_tensor` 上的 `ne`\n int64_t ne[GGML_MAX_DIMS];\n // 同 `ggml_tensor` 上的 `nb`\n size_t nb[GGML_MAX_DIMS];\n // 同 `ggml_tensor` 上的 `src[i]->data`\n void * src_address[GGML_MAX_SRC];\n};\n</code></pre>\n<p><code>set_ggml_graph_node_properties</code> 从一个 <code>ggml_tensor</code> 构建一个 <code>ggml_graph_node_properties</code>,转换成图里的节点。<br>\n<code>ggml_graph_node_has_matching_properties</code> 比较 <code>ggml_tensor</code> 和 <code>ggml_graph_node_properties</code> 的成员,判断二者是否匹配。</p>\n<p><code>ggml_backend_cuda_graph_compute</code> 中根据参数 <code>ggml_backend_t</code> 和 <code>ggml_cgraph</code> 去构建 <code>ggml_backend_t->ggml_backend_cuda_context->ggml_cuda_graph</code>。这个函数里面首先检查了是不是安培以下的 GPU,如果是,就不用 cuda graph 了。之前在 T4 上测试的,所以没用到 cuda graph。有点坑爹 release 模式下不开 <code>LLAMA_DEBUG</code>,这错误日志就不打了。</p>\n<pre><code class=\"language-cpp\">GGML_CALL static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {\n // ...\n if (cuda_ctx->cuda_graph->graph == nullptr) {\n if (ggml_cuda_info().devices[cuda_ctx->device].cc < CC_AMPERE) {\n cuda_ctx->cuda_graph->disable_due_to_gpu_arch = true;\n#ifndef NDEBUG\n GGML_CUDA_LOG_WARN("%s: disabling CUDA graphs due to GPU architecture\\n", __func__);\n#endif\n }\n }\n // ...\n}\n</code></pre>\n<p>如果启用 cuda graph,则比较当前传入的 <code>ggml_cgraph</code> 和之前当前的 cuda graph 是否相同</p>\n<pre><code class=\"language-cpp\">GGML_CALL static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {\n // ...\n if (cuda_ctx->cuda_graph->instance == nullptr) {\n cuda_graph_update_required = true;\n }\n\n // Check if the graph size has changed\n if (cuda_ctx->cuda_graph->ggml_graph_properties.size() != (size_t)cgraph->n_nodes) {\n cuda_graph_update_required = true;\n cuda_ctx->cuda_graph->ggml_graph_properties.resize(cgraph->n_nodes);\n }\n\n // Loop over nodes in GGML graph to determine if CUDA graph update is required\n // and store properties to allow this comparison for the next token\n for (int i = 0; i < cgraph->n_nodes; i++) {\n bool has_matching_properties = true;\n if (!cuda_graph_update_required) {\n has_matching_properties = ggml_graph_node_has_matching_properties(cgraph->nodes[i], &cuda_ctx->cuda_graph->ggml_graph_properties[i]);\n }\n if (!has_matching_properties) {\n cuda_graph_update_required = true;\n }\n set_ggml_graph_node_properties(cgraph->nodes[i], &cuda_ctx->cuda_graph->ggml_graph_properties[i]);\n }\n // ...\n}\n</code></pre>\n<p>再次遍历当前的 <code>ggml_cgraph</code>,更新 <code>GGML_OP_CPY</code> 类型节点的信息,因为拷贝操作的地址会随着 token 变化。</p>\n<pre><code class=\"language-cpp\">GGML_CALL static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {\n // ...\n // Loop over nodes in GGML graph to obtain info needed for CUDA graph\n cuda_ctx->cuda_graph->updated_kernel_arg.clear();\n for (int i = 0; i < cgraph->n_nodes; i++) {\n ggml_tensor * node = cgraph->nodes[i];\n\n if (node->src[0] && ggml_backend_buffer_is_cuda_split(node->src[0]->buffer)) {\n use_cuda_graph = false; // Split buffers are not supported by CUDA graph capture\n }\n\n if (node->op == GGML_OP_MUL_MAT_ID) {\n use_cuda_graph = false; // This node type is not supported by CUDA graph capture\n }\n\n if (node->op == GGML_OP_ADD && node->src[1] && node->src[1]->ne[1] > 1) {\n // disable CUDA graphs for batch size > 1 for now.\n // Changes in batch size or context size can cause changes to the grid size of some kernels.\n use_cuda_graph = false;\n }\n\n if (node->op == GGML_OP_CPY) {\n // store the copy op parameter which changes with each token.\n cuda_ctx->cuda_graph->updated_kernel_arg.push_back((char **) &(node->src[1]->data));\n // store a pointer to each copy op CUDA kernel to identify it later\n void * ptr = ggml_cuda_cpy_fn(node->src[0], node->src[1]);\n if (std::find(ggml_cuda_cpy_fn_ptrs.begin(), ggml_cuda_cpy_fn_ptrs.end(), ptr) == ggml_cuda_cpy_fn_ptrs.end()) {\n ggml_cuda_cpy_fn_ptrs.push_back(ptr);\n }\n }\n\n if (!use_cuda_graph) {\n break;\n }\n }\n\n // Disable CUDA graphs (from the next token) if the use-case is demanding too many consecutive graph updates.\n if (use_cuda_graph && cuda_graph_update_required) {\n cuda_ctx->cuda_graph->number_consecutive_updates++;\n } else {\n cuda_ctx->cuda_graph->number_consecutive_updates = 0;\n }\n // 连续四次 token 的推理(调用 `ggml_backend_cuda_graph_compute`),图都发生了变化,就放弃继续使用 cuda graph\n if (cuda_ctx->cuda_graph->number_consecutive_updates >= 4) {\n cuda_ctx->cuda_graph->disable_due_to_too_many_updates = true;\n }\n // ...\n}\n</code></pre>\n<p>然后开始借助 cudaStreamBeginCapture 捕获下面的推理过程中的 CUDA API 调用。注意如果没有开启 cuda graph,下面的这段是每次需要 eager 地执行</p>\n<pre><code class=\"language-cpp\">GGML_CALL static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {\n // ...\n // Only perform the graph execution if CUDA graphs are not enabled, or we are capturing the graph.\n // With the use of CUDA graphs, the execution will be performed by the graph launch.\n if (!use_cuda_graph || cuda_graph_update_required) {\n for (int i = 0; i < cgraph->n_nodes; i++) {\n ggml_tensor * node = cgraph->nodes[i];\n if (ggml_is_empty(node) || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE || node->op == GGML_OP_NONE) {\n continue;\n }\n bool ok = ggml_cuda_compute_forward(*cuda_ctx, node);\n if (!ok) {\n GGML_CUDA_LOG_ERROR("%s: op not supported %s (%s)\\n", __func__, node->name, ggml_op_name(node->op));\n }\n GGML_ASSERT(ok);\n }\n }\n // ...\n}\n</code></pre>\n<p>捕获完成,调用 <code>cudaGraphInstantiate</code> 实例化 cuda graph 成 <code>cudaGraphExec</code>,再根据上面统计到的 <code>GGML_OP_CPY</code> 相关的信息,更新 cuda graph,最后调用 <code>cudaGraphExecUpdate</code> 更新 <code>cudaGraphExec</code>。</p>\n<pre><code class=\"language-cpp\">GGML_CALL static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {\n // ...\n if (cuda_ctx->cuda_graph->instance == nullptr) { // Create executable graph from captured graph.\n CUDA_CHECK(cudaGraphInstantiate(&cuda_ctx->cuda_graph->instance, cuda_ctx->cuda_graph->graph, NULL, NULL, 0));\n }\n\n // Perform update to graph (if required for this token), and change copy parameter (required for every token)\n\n if (cuda_graph_update_required) {\n // Extract nodes from graph\n // First call with null argument gets number of nodes in graph\n CUDA_CHECK(cudaGraphGetNodes(cuda_ctx->cuda_graph->graph, nullptr, &cuda_ctx->cuda_graph->num_nodes));\n // Subsequent call with non-null argument gets nodes\n cuda_ctx->cuda_graph->nodes.resize(cuda_ctx->cuda_graph->num_nodes);\n cuda_ctx->cuda_graph->params.resize(cuda_ctx->cuda_graph->num_nodes);\n if (cuda_ctx->cuda_graph->num_nodes > 0) {\n CUDA_CHECK(cudaGraphGetNodes(cuda_ctx->cuda_graph->graph, cuda_ctx->cuda_graph->nodes.data(), &cuda_ctx->cuda_graph->num_nodes));\n\n // Loop over nodes, and extract kernel parameters from each node\n for (size_t i = 0; i < cuda_ctx->cuda_graph->num_nodes; i++) {\n cudaGraphNodeType node_type;\n CUDA_CHECK(cudaGraphNodeGetType(cuda_ctx->cuda_graph->nodes[i], &node_type));\n if (node_type == cudaGraphNodeTypeKernel) {\n cudaError_t stat = cudaGraphKernelNodeGetParams(cuda_ctx->cuda_graph->nodes[i], &cuda_ctx->cuda_graph->params[i]); // Get params using runtime\n if (stat == cudaErrorInvalidDeviceFunction) {\n // Fails due to incorrect handling by CUDA runtime of CUDA BLAS node.\n // We don't need to update blas nodes, so clear error and move on.\n cudaGetLastError();\n } else {\n GGML_ASSERT(stat == cudaSuccess);\n }\n }\n }\n }\n }\n\n // One of the arguments to the copy kernel is updated for each token, hence we need to\n // replace that argument with the updated value in the CUDA graph\n if (!cuda_graph_update_required) { // on update steps, the live parameters will already be captured\n int k = 0;\n for (size_t i = 0; i < cuda_ctx->cuda_graph->num_nodes; i++) {\n if(count(ggml_cuda_cpy_fn_ptrs.begin(), ggml_cuda_cpy_fn_ptrs.end(), cuda_ctx->cuda_graph->params[i].func) > 0) {\n char ** updated_kernel_arg_ptr = cuda_ctx->cuda_graph->updated_kernel_arg.at(k++);\n cuda_ctx->cuda_graph->params[i].kernelParams[1] = updated_kernel_arg_ptr;\n CUDA_CHECK(cudaGraphKernelNodeSetParams(cuda_ctx->cuda_graph->nodes[i], &cuda_ctx->cuda_graph->params[i]));\n }\n }\n }\n\n // Update graph executable\n cudaGraphExecUpdateResultInfo result_info;\n cudaError_t stat = cudaGraphExecUpdate(cuda_ctx->cuda_graph->instance, cuda_ctx->cuda_graph->graph, &result_info);\n // ...\n}\n</code></pre>\n<p>有了新的 <code>cudaGraphExec</code>,就可以运行这个图了。</p>\n<h4><a href=\"http://cpy.cu\">cpy.cu</a></h4>\n<p>commit-bc4b 定义了一个通用的函数 <code>ggml_cuda_cpy_fn</code> 根据张量类型选择拷贝函数。</p>\n<h3>examples</h3>\n<h4>simple</h4>\n<p><a href=\"https://github1s.com/ggerganov/llama.cpp/blob/45c0e2e4c1268c2d7c8c45536f15e3c9a731ecdc/examples/simple/simple.cpp\">https://github1s.com/ggerganov/llama.cpp/blob/45c0e2e4c1268c2d7c8c45536f15e3c9a731ecdc/examples/simple/simple.cpp</a></p>\n<p>一个简单 llama.cpp 应用用例。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240623/",
"title": "ollama/llama.cpp 源码阅读",
"summary": "最开始是想看下为什么 cuda graph 没有被启用",
"date_modified": "2024-06-23T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p><a href=\"https://github.com/ROCm/hipother\">https://github.com/ROCm/hipother</a><br>\nHip 的 nvidia 头文件</p>\n<p><a href=\"https://github.com/ROCm/clr\">https://github.com/ROCm/clr</a><br>\nhip/rocm release 6.2</p>\n<h1>clr/hipamd/src/</h1>\n<h2>hip_device.cpp</h2>\n<p>ihipDeviceGet 和 cuda 一样,默认 ordinal 就是 hipDevice_t</p>\n<p><img src=\"./1.jpg\" alt=\"\"></p>\n<p>常规 API 和 graph API 的默认的 memory pool 是分开的。</p>\n<p><img src=\"./2.jpg\" alt=\"\"></p>\n<p>在 stream 上做同步,是通过遍历每个 steram 上的 amd::Command,加入到一个列表里,然后实例化一个 amd::Marker 类型的 amd::Command,标识阻塞任务。<br>\nlambda 函数 waitForStream 捕获了当前上下文的 eventWaitList 和 submitMarker,累积需要同步的 event。<br>\n和 cuda 一样,stream 的同步,需要判断是不是特殊的 null stream。</p>\n<p><img src=\"./3.jpg\" alt=\"\"></p>\n<h2>hip_device_runtime.cpp</h2>\n<p>device 对外的 api。比如 hipDeviceGetAttribute 会调用 device.cpp 里面的 ihipGetDeviceProperties。</p>\n<h2>hip_context.cpp</h2>\n<p>rocm 创建 ctx,就是将 primary ctx 直接引用计数增加了一次。</p>\n<p><img src=\"./4.jpg\" alt=\"\"></p>\n<p>hipCtxSynchronize 这个 API 看代码就是不支持, 难怪之前看 cuMemFree 的那个问题的时候,cuda 是按 ctx 做的同步,rocm 直接整个device 做的同步……不过这个也挺奇怪,为什么不直接调用下 hipDeviceSynchronize,而是抛个 not support 错误。</p>\n<p><img src=\"./5.jpg\" alt=\"\"></p>\n<p>其实直接从下面这个 API 就可以看出来,对外的模糊结构体指针 hipCtx_t 本身就是一个 hip:Device 类型的指针,ctx 和 device 其实是不做区分的。</p>\n<p><img src=\"./6.jpg\" alt=\"\"></p>\n<p>Amd 文档里倒是写明了 ctx 相关 api 并不是完全支持,但是这感觉在 cuda 12 的 green ctx 出来以后,兼容性会更加严重</p>\n<p><img src=\"./7.jpg\" alt=\"\"></p>\n<p>也可以去看下 hipify 的兼容性文档<br>\n<a href=\"https://github.com/ROCm/HIPIFY/blob/455103c5428d3bf6dfe6351c93d7bde222bb9f86/docs/tables/CUDA_Driver_API_functions_supported_by_HIP.md\">https://github.com/ROCm/HIPIFY/blob/455103c5428d3bf6dfe6351c93d7bde222bb9f86/docs/tables/CUDA_Driver_API_functions_supported_by_HIP.md</a></p>\n<h2>hip_stream_ops.cpp</h2>\n<p>实现 hip::ihipStreamOperation,构建 amd::StreamOperationCommand 类型的 amd::Command。</p>\n<h2>hip_stream.cpp</h2>\n<h2>hip_table_interface.cpp</h2>\n<p>获取 HipCompilerDispatchTable 和 HipDispatchTable 上的函数指针。</p>\n<p>搞这么一个 dispatch table 结构体让 API 维持稳定,参考 clr/hipamd/src/hip_api_trace.cpp</p>\n<h1>clr/rocclr/platform/command.hpp</h1>\n<p>amd::Command 类型是提交给硬件 command 队列的类,抽象了所有OpenCL operations(因为 hip 后来才搞的,所以和 OpenCL 共用了这个类),提交给 HostQueue 执行。继承 amd::Command 的子类都需要实现 submit 接口。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240622/",
"title": "AMD ROCm 源码阅读",
"summary": "AMD YES ~",
"date_modified": "2024-06-22T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>TL;DR</p>\n<blockquote>\n<p>守护进程应该参考 <a href=\"https://github.com/NVIDIA/nvidia-persistenced/blob/a17da42d9f5e0db228e9e95eb604fad4f06f8a5f/nvidia-persistenced.c#L740\">nvidia-persistenced</a> 去写</p>\n</blockquote>\n<p>我有一个命令行应用,想要实现成守护进程,是这么写的</p>\n<pre><code class=\"language-rust\">use nix::unistd::{ForkResult, fork};\n\nfn main() {\n\tmatch unsafe { fork() } {\n\t\tOk(ForkResult::Parent { child, .. }) => {\n\t\t\t// 进程A,没有去调用 wait 等待子进程 B;\n\t\t\t// A 进程等待到 B 启动后退出\n\t\t}\n\t\tOk(ForkResult::Child) => {\n\t\t\t// A fork 出的进程 B,未被 wait,成为孤儿进程;\n\t\t\t// 启动一个 server,死循环不退出\n\t\t}\n\t}\n}\n</code></pre>\n<p>另外有一个 rust 编写的进程 C 去运行这个应用,它的实现和对应的现象是:<br>\n在进程 C 中 systemd 启动上述应用的服务,一切正常;<br>\n但是在没有 systemd 的环境下,进程 C 中会直接调用 <code>std::process::Command</code> 上述应用,调用了 <code>std::process::Command::output()</code> 等待应用退出。但是此时整个 C 进程就卡在了这里。此时通过 ps 命令可以查到,A 变成了一个僵尸进程。</p>\n<p>换了一下进程 C 中的实现,发现调用 <code>std::process::Child::wait()</code> 就没问题了,<code>std::process::Child::wait_with_output()</code> 则会出现上面的现象。</p>\n<p>阅读一下该方法的实现,可以发现它会先尝试读取 stdio 的 stdout 和 stderr,再进行 wait。于是这里 C 进程卡住的原因就是 A 进程的子进程 B 继承了 A 的 stdio fd,</p>\n<p><a href=\"https://github.com/rust-lang/rust/blob/1d96de2a20e963abb8923dfa3c6175517dfb9d2c/library/std/src/sys_common/process.rs#L136\">https://github.com/rust-lang/rust/blob/1d96de2a20e963abb8923dfa3c6175517dfb9d2c/library/std/src/sys_common/process.rs#L136</a></p>\n<pre><code class=\"language-rust\">pub fn wait_with_output(\n mut process: Process,\n mut pipes: StdioPipes,\n) -> io::Result<(ExitStatus, Vec<u8>, Vec<u8>)> {\n drop(pipes.stdin.take());\n\n let (mut stdout, mut stderr) = (Vec::new(), Vec::new());\n match (pipes.stdout.take(), pipes.stderr.take()) {\n (None, None) => {}\n (Some(out), None) => {\n let res = out.read_to_end(&mut stdout);\n res.unwrap();\n }\n (None, Some(err)) => {\n let res = err.read_to_end(&mut stderr);\n res.unwrap();\n }\n (Some(out), Some(err)) => {\n let res = read2(out, &mut stdout, err, &mut stderr);\n res.unwrap();\n }\n }\n\n let status = process.wait()?;\n Ok((status, stdout, stderr))\n}\n</code></pre>\n<p>所以需要 close 关闭或 dup 重定向之前的 fd。</p>\n<pre><code class=\"language-rust\">use nix::unistd::{ForkResult, fork};\n\nfn dup_std_fds() {\n let dev_null = std::fs::File::open("/dev/null").expect("Failed to open /dev/null");\n let dev_null_fd = dev_null.as_raw_fd();\n\n for &fd in &[libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {\n let _ = unsafe { libc::dup2(dev_null_fd, fd) };\n }\n}\n\nfn main() {\n\tmatch unsafe { fork() } {\n\t\tOk(ForkResult::Parent { child, .. }) => {\n\t\t\t// 进程A,没有去 wait 子进程 B\n\t\t}\n\t\tOk(ForkResult::Child) => {\n\t\t\t// A fork 出的进程 B,让它成为孤儿进程\n\t\t}\n\t}\n}\n</code></pre>\n",
"url": "https://forsworns.github.io///zh/blogs/20240620/",
"title": "Rust libc::fork 与 std::process::Command 混用出的僵尸进程问题",
"summary": "忽略了子进程继承父进程的 stdio fd 导致的问题",
"date_modified": "2024-06-20T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>读一下 arxiv 上刚刚上传的论文 <a href=\"https://arxiv.org/pdf/2405.04437\">vAttention: Dynamic Memory Management for Serving LLMs without PagedAttention</a>。</p>\n<p>在 vllm 项目中也有相关 <a href=\"https://github.com/vllm-project/vllm/issues/4675\">issue</a>,论文作者出现在评论区声称近期会将其实现开源。</p>\n<p>暂时搬运下公众号文章</p>\n<p><a href=\"https://mp.weixin.qq.com/s/E9Df1prkEbw23hPF59IrwQ\">https://mp.weixin.qq.com/s/E9Df1prkEbw23hPF59IrwQ</a></p>\n<p>收益:</p>\n<p>token qps是vLLM的1.97x倍;</p>\n<p>prompt的prefill阶段速度是FlashAttention的3.97倍,FlashInfer的1.45倍;</p>\n<p>使用者不需要修改之前的attention代码。</p>\n<p>论文解读<br>\nLLM推理分为两个阶段:prefill和decoding。prefill阶段主要是一次性处理所有的用户输入;decoding阶段是每次输入一个token,输出一个token。由于每一个生成的token都需要利用输入prompt以及之前的token的K和V信息,为了避免重复计算,可以使用KV-Cache把之前的token对应的K和V记录下来。</p>\n<p>KV-Cache可以显著减少K和V的计算量,但是需要显存来存储对应的值。每一个Decoder Block都需要这么多byte来存储K以及V,其中B代表batch size,L代表sequence length,H代表number of head,D代表size of head,P代表kv的数据格式需要多少比特才能存储,比如fp16就需要2 byte。如果N代表Block数量,那么一个模型总共需要的kv cache的存储空间为。</p>\n<p>Orca以及FasterTransformer这些系统里面,L一般都设置为max seq length,也就是允许的最大生成长度。很显然,根据上面的计算公式,这是一个很大的值,也成为了限制可以调度的batch size最大的影响因素。但是,LLM里面有一个很显著的特点就是每个输入的数据生成的token长度不统一,有可能第一个输入生成5个token就生成完了,另外一个输入生成5000个token都还没有生成完。我们都按照最大长度来分配kv cache的话,很多分配的显存都没有被使用,而这些没有被使用却被分配的显存还减小了batch size的大小,从而降低了系统的吞吐。</p>\n<p>vLLM受到操作系统管理虚拟内存的启发,提出了PagedAttention。PagedAttention把KV-cache分割成固定大小的block,每次需要的时候动态分配一个block。通过这样的方式,vLLM可以只在一个请求需要的的时候为其分配需要的显存。</p>\n<p>vLLM内存管理模式</p>\n<p><img src=\"fig1.png\" alt=\"\"></p>\n<p>vLLM节省内存示意图:</p>\n<p><img src=\"fig2.png\" alt=\"\"></p>\n<p>但是天下没有免费的午餐:</p>\n<p>传统的attention算子的实现都是假设K和V都是在显存中连续存储,而vLLM把KV-cache的存储从连续的虚拟内存变成了非连续的。这就使得传统的实现无法继续使用,必须重写attention kernel;<br>\n每次计算的时候需要取所有block里面的k和v,这就使得服务框架必须追踪每个block和显存地址的映射关系。而这就使得服务框架更加复杂。服务框架干的这个事情恰好就是操作系统干的virtual-to-physical address转换的事情,其实有重复;<br>\nvLLM承认他们的基于PagedAttention的实现比原始的FasterTransformer kernel实现要慢20 − 26%,这部分主要是查询Block-Tables以及执行额外的分支造成的;<br>\n实现额外的内存管理需要额外的CPU时间。最开始的vLLM的BlockTable准备占一个解码迭代步骤30%的时间。即便最新有一些修复,它依然在TensorRT-LLM里面占据了10%,让吞吐下降了11%,从412 tokens/sec降到了365 tokens/sec。<br>\nCUDA 10.2开始引入了virtual memory management API,这一组细粒度的API可以让虚拟内存和物理内存分配分开并进行自由组合。</p>\n<p>使用这一组API,我们可以像传统的推理框架一样一开始就为KV-Cache分配连续的虚拟内存,这样所有之前写的attention kernel都可以重用。不同的是,这些虚拟内存并没有对应的物理内存。物理内存的分配是像vLLM里面一样,需要的时候才分配。这就是作者提出的vAttention的核心所在。</p>\n<p><img src=\"fig3.png\" alt=\"\"></p>\n<p>cuMemCreate默认最小2MB的物理内存。这对于KV-Cache来说依然单次分配得太多。幸好这部分driver的实现NV是开源的,于是作者修改了底层driver实现,让其可以支持4KB, 64KB, 2MB的最小内存分配单元。</p>\n<p>另外,显存的分配还是有明显的时延的,在每次需要物理显存分配得时候会出现卡顿。为了解决这个问题,系统跟踪显存的使用,在预测到下一轮即将需要新分配显存的时候就启动后台线程进行分配,这样在真的需要分配的时候后台线程已经分配好。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240526/",
"title": "vAttention 论文笔记",
"summary": "Dynamic Memory Management for Serving LLMs without PagedAttention",
"date_modified": "2024-05-26T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>论文 "PyTorch2: Faster Machine Learning Through Dynamic<br>\nPython Bytecode Transformation and Graph<br>\nCompilation" 介绍了 PyTorch2 中引入的新 API,<a href=\"https://pytorch.org/docs/stable/torch.compiler.html#torch-compiler\">torch.compile</a><br>\n背后的实现原理。它旨在解决中在 Pytorch 中构建计算图的问题,并最终通过编译技术加速代码执行。</p>\n<p>torch.compile 基于以下底层技术:</p>\n<ul>\n<li>\n<p>TorchDynamo(<code>torch._dynamo</code>):内部API,基于 CPython 的特性,<a href=\"https://peps.python.org/pep-0523/\">PEP523 Frame Evaluation API</a>,以安全地捕获 PyTorch 计算图。</p>\n</li>\n<li>\n<p>TorchInductor:默认的深度学习编译器,可以为多种加速器和后端生成快速的代码。要通过torch.compile实现加速,需要使用后端编译器。对于NVIDIA和AMD GPU,它利用OpenAI Triton作为关键构建块。</p>\n</li>\n<li>\n<p>AOT (Ahead-Of-Time)Autograd:不仅捕获用户级代码,还捕获反向传播。这使得使用 TorchInductor 能够加速前向传播和反向传播。</p>\n</li>\n</ul>\n<p>借助 torch.compile,Pytorch2 的算子数量也显著减少了,参见 <a href=\"https://pytorch.org/docs/stable/torch.compiler_ir.html\">https://pytorch.org/docs/stable/torch.compiler_ir.html</a></p>\n<p><code>torch.compile</code> 的使用很简单,可以通过装饰器加在函数上</p>\n<pre><code class=\"language-python\">@torch.compile\ndef opt_foo2(x, y):\n a = torch.sin(x)\n b = torch.cos(y)\n return a + b\nprint(opt_foo2(torch.randn(10, 10), torch.randn(10, 10)))\n</code></pre>\n<p>可以直接将 <code>torch.nn.Module</code> 作为参数</p>\n<pre><code class=\"language-python\">class MyModule(torch.nn.Module):\n def __init__(self):\n super().__init__()\n self.lin = torch.nn.Linear(100, 10)\n\n def forward(self, x):\n return torch.nn.functional.relu(self.lin(x))\n\nmod = MyModule()\nopt_mod = torch.compile(mod)\nprint(opt_mod(torch.randn(10, 100)))\n</code></pre>\n<p>或者直接写 <a href=\"https://pytorch.org/tutorials/recipes/torch_compile_user_defined_triton_kernel_tutorial.html\">triton</a> 算子</p>\n<pre><code class=\"language-python\">import triton\nfrom triton import language as tl\n\n@triton.autotune(\n configs=[\n triton.Config({"BLOCK_SIZE": 4}, num_stages=3, num_warps=8),\n triton.Config({"BLOCK_SIZE": 4}, num_stages=4, num_warps=4),\n triton.Config({"BLOCK_SIZE": 2}, num_stages=3, num_warps=8),\n triton.Config({"BLOCK_SIZE": 2}, num_stages=4, num_warps=4),\n ],\n key=[],\n)\n@triton.jit\ndef add_kernel_autotuned(\n in_ptr0,\n in_ptr1,\n out_ptr,\n n_elements,\n BLOCK_SIZE: "tl.constexpr",\n):\n pid = tl.program_id(axis=0)\n block_start = pid * BLOCK_SIZE\n offsets = block_start + tl.arange(0, BLOCK_SIZE)\n mask = offsets < n_elements\n x = tl.load(in_ptr0 + offsets, mask=mask)\n y = tl.load(in_ptr1 + offsets, mask=mask)\n output = x + y\n tl.store(out_ptr + offsets, output, mask=mask)\n\n@torch.compile(fullgraph=True)\ndef add_fn(x, y):\n output = torch.zeros_like(x)\n n_elements = output.numel()\n grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),)\n add_kernel_autotuned[grid](x, y, output, n_elements)\n return output\n\nx = torch.randn(4, device="cuda")\ny = torch.randn(4, device="cuda")\nout = add_fn(x, y)\nprint(f"Vector addition of\\nX:\\t{x}\\nY:\\t{y}\\nis equal to\\n{out}")\n</code></pre>\n<h1>论文笔记</h1>\n<h2>1 摘要/导言</h2>\n<p>作为 eager 模式的框架,pytorch 易于学习和调试,但是难以借助编译器实现图级别的优化。框架只能看到局部的算子信息,无法做算子融合和调度。已有的一些尝试,基于记录/重放、python 解析、懒执行等方法,会影响 pytorch 本身的易用性。记录/重放可能导致错误结果,python 解析无法处理复杂的 python 程序,懒执行的运行时开销太大,因此都不实际。</p>\n<p>本文介绍了两个 PyTorch 的扩展,TorchDynamo 和 TorchInductor,它们实现了在 PyTorch2 中发布的 <code>torch.compile</code> 功能。TorchDynamo 是一个基于Python的即时编译器(JIT),它在不损失 Python 灵活性的情况下,使得 PyTorch 程序能够进行图编译。它挂载在 python 的 frame evaluation API 之上,通过在执行之前动态修改 Python 字节码,并将一系列 PyTorch 操作提取到一个 FX 图中来实现这一目标,然后使用可扩展的后端对其进行 JIT 编译。TorchInductor 是 TorchDynamo 的默认编译器后端,它将PyTorch 程序转换为 OpenAI 的 Triton(用于GPU)和 C++(用于CPU)。这些扩展为在 PyTorch 等 eager-mode 框架中通过编译器应用优化提供了一种新途径。</p>\n<h2>2 过去的 PyTorch 图捕获方法</h2>\n<p>eager 模式的框架,难点不仅在于执行时仅有局部信息,还在于它的代码中可以混淆任意 python 代码、第三方库,无法像基于图的框架,限制用户的行为。下面介绍下在实现 TorchDynamo 之前,pytorch 社区的尝试。</p>\n<h3>2.1 torch.jit.trace</h3>\n<p><code>torch.jit.trace</code> 使用记录/重放的方法来构建 TorchScript 图。在 Pytorch dispatcher 的层面进行记录,dispatcher 是用来把算子转换成特定设备的核函数,以及用于自动微分的,用 C++ 实现。因为记录是实现在 C++ 层面,因此它无法捕获到 python 中的控制流。比如下面这个例子</p>\n<pre><code class=\"language-python\">def example1(x):\n if len(torch.nonzero(x)) > 1:\n return x + 1\n return x - 1\n</code></pre>\n<p>假设输入是 <code>tensor([0,0])</code>,它会捕获到一个等价于下面代码的图</p>\n<pre><code class=\"language-python\">def example1_incorrect_capture(x):\n torch.nonzero(x)\n return x - 1\n</code></pre>\n<p>显然输入换成 <code>tensor([1,1])</code>,捕获到的图就非法了。同时,python 代码中任意非 pytorch 的部分,也是无法捕获的,如三方库、日志打印、程序执行的副作用。</p>\n<h3>2.2 torch.jit.script</h3>\n<p><code>torch.jit.script</code> 也用来构建 TorchScript 图,但是是通过解析 python 语法树进行静态分析,它能够正确捕获上面的 example1。但是问题是它在尝试将 python 整个实现成一个静态语言,遇到未实现的 python 组件将会导致它无法工作。它只支持部分模型,而且支持大些模型的工作量很大。</p>\n<h3>2.3 Lazy Tensor</h3>\n<p>Pytorch/XLA 使用这种方法,它是一个 C++ 层面的方法,每轮迭代它都会延后算子执行,累积成一个图,然后将图喂给 XLA 编译器。它会对图做哈希以避免重复编译。它有效而且通用性强,但是运行时开销大(额外维护图结构)、延迟高(不必要的 device/host 串行)、有时可能频繁触发重新编译。</p>\n<p>目前 Pytorch/XLA 已经集成了 TorchDynamo,它不会在每轮迭代都采用 Lazy Tensor,同时借助 TorchDynamo 来判断是否需要重新捕获图。</p>\n<h3>2.4 torch.fx.symbolic_trace</h3>\n<p><code>torch.fx.symbolic_trace</code> 也是一个基于记录/重放的图捕获技术,但是它工作在 python 层面,因此可以捕获到 python 中的条件判断。它借助一个代理对象来执行用户的代码,因此可以捕获到关于规模/值的读取行为,而不是像 <code>torch.jit.trace</code> 一样直接代入真实的张量值。它的图表示,<code>FX graph</code> 格式,也被 TorchDynamo 所采纳。</p>\n<p>但是它无法处理下面的例子,</p>\n<pre><code class=\"language-python\">def example3(x):\n global call_count\n call_count += 1\n return torch.rand(10) + x\n</code></pre>\n<p>它会生成类似下面代码的图,随机数和全局副作用都丢失了,因为它们不会和 <code>x</code> 的代理变量交互。而且即使全局副作用被捕获了,下游的编译器也大多不支持它,因为机器学习的图结构中几乎都没有 python 中的全局变量的概念。</p>\n<pre><code class=\"language-python\">def example3_incorrect_capture(x):\n return _tensor_constant0 + x\n</code></pre>\n<h3>2.6 和 JAX 中的图捕获的对比</h3>\n<p>JAX 天然是和 XLA 联合设计的,而且对用户程序也有限制,<code>jax.jit</code> 不支持依赖数据的 python 控制流,并且要求用户函数是纯函数(无副作用),因此它可以采用一种类似 <code>torch.fx.symbolic_trace</code> 的图捕获技术,并且还更简单。</p>\n<p>与之相对,torch 一开始设计的时候没有编译器的概念,是一个仅支持 eager 模式的框架,已经有大量的模型基于它构建,换条路不现实了。</p>\n<h2>3 TorchDynamo 设计实现</h2>\n<p>TorchDynamo 是一个工作在 CPython 层面的 python 字节码 JIT 编译器。它将 python 字节码翻译到 python 字节码,只是会将原始的字节码中的 pytorch 运算替换成编译后的产物,从而实现 pytorch 的算子融合。下图是它的原理。</p>\n<p><img src=\"fig1.png\" alt=\"\"></p>\n<h3>3.1 API</h3>\n<p>当使用 <code>torch.compile</code> 运行 pytorch <code>Module</code> 时,自定义的 CPython Frame Evaluation Hook (参见 3.2)将重写正在执行的每个Python 函数的字节码,以提取和编译 PyTorch 算子。这个字节码重写可以被缓存,因为它可能依赖于程序的某些动态属性,需要使用 guards(参见 3.3)来在后续调用中进行检查。</p>\n<h3>3.2 CPython Frame Evaluation Hook</h3>\n<h3>3.3 Guards</h3>\n<h3>3.4 Symbolic Evaluation</h3>\n<h3>3.5 Modeling Python Data Structures</h3>\n<h3>3.6 Inlining, Control Flow, and Closures</h3>\n<h3>3.7 Mutation and Side Effects</h3>\n<h2>4 TorchInductor 设计实现</h2>\n<h3>4.5 Triton 代码生成</h3>\n<p>Triton codegen 负责将 TorchInductor 的 IR 映射到 Triton 核函数。下图显示了上述 log2 示例生成的代码。</p>\n<p><img src=\"fig3.png\" alt=\"\"></p>\n<p>该核函数一次处理 <code>XBLOCK</code> 个元素的块。如果元素的数量不是<code>XBLOCK</code> 的倍数,则末尾可能会有一些元素被屏蔽。在代码生成过程中,我们简化了索引。例如,在 IR 中,2D strided 加载被转换为连续加载。代码生成还负责公共子表达式消除(CSE),通过生成代码时使用缓存,并分配以 tmp 开头的临时变量名。pointwise修饰符用于简化启发式块大小、自动调优和预先编译核函数的样板代码。修饰符是正在生成的核函数类型(pointwise、reduction或template),其参数是核函数的必需元数据,例如数据对齐方式。</p>\n<p>在生成 reduction 核函数时,TorchInductor有两种代码生成模式。对于较小的 reduction 操作,它将生成持久 reduction,整个reduction加载到单个块中并在寄存器/共享内存中保留;在这种情况下,reduction 直接映射到 Triton 的 reduction 操作符。对于较大的 reduction,TorchInductor 生成一个循环,将整个块用作累加器,并在循环结束时调用 Triton reduction。</p>\n<p>对于更复杂的操作(矩阵乘法和卷积),TorchInductor 有自己的模板系统,用于生成混合手写 Triton 和生成的 Triton 代码。模板使用 Jinja 编写,与 TorchInductor 的代码生成系统进行交互。</p>\n<h3>4.6 C++ 代码生成</h3>\n<p>CPU 后端会对应到 C++ 代码和 OpenMP。C++ 代码会有向量化和非向量化的实现。向量化的实现会讲大多数运算映射到 pytorch 的<code>at::vec::Vectorized</code> 类。这个类一次会处理 16 个元素,也是标准的 pytorch 核函数调用 SIMD 指令集的方式。非向量化的实现就是转换到 STL 标准库。两个实现都会尝试展开部分循环以实现并行。</p>\n<h3>4.7 Wrapper Codegen</h3>\n<p>Wrapper Codegen 指的是生成调用核函数的代码,它负责计算张量规模、管理显存分配。有两个实现,一个生成 python 代码,另一个生成 C++ 代码,python 后延更加灵活,支持一些边界情况,C++ 代码开销更小。</p>\n<p>当 <code>torch.compile</code> 指定 <code>mode="reduce-overhead"</code> 时,TrochInductor 会尝试使用 CUDA Graph 来消除 wrapper 代码的开销,基于 API <code>cuStreamBeginCapture</code> 和 <code>cuStreamEndCapture</code>,属于 CUDA 自动图捕获。如果是张量是动态规模,或者不是 CUDA 张量的话,就放弃借助 CUDA Graph。</p>\n<p>注意这里是 CUDA API 层面的图,和上面讲的算子层面的图不同。</p>\n<h3>4.8 相关的深度学习编译器</h3>\n<p>pytorch 选用了 OpenAI 的 Triton,因为它可以生成比手写库还快的核函数,大多编译器甚至没有考虑过这方面的工作,面对复杂算子会直接调用手写库。</p>\n<h1>CUDA Graph 相关缺陷</h1>\n<p><a href=\"https://pytorch.org/docs/stable/torch.compiler_cudagraph_trees.html\">https://pytorch.org/docs/stable/torch.compiler_cudagraph_trees.html</a></p>\n<p>由于 <a href=\"https://developer.nvidia.com/blog/cuda-graphs/\">CUDA Graph</a> 使用了确定性的显存地址,所以前面的执行结果会被后续的执行结果覆盖。也就是说下面的用例会出错</p>\n<pre><code class=\"language-python\">import torch\n\n@torch.compile(mode="reduce-overhead")\ndef my_model(x):\n y = torch.matmul(x, x)\n return y\n\nx = torch.randn(10, 10)\ny1 = my_model(x)\ny2 = my_model(x)\nprint(y1)\n# RuntimeError: Error: accessing tensor output of CUDAGraphs that has been overwritten by a subsequent run.\n</code></pre>\n<p>Pytorch 在实现 <code>torch.compile</code> 的时候,采用了一种启发式的方法来避免这个问题。在推理过程中会在每次调用 <code>torch.compile</code> 时开始新的迭代;在训练过程中也是如此,只要没有未调用的待处理反向传播。如果这些启发式算法不正确,可以使用 <code>torch.compiler.mark_step_begin()</code> 标记开始新的迭代,或在开始下一次运行之前(在 <code>torch.compile</code> 之外)克隆上一次迭代的张量。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240519/",
"title": "PyTorch2 论文笔记",
"summary": "Pytorch2 Recap",
"date_modified": "2024-05-19T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h2>Pytorch 张量并行 Tutorial</h2>\n<p><a href=\"https://pytorch.org/tutorials/intermediate/TP_tutorial.html\">https://pytorch.org/tutorials/intermediate/TP_tutorial.html</a></p>\n<p>该文档介绍的是如何应用 FSDP (Fully Sharded Data Parallel) 和 TP (Tensor Parallel) 训练 Transformer 类模型。</p>\n<h3>TP 是怎么起效的</h3>\n<p><a href=\"https://arxiv.org/abs/1909.08053\">Tensor Parallel(TP)</a> 是一个高效的模型并行方法,本文提到的 <a href=\"https://arxiv.org/abs/2205.05198\">Sequence Parallel (SP)</a> 是一种特殊的 TP,它在 <code>nn.LayerNorm</code> 或 <code>RMSNorm</code> 的 sequence 维度上做分片,节省训练期间激活部分的显存占用。当模型变大,这部分占用会很高,所以一般 TP 都是以 SP 的形式实施。</p>\n<p>下图展示了 Transformer 模型的 MLP 层和 Self-Attention 层是怎么采用 TP 的方式进行分片的。原始的矩阵乘法都可以被分片处理。</p>\n<p><img src=\"shard.png\" alt=\"\"></p>\n<p>Pytorch TP 的工作流程大致如下:</p>\n<h4>分片初始化:</h4>\n<ul>\n<li>决定模型各层的并行策略 <code>ParallelStyle</code>,调用 <code>parallelize_module</code>。</li>\n<li>并行化的模型参数被转换为 DTensor,DTensor 负责使用分片的计算方法允许并行化的模型。</li>\n</ul>\n<h4>运行时的前向反向更新</h4>\n<ul>\n<li>根据用户给 <code>ParallelStyle</code> 指定的输入输出 DTensor 的规模,使用集合通信方法进行转换。</li>\n<li>在并行蹭上进行分片后的计算。</li>\n</ul>\n<h3>什么时候应该使用 TP</h3>\n<p>Pytorch 的 FSDP 已经可以实现多卡上分片计算,但是卡数增多后,FSDP 就会有别的问题:</p>\n<ol>\n<li>GPU 数量过大时(128/256),FSDP 以来的 <code>allgather</code> 等集合通信会被通信耗时影响。通过在 FSDP 之上部署 TP/SP,FSDP 的尺度可以被大幅削减,我们可以在节点内部署 TP/SP,节点间使用 FSDP。</li>\n<li>由于收敛性和 GPU 显存限制,全局的 batch size 达到了数据并行的上限,无法继续提升了。TP/SP 成为了唯一的方法去继续拓展到更多的 GPU,这也意味着 GPU 数量和模型规模可以继续扩张。</li>\n<li>特定的模型,当本地的 batch size 变小以后,TP/SP 可以返回对 FLOPS 调优的矩阵乘法。</li>\n</ol>\n<p>当预训练的时候,很容易达到上限。预训练一个数亿大模型的模型会花很多月,即使使用几千个 GPU。</p>\n<ul>\n<li>问题 1 很容易碰到,例如 Llama 2 70B 使用 2000 张 GPU 训练了 35 天,多维并行的规模为 2000。</li>\n<li>当 transformer 模型变大,很快就会碰到问题 2。例如在 Llama 2 的全局 batch size 为 1000 时,即使设置 local batch size 为 1,也不能仅实施 FSDP,因为有 2000 张卡。</li>\n</ul>\n<h3>如何实施 TP</h3>\n<p>Pytorch TP API 提供的是对模型各层进行分片策略的接口。</p>\n<ul>\n<li>\n<p><code>ColwiseParallel</code> 和 <code>RowwiseParallel</code>:以行/列的方式分片 <code>nn.Linear</code> 和 <code>nn.Embedding</code></p>\n</li>\n<li>\n<p><code>SequenceParallel</code>: 对 <code>nn.LayerNorm</code>, <code>nn.Dropout</code>, <code>RMSNormPython</code> 等进行分片</p>\n</li>\n<li>\n<p><code>PrepareModuleInput</code> 和 <code>PrepareModuleOutput</code>: 设定模块的输入输出分片设置,以便采用合适的集合通信操作</p>\n</li>\n</ul>\n<p>以 <a href=\"https://github.com/pytorch/examples/blob/main/distributed/tensor_parallelism/llama2_model.py\">Llama2</a> 为例,首先借助 <code>DeviceMesh</code> 简洁地初始化 NCCL,TP 一般在节点内使用,如下面这个八卡的用例:</p>\n<pre><code class=\"language-python\"># run this via torchrun: torchrun --standalone --nproc_per_node=8 ./tp_tutorial.py\n\nfrom torch.distributed.device_mesh import init_device_mesh\n\ntp_mesh = init_device_mesh("cuda", (8,))\n</code></pre>\n<p><a href=\"https://github.com/pytorch/examples/blob/main/distributed/tensor_parallelism/llama2_model.py\">Llama2</a> 的实现中,核心的 <code>TransformerBlock </code> 由 <code>Attention</code> 和 <code>FeedForward</code> 层组成,<code>FeedForward</code> 如下</p>\n<pre><code class=\"language-python\"># forward in the FeedForward layer\ndef forward(self, x):\n return self.w2(F.silu(self.w1(x)) * self.w3(x))\n</code></pre>\n<p>显然可以对 <code>w1/w3</code> 按列进行分片,<code>w2</code> 按行进行分片,代码如下,不需要关心底层的集合通信,会被自动执行</p>\n<pre><code class=\"language-python\">from torch.distributed.tensor.parallel import ColwiseParallel, RowwiseParallel, parallelize_module\n\nlayer_tp_plan = {\n # by default ColwiseParallel input layouts is replicated\n # and RowwiseParallel output layouts is replicated\n "feed_foward.w1": ColwiseParallel(),\n "feed_forward.w2": RowwiseParallel(),\n "feed_forward.w3": ColwiseParallel(),\n}\n</code></pre>\n<p>类似地,对 <code>Attention</code> 模块,我们可以按列分片 <code>q/k/v</code> 的投影过程,然后按行对 <code>wo</code> 的线性投影进行分片。</p>\n<pre><code class=\"language-python\">layer_tp_plan = {\n # by default ColwiseParallel input layouts is replicated\n # and RowwiseParallel output layouts is replicated\n "attention.wq": ColwiseParallel(),\n "attention.wk": ColwiseParallel(),\n "attention.wv": ColwiseParallel(),\n "attention.wo": RowwiseParallel(),\n "feed_forward.w1": ColwiseParallel(),\n "feed_forward.w2": RowwiseParallel(),\n "feed_forward.w3": ColwiseParallel(),\n}\n</code></pre>\n<p>基本上模型的分片就完成了,值得注意的是人格对线形层进行按列分片,输出就会被按张量的最后一个维度去分片,而按行分片的线形层则接收在最后一个维度上进行分片的输入。如果在按列/按行分片的线性层间还有别的操作(如 <code>view()</code> 操作),需要去调整张量的形状。</p>\n<p>Llama2 的 attention 层中就有 <code>view</code> 操作。<code>wq/ wk/ wv</code> 的线性层是按列分片的,激活张量是在 <code>num_heads</code> 维度上进行分片,所以需要调整 <code>num_heads</code> 为局部的 <code>num_heads</code>。</p>\n<p>最后调用 <code>parallelize_module</code> 将模型转换到 DTensor 上,集合通信就会被自动注册到各个模块的输入和输出上。</p>\n<pre><code class=\"language-python\">for layer_id, transformer_block in enumerate(model.layers):\n layer_tp_plan = {...} # i.e. the plan we just generated\n\n # Adjust attention module to use the local number of heads\n attn_layer = transformer_block.attention\n attn_layer.n_heads = attn_layer.n_heads // tp_mesh.size()\n attn_layer.n_kv_heads = attn_layer.n_kv_heads // tp_mesh.size()\n\n parallelize_module(\n module=transformer_block,\n device_mesh=tp_mesh,\n parallelize_plan=layer_tp_plan,\n )\n</code></pre>\n<p>现在我们已经详细阐述了每个 <code>TransformerBlock</code> 的分片计划,通常在第一层中有一个 <code>nn.Embedding</code>,最后一层有一个 <code>nn.Linear</code>投影层,用户可以选择对第一个 <code>nn.Embedding</code> 进行逐行或逐列分片,并对最后一个 <code>nn.Linear</code> 投影层进行逐列分片,同时指定适当的输入和输出布局。</p>\n<pre><code class=\"language-python\">model = parallelize_module(\n model,\n tp_mesh,\n {\n "tok_embeddings": RowwiseParallel(\n input_layouts=Replicate(),\n ),\n "output": ColwiseParallel(\n output_layouts=Replicate(),\n ),\n }\n)\n</code></pre>\n<h3>SP 例子</h3>\n<p>SP 是在上面所示的 TP 的基础上进行的。基本的 TP 只在注意力模块和前馈模块中分片张量,会复制它们的模块输入和输出(即前向传递中的激活和反向传递中的梯度)。序列并行则在序列维度上进行分片。</p>\n<p>在典型的 <code>TransformerBlock</code> 中,<code>forward()</code> 函数结合了用于正则化的层(<code>LayerNorm</code> 或 <code>RMSNorm</code>)、注意力层、前馈层和残差连接。例如:</p>\n<pre><code class=\"language-python\"># forward in a TransformerBlock\ndef forward(self, x):\n h = x + self.attention(self.attention_norm(x))\n out = h + self.feed_forward(self.ffn_norm(h))\n return out\n</code></pre>\n<p>在大多数情况下,注意力和前馈模块之外的激活(和梯度)的形状为 [批次大小,序列长度,隐藏维度]。在 DTensor 的术语中,SP 使用 Shard(1) 布局来执行模块的前向传递和反向传递的激活计算。以下代码示例演示如何将序列并行应用于<code>TransformerBlock</code> 中的正则化层:</p>\n<pre><code class=\"language-python\">from torch.distributed.tensor.parallel import (\n PrepareModuleInput,\n SequenceParallel,\n)\n\nlayer_tp_plan = {\n # Now the input and output of SequenceParallel has Shard(1) layouts,\n # to represent the input/output tensors sharded on the sequence dimension\n "attention_norm": SequenceParallel(),\n "attention": PrepareModuleInput(\n input_layouts=(Shard(1),),\n desired_input_layouts=(Replicate(),),\n ),\n "attention.wq": ColwiseParallel(),\n "attention.wk": ColwiseParallel(),\n "attention.wv": ColwiseParallel(),\n "attention.wo": RowwiseParallel(output_layouts=Shard(1)),\n "ffn_norm": SequenceParallel(),\n "feed_forward": PrepareModuleInput(\n input_layouts=(Shard(1),),\n desired_input_layouts=(Replicate(),),\n ),\n "feed_forward.w1": ColwiseParallel(),\n "feed_forward.w2": RowwiseParallel(output_layouts=Shard(1)),\n "feed_forward.w3": ColwiseParallel(),\n}\n</code></pre>\n<p>可以看到,我们现在使用 <code>PrepareModuleInput</code> 将注意力和前馈层的模块输入布局从 <code>Shard(1)</code> 修改为 <code>Replicate()</code>,并将它们的输出布局标记为 <code>Shard(1)</code>。就像 TP 中发生的那样,我们只需要指定输入和输出的张量分片布局,层之间的通信将自动发生。</p>\n<p>需要注意的是,在 SP 中,我们假设 <code>TransformerBlock</code> 的输入和输出始终在序列维度上进行分片,以便多个 <code>TransformerBlock</code> 可以无缝连接。这可以通过显式指定起始的 <code>nn.Embedding</code> 层的输出和最终 <code>nn.Linear</code> 投影层的输入为 <code>Shard(1)</code> 来实现:</p>\n<pre><code class=\"language-python\">model = parallelize_module(\n model,\n tp_mesh,\n {\n "tok_embeddings": RowwiseParallel(\n input_layouts=Replicate(),\n output_layouts=Shard(1),\n ),\n "norm": SequenceParallel(),\n "output": ColwiseParallel(\n input_layouts=Shard(1),\n output_layouts=Replicate()\n ),\n }\n)\n</code></pre>\n<h3>LP</h3>\n<p>损失并行 LP(Loss Parallel)用于在计算损失函数时节省内存和通信,因为模型输出通常非常大。在 LP 中,当模型输出在巨大的词汇维度上进行分片时(比如 GPT4 使用的 cl100k_base,具有 10万种 token),可以高效地计算交叉熵损失,而无需将所有模型输出聚集到每个 GPU 上。这不仅显著减少了内存消耗,还通过减少通信开销和并行进行分片计算来提高训练速度。下图简要说明了 LP 如何通过进行分片计算来避免将所有模型输出聚集到每个 GPU 上。</p>\n<p>下图在使用 LP 进行交叉熵损失前向计算。蓝色代表分片张量,绿色代表复制张量,黄色代表具有部分值的张量(将进行全局的 reduction 集合通信)。黑色箭头表示本地计算,红色箭头表示 GPU 之间的集合通信行为。图里分两步是因为在针对类别计算损失的时候,<code>CrossEntropyLoss</code> 可以拆解成 <code>LogSoftmax</code> 和 <code>NLLLoss</code>。</p>\n<p><img src=\"loss.png\" alt=\"\"></p>\n<p>在 PyTorch 的 TP API中,可以通过上下文管理器 <code>loss_parallel</code> 启用 LP。使用 <code>loss_parallel</code>,可以直接使用 <code>torch.nn.functional.cross_entropy</code> 或 <code>torch.nn.CrossEntropyLoss</code>,而无需修改代码中的其他部分。</p>\n<p>要应用 LP,模型的预测结果通常是形状为[批次大小,序列长度,词汇大小]的张量,应在词汇大小的维度上进行分片。可以通过标记最后一个线性投影层输出的输出布局来轻松实现这一点。</p>\n<pre><code class=\"language-python\">model = parallelize_module(\n model,\n tp_mesh,\n {\n "tok_embeddings": RowwiseParallel(\n input_layouts=Replicate(),\n output_layouts=Shard(1),\n ),\n "norm": SequenceParallel(),\n "output": ColwiseParallel(\n input_layouts=Shard(1),\n # use DTensor as the output\n use_local_output=False,\n ),\n },\n)\n</code></pre>\n<p>在上述代码中,我们还在输出之前对规范化层应用了 SP。我们使用<code>use_local_output=False</code>,使输出保持为一个 DTensor,以便在<code>loss_parallel</code> 的上下文种使用。之后,可以直接调用交叉熵损失函数,如下所示。请注意,反向计算也需要在该上下文中进行。</p>\n<pre><code class=\"language-python\">import torch.nn.functional as F\nfrom torch.distributed.tensor.parallel import loss_parallel\n\npred = model(input_ids)\nwith loss_parallel():\n # assuming pred and labels are of the shape [batch, seq, vocab]\n loss = F.cross_entropy(pred.flatten(0, 1), labels.flatten(0, 1))\n loss.backward()\n</code></pre>\n<h3>TP 和 FSDP 结合</h3>\n<p>现在我们已经展示了如何将 TP/SP 应用于模型,让我们也看一下如何将 TP 和 FSDP 结合起来使用。由于 TP 会阻塞计算的通信,我们希望确保它在本地的 NVLink 间传输。在实践中,我们通常在 host 内应用 TP,并在 host 间应用FSDP。</p>\n<p><img src=\"tp_fsdp.png\" alt=\"\"></p>\n<p>图3. FSDP 和 TP在不同的设备维度上工作,FSDP 通信在 host 间进行,TP 通信在 host 内进行。</p>\n<p>通过 2D DeviceMesh 可以轻松表示这种 2D 并行模式,我们只需要将每个“子” DeviceMesh 传递给各个并行API:</p>\n<p>这样我们就可以轻松地在每个 host 内应用 TP 并在 host 间应用 FSDP,而不需要对 Llama 模型进行任何代码更改。并可以继续增加模型规模,使用大量GPU进行高效训练。</p>\n<h2>张量并行 API</h2>\n<p><code>torch.distributed.tensor.parallel.parallelize_module(module, device_mesh, parallelize_plan)</code> 接口用于对任意 <code>nn.Module</code> 实行张量并行。它将返回一个并行化的后的 <code>nn.Module</code>。用法如下,指定模型、Device Mesh 和具体参数的并行策略。</p>\n<pre><code class=\"language-python\">from torch.distributed.tensor.parallel import parallelize_module, ColwiseParallel\nfrom torch.distributed.device_mesh import init_device_mesh\n# Define the module.\nm = Model(...)\ntp_mesh = init_device_mesh("cuda", (8,))\nm = parallelize_module(m, tp_mesh, {"w1": ColwiseParallel(), "w2": RowwiseParallel()})\n</code></pre>\n<p>详细内容阅读<a href=\"https://pytorch.org/docs/stable/distributed.tensor.parallel.html\">文档</a>。</p>\n<h2>Device Mesh</h2>\n<p>关于 DeviceMesh,可以参考 <a href=\"https://pytorch.org/tutorials/recipes/distributed_device_mesh.html\">相关 文档</a>,它用于简化多维并行情况下,NCCL 集合通信分组的配置。</p>\n<p>在没有 Device Mesh 的时候,单机八卡想要分成两个节点,每个节点四张卡,做集合通信的话,初始化代码如下</p>\n<pre><code class=\"language-python\">import os\n\nimport torch\nimport torch.distributed as dist\n\n# Understand world topology\nrank = int(os.environ["RANK"])\nworld_size = int(os.environ["WORLD_SIZE"])\nprint(f"Running example on {rank=} in a world with {world_size=}")\n\n# Create process groups to manage 2-D like parallel pattern\ndist.init_process_group("nccl")\ntorch.cuda.set_device(rank)\n\n# Create shard groups (e.g. (0, 1, 2, 3), (4, 5, 6, 7))\n# and assign the correct shard group to each rank\nnum_node_devices = torch.cuda.device_count()\nshard_rank_lists = list(range(0, num_node_devices // 2)), list(range(num_node_devices // 2, num_node_devices))\nshard_groups = (\n dist.new_group(shard_rank_lists[0]),\n dist.new_group(shard_rank_lists[1]),\n)\ncurrent_shard_group = (\n shard_groups[0] if rank in shard_rank_lists[0] else shard_groups[1]\n)\n\n# Create replicate groups (for example, (0, 4), (1, 5), (2, 6), (3, 7))\n# and assign the correct replicate group to each rank\ncurrent_replicate_group = None\nshard_factor = len(shard_rank_lists[0])\nfor i in range(num_node_devices // 2):\n replicate_group_ranks = list(range(i, num_node_devices, shard_factor))\n replicate_group = dist.new_group(replicate_group_ranks)\n if rank in replicate_group_ranks:\n current_replicate_group = replicate_group\n</code></pre>\n<p>可以简化成</p>\n<pre><code class=\"language-python\">from torch.distributed.device_mesh import init_device_mesh\nmesh_2d = init_device_mesh("cuda", (2, 4), mesh_dim_names=("replicate", "shard"))\n\n# Users can access the underlying process group thru `get_group` API.\nreplicate_group = mesh_2d.get_group(mesh_dim="replicate")\nshard_group = mesh_2d.get_group(mesh_dim="shard")\n</code></pre>\n<h3>案例</h3>\n<p>Hybrid Sharding Data Parallel(HSDP) 是一个 2D 策略,在单个节点内实施 FSDP,节点间实施 DDP。</p>\n<p>下面这段代码和上面的分组拓扑一致,分成两个节点,每个节点四张卡;两个节点间 DDP,是要 replicate;节点内 FSDP,是做了 shard。</p>\n<pre><code class=\"language-python\">import torch\nimport torch.nn as nn\n\nfrom torch.distributed.device_mesh import init_device_mesh\nfrom torch.distributed.fsdp import FullyShardedDataParallel as FSDP, ShardingStrategy\n\n\nclass ToyModel(nn.Module):\n def __init__(self):\n super(ToyModel, self).__init__()\n self.net1 = nn.Linear(10, 10)\n self.relu = nn.ReLU()\n self.net2 = nn.Linear(10, 5)\n\n def forward(self, x):\n return self.net2(self.relu(self.net1(x)))\n\n\n# HSDP: MeshShape(2, 4)\nmesh_2d = init_device_mesh("cuda", (2, 4))\nmodel = FSDP(\n ToyModel(), device_mesh=mesh_2d, sharding_strategy=ShardingStrategy.HYBRID_SHARD\n)\n</code></pre>\n<h2>DTensor</h2>\n<p><a href=\"https://github.com/pytorch/pytorch/blob/main/torch/distributed/_tensor/README.md\">https://github.com/pytorch/pytorch/blob/main/torch/distributed/_tensor/README.md</a></p>\n<p>Pytorch 的 Tensor Parallel 依赖于底层的 DistributedTensor (DTCensor) 数据结构,实现 SPMD(Single Program Multiple Devices),支持 sharding 和 replication。</p>\n<p>需要注意的是当 DTensor 直接用于 Data Parallel 的时候,可能会比 pytorch 中实现的 DDP 和 FSDP 要慢,因为他们有全局的信息,而 DTensor 只是张量级别的。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240518/",
"title": "Pytorch2 Tensor Parallelism",
"summary": "Pytorch2 Recap",
"date_modified": "2024-05-18T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>Recently I met a problem with <code>cuMemFree</code>, and finally found it is in fact <a href=\"https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#implicit-synchronization\">implicit-synchronization</a>.<br>\nThis is not documented in the above CUDA tutorial and <a href=\"https://docs.nvidia.com/cuda/cuda-driver-api/group__CUDA__MEM.html#group__CUDA__MEM_1g89b3f154e17cc89b6eea277dbdf5c93a\">driver API</a>.<br>\nBut luckily, many discussions on <a href=\"https://stackoverflow.com/questions/12539593/is-cudafree-asynchronous\">StackOverflow</a> are related to it, which helps me a lot to identity the original problem.</p>\n<h2>Behaviour differences between CUDA and ROCm</h2>\n<p>One interesting thing is that <code>cuMemFree</code> only block on the context where the device pointer allocated.<br>\nIn contrast, since ROCm is open-source, I found that <a href=\"https://github.com/ROCm/clr/blob/933aa1d3a7bc4e4a2b4cfb2ad7e4c40df0b8ae61/hipamd/src/hip_memory.cpp#L69\">it blocks all of contexts on the device</a>.</p>\n<h2>PyTorch Allocator</h2>\n<p>The PyTorch experimental allocator implement this implicit-synchronization.<br>\nFollowing snippet is taken from PyTorch 2.1, and it implements the deallocation with CUDA virtual address API.</p>\n<pre><code class=\"language-cpp\">// c10/cuda/CUDACachingAllocator.cpp\nvoid unmapHandles(size_t begin, size_t end) {\n // note: unlike cudaFree, MemUnmap and MemRelease do\n // not appear to synchronize in all cases, so we have to wait for the\n // stream to finish before this memory is truly free.\n\n // cannot call c10::cuda::stream_synchronize because\n // it might grab the GIL which can lead to a deadlock\n // Locking order must be GIL -> Allocator Lock\n C10_CUDA_CHECK(cudaStreamSynchronize(stream_));\n for (auto i : c10::irange(begin, end)) {\n CUmemGenericAllocationHandle h = handles_.at(i).value();\n handles_.at(i) = c10::nullopt;\n C10_CUDA_DRIVER_CHECK(DriverAPI::get()->cuMemUnmap_(\n ptr_ + segment_size_ * i, segment_size_));\n C10_CUDA_DRIVER_CHECK(DriverAPI::get()->cuMemRelease_(h));\n }\n trimHandles();\n}\n</code></pre>\n<h2>OOM is recoverable</h2>\n<p>I'd like to share some another insight about PyTorch allocator, too. The <code>CUDA_ERROR_OUT_OF_MEMORY</code> is a recoverable error in PyTorch.</p>\n<p>Refer to the <a href=\"https://PyTorch.org/docs/stable/notes/cuda.html#memory-management\">PyTorch document</a> if you are not familiar to this.</p>\n<p><code>NativeCachingAllocator</code> initalize a single <code>DeviceCachingAllocator</code> on each device.<br>\n<code>NativeCachingAllocator::allocate()</code> is the public memory allocation API.<br>\nWhen the environment variable <code>PYTORCH_NO_CUDA_MEMORY_CACHING</code> is set to 1,<br>\n<code>forceUncachedAllocator</code> will call <code>cudaMalloc</code> directly,<br>\nor it will call <code>DeviceCachingAllocator::malloc</code> on the specific device, and allocate from the cached buffer.</p>\n<pre><code class=\"language-cpp\">// c10/cuda/CUDACachingAllocator.cpp\nclass NativeCachingAllocator : public CUDAAllocator {\nDataPtr allocate(size_t size) const override {\n int device = 0;\n C10_CUDA_CHECK(c10::cuda::GetDevice(&device));\n void* r = nullptr;\n if (forceUncachedAllocator()) {\n // Deliberately don't use cudaMallocMaybeCapturing here, to force an error\n // if someone tries to use forceUncachedAllocator while capturing.\n C10_CUDA_CHECK(cudaMalloc(&r, size));\n const c10::impl::PyInterpreter* interp = c10::impl::GPUTrace::get_trace();\n if (C10_UNLIKELY(interp)) {\n (*interp)->trace_gpu_memory_allocation(reinterpret_cast<uintptr_t>(r));\n }\n return {r, r, &uncached_delete, Device(DeviceType::CUDA, device)};\n }\n if (size != 0) {\n const_cast<NativeCachingAllocator*>(this)->malloc(\n &r, device, size, cuda::getCurrentCUDAStream(device));\n }\n return {r, r, &local_raw_delete, Device(DeviceType::CUDA, device)};\n}\n\n/** allocates a block which is safe to use from the provided stream */\nvoid malloc(void** devPtr, int device, size_t size, cudaStream_t stream) {\n Block* block = device_allocator[device]->malloc(device, size, stream);\n add_allocated_block(block);\n *devPtr = (void*)block->ptr;\n const c10::impl::PyInterpreter* interp = c10::impl::GPUTrace::get_trace();\n if (C10_UNLIKELY(interp)) {\n (*interp)->trace_gpu_memory_allocation(\n reinterpret_cast<uintptr_t>(*devPtr));\n }\n}\n}\n</code></pre>\n<p>The <code>DeviceCachingAllocator::malloc</code> will try to release some memory before re-allocate.</p>\n<pre><code class=\"language-cpp\">// c10/cuda/CUDACachingAllocator.cpp\nclass DeviceCachingAllocator {\n Block* malloc(int device, size_t orig_size, cudaStream_t stream) {\n // ...\n bool block_found =\n get_free_block(params)\n || (trigger_free_memory_callbacks(params) && get_free_block(params));\n\n // cannot find free block, try to allocate\n if (!block_found) {\n // free some blocks and re-allocate\n block_found = alloc_block(params, false, context)\n || (release_available_cached_blocks(params) &&\n alloc_block(params, false, context))\n || (C10_LIKELY(captures_underway == 0) &&\n release_cached_blocks(context) &&\n alloc_block(params, true, context));\n }\n\n // report OOM events\n // ...\n}\n</code></pre>\n<p>The <code>DeviceCachingAllocator::alloc_block()</code> will be called in the following snippet.<br>\nAnd if the error is <code>cudaErrorMemoryAllocation</code>,it will erase the inner error.</p>\n<pre><code class=\"language-cpp\">// c10/cuda/CUDACachingAllocator.cpp\nclass DeviceCachingAllocator {\n bool alloc_block(\n AllocParams& p,\n bool isRetry,\n const std::shared_ptr<GatheredContext>& ctx) {\n // ...\n if (set_fraction &&\n total_allocated_memory + size > allowed_memory_maximum) {\n p.err = cudaErrorMemoryAllocation;\n return false;\n } else if (\n CachingAllocatorConfig::expandable_segments() &&\n // our checkpointing logic for private pools doesn't support\n // the expandable_segments_ structure yet\n !p.pool->owner_PrivatePool) {\n p.block = try_allocate_expandable_block(\n p.device(), p.stream(), p.pool, p.size(), ctx);\n if (p.block) {\n p.err = cudaSuccess;\n } else {\n p.err = cudaErrorMemoryAllocation;\n }\n return bool(p.block);\n } else {\n p.err = cudaMallocMaybeCapturing(&ptr, size);\n if (p.err != cudaSuccess) {\n if (p.err == cudaErrorMemoryAllocation) {\n // erase the error\n (void)cudaGetLastError();\n } else {\n // errors not related to OOM, throw it\n C10_CUDA_CHECK(p.err);\n }\n return false;\n }\n }\n // ...\n return true;\n }\n}\n</code></pre>\n<p>The experimental allocator in PyTorch is based on CUDA virtual memory API.<br>\nBy set environment variable <code>PYTORCH_CUDA_ALLOC_CONF=expandable_segments</code>,<br>\n<code>DeviceCachingAllocator::alloc_block()</code> will call the <code>CachingAllocatorConfig::expandable_segments()</code> path,<br>\n<code>DeviceCachingAllocator::try_allocate_expandable_block</code> will call the <code>cuMemCreate</code> API.</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240513/",
"title": "cuMemFree is with implicit-synchronization",
"summary": "Analysis on cuMemFree and PyTorch memory management",
"date_modified": "2024-05-13T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Meta 的 GPU 可观测建设</h1>\n<p>最近注意到一篇<a href=\"https://atscaleconference.com/systemscale-ai-observability/\">博客</a>,里面讲了 Meta 是如何在各个层面上对 AI 系统进行可观测建设的。</p>\n<p>从底层到上层都是一些耳熟能详的工具,或者已经火过一把的 Meta 开源项目,例如:</p>\n<ul>\n<li><a href=\"https://github.com/facebookincubator/dynolog\">Meta Dynolog</a></li>\n<li>Nvidia’s Datacenter GPU Manager (DCGM)</li>\n<li><a href=\"https://pytorch.org/docs/stable/profiler.html\">Pytorch Profiler</a></li>\n<li><a href=\"https://github.com/pytorch/kineto\">Pytorch Kineto</a></li>\n</ul>\n<p>但是里面有一个有趣的项目:<a href=\"https://github.com/facebookincubator/strobelight\">Strobelight</a>,一个基于 BPF 的 GPU 可观测工具、神秘啊,别的项目博客中都贴了仓库地址,这个明明能在 github 搜到,但是没贴链接 = =。</p>\n<p>具体而言,Strobelight 是一个在 Meta 的所有主机上运行的守护进程,既充当分析器又充当分析器编排工具。可以通过某些事件(例如 OOM 事件)触发分析。Strobelight 由多个子分析器组成。通过这些子分析器,它能够从机群中的任何主机收集各种性能分析,如 CPU 堆栈、内存快照和Python 堆栈。Strobelight 在许多子分析器中依赖于 BPF。他们最近还向 Strobelight 的分析器套件中添加了基于 BPF 的 CPU->GPU 分析器。</p>\n<p>应该就是文中的典型用例 Gpusnoop,它是一个基于 BPF 的 profiler 套件。它能 hook 到一系列有趣的 GPU 事件上,例如 CUDA 核函数加载、CUDA events 同步、显存管理事件,也支持 pytorch 相关事件。</p>\n<h1>案例分析:基于 BPF trace CUDA 显存管理</h1>\n<p>通过使用 uprobes attach 到 CUDA 显存管理事件,gpusnoop 可以用于构建显存分配的时间线,并检测泄漏的显存的调用栈。</p>\n<p>下面是简化后的代码,它将 attach 到 cudaMalloc 和 cudaFree 事件,并记录所有已释放和未释放的显存,以及它们的大小和调用栈。</p>\n<p>用户侧代码示例:</p>\n<p><img src=\"user.png\" alt=\"\"></p>\n<p>BPF 侧代码示例:</p>\n<p><img src=\"bpf.png\" alt=\"\"></p>\n<p>上述代码 attach 到 cudaMalloc 事件,并跟踪请求的分配大小,然后 attach 到 cudaMalloc 的返回事件,获取分配到的地址。它使用这些数据来跟踪未释放的显存分配及其大小。然后,我们可以使用这些数据,在任何时刻跟踪未释放的显存分配及其大小和调用栈。我们还可以可视化这些数据,并检测显存泄漏,或者使用它来缩小使用最多显存的调用栈范围。</p>\n<h1>Strobelight 代码分析:基于 BPF trace CUDA 核函数加载</h1>\n<p>Gpusnoop 的代码应该没有开源,但是 <a href=\"https://github.com/facebookincubator/strobelight\">Strobelight</a> 里面倒是确实有一个 trace CUDA 核函数加载的 BPF <a href=\"https://github.com/facebookincubator/strobelight/blob/main/strobelight/src/profilers/gpuevent_snoop/GpuEventSnoop.cpp\">代码示例</a>。</p>\n<p>全局定义了一个 64M 的 BPF Ring Buffer 用来存放 BPF 侧 trace 到的 <code>gpukern_sample</code> 信息,它的定义如下,也就是 <code>cudaLaunchKernel</code> 的参数,和一些 cpu 侧的信息、栈。</p>\n<pre><code class=\"language-cpp\">struct gpukern_sample {\n int pid, ppid;\n char comm[TASK_COMM_LEN];\n uint64_t kern_func_off;\n int grid_x, grid_y, grid_z;\n int block_x, block_y, block_z;\n uint64_t stream;\n uint64_t args[MAX_GPUKERN_ARGS];\n size_t ustack_sz;\n stack_trace_t ustack;\n};\n</code></pre>\n<p>BPF 侧代码比较简短,完整贴上来了</p>\n<pre><code class=\"language-cpp\">#define SP_OFFSET(offset) (void*)PT_REGS_SP(ctx) + offset * 8\n\nSEC("uprobe")\nint BPF_KPROBE(\n handle_cuda_launch,\n u64 func_off,\n u64 grid_xy,\n u64 grid_z,\n u64 block_xy,\n u64 block_z,\n uintptr_t argv) {\n struct gpukern_sample* e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);\n\n struct task_struct* task = (struct task_struct*)bpf_get_current_task();\n\n e->pid = bpf_get_current_pid_tgid() >> 32;\n e->ppid = BPF_CORE_READ(task, real_parent, tgid);\n bpf_get_current_comm(&e->comm, sizeof(e->comm));\n\n e->kern_func_off = func_off;\n e->grid_x = (u32)grid_xy;\n e->grid_y = (u32)(grid_xy >> 32);\n e->grid_z = (u32)grid_z;\n e->block_x = (u32)block_xy;\n e->block_y = (u32)(block_xy >> 32);\n e->block_z = (u32)block_z;\n\n bpf_probe_read_user(&e->stream, sizeof(uintptr_t), SP_OFFSET(2));\n\n // Read the Cuda Kernel Launch Arguments\n for (int i = 0; i < MAX_GPUKERN_ARGS; i++) {\n const void* arg_addr;\n // We don't know how many argument this kernel has until we parse the\n // signature, so we always attemps to read the maximum number of args,\n // even if some of these arg values are not valid.\n bpf_probe_read_user(\n &arg_addr, sizeof(u64), (const void*)(argv + i * sizeof(u64)));\n\n bpf_probe_read_user(&e->args[i], sizeof(arg_addr), arg_addr);\n }\n\n // Read the Cuda Kernel Launch Stack\n e->ustack_sz =\n bpf_get_stack(ctx, e->ustack, sizeof(e->ustack), BPF_F_USER_STACK) /\n sizeof(uint64_t);\n\n bpf_ringbuf_submit(e, 0);\n return 0;\n}\n</code></pre>\n<p>用户侧基本就是 libbpf 的脚手架样板。然后需要注意的是因为是用 uprobe,得额外写一个解析 <code>/proc/$pid/exe</code> 里每个文件找 <code>cudaLaunchKernel</code> 符号的步骤,然后把 attach 上去,这里 <code>$pid</code> 是被 attach 的进程。</p>\n<p>从 ring buffer 里面 poll 到 <code>gpukern_sample</code> 后,触发下面这个回调进行分析</p>\n<pre><code class=\"language-cpp\">static int handle_event(void* ctx, void* data, size_t /*data_sz*/) {\n const struct gpukern_sample* e = (struct gpukern_sample*)data;\n\n SymUtils* symUtils = (SymUtils*)ctx;\n\n SymbolInfo symInfo = symUtils->getSymbolByAddr(e->kern_func_off, env.args);\n\n fmt::print(\n "{} [{}] KERNEL [0x{:x}] STREAM 0x{:<16x} GRID ({},{},{}) BLOCK ({},{},{}) {}\\n",\n e->comm,\n e->pid,\n e->kern_func_off,\n e->stream,\n e->grid_x,\n e->grid_y,\n e->grid_z,\n e->block_x,\n e->block_y,\n e->block_z,\n symInfo.name.substr(0, MAX_FUNC_DISPLAY_LEN) +\n (symInfo.name.length() > MAX_FUNC_DISPLAY_LEN ? "..." : ""));\n\n fmt::print("Args: ");\n for (size_t i = 0; i < symInfo.args.size() && i < MAX_GPUKERN_ARGS; i++) {\n fmt::print("{} arg{}=0x{:x}\\n ", symInfo.args[i], i, e->args[i]);\n }\n fmt::print("\\n");\n\n fmt::print("Stack: \\n");\n auto stack = symUtils->getStackByAddrs((uint64_t*)e->ustack, e->ustack_sz);\n for (auto& frame : stack) {\n frame.print();\n }\n fmt::print("{:-<40}\\n", '-');\n return 0;\n}\n</code></pre>\n<p>感觉这个项目的难度在于 <a href=\"https://github.com/facebookincubator/strobelight/blob/main/strobelight/src/utils/SymUtils.cpp\">symUtils</a> 里面的这些 parser 写起来比较难…… BPF 侧还是很简单的</p>\n<h1>为什么是 BPF</h1>\n<p>所以核心问题是为什么要用 BPF?原始博客中的几个说法感觉说服力并不强,只是从 BPF 的角度去讲了</p>\n<ul>\n<li>BPF 自身的安全性、可编程性:老生常谈。</li>\n<li>性能开销低:可随时启停,基于 uprobe,存疑。</li>\n<li>用户无感,不需要用户代码改动。</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20240427/",
"title": "基于 BPF 的 GPU 可观测:Meta strobelight",
"summary": "博客解读和示例代码分析",
"date_modified": "2024-04-27T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>前段时间 Andrej Karpathy 大神发了视频手把手带着大家了解 LLM 使用的 Tokenizer,记录一下。</p>\n<p><a href=\"https://github.com/karpathy/minbpe\">教学仓库地址</a></p>\n<p><a href=\"https://www.youtube.com/watch?v=zduSFxRajkE4\">视频地址</a></p>\n<p>视频中演示的应用,对不同的 tokenizer 的分词结果做了<a href=\"https://tiktokenizer.vercel.app\">可视化</a></p>\n<h1>引入</h1>\n<p>首先来看几个问题,实际上它们都和 tokenizer 有关:</p>\n<ul>\n<li>为什么 LLM 不能拼写单词</li>\n<li>为什么 LLM 不能做一些很简单的字符串操作,比如反转字符串</li>\n<li>为什么 LLM 在非英语任务上表现差</li>\n<li>为什么在简单数学任务上 LLM 的表现差</li>\n<li>为什么 GPT-2 在编写 Python 代码方面有困难</li>\n<li>为什么 LLM 看到 <code><end-of-text></code> 就终止了</li>\n<li>Open AI 的 "Trailing space" 提示是什么用意</li>\n<li>为什么 LLM 看到 "SolidGoldMagikarp" 就出错了</li>\n<li>为什么在使用 LLM 时,我应该使用 YAML 而不是 JSON</li>\n</ul>\n<p>接着 Andrej 在演示应用中测试了几个不同的 Tokenizer 的效果,发现 GPT-2 在处理 python 代码时,倾向于将每个空格划分为独立的 token,导致缩进中出现了大量冗余 token;而 GPT-4 使用的 cl100k_base 在处理 python 代码时,对缩进的处理更加智能。同时,GPT-4 划分出的 token 数量远小于 GPT-2。当然 token 也不是越少越好,相当于要找到一个平衡点,信息足够密集但是又可以被划分开。</p>\n<h1>基础的字节对编码</h1>\n<p>python 中字符串以 unicode 编码,借助 <code>ord</code> 方法可以查看单个字符的码点。能不能简单地使用这些数字作为输入呢?<br>\n因为这会导致我们模型的词汇量特别大,unicode 有 15 万码点,并且还在活跃地扩充中。(以 transformer 为例,导致最终用于预测的 softmax 层节点过多,要在 15 万个可能中预测一个;过大的 token 数,也意味着 embedding 要足够大,导致 transformer 模型本身的规模也变大了,上下文也会显著增加),因此我们要找到一个好的编码。</p>\n<p>unicode 下的三种编码类型,UTF-8, UTF-16, UTF-32,将 unicode 转换为二进制字节流。我们更偏向于使用 UTF-8,因为后两者都会让我们的字节流中出现大量冗余的 0。直接拿字节流显然也是不好的,单个字节 256 个可能,完全是在预测字节序列。而且所有的输入文本都会被拉伸为很长的字节流表示,transformer 的注意力 context 会被浪费掉,特别是很多模型的 context 是很有限的。Andrej 在这里实际上展示了一篇论文,MEGABYTE: Predicting Million-byte Sequences with Multiscale Transformers。</p>\n<p>于是出现了字节对编码算法,Byte Pair Encoding(BPE),Andrej 再次引用了维基百科来介绍它。实际上是一种无损压缩算法,每一轮会将当前数据转中最常见的一对编码,替换为一个新的编码,写入到字典中,循环这个过程直到达到设定的词汇量上限。(和 LZ77 等无损压缩算法不同,BPE 保证了编码后还是之前的编码数据类型,并保留了相邻 token 的相邻关系。)在大量的文本上完成训练后,就可以得到一个通用的编码字典,这里使用的训练文本和 LLM 的训练是独立的,可能不一致。训练集中的不同语言的密度,将会影响学习到的字典对它是否进行有效的压缩,决定了它最终在 token 空间中的密度。如果训练集中有更多中文,那显然更多中文的用词习惯将会被学习到,编码后输入给 LLM 的中文序列也会更短。</p>\n<p>Andrej 在这里手写了一个基础的的 BPE encoder/decoder 用作 tokenizer。</p>\n<h1>OpenAI 更为先进的 tokenizer</h1>\n<p>GPT-2 的论文,Language Models are Unsurpervised Multitask Learners,介绍了基础的 BPE 算法面临的问题:如果文本中包含了词语 dog,它在文章中以 "dog,","dog!" 等形式大量出现,那 BPE 显然会讲这些相邻字符合并到字典中进行压缩,显然这很低效。于是他们使用了正则表达式,添加了人工先验。</p>\n<p>在 OpenAI 的 github 上查看 GPT-2 的代码仓库,会发现一个 <a href=\"https://github.com/openai/gpt-2/blob/master/src/encoder.py\"><code>gpt-2/src/encoder.py</code></a>,里面有他们使用的正则:</p>\n<pre><code class=\"language-python\">regex.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\\p{L}+| ?\\p{N}+| ?[^\\s\\p{L}\\p{N}]+|\\s+(?!\\S)|\\s+""")\n</code></pre>\n<p>这里主要做的就是把字符串拆分开,数字和字母拆分开,保留 <code>'s</code>,<code>'ve</code> 这样的 token(源码上有段注释,表明了这里实际上有缺陷,即忽略了大写的情况,只匹配了小写,我们在后面将会看到 cl100k 修复了它)。值得注意的是,它还会保留单词前的单个空格。比如 <code>----you</code> 将会被分割成 <code>---</code> 和 <code>-you</code>,这里 <code>-</code> 代表空格。</p>\n<p>遗憾的是 GPT-2 开源的只是推理代码,而非训练代码,因此我们没法给定文本训练一个自己的 tokenizer。</p>\n<p>OpenAI 有另一个公开的 tokenizer 库,<a href=\"https://github.com/openai/tiktoken\">tiktoken</a> 库,它是由 rust 写的!但是同样它只是一个推理库,无法进行训练。</p>\n<p>GPT-4 的 cl100k 中使用的正则表达式可以在 tiktoken 的<a href=\"https://github.com/openai/tiktoken/blob/1b9faf2779855124f05174adf1383e53689ed94b/tiktoken_ext/openai_public.py#L85\">源码中找到</a>,</p>\n<pre><code class=\"language-python\">r"""'(?i:[sdmt]|ll|ve|re)|[^\\r\\n\\p{L}\\p{N}]?+\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]++[\\r\\n]*|\\s*[\\r\\n]|\\s+(?!\\S)|\\s+"""\n</code></pre>\n<p>这个新正则表达式中,值得关注的点除了 GPT-2 中的大小写问题被修复了,还有就是现在只会匹配长度为 1-3 的数字,不会合并超过三位数字,以防 token 中出现很长的数字。</p>\n<p>回到 GPT-2 的 <a href=\"http://encoder.py\">encoder.py</a>,会发现 OpenAI 的 encoder、decoder 确实是类似 Andrej 前面手写的 BPE tokenizer。 但是这里奇怪的一点是他们同时还有一对 byte encoder 和 byte decoder。用于 BPE encoder/decoder 前后。</p>\n<h1>特殊 token</h1>\n<p>除了 BPE 算法中组合出的 token,我们也可以加入一些人工设置的特殊 token,区分数据的不同部分。</p>\n<p>例如,GPT-2 的 token 字典的规模是 50257,这个规模是怎么来的呢?单个字节是 256,然后它们在训练时做了 50000 次合并,最后剩余一个是 <code>'<|endoftext|>'</code>。打印下字典,也会发现事实如此。类似的,微调过的对话模型 gpt-3.5-turbo 使用了 <code><im_start></code>、<code><im_end></code> 用作特殊的 token 标记对话的开始和结束。</p>\n<p>tiktoken 也允许我们加载某个基础的 tokenizer,然后创建自己的特殊 token 去拓展它。</p>\n<h1>sentencepiece</h1>\n<p><a href=\"https://github.com/google/sentencepiece\">sentencepiece</a> 是一个常用的 tokenizer 库,它不仅可用于推理,还允许你训练自己的 tokenizer,Llama 和 Mistral 系列的模型都是用了它。</p>\n<p>它和 Tiktoken 不同。Tiktoken 会首先获取 unicode 码点,然后用 UTF-8 编码成为字节流,再运行 BPE 合并字节。而 sentencepiece 会直接在 unicode 码点的层级上运转。因此会有一些罕见的码点不会经常出现,可以用超参数 <code>character_coverage</code> 来控制,稀有码点会被直接转换成 <code>UNK</code> token。如果开启了 <code>byte_fallback</code> 选项,则会编码到 utf-8 并对这些字节进行编码。</p>\n<p>sentencepiece 有很多历史包袱,可配置的参数特别多,Andrej 从 Llama-2 的配置中抄了一段过来。值得注意的是里面的 normalization 相关的选项完全可以关掉,这是之前 nlp 基础任务做正则化用的,但是显然 LLM 可以接收原始的字符串。另外,sentencepiece 有 sentence 的概念,这是因为当年曾有一种想法是在一堆独立的句子上训练 tokenizer,但是现在大家都在完整的文章上训练了,而且定义到底什么是一个句子,是很困难的。</p>\n<h1>总结</h1>\n<p>回到最初的几个问题:</p>\n<ul>\n<li>为什么 LLM 不能拼写单词</li>\n</ul>\n<p>因为字符被组合成了 tokens,其中一些 token 很长,因此模型对字符的理解会较弱。在视频中 Andrej 让 ChatGPT 去数一个词中的特定的字符个数,得到了错误的答案。我在 Poe 上用不同模型进行了尝试,如下图,只有 Llama-3 答对了。</p>\n<p><img src=\"./char-count-challenge.png\" alt=\"char-count-challenge\"></p>\n<ul>\n<li>为什么 LLM 不能做一些很简单的字符串操作,比如反转字符串</li>\n</ul>\n<p>同样由于缺乏对字符串的理解能力,LLM 也不擅长做这件事。给它一个算法,然后让它模拟倒是可行。</p>\n<ul>\n<li>为什么 LLM 在非英语任务上表现差</li>\n</ul>\n<p>LLM 训练时候小语种数据不足,tokenizer 本身也没有经过良好训练。</p>\n<ul>\n<li>为什么在简单数学任务上 LLM 的表现差</li>\n</ul>\n<p>加法类似一种字符级别的算法,但是基于 BPE,数字会被合并成 token,导致难以处理。视频中推荐阅读博客 <a href=\"https://www.beren.io/2023-02-04-Integer-tokenization-is-insane/\">Integer tokenization is insane</a>。</p>\n<ul>\n<li>为什么 GPT-2 在编写 Python 代码方面有困难</li>\n</ul>\n<p>如全文所述,GPT-2 处理 python 的缩进时,把每个空格都当做了一个 token,这也影响到了上下文和 attention 的计算。在 GPT-4 中进行了改进。</p>\n<ul>\n<li>为什么 LLM 看到 <code><end-of-text></code> 就终止了</li>\n</ul>\n<p>因为在 tokenizer 中使用它作为了特殊标记,可能是在训练时用来分割语料。特殊标记可能成为 LLM 的攻击点。</p>\n<ul>\n<li>OpenAI 的 "Trailing space" 提示是什么用意</li>\n</ul>\n<p>使用 OpenAI 的生成模型时,输入 Prompt 让它填充,例如 "Here is a tag lien for the ice cream shop:"。但是如果换用"Here is a tag lien for the ice cream shop: ",即句尾多加一个空格,就会收到 "Trailing space" 警告。</p>\n<p>这也是前文提过的,OpenAI 的 tokenizer 使用的正则,会保留单词前的单个空格,在 BPE 中会被合并到 token 中。sentencepiece 中甚至有一个选项可以在句子中添加一个空格前缀,以防止这种现象出现。</p>\n<p>多加了一个空格,就导致当前的 prompt 在转换成 token 后,出现了一个远离正常的 token 分布的奇异 token,会影响模型预测出的概率分布。这些 LLM 是构建于 token 之上的,而非我们常规认知里的的字符。</p>\n<ul>\n<li>为什么 LLM 看到 "SolidGoldMagikarp" 就会输出一些混乱的结果</li>\n</ul>\n<p>视频中推荐了文章 <a href=\"https://www.lesswrong.com/posts/aPeJE8bSo6rAFoLqg/solidgoldmagikarp-plus-prompt-generation\">SolidGoldMagikarp (plus, prompt generation)</a>。文中对 token 的嵌入做了聚类,然后发现了一些奇怪的 token 被聚集在了一起。这些 token 是从哪里来的,字面上看,它们毫无意义。有趣的是,如果你向模型输入包含这些 token 的 prompt,会得到混乱的回复。</p>\n<p>深入调查后,发现这个 SolidGoldMagikarp 其实是一名 reddit 用户。于是,可能是因为训练 tokenizer 的数据和训练语言模型的数据不同,而碰巧 tokenizer 的训练数据中包含了大量来自 reddit 的文本,然后 SolidGoldMagikarp 他发了很多帖子,或者被多次回复引用,然后被 BPE 算法学到了,合并成了一个 token。但是当训练语言模型的时候,并没有那部分 reddit 数据,于是这个 token 就不会被激活,它会被随机初始化,但是永远不会被采样到,也不会被更新,有点像未分配的内存。然后在推理的时候,你触发到了这个 token,那么就是采样到了未经初始化的嵌入,导致了未定义的行为。</p>\n<p>无端联想:前两天看了个很乐的文章,百度贴吧弱智吧的语料是很好的训练来源哈哈</p>\n<ul>\n<li>为什么在使用 LLM 时,我应该使用 YAML 而不是 JSON</li>\n</ul>\n<p>YAML 这种结构化文本在 GPT-4 上会消耗更少的 token,更加高效,更加经济。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20240423/",
"title": "Let's build the GPT Tokenizer 笔记",
"summary": "学习了一下 Andrej Karpathy 大神的 Tokenizer 视频课程",
"date_modified": "2024-04-23T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>早年 Intel Xen gVirt 实现,后来迁移到 KVMGT,ATC'14 原文</p>\n<p><a href=\"https://www.usenix.org/conference/atc14/technical-sessions/presentation/tian\">https://www.usenix.org/conference/atc14/technical-sessions/presentation/tian</a></p>\n<p>中文笔记转载自知乎专栏,收藏目的,侵删,图就不搬运了,论文原图</p>\n<p><a href=\"https://zhuanlan.zhihu.com/p/383496258?utm_id=0\">https://zhuanlan.zhihu.com/p/383496258?utm_id=0</a></p>\n<h1>总结</h1>\n<p>gVirt是基于之前的工作XenGT,基于Xen hypervisor对Intel on-chip GPU实现了全GPU虚拟化。它主要是针对图形加速而不是GPGPU计算。gVirt认为frame buffer和command buffer是GPU中对性能影响最大的因素,它允许每一个VM以直通(direct pass-through)的方式,在没有hypervisor介入的情况下访问。为了达到此目的,graphics memory resource被gVirt Mediator划分,让每一个VM都在划分的内存中有自己的frame buffer和command buffer。同时,privileged GPU instruction被gVirt Mediator在Xen的driver domain中trap and emulated。这在没有很大性能开销的情况下保证了多个VM之间的隔离性。整个过程被叫做mediated pass-through。KVMGT是gVirt迁移到KVM上的版本,从Linux内核4.10开始,已经集成到了Linux内核中。</p>\n<h1>Abstract</h1>\n<p>背景:GPU虚拟化技术是新兴虚拟化场景中的一种使能技术。</p>\n<p>解决方法:本文介绍了gVirt,一种产品级的GPU虚拟化实现,具有以下特点:1)在guest里面运行本地图形驱动的全GPU虚拟化技术 2)同时实现好的性能、好的扩展性、guest之间安全的隔离的mediated pass-through。</p>\n<p>特点:gVirt为每一个虚拟机提供了一个虚拟的完整的GPU。VM能在没有hypervisor介入的情况下直接访问对性能十分重要的资源,而来自guest的privileged operation以最小的开销进行trap-and-emulated。</p>\n<p>结果:实验表明gVirt能够对GPU密集型的工作负载实现95%的本地性能,能够最多支持7个VM。</p>\n<h1>1. Introduction</h1>\n<p>背景:GPU的使用场景越来越多,越来越丰富的GPU使用场景对同时具有good performance、full features、sharing capability的全GPU虚拟化表现出了更高的需要。现现代的桌面虚拟化,无论是本地的XenClient,还是远端在服务器上的VMare Horizon,都需要GPU虚拟化来为用户在虚拟机里面支持未经破坏的本地图形用户体验。与此同时,云服务提供商开始建立GPU加速的虚拟化实例,把GPU计算资源作为服务来sell。只有全GPU虚拟化才能够支持这些各种各样应用的需要。</p>\n<p>研究现状:当前,在performance、features、sharing capability之间找到平衡,实现全GPU虚拟化依然是一个挑战。图1展现了GPU虚拟化解决方法的光谱图。</p>\n<p>Device Emulation具有很高的复杂度和非常低的性能,无法满足现实的需要。API forwarding利用了frontend driver,转发VM里面的高级别API调用到主机上,来实现加速。然而,API Forwarding面临着支持full feature的挑战,由于对于guest graphics software stack的侵入式修改的复杂性,以及guest和host graphics software stack之间的不兼容性。Direct pass-through为一个单独的VM制定了GPU,提供了full feature和最好的性能,但是牺牲了设备在VM直接的sharing capability。Mediated将对性能影响十分大的资源采用了pass through的方式,但是对privileged operations采取了trap and emulated的方法,有很好的性能、full feature、sharing capability。(实际上就是实现了performance 和 security的tradeoff)。</p>\n<p>具体方法:本文介绍了gVirt,第一种产品级的GPU虚拟化实现,具有:1)在guest中运行native driver的全GPU虚拟化 2)同时实现good performance、scalability、VM之间secure isolation的mediated pass-through。一个虚拟化的GPU(vGPU),具有full feature,被展现给每一个VM。VM能够直接在没有hypervisor介入的情况下直接访问performance-critical resource。而来自guest的privileged operation通过trap and emulated的方式来在VM之间提供隔离性。vGPU的context是每个周期都进行切换,来在用户感知不到的情况下,让多个VM共享GPU。gVirt是在Xen中实现的,Intel CPU中的集成显卡。但是,gVirt的原理和架构对其他的GPU和hypervisor也是适用的。gVirt的source code已经开源了。</p>\n<p>贡献:本文主要有如下贡献:</p>\n<ul>\n<li>介绍了使用mediated pass-through,在guest中运行native graphics driver的全GPU虚拟化解决方法</li>\n<li>通过graphics memory resource partitioning,address space balloning、direct execution of guest command buffer来pass through performance-critical resource access</li>\n<li>通过在命令提交的时候监视和保护command buffer来隔离guest,并使用了smart shadowing</li>\n<li>通过对硬件特征和graphics driver的虚拟化扩展提高了性能(对Linux内核模式的graphics driver修改了少于100行代码)(本文确实修改了驱动,Nvidia无法修改驱动,所以对其涉及方法能够利用与Nvidia上存疑)</li>\n<li>为后续的GPU虚拟化研究提供了一个产品级的开源代码库,对Linux和Windows虚拟机都进行了全面的分析</li>\n<li>证明gVirt能够在GPU-intensive workload实现95%的本地性能,对那些对CPU和GPU都要求高的应用实现83%的性能</li>\n</ul>\n<h1>2. GPU Programming Model</h1>\n<p>大体上,Intel Processor Graphics是按照图2显示的那样工作的。</p>\n<p>render engine从command buffer中取GPU 命令,来加速不同特性中的图形渲染。display engine从frame buffer中取像素数据,然后把它们送到外部的显示器中来显示。</p>\n<p>这种架构对多数现代GPU是适用的,但是可能在graphics memory如何实现上面有不同之处。Intel Processor Graphics将系统内存作为graphics memory,而其他GPU可能使用芯片内存。系统内存能够通过GPU page table来映射进多个虚拟地址空间。一个被称为global graphics memory的2GB全局虚拟地址空间,能够被CPU和GPU访问,是通过global page table来映射的。Local graphics memory space以多个2GB本地虚拟地址空间形式支持,但是被限制只能被render engine通过local page table访问。Global graphics memory主要是frame buffer,但是也被用作command buffer。在硬件加速过程中,会进行大量对local graphics memory的访问。</p>\n<p>CPU是通过GPU-specific的命令来对GPU进行编程的,像图2中展示的那样,以生产者-消费者模型来进行编程的。graphics driver把GPU命令编程进command buffer,包括primary buffer和batch buffer,根据像OpenGL和DirectX一样的高层编程API。然后GPU从command buffer中取指令,并执行。primary buffer(即ring buffer)能够把其他的batch buffer连接在一起。primary buffer和ring buffer一个意思。batch buffer用来传输一个编程模型的多数指令(大约98%)。一个寄存器对(head,tail)被用来控制ring buffer。CPU通过更新tail来向GPU提交命令,GPU从head取命令,然后通过更新head来通知GPU,其提交命令已经执行结束。</p>\n<p>已经介绍了GPU架构的虚拟化,对本文来说,非常有必要理解真实世界中的graphics application是如何使用GPU硬件的,来让我们能够更有效的在VM中对其进行虚拟化。选择了一些具有代表性的GPU-intensive 3D workload(Phoronix Test Suite),测试了四个重要的接口:frame buffer,command buffer,携带GPU 页表的GPU Page Table Entries,包含内存映射I/O寄存器的I/O寄存器。图3展示了运行Phoronix 3D 工作负载时,对四个接口的平均访问频率。</p>\n<p>从图3可以看出,frame buffer和command buffer是对性能影响最大的资源。当应用被load的时候,很多源向量和像素被CPU写,所以frame buffer栈主要。在运行时,CPU通过命令来编程GPU,来对frame buffer进行渲染,所以command buffer称为主要被访问呢的对象。</p>\n<p>总结:其实frame buffer就是用来存数据的,处理图像的时候,首先得把要处理的数据读取到frame buffer,这个过程中,cpu频繁往frame buffer中写入数据;command buffer用来存命令,在执行的时候,cpu需要频繁把命令发送到command buffer中,所以command buffer访问频繁。</p>\n<h1>3. Design and Implementation</h1>\n<p>gVirt是一个使用mediated pass-through的全GPU虚拟化解决方案。因此,gVirt为一个VM展现了一个全面的GPU,在VM里面运行本地graphics driver。挑战主要有三点 1)虚拟化一整个精密现代GPU的复杂性 2)由于多个VM共享GPU带来的性能开销 3)VM之间的安全隔离。gVir通过mediated pass-through的技术降低了复杂性,并实现了好的性能,enforces the secure isolation with the smart shadowing scheme。</p>\n<h2>3.1 Architecture</h2>\n<p>图4展示了gVirt的整体架构,基于Xen hypervisor,Dom0作为特权VM,还有多个用户级VM。</p>\n<p>Xen hypervisor中的gVirt stub模块,扩展了内存虚拟化模块,包括用于user VM的EPT,和用于Dom0的PVMMU,来实现trap and pass-through的策略。每一个VM能直接运行本地graphics driver,并且能够直接访问对性能影响较大的资源:即frame buffer和command buffer。为了保护privileged resources,即I/O 寄存器和PTEs,来自user VM和Dom0的相关的访问,被trap and fowarded 到Dom0中的mediator driver来进行模拟。mediator利用hypercall来访问物理GPU。另外,mediator实现了一个GPU调度器,在Xen中与CPU调度器同时运行,用来负责在VM之间共享GPU。</p>\n<p>gVirt使用物理GPU来直接执行来自VM的命令,因此避免了仿真render engine的复杂性。同时,对frame buffer和command buffer的pass-through,减少了hypervisor在CPU访问上的开销。GPU调度器保证了每一个VM都有有单直接GPU执行的时间。</p>\n<p>gVirt Stub:是通过拓展Xen vMMU模块来选择性地trap或者pass-through对于某个GPU资源的guest access。传统的Xen只支持对于一个设备的全部I/O资源的pass-through或者trap,即要么采用device emulation,要么采用pass-through。gVirt通过操作EPT entry来选择性地把某块特定地址范围对user VM进行展示或隐藏。同时,利用PVMMU中的PET的一个二进制位来为Dom0选择性地trap或者pass-through某块内存区域。在两种情况下,privileged I/O都是要被trapped。所有被trapped的访问都被转发到mediator来进行模拟,mediator利用hypercall来访问物理GPU资源。</p>\n<p>Mediator:gVirt mediator driver为privileged resource access仿真虚拟GPU,并且在虚拟GPU之间进行上下文切换。gVirt依靠Dom0 graphics driver来对物理设备进行初始化和管理能源。一种分离CPU和GPU的调度机制在gVirt中实现,基于两个原因:首先,GPU上下文切花不能的开销是CPU上下文切换开销的1000倍。第二,计算机系统中,CPU的核心数量和GPU的核心数量不同。gVirt实现了一个与现存CPU调度器分开的GPU调度器。这种分离调度模型引起了对来自CPU和GPU的资源的并行访问的需要。例如,当CPU正在访问VM1的graphics memory,GPU能够访问VM2的graphics memory,同时进行。</p>\n<p>Native driver:gVirt在VM中运行native graphics driver,直接访问部分performance-critical 资源,privileged operation通过mediator进行仿真。</p>\n<p>Qemu:使用QEMU来模拟遗留的VGA模式,使用virtual BIOS来启动user VMs。gVirt extension 模块决定是否一个仿真需要应该被定向到mediator还是Qemu。、</p>\n<h2>3.2 GPU Sharing</h2>\n<p>mediator管理所有VM的vGPU,通过将特权操作trap-and-emulated。mediator处理物理GPU 中断,也有可能对特定的VM产生虚拟中断。</p>\n<p>Render engine scheduling:gVirt实现了一个粗粒度的服务质量策略。16ms被选择作为调度时间片,因为这要比人类能注意到的图像改变周期少。这样一个相对较大的时间片也是由于GPU进行context switch的时间大约是CPU context switch的1000倍,所以该时间片不能像CPU调度器一样小。来自一个VM的命令被连续提交给一个GPU,直到将该guest的时间片用完。gVirt在切换之前需要等待guest ring buffer变空,因为多数GPU是不支持抢占的,可能会影响公平性。为了最小化等待开销,gVirt实现了一种粗粒度的流控制机制,通过跟踪命令提交,保证在任何时候,堆积的命令都在一定范围之内。</p>\n<p>Render context switch:当在vGPU之间切换render context时,gVirt会保存和回复内部pipeline state和I/O 寄存器state,还有cache/TLB刷新。保存/回复I/O寄存器状态能够通过对位于render context中的一系列寄存器来进行读/写操作来进行。在gVirt中用于切换context的步骤时:1)保存现在的I/O状态 2)刷新现有的context 3)利用额外的命令来保存现有的context 4)利用额外的命令来恢复新的context 5)恢复新context中的I/O状态。</p>\n<p>gVirt用专用的ring buffer来执行额外的GPU 命令。</p>\n<p>Display management:gVirt重用Dom0 graphics driver来初始化display engine,然后管理display来显示不同的VM frame buffer。当两个vGPU有同样的分辨率的时候,只有frame buffer 位置被切换。</p>\n<h2>3.3 Pass-Through</h2>\n<p>gVirt对frame buffer和command buffer直通来加速来自VM的performance-critical 操作。对global graphics memory space,大小为2GB,本文提出graphics memory resource partitioning 和 address space balloning机制。对于local graphics memory space,每一个具有2GB大小,本文实现了每个虚拟机2GB大小,local graphics memory 只可以由GPU来访问。</p>\n<p>Graphics memory resource partitioning:gVirt在VM之间划分global graphics memory。分离CPU/GPU调度机制需要不同VM之间的global graphics memory能够被CPU和GPU同时访问呢,所以gVirt必须给每一个VM自己的资源。</p>\n<p>从图6中可以看出,global graphics memory的大小对于性能影响是较小的。</p>\n<p>Address space ballooning:本文减少了address space ballooning技术,用以减少地址转换开销。如图7所示,gVirt将划分信息暴露通过gVirt_info MMIO窗口暴露给VM graphics driver。graphics driver将其他VM的内存区域标识为“ballooned”。通过这种设计,guest view of global graphics memory space和host view是一样的。driver使用guest physical address编程的地址,能直接被硬件使用。</p>\n<p>总结:本质上来说,他就是对于每一个VM来说,将其不能够使用的部分标识为“Ballooned“,然后每一个VM都能够对整个global graphics memory space可见,只是限制了其访问范围。这样,VM里面的内存地址和host上的内存地址就一致了,不需要地址转换了。</p>\n<p>Per-VM local graphics memory:gVirt允许每一个VM使用其自己的全部的local graphics memory space。local graphics memory space只对GPU中的render engine可见,任何VM编程的有效的local graphics memory address都能被GPU直接使用。当需要切换render ownership的时候,mediator会在VM之间切换local graphics memory space。</p>\n<h2>3.4 GPU Page Table Virtualization</h2>\n<p>gVirt使用shared shadow global page table 和 per-VM shadow local page table来对GPU页表进行虚拟化。</p>\n<p>Shared shadow global page table:为了实现资源划分和address space ballooning,gVirt为所有VM实现了shared shadow global page table。每一个VM具有其自己的guest global page table,从graphics memory page number翻译到guest memory page number(GPN)。shadow global page table被从graphics memory 怕个 number翻译到Host memory page number(HPN)。shared page table为所有VM维持转换,用以支持对CPU和GPU的并行访问。gVirt实现了一个单一、共享的shadow page table,通过trapping guest PTE updates。如图8所示,MMIO space中的global page table具有512K PTE entries,每一个entry指向4KB系统内存,所以共创造了2GB的global graphics memory space。</p>\n<p>Per-VM Shadow local page tables:为了支持local graphics memory 的pass-through访问呢,gVirt实现了per-VM shadow local page table。local graphics memory只能被render engine访问。</p>\n<h2>3.5 Security</h2>\n<p>Pass-through对于性能很友好,但是必须要满足以下条件,来保证安全隔离。1)一个VM必须被禁止映射未经授权的graphics memory page 2)所有被VM编程的GPU registers 和 command,必须被验证只包含授权的graphics memory address 3)gVirt需要解决拒绝服务攻击,防止一个VM让GPU 拒绝服务</p>\n<h1>总结:</h1>\n<ul>\n<li>需要对GPU驱动进行修改</li>\n<li>估计global graphics memory只有2GB,不能无限分下去</li>\n<li>说白了,不就是command buffer和frame buffer位于global graphics memory 中,然后这两个让虚拟机里面的驱动直接访问,其他组件就得通过trap来保证安全性</li>\n<li>和”Investigating Virtual Passthrough I/O on Commodity Devices“有点不一样啊,人家那个是对与某些特权操作进行trap,然后对其余的都进行passthrough,这个是对组件</li>\n<li>后续的”gScale: Scaling up GPU Virtualization with Dynamic Sharing of Graphics Memory Space不就是关于解决scaliability的</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20240413/",
"title": "转载:A Full GPU Virtualization Solution with Mediated Pass-Through 概述",
"summary": "早年 Intel Xen gVirt 实现,后来迁移到 KVMGT",
"date_modified": "2024-04-13T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>项目地址 <a href=\"https://github.com/Forsworns/GhidrAI\">https://github.com/Forsworns/GhidrAI</a></p>\n<h2>背景介绍</h2>\n<p>如果你做过一些逆向工程,那么一定听说过 IDA Pro,但是很可惜,IDA 太贵了。那有没有可以白嫖的类似工具呢?<a href=\"https://github.com/NationalSecurityAgency/ghidra\">Ghidra</a> 是 GitHub 上的一款开源逆向工程工具,使用 Java 开发,天然具有跨平台的特性。目前它的社区仍然十分活跃。与 IDA 类似,它也提供了接口供用户进行插件开发。</p>\n<p><a href=\"https://github.com/JusticeRage/Gepetto\">Gepetto</a> 是 IDA Pro 的一款网红插件(还参加了 IDA 插件大赛但是很可惜没拿奖),问世于前年 ChatGPT 刚刚出圈的时候,所以受到了大量关注和 star。但是很可惜,我买不起 IDA 也没法获取到 OpenAI 的服务。于是在去年萌生了为 Ghidra 开发一个类似插件的想法,一定是要可以白嫖的那种!</p>\n<p>但我最初走偏了,效仿 NBA 球星杜兰特,选择一条最艰难的道路,想在 Ghidra 里面直接跑 Python 代码,从而直接利用 AI 在 Python 的生态。在这里我花了不少时间折腾 Ghidra 的 Python3 插件 <a href=\"https://github.com/mandiant/Ghidrathon\">Ghidrathon</a> 和 <a href=\"https://github.com/ninia/jep\">Jep</a>。当时也碰到了一些问题,很感谢 Ghidrathon 的维护者最后帮忙解决了。<br>\n但是随着时间的发展,可以白嫖的途径越来越多,比如我最后选用的 <a href=\"https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key\">阿里云 DashScope</a>,模型上新都会有免费体验的时间;开源社区也有很多一键搭建本地 LLM 服务的项目。而且在我们的需求场景下,模型的输入和输出都很长,网络通信的这点耗时和推理用时比起来根本不值一提。于是我意识到:人生苦短,我不能再折腾 Python 了。</p>\n<p>抱着上述想法,终于在这次过年的时候,我开发了插件 <a href=\"https://github.com/Forsworns/GhidrAI\">GhidrAI</a>。由于本人不是专业的 Java 开发人员,如果你发现代码写得很烂,敬请斧正,来者不拒。</p>\n<h2>其实就是项目 README 翻译</h2>\n<p>GhidrAI 是一个 Ghidra 扩展,使用 <a href=\"https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key\">阿里云 LLM 服务</a> 来解释函数的作用,并自动重命名其变量。最重要的是,这些服务是免费的!</p>\n<h3>环境需求</h3>\n<p>目前仅在 Ghidra 10.4 上验证了 GhidrAI。注意:Ghidra 10.4 需要 <a href=\"https://adoptium.net/temurin/releases/\">JDK 17 64-bit</a>。</p>\n<h3>构建 GhidrAI</h3>\n<p>使用以下步骤为您的环境构建 GhidrAI</p>\n<ul>\n<li>从 <a href=\"https://github.com/NationalSecurityAgency/ghidra/blob/stable/GhidraDocs/InstallationGuide.html#InstallationNotes\">此处</a> 安装 Ghidra。</li>\n<li>从 <a href=\"https://gradle.org/releases\">此处</a> 安装 Gradle。<br>\n从 <a href=\"https://github.com/Forsworns/GhidrAI\">此处</a> 下载最新的 GhidrAI 发布版。</li>\n</ul>\n<p>在 GhidrAI 源目录中运行以下命令:<br>\n注意:<strong>您可以选择设置名为 <code>GHIDRA_INSTALL_DIR</code> 的环境变量,而不是指定 <code>-PGHIDRA_INSTALL_DIR</code>。</strong></p>\n<pre><code class=\"language-bash\">gradle -PGHIDRA_INSTALL_DIR=<Ghidra 安装的绝对路径>\n</code></pre>\n<p>如果成功,您将在 GhidrAI 源目录中找到一个名为 <code>dist</code> 的新目录,其中包含您的 GhidrAI 扩展(.zip)。如果您遇到任何问题,请打开一个新问题。(运行中的问题请提供 <code> ~/.ghidra/.ghidra_${VERSION}/application.log</code> 中的日志文件。)</p>\n<h3>安装 GhidrAI</h3>\n<p>使用以下步骤将您的 GhidrAI 扩展安装到 Ghidra:</p>\n<ul>\n<li>注意: <strong>对于 <a href=\"https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key\">阿里云 DashScope</a>,您必须使用 DASHSCOPE_API_KEY 环境变量。</strong></li>\n<li>启动 Ghidra</li>\n<li>转至 <code>File > Install Extensions...</code></li>\n<li>点击绿色 + 按钮</li>\n<li>导航到您之前构建的 GhidrAI 扩展(.zip)</li>\n<li>点击 <code>确定</code></li>\n<li>重新启动 Ghidra</li>\n</ul>\n<p>或</p>\n<p>您可以直接将 GhidrAI 扩展(.zip)解压缩到 <code><Ghidra 安装的绝对路径>\\Ghidra\\Extensions</code>,Ghidra 将在下次启动时提示您配置 GhidrAI。</p>\n<h3>使用 GhidrAI</h3>\n<p><strong>选中</strong> 函数并点击鼠标右键,可以找到 GhidrAI 对应的选项。图中右边的紫色注释显然是用 AI 生成的。</p>\n<p><img src=\"action.png\" alt=\"点鼠标右键\"></p>\n<p>点击 <code>Tools > GhidrAI</code>,能够找到配置项的对话框,配置项的更多细节可以去读项目中的说明 <a href=\"https://github.com/Forsworns/GhidrAI/blob/main/data/README.md\">说明</a>.</p>\n<p><img src=\"config.png\" alt=\"配置项对话框\"></p>\n<p>GhidrAI 也提供了一个自动的分析器,你可以通过 <code>Analysis > Auto Analyze ...</code> 或 <code>Analysis > One Shot</code> 找到它。</p>\n<p><img src=\"analyzers.png\" alt=\"Automatic analyzer\"></p>\n<p><img src=\"oneshot.png\" alt=\"One shot analysis\"></p>\n<h2>画个大饼</h2>\n<p>我对这个项目还有一些想法,比如:</p>\n<ul>\n<li>首先自然是支持更多的白嫖/付费 LLM 服务途径,去薅各个云厂商的羊毛。</li>\n<li>更一般的支持 <a href=\"https://github.com/songquanpeng/one-api\">One-API</a> 的接口,便捷地请求本地服务。</li>\n<li>支持多个模型生成的结果进行对比,或允许交互选择多个潜在结果,类似现在各类代码补全工具的推荐功能。</li>\n<li>现在的实现其实是非常粗暴的,每次会话并不会携带上下文。希望通过携带更长的上下文,拿到更精确的分析结果,但是相应得响应会更慢?</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20240220/",
"title": "GhidrAI - Equip Ghidra with LLM",
"summary": "目前仅支持外部用户使用阿里云 DashScope 服务,内部用户使用 Aone 接口,寻求帮助中 :)",
"date_modified": "2024-02-20T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>ZLUDA once lets you run unmodified CUDA applications on Intel devices. Several days ago, it introduces an explosive update, and now it only supports AMD.</p>\n<p>For me, the most exciting part would be the more complete <a href=\"https://github.com/vosen/ZLUDA/blob/master/zluda_dark_api/src/lib.rs\">CUDA dark api</a>.<br>\nGreat advances have been reached compared with three years ago, when Vosen announce stopping working on this project. More inner CUDA calls are reversed and revealed to the public.</p>\n<p>Other related projects I'm tracking:</p>\n<ul>\n<li><a href=\"https://github.com/gvirtus/GVirtuS\">https://github.com/gvirtus/GVirtuS</a> (After three years, it updates recently, too!)</li>\n<li><a href=\"https://github.com/google/gvisor/issues/14\">https://github.com/google/gvisor/issues/14</a> (Track the GPU support proposal for gVisor.)</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20240215/",
"title": "Marvellous updates on ZLUDA",
"summary": "And you, Vosen, you're the real hero!",
"date_modified": "2024-02-15T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>How to pin user pages?</h1>\n<h2>Why mlock is bad</h2>\n<p>At first simply call <code>mlock</code> in the user space. But it only promise the <a href=\"https://www.man7.org/linux/man-pages/man2/mlock.2.html\">page will not be swapped out</a>. But it may be migrated to another physical backend.</p>\n<p>Refer to this <a href=\"https://stackoverflow.com/questions/15275423/are-mlock-ed-pages-static-or-can-they-be-moved-in-physical-ram\">answer on stackoverflow</a>:</p>\n<blockquote>\n<p>No. Pages that have been mlocked are managed using the kernel's unevictable LRU list. As the name suggests (and mlock() guarantees) these pages cannot be evicted from RAM. However, the pages can be migrated from one physical page frame to another. Here is an excerpt from Unevictable LRU Infrastructure (formatting added for clarity):<br>\nMIGRATING MLOCKED PAGES<br>\nA page that is being migrated has been isolated from the LRU lists and is held locked across unmapping of the page, updating the page's address space entry and copying the contents and state, until the page table entry has been replaced with an entry that refers to the new page. Linux supports migration of mlocked pages and other unevictable pages. This involves simply moving the PG_mlocked and PG_unevictable states from the old page to the new page.</p>\n</blockquote>\n<h2>use gup/pup API</h2>\n<p>We could not do this in the user space, We should use <code>get_user_pages</code>/<code>pin_user_pages</code> in the kernel.</p>\n<p>Refer to the <a href=\"https://www.kernel.org/doc/html/latest/core-api/pin_user_pages.html\">linux kernel doc</a>.</p>\n<p>Other related resources:</p>\n<ul>\n<li><a href=\"https://lwn.net/Articles/807108/\">https://lwn.net/Articles/807108/</a></li>\n<li><a href=\"https://elixir.bootlin.com/linux/v6.7.2/source/mm/gup.c#L3346\">https://elixir.bootlin.com/linux/v6.7.2/source/mm/gup.c#L3346</a></li>\n<li><a href=\"https://github.com/NVIDIA/open-gpu-kernel-modules/blob/bb2dac1f20a06f78e028c4acdc4df1c7908dd91c/kernel-open/common/inc/nv-mm.h#L49\">https://github.com/NVIDIA/open-gpu-kernel-modules/blob/bb2dac1f20a06f78e028c4acdc4df1c7908dd91c/kernel-open/common/inc/nv-mm.h#L49</a></li>\n<li><a href=\"https://zhuanlan.zhihu.com/p/579444153\">https://zhuanlan.zhihu.com/p/579444153</a></li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20240128/",
"title": "How to pin user pages?",
"summary": "Do not use mlock! It does not promise memory pinning.",
"date_modified": "2024-01-28T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>GTC 2022 How CUDA Programming Works</h1>\n<p><a href=\"https://www.nvidia.com/en-us/on-demand/session/gtcspring22-s41487/\">GTC 2022 大会上关于 GPU 架构的演讲</a> 摘录,来自英伟达 CUDA Architect,Stephen Jones。Stephen 在 PPT 中给他的演讲换了个主题名字:Why CUDA Programming Is The Way It Is,听完醍醐灌顶,感觉这个标题是很贴切了。</p>\n<h2>最宝贵的资源:内存带宽</h2>\n<p>首先 Stephen 以安培架构为例,如下图,两个重要的数据,FP64 峰值为 9.7TFLOPS,Memory Bandwith 峰值是 1555 GB/s。</p>\n<p><img src=\"image-20230601231943556.png\" alt=\"image-20230601231943556\"></p>\n<p>现在算一笔账:假设 108 个 SM 都以 1410 MHZ跑满了,SM 在每个时钟周期能加载 64 个字节,那么峰值应该是他们乘起来,9750 GB/s。但是内存的带宽仅有 1555 GB/s。</p>\n<p><img src=\"image-20230601232325647.png\" alt=\"image-20230601232325647\"></p>\n<p>再算一笔账:1555GB/s 的带宽跑满了,如果都是加载 64 比特的,也就是 8 个字节,那应该是 194 GFLOPS。所以说,FLOPS 不是关键限制,GPU 仍然可能面临“显存墙”。</p>\n<p>从底层来看,内存单元依赖一个电容元件,要么带电为 1,要么不带电为 0。</p>\n<p><img src=\"image-20230601214841309.png\" alt=\"image-20230601214841309\"></p>\n<p>DRAM 芯片由数百万个这样的单元组成一个大的矩阵。数据使用行和列来索引。例如在读取时,一行中所有的单元都被激活,状态被复制到感应放大器上。问题是这样电容会放电,数据就会被破坏。然后根据列索引读取放大器中的数据。显而易见,一个优化就是一次性读取整行数据。但是在换一行读取的时候,需要写回数据,因为前一次的读取导致电容放电了,所以换行读取的开销也很大,切换行的成本是切换列的三倍。下图中的 page,也是指这样的一行。</p>\n<p><img src=\"image-20230601220316989.png\" alt=\"image-20230601220316989\"></p>\n<p>从 DRAM 加载和读取值的速率取决于电容器充电和放电所需的时间。读的时候放电,写回的时候充电。下图是一个概念性的公式。同时注意到,一行是 1KB。</p>\n<p><img src=\"image-20230601220816648.png\" alt=\"image-20230601220816648\"></p>\n<p>在实验中,也表现也基本符合理论,读取时候的间隔 stride 增大,读取 8 个字节的速度逐渐降低。</p>\n<p><img src=\"image-20230601221033404.png\" alt=\"image-20230601221033404\"></p>\n<p>因此在写 CUDA 程序的时候同样要注意内存的布局和数据访问的方式。比如在 CPU 上大家都知道,右侧写法会比左侧满很多</p>\n<p><img src=\"image-20230601221704839.png\" alt=\"image-20230601221704839\"></p>\n<p>CUDA 中的基本执行单元是 thread block,块中的线程一定是同时处于工作状态的。每个线程运行着相同的代码,但是有自己的寄存器。SIMT 建立在独立的 PC 寄存器上,同时硬件会在 block 启动时自动填充 blockIdx 和 threadIdx,用于区分不同线程,线程控制是隐式的,每个线程有自己的状态。</p>\n<p><img src=\"image-20230601222141531.png\" alt=\"image-20230601222141531\"></p>\n<p>block 被分成了一个个 32 个线程组成的 wrap。如下图,如果每个线程读取 8 字节的数据,那么一个 wrap 会读取连续的 256 字节(因为 threadIdx 是连续的)。</p>\n<p><img src=\"image-20230601222907593.png\" alt=\"image-20230601222907593\"></p>\n<p>GPU 上的单个 SM 可以管理 64 个 wrap,也就是 2048 个线程。但是如图所示,它实际上只有四个独立的部分,也就是说,同一时间最多同时运行四个 wrap。其他的是在一个 queue 里面等待调度。</p>\n<p><img src=\"image-20230601223108656.png\" alt=\"image-20230601223108656\"></p>\n<p>继续刚刚的讨论,那么一个 SM 上实际上跑了四个 wrap,每个 wrap 假设还是加载上面的 256 字节,那么就刚好是 1KB。还记得 1KB 吗?刚好是一个页面(内存单元中的一行)的大小。因此,尽管看上去是在进行随机内存读,但是由于是四个 wrap,共计 128 个线程在同时执行,恰好就读取了内存单元的连续的一行。对应下图中右边折线图左上角的情况。这一定程度上解释了为什么一个 wrap 是 32 个线程,为什么一个 SM 上可以同时运行 4 个 wrap。Stephen 也给出了建议:不要让你的 thread block 的大小超过 128。NV 开源的 CUTLASS 的就有相关优化。</p>\n<p><img src=\"image-20230601223652707.png\" alt=\"image-20230601223652707\"></p>\n<h2>占用率</h2>\n<p>接下来讨论占用率 occupancy,以 A100 为例,108 个 SM,221184 个线程,资源是有限的,就像一个嵌入式系统,要清楚你能够利用到的硬件资源。一旦 grid 被加载,里面的 block 就会被放置到可用的 SM 上。这一步将会尽可能地让 block 广泛分布。</p>\n<p><img src=\"image-20230601220037549.png\" alt=\"image-20230601220037549\"></p>\n<p>图中每个 SM 上放置了两个 block,实际上取决于 block 的大小,单个 SM 中上限不超过 32 个,尽可能占满 SM。当一个 block 工作完成后,它会退出,硬件会安排另一个 block 到这个 SM 上,继续尽力占满 SM。但是填满 SM 到底是什么意思?</p>\n<p><img src=\"image-20230601224024378.png\" alt=\"image-20230601224024378\"></p>\n<p>这需要回到 SM 的硬件资源上,最重要的是顶部的四个:最大线程数,最大块数、每个 SM 的寄存器数量、每个 SM 的共享内存总量。另外,block 会尽可能均匀地分布到不同 SM 上,是因为这里的每个 SM 的数据加载带宽限制(由于要支持多个 SM 同时工作,所以每个 SM 的带宽是只占总带宽的一部分)。</p>\n<p><img src=\"image-20230601224551600.png\" alt=\"image-20230601224551600\"></p>\n<p>还是以一段程序为例,这里涉及到上面提到的 SM 中的几个资源,这里要注意寄存器是一个 per-thread 的资源需求,所以要乘以 block 内的 thread 数量。</p>\n<p><img src=\"image-20230601225236123.png\" alt=\"image-20230601225236123\"></p>\n<p>下面讨论一个 block 是依据什么被放到 SM 上的,注意一个 block 永远不会横跨两个 SM。如果没有足够的资源,SM 会放弃运行该 block。block 是保障并行性的最大元素,块内可以通过共享内存进行数据交换和线程同步,块内的线程总是同时处于工作状态。</p>\n<p>如下图,右侧给出了一个虚拟的 block 的资源需求。我们希望尽可能多的 block 工作在 SM 上,但是由于共享内存不足,只能放三个 block。假设共享内存需求从 64KB 变成了32 KB,那么只能放 4 个,限制条件又变成了寄存器。但是,只从 block 的执行效率上来看,我们快了 33%(如果我们忽略共享内存调整,对执行速度的潜在的影响)。</p>\n<p><img src=\"image-20230601225845534.png\" alt=\"image-20230601225845534\"></p>\n<p>现在考虑另一个 grid,它不需要共享内存,每个线程所需的寄存器数量也比较少。事实上这是一个典型的排序、数据拷贝等用途的 kernel。那么 GPU 会尝试将它放到之前的空隙里面。</p>\n<p><img src=\"image-20230601230341366.png\" alt=\"image-20230601230341366\"></p>\n<p>这里也蕴含了另一个重要的点,独立的任务可以同时运行在 GPU 上,比如下图中的不同 Stream 间,可能有计算和数据拷贝的交错,也就是上图讨论的任务调度的技巧,也称为 oversubscription。</p>\n<p><img src=\"image-20230601230616947.png\" alt=\"image-20230601230616947\"></p>\n<p><img src=\"image-20230601230736876.png\" alt=\"image-20230601230736876\"></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20230601/",
"title": "GTC 2022 How CUDA Programming Works",
"summary": "解释了一些硬件参数配置的来源",
"date_modified": "2023-06-01T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>GTC 2023 Scaling Deep Learning Training: Fast Inter-GPU Communication with NCCL</h1>\n<p><a href=\"https://www.nvidia.com/en-us/on-demand/session/gtcspring23-s51111/\">GTC 2023 大会上关于 NCCL 的演讲</a> 摘录,来自英伟达 Principal Engineer,Sylvain Jeaugey。</p>\n<p>类似于 MPI 在并行计算中的地位,NCCL 是多卡集合通信的核心框架,NV 在 2016 年开始实现了这套框架,文档见<a href=\"https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/index.html\">NVIDIA Collective Communication Library (NCCL) Documentation</a>。</p>\n<p>为什么需要 NCCL?这和模型的并行训练方法有关。最常见的并行训练方法是数据并行,在本地计算完梯度后,进行一次 NCCL 的 All Reduce 操作,让每张 GPU 上的都能同步到全局的梯度信息。缺陷是 GPU 数量增加后,batch size 需要增加,过大的 batch size 可能导致模型精度下降。</p>\n<p><img src=\"image-20230322221607084.png\" alt=\"image-20230322221607084\"></p>\n<p>第二种是模型并行,把一个大模型的各个部分放置到不同的设备上,好处是可以避免数据并行中 batch size 的增大。但是坏处是这时需要用 NCCL 的 P2P Send/Recv 操作在各个部分进行参数的更新,不同部分的计算效率、参数量不同,可能存在空隙,利用率不足。</p>\n<p><img src=\"image-20230322221748851.png\" alt=\"image-20230322221748851\"></p>\n<p>或者张量并行,将两个大型张量的计算拆分开,例如下图,使用一个 NCCL AllGather 操作收集各个部分的计算结果。</p>\n<p><img src=\"image-20230322222444334.png\" alt=\"image-20230322222444334\"></p>\n<p>现在最火的莫过于大型语言模型(LLM),将上面三者结合,是一个并行训练的典型场景:</p>\n<p><img src=\"image-20230322223211767.png\" alt=\"image-20230322223211767\"></p>\n<p>NCCL 有着丰富的特性,这里的重点是它单机内部的数据传输都是直接写成了核函数、自动拓扑分析(可以指定XML,意味着可以在虚拟机内部搞些骚操作)、Ring/Tree 结构下的集合通信实现、RDMA 支持。</p>\n<p><img src=\"image-20230322223716783.png\" alt=\"image-20230322223716783\"></p>\n<p>接着 Sylvain 仔细讲了 Ring 和 Tree 结构,以 All Reduce 为例。Ring 很朴素,直接把所有节点穿成环,好处是简单易懂、负载均衡,坏处是线性增加的延迟,因为要把数据切分成 chunk 每个节点过一遍。Tree 则是很自然的二分思想,在树上进行 reduce 操作,延迟自然就低了。NCCL 中会建立两棵树,叶子进行计算,非叶子进行集合通信。建立两棵树是为了保证一个节点在一棵树中是叶子节点,在另一棵树中则是非叶节点;同时可以保证一个节点在两棵树中必定是共有两个父节点和两个子节点。另外就是更加高级和高性能的 Collnet,主打网内计算(In Network Computing),在传输的同时进行 reduce 操作。</p>\n<p><img src=\"image-20230322225016280.png\" alt=\"image-20230322225016280\"></p>\n<p>接下来就是 NCCL 的大致工作流程:</p>\n<p><img src=\"image-20230322230710828.png\" alt=\"image-20230322230710828\"></p>\n<p>最后讲了讲今年他们搞了什么新特性,宣传一波(求求别更新了,老代码还没看懂</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20230322/",
"title": "GTC 2023 Scaling Deep Learning Training:Fast Inter-GPU Communication with NCCL",
"summary": "NCCL 的源代码好难懂……",
"date_modified": "2023-03-22T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>游泳学习笔记</h1>\n<p>做了十几年老年蛙选手,最近得空游了两周泳,记录下看<a href=\"https://space.bilibili.com/7283282\">梦觉老师教学视频</a>的学习心得。</p>\n<p>目前感觉水性提升了,自己的老年蛙熟练了,还学会了踩水、自由泳、蝶泳。</p>\n<p>= = 感觉游泳不能减肥啊,不然梦觉老师怎么越来越胖 :dog:</p>\n<h2>踩水&蛙泳</h2>\n<p>腰背挺直站立状态,蛙泳式蹬腿,浮上来换气,基本就是初级踩水了。中级需要一直浮在水上,自己感觉蹬起来频率要快点,很累,不过可以演变成倒蛙和抬头蛙。</p>\n<p>有一个视频看到,梦觉老师躺在泳池底,感觉很神奇。今天终于悟了,在水里一直吐气,自然就会下沉了(救生员见打!)。</p>\n<p>没有看梦觉老师的蛙泳视频,但是朋友告我热血蛙换完气后,头要朝下扎借助浮力,有一种波浪感。他的灵魂绘画</p>\n<h2>自由泳</h2>\n<p>原来自由泳是指除了其他几个常见泳姿之外的任意泳姿,只是因为爬泳比较快,所以比赛里大家都这么游,久而久之自爬泳就成了大家认知中的自由泳(奇怪的知识增加了.jpg)。</p>\n<p>打腿是鞭腿,鞭意味着要有速度,用大腿去带动小腿。可以理解成如果是坐着的状态,大腿会有一个下压的动作。</p>\n<p>转体是重中之重,转体头顶也要向前不要乱晃。转体时用肩膀带动手臂去划水,转体不是动腰,换气是在转体时顺带做的。</p>\n<p>划水和打腿的配合问题,可以用“江南四大才子”步伐来训练。</p>\n<p>如果别肩的话可以用<a href=\"https://www.bilibili.com/video/BV1bx411L7sA?p=26\">螃蟹游</a>的方式纠正(螃蟹游泳姿很丑但是很有意思)。</p>\n<h2>蝶泳</h2>\n<p>梦觉讲自由泳的时候顺便讲了蝶泳腿,并腿鞭腿,腰部也要参与。</p>\n<h2>仰泳</h2>\n<p>躺着进水时记得鼻子吐气防止倒灌。</p>\n<p>打腿时脚绷紧,其他类似自由泳腿。</p>\n<p>不过游了这么多年,我还不会在水上漂着 QAQ 所以目前学习进度几乎为零</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20230209/",
"title": "游泳学习笔记",
"summary": "梦觉老师永远滴神!",
"date_modified": "2023-02-09T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>我为什么喜欢 ff14</h1>\n<p>前段时间看到了一个视频 <a href=\"https://www.bilibili.com/video/BV1kj411M7NE/?spm_id_from=333.337.search-card.all.click&vd_source=38187a50caef599f13f7d437962ca324\">2023年 我为什么喜欢并向你推荐《最终幻想14》</a>,想了一下自己喜欢这款游戏的原因,就不是在做推荐了(毕竟都 2023 年了),而是给自己“浪费时间”打这个游戏做一个辩护哈哈。</p>\n<p>我对游戏很少有耐心玩下去,所以完整玩过的游戏也不多,因为比较菜所以也对操作性打击感不怎么关心,下面是其他方面的一些想法。</p>\n<h2>剧情</h2>\n<p>作为一个运营了十多年的游戏,它的剧情很有史诗感,特别是 6.0 版本已经完美收束了之前的主线,它支线任务也很好地补全了世界观。游戏中的剧情大多是围绕玩家展开的,玩家一路成为了拯救世界的大英雄,队伍里的 NPC 都是你的好伙伴,就连反派都拿你当挚友。</p>\n<p>不过老实说我对 5.0 之前的剧情无感,裹脚布一般的剧情、加上我曾经 a 了一段时间,甚至让我在前期对 NPC 都很陌生。但是作为解密篇的 5.0 的剧情还是很棒的,6.0 更是让我直呼“ff14 为什么是神”。奈何本人没文化,一句 xx 行天下。关于 6.0 的剧情分析,读到过一篇写得很好的文章,这个作者简直把我感受到的全说出来了。</p>\n<p><a href=\"https://zhuanlan.zhihu.com/p/458459679\">海德林的答案:存在主义上帝充满爱意的赌局</a></p>\n<p><a href=\"./repost\">原谅我在本站偷偷存了一份</a> :dog:</p>\n<h2>优秀的演出和配乐</h2>\n<p>首先吹一波 6.0 剧情高潮 <a href=\"https://www.bilibili.com/video/BV15F411i7ER/\">生死答问-Answers</a> 的演出,刚刚打到这里的时候,我去搜了这个视频,三语版的过场 CG 挨个看了一遍,泣不成声。我真得很好奇,十年前他们就想好了这段演出吗,十年前写下的歌词的时候是在为今天做准备吗?</p>\n<p>我 ff14 优秀的演出和配乐,简直是完美的视听盛宴。除了上面提到的《Answers》前后十年的两版动画让我潸然泪下,5.0 中爱梅的宣言配上《Shadowbringers》也曾让我不禁战栗。6.0 在天外天垓时玩家一步步(物理意义)回顾来路时的《Close in the Distance》,是对十年剧情的最好总结,同样让我热泪盈眶。当然我的播放列表里还存了很多 ff14 的配乐,它们总会把我拉回到这个奇幻的世界。</p>\n<h2>制作组</h2>\n<p>制作组很有趣,我感觉它的一些开发者对自己的工作内容有着真切的热爱。比如一些奇奇怪怪的彩蛋:<a href=\"https://www.bilibili.com/video/BV19K4y1x72p\">吉田藏在2.0版本中最神秘的彩蛋</a>、<a href=\"https://www.bilibili.com/video/BV1oV4y1P73u/\">在地图里擅自做了个心形小岛被吉田发现之后僵住的STAFF</a>。吉田还曾好奇玩家有没有发现他们埋藏的一个彩蛋。</p>\n<p>另外,吉田拯救 ff14 的故事本身就是一个传奇,可以参考芒果冰的一期<a href=\"https://www.bilibili.com/video/BV1HW411S7fu/\">网游史</a>。简而言之就是 ff14 这个项目最初就是依托答辩,吉田接手后采用一个巧妙的灭世剧情为它画上句号,然后重构了整个游戏,又经过多年打磨才有了今天。芒果冰的网游史中讲了太多运营失败的网游案例了。化腐朽为神奇的吉田却是例外,我非常敬佩他。</p>\n<p>然后就是吉田很经典的一段采访了</p>\n<blockquote>\n<p>我不想要玩家必须每天登陆游戏,我想避免强制让玩家游玩,因为大家都很忙,所以不是每个人都能每天登陆,即使像我这样的MMO死忠也是如此。所以我觉得每个大版本回来一口气玩通也是可以接受的,每次资料片推出,里面都有足够匹敌一个单机RPG的内容,玩家们会回来一起体验新的“最终幻想”,这是我们一直贯彻的设计理念。</p>\n</blockquote>\n<p>所以我很喜欢它,在 ff14 中我可以找到自己喜欢的玩法,即使很久不上号也没有什么影响,不会有日常周常、复杂的社交系统来拴住你(虽然运营也在搞通行证活动,但是对玩家影响并不大,比如我对自己的拉拉肥就很满意,不需要洗澡水不参加也无所谓)。</p>\n<h2>社区氛围</h2>\n<p>让人两眼一黑的二创视频、制作精良的考据视频、内容详细的维基百科、方便好用的辅助工具。</p>\n<p>良好的游戏氛围:散排玩起来像单机就是最好的社交模式,路人玩家清理背包送宠物(不是)。</p>\n<p>当然也有一些不太好的方面,比如贴吧把游戏里缺失的攻击性拉满了,以及前段时间沸沸扬扬的攻略组事件,但是我不逛贴吧不打高难,雨我无瓜。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20230201/",
"title": "我为什么喜欢 ff14",
"summary": "ff14 为什么是神",
"date_modified": "2023-02-01T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>FreeFlow 笔记</h1>\n<p>FreeFlow 是 NSDI 18 上非常经典的 RDMA 虚拟化的文章。</p>\n<h2>论文阅读</h2>\n<p>本文的动机是RDMA与硬件和主机的高度绑定与容器化带来的隔离性和可迁移性是背道而驰的。因此文章提出了一个 software switch,类似 OvS 在传统TCP/IP 中的角色,在控制面实现隔离和可迁移、在数据面做QoS和流量统计等。</p>\n<h3>概览</h3>\n<p>原文第三章。</p>\n<p><img src=\"./image-20230125094041055.png\" alt=\"\"></p>\n<p>FreeFlow 拦截了应用于硬件之间的通讯。FreeFlow 只允许网卡读写受控的 Shadow Memory,然后负责将它拷贝给应用,从而实现隔离。容器内的应用内存和 FreeFlow 中的 Shadow Memory 可以是同一块物理内存,从而实现零拷贝降低开销。</p>\n<p>FreeFlow 进行虚拟化的部分是 IB Verbs API,在虚拟网卡中创建虚拟的 SQ 和 RQ,与物理网卡上的 QP 和 CQ 交互。</p>\n<p><strong>设计中的挑战有两点:透明化(应用不感知不修改)、不影响性能,分别在第四和第五章的设计中解决。</strong></p>\n<p><img src=\"./image-20230125122658009.png\" alt=\"\"></p>\n<p>FreeFlow 的设计如上图灰色部分所示,包含</p>\n<ul>\n<li>FreeFlow network library (FFL),模仿标准RDMA库,从而实现对应用的透明性。</li>\n<li>FreeFlow software router (FFR),在每个主机上部署一个实例,代理所有容器的 RDMA 通讯。它和各个容器共享内存,并对他们进行隔离。FFL负责同步容器的私有内存和 FFR 中的共享内存。FFR还负责数据面的 QoS 策略、与 FFO 一起管理 IP 分配。</li>\n<li>FreeFlow network orchestrator (FFO),负责控制面如 IP 管理、Acess Control,也用来管理全局内存映射。</li>\n</ul>\n<h3>透明地支持 RDMA 操作</h3>\n<p>原文第四章,这章讲 FreeFlow 虚拟化的实现方法,需要覆盖 RDMA 通讯的各个流程和操作。</p>\n<p>RDMA 操作包含单向的 READ/WRITE 和双向的 SEND/RECV。困难点在于单向的操作中 RDMA 网卡可以静默地修改 FFR 中的内存或文件描述符,除非 FFR 持续轮询它们的状态,因此很难在物理网卡和容器内的虚拟网卡之间保持同步。这里的解决思路是容器本身是进程,FFL 和 FFR 可以共享内存和文件描述符。但是在透明化方面又会引入新的问题,容器内的应用不能直接在 IPC 共享内存中分配,所以需要透明地实现共享。</p>\n<h4>建立连接</h4>\n<p>两侧的 RDMA 端点需要建连才能通信,在双方网卡中创建 QP,注册内存缓冲区。下图讲解了通过 Verbs 建连的过程,左边是原生的应用触发的各个调用,晦涩区域则是陷入 FreeFlow 执行的操作,会在两侧的 FFR 之间建连。</p>\n<p><img src=\"image-20230126172442978.png\" alt=\"\"></p>\n<p>图中的步骤具体而言:</p>\n<ol>\n<li>应用遍历网卡寻找支持 RDMA Verbs 的设备。FFL 会拦截这些调用然后返回容器中的虚拟 RDMA 网卡。</li>\n<li>应用在它拿到的虚拟网卡中创建 QP 和 CQ,FFR 会在物理网卡中创建对应的 QP' 和 CQ'。在 FFR 创建完队列后,FFL 会将 QP-ID 和其它队列的元信息转发给应用。</li>\n<li>应用为 QP 注册内存(mem)。FFR 会在 IPC 共享内存空间内相应地分配一块内存(s-mem),大小与 mem 相同,同时会把 s-mem 注册给物理网卡的 QP'。FFR 会返回它用来创建 s-mem 的 ID(一个主机上的 IPC 内存中的唯一的名字)。有了这个 ID,FFL 可以将 s-mem 映射到它自己的虚拟内存空间。</li>\n<li>应用查询本地 QP 的地址(也即 RDMA 中的 GID)。该地址信息将会共享给另一侧,来将本地的 QP 和远端的 QP 配对起来。最后,FFR 会将 QP' 的真实的 GID 返回给容器内的应用。</li>\n<li>应用和远端交换 GID 和 QP-ID。应用可以通过诸如 TCP/IP 或 RMDA-CM 等信道完成这一步的操作。FreeFlow 不感知这一步的操作。</li>\n<li>应用使用接受方的 GID,将本地 QP 和远端容器的 QP 配对起来。FFL 会将该 GID 转发给 FFR,FFR 把该 GID 配对给 QP'。</li>\n<li>应用修改本地的 QP 到准备好发送或接收的状态。FFR 会对应修改 QP' 的状态。</li>\n</ol>\n<p>由于额外的 FFL 和 FFR 的调用,FreeFlow 会增加建连的延迟,但是这些开销是一次性的,连接也可以复用。(而且本地函数调用的开销和建连过程中的网络通信的延迟相比,并不是很显著。)</p>\n<h4>双向的操作</h4>\n<p>发送或接收方都需要执行两个步骤,第一步是使用 QP 来发送或接收数据,第二步是使用 CQ 去得到完成提醒。上面步骤图中的第 8-9 步描述了这个过程。这两步的具体内容如下:</p>\n<ol start=\"8\">\n<li>\n<p>应用触发了 SEND 调用,提供了指向 mem 的指针。FFL 首先将数据从 mem 拷贝到 s-mem,然后 FFR 触发他自己的 SEND 调用将 s-mem 发送给远端的 FFR。我们通过应用后续章节会提到的零拷贝的技术避免从 mem 到 s-mem 的内存拷贝操作。注意远端路由此时已经触发了一个 RECV 调用。</p>\n</li>\n<li>\n<p>应用 poll CQ 或等待之前操作完成的提醒。FFR 也会在对应的 QP' 和 CQ' 上进行 polling 或等待。</p>\n</li>\n</ol>\n<p>后续的SEND 操作是继续重复这两步;RECV 操作类似,只是 s-mem 和 mem 的数据接收过程是反过来的。</p>\n<p>可以看出来在 FreeFlow 中,FFL 和 FFR 相对容器内的应用都是透明的。</p>\n<h4>单向的操作</h4>\n<p>在单向操作中,客户端不仅需要服务端的 GID,还需要远端内存缓冲区的地址,和获取该内存的秘钥。这个交换信息的步骤在上述流程的第 5 步进行,在第 8 步中 FreeFlow 可以获得它们。</p>\n<p><img src=\"image-20230126172509842.png\" alt=\"\"></p>\n<p>但是,和双向的操作相比,单向的操作更难去透明地实现,主要是面临下面两个问题。</p>\n<p>首先,目标地址 mem 是在远端容器的虚拟内存中。然而,本地的 FFR 并不知道在对端它对应的 s-mem。例如,在图 6-(a) 中,当发送方尝试在 mem-2 中写入 mem-1 中的数据,它会在第 3) 步失败,因为接收端的 FFR 无法获取目标内存地址 mem-2。</p>\n<p>为了解决这个问题,FreeFlow 在 FFO 中构建了一个键值存储,FFR 可以据此了解到应用虚拟内存空间中的 mem 指针到 FFR 虚拟内存空间中的 s-mem 指针的映射。当应用注册内存到它的虚拟网卡时,更新这个映射表会增加上面建连步骤的第3步的延迟。然而,数据面的性能不会收到影响,因为 FFR 可以在本地缓存这些映射。</p>\n<p>其次,即使我们知道了远端的内存映射,WRITE 和 READ 操作也可以在不通知远端 CPU 的情况下直接修改或拷贝远端内存,所以 FFR 没法及时更新数据。例如在图 6-(b) 中,发送者找到了 s-mem-2 的正确地址,发送了数据给它。然而,在 s-mem-2 中数据可以被获取到的时候,并不会有相关的提醒让接收端的 FFR 知道什么时候该把数据从 s-mem-2 拷贝到 mem-2。一个解决该问题的方法是持续地同步 s-mem-2 和 mem-2。但是这会消耗 CPU 和内存带宽。</p>\n<p>为了解决这个问题,作者在 FreeFlow 中设计了一个基于零拷贝的机制。大致上,是让 mem 和 s-mem 映射到相同的物理内存上,因此 FFR 不需要进行任何拷贝,容器内的应用就可以得到数据。图 6-(c) 解释了该设计。通过避免内存拷贝,FreeFlow 的性能也就提高了。</p>\n<p>这里的关键是让应用程序直接分配和使用共享内存与 FFR 进行数据传输。对此,FreeFlow 提供了两个选项:</p>\n<ul>\n<li>提供新的 API 来分配共享内存:FreeFlow 创建了两个新的 Verbs 函数,ibv _malloc 和 ibv_free,让应用程序将内存创建和删除委托给 FreeFlow。这允许 FFL 直接在共享内存区域中分配这些缓冲区(与FFR共享),从而避免拷贝。该选项的缺点是需要修改应用程序代码,尽管修改应该只会发生在创建数据缓冲区的那几行代码。</li>\n<li>将应用程序的虚拟内存地址重新映射到共享内存:当应用程序将虚拟内存地址 va 作为数据缓冲区注册到私有内存时,(例如前面的建连步骤 3),FFL 会将 va 映射到的物理内存释放,并从 FFR 中分配一块共享的物理内存映射到 va。在 Linux 中,此操作仅在 va 是内存页的起始地址时有效。为了让应用程序总是在页的起始地址分配内存,FFL 拦截了 C 语言中的 malloc 之类的调用,并使其始终返回页对齐的内存地址。虽然此选项可以在不修改应用程序代码的情况下实现零拷贝,但它会强制应用程序中的所有内存分配进行页对齐,这会导致主机上的内存使用效率降低(造成更多的内存碎片)。</li>\n</ul>\n<p>在实践中,FreeFlow 建议采用第一个选项,因为它更简洁高效。但是,由于许多 RDMA 应用程序已经实现了页对齐来获得更好的性能(如RDMA-Spark),我们可以在不拦截 malloc 的情况下直接使用第二个选项。</p>\n<h4>基于事件的操作</h4>\n<p>从 CQ 中获取信息有两种方式。一种是让应用周期地 poll CQ,检查是否有完成的操作。另一种是事件驱动的,由应用创建一个事件管道,然后将 CQ 加入管道。当有操作完成时,可以通过管道中的文件描述符触发相应的事件。</p>\n<p>在 FreeFlow 中,由于原始文件描述符是由物理网卡创建的,FFR需要将文件描述符传递给 FFL,否则后者无法感知与文件操作符相关的事件。由于 FFL 和 FFR本质上是共享同一个内核的两个进程,FreeFlow 借助进程间文件描述符传递的方法,实现了 FFR 传递给 FFL 的事件管道。</p>\n<h3>FFL 和 FFR 之间的信道</h3>\n<p>原文第五章,这章主要是是 FreeFlow 中的性能优化,讲如何实现接近于原生的性能。</p>\n<p>由于 FreeFlow 通过 FFL 拦截每个 Verbs 调用,并提供 FFR 转发给物理网卡。因此,需要在 FFL 和 FFR 之间建立高效的信道,来提升 RDMA 性能、减少系统资源消耗。在本章中,我们提出了两种信道设计,分别专注于优化 RDMA 性能或资源消耗(意味着会分别增大资源消耗或降低性能),需要根据应用需求做取舍。</p>\n<h4>通过文件描述符转发 Verbs</h4>\n<p><img src=\"image-20230126172542745.png\" alt=\"\"></p>\n<p><strong>使用 RPC 在 FFL 和 FFR 之间传递 Verbs</strong>:FFL 将 API 名称和参数传递给 FFR,FFR 适当修改参数后执行 API 并将 API 调用的结果返回给 FFL。然而,由于 Verbs 调用的参数的数据结构十分复杂,该方法在 FreeFlow 中并不适用。如图 7-(a) 所示,一个典型的函数调用(如 ibv_post_send)的参数(qp,wr)和返回值(bad_wr)都是指向复杂数据结构的指针。FFL 和 FFR 是两个不同的进程,所以 FFL 中的指针在 FFR 中无效。</p>\n<p>也许有人会想到“深拷贝”,即溯源到复杂的参数和返回值的数据结构,并在 FFL 和 FFR 之间传递完整的数据对象。然而,这种方法有两个严重的缺点。首先,Verbs 中的数据结构层级非常深(嵌套了多层的指针),这样的深拷贝会损害性能。其次,FreeFlow 不可能预知并重载用户自定义的数据结构的深拷贝。</p>\n<p>为了解决这个问题,我们利用了当前 Verbs 库的结构。如图 7-(b) 所示,Verbs 库由三层组成。顶层是最复杂的层,如上所述那样难以处理。但是,当涉及与网卡文件描述符进行通信的中间层时,Verbs 库必须准备一个足够简单(无指针)的数据结构,使网卡硬件可以处理。</p>\n<p>因此,我们转发对网卡文件描述符的请求,而不是转发 Verbs 的原始函数调用。如图 7-(c) 所示,我们用一个一端连接 FFR 的 Unix 套接字的文件描述符来替换容器中的网卡文件描述符。这样,FFR 可以获取应用程序发送的命令和提供的参数。FFR 会将在容器中对虚拟队列的操作映射到在物理网卡中对实际队列的相应操作。然后,它将来自物理网卡的回复转换为虚拟网卡对虚拟队列的回复,并通过 Unix 套接字将新的回复返回给 FFL。FFL 中的网卡驱动程序通信层将正常处理该回复,但不会知道 Unix 套接字文件描述符背后的操作。</p>\n<p>虽然这种基于 Unix 套接字的方法只消耗很少的 CPU 资源,但套接字通信的固有延迟,该方法会产生额外的延迟。实验表明,在商用服务器中,Unix 套接字(以及有信号量保护的共享内存)的往返时间很容易超过 5μs。因此,图 7-(c) 中的 Unix 套接字通信通道可能成为延迟敏感应用的性能瓶颈。</p>\n<p>对于需要低延迟通信的应用,我们将在下一节中介绍快速路径的设计,它通过用 CPU 资源来换取通信延迟的优化。</p>\n<h4>FFL 与 FFR 之间的快速路径</h4>\n<p><img src=\"image-20230126172604900.png\" alt=\"\"></p>\n<p>为了加速 FFR 和 FFL 之间的通信,我们设计了一个与上节的 Unix 套接字并行的快速路径。如图 8 所示,FFL 和 FFR 共同拥有一块专用的共享内存。使用快速路径,FFR 在 CPU 的一个核上自旋,并持续检查是否有来自 FFL 的新请求被写入共享内存。一旦检测到请求,FFR 将立即执行它,同时 FFL 开始在 CPU 的一个核上自旋以检查是否响应已经就绪。在读取到响应后,FFL 将停止它的 CPU 自旋。</p>\n<p>在实验中,快速路径可以显著减少延迟。但代价就是用于读取请求和响应的自旋所花费的 CPU 开销。为了限制快速路径带来的 CPU 开销,FreeFlow 提出了两个设计:</p>\n<ul>\n<li>对于所有在同一主机上的连接了 FFL 的快速路径通道,FFR 都在同一个 CPU 核上自旋。</li>\n</ul>\n<p>-快速路径仅用于数据通路上的非阻塞功能,使得 FFL 上等待响应的 CPU 的自旋时间会很短(几微秒)。</p>\n<p>总的来说,快速路径在每个主机上仅占据一个 CPU 内核,来显著缩短消息传递的延迟(原文第 8.1.2 节实验)。 另外,如果 FFO 知道主机上没有对延迟敏感的应用(根据运行的容器镜像),它可以禁用快速路径和 CPU 自旋。</p>\n<h3>实现</h3>\n<p>原文第六章。</p>\n<p>修改了 libibverbs、librdmacm、libmlx4 实现 FFL,大概 4000 行 C 代码。用 C++ 写了一个 2000 左右的 FFR。FFO 部分用的 ZooKeeper。</p>\n<p>控制面策略示例,为了性能限制 QP 数量;数据面策略示例,rate limiter</p>\n<h3>讨论</h3>\n<p>原文第七章。</p>\n<p>FreeFlow 会带来额外的 CPU 开销,使用一个核心管理 FFL 和 FFR 之间的控制信息,支持低延迟的 IPC 通道。</p>\n<p>安全性问题:容器内应用是否可以通过 IPC 通道访问到别的容器的地址空间。FFR 为每对 QP 创建专用的共享内存,容器只能访问到自己的 QP 的共享内存。memory key 可能泄露导致通讯被窃听,但是这是单向 RDMA operation 本身的问题。</p>\n<p>FFR 不区分 FreeFlow 和普通的 RDMA 设备,因此,可以和普通的设备兼容。</p>\n<p>FreeFlow 不支持热迁移,但是可以离线迁移,重启后 IP 没有变,对端仍然可以访问到它重建连接。</p>\n<p>实验中 FreeFlow 部署在裸金属上,但是同样适用于基于 SR-IOV 获取 RDMA 网卡的 VM。</p>\n<p>拥塞控制依赖于物理网卡。</p>\n<h3>相关工作</h3>\n<p>Mellonox 在使用 MACVLAN 拆分物理设备成虚拟设备,依赖 VLAN 分发数据。但是它同样有可迁移的问题,迁移 IP 需要更新硬件中的 VLAN 路由。</p>\n<p>也有一些基于可编程硬件如 Smart NIC 和 FPGA 的方案,但是 FreeFlow 可以直接用于现有商用设备。</p>\n<p>HyV 和 VMM-bypass I/O 的设计和 FreeFlow 类似,但是有性能问题。VMware 的半虚拟化 vRDMA 设备专用与它自己的 hypervisor 和 VM,和这里容器场景不同。</p>\n<h2>源码阅读</h2>\n<p>本来想了解下 shadow memory 的设计来着,看论文描述就是直接用了共享内存</p>\n<p>源码阅读,咕咕咕</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20230126/",
"title": "FreeFlow 笔记",
"summary": "Shadow Memory 设计和实现",
"date_modified": "2023-01-26T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>The Serverless Computing Survey 笔记</h1>\n<p>读了陈全老师组的《The Serverless Computing Survey: A Technical Primer for Design Architecture》,这篇文章 2022 年发在 eurosys。</p>\n<p>这篇综述从四个角度介绍了 Serverless Computing:Virtualization, Encapsule, System Orchestration, and System Coordination。</p>\n<h2>概述</h2>\n<p>Serverless Computing 的一个狭隘定义是也就是 FaaS,函数计算即服务,一个更为普适的定义是 FaaS+BaaS(Backend-as-a-Service)。FaaS 的模型负责函数的隔离和触发,BaaS 则是 FaaS 背后整个基础架构的抽象。作者认为,这样的系统应该具有如下特性:</p>\n<ul>\n<li>Auto Scaling</li>\n<li>Flexible Scheduling</li>\n<li>Event-Driven</li>\n<li>Transparent Development</li>\n<li>Pay-as-you-go</li>\n</ul>\n<p>并在下图中,提出了四层架构:Virtualization layer、Encapsule layer、System Orchestration layer、System Coordination layer。</p>\n<p><img src=\"image-20230125144449864.png\" alt=\"\"></p>\n<h2>Virtualization layer</h2>\n<p>目前常见的沙箱机制可以分为四类:传统虚拟机、容器、安全容器、Unikernel,一些关键属性如下表所示。</p>\n<p><img src=\"image-20230125144756828.png\" alt=\"\"></p>\n<p>这里由于考虑的是 FaaS 场景,重点关注了启动时间和隔离性,最终给出了下面这个图。</p>\n<p><img src=\"image-20230125145012009.png\" alt=\"\"></p>\n<p>容器由于它出色的启动时间和通用性,在过去支配了无服务计算,但是以 Kata Container 为代表的安全容器也逐渐在特定领域的无服务计算。</p>\n<h2>Encapsule Layer</h2>\n<p>冷启动可能发生在某个函数无法获取到一个正在运行的容器或有突发的大量启动时,为此需要一些预热的技术,其中代表性的工作如下(C/R 代表 checkpoint支持,如用于可重复构建的 CRIU 工具)。</p>\n<p><img src=\"image-20230125152226766.png\" alt=\"\"></p>\n<p>具体而言,有两类预热方法:one-to-one 和 one-to-all,前者一般是池化特化的函数实例,后者则是通用的模板实例需要在被调度后进一步特化。前者面临的挑战主要是预热数量和周期难以预测、预热多种实例的内存开销;后者的挑战主要是通用模板镜像往往体积较大、预先导入了运行大量无关的库、用户隐私泄露风险。</p>\n<h2>Orchestration Layer</h2>\n<p>下图描述了负载均衡器和资源管理组件在该层的应用,可以分为三层,resource-level(R)、instance-level(I)、application-level(A)。</p>\n<p><img src=\"image-20230125153259218.png\" alt=\"\"></p>\n<p>系统调度层最近的一些工作。</p>\n<p><img src=\"image-20230125153539991.png\" alt=\"\"></p>\n<h3>Resource Level</h3>\n<p>CPU 和内存是无服务计算的基本调度资源,调度的一个原则是不要过量提供给用户这些资源。为了遵守这个原则,常见的方法是根据历史数据推算。近年的一些工作将 SLA 计入到资源调度的考量,来保障稳定性和可靠性。</p>\n<h3>Instance Level</h3>\n<p>主流的方法是用负载均衡,一般是两类:基于哈希的或者是基于多目标优化的。</p>\n<p>但是负载均衡策略因为会在实例间贡献资源,可能会导致互相干扰:导致性能下降或违背 QoS 目标。</p>\n<h3>Application Level</h3>\n<p>在应用层面上,负载均衡可以分为两类:spread 策略和 bin-pack 策略。前者会将一个应用的函数实例分散到所有的物理节点上;后者会尝试将单个应用的函数实例调度到同一个节点上。二者的区别就在于数据一致性/本地性。</p>\n<p>应用层面上有两种触发模式和两种工作流程模型,如下图所示。</p>\n<p><img src=\"image-20230125155101713.png\" alt=\"\"></p>\n<h3>安全隐患</h3>\n<ul>\n<li>微服务可能有超额的数据输入导致应用违反了实例的内存限制。</li>\n<li>恶意攻击,如 DDoS(Distributed Denial-of-Service)引发微服务的雪崩、DoW(Denial-of-Wallet)在 pay-as-you-go 模式下增加用户成本甚至耗尽资金中断服务。</li>\n<li>调用链路安全保障,需要确保调用方的合法性。</li>\n</ul>\n<h2>Coordination Layer 的 BaaS 组件</h2>\n<p>上图展示了六个关键的组件/服务:</p>\n<ul>\n<li>\n<p>存储</p>\n<p>单次无服务触发的过程中,有三个阶段需要数据库:鉴权、函数执行、日志。</p>\n<p>这里主要的问题是 I/O 容易成为服务性能的瓶颈,需要设计具有弹性的、数据本地化的存储服务。</p>\n</li>\n<li>\n<p>队列</p>\n<p>在系统的不同组件间传递消息,如 Apache Kafka。</p>\n<p>基于队列的机制可能会降低系统的性能和可用性。</p>\n</li>\n<li>\n<p>API 网关</p>\n<p>无服务计算中,实例按需启动,不绑定静态地址。</p>\n</li>\n<li>\n<p>触发器</p>\n<p>触发器和绑定的规则一起组成了可检测事件的探针,可以避免硬编码获取其他服务。</p>\n<p>除了触发事件驱动的函数,触发器也可以提供一个声明式的方式将数据和代码连接起来(例如存储服务)。</p>\n<p>最常见的四类触发器是:HTTP 请求、队列、计时器、事件。</p>\n<p>事件驱动的系统的一个代表性项目是 Triggerflow,它会在工作流的每个阶段设置一个触发器。</p>\n</li>\n<li>\n<p>数据缓存</p>\n<p>为了保障服务的稳定性,避免负载激增、达到并发上限,常见的方法是使用多级缓存。</p>\n<ul>\n<li>镜像缓存:按需加载镜像、镜像分层共享。</li>\n<li>状态缓存:让无服务应用具有状态,可结合活动数据库。</li>\n<li>checkpoint 缓存,让函数具备容错能力。除了基于 C/R 的方法,还有基于日志的容错方式。</li>\n</ul>\n</li>\n<li>\n<p>DevOps 工具</p>\n<ul>\n<li>\n<p>CI (Continuous Integration)</p>\n</li>\n<li>\n<p>CD (Continuous Delivery):滚动升级、红黑(蓝绿)部署、金丝雀部署</p>\n</li>\n<li>\n<p>CM (Continuous Monitoring)</p>\n</li>\n</ul>\n</li>\n</ul>\n<h2>性能评估</h2>\n<p>原文第六章</p>\n<p>不同运行时、语言、内存限制下的冷启动延迟如下图所示,因此常常配置 256 MB 的内存限制,因为边际效用最高。</p>\n<p><img src=\"image-20230126101440855.png\" alt=\"\"></p>\n<p>作者也补充了 一些其他观测:如 LLC (Last-Level Cache) 方面,增大缓存不会带来冷启动性能的显著提升,只有在小于 2 MB 的情况下会有很大的性能损失。 在容器化的场景下,页表管理会给 TLB 带来沉重的压力,有些项目如 2020 年 ISCA 上的 BabelFish 会尝试在容器间共享地址转换。</p>\n<p>四个云厂商对比。其中,CCI 是 Concurrent Invocation。</p>\n<p><img src=\"image-20230126101631240.png\" alt=\"\"></p>\n<h2>其他方面的限制和挑战</h2>\n<p>原文第七章</p>\n<h3>Encapsule Layer 的无服务化</h3>\n<p>无服务计算的一个显著特征是短暂的生命周期,并不保证每次请求会被同一个实例处理,它本身的无状态特征限制了它的应用场景。然而,通过一些外部的存储为它提供状态又会有诸如隐私风险等问题。</p>\n<h3>Orchestration Layer 的内存碎片</h3>\n<p>在多租场景下,可能有大量容器同时运行,并且他们会面临冷启动的问题。在这种情况下,海量的 sidecar 可能会占用大量内存导致无服务计算的容器难以实现密布,降低了资源的利用率。同时会有实例内部由于过量分配产生的内存碎片,和实例间由于调度产生的内存碎片。</p>\n<h3>Coordination Layer 的 API 和基准程序的云厂商锁定</h3>\n<p>可移植性差,容易和云厂商绑定,例如各个厂商独立的 BaaS 接口。</p>\n<p>没有真正复杂的跨平台的开源基准程序。</p>\n<h2>无服务计算的研究方向</h2>\n<p>原文第八章</p>\n<h3>应用层面的优化</h3>\n<p>应用层优化包含两部分工作流支持增强和工作流调度。</p>\n<p>工作流支持增强指函数实例之间需要能相互联通。这需要支持新的存储方式来为函数间通信赋能;需要更好的同步机制,在允许并行触发的同时保障服务质量。</p>\n<p>新的调度策略应该考虑到上面提到的函数实例间的联通性,例如 caller-callee 关系、数据本地性。这段特别提到了 FaaS 现在是把数据发送到代码侧,而不是代码发送到数据侧,记得 Google 去年在 MLSys 发的 Pathways 里面好像也提过训练这个问题。</p>\n<h3>冷启动优化方案的鲁棒性</h3>\n<p>有一些通过预测来优化冷启动的方法,但是为每个函数都收集足够的数据做预测不现实,需要更加鲁棒的方法。</p>\n<h3>加速器的无服务化</h3>\n<p>现在的 FaaS 大多没有包含专用的加速器,例如 GPU、FPGA,即使有也是以独占的形式售卖,和 FaaS 本身的理念背道而驰。因为本身这方面的虚拟化,特别是极致的伸缩没有成熟方案。这导致了两个问题:加速器售卖和使用都不够方便,缺乏伸缩性;无服务计算能够支持的应用场景受限。作者认为有三个可行的研究方向:</p>\n<ul>\n<li>调度策略,加速器作为一种资源参与调度。</li>\n<li>虚拟化方法,怎么提供伸缩性。其实这方面有一些很好的工作,如 gVirtus 虚拟化 GPU、FreeFlow 虚拟化 RDMA, 这些方案都在拦截服务调用构建虚拟设备做代理转发。</li>\n<li>批处理。加速器对 I/O 敏感,需要在批处理提升 I/O 效率和批处理带来的延迟之间做取舍。</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20230125/",
"title": "The Serverless Computing Survey 笔记",
"summary": "论文阅读笔记",
"date_modified": "2023-01-25T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>博客升级改造</h1>\n<p>最开始是2019年建立的博客,当时用 Vuepress 搞的,但是没两天发现 Vuepress 就已经 deprecated 了!</p>\n<p>但是因为显而易见的原因:懒得学 Typescript 和 Vue3,就一直没改造博客。</p>\n<p><a href=\"/assets/embarrassed.jpg\"></a></p>\n<p>前两天看了下 VitePress 已经到了 1.0 了,又看到了 <a href=\"https://github.com/clark-cui/vitepress-blog-zaun\">vitepress-blog-zaun</a> 这个项目,干脆就抄了这个老哥的代码。</p>\n<p>但是这个老哥把 markdown-it 插件的配置写错位置了,markdown-it-katex 一直用不了,看了半天才发现问题。</p>\n<p>这次折腾的过程中还碰到一个很有趣但是又很折磨的一个<a href=\"https://github.com/microsoft/WSL/issues/4197\">WSL 的问题</a>,感觉还是单另写一篇记录下比较合适。</p>\n<p>这次算是一个大改造,纪念一下具体做了些什么。</p>\n<ul>\n<li>\n<p>语言和框架更新。(不过说实话,可维护性和安全性提升?不存在的,个人博客 = = 接下来的几年估计都加不了几行 Typescript,单纯赶个时髦哈哈。</p>\n</li>\n<li>\n<p>移除了构建过程中的一些脚本,把这些功能加到了自定义主题里。说起来确实很奇怪,当时Gitalk评论、文章列表、首页文章我为什么要放到构建命令里去添加……</p>\n</li>\n<li>\n<p>升级到 Google Analytics 4(不然还发现不了 Universal GA 要不能用了 = =。</p>\n</li>\n<li>\n<p>VitePress 不支持插件了,要自己写,把之前 VuePress 里用的一些插件迁移了过来。当然看到了一些方法是写 Vite 插件给 VitePress 用。</p>\n</li>\n<li>\n<p>markdown-it-katex 社区版似乎没人维护了,大家都在自行 fork 胡乱发版,自己拷贝进来维护自用吧……</p>\n</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20230121/",
"title": "博客升级改造",
"summary": "2023年了,博客终于换到了 Typescript 和 Vue3 了",
"date_modified": "2023-01-21T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Sentinel-Rust Middleware Supports</h1>\n<p>Currently <a href=\"https://github.com/sentinel-group/sentinel-rust\">Sentinel-Rust</a> supports following RPC/Web frameworks, and provides thorough <a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/middleware\">examples</a>.</p>\n<h2>Tonic</h2>\n<p><a href=\"https://crates.io/crates/tonic\">Tonic</a> is A rust implementation of <a href=\"https://grpc.io/\">gRPC</a>, a high performance, open source, general RPC framework that puts mobile and HTTP/2 first.</p>\n<p>The are two kinds of middlewares in Tonic.</p>\n<p>- <a href=\"https://docs.rs/tonic/latest/tonic/service/interceptor/trait.Interceptor.html\"><code>tonic::service::interceptor::Interceptor</code></a></p>\n<p>- <a href=\"https://docs.rs/tower/latest/tower/trait.Service.html\"><code>tower::Service</code></a></p>\n<p>We have implemented both of them, see <a href=\"https://crates.io/crates/sentinel-tower\">sentinel-tower</a> and <a href=\"https://crates.io/crates/sentinel-tonic\">sentinel-tonic</a> on <a href=\"http://crates.io\">crates.io</a>.</p>\n<p>Here is a <a href=\"https://forsworns.github.io/zh/blogs/20221108/\">post</a> related to its implementation.</p>\n<h2>Volo</h2>\n<p><a href=\"https://crates.io/crates/volo\">Volo</a> is a high-performance and strong-extensibility Rust RPC framework that helps developers build microservices.</p>\n<p>Different from the Tower in Tonic, Volo uses the <a href=\"https://github.com/cloudwego/motore\">Motore</a> for service abstraction.</p>\n<p>For more information, see <a href=\"https://crates.io/crates/sentinel-motore\">sentinel-motore</a> on <a href=\"http://crates.io\">crates.io</a>.</p>\n<p>Here is a <a href=\"https://forsworns.github.io/zh/blogs/20221108/\">post</a> related to its implementation.</p>\n<h2>Actix Web</h2>\n<p><a href=\"https://crates.io/crates/actix-web\">Actix Web</a> is a powerful, pragmatic, and extremely fast web framework for Rust</p>\n<p>In general, a middleware in Actix Web is a type that implements the <a href=\"https://docs.rs/actix-web/4/actix_web/dev/trait.Service.html\">Service trait</a> and <a href=\"https://docs.rs/actix-web/4/actix_web/dev/trait.Transform.html\">Transform trait</a>.</p>\n<p>For more information, see <a href=\"https://crates.io/crates/sentinel-actix\">sentinel-actix</a> on <a href=\"http://crates.io\">crates.io</a>.</p>\n<p>Here is a <a href=\"https://forsworns.github.io/zh/blogs/20221108/\">post</a> related to routers and handlers in the Actix-Web.</p>\n<h2>Rocket</h2>\n<p><a href=\"https://crates.io/crates/rocket\">Rocket</a> is a web framework for Rust that makes it simple to write fast, secure web applications without sacrificing flexibility, usability, or type safety.</p>\n<p>There are two ways to implement a Sentinel middleware in Rocket.</p>\n<p>Intuitively, we can implement the <a href=\"https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html\"><code>Fairing</code></a> trait, just as the common <code>Service</code> traits in other frameworks.</p>\n<p>However, as documented in the Rocket guide,</p>\n<blockquote>\n<p>Rocket’s fairings are a lot like middleware from other frameworks, but they bear a few key distinctions:</p>\n<ul>\n<li>Fairings <strong>cannot</strong> terminate or respond to an incoming request directly.</li>\n<li>Fairings <strong>cannot</strong> inject arbitrary, non-request data into a request.</li>\n<li>Fairings <em>can</em> prevent an application from launching.</li>\n<li>Fairings <em>can</em> inspect and modify the application's configuration.</li>\n</ul>\n</blockquote>\n<p>Since it cannot terminate or respond to the request directly, the implemented <code>SentinelFairing</code> simply rewrites the URI in the Request to a given route. It can be configured via its own methods or <a href=\"https://rocket.rs/v0.5-rc/guide/state/#managed-state\">managed state</a> of <code>SentinelConfig</code>.</p>\n<p>In fact, Rocket <a href=\"https://rocket.rs/v0.5-rc/guide/fairings/#overview\">suggests</a> using request guards, instead of Fairing in this case,</p>\n<blockquote>\n<p>As a general rule of thumb, only <em>globally applicable</em> actions should be effected through fairings. You should *<strong>not*</strong> use a fairing to implement authentication or authorization (preferring to use a <a href=\"https://rocket.rs/v0.5-rc/guide/requests/#request-guards\">request guard</a> instead) <em>unless</em> the authentication or authorization applies to all or the overwhelming majority of the application. On the other hand, you <em>should</em> use a fairing to record timing and usage statistics or to enforce global security policies.</p>\n</blockquote>\n<p>So we follow this <a href=\"https://rocket.rs/v0.5-rc/guide/requests/#request-guards\">suggestion</a></p>\n<blockquote>\n<p>Request guards appear as inputs to handlers. An arbitrary number of request guards can appear as arguments in a route handler. Rocket will automatically invoke the <a href=\"https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html\"><code>FromRequest</code></a> implementation for request guards before calling the handler. Rocket only dispatches requests to a handler when all of its guards pass.</p>\n</blockquote>\n<p>and implemented a <code>SentinelGuard</code>. It can be configured via the <a href=\"https://rocket.rs/v0.5-rc/guide/state/#managed-state\">managed state</a> of <code>SentinelConfig</code>,</p>\n<p>For more information, see <a href=\"https://crates.io/crates/sentinel-rocket\">sentinel-rocket</a> on <a href=\"http://crates.io\">crates.io</a>.</p>\n<h2>Axum</h2>\n<p><a href=\"\">Axum</a> is a web application framework that focuses on ergonomics and modularity.</p>\n<blockquote>\n<p>In particular the last point is what sets <code>axum</code> apart from other frameworks. <code>axum</code> doesn't have its own middleware system but instead uses <a href=\"https://docs.rs/tower/latest/tower/trait.Service.html\"><code>tower::Service</code></a>. This means <code>axum</code> gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using <a href=\"https://crates.io/crates/hyper\"><code>hyper</code></a> or <a href=\"https://crates.io/crates/tonic\"><code>tonic</code></a>.</p>\n</blockquote>\n<p>Therefore, we can reuse the middleware in <a href=\"https://crates.io/crates/sentinel-tower\">sentinel-tower</a>. For more information, visit our <a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/middleware/axum\">example for Axum</a>.</p>\n<h2>One More Thing</h2>\n<p>Currently, <a href=\"https://github.com/sentinel-group/sentinel-rust/wiki/Usage#via-dynamic-datasource\">dynamic datasources</a> for sentinel are implemented directly in <a href=\"https://crates.io/crates/sentinel-core\">sentinel-core</a> as customized features. Maybe similar to these middlewares, splitting datasources into individual crates or a single crate with customized features is better...</p>\n<h2>Sentinel-Rust Resources</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">Tutorial</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API Doc</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">Example Codes</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20230101/",
"title": "Sentinel-Rust Middleware Supports",
"summary": "Read examples, plz.",
"date_modified": "2023-01-01T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>The design of actix-web handler</h1>\n<p>Recently I just finished my master thesis and was working on the <a href=\"https://github.com/sentinel-group/sentinel-rust/\">sentinel</a> middleware for <a href=\"https://github.com/actix/actix-web\">actix-web</a>. It is a powerful, pragmatic, and extremely fast web framework for Rust.</p>\n<p>An interesting API of actix-web attracts me: <a href=\"https://docs.rs/actix-web/4.2.1/actix_web/struct.App.html#method.service\"><code>App::service</code></a>, which dispatches routes to respective handlers. Here is an example</p>\n<pre><code class=\"language-rust\">use actix_web::{web, http, App};\nuse serde::Deserialize;\n\n#[derive(Deserialize)]\nstruct Info {\n username: String,\n}\n\n/// extract path info using serde\nasync fn index1(info: web::Path<Info>) -> String {\n format!("Welcome {}!", info.username)\n}\n/// use multiple extractors for one handler function\nasync fn index2(\n path: web::Path<Info>,\n query: web::Query<HashMap<String, String>>,\n body: web::Json<Info>\n) -> String {\n format!("Welcome {}!", path.username)\n}\n\nlet app = App::new()\n\t.service(web::resource("/{username}/index1.html") // <- define path parameters\n .route(web::get().to(index1))) // <- register handler\n .service(web::resource("/{username}/index2.html")\n .route(web::get().to(index2)));\n</code></pre>\n<center>ELEGANT! VERY ELEGANT!</center>\n<div align=center><img src=\"https://forsworns.github.io//assets/Anya.jpg\" style=\"zoom:20%;\" /></div>\n<p>Personally, I think this API is very elegant. But how is it implemented? It looks as if "variadic" arguments are derived magically in the generic. But we all know that the rust does not support this feature in its generics.</p>\n<p>Let's first read the signature of <a href=\"https://docs.rs/actix-web/4.2.1/actix_web/struct.Route.html\">actix_web::Route</a>, which registers handlers in the above example.</p>\n<pre><code class=\"language-rust\">pub fn to<F, Args>(self, handler: F) -> Self\nwhere\n F: Handler<Args>,\n Args: FromRequest + 'static,\n F::Output: Responder + 'static,\n</code></pre>\n<p>Well, it is the <a href=\"https://docs.rs/actix-web/4.2.1/actix_web/trait.Handler.html\"><code>actix_web::Handler</code></a> and <a href=\"https://docs.rs/actix-web/4.2.1/actix_web/trait.FromRequest.html\"><code>actix_web::FromRequest</code></a> that provides this flexibility in deed.</p>\n<p>:joy: Guess what? The developers of <code>actix-web</code> had already foreseen that we would be curious about its handler. And the following is transcribed from its doc.</p>\n<blockquote>\n<h2><a href=\"https://docs.rs/actix-web/latest/actix_web/trait.Handler.html#how-do-handlers-receive-variable-numbers-of-arguments\">How Do Handlers Receive Variable Numbers Of Arguments</a></h2>\n<p>Rest assured there is no macro magic here; it’s just traits.</p>\n<p>The first thing to note is that <a href=\"https://docs.rs/actix-web/latest/actix_web/trait.FromRequest.html\"><code>FromRequest</code></a> is implemented for tuples (up to 12 in length).</p>\n<p>Secondly, the <code>Handler</code> trait is implemented for functions (up to an arity of 12) in a way that aligns their parameter positions with a corresponding tuple of types (becoming the <code>Args</code> type parameter for this trait).</p>\n<p>Thanks to Rust’s type system, Actix Web can infer the function parameter types. During the extraction step, the parameter types are described as a tuple type, <a href=\"https://docs.rs/actix-web/latest/actix_web/trait.FromRequest.html#tymethod.from_request\"><code>from_request</code></a> is run on that tuple, and the <code>Handler::call</code> implementation for that particular function arity destructures the tuple into its component types and calls your handler function with them.</p>\n<p>In pseudo-code the process looks something like this:</p>\n<pre><code class=\"language-rust\">async fn my_handler(body: String, state: web::Data<MyState>) -> impl Responder {\n ...\n }\t\n// the function params above described as a tuple, names do not matter, only position\ntype InferredMyHandlerArgs = (String, web::Data<MyState>);\n\n// create tuple of arguments to be passed to handler\nlet args = InferredMyHandlerArgs::from_request(&request, &payload).await;\n\n// call handler with argument tuple\nlet response = Handler::call(&my_handler, args).await;\n\n// which is effectively...\n\nlet (body, state) = args;\nlet response = my_handler(body, state).await;\n</code></pre>\n<p>This is the source code for the 2-parameter implementation of <code>Handler</code> to help illustrate the bounds of the handler call after argument extraction:</p>\n<pre><code class=\"language-rust\">impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func\nwhere\n Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,\n Fut: Future,\n{\n type Output = Fut::Output;\n type Future = Fut;\n\n fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future {\n (self)(arg1, arg2)\n }\n}\n</code></pre>\n</blockquote>\n<p>That is, the parameters are packed into a tuple to make the API neat, while inside the <code>actix-web</code>, the tuple is parsed by <code>FromRequest</code> and passed to the handler caller <code>Handler::call</code>, which unpacked the tuple and pass arguments to the real handler. The <code>Handler</code> trait is implemented on tuples with different sizes by the macro <code>factory_tuple</code>, and it's hided in the doc.</p>\n<pre><code class=\"language-rust\">// actix-web/src/handler.rs\nmacro_rules! factory_tuple ({ $($param:ident)* } => {\n impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func\n where\n Func: Fn($($param),*) -> Fut + Clone + 'static,\n Fut: Future,\n {\n type Output = Fut::Output;\n type Future = Fut;\n\n #[inline]\n #[allow(non_snake_case)]\n fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {\n (self)($($param,)*)\n }\n }\n});\n</code></pre>\n<p>Similarly , a familiar tower in the page of <a href=\"https://docs.rs/actix-web/latest/actix_web/trait.FromRequest.html\"><code>FromRequest</code></a> :satisfied:</p>\n<p><img src=\"./from_request.png\" alt=\"\"></p>\n<p>And it is based on this macro</p>\n<pre><code class=\"language-rust\">// actix-web/src/extract.rs\nmacro_rules! tuple_from_req {\n ($fut: ident; $($T: ident),*) => {\n /// FromRequest implementation for tuple\n #[allow(unused_parens)]\n impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+)\n {\n type Error = Error;\n type Future = $fut<$($T),+>;\n\n fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {\n $fut {\n $(\n $T: ExtractFuture::Future {\n fut: $T::from_request(req, payload)\n },\n )+\n }\n }\n }\n\n pin_project! {\n pub struct $fut<$($T: FromRequest),+> {\n $(\n #[pin]\n $T: ExtractFuture<$T::Future, $T>,\n )+\n }\n }\n\n impl<$($T: FromRequest),+> Future for $fut<$($T),+>\n {\n type Output = Result<($($T,)+), Error>;\n\n fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {\n let mut this = self.project();\n\n let mut ready = true;\n $(\n match this.$T.as_mut().project() {\n ExtractProj::Future { fut } => match fut.poll(cx) {\n Poll::Ready(Ok(output)) => {\n let _ = this.$T.as_mut().project_replace(ExtractFuture::Done { output });\n },\n Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())),\n Poll::Pending => ready = false,\n },\n ExtractProj::Done { .. } => {},\n ExtractProj::Empty => unreachable!("FromRequest polled after finished"),\n }\n )+\n\n if ready {\n Poll::Ready(Ok(\n ($(\n match this.$T.project_replace(ExtractFuture::Empty) {\n ExtractReplaceProj::Done { output } => output,\n _ => unreachable!("FromRequest polled after finished"),\n },\n )+)\n ))\n } else {\n Poll::Pending\n }\n }\n }\n };\n}\n\npin_project! {\n #[project = ExtractProj]\n #[project_replace = ExtractReplaceProj]\n enum ExtractFuture<Fut, Res> {\n Future {\n #[pin]\n fut: Fut\n },\n Done {\n output: Res,\n },\n Empty\n }\n}\n</code></pre>\n<h2>Sentinel-Rust Resources</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">Tutorial</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API Doc</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">Example Codes</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20221228/",
"title": "The design of actix-web handler",
"summary": "They had already foreseen this...",
"date_modified": "2022-12-28T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Mimic Generic Specialization in Rust</h1>\n<p>Last weekend I tried to write the <a href=\"https://github.com/sentinel-group/sentinel-rust/\">sentinel</a> middleware for <a href=\"https://github.com/hyperium/tonic/\">tonic</a> and <a href=\"https://github.com/cloudwego/volo/\">volo</a>, and struggled fighting against rustc again.</p>\n<h2>Background</h2>\n<p>The tonic supports two ways to implement middleware.</p>\n<ul>\n<li><a href=\"https://docs.rs/tower/latest/tower/trait.Service.html\">tower service</a></li>\n<li><a href=\"https://docs.rs/tonic/latest/tonic/service/interceptor/index.html\">tonic interceptor</a></li>\n</ul>\n<p>Different from tonic, volo utilizes <a href=\"https://docs.rs/motore/latest/motore/service/trait.Service.html\">motore</a> as the service abstraction.</p>\n<h2>How the problem arose?</h2>\n<p>I first implement the <code>tonic::Servie</code> . Intuitively, I wrote</p>\n<pre><code class=\"language-rust\"> impl<S, B> Service<http::Request<B>> for SentinelService<S, http::Request<B>, B>\n where\n S: Service<http::Request<B>>,\n {}\n\nimpl<S, R> Service<R> for SentinelService<S, R>\n where\n S: Service<R>,\n {}\n</code></pre>\n<p>So what I expected is that: For tonic, whose request is in fact <code>http::Request<http_body::combinators::UnsyncBoxBody<bytes::Bytes, tonic::Status>></code> , will be substituted in the first generic, instead of the second. In the first case, we can call methods on <code>http::Request</code> to provide a default resource extractor for <a href=\"https://github.com/sentinel-group/sentinel-rust/\">sentinel</a>, while in the second one, the custom resource extractor is necessary.</p>\n<p>I thought that there is a similar feature in rust as <strong>SFINAE</strong> in C++. The best specialization of the generic would be chosen.</p>\n<p>However, rustc reminded me that two implementation of trait <code>Service</code> was contradicted with each other. I realized that there was no <code>SFINAE</code> in rust!</p>\n<h2>Mimic the SFINAE!</h2>\n<p>From <a href=\"https://stackoverflow.com/questions/65131776/pick-preferred-implementation-on-conflicting-trait-implementation-using-negativ\">Pick preferred implementation on conflicting trait implementation (using negative bounds) - Stack Overflow</a>, I learnt that we could use the unstable <a href=\"https://doc.rust-lang.org/beta/unstable-book/language-features/negative-impls.html\">negative_impls</a> and <a href=\"https://doc.rust-lang.org/nightly/unstable-book/language-features/auto-traits.html\">auto_traits</a> feature, to do this magic</p>\n<pre><code class=\"language-rust\">#![cfg_attr(feature = "nightly", feature(auto_traits, negative_impls))]\ntrait WithoutDefaultExtractor {}\nimpl<B> !WithoutDefaultExtractor for http::Request<B> {}\n\nimpl<S, B> Service<http::Request<B>> for SentinelService<S, http::Request<B>, B>\n where\n S: Service<R>,\n {}\n\nimpl<S, R> Service<R> for SentinelService<S, R>\n where\n S: Service<R>,\n R: WithoutDefaultExtractor,\n {}\n</code></pre>\n<center>ELEGANT! VERY ELEGANT!</center>\n<div align=center><img src=\"https://forsworns.github.io//assets/Anya.jpg\" style=\"zoom:20%;\" /></div>\n<h2>Is it perfect?</h2>\n<p>If the generic is the same, say, if we have the following code,</p>\n<pre><code class=\"language-rust\">impl<S, R> Service<R> for SentinelService<S, R>\n where\n S: Service<R>,\n {}\n\nimpl<S, R> Service<R> for SentinelService<S, R>\n where\n S: Service<R>,\n R: AnotherTrait,\n {}\n</code></pre>\n<p>the above method does not work anymore. Or we can use a Higher-Rank Trait Bounds (HRTBs) and apply the similar method? I didn't try.</p>\n<h2>Fine, I give up ...</h2>\n<p>Finally, I choose to add some feature items in my <code>Cargo.toml</code>, so that I can use the <code>#[cfg(feature="http")]</code> attribute to control the specialization by hand :(</p>\n<p>Then the code becomes</p>\n<pre><code class=\"language-rust\">#[cfg(feature = "http")]\nimpl<S, B> Service<http::Request<B>> for SentinelService<S, http::Request<B>, B>\n where\n S: Service<http::Request<B>>,\n {}\n \n#[cfg(not(feature = "http"))]\nimpl<S, R> Service<R> for SentinelService<S, R>\n where\n S: Service<R>,\n {}\n</code></pre>\n<h2>Related questions</h2>\n<p><a href=\"https://stackoverflow.com/questions/66832882/generics-partial-specialization-in-rust\">Generics partial specialization in Rust - Stack Overflow</a></p>\n<p><a href=\"https://stackoverflow.com/questions/47675493/equivalent-of-specific-template-usage-in-c-for-rust\">Equivalent of specific template usage in C++ for Rust - Stack Overflow</a></p>\n<h2>Appendix</h2>\n<h3>Constraints on GAT</h3>\n<p>During implementing the middleware, I work a lot around the GAT in <code>tower</code> and <code>motore</code>.<br>\nGenerally I found there are two scenes where we may impose constraints on GAT.</p>\n<ul>\n<li>impose a trait constraint on GAT</li>\n<li>instantiate the trait with specific GAT, <em>i.e.</em>, the equality constraint.</li>\n</ul>\n<p>The first one is widely used in the source code of <code>tower</code> and <code>motore</code>, and the second one is related to a <a href=\"https://stackoverflow.com/questions/70531785/constraint-associated-type-of-a-generic-associated-type/74597129#74597129\">question</a> on StackOverflow answered by me.</p>\n<p>Here I made a <a href=\"https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=d7b402b716b7c0911fe42b0984f32dd4\">complied example</a> to illustrate them.</p>\n<p>See how we impose constraints on GAT <code>Builder::InstanceForBuilder</code> and <code>Builder::Useless</code> and the differences between them.</p>\n<pre><code class=\"language-rust\">// Trait definitions.\n\ntrait Builder {\n type InstanceForBuilder<'a>: Instance<'a>;\n type Useless<'a>;\n\n fn build<'a>(&self, val: &'a usize) -> Self::InstanceForBuilder<'a>;\n}\n\ntrait Instance<'a> {\n // Some functions will only work when the instance has some concrete associated type.\n type InstanceProperty;\n}\n\nfn build_with_42_for_bool_instance<'a, B, I>(builder: B)\nwhere\n B : Builder<InstanceForBuilder<'a>=I>,\n <B as Builder>::Useless<'a> : std::fmt::Debug,\n I : Instance<'a, InstanceProperty=bool>+std::fmt::Debug,\n{\n builder.build(&42);\n}\n\n// Now try it out.\n\nstruct MyBuilder;\n#[derive(Debug)]\nstruct MyInstance<'a> {\n val: &'a usize,\n}\n\nimpl Builder for MyBuilder {\n type InstanceForBuilder<'a> = MyInstance<'a>;\n type Useless<'a> = &'a str;\n\n fn build<'a>(&self, val: &'a usize) -> Self::InstanceForBuilder<'a> {\n MyInstance { val }\n }\n}\n\nimpl<'a> Instance<'a> for MyInstance<'a> {\n type InstanceProperty = bool;\n}\n\nfn main() {\n let builder = MyBuilder;\n build_with_42_for_bool_instance(builder); // TODO: Doesn't work\n}\n</code></pre>\n<h3>Differences between Tower and Motore</h3>\n<p>The <code>Service</code> trait in <code>motore</code> is different from that in tower. In the motore, the metadata and extension is moved to the context argument passed along the call chain, and the request is kept by another argument.</p>\n<p>The <code>poll_ready</code> is hided. In fact, <code>actix-web</code> shares similar opinions with <code>motore</code>, it provides <a href=\"https://docs.rs/actix-web/4.2.1/actix_web/dev/macro.forward_ready.html\">actix_web::dev::forward_ready</a> and <a href=\"https://docs.rs/actix-web/4.2.1/actix_web/dev/macro.always_ready.html\">actix_web::dev::always_ready</a> to help developers reduce boilerplate codes.</p>\n<h3>Erase message from the Request in Tonic</h3>\n<p>In tower, the type of the message is erased in <code>tonic::service::interceptor::Interceptor</code> by the following magical code. The interceptor cannot modify the message of requests.</p>\n<pre><code class=\"language-rust\">// tonic/tonic/src/service/interceptor.rs\nimpl<S, F, ReqBody, ResBody> Service<http::Request<ReqBody>> for InterceptedService<S, F>\n{\n fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {\n // It is bad practice to modify the body (i.e. Message) of the request via an interceptor.\n // To avoid exposing the body of the request to the interceptor function, we first remove it\n // here, allow the interceptor to modify the metadata and extensions, and then recreate the\n // HTTP request with the body. Tonic requests do not preserve the URI, HTTP version, and\n // HTTP method of the HTTP request, so we extract them here and then add them back in below.\n let uri = req.uri().clone();\n let method = req.method().clone();\n let version = req.version();\n let req = crate::Request::from_http(req);\n let (metadata, extensions, msg) = req.into_parts();\n\t\t// Here the `msg` is erased from the `Request`:) \n match self\n .f\n .call(crate::Request::from_parts(metadata, extensions, ()))\n {\n Ok(req) => {\n let (metadata, extensions, _) = req.into_parts();\n let req = crate::Request::from_parts(metadata, extensions, msg);\n let req = req.into_http(uri, method, version, SanitizeHeaders::No);\n ResponseFuture::future(self.inner.call(req))\n }\n Err(status) => ResponseFuture::status(status),\n }\n }\n}\n</code></pre>\n<h2>Sentinel-Rust Resources</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">Tutorial</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API Doc</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">Example Codes</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20221108/",
"title": "Mimic Generic Specialization in Rust",
"summary": "Fine, I give up",
"date_modified": "2022-11-08T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>从毕业论文的致谢说起——回忆这两年</h1>\n<p>好久没有写博客了,甚至没有去转载别人的。但是最近几个月实习学到的东西很多,读/听到了一些很有意思的论文,开源的项目也看了些,比如 <a href=\"https://github.com/cilium/tetragon\">Tetragon</a> 和另一个<a href=\"https://github.com/Forsworns/sch_bpf\">基于 ebpf 的 QoS 框架</a>,还在每天晚饭后摸鱼看完了《k8s网络权威指南》(我的大佬室友,谢谢你推荐了这本书,面试聊天吹的一半的水都来自这里)。</p>\n<p>今天想写这篇博客,(其实是一个流水账,想到哪里写到哪里,充斥着用括号补充的颠三倒四的文字和奇怪的表情符号)。是周末写毕业设计写到致谢的时候,有感而发,想回忆下这两年。</p>\n<p>不要奇怪我怎么这么快就写到致谢了 QAQ:写一段正文就不想写了跑去写致谢摸鱼,正文写完不一定改过,致谢倒是修了又修</p>\n<p>顺便 :) 今天是 1024,我的好朋友你居然看到了这行,那祝我们程序员日快乐,虽然你不一定是一个程序员,而且你看到的时候一定不是当天了(你居然也这么闲能看到这里,那我们就是好朋友)</p>\n<p>先从一段聊天记录说起吧,我写了两句致谢,就跑去和杭州群的同学聊天,大概在说“啊我致谢里写你们了,感动不,在杭州的日子是这两年最开心的时间”</p>\n<p>那段日子确实是最开心的一段时间。因为是一段没有转正压力的实习,距离为暑期实习和秋招发愁的日子也还很遥远,不在学校也没有了“卷”的氛围。那段时间里干了一些单纯出于兴趣的事情,上班干完了活甚至摸鱼写了会儿sentinel-rust,下班回去躺在床上看电视里的 DDIA和6.824(虽然这些浅尝辄止的学习内容在我后来的实习和秋招面试里都没有用到)。当然最重要的还是碰到了三个小伙伴L、P、W,经常一起放电影吃零食打三国杀元气骑士西湖游麦当当游。其实认真来说在杭州的日子也十分忙碌,研究所的同事一般下班就离开了,我们几个小伙子晚上会在工位自习,周末也偶尔会在中午起床后到办公室(笑)。</p>\n<p>比较难受的事情是当时研究所的公寓没有了,需要去外面租一个月的房子。一个月的房子自然是很难租到的,后面回想起,还不如住一个月的酒店,不过事实上当时也不确定什么时候可以搬到公寓里。于是国庆前去杭州租了房子(后来证明这次租房被坑了),国庆去苏州找了好朋友玩。</p>\n<p>十一月搬到了公寓后,还和室友住错了房间,直到又过了一个月在小伙伴家看电影的时候聊到公寓的房租才恍然大悟。室友人很好,是他也不清楚这个,之前还买了电器,电器因为他周末回家基本只有我在用 QAQ。有一次钥匙落家里了,周五看完电影半夜十二点从同学家出来发现回不去了,又跑回他们家住,还搞得室友周六来加班给我送钥匙。</p>\n<p>在杭州的时候,当时还没有核酸保鲜,但是出入地铁站都要扫健康码,很是麻烦。一回到上海,发现地铁畅通无阻,自己秒变精准防控的夸夸团,没想到后来就……</p>\n<p>说到疫情,过年的时候杭州爆发了一轮疫情。记得走之前还是下雨天,老师说可以一起去做核酸,两个人等到了晚上才去的想着会没啥人,结果做一个核酸还要排好久的队。现在居然已经习惯天天上班前去测一下,不知道该高兴还是难过。当时其实都做好要留在杭州就地过年的准备了,买了不少吃的放在冰箱里。还确认了很久两地的政策。回老家后最怕的事情发生了:过年的时候我发烧了(后来想可能是去村子里帖对联一直在吹冷风吹感冒了,北方的风还是很猛的)。幸亏吃了颗退烧药睡了一觉好了,但是谨慎起见还是去做了两次核酸,是阴性,没有身败名裂上新闻。</p>\n<p>在家呆了两周又去杭州了,那会儿就已经开始面试暑期实习了,后来刚好也就三月了,就提了离职回学校了。回到了上海,本来以为是一个很平凡的春天,却不料在学校里封锁了三个月,六月才放出来,和 J 哥从相约一起上下班变成了相约宿舍开会。封校期间送啥吃啥,三个人只有我懂得“粒粒皆辛苦”的道理,看到校外形势艰难,一粒粮食也没有浪费,结果胖了好几斤。封校期间当过几次志愿者,结果第一次还睡过了。这里不得不佩服X哥(机动大牛,吾辈楷模),封校一直在做志愿者。因为功勋卓著他吃到了我在封校期间心心念念的菠萝。</p>\n<p>六月老D视频联线帮我看了房子,我出来就直接住了进去,这次租房又碰到了两位很不错的大哥。中间该理发了,想着太热了反正直接推平,就在楼下随便找了个大门上写着“不办卡、不推销”的理发店,理了一个巨丑无比的头,算是领悟到了为什么人家门上会这么写。顶着这个头,录了 Rust China Conf 的会议视频。到了九月,房子到期大房东二房东没有谈拢,因为还打算在公司呆一段时间,只能搬了家,不过搬完房子更大了,离公司更近了,门口还有个大超市,还是蛮舒服的。然后就是暑期实习,我的两个W师兄都码力通天,而且又很有耐心,原谅我问过一些很蠢的问题QAQ。</p>\n<p>最近的三个月里秋招笔试面试测评做了一堆,每天力扣力扣力扣,然后重复给每个面试官讲下自己做过的事情,再解释一遍为啥从算法跑路来做开发,总之这三个月里</p>\n<blockquote>\n<p>Standing 5 foot 10 doing the best I can.</p>\n</blockquote>\n<p>现在秋招算结束了,想快点回学校写毕设了,然后打包东西回家,一年没有回家了~明年开学毕业就成了打工人了,应该更没心思写博客啥的了,特别是这种“没有价值”的流水账。</p>\n<p>疫情的两三年里,一切过得飞快,研一在学校里的事情其实也已经记不清了,大致就是每天上课,写作业,呆在实验室(跟着实验室从 121 换到了 503)。和几个新同学一起去了次黄山玩,我出于猎奇点了一份毛豆腐然后只能一个人默默吃下半盘,玩老E的 Switch 上的游戏结果我太菜了打不过电脑开始了折磨。和老E研究了半天超声波,然后花几千块从德国买了一个汽车喇叭回来(当我们发现真相之后内心是崩溃的)。但是这还不算最难受的,最难受的事情就是自己觉得网络和安全很紧密,加上不想学 AI 课程了,选了近世代数和现代密码学原理,然后上课很痛苦,绩点也一泻千里。</p>\n<p>研究生绩点倒是没有什么用了但是好打击人QAQ。我大概是那种需要不断取得成绩来给自己自信的人。之前我常常会想,自己真得适合学习编程吗?我确信努力用错了方向的话,事倍功半都算是值得庆幸的</p>\n<blockquote>\n<p>是不是我不适合呢</p>\n<p>我从小学开始守门</p>\n<p>可是至今仍是三流</p>\n</blockquote>\n<p>我有很多不擅长的事情,也花费了大量的时间去练习:书法、篮球,我都很喜欢它们,也付出了大量的时间,但是水平却连爱好者都谈不上。所以我真得害怕自己不适合写代码,虽然我也很喜欢它。</p>\n<p>这次想写这个也是前段时间看到了同届的大佬xw在空间发的回顾长文,深有感触,本科听过他的国奖经验分享会,却不知道他也面临过很多困难。之前也读了下 ddl 的博客记录的自己学习生活的博客,写得十分详细,里面有不少让我有共鸣的内容,当时读完就感觉 ddl 可以出自传了《他改变了交大》。</p>\n<p>可惜自己已经忘了很多事情的细节,因为很少发朋友圈导致什么印象都很模糊了。而且人的记忆真得很不可靠,我已经忘记去年杭州湾智慧谷到底下过雪没有呢?每次想起,总是和在家里看窗外雪景的记忆重叠。</p>\n<p>顺便记录一些本科还有些印象的事情:</p>\n<p>新生机械赛第一次在中院通宵(这就是大学生活吗,以及老E就是在那个时候让我感觉到很强,不禁喊他E大师,对,那个无极剑圣);班风大赛拍了微电影(太意识流了评委估计都没看懂);Jiby 国庆跑来上海找我玩(然后我们呆最多的地方居然是校门对面的网吧,办的卡后来再没用过,大四发现换老板了,最近又听说倒闭了);在 NIMO 通宵的 web 组(此处还有可恶的数据结构课作业);在科协作为一个半吊子主持的比赛(被团委老师批评了 QAQ);去腿哥家里的美赛经历(吃到了介休的奇特的担担面);在阳泉受到了Emperor Wang 的国宴级款待;在天津和 Jiby 再次会师,回来以后发现我拍的照片和同期去天津的同学拍的差距极大;南京的元宇宙比赛半夜听hs讲了两小时故事(讲的什么已经忘了,以及当时当然还没有元宇宙这个词);Long Live 小组的精美自环证明(和如今成真的预言);可视化课程大作业大家写到一半出去吃了个部落情火锅,回来在121接着通宵改;寒假和魔琳坚守 409 的寒假(然后他润了);在 Intel 实习(顺便摸鱼学习 Rust);和J哥去东京开会(然后犯了很严重的错,两个人疯狂赶地铁还差点走散了,后悔QAQ)……</p>\n<p>我的学生时代呀,就这么结束了。印象里去年L曾说过“当我拖着行李走出校门的时候,看到拖着行李进来的同学,我就感觉,我的校园时光已经结束了”。其实我十月出门的时候又何尝不是这个感觉呢。</p>\n<p align=\"right\">2022年10月23-24日</p>\n<p>看了下港股美股还有一些别的想说的,但是想了想还是</p>\n<blockquote>\n<p>我没得说了,再见吧</p>\n</blockquote>\n",
"url": "https://forsworns.github.io///zh/blogs/20221024/",
"title": "从毕业论文的致谢说起——回忆这两年",
"summary": "流水账",
"date_modified": "2022-10-24T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>最近吃饭的时候看了 CNCF 20 的两个 Webinar,都是和 eBPF、Envoy 相关的,两年前的视频但是还是很有意思的。连带讨论时间还是挺长的,都在一个小时左右,记录下重点内容。</p>\n<h1><a href=\"https://www.youtube.com/watch?v=Wocn6DK3FfM\">Comparing eBPF and Istio/Envoy for Monitoring Microservice Interactions</a></h1>\n<p>演讲者来自 Flowmill,一家观测性初创公司,好像已经被 Splunk 收购了。</p>\n<p>首先是使用 service mesh 的一些好处:流量管理、安全性、可观测性</p>\n<p>接着介绍了 Envoy 的 sidecar 架构</p>\n<p><img src=\"./image-20220613110241749.png\" alt=\"\"></p>\n<p>为什么他们会选择使用 eBPF 做可观测性</p>\n<p>Envoy/Istio 是为应用层设计的,所以下层的 metrics 并不全面。</p>\n<p>演讲者还用 <a href=\"https://github.com/GoogleCloudPlatform/microservices-demo\">GoogleCloudPlatform/microservices-demo</a> 跑了个 benchmark,然后发现监控的开销降低了不少。这里信息其实不是很完善,所以提问环节也有人质疑了这个开销,问到了有没有可能是因为 envoy/istio 加了 tls 支持,加密解密带来的。回复是没有去具体考虑什么组件带来了开销,是直接使用的默认配置,想要表现开箱即用的情况下二者的差距。</p>\n<p><img src=\"./ebpf.png\" alt=\"\"></p>\n<p><img src=\"./envoy.png\" alt=\"\"></p>\n<p>最后总结下,在项目中使用两者的取舍</p>\n<p><img src=\"./ebpf_conspros.png\" alt=\"\"></p>\n<p><img src=\"./envoy_conspros.png\" alt=\"\"></p>\n<h1><a href=\"https://www.youtube.com/watch?v=sodtj1RPjlY\">How Cilium uses BPF to Supercharge Kubernetes Networking & Security</a></h1>\n<p>这个感觉更好看一些,是来自 SUSE 的工程师介绍他们为什么会选用 Cilium 做 K8S 中的网络控制,Cilium 的公司 Isovalent 的创始人也来做了个报告。</p>\n<blockquote>\n<p>引用自博客 <a href=\"https://zhuanlan.zhihu.com/p/446660758\">Cilium eBPF实现机制源码分析</a>,写得很好</p>\n<p>Cilium是由革命性内核技术eBPF驱动,用于提供、保护和观察容器工作负载(云原生)之间的网络连接的网络组件。 Cilium使用eBPF的强大功能来加速网络,并在Kubernetes中提供安全性和可观测性。现在 Cilium将eBPF的优势带到了Service Mesh的世界。Cilium服务网格使用eBPF来管理连接,实现了服务网格必备的流量管理、安全性和可观测性。</p>\n</blockquote>\n<p>首先 SUSE 工程师说一下大背景,下面是他们使用 K8S 后的架构,控制面要提供 DNS-Aware 的管理方式</p>\n<p><img src=\"./basic_topo.png\" alt=\"\"></p>\n<p>接着是说用 iptables 有什么问题,与之相对 cilium 有什么好处:</p>\n<ul>\n<li>规则一条条写起来很麻烦,而且容易写错</li>\n<li>如果 ip 是动态的时候,添加规则就更加麻烦了,特别是在实例重启之后</li>\n</ul>\n<p><img src=\"./image-20220613112102559.png\" alt=\"image-20220613112102559\"></p>\n<ul>\n<li>性能差,链表存储规则,过滤一次太慢了,特别是你规则很多的时候</li>\n<li>得为集群中每个实例设置相同规则的时候拓展性很差</li>\n</ul>\n<p><img src=\"./iptable_scalebility.png\" alt=\"\"></p>\n<p>与之相比,Cilium 要好很多,它为你做了很多脏活累活儿,还可以支持 DNS-Aware 的管理方式</p>\n<p><img src=\"./compare.png\" alt=\"\"></p>\n<p>同时它的拓展性很好,毕竟 BPF Map 都是哈希表,规则的查询插入都快很多</p>\n<p><img src=\"./cilium_scalability.png\" alt=\"\"></p>\n<p>下面就是具体解释这种优势的来源:减少了 Envoy sidecar架构下的开销。事实上 Cilium 提出了更加激进的做法:彻底推翻了现有服务网格代理中常见的 sidecar 范式,想要把服务网格代理迁移到内核层面,具体可以参考这篇博客: <a href=\"https://isovalent.com/blog/post/2021-12-08-ebpf-servicemesh\">How eBPF will solve Service Mesh - Goodbye Sidecars </a>。对此也不乏质疑之声,另有一篇文章讨论这种争议:<a href=\"https://zhuanlan.zhihu.com/p/456214160\">eBPF 和 Wasm:探索服务网格数据平面的未来</a>。</p>\n<p><img src=\"./cilium_deeper1.png\" alt=\"\"></p>\n<p>Cilium 替你干了很多事:多个 cluster 间可以方便地实现全局路由</p>\n<p><img src=\"./cilium_deeper2.png\" alt=\"\"></p>\n<p>Cilium 替你干了很多事:可以方便地进行安全通讯</p>\n<p><img src=\"./cilium_deeper3.png\" alt=\"\"></p>\n<p>Cilium 替你干了很多事:可以方便地指定各类应用层协议的规则</p>\n<p><img src=\"./cilium_deeper4.png\" alt=\"\"></p>\n<p>Cilium 替你干了很多事:可以方便地管理消息队列、数据库</p>\n<p><img src=\"./cilium_deeper5.png\" alt=\"\"></p>\n<p>Isovalent 创始人讲的就比较基础的 ebpf 相关内容了,这里不再记录。但是提到了一个很有趣的内容,<a href=\"https://github.com/cilium/cilium/projects/34\">BPF templating</a>,但是好像目前还没有实现。</p>\n<p>题外话:Isovalent 关于 Cilium 的工作做的很棒,而且他们的 eBPF 教学文档写得很详细,也一直在维护 golang 的 ebpf 包 :thumbsup:。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20220611/",
"title": "CNCF Webinar 观后感",
"summary": "看了两个 CNCF 20 研讨视频下饭",
"date_modified": "2022-06-11T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>最近在看一些虚拟化相关的东西,转载下一则专栏中 <a href=\"https://www.zhihu.com/column/huiweics\">虚拟化笔记</a> 的文章,侵删</p>\n<p>在知乎上看到的文章,然后发现曹钦翔老师也关注了作者= =</p>\n<h1><a href=\"https://zhuanlan.zhihu.com/p/361918197\">virtio详细介绍和1.1新功能</a></h1>\n<p>virtio是一种实践出来的技术,并且最终标准化,virtio是一种通用的虚拟化设备模拟标准,得到了大部分guest操作系统和hypervisor的支持,方便guest操作系统和hypervisor之间任意互相匹配。virtio出现之前hypervisor各有各的IO设备模拟方案,并在guest操作系统中大量合入驱动代码,导致一片混乱,后来xen中出来了部分virtio思想,在kvm中实现并且发扬光大,发表了论文《virtio: Towards a De-Facto Standard For Virtual I/O Devices》,论文促使virtio形成了正式标准。virtio标准最早是0.9.5版本(Virtio PCI Card Specification Version 0.9.5),于2012年形成了draft,并没有正式发布,继续发展,2016年发布了1.0版本(Virtual I/O Device (VIRTIO) Version 1.0),2019年发布了1.1版本(Virtual I/O Device (VIRTIO) Version 1.1)。</p>\n<h2>virtio详细介绍</h2>\n<p>virtio分为driver和device,driver部分运行于guest操作系统中,device部分运行于hypervisor中,driver和device是生产者和消费者模式动作,driver生产内存,device消费内存。不同virtio版本之间是互相兼容的,driver和device版本不同也可以互相运转。</p>\n<h2>基本要素</h2>\n<p><img src=\"./v2-ecfef5caf01b95cc40c6e37e69dd48f2_720w.jpg\" alt=\"\"></p>\n<ul>\n<li>device status field</li>\n</ul>\n<p>driver发现了device,driver可以正常驱动device,driver或者device出错了,driver或者device要进行reset。</p>\n<ul>\n<li>device feature bit</li>\n</ul>\n<p>driver和device协商feature以便于不同virtio版本之间兼容。</p>\n<ul>\n<li>notification</li>\n</ul>\n<p>driver和device互通通知对方,driver生产好的内存要通知device去消费,device消费完了要通知driver回收内存。</p>\n<p>driver通知deivce用doorbell机制,在kvm中是写寄存器,kvm进行拦截再通知vhost。</p>\n<p>device通知driver用中断机制,在kvm中是中断注入。</p>\n<ul>\n<li>config space</li>\n</ul>\n<p>典型的如virtio-net-device的MAC地址/MTU/最大支持队列数等。</p>\n<ul>\n<li>virtqueue</li>\n</ul>\n<p>每个virtqueue分成这三部分,descriptor/available/used,descriptor/available/used就是三个大数组,descriptor数组内容存放真正东西,available和used数组内容存放descriptor数组的下标。driver生产内存,把生产的内存地址和长度写在descriptor,然后把descriptor数据下标写到available数组中,通知device,device消费内存,消费完再把descriptor的数据下标定到used数组中,通知driver进行内存回收。</p>\n<p>chained descriptor,几个desciptor项连在一起,适用于scater-gather。</p>\n<p>indirect descriptor,主descriptor中一项指向另一个descriptor数组。</p>\n<p>一般设备的virtqueue基本可以分三类rx virtqueue/tx virtqueue/ctrl virtqueue,rx virtqueue和tx virtqueue用于进行IO,driver通过ctrl virtqueue控制device。</p>\n<pre><code>/* Virtio ring descriptors: 16 bytes. These can chain together via "next". */\nstruct vring_desc {\n /* Address (guest-physical). */\n __virtio64 addr;\n /* Length. */\n __virtio32 len;\n /* The flags as indicated above. */\n __virtio16 flags;\n /* We chain unused descriptors via this, too */\n __virtio16 next;\n};\n \nstruct vring_avail {\n __virtio16 flags;\n __virtio16 idx;\n __virtio16 ring[];\n};\n \n/* uint32_t is used here for ids for padding reasons. */\nstruct vring_used_elem {\n /* Index of start of used descriptor chain. */\n __virtio32 id;\n /* Total length of the descriptor chain which was used (written to) */\n __virtio32 len;\n};\n \ntypedef struct vring_used_elem __attribute__((aligned(VRING_USED_ALIGN_SIZE)))\n vring_used_elem_t;\n \nstruct vring_used {\n __virtio16 flags;\n __virtio16 idx;\n vring_used_elem_t ring[];\n};\n</code></pre>\n<p>used和avaible不一样是因为rx时,device给driver写数据,device写多少长度数据要给driver反回去。</p>\n<h2>初始化</h2>\n<p>device准备,driver发现device,状态更新和feature协商,driver分配virtqueue,把virtqueue地址告诉device。</p>\n<h2>承载</h2>\n<p>首先virtio设备是IO设备,IO设备得以某种方式和CPU内存联结在一起,IO设备还得以某种方式和内存交互数据,IO设备还得提供一种机制让CPU控制IO设备。</p>\n<p>virtio标准中有三种承载机制,分别是pci,mmio和channel i/o,pci是最通用的计算机bus,qemu和kvm能很好的模拟pci bus,mmio主要用于嵌入式设备,这些设备没有pci bus,channel i/o用于一些IBM机器,很少见。这里以最常见的pci来说,它的作用就是让driver正常发现device,让driver有方法控制device,如写pci配置空间,写pci bar空间。</p>\n<pre><code>typedef struct VirtIOPCIRegion {\n MemoryRegion mr;\n uint32_t offset;\n uint32_t size;\n uint32_t type;\n} VirtIOPCIRegion;\n \ntypedef struct VirtIOPCIQueue {\n uint16_t num;\n bool enabled;\n uint32_t desc[2];\n uint32_t avail[2];\n uint32_t used[2];\n} VirtIOPCIQueue;\n \nstruct VirtIOPCIProxy {\n PCIDevice pci_dev;\n MemoryRegion bar;\n union {\n struct {\n VirtIOPCIRegion common;\n VirtIOPCIRegion isr;\n VirtIOPCIRegion device;\n VirtIOPCIRegion notify;\n VirtIOPCIRegion notify_pio;\n };\n VirtIOPCIRegion regs[5];\n };\n MemoryRegion modern_bar;\n MemoryRegion io_bar;\n uint32_t legacy_io_bar_idx;\n uint32_t msix_bar_idx;\n uint32_t modern_io_bar_idx;\n uint32_t modern_mem_bar_idx;\n int config_cap;\n uint32_t flags;\n bool disable_modern;\n bool ignore_backend_features;\n OnOffAuto disable_legacy;\n uint32_t class_code;\n uint32_t nvectors;\n uint32_t dfselect;\n uint32_t gfselect;\n uint32_t guest_features[2];\n VirtIOPCIQueue vqs[VIRTIO_QUEUE_MAX];\n \n VirtIOIRQFD *vector_irqfd;\n int nvqs_with_notifiers;\n VirtioBusState bus;\n};\n</code></pre>\n<p>VirtIOPCIProxy存储virtio信息,kvm给guest注册了很多memory region,driver写这些memory region,kvm拦截,把写的值放在VirtIOPCIProxy中。</p>\n<pre><code>static void virtio_pci_modern_regions_init(VirtIOPCIProxy *proxy,\n const char *vdev_name)\n{\n static const MemoryRegionOps common_ops = {\n .read = virtio_pci_common_read,\n .write = virtio_pci_common_write,\n .impl = {\n .min_access_size = 1,\n .max_access_size = 4,\n },\n .endianness = DEVICE_LITTLE_ENDIAN,\n };\n g_string_printf(name, "virtio-pci-common-%s", vdev_name);\n memory_region_init_io(&proxy->common.mr, OBJECT(proxy),\n &common_ops,\n proxy,\n name->str,\n proxy->common.size);\n}\nstatic void virtio_pci_common_write(void *opaque, hwaddr addr,\n uint64_t val, unsigned size)\n{\n VirtIOPCIProxy *proxy = opaque;\n VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);\n \n switch (addr) {\n case VIRTIO_PCI_COMMON_DFSELECT:\n proxy->dfselect = val;\n break;\n case VIRTIO_PCI_COMMON_GFSELECT:\n proxy->gfselect = val;\n break;\n \n default:\n break;\n }\n}\n</code></pre>\n<h2>设备分类</h2>\n<p>virtio分为很多设备类型virtio-net/virtio-blk/virtio-scsi等等,virtqueue实现通用部分,每种设备再实现具体功能部分,可以扩展feature部分,在virtqueue传输的数据中定义自己功能相关标准等。</p>\n<h2>举例分析</h2>\n<p>以qemu中实现的virtio-net-pci举例来说</p>\n<p>首先它是一个virtio-net类型设备,其次它承载在pci上,所以VirtIONetPCI就把两者结合起来了。</p>\n<pre><code>struct VirtIONetPCI {\n VirtIOPCIProxy parent_obj;\n VirtIONet vdev;\n};\n</code></pre>\n<p>virtqueue实现了数据共享,它并不关心到底是网络还是存储数据,所以要在它的buf最前面加上设备类型自己的元数据头,virtio-net-pci用了virtio_net_hdr。</p>\n<pre><code>/* This header comes first in the scatter-gather list.\n * For legacy virtio, if VIRTIO_F_ANY_LAYOUT is not negotiated, it must\n * be the first element of the scatter-gather list. If you don't\n * specify GSO or CSUM features, you can simply ignore the header. */\nstruct virtio_net_hdr {\n /* See VIRTIO_NET_HDR_F_* */\n uint8_t flags;\n /* See VIRTIO_NET_HDR_GSO_* */\n uint8_t gso_type;\n __virtio16 hdr_len; /* Ethernet + IP + tcp/udp hdrs */\n __virtio16 gso_size; /* Bytes to append to hdr_len per frame */\n __virtio16 csum_start; /* Position to start checksumming from */\n __virtio16 csum_offset; /* Offset after that to place checksum */\n};\n</code></pre>\n<p>再看virtio-net-pci ctrl virtqueue传输的数据内容,基本就是打开网卡混杂模式/修改MAC/virtqueue个数/配置rss/配置offload等。</p>\n<pre><code>/*\n * Control virtqueue data structures\n *\n * The control virtqueue expects a header in the first sg entry\n * and an ack/status response in the last entry. Data for the\n * command goes in between.\n */\nstruct virtio_net_ctrl_hdr {\n uint8_t class;\n uint8_t cmd;\n} QEMU_PACKED;\n</code></pre>\n<h2>virtio1.1新功能</h2>\n<p>virtio 1.0存在的问题第一是性能不高,第二是硬件不太好实现。</p>\n<p>driver和device运行在不同的cpu,driver和device共享内存,存在不同cpu之间互相通知进行cache刷新的问题,virtio1.0 virtqueue分成三个数组,三个数组分布在不同的cacheline上需要多次cache刷新,所以virtio 1.1引入了packed ring,把virtio 1.0中的三个数组合并成一个,这样大大减少了cache刷新的次数。具体做法就是packed virtqueue把available和used当成descriptor中flag字段两个bit,driver本地存放一个driver_local_bit,把available_bit=driver_local_bit和used_bit=!driver_local_bit,device本地存放一个device_local_bit,消费完内存后used_bit=device_local_bit。</p>\n<p>通知是有开销的,virtio1.1 batch和in-order减少driver和device互相通知对方的次数,batch就是driver一次多生产几块内存,再通知device,in-order就是device按driver生产内存的顺序消费内存,消费完后只通知driver最后一块内存可以回收了,因为严格按顺序消费的,driver由此可知前面的内存也已经消费完了。</p>\n<pre><code>struct vring_packed_desc {\n /* Buffer Address. */\n uint64_t addr;\n /* Buffer Length. */\n uint32_t len;\n /* Buffer ID. */\n uint16_t id;\n /* The flags depending on descriptor type. */\n uint16_t flags;\n};\n</code></pre>\n<p>硬件实现也一样,driver写descriptor发现一次pci传输,写available数组又要发现一次pci传输,如果把descriptor和available数组合并只要一次pci传输即可。</p>\n<h2>实现情况</h2>\n<p>linux 4.18 virtio-net driver已经能支持virtio 1.1了,但vhost-net不支持virtio 1.1。</p>\n<p>qemu master实现了virtio 1.1。</p>\n<p>dpdk virtio pmd和vhost-user都支持virtio 1.1。</p>\n<h2>总结</h2>\n<p>virtio标准还会继续发展,功能会越来越多,设备类型会越来越多,如virtio GPU和virtio vIOMMU,GPU最难虚拟化,目前用的是mdev,没有IOMMU,virtio设备可以修改任意guest内存,有vIOMMU更安全,vIOMMU也可以用vt-d实现,virtio device emulation可以在qemu/kernel/dpdk中实现,virtio技术百花齐放,创新不断,是做虚拟化必须研究的技术。总结virtio的目标就是统一IO设备,虚拟机看到的所有的外设都是virtio类型,只需要安装virtio类型的驱动即可,如果硬件也能实现virtio,那么裸金属也一样了,虚拟机和裸金属互相热迁移,一个镜像走天下。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20220316/",
"title": "转载:virtio详细介绍和1.1新功能",
"summary": "之前做 IPCC 看过 virtio 相关内容,最近在 DPU 上看到又忘了 orz",
"date_modified": "2022-03-16T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Sentinel 结合 eBPF 的探索</h1>\n<p><a href=\"https://github.com/alibaba/Sentinel\">Sentinel</a> 是一款面向微服务的高可用流控防护组件;eBPF 程序,以 <a href=\"https://forsworns.github.io/zh/blogs/20210715/\">XDP</a> 为例,可以尽早地对网络包进行丢弃,减少进入协议栈的包数。</p>\n<p>本文将探索 <a href=\"https://github.com/sentinel-group/sentinel-rust\">Sentinel-Rust</a> 与 eBPF 的结合,在这个例子中,我们将端口抽象成 Sentinel 中的资源进行统一管理,根据对应资源(端口) Sentinel 的创建情况在 XDP 中丢弃包。eBPF 程序的编写基于 <a href=\"https://github.com/foniod/redbpf\">redbpf</a> 库。</p>\n<h2>内核 XDP 程序</h2>\n<p>在 <a href=\"https://github1s.com/sentinel-group/sentinel-rust/blob/HEAD/examples/ebpf/probes/src/port/main.rs\">内核程序</a> 中创建两个 eBPF map,用来做用户态 Sentinel 创建程序和内核中的 XDP 程序的通讯。<code>port_events</code> 记录的是某个端口接收到了包这一事件,而 <code>port_blocked</code> 则是一个数组,它的下标对应端口号。</p>\n<pre><code class=\"language-rust\">#[map]\nstatic mut port_events: PerfMap<PortEvent> = PerfMap::with_max_entries(1024);\n#[map]\nstatic mut port_blocked: Array<bool> = Array::with_max_entries(1 << 16);\n</code></pre>\n<p>接下来编写一个简单的 XDP 程序,我们只检测接收到的包的目的端口号,触发一个事件提交到 <code>port_events</code> 中,该事件会在用户态程序中被捕获到;XDP程序会检测 <code>port_blocked</code> 中 Sentinel 是否创建失败了,如果 Sentinel 创建失败,那么可能是由于该端口的 QPS 过高,因此可以直接丢弃掉该包。</p>\n<pre><code class=\"language-rust\">#[xdp]\npub fn block_port(ctx: XdpContext) -> XdpResult {\n if let Ok(transport) = ctx.transport() {\n let port = transport.dest();\n let event = MapData::new(PortEvent { port });\n unsafe { port_events.insert(&ctx, &event) };\n // the mmapped memory port_blocked not sync between kernel and userspace\n let blocked = unsafe { port_blocked.get(port as u32) };\n if let Some(&blocked) = blocked {\n if blocked {\n return Ok(XdpAction::Drop);\n }\n }\n }\n Ok(XdpAction::Pass)\n}\n</code></pre>\n<h2>用户态 Sentinel 程序</h2>\n<p>在 <a href=\"https://github1s.com/sentinel-group/sentinel-rust/blob/HEAD/examples/ebpf/userspace/src/port.rs\">用户态</a>,我们首先完成 Sentinel 的初始化程序,之后加载 XDP 程序并将它注入到某个网卡上(示例中选择了 <code>lo</code>)。之后我们加载 Sentinel 的流控规则。这里我们设置名为 <code>port:8000</code> 的资源的 QPS 的阈值为 1.0,即每秒仅能有一个该资源被创建。</p>\n<pre><code class=\"language-rust\">flow::load_rules(vec![Arc::new(flow::Rule {\n resource: "port:8000".into(),\n threshold: 1.0,\n calculate_strategy: flow::CalculateStrategy::Direct,\n control_strategy: flow::ControlStrategy::Reject,\n ..Default::default()\n})]);\n</code></pre>\n<p>完成上述初始化后,我们监听 MPSC 的 event 队列。当检测到 <code>port_events</code> 中的事件时,我们使用 <code>port:{}</code> 的命名格式去构建 Sentinel,当构建成功/失败时,更改 <code>port_blocked</code> 的状态以便指导 XDP 程序。</p>\n<pre><code class=\"language-rust\">while let Some((map_name, events)) = loaded.events.next().await {\n let port_blocked_map = loaded.map("port_blocked").unwrap();\n let port_blocked =\n Array::<bool>::new(port_blocked_map).unwrap();\n for event in events {\n match map_name.as_str() {\n "port_events" => {\n let event = unsafe { std::ptr::read(event.as_ptr() as *const PortEvent) };\n let entry_builder = EntryBuilder::new(format!("port:{}", event.port))\n .with_traffic_type(base::TrafficType::Inbound);\n if let Ok(entry) = entry_builder.build() {\n port_blocked\n .set(event.port as u32, false)\n .unwrap();\n entry.exit()\n } else {\n port_blocked\n .set(event.port as u32, true)\n .unwrap();\n }\n }\n _ => panic!("unexpected event"),\n }\n }\n}\n</code></pre>\n<h2>思考</h2>\n<p>当然这里有一个问题:用户态的 Sentinel 创建程序和内核中的 XDP 程序对 <code>port_blocked</code> 这个 ebpf map 的读写是不同步的,这在初始化时尤为明显。例如将 Sentinel 的规则设置为禁止 <code>8000</code> 端口的所有流量,即 <code>threshold</code> 设置为 0,仍然可以完成第一次请求。</p>\n<p>是否可以去做同步呢?一般来讲,eBPF 一定是非阻塞的程序,也可以说是原子的。LWN 的 <a href=\"https://lwn.net/Articles/825415/\">一篇文章</a> 介绍了 <code>BPF_PROG_TYPE_LSM</code> 和 <code>BPF_PROG_TYPE_LSM</code> 两类 eBPF 程序中的标志 <code>BPF_F_SLEEPABLE</code>。即使是我们有某种同步手段,阻塞 XDP 的执行似乎仍然不是一个明智的选择。</p>\n<h2>Sentinel-Rust 相关资源</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">使用指南</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API 文档</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">示例代码</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20220224/",
"title": "Sentinel 结合 eBPF 的探索",
"summary": "一则使用 Sentinel 做基于 eBPF 的流量控制的例子",
"date_modified": "2022-02-24T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Wireshark 是怎么抓包的?</h1>\n<h2>现象</h2>\n<p>之前在测试 <a href=\"https://www.gitlink.org.cn/xuos/xiuos\">XiUOS</a> 网络框架的时候,将开发板和 PC 直连,在开发板架起 TCP 服务端,PC 上开了 Wireshark,截了一张图</p>\n<p><img src=\"./pc2board.png\" alt=\"\"></p>\n<p>可以看到 Wireshark 抓到了一个长度为 <code>13194</code> 的一个帧,远超了 TCP PDU 最大长度。顿时感觉很奇怪,看了一下自己的网卡设置,也没有开启巨型帧,为什么会看到这么大的一个帧呢?</p>\n<h2>问题分析</h2>\n<p>基于一则类似的 <a href=\"https://osqa-ask.wireshark.org/questions/24699/tcp-packet-length-was-much-greater-than-mtu/\">讨论</a></p>\n<blockquote>\n<p>这是因为系统启用了 TCP Large Segment Offload(缩写为 TSO 或 LSO)。操作系统将大于 MTU 的数据包传递给网络适配器,而网络适配器驱动程序负责分解这些数据包,以匹配 MTU。TSO 是一种增强性能的可选特性,可以将其关闭,在这种情况下,操作系统将不再生成过大的帧。</p>\n</blockquote>\n<p>也就是说,这是因为抓包时抓到的是系统提供给网卡的巨型帧,在网卡层面才会进行分拆处理。</p>\n<h2>Wireshark 具体实现</h2>\n<p>基于另一则关于 Wireshark 是如何抓包的 <a href=\"https://osqa-ask.wireshark.org/questions/22956/where-exactly-wireshark-does-captures-packets/\">讨论</a>。</p>\n<p>通常这种抓包的框架都是作用于在网卡驱动和内核高层的协议栈(如 TCP/IP)之间的。Wireshark 在 Linux 下使用的是 libpcap,相关 slides 见 <a href=\"https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf\">libpcap: An Architecture and Optimization Methodology for Packet Capture</a>。之前学习 eBPF 的时候了解到 tcpdump 是基于 BPF 的。事实上,tcpdump 也是基于 libpcap 开发的,libpcap 是单独抽离出的一个库,封装了 BPF 相关调用。</p>\n<p>Windows 版本是基于 WinPcap 开发的,WinPcap 的 <a href=\"https://www.winpcap.org/misc/faq.htm#Q-26\">QA</a> 里也谈到了具体的实现:</p>\n<blockquote>\n<p>问:WinPcap如何与Windows网络连接?它是否会降低TCP/IP堆栈和应用程序的速度?<br>\n答:在Windows内核中,WinPcap作为协议驱动程序运行。它与 TCP/IP 处于同一层级。和 TCP/IP 协议栈一样,它从底层网卡驱动程序接收数据包,但只有在基于 WinPcap 的工具正在捕获数据包时才会进行接收。这意味着,当安装了 WinPcap 但未进行捕获时,对系统的影响不存在。注意,开机后第一个网络流量捕获应用程序监听适配器时,WinPcap 驱动程序才会被加载到内核中。当 WinPcap 运行时,它不会直接与 TCP/IP 进行交互。然而,尤其是在高网络负载下,WinPcap 活动(尤其是软件中断级别的活动)将影响 TCP/IP 响应。</p>\n</blockquote>\n<p>WinPcap 的设计和实现可以见论文 <a href=\"https://www.winpcap.org/docs/iscc01-wpcap.pdf\">An Architecture for High Performance Network Analysis</a>(开坑有空详细读一下)。</p>\n<h2>附录</h2>\n<h3>解析 <code>pcnpag</code> 文件</h3>\n<p>使用 Python 解析 <code>pcnpag</code> 文件、进行数据分析时参考的资料</p>\n<ul>\n<li>\n<p>IETF 对 pcapng 的规范:<a href=\"https://www.ietf.org/staging/draft-tuexen-opsawg-pcapng-02.html\">PCAP Next Generation (pcapng) Capture File Format</a></p>\n</li>\n<li>\n<p>Python 解析 pcapng 文件:<a href=\"https://github.com/rshk/python-pcapng\">rshk/python-pcapng: Pure-Python library to parse the pcap-ng format used by newer versions of dumpcap & similar tools</a></p>\n</li>\n</ul>\n<h4>例子</h4>\n<p>下面是在实验中写的一段加载 <code>pcnpag</code> 文件并计算吞吐量、包数的 Python 脚本。这里默认该文件中记录的是已经在 Wireshark 里经过过滤后导出的特定分组,所以直接对整个文件进行了计算</p>\n<pre><code class=\"language-python\">from pcapng import FileScanner\nfrom pcapng.blocks import InterfaceStatistics, EnhancedPacket\nimport pcapng.structs as ps\n\nSECOND = 1e6 # 一秒钟\nUNIT = 4*1e6 # 单位时间,大于一秒钟,计算吞吐量的窗口\nUNIT_NUM = UNIT/SECOND\n\ndef load(filename):\n curr = 0\n packet = 0\n throughput = []\n with open(filename, "rb") as f:\n scanner = FileScanner(f).__iter__()\n section_header = next(scanner)\n interface_description = next(scanner)\n interface_statistics = None\n for block in scanner:\n if type(block) is EnhancedPacket and block.packet_len>80:\n packet += 1\n volume += block.packet_len\n timestamp = (block.timestamp_high<<32)+block.timestamp_low\n curr_temp = int(timestamp/SECOND)\n if curr == curr_temp:\n throughput[-1] += block.packet_len\n else:\n throughput.append(block.packet_len)\n curr = curr_temp\n else: # InterfaceStatistics\n interface_statistics = block\n throughput = [t*8/UNIT_NUM/1000 for t in throughput] # get kbps\n return throughput, packet\n</code></pre>\n",
"url": "https://forsworns.github.io///zh/blogs/20220105/",
"title": "Wireshark 是在怎么抓包的?",
"summary": "用 Wireshark 时抓到了进击的巨型帧?",
"date_modified": "2022-01-05T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>WebAssembly in Envoy</h1>\n<p>Sentinel-Golang 最近在筹划添加 Envoy Wasm 拓展,Rust 也常常被用来做 Wasm 相关的开发,本着学习的想法进行了调研。调研后发现不仅 Proxy-Wasm 团队有提供 <a href=\"https://github.com/proxy-Wasm/proxy-Wasm-rust-sdk\">Rust SDK</a>,也有一个针对 Envoy 的在 Proxy-Wasm 之上二次开发的 <a href=\"https://github.com/tetratelabs/envoy-Wasm-rust-sdk/\">SDK</a>。目前 Sentinel-Rust 下的 <code>example/proxy/envoy</code> 下提供了一个结合 Envoy 的简单的样例,目前还没有提供任何包装。</p>\n<h2>翻译 <a href=\"https://github.com/proxy-Wasm/spec/blob/master/docs/WebAssembly-in-Envoy.md\">Proxy-Wasm Spec</a></h2>\n<p>两年前发布的 sepc,不知道有多少内容发生了变化。</p>\n<h3>背景</h3>\n<p>在 2019 年的时候,Envoy 只能静态编译使用,所有拓展就是在构建时候就要编译进来的。这也就意味着:提供了自定义的拓展的项目,例如 Istio,必须维护、分发他们自己的二进制文件,而不是使用官方的未修改过的 Envoy。</p>\n<p>对于不能控制他们的部署的项目来说,问题更加棘手,因为任何对拓展的更新或 bug 的修复都需要构建出整个项目、发版、分发下去,在生产环境中重新部署。这也意味着经常要在已经部署的拓展和控制平面的配置间做版本的迁移。</p>\n<h3>解决方案</h3>\n<p>虽然上面提到的问题可以通过动态加载的 C++ 拓展来解决一部分,但是由于 Envoy 的快速发展,并没有稳定的 ABI,甚至是 API,提供给拓展,更新 Envoy 又往往需要代码的更改,导致更新非常依赖于人工。使用 WebAssembly 编写的拓展就会简单很多了,而且它还有很多其他优势。</p>\n<h3>什么是 WebAssembly?</h3>\n<p>WebAssembly (Wasm) 是一个面向未来的可移植的可执行的二进制文件格式。代码几乎在原生的速度下在(对宿主)内存安全的沙箱中执行,显式定义了对资源的限制和与宿主环境(例如,proxy)交流的 API。</p>\n<h3>优势</h3>\n<ul>\n<li><strong>灵活性</strong>。拓展可以在运行时直接从控制平面分发和加载。这不仅意味着每个人都可以使用官方的无修改的代理、加载自定义的拓展,也意味着任何更新或 bug 的修复能在运行时推送或测试,不需要更新或重新部署一个新的二进制文件。</li>\n<li><strong>可靠性和隔离性</strong>。因为拓展都是部署在一个沙箱里的,他们出现内存错误或泄露了内存并不会影响到整个 proxy。另外,可以对沙箱的 CPU 和内存等资源的使用做限制。</li>\n<li><strong>安全</strong>。因为拓展是部署在明确定义了 API 的沙箱中的,只和 proxy 交互。他们只能获取和修改连接或请求的部分属性。另外,因为 proxy 在协调这些连接或请求,它可以选择隐藏、审查来自插件的敏感信息,例如 “Authorization” 和 “Cookie” HTTP headers,或是用户的 IP 地址。</li>\n<li><strong>多样性</strong>。有超过 30 种语言可以被编译成 WebAssembly 模块,允许各种背景的开发者编写 Proxy-Wasm 拓展。</li>\n<li><strong>可维护</strong>。由于拓展是用标准库写的,独立于 proxy 的代码,可以提供稳定的 ABI。</li>\n<li><strong>可移植性</strong>。由于宿主环境和拓展的交互是无视 proxy 的。使用 Proxy-Wasm 写的拓展可以在各种各样的 proxies 种执行,例如 Envoy、NGINX、ATS 或者甚至是在 gRPC 库中执行。</li>\n</ul>\n<h3>缺陷</h3>\n<ul>\n<li>高额的内存消耗,因为需要开启多个虚拟机,每个都有它自己的内存块。</li>\n<li>由于需要向沙箱内和向沙箱外拷贝大量报文数据,对报文进行解析的拓展的性能或低一些。</li>\n<li>CPU 密集的拓展性能较差,和原生代码相比会至多变慢两倍。</li>\n<li>增加了二进制文件的大小,因为要把 Wasm 的运行时也打包进去,WAVM 大概要 20MB,V8 大概要 10MB。</li>\n<li>Wasm 的生态才刚刚起步,开发大部分集中于浏览器端的支持,JavaScript 会是考虑的宿主环境。</li>\n</ul>\n<h3>概述</h3>\n<p>通过Proxy-Wasm,开发者可以使用他们选择的编程语言编写代理扩展,最好是使用我们提供的特定语言库。然后,这些扩展被编译成可移植的 Wasm 模块,并以该格式发布。</p>\n<p>在 Proxy 侧,一旦 Wasm 模块被加载(直接从磁盘或通过 xDS 从控制平面推送),它将被验证是否符合定义的 Proxy-Wasm 接口,并使用 Proxy 中嵌入的 Wasm 运行时进行实例化,它在每个工作线程中创建一个新的 Wasm 虚拟机。</p>\n<p>对于Envoy的每一种扩展类型,我们都创建了一个 shim 层,将扩展的接口转换为 Proxy-Wasm 的调用,因此这些接口与原生的(C++)Envoy 扩展中使用的接口非常相似,都是采用事件驱动的编程模型。</p>\n<p><img src=\"./WebAssembly-in-Envoy.svg\" alt=\"\"></p>\n<h3>运行时</h3>\n<p>为了执行 Proxy-Wasm 拓展,proxy 需要内置一个运行时,可以在沙箱中执行代码。目前有两个 C 或 C++ Wasm 运行时:基于 LLVM 的WAVM 和 V8。Envoy 同时包含 WAVM 和 V8 两个,我们在配置时可以选择。</p>\n<h3>虚拟机</h3>\n<p>当Wasm运行时实例化一个Wasm模块,它为它创建一个Wasm虚拟机(VM实例)。</p>\n<p>在虚拟机实例和Proxy-Wasm扩展之间有几种映射模型。归根结底,这是一种权衡:启动延迟和资源使用,以及隔离和安全。</p>\n<ul>\n<li><strong>Persistent in-process VM per worker thread per Wasm module (shared across multiple configured uses of Wasm extension)</strong>. A single Wasm module can contain multiple extensions (e.g. listener filter and transport socket, both in a single package). For each Wasm module, a single persistent in-process VM instance is created, and it can (but doesn’t have to) be shared by all Proxy-Wasm extensions referencing that Wasm module in the configuration.</li>\n<li><strong>Persistent in-process VM per worker thread per Wasm extension</strong>. A single persistent in-process VM instance is created for each Wasm extension and it’s shared by all Proxy-Wasm extensions referencing the given Wasm module in the configuration, similarly to how the native (C++) extensions are instantiated today.</li>\n<li><strong>Persistent in-process VM per worker thread per configured use of Wasm extension</strong>. A single persistent in-process VM instance is created for each configured use of a Proxy-Wasm extension referencing given Wasm module in the configuration. This model provides stronger isolation guarantees than the previous models, and it should be preferred in the multi-tenant environments.</li>\n<li><strong>Ephemeral (per request) in-process VM</strong>. A new ephemeral in-process VM instance is created for each request, for each Proxy-Wasm extension, and it’s destroyed immediately after the request is finished. This is expected to be prohibitively expensive.</li>\n<li><strong>Out-of-process VM</strong>. This is out of scope for this document, but for deployments loading untrusted (and potentially malicious) Wasm modules in multi-tenant environments, that require strong security guarantees and want to protect against Spectre-like attacks, proxy should communicate with an out-of-process Wasm sandbox (e.g. using Filters-over-gRPC or shared memory) that implements Proxy-Wasm, which would execute Wasm modules on its behalf and stream results back to the proxy.</li>\n</ul>\n<h3>宿主环境</h3>\n<p>沙盒 Wasm 虚拟机使用明确定义的接口与宿主环境(即 proxy)进行通信,其中包括:从 Wasm 模块导出的、代理可以调用的函数,Wasm 虚拟机可以调用的辅助函数,以及用于内存管理的 Wasm 函数。</p>\n<p>因为这个接口是非常底层的和相当稳定的,它允许我们定义一个稳定的ABI(函数原型将在一个单独的文件中定义),扩展可以使用它。</p>\n<h3>Control Plane (xDS) 集成</h3>\n<p>Proxy-Wasm 扩展可以通过使用 Envoy 的 <code>Config::DataSource</code> 在配置中引用它们来加载,它可以指向磁盘上的文件或包含从控制平面(xDS)发送的内联Wasm模块。我们正在扩展这个接口,以支持从HTTP服务器获取资源。由于加载的Wasm模块将被执行,强烈建议加强检查,如SHA256校验和,或数字签名的扩展。</p>\n<h3>错误检测和报告</h3>\n<p>在 Wasm 虚拟机崩溃的情况下(例如,由于 Wasm 扩展中的错误),代理应该创建一个新的虚拟机实例,记录关于崩溃的信息,并将其暴露给外部系统(例如,使用统计),以便控制平面可以根据这些信息采取行动。</p>\n<p>理想情况下,代理还应该跟踪崩溃的数量,当达到一个极限时,它应该停止重新启动Wasm VM(以防止进入崩溃循环),并开始拒绝连接和/或返回错误给客户。</p>\n<h3>可配置的资源限制</h3>\n<p>每个配置的 Proxy-Wasm 扩展可以设置资源限制(每个虚拟机可以分配的最大内存,以及每次调用时可以消耗的最大CPU时间),以限制资源的使用。</p>\n<h3>可配置的 API 限制</h3>\n<p>对于每个配置的 Proxy-Wasm 扩展,可用的 API 列表可以被限制,因此,只计算的扩展(如压缩)将无法访问他们不需要的 API(如HTTP/gRPC 侧调用)。</p>\n<p>此外,一些 API 可以对输入和/或输出进行审查(例如,删除返回的首部的属性,或限制 HTTP/gRPC 侧调用的主机列表)。</p>\n<h2>相关示例</h2>\n<p>开头提到的两个 SDK 的示例:</p>\n<p><a href=\"https://github.com/proxy-Wasm/proxy-Wasm-rust-sdk\">Proxy-Wasm Rust SDK</a></p>\n<p><a href=\"https://github.com/tetratelabs/envoy-Wasm-rust-sdk/\">Proxy-Wasm Envoy SDK</a></p>\n<p>其他博客示例:</p>\n<p><a href=\"https://antweiss.com/blog/extending-envoy-with-Wasm-and-rust/\">Extending Envoy with Wasm and Rust</a></p>\n<p><a href=\"https://blog.red-badger.com/extending-istio-with-rust-and-webassembly\">Extending Istio with Rust and WebAssembly</a></p>\n<h2>一点碎碎念</h2>\n<p>前段时间课题研究看了一段时间 eBPF 和它的虚拟机的移植,最近再来看 Wasm,感觉理解起来快了不少。之前只知道 Wasm 在浏览器端貌似非常成功,今天才知道它的虚拟机结合到了 Envoy 代理中提供非侵入式的插件功能,这实际上和 eBPF/XDP 之于 Linux 内核的角色类似。之前看了 CNCF 会的一则 <a href=\"https://www.youtube.com/watch?v=99jUcLt3rSk\">演讲</a>,演讲者预言:"未来十年的 Linux 属于 eBPF";希望能看到更多 Wasm 引导的技术上的进步。</p>\n<p>回想搁置掉的 eBPF 虚拟机课题,又在考虑它的实际应用场景。有看到过一些类似工作:往 Sel4 上移植 Wasm 虚拟机的小项目、在嵌入式平台移植 $\\mu$-JVM 的 RTOS 论文,但是似乎都是隔靴搔痒。为什么要移植这个虚拟机而不是另一个,到底优势在哪里,适用的场景是什么,还是很难去考量的。</p>\n<p>今天是 2022 年的新一天,希望新年里自己也能保持学习,莫要年末感叹马齿徒增。</p>\n<h2>最近的进展</h2>\n<h3>一个简单的 Envoy Wasm 插件示例</h3>\n<p>之前读了上面提到的利用 proxy-wasm 在 Envoy 里实现一个简单的插件的博客 <a href=\"https://antweiss.com/blog/extending-envoy-with-Wasm-and-rust/\">Extending Envoy with Wasm and Rust</a>,实际部署了一下,作者原始的 demo 项目有些依赖比较老了,现在 Envoy 官方的镜像已经可以正常使用了,稳定版 Rust 工具链也可以正常编译出 wasm 字节码,所以重新调试后发布在这个<a href=\"https://github.com/Forsworns/proxy-wasm-rust\">项目</a>。</p>\n<h3>多线程方面的探索调研</h3>\n<p>有了三面那个插件的 demo。我在 Sentinel-Rust 中也尝试了向 WASM target 平台的编译,折腾了半天倒是编译成功了。但是却始终无法集成到上面的 demo 中。一方面是 Sentinel 在实现的时候启动了大量的线程,但是 Wasm 的运行时是单线程的;另一方面是 proxy-wasm 的简洁来源于网关那边暴露的接口,抛开这些标准的接口,向网关的 wasm 插件中添加支持 Wasm 的一些 crate,例如 <code>rand</code>, <code>uuid</code> 都十分困难,和浏览器端使用 <code>wasm-pack</code> 直接打包又不同。</p>\n<p>浏览器端 wasm 是可以使用多线程的,在转换成 js 的时候可以使用 Web Worker 和 SharedArrayBuffer:</p>\n<ul>\n<li>\n<p><a href=\"https://github.com/w3reality/wasm-mt\">wasm-mt</a> 封装了创建 Worker 的过程。</p>\n</li>\n<li>\n<p><a href=\"https://github.com/GoogleChromeLabs/wasm-bindgen-rayon\">wasm-bindgen-rayon</a> 同样通过 Worker 实现了对 <code>rayon</code> 的支持。</p>\n</li>\n<li>\n<p><a href=\"https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html\">Parallel Raytracing</a> 中 <code>wasm-bindgen</code> 提供了直接使用 Worker 自己封装一个线程池的示例,用了一个 Rust 实现的光追算法构建了 Web 应用。</p>\n</li>\n</ul>\n<h3>文件读写方面的探索调研</h3>\n<p>直接把 Sentinel-Rust 集成到 prxoy-wasm 的 demo 里编译成 wasm 字节码再插入到 Envoy 中估计是走不通了。和小伙伴讨论了一下有没有什么曲线救国的办法,提到了想一想进程间通信的方法,像共享内存、MQ、套接字这些。</p>\n<p>但是很可惜,由于 wasm 本身在封闭沙箱里运行有自己独立的内存地址空间(是一个简单的单地址空间)、不支持多线程、网络通讯是由 host 提供的(参见 <a href=\"https://github.com/WebAssembly/design/issues/1251\">WebAssembly/design/#1251</a>)原因,在 Envoy 里嵌入的运行时应该都做不到,浏览器或者 <code>wasmtime</code> 这种可能还可行。</p>\n<p>由于感觉和 eBPF 很相似,考虑有没有像 eBPF/XDP 的 <code>pin/unpin</code> 特殊文件一样:在 prxoy-wasm 的程序里,收到流量就去写一个文件,在另一个进程里去读这个文件并创建 Sentinel,查了一下,果然是有这样的机制,可以参考</p>\n<ul>\n<li>\n<p><a href=\"https://stackoverflow.com/questions/45535301/can-i-read-files-from-the-disk-by-using-webassembly\">StackOverflow QA</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/emscripten-core/emscripten/blob/main/tests/asmfs/fopen_write.cpp\">Emscripten 的示例</a></p>\n</li>\n</ul>\n<p>但是看上去要用 Emscripten 的 emmc,生成 js 代码,估计和浏览器端的 wasm 多线程一样又是依赖于 js 的一些 feature,用 cargo 生成 wasm 字节码不知道咋样,可能需要去看看 <a href=\"https://wasi.dev/\">WASI Standard</a> 。</p>\n<p>但是就算可以跑通,又有别的问题,这个文件是否可以映射到内存去(比如有可能使用 <code>mmap</code> 调用)、怎么在进程间同步(类 AOF 文件似乎也不太合适,清理起过期的数据也困难)。</p>\n<h2>Sentinel-Rust 相关资源</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">使用指南</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API 文档</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">示例代码</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20220101/",
"title": "WebAssembly in Envoy",
"summary": "为 Sentinel-Rust 增加 Envoy 插件做的调研",
"date_modified": "2022-01-01T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>工业物联网通讯笔记</h1>\n<h1>EtherCAT</h1>\n<p><a href=\"D:/aiit/ethercat/ethercat_esc_datasheet_sec1_technology_2i3.pdf\">专有缩写</a></p>\n<p>思路:使用已有的网络连接透明传输构造EtherCAT帧的报文段。</p>\n<h2>IGH EtherCAT 开源主站安装及测试</h2>\n<p>参考 <a href=\"https://zhuanlan.zhihu.com/p/150957429\">知乎专栏</a></p>\n<p>安装好后可以看到一系列参数<br>\n<img src=\"./ethercat_tool.png\" alt=\"img\"></p>\n<p>如果开机没自动起来需要手动<code>/etc/init.d/ethercat start</code>。</p>\n<h2>基于 EtherCAT 的应用</h2>\n<p><img src=\"./application_layer_protocols.png\" alt=\"img\"></p>\n<h2>常见 ESC 设备</h2>\n<p><img src=\"./ethercat_communication_modules.png\" alt=\"img\"></p>\n<h2>EtherCAT 主从环形架构</h2>\n<p><img src=\"./EthercatOperatingPrinciple.svg\" alt=\"\"></p>\n<p><img src=\"./Architecture.png\" alt=\"\"></p>\n<h2>帧结构</h2>\n<p><img src=\"./ethercat_frame_datagram.png\" alt=\"\"></p>\n<h2>状态机</h2>\n<p><img src=\"./ethercat_state_machine_ESM.png\" alt=\"\"></p>\n<p>下方介绍的 SOES 库在 <code>esc.h</code> 中定义了状态和状态的转移:</p>\n<pre><code class=\"language-C\">#define ESCinit 0x01\n#define ESCpreop 0x02\n#define ESCboot 0x03\n#define ESCsafeop 0x04\n#define ESCop 0x08\n#define ESCerror 0x10\n\n#define INIT_TO_INIT 0x11\n#define INIT_TO_PREOP 0x21\n#define INIT_TO_BOOT 0x31\n#define INIT_TO_SAFEOP 0x41\n#define INIT_TO_OP 0x81\n#define PREOP_TO_INIT 0x12\n#define PREOP_TO_PREOP 0x22\n#define PREOP_TO_BOOT 0x32\n#define PREOP_TO_SAFEOP 0x42\n#define PREOP_TO_OP 0x82\n#define BOOT_TO_INIT 0x13\n#define BOOT_TO_PREOP 0x23\n#define BOOT_TO_BOOT 0x33\n#define BOOT_TO_SAFEOP 0x43\n#define BOOT_TO_OP 0x83\n#define SAFEOP_TO_INIT 0x14\n#define SAFEOP_TO_PREOP 0x24\n#define SAFEOP_TO_BOOT 0x34\n#define SAFEOP_TO_SAFEOP 0x44\n#define SAFEOP_TO_OP 0x84\n#define OP_TO_INIT 0x18\n#define OP_TO_PREOP 0x28\n#define OP_TO_BOOT 0x38\n#define OP_TO_SAFEOP 0x48\n#define OP_TO_OP 0x88\n</code></pre>\n<p><img src=\"./ESM_transitions.png\" alt=\"\"></p>\n<h2>EtherCAT 取址</h2>\n<p><img src=\"./address.png\" alt=\"\"><br>\n<img src=\"./address_fmmu.png\" alt=\"\"></p>\n<h1>SOES</h1>\n<p>开源 EtherCAT Slave 实现,对应有 SOEM 是 EtherCAT Master 实现。<br>\n通过配置文件配置 ESC,之后通过 <code>ESC_read</code> 和 <code>ESC_write</code> 与 ESC 交互数据,由 ESC 负责接收发送。这两个函数是硬件相关的,需要针对硬件实现(参考 <code>hal/linux-lan9252</code> 对 lan9252 的实现,<code>rt-kernel-xmc4/esc_hw.h</code> 中 <code>esc_registers</code> 定义,把指针 <code>esc_registers_t * ecat0</code> 指向了 <code>ECAT0_BASE</code>,即寄存器的起始地址)。</p>\n<h2>SOES 库工作内容</h2>\n<ul>\n<li>ESC(EtherCAT Slave Controller)硬件初始化\n<ul>\n<li>ESC 重置</li>\n<li>ESC 初始化,init SPI</li>\n<li>等待 ESC 初始化成功(轮询 ESC 的 DL 寄存器)</li>\n</ul>\n</li>\n<li>软件初始化\n<ul>\n<li>重置 Slave 状态(覆写 ESC AL 状态寄存器)</li>\n<li>重置错误信息 (清除 ESC AL 错误码寄存器)</li>\n<li>中止之前的应用层程序(可能有 SyncManager 在 block,等待接收EtherCAT 包)</li>\n</ul>\n</li>\n<li>应用\n<ul>\n<li>应用层事件(ALevent)处理,ALevent 携带了 ALControl 或 SyncManagers 的更改信息。ALControl 是用来控制状态改变的,SyncManagers 是用来将 EtherCAT 的改动写入到本地内存中的。</li>\n<li>ESC_state 用来处理状态,例如状态变化、错误处理、告知接收到信息。</li>\n<li>Mailbox handler,提供应用层协议使用的 mailboxes。</li>\n<li>在 mailbox 中,也会检查是否需要使用特定协议的 handler 来处理接收/发送的数据。</li>\n</ul>\n</li>\n</ul>\n<h2>SOES 库设计</h2>\n<h3><code>ecat_slv.c</code> 实现 slave API</h3>\n<h4>全局变量</h4>\n<ul>\n<li>定义全局变量 <code>_ESCvar</code> 类型(定义在 <code>esc.h</code> 中)的 <code>ESCvar</code>,负责存储 ESC 状态信息。</li>\n<li>全局变量 <code>MBX</code> 是 Mailbox,存储 <code>MBXBUFFERS * MAX(MBXSIZE,MBXSIZEBOOT)</code> 规模的 <code>uint8_t</code> 数据;<code>_MBXcontrol</code> 则是 Mailbox 对应的 Controller。</li>\n<li>全局变量 <code>_SMmap</code> 类型的 <code>SMmap2</code> 和 <code>SMmap3</code> 分别映射输出、输入的 SM(SyncManager)。</li>\n</ul>\n<h4>初始化</h4>\n<p><code>ecat_slv_init</code> 接收 <code>esc_cfg_t</code> 类型的设置选项进行初始化。</p>\n<h4>应用处理</h4>\n<p><code>ecat_slv</code> 是一个需要周期性调用的函数,它内部会调用 ecat_slv_poll 和 DIG_process。前者 poll EtherCAT event,检查状态机,检查 SM,检查 Mailbox,如果收到数据就根据编译选项依次检查是否为 CoE(CAN over EtherCAT)、EoE(EtherNet over EtherCAT)、FoE (File over EtherCAT),最后检查是否为 XoE(错误的报文)应用,视需要处理 eeprom;后者更新局部变量,读入收到的 EtherCAT 帧,写出发送帧。</p>\n<h4>应用相关</h4>\n<p><code>DIG_process</code> 会阅读、修改 <code>ESCvar</code>。首先检查是否处于可以修改 Output 信息的的状态下,即 Operational state。</p>\n<h5>Output:ESC 视角的 Ouput,即用户态的接收</h5>\n<ul>\n<li>如果我们在 OP 状态下,我们能够阅读 3-buffer SM 中被映射到输出的 SM 中的 PDO(Process Data Objects)数据, 默认的是 SyncManager2。我们阅读 SM2 的 ESC RAM 地址,存储到本地。我们将会读入 RXPDOsize bytes 的数据来触发一个完整的 SM 读操作。</li>\n<li>在局部变量被更新后,我们将本地的 PDO 变量传递到用户应用中。</li>\n<li>这个函数也包含了 watchdog 机制的实现,如果触发了它,会关闭输出,状态机改变到 Safe Operational。会更新 AlError 信息,告知 Master 发生了错误。</li>\n</ul>\n<h5>Input:ESC 视角的 Input,即用户态的发送</h5>\n<ul>\n<li>和输出类似,是反过来的,但是更加简单。即使是在 Safe Operational 状态下也会继续更新输入信息。</li>\n<li>首先阅读用户应用数据,写入到本地的 PDO 变量中。在局部变量刷新后,把他们写入到 Input 对应的 SM,一般是 SyncManager3。这样就可以使用用户应用数据更新 ESC 的 RAM了。</li>\n</ul>\n<h3>特别需要注意</h3>\n<p>在实现应用的时候,必须自己定义回调函数 <code>cb_get_inputs</code> 和 <code>cb_get_outputs</code>,这两者声明在 <code>ecat_slv.h</code> 中,会在 <code>DIG_process</code> 中获取输入和输出时分别触发。</p>\n<p><code>esc_cfg_t</code> 设置项中也有定义不同的hook,是可选的。</p>\n<h3><code>eep.c</code></h3>\n<p>ESI EEPROM 模拟模块。</p>\n<h3><code>esc_coe.c</code></h3>\n<h3><code>esc_eoe.c</code></h3>\n<p>ESI EEPROM 模拟模块。</p>\n<h3><code>esc_foe.c</code></h3>\n<p>ESI EEPROM 模拟模块。</p>\n<h3><code>eep.c</code></h3>\n<p>ESI EEPROM 模拟模块。</p>\n<h3><code>esc.c</code></h3>\n<p>全局变量:<br>\n<code>ESC_MBX1_sma</code> sm address?(参考<code>ESC_write (ESC_MBX1_sma, MBh, ESC_MBXHSIZE + length);</code>)<br>\n<code>ESC_MBX1_sml</code> sm length?<br>\n<code>ESC_MBX1_sme</code> sm end?<br>\n<code>ESC_MBX1_smc</code> sm controller?</p>\n<p>函数:<br>\n<code>ESC_xoeprocess</code> 负责处理错误的帧。<br>\n<code>ESC_read</code> 写 ESC 寄存器。<br>\n<code>ESC_write</code> 写 ESC 寄存器。<br>\n<code>ESC_ALeventmaskread</code> 读 ALeventMask 寄存器。<br>\n<code>ESC_ALeventmaskwrite</code> 写 ALeventMask 寄存器。</p>\n<p><code>ESC_outreqbuffer</code> 从全局的<code>MBXcontrol</code>中寻找请求发送到 outbox 的mailbox 的下标。<br>\n<code>ESC_mbxprocess</code> 是实现 mailbox protocol 的,负责 mailbox 的读、发送、重传、mailbox full event 处理。<br>\n<code>ESC_writembx</code>将 <code>esc_slv.c</code> 中的全局变量 MBX 中 <code>ESC_outreqbuffer</code> 查询到的 mailbox 发送出去。</p>\n<p>mailbox 的状态:</p>\n<ul>\n<li>0 : idle</li>\n<li>1 : claimed for inbox</li>\n<li>2 : claimed for outbox</li>\n<li>3 : request post outbox</li>\n<li>4 : outbox posted not send</li>\n<li>5 : backup outbox</li>\n<li>6 : mailbox needs to be transmitted again<br>\n分别对应宏</li>\n</ul>\n<pre><code class=\"language-C\">#define MBXstate_idle 0x00\n#define MBXstate_inclaim 0x01\n#define MBXstate_outclaim 0x02\n#define MBXstate_outreq 0x03\n#define MBXstate_outpost 0x04\n#define MBXstate_backup 0x05\n#define MBXstate_again 0x06\n</code></pre>\n<h3><code>options.h</code></h3>\n<p>默认的宏定义。用户程序可以通过定义 <code>ecat_options.h</code> 覆盖他们。</p>\n<h2>SOES 样例</h2>\n<p>目录下的 <code>rtl_slave_demo</code> 是一个简短的led亮灯示例。在这个例子中,SOES 相关 API 的应用例子封装在了 <code>void soes (void *arg)</code> 函数中通过一个线程执行,另一个线程去读取 ESCvar.ALstatus 状态和 ESCvar.ALerror 错误码,根据状态机的状态和错误码来点亮 LED,设定闪烁频次。</p>\n<p><code>rtl_lwip_eoe</code> 是一个基于 lwip 的 EtherNet over EtherCAT 示例。<code>mbox_fetch_tmo/mbox_post_tmo</code> 是带 timeout(tmo)的 mailbox fetch/post API。</p>\n<h2>SOES 中的一些数据规定</h2>\n<h3>SII-PDO 和 ESI-PDO</h3>\n<p>为了通过总线获取 SII-EEPROM(Slave Information Interface)和 ESI(EtherCAT Slave Information, 存储在 SII-EEPROM),规定了相关数据结构。其中:必须的参数,mandatory 用 M 代表;可选的参数,optional 用 O 代表。</p>\n<p><img src=\"./pdo1.png\" alt=\"img\"></p>\n<p><img src=\"./pdo2.png\" alt=\"img\"></p>\n<h2>OD</h2>\n<p>同时,对于比较复杂的 Slave 还可以定义可选的 Object Dictionary(OD)。可以在 CoE 中使用它,它遵循 CANopen DS301。</p>\n<ul>\n<li>0x0000 - 0x0FFF, Data Type Area</li>\n<li>0x1000 - 0x1FFF, Communication Area\n<ul>\n<li>RxPDO , 0x1600 - 0x17FF</li>\n<li>TxPDO , 0x1A00 - 0x1BFF</li>\n</ul>\n</li>\n<li>0x2000 - 0x5FFF, Manufacture specific area</li>\n<li>0x6000 - 0x6FFF, Input area</li>\n<li>0x7000 - 0x7FFF, Output area</li>\n<li>0x8000 - 0x8FFF, Configuration area</li>\n<li>0x9000 - 0x9FFF, Information area</li>\n<li>0xA000 - 0xAFFF, Diagnosis Area</li>\n<li>0xB000 - 0xBFFF, Service Transfer Area</li>\n<li>0xC000 - 0xEFFF, Reserved Area</li>\n<li>0xF000 - 0xFFFF, Device Area</li>\n</ul>\n<h3>SM 事件类型</h3>\n<ul>\n<li>0, Unused</li>\n<li>1, MailBox Receive, master to slave</li>\n<li>2, MailBox Send, slave to master</li>\n<li>3, Processdata output, master to slave</li>\n<li>4, Processdata input, slave to master</li>\n</ul>\n<h2>实现</h2>\n<p>HFA21 支持硬件offloading,上层透明。用透传来写的话,只需要在 UDP datagram 里去构造 EtherCAT 包,当然这样没有 ECS,而且在传输层之上,当作应用层协议来写了,也就不具备快速的on-the-fly能力了。</p>\n<p>为了方便做解析,这么写的(具体定义参考上面 EtherCAT 帧结构的图解)</p>\n<pre><code class=\"language-C\">#include <stdint.h>\n\n// gcc6 以上支持强制指定大小端(big-endian、little-endian、default)\n#define BIG_ENDIAN __attribute__((packed, scalar_storage_order("big-endian")))\n#define ETHERCAT_PORT 34980 // 0x88A4\n\ntypedef BIG_ENDIAN struct\n{\n uint16_t type : 4;\n uint16_t res : 1;\n uint16_t length : 11;\n} EcatHeader, *EcatHeaderPtr;\n\ntypedef BIG_ENDIAN union\n{\n BIG_ENDIAN struct\n {\n uint16_t position;\n uint16_t offset;\n } position; // Position Addressing\n BIG_ENDIAN struct\n {\n uint16_t address;\n uint16_t offset;\n } node; // Node Addressing\n uint32_t logical;\n} EcatAddress, *EcatAddressPtr;\n\ntypedef struct\n{\n uint8_t cmd;\n uint8_t idx;\n EcatAddress address;\n BIG_ENDIAN union\n {\n BIG_ENDIAN struct\n {\n uint16_t length : 11;\n uint16_t r : 3;\n uint16_t c : 1;\n uint16_t m : 1; // followed by more datagrams or not\n } s;\n uint16_t u; // for easy parse\n } suffix;\n uint16_t irq : 16;\n} EcatDataHeader, *EcatDataHeaderPtr;\n\ntypedef struct\n{\n EcatDataHeader header; // 10 bytes\n uint8_t *data; // 0-1486 bytes\n uint16_t work_counter; // 2bytes\n} EcatDatagram, *EcatDatagramPtr;\n\ntypedef struct\n{\n EcatHeader header;\n EcatDatagram datagram;\n} EcatData, *EcatDataPtr;\n</code></pre>\n<p>用 union 来包裹位域,否则要挨个解析,即依次将相同的数字赋值给占用不同位域的变量,而用了union只需要赋值给对应的 <code>EcatDataHeader.suffix.u</code>。整个强制指定为符合网络序的大端,否则需要手动处理主机序的转换。<br>\n比如,假设收到的帧中, <code>EcatDataHeader.suffix</code> 对应的值是0x12345678。<br>\n当前可以这么赋值:</p>\n<pre><code class=\"language-C\">EcatDataHeader header;\nuint16_t suffix = 0x1234;\nheader.suffix.u = suffix;\nprintf("%x,%x,%x,%x", header.suffix.s.length, header.suffix.s.r,header.suffix.s.c,header.suffix.s.m);\n</code></pre>\n<p>但是如果</p>\n<pre><code class=\"language-C\">typedef struct\n{\n uint8_t cmd;\n uint8_t idx;\n EcatAddress address;\n uint16_t length : 11;\n uint16_t r : 3;\n uint16_t c : 1;\n uint16_t m : 1; // followed by more datagrams or not\n uint16_t irq : 16;\n} EcatDataHeader, *EcatDataHeaderPtr;\n\nEcatDataHeader header;\nuint16_t suffix = 0x1234;\nheader.length = suffix;\nheader.r = suffix;\nheader.c = suffix;\nheader.m = suffix;\nprintf("%x,%x,%x,%x", header.length, header.r,header.c,header.m);\n</code></pre>\n<p>此时还会有大小端的问题,需要用 <code>htonl</code> 等转换。</p>\n<h1>S7 库</h1>\n<p>关于 S7,有几篇不错的博客:</p>\n<p><a href=\"http://gmiru.com/article/s7comm/\">The Siemens S7 Communication - Part 1 General Structure</a><br>\n<a href=\"http://gmiru.com/article/s7comm-part2/\">The Siemens S7 Communication - Part 2 Job Requests and Ack Data</a><br>\n<a href=\"https://www.jianshu.com/p/0e9f74d683b4\">上面两篇博客的翻译</a><br>\n<a href=\"http://blog.nsfocus.net/s7comm-readszl-0427/\">对 ReadSZL 的详解</a>,SZL 是系统状态列表(德语:System-ZustandsListen,英语:System Status Lists),用于描述PLC的当前状态,只能读取不能修改。</p>\n<p>注意在 wireshark 中可以用 <code>s7comm</code> 或 <code>tcp.port == 102</code> 来过滤 S7 的包,但是前者只能在展示时起效,后者可以在过滤时起效。</p>\n<p>该库是 Siemens 给自家 PLC 写的通讯库,使用时需要指定 IP、port、Rack,Slot。</p>\n<p>具体地:</p>\n<p>建立连接是用的TSnap7Peer的PeerConnect函数,调用了TIsoTcpSocket的 isoConnect。</p>\n<p>s7_isotcp.cpp<br>\n里面定义了TIsoTcpSocket的构造函数,设置的Timeout是3000,tcp port是用的102端口。(Rack默认是0,Slot默认是2,我们的设备Slot需要设置成1。)。</p>\n<p>发送过去的载荷是在TIsoTcpSocket 的 BuildControlPDU 里构造的 FControlPDU。</p>\n<p>Client 的具体的 Operation 都是通过 TSnap7Job 结构体代理的,定义在 s7_micro_client 中。在PerformOperation() 被调用后,就会填充它。</p>\n<p>填充方法是,比如TSnap7MicroClient::opGetOrderCode()中,在opReadSZL()中写入到TS7Buffer opData里将 void* 的TSnap7Job::pData 转换成目标类型的.。然后再从这个 TS7Buffer 里读取出来复制到TSnap7Job::pData的各个成员里。</p>\n<p>opReadMultiVars/opWriteMultiVars 会自动忽略掉Area不是DB的 DBNumber。把请求的DBNumber都填充到ReqParams里,随着首部PS7ReqHeader TSnap7Peer::PDUH_out 作为PDU的数据单元。</p>\n<p>opDBGet/opDBFill 调用了 opReadArea/opWriteArea。</p>\n<p>所有的operation都是通过PerformOperation()去管理的,而每个operation中,都是通过TIsoTcpSocket::isoExchangeBuffer来完成的发送和接收,这个函数可以接收data来修改发送出去的PDU.payload,如果接收了空指针(一半都会传入0),则会使用默认的 PDU.payload。</p>\n<p>接收到的数据会存储到ResData中,并复制到Target结束(Target是Job.pData偏移后 byte 类型的指针)。</p>\n<p>主要类的继承关系:</p>\n<p><img src=\"s7socket.png\" alt=\"img\"></p>\n<p><img src=\"s7server.png\" alt=\"img\"></p>\n<p>测试情况:</p>\n<p><img src=\"./multiread.png\" alt=\"img\"></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20211210/",
"title": "工业物联网通讯笔记",
"summary": "工业物联网通讯相关杂记:EtherCAT、S7",
"date_modified": "2021-12-10T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Do not use <code>#[serde(deny_unknown_fields)]</code> on k8s CRD struct</h1>\n<p>This blog records a potential problem in Rust when using <code>kube-rs</code>, <code>serde</code> and <code>schemars</code> together: Do not use <code>#[serde(deny_unknown_fields)]</code> on k8s CRD spec struct.</p>\n<p>Here is a minimal example: Simply add <code>#[serde(deny_unknown_fields)]</code> in the <code>kube-rs</code> official example.</p>\n<pre><code class=\"language-rust\">use schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse validator::Validate;\nuse futures::{StreamExt, TryStreamExt};\nuse k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;\nuse kube::{\n api::{Api, DeleteParams, ListParams, PatchParams, Patch, ResourceExt},\n core::CustomResourceExt,\n Client, CustomResource,\n runtime::{watcher, utils::try_flatten_applied, wait::{conditions, await_condition}},\n};\n\n// Our custom resource\n#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, Validate, JsonSchema)]\n#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]\n#[serde(deny_unknown_fields)] // here we add the macro\npub struct FooSpec {\n info: String,\n #[validate(length(min = 3))]\n name: String,\n replicas: i32,\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let client = Client::try_default().await?;\n let crds: Api<CustomResourceDefinition> = Api::all(client.clone());\n\n // Apply the CRD so users can create Foo instances in Kubernetes\n crds.patch("foos.clux.dev",\n &PatchParams::apply("my_manager"),\n &Patch::Apply(Foo::crd())\n ).await?;\n\n // Wait for the CRD to be ready\n tokio::time::timeout(\n std::time::Duration::from_secs(10),\n await_condition(crds, "foos.clux.dev", conditions::is_crd_established())\n ).await?;\n\n // Watch for changes to foos in the configured namespace\n let foos: Api<Foo> = Api::default_namespaced(client.clone());\n let lp = ListParams::default();\n let mut apply_stream = try_flatten_applied(watcher(foos, lp)).boxed();\n while let Some(f) = apply_stream.try_next().await? {\n println!("saw apply to {}", f.name());\n }\n Ok(())\n}\n</code></pre>\n<p>Then you will get an error:</p>\n<blockquote>\n<p>Error: Api(ErrorResponse { status: "Failure", message: "<a href=\"http://CustomResourceDefinition.apiextensions.k8s.io\">CustomResourceDefinition.apiextensions.k8s.io</a> "foos.clux.dev" is invalid: spec.validation.openAPIV3Schema.properties[spec].additionalProperties: Forbidden: additionalProperties and properties are mutual exclusive", reason: "Invalid", code: 422 })</p>\n</blockquote>\n<p><strong>Why?</strong></p>\n<p>Because in json schema <sup>[1]</sup>:</p>\n<blockquote>\n<p>By default, providing additional properties is valid (unless you set <code>additionalProperties</code> to false).</p>\n</blockquote>\n<p>While in <code>serde</code> <sup>[2]</sup>:</p>\n<blockquote>\n<p>Always error during deserialization when encountering unknown fields. When this attribute is not present, by default unknown fields are ignored for self-describing formats like JSON.</p>\n</blockquote>\n<p>The <code>schemars</code> is compatible with serde. There's no surprise that field <code>additionalProperties</code> is set to false when the struct is with <code>#[serde(deny_unknown_fields)]</code>.</p>\n<p>Then the "unexpected" problem with <code>kube-rs</code> looms. The generated CRD struct <code>Foo</code> will contain the spec struct <code>FooSpec</code> annotated with <code>#[serde(deny_unknown_fields)]</code>, which has an attribute <code>additionalProperties</code> of value <code>false</code>. This voilates the restrictions that applied to the CRD schema<sup>[3]</sup>:</p>\n<blockquote>\n<p>The field <code>additionalProperties</code> cannot be set to false. The field <code>additionalProperties</code> is mutually exclusive with properties.</p>\n</blockquote>\n<p>[1] <a href=\"http://json-schema.org/understanding-json-schema/reference/object.html#id5\">http://json-schema.org/understanding-json-schema/reference/object.html#id5</a></p>\n<p>[2] <a href=\"https://serde.rs/container-attrs.html#deny_unknown_fields\">https://serde.rs/container-attrs.html#deny_unknown_fields</a></p>\n<p>[3] <a href=\"https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation\">https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation</a></p>\n<h2>Sentinel-Rust Resources</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">Tutorial</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API Doc</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">Example Codes</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20211130/",
"title": "Do not use `#[serde(deny_unknown_fields)]` on k8s CRD struct",
"summary": "为 Sentinel-Rust 添加 k8s 数据源支持时,用 kube-rs 的时候碰到的一个有趣的问题",
"date_modified": "2021-11-30T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Sentinel Rust 的使用</h1>\n<p>Sentinel 是一个面向分布式服务架构的流量控制组件,它可以提升服务的稳定性。在 Sentinel 中,应用程序提供的服务或其调用的相关服务均可被视为资源,受到 Sentinel 的保护。围绕这些资源的实时状态,可以流量控制规则、熔断降级规则以及系统保护规则来避免请求大量堆积造成的系统瘫痪。Sentinel 可以被广泛地应用在各种应用场景下,与云原生也有着紧密的结合。</p>\n<center><img width=\"80%\" src=\"./upload_43398fc9372c5e85f5cc3eebd787a974.png\"></center>\n<center><b>Sentinel 的生态</b></center>\n<p>Sentinel 有着多年的发展历史,最初专注于入口流量控制,经过多年打磨后开源,并逐步提供多语言原生支持。</p>\n<center><img width=\"90%\" src=\"./upload_2d4c6aa91539fb69f517a2eb92acbf65.png\"></center>\n<center><b>Sentinel 的发展史</b></center>\n<p>本项目参考已有的 Java 与 Golang 版本,采用 Rust 开发 Sentinel 的原生版本,使用 Rust Attribute Macro 提供低侵入式的规则定义方法,支持使用 Prometheus 进行可视化监控,支持使用 etcd、Consul 等动态加载规则。</p>\n<center><img width=\"30%\" src=\"./upload_ea89f6b3e564c2ffe48d368df8a685a3.png\"></center>\n<center><b>Sentinel & Rust</b></center>\n<p>项目托管于 GitHub,链接:<a href=\"https://github.com/sentinel-group/sentinel-rust\">Sentinel-Rust</a>。在项目 <a href=\"https://github.com/sentinel-group/sentinel-rust/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97\">WiKi</a> 中提供了详细的文档,同时提供了大量的<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">示例程序</a>供参考。另外,可以在本地运行 <code>cargo doc</code> 生成 API 文档。</p>\n<h2>Sentinel 介绍</h2>\n<p>Sentinel Rust 的设计采用了<a href=\"https://refactoringguru.cn/design-patterns/chain-of-responsibility\">责任链</a>的模式,用户指定的各类规则会自动通过 <code>base::SlotChain</code> 上的插槽 (Slot) 进行检测。用户使用 Sentinel Rust (后文均用 Sentinel 表示 Sentinel Rust) ,主要需要以下几步:</p>\n<ol>\n<li>在项目配置中添加依赖,对 Sentinel 的运行环境进行相关配置并初始化。</li>\n<li>埋点(定义资源),确定系统中有哪些资源需要防护。</li>\n<li>配置规则,为每个资源配置具体的规则,规则的配置方法可参考各个模块的使用文档。</li>\n<li>编写资源防护的入口和出口代码。</li>\n</ol>\n<center><img width=\"50%\" src=\"./upload_2f3f7e31fd33a4f4437fc597c6297aa9.png\" ref=\"https://refactoring.guru/design-patterns/chain-of-responsibility\"></center>\n<center><b>责任链模式检测规则</b></center>\n<h2>使用示例</h2>\n<p>首先需要在项目中添加 Sentinel 依赖,向 <code>Cargo.toml</code> 中添加</p>\n<pre><code class=\"language-toml\">[dependencies]\nsentinel-rs = { version = "0.1.0", path = "path_to_sentinel" }\n</code></pre>\n<p>由于目前 sentinel 尚未发布,需要下载源码后,手动设置路径。如果需要使用 sentinel 的过程宏等可选特性,可以参考<a href=\"https://github.com/sentinel-group/sentinel-rust/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97#%E6%B7%BB%E5%8A%A0%E4%BE%9D%E8%B5%96\">可选特性列表</a>。</p>\n<h3>通用配置及初始化</h3>\n<p>使用 Sentinel 需要在应用启动时对 Sentinel 运行环境进行相关配置并触发初始化。<code>api</code> 下提供如下函数:</p>\n<ul>\n<li><code>init_default()</code>:从环境变量指定的配置文件以及环境变量中读取相应配置来初始化 Sentinel,若环境变量不存在则使用默认值。</li>\n<li><code>init_with_config_file(config_path: &mut String)</code>:从给定的 YAML 文件中读取相应配置来初始化 Sentinel。</li>\n<li><code>init_with_config(config_entity: ConfigEntity)</code>: 用户硬编码配置对象<code>ConfigEntity</code>来初始化 Sentinel。</li>\n</ul>\n<p>通用配置项加载策略和配置项请参考通用配置章节</p>\n<p>示例代码:</p>\n<pre><code class=\"language-rust\">use sentinel_rs::{init_default, logging};\ninit_default().unwrap_or_else(|err| logging::error!("{:?}", err));\n</code></pre>\n<p><strong>注意</strong>:必须成功调用 Sentinel 的初始化函数以后再调用埋点 API。</p>\n<h3>埋点(定义资源)</h3>\n<p>使用 Sentinel 的 Entry API 将业务逻辑封装起来,这一步称为“埋点”。每个埋点都有一个资源名称(resource),代表触发了这个资源的调用或访问。</p>\n<p>埋点 API 位于 <code>api</code> 中,通过构造 <code>EntryBuilder</code>,调用它的方法 <code>build()</code> 创建 Entry。 <code>EntryBuilder</code> 提供了链式的传参方式,未传入的参数将使用默认构造。</p>\n<p>若该次调用被拒绝,则 <code>build()</code> 会返回 <code>Result</code> 代表被 Sentinel 限流。BlockError 提供了限流原因以及触发的规则等信息,可以方便开发者获取相关信息进行记录和处理。</p>\n<h3>规则配置</h3>\n<h4>API 硬编码方式</h4>\n<p>Sentinel 支持原始的硬编码方式加载规则,可以通过各个模块的 <code>load_rules(rules)</code> 或 <code>append_rules(rules)</code> 函数加载规则,前者会覆盖之前的规则设置,后者只会向设置中追加规则。目前的版本中,这也是对单一资源加载多条规则的唯一手段。以流控规则为例:</p>\n<pre><code class=\"language-rust\">flow::load_rules(vec![Arc::new(flow::Rule {\n resource: "example".into(),\n threshold: 10.0,\n calculate_strategy: flow::CalculateStrategy::Direct,\n control_strategy: flow::ControlStrategy::Reject,\n ..Default::default()\n})]);\n</code></pre>\n<h4>标签宏硬编码方式</h4>\n<p>Sentinel 提供了易用的标签宏,可以帮助用户快速上手规则配置,我们为不同策略提供了丰富的标签宏使用 <a href=\"https://github1s.com/sentinel-group/sentinel-rust/tree/main/examples/\">示例</a>,也可以阅读后续文档了解。下面以流控规则为例</p>\n<pre><code class=\"language-rust\">#[flow(threshold=10.0, calculate_strategy=Direct)]\npub fn task() -> u32 {}\n</code></pre>\n<p>在上面的例子中,标签宏会修改 <code>task</code> 函数签名,返回 <code>Result<u32, String></code>。接着,它将自动向规则列表中追加规则,调用 <code>EntryBuilder</code> 创建 Sentinel Entry,检查指定的规则。如果该任务成功执行,会返回 <code>Ok(u32)</code> 类型的返回值;否则会返回 <code>Err(String)</code> 类型的限流原因和触发限流的参数。</p>\n<p>需要注意,当前的标签宏实现,仅支持在 Resource 上指定单一规则。</p>\n<h4>动态数据源</h4>\n<p>由 Sentinel 提供动态数据源接口进行扩展,用户可以动态地配置规则,参考使用 <a href=\"https://github1s.com/sentinel-group/sentinel-rust/blob/main/examples/datasources/etcdv3.rs\">etcd</a> 和 <a href=\"https://github1s.com/sentinel-group/sentinel-rust/blob/main/examples/datasources/consul.rs\">Consul</a> 进行配置的示例。</p>\n<h3>流量控制示例</h3>\n<p>流量控制 (Flow Control) 模块,基于令牌桶 (Token Bucket) 的思想,监控资源 (Resource) 的统计指标,然后根据 Token 计算策略来计算资源的可用 Token (也就是流量的阈值),然后根据流量控制策略对请求进行控制,避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。</p>\n<p>Sentinel 通过定义流控规则来实现对 Resource 的流量控制。Sentinel 内部会在加载流控规则(<code>flow::Rule</code>)时候,将每个规则转换成流量控制器 (<code>flow::TrafficShapingController</code>)。 每个流量控制器实例都会有自己独立的统计结构,这里统计结构是一个滑动窗口。Sentinel 内部会尽可能复用 Resource 级别的全局滑动窗口,如果流控规则的统计设置没法复用 Resource 的全局统计结构,那么Sentinel会为流量控制器创建一个全新的私有的滑动窗口,然后通过 <code>flow::StandaloneStatSlot</code> 这个统计 Slot 来维护统计指标。</p>\n<p>Sentinel 的流量控制组件对 Resource 的检查结果要么通过,要么会 block,对于 block 的流量相当于拒绝。</p>\n<p>下面展示两个例子,第一个例子通过 API 加载流控规则;第二个例子通过标签宏追加规则。</p>\n<h4>API 加载流控规则</h4>\n<pre><code class=\"language-rust\">use sentinel_rs::utils::sleep_for_ms;\nuse sentinel_rs::{base, flow, EntryBuilder};\nuse std::sync::Arc;\n\nfn main() {\n // Init sentienl configurations\n sentinel_rs::init_default().unwrap_or_else(|err| sentinel_rs::logging::error!("{:?}", err));\n let resource_name = String::from("direct_reject_flow_control_example");\n\n // Load sentinel rules\n flow::load_rules(vec![Arc::new(flow::Rule {\n resource: resource_name.clone(),\n threshold: 10.0,\n calculate_strategy: flow::CalculateStrategy::Direct,\n control_strategy: flow::ControlStrategy::Reject,\n ..Default::default()\n })]);\n let mut handlers = Vec::new();\n for _ in 0..20 {\n let res_name = resource_name.clone();\n handlers.push(std::thread::spawn(move || {\n loop {\n let entry_builder = EntryBuilder::new(res_name.clone())\n .with_traffic_type(base::TrafficType::Inbound);\n if let Ok(entry) = entry_builder.build() {\n // Passed, wrap the logic here.\n task();\n // Be sure the entry is exited finally.\n entry.borrow().exit()\n } else {\n // Blocked. We could get the block reason from the BlockError.\n sleep_for_ms(rand::random::<u64>() % 10);\n }\n }\n }));\n }\n for h in handlers {\n h.join().expect("Couldn't join on the associated thread");\n }\n}\n\n\nfn task() {\n println!("{}: passed", sentinel_rs::utils::curr_time_millis());\n sleep_for_ms(10);\n}\n</code></pre>\n<p>执行 <code>cargo run --example hello_world</code>,QPS 会被限制在 10。</p>\n<h4>标签宏追加流控规则</h4>\n<pre><code class=\"language-rust\">use sentinel_macros::flow;\nuse sentinel_rs::utils::sleep_for_ms;\n\nfn main() {\n // Init sentienl configurations\n sentinel_rs::init_default().unwrap_or_else(|err| sentinel_rs::logging::error!("{:?}", err));\n\n let mut handlers = Vec::new();\n for _ in 0..20 {\n handlers.push(std::thread::spawn(move || {\n loop {\n task().unwrap_or_else(|_| {\n // blocked\n sleep_for_ms(10);\n });\n }\n }));\n }\n for h in handlers {\n h.join().expect("Couldn't join on the associated thread");\n }\n}\n\n#[flow(\n traffic_type = "Outbound",\n calculate_strategy = "Direct",\n threshold = 10.0\n)]\nfn task() {\n println!("{}: passed", sentinel_rs::utils::curr_time_millis());\n sleep_for_ms(10);\n}\n</code></pre>\n<p>执行 <code>cargo run --example macro</code>,QPS 会被限制在 10。</p>\n<h3>Prometheus 监控</h3>\n<p>分别执行</p>\n<pre><code class=\"language-bash\">prometheus --config.file=./sentinel-rust/examples/exporter/prometheus/prometheus.yml\n</code></pre>\n<p>和</p>\n<pre><code class=\"language-bash\">cargo run --example prometheus --features="full exporter"\n</code></pre>\n<center><img width=\"100%\" src=\"./upload_f4fc50373099419ef83c0bd10769c9a3.png\" ref=\"https://refactoring.guru/design-patterns/chain-of-responsibility\"></center>\n<center><b>Prometheus 监控</b></center>\n<h3>K8S 动态添加流控规则</h3>\n<p>本地测试时分别执行</p>\n<pre><code class=\"language-bash\">prometheus --config.file=./sentinel-rust/examples/exporter/prometheus/prometheus.yml\n</code></pre>\n<p>和</p>\n<pre><code class=\"language-bash\">cargo run --example k8s --features="full ds_k8s async" -- --nocapture\n</code></pre>\n<p>将会看到动态加载的流控规则。执行</p>\n<pre><code class=\"language-bash\">kubectl api-resources\n</code></pre>\n<pre><code class=\"language-bash\">kubectl get flowresources -A\n</code></pre>\n<p>可以查询到相应的 sentinel flow rule resources:</p>\n<pre><code>NAME APIVERSION NAMESPACED KIND\nflowresources rust.datasource.sentinel.io/v1alpha1 true FlowResource\n</code></pre>\n<pre><code>NAMESPACE NAME AGE\ndefault flow-1 40s\n</code></pre>\n<h3>更多示例</h3>\n<p>项目中的 <code>example</code> 目录提供了大量示例程序和参数设置参考。</p>\n<h2>Sentinel-Rust 相关资源</h2>\n<p><a href=\"https://github.com/sentinel-group/sentinel-rust/wiki\">使用指南</a><br>\n<a href=\"https://docs.rs/sentinel-core/latest/sentinel_core/\"> API 文档</a><br>\n<a href=\"https://github.com/sentinel-group/sentinel-rust/tree/main/examples\">示例代码</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20211120/",
"title": "Sentinel Rust 介绍",
"summary": "Sentinel-Rust 介绍和示例",
"date_modified": "2021-11-20T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>转载自 <a href=\"https://www.zhihu.com/column/rdmatechnology\">RDMA</a> 知乎专栏。</p>\n<h1>概述</h1>\n<h2>1. DMA和RDMA概念</h2>\n<h2>1.1 DMA</h2>\n<p>DMA(直接内存访问)是一种能力,允许在计算机主板上的设备直接把数据发送到内存中去,数据搬运不需要CPU的参与。</p>\n<p>传统内存访问需要通过CPU进行数据copy来移动数据,通过CPU将内存中的Buffer1移动到Buffer2中。DMA模式:可以同DMA Engine之间通过硬件将数据从Buffer1移动到Buffer2,而不需要操作系统CPU的参与,大大降低了CPU Copy的开销。</p>\n<p><img src=\"./v2-d359453c9269146cd93de5eed43993c8_720w.jpg\" alt=\"img\"></p>\n<h2>1.2 RDMA</h2>\n<p>RDMA是一种概念,在两个或者多个计算机进行通讯的时候使用DMA, 从一个主机的内存直接访问另一个主机的内存。</p>\n<p><img src=\"./v2-f081e8fce13d8b00e5a786399d20ca06_720w.jpg\" alt=\"img\"></p>\n<p>RDMA是一种host-offload, host-bypass技术,允许应用程序(包括存储)在它们的内存空间之间直接做数据传输。具有RDMA引擎的以太网卡(RNIC)--而不是host--负责管理源和目标之间的可靠连接。使用RNIC的应用程序之间使用专注的QP和CQ进行通讯:</p>\n<ol>\n<li>每一个应用程序可以有很多QP和CQ</li>\n<li>每一个QP包括一个SQ和RQ</li>\n<li>每一个CQ可以跟多个SQ或者RQ相关联</li>\n</ol>\n<p><img src=\"./v2-2ec811510b13787ec81a3490e4233f60_720w.jpg\" alt=\"img\"></p>\n<h2>2. RDMA的优势</h2>\n<p>传统的TCP/IP技术在数据包处理过程中,要经过操作系统及其他软件层,需要占用大量的服务器资源和内存总线带宽,数据在系统内存、处理器缓存和网络控制器缓存之间来回进行复制移动,给服务器的CPU和内存造成了沉重负担。尤其是网络带宽、处理器速度与内存带宽三者的严重"不匹配性",更加剧了网络延迟效应。</p>\n<p>RDMA是一种新的直接内存访问技术,RDMA让计算机可以直接存取其他计算机的内存,而不需要经过处理器的处理。RDMA将数据从一个系统快速移动到远程系统的内存中,而不对操作系统造成任何影响。</p>\n<p>在实现上,RDMA实际上是一种智能网卡与软件架构充分优化的远端内存直接高速访问技术,通过将RDMA协议固化于硬件(即网卡)上,以及支持Zero-copy和Kernel bypass这两种途径来达到其高性能的远程直接数据存取的目标。 使用RDMA的优势如下:</p>\n<ul>\n<li>零拷贝(Zero-copy) - 应用程序能够直接执行数据传输,在不涉及到网络软件栈的情况下。数据能够被直接发送到缓冲区或者能够直接从缓冲区里接收,而不需要被复制到网络层。</li>\n<li>内核旁路(Kernel bypass) - 应用程序可以直接在用户态执行数据传输,不需要在内核态与用户态之间做上下文切换。</li>\n<li>不需要CPU干预(No CPU involvement) - 应用程序可以访问远程主机内存而不消耗远程主机中的任何CPU。远程主机内存能够被读取而不需要远程主机上的进程(或CPU)参与。远程主机的CPU的缓存(cache)不会被访问的内存内容所填充。</li>\n<li>消息基于事务(Message based transactions) - 数据被处理为离散消息而不是流,消除了应用程序将流切割为不同消息/事务的需求。</li>\n<li>支持分散/聚合条目(Scatter/gather entries support) - RDMA原生态支持分散/聚合。也就是说,读取多个内存缓冲区然后作为一个流发出去或者接收一个流然后写入到多个内存缓冲区里去。</li>\n</ul>\n<p>在具体的远程内存读写中,RDMA操作用于读写操作的远程虚拟内存地址包含在RDMA消息中传送,远程应用程序要做的只是在其本地网卡中注册相应的内存缓冲区。远程节点的CPU除在连接建立、注册调用等之外,在整个RDMA数据传输过程中并不提供服务,因此没有带来任何负载。</p>\n<h2>3. RDMA 三种不同的硬件实现</h2>\n<p>RDMA作为一种host-offload, host-bypass技术,使低延迟、高带宽的直接的内存到内存的数据通信成为了可能。目前支持RDMA的网络协议有:</p>\n<ol>\n<li>InfiniBand(IB): 从一开始就支持RDMA的新一代网络协议。由于这是一种新的网络技术,因此需要支持该技术的网卡和交换机。</li>\n<li>RDMA过融合以太网(RoCE): 即RDMA over Ethernet, 允许通过以太网执行RDMA的网络协议。这允许在标准以太网基础架构(交换机)上使用RDMA,只不过网卡必须是支持RoCE的特殊的NIC。RoCEv1 是数据链路层的协议,只支持同一广播域内的节点通信;RoCEv2 基于 UDP,同时支持 IPv4 和 IPv6,默认使用 4791 端口。RoCEv2 正在成为事实标准。</li>\n<li>互联网广域RDMA协议(iWARP): 即RDMA over TCP, 允许通过TCP执行RDMA的网络协议。这允许在标准以太网基础架构(交换机)上使用RDMA,只不过网卡要求是支持iWARP(如果使用CPU offload的话)的NIC。否则,所有iWARP栈都可以在软件中实现,但是失去了大部分的RDMA性能优势。</li>\n</ol>\n<p><img src=\"./v2-e854577d2b1fb56889c95d76999d6583_720w.jpg\" alt=\"img\"></p>\n<p>在三种主流的RDMA技术中,可以划分为两大阵营。一个是IB技术, 另一个是支持RDMA的以太网技术(RoCE和iWARP)。其中, IBTA力挺的技术自然是IB和RoCE, Mellanox公司(一个以色列人搞的小公司)是这方面的急先锋。而iWARP则是IEEE/IETF力挺的技术,主要是Chelsio公司在推进。RoCE和iWARP的争论,请参考Mellanox和Chelsio这两家公司发布的白皮书。</p>\n<p>在存储领域,支持RDMA的技术早就存在,比如SRP(SCSI RDMA Protocol)和iSER(iSCSI Extensions for RDMA)。 如今兴起的NVMe over Fabrics如果使用的不是FC网络的话,本质上就是NVMe over RDMA。 换句话说,NVMe over InfiniBand, NVMe over RoCE和NVMe over iWARP都是NVMe over RDMA。</p>\n<h2>4. RDMA基本术语</h2>\n<h2>4.1 Fabric</h2>\n<pre><code>A local-area RDMA network is usually referred to as a fabric.\n</code></pre>\n<p>所谓Fabric,就是支持RDMA的局域网(LAN)。</p>\n<h2>4.2 CA(Channel Adapter)</h2>\n<pre><code>A channel adapter is the hardware component that connects a system to the fabric.\n</code></pre>\n<p>CA是Channel Adapter(通道适配器)的缩写。那么,CA就是将系统连接到Fabric的硬件组件。 在IBTA中,一个CA就是IB子网中的一个终端结点(End Node)。分为两种类型,一种是HCA, 另一种叫做TCA, 它们合称为xCA。其中, HCA(Host Channel Adapter)是支持"verbs"接口的CA, TCA(Target Channel Adapter)可以理解为"weak CA", 不需要像HCA一样支持很多功能。 而在IEEE/IETF中,CA的概念被实体化为RNIC(RDMA Network Interface Card), iWARP就把一个CA称之为一个RNIC。</p>\n<p><strong>简言之,在IBTA阵营中,CA即HCA或TCA; 而在iWARP阵营中,CA就是RNIC。 总之,无论是HCA、 TCA还是RNIC,它们都是CA, 它们的基本功能本质上都是生产或消费数据包(packet)</strong></p>\n<h2>4.3 Verbs</h2>\n<p>在RDMA的持续演进中,有一个组织叫做OpenFabric Alliance所做的贡献可谓功不可没。 Verbs这个词不好翻译,大致可以理解为访问RDMA硬件的“一组标准动作”。 每一个Verb可以理解为一个Function。</p>\n<h2>5. 核心概念</h2>\n<h2>5.1 Memory Registration(MR) | 内存注册</h2>\n<p>RDMA 就是用来对内存进行数据传输。那么怎样才能对内存进行传输,很简单,注册。 因为RDMA硬件对用来做数据传输的内存是有特殊要求的。</p>\n<ul>\n<li>在数据传输过程中,应用程序不能修改数据所在的内存。</li>\n<li>操作系统不能对数据所在的内存进行page out操作 -- 物理地址和虚拟地址的映射必须是固定不变的。</li>\n</ul>\n<p>注意无论是DMA或者RDMA都要求物理地址连续,这是由DMA引擎所决定的。 那么怎么进行内存注册呢?</p>\n<ul>\n<li>创建两个key (local和remote)指向需要操作的内存区域</li>\n<li>注册的keys是数据传输请求的一部分</li>\n</ul>\n<p>注册一个Memory Region之后,这个时候这个Memory Region也就有了它自己的属性:</p>\n<ul>\n<li>context : RDMA操作上下文</li>\n<li>addr : MR被注册的Buffer地址</li>\n<li>length : MR被注册的Buffer长度</li>\n<li>lkey:MR被注册的本地key</li>\n<li>rkey:MR被注册的远程key</li>\n</ul>\n<p>对Memrory Registration:Memory Registration只是RDMA中对内存保护的一种措施,只有将要操作的内存注册到RDMA Memory Region中,这快操作的内存就交给RDMA 保护域来操作了。这个时候我们就可以对这快内存进行操作,至于操作的起始地址、操作Buffer的长度,可以根据程序的具体需求进行操作。我们只要保证接受方的Buffer 接受的长度大于等于发送的Buffer长度。</p>\n<h2>5.2 Queues | 队列</h2>\n<p>RDMA一共支持三种队列,发送队列(SQ)和接收队列(RQ),完成队列(CQ)。其中,SQ和RQ通常成对创建,被称为Queue Pairs(QP)。</p>\n<p>RDMA是基于消息的传输协议,数据传输都是异步操作。 RDMA操作其实很简单,可以理解为:</p>\n<ol>\n<li>Host提交工作请求(WR)到工作队列(WQ): 工作队列包括发送队列(SQ)和接收队列(RQ)。工作队列的每一个元素叫做WQE, 也就是WR。</li>\n<li>Host从完成队列(CQ)中获取工作完成(WC): 完成队列里的每一个叫做CQE, 也就是WC。</li>\n<li>具有RDMA引擎的硬件(hardware)就是一个队列元素处理器。 RDMA硬件不断地从工作队列(WQ)中去取工作请求(WR)来执行,执行完了就给完成队列(CQ)中放置工作完成(WC)。从生产者-消费者的角度理解就是:</li>\n<li>Host生产WR, 把WR放到WQ中去</li>\n<li>RDMA硬件消费WR</li>\n<li>RDMA硬件生产WC, 把WC放到CQ中去</li>\n<li>Host消费WC</li>\n</ol>\n<p><img src=\"./v2-ea7615096a651042d6ff0758d85ad698_720w.jpg\" alt=\"img\"></p>\n<h2>6. RDMA数据传输</h2>\n<h2>6.1 RDMA Send | RDMA发送(/接收)操作 (Send/Recv)</h2>\n<p>跟TCP/IP的send/recv是类似的,不同的是RDMA是基于消息的数据传输协议(而不是基于字节流的传输协议),所有数据包的组装都在RDMA硬件上完成的,也就是说OSI模型中的下面4层(传输层,网络层,数据链路层,物理层)都在RDMA硬件上完成。</p>\n<h2>6.2 RDMA Read | RDMA读操作 (Pull)</h2>\n<p>RDMA读操作本质上就是Pull操作, 把远程系统内存里的数据拉回到本地系统的内存里。</p>\n<h2>6.3 RDMA Write | RDMA写操作 (Push)</h2>\n<p>RDMA写操作本质上就是Push操作,把本地系统内存里的数据推送到远程系统的内存里。</p>\n<h2>6.4 RDMA Write with Immediate Data | 支持立即数的RDMA写操作</h2>\n<p>支持立即数的RDMA写操作本质上就是给远程系统Push(推送)带外(OOB)数据, 这跟TCP里的带外数据是类似的。</p>\n<p>可选地,immediate 4字节值可以与数据缓冲器一起发送。 该值作为接收通知的一部分呈现给接收者,并且不包含在数据缓冲器中。</p>\n<h1>RDMA Send/Receive 操作</h1>\n<h2>1. 前言</h2>\n<p>RDMA指的是远程直接内存访问,这是一种通过网络在两个应用程序之间搬运缓冲区里的数据的方法。RDMA与传统的网络接口不同,因为它绕过了操作系统。这允许实现了RDMA的程序具有如下特点:</p>\n<ul>\n<li>绝对的最低时延</li>\n<li>最高的吞吐量</li>\n<li>最小的CPU足迹 (也就是说,需要CPU参与的地方被最小化)</li>\n</ul>\n<h2>2. RDMA Verbs操作</h2>\n<p>使用RDMA, 我们需要有一张实现了RDMA引擎的网卡。我们把这种卡称之为HCA(主机通道适配器)。 适配器创建一个贯穿PCIe总线的从RDMA引擎到应用程序内存的通道。一个好的HCA将在导线上执行的RDMA协议所需要的全部逻辑都在硬件上予以实现。这包括分组,重组以及流量控制和可靠性保证。因此,从应用程序的角度看,只负责处理所有缓冲区即可。</p>\n<p><img src=\"./v2-f434f07e79221b59cd82e731cd62285d_720w.jpg\" alt=\"img\"></p>\n<p>在RDMA中我们使用内核态驱动建立一个数据通道。我们称之为命令通道(Command Channel)。使用命令通道,我们能够建立一个数据通道(Data Channel),该通道允许我们在搬运数据的时候完全绕过内核。一旦建立了这种数据通道,我们就能直接读写数据缓冲区。</p>\n<p>建立数据通道的API是一种称之为"verbs"的API。"verbs" API是由一个叫做OFED的Linux开源项目维护的。在站点<a href=\"https://link.zhihu.com/?target=http%3A//www.openfabrics.org\">http://www.openfabrics.org</a>上,为Windows WinOF提供了一个等价的项目。"verbs" API跟你用过的socket编程API是不一样的。但是,一旦你掌握了一些概念后,就会变得非常容易,而且在设计你的程序的时候更简单。</p>\n<h2>2. Queue Pairs</h2>\n<p>RDMA操作开始于“搞”内存。当你在对内存进行操作的时候,就是告诉内核这段内存名花有主了,主人就是你的应用程序。于是,你告诉HCA,就在这段内存上寻址,<strong>赶紧准备开辟一条从HCA卡到这段内存的通道</strong>。我们将这一动作称之为注册一个内存区域(MR)。一旦MR注册完毕,我们就可以使用这段内存来做任何RDMA操作。在下面的图中,我们可以看到注册的内存区域(MR)和被通信队列所使用的位于内存区域之内的缓冲区(buffer)。</p>\n<p>RDMA Memory Registration</p>\n<pre><code class=\"language-cpp\">struct ibv_mr {\n struct ibv_context *context;\n struct ibv_pd *pd;\n void *addr;\n size_t length;\n uint32_t handle;\n uint32_t lkey;\n uint32_t rkey;\n};\n</code></pre>\n<p><img src=\"./v2-55fa92d979172cd69a027a1401f535c2_720w.jpg\" alt=\"img\"></p>\n<p>RDMA硬件不断地从工作队列(WQ)中去取工作请求(WR)来执行,执行完了就给完成队列(CQ)中放置工作完成通知(WC)。这个WC意思就是Work Completion。表示这个WR RDMA请求已经被处理完成,可以从这个Completion Queue从取出来,表示这个RDMA请求已经被处理完毕。</p>\n<p>RDMA通信基于三条队列(SQ, RQ和CQ)组成的集合。 其中, 发送队列(SQ)和接收队列(RQ)负责调度工作,他们总是成对被创建,称之为队列对(QP)。当放置在工作队列上的指令被完成的时候,完成队列(CQ)用来发送通知。</p>\n<p>当用户把指令放置到工作队列的时候,就意味着告诉HCA那些缓冲区需要被发送或者用来接受数据。这些指令是一些小的结构体,称之为工作请求(WR)或者工作队列元素(WQE)。 WQE的发音为"WOOKIE",就像星球大战里的猛兽。一个WQE主要包含一个指向某个缓冲区的指针。一个放置在发送队列(SQ)里的WQE中包含一个指向待发送的消息的指针。一个放置在接受队列里的WQE里的指针指向一段缓冲区,该缓冲区用来存放待接受的消息。</p>\n<p>下面我们来看一下RDMA中的Work Request(SendWR和ReceWR)</p>\n<p>RDMA Send Work Request请求</p>\n<pre><code class=\"language-cpp\">struct ibv_send_wr {\n uint64_t wr_id;\n struct ibv_send_wr *next;\n struct ibv_sge *sg_list;\n int num_sge;\n enum ibv_wr_opcode opcode;\n int send_flags;\n uint32_t imm_data; /* in network byte order */\n union {\n struct {\n uint64_t remote_addr;\n uint32_t rkey;\n } rdma;\n struct {\n uint64_t remote_addr;\n uint64_t compare_add;\n uint64_t swap;\n uint32_t rkey;\n } atomic;\n struct {\n struct ibv_ah *ah;\n uint32_t remote_qpn;\n uint32_t remote_qkey;\n } ud;\n } wr;\n};\n</code></pre>\n<p>RDMA Receive Work Request请求</p>\n<pre><code class=\"language-cpp\">struct ibv_recv_wr {\n uint64_t wr_id;\n struct ibv_recv_wr *next;\n struct ibv_sge *sg_list;\n int num_sge;\n};\n</code></pre>\n<p>RDMA是一种异步传输机制。因此我们可以一次性在工作队列里放置好多个发送或接收WQE。HCA将尽可能快地按顺序处理这些WQE。当一个WQE被处理了,那么数据就被搬运了。 一旦传输完成,HCA就创建一个完成队列元素(CQE)并放置到完成队列(CQ)中去。 相应地,CQE的发音为"COOKIE"。</p>\n<p>RDMA Complete Queue Element</p>\n<pre><code class=\"language-cpp\">struct ibv_wc { \n uint64_t wr_id; \n enum ibv_wc_status status; \n enum ibv_wc_opcode opcode; \n uint32_t vendor_err; \n uint32_t byte_len; \n uint32_t imm_data; /* in network byte order */ \n uint32_t qp_num; \n uint32_t src_qp; \n int wc_flags; \n uint16_t pkey_index; \n uint16_t slid; \n uint8_t sl; \n uint8_t dlid_path_bits; \n};\n</code></pre>\n<h2>3. RDMA Send/Receive</h2>\n<p>让我们看个简单的例子。在这个例子中,我们将把一个缓冲区里的数据从系统A的内存中搬到系统B的内存中去。这就是我们所说的消息传递语义学。接下来我们要讲的一种操作为SEND,是RDMA中最基础的操作类型。</p>\n<h2>3.1 第一步</h2>\n<p>第1步:系统A和B都创建了他们各自的QP的完成队列(CQ), 并为即将进行的RDMA传输注册了相应的内存区域(MR)。 系统A识别了一段缓冲区,该缓冲区的数据将被搬运到系统B上。系统B分配了一段空的缓冲区,用来存放来自系统A发送的数据。</p>\n<p><img src=\"./v2-1282960e29ec7042ffec89dcc4f5577e_720w.jpg\" alt=\"img\"></p>\n<h2>3.2 第二步</h2>\n<p>第二步:系统B创建一个WQE并放置到它的接收队列(RQ)中。这个WQE包含了一个指针,该指针指向的内存缓冲区用来存放接收到的数据。系统A也创建一个WQE并放置到它的发送队列(SQ)中去,该WQE中的指针执行一段内存缓冲区,该缓冲区的数据将要被传送。</p>\n<p><img src=\"./v2-9dcb687ffaee99730313270214b327e6_720w.jpg\" alt=\"img\"></p>\n<h2>3.3 第三步</h2>\n<p>第三步:系统A上的HCA总是在硬件上干活,看看发送队列里有没有WQE。HCA将消费掉来自系统A的WQE, 然后将内存区域里的数据变成数据流发送给系统B。当数据流开始到达系统B的时候,系统B上的HCA就消费来自系统B的WQE,然后将数据放到该放的缓冲区上去。在高速通道上传输的数据流完全绕过了操作系统内核。</p>\n<p><img src=\"./v2-397f08428eaee59f9908dcb0ea2b1b56_720w.jpg\" alt=\"img\"></p>\n<h2>3.4 第四步</h2>\n<p>第四步:当数据搬运完成的时候,HCA会创建一个CQE。 这个CQE被放置到完成队列(CQ)中,表明数据传输已经完成。HCA每消费掉一个WQE, 都会生成一个CQE。因此,在系统A的完成队列中放置一个CQE,意味着对应的WQE的发送操作已经完成。同理,在系统B的完成队列中也会放置一个CQE,表明对应的WQE的接收操作已经完成。如果发生错误,HCA依然会创建一个CQE。在CQE中,包含了一个用来记录传输状态的字段。</p>\n<p><img src=\"./v2-397f08428eaee59f9908dcb0ea2b1b56_720w.jpg\" alt=\"img\"></p>\n<p>我们刚刚举例说明的是一个RDMA Send操作。在IB或RoCE中,传送一个小缓冲区里的数据耗费的总时间大约在1.3µs。通过同时创建很多WQE, 就能在1秒内传输存放在数百万个缓冲区里的数据。</p>\n<h2>4. 总结</h2>\n<p>在这博客中,我们学习了如何使用RDMA verbs API。同时也介绍了队列的概念,而队列概念是RDMA编程的基础。最后,我们演示了RDMA send操作,展现了缓冲区的数据是如何在从一个系统搬运到另一个系统上去的。</p>\n<h1>理解 RDMA SGL</h1>\n<h2>1. 前言</h2>\n<p>在使用RDMA操作之前,我们需要了解一些RDMA API中的一些需要的值。其中在ibv_send_wr我们需要一个sg_list的数组,sg_list是用来存放ibv_sge元素,那么什么是SGL以及什么是sge呢?对于一个使用RDMA进行开发的程序员来说,我们需要了解这一系列细节。</p>\n<h2>2. SGE简介</h2>\n<p>在NVMe over PCIe中,I/O命令支持SGL(Scatter Gather List 分散聚合表)和PRP(Physical Region Page 物理(内存)区域页), 而管理命令只支持PRP;而在NVMe over Fabrics中,无论是管理命令还是I/O命令都只支持SGL。</p>\n<p>RDMA编程中,SGL(Scatter/Gather List)是最基本的数据组织形式。 SGL是一个数组,该数组中的元素被称之为SGE(Scatter/Gather Element),<strong>每一个SGE就是一个Data Segment(数据段)</strong>。RDMA支持Scatter/Gather操作,具体来讲就是RDMA可以支持一个连续的Buffer空间,进行Scatter分散到多个目的主机的不连续的Buffer空间。Gather指的就是多个不连续的Buffer空间,可以Gather到目的主机的一段连续的Buffer空间。</p>\n<p>下面我们就来看一下ibv_sge的定义:</p>\n<pre><code class=\"language-cpp\">struct ibv_sge {\n uint64_t addr;\n uint32_t length;\n uint32_t lkey;\n};\n</code></pre>\n<ul>\n<li>addr: 数据段所在的虚拟内存的起始地址 (Virtual Address of the Data Segment (i.e. Buffer))</li>\n<li>length: 数据段长度(Length of the Data Segment)</li>\n<li>lkey: 该数据段对应的L_Key (Key of the local Memory Region)</li>\n</ul>\n<h2>2. ivc_post_send接口</h2>\n<p>而在数据传输中,发送/接收使用的Verbs API为:</p>\n<ul>\n<li>ibv_post_send() - post a list of work requests (WRs) to a send queue 将一个WR列表放置到发送队列中 ibv_post_recv() - post a list of work requests (WRs) to a receive queue 将一个WR列表放置到接收队列中</li>\n</ul>\n<p>下面以ibv_post_send()为例,说明SGL是如何被放置到RDMA硬件的线缆(Wire)上的。</p>\n<p>ibv_post_send()的函数原型</p>\n<pre><code class=\"language-cpp\">#include <infiniband/verbs.h>\n\nint ibv_post_send(struct ibv_qp *qp, \n struct ibv_send_wr *wr,\n struct ibv_send_wr **bad_wr);\n</code></pre>\n<p>ibv_post_send()将以send_wr开头的工作请求(WR)的列表发布到Queue Pair的Send Queue。 它会在第一次失败时停止处理此列表中的WR(可以在发布请求时立即检测到),并通过bad_wr返回此失败的WR。</p>\n<p>参数wr是一个ibv_send_wr结构,如中所定义。</p>\n<h2>3. ibv_send_wr结构</h2>\n<pre><code class=\"language-cpp\">struct ibv_send_wr {\n uint64_t wr_id; /* User defined WR ID */\n struct ibv_send_wr *next; /* Pointer to next WR in list, NULL if last WR */\n struct ibv_sge *sg_list; /* Pointer to the s/g array */\n int num_sge; /* Size of the s/g array */\n enum ibv_wr_opcode opcode; /* Operation type */\n int send_flags; /* Flags of the WR properties */\n uint32_t imm_data; /* Immediate data (in network byte order) */\n union {\n struct {\n uint64_t remote_addr; /* Start address of remote memory buffer */\n uint32_t rkey; /* Key of the remote Memory Region */\n } rdma;\n struct {\n uint64_t remote_addr; /* Start address of remote memory buffer */\n uint64_t compare_add; /* Compare operand */\n uint64_t swap; /* Swap operand */\n uint32_t rkey; /* Key of the remote Memory Region */\n } atomic;\n struct {\n struct ibv_ah *ah; /* Address handle (AH) for the remote node address */\n uint32_t remote_qpn; /* QP number of the destination QP */\n uint32_t remote_qkey; /* Q_Key number of the destination QP */\n } ud;\n } wr;\n};\n</code></pre>\n<p>在调用ibv_post_send()之前,必须填充好数据结构wr。 wr是一个链表,每一个结点包含了一个sg_list(i.e. SGL: 由一个或多个SGE构成的数组), sg_list的长度为num_sge。</p>\n<h2>4. RDMA 提交WR流程</h2>\n<p>下面图解一下SGL和WR链表的对应关系,并说明一个SGL (struct ibv_sge *sg_list)里包含的多个数据段是如何被RDMA硬件聚合成一个连续的数据段的。</p>\n<h2>4.1 第一步:创建SGL</h2>\n<p><img src=\"./v2-a45f31b55c22ca8aad8a139be0eb8d99_720w.jpg\" alt=\"img\"></p>\n<p>从上图中,我们可以看到wr链表中的每一个结点都包含了一个SGL,SGL是一个数组,包含一个或多个SGE。通过ibv_post_send提交一个RDMA SEND 请求。这个WR请求中,包括一个sg_list的元素。它是一个SGE链表,SGE指向具体需要发送数据的Buffer。</p>\n<h2>4.2 第二步:使用PD进行内存保护</h2>\n<p><img src=\"./v2-4e72a802e022d5742de169921c185cd8_720w.jpg\" alt=\"img\"></p>\n<p>我们在发送一段内存地址的时候,我们需要将这段内存地址通过Memory Registration注册到RDMA中。也就是说注册到PD内存保护域当中。一个SGL至少被一个MR保护, 多个MR存在同一个PD中。如图所示一段内存MR可以保护多个SGE元素。</p>\n<h2>4.3 调用ibv_post_send()将SGL发送到wire上去</h2>\n<p><img src=\"./v2-dec6f454affdc07019b8729c6c13fc96_720w.jpg\" alt=\"img\"></p>\n<p>在上图中,一个SGL数组包含了3个SGE, 长度分别为N1, N2, N3字节。我们可以看到,这3个buffer并不连续,它们Scatter(分散)在内存中的各个地方。RDMA硬件读取到SGL后,进行Gather(聚合)操作,于是在RDMA硬件的Wire上看到的就是N3+N2+N1个连续的字节。换句话说,通过使用SGL, 我们可以把分散(Scatter)在内存中的多个数据段(不连续)交给RDMA硬件去聚合(Gather)成连续的数据段。</p>\n<h2>附录一: OFED Verbs</h2>\n<p><img src=\"./v2-f784e50e30e8faae55822ef0617c01ca_720w.jpg\" alt=\"img\"></p>\n<h1>论文</h1>\n<h2>ATC21: MigrOS: Transparent Live-Migration Support for Containerised RDMA Applications</h2>\n<p>这篇文章提出容器化和 RDMA 本身是冲突的,容器化为应用提供了独立于宿主机的运行时,RDMA 则会让应用和宿主机之间的联系更加紧密。这种冲突导致容器在重启或迁移时,无法恢复被中断的 RDMA 应用。这篇文章修改了 RoCEv2 协议,增加了两个状态,增强了 IB verbs API,从而支持 RDMA 应用的中断、恢复。然后使用 <a href=\"https://github.com/checkpoint-restore/criu\">CRIU</a> 调用他们修改过的 IB verbs API,达到恢复的目的(CRIU 是一个保存进程状态、重启进程的工具)。这篇文章非常详细地介绍了 RoCEv2,拿来学习也是很有用的。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20211002/",
"title": "转载:RDMA 基础",
"summary": "基础知识",
"date_modified": "2021-10-02T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>原子类型及原子操作的内存顺序</h1>\n<p>之前虽然了解过原子类型的基本知识,但是也仅限于了解。最近在做 <a href=\"https://github.com/sentinel-group/sentinel-rust\">sentinel-group/sentinel-rust</a>,实际应用了一些,但是目前项目里还都是使用的 <code>Ordering::SeqCst</code>,即完全顺序化,可能之后会做出一些改进。</p>\n<h2>C++ Concurrency in Action</h2>\n<p>以下的部分内容摘自书籍《C++并发编程实战》的第五章,英文版原名为《C++ Concurrency in Action》。</p>\n<h3>1.1 内存模型</h3>\n<p>这里有四个需要牢记的原则:</p>\n<ol>\n<li>每个变量都是对象,包括其成员变量的对象(注意这里的对象只是在指“存储区域”)。</li>\n<li>无论是怎么样的类型,都会存储在一个或多个内存位置上。</li>\n<li>基本类型都有确定的内存位置(无论类型大小如何,即使他们是相邻的,或是数组的一部分)。</li>\n<li>相邻位域是相同内存中的一部分。</li>\n</ol>\n<p>这和并发有什么关系?</p>\n<h3>1.2 对象、内存位置和并发</h3>\n<p>当两个线程访问不同的内存位置时,不会存在任何问题,当两个线程访问同一个内存位置就要小心了。如果线程不更新数据,只读数据不需要保护或同步。当线程对内存位置上的数据进行修改,就可能会产生条件竞争。这也是为什么会有 “读写锁” 的存在(即 C++ 中的 <code>std::shared_mutex</code> 和 Rust 中的 <code>std::sync::RwLock</code>),区分 “读” 和 “写” 两种原语,在高频 “读” 时,较普通的互斥锁(即 C++ 中的 <code>std::mutex</code> 和 Rust 中的 <code>std::sync::Mutex</code>),可以降低开销。</p>\n<p>为了避免条件竞争,线程就要以一定的顺序执行。第一种方式,使用互斥量来确定访问的顺序。当同一互斥量在两个线程同时访问前锁住,那么在同一时间内就只有一个线程能够访问对应的内存位置。另一种是使用原子操作决定两个线程的访问顺序,当多个线程访问同一个内存地址时,对每个访问者都需要设定顺序。</p>\n<p>如果不规定对同一内存地址访问的顺序,那么访问就不是 “原子” 的。当两个线程都是“写入者”时,就会产生数据竞争和未定义行为。当程序对同一内存地址中的数据访问存在竞争,可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——<strong>原子操作并没有指定访问顺序</strong>,它只是会把程序拉回到定义行为的区域内,即两个写操作也不会互相干扰,所以需要程序员指定执行顺序。</p>\n<p>如果对象不是原子类型,必须确保有足够的同步操作(比如互斥量、信号量、conditional variable 等),来确定线程都遵守了修改顺序。如果使用原子操作,编译器就有责任去做同步,去使用机器的原子指令,或是在机器没有对应指令时,重新生成带锁的代码。</p>\n<ul>\n<li>\n<p>当然,原子指令往往比一般的运算指令要慢,但是如果机器上有原子指令,还是比加锁要快的。但是在 Rust 实践中,原子类型破坏了代码中的可变标识 <code>mut</code> 。毕竟 <code>mut</code> 关键字本身就可以防止因为不确定的读写顺序,导致的数据竞争和未定义行为(因为同一时刻只能有一个可变引用存在,它甚至不可以和不可变引用同时存在)。所以对于外部直接使用容器的用户来说,只能依赖函数名和注释来判断函数的用途,而无法通过函数签名中的参数类型确认了。</p>\n</li>\n<li>\n<p>当然,执行并发操作也不一定要访问共享内存,常见的如 go 中使用的 CSP model,通过 channel 通信;Erlang 中的 Actor model;Rust 中基于 Actor model 的 <code>Actix</code> 库、Rust 标准库中的各类通信 channel;往大里说,Map-Reduce 也是如此。</p>\n</li>\n</ul>\n<h3>2.0 原子操作和原子类型</h3>\n<p>这部分就是很基础的,只需要看看它们都有啥 API 就好。C++ 的标准原子类型定义在头文件 <code><atomic> </code>中,Rust 在 <code>std::sync::atomic</code> 下。原子类型有两种实现方式:</p>\n<ul>\n<li>\n<p>机器硬件上有对应的原子指令。</p>\n</li>\n<li>\n<p>也可以用互斥锁来模拟原子操作。</p>\n</li>\n</ul>\n<p>它们大多有一个 <code>is_lock_free()</code> 成员函数,这个函数可以让用户查询某原子类型的操作是直接用的原子指令(<code>x.is_lock_free()</code> 返回 <code>true</code>),还是内部用了一个锁结构(<code>x.is_lock_free()</code> 返回 <code>false</code>)。C++17中,所有原子类型有一个 <code>static constexpr</code> 成员变量,如果相应硬件上的原子类型X是无锁类型,那么 <code>X::is_always_lock_free</code>将返回 <code>true</code>。</p>\n<p>只有<code>std::atomic_flag</code>类型不提供 <code>is_lock_free()</code>。该类型是一个简单的布尔标志,并且在这种类型上的操作都是无锁的。当有一个简单无锁的布尔标志时,可以使用该类型实现一个简单的锁,并且可以通过这个锁来实现其他基础原子类型(即上面提到的第二种原子类型的实现方式)。剩下的原子类型都可以通过特化<code>std::atomic<></code>得到,并且拥有更多的功能,但不可能都是无锁的。</p>\n<p>通常,标准原子类型不能进行拷贝和赋值,它们没有拷贝构造函数和拷贝赋值操作符。但是,可以隐式转化成对应的内置类型,所以这些类型依旧支持赋值。赋值操作和成员函数的返回值,要么是存储值(赋值操作),要么是操作值(命名函数),这就能避免赋值操作符返回引用。具体而言,支持的运算如下:</p>\n<p><img src=\"./atomic-table.png\" alt=\"\"></p>\n<p>每种函数类型的操作都有一个内存序参数,这个参数可以用来指定存储的顺序。不同的内存序在不同的CPU架构下功耗不同,如果有多个处理器,额外的同步指令会消耗大量的时间,从而降低系统性能。</p>\n<p>为了兼容 C 的风格,也提供了非成员函数。大多数非成员函数的命名与对应成员函数有关,需要<code>atomic_</code>作为前缀(比如,<code>std::atomic_load()</code>)。这些函数都会重载不同的原子类型,指定内存序时会分成两种:一种没有标签,另一种以<code>_explicit</code>为后缀,并且需要额外的参数,或将内存序作为标签,亦或只有标签(例如,<code>std::atomic_store(&atomic_var,new_value)</code>与<code>std::atomic_store_explicit(&atomic_var,new_value,std::memory_order_release</code>)。C 里面没有引用的概念,传的是指针。</p>\n<p>标准原子类型不仅仅是为了避免数据竞争所造成的未定义行为,还允许用户对不同线程上的操作进行强制排序。这种强制排序是数据保护和同步操作的基础,例如:<code>std::mutex</code>和<code>std::future</code>。</p>\n<p>:::tip</p>\n<p><code>compare_exchange_strong</code> 和 <code>compare_exchange_weak</code> 是 compare-and-swap (CAS)操作,涉及到比较、交换两个步骤。但是和 <code>compare_exchange_strong</code> 不同, <code>compare_exchange_weak</code> 允许在比较成功时失败,即交换步骤可以失败,这在某些平台上更加高效 (比如你在循环质询某个值)。 而 <code>compare_exchange_strong</code> 只有在比较时,原值不等于期望值时才会失败。</p>\n<p>:::</p>\n<h3>3.3 原子操作的内存顺序</h3>\n<p>对于原子操作,可能的内存顺序如下:</p>\n<ol>\n<li>Store(写)操作,可选如下内存序:<code>memory_order_relaxed</code>, <code>memory_order_release</code>, <code>memory_order_seq_cst</code>。</li>\n<li>Load(读)操作,可选如下内存序:<code>memory_order_relaxed</code>, <code>memory_order_consume</code>, <code>memory_order_acquire</code>, <code>memory_order_seq_cst</code>。</li>\n<li>Read-modify-write(读-改-写)操作,可选如下内存序:<code>memory_order_relaxed</code>, <code>memory_order_consume</code>, <code>memory_order_acquire</code>, <code>memory_order_release</code>, <code>memory_order_acq_rel</code>, <code>memory_order_seq_cst</code>。</li>\n</ol>\n<p>虽然有六个选项,但仅代表三种内存模型:顺序一致性 (sequentially consistent),获取-释放序 (memory_order_consume, memory_order_acquire, memory_order_release 和 memory_order_acq_rel) 和自由序 (memory_order_relaxed)。</p>\n<h4>顺序一致性</h4>\n<p>默认序命名为顺序一致性,因为程序中的行为从任意角度去看,序列都保持一定顺序。如果原子实例的所有操作都是序列一致的,那么多线程就会如单线程那样以某种特殊的排序执行。目前来看,该内存序是最容易理解的,这也是将其设置为默认的原因:不同的操作也要遵守相同的顺序。因为行为简单,可以使用原子变量进行编写。通过不同的线程,可以写出所有可能的操作消除那些不一致,以及确认代码的行为是否与预期相符。所以,操作都不能重排;如果代码在一个线程中,将一个操作放在另一个操作前面,那其他线程也需要了解这个顺序。</p>\n<p>不过,简单就要付出代价。多核机器会加强对性能的惩罚,因为整个序列中的操作都必须在多个处理器上保持一致,可能需要对处理器间的同步操作进行扩展,这份代价很昂贵!即便如此,一些处理器架构,比如通用 x86 和 x86-64 架构就提供了相对廉价的顺序一致性,所以需要考虑使用顺序一致性对性能的影响,就需要去查阅目标处理器的架构文档进行更多的了解。</p>\n<p><strong>序列一致性是最简单、直观的序列,因为需要对所有线程进行全局同步,所以也是开销最大的内存序。多处理器设备上需要在处理期间,在信息交换上耗费大量的时间。为了避免这种消耗,就需考虑使用其他内存序。</strong></p>\n<h4>非顺序一致性内存</h4>\n<p>当踏出序列一致的世界时,事情就开始复杂了。不同线程看到相同操作,不一定有着相同的顺序,还有对于不同线程的操作,都会一个接着另一个执行的想法就不可行了。不仅是考虑事情同时发生的问题,还有线程没办法保证一致性。为了写出(或仅是了解)一段使用非默认内存序列的代码,绝不仅是编译器重新排列指令的事情。即使线程运行相同的代码,都能拒绝遵循事件发生的顺序,因为操作在其他线程上没有明确的顺序限制,不同的CPU缓存和内部缓冲区,在同样的存储空间中可以存储不同的值。这非常重要,这里再重申一次:线程没办法保证一致性。</p>\n<p>不仅是要摒弃串行的想法,还要放弃编译器或处理器重排指令的想法。没有明确顺序限制时,就需要所有线程要对每个独立变量统一修改顺序。对不同变量的操作可以体现在不同线程的不同序列上,提供的值要与任意附加顺序限制保持一致。</p>\n<p>踏出排序一致世界后,就使用memory_order_relaxed对所有操作进行约束。如果已经有所了解,可以跳到获取-释放序继续阅读,获取-释放序允许在操作间引入顺序关系。</p>\n<h4>自由序</h4>\n<p>原子类型上的操作以自由序执行。同一线程中对于同一变量的操作还是遵从先行关系,但不同线程不需要规定顺序。唯一的要求是在访问同一线程中的单个原子变量不能重排序,当给定线程看到原子变量的值时,随后线程的读操作就不会去检索较早的那个值。当使用memory_order_relaxed时,不需要任何额外的同步,对于每个变量的修改顺序只存在于线程间共享。</p>\n<p><strong>理解自由序</strong></p>\n<p>为了了解自由序是如何工作的,可先将每一个变量想象成在一个独立房间中拿着记事本的人。他的记事本上是一组值的列表,可以通过打电话的方式让他给你一个值,或让他写下一个新值。如果告诉他写下一个新值,他会将这个新值写在表的最后。如果让他给你一个值,他会从列表中读取一个值给你。</p>\n<p>第一次与这人交谈时,如果问他要一个值,他可能会在现有的列表中选区任意值告诉你。如果之后再问他要一个值,可能会得到与之前相同的值,或是列表下端的其他值,他不会给你列表上端的值。如果让他写一个值,并且随后再问他要一个值,他要不就给你你刚告诉他的那个值,要不就是一个列表下端的值。</p>\n<p>试想当他的笔记本上开始有5,10,23,3,1,2这几个数。如果问他索要一个值,你可能获取这几个数中的任意一个。如果他给你10,那么下次再问他要值的时候可能会再给你10,或者10后面的数,但绝对不会是5。如果那你问他要了五次,他就可能回答“10,10,1,2,2”。如果你让他写下42,他将会把这个值添加在列表的最后。如果你再问他要值,他可能会告诉你“42”,直到有其他值写在了后面,并且他愿意将那个数告诉你。</p>\n<p>现在,你有个朋友叫Carl,他也有那个计数员的电话。Carl也可以打电话给计算员,让他写下一个值或获取一个值,他对Carl回应的规则和你是一样的。他只有一部电话,所以一次只能处理一个人的请求,所以他记事本上的列表是一个简单的列表。但是,你让他写下一个新值的时候,不意味着他会将这个消息告诉Carl,反之亦然。如果Carl从他那里获取一个值“23”,之后因为你告诉他写下42,这不意味着下次他会将这件事告诉Carl。他可能会告诉Carl任意一个值,23,3,1,2,42亦或是67(是Fred在你之后告诉他的)。他会很高兴的告诉Carl“23,3,3,1,67”,与你告诉他的值完全不一致,这就像在使用便签跟踪告诉每个人的数字,如下图。</p>\n<p><img src=\"./relaxed_notebook.png\" alt=\"\"></p>\n<p>现在,不仅仅有一个人在房间里,而是在一个小农场里,每个人都有一部电话和一个笔记本,这就是原子变量。每一个变量拥有自己的修改顺序(笔记上的简单数值列表),但是每个原子变量之间没有任何关系。如果每一个调用者(你,Carl,Anne,Dave和Fred)是一个线程,对每个操作使用memory_order_relaxed就会得到上面的结果。还有些事情可以告诉小房子里的人,例如:“写下这个值,并且告诉我现在列表中的最后一个值”(exchange),或“写下这个值,当列表的最后一个值为某值时,会进行猜测,如果猜错了,则告诉我最后一个值是多少”(compare_exchange_strong),这些都不影响一般性原则。</p>\n<p><strong>要想获取额外的同步,且不使用全局排序一致,可以使用获取-释放序 (acquire-release ordering)</strong>。</p>\n<h4>获取-释放序</h4>\n<p>这是自由序 (relaxed ordering) 的加强版,虽然操作依旧没有统一顺序,但引入了同步。<strong>这种序列模型中,原子加载就是获取 (acquire) 操作 (memory_order_acquire),原子存储就是释放 (memory_order_release) 操作,原子读-改-写操作 (例如 <code>fetch_add()</code> 或<code>exchange()</code> ) 在这里,不是“获取”就是“释放”,或者两者兼有的操作 (memory_order_acq_rel)。<strong>同步在线程释放和获取间是</strong>成对的</strong>(pairwise),释放操作与获取操作同步就能读取已写入的值。</p>\n<p><strong>理解获取-释放序</strong></p>\n<p>也可以将获取-释放序与之前提到记录员相关联,这样就需要添加很多东西到模型中。首先,每个存储操作做一部分更新,当你联系一个人时,让他写下一个数字,也需要告诉他更新哪一部分:“请在423组中写下99”。对于某一组的最后一个值的存储,你也需要告诉那个人:“请写下147,这是最后存储在423组的值”。隔间中的人会及时写下这一信息,并注明这个值的来源,这个就是存储-释放操作的模型。下一次,你告诉另外一个人写下一组值时,需要改变组号:“请在424组中写入41”</p>\n<p>当你询问时就要做出一个选择:要不就仅仅询问一个值(这就是次自由加载,这种情况下,隔间中的人会给你的),要不就询问一个值以及其关于组的信息(是否是某组中的最后一个,这就是加载-获取模型)。当你询问组信息,且值不是组中的最后一个,隔间中的人会这样告诉你,“这个值是987,它是一个普通值”,但当这个值是最后一个时,他会告诉你:“数字为987,这个值是956组的最后一个,来源于Anne”。这样,获取-释放的语义就很明确了:当查询一个值,你告诉他所有组后,他会低头查看列表,看你给的这些数是不是在对应组的最后,并且告诉你那个值的属性,或继续在列表中查询。</p>\n<p><strong>如何选择</strong></p>\n<p>使用“读-改-写”操作,选择语义就很重要了。如果想要同时进行获取和释放的语义,所以 memory_order_acq_rel 是一个不错的选择,但也可以使用其他内存序。即使存储了一个值,使用 memory_order_acquire 语义的 fetch_sub 不会和任何东西同步的,因为没有释放操作。同样,使用 memory_order_release 语义的 fetch_or 也不会和任何存储操作进行同步,因为对于 fetch_or 的读取,并不是一个获取操作。使用 memory_order_acq_rel 语义的“读-改-写”操作,每一个动作都包含获取和释放操作,所以可以和之前的存储操作进行同步,并且可以对随后的加载操作进行同步,就像上面例子一样。</p>\n<p>如果将获取-释放和序列一致进行混合,“序列一致”的加载动作就如使用了获取语义的加载操作,序列一致的存储操作就如使用了释放语义的存储,“序列一致”的读-改-写操作行为就如使用了获取和释放的操作。“自由操作”依旧那么自由,但其会和额外的同步进行绑定(也就是使用“获取-释放”的语义)。</p>\n<p>尽管结果并不那么直观,每个使用锁的同学都需要了解:**锁住互斥量是一个获取操作,并且解锁这个互斥量是一个释放操作。**随着互斥量的增多,必须确保同一个互斥量在读取变量或修改变量时上锁,所以获取和释放操作必须在同一个变量上,以保证访问顺序。当互斥量保护数据时,因为锁住与解锁的操作都是序列一致的操作,就保证了结果一致。当对原子变量使用获取和释放序时,代码必然会使用锁,即使内部操作序不一致,其外部表现将会为序列一致。</p>\n<p><strong>当原子操作不需要严格的序列一致序时,可以提供成对同步的获取-释放序,这种比全局序列一致性的成本更低,且有同步操作。为了保证序列能够正常的工作,这里还需要一些权衡,还要保证隐式的跨线程行为是没有问题的。</strong></p>\n<h4>获取-释放序和memory_order_consume的数据相关性</h4>\n<p>介绍本章节的时候,说过 memory_order_consume 是“获取-释放”模型的一部分,但并没有对其进行过多的讨论。因为 memory_order_consume 很特别:完全依赖于数据,并且其展示了与线程间先行关系的不同之处。这个内存序非常特殊,即使在C++17中也不推荐使用。这里只为了完整的覆盖内存序而讨论, memory_order_consume 不应该出现在代码中。Rust 中也没有迁移该顺序。</p>\n<p>数据依赖的概念相对简单:第二个操作依赖于第一个操作的结果,这样两个操作之间就有了数据依赖。这里有两种新关系用来处理数据依赖:<em>前序依赖</em> (dependency-ordered-before) 和<em>携带依赖</em> (carries-a-dependency-to)。携带依赖对于数据依赖的操作,严格应用于一个独立线程和其基本模型。如果 A 操作结果要使用操作B的操作数,则 A 将携带依赖于 B。如果 A 操作的结果是一个标量 (比如 <code>int</code>),而后的携带依赖关系仍然适用于,当 A 的结果存储在一个变量中,并且这个变量需要被其他操作使用。这个操作可以传递,所以当 A 携带依赖 B,并且 B 携带依赖 C,就可以得出 A 携带依赖 C 的关系。</p>\n<p>当不影响线程间的先行关系时,对于同步来说没有任何好处:当 A 前序依赖 B,那么 A 线程间也前序依赖 B。</p>\n<p>这种内存序在原子操作载入指向数据的指针时很重要,当使用 memory_order_consume 作为加载语义,并且 memory_order_release 作为存储语义时,就要保证指针指向的值已同步,并且不要求其他非独立数据同步。</p>\n<h3>3.5 栅栏</h3>\n<p>栅栏操作,<code>std::atomic_thread_fence(a_memory_order)</code>,会对内存序列进行约束,使其无法对任何数据进行修改,典型的做法是与使用 memory_order_relaxed 约束序的原子操作一起使用。栅栏属于全局操作,执行栅栏操作可以影响到在线程中的其他原子操作。因为这类操作就像画了一条任何代码都无法跨越的线一样,所以栅栏操作通常也被称为<em>内存栅栏</em> (memory barriers) 。回忆一下 3.3 节,Relaxed 顺序下的自由操作可以使用编译器或者硬件的方式,在独立的变量上自由的重新排序。不过,栅栏操作就会限制这种自由。</p>\n<h3>3.7 非原子操作排序</h3>\n<p>对非原子操作的排序,可以通过使用原子操作进行,“序前”作为“先行”的一部分,如果一个非原子操作是“序前”于一个原子操作,并且这个原子操作需要“先行”与另一个线程的操作,那么这个非原子操作也就“先行”于在其他线程的操作了。 对于C++标准库的高级同步工具来说,这些只是基本工具。</p>\n<p>总而言之,C++ 标准库提供了大量的同步机制,这些机制会为同步关系之间的顺序进行保证。这样就可以使用它们进行数据同步,并保证同步关系间的顺序。<strong>以下的工具都可以提供同步</strong>:</p>\n<p><strong>std::thread</strong></p>\n<ul>\n<li>std::thread构造新线程时,构造函数与调用函数或新线程的可调用对象间的同步。</li>\n<li>对std::thread对象调用join,可以和对应的线程进行同步。</li>\n</ul>\n<p><strong>std::mutex, std::timed_mutex, std::recursive_mutex, std::recursibe_timed_mutex</strong></p>\n<ul>\n<li>对给定互斥量对象调用lock和unlock,以及对try_lock,try_lock_for或try_lock_until,会形成该互斥量的锁序。</li>\n<li>对给定的互斥量调用unlock,需要在调用lock或成功调用try_lock,try_lock_for或try_lock_until之后,这样才符合互斥量的锁序。</li>\n<li>对try_lock,try_lock_for或try_lock_until失败的调用,不具有任何同步关系。</li>\n</ul>\n<p><strong>std::shared_mutex , std::shared_timed_mutex</strong></p>\n<ul>\n<li>对给定互斥量对象调用lock、unlock、lock_shared和unlock_shared,以及对 try_lock , try_lock_for , try_lock_until , try_lock_shared , try_lock_shared_for或 try_lock_shared_until的成功调用,会形成该互斥量的锁序。</li>\n<li>对给定的互斥量调用unlock,需要在调用lock或shared_lock,亦或是成功调用try_lock , try_lock_for, try_lock_until, try_lock_shared, try_lock_shared_for或try_lock_shared_until之后,才符合互斥量的锁序。</li>\n<li>对try_lock,try_lock_for,try_lock_until,try_lock_shared,try_lock_shared_for或try_lock_shared_until 失败的调用,不具有任何同步关系。</li>\n</ul>\n<p><strong>std::shared_mutex和std::shared_timed_mutex</strong></p>\n<ul>\n<li>成功的调用std::promise对象的set_value或set_exception与成功的调用wait或get之间同步,或是调用wait_for或wait_until的返回例如future状态std::future_status::ready与promise共享同步状态。</li>\n<li>给定std::promise对象的析构函数,该对象存储了一个std::future_error异常,成功的调用wait或get后,共享同步状态与promise之间的同步,或是调用wait_for或wait_until返回的future状态std::future_status::ready时,与promise共享同步状态。</li>\n</ul>\n<p><strong>std::packaged_task , std::future和std::shared_future</strong></p>\n<ul>\n<li>成功的调用std::packaged_task对象的函数操作符与成功的调用wait或get之间同步,或是调用wait_for或wait_until的返回future状态std::future_status::ready与打包任务共享同步状态。</li>\n<li>std::packaged_task对象的析构函数,该对象存储了一个std::future_error异常,其共享同步状态与打包任务之间的同步在于成功的调用wait或get,或是调用wait_for或wait_until返回的future状态std::future_status::ready与打包任务共享同步状态。</li>\n</ul>\n<p><strong>std::async , std::future和std::shared_future</strong></p>\n<ul>\n<li>使用std::launch::async策略性的通过std::async启动线程执行任务与成功的调用wait和get之间是同步的,或调用wait_for或wait_until返回的future状态std::future_status::ready与产生的任务共享同步状态。</li>\n<li>使用std::launch::deferred策略性的通过std::async启动任务与成功的调用wait和get之间是同步的,或调用wait_for或wait_until返回的future状态std::future_status::ready与promise共享同步状态。</li>\n</ul>\n<p><strong>std::experimental::future , std::experimental::shared_future和持续性</strong></p>\n<ul>\n<li>异步共享状态变为就绪的事件与该共享状态上调度延续函数的调用同步。</li>\n<li>持续性函数的完成与成功调用wait或get的返回同步,或调用wait_for或wait_until返回的期望值状态std::future_status::ready与调用then构建的持续性返回的future同步,或是与在调度用使用这个future的操作同步。</li>\n</ul>\n<p><strong>std::experimental::latch</strong></p>\n<ul>\n<li>对std::experimental::latch实例调用count_down或count_down_and_wait与在该对象上成功的调用wait或count_down_and_wait之间是同步的。</li>\n</ul>\n<p><strong>std::experimental::barrier</strong></p>\n<ul>\n<li>对std::experimental::barrier实例调用arrive_and_wait或arrive_and_drop与在该对象上随后成功完成的arrive_and_wait之间是同步的。</li>\n</ul>\n<p><strong>std::experimental::flex_barrier</strong></p>\n<ul>\n<li>对std::experimental::flex_barrier实例调用arrive_and_wait或arrive_and_drop与在该对象上随后成功完成的arrive_and_wait之间是同步的。</li>\n<li>对std::experimental::flex_barrier实例调用arrive_and_wait或arrive_and_drop与在该对象上随后完成的给定函数之间是同步的。</li>\n<li>对std::experimental::flex_barrier实例的给定函数的返回与每次对arrive_and_wait的调用同步,当调用给定函数线程会在栅栏处阻塞等待。</li>\n</ul>\n<p><strong>std::condition_variable和std::condition_variable_any</strong></p>\n<ul>\n<li>条件变量不提供任何同步关系,它们是对忙等待的优化,所有同步都由互斥量提供。</li>\n</ul>\n<h2>CPP Reference:<code>std::memory_order</code></h2>\n<table>\n<thead>\n<tr>\n<th>Value</th>\n<th>Explanation</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>memory_order_relaxed</code></td>\n<td>Relaxed operation: there are no synchronization or ordering constraints imposed on other reads or writes, only this operation's atomicity is guaranteed (see <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering\">Relaxed ordering</a> below)</td>\n</tr>\n<tr>\n<td><code>memory_order_consume</code></td>\n<td>A load operation with this memory order performs a <em>consume operation</em> on the affected memory location: no reads or writes in the current thread dependent on the value currently loaded can be reordered before this load. Writes to data-dependent variables in other threads that release the same atomic variable are visible in the current thread. On most platforms, this affects compiler optimizations only (see <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Consume_ordering\">Release-Consume ordering</a> below)</td>\n</tr>\n<tr>\n<td><code>memory_order_acquire</code></td>\n<td>A load operation with this memory order performs the <em>acquire operation</em> on the affected memory location: no reads or writes in the current thread can be reordered before this load. All writes in other threads that release the same atomic variable are visible in the current thread (see <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering\">Release-Acquire ordering</a> below)</td>\n</tr>\n<tr>\n<td><code>memory_order_release</code></td>\n<td>A store operation with this memory order performs the <em>release operation</em>: no reads or writes in the current thread can be reordered after this store. All writes in the current thread are visible in other threads that acquire the same atomic variable (see <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering\">Release-Acquire ordering</a> below) and writes that carry a dependency into the atomic variable become visible in other threads that consume the same atomic (see <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Consume_ordering\">Release-Consume ordering</a> below).</td>\n</tr>\n<tr>\n<td><code>memory_order_acq_rel</code></td>\n<td>A read-modify-write operation with this memory order is both an <em>acquire operation</em> and a <em>release operation</em>. No memory reads or writes in the current thread can be reordered before or after this store. All writes in other threads that release the same atomic variable are visible before the modification and the modification is visible in other threads that acquire the same atomic variable.</td>\n</tr>\n<tr>\n<td><code>memory_order_seq_cst</code></td>\n<td>A load operation with this memory order performs an <em>acquire operation</em>, a store performs a <em>release operation</em>, and read-modify-write performs both an <em>acquire operation</em> and a <em>release operation</em>, plus a single total order exists in which all threads observe all modifications in the same order (see <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order#Sequentially-consistent_ordering\">Sequentially-consistent ordering</a> below)</td>\n</tr>\n</tbody>\n</table>\n<p>原文链接: <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order\">Cpp Reference</a></p>\n<h2>Rust 死灵书: Atomics</h2>\n<p>相关类型和 Ordering 定义在 <code>std::sync::atomic</code> 下。</p>\n<p>Rust 在 Atomics Ordering 的设计上继承了C++20,这并不是因为这个模型设计的有多么出色,多么容易理解。事实上,这个模型很复杂,而且有一些已知的 <a href=\"http://plv.mpi-sws.org/c11comp/popl15.pdf\">缺陷</a>。</p>\n<p>Rather, it is a pragmatic concession to the fact that <em>everyone</em> is pretty bad at modeling atomics. At very least, we can benefit from existing tooling and research around the C/C++ memory model. (You'll often see this model referred to as "C/C++11" or just "C11". C just copies the C++ memory model; and C++11 was the first version of the model but it has received some bugfixes since then.)</p>\n<p>Trying to fully explain the model in this book is fairly hopeless. It's defined in terms of madness-inducing causality graphs that require a full book to properly understand in a practical way. If you want all the nitty-gritty details, you should check out the <a href=\"https://en.cppreference.com/w/cpp/atomic/memory_order\">C++ specification</a>. Still, we'll try to cover the basics and some of the problems Rust developers face.</p>\n<p>The C++ memory model is fundamentally about trying to bridge the gap between the semantics we want, the optimizations compilers want, and the inconsistent chaos our hardware wants. <em>We</em> would like to just write programs and have them do exactly what we said but, you know, fast. Wouldn't that be great?</p>\n<p>原文链接: <a href=\"https://doc.rust-lang.org/nomicon/atomics.html\">The Rustonomicon</a></p>\n<h2>AtomicF64 的实现</h2>\n<p>当然用 <code>AtomicPtr</code> 实现,替换掉指针是可以的。但是有一种更加有趣的做法是用 <code>AtomicU64</code> 来存储,然后写入和读取的时候做转换就行了。</p>\n<p>例如,在 <a href=\"https://github.com/tikv/rust-prometheus\">Prometheus 的 Rust Client</a> 实现中,就是这么干的,直接用了 <code>f64::from_bits()</code> 和 <code>f64::to_bits()</code> 做转化:</p>\n<pre><code class=\"language-rust\">//! prometheus-0.12.0/src/atomic64.rs\n/// A atomic float.\n#[derive(Debug)]\npub struct AtomicF64 {\n inner: StdAtomicU64,\n}\n\n#[inline]\nfn u64_to_f64(val: u64) -> f64 {\n f64::from_bits(val)\n}\n\n#[inline]\nfn f64_to_u64(val: f64) -> u64 {\n f64::to_bits(val)\n}\n\nimpl Atomic for AtomicF64 {\n type T = f64;\n\n fn new(val: Self::T) -> AtomicF64 {\n AtomicF64 {\n inner: StdAtomicU64::new(f64_to_u64(val)),\n }\n }\n\n #[inline]\n fn set(&self, val: Self::T) {\n self.inner.store(f64_to_u64(val), Ordering::Relaxed);\n }\n\n #[inline]\n fn get(&self) -> Self::T {\n u64_to_f64(self.inner.load(Ordering::Relaxed))\n }\n\n #[inline]\n fn inc_by(&self, delta: Self::T) {\n loop {\n let current = self.inner.load(Ordering::Acquire);\n let new = u64_to_f64(current) + delta;\n let result = self.inner.compare_exchange_weak(\n current,\n f64_to_u64(new),\n Ordering::Release,\n Ordering::Relaxed,\n );\n if result.is_ok() {\n return;\n }\n }\n }\n\n #[inline]\n fn dec_by(&self, delta: Self::T) {\n self.inc_by(-delta);\n }\n}\n\nimpl AtomicF64 {\n /// Store the value, returning the previous value.\n pub fn swap(&self, val: f64, ordering: Ordering) -> f64 {\n u64_to_f64(self.inner.swap(f64_to_u64(val), ordering))\n }\n}\n</code></pre>\n",
"url": "https://forsworns.github.io///zh/blogs/20210822/",
"title": "原子类型及原子操作的内存顺序",
"summary": "从内存模型谈起:C++/Rust 原子类型、原子操作的内存顺序",
"date_modified": "2021-08-22T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>谈元编程与表达能力</h1>\n<p>本文转载自<a href=\"https://draveness.me/metaprogramming/\">谈元编程与表达能力 - 面向信仰编程 (draveness.me)</a>,最开始是关注作者公众号“真没什么逻辑”,找到他的博客的,是大佬。</p>\n<p>在这篇文章中,作者会介绍不同的编程语言如何增强自身的表达能力,在写这篇文章的时候其实就已经想到这可能不是一篇有着较多受众和读者的文章。不过作者仍然想跟各位读者分享一下对不同编程语言的理解,同时也对自己的知识体系进行简单的总结。</p>\n<p><img src=\"2017-12-10-metaprogramming.png\" alt=\"metaprogramming\"></p>\n<p>当我们刚刚开始学习和了解编程这门手艺或者说技巧时,一切的知识与概念看起来都非常有趣,随着学习的深入和对语言的逐渐了解,我们可能会发现原来看起来无所不能的编程语言成为了我们的限制,尤其是在我们想要使用一些<strong>元编程</strong>技巧的时候,你会发现有时候语言限制了我们的能力,我们只能一遍一遍地写重复的代码来解决本可以轻松搞定的问题。</p>\n<h2>元编程</h2>\n<p>元编程(Metaprogramming)是计算机编程中一个非常重要、有趣的概念,<a href=\"https://en.wikipedia.org/wiki/Metaprogramming\">维基百科</a> 上将元编程描述成一种计算机程序可以<strong>将代码看待成数据</strong>的能力。</p>\n<blockquote>\n<p>Metaprogramming is a programming technique in which computer programs have the ability to treat programs as their data.</p>\n</blockquote>\n<p>如果能够将代码看做数据,那么代码就可以像数据一样在运行时被修改、更新和替换;元编程赋予了编程语言更加强大的表达能力,能够让我们将一些计算过程从运行时挪到编译时、通过编译期间的展开生成代码或者允许程序在运行时改变自身的行为。</p>\n<p><img src=\"2017-12-10-metaprogramming-usage.png\" alt=\"metaprogramming-usage\"></p>\n<p>总而言之,<strong>元编程其实是一种使用代码生成代码的方式</strong>,无论是编译期间生成代码,还是在运行时改变代码的行为都是『生成代码』的一种,下面的代码其实就可以看作一种最简单的元编程技巧:</p>\n<pre><code class=\"language-c\">int main() {\n for(int i = 0; i < 10; i++) {\n char *echo = (char*)malloc(6 * sizeof(char));\n sprintf(echo, "echo %d", i);\n system(echo);\n }\n return 0;\n}\n</code></pre>\n<p>这里的代码其实等价于执行了以下的 shell 脚本,也可以说这里使用了 C 语言的代码生成来生成 shell 脚本:</p>\n<pre><code class=\"language-shell\">echo 0\necho 1\n...\necho 9\n</code></pre>\n<h2>编译时和运行时</h2>\n<p>现代的编程语言大都会为我们提供不同的元编程能力,从总体来看,根据『生成代码』的时机不同,我们将元编程能力分为两种类型,其中一种是编译期间的元编程,例如:宏和模板;另一种是运行期间的元编程,也就是运行时,它赋予了编程语言在运行期间修改行为的能力,当然也有一些特性既可以在编译期实现,也可以在运行期间实现。</p>\n<p><img src=\"2017-12-10-compile-and-execute.png\" alt=\"compile-and-execute\"></p>\n<p>不同的语言对于泛型就有不一样的实现,Java 的泛型就是在编译期间实现的,它的泛型其实是伪泛型,在编译期间所有的泛型就会被编译器擦除(type erasure),生成的 Java 字节码是不包含任何的泛型信息的,但是 C# 对于泛型就有着不同的实现了,它的泛型类型在运行时进行替换,为实例化的对象保留了泛型的类型信息。</p>\n<blockquote>\n<p>C++ 的模板其实与这里讨论的泛型有些类似,它会为每一个具体类型生成一份独立的代码,而 Java 的泛型只会生成一份经过类型擦除后的代码,总而言之 C++ 的模板完全是在编译期间实现的,而 Java 的泛型是编译期间和运行期间协作产生的;模板和泛型虽然非常类似,但是在这里提到的模板大都特指 C++ 的模板,而泛型这一概念其实包含了 C++ 的模板。</p>\n</blockquote>\n<p>虽然泛型和模板为各种编程语言提供了非常强大的表达能力,但是在这篇文章中,我们会介绍另外两种元编程能力:<em>宏</em>和<em>运行时</em>,前者是在编译期间完成的,而后者是在代码运行期间才发生的。</p>\n<h2>宏(Macro)</h2>\n<p>宏是很多编程语言具有的特性之一,它是一个将输入的字符串映射成其他字符串的过程,这个映射的过程也被我们称作宏展开。</p>\n<p><img src=\"2017-12-10-macro-expansion.png\" alt=\"macro-expansion\"></p>\n<p>宏其实就是一个在编译期间中定义的展开过程,通过预先定义好的宏,我们可以使用少量的代码完成更多的逻辑和工作,能够减少应用程序中大量的重复代码。</p>\n<p>很多编程语言,尤其是编译型语言都实现了宏这个特性,包括 C、Elixir 和 Rust,然而这些语言却使用了不同的方式来实现宏;我们在这里会介绍两种不同的宏,一种是基于文本替换的宏,另一种是基于语法的宏。</p>\n<p><img src=\"2017-12-10-different-kinds-of-macros.png\" alt=\"different-kinds-of-macros\"></p>\n<p>C、C++ 等语言使用基于文本替换的宏,而类似于 Elixir、Rust 等语言的宏系统其实都是基于语法树和语法元素的,它的实现会比前者复杂很多,应用也更加广泛。</p>\n<p>在这一节的剩余部分,我们会分别介绍 C、Elixir 和 Rust 三种不同的编程语言实现的宏系统,它们的使用方法、适用范围和优缺点。</p>\n<h3>C</h3>\n<p>作者相信很多工程师入门使用的编程语言其实都是 C 语言,而 C 语言的宏系统看起来还是相对比较简单的,虽然在实际使用时会遇到很多非常诡异的问题。C 语言的宏使用的就是文本替换的方式,所有的宏其实并不是通过编译器展开的,而是由预编译器来处理的。</p>\n<p><img src=\"2017-12-10-preprocessor.png\" alt=\"preprocesso\"></p>\n<p>编译器 GCC 根据『长相』将 C 语言中的宏分为两种,其中的一种宏与编程语言中定义变量非常类似:</p>\n<pre><code class=\"language-c\">#define BUFFER_SIZE 1024\n\nchar *foo = (char *)malloc(BUFFER_SIZE);\nchar *foo = (char *)malloc(1024);\n</code></pre>\n<p>这些宏的定义就是一个简单的标识符,它们会在预编译的阶段被预编译器替换成定义后半部分出现的<strong>字符</strong>,这种宏定义其实比较类似于变量的声明,我们经常会使用这种宏定义替代一些无意义的数字,能够让程序变得更容易理解。</p>\n<p>另一种宏定义就比较像对函数的定义了,与其他 C 语言的函数一样,这种宏在定义时也会包含一些宏的参数:</p>\n<pre><code class=\"language-c\">#define plus(a, b) a + b\n#define multiply(a, b) a * b\n</code></pre>\n<p>通过在宏的定义中引入参数,宏定义的内部就可以直接使用对应的标识符引入外界传入的参数,在定义之后我们就可以像使用函数一样使用它们:</p>\n<pre><code class=\"language-c\">#define plus(a, b) a + b\n#define multiply(a, b) a * b\n\nint main(int argc, const char * argv[]) {\n printf("%d", plus(1, 2)); // => 3\n printf("%d", multiply(3, 2)); // => 6\n return 0;\n}\n</code></pre>\n<p>上面使用宏的代码与下面的代码是完全等价的,在预编译阶段之后,上面的代码就会被替换成下面的代码,也就是编译器其实是不负责宏展开的过程:</p>\n<pre><code class=\"language-c\">int main(int argc, const char * argv[]) {\n printf("%d", 1 + 2); // => 3\n printf("%d", 3 * 2); // => 6\n return 0;\n}\n</code></pre>\n<p>宏的作用其实非常强大,基于文本替换的宏能做到很多函数无法做到的事情,比如使用宏根据传入的参数创建类并声明新的方法:</p>\n<pre><code class=\"language-c\">#define pickerify(KLASS, PROPERTY) interface \\\n KLASS (Night_ ## PROPERTY ## _Picker) \\\n @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \\\n @end \\\n @implementation \\\n KLASS (Night_ ## PROPERTY ## _Picker) \\\n - (DKColorPicker)dk_ ## PROPERTY ## Picker { \\\n return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \\\n } \\\n - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \\\n objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \\\n [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\\\n NSMutableDictionary *pickers = [self valueForKeyPath:@"pickers"];\\\n [pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \\\n } \\\n @end\n\n@pickerify(Button, backgroundColor);\n</code></pre>\n<p>上面的代码是我在一个 iOS 的开源库 <a href=\"https://github.com/Draveness/DKNightVersion/blob/master/DKNightVersion/DKNightVersion.h#L57-L72\">DKNightVersion</a> 中使用的代码,通过宏的文本替换功能,我们在这里创建了类、属性并且定义了属性的 getter/setter 方法,然而使用者对此其实是一无所知的。</p>\n<p>C 语言中的宏只是提供了一些文本替换的功能再加上一些高级的 API,虽然它非常强大,但是强大的事物都是一把双刃剑,再加上 C 语言的宏从实现原理上就有一些无法避免的缺陷,所以在使用时还是要非常小心。</p>\n<p>由于预处理器只是对宏进行替换,并没有做任何的语法检查,所以在宏出现问题时,编译器的报错往往会让我们摸不到头脑,不知道哪里出现了问题,还需要脑内对宏进行展开分析出现错误的原因;除此之外,类似于 <code>multiply(1+2, 3)</code> 的展开问题导致人和机器对于同一段代码的理解偏差,作者相信也广为人知了;更高级一些的<strong>分号吞噬</strong>、<strong>参数的重复调用</strong>以及<strong>递归引用时不会递归展开</strong>等问题其实在这里也不想多谈。</p>\n<pre><code class=\"language-c\">multiply(1+2, 3) // #=> 1+2 * 3\n</code></pre>\n<h4>卫生宏</h4>\n<p>然而 C 语言宏的实现导致的另一个问题却是非常严重的:</p>\n<pre><code class=\"language-c\">#define inc(i) do { int a = 0; ++i; } while(0)\n\nint main(int argc, const char * argv[]) {\n int a = 4, b = 8;\n inc(a);\n inc(b);\n printf("%d, %d\\n", a, b); // => 4, 9 !!\n return 0;\n}\n</code></pre>\n<blockquote>\n<p>这一小节与卫生宏有关的 C 语言代码取自 <a href=\"https://en.wikipedia.org/wiki/Hygienic_macro\">Hygienic macro</a> 中的代码示例。</p>\n</blockquote>\n<p>上述代码中的 <code>printf</code> 函数理应打印出 <code>5, 9</code> 然而却打印出了 <code>4, 9</code>,我们来将上述代码中使用宏的部分展开来看一下:</p>\n<pre><code class=\"language-c\">int main(int argc, const char * argv[]) {\n int a = 4, b = 8;\n do { int a = 0; ++a; } while(0);\n do { int a = 0; ++b; } while(0);\n printf("%d, %d\\n", a, b); // => 4, 9 !!\n return 0;\n}\n</code></pre>\n<p>这里的 <code>a = 0</code> 按照逻辑应该不发挥任何的作用,但是在这里却覆盖了上下文中 <code>a</code> 变量的值,导致父作用域中变量 <code>a</code> 的值并没有 <code>+1</code>,这其实就是因为 C 语言中实现的宏不是<em>卫生宏</em>(Hygiene macro)。</p>\n<p>作者认为卫生宏(Hygiene macro)是一个非常让人困惑的翻译,它其实指一些<strong>在宏展开之后不会意外捕获上下文中标识符的宏</strong>,从定义中我们就可以看到 C 语言中的宏明显不是卫生宏,而接下来要介绍的两种语言的宏系统就实现了卫生宏。</p>\n<h3>Elixir</h3>\n<p>Elixir 是一门动态的函数式编程语言,它被设计用来构建可扩展、可维护的应用,所有的 Elixir 代码最终都会被编译成二进制文件运行在 Erlang 的虚拟机 Beam 上,构建在 Erlang 上的 Elixir 也继承了很多 Erlang 的优秀特性。然而在这篇文章中并不会展开介绍 Elixir 语言以及它的某些特点和应用,我们只想了解 Elixir 中的宏系统是如何使用和实现的。</p>\n<p><img src=\"2017-12-10-elixir-logo.png\" alt=\"elixir-logo\"></p>\n<p>宏是 Elixir 具有强大表达能力的一个重要原因,通过内置的宏系统可以减少系统中非常多的重复代码,我们可以使用 <code>defmacro</code> 定义一个宏来实现 <code>unless</code> 关键字:</p>\n<pre><code class=\"language-elixir\">defmodule Unless do\n defmacro macro_unless(clause, do: expression) do\n quote do\n if(!unquote(clause), do: unquote(expression))\n end\n end\nend\n</code></pre>\n<p>Elixir</p>\n<p>这里的 <code>quote</code> 和 <code>unquote</code> 是宏系统中最重要的两个函数,你可以从字面上理解 <code>quote</code> 其实就是在一段代码的两侧加上双引号,让这段代码变成字符串,而 <code>unquote</code> 会将传入的多个参数的文本<strong>原封不动</strong>的插入到相应的位置,你可以理解为 <code>unquote</code> 只是将 <code>clause</code> 和 <code>expression</code> 代表的字符串当做了返回值。</p>\n<pre><code class=\"language-elixir\">Unless.macro_unless true, do: IO.puts "this should never be printed"\n</code></pre>\n<p>上面的 Elixir 代码在真正执行之前会被替换成一个使用 <code>if</code> 的表达式,我们可以使用下面的方法获得宏展开之后的代码:</p>\n<pre><code class=\"language-elixir\">iex> expr = quote do: Unless.macro_unless true, do: IO.puts "this should never be printed"\niex> expr |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.puts\nif(!true) do\n IO.puts("this should never be printed")\nend\n:ok\n</code></pre>\n<p>当我们为 <code>quote</code> 函数传入一个表达式的时候,它会将当前的表达式转换成一个抽象语法树:</p>\n<pre><code class=\"language-elixir\">{% raw %}{{:., [], [{:__aliases__, [alias: false], [:Unless]}, :macro_unless]}, [],\n [true,\n [do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],\n ["this should never be printed"]}]]}{% endraw %}\n</code></pre>\n<p>在 Elixir 中,抽象语法数是可以直接通过下面的 <code>Code.eval_quoted</code> 方法运行:</p>\n<pre><code class=\"language-elixir\">iex> Code.eval_quoted [expr]\n** (CompileError) nofile:1: you must require Unless before invoking the macro Unless.macro_unless/2\n (elixir) src/elixir_dispatch.erl:97: :elixir_dispatch.dispatch_require/6\n (elixir) lib/code.ex:213: Code.eval_quoted/3\niex> Code.eval_quoted [quote(do: require Unless), expr]\n{[Unless, nil], []}\n</code></pre>\n<p>我们只运行当前的语法树,我们会发现当前的代码由于 <code>Unless</code> 模块没有加载导致宏找不到报错,所以我们在执行 <code>Unless.macro_unless</code> 之前需要先 <code>require</code> 对应的模块。</p>\n<p><img src=\"2017-12-10-elixir-macro.png\" alt=\"elixir-macro\"></p>\n<p>在最开始对当前的宏进行定义时,我们就会发现宏其实输入的是一些语法元素,实现内部也通过 <code>quote</code> 和 <code>unquote</code> 方法对当前的语法树进行修改,最后返回新的语法树:</p>\n<pre><code class=\"language-elixir\">{% raw %}defmacro macro_unless(clause, do: expression) do\n quote do\n if(!unquote(clause), do: unquote(expression))\n end\nend\n\niex> expr = quote do: Unless.macro_unless true, do: IO.puts "this should never be printed"\n{{:., [], [{:__aliases__, [alias: false], [:Unless]}, :macro_unless]}, [],\n [true,\n [do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],\n ["this should never be printed"]}]]}\n\niex> Macro.expand_once expr, __ENV__\n{:if, [context: Unless, import: Kernel],\n [{:!, [context: Unless, import: Kernel], [true]},\n [do: {{:., [],\n [{:__aliases__, [alias: false, counter: -576460752303422687], [:IO]},\n :puts]}, [], ["this should never be printed"]}]]}{% endraw %}\n</code></pre>\n<p>Elixir 中的宏相比于 C 语言中的宏更强大,这是因为它不是对代码中的文本直接进行替换,它能够为我们直接提供操作 Elixir 抽象语法树的能力,让我们能够参与到 Elixir 的编译过程,影响编译的结果;除此之外,Elixir 中的宏还是卫生宏(Hygiene Macro),宏中定义的参数并不会影响当前代码执行的上下文。</p>\n<pre><code class=\"language-elixir\">defmodule Example do\n defmacro hygienic do\n quote do\n val = 1\n end\n end\nend\n\niex> val = 42\n42\niex> Example.hygienic\n1\niex> val\n42\n</code></pre>\n<p>在上述代码中,虽然宏内部的变量与当前环境上下文中的变量重名了,但是宏内部的变量并没有影响上下文中 <code>val</code> 变量的变化,所以 Elixir 中宏系统是『卫生的』,如果我们真的想要改变上下文中的变量,可以使用 <code>var!</code> 来做这件事情:</p>\n<pre><code class=\"language-elixir\">defmodule Example do\n defmacro unhygienic do\n quote do\n var!(val) = 2\n end\n end\nend\n\niex> val = 42\n42\niex> Example.unhygienic\n2\niex> val\n2\n</code></pre>\n<p>相比于使用文本替换的 C 语言宏,Elixir 的宏系统解决了很多问题,例如:卫生宏,不仅如此,Elixir 的宏还允许我们修改当前的代码中的语法树,提供了更加强大的表达能力。</p>\n<h3>Rust</h3>\n<p>Elixir 的宏系统其实已经足够强大了,不止避免了基于文本替换的宏带来的各种问题,我们还可以直接使用宏操作上下文的语法树,作者在一段时间内都觉得 Elixir 的宏系统是接触到的最强大的宏系统,直到开始学习 <a href=\"https://www.rust-lang.org/en-US/\">Rust</a> 才发现更复杂的宏系统。</p>\n<p><img src=\"2017-12-10-rust-logo.png\" alt=\"rust-logo\"></p>\n<p>Rust 是一门非常有趣的编程语言,它是一门有着极高的性能的系统级的编程语言,能够避免当前应用中发生的段错误并且保证线程安全和内存安全,但是这些都不是我们今天想要关注的事情,与 Elixir 一样,在这篇文章中我们仅仅关心 Rust 的宏系统到底是什么样的:</p>\n<pre><code class=\"language-rust\">macro_rules! foo {\n (x => $e:expr) => (println!("mode X: {}", $e));\n (y => $e:expr) => (println!("mode Y: {}", $e));\n}\n</code></pre>\n<p>上面的 Rust 代码定义了一个名为 <code>foo</code> 的宏,我们在代码中需要使用 <code>foo!</code> 来调用上面定义的宏:</p>\n<pre><code class=\"language-rust\">fn main() {\n foo!(y => 3); // => mode Y: 3\n}\n</code></pre>\n<p>上述的宏 <code>foo</code> 的主体部分其实会将传入的<strong>语法元素</strong>与宏中的条件进行模式匹配,如果匹配到了,就会返回条件右侧的表达式,到这里其实与 Elixir 的宏系统没有太大的区别,Rust 宏相比 Elixir 更强大主要在于其提供了更加灵活的匹配系统,在宏 <code>foo</code> 的定义中使用的 <code>$e:expr</code> 就会匹配一个表达式并将表达式绑定到 <code>$e</code> 这个上下文的变量中,除此之外,在 Rust 中我们还可以组合使用以下的匹配符:</p>\n<p><img src=\"2017-12-10-rust-macro-matcher-and-example.png\" alt=\"rust-macro-matcher-and-example\"></p>\n<p>为了实现功能更强大的宏系统,Rust 的宏还提供了重复操作符和递归宏的功能,结合这两个宏系统的特性,我们能直接使用宏构建一个生成 HTML 的 DSL:</p>\n<pre><code class=\"language-rust\">{% raw %}macro_rules! write_html {\n ($w:expr, ) => (());\n\n ($w:expr, $e:tt) => (write!($w, "{}", $e));\n\n ($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{\n write!($w, "<{}>", stringify!($tag));\n write_html!($w, $($inner)*);\n write!($w, "</{}>", stringify!($tag));\n write_html!($w, $($rest)*);\n }};\n}{% endraw %}\n</code></pre>\n<p>在上述的 <code>write_html</code> 宏中,我们总共有三个匹配条件,其中前两个是宏的终止条件,第一个条件不会做任何的操作,第二个条件会将匹配到的 Token 树求值并写回到传入的字符串引用 <code>$w</code> 中,最后的条件就是最有意思的部分了,在这里我们使用了形如的 <code>$(...)*</code> 语法来<strong>匹配零个或多个相同的语法元素</strong>,例如 <code>((inner:tt)*</code> 就是匹配零个以上的 Token 树(tt);在右侧的代码中递归调用了 <code>write_html</code> 宏并分别传入 <code>((inner)*</code> 和 <code>((rest)*</code> 两个参数,这样我们的 <code>write_html</code> 就能够解析 DSL 了。</p>\n<p>有了 <code>write_html</code> 宏,我们就可以直接使用形如 <code>html[head[title["Macros guide"]]</code> 的代码返回如下所示的 HTML:</p>\n<pre><code class=\"language-html\"><html><head><title>Macros guide</title></head></html>\n</code></pre>\n<blockquote>\n<p>这一节中提供的与 Rust 宏相关的例子都取自 <a href=\"https://doc.rust-lang.org/book/first-edition/macros.html\">官方文档</a> 中对宏的介绍这一部分内容。</p>\n</blockquote>\n<p>Rust 的宏系统其实是基于一篇 1986 年的论文 <a href=\"https://www.cs.indiana.edu/ftp/techreports/TR206.pdf\">Macro-by-Example</a> 实现的,如果想要深入了解 Rust 的宏系统可以阅读这篇论文;Rust 的宏系统确实非常完备也足够强大,能够做很多我们使用 C 语言宏时无法做到的事情,极大地提高了语言的表达能力。</p>\n<h2>运行时(Runtime)</h2>\n<p>宏是一种能在程序执行的预编译或者编译期间改变代码行为的能力,通过编译期的处理过程赋予编程语言元编程能力;而运行时,顾名思义一般是指<strong>面向对象</strong>的编程语言在程序运行的某一个时间的上下文,在这里我们想要介绍的运行时可以理解为<strong>能够在运行期间改变对象行为的机制</strong>。</p>\n<p><img src=\"2017-12-10-phases.png\" alt=\"phases\"></p>\n<p>当相应的行为在当前对象上没有被找到时,运行时会提供一个改变当前对象行为的入口,在篇文章中提到的运行时不是广义上的运行时系统,它特指<strong>面向对象语言在方法决议的过程中为外界提供的入口,让工程师提供的代码也能参与到当前的方法决议和信息发送的过程</strong>。</p>\n<p>在这一节中,我们将介绍的两个使用了运行时的面向对象编程语言 Objective-C 和 Ruby,它们有着相似的消息发送的流程,但是由于 OOP 模型实现的不同导致方法调用的过程稍微有一些差别;除此之外,由于 Objective-C 是需要通过编译器编译成二进制文件才能执行的,而 Ruby 可以直接被各种解释器运行,所以两者的元编程能力也会受到这一差别的影响,我们会在下面展开进行介绍。</p>\n<h3>Objective-C</h3>\n<p>Objective-C 是一种通用的面向对象编程语言,它将 Smalltalk 消息发送的语法引入了 C 语言;ObjC 语言的面向对象模型其实都是运行在 ObjC Runtime 上的,整个运行时也为 ObjC 提供了方法查找的策略。</p>\n<p><img src=\"2017-12-10-objc-class-hierachy.png\" alt=\"objc-class-hierachy\"></p>\n<p>如上图所示,我们有一个 <code>Dog</code> 类的实例,当我们执行了 <code>dog.wtf</code> 方法时,运行时会先向右再向上的方式在整个继承链中查找相应的方法是否存在,如果当前方法在整个继承链中都完全不存在就会进入<strong>动态方法决议</strong>和<strong>消息转发</strong>的过程。</p>\n<p><img src=\"2017-12-10-objc-message-resolution-and-forwarding.png\" alt=\"objc-message-resolution-and-forwarding\"></p>\n<blockquote>\n<p>上述图片取自 <a href=\"https://draveness.me/racdelegateproxy\">从代理到 RACSignal</a>,使用时对图片中的颜色以及字号稍作修改。</p>\n</blockquote>\n<p>当 ObjC 的运行时在方法查找的过程中已经查找到了上帝类 <code>NSObject</code> 时,仍然没有找到方法的实现就会进入上面的流程,先执行的 <code>+resolveInstanceMethod:</code> 方法就是一个可以为当前的类添加方法的入口:</p>\n<pre><code class=\"language-objc\">void dynamicMethodIMP(id self, SEL _cmd) { }\n\n+ (BOOL)resolveInstanceMethod:(SEL)aSEL {\n if (aSEL == @selector(resolveThisMethodDynamically)) {\n class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");\n return YES;\n }\n return [super resolveInstanceMethod:aSel];\n}\n</code></pre>\n<p>在这里可以通过 <code>class_addMethod</code> 动态的为当前的类添加新的方法和对应的实现,如果错过了这个入口,我们就进入了消息转发的流程;在这里,我们有两种选择,一种情况是通过 <code>-forwardTargetForSelector:</code> 将当前方法的调用直接转发到其他方法上,另一种就是组合 <code>-methodSignatureForSelector:</code> 和 <code>-forwardInvocation:</code> 两个方法,直接执行一个 <code>NSInvocation</code> 对象。</p>\n<pre><code class=\"language-objc\">- (void)forwardInvocation:(NSInvocation *)anInvocation {\n if ([someOtherObject respondsToSelector:[anInvocation selector]]) {\n [anInvocation invokeWithTarget:someOtherObject];\n } else {\n [super forwardInvocation:anInvocation];\n }\n}\n</code></pre>\n<p><code>-forwardTargetForSelector:</code> 方法只能简单地将方法直接转发给其他的对象,但是在 <code>-forwardInvocation:</code> 中我们可以得到一个 <code>NSInvocation</code> 实例,可以自由地选择需要执行哪些方法,并修改当前方法调用的上下文,包括:方法名、参数和目标对象。</p>\n<p>虽然 Objective-C 的运行时系统能够为我们提供动态方法决议的功能,也就是某一个方法在编译期间哪怕不存在,我们也可以在运行时进行调用,这虽然听起来很不错,在很多时候我们都可以通过 <code>-performSelector:</code> 调用<strong>编译器看起来不存的方法</strong>,但是作为一门执行之前需要编译的语言,如果我们在 <code>+resolveInstanceMethod:</code> 中确实动态实现了一些方法,但是编译器在编译期间对这一切都毫不知情。</p>\n<pre><code class=\"language-objc\">void dynamicMethodIMP(id self, SEL _cmd) { }\n+ (BOOL)resolveInstanceMethod:(SEL)aSEL {\n NSString *selector = NSStringFromSelector(aSEL);\n if ([selector hasPrefix:@"find"]) {\n class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");\n return YES;\n }\n return [super resolveInstanceMethod:aSel];\n}\n\n- (void)func {\n [self findFoo];\n [self findBar];\n [self find];\n}\n</code></pre>\n<p>从 <code>-func</code> 中调用的三个以 <code>find</code> 开头的方法其实会在运行期间添加到当前类上,但是编译器在编译期间对此一无所知,所以它会提示编译错误,在编译期间将可以运行的代码拦截了下来,这样的代码如果跳过编译器检查,直接运行是不会出问题的,但是代码的执行必须通过编译器编译,这一过程是无法跳过的。</p>\n<p><img src=\"2017-12-10-objc-compile-and-execute.png\" alt=\"objc-compile-and-execute\"></p>\n<p>我们只能通过 <code>-performSelector:</code> 方法绕过编译器的检查,不过使用 <code>-performSelector:</code> 会为代码添加非常多的噪音:</p>\n<pre><code class=\"language-objc\">- (void)func {\n [self performSelector:@selector(findFoo)];\n [self performSelector:@selector(findBar)];\n [self performSelector:@selector(find)];\n}\n</code></pre>\n<p>所以虽然 Objective-C 通过运行时提供了比较强大的元编程能力,但是由于代码执行时需要经过编译器的检查,所以在很多时候我们都没有办法直接发挥运行时为我们带来的好处,需要通过其他的方式完成方法的调用。</p>\n<h3>Ruby</h3>\n<p>除了 Objective-C 之外,Ruby 也提供了一些相似的运行时修改行为的特性,它能够在运行时修改自身特性的功能还是建立在它的 OOP 模型之上;Ruby 提供了一些在运行期间能够改变自身行为的入口和 API 可以帮助我们快速为当前的类添加方法或者实例变量。</p>\n<p><img src=\"2017-12-10-ruby-class-hierachy.png\" alt=\"ruby-class-hierachy\"></p>\n<p>当我们调用 <code>Dog</code> 实例的一个方法时,Ruby 会先找到当前对象的类,然后在由 <code>superclass</code> 构成的链上查找并调用相应的方法,这是 OOP 中非常常见的,<strong>向右再向上</strong>的方法查找过程。</p>\n<p>与 Objective-C 几乎相同,Ruby 也提供了类似与 <code>+resolveInstanceMethod:</code> 的方法,如果方法在整个继承链上都完全不存在时,就会调用 <code>#method_missing</code> 方法,并传入与这次方法调用有关的参数:</p>\n<pre><code class=\"language-ruby\">def method_missing(method, *args, &block)\nend\n</code></pre>\n<p>传入的参数包括方法的符号,调用原方法时传入的参数和 block,在这里我们就可以为当前的类添加方法了:</p>\n<pre><code class=\"language-ruby\">class Dog\n def method_missing(m, *args, &block)\n if m.to_s.start_with? 'find'\n define_singleton_method(m) do |*args|\n puts "#{m}, #{args}"\n end\n send(m, *args, &block)\n else\n super\n end\n end\nend\n</code></pre>\n<p>通过 Ruby 提供的一些 API,例如 <code>define_method</code>、<code>define_singleton_method</code> 我们可以直接在运行期间快速改变对象的行为,在使用时也非常简单:</p>\n<pre><code class=\"language-ruby\">pry(main)> d = Dog.new\n=> #<Dog:0x007fe31e3f87a8>\npry(main)> d.find_by_name "dog"\nfind_by_name, ["dog"]\n=> nil\npry(main)> d.find_by_name "dog", "another_dog"\nfind_by_name, ["dog", "another_dog"]\n=> nil\n</code></pre>\n<p>当我们调用以 <code>find</code> 开头的实例方法时,由于在当前实例的类以及父类上没有实现,所以就会进入 <code>#method_missing</code> 方法并为<strong>当前实例</strong>定义新的方法 <code>#find_by_name</code>。</p>\n<blockquote>\n<p>注意:当前的 <code>#find_by_name</code> 方法只是定义在当前实例上的,存储在当前实例的单类上。</p>\n</blockquote>\n<p>由于 Ruby 是脚本语言,解释器在脚本执行之前不会对代码进行检查,所以哪怕在未执行期间并不存在的 <code>#find_by_name</code> 方法也不会导致解释器报错,在运行期间通过 <code>#define_singleton_method</code> 动态地 定义了新的 <code>#find_by_name</code> 方法修改了对象的行为,达到了为对象批量添加相似功能的目的。</p>\n<h2>总结</h2>\n<p>在文章中介绍的两种不同的元编程能力,宏系统和运行时,前者通过预先定义好的一些宏规则,在预编译和编译期间对代码进行展开和替换,而后者提供了在运行期间改变代码行为的能力,两种方式的本质都是通过少量的代码生成一些非常相似的代码和逻辑,能够增强编程语言的表达能力并减少开发者的工作量。</p>\n<p>无论是宏还是运行时其实都是简化程序中代码的一种手段,归根结底就是一种使用代码生成代码的思想,如果我们能够掌握这种元编程的思想并在编程中熟练的运用就能够很好地解决程序中一些诡异的问题,还能消灭重复的代码,提高我们运用以及掌控编程语言的能力,能够极大地增强编程语言的表达能力,所以元编程确实是一种非常重要并且需要学习的思想。</p>\n<h2>Reference</h2>\n<ul>\n<li><a href=\"https://en.m.wikipedia.org/wiki/Metaprogramming\">Metaprogramming</a></li>\n<li><a href=\"https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/differences-between-cpp-templates-and-csharp-generics\">C++ 模板和 C# 泛型之间的区别(C# 编程指南)</a></li>\n<li><a href=\"https://www.zhihu.com/question/33304378\">C++ 模板和 Java 泛型有什么异同?</a></li>\n<li><a href=\"https://en.wikipedia.org/wiki/Macro_(computer_science)\">Macro (computer science)</a></li>\n<li><a href=\"https://elixir-lang.org/getting-started/meta/macros.html\">Macros · Elixir Doc</a></li>\n<li><a href=\"https://gcc.gnu.org/onlinedocs/cpp/Macros.html\">Macros · GCC</a></li>\n<li><a href=\"http://hbprotoss.github.io/posts/cyu-yan-hong-de-te-shu-yong-fa-he-ji-ge-keng.html\">C 语言宏的特殊用法和几个坑</a></li>\n<li><a href=\"https://en.wikipedia.org/wiki/Hygienic_macro\">Hygienic macro</a></li>\n<li><a href=\"https://elixirschool.com/en/lessons/advanced/metaprogramming/\">Metaprogramming · ElixirSchool</a></li>\n<li><a href=\"https://doc.rust-lang.org/book/first-edition/macros.html\">Macros · Rust Doc</a></li>\n<li><a href=\"https://www.cs.indiana.edu/ftp/techreports/TR206.pdf\">Macro-by-Example</a></li>\n<li><a href=\"https://www.rust-lang.org/en-US/\">Rust</a></li>\n<li><a href=\"https://draveness.me/message\">从源代码看 ObjC 中消息的发送</a></li>\n<li><a href=\"https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html\">Dynamic Method Resolution</a></li>\n<li><a href=\"https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW1\">Message Forwarding</a></li>\n<li><a href=\"https://draveness.me/racdelegateproxy\">从代理到 RACSignal</a></li>\n<li><a href=\"https://developer.apple.com/documentation/objectivec/nsobject/1418500-resolveinstancemethod\">resolveInstanceMethod(_:)</a></li>\n<li><a href=\"http://rubylearning.com/satishtalim/ruby_method_missing.html\">Ruby Method Missing</a></li>\n<li><a href=\"https://www.leighhalliday.com/ruby-metaprogramming-method-missing\">Ruby Metaprogramming - Method Missing</a></li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20210801/",
"title": "转载:谈元编程与表达能力",
"summary": "很有趣的文章,之前那篇讨论泛型,这里讨论宏",
"date_modified": "2021-08-01T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>转载:泛型和元编程的模型:Java, Go, Rust, Swift, D等</h1>\n<p>这篇文章最开始是在公众号上读到的,译者在<a href=\"https://blog.csdn.net/weixin_45583158/article/details/109664612\"><em>某博客网站</em>上也发了</a>。英文原文是 <a href=\"https://thume.ca/2019/07/14/a-tour-of-metaprogramming-models-for-generics/\">Models of Generics and Metaprogramming: Go, Rust, Swift, D and More</a> 。废话不多说,下面开始粘贴正文过来存档(至于为什么粘贴英文原文,因为公众号和<em>某博客网站</em>复制都不能直接粘贴过来 markdown,格式都乱了 :angry:。既然是转载嘛,就懒一点了。</p>\n<p>In some domains of programming it’s common to want to write a data structure or algorithm that can work with elements of many different types, such as a generic list or a sorting algorithm that only needs a comparison function. Different programming languages have come up with all sorts of solutions to this problem: From just pointing people to existing general features that can be useful for the purpose (e.g C, Go) to generics systems so powerful they become Turing-complete (e.g. <a href=\"https://sdleffler.github.io/RustTypeSystemTuringComplete/\">Rust</a>, <a href=\"http://matt.might.net/articles/c++-template-meta-programming-with-lambda-calculus/\">C++</a>). In this post I’m going to take you on a tour of the generics systems in many different languages and how they are implemented. I’ll start from how languages without a special generics system like C solve the problem and then I’ll show how gradually adding extensions in different directions leads to the systems found in other languages.</p>\n<p>One reason I think generics are an interesting case is that they’re a simple case of the general problem of metaprogramming: writing programs that can generate classes of other programs. As evidence I’ll describe how three different fully general metaprogramming methods can be seen as extensions from different directions in the space of generics systems: dynamic languages like Python, procedural macro systems like <a href=\"https://wiki.haskell.org/A_practical_Template_Haskell_Tutorial\">Template Haskell</a>, and staged compilation like <a href=\"https://ziglang.org/#Generic-data-structures-and-functions\">Zig</a> and <a href=\"http://terralang.org/\">Terra</a>.</p>\n<h2>Overview</h2>\n<p>I made a flow chart of all the systems I discuss to give you an overview of what this post will contain and how everything fits together:</p>\n<p><img src=\"./flowchart-2x.png\" alt=\"Timing\"></p>\n<h2>The basic ideas</h2>\n<p>Let’s say we’re programming in a language without a generics system and we want to make a generic stack data structure which works for any data type. The problem is that each function and type definition we write only works for data that’s the same size, is copied the same way, and generally acts the same way.</p>\n<p>Two ideas for how to get around this are to find a way to make all data types act the same way in our data structure, or to make multiple copies of our data structure with slight tweaks to deal with each data type the correct way. These two ideas form the basis of the two major classes of solutions to generics: “boxing” and “monomorphization”.</p>\n<p>Boxing is where we put everything in uniform “boxes” so that they all act the same way. This is usually done by allocating things on the heap and just putting pointers in the data structure. We can make pointers to all different types act the same way so that the same code can deal with all data types! However this can come at the cost of extra memory allocation, dynamic lookups and cache misses. In C this corresponds to making your data structure store <code>void*</code> pointers and just casting your data to and from <code>void*</code> (allocating on the heap if the data isn’t already on the heap).</p>\n<p>Monomorphization is where we copy the code multiple times for the different types of data we want to store. This way each instance of the code can directly use the size and methods of the data it is working with, without any dynamic lookups. This produces the fastest possible code, but comes at the cost of bloat in code size and compile times as the same code with minor tweaks is compiled many times. In C this corresponds to <a href=\"https://www.cs.grinnell.edu/~rebelsky/musings/cnix-macros-generics\">defining your entire data structure in a macro</a> and calling it for each type you want to use it with.</p>\n<p>Overall the tradeoff is basically that boxing leads to better compile times but can hurt runtime performance, whereas monomorphization will generate the fastest code but takes extra time to compile and optimize all the different generated instances. They also differ in how they can be extended: Extensions to boxing allow more dynamic behavior at runtime, while monomorphization is more flexible with how different instances of generic code can differ. It’s also worth noting that in some larger programs the performance advantage of monomorphization might be canceled out by the additional instruction cache misses from all the extra generated code.</p>\n<p>Each of these schools of generics has many directions it can be extended in to add additional power or safety, and different languages have taken them in very interesting directions. Some languages like Rust and C# even provide both options!</p>\n<h2>Boxing</h2>\n<p>Let’s start with an example of the basic boxing approach in Go:</p>\n<pre><code class=\"language-go\">type Stack struct {\n values []interface{}\n}\n\nfunc (this *Stack) Push(value interface{}) {\n this.values = append(this.values, value)\n}\n\nfunc (this *Stack) Pop() interface{} {\n x := this.values[len(this.values)-1]\n this.values = this.values[:len(this.values)-1]\n return x\n}\n</code></pre>\n<p>Example languages that use basic boxing: C (<code>void*</code>), Go (<code>interface{}</code>), pre-generics Java (<code>Object</code>), pre-generics Objective-C (<code>id</code>)</p>\n<h2>Type-erased boxed generics</h2>\n<p>Here’s some problems with the basic boxing approach:</p>\n<ul>\n<li>Depending on the language we often need to cast values to and from the correct type every time we read or write to the data structure.</li>\n<li>Nothing stops us from putting elements of different types into the structure, which could allow bugs that manifest as runtime crashes.</li>\n</ul>\n<p>A solution to both of these problems is to add generics functionality to the type system, while still using the basic boxing method exactly as before at runtime. This approach is often called type erasure, because the types in the generics system are “erased” and all become the same type (like <code>Object</code>) under the hood.</p>\n<p>Java and Objective-C both started out with basic boxing, and later added language features for generics with type erasure, even using the exact same collection types as before for compatibility, but with optional generic type parameters. See the following example from the <a href=\"https://en.wikipedia.org/wiki/Generics_in_Java\">Wikipedia article on Java Generics</a>:</p>\n<pre><code class=\"language-java\">List v = new ArrayList();\nv.add("test"); // A String that cannot be cast to an Integer\nInteger i = (Integer)v.get(0); // Run time error\n\nList<String> v = new ArrayList<String>();\nv.add("test");\nInteger i = v.get(0); // (type error) compilation-time error\n</code></pre>\n<h3>Inferred boxed generics with a uniform representation</h3>\n<p>OCaml takes this idea even further with a uniform representation where there are no primitive types that require an additional boxing allocation (like <code>int</code> needing to be turned into an <code>Integer</code> to go in an <code>ArrayList</code> in Java), because everything is either already boxed or represented by a pointer-sized integer, so everything is one machine word. However when the garbage collector looks at data stored in generic structures it needs to tell pointers from integers, so integers are tagged using a 1 bit in a place where valid aligned pointers never have a 1 bit, leaving only 31 or 63 bits of range.</p>\n<p>OCaml also has a type inference system so you can write a function and the compiler will infer the most generic type possible if you don’t annotate it, which can lead to functions that look like a dynamically typed language:</p>\n<pre><code class=\"language-ocaml\">let first (head :: tail) = head\n(* inferred type: 'a list -> 'a *)\n</code></pre>\n<p>The inferred type is read as “a function from a list of elements of type <code>'a</code> to something of type <code>'a</code>”. Which encodes the relation that the return type is the same as the list type but can be any type.</p>\n<h2>Interfaces</h2>\n<p>A different limitation in the basic boxing approach is that the boxed types are <em>completely</em> opaque. This is fine for data structures like a stack, but things like a generic sorting function need some extra functionality, like a type-specific comparison function. There’s a number of different ways of both implementing this at runtime and exposing this in the language, which are technically different axes and you can <a href=\"http://okmij.org/ftp/Computation/typeclass.html\">implement the same language using multiple of these approaches</a>. However, it seems like different language features mostly lend themselves towards being implemented a certain way, and then language extensions take advantage of the strengths of the chosen implementation. This means there’s mostly two families of languages based around the different runtime approaches: vtables and dictionary passing.</p>\n<h3>Interface vtables</h3>\n<p>If we want to expose type-specific functions while sticking with the boxing strategy of a uniform way of working with everything, we can just make sure that there’s a uniform way to find the type-specific function we want from an object. This approach is called using “vtables” (shortened from “virtual method tables” but nobody uses the full name) and how it is implemented is that at offset zero in every object in the generic structure is a pointer to some tables of function pointers with a consistent layout. These tables allow the generic code to look up a pointer to the type-specific functions in the same way for every type by indexing certain pointers at fixed offsets.</p>\n<p>译者注,图示如下:</p>\n<p><img src=\"./11add66dba39980d3e8333f8a746caa2.png\" alt=\"img\"></p>\n<p>This is how <code>interface</code> types are implemented in Go and <code>dyn</code> <code>trait</code> objects are implemented in Rust. When you cast a type to an interface type for something it implements, it creates a wrapper that contains a pointer to the original object and a pointer to a vtable of the type-specific functions for that interface. However this requires an extra layer of pointer indirection and a different layout, which is why sorting in Go uses <a href=\"https://golang.org/pkg/sort/#Interface\">an interface for the container with a Swap method</a> instead of taking a slice of a <code>Comparable</code> interface, because it would require allocating an entire new slice of the interface types and then it would only sort that and not the original slice!</p>\n<p>译者注:<br>\nGo 语言对slice进行排序,需要在slice(切片)上实现Sort.Interface接口,如下所示:</p>\n<pre><code class=\"language-go\">type Interface interface {\n Len() int // Len 为集合内元素的总数\n Less(i, j int) bool //如果index为i的元素小于index为j的元素,则返回true,否则返回false\n Swap(i, j int) // Swap 交换索引为 i 和 j 的元素\n}\n</code></pre>\n<p>使用方式:</p>\n<pre><code class=\"language-go\">package main\n\nimport (\n "fmt"\n "sort"\n)\n\n//定义interface{},并实现sort.Interface接口的三个方法\ntype IntSlice []int\n\nfunc (c IntSlice) Len() int {\n return len(c)\n}\nfunc (c IntSlice) Swap(i, j int) {\n c[i], c[j] = c[j], c[i]\n}\nfunc (c IntSlice) Less(i, j int) bool {\n return c[i] < c[j]\n}\nfunc main() {\n a := IntSlice{1, 3, 5, 7, 2}\n b := []float64{1.1, 2.3, 5.3, 3.4}\n c := []int{1, 3, 5, 4, 2}\n fmt.Println(sort.IsSorted(a)) //false\n if !sort.IsSorted(a) {\n sort.Sort(a) \n }\n if !sort.Float64sAreSorted(b) {\n sort.Float64s(b)\n }\n if !sort.IntsAreSorted(c) {\n sort.Ints(c)\n }\n fmt.Println(a)//[1 2 3 5 7]\n fmt.Println(b)//[1.1 2.3 3.4 5.3]\n fmt.Println(c)// [1 2 3 4 5]\n}\n</code></pre>\n<p>对于Java来说,对数组排序需要在数组/集合元素上实现Comparable 接口,代码如下:</p>\n<pre><code class=\"language-java\">class Simpson implements Comparable<Simpson> {\n String name;\n Simpson(String name) {\n this.name = name;\n }\n @Override\n public int compareTo(Simpson simpson) {\n return this.name.compareTo(simpson.name);\n }\n}\n\npublic class SimpsonSorting {\n public static void main(String... sortingWithList) {\n List<SimpsonCharacter> simpsons = new ArrayList<>();\n simpsons.add(new SimpsonCharacter("Homer "));\n simpsons.add(new SimpsonCharacter("Marge "));\n simpsons.add(new SimpsonCharacter("Bart "));\n simpsons.add(new SimpsonCharacter("Lisa "));\n Collections.sort(simpsons);\n simpsons.stream().map(s -> s.name).forEach(System.out::print);\n Collections.reverse(simpsons);\n simpsons.stream().forEach(System.out::print);\n }\n}\n</code></pre>\n<h3>Object-oriented programming</h3>\n<p>Object oriented programming is a language feature that makes good use of the power of vtables. Instead of having separate interface objects that contain the vtables, object-oriented languages like Java just have a vtable pointer at the start of every object. Java-like languages have a system of inheritance and interfaces that can be implemented entirely with these object vtables.</p>\n<p>As well as providing additional features, embedding vtables in every object also solves the earlier problem of needing to construct new interface types with indirection. Unlike <code>Go</code>, in Java <a href=\"https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html#sort(java.lang.Object%5B%5D)\">the sorting function</a> can just use the <code>Comparable</code> interface on types that implement it.</p>\n<h3>Reflection</h3>\n<p>Once you have vtables, it’s not too difficult to have the compiler also generate tables of other type information like field names, types and locations. This allows accessing all the data in a type with the same code that can inspect all the data in any other type. This can be used to add a “reflection” feature to your language which can be used to implement things like serialization and pretty printing for arbitrary types. As an extension of the boxing paradigm it has the same tradeoff that it only requires one copy of the code but requires a lot of slow dynamic lookups, which can lead to slow serialization performance.</p>\n<p>Examples of languages with reflection features they use for serialization and other things include Java, C# and Go.</p>\n<h3>Dynamically typed languages</h3>\n<p>Reflection is very powerful and can do a lot of different metaprogramming tasks, but one thing it can’t do is create new types or edit the type information of existing values. If we add the ability to do this, as well as make the default access and modification syntaxes go through reflection, we end up with dynamically typed languages! The incredibly flexibility to do metaprogramming in languages like Python and Ruby comes from effectively super-powered reflection systems that are used for everything.</p>\n<p>“But Tristan, that’s not how dynamic languages work, they just implement everything with hash tables!” you may say. Well, hash tables are just a good data structure for implementing editable type information tables! Also, that’s just how some interpreters like CPython do things. If you look at how a high performance JIT like V8 implements things, <a href=\"https://v8.dev/blog/fast-properties\">it looks a lot like vtables and reflection info</a>! V8’s hidden classes (vtables and reflection info) and object layout are similar to what you might see in a Java VM, just with the capability for objects to change to a new vtable at runtime. This is not a coincidence because nothing is ever a coincidence: The person <a href=\"https://en.wikipedia.org/wiki/Chrome_V8\">listed on Wikipedia as the creator of V8</a> previously <a href=\"https://en.wikipedia.org/wiki/Lars_Bak_(computer_programmer)\">worked on a high-performance Java VM</a>.</p>\n<h3>Dictionary Passing</h3>\n<p>Another way of implementing dynamic interfaces than associating vtables with objects is to pass a table of the required function pointers along to generic functions that need them. This approach is in a way similar to constructing Go-style interface objects at the call site, just that the table is passed as a hidden argument instead of packaged into a bundle as one of the existing arguments.</p>\n<p>This approach is used by <a href=\"http://okmij.org/ftp/Computation/typeclass.html\">Haskell type classes</a> although GHC has the ability to do a kind of monomorphization as an optimization through inlining and specialization. Dictionary passing is also used by OCaml with an explicit argument in the form of <a href=\"https://v1.realworldocaml.org/v1/en/html/first-class-modules.html\">first class modules</a>, but there’s a proposal to <a href=\"https://tycon.github.io/modular-implicits.html\">add a mechanism to make the parameter implicit</a>.</p>\n<h3>Swift Witness Tables</h3>\n<p>Swift makes the interesting realization that by using dictionary passing and also putting the size of types and how to move, copy and free them into the tables, they can provide all the information required to work with any type in a uniform way without boxing them. This way Swift can implement generics <a href=\"https://www.reddit.com/r/rust/comments/7gkiie/implementing_swift_generics_video/\">without monomorphization and without allocating everything into a uniform representation</a>! They still pay the cost of all the dynamic lookups that all boxing-family implementations pay, but they save on the allocation, memory and cache-incoherency costs. The Swift compiler also has the ability to specialize (monomorphize) and inline generics within a module and across modules with functions <a href=\"https://github.com/apple/swift-evolution/blob/master/proposals/0193-cross-module-inlining-and-specialization.md\">annotated <code>@inlinable</code></a> to avoid these costs if it wants to, presumably using heuristics about how much it would bloat the code.</p>\n<p>This functionality also explains how Swift can <a href=\"https://github.com/apple/swift-evolution/blob/master/proposals/0260-library-evolution.md\">implement ABI stability</a> in a way that allows adding and rearranging fields in <code>struct</code>s, although they provide a <code>@frozen</code> attribute to opt out of dynamic lookups for performance reasons.</p>\n<h3>Intensional Type Analysis</h3>\n<p>One more way to implement interfaces for your boxed types is to add a type ID in a fixed part of the object like where a vtable pointer would go, then generate functions for each interface method that effectively have a big <code>switch</code> statement over all the types that implement that interface method and dispatch to the correct type-specific method.</p>\n<p>I’m not aware of any languages that use this technique, but C++ compilers and Java VMs do something similar to this when they use profile-guided optimization to learn that a certain generic call site mostly acts on objects of certain types. They’ll replace the call site with a check for each common type and then a static dispatch for that common type, with the usual dynamic dispatch as a fallback case. This way the branch predictor can predict the common case branch will be taken and continue dispatching instructions through the static call.</p>\n<h2>Monomorphization</h2>\n<p>Now, the alternative approach to boxing is monomorphization. In the monomorphization approach we need to find some way to output multiple versions of our code for each type we want to use it with. Compilers have multiple phases of representations that the code passes through as it is compiled, and we theoretically can do the copying at any of these stages.</p>\n<h3>Generating source code</h3>\n<p>The simplest approach to monomorphization is to do the copying at the stage of the first representation: source code! This way the compiler doesn’t even have to have generics support in it, and this is what users of languages like C and Go, where the compiler doesn’t support generics, sometimes do.</p>\n<p>In C you can use the preprocessor and define your data structure in a macro or a header that you include multiple times with different <code>#define</code>s. In Go there are scripts like <a href=\"https://github.com/cheekybits/genny\">genny</a> that make this code generation process easy.</p>\n<p>The downside of this is that duplicating source code can have a lot of warts and edge cases to look out for depending on the language, and also gives the compiler lots of extra work to do parsing and type checking basically the same code many times. Again depending on language and tools this method’s generics can be ugly to write and use, like how if you write one inside a C macro every line has to end with a backslash and all type and function names need to have the type name manually concatenated onto their identifiers to avoid collisions.</p>\n<h3>D string mixins</h3>\n<p>Code generation does have something going for it though, which is that you can generate the code using a fully powered programming language, and also it uses a representation that the user already knows.</p>\n<p>Some languages that implement generics in some other way also include a clean way of doing code generation to address more general metaprogramming use cases not covered by their generics system, like serialization. The clearest example of this is D’s <a href=\"https://dlang.org/articles/mixin.html\">string mixins</a> which enable generating D code as strings using the full power of D during the middle of a compile.</p>\n<h3>Rust procedural macros</h3>\n<p>A similar example but with a representation only one step into the compiler is <a href=\"https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-Rust-2018.html\">Rust’s procedural macros</a>, which take token streams as input and output token streams, while providing utilities to convert token streams to and from strings. The advantage of this approach is that token streams can preserve source code location information. A macro can directly paste code the user wrote from input to output as tokens, then if the user’s code causes a compiler error in the macro output the error message the compiler prints will correctly point to the file, line and columns of the broken part of the user’s code, but if the macro generates broken code the error message will point to the macro invocation. For example if you use <a href=\"https://docs.rs/log-derive/\">a macro that wraps a function in logging calls</a> and make a mistake in the implementation of the wrapped function, the compiler error will point directly to the mistake in your file, rather than saying the error occurred in code generated by the macro.</p>\n<h3>Syntax tree macros</h3>\n<p>Some languages do take the step further and offer facilities for consuming and producing Abstract Syntax Tree (AST) types in macros written in the language. Examples of this include <a href=\"https://wiki.haskell.org/A_practical_Template_Haskell_Tutorial\">Template Haskell</a>, <a href=\"https://nim-lang.org/docs/tut3.html\">Nim macros</a>, <a href=\"http://ocamllabs.io/doc/ppx.html\">OCaml PPX</a> and nearly all <a href=\"https://en.wikipedia.org/wiki/Lisp_(programming_language)\">Lisp</a>s.</p>\n<p>One problem with AST macros is that you don’t want to require users to learn a bunch of functions for constructing AST types as well as the base languages. The Lisp family of languages address this by making the syntax and the AST structure very simple with a very direct correspondence, but constructing the structures can still be tedious. Thus, all the languages I mention have some form of “quote” primitive where you provide a fragment of code in the language and it returns the syntax tree. These quote primitives also have a way to splice syntax tree values in like string interpolation. Here’s an example in Template Haskell:</p>\n<pre><code class=\"language-haskell\">-- using AST construction functions\ngenFn :: Name -> Q Exp\ngenFn f = do\n x <- newName "x"\n lamE [varP x] (appE (varE f) (varE x))\n\n-- using quotation with $() for splicing\ngenFn' :: Name -> Q Exp\ngenFn' f = [| \\x -> $(varE f) x |]\n</code></pre>\n<p>One disadvantage of doing procedural macros at the syntax tree level instead of token level is that syntax tree types often change with the addition of new language features, while token types can remain compatible. For example OCaml’s PPX system needs <a href=\"https://github.com/ocaml-ppx/ocaml-migrate-parsetree\">special infrastructure to migrate parse trees</a> to and from the language version used by a macro. Whereas Rust has libraries that add <a href=\"https://github.com/dtolnay/syn\">parsing</a> and <a href=\"https://github.com/dtolnay/quote\">quotation</a> utilities so you can write procedural macros in a style similar to syntax tree macros. Rust even has <a href=\"https://github.com/dtolnay/reflect\">an experimental library that tries to replicate the interface provided by reflection</a>!</p>\n<h3>Templates</h3>\n<p>The next type of generics is just pushing the code generation a little further in the compiler. Templates as found in C++ and D are a way of implementing generics where you can specify “template parameters” on types and functions and when you instantiate a template with a specific type, that type is substituted into the function, and then the function is type checked to make sure that the combination is valid.</p>\n<pre><code class=\"language-cpp\">template <class T> T myMax(T a, T b) {\n return (a>b?a:b);\n}\n\ntemplate <class T> struct Pair {\n T values[2];\n};\n\nint main() {\n myMax(5, 6);\n Pair<int> p { {5,6} };\n // This would give us a compile error inside myMax\n // about Pair being an invalid operand to `>`:\n // myMax(p, p);\n}\n</code></pre>\n<p>One problem with the template system is that if you include a template function in your library and a user instantiates it with the wrong type they may get an inscrutable compile error inside your library. This is very similar to what can happen with libraries in dynamically typed languages when a user passes in the wrong type. <a href=\"https://dlang.org/\">D</a> has an interesting solution to this which is similar to what popular libraries in dynamic languages do: just use helper functions to check the types are valid, the error messages will clearly point to the helpers if they fail! Here’s the same example in D, note the <code>if</code> in the signature and the generally better syntax (<code>!</code> is how you provide template parameters):</p>\n<pre><code class=\"language-D\">// We're going to use the isNumeric function in std.traits\nimport std.traits;\n\n// The `if` is optional (without it you'll get an error inside like C++)\n// The `if` is also included in docs and participates in overloading!\nT myMax(T)(T a, T b) if(isNumeric!T) {\n return (a>b?a:b);\n}\n\nstruct Pair(T) {\n T[2] values;\n}\n\nvoid main() {\n myMax(5, 6);\n Pair!int p = {[5,6]};\n // This would give a compile error saying that `(Pair!int, Pair!int)`\n // doesn't match the available instance `myMax(T a, T b) if(isNumeric!T)`:\n // myMax(p, p);\n}\n</code></pre>\n<p><a href=\"https://en.cppreference.com/w/cpp/language/constraints\">C++20 has a feature called “concepts”</a> that serves the same purpose except with a design more like defining interfaces and type constraints.</p>\n<h3>Compile time functions</h3>\n<p>D’s templates have a number of extensions that allow you to use features like compile time function evaluation and <code>static if</code> to basically make templates act like functions that take a compile time set of parameters and return a non-generic runtime function. This makes D templates into a fully featured metaprogramming system, and as far as I understand modern C++ templates have similar power but with less clean mechanisms.</p>\n<p>There’s some languages that take the “generics are just compile time functions” concept and run with it even further, like Zig:</p>\n<pre><code>fn Stack(comptime T: type) type {\n return struct {\n items: []T,\n len: usize,\n\n const Self = @This();\n pub fn push(self: Self, item: T) {\n // ...\n }\n };\n}\n</code></pre>\n<p>Zig does this using the same language at both compile time and runtime, with functions split up based on parameters marked <code>comptime</code>. There’s another language that uses a separate but similar language at the meta level called <a href=\"http://terralang.org/\">Terra</a>. Terra is a dialect of Lua that allows you to construct lower level C-like functions inline and then manipulate them at the meta level using Lua APIs as well as quoting and splicing primitives:</p>\n<pre><code class=\"language-lua\">function MakeStack(T)\n local struct Stack {\n items : &T; -- &T is a pointer to T\n len : int;\n }\n terra Stack:push(item : T)\n -- ...\n end\n return Stack\nend\n</code></pre>\n<p>Terra’s crazy level of metaprogramming power allows it to do things <a href=\"http://terralang.org/#compiling-a-language\">like implement optimizing compilers for domain specific languages as simple functions</a>, or implement the interface and object systems of <a href=\"https://github.com/zdevito/terra/blob/master/tests/lib/javalike.t\">Java</a> and <a href=\"https://github.com/zdevito/terra/blob/master/tests/lib/golike.t\">Go</a> in a library with a small amount of code. Then it can save out generated runtime-level code as dependency-free object files.</p>\n<h3>Rust generics</h3>\n<p>The next type of monomorphized generics of course moves the code generation one step further into the compiler, after type checking. I mentioned that the type of inside-the-library errors you can get with C++ are like the errors you can get in a dynamically typed language, this is of course because there’s basically only one type of type in template parameters, like a dynamic language. So that means we can fix the problem by adding a type system to our meta level and having multiple types of types with static checking that they support the operations you use. This is how generics work in Rust, and at the language level also how they work in Swift and Haskell.</p>\n<p>In Rust you need to declare “trait bounds” on your type parameters, where <code>trait</code>s are like interfaces in other languages and declare a set of functionality provided by the type. The Rust compiler will check that the body of your generic functions will work with any type conforming to your trait bounds, and also not allow you to use functionality of the type not declared by the trait bounds. This way users of generic functions in Rust can <em>never</em> get compile errors inside a library function when they instantiate it. The compiler also only has to type check each generic function once.</p>\n<pre><code class=\"language-rust\">fn my_max<T: PartialOrd>(a: T, b: T) -> T {\n if a > b { a } else { b }\n}\n\nstruct Pair<T> {\n values: [T; 2],\n}\n\nfn main() {\n my_max(5,6);\n let p: Pair<i32> = Pair { values: [5,6] };\n // Would give a compile error saying that\n // PartialOrd is not implemented for Pair<i32>:\n // my_max(p,p);\n}\n</code></pre>\n<p>At the language level this is very similar to the kind of type system you need to implement generics with interface support using the boxing approach to generics, which is why Rust can support both using the same system! Rust 2018 even added a uniform syntax where a <code>v: &impl SomeTrait</code> parameter gets monomorphized but a <code>v: &dyn SomeTrait</code> parameter uses boxing. This property also allows compilers like Swift’s and Haskell’s GHC to monomorphize as an optimization even though they default to boxing.</p>\n<h3>Machine code monomorphization</h3>\n<p>The logical next step in monomorphized generics models is pushing it further in the compiler, after the backend. Just like we can copy source code templates that are annotated with placeholders for the generic type, we can generate machine code with placeholders for the type-specific parts. Then we can stamp these templates out very quickly with a <code>memcpy</code> and a few patches like how a linker works! The downside is that each monomorphized copy couldn’t be specially optimized by the optimizer, but because of the lack of duplicate optimization, compilation can be way faster. We could even make the code stamper a tiny JIT that gets included in binaries and stamps out the monomorphized copies at runtime to avoid bloating the binaries.</p>\n<p>Actually I’m not aware of any language that works this way, it’s just an idea that came to me while writing as a natural extension of this taxonomy, which is exactly the kind of thing I hoped for from this exercise! I hope this post gives you a clearer picture of the generics systems in different languages and how they can be fit together into a coherent taxonomy, and prompts you to think about the directions in concept-space where we might find new cool programming languages.</p>\n<p>参考资料</p>\n<p>原文:<a href=\"https://thume.ca/2019/07/14/a-tour-of-metaprogramming-models-for-generics/\">Models of Generics and Metaprogramming: Go, Rust, Swift, D and More - Tristan Hume (thume.ca)</a></p>\n<p>译文:<a href=\"https://blog.csdn.net/weixin_45583158/article/details/109664612\">泛型和元编程的模型:Java, Go, Rust, Swift, D等_高可用架构的博客-CSDN博客</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210728/",
"title": "转载:泛型和元编程的模型:Java, Go, Rust, Swift, D等",
"summary": "很有趣的文章,类似的推荐阅读《七周七语言:理解多种编程范型》",
"date_modified": "2021-07-28T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>XDP Tutorial 学习笔记</h1>\n<p>xdp 的相关论文发在 2018 年 CONEXT 上,文章名称是 "The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel",是 OA 的,可以直接<a href=\"https://dl.acm.org/doi/10.1145/3281411.3281443\">下载来看</a>。</p>\n<p>学习一下 xdp 官方提供的教程,项目地址额为 <a href=\"https://github.com/xdp-project/xdp-tutorial\">xdp-project/xdp-tutorial: XDP tutorial</a>。该教程依赖的 libbpf 是19年的一版,直接用新版会有问题,需要下项目里的子模块指定的 <a href=\"https://github.com/libbpf/libbpf/tree/b91f53ec5f1aba2a9d01dc00c4434063abd921e8\">libbpf/libbpf at b91f53ec5f1aba2a9d01dc00c4434063abd921e8</a>。</p>\n<h2>教程的简介</h2>\n<p>比较基础的章节是 <code>basic01</code> 到 <code>basic04</code> 目录下的内容。</p>\n<ul>\n<li><code>basic02</code>:讲解了 libbpf 怎么加载 bpf 代码的。让读者自己实现一个简化的加载过程。用户实现的函数,使用 <code>_</code> 前缀与教程中 xdp 团队提供的 api 相区分。相应的 api 是没有 <code>_</code> 前缀的,位于 <code>common</code> 目录下。例如,<code>common/common_user_bpf_xdp.c</code> 下的<code>load_bpf_and_xdp_attach()</code> 函数。</li>\n<li><code>basic03</code>:讲解了 bpf map 的使用。</li>\n<li><code>basic04</code>:讲解了跨应用共享 bpf map,使用的是 pinning maps 技术。</li>\n</ul>\n<p><code>tracing01</code> 到 <code>tracing04</code> 是做 tracing 方面的应用。</p>\n<p><code>packet01</code> 到 <code>packet03</code> 是从包的层面上做了 parsing、rewriting、redirecting。</p>\n<p><code>advance01</code> 是 xdp 和 tc 交互的例子。</p>\n<p><code>advance03</code> 很有趣,是一个比较完整的例子,展示了如何通过 xdp 在用户空间解析 IPV6 ICMP 报文,并发送回复。是用了一种新型的 socket 地址类型,<code>AF_XDP</code>,可以在 kernel 的文档中找到<a href=\"https://www.kernel.org/doc/html/latest/networking/af_xdp.html\"> AF_XDP 的介绍</a>。</p>\n<p>这些教程中的 Assignment 的答案分布:<code>advance</code> 和 <code>tracing</code> 部分的答案就是在代码里的。<code>basic</code> 和 <code>packet</code> 部分的是在 <code>basic-solutions</code> 和<code>packet-solutions</code> 目录下。</p>\n<h2>Advance03 示例的笔记</h2>\n<p>xdp 没有完全绕过内核,但是可以让包跳过内核的网络栈,直接从用户空间读取,可以通过 <code>AF_XDP</code> 的 <code>XDP_REDIRECT</code> 语义实现。</p>\n<p>首先简要记录一下 <code>AF_XDP</code> 套接字。<code>AF_XDP</code> socket, 缩写为 XSK,可以通过系统调用 <code>socket()</code> 创建。每个 XSK 都有两个环来存储数据,一个 RX ring 和一个 TX ring。套接字能够用 RX ring 接收包,通过 TX ring 发送包。这些环是通过 <code>setsockopts</code> 中的选项 <code>XDP_RX_RING</code> 和 <code>XDP_TX_RING</code> 设置的。每个套接字至少需要其中的一个(以作为单侧的 source/sink node)。RX/TX ring 指向内存中一块叫做 UMEM 的数据。RX 和 TX 能够共享同一块 UMEM 区域,以防在 RX 和 TX 之间频繁地进行数据拷贝。另外,如果由于潜在的重传,一个包需要被保存一段时间,这些指针也能暂时指向别的包,避免拷贝数据。</p>\n<p>在 BPF 侧的 AF_XDP 程序,参数是 <code>struct xdp_md</code>,包含原始 frame 的数据。可以返回一些状态来表示对该 frame 的处理意见,比如:</p>\n<ul>\n<li><code>XDP_PASS</code>:继续传递到 Linux 后续的协议栈中处理。</li>\n<li><code>XDP_REDIRECT</code>:将包通过 UMEM 传递到用户空间处理。</li>\n<li><code>XDP_DROP</code>:直接丢弃这个包。</li>\n<li><code>XDP_TX</code> 可以直接发回给网卡,可以用来在内核中做快速的回复。比如下面 Advance03 中做的事情,去交换 ICMP 报文的发送方和接收方。该例子其实可以在内核中完成,然后用 <code>XDP_TX</code> 发回去,不是必须 redirect 到用户空间再做。</li>\n</ul>\n<h3>AF_XDP 的性能提升从何而来?</h3>\n<p>AF_XDP socket 非常快,在这个性能提升的背后隐藏了多少秘密呢? AF_XDP 的 idea 背后的基础可以追溯到 <a href=\"https://en.wikipedia.org/wiki/Van_Jacobson\">Van Jacobson</a> 的关于 <a href=\"https://lwn.net/Articles/169961/\">network channels</a> 的报告中。在该报告中,描述了如何直接从驱动的 RX-queue (接收队列)去创建一个无锁的 <a href=\"https://lwn.net/Articles/169961/\">channel</a> 构建 AF_XDP socket。</p>\n<p>(前面介绍 <code>AF_XDP</code> 的内容也提到了),AF_XDP 使用的队列是 Single-Producer/Single-Consumer (SPSC) 的描述符(descriptor)环形队列:</p>\n<ul>\n<li>\n<p><strong>Single-Producer</strong> (SP) 绑定到了某个特定的 RX <strong>queue id</strong> 上,通过 NAPI-softirq 确保在软中断(softirq)触发期间,只有一个 CPU 来处理一个 RX-queue id。</p>\n<p>:::tip</p>\n<p>NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,否则包太多了,不停触发中断。而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据。</p>\n<p>:::</p>\n</li>\n<li>\n<p><strong>Single-Consumer</strong> (SC) 则是一个应用,从环中读取指向 UMEM 区域的描述符(descriptor)。</p>\n</li>\n</ul>\n<p>因此不需要对每个包都分配一次内存。可以在事先为 UMEM 内存区域进行分配(因此 UMEM 是有界的)。UMEM 包含了一些大小相同的块,环中的指针会引用它们的地址来引用这些块。这个地址就是在整个 UMEM 区域中的偏移量。用户空间负责为 UMEM 分配内存,分配的方法很灵活,可以用 malloc、mmap、huge pages 等形式。这个内存空间通过在 <code>setsockopt()</code> 方法中设置 <code>XDP_UMEM_REG</code> 触发相应的系统调用,注册到内核中。<strong>需要注意的是</strong>:这样就意味着你需要负责及时地将 frame 返回给 UMEM,并且需要为你的应用提前分配足够的内存。</p>\n<p>Van Jacobson 在报告中谈到的 <a href=\"http://www.lemis.com/grog/Documentation/vj/lca06vj.pdf\">transport signature</a>,在 XDP/eBPF 程序中体现为选择将 frame <code>XDP_REDIRECT</code> 到哪个 AF_XDP socket。</p>\n<h3>示例代码阅读</h3>\n<p>打开 <code>advanced03-AF_XDP/af_xdp_kern.c</code>,它很精简,只有四十行代码。首先定义了两个 bpf map,一个存储 XSK,一个存储包的数量数据。然后定义了一个 bpf 程序,它的参数是 <code>struct xdp_md</code>,所以它是一个 <strong>BPF_PROG_TYPE_XDP</strong> 类型的 BPF 程序。这段程序通过 <code>SEC()</code> 宏放在了<code>xdp_sock</code> 段中,用 bpf helper 函数来和定义好的 bpf map 交互。注意其中的代码</p>\n<pre><code class=\"language-c\">/* We pass every other packet */\nif ((*pkt_count)++ & 1) \n\treturn XDP_PASS;\n</code></pre>\n<p>是间隔一个地直接返回 <code>XDP_PASS</code>,下一个包才会用 <code>bpf_redirect_map</code> 去转发。也就是说,过滤掉了一半的包。</p>\n<p>在用户空间代码 <code>advanced03-AF_XDP/af_xdp_user.c</code> 中。首先是做了 bpf 用户空间程序必要的一些工作,比如 <code>setrlimit(RLIMIT_MEMLOCK, &rlim)</code> 去释放内存限制。这也是为什么必须用 sudo 权限运行 bpf 程序。</p>\n<p>用 <code>stats_record</code> 结构体记录收发数量,在代码最后会单独开一个线程去调用 <code>stats_poll()</code> 函数打印实时的收发数据,用信号 <code>signal(SIGINT, exit_application)</code> 注册 <code>exit_application()</code> 函数,在结束时设置变量,帮助 <code>stats_poll()</code> 停止监测。</p>\n<p><code>xsk_socket_info</code> 结构体包装 <code>xsk_socket</code>,<code>xsk_umem_info</code> 结构体包装 <code>xsk_umem</code>。这部分代码会反复用到缩写 PROD 代表 producer,也就是发送端 tx;缩写 CONS 代表 consumer,也就是接收端 rx。因为 XSK 默认是 Single-Producer-Single-Consumer 的。</p>\n<p><code>xsk_configure_socket()</code> 初始化了 XSK,注意这里初始化发送端和接收端时,是传设置项 <code>xsk_cfg</code> 给库函数 <code>xsk_socket__create()</code>。<code>xsk_cfg.rx_size</code> 和 <code>xsk_cfg.tx_size</code> 分别初始化成了 <code>XSK_RING_CONS__DEFAULT_NUM_DESCS</code> 和 <code>XSK_RING_PROD__DEFAULT_NUM_DESCS</code>,他们会在库函数 <code>xsk_socket__create()</code> 中传递给系统调用 <code>setsockopt()</code> 去完成 XSK 中的 tx 和 rx 的创建。他们是定义在 <code>xsk.h</code> 中的宏,值都是 2048。事实上,只能被初始化成2的幂次。因为在库里的 <code>xsk.h</code> 中,获取 <code>xdp_desc</code> 的函数是这么定义的</p>\n<pre><code class=\"language-C\">static inline struct xdp_desc *xsk_ring_prod__tx_desc(struct xsk_ring_prod *tx,\n\t\t\t\t\t\t __u32 idx)\n{\n\tstruct xdp_desc *descs = (struct xdp_desc *)tx->ring;\n\n\treturn &descs[idx & tx->mask];\n}\n\nstatic inline const struct xdp_desc *\nxsk_ring_cons__rx_desc(const struct xsk_ring_cons *rx, __u32 idx)\n{\n\tconst struct xdp_desc *descs = (const struct xdp_desc *)rx->ring;\n\n\treturn &descs[idx & rx->mask];\n}\n\n\n/* Rx/Tx descriptor */\nstruct xdp_desc {\n\t__u64 addr;\n\t__u32 len;\n\t__u32 options;\n};\n</code></pre>\n<p>注意 <code>idx & tx->mask</code> 和 <code>idx & rx->mask</code> 是在用按位与运算去防止下标溢出,相当于在取模。这里的 <code>mask</code> 是在库里的 <code>xsk.c</code> 中的<code>xsk_socket__create()</code> 函数中初始化的,都是初始化成 <code>size-1</code> 的,也就是 2047,各位都是 1,如果 <code>size</code> 不是 2 的幂次,显然就不能这么干了。</p>\n<p>创建好 XSK,就可以监听了,这部分逻辑写在 <code>rx_and_process()</code> 中,用 <code>poll(struct pollfd *__fds, nfds_t __nfds, -1)</code> 系统调用去监听之前创建好的 XSK,在没有触发事件时阻塞。收到包后,调用 <code>handle_receive_packets()</code> 在 XSK 对应的 umem 中读取 rx 端,也就是 consumer 接收到的包。经过最深层的 <code>process_packet()</code> 处理,做的就是把包的指针转换成各层的首部,然后读取他们。因为实验中只有 IPV6 ICMP 报文,所以就直接像下面这样写了。处理完后,写入到 umem 中 tx 也就是 producer 管理的内存中。</p>\n<pre><code class=\"language-c\">static bool process_packet(struct xsk_socket_info *xsk, uint64_t addr, uint32_t len)\n{\n\tuint8_t *pkt = xsk_umem__get_data(xsk->umem->buffer, addr);\n\t// get header one by one\n\tstruct ethhdr *eth = (struct ethhdr *) pkt;\n // pointer adds 1*sizeof(ethhdr) in fact\n\tstruct ipv6hdr *ipv6 = (struct ipv6hdr *) (eth + 1); \n // pointer adds 1*sizeof(ipv6hdr) in fact\n\tstruct icmp6hdr *icmp = (struct icmp6hdr *) (ipv6 + 1);\n\t\t\n // ...\n \n\t// exchange source and destination\n\tmemcpy(tmp_mac, eth->h_dest, ETH_ALEN);\n\tmemcpy(eth->h_dest, eth->h_source, ETH_ALEN);\n\tmemcpy(eth->h_source, tmp_mac, ETH_ALEN);\n\n // ...\n\n\ticmp->icmp6_type = ICMPV6_ECHO_REPLY;\n\t// replace icmp checksum in the packet\n\tcsum_replace2(&icmp->icmp6_cksum,\n\t\t\t\thtons(ICMPV6_ECHO_REQUEST << 8),\n\t\t\t\thtons(ICMPV6_ECHO_REPLY << 8));\n\t// check remaining space in the ring \n\tret = xsk_ring_prod__reserve(&xsk->tx, 1, &tx_idx);\n\tif (ret != 1) {\n\t\t/* No more transmit slots, drop the packet */\n\t\treturn false;\n\t}\n\t// write to tx\n\txsk_ring_prod__tx_desc(&xsk->tx, tx_idx)->addr = addr;\n\txsk_ring_prod__tx_desc(&xsk->tx, tx_idx)->len = len;\n\txsk_ring_prod__submit(&xsk->tx, 1);\n \n\treturn true;\n}\n</code></pre>\n<p>至此该节内容结束。</p>\n<h3>可能碰到的问题</h3>\n<ul>\n<li>\n<p>首先这些 BPF 相关的 demo 都是需要 <code>sudo</code> 去跑的,需要管理员权限。</p>\n</li>\n<li>\n<p>系统内核太旧了,本身不支持 <code>AF_XDP</code> socket。</p>\n</li>\n<li>\n<p>最常见的错误:为什么我在 AF_XDP socket 上看不到任何流量?</p>\n<p>正如你在上面了解到的,AF_XDP socket 绑定到了一个 <strong>single RX-queue id</strong> (出于性能考量)。因此,用户空间的程序只会收到某个特定的 RX-queue id 下的 frames。然而事实上网卡会通过 RSS-Hashing,把流量散列到不同的 RX-queues 之间。因此,流量可能没有到达你所期望的那个队列。</p>\n<p>:::tip</p>\n<p>RSS (Receive Side Scaling) Hashing 是一种能够在多处理器系统下使接收报文在多个CPU之间高效分发的网卡驱动技术。网卡对接收到的报文进行解析,获取IP地址、协议和端口五元组信息。网卡通过配置的 HASH 函数根据五元组信息计算出 HASH 值,也可以根据二、三或四元组进行计算。取HASH值的低几位(这个具体网卡可能不同)作为 RETA (redirection table) 的索引,根据 RETA 中存储的值分发到对应的 CPU。</p>\n<p>:::</p>\n<p>为了解决这个问题,你必须配置网卡,让流进入一个特定的 RX-queue,可以通过 ethtool 或 TC HW offloading filter 设置。下面的例子展示了如何配置网卡,将所有的 UDP ipv4 流量都导入 <em>RX-queue id</em> 42:</p>\n<pre><code>ethtool -N <interface> flow-type udp4 action 42\n</code></pre>\n<p>参数 <em>action</em> 指定了目标 <em>RX-queue</em>。一般来说,上面的这个流量转发的规则包含了匹配准则和 action。L2、L3 和 L4 header 值能被用来指定匹配准则。如果想要阅读更详细的文档,请查看 ethtool 的 man page (<code>man ethtool</code>)。它记载了 header 中所有能够用来作为匹配准则的值。</p>\n<p>其他替代的方案:</p>\n<ol>\n<li>创建和 RX-queue 数量相同的 AF_XDP sockets,然后由用户空间使用 <code>poll/select</code> 等方法轮询这些 sockets。</li>\n<li>出于测试目的,也可以把 RX-queue 的数量削减到 1,例如:使用命令 <code>ethtool -L <interface> combined 1</code>。</li>\n</ol>\n<p>但是在用 <code>testenv/testenv.sh</code> 脚本虚拟出来的网卡用不了 <code>ethtool</code> 的上面这些和 RX-queue 相关的命令。</p>\n</li>\n</ul>\n<h3>zero-copy 模式</h3>\n<p>正如前面提过的 AF_XDP 依赖于驱动的 <code>XDP_REDIRECT</code> action 实现。对于所有实现了 <code>XDP_REDIRECT</code> action 的驱动,就都支持 “copy-mode” 下的 AF_XDP。“copy-mode” 非常快,只拷贝一次 frame(包括所有 XDP 相关的 meta-data)到 UMEM 区域。用户空间的 API 也是如此。</p>\n<p>为了支持 AF_XDP 的 “zero-copy” 模式,驱动需要在 NIC RX-ring 结构中直接实现并暴露出注册和使用 UMEM 区域的 API,以便使用 DMA。针对你的应用场景,在支持 “zero-copy” 的驱动上使用 “copy-mode” 仍然可能是有意义的。如果出于某些原因,并不是 RX-queue 中所有的流量都是要发给 AF_XDP socket 的,XDP 程序在 <code>XDP_REDIRECT</code> 和 <code>XDP_PASS</code> 间交替,如上面的 Advance03 示例中的那样,那么 “copy-mode” 可能是更好的选择。因为在 “zero-copy” 模式下使用 XDP_PASS 的代价很高,涉及到了为 frame 分配内存和执行内存拷贝。</p>\n<h3>在 STM32MP157A 开发板上跑这个 demo 碰到的问题</h3>\n<ol>\n<li>\n<p>板载系统没有开启 AF_XDP_SOCKET 支持(幸亏厂商提供了基于 5.4.31 内核的 Ubuntu 18.04,而且提供了他们构建开发板时的项目源码,只需要改下配置项,重新编译下内核,但凡他们搞个低版本的,闹不好我就寄了)。那么需要在内核源码目录下的<code>.config</code> 中重新编译一份 arm 架构的内核,将生成的 uImage 镜像和设备树文件拷贝到板子的 <code>/boot</code> 目录下。板子我是用的 sd 卡安装的 ubuntu,boot 目录没有自动挂载到,还要到 <code>/dev</code> 下找到它所在的分区(记录一下,我自己的板子是 block1p4),对应的 u-boot 的配置文件中如果启动的路径不对,可能也要修改。这里就庆幸自己是拿的 sd 卡装的,不然在只能进入到 u-boot 终端的情况下,只用 tftp 还处理不了 <code>boot</code> 目录下错误的路径配置。</p>\n</li>\n<li>\n<p>编译上面的例子时候,板子缺少 <code>libelf-dev</code> 包,会报错丢失 <code><gelf.h></code> 头文件。</p>\n</li>\n<li>\n<p>编译上面的例子时候,板子的<code>/usr/include/</code> 下没有 <code>asm</code> 文件夹,只有 <code>asm_generic</code>。有人博客里写,给 <code>asm_generic</code> 链接到 <code>asm</code> 就行了。亲测不是如此,二者包含的头文件并不相同。</p>\n<p>后来发现该目录下,还有一个 <code>arm </code>开头的文件夹,推测里面应该包含了板子 <code>arm</code> 架构下的相关头文件。打开后果然如此,有一个<code>asm</code>,那么只需要在<code>/usr/include</code> 下做一个软连接 <code>ln -s</code> 到它,命名成 <code>asm</code> 就行了。</p>\n</li>\n</ol>\n<h2>其他可以参考的资料</h2>\n<p>Linux manual page 上的 <a href=\"https://man7.org/linux/man-pages/man7/bpf-helpers.7.html\">bpf-helpers</a> 页面。</p>\n<p>前几篇 bpf 的相关笔记。</p>\n<p>组会和同学分享了近期的学习积累,做了一个 ppt,包含一些相关论文的概述,<a href=\"./eBPF.pptx\">slides 链接</a>。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210715/",
"title": "XDP Tutorial 学习笔记(附 tutorial slides)",
"summary": "学习一下 libbpf 下 XDP 的使用",
"date_modified": "2021-07-15T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>BPF Type Format (BTF)</h1>\n<p>翻译自:<a href=\"https://www.kernel.org/doc/html/latest/bpf/btf.html\">Linux Kernel Doc</a></p>\n<h2>1. Introduction</h2>\n<p>BTF (BPF Type Format) 是编码 BPF 程序、映射相关的 debug 信息的元数据格式。名称 BTF 最开始是用来描述数据类型的,之后被扩展,包含了预定义的子例程的函数信息以及源码中的 line info。</p>\n<p>debug 信息被用来格式化打印和提供函数签名等信息。函数签名优化了 bpf 程序/函数的内核符号。line info 则可以生成经过标注的字节码,jited 代码和验证器记录。</p>\n<p>BTF 包含两部分:</p>\n<ul>\n<li>BTF kernel API</li>\n<li>BTF ELF file format</li>\n</ul>\n<p>内核 API 是用户空间和内核之间沟通的桥梁。内核在使用 BTF 信息之前验证了它。ELF 文件格式则是一个用户空间的 ELF 文件和 libbpf loader 之间的协议。</p>\n<p>类型和字符串段是 BTF 内核 API 的一部分,描述了 bpf 程序引用到的(几乎是类型相关的)debug 信息。</p>\n<h2>2. BTF Type and String Encoding</h2>\n<p>头文件 <code>include/uapi/linux/btf.h</code> 中提供了类型和字符串是如何编码的高阶定义。</p>\n<p>data blob 的开头必须是:</p>\n<pre><code class=\"language-c\">struct btf_header {\n __u16 magic;\n __u8 version;\n __u8 flags;\n __u32 hdr_len;\n\n /* All offsets are in bytes relative to the end of this header */\n __u32 type_off; /* offset of type section */\n __u32 type_len; /* length of type section */\n __u32 str_off; /* offset of string section */\n __u32 str_len; /* length of string section */\n};\n</code></pre>\n<p>魔数 <code>magic</code> 是 <code>0xeB9F</code>,它在大端和小端系统中编码顺序不同,因此可以用来检验 BTF 是由大端还是小端目标机器生成的。<code>btf_header</code> 被设计成了可拓展的,当 data blob 生成时,它的<code>hdr_len</code> 等于<code>sizeof(struct btf_header)</code>。</p>\n<h3>2.1 String Encoding</h3>\n<p>在上面的结构体中,字符串段第一个字符串一定是一个空字符串。字符串表剩下的部分是其他以空字符为结尾的字符串的拼接。</p>\n<h3>2.2 Type Encoding</h3>\n<p>类型 ID <code>0</code> 预留给了 <code>void</code> 类型。类型部分是顺序解析的,并且分配了类型 ID 给可以解析的类型,是从 <code>1</code> 开始的。目前支持下面这些类型:</p>\n<pre><code class=\"language-c\">#define BTF_KIND_INT 1 /* Integer */\n#define BTF_KIND_PTR 2 /* Pointer */\n#define BTF_KIND_ARRAY 3 /* Array */\n#define BTF_KIND_STRUCT 4 /* Struct */\n#define BTF_KIND_UNION 5 /* Union */\n#define BTF_KIND_ENUM 6 /* Enumeration */\n#define BTF_KIND_FWD 7 /* Forward */\n#define BTF_KIND_TYPEDEF 8 /* Typedef */\n#define BTF_KIND_VOLATILE 9 /* Volatile */\n#define BTF_KIND_CONST 10 /* Const */\n#define BTF_KIND_RESTRICT 11 /* Restrict */\n#define BTF_KIND_FUNC 12 /* Function */\n#define BTF_KIND_FUNC_PROTO 13 /* Function Proto */\n#define BTF_KIND_VAR 14 /* Variable */\n#define BTF_KIND_DATASEC 15 /* Section */\n</code></pre>\n<p>注意类型段不只是纯粹的类型信息,还编码了为 debug 而存在的信息。例如 <code>BTF_KIND_FUNC</code> 就不是一个类型,它代表着一个定义好的程序。每个类型都包含有下面的公共数据:</p>\n<pre><code class=\"language-c\">struct btf_type {\n __u32 name_off;\n /* 32 位的 "info" 变量各位的含义\n * bits 0-15: vlen (e.g. # of struct's members)\n * bits 16-23: unused\n * bits 24-27: kind (e.g. int, ptr, array...etc)\n * bits 28-30: unused\n * bit 31: kind_flag, currently used by\n * struct, union and fwd\n */\n __u32 info;\n /* "size" is used by INT, ENUM, STRUCT and UNION.\n * "size" tells the size of the type it is describing.\n *\n * "type" is used by PTR, TYPEDEF, VOLATILE, CONST, RESTRICT,\n * FUNC and FUNC_PROTO.\n * "type" is a type_id referring to another type.\n */\n union {\n __u32 size;\n __u32 type;\n };\n};\n</code></pre>\n<p>对于特定的类型,公共数据之后就是他们各自独特的数据。 结构体<code>struct btf_type</code>中的<code>name_off</code> 指定了特定类型在字符串表中的偏移。</p>\n<h2>3. BTF Kernel API</h2>\n<p>下列 bpf 系统调用命令包含了 BTF,除此以外还有很多<a href=\"https://www.kernel.org/doc/html/latest/bpf/btf.html#btf-kernel-api\">其他调用</a></p>\n<ul>\n<li>\n<p>BPF_BTF_LOAD:将一个 blob 的 BTF 数据加载到 kernel 中。</p>\n</li>\n<li>\n<p>BPF_MAP_CREATE:创建有 BTF 键和类型信息值的映射。</p>\n</li>\n<li>\n<p>BPF_PROG_LOAD:加载有 BTF 函数和 line 信息的程序。</p>\n</li>\n<li>\n<p>BPF_BTF_GET_FD_BY_ID:得到一个 BTF 文件描述符 fd。</p>\n</li>\n<li>\n<p>BPF_OBJ_GET_INFO_BY_FD:该函数将返回 BTF,函数信息,line 信息和其他 BTF 相关信息。</p>\n</li>\n</ul>\n<p>工作流通常看上去是这样的:</p>\n<pre><code>Application:\n BPF_BTF_LOAD\n |\n v\n BPF_MAP_CREATE and BPF_PROG_LOAD\n |\n V\n ......\n\nIntrospection tool:\n ......\n BPF_{PROG,MAP}_GET_NEXT_ID (get prog/map id's)\n |\n V\n BPF_{PROG,MAP}_GET_FD_BY_ID (get a prog/map fd)\n |\n V\n BPF_OBJ_GET_INFO_BY_FD (get bpf_prog_info/bpf_map_info with btf_id)\n | |\n V |\n BPF_BTF_GET_FD_BY_ID (get btf_fd) |\n | |\n V |\n BPF_OBJ_GET_INFO_BY_FD (get btf) |\n | |\n V V\n pretty print types, dump func signatures and line info, etc.\n</code></pre>\n<h2>4. ELF File Format Interface</h2>\n<h3>4.1 .BTF section</h3>\n<p><code>.BTF</code> 段包含着类型数据和字符串数据。这部分的格式和 <a href=\"https://www.kernel.org/doc/html/latest/bpf/btf.html#btf-type-and-string-encoding\">2. BTF Type and String Encoding</a> 中描述的一样。</p>\n<h3>4.2 .BTF.ext section</h3>\n<p><code>.BTF.ext</code> 段编码了 func_info 和 line_info,需要使用 loader 才会被加载到内核中。</p>\n<p><code>.BTF.ext </code> 段的详细文档在文件 <code>tools/lib/bpf/btf.h</code> 和 <code>tools/lib/bpf/btf.c</code>。</p>\n<p>当前相关的头文件中的定义如下:</p>\n<pre><code class=\"language-c\">struct btf_ext_header {\n __u16 magic;\n __u8 version;\n __u8 flags;\n __u32 hdr_len;\n\n /* All offsets are in bytes relative to the end of this header */\n __u32 func_info_off;\n __u32 func_info_len;\n __u32 line_info_off;\n __u32 line_info_len;\n};\n</code></pre>\n<p>类似于 <code>.BTF</code> 段, 但是不是 <code>string/info</code> 段,它包含的是 <code>func_info</code> 和 <code>line_info</code> 段。</p>\n<p>可以参考 <a href=\"https://www.kernel.org/doc/html/latest/bpf/btf.html#bpf-prog-load\">3.3 BPF_PROG_LOAD</a> 中 <code>func_info</code> 和 <code>line_info</code> 的详细信息。</p>\n<p><code>func_info</code> 如下组织:</p>\n<pre><code>func_info_rec_size\nbtf_ext_info_sec for section #1 /* func_info for section #1 */\nbtf_ext_info_sec for section #2 /* func_info for section #2 */\n...\n</code></pre>\n<p>当 <code>.BTF.ext</code> 生成时,<code>func_info_rec_size</code> 定义了<code>bpf_func_info</code> 结构的大小。在下面定义的 <code>btf_ext_info_sec</code> 是每个特定的 ELF 段中的一系列 <code>func_info</code>:</p>\n<pre><code class=\"language-c\">struct btf_ext_info_sec {\n __u32 sec_name_off; /* offset to section name */\n __u32 num_info;\n /* Followed by num_info * record_size number of bytes */\n __u8 data[0];\n};\n</code></pre>\n<p>这里的 <code>num_info</code> 必须大于 0。</p>\n<p><code>line_info</code> 如下组织:</p>\n<pre><code>line_info_rec_size\nbtf_ext_info_sec for section #1 /* line_info for section #1 */\nbtf_ext_info_sec for section #2 /* line_info for section #2 */\n...\n</code></pre>\n<p>当 <code>.BTF.ext</code> 生成时,<code>line_info_rec_size</code> 定义了 <code>bpf_line_info</code> 结构的大小。</p>\n<p><code>bpf_func_info->insn_off</code> 和 <code>bpf_line_info->insn_off</code> 在内核 API 和 ELF API 中有着不同的阐释。在内核 API 中,<code>insn_off</code> 是指令在 <code>struct bpf_insn</code> 内的偏移。对于 ELF API,<code>insn_off</code> 是相对于段 <code>btf_ext_info_sec->sec_name_off</code>开始的字节偏移。</p>\n<h3>4.2 .BTF_ids section</h3>\n<p><code>.BTF_ids</code> 段编码了内核中定义的 BTF ID 值。借助头文件 <code>include/linux/btf_ids.h</code> 中定义的宏,可以在内核编译时创建这一段数据。内核代码能够使用下面的语法来创建 BTF ID 值的列表和有序列表:</p>\n<pre><code class=\"language-c\">BTF_ID_LIST(list)\nBTF_ID(type1, name1)\nBTF_ID(type2, name2)\n</code></pre>\n<p>这将会生成下面的 .BTF_ids 段的布局:</p>\n<pre><code>__BTF_ID__type1__name1__1:\n.zero 4\n__BTF_ID__type2__name2__2:\n.zero 4\n</code></pre>\n<p><code>u32 list[];</code> 变量被定义来获取列表。</p>\n<p><code>BTF_ID_UNUSED</code> 宏定义了四个零字节。当我们想要在 BTF_ID_LIST 中定义 unused entry,可以这样做:</p>\n<pre><code class=\"language-c\">BTF_ID_LIST(bpf_skb_output_btf_ids)\nBTF_ID(struct, sk_buff)\nBTF_ID_UNUSED\nBTF_ID(struct, task_struct)\n</code></pre>\n<p><code>BTF_SET_START/END</code> 这一对宏定义了有序列表 BTF ID 值和它们的计数值,它的语法如下:</p>\n<pre><code class=\"language-c\">BTF_SET_START(set)\nBTF_ID(type1, name1)\nBTF_ID(type2, name2)\nBTF_SET_END(set)\n</code></pre>\n<p>这将会生成下面的 .BTF_ids 段的布局:</p>\n<pre><code>A__BTF_ID__set__set:\n.zero 4\n__BTF_ID__type1__name1__3:\n.zero 4\n__BTF_ID__type2__name2__4:\n.zero 4\n</code></pre>\n<p><code>struct btf_id_set set;</code> 变量可以获取到列表。<code>typeX</code> 名字可以是下面的任一项:</p>\n<pre><code class=\"language-c\">struct, union, typedef, func\n</code></pre>\n<p>并且可以被用来在解析 BTF ID 值的时候作为过滤器使用。所有的 BTF ID 列表和有序列表都被编译到了 <code>.BTF_ids</code> 段,它在<code>resolve_btfids</code>构建出的内核的链接阶段解析。</p>\n<h2>5. Using BTF</h2>\n<h3>5.1 bpftool map pretty print</h3>\n<p>借助 BTF,映射的健值能够以域的形式打印出来,而不是仅仅打印出裸字节。这对于大型的结构或是你的数据结构中各比特位有独立意义时很有价值。例如下面的映射:</p>\n<pre><code class=\"language-c\">enum A { A1, A2, A3, A4, A5 };\ntypedef enum A ___A;\nstruct tmp_t {\n char a1:4;\n int a2:4;\n int :4;\n __u32 a3:4;\n int b;\n ___A b1:4;\n enum A b2:4;\n};\nstruct bpf_map_def SEC("maps") tmpmap = {\n .type = BPF_MAP_TYPE_ARRAY,\n .key_size = sizeof(__u32),\n .value_size = sizeof(struct tmp_t),\n .max_entries = 1,\n};\nBPF_ANNOTATE_KV_PAIR(tmpmap, int, struct tmp_t);\n</code></pre>\n<p>可以这样使用 bpftool 来优雅地打印:</p>\n<pre><code class=\"language-json\">[{\n "key": 0,\n "value": {\n "a1": 0x2,\n "a2": 0x4,\n "a3": 0x6,\n "b": 7,\n "b1": 0x8,\n "b2": 0xa\n }\n }\n]\n</code></pre>\n<h3>5.2 bpftool prog dump</h3>\n<p>下面的例子展示了 func_info 和 line_info 能够更好地帮助 dump 出内核符号名称、函数原型和 line 信息:</p>\n<pre><code class=\"language-bash\">$ bpftool prog dump jited pinned /sys/fs/bpf/test_btf_haskv\n[...]\nint test_long_fname_2(struct dummy_tracepoint_args * arg):\nbpf_prog_44a040bf25481309_test_long_fname_2:\n; static int test_long_fname_2(struct dummy_tracepoint_args *arg)\n 0: push %rbp\n 1: mov %rsp,%rbp\n 4: sub $0x30,%rsp\n b: sub $0x28,%rbp\n f: mov %rbx,0x0(%rbp)\n 13: mov %r13,0x8(%rbp)\n 17: mov %r14,0x10(%rbp)\n 1b: mov %r15,0x18(%rbp)\n 1f: xor %eax,%eax\n 21: mov %rax,0x20(%rbp)\n 25: xor %esi,%esi\n; int key = 0;\n 27: mov %esi,-0x4(%rbp)\n; if (!arg->sock)\n 2a: mov 0x8(%rdi),%rdi\n; if (!arg->sock)\n 2e: cmp $0x0,%rdi\n 32: je 0x0000000000000070\n 34: mov %rbp,%rsi\n; counts = bpf_map_lookup_elem(&btf_map, &key);\n[...]\n</code></pre>\n<h3>5.3 Verifier Log</h3>\n<p>下面的例子展示了 line_info 是如何帮助 debug 验证错误的:</p>\n<pre><code class=\"language-bash\"> /* The code at tools/testing/selftests/bpf/test_xdp_noinline.c\n * is modified as below.\n */\n data = (void *)(long)xdp->data;\n data_end = (void *)(long)xdp->data_end;\n /*\n if (data + 4 > data_end)\n return XDP_DROP;\n */\n *(u32 *)data = dst->dst;\n\n$ bpftool prog load ./test_xdp_noinline.o /sys/fs/bpf/test_xdp_noinline type xdp\n ; data = (void *)(long)xdp->data;\n 224: (79) r2 = *(u64 *)(r10 -112)\n 225: (61) r2 = *(u32 *)(r2 +0)\n ; *(u32 *)data = dst->dst;\n 226: (63) *(u32 *)(r2 +0) = r1\n invalid access to packet, off=0 size=4, R2(id=0,off=0,r=0)\n R2 offset is outside of the packet\n</code></pre>\n<h2>6. BTF Generation</h2>\n<p>你需要使用最新的 <a href=\"https://git.kernel.org/pub/scm/devel/pahole/pahole.git/\">pahole</a> 或 8.0 版本以上的 llvm来生成 BTF。</p>\n<p>pahole 是一个 dwarf2btf 转换器,它还不支持 <code>.BTF.ext</code> 和 <code>btf</code> <code>BTF_KIND_FUNC</code> 类型。例如:</p>\n<pre><code class=\"language-bash\">-bash-4.4$ cat t.c\nstruct t {\n int a:2;\n int b:3;\n int c:2;\n} g;\n-bash-4.4$ gcc -c -O2 -g t.c\n-bash-4.4$ pahole -JV t.o\nFile t.o:\n[1] STRUCT t kind_flag=1 size=4 vlen=3\n a type_id=2 bitfield_size=2 bits_offset=0\n b type_id=2 bitfield_size=3 bits_offset=2\n c type_id=2 bitfield_size=2 bits_offset=5\n[2] INT int size=4 bit_offset=0 nr_bits=32 encoding=SIGNED\n</code></pre>\n<p>llvm 能够直接生成 <code>.BTF</code> 和 <code>.BTF.ext</code>,选项是 -g,目标选项是 bpf。 使用 readelf 工具的 -S 选项能够显示出汇编格式的 BTF 代码:</p>\n<pre><code class=\"language-asm\">-bash-4.4$ cat t2.c\ntypedef int __int32;\nstruct t2 {\n int a2;\n int (*f2)(char q1, __int32 q2, ...);\n int (*f3)();\n} g2;\nint main() { return 0; }\nint test() { return 0; }\n-bash-4.4$ clang -c -g -O2 -target bpf t2.c\n-bash-4.4$ readelf -S t2.o\n ......\n [ 8] .BTF PROGBITS 0000000000000000 00000247\n 000000000000016e 0000000000000000 0 0 1\n [ 9] .BTF.ext PROGBITS 0000000000000000 000003b5\n 0000000000000060 0000000000000000 0 0 1\n [10] .rel.BTF.ext REL 0000000000000000 000007e0\n 0000000000000040 0000000000000010 16 9 8\n ......\n-bash-4.4$ clang -S -g -O2 -target bpf t2.c\n-bash-4.4$ cat t2.s\n ......\n .section .BTF,"",@progbits\n .short 60319 # 0xeb9f\n .byte 1\n .byte 0\n .long 24\n .long 0\n .long 220\n .long 220\n .long 122\n .long 0 # BTF_KIND_FUNC_PROTO(id = 1)\n .long 218103808 # 0xd000000\n .long 2\n .long 83 # BTF_KIND_INT(id = 2)\n .long 16777216 # 0x1000000\n .long 4\n .long 16777248 # 0x1000020\n ......\n .byte 0 # string offset=0\n .ascii ".text" # string offset=1\n .byte 0\n .ascii "/home/yhs/tmp-pahole/t2.c" # string offset=7\n .byte 0\n .ascii "int main() { return 0; }" # string offset=33\n .byte 0\n .ascii "int test() { return 0; }" # string offset=58\n .byte 0\n .ascii "int" # string offset=83\n ......\n .section .BTF.ext,"",@progbits\n .short 60319 # 0xeb9f\n .byte 1\n .byte 0\n .long 24\n .long 0\n .long 28\n .long 28\n .long 44\n .long 8 # FuncInfo\n .long 1 # FuncInfo section string offset=1\n .long 2\n .long .Lfunc_begin0\n .long 3\n .long .Lfunc_begin1\n .long 5\n .long 16 # LineInfo\n .long 1 # LineInfo section string offset=1\n .long 2\n .long .Ltmp0\n .long 7\n .long 33\n .long 7182 # Line 7 Col 14\n .long .Ltmp3\n .long 7\n .long 58\n .long 8206 # Line 8 Col 14\n</code></pre>\n",
"url": "https://forsworns.github.io///zh/blogs/20210706/",
"title": "BPF Type Format (BTF)",
"summary": "BPF Type Format (BTF) 文章翻译笔记",
"date_modified": "2021-07-06T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>使用 libbpf-bootstrap 构建 BPF 程序</h1>\n<p>libbpf 是一个比 <a href=\"https://github.com/iovisor/bcc/\">BCC</a> 更新的 BPF 开发库,也是最新的 BPF 开发推荐方式,官方提供了 <a href=\"https://github.com/libbpf/libbpf\">C</a> 和 <a href=\"https://github.com/libbpf/libbpf-rs\">Rust</a> 的库实现。libbpf 支持最新的 BPF CO-RE 特性(单次编译到处执行),它不像 BCC 依赖于 Clang/LLVM 运行时,也不是通过封装后的 Python 接口书写而是直接使用 C 或 Rust 进行开发,也不需要内核开发头文件。所以转向 libbpf 的使用吧。为了方便初学者和习惯了 BCC 的开发者,官方提供了 <a href=\"https://github.com/libbpf/libbpf-bootstrap\">libbpf-bootstrap</a> 示例项目,BCC 项目里也有一些用 libbpf 构建的工具 <a href=\"https://github.com/iovisor/bcc/tree/master/libbpf-tools\">bcc/libbpf-tools</a>。</p>\n<p>下面的内容翻译自相关<a href=\"https://nakryiko.com/posts/libbpf-bootstrap/\">博客:Building BPF applications with libbpf-bootstrap</a></p>\n<p>使用 <a href=\"https://github.com/libbpf/libbpf-bootstrap\">libbpf-bootstrap</a> 脚手架项目快速上手 libbpf,该项目已经配置好了环境,能够让开发者直接找到 BPF 的乐趣所在。下面我们将一起查看 libbpf-bootstrap 到底干了些什么,以及这一切是如何发挥作用的。</p>\n<h2>为什么使用 libbpf-bootstrp?</h2>\n<p>BPF 是一种令人惊异的内核技术,它能够让开发者一览内核函数是如何工作的,即使该开发者没有内核开发的经验,也不需要该开发者花费大量时间在内核开发环境配置上。BPF 也降低了在检阅内核工作状态时 OS 崩溃的风险。一旦你掌握了 BPF,你就会了解其中的乐趣和它无穷的能力。</p>\n<p>但是 BPF 的起步对很多人来说,仍然会是一个令人生畏的环节。因为构建一个 BPF 应用程序的工作流,即使是一个 “hello world” 程序都需要大量的步骤。这会是令人失望的,而且会吓走一大片开发者。这个过程不难,但是知晓其中必要的步骤仍然可能会劝退很多人,即使他们知道 BPF 的威力。</p>\n<p><a href=\"https://github.com/libbpf/libbpf-bootstrap\">libbpf-bootstrap</a> 就是这样一个 BPF 游乐场,它已经尽可能地为初学者配置好了环境,帮助他们可以直接步入到 BPF 程序的书写。它综合了 BPF 社区多年来的最佳实践,并且提供了一个现代化的、便捷的工作流。libbpf-bootstrap 依赖于 libbpf 并且使用了一个很简单的 Makefile。对于需要更高级设置的用户,它也是一个好的起点。即使这个 Makefile不会被直接使用到,也可以很轻易地迁移到别的构建系统上。</p>\n<p>libbpf-bootstrap 中有两个示例 BPF 程序(目前已经不止这两个了): <code>minimal</code> 和 <code>bootstrap</code>。<code>minimal</code> 是能够编译、加载和运行的最小化的 BPF 程序,它做的就是 BPF 中等价的<code>printf("Hello, World!")</code>。既然是最小化的一个程序,它也不会依赖于很新的内核特性,即使是旧的内核版本,它也应该会正常工作。</p>\n<p>运行 <code>minimal</code> 示例可以很快地在本地完成一个小的测试,但是它不能反映出用于生产环境下的 BPF 程序是否在各种各样的内核上都是可以使用的。<code>bootstrap</code> 是一个这样的示例,它构建了一个最小化的可迁移的 BPF 程序。为了满足这个需求,它依赖于 <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/\">BPF CO-RE</a> 特性和内核的 <a href=\"https://nakryiko.com/posts/btf-dedup/\">BTF</a> 支持,所以确保你的 Linux 内核在构建的时候选择了<code>CONFIG_DEBUG_INFO_BTF=y</code> 内核选项。可以参考 <a href=\"https://github.com/libbpf/libbpf#bpf-co-re-compile-once--run-everywhere\">libbpf README</a>,查阅已经配置好这些的 Linux 发行版。如果你想要减少构建自定义内核的麻烦,就尽可能地使用更新的内核吧。</p>\n<p>另外,<code>bootstrap</code> 还演示了 BPF 全局变量(Linux 5.5+)和 <a href=\"https://nakryiko.com/posts/bpf-ringbuf\">BPF ring buffer</a>(Linux 5.8+)的使用。这些特性不是构建 BPF 程序必要的组件,但是他们带来了巨大的可用性提升和更现代化的 BPF 程序开发方法,所以他们被加入到了这个示例中。</p>\n<h2>背景</h2>\n<p>BPF 是一个持续演进的技术,这意味着新特性将会被持续添加进来,所以视你要采用的 BPF 特性,你可能需要更新的内核版本。但是 BPF 社区非常严肃地考虑了后向兼容性,所以旧的内核仍然可以运行 BPF 程序,假如你不需要新功能的话。所以你的 BPF 程序的逻辑越简单,特性越少,你的 BPF 程序就可以运行在越旧的内核上。</p>\n<p>BPF 的用户体验是一直在提升的,更新的内核版本中的 BPF 提供了更加巨大的易用性上的改进。所以如果你只是刚起步,不需要支持旧版的内核,还是用最新的内核吧,让自己少掉点头发。</p>\n<p>BPF 程序一般是用 C 语言写的,会有一些代码结构方面的拓展,来让 <a href=\"https://github.com/libbpf/libbpf\">libbpf</a> 知晓 BPF 代码的结构,更高效地处理他们,<a href=\"https://clang.llvm.org/\">Clang</a> 是 BPF 代码编译推荐使用的编译器,通常也会推荐使用最新的 Clang。Clang 10 或者更新的版本能够处理大多数的 BPF 特性,但是更先进的 <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/\">BPF CO-RE</a> 特性需要 Clang 11 甚至是 Clang 12(例如,一些最近的 CO-RE relocation built-ins)。</p>\n<p>libbpf-bootstrap 打包了 libbpf (作为一个 Git submodule)和 bpftool (只适用于 x86-64 体系)来避免任何你的某个特定 Linux 发行版的依赖需求。<strong>你的系统需要安装 <code>zlib</code> (<code>libz-dev</code> 或 <code>zlib-devel</code> 包) 和<code>libelf</code> (<code>libelf-dev</code> 或 <code>elfutils-libelf-devel</code> package) 。这些是 <code>libbpf</code> 编译和正确运行的必要依赖</strong>,(注意对于 BTF 的支持情况,需要参考官方文档或之前的<a href=\"/zh/blogs/20210311/#libbpf\">笔记</a>,只有较新的发行版直接暴露了 BTF 到 <code>/sys/kernel/btf/vmlinux</code>)。</p>\n<p>这篇文章不是 BPF 技术的入门介绍,所以假定读者已经知晓了基本的概念,比如 BPF program,BPF map,BPF hooks (attach points) 。如果你需要重温一下基础知识,可以看<a href=\"https://docs.cilium.io/en/latest/bpf/\">这些</a> <a href=\"https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/\">资料</a>。</p>\n<p>下面将会详细介绍 <a href=\"https://github.com/libbpf/libbpf-bootstrap\">libbpf-bootstrap</a> 的结构,它的 Makefile 和 <code>minimal</code> 、<code>bootstrap</code> 两个示例。我们将会了解 libbpf 的代码风格,了解如何把 BPF C 程序构建成使用 libbpf 作为 BPF program loader 的形式,以及如何使用用户空间的 libbpf API 和你的 BPF 程序交互。</p>\n<h2>Libbpf-bootstrap 概览</h2>\n<p>下面就是 <a href=\"https://github.com/libbpf/libbpf-bootstrap\"><code>libbpf-bootstrap</code></a> 的目录结构</p>\n<pre><code>$ tree\n.\n├── libbpf\n│ ├── ...\n│ ... \n├── LICENSE\n├── README.md\n├── src\n│ ├── bootstrap.bpf.c\n│ ├── bootstrap.c\n│ ├── bootstrap.h\n│ ├── Makefile\n│ ├── minimal.bpf.c\n│ ├── minimal.c\n│ ├── vmlinux_508.h\n│ └── vmlinux.h -> vmlinux_508.h\n└── tools\n ├── bpftool\n └── gen_vmlinux_h.sh\n\n16 directories, 85 files\n</code></pre>\n<p><code>libbpf-bootstrap</code> 把 libbpf 打包成了 <code>libbpf/</code> 子目录下的一个子模块来避免系统侧对 libbpf 的依赖。</p>\n<p><code>tools/</code> 包含了 <code>bpftool</code> 的二进制文件,用来构建你的 BPF 程序的 <a href=\"https://nakryiko.com/posts/bcc-to-libbpf-howto-guide/#bpf-skeleton-and-bpf-app-lifecycle\">BPF skeletons</a>。 类似 libbpf,它被打包进来以避免依赖问题。</p>\n<p>另外, bpftool 能被用来生成你自己的包含内核类型定义的 <code>vmlinux.h</code>头文件。 一般来说你不需要这么做,因为 libbpf-bootstrap 已经在 <code>src/</code> 子目录下提供了预先生成的 <a href=\"https://raw.githubusercontent.com/libbpf/libbpf-bootstrap/master/src/vmlinux_508.h\">vmlinux.h</a>。 它基于 Linux 5.8 内核选项的默认设置,激活了一些额外的和 BPF 相关的功能的配置项。这意味着它已经有了一些通用的内核类型和常量。因为有 <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/\">BPF CO-RE</a>, <code>vmlinux.h</code> 不需要特定地去匹配你的内核配置和版本。但是如果你仍然要生成你自己的 <code>vmlinux.h</code>,尽管参考 <a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/tools/gen_vmlinux_h.sh\"><code>tools/gen_vmlinux_h.sh</code></a> 脚本吧,去看看它是如何做的。</p>\n<p><a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/src/Makefile\">Makefile</a> 定义了必要的构建规则,来编译所有提供的 BPF 应用。它遵从一个简单的文件命名规则。</p>\n<ul>\n<li><code><app>.bpf.c</code> 文件是 BPF C 代码包含了将在内核上下文中执行的逻辑。</li>\n<li><code><app>.c</code> 是用户空间的 C 代码,加载了 BPF 代码,在应用的整个生命周期内和它交互。</li>\n<li><em>optional</em> <code><app>.h</code> 是一个头文件,包含了常见的类型定义,是在 BPF 代码和用户空间代码之间共享的。</li>\n</ul>\n<h2>Minimal app</h2>\n<p><code>minimal</code> 是一个给初学者的很好的例子。它是一个最小化的 BPF 试验场所。它不使用 CO-RE 特性,所以你可以在旧些的内核上使用它,只需要 include 你的内核类型定义。这个例子不适合拿来做生产环境下用,但是做学习用途还是很好的。</p>\n<h3>The BPF side</h3>\n<p>下面就是 BPF 侧的代码 (<a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/src/minimal.bpf.c\">minimal.bpf.c</a>) :</p>\n<pre><code class=\"language-c\">// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause\n/* Copyright (c) 2020 Facebook */\n#include <linux/bpf.h>\n#include <bpf/bpf_helpers.h>\n\nchar LICENSE[] SEC("license") = "Dual BSD/GPL";\n\nint my_pid = 0;\n\nSEC("tp/syscalls/sys_enter_write")\nint handle_tp(void *ctx)\n{\n\tint pid = bpf_get_current_pid_tgid() >> 32;\n\n\tif (pid != my_pid)\n\t\treturn 0;\n\n\tbpf_printk("BPF triggered from PID %d.\\n", pid);\n\n\treturn 0;\n}\n</code></pre>\n<p><code>#include <linux/bpf.h></code> 导入了一些基础的、必要的 BPF 相关的类型和常量,以便使用内核侧的 BPF API,例如 BPF helper function flags。这个头文件是之后导入 <code>bpf_helpers.h</code> 这个头文件所必须的前提。而 <code>bpf_helpers.h</code> 是由 <code>libbpf</code> 提供的,包含了大多数常用的宏、常量和 BPF helper 的定义,几乎会在每个 BPF 应用中用到。例如上面用到的<code>bpf_get_current_pid_tgid()</code> 就是一个 BPF helper。</p>\n<p><code>LICENSE</code> 变量定义了你的 BPF 代码的 license。在内核开发中,明确 license 是必须的。一些 BPF 功能对于不兼容 GPL 的代码是不可用的。注意特殊的 <code>SEC("license")</code> 注解。 定义在 <code>bpf_helpers.h</code> 中的 <code>SEC()</code> 把变量和函数放到了特殊的段中。 <code>SEC("license")</code> 和一些其他的段名,是 libbpf 规定的,只要遵循就好了。</p>\n<p>接下来,我们看看 BPF 特性“全局变量”是如何使用的。代码 <code>int my_pid = 0;</code> 所做的正是你所想象的事情:它定义了一个全局变量,BPF 代码可以读取和更新它,就像其他用户空间的 C 代码对待全局变量那样。使用 BPF 全局变量维护程序的状态很方便,而且性能表现也不错。另外,这样的全局变量能够从用户侧读写。这个特性是从 Linux 5.5 之后才支持的。在用额外的设置项配置 BPF 程序的时候常常会用到它。它也经常用于在内核中的 BPF 代码和用户侧的控制代码之间传递数据。</p>\n<p><code>SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { ... }</code> 定义了 BPF 程序,它会被加载到内核中。它是由一个普通的 C 函数定义的,使用 <code>SEC()</code> 宏放在一个特殊的段中。段名定义了 libbpf 程序创建的是什么类型的 BPF 程序,以及它是附着到内核上哪个地方的。在这个例子中,我们是定义了一个 tracepoint BPF 程序,每次用户空间的应用调用了系统调用 <code>write()</code> 的时候,就会触发它。</p>\n<blockquote>\n<p>在同一个 BPF C 程序文件中,可能有多个 BPF 程序。他们可以是不同类型的,有着不同的 <code>SEC()</code> 宏。例如,你可以用不同的 BPF 程序追踪不同的系统调用或其他事件(如网络包的处理)。你也可以使用相同的 <code>SEC()</code> 宏来定义多个 BPF 程序,libbpf 会自动处理他们。在同一个 BPF C 代码文件中的所有的 BPF 程序共享所有的全局状态,例如上面例子中的 <code>my_pid</code> 变量,如果使用了 BPF map,它也是共享的。这常常用在 BPF 程序的协作中。</p>\n</blockquote>\n<p>下面仔细看看 BPF 程序 <code>handle_tp</code> 是在干嘛:</p>\n<pre><code class=\"language-c\"> int pid = bpf_get_current_pid_tgid() >> 32;\n\n if (pid != my_pid)\n return 0;\n</code></pre>\n<p>这部分获取了 PID,或者说是内核术语中的 "TGID" ,它存储在 <code>bpf_get_current_pid_tgid()</code> 返回值的高 32 位。接着,它会查看触发了 <code>write()</code> 系统调用的进程是否是我们的 <code>minimal</code> 进程。这对于一个很繁忙的系统是十分重要的,因为很可能有大量不相关的进程触发了 <code>write()</code>,使得你很难用这段 BPF 代码进行实验得到预期的结果。全局变量 <code>my_pid</code> 是通过下面的用户空间的代码进行初始化的,它会被初始化成真实的 PID 值。</p>\n<pre><code class=\"language-c\">\tbpf_printk("BPF triggered from PID %d.\\n", pid);\n</code></pre>\n<p>这就是 BPF 中的 <code>printf("Hello, world!\\n")</code>。它输出格式化的字符串到一个特殊的文件,叫作 <code>/sys/kernel/debug/tracing/trace_pipe</code>,你可以从控制台中去查看它的内容,注意你需要 <code>sudo</code> 来取得查看它的权限:</p>\n<pre><code class=\"language-bash\">$ sudo cat /sys/kernel/debug/tracing/trace_pipe\n\t<...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: BPF triggered from PID 3840345.\n\t<...>-3840345 [010] d... 3220702.101265: bpf_trace_printk: BPF triggered from PID 3840345.\n</code></pre>\n<blockquote>\n<p><code>bpf_printk()</code> 帮助函数和 <code>trace_pipe</code> 文件一般不在生产环境中使用,它们是用来辅助 BPF 程序的 debug 的,帮助开发者知道自己的代码到底干了些什么事情。目前还没有 BPF 的调试工具,所以这种输出调试是目前最方便的调试方法了。</p>\n</blockquote>\n<p>这就是 BPF 侧的 <code>minimal</code> 应用了,你可以加一些别的代码到 <code>handle_tp()</code> 中,按你所需去拓展它。</p>\n<h3>The user-space side</h3>\n<p>让我们看看用户空间到底做了啥 (<a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/src/minimal.c\">minimal.c</a>),我们会跳过一些显然的部分,但是无论如何,读者都应该去看一下完整的代码。</p>\n<pre><code class=\"language-c\">#include "minimal.skel.h"\n</code></pre>\n<p>这里导入了 BPF 代码 <code>minimal.bpf.c</code> 中的 BPF skeleton。它是在 Makefile中的某一步,由 bpftool 自动生成的文件,它高度抽象了<code>minimal.bpf.c</code> 的结构。它也简化了 BPF 代码部署的逻辑,将编译出的 BPF 目标代码嵌入到了头文件中,该头文件又会被用户空间的代码所引用。你的应用程序的二进制文件中不会有其他多余的文件了,就导入它就好了。</p>\n<blockquote>\n<p>BPF skeleton 是完全由 <code>libbpf</code> 构造出的,内核对它一无所知。但是它的存在显著提升了 BPF 开发体验,所以, 最好熟悉一下它。可以看这篇 <a href=\"https://nakryiko.com/posts/bcc-to-libbpf-howto-guide/#bpf-skeleton-and-bpf-app-lifecycle\">博客</a> 来了解 BPF skeleton 的细节。</p>\n</blockquote>\n<p>libbpf-bootstrap 的 BPF skeletons 在成功 <code>make</code> 后,生成到了 <code>src/.output/<app>.skel.h</code> 中。为了获取直观感受,下面是 <code>minimal.bpf.c</code> 的 skeletons 高度抽象后的概览:</p>\n<pre><code class=\"language-c\">/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */\n\n/* THIS FILE IS AUTOGENERATED! */\n#ifndef __MINIMAL_BPF_SKEL_H__\n#define __MINIMAL_BPF_SKEL_H__\n\n#include <stdlib.h>\n#include <bpf/libbpf.h>\n\nstruct minimal_bpf {\n\tstruct bpf_object_skeleton *skeleton;\n\tstruct bpf_object *obj;\n\tstruct {\n\t\tstruct bpf_map *bss;\n\t} maps;\n\tstruct {\n\t\tstruct bpf_program *handle_tp;\n\t} progs;\n\tstruct {\n\t\tstruct bpf_link *handle_tp;\n\t} links;\n\tstruct minimal_bpf__bss {\n\t\tint my_pid;\n\t} *bss;\n};\n\nstatic inline void minimal_bpf__destroy(struct minimal_bpf *obj) { ... }\nstatic inline struct minimal_bpf *minimal_bpf__open_opts(const struct bpf_object_open_opts *opts) { ... }\nstatic inline struct minimal_bpf *minimal_bpf__open(void) { ... }\nstatic inline int minimal_bpf__load(struct minimal_bpf *obj) { ... }\nstatic inline struct minimal_bpf *minimal_bpf__open_and_load(void) { ... }\nstatic inline int minimal_bpf__attach(struct minimal_bpf *obj) { ... }\nstatic inline void minimal_bpf__detach(struct minimal_bpf *obj) { ... }\n\n#endif /* __MINIMAL_BPF_SKEL_H__ */\n</code></pre>\n<p>上面的自动生成的代码中,会有一个 <code>struct bpf_object *obj;</code> ,它会被传递给 libbpf 的 API。它也包含有 <code>maps</code>, <code>progs</code> 和 <code>links</code> 等段,可以直接获取到你的 BPF 代码中定义的 BPF map 和程序。例如,前面提到的 BPF 程序 <code>handle_tp</code> 。这些引用能够直接传递给 libbpf的 API 去完成 BPF map/program/link 相关的工作。Skeleton 也可以包含可选的 bss、data、rodata 段,从而可以直接从用户空间访问 BPF 全局变量而不必使用额外的系统调用。在这种情况下,我们的<code>my_pid</code> BPF 变量对应的是 <code>bss->my_pid</code> 域。</p>\n<p>现在看看 <code>minimal</code> 应用的 <code>main()</code> 函数在干些什么:</p>\n<pre><code class=\"language-c\">int main(int argc, char **argv)\n{\n\tstruct minimal_bpf *skel;\n\tint err;\n\n\t/* Set up libbpf errors and debug info callback */\n\tlibbpf_set_print(libbpf_print_fn);\n</code></pre>\n<p><code>libbpf_set_print()</code> 提供了一个自定义的回调给所有的 libbpf 日志输出。这很有用,特别是在活跃的开发时期,因为它允许捕获有用的 libbpf 调试日志。默认情况下,libbpf 将只打印错误级别的信息。调试日志则会帮助我们更快地定位问题。</p>\n<blockquote>\n<p>想报告 libbpf 或你的基于 libbpf 开发的应用的问题,可以发送邮件到 <a href=\"mailto://bpf@vger.kernel.org\">bpf@vger.kernel.org</a> 邮件列表,记得附上你的调试信息。</p>\n</blockquote>\n<p>在 <code>minimal</code> 这个示例中, <code>libbpf_print_fn()</code> 只是把所有内容都打印到标准输出 stdout。</p>\n<pre><code class=\"language-c\">\t/* Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything */\n\tbump_memlock_rlimit();\n</code></pre>\n<p>这是一步令人困惑但也是必要的步骤,大多数 BPF 程序都要这么去做。它放松了内核中对每个用户内存的约束,允许 BPF 子系统分配必要的资源给你的 BPF 程序和 BPF maps 等。这个限制很可能会被马上移除掉,但是目前你需要打开这个内存限制,即 <code>RLIMIT_MEMLOCK</code> <a href=\"https://man7.org/linux/man-pages/man2/getrlimit.2.html\">limit</a> 。通过 <code>minimal</code> 代码中使用的 <code>setrlimit(RLIMIT_MEMLOCK, ...)</code> ,是最简单也最便捷的方法。</p>\n<pre><code class=\"language-c\">\t/* Load and verify BPF application */\n\tskel = minimal_bpf__open_and_load();\n\tif (!skel) {\n\t\tfprintf(stderr, "Failed to open and load BPF skeleton\\n");\n\t\treturn 1;\n\t}\n</code></pre>\n<p>现在,使用自动生成的 BPF skeleton,加载 BPF 程序到内核中,然后让 BPF verifier 校验它是否合法,如果这步成功了,你的 BPF 代码就是正确的,而且可以附着到任何一个你需要的 BPF hook 上。</p>\n<pre><code class=\"language-c\">\t/* ensure BPF program only handles write() syscalls from our process */\n\tskel->bss->my_pid = getpid();\n</code></pre>\n<p>但是首先,我们需要与 BPF 交流我们的用户态程序的 PID,以便它能够过滤掉不相关的进程触发的 <code>write()</code>事件。上面的这行代码会直接设置映射过的内存区域的 BPF 全局变量 <code>my_pid</code>。如上面提到的,这就是用户态读写 BPF 全局变量的方式。</p>\n<pre><code class=\"language-c\">\t/* Attach tracepoint handler */\n\terr = minimal_bpf__attach(skel);\n\tif (err) {\n\t\tfprintf(stderr, "Failed to attach BPF skeleton\\n");\n\t\tgoto cleanup;\n\t}\n\n\tprintf("Successfully started!\\n");\n</code></pre>\n<p>终于,我们可以将 BPF 程序<code>handle_tp</code> 附着到到内核的锚点上(即上面的 BPF hook)。BPF 程序会随之响应,内核会开始在内核上下文中,回应每个 <code>write()</code> 系统调用,执行我们自定义的 BPF 代码。</p>\n<blockquote>\n<p>通过查看 <code>SEC()</code> 注解,libbpf 能够自动决定在什么地方附着 BPF 程序。这并非对所有类型的 BPF 程序都适用,但是对大多数还是适用的,比如:tracepoints、kprobes 等等(具体的 BPF 程序种类,可以参考之前的<a href=\"/zh/blogs/20210329/#%E5%B8%B8%E8%A7%81-bpf-prog-type-%E5%AE%9A%E4%B9%89\">笔记</a>)。另外,libbpf 提供了额外的 API 来附着 BPF 程序,可以通过用户的编程实现。</p>\n</blockquote>\n<pre><code class=\"language-c\">\tfor (;;) {\n\t\t/* trigger our BPF program */\n\t\tfprintf(stderr, ".");\n\t\tsleep(1);\n\t}\n</code></pre>\n<p>上面代码中的无穷循环确保了 BPF 程序 <code>handle_tp</code> 能够一直附着在内核中,直到用户关掉进程,如按下 <code>Ctrl-C</code>。同时,它还会周期性地(每秒)调用 <code>fprintf(stderr, ...)</code>,从而触发一次 <code>write()</code> 系统调用。通过这种方法,可以通过 <code>handle_tp</code> 监控内核的内部情况和状态随时间的变化。</p>\n<pre><code class=\"language-makefile\">cleanup:\n\tminimal_bpf__destroy(skel);\n\treturn -err;\n}\n</code></pre>\n<p>如果前面任一个步骤错误了,<code>minimal_bpf__destroy()</code> 将会像上面这几行代码所述,在内核和用户空间清除所有的资源。这是一个好习惯,但是即使你的程序还没清理就崩溃了,内核也仍然能够清理掉资源。好吧,至少大多数情况下是这样的。也有一些类型的 BPF 程序,会在内核中一直保持活跃,即使它自己的用户空间的进程已经结束了。所以必要的话还是确保你检查过释放掉资源了。这就是 <code>minimal</code> 应用的全部的内容了,使用了 BPF skeleton 后,这一切都是很直截了当的。</p>\n<h2>Makefile</h2>\n<p>既然我们已经浏览过了 <code>minimal</code> 应用,我们已经有足够的知识来看看 <a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/src/Makefile\">Makefile</a> 到底干了些什么。我们将跳过样板部分,关注核心的部分。</p>\n<pre><code class=\"language-makefile\">INCLUDES := -I$(OUTPUT)\nCFLAGS := -g -Wall\nARCH := $(shell uname -m | sed 's/x86_64/x86/')\n</code></pre>\n<p>这里我们定义一些在编译时使用的额外的参数。默认情况下,所有的中间文件都会写入到 <code>src/.output/</code> 子文件夹下。所以这个文件夹会被添加到 C 编译器的包含路径中,以便找到 BPF skeletons 和 libbpf 头文件。所有的用户空间文件在编译时都会带有调试信息(即 <code>-g</code> 选项),并且不会有任何的优化,来简化调试工作。 <code>ARCH</code> 参数捕获了宿主机的操作系统的架构,之后和定义在 libbpf 库中<code>bpf_tracing.h</code> 底层的 tracing helper 宏一起被传入到 BPF 代码编译步骤中。</p>\n<pre><code class=\"language-makefile\">APPS = minimal bootstrap\n</code></pre>\n<p>这里提供了目标的应用名称,添加到 <code>APPS</code> 变量中的会被编译。每个应用都定义了相关的 make 目标,所以你可以通过下面的命令构建对应的文件:</p>\n<pre><code class=\"language-bash\">$ make minimal\n</code></pre>\n<p>整个构建的过程分为下面的几步。首先,libbpf 以一个静态库的形式构建,它的 API 头文件之后被安装到了 <code>.output/</code> 中:</p>\n<pre><code class=\"language-makefile\"># Build libbpf\n$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf\n\t$(call msg,LIB,$@)\n\t$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1\t\t \\\n\t\t OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@)\t\t \\\n\t\t INCLUDEDIR= LIBDIR= UAPIDIR=\t\t\t \\\n\t\t install\n</code></pre>\n<p>如果你想要构建系统层面的共享库 <code>libbpf</code> ,你可以移除上面的步骤,然后对应地调整编译规则。</p>\n<p>下一步构建了 BPF C 代码,即 <code>*.bpf.c</code>,编译到了一个目标文件:</p>\n<pre><code class=\"language-makefile\"># Build BPF code\n$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) vmlinux.h | $(OUTPUT)\n\t$(call msg,BPF,$@)\n\t$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $(filter %.c,$^) -o $@\n\t$(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info\n</code></pre>\n<p>我们使用 Clang 来编译, <code>-g</code> 是必须的选项,来让 Clang 生成 BTF 相关的调试信息。 <code>-O2</code> 也是 BPF 编译中必要的, <code>-D__TARGET_ARCH_$(ARCH)</code> 为 <code>bpf_tracing.h</code> 定义了必要的宏来处理底层的 <code>struct pt_regs</code> 宏。你可以忽略它如果你不是在处理内核探测程序 kprobes 和 <code>struct pt_regs</code>。最后,我们从生成的 <code>.o</code> 文件中去除掉 DWARF 信息。因为它不会被用到,基本上都是 Clang 编译的副产物。</p>\n<blockquote>\n<p>BTF 是确保 BPF 正常工作的唯一的必要信息,因此会被保留下来。减小最终的 <code>.bpf.o</code> 文件是十分必要的,因为它将通过 BPF skeleton 被嵌入到最后的二进制应用中,所以要避免因为不必要的 DWARF 数据增加它的大小。</p>\n</blockquote>\n<p>既然我们已经生成了一个 <code>.bpf.o</code> 文件,<code>bpftool</code>可以用来生成一个对应的 BPF skeleton 头文件,即<code>.skel.h</code>,是通过<code>bpftool gen skeleton</code> 命令完成的:</p>\n<pre><code class=\"language-makefile\"># Generate BPF skeletons\n$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)\n\t$(call msg,GEN-SKEL,$@)\n\t$(Q)$(BPFTOOL) gen skeleton $< > $@\n</code></pre>\n<p>通过这种方式,我们确保了无论何时更新 BPF skeleton,用户空间的的应用也会被更新。因为他们需要在编译时将 BPF skeleton 嵌入进去。用户空间的 <code>.c</code> → <code>.o</code> 编译则是相当直接的:</p>\n<pre><code class=\"language-makefile\"># Build user-space code\n$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h\n\n$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)\n\t$(call msg,CC,$@)\n\t$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@\n</code></pre>\n<p>最后,只使用用户空间的 <code>.o</code> 文件,以及 <code>libbpf.a</code> 静态库,就生成了最终的二进制文件。<code>-lelf</code> 和 <code>-lz</code> 是 libbpf 的依赖,需要显式地提供给编译器:</p>\n<pre><code class=\"language-makefile\"># Build application binary\n$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)\n\t$(call msg,BINARY,$@)\n\t$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@\n</code></pre>\n<p>也就是说,在运行上面几个步骤后,你将会得到一个很小的用户空间的二进制文件。通过 BPF skeleton,编译出的 BPF 代码被嵌入到了这个二进制文件中,静态链接了 libbpf。所以它不再依赖于系统侧全局的 <code>libbpf</code>。这个二进制文件仅有 200KB,运行起来很快、可以独立执行,正如 <a href=\"http://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html\">Brendan Gregg 所述</a>。</p>\n<h2>Bootstrap app</h2>\n<p>我们已经介绍了 <code>minimal</code> 应用是什么样的,以及是如何编译的,下面我们就看看 <code>bootstrap</code> 中显示出的别的 BPF 特性。 <code>bootstrap</code> 是我之前写到的,是一个适用于生产环境下的 BPF 应用、它依赖于 BPF CO-RE (read why <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/\">here</a>) 特性,需要 Linux 内核在编译时选择 <code>CONFIG_DEBUG_INFO_BTF=y</code> (see <a href=\"https://github.com/libbpf/libbpf#bpf-co-re-compile-once--run-everywhere\">here</a>)。</p>\n<p><code>bootstrap</code> 追踪的是 <code>exec()</code> 系统调用,使用的是 <code>SEC("tp/sched/sched_process_exec") handle_exit</code> BPF 程序,大致上和进程的创建有关(这里忽略掉 <code>fork()</code>)。另外,它追踪了 <code>exit()</code> 调用,这个是用的 <code>SEC("tp/sched/sched_process_exit") handle_exit</code> BPF 程序,来监控每个进程是何时结束的。这两个 BPF 程序,共同协作,允许捕获到每个新建进程的信息,例如二进制文件名,每个进程的生命周期,收集进程消亡时的数据信息,如 exit code 或消耗的资源等。如果你想要看看内核到底在干嘛,它会是一个很好的开始。</p>\n<p><code>bootstrap</code> 也用了libc 的部分 <a href=\"https://www.gnu.org/software/libc/manual/html_node/Argp.html\">argp API</a> 来解析命令行参数,可以参考 <a href=\"http://download.savannah.nongnu.org/releases-noredirect/argpbook/step-by-step-into-argp.pdf\">"Step-by-Step into Argp" tutorial</a> 来了解这个 库是咋用的。用它我们提供了一些选项给程序,比如可以解析生命周期时长参数,即下面的<code>min_duration_ns</code> 只读变量。使用命令 <code>sudo ./bootstrap -d 100</code> 来显示最近 100 ms 存活的进程。详细的模式可以用 <code>sudo ./bootstrap -v</code>,激活 <code>libbpf</code> 调试信息。</p>\n<h3>Includes: vmlinux.h, libbpf and app headers</h3>\n<p>下面是 <a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/src/bootstrap.bpf.c\">bootstrap.bpf.c</a> 导入的头文件:</p>\n<pre><code class=\"language-c\">#include "vmlinux.h"\n#include <bpf/bpf_helpers.h>\n#include <bpf/bpf_tracing.h>\n#include <bpf/bpf_core_read.h>\n#include "bootstrap.h"\n</code></pre>\n<p>和 <code>minimal.bpf.c</code> 不同的是,我们使用了 <code>vmlinux.h</code> 头文件,在一个文件中包含了内核中所有的类型。他是 libbpf-bootstrap 项目里 <a href=\"https://raw.githubusercontent.com/libbpf/libbpf-bootstrap/master/src/vmlinux_508.h\">预先生成的</a> ,但是开发者也可以自己使用 <code>bpftool</code> 来生成,具体可以参考 <a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/tools/gen_vmlinux_h.sh\">gen_vmlinux_h.sh</a>。</p>\n<blockquote>\n<p><code>vmlinux.h</code> 中所有的类型都会携带着额外的标签 <code>__attribute__((preserve_access_index))</code>,它会让 Clang 生成具有 <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/#reading-kernel-structure-s-fields\">BPF CO-RE relocations</a>,的程序,允许 libbpf 将你的 BPF 代码放到宿主机内核内存的特定位置,即使它和脚手架项目最初生成的那个 <code>vmlinux.h</code> 不同。这是构建可迁移的预编译出的 BPF 应用很关键的一步,从而不需要将整个 Clang/LLVM 工具链部署到目标系统上。与之相对的是 BCC 的方法,在运行时编译 BPF 代码,有很多<a href=\"https://nakryiko.com/posts/bcc-to-libbpf-howto-guide/#why-libbpf-and-bpf-co-re\">弊端</a>。</p>\n</blockquote>\n<p>:::tip</p>\n<p><code>vmlinux.h</code> 不能和其他系统侧的内核头文件结合,显然,若是那么干了,你将会碰到重复定义的问题。所以只使用 libbpf 提供的 <code>vmlinux.h</code> 头文件就好了。</p>\n<p>:::</p>\n<p>除了 <code>bpf_helpers.h</code>,我们也使用了一些其他的 libbpf 提供的头文件,如<code>bpf_tracing.h</code> 和 <code>bpf_core_read.h</code>,提供了一些额外的宏来写具有 CO-RE 特性的 BPF 应用。最后,<code>bootstrap.h</code> 包含了通用的类型定义,在 BPF 和用户空间的代码之间共享。</p>\n<h3>BPF maps</h3>\n<blockquote>\n<p><code>bootstrap</code> 展示了 BPF maps 的使用方法,它是 BPF 中的抽象数据结构。许多不同的数据结构都可以被建模为 BPF maps:例如数组、哈希表、per-socket 和 per-task 的本地存储、BPF perf 和 ring buffers,甚至是其他奇特的用法。重要的是大多数 BPF maps 允许执行差序、更新、按照键删除元素等方法。一些 BPF maps 允许额外的操作,比如 <a href=\"https://nakryiko.com/posts/bpf-ringbuf/\">BPF ring buffer</a>,允许数据入队,但是用于都不从 BPF 侧删除它。BPF maps 是用来在 BPF 程序和用户空间之间共享状态的。另一个起到这种作用的是 BPF 全局变量,它在底层也是用 BPF maps 实现的。</p>\n</blockquote>\n<p>在 <code>bootstrap</code> 中,我们定义了名叫 BPF map 的 <code>exec_start</code> 的 <code>BPF_MAP_TYPE_HASH</code> 类型的哈希表。它最大容纳 8192 个元素,键是 <code>pid_t</code> 类型的,值是一个 64 位的无符号整型,存储了进程运行事件的纳秒粒度的时间戳。这就是所谓的 BTF-defined map,<code>SEC(".maps")</code> 标注是必要的,让 libbpf 知晓它需要在内核中创建对应的 BPF map,在 BPF 代码中:</p>\n<pre><code class=\"language-c\">struct {\n\t__uint(type, BPF_MAP_TYPE_HASH);\n\t__uint(max_entries, 8192);\n\t__type(key, pid_t);\n\t__type(value, u64);\n} exec_start SEC(".maps");\n</code></pre>\n<p>在这样一个哈希表中添加、更新元素是很简单的:</p>\n<pre><code class=\"language-c\">\tpid_t pid;\n\tu64 ts;\n\n\t/* remember time exec() was executed for this PID */\n\tpid = bpf_get_current_pid_tgid() >> 32;\n\tts = bpf_ktime_get_ns();\n\tbpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);\n</code></pre>\n<p><code>bpf_map_update_elem()</code> BPF helper 接收 map 它自己的指针、键和值的指针,在这个例子中 <code>BPF_ANY</code> 表示的是或者添加一个新的键或者更新已有的键值对。</p>\n<p>注意第二个 BPF 程序(<code>handle_exit</code>)从同一个 BPF map 中查询元素,之后删除它。它展示了 <code>exec_start</code> map 是在两个 BPF 程序之间共享的:</p>\n<pre><code class=\"language-c\">\tpid_t pid;\n\tu64 *start_ts;\n\t...\n\tstart_ts = bpf_map_lookup_elem(&exec_start, &pid);\n\tif (start_ts)\n\t\tduration_ns = bpf_ktime_get_ns() - *start_ts;\n\t...\n\tbpf_map_delete_elem(&exec_start, &pid);\n</code></pre>\n<h4>Read-only BPF configuration variables</h4>\n<p><code>bootstrap</code> 和 <code>minimal</code> 不同,使用的是只读的全局变量:</p>\n<pre><code class=\"language-c\">const volatile unsigned long long min_duration_ns = 0;\n</code></pre>\n<p><code>const volatile</code> 是重要的,它为 BPF 代码和用户空间的代码标记了只读变量。它具体定义了 <code>min_duration_ns</code> 的值,同时在 BPF 程序的验证期间,BPF verifier 是知晓它的。这就允许 BPF verifier 优化无效的代码,也就是这样只读的变量限制下访问不到的代码路径,即减少了不可到达的分支逻辑。这个特性在一些更加高级的用例里是很受欢迎的,例如可移植性的检查和其他配置项。</p>\n<blockquote>\n<p><code>volatile</code> 是让 Clang 不去优化掉该变量、忽略掉用户空间所提供的值所必要的。否则,Clang 可以自由地移除掉该变量,这不是我们想要的结果。</p>\n</blockquote>\n<p>在用户侧代码 <a href=\"https://github.com/libbpf/libbpf-bootstrap/blob/master/src/bootstrap.c\">bootstrap.c</a> 中,初始化自由的只读全局变量是有一点点不太一样的。他们需要在 BPF skeleton 被加载到内核前就设置好。所以,不能直接使用一个单步的 <code>bootstrap_bpf__open_and_load()</code>。我们需要先使用 <code>bootstrap_bpf__open()</code> 来创建 skeleton,然后设置只读变量值,再调用 <code>bootstrap_bpf__load()</code> 把 skeleton 加载到内核里:</p>\n<pre><code class=\"language-c\">\t/* Load and verify BPF application */\n\tskel = bootstrap_bpf__open();\n\tif (!skel) {\n\t\tfprintf(stderr, "Failed to open and load BPF skeleton\\n");\n\t\treturn 1;\n\t}\n\n\t/* Parameterize BPF code with minimum duration parameter */\n\tskel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;\n\n\t/* Load & verify BPF programs */\n\terr = bootstrap_bpf__load(skel);\n\tif (err) {\n\t\tfprintf(stderr, "Failed to load and verify BPF skeleton\\n");\n\t\tgoto cleanup;\n\t}\n</code></pre>\n<p>注意只读变量是 skeleton 中 <code>rodata</code> 段的一部分,不是 <code>data</code> 或 <code>bss</code> 段,所以是这么取它的: <code>skel->rodata->min_duration_ns</code>。在 BPF skeleton 被加载后,用户空间的的代码只能读取只读变量的值。BPF 代码也只能阅读这些变量。 一旦检测到写只读变量的操作,BPF verifier 将会拒绝 BPF 程序。</p>\n<h3>BPF ring buffer</h3>\n<p><code>bootstrap</code> 大量地使用了 BPF ring buffer map 来准备和发送数据到用户空间。它使用了 <code>bpf_ringbuf_reserve()</code>/<code>bpf_ringbuf_submit()</code> <a href=\"https://nakryiko.com/posts/bpf-ringbuf/#bpf-ringbuf-reserve-commit-api\">combo</a> 以获得最佳的可用性和性能,可以阅读 <a href=\"https://nakryiko.com/posts/bpf-ringbuf/\">BPF ring buffer 相关博客</a> 来深入理解。那篇文章深入探究了相似的内容,解读了另一个独立的分支 <a href=\"https://github.com/libbpf/bpf-ringbuf-examples/\">bpf-ringbuf-examples</a> 中的例子。它会给你一个很好的例子,帮助你了解如何使用 BPF perf buffer。</p>\n<h3>BPF CO-RE</h3>\n<p>BPF CO-RE (Compile Once – Run Everywhere) 是一个很大的话题, <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/\">另有一篇博客</a> 详细描述了它,可以参阅它去理解。这里有一个来自 <code>bootstrap.bpf.c</code> 中的例子,利用了 BPF CO-RE 特性来从内核的结构体 <code>struct task_struct</code> 中读取数据:</p>\n<pre><code class=\"language-c\">\te->ppid = BPF_CORE_READ(task, real_parent, tgid);\n</code></pre>\n<p>在非 BPF 的世界中,可以很简单地写作 <code>e->ppid = task->real_parent->tgid;</code>,但是 BPF verifier 需要付出额外的努力,因为任意地去内核内存是存在风险的。 <code>BPF_CORE_READ()</code> 就用了一个简洁的方式处理这个问题,它在读取指针对应位置的过程中,记录了 BPF CO-RE 重定位带来的地址偏移,允许 libbpf 将所有字段偏移量调整到宿主机器内核的特定内存布局上。可以参考 <a href=\"https://nakryiko.com/posts/bpf-portability-and-co-re/#reading-kernel-structure-s-fields\">这篇博客</a> 来深入了解。</p>\n<h2>Conclusion</h2>\n<p>这篇文章大概囊括了 <code>libbpf-bootstrap</code> 和 BPF/libbpf 的方方面面。希望 <code>libbpf-bootstrap</code> 让你度过 BPF 开发的起步阶段,避免配置环境的痛苦,让你的时间更多地用在 BPF 本身上。对于更有经验的 BPF 开发者,这篇文章应该已经揭示了 BPF 在可用性方面的提升,如 BPF skeleton、BPF ringbuf、BPF CO-RE,以防你没有紧密地追踪 BPF 的最新进展。</p>\n<h2>补充 BPF Map 相关内容</h2>\n<p>该部分内容来自本博客之前引用过的一篇文章,<a href=\"https://blog.csdn.net/sinat_38816924/article/details/115607570\">原文链接</a> 。作者是阅读了 <a href=\"https://www.oreilly.com/library/view/linux-observability-with/9781492050193/\">Linux Observability with BPF</a> 这本书做的笔记,这本书的电子版在 <a href=\"https://z-lib.org/\">Z-Library</a> 上能找到。</p>\n<p>消息传递来唤醒程序的行为,在软件工程中很常见。一个程序可以通过发送消息来修改另一个程序的行为;这也允许这些程序之间交换信息。关于 BPF 最吸引人的一个方面是,运行在内核上的代码和加载所述代码的用户空间程序可以在运行时使用消息传递相互通信。BPF maps 用来实现此功能。BPF maps 是驻留在内核中的键/值存储。任何知道它们的 BPF 程序都可以访问它们。在用户空间中运行的程序也可以使用文件描述符访问这些映射。只要事先正确指定数据大小,就可以在 maps 中存储任何类型的数据。</p>\n<h3>使用BPF系统调用操作 BPF maps</h3>\n<p>bpf 系统调用的原型如下:</p>\n<pre><code class=\"language-c\">#include <linux/bpf.h>\nint bpf(int cmd, union bpf_attr *attr, unsigned int size);\n</code></pre>\n<p>例如创建一个 hash-table map。其中key和value都是无符号整形。</p>\n<pre><code class=\"language-c\">union bpf_attr my_map {\n .map_type = BPF_MAP_TYPE_HASH,\n .key_size = sizeof(int),\n .value_size = sizeof(int),\n .max_entries = 100,\n .map_flags = BPF_F_NO_PREALLOC,\n};\nint fd = bpf(BPF_MAP_CREATE, &my_map, sizeof(my_map));\n</code></pre>\n<h3>使用 BPF helper 创建BPF maps</h3>\n<p>helper函数bpf_map_create包装了刚才看到的代码,以便更容易根据需要初始化映射。我们可以使用它创建上一个map,只需一行代码:</p>\n<pre><code class=\"language-c\">int fd;\nfd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100,BPF_F_NO_PREALOC);\n</code></pre>\n<p>如果是将要加载到内核的代码,也可以如下这样创建map。创建原理是:<code>bpf_load.c</code> 扫描目标文件时候,解析到 maps section,会通过 bpf syscall 创建 maps。</p>\n<pre><code class=\"language-c\">struct bpf_map_def SEC("maps") my_map = {\n .type = BPF_MAP_TYPE_HASH,\n .key_size = sizeof(int),\n .value_size = sizeof(int),\n .max_entries = 100,\n .map_flags = BPF_F_NO_PREALLOC,\n};\n</code></pre>\n<p>用户空间的程序,调用 <code>load_bpf_file</code> 函数,将 <code>bpf</code> 程序加载的内核。<code>load_bpf_file</code> 会扫描 bpf 程序(elf 格式)的各个 section。对于名为 maps 的 section,<code>load_bpf_file</code> 会从中提取出maps的信息,并调用 <code>syscall(__NR_bpf, 0, attr, size);</code> 系统调用,创建map。</p>\n<h3>Working with BFP Maps</h3>\n<p>内核和用户空间之间的通信将是您编写的每个BPF程序的一个基本部分。给内核编写代码时访问 map 的 api 与给用户空间程序编写代码不同。对于 <code>bpf_map_update_elem</code> 这个程序:运行在内核的代码从 <code>bpf_helpers.h</code> 加载;运行在用户空间的代码从<code>tools/lib/bpf/bpf.h</code> 加载;这样区分的原因是,内核空间可以直接访问 maps;而用户空间访问 maps 需要通过文件描述符。在内核上运行,可以在原子方式更新元素。在用户空间运行的代码,内核需要复制值以用于更新 map。这个非原子操作,可能失败。如果失败,失败原因填充到全局变量 errno 中。</p>\n<p>对于5.4内核源码 bpf_helpers.h 的位置如下:</p>\n<pre><code class=\"language-bash\">find . -name "bpf_helpers.h"\n# tools/testing/selftests/bpf/bpf_helpers.h\n</code></pre>\n<h3>更新元素</h3>\n<p>我们先看从内核中更新map的函数。</p>\n<pre><code class=\"language-c\">// tools/testing/selftests/bpf/bpf_helpers.h\nstatic int (*bpf_map_update_elem)(void *map, const void *key, const void *value,\n\t\t\t\t unsigned long long flags) =\n\t(void *) BPF_FUNC_map_update_elem;\n// #define BPF_FUNC_map_update_elem 2\n</code></pre>\n<p>内核中出现这些奇奇怪怪的数字很正常。我暂时不知道这个2是什么鬼。</p>\n<p>内核中的 bpf_map_update_elem 函数有四个参数。第一个是指向我们已经定义的 map 的指针。第二个是指向要更新的键的指针。因为内核不知道我们要更新的键的类型,所以这个方法被定义为指向 void 的不透明指针,这意味着我们可以传递任何数据。第三个参数是我们要插入的值。此参数使用与键参数相同的语义。我们在本书中展示了一些如何利用不透明指针的高级示例。您可以使用此函数中的第四个参数来更改map的更新方式。此参数可以采用三个值:</p>\n<p>如果传递0,则告诉内核如果元素存在,则要更新该元素;如果元素不存在,则要在映射中创建该元素。[0 可以用 BPF_ANY 宏表示]<br>\n如果传递1,则告诉内核仅在元素不存在时创建该元素。[1 可以用 BPF_NOEXIST 宏表示]<br>\n如果传递2,内核将只在元素存在时更新它。[2 可以用 BPF_EXIST 宏表示]</p>\n<p>也可以从用户空间程序中更新 map。执行此操作的帮助程序与我们刚才看到的类似;唯一的区别是,它们使用文件描述符访问 map,而不是直接使用指向 map 的指针。正如您所记得的,用户空间程序总是使用文件描述符访问 map。</p>\n<pre><code class=\"language-c\">// tools/lib/bpf/bpf.h\n#ifndef LIBBPF_API\n#define LIBBPF_API __attribute__((visibility("default")))\n#endif\nLIBBPF_API int bpf_map_update_elem(int fd, const void *key, const void *value,\n\t\t\t\t __u64 flags);\n</code></pre>\n<p>这里的fd获取方式有两种。第一中,是使用 bpf_create_map 函数返回的 fd。也可以通过全局变量 map_fd 访问。</p>\n<h3>读取元素</h3>\n<p><code>bpf_map_lookup_elem</code>:从 map 中读取内容。同样,也分为内核空间和用户空间两种形式。</p>\n<pre><code class=\"language-c\">// 内核空间\n// tools/testing/selftests/bpf/bpf_helpers.h\nstatic void *(*bpf_map_lookup_elem)(void *map, const void *key) =\n\t(void *) BPF_FUNC_map_lookup_elem;\n//#define BPF_FUNC_map_lookup_elem 1\n</code></pre>\n<pre><code class=\"language-c\">// 用户空间\n// tools/lib/bpf/bpf.h\n#ifndef LIBBPF_API\n#define LIBBPF_API __attribute__((visibility("default")))\n#endif\nLIBBPF_API int bpf_map_lookup_elem(int fd, const void *key, void *value);\n</code></pre>\n<p>它们的第一个参数也有所不同;内核方法引用映射,而用户空间帮助程序将映射的文件描述符标识符作为其第一个参数。第三个参数是指向代码中要存储从映射中读取的值的变量的指针。</p>\n<h3>删除元素</h3>\n<p>同样有两种:运行在用户空间,运行在内核空间。如果删除的 key 不存在,返回一个负数;error 被设置成 ENOENT。</p>\n<pre><code class=\"language-c\">static int (*bpf_map_delete_elem)(void *map, const void *key) =\n\t(void *) BPF_FUNC_map_delete_elem;\n</code></pre>\n<pre><code class=\"language-c\">LIBBPF_API int bpf_map_delete_elem(int fd, const void *key);\n</code></pre>\n<h3>迭代遍历元素</h3>\n<p>bpf_map_get_next_key,此指令仅适用于在用户空间上运行的程序。</p>\n<pre><code class=\"language-c\">LIBBPF_API int bpf_map_get_next_key(int fd, const void *key, void *next_key);\n</code></pre>\n<p>第一个参数:map 的文件描述符。第二个参数:lookup_key,你希望查找的属性值对应的 key。第三个参数:next_key,map 中的 next key。</p>\n<p>当您调用这个帮助程序时,BPF 会尝试在这个 map 中找到作为查找键传递的键的元素;然后,它会用映射中相邻的键设置下一个next_key 参数。因此,如果您想知道哪个键在键 1 之后,您需要将 1 设置为 lookup_key,BPF 会将与之相邻的 key 设置为下一个next_key 参数的值。</p>\n<p>如果要打印映射中的所有值,可以使用 bpf_map_get_next_key 键和映射中不存在的查找键。这将强制 BPF 从地图的开头开始。</p>\n<p>当 bpf_map_get_next_key 到达 map 的末尾时候,返回一个负数,errno 值被设置成 ENOENT。</p>\n<p>您可以想象,bpf_map_get_next_key 可以从地图中的任何一点开始查找 key;如果您只希望另一个特定 key 的下一个 key,则不需要从map 的开头开始。</p>\n<p>另外,我们还需要知道 bpf_map_get_next_key 的另一个行为。许多编程语言会在迭代遍历之前,复制 map。因为遍历的时候,如果有代码删除将要遍历的元素,将会很危险。bpf_map_get_next_key 遍历的时候,没有复制 map。如果遍历的时候,map 中存在元素被删除,bpf_map_get_next_key 会自动跳过它。</p>\n<h3>查找删除元素</h3>\n<p>bpf_map_lookup_and_delete_elem:一个元素通过 key 进行搜索。搜索到之后,删除这个元素,同时将元素的值放在 value 中。这个也是仅仅适用于用户空间。</p>\n<pre><code class=\"language-c\">LIBBPF_API int bpf_map_lookup_and_delete_elem(int fd, const void *key,void *value);\n</code></pre>\n<h3>并发访问 map</h3>\n<p>使用 BPF 映射的挑战之一是许多程序可以同时访问相同的映射。这会在我们的 BPF 程序中引入竞争条件。为了防止竞争情况,BPF 引入了 BPF 自旋锁的概念,它允许您在对 map 元素进行操作时锁定对 map 元素的访问。自旋锁仅适用于 array、hash 和 cgroup 存 maps。</p>\n<pre><code class=\"language-c\">// 信号量\n// /usr/include/linux\nstruct bpf_spin_lock {\n\t__u32\tval;\n};\n\n// 内核\n// 加锁+解锁\n// tools/testing/selftests/bpf/bpf_helpers.h\nstatic void (*bpf_spin_lock)(struct bpf_spin_lock *lock) =\n\t(void *) BPF_FUNC_spin_lock;\nstatic void (*bpf_spin_unlock)(struct bpf_spin_lock *lock) =\n\t(void *) BPF_FUNC_spin_unlock;\n</code></pre>\n<p>我这里复制下书上的事例。这个访问控制,精度比较细。对每一个元素使用了自旋锁。另外这个 map 必须用 BPF 类型格式(BPF Type Format, BTF)注释,这样 verifier 就知道如何解释这个结构。类型格式通过向二进制对象添加调试信息,使内核和其他工具对BPF数据结构有了更丰富的理解。</p>\n<pre><code class=\"language-c\">struct concurrent_element {\n struct bpf_spin_lock semaphore;\n int count;\n}\n\nstruct bpf_map_def SEC("maps") concurrent_map = {\n .type = BPF_MAP_TYPE_HASH,\n .key_size = sizeof(int),\n .value_size = sizeof(struct concurrent_element),\n .max_entries = 100,\n};\n\nBPF_ANNOTATE_KV_PAIR(concurrent_map, int, struct concurrent_element);\n\nint bpf_program(struct pt_regs *ctx) {\n\tint key = 0;\n struct concurrent_element init_value = {};\n struct concurrent_element *read_value;\n bpf_map_create_elem(&concurrent_map, &key, &init_value, BPF_NOEXIST);\n read_value = bpf_map_lookup_elem(&concurrent_map, &key);\n bpf_spin_lock(&read_value->semaphore);\n read_value->count += 100;\n bpf_spin_unlock(&read_value->semaphore);\n}\n</code></pre>\n<p>用户空间更改 map 的话,使用 <code>bpf_map_update_elem</code> 和 <code>bpf_map_lookup_elem_flags</code> 的时候,添加 <code>BPF_F_LOCK</code> flags。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210627/",
"title": "使用 libbpf-bootstrap 构建 BPF 程序",
"summary": "libbpf介绍,相关博客翻译笔记",
"date_modified": "2021-06-27T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>SJTU HPC 使用笔记</h1>\n<p><a href=\"https://docs.hpc.sjtu.edu.cn/\">上海交大超算平台用户手册 文档 (sjtu.edu.cn)</a></p>\n<p>登录节点 <code>ssh user@login.hpc.sjtu.edu.cn</code></p>\n<p>可视化平台 <a href=\"https://studio.hpc.sjtu.edu.cn/\">https://studio.hpc.sjtu.edu.cn/</a></p>\n<p>安装包、编译最好提前申请计算资源,在登陆节点运行计算密集的作业,将会被程序自动查杀,您的账号会被加入到黑名单,并在30-120 分钟内无法登陆,申请节点方法如下</p>\n<pre><code class=\"language-bash\">srun -p small -n 1 --pty /bin/bash\n</code></pre>\n<p>数据传输用 scp 传给 data 节点:<strong><a href=\"http://data.hpc.sjtu.edu.cn\">data.hpc.sjtu.edu.cn</a></strong></p>\n<p>远程桌面会计费</p>\n<p>默认单作业最长运行 7 天</p>\n<p>不提供商业软件,开源软件需要查看列表,没有的写邮件申请</p>\n<p>moudle av 查看有啥能加载的模块, moudle list 查看加载了什么模块</p>\n<p>交互命令行下,记得设置发送心跳包,XShell 是在会话属性—连接——保持活动状态中</p>\n<p>没法用任何涉及 sudo 的命令,但是好像是不让自己安装软件的(把软件安装到用户空间……逃</p>\n<p>vscode 的 remote explorer 里创建 ssh target 连过去还是很爽的</p>\n<p>HPC 提供的是 32 GB 的 V100,既然是按时计费没用满血亏</p>\n<h2>使用方法</h2>\n<p>使用前请阅读文档!不要在登录节点进行大型任务以防影像别人的正常登录。有两种提交任务的方法:</p>\n<ol>\n<li>\n<p>使用hpc教程中推荐的任务脚本提交方式。好处是可以在hpc占用高时,由系统调度任务,不必等待分配,坏处是在不确定程序正确性时可能空跑很久浪费资金,且查看实验输出时不方便</p>\n</li>\n<li>\n<p>请求节点开启bash进行交互,例如,可以使用以下命令申请一个带单个GPU,六个CPU的节点:<br>\nsrun -n 1 -p dgx2 --gres=gpu:1 --cpus-per-task=6 --pty /bin/bash<br>\n等待分配好节点,配合 tmux/screen 使用</p>\n</li>\n</ol>\n<h2>邮件提醒</h2>\n<p>邮件提醒的 slurm 示例脚本,提醒事件的可选项有 ALL, BEGIN, END, FAIL</p>\n<pre><code class=\"language-bash\">#!/bin/bash\n\n#SBATCH --job-name=test\n#SBATCH --partition=small\n#SBATCH -n 20\n#SBATCH --ntasks-per-node=20\n#SBATCH --output=%j.out\n#SBATCH --error=%j.err\n#SBATCH --mail-type=end # 作业结束时,邮件提醒\n#SBATCH --mail-user=XX@sjtu.edu.cn\n</code></pre>\n<h2>Pytorch 使用示例</h2>\n<p><code>module load miniconda3</code> 加载 miniconda3 模块</p>\n<p>在 DGX-2 上使用 pytorch。作业使用单节点,分配 2 块 GPU,GPU:CPU 配比 1:6。脚本名称可设为 slurm.test</p>\n<pre><code class=\"language-bash\">#!/bin/bash\n#SBATCH -J test\n#SBATCH -p dgx2\n#SBATCH -o %j.out\n#SBATCH -e %j.err\n#SBATCH -N 1\n#SBATCH --ntasks-per-node=1\n#SBATCH --cpus-per-task=12\n#SBATCH --gres=gpu:2\n\n# module load cuda # 在其他 partition 上不主动加载不行,可能 DGX-2 上默认加载了,不过可能其他分区也不应该使用显卡资源\n# 因为在装包的时候想看一下 cuda 版本,交互是用的 small 分区,不加载 cuda module 找不到 nvcc 应用\nmodule load miniconda3\nsource activate pytorch-env\n\npython -c 'import torch; print(torch.__version__); print(torch.zeros(10,10).cuda().shape)'\n</code></pre>\n<p>其实就是把想要执行的任务写到脚本里,不想用脚本的话,看下面的笔记,tmux维持着交互式的窗口就行了</p>\n<p>conda init 过以后,<strong>切环境记得先 deactivate,再 activate 目标环境……(为什么不自动 deactivate 掉前一个环境呢???</strong></p>\n<p>使用以下指令提交作业</p>\n<pre><code class=\"language-bash\">$ sbatch slurm.test\n</code></pre>\n<h2>关于计费</h2>\n<p><a href=\"https://studio.hpc.sjtu.edu.cn/pun/sys/activejobs\">Active Jobs - HPC Studio (sjtu.edu.cn)</a></p>\n<p><a href=\"https://account.hpc.sjtu.edu.cn/#/login\">HPC与AI平台 (sjtu.edu.cn)</a></p>\n<p>在 Active jobs 里可以看到自己的使用的计算资源,登录节点和数据节点是不计费的</p>\n<p>关闭 ssh 窗口后会话终止,会自动停止掉计费,使用类似 screen、tmux 的工具的话自然会接着计费</p>\n<h2>Tmux</h2>\n<p>服务器装的是 tmux,和之前用过的 screen 类似,可以在 ssh 会话结束后保持会话期间的命令正常运行</p>\n<p>tmux 按 <code>ctrl+b</code> 后可以输入命令选项(类似 vim 的命令模式),比如<code>%</code>是左右分屏,<code>"</code>是上下分屏,<code>d</code> 是退出当前 session,<code>x</code> 是关掉当前session</p>\n<p><strong>注意</strong>:在 login 节点创建的 tmux session 在退出后才会继续运行,如果是用计算节点创建的 tmux session,关掉本地命令行,远端计算资源和所有 session 也会随之释放。所以正确的使用姿势(不想用 slurm 脚本的话)是登录到 login 节点,创建 tmux session,进session 后再申请计算资源。但是此时如果用 tmux 分屏,默认还是 login 节点,所以想分屏后命令行仍然是计算节点的话,需要在计算节点的 session 下再次创建 session,但是 tmux 不推荐这么搞,会提示使用特殊的方式启动……</p>\n<p>如果用域名登陆服务器,每次登陆可能会给解析到不同节点,用ip登吧</p>\n<h2>常用命令</h2>\n<p>资源监视:<code>nvidia-smi</code> 查看显卡相关信息,<code>top</code> 查看 cpu、内存等资源,<code>df</code> 查看磁盘容量;可以配合 <code>watch</code> 命令使用,比如每十秒打印一次显卡情况 <code>watch -n 10 nvidia-smi</code></p>\n<p>有标准输出的程序,想在它执行中间干点别的事情。可以这样:或者直接把输出流重定向到文件里并且让它默认到后台去执行,比如<code>python main.py > log.txt &</code>。或者用<code>ctrl+z</code> 先把任务切到后台,然后再开别的任务,最后再用 <code>jobs</code> 配合 <code>fg</code> 命令切回来,比如 <code>jobs</code> 下看到它是 1 号,那<code>fg 1</code> 就又回来接着执行它了</p>\n<p>永远记住<code>man</code> 命令,想不起来 <code>man</code> 一下</p>\n<p>vi 和 vim 的一些区别,比如 vi 不允许在编辑模式下(按下 i 时)用方向键移动光标</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210506/",
"title": "SJTU HPC平台使用",
"summary": "使用心得、常用命令",
"date_modified": "2021-05-06T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>在 Typora 中自动上传图片到云端</h1>\n<p>最近才看到 <a href=\"https://support.typora.io/Upload-Image/\">Upload Images (typora.io)</a> 新出的功能,在 Windows 端 0.9.84 的发行版之后,可以在粘贴图片的时候自动上传到云端。</p>\n<p>之前在偏好设置中都是勾选的保存到本地,图床当然是好东西,但是不是自动上传的,导致最后部署博客的时候,这些资源也会被打包进去,博客越来越臃肿。</p>\n<p><img src=\"./old.png\" alt=\"\"></p>\n<p>目前 Typora 会支持一些第三方图床平台,在 Typora 里激活相关选项后,这些第三方平台会把你的图片上传到他们的服务器或者第四方的云存储平台 = =,所以 Typora 说了,自行关注许可条件、隐私政策、可靠性、稳定性等问题。具体而言,当前支持的有:</p>\n<ul>\n<li>iPic (macOS)</li>\n<li>uPic (macOS)</li>\n<li>PicGo.app (macOS / Windows / Linux, Simplified Chinese language only)</li>\n<li>PicGo (Command Line) (Windows / Linux)</li>\n<li>Custom (macOS / Windows / Linux)</li>\n</ul>\n<p>最后选用了 PicGo 搭配阿里云 OSS 的方式。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210430/",
"title": "在 Typora 中自动上传图片到云端",
"summary": "不用拿github当图床了(×",
"date_modified": "2021-04-30T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>文件系统学习笔记</h1>\n<h2>虚拟机共享文件夹下无法创建软链接</h2>\n<p>因为 wsl2 不支持 eBPF 的一些最新特性,用 virtual box 装了个虚拟机 (感谢 virtual 不仅长期免费还支持了 Hyper-V,开虚拟机关 wsl2,开 wsl2 关虚拟机太痛苦了)。碰到了一个奇怪的现象,libbpf 的测试样例,在 wsl2 里编译得了跑不了,在 virtual box 里的虚拟机上是跑得了编译不了……人傻了,一看编译工具的版本也都一样呀,见了鬼了。</p>\n<p>后来解压 github 下到的压缩文件后,才发现软连接全部失效。原来是因为我把文件都放到了 virtual box 里的虚拟机 Ubuntu 和宿主机 Windows 的共享文件夹下了。共享文件夹下会有权限问题,无法创建软链接。推测也可能是文件系统的原因所以禁止了?毕竟 windows 和 linux 创建软链接的方式不同。</p>\n<p>::: tip Note</p>\n<p>软链接是用 <code>ln -s</code> 创建的新的 iNode,可以当成指针去理解,指向链接的文件或目录;<code>ls</code> 后可以看到文件名带一个箭头指着被链接的文件;在 github上 也是用特殊图标标识的文件,打开文件内容只有一个指向的文件路径名,用神器 <a href=\"https://github.com/conwnet/github1s\">github1s</a> 打开也是这样,内容只有一个被指向的文件名;但是你在本地 VS Code 编辑器之类的方式查看,是会打开指向的文件的。硬链接只能链接文件,占用大量空间,共享 iNode,共享文件内容,实时同步更新内容。想查看 iNode,ls的时候加上 i 就行。</p>\n<p>:::</p>\n<h2>文件系统</h2>\n<p>::: tip Note</p>\n<p>HPFS 仅适用于 Windows NT 3.1,3.5 和 3.51。Windows NT 4.0 不支持也不能获取 HPFS 分区;同时,只有 Windows 98/95 和 Windows 2000 支持 FAT32 文件系统。</p>\n<p>:::</p>\n<h3>FAT 概述</h3>\n<p>FAT 是 Windows NT 上支持的最简单的文件系统。FAT 文件系统使用 file allocation table (FAT) 来组织,这个表在卷的最上端。为了保护这些卷,FAT 需要被拷贝两份以防一份损坏,根目录下的FAT 表必须存储在固定的区域,以便系统启动文件能够正确地定位它们。</p>\n<p>使用 FAT 的磁盘是按簇(cluster)分划的,带下取决于卷(volume)的大小。簇的数量是 16 位的,同时必须是 2 的指数。当一个文件创建的时候,也就在目录下创建了一个条目,相应的包含了数据的第一个簇号也就创建了。FAT 表中的条目或者表示这是文件的最后一个簇,或者指向下一个簇,就像链表一样。</p>\n<p>FAT 表的更新是很重要的,但它也是很耗时的。如果 FAT 表不经常更新,会导致文件的缺失。但是它又是十分耗时的,因为在更新的时候,硬盘的磁头必须重新放置到硬盘的逻辑 0 位置,毕竟 FAT 表是放置在这里的(FAT 应用的时候还只有机械硬盘?)。</p>\n<p>FAT 文件结构是没有组织的,只需要提供文件第一次打开的位置就可以取得它。另外,FAT 支持只读、隐藏、系统文件和打包文件这几种标签。</p>\n<p>下面这个表展示了 FAT 文件系统是如何组织一个卷的:</p>\n<p><img src=\"./recover-FAT-volume-structure.gif\" alt=\"\"></p>\n<h4>FAT 命名规则</h4>\n<p>FAT 使用传统的文件命名规则,所有的文件名必须是 ACII 字符。文件或目录的名字只能是八个字母长的,只有一个<code>.</code>,最多有三个字符的拓展名。文件名必须是字母或数字开头的。文件名不能包含下面的这些字符:</p>\n<pre><code>. " / \\ [ ] : ; | = ,\n</code></pre>\n<p>如果这些字符被用到的了,可能会发生不可预料的错误。名字也不能有空格。</p>\n<p>下列名称是保留词:</p>\n<p>CON, AUX, COM1, COM2, COM3, COM4, LPT1, LPT2, LPT3, PRN, NUL</p>\n<h4>FAT 的优点</h4>\n<p>在 Windows NT 支持的各种文件系统上都不可能撤销删除操作。撤销删除功能尝试直接获取硬件,在 Windows NT 下是不可能做到的。然而,如果文件是存放在一个 FAT 分区,并且系统是在 MS-DOS 下启动的,文件可以被恢复。</p>\n<p>FAT 文件系统在分区为 200 MB 的情况下表现最好,因为 FAT 的开销很小。</p>\n<h4>FAT 的缺点</h4>\n<p>当使用的硬盘或分区超过了 200 MB,那么不应该使用 FAT了。因为卷的大小增加了,FAT 的性能表现会显著下降。</p>\n<p>也不能为 FAT 下的文件设置权限。</p>\n<p>FAT 的分区大小限制是 4GB(在 Windows NT 下)或 2GB(在 MS-DOS 下)。</p>\n<h4>FAT32, FAT16, FAT12</h4>\n<p>下标展示了 FAT 系统的区别</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:center\">文件系统</th>\n<th style=\"text-align:center\">FAT 表中每个簇的字节数</th>\n<th>簇的限制</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:center\">FAT12</td>\n<td style=\"text-align:center\">1.5</td>\n<td>Fewer than 4087 clusters.</td>\n</tr>\n<tr>\n<td style=\"text-align:center\">FAT16</td>\n<td style=\"text-align:center\">2</td>\n<td>Between 4087 and 65526 clusters, inclusive.</td>\n</tr>\n<tr>\n<td style=\"text-align:center\">FAT32</td>\n<td style=\"text-align:center\">4</td>\n<td>Between 65526 and 268,435,456 clusters, inclusive.</td>\n</tr>\n</tbody>\n</table>\n<h3>HPFS 概述</h3>\n<p>HPFS 文件系统最先是为 OS/2 1.2 研发的,用来支持市场上助教出现的更大的硬盘的读取。另外,逐渐增长的网络服务市场也呼唤着一种新的文件系统,以拓展文件的命名、组织、安全性。HPFS 维持了 FAT 的目录组织形式,但是加入了基于文件名的自动排序。文件名拓展到了 254 双字符。HPFS 也支持数据和特殊标签的文件组合,来增加支持其他命名规则的灵活性。 另外,分配的单元从一个簇变成了物理扇区(512 字节),减少了损失的硬盘空间。</p>\n<p>使用 HPFS,相较 FAT,目录条目记录了更多的信息。标签信息包含了修改、创建、获取的日期和时间。HPFS 下的目录条目不再指向文件的第一个簇,而是指向 FNODE。FNODE 能够包含文件的数据,或指向文件数据的指针,或者其他指向文件数据的结构。</p>\n<p>HPFS 尽可能地分配连续的空间给文件数据,这是为了增加做序列处理的速度。</p>\n<p>HPFS 把硬盘分成了一系列 8MB 的段(band),一个文件总是尽可能地包含于一个段中。在这些段之间是 2K 的 allocation bitmaps,维护了段中的各个扇区的分配情况。分段增长了性能,因为在决定文件存放在哪里时,硬盘磁头不需要返回逻辑顶端(通常是柱面 0),而是最近的段 allocation bitmaps。</p>\n<p>另外,HPFS 也包含了一些特殊数据:</p>\n<h4>Super Block</h4>\n<p>Super Block 位于逻辑扇区 16,包含了一个指向根目录 FNODE 的指针。使用 HPFS 最大的隐患是如果 Super Block 丢失了或者损坏掉了,分区文件就会丢失,尽管剩余的硬盘都是好的。可以通过将其他文件都拷贝到一个 16 号扇区良好的硬盘上,重建 Super Block。但是这个过程很麻烦。</p>\n<h4>Spare Block</h4>\n<p>Spare Block 位于逻辑扇区 17,包含有一个表,记录了 "hot fixes" 和空余的文件目录块。在 HPFS 下,当检测到坏的扇区,"hot fixes" 条目就被用来指向好的扇区,对坏掉的扇区进行替换。这个处理写入错误的技术也被称为 hot fixing,它把数据从一个扇区移到另一个,并将原先的扇区标记成坏掉的。这个行为对于任意进行硬盘读写的应用都是透明的(即这些应用都不知道硬盘是否有问题)。使用支持 hot fixing 的文件系统,在遇到扇区受损的情况,可以避免让用户收到类似 FAT 中的 “Abort, Retry, or Fail?” 这样的错误信息。</p>\n<p>::: tip Note</p>\n<p>Windows NT 上使用的 HPFS 并不支持 hot fixing。</p>\n<p>:::</p>\n<h4>HPFS 的优点</h4>\n<p>HPFS 最适合 200-400 MB 的硬盘。</p>\n<h4>HPFS 的缺点</h4>\n<p>因为 HPFS 的开销,在卷的大小在 200MB 以下时,它并不是一个很好的选择。另外,当卷的大小大于 400MB 时,也会有性能的损耗。</p>\n<h3>NTFS 概述</h3>\n<p>从用户的角度来看,NTFS 仍然是将文件有序地组织到目录下,就和 HPFS一样。然而,和 FAT 或 HPFS 不同,在 NTFS 中,磁盘上没有特殊的对象,也不依赖于底层的硬件,例如 512 字节的扇区。另外,在磁盘中也没有诸如 FAT 中的 FAT 表和 HPFS 中的 Super Block 这样的特殊的位置。</p>\n<p>NTFS 的目标是提供:</p>\n<ul>\n<li>可靠的服务,这在高端的系统或文件服务器中尤其重要</li>\n<li>一个易于扩展功能的平台</li>\n<li>支持 POSIX</li>\n<li>避免 FAT 和 HPFS 的劣势</li>\n</ul>\n<p>下面对这些目标逐个进行分析,看 NTFS 是如何实现的</p>\n<h4>Reliability</h4>\n<p>为了确保 NTFS 的可靠性,主要考虑了三个因素:可恢复性,单扇区的错误消除和 hot fixing.</p>\n<p>NTFS 是可恢复的文件系统,因为它记录了文件系统中所有的事务。当在 FAT 和 HPFS 上执行 CHKDSK 的时候,我们检测了目录、分配表、文件表中指针的一致性。在 NTFS 下,因为维护了一系列事务的历史数据,所以当想要恢复文件系统中的一致性时,只需要回滚这些事务到最后一个提交记录点就行了。</p>\n<p>在 FAT 或 HPFS 下,如果一个文件系统中的特殊文件所在的扇区坏掉了,那么就会引发单扇区错误。NTFS 则可以通过两种方法避免这种问题:首先,不在磁盘上使用特殊的文件对象,而是追踪和保护磁盘上所有的对象。其次,在 NTFS 下,主文件表(Master File Table)有多个备份(具体数量取决于卷大小)。</p>\n<p>和 OS/2 版本的 HPFS 相同,NTFS 支持 hot fixing.</p>\n<h4>Added functionality</h4>\n<p>Windows 各个层面上的设计目标之一自然是提供一个可扩展的平台,NTFS 也是如此。NTFS 提供了一个可扩展的平台,其他文件系统也可以使用。除此以外,NTFS 完全支持 Windows NT 安全模型,支持多种数据流,而不再是单文件、单数据流。最后,在 NTFS 中,用户能够在文件上添加它自己定义的标签。</p>\n<h4>POSIX support</h4>\n<p>NTFS 兼容于 POSIX.1,因为它支持 POSIX.1 的下列要求:</p>\n<p>对大小写敏感:</p>\n<p>在 POSIX 下,README.TXT,Readme.txt 和 readme.txt 是不同的文件</p>\n<p>额外的时间戳:</p>\n<p>它有一个额外的时间戳记录文件上次被访问的时间</p>\n<p>硬链接:</p>\n<p>硬链接是指两个不同的文件名、在不同的目录下,指向同一个数据。</p>\n<h4>Remove limitations</h4>\n<p>首先,NTFS 已经极大地增加了文件和卷的大小,以至于他们能够达到 2^64 字节。NTFS 也使用了 FAT 中的簇的概念,以避免 HPFS 的扇区大小固定不变的问题。因为系统始终是要考虑可移植性的,512 字节的扇区大小很大概率并不是适于分配。而把簇当做硬件资源分配单位更加灵活。最后,NTFS 中所有的文件名都是 Unicode 编码的,同时支持长文件名和传统的 “8.3文件名”(即 8 个字符名字、3 个字符文件类型拓展名)。</p>\n<h4>NTFS 的优点</h4>\n<p>NTFS 最适于用于卷大小在 400MB 以上的情况。因为在使用更大的卷大小的时候,NTFS 的性能不会下降。</p>\n<p>在 NTFS 分区上,用户不需要去使用磁盘修复工具,因为 NTFS 在设计上就带有可恢复性。</p>\n<h4>NTFS 的缺点</h4>\n<p>卷大小在 400MB 以下的时候不推荐使用 NTFS,因为 NTFS 的空间开销巨大。NTFS 对 100MB 的分区就要占用 4MB。NTFS 文件系统中没有设计文件加密的方法。因此,用户可以在 MS-DOS 或其他操作系统下启动,使用底层的硬盘编辑功能来查看存储在 NTFS 卷上的数据。</p>\n<h4>NTFS 命名规则</h4>\n<p>文件和目录名最多能是 255 字符长的,包括任意拓展名。名字保留了大小写,但是不做区分。NTFS 文件名不能包含下列字符:</p>\n<pre><code>? " / \\ < > * | :\n</code></pre>\n<p>在命令行中,只能创建最多为 253 字符的文件名。</p>\n<p>::: tip Note</p>\n<p>在任意文件系统中,下层的硬件限制都可能会施加额外的分区大小限制。例如,启动分区最大只能是 7.8GB,而分区表中也有 2TB 的限制。</p>\n<p>:::</p>\n<p>对于 Windows 上文件系统更进一步的讨论可以参见 Windows 官方文档:</p>\n<ul>\n<li>Windows NT Server "Concepts and Planning Guide," Chapter 5, section titled "Choosing a File System"</li>\n<li>Windows NT Workstation 4.0 Resource Kit, Chapter 18, "Choosing a File System"</li>\n<li>Windows NT Server 4.0 Resource Kit "Resource Guide," Chapter 3, section titled "Which File System to Use on Which Volumes"</li>\n</ul>\n<h2>ReFS</h2>\n<p>该复原文件系统 (ReFS) 是 Microsoft 的最新文件系统,可最大程度提升数据可用性、跨各种工作负载高效扩展到大数据集,并通过损坏复原提供数据完整性。 据微软<a href=\"https://docs.microsoft.com/zh-cn/windows-server/storage/refs/refs-overview\">官方文档</a>:</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Feature</th>\n<th style=\"text-align:left\">ReFS</th>\n<th style=\"text-align:left\">NTFS</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">最大文件名称长度</td>\n<td style=\"text-align:left\">255 个 Unicode 字符</td>\n<td style=\"text-align:left\">255 个 Unicode 字符</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">最大路径名称长度</td>\n<td style=\"text-align:left\">32K Unicode 字符</td>\n<td style=\"text-align:left\">32K Unicode 字符</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">文件大小上限</td>\n<td style=\"text-align:left\">35 PB (pb)</td>\n<td style=\"text-align:left\">256 TB</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">最大卷大小</td>\n<td style=\"text-align:left\">35 PB</td>\n<td style=\"text-align:left\">256 TB</td>\n</tr>\n</tbody>\n</table>\n<h4>主要优点</h4>\n<h5>复原</h5>\n<p>ReFS 引入了一项新功能,可以准确地检测到损坏并且还能够在保持联机状态的同时修复这些损坏,从而有助于增加你的数据的完整性和可用性:</p>\n<ul>\n<li><strong>完整性流</strong> - ReFS 将校验和用于元数据和文件数据(可选),这使得 ReFS 能够可靠地检测到损坏。</li>\n<li><strong>存储空间集成</strong> - 在与镜像或奇偶校验空间配合使用时,ReFS 可使用存储空间提供的备用数据副本自动修复检测到的损坏。 修复过程将本地化到损坏区域且联机执行,并且不会出现卷停机时间。</li>\n<li><strong>挽救数据</strong> - 如果某个卷损坏并且损坏数据的备用副本不存在,则 ReFS 将从命名空间中删除损坏的数据。 ReFS 在处理大多数不可更正的损坏时可将卷保持在联机状态,但在极少数情况下,ReFS 需要将卷保持在脱机状态。</li>\n<li><strong>主动纠错</strong> - 除了在读取和写入前对数据进行验证之外,ReFS 还引入了称为“清理器”的数据完整性扫描仪 。 此清理器会定期扫描卷,从而识别潜在损坏,然后主动触发损坏数据的修复。</li>\n</ul>\n<h5>性能</h5>\n<p>除了提供复原能力改进之外,ReFS 还针对对性能极其敏感和虚拟化的工作负载引入新功能。 实时层优化、块克隆和稀疏 VDL 都是不断发展的 ReFS 功能的绝佳示例,它们专为支持各种动态工作负载而设计:</p>\n<ul>\n<li>\n<p><strong><a href=\"https://docs.microsoft.com/zh-cn/windows-server/storage/refs/mirror-accelerated-parity\">镜像加速奇偶校验</a></strong> - 镜像加速奇偶校验既可以提供高性能,也可为你的数据提供高效的容量存储。</p>\n<ul>\n<li>\n<p>为了提供高性能和高效的容量存储,ReFS 会将卷划分为两个逻辑存储组,称为层。 这些层可具有自己的驱动器和复原类型,这使得能够针对性能或容量对每个层进行优化。 某些示例配置包括:</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">性能层</th>\n<th style=\"text-align:left\">容量层</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">镜像的 SSD</td>\n<td style=\"text-align:left\">镜像的 HDD</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">镜像的 SSD</td>\n<td style=\"text-align:left\">奇偶校验 SSD</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">镜像的 SSD</td>\n<td style=\"text-align:left\">奇偶校验 HDD</td>\n</tr>\n</tbody>\n</table>\n</li>\n<li>\n<p>在配置了这些层后,ReFS 就会使用它们为热数据提供快速存储,以及为冷数据提供节省空间的存储:</p>\n<ul>\n<li>所有写入都将在性能层中发生,并且在性能层中保留的大数据区块将高效地实时移到容量层中。</li>\n<li>如果使用混合部署 (将闪存驱动器和 HDD 驱动器混合) , <a href=\"https://docs.microsoft.com/zh-cn/windows-server/storage/storage-spaces/understand-the-cache\">则存储空间直通中的缓存</a> 可帮助加快读取速度,同时降低虚拟化工作负荷的数据碎片特性的影响。 否则,如果使用的是双闪存部署,则读取也会出现在性能层中。</li>\n</ul>\n</li>\n<li>\n<p>对于服务器部署,镜像加速奇偶校验仅在<a href=\"https://docs.microsoft.com/zh-cn/windows-server/storage/storage-spaces/storage-spaces-direct-overview\">存储空间直通</a>上受支持。 建议仅将镜像加速奇偶校验用于存档和备份工作负荷。 对于虚拟化和其他高性能随机工作负载,我们建议使用三向镜像以获得更好的性能。</p>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>加快 VM 操作</strong> - ReFS 引入了为改善虚拟化工作负载的性能而专门设计的新功能:</p>\n<ul>\n<li><a href=\"https://docs.microsoft.com/zh-cn/windows-server/storage/refs/block-cloning\">块克隆</a> - 块克隆可加快复制操作的速度,并且能够实现快速、低影响的 VM 检查点合并操作。</li>\n<li>稀疏 VDL - 稀疏 VDL 允许 ReFS 将文件快速清零,从而将创建固定 VHD 所需的时间从几十分钟减少到仅仅几秒钟。</li>\n</ul>\n</li>\n<li>\n<p><strong>可变群集大小</strong> - ReFS 支持 4K 和 64K 的群集大小。 4K 是针对大多数部署的建议的群集大小,但 64K 群集适合于大型的、顺序 IO 工作负载。</p>\n</li>\n</ul>\n<h5>可伸缩性</h5>\n<p>ReFS 设计为支持非常大的数据集(数百万 TB 字节),而不会对性能有负面影响,并且与以前的文件相比实现了更好的扩展性。</p>\n<h2>FatFs</h2>\n<p>FatFS 是一个为嵌入式系统开发的通用 FAT 文件系统模块(腾讯的 TencentOS-tiny 的文件系统就是在该模块外做了包装)。FatFs 是用 ANSI C 写的,和硬盘 I/O 层是分离的。因此,他和硬件架构是独立的。它可以不做任何改动地部署在低成本的微控制器中。</p>\n<p><img src=\"./fatfs_layers.png\" alt=\"\"></p>\n<p>特性:</p>\n<ul>\n<li>和 Windows 相兼容的 FAT 文件系统。</li>\n<li>平台无关,容易移植。</li>\n<li>代码和工作区域占的空间很小。</li>\n<li>各种配置选项:\n<ul>\n<li>支持多卷(物理驱动和分区)</li>\n<li>支持包括 DBCS 方式在内的多种字符编码(Windows 中的两类字符编码表:ANSI/OEM code pages,在不同地区使用不同编码表,同一个编码可能对应不同的字符。DBCS 全称双位元组字元,DBCS 和 Unicode 都会被用于字符编码,Unicode 的字元是16位的,在 C 中使用的是宽字符;DBCS 则是 8 位,但是某些元组需要和别的元组共同定义字元,即不定长。Windows 只在东亚地区提供 DBCS)。</li>\n<li>长文件名支持</li>\n<li>RTOS 支持</li>\n<li>支持多种扇区大小</li>\n<li>支持只读特性、简洁API,支持 I/O buffer ……</li>\n</ul>\n</li>\n</ul>\n<p>API 接口参加官方文档:<a href=\"http://irtos.sourceforge.net/FAT32_ChaN/doc/00index_e.html\">ELM - FatFs Generic FAT File System Module (sourceforge.net)</a></p>\n<h3>其他文件系统</h3>\n<p>Linux 下存在几十个文件系统类型:ext2,ext3,ext4,xfs,brtfs,zfs(使用命令 <code>man 5 fs</code> 可以取得全部文件系统的介绍)</p>\n<p>ext2,ext3,ext4 是一系列的,其中 EXT 为扩展文件系统(Extended file system)。</p>\n<ul>\n<li>ext2 具有极快的速度和极小的CPU占用率,可用于硬盘和移动存储设备</li>\n<li>ext3 增加日志功能,可回溯追踪</li>\n<li>ext4 日志式文件系统,支持1EB(1024*1024TB),最大单文件16TB,支持连续写入可减少文件碎片。rhel6默认文件系统</li>\n<li>xfs 可以管理500T的硬盘。rhel7默认文件系统</li>\n<li>brtfs 文件系统针对固态盘做优化,</li>\n<li>zfs 是第一个 128 位文件系统</li>\n</ul>\n<h3>网络文件系统</h3>\n<p>NFS:全称即为网络文件系统,可以让类 Unix 的机器互相共享文件,作为文件服务器,基于 RPC Server。是一种分布式文件系统协议,它允许网络中的计算机之间通过TCP/IP网络共享资源。在NFS的应用中,本地NFS的客户端应用可以透明地读写位于远端NFS服务器上的文件,就像访问本地文件一样。</p>\n<p>SMB:支持SMB协议的网络文件系统。SMB 是一种应用层网络传输协议,使网络上的机器能够共享计算机文件、打印机、串行端口和通讯等资源。它也提供认证的进程间通讯技能,主要应用于 Windows 上。</p>\n<p>CIFS:全称 Common Internet File System,Windows 上用来实现网上邻居的协议。是在SMB的基础上发展,扩展到Internet上的协议。他和具体的OS无关,在 Unix上安装 Samba 后可使用 CIFS。它使程序可以访问远程 Internet 计算机上的文件并要求此计算机的服务。CIFS 使用客户/服务器模式。客户程序请求远在服务器上的服务器程序为它提供服务。服务器获得请求并返回响应。</p>\n<p>Samba:可以让类 Unix 机器与 Windows 机器之间共享文件(与 SMB 和 CIFS 交互),基于 NetBIOS(Network Basic Input/Output System)协议。</p>\n<p>基于网络文件系统,可以构建 NAS。NAS(Network Attached Storage)被定义为一种特殊的专用数据存储服务器,包括存储器件(例如磁盘阵列、CD/DVD驱动器、磁带驱动器或可移动的存储介质)和内嵌系统软件,可提供跨平台文件共享功能。NAS通常在一个LAN上占有自己的节点,无需应用服务器的干预,允许用户在网络上存取数据,在这种配置中,NAS集中管理和处理网络上的所有数据,将负载从应用或企业服务器上卸载下来,有效降低总拥有成本,保护用户投资。<br>\nNAS本身能够支持多种协议(如NFS、CIFS、FTP、HTTP等),而且能够支持各种操作系统。通过任何一台工作站,采用IE或Netscape浏览器就可以对NAS设备进行直观方便的管理。</p>\n<h2>VFS</h2>\n<blockquote>\n<p>Virtual filesystems are the magic abstraction that makes the "everything is a file" philosophy of Linux possible.</p>\n</blockquote>\n<p>什么是文件系统呢?按照早期的 Linux 贡献者 <a href=\"https://www.pearson.com/us/higher-education/program/Love-Linux-Kernel-Development-3rd-Edition/PGM202532.html\">Robert Love</a> 所说,“文件系统就是数据结构化的存储。“ 然而,这种表述同样适用于 VFAT (Virtual File Allocation Table),Git,和 <a href=\"http://cassandra.apache.org/\">Cassandra</a>(一种 <a href=\"https://en.wikipedia.org/wiki/NoSQL\">NoSQL 数据库</a>)。所以到底什么因素让文件系统与众不同?</p>\n<h3>文件系统基础</h3>\n<p>Linux 内核要求一个文件系统在具名的文件对象上实现 <code>open()</code>,<code>read()</code>,<code>write()</code> 这些方法。从面向对象的视角来说,内核将通用的文件系统视为一个抽象的接口,这三个函数是 "虚函数",没有默认的定义。相应地,内核默认的文件系统实现叫做 virtual filesystem (VFS)。</p>\n<p><img src=\"./virtualfilesystems_1-console.png\" alt=\"If we can open(), close(), read() and write(), it is a file as this console session shows.\"></p>\n<p>如果我们能在一个对象上调用 <code>open()</code>,<code>read()</code> 和 <code>write()</code>,那它就是一个文件,如上图所示。</p>\n<p>VFS 是类 Unix 系统中一个著名的论断的基础,即 “everything is a file”。考虑上面的例子中,<code>/dev/console</code> 设备的奇特表现。上图展示了一个虚拟的 tty 终端上的交互式的 Bash 会话。发送字符串给该虚拟控制台设备,会让这个字符串原封不动地出现在虚拟屏幕上。VFS 还有很多奇特甚至更加奇特的性质,例如:<a href=\"https://lwn.net/Articles/22355/\">seek in them</a>。</p>\n<p>广为人知的文件系统比如 <code>ext4</code>、<code>NFS</code> 和 <code>/proc</code> 都用了一种称为 <a href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fs.h\">file_operations</a> 的数据结构提供了上面提到的三个函数的实现。除此以外,一些文件系统也用面向对象的方式重载了 VFS 的函数。正如 Robert Love 指出的,VFS 的抽象机制使得 Linnux 用户可以无忧无虑地从别的操作系统或抽象实体(例如 <code>pipes</code> 管道)复制文件,而不用担心内部的数据格式。一个用户空间的进程可以使用系统调用,使用 <code>read()</code> 从一个文件系统中向内核数据结构中复制一个文件,然后再使用 <code>write()</code> 方法向另一个文件系统中输出数据。</p>\n<p>VFS 基类相关的函数定义在内核代码中的文件 <code>fs/*.c</code> 中,而 <code>fs/</code> 目录下的子文件夹则是特定文件系统的实现。内核也包含了类似文件系统的实体例如 <code>cgroups</code>(<code>cgroups</code> 是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,可以对 cpu,内存等资源实现精细化的控制), <code>/dev</code> 和 <code>tmpfs</code>(tmpfs是类 Unix 系统上的一种基于内存的文件系统,用于缓存),因为他们和启动有关,因此都定义在 <code>init</code> 目录下。<code>cgroups</code>,<code>/dev</code> 和 <code>tmpfs</code> 并不直接调用之前提到的文件操作的三个函数,而是直接读写内存。</p>\n<p>下面的图片大致介绍了用户空间是如何获取到挂载在 Linux 上的各种各样的文件系统的。图中没有画出来 <code>pipes</code>,<code>dmesg</code> 和 <code>POSIX clocks</code>,他们也实现了 <code>file_operations</code> 结构体,是通过 VFS 来获取的。</p>\n<p><img src=\"./virtualfilesystems_2-shim-layer.png\" alt=\"How userspace accesses various types of filesystems\"></p>\n<p>VFS 是一个系统调用和特定的 <code>file_operation</code> 实现如 <code>ext4</code> 、<code>procfs</code> 之间的夹层。而 <code>file_operations</code> 函数能够和设备驱动、内存、<code>tmpfs</code>、<code>devtmpfs</code> 和 <code>cgroups</code> 等交互。</p>\n<p>VFS 的存在提升了代码的复用性,因为和文件系统相关的基础方法不需要为每个都实现一次了。当然这也意味着复用的代码如果有问题,所有相关的模块都会受到影响。</p>\n<h3>一个例子:/tmp</h3>\n<p>一个查阅 VFS 是如何在系统上实现的简单的方法是输入 <code>mount | grep -v sd | grep -v :/</code>,这个命令会列出所有挂载着的、不在磁盘上也不是网络文件系统(NFS)的文件系统,也就是说是 VFS 了。那么其中就会有 <code>/tmp</code>。一般情况下都不会把它放在真实的物理存储上。</p>\n<p>为什么不建议放在真实的存储上呢?因为 <code>/tmp</code> 下的文件是临时的,在上面要创建 tmpfs,而存储设备比内存要慢。同时,相比内存,存储设备在频繁地读写下会更加容易受损。最后,<code>/tmp</code> 中的文件可能含有敏感数据,所以让他们在每次重启后消失是系统的一个特性。</p>\n<p>不幸的是,一些 Linux 发行版的安装脚本仍然将 <code>/tmp</code> 默认创建到了存储上,按照 Arch Linux 的 <a href=\"https://wiki.archlinux.org/index.php/Tmpfs\">Arch Wiki</a>,就可以修复这个问题。但是分配给 tmpfs 的内存是不能拿来做别的事情的。换句话说,系统可能会因为用完了内存而崩溃。另一个建议:在编辑 <code>/etc/fstab</code> 文件时,记得输入一个换行来结尾,否则你的系统不会启动……</p>\n<h3>另一个例子:/proc and /sys</h3>\n<p>除了 <code>/tmp</code>,大多数 Linux 用户比较熟悉的 VFS 还有 <code>/proc</code> 和 <code>/sys</code>。(<code>/dev</code> 依赖于共享内存,因此没有实现相应的 <code>file_operations</code>)。</p>\n<p><code>procfs</code> 提供了内核瞬间状态和它控制着的用户空间进程的快照。在<code>/proc</code> 内,内核发布它提供的功能的相关信息,例如中断、共享内存和调度器等信息。另外,<code>/proc/sys</code> 是从用户空间获取通过 <a href=\"http://man7.org/linux/man-pages/man8/sysctl.8.html\">sysctl command</a> 进行设置的选项的地方。标号为 PID 的进程的状态和数据会在子文件夹 <code>proc/<PID></code> 下记录。</p>\n<p><img src=\"./virtualfilesystems_4-proc-meminfo.png\" alt=\"Console\"></p>\n<p><code>/proc/meminfo</code> 是一个空文件,然而它却包含着重要的信息。<code>/proc</code> 文件展示了 VFS 能和磁盘上的文件系统之间的差别。一方面,<code>/proc/meminfo</code> 包含了信息,另一方面它却是空文件,这是怎么做到的呢?</p>\n<p>这个情况是使人回想起康奈尔大学物理学家 N. David Mermin 在1985年写的一篇著名的文章 <a href=\"http://www-f1.ijs.si/~ramsak/km1/mermin.moon.pdf\">Is the moon there when nobody looks?Reality and the quantum theory</a> 。事实上,在进程向 <code>/proc</code> 请求的时候,内核才收集了内存的相关信息,而没有人请求的时候,它里面其实是沙也没有的。正如这名物理学家说的,"It is a fundamental quantum doctrine that a measurement does not, in general, reveal a preexisting value of the measured property."</p>\n<p>于是 <code>procfs</code> 是空的这件事就显而易见了,因为那里的信息是动态的。而 <code>sysfs</code> 是不同的,我们看看 <code>/proc</code> 下和 <code>/sus</code> 下各有多少大于一字节的文件。</p>\n<p><img src=\"./virtualfilesystems_6-filesize.png\" alt=\"Console\"></p>\n<p>上图中,<code>procfs</code> 只有一个,看名字是和内核配置相关的文件,这是我们意料之中的事情,因为每次启动的时候都要生成它。另一方面,<code>/sys</code> 下则有很多文件,大多数都会消耗掉一页内存。通常而言,<code>sysfs</code> 文件只包含一串数字或字符串,和 <code>/proc/meminfo</code> 中的表状的数据不同。</p>\n<p><code>sysfs</code> 的目的是暴露出内核 <strong>kobject</strong> 的可读和可写特性给用户空间的应用。kobject 是用来搞引用计数的,当最后一个相关引用删除掉后,系统会回收相关资源。<code>/sys</code> 包含了内核中大多数可以<a href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/ABI/stable\">供用户使用的稳定 ABI</a>,也就是在任何情况下都<a href=\"https://lkml.org/lkml/2012/12/23/75\">不可能出错</a>。这并不意味着 <code>sysfs</code> 里的文件是静态的,这将会违背引用计数。</p>\n<p>内核的稳定 ABI 限制了什么样的数据能够出现在 <code>/sys</code> 中,而不是任意时刻出现的任意文件。打印出 <code>sysfs</code> 中的文件能够帮助我们知晓各个设备、模块、文件系统的相关配置到底是什么样的。逻辑上 <code>procfs</code> 也是内核稳定 ABI 的一部分,尽管内核文档并没有明说。</p>\n<p><img src=\"./virtualfilesystems_7-sysfs.png\" alt=\"Console\"></p>\n<p><code>sysfs</code> 中的文件或者可读,或者可写,或者二者兼具。上图中文件 <code>/sys/block/sda/removable</code> 中的 0 表示 sda 上的硬盘不是 removable 的。</p>\n<h3>使用 eBPF 和 bcc 工具来监听 VFS</h3>\n<p>最简单的了解内核是如何管理 <code>sysfs</code> 文件的方法就是实际地去观察它,其中最简洁的一种观察方式就是使用 <a href=\"/zh/blogs/20210329/\">eBPF</a>。内核源码告诉了读者内核能做什么,使用 eBPF 工具则展示出了内核实际上是怎么工作的。</p>\n<p>幸运的是,通过 <a href=\"https://github.com/iovisor/bcc\">bcc</a> 使用 eBPF 是非常容易的,主要的发行版都带有它,也有一些相关文档如 <a href=\"http://brendangregg.com/ebpf.html\">Brenden Gregg</a>。bcc 脚本都是 Python 脚本,带一些 C, 所以很容易使用。</p>\n<p>为了了解 VFS 是怎么工作的,可以尝试 bcc 脚本 <a href=\"https://github.com/iovisor/bcc/blob/master/tools/vfscount_example.txt\">vfscount</a> 或 <a href=\"https://github.com/iovisor/bcc/blob/master/tools/vfsstat.py\">vfsstat</a>。下图就显示了每秒都有很多调用 <code>vsf_open()</code> 之类函数的。</p>\n<p><img src=\"./virtualfilesystems_8-vfsstat.png\" alt=\"Console - vfsstat.py\"></p>\n<p><code>vfsstat.py</code> 是 python 脚本,使用了一些 C 的代码统计 VFS 函数调用了多少次。下面看一个不怎么一般的例子,我们看看当 USB 设备接入的时候,会发生什么</p>\n<p><img src=\"./virtualfilesystems_9-ebpf.png\" alt=\"Console when USB is inserted\"></p>\n<p>上图中第一个例子中,每当 <code>sysfs_create_files()</code> 命令运行的时候,bcc 脚本 <a href=\"https://github.com/iovisor/bcc/blob/master/tools/trace_example.txt\">trace.py</a> 打印出新的消息。我们可以看到,作为 USB 接入的回应,会有一个 kworker 线程启动 <code>sysfs_create_files()</code>,但是它创建了什么文件呢?</p>\n<p>第二个例子就展现了 eBPF 的威力。这个例子里,<code>trace.py</code> 通过 <code>-K</code> 选项打印了内核的回溯信息和 <code>sysfs_create_files()</code> 创建的文件名。单引号内的是 C 语言的源码,包含一个很容易被识别的模板字符串,Python 脚本会调用 LLVM 将它编译并在内核中的虚拟机执行。第二个命令里必须有完整的 <code>sysfs_create_files()</code> 函数签名,模板字符串才能取其中的参数。</p>\n<p>bcc 的错误提示的可读性不错,如果是遗漏了 <code>-I</code> 参数,错误信息会是 "Failed to compile BPF text." 如果你了解 python 或 C,也可以很容易地拓展它。</p>\n<p>当 USB 插入时,内核回溯信息显示出 PID 7711 是一个 kworker 线程,在 sysfs 中创建了一个名为 "events" 的文件。在移除 USB 的时候,events 文件也随之删除了,和引用计数的思想一致。</p>\n<p>在 USB 接入的时候,使用 eBPF 观察 <code>sysfs_create_link()</code> ,会看到它创建不超过 48 个的符号链接(文中的图片不包含这个现象)。到底 events 文件的作用是什么呢?使用 <a href=\"http://northstar-www.dartmouth.edu/doc/solaris-forte/manuals/c/user_guide/cscope.html\">cscope</a> 可以发现函数 <a href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/block/genhd.c#n665\">__device_add_disk()</a> 调用了 <code>disk_add_events()</code>,然后在 events 文件中可能会写入 "media_change" 或 "eject_request" 。这里,kernel's block layer 告知用户空间,USB 接入的存储设备到底还在不在。使用 bcc 明显比去读源码要快多了。</p>\n<h3>VFS 的魔力:read-only root filesystem 赋能嵌入式设备</h3>\n<p>确实,没有人是通过拔电线来关闭服务器或桌面系统的。为什么呢?因为物理存储上挂载着的文件系统可能有待写入的文件,或者记录文件系统状态的数据结构可能不是同步。当这种情况发生的时候,系统管理员需要等待下次启动,并运行 <a href=\"http://www.man7.org/linux/man-pages/man8/fsck.8.html\">fsck filesystem-recovery tool</a>,最坏的情况下,将会丢失数据。</p>\n<p>但是,也有许多 IoT 设备和嵌入式设备,例如路由器、恒温器、汽车都在运行 Linux。许多这类设备都缺乏用户界面,甚至没有办法优雅地关机。考虑用跨接引线启动的汽车,如果电池没电了,那么 <a href=\"https://wiki.automotivelinux.org/_media/eg-rhsa/agl_referencehardwarespec_v0.1.0_20171018.pdf\">Linux-running head unit</a> 就会时开时关。显然不可能有一个满场的 fsck 文件系统检测过程,这是怎么做到的呢?答案就是嵌入式设备依赖于 <a href=\"https://elinux.org/images/1/1f/Read-only_rootfs.pdf\">a read-only root fileystem</a> (ro-rootfs for short)。</p>\n<p>ro-rootfs 是为什么嵌入式设备并不需要频繁地进行 fsck。ro-rootfs 还提供了许多其他优点,其中一个就是如果嵌入到 Linux 进程中,恶意软件不能写入信息到 <code>/usr</code> 或 <code>/lib</code>。另一个就是文件系统中的大部分文件都是不可变的,这对于远程设备是十分关键的,因为维护者他们的本地系统需要和远端现场一致的系统。而最重要的优点就是 ro-rootfs 强迫开发者在项目的设计阶段就决定下来什么是不能变的。开发和使用 ro-rootfs 可能是不方便的甚至是痛苦的,正如编程语言中的常量(<a href=\"https://www.meetup.com/ACCU-Bay-Area/events/drpmvfytlbqb/\">const variables in programming languages</a>)所表现的,但是它带来的好处确实可以弥补这部分不便。</p>\n<p>创建一个只读的 rootfs 不需要嵌入式开发者的额外努力,这也就是 VFS 发挥作用的地方。Linux 需要目录 <code>/var</code> 下面的文件是可写入的,同时许多嵌入式应用会尝试在 <code>$HOME</code> 中去创建隐藏的配置文件(即<code>dot-files</code>),但是我们又可能会要求 rootfs 这部分是只读的。一个解决方案是提前生成这些配置文件,把他们写到 rootfs 中。对于 <code>/var</code>, 一个方法是把它挂载到独立的可写区域,让根目录 <code>/</code> 保持只读。使用 bind 或 overlay mounts 是另一种替代方法,具体见下一节的描述。</p>\n<h3>VFS 的魔力:bind mounts 和 overlay mounts 及其在容器中的使用</h3>\n<p>命令 <code>man mount</code> 是学习 bind mounts 和 overlay mounts 的最好的场所。他们给了嵌入式开发者和系统管理员这样的特殊能力:在一个路径下创建文件系统,在应用中通过另一个路径把它提供给应用。对于嵌入式系统来说,隐含的就是可以在可以在不可写入的闪存设备上的 <code>/var</code> 目录下存储文件,但是在启动时候挂载一个 tmpfs 的路径到 <code>/var</code> 目录下,那么应用就可以按意愿在那里写入数据了。下次启动的时候,<code>/var</code> 里的改动会消失(也就是上次挂载 tmpfs 后的改动),只有原始的不可变的数据还在。 Overlay mounts 提供了一个 tmpfs 和底层文件系统之间的联合体,允许显示地改动 ro-rootfs 中已经存在的文件,bind mounts 则可以新建 tmpfs 空目录,在 ro-rootfs 文件目录下呈现出可写特性。虽然 overlayfs 是一个更合适的文件系统类型,但是在 <a href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/sharedsubtree.txt\">VFS namespace facility</a> 实现中采用的是 bind mounts。</p>\n<p>基于上面对二者的描述,不必惊异,<a href=\"https://coreos.com/os/docs/latest/kernel-modules.html\">Linux containers</a> 也使用了大量相关的特性。让我们用 bcc 的 <code>mountsnoop</code> 工具来看看 <a href=\"https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html\">systemd-nspawn</a> 干了啥。</p>\n<p><img src=\"./virtualfilesystems_11-system-nspawn.png\" alt=\"Console - system-nspawn invocation\"></p>\n<p>上图中,<code>system-nspawn</code> 启动了一个容器。让我们看看发生了什么:</p>\n<p><img src=\"./virtualfilesystems_12-mountsnoop.png\" alt=\"Console - Running mountsnoop\"></p>\n<p>在启动容器的时候执行 mountsnoop,会显示出容器运行时依赖于 bind mounts(图中只展示了开始的阶段)。</p>\n<p><code>systemd-nspawn</code> 在宿主机中选中的 procfs 和 sysfs 文件提供给容器它自己的 rootfs。除了 MS_BIND 标志设置了 bind-mounting,其他一些“mount”系统调用会触发的标志也决定了宿主命名空间和容器命名空间直接的关系。例如,bind mount 能够传播 <code>/proc</code> 和 <code>/sys</code> 中的改动到容器中或者隐藏起来,这取决于调用方式。</p>\n<h3>Summary</h3>\n<p>Linux 内核源码太多了,很难理解,而且还有用户空间的英语和 glibc 等 C 库中的系统调用接口。一种阅读方法是重点理解面向用户的系统调用和主要内核接口,例如这里的 file_operations 表。file operations 是使得 "everything is a file" 的基础。源码根目录 <code>fs/</code> 下的内核 C 源码实现了虚拟文件系统 VFS,是存储设备和文件系统之间的夹层。通过 Linux 命名空间做 Bind 和 overlay mounts 是 VFS 的魔法操作,是实现容器和只读根目录的基础之一。结合阅读源码,eBPF 内核工具和它的 bcc 接口让窥探内核变得更加容易了。</p>\n<h3>VFS in Linux Kernal</h3>\n<p>官方文档:<a href=\"https://www.kernel.org/doc/html/latest/filesystems/vfs.html\">Overview of the Linux Virtual File System — The Linux Kernel documentation</a></p>\n<h2>Reference</h2>\n<p><a href=\"https://docs.microsoft.com/en-us/troubleshoot/windows-client/backup-and-storage/fat-hpfs-and-ntfs-file-systems\">Overview of FAT, HPFS, and NTFS File Systems - Windows Client | Microsoft Docs</a></p>\n<p><a href=\"https://opensource.com/article/19/3/virtual-filesystems-linux\">Virtual filesystems: Why we need them and how they work | Opensource.com</a></p>\n<p><a href=\"https://blog.csdn.net/JCRunner/article/details/50252705?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&dist_request_id=1331969.108.16184948965581971&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control\">SMB CIFS Samba NFS NAS</a></p>\n<p>其他有趣的文章</p>\n<ul>\n<li><a href=\"https://developers.redhat.com/cheat-sheets/linux-commands-cheat-sheet/?intcmp=70160000000h1jYAAQ&utm_source=intcallout&utm_campaign=linuxcontent\">Linux commands cheat sheet</a></li>\n<li><a href=\"https://developers.redhat.com/cheat-sheets/advanced-linux-commands/?intcmp=70160000000h1jYAAQ&utm_source=intcallout&utm_campaign=linuxcontent\">Advanced Linux commands cheat sheet</a></li>\n<li><a href=\"https://opensource.com/downloads/cheat-sheet-networking?intcmp=70160000000h1jYAAQ&utm_source=intcallout&utm_campaign=linuxcontent\">Linux networking cheat sheet</a></li>\n<li><a href=\"https://opensource.com/resources/what-are-linux-containers?intcmp=70160000000h1jYAAQ&utm_source=intcallout&utm_campaign=linuxcontent\">What are Linux containers?</a></li>\n<li><a href=\"https://tech.meituan.com/2015/03/31/cgroups.html\">Linux资源管理之cgroups简介</a></li>\n<li><a href=\"https://blog.csdn.net/qq_17416741/article/details/50921434\">Linux中tmpfs</a></li>\n</ul>\n<p>网站</p>\n<ul>\n<li>\n<p><a href=\"https://lwn.net/\">LWN.net</a></p>\n</li>\n<li>\n<p><a href=\"https://lkml.org/\">LKLM</a></p>\n</li>\n<li>\n<p><a href=\"https://opensource.com/tags/linux?intcmp=70160000000h1jYAAQ&utm_source=intcallout&utm_campaign=linuxcontent\">opensource.com</a></p>\n</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20210412/",
"title": "文件系统学习笔记",
"summary": "FAT, HPFS, NTFS, FatFs, VFS",
"date_modified": "2021-04-12T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Makefile 笔记</h1>\n<p>记录一些遇到的问题</p>\n<h2>伪目标 Phony Target</h2>\n<p>当目录下路径或文件与 Makefile中 的目标名冲突的时候,可以通过定义伪目标的方式避免冲突:</p>\n<p>如下面这个例子</p>\n<pre><code class=\"language-bash\">touch clean\nmake clean\n</code></pre>\n<p>这样不会执行 Makefile 中定义的 clean 目标,而是会提示我们 <code>clean is up to date</code></p>\n<p>此时需要使用 <code>.PHONY</code> 关键字来定义 clean</p>\n<p>在 Makefile 中需要写</p>\n<pre><code class=\"language-makefile\">.PHONY: clean\n</code></pre>\n<h2>预定义的变量</h2>\n<table>\n<thead>\n<tr>\n<th>预定义变量</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>AR</td>\n<td>库文件维护程序的名称,默认值为ar</td>\n</tr>\n<tr>\n<td>AS</td>\n<td>汇编程序的名称,默认值为as</td>\n</tr>\n<tr>\n<td>CC</td>\n<td>C 编译器的名称,默认值为cc</td>\n</tr>\n<tr>\n<td>CPP</td>\n<td>C 预编译器的名称,默认值为$(CC) –E</td>\n</tr>\n<tr>\n<td>CXX</td>\n<td>C++编译器的名称,默认值为g++</td>\n</tr>\n<tr>\n<td>FC</td>\n<td>FORTARAN 编译器的名称,默认值为f77</td>\n</tr>\n<tr>\n<td>RM</td>\n<td>文件删除程序的名称,默认值为rm -f</td>\n</tr>\n<tr>\n<td>ARFLAGS</td>\n<td>库文件维护程序的选项,无默认值</td>\n</tr>\n<tr>\n<td>ASFLAGS</td>\n<td>汇编程序的选项,无默认值</td>\n</tr>\n<tr>\n<td>CFLAGS</td>\n<td>C 编译器的选项,无默认值</td>\n</tr>\n<tr>\n<td>CPPFLAGS</td>\n<td>C 预编译的选项,无默认值</td>\n</tr>\n<tr>\n<td>CXXFLAGS</td>\n<td>C++编译器的选项,无默认值</td>\n</tr>\n<tr>\n<td>FFLAGS</td>\n<td>Fortran 编译器的选项,无默认值</td>\n</tr>\n</tbody>\n</table>\n<h2>默认CC</h2>\n<p>在默认情况下,编译器使用 <code>cc</code>,可以通过<code>update-alternatives --list cc</code> 命令查看安装的 C 编译器种类,然后使用 <code>update-alternatives --set cc</code> 按照提示设置。</p>\n<h2>内存泄漏检测</h2>\n<p>之前用过 valgrind 工具辅助分析。还有一个比较好用的是 sanitize,可以在编译时作为 <code>CFLAGS</code> 添加,比如使用 address 选项检测非法内存地址</p>\n<pre><code class=\"language-makefile\">ifeq ($(ASAN),1) # 需要定义该参数\nCFLAGS += -fsanitize=address\nLDFLAGS += -fsanitize=address\nendif\n</code></pre>\n<p><code>-fsanitize=leak</code> 则可以检查泄漏</p>\n<h2>代码覆盖率</h2>\n<p>gcov是在代码运行时统计代码覆盖率的工具,随着gcc一起发布的。<br>\n它的使用很简单,需要在编译和链接时增加-fprofile-arcs -ftest-coverage生成二进制文件。</p>\n<pre><code class=\"language-makefile\">ifeq ($(COVERAGE),1)\nCFLAGS += -fprofile-arcs -ftest-coverage\nLDFLAGS += -fprofile-arcs\nendif\n</code></pre>\n<p>gcov主要使用.gcno和.gcda两个文件。<br>\n.gcno是由-ftest-coverage产生的,它包含了重建基本块图和相应的块的源码的行号的信息。<br>\n.gcda是由加了-fprofile-arcs编译参数的编译后的文件运行所产生的,它包含了弧跳变的次数和其他的概要信息。</p>\n<h2>为目录下所有文件生成目标代码</h2>\n<p>可以使用例如下面 Makefile 的写法,使用通配符</p>\n<pre><code class=\"language-makefile\">SOURCES = $(wildcard *.c)\nOBJS = $(patsubst %.c,%.o,$(SOURCES))\n\nAll:$(OBJS)\n</code></pre>\n<h2>特殊符号</h2>\n<p><code>$@</code> 表示目标文件</p>\n<p><code>$^</code> 表示所有的依赖文件</p>\n<p><code>$<</code> 表示第一个依赖文件</p>\n<p><code>$?</code> 表示比目标还要新的依赖文件列表</p>\n<p><code>$%</code> 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是<code>foo.a(bar.o)</code>,那么,<code>$%</code>就是<code>bar.o</code>,<code>$@</code>就是<code>foo.a</code>。如果目标不是函数库文件,那么,其值为空。</p>\n<p><code>$+</code> 这个变量很像<code>$^</code>,也是所有依赖目标的集合。只是它不去除重复的依赖目标。</p>\n<p><code>$*</code> 这个变量表示目标模式中 <code>% </code>及其之前的部分。如果目标是 <code>dir/a.foo.b</code>,并且目标的模式是<code>a.%.b</code>,那么,<code>$*</code>的值就是<code>dir/a.foo</code>。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么<code>$*</code>也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么<code>$*</code>就是除了后缀的那一部分。例如:如果目标是“foo.c”,因为“.c”是make所能识别的后缀名,所以,<code>$*</code>的值就是<code>foo</code>。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用<code>$*</code>,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么<code>$*</code>就是空值。</p>\n<h2>赋值符号</h2>\n<ul>\n<li><code>=</code> 是最基本的赋值</li>\n<li><code>:=</code> 是覆盖之前的值</li>\n<li><code>?=</code> 是如果没有被赋值过就赋予等号后面的值</li>\n<li><code>+=</code> 是添加等号后面的值</li>\n</ul>\n<h2>递归编译多目录</h2>\n<p>假设目标代码和可执行文件都在 <code>debug</code> 目录下,其他都是源代码文件,则根目录下 Makefile</p>\n<pre><code class=\"language-makefile\">#设置编译器\nCC=gcc\n#debug文件夹里的makefile文件需要最后执行,所以这里需要执行的子目录要排除debug文件夹,这里使用awk排除了debug文件夹,读取剩下的文件夹\nSUBDIRS=$(shell ls -l | grep ^d | awk '{if($$9 != "debug") print $$9}')\n#无需下一行的注释代码,因为我们已经知道debug里的makefile是最后执行的,所以最后直接去debug目录下执行指定的makefile文件就行,具体下面有注释\n#DEBUG=$(shell ls -l | grep ^d | awk '{if($$9 == "debug") print $$9}')\n#记住当前工程的根目录路径\nROOT_DIR=$(shell pwd)\n#最终bin文件的名字,可以更改为自己需要的\nBIN=myapp\n#目标文件所在的目录\nOBJS_DIR=debug/obj\n#bin文件所在的目录\nBIN_DIR=debug/bin\n#获取当前目录下的c文件集,放在变量CUR_SOURCE中\nCUR_SOURCE=${wildcard *.c}\n#将对应的c文件名转为o文件后放在下面的CUR_OBJS变量中\nCUR_OBJS=${patsubst %.c, %.o, $(CUR_SOURCE)}\n#将以下变量导出到子shell中,本次相当于导出到子目录下的makefile中\nexport CC BIN OBJS_DIR BIN_DIR ROOT_DIR\n#注意这里的顺序,需要先执行SUBDIRS最后才能是DEBUG\nall:$(SUBDIRS) $(CUR_OBJS) DEBUG\n#递归执行子目录下的makefile文件,这是递归执行的关键\n$(SUBDIRS):ECHO\n make -C $@\nDEBUG:ECHO\n #直接去debug目录下执行makefile文件\n make -C debug\nECHO:\n @echo $(SUBDIRS)\n#将c文件编译为o文件,并放在指定放置目标文件的目录中即OBJS_DIR\n$(CUR_OBJS):%.o:%.c\n $(CC) -c $^ -o $(ROOT_DIR)/$(OBJS_DIR)/$@\nCLEAN:\n @rm $(OBJS_DIR)/*.o\n @rm -rf $(BIN_DIR)/*\n</code></pre>\n<p>子目录 makefile</p>\n<pre><code class=\"language-makefile\">#子目录的Makefile直接读取其子目录就行,使用 grep 过滤出来目录(ls 后会以d为标识开头)\nSUBDIRS=$(shell ls -l | grep ^d | awk '{print $$9}')\n#以下同根目录下的makefile的相同代码的解释\nCUR_SOURCE=${wildcard *.c}\nCUR_OBJS=${patsubst %.c, %.o, $(CUR_SOURCE)}\nALL:$(SUBDIRS) $(CUR_OBJS)\n$(SUBDIRS):ECHO\n make -C $@\n$(CUR_OBJS):%.o:%.c\n $(CC) -c $^ -o $(ROOT_DIR)/$(OBJS_DIR)/$@\nECHO:\n @echo $(SUBDIRS)\n</code></pre>\n<p>生成的目标文件和可执行文件目录<code>debug</code>下 makefile</p>\n<pre><code class=\"language-makefile\">OBJS=*.o\nODIR=obj\n$(ROOT_DIR)/$(BIN_DIR)/$(BIN):$(ODIR)/$(OBJS)\n $(CC) -o $@ $^\n</code></pre>\n<h2>编译时添加宏定义</h2>\n<p>假设代码如下,检查是否存在</p>\n<pre><code class=\"language-c\">// test.c\n#include <stdio.h> \n#include <stdlib.h> \n \nint main(int argc, char* argv[]) \n{ \n \n#ifndef A_MACRO\n printf("\\n"); \n#endif \n \n return 0; \n}\n</code></pre>\n<p>命令行直接使用 gcc 编译时,可以传递 <code>-D</code> 参数来定义</p>\n<pre><code class=\"language-bash\">gcc test.c -D A_MACRO\n</code></pre>\n<p>在 Makefile 中,当然可以在 gcc 后面写 -D,也可以直接加在 <code>CFLAGS</code> 中,同时也可以用下面的方式给 make 传递参数决定是否定义该宏</p>\n<pre><code class=\"language-makefile\">MACROS=\nCFLAGS=-g $(MACROS)\nall:a.out\ng++ CFLAGS -o a.out\n</code></pre>\n<p>使用 make 时,便可以这样写</p>\n<pre><code class=\"language-bash\">make DEBUG='-D A_MACRO'\n</code></pre>\n<h2>杂项</h2>\n<ul>\n<li>\n<p>当没有指明目标时,make 默认执行 Makefile 中定义的第一个目标,也称为默认目标</p>\n</li>\n<li>\n<p>在 Makefile 中,使用 <code>@echo</code> 可以执行 <code>echo</code> 命令</p>\n</li>\n<li>\n<p>在 Makefile 中,目标后可以添加其他目标,表示依赖关系,即当前目标需要这些目标作为支持,会先构建依赖着的其他目标,再执行剩下的命令,即基本语法是</p>\n<pre><code class=\"language-makefile\">target: prerequisites\n\tcommand\n</code></pre>\n</li>\n<li>\n<p>直接使用 <code>=</code> 就可以在 Makefile 中定义变量如 <code>CC = gcc</code>,使用 <code>$(CC)</code> 可以读取它</p>\n</li>\n</ul>\n<h1>cmake 笔记</h1>\n<p>使用 cmake 读取 CMakeList.txt 可以自动生成需要的 Makefile。CMakeList 的语法:</p>\n<h2>指定安装目录</h2>\n<p>通过定义 <code>CMAKE_INSTALL_PREFIX:PATH</code> 更改安装路径,在无法获取 sudo 权限时候可以使用这种方式安到当前用户目录下?</p>\n<pre><code class=\"language-bash\">mkdir build\ncd build\ncmake -DCMAKE_INSTALL_PREFIX:PATH=$(realpath ../install) ..\nmake -j N install\n</code></pre>\n<p>例如这个例子中,通过 <code>realpath</code> 获取相对路径的绝对路径名,从而安装在上级的 install 目录下</p>\n<h2>多级目录、多个链接库</h2>\n<p>例如这里我有一个</p>\n<pre><code class=\"language-bash\">.\n├── main.c\n├── first.c\n├── first.h\n└── second\n</code></pre>\n<p>这样的文件目录,怎么在根目录下构建 <code>main.c</code> 呢?同时依赖于 <code>first.c</code> 文件和 <code>second</code> 目录下的两个库(均为 C 实现),同时 <code>first.c</code> 又依赖于 <code>second</code>目录下的库。</p>\n<p>在根目录下,编写</p>\n<pre><code class=\"language-cmake\">cmake_minimum_required (VERSION 3.10.2)\nproject (laser)\n\nset(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")\nadd_definitions(-DENABLE_DBG)\n\n# second lib\nadd_subdirectory(second) # add this directory to project, traverse it, and apply the `CMakeLists.txt` inside it\nlist(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/second")\nset_target_properties(second PROPERTIES LINKER_LANGUAGE C)\n\n# lib for first main logic\nadd_library(first first.c)\ntarget_link_libraries(first second) # built target for `first` lib would be dependent on on second lib\ntarget_include_directories(first PUBLIC\n ${PROJECT_BINARY_DIR}\n ${EXTRA_INCLUDES}\n ) # the headers in the second lib would be added to include path, so that we do not need to configure relative paths when include them in codes\n\n# main executable file\nset(SRC_LIST main.c)\nadd_executable(laser ${SRC_LIST})\ntarget_link_libraries(laser first)\n# or target_link_libraries(laser first second), up to your requirements on second lib\n</code></pre>\n<p>对 first 和 second 两个库和 main.c 对应的代码都构建编译 target,通过 <code>target_link_libraries</code> 去声明依赖关系,这样会自动先编译 second 库,再编译 first 库,把 second 库链接给它,再链接 first 库编译 main.c 对应的可执行文件。使用 cmake 就是一个声明依赖关系的过程,而且很多步骤 cmake 都会自动帮忙干。</p>\n<p>那么在 second 目录下,还需要加一个,定义一个名为 second 的库,并用 <code>file()</code> 函数方便地选取目录下所有文件</p>\n<pre><code class=\"language-cmake\">file(GLOB SOURCES *.c)\nfile(GLOB HEADERS *.h)\n\nadd_library(second ${SOURCES} ${HEADERS})\n</code></pre>\n<h1>Reference</h1>\n<p><a href=\"http://www.gnu.org/software/make/\">Make - GNU Project - Free Software Foundation</a></p>\n<p><a href=\"https://github.com/seisman/how-to-write-makefile\">seisman/how-to-write-makefile: 跟我一起写Makefile重制版 (github.com)</a></p>\n<p><a href=\"https://blog.csdn.net/u014470361/article/details/103447678\">gcc编译选项-fprofile-arcs -ftest-coverage之代码覆盖率_夜风的博客-CSDN博客</a></p>\n<p><a href=\"https://www.cnblogs.com/Shirlies/p/4282182.html\">多文件目录下makefile文件递归执行编译所有c文件 - Shirlies - 博客园 (cnblogs.com)</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210409/",
"title": "Makefile 笔记",
"summary": "人傻就要多记笔记",
"date_modified": "2021-04-09T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>eBPF 用户空间虚拟机实现相关</h1>\n<p>续上一篇<a href=\"/zh/blogs/20210311/\">BPF笔记</a>,这次记录一些实践相关的内容。不再区分classical BPF和eBPF,统称BPF。</p>\n<p>开发板相关的内容在<a href=\"/zh/blogs/20210223/\">另一篇笔记</a>。</p>\n<h2>Linux 内核相关</h2>\n<p>Linux 内核中的 BPF 实现有两个项目,分别都只有一条独立的master分支(避免维护时 rebase 起来混乱),<a href=\"https://kernel.googlesource.com/pub/scm/linux/kernel/git/bpf/bpf\">bpf</a> 分支和 <a href=\"https://kernel.googlesource.com/pub/scm/linux/kernel/git/bpf/bpf-next/\">bpf-next</a> 分支。类似于 net 分支和 net-next 分支,bpf 分支是较为固定的,用来做些修补工作;bpf-next 是用来开发一些之后的 feature,或者做改进用的分支。源码在<code>/kernel/bpf</code> 下。</p>\n<p>文档在 <code>/Documentation/bpf/btf.rst</code> 。</p>\n<p><code>/include/uapi/linux/bpf_common.h</code> 和 <code>/include/uapi/linux/bpf.h</code> 定义了指令集。</p>\n<p><code>/samples/bpf</code> 是一些 bpf 相关的样例(开发时的测试代码不应该放在这个目录下,因为逻辑可能比一般的样例复杂,需要测试边界情况),</p>\n<p><code>/tools/bpf/bpftool</code> 下面放一些用户空间用来 debug ebpf 程序的工具。</p>\n<p><code>/tools/testing/selftests/bpf</code> 下是测试代码。</p>\n<p>对 BPF 指令的测试写在 <code>/lib/test_bpf.c</code> 和 <code>/lib/test_verifier.c</code> 中。</p>\n<p>测试 BPF(相关文档为 <code>/Documentation/dev-tools/kselftest.rst</code> ):</p>\n<pre><code class=\"language-bash\">cd tools/testing/selftests/bpf/\nmake\nsudo ./test_verifier\n# This will generate `Summary: 418 PASSED, 0 FAILED`\n# to run all of the tests\nsudo make run_tests\n</code></pre>\n<p><code>llc --version</code> 查看 LLVM 是否支持 BPF:查看输出的 <code>Registered Targets</code>。</p>\n<p><code>llc -march bpf -mcpu=help</code> 可以用来查看和设置支持的 BPF 指令集。</p>\n<p>使用 <code>clang</code> 前端编译 C 代码 <code>test.c</code> 到中间 BPF 代码 <code>test.o</code>:</p>\n<pre><code class=\"language-bash\">clang -target bpf -c test.c -o test.o\n</code></pre>\n<p>用 <code>readelf</code> 去读<code>test.o</code>:</p>\n<pre><code class=\"language-bash\">readelf -a test.o\n</code></pre>\n<p>在内核中,BPF 相关的调用都是通过一个核心的系统调用 <code>bpf()</code> 来执行的,例如加载程序到内核、管理 BPF maps、管理 map entries、程序和 map 的持久化(通过 <a href=\"#Object-Pinning\">Pinning</a> 实现)等操作。</p>\n<p>在内核中的具体流程如下:</p>\n<p><img src=\"./bpf_inkernel.png\" alt=\"\"></p>\n<p><img src=\"./bpf_process.png\" alt=\"\"></p>\n<p>BPF 在内核中的执行都是事件驱动的,例如:</p>\n<ul>\n<li>挂载了 BPF 程序的网络设备将会在它接收到包的时候执行程序</li>\n<li>内核地址上挂载了 BPF 程序的 kprobe 时,只要该地址的程序执行了,就会触发 kprobe 的回调函数,接着触发 BPF 程序</li>\n</ul>\n<p>示意图如下:</p>\n<p><img src=\"./ebpf_event.png\" alt=\"\"></p>\n<h2>BPF 指令集</h2>\n<p>这部分翻译自 uBPF 的文档,它是上面提到的一个 BPF JIT Complier 的实现。</p>\n<p>BPF 使用了 11 个 64位寄存器,32位称为半寄存器(subregister)和一个程序计数器(program counter),一个大小为 512 字节的 BPF 栈。寄存器以 r0 - r10 命名,r0 - r9 是指令可以使用的寄存器。默认情况下,运行模式是 64 位的。</p>\n<p>BPF 程序指令都是64位的,假设 BPF 虚拟机指令集的编码顺序和宿主机的顺序相同,于是在下面的描述中不关心大端小端的问题。</p>\n<p>所有的 BPF 指令都有着相同的编码方式:</p>\n<pre><code>msb lsb\n+------------------------+----------------+----+----+--------+\n|immediate |offset |src |dst |opcode |\n+------------------------+----------------+----+----+--------+\n</code></pre>\n<p>从最低位到最高位分别是:</p>\n<ul>\n<li>8 位的 opcode,有 BPF_X 类型的基于寄存器的指令,也有 BPF_K 类型的基于立即数的指令</li>\n<li>4 位的目标寄存器 (dst)</li>\n<li>4 位的原始寄存器 (src)</li>\n<li>16 位的偏移(有符号),是相对于栈、映射值(map values)、数据包(packet data)等的相对偏移量</li>\n<li>32 位的立即数 (imm)(有符号)</li>\n</ul>\n<p>大多数指令都不会使用所有的域,未被使用的部分就会被置为0。</p>\n<p>opcode 中最低的 3 位是 <code>instruction class</code> (<code>cls</code>),将 opcodes 分了组,具体为下面几种</p>\n<p>LD/LDX/ST/STX opcode 具有如下结构</p>\n<pre><code>msb lsb\n+---+--+---+\n|mde|sz|cls|\n+---+--+---+\n</code></pre>\n<p>其中两位的 <code>sz</code> 域指定了内存位置的大小,3 位的 <code>mde</code> 域是内存获取模式,uBPF 只支持通用的 "MEM" 获取方式。</p>\n<p>ALU/ALU64/JMP opcode 具有如下结构</p>\n<pre><code>msb lsb\n+----+-+---+\n|op |s|cls|\n+----+-+---+\n</code></pre>\n<p>如果 <code>s</code> 位是 0,那么 source operand 就会是指令中立即数 <code>imm</code> 对应的域。如果 <code>s</code> 是 1,那么 source operand 就会是指令中 <code>src</code> 域。4 位的 <code>op</code> 域则指定了将要执行哪个 ALU 或 branch 操作。</p>\n<h3>ALU 指令</h3>\n<h4>64-bit</h4>\n<table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Mnemonic</th>\n<th>Pseudocode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x07</td>\n<td>add dst, imm</td>\n<td>dst += imm</td>\n</tr>\n<tr>\n<td>0x0f</td>\n<td>add dst, src</td>\n<td>dst += src</td>\n</tr>\n<tr>\n<td>0x17</td>\n<td>sub dst, imm</td>\n<td>dst -= imm</td>\n</tr>\n<tr>\n<td>0x1f</td>\n<td>sub dst, src</td>\n<td>dst -= src</td>\n</tr>\n<tr>\n<td>0x27</td>\n<td>mul dst, imm</td>\n<td>dst *= imm</td>\n</tr>\n<tr>\n<td>0x2f</td>\n<td>mul dst, src</td>\n<td>dst *= src</td>\n</tr>\n<tr>\n<td>0x37</td>\n<td>div dst, imm</td>\n<td>dst /= imm</td>\n</tr>\n<tr>\n<td>0x3f</td>\n<td>div dst, src</td>\n<td>dst /= src</td>\n</tr>\n<tr>\n<td>0x47</td>\n<td>or dst, imm</td>\n<td>dst |= imm</td>\n</tr>\n<tr>\n<td>0x4f</td>\n<td>or dst, src</td>\n<td>dst |= src</td>\n</tr>\n<tr>\n<td>0x57</td>\n<td>and dst, imm</td>\n<td>dst &= imm</td>\n</tr>\n<tr>\n<td>0x5f</td>\n<td>and dst, src</td>\n<td>dst &= src</td>\n</tr>\n<tr>\n<td>0x67</td>\n<td>lsh dst, imm</td>\n<td>dst <<= imm</td>\n</tr>\n<tr>\n<td>0x6f</td>\n<td>lsh dst, src</td>\n<td>dst <<= src</td>\n</tr>\n<tr>\n<td>0x77</td>\n<td>rsh dst, imm</td>\n<td>dst >>= imm (logical)</td>\n</tr>\n<tr>\n<td>0x7f</td>\n<td>rsh dst, src</td>\n<td>dst >>= src (logical)</td>\n</tr>\n<tr>\n<td>0x87</td>\n<td>neg dst</td>\n<td>dst = -dst</td>\n</tr>\n<tr>\n<td>0x97</td>\n<td>mod dst, imm</td>\n<td>dst %= imm</td>\n</tr>\n<tr>\n<td>0x9f</td>\n<td>mod dst, src</td>\n<td>dst %= src</td>\n</tr>\n<tr>\n<td>0xa7</td>\n<td>xor dst, imm</td>\n<td>dst ^= imm</td>\n</tr>\n<tr>\n<td>0xaf</td>\n<td>xor dst, src</td>\n<td>dst ^= src</td>\n</tr>\n<tr>\n<td>0xb7</td>\n<td>mov dst, imm</td>\n<td>dst = imm</td>\n</tr>\n<tr>\n<td>0xbf</td>\n<td>mov dst, src</td>\n<td>dst = src</td>\n</tr>\n<tr>\n<td>0xc7</td>\n<td>arsh dst, imm</td>\n<td>dst >>= imm (arithmetic)</td>\n</tr>\n<tr>\n<td>0xcf</td>\n<td>arsh dst, src</td>\n<td>dst >>= src (arithmetic)</td>\n</tr>\n</tbody>\n</table>\n<h4>32-bit</h4>\n<p>这些指令只使用低的 32 位,并且会将目标寄存器的高 32 位置零。</p>\n<table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Mnemonic</th>\n<th>Pseudocode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x04</td>\n<td>add32 dst, imm</td>\n<td>dst += imm</td>\n</tr>\n<tr>\n<td>0x0c</td>\n<td>add32 dst, src</td>\n<td>dst += src</td>\n</tr>\n<tr>\n<td>0x14</td>\n<td>sub32 dst, imm</td>\n<td>dst -= imm</td>\n</tr>\n<tr>\n<td>0x1c</td>\n<td>sub32 dst, src</td>\n<td>dst -= src</td>\n</tr>\n<tr>\n<td>0x24</td>\n<td>mul32 dst, imm</td>\n<td>dst *= imm</td>\n</tr>\n<tr>\n<td>0x2c</td>\n<td>mul32 dst, src</td>\n<td>dst *= src</td>\n</tr>\n<tr>\n<td>0x34</td>\n<td>div32 dst, imm</td>\n<td>dst /= imm</td>\n</tr>\n<tr>\n<td>0x3c</td>\n<td>div32 dst, src</td>\n<td>dst /= src</td>\n</tr>\n<tr>\n<td>0x44</td>\n<td>or32 dst, imm</td>\n<td>dst |= imm</td>\n</tr>\n<tr>\n<td>0x4c</td>\n<td>or32 dst, src</td>\n<td>dst |= src</td>\n</tr>\n<tr>\n<td>0x54</td>\n<td>and32 dst, imm</td>\n<td>dst &= imm</td>\n</tr>\n<tr>\n<td>0x5c</td>\n<td>and32 dst, src</td>\n<td>dst &= src</td>\n</tr>\n<tr>\n<td>0x64</td>\n<td>lsh32 dst, imm</td>\n<td>dst <<= imm</td>\n</tr>\n<tr>\n<td>0x6c</td>\n<td>lsh32 dst, src</td>\n<td>dst <<= src</td>\n</tr>\n<tr>\n<td>0x74</td>\n<td>rsh32 dst, imm</td>\n<td>dst >>= imm (logical)</td>\n</tr>\n<tr>\n<td>0x7c</td>\n<td>rsh32 dst, src</td>\n<td>dst >>= src (logical)</td>\n</tr>\n<tr>\n<td>0x84</td>\n<td>neg32 dst</td>\n<td>dst = -dst</td>\n</tr>\n<tr>\n<td>0x94</td>\n<td>mod32 dst, imm</td>\n<td>dst %= imm</td>\n</tr>\n<tr>\n<td>0x9c</td>\n<td>mod32 dst, src</td>\n<td>dst %= src</td>\n</tr>\n<tr>\n<td>0xa4</td>\n<td>xor32 dst, imm</td>\n<td>dst ^= imm</td>\n</tr>\n<tr>\n<td>0xac</td>\n<td>xor32 dst, src</td>\n<td>dst ^= src</td>\n</tr>\n<tr>\n<td>0xb4</td>\n<td>mov32 dst, imm</td>\n<td>dst = imm</td>\n</tr>\n<tr>\n<td>0xbc</td>\n<td>mov32 dst, src</td>\n<td>dst = src</td>\n</tr>\n<tr>\n<td>0xc4</td>\n<td>arsh32 dst, imm</td>\n<td>dst >>= imm (arithmetic)</td>\n</tr>\n<tr>\n<td>0xcc</td>\n<td>arsh32 dst, src</td>\n<td>dst >>= src (arithmetic)</td>\n</tr>\n</tbody>\n</table>\n<h4>Byteswap 指令</h4>\n<table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Mnemonic</th>\n<th>Pseudocode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0xd4 (imm == 16)</td>\n<td>le16 dst</td>\n<td>dst = htole16(dst)</td>\n</tr>\n<tr>\n<td>0xd4 (imm == 32)</td>\n<td>le32 dst</td>\n<td>dst = htole32(dst)</td>\n</tr>\n<tr>\n<td>0xd4 (imm == 64)</td>\n<td>le64 dst</td>\n<td>dst = htole64(dst)</td>\n</tr>\n<tr>\n<td>0xdc (imm == 16)</td>\n<td>be16 dst</td>\n<td>dst = htobe16(dst)</td>\n</tr>\n<tr>\n<td>0xdc (imm == 32)</td>\n<td>be32 dst</td>\n<td>dst = htobe32(dst)</td>\n</tr>\n<tr>\n<td>0xdc (imm == 64)</td>\n<td>be64 dst</td>\n<td>dst = htobe64(dst)</td>\n</tr>\n</tbody>\n</table>\n<h3>Memory 指令</h3>\n<table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Mnemonic</th>\n<th>Pseudocode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x18</td>\n<td>lddw dst, imm</td>\n<td>dst = imm</td>\n</tr>\n<tr>\n<td>0x20</td>\n<td>ldabsw src, dst, imm</td>\n<td>See kernel documentation</td>\n</tr>\n<tr>\n<td>0x28</td>\n<td>ldabsh src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x30</td>\n<td>ldabsb src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x38</td>\n<td>ldabsdw src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x40</td>\n<td>ldindw src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x48</td>\n<td>ldindh src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x50</td>\n<td>ldindb src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x58</td>\n<td>ldinddw src, dst, imm</td>\n<td>...</td>\n</tr>\n<tr>\n<td>0x61</td>\n<td>ldxw dst, [src+off]</td>\n<td>dst = *(uint32_t *) (src + off)</td>\n</tr>\n<tr>\n<td>0x69</td>\n<td>ldxh dst, [src+off]</td>\n<td>dst = *(uint16_t *) (src + off)</td>\n</tr>\n<tr>\n<td>0x71</td>\n<td>ldxb dst, [src+off]</td>\n<td>dst = *(uint8_t *) (src + off)</td>\n</tr>\n<tr>\n<td>0x79</td>\n<td>ldxdw dst, [src+off]</td>\n<td>dst = *(uint64_t *) (src + off)</td>\n</tr>\n<tr>\n<td>0x62</td>\n<td>stw [dst+off], imm</td>\n<td>*(uint32_t *) (dst + off) = imm</td>\n</tr>\n<tr>\n<td>0x6a</td>\n<td>sth [dst+off], imm</td>\n<td>*(uint16_t *) (dst + off) = imm</td>\n</tr>\n<tr>\n<td>0x72</td>\n<td>stb [dst+off], imm</td>\n<td>*(uint8_t *) (dst + off) = imm</td>\n</tr>\n<tr>\n<td>0x7a</td>\n<td>stdw [dst+off], imm</td>\n<td>*(uint64_t *) (dst + off) = imm</td>\n</tr>\n<tr>\n<td>0x63</td>\n<td>stxw [dst+off], src</td>\n<td>*(uint32_t *) (dst + off) = src</td>\n</tr>\n<tr>\n<td>0x6b</td>\n<td>stxh [dst+off], src</td>\n<td>*(uint16_t *) (dst + off) = src</td>\n</tr>\n<tr>\n<td>0x73</td>\n<td>stxb [dst+off], src</td>\n<td>*(uint8_t *) (dst + off) = src</td>\n</tr>\n<tr>\n<td>0x7b</td>\n<td>stxdw [dst+off], src</td>\n<td>*(uint64_t *) (dst + off) = src</td>\n</tr>\n</tbody>\n</table>\n<h3>Branch 指令</h3>\n<table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Mnemonic</th>\n<th>Pseudocode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x05</td>\n<td>ja +off</td>\n<td>PC += off</td>\n</tr>\n<tr>\n<td>0x15</td>\n<td>jeq dst, imm, +off</td>\n<td>PC += off if dst == imm</td>\n</tr>\n<tr>\n<td>0x1d</td>\n<td>jeq dst, src, +off</td>\n<td>PC += off if dst == src</td>\n</tr>\n<tr>\n<td>0x25</td>\n<td>jgt dst, imm, +off</td>\n<td>PC += off if dst > imm</td>\n</tr>\n<tr>\n<td>0x2d</td>\n<td>jgt dst, src, +off</td>\n<td>PC += off if dst > src</td>\n</tr>\n<tr>\n<td>0x35</td>\n<td>jge dst, imm, +off</td>\n<td>PC += off if dst >= imm</td>\n</tr>\n<tr>\n<td>0x3d</td>\n<td>jge dst, src, +off</td>\n<td>PC += off if dst >= src</td>\n</tr>\n<tr>\n<td>0xa5</td>\n<td>jlt dst, imm, +off</td>\n<td>PC += off if dst < imm</td>\n</tr>\n<tr>\n<td>0xad</td>\n<td>jlt dst, src, +off</td>\n<td>PC += off if dst < src</td>\n</tr>\n<tr>\n<td>0xb5</td>\n<td>jle dst, imm, +off</td>\n<td>PC += off if dst <= imm</td>\n</tr>\n<tr>\n<td>0xbd</td>\n<td>jle dst, src, +off</td>\n<td>PC += off if dst <= src</td>\n</tr>\n<tr>\n<td>0x45</td>\n<td>jset dst, imm, +off</td>\n<td>PC += off if dst & imm</td>\n</tr>\n<tr>\n<td>0x4d</td>\n<td>jset dst, src, +off</td>\n<td>PC += off if dst & src</td>\n</tr>\n<tr>\n<td>0x55</td>\n<td>jne dst, imm, +off</td>\n<td>PC += off if dst != imm</td>\n</tr>\n<tr>\n<td>0x5d</td>\n<td>jne dst, src, +off</td>\n<td>PC += off if dst != src</td>\n</tr>\n<tr>\n<td>0x65</td>\n<td>jsgt dst, imm, +off</td>\n<td>PC += off if dst > imm (signed)</td>\n</tr>\n<tr>\n<td>0x6d</td>\n<td>jsgt dst, src, +off</td>\n<td>PC += off if dst > src (signed)</td>\n</tr>\n<tr>\n<td>0x75</td>\n<td>jsge dst, imm, +off</td>\n<td>PC += off if dst >= imm (signed)</td>\n</tr>\n<tr>\n<td>0x7d</td>\n<td>jsge dst, src, +off</td>\n<td>PC += off if dst >= src (signed)</td>\n</tr>\n<tr>\n<td>0xc5</td>\n<td>jslt dst, imm, +off</td>\n<td>PC += off if dst < imm (signed)</td>\n</tr>\n<tr>\n<td>0xcd</td>\n<td>jslt dst, src, +off</td>\n<td>PC += off if dst < src (signed)</td>\n</tr>\n<tr>\n<td>0xd5</td>\n<td>jsle dst, imm, +off</td>\n<td>PC += off if dst <= imm (signed)</td>\n</tr>\n<tr>\n<td>0xdd</td>\n<td>jsle dst, src, +off</td>\n<td>PC += off if dst <= src (signed)</td>\n</tr>\n<tr>\n<td>0x85</td>\n<td>call imm</td>\n<td>Function call</td>\n</tr>\n<tr>\n<td>0x95</td>\n<td>exit</td>\n<td>return r0</td>\n</tr>\n</tbody>\n</table>\n<p>更加详细的协议参考<a href=\"#Reference\">内核 bpf 相关文档</a></p>\n<h2>常见的 bpf_prog_type</h2>\n<table>\n<thead>\n<tr>\n<th><strong>bpf_prog_type</strong></th>\n<th><strong>BPF prog</strong> 入口参数(R1)</th>\n<th><strong>程序类型</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>BPF_PROG_TYPE_SOCKET_FILTER</strong></td>\n<td><strong>struct __sk_buff</strong></td>\n<td>用于过滤进出口网络报文,功能上和 cBPF 类似。</td>\n</tr>\n<tr>\n<td><strong>BPF_PROG_TYPE_KPROBE</strong></td>\n<td><strong>struct</strong> <strong>pt_regs</strong></td>\n<td>用于 kprobe 功能的 BPF 代码。</td>\n</tr>\n<tr>\n<td><strong>BPF_PROG_TYPE_TRACEPOINT</strong></td>\n<td>这类 BPF 的参数比较特殊,根据 tracepoint 位置的不同而不同。</td>\n<td>用于在各个 tracepoint 节点运行。</td>\n</tr>\n<tr>\n<td><strong>BPF_PROG_TYPE_XDP</strong></td>\n<td><strong>struct</strong> <strong>xdp_md</strong></td>\n<td>用于控制 XDP(eXtreme Data Path)的 BPF 代码。</td>\n</tr>\n<tr>\n<td><strong>BPF_PROG_TYPE_PERF_EVENT</strong></td>\n<td><strong>struct bpf_perf_event_data</strong></td>\n<td>用于定义 perf event 发生时回调的 BPF 代码。</td>\n</tr>\n<tr>\n<td><strong>BPF_PROG_TYPE_CGROUP_SKB</strong></td>\n<td><strong>struct __sk_buff</strong></td>\n<td>用于在 network cgroup 中运行的 BPF 代码。功能上和 Socket_Filter 近似。具体用法可以参考范例 test_cgrp2_attach。</td>\n</tr>\n<tr>\n<td><strong>BPF_PROG_TYPE_CGROUP_SOCK</strong></td>\n<td><strong>struct bpf_sock</strong></td>\n<td>另一个用于在 network cgroup 中运行的 BPF 代码,范例 test_cgrp2_sock2 中就展示了一个利用 BPF 来控制 host 和 netns 间通信的例子。</td>\n</tr>\n</tbody>\n</table>\n<p>与网络相关的有XDP、socket、tc等。</p>\n<p>事实上 BPF 程序类型就是由 BPF side 的代码的函数参数确定的,比如写了一个函数,参数是 <code>struct __sk_buff</code> 类型的,它就是一个 <strong>BPF_PROG_TYPE_SOCKET_FILTER</strong> 类型的 BPF 程序。</p>\n<h2>其他的 BPF 相关概念</h2>\n<h3>Helper Function</h3>\n<p>BPF 系统调用的原型如下:</p>\n<pre><code class=\"language-c\">#include <linux/bpf.h>\nint bpf(int cmd, union bpf_attr *attr, unsigned int size);\n</code></pre>\n<p>bpf系统调用要执行的操作由 <code>cmd</code> 参数确定。每个操作都有一个附带的参数,通过 <code>attr</code> 提供,<code>size</code> 参数是 <code>attr</code> 的大小。</p>\n<p><code>cmd</code> 参数可以是一系列 helper 的宏定义,如 BPF_MAP_CREATE、 BPF_MAP_LOOKUP_ELEM、BPF_MAP_UPDATE_ELEM、BPF_MAP_DELETE_ELEM、BPF_MAP_GET_NEXT_KEY、BPF_PROG_LOAD;分别是对应 map 创建、map 中元素查找、map 中更新元素、map 中删除元素、获取相邻的 key、加载 bpf 程序。<code>bpf_attr</code> 则相当复杂,不展开说了,实际上也很少直接使用该系统调用,都是使用 BPF helper 函数的。</p>\n<p>Helper functions 是一些与内核交互的常用 API, 可以从内核获取或写入数据。不同的 BPF 程序类型 <code>bpf_prog_type</code> 可用的 helper function 不同,例如与 socket 有关的 BPF 函数就只允许使用一小部分的 helpers,而与 tc (traffic control 的缩写,更加靠近传输层,而且此时包都还没解析)相关的 BPF 函数可以调用更多的 helpers。</p>\n<p>内核将 helper 函数抽象到了宏 <code>BPF_CALL_0()</code> 到 <code>BPF_CALL_5()</code>,让定义过程更加简单,更加可读。下面这个例子就节选自一个用来更新映射元素的 helper function,它调用了映射上实现的相关的回调函数</p>\n<pre><code class=\"language-c\">// 注意这是一个宏\nBPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key,\n void *, value, u64, flags)\n{\n WARN_ON_ONCE(!rcu_read_lock_held());\n return map->ops->map_update_elem(map, key, value, flags);\n}\n\nconst struct bpf_func_proto bpf_map_update_elem_proto = {\n .func = bpf_map_update_elem,\n .gpl_only = false,\n .ret_type = RET_INTEGER,\n .arg1_type = ARG_CONST_MAP_PTR,\n .arg2_type = ARG_PTR_TO_MAP_KEY,\n .arg3_type = ARG_PTR_TO_MAP_VALUE,\n .arg4_type = ARG_ANYTHING,\n};\n\n</code></pre>\n<p>这种提前定义宏的好处是:在 cBPF 中,当添加新的 helper时,JIT 需要对这种扩展做支持;但是在 eBPF 中,底层的 BPF 寄存器的分配已经提前做好了,JIT 此时只需要触发一个调用指令。因此 eBPF 的 helper functions 更容易拓展。</p>\n<p>helper functions 数量众多,可以参考附录中的<a href=\"https://github.com/iovisor/bpf-docs/blob/master/bpf_helpers.rst/\">eBPF-Helpers</a>。</p>\n<h3>Verifier</h3>\n<p>上面 helper function 代码中的 <code>bpf_func_proto</code> 是用来传递 verifier 的必要数据的,用来验证 helper 提供的参数类型和当下 BPF 寄存器中的内容符合。参数类型可以是任意的值,也可以是针对一个 buffer 的 <指针,大小> 的数据对,告诉 helper 它应该从哪里读取数据或写入数据, verifier 会验证 buffer 之前是否有被初始化过。</p>\n<p>In-kernel Verifier还会验证:</p>\n<ul>\n<li>对注入代码进行一次 DAG(Directed Acyclic Graph,有向无环图)检测,以保证其中没有循环存在</li>\n<li>eBPF 的代码长度上限为 4096 条 BPF 指令(在内核 5.1 版本以上,这个上限提升到了 100 万)。</li>\n<li>存在可能会跳出 eBPF 代码范围的 JMP</li>\n<li>分支(branch)不允许超过 1024 个;经检测的指令数也必须在 96K 以内</li>\n<li>支持运行尾调用(tail calls),即在 eBPF 程序末尾调用另一个 eBPF 程序,但是也是有限制的,最多可以嵌套 33 次 尾调用</li>\n<li>BPF helper 函数中最多允许5个函数参数</li>\n</ul>\n<p>verifier执行的第一个检查是对VM将要加载的代码的静态分析。第一次检查的目的是确保程序有一个预期的结束。为此,verifier 使用代码创建一个直接非循环图(DAG)。verifier 分析的每条指令都成为图中的一个节点,每个节点都链接到下一条指令。在 verifier 生成这个图之后,它将执行深度优先搜索(DFS),以确保程序完成并且代码不包含危险路径。这意味着它将遍历图的每个分支,一直遍历到分支的底部,以确保没有递归循环。</p>\n<p>verifier 执行的第二个检查是BPF程序的一个空运行。这意味着verifier将尝试分析程序将要执行的每条指令,以确保它不会执行任何无效的指令。此执行还检查是否正确访问和取消引用了所有内存指针。最后,空运行将程序中的控制流通知 verifier,以确保无论程序采用哪种控制路径,它都会到达 BPF_EXIT 指令。为此,verifier 跟踪堆栈中所有已访问的分支路径,在选择新路径之前对其进行评估,以确保不会多次访问特定路径。在这两个检查通过之后,verifier 认为程序可以安全地执行。</p>\n<p>bpf 系统调用允许您 debug verifier 的检查,如果您对分析程序如何被分析感兴趣的话。使用此系统调用加载程序时,可以设置几个属性,使验证器打印其操作日志。</p>\n<h3>Maps</h3>\n<p>BPF 映射是内核中的键值型数据结构,他们能够从 BPF 程序中获取,来在不同的 BPF 调用之间传递状态信息。他们也能通过用户空间的文件描述符来获取,也能在 BPF 程序或用户空间的应用之间共享。注意共享 BPF 映射的 BPF 程序不一定是相同的程序类型,例如,一个 tracing 程序也可能可以和网络程序共享映射,当下一个单独的 BPF 程序最多可以同时获取到 64 个不同的映射。</p>\n<p><img src=\"./bpf_map.png\" alt=\"bpf_map.png\"></p>\n<p>映射的实现是在内核中,有通用的,也有仅能在少数 helper functions 中使用的。通用的映射有<code>BPF_MAP_TYPE_HASH</code>, <code>BPF_MAP_TYPE_ARRAY</code>, <code>BPF_MAP_TYPE_PERCPU_HASH</code>, <code>BPF_MAP_TYPE_PERCPU_ARRAY</code>, <code>BPF_MAP_TYPE_LRU_HASH</code>, <code>BPF_MAP_TYPE_LRU_PERCPU_HASH</code> and <code>BPF_MAP_TYPE_LPM_TRIE</code>。他们都是用的是相同的 BPF helper functions 来实现增删查改,同时针对不同的语义和应用特征实现了不同的后端操作。</p>\n<p>现有的不通用的映射有 <code>BPF_MAP_TYPE_PROG_ARRAY</code>, <code>BPF_MAP_TYPE_PERF_EVENT_ARRAY</code>, <code>BPF_MAP_TYPE_CGROUP_ARRAY</code>, <code>BPF_MAP_TYPE_STACK_TRACE</code>, <code>BPF_MAP_TYPE_ARRAY_OF_MAPS</code>, <code>BPF_MAP_TYPE_HASH_OF_MAPS</code>。例如,<code>BPF_MAP_TYPE_PROG_ARRAY</code> 是一个包含有其他 BPF 程序的数组, <code>BPF_MAP_TYPE_ARRAY_OF_MAPS</code> 和 <code>BPF_MAP_TYPE_HASH_OF_MAPS</code> 都持有着指向其他映射的指针,为了在运行时能原子地更换 BPF 映射。通过这种实现满足了这个特殊的需求。因为状态是需要在不同的 BPF 程序调用中共享的,所以不能单独通过一个 BPF helper function实现,而是要用这种方法。</p>\n<p>补充 MAP 类型细节:</p>\n<ul>\n<li>BPF_MAP_TYPE_HASH:第一个添加到BPF的通用 map。它们的实现和用法类似于您可能熟悉的其他哈希表。</li>\n<li>BPF_MAP_TYPE_ARRAY:添加到内核的第二种 BPF 映射。它的所有元素都在内存中预先分配并设置为零值。键是数组中的索引,它们的大小必须正好是4个字节。使用 Array maps 的一个缺点是不能删除map中的元素。如果尝试在 array maps 上使用map_delete_elem,则调用将失败,结果将导致错误 EINVAL。</li>\n<li>BPF_MAP_TYPE_PROG_ARRAY:您可以使用这种类型的 map,来存储BPF程序的文件描述。与 bpf_tail_call 调用结合,此 map 允许您在程序之间跳转,绕过单个 bpf 程序的最大指令限制,并降低实现复杂性。</li>\n<li>BPF_MAP_TYPE_PERF_EVENT_ARRAY:这些类型的 map 将 perf_events 数据存储在缓冲环中,缓冲环在BPF程序和用户空间程序之间实时通信。它们被设计用来将内核的跟踪工具发出的事件转发给用户空间程序进行进一步处理。</li>\n<li>BPF_MAP_TYPE_PERCPU_HASH:Per-CPU Hash Maps 是 BPF_MAP_TYPE_HASH 的改进版本。当您分配其中一个 map 时,每个CPU都会看到自己独立的map版本,这使得它更高效地进行高性能的查找和聚合。如果您的BPF程序收集度量并将它们聚合到 hash-table maps中,那么这种map非常有用。</li>\n<li>BPF_MAP_TYPE_PERCPU_ARRAY:Per-CPU Array Maps 是 BPF_MAP_TYPE_ARRAY 的改进版本。当您分配这些 map 中的一个时,每个CPU都会看到自己的独立版本的 map,这使得它更高效地进行高性能的查找和聚合。</li>\n<li>BPF_MAP_TYPE_STACK_TRACE:这种类型的 map 存储运行进程的堆栈跟踪。除了这个 map 之外,内核开发人员已经添加了帮助函数bpf_get_stackid来帮助您用堆栈跟踪填充这个映射。</li>\n<li>BPF_MAP_TYPE_CGROUP_ARRAY:这种类型的 map 存储指向 cgroup 的文件描述符标识符。<br>\nBPF_MAP_TYPE_LRU_HASH and BPF_MAP_TYPE_LRU_PERCPU_HASH:如果 map 满了,删除不常使用的 map,为新元素提供空间。percpu 则是针对每个 cpu。</li>\n<li>BPF_MAP_TYPE_LPM_TRIE:一个匹配最长前缀的字典树数据结构,适用于将 IP 地址匹配到一个范围。这些 map 要求其 key 大小为 8 的倍数,范围为 8 到 2048。如果您不想实现自己的 key,那么内核提供了一个可以用于这些 key 的结构,称为 bpf_lpm_trie_key。</li>\n<li>BPF_MAP_TYPE_ARRAY_OF_MAPS and BPF_MAP_TYPE_HASH_OF_MAPS:存储对其他映射的引用的两种类型的映射。它们只支持一个级别的间接寻址。</li>\n<li>BPF_MAP_TYPE_DEVMAP:存储对网络设备的引用。可以构建指向特定网络设备的端口的虚拟映射,然后使用 bpf_redirect_map 重定向数据包。</li>\n<li>BPF_MAP_TYPE_CPUMAP:可以将数据包转发到不同的 cpu</li>\n<li>BPF_MAP_TYPE_XSKMAP:一种存储对打开的套接字的引用的映射类型。用于套接字转发。</li>\n<li>BPF_MAP_TYPE_SOCKMAP和BPF_MAP_TYPE_SOCKHASH 是两个专门的 map。它们存储对内核中打开的套接字的引用。与前面的映射一样,这种类型的映射与 bpf_redirect_map 一起使用,将套接字缓冲区从当前 XDP 程序转发到不同的套接字。</li>\n<li>BPF_MAP_TYPE_CGROUP_STORAGE 和 BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE:略</li>\n<li>BPF_MAP_TYPE_REUSEPORT_SOCKARRAY:略</li>\n<li>BPF_MAP_TYPE_QUEUE:队列映射使用先进先出(FIFO)存储来保存映射中的元素。它们是用 BPF_MAP_TYPE_QUEUE 类型定义的。FIFO 意味着,当您从映射中获取一个元素时,结果将是映射中存在时间最长的元素。<br>\nBPF_MAP_TYPE_STACK:栈映射使用后进先出(LIFO)存储来保存映射中的元素。它们是用类型 BPF_MAP_TYPE_STACK 定义的。后进先出意味着,当您从映射中获取元素时,结果将是最近添加到映射中的元素。</li>\n</ul>\n<h3>Object Pinning</h3>\n<p>BPF 映射和程序是一种内核资源,只能通过文件描述符相关的 API 来获取它们。更底层的是匿名的索引节点(内核中的 inodes)。这种做法有许多优势和劣势:</p>\n<p>用户空间的应用程序能够使用大多数文件描述符相关的 API,文件描述符在 Unix 内部的 socket 通讯中是透明的,但是与此同时,文件描述符又只能在程序的生命周期内使用,这使得映射的共享难以实现。</p>\n<p>因此它带来了一系列棘手的复杂情况,例如在 iproute2 中,tc 和 XDP 在内核中启动并加载后最终会消亡掉。这样不可能从用户空间获取映射了。但是从用户空间获取映射是很有用的,例如,当映射在 data path 的入口位置和出口位置之间共享时。同时,第三方应用也可能想要在 BPF 程序运行时去监测或更新映射内容。</p>\n<p><img src=\"bpf_fs.png\" alt=\"\"></p>\n<p>为了突破这种限制,实现了一小部分内核空间的 BPF 文件系统。在这里, BPF 映射和程序能够被钉在上面,也叫作 object pinning。BPF 系统调用也进行了一定拓展,支持了两个新的命令,可以支持钉上(<code>BPF_OBJ_PIN</code>)或获取(<code>BPF_OBJ_GET</code>)这两种行为。</p>\n<p>BPF 的文件系统不是单例,它支持挂载多个实例、硬或软链接。</p>\n<h3>Tail Calls</h3>\n<p>尾调用能够允许一个 BPF 程序去调用另一个,不需要返回旧的程序中。这种调用方式开销很小,和函数调用不同,它在是用 long jump 命令实现的,可以直接复用相同的栈帧(即常见的尾递归优化)。</p>\n<p><img src=\"./bpf_tailcall.png\" alt=\"\"></p>\n<p>这样的程序能够独立进行验证。但是只有相同类型的 BPF 程序(即 bpf_prog_type 相同)才能进行尾调用;同时在 JIT 方面也要匹配, 即尾调用中 JIT 代码只能接在 JIT 代码后,直接解释的代码只能接在直接解释的代码后,不能在另一类代码后进行尾调用。</p>\n<p>尾调用相关操作有两部分:第一部分是建立一个叫做程序数组的特殊的映射(<code>BPF_MAP_TYPE_PROG_ARRAY</code>),能在用户空间进行键值数据的修改,值就是尾调用 BPF 程序的文件描述符。第二部分是<code>bpf_tail_call()</code>帮助函数,它需要传入上下文(context)、程序数组的引用、和想查询的键。内核会把这个帮助函数内联到特定的 BPF 指令中。这个程序数组在用户空间是 write-only 的。</p>\n<p>内核根据提供的文件描述符查找相关的 BPF 程序,原子地替换掉给定的映射槽处的程序指针。当在映射中按照所给键值找不到指针时,内核会跳过它然后接着执行<code>bpf_tail_call()</code> 之后的函数。尾调用很有用,例如在解析网络数据包时,就可以使用尾调用逐层解析。</p>\n<h3>BPF to BPF Calls</h3>\n<p><img src=\"./bpf_call.png\" alt=\"\"></p>\n<p>除了 BPF helper 函数和 BPF 尾调用,一个新添加的特性是 BPF 程序调用 BPF 程序。在这个特性被引入内核之前,一般 BPF C 程序需要定义一些可重用代码,单独存在一个头文件中,他们都是 <code>always_inline</code> 的。使用 LLVM 进行编译的时候,这些可重用代码会被内联到需要的地方,导致生成的代码体积很大。具体形式如下:</p>\n<pre><code class=\"language-C\">#include <linux/bpf.h>\n\n#ifndef __section\n# define __section(NAME) \\\n __attribute__((section(NAME), used))\n#endif\n\n#ifndef __inline\n# define __inline \\\n inline __attribute__((always_inline))\n#endif\n\nstatic __inline int foo(void)\n{\n return XDP_DROP;\n}\n\n__section("prog")\nint xdp_drop(struct xdp_md *ctx)\n{\n return foo();\n}\n\nchar __license[] __section("license") = "GPL";\n</code></pre>\n<p>这么干的原因就是在 BPF 验证器、加载器、解释器、JIT 中都缺少对函数的支持。从Linux 4.16, LLVM 6.0 开始,就支持这个特性了。BPF 程序不必再到处使用 <code>always_inline</code> 标签了,前面的代码可以被更自然地表达为</p>\n<pre><code class=\"language-C\">#include <linux/bpf.h>\n\n#ifndef __section\n# define __section(NAME) \\\n __attribute__((section(NAME), used))\n#endif\n\nstatic int foo(void)\n{\n return XDP_DROP;\n}\n\n__section("prog")\nint xdp_drop(struct xdp_md *ctx)\n{\n return foo();\n}\n\nchar __license[] __section("license") = "GPL";\n</code></pre>\n<p>主流的 BPF JIT 编译器例如在 <code>x86_64</code> 和 <code>arm64</code> 上的会支持 BPF 到 BPF 的调用,其他则会添加类似的特性。这个特性对性能优化意义重大,因为它显著减少了生成的 BPF 代码的大小。因此从指令缓存的角度,这样生成的机器码对 CPU 更加友好。</p>\n<p>BPF 函数之间的调用方法和 BPF helper 函数的调用方法一致,意味着从 <code>r1</code> 到 <code>r5</code> 这五个寄存器都是用来向被调用的函数传递参数的(回忆:BPF helper 函数中最多允许5个函数参数),函数运行结果返回到寄存器<code>r0</code>。寄存器 <code>r1</code> 到 <code>r5</code> 都是暂存寄存器,而寄存器<code>r6</code> 到 <code>r9</code> 则可以在不同调用之间保存数据。最大允许的调用深度是8。一个调用者可以传递指针(如调用者的栈帧)给被调用方,反过来不行。</p>\n<p>BPF JIT 编译器为每个函数编译出单独的映像,JIT 最终会修正映像中的函数调用地址将他们串联起来。事实证明,这样对 JIT 进行的改动最小,因为使用这种方法 JIT 可以将BPF到BPF的调用视为常规的 BPF helper 函数调用。</p>\n<p>知道 Linux 5.9,BPF 尾调用和 BPF 子程序都相互排斥,使用了尾调用的 BPF 程序不能使用 BPF 子程序来减小生成的程序映像大小、加速程序加载。之后的 Linux 5.10 最终允许了用户能够同时利用二者的优点,使得 BPF 子程序和尾递归可以同时使用了。</p>\n<p>这个改进是有代价的,将这两个特性混合在一起可能会造成内核堆栈溢出。为了一窥其中奥秘,可以看下面这张图片,它就解释了二者是如何混合的:</p>\n<p><img src=\"./bpf_tailcall_subprograms.png\" alt=\"bpf_tailcall_subprograms.png\"></p>\n<p>尾调用不改变栈顶指针位置,在真正跳转到目标程序地址之前,会把当前栈帧释放掉。正如我们在上面例子里看到的,如果一个尾调用在子函数 <code>subfunc1</code> 中发生了,调用了<code>func2</code>,那么 <code>func1</code> 的栈帧将会在栈中存在下去。一旦最终的 <code>func3</code> 函数终结了,所有前面的栈帧都会被展开控制权会回到 BPF 函数调用方。</p>\n<p>内核引入了别的逻辑来检测这两个特性的结合。在上图的整个链式调用中,对每个子程序的栈的大小限制为 256 字节,注意如果验证器检测到了 BPF2BPF 调用,那么主函数也会被视为是一个子函数(即整个链上大家都有这个栈大小的限制)。凭借这个限制,BPF 程序调用链最多能够消耗 8KB 大小的栈空间。这个上限的计算方式是每个栈帧 256 字节,乘上尾调用上限 33。没有这个限制,BPF 程序能够使用512 字节的栈空间,则最大可能会占用 16KB 的栈,最终可能会在某些架构的机器上就堆栈溢出了。</p>\n<p>另一个值得一提的点是这种特性的结合限制只在 x86-64 架构上得到了支持。</p>\n<h3>JIT</h3>\n<p><img src=\"./bpf_jit.png\" alt=\"bpf_jit.png\"></p>\n<p>64 位的 <code>x86_64</code>、 <code>arm64</code>、 <code>ppc64</code>、 <code>s390x</code>、 <code>mips64</code>、 <code>sparc64</code> 和 32 位的 <code>arm</code>、<code>x86_32</code> 架构都在内核中写好了eBPF JIT 编译器,他们都有着相同的特性,可以使用下面的方式激活</p>\n<pre><code># echo 1 > /proc/sys/net/core/bpf_jit_enable\n</code></pre>\n<p>32 位的 <code>mips</code>、 <code>ppc</code> 和<code>sparc</code> 的架构现在有一个 cBPF JIT 编译器。上面提到的架构也依然有 cBPF JIT,其他架构没有 JIT,只能通过内核中的解释器来跑。</p>\n<p>在内核源码树中,能够轻易地被找到 eBPF JIT 的相关支持,只需要 <code>git grep</code> 一下 <code>HAVE_EBPF_JIT</code>:</p>\n<pre><code># git grep HAVE_EBPF_JIT arch/\narch/arm/Kconfig: select HAVE_EBPF_JIT if !CPU_ENDIAN_BE32\narch/arm64/Kconfig: select HAVE_EBPF_JIT\narch/powerpc/Kconfig: select HAVE_EBPF_JIT if PPC64\narch/mips/Kconfig: select HAVE_EBPF_JIT if (64BIT && !CPU_MICROMIPS)\narch/s390/Kconfig: select HAVE_EBPF_JIT if PACK_STACK && HAVE_MARCH_Z196_FEATURES\narch/sparc/Kconfig: select HAVE_EBPF_JIT if SPARC64\narch/x86/Kconfig: select HAVE_EBPF_JIT if X86_64\n</code></pre>\n<p>JIT 编译器能够显著加速 BPF 程序的执行,因为相比于解释器他们减少了单指令的开销。通常指令都能够一一映射到底层架构上的原始指令上。这也减少了最终的可执行映像大小,因而对 CPU 指令缓存来说更为友好。特别是在复杂的 CISC 指令集下,例如 <code>x86</code>,JITs 都会被优化以生成最精悍的指令,来减少程序翻译后的大小。</p>\n<h3>Hardening</h3>\n<p>BPF 将整个 BPF 解释器映像(<code>struct bpf_prog</code>)和 JIT 编译出的映像(<code>struct bpf_binary_header</code>)在程序生命周期内在内核中是只读的,以防止潜在的代码损坏。例如出于内核的 bug,代码可能会损坏,进而导致内核宕机。</p>\n<p>支持将将映像设置为只读的架构能够通过下面的方式查看,同样是使用<code>git grep</code>:</p>\n<pre><code>$ git grep ARCH_HAS_SET_MEMORY | grep select\narch/arm/Kconfig: select ARCH_HAS_SET_MEMORY\narch/arm64/Kconfig: select ARCH_HAS_SET_MEMORY\narch/s390/Kconfig: select ARCH_HAS_SET_MEMORY\narch/x86/Kconfig: select ARCH_HAS_SET_MEMORY\n</code></pre>\n<p>选项 <code>CONFIG_ARCH_HAS_SET_MEMORY</code> 不是可配置的,它总是内置的。其他架构在未来也可能会支持这一特性。</p>\n<p>对于<code>x86_64</code> JIT 编译器,假设它在写操作会设置 <code>CONFIG_RETPOLINE</code> 项(通常现代操作系统都会这么干),那么JIT 地从尾调用中编译 indirect jump 是通过一个 retpoline 来实现的(<a href=\"https://stackoverflow.com/questions/48089426/what-is-a-retpoline-and-how-does-it-work?r=SearchResults\">retpoline</a> 是用来缓解内核或跨进程内存泄露的,又称 <a href=\"https://spectreattack.com/spectre.pdf\">Spectre </a>攻击,参考<a href=\"https://lkml.org/lkml/2018/1/3/780\">lkml 邮件</a>)。</p>\n<p>当 <code>/proc/sys/net/core/bpf_jit_harden</code> 设置为 <code>1</code> 的时候,另一个针对非管理员用户的 JIT 编译的 hardening 操作生效了。这会稍微牺牲性能,以减少来自未信任的用户的潜在的攻击。即使牺牲了性能,表现仍然是比直接完全地使用解释器要好。</p>\n<p>当下,激活 hardening 后,使用 JIT 编译 BPF 程序,会屏蔽 BPF 程序中所有用户提供的 32 位和 64 位常量,以应对 JIT spraying attacks。JIT spraying attacks 会注入原生指令的 opcode 作为立即数。这会带来很大的问题,因为这些立即数是存储在可执行的内核内存区域中。如果因为某些内核 bug,程序计数器跳转到了被注入的指令处,可能会执行这些被注入的程序。</p>\n<p>JIT 常量屏蔽可以通过随机化真正的指令避免这种情况,意味着最后的指令运算通过重写指令,将一个基于立即数的指令码,转换到了基于寄存器的指令码。重写指令的方法共两步:加载一个被随机屏蔽的立即数 <code>rnd ^ imm</code> 到寄存器中,使用<code>rnd</code> 与寄存器中的值做异或,在寄存器中得到初始的 <code>imm</code> 立即数,能够用来做真正的指令运算。下面的例子展示了一个 load 指令操作,其他常见的指令也都是这么屏蔽的。在 hardening 禁用的情况下使用 JIT 编译一个程序的例子:</p>\n<pre><code># echo 0 > /proc/sys/net/core/bpf_jit_harden\n\n ffffffffa034f5e9 + <x>:\n [...]\n 39: mov $0xa8909090,%eax\n 3e: mov $0xa8909090,%eax\n 43: mov $0xa8ff3148,%eax\n 48: mov $0xa89081b4,%eax\n 4d: mov $0xa8900bb0,%eax\n 52: mov $0xa810e0c1,%eax\n 57: mov $0xa8908eb4,%eax\n 5c: mov $0xa89020b0,%eax\n [...]\n</code></pre>\n<p>相同的程序,当开启了 hardening 时,以非管理员用户加载 BPF 程序时,常量会被屏蔽:</p>\n<pre><code># echo 1 > /proc/sys/net/core/bpf_jit_harden\n\n ffffffffa034f1e5 + <x>:\n [...]\n 39: mov $0xe1192563,%r10d\n 3f: xor $0x4989b5f3,%r10d\n 46: mov %r10d,%eax\n 49: mov $0xb8296d93,%r10d\n 4f: xor $0x10b9fd03,%r10d\n 56: mov %r10d,%eax\n 59: mov $0x8c381146,%r10d\n 5f: xor $0x24c7200e,%r10d\n 66: mov %r10d,%eax\n 69: mov $0xeb2a830e,%r10d\n 6f: xor $0x43ba02ba,%r10d\n 76: mov %r10d,%eax\n 79: mov $0xd9730af,%r10d\n 7f: xor $0xa5073b1f,%r10d\n 86: mov %r10d,%eax\n 89: mov $0x9a45662b,%r10d\n 8f: xor $0x325586ea,%r10d\n 96: mov %r10d,%eax\n [...]\n</code></pre>\n<p>两个程序在语义上是等价的,除了原始代码中的立即数在第二个程序的反汇编中都无法再看得到了。</p>\n<p>与此同时,hardening 也禁用了对管理员用户的 JIT kallsyms exposure,JIT 映像的地址不再暴露给 <code>/proc/kallsyms</code> 。</p>\n<p>另外, Linux 内核也提供了选项 <code>CONFIG_BPF_JIT_ALWAYS_ON</code>,从内核中移除整个 BPF 解释器并永久地激活 JIT 编译器。在使用基于 VM 的设置的 Spectre v2 环境下,当客户机不打算使用宿主机内核的 BPF 解释器来装载 Spectre 攻击的时候,这也被发展成了内核移植的一部分。对于基于容器的环境,选项 <code>CONFIG_BPF_JIT_ALWAYS_ON</code> 是可选的,但是在 JIT 被激活的情况下,解释器可能也被编译出来了以减轻内核的复杂程度。因此,在主流的结构中,例如<code>x86_64</code> 和 <code>arm64</code> 中,对广泛使用的 JIT,通常也建议开启这个选项。</p>\n<p>最后,内核提供了一个选项来禁止非管理员用户使用 <code>bpf(2)</code> 系统调用,这是通过 <code>/proc/sys/kernel/unprivileged_bpf_disabled</code> sysctl knob 实现的。这是一个一次性的开关,意味着一旦设置为 <code>1</code>,没有其他选项能够把它变为 <code>0</code>,直到内核重启。当只设置 <code>CAP_SYS_ADMIN</code> 时,离开初始空间的管理员进程从这之后就能够使用 <code>bpf(2)</code> 系统调用。</p>\n<pre><code># echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled\n</code></pre>\n<h3>Offloads</h3>\n<p><img src=\"./bpf_offload.png\" alt=\"bpf_offload.png\"></p>\n<p>BPF 写的网络相关的程序,特别是 tc 和 XDP 相关程序,都有 offload-interface,可以脱离内核在网卡中执行 BPF 代码(直接在网卡中处理因而因而更加高效甚至不用内核参与)。</p>\n<p>当下,Netronome 的驱动 <code>nfp</code> 已经支持了通过 JIT 编译器将 BPF 程序编译到网卡上特定指令集。也支持了 BPF maps,因此在网卡上 BPF 程序也能实现映射的查询、更新和删除。</p>\n<h2>BPF 虚拟机的具体实现</h2>\n<p>Linux 内核中当然是有 BPF 的 虚拟机实现的,但是内核代码浩如烟海,而且是内核态的实现。</p>\n<p>用户态的虚拟机实现有:</p>\n<p>iovisor 组织用 C 重写了一个 <a href=\"https://github.com/iovisor/ubpf/\">ubpf</a>。类似的有仿照 ubpf 写的 rust 版本 <a href=\"https://github.com/qmonnet/rbpf\">rbpf</a>。</p>\n<p>但是ubpf 只支持 <code>x86-64</code>,rbpf 仿照了前者的代码,作者表示没有时间去做其他架构的支持。</p>\n<p>同时他们都是在运行期间做的地址验证,实际上没有提前做验证。如 ubpf 对 memory 相关 load/ store 指令的地址验证就是只能在运行期进行,同时它没有对栈的地址做验证 (所以 ubpf 和 rbpf 为啥要做两次循环……一次验证一次运行……)。</p>\n<p><a href=\"/zh/blogs/20210223/\">STM32MP157</a> 开发板上,小核是 Cortex-M4,Cortex-M4 是使用的是 ARMv7-M 架构,可以参考<a href=\"https://developer.arm.com/documentation/ddi0403/d/Application-Level-Architecture/The-ARMv7-M-Instruction-Set/About-the-instruction-set?lang=en\">ARMV7-M Documentation – Arm Developer</a>。</p>\n<p>仿照 x86-64 的实现去为 ARM v7-M 实现一套。首先看字节序,x86-64 是小端的,ARM v7-M 默认也是小端,这点可以放心了。</p>\n<p>下面的章节分析一下 <a href=\"#x86-64\">x86-64</a> 和 <a href=\"#ARMv7-M\">ARMv7-M</a> 寄存器的映射,然后就可以着手看怎么改了。</p>\n<p>最后<a href=\"/zh/blogs/20210223/\">移植</a>到开发板上试一下~效果如下图。</p>\n<p>后续的工作的话:</p>\n<ul>\n<li>\n<p>考虑把腾讯的那个物联网实时操作系统的移植和这里 BPF 虚拟机的移植整合一下,实现多任务处理~</p>\n</li>\n<li>\n<p>有空给ubpf 和 rbpf 都贡献一下,同时可以考虑给 RISC-V 架构也都搞一轮……(逃</p>\n</li>\n</ul>\n<h2>补充</h2>\n<p>这部分只做了解,为了便于阅读和修改 BPF 虚拟机实现代码。关于跨平台,该<a href=\"https://sourceforge.net/p/predef/wiki/Home/\">站点</a>收录了相关的宏定义,非常实用。</p>\n<h3>Calling Convention</h3>\n<p>Caller-saved register(又名易失性寄存器AKA volatile registers, or call-clobbered)用于保存不需要在各个调用之间保留的临时数量。因此,如果要在过程调用后恢复该值,则调用方有责任将这些寄存器压入堆栈或将其复制到其他位置。不过,让调用销毁这些寄存器中的临时值是正常的。从被调用方的角度来看,您的函数可以自由覆盖(也就是破坏)这些寄存器,而无需保存/恢复。</p>\n<p>Callee-saved register(又称非易失性寄存器AKA non-volatile registers, or call-preserved)用于保存应在每次调用中保留的长寿命值。当调用者进行过程调用时,可以期望这些寄存器在被调用者返回后将保持相同的值,这使被调用者有责任在返回调用者之前保存它们并恢复它们, 还是不要碰它们。</p>\n<p>更通俗的理解:</p>\n<p>“ 调用者保存”( caller saving )方法:如果采用调用者保存策略,那么在一个调用者调用别的过程时,必须保存调用者所要保存的寄存器,以备调用结束返回后,能够再次访问调用者。<br>\n“ 被调用者保存”( callee saving )方法:如果采用被调用者保存策略,那么被调用的过程必须保存它要用的寄存器,保证不会破坏过程调用者的程序执行环境,并在过程调用结束返回时,恢复这些寄存器的内容。</p>\n<h3>GNU ASM</h3>\n<p>下面 x86-64 的示例程序,采用的都是 AT&T 语法,也是 GNU 流行的语法,和之前汇编课上的语法略有区别:需要在寄存器前面添加前缀%,例如<code>%eax</code>;在AT&T语法中,第一个是源操作数,第二个是目标操作数。Intel 语法和 AT&T 语法的常见指令格式对比如下</p>\n<p><img src=\"./asm_grammar.jpg\" alt=\"\"></p>\n<p>只需要使用 asm 就可以在 C/C++ 下写汇编程序</p>\n<pre><code class=\"language-C\">#ifdef __x86_64__\nasm volatile (\n "mov $0xf0, %rax;"\n);\n#elif __ARM_ARCH_7__\nasm volatile (\n "mov r0, #0xf0;"\n);\n#endif\n</code></pre>\n<p>有一个很有趣的现象是,写 ARM 的汇编指令却不需要遵守类似上面 AT&Y 的语法规则,可以直接按 ARM 官方文档的写法。也有老哥在 <a href=\"https://stackoverflow.com/questions/43574163/why-is-gnu-as-syntax-different-between-x86-and-arm\">StackOverflow</a> 上问了这个问题。解答是:</p>\n<p>为啥 GNU Assembler (GAS) 在 x86 上用 AT&T 语法呢?是考虑到了 x86 上 AT&T's 汇编器的可移植性。AT&T 没有使用 Intel 官方的 x86 汇编语法,而是选择了基于他们早期的 68000 和 PDP-11 汇编器创建新的语法。当 x86 支持被添加到了 GNU compiler (GCC) 的时候,它生成的是 AT&T 语法的汇编程序,因为他们用的汇编器就是这样的。</p>\n<p>然而没有 AT&T 为 ARM CPU 写的汇编器,当 GNU 开始一直 GCC 和 GAS 到 ARM 目标机器上的时候,没理由继续创建一个新的、移植性差的语法了。于是他们就选用了 ARM 的官方语法。这就意味着你能够查询 ARM 的官方文档,在 GNU 汇编器中使用上面的标准的 ARM 指令。</p>\n<h3>x86-64</h3>\n<p>Intel 最初提出的是 IA-64,但是由于不兼容 IA-32(x86-32),销量并不理想。AMD 于是推出了一款兼容 IA-32 的指令,叫 x86-64,干脆取名为 AMD64。x86-64 上不同数据类型的长度,其中 long double 多余的位是为了对齐。</p>\n<p><img src=\"./x86_type.jpg\" alt=\"这里写图片描述\"></p>\n<p>General purpose registers (GPRs) 在 x86-32,x86-64 上分别如下</p>\n<p><img src=\"./x86_32register.jpg\" alt=\"\"></p>\n<p>x86-32 扩展了 x86-16 的寄存器,加了字母 E 来标识,低位仍然可以视作 16 位的寄存器。同样的,x86-64 进一步扩展了 x86-32 的寄存器,用 R来标识,如下图</p>\n<p><img src=\"./x86_64register.jpg\" alt=\"\"></p>\n<p>具体的使用方面如下图。可以看到,在名称上需要取低位的时候,旧的架构中存在的寄存器,仍然可以用旧的架构中的名字;新增的寄存器,则通过后缀 Byte (B)、Word (W)、Double Word(DW)来区分。</p>\n<p><img src=\"./x86_64use.jpg\" alt=\"\"></p>\n<p>下面的图阐述了寄存器们具体的功能,同时除了 <code>rsp</code> 和明确标识被调用者保护的 <code>rbx</code>,<code>rbp</code>,<code>r12</code>,<code>r13</code>,<code>r14</code>,<code>r15</code>,其余都是调用者需要保护的寄存器。</p>\n<p><img src=\"./x86_64bank.jpg\" alt=\"\"></p>\n<p>下面是一个表达式运算的例子,先进行类型转换,与运算中更长的类型统一位数后再进行计算</p>\n<p><img src=\"x86_cal.jpg\" alt=\"\"></p>\n<p>函数调用的例子,在 x86-32 中需要用到栈,但是在 x86-64 中可以更简单地直接调用寄存器</p>\n<p><img src=\"./x86_call.jpg\" alt=\"\"></p>\n<p>更加详细的过程调用参数传递规范,有下表</p>\n<p><img src=\"./x86_specification.jpg\" alt=\"\"></p>\n<p>根据上表,一个较为复杂的例子如下</p>\n<p><img src=\"./x86_example.jpg\" alt=\"\"></p>\n<h3>ARMv7-M</h3>\n<p>如之前<a href=\"/zh/blogs/20210311/\">笔记</a>所述,ARMv7-M 适用于嵌入式系统等功耗低的场景,只支持Thumb指令集,用于微处理器领域。它的寄存器为 32 位。以 Cortex-M4 为例,它包含了 32 个寄存器,其中 13 个是通用寄存器(GPR),还有一些是具有特殊意义的寄存器。具体而言,GPR 为 R0-R12,R13 是 Stack Pointer(SP),R14 是 Link Register (LR),R15 是 Program Counter(PC),其余都是 Special-purpose Program Status Registers, (xPSR)。</p>\n<p><img src=\"./arm_register.png\" alt=\"\"></p>\n<p>从上图中可以看出通用寄存器分为</p>\n<ul>\n<li>low registers 是 R0 到 R7,所有指令都能使用他们</li>\n<li>high registers 是 R8 到 R12,32 位指令可以使用,都是16位指令不能使用</li>\n</ul>\n<p>特殊的寄存器:</p>\n<ul>\n<li>SP:R13 会忽略掉位 [1:0] 的写操作,因此它自动是按 word 对齐的(4个Byte)。处理异常的 Handler 模式下总是使用 SP_main,但是也可以配置 Thread 模式使用 SP_main 或 SP_process。"Thumb" 模式下的 Push/Pop 指令用这个寄存器。</li>\n<li>LR:R14 是链接子程序的寄存器。当执行 Branch and Link (BL) 或 Branch and Link with Exchange (BLX) 指令的时候,LR 接收从 PC 返回的地址。LR 也用来处理异常的返回。在其他时候,也可以把它当做通用寄存器。</li>\n<li>PC:R15 的第 0 位总是0,指令地址都是按 word 或 half-word 为界对齐的。</li>\n</ul>\n<p>至于其他 16 个寄存器,一般都是用于系统控制,见<a href=\"https://developer.arm.com/documentation/100166/0001/System-Control?search=5eec6e71e24a5e02d07b259a\">Cortex-M4 Manual</a>第四章:System Control。</p>\n<p>在处理函数调用时,参考 <a href=\"https://en.wikipedia.org/wiki/Calling_convention\">维基百科</a> 和 <a href=\"https://stackoverflow.com/questions/261419/what-registers-to-save-in-the-arm-c-calling-convention\">StackOverflow</a>,32 位 ARM 的 Calling Convention (即 caller-saved 还是 callee-saved)是遵从 <a href=\"https://developer.arm.com/documentation/ihi0042/f/\">AAPCS</a> 第5.1.1 寄存器相关章节。从功能上来讲</p>\n<ul>\n<li>R12:Intra-Procedure-call scratch register,调用者保存</li>\n<li>R4 ~ R11:局部变量,除了 R9 都是被调用者存储的寄存器,R9在某些情况下是特殊寄存器</li>\n<li>R0 ~ R3:传递给子程序的参数和子程序返回的结果,调用者保存</li>\n</ul>\n<p>用一张表描述</p>\n<p><img src=\"arm_calling.png\" alt=\"\"></p>\n<p>例子:</p>\n<p><img src=\"./arm_convention1.png\" alt=\"\"></p>\n<p><img src=\"./arm_convention2.png\" alt=\"\"></p>\n<h1>Reference</h1>\n<p><a href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/bpf/bpf_devel_QA.rst\">BPF</a> 开发QA:解释了两条bpf相关的内核分支的作用,规定了汇报bug、提交补丁的方法。</p>\n<p>Facebook 的 BPF 相关团队成员<a href=\"https://nakryiko.com/\">博客</a>,大部分内容都摘录、翻译自这里。</p>\n<p><a href=\"https://github.com/iovisor/bpf-docs/blob/master/bpf_helpers.rst/\">eBPF-Helpers</a>,ebpf 程序的 API 函数文档,貌似和<a href=\"https://man7.org/linux/man-pages/man7/bpf-helpers.7.html\">这里的内容重复</a>。</p>\n<p><a href=\"https://docs.cilium.io/en/latest/bpf/\">Cilium 文档</a> 详细讲解了 bpf 和 xdp。</p>\n<p><a href=\"https://github.com/iovisor/bpf-docs/blob/master/eBPF.md\">iovisor eBPF spec</a> 列出了 eBPF opcode,项目是 iovisor 总结的系列文档、pre。</p>\n<p><a href=\"https://www.kernel.org/doc/Documentation/networking/filter.txt\">内核 bpf 相关文档</a></p>\n<p>架构、指令集相关:</p>\n<p><a href=\"https://blog.csdn.net/l919898756/article/details/103142439\">Caller-saved register and Callee-saved register</a></p>\n<p><a href=\"https://blog.csdn.net/yongchaocsdn/article/details/78336233\">X86-64指令系统_yongchaocsdn的博客-CSDN博客</a></p>\n<p><a href=\"https://developer.arm.com/documentation/ddi0403/d/Application-Level-Architecture/The-ARMv7-M-Instruction-Set/About-the-instruction-set?lang=en\">ARMv7-M Documentation – Arm Developer</a></p>\n<p><a href=\"https://developer.arm.com/documentation/100166/0001/System-Control?search=5eec6e71e24a5e02d07b259a\">ARM Cortex-M4 Processor Technical Reference Manual</a></p>\n<p><a href=\"https://www.ic.unicamp.br/~celio/mc404-2014/docs/gnu-arm-directives.pdf\">GNU ARM Quick Reference</a></p>\n<p><a href=\"https://sourceforge.net/p/predef/wiki/Home/\">C/C++跨平台宏定义</a></p>\n<p><a href=\"https://blog.csdn.net/sinat_38816924/article/details/115607570\">bpf map简介</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210329/",
"title": "eBPF 用户空间虚拟机实现相关",
"summary": "用户态虚拟机实现、eBPF 的更多知识",
"date_modified": "2021-03-29T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Wifi连接受到的电磁干扰</h1>\n<h2>关于 HDMI</h2>\n<p>最近遇到了一个很玄学的问题,笔记本电脑带到实验室就没法连 SJTU 的无线上网了,尝试了各种设置均无效,以为是无线网卡出问题了。结果去上课又发现好好的。难道是网络信息中心针对我,只有我座位上的无线有问题 = =</p>\n<p>回到实验室后,我离开了座位,跑到了远一点的地方测试,可以上网了,没想到还真是我座位的问题(×)。对比后发现区别只在电源线、HDMI 转接线和键盘 USB 连接线。插拔 HDMI 转接线总算确定了问题源头。老E提醒我,他暑假曾有一段时间困扰于 USB 3.0 和无线网卡相互干扰,插上移动硬盘就断网,也是折腾了很久才排查清楚。</p>\n<p>唉这转接线前两天还用得好好的,拿来打狒狒14也没问题,就很无语。</p>\n<p>知道了无线网卡和 HDMI 线有可能出现电磁干扰,在<a href=\"https://www.zhihu.com/question/34787444/answer/60590570\">知乎</a>上找到了这样一个回答,原问题很有趣,是在讨论<a href=\"https://www.zhihu.com/question/34787444/answer/60590570\">你碰到过的最难调试的 Bug 是什么样的?</a>,里面有一个很经典的<a href=\"#%E9%A6%99%E8%8D%89%E5%86%B0%E6%B7%87%E6%B7%8BBug\">香草冰淇淋的故事</a>(我也不知道真实性)。</p>\n<blockquote>\n<p><strong>HDMI线有干扰</strong>,影响到了WIFI信道,加上这个AC7260无线网卡本身设计不良,容易串入干扰。由于HDMI工作频率是根据视频信号码率决定的,通过修改分辨率,改变HDMI工作频率,使干扰谐波信号跳开了2.4G和5G信道。另外通过HDMI传输信号会有一个接口协商初始化过程,只有电视机切换到这个HDMI源,完成初始化,才会在HDMI线上有数据,这点和VGA,YPBPR等模拟信号不同。</p>\n<p>之前分析问题没有往HDMI方面想,主要是视频播放会有一个缓冲,因此,刚开电视切换到HDMI的时候,一切看起来是正常的(但此时后台网络已断),过了几分钟缓冲读完了才停顿。因此分析问题时很难和HDMI线联系上。</p>\n<p>此问题其实做音视频类产品的项目经常遇见,由于HDMI频率高,传输长,因此很多输出源有意加重输出信号,导致EMI干扰严重,又由于很多HDMI线材质低劣,偷工减料,缺少屏蔽措施,因此HDMI接口往往成为电磁干扰的重灾区,也导致大量HDMI接口的兼容性问题(我这个破电视就挑信号源,有些1080P不显示)。因此能用YPBPR模拟线,或者能用DP接口,我都是躲开HDMI的。</p>\n</blockquote>\n<h2>转载:关于 USB 3.0</h2>\n<p>首先,需要明确一点事实,任何有线信号都会向外辐射电磁波,除非它是直流电(没有频率变化)、或者完全屏蔽。干扰的强度与线缆上传输的信号(如电压、电流、频率)有关。</p>\n<p>举一个特别现实的例子:一般在高压线的下方,如果要传输以太网信号,最可靠的方法是用光纤,如果用双绞线铜缆传输信号,很有可能会被干扰。</p>\n<p>大多数电子器件在设计的时候都要考虑电磁屏蔽和抗干扰的问题。所以,尽管USB3.0是有线信号,但仍然可能向外辐射电磁波,对其它信号产生干扰。</p>\n<p>那么有人会问了,USB3.0不是5GHz吗?Wifi是2.4GHz怎么会有干扰呢?问题出在USB传输线上。</p>\n<p>USB3.0的传输频率确实是5GHz串行,但USB3.0使用4条数据线组成2组,每组负责一个传输方向,实现全双工双向5GHz,而每条数据线的基准频率是2.5GHz。</p>\n<p>所以,总带宽是5GHz没错,但每条线上是2.5GHz,这个频率距离2.4G Wifi的频率太近了,又因为高频设备大多数都使用了SSC技术(扩频时钟?)使得信号不完全分布在一个固定频率上,所以就波及了2.5GHz附近的其它频率,所以对Wifi和蓝牙产生了较大的干扰。</p>\n<p>通常来说USB3.0线缆的屏蔽性是很好的,但主要的问题出在接头处。如果拆开一个USB3.0的线,会发现外面有屏蔽层之类的保护,但这些东西不是完全屏蔽的,在接头处是裸露的,并且不完全封闭,这就使得USB3.0在接头处对外产生了较大的干扰。</p>\n<p>如果把移动硬盘的接头以及前半部分全屏蔽起来,就能大大降低干扰,而比较一下即使把整个硬盘都屏蔽起来,降低的效果不明显,说明干扰主要在接头部分。</p>\n<p>所以,总结下来就是USB传输的时候会产生噪声,影响Wifi的使用,解决方法要么是使用屏蔽设备(包括USB线缆的接头都要改造),要么使用5G的wifi。</p>\n<p>USB-IF有官方的文献,参考: <a href=\"http://www.usb.org/developers/\">http://www.usb.org/developers/</a></p>\n<p>题外话:</p>\n<p>1、USB3.1要搞10GHz传输了,那么按照规范上来看,数据线上的频率应该是5GHz,所以,如果有一天升级到USB3.1,那么5G Wifi恐怕也要中枪了。</p>\n<p>2、传输频率低于总带宽的事情也算常见,网线(双绞线)就算一个,六类线(CAT6)以及更高规格的线缆上,总带宽是每条线的传输频率*数据线个数。</p>\n<p>3、为什么移动设备不怎么愿意用USB3.0接口?因为移动设备太小,电磁环境太复杂,有Wifi和各种频率的手机信号,现在再来个USB3.0,电磁屏蔽不好做。加上本身USB2.0还算不上传输瓶颈,所以就不着急上USB3.0了。</p>\n<h2>香草冰淇淋Bug</h2>\n<p>2000年通用公司庞帝雅克部门收到一封客户投诉:</p>\n<blockquote>\n<p>"This is the second time I have written you, and I don't blame you for not answering me, because I kind of sounded crazy, but it is a fact that we have a tradition in our family of ice cream for dessert after dinner each night. But the kind of ice cream varies so, every night, after we've eaten, the whole family votes on which kind of ice cream we should have and I drive down to the store to get it. It's also a fact that I recently purchased a new Pontiac and since then my trips to the store have created a problem. You see, every time I buy vanilla ice cream, when I start back from the store my car won't start. If I get any other kind of ice cream, the car starts just fine. I want you to know I'm serious about this question, no matter how silly it sounds: 'What is there about a Pontiac that makes it not start when I get vanilla ice cream, and easy to start whenever I get any other kind?'"</p>\n</blockquote>\n<p>“这是我为了同一件事第二次写信给你们,我不怪你们没有回信给我,因为我也知道大家都会认为我疯了,但这的确是一个事实。我们家有一个传统的习惯:就是在吃完晚餐后,都会以冰淇淋来当饭后甜点。由于冰淇淋的口味很多,所以每天在饭后才投票决定要吃哪种口味,等大家决定后,我就会开车去买。但最近我买了一部新的庞帝雅克后,问题就发生了。<br>\n你们知道吗?每当我买的冰淇淋是香草口味时,车子就发不动。但如果买的是其它口味,车子发动就顺得很。尽管这个问题听起来很猪头,但我是非常认真的。<br>\n为什么当我买了香草冰淇淋,这部庞帝雅克就会秀逗(不能发动),而我不管什么时候买其它口味的冰淇淋,它就是一尾活龙?为什么?为什么?”</p>\n<p>尽管庞帝雅克的部门经理很难相信这个事情, 但还是派了一位工程师去查看究竟。<br>\n当工程师去找这位仁兄时,发现这封信竟是出自于一位事业成功、乐观、且受了高等教育的人,不像是恶意捣乱。</p>\n<p>工程师和客户的见面时间刚好是在用完晚餐后,两人于是立刻驾上汽车往冰淇淋店开去。那个晚上投票结果是香草口味,当买好冰淇淋回到车上后 ,车子果然又趴窝了。</p>\n<p>这位工程师之后又来了三个晚上:<br>\n第一晚,巧克力冰淇淋,车子正常。<br>\n第二晚,草莓冰淇淋,车子也没事。<br>\n第三晚,香草冰淇淋,车子又罢工了!</p>\n<p>这位思维缜密的工程师,当然不会相信车子真的对香草冰激凌过敏。因此他继续不断用类似的行程进行测试,希望能解决这个问题。</p>\n<p>工程师开始记下从开始到现在所发生的种种详细数据,如时间 、车子使用油的种类、车子开出及开回的时间等等,根据数据显示他有了一个结论:这位仁兄买香草冰淇淋所花的时间比其它口味的要少!</p>\n<p>为什么呢?</p>\n<p>原因是出在这家冰淇淋店的内部设置的问题。</p>\n<p>因为,香草冰淇淋是最畅销的口味,店家为了让顾客每次都能很快的拿取,所以将香草口味特别分开陈列在 单独的冰柜,并将冰柜放置在店的前端;至于其它口味则放置在距离收银台较远的后端。</p>\n<p>现在,工程师所要知道的疑问是:为什么这部车会因为从熄火到重新启动的时间较短就会秀逗?原因绝对不是香草冰淇淋的关系,工程师很快地浮现出答案,应该是<strong>汽锁</strong>(vapour lock)。</p>\n<p>因为当这位仁兄买其它口味时,由于时间较久,引擎有足够的时间散热 ,重新发动就没有太大的问题。但是买香草口味时,由于花的时间较短,引擎太热以至于还无法让汽锁有足够的散热时间。</p>\n<p>问题原因终于找到了。</p>\n<h1>Reference</h1>\n<p><a href=\"https://www.zhihu.com/question/34787444/answer/60590570\">你碰到过的最难调试的 Bug 是什么样的?</a></p>\n<p><a href=\"https://blog.csdn.net/chrovery/article/details/47720731\">为什么WiFi和USB3.0会互相干扰?</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210315/",
"title": "Wifi连接受到的电磁干扰",
"summary": "奇怪的知识增加了",
"date_modified": "2021-03-15T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Rust 基础备忘</h1>\n<p>转载和记录一些印象不深的碎片知识</p>\n<h2>String vs str</h2>\n<p>当我们需要引用一个被拥有的UTF-8文本的区间(range),或者当我们使用字符串字面量(string literals)时,我们就需要使用字符串切片(也就是 <code>str</code>)。<code>&str</code> 不负责任地说,可以理解成 C++ 中的 <code>char *</code> 和 <code>string_view</code>。<code>string_view</code>是 C++17 中为解决字符串频繁拷贝问题而提出的 ,在一些只需要做查找、遍历、打印的函数中,参数的常量引用传递并不能完全解决拷贝问题,如果传参时候传的是常量引用传递,内部一旦使用赋值等运算仍然会发生拷贝,会在堆上重新分配空间浪费时间,所以它和<code>&str</code>都相当于是字符指针的包装类型,不拥有数据,只是划一个区间。</p>\n<p><code>String</code> 则可以认为和 C++ 中相同,是一个会自动分配空间的容器(而 Java 的 String 是常量)。</p>\n<h3>相互转化</h3>\n<p>像<code>println!</code>,<code>format!</code> 这些宏都是要传 <code>&str</code> 的。</p>\n<p><code>String</code> 转 <code>&str</code>:</p>\n<ul>\n<li><code>String</code> 类型在引用时, <code>&String</code> 可以自动转化为 <code>&str</code>,编译器会帮忙干活,该特性叫 <code>deref coercing</code></li>\n<li>使用<code>&some_string[..]</code> 这样完整的写法,利用了String重载的Index操作</li>\n<li><code>as_str()</code>,<code>as_ref()</code>,<code>as_borrow()</code></li>\n</ul>\n<p><code>&str</code> 转 <code>String</code>:</p>\n<ul>\n<li><code>into()</code> (这本质上是因为 <code>String</code> 实现了 <code>From<&'_ str></code> 这个 trait ,调用了<code>to_owned()</code></li>\n<li>to_owned(),因为原来没有所有权么,所以要 <code>to_owned</code> 成 <code>String</code> 拿到所有权</li>\n<li><code>to_string()</code> 调用的是 <code>String::from()</code></li>\n<li><code>String::from()</code></li>\n</ul>\n<h3>内存的分配</h3>\n<p>讨论内存分配的例子:</p>\n<p><code>let my_name = "Pascal".to_string();</code></p>\n<p>那么</p>\n<pre><code>buffer\n / capacity\n / / length\n / / /\n +–––+–––+–––+\nstack frame │ • │ 8 │ 6 │ <- my_name: String\n +–│–+–––+–––+\n │\n [–│–––––––– capacity –––––––––––]\n │\n +–V–+–––+–––+–––+–––+–––+–––+–––+\n heap │ P │ a │ s │ c │ a │ l │ │ │\n +–––+–––+–––+–––+–––+–––+–––+–––+\n\n [––––––– length ––––––––]\n</code></pre>\n<p>Rust会在栈上存储<code>String</code>对象。这个对象里包含以下三个信息: 一个<strong>指针</strong>指向一块分配在堆上的缓冲区,这也是数据真正存储的地方,数据的<strong>容量</strong>和<strong>长度</strong>。因此,<code>String</code>对象本身长度总是固定的三个字(word)。</p>\n<p>如果我们只是对存储在<code>my_name</code>中的last name感兴趣,我们可以像下面这样来获取一个针对字符串中的特定部分的引用:</p>\n<pre><code class=\"language-rust\">let mut my_name = "Pascal".to_string();\nmy_name.push_str( " Precht");\nlet last_name = &my_name[7..];\n</code></pre>\n<p>通过指定从第7个字节(因为有空格)开始一直到缓冲区的结尾(".."),<code>last_name</code>现在是一个引用自<code>my_name</code>拥有的文本的字符串切片(string slice)。它借用了这个文本。这里是它在内存中的样子:</p>\n<pre><code>my_name: String last_name: &str\n [––––––––––––] [–––––––]\n +–––+––––+––––+–––+–––+–––+\nstack frame │ • │ 16 │ 13 │ │ • │ 6 │ \n +–│–+––––+––––+–––+–│–+–––+\n │ │\n │ +–––––––––+\n │ │\n │ │\n │ [–│––––––– str –––––––––]\n +–V–+–––+–––+–––+–––+–––+–––+–V–+–––+–––+–––+–––+–––+–––+–––+–––+\n heap │ P │ a │ s │ c │ a │ l │ │ P │ r │ e │ c │ h │ t │ │ │ │\n +–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+\n</code></pre>\n<p>注意<code>last_name</code>没有在栈上存储容量信息。这是因为它只是对一个字符串切片的引用,而该字符串管理它的容量。这个字符串切片,即<code>str</code>本身,是不确定大小(unsized)的。 而且,在实际使用中,字符串切片总是以引用的形式出现,也就是它们的类型总是<code>&str</code>而不是<code>str</code>。</p>\n<p>有两种情况我们需要使用字符串切片:要么创建一个对子字符串的引用,或者我们使用<strong>字符串字面量</strong>(string literals)。</p>\n<p>一个字符串字面量由一串被双引号包含的文本创建,就像这样:</p>\n<pre><code class=\"language-rust\">let my_name = "Pascal Precht"; // This is a `&str` not a `String`\n</code></pre>\n<p>下一个问题是,如果<code>&str</code>是一个引用了被(某人)拥有的<code>String</code>的切片,假定这个文本在适当的地方被创建,那么这么<code>String</code>的所有者是谁?</p>\n<p>很显然,字符串字面量有点特殊。他们是引用自“预分配文本(preallocated text)”的字符串切片,这个预分配文本存储在可执行程序的只读内存中。换句话说,这是装载我们程序的内存并且不依赖于在堆上分配的缓冲区。</p>\n<p>也就是说,栈上还有一个入口,指向当程序执行时预分配的内存。</p>\n<pre><code>my_name: &str\n [–––––––––––]\n +–––+–––+\nstack frame │ • │ 6 │ \n +–│–+–––+\n │ \n +––+ \n │\n preallocated +–V–+–––+–––+–––+–––+–––+\n read-only │ P │ a │ s │ c │ a │ l │\n memory +–––+–––+–––+–––+–––+–––+\n</code></pre>\n<p>当我们对<code>String</code>和<code>&str</code>的区别有了更好的理解之后,另一个问题也就随之而来了。</p>\n<h3>应该使用哪一个?</h3>\n<p>显然,这取决于很多因素,但是一般地,保守来讲,如果我们正在构建的API不需要拥有或者修改使用的文本,那么应该使用<code>&str</code>而不是<code>String</code>。</p>\n<h2>Life Time</h2>\n<p>仅在编译期存在,与运行无关</p>\n<p>生命周期参数类似模板参数,可以任意指定名称,除了保留的 <code>'static</code></p>\n<p>当生命周期的名称不重要的时候,可以使用 <code>'_</code> 代表一个不具名的生命周期参数。</p>\n<pre><code class=\"language-rust\">struct Config {\n ...\n}\n\nstruct App {\n config: &Config\n}\n</code></pre>\n<p>如果像上面这么直接用一个引用,无法通过编译,需要提供一个具名生命周期参数,如下</p>\n<pre><code class=\"language-rust\">struct App<'a> {\n config: &'a Config\n}\n</code></pre>\n<p>当提供了这样一个生命周期后,编译器可以保证引用类型 <code>&Config</code> 变量 <code>config</code> 和 <code>App</code> 具有相同的生命周期,所以不会出现野指针。即编译器会根据编程者对生命周期的描述,进行检查保证引用都会在声明周期之内。</p>\n<p>回顾所有权的定义,在变量超出所有权上下文后,就会被自动 drop 掉,对于引用也是如此</p>\n<pre><code class=\"language-rust\">fn main() {\n let r;\n {\n let x = 1;\n r = &x;\n }\n println!("{}", r)\n}\n</code></pre>\n<p>该程序会报错 x 的生命周期不够长,在被 drop 后,r 仍然持有对 x 的引用。</p>\n<h3>引用传参</h3>\n<p>当传递引用时</p>\n<pre><code class=\"language-rust\">fn some_function<'a>(val: &'a i32) {\n ...\n}\n</code></pre>\n<p><code>some_function</code> 接收一个对于<code>i32</code> 类型的引用,随便给了一个生命周期参数<code>'a</code>。于是编译器就知道<code>some_function</code> 不会也不应该去把 <code>val</code> 存储到任何一个可能超出该函数生命周期的地方。</p>\n<p>如果 <code>some_function</code> 采用生命周期参数<code>'static</code>就不同了,Rust 会认为该参数是一个全局变量。这种情况下,只有<code>static</code> 变量才能作为函数参数。</p>\n<h3>返回引用</h3>\n<pre><code class=\"language-rust\">fn smallest_number<'a>(n: &'a [i32]) -> &'a i32 {\n let mut s = &n[0];\n for r in &n[1..] {\n if r < s {\n s = r;\n }\n }\n s\n}\n</code></pre>\n<p>上面的生命周期标识表明返回值和参数的生命周期是相同的,在调用函数处,如果输入参数的生命周期结束了,返回值也就不能再被引用了。事实上,上面的代码中不显式地标明生命周期参数也可以,编译器会自动完成推导。</p>\n<pre><code class=\"language-rust\">let s;\n{\n let numbers = [2, 4, 1, 0, 9];\n s = smallest_number(&numbers);\n}\nprintln!("{}", s)\n</code></pre>\n<p>也就是说,这段代码会报错,因为在括号外,<code>numbers</code> 被 drop 掉了,所以 <code>s</code>也就无法引用函数返回值了。</p>\n<h3>结构体</h3>\n<p>也就是一开始的那段代码。为什么编译器不能自动帮我们拓展一下生命周期?事实上在早期的编译器实现中,确实是这么干的,但是开发者发现有时会引发歧义,不如明确标识出引用的生命周期。</p>\n<p>需要注意的是,当前面代码中的 <code>App</code>结构体被其他类型借用时,也需要提供生命周期参数,即</p>\n<pre><code class=\"language-rust\">struct Platform<'a> {\n app: App<'a>\n}\n</code></pre>\n<h3>多个生命周期参数</h3>\n<p>考虑下面两种定义,第二种才是符合调用时要求的定义</p>\n<pre><code class=\"language-rust\">/// The same lifetime annotation\nstruct Point<'a> {\n x: &'a i32,\n y: &'a i32\n}\n/// Different lifetime annotation\nstruct Point<'a, 'b> {\n x: &'a i32,\n y: &'b i32\n}\n</code></pre>\n<pre><code class=\"language-rust\">fn main() {\n let x = 3;\n let r;\n {\n let y = 4;\n let point = Point { x: &x, y: &y };\n r = point.x\n }\n println!("{}", r);\n}\n</code></pre>\n<p>在第一种定义下,编译器会自动选择更短的生命周期,即成员<code>x</code> 和 <code>y</code> 都会被当做 <code>y</code> 的生命周期。</p>\n<h2>Trait</h2>\n<ul>\n<li>Trait 比较烦的一点是在使用相关的类的时候,记得把它实现的 trait 也要 use 到。</li>\n<li>递归相关的 trait、复制相关的 trait 遇到问题可以回顾过往的笔记。</li>\n</ul>\n<h2>与 C++ 结合</h2>\n<p>标准库中几个常用的</p>\n<ul>\n<li>std::os::raw</li>\n<li>std::ffi</li>\n</ul>\n<p><a href=\"http://crates.io/\">http://crates.io/</a> 上的库</p>\n<ul>\n<li>clib</li>\n<li>inx</li>\n</ul>\n<p><code>std::io::Error::last_os_error</code> 这个函数,是用来捕获函数操作失败后,内核反馈给我们的错误。</p>\n<h2>杂项</h2>\n<ul>\n<li>\n<p>关键字 <code>ref</code>,<code>deref</code> 等价于 <code>&</code> 和 <code>*</code>,即</p>\n<pre><code class=\"language-rust\">let a = &3u8 ;\nlet ref b = 3u8;\nassert_eq!(*a,*b);\n</code></pre>\n</li>\n<li>\n<p>2018 版里,不用 <code>extern crate</code> 了,可以直接 <code>use</code></p>\n</li>\n</ul>\n<h2>安全性</h2>\n<blockquote>\n<p>If it compiles, then it works.</p>\n</blockquote>\n<h3>C++的情况</h3>\n<p>C++把内存使用分为两种情况:值对象和指针对象。值语义的对象超出作用域会自动调用析构函数销毁,传递或者赋值的时候会进行一次拷贝。指针语义则交给人肉来管理,或者使用智能指针来引用计数。值对象在传递赋值中拷贝一次比较浪费,所以C++后来有了移动构造函数。值在移动以后,关联的数据移动到新值。</p>\n<h3>Rust是怎么做的</h3>\n<p>Rust则是在C++的基础上进一步优化。Rust的对象有一个所有者,和多个引用。Rust只允许值有一个所有者,传递和赋值会导致所有权移动。这看起来像C++的 <code>unique_ptr</code>,但实际上更像C++的移动语义。也就是说C++拷贝是隐式的移动是显式的,Rust移动是隐式的。当然Rust在这里有编译器的静态分析,没有运行时开销。很多地方并不想移动值,只是借用一下,Rust也使用了引用的概念,来表达指针语义。一个常见内存问题是指针指向了一个无效的内存地址,Rust却没这个问题。Rust编译器强制让你证明值的生命周期大于它的引用的生命周期。有些编译器搞不清楚的地方需要添加生命周期标记,来告诉编译器。</p>\n<h2>References</h2>\n<p><a href=\"https://blog.thoughtram.io/string-vs-str-in-rust/\">String vs str in Rust</a></p>\n<p><a href=\"https://zhuanlan.zhihu.com/p/61652809\">知乎专栏:使用套接字联网 API</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210312/",
"title": "Rust 基础备忘",
"summary": "大基本功",
"date_modified": "2021-03-12T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>BPF 和 eBPF 初探</h1>\n<h2>BPF论文笔记</h2>\n<p>该文章由 UCB 发表在 1992 年的 Winter USENIX,题目是《The BSD Packet Filter: A New Architecture for User-level Packet Capture》。</p>\n<p>BPF 全名为 BSD Packet Filter,最初被应用于网络监测,例如知名的<code>TCPdump</code> 工具中,它可以在内核态根据用户定义的规则直接过滤收到的包,相较竞争者 CSPF 更加高效。它设计了一个基于寄存器的虚拟机用来过滤包,而 CSPF 则使用的是基于栈的虚拟机。</p>\n<p>BPF 有两个组成部分:</p>\n<ul>\n<li>Tap 部分负责收集数据</li>\n<li>Filter 部分负责按规则过滤包</li>\n</ul>\n<p><img src=\"./bpf_overview.png\" alt=\"\"></p>\n<p>收到包以后,驱动不仅会直接发给协议栈,还会发给 BPF 一份, BPF根据不同的filter直接“就地”进行过滤,不会再拷贝到内核中的其他buffer之后再就行处理,否则就太浪费资源了。处理后才会拷贝需要的部分到用户可以拿到的 buffer 中,用户态的应用只会看到他们需要的数据。</p>\n<p>注意在BPF中进行处理的时候,不是一个一个包进行处理的,因为接收到包之间的时间间隔太短,使用<code>read</code>系统调用又是很费事的,所以 BPF 都是把接收到的数据打包起来进行分析,为了区分开这些数据,BPF 会包一层首部(header),用来作为数据的边界。</p>\n<p>作者先比较了两种极端的情况,在接收所有包和拒绝所有包的情况下 BPF 和竞争者 NIT 的表现。</p>\n<p><img src=\"./bpf_exp1.png\" alt=\"\"></p>\n<p><img src=\"./bpf_exp2.png\" alt=\"\"></p>\n<p>横轴是包的大小,纵轴是平均时间开销,斜率是读写速度。y轴上的截距是包长为0时候,也即对于每个包来说,固定的调用 filter 的开销。由于需要分配和初始化 buffer,NIT 调用时长在 80-100 微秒,而 BPF 则只要 5 微秒。除此以外,随着包长增加,可以看到当接受所有包时,虽然都会将包拷贝到 buffer 中,BPF 要更快一些。同时,当拒绝所有包的时候,由于 BPF 直接就地过滤掉了所有的包,不需要任何拷贝,所以它的开销几乎是常数,即固有的 filter 调用时间。</p>\n<p>在网络监测中,一般来说(除非开启混乱模式),丢弃的信息要多于需要的信息,因此 BPF 在一般情况下优势巨大。</p>\n<p>事实上,一个 filter 就好似一个断言,或真或假,代表是否需要该包。</p>\n<p>为了验证断言,CSPF 采用的是如下的树形结构,好处是思路清晰。但是遍历树时需要使用栈,每次或者向栈中推入常量或包的数据,或者在栈顶两个元素之间进行二元运算。在跑完整个树结构后,再读取栈顶元素,如果是非零值或栈是空的,才会接收该包,否则丢弃。就算可以使用短路运算去优化实现代码,也依然有很大问题,首先网络是分层的,包结构里有很多首部,逐层嵌套,每次进行判断都要重新拆解包。其次接收也是接收一整个包,而不去考虑会有很多不需要的数据,明显是比 BPF 低效的。</p>\n<p><img src=\"./bpf_tree.png\" alt=\"\"></p>\n<p>而 BPF 则使用了下图的 CFG(Control Flow Graph), CFG 是一个 DAG (Directed Acyclic Graph),左边分支是说明节点是 false,右边分支是说明节点是 true。该结构运算时更多地使用寄存器,这也是一个更快速的原因。该方法的问题就是 DAG 怎么构造,怎么排序,这本身是另一个算法问题了,文中没有进行讨论。</p>\n<p><img src=\"./bpf_cfg.png\" alt=\"\"></p>\n<p>BPF 的虚拟机设计,没有采用三地址形式的代码,而是采用的多为二元运算、单地址的运算。它也定义了一系列如下的 32 位的运算指令。在实现时是用的宏,但是在文中为了便于阅读,用了汇编形式。注意到取址运算很多是相对于包来说的,因为本来这个虚拟机就是用来分析包的。</p>\n<p><img src=\"./bpf_code.png\" alt=\"\"></p>\n<p><img src=\"./bpf_instruction.png\" alt=\"\"></p>\n<p><img src=\"./bpf_addr.png\" alt=\"\"></p>\n<p>由于数据在包中的位置不固定,BPF 定义了一个运算来简化地址运算的步骤,即 <code>4*([14]&0xf)</code> ,其实是在分析 IP header,乘 4 是因为 offset 是字长为单位,是 4 个字节。是下面代码的缩写。</p>\n<p><img src=\"./bpf_abbr.png\" alt=\"\"></p>\n<h2>eBPF (extended BPF)</h2>\n<p><a href=\"https://ebpf.io/zh-cn/\">官方网站</a>。</p>\n<p>Linux 内核一直是实现监控/可观测性、网络和安全功能的理想环境。 不过很多情况下这并非易事,因为这些工作需要修改内核源码或加载内核模块, 最终实现形式是在已有的层层抽象之上叠加新的抽象。 eBPF 是一项革命性技术,它能在内核中运行沙箱程序(sandbox programs), 而无需修改内核源码或者加载内核模块。</p>\n<p>将 Linux 内核变成可编程之后,就能基于现有的(而非增加新的)抽象层来打造更加智能、 功能更加丰富的基础设施软件,而不会增加系统的复杂度,也不会牺牲执行效率和安全性。</p>\n<p><img src=\"./ebpf_overview.png\" alt=\"img\"></p>\n<p><a href=\"https://lkml.org/lkml/2015/4/14/232\">Ingo Molnár</a> 在 2015 年在提议合并 Linux 分支时这样描述 eBPF :</p>\n<blockquote>\n<p>One of the more interesting features in this cycle is the ability to attach eBPF programs (user-defined, sandboxed bytecode executed by the kernel) to kprobes. This allows user-defined instrumentation on a live kernel image that can never crash, hang or interfere with the kernel negatively.</p>\n</blockquote>\n<h2>主要项目</h2>\n<p><a href=\"https://ebpf.io/projects/\"><strong>项目列表</strong></a></p>\n<h3>BCC: Toolkit and library for efficient BPF-based kernel tracing</h3>\n<p>BCC 是一个基于 eBPF 的高效跟踪检测内核、运行程序的工具,并且包含了须有有用的命令行工具和示例程序。BCC减轻了使用 C 语言编写 eBPF 程序的难度,它包含了一个 LLVM 之上的包裹层,前端使用 Python 和 Lua。它也提供了一个高层的库可以直接整合进应用。它适用于许多任务,包括性能分析和网络流量控制。下图是BCC给出的常见工具:</p>\n<p><img src=\"./bcc_tracing_tools_2019.png\" alt=\"\"></p>\n<h3>bpftrace: High-level tracing language for Linux eBPF</h3>\n<p>bpftrace 是一个基于 Linux eBPF 的高级编程语言。语言的设计是基于 awk 和 C,以及之前的一些 tracer 例如 DTrace 和 SystemTap。bpftrace 使用了 LLVM 作为后端,来编译 compile 脚本为 eBPF 字节码,利用 BCC 作为库和 Linux eBPF 子系统、已有的监测功能、eBPF 附着点交互。</p>\n<h3>Cilium: eBPF-based Networking, Security, and Observability</h3>\n<p>Cilium 是一个开源项目提供了借助 eBPF 增强的网络、安全和监测功能。它从根本上被专门设计成了将 eBPF 融入到 Kubernetes (k8s)并且强调了容器新的规模化、安全性、透明性需求。</p>\n<h3>Falco: Cloud Native Runtime Security</h3>\n<p>Falco 是一个行为监测器,用来监测应用中的反常行为。在 eBPF 的帮助下,Falco 在 Linux 内核中审计了系统。它将收集到的数据和其他输入例如容器运行时的评价标准和 Kubernetes 的评价标准聚合,允许持续不断地对容器、应用、主机和网络进行监测。</p>\n<h3>Katran: A high performance layer 4 load balancer</h3>\n<p>Katran 是一个 C++ 库和 eBPF 程序,可以用来建立高性能的 layer 4 负载均衡转发屏幕。Katran 利用Linux 内核中的 XDP 基础构件来提供一个核内的快速包处理功能。它的性能随着网卡接收队列数量线性增长,它也可以使用 RSS 来做 L7 的负载均衡。</p>\n<h2>核心架构</h2>\n<h3>Linux Kernel (eBPF Runtime)</h3>\n<p>Linux kernel 包含了需要运行 eBPF 程序的 eBPF 运行时。它实现了 <code>bpf(2)</code> 系统调用来和程序、<a href=\"#BTF\">BTF</a> 和可以运行 eBPF 程序的各种挂载点进行交互。内核包含了一个 eBPF 验证器,来做安全检测,以及一个 JIT 编译器来将程序直接转换成原生的机器码。用户空间的工具例如 bpftool 和 libbpf 都作为上游会被内核团队维护。</p>\n<h3>LLVM Compiler (eBPF Backend)</h3>\n<p>LLVM 编译器基础构件包含了 eBPF 后端,能够将类似 C 语言语法书写出的程序转换到 eBPF 指令。LLVM 生成了 eBPF ELF 可执行文件,包含了程序码、映射描述、位置信息和 BTF 元数据。这些 ELF 文件包含了所有 eBPF loader 必须的信息例如 libbpf,来在 Linux 内核中准备和加载程序。LLVM 项目也包含了其他开发者工具例如 eBPF object file disassembler。</p>\n<h2>eBPF 库</h2>\n<h3>libbpf</h3>\n<p>libbpf 是一个基于 C/C++ 的库,由 Linux 开发团队维护。它包含了一个 eBPF loader,接管处理 LLVM 生成的 eBPF ELF 可执行文件,加载到内核中。它支持了 BCC 中没有的特性例如全局变量和 BPF skeletons。</p>\n<p>Libbpf 可以支持构建单次编译任意执行(CO-RE)的应用,但是,和 BCC 不同,不需要构建 Clang/LLVM 运行时,也不需要获取 kernel-devel 头文件。但是使用 CO-RE 特性需要内核支持 <a href=\"#BTF\">BTF</a>,下面一些主要的 Linux 发行版已经带有了 BTF :</p>\n<ul>\n<li>Fedora 31+</li>\n<li>RHEL 8.2+</li>\n<li>Arch Linux (from kernel 5.7.1.arch1-1)</li>\n<li>Ubuntu 20.10</li>\n<li>Debian 11 (amd64/arm64)</li>\n</ul>\n<p>可以通过搜索相关文件查看内核是否实现了 <a href=\"#BTF\">BTF</a> 支持:</p>\n<pre><code class=\"language-bash\">ls -la /sys/kernel/btf/vmlinux\n</code></pre>\n<h3>libbpf-rs & redbpf</h3>\n<p>libbpf-rs 是一个安全的、符合 Rust 语法的 libbpf API 包裹层。libbpf-rs 和 libbpf-cargo(cargo 的插件)运行开发者编写的 CO-RE 的 eBPF 程序。redbpf 是一个 Rust eBPF 工具链,包含了一系列 Rust 库来编写 eBPF 程序。</p>\n<h1>补充记录:SystemTap</h1>\n<p>以前用过的 SystemTap 是基于 Kprobe 实现的。SystemTap的框架允许用户开发简单的脚本,用于调查和监视内核空间中发生的各种内核函数,系统调用和其他事件。它是一个允许用户开发自己的特定于内核的取证和监视工具的系统。工作原理是通过将脚本语句翻译成C语句,编译成内核模块。模块加载之后,将所有探测的事件以钩子的方式挂到内核上,当任何处理器上的某个事件发生时,相应钩子上句柄就会被执行。最后,当systemtap会话结束之后,钩子从内核上取下,移除模块。整个过程用一个命令 stap 就可以完成。</p>\n<h1>论文</h1>\n<h2>ATC18: The design and implementation of hyperupcalls</h2>\n<p>使用 ebpf 在 hypervisor 中运行客户机的经过验证代码。</p>\n<p>Hypervisor 往往把 Guest 视为黑箱,二者的交互需要 Context Switch 做中转。也有一些不需要 Context Switch 的设计,但是一侧数据结构发生改变,另一侧的代码也要更新,难以维护。由</p>\n<p>注册步骤。客户机将 C 代码编译到了可信的 eBPF 字节码,其中可能引用了客户机的数据结构。<br>\n客户机将生成的字节码注入到 hypervisor 中,验证安全性、将它编译到原生的指令上,加入到虚拟机的 hyperupcall 列表中。</p>\n<p>执行步骤,当某个事件发生,触发 hyperupcall,可以获取并更新客户机的数据结构。</p>\n<h2>TON: A framework for eBPF-based network functions in an era of microservices</h2>\n<p><a href=\"polycube-network.readthedocs.io\">Polycube</a> 的设计文章,2021 年发表在期刊 TON 上。</p>\n<p>微服务框架。Polycube 将网络功能统一抽象成 cube,在用户空间创建一个对于service不可见的守护进程统一进行管理,当收到 REST (Representational State Transfer) API 形式的请求,会首先发给这个守护进程,它作为代理,将请求分发到某个 service 的不同实例上,把回复返还给请求方。</p>\n<p>Network Functions Virtualization<br>\n网桥,路由器,NAT,负载平衡器,防火墙,DDoS缓解器现在都可以通过软件形式实现。但是他们往往都是 bypass 内核的。Polycube 希望通过 eBPF 动态地在内核中注册 Network Functions。</p>\n<p>Polycube 组合各个网络功能来构建任意服务链,并提供到名称空间,容器,虚拟机和物理主机的自定义网络连接。Polycube 还支持多租户,可以同时启用多个虚拟网络 [11]。</p>\n<p>当时分享时被问到的一个问题是和 Cilium 有什么区别,orz 还是没搞清,之后再看吧。</p>\n<p>:::tip Linux 处理 TCP 包有两条路径,fast path 和 slow path。使用快速路径只进行最少的处理,如处理数据段、发生ACK、存储时间戳等。使用慢速路径可以处理乱序数据段、PAWS(Protect Againest Wrapped Sequence numbers,解决在高带宽下,TCP序列号在一次会话中可能被重复使用而带来的问题)、socket内存管理和紧急数据等。:::</p>\n<h2>Bringing the Power of eBPF to Open vSwitch</h2>\n<p>发表在 2018 年的 Linux Plumbers Conference 上,Open vSwitch 是一个运行在 Linux 下的软件定义交换机,它最初是通过内核中的 openvswitch.ko 这个模块实现的,但是项目组现在开辟了两个新的项目,OVS-eBPF 和 OVS-AFXDP。前者的目的是使用 eBPF 重写现有的流量处理功能,attach 到了 TC 的事件上;而后者则是使用 AF_XDP 套接字 bypass 掉内核,把流量处理转移到用户空间。</p>\n<p>OVS eBPF datapath 包含多个 eBPF 程序和用户态的 ovs-vswitchd 作为控制平面。eBPF 程序是通过尾调用相连的,eBPF maps 在这些 eBPF 程序和用户空间应用之间是共享的。</p>\n<h2>OSDI20: hXDP: Efficient Software Packet Processing on FPGA NICs</h2>\n<p>在 FPGA 上实现 eBPF/XDP,削减 eBPF 指令集、并行执行,最终在时钟频率为 156MHz 的 FPGA 上达到 GHZ CPU 处理包的速度。</p>\n<h2>CONEXT 19: RSS++ load and state-aware receive side scaling</h2>\n<p>接收方缩放(Receive Side Scaling,RSS)是一种网络驱动程序技术,可在多处理器系统中的多个CPU之间有效分配网络接收处理。<br>\n接收方缩放(RSS)也称为多队列接收,它在多个基于硬件的接收队列之间分配网络接收处理,从而允许多个CPU处理入站网络流量。RSS可用于缓解单个CPU过载导致的接收中断处理瓶颈,并减少网络延迟。<br>\n它的作用是在每个传入的数据包上发出带有预定义哈希键的哈希函数。哈希函数将数据包的IP地址,协议(UDP或TCP)和端口(5个元组)作为键并计算哈希值。(如果配置的话,RSS哈希函数只能使用2,3或4个元组来创建密钥)。<br>\n哈希值的多个最低有效位(LSB)用于索引间接表。间接表中的值用于将接收到的数据分配给CPU。</p>\n<p>传统 RSS 只依赖哈希,分布可能是不均匀的;一般的负载均衡是关注服务器之间的,这里的是关注的单机的多核负载均衡。在接收时,某个CPU可能缓存更多的包,导致占用率过高,出现丢包的情况,延迟也会相应增加</p>\n<p>通过动态修改 RSS 表在 CPU 核心之间分配流量。RSS++摆脱了延迟分布的长尾,提升了CPU利用率,还支持削减参与到包转发过程中的 CPU 核心数,避免核心数过量。比如左边图片,第三段,自动空余出了 CPU。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210311/",
"title": "BPF和eBPF初探",
"summary": "相关材料整理",
"date_modified": "2021-03-11T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>USART, UART, RS232, USB, SPI, I2C, TTL都是啥</h1>\n<p>翻译自 StackExchange<sup><a href=\"#ref1\">1</a></sup>。</p>\n<h2>Serial</h2>\n<p>串行设备是一系列时分复用设备的统称,意味着数据是按时序传输的,通常是一个比特接着一个比特。下面提到的协议都是串行协议。</p>\n<p>串行接口有两种基础的类型:同步和异步。</p>\n<p>同步接口数据的传输的时机和一个明确的时钟信号有关,这个时钟信号通常也会被提供。经典的例子是SPI,但是也有其他特殊的形式比如用于音频转换的 I2S,JTAG, FPG 配置接口等。许多并行传输的方式只是将该思路拓展到一次传输多个比特。通常(但不是绝对)会先传输最高位(most significant bit,MSB)。</p>\n<p>异步接口通常将时间时序编码到数据中,对于串口和相关的标准例如 RS232,一个字长(4 字节)的时间是在起始比特的位置进行设定,接收方以正确的时间间隔采样就足够了。其他接口有一些复杂,需要更加精巧的时钟信号恢复方法。UART("Universal Asynchronous Receiver Transmitter),实际上是一个功能模块的称呼,它常常被用来实现字长、速率、起始终止标记可变的串口。而像 RS232, RS422这样的标准则是针对板外电子信号的传输。通常 UART 发送最低位(least significant bit,LSB)。</p>\n<h2>UART</h2>\n<p>UART (Universal Asynchronous Receiver Transmitter) 是最常用的串行协议。它十分古老,也十分简单。大多数控制器在板子上都有一个 UART 硬件接口。它使用一条数据线路用来传输数据,另一条线路用来接收数据。通常会使用如下的方法直接传输 8-bit 的数据:1 start bit (low level), 8 data bits and 1 stop bit (high level)。低电平的开始比特和高电平的终止比特意味着,在传输开始的时候总是有一个从高到低的转换。没有给电压分级,所以你可以使用 3.3 V 或 5 V,只要你的微处理器是工作在这个电压上就行。注意想要通过 UART 通讯的微处理器必须对传输速率、比特率达成一致,因为他们只有在起始位下落处是可以实现同步的,这也是为什么该协议会被叫做异步的。</p>\n<p>对长距离通讯来说,5 V UART 并不可靠,因此它常常被转换到更高的电压,一般是+12 V 代表"0"、-12 V 代表 "1"。数据格式仍然相同。这实际上就是 RS-232,(也可以称为 EIA-232,虽然没人这么说)。</p>\n<p>UART 的一个很大的弊端在于依赖于时间,解决方案就是 USART (Universal Synchronous/Asynchronous Receiver Transmitter)。这种协议可以实现 UART 和同步的协议。在同步模式下,在同步情况下,不仅有数据传输,还会传输一个时钟信号。对每一位,时钟脉冲会告诉接收者它是否应该锁定那一位。同步协议或者需要更高的带宽,例如 Manchester 编码或者需要另一根时钟信号线,例如 SPI 和I2C。</p>\n<h2>SPI</h2>\n<p>SPI (Serial Peripheral Interface) 是另一种非常简单的串行协议。master 发送一个时钟信号,在每个时钟脉冲它发送一比特给 slave,并读入来自 slave 的一比特。clock 信号名字是 SCK,Master 输出 Slave 输入是 MOSI ,Master 输入 Slave 输出是 MISO。通过使用 SS (Slave Select) 信号,master 能够同时控制总线上的多个 slave。 有两种方法来连接多个 slave 设备,一种是上面提到的使用 SS 信号,另一种是菊花链,第二种方法使用更少的引脚但是软件上更复杂。</p>\n<h2>I2C</h2>\n<p>I2C (Inter-Integrated Circuit, 发音是 "I squared C") 也是一个同步协议,,它在设计上更加巧妙。仅有两条线,一条是用来传输时钟信号 (SCL) 另一条是传输数据 (SDA)。这就意味着 master 和 slave 能够在创建时钟信号的 master 的控制下,同时在一条线上传输数据。I2C 不用 SS 信号来选择特定的 slave 设备,但是它有地址。master 发送的第一个字节有一个7位的地址,所以在总线上能用 127 个设备,剩余1位是读写标志,指示下一字节是来自 master 的还是来自 slave 的。在每个字节之后,接收方必须发送一个 "0" 来表示它接收到了该字节,master 会在第九个时钟脉冲用锁存器(latch)锁定它。</p>\n<p>如果 master 想要写一个字节,下面的过程会重复:master 把一个比特放在总线上的比特后面,然后每次都使用一个时钟脉冲来标记数据可以被读取了。如果 master 想要接收数据,它只需要产生时钟脉冲就可以了。slave 将会负责准备下一个时钟脉冲时的比特。这个协议的专利权在 NXP。为了减少开销,Atmel 使用了 另一个协议TWI (2-wire interface),它和 I2C很相似,所以 AVR device 不使用 I2C 而是使用 TWI。</p>\n<p>在同一条线上,两个或更多的信号可能会导致冲突,特别是一个设备发送 1,另一个发送 0 的时候。因此总线是 wired-OR 的:当两个电阻器都把总线拉升到高电平,设备智慧发送低电平,如果他们想要发送高电平,只能放弃掉总线。</p>\n<h2>TTL</h2>\n<p>TTL (Transistor Transistor Logic) 它并不是一个协议。它是数字逻辑中一种更古老的技术,但是这个名字通常被用来指代 5 V 的供电电压。有人会把它和 UART 混淆。</p>\n<h2>Reference</h2>\n<ol>\n<li><a href=\"https://electronics.stackexchange.com/questions/37814/usart-uart-rs232-usb-spi-i2c-ttl-etc-what-are-all-of-these-and-how-do-th\">StackExchange</a> <div id=\"ref1\"/></li>\n</ol>\n",
"url": "https://forsworns.github.io///zh/blogs/20210310/",
"title": "USART, UART, RS232, USB, SPI, I2C, TTL都是啥",
"summary": "串行通信协议总结",
"date_modified": "2021-03-10T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>虚拟化基础学习笔记</h1>\n<h2>虚拟化和Hypervisor</h2>\n<p>这部分主要抄录自<a href=\"#ref1\"><sup>[1]</sup></a>。这部分首先介绍虚拟化和 hypervisor ,再探索两个基于 Linux 的 hypervisor:KVM 和 Lguest,再探索 QEMU。</p>\n<p>虚拟化就是通过某种方式隐藏底层物理硬件的过程,从而让多个操作系统可以透明地使用和共享它。这种架构的另一个更常见的名称是平台虚拟化。下图就展示了该分层架构。</p>\n<p><img src=\"./virtual.gif\" alt=\"常用硬件虚拟化的简单分层架构\"></p>\n<p>平台虚拟化的好处很多。美国环境保护署(EPA)报告的一组有趣的统计数据就证明了其好处。EPA 研究服务器和数据中心的能源效率时发现,实际上服务器只有 5% 的时间是在工作的。在其他时间,服务器都处于 “休眠” 状态。在单个服务器上的虚拟化平台能够改善服务器的利用率,但是减少服务器的数量才是它的最大功用。减少服务器数量意味着减少不动资产、能耗、冷却和管理成本。使用更少的硬件还能提高可靠性。</p>\n<p>hypervisor 是一种运行在物理服务器和操作系统之间的中间层软件,可以允许多个操作系统和应用共享一套基础物理硬件。可以将hypervisor 看做是虚拟环境中的“元”操作系统,可以协调访问服务器上的所有物理设备和虚拟机,所以又称为虚拟机监视器(virtual machine monitor)。hypervisor 是所有虚拟化技术的核心,非中断的支持多工作负载迁移是 hypervisor 的基本功能。当服务器启动并执行 hypervisor 时,会给每一台虚拟机分配适量的内存,cpu,网络和磁盘资源,并且加载所有虚拟机的客户操作系统。</p>\n<h3>Hypervisor分类</h3>\n<p>hypervisor 可以划分为两大类。首先是类型 1,这种 hypervisor 是直接运行在物理硬件之上的。其次是类型 2,这种 hypervisor 运行在另一个操作系统(运行在物理硬件之上)中。类型 1 hypervisor 的一个例子是基于内核的虚拟机(KVM —— 它本身是一个基于操作系统的 hypervisor)。类型 2 hypervisor 包括 QEMU 和 WINE。</p>\n<h3>Hypervisor 构成</h3>\n<p>hypervisor(不管是什么类型)仅是一个从其来宾操作系统抽象机器硬件的分层应用程序。通过这种方式,每个来宾操作系统看到的仅是一个 VM 而不是真实的硬件机器。我们大致看一下 hypervisor 的内部组成,以及它在 VM(来宾操作系统)上的表示。</p>\n<p>hypervisor 需要少量设施启动来宾操作系统:一个需要驱动的内核映像、一个配置(比如 IP 地址和所需的内存量)、一个磁盘盒一个网络设备。磁盘和网络设备通常映射到机器的物理磁盘和网络设备(如下图所示)。最后,需要使用一组来宾操作系统工具启动和管理来宾操作系统。</p>\n<p><img src=\"./resources.gif\" alt=\"hypervisor 中的最小资源映射\"></p>\n<p>然后,一个简化的 hypervisor 架构实现最后的关键功能,从而使来宾操作系统可以和宿主操作系统同时运行。实现这个功能需要一些特定的要素,如下图所示。首先,类似于将用户空间应用程序和内核函数连接起来的系统调用,一个通常可用的虚拟化调用(hapercall,hypervisor 对操作系统进行的系统调用)层允许来宾系统向宿主操作系统发出请求。可以在内核中虚拟化 I/O,或通过来宾操作系统的代码支持它。故障必须由 hypervisor 亲自处理,从而解决实际的故障,或将虚拟设备故障发送给来宾操作系统。hypervisor 还必须处理在来宾操作系统内部发生的异常。(毕竟,来宾操作系统发生的错误仅会停止该系统,而不会影响 hypervisor 或其他来宾操作系统)。hypervisor 的核心要素之一是页映射器,它将硬件指向特定操作系统(来宾或 hypervisor)的页。最后,需要使用一个高级别的调度器在hypervisor和来宾操作系统之间传输控制。</p>\n<p><img src=\"./linux.gif\" alt=\"简化的基于 Linux 的hypervisor\"></p>\n<h4>KVM</h4>\n<p>KVM (Kernel-based Virtual Machine)针对运行在 x86 硬件硬件上的、驻留在内核中的虚拟化基础结构。KVM 是第一个成为原生 Linux 内核(2.6.20)的一部分的 hypervisor。</p>\n<p><img src=\"./KVM.gif\" alt=\"KVM hypervisor的高级别视图\"></p>\n<p>KVM 在平台虚拟化中利用 QEMU,并使用 Linux 作为hypervisor,因此实现了这个构思,即让来宾操作系统能够和其他 Linux 应用程序协调执行。</p>\n<h4>Lguest</h4>\n<p><img src=\"./Lguest.gif\" alt=\"实现 x86 准虚拟化的 Lguest 的架构\"></p>\n<h3>Linux hypervisor</h3>\n<p>下面讨论两个基于 Linux 的 hypervisor。</p>\n<h3>QEMU</h3>\n<p>这部分主要抄录自<a href=\"#Reference\"><sup>[2]</sup></a>。</p>\n<p>QEMU 是一个系统模拟器,不仅提供访客系统的虚拟化平台,还提供了整个系统的虚拟化,包括 PCI host controller,disk,network,video hardware,USB controller 和其他硬件设施。</p>\n<h2>Virtio</h2>\n<p>这部分主要翻译自<a href=\"#Reference\"><sup>[3]</sup></a>。</p>\n<p>简而言之,Virtio 是一个半虚拟化 hypervisor 中的虚拟设备之上的一个抽象层。 virtio 是由 Rusty Russell 开发的,最初的目的是支持上面提到的,他自己开发的虚拟化方案 lguest。我们先讨论一下半虚拟化和虚拟设备,然后探索 virtio的实现细节,我们关注的是 2.6.30 内核中的virtio框架。</p>\n<p>Linux 提供了大量各有优劣的实现 hypervisor 的方案,诸如 KVM,lguest 和 User-mode Linux。有了这些不同的方案,可以按需选择,比如各种虚拟化外设。virtio 提供了一个统一的前端来进行外设虚拟化,提供了标准化的接口来提高不同平台代码的重用性。</p>\n<h3>全虚拟化和半虚拟化</h3>\n<p>让我们讨论一下两类虚拟化机制:全虚拟化和半虚拟化。在全虚拟化中,访客系统运行在完全虚拟的环境中,但是他不知道自己是在虚拟环境下,不需要进行特殊的配置。相对的,半虚拟化中,访客操作系统不仅知道他是在 hypervisor 中运行,而且它还会提供代码使得 guest-to-hypervisor 的转换更加高效。</p>\n<p>在全虚拟化机制下,hypervisor 必须模拟外设硬件,模拟最底层的信息,比如模拟一个网卡驱动。尽管在这层抽象下的虚拟化是彻底而纯净的,它也是最低效和极度复杂的一种虚拟化方案。在半虚拟化机制下,访客和 hypervisor 能够配合地让虚拟化更加高效,它的缺陷是操作系统知道自己是运行在虚拟化系统上的,需要进行改动。</p>\n<p><img src=\"./full_para.gif\" alt=\"全虚拟化和半虚拟化中的设备虚拟化\"></p>\n<p>上图中,在传统的全虚拟化环境中,在访客系统发出请求时,hypervisor 需要陷入 trap 中,然后去模拟硬件的行为。尽管这样更具扩展性,能够允许不加改动的操作系统运行为访客系统,但是不够高效。上图右边的半虚拟化环境下,访客操作系统明白它运行在 hypervisor 上,它知道自己有前端驱动,hypervisor 则负责实现了特定外设的驱动后端。这些前后端驱动就是 virtio 的来源,它为虚拟设备提供了标准化的接口,提升开发效率和代码复用率。</p>\n<h3>Linux guest的抽象</h3>\n<p>在之前的章节中,我们看到 virtio 是半虚拟化 hypervisor 中一系列常见的虚拟设备的抽象。这种设计允许 hypervisor 向外暴露一系列常见的虚拟化设备,并提供公共 API。下图就解释了它的重要性。借助半虚拟化 hypervisor,访客实现了一系列公共的接口,特定的外设则被后端的驱动所模拟。后端驱动并不必是公共的,只要他们能够实现前端需要的行为就行了。</p>\n<p><img src=\"./driver.gif\" alt=\"Driver abstractions with virtio\"></p>\n<p>注意在真实环境下,外设的虚拟化发生在用户空间,使用的是 QEMU,所以后端驱动借助 QEMU 和 hypervisor 的用户空间来通信并管理设备的 I/O 。</p>\n<p>virtio 的 API 依赖于一个简单的 buffer 抽象,来包裹访客需要的命令和数据。让我们深入 virtio API 和他的组成部分。</p>\n<h3>Virtio 架构</h3>\n<p>除了在访客中实现的前端驱动和 hypervisor 中实现的后端驱动,virtio 还定义了两层来实现 guest-to-hypervisor 的通信。在最上层(也叫作 virtio)是虚拟队列接口,将前端和后端联系起来。驱动能够使用 0 或多个队列,取决于他们的需求。例如,virtio 网络驱动使用两个虚拟队列(一个接受,另一个传输),而 virtio block 驱动就只使用一个。虚拟队列实际上成环状的,但是也不是必须如此,只要前后端保持一致就行了。</p>\n<p><img src=\"./architecture.gif\" alt=\"High-level architecture\"></p>\n<p>上图中罗列了五个前端驱动,即block devices (例如磁盘),network devices,PCI emulation,balloon driver (用来动态管理访客的内存使用) 和 console driver。每个前端驱动都有一个对应的后端驱动。</p>\n<h3>概念框架</h3>\n<p>从访客的视角来看,下图定义了 object hierarchy。在最上方的是 virtio_driver,它代表了访客的前端驱动。符合这个驱动的设备被封装成了 virtio_device(一种访客中的设备表示形式)。它与 virtio_config_ops 结构有关,该结构则负责定义和配置 virtio device的行为。virtqueue 又关联到了 virtio_device,在服务时维护一个对设备的引用。最后每个 virtqueue 对象引用了 virtqueue_ops 对象,该对象定义了底层队列用来服务 hypervisor 驱动的行为。</p>\n<p><img src=\"./hierarchy.gif\" alt=\"Object hierarchy of the Virtio front end\"></p>\n<p>进程在一开始会创建一个 virtio driver,然后通过 register_virtio_driver 来注册它。 virtio_driver 结构定义了上层设备驱动,列出了驱动至此的设备的 ID,一个支持的特性的表(取决于外设的类型)以及一系列回调函数。当 hypervisor 在设备列表中识别出来一个匹配设备ID的新设备,就会调用 probe 函数来传输 virtio_device 对象(该函数在virtio_driver 对象中提供)。该对象和管理外设使用的数据(这和特定的驱动有关)一起被缓存下来。取决于外设的类型,virtio_config_ops函数可能会被触发,来读取或设置外设特定的选项。例如,从一个 virtio_blk 设备获取硬盘的读写状态,或是设置 block device 的 block size。</p>\n<p>注意 virtio_device 不会包含 virtqueue 的引用 (但是 virtqueue 确实引用了virtio_device)。为了识别和 virtqueue 相关联的 virtio_device,你可以使用 virtio_config_ops 对象的 find_vq 函数。这个对象会返回和 virtio_device 实例相关的虚拟队列。find_vq 函数也会允许给 virtqueue 提供回调函数的特化,用来告知访客系统来自 hyperviso 的 response buffer。</p>\n<p>virtqueue 是一个简单的结构,声明了一个额外的回调函数(该回调函数会在 hypervisor 消耗掉 buffer中的数据时候调用),一个 virtio_device 的引用,一个 virtqueue_ops 的引用和一个特殊的 priv 引用指向了要使用的底层实现。尽管回调函数是可选的,也可以动态启用或禁用该回调。</p>\n<p>但是该结构的核心是 virtqueue_ops,它定义了指令和数据是怎样在访客和 hypervisor之间移动的。让我们看看如何向队列添加或删除对象。</p>\n<h3>Virtio buffer</h3>\n<p>前端驱动和后端驱动是通过 buffer 来交流的。对于一个 I/O 请求,访客提供一个或多个 buffer 来表示它。例如,你可以提供三个 buffer,第一个代表一个读请求、之后的两个代表对应的响应数据。在内部,该配置是使用 scatter-gather list 来表示的,列表的每一项都是一个地址和一个长度。</p>\n<h3>Core API</h3>\n<h3>Virtio Vring</h3>\n<p>详见 <a href=\"#Reference\"><sup>[4]</sup></a> 。</p>\n<h2>Docker</h2>\n<h2>Reference</h2>\n<ol>\n<li><a href=\"https://www.ibm.com/developerworks/cn/linux/l-hypervisor/\">剖析 Linux hypervisor</a> <div id=\"ref1\"/></li>\n<li><a href=\"https://www.cnblogs.com/echo1937/p/7138294.html\">https://www.cnblogs.com/echo1937/p/7138294.html</a></li>\n<li><a href=\"https://developer.ibm.com/articles/l-virtio/\">Virtio: An I/O virtualization framework for Linux</a></li>\n<li><a href=\"https://www.cnblogs.com/yi-mu-xi/p/12544695.html\">virtio之vring</a></li>\n<li><a href=\"https://www.linuxprobe.com/docker-and-vm.html\">容器、虚拟机与Docker概念全解析</a></li>\n</ol>\n",
"url": "https://forsworns.github.io///zh/blogs/20210226/",
"title": "虚拟化基础学习笔记",
"summary": "基础知识",
"date_modified": "2021-02-26T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Rust宏学习笔记</h1>\n<p>前面的部分基本是 Rust 语言手册翻译。</p>\n<p>Rust 中的宏相较C++更为强大。C++ 中的宏在预处理阶段可以展开为文本,Rust 的宏则是对语法的扩展,是在构建语法树时,才展开的宏。</p>\n<h2>Rust中宏的分类</h2>\n<p>Rust 中宏可以分为很多类,包括通过 macro_rules 定义的<strong>声明式宏</strong>和三种<strong>过程式宏</strong></p>\n<ul>\n<li>custom derive 可推导宏,借助 <code>#[derive]</code> 属性标签,它可以用在 struct 和 enum 上</li>\n<li>attribute-like 本身就是一个标签,可以作用于任何地方</li>\n<li>function-like 看上去像函数,但是作用在 token 上,即把token作为函数参数</li>\n</ul>\n<p>所以为什么需要宏?</p>\n<p>为了偷懒、为了让代码更简洁。使用宏可以快速生成大量代码,避免重复劳动。Rust 宏扩展了语法,你是不会想要每次都老老实实地写繁复的代码的,所以学一点魔法!</p>\n<p>为什么不用函数或者模板?</p>\n<ul>\n<li>Rust 的函数必须限定好参数类型和参数个数,而且他并没有提供变长模板参数,所以嘛,哈哈。事实上有不少库为了应对未知个数参数的情况,手写了不同个数参数的函数,而且很蛋疼的是 Rust 也不允许同名函数的重载 :) 当然我还是最喜欢Rust了。</li>\n<li>宏在编译期展开,所以可以用来给 struct 添加 trait,这必须在运行前完成,而函数需要等到运行时才会执行。</li>\n</ul>\n<p>但是坏处(如果算的话)就是宏更难书写、理解和维护;同时函数可以定义、引入在文件里的任何地方,而在使用宏之前必须确保他被定义、引入到上方的代码中了。</p>\n<p>下面开始记录宏的写法!</p>\n<h2>声明式宏</h2>\n<p>在Rust中,应用最广泛的一种宏就是声明式宏,类似于模式匹配的写法,将传入的 Rust 代码与预先指定的模式进行比较,在不同模式下生成不同的代码。</p>\n<p>使用<code>macro_rules!</code>来定义一个声明式宏。</p>\n<p>最基础的例子是很常见的<code>vec!</code>:</p>\n<pre><code class=\"language-rust\">let v: Vec<u32> = vec![1, 2, 3];\n</code></pre>\n<p>简化版的定义是(实际的版本有其他分支,而且该分支下要预先分配内存防止在push时候再动态分划)</p>\n<pre><code class=\"language-rust\">#[macro_export]\nmacro_rules! vec {\n ( $( $x:expr ),* ) => {\n {\n let mut temp_vec = Vec::new();\n $(\n temp_vec.push($x);\n )*\n temp_vec\n }\n };\n}\n</code></pre>\n<p>::: <code>$( $x:expr ),*</code>和<code>$( $x:expr,)*</code>的区别是什么?</p>\n<p>前者,最后的<code>,</code>是<a href=\"https://doc.rust-lang.org/reference/macros-by-example.html\"><strong>MacroRepSep</strong></a>,意味着 <code>1,2,3</code>是一个合法的序列。</p>\n<p>后者,最后的<code>,</code>是<a href=\"https://doc.rust-lang.org/reference/macros-by-example.html\"><strong>MacroMatch</strong></a> 的一部分,意味着 <code>1,2,3,</code>才是一个合法的序列。</p>\n<p>:::</p>\n<p><code>#[macro_export]</code>标签是用来声明:只要 use 了这个crate,就可以使用该宏。同时包含被 export 出的宏的模块,在声明时必须放在前面,否则靠前的模块里找不到这些宏。</p>\n<p>按照官方文档的说法,<code>macro_rules!</code>目前有一些设计上的问题,日后将推出新的机制来取代他。但是他依然是一个很有效的语法扩展方法。</p>\n<p>这里一个注意点是:如果想要创建临时变量,那么必须要像上面这个例子这样,放在某个块级作用域内,以便自动清理掉,否则会认为是不安全的行为。</p>\n<p>:::声明宏中支持的语法树元变量类型</p>\n<p>出自 <a href=\"https://doc.rust-lang.org/reference/macros-by-example.html#metavariables\">Macros By Example - The Rust Reference</a>。</p>\n<p>回顾编译原理 :)</p>\n<ul>\n<li><code>item</code>: 随便一个什么 <a href=\"https://doc.rust-lang.org/reference/items.html\">东西</a>,准确定义参考上述手册中</li>\n<li><code>block</code>: 一个 <a href=\"https://doc.rust-lang.org/reference/expressions/block-expr.html\">块表达式</a></li>\n<li><code>stmt</code>: 一个 <a href=\"https://doc.rust-lang.org/reference/statements.html\">语句</a>,但是不包含结尾的分号,除了必须有分号的 item statements</li>\n<li><code>pat_param</code>: 一个 <a href=\"https://doc.rust-lang.org/reference/patterns.html\">匹配模式</a></li>\n<li><code>pat</code>: 等价于 <code>pat_param</code></li>\n<li><code>expr</code>: 一个 <a href=\"https://doc.rust-lang.org/reference/expressions.html\">表达式</a></li>\n<li><code>ty</code>: 一种 <a href=\"https://doc.rust-lang.org/reference/types.html#type-expressions\">类型</a></li>\n<li><code>ident</code>: 一个 <a href=\"https://doc.rust-lang.org/reference/identifiers.html\">标识符或关键字</a></li>\n<li><code>path</code>: 一条 <a href=\"https://doc.rust-lang.org/reference/paths.html#paths-in-types\">TypePath</a> 形式的路径</li>\n<li><code>tt</code>: <a href=\"https://doc.rust-lang.org/reference/macros.html#macro-invocation\">Token 树</a> (一个独立的 <a href=\"https://doc.rust-lang.org/reference/tokens.html\">token</a> 或一系列在匹配完整的定界符 <code>()</code>、<code>[]</code> 或 <code>{}</code> 中的 token)</li>\n<li><code>meta</code>: <a href=\"https://doc.rust-lang.org/reference/attributes.html\">标签</a> 中的内容</li>\n<li><code>lifetime</code>: 一个 <a href=\"https://doc.rust-lang.org/reference/tokens.html#lifetimes-and-loop-labels\">生命周期标识</a></li>\n<li><code>vis</code>: 可能不存在的 <a href=\"https://doc.rust-lang.org/reference/visibility-and-privacy.html\">可见性标记</a>(并不是所有函数、类型都会使用 <code>pub</code> 进行标记,所以可能是不存在的)</li>\n<li><code>literal</code>: 匹配 <a href=\"https://doc.rust-lang.org/reference/expressions/literal-expr.html\">文本表达式</a></li>\n</ul>\n<p>:::</p>\n<h2>过程式宏</h2>\n<p>第二类是过程式的宏,它更像函数,他接受一些代码作为参数输入,然后对他们进行加工,生成新的代码,他不是在做声明式宏那样的模式匹配。三种过程式宏都是这种思路。</p>\n<p>不能在原始的crate中直接写过程式宏,需要把过程式宏放到一个单独的crate中(以后可能会消除这种约定)。定义过程式宏的方法如下:</p>\n<pre><code class=\"language-rust\">use proc_macro;\n\n#[some_attribute]\npub fn some_name(input: TokenStream) -> TokenStream {\n}\n</code></pre>\n<p>需要引入<code>proc_macro</code> 这个 crate,然后标签是用来声明它是哪种过程式宏的,接着就是一个函数定义,函数接受 <code>TokenStream</code>,返回 <code>TokenStream</code>。<code>TokenStream</code> 类型就定义在 <code>proc_macro</code> 包中,表示 token 序列。除了标准库中的这个包,还可以使用<code>proc_macro2</code> 包,使用 <code>proc_macro2::TokenStream::from()</code> 和 <code>proc_macro::TokenStream::from()</code> 可以很便捷地在两个包的类型间进行转换。使用 <code>proc_macro2</code> 的好处是可以在过程宏外部使用 <code>proc_macro2</code> 的类型,相反 <code>proc_macro</code> 中的类型只可以在过程宏的上下文中使用。且 <code>proc_macro2</code> 写出的宏更容易编写测试代码。</p>\n<p>下面详细说明如何定义三类过程宏。</p>\n<h3>Custom Derive 宏</h3>\n<p>在本节中,我们的目的是实现下面的代码,使用编译器为我们生成名为 <code>HelloMacro</code> 的 <code>Trait</code></p>\n<pre><code class=\"language-rust\">use hello_macro::HelloMacro;\nuse hello_macro_derive::HelloMacro;\n\n#[derive(HelloMacro)]\nstruct Pancakes;\n\nfn main() {\n Pancakes::hello_macro();\n}\n</code></pre>\n<p>该 <code>Trait</code> 的定义如下,目的是打印实现该宏的类型名</p>\n<pre><code class=\"language-rust\">pub trait HelloMacro {\n fn hello_macro();\n}\n</code></pre>\n<p>由于过程宏不能在原 crate 中实现,我们需要如下在 <code>hello_crate</code> 的目录下新建一个 <code>hello_macro_derive</code> crate</p>\n<pre><code class=\"language-bash\">cargo new hello_macro_derive --lib\n</code></pre>\n<p>在新的 crate 内,我们需要修改 <code>Cargo.toml</code> 配置文件,</p>\n<pre><code class=\"language-toml\">[lib]\nproc-macro = true\n\n[dependencies]\nsyn = "1.0"\nquote = "1.0"\n</code></pre>\n<p>在 <code>src/lib.rs</code> 中可以着手实现该宏,其中 <code>syn</code> 是用来解析 rust 代码的,而quote则可以用已有的变量生成代码的 <code>TokenStream</code>,可以认为 <code>quote!</code> 宏内的就是我们想要生成的代码</p>\n<pre><code class=\"language-rust\">extern crate proc_macro;\n\nuse proc_macro::TokenStream;\nuse quote::quote;\nuse syn;\n\n#[proc_macro_derive(HelloMacro)]\npub fn hello_macro_derive(input: TokenStream) -> TokenStream {\n // Construct a representation of Rust code as a syntax tree\n // that we can manipulate\n let ast = syn::parse(input).unwrap();\n\n // Build the trait implementation\n impl_hello_macro(&ast)\n}\n\nfn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {\n let name = &ast.ident;\n let gen = quote! {\n impl HelloMacro for #name {\n fn hello_macro() {\n println!("Hello, Macro! My name is {}!", stringify!(#name));\n }\n }\n };\n gen.into()\n}\n</code></pre>\n<p>另外,<strong>Custom Derive 宏可以携带Attributes,称为 Derive macro helper attributes</strong>,具体编写方法可以参考 <a href=\"https://doc.rust-lang.org/reference/procedural-macros.html#derive-macro-helper-attributes\">Reference</a>(Rust 中共有<a href=\"https://doc.rust-lang.org/reference/attributes.html\">四类 Attributes</a>)。关于 Derive macro helper attributes 这里有一个坑就是<strong>在使用 <code>cfg_attr</code> 时,需要把 Attributes 放在宏之前。</strong></p>\n<p>举个栗子:</p>\n<p>使用 kube-rs 可以很方便地定义 CRD(Custom Resource Definition):</p>\n<pre><code class=\"language-rust\">#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]\n#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]\nstruct FooSpec {\n info: String,\n}\n</code></pre>\n<p>我第一反应是 <code>#[kube]</code> 是一个 Attribute-Like 宏,但是查阅 kube-rs 文档才发现它其实是 <code>CustomResource</code> Custom Derive 宏的 Attribute。这里我们想用 <code>cfg_attr</code> 来控制是否去做 derive,一开始就想当然地这么写了:</p>\n<pre><code class=\"language-rust\">#[cfg_attr(feature="use_kube_rs",\n derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema),\n kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)\n)]\nstruct FooSpec {\n info: String,\n}\n</code></pre>\n<p>然而这是错误的打开方式,需要写成:</p>\n<pre><code class=\"language-rust\">#[cfg_attr(feature="use_kube_rs",\n kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced),\n derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)\n)]\nstruct FooSpec {\n info: String,\n}\n</code></pre>\n<p>Attributes 需要写在宏的 derive 前面。</p>\n<h3>Attribute-Like 宏</h3>\n<p>attribute-like 宏和 custom derive 宏很相似,只是标签可以自定义,更加灵活,甚至可以使用在函数上。他的使用方法如下,比如假设有一个宏为 <code>route</code> 的宏</p>\n<pre><code class=\"language-rust\">#[route(GET, "/")] \nfn index() { ... }\n</code></pre>\n<p>按下面的语法定义 <code>route</code> 宏</p>\n<pre><code class=\"language-rust\">#[proc_maco_attribute]\npub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { ... }\n</code></pre>\n<p>其中 <code>attr</code> 参数是上面的 <code>Get</code>,<code>"/"</code> ;<code>item</code> 参数是 <code>fn index(){}</code> 。</p>\n<h3>Function-Like 宏</h3>\n<p>这种宏看上去和 <code>macro_rules!</code> 比较类似,但是在声明式宏只能用 <code>match</code> 去做模式匹配,但是在这里可以有更复杂的解析方式,所以可以写出来</p>\n<pre><code class=\"language-rust\">let sql = sql!(SELECT * FROM posts WHERE id=1);\n</code></pre>\n<p>上面这个 <code>sql</code> 宏的定义方法如下</p>\n<pre><code class=\"language-rust\">#[proc_macro]\npub fn sql(input: TokenStream) -> TokenStream { ... }\n</code></pre>\n<h2>好用的库</h2>\n<p><a href=\"https://doc.rust-lang.org/proc_macro/index.html\">proc_macro</a>:默认 token 流库,只能在过程宏中使用,编译器要用它,将它作为过程宏的返回值,大多数情况我们不需要,只需要在宏返回结果的时候把 <code>proc_macro2::TokenSteam</code> 的流 <code>into()</code> 到 <code>proc_macro::TokenSteam</code> 就行了。</p>\n<p><a href=\"https://crates.io/crates/proc_macro2\">proc_macro2</a>:我们真正在使用的过程宏库,可以在过程宏外使用。</p>\n<p><a href=\"https://crates.io/crates/syn\">syn</a>:过程宏左护法,可以将 <code>TokenStream</code> 解析成语法树,注意两个 <code>proc_macro</code> 和 <code>proc_macro</code> 都支持,需要看文档搞清楚库函数到底是在解析哪个库中的 <code>TokenStream</code>。</p>\n<p><a href=\"https://crates.io/crates/quote\">quote</a>:过程宏右护法,将语法树解析成 <code>TokenStream</code>。只要一个 <code>quote!{}</code> 就够了!<code>quote!{}</code> 宏内都是字面量,即纯纯的代码,要替换进去的变量是用的 <code>#</code> 符号标注,为了和声明宏中使用的 <code>$</code> 相区分(也就意味着用 <code>quote</code> 写过程宏的时候,可以和声明宏结合 🤤 )。模式匹配时用到的表示重复的符号和声明宏中一样,是使用 <code>*</code>。</p>\n<p><a href=\"https://crates.io/crates/darling\">darling</a> 好用到跺 jio jio 的标签宏解析库,让人直呼 Darling!</p>\n<h2>MacroKata</h2>\n<p>2022年12月更新</p>\n<p>看到了一个宏教程项目 <a href=\"https://tfpk.github.io/macrokata/\">MacroKata</a>,刷了一下,目前<strong>教程中仅包含声明式的宏</strong>,读到了一些之前没注意的点。</p>\n<p>对于声明式宏:</p>\n<ul>\n<li>\n<p>除了<code>$ </code>和分隔符(<code>{}</code>、<code>()</code>、<code>[]</code>)外任意token都可以用在模式里面,如</p>\n<pre><code class=\"language-rust\">macro_rules! math {\n ($a:literal plus $b:literal) => {\n $a+$b\n };\n (square $a:literal) => {\n $a*$a\n };\n}\n</code></pre>\n</li>\n<li>\n<p>把宏包装成函数接口可以避免被 <code>cargo expand</code> 展开,比如教程中为了简洁,就尽可能把 <code>println!</code> 单独包装到了函数里</p>\n<blockquote>\n<p>However, <code>macrokata</code> tries to avoid (as much as possible) using macros we didn't define inside the main function. The reason for this is that, if we did use <code>println!</code> you would see its expansion as well.</p>\n</blockquote>\n</li>\n<li>\n<p>重复的参数模式只可以放到末尾,除非有明确的分隔符,否则不知道到底匹配多少个,会带来歧义。比如下面这个例子,想要表达至少有两个参数。第一种是不可行的,因为在匹配规则的时候无法往后看是否是最后一个参数。</p>\n<pre><code class=\"language-rust\">// wrong! \nmacro_rules! sum {\n ($($expr:expr),+ , $lastexpr:expr) => {\n $($expr + )+ $lastexpr\n }\n}\n// right!\nmacro_rules! sum {\n ($lastexpr:expr, $($expr:expr),+) => {\n $lastexpr $(+$expr)+ \n }\n}\n</code></pre>\n</li>\n<li>\n<p>声明式的宏的匹配带有顺序,匹配到合法项后就不会继续匹配了,比如下面这个例子中, <code>'a'</code> 是一个字面量,但是匹配到了第一条,导致第二条更加严格的模式没有被匹配到。</p>\n<pre><code class=\"language-rust\">macro_rules! ordering {\n ($j:expr) => { "This was an expression" };\n ($j:literal) => { "This was a literal" };\n}\n\nlet expr1 = ordering!('a'); // => "This was an expression".\nlet expr1 = ordering!(3 + 5); // => "This was an expression".\n</code></pre>\n</li>\n<li>\n<p>嵌套的重复的参数:<code>( $( $( $val:expr ),+ );+ )</code>,当然 separator 可以随便替换成任意除<code>*</code>、<code>+</code>、<code>?</code>(这三个用于模式里面表示重复次数,所以会带来歧义)、<code>$</code>、分隔符之外的token。</p>\n</li>\n<li>\n<p>声明式宏调用声明式宏的时候,内部的宏能够看到的AST是不透明的,因此一般只能和外界采用相同的参数类型。但是<code>ident</code>、<code>lifetime</code>、<code>tt</code>比较特殊,可以被内部的<code>literal</code> 匹配。如下面这个例子</p>\n<pre><code class=\"language-rust\">macro_rules! foo {\n ($l:expr) => { bar!($l); }\n// ERROR: ^^ no rules expected this token in macro call\n}\n\nmacro_rules! bar {\n (3) => {}\n}\n\nfoo!(3);\n\n// compiles OK\nmacro_rules! foo {\n ($l:tt) => { bar!($l); }\n}\n\nmacro_rules! bar {\n (3) => {}\n}\n\nfoo!(3);\n</code></pre>\n</li>\n<li>\n<p>宏可以递归,比如下面这个宏</p>\n<pre><code class=\"language-rust\">enum LinkedList {\n Node(i32, Box<LinkedList>),\n Empty\n}\n\nmacro_rules! linked_list {\n () => {\n LinkedList::Empty\n };\n ($expr:expr $(, $exprs:expr)*) => {\n LinkedList::Node($expr, Box::new(linked_list!($($exprs),*)))\n }\n}\n\nfn main() {\n let my_list = linked_list!(3, 4, 5);\n}\n</code></pre>\n<p>但是宏递归很慢,因此默认 rustc 会有 128 层的限制,可以在包层面配置标签 <code>#![recursion_limit = "256"]</code>。</p>\n</li>\n</ul>\n<h2>收录有趣的宏样例</h2>\n<p>本章收录到的宏尽可能短小、独立、有趣。</p>\n<h3>你这写的啥啊</h3>\n<p>记录一下自己写的一些有趣的宏,以防下次碰到这种情况忘记咋写。</p>\n<ul>\n<li>\n<p>这里的实际需求是处理标签宏参数,用了 <a href=\"https://crates.io/crates/darling\">darling</a> 库做解析,然后处理一些 <code>Option</code> 类型的可选参数,如果标签宏参数中没有它(即 <code>darling</code> 解析出 <code>None</code>),就不理会它,在后续构造中使用默认值。感觉有意思的地方在于过程宏和声明宏的混合使用,在写出来之前我没想到这么写真能跑 = =</p>\n<pre><code class=\"language-rust\">macro_rules! expand_attribute {\n ($($attr:expr),*) => {\n {\n let mut token = TokenStream2::new();\n $(if let Some(val) = $attr {\n token.extend(quote!{$attr: #val,});\n })*\n token\n }\n };\n}\n</code></pre>\n<p>使用时是这么用的</p>\n<pre><code class=\"language-rust\">use darling::FromMeta;\n\n#[derive(Debug, FromMeta)]\nstruct Attrs{\n #[darling(default)]\n pub param1: Option<f64>,\n #[darling(default)]\n pub param2: Option<f64>,\n #[darling(default)]\n pub param3: Option<f64>,\n}\n\n#[derive(Debug, Default)]\nstruct Struct{\n pub param1: f64,\n pub param2: f64,\n pub neccessary: String, // cannot be empty or any default value\n}\n\n#[proc_macro_attribute]\npub fn an_attribute(attr: TokenStream, item: TokenStream) -> TokenStream {\n let Attrs {\n param1, \n param2, \n ... // Attrs::param3 is not useful in Struct\n } = match Attrs::from_list(&attr) {\n Ok(v) => v,\n Err(e) => {\n return TokenStream::from(e.write_errors());\n }\n };\n let optional_params = expand_attribute!(param1, param2);\n let build_a_struct = quote! {\n Struct {\n neccessary: "0817", \n #optional_params\n ..Default::default()\n }\n };\n // TL;DR\n}\n</code></pre>\n<p>看得出来还是比较繁琐的,</p>\n</li>\n<li>\n<p>这里的实际需求是用标签宏修改原函数返回值为 Result,是在 <a href=\"https://github.com/sentinel-group/sentinel-rust\">sentinel-group/sentinel-rust</a> 的实现中,用来快速给一个函数或方法创建 sentinel 的。当时的想法是用 Result 来表达某个流是否被阻碍,同时可以传递 Sentinel 的告警给用户,实现出来的很垃圾,可以说是只支持使用一个规则。没有试过多个这样的标签宏嵌套,但是估计是回调地狱重现世间 :sweat_smile: (或许可以用 <code>std::Result::flatten()</code> 来避免,但是它目前还是 nightly 的 API)。</p>\n<p>这里的实现也有点蠢,是用的 quote 和 syn 自动解析的修改后的函数签名,尝试过手动构造,但是太恶心了构造不来。</p>\n<pre><code class=\"language-rust\">pub(crate) fn process_func(mut func: ItemFn) -> ItemFn {\n let output = func.sig.output;\n // Currently, use quote/syn to automatically generate it,\n // don't know if there is a better way.\n // Seems hard to parse new ReturnType only or construct ReturnType by hand.\n let dummy_func = match output {\n ReturnType::Default => {\n quote! {\n fn dummy() -> Result<(), String> {}\n }\n }\n ReturnType::Type(_, return_type) => {\n quote! {\n fn dummy() -> Result<#return_type, String> {}\n }\n }\n };\n let dummy_func: ItemFn = syn::parse2(dummy_func).unwrap();\n // replace the old ReturnType to the dummy function ReturnType\n func.sig.output = dummy_func.sig.output;\n func\n}\n</code></pre>\n</li>\n</ul>\n<h3>还得学习一个</h3>\n<p>本章节抄录一些别人写的黑魔法宏。</p>\n<h4>MacroKata 中的柯里化示例</h4>\n<h5>匿名函数 自动推导返回类型</h5>\n<p>通过声明式宏的递归逐层展开</p>\n<pre><code class=\"language-rust\">macro_rules! curry {\n (_, $block:block) => {$block};\n (($argident:ident : $argtype:ty) => $(($argidents:ident: $argtypes:ty) =>)* _, $block:block) => {\n move |$argident: $argtype| {\n print_curried_argument($argident);\n curry!($(($argidents: $argtypes) =>)* _, $block)\n }\n };\n}\n</code></pre>\n<pre><code class=\"language-rust\">fn main() {\n let is_between = curry!((min: i32) => (max: i32) => (item: &i32) => _, {\n min < *item && *item < max\n });\n\n let curry_filter_between = curry!((min: i32) => (max:i32) => (vec: &Vec<i32>) => _, {\n let filter_between = is_between(min)(max);\n vec.iter().filter_map(|i| if filter_between(i) { Some(*i) } else { None }).collect()\n });\n\n let between_3_7 = curry_filter_between(3)(7);\n let between_5_10 = curry_filter_between(5)(10);\n\n let my_vec = vec![1, 3, 5, 6, 7, 9];\n // 5,6\n let some_numbers: Vec<i32> = between_3_7(&my_vec);\n // 6,7,9\n let more_numbers: Vec<i32> = between_5_10(&my_vec);\n}\n\n</code></pre>\n<h5>显示写出返回类型</h5>\n<p>下面的<code>box_type!</code>宏同样通过声明式宏的递归构造出返回类型</p>\n<pre><code class=\"language-rust\">macro_rules! curry_unwrapper {\n ($block:block) => {\n $block\n };\n (\n $argname:ident: $argtype:ty,\n $($argnames:ident: $argtypes:ty,)*\n $block:block\n ) => {\n Box::new(move |$argname : $argtype | {\n curry_unwrapper!($($argnames: $argtypes,)* $block)\n })\n }\n}\n\nmacro_rules! box_type {\n (=> $type:ty) => {\n $type\n };\n ($type:ty $(,$argtypes:ty )* => $restype:ty) => {\n Box<dyn Fn($type) -> box_type!($($argtypes ),* => $restype)>\n }\n}\n\nmacro_rules! curry_fn {\n (\n $ident:ident,\n ($argname:ident: $argtype:ty)\n -> $(($argnames:ident: $argtypes:ty))->*\n => $restype:ty, $block:block\n ) => {\n fn $ident($argname: $argtype) -> box_type!($($argtypes ),* => $restype) {\n curry_unwrapper!($($argnames: $argtypes,)* $block)\n }\n }\n}\n\nfn main() {\n curry_fn!(add, (a: i32) -> (b: i32) -> (c: i32) -> (d: i32) => i32, {\n a + b + c + d\n });\n\n let res = add(3)(2)(3)(4);\n}\n</code></pre>\n<h2>References</h2>\n<p><a href=\"https://doc.rust-lang.org/book/ch19-06-macros.html\">Macros - The Rust Programming Language (rust-lang.org)</a></p>\n<p><a href=\"https://doc.rust-lang.org/reference/macros.html\">Macros - The Rust Reference</a></p>\n<p><a href=\"https://danielkeep.github.io/tlborm/book/index.html\">The Little Book of Rust Macros</a></p>\n<p><a href=\"https://dengjianping.github.io/2019/02/28/%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E8%BF%87%E7%A8%8B%E5%AE%8F(proc-macro).html\">如何编写一个过程宏(proc-macro)</a></p>\n<p><a href=\"https://tfpk.github.io/macrokata/\">MacroKata - Exercises for Rust Macros</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210224/",
"title": "Rust宏学习笔记",
"summary": "Keep Learning!",
"date_modified": "2021-02-24T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>STM32MP157A和IPCC</h1>\n<p>STM32MP157 是基于 ARM 的 32 位的 MPU,很多厂商都基于这个 MPU 包装了开发板,这类板子很有意思的地方在于 ST 的这款 MPU 搭载了两个处理器,其中一个包含两颗 ARM Cortex A7,另一个则是单个 ARM Cortex M4。前者可以用来运行 Linux,且具有可信执行环境;后者一般用来管理传感器或可以运行一些实时物联网操作系统,比如 RTOS、TencentOS-tiny 等。</p>\n<p>关于MPU和MCU的区别,参考了<a href=\"https://zhuanlan.zhihu.com/p/106860696?utm_source=wechat_session\">知乎相关专栏</a>。MPU的全称叫Micro Processor Unit,MCU的全称是Mirco Controller Unit。首先这两个词都有一个Micro开头,其实这就表明了这是计算/控制单元小型化后出现的技术。原来有多片分立的元件组成的计算机系统向高度集成化发展,多个芯片/元件的功能在向一颗芯片集中。</p>\n<p>搭载MCU的计算机系统不承担主要的工作负载,而主要是起辅助/协调/控制作用。因此这种情况下集成化的计算机系统就不太需要强大的计算/处理能力。所以对应的形态应该是运行频率低、运算能力一般,但是需要集成化程度高(使用方便)、价格低廉(辅助系统不应增加太多成本)等因素。而随着ARM的32位MCU的出现,采用ARM的M系列的MCU也开始逐步扩大市场。以ST、NXP公司的产品为主要代表。MPU从一开始就定位了具有相当的处理和运算能力,一帮需要运行较大型的操作系统来实现复杂的任务处理。因此这就决定了MPU应该具备比较高的主频,和较为强大的运算能力。</p>\n<p>ARM Cortex芯片系列:</p>\n<ul>\n<li>ARM Cortex-A:支持 ARM和Thumb指令集,并支持虚拟地址和内存管理,用于应用领域。</li>\n<li>ARM Cortex-R:支持 ARM和Thumb指令集,只支持物理地址,支持内存管理,用于实时性领域。</li>\n<li>ARM Cortex-M:只支持Thumb指令集,用于微处理器领域。</li>\n</ul>\n<h2>开发板、STM32CubeIDE 注意点备忘(实践相关,很乱,放前面是为了自己快点看到,读者可以先跳过)</h2>\n<ul>\n<li>\n<p>换了机器后,在 STM32CubeIDE 里直接打开原来是 working space 是不行的,它记录的是绝对路径,记得把 working space 下的工程都删掉,重新导入工程;同时记得在 project 选项栏下选 make clean 把原先的记录都清掉。</p>\n</li>\n<li>\n<p>build 项目的时候注意选好是哪个项目,注意是用的 JTAG 还是 A7 上的 Linux (根据板子启动模式的不同来选择),如果是 linux 的话对每个项目可以单独选择它在远端的路径、名称(Remote Settings),也是在项目的 property 里设置。</p>\n</li>\n<li>\n<p>用 A7 上的 Linux 调试的时候(STM32CubeIDE 里调试选项的生产模式),可以用串口连接它,开一个终端;也可以给板子连上网线,自己另开一个控制台用 ssh 来连接。后者要方便一些,因为我们的程序中可以要输出一些调试值到串口,影响 A7 的使用。但是还是需要用 USB 线去连开发机,加载出来 Remote NDIS 网卡,在 CubeIDE 里 Debug Configuration 里设置调试器 IP 为 Remote NDIS 网卡的地址,否则调试可能会出问题:我试了一下给板子接上网线,在 CubeIDE 里用局域网内的地址,虽然可以把编译出的项目文件拷贝到 A7 上的 <code>/usr/local/project</code> (这里后面有一条详细写了),但是没法用 OpenOCD 下断点调试。</p>\n</li>\n<li>\n<p>记得在项目的 property 属性(可以通过邮件点击项目找到)里的 C/C++ General 下的 Path and Symbols 看有没有添加上头文件路径等。</p>\n</li>\n<li>\n<p>开发板上烧写 Ubuntu18.04,参考华清远见提供的手册,往 flash 上烧写要用 emmc 对应的配置,但是实测失败了,往 sd 卡上烧写ubuntu 的 raw 镜像倒是成功了(注意 sd 卡启动是 101,emmc 启动是010)。</p>\n</li>\n<li>\n<p>官方提供的 Ubuntu 18.04 镜像上没有支持 usb otg,所以用 Ubuntu 做多核通信部分不能按手册里的方式配置,可以给板子接个网线,因为 STM32CubeIDE 是用 ssh 连接的,用 scp 复制的文件,这个镜像默认是没有 netplan 工具的,所以要按之前版本的 Ubuntu 改静态 IP 的方法,在 <code>/etc/network/interfaces</code> 中修改成静态 IP,在 <code>/etc/resolv.conf</code> 里修改 DNS(注意两边网卡名匹配)。然后<code>ip addr flush dev ${your-net-device}</code> 刷新设置、<code>ifdown ${your-net-device</code>} 、 <code>ifup ${your-net-device}</code> 重启网卡。也可以把这些设置提前写到别的文件里,在 <code>~/.bashrc</code> 里用这些文件通过 <code>cp</code> 等方式默认配置文件,这样每次开机都可以自己覆盖掉(当然下面记录的部分是用的默认的 st open linux,所以是用 usb otg 搞得 RNDIS,从而连接的)</p>\n</li>\n<li>\n<p>这个镜像也没有 <code>modprobe</code> 工具,如果用到需要自己安 <code>kmod</code> 包</p>\n</li>\n<li>\n<p>因为板子是 Cortex-A7 的,在这个板子上的 apt 需要换用 arm 源,华为有提供而且可以 wget 直接下……如果配的是 x86 源的会报找不到源还以为是网不好;同时记得注意版本,18.04 名字是 Bionic Beaver,链接都要换成 Bionic,可以用 <code>sed -i 's/xxxx/Bionic/g' /etc/apt/source.list</code> 做快速替换</p>\n</li>\n<li>\n<p>STM32CubeIDE 选 Linux 调试是用 ssh 和板子上的 Linux 建立的连接,默认是用的 root 用户,我没有找到在哪里改用别的用户连接开发板,所以只能在板上的系统里开启允许 root 用户建立 ssh 的选项。需要修改 <code>/etc/ssh/sshd_config</code> 文件,把 <code>PermitRootLogin prohibit-password</code> 改成 <code>PermitRootLogin yes</code>,然后重启相关服务 <code>sudo service ssh restart</code></p>\n</li>\n<li>\n<p>STM32CubeIDE 会把编译出的 elf 文件发送给板子,存在上面提到的 Remote Settings 中默认的 <code>/usr/local/project/${project-name}</code> 下面,可以去板子上的 Linux 这个目录下找,可以看到下面这样的自动生成的脚本。</p>\n<pre><code class=\"language-bash\">#!/bin/sh\nrproc_class_dir="/sys/class/remoteproc/remoteproc0"\nfmw_dir="/lib/firmware"\nfmw_name="xxxxx.elf"\n\nif [ $1 == "start" ]\nthen\n\t# Start the firmware\n\tcp lib/firmware/$fmw_name $fmw_dir\n\techo -n "$fmw_name" > $rproc_class_dir/firmware\n\techo -n start > $rproc_class_dir/state\nfi\n\nif [ $1 == "stop" ]\nthen\n\t# Stop the firmware\n\techo -n stop > $rproc_class_dir/state\nfi\n</code></pre>\n<p>然后是用的 remoteproc 的方式在 Cortex-M4 上加载的 elf 文件,所以理论上,我们在 Cortex-A7 上可以通过这种方式,向 Cortex-M4 提供准备好的 elf 文件。当然,这里 remoteproc 只是在做开关机等操作,在启动后,Cortex-A7 和 Cortex-M4 日常的通信可以用 <code>/dev/RPMsg0</code> 和 <code>/dev/RPMsg1</code></p>\n</li>\n</ul>\n<h2>硬件</h2>\n<p>搭载的 SOC 是 STM32MP157AAA3,在 STM32Cube 里面记住选这个。</p>\n<p>板子上还有一个缩写为 MPU 的东西,叫 Memory Protection Unit,即内存保护单元,其作用如下</p>\n<ul>\n<li>阻止用户应用程序破坏操作系统使用的数据。</li>\n<li>阻止一个任务访问其它任务的数据区,从而把任务隔开。</li>\n<li>可以把关键数据区设置为只读,从根本上消除了被破坏的可能。</li>\n<li>检测意外的存储访问,如,堆栈溢出,数组越界。</li>\n<li>此外,还可以通过MPU设置存储器regions的其它访问属性,比如是否缓冲等。</li>\n</ul>\n<p>仿真器上可以接一路板子上的调试串口来调试 Cortex M4 上的程序,用 USB OTG 和电脑连接。</p>\n<p>板子上的的 USB OTG 是用来做数据传输、镜像烧录的。</p>\n<p>在 Wi-Fi 蓝牙天线接口的旁边有一个拨片开关是用来设置启动配置的,实测和下图中定义的不符?</p>\n<ul>\n<li><strong>001</strong>是工程模式(写字母的一端上方,代表1;写数字的一端下方,代表0)。可以进行 Cortex M4 的开发和调试。</li>\n<li><strong>101</strong>是让 Cortex A7 加载 TF 卡内镜像启动, <strong>000</strong> 是烧录镜像给板载 Flash,<strong>010</strong>是加载板载 Flash 内镜像启动。</li>\n<li>注意 0 和 1 可能搞反,启动 / 调试不了拨过来重新试一下。</li>\n</ul>\n<p><img src=\"./boot_mode.png\" alt=\"boot_mode\"></p>\n<p>电源旁边的是 reset 键,另一个是可以自定义的按键。</p>\n<p>因为 MPU 内部的接口是固定的,板子则是厂商自定义的,所以在<strong>使用板子前需要在 STM32CubeMX 里先设定好用到的接口</strong>。在原理图目录下的 pdf 中可以查看开发版上的引脚。比如第十三章中讲解 GPIO 的,查到引脚是 PZ5,PZ6,PZ7,于是在STM32CubeIDE中需要调用STM32CubeMX工具,将这几个引脚设置为GPIO_OUTPUT。</p>\n<p>STM32MP157 有三个运行环境,有一些 peripheral 在某些 context 是没法用的。</p>\n<ul>\n<li>Arm dual core Cortex-A7 secure (Trustzone), running a Secure Monitor or Secure OS like <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/OP-TEE_overview\">OP-TEE</a></li>\n<li>Arm dual core Cortex-A7 non secure , running <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/STM32MP15_Linux_kernel_overview\">Linux</a></li>\n<li>Arm Cortex-M4 (non-secure), running <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/STM32CubeMP1_architecture\">STM32Cube</a></li>\n</ul>\n<p>大多数 peripheral 只能分配给一个 context,比如<a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/USART_internal_peripheral\">USART</a> 和 <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/I2C_internal_peripheral\">I2C</a>。<br>\n有一些则可以在多个 contexts 之间共享,一般是系统的 peripheral 如 <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/PWR_internal_peripheral\">PWR</a> 和 <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/RCC_internal_peripheral\">RCC</a>。</p>\n<p>具体的分配规则如下两图所示,第一幅图是第二幅图的图例,文字版见官方<a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/STM32MP15_peripherals_overview\">wiki</a>。</p>\n<img src=\"./STM32MP1IPsOverview_legend.png\" style=\"zoom:150%;\" />\n<p><img src=\"./STM32MP1IPsOverview.png\" alt=\"\"></p>\n<h2>软件工具</h2>\n<p>STM32CubeProgrammer 是用来烧录镜像的工具。</p>\n<p>STM32CubeMX 可提供以下服务(在使用前一般要对 MPU 内部的 peripheral 进行设置,MPU 外部是做开发板的人已经连好的):</p>\n<ul>\n<li>STM32 微控制器和微处理器的选择</li>\n<li>引脚排列,时钟,外设和中间件配置</li>\n<li>项目创建和初始化代码的生成</li>\n</ul>\n<p><strong>STM32CubeIDE 是集成了 STM32CubeMX</strong> 的 IDE ,创建项目的时候就可以自动加载它先进行引脚等的配置,同时该IDE还自带了 ARM 工具链和 GDB 调试工具。</p>\n<p>在用 STM32CubeIDE 进行初始化设置的时候,<strong>记得在 Project Manager 中勾选 “Generate peripheral initialization as a pair of '.c/.h' files per peripheral”</strong>,这样生成的代码更容易阅读。</p>\n<p>同时在<strong>设置引脚的时候,记得右键勾选 pin reserved</strong>,把引脚分配给某个运行上下文(context 在后面 IPCC 部分也会提到),否则按照默认设置是 free,就只是设置了引脚为输入、输出等,但是还是不知道是哪个 cpu 控制着它,STM32CubeMX 就不会自动为我们生成初始化代码。</p>\n<p>配置完成后使用 "ctrl+s" 就可以自动保存配置并生成初始化代码。</p>\n<h2>STM32学习笔记</h2>\n<p>华清远见教程手册第三部分是 Cortex M4 的实验内容;第四部分是 Cortex A7 的实验内容。</p>\n<h3>Cortex M4</h3>\n<p>先来看一下架构图,我们在编程时,只需要操作硬件抽象层(HAL层)的 API 就行了。</p>\n<p><img src=\"./STM32CubeMPUPackageArchitecture.png\" alt=\"\"></p>\n<h4>GPIO</h4>\n<p>华清远见教程 P247 左右。在创建工程后注意 IDE 会先调用 STM32CubeMX 让你初始化板子,不然很多相关头文件都要自己手动 include,初始化等工作也要自己做,配置后会自动为我们生成工程文件。同时因为有两个核,还要设置 Pin Reservation 给 Cortex M4。在Project Manager 的 Code Generator 处选择为每个外设生成单独的 <code>.c/.h</code> 文件以便阅读。在 STM32CubeMX 里左键点击引脚可以进行类型选择,选择后右键再点击可以选择分配给的是哪个核,下方还有搜索框可以搜索引脚,被搜索到的引脚在 Pinout View 中会闪烁。这里因为我们买的是华清远见做好的板子,所以哪个引脚对应什么功能,是华清远见已经连接好的,比如这里 LED 它给连接的就是 PZ5,PZ6,PZ7 这三个引脚。</p>\n<p>IO 口可以由软件配置成4种模式,其实操作的是GPIO 的端口模式寄存器</p>\n<ul>\n<li>输入(复位状态)/input(reset state)</li>\n<li>通用输出模式/ general purpose output mode</li>\n<li>复用功能模式/ alternate function mode</li>\n<li>模拟模式/ analog mode</li>\n</ul>\n<p>对应的宏为 <code>GPIO_MODE_INPUT</code>,<code>GPIO_MODE_OUTPUT_PP</code>,<code>GPIO_MODE_OUTPUT_OD</code>, <code>GPIO_MODE_AF_PP</code>,<code>GPIO_MODE_AF_OD</code>,分別是对应不同的寄存器值。</p>\n<p>IO 操作重要结构体:<code>GPIO_InitTypeDef</code>中定义了Pin、Mode、Pull、Speed、Alternate。</p>\n<p>设置 GPIO 引脚调用的HAL 函数:<br>\n<code>void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);</code><br>\n第一个参数传的是GPIO 所在的组,第二个是该组的几号管脚,第三个是对管脚进行置位。</p>\n<h4>按键扫描</h4>\n<p>华清远见教程 P256 左右。</p>\n<p><code>void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)</code>可以读引脚输入值,默认是 pullup,所以按下后是 0 。</p>\n<p><code>void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)</code>来对输出引脚值取反。</p>\n<h4>外部中断</h4>\n<p>除了常规的设置引脚以外,记得要在左侧列表下的 GPIO 设置项目的 NVIC 界面中勾选使能对应的中断,否则要自己在 <code>gpio.c</code> 中启用外部中断并定义优先级</p>\n<pre><code class=\"language-c\">HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);\nHAL_NVIC_EnableIRQ(EXTI0_IRQn);\n</code></pre>\n<p>同时要在 <code>stm32mp1xx_it.h/c</code> 中定义 <code>void EXTI0_IRQHandler(void)</code>,调用 <code>HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_XXX)</code>,同时注册一个回调函数 <code>void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)</code>。同时要尽可能避免在中断处理程序(ISR)中去调用 <code>HAL_Delay()</code>,因为后者也是通过中断实现的,用在一起需要调整 ISR 的优先级。</p>\n<h4>串行通讯接口</h4>\n<p>串口通信的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。串口通信是异步的。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。</p>\n<ul>\n<li>\n<p>波特率:这是一个衡量通信速度的参数。它表示每秒钟传送的bit 的个数。例如 300 波特表示每秒钟发送300 个bit。当我们提到时钟周期时,我们就是指波特率。例如如果协议需要4800波特率,那么时钟是4800Hz。这意味着串口通信在数据线上的采样率为4800Hz。通常电话线的波特率为14400,28800 和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB 设备的通信。</p>\n</li>\n<li>\n<p>数据位:这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据不会是8 位的,标准的值是5、7 和8 位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7 位)。扩展的ASCII 码是0~255(8 位)。如果数据使用简单的文本(标准ASCII 码),那么每个数据包使用7 位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。</p>\n</li>\n<li>\n<p>停止位:用于表示单个包的最后一位。典型的值为1 ,1.5 和2 位。这里的1.5 位的数据宽度,就是1.5 个波特率,由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。</p>\n</li>\n<li>\n<p>奇偶校验位:在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位位1 ,这样就有3 个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。</p>\n</li>\n<li>\n<p>硬件流控制: 硬件流控制常用的有RTS/CTS 流控制和DTR/ R(数据终端就绪/数据设置就绪)流控制。硬件流控制必须将相应的电缆线连上,用RTS/CTS(请求发送/清除发送)流控制时,应将通讯两端的RTS、CTS 线对应相连,数据终端设备(如计算机)使用RTS 来起始调制解调器或其它数据通讯设备的数据流,而数据通讯设备(如调制解调器)则用CTS 来起动和暂停来自计算机的数据流。这种硬件握手方式的过程为:我们在编程时根据接收端缓冲区大小设置一个高位标志(可为缓冲区大小的75%)和一个低位标志(可为缓冲区大小的25%),当缓冲区内数据量达到高位时,我们在接收端将CTS 线置低电平(送逻辑0),当发送端的程序检测到CTS 为低后,就停止发送数据,直到接收端缓冲区的数据量低于低位而将CTS 置高电平。RTS 则用来标明接收设备有没有准备好接收数据。</p>\n</li>\n</ul>\n<p>STM32 串口设置一般可以总结为如下几个步骤:</p>\n<ol>\n<li>串口时钟使能,GPIO 时钟使能</li>\n<li>设置引脚复用映射</li>\n<li>GPIO 初始化设置,模式为复用功能</li>\n<li>串口参数初始化:设置波特率,字长,奇偶校验等参数</li>\n<li>开启中断并初始化NVIC,使能中断(如果需要开启中断才需要这个步骤)</li>\n<li>使能串口</li>\n<li>编写中断处理函数</li>\n</ol>\n<p>相关 HAL API:</p>\n<p><code>HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);</code> 轮询方式发送。</p>\n<p><code>HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);</code> 轮询方式接收。</p>\n<p><code>HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);</code> 中断方式发送,需要重载 <code>void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);</code> 。</p>\n<p><code>HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);</code> 中断方式接收,需要重载 <code>void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);</code> 。</p>\n<p>在配置时,除了需要找到 UART4 的 TX 和 RX 对应的引脚外,还需要在左侧列表中找到 UART4,并把它在 Cortex-M4 上激活,激活后,模式下拉选择 Asynchronous,如果要用中断方式,需要在 NVIC Settting 栏下勾选 Enabled 使能串口中断。</p>\n<h4>HSEM</h4>\n<p>STM32MP157 手册第 11 章。</p>\n<p>在讨论 IPCC 之前,STM32MP1官方 wiki 里也提到了 HSEM, 即 Hardware Semaphore,说是用于coprocessor的,但是读了STM32MP157的手册发现似乎是用于 Cortex A7 的两个核的。</p>\n<p>HSEM总共设计了32个32位(4 Bytes)的寄存器用于存储信号量,对这些寄存器的读写都是以字节(4 Bytes)为单位的,以半字、Byte方式读写是无效的。</p>\n<p>关于信号量的使用,有一本很好的书,<strong><a href=\"https://www.researchgate.net/publication/249954903_The_Little_Book_of_Semaphores\">The Little Book Of Semaphores</a></strong>。</p>\n<h4>IPCC</h4>\n<p>STM32MP157 手册第 12 章、<a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/IPCC_internal_peripheral\">官方wiki</a>。</p>\n<p>IPCC全称 inter-processor communication controller 可以用来在两个处理器间传递数据,他提供的是 non-blocking 的信号机制,使用原子操作发送和获取信息。视通信的模式,从 MCU SRAM 中分划出一部分作为共享内存。IPCC是不安全的 peripheral。在启动时无法使用。</p>\n<p>IPCC peripheral 提供了管理 IPCC 通信的机制。每个处理器都有自己独立的寄存器组和中断。</p>\n<p>该芯片上共有6条双向通道,每条通道又被划分为两条朝向相反的单向的子通道。</p>\n<h5>单向子通道</h5>\n<p>子通道包含:</p>\n<ul>\n<li>一个 flag,该 flag 在occupied 和 free 之间变化,当发送数据时,发送方可以设置为 occupied,接收方收到后清空为 free。去看手册会发现,他是靠寄存器实现的,事实上可以看成是 hardware 的 semaphore 或者是 mutex :happy:,起到互斥的作用 。</li>\n<li>两个相关的中断。这两个中断在不同通道间是共享的,也就是说同一时刻只有一个通道可以触发中断让处理器对他进行读写。即使是你有办法让 6 条通道火力全开,并行传输,也会受到这里的限制。这在下面的概况图中表示为外围设备部分的6条通道,是共同经过一个 OR 门触发 IRQ 的。\n<ul>\n<li>RXO:"receiver" 端的 RX 通道被占用,即Rx_Occupied</li>\n<li>TXF:"sender" 端的 TX 通道是空闲的,即Tx_Free</li>\n</ul>\n</li>\n<li>两个相关的中断掩码</li>\n</ul>\n<h5>通信模式</h5>\n<p>理论上 IPCC 可以有三种运行通信模式:</p>\n<ul>\n<li>Simplex communication mode\n<ul>\n<li>只使用一个子通道。</li>\n<li>单向信息:一旦发送方的处理器已经向内存发送了数据,他就立刻把信道状态 flag 设置为 occupied 状态;接收方一旦读取完了信息就把 flag 清除为 free 状态。</li>\n</ul>\n</li>\n<li>Half-duplex communication mode\n<ul>\n<li>只使用一个子信道。</li>\n<li>双向信息:一旦发送方的处理器已经向内存发送了数据,他就立刻把信道状态 flag 设置为 occupied 状态;当接收方读取完信息且 response 在共享内存中可用了,再清空 flag。</li>\n</ul>\n</li>\n<li>Full-duplex communication mode\n<ul>\n<li>以异步的方式使用子信道。</li>\n<li>通过将信道设置为 occupied 状态,任何一个处理器都可以异步地发送信息;当接收处理器收到信息后,清楚 flag。该模式可以被视为两个 simplex 模式在一个给定信道上的结合。</li>\n</ul>\n</li>\n</ul>\n<p>但是STM32MP157的手册中只有前两种,下面分别给出具体的电位图,理解起来更方便:</p>\n<p><img src=\"./simplex.png\" alt=\"\"></p>\n<p><img src=\"./half_duplex.png\" alt=\"\"></p>\n<h5>架构</h5>\n<p>IPCC的架构为</p>\n<ul>\n<li>IPCC processor 1 是 Cortex-A7 non-secure, 使用<a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Linux_Mailbox_framework_overview\">Linux mailbox framework</a>进行管理。</li>\n<li>IPCC processor 2 是 Cortex-M4,适应 <a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/STM32CubeMP1_architecture\">IPCC HAL driver</a>进行管理。</li>\n</ul>\n<p>可以用下图概括</p>\n<p><img src=\"./IPCC_overview.png\" alt=\"\"></p>\n<h5>Linux Mailbox <div id=\"mailbox\"/></h5>\n<p>Mailbox框架用于异构多核系统中处理器之间的数据和信号传递。该框架下有 controller 和 client:</p>\n<ul>\n<li>Controller 用来设置和管理来自 IPCC peripheral 的 IRQ,叫 stm32_ipcc。</li>\n<li>Client 用来管理数据的发送和接收,用户可以自定义Client,例如 RPMsg 框架使用了该 mailbox 进行处理器间的通信,在这种情况下 Client 就是 remoteproc 驱动。</li>\n</ul>\n<p>架构图如下</p>\n<p><img src=\"./Mailbox_overview.png\" alt=\"\"></p>\n<p>使用细节参见文档:</p>\n<p><a href=\"https://github.com/STMicroelectronics/linux/blob/v5.4-stm32mp/Documentation/mailbox.txt\">https://github.com/STMicroelectronics/linux/blob/v5.4-stm32mp/Documentation/mailbox.txt</a></p>\n<p><a href=\"https://github.com/STMicroelectronics/linux/blob/v5.4-stm32mp/include/linux/mailbox_controller.h\">https://github.com/STMicroelectronics/linux/blob/v5.4-stm32mp/include/linux/mailbox_controller.h</a></p>\n<p><a href=\"https://github.com/STMicroelectronics/linux/blob/v5.4-stm32mp/include/linux/mailbox_client.h\">https://github.com/STMicroelectronics/linux/blob/v5.4-stm32mp/include/linux/mailbox_client.h</a></p>\n<p>具体设置参见:</p>\n<p><a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Linux_Mailbox_framework_overview\">Linux mailbox framework</a></p>\n<p><a href=\"https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Menuconfig_or_how_to_configure_kernel\">https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Menuconfig_or_how_to_configure_kernel</a></p>\n<h5>RPMsg <div id=\"rpmsg\"/></h5>\n<p>RPMsg是基于 <strong>virtio</strong> 的 messaging bus,允许本地处理器和远程处理器进行交流。它是用 virtio vring 在共享内存上发送和接收数据的。</p>\n<p>具体而言,vring 是单向的,总共需要开两个实现双向传输,在两个处理器都能看到的内存空间上会开辟共享的 buffer。使用 <a href=\"#mailbox\">Mailbox framework</a> 来通知处理器在共享内存中有待读取的数据。</p>\n<p>基于这些框架, RPMsg框架实现了一个基于信道的通讯方式,信道使用文本信息进行区分,同时有本地地址和远端地址,类似 socket 通信?需要注意的是在远端也需要实现 RPMsg框架,具体的实现有很多种,比较出名的是 <a href=\"https://www.openampproject.org/\"><strong>OpenAMP</strong></a>。</p>\n<blockquote>\n<p>实践相关:在 STM32CubeIDE 里添加 OpenAMP 支持后,会自动添加 OpenAMP 到 <code>Middleware</code> 目录下,在 <code>M4</code> 目录下的是 ST 在它之上做的一层包裹。包裹后视作 virtual uart 结构,可以支持双向通信。</p>\n<p>底层的IPCC,是创建了两个 vring,分别分配给 channel1 和 channel2,而在<code>mbox_ipcc.c</code> 的注释里直接画出来了通信的方向:</p>\n<pre><code class=\"language-c\">/*\n* Channel direction and usage:\n*\n* ======== <-- new msg ---=============--------<------ =======\n* || || || CHANNEL 1 || || ||\n* || A7 || ------->-------=============--- buf free--> || M4 ||\n* || || || ||\n* ||master|| <-- buf free---=============--------<------ ||slave||\n* || || || CHANNEL 2 || || ||\n* ======== ------->-------=============----new msg --> =======\n*/\n</code></pre>\n<p>A7是被当做 master(<code>RPMSG_MASTER</code> / <code>VIRTIO_DEV_SLAVE</code>),rpmsg 的 endpoint 存储在 <code>VIRT_UART_HandleTypeDef huart0</code> 中;M4 是 slave(<code>RPMSG_REMOTE</code> / <code>VIRTIO_DEV_SLAVE</code>),rpmsg 的 endpoint 存储在 <code>VIRT_UART_HandleTypeDef huart1</code> 中。vring 的具体定义在 rsc_table.c 中,每个 vring 上有 16 个 buffer。共享内存的总大小是 32KB,是定义在链接文件 <code>STM32MP157AAAX_RAM.ld</code> 中的,定义成了</p>\n<pre><code>RAM2_ipc_shm\t\t(xrw)\t: ORIGIN = 0x10040000,\tLENGTH = 0x00008000\n</code></pre>\n<p>在 OpenAMP 的 <code>rpmsg_virtio.h</code> 中定义的 <code>RPMSG_BUFFER_SIZE</code> 则是 512。</p>\n</blockquote>\n<p>在 RPMsg client 的实现中,需要关注两个概念:</p>\n<ul>\n<li>\n<p>RPMsg channel:</p>\n<p>RPMsg client 是关联于 RPMsg channel 的,RPMsg channel 建立在本地和远端处理器之间。client使用文本信息,即 service name,注册在 RPMsg 框架中,当找到了本地注册过的服务名和远端发布的服务名,就建立通讯信道。</p>\n</li>\n<li>\n<p>RPMsg endpoint:RPMsg 端点通过RPMsg 信道,提供逻辑连接。一个端点有它独特的地址和对应的回调函数,允许用户在同一个信道上绑定多个端点。当一个用户驱动用本地地址创建了一个端点,所有目标地址和该端点本地地址相同的到达数据都会被路由到该端点。注意每个信道有一个默认的端点,所以应用即使不创建新端点,也可以进行通信。</p>\n</li>\n</ul>\n<p>架构图如下</p>\n<p><img src=\"./Rpmsg_overview.png\" alt=\"\"></p>\n<p>文档:<a href=\"https://www.kernel.org/doc/Documentation/rpmsg.txt\">https://www.kernel.org/doc/Documentation/rpmsg.txt</a></p>\n<h5>RPROC</h5>\n<p>全称为 remote processor (RPROC) ,该框架允许不同的平台、架构忽略硬件差异去远程管理(开关、加载固件)其他处理器</p>\n<p>STM32MP157中,有两个组成部分</p>\n<p><strong>remoteproc</strong>,抽象的远程处理器管理框架,功能如下:</p>\n<ul>\n<li>在远程处理器的内存中加载 ELF firmware</li>\n<li>解析 firmware resource table ,以控制相关资源(例如IPC、memory)</li>\n<li>控制远程处理器的执行,远程让他运行或停止,即 Life Cycle Management (LCM)</li>\n<li>提供检测、debug 远程固件的服务</li>\n</ul>\n<p><strong>stm32_rproc</strong>,ST公司编写特定的远程处理器驱动,功能如下:</p>\n<ul>\n<li>将供应商编写的特定回调函数注册到 RPROC 框架中</li>\n<li>管理远程平台中与处理器相关的资源(例如 registers、watchdogs、reset、clock 和 memories)</li>\n<li>通过 mailbox 框架向远程的处理器推送通知(kicks)</li>\n</ul>\n<p>架构图如下,<a href=\"#rpmsg\">RPMsg</a> 是建立在 RPROC 之上的,但是也有直接使用RPROC的其他应用如 sysfs 和 debugfs。</p>\n<p><img src=\"./Remoteproc_overview.png\" alt=\"\"></p>\n<p>文档:<a href=\"https://www.kernel.org/doc/Documentation/remoteproc.txt\">https://www.kernel.org/doc/Documentation/remoteproc.txt</a></p>\n<h5>peripheral 设置</h5>\n<p>IPCC peripheral 连接了 Cortex A7 和 Cortex M4,因此需要在两个处理器上都进行设置,内部的 peripheral 在使用时候需要通过STM32CubeMX对板子进行设置</p>\n<p>默认设置 processor1 是 Cortex A7 non-secure,processor2 是 Cortex M4。信道默认按下表设置</p>\n<table>\n<thead>\n<tr>\n<th>信道</th>\n<th>模式</th>\n<th>用途</th>\n<th>软件框架(A7)</th>\n<th>软件框架(M4)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>信道1</td>\n<td>全双工</td>\n<td>M4发送,A7接收</td>\n<td>RPMsg</td>\n<td>OpenAMP</td>\n</tr>\n<tr>\n<td>信道2</td>\n<td>全双工</td>\n<td>A7发送,M4接收</td>\n<td>RPMsg</td>\n<td>OpenAMP</td>\n</tr>\n<tr>\n<td>信道3</td>\n<td>Simplex</td>\n<td>用于终止 Cortex M4 的运行</td>\n<td>RemoteProc</td>\n<td>CprocSync cube utility</td>\n</tr>\n<tr>\n<td>信道4</td>\n<td></td>\n<td>free</td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td>信道5</td>\n<td></td>\n<td>free</td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td>信道6</td>\n<td></td>\n<td>free</td>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n<p>IPCC相关设置不能直接单独在某个 context 下开启,一定是两个处理器都参与其中的,在STM32CubeMX中需要设置成 A7 non-secure 和 M4 两个 context。</p>\n<h5>相关代码</h5>\n<p>1.2.0 版本下 HAL 和 LL 层的源码:</p>\n<ul>\n<li><code>\\Cortex-M4\\STM32Cube_FW_MP1_V1.2.0\\Drivers\\STM32MP1xx_HAL_Driver\\Inc\\stm32mp1xx_ll_ipcc.h</code></li>\n<li><code>\\Cortex-M4\\STM32Cube_FW_MP1_V1.2.0\\Drivers\\STM32MP1xx_HAL_Driver\\Src\\stm32mp1xx_hal_ipcc.c</code></li>\n<li><code>\\Cortex-M4\\STM32Cube_FW_MP1_V1.2.0\\Drivers\\STM32MP1xx_HAL_Driver\\Inc\\stm32mp1xx_hal_ipcc.h</code></li>\n</ul>\n<h3>Cortex A7</h3>\n<h4>环境搭建</h4>\n<p>首先安装了Xshell用于串口调试,之后需要安装 STM32CubeProgrammer,在 STM32 平台上的 Flash 设备中创建分区并对各个分区进行镜像烧录。可以使用 STM32CubeProgrammer工具来烧录 STM32 MPU板上支持的所有 Flash 设备,可以对 eMMC 和 SDCard中的镜像更新。</p>\n<h4>OpenSTLinux源码编译</h4>\n<p>教程第 35 章,P382 左右</p>\n<h4>OpenSTLinux镜像烧录</h4>\n<p>教程第 36 章,P401 左右</p>\n<p>该板子支持通过 STM32CubeProgrammer 工具进行镜像的烧录,还可以通过 BootLoader下的 ums 工具配合 ubuntu 系统进行单个镜像更新,使用 TFTP 下载方式进行镜像的下载,使用 scp 方式更新 linux 内核和设备树。</p>\n<p>在烧录前需要将开发板断电,挑战拨码开关,将开关拨到 <strong>000</strong>。</p>\n<p><img src=\"./boot_photo.png\" alt=\"\"></p>\n<p>使用 USB 方式烧写镜像的时候,要用 USB mini 线来进行数据传输,注意这里是接在板子上的那个 USB mini。</p>\n<p>同时接上调试器的 USB mini,根据设备管理器里的端口信息,打开 Xshell,在连接选项中选择 Serial 连接协议,然后设置好端口号等信息,然后进行连接。</p>\n<p>之后打开 STM32CubeProgrammer,开发板上电,在右上角切换到USB连接设置下,可以看到接着板子的 USB 端口(如果看不到尝试复位),点击 Connect 按钮,右上角会显示为绿色的 Connected。</p>\n<p>打开镜像所在目录,不同文件夹下是不同的 tsv 格式的配置文件。例如文件名称中带有 emmc 的是片上 flash 烧录设置,而 sdcard 的是 TF 卡的烧录配置,后缀带有 trusted 和 optee 的是不同的启动模式,二者均为带有安全机制的启动方式。烧录前需要将配置文件复制到上级目录中,和镜像文件放在一起(也可以不移动,在后面的 STM32CubeProgrammer 中选择 binaries path)。在 STM32CubeProgrammer 中,点击 Open File,选择刚刚复制过去的配置文件。等待几分钟即可完成烧录,将开关拨片拨至 <strong>010</strong> 来启动系统。</p>\n<p>也可以通过 USB Image Tool 工具制作 TF 系统卡。在 USB Image Tool 中可以看到 TF 卡和读卡器,读到盘符后将 raw 格式的 Weston 镜像。打开 Favorites 选项卡,添加一个镜像后点击 Restore, 等待烧录完成后,将开关拨片拨至 <strong>101</strong> 来启动系统。</p>\n<h3>利用 OpenAMP 的多核协同工作(A7 和 M4 通信)</h3>\n<p>需要在 CubeMX 里同时选择 OpenAMP 和 IPCC 下的 Cortex M4 选项,注意还需要勾选 activated</p>\n<p>STM32MP157 默认设置的共享内存是 32K</p>\n<p>第 56 章, P613 附近</p>\n<p>源码在 <code>\\Cortex-M4\\STM32Cube_FW_MP1_V1.2.0\\Projects\\STM32MP157A-FSMP1\\Applications\\OpenAMP\\OpenAMP_TTY_echo</code> 目录下</p>\n<p>需要将整个文件目录都导入 STM32CubeIDE,同时需要连接两条 USB mini 线,开发版会被识别为 RNDIS 网络适配器设备,在设备管理器中如果找不到该设备,可能是被识别成了串口或 USB 设备,需要卸载驱动重装。还不能用只能换线、换电脑试试了。</p>\n<p>下面记录下 STMicroelectronics Micro Controller Development Application Team 的文档中对该示例的描述。</p>\n<p>该示例主要演示了如何使用 OpenAMP MW 和 Virtual UART 来创建看上去就像 Linux 系统中的 TTY 一样的设备的处理器间通讯通道。</p>\n<p>在该示例中,调用的是 CPU2 (Cortex-M4,CM4)上的固件,默认的 CPU1 是 Cortex-A7 (CA7),在 CPU1 上需要运行 Linux。</p>\n<p>OpenAMP MW 使用下列硬件资源:</p>\n<ul>\n<li>IPCC peripheral,用来在 CA7 和 CM4 之间传递事件信号(mailbox)。</li>\n<li>MCU SRAM peripheral 用来缓存通信信息 (virtio buffers) 。</li>\n<li>该例子中保留的共享内存区域:SHM_ADDR=0x10040000,SHM_SIZE=128k。它定义在 <code>platform_info.c</code> 文件中。</li>\n</ul>\n<p>OpenAMP 的工作流程:</p>\n<ul>\n<li>主处理器使用 <code>remoteproc</code> 去在远程处理器上装载和运行一个远程应用</li>\n<li>在远程应用运行时,在主从应用之间建立 rpmsg 信道</li>\n<li>使用 <code>rpmsg</code> API 进行 IPC</li>\n</ul>\n<p>具体而言,以主处理器向从处理器传递消息为例,可以总结为下图(这里used是说已经读取过,可以丢弃的buffer):</p>\n<p><img src=\"./Arm_to_pru.png\" alt=\"\"></p>\n<p>Master (图中是ARM Host)的步骤:</p>\n<ul>\n<li>Step 1a:重新分配一个buffer</li>\n<li>Step 1b (与 Step 1a 二选一):从 slave的Vring中取一个用过的buffer</li>\n<li>Step 2:将数据转移到 Step 1a 或 Step 1b 拿到的 buffer 中</li>\n<li>Step 3:将新填入数据的 buffer 添加到 slave 的 Vring 的 available 列表中,等待读取</li>\n<li>Step 4:使用 Mailbox 给 slave 发送一个读取的信号</li>\n</ul>\n<p>Slave (图中是 PRU0,不用管它是啥)的步骤:</p>\n<ul>\n<li>Step 5:在 Mailbox 中发现了来自 master 的信号,从处理器被告知有新数据可读了</li>\n<li>Step 6:从 slave 自己的 Vring 中读取数据</li>\n<li>Step 7:将数据转移到自己的 buffer 中</li>\n<li>Step 8:将空 buffer 放回自己的 Vring 已使用的 buffer 列表中</li>\n<li>Step 9:使用另一个 Mailbox,告知主处理器自己处理完数据了</li>\n</ul>\n<p>在这个例子中:</p>\n<ul>\n<li>\n<p>CPU2 初始化 OpenAMP MW ,OpenAMP 通过 硬件抽象层(HAL)初始化了 IPCC peripheral,设置了 openamp-rpmsg 框架。</p>\n</li>\n<li>\n<p>CPU2 创建了 两个 rpmsg 信道给 两个虚拟的 UART 实例 UART0 和 UART1。</p>\n</li>\n<li>\n<p>CPU2 在两条信道上等待来自 CPU1 的消息。</p>\n</li>\n<li>\n<p>当 CPU2 接收到来自一个虚拟 UART 实例 / rpmsg 信道的消息时,它在同一个 UART 实例 / rpmsg 信道上将消息发送回 CPU1。</p>\n</li>\n</ul>\n<p><strong>注意</strong>:</p>\n<ul>\n<li>\n<p>在 Cortex-A7 上需要烧录好 Linux</p>\n</li>\n<li>\n<p>在 Cortex-M4 上 logging 被重定向到了 MCUSRAM 的共享内存中,能够在 Linux 的控制台中进行展示,使用这个命令即可查看 <code>cat /sys/kernel/debug/remoteproc/remoteproc0/trace0</code></p>\n</li>\n<li>\n<p>下面的命令需要在 Cortex-A7 的 Linux 控制台中运行,在执行完这个命令后,在 Linux 控制台中我们会得到 <code>"Hello Virtual UART0"</code> 和 <code>"Hello Virtual UART1"</code>。也就是说,在 CA7 中 Linux 是把消息通道识别成了一个文件描述符 (事实上,所有的文件、外设都是这样的,直接在文件系统中,当做一个普通的文件来处理,应该也是使用 socket 在通信)。</p>\n<pre><code class=\"language-shell\">stty -onlcr -echo -F /dev/ttyRPMSG0 # 设置终端取消该设备的echo,回车换行符转换换行符\ncat /dev/ttyRPMSG0 &\nstty -onlcr -echo -F /dev/ttyRPMSG1\ncat /dev/ttyRPMSG1 &\necho "Hello Virtual UART0" >/dev/ttyRPMSG0\necho "Hello Virtual UART1" >/dev/ttyRPMSG1\n</code></pre>\n</li>\n</ul>\n<pre><code>\n**注意**:\n\n当使用 `HAL_Delay()`的时候,一定要小心,该函数基于 HAL 中时间相关的 Interrupt Service Routine (ISR) 提供的了毫秒级的准确延时。 如果在一个 peripheral ISR 中去调用 `HAL_Delay()`,那么相比 peripheral ISR,HAL 时间的中断将有更高的优先级(在数字表示上更小的优先级),否则调用方的 ISR 将会被阻塞。想要更改时间中断的优先级,我们需要使用 `HAL_NVIC_SetPriority()` 函数。我们还需要保证 HAL 时间单位总是被设置成 1 毫秒以确保我们 HAL 的行为是正确的。\n\n该示例中的传输,Channel 1 和 Channel 2 分别承担一个方向的传输。\n\n```C\n/*\n * Channel direction and usage:\n *\n * ======== <-- new msg ---=============--------<------ =======\n * || || || CHANNEL 1 || || ||\n * || A7 || ------->-------=============--- buf free--> || M4 ||\n * || || || ||\n * ||master|| <-- buf free---=============--------<------ ||slave||\n * || || || CHANNEL 2 || || ||\n * ======== ------->-------=============----new msg --> =======\n */\n</code></pre>\n<p>在第三方库中,virtual_driver 库目录下的 <code>virtual_uart.h/c</code>包含实际开发中用到的接口,是调用的 <code>OpenAMP</code>库,<code>OpenAMP</code> 使用了 <code>libmetal</code> 库的通用接口去管理外设、内存、中断。关于 <code>OpenAMP</code> 官方代码库下的 <code>doc</code> 目录有提供一份用户手册供参考。</p>\n<p>在 <code>openamp_conf.h</code> 中可以通过宏设置是使用 IPCC 还是 HSEM 作为通信机制,也可以通过宏选择是使用 UART 还是 I2C。</p>\n<p>CM4 需要一个进程循环使用 <code>openamp.c</code> 中的 <code>OPENAMP_check_for_message()</code> 来查看是否接收到了数据,如果接收到会调用上面的回调,回调中如果拷贝了接收到的数据,此时就可以在循环中查看接收到的数据,记得清零接收到数据的标记。</p>\n<p>CM4 需要调用 <code>virt_uart.c</code> 中的 <code>VIRT_UART_Transmit()</code> 去向 CA7 传输数据,最大传输数据不可以超过 <code>RPMSG_BUFFER_SIZE-16</code>,该常量定义在 <code>rpmsg_virtio.h</code> 中,是512,也即只能传输 496 字节的数据。所以比较可行的方法是双方互相发送字符串指令去调用对方上的函数?直接传输数据等不太现实。同时,CM4 使用<code>virt_uart.c</code>中的函数 <code>VIRT_UART_RegisterCallback()</code> 向 virtual UART Handler 注册回调函数,在接收到数据后,从 handler 中读取数据大小 <code>handler->RxXferSize</code> 和数据 <code>huart->pRxBuffPtr</code>,设置标记表示读取到数据等。<code>virt_uart.c</code> 中也定义了 <code>VIRT_UART_Init()</code>,用来调用 <code>openamp.c</code>中的<code>OPENAMP_create_endpoint()</code> 创建通信端点。<code>VIRT_UART_read_cb()</code>是更低一层的接口,用来向端点所在的 <code>VIRT_UART_HandleTypeDef</code>中写入数据。注意端点本身是在 Virt UART handler 结构体中的,这里找到端点对应的 handler 的方法和 Linux 中链表的使用十分类似,也是定义了一个宏,根据结构体成员的地址和偏移量计算结构体的地址。</p>\n<p><code>openamp.h</code>和<code>openamp.c</code> 是对 <code>rpmsg.h</code> 和 <code>rpmsg.c</code> 中的 API 进行了封装。</p>\n<p><code>mbox_ipcc.c</code> 中,通过 <code>MAILBOX_INIT()</code> 函数,调用<code>HAL_IPCC_ActivateNotification()</code>在两个 IPCC channel 上分别注册回调函数。在接收到数据后,回调函数中设置标志提示 <code>MAILBOX_Poll()</code>函数可以进行读取了,同时会调用提醒 CPU 在哪个 ipcc handler 上哪个通道接收到了数据。在<code>MAILBOX_Poll()</code> 中会检查两个通道上的标记,如果接收信道检测到数据或发送信道为空,则可以调用 <code>rproc_virtio_notified()</code> 来提醒 virtio_device。在 <code>MAILBOX_Notify()</code> 中则会检测信道是否为空(会等待到信道为空),提醒 CA7。</p>\n<p>CM4 上的常用宏</p>\n<pre><code class=\"language-c\">// 普通标记\ntypedef enum\n{\n RESET = 0,\n SET = !RESET\n} FlagStatus, ITStatus;\n// HAL的标记\ntypedef enum \n{\n HAL_OK = 0x00U,\n HAL_ERROR = 0x01U,\n HAL_BUSY = 0x02U,\n HAL_TIMEOUT = 0x03U\n} HAL_StatusTypeDef;\n// HAL中锁的标记\ntypedef enum \n{\n HAL_UNLOCKED = 0x00U,\n HAL_LOCKED = 0x01U \n} HAL_LockTypeDef;\n</code></pre>\n<p>根据 <code>stm32mp1xx_hal_ipcc.c</code>中的注释,</p>\n<ul>\n<li>\n<p>For a given channel (from 0 to IPCC_CHANNEL_NUMBER), for a given direction IPCC_CHANNEL_DIR_TX or IPCC_CHANNEL_DIR_RX, you can choose to communicate<br>\nin polling mode or in interrupt mode using IPCC.<br>\nBy default, the IPCC HAL driver handle the communication in polling mode.<br>\nBy setting a callback for a channel/direction, this communication use<br>\nthe interrupt mode.</p>\n</li>\n<li>\n<p>Polling mode:</p>\n<ul>\n<li>\n<p>To transmit information, use HAL_IPCC_NotifyCPU() with IPCC_CHANNEL_DIR_TX. To know when the other processor has handled<br>\nthe notification, poll the communication using HAL_IPCC_NotifyCPU with IPCC_CHANNEL_DIR_TX.</p>\n</li>\n<li>\n<p>To receive information, poll the status of the communication with<br>\nHAL_IPCC_GetChannelStatus with IPCC_CHANNEL_DIR_RX. To notify the other<br>\nprocessor that the information has been received, use HAL_IPCC_NotifyCPU<br>\nwith IPCC_CHANNEL_DIR_RX.</p>\n</li>\n</ul>\n</li>\n<li>\n<p>Interrupt mode:</p>\n<ul>\n<li>\n<p>Configure a callback for the channel and the direction using HAL_IPCC_ConfigChannel().<br>\nThis callback will be triggered under interrupt.</p>\n</li>\n<li>\n<p>To transmit information, use HAL_IPCC_NotifyCPU() with<br>\nIPCC_CHANNEL_DIR_TX. The callback configured with HAL_IPCC_ConfigChannel() and<br>\nIPCC_CHANNEL_DIR_TX will be triggered once the communication has been handled by the<br>\nother processor.</p>\n</li>\n<li>\n<p>To receive information, the callback configured with HAL_IPCC_ConfigChannel() and<br>\nIPCC_CHANNEL_DIR_RX will be triggered on reception of a <a href=\"http://communication.To\">communication.To</a> notify the other<br>\nprocessor that the information has been received, use HAL_IPCC_NotifyCPU<br>\nwith IPCC_CHANNEL_DIR_RX.</p>\n</li>\n<li>\n<p>HAL_IPCC_TX_IRQHandler must be added to the IPCC TX IRQHandler</p>\n</li>\n<li>\n<p>HAL_IPCC_RX_IRQHandler must be added to the IPCC RX IRQHandler</p>\n</li>\n</ul>\n</li>\n</ul>\n<p><code>lock_resource.c</code> 中的 <code>Periph_Lock()</code> 函数可以尝试锁定 peripheral,但是如果超过时间限制还没有获取,会返回超时错误。其中设备的 HSM ID 是通过 <code>GET_HSEM_SEM_INDEX</code> 宏定义的,该宏用了很多次三目运算符对每类设备进行判断,如果都不是会返回超出 HSEM 数量的数字,在该开发板上只有32个 Hardware Semaphore,因此该数字是32 (HSEM_SEMID_MAX + 1)。这些常量一般都定义在 <code>stm32mp157cxx_cm4.h</code> 文件中。</p>\n<p><code>virtio.c</code> 中给了一些描述设备名和特性的函数,除此以外,是重要的 <code>virtio_create_virtqueues()</code>,用来根据一系列输入设备指针、队列数量、回调函数等参数创建多个 virtio queue,该函数返回 0 则代表创建成功。因为每次创建 virtio queue 的时候,是在调用 <code>virtqueue.c</code> 中的 <code>virtqueue_create()</code>,如果成功了,则返回 <code>VQUEUE_SUCCESS</code> 值,该常量是0 。</p>\n<p>头文件<code>virtio.h</code>中,定义了一个函数指针表结构体 <code>virtio_dispatch</code>,附属于 <code>virtio_device</code> 结构体,意味着需要为每种支持的设备单独实现一遍这些函数。</p>\n<p><code>virtqueue.c</code> 定义了数据结构及其方法,<code>virtqueue_create()</code>中 virtio 队列 vq 的 <code>notify</code>函数是需要根据设备定义的。</p>\n<h3>相关数据结构:</h3>\n<h4>Libmetal helper data struct</h4>\n<pre><code>struct metal_io_region {\n\tchar name[64]; /**< I/O region name */\n\tvoid *virt; /**< base virtual address */\n\tconst metal_phys_addr_t *physmap; /**< table of base physical address\n\t of each of the pages in the I/O\n\t region */\n\tsize_t size; /**< size of the I/O region */\n\tunsigned long page_shift; /**< page shift of I/O region */\n\tmetal_phys_addr_t page_mask; /**< page mask of I/O region */\n\tunsigned int mem_flags; /**< memory attribute of the\n\t I/O region */\n\tstruct metal_io_ops ops; /**< I/O region operations */\n};\n\n\n/** Libmetal device structure. */\nstruct metal_device {\n\tconst char *name; /**< Device name */\n\tstruct metal_bus *bus; /**< Bus that contains device */\n\tunsigned num_regions; /**< Number of I/O regions in\n\t device */\n\tstruct metal_io_region regions[METAL_MAX_DEVICE_REGIONS]; /**< Array of\n\t I/O regions in device*/\n\tstruct metal_list node; /**< Node on bus' list of devices */\n\tint irq_num; /**< Number of IRQs per device */\n\tvoid *irq_info; /**< IRQ ID */\n};\n</code></pre>\n<h4>Remoteproc data struct</h4>\n<pre><code>struct remoteproc {\n\tstruct metal_device dev; /**< Each remoteproc has a device, each device knows its memories regions */\n\tmetal_mutex_t lock; /**< mutex lock */\n\tvoid *rsc_table; /**< pointer to resource table */\n\tsize_t rsc_len; /**< length of the resoruce table */\n\tstruct remoteproc_ops *ops; /**< pointer to remoteproc operation */\n\tmetal_phys_addr_t bootaddr; /**< boot address */\n\tstruct loader_ops *loader_ops; /**< image loader operation */\n\tunsigned int state; /**< remoteproc state */\n\tstruct metal_list vdevs; /**< list of vdevs (can we limited to one for code size but linux and resource table supports multiple */\n\tvoid *priv; /**< remoteproc private data */\n};\n\nstruct remoteproc_vdev {\n\tstruct metal_list node; /**< node */\n\tstruct remoteproc *rproc; /**< pointer to the remoteproc instance */\n\tstruct virtio_dev; /**< virtio device */\n\tuint32_t notify_id; /**< virtio device notification ID */\n\tvoid *vdev_rsc; /**< pointer to the vdev space in resource table */\n\tstruct metal_io_region *vdev_io; /**< pointer to the vdev space I/O region */ \n\tint vrings_num; /**< number of vrings */\n\tstruct rproc_vrings[1]; /**< vrings array */\n};\n\nstruct remoteproc_vring {\n\tstruct remoteproc_vdev *rpvdev; /**< pointer to the remoteproc vdev */\n\tuint32_t notify_id; /**< vring notify id */\n\tsize_t len; /**< vring length */\n\tuint32_t alignment; /**< vring alignment */\n\tvoid *va; /**< vring start virtual address */\n\tstruct metal_io_region *io; /**< pointer to the vring I/O region */\n};\n</code></pre>\n<h4>Virtio Data struct</h4>\n<pre><code>struct virtio_dev {\n\tint index; /**< unique position on the virtio bus */\n\tstruct virtio_device_id id; /**< the device type identification (used to match it with a driver). */\n\tstruct metal_device *dev; /**< do we need this in virtio device ? */\n\tmetal_spinlock lock; /**< spin lock */\n\tuint64_t features; /**< the features supported by both ends. */\n\tunsigned int role; /**< if it is virtio backend or front end. */\n\tvoid (*rst_cb)(struct virtio_dev *vdev); /**< user registered virtio device callback */\n\tvoid *priv; /**< pointer to virtio_dev private data */\n\tint vrings_num; /**< number of vrings */\n\tstruct virtqueue vqs[1]; /**< array of virtqueues */\n};\n\nstruct virtqueue {\n\tchar vq_name[VIRTQUEUE_MAX_NAME_SZ]; /**< virtqueue name */\n\tstruct virtio_device *vdev; /**< pointer to virtio device */\n\tuint16_t vq_queue_index;\n\tuint16_t vq_nentries;\n\tuint32_t vq_flags;\n\tint vq_alignment;\n\tint vq_ring_size;\n\tboolean vq_inuse;\n\tvoid *vq_ring_mem;\n\tvoid (*callback) (struct virtqueue * vq); /**< virtqueue callback */\n\tvoid (*notify) (struct virtqueue * vq); /**< virtqueue notify remote function */\n\tint vq_max_indirect_size;\n\tint vq_indirect_mem_size;\n\tstruct vring vq_ring;\n\tuint16_t vq_free_cnt;\n\tuint16_t vq_queued_cnt;\n\tstruct metal_io_region *buffers_io; /**< buffers shared memory */\n\n\t/*\n\t * Head of the free chain in the descriptor table. If\n\t * there are no free descriptors, this will be set to\n\t * VQ_RING_DESC_CHAIN_END.\n\t */\n\tuint16_t vq_desc_head_idx;\n\n\t/*\n\t * Last consumed descriptor in the used table,\n\t * trails vq_ring.used->idx.\n\t */\n\tuint16_t vq_used_cons_idx;\n\n\t/*\n\t * Last consumed descriptor in the available table -\n\t * used by the consumer side.\n\t */\n\tuint16_t vq_available_idx;\n\n\tuint8_t padd;\n\t/*\n\t * Used by the host side during callback. Cookie\n\t * holds the address of buffer received from other side.\n\t * Other fields in this structure are not used currently.\n\t * Do we needed??/\n\tstruct vq_desc_extra {\n\t\tvoid *cookie;\n\t\tstruct vring_desc *indirect;\n\t\tuint32_t indirect_paddr;\n\t\tuint16_t ndescs;\n\t} vq_descx[0];\n};\n\nstruct vring {\n\tunsigned int num; /**< number of buffers of the vring */\n\tstruct vring_desc *desc;\n\tstruct vring_avail *avail;\n\tstruct vring_used *used;\n};\n</code></pre>\n<h4>RPMsg Data struct</h4>\n<pre><code>struct rpmsg_virtio_device {\n\tstruct virtio_dev *vdev; /**< pointer to the virtio device */\n\tstruct virtqueue *rvq; /**< pointer to receive virtqueue */\n\tstruct virtqueue *svq; /**< pointer to send virtqueue */\n\tint buffers_number; /**< number of shared buffers */\n\tstruct metal_io_region *shbuf_io; /**< pointer to the shared buffer I/O region */\n\tvoid *shbuf;\n\tint (*new_endpoint_cb)(const char *name, uint32_t addr); /**< name service announcement user designed callback which is used for when there is a name service announcement, there is no local endpoints waiting to bind */\n\tstruct metal_list endpoints; /**< list of endpoints */\n};\n\nstruct rpmsg_endpoint {\n\tchar name[SERVICE_NAME_SIZE];\n\tstruct rpmsg_virtio_dev *rvdev; /**< pointer to the RPMsg virtio device */\n\tuint32_t addr; /**< endpoint local address */\n\tuint32_t dest_addr; /**< endpoint default target address */\n\tint (*cb)(struct rpmsg_endpoint *ept, void *data, struct metal_io_region *io, size_t len, uint32_t addr); /**< endpoint callback */\n\tvoid (*destroy)(struct rpmsg_endpoint *ept); /**< user registerd endpoint destory callback */\n\t/* Whether we need another callback for ack ns announcement? */\n};\n</code></pre>\n<h2>Tiny OS 移植</h2>\n<p>首先在 STM32CubeIDE中用 STM32CubeMX仿照 LED 和外部中断那两个示例,设置好引脚和中断。</p>\n<p>接下来移植 Tiny OS。STM32CubeIDE 是基于 Eclipse 和 arm-gcc 的,添加PATH 和修改配置文件可以凭借类似的经验或文档来。</p>\n<p>事实上可以让 STM32CubeMX 自动生成带 FreeRTOS 的代码,然后参考 <code>RT_CM4/Core/Inc</code> 目录下的 FreeRTOSConfig.h 来设置。</p>\n<p>如果是用 STM32CubeMX 配置 RTOS,会提示使用 HAL timebase source 而不是用 Systick。可以通过前面的 Pinout 部分的 SYS 选项设置,需要启用 TIMx,这个在下小节记录。</p>\n<p>在 <code>board</code> 目录下找到最接近的一个。STM32F103 是 Cortex-M3,STM32F401 是 84 MHZ Cortex-M4,STM32F407 是 168 MHZ Cortex-M4,STM32F745 是 Cortex-M7。所以我们使用 STM32F401 或 STM32F407 下面的 <code>board/xxx/tos_config.h</code>,把 <code>#include "stm32f4xx.h"</code> 修改为 <code>#include "stm32mp1xx.h"</code>(因为板子是 mp157)。</p>\n<p>主要需要的源码就是 <code>arch/arm/arm-v7m/</code> 下的 <code>common</code> 和 <code>cortex-m4/gcc</code> (由于 STM32CubeIDE 自带了 gcc );<code>kernel</code> 目录下的源码;<code>osal/cmsis_os</code> 也需要一份,以便使用标准接口。</p>\n<p>刚刚 tos_config 参考的 <code>board/xxx/</code> 示例里面,有写如何在<code>main.c</code>中启动os,添加应用。他们把板子的初始化放到 <code>mcu_init.c</code> 中了,我们也这么移动,让<code>main.c</code> 简洁一些。</p>\n<p>在 STM32CubeIDE 中设置 <code>include</code> 目录。</p>\n<p>重复定义项需要删掉:</p>\n<ul>\n<li><code>stm32mp1xx_it.c</code> 自动生成的<code>PendSV_Handler()</code></li>\n</ul>\n<p>示例中使用的 cmsis_os 是旧版的接口(新版中没有),所以要用的是 <code>cmsis_os.h</code>。或者在 <code>platform/vendor_bsp/st/CMSIS/RTOS2/Template</code> 下的 <code>cmsis_os.h</code> 中则根据预处理宏选择了头文件的版本,如果是要求用 2.0 往上的接口,会用宏做替换。</p>\n<p>此时可以尝试编译了,成功编译后,接下来用一小段程序验证一下是否成功跑起来了os,创建一个应用入口程序(这里用的是 cmsis_os2格式的接口,也可以用 TencentOS-tiny 创建 task 的风格,腾讯官方都做了支持;分配给每个各个任务的栈大小需要提前考虑好,不然各种奇葩的错误……):</p>\n<pre><code class=\"language-c\">__weak void application_entry(void *arg)\n{\n while (1) {\n // printf("This is a demo task,please use your task entry!\\r\\n");\n HAL_GPIO_TogglePin(GPIOZ, GPIO_PIN_5);\n HAL_Delay(500);\n }\n}\n</code></pre>\n<p>上面这段写入板子后,正常情况应该看到 LED 闪烁。</p>\n<p>本来还想测试按键中断,但是暂时不知道为什么启动 os 后按键中断无法触发。</p>\n<p>如果是打算做AMP相关的实验,还需要把 IPCC 相关的都激活,这里因为是在 MPU 内部,所以不需要设置引脚。Middleware 那里把 OpenAMP 相关激活,FreeRTOS 亮着但是默认是禁用的,不用管。</p>\n<p>后续可能会用到的源码:<code>platform\\xx\\st</code> 下有有关 ST 的 HAL 实现,但是目前还没有针对 stm32mp1xx 的。但是查看 <code>platform/hal/st/stm32f1xx/src/tos_hal_uart.c</code> 和 <code>platform/hal/st/stm32f4xx/src/tos_hal_uart.c</code> 后,发现并没有多大的区别,只是 stm32f1xx 板子上的 <code>HAL_UART_Transmit()</code> 函数会返回状态值,可以判断是否传输成功,那么对 stm32mp1xx,用到的时候自己去看着改一下就行了。</p>\n<p>应该还有一些常量和宏的定义可能需要修改但是目前还没有用到。</p>\n<p>在 <code>/components/</code>目录下选择我们需要的文件系统部分 <code>fs</code> 和 elf 文件读取功能支持 <code>elfloader</code>,添加到 STM32CubeIDE 中</p>\n<h4>HAL timebase source / Systick</h4>\n<p>如果是用 STM32CubeIDE 自动生成带 RTOS 的代码,会有相关提示,应该还是说程序的中断和时钟中断优先级的问题</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210223/",
"title": "STM32MP157A和IPCC",
"summary": "开发板学习",
"date_modified": "2021-02-23T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>为什么同一个文件在不同电脑上大小不一样</h1>\n<p>今天一个朋友问了我一个问题:为啥同一个文件在不同电脑上大小不一样啊?为啥啊?我直接懵了,这啥啊?为啥啊?直到看到了一篇文章<a href=\"#ref1\"><sup>[1]</sup></a>,才解答了我和朋友的困惑。</p>\n<h2>簇的大小导致了文件大小不同</h2>\n<p>事实上这种事情经常发生,比如你在云盘上存小视频,明明它是 60G。结果下载下来以后,发现你的文件夹变成 80G 了,难道人家还赠送了你 20G 的小视频?这当然不可能发生。</p>\n<p>核心原因就是</p>\n<ul>\n<li>磁盘空间是按簇来划分的。即使是一个 Byte 大小的文件也会占用一个簇的空间。</li>\n<li>磁盘格式化时可以设置簇大小,一般从 512 到 131,072 Bytes。</li>\n<li>不同的实用程序以不同的方式显示磁盘空间。</li>\n<li>网盘一般显示的是文件的实际大小。</li>\n</ul>\n<h2>实例分析</h2>\n<p>假设你有一个 1 Byte 的文件,你用命令行查看会是 1 Byte,用文件浏览器看他又成了 1 KB。</p>\n<p><img src=\"./one-byte-file-cmd.png\" alt=\"A one-byte file, listed in the Windows Command Prompt\"></p>\n<p><img src=\"./one-byte-file-explorer-600x133.png\" alt=\"A one-byte file, listed in the Windows File Explorer\"></p>\n<p>数据在磁盘上会存放在大小为 512 或 4096 Bytes 的扇区上。文件存储系统会记录文件在磁盘上的存储方式相关信息,包括是在哪个扇区上。但是文件系统一般不会一个一个扇区地进行记录和跟踪,而是把多个扇区并在一起管理,称为簇。</p>\n<p>簇一般是 1, 2, 4, 8, 16 或更多个相邻扇区。文件系统会记录文件的位置,并维护一个列表,记录分配给文件的簇。</p>\n<p>使用 CHKDSK 命令可以查看簇大小(即下图中的一个 allocation unit)。</p>\n<p><img src=\"./chkdsk-report.png\" alt=\"CHKDSK report showing cluster size\"></p>\n<p>理论上,当我们创建一个 1 Byte 大小的文件,文件系统会:</p>\n<ul>\n<li>在他的文件表中创建一个列表项,俗称为”directory listing”。</li>\n<li>从磁盘上分配一个簇来存储文件。</li>\n<li>将数据写到磁盘中。</li>\n</ul>\n<p>文件将会被被分配一个簇,即使他不需要这么大的空间。假设簇大小是上图的 4KB,则一个 1 Byte 的文件会消耗 4KB 空间,当它变成 4097 Byte 的时候,它会占用两个簇,从而是 8KB 的空间。</p>\n<h3>但是文件管理器显示的是 1KB,而不是 4KB</h3>\n<p>事实上,文件系统不止需要记录文件的数据,也需要记录文件名、分配给它的簇、时间戳、标签、权限等等诸多信息,所有的这些元数据(meta-data)都占用了文件 directory listing 中的空间。</p>\n<p>在 NTFS 文件系统中,directory listing 的空间是一整块进行分配的,不管元数据实际有多大,每次分配都会是 1,024 Bytes。</p>\n<p>所以是这么优化的:当文件很小的时候,不分配簇,而是利用 directory listing 中的剩余空间去存放它。这样,就可以节约簇的分配(这也是为什么你甚至可以看到文件属性中,文件的实际大小比占用空间小,占用空间甚至是0的情况)。</p>\n<h2>补充</h2>\n<p>在fat32文件系统的情况下,分区大小在2GB~8GB时簇的大小为4KB;分区大小在8GB~16GB时簇的大小为8KB;分区大小在 16GB~32GB时,簇的大小则达到了16KB。而ntfs格式中,当分区的大小在2GB以下时,簇的大小都比相应的fat32簇小;当分区的大小在2GB以上时(2GB~2TB),簇的大小都为4KB。也就是说,ntfs可以比fat32更有效地管理磁盘空间,最大限度地避免了磁盘空间的浪费。</p>\n<h2>References</h2>\n<ol>\n<li><a href=\"https://askleo.com/why-is-the-same-file-a-different-size-in-different-places/\">Why is the Same File a Different Size in Different Places?</a> <div id=\"ref1\"/></li>\n</ol>\n",
"url": "https://forsworns.github.io///zh/blogs/20210204/",
"title": "为什么同一个文件在不同电脑上大小不一样",
"summary": "每天一个没用的知识(×)",
"date_modified": "2021-02-04T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>傻瓜笔记:重要文档下载方式(3GPP标准、RFC)</h1>\n<h2>3GPP</h2>\n<p>想下载 3GPP 的标准都研究了半天,这里持续记录一下一些重要文档的下载方式以防自己忘记。</p>\n<h3>3GPP专用缩写解释</h3>\n<p>首先是一些缩写,这个[老哥](<a href=\"https://blog.csdn.net/spring_sh/article/details/106316924\">3GPP TR/TS_spring_sh的博客-CSDN博客</a>)解释了一下:</p>\n<p>TSG 设立项目,<br>\nTSG / WG 进行研究,<br>\n研究项目称为 SI (study item): 输出 TR(technical report);<br>\n标准化项目称为 WI (work item): 由TR输出 TS(technical specification);</p>\n<p>会议及提案, 比如:<br>\n<a href=\"http://ftp.3gpp.org/tsg_ran/TSG_RAN/TSGR_86/\">ftp.3gpp.org/tsg_ran/TSG_RAN/TSGR_86/</a><br>\n代表第86次会议,其下有提案</p>\n<h3>3GPP标准下载方法</h3>\n<p><a href=\"https://www.3gpp.org/specifications/79-specification-numbering\">Specification Numbering (3gpp.org)</a> 有一个大表,是 3GPP 各个系列标号对应的含义,比如38是Radio technology beyond LTE,也就是 4G 之后的技术啦,也就是现在的 5G-NR。</p>\n<p>找到想要的标号后点击他,可以进入该标号对应的系列,比如进入了[38系列](<a href=\"https://www.3gpp.org/DynaReport/38-series.htm\">3GPP specification series: 38series</a>),这里列出了一些技术。</p>\n<p>在这个列表里找一个感兴趣的,比如<a href=\"https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=3219\">Specification # 38.401 (3gpp.org)</a>,进入他的详情页,默认是在 General 选项卡。</p>\n<p>Type就是上面提到的是标准(TS)还是报告(TR);Radio technology中打钩的项目代表和哪一项有关系,比如上面的 38.401 就只打了 5G 的钩。</p>\n<p>可以点击 General 选项卡上的 “Click to see all versions of this specification” 进 FTP 页面下载想要的版本。</p>\n<p>也可以进 Versions 选项卡,点 Version 栏的版本标号,下载文件。</p>\n<h3>收藏、导读</h3>\n<ul>\n<li>\n<p>37系列都是Multiple radio access technology</p>\n</li>\n<li>\n<p>37.213 Physical layer procedures for shared spectrum channel access</p>\n</li>\n<li>\n<p>37.824 Coexistence between NB-IoT and NR,图5.1.1-1是NR的 bandwidth</p>\n</li>\n<li>\n<p>37.834 Study on Wireless Local Area Network (WLAN) ,主要针对UE对 wifi和NR的切换选择</p>\n</li>\n<li>\n<p>37.890 Feasibility Study on 6 GHz for LTE and NR in Licensed and Unlicensed Operations</p>\n<p>Status 给出了三个ITU region在6G HZ左右的分配情况</p>\n</li>\n<li>\n<p>23.729 Study on unlicensed spectrum offloading system enhancements</p>\n</li>\n<li>\n<p>23.501 System architecture for the 5G System (5GS),5.33章节是有关URLLC的</p>\n</li>\n<li>\n<p>22.804 是应用的大杂烩:Communication for Automation in Vertical Domains</p>\n</li>\n<li>\n<p>22.261中讲了5G service requirement<br>\n异构下主要是要 collision 避免、冲突解析<br>\n注意不仅仅是WiFi等其他存在异构,5G本身就有很多不同的标准,不只有NR,以及旧的<br>\n6.9章还提到了中继UE的概念<br>\n6.21指出,ran的sharing也很重要,写在22.101的28.2节<br>\n表格7.1-1则提到了速度需求,表7.6.1则是URLLC的需求<br>\n7.2章和7.6章则重点讲了URLLC<br>\n注意推荐了几个标准TS 22.104是给CPS的,TS 22.186是给V2X的,高铁是TS 22.289<br>\n附录A、C也可以看看</p>\n</li>\n<li>\n<p>36.889是 LAA</p>\n</li>\n<li>\n<p>37.213是频谱共享,很重要</p>\n</li>\n<li>\n<p>22.263中描述了音视频的要求</p>\n</li>\n<li>\n<p>38.133 RRM requirements</p>\n</li>\n<li>\n<p>38.331 RRC 资源分配 Radio Resource Control between UE and NG-RAN</p>\n</li>\n<li>\n<p>38.533 RRM</p>\n</li>\n<li>\n<p>38.300 是NG-RAN的概述</p>\n</li>\n<li>\n<p>38.201说了物理层基本没变</p>\n</li>\n<li>\n<p>38.889是讲非授权频段的</p>\n</li>\n<li>\n<p>23.501是System architecture。有一部分提到了URLLC,提到了要靠重复冗余的传输确保reliability。</p>\n</li>\n<li>\n<p>21.916 是Release 16的概述</p>\n</li>\n<li>\n<p>38.805是60Ghz的共享</p>\n</li>\n<li>\n<p>38.812是NOMA的</p>\n</li>\n<li>\n<p>38.824是URLLC物理层上的增强,也比较重要</p>\n</li>\n<li>\n<p>38.901是NR的channel model</p>\n</li>\n<li>\n<p>38.873是TDD,但是不重要,应该不会引用他</p>\n</li>\n</ul>\n<h3>附:3GPP 组织结构图</h3>\n<p><img src=\"./3gpp.png\" alt=\"3GPP 组织结构图\"></p>\n<h2>RFC</h2>\n<p><a href=\"https://www.ietf.org/standards/rfcs/\">IETF | RFCs</a> 官网里面直接搜索就好了~</p>\n<p><a href=\"https://datatracker.ietf.org/doc/rfc768/\">RFC 768 - User Datagram Protocol (ietf.org)</a> 这个网站很有意思,还可以自动给 bibtex 很方便引用~</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210203/",
"title": "傻瓜笔记:重要文档下载方式(3GPP标准、RFC)",
"summary": "前两天查阅3GPP标准太痛苦了遂记录之",
"date_modified": "2021-02-03T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Rust:如何创建一个的递归的trait?</h1>\n<p>最近遇到了这个情况,在 [StackOverflow](<a href=\"https://stackoverflow.com/questions/65845197/how-to-define-a-recursive-trait-bound-in-rust\">How to define a recursive trait bound in Rust? - Stack Overflow</a>)上提了一个问题,顺便做一下记录和翻译。</p>\n<h2>问题陈述</h2>\n<pre><code class=\"language-rust\">struct LinkNode {\n next: Option<Box<LinkNode>>\n}\n\nimpl LinkNode{\n fn get_next(&self) -> Option<Box<LinkNode>>{\n None\n }\n fn append_next(mut self, next: LinkNode) -> Self{\n self\n }\n}\n</code></pre>\n<p>从官方文档中,我们知道可以通过这样的方式来创建一个递归的链式结构。</p>\n<p>但是如果想要把它抽象成一个trait该怎么搞呢?</p>\n<h3>初步尝试</h3>\n<p>可能直觉上是这样的</p>\n<pre><code class=\"language-rust\">pub trait Linkable {\n fn get_next(&self) -> Option<Box<impl Linkable>>; \n // or\n // fn get_next(&self) -> impl Linkable; \n fn append_next(&mut self, next: impl Linkable) -> Self;\n}\n</code></pre>\n<p>但是很不幸,出于技术原因,<strong>目前</strong><code>impl trait</code>语法糖的使用很受限制,不仅不能在<code>Box<></code>中作为模板参数,也不能在trait中直接作为返回类型。</p>\n<p>那可能想到,既然这个语法糖没法应用,换成下面这样可以吗?</p>\n<pre><code class=\"language-rust\">pub trait Linkable<T:Linkable<T> + Clone> : Clone {\n fn get_next(&self) -> Option<Box<T>>;\n fn append_next(&mut self, next: T) -> Self;\n}\n</code></pre>\n<p>很不幸,这样的写法虽然可以通过编译,但是在构造的时候需要递归传递参数。这不是麻烦不麻烦的问题(模板参数编译器可以为我们推导出来,不需要显示声明),而是压根没法构造出来,因为这样会无限递归下去,不存在尽头。</p>\n<p>这种实现可能存在[官方文档](<a href=\"https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes\">Using Box to Point to Data on the Heap - The Rust Programming Language (rust-lang.org)</a>)中通过<code>enum</code>递归地构造链表的例子,那样的解决方式。也即通过特化给模板定义一个递归的结束方式,这样的思路在早期C++的模板编程中也有用到,不过现在C++有了可变模版参数。但是而且和C++不同,Rust没法同名重载(据说这是为了避免因此产生的错误,但是在这里似乎表达能力被削弱了),我还没有想到实现的方法QAQ</p>\n<h3>Trait Object? 不是最优解!</h3>\n<p>参考我之前的<a href=\"/zh/blogs/20210120/\">一篇博客</a>中记录的关于<code>Clone</code> trait的内容,我知道多用几个trait object是可以像下面这样搞出来的,但是这样显然是有成本问题的,因为要借助dynamic dispatch,Rust中的<code>dyn</code>指针是两倍空间开销,加上类似C++虚函数的调用方法,性能上有代价,我们这里其实也不需要运行期的多态。而且这样写读起来太恶心了……</p>\n<pre><code class=\"language-rust\">pub trait Linkable: LinkClone{\n fn get_next(&self) -> Option<Box<dyn Linkable>>;\n}\n\npub trait LinkAppend {\n fn append_next(&mut self, next: Box<dyn Linkable>) -> Box<dyn Linkable>;\n}\npub trait LinkClone{\n fn clone_box(&self) -> Box<dyn Linkable>;\n}\n\nimpl<T> LinkClone for T\nwhere\n T: 'static + Linkable+ LinkAppend + Clone,\n{\n fn clone_box(&self) -> Box<dyn Linkable> {\n Box::new(self.clone())\n }\n}\n\nimpl Clone for Box<dyn Linkable> {\n fn clone(&self) -> Box<dyn Linkable> {\n self.clone_box()\n }\n}\n</code></pre>\n<h2>可行实现</h2>\n<p>最后推荐两种可行的实现</p>\n<h3>使用Associated Type</h3>\n<p>为<code>struct</code>实现这个<code>trait</code>的时候需要指定关联的类型到底是啥,可能不太灵活,但是大多数场合够用了。</p>\n<p>但是这样还有一个坑就是,我们似乎没有办法写出来<code>Box<dyn Linkable></code></p>\n<pre><code class=\"language-rust\">pub trait Linkable {\n type Next: Linkable;\n \n fn get_next(&self) -> Option<Self::Next>;\n fn append_next(&mut self, next: Self::Next) -> &Self;\n}\n</code></pre>\n<pre><code class=\"language-rust\">impl Linkable for LinkNode {\n type Next = LinkNode;\n \n fn get_next(&self) -> Option<Box<LinkNode>> { \n None\n }\n fn append_next(&mut self, next: LinkNode) -> &Self {\n self\n }\n}\n</code></pre>\n<h3>回避Trait的模板参数</h3>\n<p>这个方法其实和Associated Type很类似,但是它更加灵活,模板参数可以是任意的~</p>\n<p>在我们的链表的例子里,这种实现可以支持<em>爸爸的爸爸是儿子的爷爷</em>这样的关系,但是你用Associated Type,<em>儿子就不认识爸爸的爸爸</em>了。</p>\n<pre><code class=\"language-rust\">pub trait Linkable {\n fn get_next<T:Linkable>(&self) -> Next<T>; \n fn append_next<T:Linkable>(&mut self, next: Next<T>) -> Self;\n}\n\nstruct Next<T: Linkable> {\n node: T,\n}\n</code></pre>\n<h2>一个好用的crate</h2>\n<p>新发现了这个crate:<a href=\"https://crates.io/crates/auto_enums\">auto_enum</a>。</p>\n<p>似乎是通过生成枚举变量返回多种类型的,貌似这个crate只能用在包含显示的分支处,因为需要分析你的代码。你需要确保返回类型是可枚举的。</p>\n<p>生成代码的思路应该是和这个<a href=\"https://stackoverflow.com/questions/57066471/how-do-i-implement-a-trait-for-an-enum-and-its-respective-variants\">问题</a>差不多的,做一层包裹,然后通过模式匹配对不同的被包裹的类型,调用trait上对应的方法。</p>\n<p>所以如果我们的struct类型数是可以枚举的,用这种方法也可行。但是还是比较麻烦的,如果想要新增实现了trait的类型,还需要去改动enum和在他上面trait的实现。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210123/",
"title": "Rust:如何创建一个的递归的trait?",
"summary": "Rust中创建一个递归的Trait的“一百种”实现方式",
"date_modified": "2021-01-23T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<h1>Rust:克隆trait object?</h1>\n<p>最近遇到了这个问题,参考了<a href=\"https://stackoverflow.com/questions/30353462/how-to-clone-a-struct-storing-a-boxed-trait-object\">stack overflow</a>,顺便做一下记录和翻译。</p>\n<h2>Trait Object</h2>\n<p>Trait及Trait Object最基础的内容可以回顾官方文档。</p>\n<p>Trait Object实现的是<strong>Dynamic Dispatch</strong> 。这是一个术语,描述的是编译器在编译期时并不能知道调用哪个方法,只有在运行时才能确定的情况,也叫<strong>late-binding</strong>。</p>\n<p>Trait Object通过在运行时提供具体的值来实现Dynamic Dispatch。Trait Object包含一个指向data指针和一个指向“vtable” 的指针。data 指针提供了trait object 存储的数据的地址,vtable 指针则指向了关联着实现了该trait的不同类型的vtable (“virtual method table”) ,其中存储着可以调用的方法(也就是虚函数表)。Trait objects有时也被称作type erasure,因为编译器并不清楚运行期时的具体类型。</p>\n<p>注意我们这里讨论的是多态,和泛型不同。当然Rust在模板中,可以添加限制:trait bound,但是在运行时依然只能有一种类型。再具体一些,例如</p>\n<pre><code class=\"language-rust\">pub struct GenericObject<T: Trait> {\n contents: Vec<Box<T>>\n}\n</code></pre>\n<p>和</p>\n<pre><code class=\"language-rust\">pub struct TraitObject {\n contents: Vec<Box<dyn Trait>>\n}\n</code></pre>\n<p>是不同的。前者在实例化的时候,<code>contents</code>只能包含一种实现了<code>Trait</code>的类型,而后者的<code>contents</code>中可以包含任意实现了<code>Trait</code>的类型。</p>\n<p>做个类比,trait及trait object类似Java中的interface或者C++的虚类的用法,Rust没有继承语义,所以通过这种<code>impl xx_trait for xx_struct</code> 的方式实现继承和多态;而trait bound则更接近C++中模板的concept概念,是一种模板特化的语法糖。</p>\n<h2>具体问题分析</h2>\n<p>在创建结构体的时候,我们可能想要在其中保存实现了某个trait的object,此时就需要用到trait object。例如下面的例子中,我们创建了一个名为Animal的trait,用来刻画动物应该具有的特征,他们需要能够讲话!于是提供了一个名为<code>speak</code>的接口。而另一个名为AnimalHouse的trait中,去实现一个动物们居住的房子,这个房子,显然是可以住进任何动物的,所以我们用<code>Box<dyn Animal></code>来表示这里需要一个trait object,他需要实现Animal这个trait。</p>\n<pre><code class=\"language-Rust\">trait Animal {\n fn speak(&self);\n}\n\nstruct Dog {\n name: String,\n}\n\nimpl Dog {\n fn new(name: &str) -> Dog {\n return Dog {\n name: name.to_string(),\n };\n }\n}\n\nimpl Animal for Dog {\n fn speak(&self) {\n println!{"{}: ruff, ruff!", self.name};\n }\n}\n\nstruct AnimalHouse {\n animal: Box<dyn Animal>,\n}\n\nfn main() {\n let house = AnimalHouse {\n animal: Box::new(Dog::new("Bobby")),\n };\n house.animal.speak();\n}\n</code></pre>\n<p>首先,克隆一个 <code>Box</code> 其实不具有好的语义,因为它和 C++ 中的 <code>unique_ptr</code> 一般,具有独占的语义。<br>\n如果想要多个指针指向同一个对象,该使用 <code>Rc</code>,具有 <code>shared_ptr</code> 的语义。<br>\n那么这里的克隆显然是想要深拷贝一份。那直接 <code>(*box).clone()</code> 好不好呢?也不好,如下。</p>\n<p>这个时候,如果我们想要复制<code>house</code>变量,如<code>house.clone()</code>就会报错,提示我们没有实现<code>Clone</code>Trait,但是当你给<code>AnimalHouse</code>和<code>Animal</code>都derive了一个,又会导致<code>Animal</code>类型<code>not object-safe [E0038]</code>,这是什么原因呢?事实上这个问题是<code>Clone</code> Trait导致的,我们直接做<code>&house as &Clone</code>也是无法进行类型转换的。</p>\n<p>因为<code>Clone</code>这个Trait本身是要求实现者是实现了<code>Sized</code>的Trait的,即在克隆时候,要保证大小是确定的,能够开辟等量的空间进行复制。但是<code>Clone</code>的方法<code>fn clone(&self) -> Self</code>和<code>fn clone_from(&mut self, source: &Self) </code>中,除了<code>self</code>以外的参数或返回值也含有<code>Self</code>类型。</p>\n<p>回顾上面谈到的,trait object在实现的时候dynamic dispatch的,我们根本不知道这个trait object对应的实际类型,因为它可以是任何一个实现了该trait的类型的值,所以<code>Self</code>在这里的大小不是<code>Self: Sized</code>的,这样的trait是不能成为trait object的。</p>\n<p>最开始给出的<a href=\"https://stackoverflow.com/questions/30353462/how-to-clone-a-struct-storing-a-boxed-trait-object\">stack overflow</a>中的老哥,给出了一个很有趣的解决方案。</p>\n<pre><code class=\"language-rust\">trait Animal: AnimalClone {\n fn speak(&self);\n}\n\n// Splitting AnimalClone into its own trait allows us to provide a blanket\n// implementation for all compatible types, without having to implement the\n// rest of Animal. In this case, we implement it for all types that have\n// 'static lifetime (*i.e.* they don't contain non-'static pointers), and\n// implement both Animal and Clone. Don't ask me how the compiler resolves\n// implementing AnimalClone for Animal when Animal requires AnimalClone; I\n// have *no* idea why this works.\ntrait AnimalClone {\n fn clone_box(&self) -> Box<dyn Animal>;\n}\n\nimpl<T> AnimalClone for T\nwhere\n T: 'static + Animal + Clone,\n{\n fn clone_box(&self) -> Box<dyn Animal> {\n Box::new(self.clone())\n }\n}\n\n// We can now implement Clone manually by forwarding to clone_box.\nimpl Clone for Box<dyn Animal> {\n fn clone(&self) -> Box<dyn Animal> {\n self.clone_box()\n }\n}\n\n#[derive(Clone)]\nstruct Dog {\n name: String,\n}\n\nimpl Dog {\n fn new(name: &str) -> Dog {\n Dog {\n name: name.to_string(),\n }\n }\n}\n\nimpl Animal for Dog {\n fn speak(&self) {\n println!("{}: ruff, ruff!", self.name);\n }\n}\n\n#[derive(Clone)]\nstruct AnimalHouse {\n animal: Box<dyn Animal>,\n}\n\nfn main() {\n let house = AnimalHouse {\n animal: Box::new(Dog::new("Bobby")),\n };\n let house2 = house.clone();\n house2.animal.speak();\n}\n</code></pre>\n<p>也是挺离谱的,通过构造一个辅助的Trait <code>AnimalClone</code>,作为<code>Animal</code>的super trait,绕开object-safe的问题。</p>\n<p>还有另一个解决方法:Rust中的<code>Box</code>智能指针类似于C++中的<code>unique_ptr</code>,唯一指向某个object,所以调用<code>clone()</code>的话我们必然是在克隆它指向的trait object。而类似<code>shared_ptr</code>,Rust也提供了<code>RC</code>智能指针,运行多个指针同时指向同一个object。因此一个可行的解决方法是将<code>Animal</code>类中<code>Box</code>指针换成<code>RC</code>,此时可以完成克隆。但是注意这里只是把指针克隆了一个,即新建了同一个指向trait object的指针,并没有实现对trait object的克隆。治标不治本!</p>\n<p>部分内容也参考自:</p>\n<ul>\n<li>\n<p><a href=\"https://blog.knoldus.com/get-your-hands-wet-with-traits-object-of-rust/\">https://blog.knoldus.com/get-your-hands-wet-with-traits-object-of-rust/</a></p>\n</li>\n<li>\n<p><a href=\"https://www.136.la/jiaocheng/show-7351.html\">https://www.136.la/jiaocheng/show-7351.html</a></p>\n</li>\n</ul>\n<h2>2023/01/05 更新</h2>\n<p>最近写 rocket 的中间件的时候又碰到了这个东西 = = Interesting</p>\n<p><a href=\"https://docs.rs/rocket/0.5.0-rc.2/rocket/route/trait.Cloneable.html\">Cloneable in rocket::route - Rust (docs.rs)</a></p>\n<p>看上去是 rocket 在管理路由的时候,在 <code>rocket::Route</code> 用了 <code>Box<dyn Handler></code> 来存储任意实现了 <code>trait Handler</code> 的类型的句柄,因此在克隆的时候碰到了这个问题,于是采用了上面讨论的这种方法。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20210120/",
"title": "Rust:克隆trait object?",
"summary": "Rust中克隆Trait Object遇到的坑",
"date_modified": "2021-01-20T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>WebRTC on WSL2 Ubuntu2 折腾笔记</h1>\n<p>[[toc]]</p>\n<p>WebRTC的安卓开发环境只能在Linux系统上使用,因此我在Windows下的WSL2中搭建了环境,我的WSL2安装的是Ubuntu20,在搭建过程中遇到了一些坑,记录下来。部分内容参考自<a href=\"https://www.cnblogs.com/hejunlin/p/12526727.html\">博客</a>。</p>\n<h2>Android环境搭建</h2>\n<p>首先我们需要参考<a href=\"https://webrtc.googlesource.com/src/+/refs/heads/master/docs/native-code/android/index.md\">官方文档</a>,发现需要先安装<a href=\"https://webrtc.googlesource.com/src/+/refs/heads/master/docs/native-code/development/prerequisite-sw/index.md\">prerequisite software</a></p>\n<pre><code class=\"language-shell\">git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git\nexport PATH=/path/to/depot_tools:$PATH # 建议写入~/.bashrc\n</code></pre>\n<p>接着运行</p>\n<pre><code class=\"language-shell\">fetch --nohooks webrtc_android # 会下很久……万幸有输出会提示没断开连接\ngclient --nohooks sync\ngclient runhooks\n</code></pre>\n<p>在这一步WSL2需要配置代理,同时后面用到<code>download_google_storage</code>也可能有代理问题,所以同时最好给gclient单独配置<code>your_webrtc_directory/http_proxy.boto</code>文件设置代理规则,建议将下述命令写入<code>~/.bashrc</code></p>\n<pre><code class=\"language-shell\">export hostip=$(cat /etc/resolv.conf |grep -oP '(?<=nameserver\\ ).*')\necho -e "[Boto]\\nproxy = ${hostip}\\nproxy_port = 8888" > your_webrtc_directory/http_proxy.boto\nalias setss='export https_proxy="http://${hostip}:8888";export http_proxy="http://${hostip}:8888";export all_proxy="http://${hostip}:8888";'\n</code></pre>\n<p>之后在windows中打开SSR/V2Ray/Clash等代理工具,设置允许本地代理,选择允许来自局域网的连接,将端口设置到8888,运行<code>source ~/.bashrc</code>和<code>setss</code>,设置WSL2下的代理规则。</p>\n<p>在运行<code>gclient runhook</code>时,Ubuntu20中因为没有安装python2.7会报相关错误,<code>sudo apt install python</code>后解决。</p>\n<p>重新运行<code>gclient runhook</code>,接着会产生无法下载debian_sid_i386-sysroot的问题,这是DNS有问题,直接在浏览器打开<a href=\"https://commondatastorage.googleapis.com/chrome-linux-sysroot/toolchain/d967bcef40477dbc39acef141ff22bf73f3e7cdb/debian_sid_i386_sysroot.tar.xz\">下载链接</a>也下载不到。修改Windows下无线网卡的DNS为谷歌的8.8.8.8/8.8.4.4后,可以在浏览器中下载到,移动到了<code>your_webrtc_directory/src/build/linux/debian_sid_i386-sysroot</code>中,修改<code>your_webrtc_directory/src/build/linux/sysroot_scripts/install-sysroot.py</code>为</p>\n<pre><code class=\"language-python\">tarball = os.path.join(sysroot, tarball_filename)\nif not os.path.exists(tarball): # 检查是否已经有了 \n if os.path.isdir(sysroot):\n shutil.rmtree(sysroot)\n\t\t……\n response = urlopen(url) # 或者在这里设置代理为hostip:8888也行\n with open(tarball, "wb") as f:\n \t\t……\n</code></pre>\n<p>重新运行<code>gclient runhook</code>,同样方法处理之后的amd64 sysroot下载不到的问题。之后可能会有clang-llvm的安装问题,通过在Windows下的代理中设置DNS为谷歌的8.8.8.8后解决。</p>\n<p>之后的就都正常下载下来了,如果出问题,重新跑一下<code>setss</code>。</p>\n<p>之后开始安装编译过程中必要的工具</p>\n<pre><code class=\"language-shell\"># 在your_webrtc_directory下\ncd src \nbuild/install-build-deps-android.sh \ngn gen out/Debug --args='target_os="android" target_cpu="arm"'\nautoninja -C out/Debug # 会花费很长时间\nautoninja -C out/Debug AppRTCMobile # 只编译AppRTCMobile\n</code></pre>\n<p>至此,编译完成了!</p>\n<p>切换到Release m85,之后固定在这个版本</p>\n<pre><code class=\"language-shell\">git checkout -b m85 refs/remotes/branch-heads/4183\n</code></pre>\n<h1>Linux环境搭建</h1>\n<p>项目结构和上面类似,但是有一些example没有,也不知道别的有没有区别……就直接重新搭建了一份</p>\n<p>过程类似,因为depot_tools安过了,所以第一步可以跳过了</p>\n<pre><code class=\"language-shell\">fetch --nohooks webrtc_android\ngclient --nohooks sync\ngclient runhooks\n</code></pre>\n<p>同样会遇到root image下不到的情况,类似上面可以处理掉</p>\n<p>之后使用GN生产Ninja编译配置文件</p>\n<pre><code class=\"language-shell\">gn gen out/Default\n# gn gen out/Default --args='is_debug=false' # release version\n# gn clean out/Default # clean builds\nninja -C out/Default # compile\n</code></pre>\n<p>切换到Release m85,之后固定在这个版本</p>\n<pre><code class=\"language-shell\">git checkout -b m85 refs/remotes/branch-heads/4183\n</code></pre>\n<p>在<code>src</code>目录下查看一下文件大小</p>\n<pre><code class=\"language-shell\">du --max-depth=1 -h\n\n27M ./data\n17G ./third_party\n1.9M ./p2p\n968K ./rtc_tools\n9.9M ./sdk\n1.3M ./call\n4.3M ./rtc_base\n104K ./stats\n4.3M ./pc\n4.3G ./out\n502M ./examples\n325M ./.git\n164K ./docs\n2.8M ./video\n696K ./audio\n948K ./logging\n83M ./base\n188K ./system_wrappers\n92M ./buildtools\n49M ./testing\n1.9M ./media\n1.5M ./tools_webrtc\n6.7M ./test\n640M ./build\n20K ./build_overrides\n1.4G ./resources\n1.3M ./common_audio\n12K ./style-guide\n368K ./common_video\n1.1G ./tools\n2.8M ./api\n20M ./modules\n25G .\n</code></pre>\n<p>过滤掉大的、没必要改动的文件夹</p>\n<h1>Android Studio配置</h1>\n<p>官网上的方法已经标出了无法使用,推荐直接将<code>src/examples/androidapp/</code>下的代码拷贝出来。</p>\n<p>用Android Studio创建一个项目,创建时<code>minSdkVersion</code>设置为21而不是默认的16,因为webrtc包不支持更低的版本。package name建议设置成了<code>org.appspot.apprtc</code>,在Android Studio项目目录结构中,把<code>src/examples/androidapp/</code>下的文件放到对应位置。注意 <code>src/examples/androidapp/third_party/autobanh/lib/autobanh.jar</code>文件需要拷贝到 <code>src/libs</code> 目录下,<code>third_party</code>中的其他文件可以删掉了。其他的比如<code>build.gradle</code>在<code>app</code> Module下,<code>res</code>文件夹是在<code>src/main</code>下,<code>org</code>放到<code>src/main/java</code>下。</p>\n<p>这时需要用Android Studio的Refactor选项中的Migrate to AndroidX,升级陈旧的依赖。但是这里有个坑是Nullable注解依赖不会自动更新,所以需要将java源代码中所有的<code>import android.support.annotation.Nullable;</code>替换为<code>import androidx.annotation.Nullable;</code>。然后sync一下gradle,就可以build了。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20201023/",
"title": "WebRTC折腾笔记",
"summary": "WebRTC on WSL2 Ubuntu2 折腾笔记",
"date_modified": "2020-10-23T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>想要一只看板娘</h1>\n<p>[[toc]]</p>\n<p>这个博客是用Vuepress搭建的,每次看到别人很好康的博客,就自惭形秽。</p>\n<p>最近在读别人的博客的时候,发现人家也是用的Vuepress,但是里面有看板娘,心动了,我也来试一试。</p>\n<p>调查了一下,这方面集成度比较高的有Vuepress插件<a href=\"https://github.com/JoeyBling/vuepress-plugin-helper-live2d\">vuepress-plugin-helper-live2d</a>。但是该插件仅提供了一个Live2D的模型展示;后面又找了一下,发现之前看到的是<a href=\"https://github.com/stevenjoezhang/live2d-widget\">Live2D Widget</a>这个项目,作者提供了后端可以支持多种模型切换、换装。</p>\n<p>Live2D Widget的默认使用方法很简单,在head里加载上就行了。那么对Vuepress来说,只需要在<a href=\"https://github.com/Forsworns/blog/tree/master/blog/.vuepress\"><code>blog/.vuepress/config.js</code></a>中添加</p>\n<pre><code class=\"language-javascript\">module.exports = {\n\thead:{\n\t\t['link', { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css" }],\n ['script', { src: "https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/autoload.js" }],\n } \n}\n</code></pre>\n<p>现在左下角有了Live2D模型了,虽然没有什么用:see_no_evil:,但是很好看。</p>\n<p>其实该博客在搭建的时候还是踩了一些坑的,但是因为还在搭建博客……之前就没有写总结,现在又快忘光了。从仅有的<a href=\"https://github.com/Forsworns/blog\">README</a>和代码中,我之前是给每个页面单独定义过<a href=\"https://github.com/Forsworns/blog/tree/master/blog/.vuepress\">Layout组件</a>的(见<code>blog/.vuepress/components/*Layout.vue</code>)。之后有空去考虑只在BlogLayout.vue中显示看板娘吧,应该可以参考另一篇<a href=\"https://blog.csdn.net/qq_36357242/article/details/100063063\">博文</a>。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20200818/",
"title": "想要一只看板娘",
"summary": "基于Vuepress搭建的博客的美化",
"date_modified": "2020-08-18T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>学习rCore</h1>\n<p>[[toc]]</p>\n<p>之前花了好长时间去读<a href=\"https://doc.rust-lang.org/book/#the-rust-programming-language\">The Rust Programming Language</a>,写了写书里的实例,也在LeetCode上用Rust<a href=\"https://github.com/Forsworns/OJ_Diary\">写了一些题</a>,但是一直没有动手做一个项目。</p>\n<p>最近发现同学在github上点赞了rCore项目,再次感叹人与人的差距,这就是强者的os课设么。网抑云,启动!我os课设计当时写的是调度器,现在想起来能跑起来都是一件神奇的事情。当时也没有记笔记的习惯,现在基本忘光了,这次就边学习边记录一下,内容基本都来源于<a href=\"https://rcore-os.github.io/rCore_tutorial_doc/\">rCore的教程</a>。也算激励自己学习(狒狒14太好玩了不想写代码了orz)。</p>\n<h2>第一章 独立可执行程序</h2>\n<ul>\n<li>\n<p>使用nightly的Rust需要指定版本确保<a href=\"https://stackoverflow.com/questions/2171177/what-is-an-application-binary-interface-abi/2456882\">ABI</a>稳定性,需要在工作目录下建一个名为 <code>rust-toolchain</code> 的文件,写入如<code>nightly-2020-01-27</code>的工具链版本。</p>\n</li>\n<li>\n<p><code>rustup show</code>或<code>rustc --version</code>查看Rust版本。</p>\n</li>\n<li>\n<p><code>cargo new --bin</code>和<code>cargo new --lib</code>分别创建binary和library项目。</p>\n</li>\n<li>\n<p>使用<code>#![no_std]</code>禁用标准库,这类宏只能置于文件头部;之后需要实现错误处理,使用宏<code>#[panic_handler]</code>并实现<code>panic</code>函数。</p>\n</li>\n<li>\n<p>在<code>Cargo.toml</code>中禁用exception handling</p>\n<pre><code class=\"language-toml\">[profile.dev] # cargo build\npanic = "abort"\n\n[profile.release] # cargo build --release\npanic = "abort"\n</code></pre>\n</li>\n<li>\n<p>移除runtime system(链接到标准库的rust程序会先跳转到 C runtime library 中的 <strong>crt0(C runtime zero)</strong> 进入 C runtime 设置 C 程序运行所需要的环境(比如:创建堆栈,设置寄存器参数等,之后跳转到 Rust runtime 的 <strong>入口点(entry point)</strong> 进入 Rust runtime 继续设置)。使用<code>#![no_main]</code>移除后并去除main函数后,显式地添加C runtime的入口,C语言函数<code>_start()</code>,并使用宏<code>#[no_mangle]</code>防止编译器改变函数名。</p>\n</li>\n<li>\n<p>用rustc编译时,<code>cargo rustc -- -C link-arg=-nostartfiles</code>可以防止链接到C runtime。注意前一个<code>--</code>是cargo的参数,后面的是编译器rustc的参数。</p>\n</li>\n</ul>\n<h2>第二章 最小化内核</h2>\n<ul>\n<li>\n<p>Rust的编译需要指定目标元组:(cpu 架构、供应商、操作系统和 ABI),如<code>x86_64-unknown-linux-gnu</code>。使用<code>rustc --version --verbose</code>可以查看当前默认的目标平台,使用<code>rustc -Z unstable-options --print target-spec-json --target x86_64-unknown-linux-gnu</code>。</p>\n</li>\n<li>\n<p>rCore使用了riscv,因此需要用<code>cargo build --target riscv64imac-unknown-none-elf</code>命令或直接在<code>.cargo/config</code>中写入</p>\n<pre><code class=\"language-toml\">[build]\ntarget = "riscv64imac-unknown-none-elf"\n</code></pre>\n<p>来为项目设置目标三元组。</p>\n</li>\n<li>\n<p>使用<code>cargo install cargo-binutils</code>和<code>rustup component add llvm-tools-preview</code>安装binutils命令行工具,以使用objdump、objcopy等工具。</p>\n<p>具体得,使用file工具查看文件类型等信息;使用 <code>rust-objdump target/riscv64imac-unknown-none-elf/debug/xxx -x --arch-name=riscv64</code>查看文件元信息,使用<code>-d</code>则可进行反汇编;使用<code>rust-objcopy target/riscv64imac-unknown-none-elf/debug/xxx --strip-all -O binary target/riscv64imac-unknown-none-elf/debug/kernel.bin</code>丢弃所有符号表及调试信息,生成二进制内核镜像文件。</p>\n</li>\n<li></li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20200817/",
"title": "学习rCore",
"summary": "参考rCore教程用Rust写一个简单的内核",
"date_modified": "2020-08-17T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>Yet Another JSON Lib</h1>\n<p>[[toc]]</p>\n<p>一直找不到一个简单的Modern C++项目练手,在github上找了很久都太复杂了。最近在知乎上读到了一个写JSON库解析的<a href=\"https://zhuanlan.zhihu.com/json-tutorial\">系列教程</a>(后续简写为教程),作者是写给C初学者的。</p>\n<p>想了想我太菜了,果然还是这种事无巨细的教程适合我QAQ,于是就动手了:<a href=\"https://github.com/Forsworns/yJson\">项目地址</a>。</p>\n<p>既然主要目的是练习新标准,所以难免有拿着锤子看到哪都是钉子的感觉。可惜这个项目里没有多少用到模板的地方,好多deep♂dark♂magic都还没有试过。设计上和教程相同,也没有过多使用OOP。</p>\n<p>那么在原教程的基础上的主要改动如下:</p>\n<h3>命名空间、作用域</h3>\n<p>很自然的使用了命名空间防止冲突,干掉了原来的前缀。</p>\n<p><code>using</code>的新语义用来替代<code>typedef</code>也很舒服。</p>\n<p>c++17可以在<code>if/switch</code>中定义并初始化变量,在<code>parseArray</code>和<code>parseObject</code>中。</p>\n<h3>强枚举类型</h3>\n<p>主要就是把原来的枚举都改成了强枚举类型。教程中用到的</p>\n<p>不过现在用起来可能会更冗杂……毕竟需要加类名。</p>\n<h3>使用了的智能指针</h3>\n<p>C++内存、锁、套接字等资源管理的思路是RAII(Resource Acquisition Is Initialization),资源在初始化时获取、在离开作用域后销毁。感谢智能指针,干掉了教程里的<code>free</code>,在教育意义上似乎是种退步:laughing:。但是好处是不用担心内存泄漏问题了。</p>\n<p>在<code>parseObject</code>函数中,没有直接创建指向<code>Entry el</code>的<code>val</code>的指针,而是选择和解析<code>el->key</code>一样,使用临时变量<code>elKey</code>和<code>elVal</code>传递。有两种常见的错误方式:如果是用<code>auto elVal = make_shared<Value>(el->val)</code>会将<code>el->val</code>复制一份,调用<code>parseValue(s, elVal)</code>不会修改<code>el->Val</code>;如果是用<code>shared_ptr<Value> elVal(&el->val) </code>,则会导致<code>shared_ptr</code>在执行析构的时候,重复析构<code>el->val</code>。</p>\n<h3>去掉了手写的数据结构</h3>\n<p>干掉了手写的复杂数据结构,改用了标准库,比起教程似乎也是一种退步:laughing:。比较值得一提的是,在解析对象的时候,本来设想的是使用<code>unordered_map</code>。但是由于数据结构间互相引用,需要前向声明一个不完整的类。这个时候就出现了一个坑,C++标准库中的容器不支持使用不完整类型,似乎boost做了支持(未考证),而标准库则直到C++17才允许部分容器使用不完整类型。也就是说,目前只有<code>vector</code>,<code>list</code>和<code>forward_list</code>支持使用不完整类型,如下</p>\n<pre><code class=\"language-cpp\">class A; // forward declaration\nusing myVec = vector<A>; // right\nvector<A> a; // right\n// using myMap = map<int,A>; wrong\n// map<int,A> b; // wrong\nclass A{};\n</code></pre>\n<h3>未做OOP实现 建议<a href=\"https://github.com/zsmj2017/MiniJson\">参考</a></h3>\n",
"url": "https://forsworns.github.io///zh/blogs/20200816/",
"title": "Yet Another JSON Lib",
"summary": "阅读了知乎上的教程用Modern C++重写~",
"date_modified": "2020-08-16T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>NS3 简记</h1>\n<p>[[toc]]</p>\n<h2>运行脚本</h2>\n<p>运行c++脚本 <code>./waf --run=app_name --command-template="%s --arg_name=arg_value" </code>。要调用gdb可以在command-template里面加,如<code>--command-template="gdb %s --arg_name=arg_value" </code>。</p>\n<p>如果是python脚本,是要用 <code>./waf --pyrun=app_path --command-template="%s --arg_name=arg_value" </code>。注意这里要用路径,不像运行c++脚本可以直接写<code>scratch</code>目录下的脚本名字。</p>\n<p>跑python脚本的时候在<code>scratch</code>目录生成<code>__pycache__</code>可能会导致任务执行失败,要及时清理掉cache。也可以改ns3根目录下的<code>wscript</code>中的<code>def add_scratch_programs(bld)</code>函数自动跳过该目录。</p>\n<h2>本地编译文档</h2>\n<p>在线文档有些慢,不如在本地编译一个。需要安装doxygen,同时重新配置一下waf</p>\n<pre><code class=\"language-shell\">sudo apt-get install doxygen\n./waf configure --enable-examples --enable-tests\n./waf --doxygen\n</code></pre>\n<p>如果出现错误提示某些脚本里存在<code>'\\r'</code>不能识别,可能是因为Linux下换行符和Windows下不同,用VScode这些编辑器转换CRLF到LF就行了。</p>\n<h2>Trace</h2>\n<p>ns3的trace系统定义了一系列的source去追踪不同的变量,在变量发生变化时可以触发sink记录这种变化。在运行模拟实验时,通过用户定义的回调函数来做为trace souce的trace sink。这些source回调函数一般在类内定义为私有变量,命名规则为<code>m_aTraceFunction</code>。在定义的时候,这些回调函数的类型为<code>TracedCallback<T1, T2, ...> </code>。</p>\n<p>需要指出,ns3的回调函数的返回类型默认都是<code>void</code>,模板中的<code>T</code>是回调函数的参数变量类型。那么我们在写sink的时候,需要定义成<code>void aTraceSink(std::string context, T1 xx, T2 xx, ...)</code>。这里的<code>std::string context</code>是表明我们自定义sink与哪个source相连接的,即此刻的变化是从哪个节点发出的。</p>\n<p>绑定sink回调时候使用<code>Config::Connet(a context, MakeCallback(&aTraceSink))</code>。</p>\n<p>自定义sink的时候,<code>std::string context</code>参数也可以省略掉,写成<code>void anotherTraceSink(T1 xx, T2 xx, ...)</code>,在绑定到监听的对象上时使用的是<code>Config::ConnectWithoutContext</code>。</p>\n<p>绑定sink也可以用<code>obj.TraceConnect</code>和<code>obj.TraceConnectWithoutContext</code>绑定到一个具体的对象上,用法与<code>Config::Connect</code>类似,因为后者本来就是调用了前者实现的,详见官方tutorial。</p>\n<h2>Context</h2>\n<p>Context其实就是节点、应用、函数的名字,比如<code>/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/Mac/$ns3::AdhocWifiMac/Txop/CwTrace</code>写的是任意节点上的任意网卡上的任意无线网卡上的Mac层的传输时的congestion window的Trace?然后在ns3文档的api列表中找到<code>CwTrace</code>的定义,写一个回调用<code>Config::Connect</code>到这个context就可以监听了。用下面的函数可以便捷地从Context中提取NodeId。</p>\n<pre><code class=\"language-cpp\">uint32_t ContextToNodeId(std::string context) {\n std::string sub = context.substr(10); // skip "/NodeList/"\n uint32_t pos = sub.find("/Device");\n NS_LOG_DEBUG("Found NodeId " << atoi(sub.substr(0, pos).c_str()));\n return atoi(sub.substr(0, pos).c_str());\n}\n</code></pre>\n<h2>在已有的模块里新增文件</h2>\n<p>记得改模块目录下的wscript,把新增的文件编译进<code>build/ns3</code>里,否则去<code>scrach</code>下写测试还是找不到新增的文件。</p>\n<p>有时候会破坏python binding的文件依赖,不会改模块binding目录下的设置,把python binding关了不要生成python的对应包了= =<code>.\\build -- --diable-python</code> 或者<code>.\\nsxxx\\waf configure --disable-python</code></p>\n<p>如果是新增模块,可以用waf自动生成,详见官方文档。</p>\n<h2>Python binding</h2>\n<p>如果不需要python binding,只用C++,官方建议就是直接用<code>./build.py -- --disable-python </code>或<code>./waf --disable-python</code>,这样build快而且不会出现和python有关的问题。</p>\n<p>如果想用python binding,但激活了Anaconda中的环境,在build时python binding会无法enable。</p>\n<p>使用<code>.\\waf configure</code>之后,发现具体问题是"Testing pyembed configuration : Could not build a python embedded interpreter"错误。</p>\n<p>似乎很多使用waf构建的项目中都会出现这个问题,我找不到合适的解决方法。尝试把anaconda环境deactivated掉就好了。这里deactivated后,最好改一下<code>build.py</code> 和<code>waf</code>中的解释器,默认是<code>#! /usr/bin/env python</code>在一些机器上可能会去调用<code>pyhon2</code>。改成<code>#! /usr/bin/env python3</code>。</p>\n<p>然后在用官方提供的<code>build.py</code>脚本或者用<code>waf</code>构建后,再激活Anaconda中的某个环境,<code>waf</code>会自动link一遍build过的python binding,在Anaconda的某个环境里就可用了~</p>\n<p>可惜一切努力全部木大了。python binding不支持很多底层的api,而且<strong>不支持使用回调的tracing</strong>,只可以使用pcap和ascii文件。同时后面会提到的一个repo,我这边又build不了,这也导致我只能去考虑在ns3中混编python,遇到了很多新坑。</p>\n<h2>ns3混编(embedding) Python</h2>\n<p>直接用python binding是不可能了,虽然有C++的tensorflow,但是看了一下配置又很麻烦。就想用cpp调用python,查了一下写了一些测试似乎很方便嘛。想着这样算法的实现上可以用python灵活简单一点,也有大段现成的算法实现。那就看看怎么embedding python into c++。:yum:</p>\n<h3>一般情况下的c++/python混编</h3>\n<p>一般的情况下,在C++中混编python只需要加上python的头文件<code>#include 'Python.h'</code> (这里要注意可能需要用绝对路径,看你python怎么装的),然后为g++添加如下参数进行编译就行了</p>\n<pre><code class=\"language-shell\">g++ callpy.cpp `python3-config --cflags` `python3-config --ldflags` -fPIC # 添加的参数会自动展开为头文件和链接库参数\n</code></pre>\n<p>使用<code>Py_Initialize ();</code>和<code>Py_Finalize ();</code>可以初始化和关闭Python解释器。在C++中,python的变量都被创建为一个类型为<code>PyObject</code>的指针。</p>\n<p>模块导入方面,若是使用 <code>PyRun_SimpleString ("import os");</code>导入模块,则模块在C++代码中可见,可以使用<code>PyRun_SimpleString (print(os.getcwd());</code>若使用的是 <code>pName = PyUnicode_DecodeFSDefault ("os"); pModule = PyImport_ImportModule (pName);</code>导入模块,则无法通过``PyRun_SimpleString<code>去使用</code>os`。</p>\n<p>使用<code>PyModule_GetDict(pModule)</code>从模块中获取一个字典结构。</p>\n<p>使用<code>pFunc = PyDict_GetItemString(pDict,"disp")</code>去从字典中获取名为<code>disp</code>的函数,使用<code>PyCallable_Check(pFunc)</code>检查获取的指针是否指向一个可以调用的函数。</p>\n<p><code>pArgs = PyTuple_New(0)</code>创建一个空的元组<code>pArgs</code>去作为函数参数,它可以通过<code> PyTuple_SetItem(pArgs, 0, Py_BuildValue(""));</code> 初始化,之后使用<code>PyObject_CallObject (pFunc, pArgs);</code>调用函数。</p>\n<p>使用函数<code>PyObject* Py_BuildValue(char *format, ...)</code>可以把C++的变量转换成一个Python对象。当需要从C++传递变量到Python时,就会使用这个函数。<code>format</code>参数中常用的格式有</p>\n<ul>\n<li>i 表示int</li>\n<li>I 表示unsigned int</li>\n<li>f 表示float</li>\n<li>O 表示一个Python对象</li>\n<li>更多见Python<a href=\"https://docs.python.org/3/c-api/arg.html\">文档</a></li>\n</ul>\n<p>例如<code>PyTuple_SetItem(pArgs, 0, Py_BuildValue("i",3));</code>,把第一个参数设置成了整型变量3。如果是直接用去构造函数的参数,往往需要写成元组形式如<code>pArgs=Py_BuildValue("(ii)",3,3)</code>,在只有一个参数的时候尤其需要注意如<code>pArgs=Py_BuildValue("(i)",3)</code>。想要取出python函数的返回值要用<code>PyArg_Parse</code>或<code>PyArg_ParseTuple</code>,使用引用传递按上面的<code>format</code>字符串赋值给C++变量,如<code>PyArg_Parse (retObj, "d", &ret);</code>。这些api在使用的时候一定要注意<code>format</code>字符串中的数据格式!如果把数据格式写错了,bug会很难找。</p>\n<p>使用<code>instanceObj = PyObject_CallMethod(pModule,"clsName",NULL);</code>可以创建一个<code>clsName</code>类型的对象。对象的<code>disp</code>方法可以用<code> PyObject_CallMethod(instanceObj,"disp",NULL)</code>调用,如果附加参数的话需要直接用前面<code>format</code>参数的模板语法。(官方文档推荐使用<code>PyInstanceMethod_New</code>去创建实例,但是实际使用时似乎创建失败也不会返回NULL,导致很难debug)。</p>\n<p>在python代码中使用面向对象的思想,是为了维护一些在C++中调用方法去更改的变量。后来测试发现在C++中使用<code>PyImport_ImportModule()</code>加载模块后,模块中的全局变量会自动加载,因此在调用函数的时候,也可以通过global关键字维护全局变量,避免使用对象的概念,在C++里调用的时候会简单一些。</p>\n<p>如果在C++中导入py脚本出现错误,试着先独立运行python脚本,确保它是正确的。出现问题时尝试print输出调试,python中的错误没法报出来,只能<code>print()</code>输出调试。</p>\n<p>更多具体的混编写法,可以参考<a href=\"https://docs.python.org/3/c-api/\">python官方文档</a>中的介绍。</p>\n<h3>可行方案:修改wscript</h3>\n<p>但是写了一段时间真把cpp和python往一起整合的时候,才发现,“不对啊,ns3是用waf去管理编译过程的”。:imp:天真地以为把<code>build</code>目录下的<code>ns3</code>头文件和<code>lib</code>里的动态链接库的路径都加到g++后就行了。但是果然失败了……</p>\n<p>于是求助万能的google,可惜网上似乎没有这么干的人= =。没办法,自己去读了一下<a href=\"https://waf.io/apidocs/tools/python.html\">waf的文档</a>吧,刚好发现了这段</p>\n<pre><code class=\"language-python\"># Support for Python, detect the headers and libraries and provide use variables to link C/C++ programs against them:\ndef options(opt):\n\topt.load('compiler_c python')\ndef configure(conf):\n conf.load('compiler_c python')\n conf.check_python_version((2,4,2))\n conf.check_python_headers()\ndef build(bld):\n bld.program(features='pyembed', source='a.c', target='myprog')\n bld.shlib(features='pyext', source='b.c', target='mylib')\n</code></pre>\n<p>假设环境都合适,那这段的意思就是,要在waf中使用c++和python混编,只需要在<code>build</code>函数里面调用 <code>bld.program(features='pyembed', source='a.c', target='myprog')</code>。再浓缩一下,就是要把生成程序的函数的<code>feature</code>参数设置成<code>'pyembed'</code>。这下我们知道了混编python改waf配置就可以了。</p>\n<p>经过尝试,对于ns3的具体做法是打开ns3主目录下的<code>wscript</code>,搜索一下<code>options</code>函数在哪里,然后做如下改动。</p>\n<pre><code class=\"language-python\"># 修改原来的option函数,加载python解释器\ndef options(opt):\n # options provided by the modules\n opt.load('compiler_c')\n opt.load('compiler_cxx')\n opt.load('cflags')\n opt.load('gnu_dirs')\n opt.load('python')\n # other commands\n</code></pre>\n<p><code>configure</code>函数似乎是可改可不改的,毕竟是自己的机子,默认没问题,不检查环境也行~</p>\n<p>重要的是创建程序时的<code>features</code>参数。因为依赖复杂,ns3的wscript写法也比较复杂,和上面简单的waf示例脚本不同,ns3的wscript中为每个程序创建了一个对象,分别设置各种选项,然后为每个程序添加依赖项。这里我们需要找到<code>create_ns3_program</code>函数的定义,然后做如下修改</p>\n<pre><code class=\"language-python\"># 在features参数后面添加一个pyembed就行了\ndef create_ns3_program(bld, name, dependencies=('core',)):\n program = bld(features='cxx cxxprogram pyembed') # waf可以通过空格分隔选项\n program.is_ns3_program = True\n program.name = name\n</code></pre>\n<p>这样做其实挺粗暴的,其他一些没有用python的cpp代码也会被添加这项feature,可能更好的做法是单独在<code>scratch</code>下写wscript,但是我不会:broken_heart:。</p>\n<p>另外因为会有<code>__pycache__</code>生成,最好再在上面的wscript中添加如下代码,在编译时跳过cache,否则要每次清理掉cache再编译。</p>\n<pre><code class=\"language-python\">def add_scratch_programs(bld):\n ...\n try:\n ...\n if os.path.isdir(os.path.join("scratch", filename)):\n if filename == "__pycache__":\n continue\n ...\n</code></pre>\n<p>还有需要注意的一点是,如果在cpp中导入了自己的python包,要注意下包的路径,否则会找不到。因为ns3的脚本运行时候的路径是在根目录。可以把包copy到ns3的根目录,或者在用<code>PyRun_SimpleString ("sys.path.append('./where you place package')");</code>加个路径。</p>\n<p>如果是用 anaconda 这种,还需要向路径中添加当前环境的包的位置。</p>\n<p>改完之后,万幸,代码能跑起来了~</p>\n<p>体会就是“只要我们不停下脚步,道路就会不断延伸”。</p>\n<p>▏n<br>\n█▏ 、⺍<br>\n█▏ ⺰ʷʷィ<br>\n█◣▄██◣<br>\n◥██████▋<br>\n ◥████ █▎<br>\n ███▉ █▎<br>\n ◢████◣⌠ₘ℩<br>\n ██◥█◣\\≫<br>\n ██ ◥█◣<br>\n █▉ █▊<br>\n █▊ █▊<br>\n █▊ █▋<br>\n █▏ █▙<br>\n █</p>\n<h3>两年后重新编译这个混编的项目</h3>\n<p>混编是基于<a href=\"https://bitbucket.org/ns3lteu/ns-3-dev-lbt/src/laa-wifi-coexistence/src/\">别人做 lte-u 的版本</a> 做的,只是在 scratch 目录下面添加了自己的代码。</p>\n<p>我现在的 WSL 是安装的 Ubuntu 20,默认是 gcc9 和 python3.9,但是很奇怪的是 <code>/usr/include/</code> 下默认是安装的 python3.8-dev 的头文件,混编很麻烦会有各种奇怪的问题,没法编译过。当时没有写清楚配置,现在又麻烦了。</p>\n<p>既然已经想不起来当时的配置了,只能排列组合尝试了(没有找到 gcc、python-dev 和 python 的对应版本的表)。最后确认是 gcc 7.3,python3.6-dev 和 Python 3.5,成功编译过了。但是当时肯定不是这个组合的,python 代码里用了 <code>f"{}"</code>格式字符串,应该当时是在 python3.6 以上的。</p>\n<p>安装 gcc 7.3:apt 源上的gcc7 不是 gcc 7.3,是 gcc 7.5,会有 LTO 不匹配的问题,类似于 <a href=\"https://segmentfault.com/a/1190000022655994\">该老哥碰到的</a>。所以 gcc 7.3 要从头编译,之后可以用 update-alternative 管理下本机的多版本 gcc,注意在编译 ns3 项目的时候切换回来就行了,同时保持 g++ 和 gcc 版本一致。编译过程中可能会碰到 glibc 新版本丢弃掉了 <code><sys/ustat.h></code> 的 <a href=\"https://blog.csdn.net/weixin_46584887/article/details/122538399\">问题</a> 和一个静态检查的<a href=\"https://stackoverflow.com/questions/63437209/error-narrowing-conversion-of-1-from-int-to-long-unsigned-int-wnarrowi\">问题</a>。</p>\n<p>想要安装低版本的 python3.6-dev,想要添加额外的源,可以参考这个<a href=\"https://stackoverflow.com/questions/43621584/why-cant-i-install-python3-6-dev-on-ubuntu16-04\">链接</a>。注意要安的是 python3.6-dev,确保 <code>/usr/include</code> 下有 python3.6 头文件目录,代码里会引用这个下面的 <code>Python.h</code>。最新的 conda 安装目录下的 <code>include</code> 中默认携带的是 python3.9-dev 的头文件,不能直接拿来用。</p>\n<p>之后 conda 里安 python 3.6 的环境,否则也可能会报 LTO 版本不一致问题。此时可以在进入到 conda 的 python 3.6 环境下编译 ns3 项目了。</p>\n<p>以及 Python 出问题的地方可以用,<code>PyErr_Print()</code> 函数可以用来打印错误信息。</p>\n<h3>补充:ns3文档中相关内容</h3>\n<p>事实上,对于在编译时添加别的依赖ns3文档中有<a href=\"https://www.nsnam.org/wiki/HOWTO_use_ns-3_with_other_libraries\">相关描述</a>(当然去读主目录下的<code>wscript</code>也可以发现,可以试着在<code>wscript</code>中搜索<code>CXXFLAGS</code>),可以用<code>CCFLAGS_EXTRA</code>这些选项为编译器添加参数或者在<code>wscript</code>里面改,原因是<code>wscript</code>中这样定义过了</p>\n<pre><code class=\"language-python\"># append user defined flags after all our ones\nfor (confvar, envvar) in [['CCFLAGS', 'CCFLAGS_EXTRA'],\n ['CXXFLAGS', 'CXXFLAGS_EXTRA'],\n ['LINKFLAGS', 'LINKFLAGS_EXTRA'],\n ['LINKFLAGS', 'LDFLAGS_EXTRA']]:\n</code></pre>\n<p>于是<a href=\"https://stackoverflow.com/questions/11876088/how-to-build-ns-3-to-use-c0x-c11-libraries\">stackoverflow</a>上有人提到如果是为编译器添加c++11选项可以这么做</p>\n<pre><code class=\"language-bash\">CXXFLAGS="-std=c++0x" ./waf build\n</code></pre>\n<p>但是我试过这类方法,失败了,原因是用了<code>CXXFLAGS</code>,<code>CXXDEFINES</code>,<code>LINKFLAGS</code>这些参数对<code>python3-config --cflags</code>,<code>python3-config --ldflags</code> 和<code>-fPIC</code>都不合适= =我不知道咋用这种方法设置了。</p>\n<h2>一个很有趣的repo</h2>\n<p>之前打算用python的binding api,刚好发现了这个repo:<a href=\"https://github.com/tkn-tub/ns3-gym\">ns3-gym</a> 。</p>\n<p>但是把这个repo clone下来后,先去编译ns3的部分,再按README中<code>pip3 install ./src/opengym/model/ns3gym</code>安python的api,不是像其他模块那样用pybindgen自动生成的api。</p>\n<p>但是很可惜没用build成功,具体情况记录在该<a href=\"https://github.com/tkn-tub/ns3-gym/issues/32\">issue</a>。问题主要出在protobuf和zmq上。protobuf作者用了一个比较旧的版本,从PPA拉取后却检测不到,后来发现是登录用户的环境变量没加<code>/usr/bin</code>,这个主要是用来编译<code>src/opengym/model/messages.proto</code>和提供链接库的,编译方法是在<code>src/opengym/model</code>下调用<code>protoc ./messages.proto --cpp_out=./</code>。而且这里如果anaconda环境中安了python版的protobuf,同样需要关掉anaconda的环境,用<code>which protoc</code>看一下吧。zmq的话,作者提到用 <strong>libzmq5-dev</strong>,但是ubuntu16.04上只能找到 <strong>libzmq3-dev</strong>。不过似乎是没有什么兼容性问题的,毕竟最后可以跑起来。zmq这里出问题的原因是作者接受了一个pr,改了api的调用方式,但是我们的版本里某些参数似乎还是optional的。不写平台、版本乱提pr害人不浅= =</p>\n<h2>TIPS</h2>\n<ul>\n<li>\n<p>总是考虑去用Helper</p>\n<p>ns3中的很多类都有helper类,尝试使用它们~</p>\n</li>\n<li>\n<p>直接用官方的ShowProgress绑定到std::cout上会有问题</p>\n</li>\n<li>\n<p>Schedule用来安排函数的执行时,只需要指定运行时间并引用函数的指针,如<code>Simulator::Schedule (Seconds (1), &FunctionName)</code>;用来安排某个类的方法的执行时,要写出执行该方法的对象的指针,如如<code>Simulator::Schedule (Seconds (1), &ClassName::FunctionName,ObjectOfTheClass)</code>。</p>\n</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20200616/",
"title": "NS3 简记",
"summary": "NS3中混编C++/python代码的痛苦经历",
"date_modified": "2020-06-16T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>Ubuntu启动Grub引导错误</h1>\n<p>[[toc]]</p>\n<p>把系统玩坏了,重新安装系统后,发现启动不了Ubuntu系统,会停留在Grub界面,类似下图</p>\n<p><img src=\"./grub.png\" alt=\"\"></p>\n<p>首先在这个界面下可以输入<code>exit</code> 回到windows的启动</p>\n<h3>解决方案</h3>\n<p>通过阅读Grub2的帮助文档,可以发现解决方案(<a href=\"https://help.ubuntu.com/community/Grub2/Troubleshooting\">文档链接</a>)</p>\n<p>使用如下命令手动挂载image</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">步骤</th>\n<th>说明</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">1. <code>set root=(hdX,Y)</code></td>\n<td>Confirm the correct X,Y values and press ENTER.</td>\n</tr>\n<tr>\n<td style=\"text-align:left\"></td>\n<td>Example: If the Ubuntu system is on sda5, enter: <code>set root=(hd0,12)</code></td>\n</tr>\n<tr>\n<td style=\"text-align:left\">2. <code>linux /vmlinuz root=/dev/sdXY ro</code></td>\n<td>Example: <code>linux /vmlinuz root=/dev/sda12 ro</code></td>\n</tr>\n<tr>\n<td style=\"text-align:left\"></td>\n<td>If the <em>vmlinuz</em> symlink does not exist, use the full path to the kernel in <code>/boot</code>. Example: <code>linux /boot/vmlinuz-3.2.0-14-generic root=/dev/sda1 ro</code></td>\n</tr>\n<tr>\n<td style=\"text-align:left\">3. <code>initrd /initrd-xxx-xxx.img</code></td>\n<td>Selects the latest initrd image. If the <em>initrd</em> symlink does not exist, use the full path to the initrd image in <code>/boot</code> as above <code>vmlinuz</code>.</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">4. <code>boot</code></td>\n<td>Boot to the latest kernel on the selected partition.</td>\n</tr>\n</tbody>\n</table>\n<p>:::tip</p>\n<p>GNU GRUB(GRand Unified Bootloader,简称“GRUB”)是一个来自GNU项目的多操作系统启动程序。GRUB是多启动规范的实现,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。GRUB可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。</p>\n<p>:::</p>\n<p>:::tip</p>\n<p>vmlinuz指的是内核,作用:进程管理、内存管理、文件管理、驱动管理、网络管理。</p>\n<p>initrd.img是一个小的映象, 放的是和启动相关的驱动模块。通常的步骤是先启动内核,然后内核挂载initrd.img,并执行里面的脚本来进一步挂载各种各样的模块。其中最重要的就是根文件系统驱动模块,有了它才能挂载根文件系统,继而运行用户空间的第一个应用程序init或者systemd,完成系统后续的启动。</p>\n<p>:::</p>\n<h3>引导文件修复</h3>\n<p>成功进入系统后,使用如下命令对引导程序进行修复</p>\n<pre><code class=\"language-shell\"># 修复grub\nsudo update-grub\nsudo grub-install /dev/sda \nreboot #重启\n</code></pre>\n<p>也可以试着回到windows中修改启动项,以管理员的身份在cmd中敲入命令:</p>\n<p><code>bcdedit /set "{bootmgr}" path \\EFI\\ubuntu\\grubx64.efi</code></p>\n<p>实在不行推荐使用<code>boot-repair</code>工具</p>\n<pre><code class=\"language-shell\">sudo add-apt-repository ppa:yannubuntu/boot-repair\nsudo apt-get update\nsudo apt-get install boot-repair\n</code></pre>\n<p>之后在应用菜单中选择boot-repair(引导修复)使用该工具修复。如果修复后出现了多余的启动项,在<code>/boot/grub/grub.cfg </code>中删掉相关的条目即可。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20191112/",
"title": "Ubuntu启动Grub引导错误",
"summary": "重装双系统碰到的Grub引导错误",
"date_modified": "2019-11-12T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>移动通信和SIM卡</h1>\n<p>[[toc]]</p>\n<h3>移动通信的多址接入技术</h3>\n<p>移动通信(cellular network)中的多址接入技术有FDMA、TDMA、CDMA、SDMA、OFDM、NOMA。</p>\n<p>第一代移动通信系统(1G)主要采用频分多址接入方式(FDMA),第二代移动通信系统(2G)主要采用时分多址接入方式(TDMA),第三代移动通信系统(3G)主要采用码分多址接入方式(CDMA),第四代通信系统(4G)主要采用正交频分复用多址接入方式(OFDM)。</p>\n<h4>FDMA(频分复用)</h4>\n<p>频分多址(Frequency Division Multiple Access,FDMA),是把总带宽被分隔成多个正交的信道,每个用户占用一个信道。例如,把分配给无线蜂窝电话通信的频段分为30个信道,每一个信道都能够传输语音通话、数字服务和数字数据。频分多址是模拟高级移动电话服务(AMPS)中的一种基本的技术,北美地区应用最广泛的蜂窝电话系统。采用频分多址,每一个信道每一次只能分配给一个用户。频分多址还用于全接入通信系统(TACS)。</p>\n<h4>TDMA(时分多址)</h4>\n<p>时分多址(Time division multiple access,TDMA) 是一种为实现共享传输介质(一般是无线电领域)或者网络的通信技术。它允许多个用户在不同的时间片(时隙)来使用相同的频率。用户迅速的传输,一个接一个,每个用户使用他们自己的时间片。这允许多用户共享同样的传输媒体(例如:无线电频率)。</p>\n<h4>CDMA(码分多址)</h4>\n<p>码分多址(Code Division Multiple Access,CDMA)是指利用码序列相关性实现的多址通信。码分多址的基本思想是靠不同的地址码来区分地址。每个地址配有不同的地址码,用户所发射的载波(为同一载波)既受基带数字信号调制,又受地址码调制,接收时,只有确知其配给地址码的接收机,才能解调出相应的基带信号,而其他接收机因地址码不同,无法解调出信号。划分是根据码型结构不同来实现和识别的,一般选择伪随机码(PN码)作地址码。由于PN码的码元宽度远小于PCM信号码元宽度(通常为整数倍),这就使得加了伪随机码的信号频谱远大于原基带信号的频谱,因此,码分多址也称为扩频多址。</p>\n<h4>SDMA(空分复用接入)</h4>\n<p>SDMA(Space Division Multiple Access,空分复用接入)是一种卫星通信模式,它利用碟形天线的方向性来优化无线频域的使用并减少系统成本。这种技术是利用空间分割构成不同的信道。</p>\n<h4>OFDM(正交频分复用)</h4>\n<p>正交频分复用(Orthogonal Frequency Division Multiplex,OFDM)。正交频分复用是在频分复用的基础上进一步压缩频带,提高频谱利用率。如下图所示,用户之间的频带有所交叠,但是每个用户频带功率最大的那个点其他的信号能量都为0,所以在每个用户频带功率最大值点处,各个用户信号依旧是正交的。</p>\n<h4>NOMA (非正交多址)</h4>\n<p>NOMA跟以往的多址接入技术不同,NOMA采用非正交的功率域来区分用户。所谓非正交就是说用户之间的数据可以在同一个时隙,同一个频点上传输,而仅仅依靠功率的不同来区分用户。在发送端采用非正交发送,主动引入干扰信息,在接收端通过串行干扰删除技术实现正确解调。与正交传输相比,接收机复杂度有所提升,但可以获得更高的频谱效率。</p>\n<ul>\n<li>串行干扰消除(SIC):引入干扰信息可以获得更高的频谱效率,但是同样也会遇到多址干扰(MAI)的问题。关于消除多址干扰的问题。NOMA在接收端采用SIC接收机来实现多用户检测。串行干扰消除技术的基本思想是采用逐级消除干扰策略,在接收信号中对用户逐个进行判决,进行幅度恢复后,将该用户信号产生的多址干扰从接收信号中减去,并对剩下的用户再次进行判决,如此循环操作,直至消除所有的多址干扰。</li>\n<li>功率复用:SIC在接收端消除多址干扰(MAI),需要在接收信号中对用户进行判决来排出消除干扰的用户的先后顺序,而判决的依据就是用户信号功率大小。基站在发送端会对不同的用户分配不同的信号功率,来获取系统最大的性能增益,同时达到区分用户的目的,这就是功率复用技术。</li>\n</ul>\n<h3>技术演进</h3>\n<h4>2G</h4>\n<p>2G包含GSM。全球移动通信系统(Global System for Mobile Communications) ,缩写为GSM,由欧洲电信标准组织ETSI制订的一个数字移动通信标准。它的空中接口采用时分多址技术 。自90年代中期投入商用以来,被全球超过100个国家采用。GSM标准的无处不在使得在移动电话运营商之间签署"漫游协定"后用户的国际漫游变得很平常。 GSM 较之它以前的标准最大的不同是它的信令和语音信道都是数字式的,因此GSM被看作是第二代 (2G)移动电话系统。</p>\n<h4>3G</h4>\n<p>3G包含UMTS和LTE。第三代 (3G)移动电话系统因为主要运用了CDMA技术,故3G三大标准命名为美国CDMA2000,欧洲WCDMA,中国TD-SCDMA。</p>\n<h4>4G</h4>\n<p>4G包含WiMax和LTE。LTE是long Term Evolution(长期演进)的缩写。3GPP标准化组织最初制定LTE标准时,定位为3G技术的演进升级。后来,LTE技术的发展远远超出了预期,LTE的后续演进版本Release10/11(即LTE-A)被确定为4G标准。LTE根据双工方式不同,分为LTE-TDD和LTE-FDD两种制式,其中LTE-TDD又称为TD- LTE。</p>\n<h4>5G</h4>\n<p>包含三大应用场景:</p>\n<ul>\n<li>eMBB:增强型移动宽带(人类通信)</li>\n<li>mMTC:海量机器类通信(物联网)</li>\n<li>URLLC:超可靠、低时延通信(无人驾驶、工业自动化)<br>\n五大创新:</li>\n<li>mmWave:使用目前波段较小的mmWave(毫米波),频率高,便于提高频谱带宽,传输速度快(由香农第二定理)。</li>\n<li>Massive MIMO:Multiple-Input Multiple-Output。基站的天线变多了,并且手机的接受能力也变强了,源头上多根天线发送,接收对象多根天线接受。为了进一步提升5G网络的覆盖面积,5G网络将原有的宏基站改为了微基站。</li>\n<li>Beam Management意为波束赋形,它主要是改变了信号的发射形式进行的改变。基于天线阵列的信号预处理技术,通过调整天线阵列中的每个阵元的加权系数产生具有指向性的波束。</li>\n<li>LDPC/Polar:Polar Code(极化码)为控制信道的编码方案,LDPC码作为数据信道的编码方案。</li>\n<li>AS Layer:一种新型的架构模式,主要是以正交频分多任务(OFDM)为基础的弹性参数物理层,它可以最多包含5个次载波。该架构可以同时回应更快速的数据与响应速度。</li>\n</ul>\n<p>:::tip<br>\n香农第二定理:</p>\n<p>在噪声与信号独立的高斯白噪信道中,假设信号的功率为S,噪声功率为N,信道通频带宽为W(Hz),则该信道的信道容量C为</p>\n<p>$C=W \\log _{2}\\left(1+\\frac{S}{N}\\right)$</p>\n<p>单位是bps。这里信道容量是一定信噪比和传输带宽下,信道的传输速率上限。<br>\n:::</p>\n<h4>3GPP</h4>\n<p>3GPP最初的工作范围是为第三代移动通信系统制定全球适用的技术规范和技术报告,之后致力于移动通信的标准化。3GPP制定的标准规范以Release作为版本进行管理。</p>\n<h3>SIM卡</h3>\n<p>SIM(Subscriber Identity Model,客户识别模块)卡为大规模集成电路卡片。卡片内部存了数字移动电话客户的信息、加密密钥等内容,可对客户身份进行鉴别,并对客户通话时的语音信息进行加密。SIM卡的使用,防止通话被窃听。SIM卡的制作是严格按照GSM国际标准和规范来完成的,它使客户的正常通信得到了可靠的保障。一张SIM卡唯一标识一个客户。一张SIM卡可以插入任何一部手机中使用,而使用手机所产生的通信费用则自动记录在该SIM卡所唯一标识的客户的帐户上。</p>\n<p>硬件结构上SIM卡是一个装有微处理器(CPU)的芯片卡,它的内部有5个模块,并且每个模块都对应一个功能:微处理器CPU、程序存储器ROM(3~8kbit)、工作存储器RAM(6~16kbit)数据存储器EEPROM(16~256kbit)和串行通信单元。这5个模块被胶封在SIM卡铜制接口后与普通IC卡封装方式相同。这5个模块必须集成在一块集成电路中,否则其安全性会受到威胁,因为芯片间的连线可能成为非法存取和盗用SIM卡的重要线索。</p>\n<p>SIM卡的背面有以五个一排,被排成四排的一组数字,在这组数字最前面的六位数字所代表的是中国的代号。第七位数字则代表的是接入号码。第八位数字代表的是该SIM卡的功能位。第九和第十位数字代表了该SIM卡所处的省份。至于第十一和第十二位数字则代表的是该SIM卡的年号,而第十三位数字则是SIM卡供应商的代码。从第十四位开始至第十九位数字则代表了该SIM卡的用户识别码。最后一个数字是校验位。</p>\n<h4>SIM卡存储的数据</h4>\n<ul>\n<li>\n<p>由SIM卡生产厂商存入的系统原始数据</p>\n</li>\n<li>\n<p>存储手机的固定信息,手机在出售之前都会被SIM卡中心记录到SIM卡当中,主要包括鉴权和加密信息、国际移动用户识别码(IMSI)、IMSI认证算法、加密密匙生成算法、密匙生成前的用户密匙的生成算法(这三种算法均为128位)</p>\n</li>\n<li>\n<p>用户自己存入的数据,如短消息、固定拨号、缩位拨号、性能参数、话费记数等;能够存储有关的电话号码,也就是具备电话簿功能。</p>\n</li>\n<li>\n<p>有关于网络方面的数据,用户在用卡过程中自动存入和更新的网络接续和用户信息类数据,包括最近一次位置登记时手机所在位置识别号、设置的周期性位置更新间隔时间、临时移动用户号等。不过这种数据的存放是暂时性的,也就是说它并不是永久的存放于SIM卡之中。</p>\n</li>\n<li>\n<p>相关的业务代码,这一点相信也是大家很熟悉的,那就是非常重要的个人识别码(PIN码),还有就是解开锁定用的解锁码(PUK)等等。</p>\n</li>\n</ul>\n<h4>IMEI、IMSI、KI、ICCID</h4>\n<ul>\n<li>国际移动用户识别码(IMSI:International Mobile Subscriber Identification Number)是区别移动用户的标志,储存在SIM卡中,可用于区别移动用户的有效信息。IMSI总长度不超过15位,同样使用0~9的数字。其中MCC是移动用户所属国家代号,占3位数字,中国的MCC规定为460;MNC是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网;MSIN是移动用户识别码,用以识别某一移动通信网中的移动用户。</li>\n<li>KI (Key Identifier)是SIM卡与运营商之间加密数据传递的密钥。当系统进行验证时会同时使用KI及IMSI,经过一连串系统安全认证讯息后产生随机变量,进行加密计算后验证身份入网。</li>\n<li>国际移动设备识别码(IMEI:International Mobile Equipment Identification Number)是区别移动设备的标志,储存在移动设备中,可用于监控被窃或无效的移动设备。移动终端设备通过键入“*#06#” 即可查得。其总长为15位,每位数字仅使用0~9的数字。其中TAC代表型号装配码,由欧洲型号标准中心分配;FAC代表装配厂家号码;SNR为产品序号,用于区别同一个TAC和FAC中的每台移动设备;SP是备用编码。</li>\n<li>ICCID:Integrate circuit card identity 集成电路卡识别码即SIM卡卡号,相当于手机号码的身份证。 ICCID为IC卡的唯一识别号码,共有20位数字组成。</li>\n</ul>\n<p>由此看来,只要知道SIM卡的KI、IMSI值,我们就可以通过软件仿真出SIM卡的功能,甚至可以利用多组KI、IMSI值,用一张微处理器卡片来同时仿真本来需要多张SIM所完成的功能,这就是“一卡多号”技术。</p>\n<h3>小米eSIM文档简记</h3>\n<p><a href=\"http://doc.miot.10046.mi.com/esim/eSIM.html\">原文档传送门</a></p>\n<p>具体细节应该阅读GSMA SGP协议。</p>\n<p>eSIM指嵌入式SIM卡(Embedded SIM),支持通过空中远程配置SIM卡数据,最初应用在物联网领域,近年逐渐发展到消费电子领域。主要特点有:</p>\n<p>1、物理尺寸小,可直接封装在通信模块上,节省硬件空间,适应恶劣的工作环境,使用寿命长,可有效的保证移动通信的稳定性和设备的安全性。</p>\n<p>2、可以动态远程下载SIM卡数据,方便用户主动触发下载/管理卡数据,或者动态更换卡数据,灵活选择签约运营商。</p>\n<p>GSMA制定了两套eSIM标准,一套面向M2M市场,一套面向公众消费市场。在M2M领域,eSIM封装一般采用不可插拔形态,而在消费电子领域,eSIM封装既可以采用不可插拔形态, 也可以采用可插拔形态。M2M领域的设备主要包括公用仪表、监控摄像头和车载设备等,M2M设备根据需要可能放置于偏远或高温高湿等较恶劣的环境中;消费电子设备通常包括手机、平板电脑和可穿戴设备等。由于M2M设备所处的环境限制且M2M业务通常由平台侧发起,M2M eUICC中必须包含预置号码使得平台能够与eUICC建立初始连接;而消费电子设备比较灵活,可以通过Wifi、蓝牙等方式建立通信连接,因此不是必须包含预置号码。</p>\n<p>eSIM发行后,为了实现对卡片上的文件和参数集进行配置管理,需要设计一套eSIM卡远程配置(Remote SIM Provisioning,RSP)管理系统。</p>\n<h4>M2M</h4>\n<ul>\n<li>\n<p>eUICC:嵌入式UICC;</p>\n</li>\n<li>\n<p>SM-SR:签约管理安全路由服务器,主要功能是实现eUICC远程配置数据的安全路由和传输;</p>\n</li>\n<li>\n<p>SM-DP :签约管理数据准备服务器,主要功能是eUICC卡数据的生成和管理,需要与其他的运营商后台系统以及制卡中心进行交互,获取并生成相应的卡数据。</p>\n</li>\n<li>\n<p>MNO:运营商(小米移动物联网平台),主要负责eUICC卡片信息的维护与管理,并提供开户、注销、码号迁移等功能。</p>\n</li>\n<li>\n<p>EUM:eUICC生产商;</p>\n</li>\n<li>\n<p>CI:证书发行。</p>\n</li>\n</ul>\n<p>eUICC远程管理平台是eUICC远程管理的核心,主要功能是实现eUICC远程下载个人数据,并且通过提供授权认证、防攻击、隐私保护和完整性保护等措施保证下载过程的安全性。eUICC远程管理平台根据功能可以划分为数据准备SM-DP和数据安全路由SM-SR两部分。在数据准备阶段,SM-DP接收来自MNO的数据参数生成个人数据并进行加密。在数据传输过程中,SM-SR建立到eUICC的安全通道,将个人数据和消息通过安全路由下载到目标eUICC中,并负责将eUICC发送的消息路由到管理平台。同时,SM-SR也负责管理已经下载到eUICC中的个人数据,如激活、去激活、删除等。</p>\n<h4>消费电子设备</h4>\n<p>相比M2M的eSIM架构,SM-SR的功能由SM-DP+以及LPA取代(某些部署模式下仍存在SM-SR),并且新增加了本地Profile代理(LPA,Local Profile Agent)以及发现服务器(SM-DS)。</p>\n<p>LPA是新架构中的关键组成,它实现了三部分功能:</p>\n<ul>\n<li>\n<p>本地发现(Local Discovery Service),从发现服务器(SM-DS)获取能够给eUICC提供Profile Package的目标SM-DP+的地址;</p>\n</li>\n<li>\n<p>Profile下载(Local Profile Download),作为eUICC与SM-DP+之间的代理,从SM-DP+获取Profile数据包,再转移到eUICC中;</p>\n</li>\n<li>\n<p>本地用户接口(Local User Interface),面向用户提供Profile管理的接口和功能,包括Profile的激活、去激活、删除等。</p>\n</li>\n</ul>\n<p>SM-DS负责为终端设备提供目标SM-DP+的地址,使得终端设备可以找到该SM-DP+并下载所需的Profile Package,其功能包括:</p>\n<ol>\n<li>eUICC ID注册:,当为目标eUICC准备的Profile Package就绪后,SM-DP+向SM-DS注册该eUICC ID;</li>\n<li>SM-DP地址提供:目标终端设备的LPA访问SM-DS,SM-DS向其提供SM-DP+的URL地址。</li>\n</ol>\n<p>激活码(Activation Code):激活码用于启动到指定SM-DP+的Profile下载流程,包括SM-DP+地址、激活码令牌、SMDPid等信息,它能够唯一标识运营商/业务提供商,可以支持二维码扫描、人工输入等方式输入激活码。</p>\n<p>一个常见的基于以上eUICC远程业务配置融合方案的业务操作过程由以下几个步骤组成:</p>\n<ol>\n<li>\n<p>运营商与用户达成业务订购协议,向SM-DP+发起创建Profile Package的指令。</p>\n</li>\n<li>\n<p>SM-DP+创建Profile Package,其中包括与可插拔SIM卡一样的IMSI、Ki、AKA等密钥及数据,并且对该Profile Package进行加密。</p>\n</li>\n<li>\n<p>SM-DP+向SM-DS发起eUICC ID注册,告知SM-DS为目标eUICC准备的Profile Package已经就绪。</p>\n</li>\n<li>\n<p>终端设备发起SM-DS查询获取SM-DP+的地址,此外,还可以由运营商在定制终端内预置SM-DP+信息、用户扫描二维码等途径获取SM-DP+地址。之后消费电子设备可通过LPA接入到SM-DP+并下载经定制的Profile Package。</p>\n</li>\n<li>\n<p>LPA把Profile Package转发给eUICC,由eUICC对Profile Package进行解密并安装Profile。</p>\n</li>\n<li>\n<p>用户通过LPA操作eUICC上存储的Profile,如:激活、切换、删除等。</p>\n</li>\n</ol>\n<h4>案例</h4>\n<p>基于TEE/eSE的eSIM方案,在小米手机上已经成功商用,小米漫游业务就是基于该方案的产品。小米漫游面向出境用户,提供海外数据流量服务。用户在手机上购买目的地区的流量套餐后,利用上述技术,会自动下载一个当地的SIM卡的profile到手机的TEE/eSE中,用户到达目的地后,就可以直接启动该流量套餐。(这里小米只会提供一个运营商的账号,持续使用?)</p>\n<p>:::tip</p>\n<p>eSE</p>\n<p>基于硬件芯片的模块,安全级别可以做到最高。</p>\n<p>eSE(嵌入式安全芯片)是一种防篡改的芯片,其大小不一,设计也可不同,并可嵌入在任意一种移动设备中。基于硬件芯片的模块,安全级别可以做到最高,如果是在eSE里实现的eSIM功能,其功能不仅仅是目前运营商业务,意味着eSE的适用范围较广,可保证任意一种设备以及各种用例(例如支付、票券兑换、交通、访问控制、票务、公司、云计算、电子政务等)中应用程序的安全。</p>\n<p>:::</p>\n<p>:::tip</p>\n<p>REE(Rich Execution Environment)</p>\n<p>所有移动设备都支持REE,用于运行通用OS:Android、iOS、Linux,为上层App提供设备的所有功能,是开放的、可扩展的且通用的。但是基于OS实现的App隔离极易被绕过;OS代码庞大,漏洞频发;OS很难被检验和认证;OS可以看到App内部的所有数据;缺乏隔离意味着App无法安全存储密钥。</p>\n<p>:::</p>\n<p>:::tip</p>\n<p>TEE(Trusted Execution Environment)可信执行环境</p>\n<p>受硬件机制保护,TEE隔离于REE、只能通过特定的入口与TEE通信、并不规定某一种硬件实现方法;TEE运行时使用CPU的全部性能(独占),具有高性能;TEE可以访问REE的内存、REE无法访问受硬件保护的TEE内存,通信快速;TEE中可以同时运行多个Trusted Application(TA)。</p>\n<p>由GlobalPlatform(GP)标准化,TEE中的可执行代码在执行前先要被验证(validate)。</p>\n<p>:::</p>\n<h3>安卓eSIM文档简记</h3>\n<p><a href=\"https://source.android.google.cn/devices/tech/connect/esim-overview\">原文档传送门</a></p>\n<p>嵌入式 SIM(又称 eSIM 或 eUICC)是一种最新技术,可让移动用户在没有实体 SIM 卡的情况下,下载运营商配置文件并激活运营商服务。该技术是由 GSMA 推动的全球规范,支持在任何移动设备上进行远程 SIM 配置。从 Android 9 开始,Android 框架提供了用于访问 eSIM 和管理 eSIM 上的订阅配置文件的标准 API。借助这些 eUICC API,第三方可以在支持 eSIM 的 Android 设备上开发自己的运营商应用和 Local Profile Assistant (LPA)。</p>\n<p>LPA 是一款独立的系统应用,应包含在 Android 编译映像中。对 eSIM 上配置文件的管理通常由 LPA 完成,因为它充当着 SM-DP+(用来准备、存储配置文件包并将其交付给设备的远程服务)和 eUICC 芯片之间的桥梁。LPA APK 可以选择性地包含一个界面组件(又称 LPA 界面或 LUI),以便为最终用户提供一个中心位置来管理所有嵌入式订阅配置文件。Android 框架可自动发现可用性最高的 LPA 并与之连接,然后通过 LPA 实例路由所有 eUICC 操作。</p>\n<p>有兴趣开发运营商应用的移动网络运营商可以参阅 <a href=\"https://developer.android.google.cn/reference/android/telephony/euicc/EuiccManager\">EuiccManager</a> 中的 API,其中介绍了高级配置文件管理操作(例如 <code>downloadSubscription()</code>、<code>switchToSubscription()</code> 和 <code>deleteSubscription()</code>)。</p>\n<p>如果您是有兴趣自行开发 LPA 系统应用的原始设备制造商 (OEM),那么您必须为 Android 框架扩展 <a href=\"https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/service/euicc/EuiccService.java\">EuiccService</a> 以连接到您的 LPA 服务。此外,您还应使用 <a href=\"https://android.googlesource.com/platform/frameworks/base/+/master/telephony/java/android/telephony/euicc/EuiccCardManager.java\">EuiccCardManager</a> 中的 API,这些 API 提供了基于 GSMA 远程 SIM 配置 (RSP) v2.0 的 ES10x 函数。此类函数用于向 eUICC 芯片发出命令(例如 <code>prepareDownload()</code>、<code>loadBoundProfilePackage()</code>、<code>retrieveNotificationList()</code> 和 <code>resetMemory()</code>)。</p>\n<p><a href=\"https://developer.android.google.cn/reference/android/telephony/euicc/EuiccManager\">EuiccManager</a> 中的 API 需要一个正确实现的 LPA 应用才能正常运行,且 <a href=\"https://android.googlesource.com/platform/frameworks/base/+/master/telephony/java/android/telephony/euicc/EuiccCardManager.java\">EuiccCardManager</a> API 的调用程序必须是 LPA。这是 Android 框架的强制要求。</p>\n<p>搭载 Android 10 或更高版本的设备可以支持具有多个 eSIM 卡的设备。</p>\n<h3>SIM卡写号方式</h3>\n<ul>\n<li>\n<p>通过USIM应用程序复制粘贴。</p>\n</li>\n<li>\n<p>APP通过OMAPI接口写入。安卓可以使用OMAPI(Open Mobile API)。</p>\n</li>\n<li>\n<p>OTA(Over the Air Technology)短信写号。该方法需要卡上原先有一个种子号用来接收短信。</p>\n</li>\n<li>\n<p>BIP。BIP需要一个种子号,使用种子号的数据流量和服务器通信。</p>\n</li>\n<li>\n<p>STK。IOS只能通过STK私有的API接口写号。</p>\n</li>\n</ul>\n<p>有些特殊的卡比如蓝牙卡和SWP卡,有特殊接口手机传输指令到SIM卡。</p>\n<p>运营商的空白卡在里面集成了iccid、ki和opc,没有imsi。营业厅写号写的是imsi。</p>\n<h3>STK(SIM Tool Kit)</h3>\n<p>STK/UTK 是Sim卡工具包,其中定制了与运营商相关的应用。SIM卡是插在Modem中的,要读取SIM卡的内容,就必须要经过Modem层,而与Modem层进行交互离不开AT指令。安卓的RIL层(Radio Interface Layer)可以发送AT指令。运营商读取SIM卡流程可以归结为:STK应用---->RILJ---->RILC---->Modem---->运营商的基站。</p>\n<h3>IOS Core Telephony API</h3>\n<ul>\n<li>\n<p>CTCarrier定义了Carrier(真实的运营商)的类。类的方法提供了接口去获取运营商的名字,卡上的IMSI中的MCC,运营商国家编码。</p>\n</li>\n<li>\n<p>CTTelephonyNetworkInfo类提供了移动服务提供商的信息。</p>\n</li>\n<li>\n<p>CTSubscriber是订阅移动网络变化事件的类,可用CTSubscriberInfo实例化后获取,与CTSubscriberDelegate相配合。</p>\n</li>\n<li>\n<p>CTCellularData用来确定app能否获取移动数据。</p>\n</li>\n<li>\n<p>CTCellularPlanProvisioningRequest用来创建一个向eSIM服务器发送请求的对象,服务器需要符合上面的SMDP+标准。使用之前的api创建对象后,使用CTCellularPlanProvisioning类的实例来下载和安装eSIM。使用CTCellularPlanProvisioning类实例的addPlan()方法后,IOS会引导用户进行eSIM切换和设置。用户授权后可以后台进行eSIM的安装,使用beginBackgroundTask(expirationHandler:) 让addPlan可以在后台切换。</p>\n</li>\n</ul>\n<h3>微软对eSIM的支持</h3>\n<p>在 Intune 中,可以导入移动运营商提供的一次性使用的激活码。 要在 eSIM 模块上配置手机网络流量套餐,请将这些激活码部署到支持 eSIM 的设备。 当 Intune 安装激活码时,eSIM 硬件模块会使用激活码中的数据联系移动运营商。 完成后,eSIM 配置文件将下载到设备上,并配置为激活手机网络。<br>\n要使用 Intune 将 eSIM 部署到设备,需要以下条件:</p>\n<ul>\n<li>支持 eSIM 的设备,例如,Surface LTE:请参阅设备是否支持 eSIM。 或者,请参阅一些已知支持 eSIM 的设备的列表(在本文中)。</li>\n<li>已注册并且由 Intune 托管 MDM 的 Windows 10 Fall Creators Update PC(1709 或更高版本)</li>\n<li>移动运营商提供的激活码 。 这些一次性使用的激活码被添加到 Intune,并部署到支持 eSIM 的设备。 请联系移动运营商获取 eSIM 激活码。</li>\n</ul>\n<p>CSV 文件要求<br>\n使用具有激活码的 csv 文件时,请确保你或你的移动运营商遵循以下要求:</p>\n<ul>\n<li>\n<p>该文件必须采用 csv 格式 (filename.csv)。</p>\n</li>\n<li>\n<p>文件结构必须严格遵循格式要求。 否则,会导入失败。 Intune 在导入时检查文件,并且如果发现错误则会失败。</p>\n</li>\n<li>\n<p>激活码为一次性使用。 建议不要导入先前导入过的激活码,因为在部署到相同或不同的设备时可能会导致问题。</p>\n</li>\n<li>\n<p>每个文件应特定于单个移动运营商,并且所有激活码应特定于同一计费套餐。 Intune 将激活码随机分配给目标设备。 无法保证哪个设备会获得特定的激活码。</p>\n</li>\n<li>\n<p>一个 csv 文件中最多可以导入 1000 个激活码。</p>\n</li>\n</ul>\n<p>csv 的第一行和第一个单元格是移动运营商 eSIM 激活服务的 URL,称为 SM-DP +(订阅管理器数据准备服务器)。 URL 应为完全限定的域名 (FQDN),不带任何逗号。第二行和所有后续行都是包含两个值的唯一一次性使用的激活码:第一列是唯一的 ICCID(SIM 芯片的标识符)第二列是匹配的 ID,只用逗号分隔它们(末尾没有逗号)。</p>\n<p>eSIM 激活码为一次性使用。 Intune 在设备上安装激活码后,eSIM 模块会联系移动运营商以下载手机网络配置文件。 该联系人会完成将设备注册到移动运营商网络。</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20191109/",
"title": "移动通信和SIM卡",
"summary": "移动通信和SIM相关内容简记",
"date_modified": "2019-11-09T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>拟阵与贪心算法</h1>\n<p>[[toc]]</p>\n<p>拟阵可以用来分析和阐释贪心算法成立的原因。</p>\n<h2>子集系统</h2>\n<p>在了解拟阵前,需要先了解<strong>子集系统</strong>。子集系统定义在有序二元组$M=(S,L)$上,满足下面几个条件</p>\n<ul>\n<li>基础集合S是一个有限集。</li>\n<li>L是由S子集构成的集合(注意L是集合的集合)且L非空。</li>\n<li>遗传性:$\\forall A\\subseteq B$,如果$B\\subseteq L$,则$A\\subseteq L$(即L中元素的子集必在L中)。</li>\n</ul>\n<p>注意由第二、三点可知,空集$\\emptyset$必然在L中(包含空集的集合不是空集:rofl:)。</p>\n<p>例子:集合$S_1={1,2,3}$,$L_1$可以是${\\emptyset,{1},{2},{1,2}}$。</p>\n<h2>拟阵</h2>\n<p><strong>拟阵</strong>是特殊的子集系统,它需要满足交换性(又称独立扩展公理):$\\forall A\\subseteq L$,$B\\subseteq L$,$|A|<|B|$,$\\exists x\\in B\\setminus A$,$A\\cup{x}\\subseteq L$。</p>\n<p>即$L_2={\\emptyset,{1},{2},{3},{1,2}}$是{1,2,3}的子集系统,但是不是一个拟阵。</p>\n<p>而$L_3={\\emptyset,{1},{2},{1,2}}$是拟阵。</p>\n<p>若$S$中元素带权值$w(s)$,则$M$为带权拟阵。</p>\n<p>拟阵的并仍是拟阵。</p>\n<h2>独立集</h2>\n<p><strong>独立集</strong>是拟阵中的元素,仍然用$M=(S,L)$表示子集系统。对于$A\\subseteq S$,如果$A\\in L$,则$A$为独立集。对于独立集$A$,若存在$x\\in S$,满足$x\\notin A$且$A\\cup {x}\\in L$,则称$A$为可扩展的。不可扩展的独立集为<strong>极大独立集</strong>。</p>\n<p>它有几个性质:</p>\n<ul>\n<li>空集一定是独立的,也就是说,$\\emptyset\\in L$(与子集系统定义的遗传性的推论相符)。</li>\n<li>遗传性:每个独立集的子集是独立的(与子集系统定义遗传性相符)。</li>\n<li>交换性:如果$A$和$B$是$L$的两个独立集,$A$比$B$有更多的元素,则在$A$中存在一个元素,当其加入$B$时得到一个比$B$更大独立集 (与拟阵定义中的交换性相符)。</li>\n</ul>\n<p>关于拟阵和极大独立集有如下定理:</p>\n<ol>\n<li>\n<p>拟阵$M=(S,L)$的所有极大独立集都有相同大小。</p>\n</li>\n<li>\n<p>设加权拟阵$M=(S,L)$的权函数为$w(s)$且$S$已按权非升序排序。设$x$是第一个使得${x}$独立的元素,若这样的元素存在,则存在一个包含${x}$的最大权独立子集。</p>\n</li>\n<li>\n<p>设${x}$是对于加权拟阵$M=(S,L)$由定理2中所选择的S的第一个元素,则剩下问题可归结为求加权矩阵$M'=(S',L')$的最大权独立集问题,其中$S'={ y|y \\in S, (x,y) \\in L }$,$L'={B|B\\in S\\setminus{x}, B\\cup{x}\\in L}$</p>\n</li>\n</ol>\n<p>定理的证明思路:</p>\n<ol>\n<li>\n<p>由交换性可证定理1。</p>\n</li>\n<li>\n<p>设$B$是任意非空的最优子集,且$x\\notin B$,$\\forall y\\in B$,$w(x)\\ge w(y)$。构造集合$A={x}$,由交换性,可反复地在$B$中找出新元素加入$A$中直到$|A|<|B|$,且$A$仍是独立集。因此$\\forall y\\in B$,使得$A=B-{y}+{x}$。$w(A)=w(B)-w(y)+w(x)\\ge w(B)$。定理2得证。</p>\n</li>\n<li>\n<p>对于定理3,在$M'=(S',L')$中的最优解$X’$,$X'\\in L'$,由$L'$定义得$X'\\cup {x}\\in L$。$M=(S,L)$中包含x的最优解X,$X-{x}\\subset S-{x}$且$X\\in L$,即$X-{x}\\in L'$且为$L'$中最优解。定理3得证。</p>\n</li>\n</ol>\n<p><strong>定理二和定理三本质提供了一个递归的算法,由它利用数学归纳法可以推出拟阵上使用贪心算法的正确性。</strong></p>\n<h2>子集优化问题与贪心算法</h2>\n<p>子集优化问题:给集合$S$中每个元素赋予一个正值$w(s)$,在子集系统中选一个元素$E\\in L$,使得$w(E)$最大,其中$w(E)=\\sum \\limits_{e\\in E} w(e)$。</p>\n<p>该优化问题的解决方法是求权值最大的极大独立集。<strong>在拟阵中,可以使用贪心算法</strong>。将基础集合$S$中的所有元素$s$按$w(s)$从大到小排序,能加就加入到子集$E$中的元素$s$,就把它加入到$E$中。回顾最小生成树问题的Kruskal算法和Prim算法,是不是就是这么解决的?注意:权值和最小等价于这里的最大,可以把权值视为负数就等价了。</p>\n<pre><code>GreedyAlgorithm(M, w){\n A := ∅;\n (S,L) := M;\n Sort S to the descending order;\n For each x in S do\n If(A∪{x} in L) \n \tThen A := A∪{x};\n Return A;\n}\n</code></pre>\n<h2>案例分析:部分背包</h2>\n<blockquote>\n<p>给定一个最大载重量为T的背包和N种物品。已知第$i$种物品最多放入$V_i$,其单位价值为$W_i$,确定一个方案,使得装入背包中的所有物品总价值最大。</p>\n</blockquote>\n<p>对于部分背包问题,我们首先进行一步转化:将体积为$V_i$,单位价值为$W_i$的物品变成$V_i$个体积为1,价值为$W_i$的物品。定义$M=(S,L)$:</p>\n<ul>\n<li>S是所有物品的集合。</li>\n<li>$L={X:X\\subseteq S,|X|\\le T}$<br>\n这个M显然满足拟阵的前两个条件。</li>\n</ul>\n<p>根据$L$的定义,$\\forall A\\in L$,$\\forall B\\subseteq A$,满足$|B|\\le |A|\\le T$,有$B\\in L$,因此$M$满足遗传性,是一个子集系统。</p>\n<p>对于$\\forall A\\in L$,$B\\in L$,$|A|<|B|$,随意选取一个$x \\in B\\setminus A$,令$C=A\\cup {x}$,显然有$|C|=|A|+1\\le|B|$,所以M满足交换性。因此M是一个拟阵。</p>\n<p>背包问题目标是使权值最大,上面提到了$S$是所有物品的集合,将$S$的任意元素$x$的权值$w(x)$定义为$x$的价值,那么问题就转化成了求背包问题的拟阵中权值最大的独立集,也就是上面的子集优化问题的变种。显然权值最大的独立集是极大独立集,同时因为$M$是拟阵,故可以用贪心算法求解。</p>\n<h2>案例分析:最小生成树(MST)</h2>\n<blockquote>\n<p>一个有 $n$ 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有$ n$ 个结点,并且有保持图连通的最少的边。即在一给定的无向图$G = (V, E)$ 中,$(u, v)$ 代表连接顶点 $u $与顶点 $v$ 的边,而 $w(u, v)$ 代表此边的权重,若存在 $T$ 为 $E$ 的子集且为无循环图,使得 $w(T)$ 最小,则此 $T$ 为 $G$ 的最小生成树。</p>\n</blockquote>\n<p>对于最小生成树问题,考虑无向图$G=(V,E)$,我们可以定义$M=(S,L)$:</p>\n<ul>\n<li>$S$是边集$E$。</li>\n<li>$L={X|X\\subseteq E$ 且 $x$组成的图无环$}$</li>\n</ul>\n<p>这个$M$显然满足拟阵的前两个条件。</p>\n<p>根据$L$的定义,对$∀x∈L$,$∀y⊆x$,假设$y$形成环,则$x$形成环,矛盾,所以$y$不形成环,所以$y∈L$,因此$M$满足遗传性。</p>\n<p>考虑$\\forall A \\in L$,$B\\in L$,$|A|<|B|$,我们将$A$组成的森林命名为$GA$,$B$组成的森林命名为$GB$。$GA$有$|V|-|A|$个连通分量,$GB$有$|V|-|B|$个连通分量。$|A|<|B|$,所以$|V|-|B|<|V|-|A|$,所以$GB$中存在的一个连通分量$T$,$T$中的点在$GA$中不连通。那么$T$中必然存在一条边$x$连接$GA$中不同的连通分量的边,显然$x\\notin A$且$x\\in B$,且$A\\cup {x}$无环,即$A\\cup{x}\\in L$。所以M满足交换性。因此M是一个拟阵。</p>\n<p>因为M是一个拟阵,故可以用贪心算法解决它的子集优化问题。</p>\n<h2>补充:拟阵的秩</h2>\n<p>极大独立集的基数被定义为它所包含的元素个数。</p>\n<p>对于拟阵$M=(S,L)$,$L$中的极大独立集称为<strong>拟阵的基</strong>。</p>\n<p>用$\\beta$表示基的集合,则$\\beta$非空且它满足<strong>基交换性</strong>,即若$A,B\\in\\beta,A\\neq B,a\\in A\\setminus B$,则$\\exists b\\in B\\setminus A$使得$A-{a}+{b}\\in\\beta$。<br>\n基交换性得每个基大小相同。</p>\n<p><strong>拟阵的秩</strong>就是基的大小。</p>\n<p>子集$A$的秩通过<strong>秩函数</strong>$r(A)$定义,它有以下几种性质:</p>\n<ul>\n<li>秩函数的值总是非负的</li>\n<li>对于任意$E$的子集$A$,有$r(A) \\leq|A|$</li>\n<li>对于$E$的任意两个子集$A$ 和$B$ ,有$r(A \\cup B)+r(A \\cap B) \\leq r(A)+r(B)$,这意味着秩函数是一个子模函数(见补充:子模函数)</li>\n<li>对于任意集合A和元素x,有$r(A) \\leq r(A \\cup{x}) \\leq r(A)+1$</li>\n</ul>\n<p>子集A的元素个与其秩的差$|A|-r(A)$叫作<strong>子集$A$的零化度或补秩</strong>,它是从A中移除元素使得A成为独立集的最小移除数量。</p>\n<p>基础集合$S$在拟阵$M$上的零化度叫做<strong>拟阵$M$的零化度或$M$的补秩</strong>。</p>\n<h2>补充:次模函数</h2>\n<p>次模(submodular)函数:又称“子模函数”或“亚模函数”,次模函数具有次模性(submodularity),它是经济学上边际效益递减(property of diminishing returns)现象的形式化描述(若对于连续函数,单调递增时,二阶导数递减)。</p>\n<p>:::tip</p>\n<p>次模函数定义:</p>\n<p>给定一个集合函数$f:2^V→\\R$ ,其将有限集V的一个子集$S\\subseteq V$映射为一个实数。</p>\n<p>如果对于任意S,满足:</p>\n<p>$f(S\\cup T)+f(S\\cap T)\\leq f(S)+f(T)$</p>\n<p>则称$f(\\cdot)$是次模函数。</p>\n<p>:::</p>\n<p>从边际效益递减的角度考虑,次模函数还有一种等价定义</p>\n<p>:::tip</p>\n<p>次模函数定义:</p>\n<p>从边际效益递减的角度考虑,次模函数还有一种等价定义:对任意的$R\\subseteq S \\subseteq V$,并且$s\\in V\\setminus S$,</p>\n<p>$f(S\\cup {s})-f(S)\\leq f(R\\cup {s})-f(R)$</p>\n<p>:::</p>\n<p>上式指出,当集合越来越大,s的“价值”将越来越小,正是边际效益递减的特性。这个现象在自然界普遍存在,例如:香农熵函数就是随机变量集合上的次模函数。当$S\\subseteq T$时有$f(S)\\leq f(T)$,则称该次模函数是单调的(monotone)。<br>\n更进一步,次模性是convexity(凸性)的离散模拟。由于convexity使得连续函数更容易最优化,因而次模性在组合优化中重要作用。当目标函数是次模函数时,许多组合优化问题能够在多项式时间内得到最优解或近似解。次模函数最大化被证明是一个NP-hard问题,幸运的是,存在高效并且解的质量有保证的近似算法。一个流行的结果是:最大化一个单调非负的带基数约束(cardinality constraint,即对子集S大小的约束)的次模函数,贪心算法至少能够达到 $(1-1/e)f(S_{opt})$的结果,其中$f(S_{opt})$表示问题的最优解,1-1/e大约是0.63。</p>\n<p>$f(S_{app})\\geq (1-\\frac{1}{e})f(S_{opt})$</p>\n<h2>References</h2>\n<p>基于次模函数最大化的方法: <a href=\"https://blog.csdn.net/hohaizx/article/details/82936743\">https://blog.csdn.net/hohaizx/article/details/82936743</a></p>\n<p>浅谈子集系统、拟阵与贪心:<a href=\"https://blog.csdn.net/MaverickFW/article/details/78207719\">https://blog.csdn.net/MaverickFW/article/details/78207719</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20191103/",
"title": "拟阵与贪心算法",
"summary": "何为拟阵及其与贪心算法的关系",
"date_modified": "2019-11-03T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>Lyapunov在探讨队列稳定性时的应用</h1>\n<p>[[toc]]</p>\n<h2>虚拟队列</h2>\n<p>最近读到虚拟队列(virtual queue)技术,可以用在涉及公平性问题的模型中(如各种涉及调度、分配的问题)。通过创建一组虚拟队列,分配给每个实体$i$一个队列$q_i$来处理公平限制条件。如果用$L_i(t)$来表示队列$q_i$在$t$时刻的长度,也可以反映出调度/分配算法对实体$i$的负债(debt)。这里的思想很朴素:如果一直未对实体$i$进行调度/分配,那么相当于是亏欠了该实体,应该在之后调度/分配时给予关照:yum:。</p>\n<p>他的数学形式是<br>\n$Q_i(t) = [Q_i(t - 1) + a_i(t-1) - b_i(t - 1)]+$,<br>\n其中 $[x]^+ = \\max{x,0}$,$a_i$是确保公平的条件,是某个实体至少需要选择的比例(在0到1之间),$b_i$是是否选择了该实体(选择为1,未选择为0)。从队列的角度看,a_i是到达队列中的项,等待被服务,$b_i$是被服务,离开队列的项。 一般在初始时刻设置队列为空,即$Q_i(0) = 0$。如果每轮未选择某个实体,负债会累加。如果算法在每次分配时只是选择队列最长的(负债最高的),那么高负债的就会被优先处理。</p>\n<p>当队列长度稳定时候,虚拟队列稳定,公平调度/分配的目的达到了。</p>\n<h2>稳定性</h2>\n<p>何为稳定性?</p>\n<p>:::tip<br>\n设系统初于某一个起始的平衡状态,在外作用下它离开了平衡状态,当外作用消失,弱经过较长的时间它能恢复到原来的平衡状态,则称系统是稳定的,或称系统具有稳定性。否则是不稳定的。<br>\n:::</p>\n<p>Lyapunov对稳定性做出了严格的数学定义(Lyapunov总共定义了四种):</p>\n<p>点集$S(\\epsilon)$表示以$X_e$为中心,$\\epsilon$为半径的超球体,若$X\\in S(\\epsilon)$,则$|X-X_e|\\le \\epsilon$,当$\\epsilon$很小,则称S(\\epsilon)为$X_e$的邻域(也就是在空间内,稳定状态X_e附近的球形空间内的状态)。</p>\n<p>系统的齐次状态方程是$\\dot{X} = f(X,t)$(\\dot{X}是对$X$求导),$f$是与$X$同纬的向量函数,一般为时变的非线性函数,若不显含$t$,则为定常非线性系统(就是说不含有时间项$t$的是常微分方程)。假设方程在初始条件$(t_0,X_0)$下,有唯一解$X=\\Phi(t;X_0,t_0)$。</p>\n<p>首先是Lyapunov意义下的稳定:</p>\n<p>对系统$\\dot{X} = f(X,t)$的某一平衡状态X_e,对任意选定的实数$\\epsilon>0$,都对应存在实数$\\delta(\\epsilon,t_0)>0$,使当$|X-X_e|\\le \\delta(\\epsilon,t_0)>0$时,从任意初态X_0出发的解都满足$|\\Phi(t;X_0,t_0)-X_e|<\\epsilon$,$\\forall t>t_0$,则称平衡状态X_e是Lyapunov意义下稳定的(就是说初始状态不偏离平衡位置很远的时候,之后都不会过于偏离平衡位置)。其中实数$\\delta$与$\\epsilon$有关,一般也与$t_0$有关。</p>\n<p>其次是渐进稳定:</p>\n<p>当$t$无限增长时,轨迹$X(t)=\\Phi(t;X_0,t_0)$不仅不超出$S(\\epsilon)$,而且最终收敛于$X_e$,则称这种平衡状态$X_e$是渐进稳定的,即$\\lim\\limits_{t\\to \\infty}|\\Phi(t;X_0,t_0)-X_e|=0$。</p>\n<p>我们在分析队列长度时,只需要保证渐进稳定就可以了,没必要约束到每时每刻(Lyapunov意义下的稳定)。</p>\n<p>除此之外,他还提出了Lyapunov第一法和Lyapunov第二法,第一法通过求解系统微分方程,根据解的性质分析稳定性;第二法构造标量Lyapunov函数,研究它的正定性直接判系统的稳定性。一般提到的Lyapunov方法是Lyapunov第二法。</p>\n<h2>Lyapunov趋势定理</h2>\n<p>随机排队网络模型(queueing network)的稳定性或是最优控制常使用的工具是Lyapunov 趋势(drift)。</p>\n<p>沿用之前的队列长度记号$Q(t)=\\left(Q_{1}(t), Q_{2}(t), \\ldots, Q_{N}(t)\\right)$,定义一个平方Lyapunov函数(Quadratic Lyapunov functions)L,用来表示当前积压的工作(backlogs),也即之前提到的负债(debt):</p>\n<p>$L(t)=\\frac{1}{2} \\sum_{i=1}^{N} Q_{i}(t)^{2}$</p>\n<p>函数L的输出显然是一个标量,定义Lyapunov趋势为:</p>\n<p>$\\Delta(t)=L(t+1)-L(t)$</p>\n<p>假设队列长度按之前描述的方式增长($Q_i(t+1) = [Q_i(t) + a_i(t) - b_i(t)]+$),那么显然有</p>\n<p>$Q_{i}(t+1)^{2}=\\max \\left[Q_{i}(t)+a_{i}(t)-b_{i}(t), 0\\right]^{2} \\leq\\left(Q_{i}(t)+a_{i}(t)-b_{i}(t)\\right)^{2}$</p>\n<p>经过移项得</p>\n<p>$\\Delta(t) \\leq B(t)+\\sum_{i=1}^{N} Q_{i}(t)\\left(a_{i}(t)-b_{i}(t)\\right)$,</p>\n<p>其中$B(t)$是</p>\n<p>$B(t)=\\frac{1}{2} \\sum_{i=1}^{N}\\left[a_{i}(t)^{2}+b_{i}(t)^{2}-2 a_{i}(t) b_{i}(t)\\right]$</p>\n<p>显然$B(t)$有界</p>\n<p>$E[B(t) | Q(t)] \\leq B$</p>\n<p>于是对Lyapunov 趋势取期望,为</p>\n<p>$E[\\Delta(t) | Q(t)] \\leq B+\\sum_{i=1}^{N} Q_{i}(t) E\\left[a_{i}(t)-b_{i}(t) | Q(t)\\right]$</p>\n<p>:::tip<br>\n<strong>Lyapunov 趋势定理</strong>:如果对$a_i(t)$,$b_i(t)$ ,$\\exists \\epsilon$<br>\n$E\\left[a_{i}(t)-b_{i}(t) | Q(t)\\right] \\leq-\\epsilon$, $\\forall i,t$,也即如果$E[\\Delta(t) | Q(t)] \\leq B-\\epsilon \\sum_{i=1}^{N} Q_{i}(t)$,<br>\n那么</p>\n<p>$\\frac{1}{t} \\sum_{\\tau=0}^{t-1} \\sum_{i=1}^{N} E\\left[Q_{i}(\\tau)\\right] \\leq \\frac{B}{\\epsilon}+\\frac{E[L(0)]}{\\epsilon t}$, $\\forall t>0$</p>\n<p>队列稳定<br>\n:::</p>\n<p>Lyapunov趋势定理的证明:在<br>\n$E[\\Delta(t) | Q(t)] \\leq B-\\epsilon \\sum_{i=1}^{N} Q_{i}(t)$两侧取期望,得到</p>\n<p>$E[\\Delta(t)] \\leq B-\\epsilon \\sum_{i=1}^{N} E\\left[Q_{i}(t)\\right]$,对$\\tau \\in{0,1, \\ldots, t-1}$累加求和该式子,得到</p>\n<p>$E[L(t)]-E[L(0)] \\leq B t-\\epsilon \\sum_{\\tau=0}^{t-1} \\sum_{i=1}^{N} E\\left[Q_{i}(\\tau)\\right]$</p>\n<p>注意到$E[L(t)]$非负,移项后可证。</p>\n<h2>Lyapunov优化</h2>\n<p>同样考虑之前提到的随机排队网络模型,定义$p(t)$为t时刻的网络惩罚项(network penalty)。假设目标是稳定队列的同时最小化p(t)对时间的均值(当需要最大化r(t)的时候,可以定义为p(t)=-r(t))。</p>\n<p>为了达到这个目标,算法可以被设计为最小化下面这个bound(drift-plus-penalty expression):</p>\n<p>$\\Delta(t)+V p(t)$,这里$V$是一个非负的权重,用来做队列稳定和优化目标的tradeoff。不妨假设$p(t)$存在下界$p_{\\min}$,即$p(t) \\geq p_{\\min } \\forall t \\in{0,1,2, \\ldots}$。</p>\n<p>:::tip<br>\n<strong>Lyapunov Optimization定理</strong>:<br>\n如果$B \\geq 0, \\epsilon>0, V \\geq 0, p^{<em>}$,$\\forall t$,也即$E[\\Delta(t)+V p(t) | Q(t)] \\leq B+V p^{</em>}-\\epsilon \\sum_{i=1}^{N} Q_{i}(t)$</p>\n<p>那么$\\forall t>0$</p>\n<p>$\\frac{1}{t} \\sum_{\\tau=0}^{t-1} E[p(\\tau)] \\leq p^{*}+\\frac{B}{V}+\\frac{E[L(0)]}{V t}$</p>\n<p>$\\frac{1}{t} \\sum_{\\tau=0}^{t-1} \\sum_{i=1}^{N} E\\left[Q_{i}(\\tau)\\right] \\leq \\frac{B+V\\left(p^{*}-p_{\\text {min}}\\right)}{\\epsilon}+\\frac{E[L(0)]}{\\epsilon t}$<br>\n:::</p>\n<p>证明方法与上面的类似,对条件取期望后,对不同$t$时刻的式子累加得证。</p>\n<h2>References</h2>\n<p>虚拟队列:Stochastic network optimization with application to communication and queueing systems</p>\n<p>队列稳定性:Combinatorial Sleeping Bandits with Fairness Constraints</p>\n<p>Lyapunov优化:<a href=\"http://en.wikipedia.org/wiki/Lyapunov_optimization\">http://en.wikipedia.org/wiki/Lyapunov_optimization</a></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20191102/",
"title": "Lyapunov函数在探讨队列稳定性时的应用",
"summary": "虚拟队列方法及利用Lyapunov函数证明队列稳定性",
"date_modified": "2019-11-02T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>使用Alias命令接收参数</h1>\n<p>因为实习的缘故,系统得学习了一下shell命令,在练习时,因为每次新建脚本后需要添加执行权限还要用到</p>\n<pre><code class=\"language-shell\">touch xxx.sh\nchmod +x xxx.sh # chmod 777 xxx.sh\n</code></pre>\n<p>比较麻烦,所以想把上面的命令别名成</p>\n<pre><code class=\"language-shell\">alias touchs="touch $1;chmod +x $1;"\n</code></pre>\n<p>但是执行后,再次执行<code>alias</code>查看更改,发现变成了</p>\n<pre><code class=\"language-shell\">alias touchs='touch ;chmod +x ;'\n</code></pre>\n<p>以为是双引号的缘故(双引号字符串进行转义且转换参数),换成了</p>\n<pre><code class=\"language-shell\">alias touchs='touch $1;chmod +x $1;'\n</code></pre>\n<p>但是还是不对,查阅后发现需要使用定义函数的方式曲线救国,这样执行定义的<code>touchs</code>的时候就是在执行一个函数了,那么参数就被传到了函数中(注意函数的参数也是从<code>$1</code>开始的,<code>$0</code>是函数名。</p>\n<p>所以正确的方法应该是</p>\n<pre><code class=\"language-shell\">alias touchs='touch_script(){ touch $1;chmod +x $1;};touch_script'\n</code></pre>\n<p>:::tip</p>\n<p>永久更改需要在<code>~/.bashrc</code>中添加上面的语句。</p>\n<p>:::</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20190919/",
"title": "使用Alias命令接收参数",
"summary": "通过在定义函数使得alias命令接收参数",
"date_modified": "2019-09-19T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>压缩感知小结</h1>\n<p>最近在写学校要求的总结,顺便整理了一下之前看过的压缩感知的内容。</p>\n<p>压缩感知是2006年由陶哲轩等人于文章中共同提出的,文中首先提出了有限等距性质(RIP)。之后RIP作为理论基础被广泛引用于各类压缩感知文献,是信号处理领域一个重大的发现。压缩感知中的采样模型为$y=\\Theta x$。其中$s$是$k$稀疏的信号(即向量$s$中有$k$个非零分量),$y$是观测到的结果,$\\Theta$是重建矩阵。但是一般来说,我们能够拿到的数据x都不是稀疏的结果,所以往往就需要使用某些变换域下的稀疏基,作为基底,将原始的数据x转换为稀疏基底上的稀疏系数s。那么常用的稀疏基底就是傅里叶基、小波基,分别对应傅里叶变换和小波变换。近年来也有一些稀疏基方面的研究,比如curvelet、shearlet等,这些基底基于小波变换,稀疏性质要优于小波变换。假设我们选定了变换域之后对数据x进行了变换,由于这些变换都是线性的,那么可以写作$x=\\Psi s$。而之前的采样模型可以描述为由采样矩阵构建$\\Phi$的$y=\\Phi x=\\Phi\\Psi s$,于是有重建矩阵$\\Theta=\\Phi\\Psi$。</p>\n<p>根据信号处理中著名的“香农-奈奎斯特采样定理”,在将一个模拟信号转换为数字信号时,需要至少以模拟信号中最高频率的两倍的采样频率进行采样,才能保证采样后的数字信号保存了模拟信号中的信息。但是根据“有限等距性质”,对于一个$k$稀疏的向量$s$,只要在采样时,重建矩阵$\\Theta$满足下式($k$阶RIP):</p>\n<p>$1-\\delta\\leq\\frac{|\\Theta u|_2}{|u|_2}\\leq1+\\delta$,</p>\n<p>即可从采样结果$\\Theta s$中重建出信号$s$,其中$u$是任意的$2k$稀疏向量,$\\delta$是一个大于$0$的常数,$\\delta$要尽可能得小。该性质是充分的,但是它很难被验证,因为u需要是任意的k稀疏信号且$\\delta$的值难以评估。有另外一种描述方式是使用矩阵的spark常数来评估重建矩阵$\\Theta$的性质,只要矩阵$\\Theta$的spark常数大于$2k$,那么$k$稀疏的信号$s$可以由采样结果$\\Theta s$恢复出来。这里的spark常数与矩阵的秩类似。众所周知,矩阵的秩是指矩阵中最大的不线性相关的行(列)数,spark常数则是矩阵的最小线性相关行(列)数,对于重建矩阵$Theta$,我们考虑的是列方向上的相关性。例如矩阵</p>\n<p>$\\begin{matrix}<br>\n1 & 0 & 2 \\<br>\n0 & 3 & 0 \\<br>\n0 & 0 & 0<br>\n\\end{matrix}$</p>\n<p>的spark常数就是2,而矩阵</p>\n<p>$\\begin{matrix}<br>\n1 & 0 & 2 \\<br>\n0 & 3 & 1 \\<br>\n0 & 0 & 0<br>\n\\end{matrix}$</p>\n<p>的spark常数则是3。</p>\n<p>对于RIP,不加证明得直观来看,是要求在采样后仍然能保持原有数据的距离信息,试想对于一个稀疏的信号,如果在采样时没有采样到关键的分量(绝对值大的分量),那么自然是无法满足RIP性质的,因此RIP其实是保证了设计出的重建矩阵要捕捉到关键信息。RIP和spark常数其实是十分接近的。可能我们都会注意到,在用spark常数的评估重建矩阵的性质时,针对的对象是$k$稀疏的信号,但是spark却要求是$2k$的。这是因为对于任意两个$k$稀疏的信号,他们作差后得到的信号自然最差是一个$2k$稀疏的信号。而在观测信号时,增加一个与之前的观测线性相关的新观测是没有意义的,因为这个新的观测结果本身就可以从之前的观测中线性组合出来,根本无法得到额外的信息。恰好$\\Theta$的spark常数大于$2k$,意味着经过采样后,两个$k$稀疏的信号的差距(距离)被保持了,也就是说是k稀疏的信号经过采样后仍然是可以区分的。</p>\n<p>虽然有了RIP和spark常数这两个性质来评估重建矩阵,采样矩阵$\\Phi$仍然很难设计,所幸大量实践证明,随机的采样矩阵的性质已经足够优秀[3]。事实上在采样设备的物理实现中,常常使用的设计也是由伯努利分布生成的0/1矩阵。这样的0/1矩阵中,$0$代表未观测到的信息,$1$表示进行了观测。整个问题的数学模型和理论基础至此介绍完毕。</p>\n<p>其实这类问题有一些通用的解法,在这里做简要总结。首先是基于凸优化的方法,构建一个损失函数作为优化的目标。损失函数中包含一个重建误差的损失项和先验条件的损失项。先验条件可以是在变换域上的稀疏性假设,也可以是由图像性质衍生出的限制如图像各个局部的相关性等。通过先验条件来限定解空间的范围,在受限的解空间内求得最优解。还有一类方法是构建子空间的贪心算法,如正交匹配追踪(OMP)算法,每次选取重建矩阵中一个与残差内积最大的列,OMP认为该列的观测与剩余的列的观测相比,包含了最多的信息,之后使用最小二乘法和选取出的列组成的新重建矩阵来更新对稀疏系数和残差的估计,算法在残差降低到一定阈值后停止。但是这类贪心法的误差往往较大,精确恢复所需的观察数量很大。也有的算法假设图像各块生成于高斯混合模型,通过拟合生成式的高斯混合模型生成出图像。值得关注的是,最近深度学习的技术也被广泛应用到了压缩感知中:ISTA-Net通过将更新过程展开至一个深度神经网络,获得了非常好的重建效果(基于稀疏表示的假设,在每个phase中用卷积找到稀疏表示,由于主要使用卷积,模型也很小);Tensor ADMM-Net则将Tensor的低秩(使用tensor的核范数)和ADMM框架整合到深度神经网络中,同样效果显著。这两者都属于探索可解释的网络的例子,将数值更新算法展开成了网络。$\\lambda$-Net只做了超光谱的实验,是基于U-Net端到端的实现。</p>\n<p>有一篇很好的文章,写Plug and Play ADMM的框架下重建算法的收敛性证明。事实上,Plug and Play ADMM框架恰好就是上面第一类优化方法的框架。这些方法都可以视作是一个损失函数梯度方向上的有限的更新加上一个受限的降噪过程(文中定义了bounded gradient和bounded denoiser两个概念)。更进一步,我们甚至不需要写出一个具体的损失函数,也不需要一个具体的数学推导过程,也不需要将先验条件形式化。只要选用一个更新方法,可以是ADMM框架,可以是ISTA/FISTA算法,可以是Alternating Projection的方式,更新后再选用一个或一些降噪处理的方法,组合起来进行迭代就可以重建出图像了。对于类似的问题,该方案也是通用的。</p>\n<p>综述文章:M. Rani, S. B. Dhok, and R. B. Deshmukh, “A systematic review of compressive sensing: Concepts, implementations and applications,” IEEE Access, vol. 6, pp. 4875–4894, 01 2018.</p>\n<p>优化角度比较通用的Plug-and-Play ADMM框架及其收敛性证明:S. H. Chan,X. Wang and O. A. Elgendy, “Plug-and-Play ADMM for Image Restoration: Fixed Point Convergence and Applications”, arXiv, 2016.</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20190908/",
"title": "压缩感知小结",
"summary": "知识总结和一小部分心得",
"date_modified": "2019-09-08T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<p>[[toc]]</p>\n<p>转自 <a href=\"https://www.cyberciti.biz/faq/howto-use-linux-unix-man-pages/\">Linux / UNIX: Getting help with man pages and how to use them</a></p>\n<p>在读一些文档的时候看到如 <a href=\"https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html\">cmake-commands(7)</a>,<a href=\"https://man7.org/linux/man-pages/man2/bpf.2.html\">bpf(2)</a>,不知道数字是啥意思……查阅后原来是 man 的章节分类,记录一下:</p>\n<ul>\n<li>Section # 1 : User command (executable programs or shell commands)</li>\n<li>Section # 2 : System calls (functions provided by the kernel)</li>\n<li>Section # 3 : Library calls (functions within program libraries)</li>\n<li>Section # 4 : Special files (usually found in /dev)</li>\n<li>Section # 5 : File formats and conventions eg /etc/passwd</li>\n<li>Section # 6 : Games</li>\n<li>Section # 7 : Miscellaneous (including macro packages and conventions),</li>\n<li>Section # 8 : System administration commands (usually only for root)</li>\n<li>Section # 9 : Kernel routines [Non standard]</li>\n</ul>\n<p>所以,<code>useradd(8)</code> 指代的就来自 sys admin section # 8 的 user add 命令的文档。</p>\n<h1>原文节选:怎样科学阅读 Linux 下的 man pages</h1>\n<h2>man 命令</h2>\n<p>man 命令可以用来打印 man (manual) pages。</p>\n<p>man 命令格式:</p>\n<pre><code class=\"language-bash\">man {command-name}\nman {section} {command-name}\n</code></pre>\n<p>例如,查看 clear 命令的帮助页面:<code>man clear</code>。</p>\n<p>查看特定章节:<code>man 5 passwd</code>。</p>\n<h3>查询某个命令的帮助页面</h3>\n<pre><code class=\"language-bash\">$ man -f printf\n</code></pre>\n<p>示例输出:</p>\n<pre><code>printf (1) - format and print data\nprintf (3) - formatted output conversion\n</code></pre>\n<p>这等价于</p>\n<pre><code class=\"language-bash\">$ whatis -r printf\n</code></pre>\n<h3>在 man page 中检索关键字的例子</h3>\n<p>找到所有带有该关键字的命令的帮助内容。</p>\n<pre><code class=\"language-bash\">$ man -k passwd\n$ man -k printf\n</code></pre>\n<p>等价于</p>\n<pre><code class=\"language-bash\">$ apropos printf\n$ apropos passwd\n</code></pre>\n<h3>打开所有匹配到的 man pages</h3>\n<p>类似 <code>man -k</code> ,但是这次会直接打开它们的详情页,可以按 <code>[Enter]/[CTRL+D]</code>键跳过。</p>\n<h2>Info</h2>\n<p>也可以通过 <code>info</code> 命令查看文档,有时比 man 提供的内容更加丰富,例如:</p>\n<pre><code class=\"language-bash\">$ man date\n$ info date\n</code></pre>\n<p>Info 页面的命令</p>\n<ul>\n<li>q – 退出 info 页面</li>\n<li>n – 下一章</li>\n<li>p – 上一章</li>\n<li>u – 上一层</li>\n</ul>\n<h2>/usr/share/doc</h2>\n<p><code>/usr/share/doc</code> 下也有一些有趣的帮助文档。</p>\n<p>除了本文外,该系列还有几则安装和使用 Man 的教程:</p>\n<ul>\n<li>How to add/install man pages in Alpine Linux</li>\n<li>How to install man pages on a CentOS Linux 6/7</li>\n<li>Unix / Linux: Display Color Man Pages</li>\n<li>HowTo: Linux / UNIX Create a Manpage</li>\n<li>Ubuntu Linux install man pages</li>\n</ul>\n",
"url": "https://forsworns.github.io///zh/blogs/20190901/",
"title": "Man 命令查出来的数字是啥意思",
"summary": "Unix Man 相关命令",
"date_modified": "2019-09-01T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>Anaconda中的cudnn版本问题</h1>\n<p>[[toc]]</p>\n<h2>错误信息</h2>\n<p>今天想更新一下win10电脑上的tensorflow和keras,结果就收到了报错:</p>\n<pre><code class=\"language-shell\">Loaded runtime CuDNN library: 7.1.4 but source was compiled with: 7.4.1. \nCuDNN library major and minor version needs to match or have higher minor version in case of CuDNN 7.0 or later version.\nIf using a binary install, upgrade your CuDNN library. \nIf building from sources, make sure the library loaded at runtime is compatible with the version specified during compile configuration.\n</code></pre>\n<p>很自然得升级了cudnn,但是错误没有解决,折腾了好久反复升降级</p>\n<p><img src=\"./angry.jpg\" alt=\"\"></p>\n<h2>逃避虽可耻但有用</h2>\n<p>最后在崩溃边缘发现了原来是一直在使用Anaconda中的cudnn,目录在<code>\\anaconda\\pkgs\\cudnn-7.1.4-cuda9.0_0\\Library</code>或<code>\\anaconda\\envs\\xxx\\Library</code>,但是anaconda那里又没有办法拿到7.4.1的<code>cudnn</code>更新……遂选择了暂时放弃,回退<code>tensorflow-gpu 1.10</code>。</p>\n<h2>一个完美的解决方案</h2>\n<p>发现了一篇很好的<a href=\"https://blog.csdn.net/Tilamy/article/details/88616201\">博客</a>,按照博主的做法,我成功更新了环境。为扩散和防止链接失效,这里重述一下:</p>\n<p>博主提到可以到<a href=\"https://anaconda.org/anaconda/cudnn/files\">Anaconda官网</a>那里去下载所需的版本然后手动对上面提到的文件夹内容进行覆盖。但是Anaconda并没有提供<code>cudnn7.4.1</code> 。我直接试着将从英伟达官网下载的<code>cudnn7.4.1</code>文件(<code>bin\\cudnn64_7.dll</code>,<code>lib\\x64\\cudnn.lib</code>,<code>include\\cudnn.h</code>),覆盖到了<code>\\anaconda\\pkgs\\cudnn-7.1.4-cuda9.0_0\\Library</code>和<code>\\anaconda\\envs\\xxx\\Library</code>下对应文件。这个时候跑通了!:tada:</p>\n<h2>后记</h2>\n<p>本来还好奇我明明下载的是<code>cuda 10.0</code>对应的<code>cudnn 7.4.1</code>,而在<code>Anaconda</code>目录下覆盖的文件夹名称里含有<code>cuda 9.0</code>,竟然还能使用。后来才发现 <code>Anaconda</code>自身提供了9.0和10.0两个版本的<code>cuda</code>……估计根本没有用系统中安装的英伟达套件。</p>\n<p>::: tip<br>\n用<code>conda</code>升级库的时候还是要小心的,不要随便手动指定版本。<br>\n:::</p>\n",
"url": "https://forsworns.github.io///zh/blogs/20190824/",
"title": "Anaconda中的cudnn版本问题",
"summary": "Loaded runtime CuDNN library:7.1.4 but source was compiled with:7.4.1",
"date_modified": "2019-08-24T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
},
{
"content_html": "<h1>第一篇博客</h1>\n<p>今天是7月21号。</p>\n<p>完成了基于VuePress的博客搭建。博客的源码见</p>\n<p><a href=\"https://github.com/Forsworns/blog\">https://github.com/Forsworns/blog</a></p>\n<p>现在的样式还是默认的,改进的空间也太大了。</p>\n<p><img src=\"https://forsworns.github.io//assets/embarrassed.jpg\" alt=\"\"></p>\n",
"url": "https://forsworns.github.io///zh/blogs/20190721/",
"title": "第一篇博客",
"summary": "这是一篇平平无奇的初始博客。",
"date_modified": "2019-07-21T00:00:00.000Z",
"author": {
"name": "Peihao Yang",
"url": "https://forsworns.github.io/"
}
}
]
}