├── README.md ├── cgroups ├── README.md ├── 文档 │ ├── Challenges with the memory resource controller and its performance.pdf │ ├── Linux’s new unified control group system.pdf │ ├── Using Linux Control Groups and Systemd to Manage CPU Time and Memory.pdf │ ├── 5 years of cgroup v2.pdf │ ├── An introduction to Control Groups (cgroups).pdf │ ├── An introduction to cgroups and cgroupspy.pdf │ ├── CgroupMemcgMaster.pdf │ ├── Managing Resources with cgroups.pdf │ ├── README.md │ ├── Resource Management.pdf │ ├── Ressource Management in Linux with Control Groups.pdf │ ├── System Programming for Linux Containers Control Groups (cgroups).pdf │ ├── cgroups_intro.pdf │ ├── cgroups介绍.pdf │ └── red_hat_enterprise_linux-6-resource_management_guide-en-us.pdf └── 文章 │ ├── Cgroup - Linux内存资源管理.md │ ├── Cgroup - Linux的IO资源隔离.md │ ├── Cgroup - Linux的网络资源隔离.md │ ├── Cgroup - 从CPU资源隔离说起.md │ ├── Cgroups控制cpu,内存,io示例.md │ ├── How I Used CGroups to Manage System Resources.md │ ├── Linux Control Groups V1 和 V2 原理和区别.md │ ├── Linux资源管理之cgroups简介.md │ ├── README.md │ ├── [译] Control Group v2(cgroupv2 权威指南)(KernelDoc, 2021).md │ ├── cgroups 管理进程磁盘 io.md │ ├── linux cgroups 概述.md │ ├── 彻底搞懂容器技术的基石: cgroup.md │ ├── 深入理解 Linux Cgroup 系列(一):基本概念.md │ ├── 深入理解 Linux Cgroup 系列(三):内存.md │ ├── 深入理解 Linux Cgroup 系列(二):玩转 CPU.md │ ├── 用 cgroups 管理 cpu 资源.md │ └── 用 cgruops 管理进程内存占用.md ├── ebpf ├── 文档 │ ├── Advanced_BPF_Kernel_Features_for_the_Container_Age_FOSDEM.pdf │ ├── BPF to eBPF.pdf │ ├── Calico-eBPF-Dataplane-CNCF-Webinar-Slides.pdf │ ├── Combining System Visibility and Security Using eBPF.pdf │ ├── DPDK+eBPF.pdf │ ├── Experience and Lessons Learned.pdf │ ├── Fast Packet Processing using eBPF and XDP.pdf │ ├── Kernel Tracing With eBPF.pdf │ ├── Kernel analysis using eBPF.pdf │ ├── Making the Linux TCP stack more extensible with eBPF.pdf │ ├── Performance Analysis Superpowers with Linux eBPF.pdf │ ├── Performance Implications of Packet Filtering with Linux eBPF.pdf │ ├── README.md │ ├── The Next Linux Superpower eBPF Primer.pdf │ ├── eBPF - From a Programmer’s Perspective.pdf │ ├── eBPF In-kernel Virtual Machine & Cloud Computin.pdf │ ├── eBPF for perfomance analysis and networking.pdf │ ├── eBPF in CPU Scheduler.pdf │ └── eBPF-based Content and Computation-aware Communication for Real-time Edge Computing.pdf └── 文章 │ ├── BPF 和 eBPF 初探.md │ ├── Linux 内核监测技术 eBPF.md │ ├── README.md │ ├── eBPF 如何简化服务网格.md │ ├── eBPF 概述,第 1 部分:介绍.md │ ├── eBPF 概述,第 2 部分:机器和字节码.md │ ├── eBPF 概述,第 3 部分:软件开发生态.md │ ├── eBPF 概述,第 4 部分:在嵌入式系统运行.md │ ├── eBPF 概述,第 5 部分:跟踪用户进程.md │ ├── eBPF 用户空间虚拟机实现相关.md │ ├── eBPF详解.md │ ├── 【译]】大规模微服务利器:eBPF + Kubernetes(KubeCon, 2020).md │ ├── 什么是 eBPF.md │ ├── 基于 eBPF 实现容器运行时安全.md │ └── 深入理解 Cilium 的 eBPF 收发包路径.md ├── io_uring.pdf ├── io_uring ├── 文档 │ ├── Boosting Compaction in B-Tree Based Key-Value Store by Exploiting Parallel Reads in Flash SSDs.pdf │ ├── Enabling Financial-Grade Secure Infrastructure with Confidential Computing.pdf │ ├── IO-uring speed the RocksDB & TiKV.pdf │ ├── Improved Storage Performance Using the New Linux Kernel I.O Interface.pdf │ ├── O Stack.pdf │ ├── O is faster than the OS.pdf │ ├── Programming Emerging Storage Interfaces.pdf │ ├── README.md │ ├── StefanMetzmacher_sambaxp2021_multichannel_io-uring-rev0-presentation.pdf │ ├── The Evolution of File Descriptor Monitoring in Linux.pdf │ ├── io_uring-BPF.pdf │ └── io_uring-徐浩-阿里云.pdf ├── 文章 │ ├── Efficient IO with io_uring.md │ ├── Linux 5.1 的 io_uring.md │ ├── Submission Queue Polling.md │ ├── The Low-level io_uring Interface.md │ ├── What is io_uring.md │ ├── [译] Linux 异步 I_O 框架 io_uring:基本原理、程序示例与性能压测(2020).md │ ├── io_uring 系统性整理.md │ ├── io_uring 高效 IO.md │ ├── io_uring_enter.md │ ├── io_uring_register.md │ ├── io_uring_setup.md │ ├── io_uring(1) – 我们为什么会需要 io_uring.md │ ├── io_uring(2)- 从创建必要的文件描述符 fd 开始.md │ ├── 下一代异步 IO io_uring 技术解密.md │ ├── 小谈io_uring.md │ ├── 智汇华云-新时代IO利器-io_uring.md │ └── 浅析开源项目之io_uring.md └── 示例程序-提交队列轮询.c ├── llvm ├── 文档 │ ├── A Compilation Framework for Lifelong Program Analysis & Transformation.pdf │ ├── Introduction to the LLVM Compiler System.pdf │ └── README.md └── 文章 │ ├── LLVM 入门篇.md │ ├── LLVM-Clang入门.md │ ├── LLVM编译器框架介绍.md │ ├── README.md │ ├── llvm编译的基本概念和流程.md │ ├── 后端技术的重用:LLVM不仅仅让你高效.md │ ├── 编译优化|LLVM代码生成技术详解及在数据库中的应用.md │ └── 编译器及底层名词解释.md └── 【80页】eBPF学习笔记.pdf /cgroups/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cgroups/文档/ Challenges with the memory resource controller and its performance.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/ Challenges with the memory resource controller and its performance.pdf -------------------------------------------------------------------------------- /cgroups/文档/ Linux’s new unified control group system.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/ Linux’s new unified control group system.pdf -------------------------------------------------------------------------------- /cgroups/文档/ Using Linux Control Groups and Systemd to Manage CPU Time and Memory.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/ Using Linux Control Groups and Systemd to Manage CPU Time and Memory.pdf -------------------------------------------------------------------------------- /cgroups/文档/5 years of cgroup v2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/5 years of cgroup v2.pdf -------------------------------------------------------------------------------- /cgroups/文档/An introduction to Control Groups (cgroups).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/An introduction to Control Groups (cgroups).pdf -------------------------------------------------------------------------------- /cgroups/文档/An introduction to cgroups and cgroupspy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/An introduction to cgroups and cgroupspy.pdf -------------------------------------------------------------------------------- /cgroups/文档/CgroupMemcgMaster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/CgroupMemcgMaster.pdf -------------------------------------------------------------------------------- /cgroups/文档/Managing Resources with cgroups.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/Managing Resources with cgroups.pdf -------------------------------------------------------------------------------- /cgroups/文档/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cgroups/文档/Resource Management.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/Resource Management.pdf -------------------------------------------------------------------------------- /cgroups/文档/Ressource Management in Linux with Control Groups.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/Ressource Management in Linux with Control Groups.pdf -------------------------------------------------------------------------------- /cgroups/文档/System Programming for Linux Containers Control Groups (cgroups).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/System Programming for Linux Containers Control Groups (cgroups).pdf -------------------------------------------------------------------------------- /cgroups/文档/cgroups_intro.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/cgroups_intro.pdf -------------------------------------------------------------------------------- /cgroups/文档/cgroups介绍.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/cgroups介绍.pdf -------------------------------------------------------------------------------- /cgroups/文档/red_hat_enterprise_linux-6-resource_management_guide-en-us.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/cgroups/文档/red_hat_enterprise_linux-6-resource_management_guide-en-us.pdf -------------------------------------------------------------------------------- /cgroups/文章/Cgroups控制cpu,内存,io示例.md: -------------------------------------------------------------------------------- 1 | # Cgroups控制cpu,内存,io示例 2 | 3 | > Cgroups是control groups的缩写,最初由Google工程师提出,后来编进linux内核。 4 | > 5 | > Cgroups是实现IaaS虚拟化(kvm、lxc等),PaaS容器沙箱(Docker等)的资源管理控制部分的底层基础。 6 | 7 | 百度私有PaaS云就是使用轻量的cgoups做的应用之间的隔离,以下是关于百度架构师许立强,对于虚拟机VM,应用沙盒,cgroups技术选型的理解 8 | 9 | ![img](https://images0.cnblogs.com/i/319578/201405/252326220902394.jpg) 10 | 11 | 本文用脚本运行示例进程,来验证Cgroups关于cpu、内存、io这三部分的隔离效果。 12 | 13 | 测试机器:CentOS release 6.4 (Final) 14 | 15 | 启动Cgroups 16 | 17 | ``` 18 | service cgconfig start #开启cgroups服务 19 | chkconfig cgconfig on #开启启动 20 | ``` 21 | 22 | 在/cgroup,有如下文件夹,默认是多挂载点的形式,即各个子系统的配置在不同的子文件夹下 23 | 24 | ``` 25 | [root@localhost /]# ls /cgroup/ 26 | blkio cpu cpuacct cpuset devices freezer memory net_cls 27 | ``` 28 | 29 | ## cgroups管理进程cpu资源 30 | 31 | 跑一个耗cpu的脚本 32 | 33 | ``` 34 | x=0 35 | while [ True ];do 36 | x=$x+1 37 | done; 38 | ``` 39 | 40 | top可以看到这个脚本基本占了100%的cpu资源 41 | 42 | ``` 43 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 44 | 30142 root 20 0 104m 2520 1024 R 99.7 0.1 14:38.97 sh 45 | ``` 46 | 47 | 下面用cgroups控制这个进程的cpu资源 48 | 49 | ``` 50 | mkdir -p /cgroup/cpu/foo/ #新建一个控制组foo 51 | echo 50000 > /cgroup/cpu/foo/cpu.cfs_quota_us #将cpu.cfs_quota_us设为50000,相对于cpu.cfs_period_us的100000是50% 52 | echo 30142 > /cgroup/cpu/foo/tasks 53 | ``` 54 | 55 | 然后top的实时统计数据如下,cpu占用率将近50%,看来cgroups关于cpu的控制起了效果 56 | 57 | ``` 58 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 30142 root 20 0 105m 2884 1024 R 49.4 0.2 23:32.53 sh 59 | ``` 60 | 61 | cpu控制组foo下面还有其他的控制,还可以做更多其他的关于cpu的控制 62 | 63 | ``` 64 | [root@localhost ~]# ls /cgroup/cpu/foo/ 65 | cgroup.event_control cgroup.procs cpu.cfs_period_us cpu.cfs_quota_us cpu.rt_period_us cpu.rt_runtime_us cpu.shares cpu.stat notify_on_release tasks 66 | ``` 67 | 68 | ## cgroups管理进程内存资源 69 | 70 | 跑一个耗内存的脚本,内存不断增长 71 | 72 | ``` 73 | x="a" 74 | while [ True ];do 75 | x=$x$x 76 | done; 77 | ``` 78 | 79 | top看内存占用稳步上升 80 | 81 | ``` 82 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 30215 root 20 0 871m 501m 1036 R 99.8 26.7 0:38.69 sh 83 | 30215 root 20 0 1639m 721m 1036 R 98.7 38.4 1:03.99 sh 84 | 30215 root 20 0 1639m 929m 1036 R 98.6 49.5 1:13.73 sh 85 | ``` 86 | 87 | 下面用cgroups控制这个进程的内存资源 88 | 89 | ``` 90 | mkdir -p /cgroup/memory/foo 91 | echo 1048576 > /cgroup/memory/foo/memory.limit_in_bytes #分配1MB的内存给这个控制组 92 | echo 30215 > /cgroup/memory/foo/tasks 93 | ``` 94 | 95 | 发现之前的脚本被kill掉 96 | 97 | ``` 98 | [root@localhost ~]# sh /home/test.sh 99 | 已杀死 100 | ``` 101 | 102 | 因为这是强硬的限制内存,当进程试图占用的内存超过了cgroups的限制,会触发out of memory,导致进程被kill掉。 103 | 104 | 实际情况中对进程的内存使用会有一个预估,然后会给这个进程的限制超配50%比如,除非发生内存泄露等异常情况,才会因为cgroups的限制被kill掉。 105 | 106 | 也可以通过配置关掉cgroups oom kill进程,通过memory.oom_control来实现(oom_kill_disable 1),但是尽管进程不会被直接杀死,但进程也进入了休眠状态,无法继续执行,仍让无法服务。 107 | 108 | 关于内存的控制,还有以下配置文件,关于虚拟内存的控制,以及权值比重式的内存控制等 109 | 110 | ``` 111 | [root@localhost /]# ls /cgroup/memory/foo/ 112 | cgroup.event_control memory.force_empty memory.memsw.failcnt memory.memsw.usage_in_bytes memory.soft_limit_in_bytes memory.usage_in_bytes tasks 113 | cgroup.procs memory.limit_in_bytes memory.memsw.limit_in_bytes memory.move_charge_at_immigrate memory.stat memory.use_hierarchy 114 | memory.failcnt memory.max_usage_in_bytes memory.memsw.max_usage_in_bytes memory.oom_control memory.swappiness notify_on_release 115 | ``` 116 | 117 | ## cgroups管理进程io资源 118 | 119 | 跑一个耗io的脚本 120 | 121 | ``` 122 | dd if=/dev/sda of=/dev/null & 123 | ``` 124 | 125 | 通过iotop看io占用情况,磁盘速度到了284M/s 126 | 127 | ``` 128 | 30252 be/4 root 284.71 M/s 0.00 B/s 0.00 % 0.00 % dd if=/dev/sda of=/dev/null 129 | ``` 130 | 131 | 下面用cgroups控制这个进程的io资源 132 | 133 | ``` 134 | mkdir -p /cgroup/blkio/foo 135 | 136 | echo '8:0 1048576' > /cgroup/blkio/foo/blkio.throttle.read_bps_device 137 | #8:0对应主设备号和副设备号,可以通过ls -l /dev/sda查看 138 | echo 30252 > /cgroup/blkio/foo/tasks 139 | ``` 140 | 141 | 再通过iotop看,确实将读速度降到了1M/s 142 | 143 | ``` 144 | 30252 be/4 root 993.36 K/s 0.00 B/s 0.00 % 0.00 % dd if=/dev/sda of=/dev/null 145 | ``` 146 | 147 | 对于io还有很多其他可以控制层面和方式,如下 148 | 149 | ``` 150 | [root@localhost ~]# ls /cgroup/blkio/foo/ 151 | blkio.io_merged blkio.io_serviced blkio.reset_stats blkio.throttle.io_serviced blkio.throttle.write_bps_device blkio.weight cgroup.procs 152 | blkio.io_queued blkio.io_service_time blkio.sectors blkio.throttle.read_bps_device blkio.throttle.write_iops_device blkio.weight_device notify_on_release 153 | blkio.io_service_bytes blkio.io_wait_time blkio.throttle.io_service_bytes blkio.throttle.read_iops_device blkio.time cgroup.event_control tasks 154 | ``` 155 | 156 | > 原文链接:https://www.cnblogs.com/yanghuahui/p/3751826.html 157 | -------------------------------------------------------------------------------- /cgroups/文章/Linux Control Groups V1 和 V2 原理和区别.md: -------------------------------------------------------------------------------- 1 | # Linux Control Groups V1 和 V2 原理和区别 2 | 3 | (备注: 不显示声明就是基于V1版本来讲解的) 4 | 5 | 1. 什么是 `cgroups`. 6 | 2. 为什么我们需要 `cgroups`. 7 | 3. `crgoups` 是如何实现的. 8 | 4. 如何使用 `Cgroups` 9 | 5. `Cgroup` V2 版本有什么不一样 10 | 6. 总结 11 | 12 | ## 什么是 cgroups 13 | 14 | ### cgroup 基本概念 15 | 16 | `cgroups` 机制是用来限制一个进程或者多个进程的资源。 17 | 18 | 概念: 19 | 20 | 1. Subsystem(子系统): 每种可以控制的资源都被定义成一个子系统,比如CPU子系统,Memory子系统。 21 | 2. Control Group: cgroup 是用来对一个 subsystem(子系统)或者多个子系统的资源进行控制。 22 | 3. 层级(Hierarchy): Control group 使用层次结构 (Tree) 对资源做划分。参考下图: 23 | 24 | ![This is an memory_cgroup.png](https://mikechengwei.github.io/2020/06/03/cgroup%E5%8E%9F%E7%90%86/memory_cgroup.png) 25 | 26 | 每个层级都会有一个根节点, 子节点是根节点的比重划分。 27 | 28 | 关系: 29 | 30 | 1. 一个子系统最多附加到一个层级(Hierarchy) 上。 31 | 2. 一个 层级(Hierarchy) 可以附加多个子系统 32 | 33 | ### 进程和Cgroup的关系 34 | 35 | 一个进程限制内存和CPU资源,就会绑定到CPU Cgroup和Memory Cgroup的节点上,Cpu cgroup 节点和Memory cgroup节点 属于两个不同的Hierarchy 层级。进程和 cgroup 节点是多对多的关系,因为一个进程涉及多个子系统,每个子系统可能属于不同的层次结构(Hierarchy)。 36 | 37 | 如图: 38 | 39 | ![This is an 进程和cgroup的关系.png](https://mikechengwei.github.io/2020/06/03/cgroup%E5%8E%9F%E7%90%86/%E8%BF%9B%E7%A8%8B%E5%92%8Ccgroup%E7%9A%84%E5%85%B3%E7%B3%BB.png) 40 | 41 | 上图 P 代表进程,因为多个进程可能共享相同的资源,所以会抽象出一个 `CSS_SET`, 每个进程会属于一个CSS_SET 链表中,同一个 `CSS_SET` 下的进程都被其管理。一个 `CSS_SET` 关联多个 Cgroup节点,也就是关联多个子系统的资源控制,那么 `CSS_SET`和 `Cgroup`节点就是多对多的关系。 42 | 43 | 参考下 `CSS_SET` 结构定义: 44 | 45 | ``` 46 | #ifdef CONFIG_CGROUPS 47 | /* Control Group info protected by css_set_lock */ 48 | struct css_set __rcu *cgroups; 关联的cgroup 节点 49 | /* cg_list protected by css_set_lXock and tsk->alloc_lock */ 50 | struct list_head cg_list; // 关联所有的进程 51 | #endif 52 | ``` 53 | 54 | ## 为什么我们需要 `cgroups` 55 | 56 | 我们希望能够细粒度的控制资源,我们可以为一个系统的不同用户提供不同的资源使用量,比如一个学校的校园服务器,老师用户可以使用15%的资源,学生用户可以使用5%的资源。我们可以用 `cgroups` 机制做到。 57 | 58 | ## crgoups 是如何实现的 59 | 60 | ### cgroups 数据结构 61 | 62 | - 每个进程都会指向一个 `CSS_SET` 数据结构.(上文 进程和cgroups关系已经提供过) 63 | 64 | 参考源码: 65 | 66 | ``` 67 | struct task_struct { //进程的数据结构 68 | ... 69 | #ifdef CONFIG_CGROUPS 70 | /* Control Group info protected by css_set_lock */ 71 | struct css_set __rcu *cgroups; 关联的cgroup 节点 72 | /* cg_list protected by css_set_lXock and tsk->alloc_lock */ 73 | struct list_head cg_list; // 关联所有的进程 74 | #endif 75 | ... 76 | } 77 | ``` 78 | 79 | - 一个 `CSS_SET` 关联多个 `cgroup_subsys_state` 对象,`cgroup_subsys_state` 指向一个 cgroup 子系统。所以进程和 cgroup 是不直接关联的,需要通过 `cgroup_subsys_state` 对象确定属于哪个层级,属于哪个 `Cgroup` 节点。 80 | 81 | 参考下 `CSS_SET`源码: 82 | 83 | 84 | 85 | - 一个 Cgroup Hierarchy(层次)其实是一个文件系统, 可以挂载在用户空间查看和操作。 86 | - 我们可以查看 绑定任何一个cgroup节点下的所有进程Id(PID). 87 | - 实现原理: 通过进程的fork和退出,从 `CSS_SET` attach 或者 detach 进程。 88 | 89 | ### cgroups 文件系统 90 | 91 | 上面我们了解到进程和Cgroup的关系,那么在用户空间内的进程是如何使用 Cgroup功能的呢? 92 | 93 | Cgroup 通过 VFS 文件系统将功能暴露给用户,用户创建一些文件,写入一些参数即可使用,那么用户使用Crgoup功能会创建哪些文件? 94 | 95 | 文件如下: 96 | 97 | - tasks 文件: 列举绑定到某个 cgroup的 所有进程ID(PID). 98 | - cgroup.procs 文件: 列举 一个Cgroup节点下的所有 线程组Id. 99 | - `notify_on_release` flag 文件: :填 0 或 1,表示是否在 cgroup 中最后一个 task 退出时通知运行release agent,默认情况下是 0,表示不运行。 100 | - release_agent 文件: 指定 release agent 执行脚本的文件路径(该文件在最顶层 cgroup 目录中存在),在这个脚本通常用于自动化umount无用的 cgroup 101 | - 每个子系统也会创建一些特有的文件。 102 | 103 | #### 什么是 VFS 文件系统 104 | 105 | VFS 是一个内核抽象层,能够隐藏具体文件系统的实现细节,从而给用户态进程提供一套统一的 API 接口。VFS 使用了一种通用文件系统的设计,具体的文件系统只要实现了 VFS 的设计接口,就能够注册到 VFS 中,从而使内核可以读写这种文件系统。 这很像面向对象设计中的抽象类与子类之间的关系,抽象类负责对外接口的设计,子类负责具体的实现。其实,VFS本身就是用 c 语言实现的一套面向对象的接口。 106 | 107 | #### clone_children flag 是干什么的 108 | 109 | 这个标志只会影响 cpuset 子系统,如果这个标志在 cgroup 中开启,一个新的 cpuset 子系统 cgroup节点 的配置会继承它的父级cgroup节点配置。 110 | 111 | ## 如何使用 Cgroups 112 | 113 | 我们创建一个 `Cgroup`,使用到 “cpuset” cgroup子系统,可以按照下面的步骤: 114 | 115 | 1. mount -t tmpfs cgroup_root /sys/fs/cgroup 116 | 2. mkdir /sys/fs/cgroup/cpuset 117 | 3. mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset 118 | 4. 通过创建和写入新的配置到 `/sys/fs/cgroup/cpuset` 虚拟文件系统,创建新的 cgroup 119 | 5. 启动一个 父进程任务 120 | 6. 得到进程PID,写入到 `/sys/fs/cgroup/cpuset` tasks 文件中 121 | 7. fork,exec 或者 clone 父进程任务。 122 | 123 | 举个例子,我们可以创建一个cgroup名字叫 “Charlie”,包含CPU资源 2到3核,memory 节点为1,操作如下: 124 | 125 | ``` 126 | mount -t tmpfs cgroup_root /sys/fs/cgroup 127 | mkdir /sys/fs/cgroup/cpuset 128 | mount -t cgroup cpuset -ocpuset /sys/fs/cgroup/cpuset 129 | cd /sys/fs/cgroup/cpuset 130 | mkdir Charlie 131 | cd Charlie 132 | /bin/echo 2-3 > cpuset.cpus 133 | /bin/echo 1 > cpuset.mems 134 | /bin/echo $$ > tasks 135 | 136 | ## 查看cgroup信息 137 | sh 138 | # sh 是进入当前cgroup 139 | cat /proc/self/cgroup 140 | ``` 141 | 142 | ## `Cgroup` V2 版本有什么不一样 143 | 144 | 不同于 v1 版本, cgroup v2 版本只有一个层级 Hierarchy(层级). 145 | 146 | cgroup v2 的层级可以通过下面的命令进行挂载: 147 | 148 | ``` 149 | # mount -t cgroup2 none $MOUNT_POINT 150 | ``` 151 | 152 | cgroup2 文件系统有一个根 Cgroup ,以 `0x63677270`数字来标识,所有支持v2版本的子系统控制器会自动绑定到 v2的唯一层级上并绑定到根 Cgroup.没有使用 cgroup v2版本的进程,也可以绑定到 v1版本的层级上,保证了前后版本的兼容性。 153 | 154 | 在V2 版本中,因为只有一个层级,所有进程只绑定到cgroup的叶子节点。 155 | 156 | 如图: 157 | 158 | ![img](https://mikechengwei.github.io/2020/06/03/cgroup%E5%8E%9F%E7%90%86/V2%E5%B1%82%E7%BA%A7.png) 159 | 160 | 节点说明: 161 | 162 | - 父节点开启的子系统控制器控制到儿子节点,比如 A节点开启了memory controller,那么 C节点cgroup就可以控制进程的memory. 163 | - 叶子节点不能控制开启哪些子系统的controller,因为叶子节点关联进程Id.所以非叶子节点不能控制进程的使用资源。 164 | 165 | cgroup v2的cgroup目录下文件说明: 166 | 167 | - `cgroup.procs`文件,用来关联 进程Id。这个文件在V1版本使用列举线程组Id的。 168 | 169 | - cgroup.controllers文件(只读)和cgroup.subtree_control文件 是用来控制 子 Cgroup 节点可以使用的 子系统控制器。 170 | 171 | - tasks文件用来 关联进程信息,只有叶子节点有此文件。 172 | 173 | ### 为什么这么改造? 174 | 175 | v1 版本为了灵活一个进程可能绑定多个层级(Hierarchy),但是通常是每个层级对应一个子系统,多层级就显得没有必要。所以一个层级包含所有的子系统就比较简单容易管理。 176 | 177 | ### 线程模式 178 | 179 | `Cgroup` v2 版本支持线程模式,将 `threaded` 写入到 cgroup.type 就会开启 Thread模式。当开始线程模式后,一个进程的所有线程属于同一个cgroup,会采用Tree结构进行管理。 180 | 181 | ## 总结 182 | 183 | 通过对 Cgroup的学习,大致了解 Linux Crgoup 的数据结构,V2 版本层级结构的优化和 支持线程模式的功能。 184 | 185 | > 原文链接:https://mikechengwei.github.io/2020/06/03/cgroup%E5%8E%9F%E7%90%86/ 186 | -------------------------------------------------------------------------------- /cgroups/文章/Linux资源管理之cgroups简介.md: -------------------------------------------------------------------------------- 1 | ### 引子 2 | 3 | cgroups 是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,可以对 cpu,内存等资源实现精细化的控制,目前越来越火的轻量级容器 Docker 就使用了 cgroups 提供的资源限制能力来完成cpu,内存等部分的资源控制。 4 | 5 | 另外,开发者也可以使用 cgroups 提供的精细化控制能力,限制某一个或者某一组进程的资源使用。比如在一个既部署了前端 web 服务,也部署了后端计算模块的八核服务器上,可以使用 cgroups 限制 web server 仅可以使用其中的六个核,把剩下的两个核留给后端计算模块。 6 | 7 | 本文从以下四个方面描述一下 cgroups 的原理及用法: 8 | 9 | 1. cgroups 的概念及原理 10 | 2. cgroups 文件系统概念及原理 11 | 3. cgroups 使用方法介绍 12 | 4. cgroups 实践中的例子 13 | 14 | ### 概念及原理 15 | 16 | #### cgroups子系统 17 | 18 | cgroups 的全称是control groups,cgroups为每种可以控制的资源定义了一个子系统。典型的子系统介绍如下: 19 | 20 | 1. cpu 子系统,主要限制进程的 cpu 使用率。 21 | 2. cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。 22 | 3. cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。 23 | 4. memory 子系统,可以限制进程的 memory 使用量。 24 | 5. blkio 子系统,可以限制进程的块设备 io。 25 | 6. devices 子系统,可以控制进程能够访问某些设备。 26 | 7. net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。 27 | 8. freezer 子系统,可以挂起或者恢复 cgroups 中的进程。 28 | 9. ns 子系统,可以使不同 cgroups 下面的进程使用不同的 namespace。 29 | 30 | 这里面每一个子系统都需要与内核的其他模块配合来完成资源的控制,比如对 cpu 资源的限制是通过进程调度模块根据 cpu 子系统的配置来完成的;对内存资源的限制则是内存模块根据 memory 子系统的配置来完成的,而对网络数据包的控制则需要 Traffic Control 子系统来配合完成。本文不会讨论内核是如何使用每一个子系统来实现资源的限制,而是重点放在内核是如何把 cgroups 对资源进行限制的配置有效的组织起来的,和内核如何把cgroups 配置和进程进行关联的,以及内核是如何通过 cgroups 文件系统把cgroups的功能暴露给用户态的。 31 | 32 | #### cgroups 层级结构(Hierarchy) 33 | 34 | 内核使用 cgroup 结构体来表示一个 control group 对某一个或者某几个 cgroups 子系统的资源限制。cgroup 结构体可以组织成一颗树的形式,每一棵cgroup 结构体组成的树称之为一个 cgroups 层级结构。cgroups层级结构可以 attach 一个或者几个 cgroups 子系统,当前层级结构可以对其 attach 的 cgroups 子系统进行资源的限制。每一个 cgroups 子系统只能被 attach 到一个 cpu 层级结构中。 35 | 36 | ![cgroups层级结构示意图](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2015/b3270d03.png) 37 | 38 | 比如上图表示两个cgroups层级结构,每一个层级结构中是一颗树形结构,树的每一个节点是一个 cgroup 结构体(比如cpu_cgrp, memory_cgrp)。第一个 cgroups 层级结构 attach 了 cpu 子系统和 cpuacct 子系统, 当前 cgroups 层级结构中的 cgroup 结构体就可以对 cpu 的资源进行限制,并且对进程的 cpu 使用情况进行统计。 第二个 cgroups 层级结构 attach 了 memory 子系统,当前 cgroups 层级结构中的 cgroup 结构体就可以对 memory 的资源进行限制。 39 | 40 | 在每一个 cgroups 层级结构中,每一个节点(cgroup 结构体)可以设置对资源不同的限制权重。比如上图中 cgrp1 组中的进程可以使用60%的 cpu 时间片,而 cgrp2 组中的进程可以使用20%的 cpu 时间片。 41 | 42 | \####cgroups与进程 43 | 44 | 上面的小节提到了内核使用 cgroups 子系统对系统的资源进行限制,也提到了 cgroups 子系统需要 attach 到 cgroups 层级结构中来对进程进行资源控制。本小节重点关注一下内核是如何把进程与 cgroups 层级结构联系起来的。 45 | 46 | 在创建了 cgroups 层级结构中的节点(cgroup 结构体)之后,可以把进程加入到某一个节点的控制任务列表中,一个节点的控制列表中的所有进程都会受到当前节点的资源限制。同时某一个进程也可以被加入到不同的 cgroups 层级结构的节点中,因为不同的 cgroups 层级结构可以负责不同的系统资源。所以说进程和 cgroup 结构体是一个多对多的关系。 47 | 48 | ![cgroups层级结构示意图](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2015/3982f44c.png) 49 | 50 | 上面这个图从整体结构上描述了进程与 cgroups 之间的关系。最下面的`P`代表一个进程。每一个进程的描述符中有一个指针指向了一个辅助数据结构`css_set`(cgroups subsystem set)。 指向某一个`css_set`的进程会被加入到当前`css_set`的进程链表中。一个进程只能隶属于一个`css_set`,一个`css_set`可以包含多个进程,隶属于同一`css_set`的进程受到同一个`css_set`所关联的资源限制。 51 | 52 | 上图中的”M×N Linkage”说明的是`css_set`通过辅助数据结构可以与 cgroups 节点进行多对多的关联。但是 cgroups 的实现不允许`css_set`同时关联同一个cgroups层级结构下多个节点。 这是因为 cgroups 对同一种资源不允许有多个限制配置。 53 | 54 | 一个`css_set`关联多个 cgroups 层级结构的节点时,表明需要对当前`css_set`下的进程进行多种资源的控制。而一个 cgroups 节点关联多个`css_set`时,表明多个`css_set`下的进程列表受到同一份资源的相同限制。 55 | 56 | ### cgroups文件系统 57 | 58 | Linux 使用了多种数据结构在内核中实现了 cgroups 的配置,关联了进程和 cgroups 节点,那么 Linux 又是如何让用户态的进程使用到 cgroups 的功能呢? Linux内核有一个很强大的模块叫 VFS (Virtual File System)。 VFS 能够把具体文件系统的细节隐藏起来,给用户态进程提供一个统一的文件系统 API 接口。 cgroups 也是通过 VFS 把功能暴露给用户态的,cgroups 与 VFS 之间的衔接部分称之为 cgroups 文件系统。下面先介绍一下 VFS 的基础知识,然后再介绍下 cgroups 文件系统的实现。 59 | 60 | #### VFS 61 | 62 | VFS 是一个内核抽象层,能够隐藏具体文件系统的实现细节,从而给用户态进程提供一套统一的 API 接口。VFS 使用了一种通用文件系统的设计,具体的文件系统只要实现了 VFS 的设计接口,就能够注册到 VFS 中,从而使内核可以读写这种文件系统。 这很像面向对象设计中的抽象类与子类之间的关系,抽象类负责对外接口的设计,子类负责具体的实现。其实,VFS本身就是用 c 语言实现的一套面向对象的接口。 63 | 64 | ##### 通用文件模型 65 | 66 | VFS 通用文件模型中包含以下四种元数据结构: 67 | 68 | 1. 超级块对象(superblock object),用于存放已经注册的文件系统的信息。比如ext2,ext3等这些基础的磁盘文件系统,还有用于读写socket的socket文件系统,以及当前的用于读写cgroups配置信息的 cgroups 文件系统等。 69 | 2. 索引节点对象(inode object),用于存放具体文件的信息。对于一般的磁盘文件系统而言,inode 节点中一般会存放文件在硬盘中的存储块等信息;对于socket文件系统,inode会存放socket的相关属性,而对于cgroups这样的特殊文件系统,inode会存放与 cgroup 节点相关的属性信息。这里面比较重要的一个部分是一个叫做 inode_operations 的结构体,这个结构体定义了在具体文件系统中创建文件,删除文件等的具体实现。 70 | 3. 文件对象(file object),一个文件对象表示进程内打开的一个文件,文件对象是存放在进程的文件描述符表里面的。同样这个文件中比较重要的部分是一个叫 file_operations 的结构体,这个结构体描述了具体的文件系统的读写实现。当进程在某一个文件描述符上调用读写操作时,实际调用的是 file_operations 中定义的方法。 对于普通的磁盘文件系统,file_operations 中定义的就是普通的块设备读写操作;对于socket文件系统,file_operations 中定义的就是 socket 对应的 send/recv 等操作;而对于cgroups这样的特殊文件系统,file_operations 中定义的就是操作 cgroup 结构体等具体的实现。 71 | 4. 目录项对象(dentry object),在每个文件系统中,内核在查找某一个路径中的文件时,会为内核路径上的每一个分量都生成一个目录项对象,通过目录项对象能够找到对应的 inode 对象,目录项对象一般会被缓存,从而提高内核查找速度。 72 | 73 | \#####cgroups文件系统的实现 74 | 75 | 基于 VFS 实现的文件系统,都必须实现 VFS 通用文件模型定义的这些对象,并实现这些对象中定义的部分函数。cgroup 文件系统也不例外,下面来看一下 cgroups 中这些对象的定义。 76 | 77 | 首先看一下 cgroups 文件系统类型的结构体: 78 | 79 | ```c 80 | static struct file_system_type cgroup_fs_type = { 81 | .name = "cgroup", 82 | .mount = cgroup_mount, 83 | .kill_sb = cgroup_kill_sb, 84 | }; 85 | ``` 86 | 87 | 这里面两个函数分别代表安装和卸载某一个 cgroup 文件系统所需要执行的函数。每次把某一个 cgroups 子系统安装到某一个装载点的时候,cgroup_mount 方法就会被调用,这个方法会生成一个 cgroups_root(cgroups层级结构的根)并封装成超级快对象。 88 | 89 | 然后看一下 cgroups 超级块对象定义的操作: 90 | 91 | ```c 92 | static const struct super_operations cgroup_ops = { 93 | .statfs = simple_statfs, 94 | .drop_inode = generic_delete_inode, 95 | .show_options = cgroup_show_options, 96 | .remount_fs = cgroup_remount, 97 | }; 98 | ``` 99 | 100 | 这里只有部分函数的实现,这是因为对于特定的文件系统而言,所支持的操作可能仅是 super_operations 中所定义操作的一个子集,比如说对于块设备上的文件对象,肯定是支持类似 fseek 的查找某个位置的操作,但是对于 socket 或者 cgroups 这样特殊的文件系统,就不支持这样的操作。 101 | 102 | 同样简单看下 cgroups 文件系统对 inode 对象和 file 对象定义的特殊实现函数: 103 | 104 | ```c 105 | static const struct inode_operations cgroup_dir_inode_operations = { 106 | .lookup = cgroup_lookup, 107 | .mkdir = cgroup_mkdir, 108 | .rmdir = cgroup_rmdir, 109 | .rename = cgroup_rename, 110 | }; 111 | 112 | static const struct file_operations cgroup_file_operations = { 113 | .read = cgroup_file_read, 114 | .write = cgroup_file_write, 115 | .llseek = generic_file_llseek, 116 | .open = cgroup_file_open, 117 | .release = cgroup_file_release, 118 | }; 119 | ``` 120 | 121 | 本文并不去研究这些函数的代码实现是什么样的,但是从这些代码可以推断出,cgroups 通过实现 VFS 的通用文件系统模型,把维护 cgroups 层级结构的细节,隐藏在 cgroups 文件系统的这些实现函数中。 122 | 123 | 从另一个方面说,用户在用户态对 cgroups 文件系统的操作,通过 VFS 转化为对 cgroups 层级结构的维护。通过这样的方式,内核把 cgroups 的功能暴露给了用户态的进程。 124 | 125 | ### cgroups使用方法 126 | 127 | #### cgroups文件系统挂载 128 | 129 | Linux中,用户可以使用mount命令挂载 cgroups 文件系统,格式为: `mount -t cgroup -o subsystems name /cgroup/name`,其中 subsystems 表示需要挂载的 cgroups 子系统, /cgroup/name 表示挂载点,如上文所提,这条命令同时在内核中创建了一个cgroups 层级结构。 130 | 131 | 比如挂载 cpuset, cpu, cpuacct, memory 4个subsystem到/cgroup/cpu_and_mem 目录下,就可以使用 `mount -t cgroup -o remount,cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem` 132 | 133 | 在centos下面,在使用`yum install libcgroup`安装了cgroups模块之后,在 /etc/cgconfig.conf 文件中会自动生成 cgroups 子系统的挂载点: 134 | 135 | ```c 136 | mount { 137 | cpuset = /cgroup/cpuset; 138 | cpu = /cgroup/cpu; 139 | cpuacct = /cgroup/cpuacct; 140 | memory = /cgroup/memory; 141 | devices = /cgroup/devices; 142 | freezer = /cgroup/freezer; 143 | net_cls = /cgroup/net_cls; 144 | blkio = /cgroup/blkio; 145 | } 146 | ``` 147 | 148 | 上面的每一条配置都等价于展开的 mount 命令,例如`mount -t cgroup -o cpuset cpuset /cgroup/cpuset`。这样系统启动之后会自动把这些子系统挂载到相应的挂载点上。 149 | 150 | \####子节点和进程 151 | 152 | 挂载某一个 cgroups 子系统到挂载点之后,就可以通过在挂载点下面建立文件夹或者使用cgcreate命令的方法创建 cgroups 层级结构中的节点。比如通过命令`cgcreate -t sankuai:sankuai -g cpu:test`就可以在 cpu 子系统下建立一个名为 test 的节点。结果如下所示: 153 | 154 | ```shell 155 | [root@idx cpu]# ls 156 | cgroup.event_control cgroup.procs cpu.cfs_period_us cpu.cfs_quota_us cpu.rt_period_us cpu.rt_runtime_us cpu.shares cpu.stat lxc notify_on_release release_agent tasks test 157 | ``` 158 | 159 | 然后可以通过写入需要的值到 test 下面的不同文件,来配置需要限制的资源。每个子系统下面都可以进行多种不同的配置,需要配置的参数各不相同,详细的参数设置需要参考 cgroups 手册。使用 cgset 命令也可以设置 cgroups 子系统的参数,格式为 `cgset -r parameter=value path_to_cgroup`。 160 | 161 | 当需要删除某一个 cgroups 节点的时候,可以使用 cgdelete 命令,比如要删除上述的 test 节点,可以使用 `cgdelete -r cpu:test`命令进行删除 162 | 163 | 把进程加入到 cgroups 子节点也有多种方法,可以直接把 pid 写入到子节点下面的 task 文件中。也可以通过 cgclassify 添加进程,格式为 `cgclassify -g subsystems:path_to_cgroup pidlist`,也可以直接使用 cgexec 在某一个 cgroups 下启动进程,格式为`gexec -g subsystems:path_to_cgroup command arguments`. 164 | 165 | ### 实践中的例子 166 | 167 | 相信大多数人都没有读过 Docker 的源代码,但是通过这篇文章,可以估计 Docker 在实现不同的 Container 之间资源隔离和控制的时候,是可以创建比较复杂的 cgroups 节点和配置文件来完成的。然后对于同一个 Container 中的进程,可以把这些进程 PID 添加到同一组 cgroups 子节点中已达到对这些进程进行同样的资源限制。 168 | 169 | 通过各大互联网公司在网上的技术文章,也可以看到很多公司的云平台都是基于 cgroups 技术搭建的,其实也都是把进程分组,然后把整个进程组添加到同一组 cgroups 节点中,受到同样的资源限制。 170 | 171 | 笔者所在的广告组,有一部分任务是给合作的广告投放网站生成“商品信息”,广告投放网站使用这些信息,把广告投放在他们各自的网站上。但是有时候会有恶意的爬虫过来爬取商品信息,所以我们生成了另外“一小份”数据供优先级较低的用户下载,这时候基本能够区分开大部分恶意爬虫。对于这样的“一小份”数据,对及时更新的要求不高,生成商品信息又是一个比较费资源的任务,所以我们把这个任务的cpu资源使用率限制在了50%。 172 | 173 | 首先在 cpu 子系统下面创建了一个 halfapi 的子节点:`cgcreate abc:abc -g cpu:halfapi`。 174 | 175 | 然后在配置文件中写入配置数据:`echo 50000 > /cgroup/cpu/halfapi/cpu.cfs_quota_us`。`cpu.cfs_quota_us`中的默认值是100000,写入50000表示只能使用50%的 cpu 运行时间。 176 | 177 | 最后在这个cgroups中启动这个任务:`cgexec -g "cpu:/halfapi" php halfapi.php half >/dev/null 2>&1` 178 | 179 | 在 cgroups 引入内核之前,想要完成上述的对某一个进程的 cpu 使用率进行限制,只能通过 nice 命令调整进程的优先级,或者 cpulimit 命令限制进程使用进程的 cpu 使用率。但是这些命令的缺点是无法限制一个进程组的资源使用限制,也就无法完成 Docker 或者其他云平台所需要的这一类轻型容器的资源限制要求。 180 | 181 | 同样,在 cgroups 之前,想要完成对某一个或者某一组进程的物理内存使用率的限制,几乎是不可能完成的。使用 cgroups 提供的功能,可以轻易的限制系统内某一组服务的物理内存占用率。 对于网络包,设备访问或者io资源的控制,cgroups 同样提供了之前所无法完成的精细化控制。 182 | 183 | ### 结语 184 | 185 | 本文首先介绍了 cgroups 在内核中的实现方式,然后介绍了 cgroups 如何通过 VFS 把相关的功能暴露给用户,然后简单介绍了 cgroups 的使用方法,最后通过分析了几个 cgroups 在实践中的例子,进一步展示了 cgroups 的强大的精细化控制能力。 186 | 187 | > 原文链接:https://tech.meituan.com/2015/03/31/cgroups.html 188 | -------------------------------------------------------------------------------- /cgroups/文章/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cgroups/文章/cgroups 管理进程磁盘 io.md: -------------------------------------------------------------------------------- 1 | # 用 cgroups 管理进程磁盘 io 2 | 3 | linux 的 cgroups 还可以限制和监控进程的磁盘 io。这个功能通过 blkio 子系统实现。 4 | 5 | blkio 子系统里东西很多。不过大部分都是只读的状态报告,可写的参数就只有下面这几个: 6 | 7 | ``` 8 | blkio.throttle.read_bps_device 9 | blkio.throttle.read_iops_device 10 | blkio.throttle.write_bps_device 11 | blkio.throttle.write_iops_device 12 | blkio.weight 13 | blkio.weight_device 14 | ``` 15 | 16 | 这些都是用来控制进程的磁盘 io 的。很明显地分成两类,其中带“throttle”的,顾名思义就是节流阀,将流量限制在某个值下。而“weight”就是分配 io 的权重。 17 | 18 | “throttle”的那四个参数看名字就知道是做什么用的。拿 blkio.throttle.read_bps_device 来限制每秒能读取的字节数。先跑点 io 出来 19 | 20 | ``` 21 | dd if=/dev/sda of=/dev/null & 22 | [1] 2750 23 | ``` 24 | 25 | 用 iotop 看看目前的 io 26 | 27 | ``` 28 | TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND 29 | 2750 be/4 root 66.76 M/s 0.00 B/s 0.00 % 68.53 % dd if=/dev/sda of=/dev/null 30 | ... 31 | ``` 32 | 33 | 然后修改一下资源限制,把进程加入控制组 34 | 35 | ``` 36 | echo '8:0 1048576' >/sys/fs/cgroup/blkio/foo/blkio.throttle.read_bps_device 37 | echo 2750 >/sys/fs/cgroup/blkio/foo/tasks 38 | ``` 39 | 40 | 这里的 8:0 就是对应块设备的主设备号和副设备号。可以通过 `ls -l` 设备文件名查看。如 41 | 42 | ``` 43 | # ls -l /dev/sda 44 | brw-rw----. 1 root disk 8, 0 Oct 24 11:27 /dev/sda 45 | ``` 46 | 47 | 这里的 8, 0 就是对应的设备号。所以,cgroups 可以对不同的设备做不同的限制。然后来看看效果 48 | 49 | ``` 50 | TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND 51 | 2750 be/4 root 989.17 K/s 0.00 B/s 0.00 % 96.22 % dd if=/dev/sda of=/dev/null 52 | ... 53 | ``` 54 | 55 | 可见,进程的每秒读取立马就降到了 1MB 左右。要解除限制,写入如 “8:0 0” 到文件中即可 56 | 57 | 对写入的限制也类似。只是测试的时候要注意,dd 需要加上 oflag=sync,避免文件系统缓存影响。如 58 | 59 | ``` 60 | dd if=/dev/zero of=/data/test.dat oflag=sync 61 | ``` 62 | 63 | 还有别犯傻把 of 输出直接写设备文件 :-) 64 | 65 | 不过需要注意的是,这种方式对小于采样间隔里产生的大量 io 是没用的。比如,就算在 1s 内产生一个每秒写入 100M 的峰值,也不会因此被限制掉。 66 | 67 | 再看看 blkio.weight 。blkio 的 throttle 和 weight 方式和 cpu 子系统的 quota 和 shares 有点像,都是一种是绝对限制,另一种是相对限制,并且在不繁忙的时候可以充分利用资源,权重值的范围在 10 – 1000 之间。 68 | 69 | 测试权重方式要麻烦一点。因为不是绝对限制,所以会受到文件系统缓存的影响。如在虚拟机中测试,要关闭虚机如我用的 VirtualBox 在宿主机上的缓存。如要测试读 io 的效果,先生成两个几个 G 的大文件 /tmp/file_1,/tmp/file_2 ,可以用 dd 搞。然后设置两个权重 70 | 71 | ``` 72 | # echo 500 >/sys/fs/cgroup/blkio/foo/blkio.weight 73 | # echo 100 >/sys/fs/cgroup/blkio/bar/blkio.weight 74 | ``` 75 | 76 | 测试前清空文件系统缓存,以免干扰测试结果 77 | 78 | ``` 79 | sync 80 | echo 3 >/proc/sys/vm/drop_caches 81 | ``` 82 | 83 | 在这两个控制组中用 dd 产生 io 测试效果。 84 | 85 | ``` 86 | # cgexec -g "blkio:foo" dd if=/tmp/file_1 of=/dev/null & 87 | [1] 1838 88 | # cgexec -g "blkio:bar" dd if=/tmp/file_2 of=/dev/null & 89 | [2] 1839 90 | ``` 91 | 92 | 还是用 iotop 看看效果 93 | 94 | ``` 95 | TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND 96 | 1839 be/4 root 48.14 M/s 0.00 B/s 0.00 % 99.21 % dd if=/tmp/file_2 of=/dev/null 97 | 1838 be/4 root 223.59 M/s 0.00 B/s 0.00 % 16.44 % dd if=/tmp/file_1 of=/dev/null 98 | ``` 99 | 100 | 两个进程每秒读的字节数虽然会不断变动,但是大致趋势还是维持在 1:5 左右,和设定的 weight 比例一致。blkio.weight_device 是分设备的。写入时,前面再加上设备号即可。 101 | 102 | blkio 子系统里还有很多统计项 103 | 104 | - blkio.time 105 | 106 | 各设备的 io 访问时间,单位毫秒 107 | 108 | - blkio.sectors 109 | 110 | 换入者或出各设备的扇区数 111 | 112 | - blkio.io_serviced 113 | 114 | 各设备中执行的各类型 io 操作数,分read、write、sync、async 和 total 115 | 116 | - blkio.io_service_bytes 117 | 118 | 各类型 io 换入者或出各设备的字节数 119 | 120 | - blkio.io_service_time 121 | 122 | 各设备中执行的各类型 io 时间,单位微秒 123 | 124 | - blkio.io_wait_time 125 | 126 | 各设备中各类型 io 在队列中的 等待时间 127 | 128 | - blkio.io_merged 129 | 130 | 各设备中各类型 io 请求合并的次数 131 | 132 | - blkio.io_queued 133 | 134 | 各设备中各类型 io 请求当前在队列中的数量 135 | 136 | 通过这些统计项更好地统计、监控进程的 io 情况 137 | 用 138 | 139 | ``` 140 | echo 1 >blkio.reset_stats 141 | ``` 142 | 143 | 可以将所有统计项清零。 144 | 145 | > 原文链接:http://xiezhenye.com/2013/10/%e7%94%a8-cgroups-%e7%ae%a1%e7%90%86-cpu-%e8%b5%84%e6%ba%90.html 146 | -------------------------------------------------------------------------------- /cgroups/文章/linux cgroups 概述.md: -------------------------------------------------------------------------------- 1 | # linux cgroups 概述 2 | 3 | 从 2.6.24 版本开始,linux 内核提供了一个叫做 cgroups(控制组)的特性。cgroups 就是 control groups 的缩写,用来对一组进程所占用的资源做限制、统计、隔离。也是目前轻量级虚拟化技术 lxc (linux container)的基础之一。每一组进程就是一个控制组,也就是一个 cgroup。cgroups 分为几个子系统,每个子系统代表一种设施或者说是资源控制器,用来调度某一类资源的使用,如 cpu 时钟、内存、块设备 等。在实现上,cgroups 并没有增加新的系统调用,而是表现为一个 cgroup 文件系统,可以把一个或多个子系统挂载到某个目录。如 4 | 5 | ``` 6 | mount -t cgroup -o cpu cpu /sys/fs/cgroup/cpu 7 | ``` 8 | 9 | 就将 cpu 子系统挂载在了 /sys/fs/cgroup/cpu 。也可以在一个目录上挂载多个子系统,甚至全部挂载到一个目录也是可以的,不过我觉得,把每个子系统都挂载在不同目录会有更好的灵活性。用 `mount|awk '$5=="cgroup" {print $0}'` 可以看到当前挂载的控制组。用 `cat /proc/cgroups` 可以看到当前所有控制组的状态。下面这个脚本,可以把全部子系统各种挂载到各自的目录上去。 10 | 11 | ``` 12 | #!/bin/bash 13 | 14 | cgroot="${1:-/sys/fs/cgroup}" 15 | subsys="${2:-blkio cpu cpuacct cpuset devices freezer memory net_cls net_prio ns perf_event}" 16 | 17 | mount -t tmpfs cgroup_root "${cgroot}" 18 | for ss in $subsys; do 19 | mkdir -p "$cgroot/$ss" 20 | mount -t cgroup -o "$ss" "$ss" "$cgroot/$ss" 21 | done 22 | ``` 23 | 24 | 看看那些目录里都有些啥,比如 ls 一下 /sys/fs/cgroup/cpu。 25 | 26 | ``` 27 | cgroup.event_control cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release tasks 28 | cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat release_agent 29 | ``` 30 | 31 | 其中 “cpu.” 开头的就是这个子系统里特有的东西。其他的那些是每个子系统所对应目录里都有的。这些文件就是用来读取资源使用信息和进行资源限制的。要创建一个控制组,就在需要的子系统里创建一个目录即可。如 `mkdir /sys/fs/cgroup/cpu/foo` 就创建了一个 /foo 的控制组。在新建的目录里就会出现同样一套文件。在这个目录里,也一样可以继续通过创建目录来创建 cgroup。也就是说,cgroup 是可以和目录结构一样有层次的。对与每个子系统挂载点点目录,就相当于根目录。每一条不同的路径就代表了一个不同的 cgroup。在不同的子系统里,路径相同就代表了同一个控制组。如,在 cpu、memory 中都有 foo/bar 目录,就可以用 那 /foo/bar 来操作 cpu、memory 两个子系统。对于同一个子系统,每个进程都属于且只属于一个 cgroup,默认是在根 cgroup。层次结构方便了控制组的组织和管理,对于某些配置项来说,层次结构还和资源分配有关。另外,也可以修改某个目录的 owner ,让非 root 用户也能操作某些特定的安全组。 32 | 33 | cgroups 的设置和信息读取是通过对那些文件的读写来进行的。例如 34 | 35 | ``` 36 | # echo 2048 >/sys/fs/cgroup/cpu/foo/cpu.shares 37 | ``` 38 | 39 | 就把 /foo 这个控制组的 cpu.shares 参数设为了 2048。 40 | 41 | 前面说,有些文件是每个目录里共有的。那些就是通用的设置。其中,tasks 和 cgroups.procs 是用来管理控制组中的进程的。要把一个进程加入到某个控制组,把 pid 写入到相应目录的 tasks 文件即可。如 42 | 43 | ``` 44 | # echo 5678 >/sys/fs/cgroup/cpu/foo/tasks 45 | ``` 46 | 47 | 就把 5678 进程加入到了 /foo 控制组。那么 tasks 和 cgroups.procs 有什么区别呢?前面说的对“进程”的管理限制其实不够准确。系统对任务调度的单位是线程。在这里,tasks 中看到的就是线程 id。而 cgroups.procs 中是线程组 id,也就是一般所说的进程 id 。将一个一般的 pid 写入到 tasks 中,只有这个 pid 对应的线程,以及由它产生的其他进程、线程会属于这个控制组,原有的其他线程则不会。而写入 cgroups.procs 会把当前所有的线程都加入进去。如果写入 cgroups.procs 的不是一个线程组 id,而是一个一般的线程 id,那会自动找到所对应的线程组 id 加入进去。进程在加入一个控制组后,控制组所对应的限制会即时生效。想知道一个进程属于哪些控制组,可以通过 `cat /proc//cgroup` 查看。 48 | 49 | 要把进程移出控制组,把 pid 写入到根 cgroup 的 tasks 文件即可。因为每个进程都属于且只属于一个 cgroup,加入到新的 cgroup 后,原有关系也就解除了。要删除一个 cgroup,可以用 rmdir 删除相应目录。不过在删除前,必须先让其中的进程全部退出,对应子系统的资源都已经释放,否则是无法删除的。 50 | 51 | 前面都是通过文件系统访问方式来操作 cgroups 的。实际上,也有一组命令行工具。 52 | 53 | `lssubsys -am` 可以查看各子系统的挂载点,还有一组“cg”开头的命令可以用来管理。其中 cgexec 可以用来直接在某些子系统中的指定控制组运行一个程序。如 `cgexec -g "cpu,blkio:/foo" bash` 。其他的命令和具体的参数可以通过 man 来查看。 54 | 55 | 下面是个 bash 版的 cgexec,演示了 cgroups 的用法,也可以在不确定是否安装命令行工具的情况下使用。 56 | 57 | ``` 58 | #!/bin/bash 59 | 60 | # usage: 61 | # ./cgexec.sh cpu:g1,memory:g2/g21 sleep 100 62 | 63 | blkio_dir="/sys/fs/cgroup/blkio" 64 | memory_dir="/sys/fs/cgroup/memory" 65 | cpuset_dir="/sys/fs/cgroup/cpuset" 66 | perf_event_dir="/sys/fs/cgroup/perf_event" 67 | freezer_dir="/sys/fs/cgroup/freezer" 68 | net_cls_dir="/sys/fs/cgroup/net_cls" 69 | cpuacct_dir="/sys/fs/cgroup/cpuacct" 70 | cpu_dir="/sys/fs/cgroup/cpu" 71 | hugetlb_dir="/sys/fs/cgroup/hugetlb" 72 | devices_dir="/sys/fs/cgroup/devices" 73 | 74 | groups="$1" 75 | shift 76 | 77 | IFS=',' g_arr=($groups) 78 | for g in ${g_arr[@]}; do 79 | IFS=':' g_info=($g) 80 | if [ ${#g_info[@]} -ne 2 ]; then 81 | echo "bad arg $g" >&2 82 | continue 83 | fi 84 | g_name=${g_info[0]} 85 | g_path=${g_info[1]} 86 | if [ "$g_path" == "${g_path#/}" ]; then 87 | g_path="/$g_path" 88 | fi 89 | echo $g_name $g_path 90 | var="${g_name}_dir" 91 | d=${!var} 92 | if [ -z "$d" ]; then 93 | echo "bad cg name $g_name" >&2 94 | continue 95 | fi 96 | path="${d}${g_path}" 97 | if [ ! -d "$path" ]; then 98 | echo "cg not exists" >&2 99 | continue 100 | fi 101 | echo "$$" >"${path}/tasks" 102 | done 103 | 104 | exec $* 105 | ``` 106 | 107 | 参考资料: 108 | [cgroups docs – kernel.org](https://www.kernel.org/doc/Documentation/cgroups/) 109 | 110 | [Resource Management Guide – redhat.com](https://access.redhat.com/site/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/pref-Resource_Management_Guide-Preface.html) 111 | 112 | [How I Used CGroups to Manage System Resources – oracle.com](http://www.oracle.com/technetwork/articles/servers-storage-admin/resource-controllers-linux-1506602.html) 113 | 114 | > 原文链接:http://xiezhenye.com/2013/10/linux-cgroups-%e6%a6%82%e8%bf%b0.html 115 | -------------------------------------------------------------------------------- /cgroups/文章/彻底搞懂容器技术的基石: cgroup.md: -------------------------------------------------------------------------------- 1 | # 彻底搞懂容器技术的基石: cgroup 2 | 3 | # **为什么要关注 cgroup & namespace** 4 | 5 | ## **云原生/容器技术的井喷式增长** 6 | 7 | 自 1979年,Unix 版本7 在开发过程中引入 Chroot Jail 以及 Chroot 系统调用开始,直到 2013 年开源出的 Docker,2014 年开源出来的 Kubernetes,直到现在的云原生生态的火热。容器技术已经逐步成为主流的基础技术之一。 8 | 9 | 在越来越多的公司、个人选择了云服务/容器技术后,资源的分配和隔离,以及安全性变成了人们关注及讨论的热点话题。 10 | 11 | 其实容器技术使用起来并不难,但要真正把它用好,大规模的在生产环境中使用, 那我们还是需要掌握其核心的。 12 | 13 | 以下是容器技术&云原生生态的大致发展历程: 14 | 15 | ![image](https://user-images.githubusercontent.com/87457873/150334290-d9cf2d5c-f24b-4c12-81e1-7080ebe5a42d.png) 16 | 17 | 图 1 ,容器技术发展历程 18 | 19 | 从图中,我们可以看到容器技术、云原生生态的发展轨迹。容器技术其实很早就出现了,但为何在 Docker 出现后才开始有了较为显著的发展?早期的 chroot 、 Linux VServer 又有哪些问题呢? 20 | 21 | ## **Chroot 带来的安全性问题** 22 | 23 | ![image](https://user-images.githubusercontent.com/87457873/150334304-b8e51f6c-b66e-488e-bf3d-f263426cc63d.png) 24 | 25 | 图 2 ,chroot 示例 26 | 27 | Chroot 可以将进程及其子进程与操作系统的其余部分隔离开来。但是,对于 root process ,却可以任意退出 chroot。 28 | 29 | ```javascript 30 | package main 31 | 32 | import ( 33 | "log" 34 | "os" 35 | "syscall" 36 | ) 37 | 38 | func getWd() (path string) { 39 | path, err := os.Getwd() 40 | if err != nil { 41 | log.Println(err) 42 | } 43 | log.Println(path) 44 | return 45 | } 46 | 47 | func main() { 48 | RealRoot, err := os.Open("/") 49 | defer RealRoot.Close() 50 | if err != nil { 51 | log.Fatalf("[ Error ] - /: %v\n", err) 52 | } 53 | path := getWd() 54 | 55 | err = syscall.Chroot(path) 56 | if err != nil { 57 | log.Fatalf("[ Error ] - chroot: %v\n", err) 58 | } 59 | getWd() 60 | 61 | err = RealRoot.Chdir() 62 | if err != nil { 63 | log.Fatalf("[ Error ] - chdir(): %v", err) 64 | } 65 | getWd() 66 | 67 | err = syscall.Chroot(".") 68 | if err != nil { 69 | log.Fatalf("[ Error ] - chroot back: %v", err) 70 | } 71 | getWd() 72 | } 73 | ``` 74 | 75 | 分别以普通用户和 sudo 的方式运行: 76 | 77 | ```javascript 78 | ➜ chroot go run main.go 79 | 2021/11/18 00:46:21 /tmp/chroot 80 | 2021/11/18 00:46:21 [ Error ] - chroot: operation not permitted 81 | exit status 1 82 | ➜ chroot sudo go run main.go 83 | 2021/11/18 00:46:25 /tmp/chroot 84 | 2021/11/18 00:46:25 / 85 | 2021/11/18 00:46:25 (unreachable)/ 86 | 2021/11/18 00:46:25 / 87 | ``` 88 | 89 | 可以看到如果是使用 `sudo`来运行的时候,程序在当前目录和系统原本的根目录下进行了切换。而普通用户则无权限操作。 90 | 91 | ## **Linux VServer 的安全漏洞** 92 | 93 | Linux-VServer 是一种基于 *Security Contexts* 的软分区技术,可以做到虚拟服务器隔离,共享相同的硬件资源。主要问题是 VServer 应用程序针对 "chroot-again" 类型的攻击没有很好的进行安全保护,攻击者可以利用这个漏洞脱离限制环境,访问限制目录之外的任意文件。(自 2004年开始,国家信息安全漏洞库就登出了相关漏洞问题) 94 | 95 | ![image](https://user-images.githubusercontent.com/87457873/150334335-2b75e1a2-b9e8-40f3-bd5c-bee5f49407b9.png) 96 | 97 | 图 3 ,国家信息安全漏洞库网站图标 98 | 99 | ## **现代化容器技术带来的优势** 100 | 101 | - 轻量级,基于 Linux 内核所提供的 cgroup 和 namespace 能力,创建容器的成本很低; 102 | - 一定的隔离性; 103 | - 标准化,通过使用容器镜像的方式进行应用程序的打包和分发,可以屏蔽掉因为环境不一致带来的诸多问题; 104 | - DevOps 支撑(可以在不同环境,如开发、测试和生产等环境之间轻松迁移应用,同时还可保留应用的全部功能); 105 | - 为基础架构增添防护,提升可靠性、可扩展性和信赖度; 106 | - DevOps/GitOps 支撑 (可以做到快速有效地持续性发布,管理版本及配置); 107 | - 团队成员间可以有效简化、加速和编排应用的开发与部署; 108 | 109 | 在了解了为什么要关注 cgroup 和 namespace 等技术之后,那我们就进入到本篇的重点吧,来一起学习下 cgroup 。 110 | 111 | # **什么是 cgroup** 112 | 113 | cgroup 是 Linux 内核的一个功能,用来限制、控制与分离一个进程组的资源(如CPU、内存、磁盘输入输出等)。它是由 Google 的两位工程师进行开发的,自 2008 年 1 月正式发布的 Linux 内核 v2.6.24 开始提供此能力。 114 | 115 | cgroup 到目前为止,有两个大版本, cgroup v1 和 v2 。以下内容以 cgroup v2 版本为主,涉及两个版本差别的地方会在下文详细介绍。 116 | 117 | cgroup 主要限制的资源是: 118 | 119 | - CPU 120 | - 内存 121 | - 网络 122 | - 磁盘 I/O 123 | 124 | 当我们将可用系统资源按特定百分比分配给 cgroup 时,剩余的资源可供系统上的其他 cgroup 或其他进程使用。 125 | 126 | ![image](https://user-images.githubusercontent.com/87457873/150334348-4db0fe51-c733-4a54-b399-2b5cdfe60109.png) 127 | 128 | 图 4 ,cgroup 资源分配及剩余可用资源示例 129 | 130 | ## **cgroup 的组成** 131 | 132 | cgroup 代表“控制组”,并且不会使用大写。cgroup 是一种分层组织进程的机制, 沿层次结构以受控的方式分配系统资源。我们通常使用单数形式用于指定整个特征,也用作限定符如 “cgroup controller” 。 133 | 134 | cgroup 主要有两个组成部分: 135 | 136 | - core - 负责分层组织过程; 137 | - controller - 通常负责沿层次结构分配特定类型的系统资源。每个 cgroup 都有一个 `cgroup.controllers` 文件,其中列出了所有可供 cgroup 启用的控制器。当在 `cgroup.subtree_control` 中指定多个控制器时,要么全部成功,要么全部失败。在同一个控制器上指定多项操作,那么只有最后一个生效。每个 cgroup 的控制器销毁是异步的,在引用时同样也有着延迟引用的问题; 138 | 139 | 所有 cgroup 核心接口文件都以 `cgroup` 为前缀。每个控制器的接口文件都以控制器名称和一个点为前缀。控制器的名称由小写字母和“*”组成,但永远不会以“*”开头。 140 | 141 | ## **cgroup 的核心文件** 142 | 143 | - cgroup.type - (单值)存在于非根 cgroup 上的可读写文件。通过将“threaded”写入该文件,可以将 cgroup 转换为线程 cgroup,可选择 4 种取值,如下: 144 | 1. domain - 一个正常的有效域 cgroup 145 | 2. domain threaded - 线程子树根的线程域 cgroup 146 | 3. domain invalid - 无效的 cgroup 147 | 4. threaded - 线程 cgroup,线程子树 148 | - cgroup.procs - (换行分隔)所有 cgroup 都有的可读写文件。每行列出属于 cgroup 的进程的 PID。PID 不是有序的,如果进程移动到另一个 cgroup ,相同的 PID 可能会出现不止一次; 149 | - cgroup.controllers - (空格分隔)所有 cgroup 都有的只读文件。显示 cgroup 可用的所有控制器; 150 | - cgroup.subtree_control - (空格分隔)所有 cgroup 都有的可读写文件,初始为空。如果一个控制器在列表中出现不止一次,最后一个有效。当指定多个启用和禁用操作时,要么全部成功,要么全部失败。 151 | 1. 以“+”为前缀的控制器名称表示启用控制器 152 | 2. 以“-”为前缀的控制器名称表示禁用控制器 153 | - cgroup.events - 存在于非根 cgroup 上的只读文件。 154 | 1. populated - cgroup 及其子节点中包含活动进程,值为1;无活动进程,值为0. 155 | 2. frozen - cgroup 是否被冻结,冻结值为1;未冻结值为0. 156 | - cgroup.threads - (换行分隔)所有 cgroup 都有的可读写文件。每行列出属于 cgroup 的线程的 TID。TID 不是有序的,如果线程移动到另一个 cgroup ,相同的 TID 可能会出现不止一次。 157 | - cgroup.max.descendants - (单值)可读写文件。最大允许的 cgroup 子节点数量。 158 | - cgroup.max.depth - (单值)可读写文件。低于当前节点最大允许的树深度。 159 | - cgroup.stat - 只读文件。 160 | 1. nr_descendants - 可见后代的 cgroup 数量。 161 | 2. nr_dying_descendants - 被用户删除即将被系统销毁的 cgroup 数量。 162 | - cgroup.freeze - (单值)存在于非根 cgroup 上的可读写文件。默认值为0。当值为1时,会冻结 cgroup 及其所有子节点 cgroup,会将相关的进程关停并且不再运行。冻结 cgroup 需要一定的时间,当动作完成后, cgroup.events 控制文件中的 “frozen” 值会更新为“1”,并发出相应的通知。cgroup 的冻结状态不会影响任何 cgroup 树操作(删除、创建等); 163 | - cgroup.kill - (单值)存在于非根 cgroup 上的可读写文件。唯一允许值为1,当值为1时,会将 cgroup 及其所有子节点中的 cgroup 杀死(进程会被 SIGKILL 杀掉)。一般用于将一个 cgroup 树杀掉,防止叶子节点迁移; 164 | 165 | ## **cgroup 的归属和迁移** 166 | 167 | 系统中的每个进程都属于一个 cgroup,一个进程的所有线程都属于同一个 cgroup。一个进程可以从一个 cgroup 迁移到另一个 cgroup 。进程的迁移不会影响现有的后代进程所属的 cgroup。 168 | 169 | ![image](https://user-images.githubusercontent.com/87457873/150334374-6084301c-7d16-4918-a04c-df25e6e37bb3.png) 170 | 171 | 图 5 ,进程及其子进程的 cgroup 分配;跨 cgroup 迁移示例 172 | 173 | 跨 cgroup 迁移进程是一项代价昂贵的操作并且有状态的资源限制(例如,内存)不会动态的应用于迁移。因此,经常跨 cgroup 迁移进程只是作为一种手段。不鼓励直接应用不同的资源限制。 174 | 175 | ### **如何实现跨 cgroup 迁移** 176 | 177 | 每个cgroup都有一个可读写的接口文件 “cgroup.procs” 。每行一个 PID 记录 cgroup 限制管理的所有进程。一个进程可以通过将其 PID 写入另一 cgroup 的 “cgroup.procs” 文件来实现迁移。 178 | 179 | 但是这种方式,只能迁移一个进程在单个 write(2) 上的调用(如果一个进程有多个线程,则会同时迁移所有线程,但也要参考线程子树,是否有将进程的线程放入不同的 cgroup 的记录)。 180 | 181 | 当一个进程 fork 出一个子进程时,该进程就诞生在其父亲进程所属的 cgroup 中。 182 | 183 | 一个没有任何子进程或活动进程的 cgroup 是可以通过删除目录进行销毁的(即使存在关联的僵尸进程,也被认为是可以被删除的)。 184 | 185 | # **什么是 cgroups** 186 | 187 | 当明确提到多个单独的控制组时,才使用复数形式 “cgroups” 。 188 | 189 | cgroups 形成了树状结构。(一个给定的 cgroup 可能有多个子 cgroup 形成一棵树结构体)每个非根 cgroup 都有一个 `cgroup.events` 文件,其中包含 `populated` 字段指示 cgroup 的子层次结构是否具有实时进程。所有非根的 `cgroup.subtree_control` 文件,只能包含在父级中启用的控制器。 190 | 191 | ![image](https://user-images.githubusercontent.com/87457873/150334393-f3041cd4-57b7-436c-9708-64f1605f80ac.png) 192 | 193 | 图 6 ,cgroups 示例 194 | 195 | 如图所示,cgroup1 中限制了使用 cpu 及 内存资源,它将控制子节点的 CPU 周期和内存分配(即,限制 cgroup2、cgroup3、cgroup4 中的cpu及内存资源分配)。cgroup2 中启用了内存限制,但是没有启用cpu的资源限制,这就导致了 cgroup3 和 cgroup4 的内存资源受 cgroup2中的 mem 设置内容的限制;cgroup3 和 cgroup4 会自由竞争在 cgroup1 的 cpu 资源限制范围内的 cpu 资源。 196 | 197 | 由此,也可以明显的看出 cgroup 资源是自上而下分布约束的。只有当资源已经从上游 cgroup 节点分发给下游时,下游的 cgroup 才能进一步分发约束资源。所有非根的 `cgroup.subtree_control` 文件只能包含在父节点的 `cgroup.subtree_control` 文件中启用的控制器内容。 198 | 199 | 那么,子节点 cgroup 与父节点 cgroup 是否会存在内部进程竞争的情况呢? 200 | 201 | 当然不会。cgroup v2 中,设定了非根 cgroup 只能在没有任何进程时才能将域资源分发给子节点的 cgroup。简而言之,只有不包含任何进程的 cgroup 才能在其 `cgroup.subtree_control` 文件中启用域控制器,这就保证了,进程总在叶子节点上。 202 | 203 | ## **挂载和委派** 204 | 205 | ### **cgroup 的挂载方式** 206 | 207 | - memory_recursiveprot - 递归地将 memory.min 和 memory.low 保护应用于整个子树,无需显式向下传播到叶节点的 cgroup 中,子树内叶子节点可以自由竞争; 208 | - memory_localevents - 只能挂载时设置或者通过从 init 命名空间重新挂载来修改,这是系统范围的选项。只用当前 cgroup 的数据填充 memory.events,如果没有这个选项,默认会计数所有子树; 209 | - nsdelegate - 只能挂载时设置或者通过从 init 命名空间重新挂载来修改,这也是系统范围的选项。它将 cgroup 命名空间视为委托边界,这是两种委派 cgroup 的方式之一; 210 | 211 | ### **cgroup 的委派方式** 212 | 213 | - 设置挂载选项 nsdelegate ; 214 | - 授权用户对目录及其 `cgroup.procs`、`cgroup.threads` 和 `cgroup.subtree_control` 文件的写访问权限 215 | 216 | 两种方式的结果相同。一旦被委派,用户就可以在目录下建立子层次结构,所有的资源分配都受父节点的制约。目前,cgroup 对委托子层次结构中的 cgroup 数量或嵌套深度没有任何限制(之后可能会受到明确限制)。 217 | 218 | 前面提到了跨 cgroup 迁移,从委派中,我们可以很明确的得知跨 cgroup 迁移对于普通用户来讲,是有限制条件的。即,是否对目前 cgroup 的 “cgroup.procs” 文件具有写访问权限以及是否对源 cgroup 和目标 cgroup 的共同祖先的 “cgroup.procs” 文件具有写访问权限。 219 | 220 | ### **委派和迁移** 221 | 222 | ![image](https://user-images.githubusercontent.com/87457873/150334424-2ceb7790-379b-47a7-9e91-30b34adbf933.png) 223 | 224 | 图 7 ,委派权限示例 225 | 226 | 如图,普通用户 User0 具有 cgroup[1-5] 的委派权限。 227 | 228 | 为什么 User0 想将进程 从 cgroup3 迁移至 cgroup5会失败呢? 229 | 230 | 这是由于 User0 的权限只到 cgroup1 和 cgroup2 层,并不具备 cgroup0 的权限。而委派中的授权用户明确指出需要共同祖先的 “cgroup.procs” 文件具有写访问权限!(即,需要图中 cgroup0 的权限,才可以实现) 231 | 232 | ## **资源分配模型及功能** 233 | 234 | 以下是 cgroups 的资源分配模型: 235 | 236 | - 权重 - (例如,cpu.weight) 所有权重都在 [1, 10000] 范围内,默认值为 100。按照权重比率来分配资源。 237 | - 限制 - [0, max] 范围内,默认为“max”,即 noop(例如,io.max)。限制可以被过度使用(子节点限制的总和可能超过父节点可用的资源量)。 238 | - 保护 - [0, max] 范围内,默认为 0,即 noop(例如,io.low)。保护可以是硬保证或尽力而为的软边界,保护也可能被过度使用。 239 | - 分配 - [0, max] 范围内,默认为 0,即没有资源。分配不能被过度使用(子节点分配的总和不能超过父节点可用的资源量)。 240 | 241 | cgroups 提供了如下功能: 242 | 243 | - 资源限制 - 上面 cgroup 部分已经示例,cgroups 可以以树状结构来嵌套式限制资源。 244 | - 优先级 - 发生资源争用时,优先保障哪些进程的资源。 245 | - 审计 - 监控及报告资源限制及使用。 246 | - 控制 - 控制进程的状态(起、停、挂起)。 247 | 248 | # **cgroup v1 与 cgroup v2** 249 | 250 | ## **被弃用的核心功能** 251 | 252 | cgroup v2 和 cgroup v1 有很大的不同,我们一起来看看在 cgroup v2 中弃用了哪些 cgroup v1 的功能: 253 | 254 | - 不支持包括命名层次在内的多个层次结构; 255 | - 不支持所有 v1 安装选项; 256 | - “tasks” 文件被删除,“cgroup.procs” 没有排序 257 | - 在 cgroup v1 中线程组 ID 的列表。不保证此列表已排序或没有重复的 TGID,如果需要此属性,用户空间应排序/统一列表。将线程组 ID 写入此文件会将该组中的所有线程移动到此 cgroup 中; 258 | - `cgroup.clone_children` 被删除。clone_children 仅影响 cpuset controller。如果在 cgroup 中启用了 clone_children (设置:1),新的 cpuset cgroup 将在初始化期间从父节点的 cgroup 复制配置; 259 | - /proc/cgroups 对于 v2 没有意义。改用根目录下的“cgroup.controllers”文件; 260 | 261 | ## **cgroup v1 的问题** 262 | 263 | cgroup v2 和 v1 中最显著的不同就是 cgroup v1 允许任意数量的层次结构, 但这会带来一些问题的。我们来详细聊聊。 264 | 265 | 挂载 cgroup 层次结构时,你可以指定要挂载的子系统的逗号分隔列表作为文件系统挂载选项。默认情况下,挂载 cgroup 文件系统会尝试挂载包含所有已注册子系统的层次结构。 266 | 267 | 如果已经存在具有完全相同子系统集的活动层次结构,它将被重新用于新安装。 268 | 269 | 如果现有层次结构不匹配,并且任何请求的子系统正在现有层次结构中使用,则挂载将失败并显示 -EBUSY。否则,将激活与请求的子系统相关联的新层次结构。 270 | 271 | 当前无法将新子系统绑定到活动 cgroup 层次结构,或从活动 cgroup 层次结构中取消绑定子系统。当 cgroup 文件系统被卸载时,如果在顶级 cgroup 之下创建了任何子 cgroup,即使卸载,该层次结构仍将保持活动状态;如果没有子 cgroup,则层次结构将被停用。 272 | 273 | 这就是 cgroup v1 中的问题,在 cgroup v2 中就很好的进行了解决。 274 | 275 | # **cgroup 和容器的联系** 276 | 277 | 这里我们以 Docker 为例。创建一个容器,并对其可使用的 CPU 和内存进行限制: 278 | 279 | ```javascript 280 | ➜ ~ docker run --rm -d --cpus=2 --memory=2g --name=2c2g redis:alpine 281 | e420a97835d9692df5b90b47e7951bc3fad48269eb2c8b1fa782527e0ae91c8e 282 | ➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/cpu.max 283 | 200000 100000 284 | ➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/memory.max 285 | 2147483648 286 | ➜ ~ 287 | ➜ ~ docker run --rm -d --cpus=0.5 --memory=0.5g --name=0.5c0.5g redis:alpine 288 | 8b82790fe0da9d00ab07aac7d6e4ef2f5871d5f3d7d06a5cdb56daaf9f5bc48e 289 | ➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/cpu.max 290 | 50000 100000 291 | ➜ ~ cat /sys/fs/cgroup/system.slice/docker-`docker ps -lq --no-trunc`.scope/memory.max 292 | 536870912 293 | ``` 294 | 295 | 从上面的示例可以看到,当我们使用 Docker 创建出新的容器并且为他指定 CPU 和 内存限制后,其对应的 cgroup 配置文件的 `cpu.max` 和 `memory.max`都设置成了相应的值。 296 | 297 | 如果你想要对一些已经在运行的容器进行资源配额的检查的话,也可以直接去查看其对应的配置文件中的内容。 298 | 299 | # **总结** 300 | 301 | 以上就是关于容器技术的基石之一的 cgroup 的详细介绍了。接下来我还会写关于 namespace 以及其他容器技术相关的内容! 302 | 303 | > 原文链接:https://cloud.tencent.com/developer/article/1905320?from=article.detail.1495300 304 | 305 | -------------------------------------------------------------------------------- /cgroups/文章/深入理解 Linux Cgroup 系列(一):基本概念.md: -------------------------------------------------------------------------------- 1 | # 深入理解 Linux Cgroup 系列(一):基本概念 2 | 3 | `Cgroup` 是 Linux kernel 的一项功能:它是在一个系统中运行的层级制进程组,你可对其进行资源分配(如 CPU 时间、系统内存、网络带宽或者这些资源的组合)。通过使用 cgroup,系统管理员在分配、排序、拒绝、管理和监控系统资源等方面,可以进行精细化控制。硬件资源可以在应用程序和用户间智能分配,从而增加整体效率。 4 | 5 | cgroup 和 `namespace` 类似,也是将进程进行分组,但它的目的和 `namespace` 不一样,`namespace` 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。 6 | 7 | cgroup 分 v1和 v2 两个版本,v1 实现较早,功能比较多,但是由于它里面的功能都是零零散散的实现的,所以规划的不是很好,导致了一些使用和维护上的不便,v2 的出现就是为了解决 v1 中这方面的问题,在最新的 4.5 内核中,cgroup v2 声称已经可以用于生产环境了,但它所支持的功能还很有限,随着 v2 一起引入内核的还有 cgroup namespace。v1 和 v2 可以混合使用,但是这样会更复杂,所以一般没人会这样用。 8 | 9 | ## 1. 为什么需要 cgroup 10 | 11 | ------ 12 | 13 | 在 Linux 里,一直以来就有对进程进行分组的概念和需求,比如 session group, progress group等,后来随着人们对这方面的需求越来越多,比如需要追踪一组进程的内存和 IO 使用情况等,于是出现了 cgroup,用来统一将进程进行分组,并在分组的基础上对进程进行监控和资源控制管理等。 14 | 15 | ## 2. 什么是 cgroup 16 | 17 | ------ 18 | 19 | 术语 cgroup 在不同的上下文中代表不同的意思,可以指整个 Linux 的 cgroup 技术,也可以指一个具体进程组。 20 | 21 | cgroup 是 Linux 下的一种将进程按组进行管理的机制,在用户层看来,cgroup 技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个 `subsystem` 关联,树的作用是将进程分组,而 `subsystem` 的作用就是对这些组进行操作。cgroup 主要包括下面两部分: 22 | 23 | - **subsystem** : 一个 subsystem 就是一个内核模块,他被关联到一颗 cgroup 树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem 经常被称作 `resource controller`,因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如 perf_event subsystem。到目前为止,Linux 支持 12 种 subsystem,比如限制 CPU 的使用时间,限制使用的内存,统计 CPU 的使用情况,冻结和恢复一组进程等,后续会对它们一一进行介绍。 24 | - **hierarchy** : 一个 `hierarchy` 可以理解为一棵 cgroup 树,树的每个节点就是一个进程组,每棵树都会与零到多个 `subsystem` 关联。在一颗树里面,会包含 Linux 系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗 cgroup 树,每棵树都和不同的 subsystem 关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的 subsystem 关联。目前 Linux 支持 12 种 subsystem,如果不考虑不与任何 subsystem 关联的情况(systemd 就属于这种情况),Linux 里面最多可以建 12 颗 cgroup 树,每棵树关联一个 subsystem,当然也可以只建一棵树,然后让这棵树关联所有的 subsystem。当一颗 cgroup 树不和任何 subsystem 关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,`systemd`就是一个这样的例子。 25 | 26 | ## 3. 将资源看作一块饼 27 | 28 | ------ 29 | 30 | 在 `CentOS 7` 系统中(包括 Red Hat Enterprise Linux 7),通过将 cgroup 层级系统与 systemd 单位树捆绑,可以把资源管理设置从进程级别移至应用程序级别。默认情况下,systemd 会自动创建 `slice`、`scope` 和 `service` 单位的层级(具体的意思稍后再解释),来为 cgroup 树提供统一结构。可以通过 `systemctl` 命令创建自定义 slice 进一步修改此结构。 31 | 32 | 如果我们将系统的资源看成一块馅饼,那么所有资源默认会被划分为 3 个 cgroup:`System`, `User` 和 `Machine`。每一个 cgroup 都是一个 `slice`,每个 slice 都可以有自己的子 slice,如下图所示: 33 | 34 | 35 | 36 | ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/qFG6mghhA4aEZBic3BE14JEfwAE3icib8myLVHRyGv78zCUZUk0CMqVicOefsWiaTqWtibPcvWPxbJZDxwQtqZRqeRXw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 37 | 38 | 39 | 40 | 下面我们以 CPU 资源为例,来解释一下上图中出现的一些关键词。 41 | 42 | 如上图所示,系统默认创建了 3 个顶级 `slice`(`System`, `User` 和 `Machine`),每个 slice 都会获得相同的 CPU 使用时间(仅在 CPU 繁忙时生效),如果 `user.slice` 想获得 `100%` 的 CPU 使用时间,而此时 CPU 比较空闲,那么 `user.slice` 就能够如愿以偿。这三种顶级 slice 的含义如下: 43 | 44 | - **system.slice** —— 所有系统 service 的默认位置 45 | - **user.slice** —— 所有用户会话的默认位置。每个用户会话都会在该 slice 下面创建一个子 slice,如果同一个用户多次登录该系统,仍然会使用相同的子 slice。 46 | - **machine.slice** —— 所有虚拟机和 Linux 容器的默认位置 47 | 48 | 控制 CPU 资源使用的其中一种方法是 `shares`。shares 用来设置 CPU 的相对值(你可以理解为权重),并且是针对所有的 CPU(内核),默认值是 1024。因此在上图中,httpd, sshd, crond 和 gdm 的 CPU shares 均为 `1024`,System, User 和 Machine 的 CPU shares 也是 `1024`。 49 | 50 | 假设该系统上运行了 `4` 个 service,登录了两个用户,还运行了一个虚拟机。**同时假设每个进程都要求使用尽可能多的 CPU 资源(每个进程都很繁忙)。** 51 | 52 | - `system.slice` 会获得 `33.333%` 的 CPU 使用时间,其中每个 service 都会从 system.slice 分配的资源中获得 `1/4` 的 CPU 使用时间,即 `8.25%` 的 CPU 使用时间。 53 | - `user.slice` 会获得 `33.333%` 的 CPU 使用时间,其中每个登录的用户都会获得 `16.5%` 的 CPU 使用时间。假设有两个用户:`tom` 和 `jack`,如果 tom 注销登录或者杀死该用户会话下的所有进程,jack 就能够使用 `33.333%` 的 CPU 使用时间。 54 | - `machine.slice` 会获得 `33.333%` 的 CPU 使用时间,如果虚拟机被关闭或处于 idle 状态,那么 system.slice 和 user.slice 就会从这 `33.333%` 的 CPU 资源里分别获得 `50%` 的 CPU 资源,然后均分给它们的子 slice。 55 | 56 | 如果想严格控制 CPU 资源,设置 CPU 资源的使用上限,即不管 CPU 是否繁忙,对 CPU 资源的使用都不能超过这个上限。可以通过以下两个参数来设置: 57 | 58 | ```c 59 | cpu.cfs_period_us = 统计CPU使用时间的周期,单位是微秒(us) 60 | cpu.cfs_quota_us = 周期内允许占用的CPU时间(指单核的时间,多核则需要在设置时累加) 61 | ``` 62 | 63 | systemctl 可以通过 `CPUQuota` 参数来设置 CPU 资源的使用上限。例如,如果你想将用户 tom 的 CPU 资源使用上限设置为 `20%`,可以执行以下命令: 64 | 65 | ```shell 66 | $ systemctl set-property user-1000.slice CPUQuota=20% 67 | ``` 68 | 69 | 在使用命令 `systemctl set-property` 时,可以使用 tab 补全: 70 | 71 | ```shell 72 | $ systemctl set-property user-1000.slice 73 | AccuracySec= CPUAccounting= Environment= LimitCPU= LimitNICE= LimitSIGPENDING= SendSIGKILL= 74 | BlockIOAccounting= CPUQuota= Group= LimitDATA= LimitNOFILE= LimitSTACK= User= 75 | BlockIODeviceWeight= CPUShares= KillMode= LimitFSIZE= LimitNPROC= MemoryAccounting= WakeSystem= 76 | BlockIOReadBandwidth= DefaultDependencies= KillSignal= LimitLOCKS= LimitRSS= MemoryLimit= 77 | BlockIOWeight= DeviceAllow= LimitAS= LimitMEMLOCK= LimitRTPRIO= Nice= 78 | BlockIOWriteBandwidth= DevicePolicy= LimitCORE= LimitMSGQUEUE= LimitRTTIME= SendSIGHUP= 79 | ``` 80 | 81 | 这里有很多属性可以设置,但并不是所有的属性都是用来设置 cgroup 的,我们只需要关注 `Block`, `CPU` 和 `Memory`。 82 | 83 | 如果你想通过配置文件来设置 cgroup,`service` 可以直接在 `/etc/systemd/system/xxx.service.d` 目录下面创建相应的配置文件,`slice` 可以直接在 `/run/systemd/system/xxx.slice.d` 目录下面创建相应的配置文件。事实上通过 systemctl 命令行工具设置 cgroup 也会写到该目录下的配置文件中: 84 | 85 | ```shell 86 | $ cat /run/systemd/system/user-1000.slice.d/50-CPUQuota.conf 87 | [Slice] 88 | CPUQuota=20% 89 | ``` 90 | 91 | 查看对应的 cgroup 参数: 92 | 93 | ```shell 94 | $ cat /sys/fs/cgroup/cpu,cpuacct/user.slice/user-1000.slice/cpu.cfs_period_us 95 | 100000 96 | 97 | $ cat /sys/fs/cgroup/cpu,cpuacct/user.slice/user-1000.slice/cpu.cfs_quota_us 98 | 20000 99 | ``` 100 | 101 | 这表示用户 tom 在一个使用周期内(`100` 毫秒)可以使用 `20` 毫秒的 CPU 时间。不管 CPU 是否空闲,该用户使用的 CPU 资源都不会超过这个限制。 102 | 103 | > `CPUQuota` 的值可以超过 100%,例如:如果系统的 CPU 是多核,且 `CPUQuota` 的值为 `200%`,那么该 slice 就能够使用 2 核的 CPU 时间。 104 | 105 | ## 4. 总结 106 | 107 | ------ 108 | 109 | 本文主要介绍了 cgroup 的一些基本概念,包括其在 CentOS 系统中的默认设置和控制工具,以 CPU 为例阐述 cgroup 如何对资源进行控制。下一篇文章将会通过具体的示例来观察不同的 cgroup 设置对性能的影响。 110 | 111 | > 原文链接:https://mp.weixin.qq.com/s?__biz=MzU1MzY4NzQ1OA==&mid=2247484140&idx=1&sn=c18a86d6a2d426f4d627dafd85f5ae3a&scene=21#wechat_redirect 112 | -------------------------------------------------------------------------------- /cgroups/文章/深入理解 Linux Cgroup 系列(三):内存.md: -------------------------------------------------------------------------------- 1 | # 深入理解 Linux Cgroup 系列(三):内存 2 | 3 | 通过上篇文章的学习,我们学会了如何查看当前 cgroup 的信息,如何通过操作 /sys/fs/cgroup 目录来动态设置 cgroup,也学会了如何设置 CPU shares 和 CPU quota 来控制 slice 内部以及不同 slice 之间的 CPU 使用时间。本文将把重心转移到内存上,通过具体的示例来演示如何通过 cgroup 来限制内存的使用。 4 | 5 | **1.寻找走失内存** 6 | 7 | 上篇文章告诉我们,CPU controller 提供了两种方法来限制 CPU 使用时间,其中 `CPUShares` 用来设置相对权重,`CPUQuota` 用来限制 user、service 或 VM 的 CPU 使用时间百分比。例如:如果一个 user 同时设置了 CPUShares 和 CPUQuota,假设 CPUQuota 设置成 `50%`,那么在该 user 的 CPU 使用量达到 50% 之前,可以一直按照 CPUShares 的设置来使用 CPU。 8 | 9 | 对于内存而言,在 CentOS 7 中,systemd 已经帮我们将 memory 绑定到了 /sys/fs/cgroup/memory。`systemd` 只提供了一个参数 `MemoryLimit` 来对其进行控制,该参数表示某个 user 或 service 所能使用的物理内存总量。拿之前的用户 tom 举例, 它的 UID 是 1000,可以通过以下命令来设置: 10 | 11 | ```javascript 12 | $ systemctl set-property user-1000.slice MemoryLimit=200M 13 | ``` 14 | 15 | 现在使用用户 `tom` 登录该系统,通过 `stress` 命令产生 8 个子进程,每个进程分配 256M 内存: 16 | 17 | ```javascript 18 | $ stress --vm 8 --vm-bytes 256M 19 | ``` 20 | 21 | 按照预想,stress 进程的内存使用量已经超出了限制,此时应该会触发 `oom-killer`,但实际上进程仍在运行,这是为什么呢?我们来看一下目前占用的内存: 22 | 23 | ```javascript 24 | $ cd /sys/fs/cgroup/memory/user.slice/user-1000.slice 25 | 26 | $ cat memory.usage_in_bytes 27 | 209661952 28 | ``` 29 | 30 | 奇怪,占用的内存还不到 200M,剩下的内存都跑哪去了呢?别慌,你是否还记得 linux 系统中的内存使用除了包括物理内存,还包括交换分区,也就是 swap,我们来看看是不是 swap 搞的鬼。先停止刚刚的 stress 进程,稍等 30 秒,观察一下 swap 空间的占用情况: 31 | 32 | ```javascript 33 | $ free -h 34 | total used free shared buff/cache available 35 | Mem: 3.7G 180M 3.2G 8.9M 318M 3.3G 36 | Swap: 3.9G 512K 3.9G 37 | ``` 38 | 39 | 重新运行 stress 进程: 40 | 41 | ```javascript 42 | $ stress --vm 8 --vm-bytes 256M 43 | ``` 44 | 45 | 查看内存使用情况: 46 | 47 | ```javascript 48 | $ cat memory.usage_in_bytes 49 | 209637376 50 | ``` 51 | 52 | 发现内存占用刚好在 200M 以内。再看 swap 空间占用情况: 53 | 54 | ```javascript 55 | $ free 56 | total used free shared buff/cache available 57 | Mem: 3880876 407464 3145260 9164 328152 3220164 58 | Swap: 4063228 2031360 2031868 59 | ``` 60 | 61 | 和刚刚相比,多了 `2031360-512=2030848k`,现在基本上可以确定当进程的使用量达到限制时,内核会尝试将物理内存中的数据移动到 swap 空间中,从而让内存分配成功。我们可以精确计算出 tom 用户使用的物理内存+交换空间总量,首先需要分别查看 tom 用户的物理内存和交换空间使用量: 62 | 63 | ```javascript 64 | $ egrep "swap|rss" memory.stat 65 | rss 209637376 66 | rss_huge 0 67 | swap 1938804736 68 | total_rss 209637376 69 | total_rss_huge 0 70 | total_swap 1938804736 71 | ``` 72 | 73 | 可以看到物理内存使用量为 `209637376` 字节,swap 空间使用量为 `1938804736` 字节,总量为 `(209637376+1938804736)/1024/1024=2048` M。而 stress 进程需要的内存总量为 `256*8=2048` M,两者相等。 74 | 75 | 这个时候如果你每隔几秒就查看一次 `memory.failcnt` 文件,就会发现这个文件里面的数值一直在增长: 76 | 77 | ```javascript 78 | $ cat memory.failcnt 79 | 59390293 80 | ``` 81 | 82 | 从上面的结果可以看出,当物理内存不够时,就会触发 memory.failcnt 里面的数量加 1,但此时进程不一定会被杀死,内核会尽量将物理内存中的数据移动到 swap 空间中。 83 | 84 | **02.关闭 swap** 85 | 86 | 为了更好地观察 cgroup 对内存的控制,我们可以用户 tom 不使用 swap 空间,实现方法有以下几种: 87 | 88 | 1. 将 `memory.swappiness` 文件的值修改为 0: $ echo 0 > /sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.swappiness 这样设置完成之后,即使系统开启了交换空间,当前 cgroup 也不会使用交换空间。 89 | 2. 直接关闭系统的交换空间: $ swapoff -a 如果想永久生效,还要注释掉 `/etc/fstab` 文件中的 swap。 90 | 91 | 如果你既不想关闭系统的交换空间,又想让 tom 不使用 swap 空间,上面给出的第一个方法是有问题的: 92 | 93 | - 你只能在 tom 用户登录的时候修改 `memory.swappiness` 文件的值,因为如果 tom 用户没有登录,当前的 cgroup 就会消失。 94 | - 即使你修改了 `memory.swappiness` 文件的值,也会在重新登录后失效 95 | 96 | 如果按照常规思路去解决这个问题,可能会非常棘手,我们可以另辟蹊径,从 PAM 入手。 97 | 98 | Linux PAM([Pluggable Authentication Modules](https://cloud.tencent.com/developer/audit/support-plan/5941001?from=10680)) 是一个系统级用户认证框架,PAM 将程序开发与认证方式进行分离,程序在运行时调用附加的“认证”模块完成自己的工作。本地系统管理员通过配置选择要使用哪些认证模块,其中 `/etc/pam.d/` 目录专门用于存放 PAM 配置,用于为具体的应用程序设置独立的认证方式。例如,在用户通过 ssh 登录时,将会加载 `/etc/pam.d/sshd` 里面的策略。 99 | 100 | 从 `/etc/pam.d/sshd` 入手,我们可以先创建一个 shell 脚本: 101 | 102 | ```javascript 103 | $ cat /usr/local/bin/tom-noswap.sh 104 | #!/bin/bash 105 | 106 | if [ $PAM_USER == 'tom' ] 107 | then 108 | echo 0 > /sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.swappiness 109 | fi 110 | ``` 111 | 112 | 然后在 `/etc/pam.d/sshd` 中通过 pam_exec 调用该脚本,在 `/etc/pam.d/sshd` 的末尾添加一行,内容如下: 113 | 114 | ```javascript 115 | $ session optional pam_exec.so seteuid /usr/local/bin/tom-noswap.sh 116 | ``` 117 | 118 | 现在再使用 tom 用户登录,就会发现 `memory.swappiness` 的值变成了 0。 119 | 120 | > 这里需要注意一个前提:至少有一个用户 tom 的登录会话,且通过 `systemctl set-property user-1000.slice MemoryLimit=200M` 命令设置了 limit,`/sys/fs/cgroup/memory/user.slice/user-1000.slice` 目录才会存在。所以上面的所有操作,一定要保证至少保留一个用户 tom 的登录会话。 121 | 122 | **3.控制内存使用** 123 | 124 | 关闭了 swap 之后,我们就可以严格控制进程的内存使用量了。还是使用开头提到的例子,使用用户 tom 登录该系统,先在第一个 shell 窗口运行以下命令: 125 | 126 | ```javascript 127 | $ journalctl -f 128 | ``` 129 | 130 | 打开第二个 shell 窗口(还是 tom 用户),通过 stress 命令产生 8 个子进程,每个进程分配 256M 内存: 131 | 132 | ```javascript 133 | $ stress --vm 8 --vm-bytes 256M 134 | stress: info: [30150] dispatching hogs: 0 cpu, 0 io, 8 vm, 0 hdd 135 | stress: FAIL: [30150] (415) <-- worker 30152 got signal 9 136 | stress: WARN: [30150] (417) stress: FAIL: [30150] (415) <-- worker 30151 got signal 9 137 | stress: WARN: [30150] (417) now reaping child worker processes 138 | stress: FAIL: [30150] (415) <-- worker 30154 got signal 9 139 | stress: WARN: [30150] (417) now reaping child worker processes 140 | stress: FAIL: [30150] (415) <-- worker 30157 got signal 9 141 | stress: WARN: [30150] (417) now reaping child worker processes 142 | stress: FAIL: [30150] (415) <-- worker 30158 got signal 9 143 | stress: WARN: [30150] (417) now reaping child worker processes 144 | stress: FAIL: [30150] (451) failed run completed in 0s 145 | ``` 146 | 147 | 现在可以看到 stress 进程很快被 kill 掉了,回到第一个 shell 窗口,会输出以下信息: 148 | 149 | ![img](https://ask.qcloudimg.com/http-save/4069756/r8i71wi9ji.jpeg?imageView2/2/w/1620) 150 | 151 | 由此可见 cgroup 对内存的限制奏效了,stress 进程的内存使用量超出了限制,触发了 oom-killer,进而杀死进程。 152 | 153 | **4.更多文档** 154 | 155 | 加个小插曲,如果你想获取更多关于 cgroup 的文档,可以通过 yum 安装 `kernel-doc` 包。安装完成后,你就可以进入 `/usr/share/docs` 的子目录,查看每个 cgroup controller 的详细文档。 156 | 157 | ```javascript 158 | $ cd /usr/share/doc/kernel-doc-3.10.0/Documentation/cgroups 159 | $ ll 160 | 总用量 172 161 | 4 -r--r--r-- 1 root root 918 6月 14 02:29 00-INDEX 162 | 16 -r--r--r-- 1 root root 16355 6月 14 02:29 blkio-controller.txt 163 | 28 -r--r--r-- 1 root root 27027 6月 14 02:29 cgroups.txt 164 | 4 -r--r--r-- 1 root root 1972 6月 14 02:29 cpuacct.txt 165 | 40 -r--r--r-- 1 root root 37225 6月 14 02:29 cpusets.txt 166 | 8 -r--r--r-- 1 root root 4370 6月 14 02:29 devices.txt 167 | 8 -r--r--r-- 1 root root 4908 6月 14 02:29 freezer-subsystem.txt 168 | 4 -r--r--r-- 1 root root 1714 6月 14 02:29 hugetlb.txt 169 | 16 -r--r--r-- 1 root root 14124 6月 14 02:29 memcg_test.txt 170 | 36 -r--r--r-- 1 root root 36415 6月 14 02:29 memory.txt 171 | 4 -r--r--r-- 1 root root 1267 6月 14 02:29 net_cls.txt 172 | 4 -r--r--r-- 1 root root 2513 6月 14 02:29 net_prio.txt 173 | ``` 174 | 175 | > 原文链接:https://cloud.tencent.com/developer/article/1495300 176 | -------------------------------------------------------------------------------- /cgroups/文章/用 cgroups 管理 cpu 资源.md: -------------------------------------------------------------------------------- 1 | # 用 cgroups 管理 cpu 资源 2 | 3 | 这回说说怎样通过 cgroups 来管理 cpu 资源。先说控制进程的 cpu 使用。在一个机器上运行多个可能消耗大量资源的程序时,我们不希望出现某个程序占据了所有的资源,导致其他程序无法正常运行,或者造成系统假死无法维护。这时候用 cgroups 就可以很好地控制进程的资源占用。这里单说 cpu 资源。 4 | 5 | cgroups 里,可以用 cpu.cfs_period_us 和 cpu.cfs_quota_us 来限制该组中的所有进程在单位时间里可以使用的 cpu 时间。这里的 cfs 是完全公平调度器的缩写。cpu.cfs_period_us 就是时间周期,默认为 100000,即百毫秒。cpu.cfs_quota_us 就是在这期间内可使用的 cpu 时间,默认 -1,即无限制。 6 | 7 | 跑一个耗 cpu 的程序 8 | 9 | ``` 10 | # echo 'while True: pass'|python & 11 | [1] 1532 12 | ``` 13 | 14 | top 一下可以看到,这进程占了 100% 的 cpu 15 | 16 | ``` 17 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 18 | 1532 root 20 0 112m 3684 1708 R 99.6 0.7 0:30.42 python 19 | ... 20 | ``` 21 | 22 | 然后就来对这个进程做一下限制。先把 /foo 这个控制组的限制修改一下,然后把进程加入进去。 23 | 24 | ``` 25 | echo 50000 >/sys/fs/cgroup/cpu/foo/cpu.cfs_quota_us 26 | echo 1532 >/sys/fs/group/cpu/foo/tasks 27 | ``` 28 | 29 | 可见,修改设置只需要写入相应文件,将进程加入 cgroup 也只需将 pid 写入到其中的 tasks 文件即可。这里将 cpu.cfs_quota_us 设为 50000,相对于 cpu.cfs_period_us 的 100000 即 50%。再 top 一下看看效果。 30 | 31 | ``` 32 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 33 | 1532 root 20 0 112m 3684 1708 R 50.2 0.7 5:00.31 python 34 | ... 35 | ``` 36 | 37 | 可以看到,进程的 cpu 占用已经被成功地限制到了 50% 。这里,测试的虚拟机只有一个核心。在多核情况下,看到的值会不一样。另外,cfs_quota_us 也是可以大于 cfs_period_us 的,这主要是对于多核情况。有 n 个核时,一个控制组中的进程自然最多就能用到 n 倍的 cpu 时间。 38 | 39 | 这两个值在 cgroups 层次中是有限制的,下层的资源不能超过上层。具体的说,就是下层的 cpu.cfs_period_us 值不能小于上层的值,cpu.cfs_quota_us 值不能大于上层的值。 40 | 41 | 另外的一组 cpu.rt_period_us、cpu.rt_runtime_us 对应的是实时进程的限制,平时可能不会有机会用到。 42 | 43 | 在 cpu 子系统中,cpu.stat 就是用前面那种方法做的资源限制的统计了。nr_periods、nr_throttled 就是总共经过的周期,和其中受限制的周期。throttled_time 就是总共被控制组掐掉的 cpu 使用时间。 44 | 45 | 还有个 cpu.shares, 它也是用来限制 cpu 使用的。但是与 cpu.cfs_quota_us、cpu.cfs_period_us 有挺大区别。cpu.shares 不是限制进程能使用的绝对的 cpu 时间,而是控制各个组之间的配额。比如 46 | 47 | ``` 48 | /cpu/cpu.shares : 1024 49 | /cpu/foo/cpu.shares : 2048 50 | ``` 51 | 52 | 那么当两个组中的进程都满负荷运行时,/foo 中的进程所能占用的 cpu 就是 / 中的进程的两倍。如果再建一个 /foo/bar 的 cpu.shares 也是 1024,且也有满负荷运行的进程,那 /、/foo、/foo/bar 的 cpu 占用比就是 1:2:1 。前面说的是各自都跑满的情况。如果其他控制组中的进程闲着,那某一个组的进程完全可以用满全部 cpu。可见通常情况下,这种方式在保证公平的情况下能更充分利用资源。 53 | 54 | 此外,还可以限定进程可以使用哪些 cpu 核心。cpuset 子系统就是处理进程可以使用的 cpu 核心和内存节点,以及其他一些相关配置。这部分的很多配置都和 NUMA 有关。其中 cpuset.cpus、cpuset.mems 就是用来限制进程可以使用的 cpu 核心和内存节点的。这两个参数中 cpu 核心、内存节点都用 id 表示,之间用 “,” 分隔。比如 0,1,2 。也可以用 “-” 表示范围,如 0-3 。两者可以结合起来用。如“0-2,6,7”。在添加进程前,cpuset.cpus、cpuset.mems 必须同时设置,而且必须是兼容的,否则会出错。例如 55 | 56 | ``` 57 | # echo 0 >/sys/fs/cgroup/cpuset/foo/cpuset.cpus 58 | # echo 0 >/sys/fs/cgroup/cpuset/foo/cpuset.mems 59 | ``` 60 | 61 | 这样, /foo 中的进程只能使用 cpu0 和内存节点0。用 62 | 63 | ``` 64 | # cat /proc//status|grep '_allowed_list' 65 | ``` 66 | 67 | 可以验证效果。 68 | 69 | cgroups 除了用来限制资源使用外,还有资源统计的功能。做云计算的计费就可以用到它。有一个 cpuacct 子系统专门用来做 cpu 资源统计。cpuacct.stat 统计了该控制组中进程用户态和内核态的 cpu 使用量,单位是 USER_HZ,也就是 jiffies、cpu 滴答数。每秒的滴答数可以用 `getconf CLK_TCK` 来获取,通常是 100。将看到的值除以这个值就可以换算成秒。 70 | 71 | cpuacct.usage 和 cpuacct.usage_percpu 是该控制组中进程消耗的 cpu 时间,单位是纳秒。后者是分 cpu 统计的。 72 | 73 | P.S. 2014-4-22 74 | 75 | 发现在 SLES 11 sp2、sp3 ,对应内核版本 3.0.13、 3.0.76 中,对 cpu 子系统,将 pid 写入 cgroup.procs 不会实际生效,要写入 tasks 才行。在其他环境中,更高版本或更低版本内核上均未发现。 76 | 77 | > 原文链接:http://xiezhenye.com/2013/10/%e7%94%a8-cgroups-%e7%ae%a1%e7%90%86-cpu-%e8%b5%84%e6%ba%90.html 78 | -------------------------------------------------------------------------------- /cgroups/文章/用 cgruops 管理进程内存占用.md: -------------------------------------------------------------------------------- 1 | # 用 cgruops 管理进程内存占用 2 | 3 | cgroups 中有个 memory 子系统,用于限制和报告进程的内存使用情况。 4 | 5 | 其中,很明显有两组对应的文件,一组带 memsw ,另一组不带 6 | 7 | ``` 8 | memory.failcnt 9 | memory.limit_in_bytes 10 | memory.max_usage_in_bytes 11 | memory.usage_in_bytes 12 | 13 | memory.memsw.failcnt 14 | memory.memsw.limit_in_bytes 15 | memory.memsw.max_usage_in_bytes 16 | memory.memsw.usage_in_bytes 17 | ``` 18 | 19 | 带 memsw 的表示虚拟内存,即物理内存加交换区。不带 memsw 的那组仅包括物理内存。其中,limit_in_bytes 是用来限制内存使用的,其他的则是统计报告。 20 | 21 | ``` 22 | # echo 10485760 >/sys/fs/cgroup/memory/foo/memory.limit_in_bytes 23 | ``` 24 | 25 | 即可限制该组中的进程使用的物理内存总量不超过 10MB。对 memory.memsw.limit_in_bytes 来说,则是限制虚拟内存使用。memory.memsw.limit_in_bytes 必须大于或等于 memory.limit_in_byte。这些值还可以用更方便的 100M,20G 这样的形式来设置。要解除限制,就把这个值设为 -1 即可。 26 | 27 | 这种方式限制进程内存占用会有个风险。当进程试图占用的内存超过限制,访问内存时发生缺页,又没有足够的非活动内存页可以换出时会触发 oom ,导致进程直接被杀,从而造成可用性问题。即使关闭控制组的 oom killer,进程在内存不足的时候,虽然不会被杀,但是会长时间进入 D (等待系统调用的不可中断休眠)状态,无法继续执行,导致仍然无法服务。因此,我认为,用 memory.limit_in_bytes 或 memory.memsw.limit_in_bytes 限制进程内存占用仅应当作为一个保险,避免在进程异常时耗尽系统资源。如,预期一组进程最多只会消耗 1G 内存,那么可以设置为 1.4G 。这样在发生内存泄露等异常情况时,可以避免造成更严重问题。 28 | 29 | 在 memory 子系统中,还有一个 memory.soft_limit_in_bytes 。和 memory.limit_in_bytes 的差异是,这个限制并不会阻止进程使用超过限额的内存,只是在系统内存不足时,会优先回收超过限额的进程占用的内存,使之向限定值靠拢。 30 | 31 | 前面说控制组的 oom killer 是可以关闭的,就是通过 memory.oom_control 来实现的。`cat memory.oom_control` 可以看到当前设置以及目前是否触发了 oom 。`echo 1 >memory.oom_control` 就可以禁用 oom killer。 32 | 33 | usage_in_bytes、max_usage_in_bytes、failcnt 则分别对应 当前使用量,最高使用量和发生的缺页次数。 34 | 35 | memory 子系统中还有一个很重要的设置是 memory.use_hierarchy 这是个布尔开关,默认为 0。此时不同层次间的资源限制和使用值都是独立的。当设为 1 时,子控制组进程的内存占用也会计入父控制组,并上溯到所有 memory.use_hierarchy = 1 的祖先控制组。这样一来,所有子孙控制组的进程的资源占用都无法超过父控制组设置的资源限制。同时,在整个树中的进程的内存占用达到这个限制时,内存回收也会影响到所有子孙控制组的进程。这个值只有在还没有子控制组时才能设置。之后在其中新建的子控制组默认的 memory.use_hierarchy 也会继承父控制组的设置。 36 | 37 | memory.swappiness 则是控制内核使用交换区的倾向的。值的范围是 0 – 100。值越小,越倾向使用物理内存。设为 0 时,只有在物理内存不足时才会使用交换区。默认值是系统全局设置: /proc/sys/vm/swappiness 。 38 | 39 | memory.stat 就是内存使用情况报告了。包括当前资源总量、使用量、换页次数、活动页数量等等。 40 | 41 | > 原文链接:http://xiezhenye.com/2013/10/%e7%94%a8-cgruops-%e7%ae%a1%e7%90%86%e8%bf%9b%e7%a8%8b%e5%86%85%e5%ad%98%e5%8d%a0%e7%94%a8.html 42 | -------------------------------------------------------------------------------- /ebpf/文档/Advanced_BPF_Kernel_Features_for_the_Container_Age_FOSDEM.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Advanced_BPF_Kernel_Features_for_the_Container_Age_FOSDEM.pdf -------------------------------------------------------------------------------- /ebpf/文档/BPF to eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/BPF to eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/Calico-eBPF-Dataplane-CNCF-Webinar-Slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Calico-eBPF-Dataplane-CNCF-Webinar-Slides.pdf -------------------------------------------------------------------------------- /ebpf/文档/Combining System Visibility and Security Using eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Combining System Visibility and Security Using eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/DPDK+eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/DPDK+eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/Experience and Lessons Learned.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Experience and Lessons Learned.pdf -------------------------------------------------------------------------------- /ebpf/文档/Fast Packet Processing using eBPF and XDP.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Fast Packet Processing using eBPF and XDP.pdf -------------------------------------------------------------------------------- /ebpf/文档/Kernel Tracing With eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Kernel Tracing With eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/Kernel analysis using eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Kernel analysis using eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/Making the Linux TCP stack more extensible with eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Making the Linux TCP stack more extensible with eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/Performance Analysis Superpowers with Linux eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Performance Analysis Superpowers with Linux eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/Performance Implications of Packet Filtering with Linux eBPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/Performance Implications of Packet Filtering with Linux eBPF.pdf -------------------------------------------------------------------------------- /ebpf/文档/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ebpf/文档/The Next Linux Superpower eBPF Primer.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/The Next Linux Superpower eBPF Primer.pdf -------------------------------------------------------------------------------- /ebpf/文档/eBPF - From a Programmer’s Perspective.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/eBPF - From a Programmer’s Perspective.pdf -------------------------------------------------------------------------------- /ebpf/文档/eBPF In-kernel Virtual Machine & Cloud Computin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/eBPF In-kernel Virtual Machine & Cloud Computin.pdf -------------------------------------------------------------------------------- /ebpf/文档/eBPF for perfomance analysis and networking.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/eBPF for perfomance analysis and networking.pdf -------------------------------------------------------------------------------- /ebpf/文档/eBPF in CPU Scheduler.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/eBPF in CPU Scheduler.pdf -------------------------------------------------------------------------------- /ebpf/文档/eBPF-based Content and Computation-aware Communication for Real-time Edge Computing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/ebpf/文档/eBPF-based Content and Computation-aware Communication for Real-time Edge Computing.pdf -------------------------------------------------------------------------------- /ebpf/文章/BPF 和 eBPF 初探.md: -------------------------------------------------------------------------------- 1 | ## BPF论文笔记 2 | 3 | 该文章由 UCB 发表在 1992 年的 Winter USENIX,题目是《The BSD Packet Filter: A New Architecture for User-level Packet Capture》。 4 | 5 | BPF 全名为 BSD Packet Filter,最初被应用于网络监测,例如知名的`TCPdump` 工具中,它可以在内核态根据用户定义的规则直接过滤收到的包,相较竞争者 CSPF 更加高效。它设计了一个基于寄存器的虚拟机用来过滤包,而 CSPF 则使用的是基于栈的虚拟机。 6 | 7 | BPF 有两个组成部分: 8 | 9 | - Tap 部分负责收集数据 10 | - Filter 部分负责按规则过滤包 11 | 12 | ![img](https://forsworns.github.io/assets/img/bpf_overview.89e8308e.png) 13 | 14 | 收到包以后,驱动不仅会直接发给协议栈,还会发给 BPF 一份, BPF根据不同的filter直接“就地”进行过滤,不会再拷贝到内核中的其他buffer之后再就行处理,否则就太浪费资源了。处理后才会拷贝需要的部分到用户可以拿到的 buffer 中,用户态的应用只会看到他们需要的数据。 15 | 16 | 注意在BPF中进行处理的时候,不是一个一个包进行处理的,因为接收到包之间的时间间隔太短,使用`read`系统调用又是很费事的,所以 BPF 都是把接收到的数据打包起来进行分析,为了区分开这些数据,BPF 会包一层首部(header),用来作为数据的边界。 17 | 18 | 作者先比较了两种极端的情况,在接收所有包和拒绝所有包的情况下 BPF 和竞争者 NIT 的表现。 19 | 20 | ![img](https://forsworns.github.io/assets/img/bpf_exp1.9bdcd4a1.png) 21 | 22 | ![img](https://forsworns.github.io/assets/img/bpf_exp2.5cda3566.png) 23 | 24 | 横轴是包的大小,纵轴是平均时间开销,斜率是读写速度。y轴上的截距是包长为0时候,也即对于每个包来说,固定的调用 filter 的开销。由于需要分配和初始化 buffer,NIT 调用时长在 80-100 微秒,而 BPF 则只要 5 微秒。除此以外,随着包长增加,可以看到当接受所有包时,虽然都会将包拷贝到 buffer 中,BPF 要更快一些。同时,当拒绝所有包的时候,由于 BPF 直接就地过滤掉了所有的包,不需要任何拷贝,所以它的开销几乎是常数,即固有的 filter 调用时间。 25 | 26 | 在网络监测中,一般来说(除非开启混乱模式),丢弃的信息要多于需要的信息,因此 BPF 在一般情况下优势巨大。 27 | 28 | 事实上,一个 filter 就好似一个断言,或真或假,代表是否需要该包。 29 | 30 | 为了验证断言,CSPF 采用的是如下的树形结构,好处是思路清晰。但是遍历树时需要使用栈,每次或者向栈中推入常量或包的数据,或者在栈顶两个元素之间进行二元运算。在跑完整个树结构后,再读取栈顶元素,如果是非零值或栈是空的,才会接收该包,否则丢弃。就算可以使用短路运算去优化实现代码,也依然有很大问题,首先网络是分层的,包结构里有很多首部,逐层嵌套,每次进行判断都要重新拆解包。其次接收也是接收一整个包,而不去考虑会有很多不需要的数据,明显是比 BPF 低效的。 31 | 32 | ![img](https://forsworns.github.io/assets/img/bpf_tree.1be83877.png) 33 | 34 | 而 BPF 则使用了下图的 CFG(Control Flow Graph), CFG 是一个 DAG (Directed Acyclic Graph),左边分支是说明节点是 false,右边分支是说明节点是 true。该结构运算时更多地使用寄存器,这也是一个更快速的原因。该方法的问题就是 DAG 怎么构造,怎么排序,这本身是另一个算法问题了,文中没有进行讨论。 35 | 36 | ![img](https://forsworns.github.io/assets/img/bpf_cfg.cf78143a.png) 37 | 38 | BPF 的虚拟机设计,没有采用三地址形式的代码,而是采用的多为二元运算、单地址的运算。它也定义了一系列如下的 32 位的运算指令。在实现时是用的宏,但是在文中为了便于阅读,用了汇编形式。注意到取址运算很多是相对于包来说的,因为本来这个虚拟机就是用来分析包的。 39 | 40 | ![image](https://user-images.githubusercontent.com/87457873/150482087-daae4825-6eef-42c0-8805-113a3280be4f.png) 41 | 42 | ![img](https://forsworns.github.io/assets/img/bpf_instruction.430549d3.png) 43 | 44 | ![img](https://forsworns.github.io/assets/img/bpf_addr.6f673235.png) 45 | 46 | 由于数据在包中的位置不固定,BPF 定义了一个运算来简化地址运算的步骤,即 `4*([14]&0xf)` ,其实是在分析 IP header,乘 4 是因为 offset 是字长为单位,是 4 个字节。是下面代码的缩写。 47 | 48 | ![image](https://user-images.githubusercontent.com/87457873/150482132-9774c37c-d4af-4f02-967d-18daedc40a53.png) 49 | 50 | ## eBPF (extended BPF) 51 | 52 | Linux 内核一直是实现监控/可观测性、网络和安全功能的理想环境。 不过很多情况下这并非易事,因为这些工作需要修改内核源码或加载内核模块, 最终实现形式是在已有的层层抽象之上叠加新的抽象。 eBPF 是一项革命性技术,它能在内核中运行沙箱程序(sandbox programs), 而无需修改内核源码或者加载内核模块。 53 | 54 | 将 Linux 内核变成可编程之后,就能基于现有的(而非增加新的)抽象层来打造更加智能、 功能更加丰富的基础设施软件,而不会增加系统的复杂度,也不会牺牲执行效率和安全性。 55 | 56 | ![img](https://forsworns.github.io/assets/img/ebpf_overview.1a1bb6f1.png) 57 | 58 | [Ingo Molnár](https://lkml.org/lkml/2015/4/14/232) 在 2015 年在提议合并 Linux 分支时这样描述 eBPF : 59 | 60 | > 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. 61 | 62 | ## 主要项目 63 | 64 | [**项目列表**](https://ebpf.io/projects/) 65 | 66 | ### BCC: Toolkit and library for efficient BPF-based kernel tracing 67 | 68 | BCC 是一个基于 eBPF 的高效跟踪检测内核、运行程序的工具,并且包含了须有有用的命令行工具和示例程序。BCC减轻了使用 C 语言编写 eBPF 程序的难度,它包含了一个 LLVM 之上的包裹层,前端使用 Python 和 Lua。它也提供了一个高层的库可以直接整合进应用。它适用于许多任务,包括性能分析和网络流量控制。下图是BCC给出的常见工具: 69 | 70 | ![img](https://forsworns.github.io/assets/img/bcc_tracing_tools_2019.b6d70998.png) 71 | 72 | ### bpftrace: High-level tracing language for Linux eBPF 73 | 74 | bpftrace 是一个基于 Linux eBPF 的高级编程语言。语言的设计是基于 awk 和 C,以及之前的一些 tracer 例如 DTrace 和 SystemTap。bpftrace 使用了 LLVM 作为后端,来编译 compile 脚本为 eBPF 字节码,利用 BCC 作为库和 Linux eBPF 子系统、已有的监测功能、eBPF 附着点交互。 75 | 76 | ### Cilium: eBPF-based Networking, Security, and Observability 77 | 78 | Cilium 是一个开源项目提供了借助 eBPF 增强的网络、安全和监测功能。它从根本上被专门设计成了将 eBPF 融入到 Kubernetes (k8s)并且强调了容器新的规模化、安全性、透明性需求。 79 | 80 | ### Falco: Cloud Native Runtime Security 81 | 82 | Falco 是一个行为监测器,用来监测应用中的反常行为。在 eBPF 的帮助下,Falco 在 Linux 内核中审计了系统。它将收集到的数据和其他输入例如容器运行时的评价标准和 Kubernetes 的评价标准聚合,允许持续不断地对容器、应用、主机和网络进行监测。 83 | 84 | ### Katran: A high performance layer 4 load balancer 85 | 86 | Katran 是一个 C++ 库和 eBPF 程序,可以用来建立高性能的 layer 4 负载均衡转发屏幕。Katran 利用Linux 内核中的 XDP 基础构件来提供一个核内的快速包处理功能。它的性能随着网卡接收队列数量线性增长,它也可以使用 RSS 来做 L7 的负载均衡。 87 | 88 | ## 核心架构 89 | 90 | ### Linux Kernel (eBPF Runtime) 91 | 92 | Linux kernel 包含了需要运行 eBPF 程序的 eBPF 运行时。它实现了 `bpf(2)` 系统调用来和程序、[BTF](https://forsworns.github.io/zh/blogs/20210311/#BTF) 和可以运行 eBPF 程序的各种挂载点进行交互。内核包含了一个 eBPF 验证器,来做安全检测,以及一个 JIT 编译器来将程序直接转换成原生的机器码。用户空间的工具例如 bpftool 和 libbpf 都作为上游会被内核团队维护。 93 | 94 | ### LLVM Compiler (eBPF Backend) 95 | 96 | LLVM 编译器基础构件包含了 eBPF 后端,能够将类似 C 语言语法书写出的程序转换到 eBPF 指令。LLVM 生成了 eBPF ELF 可执行文件,包含了程序码、映射描述、位置信息和 BTF 元数据。这些 ELF 文件包含了所有 eBPF loader 必须的信息例如 libbpf,来在 Linux 内核中准备和加载程序。LLVM 项目也包含了其他开发者工具例如 eBPF object file disassembler。 97 | 98 | ## eBPF 库 99 | 100 | ### libbpf 101 | 102 | libbpf 是一个基于 C/C++ 的库,由 Linux 开发团队维护。它包含了一个 eBPF loader,接管处理 LLVM 生成的 eBPF ELF 可执行文件,加载到内核中。它支持了 BCC 中没有的特性例如全局变量和 BPF skeletons。 103 | 104 | Libbpf 可以支持构建单次编译任意执行(CO-RE)的应用,但是,和 BCC 不同,不需要构建 Clang/LLVM 运行时,也不需要获取 kernel-devel 头文件。但是使用 CO-RE 特性需要内核支持 [BTF](https://forsworns.github.io/zh/blogs/20210311/#BTF),下面一些主要的 Linux 发行版已经带有了 BTF : 105 | 106 | - Fedora 31+ 107 | - RHEL 8.2+ 108 | - Arch Linux (from kernel 5.7.1.arch1-1) 109 | - Ubuntu 20.10 110 | - Debian 11 (amd64/arm64) 111 | 112 | 可以通过搜索相关文件查看内核是否实现了 [BTF](https://forsworns.github.io/zh/blogs/20210311/#BTF) 支持: 113 | 114 | ```bash 115 | ls -la /sys/kernel/btf/vmlinux 116 | ``` 117 | 118 | ### libbpf-rs & redbpf 119 | 120 | libbpf-rs 是一个安全的、符合 Rust 语法的 libbpf API 包裹层。libbpf-rs 和 libbpf-cargo(cargo 的插件)运行开发者编写的 CO-RE 的 eBPF 程序。redbpf 是一个 Rust eBPF 工具链,包含了一系列 Rust 库来编写 eBPF 程序。 121 | 122 | # 补充记录:SystemTap 123 | 124 | 以前用过的 SystemTap 是基于 Kprobe 实现的。SystemTap的框架允许用户开发简单的脚本,用于调查和监视内核空间中发生的各种内核函数,系统调用和其他事件。它是一个允许用户开发自己的特定于内核的取证和监视工具的系统。工作原理是通过将脚本语句翻译成C语句,编译成内核模块。模块加载之后,将所有探测的事件以钩子的方式挂到内核上,当任何处理器上的某个事件发生时,相应钩子上句柄就会被执行。最后,当systemtap会话结束之后,钩子从内核上取下,移除模块。整个过程用一个命令 stap 就可以完成。 125 | 126 | # 论文 127 | 128 | ## ATC18: The design and implementation of hyperupcalls 129 | 130 | 使用 ebpf 在 hypervisor 中运行客户机的经过验证代码。 131 | 132 | Hypervisor 往往把 Guest 视为黑箱,二者的交互需要 Context Switch 做中转。也有一些不需要 Context Switch 的设计,但是一侧数据结构发生改变,另一侧的代码也要更新,难以维护。由 133 | 134 | 注册步骤。客户机将 C 代码编译到了可信的 eBPF 字节码,其中可能引用了客户机的数据结构。 客户机将生成的字节码注入到 hypervisor 中,验证安全性、将它编译到原生的指令上,加入到虚拟机的 hyperupcall 列表中。 135 | 136 | 执行步骤,当某个事件发生,触发 hyperupcall,可以获取并更新客户机的数据结构。 137 | 138 | ## TON: A framework for eBPF-based network functions in an era of microservices 139 | 140 | [Polycube](https://forsworns.github.io/zh/blogs/20210311/polycube-network.readthedocs.io) 的设计文章,2021 年发表在期刊 TON 上。 141 | 142 | 微服务框架。Polycube 将网络功能统一抽象成 cube,在用户空间创建一个对于service不可见的守护进程统一进行管理,当收到 REST (Representational State Transfer) API 形式的请求,会首先发给这个守护进程,它作为代理,将请求分发到某个 service 的不同实例上,把回复返还给请求方。 143 | 144 | Network Functions Virtualization 网桥,路由器,NAT,负载平衡器,防火墙,DDoS缓解器现在都可以通过软件形式实现。但是他们往往都是 bypass 内核的。Polycube 希望通过 eBPF 动态地在内核中注册 Network Functions。 145 | 146 | Polycube 组合各个网络功能来构建任意服务链,并提供到名称空间,容器,虚拟机和物理主机的自定义网络连接。Polycube 还支持多租户,可以同时启用多个虚拟网络 [11]。 147 | 148 | 当时分享时被问到的一个问题是和 Cilium 有什么区别,orz 还是没搞清,之后再看吧。 149 | 150 | Linux 处理 TCP 包有两条路径,fast path 和 slow path。使用快速路径只进行最少的处理,如处理数据段、发生ACK、存储时间戳等。使用慢速路径可以处理乱序数据段、PAWS(Protect Againest Wrapped Sequence numbers,解决在高带宽下,TCP序列号在一次会话中可能被重复使用而带来的问题)、socket内存管理和紧急数据等。::: 151 | 152 | ## Bringing the Power of eBPF to Open vSwitch 153 | 154 | 发表在 2018 年的 Linux Plumbers Conference 上,Open vSwitch 是一个运行在 Linux 下的软件定义交换机,它最初是通过内核中的 openvswitch.ko 这个模块实现的,但是项目组现在开辟了两个新的项目,OVS-eBPF 和 OVS-AFXDP。前者的目的是使用 eBPF 重写现有的流量处理功能,attach 到了 TC 的事件上;而后者则是使用 AF_XDP 套接字 bypass 掉内核,把流量处理转移到用户空间。 155 | 156 | OVS eBPF datapath 包含多个 eBPF 程序和用户态的 ovs-vswitchd 作为控制平面。eBPF 程序是通过尾调用相连的,eBPF maps 在这些 eBPF 程序和用户空间应用之间是共享的。 157 | 158 | ## OSDI20: hXDP: Efficient Software Packet Processing on FPGA NICs 159 | 160 | 在 FPGA 上实现 eBPF/XDP,削减 eBPF 指令集、并行执行,最终在时钟频率为 156MHz 的 FPGA 上达到 GHZ CPU 处理包的速度。 161 | 162 | ## CONEXT 19: RSS++ load and state-aware receive side scaling 163 | 164 | 接收方缩放(Receive Side Scaling,RSS)是一种网络驱动程序技术,可在多处理器系统中的多个CPU之间有效分配网络接收处理。 接收方缩放(RSS)也称为多队列接收,它在多个基于硬件的接收队列之间分配网络接收处理,从而允许多个CPU处理入站网络流量。RSS可用于缓解单个CPU过载导致的接收中断处理瓶颈,并减少网络延迟。 它的作用是在每个传入的数据包上发出带有预定义哈希键的哈希函数。哈希函数将数据包的IP地址,协议(UDP或TCP)和端口(5个元组)作为键并计算哈希值。(如果配置的话,RSS哈希函数只能使用2,3或4个元组来创建密钥)。 哈希值的多个最低有效位(LSB)用于索引间接表。间接表中的值用于将接收到的数据分配给CPU。 165 | 166 | 传统 RSS 只依赖哈希,分布可能是不均匀的;一般的负载均衡是关注服务器之间的,这里的是关注的单机的多核负载均衡。在接收时,某个CPU可能缓存更多的包,导致占用率过高,出现丢包的情况,延迟也会相应增加 167 | 168 | 通过动态修改 RSS 表在 CPU 核心之间分配流量。RSS++摆脱了延迟分布的长尾,提升了CPU利用率,还支持削减参与到包转发过程中的 CPU 核心数,避免核心数过量。比如左边图片,第三段,自动空余出了 CPU。 169 | 170 | > 原文链接:https://forsworns.github.io/zh/blogs/20210311/ 171 | -------------------------------------------------------------------------------- /ebpf/文章/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ebpf/文章/eBPF 如何简化服务网格.md: -------------------------------------------------------------------------------- 1 | # eBPF 如何简化服务网格 2 | 3 | 今天有几个服务网格的产品和项目,承诺简化应用微服务之间的连接,同时提供额外的功能,如安全连接、可观察性和流量管理。但正如我们在过去几年中反复看到的那样,对服务网格的兴奋已经被对额外的复杂性和开销的实际担忧所抑制。让我们来探讨一下 eBPF 是如何让我们精简服务网格,使服务网格的数据平面更有效率,更容易部署。 4 | 5 | ### Sidecar 问题 6 | 7 | 今天的 Kubernetes 服务网格解决方案要求你在每一个应用 Pod 上添加一个代理 sidecar 容器,如 Envoy 或 Linkerd-proxy。这是正确的:即使在一个非常小的环境中,比如说有 20 个服务,每个服务运行五个 Pod,分布在三个节点上,你也有 100 个代理容器。无论代理的实现多么小和有效,这种纯粹的重复都会耗费资源。 8 | 9 | 每个代理使用的内存与它需要能够通信的服务数量有关。Pranay Singhal 写了他配置 Istio 的经验,将每个代理的消耗从 1GB 左右减少到更合理的 60-70MB。但是,即使在我们的小环境中,在三个节点上有 100 个代理,这种优化配置仍然需要每个节点 2GB 左右。 10 | 11 | [![1.jpg](http://dockone.io/uploads/article/20211028/e7b5d65d71e4df4685c41621e097f9f6.jpg)](http://dockone.io/uploads/article/20211028/e7b5d65d71e4df4685c41621e097f9f6.jpg) 12 | 13 | 14 | *来自redhat.com/architect/why-when-service-mesh——每个微服务都有自己的代理sidecar* 15 | 16 | 为什么我们需要所有这些 sidecar?这种模式允许代理容器与 Pod 中的应用容器共享一个网络命名空间。网络命名空间是 Linux 内核的结构,它允许容器和 Pod 拥有自己独立的网络堆栈,将容器化的应用程序相互隔离。这使得应用之间互不相干,这就是为什么你可以让尽可能多的 Pod 在 80 端口上运行一个 Web 应用 —— 网络命名空间意味着它们各自拥有自己的 80 端口。代理必须共享相同的网络命名空间,这样它就可以拦截和处理进出应用容器的流量。 17 | 18 | ### 引入 eBPF 19 | 20 | eBPF 是一种内核技术,允许自定义程序在内核中运行。这些程序在响应事件时运行,有成千上万个可能的事件,eBPF 程序可以被附加到这些事件上。这些事件包括轨迹点、进入或退出任何功能(在内核或用户空间)或对服务网格来说很重要的 —— 抵达的网络数据包。 21 | 22 | 重要的是,**每个节点只有一个内核**;在一个节点上运行的所有容器(也就是所有的 Pod)共享同一个内核。如果你在内核中添加一个 eBPF 程序到一个事件中,它将被触发,无论哪个进程引起该事件,无论它是在应用容器中运行还是直接运行在主机上。 23 | 24 | [![2.jpg](http://dockone.io/uploads/article/20211028/e6f2329df39f0ed2502130efc046ddf3.jpg)](http://dockone.io/uploads/article/20211028/e6f2329df39f0ed2502130efc046ddf3.jpg) 25 | 26 | 27 | *每台主机一个内核* 28 | 29 | 这就是为什么 eBPF 对于 Kubernetes 中的任何一种 instrumentation 来说都是如此令人兴奋的技术 —— 你只需要在每个节点上添加一次 instrumentation ,所有的应用程序 pod 都会被覆盖。无论你是在寻求可观察性、安全性还是网络,由 eBPF 驱动的解决方案都可以在不需要 sidecar 的情况下对应用进行检测。 30 | 31 | 基于 eBPF 的 Cilium 项目(最近 以孵化级别加入云计算基金会)将这种 “无 sidecar” 模式带到了服务网格的世界。除了传统的 sidecar 模型,Cilium 还支持每个节点使用一个 Envoy 代理实例运行服务网格的数据平面。使用我们前面的例子,这就把代理实例的数量从 100 个减少到只有 3 个。 32 | 33 | ![image](https://user-images.githubusercontent.com/87457873/150500486-c763fe1e-f4d8-42d2-ab45-7d402ba99d5f.png) 34 | 35 | 36 | *用无 sidecar 代理模式减少代理实例* 37 | 38 | ### 减少 YAML 39 | 40 | 在 sidecar 模型中,指定每个应用 Pod 的 YAML 需要被修改以添加 sidecar 容器。这通常是自动化的 —— 例如,使用一个 mutating webhook,在每个应用 Pod 部署的时候注入 sidecar。 41 | 42 | 以 Istio 为例,这需要标记 Kubernetes 命名空间和/或 Pod,以定义是否应该注入 sidecar—— 当然也需要为集群启用 mutating webhook。 43 | 44 | 但如果出了问题怎么办?如果命名空间或 Pod 的标签不正确,那么 sidecar 将不会被注入,Pod 将不会被连接到服务网格。更糟糕的是,如果攻击者破坏了集群,并能够运行一个恶意的工作负载 —— 例如,一个加密货币矿工,他们将不太可能标记它,以便它加入服务网格。它不会通过服务网格提供的流量观察能力而被发现。 45 | 46 | 相比之下,在支持 eBPF 的无 sidecar 代理模型中,pod 不需要任何额外的 YAML 就可以被检测。相反,一个 CRD 被用来在集群范围内配置服务网格。即使是已经存在的 pod 也可以成为服务网格的一部分,而不需要重新启动。 47 | 48 | 如果攻击者试图通过直接在主机上运行工作负载来绕过 Kubernetes 编排,eBPF 程序可以检测并控制这一活动,因为这一切都可以从内核看到。 49 | 50 | ### eBPF 支持的网络效率 51 | 52 | 支持 eBPF 的网络允许数据包走捷径,绕过内核的部分网络堆栈,这可以使 Kubernetes 网络的性能得到显著改善。让我们看看这在服务网格数据平面中是如何应用的。 53 | 54 | [![4.jpg](http://dockone.io/uploads/article/20211028/c2313d4864abca511ae2f546845afe27.jpg)](http://dockone.io/uploads/article/20211028/c2313d4864abca511ae2f546845afe27.jpg) 55 | 56 | 57 | *在 eBPF 加速、无 sidecar 的服务网格模型中,网络数据包通过的路径要短得多* 58 | 59 | 在服务网格的情况下,代理在传统网络中作为 sidecar 运行,数据包到达应用程序的路径相当曲折:入站数据包必须穿越主机 TCP/IP 栈,通过虚拟以太网连接到达 Pod 的网络命名空间。从那里,数据包必须穿过 Pod 的网络堆栈到达代理,代理将数据包通过回环接口转发到应用程序。考虑到流量必须在连接的两端流经代理,与非服务网格流量相比,这将导致延迟的显著增加。 60 | 61 | 基于 eBPF 的 Kubernetes CNI 实现,如 Cilium,可以使用 eBPF 程序,明智地钩住内核中的特定点,沿着更直接的路线重定向数据包。这是可能的,因为 Cilium 知道所有的 Kubernetes 端点和服务的身份。当数据包到达主机时,Cilium 可以将其直接分配到它所要去的代理或 Pod 端点。 62 | 63 | ### 网络中的加密 64 | 65 | 如果一个网络解决方案能够意识到 Kubernetes 服务,并在这些服务的端点之间提供网络连接,那么它能够提供服务网格数据平面的能力就不足为奇。但这些能力可以超越基本的连接。一个例子是透明加密。 66 | 67 | 通常使用服务网格来确保所有的应用流量都是经过认证和加密的。这是通过双向 TLS(mTLS)实现的;服务网格代理组件作为网络连接的端点,并与其远程对等物协商一个安全的 TLS 连接。这种连接对代理之间的通信进行加密,而不需要对应用程序做任何改变。 68 | 69 | 但在应用层管理的 TLS 并不是实现组件间认证和加密流量的唯一方法。另一个选择是在网络层加密流量,使用 IPSec 或 WireGuard。因为它在网络层操作,这种加密不仅对应用程序完全透明,而且对代理也是透明的 —— 它可以在有或没有服务网格时启用。如果你使用服务网格的唯一原因是提供加密,你可能想考虑网络级加密。它不仅更简单,而且还可以用来验证和加密节点上的任何流量 —— 它不只限于那些启用了 sidecar 的工作负载。 70 | 71 | ### eBPF 是服务网格的数据平面 72 | 73 | 现在,eBPF 在 Linux 生产发行版使用的内核版本中得到广泛支持,企业可以利用它来获得更有效的网络解决方案,并作为服务网格的更有效的数据平面。 74 | 75 | 去年,我代表 CNCF 的技术监督委员会,对服务网格领域的整合和清晰化做了一些预测。在同一主题演讲中,我谈到 eBPF 有可能成为更多项目和更广泛部署能力的基础。这两个想法现在正结合在一起,因为 eBPF 似乎是服务网格数据平面的自然路径。 76 | 77 | 原文链接:https://mp.weixin.qq.com/s/4Ug8OBuhkO8ExMaR57ruZQ 78 | -------------------------------------------------------------------------------- /ebpf/文章/eBPF 概述,第 1 部分:介绍.md: -------------------------------------------------------------------------------- 1 | ## 1. 前言 2 | 3 | **有兴趣了解更多关于 eBPF 技术的底层细节?那么请继续移步,我们将深入研究 eBPF 的底层细节,从其虚拟机机制和工具,到在远程资源受限的嵌入式设备上运行跟踪。** 4 | 5 | 注意:本系列博客文章将集中在 eBPF 技术,因此对于我们来讲,文中 BPF 和 eBPF 等同,可相互使用。BPF 名字/缩写已经没有太大的意义,因为这个项目的发展远远超出了它最初的范围。BPF 和 eBPF 在该系列中会交替使用。 6 | 7 | - [第 1 部分](https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/)和[第 2 部分](https://www.collabora.com/news-and-blog/blog/2019/04/15/an-ebpf-overview-part-2-machine-and-bytecode/) 为新人或那些希望通过深入了解 eBPF 技术栈的底层技术来进一步了解 eBPF 技术的人提供了深入介绍。 8 | - [第 3 部分](https://www.collabora.com/news-and-blog/blog/2019/04/26/an-ebpf-overview-part-3-walking-up-the-software-stack/)是对用户空间工具的概述,旨在提高生产力,建立在第 1 部分和第 2 部分中介绍的底层虚拟机机制之上。 9 | - [第 4 部分](https://www.collabora.com/news-and-blog/blog/2019/05/06/an-ebpf-overview-part-4-working-with-embedded-systems/)侧重于在资源有限的嵌入式系统上运行 eBPF 程序,在嵌入式系统中完整的工具链技术栈(BCC/LLVM/python 等)是不可行的。我们将使用占用资源较小的嵌入式工具在 32 位 ARM 上交叉编译和运行 eBPF 程序。只对该部分感兴趣的读者可选择跳过其他部分。 10 | - [第 5 部分](https://www.collabora.com/news-and-blog/blog/2019/05/14/an-ebpf-overview-part-5-tracing-user-processes/)是关于用户空间追踪。到目前为止,我们的努力都集中在内核追踪上,所以是时候我们关注一下用户进程了。 11 | 12 | 如有疑问时,可使用该流程图: 13 | 14 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-flowchart.png) 15 | 16 | ## 2. eBPF 是什么? 17 | 18 | eBPF 是一个基于寄存器的虚拟机,使用自定义的 64 位 RISC 指令集,能够在 Linux 内核内运行即时本地编译的 "BPF 程序",并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使 Linux 能够作为其他虚拟机的管理程序。eBPF 也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块([LTTng](https://lttng.org/docs/v2.10/#doc-lttng-modules) 或 [SystemTap](https://kernelnewbies.org/SystemTap)),而且几乎所有的 Linux 发行版都默认启用。熟悉 DTrace 的读者可能会发现 [DTrace/BPFtrace 对比](http://www.brendangregg.com/blog/2018-10-08/dtrace-for-linux-2018.html)非常有用。 19 | 20 | 在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然 eBPF 程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情 - 这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF 经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地 JIT 编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的 [eBPF 参考](http://www.brendangregg.com/ebpf.html)中找到。 21 | 22 | 基于设计,eBPF 虚拟机和其程序有意地设计为**不是**图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragma unroll 指令】),所以每个 eBPF 程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个 MOV 指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有 BPF_MAXINSNS 指令(默认 4096)、"主"函数需要一个参数(context)等等。当 eBPF 程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。 23 | 24 | > 译者注: BPF_MAXINSNS 这个限制已经被放宽至 100 万条指令(BPF_COMPLEXITY_LIMIT_INSNS),但是非特权执行的 BPF 程序这个限制仍然会保留。 25 | 26 | 历史上,eBPF (cBPF) 虚拟机只在内核中可用,用于过滤网络数据包,与用户空间程序没有交互,因此被称为 "伯克利数据包过滤器"【译者注:早期的 BPF 实现被称为经典 cBPF】。从内核 v3.18(2014 年)开始,该虚拟机也通过 [bpf() syscall](https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf) 和[uapi/linux/bpf.h](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h) 暴露在用户空间,这导致其指令集在当时被冻结,成为公共 ABI,尽管后来仍然可以(并且已经)添加新指令。 27 | 28 | 因为内核内的 eBPF 实现是根据 GPLv2 授权的,它不能轻易地被非 GPL 用户重新分发,所以也有一个替代的 Apache 授权的用户空间 eBPF 虚拟机实现,称为 "uBPF"。撇开法律条文不谈,基于用户空间的实现对于追踪那些需要避免内核-用户空间上下文切换成本的性能关键型应用很有用。 29 | 30 | ## 3. eBPF 是怎么工作的? 31 | 32 | eBPF 程序在事件触发时由内核运行,所以可以被看作是一种函数挂钩或事件驱动的编程形式。从用户空间运行按需 eBPF 程序的价值较小,因为所有的按需用户调用已经通过正常的非 VM 内核 API 调用("syscalls")来处理,这里 VM 字节码带来的价值很小。事件可由 kprobes/uprobes、tracepoints、dtrace probes、socket 等产生。这允许在内核和用户进程的指令中钩住(hook)和检查任何函数的内存、拦截文件操作、检查特定的网络数据包等等。一个比较好的参考是 [Linux 内核版本对应的 BPF 功能](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md)。 33 | 34 | 如前所述,事件触发了附加的 eBPF 程序的执行,后续可以将信息保存至 map 和环形缓冲区(ringbuffer)或调用一些特定 API 定义的内核函数的子集。一个 eBPF 程序可以链接到多个事件,不同的 eBPF 程序也可以访问相同的 map 以共享数据。一个被称为 "program array" 的特殊读/写 map 存储了对通过 bpf() 系统调用加载的其他 eBPF 程序的引用,在该 map 中成功的查找则会触发一个跳转,而且并不返回到原来的 eBPF 程序。这种 eBPF 嵌套也有限制,以避免无限的递归循环。 35 | 36 | 运行 eBPF 程序的步骤: 37 | 38 | 1. 用户空间将字节码和程序类型一起发送到内核,程序类型决定了可以访问的内核区域【译者注:主要是 BPF 帮助函数的各种子集】。 39 | 2. 内核在字节码上运行验证器,以确保程序可以安全运行(kernel/bpf/verifier.c)。 40 | 3. 内核将字节码编译为本地代码,并将其插入(或附加到)指定的代码位置。【译者注:如果启用了 JIT 功能,字节码编译为本地代码】。 41 | 4. 插入的代码将数据写入环形缓冲区或通用键值 map。 42 | 5. 用户空间从共享 map 或环形缓冲区中读取结果值。 43 | 44 | map 和环形缓冲区结构是由内核管理的(就像管道和 FIFO 一样),独立于挂载的 eBPF 或访问它们的用户程序。对 map 和环形缓冲区结构的访问是异步的,通过文件描述符和引用计数实现,可确保只要有至少一个程序还在访问,结构就能够存在。加载的 JIT 后代码通常在加载其的用户进程终止时被删除,尽管在某些情况下,它仍然可以在加载进程的生命期之后继续存在。 45 | 46 | 为了方便编写 eBPF 程序和避免进行原始的 bpf()系统调用,内核提供了方便的 [libbpf 库](https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf),包含系统调用函数包装器,如 [bpf_load_program](https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf/bpf.c#L214) 和结构定义(如 [bpf_map](https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf/libbpf.c#L157)),在 LGPL 2.1 和 BSD 2-Clause 下双重许可,可以静态链接或作为 DSO。内核代码也提供了一些使用 libbpf 简洁的例子,位于目录 [samples/bpf/](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/) 中。 47 | 48 | ## 4. 样例学习 49 | 50 | 内核开发者非常可怜,因为内核是一个独立的项目,因而没有用户空间诸如 Glibc、LLVM、JavaScript 和 WebAssembly 诸如此类的好东西! - 这就是为什么内核中 eBPF 例子中会包含原始字节码或通过 libbpf 加载预组装的字节码文件。我们可以在 [sock_example.c](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c) 中看到这一点,这是一个简单的用户空间程序,使用 eBPF 来计算环回接口上统计接收到 TCP、UDP 和 ICMP 协议包的数量。 51 | 52 | 我们跳过微不足道的的 [main](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L98) 和 [open_raw_sock](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.h#L13) 函数,而专注于神奇的代码 [test_sock](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L35)。 53 | 54 | ```c 55 | static int test_sock(void) 56 | { 57 | int sock = -1, map_fd, prog_fd, i, key; 58 | long long value = 0, tcp_cnt, udp_cnt, icmp_cnt; 59 | 60 | map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 256, 0); 61 | if (map_fd < 0) {printf("failed to create map'%s'\n", strerror(errno)); 62 | goto cleanup; 63 | } 64 | 65 | struct bpf_insn prog[] = {BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), 66 | BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */), 67 | BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ 68 | BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), 69 | BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ 70 | BPF_LD_MAP_FD(BPF_REG_1, map_fd), 71 | BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), 72 | BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), 73 | BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ 74 | BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */ 75 | BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ 76 | BPF_EXIT_INSN(),}; 77 | size_t insns_cnt = sizeof(prog) / sizeof(struct bpf_insn); 78 | 79 | prog_fd = bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, prog, insns_cnt, 80 | "GPL", 0, bpf_log_buf, BPF_LOG_BUF_SIZE); 81 | if (prog_fd < 0) {printf("failed to load prog'%s'\n", strerror(errno)); 82 | goto cleanup; 83 | } 84 | 85 | sock = open_raw_sock("lo"); 86 | 87 | if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0) {printf("setsockopt %s\n", strerror(errno)); 88 | goto cleanup; 89 | } 90 | ``` 91 | 92 | 首先,通过 libbpf API 创建一个 BPF map,该行为就像一个最大 256 个元素的固定大小的数组。按 [IPROTO_*](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/in.h#L28) 定义的键索引网络协议(2 字节的 word),值代表各自的数据包计数(4 字节大小)。除了数组,eBPF 映射还实现了[其他数据结构类型](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L113),如栈或队列。 93 | 94 | 接下来,eBPF 的字节码指令数组使用方便的[内核宏](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_insn.h)进行定义。在这里,我们不会讨论字节码的细节(这将在第 2 部分描述机器后进行)。更高的层次上,字节码从数据包缓冲区中读取协议字,在 map 中查找,并增加特定的数据包计数。 95 | 96 | 然后 BPF 字节码被加载到内核中,并通过 libbpf 的 bpf_load_program 返回 fd 引用来验证正确/安全。调用指定了 eBPF [程序类型](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L138),这决定了它可以访问哪些内核子集。因为样例是一个 SOCKET_FILTER 类型,因此提供了一个指向当前网络包的参数。最后,eBPF 的字节码通过套接字层被附加到一个特定的原始套接字上,之后在原始套接字上接受到的每一个数据包运行 eBPF 字节码,无论协议如何。 97 | 98 | 剩余的工作就是让用户进程开始轮询共享 map 的数据。 99 | 100 | ```c 101 | for (i = 0; i < 10; i++) { 102 | key = IPPROTO_TCP; 103 | assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); 104 | 105 | key = IPPROTO_UDP; 106 | assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0); 107 | 108 | key = IPPROTO_ICMP; 109 | assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0); 110 | 111 | printf("TCP %lld UDP %lld ICMP %lld packets\n", 112 | tcp_cnt, udp_cnt, icmp_cnt); 113 | sleep(1); 114 | } 115 | } 116 | ``` 117 | 118 | ## 5. 总结 119 | 120 | 第 1 部分介绍了 eBPF 的基础知识,我们通过如何加载字节码和与 eBPF 虚拟机通信的例子进行了讲述。由于篇幅限制,编译和运行例子作为留给读者的练习。我们也有意不去分析具体的 eBPF 字节码指令,因为这将是第 2 部分的重点。在我们研究的例子中,用户空间通过 libbpf 直接用 C 语言从内核虚拟机中读取 eBPF map 值(使用 10 次 1 秒的睡眠!),这很笨重,而且容易出错,而且很快就会变得很复杂,所以在第 3 部分,我们将研究更高级别的工具,通过脚本或特定领域的语言自动与虚拟机交互。 121 | 122 | > 原文链接:https://kubesphere.io/zh/blogs/ebpf-guide/ 123 | -------------------------------------------------------------------------------- /ebpf/文章/eBPF 概述,第 2 部分:机器和字节码.md: -------------------------------------------------------------------------------- 1 | 在我们的[第一篇文章](https://kubesphere.com.cn/blogs/ebpf-guide/)中,我们介绍了 eBPF VM、它刻意的设计限制以及如何从用户空间进程与其交互。如果您还没有阅读它,您可能需要在继续阅读本篇文章之前阅读上一篇文章,因为如果没有适当了解,直接从机器和字节码细节开始学习可能会很困难。如有疑问,请参阅第一部分开头的流程图。 2 | 3 | 本系列的第二部分更深入地研究了第一部分中研究的 eBPF VM 和程序。掌握这种底层知识不是强制性的,但对于本系列的其余部分来说是非常有用的基础,我们将在其中检查建立在这些机制之上的更高级别的工具。 4 | 5 | ## 虚拟机 6 | 7 | eBPF 是一个 RISC 寄存器机,共有 [11 个 64 位寄存器](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L45),一个程序计数器和一个 512 字节固定大小的堆栈。九个寄存器是通用读写的,一个是只读堆栈指针,程序计数器是隐式的,即我们只能跳转到计数器的某个偏移量。VM 寄存器始终为 64 位宽(即使在 32 位 ARM 处理器内核中运行!)并且如果最高有效的 32 位为零,则支持 32 位子寄存器寻址 - 这将在第四部分在嵌入式设备上交叉编译和运行 eBPF 程序非常有用。 8 | 9 | 这些寄存器是: 10 | 11 | | | | 12 | | :-------- | :--------------------------------------------------------- | 13 | | r0: | 存储函数调用和当前程序退出代码的返回值 | 14 | | r1 - r5: | 作为函数调用的参数,在程序开始时 r1 包含 “上下文” 参数指针 | 15 | | r6 - r9: | 这些在内核函数调用之间被保留 | 16 | | r10: | 每个 eBPF 程序512字节堆栈的只读指针 | 17 | 18 | 在加载时提供的 eBPF [程序类型](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L136)准确地决定了哪些内核函数子集可以调用,以及在程序启动时通过 r1 提供的 “上下文” 参数。r0 中存储的程序退出值的含义也是由程序类型决定的。 19 | 20 | 每个函数调用在寄存器 r1 - r5 中最多可以有 5 个参数;这适用于 eBPF 到 eBPF 和内核函数的调用。寄存器 r1 - r5 只能存储数字或指向堆栈的指针(作为参数传递给函数),从不直接指向任意内存的指针。所有内存访问都必须先将数据加载到 eBPF 堆栈中,然后才能在 eBPF 程序中使用它。此限制有助于 eBPF 验证器,它简化了内存模型以实现更轻松的正确性检查。 21 | 22 | BPF 可访问的内核“辅助”函数由内核核心(不可通过模块扩展)通过类似于定义系统调用的 API 定义,使用 [BPF_CALL_*](https://github.com/torvalds/linux/blob/v4.20/include/linux/filter.h#L441) 宏。[bpf.h](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L420) 试图为所有 BPF 可访问的内核“辅助”函数提供参考。例如 [bpf_trace_printk](https://github.com/torvalds/linux/blob/v4.20/kernel/trace/bpf_trace.c#L163) 的定义使用 BPF_CALL_5 和 5 对类型/参数名称。定义[参数数据类型](https://github.com/torvalds/linux/blob/v4.20/kernel/trace/bpf_trace.c#L276)很重要,因为在每个 eBPF 程序加载时,eBPF 验证器确保寄存器数据类型与被调用方参数类型匹配。 23 | 24 | eBPF 指令也是固定大小的 64 位编码,大约 100 条指令(目前...)分为 [8 类](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf_common.h#L5)。VM 支持来自通用内存( map 、堆栈、“上下文”如数据包缓冲区等)的 1 - 8 字节加载/存储、向前/向后(非)条件跳转、算术/逻辑运算和函数调用。如需深入了解操作码格式,请参阅 Cilium 项目[指令集文档](https://cilium.readthedocs.io/en/latest/bpf/#instruction-set)。IOVisor 项目还维护了一个有用的[指令规范](https://github.com/iovisor/bpf-docs/blob/master/eBPF.md)。 25 | 26 | 在本系列第一部分研究的示例中,我们使用了一些有用的[内核宏](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_insn.h)来使用以下[结构](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L64)创建 eBPF 字节码指令数组(所有指令都以这种方式编码): 27 | 28 | ``` 29 | struct bpf_insn { 30 | __u8 代码;/* opcode */ 31 | __u8 dst_reg:4; /* dest register */ 32 | __u8 src_reg:4; /* source register */ 33 | __s16 off; /* signed offset */ 34 | __s32 imm; /* signed immediate constant */ 35 | }; 36 | 37 | msb lsb 38 | +------------------------+----------------+----+----+--------+ 39 | |immediate |offset |src |dst |opcode | 40 | +------------------------+----------------+----+----+--------+ 41 | ``` 42 | 43 | 让我们看一下 [BPF_JMP_IMM](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_insn.h#L167) 指令,它针对立即值对条件跳转进行编码。下面的宏注释对指令逻辑应该是不言自明的。操作码编码指令类 BPF_JMP 、操作(通过 BPF_OP 位域传递以确保正确性)和表示它是对立即数/常量值 BPF_K 的操作的标志进行编码。 44 | 45 | ``` 46 | #define BPF_OP(code) ((code) & 0xf0) 47 | #define BPF_K 0x00 48 | 49 | /* 针对立即数的条件跳转,if (dst_reg 'op' imm32) goto pc + off16 */ 50 | 51 | #define BPF_JMP_IMM(OP, DST, IMM, OFF) \ 52 | ((struct bpf_insn) { \ 53 | .code = BPF_JMP | BPF_OP(OP) | BPF_K, \ 54 | .dst_reg = DST, \ 55 | .src_reg = 0 56 | , \ 57 | .off = OFF, \ .imm = IMM }) 58 | ``` 59 | 60 | 如果我们计算值或反汇编包含 BPF_JMP_IMM ( BPF_JEQ , BPF_REG_0 , 0 , 2 ) 的 eBPF 字节码二进制文件,我们会发现它是 0x020015 格式。这个特定的字节码非常频繁地用于测试存储在 r0 中的函数调用的返回值;如果 r0 == 0,它会跳过接下来的 2 条指令。 61 | 62 | ## 重温我们的字节码 63 | 64 | 现在我们已经掌握了必要的知识来完全理解本系列第一部分中使用的字节码 eBPF 示例,我们将逐步解释它。请记住,[sock_example.c](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c) 是一个简单的用户态程序,它使用 eBPF 来计算在环回接口上接收到的 TCP、UDP 和 ICMP 协议数据包的数量。 65 | 66 | 在高层次上,代码所做的是从接收到的数据包中读取协议编号,然后将其推送到 eBPF 堆栈上,用作 map_lookup_elem 调用的索引,该调用获取相应协议的数据包计数。map_lookup_elem 函数采用 r0 中的索引(或键)指针和 r1 中的 map 文件描述符。如果查找调用成功,r0 将包含一个指向存储在协议索引处的 map 值的指针。然后我们原子地增加 map 值并退出。 67 | 68 | ``` 69 | BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), 70 | ``` 71 | 72 | 当 eBPF 程序启动时,上下文(在这种情况下是数据包缓冲区)由 r1 中的地址指向。r1 将在函数调用期间用作参数,因此我们也将其存储在 r6 中作为备份。 73 | 74 | ``` 75 | BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */), 76 | ``` 77 | 78 | 该指令将一个字节( BPF_B )从上下文缓冲区(在本例中为网络数据包缓冲区)中的偏移量加载到 r0 中,因此我们提供要加载到 r0 的 [iphdr 结构](https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/ip.h#L86)中的协议字节的偏移量。 79 | 80 | ``` 81 | BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ 82 | ``` 83 | 84 | 将包含先前读取的协议的字 ( BPF_W ) 压入堆栈(由 r10 指向,以偏移量 -4 字节开头)。 85 | 86 | ``` 87 | BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), 88 | BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ 89 | ``` 90 | 91 | 将堆栈地址指针移至 r2 并减去 4,因此现在 r2 指向协议值,用作下一次 map 键查找的参数。 92 | 93 | ``` 94 | BPF_LD_MAP_FD(BPF_REG_1, map_fd), 95 | ``` 96 | 97 | 将本地进程内的文件描述符引用包含协议包计数的 map 到 r1 寄存器。 98 | 99 | ``` 100 | BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), 101 | ``` 102 | 103 | 使用 r2 指向的堆栈中的协议值作为键执行 map 查找调用。结果存储在 r0 中:指向由键索引的值的指针地址。 104 | 105 | ``` 106 | BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), 107 | ``` 108 | 109 | 还记得 0x020015 格式吗?这与第一部分的字节码相同。如果 map 查找没有成功,则 r0 == 0 所以我们跳过接下来的两条指令。 110 | 111 | ``` 112 | BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ 113 | BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */ 114 | ``` 115 | 116 | 增加 r0 指向的地址处的 map 值。 117 | 118 | ``` 119 | BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ 120 | BPF_EXIT_INSN(), 121 | ``` 122 | 123 | 将 eBPF retcode 设置为 0 并退出。 124 | 125 | 尽管这个 sock_example 逻辑非常简单(它只是在 map 中增加的一些数字),但在原始字节码中实现或理解是困难的。更复杂的任务在像这样的汇编程序中完成时变得极其困难。展望未来,我们将开始使用更高级的语言和工具,以更少的工作开启更强大的 eBPF 用例。 126 | 127 | ## 总结 128 | 129 | 在这一部分中,我们仔细观察了 eBPF 虚拟机的寄存器和指令集,了解了 eBPF 可访问的内核函数是如何从字节码中调用的,以及它们是如何被核心内核通过类似 syscall 的特殊目的 API 定义的。我们也完全理解了第一部分例子中使用的字节码。还有一些未探索的领域,如创建多个 eBPF 程序函数或链式 eBPF 程序以绕过 Linux 发行版的 4096 条指令限制。也许我们会在以后的文章中探讨这些。 130 | 131 | 现在,主要的问题是编写原始字节码很困难的,这非常像编写汇编代码,而且编写效率低下。在第三部分中,我们将开始研究使用高级语言编译成 eBPF 字节码,到此为止我们已经了解了虚拟机工作的底层基础知识。 132 | -------------------------------------------------------------------------------- /ebpf/文章/eBPF 概述,第 3 部分:软件开发生态.md: -------------------------------------------------------------------------------- 1 | ## 1. 前言 2 | 3 | 在本系列的[第 1 部分](https://kubesphere.com.cn/blogs/ebpf-guide/)和[第 2 部分](https://kubesphere.com.cn/blogs/ebpf-machine-bytecode/)中,我们对 eBPF 虚拟机进行了简洁的深入研究。阅读上述部分并不是理解第 3 部分的必修课,尽管很好地掌握了低级别的基础知识确实有助于更好地理解高级别的工具。为了理解这些工具是如何工作的,我们先定义一下 eBPF 程序的高层次组件: 4 | 5 | - **后端**:这是在内核中加载和运行的 eBPF 字节码。它将数据写入内核 map 和环形缓冲区的**数据结构**中。 6 | - **加载器:\**它将字节码\**后端**加载到内核中。通常情况下,当加载器进程终止时,字节码会被内核自动卸载。 7 | - **前端:\**从\**数据结构**中读取数据(由**后端**写入)并将其显示给用户。 8 | - **数据结构**:这些是**后端**和**前端**之间的通信手段。它们是由内核管理的 map 和环形缓冲区,可以通过文件描述符访问,并需要在**后端**被加载之前创建。它们会持续存在,直到没有更多的**后端**或**前端**进行读写操作。 9 | 10 | 在第 1 部分和第 2 部分研究的 [sock_example.c](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c) 中,所有的组件都被放置在一个 C 文件中,所有的动作都由用户进程完成。 11 | 12 | - [第 40-45 行](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L40-L45)创建 map**数据结构**。 13 | - [第 47-61 行](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L47-L61)定义**后端**。 14 | - [第 63-76 行](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L63-L76)在内核中**加载**后端 15 | - [第 78-91 行](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L78-L91)是**前端**,负责将从 map 文件描述符中读取的数据打印给用户。 16 | 17 | eBPF 程序可以更加复杂:多个**后端**可以由一个(或单独的多个!)**加载器**进程加载,写入多个**数据结构**,然后由多个**前端**进程读取,所有这些都可以发生在一个跨越多个进程的用户 eBPF 应用程序中。 18 | 19 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-Part3-Diagram1.png) 20 | 21 | ## 2. 层级 1:容易编写的后端:LLVM eBPF 编译器 22 | 23 | 我们在前面的文章中看到,在内核中编写原始的 eBPF 字节码是不仅困难而且低效,这非常像用处理器的汇编语言编写程序,所以很自然地开发了一个能够将 LLVM 中间表示编译成 eBPF 程序的模块,并从 2015 年的 v3.7 开始发布(GCC 到现在为止仍然不支持 eBPF)。这使得多种高级语言如 C、Go 或 Rust 的子集可以被编译到 eBPF。最成熟和最流行的是基于 C 语言编写的方式,因为内核也是用 C 写的,这样就更容易复用现有的内核头文件。 24 | 25 | LLVM 将 "受限制的 C" 语言(记住,没有无界循环,最大 4096 条指令等等,见第 1 部分开始)编译成 ELF 对象文件,其中包含特殊区块(section),并可基于 bpf()系统调用,使用 libbpf 等库加载到内核中。这种设计有效地将**后端**定义从**加载器**和**前端**中分离出来,因为 eBPF 字节码包含在 ELF 文件中。 26 | 27 | 内核还在 [samples/bpf/](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/) 下提供了使用这种模式的例子:*_kern.c 文件被编译为 *_kern.o(**后端**代码),被 *_user.c(**装载器**和**前端**)加载。 28 | 29 | 将本系列第 1 和第 2 部分的 [sock_exapmle.c 原始字节码](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L47-L61) 转换为 "受限的 C" 代码“ [sockex1_kern.c](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L47-L61),这比原始字节码更容易理解和修改。 30 | 31 | ``` 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include "bpf_helpers.h" 37 | 38 | struct bpf_map_def SEC("maps") my_map = { 39 | .type = BPF_MAP_TYPE_ARRAY, 40 | .key_size = sizeof(u32), 41 | .value_size = sizeof(long), 42 | .max_entries = 256, 43 | }; 44 | 45 | SEC("socket1") 46 | int bpf_prog1(struct __sk_buff *skb) 47 | {int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); 48 | long *value; 49 | 50 | value = bpf_map_lookup_elem(&my_map, &index); 51 | if (value) 52 | __sync_fetch_and_add(value, skb->len); 53 | 54 | return 0; 55 | } 56 | char _license[] SEC("license") = "GPL"; 57 | ``` 58 | 59 | 产生的 eBPF ELF 对象 sockex1_kern.o,包含了分离的**后端**和**数据结构**定义。**加载器**和**前端**[sockex1_user.c](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sockex1_user.c),用于解析 ELF 文件、[创建](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_load.c#L270)所需的 map 和[加载字节码](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_load.c#L630)中内核函数 bpf_prog1(),然后**前端**像以前一样[继续运行](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sockex1_user.c#L32-L48)。 60 | 61 | 引入这个 "受限的 C" 抽象层所做的权衡是使 eBPF**后端**代码更容易用高级语言编写,代价是增加**加载器**的复杂性(现在需要解析 ELF 对象),而**前端**大部分不受影响。 62 | 63 | ## 3. 层级 2:自动化后端/加载器/前端的交互:BPF 编译器集合(BCC) 64 | 65 | 并不是每个人手头都有内核源码,特别是在生产中,而且一般来说,将基于 eBPF 工具与特定的内核源码版本捆绑在一起并不是一个好主意。设计和实现 eBPF 程序的**后端**,**前端**,**加载器**和**数据结构**之间的相互作用可能是非常复杂,这也比较容易出错和耗时(特别是在 C 语言中),这被认为是一种危险的低级语言。除了这些风险之外,开发人员还经常为常见问题重新造轮子,会造成无尽的设计变化和实现。为了减轻这些痛苦,社区创建了 BCC 项目:其为编写、加载和运行 eBPF 程序提供了一个易于使用的框架,除了上面举例的 "限制性 C" 之外,还可以通过编写简单的 python 或 lua 脚本来实现。 66 | 67 | BCC 项目有两个部分。 68 | 69 | - 编译器集合(BCC 本身):这是用于编写 BCC 工具的框架,也是我们文章的重点。请继续阅读。 70 | - BCC-tools:这是一个不断增长的基于 eBPF 且经过测试的程序集,提供了使用的例子和手册。更多信息见[本教程](https://github.com/iovisor/bcc/blob/master/docs/tutorial.md)。 71 | 72 | BCC 的安装包很大:它依赖于 LLVM/clang 将 "受限的 C"、python/lua 等编译成 eBPF,它还包含像 libbcc(用 C++ 编写)、libbpf 等库实现【译者注:原文 python/lua 顺序有错,另外 libcc 是 BCC 项目,libbpf 目前已经是内核代码一部分】。部分内核代码的也被复制到 BCC 代码中,所以它不需要基于完整的内核源(只需要头文件)进行构建。它可以很容易地占用数百 MB 的空间,这对于小型嵌入式设备来说不友好,我们希望这些设备也可以从 eBPF 的力量中受益。探索嵌入式设备由于大小限制问题的解决方案,将是我们在第 4 部分的重点。 73 | 74 | eBPF 程序组件在 BCC 组织方式如下: 75 | 76 | - **后端**和**数据结构**:用 "限制性 C" 编写。可以在单独的文件中,或直接作为多行字符串存储在**加载器/前端**的脚本中,以方便使用。参见:[语言参考](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#bpf-c)。【译者注:在 BCC 实现中,后端代码采用面向对象的做法,真正生成字节码的时候,BCC 会进行一次预处理,转换成真正的 C 语言代码方式,这也包括 map 等数据结构的定义方面】。 77 | - **加载器**和**前端**:可用非常简单的高级 python/lua 脚本编写。参见:[语言参考](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#bcc-python)。 78 | 79 | 因为 BCC 的主要目的是简化 eBPF 程序的编写,因此它尽可能地标准化和自动化:在后台完全自动化地通过 LLVM 编译 "受限的 C"**后端**,并产生一个标准的 ELF 对象格式类型,这种方式允许加载器对所有 BCC 程序只实现一次,并将其减少到最小的 API(2 行 python)。它还将**数据结构**的 API 标准化,以便于通过[前端](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c)访问。简而言之,它将开发者的注意力集中在编写**前端**上,而不必担心较低层次的细节问题。 80 | 81 | 为了最好地说明它是如何工作的,我们来看一个简单的具体例子,它是对前面文章中的 [sock_example.c](https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c) 的重新实现。该程序统计回环接口上收到了 TCP、UDP 和 ICMP 数据包的数量。 82 | 83 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-Part3-Diagram2.jpeg) 84 | 85 | 与此前直接用 C 语言编写的方式不同,用 BCC 实现具有以下优势: 86 | 87 | - 忘掉原始字节码:你可以用更方便的 "限制性 C" 编写所有**后端**。 88 | - 不需要维护任何 LLVM 的 "限制性 C" 构建逻辑。代码被 BCC 在脚本执行时直接编译和加载。 89 | - 没有危险的 C 代码:对于编写**前端**和**加载器**来说,Python 是一种更安全的语言,不会出现像空解引用(null dereferences)的错误。 90 | - 代码更简洁,你可以专注于应用程序的逻辑,而不是具体的机器问题。 91 | - 脚本可以被复制并在任何地方运行(假设已经安装了 BCC),它不会被束缚在内核的源代码目录中。 92 | - 等等。 93 | 94 | 在上面的例子中,我们使用了 BPF.SOCKET_FILTER 程序类型,其结果是我们挂载的 C 函数得到一个网络数据包缓冲区作为 context 上下文参数【译者注:本例中为 struct _*sk_buff \*skb】。我们还可以使用 BPF.KPROBE 程序类型来探测任意的内核函数。我们继续优化,不再使用与上面相同的接口,而是使用一个特殊的 kprobe*_* 函数名称前缀,以描述一个更高级别的 BCC API。 95 | 96 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-Part3-Diagram3.jpeg) 97 | 98 | 这个例子来自于 [bcc/examples/tracing/bitehist.py](https://github.com/iovisor/bcc/blob/v0.8.0/examples/tracing/bitehist.py)。它通过挂载在 blk_account_io_completion() 内核函数来打印一个 I/O 块大小的直方图。 99 | 100 | 请注意:eBPF 的加载是根据 **kprobe**__blk_account_io_completion() 函数的名称自动发生的(加载器隐含实现)! 【译者注:kprobe__ 前缀会被 BCC 编译代码过程中自动识别并转换成对应的附加函数调用】从用 libbpf 在 C 语言中编写和加载字节码以来,我们已经走了很远。 101 | 102 | ## 4. 层级 3:Python 太低级了:BPFftrace 103 | 104 | 在某些用例中,BCC 仍然过于底层,例如在事件响应中检查系统时,时间至关重要,需要快速做出决定,而编写 python/"限制性 C" 会花费太多时间,因此 BPFtrace 建立在 BCC 之上,通过特定领域语言(受 AWK 和 C 启发)提供更高级别的抽象。根据[声明帖](http://www.brendangregg.com/blog/2018-10-08/dtrace-for-linux-2018.html),该语言类似于 DTrace 语言实现,也被称为 DTrace 2.0,并提供了良好的介绍和例子。 105 | 106 | BPFtrace 在一个强大而安全(但与 BCC 相比仍有局限性)的语言中抽象出如此多的逻辑,是非常让人惊奇的。这个单行 shell 程序统计了每个用户进程系统调用的次数(访问[内置变量](https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#1-builtins)、[map 函数](https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#map-functions) 和[count()](https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md#2-count-count)文档获取更多信息)。 107 | 108 | ``` 109 | bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@[pid, comm] = count();}' 110 | ``` 111 | 112 | BPFtrace 在某些方面仍然是一个正在进行的工作。例如,目前还没有简单的方法来定义和运行一个套接字过滤器来实现像我们之前所列举的 sock_example 这样的工具。它可能通过在 BPFtrace 中用 kprobe:netif_receive_skb 钩子完成,但这种情况下 BCC 仍然是一个更好的套接字过滤工具。在任何情况下(即使在目前的状态下),BPFTrace 对于在寻求 BCC 的全部功能之前的快速分析/调试仍然非常有用。 113 | 114 | ## 5. 层级 4:云环境中的 eBPF:IOVisor 115 | 116 | [IOVisor](https://www.iovisor.org/) 是 Linux 基金会的一个[合作项目](https://www.linuxfoundation.org/projects/),基于本系列文章中介绍的 eBPF 虚拟机和工具。它使用了一些非常高层次的热门概念,如 "通用输入/输出",专注于向云/数据中心开发人员和用户提供 eBPF 技术。 117 | 118 | - 内核 eBPF 虚拟机成为 "IO Visor 运行时引擎" 119 | - 编译器后端成为 "IO Visor 编译器后端" 120 | - 一般的 eBPF 程序被重新命名为 "IO 模块" 121 | - 实现包过滤器的特定 eBPF 程序成为 "IO 数据平面模块/组件" 122 | - 等等。 123 | 124 | 考虑到原来的名字(扩展的伯克利包过滤器),并没有代表什么意义,也许所有这些重命名都是受欢迎和有价值的,特别是如果它能使更多的行业利用 eBPF 的力量。 125 | 126 | IOVisor 项目创建了 [Hover 框架](https://github.com/iovisor/iomodules),也被称为 "IO 模块管理器",它是一个管理 eBPF 程序(或 IO 模块)的用户空间后台服务程序,能够将 IO 模块推送和拉取到云端,这类似于 Docker daemon 发布/获取镜像的方式。它提供了一个 CLI,Web-REST 接口,也有一个[花哨的 Web UI](https://github.com/iovisor/hoverui)。Hover 的重要部分是用 Go 编写的,因此,除了正常的 BCC 依赖性外,它还依赖于 Go 的安装,这使得它体积变得很大,这并不适合我们最终在第 4 部分中的提及的小型嵌入式设备。 127 | 128 | ## 6. 总结 129 | 130 | 在这一部分,我们研究了建立在 eBPF 虚拟机之上的用户空间生态系统,以提高开发人员的工作效率和简化 eBPF 程序部署。这些工具使得使用 eBPF 非常容易,用户只需 "apt-get install bpftrace" 就可以运行单行程序,或者使用 Hover 守护程序将 eBPF 程序(IO 模块)部署到 1000 台机器上。然而,所有这些工具,尽管它们给开发者和用户提供了所有的力量,但却需要很大的磁盘空间,甚至可能无法在 32 位 ARM 系统上运行,这使得它们不是很适合小型嵌入式设备,所以这就是为什么在第 4 部分我们将探索其他项目,试图缓解运行针对嵌入式设备生态系统的 eBPF 程序。 131 | -------------------------------------------------------------------------------- /ebpf/文章/eBPF 概述,第 4 部分:在嵌入式系统运行.md: -------------------------------------------------------------------------------- 1 | ## 1. 前言 2 | 3 | 在本系列的[第 1 部分](https://kubesphere.com.cn/blogs/ebpf-guide/)和[第 2 部分](https://kubesphere.com.cn/blogs/ebpf-machine-bytecode/),我们介绍了 eBPF 虚拟机内部工作原理,在[第 3 部分](https://kubesphere.com.cn/blogs/ebpf-software-stack/)我们研究了基于底层虚拟机机制之上开发和使用 eBPF 程序的主流方式。 4 | 5 | 在这一部分中,我们将从另外一个视角来分析项目,尝试解决嵌入式 Linux 系统所面临的一些独特的问题:如需要非常小的自定义操作系统镜像,不能容纳完整的 BCC LLVM 工具链/python 安装,或试图避免同时维护主机的交叉编译(本地)工具链和交叉编译的目标编译器工具链,以及其相关的构建逻辑,即使在使用像 OpenEmbedded/Yocto 这样的高级构建系统时也很重要。 6 | 7 | ## 2. 关于可移植性 8 | 9 | 在第 3 部分研究的运行 eBPF/BCC 程序的主流方式中,可移植性并不是像在嵌入式设备上面临的问题那么大:eBPF 程序是在被加载的同一台机器上编译的,使用已经运行的内核,而且头文件很容易通过发行包管理器获得。嵌入式系统通常运行不同的 Linux 发行版和不同的处理器架构,与开发人员的计算机相比,有时具有重度修改或上游分歧的内核,在构建配置上也有很大的差异,或还可能使用了只有二进制的模块。 10 | 11 | eBPF 虚拟机的字节码是通用的(并未与特定机器相关),所以一旦编译好 eBPF 字节码,将其从 x86_64 移动到 ARM 设备上并不会引起太多问题。当字节码探测内核函数和数据结构时,问题就开始了,这些函数和数据结构可能与目标设备的内核不同或者会不存在,所以至少目标设备的内核头文件必须存在于构建 eBPF 程序字节码的主机上。新的功能或 eBPF 指令也可能被添加到以后的内核中,这可以使 eBPF 字节码向前兼容,但不能在内核版本之间向后兼容(参见[内核版本与 eBPF 功能](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md))。建议将 eBPF 程序附加到稳定的内核 ABI 上,如跟踪点(tracepoint),这可以缓解常见的可移植性。 12 | 13 | 最近一个重要的工作已经开始,通过在 LLVM 生成的 eBPF 对象代码中嵌入数据类型信息,通过增加 BTF(BTF 类型格式)数据,以增加 eBPF 程序的可移植性(CO-RE 一次编译,到处运行)。更多信息见这里的[补丁](https://lwn.net/Articles/750695/)和[文章](https://lwn.net/Articles/773198/)。这很重要,因为 BTF 涉及到 eBPF 软件技术栈的所有部分(内核虚拟机和验证器、clang/LLVM 编译器、BCC 等),但这种方式可带来很大的便利,允许重复使用现有的 BCC 工具,而不需要特别的 eBPF 交叉编译和在嵌入式设备上安装 LLVM 或运行 BPFd。截至目前,CO-RE BTF 工作仍处于早期开发阶段,还需要付出相当多的工作才能可用【译者注:当前在高版本内核已经可以使用或者编译内核时启用了 BTF 编译选项】。也许我们会在其完全可用后再发表一篇博文。 14 | 15 | ## 3. BPFd 16 | 17 | [BPFd](https://lwn.net/Articles/744522/)(项目地址 https://github.com/joelagnel/bpfd)更像是一个为 Android 设备开发的概念验证,后被放弃,转而通过 [adeb](https://github.com/joelagnel/adeb) 包运行一个完整的设备上的 BCC 工具链【译者注:BCC 在 adeb 的编译文档参见[这里](https://github.com/joelagnel/adeb/blob/master/BCC.md)】。如果一个设备足够强大,可以运行 Android 和 Java,那么它也可能可以安装 BCC/LLVM/python。尽管这个实现有些不完整(通信是通过 Android USB 调试桥或作为一个本地进程完成的,而不是通过一个通用的传输层),但这个设计很有趣,有足够时间和资源的人可以把它拿起来合并,继续搁置的 [PR 工作](https://github.com/iovisor/bcc/pull/1675)。 18 | 19 | 简而言之,BPFd 是一个运行在嵌入式设备上的守护程序,作为本地内核/libbpf 的一个远程过程调用(RPC)接口。Python 在主机上运行,调用 BCC 来编译/部署 eBPF 字节码,并通过 BPFd 创建/读取 map。BPFd 的主要优点是,所有的 BCC 基础设施和脚本都可以工作,而不需要在目标设备上安装 BCC、LLVM 或 python,BPFd 二进制文件只有 100kb 左右的大小,并依赖 libc。 20 | 21 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-Part4-Diagram1.jpeg) 22 | 23 | ## 4. Ply 24 | 25 | [ply](https://wkz.github.io/ply/) 项目实现了一种与 BPFtrace 非常相似的高级领域特定语言(受到 AWK 和 C 的启发),其明确的目的是将运行时的依赖性降到最低。它只依赖于一个现代的 libc(不一定是 GNU 的 libc)和 shell(与 sh 兼容)。Ply 本身实现了一个 eBPF 编译器,需要根据目标设备的内核头文件进行构建,然后作为一个单一的二进制库和 shell 包装器部署到目标设备上。 26 | 27 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-Part4-Diagram2.jpeg) 28 | 29 | 为了更好解释 ply,我们把第 3 部分中的 BPFtrace 例子和与 ply 实现进行对比: 30 | 31 | - BPFtrace:要运行该例子,你需要数百 MB 的 LLVM/clang、libelf 和其他依赖项: 32 | 33 | `bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@[pid, comm] = count();}'` 34 | 35 | - ply:你只需要一个 ~50kb 的二进制文件,它产生的结果是相同的,语法几乎相同: 36 | 37 | `ply 'tracepoint:raw_syscalls/sys_enter {@[pid, comm] = count();}'` 38 | 39 | Ply 仍在大量开发中(最近的 v2.0 版本是完全重写的)【译者注:当前最新版本为 2.1.1,最近一次代码提交是 8 个月前,活跃度一般】,除了一些示例之外,该语言还不不稳定或缺乏文档,它不如完整的 BCC 强大,也没有 BPFtrace 丰富的功能特性,但它对于通过 ssh 或串行控制台快速调试远程嵌入式设备仍然非常有用。 40 | 41 | ## 5. Gobpf 42 | 43 | [Gobpf](https://github.com/iovisor/gobpf) 及其合并的子项目(goebpf, gobpf-elf-loader),是 IOVisor 项目的一部分,为 BCC 提供 Golang 语言绑定。eBPF 的内核逻辑仍然用 "限制性 C" 编写,并由 LLVM 编译,只有标准的 python/lua 用户空间脚本被 Go 取代。这个项目对嵌入式设备的意义在于它的 eBPF [elf 加载模块](https://github.com/iovisor/gobpf/tree/master/elf),其可以被交叉编译并在嵌入式设备上独立运行,以加载 eBPF 程序至内核并与与之交互。 44 | 45 | ![img](https://pek3b.qingstor.com/kubesphere-community/images/eBPF-Part4-Diagram3.jpeg) 46 | 47 | 值得注意的是,go 加载器可以被写成通用的(我们很快就会看到),因此它可以加载和运行任何 eBPF 字节码,并在本地重新用于多个不同的跟踪会话。 48 | 49 | 使用 gobpf 很痛苦的,主要是因为缺乏文档。目前最好的 "文档" 是 [tcptracer 源码](https://github.com/weaveworks/tcptracer-bpf),它相当复杂(他们使用 kprobes 而不依赖于特定的内核版本!),但从它可以学到很多。Gobpf 本身也是一项正在进行的工作:虽然 elf 加载器相当完整,并支持加载带有套接字、(k|u)probes、tracepoints、perf 事件等加载的 eBPF ELF 对象,但 bcc go 绑定模块还不容易支持所有这些功能。例如,尽管你可以写一个 socket_ilter ebpf 程序,将其编译并加载到内核中,但你仍然不能像 BCC 的 python 那样从 go 用户空间轻松地与 eBPF 进行交互,BCC 的 API 更加成熟和用户友好。无论如何,gobpf 仍然比其他具有类似目标的项目处于更好的状态。 50 | 51 | 让我们研究一个简单的例子来说明 gobpf 如何工作的。首先,我们将在本地 x86_64 机器上运行它,然后交叉编译并在 32 位 ARMv7 板上运行它,比如流行的 Beaglebone 或 Raspberry Pi。我们的文件目录结构如下: 52 | 53 | ``` 54 | $ find . -type f 55 | ./src/open-example.go 56 | ./src/open-example.c 57 | ./Makefile 58 | ``` 59 | 60 | **open-example.go**:这是建立在 gobpf/elf 之上的 eBPF ELF 加载器。它把编译好的 "限制性 C" ELF 对象作为参数,加载到内核并运行,直到加载器进程被杀死,这时内核会自动卸载 eBPF 逻辑【译者注:通常情况是这样的,也有场景加载器退出,ebpf 程序继续运行的】。我们有意保持加载器的简单性和通用性(它加载在对象文件中发现的任何探针),因此加载器可以被重复使用。更复杂的逻辑可以通过使用 [gobpf 绑定](https://github.com/iovisor/gobpf/blob/master/bcc/module.go) 模块添加到这里。 61 | 62 | ``` 63 | package main 64 | 65 | import ( 66 | "fmt" 67 | "os" 68 | "os/signal" 69 | "github.com/iovisor/gobpf/elf" 70 | ) 71 | 72 | func main() {mod := elf.NewModule(os.Args[1]) 73 | 74 | err := mod.Load(nil); 75 | if err != nil {fmt.Fprintf(os.Stderr, "Error loading'%s'ebpf object: %v\n", os.Args[1], err)os.Exit(1) 76 | } 77 | 78 | err = mod.EnableKprobes(0) 79 | if err != nil {fmt.Fprintf(os.Stderr, "Error loading kprobes: %v\n", err) 80 | os.Exit(1) 81 | } 82 | 83 | sig := make(chan os.Signal, 1) 84 | signal.Notify(sig, os.Interrupt, os.Kill) 85 | // ... 86 | } 87 | ``` 88 | 89 | **open-example.c**:这是上述加载器加载至内核的 "限制性 C" 源代码。它挂载在 do_sys_open 函数,并根据 [ftrace format](https://raw.githubusercontent.com/torvalds/linux/v4.20/Documentation/trace/ftrace.rst) 将进程命令、PID、CPU、打开文件名和时间戳打印到跟踪环形缓冲区,(详见 "输出格式" 一节)。打开的文件名作为 [do_sys_open call](https://github.com/torvalds/linux/blob/v4.20/fs/open.c#L1048) 的第二个参数传递,可以从代表函数入口的 CPU 寄存器的上下文结构中访问。 90 | 91 | ``` 92 | #include 93 | #include 94 | #include 95 | 96 | SEC("kprobe/do_sys_open") 97 | int kprobe__do_sys_open(struct pt_regs *ctx) 98 | {char file_name[256]; 99 | 100 | bpf_probe_read(file_name, sizeof(file_name), PT_REGS_PARM2(ctx)); 101 | 102 | char fmt[] = "file %s\n"; 103 | bpf_trace_printk(fmt, sizeof(fmt), &file_name); 104 | 105 | return 0; 106 | } 107 | 108 | char _license[] SEC("license") = "GPL"; 109 | __u32 _version SEC("version") = 0xFFFFFFFE; 110 | ``` 111 | 112 | 在上面的代码中,我们定义了特定的 "SEC" 区域,这样 gobpf 加载器就可获取到哪里查找或加载内容的信息。在我们的例子中,区域为 kprobe、license 和 version。特殊的 0xFFFFFFFE 值告诉加载器,这个 eBPF 程序与任何内核版本都是兼容的,因为打开系统调用而破坏用户空间的机会接近于 0。 113 | 114 | **Makefile**:这是上述两个文件的构建逻辑。注意我们是如何在 include 路径中加入 "arch/x86/..." 的;在 ARM 上它将是 "arch/arm/..."。 115 | 116 | ``` 117 | SHELL=/bin/bash -o pipefail 118 | LINUX_SRC_ROOT="/home/adi/workspace/linux" 119 | FILENAME="open-example" 120 | 121 | ebpf-build: clean go-build 122 | clang \ 123 | -D__KERNEL__ -fno-stack-protector -Wno-int-conversion \ 124 | -O2 -emit-llvm -c "src/${FILENAME}.c" \ 125 | -I ${LINUX_SRC_ROOT}/include \ 126 | -I ${LINUX_SRC_ROOT}/tools/testing/selftests \ 127 | -I ${LINUX_SRC_ROOT}/arch/x86/include \ 128 | -o - | llc -march=bpf -filetype=obj -o "${FILENAME}.o" 129 | 130 | go-build: 131 | go build -o ${FILENAME} src/${FILENAME}.go 132 | 133 | clean: 134 | rm -f ${FILENAME}* 135 | ``` 136 | 137 | 运行上述 makefile 在当前目录下产生两个新文件: 138 | 139 | - open-example:这是编译后的 src/*.go 加载器。它只依赖于 libc 并且可以被复用来加载多个 eBPF ELF 文件运行多个跟踪。 140 | - open-example.o:这是编译后的 eBPF 字节码,将在内核中加载。 141 | 142 | “open-example" 和 "open-example.o" ELF 二进制文件可以进一步合并成一个;加载器可以包括 eBPF 二进制文件作为资产,也可以像 [tcptracer](https://github.com/weaveworks/tcptracer-bpf/blob/master/pkg/tracer/tcptracer-ebpf.go#L80) 那样在其源代码中直接存储为字节数。然而,这超出了本文的范围。 143 | 144 | 运行例子显示以下输出(见 [ftrace 文档](https://kubesphere.io/zh/blogs/ebpf-working-with-embedded-systems/(https://raw.githubusercontent.com/torvalds/linux/v4.20/Documentation/trace/ftrace.rst)) 中的 "输出格式" 部分)。 145 | 146 | ``` 147 | # (./open-example open-example.o &) && cat /sys/kernel/debug/tracing/trace_pipe 148 | electron-17494 [007] ...3 163158.937350: 0: file /proc/self/maps 149 | systemd-1 [005] ...3 163160.120796: 0: file /proc/29261/cgroup 150 | emacs-596 [006] ...3 163163.501746: 0: file /home/adi/ 151 | (...) 152 | ``` 153 | 154 | 沿用我们在本系列的第 3 部分中定义的术语,我们的 eBPF 程序有以下部分组成: 155 | 156 | - **后端**:是 open-example.o ELF 对象。它将数据写入内核跟踪环形缓冲区。 157 | - **加载器**:这是编译过的 open-example 二进制文件,包含 gobpf/elf 加载器模块。只要它运行,数据就会被添加到跟踪缓冲区中。 158 | - **前端**:这就是 `cat /sys/kernel/debug/tracing/trace_pipe`。非常 UNIX 风格。 159 | - **数据结构**:内核跟踪环形缓冲区。 160 | 161 | 现在将我们的例子交叉编译为 32 位 ARMv7。 基于你的 ARM 设备运行的内核版本: 162 | 163 | - 内核版本>=5.2:只需改变 makefile,就可以交叉编译与上述相同的源代码。 164 | - 内核版本<5.2:除了使用新的 makefile 外,还需要将 PT_REGS_PARM* 宏从 [这个 patch](https://lore.kernel.org/bpf/20190304205019.15071-1-adrian.ratiu@collabora.com/) 复制到 "受限制 C" 代码。 165 | 166 | 新的 makefile 告诉 LLVM/Clang,eBPF 字节码以 ARMv7 设备为目标,使用 32 位 eBPF 虚拟机子寄存器地址模式,以便虚拟机可以正确访问本地处理器提供的 32 位寻址内存(还记得第 2 部分中介绍的所有 eBPF 虚拟机寄存器默认为 64 位宽),设置适当的包含路径,然后指示 Go 编译器使用正确的交叉编译设置。在运行这个 makefile 之前,需要一个预先存在的交叉编译器工具链,它被指向 CC 变量。 167 | 168 | ``` 169 | SHELL=/bin/bash -o pipefail 170 | LINUX_SRC_ROOT="/home/adi/workspace/linux" 171 | FILENAME="open-example" 172 | 173 | ebpf-build: clean go-build 174 | clang \ 175 | --target=armv7a-linux-gnueabihf \ 176 | -D__KERNEL__ -fno-stack-protector -Wno-int-conversion \ 177 | -O2 -emit-llvm -c "src/${FILENAME}.c" \ 178 | -I ${LINUX_SRC_ROOT}/include \ 179 | -I ${LINUX_SRC_ROOT}/tools/testing/selftests \ 180 | -I ${LINUX_SRC_ROOT}/arch/arm/include \ 181 | -o - | llc -march=bpf -filetype=obj -o "${FILENAME}.o" 182 | 183 | go-build: 184 | GOOS=linux GOARCH=arm CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc \ 185 | go build -o ${FILENAME} src/${FILENAME}.go 186 | 187 | clean: 188 | rm -f ${FILENAME}* 189 | ``` 190 | 191 | 运行新的 makefile,并验证产生的二进制文件已经被正确地交叉编译: 192 | 193 | ``` 194 | [adi@iwork]$ file open-example* 195 | open-example: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter (...), stripped 196 | open-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped 197 | ``` 198 | 199 | 然后将加载器和字节码复制到设备上,与在 x86_64 主机上使用上述相同的命令来运行。记住,只要修改和重新编译 C eBPF 代码,加载器就可以重复使用,用于运行不同的跟踪。 200 | 201 | ``` 202 | [root@ionelpi adi]# (./open-example open-example.o &) && cat /sys/kernel/debug/tracing/trace_pipe 203 | ls-380 [001] d..2 203.410986: 0: file /etc/ld-musl-armhf.path 204 | ls-380 [001] d..2 203.411064: 0: file /usr/lib/libcap.so.2 205 | ls-380 [001] d..2 203.411922: 0: file / 206 | zcat-397 [002] d..2 432.676010: 0: file /etc/ld-musl-armhf.path 207 | zcat-397 [002] d..2 432.676237: 0: file /usr/lib/libtinfo.so.5 208 | zcat-397 [002] d..2 432.679431: 0: file /usr/bin/zcat 209 | gzip-397 [002] d..2 432.693428: 0: file /proc/ 210 | gzip-397 [002] d..2 432.693633: 0: file config.gz 211 | ``` 212 | 213 | 由于加载器和字节码加起来只有 2M 大小,这是一个在嵌入式设备上运行 eBPF 的相当好的方法,而不需要完全安装 BCC/LLVM。 214 | 215 | ## 6. 总结 216 | 217 | 在本系列的第 4 部分,我们研究了可以用于在小型嵌入式设备上运行 eBPF 程序的相关项目。不幸的是,当前使用这些项目还是比较很困难的:它们有的被遗弃或缺乏人力,在早期开发时一切都在变化,或缺乏基本的文档,需要用户深入到源代码中并自己想办法解决。正如我们所看到的,gobpf 项目作为 BCC/python 的替代品是最有活力的,而 ply 也是一个有前途的 BPFtrace 替代品,其占用空间最小。随着更多的工作投入到这些项目中以降低使用者的门槛,eBPF 的强大功能可以用于资源受限的嵌入式设备,而无需移植/安装整个 BCC/LLVM/python/Hover 技术栈。 218 | -------------------------------------------------------------------------------- /ebpf/文章/什么是 eBPF.md: -------------------------------------------------------------------------------- 1 | ![logo](https://pic.crazytaxii.com/logo-big-9cf8920e80cdc57e6ea60825ebe287ca.png) 2 | 3 | eBPF 是一项革命性的技术,它能在操作系统内核中运行沙箱程序。被用于安全并有效地扩展内核的能力而无需修改内核代码或者加载内核模块。 4 | 5 | 从古至今,由于内核有监视和控制整个系统的特权,操作系统一直都是实现可观察性、安全性和网络功能的理想场所。同时,操作系统内核也很难进化,因为它的核心角色以及对稳定和安全的高度要求。因此,操作系统级别的创新相比操作系统之外实现的功能较少。 6 | 7 | ![overview](https://pic.crazytaxii.com/overview-bf463455a5666fc3fb841b9240d588ff.png) 8 | 9 | eBPF 从根本上改变了这个定律。通过允许在操作系统内运行沙箱程序,应用开发者能够运行 eBPF 程序在运行时为操作系统增加额外的功能。然后操作系统保证安全和执行效率,就像借助即时编译器(JIT compiler)和验证引擎在本地编译那样。这引发了一波基于 eBPF 的项目,涵盖了一系列广泛的使用案例,包括下一代网络、可观察性和安全功能。 10 | 11 | 现在,eBPF 被广泛用于:在现代数据中心和云原生环境中提供高性能网络和负载均衡;以低开销提取细粒度的安全可观察性数据;帮助应用开发者追踪应用程序;洞悉性能问题和加强容器运行时的安全性等等。一切皆有可能,而 eBPF 释放的创新才刚刚开始。 12 | 13 | ## 介绍 14 | 15 | 如果你想要深入了解 eBPF,查看 [eBPF & XDP Reference Guide](https://cilium.readthedocs.io/en/stable/bpf/)。无论你是一个想要构建 eBPF 程序的开发者还是对使用 eBPF 技术的解决方案感兴趣,都有必要了解一下基本概念和架构。 16 | 17 | ### 钩子(Hook) 18 | 19 | eBPF 程序是事件驱动的,当内核或应用程序通过某个锚点时就会运行。预定义的钩子包括系统调用、函数进入/退出、内核追踪点、网络事件等等。 20 | 21 | ![syscall_hook](https://pic.crazytaxii.com/syscall_hook-b4f7d64d4d04806a1de60126926d5f3a.png) 22 | 23 | 如果预定义的钩子不存在,可以创建一个内核探针(kprobe)或用户探针(uprobe)来将 eBPF 程序附加至内核或用户应用程序的任何地方。 24 | 25 | ![hook_overview](https://pic.crazytaxii.com/hook_overview-99c69bbff092c35b9c83f00a80fed240.png) 26 | 27 | ### 怎么写 eBPF 程序? 28 | 29 | 在很多情况下,并不直接使用 eBPF,而是通过 Cilium、bcc 或 bpftrace 等项目间接使用,它们在 eBPF 之上提供了一层抽象,无需直接编写程序而是提供了一些能力,由 eBPF 来实现。 30 | 31 | ![clang](https://pic.crazytaxii.com/clang-a7160cd231b062b321f2a479a4d0848f.png) 32 | 33 | 要是没有上层抽象的话,就要直接编写程序了。Linux 内核期望 ePBF 程序以字节码的形式加载。直接编写字节码不太可能,实际开发中更常见的是使用 LLVM 等编译器套件将伪 C 代码编译成 eBPF 字节码。 34 | 35 | ### Loader & Verification 架构 36 | 37 | 当所需的钩子被确定后,可以使用 [bpf 系统调用](https://man7.org/linux/man-pages/man2/bpf.2.html) 将 eBPF 程序加载至 Linux 内核。通常使用 eBPF 库完成,下节将介绍可用的开发工具链。 38 | 39 | ![go](https://pic.crazytaxii.com/go-1a1bb6f1e64b1ad5597f57dc17cf1350.png) 40 | 41 | 当程序被加载至 Linux 内核时,在被连接到所请求的钩子之前要经过两个步骤: 42 | 43 | ### 验证 44 | 45 | 验证步骤确保 eBPF 程序可以安全运行,将检查程序是否符合以下条件: 46 | 47 | ![loader](https://pic.crazytaxii.com/loader-7eec5ccd8f6fbaf055256da4910acd5a.png) 48 | 49 | - 加载 eBPF 程序的进程的权限。除非启用非特权 eBPF,否则只有特权(privileged)进程可以加载 eBPF 程序。 50 | - 程序不会崩溃或损害系统。 51 | - 程序是会运行完成的(不会处于循环状态,这样会耽误进一步处理)。 52 | 53 | ### JIT 编译 54 | 55 | JIT(Just-In-Time)编译步骤将程序的通用字节码翻译成机器特定的指令集来优化程序的执行速度。这使得 eBPF 程序可以像本地编译的内核代码或作为内核模块加载的代码一样高效运行。 56 | 57 | ### Maps 58 | 59 | 共享收集的信息和存储状态的能力对 eBPF 程序来说至关重要。为此,eBPF 程序可以利用 eBPF maps 的概念来存储和检索数据。eBPF maps 可被 eBPF 程序以及用户空间的应用程序通过系统调用来访问。 60 | 61 | ![map](https://pic.crazytaxii.com/map_architecture-e7909dc59d2b139b77f901fce04f60a1.png) 62 | 63 | map 类型支持多种数据结构: 64 | 65 | - 哈希表、数组 66 | - LRU(Least Recently Used) 67 | - 环形缓冲区(Ring Buffer) 68 | - 栈 69 | - LPM(Longest Prefix match) 70 | 71 | ### 辅助调用 72 | 73 | eBPF 程序不能随意调用内核函数。这样做的话需要使 eBPF 程序与特定的内核版本绑定,并使程序的兼容性变得复杂。相反,eBPF 程序调用辅助函数,这是内核提供的一个稳定的 API。 74 | 75 | ![helper](https://pic.crazytaxii.com/helper-6e18b76323d8520107fab90c033edaf4.png) 76 | 77 | 辅助调用的集合还在持续扩充中,以下是可用的辅助调用: 78 | 79 | - 生成随机数 80 | - 获取当前时间和日期 81 | - eBPF map 访问 82 | - 获取进程/cgroup 上下文 83 | - 操控网络包和转发逻辑 84 | 85 | ### 尾部调用和函数调用 86 | 87 | eBPF 程序可与尾部 & 函数调用的概念相结合。函数调用允许在 eBPF 程序中定义和调用函数。尾部调用能够调用和执行另一个 eBPF 程序并替换执行上下文,类似于常规进程进行 execve() 系统调用。 88 | 89 | ![tailcall](https://pic.crazytaxii.com/tailcall-106a9d37e6b2b88e24b923d96e852dd5.png) 90 | 91 | ### eBPF 安全性 92 | 93 | 能力越大责任越大。 94 | 95 | eBPF 是一项非常强大的技术,目前运行在许多关键软件基础设施组件的核心。在 eBPF 的开发中,当 eBPF 被考虑纳入 Linux 内核时,eBPF 的安全性是最关键的方面。eBPF 的安全性通过几个层面得到保证: 96 | 97 | #### 所需的权限 98 | 99 | 除非非特权的 eBPF 被启用,否则所有打算将 eBPF 加载到 Linux 内核的进程必须以特权模式(root 权限)运行或需要 CAP_BPF 能力。这就意味着不受信任的程序无法加载 eBPF 程序。 100 | 101 | 如果启用了非特权 eBPF,非特权进程可以加载特定的 eBPF 程序,但功能被阉割,对内核的访问也受限制。 102 | 103 | #### 验证器 104 | 105 | 如果一个进程被允许加载 eBPF 程序,所有程序仍会通过 eBPF 验证器。eBPF 验证器确保程序本身的安全性。例如: 106 | 107 | - eBPF 程序会被验证它们总是可以执行完成,永不被阻塞或死循环。只有当验证器能够确保循环包含了退出条件并保证为真时,程序才会被接受。 108 | - 程序不得使用任何未初始化的变量或越界访问内存。 109 | - 程序不能太大,不可能加载任意大的 eBPF 程序。 110 | - 程序的复杂度必须有限。验证器将评估所有可能的执行路径,并且必须能够在有限时间内完成分析。 111 | 112 | #### 加固 113 | 114 | 在验证成功后,eBPF 程序会根据是从特权还是非特权进程加载的来运行一个“加固进程”: 115 | 116 | - **程序执行保护**:持有 eBPF 程序的内核内存被保护且只读。出于任何原因,不论是内核 bug 还是恶意操作,如果 eBPF 程序被试图修改,内核将崩溃而不是允许它继续执行被破坏/篡改的程序。 117 | - **针对 Spectre 的缓解措施**:CPU 可能会预测错分支,并留下可观察的副作用,这些副作用可以通过旁路被提取出来。举几个例子:eBPF 程序屏蔽了内存访问来将瞬时指令下的访问重定向到受控区域,验证器也会遵循只有在投机执行下才能访问的程序执行路径,JIT 编译器在尾调用不能转换为直接调用的情况下发出 Retpolines。 118 | - **常量盲化**:代码中的所有常量都被屏蔽以防止 JIT 喷涂攻击([JIT spraying](https://en.wikipedia.org/wiki/JIT_spraying))。这可以防止攻击者将可执行代码作为常量注入,存在另一个内核 bug 的情况下,会允许攻击者跳入 eBPF 程序的内存段执行代码。 119 | 120 | #### 抽象的运行时上下文 121 | 122 | eBPF 程序不能随意直接访问内核内存,要想访问程序上下文之外的数据和数据结果必须通过 eBPF 辅助工具(eBPF helper)来完成,如此保证了数据访问的一致性并限制了 eBPF 程序的访问权限。举个栗子,如果确保安全的情况下,一个正在运行的 eBPF 程序就被允许修改某些数据,但不能随机修改内核中的数据。 123 | 124 | ## 开发工具链 125 | 126 | 开发者可以根据不同的需求选择合适的工具来开发和管理 eBPF 项目: 127 | 128 | ### bcc 129 | 130 | BCC 是一个使用户能够编写嵌入 eBPF 程序的 Python 脚本的框架,主要用于追踪和剖析应用程序和系统,利用 eBPF 程序来在用户空间收集统计数据或生成事件,并以对人类友好的形式展示。运行 Python 程序会生成 eBPF 字节码并将其加载进内核。 131 | 132 | ![bcc](https://pic.crazytaxii.com/bcc-def942c66b8c7565f0cfeab1c1017a80.png) 133 | 134 | ### bpftrace 135 | 136 | bpftrace 是一种 Linux eBPF 的高级追踪语言,在 4.x 版本的内核中可用。bpftrace 使用 LLVM 作为后端来将脚本编译为 eBPF 字节码,利用 BCC 和 Linux eBPF 子系统以及已有的 Linux 追踪功能进行交互:内核动态追踪(kprobes)、用户级动态追踪(uprobes)和追踪点(tracepoint)。bpftrace 语言的灵感来自于 awk、C 和 Dtrace 还有 SystemTap 这样的老一辈追踪器。 137 | 138 | ![bpftrace](https://pic.crazytaxii.com/bpftrace-c53dfcbff6ea67a8f00896bd76e4c07c.png) 139 | 140 | ### eBPF Go 库 141 | 142 | [gobpf](https://github.com/iovisor/gobpf) 是一个通用的 eBPF Go 库,将获取 eBPF 字节码的过程和加载/管理程序解耦。eBPF 程序通常由高级编程语言编写,然后使用 clang/LLVM 编译器来编译成字节码。 143 | 144 | ![go](https://pic.crazytaxii.com/go-1a1bb6f1e64b1ad5597f57dc17cf1350.png) 145 | 146 | ### libbpf C/C++ 库 147 | 148 | [libbpf](https://github.com/libbpf/libbpf) 是一个基于 C/C++ 的通用 eBPF 库,帮助将由 clang/LLVM 编译器生成的 eBPF 对象文件和加载至内核解耦,提供易用的 API 来抽象与 BPF 系统调用的交互。 149 | 150 | ## 更多 151 | 152 | ### 相关文档 153 | 154 | - [eBPF & XDP Reference Guide](https://cilium.readthedocs.io/en/stable/bpf/) 155 | 156 | Cilium 相关文档 157 | 158 | - [BPF Documentation](https://www.kernel.org/doc/html/latest/bpf/index.html) 159 | 160 | Linux 内核 BPF 文档 161 | 162 | - [BPF Design Q&A](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/bpf/bpf_design_QA.rst) 163 | 164 | 内核相关的 eBPF 问题 FAQ 165 | 166 | ### 相关教程 167 | 168 | - [Learn eBPF Tracing: Tutorial and Examples](http://www.brendangregg.com/blog/2019-01-01/learn-ebpf-tracing.html) 169 | 170 | Brendan Gregg 的博客 171 | 172 | - [XDP Hands-On Tutorials](https://github.com/xdp-project/xdp-tutorial) 173 | 174 | - [BCC, libbpf and BPF CO-RE Tutorials](https://facebookmicrosites.github.io/bpf/blog/) 175 | 176 | Facebook 的 BPF 博客 177 | 178 | ### 演讲 179 | 180 | - [eBPF and Kubernetes: Little Helper Minions for Scaling Microservices](https://www.youtube.com/watch?v=99jUcLt3rSk) 181 | 182 | Daniel Borkmann,KubeCon EU,2020 年八月 183 | 184 | - [eBPF - Rethinking the Linux Kernel](https://www.infoq.com/presentations/facebook-google-bpf-linux-kernel/) 185 | 186 | Thomas Graf,QCon London,2020 年四月 187 | 188 | - [BPF as a revolutionary technology for the container landscape](https://www.youtube.com/watch?v=U3PdyHlrG1o&t=7) 189 | 190 | Daniel Borkmann,FOSDEM,2020 年二月 191 | 192 | - [BPF at Facebook](https://www.youtube.com/watch?v=ZYBXZFKPS28) 193 | 194 | Alexei Starovoitov,Performance Summit,2019 年十二月 195 | 196 | - [BPF: A New Type of Software](https://youtu.be/7pmXdG8-7WU?t=8) 197 | 198 | Brendan Gregg,Ubuntu Masters,2019 年十月 199 | 200 | - [The ubiquity but also the necessity of eBPF as a technology](https://www.youtube.com/watch?v=mFxs3VXABPU) 201 | 202 | David S. Miller,Kernel Recipes,2019 年十月 203 | 204 | - [BPF and Spectre: Mitigating transient execution attacks](https://www.youtube.com/watch?v=6N30Yp5f9c4) 205 | 206 | Daniel Borkmann,eBPF Summit,2021 年八月 207 | 208 | - [BPF Internals](https://www.usenix.org/conference/lisa21/presentation/gregg-bpf) 209 | 210 | Brendan Gregg,USENIX LISA,2021 年六月 211 | 212 | ### Cilium 213 | 214 | - [Advanced BPF Kernel Features for the Container Age](https://www.youtube.com/watch?v=PJY-rN1EsVw) 215 | 216 | Daniel Borkmann,FOSDEM,2021 年二月 217 | 218 | - [Kubernetes Service Load-Balancing at Scale with BPF & XDP](https://www.youtube.com/watch?v=UkvxPyIJAko&t=21s) 219 | 220 | Daniel Borkmann & Martynas Pumputis,Linux Plumbers,2020 年八月 221 | 222 | - [Liberating Kubernetes from kube-proxy and iptables](https://www.youtube.com/watch?v=bIRwSIwNHC0) 223 | 224 | Martynas Pumputis,KubeCon US 2019 225 | 226 | - [Understanding and Troubleshooting the eBPF Datapath in Cilium](https://www.youtube.com/watch?v=Kmm8Hl57WDU) 227 | 228 | Nathan Sweet,KubeCon US 2019 229 | 230 | - [Transparent Chaos Testing with Envoy, Cilium and BPF](https://www.youtube.com/watch?v=gPvl2NDIWzY) 231 | 232 | Thomas Graf,KubeCon EU 2019 233 | 234 | - [Cilium - Bringing the BPF Revolution to Kubernetes Networking and Security](https://www.youtube.com/watch?v=QmmId1QEE5k) 235 | 236 | Thomas Graf,All Systems Go!,2018 年九月 237 | 238 | - [How to Make Linux Microservice-Aware with eBPF](https://www.youtube.com/watch?v=_Iq1xxNZOAo) 239 | 240 | Thomas Graf,QCon San Francisco,2018 年 241 | 242 | - [Accelerating Envoy with the Linux Kernel](https://www.youtube.com/watch?v=ER9eIXL2_14) 243 | 244 | Thomas Graf,KubeCon EU 2018 245 | 246 | - [Cilium - Network and Application Security with BPF and XDP](https://www.youtube.com/watch?v=ilKlmTDdFgk) 247 | 248 | Thomas Graf,DockerCon Austin,2017 年四月 249 | -------------------------------------------------------------------------------- /ebpf/文章/基于 eBPF 实现容器运行时安全.md: -------------------------------------------------------------------------------- 1 | ### 前言 2 | 3 | 随着容器技术的发展,越来越多业务甚至核心业务开始采用这一轻量级虚拟化方案。作为一项依然处于发展阶段的新技术,容器的安全性在不断提高,也在不断地受到挑战。天翼云云容器引擎于去年 11 月底上线,目前已经在 22 个自研资源池部署上线。天翼云云容器引擎使用 eBPF 技术实现了细粒度容器安全,对主机和容器异常行为进行检测,对有问题的节点和容器进行自动隔离,保证了多租户容器平台容器运行时安全。 4 | 5 | BPF 是一项革命性的技术,可在无需编译内核或加载内核模块的情况下,安全地高效地附加到内核的各种事件上,对内核事件进行监控、跟踪和可观测性。BPF 可用于多种用途,如:开发性能分析工具、软件定义网络和安全等。我很荣幸获得今年 openEuler Summit 大会的演讲资格,做 BPF 技术知识和实践经验的分享。本文将作为技术分享,从 BPF 技术由来、架构演变、BPF 跟踪、以及容器安全面对新挑战,如何基于 BPF 技术实现容器运行时安全等方面进行介绍。 6 | 7 | ### 初出茅庐:BPF 只是一种数据包过滤技术 8 | 9 | BPF 全称是「Berkeley Packet Filter」,中文翻译为「伯克利包过滤器」。它源于 1992 年伯克利实验室,Steven McCanne 和 Van Jacobson 写得一篇名为《The BSD Packet Filter: A New Architecture for User-level Packet Capture》的论文。该论文描述是在 BSD 系统上设计了一种新的用户级的数据包过滤架构。在性能上,新的架构比当时基于栈过滤器的 CSPF 快 20 倍,比之前 Unix 的数据包过滤器,例如:SunOS 的 NIT(The Network Interface Tap )快 100 倍。 10 | 11 | BPF 在数据包过滤上引入了两大革新来提高性能: 12 | 13 | - BPF 是基于寄存器的过滤器,可以有效地工作在基于寄存器结构的 CPU 之上。 14 | - BPF 使用简单无共享的缓存模型。数据包会先经过 BPF 过滤再拷贝到缓存,缓存不会拷贝所有数据包数据,这样可以最大程度地减少了处理的数据量从而提高性能。 15 | 16 | 17 | 18 | [![1.png](http://dockone.io/uploads/article/20210320/5a9d49975f2d4c00addd2a0f21d0f7ce.png)](http://dockone.io/uploads/article/20210320/5a9d49975f2d4c00addd2a0f21d0f7ce.png) 19 | 20 | 21 | 22 | ### Linux 超能力终于到来了:eBPF 架构演变 23 | 24 | #### eBPF 介绍 25 | 26 | 2013 年 BPF 技术沉默了 20 年之后,Alexei Starovoitov 提出了对 BPF 进行重大改写。2013 年 9 月 Alexei 发布了补丁,名为「extended BPF」。eBPF 实现的最初目标是针对现代硬件进行优化。eBPF 增加了寄存器数量,将原有的 2 个 32 位寄存器增加到 10 个 64 位寄存器。由于寄存器数量和宽度的增加,函数参数可以交换更多的信息,编写更复杂的程序。eBPF 生成的指令集比旧的 BPF 解释器生成的机器码执行速度提高了 4 倍。 27 | 28 | 当时 BPF 程序仍然限于内核空间使用,只有少数用户空间程序可以编写内核的 BPF 过滤器,例如:tcpdump 和 seccomp 。2014 年 3 月, 经过 Alexei Starovoitov 和 Daniel Borkmann 的进一步开发, Daniel 将 eBPF 提交到 Linux 内核中。2014 年 6 月 BPF JIT 组件提交到 Linux 3.15 中。2014 年 12 月 系统调用 bpf 提交到 Linux 3.18 中。随后,Linux 4.x 加入了 BPF 对 kprobes、uprobe、tracepoints 和 perf_evnets 支持。至此,eBPF 完成了架构演变,eBPF 扩展到用户空间成为了 BPF 技术的转折点。正如 Alexei 在提交补丁的注释中写到:“这个补丁展示了 eBPF 的潜力”。当前 eBPF 不再局限于网络栈,成为内核顶级的子系统。 29 | 30 | 后来,Alexei 将 eBPF 改为 BPF。原来的 BPF 就被称为 cBPF「classic BPF」。现在 cBPF 已经基本废弃,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码透明地转换成 eBPF 再执行。 31 | 32 | 下面是 cBPF 和 eBPF 的对比: 33 | 34 | [![2.png](http://dockone.io/uploads/article/20210320/6eb4f6ba6842ff213ef329a918077ee7.png)](http://dockone.io/uploads/article/20210320/6eb4f6ba6842ff213ef329a918077ee7.png) 35 | 36 | 37 | 接下来,让我们来看看演变后的 BPF 架构。 38 | 39 | #### eBPF 架构演变 40 | 41 | BPF 是一个通用执行引擎,能够高效地安全地执行基于系统事件的特定代码。BPF 内部由字节码指令,存储对象和帮助函数组成。从某种意义上看,BPF 和 Java 虚拟机功能类似。对于 Java 开发人员而言,可以使用 javac 将高级编程语言编译成机器代码,Java 虚拟机是运行该机器代码的专用程序。相应地,BPF 开发人员可以使用编译器 LLVM 将 C 代码编译成 BPF 字节码,字节码指令在内核执行前必须通过 BPF 验证器进行验证,同时使用内核中的 BPF JIT 模块,将字节码指令直接转成内核可执行的本地指令。编译后的程序附加到内核的各种事件上,以便在 Linux 内核中运行该 BPF 程序。下图是 BPF 架构图: 42 | 43 | [![3.png](http://dockone.io/uploads/article/20210320/0aef2648259cacad4e5f2f153295ae7b.png)](http://dockone.io/uploads/article/20210320/0aef2648259cacad4e5f2f153295ae7b.png) 44 | 45 | 46 | BPF 使内核具有可编程性。BPF 程序是运行在各种内核事件上的小型程序。这与 JavaScript 程序有一些相似之处:JavaScript 是允许在浏览器事件,例如:鼠标单击上运行的微型 Web 程序。BPF 是允许内核在系统和应用程序事件,例如:磁盘 I/O 上运行的微型程序。内核运行 BPF 程序之前,需要知道程序附加的执行点。程序执行点是由 BPF 程序类型确定。通过查看 /kernel-src/sample/bpf/bpf_load.c 可以查看 BPF 程序类型。下面是定义在 bpf 头文件中的 bpf 程序类型: 47 | 48 | [![4.png](http://dockone.io/uploads/article/20210320/56f462562057768dbbbdf36d6dd47cfa.png)](http://dockone.io/uploads/article/20210320/56f462562057768dbbbdf36d6dd47cfa.png) 49 | 50 | 51 | BPF 映射提供了内核和用户空间双向数据共享,允许用户从内核和用户空间读取和写入数据。BPF 映射的数据结构类型可以从简单数组、哈希映射到自定义类型映射。下面是定义在 BPF 头文件中的 BPF 映射类型: 52 | 53 | [![5.png](http://dockone.io/uploads/article/20210320/4203406ac4d48e44111a6104bbe1eea0.png)](http://dockone.io/uploads/article/20210320/4203406ac4d48e44111a6104bbe1eea0.png) 54 | 55 | 56 | 57 | #### BPF 与传统 Linux 内核模块的对比 58 | 59 | BPF 看上去更像内核模块,所以总是会拿来与 Linux 内核模块方式进行对比,但 BPF 与内核模块不同。BPF 在安全性、入门门槛上及高性能上比内核模块都有优势。 60 | 61 | 传统 Linux 内核模块开发,内核开发工程师通过直接修改内核代码,每次功能的更新都需要重新编译打包内核代码。内核工程师可以开发即时加载的内核模块,在运行时加载到 Linux 内核中,从而实现扩展内核功能的目的。然而每次内核版本的官方更新,可能会引起内核 API 的变化,因此你编写的内核模块可能会随着每一个内核版本的发布而不可用,这样就必须得为每次的内核版本更新调整你的模块代码,并且,错误的代码会造成内核直接崩溃。 62 | 63 | BPF 具有强安全性。BPF 程序不需要重新编译内核,并且 BPF 验证器会保证每个程序能够安全运行,确保内核本身不会崩溃。BPF 虚拟机会使用 BPF JIT 编译器将 BPF 字节码生成本地机器字节码,从而能获得本地编译后的程序运行速度。 64 | 65 | 下面是 BPF 与 Linux 内核模块的对比: 66 | 67 | [![6.png](http://dockone.io/uploads/article/20210320/790c0b51c2f28840733ca8832a548c3c.png)](http://dockone.io/uploads/article/20210320/790c0b51c2f28840733ca8832a548c3c.png) 68 | 69 | 70 | 71 | ### BPF 实践中的第一公民:BPF 跟踪 72 | 73 | BPF 跟踪是 Linux 可观测性的新方法。在 BPF 技术的众多应用场景中,BPF 跟踪是应用最广泛的。2013 年 12 月 Alexei 已将 eBPF 用于跟踪。BPF 跟踪支持的各种内核事件包括:kprobes、uprobes、tracepoint 、USDT 和 perf_events: 74 | 75 | - kprobes:实现内核动态跟踪。kprobes 可以跟踪到 Linux 内核中的函数入口或返回点,但是不是稳定 ABI 接口,可能会因为内核版本变化导致,导致跟踪失效。 76 | - uprobes:用户级别的动态跟踪。与 kprobes 类似,只是跟踪的函数为用户程序中的函数。 77 | - tracepoints:内核静态跟踪。tracepoints 是内核开发人员维护的跟踪点,能够提供稳定的 ABI 接口,但是由于是研发人员维护,数量和场景可能受限。 78 | - USDT:为用户空间的应用程序提供了静态跟踪点。 79 | - perf_events:定时采样和 PMC。 80 | 81 | 82 | 83 | [![7.png](http://dockone.io/uploads/article/20210320/35c2898469dbdc56b9bda0935ebbaf73.png)](http://dockone.io/uploads/article/20210320/35c2898469dbdc56b9bda0935ebbaf73.png) 84 | 85 | 86 | 87 | ### 容器安全 88 | 89 | #### 容器生态链带来新挑战 90 | 91 | 虚拟机(VM)是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此占用大量空间。VM 启动也较慢。 92 | 93 | 容器是一种应用层抽象,用于将代码和依赖资源打包在一起。多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行。与虚拟机相比,容器占用的空间比较少(容器镜像大小通常只有几十兆),瞬间就能完成启动。 94 | 95 | 容器技术面临的新挑战: 96 | 97 | - 容器共享宿主机内核,隔离性相对较弱! 98 | - 有 root 权限的用户可以访问所有容器资源!某容器提权后可能影响全局! 99 | - 容器在主机网络之上构建了一层 Overlay 网络,使容器间的互访避开了传统网络安全的防护! 100 | - 容器的弹性伸缩性,使有些容器只是短暂运行,短暂运行的容器行为异常不容易被发现! 101 | - 容器和容器编排给系统增加了新的元素,带来新的风险! 102 | 103 | 104 | 105 | [![8.png](http://dockone.io/uploads/article/20210320/7c2b6b2cfaeb7c2f55ac7d97b83b8864.png)](http://dockone.io/uploads/article/20210320/7c2b6b2cfaeb7c2f55ac7d97b83b8864.png) 106 | 107 | 108 | 109 | #### 容器安全事故:容器逃逸 110 | 111 | 在容器安全问题中,容器逃逸是最为严重,它直接影响到了承载容器的底层基础设施的保密性、完整性和可用性。下面的情况会导致容器逃逸: 112 | 113 | 危险配置导致容器逃逸。在这些年的迭代中,容器社区一直在努力将「纵深防御」、「最小权限」等理念和原则落地。例如,Docker 已经将容器运行时的 Capabilities 黑名单机制改为如今的默认禁止所有 Capabilities,再以白名单方式赋予容器运行所需的最小权限。如果容器启动,配置危险能力,或特权模式容器,或容器以 root 用户权限运行都会导致容器逃逸。下面是容器运行时默认的最小权限。 114 | 115 | [![9.png](http://dockone.io/uploads/article/20210320/9054c95b0ea346f306144e5f632a0d15.png)](http://dockone.io/uploads/article/20210320/9054c95b0ea346f306144e5f632a0d15.png) 116 | 117 | 118 | 危险挂载导致容器逃逸。Docker Socket 是 Docker 守护进程监听的 Unix 域套接字,用来与守护进程通信——查询信息或下发命令。如果在攻击者可控的容器内挂载了该套接字文件(/var/run/docker.sock),容器逃逸就相当容易了,除非有进一步的权限限制。 119 | 120 | [![10.png](http://dockone.io/uploads/article/20210320/3c4c097cf70e7788a374129f493caaaa.png)](http://dockone.io/uploads/article/20210320/3c4c097cf70e7788a374129f493caaaa.png) 121 | 122 | 123 | 下面通过一个小实验来展示这种逃逸可能性: 124 | 125 | 1、准备 dockertest 镜像,该镜像是基于 ubuntu 镜像安装 docker,通过 docker commit 生成。 126 | 127 | 2、创建一个容器并挂载 /var/run/docker.sock: 128 | 129 | ``` 130 | [root@bpftest ~]#docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock dockertest 131 | ``` 132 | 133 | 134 | 3、接着使用该客户端通过 Docker Socket 与 Docker 守护进程通信,发送命令创建并运行一个新的容器,将宿主机的根目录挂载到新创建的容器内部; 135 | 136 | ``` 137 | [root@bpftest ~]#docker exec -it /bin/bash 138 | [root@bpftest ~]#docker ps 139 | [root@bpftest ~]#docker run -it -v /:/host dockertest /bin/bash 140 | ``` 141 | 142 | 143 | 144 | [![11.png](http://dockone.io/uploads/article/20210320/064f49325358ec40ad0c15675879a924.png)](http://dockone.io/uploads/article/20210320/064f49325358ec40ad0c15675879a924.png) 145 | 146 | 147 | 相关程序漏洞导致容器逃逸,例如:runc 漏洞 CVE-2019-5736 导致容器逃逸。 148 | 149 | 内核漏洞导致容器逃逸,例如:Copy_on_Write 脏牛漏洞,向 vDSO 内写入 shellcode 并劫持正常函数的调用过程,导致容器逃逸。 150 | 151 | 下面是 2019 年排名最高的容器安全事故列表: 152 | 153 | [![12.png](http://dockone.io/uploads/article/20210320/e459d89916c5ca225e3b7fdebd476759.png)](http://dockone.io/uploads/article/20210320/e459d89916c5ca225e3b7fdebd476759.png) 154 | 155 | 156 | 157 | ### 容器安全主控引擎 158 | 159 | #### 主机和容器异常活动的检测 160 | 161 | 确保容器运行时安全的关键点: 162 | 163 | - 降低容器的攻击面,每个容器以最小权限运行,包括容器挂载的文件系统、网络控制、运行命令等。 164 | - 确保每个用户对不同的容器拥有合适的权限。 165 | - 使用安全容器控制容器之间的访问。当前,Linux 的 Docker 容器技术隔离技术采用 Namespace 和 Cgroups 无法阻止提权攻击或打破沙箱的攻击。 166 | - 使用 eBPF 跟踪技术自动生成容器访问控制权限。包括:容器对文件的可疑访问,容器对系统的可疑调用,容器之间的可疑互访,检测容器的异常进程,对可疑行为进行取证。例如: 167 | - 检测容器运行时是否创建其他进程。 168 | - 检测容器运行时是否存在文件系统读取和写入的异常行为,例如在运行的容器中安装了新软件包或者更新配置。 169 | - 检测容器运行时是否打开了新的监听端口或者建立意外连接的异常网络活动。 170 | - 检测容器中用户操作及可疑的 shell 脚本的执行。 171 | 172 | 173 | 174 | [![13.png](http://dockone.io/uploads/article/20210320/a5271a91a369e7680bf2b904864d7557.png)](http://dockone.io/uploads/article/20210320/a5271a91a369e7680bf2b904864d7557.png) 175 | 176 | 177 | 178 | #### 隔离有问题的容器和节点 179 | 180 | 如果检测到有问题的容器或节点,可以将节点设置为维护状态,或使用网络策略隔离有问题的容器,或将 deployment 的副本数设置为 0,删除有问题的容器。同时,使用 sidecar WAF,进行虚拟补丁等进行容器应用安全防范。 181 | 182 | [![14.png](http://dockone.io/uploads/article/20210320/7f3d60469d852ae5b382b91a495235c2.png)](http://dockone.io/uploads/article/20210320/7f3d60469d852ae5b382b91a495235c2.png) 183 | 184 | 185 | 186 | #### 大型公有云容器平台安全主控引擎 187 | 188 | 下面是天翼云云容器引擎产品为了保证容器运行时安全实现的安全主控引擎: 189 | 190 | - Pod 通过 Sidecar 注入 WAF 组件对容器进行深度攻击防御。 191 | - 容器安全代理 Sage 组件以 Daemonset 形式部署在各个节点上,用来收集容器和主机异常行为,并通过自己的 Sidecar 推送到消息队列中。 192 | - 安全主控引擎组件 jasmine 从消息队列中拉取事件,对数据进行分析,对有故障的容器和主机进行隔离。并将事件推送给 SIEM 安全信息事件管理平台进行管理。 193 | 194 | 195 | 196 | [![15.png](http://dockone.io/uploads/article/20210320/7d7c5b21d1060ad6d1d63c6a5aecb8a2.png)](http://dockone.io/uploads/article/20210320/7d7c5b21d1060ad6d1d63c6a5aecb8a2.png) 197 | 198 | 199 | 200 | > 原文链接:https://mp.weixin.qq.com/s/UiR8rjTt2SgJo5zs8n5Sqg 201 | -------------------------------------------------------------------------------- /io_uring.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring.pdf -------------------------------------------------------------------------------- /io_uring/文档/Boosting Compaction in B-Tree Based Key-Value Store by Exploiting Parallel Reads in Flash SSDs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/Boosting Compaction in B-Tree Based Key-Value Store by Exploiting Parallel Reads in Flash SSDs.pdf -------------------------------------------------------------------------------- /io_uring/文档/Enabling Financial-Grade Secure Infrastructure with Confidential Computing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/Enabling Financial-Grade Secure Infrastructure with Confidential Computing.pdf -------------------------------------------------------------------------------- /io_uring/文档/IO-uring speed the RocksDB & TiKV.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/IO-uring speed the RocksDB & TiKV.pdf -------------------------------------------------------------------------------- /io_uring/文档/Improved Storage Performance Using the New Linux Kernel I.O Interface.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/Improved Storage Performance Using the New Linux Kernel I.O Interface.pdf -------------------------------------------------------------------------------- /io_uring/文档/O Stack.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/O Stack.pdf -------------------------------------------------------------------------------- /io_uring/文档/O is faster than the OS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/O is faster than the OS.pdf -------------------------------------------------------------------------------- /io_uring/文档/Programming Emerging Storage Interfaces.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/Programming Emerging Storage Interfaces.pdf -------------------------------------------------------------------------------- /io_uring/文档/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /io_uring/文档/StefanMetzmacher_sambaxp2021_multichannel_io-uring-rev0-presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/StefanMetzmacher_sambaxp2021_multichannel_io-uring-rev0-presentation.pdf -------------------------------------------------------------------------------- /io_uring/文档/The Evolution of File Descriptor Monitoring in Linux.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/The Evolution of File Descriptor Monitoring in Linux.pdf -------------------------------------------------------------------------------- /io_uring/文档/io_uring-BPF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/io_uring-BPF.pdf -------------------------------------------------------------------------------- /io_uring/文档/io_uring-徐浩-阿里云.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/io_uring/文档/io_uring-徐浩-阿里云.pdf -------------------------------------------------------------------------------- /io_uring/文章/Linux 5.1 的 io_uring.md: -------------------------------------------------------------------------------- 1 | 虽然在 Facebook 上分享了一些技术文章的心得,被网友建议说可以在博客上,但似乎之前想在博客上一些比较长且整理过的东西,那么如果放的心得让更多的人看到,并且有机会交流也是的事情,接下来应该会很快将之前的笔记誊不错过来。 2 | 3 | https://www.facebook.com/kkcliu/posts/10157179358206129 4 | 5 | # io_uring 6 | 7 | 前阵子在 epoll 的时候刚刚看到了一个讨论串写出来的 io_uring,其实原本没听过 io_uring 是什么,后来才发现新版的 Linux kernel.1 会知道,这个主要目的是加入这个文章很好的写出原本 Linux 原生 AIO 的问题,其实一般来说 AIO 的效果应该会比 epoll 还好,简单一点的比较可以看 stackoverflow 的,[https: //stackoverflow.com/questions/5844955/whats-事件驱动和异步epoll-and-a之间的差异](https://stackoverflow.com/questions/5844955/whats-the-difference-between-event-driven-and-asynchronous-between-epoll-and-a) 8 | 9 | - epoll 是一个阻塞操作(epoll_wait())——你阻塞线程直到某个事件发生,然后你将事件分派到代码中的不同过程/函数/分支。 10 | - 在 AIO 中,您将回调函数(完成例程)的地址传递给系统,系统会在发生某些事情时调用您的函数。 11 | 12 | 简单来说 epoll 是等待事件发生,才执行,epoll_wait 是一个阻塞的操作,而 AIO 是把的回调函数,AIO 交给系统执行,真正做到了异步,Mysql 的 innodb 也是使用原生 linux蛮推荐多看一下Linux下的问题,所以这里太流行了,可以这样cloudflare [https:](https://blog.cloudflare.com/io_submit-the-epoll-alternative-youve-never-heard-about/) //blog..cloudflare.com/io_submit-the-the-cloudflare.com/io_submit-the-the-cloudflare.com/io_submit-the-the-cloudflare.com/io_submit-the-the-cloudflare.com/io_submit-the-the-cloudflare.com [-youve-never-hear-about/](https://blog.cloudflare.com/io_submit-the-epoll-alternative-youve-never-heard-about/),有介绍如何评价使用 AIO,也提到了 Lin Alternative 的一些问题,你的地方好像提到了我们对 AIO 的: 13 | 14 | > AIO 是一种可怕的临时设计,其主要借口是“其他天赋较低的人做出了该设计,我们正在实施它以实现兼容性,因为数据库人员 - 他们很少有任何品味 - 实际使用它”。但是AIO总是真的很丑。 15 | 16 | 除了是又看到 Facebook 分享的幻灯片:[https](https://www.slideshare.net/ennael/kernel-recipes-2019-faster-io-through-iouring) : //www.slideshare.net/ennael/kernel-recipes-2019-faster-io-through-iouring 和 Hackernews [https://news.ycombinator.com/item?id =19843464](https://news.ycombinator.com/item?id=19843464)的介绍,最重要的是性能真的好上多,从这里[https://github.com/frevib/io_uring-echo-server/blob/io-uring-feat-fast-poll/benchmarks /benchmarks.md](https://github.com/frevib/io_uring-echo-server/blob/io-uring-feat-fast-poll/benchmarks/benchmarks.md),可以找到 epoll vs io_uring 的 benchmark ,可以快速到 io_uring 的功效到 40% 以上。 17 | 18 | ![img](https://kkc.github.io/2020/08/19/io-uring/benchmark.png) 19 | 20 | 然后也看到了很多不同的项目,比如libuv、rust、ceph、rocksdb,正在讨论数据库和云相关的集成,这对相关的产业会有很大的影响,省的成本光用想的就很重要很神奇,虽然要等到大家升到5.1不容易,但是会期待这个发展了。 21 | 22 | 后记:首席副总裁 Champ 的问题是因为只有 DIRECT_IO,对于很多程序来说,都是为了解决 Linux AIO 的问题。 23 | 24 | # 参考 25 | 26 | - https://kernel.dk/io_uring-whatsnew.pdf 27 | - https://github.com/agnivade/frodo 28 | - https://github.com/hodgesds/iouring-go 29 | - https://lwn.net/Articles/776703/ 30 | - https://www.slideshare.net/ennael/kernel-recipes-2019-faster-io-through-iouring 31 | 32 | > 原文链接:https://kkc.github.io/2020/08/19/io-uring/ 33 | 34 | -------------------------------------------------------------------------------- /io_uring/文章/What is io_uring.md: -------------------------------------------------------------------------------- 1 | `io_uring` is a new asynchronous I/O API for Linux created by Jens Axboe from Facebook. It aims at providing an API without the limitations of the current [select(2)](http://man7.org/linux/man-pages/man2/select.2.html), [poll(2)](http://man7.org/linux/man-pages/man2/poll.2.html), [epoll(7)](http://man7.org/linux/man-pages/man7/epoll.7.html) or [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) family of system calls, which we discussed in the previous section. Given that users of asynchronous programming models choose it in the first place for performance reasons, it makes sense to have an API that has very low performance overheads. We shall see how `io_uring` achieves this in subsequent sections. 2 | 3 | ## The io_uring interface 4 | 5 | The very name io_uring comes from the fact that the interfaces uses ring buffers as the main interface for kernel-user space communication. While there are system calls involved, they are kept to a minimum and there is a polling mode you can use to reduce the need to make system calls as much as possible. 6 | 7 | See also 8 | 9 | - [Submission queue polling tutorial](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) with example program. 10 | 11 | ### The mental model 12 | 13 | The mental model you need to construct in order to use `io_uring` to build programs that process I/O asynchronously is fairly simple. 14 | 15 | - There are 2 ring buffers, one for submission of requests (submission queue or SQ) and the other that informs you about completion of those requests (completion queue or CQ). 16 | - These ring buffers are shared between kernel and user space. You set these up with [`io_uring_setup()`](https://unixism.net/loti/ref-iouring/io_uring_setup.html#c.io_uring_setup) and then mapping them into user space with 2 [mmap(2)](http://man7.org/linux/man-pages/man2/mmap.2.html) calls. 17 | - You tell io_uring what you need to get done (read or write a file, accept client connections, etc), which you describe as part of a submission queue entry (SQE) and add it to the tail of the submission ring buffer. 18 | - You then tell the kernel via the [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) system call that you’ve added an SQE to the submission queue ring buffer. You can add multiple SQEs before making the system call as well. 19 | - Optionally, [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) can also wait for a number of requests to be processed by the kernel before it returns so you know you’re ready to read off the completion queue for results. 20 | - The kernel processes requests submitted and adds completion queue events (CQEs) to the tail of the completion queue ring buffer. 21 | - You read CQEs off the head of the completion queue ring buffer. There is one CQE corresponding to each SQE and it contains the status of that particular request. 22 | - You continue adding SQEs and reaping CQEs as you need. 23 | - There is a [polling mode available](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll), in which the kernel polls for new entries in the submission queue. This avoids the system call overhead of calling [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) every time you submit entries for processing. 24 | 25 | See also 26 | 27 | - [The Low-level io_uring Interface](https://unixism.net/loti/low_level.html#low-level) 28 | 29 | ## io_uring performance 30 | 31 | Because of the shared ring buffers between the kernel and user space, io_uring can be a zero-copy system. Copying bytes around becomes necessary when system calls that transfer data between kernel and user space are involved. But since the bulk of the communication in `io_uring` is via buffers shared between the kernel and user space, this huge performance overhead is completely avoided. While system calls (and we’re used to making them a lot) may not seem like a significant overhead, in high performance applications, making a lot of them will begin to matter. Also, system calls are not as cheap as they used to be. Throw in workarounds the operating system has in place to deal with [Specter and Meltdown](https://meltdownattack.com/), we are talking non-trivial overheads. So, avoiding system calls as much as possible is a fantastic idea in high-performance applications indeed. 32 | 33 | While using synchronous programming interfaces or even when using asynchronous programming interfaces under Linux, there is at least one system call involved in the submission of each request. In `io_uring`, you can add several requests, simply by adding multiple SQEs each describing the I/O operation you want and make a single call to io_uring_enter. For starers, that’s a win right there. But it gets better. 34 | 35 | You can have the kernel poll and pick up your SQEs for processing as you add them to the submission queue. This avoids the [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) call you need to make to tell the kernel to pick up SQEs. For high-performance applications, this means even lesser system call overheads. See [the submission queue polling tutorial](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) for more details. 36 | 37 | With some clever use of shared ring buffers, `io_uring` performance is really memory-bound, since in polling mode, we can do away with system calls altogether. It is important to remember that performance benchmarking is a relative process with some kind of a common point of reference. According to the [io_uring paper](https://kernel.dk/io_uring.pdf), on a reference machine, in polling mode, `io_uring` managed to clock 1.7M 4k IOPS, while [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) manages 608k. Although much more than double, this isn’t a fair comparison since [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) doesn’t feature a polled mode. But even when polled mode is disabled, `io_uring` hits 1.2M IOPS, close to double that of [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html). 38 | 39 | To check the raw throughput of the `io_uring` interface, there is a no-op request type. With this, on the reference machine, `io_uring` achieves 20M messages per second. See [`io_uring_prep_nop()`](https://unixism.net/loti/ref-liburing/submission.html#c.io_uring_prep_nop) for more details. 40 | 41 | ## An example using the low-level API 42 | 43 | Writing a small program that reads files and prints them on to the console, like how the Unix `cat` utility does might be a good starting point to get your hands wet with the `io_uring` API. Please see the next chapter for one such example. 44 | 45 | ## Just use liburing 46 | 47 | While being acquainted with the low-level `io_uring` API is most certainly a good thing, in real, serious programs you probably want to use the higher-level interface provided by liburing. Programs like [QEMU](https://qemu.org/) already use it. If liburing never existed, you’d have built some abstraction layer over the low-lever `io_uring` interface. liburing does that for you and it is a well thought-out interface as well. In short, you should probably put in some effort to understand how the low-level `io_uring` interface works, but by default you should really be using `liburing` in your programs. 48 | 49 | While there is a reference section here for it, there are some examples based on `liburing` we’ll see in the subsequent chapters. 50 | 51 | > 原文链接:https://unixism.net/loti/what_is_io_uring.html 52 | -------------------------------------------------------------------------------- /io_uring/文章/io_uring 系统性整理.md: -------------------------------------------------------------------------------- 1 | # io_uring 系统性整理 2 | 3 | 这里有个误解,I/O模型其实是针对整个系统的所有I/O操作的,但是平时很少对文件系统使用异步读写,同步或直接映射的情况比较多。更别提多路复用了,这个机制基本只用在network中。 4 | 5 | [lwn Kernel article index](https://lwn.net/Kernel/Index/) 6 | 7 | ## I/O 模型 8 | 9 | - blocking I/O 10 | 11 | ![blocking](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/io-blocking.gif) 12 | 13 | 同步阻塞,直到内核收到数据返回给线程。 14 | 15 | - nonblocking I/O 16 | 17 | ![nonblocking](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/io-nonblocking.gif) 18 | 19 | 同步不阻塞,但是如果内核没收到数据会返回一个 `EWOULDBLOCK` 20 | 21 | - I/O multiplexing (select and poll) 22 | 23 | ![multiplex](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/io-multiplex.gif) 24 | 25 | 异步阻塞,使用selet(using select requires two system calls instead of one)、poll系统调用循环等待socket可读时,使用recvfrom收取数据。主要优势在于能够在单线程监控多个文件描述符fd。 26 | 27 | 初次之外还有epoll,使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次 28 | 29 | 优点有: 30 | 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。 31 | 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 32 | 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。 33 | 34 | 这种方法基本等价于 一个进程创建多个线程,每个线程维护一个blocking I/O 35 | 36 | - signal driven I/O (SIGIO) 37 | 38 | ![multiplex](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/io-sigio.gif) 39 | 40 | 非阻塞,通过sigaction系统调用安装signal handler,当datagram数据报可读时,向I/O接收进程发送SIGIO信号,可以在signal handler里面读这个数据,然后通知main loop;也可以先通知main loop,让main loop去读这个数据。 41 | 42 | - asynchronous I/O (the POSIX aio_functions) 43 | 44 | ![aio](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/io-aio.gif) 45 | 46 | 异步非阻塞,也是调用aio_read之后立刻返回,和SIGIO的区别是直到接收到数据并将数据传输到用户时,才产生完成信号。 47 | 48 | ### comparison 49 | 50 | ![comparison](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/io-comparison.gif) 51 | 52 | 参考: 53 | [io models](http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html) 54 | [彻底理解 IO多路复用](https://juejin.im/post/6844904200141438984) 55 | [聊聊IO多路复用之select、poll、epoll详解](https://www.jianshu.com/p/dfd940e7fca2) 56 | 57 | ## Asynchronous I/O 58 | 59 | 首先确定这里的AIO是内核态的,由libaio封装的系统调用运行库,而不是glibc用户态AIO,使用多线程模拟的。 60 | 61 | linux kernel AIO的主要缺点在于项目泥潭,bug太多,项目设计和领导更换,而且实现比较复杂,直到现在只能比较稳定支持以O_DIRECT(直接映射修改,bypass page cache)方式打开文件,需要自己处理buffer、offset对其这些问题,不能用page cache层以bio的方式读写block数据。 62 | 63 | 因为使用page buffer层时涉及到block driver里面的队列,相比O_DIRECT多出很多阻塞点,因此实现起来比较令人恼火。因此这个项目根本就没实现起来。 64 | 65 | 因此io_uring的主要对比对象是多路复用和DPDK、SPDK,是一个事实上的新异步IO API 66 | 67 | Linux AIO does suffer from a number of ailments. The subsystem is quite complex and requires explicit code in any I/O target for it to be supported. 68 | 69 | 实现不了的地方基本上都开一个kernel thread跑,感觉开销更大了。 70 | 71 | 参考: 72 | [Linux Asynchronous I/O](https://oxnz.github.io/2016/10/13/linux-aio/) 73 | 74 | [Fixing asynchronous I/O, again](https://lwn.net/Articles/671649/) 75 | [Linux kernel AIO这个奇葩](https://www.aikaiyuan.com/4556.html) 76 | [2017Toward non-blocking asynchronous I/O](https://lwn.net/Articles/724198/) 77 | 78 | ## io_uring 79 | 80 | 参考: 81 | 82 | [Kernel Recipes 2019 - Faster IO through io_uring](https://www.youtube.com/watch?v=-5T4Cjw46ys) 83 | 84 | [20190115Ringing in a new asynchronous I/O API](https://lwn.net/Articles/776703/) 85 | 86 | [20200715Operations restrictions for io_uring](https://lwn.net/Articles/826053/) 87 | 88 | [20200320Automatic buffer selection for io_uring](https://lwn.net/Articles/815491/) 89 | 90 | [20200124The rapid growth of io_uring](https://lwn.net/Articles/810414/) 91 | 92 | [20200511Hussain: Lord of the io_uring](https://lwn.net/Articles/820220/) 93 | 94 | [Linux异步IO新时代:io_uring](https://kernel.taobao.org/2019/06/io_uring-a-new-linux-asynchronous-io-API/) 95 | 96 | [20200716io_uring: add restrictions to support untrusted applications and guests](https://lwn.net/Articles/826255/) 97 | 98 | [20200225io_uring support for automatic buffers](https://lwn.net/Articles/813311/) 99 | 100 | [io_uring(1) – 我们为什么会需要 io_uring](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/) 101 | 102 | [linux “io_uring” 提权漏洞(CVE-2019-19241)分析](https://www.anquanke.com/post/id/200486) 103 | 104 | [io_uring(2)- 从创建必要的文件描述符 fd 开始](https://www.byteisland.com/io_uring(2)-从创建必要的文件描述符-fd-开始/) 105 | 106 | uring这个词没有翻译”something that looks a little less like io_urine”. 107 | 108 | 这是一个为了高速I/O提出的新的一系列系统调用,简单来说就是新的ring buffer。之前的异步I/O策略是libaio,这个机制饱受诟病,于是Jens Axboe直接提出io_uring,性能远超aio。 109 | 110 | 从5.7开始超出纯I/O的范畴,io_uring开始为一部接口提供FAST POLL机制,用户无需再像使用select、event poll等多路复用机制来监听文件句柄,只要把读写请求直接丢到io_uring的submission queue中提交 ,当文件句柄不可读写时,内核会主动添加poll handler,当文件句柄可读写时主动调用poll handler再次下发读写请求,从而减少系统调用次数提高性能 111 | 112 | 这是一个线程粒度的异步I/O机制,分为 submission queue和completion queue,在使用系统调用申请之后,直接返回可以使用mmap映射的file discriptor。 113 | 114 | 应用程序可以直接使用mmap映射的两个ring buffer直接与内核进行I/O数据传输交换,减少了大量系统调用的开销。 115 | 116 | 具体流程: 117 | 118 | - setup `int io_uring_setup(int entries, struct io_uring_params *params);` 119 | 120 | 其中entries表示submission and completion queues两个队列的大小 121 | 122 | param中设置两个队列和具体的状态 123 | 124 | ``` 125 | struct io_uring_params { 126 | __u32 sq_entries; 127 | __u32 cq_entries; 128 | __u32 flags; 129 | __u16 resv[10]; 130 | struct io_sqring_offsets sq_off; 131 | struct io_cqring_offsets cq_off; 132 | }; 133 | ``` 134 | 135 | 最终实现目的通过file descriptor与内核共享ring buffer 136 | 137 | ``` 138 | subqueue = mmap(0, params.sq_off.array + params.sq_entries*sizeof(__u32), 139 | PROT_READ|PROT_WRITE|MAP_SHARED|MAP_POPULATE, 140 | ring_fd, IORING_OFF_SQ_RING); 141 | 142 | sqentries = mmap(0, params.sq_entries*sizeof(struct io_uring_sqe), 143 | PROT_READ|PROT_WRITE|MAP_SHARED|MAP_POPULATE, 144 | ring_fd, IORING_OFF_SQES); 145 | 146 | 147 | cqentries = mmap(0, params.cq_off.cqes + params.cq_entries*sizeof(struct io_uring_cqe), 148 | PROT_READ|PROT_WRITE|MAP_SHARED|MAP_POPULATE, 149 | ring_fd, IORING_OFF_CQ_RING); 150 | ``` 151 | 152 | 相关资料: 153 | [Ringing in a new asynchronous I/O API](https://lwn.net/Articles/776703/) 154 | [The rapid growth of io_uring](https://lwn.net/Articles/810414/) 155 | 156 | ``` 157 | #include 158 | #include 159 | #include 160 | #include 161 | 162 | int main() 163 | { 164 | struct io_uring ring; 165 | io_uring_queue_init(32, &ring, 0); 166 | 167 | struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); 168 | int fd = open("/home/carter/test.txt", O_WRONLY | O_CREAT); 169 | struct iovec iov = { 170 | .iov_base = "Hello world", 171 | .iov_len = strlen("Hello world"), 172 | }; 173 | io_uring_prep_writev(sqe, fd, &iov, 1, 0); 174 | io_uring_submit(&ring); 175 | 176 | struct io_uring_cqe *cqe; 177 | 178 | for (;;) { 179 | io_uring_peek_cqe(&ring, &cqe); 180 | if (!cqe) { 181 | puts("Waiting..."); 182 | // accept 新连接,做其他事 183 | } else { 184 | puts("Finished."); 185 | break; 186 | } 187 | } 188 | io_uring_cqe_seen(&ring, cqe); 189 | io_uring_queue_exit(&ring); 190 | } 191 | ``` 192 | 193 | ## 总结邮件 194 | 195 | 今天重新思考了一下IO模型,并阅读了io_uring和多路复用相关代码,感觉突然想通了。io_uring取代了AIO而不是取代了usercopy,usercopy部署的安全策略未必适用擅长传输大量数据的io_uring,这个工作可以推后再进行。具体如下。 196 | 197 | 一、将I/O模型的设计和实现分离 198 | 199 | I/O在操作系统中含义包括与I/O设备通信和输入输出数据,I/O模型是针对第一种含义提出的解决方案。 200 | 201 | linux在实现I/O的过程中参考了这些模型进行实现,但并没有在同一层次进行实现。例如LKM开发中定义的file_operations实际只包括 read(同步)/read_iter(异步)/mmap/poll 等函数指针,在I/O模型中的阻塞同步和非阻塞同步的情况可以通过使用read/read_iter 附加O_NONBLOCK的方式实现。 202 | 203 | select epoll 多路复用和AIO这些I/O模型,则是分别在与之相同或不同的层次对底层函数进行封装。 204 | 205 | 例如 select 系统调用是在内核层通过vfs_poll遍历相关的file_descriptor,glibc实现的AIO是在用户空间多线程调用这些阻塞/非阻塞的同步/异步系统调用,epoll(更像是一个通知机制)是将select/poll中需要每次都传递的file descriptor都保存在内核中,减少了usercopy;通过event监听callback进行通知,减少了对fd的遍历开销。 206 | 207 | 二、思考io_uring的设计和实现 208 | 209 | io_uring的设计借鉴了以上的优点,在内核空间通过kthread实现对阻塞读写任务的托管,并加入了zero copy特性,开发者可以通过一次系统调用唤醒线程一直向共享ringbuffer中写数据,而不是每次写数据都需要系统调用,这在内核和用户通信范畴内很大程度上减少了系统调用的次数,消除了usercopy的负担。 210 | 211 | 但无法否认io_uring是对下层file_operations的封装,下层函数又是device driver file_operations的封装(甚至对buffer I/O中间还有一层page cache、一层block layer、一层I/O schedule),因此io_uring在许多情况无法获得SPDK用户空间直通driver的性能优势。 212 | 213 | 三、对安全问题的思考 214 | 215 | 我目前理解的安全风险主要来自于usercopy造成的out-of-bound、information/pointer leakage和race情况,尤其是struct结构可能存在的函数/数据指针,但是io_uring消除掉的usercopy主要负责大量I/O数据的传输,而非带有指针的控制数据结构(io_uring中的控制数据也在用copy_*_user传输,如图),因此对安全问题的认识比我预期要复杂一些(主要问题可能是OOB和Iago攻击),需要加深对漏洞形式的理解,但好处是急迫程度下降了。 216 | 217 | 我只能继续积累漏洞阅读量提升认知水平,思考copy_*_user可以部署的安全机制和策略。 218 | 219 | ![usercopy](https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/usercopy.PNG) 220 | 221 | 四、总结 222 | 223 | 我这阶段应该继续把重点放在kernel extension问题的描述上,对这个问题我已经基本有了一定想法,大致是将威胁模型定位为kernel rootkits,通过修改页表或切换地址空间构建运行时沙箱,使用可信基截获、保护gateway,使用hook方式监控driver相关函数和数据I/O。可以将性能的提升和对DPDK/SPDK使用的UIO和VFIO保护作为贡献点(这可能是这次突发奇想的意外收获),现在面临的问题是不确定相关方案是否有实现、近期driver保护方案相关只有四篇。 224 | 225 | > 原文链接:https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/ 226 | -------------------------------------------------------------------------------- /io_uring/文章/io_uring_register.md: -------------------------------------------------------------------------------- 1 | io_uring_register - register files or user buffers for asynchronous I/O 2 | 3 | ## SYNOPSIS 4 | 5 | ``` 6 | #include 7 | ``` 8 | 9 | - int `io_uring_register`(unsigned int *fd*, unsigned int *opcode*, void **arg*, unsigned int *nr_args*) 10 | 11 | 12 | 13 | ## DESCRIPTION 14 | 15 | The **io_uring_register**() system call registers user buffers or files for use in an **io_uring**(7) instance referenced by *fd*. Registering files or user buffers allows the kernel to take long term references to internal data structures or create long term mappings of application memory, greatly reducing per-I/O overhead. 16 | 17 | *fd* is the file descriptor returned by a call to **io_uring_setup**(2). *opcode* can be one of: 18 | 19 | - **IORING_REGISTER_BUFFERS** 20 | 21 | *arg* points to a *struct iovec* array of *nr_args* entries. The buffers associated with the iovecs will be locked in memory and charged against the user’s **RLIMIT_MEMLOCK** resource limit. See **getrlimit**(2) for more information. Additionally, there is a size limit of 1GiB per buffer. Currently, the buffers must be anonymous, non-file-backed memory, such as that returned by **malloc**(3) or **mmap**(2) with the **MAP_ANONYMOUS** flag set. It is expected that this limitation will be lifted in the future. Huge pages are supported as well. Note that the entire huge page will be pinned in the kernel, even if only a portion of it is used. 22 | 23 | After a successful call, the supplied buffers are mapped into the kernel and eligible for I/O. To make use of them, the application must specify the **IORING_OP_READ_FIXED** or **IORING_OP_WRITE_FIXED** opcodes in the submission queue entry (see the *struct io_uring_sqe* definition in **io_uring_enter**(2)), and set the *buf_index* field to the desired buffer index. The memory range described by the submission queue entry’s *addr* and *len* fields must fall within the indexed buffer. 24 | 25 | It is perfectly valid to setup a large buffer and then only use part of it for an I/O, as long as the range is within the originally mapped region. 26 | 27 | An application can increase or decrease the size or number of registered buffers by first unregistering the existing buffers, and then issuing a new call to **io_uring_register**() with the new buffers. 28 | 29 | Note that registering buffers will wait for the ring to idle. If the application currently has requests in-flight, the registration will wait for those to finish before proceeding. 30 | 31 | An application need not unregister buffers explicitly before shutting down the io_uring instance. Available since 5.1. 32 | 33 | - **IORING_UNREGISTER_BUFFERS** 34 | 35 | This operation takes no argument, and *arg* must be passed as NULL. All previously registered buffers associated with the io_uring instance will be released. Available since 5.1. 36 | 37 | - **IORING_REGISTER_FILES** 38 | 39 | Register files for I/O. *arg* contains a pointer to an array of *nr_args* file descriptors (signed 32 bit integers). 40 | 41 | To make use of the registered files, the **IOSQE_FIXED_FILE** flag must be set in the *flags* member of the *struct io_uring_sqe*, and the *fd* member is set to the index of the file in the file descriptor array. 42 | 43 | The file set may be sparse, meaning that the **fd** field in the array may be set to **-1.** See **IORING_REGISTER_FILES_UPDATE** for how to update files in place. 44 | 45 | Note that registering files will wait for the ring to idle. If the application currently has requests in-flight, the registration will wait for those to finish before proceeding. See **IORING_REGISTER_FILES_UPDATE** for how to update an existing set without that limitation. 46 | 47 | Files are automatically unregistered when the io_uring instance is torn down. An application need only unregister if it wishes to register a new set of fds. Available since 5.1. 48 | 49 | - **IORING_REGISTER_FILES_UPDATE** 50 | 51 | This operation replaces existing files in the registered file set with new ones, either turning a sparse entry (one where fd is equal to -1) into a real one, removing an existing entry (new one is set to -1), or replacing an existing entry with a new existing entry. *arg* must contain a pointer to a struct io_uring_files_update, which contains an offset on which to start the update, and an array of file descriptors to use for the update. *nr_args* must contain the number of descriptors in the passed in array. Available since 5.5. 52 | 53 | - **IORING_UNREGISTER_FILES** 54 | 55 | This operation requires no argument, and *arg* must be passed as NULL. All previously registered files associated with the io_uring instance will be unregistered. Available since 5.1. 56 | 57 | - **IORING_REGISTER_EVENTFD** 58 | 59 | It’s possible to use eventfd(2) to get notified of completion events on an io_uring instance. If this is desired, an eventfd file descriptor can be registered through this operation. *arg* must contain a pointer to the eventfd file descriptor, and *nr_args* must be 1. Available since 5.2. 60 | 61 | - **IORING_REGISTER_EVENTFD_ASYNC** 62 | 63 | This works just like **IORING_REGISTER_EVENTFD** , except notifications are only posted for events that complete in an async manner. This means that events that complete inline while being submitted do not trigger a notification event. The arguments supplied are the same as for **IORING_REGISTER_EVENTFD.** Available since 5.6. 64 | 65 | - **IORING_UNREGISTER_EVENTFD** 66 | 67 | Unregister an eventfd file descriptor to stop notifications. Since only one eventfd descriptor is currently supported, this operation takes no argument, and *arg* must be passed as NULL and *nr_args* must be zero. Available since 5.2. 68 | 69 | - **IORING_REGISTER_PROBE** 70 | 71 | This operation returns a structure, io_uring_probe, which contains information about the opcodes supported by io_uring on the running kernel. *arg* must contain a pointer to a struct io_uring_probe, and *nr_args* must contain the size of the ops array in that probe struct. The ops array is of the type io_uring_probe_op, which holds the value of the opcode and a flags field. If the flags field has **IO_URING_OP_SUPPORTED** set, then this opcode is supported on the running kernel. Available since 5.6. 72 | 73 | - **IORING_REGISTER_PERSONALITY** 74 | 75 | This operation registers credentials of the running application with io_uring, and returns an id associated with these credentials. Applications wishing to share a ring between separate users/processes can pass in this credential id in the sqe **personality** field. If set, that particular sqe will be issued with these credentials. Must be invoked with *arg* set to NULL and *nr_args* set to zero. Available since 5.6. 76 | 77 | - **IORING_UNREGISTER_PERSONALITY** 78 | 79 | This operation unregisters a previously registered personality with io_uring. *nr_args* must be set to the id in question, and *arg* must be set to NULL. Available since 5.6. 80 | 81 | ## RETURN VALUE 82 | 83 | On success, **io_uring_register**() returns 0. On error, -1 is returned, and *errno* is set accordingly. 84 | 85 | ## ERRORS 86 | 87 | - **EBADF** 88 | 89 | One or more fds in the *fd* array are invalid. 90 | 91 | - **EBUSY** 92 | 93 | **IORING_REGISTER_BUFFERS** or **IORING_REGISTER_FILES** was specified, but there were already buffers or files registered. 94 | 95 | - **EFAULT** 96 | 97 | buffer is outside of the process’ accessible address space, or *iov_len* is greater than 1GiB. 98 | 99 | - **EINVAL** 100 | 101 | **IORING_REGISTER_BUFFERS** or **IORING_REGISTER_FILES** was specified, but *nr_args* is 0. 102 | 103 | - **EINVAL** 104 | 105 | **IORING_REGISTER_BUFFERS** was specified, but *nr_args* exceeds **UIO_MAXIOV** 106 | 107 | - **EINVAL** 108 | 109 | **IORING_UNREGISTER_BUFFERS** or **IORING_UNREGISTER_FILES** was specified, and *nr_args* is non-zero or *arg* is non-NULL. 110 | 111 | - **EMFILE** 112 | 113 | **IORING_REGISTER_FILES** was specified and *nr_args* exceeds the maximum allowed number of files in a fixed file set. 114 | 115 | - **EMFILE** 116 | 117 | **IORING_REGISTER_FILES** was specified and adding *nr_args* file references would exceed the maximum allowed number of files the user is allowed to have according to the **RLIMIT_NOFILE** resource limit and the caller does not have **CAP_SYS_RESOURCE** capability. Note that this is a per user limit, not per process. 118 | 119 | - **ENOMEM** 120 | 121 | Insufficient kernel resources are available, or the caller had a non-zero **RLIMIT_MEMLOCK** soft resource limit, but tried to lock more memory than the limit permitted. This limit is not enforced if the process is privileged (**CAP_IPC_LOCK**). 122 | 123 | - **ENXIO** 124 | 125 | **IORING_UNREGISTER_BUFFERS** or **IORING_UNREGISTER_FILES** was specified, but there were no buffers or files registered. 126 | 127 | - **ENXIO** 128 | 129 | Attempt to register files or buffers on an io_uring instance that is already undergoing file or buffer registration, or is being torn down. 130 | 131 | - **EOPNOTSUPP** 132 | 133 | User buffers point to file-backed memory. 134 | 135 | > 原文链接:https://unixism.net/loti/ref-iouring/io_uring_register.html 136 | 137 | -------------------------------------------------------------------------------- /io_uring/文章/io_uring_setup.md: -------------------------------------------------------------------------------- 1 | io_uring_setup - setup a context for performing asynchronous I/O 2 | 3 | ## SYNOPSIS 4 | 5 | ``` 6 | #include 7 | ``` 8 | 9 | - int `io_uring_setup`(u32 *entries*, *struct* io_uring_params **p*) 10 | 11 | 12 | 13 | ## DESCRIPTION 14 | 15 | The io_uring_setup() system call sets up a submission queue (SQ) and completion queue (CQ) with at least *entries* entries, and returns a file descriptor which can be used to perform subsequent operations on the io_uring instance. The submission and completion queues are shared between userspace and the kernel, which eliminates the need to copy data when initiating and completing I/O. 16 | 17 | *params* is used by the application to pass options to the kernel, and by the kernel to convey information about the ring buffers. 18 | 19 | ``` 20 | struct io_uring_params { 21 | __u32 sq_entries; 22 | __u32 cq_entries; 23 | __u32 flags; 24 | __u32 sq_thread_cpu; 25 | __u32 sq_thread_idle; 26 | __u32 features; 27 | __u32 resv[4]; 28 | struct io_sqring_offsets sq_off; 29 | struct io_cqring_offsets cq_off; 30 | }; 31 | ``` 32 | 33 | The *flags*, *sq_thread_cpu*, and *sq_thread_idle* fields are used to configure the io_uring instance. *flags* is a bit mask of 0 or more of the following values ORed together: 34 | 35 | - **IORING_SETUP_IOPOLL** 36 | 37 | Perform busy-waiting for an I/O completion, as opposed to getting notifications via an asynchronous IRQ (Interrupt Request). The file system (if any) and block device must support polling in order for this to work. Busy-waiting provides lower latency, but may consume more CPU resources than interrupt driven I/O. Currently, this feature is usable only on a file descriptor opened using the **O_DIRECT** flag. When a read or write is submitted to a polled context, the application must poll for completions on the CQ ring by calling **io_uring_enter**(2). It is illegal to mix and match polled and non-polled I/O on an io_uring instance. 38 | 39 | - **IORING_SETUP_SQPOLL** 40 | 41 | When this flag is specified, a kernel thread is created to perform submission queue polling. An io_uring instance configured in this way enables an application to issue I/O without ever context switching into the kernel. By using the submission queue to fill in new submission queue entries and watching for completions on the completion queue, the application can submit and reap I/Os without doing a single system call. 42 | 43 | If the kernel thread is idle for more than *sq_thread_idle* milliseconds, it will set the **IORING_SQ_NEED_WAKEUP** bit in the *flags* field of the *struct io_sq_ring*. When this happens, the application must call **io_uring_enter**(2) to wake the kernel thread. If I/O is kept busy, the kernel thread will never sleep. An application making use of this feature will need to guard the **io_uring_enter**(2) call with the following code sequence: 44 | 45 | ``` 46 | /* 47 | * Ensure that the wakeup flag is read after the tail pointer has been 48 | * written. 49 | */ 50 | smp_mb(); 51 | if (*sq_ring->flags & IORING_SQ_NEED_WAKEUP) 52 | io_uring_enter(fd, 0, 0, IORING_ENTER_SQ_WAKEUP); 53 | ``` 54 | 55 | where *sq_ring* is a submission queue ring setup using the *struct io_sqring_offsets* described below. 56 | 57 | > To successfully use this feature, the application must register a set of files to be used for IO through **io_uring_register**(2) using the **IORING_REGISTER_FILES** opcode. Failure to do so will result in submitted IO being errored with **EBADF.** 58 | 59 | - **IORING_SETUP_SQ_AFF** 60 | 61 | If this flag is specified, then the poll thread will be bound to the cpu set in the *sq_thread_cpu* field of the *struct io_uring_params*. This flag is only meaningful when **IORING_SETUP_SQPOLL** is specified. 62 | 63 | - **IORING_SETUP_CQSIZE** 64 | 65 | Create the completion queue with *struct io_uring_params.cq_entries* entries. The value must be greater than *entries*, and may be rounded up to the next power-of-two. 66 | 67 | If no flags are specified, the io_uring instance is setup for interrupt driven I/O. I/O may be submitted using **io_uring_enter**(2) and can be reaped by polling the completion queue. 68 | 69 | The *resv* array must be initialized to zero. 70 | 71 | *features* is filled in by the kernel, which specifies various features supported by current kernel version. 72 | 73 | - **IORING_FEAT_SINGLE_MMAP** 74 | 75 | If this flag is set, the two SQ and CQ rings can be mapped with a single *mmap(2)* call. The SQEs must still be allocated separately. This brings the necessary *mmap(2)* calls down from three to two. 76 | 77 | - **IORING_FEAT_NODROP** 78 | 79 | If this flag is set, io_uring supports never dropping completion events. If a completion event occurs and the CQ ring is full, the kernel stores the event internally until such a time that the CQ ring has room for more entries. If this overflow condition is entered, attempting to submit more IO with fail with the **-EBUSY** error value, if it can’t flush the overflown events to the CQ ring. If this happens, the application must reap events from the CQ ring and attempt the submit again. 80 | 81 | - **IORING_FEAT_SUBMIT_STABLE** 82 | 83 | If this flag is set, applications can be certain that any data for async offload has been consumed when the kernel has consumed the SQE. 84 | 85 | - **IORING_FEAT_RW_CUR_POS** 86 | 87 | If this flag is set, applications can specify *offset* == -1 with **IORING_OP_{READV,WRITEV}** , **IORING_OP_{READ,WRITE}_FIXED** , and **IORING_OP_{READ,WRITE}** to mean current file position, which behaves like *preadv2(2)* and *pwritev2(2)* with *offset* == -1. It’ll use (and update) the current file position. This obviously comes with the caveat that if the application has multiple reads or writes in flight, then the end result will not be as expected. This is similar to threads sharing a file descriptor and doing IO using the current file position. 88 | 89 | - **IORING_FEAT_CUR_PERSONALITY** 90 | 91 | If this flag is set, then io_uring guarantees that both sync and async execution of a request assumes the credentials of the task that called *io_uring_enter(2)* to queue the requests. If this flag isn’t set, then requests are issued with the credentials of the task that originally registered the io_uring. If only one task is using a ring, then this flag doesn’t matter as the credentials will always be the same. Note that this is the default behavior, tasks can still register different personalities through *io_uring_register(2)* with **IORING_REGISTER_PERSONALITY** and specify the personality to use in the sqe. 92 | 93 | The rest of the fields in the *struct io_uring_params* are filled in by the kernel, and provide the information necessary to memory map the submission queue, completion queue, and the array of submission queue entries. *sq_entries* specifies the number of submission queue entries allocated. *sq_off* describes the offsets of various ring buffer fields: 94 | 95 | ``` 96 | struct io_sqring_offsets { 97 | __u32 head; 98 | __u32 tail; 99 | __u32 ring_mask; 100 | __u32 ring_entries; 101 | __u32 flags; 102 | __u32 dropped; 103 | __u32 array; 104 | __u32 resv[3]; 105 | }; 106 | ``` 107 | 108 | Taken together, *sq_entries* and *sq_off* provide all of the information necessary for accessing the submission queue ring buffer and the submission queue entry array. The submission queue can be mapped with a call like: 109 | 110 | ``` 111 | ptr = mmap(0, sq_off.array + sq_entries * sizeof(__u32), 112 | PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE, 113 | ring_fd, IORING_OFF_SQ_RING); 114 | ``` 115 | 116 | where *sq_off* is the *io_sqring_offsets* structure, and *ring_fd* is the file descriptor returned from **io_uring_setup**(2). The addition of *sq_off.array* to the length of the region accounts for the fact that the ring located at the end of the data structure. As an example, the ring buffer head pointer can be accessed by adding *sq_off.head* to the address returned from **mmap**(2): 117 | 118 | ``` 119 | head = ptr + sq_off.head; 120 | ``` 121 | 122 | The *flags* field is used by the kernel to communicate state information to the application. Currently, it is used to inform the application when a call to **io_uring_enter**(2) is necessary. See the documentation for the **IORING_SETUP_SQPOLL** flag above. The *dropped* member is incremented for each invalid submission queue entry encountered in the ring buffer. 123 | 124 | The head and tail track the ring buffer state. The tail is incremented by the application when submitting new I/O, and the head is incremented by the kernel when the I/O has been successfully submitted. Determining the index of the head or tail into the ring is accomplished by applying a mask: 125 | 126 | ``` 127 | index = tail & ring_mask; 128 | ``` 129 | 130 | The array of submission queue entries is mapped with: 131 | 132 | ``` 133 | sqentries = mmap(0, sq_entries * sizeof(struct io_uring_sqe), 134 | PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE, 135 | ring_fd, IORING_OFF_SQES); 136 | ``` 137 | 138 | The completion queue is described by *cq_entries* and *cq_off* shown here: 139 | 140 | ``` 141 | struct io_cqring_offsets { 142 | __u32 head; 143 | __u32 tail; 144 | __u32 ring_mask; 145 | __u32 ring_entries; 146 | __u32 overflow; 147 | __u32 cqes; 148 | __u32 resv[4]; 149 | }; 150 | ``` 151 | 152 | The completion queue is simpler, since the entries are not separated from the queue itself, and can be mapped with: 153 | 154 | ``` 155 | ptr = mmap(0, cq_off.cqes + cq_entries * sizeof(struct io_uring_cqe), 156 | PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE, ring_fd, 157 | IORING_OFF_CQ_RING); 158 | ``` 159 | 160 | Closing the file descriptor returned by **io_uring_setup**(2) will free all resources associated with the io_uring context. 161 | 162 | ## RETURN VALUE 163 | 164 | **io_uring_setup**(2) returns a new file descriptor on success. The application may then provide the file descriptor in a subsequent **mmap**(2) call to map the submission and completion queues, or to the **io_uring_register**(2) or **io_uring_enter**(2) system calls. 165 | 166 | On error, -1 is returned and *errno* is set appropriately. 167 | 168 | ## ERRORS 169 | 170 | - **EFAULT** 171 | 172 | params is outside your accessible address space. 173 | 174 | - **EINVAL** 175 | 176 | The resv array contains non-zero data, p.flags contains an unsupported flag, *entries* is out of bounds, **IORING_SETUP_SQ_AFF** was specified, but **IORING_SETUP_SQPOLL** was not, or **IORING_SETUP_CQSIZE** was specified, but *io_uring_params.cq_entries* was invalid. 177 | 178 | - **EMFILE** 179 | 180 | The per-process limit on the number of open file descriptors has been reached (see the description of **RLIMIT_NOFILE** in **getrlimit**(2)). 181 | 182 | - **ENFILE** 183 | 184 | The system-wide limit on the total number of open files has been reached. 185 | 186 | - **ENOMEM** 187 | 188 | Insufficient kernel resources are available. 189 | 190 | - **EPERM** 191 | 192 | **IORING_SETUP_SQPOLL** was specified, but the effective user ID of the caller did not have sufficient privileges. 193 | 194 | ## SEE ALSO 195 | 196 | **io_uring_register**(2), **io_uring_enter**(2) 197 | 198 | > 原文链接:https://unixism.net/loti/ref-iouring/io_uring_setup.html 199 | 200 | -------------------------------------------------------------------------------- /io_uring/文章/io_uring(1) – 我们为什么会需要 io_uring.md: -------------------------------------------------------------------------------- 1 | 当前 Linux 对文件的操作有很多种方式,最古老的最基本就是 `read` 和 `write` 这样的原始接口,这样的接口简洁直观,但是真的是足够原始,效率什么自然不是第一要素,当然为了符合 POSIX 标准,我们需要它。一段时间之后,程序员们发现,人们需要更为简单的 API,于是出现了 `pread` 和 `pwrite` 它允许我们在读写时直接传递 offset,显而易见它表现的更为优秀,在减少编码的同时,提高了代码的健壮性[1](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fn-748-1)。后来又出现了 `preadv` 和 `pwritev` 这种可以一次性发送多个 IO 的高效接口;接着又出现了变种函数 `preadv2` 和 `pwritev2` 他们不仅仅可以发送向量型的 IO,offset,还能设置本次 IO 的标志,比如 RWF_DSYNC、RWF_HIPRI、RWF_SYNC 等等(暂时没有其他)。 2 | 3 | 上面介绍的一系列的接口全部都是同步接口,意思就是在读写 IO 时,caller 一定会阻塞起来等待结果返回,对于普通的传统编程模型,这其实没有什么大不了的,编程简单且结果可以预测;但是在高效情况下呢?同步导致的后果就是 caller 不再能够继续执行其他的操作,只能静静的等待 IO 结果返回,其实他明明可以利用这段时间继续处理下一个操作,好比是一个 ftp 服务器,当接收到客户机上传的文件,然后将文件写入到本机的过程中时,假设 ftp 服务程序忙于等待文件读写结果的返回,那么就会拒绝到其他正在需要连接的客户机请求[2](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fn-748-2)。有没有更好的方式?当然有,那就是采用异步 IO 模型,当一个客户机上传文件时,直接将 IO 的 buffer 提交给内核即可,然后 caller 继续接受下一个客户请求,在内核处理完毕 IO 之后,主动调用各种通知机制,告诉 caller 上一个 IO 已经完成,完成状态保存在某某位置,请查看。 4 | 5 | 有一个更为形象的例子,就好比你去 JD 买东西,在网站上下单之后,你就下楼去等快递,就一直等啊一直等,直到等到为止,这就是同步 IO;假设换个方式,你在网站上下单之后就不管了,等着快递到了楼下之后打个电话给你,说放到快递柜了,让你下楼去取,这就是 poll/epoll 模型,也就是非阻塞轮询模式,你下楼取快递的过程还是同步的方式;好吧,还有一种更妙的方式就是,你在网站下单之后,就不管了,快递直接送到你手里,这就是异步模式[3](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fn-748-3)。 6 | 7 | ### 尴尬的 AIO 8 | 9 | Great,原来我们是如此迫切的需要异步 IO,他能帮助我们做更多的事情而无需增加 caller 更多的复杂度,AIO 应运而生,POSIX 也适时的添加了 `aio_read` 和 `aio_write` 这样的标准接口,好像一切都那么顺利,世界来到了一个完美的位面。Unfortunately,aio 满足了我们的要求,但是他也存在很多的缺陷。 10 | 11 | 第一,最大的缺陷就是不支持 buffer-io,也就是说,在采用 aio 的时候,你只能使用 O_DIRECT 来发送这一个 IO,带来的影响就是你不再能够借助文件系统缓存来缓存当前的 IO 请求,于是你在得到的同时失去了一些东西。 12 | 13 | 其次,尽管你强迫所有的 IO 都采用异步 IO,但是有时候确做不到,你的 caller 尽管将任务发送给了内核,但是内核还是通过工作队列或者线程完成的提交工作,假设在写元数据区域的时候,submission 会被挂起等待,假设存储设备的所有通道都很忙的时候,submission 需要挂起等待。于是,这些不确定性的存在,导致你的 caller 在处理完成状态的时候也不得不妥协。 14 | 15 | 最后,API 函数并不是很友好,基本上每一个 IO 的提交都需要要拷贝 64 + 8 个字节,而完成状态需要拷贝 32 个字节,这里就是 104 个字节的拷贝,当然,这个消耗是否可以承受是和你的 IO 大小有关的,如果你发送的大的 IO 的话,这点消耗可以忽略。同时,每一个 IO 至少需要两次系统调用才能完成(submit 和 wait-for-completion),这在有 spectre/meltdown 的机器上是一个严重的灾难。 16 | 17 | 导致最终的问题就是,在各种测试场景上,aio 欣欣向荣,然后在实际生产环境中,被采纳的很少,这就成为尴尬的 AIO[4](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fn-748-4)。 18 | 19 | ### 那么应该是什么样子的呢? 20 | 21 | 有了问题就需要有解决方案,设计解决方案就需要确立目标,于是新接口需要什么目标? 22 | 23 | **足够简单且难以滥用**:所有的用户可见接口都需要满足这个要求,接口一定需要让使用者容易理解且不容易被错误使用。 24 | 25 | **可扩展**:尽管这个接口是为了存储设备而建立的,但是他需要有足够的扩展性让将来支持非阻塞设备以及网络数据传输。 26 | 27 | **功能丰富**:这个没什么好说的,什么接口都需要支持足够丰富的功能,而且需要减少和核外应用程序的耦合度,尽可能减少交互复杂度。 28 | 29 | **高效**:高效率本来就是目标。 30 | 31 | **可伸缩性**:高效和低延迟很重要,但是峰值速率对于存储设备来讲也很重要,为了适应新的硬件要求,接口还需要考虑到伸缩性。 32 | 33 | 于是,我们需要重新设计接口,新的异步接口 io_uring 就在这样的环境下诞生了,可以预见的就是,在不久的将来,他会成为一个 POSIX 标准存在,也会被更多的企业级软件所采用[5](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fn-748-5)。在 Jens Axboe 自己的测试环境中,io_uring 在特定情况下,会比 SPDK 拥有更好的表现。 34 | 35 | ### io_uring 实现了什么 36 | 37 | 实际上,io_uring 有大量的基于原有 aio 的逻辑代码,这是为了获得更为全面的支持,比如可以通过 eventfd 通知核外收割 IO 完成事件,或者通过 SIGIO 信号通知核外收割 IO 完成事件,以及可以通过 io_uring 的 poll 模式实现一个统一的编程模型,这些都是一些外围的、扩展的支持,没有他们也并不影响整个 io_uring 优秀的设计。 38 | 39 | 一般情况下,核外通过 setup 系统调用创建 fd,然后 mmap 内存实现内核与核外的交互,当需要提交数据时使用 submit 系统调用发送 IO 数据。看起来好像没有什么了不起,除了比 aio 每一个 IO 少提交了几个字节之外,没有什么更惊艳的实现,是的,如果仅仅只是这样,io_uring 确实并不出色,但是他还有: 40 | 41 | **io_uring_enter 收割完成状态**:在我们发送 IO 的同时,使用同一个系统调用就可以回收完成状态,这样的好处就是一次系统调用接口就完成了原本需要两次系统调用的工作,大大的减少了系统调用的次数,也就是减少了内核核外的切换,这是一个很明显的优化,内核与核外的切换极其耗时。 42 | 43 | **sqo_thread 内核发送线程**:也许你还是不满足自己来发送数据,那么内核给你准备了一个 sqo_thread,他的作用就是你只需要在准备好数据之后,通过一次 io_uring_enter 设置的 IORING_ENTER_SQ_WAKEUP 参数即可唤醒线程启动发送,而不需要你每次都自己去发送数据,在合适的情况下,sqo_thread 就不停歇的永久为你发送数据,除非你不再往 sqes 环中填写数据,sqo_thread 会在无数据之后的 1s 之后休眠,等待再次唤醒。这样的好处就是,也许你的整个 IO 过程中仅仅只需要发起 1 次系统调用,这是多么神奇的优化。 44 | 45 | **io_poll 模式**:很多时候,完成状态的探测都是使用的中断模型,也就是等待后端数据处理完毕之后,内核会发起一个 SIGIO 或者 eventfd 的 EPOLLIN 状态提醒核外有数据已经完成,可以开始处理。但是内核还是给你提供了一个更为激进的模式,那就是 io_poll 模式,核外程序可以不停的轮询你需要的完成事件数量(通过 IORING_ENTER_GETEVENTS),不断的轮询探测完成状态,直到足够数量的 IO 完成,然后返回,这期间节省了中断返回的时间,又从一定的层次上加速了 IO 的推进(在 NVMe 上有较高的性能提升)。 46 | 47 | **fixed_file 模式**:如果你的文件相对固定,那么可以通过 IORING_REGISTER_FILES 参数将他们注册到内核,这样内核在注册阶段就批量完成文件的一些基本操作,之后的 io 再次发送时不再需要不停的对文件描述符进行基本信息设置,节省了一定量的 IO 时间。 48 | 49 | **fixed_buffer 模式**:如果你下发的 buffer 相对固定,也可以通过 IORING_REGISTER_BUFFERS 将他们注册到内核。通过固定的 buffer 传递 IOV 的地址和长度,减少了内存拷贝和基础信息检测等等。 50 | 51 | 对于核外而言,内核提供了足够多的选择,每一种方案都有着不同角度的优化提升,这些优化方案可以自行组合(当然需要正确使用),当 io_uring 全速运转时,才是他真正魅力所在。 52 | 53 | ------ 54 | 55 | 1. 我在某处的文章上看到过,pread 所做的偏移只针对本次操作而不影响文件整体的文件偏移指针,就是用 `lseek` 偏移的那个。 [↩︎](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fnref-748-1) 56 | 2. 这只是一个假设,一般情况下服务程序会为每一个客户机 fork 一个进程进行服务的,当然这也不是什么最佳的处理方式,在更多的客户机同时链接时,会导致主机资源消耗殆尽的情况。 [↩︎](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fnref-748-2) 57 | 3. 这个例子我从别处看过来的,别人举例就是比我高明。 [↩︎](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fnref-748-3) 58 | 4. 我个人这么认为的,实际上,aio 确实使用场景不多。 [↩︎](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fnref-748-4) 59 | 5. 目前已知的 qemu 已经开始采用 io_uring 进行适配,很快将可以通过这个接口来加速虚拟机 IO 的流程。 [↩︎](https://www.byteisland.com/io_uring(1)-我们为什么会需要-io_uring/#fnref-748-5) 60 | 61 | > 原文链接:https://www.byteisland.com/io_uring%EF%BC%881%EF%BC%89-%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E9%9C%80%E8%A6%81-io_uring/ 62 | 63 | -------------------------------------------------------------------------------- /io_uring/文章/下一代异步 IO io_uring 技术解密.md: -------------------------------------------------------------------------------- 1 | ## 概述 2 | 3 | Alibaba Cloud Linux 2 是阿里云操作系统团队基于开源 Linux 4.19 LTS 版本打造的一款针对云应用场景的下一代 Linux OS 发行版。在首次推出一年后,阿里云操作系统团队对外正式发布了Alibaba Cloud Linux 2 LTS 版本。LTS 版本的发布是一个重要的里程碑,标志着阿里云操作系统团队将为 Alibaba Cloud Linux 2 提供长期技术支持、稳定的更新和更好的服务,为 Alibaba Cloud Linux 2 的客户提供更多保障。 4 | 5 | 上一篇我们在 Alibaba Cloud Linux 2 上对比测试了 io_uring 与 libaio 以及 SPDK,可以看到 io_uring 带来的性能提升非常明显。这篇文章我们详细分析下 io_uring 的原理,以及我们在 io_uring 社区所做的工作。 6 | 7 | ## io_uring 原理介绍 8 | 9 | 为了从根本上解决当前 Linux aio 存在的问题和约束,io_uring 从零开始全新设计的了异步 IO 框架。其设计的主要目标如下: 10 | 11 | - 简单易用,方便应用集成。 12 | - 可扩展,不仅仅为 block IO 使用,同样可以用于网络 IO。 13 | - 特性丰富,满足所有应用,如 buffer io。 14 | - 高效,尤其是针对大部分场景的 512 字节或 4K IO。 15 | - 可伸缩,满足峰值场景的性能需要。 16 | 17 | io_uring 为了避免在提交和完成事件中的内存拷贝,设计了一对共享的 ring buffer 用于应用和内核之间的通信。其中,针对提交队列(SQ),应用是 IO 提交的生产者(producer),内核是消费者(consumer);反过来,针对完成队列(CQ),内核是完成事件的生产者,应用是消费者。 18 | 19 | ![img](https://kernel.taobao.org/2020/08/Introduction_to_IO_uring/1.png) 20 | 21 | 共享环的设计主要带来以下 3 个好处: 22 | 23 | - 提交、完成请求时节省应用和内核之间的内存拷贝; 24 | - 使用 SQPOLL 高级特性时,应用程序无需调用系统调用; 25 | - 无锁操作,用 memory ordering 实现同步。 26 | 27 | ### io_uring 系统调用 28 | 29 | io_uring 一共提供了 3 个系统调用:io_uring_setup(),io_uring_enter(),以及io_uring_register(),位于 fs/io_uring.c。 30 | 31 | ``` 32 | /** * io_uring_setup - setup a context for performing asynchronous I/O */ 33 | int io_uring_setup(u32 entries, struct io_uring_params *p); 34 | /** * io_uring_enter - initiate and/or complete asynchronous I/O */ 35 | int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, 36 | unsigned int flags, sigset_t *sig) 37 | 38 | /** * io_uring_register - register files or user buffers for asynchronous I/O */ 39 | int io_uring_register(int fd, unsigned int opcode, void *arg, 40 | unsigned int nr_args) 41 | ``` 42 | 43 | Alibaba Cloud Linux 2 LTS 版本支持的异步操作如下,更多的特性支持持续完善中。 44 | 45 | - IORING_OP_NOP 仅产生一个完成事件,除此之外没有任何操作。 46 | - IORING_OP_READV / IORING_OP_WRITEV 提交 readv() / writev() 操作,大多数场景最核心的操作。 47 | - IORING_OP_READ_FIXED / IORING_OP_WRITE_FIXED 使用已注册的 buffer 来提交 IO 操作,由于这些 buffer 已经完成映射,可以降低系统调用的开销。 48 | - IORING_OP_FSYNC 下发 fsync() 调用。 49 | - IORING_OP_POLL_ADD / IORING_OP_POLL_REMOVE 使用 IORING_OP_POLL_ADD 可对一组文件描述符 (file descriptors) 执行 poll() 操作;可以使用 IORING_OP_POLL_REMOVE 显式地取消 poll()。这种方式可以用来异步地监控一组文件描述符。 50 | - IORING_OP_SYNC_FILE_RANGE 执行 sync_file_range() 调用,是对 fsync() 的一个增强。 51 | - IORING_OP_SENDMSG / IORING_OP_RECVMSG 在 sendmsg() 和 recvmsg() 基础上,提供异步收发网络包功能。 52 | - IORING_OP_TIMEOUT 用户态程序等待 IO 完成事件时,可以通过 IORING_OP_TIMEOUT 设置一个超时时间,类似 io_getevents(2) 的 timeout 机制。 53 | 54 | ### io_uring 用户态库 liburing 55 | 56 | 为了简化使用,原作者 Jens 开发了一套 liburing 库,用户无需了解诸多 io_uring 细节便可以使用起来,如无需关心 memory barrier,以及 ring buffer 的管理等。相关接口在头文件 /usr/include/liburing.h 中定义。 57 | 58 | Alibaba Cloud Linux 2 LTS 提供了 liburing 和 liburing-devel 包供用户安装。 59 | 60 | ``` 61 | sodo yum install liburing liburing-devel 62 | ``` 63 | 64 | 基于 liburing 的一个 helloworld 示例如下: 65 | 66 | ``` 67 | #include #include #include #include #include #define ENTRIES 4 int main(int argc, char *argv[]) 68 | { 69 | struct io_uring ring; 70 | struct io_uring_sqe *sqe; 71 | struct io_uring_cqe *cqe; 72 | struct iovec iov = { 73 | .iov_base = "Hello World", 74 | .iov_len = strlen("Hello World"), 75 | }; 76 | int fd, ret; 77 | if (argc != 2) { 78 | printf("%s: \n", argv[0]); 79 | return 1; 80 | } 81 | /* setup io_uring and do mmap */ 82 | ret = io_uring_queue_init(ENTRIES, &ring, 0); 83 | if (ret < 0) { 84 | printf("io_uring_queue_init: %s\n", strerror(-ret)); 85 | return 1; 86 | } 87 | fd = open("testfile", O_WRONLY | O_CREAT); 88 | if (fd < 0) { 89 | printf("open failed\n"); 90 | ret = 1; 91 | goto exit; 92 | } 93 | /* get an sqe and fill in a WRITEV operation */ 94 | sqe = io_uring_get_sqe(&ring); 95 | if (!sqe) { 96 | printf("io_uring_get_sqe failed\n"); 97 | ret = 1; 98 | goto out; 99 | } 100 | io_uring_prep_writev(sqe, fd, &iov, 1, 0); 101 | /* tell the kernel we have an sqe ready for consumption */ 102 | ret = io_uring_submit(&ring); 103 | if (ret < 0) { 104 | printf("io_uring_submit: %s\n", strerror(-ret)); 105 | goto out; 106 | } 107 | /* wait for the sqe to complete */ 108 | ret = io_uring_wait_cqe(&ring, &cqe); 109 | if (ret < 0) { 110 | printf("io_uring_wait_cqe: %s\n", strerror(-ret)); 111 | goto out; 112 | } 113 | /* read and process cqe event */ 114 | io_uring_cqe_seen(&ring, cqe); 115 | out: 116 | close(fd); 117 | exit: 118 | /* tear down */ 119 | io_uring_queue_exit(&ring); 120 | return ret; 121 | } 122 | ``` 123 | 124 | 更多的示例可参考: 125 | 126 | https://github.com/axboe/liburing/tree/master/examples/ https://github.com/axboe/liburing/tree/master/test 127 | 128 | ## io_uring 高级特性 129 | 130 | ### Polled IO 131 | 132 | IORING_SETUP_IOPOLL,与非 polling 模式等待硬件中断唤醒不同,内核将采用 polling 模式不断轮询硬件以确认 IO 请求是否已经完成,这在追求低延时和高 IOPS 的应用场景非常有用。 133 | 134 | ### Kernel Side Polling 135 | 136 | IORING_SETUP_SQPOLL,当前应用更新 SQ ring 并填充一个新的 sqe,内核线程 sqthread 会自动完成提交,这样应用无需每次调用 io_uring_enter() 系统调用来提交 IO。应用可通过 IORING_SETUP_SQ_AFF 和 sq_thread_cpu 绑定特定的 CPU。 同时,为了节省无 IO 场景的 CPU 开销,该内核线程会在一段时间空闲后自动睡眠。应用在下发新的 IO 时,通过 IORING_ENTER_SQ_WAKEUP 唤醒该内核线程,该操作在 liburing 中都已封装完成。 137 | 138 | ### Fixed Files 139 | 140 | IORING_REGISTER_FILES / IORING_REGISTER_FILES_UPDATE / IORING_UNREGISTER_FILES,通过 io_uring_register() 系统调用提前注册一组 file,缓解每次 IO 操作因 fget() / fput() 带来的开销。 141 | 142 | ### Fixed Buffers 143 | 144 | IORING_REGISTER_BUFFERS / IORING_UNREGISTER_BUFFERS,通过 io_uring_register() 系统调用注册一组固定的 IO buffers,当应用重用这些 IO buffers 时,只需要 map / unmap 一次即可,而不是每次 IO 都要去做,减少get_user_pages() / put_page() 带来的开销。 145 | 146 | ### Linked SQE 147 | 148 | IOSQE_IO_LINK,建立 sqe 序列之间的关联,这在诸如 copy 之类的操作中非常有用。使用 linked sqe 后,copy 操作的写请求链接在读请求之后,应用程序无需等待读请求数据返回后再下发写请求,而是共享了同一个 buffer,避免了上下文切换的开销。 149 | 150 | ## 社区工作 151 | 152 | 阿里云操作系统团队在 backport io_uring 特性到 Alibaba Cloud Linux 2 的过程中,进一步优化性能,并加固 io_uring 的稳定性,相关工作以补丁的形式回馈到社区。 153 | 154 | ### 性能优化 155 | 156 | - engines/io_uring: delete fio_option_is_set() calls when submitting sqes fio io_uring 提交 IO 性能提升 30%。 157 | - __io_uring_get_cqe: eliminate unnecessary io_uring_enter() syscalls 在某些场景下,减少 50% 的 io_uring_enter() 系统调用开销。 158 | - ext4: start to support iopoll method 159 | - io_uring: io_uring_enter(2) don’t poll while SETUP_IOPOLL|SETUP_SQPOLL enabled 能带来 13% 的性能提升,同时减少 20% 的 CPU 开销。 160 | 161 | ### 代码优化和特性重构 162 | 163 | - io_uring: cleanup io_alloc_async_ctx() 164 | - io_uring: refactor file register/unregister/update handling 重构 file register/unregister/update 特性,能更好地处理大量文件场景。 165 | - io_uring: do not always copy iovec in io_req_map_rw() 166 | - io_uring: avoid whole io_wq_work copy for requests completed inline 167 | - io_uring: avoid unnecessary io_wq_work copy for fast poll feature 168 | - e697deed834d io_uring: check file O_NONBLOCK state for accept 169 | 170 | ### BugFix 171 | 172 | - io_uring: fix __io_iopoll_check deadlock in io_sq_thread 173 | - io_uring: fix poll_list race for SETUP_IOPOLL|SETUP_SQPOLL 174 | - io_uring: restore req->work when canceling poll request 175 | - io_uring: only restore req->work for req that needs do completion 176 | - io_uring: use cond_resched() in io_ring_ctx_wait_and_kill() 177 | - io_uring: fix mismatched finish_wait() calls in io_uring_cancel_files() 178 | - io_uring: handle -EFAULT properly in io_uring_setup() 179 | - io_uring: reset -EBUSY error when io sq thread is waken up 180 | - io_uring: don’t submit sqes when ctx->refs is dying 181 | - io_uring: fix io_kiocb.flags modification race in IOPOLL mode 182 | - io_uring: don’t fail links for EAGAIN error in IOPOLL mode 183 | - io_uring: add memory barrier to synchronize io_kiocb’s result and iopoll_completed 184 | - io_uring: fix possible race condition against REQ_F_NEED_CLEANUP 185 | 186 | > 原文链接:https://kernel.taobao.org/2020/08/Introduction_to_IO_uring/ 187 | -------------------------------------------------------------------------------- /io_uring/文章/小谈io_uring.md: -------------------------------------------------------------------------------- 1 | ## 什么是io_uring? 2 | 3 | 在 Linux 底下操作 IO 有以下方式: 4 | 5 | - `read` 系列 6 | - `pread` 7 | - `preadv` 8 | 9 | 但他们都是synchronous 的,所以POSIX 有实做`aio_read`,但其乏善可陈且效能欠佳。 10 | 11 | 事实上Linux 也有自己的native async IO interface,但是包含以下缺点: 12 | 13 | 1. async IO 只支援`O_DIRECT`(or un-buffered) accesses -> file descriptor 的设定 14 | 2. IO submission 伴随104 bytes 的data copy (for IO that's supposedly zero copy),此外一次IO 必须呼叫两个system call (submit + wait-for-completion) 15 | 3. 有很多可能会导致async 最后变成blocking (如: submission 会block 等待meta data; request slots 如果都被占满, submission 亦会block 住) 16 | 17 | Jens Axboe 一开始先尝试改写原有的native aio,但是失败收场,因此他决定提出一个新的interface,包含以下目标(越后面越重要): 18 | 19 | 1. 易于理解和直观的使用 20 | 2. Extendable,除了 block oriented IO,networking 和 non-block storage 也要能用 21 | 3. 效率 22 | 4. 可扩展性 23 | 24 | 在设计io_uring 时,为了避免过多data copy,Jens Axboe 选择透过shared memory 来完成application 和kernel 间的沟通。其中不可避免的是同步问题,使用single producer and single consumer ring buffer 来替代shared lock 解决shared data 的同步问题。而这沟通的管道又可分为submission queue (SQ) 和completion queue (CQ)。 25 | 26 | 以CQ 来说,kernel 就是producer,user application 就是consumer。SQ 则是相反。 27 | 28 | ## 链接请求 29 | 30 | CQEs can arrive in any order as they become available。(举例: 先读在HDD 上的A.txt,再读SSD 上的B.txt,若限制完成顺序的话,将会影响到效能)。事实上,也可以强制ordering (see [example](https://kernel.dk/io_uring.pdf) ) 31 | 32 | liburing 预设会非循序的执行submit queue 上的operation,但是有些特别的情况,我们需要这些operation 被循序的执行,如:`write`+ `close`。所以我们可以透过添加 `IOSQE_IO_LINK` 来达到效果。详细用法可参考[linking request](https://unixism.net/loti/tutorial/link_liburing.html) 33 | 34 | ## 提交队列轮询 35 | 36 | liburing 可以透过设定flag:`IORING_SETUP_SQPOLL`切换成poll 模式,这个模式可以避免使用者一直呼叫`io_uring_enter`(system call)。此模式下,kernel thread 会一直去检查submission queue 上是否有工作要做。详细用法可参考[Submission Queue Polling](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) 37 | 38 | 值得注意的是kernel thread 的数量需要被控制,否则大量的CPU cycle 会被k-thread 占据。为了避免这个机制,liburing 的kthread 在一定的时间内没有收到工作要做,kthread 就会sleep,所以下一次要做submission queue 上的工作就需要走原本的方式:`io_uring_enter()` 39 | 40 | 使用 liburing 时,您永远不会直接调用 io_uring_enter() 系统调用。这通常由 liburing 的 io_uring_submit() 函数处理。它会自动确定您是否使用轮询模式,并处理您的程序何时需要调用 io_uring_enter() 而无需您费心。 41 | 42 | ## 内存排序 43 | 44 | 如果要直接使用liburing 就不用管这个议题,但是如果是要操作raw interface,那这个就很重要。提供两种操作: 45 | 46 | 1. `read_barrier()`:确保在进行后续内存读取之前可以看到之前的写入 47 | 2. `write_barrier()`:在先前的写入之后订购此写入 48 | 49 | > 内核将在读取 SQ 环尾之前包含一个 read_barrier(),以确保来自应用程序的尾部写入可见。从 CQ 环的角度来看,由于消费者/生产者的角色是相反的,应用程序只需要在读取 CQ 环尾之前发出 read_barrier() 以确保它看到内核所做的任何写入。 50 | 51 | ## 解放图书馆 52 | 53 | - 不再需要样板代码来设置 io_uring 实例 54 | - 为基本用例提供简化的 API。 55 | 56 | ### 高级用例和功能 57 | 58 | #### 固定文件和缓冲区 59 | 60 | #### 轮询 IO 61 | 62 | I/O 依靠硬件中断来发出完成事件的信号。当 IO 被轮询时,应用程序将反复询问硬件驱动程序提交的 IO 请求的状态。 63 | 64 | [注][真实轮询示例](https://unixism.net/loti/tutorial/sq_poll.html) 65 | [注] 提交队列轮询仅与固定文件(非固定缓冲区)结合使用 66 | 67 | #### 内核端轮询 68 | 69 | 会有kernel thread 主动侦测SQ 上是否有东西,这样可以避免呼叫syscall: `io_uring_enter` 70 | 71 | ## 原始程式码 72 | 73 | ### `io_uring_setup` 74 | 75 | 基本的设定。我们关注的是setup 时需要设定哪些关于 `IORING_SETUP_SQPOLL` 的操作,预期找到kthread 的建立,kthread 的工作内容等等。从 `io_sq_offload_create` 可知offload 和kthread 有关。 76 | 77 | 往里面看可以找到[create_io_thread](https://github.com/torvalds/linux/blob/65090f30ab791810a3dc840317e57df05018559c/kernel/fork.c#L2444),透过 `copy_process` 达到fork,搭配 `wake_up_new_task` 启动process。该process 要做的事为[io_sq_thread](https://github.com/torvalds/linux/blob/master/fs/io_uring.c#L6882), 78 | 79 | ### `io_uring_enter` 80 | 81 | [io_uring_enter](https://github.com/torvalds/linux/blob/master/fs/io_uring.c#L9332)在prepare 完write/read 之类的operation 后会被呼叫,这里我们只关注在poll 模式下的行为: 82 | 83 | ``` 84 | if (ctx->flags & IORING_SETUP_SQPOLL) { 85 | io_cqring_overflow_flush(ctx, false); 86 | 87 | ret = -EOWNERDEAD; 88 | if (unlikely(ctx->sq_data->thread == NULL)) 89 | goto out; 90 | if (flags & IORING_ENTER_SQ_WAKEUP) 91 | wake_up(&ctx->sq_data->wait); 92 | if (flags & IORING_ENTER_SQ_WAIT) { 93 | ret = io_sqpoll_wait_sq(ctx); 94 | if (ret) 95 | goto out; 96 | } 97 | submitted = to_submit; 98 | } else if ... 99 | ``` 100 | 101 | 1. 若 `kthread` 闲置太久,为了避免霸占CPU,所以会主动sleep,所以若看到flag:`IORING_ENTER_SQ_WAKEUP`设起,就必须要唤醒kthread。 102 | 2. [PATCH:为 SQPOLL SQ 环等待提供 IORING_ENTER_SQ_WAIT](https://www.spinics.net/lists/io-uring/msg04097.html) 103 | 104 | ## 安装 liburing 105 | 106 | 1. 下载source [code](https://github.com/axboe/liburing/releases) 107 | 2. 。/配置 108 | 3. 须藤使安装 109 | 4. 编译示例: gcc -Wall -O2 -D_GNU_SOURCE -o io_uring-test io_uring-test.c -luring 110 | 111 | ## 自由流动 112 | 113 | io_uring_queue_init -> alloc iov -> io_uring_get_sqe -> io_uring_prep_readv -> io_uring_sqe_set_data -> io_uring_submit 114 | 115 | io_uring_wait_cqe -> io_uring_cqe_get_data -> io_uring_cqe_seen -> io_uring_queue_exit 116 | 117 | ## liburing/io_uring API 118 | 119 | ### i_uring 120 | 121 | 1. io_uring_setup(u32 个条目,结构 io_uring_params *p) 122 | 123 | - 描述:建立一个提交队列(SQ)和完成队列(CQ)至少有条目条目,并返回一个文件描述符,该文件描述符可用于对io_uring实例执行后续操作。 124 | 125 | - 关系:通过 liburing 函数包装: `io_uring_queue_init` 126 | 127 | - 标志:成员 128 | 129 | ``` 130 | struct io_uring_params 131 | ``` 132 | 133 | - IORING_SETUP_IOPOLL: 134 | - 忙于等待 I/O 完成 135 | - 提供更低的延迟,但可能比中断驱动的 I/O 消耗更多的 CPU 资源 136 | - 仅可用于使用 O_DIRECT 标志打开的文件描述符 137 | - 在 io_uring 实例上混合和匹配轮询和非轮询 I/O 是非法的 138 | - IORING_SETUP_SQPOLL: 139 | - 设置后创建内核线程进行提交队列轮询 140 | - `IORING_SQ_NEED_WAKEUP`如果内核线程空闲超过`sq_thread_idle`毫秒,将设置标志 141 | - `io_uring_register`在 linux 5.11 之前,应用程序必须通过操作`IORING_REGISTER_FILES`码注册一组用于 IO 的文件 142 | - IORING_SETUP_SQ_AFF: 143 | - poll线程将绑定到cpu设置的`sq_thread_cpu`字段`struct io_uring_params` 144 | 145 | - 如果未指定标志,则为中断驱动的 I/O 设置 io_uring 实例 146 | 147 | 1. io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args) 148 | 149 | - 操作码: 150 | - IORING_REGISTER_BUFFERS: 151 | - uffers 被映射到内核并有资格进行 I/O 152 | - 使用它们,应用程序必须在提交队列条目中指定`IORING_OP_READ_FIXED`或`IORING_OP_WRITE_FIXED`操作码,并将该`buf_index`字段设置为所需的缓冲区索引 153 | - IORING_REGISTER_FILES: 154 | - 描述:I/O 的寄存器文件。包含指向文件描述符`arg`数组的指针`nr_args` 155 | - 使用它们,`IOSQE_FIXED_FILE`flag 必须设置在 的 flags 成员中`struct io_uring_sqe`,并且该`fd`成员设置为文件描述符数组中文件的索引 156 | - IORING_REGISTER_EVENTFD: 157 | - 可以用于`eventfd()`获取 io_uring 实例上的完成事件的通知。 158 | 159 | 1. io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig) 160 | 161 | - 说明:单次调用既可以提交新的 I/O,也可以等待本次调用或之前调用的 I/O 完成 `io_uring_enter()` 162 | - 标志: 163 | - IORING_ENTER_GETEVENTS: 164 | - `min_complete`在返回之前等待指定数量的事件 165 | - 可以与 to_submit 一起设置为在单个系统调用中提交和完成事件 166 | - IORING_ENTER_SQ_WAKEUP 167 | - IORING_ENTER_SQ_WAIT 168 | - 操作码: 169 | - IORING_OP_READV 170 | - IORING_OP_WRITEV 171 | - 一些细节: 172 | - 如果 io_uring 实例被配置为轮询,通过`IORING_SETUP_IOPOLL`在对 的调用中指定`io_uring_setup()`,则`min_complete`含义略有不同。传递值 0 指示内核返回任何已经完成的事件,而不会阻塞。如果`min_complete`是一个非零值,如果有任何完成事件可用,内核仍然会立即返回。如果没有可用的事件完成,则调用将轮询直到一个或多个完成变得可用,或者直到进程超过其调度程序时间片。 173 | 174 | ### 解放 175 | 176 | ## io_uring 与 epoll 177 | 178 | ### io_uring 比 epoll 慢? 179 | 180 | - [解放 #189](https://github.com/axboe/liburing/issues/189) 181 | - [解放 #215](https://github.com/axboe/liburing/issues/215) 182 | - [网络 #10622](https://github.com/netty/netty/issues/10622) 183 | 184 | ## 参考 185 | 186 | - [约灵之主](https://unixism.net/loti/what_is_io_uring.html) 187 | - [使用 io_uring 实现高效 IO - 官方 pdf](https://kernel.dk/io_uring.pdf) 188 | - [i_uring](https://unixism.net/2020/04/io-uring-by-example-part-1-introduction/) 189 | - [解放例子](https://unixism.net/loti/tutorial/index.html) 190 | - [解放网络服务器](https://github.com/shuveb/loti-examples/blob/master/webserver_liburing.c) 191 | 192 | > 原文链接:https://hackmd.io/@sysprog/iouring#What-is-io_uring 193 | 194 | -------------------------------------------------------------------------------- /io_uring/示例程序-提交队列轮询.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 512 9 | #define FILE_NAME1 "/tmp/io_uring_sq_test.txt" 10 | #define STR1 "What is this life if, full of care,\n" 11 | #define STR2 "We have no time to stand and stare." 12 | 13 | void print_sq_poll_kernel_thread_status() { 14 | 15 | if (system("ps --ppid 2 | grep io_uring-sq" ) == 0) 16 | printf("Kernel thread io_uring-sq found running...\n"); 17 | else 18 | printf("Kernel thread io_uring-sq is not running.\n"); 19 | } 20 | 21 | int start_sq_polling_ops(struct io_uring *ring) { 22 | int fds[2]; 23 | char buff1[BUF_SIZE]; 24 | char buff2[BUF_SIZE]; 25 | char buff3[BUF_SIZE]; 26 | char buff4[BUF_SIZE]; 27 | struct io_uring_sqe *sqe; 28 | struct io_uring_cqe *cqe; 29 | int str1_sz = strlen(STR1); 30 | int str2_sz = strlen(STR2); 31 | 32 | fds[0] = open(FILE_NAME1, O_RDWR | O_TRUNC | O_CREAT, 0644); 33 | if (fds[0] < 0 ) { 34 | perror("open"); 35 | return 1; 36 | } 37 | 38 | memset(buff1, 0, BUF_SIZE); 39 | memset(buff2, 0, BUF_SIZE); 40 | memset(buff3, 0, BUF_SIZE); 41 | memset(buff4, 0, BUF_SIZE); 42 | strncpy(buff1, STR1, str1_sz); 43 | strncpy(buff2, STR2, str2_sz); 44 | 45 | int ret = io_uring_register_files(ring, fds, 1); 46 | if(ret) { 47 | fprintf(stderr, "Error registering buffers: %s", strerror(-ret)); 48 | return 1; 49 | } 50 | 51 | sqe = io_uring_get_sqe(ring); 52 | if (!sqe) { 53 | fprintf(stderr, "Could not get SQE.\n"); 54 | return 1; 55 | } 56 | io_uring_prep_write(sqe, 0, buff1, str1_sz, 0); 57 | sqe->flags |= IOSQE_FIXED_FILE; 58 | 59 | sqe = io_uring_get_sqe(ring); 60 | if (!sqe) { 61 | fprintf(stderr, "Could not get SQE.\n"); 62 | return 1; 63 | } 64 | io_uring_prep_write(sqe, 0, buff2, str2_sz, str1_sz); 65 | sqe->flags |= IOSQE_FIXED_FILE; 66 | 67 | io_uring_submit(ring); 68 | 69 | for(int i = 0; i < 2; i ++) { 70 | int ret = io_uring_wait_cqe(ring, &cqe); 71 | if (ret < 0) { 72 | fprintf(stderr, "Error waiting for completion: %s\n", 73 | strerror(-ret)); 74 | return 1; 75 | } 76 | /* Now that we have the CQE, let's process the data */ 77 | if (cqe->res < 0) { 78 | fprintf(stderr, "Error in async operation: %s\n", strerror(-cqe->res)); 79 | } 80 | printf("Result of the operation: %d\n", cqe->res); 81 | io_uring_cqe_seen(ring, cqe); 82 | } 83 | 84 | print_sq_poll_kernel_thread_status(); 85 | 86 | sqe = io_uring_get_sqe(ring); 87 | if (!sqe) { 88 | fprintf(stderr, "Could not get SQE.\n"); 89 | return 1; 90 | } 91 | io_uring_prep_read(sqe, 0, buff3, str1_sz, 0); 92 | sqe->flags |= IOSQE_FIXED_FILE; 93 | 94 | sqe = io_uring_get_sqe(ring); 95 | if (!sqe) { 96 | fprintf(stderr, "Could not get SQE.\n"); 97 | return 1; 98 | } 99 | io_uring_prep_read(sqe, 0, buff4, str2_sz, str1_sz); 100 | sqe->flags |= IOSQE_FIXED_FILE; 101 | 102 | io_uring_submit(ring); 103 | 104 | for(int i = 0; i < 2; i ++) { 105 | int ret = io_uring_wait_cqe(ring, &cqe); 106 | if (ret < 0) { 107 | fprintf(stderr, "Error waiting for completion: %s\n", 108 | strerror(-ret)); 109 | return 1; 110 | } 111 | /* Now that we have the CQE, let's process the data */ 112 | if (cqe->res < 0) { 113 | fprintf(stderr, "Error in async operation: %s\n", strerror(-cqe->res)); 114 | } 115 | printf("Result of the operation: %d\n", cqe->res); 116 | io_uring_cqe_seen(ring, cqe); 117 | } 118 | printf("Contents read from file:\n"); 119 | printf("%s%s", buff3, buff4); 120 | } 121 | 122 | int main() { 123 | struct io_uring ring; 124 | struct io_uring_params params; 125 | 126 | if (geteuid()) { 127 | fprintf(stderr, "You need root privileges to run this program.\n"); 128 | return 1; 129 | } 130 | 131 | print_sq_poll_kernel_thread_status(); 132 | 133 | memset(¶ms, 0, sizeof(params)); 134 | params.flags |= IORING_SETUP_SQPOLL; 135 | params.sq_thread_idle = 2000; 136 | 137 | int ret = io_uring_queue_init_params(8, &ring, ¶ms); 138 | if (ret) { 139 | fprintf(stderr, "Unable to setup io_uring: %s\n", strerror(-ret)); 140 | return 1; 141 | } 142 | start_sq_polling_ops(&ring); 143 | io_uring_queue_exit(&ring); 144 | return 0; 145 | } -------------------------------------------------------------------------------- /llvm/文档/ A Compilation Framework for Lifelong Program Analysis & Transformation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/llvm/文档/ A Compilation Framework for Lifelong Program Analysis & Transformation.pdf -------------------------------------------------------------------------------- /llvm/文档/Introduction to the LLVM Compiler System.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/llvm/文档/Introduction to the LLVM Compiler System.pdf -------------------------------------------------------------------------------- /llvm/文档/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /llvm/文章/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /llvm/文章/llvm编译的基本概念和流程.md: -------------------------------------------------------------------------------- 1 | 近期跟着《LLVM Cookbook》学习了一下LLVM相关的内容,趁着学完还算熟悉,赶紧写一下笔记和总结,方便以后的回顾。LLVM的全称是Low Level Virtual Machine,名字已经解释了好多内容。作为一个编译器的基础框架,提供了各种工具和编译的基础设施,允许我们自定义前端,实现特定的优化方法,并绑定自己的后端。特别是MLIR和IREE的发布,加上当下芯片行业越来越热,LLVM在社区和产业界的影响力也随之扩大,了解和学习LLVM还是很有必要的。 2 | 3 | ## 代码形式及转换关系 4 | 5 | LLVM涉及到的内容比较多,这里把东西先列出来, 6 | 7 | ### 代码形式的基本概念 8 | 9 | 1. **main.c** c源代码,对于llvm来说即前端代码,能够在clang或其他前端的处理下生成LLVM的IR和bitcode,从而进行后续的操作。 10 | 2. **main.ll** llvm IR, 可读的llvm汇编代码。 11 | 3. **main.bc** llvm bitcode,LLVM的主要表示,后续工具能够直接在这一层表示上运行,又被称为llvm字节码,是LLVM IR编码成位流的编码格式。 12 | 4. **main.s** 目标架构下的汇编代码,不同架构,不同格式下均不一致,这也是llvm想为用户屏蔽的底层信息。x86和arm等均有自己的表示。 13 | 5. **main.o** 目标架构下的object文件,经过编译生成的二进制文件,能够进行链接操作。 14 | 6. **a.out** 对目标架构下的object文件链接后的产物,能够在目标架构下执行。 15 | 16 | 用图示表示的会更清晰一些,这里画了一下几种代码格式之间的转换关系 17 | 18 | [![img](http://qn.throneclay.top/image/jpg/llvm_file.jpg_out.jpg)](http://qn.throneclay.top/image/jpg/llvm_file.jpg_out.jpg) 19 | 20 | ### 用到的常用工具 21 | 22 | llvm的文档工作还是做的非常好的,这里节选了llvm官方文档对自家工具的介绍,重点说一下常用的一些命令,详细全文见https://llvm.org/docs/CommandGuide/index.html 23 | 24 | 1. **clang/clang++** c语言的前端工具,能够完成c/c++代码到LLVM的转换。输入是c/c++的源码。 25 | 26 | ``` 27 | clang --emit-llvm -S -c main.c -o main.ll 28 | clang --emit-llvm -c main.c -o main.bc 29 | ``` 30 | 31 | 2. **opt** llvm 优化器,能够对Bitcode执行优化(PASS)的工具。输入是LLVM bitcode,输出是优化后的LLVM bitcode 32 | 33 | 3. **llvm-as** llvm汇编器,输入是LLVM IR,输出为LLVM bitcode。 34 | 35 | 4. **llvm-dis** llvm反汇编器,输入是bitcode,输出为LLVM IR。 36 | 37 | 5. **llc** llvm静态编译器,根据特定的后端,将bitcode编译对应的汇编代码 38 | 39 | 6. **lli** bitcode立即执行工具,使用jit或解释器执行一个bitcode。输入为bitcode,输出执行效果。 40 | 41 | 7. **llvm-link** llvm链接器,将多个bitcode链接为一个bitcode,输入为多个bitcoede,输出一个链接后的bitcode。 42 | 43 | 8. **llvm-config** 输出llvm编译选项,根据选项,输出合适的llvm的编译选项。 44 | 45 | [![img](http://qn.throneclay.top/image/jpg/two_links_out.jpg)](http://qn.throneclay.top/image/jpg/two_links_out.jpg) 46 | 47 | ### 编译链接时会用到的LLVM lib 48 | 49 | 通常这部分会使用llvm-config来代替调,但了解这些库的功能对于未来可能的debug还是很有帮助的。(参考 http://faculty.sist.shanghaitech.edu.cn/faculty/songfu/course/spring2018/CS131/llvm.pdf -Getting to know LLVM’s basic libraries) 50 | 51 | 1. **libLLVMCore** 包含LLVM IR(bitcode)相关的逻辑,IR构造(data layout,instructions,basic block和functions)以及IR校验器。 52 | 2. **libLLVMAnalysis** 包含IR的分析过程,如别名分析,依赖分析,常量折叠,循环信息,内存依赖分析和指令简化。 53 | 3. **libLLVMCodeGen** 该库实现与目标无关的代码生成和机器级别的分析和转换。 54 | 4. **libLLVMTarget** 对目标机器信息提供通用的抽象封装接口。实现是在libLLVMCodeGen中的通用后端算法和libLLVM[target Marchine]CodeGen库中的特定后端算法。 55 | 5. **libLLVMX86CodeGen** 如上面所说,这个库里是x86目标的特定后端算法,包括代码生成信息、转换和分析过程。 56 | 6. **libLLVMARMCodeGen** 这个库里是Arm目标的特定后端算法,包括代码生成信息、转换和分析过程。这种库还有很多,就不分别列举了。 57 | 7. **libLLVMSupport** 库里包含通用工具集合。包括错误,整型和浮点处理、命令行解析、调试、文件支持和字符串处理。 58 | 59 | ## pipeline 60 | 61 | 完整的llvm工作pipeline如下: 62 | 63 | [![img](http://qn.throneclay.top/image/jpg/llvm-pipeline_out.jpg)](http://qn.throneclay.top/image/jpg/llvm-pipeline_out.jpg) 64 | -------------------------------------------------------------------------------- /llvm/文章/后端技术的重用:LLVM不仅仅让你高效.md: -------------------------------------------------------------------------------- 1 | - 在编译器后端,做代码优化和为每个目标平台生成汇编代码,工作量是很大的。那么,有什么办法能降低这方面的工作量,提高我们的工作效率呢?**答案就是利用现成的工具。** 2 | 3 | - 在前端部分,我就带你使用Antlr生成了词法分析器和语法分析器。那么在后端部分,我们也可以获得类似的帮助,比如利用LLVM和GCC这两个后端框架。 4 | 5 | - 相比前端的编译器工具,如Lex(Flex)、Yacc(Bison)和Antlr等,对于后端工具,了解的人比较少,资料也更稀缺,如果你是初学者,那么上手的确有一些难度。不过我们已经用20~24讲,铺垫了必要的基础知识,也尝试了手写汇编代码,这些知识足够你学习和掌握后端工具了。 6 | 7 | - 本节课,我想先让你了解一些背景信息,所以会先概要地介绍一下LLVM和GCC这两个有代表性的框架的情况,这样,当我再更加详细地讲解LLVM,带你实际使用一下它的时候,你接受起来就会更加容易了。 8 | 9 | - ## 两个编译器后端框架:LLVM和GCC 10 | 11 | - LLVM是一个开源的编译器基础设施项目,主要聚焦于编译器的后端功能(代码生成、代码优化、JIT……)。它最早是美国伊利诺伊大学的一个研究性项目,核心主持人员是Chris Lattner(克里斯·拉特纳)。 12 | 13 | - LLVM的出名是由于苹果公司全面采用了这个框架。苹果系统上的C语言、C++、Objective-C的编译器Clang就是基于LLVM的,最新的Swift编程语言也是基于LLVM,支撑了无数的移动应用和桌面应用。无独有偶,在Android平台上最新的开发语言Kotlin,也支持基于LLVM编译成本地代码。 14 | 15 | - 另外,由Mozilla公司(Firefox就是这个公司的产品)开发的系统级编程语言RUST,也是基于LLVM开发的。还有一门相对小众的科学计算领域的语言,叫做Julia,它既能像脚本语言一样灵活易用,又可以具有C语言一样的速度,在数据计算方面又有特别的优化,它的背后也有LLVM的支撑。 16 | 17 | - OpenGL和一些图像处理领域也在用LLVM,我还看到一个资料,**说阿里云的工程师实现了一个Cava脚本语言,用于配合其搜索引擎系统HA3。** 18 | 19 | - ![img](https://static001.geekbang.org/resource/image/d2/ac/d212b52e14007278e8ee417e20e94bac.png) 20 | 21 | - 22 | 23 | - 还有,在人工智能领域炙手可热的TensorFlow框架,在后端也是用LLVM来编译。它把机器学习的IR翻译成LLVM的IR,然后再翻译成支持CPU、GPU和TPU的程序。 24 | 25 | - 所以这样看起来,你所使用的很多语言和工具,背后都有LLVM的影子,只不过你可能没有留意罢了。所以在我看来,要了解编译器的后端技术,就不能不了解LLVM。 26 | 27 | - 与LLVM起到类似作用的后端编译框架是GCC(GNU Compiler Collection,GNU编译器套件)。它支持了GNU Linux上的很多语言,例如C、C++、Objective-C、Fortran、Go语言和Java语言等。其实,它最初只是一个C语言的编译器,后来把公共的后端功能也提炼了出来,形成了框架,支持多种前端语言和后端平台。最近华为发布的方舟编译器,据说也是建立在GCC基础上的。 28 | 29 | - LLVM和GCC很难比较优劣,因为这两个项目都取得了很大的成功。 30 | 31 | - 在本课程中,我们主要采用LLVM,但其中学到的一些知识,比如IR的设计、代码优化算法、适配不同硬件的策略,在学习GCC或其他编译器后端的时候,也是有用的,从而大大提升学习效率。 32 | 33 | - 接下来,我们先来看看LLVM的构成和特点,让你对它有个宏观的认识。 34 | 35 | - ## 了解LLVM的特点 36 | 37 | - LLVM能够支持多种语言的前端、多种后端CPU架构。在LLVM内部,使用类型化的和SSA特点的IR进行各种分析、优化和转换: 38 | 39 | - ![img](https://static001.geekbang.org/resource/image/07/1c/079aa0c78325b3a4420d78523b5aa51c.png) 40 | 41 | - 42 | 43 | - LLVM项目包含了很多组成部分: 44 | 45 | - - LLVM核心(core)。就是上图中的优化和分析工具,还包括了为各种CPU生成目标代码的功能;这些库采用的是LLVM IR,一个良好定义的中间语言,在上一讲,我们已经初步了解它了。 46 | 47 | - - Clang前端(是基于LLVM的C、C++、Objective-C编译器)。 48 | 49 | - - LLDB(一个调试工具)。 50 | 51 | - - LLVM版本的C++标准类库。 52 | 53 | - - 其他一些子项目。 54 | 55 | - **我个人很喜欢LLVM,想了想,主要有几点原因:** 56 | 57 | - 首先,LLVM有良好的模块化设计和接口。以前的编译器后端技术很难复用,而LLVM具备定义了良好接口的库,方便使用者选择在什么时候,复用哪些后端功能。比如,针对代码优化,LLVM提供了很多算法,语言的设计者可以自己选择合适的算法,或者实现自己特殊的算法,具有很好的灵活性。 58 | 59 | - 第二,LLVM同时支持JIT(即时编译)和AOT(提前编译)两种模式。过去的语言要么是解释型的,要么编译后运行。习惯了使用解释型语言的程序员,很难习惯必须等待一段编译时间才能看到运行效果。很多科学工作者,习惯在一个REPL界面中一边写脚本,一边实时看到反馈。LLVM既可以通过JIT技术支持解释执行,又可以完全编译后才执行,这对于语言的设计者很有吸引力。 60 | 61 | - 第三,有很多可以学习借鉴的项目。Swift、Rust、Julia这些新生代的语言,实现了很多吸引人的特性,还有很多其他的开源项目,而我们可以研究、借鉴它们是如何充分利用LLVM的。 62 | 63 | - 第四,全过程优化的设计思想。LLVM在设计上支持全过程的优化。Lattner和Adve最早关于LLVM设计思想的文章[《LLVM: 一个全生命周期分析和转换的编译框架》,](https://llvm.org/pubs/2003-09-30-LifelongOptimizationTR.pdf)就提出计算机语言可以在各个阶段进行优化,包括编译时、链接时、安装时,甚至是运行时。 64 | 65 | - 以运行时优化为例,基于LLVM我们能够在运行时,收集一些性能相关的数据对代码编译优化,可以是实时优化的、动态修改内存中的机器码;也可以收集这些性能数据,然后做离线的优化,重新生成可执行文件,然后再加载执行,**这一点非常吸引我,**因为在现代计算环境下,每种功能的计算特点都不相同,确实需要针对不同的场景做不同的优化。下图展现了这个过程(图片来源《 LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation》): 66 | 67 | - ![img](https://static001.geekbang.org/resource/image/07/6e/071b0421588472cda2033c75124ee96e.png) 68 | 69 | - 70 | 71 | - **我建议你读一读Lattner和Adve的这篇论文**(另外强调一下,当你深入学习编译技术的时候,阅读领域内的论文就是必不可少的一项功课了)。 72 | 73 | - 第五,LLVM的授权更友好。GNU的很多软件都是采用GPL协议的,所以如果用GCC的后端工具来编写你的语言,你可能必须要按照GPL协议开源。而LLVM则更友好一些,你基于LLVM所做的工作,完全可以是闭源的软件产品。 74 | 75 | - 而我之所以说:“LLVM不仅仅让你更高效”,就是因为上面它的这些特点。 76 | 77 | - 现在,你已经对LLVM的构成和特点有一定的了解了,接下来,我带你亲自动手操作和体验一下LLVM的功能,这样你就可以迅速消除对它的陌生感,快速上手了。 78 | 79 | - ## 体验一下LLVM的功能 80 | 81 | - 首先你需要安装一下LLVM(参照[官方网站](http://releases.llvm.org/)上的相关介绍下载安装)。因为我使用的是macOS,所以用brew就可以安装。 82 | 83 | - ```plain 84 | brew install llvm 85 | ``` 86 | 87 | - 因为LLVM里面带了一个版本的Clang和C++的标准库,与本机原来的工具链可能会有冲突,所以brew安装的时候并没有在/usr/local下建立符号链接。你在用LLVM工具的时候,要配置好相关的环境变量。 88 | 89 | - ```plain 90 | # 可执行文件的路径 91 | export PATH="/usr/local/opt/llvm/bin:$PATH" 92 | # 让编译器能够找到LLVM 93 | export LDFLAGS="-L/usr/local/opt/llvm/lib" 94 | export CPPFLAGS="-I/usr/local/opt/llvm/include” 95 | ``` 96 | 97 | - 安装完毕之后,我们使用一下LLVM自带的命令行工具,分几步体验一下LLVM的功能: 98 | 99 | - 1.从C语言代码生成IR; 100 | 101 | - 2.优化IR; 102 | 103 | - 3.从文本格式的IR生成二进制的字节码; 104 | 105 | - 4.把IR编译成汇编代码和可执行文件。 106 | 107 | - 从C语言代码生成IR代码比较简单,上一讲中我们已经用到过一个C语言的示例代码: 108 | 109 | - ```plain 110 | //fun1.c 111 | int fun1(int a, int b){ 112 | int c = 10; 113 | return a+b+c; 114 | } 115 | ``` 116 | 117 | - 用前端工具Clang就可以把它编译成IR代码: 118 | 119 | - ```plain 120 | clang -emit-llvm -S fun1.c -o fun1.ll 121 | ``` 122 | 123 | - 其中,-emit-llvm参数告诉Clang生成LLVM的汇编码,也就是IR代码(如果不带这个参数,就会生成针对目标机器的汇编码)所生成的IR我们上一讲也见过,你现在应该能够读懂它了。你可以多写几个不同的程序,看看生成的IR是什么样的,比如if语句、循环语句等等(这时你完成了第一步): 124 | 125 | - ```plain 126 | ; ModuleID = 'function-call1.c' 127 | source_filename = "function-call1.c" 128 | target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" 129 | target triple = "x86_64-apple-macosx10.14.0" 130 | 131 | ; Function Attrs: noinline nounwind optnone ssp uwtable 132 | define i32 @fun1(i32, i32) #0 { 133 | %3 = alloca i32, align 4 134 | %4 = alloca i32, align 4 135 | %5 = alloca i32, align 4 136 | store i32 %0, i32* %3, align 4 137 | store i32 %1, i32* %4, align 4 138 | store i32 10, i32* %5, align 4 139 | %6 = load i32, i32* %3, align 4 140 | %7 = load i32, i32* %4, align 4 141 | %8 = add nsw i32 %6, %7 142 | %9 = load i32, i32* %5, align 4 143 | %10 = add nsw i32 %8, %9 144 | ret i32 %10 145 | } 146 | 147 | attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } 148 | 149 | !llvm.module.flags = !{!0, !1} 150 | !llvm.ident = !{!2} 151 | 152 | !0 = !{i32 1, !"wchar_size", i32 4} 153 | !1 = !{i32 7, !"PIC Level", i32 2} 154 | !2 = !{!"clang version 8.0.0 (tags/RELEASE_800/final)"} 155 | ``` 156 | 157 | - 上一讲我们提到过,可以对生成的IR做优化,让代码更短,你只要在上面的命令中加上-O2参数就可以了(这时你完成了第二步): 158 | 159 | - ```plain 160 | clang -emit-llvm -S -O2 fun1.c -o fun1.ll 161 | ``` 162 | 163 | - 这个时候,函数体的核心代码就变短了很多。这里面最重要的优化动作,是从原来使用内存(alloca指令是在栈中分配空间,store指令是往内存里写入值),优化到只使用寄存器(%0、%1是参数,%3、%4也是寄存器)。 164 | 165 | - ```plain 166 | define i32 @fun1(i32, i32) #0 { 167 | %3 = add nsw i32 %0, %1 168 | %4 = add nsw i32 %3, 10 169 | ret i32 %4 170 | } 171 | ``` 172 | 173 | - 你还可以用opt命令来完成上面的优化,具体我们在27、28讲中讲优化算法的时候再细化。 174 | 175 | - **另外,LLVM的IR有两种格式。**在示例代码中显示的,是它的文本格式,文件名一般以.ll结尾。第二种格式是字节码(bitcode)格式,文件名以.bc结尾。**为什么要用两种格式呢?**因为文本格式的文件便于程序员阅读,而字节码格式的是二进制文件,便于机器处理,比如即时编译和执行。生成字节码格式之后,所占空间会小很多,所以可以快速加载进内存,并转换为内存中的对象格式。而如果加载文本文件,则还需要一个解析的过程,才能变成内存中的格式,效率比较慢。 176 | 177 | - 调用llvm-as命令,我们可以把文本格式转换成字节码格式: 178 | 179 | - ```plain 180 | llvm-as fun1.ll -o fun1.bc 181 | ``` 182 | 183 | - 我们也可以用clang直接生成字节码,这时不需要带-S参数,而是要用-c参数。 184 | 185 | - ```plain 186 | clang -emit-llvm -c fun1.c -o fun1.bc 187 | ``` 188 | 189 | - 因为.bc文件是二进制文件,不能直接用文本编辑器查看,而要用hexdump命令查看(这时你完成了第三步): 190 | 191 | - ```plain 192 | hexdump -C fun1.bc 193 | ``` 194 | 195 | - ![img](https://static001.geekbang.org/resource/image/74/b1/7466ca0d3d8beb0c4d570091512da1b1.png) 196 | 197 | - LLVM的一个优点,就是可以即时编译运行字节码,不一定非要编译生成汇编码和可执行文件才能运行(这一点有点儿像Java语言),这也让LLVM具有极高的灵活性,比如,可以在运行时根据收集的性能信息,改变优化策略,生成更高效的机器码。 198 | 199 | - 再进一步,我们可以把字节码编译成目标平台的汇编代码。我们使用的是llc命令,命令如下: 200 | 201 | - ```plain 202 | llc fun1.bc -o fun1.s 203 | ``` 204 | 205 | - 用clang命令也能从字节码生成汇编代码,要注意带上-S参数就行了: 206 | 207 | - ```plain 208 | clang -S fun1.bc -o fun1.s 209 | ``` 210 | 211 | - **到了这一步,我们已经得到了汇编代码,**接着就可以进一步生成目标文件和可执行文件了。 212 | 213 | - 实际上,使用LLVM从源代码到生成可执行文件有两条可能的路径: 214 | 215 | - ![img](https://static001.geekbang.org/resource/image/5a/d4/5ad8793ffba445c8f95d417f4ae9e6d4.jpg) 216 | 217 | - - 第一条路径,是把每个源文件分别编译成字节码文件,然后再编译成目标文件,最后链接成可执行文件。 218 | 219 | - - 第二条路径,是先把编译好的字节码文件链接在一起,形成一个更大的字节码文件,然后对这个字节码文件进行进一步的优化,之后再生成目标文件和可执行文件。 220 | 221 | - 第二条路径比第一条路径多了一个优化的步骤,第一条路径只对每个模块做了优化,没有做整体的优化。所以,如有可能,尽量采用第二条路径,这样能够生成更加优化的代码。 222 | 223 | - 现在你完成了第四步,对LLVM的命令行工具有了一定的了解。总结一下,我们用到的命令行工具包括:clang前端、llvm-as、llc,其他命令还有opt(代码优化)、llvm-dis(将字节码再反编译回ll文件)、llvm-link(链接)等,你可以看它们的help信息,并练习使用。 224 | 225 | - 在熟悉了命令行工具之后,我们就可以进一步在编程环境中使用LLVM了,不过在此之前,需要搭建一个开发环境。 226 | 227 | - ## 建立C++开发环境来使用LLVM 228 | 229 | - LLVM本身是用C开发的,所以最好采用C调用它的功能。当然,采用其他语言也有办法调用LLVM: 230 | 231 | - - C语言可以调用专门的C接口; 232 | 233 | - - 像Go、Rust、Python、Ocaml、甚至Node.js都有对LLVM API的绑定; 234 | 235 | - - 如果使用Java,也可以通过JavaCPP(类似JNI)技术调用LLVM。 236 | 237 | - 在课程中,我用C来做实现,因为这样能够最近距离地跟LLVM打交道。与此同时,我们前端工具采用的Antlr,也能够支持C开发环境。**所以,我为playscript建立了一个C++的开发环境。** 238 | 239 | - **开发工具方面:**原则上只要一个编辑器加上工具链就行,但为了提高效率,有IDE的支持会更好(我用的是JetBrains的Clion)。 240 | 241 | - **构建工具方面:**目前LLVM本身用的是CMake,而Clion刚好也采用CMake,所以很方便。 242 | 243 | - **这里我想针对CMake多解释几句,**因为越来越多的C项目都是用CMake来管理的,LLVM以及Antlr的C版本也采用了CMake,**你最好对它有一定了解。** 244 | 245 | - CMake是一款优秀的工程构建工具,它类似于Java程序员们习惯使用的Maven工具。对于只包含少量文件或模块的C或C++程序,你可以仅仅通过命令行带上一些参数就能编译。 246 | 247 | - 不过,实际的项目都会比较复杂,往往会包含比较多的模块,存在比较复杂的依赖关系,编译过程也不是一步能完成的,要分成多步。这时候我们一般用make管理项目的构建过程,这就要学会写make文件。但手工写make文件工作量会比较大,而CMake就是在make的基础上再封装了一层,它能通过更简单的配置文件,帮我们生成make文件,帮助程序员提升效率。 248 | 249 | - 整个开发环境的搭建我在课程里就不多写了,你可以参见示例代码所附带的文档。文档里有比较清晰的说明,可以帮助你把环境搭建起来,并运行示例程序。 250 | 251 | - 另外,我知道你可能对C++并不那么熟悉。但你应该学过C语言,所以示例代码还是能看懂的。 252 | 253 | - ## 课程小结 254 | 255 | - 256 | 257 | - 本节课,为了帮助你理解后端工具,我先概要介绍了后端工具的情况,接着着重介绍了LLVM的构成和特点,然后又带你熟悉了它的命令行工具,让你能够生成文本和字节码两种格式的IR,并生成可执行文件,最后带你了解了LLVM的开发环境。 258 | 259 | - 本节课的内容比较好理解,因为侧重让你建立跟LLVM的熟悉感,没有什么复杂的算法和原理,而我想强调的是以下几点: 260 | 261 | - 1.后端工具对于语言设计者很重要,我们必须学会善加利用; 262 | 263 | - 2.LLVM有很好的模块化设计,支持即时编译(JIT)和提前编译(AOT),支持全过程的优化,并且具备友好的授权,值得我们好好掌握; 264 | 265 | - 3.你要熟悉LLVM的命令行工具,这样可以上手做很多实验,加深对LLVM的了解。 266 | 267 | - 最后,我想给你的建议是:一定要动手安装和使用LLVM,写点代码测试它的功能。比如,写点儿C、C++等语言的程序,并翻译成IR,进一步熟悉LLVM的IR。下一讲,我们就要进入它的内部,调用它的API来生成IR和运行了! 268 | 269 | - 270 | 271 | - ## 一课一思 272 | 273 | - 很多语言都获得了后端工具的帮助,比如可以把Android应用直接编译成机器码,提升运行效率。你所经常使用的计算机语言采用了什么后端工具?有什么特点?欢迎在留言区分享。 274 | 275 | - 最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你分享给更多的朋友。 276 | 277 | -------------------------------------------------------------------------------- /llvm/文章/编译优化|LLVM代码生成技术详解及在数据库中的应用.md: -------------------------------------------------------------------------------- 1 | # 1. 前言 2 | 3 | 随着IT基础设施的发展,现代的数据处理系统需要处理更多的数据、支持更为复杂的算法。数据量的增长和算法的复杂化,为数据分析系统带来了严峻的性能挑战。近年来,我们可以在数据库、大数据系统和AI平台等领域看到很多性能优化的技术,技术涵盖体系结构、编译技术和高性能计算等领域。作为编译优化技术的代表,本文主要介绍基于LLVM的代码生成技术(简称Codeden)。 4 | 5 | LLVM是一款非常流行的开源编译器框架,支持多种语言和底层硬件。开发者可以基于LLVM搭建自己的编译框架并进行二次开发,将不同的语言或者逻辑编译成运行在多种硬件上的可执行文件。对于Codegen技术来说,我们主要关注LLVM IR的格式以及生成LLVM IR的API。在本文的如下部分,我们首先对LLVM IR进行介绍,然后介绍Codegen技术的原理和使用场景,最后我们介绍在阿里云自研的云原生数据仓库产品AnalyticDB PostgreSQL中,Codegen的典型应用场景。 6 | 7 | # 2. LLVM IR简介及上手教程 8 | 9 | 在编译器理论与实践中,IR是非常重要的一环。IR的全称叫做Intermediate Representation,翻译过来叫“中间表示”。 对于一个编译器来说,从上层抽象的高级语言到底层的汇编语言,要经历很多个环节(pass),经历不同的表现形式。而编译优化技术有很多种,每种技术作用的编译环节不同。但是IR是一个明显的分水岭。IR以上的编译优化,不需要关心底层硬件的细节,比如硬件的指令集、寄存器文件大小等。IR以下的编译优化,需要和硬件打交道。LLVM最为著名是它的IR的设计。得益于巧妙地IR设计,LLVM向上可以支持不同的语言,向下可以支持不同的硬件,而且不同的语言可以复用IR层的优化算法。 10 | 11 | ![img](https://pics1.baidu.com/feed/f703738da97739128ebe4469d9203010377ae2e4.jpeg?token=c37f78e58526fcf3c0952d284749f475) 12 | 13 | 上图展示了LLVM的一个框架图。LLVM把整个编译过程分为三步:(1)前端,把高级语言转换为IR。(2)中端,在IR层做优化。(3) 后端,把IR转化为对应的硬件平台的汇编语言。因此LLVM的扩展性很好。比如你要实现一个名为toyc的语言、希望运行在ARM平台上,你只需要实现一个toyc->LLVM IR的前端,其他部分调LLVM的模块就可以了。或者你要搞一个新的硬件平台,那么只需要搞定LLVM IR->新硬件这一阶段,然后该硬件就可以支持很多种现存的语言。因此,IR是LLVM最有竞争力的地方,同时也是学习使用LLVM Codegen的最核心的地方。 14 | 15 | **2.1 LLVM IR基本知识** 16 | 17 | LLVM的IR格式非常像汇编,对于学习过汇编语言的同学来说,学会使用LLVM IR进行编程非常容易。对于没学过汇编语言的同学,也不用担心,汇编其实并不难。汇编难的不是学会,而是工程实现。因为汇编语言的开发难度,会随着工程复杂度的提升呈指数级上升。接下来我们需要了解IR中最重要的三部分,指令格式、Basic Block & CFG,还有SSA。完整的LLVM IR信息请参考https://llvm.org/docs/LangRef.html。 18 | 19 | **指令格式**。LLVM IR提供了一种类似于汇编语言的三地址码式的指令格式。下面的代码片段是一个非常简单的用LLVM IR实现的函数,该函数的输入是5个i32类型(int32)的整数,函数的功能是计算这5个数的和并返回。LLVM IR是支持一些基本的数据类型的,比如i8、i32、浮点数等。LLVM IR中得变量的命名是以 "%"开头,默认%0是函数的第一个参数、%1是第二个参数,依次类推。机器生成的变量一般是以数字进行命名,如果是手写的话,可以根据自己的喜好选择合适的命名方法。LLVM IR的指令格式包括操作符、类型、输入、返回值。例如 "%6 = add i32 %0, %1"的操作符号是"add"、类型是"i32"、输入是"%0"和“%1”、返回值是"%6"。总的来说,IR支持一些基本的指令,然后编译器通过这些基本指令的来完成一些复杂的运算。例如,我们在C中写一个形如“A * B + C”的表达式在LLVM IR中是通过一条乘法和一条加法指令来完成的,另外可能也包括一些类型转换指令。 20 | 21 | ``` 22 | define i32 @ir_add(i32, i32, i32, i32, i32){ %6 = add i32 %0, %1 %7 = add i32 %6, %2 %8 = add i32 %7, %3 %9 = add i32 %8, %4 ret i32 %9} 23 | ``` 24 | 25 | **Basic Block & CFG**。了解了IR的指令格式以后,接下来我们需要了解两个概念:Basic Block(基本块,简称BB)和Control Flow Graph(控制流图,CFG)。下图(左)展示了一个简单的C语言函数,下图(中)是使用clang编译出来的对应的LLVM IR,下图(右)是使用graphviz画出来的CFG。结合这张图,我们解释下Basic Block和CFG的概念。 26 | 27 | ![img](https://pics4.baidu.com/feed/9c16fdfaaf51f3de34a9b779b4d746173a297951.jpeg?token=05821836b6504a5280938181bbf127be) 28 | 29 | 在我们平时接触到的高级语言中,每种语言都会有很多分支跳转语句,比如C语言中有for, while, if等关键字,这些关键字都代表着分支跳转。开发者通过分支跳转来实现不同的逻辑运算。汇编语言通常通过有条件跳转和无条件跳转两种跳转指令来实现逻辑运算,LLVM IR同理。比如在LLVM IR中"br label %7"意味着无论如何都跳转到名为%7的label那里,这是一条无条件跳转指令。"br i1 %10, label %11, label %22"是有条件跳转,意味着这如果%10是true则跳转到名为%11的label,否则跳转到名为%22的label。 30 | 31 | 在了解了跳转指令这个概念后,我们介绍Basic Block的概念。一个Basic Block是指一段串行执行的指令流,除了最后一句之外不会有跳转指令,Basic Block入口的第一条指令叫做“Leading instruction”。除了第一个Basic Block之外,每个Basic Block都会有一个名字(label)。第一个Basic Block也可以有,只是有时候没必要。例如在这段代码当中一共有5个Basic Block。Basic Block的概念,解决了控制逻辑的问题。通过Basic Block, 我们可以把代码划分成不同的代码块,在编译优化中,有的优化是针对单个Basic Block的,有些是针对多个Basic Block的。 32 | 33 | CFG(Control Flow Graph, 控制流图)其实就是由Basic Block以及Basic Block之间的跳转关系组成的一个图。例如上图所示的代码,一共有5个Basic Block,箭头列出了Basic Block之间的跳转关系,共同组成了一个CFG。如果一个Basic Block只有一个箭头指向别的Block,那么这个跳转就是无条件跳转,否则是有条件跳转。CFG是编译理论中一个比较简单而且很基础的概念,CFG更进一步是DFG(Data Flow Graph,数据流图),很多进阶的编译优化算法都是基于DFG的。对于使用LLVM进行Codegen开发的同学,理解CFG的概念即可。 34 | 35 | **SSA**。SSA的全称是Static Single Assignment(静态单赋值),这是编译技术中非常基础的一个理念。SSA是学习LLVM IR必须熟悉的概念,同时也是最难理解的一个概念。细心的读者在观察上面列出的IR代码时会发现,每个“变量”只会被赋值一次,这就是SSA的核心思想。因为从编译器的角度来看,编译器不关心“变量”,编译器是以“数据”为中心进行设计的。每个“变量”的每次写入,都生成了一个新的数据版本,编译器的优化是围绕数据版本展开的。接下来我们用如下的C语言代码来解释这一思想。 36 | 37 | ![img](https://pics2.baidu.com/feed/ac345982b2b7d0a20039a56dd1d6c0014a369a0b.jpeg?token=3a4e00b6a2128c0e576165a2a01b222e) 38 | 39 | 上图(左)展示了一段简单的C代码,上图(右)是这段代码的SSA版本,也就是“编译器眼中的代码”。在C语言中,我们知道数据都是用变量来存储的,因此数据操作的核心是变量,开发者需要关心变量的生存时间、何时被赋值、何时被使用。但是编译器只关心数据的流向,因此每次赋值操作都会生成一个新的左值。例如左边代码只有一个a, 但是在右边的代码有4个变量,因为a里面的数据一共有4个版本。除了每次赋值操作会生成一个新的变量,最后的一个phi节点会生成一个新的变量。在SSA中,每个变量都代表数据的一个版本。也就是说,高级语言以变量为核心,而SSA格式以数据为核心。SSA中每次赋值操作都会生成一个版本的数据,因此在写IR的时候,时刻牢记IR的变量和高层语言不同,一个IR的变量代表数据的一个版本。Phi节点是SSA中的一个重要概念。在这个例子当中,a_4的取值取决于之前执行了哪个分支,如果执行了第一个分支,那么a_4 = a_1, 依次类推。Phi节点通过判断这段代码是从哪个Basic Block跳转而来,选择合适的数据版本。LLVM IR自然也是需要开发者写Phi节点的,在循环、条件分支跳转的地方,往往需要手写很多phi节点,这是写LLVM IR时逻辑上比较难处理的地方。 40 | 41 | **2.2 学会使用LLVM IR写程序** 42 | 43 | 熟悉LLVM IR最好的办法就是使用IR写几个程序。在开始写之前,建议先花30分钟-1个小时再粗略阅读下官方手册(https://llvm.org/docs/LangRef.html),熟悉下都有哪些指令的类型。接下来我们通过两个简单的case熟悉下LLVM IR编程的全部流程。 44 | 45 | 下面是一个循环加法的函数片段。这个函数一共包含三个Basic Block,loop、loop_body和final。其中loop是整个函数的开始,loop_body是函数的循环体,final是函数的结尾。在第5行和第6行,我们使用phi节点来实现结果和循环变量。 46 | 47 | ``` 48 | define i32 @ir_loopadd_phi(i32*, i32){ br label %loop loop: %i = phi i32 [0,%2], [%newi,%loop_body] %res = phi i32[0,%2], [%new_res, %loop_body] %break_flag = icmp sge i32 %i, %1 br i1 %break_flag, label %final, label %loop_body loop_body: %addr = getelementptr inbounds i32, i32* %0, i32 %i %val = load i32, i32* %addr, align 4 %new_res = add i32 %res, %val %newi = add i32 %i, 1 br label %loopfinal: ret i32 %res;} 49 | ``` 50 | 51 | 下面是一个数组冒泡排序的函数片段。这个函数包含两个循环体。LLVM IR实现循环本身就比较复杂,两个循环嵌套会更加复杂。如果能够用LLVM IR实现一个冒泡算法,基本上就理解了LLVM的整个逻辑了。 52 | 53 | ``` 54 | define void @ir_bubble(i32*, i32) { %r_flag_addr = alloca i32, align 4 %j = alloca i32, align 4 %r_flag_ini = add i32 %1, -1 store i32 %r_flag_ini, i32* %r_flag_addr, align 4 br label %out_loop_headout_loop_head: ;check break store i32 0, i32* %j, align 4 %tmp_r_flag = load i32, i32* %r_flag_addr, align 4 %out_break_flag = icmp sle i32 %tmp_r_flag, 0 br i1 %out_break_flag, label %final, label %in_loop_head in_loop_head: ;check break %tmpj_1 = load i32, i32* %j, align 4 %in_break_flag = icmp sge i32 %tmpj_1, %tmp_r_flag br i1 %in_break_flag, label %out_loop_tail, label %in_loop_body in_loop_body: ;read & swap %tmpj_left = load i32, i32* %j, align 4 %tmpj_right = add i32 %tmpj_left, 1 %left_addr = getelementptr inbounds i32, i32* %0, i32 %tmpj_left %right_addr = getelementptr inbounds i32, i32* %0, i32 %tmpj_right %left_val = load i32, i32* %left_addr, align 4 %right_val = load i32, i32* %right_addr, align 4 ;swap check %swap_flag = icmp sge i32 %left_val, %right_val %left_res = select i1 %swap_flag, i32 %right_val, i32 %left_val %right_res = select i1 %swap_flag, i32 %left_val, i32 %right_val store i32 %left_res, i32* %left_addr, align 4 store i32 %right_res, i32* %right_addr, align 4 br label %in_loop_end in_loop_end: ;update j %tmpj_2 = load i32, i32* %j, align 4 %newj = add i32 %tmpj_2, 1 store i32 %newj, i32* %j, align 4 br label %in_loop_headout_loop_tail: ;update r_flag %tmp_r_flag_1 = load i32, i32* %r_flag_addr, align 4 %new_r_flag = sub i32 %tmp_r_flag_1, 1 store i32 %new_r_flag, i32* %r_flag_addr, align 4 br label %out_loop_headfinal: ret void} 55 | ``` 56 | 57 | 我们把如上的LLVM IR用clang编译器编译成object文件,然后和C语言写的程序链接到一起,即可正常调用。在上面提到的case中,我们只使用了i32、i64等基本数据类型,LLVM IR中支持struct等高级数据类型,可以实现更为复杂的功能。 58 | 59 | **2.3 使用LLVM API实现Codegen** 60 | 61 | 编译器本质上就是调用各种各样的API,根据输入去生成对应的代码,LLVM Codegen也不例外。在LLVM内部,一个函数是一个class,一个Basic Block试一个class, 一条指令、一个变量都是一个class。用LLVM API实现codegen就是根据需求,用LLVM内部的数据结构去实现相应的IR。 62 | 63 | ``` 64 | Value *constant = Builder.getInt32(16); Value *Arg1 = fooFunc->arg_begin(); Value *val = createArith(Builder, Arg1, constant); Value *val2 = Builder.getInt32(100); Value *Compare = Builder.CreateICmpULT(val, val2, "cmptmp"); Value *Condition = Builder.CreateICmpNE(Compare, Builder.getInt1(0), "ifcond"); ValList VL; VL.push_back(Condition); VL.push_back(Arg1); BasicBlock *ThenBB = createBB(fooFunc, "then"); BasicBlock *ElseBB = createBB(fooFunc, "else"); BasicBlock *MergeBB = createBB(fooFunc, "ifcont"); BBList List; List.push_back(ThenBB); List.push_back(ElseBB); List.push_back(MergeBB); Value *v = createIfElse(Builder, List, VL); 65 | ``` 66 | 67 | 如上是一个用LLVM API实现codegen的例子。其实这就是个用C++写IR的过程,如果知道如何写IR的话,只需要熟悉下这套API就可以了。这套API提供了一些基本的数据结构,比如指令、函数、基本块、llvm builder等,然后我们只需要调用相应的函数去生成这些对象即可。一般来说,首先我们先生成函数的原型,包括函数名字、参数列表、返回类型等。然后我们在根据函数的功能,确定都需要有哪些Basic Block以及Basic Block之间的跳转关系,然后生成相应的Basic。最后我们再按照一定的顺序去给每个Basic Block填充指令。逻辑是,这个流程和用LLVM IR写代码是相仿的。 68 | 69 | # 3. Codegen技术分析 70 | 71 | 如果我们用上文所描述的方法,生成一些简单的函数,并且用C写出对应的版本进行性能对比,我们就会发现,LLVM IR的性能并不会比C快。一方面,计算机底层执行的是汇编,C语言本身和汇编是非常接近的,了解底层的程序员往往能够从C代码中推测出大概会生成什么样的汇编。另一方面,现代编译器往往做了很多优化,一些大大减轻了程序员的优化负担。因此,使用LLVM IR进行Codegen并不会获得比手写C更好的性能,而且使用LLVM Codegen有一些明显的缺点。想要真正用好LLVM,我们还需要熟悉LLVM的特点。 72 | 73 | **3.1 缺点分析** 74 | 75 | **缺点1:开发难。**实际开发中几乎不会有工程使用汇编作为主要开发语言,因为开发难度太大了,有兴趣的小伙伴可以试着写个快排感受一下。即使是数据库、操作系统这样的基础软件,往往也只是在少数的地方会用到汇编。使用LLVM IR开发会有类似的问题。比如上文展示的最复杂例子是冒泡算法。开发者用C写个冒泡只需要几分钟,但是用LLVM IR写个冒泡可能要一个小时。另外,LLVM IR很难处理复杂的数据结构,比如结构体、类。除了LLVM IR中的那些基本数据结构外,新增一个复杂的数据结构非常难。因此在实际的开发当中,采用Codegen会导致开发难度指数级上升。 76 | 77 | **缺点2:调试难**。开发者通常通过单步跟踪的方式去调试代码,但是LLVM IR是不支持的。一旦代码出问题,只能是人肉一遍一遍看LLVM IR。如果懂汇编的话,可以通过单步跟踪生成的汇编进行调试,但是汇编语言和IR之间并不是简单的映射关系,因此只能一定程度上降低调试难度,并不完全解决调试的问题。 78 | 79 | **缺点3: 运行成本**。生成LLVM IR往往很快,但是生成的IR需要调用LLVM 中的工具进行优化、以及编译成二进制文件,这个过程是需要时间的(请联想一下GCC编译的速度)。在数据库的开发过程中,我们的经验值是每个函数大约需要10ms-100ms的codegen成本。大部分的时间花在了优化IR和IR到汇编这两步。 80 | 81 | **3.2 适用场景** 82 | 83 | 了解了LLVM Codegen的缺点,我们才能去分析其优点、选择合适场景。下面这部分是团队在开发过程中总结的适合使用LLVM Codegen的场景。 84 | 85 | **场景1:Java/python等语言**。上文中提到过LLVM IR并不会比C快,但是会比Java/python等语言快啊。例如在Java中,有时候为了提升性能,会通过JNI调用一些C的函数提升性能。同理,Java也可以调用LLVM IR生成的函数提升性能。 86 | 87 | **场景2:硬件和语言不兼容**。LLVM支持多种后端,比如X86、ARM和GPU。对于一些硬件与语言不兼容的场景,可以利用LLVM实现兼容。例如如果我们的系统是用Java语言开发、想要调用GPU,可以考虑用LLVM IR生成GPU代码,然后通过JNI的方法进行调用。这套方案不仅支持NVIDIA的GPU,也支持AMD的GPU,而且对应生成的IR也可以在CPU上执行。 88 | 89 | **场景3:逻辑简化**。以数据库为例,数据库执行引擎在执行过程中需要做大量的数据类型、算法逻辑相关的判断。这主要是由于SQL中的数据类型和逻辑,很多是在数据库开发时无法确定的,只能在运行时决定。这一部分过程,也被称为“解释执行”。我们可以利用LLVM在运行时生成代码,由于这个时候数据类型和逻辑已经确定,我们可以在LLVM IR中删除那些不必要的判断操作,从而实现性能的提升。 90 | 91 | # 4. LLVM在数据库中的应用 92 | 93 | 在数据库当中,团队是用LLVM来进行表达式的处理,接下来我们以PostgreSQL数据库和云原生数据仓库AnalyticDB PostgreSQL为对比,解释LLVM的应用方法。 94 | 95 | PostgreSQL为了实现表达式的解释执行,采用了一套“拼函数”的方案。PostgreSQL中实现了大量C函数,比如加减法、大小比较等,不同类型的都有。SQL在生成执行计划阶段会根据表达式符号的类型和数据类型选择相应的函数、把指针存下来,等执行的时候再调用。因此对于 "a > 10 and b < 5"这样的过滤条件,假设a和b都是int32,PostgreSQL实际上调用了“Int8AndOp(Int32GT(a, 10), Int32LT(b, 5))”这样一个函数组合,就像搭积木一样。这样的方案有两个明显的性能问题。一方面这种方案会带来比较多次数的函数调用,函数调用本身是有成本的。另一方面,这种方案必须要实现一个统一的函数接口,函数内部和外部都需要做一些类型转换,这也是额外的性能开销。Odyssey使用LLVM 进行codegen,可以实现最小化的代码。因为在SQL下发以后,数据库是知道表达式的符号和输入数据的类型的,因此只需要根据需求选取相应的IR指令就可以了。因此只需要三条IR指令,就可以实现这个表达式,然后我们把表达式封装成一个函数,就可以在执行的时候调用了。这次操作,把多次函数调用简化成了一次函数调用,大大减少了指令的总数量。 96 | 97 | ``` 98 | // 样例SQLselect count(*) from table where a > 10 and b < 5;// PostgreSQL解释执行方案:多次函数调用result = Int8AndOp(Int32GT(a, 10), Int32LT(b, 5));// AnalyticDB PostgreSQL方案:使用LLVM codegen生成最小化底层代码%res1 = icmp ugt i32 %a, 10;%res2 = icmp ult i32 %b, 5; %res = and i8 %res1, %res2; 99 | ``` 100 | 101 | 在数据库中,表达式主要出现在几个场景。一类是过滤条件,通常出现在where条件中。一类是输出列表,一般跟在select之后。有些算子,比如join、agg等,它的判断条件中也可能会出现一些比较复杂的表达式。因此表达式的处理是会出现在数据库执行引擎的各个模块的。在AnalyticDB PostgreSQL版中,开发团队抽象出了一个表达式处理框架,通过LLVM Codegen来处理这些表达式,从而提高了执行引擎的整体性能。 102 | 103 | ![img](https://pics1.baidu.com/feed/f31fbe096b63f624870b873f9d7d5df01a4ca37e.jpeg?token=a69148209b808edc7929c4afd9f7f3ea) 104 | 105 | # 5. 总结 106 | 107 | LLVM作为一个流行的开源编译框架,近年来被用于数据库、AI等系统的性能加速。由于编译器理论本身门槛较高,因此LLVM的学习有一定的难度。而且从工程上,还需要对LLVM的工程特点和性能特征有比较准确的理解,才能找到合适的加速场景。阿里云数据库团队的云原生数据仓库产品AnalyticDB PostgreSQL版基于LLVM实现了一套运行时的表达式处理框架,能够有效地提高系统在进行复杂数据分析时地性能。 108 | -------------------------------------------------------------------------------- /llvm/文章/编译器及底层名词解释.md: -------------------------------------------------------------------------------- 1 | ## LLVM 2 | 3 | - clang: C语言编译器,类似于gcc 4 | - clang++: C++编译器,类似于g++。clang++只是clang的一个别名; 5 | - clang-format:按照固定的规范格式化C/C++代码,非常智能。文档请见:http://clang.llvm.org/docs/ClangFormat.html 6 | - clang-modernize:把按照C++98标准写的代码,转成C++11标准的。文档请见:http://clang.llvm.org/extra/ModernizerUsage.html 7 | - llvm-as:LLVM 汇编器 8 | - llvm-dis: LLVM 反汇编器 9 | - opt:LLVM 优化器 10 | - llc:LLVM 静态编译器 11 | - lli:LLVM的字节码执行器(某些平台下支持JIT) 12 | - llvm-link:LLVM的字节码链接器 13 | - llvm-ar:LLVM的静态库打包器,类似unix的ar。 14 | - llvm-nm:类似于unix的nm 15 | - llvm-ranlib:为 llvm-ar 打包的文件创建索引 16 | - llvm-prof:将 ‘llvmprof.out’ raw 数据格式化成人类可读的报告 17 | - llvm-ld :带有可装载的运行时优化支持的通用目标连接器 18 | - llvm-config:打印出配置时 LLVM 编译选项、库、等等 19 | - llvmc:一个通用的可定制的编译器驱动 20 | - llvm-diff:比较两个模块的结构 21 | - bugpoint:自动案例测试减速器 22 | - llvm-extract:从 LLVM 字节代码文件中解压出一个函数 23 | - llvm-bcanalyzer:字节代码分析器 (分析二进制编码本身,而不是它代表的程序) 24 | - FileCheck:灵活的文件验证器,广泛的被测试工具利用 25 | - tblgen:目标描述阅读器和生成器 26 | - lit:LLVM 集成测试器,用于运行测试 27 | -------------------------------------------------------------------------------- /【80页】eBPF学习笔记.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0voice/kernel_new_features/15f4834a469de52b2457ff0385060b6ef5df2c16/【80页】eBPF学习笔记.pdf --------------------------------------------------------------------------------