├── .gitignore ├── README.md ├── cloud_storage ├── README.md ├── basic │ ├── 001_cuckoo_hashing.md │ ├── 002_bloomfilter.md │ ├── 003_cuckoo_filter.md │ └── 004_consistent_hashing.md ├── implementation_consideration.md ├── introduction.md ├── public_vs_private.md ├── storage_types.md ├── swift │ ├── architectural.md │ ├── create_object.md │ ├── disadvantages.md │ ├── ring.md │ └── summary.md └── thinking_about_cloud_storage.md ├── common ├── 016_ipv6.md └── 017_tls.md ├── container ├── 002_what_is_container.md ├── 003_namespace_and_cgroup.md ├── cgroup │ ├── 001_cgroup_introduction.md │ ├── 002_cgroup_no_subsystem.md │ ├── 003_cgroup_pids.md │ ├── 004_cgroup_memeory.md │ └── 005_cgroup_cpu.md ├── docker │ ├── 000_docker_index.md │ ├── 001_how_does_hello_world_work.md │ ├── 002_what_is_image.md │ ├── 003_run_hello_world_without_docker.md │ ├── 004_runtime.md │ ├── 005_how_does_docker_manage_images.md │ ├── 006_what_happened_behind_the_docker_create_command.md │ ├── 007_what_happened_behind_the_docker_start_command.md │ └── 010_how_to_read_code.md └── namespace │ ├── 001_namespace_introduction.md │ ├── 002_namespace_uts.md │ ├── 003_namespace_ipc.md │ ├── 004_namespace_mount.md │ ├── 005_namespace_pid.md │ ├── 006_namespace_network.md │ ├── 007_namespace_user_01.md │ ├── 008_namespace_user_02.md │ └── 009_create_simple_container.md ├── devkit ├── python.vimrc ├── sublime.md └── vim.md ├── linux ├── 001_start_process_of_linux.md ├── 002_mount_01.md ├── 003_mount_02.md ├── 004_memeory_management.md ├── 005_swap_space.md ├── 006_process_memory.md ├── 007_linux_oom_killer.md ├── 008_cpu.md ├── 009_vfs.md ├── 010_file_system_comparison.md ├── 011_aufs.md ├── 012_btrfs.md ├── 013_network.md ├── 014_network_receiving_data.md ├── 015_network_sending_data.md ├── 018_iptables.md ├── 019_tty.md ├── 020_session_and_job.md ├── 021_network_tun-tap.md ├── 022_network_veth.md └── 023_network_bridge.md ├── python └── 001_i18n.md └── raspberrypi └── 001_introduction.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | tmp.md 3 | tmp.txt 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 个人博客 2 | 3 | 内容比较杂,既记录自己的一些学习过程,也记录一些自己的经验总结,所以质量有好有坏,请抱着怀疑的态度来阅读,欢迎批评指正。 4 | 5 | ## 通用 6 | 7 | * [IPv6简介](common/016_ipv6.md) 8 | * [SSL/TLS及证书概述](common/017_tls.md) 9 | 10 | ## Linux 11 | 12 | * [Linux的启动过程](linux/001_start_process_of_linux.md) 13 | * [Linux mount (第一部分)](linux/002_mount_01.md) 14 | * [Linux mount (第二部分 - Shared subtrees)](linux/003_mount_02.md) 15 | * [Linux内存管理](linux/004_memeory_management.md) 16 | * [Linux交换空间(swap space)](linux/005_swap_space.md) 17 | * [Linux进程的内存使用情况](linux/006_process_memory.md) 18 | * [inux OOM killer](linux/007_linux_oom_killer.md) 19 | * [Linux CPU使用率](linux/008_cpu.md) 20 | * [Linux虚拟文件系统简介](linux/009_vfs.md) 21 | * [Linux下常见文件系统对比](linux/010_file_system_comparison.md) 22 | * [Linux文件系统之aufs](linux/011_aufs.md) 23 | * [Btrfs文件系统之subvolume与snapshot](linux/012_btrfs.md) 24 | * [网络分层概述](linux/013_network.md) 25 | * [Linux网络 - 数据包的接收过程](linux/014_network_receiving_data.md) 26 | * [Linux网络 - 数据包的发送过程](linux/015_network_sending_data.md) 27 | * [netfilter/iptables简介](linux/018_iptables.md) 28 | * [Linux TTY/PTS概述](linux/019_tty.md) 29 | * [Linux session和进程组概述](linux/020_session_and_job.md) 30 | * [虚拟网络设备之tun/tap](linux/021_network_tun-tap.md) 31 | * [虚拟网络设备之veth](linux/022_network_veth.md) 32 | * [Linux虚拟网络设备之bridge(桥)](linux/023_network_bridge.md) 33 | 34 | ## 容器技术 35 | 36 | * [容器概述](container/002_what_is_container.md) 37 | :如果对容器的一些概念有些模糊,建议看下这个 38 | * [Namespace & CGroup](container/003_namespace_and_cgroup.md) 39 | :详细介绍了Linux内核里面不同类型的Namespace和CGroup。 40 | * [docker](container/docker/000_docker_index.md):介绍docker背后是怎么实现的。 41 | 42 | ## Python 43 | * [Python多国语言支持](python/001_i18n.md) -------------------------------------------------------------------------------- /cloud_storage/README.md: -------------------------------------------------------------------------------- 1 | ### 介绍 2 | 3 | 本人目前从事对象存储的开发,将自己学习以及工作过程中的一些理解整理出来,一方面梳理一下自己的思路,也希望对初学者有帮助。 4 | 5 | #### 云存储 6 | 7 | * [公有云和私有云存储的区别](/cloud_storage/public_vs_private.md) 8 | * [云存储的分类](/cloud_storage/storage_types.md) 9 | 10 | #### 基础知识 11 | 12 | * [cuckoo hashing](/cloud_storage/basic/001_cuckoo_hashing.md) 13 | * [bloomfilter](/cloud_storage/basic/002_bloomfilter.md) 14 | * [cuckoo filter](/cloud_storage/basic/003_cuckoo_filter.md) 15 | 16 | 17 | #### Openstack Swift 18 | TBD 19 | 20 | ### CEPH 21 | TBD 22 | -------------------------------------------------------------------------------- /cloud_storage/basic/001_cuckoo_hashing.md: -------------------------------------------------------------------------------- 1 | # Cuckoo Hashing概述 2 | 3 | 最近在研究Cuckoo Filter, 于是将hash表相关的数据结构重新学习了一下,在这里将自己的理解整理下来,仅供参考。 4 | 5 | 本文将先介绍Cuckoo Hashing,后续会介绍Bloom Filter和Cuckoo Filter。 6 | 7 | ## 传统Hashing 8 | 在当前主流的开发语言中,都有现成的基于哈希表的key/value map来存储数据,如HashMap(Java), dict(Python), unordered_map(C++), map(Go), 相对于基于平衡二叉树的map(如Java的TreeMap,C++的map),基于哈希表的map不是有序的,但是它的插入查找速度要快(前提条件是hash函数设置的比较合理)。 9 | 10 | 对于hash冲突,当前主要的有两种解决办法,一种是Open Addressing,一种是Chaining 11 | 12 | ### Open Addressing 13 | 当发现冲突之后,在hash表的其它位置再找一个空闲的位置来放置新的元素,怎么再找一个新的位置呢?有很多种办法,比如linear probing,quadratic probing等,哪种方法好取决于数据的特点,但目的都是要尽可能的减少冲突,并尽可能的将hash值一样的数据放在一起来利用CPU cache提高性能,这里以线性探测为例,介绍一下冲突解决的办法。 14 | 15 | 为了简单起见,这里假设哈希表的长度为10(在具体实现的时候,长度一般是质数,这样可以让数据更分散,减少冲突) 16 | 17 | #### 插入 18 | 假设hash表的长度是10, 里面已经有了21,12,35, 60四个数据,现在我们想插入22,23,24 19 | ``` 20 | 0 1 2 3 4 5 6 7 8 9 21 | +----+----+----+----+----+----+----+----+----+----+ 22 | | 60 | 21 | 12 | | | 35 | | | | | 23 | +----+----+----+----+----+----+----+----+----+----+ 24 | ``` 25 | 第一步,插入22,由于位置已经被12占了,于是只能往后挪一步,放到3的位置 26 | ``` 27 | 0 1 2 3 4 5 6 7 8 9 28 | +----+----+----+----+----+----+----+----+----+----+ 29 | | 60 | 21 | 12 | 22 | | 35 | | | | | 30 | +----+----+----+----+----+----+----+----+----+----+ 31 | ``` 32 | 插入23,由于位置已经被22占了,只能往后挪一步,放到4的位置 33 | ``` 34 | 0 1 2 3 4 5 6 7 8 9 35 | +----+----+----+----+----+----+----+----+----+----+ 36 | | 60 | 21 | 12 | 22 | 23 | 35 | | | | | 37 | +----+----+----+----+----+----+----+----+----+----+ 38 | ``` 39 | 插入24,由于位置已经被23占了,只能往后挪,由于下一个位置被35占了,只能继续往后挪,最后放到了6的位置 40 | ``` 41 | 0 1 2 3 4 5 6 7 8 9 42 | +----+----+----+----+----+----+----+----+----+----+ 43 | | 60 | 21 | 12 | 22 | 23 | 35 | 24 | | | | 44 | +----+----+----+----+----+----+----+----+----+----+ 45 | ``` 46 | 想象一下现在要插入31,就需要挪到7的位置。 47 | 48 | #### 查找 49 | 查找的时候跟插入的时候一样,比如要找24,发现4的位置是23,于是继续挨个的往后找,直到6为止。 50 | 51 | 如果是查找一个不存在的值呢?比如查找31,发现1的位置是不是31,于是挨个的往后找,直到走到7的位置,发现是空的,才表示当前哈希表中没有31. 52 | 53 | #### 删除 54 | 55 | 当待删除的数据后面有数据时,只能标记为已删除,但不能真正的删除掉该数据,不然会导致后面的数据有可能找不到,比如删除35,如果35被真正的删除掉了之后,就会变成下面的样子 56 | ``` 57 | 0 1 2 3 4 5 6 7 8 9 58 | +----+----+----+----+----+----+----+----+----+----+ 59 | | 60 | 21 | 12 | 22 | 23 | | 24 | | | | 60 | +----+----+----+----+----+----+----+----+----+----+ 61 | ``` 62 | 当再查找24的时候,首先看4的位置不是24,再往后找,发现5的位置是空的,于是返回24不存在,但实际上24是在哈希表中的。 63 | 64 | ### Chaining 65 | 66 | 这种方法比较直观,根据hash code找到对应的位置(一般称为bucket),然后在位置后面的linked list中添加,查找,删除元素即可。 67 | 68 | 初始状态: 69 | ``` 70 | 0 1 2 3 4 5 6 7 8 9 71 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 72 | | | | | | | | | | | | 73 | +--↓--+--↓--+--↓--+-----+-----+--↓--+-----+-----+-----+-----+ 74 | ↓ ↓ ↓ ↓ 75 | +--+ +--+ +--+ +--+ 76 | |60| |21| |12| |35| 77 | +--+ +--+ +--+ +--+ 78 | ``` 79 | 插入22,23,24之后 80 | ``` 81 | 0 1 2 3 4 5 6 7 8 9 82 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 83 | | | | | | | | | | | | 84 | +--↓--+--↓--+--↓--+--↓--+--↓--+--↓--+-----+-----+-----+-----+ 85 | ↓ ↓ ↓ ↓ ↓ ↓ 86 | +--+ +--+ +--+ +--+ +--+ +--+ 87 | |60| |21| |12| |23| |24| |35| 88 | +--+ +--+ +--+ +--+ +--+ +--+ 89 | ↓ 90 | +--+ 91 | |22| 92 | +--+ 93 | ``` 94 | ### 特点 95 | 两种方法都有一个缺点,就是都没法控制最坏情况下的性能。 96 | 97 | #### Open Addressing 98 | * 当表里面的数据过满的时候,性能会大幅度的下降,比如哈希表满了之后,就会退化为O(n)的复杂度,所以一般实现的时候会在哈希表的使用率达到50%的时候,进行rehash。 99 | * 相同哈希值的数据存储在连续的地址空间中,可以充分利用cpu的cache,一次读取一批数据,然后进行比较 100 | 101 | #### Chaining 102 | * 插入,查找,删除的时候都只会和同自己hash code一样的数据进行比较,减少了比较次数 103 | * 一般只会在linked list过长的时候才会rehash,理论上空间利用率较高 104 | 105 | ## Cuckoo Hashing 106 | Cuckoo Hashing的最大特点就是能保证最坏情况下的查找性能能达到O(1)。 107 | 108 | Cuckoo Hashing采用两张hash表和两个hash函数,为了简单起见,假设第一个hash函数func1是取数字的十位数,第二个hash函数func2取个位数,那么对于数字25来说,func1(25) = 2, func2(25) = 5,同理func1(5) = 0, func2(5) = 5, func1(125) = 2, func2(125) = 5。 109 | 110 | ### 插入 111 | 下面我们依次插入5, 25, 28, 8, 125,看看结果 112 | 113 | * func1(5) = 0, func2(5) = 5,它可以放在T1的第0个位置和T2的第5个位置,我们这里选择T1表优先(具体实现时哪个表优先都可以) 114 | ``` 115 | 0 1 2 3 4 5 6 7 8 9 116 | +----+----+----+----+----+----+----+----+----+----+ 117 | T1: | 5 | | | | | | | | | | 118 | +----+----+----+----+----+----+----+----+----+----+ 119 | 0 1 2 3 4 5 6 7 8 9 120 | +----+----+----+----+----+----+----+----+----+----+ 121 | T2: | | | | | | | | | | | 122 | +----+----+----+----+----+----+----+----+----+----+ 123 | 124 | ``` 125 | 126 | * func1(25) = 2, func2(25) = 5,它可以放在T1的第2个位置和T2的第5个位置,T1表优先,于是放到T1的2号位置 127 | ``` 128 | 0 1 2 3 4 5 6 7 8 9 129 | +----+----+----+----+----+----+----+----+----+----+ 130 | T1: | 5 | | 25 | | | | | | | | 131 | +----+----+----+----+----+----+----+----+----+----+ 132 | 0 1 2 3 4 5 6 7 8 9 133 | +----+----+----+----+----+----+----+----+----+----+ 134 | T2: | | | | | | | | | | | 135 | +----+----+----+----+----+----+----+----+----+----+ 136 | 137 | ``` 138 | 139 | * func1(28) = 2, func2(28) = 8,它可以放在T1的第2个位置和T2的第8个位置,但由于T1表的2号位已经被占了,现在有两种选择,一种是抢占2的位置,另一种是优先空位置,这里我们采取优先空位置的策略,将28放入T2的8号位 140 | ``` 141 | 0 1 2 3 4 5 6 7 8 9 142 | +----+----+----+----+----+----+----+----+----+----+ 143 | T1: | 5 | | 25 | | | | | | | | 144 | +----+----+----+----+----+----+----+----+----+----+ 145 | 0 1 2 3 4 5 6 7 8 9 146 | +----+----+----+----+----+----+----+----+----+----+ 147 | T2: | | | | | | | | | 28 | | 148 | +----+----+----+----+----+----+----+----+----+----+ 149 | 150 | ``` 151 | 152 | * func1(8) = 0, func2(8) = 8,它可以放在T1的第0个位置和T2的第8个位置,但不幸的是两个位置都被人占了,于是它得抢一个,那是抢5的位置还是28的位置呢,在实现的时候一般是随机的,这里假设抢28的位置,于是28被拿出来,将8放进去 153 | ``` 154 | 0 1 2 3 4 5 6 7 8 9 155 | +----+----+----+----+----+----+----+----+----+----+ 156 | T1: | 5 | | 25 | | | | | | | | 157 | +----+----+----+----+----+----+----+----+----+----+ 158 | 0 1 2 3 4 5 6 7 8 9 159 | +----+----+----+----+----+----+----+----+----+----+ 160 | T2: | | | | | | | | | 8 | | 161 | +----+----+----+----+----+----+----+----+----+----+ 162 | 163 | ``` 164 | 165 | 那28怎么办呢?它只能去抢25的位置,于是25被拿出来,放入28 166 | ``` 167 | 0 1 2 3 4 5 6 7 8 9 168 | +----+----+----+----+----+----+----+----+----+----+ 169 | T1: | 5 | | 28 | | | | | | | | 170 | +----+----+----+----+----+----+----+----+----+----+ 171 | 0 1 2 3 4 5 6 7 8 9 172 | +----+----+----+----+----+----+----+----+----+----+ 173 | T2: | | | | | | | | | 8 | | 174 | +----+----+----+----+----+----+----+----+----+----+ 175 | 176 | ``` 177 | 178 | 还好25还有一个空位置,于是被放入T2的5号位 179 | ``` 180 | 0 1 2 3 4 5 6 7 8 9 181 | +----+----+----+----+----+----+----+----+----+----+ 182 | T1: | 5 | | 28 | | | | | | | | 183 | +----+----+----+----+----+----+----+----+----+----+ 184 | 0 1 2 3 4 5 6 7 8 9 185 | +----+----+----+----+----+----+----+----+----+----+ 186 | T2: | | | | | | 25 | | | 8 | | 187 | +----+----+----+----+----+----+----+----+----+----+ 188 | 189 | ``` 190 | 191 | * func1(125) = 2, func2(125) = 5,这个路径就会更长,并且不论是选择先抢28还是25的位置,都会发现会进入一个环,没有空位置可用,这个时候就需要进行re-hash,否则插入数据就会失败 192 | 193 | ### 查找 194 | 查找很简单,比如要查找25,只需查看T1的2号位和T2的5号位,如果25在这两个位置的任意一个,查找成功,否则失败。 195 | 196 | ### 删除 197 | 删除和查找一样,找到对应的数据,然后删掉即可 198 | 199 | ### 总结 200 | 在具体实现的时候,为了优化性能,可能会和这里描述的有一些细微的不同,但总体思路是一样的。为了减少冲突,在实现时可以选择多于两张hash表,或者hash表的一个位置放多个数据,由于计算hash值比较耗CPU,一般会选择第二种方法,比如上面的例子,如果一个位置可以放两个值得话,插入125就没有问题,它可以和25或者28放在一起。 201 | 202 | * 插入的速度取决于冲突的情况,有时可能会比较差 203 | * 查找的时候只需要检查两个位置,速度快 204 | * 空间利用率高,由于采取多个hash函数,有多个hash表,冲突的几率比较低 205 | 206 | 跟传统的Hashing算法相比,Cuckoo Hashing有更好的查找性能和空间利用率,非常适合查找频率大于插入频率的场合。 207 | 208 | ## 参考 209 | 210 | * [Cuckoo Hashing](https://www.cs.tau.ac.il/~shanir/advanced-seminar-data-structures-2009/bib/pagh01cuckoo.pdf) 211 | * [Cuckoo Hashing的应用及性能优化](https://yq.aliyun.com/articles/563053) 212 | 213 | -------------------------------------------------------------------------------- /cloud_storage/basic/002_bloomfilter.md: -------------------------------------------------------------------------------- 1 | # Bloomfilter概述 2 | 3 | Bloomfilter主要用来快速判断某个元素一定不在指定集合内(不能判断元素一定在集合内),具有运行速度快,占用内存少的优点。 4 | 5 | ``` 6 | +-------------+ 7 | | | 8 | | | 9 | +-------------+ | | 10 | +--------+ get/put | | | | 11 | | client | <------------->| bloomfilter |<--------->| DataSet | 12 | +--------+ | | | | 13 | +-------------+ | | 14 | | | 15 | | | 16 | +-------------+ 17 | ``` 18 | 19 | 如上图所示, bloomfilter处于DataSet和需要访问DataSet的client之间,client在对DataSet进行修改的时候通知bloomfilter,读取DataSet的时候先询问bloomfilter数据是否在DataSet中,如果不存在则立即返回,否则继续访问DataSet。 20 | 21 | 在需要bloomfilter的应用场景中,通常访问DataSet的开销比较大,比如DataSet存放在磁盘上或者网络上 22 | 23 | ## 实现 24 | 25 | bloomfilter主要由两部分构成,存放数据的bit数组和处理数据的hash函数,数组的长度和hash函数的个数取决于DataSet的大小以及所期望的fpp值(后面介绍)。 26 | 27 | ### 插入 28 | 29 | 一般在往DataSet里面插入数据之前,先更新bloomfilter,这样可以保证只要数据成功写入DataSet,那么bloomfilter就一定已经更新成功,数据的正确性就得到了保证,不然就会出现数据写成功了,但bloomfilter没有更新成功的情况,导致下次读取数据的时候bloomfilter报数据不存在。 30 | 31 | 如何更新bloomfilter呢?我们这里假设采用两个hash函数fun1和fun2, bit数组的长度是10,依次插入两个数据d1和d2。 32 | 33 | 1. 用两个hash函数对d1进行hash,假设hash结果跟bit数组的长度取余之后为分别是2和5,那么将bit数组的第2和第5位设置位1 34 | ``` 35 | 0 1 2 3 4 5 6 7 8 9 36 | +---+---+---+---+---+---+---+---+---+---+ 37 | | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 38 | +---+---+---+---+---+---+---+---+---+---+ 39 | ``` 40 | 41 | 2. 用两个hash函数对d1进行hash,假设hash结果跟bit数组的长度取余之后为分别是2和8, ,那么将bit数组的第2和第8位设置位1 42 | ``` 43 | 0 1 2 3 4 5 6 7 8 9 44 | +---+---+---+---+---+---+---+---+---+---+ 45 | | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 46 | +---+---+---+---+---+---+---+---+---+---+ 47 | ``` 48 | 49 | ### 判断是否存在 50 | 51 | 当需要读取DataSet的数据的时候,先访问bloomfilter,判断数据是否存在,如果不存在,那么就可以提前直接返回,不需要再访问DataSet了。 52 | 53 | 判断是否存在和上面的插入操作一样,假设现在访问d1,那么同样的用两个hash函数对d1进行hash,然后判断第2和第5位是不是都是1,如果都是1,表示d1有可能存在于DataSet中,需要继续访问DataSet,只要有任何一位不是1,表示一定不存在,可以直接返回。这里为什么是有可能存在呢?因为bloomfilter不支持remove操作,所以如果d1已经从DataSet中移除了,bloomfilter是不知道的,并且hash值冲突也可能造成误报,这也是bloomfilter不能判断元素一定在DataSet内的原因。 54 | 55 | 为什么bloomfilter不支持remove呢?假设现在有一个d3,hash后的位置是5和6,于是bit数组变成了下面这样: 56 | ``` 57 | 0 1 2 3 4 5 6 7 8 9 58 | +---+---+---+---+---+---+---+---+---+---+ 59 | | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 60 | +---+---+---+---+---+---+---+---+---+---+ 61 | ``` 62 | 63 | 接着删除掉d2和d3,如果只是简单的将d2的2和8以及d3的5和6位设置为0的话,整个bit数组就会变成全0,这样在读取d1的时候,就会报d1不存在,但实际上d1还在DataSet里面,于是出现了正确性的问题。 64 | 65 | ## 参数 66 | 67 | ### False Positive 68 | 69 | 表示bloomfilter报数据存在,但在DataSet中找不到数据的情况,一般是由下面这些原因引起的 70 | 71 | 1. DataSet的数据被更新或者删除 72 | 2. 有hash结果一样的数据(由于是多个hash函数,这种情况几率较小) 73 | 3. bit数组比较满了,导致大家的hash结果相互重叠。(对于上面的bloomfilter,如果来读取一个d4,假设它的hash结果是6和8,那么bloomfilter就会报数据存在,可实际上DataSet中没有d4,于是出现False Positive.) 74 | 75 | ### FPP(False Positive Probability) 76 | 77 | 表示bloomfilter报数据存在,但在DataSet中找不到数据的几率,这个数字越小越好,一般由DataSet的大小,bit数组的长度以及hash函数的个数决定。有一个通用的公式计算预期的FPP=(1-e^(-kn/m))^k,这里e是常数,n是预估的DataSet的大小,m是bit数组的长度,k是hash函数的个数。 78 | 公式详情请参考[wikipedia关于Bloom filter的介绍](https://en.wikipedia.org/wiki/Bloom_filter) 79 | 80 | ### bit数组 81 | 82 | 理想情况下我们希望的是bit数组越小越好,这样能节约内存,但太小的数组容易被填满,导致FPP偏高。 83 | 84 | ### hash函数 85 | 86 | 对于hash函数而言,最好是结果足够分散,并且速度快,hash函数之间相互独立。hash函数越多,运算速度越慢,bit数组越容易被充满,bit数组越满FPP就会越高,但如果hash函数太少,会导致冲突变多,也会影响FPP。它的理想值k = (m/n)ln(2),n是预估的DataSet的大小,m是bit数组的长度 87 | 88 | ### 初始化bloomfilter 89 | 90 | 有了上面这些影响bloomfilter的参数,我们就得到了初始化bloomfilter的过程: 91 | 92 | 1. 评估DataSet的大小n 93 | 2. 根据可供支配内存的大小选择一个bit数组的长度m 94 | 3. 根据前两步的m和n, 计算最优的hash函数个数k = (m/n)ln(2) 95 | 4. 再根据上面FPP里面介绍的公式,由前三步的n,m,k计算出预期的FPP,如果FPP值高于自己的要求,那么重新回到第二步,增加数组的长度m,接着重复3和4步,直到FPP值达到预期。 96 | 97 | ## 注意 98 | 99 | 1. 如果实际的DataSet大小比预估的大,那么得到的实际FPP也会高出预期值。 100 | 2. 如果remove操作的比例比较高,那么实际的FPP也会高出预期 101 | 3. 这里没提到数据的update,因为一般情况下一个update就相当于一次delete加一次add操作 102 | 103 | 所以对于bloomfilter来说,预估一定要尽量准确,大了浪费空间,小了效果会打折,同时remove所占的比例不能太高,否则效果同样会打折。在实际使用过程中,一定要用实际的数据实测一下,看具体效果如何,并根据测试结果调整m和期待的FPP大小。 104 | 105 | ## 参考 106 | 107 | * [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) 108 | * [bloomfilter tutorial](http://llimllib.github.io/bloomfilter-tutorial/) -------------------------------------------------------------------------------- /cloud_storage/basic/003_cuckoo_filter.md: -------------------------------------------------------------------------------- 1 | # Cuckoo filter概述 2 | 3 | [上一篇介绍了bloomfilter](002_bloomfilter.md),它的一个最大缺点就是不支持删除,这里将介绍的Cuckoo filter是一种支持删除的filter。 4 | 5 | 相对于bloomfilter,它的主要优点有: 6 | 7 | 1. 支持删除操作 8 | 2. 查询效率更高 9 | 3. 更高的空间利用率 10 | 11 | ## 实现 12 | 13 | Cuckoo filter基于Cuckoo hashing,也是两个table加两个hash函数。 14 | 15 | 回顾一下[前面介绍的cuckoo hashing](001_cuckoo_hashing.md),如果两个key出现冲突会怎么样?会将原来位置上的key挤掉,那么原来位置上的key去哪里呢?怎么知道它在另一个table中的位置呢?对于cuckoo hashing来说,这个不是问题,因为数据就存在cuckoo hashing里面,根据key和hash函数就可以算出它在另一个table中的位置。 但Cuckoo filter为了节约空间,不可能将key存储起来,那么当冲突发生的时候,原来位置上的key怎么知道它在另一个table中的位置呢? 16 | 17 | ### 插入 18 | 19 | 对于cuckoo hashing来说,两个hash函数是相互独立的,因为key会存储在cuckoo hashing中,所以根据key和两个hash函数可以随时算出key在两个table中的位置。 20 | ``` 21 | h1(x) = hash1(x) <---- 这里x表示key 22 | h2(x) = hash2(x) 23 | ``` 24 | 25 | 对于cuckoo filter来说,它不存储key,但会存储一个fingerprint,好处是fingerprint相对于key来说占用的空间会小很多 26 | ``` 27 | h1(x) = hash1(x), 28 | h2(x) = h1(x) ⊕ hash2(x’s fingerprint) 29 | ``` 30 | 31 | 由于是异或操作,根据h2(x)和fingerprint可以得到h1(x) 32 | ``` 33 | h1(x) = h2(x) ⊕ hash2(x’s fingerprint) 34 | ``` 35 | 36 | 所以当冲突发生的时候,根据当前位置的hash值(即当前位置的索引)和fingerprint就可以算出在另一table中的位置。 37 | 38 | ### 查找 39 | 40 | 根据插入时的算法,算出h1(x)和h2(x),再去t1和t2的相应位置找是否有同样fingerprint的记录,如果没有,表示key一定不存在,否则key在DataSet中可能存在 41 | 42 | ### 删除 43 | 44 | 删除跟查找一样,根据插入时的算法,算出h1(x)和h2(x),再去t1和t2的相应位置找是否有同样fingerprint的记录,如果有,删掉一条(fingerprint可能有重复) 45 | 46 | ## 注意事项 47 | 48 | ### 数据冲突 49 | 50 | 如果出现x1和x2,hash1(x1) == hash1(x2),且它们的fingerprint一样,cuckoo filter没法区分出x1和x2是否是同一个key。它会在table里面存储两个一样的fingerprint。由于delete的时候只会删除一个fingerprint,所以不会出现正确性问题。 51 | 52 | 由于这个特点,就要求在使用cuckoo filter的时候,不要重复的往里面插入相同的数据 53 | 54 | * 一方面由于重复的数据都放在同样的位置,且同一个位置的空间有限,会导致数据插入失败 55 | * 重复插入需要同等数量的删除操作,否则就会出现数据不存在,但cuckoo filter报存在的情况,增加FPP 56 | 57 | **特别注意:删除操作次数一定不能多于插入操作次数**,否则会导致数据准确性问题。如上面的x1和x2,如果插入它们之后连续调用两次删除x1的操作,那么接下来查找x2的时候就会报数据不存在,出现数据准确性错误。 58 | 59 | ### insert失败 60 | 61 | 跟bloomfilter相比,cuckoo filter可能会出现insert失败的情况,空间满了或者冲突过多(同cuckoo hashing一样),出现这种情况后,当前的cuckoo filter就不能再用了,由于没有保存key,所以没法进行rehash,只能调整cuckoo filter的参数,然后根据DataSet重新构建全新的cuckoo filter。而bloomfilter就没有这种情况,冲突多只会提高FPP,不会因为数据插入不进去而导致正确性问题。 62 | 63 | ## 最佳实践 64 | 65 | 关于bucket的最佳大小和fingerprint的最佳长度, [Cuckoo Filter论文](https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf)里给出了详细的推导过程以及测试结果,详情情况请参考论文。 66 | 67 | ### hash table每个bucket的大小 68 | 69 | 4是一个很合适的大小,大点的值会让table的占用率变高,但同时也需要更长的fingerprint的来避免冲突,造成整体空间消耗变大 70 | 71 | ### fingerprint的长度 72 | 73 | fingerprint的长度跟数据规模n以及bucket的大小有关,在bucket大小为4的情况下,2^15 ~ 2^30的数据量只需4 ~ 6 bits 74 | 75 | ## 优点分析 76 | 77 | 在文章的最开始说了cuckoo filter的三个优点,上面只分析了第一个优点,支持删除(update=delete+insert),这里简单分析一下其它的两个优点 78 | 79 | ### 查询效率更高 80 | 81 | 如果应用场景需要很低的FPP,那么bloomfilter需要采用较大的table和较多的hash函数,但cuckoo filter始终只需要计算两次hash 82 | 83 | * cuckoo filter的hash计算少,速度快 84 | * cuckoo filter只会读两个位置的数据,而bloomfilter里面bit位可能比较分散,需要读多个位置的数据。 85 | 86 | ### 空间利用率高 87 | 88 | bloomfilter对于每个hash结果的位置只需做一个标记,只占用1 bit的空间,而cuckoo filter需要记录fingerprint,明显需要更多的空间,为什么说cuckoo filter的空间利用率高呢? 89 | 90 | 因为在应用场景需要很低FPP的时候(< %3),bloomfilter由于冲突几率高,需要更大的bit数组长度来满足要求,但cuckoo filter由于冲突低,只需要较短的table即能满足要求。 91 | 92 | 所以虽然cuckoo filter中每个元素占用的空间大,但由于table的长度短,所以在当FPP低于一定值的时候(理论值是3%),cuckoo filter的空间利用率要高于bloomfilter 93 | 94 | ## 结论 95 | 96 | 通过上面的分析可以看出,cuckoo filter并不是在所有的场合都好于bloomfilter,当有大量删除操作的时候,cuckoo filter肯定要优于bloomfilter,但如果对FPP的要求不是很高,bloomfilter就不需要太多的hash函数和很长的table,无论从性能还是空间利用率都要优于cuckoo filter,同时bloomfilter的容错性较好,反复插入同样的数据不会影响正确性,同时也不需要关心删除操作。 97 | 98 | ## 参考 99 | 100 | * [Cuckoo Filter: Practically Better Than Bloom](https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf) 101 | -------------------------------------------------------------------------------- /cloud_storage/basic/004_consistent_hashing.md: -------------------------------------------------------------------------------- 1 | # 一致性hashing在云存储中的应用概述 2 | 3 | 在分布式系统中,有很多个节点,在处理请求的时候,一般是将请求进行hash,然后根据节点的数量进行取模,得到请求最终发往的节点上,这样可以让每个节点的负载变得均衡,但问题是,如果节点的数量发生变化,hash后再取模,很多request就会发到其它的节点上去,对于无状态的应用来说,问题不大,但对于有状态的应用来说,就会带来很大的问题,比如cache,如果大量的request发到其它的节点上去,就会造成cache失效,需要cache节点重启加载数据,对于存储来说,更严重,原来存储到A节点的数据,现在变成了存在B节点上,就涉及到数据的迁移,否则新的request发到B节点后,会找不到数据。 4 | 5 | 一致性hashing要解决的问题就是当节点数量发生变化的时候,只让少量的数据迁移到其它的node,并且保持节点负载的均衡。通常采用如下这些方法。 6 | 7 | ## 传统hashing 8 | 9 | 假设我们节点的数量是5,对于传统的hashing,每个节点对应一个bucket(slot),相当于hash表的长度是5,对于10~14这5个数字而言,采用简单的取余方法,在hash表中的位置如下 10 | ``` 11 | 0 1 2 3 4 12 | +----+----+----+----+----+ 13 | | 10 | 11 | 12 | 13 | 14 | 14 | +----+----+----+----+----+ 15 | ``` 16 | 17 | * 增加一个节点会变成下面的样子 18 | ``` 19 | 0 1 2 3 4 5 20 | +----+----+----+----+----+----+ 21 | | 12 | 13 | 14 | | 10 | 11 | 22 | +----+----+----+----+----+----+ 23 | ``` 24 | 25 | * 减少一个节点后变成下面的样子 26 | ``` 27 | 0 1 2 3 28 | +----+----+-------+----+ 29 | | 12 | 13 | 10,14 | 11 | 30 | +----+----+-------+----+ 31 | ``` 32 | 33 | 从上面可以看出,当增加和减少一个节点的时候,这些数字在hash表中的位置全部都发生了变化 34 | 35 | ## 改进1 (解决移动多的问题) 36 | 上面传统hash的问题主要是hash表的长度是节点的数量,现在考虑一种改进办法,hash表的长度固定,每个节点对应一段范围的hash值,这个固定长度可以很长,比如int的范围,这里为了简单起见,假设hash表的长度是10,节点数是5 37 | 38 | 这里假设5个节点均匀的分布在hash表中,还是上面几个数字,会得到下面的结果,10和11在n1上,12和13在n2上,14在n3上,n4和n5上没有数据 39 | ``` 40 | 0 1 2 3 4 5 6 7 8 9 41 | +----+----+----+----+----+----+----+----+----+----+ 42 | | 10 | 11 | 12 | 13 | 14 | | | | | | 43 | +----+----+----+----+----+----+----+----+----+----+ 44 | ↓ ↓ ↓ ↓ ↓ 45 | n1 n2 n3 n4 n5 46 | ``` 47 | 48 | * 增加一个节点,假设把增加的节点放在n1和n2之间,可以看出只有12的位置发生了变化,从n2变到了n6,其它的都没变 49 | ``` 50 | 0 1 2 3 4 5 6 7 8 9 51 | +----+----+----+----+----+----+----+----+----+----+ 52 | | 10 | 11 | 12 | 13 | 14 | | | | | | 53 | +----+----+----+----+----+----+----+----+----+----+ 54 | ↓ ↓ ↓ ↓ ↓ ↓ 55 | n1 n6 n2 n3 n4 n5 56 | ``` 57 | 58 | * 删除一个节点,假设删掉n2,可以看出只有12和13的位置发生了变化,从n2变到了n3 59 | ``` 60 | 0 1 2 3 4 5 6 7 8 9 61 | +----+----+----+----+----+----+----+----+----+----+ 62 | | 10 | 11 | 12 | 13 | 14 | | | | | | 63 | +----+----+----+----+----+----+----+----+----+----+ 64 | ↓ ↓ ↓ ↓ 65 | n1 n3 n4 n5 66 | ``` 67 | 68 | 从上面可看出,这种方法当节点数量发生变化的时候,只有很少一部分数据所属的节点发生变化,但这种方法有很多缺点。 69 | * 数据hash并得到它在hash表的位置后,还要根据范围查找到它所属于的节点,查找的过程需要O(logn)的复杂度 70 | * hash表的长度固定,但节点数量在变化,如何根据节点数来分配每个节点的范围? 71 | * 一种是这里例子中的平均分配,但很明显,当增加和减少节点后,节点的负载会很不均衡 72 | * 一种是随机(或者hash节点并映射到hash表中),当节点数量较少时,随机也不能保证均衡(随机一般只有在数据基数比较大时才会相对比较平均) 73 | * 节点的位置无论采取平均分配还是随机分配,都没法解决节点之间的差异问题,比如有些节点的容量大性能好,但它还是和其它节点一样分配范围 74 | 75 | ## 改进2 (解决分配不均衡问题) 76 | 解决办法是增加虚拟节点,将一个物理节点变成N个虚拟节点,N的大小可以根据每个节点的性能设置的不一样,但虚拟节点到物理节点的对应关系固定。 77 | 78 | 但这种方法也不能采取平均分配的方法,因为当物理节点的数量发生变化时,虚拟节点的数量也发生了变化,由于hash表的长度不会变,所以还是有不均衡的问题。 79 | 80 | 对于随机的办法,由于虚拟节点变多了,随机之后会相对比较均衡,出现大的不均衡的几率会大大降低,但这种方法还是有缺点。 81 | * 不可控,适用于对于均衡性要求不是很高的场合, 82 | * 没有解决查找时需要O(logn)时间复杂度的问题。 83 | * 对于云存储来说,当节点发生变化时,虚拟节点所负责的范围可能会发生变化,在实现的过程中,如何只将变化的那部分移动到其它节点对某些架构来说是一个挑战。 84 | 85 | >到这里为止,就是我们常说的一致性hashing。 86 | 87 | ## 改进3 (解决分配不均衡问题和查找慢的问题) 88 | 还是添加虚拟节点,但这次虚拟节点跟物理节点的关系不固定,并且虚拟节点的数量固定。 89 | 90 | 由于虚拟节点的数量和hash表的长度都是固定的,所以每个虚拟节点负责的范围也是固定不变的,这样的一个好处是当虚拟节点对应的物理节点发生变化的时候,可以将整个虚拟节点的所有数据移动到新节点上去,在代码实现上来说会容易很多。 91 | 92 | 但这也引入了一个问题,虚拟节点的数量设置多少合适?这个数量跟hash表的长度不一样,hash表的长度只是一个虚拟的范围,不对应具体的实体,所以长一点没关系。 93 | * 虚拟节点太少 94 | * 会导致一个物理节点对应的虚拟节点太少,从而一个虚拟节点上存放的数据会很多,不利于增加/减少物理节点时数据的迁移,也影响磁盘坏掉之后的数据恢复速度,因为一个虚拟节点只能移动到一个物理节点上,一个物理节点拥有的虚拟节点越多,移动的时候并发性就越好, 95 | * 当物理节点增长时,可能会出现虚拟节点不够用的情况(某些物理节点没有对应的虚拟节点) 96 | * 虚拟节点设置的太多 97 | * 保存虚拟节点到物理节点的对应关系要消耗很多空间,不利于在网络共享 98 | * 在实际代码实现的时候,每个虚拟节点的维护都需要一定成本,虚拟节点越多,需要的系统资源会越多 99 | 100 | 由于虚拟节点是固定的,所以根据hash值能O(1)复杂度内找到虚拟节点,那么虚拟节点到物理节点的对应关系怎么管理呢?这个要看具体的实现,但大概思路是一样的。 101 | 1. 先算出每个物理节点平均应该拥有的虚拟节点个数,然后随机的或者按照顺序的给它们建立映射关系,比如有4个物理几点,100个虚拟节点,就可以简单的前25个虚拟节点给第一个物理节点,然后后面的以此类推,只要数据hash后是分散的,每个物理节点收到的请求量就是平均的 102 | 2. 当增加节点时,算出增加节点后每个物理节点平均拥有的虚拟节点个数,对大于这个数量的节点,让出对应数量的虚拟节点给新节点 103 | 3. 当减少节点时,算出减少节点后每个物理节点平均拥有的虚拟节点个数,对小于这个数量的节点,接收相应数量的虚拟节点 104 | 105 | 维护映射关系需要多少内存?假设我们有65536个虚拟节点,由于物理节点的数量不会超过这个数量,我们只需要一个存放两字节整数的数组就可以了,index就是虚拟节点ID,里面的数据就是物理节点ID,那么理论上需要65536*2大于12M内存。 106 | 107 | ## 如何映射多份copy 108 | 前面介绍的都是一个数据映射到一个物理节点,对于云存储来说,经常需要一个数据保存多份来保持数据的持久性,这时候一份数据就需要对应多个物理节点,这种又是怎么映射的呢? 109 | 110 | 最简单的办法是根据虚拟节点找后续的物理节点,假设只有一份copy时的映射表是a[1024],有一个数据A对应的虚拟节点是5,找到5对应的物理节点a[5]后,从a[6]继续往后找,直到找到两个跟a[5]不一样的物理节点。 111 | 112 | 采用这种办法要注意的一点是最开始时虚拟节点到物理节点的映射关系要尽量分散,太规律的话采用这种方法可能导致一个节点的所有虚拟节点的其它几份copy在相同的几个其它物理节点上,这样当一个物理节点挂掉后,另外几个节点的负载会突然增加很多。 113 | 114 | 当然实际使用的时候还涉及到节点的分类,比如分rack,分zone,一个虚拟节点的多份拷贝要尽量分散在不同的zone和rack上,这时候在定位到一个物理节点后,往后找的时候不仅要判断不是在同一个物理节点上,还要判断不在同一个zone和rack上。 115 | 116 | 增加或删除节点时怎么处理呢? 117 | 118 | ## 参考 119 | 120 | * [Consistent Hashing](http://courses.cse.tamu.edu/caverlee/csce438/readings/consistent-hashing.pdf) 121 | * [Building a Consistent Hashing Ring](https://docs.openstack.org/swift/latest/ring_background.html) 122 | -------------------------------------------------------------------------------- /cloud_storage/implementation_consideration.md: -------------------------------------------------------------------------------- 1 | ### 实现云存储软件时需要考虑的一些问题 2 | 3 | #### 数据存多少份比较合适 4 | 5 | 目前普遍的采用存储3份,但这样造成存储利用率低,有些软件提供Erasure Code功能,只要存储1.x份就可以达到相同级别的数据持久性,但缺点是性能慢,代码实现复杂。 6 | 7 | #### 根据文件名称或者ID怎么定位到文件存储位置 8 | 9 | Hash Ring或者直接用一个分布式的K/V系统管理元数据 10 | 11 | #### 操作的过程中出现错误怎么处理 12 | 13 | 比如上传,更新或者删除的过程中,由于网络问题,上传文件时写成功了两份,有一份失败,是返回客户失败还是成功呢,如果返回成功,剩下的那份没成功的咋办?如果返回失败,已经上传的那两份咋办? 14 | 15 | #### 如何保证数据均匀分布,避免某些node的压力过大 16 | 17 | 比如一个node的磁盘快满了,而刚刚新加的node的盘才用了10%,如果数据访问是平均的,那盘满的node就会压力大。 18 | 19 | #### 如何扩容动态扩容,扩容的过程中不影响使用 20 | 21 | Hash Ring一般采取的方法是逐步的加入新硬件,边加边移动数据 22 | 23 | #### 磁盘坏了如何能及时发现,如何更换一块新盘 24 | 25 | 坏一块硬盘就跑去换一块,成本较高,如何做到坏了很多块之后才需要去统一换一下。 比如坏了2块硬盘后,系统能检测到这两块硬盘上没有存储相同的文件,所以至少还有2份拷贝,相对来说还很安全。 26 | 27 | #### 如何发现磁盘里面有坏道,检测出来后如何处理 28 | 29 | 定时扫描所有的文件,判断最后访问时间,如果最后访问时间太久(可设置),就试着读取该文件,如果读取失败,立即恢复该文件。 30 | 31 | #### node挂掉后如何处理 32 | 33 | 比如node的硬件坏了,需要更换硬件,但更换硬件会需要几天,这段时间设备上的数据怎么处理?如果当成这个node的磁盘都坏了,进行恢复,一般一个node至少挂十几块硬盘,将这部分数据恢复到系统的其它地方,也要花很长的时间 34 | 35 | #### 如何升级硬件 36 | 37 | 比如几年前用的都是1T的硬盘,单位存储密度较低,现在想做下升级,升级到5T的硬盘,同时将node的cpu和内存也做下升级 38 | 39 | #### 如何升级软件 40 | 41 | 软件在不停的开发,改了很多bug,加入和很多新的功能,如何在不影响现有业务的情况下进行升级呢? 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /cloud_storage/introduction.md: -------------------------------------------------------------------------------- 1 | #### 云存储解决什么问题? 2 | 3 | * 安全,数据不丢失 4 | * 方便扩容,且无上限 5 | * 访问方式多样,在哪里都能访问 6 | * 速度快 7 | 8 | #### 公有云和私有云的特点? 9 | 10 | 站在用户的角度,公有云直接用即可,初始费用低,但后续费用高(按网络访问收费),私有云开始投入多,需要自建机房,但后续只需要维护费用,对于访问频繁的应用来说,总体来说私有云要便宜。 11 | 12 | 从产品的角度来说,公有云规模大,提供标准化的服务,内部细节对外不可见,有专业团队维护。私有云可定制性强(如存几份备份,是否加密压缩等),会暴露更多实现细节,并提供方便易用的管理维护工具,客户可以根据自己的需要进行定制化配置,并可以自己日常维护(遇到问题时需要厂商支持)。 13 | 14 | 由于私有云可以只运行在私有网络里,并且不和别人共享硬件,相对公有云来说从物理上要更安全。 15 | 16 | #### 云存储有哪些类型 17 | 18 | * object storage 19 | 20 | 提供key到value的访问方式, 如根据object id或者object name来访问云存储系统里面的数据。 如Openstack Swift, Amazon S3, Ceph. 21 | * file system 22 | 23 | 提供像文件系统一样的访问方式,如根据object id或者object name获取所有子目录信息。 如 Ceph. 24 | * block storage 25 | 26 | 目前看到的都和虚拟机的image相关,没有深入了解。 如 Openstack Cinder, Ceph. 27 | 28 | 29 | 这里提到的访问方式指的是逻辑上的访问方式,底层的访问实现可以是RESTful、NFS、webservice等。 30 | -------------------------------------------------------------------------------- /cloud_storage/public_vs_private.md: -------------------------------------------------------------------------------- 1 | # 公有云和私有云的区别 2 | 3 | 从技术上来说,两者没有太大区别,区别主要在使用方式上,私有云对外开放了之后就是公有云,公有云只允许有限范围内的访问就变成的私有云。 4 | 5 | 针对云存储系统,站在使用者的角度,主要的区别如下(目前就想到这么多) 6 | 7 | ### 公有云 8 | 9 | * 优点 10 | ``` 11 | 1. 按配置和访问量收费,单次费用便宜 12 | 2. 不用自己维护,不需要专业的团队 13 | 3. 上手快,前期成本低 14 | ``` 15 | 16 | * 缺点 17 | ``` 18 | 1. 由于按访问量收费,长期使用成本会比较高 19 | 2. 只能用现成的接口和标准,无法定制 20 | 3. 安全性较差,网络是公开的,并且数据也是放在大家都共享的设备上 21 | ``` 22 | 23 | ### 私有云的优点 24 | * 优点 25 | ``` 26 | 1. 后期使用成本低,只需要支付硬件和软件的维护费用 27 | 2. 可定制性强,丰俭由人,一般私有云提供商会提供很多针对不同场景的优化,包括性能和存储效率 28 | 3. 数据安全可控,可以做到物理隔绝 29 | ``` 30 | 31 | * 缺点 32 | ``` 33 | 1. 前期成本高,需要买设备建机房 34 | 2. 对后期维护要求高,需要选择靠谱的硬软件供应商 35 | ``` 36 | 37 | ### 总结 38 | * 如果对数据的安全要求很高,比如银行,那只能用私有云 39 | * 如果是小公司,业务负载不是很大,基于成本考虑,用公有云 40 | * 如果是大公司或者是有条件的公司,用私有云,可以定制更符合自己的需求,数据也更可控,如果是重度使用,长期总体成本也更低。 41 | -------------------------------------------------------------------------------- /cloud_storage/storage_types.md: -------------------------------------------------------------------------------- 1 | # 云存储的种类 2 | 3 | 常见的云存储有块存储(block storage),文件存储(file storage)和对象存储(object storage) 4 | 5 | ## 块存储 6 | 7 | * 访问方式: 基于块的随机读写 8 | * 接口: iSCSI 9 | * 主要应用场景:直接读写裸设备,如Databases,Virtual machines 10 | * 优点: 11 | - 基于块设备的读写,速度快 12 | 13 | ## 文件存储 14 | 15 | * 访问方式: 基于文件的随机读写 16 | * 接口: CIFS/SMB/NFS 17 | * 主要应用场景:需要存储大量文件,且需要对文件进行修改,如芯片设计,影视制作等 18 | * 优点: 19 | - 支持目录和文件操作 20 | - 接口协议兼容性好 21 | 22 | ## 对象存储 23 | 24 | * 访问方式: 随机读,顺序写且不支持更新 25 | * 接口: S3(基于http/https) 26 | * 主要应用场景:需要存储大量文件,但不需要对文件进行修改,如视频服务器,备份系统等 27 | * 优点: 28 | - 有单独的元数据管理,便于数据索引和分析 29 | - 无限扩容(没有文件存储那样的目录结构限制) 30 | - 读写速度快(不需要像文件存储那样要先访问目录) 31 | - 可以适当放宽性能要求,比如用Erasure Code取代3份copy,减少开支(文件存储需要支持随机写,做EC会严重影响性能,冷数据可以考虑用EC) 32 | - 灵活性高,对多site的支持更好 33 | -------------------------------------------------------------------------------- /cloud_storage/swift/architectural.md: -------------------------------------------------------------------------------- 1 | ### Swift 数据模型 2 | 3 | 图1 4 | 5 | 当使用Swift时,主要包含下面几个步骤 6 | 7 | 1. 创建一个新帐号(如何创建帐号跟采用的认证系统有关,详细信息见Tempauth或者Keystone) 8 | 2. 用新帐号登录,创建container (见RESTful API) 9 | 3. 然后就可以在对应的container下面创建、删除文件了 (见RESTful API) 10 | 11 | 对文件进行操作的时候,必须告诉Swift该文件属于哪个帐号的哪个container,不然它不知道到哪台服务器上操作该文件。 12 | 13 | ### Swift主要的Server 14 | 15 | 这里的Server不是指具体的服务器机器,而是我们常说的进程,一个node(指具体的一台服务器机器)上可以放多个Server 16 | 17 | 图2 18 | * Proxy Server 19 | * 存储系统的入口,所有的client都和它打交道 20 | * session管理,帐号认证管理(可选,如Tempauth) 21 | * 分发客户的请求到各种服务器,比如将创建container的请求转发到container Server, 将创建对象的服务转发到Object Server 22 | * 上传和读取文件时,所有的流量都要经过它,但它不缓存任何文件 23 | * 由于一般情况下Proxy Server会单独的部署在一个node上,所以也可以将它看成一个node 24 | 25 | * Object Server 26 | 27 | 负责接收Proxy server转发过来的文件操作请求,然后将文件存储在本地相应的磁盘上,一般情况下一个node有很多块磁盘,但只有一个Object Server 28 | 29 | * Container Server 30 | 31 | 负责接收Proxy server转发过来的Container操作请求,比如创建、删除Container, 或者往它里面添加、删除Object,根据请求的内容更新对应Container DB里面的内容,一般一个node上只有一个Container Server 32 | 33 | * Account Server 34 | 35 | 负责接收Proxy server转发过来的Account操作请求(比如添加或者删除帐号,或者往它里面添加、删除Container), 根据请求的内容更新对应Account DB里面的内容,一般一个node上只有一个Account Server 36 | 37 | 注意:Account Server不负责帐号的认证。 只负责管理Account里面的Container。 38 | 39 | * Storage Server 40 | 41 | 它是个逻辑概念,不存在具体的这么一个进程,是Object Server, Container Server, Account Server的统称,由于一般情况下这三个Server都放在同一个node上,所以也可以将Storage Server理解成一个存放这三个Server的node 42 | 43 | ### 各个Server之间的关系 44 | 45 | * 可以将所有的Server都放在同一台机器上, 也可以分开放,他们之间在物理上没有必然的联系。 46 | * 但通常情况下, 一般将Proxy Server放在一个单独的node, 将所有的Storage Server放到另一个node(因为Container Server, Account Server占用的空间较小,放在单独的机器上浪费空间。) 47 | 48 | ### 其它的Server 49 | 50 | * Replication 51 | 52 | * 遍历本地的partition,找到本地partition对应的其他server(比如系统设置的是3份备份,则本地的每个partition都能找到其它的两个node,他们上面也放有相同的partition),然后和其他node上的partition比较,如果自己比对方新,则将自己新的文件同步给对方。(当自己的文件比对方旧时,什么都不做,因为他不知道要拉对方的什么数据过来) 53 | * 删除掉标记为删除的object, container, account 54 | 55 | * Updaters 56 | 57 | * swift-object-updater: 当有object创建的时候,需要更新相应的container,让新建的object包含在对应的container里面,这里的更新操作有时可能失败,比如网络问题或者Container Server很忙,这个时候系统会将更新操作记录在磁盘上,等待Updater Server后续继续更新Account Server。(同样适用于删除object) 58 | * swift-container-updater: 同swift-object-updater类似 59 | * swift-account-updater: 没有,因为增加和删除account的时候,不需要更新任何东西 60 | 61 | 62 | * Auditors 63 | 64 | swift-account-auditor,swift-container-auditor,swift-object-auditor: 定期检查本地节点上相应数据,如果发现数据有损坏(如磁盘出现坏道,人为修改文件等),则将有问题的数据隔离,等待Replication Server同步正常的数据过来 65 | -------------------------------------------------------------------------------- /cloud_storage/swift/create_object.md: -------------------------------------------------------------------------------- 1 | ### Swift 数据模型 2 | 3 | 图1 4 | 5 | 当第一次使用Swift时,主要包含下面几个步骤 6 | 7 | 1. 创建一个新帐号(如何创建帐号跟采用的认证系统有关,详细信息见Tempauth或者Keystone) 8 | 2. 用新帐号登录,创建container 9 | 3. 然后就可以在对应的container下面创建文件了 10 | 11 | 当从服务器上取一个文件的时候,必须告诉该文件属于哪个帐号的哪个container,不然服务器不知道到哪里拿这个文件。 12 | 13 | ### Swift主要的server 14 | 15 | * Proxy Server 16 | * session管理,帐号管理认证(Tempauth) 17 | * 分发客户的请求到各种服务器,比如将创建container的请求转发到container Server, 将创建对象的服务转发到Object Server 18 | 19 | * Object Server 20 | 21 | 存储具体的对象,也就是我们常说的文件 22 | 23 | * Container Server 24 | 25 | 存储object 到 container的对应关系,即每个container里面都有哪些object 26 | 27 | * Account Server 28 | 29 | 存储container 到 account的对应关系,即每个account里面都有哪些container 30 | 31 | * storage Server 32 | 33 | Object Server, Container Server, Account Server的统称 34 | 35 | ### 各个server之间的关系 36 | 可以将所有的服务器都放在同一台机器上, 也可以分开放,他们之间在物理上没有必然的联系。 37 | 但通常情况下, 一般将proxy server放在一个单独的节点, 将所有的storage server放到另一个节点(因为Container Server, Account Server占用的空间较小,放在单独的机器上浪费空间。), 于是一般情况下的网络拓扑图如下: 38 | 39 | 图2 40 | 41 | 这里的device可以理解为一块磁盘或者一个挂载点,所以一般情况下一台机器有多个磁盘或者挂载点, 就有多少个device。 42 | 43 | ### 登录服务器 44 | 45 | * 帐号认证 46 | 47 | curl -i https://storage.clouddrive.com/v1/auth -H "X-Auth-User: jdoe" -H "X-Auth-Key: jdoepassword" 48 | * https://storage.clouddrive.com是proxy server所在机器的URL 49 | * jdoe是帐号 50 | * jdoepassword是密码 51 | 52 | * 返回 53 | ``` 54 | HTTP/1.1 204 No Content 55 | Date: Mon, 12 Nov 2010 15:32:21 56 | Server: Apache 57 | X-Storage-Url: $publicURL 58 | X-Auth-Token: $token 59 | Content-Length: 0 60 | Content-Type: text/plain; charset=UTF-8 61 | ``` 62 | 63 | * $publicURL: 后续所有操作要访问的URL。 64 | 既然前面登陆的时候已经输入了proxy server的URL,这里为什么还要返回一个URL呢,直接用前面的URL不就可以了? 这个主要用来做负载均衡, 一般情况下一个系统有多台proxy server, 登陆的时候系统会返回一台负载相对较小的proxy server回来。如果系统只有一台proxy server,那这里返回的URL应该和你登录的时候指定的一样,比如都是https://storage.clouddrive.com。 Swift 根据什么样的策略做负载均衡? 待研究。 65 | * $token: 相当于session id, 服务器用这个ID来检查当前会话是否已认证过 66 | 67 | ### 上传一个文件 68 | 69 | curl –X PUT -i -H "X-Auth-Token: $token" -T ./file1 $publicURL/container/file1 70 | 71 | * 首先proxy server收到文件上传请求,在请求的URL中含有account、container和object名称 72 | * proxy server会根据account/containner/file1计算出一个4字节的hash code 73 | * 取hash code的后n位(n<=32),在object ring里面找对应的partition和devices(一个device包含多个partition) 74 | 75 | 比如在创建ring的时候指定的replica为3份, 那么这里会得到3个devices。 如果整个系统的devices数量不够3个,比如说只有2个, 那么这里只返回2个devices,也就是说只存2份replica。(在同台机器的同一挂载点放两份数据意义不大) 76 | 77 | * 根据account/containner计算container的hash code,并根据hash code的后m位(m<=32),在container ring里面找对应的partition和devices(container的partition和object的partition是相互独立的,没有什么关系) 78 | * 依次跟每个device所在的object server建立http连接,URL里面包含/object device/object partition/account/containner/file1, HTTP HEADER信息里面包含container所在的partition和device(一个连接里面包含container的一个device,这样不同的object server会更新不同container server上的数据)。 79 | 80 | 如果创建container ring的时候指定的replica数目是4, 创建object ring的时候指定的replica数目是3, 那么这里container的devices数目和object的devices数目不一样,会不会导致只更新了3个container replica的信息,还有一个没更新到? 有待考证 81 | 82 | * 依次接收客户端的数据,并转发给每个device上的object server。(如果两个device在一台机器上,会往同一台机器上传两份,这种情况只出现在系统里面只有两台object server的情况下) 83 | * object server收到请求后,会将文件保存在指定device上的partition对应的目录下 84 | * object server根据http HEAD里的信息,连接container所在device上的container server, 并请求更新container, HTTP请求中包含container的partition和device信息。 85 | 86 | 这里的请求如果失败,会纪录下来,由updater进程后续去更新。 87 | 88 | * container server收到请求后,会更新指定device上的partition对应的目录下的container信息,使其包含这个新建的object 89 | * proxy server等待所有的object server完成相应的工作,如果发现有大于(n//2)+1个replica写入成功(这里的n是总的待写入的replica的数目),就会返回成功。(如果有1个失败了咋办? 见后续介绍) 90 | 91 | ### 创建container 92 | 创建contianer和上传文件的原理是一样的,也是根据hash code在container ring里面找对应的partition和devices, 然后跟每个device上的container server建立http连接并转发相关的信息。 93 | 94 | ### 删除object 95 | 删除object和创建object的流程是一样的,也是根据object名称找到partition和不同的devices,然后依次删掉它们,和创建不同的地方是不需要传输文件。 96 | 97 | ### 总结 98 | 从上面可以看出,只要弄懂了ring,以及partition、device的概念,其他的地方都很好理解。 99 | -------------------------------------------------------------------------------- /cloud_storage/swift/disadvantages.md: -------------------------------------------------------------------------------- 1 | 2 | ### Swift 的缺点 3 | 4 | * 没有机制自动控制数据均匀分布 5 | 6 | 如果数据足够多的话,理论上hash(account/container/object)和文件大小应该是均匀分布的,但如果出现偏差怎么办,比如刚好一个node上放了很多上G的文件,把空间占满了,虽然其他的node还有大量的空间,但也需要对这个node做手工调整,不然下次hash值再落到这个node上的文件将不会被存储。 7 | 8 | * 扩容不太方便 9 | 10 | 扩容需要移动数据,比如原来是5个节点,现在要再增加5个节点,那么需要移动一半的数据,当容量非常大时,持续的时间很长,并且对整个系统会造成很大的压力。 11 | 12 | * Region功能较弱 13 | 14 | 比如有两个Region,我希望存储的数据为当前Region存两份,另一个Region存一份。 访问不同的Region会从对应的Region拿数据 15 | 16 | * 不支持Erasure Code 17 | 18 | 在对性能要求不高的情况下,EC能大大的提高存储效率 19 | 20 | * 不支持策略配置 21 | 22 | Swift不支持在Account或者Container上设置存储策略。 一个云存储系统可能用于多种用途,比如在一个公司内部,有经常访问的数据和不经常访问的备份数据,他们不希望为了这两个不同的需求搭建两套存储系统。 23 | -------------------------------------------------------------------------------- /cloud_storage/swift/ring.md: -------------------------------------------------------------------------------- 1 | ### ring的作用 2 | 3 | swift中有3个ring,他们分别是account ring, container ring和object ring, 他们的数据结构和计算方式完全一样,但他们之间是相互独立的, 没有任何关系。 4 | 5 | * account ring 6 | 根据account name可以查找到对应的account信息存放在哪个device 7 | * container ring 8 | 根据container name可以查找到对应的container信息存放在哪个device 9 | * object ring 10 | 根据object name可以查找到对应的ring信息存放在哪个device 11 | 12 | ### ring的结构 13 | 14 | 官方网站和其他很多文章都都从ring的角度介绍swift的ring, 在我个人来看, ring更像一个table,这里会以一个table的角度来介绍ring(个人觉得更直观)。 15 | 16 | 这里有几个概念需要明确一下 17 | 18 | * replica 19 | 就是文件备份,有几个replica就表示对于每个文件,系统中会存几分 20 | * 节点 21 | 就是我们常说的一台服务器,他上面会挂很多硬盘 22 | * device 23 | 可以理解为实际的硬盘或者虚拟的硬盘,一个节点上可以有很多device 24 | * partition 25 | 是一个逻辑的抽象概念,没有具体的实物与之对应,可以理解为文件夹,相当于在系统里面创建了很多很多个文件夹,然后将他们均匀的放到不同的device上。 26 | 27 | 上传一个文件的流程(假设replica是3): 28 | * proxy服务器收到上传请求,然后根据/account/container/object这样一串名字计算hash码。 29 | * 取hash码的后n位,得到的数字就是partition的id, 一个partition会放在三个不同的device上,以防某一份丢失后导致数据丢失。 30 | * 根据partition id从ring里面找到对应的3个device。 31 | * 依次将文件上传到各个device所在的节点上去 32 | * 节点上的object server收到请求后,就会将文件存在相应device的相应partition中,可以理解为放到相应磁盘的相应目录下去。(具体存储的目录结构和格式会在后续介绍到) 33 | 34 | ### ring的生成 35 | 36 | ring的生成和调整由命令swift-ring-builder完成, 关于swift-ring-builder的具体用法见相关的文档。 37 | 38 | 我们常用的场景主要是添加或者删除device,然后rebalance整个ring。 运行命令前,请确保先备份原来的ring文件。 运行完之后需要手动将相应的ring文件拷贝到其他的所有机器上。 39 | 40 | 注意: swift-ring-builder只是用来操纵ring文件, ring文件发生变化后, 相应object的存放位置变化由其他的机制来触发(后续会介绍到), 这里的rebalance只是重新调整ring文件,不对系统中存储的内容做任何改动。 41 | 42 | swift-ring-builder object.builder create 20 3 1 43 | swift-ring-builder object.builder add r1z1-127.0.0.1:6010/sdb1 1 44 | swift-ring-builder object.builder add r1z1-127.0.0.1:6010/sdb2 1 45 | swift-ring-builder object.builder add r1z2-127.0.0.2:6010/sdb1 2 46 | swift-ring-builder object.builder add r1z3-127.0.0.2:6010/sdb2 2 47 | swift-ring-builder object.builder add r1z2-127.0.0.3:6010/sdb1 1 48 | swift-ring-builder object.builder add r1z3-127.0.0.3:6010/sdb2 1 49 | swift-ring-builder object.builder rebalance 50 | swift-ring-builder container.builder create 16 3 1 51 | swift-ring-builder container.builder add r1z1-127.0.0.1:6011/sdb1 1 52 | swift-ring-builder container.builder add r1z1-127.0.0.1:6011/sdb2 1 53 | swift-ring-builder container.builder add r1z2-127.0.0.2:6011/sdb1 2 54 | swift-ring-builder container.builder add r1z3-127.0.0.2:6011/sdb2 2 55 | swift-ring-builder container.builder rebalance 56 | swift-ring-builder account.builder create 8 3 1 57 | swift-ring-builder account.builder add r1z1-127.0.0.1:6011/sdb1 1 58 | swift-ring-builder account.builder add r1z1-127.0.0.1:6011/sdb2 1 59 | swift-ring-builder account.builder add r1z2-127.0.0.2:6011/sdb1 2 60 | swift-ring-builder account.builder add r1z3-127.0.0.2:6011/sdb2 2 61 | swift-ring-builder account.builder rebalance 62 | 63 | 上面命令的参数意义请参考swift-ring-builder的使用文档。这里有几点需要注意: 64 | 65 | * 权重主要跟存储容量有关 66 | 67 | 比如1T的硬盘设置权重为1, 2T的硬盘设置权重为2,这样swift就会往2T的硬盘中存两倍于1T硬盘的数据, 如果1T和2T的硬盘都设置为1,造成的后果就是1T的盘存满了,2T的盘才用了一半。 68 | 69 | * account,container和object是不同的ring,他们生成的文件也是单独分开的 70 | * 往ring里面加的是device, 不是整个节点, 也就是说需要一块硬盘一块硬盘的往ring里面加, 没法一次把一个节点上的所有磁盘加进去。 71 | * 同一节点上的device用同样的ip和端口(如果使用不用的端口会怎么样?想不出这么做有什么好处,待研究), 不同节点的device可以用不同的端口 72 | * 每个ring的part_power可以不一样, 但从数据均匀分布的角度来看,最好设置一样。 73 | 74 | * 比如现在有20个device,每个object存3份, 将part_power设置成8,就有2^8*3=192个partition,意味着这个系统支持的最大device数为192, 如果以后device数量增长到200,就会有至少8个device上不会有数据,处于闲置状态,所以设置part_power的时候不仅要考虑现有的device数量,还要考虑以后可能扩展到的device数量。 75 | * 如果account ring的part_power为2,每个account存3份, 则account的partition数目为2^2*3=12, 则account的信息最多会放到12个device上,当系统的device大于12的时候,除这12个device外,其他的device上就不会存放account信息,于是这12个device上的数据就会比其他device的多, 如果account信息很多,则会造成数据在各个device上分布的不均匀。 76 | 77 | * 每个ring的replica数目理论上可以不一样(没有仔细研究),比如account和container的replica数量可以设置的多点,丢失的可能性更低。 78 | 79 | ### ring生成的算法 80 | 81 | ### rebalance的算法 82 | 83 | -------------------------------------------------------------------------------- /cloud_storage/swift/summary.md: -------------------------------------------------------------------------------- 1 | #### 学习swift的一些感悟 2 | * 以前写代码都是单台机器上运行,不是多线程就是多进程,就算有多台机器通讯,也是很简单的点对点通信或者类似代理服务器那样的数据转发,从来没接触过分布式系统,所以对分布式系统感到很神秘,这也是刚开始学习swift觉得难的地方,很多概念搞不清楚。 3 | * 学习新东西,特别是以前没接触过的东西,刚上手时觉得各种不适应,各种不理解,但随着了解的的深入,发现原来也就那么回事,也没有什么特别高深的东西,只是以前从来没想到过,当到这一步的时候,会发现自己的想问题的思路比以前开阔多了。 4 | * 静下心来,尝试学一些跟以前做的东西思路很不一样的东西,试着去接受新的思维,新的习惯,这种感觉还是挺爽的。 5 | 6 | #### 博客内容 7 | 这个系列的博客主要是介绍swift的一些设计思想,不包含怎么安装和使用swift,也不包括源码的剖析。 8 | 9 | 通过博客,希望能把下面的疑问解释清楚: 10 | 11 | * 为什么网上都说数据要存3份才能达到很高的持久性,有没有办法不需要存3份,比如2.5份,就能达到期望? 12 | * 可用性(Availability 99.99%)和持久性(Durability 99.999999999%)是咋算出来的? http://en.wikipedia.org/wiki/High_availability 13 | * 上传一个文件的流程是怎么样的,系统是根据什么样的调度算法决定文件放到哪个服务器上? 14 | * 更新或者删除一个文件的流程是怎么样的,系统是根据什么样的调度算法知道文件放在了哪个服务器? 15 | * 如何做到存储均匀分布,避免出现一台服务器的磁盘使用率都90%了,其他的才10%? 16 | * 如果需要扩容,怎么加硬盘或者加设备,流程是怎么样的,是怎么做到不影响正常使用的? 17 | * 整个磁盘坏了怎么处理?怎么做到不影响正常使用的? 18 | * 磁盘里的某个扇区坏了怎么处理?怎么判断哪些文件需要恢复? 19 | * 整个机器坏了怎么办(这里的机器指存储服务器,一般一台这样的机器有几十块硬盘)?怎么做到不影响正常使用的? 20 | * 上传,更新或者删除的过程中,出现错误怎么办,比如由于网络问题,上传文件时成功了一份,其他两份失败,是返回客户失败还是成功呢,如果返回成功,剩下的两份没成功的咋办?如果返回失败,已经上传的那一份咋办?怎么保证事务? 21 | * proxy、account、container、object服务程序都跑在哪里的?他们是什么样的关系? 22 | * account、container、object是以什么样的格式存储的,在哪里可以看到他们里面的数据? 23 | * rsync在swift中起了什么作用,在哪里会用到? 24 | * ring是个什么东东,ring是怎么生成和rebalance的? 哪些进程或者工具会用到ring? 25 | * 如何升级软件系统? 如何升级硬件系统? 26 | -------------------------------------------------------------------------------- /cloud_storage/thinking_about_cloud_storage.md: -------------------------------------------------------------------------------- 1 | #### 云存储解决什么问题? 2 | 先看一下传统的存储,比如我买了一台电脑,里面有一块硬盘,固定容量1T 3 | * 如果我要存储的东西超过了1T,就只能换个更大的硬盘,换硬盘的过程持续时间长,并且换盘的过程中没办法再用电脑 4 | * 万一硬盘坏了怎么办?数据就完蛋了 5 | * 为了随时能访问硬盘中的文件,到哪里我都得抱着电脑 6 | 7 | 对于公司来说,数据量更大,单机存储容量远远不能满足需求,对上面几个问题更敏感。 8 | 9 | 云存储正是为了解决上述问题而诞生的,他首先要解决的问题就是: 10 | * 数据不能丢失 11 | * 容量扩展很方便,且无上限 12 | * 访问方式多样,在哪里都能访问到里面的数据 13 | * 速度要快 14 | 15 | #### 谁需要云存储? 16 | * 普通用户: 拍了好多照片,都很珍贵,为了避免丢失,正在考虑在多台电脑上都保留一份,但是, 17 | * 家里只有一台电脑怎么办?难道为了照片的事情再去买台电脑或者移动硬盘吗? 18 | * 每次有新照片后都要拷贝到多个地方,麻烦,还容易漏拷。 19 | * 家里来贼了或者火灾了怎么办? 20 | * 数据越来越多,一块硬盘放不下了怎么办? 21 | 22 | 如果有一个访问速度快,容量大,价格便宜的靠谱的网盘就好了。 23 | 24 | * 互联网公司(Apple,豌豆荚,优酷等): 需要存储很多客户的数据,如照片、视频啥的,并且客户需要随时随地能访问到这些数据。为了解决这些问题,需要: 25 | * 买设备,建机房或者托管 26 | * 为了满足不同网络的用户需求,机房需要支持不同的运营商(电信、联通等)网络 27 | * 要专门的人管这些设备,出问题了要及时的处理 28 | * 要保证性能能跟得上 29 | * 业务在不停的增长,需要的容量也在不断的上升,原来规划的容量不够用了,需要不停的扩展 30 | * 要保证设备或者磁盘挂了不影响正常的业务 31 | * 要保证设备或者磁盘挂了不造成数据丢失 32 | * 其他很多琐碎的事情 33 | 34 | 可问题是: 35 | * 对于小公司,根本没资源和能力做这些事情。 36 | * 对于中型公司,有自己的开发团队,但要达到这样的目标也不容易,完成这部分工作有时比完成自己的主要业务还难。 37 | * 对于大型公司,有自己强大的开发团队,但存储不是公司的主营业务,也不想在这方面耗费太多的资源。 38 | 39 | 如果有一个能在公网上提供存储服务的系统就好了,只要价格在能接受的范围内。 40 | 41 | * 传统公司(银行、交通部门、医院等): 需要存储很多私有数据,这些数据只是在公司内部能访问到。为了解决这些问题,需要: 42 | * 建机房,买设备 43 | * 要专门的人管这些设备,出问题了要及时的处理 44 | * 要保证性能能跟得上需求 45 | * 要解决单机容量有限的问题,做到动态扩展 46 | * 要保证设备或者磁盘挂了不影响正常的业务 47 | * 要保证设备或者磁盘挂了不造成数据丢失 48 | 49 | 这类公司没有专业的开发和维护团队,根本不可能完成这项工作,他们需要的是私有云提供商帮他们搞定这些事情,他们负责机房相关工作就可以了。 50 | 51 | #### 谁提供云存储? 52 | 53 | * 公有云提供商(七牛, Amazon, 阿里云等), 他们自己建(租)机房,铺(租)线路,自己开发云存储软件,买存储设备,搭建自己的云存储系统。然后提供统一的接口给开发人员使用。 54 | * 公有云提供商(电信公司),他们的优势是有自己的机房和线路,缺点是没有软件研发能力,也没有软件系统维护能力。 于是他们使用第三方的软件(私有云提供商),并委托第三方的团队来组建和维护云存储系统。 55 | * 私用云提供商(华为,EMC等),有云存储软件,也有存储硬件,主要卖给需要搭建云存储的厂商(比如银行,电信公司等)。 56 | 57 | #### 什么样的云存储才是好的云存储? 58 | * 普通使用者的角度(大众, 通常是间接使用): 59 | * 一天24小时可用 60 | * 速度快 61 | * 数据安全可靠(不会被第三者看到,不会丢失) 62 | * 客户端易用 63 | * 价格便宜 64 | * 软件开发者的角度(Dropbox, 豌豆荚等): 软件开发者一般是为了满足自己或者客户的需求 65 | * 功能丰富 (如支持版本系统,搭建静态网站,支持CDN加速等) 66 | * API易用 67 | * 访问方式多样(RESTful, NFS, CIFS等) 68 | * 稳定,安全可靠,速度快,便宜 69 | * 云存储系统搭建和维护者的角度(公有云或者私有云): 70 | * 硬件要稳定,磁盘损坏率不要太高(太高意味着更高的维护成本和更高的数据丢失可能性) 71 | * 软件要稳定,不要动不动系统不可用。 72 | * 易用。 73 | * 要有系统宏观状态数据,便于根据系统情况进行调整(如根据容量使用趋势及时扩容, 根据线路拥堵情况调整线路) 74 | * 调整方便(如扩容,替换设备) 75 | * 丰富的告警系统,对系统的任何异常及时报出来 76 | * 磁盘坏掉后要能有很好的办法进行处理(如自动检索系统,把坏盘中的数据再复制一份放到系统中; 或者使用一块新盘替换掉原来的坏盘,如果坏一块盘就去一次机房,成本很高,最好能做到在没有风险的情况下尽可能的少去机房,也就是可能等坏了几块盘之后再去机房统一换一次) 77 | * 设备坏掉后要能很方便的恢复。 78 | * 节约成本。 79 | * 是否不需要存储3份备份,就能达到很高的耐久性(99.999999999%),若能节省1/3的空间,对于云存储系统来说,是非常可观的。 80 | * 升级方便,不会影响正常使用 (软件系统升级,硬件系统升级) 81 | 82 | 上面前两类人不关心云存储的内部实现。 而后一种非常关注内部实现原理。 83 | 84 | #### 公有云和私有云各自的特点? 85 | * 公有云: 86 | * 规模大。 87 | * 专业团队维护,内部细节对外不可见,只要专业人员能hold住,很多问题都不是问题,对外好忽悠。 88 | * 客户数据特点多样化,不好做针对性的优化 89 | * 网络环境差,需要做很多这方面的优化。 90 | * 私有云: 91 | * 高度可订制 92 | * 有些客户的数据比较大,但又需要较短的响应速度; 93 | * 有些用户的数据不经常访问,需要压缩存储。 94 | * 有些用户需要像访问文件系统一样访问存储系统 95 | * 有些用户觉得有些数据不是很重要,保存两份就够了,有些数据很重要,需要保存三份。 96 | * 客户维护能力弱,对软件的质量要求高 (假如卖给了100个用户,如果系统做的不好,客户天天找,相当于要维护100个云存储系统) 97 | * 受网络影响相对较小,于是对访问速度有更高要求。(用百度网盘10M的下载速度觉得很快了,但如果企业内部的私有云只有10M的速度,肯定要骂人了) 98 | 99 | 100 | #### 云存储有哪些类型 101 | * object storage 102 | 103 | 提供key到value的访问方式, 如根据object id或者object name来访问云存储系统里面的数据。 如Openstack Swift, Amazon S3, Ceph. 104 | * file system 105 | 106 | 提供像文件系统一样的访问方式,如根据object id或者object name获取所有子目录信息。 如 Ceph. 107 | * block storage 108 | 109 | 目前看到的都和虚拟机的image相关,没有深入了解。 如 Openstack Cinder, Ceph. 110 | 111 | 112 | 这里提到的访问方式指的是逻辑上的访问方式,底层的访问实现可以是RESTful、NFS、webservice等。 113 | 114 | 115 | #### 网盘和常说的云存储有什么关系? 116 | 117 | * 云存储面向开发人员,网盘面向最终用户。 118 | * 云存储提供基本的存储服务,网盘是基于云存储开发的提供给最终用户的软件产品。 比如Dropbox使用的就是Amazon的S3, 百度网盘使用自家的云存储服务。 119 | * 网盘的服务质量跟底层的云存储息息相关,如果底层的云存储不稳定,经常丢失数据, 网盘也就不靠谱。 120 | 121 | #### 云存储和常说的软件定义存储(SDS)有什么关系? 122 | 软件定义存储目前还没有统一的定义,大体思路是: 凡是能由软件来控制的地方,尽可能交由软件控制。 123 | 软件定义存储要比现有的常见云存储系统要更灵活,更加高度可订制。 124 | 125 | 以上就是我对云存储的一些看法,随着对云存储理解的深入,我会不时的更新该文档。 126 | -------------------------------------------------------------------------------- /container/002_what_is_container.md: -------------------------------------------------------------------------------- 1 | # 容器概述 2 | 3 | 在这里尝试简单回答一些常见的关于容器的疑问,仅供参考。 4 | 5 | ## 容器是什么 6 | 简单点说,容器就是一个或多个进程以及他们所能访问的资源的集合。 7 | 8 | 容器技术的本质是对计算机系统资源的隔离和控制,让原来全局的资源变得只能部分进程之间共享,这跟我们常说的虚拟机这种虚拟化技术没有关系,最新的标准在制定过程中,包括镜像的格式,容器运行时的一些规范,具体见[Open Container Initiative(OCI)](https://www.opencontainers.org/)。 9 | 10 | ## 容器和虚拟机的差别 11 | 从技术角度来看,他们是不同的两种技术,没有任何关系,但由于他们的应用场景有重叠的地方,所以人们经常比较他们两个 12 | 13 | * 容器目前只能在Linux上运行,容器里面只能跑Linux 14 | * 虚拟机可以在所有主流平台上运行,比如Windows,Linux,Mac等,并且能模拟不同的系统平台,如在Windows下安装Linux的虚拟机 15 | 16 | * 容器是Linux下一组进程以及他们所能访问资源的集合,所有容器共享一个内核,要比虚拟机轻量级,占用系统资源少,并且容器比虚拟机要快,包括启动速度,生成快照速度等 17 | * 虚拟机是一整套的虚拟环境,包括BIOS, 虚拟网卡, 磁盘, CPU,以及操作系统等, 启动慢,占用硬件资源多. 18 | 19 | * 由于虚拟机的和主机只是共享硬件资源,隔离程度要比容器高,所以相对来说虚拟机更安全 20 | 21 | ## 什么时候应该用虚拟机,什么时候应该用容器 22 | 打个比方,A准备在网上卖东西,是自己搭建一套电商平台,还是直接在淘宝上开个小店呢?这就得看A的需求,自己搭建平台当然好,完全自己控制,想有什么功能都可以,但缺点是成本太高,淘宝上开店成本小,但要受淘宝的很多限制。想想如果淘宝的店子就能满足需求,为什么还费那么大劲去自己搭建平台呢? 23 | 24 | 虚拟机和容器的关系也差不多,虚拟机当然好,完全控制在自己手中的一套系统,没有条条框框的限制,缺点就是启动慢耗资源;容器启动快耗资源少,但受到的限制比较多(后面介绍docker的时候会提到这些限制)。所以一般的原则是如果容器能满足需求,就用容器,如果容器满足不了就用虚拟机。如何判断容器是否满足需求呢?大致原则是如果应用对硬件和内核没有特殊需求,一般都能使用容器,否则需要咨询专家。 25 | 26 | 当然这里只是从功能和资源消耗的角度来考虑,实际情况要比这个复杂的多,包括管理是否方便,相关的技术是否成熟等等。 27 | 28 | ## docker和容器的关系 29 | docker是容器管理技术的一种实现,用来管理容器,就像VMware是虚拟机的一种实现一样,除了docker,还有[LXC/LXD](https://linuxcontainers.org/),[Rocket](https://coreos.com/rkt/),[systemd-nspawn](https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html),只是docker做的最好,所以我们一说容器,就想到了docker。 30 | 31 | ## 为什么容器只出现在Linux里面 32 | 因为Linux中有资源隔离和管理的机制(Namespace,CGroups),有COW(copy on write)文件系统等容器所需要的基础技术。当然其他平台也有类似的东西,但功能都没有Linux下的完善,不过随着容器技术越来越流行,其他的系统平台也在慢慢的实现和完善类似的这些技术。 33 | 34 | ## 为什么容器里面只能运行Linux 35 | 因为Linux下的所有容器共享一个Linux内核,所以容器里面只能跑Linux系统 36 | 37 | ## 不同的Linux发行版为什么可以共享内核 38 | 先看一下Linux下启动一个进程的大概过程,当在Shell里面启动一个程序的时候,会发生如下过程: 39 | 40 | 1. Shell运行系统调用,通知内核启动一个指定位置上的程序 41 | 42 | 2. 内核加载指定位置上的可执行文件以及它所依赖的动态库 43 | 44 | 3. 初始化进程的地址空间,并把可执行文件映射到相应的内存地址(如果文件比较大的话,不会把整个可执行程序一次性加载到内存中,只是映射过去,然后利用内存缺页中断在需要的时候将所需的内容加载到内存中去) 45 | 46 | 4. 开始调度进程(简单点说就是内核会让这个进程时不时的运行一会儿) 47 | 48 | 从上面的过程可以看出,内核唯一需要和应用层达成一致的是可执行程序的文件格式,不然内核就没法加载程序并调度进程。 49 | 50 | 当进程开始运行后,进程和内核的交互就是系统调用,当进程需要访问由内核管理的资源时,采用软件中断的方式和内核交互,每个系统调用都有一个中断号,并且这个号不会随着内核版本变化而变化。 51 | 52 | >关于内核所支持的系统调用请参考[这里](http://man7.org/linux/man-pages/man2/syscalls.2.html),系统调用因为涉及到用户态到内核态的切换,所以非常耗时,但Linux里面有一项叫做[VDSO](http://man7.org/linux/man-pages/man7/vdso.7.html)的技术,可以将内核态的一些地址直接映射到所有进程的用户态空间,于是系统调用就跟访问普通的变量一样了,比如我们常用的gettimeofday函数,就不需要切换到内核态,直接读取内核映射到进程内存空间的内容就可以了,非常快。 53 | 54 | 从上面可以看出,Linux内核和应用层进程之间的关系是松耦合的,只要保证两个条件: 55 | 56 | * 内核能识别应用层程序的格式 57 | * 应用层需要的系统调用内核能支持 58 | 59 | 由于Linux下可执行文件和动态库的格式以及系统调用的接口都比较稳定,所以不同的Linux发行版在大部分情况下都可以共享同一个内核。一般来说,新的内核兼容老的Linux发行版,但太老的内核不一定支持新的Linux发行版。如果你的应用对内核有特殊需求,那么应该考虑一下用容器是否是一个明智的选择。 60 | 61 | ## 容器启动为什么那么快? 62 | 容器的本质是一个或多个进程以及他们所能访问的资源的集合。启动一个容器的步骤大概就是: 63 | 64 | 1. 配置好相关资源,如内存、磁盘、网络等 65 | 配置资源就是往系统中添加一些配置,非常快 66 | 67 | 2. 初始化容器所用到的文件目录结构 68 | 由于Linux下有COW(copy on write)的文件系统,如Btrfs、aufs,所以可以很快的根据镜像生成容器的文件系统目录结构。 69 | 70 | 3. 启动进程 71 | 和启动一个普通的进程没有区别,对Linux内核来说,所有的应用层进程都是一样的 72 | 73 | 从上面可以看出启动容器的过程中没有耗时的操作,这也是为什么容器能在毫秒级别启动起来的原因 74 | 75 | ## 启动容器会占用很多资源导致系统变慢吗 76 | 由于Namespace和CGroups已经是Linux内核的一部分了,所以应用层运行的进程一定会属于某个Namespace和CGroups(如果没有指定,就属于默认的Namespace和CGroups),也就是说,就算我们不用Docker,所有的进程都已经运行在默认容器中了。对内核来说,默认容器中运行的进程和Docker创建的容器中运行的进程没有什么区别,就是他们所属的容器号不一样。 77 | 78 | 所以说创建新容器会不会影响主机性能完全取决于容器里面运行什么东西。如果运行的是耗资源的进程,那么肯定会对主机性能造成影响,但这种影响可以在一定程度上由CGroups控制住,不至于对主机带来灾难性的影响。如果容器里面运行的是不耗资源的进程,那么对系统就没有影响,只是容器里面的文件系统可能会占用一些磁盘空间。 79 | 80 | -------------------------------------------------------------------------------- /container/003_namespace_and_cgroup.md: -------------------------------------------------------------------------------- 1 | # Linux Namespace和Cgroup 2 | 3 | 为了方便阅读,将自己写的所有关于namespace和cgroup的文章统一列在这里,希望对有需要的人有所帮助,后续有新的内容后将会更新这里的列表。 4 | 5 | ## namespace 6 | 包含了Linux目前常用的6个namespace的介绍 7 | 8 | * [Linux Namespace系列(01):Namespace概述](namespace/001_namespace_introduction.md) 9 | * [Linux Namespace系列(02):UTS namespace (CLONE_NEWUTS)](namespace/002_namespace_uts.md) 10 | * [Linux Namespace系列(03):IPC namespace (CLONE_NEWIPC)](namespace/003_namespace_ipc.md) 11 | * [Linux Namespace系列(04):mount namespaces (CLONE_NEWNS)](namespace/004_namespace_mount.md) 12 | * [Linux Namespace系列(05):pid namespace (CLONE_NEWPID)](namespace/005_namespace_pid.md) 13 | * [Linux Namespace系列(06):network namespace (CLONE_NEWNET)](namespace/006_namespace_network.md) 14 | * [Linux Namespace系列(07):user namespace (CLONE_NEWUSER) (第一部分)](namespace/007_namespace_user_01.md) 15 | * [Linux Namespace系列(08):user namespace (CLONE_NEWUSER) (第二部分)](namespace/008_namespace_user_02.md) 16 | * [Linux Namespace系列(09):利用Namespace创建一个简单可用的容器](namespace/009_create_simple_container.md) 17 | 18 | ## cgroup 19 | 目前只包含了pid、cpu和memory这三个常用的subsystem,后续会根据情况增加更多类型的介绍 20 | 21 | * [Linux Cgroup系列(01):Cgroup概述](cgroup/001_cgroup_introduction.md) 22 | * [Linux Cgroup系列(02):创建并管理cgroup](cgroup/002_cgroup_no_subsystem.md) 23 | * [Linux Cgroup系列(03):限制cgroup的进程数(subsystem之pids)](cgroup/003_cgroup_pids.md) 24 | * [Linux Cgroup系列(04):限制cgroup的内存使用(subsystem之memory)](cgroup/004_cgroup_memeory.md) 25 | * [Linux Cgroup系列(05):限制cgroup的CPU使用(subsystem之cpu)](cgroup/005_cgroup_cpu.md) -------------------------------------------------------------------------------- /container/cgroup/002_cgroup_no_subsystem.md: -------------------------------------------------------------------------------- 1 | # Linux Cgroup系列(02):创建并管理cgroup 2 | 3 | 本文将创建并挂载一颗不和任何subsystem绑定的cgroup树,用来演示怎么创建、删除子cgroup,以及如何往cgroup中添加和删除进程。 4 | 5 | 由于不和任何subsystem绑定,所以这棵树没有任何实际的功能,但这不影响我们的演示,还有一个好处就是我们不会受subsystem功能的影响,可以将精力集中在cgroup树上。 6 | 7 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 8 | 9 | ## 挂载cgroup树 10 | 开始使用cgroup前需要先挂载cgroup树,下面先看看如何挂载一颗cgroup树,然后再查看其根目录下生成的文件 11 | ```bash 12 | #准备需要的目录 13 | dev@ubuntu:~$ mkdir cgroup && cd cgroup 14 | dev@ubuntu:~/cgroup$ mkdir demo 15 | 16 | #由于name=demo的cgroup树不存在,所以系统会创建一颗新的cgroup树,然后挂载到demo目录 17 | dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o none,name=demo demo ./demo 18 | 19 | #挂载点所在目录就是这颗cgroup树的root cgroup,在root cgroup下面,系统生成了一些默认文件 20 | dev@ubuntu:~/cgroup$ ls ./demo/ 21 | cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks 22 | 23 | #cgroup.procs里包含系统中的所有进程 24 | dev@ubuntu:~/cgroup$ wc -l ./demo/cgroup.procs 25 | 131 ./demo/cgroup.procs 26 | 27 | ``` 28 | 下面是每个文件的含义: 29 | 30 | * cgroup.clone_children 31 | 这个文件只对cpuset(subsystem)有影响,当该文件的内容为1时,新创建的cgroup将会继承父cgroup的配置,即从父cgroup里面拷贝配置文件来初始化新cgroup,可以参考[这里](https://lkml.org/lkml/2010/7/29/368) 32 | 33 | * cgroup.procs 34 | 当前cgroup中的所有进程ID,系统不保证ID是顺序排列的,且ID有可能重复 35 | 36 | * cgroup.sane_behavior 37 | 具体功能不详,可以参考[这里](https://lkml.org/lkml/2014/7/2/684)和[这里](https://lkml.org/lkml/2014/7/2/686) 38 | 39 | * notify_on_release 40 | 该文件的内容为1时,当cgroup退出时(不再包含任何进程和子cgroup),将调用release_agent里面配置的命令。新cgroup被创建时将默认继承父cgroup的这项配置。 41 | 42 | * release_agent 43 | 里面包含了cgroup退出时将会执行的命令,系统调用该命令时会将相应cgroup的相对路径当作参数传进去。 注意:这个文件只会存在于root cgroup下面,其他cgroup里面不会有这个文件。 44 | 45 | * tasks 46 | 当前cgroup中的所有线程ID,系统不保证ID是顺序排列的 47 | 48 | 后面在介绍如何往cgroup中添加进程时会介绍cgroup.procs和tasks的差别。 49 | 50 | ## 创建和删除cgroup 51 | 挂载好上面的cgroup树之后,就可以在里面建子cgroup了 52 | ```bash 53 | #创建子cgroup很简单,新建一个目录就可以了 54 | dev@ubuntu:~/cgroup$ cd demo 55 | dev@ubuntu:~/cgroup/demo$ sudo mkdir cgroup1 56 | 57 | #在新创建的cgroup里面,系统默认也生成了一些文件,这些文件的意义和root cgroup里面的一样 58 | dev@ubuntu:~/cgroup/demo$ ls cgroup1/ 59 | cgroup.clone_children cgroup.procs notify_on_release tasks 60 | 61 | #新创建的cgroup里没有任何进程和线程 62 | dev@ubuntu:~/cgroup/demo$ wc -l cgroup1/cgroup.procs 63 | 0 cgroup1/cgroup.procs 64 | dev@ubuntu:~/cgroup/demo$ wc -l cgroup1/tasks 65 | 0 cgroup1/tasks 66 | 67 | #每个cgroup都可以创建自己的子cgroup,所以我们也可以在cgroup1里面创建子cgroup 68 | dev@ubuntu:~/cgroup/demo$ sudo mkdir cgroup1/cgroup11 69 | dev@ubuntu:~/cgroup/demo$ ls cgroup1/cgroup11 70 | cgroup.clone_children cgroup.procs notify_on_release tasks 71 | 72 | #删除cgroup也很简单,删除掉相应的目录就可以了 73 | dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/ 74 | rmdir: failed to remove 'cgroup1/': Device or resource busy 75 | #这里删除cgroup1失败,是因为它里面包含了子cgroup,所以不能删除, 76 | #如果cgroup1包含有进程或者线程,也会删除失败 77 | 78 | #先删除cgroup11,再删除cgroup1就可以了 79 | dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/cgroup11/ 80 | dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/ 81 | ``` 82 | 83 | ## 添加进程 84 | 创建新的cgroup后,就可以往里面添加进程了。注意下面几点: 85 | 86 | * 在一颗cgroup树里面,一个进程必须要属于一个cgroup。 87 | * 新创建的子进程将会自动加入父进程所在的cgroup。 88 | * 从一个cgroup移动一个进程到另一个cgroup时,只要有目的cgroup的写入权限就可以了,系统不会检查源cgroup里的权限。 89 | * 用户只能操作属于自己的进程,不能操作其他用户的进程,root账号除外。 90 | 91 | ```bash 92 | #--------------------------第一个shell窗口---------------------- 93 | #创建一个新的cgroup 94 | dev@ubuntu:~/cgroup/demo$ sudo mkdir test 95 | dev@ubuntu:~/cgroup/demo$ cd test 96 | 97 | #将当前bash加入到上面新创建的cgroup中 98 | dev@ubuntu:~/cgroup/demo/test$ echo $$ 99 | 1421 100 | dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > cgroup.procs' 101 | #注意:一次只能往这个文件中写一个进程ID,如果需要写多个的话,需要多次调用这个命令 102 | 103 | #--------------------------第二个shell窗口---------------------- 104 | #重新打开一个shell窗口,避免第一个shell里面运行的命令影响输出结果 105 | #这时可以看到cgroup.procs里面包含了上面的第一个shell进程 106 | dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs 107 | 1421 108 | 109 | #--------------------------第一个shell窗口---------------------- 110 | #回到第一个窗口,运行top命令 111 | dev@ubuntu:~/cgroup/demo/test$ top 112 | #这里省略输出内容 113 | 114 | #--------------------------第二个shell窗口---------------------- 115 | #这时再在第二个窗口查看,发现top进程自动和它的父进程(1421)属于同一个cgroup 116 | dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs 117 | 1421 118 | 16515 119 | dev@ubuntu:~/cgroup/demo/test$ ps -ef|grep top 120 | dev 16515 1421 0 04:02 pts/0 00:00:00 top 121 | dev@ubuntu:~/cgroup/demo/test$ 122 | 123 | #在一颗cgroup树里面,一个进程必须要属于一个cgroup, 124 | #所以我们不能凭空从一个cgroup里面删除一个进程,只能将一个进程从一个cgroup移到另一个cgroup, 125 | #这里我们将1421移动到root cgroup 126 | dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > ../cgroup.procs' 127 | dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs 128 | 16515 129 | #移动1421到另一个cgroup之后,它的子进程不会随着移动 130 | 131 | #--------------------------第一个shell窗口---------------------- 132 | ##回到第一个shell窗口,进行清理工作 133 | #先用ctrl+c退出top命令 134 | dev@ubuntu:~/cgroup/demo/test$ cd .. 135 | #然后删除创建的cgroup 136 | dev@ubuntu:~/cgroup/demo$ sudo rmdir test 137 | ``` 138 | ## 权限 139 | 上面我们都是用sudo(root账号)来操作的,但实际上普通账号也可以操作cgroup 140 | ```bash 141 | #创建一个新的cgroup,并修改他的owner 142 | dev@ubuntu:~/cgroup/demo$ sudo mkdir permission 143 | dev@ubuntu:~/cgroup/demo$ sudo chown -R dev:dev ./permission/ 144 | 145 | #1421原来属于root cgroup,虽然dev没有root cgroup的权限,但还是可以将1421移动到新的cgroup下, 146 | #说明在移动进程的时候,系统不会检查源cgroup里的权限。 147 | dev@ubuntu:~/cgroup/demo$ echo 1421 > ./permission/cgroup.procs 148 | 149 | #由于dev没有root cgroup的权限,再把1421移回root cgroup失败 150 | dev@ubuntu:~/cgroup/demo$ echo 1421 > ./cgroup.procs 151 | -bash: ./cgroup.procs: Permission denied 152 | 153 | #找一个root账号的进程 154 | dev@ubuntu:~/cgroup/demo$ ps -ef|grep /lib/systemd/systemd-logind 155 | root 839 1 0 01:52 ? 00:00:00 /lib/systemd/systemd-logind 156 | #因为该进程属于root,dev没有操作它的权限,所以将该进程加入到permission中失败 157 | dev@ubuntu:~/cgroup/demo$ echo 839 >./permission/cgroup.procs 158 | -bash: echo: write error: Permission denied 159 | #只能由root账号添加 160 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 839 >./permission/cgroup.procs' 161 | 162 | #dev还可以在permission下创建子cgroup 163 | dev@ubuntu:~/cgroup/demo$ mkdir permission/c1 164 | dev@ubuntu:~/cgroup/demo$ ls permission/c1 165 | cgroup.clone_children cgroup.procs notify_on_release tasks 166 | 167 | #清理 168 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 839 >./cgroup.procs' 169 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 1421 >./cgroup.procs' 170 | dev@ubuntu:~/cgroup/demo$ rmdir permission/c1 171 | dev@ubuntu:~/cgroup/demo$ sudo rmdir permission 172 | ``` 173 | 174 | ## cgroup.procs vs tasks 175 | 上面提到cgroup.procs包含的是进程ID, 而tasks里面包含的是线程ID,那么他们有什么区别呢? 176 | ```bash 177 | #创建两个新的cgroup用于演示 178 | dev@ubuntu:~/cgroup/demo$ sudo mkdir c1 c2 179 | 180 | #为了便于操作,先给root账号设置一个密码,然后切换到root账号 181 | dev@ubuntu:~/cgroup/demo$ sudo passwd root 182 | dev@ubuntu:~/cgroup/demo$ su root 183 | root@ubuntu:/home/dev/cgroup/demo# 184 | 185 | #系统中找一个有多个线程的进程 186 | root@ubuntu:/home/dev/cgroup/demo# ps -efL|grep /lib/systemd/systemd-timesyncd 187 | systemd+ 610 1 610 0 2 01:52 ? 00:00:00 /lib/systemd/systemd-timesyncd 188 | systemd+ 610 1 616 0 2 01:52 ? 00:00:00 /lib/systemd/systemd-timesyncd 189 | #进程610有两个线程,分别是610和616 190 | 191 | #将616加入c1/cgroup.procs 192 | root@ubuntu:/home/dev/cgroup/demo# echo 616 > c1/cgroup.procs 193 | #由于cgroup.procs存放的是进程ID,所以这里看到的是616所属的进程ID(610) 194 | root@ubuntu:/home/dev/cgroup/demo# cat c1/cgroup.procs 195 | 610 196 | #从tasks中的内容可以看出,虽然只往cgroup.procs中加了线程616, 197 | #但系统已经将这个线程所属的进程的所有线程都加入到了tasks中, 198 | #说明现在整个进程的所有线程已经处于c1中了 199 | root@ubuntu:/home/dev/cgroup/demo# cat c1/tasks 200 | 610 201 | 616 202 | 203 | #将616加入c2/tasks中 204 | root@ubuntu:/home/dev/cgroup/demo# echo 616 > c2/tasks 205 | 206 | #这时我们看到虽然在c1/cgroup.procs和c2/cgroup.procs里面都有610, 207 | #但c1/tasks和c2/tasks中包含了不同的线程,说明这个进程的两个线程分别属于不同的cgroup 208 | root@ubuntu:/home/dev/cgroup/demo# cat c1/cgroup.procs 209 | 610 210 | root@ubuntu:/home/dev/cgroup/demo# cat c1/tasks 211 | 610 212 | root@ubuntu:/home/dev/cgroup/demo# cat c2/cgroup.procs 213 | 610 214 | root@ubuntu:/home/dev/cgroup/demo# cat c2/tasks 215 | 616 216 | #通过tasks,我们可以实现线程级别的管理,但通常情况下不会这么用, 217 | #并且在cgroup V2以后,将不再支持该功能,只能以进程为单位来配置cgroup 218 | 219 | #清理 220 | root@ubuntu:/home/dev/cgroup/demo# echo 610 > ./cgroup.procs 221 | root@ubuntu:/home/dev/cgroup/demo# rmdir c1 222 | root@ubuntu:/home/dev/cgroup/demo# rmdir c2 223 | root@ubuntu:/home/dev/cgroup/demo# exit 224 | exit 225 | ``` 226 | 227 | ## release_agent 228 | 当一个cgroup里没有进程也没有子cgroup时,release_agent将被调用来执行cgroup的清理工作。 229 | 230 | ```bash 231 | #创建新的cgroup用于演示 232 | dev@ubuntu:~/cgroup/demo$ sudo mkdir test 233 | #先enable release_agent 234 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 1 > ./test/notify_on_release' 235 | 236 | #然后创建一个脚本/home/dev/cgroup/release_demo.sh, 237 | #一般情况下都会利用这个脚本执行一些cgroup的清理工作,但我们这里为了演示简单,仅仅只写了一条日志到指定文件 238 | dev@ubuntu:~/cgroup/demo$ cat > /home/dev/cgroup/release_demo.sh << EOF 239 | #!/bin/bash 240 | echo \$0:\$1 >> /home/dev/release_demo.log 241 | EOF 242 | 243 | #添加可执行权限 244 | dev@ubuntu:~/cgroup/demo$ chmod +x ../release_demo.sh 245 | 246 | #将该脚本设置进文件release_agent 247 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo /home/dev/cgroup/release_demo.sh > ./release_agent' 248 | dev@ubuntu:~/cgroup/demo$ cat release_agent 249 | /home/dev/cgroup/release_demo.sh 250 | 251 | #往test里面添加一个进程,然后再移除,这样就会触发release_demo.sh 252 | dev@ubuntu:~/cgroup/demo$ echo $$ 253 | 27597 254 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./test/cgroup.procs' 255 | dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./cgroup.procs' 256 | 257 | #从日志可以看出,release_agent被触发了,/test是cgroup的相对路径 258 | dev@ubuntu:~/cgroup/demo$ cat /home/dev/release_demo.log 259 | /home/dev/cgroup/release_demo.sh:/test 260 | ``` 261 | 262 | ## 结束语 263 | 本文介绍了如何操作cgroup,由于没有和任何subsystem关联,所以在这颗树上的所有操作都没有实际的功能,不会对系统有影响。从下一篇开始,将介绍具体的subsystem。 264 | 265 | ## 参考 266 | * [CGROUPS v1](https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt) 267 | -------------------------------------------------------------------------------- /container/cgroup/003_cgroup_pids.md: -------------------------------------------------------------------------------- 1 | # Linux Cgroup系列(03):限制cgroup的进程数(subsystem之pids) 2 | 3 | [上一篇文章](002_cgroup_no_subsystem.md)中介绍了如何管理cgroup,从这篇开始将介绍具体的subsystem。 4 | 5 | 本篇将介绍一个简单的subsystem,名字叫[pids](https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt),功能是限制cgroup及其所有子孙cgroup里面能创建的总的task数量。 6 | 7 | >注意:这里的task指通过fork和clone函数创建的进程,由于clone函数也能创建线程(在Linux里面,线程是一种特殊的进程),所以这里的task也包含线程,本文统一以进程来代表task,即本文中的进程代表了进程和线程 8 | 9 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 10 | 11 | ## 创建子cgroup 12 | 在ubuntu 16.04里面,systemd已经帮我们将各个subsystem和cgroup树绑定并挂载好了,我们直接用现成的就可以了。 13 | ```bash 14 | #从这里的输出可以看到,pids已经被挂载在了/sys/fs/cgroup/pids,这是systemd做的 15 | dev@dev:~$ mount|grep pids 16 | cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) 17 | ``` 18 | 19 | 创建子cgroup,取名为test 20 | ```bash 21 | #进入目录/sys/fs/cgroup/pids/并新建一个目录,即创建了一个子cgroup 22 | dev@dev:~$ cd /sys/fs/cgroup/pids/ 23 | dev@dev:/sys/fs/cgroup/pids$ sudo mkdir test 24 | #这里将test目录的owner设置成dev账号,这样后续操作就不用每次都敲sudo了,省去麻烦 25 | dev@dev:/sys/fs/cgroup/pids$ sudo chown -R dev:dev ./test/ 26 | ``` 27 | 28 | 再来看看test目录下的文件 29 | ```bash 30 | #除了上一篇中介绍的那些文件外,多了两个文件 31 | dev@dev:/sys/fs/cgroup/pids$ cd test 32 | dev@dev:/sys/fs/cgroup/pids/test$ ls 33 | cgroup.clone_children cgroup.procs notify_on_release pids.current pids.max tasks 34 | ``` 35 | 36 | 下面是这两个文件的含义: 37 | 38 | * pids.current: 表示当前cgroup及其所有子孙cgroup中现有的总的进程数量 39 | ```bash 40 | #由于这是个新创建的cgroup,所以里面还没有任何进程 41 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 42 | 0 43 | ``` 44 | 45 | * pids.max: 当前cgroup及其所有子孙cgroup中所允许创建的总的最大进程数量,在根cgroup下没有这个文件,原因显而易见,因为我们没有必要限制整个系统所能创建的进程数量。 46 | ```bash 47 | #max表示没做任何限制 48 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 49 | max 50 | ``` 51 | 52 | ## 限制进程数 53 | 这里我们演示一下如何让限制功能生效 54 | ```bash 55 | #--------------------------第一个shell窗口---------------------- 56 | #将pids.max设置为1,即当前cgroup只允许有一个进程 57 | dev@dev:/sys/fs/cgroup/pids/test$ echo 1 > pids.max 58 | #将当前bash进程加入到该cgroup 59 | dev@dev:/sys/fs/cgroup/pids/test$ echo $$ > cgroup.procs 60 | #--------------------------第二个shell窗口---------------------- 61 | #重新打开一个bash窗口,在里面看看cgroup “test”里面的一些数据 62 | #因为这是一个新开的bash,跟cgroup ”test“没有任何关系,所以在这里运行命令不会影响cgroup “test” 63 | dev@dev:~$ cd /sys/fs/cgroup/pids/test 64 | #设置的最大进程数是1 65 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 66 | 1 67 | #目前test里面已经有了一个进程,说明不能在fork或者clone进程了 68 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 69 | 1 70 | #这个进程就是第一个窗口的bash 71 | dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs 72 | 3083 73 | #--------------------------第一个shell窗口---------------------- 74 | #回到第一个窗口,随便运行一个命令,由于当前pids.current已经等于pids.max了, 75 | #所以创建新进程失败,于是命令运行失败,说明限制生效 76 | dev@dev:/sys/fs/cgroup/pids/test$ ls 77 | -bash: fork: retry: No child processes 78 | -bash: fork: retry: No child processes 79 | -bash: fork: retry: No child processes 80 | -bash: fork: retry: No child processes 81 | -bash: fork: Resource temporarily unavailable 82 | ``` 83 | 84 | ## 当前cgroup和子cgroup之间的关系 85 | 当前cgroup中的pids.current和pids.max代表了当前cgroup及所有子孙cgroup的所有进程,所以子孙cgroup中的pids.max大小不能超过父cgroup中的大小,如果子cgroup中的pids.max设置的大于父cgroup里的大小,会怎么样?请看下面的演示 86 | ```bash 87 | #继续使用上面的两个窗口 88 | #--------------------------第二个shell窗口---------------------- 89 | #将pids.max设置成2 90 | dev@dev:/sys/fs/cgroup/pids/test$ echo 2 > pids.max 91 | #在test下面创建一个子cgroup 92 | dev@dev:/sys/fs/cgroup/pids/test$ mkdir subtest 93 | dev@dev:/sys/fs/cgroup/pids/test$ cd subtest/ 94 | #将subtest的pids.max设置为5 95 | dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo 5 > pids.max 96 | #将当前bash进程加入到subtest中 97 | dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo $$ > cgroup.procs 98 | #--------------------------第三个shell窗口---------------------- 99 | #重新打开一个bash窗口,看一下test和subtest里面的数据 100 | #test里面的数据如下: 101 | dev@dev:~$ cd /sys/fs/cgroup/pids/test 102 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 103 | 2 104 | #这里为2表示目前test和subtest里面总的进程数为2 105 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 106 | 2 107 | dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs 108 | 3083 109 | 110 | #subtest里面的数据如下: 111 | dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.max 112 | 5 113 | dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.current 114 | 1 115 | dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs 116 | 3185 117 | #--------------------------第一个shell窗口---------------------- 118 | #回到第一个窗口,随便运行一个命令,由于test里面的pids.current已经等于pids.max了, 119 | #所以创建新进程失败,于是命令运行失败,说明限制生效 120 | dev@dev:/sys/fs/cgroup/pids/test$ ls 121 | -bash: fork: retry: No child processes 122 | -bash: fork: retry: No child processes 123 | -bash: fork: retry: No child processes 124 | -bash: fork: retry: No child processes 125 | -bash: fork: Resource temporarily unavailable 126 | #--------------------------第二个shell窗口---------------------- 127 | #回到第二个窗口,随便运行一个命令,虽然subtest里面的pids.max还大于pids.current, 128 | #但由于其父cgroup “test”里面的pids.current已经等于pids.max了, 129 | #所以创建新进程失败,于是命令运行失败,说明子cgroup中的进程数不仅受自己的pids.max的限制, 130 | #还受祖先cgroup的限制 131 | dev@dev:/sys/fs/cgroup/pids/test/subtest$ ls 132 | -bash: fork: retry: No child processes 133 | -bash: fork: retry: No child processes 134 | -bash: fork: retry: No child processes 135 | -bash: fork: retry: No child processes 136 | -bash: fork: Resource temporarily unavailable 137 | ``` 138 | ## pids.current > pids.max的情况 139 | 并不是所有情况下都是pids.max >= pids.current,在下面两种情况下,会出现pids.max < pids.current 的情况: 140 | 141 | * 设置pids.max时,将其值设置的比pids.current小 142 | ```bash 143 | #继续使用上面的三个窗口 144 | #--------------------------第三个shell窗口---------------------- 145 | #将test的pids.max设置为1 146 | dev@dev:/sys/fs/cgroup/pids/test$ echo 1 > pids.max 147 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 148 | 1 149 | #这个时候就会出现pids.current > pids.max的情况 150 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 151 | 2 152 | 153 | #--------------------------第一个shell窗口---------------------- 154 | #回到第一个shell 155 | #还是运行失败,说明虽然pids.current > pids.max,但限制创建新进程的功能还是会生效 156 | dev@dev:/sys/fs/cgroup/pids/test$ ls 157 | -bash: fork: retry: No child processes 158 | -bash: fork: retry: No child processes 159 | -bash: fork: retry: No child processes 160 | -bash: fork: retry: No child processes 161 | -bash: fork: Resource temporarily unavailable 162 | ``` 163 | * pids.max只会在当前cgroup中的进程fork、clone的时候生效,将其他进程加入到当前cgroup时,不会检测pids.max,所以将其他进程加入到当前cgroup有可能会导致pids.current > pids.max 164 | ```bash 165 | #继续使用上面的三个窗口 166 | #--------------------------第三个shell窗口---------------------- 167 | #将subtest中的进程移动到根cgroup下,然后删除subtest 168 | dev@dev:/sys/fs/cgroup/pids/test$ sudo sh -c 'echo 3185 > /sys/fs/cgroup/pids/cgroup.procs' 169 | #里面没有进程了,说明移动成功 170 | dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs 171 | #移除成功 172 | dev@dev:/sys/fs/cgroup/pids/test$ rmdir subtest/ 173 | 174 | #这时候test下的pids.max等于pids.current了 175 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 176 | 1 177 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 178 | 1 179 | 180 | #--------------------------第二个shell窗口---------------------- 181 | #将当前bash加入到test中 182 | dev@dev:/sys/fs/cgroup/pids/test/subtest$ cd .. 183 | dev@dev:/sys/fs/cgroup/pids/test$ echo $$ > cgroup.procs 184 | 185 | #--------------------------第三个shell窗口---------------------- 186 | #回到第三个窗口,查看相关信息 187 | #第一个和第二个窗口的bash都属于test 188 | dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs 189 | 3083 190 | 3185 191 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 192 | 1 193 | #出现了pids.current > pids.max的情况,这是因为我们将第二个窗口的shell加入了test 194 | dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 195 | 2 196 | #--------------------------第二个shell窗口---------------------- 197 | #对fork调用的限制仍然生效 198 | dev@dev:/sys/fs/cgroup/pids/test$ ls 199 | -bash: fork: retry: No child processes 200 | -bash: fork: retry: No child processes 201 | -bash: fork: retry: No child processes 202 | -bash: fork: retry: No child processes 203 | -bash: fork: Resource temporarily unavailable 204 | ``` 205 | 206 | 清理 207 | ```bash 208 | #--------------------------第三个shell窗口---------------------- 209 | dev@dev:/sys/fs/cgroup/pids/test$ sudo sh -c 'echo 3185 > /sys/fs/cgroup/pids/cgroup.procs' 210 | dev@dev:/sys/fs/cgroup/pids/test$ sudo sh -c 'echo 3083 > /sys/fs/cgroup/pids/cgroup.procs' 211 | dev@dev:/sys/fs/cgroup/pids/test$ cd .. 212 | dev@dev:/sys/fs/cgroup/pids$ sudo rmdir test/ 213 | ``` 214 | 215 | ## 结束语 216 | 本文介绍了如何利用pids这个subsystem来限制cgroup中的进程数,以及一些要注意的地方,总的来说pids比较简单。下一篇将介绍稍微复杂点的内存控制。 217 | 218 | ## 参考 219 | * [Process Number Controller](https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt) 220 | -------------------------------------------------------------------------------- /container/cgroup/005_cgroup_cpu.md: -------------------------------------------------------------------------------- 1 | # Linux Cgroup系列(05):限制cgroup的CPU使用(subsystem之cpu) 2 | 3 | 在cgroup里面,跟CPU相关的子系统有[cpusets](https://www.kernel.org/doc/Documentation/cgroup-v1/cpusets.txt)、[cpuacct](https://www.kernel.org/doc/Documentation/cgroup-v1/cpuacct.txt)和[cpu](https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)。 4 | 5 | 其中cpuset主要用于设置CPU的亲和性,可以限制cgroup中的进程只能在指定的CPU上运行,或者不能在指定的CPU上运行,同时cpuset还能设置内存的亲和性。设置亲和性一般只在比较特殊的情况才用得着,所以这里不做介绍。 6 | 7 | cpuacct包含当前cgroup所使用的CPU的统计信息,信息量较少,有兴趣可以去看看它的文档,这里不做介绍。 8 | 9 | 本篇只介绍cpu子系统,包括怎么限制cgroup的CPU使用上限及相对于其它cgroup的相对值。 10 | 11 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 12 | 13 | ## 创建子cgroup 14 | 在ubuntu下,systemd已经帮我们mount好了cpu子系统,我们只需要在相应的目录下创建子目录就可以了 15 | 16 | ```bash 17 | #从这里的输出可以看到,cpuset被挂载在了/sys/fs/cgroup/cpuset, 18 | #而cpu和cpuacct一起挂载到了/sys/fs/cgroup/cpu,cpuacct下面 19 | dev@ubuntu:~$ mount|grep cpu 20 | cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) 21 | cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) 22 | 23 | #进入/sys/fs/cgroup/cpu,cpuacct并创建子cgroup 24 | dev@ubuntu:~$ cd /sys/fs/cgroup/cpu,cpuacct 25 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct$ sudo mkdir test 26 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct$ cd test 27 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ ls 28 | cgroup.clone_children cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.stat tasks 29 | cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.shares notify_on_release 30 | ``` 31 | 32 | 除了cgroup里面通用的cgroup.clone_children、tasks、cgroup.procs、notify_on_release这几个文件外,以cpuacct.开头的文件跟cpuacct子系统有关,我们这里只需要关注cpu.开头的文件。 33 | 34 | #### cpu.cfs_period_us & cpu.cfs_quota_us 35 | cfs_period_us用来配置时间周期长度,cfs_quota_us用来配置当前cgroup在设置的周期长度内所能使用的CPU时间数,两个文件配合起来设置CPU的使用上限。两个文件的单位都是微秒(us),cfs_period_us的取值范围为1毫秒(ms)到1秒(s),cfs_quota_us的取值大于1ms即可,如果cfs_quota_us的值为-1(默认值),表示不受cpu时间的限制。下面是几个例子: 36 | ``` 37 | 1.限制只能使用1个CPU(每250ms能使用250ms的CPU时间) 38 | # echo 250000 > cpu.cfs_quota_us /* quota = 250ms */ 39 | # echo 250000 > cpu.cfs_period_us /* period = 250ms */ 40 | 41 | 2.限制使用2个CPU(内核)(每500ms能使用1000ms的CPU时间,即使用两个内核) 42 | # echo 1000000 > cpu.cfs_quota_us /* quota = 1000ms */ 43 | # echo 500000 > cpu.cfs_period_us /* period = 500ms */ 44 | 45 | 3.限制使用1个CPU的20%(每50ms能使用10ms的CPU时间,即使用一个CPU核心的20%) 46 | # echo 10000 > cpu.cfs_quota_us /* quota = 10ms */ 47 | # echo 50000 > cpu.cfs_period_us /* period = 50ms */ 48 | ``` 49 | 50 | #### cpu.shares 51 | shares用来设置CPU的相对值,并且是针对所有的CPU(内核),默认值是1024,假如系统中有两个cgroup,分别是A和B,A的shares值是1024,B的shares值是512,那么A将获得1024/(1204+512)=66%的CPU资源,而B将获得33%的CPU资源。shares有两个特点: 52 | 53 | * 如果A不忙,没有使用到66%的CPU时间,那么剩余的CPU时间将会被系统分配给B,即B的CPU使用率可以超过33% 54 | * 如果添加了一个新的cgroup C,且它的shares值是1024,那么A的限额变成了1024/(1204+512+1024)=40%,B的变成了20% 55 | 56 | 从上面两个特点可以看出: 57 | 58 | * 在闲的时候,shares基本上不起作用,只有在CPU忙的时候起作用,这是一个优点。 59 | * 由于shares是一个绝对值,需要和其它cgroup的值进行比较才能得到自己的相对限额,而在一个部署很多容器的机器上,cgroup的数量是变化的,所以这个限额也是变化的,自己设置了一个高的值,但别人可能设置了一个更高的值,所以这个功能没法精确的控制CPU使用率。 60 | 61 | #### cpu.stat 62 | 包含了下面三项统计结果 63 | 64 | * nr_periods: 表示过去了多少个cpu.cfs_period_us里面配置的时间周期 65 | * nr_throttled: 在上面的这些周期中,有多少次是受到了限制(即cgroup中的进程在指定的时间周期中用光了它的配额) 66 | * throttled_time: cgroup中的进程被限制使用CPU持续了多长时间(纳秒) 67 | 68 | ## 示例 69 | 这里以cfs_period_us & cfs_quota_us为例,演示一下如何控制CPU的使用率。 70 | ```bash 71 | #继续使用上面创建的子cgroup: test 72 | #设置只能使用1个cpu的20%的时间 73 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 50000 > cpu.cfs_period_us" 74 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 10000 > cpu.cfs_quota_us" 75 | 76 | #将当前bash加入到该cgroup 77 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ echo $$ 78 | 5456 79 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 5456 > cgroup.procs" 80 | 81 | #在bash中启动一个死循环来消耗cpu,正常情况下应该使用100%的cpu(即消耗一个内核) 82 | dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ while :; do echo test > /dev/null; done 83 | 84 | #--------------------------重新打开一个shell窗口---------------------- 85 | #通过top命令可以看到5456的CPU使用率为20%左右,说明被限制住了 86 | #不过这时系统的%us+%sy在10%左右,那是因为我测试的机器上cpu是双核的, 87 | #所以系统整体的cpu使用率为10%左右 88 | dev@ubuntu:~$ top 89 | Tasks: 139 total, 2 running, 137 sleeping, 0 stopped, 0 zombie 90 | %Cpu(s): 5.6 us, 6.2 sy, 0.0 ni, 88.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 91 | KiB Mem : 499984 total, 15472 free, 81488 used, 403024 buff/cache 92 | KiB Swap: 0 total, 0 free, 0 used. 383332 avail Mem 93 | 94 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 95 | 5456 dev 20 0 22640 5472 3524 R 20.3 1.1 0:04.62 bash 96 | 97 | #这时可以看到被限制的统计结果 98 | dev@ubuntu:~$ cat /sys/fs/cgroup/cpu,cpuacct/test/cpu.stat 99 | nr_periods 1436 100 | nr_throttled 1304 101 | throttled_time 51542291833 102 | ``` 103 | 104 | ## 结束语 105 | 使用cgroup限制CPU的使用率比较纠结,用cfs_period_us & cfs_quota_us吧,限制死了,没法充分利用空闲的CPU,用shares吧,又没法配置百分比,极其难控制。总之,使用cgroup的cpu子系统需谨慎。 106 | 107 | ## 参考 108 | * [CFS Bandwidth Control](https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt) 109 | * [cpu](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/sec-cpu.html) -------------------------------------------------------------------------------- /container/docker/000_docker_index.md: -------------------------------------------------------------------------------- 1 | # 走进docker系列:开篇 2 | 3 | 本人docker初学者,边学习边总结,一方面加深自己的理解,另一方面希望对其他想深入了解docker的同学有所帮助。 4 | 5 | 由于本人缺乏实战经验,错误在所难免,欢迎批评指正,谢谢。 6 | 7 | ## 包含的内容 8 | 本系列主要介绍三个github上的项目: **[moby](https://github.com/moby/moby)、[containerd](https://github.com/containerd/containerd)、[runc](https://github.com/opencontainers/runc)**. 9 | 10 | 由于只介绍docker核心的东西,所以**不会包含**下面这些项目: 11 | 12 | * [compose](https://github.com/docker/compose):使用Python语言开发,将多个相关的容器配置在一起,从而可以同时创建、启动、停止和监控它们。 13 | * [machine](https://github.com/docker/machine):帮助安装docker到指定位置,包括本地虚拟机、远端云主机等,同时还管理这些主机的信息,可以很方便的操作安装在不同主机上的这些docker。 14 | * [kitematic](https://github.com/docker/kitematic):桌面版的docker客户端(图形界面),使用JavaScript基于[electron](https://electron.atom.io/)开发。 15 | * [toolbox](https://github.com/docker/toolbox):帮助安装docker环境到Windows和Mac平台,包括Docker引擎、Compose、 Machine和 Kitematic,当然docker引擎是安装在虚拟机里面的,本地只有客户端,使用哪个虚拟机依赖于平台,toolbox会帮你搞定这一切。 16 | * [distribution](https://github.com/docker/distribution):[Registry 2.0](https://github.com/docker/distribution/blob/master/docs/spec/api.md)的实现,主要是管理和分发docker镜像,[Docker Hub](https://hub.docker.com/)背后的技术。 17 | * [swarmkit](https://github.com/docker/swarmkit):嵌入在docker里面的容器编排系统,可以简单的把它和docker的关系理解成IE浏览器和Windows的关系,捆绑销售。 18 | 19 | ## 面向读者 20 | 21 | 本系列主要**专注docker背后的技术和实现思路**,**不介绍怎么使用docker,不介绍代码细节**。 22 | 23 | * 如果你是docker初学者,想了解怎么使用docker,那么本系列不适合你。 24 | * 如果你已经熟悉了基本的操作,想了解下高级点的参数,或者想了解背后到底发生了什么,便于自己更好的使用docker,更好的解决碰到的问题,那么本系列适合你。 25 | * 如果你是一名开发人员,想了解docker的代码实现细节,但又不知道从何处下手,本系列也许会给你一些启发。 26 | 27 | ## docker版本 28 | 自从docker决定将swarm整合进来弄企业版之后,代码一直在调整,docker的一些目录和程序名称也在发生变化,所以本系列的内容没法覆盖所有docker版本,只能挑其中的一个。 29 | 30 | 自v17.03开始,docker采用了新的发行方式,版本的发行周期变成了一个月一次,并且也分了企业版和社区版,在本系列中,将以**v17.03社区版**作为参考,建议大家阅读本系列时,手头的docker版本不低于v17.03。 31 | 32 | docker完整的变更列表请参考[这里](https://github.com/moby/moby/blob/master/CHANGELOG.md)。 33 | 34 | ## docker和moby的关系 35 | 2017-04-18,在DockerCon 2017上,docker公司正式宣布成立moby项目,同时将github上的docker/docker项目重命名成了moby/moby,虽然会自动重定向,但代码里的相关引用不排除会有问题,需要留意。 36 | 37 | 这里不评价这次变化,对普通使用者来说,不会发生任何变化,还是熟悉的命令,熟悉的参数,对开发人员来说,代码的位置变了,但代码还是那份代码。 38 | 39 | 以后moby会变成什么样,现在还不清楚,有可能和docker的关系会变成[blink](https://www.chromium.org/blink)和chrome的关系一样,静观其变,希望不要影响我们学习。 40 | 41 | >注意:若没有特别说明,本系列提到的docker源码,都指的是moby的代码 42 | 43 | ## 文章列表 44 | 该系列的所有文章都会列在这里,便于大家选择阅读。由于本人时间安排发生变化,**本系列停止更新**,后面不确定是否会继续,非常抱歉。 45 | 46 | * [走进docker(01):hello-world的背后发生了什么?](001_how_does_hello_world_work.md) 47 | * [走进docker(02):image(镜像)是什么?](002_what_is_image.md) 48 | * [走进docker(03):如何绕过docker运行hello-world?](003_run_hello_world_without_docker.md) 49 | * [走进docker(04):什么是容器的runtime?](004_runtime.md) 50 | * [走进docker(05):docker在本地如何管理image(镜像)?](005_how_does_docker_manage_images.md) 51 | * [走进docker(06):docker create命令背后发生了什么?](006_what_happened_behind_the_docker_create_command.md) 52 | * [走进docker(07):docker start命令背后发生了什么?](007_what_happened_behind_the_docker_start_command.md) 53 | * [走进docker(10):如何读docker的代码?](010_how_to_read_code.md) 54 | 55 | ## 建议阅读 56 | 在阅读本系列之前,如果对Linux不是很熟的话,建议先阅读[主目录](/README.md)里面的Linux部分。 57 | 58 | ## 获取docker相关的代码 59 | 由于现在docker依赖的containerd和runc是github上两个单独的项目,如果你需要分析docker的代码,请确保containerd和runc的版本和docker的版本是一致的,检查办法如下: 60 | 61 | ```bash 62 | #假设我们已经将docker的源代码clone到了/home/dev/repos/docker目录下 63 | dev@debian:~/repos/docker$ git branch 64 | * master 65 | 66 | #列出17.03相关的tag 67 | dev@debian:~/repos/docker$ git tag|grep 17.03 68 | v17.03.0-ce 69 | v17.03.0-ce-rc1 70 | v17.03.1-ce 71 | v17.03.1-ce-rc1 72 | 73 | #取最新的v17.03.1-ce 74 | dev@debian:~/repos/docker$ git checkout -b v17.03.1-ce v17.03.1-ce 75 | Switched to a new branch 'v17.03.1-ce' 76 | dev@debian:~/repos/docker$ git branch 77 | master 78 | * v17.03.1-ce 79 | 80 | #查看docker所用的runc和containerd的commit id 81 | dev@debian:~/repos/docker$ egrep "RUNC_COMMIT|CONTAINERD_COMMIT" ./hack/dockerfile/binaries-commits 82 | # When updating RUNC_COMMIT, also update runc in vendor.conf accordingly 83 | RUNC_COMMIT=54296cf40ad8143b62dbcaa1d90e520a2136ddfe 84 | CONTAINERD_COMMIT=4ab9917febca54791c5f071a9d1f404867857fcc 85 | 86 | #查看runc和containerd的库路径 87 | dev@debian:~/repos/docker$ egrep "runc.git|containerd.git" ./hack/dockerfile/install-binaries.sh 88 | git clone https://github.com/docker/runc.git "$GOPATH/src/github.com/opencontainers/runc" 89 | git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" 90 | ``` 91 | 根据上面的结果,先将runc和containerd克隆下来,然后checkout相应的commit id,这样就可以配合docker的代码一起看了。这里是上面例子中找到的containerd和runc的信息: 92 | 93 | * containerd: https://github.com/docker/containerd.git 4ab9917febca54791c5f071a9d1f404867857fcc 94 | * runc: https://github.com/docker/runc.git 54296cf40ad8143b62dbcaa1d90e520a2136ddfe 95 | 96 | **注意:** 97 | 98 | * 可能是为了方便对runc进行修改,docker将github.com/opencontainers/runc克隆到了github.com/docker/runc,在docker v17.03里面,runc是从github.com/docker/runc.git拉的代码,然后放在本地的opencontainers/runc目录下,假装是opencontainers的runc,这个需要留意,别pull了错误的库。 99 | * 上面显示containerd的地址是https://github.com/docker/containerd.git,这个没有关系,github已经将这个地址重定向到了https://github.com/containerd/containerd.git 100 | -------------------------------------------------------------------------------- /container/docker/003_run_hello_world_without_docker.md: -------------------------------------------------------------------------------- 1 | # 走进docker(03):如何绕过docker运行hello-world? 2 | 3 | 上一篇介绍了image的格式,这里我们就来用一下hello-world这个image,看怎么输出和```docker run hello-world```同样的内容。 4 | 5 | ## 相关工具 6 | 本文将用到三个工具,分别是[skopeo](https://github.com/projectatomic/skopeo)、[oci-image-tools](https://github.com/opencontainers/image-tools)和[runc](https://github.com/opencontainers/runc)。 7 | 8 | * skopeo: 用来从Docker Hub上拉取image,并保存为OCI格式 9 | * oci-image-tools: 包含几个用来操作本地image的工具 10 | * runc: 运行容器 11 | 12 | runc可以用docker自带的docker-runc命令替代,效果是一样的,skopeo的安装可以参考[上一篇](002_what_is_image.md)最后的介绍或者[github上的主页](https://github.com/projectatomic/skopeo),oci-image-tools的安装请参考[github上的主页](https://github.com/opencontainers/image-tools)。 13 | 14 | ## 获取hello-world的image 15 | 利用skopeo获得hello-world的oci格式的image 16 | ```bash 17 | dev@debian:~/images$ skopeo copy docker://hello-world oci:hello-world 18 | dev@debian:~/images$ tree hello-world/ 19 | hello-world/ 20 | ├── blobs 21 | │   └── sha256 22 | │   ├── 0a2ad94772e366c2b7f2266ca46daa0c38efe08811cf1c1dee6558fcd7f2b54e 23 | │   ├── 78445dd45222097f5f8d5a16e48dc19c4ca162dcdb80010ab6f1ccfc7e2c0fa3 24 | │   └── 998a60597add14861de504277c0d850e9181b1768011f51c7daaf694dfe975ef 25 | ├── oci-layout 26 | └── refs 27 | └── latest 28 | ``` 29 | 30 | 然后我们看看hello-world这个image的文件系统都有些什么文件 31 | ```bash 32 | #利用oci-image-tool unpack,将image解压到hello-world-filesystem目录 33 | dev@debian:~/images$ mkdir hello-world-filesystem 34 | dev@debian:~/images$ oci-image-tool unpack --ref latest hello-world hello-world-filesystem 35 | dev@debian:~/images$ tree hello-world-filesystem/ 36 | hello-world-filesystem/ 37 | └── hello 38 | 39 | 0 directories, 1 file 40 | dev@debian:~/images$ file hello-world-filesystem/hello 41 | hello-world-filesystem/hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=4999eecfa472a2341b53954c0eca1e893f01305c, stripped 42 | ``` 43 | 44 | 从上面的结果可以看出,hello-world这个image就只包含一个名字叫做hello的静态链接的可执行文件。 45 | 46 | ## 根据image生成runtime的bundle 47 | runtime的bundle就是运行容器时需要的东西的集合。 48 | ```bash 49 | dev@debian:~/images$ mkdir hello-world-bundle 50 | dev@debian:~/images$ oci-image-tool create --ref latest hello-world hello-world-bundle 51 | dev@debian:~/images$ tree hello-world-bundle 52 | hello-world-bundle 53 | ├── config.json 54 | └── rootfs 55 | └── hello 56 | 57 | 1 directory, 2 files 58 | ``` 59 | 60 | 从这里生成的bundle可以看出,bundle里面就是一个配置文件加上rootfs,rootfs里面的东西就是image里面的文件系统部分,config.json是对容器的描述,比如rootfs的路径,容器启动后要运行什么命令等,后续介绍runtime标准的时候再详细介绍。 61 | 62 | ## 使用runc运行该bundle 63 | 有了bundle后,就可以用runc来运行该容器了 64 | 65 | >这里直接用docker的docker-runc代替runc命令,如果你自己编译了opencontainer的runc,那么用runc命令也是一样的。 66 | 67 | ``` 68 | dev@debian:~/images$ cd hello-world-bundle/ 69 | #oci-image-tool帮我们生成的config文件版本和runc需要的版本不一致, 70 | #所以这里先将它删掉,然后用runc spec命令生成一个默认的config文件 71 | dev@debian:~/images/hello-world-bundle$ rm config.json 72 | dev@debian:~/images/hello-world-bundle$ docker-runc spec 73 | 74 | #默认生成的config里面指定容器启动的进程为sh, 75 | #我们需要将它换成我们的hello程序 76 | #这里请用自己熟悉的编辑器修改config.json文件, 77 | #将里面的"args": ["sh"]改成"args": ["/hello"] 78 | dev@debian:~/images/hello-world-bundle$ vim config.json 79 | 80 | #然后用runc运行该容器,这里命令行里的hello是给容器取的名字, 81 | #可以是任意不和其它容器冲突的字符串 82 | dev@debian:~/images/hello-world-bundle$ sudo docker-runc run hello 83 | Hello from Docker! 84 | This message shows that your installation appears to be working correctly. 85 | 86 | To generate this message, Docker took the following steps: 87 | 1. The Docker client contacted the Docker daemon. 88 | 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 89 | 3. The Docker daemon created a new container from that image which runs the 90 | executable that produces the output you are currently reading. 91 | 4. The Docker daemon streamed that output to the Docker client, which sent it 92 | to your terminal. 93 | 94 | To try something more ambitious, you can run an Ubuntu container with: 95 | $ docker run -it ubuntu bash 96 | 97 | Share images, automate workflows, and more with a free Docker ID: 98 | https://cloud.docker.com/ 99 | 100 | For more examples and ideas, visit: 101 | https://docs.docker.com/engine/userguide/ 102 | 103 | ``` 104 | 105 | ## 结束语 106 | 该篇展示了如何不通过docker而运行hello-world容器,主要目的为了了解镜像以及runc之间的关系,同时也触发我们思考一个问题,既然我们可以绕过docker运行容器,那我们为什么还要用docker呢? -------------------------------------------------------------------------------- /container/docker/004_runtime.md: -------------------------------------------------------------------------------- 1 | # 走进docker(04):什么是容器的runtime? 2 | 3 | 我们都知道runc是容器runtime的一个实现,那到底什么是runtime?包含了哪些内容? 4 | 5 | 容器的runtime和image一样,也有标准,也由[OCI (Open Containers Initiative)](https://www.opencontainers.org/)负责维护,地址为[Runtime Specification](https://github.com/opencontainers/runtime-spec/blob/master/spec.md),了解runtime标准有利于我们更好的理解docker和runc的关系,本文将对该标准做一个简单的解释。 6 | 7 | ## 规范内容 8 | 在Linux平台上,跟runtime有关的规范主要有四个,分别是[Runtime and Lifecycle (runtime.md)](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md)、[Container Configuration file (config.md)](https://github.com/opencontainers/runtime-spec/blob/master/config.md)、[Linux Container Configuration (config-linux.md)](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md)和[Linux Runtime (runtime-linux.md)](https://github.com/opencontainers/runtime-spec/blob/master/runtime-linux.md) . 9 | 10 | 除了这四个之外,还有一个[Filesystem Bundle (bundle.md)](https://github.com/opencontainers/runtime-spec/blob/master/bundle.md)。 11 | 12 | 下面分别介绍这些规范。 13 | 14 | ### [Filesystem Bundle](https://github.com/opencontainers/runtime-spec/blob/master/bundle.md) 15 | 在[上一篇](003_run_hello_world_without_docker.md)中,已经用过bundle了,hello-world的bundle看起来是这个样子的: 16 | ```bash 17 | dev@debian:~/images$ tree hello-world-bundle 18 | hello-world-bundle 19 | ├── config.json 20 | └── rootfs 21 | └── hello 22 | 23 | 1 directory, 2 files 24 | ``` 25 | 26 | bundle中包含了运行容器所需要的所有信息,有了这个bundle后,符合runtime标准的程序(比如runc)就可以根据bundle启动容器了。 27 | 28 | bundle包含一个config.json文件和容器的根文件系统目录,config.json就是后面要介绍的```Container Configuration file```,标准要求该配置文件必须叫这个名字,不过对容器的根文件系统目录没有要求,只要在config.json里面将路径配置正确就可以了,不过一般约定俗成都叫rootfs。 29 | 30 | 实际使用过程中,根文件系统目录可能在其它的地方,只要config.json里面配置正确的路径就可以了,但如果bundle需要打包和其它人分享的话,必须将根文件系统和config.json打包在一起,并且不包含外层的文件夹。 31 | 32 | ### [Container Configuration file](https://github.com/opencontainers/runtime-spec/blob/master/config.md) 33 | 该规范定义了上面介绍的config.json里面应该包含哪些内容,字段很多,这里不一一详细介绍,只简单说明一下,完整的示例请参考[这里](https://github.com/opencontainers/runtime-spec/blob/master/config.md#configuration-schema-example): 34 | 35 | * ociVersion(必须):对应的OCI标准版本 36 | * root(必须):根文件系统的位置 37 | * mounts:需要挂载哪些目录到容器里面。如果是Linux平台,这里面必须要包含/proc、/sys,/dev/pts,/dev/shm这四个目录 38 | * process:容器启动后执行什么命令 39 | * hostname:容器的主机名,相关原理可参考[UTS namespace (CLONE_NEWUTS)](/container/namesapce/002_namespace_uts.md) 40 | * platform(必须):平台信息,如 amd64 + Linux 41 | * linux:Linux平台的特殊配置,这里包含下面要介绍的```Linux Container Configuration```里面的内容 42 | * hooks:配置容器运行生命周期中会调用的hooks,包括prestart、poststart和poststop,容器的生命周期见后面```Runtime and Lifecycle```介绍。 43 | * annotations:容器的注释,相当于容器标签,key:value格式 44 | 45 | ### [Linux Container Configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md) 46 | 该规范是Linux平台上对[Container Configuration file](https://github.com/opencontainers/runtime-spec/blob/master/config.md)的扩展,这部分的内容也包含在上面的config.json文件中。 47 | 48 | * namespaces: namespace相关的配置,相关原理可参考[Namespace概述](/container/namespace/001_namespace_introduction.md)及这些namespace([UTS](/container/namespace/002_namespace_uts.md)、[IPC](/container/namespace/003_namespace_ipc.md)、[mount](/container/namespace/004_namespace_mount.md)、[pid](/container/namespace/005_namespace_pid.md)、[network](/container/namespace/006_namespace_network.md)、[user 1](/container/namespace/007_namespace_user_01.md)、[user 2](/container/namespace/008_namespace_user_02.md))。 49 | * uidMappings,gidMappings:配置主机和容器用户/组之间的对应关系,原理可参考[user namespace](/container/namespace/007_namespace_user_01.md) 50 | * devices:设置哪些设备可以在容器内被访问到。除了这里指定的设备外,/dev/null、/dev/zero、/dev/full、/dev/random、/dev/urandom、/dev/tty、/dev/console(如果在process的配置里面启动terminal的话)和/dev/ptmx这些设备默认就能在容器内访问到,即runtime的实现需要默认将这些设备bind到容器内,dev/tty和/dev/ptmx的原理可以参考[TTY/PTS概述](/linux/019_tty.md) 51 | * cgroupsPath:cgroup的路径,可参考[Cgroup概述](/container/cgroup/001_cgroup_introduction.md) 52 | * resources:Cgroup中具体子项的配置,包括[device whitelist](https://www.kernel.org/doc/Documentation/cgroup-v1/devices.txt), [memory](/container/cgroup/004_cgroup_memeory.md), [cpu](/container/cgroup/005_cgroup_cpu.md), [blockIO](https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt), [hugepageLimits](https://www.kernel.org/doc/Documentation/cgroup-v1/hugetlb.txt), network([net_cls cgroup](https://www.kernel.org/doc/Documentation/cgroup-v1/net_cls.txt)和[net_prio cgroup](https://www.kernel.org/doc/Documentation/cgroup-v1/net_prio.txt)), [pids](/container/cgroup/003_cgroup_pids.md) 53 | * intelRdt:和[Intel Resource Director Technology](https://www.kernel.org/doc/Documentation/x86/intel_rdt_ui.txt)有关 54 | * sysctl:调整容器运行时的kernel参数,主要是一些网络参数,因为每个network namespace都有自己的协议栈,所以可以修改自己协议栈的参数而不影响别人 55 | * seccomp:和安全相关的配置,见[Seccomp ](https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt) 56 | * rootfsPropagation:设置Propagation类型。可以参考[Shared subtrees](/linux/003_mount_02.md) 57 | * maskedPaths:设置容器内的哪些目录对用户不可见 58 | * readonlyPaths:设置容器内的哪些目录是只读的 59 | * mountLabel:和Selinux有关。 60 | 61 | ### [Runtime and Lifecycle](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md) 62 | 该规范主要定义了跟容器运行时相关的三部分内容,容器的状态、容器相关的操作以及容器的生命周期。 63 | #### 容器的状态 64 | 当查询容器的状态时,返回的状态里面至少包含如下信息: 65 | ``` 66 | { 67 | "ociVersion": "0.2.0", 68 | "id": "oci-container1", 69 | "status": "running", 70 | "pid": 4422, 71 | "bundle": "/containers/redis", 72 | "annotations": { 73 | "myKey": "myValue" 74 | } 75 | } 76 | ``` 77 | 78 | * ociVersion (必须): 创建该容器时使用的OCI runtime的版本 79 | * id (必须): 容器ID,本机全局唯一 80 | * status (必须): 容器的运行时状态,包含如下状态: 81 | ``` 82 | creating: 创建中 83 | created: 创建完成 84 | running: 运行中 85 | stopped: 运行结束 86 | 87 | 实现runtime时可以包含更多的状态,但不能改变这几个状态的含义 88 | ``` 89 | * pid (容器是running状态时必须): 容器内第一个进程在系统初始pid namespace中的pid,即在容器外面看到的pid 90 | * bundle (REQUIRED): bundle所在位置的绝对路径。bundle里面包含了容器的配置文件和根文件系统。 91 | * annotations: 容器的注释,相当于容器标签,来自于容器的配置文件,key:value格式。 92 | 93 | #### 容器相关的操作 94 | 该部分定义了一个符合runtime标准的实现(如runc)至少需要实现下面这些命令: 95 | 96 | * state: 返回容器的状态,包含上面介绍的那些内容. 97 | * create: 创建容器,这一步执行完成后,容器创建完成,修改bundle中的config.json将不再对已创建的容器产生影响 98 | * start: 启动容器,执行config.json中process部分指定的进程 99 | * kill: 通过给容器发送信号来停止容器,信号的内容由kill命令的参数指定 100 | * delete: 删除容器,如果容器正在运行中,则删除失败。删除操作会删除掉create操作时创建的所有内容。 101 | 102 | #### 容器的生命周期 103 | 104 | >这里以runc为例,说明容器的生命周期 105 | 106 | 1. 执行命令```runc create```创建容器,参数中指定bundle的位置以及容器的ID,容器的状态变为creating 107 | 2. runc根据bundle中的config.json,准备好容器运行时需要的环境和资源,但不运行process中指定的进程,这步执行完成之后,表示容器创建成功,修改config.json将不再对创建的容器产生影响,这时容器的状态变成created。 108 | 3. 执行命令```runc start```启动容器 109 | 4. runc执行config.json中配置的prestart钩子 110 | 5. runc执行config.json中process指定的程序,这时容器状态变成了running 111 | 6. runc执行poststart钩子。 112 | 7. 容器由于某些原因退出,比如容器中的第一个进程主动退出,挂掉或者被kill掉等。这时容器状态变成了stoped 113 | 8. 执行命令```runc delete```删除容器,这时runc就会删除掉上面第2步所做的所有工作。 114 | 9. runc执行poststop钩子 115 | 116 | ### [Linux Runtime](https://github.com/opencontainers/runtime-spec/blob/master/runtime-linux.md) 117 | 该规范是Linux平台上对[Runtime and Lifecycle](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md)的补充,目前该规范很简单,只要求容器运行起来后,里面必须建立下面这些软连接: 118 | ``` 119 | # ls -l /dev/fd /dev/std* 120 | lrwxrwxrwx 1 root root 13 May 4 12:32 /dev/fd -> /proc/self/fd 121 | lrwxrwxrwx 1 root root 15 May 4 12:32 /dev/stderr -> /proc/self/fd/2 122 | lrwxrwxrwx 1 root root 15 May 4 12:32 /dev/stdin -> /proc/self/fd/0 123 | lrwxrwxrwx 1 root root 15 May 4 12:32 /dev/stdout -> /proc/self/fd/1 124 | ``` 125 | 126 | ## 结束语 127 | 简单点说,docker负责准备runtime的bundle,而runc负责运行该bundle,并管理容器的整个生命周期。 128 | 129 | 但对于docker来说,并不是只要准备好根文件系统和配置文件就可以了,比如对于网络,runtime没有做任何要求,只要在config.json中指定network namespace就行了(不指定就新建一个),而至于这个network namespace里面有哪些东西则完全由docker负责,docker需要保证新network namespace里面有合适的设备来和外界通信。 130 | 131 | ## 参考 132 | 133 | * [Open Container Initiative Runtime Specification](https://github.com/opencontainers/runtime-spec) 134 | -------------------------------------------------------------------------------- /container/docker/006_what_happened_behind_the_docker_create_command.md: -------------------------------------------------------------------------------- 1 | # 走进docker(06):docker create命令背后发生了什么? 2 | 3 | 有了image之后,就可以开始创建并启动容器了,平时我们都是用```docker run```命令直接创建并运行一个容器,它的背后其实包含独立的两步,一步是```docker create```创建容器,另一步是```docker start```启动容器,本篇将先介绍在```docker create```这一步中,docker做了哪些事情。 4 | 5 | 简单点来说,dockerd在收到客户端的创建容器请求后,做了两件事情,一是准备容器需要的layer,二是检查客户端传过来的参数,并和image配置文件中的参数进行合并,然后存储成容器的配置文件。 6 | 7 | ## 创建容器 8 | 创建一个容器用于示例 9 | ``` 10 | #创建一个容器,并取名为docker_test, 11 | #-i是为了让容器能接受用户的输入,-t是指定docker为容器创建一个tty, 12 | #因为ubuntu这个镜像默认启动的进程是bash,而bash需要tty,否则会异常退出 13 | dev@dev:~$ docker create -it --name docker_test ubuntu 14 | 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368 15 | 16 | dev@dev:~$ docker ps -a 17 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 18 | 967438113fba ubuntu "/bin/bash" 6 seconds ago Created docker_test 19 | ``` 20 | 21 | ## layer的元数据 22 | 创建容器时,docker会为每个容器创建两个新的layer,一个是只读的init layer,里面包含docker为容器准备的一些文件,另一个是容器的可写mount layer,以后在容器里面对rootfs的所有增删改操作的结果都会存在这个layer中。 23 | 24 | ``` 25 | # layer的元数据存储在layerdb/mounts/目录下,目录名称就是容器的ID 26 | # 里面包含了三个文件 27 | root@dev:/var/lib/docker/image/aufs/layerdb/mounts/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# ls 28 | init-id mount-id parent 29 | 30 | # mount-id文件包含了mount layer的cacheid 31 | root@dev:/var/lib/docker/image/aufs/layerdb/mounts/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# cat mount-id 32 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 33 | 34 | # init-id文件包含了init layer的cacheid 35 | # init layer的cacheid就是在mount layer的cacheid后面加上了一个“-init” 36 | root@dev:/var/lib/docker/image/aufs/layerdb/mounts/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# cat init-id 37 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init 38 | 39 | # parent里面包含的是image的最上一层layer的chainid 40 | # 表示这个容器的init layer的父layer是image的最顶层layer 41 | root@dev:/var/lib/docker/image/aufs/layerdb/mounts/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# cat parent 42 | sha256:9840d207a275f956f3e634148044e63dc78df511fd72e22d8cb3dad57dc49bf6 43 | 44 | # 可以根据parent的chainid得到它的diffid, 45 | # 这个diffid对应的确实是ubuntu:latest的最顶层layer 46 | # 如何得到image的layer信息请参考上一篇文章: 47 | # 走进docker(05):docker在本地如何管理image(镜像)? 48 | root@dev:/var/lib/docker/image# cat ./aufs/layerdb/sha256/9840d207a275f956f3e634148044e63dc78df511fd72e22d8cb3dad57dc49bf6/diff 49 | sha256:d8b353eb3025c49e029567b2a01e517f7f7d32537ee47e64a7eac19fa68a33f3 50 | ``` 51 | 52 | 新加的这两层layer比较特殊,只保存在layerdb/mounts下面,在layerdb/sha256目录下没有相关信息,说明docker将container的layer和image的layer的元数据放在了不同的两个目录中 53 | 54 | ## layer的数据 55 | container layer的数据和image layer的数据的管理方式是一样的,都存在```/var/lib/docker/```目录下面。 56 | 57 | #### layers目录 58 | 该目录下包含了每个layer的祖先layer的cacheid 59 | ``` 60 | root@dev:/var/lib/docker/aufs# cat ./layers/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init 61 | 7938f2b32c53a9e0d3974f9579dd9dbb450202e1e11fe514e31556d4ea808c4e 62 | 4c10796e21c796a6f3d83eeb3613c566ca9e0fd0a596f4eddf5234b87955b3c8 63 | fd0ba28a44491fd7559c7ffe0597fb1f95b63207a38a3e2680231fb2f6fe92bd 64 | b656bf5f0688069cd90ab230c029fdfeb852afcfd0d1733d087474c86a117da3 65 | 1e83d2ea184e08eed978127311cc96498e319426abe2fb5004d4b1454598bd76 66 | 67 | #从这里可以看出mount layer在init layer的上面 68 | root@dev:/var/lib/docker/aufs# cat ./layers/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 69 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init 70 | 7938f2b32c53a9e0d3974f9579dd9dbb450202e1e11fe514e31556d4ea808c4e 71 | 4c10796e21c796a6f3d83eeb3613c566ca9e0fd0a596f4eddf5234b87955b3c8 72 | fd0ba28a44491fd7559c7ffe0597fb1f95b63207a38a3e2680231fb2f6fe92bd 73 | b656bf5f0688069cd90ab230c029fdfeb852afcfd0d1733d087474c86a117da3 74 | 1e83d2ea184e08eed978127311cc96498e319426abe2fb5004d4b1454598bd76 75 | 76 | #上面的7938f2...就是ubuntu:latest最上一层layer的cacheid 77 | root@dev:~# find /var/lib/docker/image/ -name cache-id|xargs grep 7938f2b32c53a9e0d3974f9579dd9dbb450202e1e11fe514e31556d4ea808c4e 78 | /var/lib/docker/image/aufs/layerdb/sha256/9840d207a275f956f3e634148044e63dc78df511fd72e22d8cb3dad57dc49bf6/cache-id:7938f2b32c53a9e0d3974f9579dd9dbb450202e1e11fe514e31556d4ea808c4e 79 | #9840d2...是ubuntu:latest最上一层layer的chainid 80 | ``` 81 | 82 | #### diff目录 83 | 该目录下存放着每个layer所包含的数据 84 | ``` 85 | #mount layer是新建的供容器写数据layer, 86 | #由于容器还没有运行,所以这里没有任何数据 87 | root@dev:/var/lib/docker/aufs/diff# tree 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 88 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 89 | 90 | 0 directories, 0 files 91 | 92 | #init layer包含了docker为每个容器所预先准备的文件 93 | root@dev:/var/lib/docker/aufs/diff# tree 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init/ 94 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init/ 95 | ├── dev 96 | │   └── console 97 | └── etc 98 | ├── hostname 99 | ├── hosts 100 | ├── mtab -> /proc/mounts 101 | └── resolv.conf 102 | 103 | 2 directories, 5 files 104 | ``` 105 | 106 | init layer里面的文件有什么作用呢?从下面的结果可以看出,除了mtab文件是指向/proc/mounts的软连接之外,其他的都是空的普通文件。 107 | 108 | ``` 109 | root@dev:/var/lib/docker/aufs/diff/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init# ls -l dev 110 | total 0 111 | -rwxr-xr-x 1 root root 0 Jun 25 11:25 console 112 | root@dev:/var/lib/docker/aufs/diff/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init# ls -l etc 113 | total 0 114 | -rwxr-xr-x 1 root root 0 Jun 25 11:25 hostname 115 | -rwxr-xr-x 1 root root 0 Jun 25 11:25 hosts 116 | lrwxrwxrwx 1 root root 12 Jun 25 11:25 mtab -> /proc/mounts 117 | -rwxr-xr-x 1 root root 0 Jun 25 11:25 resolv.conf 118 | ``` 119 | 120 | 这几个文件都是Linux运行时必须的文件,如果缺少的话会导致某些程序或者库出现异常,所以docker需要为容器准备好这些文件: 121 | 122 | * /dev/console: 在Linux主机上,该文件一般指向主机的当前控制台,有些程序会依赖该文件。在容器启动的时候,docker会为容器创建一个pts,然后通过bind mount的方式将pts绑定到容器里面的/dev/console上,这样在容器里面往这个文件里面写东西就相当于往容器的控制台上打印数据。这里创建一个空文件相当于占个坑,作为后续bind mount的目的路径。 123 | * hostname,hosts,resolv.conf:对于每个容器来说,容器内的这几个文件内容都有可能不一样,这里也只是占个坑,等着docker在外面生成这几个文件,然后通过bind mount的方式将这些文件绑定到容器中的这些位置,即这些文件都会被宿主机中的文件覆盖掉。 124 | * /etc/mtab:这个文件在新的Linux发行版中都指向/proc/mounts,里面包含了当前mount namespace中的所有挂载信息,很多程序和库会依赖这个文件。 125 | 126 | >注意: 这里mtab指向的路径是固定的,但内容是变化的,取决于你从哪里打开这个文件,当在宿主机上打开时,是宿主机上/proc/mounts的内容,当启动并进入容器后,在容器中打开看到的就是容器中/proc/mounts的内容。 127 | 128 | #### mnt目录 129 | 里面存放的是经过aufs文件系统mount之后的layer数据,即当前layer和所有的下层layer合并之后的数据,对于aufs文件系统来说,只有在运行容器的时候才会被docker所mount,所以容器没启动的时候,这里看不到任何文件。 130 | ``` 131 | root@dev:/var/lib/docker/aufs/mnt# tree 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 132 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 133 | 134 | 0 directories, 0 files 135 | root@dev:/var/lib/docker/aufs/mnt# tree 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init/ 136 | 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init/ 137 | 138 | 0 directories, 0 files 139 | ``` 140 | 141 | ## 配置文件 142 | docker将用户指定的参数和image配置文件中的部分参数进行合并,然后将合并后生成的容器的配置文件放在/var/lib/docker/containers/下面,目录名称就是容器的ID 143 | ``` 144 | root@dev:/var/lib/docker/containers/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# tree 145 | . 146 | ├── checkpoints 147 | ├── config.v2.json 148 | └── hostconfig.json 149 | 150 | 1 directory, 2 files 151 | ``` 152 | 153 | * config.v2.json: 通用的配置,如容器名称,要执行的命令等 154 | * hostconfig.json: 主机相关的配置,跟操作系统平台有关,如cgroup的配置 155 | * checkpoints: 容器的checkpoint这个功能在当前版本还是experimental状态。 156 | 157 | 这里不详细介绍配置项的内容,后续介绍某些具体配置项的时候再来看这些文件。 158 | 159 | >checkpoints这个功能很强大,可以在当前node做一个checkpoint,然后再到另一个node上继续运行,相当于无缝的将一个正在运行的进程先暂停,然后迁移到另一个node上并继续运行。 160 | 161 | ## 结束语 162 | docker create命令干的活比较少,主要是准备container的layer和配置文件,配置文件中的项比较多,后续会挑一些常用的项进行专门介绍。 163 | 164 | -------------------------------------------------------------------------------- /container/docker/010_how_to_read_code.md: -------------------------------------------------------------------------------- 1 | ## 代码相关 2 | 看代码之前一定要先了解docker背后的相关技术以及思路,文档的更新可能滞后,但代码不会骗人,看代码的目的是为了了解实现细节,以及验证某些想法推测,或者某些部分不太清楚,需要从代码来反推实现思路,对代码熟悉之后,就能更好的处理遇到的docker问题,也可以对docker做定制开发。 3 | 4 | 反过来如果对docker背后的技术一点都不了解,想完全从代码来反推原理是不太现实的,就像不懂操作系统的相关知识而去直接看Linux的内核代码一样,效率很低不说,有可能还看不明白。 5 | 6 | 本人刚开始看代码,所以给不出什么好的建议,还是老老实实一个命令一个命令的来跟踪代码吧。 7 | 8 | >containerd和runc原来都属于docker项目,只是后来由于某些原因独立了出去 9 | 10 | #### docker客户端程序 11 | 代码在[docker](https://github.com/docker/docker)里面,main函数在cmd/docker/docker.go,其命令行的处理用的是github.com/spf13/cobra这个库,稍微了解一下该库对看代码有帮助。 12 | 13 | 如果想直接看命令干了些啥,cli/command下面包含了所有的命令,并且以文件夹进行组织,比如想看看docker run命令都干了些啥,可以直接看cli/command/container/run.go里面的NewRunCommand函数,它里面包含了命令行参数的注册,以及命令的执行函数```RunE: func(cmd *cobra.Command, args []string) error ```,直接看该执行函数的内容就可以了。 14 | 15 | #### dockerd服务器端程序 16 | 代码也在[docker](https://github.com/docker/docker)里面,main函数在cmd/dockerd/docker.go,通过http对外提供服务,router的初始化在cmd/dockerd/daemon.go的initRouter函数中,具体每个router的定义在api/server/router下面的子目录里面,通过router就能找到具体干活的函数。 17 | 18 | #### docker-containerd进程 19 | 代码在[containerd](https://github.com/containerd/containerd)里面,main函数在containerd/main.go,对外通过[grpc](http://www.grpc.io/)提供服务,所以了解下grpc框架就能看懂代码是怎么调的了。 20 | 21 | 如果只想了解一下干了些啥,那么看API接口的定义和实现就可以了,它们分别在api/grpc/types/api.proto(api.pb.go)和api/grpc/server/server.go里面 22 | 23 | #### docker-containerd-shim进程 24 | 代码也在[containerd](https://github.com/containerd/containerd)里面,当需要创建container或者使用exec命令在指定container里面执行程序时,docker-containerd就会启动一个docker-containerd-shim进程,抛开exec命令不谈,可以理解为一个docker-containerd-shim对应一个container 25 | 26 | containerd-shim是一个小程序,负责container运行时这一块的工作,相当于是对runc的一个包装,具体干活还得靠runc,main函数在containerd-shim/main.go。 27 | 28 | #### docker-runc进程 29 | 代码在[runc](https://github.com/docker/runc.git)里面,docker的runc项目克隆自opencontainer的runc项目,应该只有一些细微差别,所以看哪个都可以。 30 | 31 | runc进程由containerd-shim启动,runc启动容器里面的进程后就退出了,由于containerd-shim调用了```syscall.RawSyscall(syscall.SYS_PRCTL, prSetChildSubreaper, uintptr(i), 0)```,所以当runc退出的时候,它的所有子孙进程都变成了containerd-shim的子孙进程,这也是为什么我们通过ps命令看不到runc的原因。从Linux 3.4开始,[prctl](http://man7.org/linux/man-pages/man2/prctl.2.html)增加了对PR_SET_CHILD_SUBREAPER的支持,这样就可以控制孤儿进程可以被谁接管,而不是像以前一样只能由init进程接管。 32 | 33 | -------------------------------------------------------------------------------- /container/namespace/001_namespace_introduction.md: -------------------------------------------------------------------------------- 1 | # Linux Namespace系列(01):Namespace概述 2 | 3 | Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。 4 | 5 | >下面的所有例子都在ubuntu-server-x86_64 16.04下执行通过 6 | 7 | ## Linux内核支持的namespaces 8 | 9 | 目前,Linux内核里面实现了7种不同类型的namespace。 10 | 11 | ``` 12 | 名称 宏定义 隔离内容 13 | Cgroup CLONE_NEWCGROUP Cgroup root directory (since Linux 4.6) 14 | IPC CLONE_NEWIPC System V IPC, POSIX message queues (since Linux 2.6.19) 15 | Network CLONE_NEWNET Network devices, stacks, ports, etc. (since Linux 2.6.24) 16 | Mount CLONE_NEWNS Mount points (since Linux 2.4.19) 17 | PID CLONE_NEWPID Process IDs (since Linux 2.6.24) 18 | User CLONE_NEWUSER User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8) 19 | UTS CLONE_NEWUTS Hostname and NIS domain name (since Linux 2.6.19) 20 | ``` 21 | 22 | >**注意:** 由于Cgroup namespace在4.6的内核中才实现,并且和cgroup v2关系密切,现在普及程度还不高,比如docker现在就还没有用它,所以在namespace这个系列中不会介绍Cgroup namespace。 23 | 24 | ## 查看进程所属的namespaces 25 | 系统中的每个进程都有/proc/[pid]/ns/这样一个目录,里面包含了这个进程所属namespace的信息,里面每个文件的描述符都可以用来作为setns函数(后面会介绍)的参数。 26 | 27 | ```bash 28 | #查看当前bash进程所属的namespace 29 | dev@ubuntu:~$ ls -l /proc/$$/ns 30 | total 0 31 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 cgroup -> cgroup:[4026531835] #(since Linux 4.6) 32 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 ipc -> ipc:[4026531839] #(since Linux 3.0) 33 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 mnt -> mnt:[4026531840] #(since Linux 3.8) 34 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 net -> net:[4026531957] #(since Linux 3.0) 35 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 pid -> pid:[4026531836] #(since Linux 3.8) 36 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 user -> user:[4026531837] #(since Linux 3.8) 37 | lrwxrwxrwx 1 dev dev 0 7月 7 17:24 uts -> uts:[4026531838] #(since Linux 3.0) 38 | ``` 39 | 40 | * 上面每种类型的namespace都是在不同的Linux版本被加入到/proc/[pid]/ns/目录里去的,比如pid namespace是在Linux 3.8才被加入到/proc/[pid]/ns/里面,但这并不是说到3.8才支持pid namespace,其实pid namespace在2.6.24的时候就已经加入到内核了,在那个时候就可以用pid namespace了,只是有了/proc/[pid]/ns/pid之后,使得操作pid namespace更方便了 41 | 42 | * 虽然说cgroup是在Linux 4.6版本才被加入内核,可是在Ubuntu 16.04上,尽管内核版本才4.4,但也支持cgroup namespace,估计应该是Ubuntu将4.6的cgroup namespace这部分代码patch到了他们的4.4内核上。 43 | 44 | * 以ipc:[4026531839]为例,ipc是namespace的类型,4026531839是inode number,如果两个进程的ipc namespace的inode number一样,说明他们属于同一个namespace。这条规则对其他类型的namespace也同样适用。 45 | 46 | * 从上面的输出可以看出,对于每种类型的namespace,进程都会与一个namespace ID关联。 47 | 48 | 49 | ## 跟namespace相关的API 50 | 和namespace相关的函数只有三个,这里简单的看一下,后面介绍[UTS namespace](002_namespace_uts.md)的时候会有详细的示例 51 | 52 | ### [clone](http://man7.org/linux/man-pages/man2/clone.2.html): 创建一个新的进程并把他放到新的namespace中 53 | ```c 54 | int clone(int (*child_func)(void *), void *child_stack 55 | , int flags, void *arg); 56 | 57 | flags: 58 | 指定一个或者多个上面的CLONE_NEW*(当然也可以包含跟namespace无关的flags), 59 | 这样就会创建一个或多个新的不同类型的namespace, 60 | 并把新创建的子进程加入新创建的这些namespace中。 61 | ``` 62 | 63 | ### [setns](http://man7.org/linux/man-pages/man2/setns.2.html): 将当前进程加入到已有的namespace中 64 | ```c 65 | int setns(int fd, int nstype); 66 | 67 | fd: 68 | 指向/proc/[pid]/ns/目录里相应namespace对应的文件, 69 | 表示要加入哪个namespace 70 | 71 | nstype: 72 | 指定namespace的类型(上面的任意一个CLONE_NEW*): 73 | 1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建, 74 | 并通过UNIX domain socket传给当前进程, 75 | 那么就需要通过nstype来指定fd指向的namespace的类型 76 | 2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的, 77 | 那么nstype设置为0即可 78 | ``` 79 | 80 | ### [unshare](http://man7.org/linux/man-pages/man2/unshare.2.html): 使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace) 81 | ```c 82 | int unshare(int flags); 83 | 84 | flags: 85 | 指定一个或者多个上面的CLONE_NEW*, 86 | 这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace 87 | ``` 88 | 89 | ### clone和unshare的区别 90 | clone和unshare的功能都是创建并加入新的namespace, 他们的区别是: 91 | 92 | * unshare是使当前进程加入新的namespace 93 | * clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变 94 | 95 | ## 其它 96 | 当一个namespace中的所有进程都退出时,该namespace将会被销毁。当然还有其他方法让namespace一直存在,假设我们有一个进程号为1000的进程,以ipc namespace为例: 97 | 98 | 1. 通过mount --bind命令。例如mount --bind /proc/1000/ns/ipc /other/file,就算属于这个ipc namespace的所有进程都退出了,只要/other/file还在,这个ipc namespace就一直存在,其他进程就可以利用/other/file,通过setns函数加入到这个namespace 99 | 100 | 2. 在其他namespace的进程中打开/proc/1000/ns/ipc文件,并一直持有这个文件描述符不关闭,以后就可以用setns函数加入这个namespace。 101 | 102 | ## 参考 103 | 104 | * [overview of Linux namespaces](http://man7.org/linux/man-pages/man7/namespaces.7.html) 105 | * [Namespaces in operation, part 1: namespaces overview](https://lwn.net/Articles/531114/) 106 | -------------------------------------------------------------------------------- /container/namespace/003_namespace_ipc.md: -------------------------------------------------------------------------------- 1 | # Linux Namespace系列(03):IPC namespace (CLONE_NEWIPC) 2 | 3 | IPC namespace用来隔离[System V IPC objects](http://man7.org/linux/man-pages/man7/svipc.7.html)和[POSIX message queues](http://man7.org/linux/man-pages/man7/mq_overview.7.html)。其中System V IPC objects包含Message queues、Semaphore sets和Shared memory segments. 4 | 5 | 对于其他几种IPC,下面是我的理解,有可能不对,仅供参考,欢迎指正: 6 | 7 | * signal没必要隔离,因为它和pid密切相关,当pid隔离后,signal自然就隔离了,能不能跨pid namespace发送signal则由pid namespace决定 8 | * pipe好像也没必要隔离,对匿名pipe来说,只能在父子进程之间通讯,所以隔离的意义不大,而命名管道和文件系统有关,所以只要做好文件系统的隔离,命名管道也就隔离了 9 | * socket和协议栈有关,而不同的network namespace有不同的协议栈,所以socket就被network namespace隔离了 10 | 11 | >下面的所有例子都在ubuntu-server-x86_64 16.04下执行通过 12 | 13 | ## namespace相关tool 14 | 从这篇文章开始,不再像介绍UTS namespace那样自己写代码,而是用ubuntu 16.04中现成的两个工具,他们的实现和上一篇文章中介绍UTS namespace时的代码类似,只是多了一些参数处理 15 | 16 | * nsenter:加入指定进程的指定类型的namespace,然后执行参数中指定的命令。详情请参考[帮助文档](http://man7.org/linux/man-pages/man1/nsenter.1.html)和[代码](https://github.com/karelzak/util-linux/blob/master/sys-utils/nsenter.c)。 17 | * unshare:离开当前指定类型的namespace,创建且加入新的namespace,然后执行参数中指定的命令。详情请参考[帮助文档](http://man7.org/linux/man-pages/man1/unshare.1.html)和[代码](https://github.com/karelzak/util-linux/blob/master/sys-utils/unshare.c)。 18 | 19 | ## 示例 20 | 这里将以消息队列为例,演示一下隔离效果,在本例中将用到两个ipc相关的命令 21 | 22 | * ipcmk - 创建shared memory segments, message queues, 和semaphore arrays 23 | * ipcs - 查看shared memory segments, message queues, 和semaphore arrays的相关信息 24 | 25 | 为了使演示更直观,我们在创建新的ipc namespace的时候,同时也创建新的uts namespace,然后为新的utsnamespace设置新hostname,这样就能通过shell提示符一眼看出这是属于新的namespace的bash,后面的文章中也采取这种方式启动新的bash。 26 | 27 | 在这个示例中,我们将用到两个shell窗口 28 | ```bash 29 | #--------------------------第一个shell窗口---------------------- 30 | #记下默认的uts和ipc namespace number 31 | dev@ubuntu:~$ readlink /proc/$$/ns/uts /proc/$$/ns/ipc 32 | uts:[4026531838] 33 | ipc:[4026531839] 34 | 35 | #确认hostname 36 | dev@ubuntu:~$ hostname 37 | ubuntu 38 | 39 | #查看现有的ipc Message Queues,默认情况下没有message queue 40 | dev@ubuntu:~$ ipcs -q 41 | ------ Message Queues -------- 42 | key msqid owner perms used-bytes messages 43 | 44 | #创建一个message queue 45 | dev@ubuntu:~$ ipcmk -Q 46 | Message queue id: 0 47 | dev@ubuntu:~$ ipcs -q 48 | ------ Message Queues -------- 49 | key msqid owner perms used-bytes messages 50 | 0x12aa0de5 0 dev 644 0 0 51 | 52 | 53 | #--------------------------第二个shell窗口---------------------- 54 | #重新打开一个shell窗口,确认和上面的shell是在同一个namespace, 55 | #能看到上面创建的message queue 56 | dev@ubuntu:~$ readlink /proc/$$/ns/uts /proc/$$/ns/ipc 57 | uts:[4026531838] 58 | ipc:[4026531839] 59 | dev@ubuntu:~$ ipcs -q 60 | ------ Message Queues -------- 61 | key msqid owner perms used-bytes messages 62 | 0x12aa0de5 0 dev 644 0 0 63 | 64 | #运行unshare创建新的ipc和uts namespace,并且在新的namespace中启动bash 65 | #这里-i表示启动新的ipc namespace,-u表示启动新的utsnamespace 66 | dev@ubuntu:~$ sudo unshare -iu /bin/bash 67 | root@ubuntu:~# 68 | 69 | #确认新的bash已经属于新的ipc和uts namespace了 70 | root@ubuntu:~# readlink /proc/$$/ns/uts /proc/$$/ns/ipc 71 | uts:[4026532455] 72 | ipc:[4026532456] 73 | 74 | #设置新的hostname以便和第一个shell里面的bash做区分 75 | root@ubuntu:~# hostname container001 76 | root@ubuntu:~# hostname 77 | container001 78 | 79 | #当hostname改变后,bash不会自动修改它的命令行提示符 80 | #所以运行exec bash重新加载bash 81 | root@ubuntu:~# exec bash 82 | root@container001:~# 83 | root@container001:~# hostname 84 | container001 85 | 86 | #现在各个bash进程间的关系如下 87 | #bash(24429)是shell窗口打开时的bash 88 | #bash(27668)是运行sudo unshare创建的bash,和bash(24429)不在同一个namespace 89 | root@container001:~# pstree -pl 90 | ├──sshd(24351)───sshd(24428)───bash(24429)───sudo(27667)───bash(27668)───pstree(27695) 91 | 92 | #查看message queues,看不到原来namespace里面的消息,说明已经被隔离了 93 | root@container001:~# ipcs -q 94 | ------ Message Queues -------- 95 | key msqid owner perms used-bytes messages 96 | 97 | #创建一条新的message queue 98 | root@container001:~# ipcmk -Q 99 | Message queue id: 0 100 | root@container001:~# ipcs -q 101 | ------ Message Queues -------- 102 | key msqid owner perms used-bytes messages 103 | 0x54b08fc2 0 root 644 0 0 104 | 105 | #--------------------------第一个shell窗口---------------------- 106 | #回到第一个shell窗口,看看有没有受到新namespace的影响 107 | dev@ubuntu:~$ ipcs -q 108 | ------ Message Queues -------- 109 | key msqid owner perms used-bytes messages 110 | 0x12aa0de5 0 dev 644 0 0 111 | #完全无影响,还是原来的信息 112 | 113 | #试着加入第二个shell窗口里面bash的uts和ipc namespace 114 | #-t后面跟pid用来指定加入哪个进程所在的namespace 115 | #这里27668是第二个shell中正在运行的bash的pid 116 | #加入成功后将运行/bin/bash 117 | dev@ubuntu:~$ sudo nsenter -t 27668 -u -i /bin/bash 118 | 119 | #加入成功,bash的提示符也自动变过来了 120 | root@container001:~# readlink /proc/$$/ns/uts /proc/$$/ns/ipc 121 | uts:[4026532455] 122 | ipc:[4026532456] 123 | 124 | #显示的是新namespace里的message queues 125 | root@container001:~# ipcs -q 126 | ------ Message Queues -------- 127 | key msqid owner perms used-bytes messages 128 | 0x54b08fc2 0 root 644 0 0 129 | 130 | ``` 131 | 132 | ## 结束语 133 | 上面介绍了IPC namespace和两个常用的跟namespace相关的工具,从演示过程可以看出,IPC namespace差不多和UTS namespace一样简单,没有太复杂的逻辑,也没有父子namespace关系。不过后续将要介绍的其他namespace就要比这个复杂多了。 -------------------------------------------------------------------------------- /container/namespace/004_namespace_mount.md: -------------------------------------------------------------------------------- 1 | # Linux Namespace系列(04):mount namespaces (CLONE_NEWNS) 2 | 3 | Mount namespace用来隔离文件系统的挂载点, 使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。 4 | 5 | 当前进程所在mount namespace里的所有挂载信息可以在/proc/[pid]/mounts、/proc/[pid]/mountinfo和/proc/[pid]/mountstats里面找到。 6 | 7 | Mount namespaces是第一个被加入Linux的namespace,由于当时没想到还会引入其它的namespace,所以取名为CLONE_NEWNS,而没有叫CLONE_NEWMOUNT。 8 | 9 | 每个mount namespace都拥有一份自己的挂载点列表,当用clone或者unshare函数创建新的mount namespace时,新创建的namespace将拷贝一份老namespace里的挂载点列表,但从这之后,他们就没有关系了,通过mount和umount增加和删除各自namespace里面的挂载点都不会相互影响。 10 | 11 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 12 | 13 | ## 演示 14 | 15 | ```bash 16 | #--------------------------第一个shell窗口---------------------- 17 | #先准备两个iso文件,用于后面的mount测试 18 | dev@ubuntu:~$ mkdir iso 19 | dev@ubuntu:~$ cd iso/ 20 | dev@ubuntu:~/iso$ mkdir -p iso01/subdir01 21 | dev@ubuntu:~/iso$ mkdir -p iso02/subdir02 22 | dev@ubuntu:~/iso$ mkisofs -o ./001.iso ./iso01 23 | dev@ubuntu:~/iso$ mkisofs -o ./002.iso ./iso02 24 | dev@ubuntu:~/iso$ ls 25 | 001.iso 002.iso iso01 iso02 26 | #准备目录用于mount 27 | dev@ubuntu:~/iso$ sudo mkdir /mnt/iso1 /mnt/iso2 28 | 29 | #查看当前所在的mount namespace 30 | dev@ubuntu:~/iso$ readlink /proc/$$/ns/mnt 31 | mnt:[4026531840] 32 | 33 | #mount 001.iso 到 /mnt/iso1/ 34 | dev@ubuntu:~/iso$ sudo mount ./001.iso /mnt/iso1/ 35 | mount: /dev/loop1 is write-protected, mounting read-only 36 | 37 | #mount成功 38 | dev@ubuntu:~/iso$ mount |grep /001.iso 39 | /home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime) 40 | 41 | #创建并进入新的mount和uts namespace 42 | dev@ubuntu:~/iso$ sudo unshare --mount --uts /bin/bash 43 | #更改hostname并重新加载bash 44 | root@ubuntu:~/iso# hostname container001 45 | root@ubuntu:~/iso# exec bash 46 | root@container001:~/iso# 47 | 48 | #查看新的mount namespace 49 | root@container001:~/iso# readlink /proc/$$/ns/mnt 50 | mnt:[4026532455] 51 | 52 | #老namespace里的挂载点的信息已经拷贝到新的namespace里面来了 53 | root@container001:~/iso# mount |grep /001.iso 54 | /home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime) 55 | 56 | #在新namespace中mount 002.iso 57 | root@container001:~/iso# mount ./002.iso /mnt/iso2/ 58 | mount: /dev/loop0 is write-protected, mounting read-only 59 | root@container001:~/iso# mount |grep iso 60 | /home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime) 61 | /home/dev/iso/002.iso on /mnt/iso2 type iso9660 (ro,relatime) 62 | 63 | #umount 001.iso 64 | root@container001:~/iso# umount /mnt/iso1 65 | root@container001:~/iso# mount |grep iso 66 | /home/dev/iso/002.iso on /mnt/iso2 type iso9660 (ro,relatime) 67 | 68 | #/mnt/iso1目录变为空 69 | root@container001:~/iso# ls /mnt/iso1 70 | root@container001:~/iso# 71 | 72 | #--------------------------第二个shell窗口---------------------- 73 | #打开新的shell窗口,老namespace中001.iso的挂载信息还在 74 | #而在新namespace里面mount的002.iso这里看不到 75 | dev@ubuntu:~$ mount |grep iso 76 | /home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime) 77 | #iso1目录里面也有内容 78 | dev@ubuntu:~$ ls /mnt/iso1 79 | subdir01 80 | #说明两个namespace中的mount信息是隔离的 81 | ``` 82 | 83 | ## Shared subtrees 84 | 在某些情况下,比如系统添加了一个新的硬盘,这个时候如果mount namespace是完全隔离的,想要在各个namespace里面用这个硬盘,就需要在每个namespace里面手动mount这个硬盘,这个是很麻烦的,这时[Shared subtrees](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt)就可以帮助我们解决这个问题。 85 | 86 | 关于Shared subtrees的详细介绍请参考[Linux mount (第二部分)](/linux/003_mount_02.md),里面有他的详细介绍以及bind nount的例子。 87 | 88 | ### 演示 89 | 对Shared subtrees而言,mount namespace和bind mount的情况差不多,这里就简单演示一下shared和private两种类型 90 | ```bash 91 | #--------------------------第一个shell窗口---------------------- 92 | #准备4个虚拟的disk,并在上面创建ext2文件系统,用于后续的mount测试 93 | dev@ubuntu:~/iso$ cd && mkdir disks && cd disks 94 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk1.img 95 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk2.img 96 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk3.img 97 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk4.img 98 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk1.img 99 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk2.img 100 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk3.img 101 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk4.img 102 | #准备两个目录用于挂载上面创建的disk 103 | dev@ubuntu:~/disks$ mkdir disk1 disk2 104 | dev@ubuntu:~/disks$ ls 105 | disk1 disk1.img disk2 disk2.img disk3.img disk4.img 106 | 107 | 108 | #显式的分别以shared和private方式挂载disk1和disk2 109 | dev@ubuntu:~/disks$ sudo mount --make-shared ./disk1.img ./disk1 110 | dev@ubuntu:~/disks$ sudo mount --make-private ./disk2.img ./disk2 111 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 112 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 113 | 173 24 7:2 / /home/dev/disks/disk2 rw,relatime 114 | 115 | #查看mount namespace编号 116 | dev@ubuntu:~/disks$ readlink /proc/$$/ns/mnt 117 | mnt:[4026531840] 118 | 119 | #--------------------------第二个shell窗口---------------------- 120 | #重新打开一个新的shell窗口 121 | dev@ubuntu:~$ cd ./disks 122 | #创建新的mount namespace 123 | #默认情况下,unshare会将新namespace里面的所有挂载点的类型设置成private, 124 | #所以这里用到了参数--propagation unchanged, 125 | #让新namespace里的挂载点的类型和老namespace里保持一致。 126 | #--propagation参数还支持private|shared|slave类型, 127 | #和mount命令的那些--make-private参数一样, 128 | #他们的背后都是通过调用mount(...)函数传入不同的参数实现的 129 | dev@ubuntu:~/disks$ sudo unshare --mount --uts --propagation unchanged /bin/bash 130 | root@ubuntu:~/disks# hostname container001 131 | root@ubuntu:~/disks# exec bash 132 | root@container001:~/disks# 133 | 134 | #确认已经是在新的mount namespace里面了 135 | root@container001:~/disks# readlink /proc/$$/ns/mnt 136 | mnt:[4026532463] 137 | 138 | #由于前面指定了--propagation unchanged, 139 | #所以新namespace里面的/home/dev/disks/disk1也是shared, 140 | #且和老namespace里面的/home/dev/disks/disk1属于同一个peer group 105 141 | #因为在不同的namespace里面,所以这里挂载点的ID和原来namespace里的不一样了 142 | root@container001:~/disks# cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 143 | 221 177 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 144 | 222 177 7:2 / /home/dev/disks/disk2 rw,relatime 145 | 146 | #分别在disk1和disk2目录下创建disk3和disk4,然后挂载disk3,disk4到这两个目录 147 | root@container001:~/disks# mkdir ./disk1/disk3 ./disk2/disk4 148 | root@container001:~/disks# mount ./disk3.img ./disk1/disk3/ 149 | root@container001:~/disks# mount ./disk4.img ./disk2/disk4/ 150 | root@container001:~/disks# cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 151 | 221 177 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 152 | 222 177 7:2 / /home/dev/disks/disk2 rw,relatime 153 | 223 221 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107 154 | 227 222 7:4 / /home/dev/disks/disk2/disk4 rw,relatime 155 | 156 | #--------------------------第一个shell窗口---------------------- 157 | #回到第一个shell窗口 158 | 159 | #可以看出由于/home/dev/disks/disk1是shared,且两个namespace里的这个挂载点都属于peer group 105, 160 | #所以在新namespace里面挂载的disk3,在老的namespace里面也看的到 161 | #但是看不到disk4的挂载信息,那是因为/home/dev/disks/disk2是private的 162 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 163 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 164 | 173 24 7:2 / /home/dev/disks/disk2 rw,relatime 165 | 224 164 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107 166 | 167 | #我们可以随时修改挂载点的propagation type 168 | #这里我们通过mount命令将disk3改成了private类型 169 | dev@ubuntu:~/disks$ sudo mount --make-private /home/dev/disks/disk1/disk3 170 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk3| sed 's/ - .*//' 171 | 224 164 7:3 / /home/dev/disks/disk1/disk3 rw,relatime 172 | 173 | #--------------------------第二个shell窗口---------------------- 174 | #回到第二个shell窗口,disk3的propagation type还是shared, 175 | #表明在老的namespace里面对propagation type的修改不会影响新namespace里面的挂载点 176 | root@container001:~/disks# cat /proc/self/mountinfo |grep disk3| sed 's/ - .*//' 177 | 223 221 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107 178 | 179 | ``` 180 | 181 | 182 | 关于mount命令和mount namespace的配合,里面有很多技巧,后面如果需要用到更复杂的用法,会再做详细的介绍。 183 | 184 | ## 参考 185 | * [kernel:Shared Subtrees](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) 186 | * [lwn:Shared subtrees](https://lwn.net/Articles/159077/) 187 | * [lwn:Mount namespaces, mount propagation, and unbindable mounts](https://lwn.net/Articles/690679/) 188 | * [lwn:Mount namespaces and shared subtrees](https://lwn.net/Articles/689856/) -------------------------------------------------------------------------------- /container/namespace/006_namespace_network.md: -------------------------------------------------------------------------------- 1 | # Linux Namespace系列(06):network namespace (CLONE_NEWNET) 2 | 3 | network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。 4 | 5 | 每个新的network namespace默认有一个本地环回接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。 6 | 7 | 当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起 8 | 9 | 标记为"local devices"的设备不能从一个namespace移动到另一个namespace,比如loopback, bridge, ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。 10 | ```bash 11 | #这里“on”表示该设备不能被移动到其他network namespace 12 | dev@ubuntu:~$ ethtool -k lo|grep netns-local 13 | netns-local: on [fixed] 14 | ``` 15 | 16 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 17 | 18 | ## 示例 19 | 20 | 本示例将演示如何创建新的network namespace并同外面的namespace进行通信。 21 | 22 | ```bash 23 | #--------------------------第一个shell窗口---------------------- 24 | #记录默认network namespace ID 25 | dev@ubuntu:~$ readlink /proc/$$/ns/net 26 | net:[4026531957] 27 | 28 | #创建新的network namespace 29 | dev@ubuntu:~$ sudo unshare --uts --net /bin/bash 30 | root@ubuntu:~# hostname container001 31 | root@ubuntu:~# exec bash 32 | root@container001:~# readlink /proc/$$/ns/net 33 | net:[4026532478] 34 | 35 | #运行ifconfig啥都没有 36 | root@container001:~# ifconfig 37 | root@container001:~# 38 | 39 | #启动lo (这里不详细介绍ip这个tool的用法,请参考man ip) 40 | root@container001:~# ip link set lo up 41 | root@container001:~# ifconfig 42 | lo Link encap:Local Loopback 43 | inet addr:127.0.0.1 Mask:255.0.0.0 44 | inet6 addr: ::1/128 Scope:Host 45 | UP LOOPBACK RUNNING MTU:65536 Metric:1 46 | RX packets:0 errors:0 dropped:0 overruns:0 frame:0 47 | TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 48 | collisions:0 txqueuelen:1 49 | RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) 50 | 51 | root@container001:~# ping 127.0.0.1 52 | PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data. 53 | 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.070 ms 54 | 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.015 ms 55 | 56 | #获取当前bash进程的PID 57 | root@container001:~# echo $$ 58 | 15812 59 | 60 | #--------------------------第二个shell窗口---------------------- 61 | #创建新的虚拟以太网设备,让两个namespace能通讯 62 | dev@ubuntu:~$ sudo ip link add veth0 type veth peer name veth1 63 | 64 | #将veth1移动到上面第一个窗口中的namespace 65 | #这里15812是上面bash的PID 66 | dev@ubuntu:~$ sudo ip link set veth1 netns 15812 67 | 68 | #为veth0分配IP并启动veth0 69 | dev@ubuntu:~$ sudo ip address add dev veth0 192.168.8.1/24 70 | dev@ubuntu:~$ sudo ip link set veth0 up 71 | dev@ubuntu:~$ ifconfig veth0 72 | veth0 Link encap:Ethernet HWaddr 9a:4d:d5:96:b5:36 73 | inet addr:192.168.8.1 Bcast:0.0.0.0 Mask:255.255.255.0 74 | inet6 addr: fe80::984d:d5ff:fe96:b536/64 Scope:Link 75 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 76 | RX packets:8 errors:0 dropped:0 overruns:0 frame:0 77 | TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 78 | collisions:0 txqueuelen:1000 79 | RX bytes:648 (648.0 B) TX bytes:648 (648.0 B) 80 | 81 | #--------------------------第一个shell窗口---------------------- 82 | #为veth1分配IP地址并启动它 83 | root@container001:~# ip address add dev veth1 192.168.8.2/24 84 | root@container001:~# ip link set veth1 up 85 | root@container001:~# ifconfig veth1 86 | veth1 Link encap:Ethernet HWaddr 6a:dc:59:79:3c:8b 87 | inet addr:192.168.8.2 Bcast:0.0.0.0 Mask:255.255.255.0 88 | inet6 addr: fe80::68dc:59ff:fe79:3c8b/64 Scope:Link 89 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 90 | RX packets:8 errors:0 dropped:0 overruns:0 frame:0 91 | TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 92 | collisions:0 txqueuelen:1000 93 | RX bytes:648 (648.0 B) TX bytes:648 (648.0 B) 94 | 95 | #连接成功 96 | root@container001:~# ping 192.168.8.1 97 | PING 192.168.8.1 (192.168.8.1) 56(84) bytes of data. 98 | 64 bytes from 192.168.8.1: icmp_seq=1 ttl=64 time=0.098 ms 99 | 64 bytes from 192.168.8.1: icmp_seq=2 ttl=64 time=0.023 ms 100 | ``` 101 | 102 | 到目前为止,两个namespace之间可以网络通信了,但在container001里还是不能访问外网。下面将通过NAT的方式让container001能够上外网。这部分内容完全是网络相关的知识,跟namespace已经没什么关系了。 103 | 104 | ```bash 105 | #--------------------------第二个shell窗口---------------------- 106 | #回到上面示例中的第二个窗口 107 | 108 | #确认IP forward是否已经开通,这里1表示开通了 109 | #如果你的机器上是0,请运行这个命令将它改为1: sudo sysctl -w net.ipv4.ip_forward=1 110 | dev@ubuntu:~$ cat /proc/sys/net/ipv4/ip_forward 111 | 1 112 | 113 | #添加NAT规则,这里ens32是机器上连接外网的网卡 114 | #关于iptables和nat都比较复杂,这里不做解释 115 | dev@ubuntu:~$ sudo iptables -t nat -A POSTROUTING -o ens32 -j MASQUERADE 116 | 117 | #--------------------------第一个shell窗口---------------------- 118 | #回到第一个窗口,添加默认网关 119 | root@container001:~# ip route add default via 192.168.8.1 120 | root@container001:~# route -n 121 | Kernel IP routing table 122 | Destination Gateway Genmask Flags Metric Ref Use Iface 123 | 0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 veth1 124 | 192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1 125 | 126 | #这样就可以访问外网了 127 | #由于测试环境的限制,所以采用下面的方式检测网络是否畅通 128 | #如果网络没有什么限制的话,随便ping一个外部的IP测试就可以了 129 | root@container001:~# curl -I www.google.com 130 | HTTP/1.1 200 OK 131 | Date: Fri, 15 Jul 2016 08:12:03 GMT 132 | 133 | ``` 134 | 135 | network namespace的概念比较简单,但如何做好网络的隔离和连通却比较难,包括性能和安全相关的考虑,需要很好的Linux网络知识。后续在介绍docker网络管理的时候会对Linux网络做一个更详细的介绍。 136 | 137 | ## ip netns 138 | 在单独操作network namespace时,ip netns是一个很方便的工具,并且它可以给namespace取一个名字,然后根据名字来操作namespace。那么给namespace取名字并且根据名字来管理namespace里面的进程是怎么实现的呢?请看下面的脚本(也可以直接看它的[源代码](https://github.com/shemminger/iproute2/blob/master/ip/ipnetns.c)): 139 | ```bash 140 | #开始之前,获取一下默认network namespace的ID 141 | dev@ubuntu:~$ readlink /proc/$$/ns/net 142 | net:[4026531957] 143 | 144 | #创建一个用于绑定network namespace的文件, 145 | #ip netns将所有的文件放到了目录/var/run/netns下, 146 | #所以我们这里重用这个目录,并且创建一个我们自己的文件netnamespace1 147 | dev@ubuntu:~$ sudo mkdir -p /var/run/netns 148 | dev@ubuntu:~$ sudo touch /var/run/netns/netnamespace1 149 | 150 | #创建新的network namespace,并在新的namespace中启动新的bash 151 | dev@ubuntu:~$ sudo unshare --net bash 152 | #查看新的namespace ID 153 | root@ubuntu:~# readlink /proc/$$/ns/net 154 | net:[4026532448] 155 | 156 | #bind当前bash的namespace文件到上面创建的文件上 157 | root@ubuntu:~# mount --bind /proc/$$/ns/net /var/run/netns/netnamespace1 158 | #通过ls -i命令可以看到文件netnamespace1的inode号和namespace的编号相同,说明绑定成功 159 | root@ubuntu:~# ls -i /var/run/netns/netnamespace1 160 | 4026532448 /var/run/netns/netnamespace1 161 | 162 | #退出新创建的bash 163 | root@ubuntu:~# exit 164 | exit 165 | #可以看出netnamespace1的inode没变,说明我们使用了bind mount后 166 | #虽然新的namespace中已经没有进程了,但这个新的namespace还存在 167 | dev@ubuntu:~$ ls -i /var/run/netns/netnamespace1 168 | 4026532448 /var/run/netns/netnamespace1 169 | 170 | #上面的这一系列操作等同于执行了命令: ip netns add netnamespace1 171 | #下面的nsenter命令等同于执行了命令: ip netns exec netnamespace1 bash 172 | 173 | #我们可以通过nsenter命令再创建一个新的bash,并将它加入netnamespace1所关联的namespace(net:[4026532448]) 174 | dev@ubuntu:~$ sudo nsenter --net=/var/run/netns/netnamespace1 bash 175 | root@ubuntu:~# readlink /proc/$$/ns/net 176 | net:[4026532448] 177 | ``` 178 | 179 | 从上面可以看出,给namespace取名字其实就是创建一个文件,然后通过mount --bind将新创建的namespace文件和该文件绑定,就算该namespace里的所有进程都退出了,内核还是会保留该namespace,以后我们还可以通过这个绑定的文件来加入该namespace。 180 | 181 | 通过这种办法,我们也可以给其他类型的namespace取名字(有些类型的 namespace可能有些特殊,本人没有一个一个的试过)。 182 | 183 | ## 参考 184 | * [Namespaces in operation, part 7: Network namespaces](https://lwn.net/Articles/580893/) -------------------------------------------------------------------------------- /container/namespace/009_create_simple_container.md: -------------------------------------------------------------------------------- 1 | # Linux Namespace系列(09):利用Namespace创建一个简单可用的容器 2 | 3 | 本文将演示如何利用namespace创建一个完整的容器,并在里面运行busybox。如果对namespace不是很熟悉,请先参考前面几遍介绍不同类型namespace的文章。 4 | 5 | busybox是一个Linux下的可执行程序,采用静态链接,不依赖于其他任何动态库。他里面实现了一些Linux下常用的命令,如ls,hostname,date,ps,mount等等,详细的介绍请参考它的[官网](https://busybox.net/)。 6 | 7 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 8 | 9 | ## 准备container的根目录 10 | ```bash 11 | #创建一个单独的目录,后续所有的操作都在该目录下进行,目录名称无特殊要求 12 | dev@ubuntu:~$ mkdir chroot && cd chroot 13 | #下载编译好的busybox可执行文件 14 | dev@ubuntu:~/chroot$ wget https://busybox.net/downloads/binaries/1.21.1/busybox-x86_64 15 | #创建new_root/bin目录,new_root将会是新容器的根目录,bin目录用来放busybox 16 | #由于/bin默认就在PATH中,所以里面放的程序都可以直接在shell里面执行,不需要带完整的路径 17 | dev@ubuntu:~/chroot$ mkdir -p new_root/bin 18 | dev@ubuntu:~/chroot$ chmod +x ./busybox-x86_64 19 | 20 | #将busybox-x86_64移到bin目录下,并重命名为busybox 21 | dev@ubuntu:~/chroot$ mv busybox-x86_64 new_root/bin/busybox 22 | #运行ls试试,确保busybox能正常工作 23 | dev@ubuntu:~/chroot$ ./new_root/bin/busybox ls 24 | new_root 25 | 26 | #安装busybox到bin目录,不安装的话每次执行ls命令都需要使用上面那种格式: busybox ls 27 | #安装之后就会创建一个ls到busybox的硬链接,这样执行ls的时候就不用再输入前面的busybox了 28 | dev@ubuntu:~/chroot$ ./new_root/bin/busybox --install ./new_root/bin/ 29 | 30 | #运行下bin下面的ls,确保安装成功 31 | dev@ubuntu:~/chroot$ ls -l ./new_root/bin/ls 32 | -rwxrwxr-x 348 dev dev 973200 7月 9 2013 ./new_root/bin/ls 33 | dev@ubuntu:~/chroot$ ./new_root/bin/ls 34 | new_root 35 | 36 | #使用chroot命令,切换根目录 37 | dev@ubuntu:~/chroot$ sudo chroot ./new_root/ sh 38 | 39 | #切换成功,由于new_root下面只有busybox,没有任何配置文件, 40 | #所以shell的提示符里只包含当前目录 41 | #尝试运行几个命令,一切正常 42 | / # ls 43 | bin 44 | / # which ls 45 | /bin/ls 46 | / # hostname 47 | ubuntu 48 | / # id 49 | uid=0 gid=0 groups=0 50 | / # exit 51 | dev@ubuntu:~/chroot$ 52 | ``` 53 | 54 | ## 创建容器并做相关配置 55 | ```bash 56 | #新建/data目录用来在主机和容器之间共享数据 57 | dev@ubuntu:~/chroot$ sudo mkdir /data 58 | dev@ubuntu:~/chroot$ sudo chown dev:dev /data 59 | dev@ubuntu:~/chroot$ touch /data/001 60 | 61 | #创建新的容器,指定所有namespace相关的参数, 62 | #这里--propagation private是为了让容器里的mount point都变成private的, 63 | #这是因为pivot_root命令需要原来根目录的挂载点为private, 64 | #只有我们需要在host和container之间共享挂载信息的时候,才需要使用shared或者slave类型 65 | dev@ubuntu:~/chroot$ unshare --user --mount --ipc --pid --net --uts -r --fork --propagation private bash 66 | #设置容器的主机名 67 | root@ubuntu:~/chroot# hostname container01 68 | root@ubuntu:~/chroot# exec bash 69 | 70 | #创建old_root用于pivot_root命令,创建data目录用于绑定/data目录 71 | root@container01:~/chroot# mkdir -p ./new_root/old_root/ ./new_root/data/ 72 | 73 | #由于pivot_root命令要求老的根目录和新的根目录不能在同一个挂载点下, 74 | #所以这里利用bind mount,在原地创建一个新的挂载点 75 | root@container01:~/chroot# mount --bind ./new_root/ ./new_root/ 76 | 77 | #将/data目录绑定到new_root/data,这样pivot_root后,就能访问/data下的东西了 78 | root@container01:~/chroot# mount --bind /data ./new_root/data 79 | 80 | #进入new_root目录,然后切换根目录 81 | root@container01:~/chroot# cd new_root/ 82 | root@container01:~/chroot/new_root# pivot_root ./ ./old_root/ 83 | 84 | #但shell提示符里显示的当前目录还是原来的目录,没有切换到‘/’下, 85 | #这是因为当前运行的shell还是host里面的bash 86 | root@container01:~/chroot/new_root# ls 87 | bin data old_root 88 | 89 | #重新加载new_root下面的shell,这样contianer和host就没有关系了, 90 | #从shell提示符中可以看出,当前目录已经变成了‘/’ 91 | root@container01:~/chroot/new_root# exec sh 92 | / # 93 | #由于没有/etc目录,也就没有相关的profile,于是shell的提示符里面只包含当前路径。 94 | 95 | #设置PS1环境变量,让shell提示符好看点,这里直接写了root在提示符里面, 96 | #是因为我们新的container里面没有账号相关的配置文件, 97 | #虽然系统知道当前账号的ID是0,但不知道账号的用户名是什么。 98 | / # export PS1='root@$(hostname):$(pwd)# ' 99 | root@container01:/# 100 | 101 | #没有/etc目录,没有user相关的配置文件,所以不知道ID为0的用户名是什么 102 | root@container01:/# whoami 103 | whoami: unknown uid 0 104 | 105 | #mount命令依赖于/proc目录,所以这里mount操作失败 106 | root@container01:/# mount 107 | mount: no /proc/mounts 108 | 109 | #重新mount /proc 110 | root@container01:/# mkdir /proc 111 | root@container01:/# mount -t proc none /proc 112 | 113 | #这时可以看到所有的mount信息了,从host复制过来的mount信息都挂载在/old_root目录下 114 | root@container01:/# mount 115 | /dev/mapper/ubuntu--vg-root on /old_root type ext4 (rw,relatime,errors=remount-ro,data=ordered) 116 | udev on /old_root/dev type devtmpfs (rw,nosuid,relatime,size=1005080k,nr_inodes=251270,mode=755) 117 | devpts on /old_root/dev/pts type devpts (rw,nosuid,noexec,relatime,mode=600,ptmxmode=000) 118 | tmpfs on /old_root/dev/shm type tmpfs (rw,nosuid,nodev) 119 | ...... 120 | /dev/mapper/ubuntu--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered) 121 | /dev/mapper/ubuntu--vg-root on /data type ext4 (rw,relatime,errors=remount-ro,data=ordered) 122 | none on /proc type proc (rw,nodev,relatime) 123 | 124 | #umount掉/old_root下的所有mount point 125 | root@container01:/# umount -l /old_root 126 | #这时候就只剩下根目录,/proc,/data三个挂载点了 127 | root@container01:/# mount 128 | /dev/mapper/ubuntu--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered) 129 | /dev/mapper/ubuntu--vg-root on /data type ext4 (rw,relatime,errors=remount-ro,data=ordered) 130 | none on /proc type proc (rw,nodev,relatime) 131 | 132 | #试试cd命令,提示失败, $HOME还是指向老的/home/dev, 133 | #除了$HOME之外,还有其他一些环境变量也有同样的问题。 134 | #这主要是由于我们新的container中缺少配置文件,导致很多环境变量没有更新。 135 | root@container01:/# cd 136 | sh: cd: can not cd to /home/dev 137 | 138 | #试试ps,显示的是container里面启动的进程 139 | root@container01:/# ps 140 | PID USER TIME COMMAND 141 | 1 0 0:00 sh 142 | 55 0 0:00 ps 143 | 144 | #touch文件001成功 145 | root@container01:/# touch /data/001 146 | #新创建一个002文件 147 | root@container01:/# touch /data/002 148 | root@container01:/# ls /data 149 | 001 002 150 | 151 | #退出contianer01,在/data目录能看到我们上面在container01种创建的002文件 152 | root@container01:/# exit 153 | dev@ubuntu:~/chroot$ ls /data 154 | 001 002 155 | ``` 156 | 157 | ## 结束语 158 | 159 | 本文利用busybox和pivot_root演示了如何创建一个简单的容器,并且实现了在host和container之间共享文件夹。这个容器的功能非常简单,很多目录都没有构建,导致只能运行busybox里面的部分命令,有些命令运行时会有异常。要想构造一个完整易用的容器,还需要很多工作要做,这里只演示了冰山一角,在后续的“docker系列”中,将深入分析docker是如何一步一步帮助我们构建安全易用的contianer的,敬请期待。 160 | 161 | ## 参考 162 | * [Video : Cgroups, namespaces, and beyond: what are containers made from?](https://www.youtube.com/watch?v=sK5i-N34im8) 163 | -------------------------------------------------------------------------------- /devkit/python.vimrc: -------------------------------------------------------------------------------- 1 | set nocompatible " be iMproved, required 2 | filetype off " required 3 | 4 | " set the runtime path to include Vundle and initialize 5 | set rtp+=~/.vim/bundle/Vundle.vim 6 | call vundle#begin() 7 | " alternatively, pass a path where Vundle should install plugins 8 | "call vundle#begin('~/some/path/here') 9 | 10 | " let Vundle manage Vundle, required 11 | Plugin 'VundleVim/Vundle.vim' 12 | 13 | " The following are examples of different formats supported. 14 | " Keep Plugin commands between vundle#begin/end. 15 | " plugin on GitHub repo 16 | 17 | Plugin 'davidhalter/jedi-vim' 18 | Plugin 'kien/ctrlp.vim' 19 | Plugin 'SirVer/ultisnips' 20 | " Syntax Checking 21 | Plugin 'w0rp/ale' 22 | " Code Formatter 23 | " Plugin 'google/yapf' 24 | Plugin 'scrooloose/nerdcommenter' 25 | 26 | " plugin from http://vim-scripts.org/vim/scripts.html 27 | " Plugin 'L9' 28 | " Git plugin not hosted on GitHub 29 | " Plugin 'git://git.wincent.com/command-t.git' 30 | " git repos on your local machine (i.e. when working on your own plugin) 31 | " Plugin 'file:///home/gmarik/path/to/plugin' 32 | " The sparkup vim script is in a subdirectory of this repo called vim. 33 | " Pass the path to set the runtimepath properly. 34 | " Plugin 'rstacruz/sparkup', {'rtp': 'vim/'} 35 | " Install L9 and avoid a Naming conflict if you've already installed a 36 | " different version somewhere else. 37 | " Plugin 'ascenator/L9', {'name': 'newL9'} 38 | 39 | " All of your Plugins must be added before the following line 40 | call vundle#end() " required 41 | filetype plugin indent on " required 42 | " To ignore plugin indent changes, instead use: 43 | "filetype plugin on 44 | " 45 | " Brief help 46 | " :PluginList - lists configured plugins 47 | " :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate 48 | " :PluginSearch foo - searches for foo; append `!` to refresh local cache 49 | " :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal 50 | " 51 | " see :h vundle for more details or wiki for FAQ 52 | " Put your non-Plugin stuff after this line 53 | 54 | syntax on 55 | let mapleader=',' "leader setting 56 | set nu 57 | set hlsearch 58 | 59 | set linespace=4 60 | set tabstop=4 61 | set shiftwidth=4 62 | set softtabstop=4 63 | set expandtab 64 | set autoindent 65 | 66 | set backspace=indent,eol,start "backspace over pretty much anything 67 | 68 | let g:UltiSnipsExpandTrigger = '' -------------------------------------------------------------------------------- /devkit/sublime.md: -------------------------------------------------------------------------------- 1 | # 配置sublime 2 | 3 | ## 安装 4 | 5 | 1. 首先[下载安装 sublime 3](https://www.sublimetext.com/3) 6 | 2. [安装 packagecontrol](https://packagecontrol.io/installation) 7 | 8 | ## 安装通用插件 9 | * [SideBarEnhancements](https://packagecontrol.io/packages/SideBarEnhancements) 10 | * [word count](https://packagecontrol.io/packages/LaTeX%20Word%20Count): Control + Shift + C 11 | 12 | ## markdown 13 | 格式介绍: 14 | [Mastering Markdown](https://guides.github.com/features/mastering-markdown/) 15 | [Basic writing and formatting syntax](https://help.github.com/articles/basic-writing-and-formatting-syntax/) -------------------------------------------------------------------------------- /devkit/vim.md: -------------------------------------------------------------------------------- 1 | ## 安装docker 2 | 3 | 参考官网 4 | 5 | ## 配置容器 6 | 7 | 1. 启动容器 8 | ``` 9 | $ docker run -dit -v /container/shared/:/shared --name dev -h dev debian 10 | 4ed302ce79dc950823f498833f24627f9474da2f89fbab0b5195159d685e283c 11 | $ docker exec -it dev bash 12 | root@dev:/# 13 | ``` 14 | 15 | 2. 配置Debian源 16 | ``` 17 | # 先安装vim 18 | root@dev:/# apt-get update 19 | # vim-nox包含了对python, ruby的支持 20 | root@dev:/# apt-get install vim-nox 21 | 22 | # 配置软件源,这样下次安装东西的时候快一些 23 | root@dev:/# vim /etc/apt/sources.list 24 | # 使用这个源:http://mirrors.ustc.edu.cn/debian 25 | root@dev:/# apt-get update 26 | ``` 27 | 28 | 3. 安装一些必须的软件 29 | ``` 30 | root@dev:/# apt-get install git curl 31 | ``` 32 | 33 | ## python 34 | 35 | 1. 创建一个python账户 36 | ``` 37 | root@dev:/# adduser python 38 | root@dev:/# su - python 39 | python@dev:~$ 40 | ``` 41 | 42 | 2. 安装一些必须的软件 43 | ``` 44 | root@dev:/# apt-get python3 45 | ``` 46 | 47 | 3. 安装vundle 48 | ``` 49 | # 安装vundle 50 | python@dev:~$ git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim 51 | ``` 52 | 53 | 4. 配置vimrc 54 | ``` 55 | python@dev:~$ cp python.vimrc .vimrc 56 | # jedi的默认快捷键 57 | let g:jedi#goto_command = "d" 58 | let g:jedi#goto_assignments_command = "g" 59 | let g:jedi#goto_definitions_command = "" 60 | let g:jedi#documentation_command = "K" 61 | let g:jedi#usages_command = "n" 62 | let g:jedi#completions_command = "" 63 | let g:jedi#rename_command = "r" 64 | ``` 65 | 66 | 5. 安装插件 67 | ``` 68 | python@dev:~$ vim +PluginInstall +qall 69 | ``` 70 | -------------------------------------------------------------------------------- /linux/001_start_process_of_linux.md: -------------------------------------------------------------------------------- 1 | # Linux的启动过程 2 | 3 | 本文将简单介绍一下Linux的启动过程,希望对那些安装Linux的过程中遇到了问题的朋友有些帮助 4 | 5 | >**声明:** 本人没用过UEFI模式和GPT分区格式,所有关于这两部分的内容都是网络上找的资料,仅供参考。 6 | 7 | ## 典型启动顺序 8 | 1. 计算机通电后,CPU开始从一个固定的地址加载代码并开始执行,这个地址就是BIOS的驱动程序所在的位置,于是BIOS的驱动开始执行。 9 | 10 | 2. BIOS驱动首先进行一些自检工作,然后根据配置的启动顺序,依次尝试加载启动程序。比如配置的启动顺序是CD->网卡01->USB->硬盘。 BIOS 将先检查是否能从CD启动,如果不行,接着试着从网卡启动,再试USB盘,最后再试硬盘。 11 | 12 | 3. CD,U盘和硬盘的启动都是一样的,对BIOS来说,它们都是块设备,BIOS通过硬件访问接口直接访问这些块设备(如通过IDE访问硬盘),加载固定位置的内容到内存,然后跳转到那个内存的位置开始执行,这里固定位置所存放的就是Bootloader的代码,从这个时间点开始,启动的工作就由BIOS交接到了Bootloader手中了。对大多数发行版来说,CD和U盘里面放的都是安装程序,里面用的Bootloader一般都是[isolinux](http://www.syslinux.org/wiki/index.php?title=ISOLINUX),而硬盘里面存放的是安装好的系统,常用的Bootloader是[GRUB2](https://www.gnu.org/software/grub/),当然开源的Bootloader有[很多种](https://en.wikipedia.org/wiki/Comparison_of_boot_loaders),并且各有各的特点. 13 | 14 | 4. 从网卡启动稍微有所不同,当然前提条件是网卡支持PXE启动。 下面是大概的步骤 15 | 1. 从网卡中加载PXE firmware到内存并执行,里面主要包含一个很小的网络驱动和TFTP client的实现 16 | 17 | 2. 发送UDP广播到当前局域网,向DHCP服务器要IP和NBP(Network Boot Program)的地址 18 | 19 | 3. DHCP服务器收到广播后,会发送应答,里面包含分配给请求机器的IP以及NBP的所在位置 20 | 21 | 4. 将分配的IP应用到网卡上,然后根据收到的NBP的地址,用TFTP协议到相应的服务器上取相应的NBP文件(取文件的过程不再是广播,而是点对点的文件传输过程,所以当前网卡必须要有IP) 22 | 23 | 6. 开始执行取到的NBP(Linux一般使用[pxelinux](http://www.syslinux.org/wiki/index.php?title=PXELINUX)作为NBP) 24 | 25 | >从上面的过程可以看出,一个PXE服务器至少包含一个DHCP server和一个TFTP server。 26 | 27 | ## 以硬盘启动及GRUB2为例,接着介绍Linux的启动过程 28 | 1. BIOS加载硬盘[MBR](https://en.wikipedia.org/wiki/Master_boot_record)中的[GRUB](https://zh.wikipedia.org/wiki/GNU_GRUB)后,启动过程就被GRUB2接管 29 | 30 | 2. 由于MBR里面空间很小,GRUB2只能放部分代码到里面,所以它采用了好几级的结构来加载自己,详情请点[这里](https://en.wikipedia.org/wiki/GNU_GRUB#Booting),总之,最后GRUB2会加载/boot/grub/下的驱动到内存中。 31 | 32 | 3. GRUB2加载内核和initrd image,并启动内核。GRUB2和内核之间的协议请参考[i386/boot.txt](https://www.kernel.org/pub/linux/kernel/people/marcelo/linux-2.4/Documentation/i386/boot.txt)。 33 | 34 | 4. 内核接管整个系统后,加载/sbin/init并创建第一个用户态的进程 35 | 36 | 5. init进程开始调用一系列的脚本来创建很多子进程,这些子进程负责初始化整个系统 37 | 38 | 39 | ## 注意事项: 40 | 41 | ### GRUB2 42 | GRUB2需要加载/boot下的grub模块才能工作,所以格式化Linux分区一定要注意,如果不小心格式化了/boot所在的分区,会导致GRUB2用不了,从而启动不了任何系统。 43 | GRUB2同时需要加载硬盘上的Linux内核文件,所以它也需要有文件系统的驱动,当然它只需要读取文件,所以驱动很小。GRUB2已经支持所有的常见文件系统,并且完全支持LVM和RAID。 44 | 45 | 参考: 46 | 47 | * [GRUB2: Differences from previous versions](http://www.gnu.org/software/grub/manual/grub.html#Changes-from-GRUB-Legacy) 48 | * [GRUB2: features](http://www.gnu.org/software/grub/manual/grub.html#Features) 49 | 50 | ### [BIOS](https://en.wikipedia.org/wiki/BIOS) VS [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface) 51 | UEFI可以简单理解为新一代的BIOS,支持更多新的功能,当然它也向下兼容BIOS,现在新的主板都支持UEFI,只是我们BIOS叫习惯了,所以就算主板已经支持新的UEFI,我们还是把它当BIOS用。UEFI的优点请参考[这里](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface#Advantages)。 52 | 53 | BIOS和UEFI两者启动系统的方式不一样,BIOS是读取硬盘第一个扇区的MBR到内存中,然后将控制权交给MBR里的Bootloader。而UEFI是读取efi分区,如果efi分区存在且里面有启动程序的话,将控制权交给启动程序,否则和BIOS一样,读取硬盘第一个扇区的MBR到内存中,将控制权交给MBR里面的Bootloader。从这里可以看出: 54 | 55 | * UEFI是兼容BIOS的,就是说就算主板支持UEFI,只要我们不用efi分区,主板还是按照原来BIOS的方式来启动系统 56 | * 两者只能选其一,使用efi分区里面的启动程序,或者是MBR里面的Bootloader 57 | 58 | 那什么时候应该用UEFI呢? 59 | 60 | * 如果这台机器原来没有任何系统,那可以完全不用关心是BIOS还是UEFI,因为就算BIOS模式,Linux也可以从GPT盘启动 61 | * 如果机器上已经有了一个系统,那么就必须确保新安装的Linux和原有的系统采取同样的模式。 62 | 63 | 如何判断原系统的模式: 64 | 65 | * [Windows 8](http://www.howtogeek.com/175649/what-you-need-to-know-about-using-uefi-instead-of-the-bios/)及以上版本默认采用UEFI模式, Windows 7默认用BIOS模式 66 | * [Ubuntu](https://help.ubuntu.com/community/UEFI#Identifying_if_an_Ubuntu_has_been_installed_in_UEFI_mode) 67 | 68 | 如何以UEFI模式安装: [Ubuntu](https://help.ubuntu.com/community/UEFI) 69 | 70 | 71 | 参考: 72 | 73 | * [What is the difference in “Boot with BIOS” and “Boot with UEFI”](http://superuser.com/questions/496026/what-is-the-difference-in-boot-with-bios-and-boot-with-uefi) 74 | * [Learn How UEFI Will Replace Your PC’s BIOS](http://www.howtogeek.com/56958/) 75 | 76 | ### MBR VS [GPT](https://en.wikipedia.org/wiki/GUID_Partition_Table) 77 | MBR格式硬盘的布局 78 | ``` 79 | ------------------------------------------------------------------ 80 | | | | | |-------------------------------| 81 | |MBR| 主分区1 | 主分区2 | 主分区3 | 扩展 |逻辑分区1|...|逻辑分区n | 82 | | | | | |-------------------------------| 83 | ------------------------------------------------------------------ 84 | ↓ 85 | 扩展分区是一个特殊的主分区,分区最前面包含所有逻辑分区的描述,包含大小,位置等 86 | ``` 87 | 88 | * 由于留给MBR的空间太小,所以MBR格式的硬盘只能支持四个分区,就是我们常说的四个主分区。如果想把磁盘分成大于4个分区,就需要将其中的一个或者多个分区设置成扩展分区,然后在扩展分区里面划分逻辑分区。 89 | * 对Linux而言,可以安装在主分区和逻辑分区里面,所以怎么划分硬盘都没关系。但对于Windows而言,由于只支持安装在主分区里面,所以必须至少有一个主分区,如果我们安装Linux时不小心将磁盘全部划分成逻辑分区,则以后要安装Windows就比较麻烦,需要重新划分磁盘分区格式。 90 | * 同样由于留给MBR的空间太小,它所能表述的磁盘空间有限,只能支持小于2T的硬盘。 91 | 92 | GPT主要用来替换MBR,并且配合UEFI使用。 在Windows和OS X上,只支持通过UEFI方式启动GPT硬盘,而FreeBSD,Linux依然支持BIOS模式启动GPT硬盘。 93 | 94 | GPT的主要优点: 95 | 96 | * 支持几乎无限制的磁盘分区个数,再也不需要主分区、扩展分区和逻辑分区这些概念了 97 | * 支持超过2T的硬盘 98 | * 分区数据在磁盘的不同位置存有多份,且有CRC校验码,所以更安全 99 | 100 | 参考: 101 | 102 | * [What’s the Difference Between GPT and MBR When Partitioning a Drive?](http://www.howtogeek.com/193669/whats-the-difference-between-gpt-and-mbr-when-partitioning-a-drive/) 103 | 104 | ### 内核参数和initrd image 105 | 106 | 下面是一个GRUB2配置的例子 107 | ``` 108 | kernel /boot/vmlinuz-2.6.9-1.667 ro root=/dev/hda5 quiet 109 | initrd /boot/initrd-2.6.9-1.667.img 110 | ``` 111 | 当GRUB2加载完Linux内核(/boot/vmlinuz-2.6.9-1.667)后,将这里的“ro root=/dev/hda5 quiet”做为参数传给Linux内核,然后将控制权交给Linux内核。Linux支持的内核参数请点[这里](https://www.kernel.org/doc/Documentation/kernel-parameters.txt),其中一个重要的参数是"init" 112 | 113 | ``` 114 | 'init=...' 115 | 指定init程序的位置,Linux内核初始化完成后,将运行该位置所指定的程序 , 116 | 并将该进程作为第一个用户态进程,设置其进程ID为1 117 | 如果没有指定这个参数,或者这个参数指定的位置不存在, 118 | Linux内核将依次搜索/sbin/init, /etc/init, /bin/init, /bin/sh这些路径, 119 | 如果都不存在,Linux将启动失败。 120 |   这里指定的init程序可以是可执行文件,软链接,也可以是脚本。 121 | 122 | ``` 123 | 124 | #### initrd image是干嘛的呢? 125 | 126 | 我们都知道Linux内核模块的概念,比方说Linux支持N种不同的文件系统,Ext2/3/4,XFS, Btrfs等等,那需要把所有的这些文件系统驱动都编译进内核吗?当然不需要,因为这样做会导致内核太大,运行时占用太多的内存,取而代之,我们会把这些驱动编译成一个一个的内核模块,在需要用到的时候再把它们加载进内核,其它时间存放在磁盘上就好了。 127 | 128 | 现在有个问题,在GRUB将控制权交给Linux内核后,内核需要启动init程序,这个init程序是放在某个磁盘分区上的,这个磁盘分区用的是N个文件系统中的某一个,内核到哪里找这个文件系统的驱动呢?这个时候initrd image出场了,它里面包含了很多驱动模块,并且用的是内存文件系统,内存文件系统的驱动已经编译到内核中了,所以内核是可以直接访问initrd image的(老版本的initrd可能用的其它格式,但不管怎么样,肯定是被内核支持的格式)。当然initrd image里面不仅仅只包含文件系统的驱动,还有其它的很多文件,这个跟每个发行版有关,具体的内容可以参考相应的发行版。 129 | 130 | #### init 131 | 内核启动的第一个用户态进程init到底是个什么东东?其实它就是一个普通的程序,内核并没有对它做什么要求,只是别退出就好,init进程如果挂了的话,系统就崩溃了,至于init进程干些啥,启动其它的哪些进程,跟内核已经没有关系了,内核的任务就是管理硬件资源并调度这些用户态进程。我们也可以写一个我们自己的init程序放到那里,它也会正常的被内核启动起来。 132 | 133 | 除了在init进程里指定了handler的信号外,内核会帮init进程屏蔽掉其他所有信号,包括普通进程无法捕获和屏蔽的信号SIGKILL和SIGSTOP,这样可以防止其他进程不小心kill掉init进程导致系统挂掉。这是内核给用户态启动的第一个进程的特殊待遇。 134 | 135 | init是用户态的第一个进程,所以非常重要,各个Linux发行版都用这个进程来创建很多子进程,然后让这些子进程来初始化用户态的环境,如mount各个分区,启动各个服务等,现在各个发行版主要采用这三种框架中的一种[sysvinit](http://savannah.nongnu.org/projects/sysvinit),[upstart](http://upstart.ubuntu.com/),[systemd](https://www.freedesktop.org/wiki/Software/systemd/) 136 | 137 | 简单点说,sysvinit出现最早,简单易用,但缺点是速度慢,比如有10个服务需要在开机时启动,那么sysvinit只能一个接一个的启动它们,即使他们之间没有任何关系,也不能并行的启动。于是出现了upstart,upstart基于事件驱动,可以让没有关系的服务并行的启动,这样可以加快开机速度。但是人们觉得还是不够快,于是出现了systemd,它可以通过一定的技术和技巧让有关系的服务也能并发的启动,当然导致的结果是systemd比较复杂。这里只提到了启动速度,当然还有其他方面的改进,详情请参考: 138 | 139 | * [浅析 Linux 初始化 init 系统,第 1 部分: sysvinit](https://www.ibm.com/developerworks/cn/linux/1407_liuming_init1/) 140 | * [浅析 Linux 初始化 init 系统,第 2 部分: UpStart](https://www.ibm.com/developerworks/cn/linux/1407_liuming_init2/) 141 | * [浅析 Linux 初始化 init 系统,第 3 部分: Systemd](https://www.ibm.com/developerworks/cn/linux/1407_liuming_init3/) 142 | -------------------------------------------------------------------------------- /linux/002_mount_01.md: -------------------------------------------------------------------------------- 1 | # Linux mount (第一部分) 2 | 3 | 本篇将介绍一些比较实用的mount用法,包括挂载内核中的虚拟文件系统、loop device和bind mount。 4 | 5 | >下面的所有例子都在ubuntu-server-x86_64 16.04下执行通过 6 | 7 | ## 基本用法 8 | mount命令的标准格式如下 9 | ```bash 10 | mount -t type -o options device dir 11 | ``` 12 | 13 | * device: 要挂载的设备(必填)。有些文件系统不需要指定具体的设备,这里可以随便填一个字符串 14 | * dir: 挂载到哪个目录(必填) 15 | * type: 文件系统类型(可选)。大部分情况下都不用指定该参数,系统都会自动检测到设备上的文件系统类型 16 | * options: 挂载参数(可选)。 17 | options一般分为两类,一类是Linux VFS所提供的通用参数,就是每个文件系统都可以使用这类参数,详情请参考“[FILESYSTEM-INDEPENDENT MOUNT OPTIONS](http://man7.org/linux/man-pages/man8/mount.8.html)”。另一类是每个文件系统自己支持的特有参数,这个需要参考每个文件系统的文档,如btrfs支持的参数可以在[这里](https://btrfs.wiki.kernel.org/index.php/Mount_options)找到。 18 | 19 | 网上关于如何利用mount命令挂载设备的例子很多,这里就不再啰嗦了。 20 | 21 | ## 挂载虚拟文件系统 22 | proc、tmpfs、sysfs、devpts等都是Linux内核映射到用户空间的虚拟文件系统,他们不和具体的物理设备关联,但他们具有普通文件系统的特征,应用层程序可以像访问普通文件系统一样来访问他们。 23 | 24 | 这里只是示例一下怎么挂载他们,不会对他们具体的功能做详细介绍。 25 | ```bash 26 | #将内核的proc文件系统挂载到/mnt, 27 | #这样就可以在/mnt目录下看到系统当前运行的所有进程的信息, 28 | #由于proc是内核虚拟的一个文件系统,并没有对应的设备, 29 | #所以这里-t参数必须要指定,不然mount就不知道要挂载啥了。 30 | #由于没有对应的源设备,这里none可以是任意字符串, 31 | #取个有意义的名字就可以了,因为用mount命令查看挂载点信息时第一列显示的就是这个字符串。 32 | dev@ubuntu:~$ sudo mount -t proc none /mnt 33 | 34 | #在内存中创建一个64M的tmpfs文件系统,并挂载到/mnt下, 35 | #这样所有写到/mnt目录下的文件都存储在内存中,速度非常快, 36 | #不过要注意,由于数据存储在内存中,所以断电后数据会丢失掉 37 | dev@ubuntu:~$ sudo mount -t tmpfs -o size=64m tmpfs /mnt 38 | ``` 39 | 40 | ## 挂载 [loop device](https://en.wikipedia.org/wiki/Loop_device) 41 | 在Linux中,硬盘、光盘、软盘等都是常见的块设备,他们在Linux下的目录一般是/dev/hda1, /dev/cdrom, /dev/sda1,/dev/fd0这样的。而loop device是虚拟的块设备,主要目的是让用户可以像访问上述块设备那样访问一个文件。 loop device设备的路径一般是/dev/loop0, dev/loop1, ...等,具体的个数跟内核的配置有关,Ubuntu16.04下面默认是8个,如果8个都被占用了,那么就需要修改内核参数来增加loop device的个数。 42 | 43 | ### ISO文件 44 | 需要用到loop device的最常见的场景是mount一个ISO文件,示例如下 45 | ```bash 46 | #利用mkisofs构建一个用于测试的iso文件 47 | dev@ubuntu:~$ mkdir -p iso/subdir01 48 | dev@ubuntu:~$ mkisofs -o ./test.iso ./iso 49 | 50 | #mount ISO 到目录 /mnt 51 | dev@ubuntu:~$ sudo mount ./test.iso /mnt 52 | mount: /dev/loop0 is write-protected, mounting read-only 53 | 54 | #mount成功,能看到里面的文件夹 55 | dev@ubuntu:~$ ls /mnt 56 | subdir01 57 | 58 | #通过losetup命令可以看到占用了loop0设备 59 | dev@ubuntu:~$ losetup -a 60 | /dev/loop0: []: (/home/dev/test.iso) 61 | 62 | ``` 63 | 64 | ### 虚拟硬盘 65 | loop device另一种常用的用法是虚拟一个硬盘,比如我想尝试下btrfs这个文件系统,但系统中目前的所有分区都已经用了,里面都是有用的数据,不想格式化他们,这时虚拟硬盘就有用武之地了,示例如下 66 | ```bash 67 | #因为btrfs对分区的大小有最小要求,所以利用dd命令创建一个128M的文件 68 | dev@ubuntu:~$ dd if=/dev/zero bs=1M count=128 of=./vdisk.img 69 | 70 | #在这个文件里面创建btrfs文件系统 71 | #有些同学可能会想,硬盘一般不都是先分区再创建文件系统的吗? 72 | #是的,分区是为了方便磁盘的管理, 73 | #但对于文件系统来说,他一点都不关心分区的概念,你给他多大的空间,他就用多大的空间, 74 | #当然这里也可以先用fdisk在vdisk.img中创建分区,然后再在分区上创建文件系统, 75 | #只是这里的虚拟硬盘不需要用作其他的用途,为了方便,我就把整个硬盘全部给btrfs文件系统, 76 | dev@ubuntu:~$ mkfs.btrfs ./vdisk.img 77 | #这里会输出一些信息,提示创建成功 78 | 79 | #mount虚拟硬盘 80 | dev@ubuntu:~$ sudo mount ./vdisk.img /mnt/ 81 | 82 | #在虚拟硬盘中创建文件成功 83 | dev@ubuntu:~$ sudo touch /mnt/aaaaaa 84 | dev@ubuntu:~$ ls /mnt/ 85 | aaaaaa 86 | 87 | #加上刚才上面mount的iso文件,我们已经用了两个loop device了 88 | dev@ubuntu:~$ losetup -a 89 | /dev/loop0: []: (/home/dev/test.iso) 90 | /dev/loop1: []: (/home/dev/vdisk.img) 91 | 92 | ``` 93 | 94 | ## 挂载多个设备到一个文件夹 95 | 细心的朋友可能已经发现了,在上面的例子中,将test.iso和vdisk.img都mount到了/mnt目录下,这个在Linux下是支持的,默认会用后面的mount覆盖掉前面的mount,只有当umount后面的device后,原来的device才看的到。 看下面的例子 96 | 97 | ```bash 98 | #先umount上面的iso和vdisk.img 99 | dev@ubuntu:~$ sudo umount ./test.iso 100 | dev@ubuntu:~$ sudo umount ./vdisk.img 101 | 102 | #在/mnt目录下先创建一个空的test文件夹 103 | dev@ubuntu:~$ sudo mkdir /mnt/test 104 | dev@ubuntu:~$ ls /mnt/ 105 | test 106 | 107 | #mount iso文件 108 | dev@ubuntu:~$ sudo mount ./test.iso /mnt 109 | #再看/mnt里面的内容,已经被iso里面的内容给覆盖掉了 110 | dev@ubuntu:~$ ls /mnt/ 111 | subdir01 112 | 113 | #再mount vdisk.img 114 | dev@ubuntu:~$ sudo mount ./vdisk.img /mnt/ 115 | #再看/mnt里面的内容,已经被vdisk.img里面的内容给覆盖掉了 116 | dev@ubuntu:~$ ls /mnt/ 117 | aaaaaa 118 | 119 | #通过mount命令可以看出,test.iso和vdisk.img都mount在了/mnt 120 | #但我们在/mnt下只能看到最后一个mount的设备里的东西 121 | dev@ubuntu:~$ mount|grep /mnt 122 | /home/dev/test.iso on /mnt type iso9660 (ro,relatime) 123 | /home/dev/vdisk.img on /mnt type btrfs (rw,relatime,space_cache,subvolid=5,subvol=/) 124 | 125 | #umount /mnt,这里也可以用命令sudo umount ./vdisk.img,一样的效果 126 | dev@ubuntu:~$ sudo umount /mnt 127 | #test.iso文件里面的东西再次出现了 128 | dev@ubuntu:~$ ls /mnt/ 129 | subdir01 130 | 131 | #再次umount /mnt,这里也可以用命令sudo umount ./test.iso,一样的效果 132 | dev@ubuntu:~$ sudo umount /mnt 133 | #最开始/mnt目录里面的文件可以看到了 134 | dev@ubuntu:~$ ls /mnt/ 135 | test 136 | 137 | ``` 138 | 有了这个功能,平时挂载设备的时候就不用专门去创建空目录了,随便找个暂时不用的目录挂上去就可以了。 139 | 140 | ## 挂载一个设备到多个目录 141 | 当然我们也可以把一个设备mount到多个文件夹,这样在多个文件夹中都可以访问该设备中的内容。 142 | ```bash 143 | #新建两目录用于挂载点 144 | dev@ubuntu:~$ sudo mkdir /mnt/disk1 /mnt/disk2 145 | #将vdisk.img依次挂载到disk1和disk2 146 | dev@ubuntu:~$ sudo mount ./vdisk.img /mnt/disk1 147 | dev@ubuntu:~$ sudo mount ./vdisk.img /mnt/disk2 148 | 149 | #这样在disk1下和disk2下面都能看到相同的内容 150 | dev@ubuntu:~$ tree /mnt 151 | /mnt 152 | ├── disk1 153 | │   └── aaaaaa 154 | └── disk2 155 | └── aaaaaa 156 | 157 | #在disk1下创建一个新文件 158 | dev@ubuntu:~$ sudo touch /mnt/disk1/bbbbbb 159 | #这个文件在disk2下面也能看到 160 | dev@ubuntu:~$ tree /mnt 161 | /mnt 162 | ├── disk1 163 | │   ├── aaaaaa 164 | │   └── bbbbbb 165 | └── disk2 166 | ├── aaaaaa 167 | └── bbbbbb 168 | 169 | ``` 170 | 171 | ## bind mount 172 | bind mount功能非常强大,可以将任何一个挂载点、普通目录或者文件挂载到其他地方,是玩转Linux的必备技能 173 | 174 | ### 基本功能 175 | bind mount会将源目录绑定到目的目录,然后在目的目录下就可以看到源目录里的文件 176 | 177 | ```bash 178 | #准备要用到的目录 179 | dev@ubuntu:~$ mkdir -p bind/bind1/sub1 180 | dev@ubuntu:~$ mkdir -p bind/bind2/sub2 181 | dev@ubuntu:~$ tree bind 182 | bind 183 | ├── bind1 184 | │   └── sub1 185 | └── bind2 186 | └── sub2 187 | 188 | #bind mount后,bind2里面显示的就是bind1目录的内容 189 | dev@ubuntu:~$ sudo mount --bind ./bind/bind1/ ./bind/bind2 190 | dev@ubuntu:~$ tree bind 191 | bind 192 | ├── bind1 193 | │   └── sub1 194 | └── bind2 195 | └── sub1 196 | 197 | ``` 198 | 199 | ### readonly bind 200 | 我们可以在bind的时候指定readonly,这样原来的目录还是能读写,但目的目录为只读 201 | ```bash 202 | #通过readonly的方式bind mount 203 | dev@ubuntu:~$ sudo mount -o bind,ro ./bind/bind1/ ./bind/bind2 204 | dev@ubuntu:~$ tree bind 205 | bind 206 | ├── bind1 207 | │   └── sub1 208 | └── bind2 209 | └── sub1 210 | 211 | #bind2目录为只读,没法touch里面的文件 212 | dev@ubuntu:~$ touch ./bind/bind2/sub1/aaa 213 | touch: cannot touch './bind/bind2/sub1/aaa': Read-only file system 214 | 215 | #bind1还是能读写 216 | dev@ubuntu:~$ touch ./bind/bind1/sub1/aaa 217 | 218 | #我们可以在bind1和bind2目录下看到刚创建的文件 219 | dev@ubuntu:~$ tree bind 220 | bind 221 | ├── bind1 222 | │   └── sub1 223 | │   └── aaa 224 | └── bind2 225 | └── sub1 226 | └── aaa 227 | 228 | ``` 229 | 如果我们想让当前目录readonly,那么可以bind自己,并且指定readonly参数: 230 | ```bash 231 | #bind mount并且指定readonly 232 | dev@ubuntu:~$ sudo mount -o bind,ro ./bind/bind1/ ./bind/bind1 233 | 234 | #创建新文件失败 235 | dev@ubuntu:~$ touch ./bind/bind1/sub1/aaa 236 | touch: cannot touch './bind/bind1/sub1/aaa': Read-only file system 237 | 238 | #umount之后,文件夹恢复到原来的读写权限 239 | dev@ubuntu:~$ sudo umount ./bind/bind1/ 240 | ##touch文件成功 241 | dev@ubuntu:~$ touch ./bind/bind1/sub1/aaa 242 | dev@ubuntu:~$ 243 | 244 | ``` 245 | 246 | ### bind mount单个文件 247 | 我们也可以bind mount单个文件,这个功能尤其适合需要在不同版本配置文件之间切换的时候 248 | ```bash 249 | #创建两个用于测试的文件 250 | dev@ubuntu:~$ echo aaaaaa > bind/aa 251 | dev@ubuntu:~$ echo bbbbbb > bind/bb 252 | dev@ubuntu:~$ cat bind/aa 253 | aaaaaa 254 | dev@ubuntu:~$ cat bind/bb 255 | bbbbbb 256 | 257 | #bind mount后,bb里面看到的是aa的内容 258 | dev@ubuntu:~$ sudo mount --bind ./bind/aa bind/bb 259 | dev@ubuntu:~$ cat bind/bb 260 | aaaaaa 261 | 262 | #即使我们删除aa文件,我们还是能够通过bb看到aa里面的内容 263 | dev@ubuntu:~$ rm bind/aa 264 | dev@ubuntu:~$ cat bind/bb 265 | aaaaaa 266 | 267 | #umount bb文件后,bb的内容出现了,不过aa的内容再也找不到了 268 | dev@ubuntu:~$ sudo umount bind/bb 269 | dev@ubuntu:~$ cat bind/bb 270 | bbbbbb 271 | 272 | ``` 273 | 274 | ### move一个挂载点到另一个地方 275 | move操作可以将一个挂载点移动到别的地方,这里以bind mount为例来演示,当然其他类型的挂载点也可以通过move操作来移动。 276 | ```bash 277 | #umount上面操作所产生的挂载点 278 | dev@ubuntu:~$ sudo umount /home/dev/bind/bind1 279 | dev@ubuntu:~$ sudo umount /home/dev/bind/bind2 280 | 281 | #bind mount 282 | dev@ubuntu:~$ sudo mount --bind ./bind/bind1/ ./bind/bind2/ 283 | dev@ubuntu:~$ ls ./bind/bind* 284 | ./bind/bind1: 285 | sub1 286 | 287 | ./bind/bind2: 288 | sub1 289 | 290 | #move操作要求mount point的父mount point不能为shared。 291 | #在这里./bind/bind2/的父mount point为'/',所以需要将'/'变成private后才能做move操作 292 | #关于shared、private的含义将会在下一篇介绍 293 | dev@ubuntu:~$ findmnt -o TARGET,PROPAGATION / 294 | TARGET PROPAGATION 295 | / shared 296 | dev@ubuntu:~$ sudo mount --make-private / 297 | dev@ubuntu:~$ findmnt -o TARGET,PROPAGATION / 298 | TARGET PROPAGATION 299 | / private 300 | 301 | #move成功,在mnt下能看到bind1里面的内容 302 | dev@ubuntu:~$ sudo mount --move ./bind/bind2/ /mnt 303 | dev@ubuntu:~$ ls /mnt/ 304 | sub1 305 | #由于bind2上的挂载点已经被移动到了/mnt上,于是能看到bind2目录下原来的文件了 306 | dev@ubuntu:~$ ls ./bind/bind2/ 307 | sub2 308 | ``` 309 | ## 结束语 310 | 在这篇文章中演示了一些比较实用的mount操作,尤其是bind mount,至于在哪些情况下要用哪些功能,需要我们自己去挖掘。下一篇中将介绍mount相关的“Shared subtrees” 311 | 312 | ## 参考 313 | 314 | * [mount a filesystem](http://man7.org/linux/man-pages/man8/mount.8.html) 315 | -------------------------------------------------------------------------------- /linux/003_mount_02.md: -------------------------------------------------------------------------------- 1 | # Linux mount (第二部分 - Shared subtrees) 2 | 3 | 简单点说,[Shared subtrees](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt)就是一种控制子挂载点能否在其他地方被看到的技术,它只会在bind mount和mount namespace中用到,属于不怎么常用的功能。本篇将以bind mount为例对Shared subtrees做一个简单介绍, 4 | 5 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 6 | 7 | ## 概述 8 | 回想一下[上一篇](002_mount_01.md)中介绍的bind mount部分,如果bind在一起的两个目录下的子目录再挂载了设备的话,他们之间还能相互看到子目录里挂载的内容吗? 比如在第一个目录下的子目录里面再mount了一个设备,那么在另一个目录下面能看到这个mount的设备里面的东西吗?答案是要看bind mount的propagation type。那什么是propagation type呢? 9 | 10 | peer group和propagation type都是随着shared subtrees一起被引入的概念,下面分别对他们做一个介绍。 11 | 12 | ### peer group 13 | peer group就是一个或多个挂载点的集合,他们之间可以共享挂载信息。目前在下面两种情况下会使两个挂载点属于同一个peer group(前提条件是挂载点的propagation type是shared) 14 | 15 | * 利用mount --bind命令,将会使源和目的挂载点属于同一个peer group,当然前提条件是‘源’必须要是一个挂载点。 16 | * 当创建新的mount namespace时,新namespace会拷贝一份老namespace的挂载点信息,于是新的和老的namespace里面的相同挂载点就会属于同一个peer group。 17 | 18 | ### propagation type 19 | 每个挂载点都有一个propagation type标志, 由它来决定当一个挂载点的下面创建和移除挂载点的时候,是否会传播到属于相同peer group的其他挂载点下去,也即同一个peer group里的其他的挂载点下面是不是也会创建和移除相应的挂载点.现在有4种不同类型的propagation type: 20 | 21 | * MS_SHARED: 从名字就可以看出,挂载信息会在同一个peer group的不同挂载点之间共享传播. 当一个挂载点下面添加或者删除挂载点的时候,同一个peer group里的其他挂载点下面也会挂载和卸载同样的挂载点 22 | 23 | * MS_PRIVATE: 跟上面的刚好相反,挂载信息根本就不共享,也即private的挂载点不会属于任何peer group 24 | 25 | * MS_SLAVE: 跟名字一样,信息的传播是单向的,在同一个peer group里面,master的挂载点下面发生变化的时候,slave的挂载点下面也跟着变化,但反之则不然,slave下发生变化的时候不会通知master,master不会发生变化。 26 | 27 | * MS_UNBINDABLE: 这个和MS_PRIVATE相同,只是这种类型的挂载点不能作为bind mount的源,主要用来防止递归嵌套情况的出现。这种类型不常见,本篇将不介绍这种类型,有兴趣的同学请参考[这里的例子](https://lwn.net/Articles/690679/)。 28 | 29 | 还有一些概念需要澄清一下: 30 | 31 | * propagation type是挂载点的属性,每个挂载点都是独立的 32 | * 挂载点是有父子关系的,比如挂载点/和/mnt/cdrom,/mnt/cdrom都是‘/’的子挂载点,‘/’是/mnt/cdrom的父挂载点 33 | * 默认情况下,如果父挂载点是MS_SHARED,那么子挂载点也是MS_SHARED的,否则子挂载点将会是MS_PRIVATE,跟爷爷挂载点没有关系 34 | 35 | ## 示例 36 | 37 | 这里将只演示bind mount的情况,mount namespace的情况请参考[这里](/container/namespace/004_namespace_mount.md) 38 | 39 | ### 准备环境 40 | ```bash 41 | #准备4个虚拟的disk,并在上面创建ext2文件系统,用于后续的mount测试 42 | dev@ubuntu:~$ mkdir disks && cd disks 43 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk1.img 44 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk2.img 45 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk3.img 46 | dev@ubuntu:~/disks$ dd if=/dev/zero bs=1M count=32 of=./disk4.img 47 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk1.img 48 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk2.img 49 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk3.img 50 | dev@ubuntu:~/disks$ mkfs.ext2 ./disk4.img 51 | #准备两个目录用于挂载上面创建的disk 52 | dev@ubuntu:~/disks$ mkdir disk1 disk2 53 | dev@ubuntu:~/disks$ ls 54 | disk1 disk1.img disk2 disk2.img disk3.img disk4.img 55 | 56 | #确保根目录的propagation type是shared, 57 | #这一步是为了保证大家的操作结果和示例中的一样 58 | dev@ubuntu:~/disks$ sudo mount --make-shared / 59 | ``` 60 | 61 | ### 查看propagation type和peer group 62 | 默认情况下,子挂载点会继承父挂载点的propagation type 63 | ```bash 64 | #显式的以shared方式挂载disk1 65 | dev@ubuntu:~/disks$ sudo mount --make-shared ./disk1.img ./disk1 66 | #显式的以private方式挂载disk2 67 | dev@ubuntu:~/disks$ sudo mount --make-private ./disk2.img ./disk2 68 | 69 | #mountinfo比mounts文件包含有更多的关于挂载点的信息 70 | #这里sed主要用来过滤掉跟当前主题无关的信息 71 | #shared:105表示挂载点/home/dev/disks/disk1是以shared方式挂载,且peer group id为105 72 | #而挂载点/home/dev/disks/disk2没有相关信息,表示是以private方式挂载 73 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk | sed 's/ - .*//' 74 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 75 | 173 24 7:2 / /home/dev/disks/disk2 rw,relatime 76 | 77 | #分别在disk1和disk2目录下创建目录disk3和disk4,然后挂载disk3,disk4到这两个目录 78 | dev@ubuntu:~/disks$ sudo mkdir ./disk1/disk3 ./disk2/disk4 79 | dev@ubuntu:~/disks$ sudo mount ./disk3.img ./disk1/disk3 80 | dev@ubuntu:~/disks$ sudo mount ./disk4.img ./disk2/disk4 81 | 82 | #查看挂载信息,第一列的数字是挂载点ID,第二例是父挂载点ID, 83 | #从结果来看,176和164的类型都是shared,而179和173的类型都是private的, 84 | #说明在默认mount的情况下,子挂载点会继承父挂载点的propagation type 85 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 86 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 87 | 173 24 7:2 / /home/dev/disks/disk2 rw,relatime 88 | 176 164 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:107 89 | 179 173 7:4 / /home/dev/disks/disk2/disk4 rw,relatime 90 | ``` 91 | 92 | ### shared 和 private mount 93 | ```bash 94 | #umount掉disk3和disk4,创建两个新的目录bind1和bind2用于bind测试 95 | dev@ubuntu:~/disks$ sudo umount /home/dev/disks/disk1/disk3 96 | dev@ubuntu:~/disks$ sudo umount /home/dev/disks/disk2/disk4 97 | dev@ubuntu:~/disks$ mkdir bind1 bind2 98 | 99 | #bind的方式挂载disk1到bind1,disk2到bind2 100 | dev@ubuntu:~/disks$ sudo mount --bind ./disk1 ./bind1 101 | dev@ubuntu:~/disks$ sudo mount --bind ./disk2 ./bind2 102 | 103 | #查看挂载信息,显然默认情况下bind1和bind2的propagation type继承自父挂载点24(/),都是shared。 104 | #由于bind2的源挂载点disk2是private的,所以bind2没有和disk2在同一个peer group里面, 105 | #而是重新创建了一个新的peer group,这个group里面就只有它一个。 106 | #因为164和176都是shared类型且是通过bind方式mount在一起的,所以他们属于同一个peer group 105。 107 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 108 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 109 | 173 24 7:2 / /home/dev/disks/disk2 rw,relatime 110 | 176 24 7:1 / /home/dev/disks/bind1 rw,relatime shared:105 111 | 179 24 7:2 / /home/dev/disks/bind2 rw,relatime shared:109 112 | 113 | #ID为24的挂载点为根目录的挂载点 114 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep ^24| sed 's/ - .*//' 115 | 24 0 252:0 / / rw,relatime shared:1 116 | 117 | #这时disk3和disk4目录都是空的 118 | dev@ubuntu:~/disks$ ls bind1/disk3/ 119 | dev@ubuntu:~/disks$ ls bind2/disk4/ 120 | dev@ubuntu:~/disks$ ls disk1/disk3/ 121 | dev@ubuntu:~/disks$ ls disk2/disk4/ 122 | 123 | #重新挂载disk3和disk4 124 | dev@ubuntu:~/disks$ sudo mount ./disk3.img ./disk1/disk3 125 | dev@ubuntu:~/disks$ sudo mount ./disk4.img ./disk2/disk4 126 | 127 | #由于disk1/和bind1/属于同一个peer group, 128 | #所以在挂载了disk3后,在两个目录下都能看到disk3下的内容 129 | dev@ubuntu:~/disks$ ls disk1/disk3/ 130 | lost+found 131 | dev@ubuntu:~/disks$ ls bind1/disk3/ 132 | lost+found 133 | 134 | #而disk2/是private类型的,所以在他下面挂载disk4不会通知bind2, 135 | #于是bind2下的disk4目录是空的 136 | dev@ubuntu:~/disks$ ls disk2/disk4/ 137 | lost+found 138 | dev@ubuntu:~/disks$ ls bind2/disk4/ 139 | dev@ubuntu:~/disks$ 140 | 141 | #再看看disk3,虽然182和183的父挂载点不一样,但由于他们父挂载点属于同一个peer group, 142 | #且disk3是以默认方式挂载的,所以他们属于同一个peer group 143 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |egrep "disk3"| sed 's/ - .*//' 144 | 182 164 7:3 / /home/dev/disks/disk1/disk3 rw,relatime shared:111 145 | 183 176 7:3 / /home/dev/disks/bind1/disk3 rw,relatime shared:111 146 | 147 | #umount bind1/disk3后,disk1/disk3也相应的自动umount掉了 148 | dev@ubuntu:~/disks$ sudo umount bind1/disk3 149 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk3 150 | dev@ubuntu:~/disks$ 151 | ``` 152 | 153 | ### slave mount 154 | ```bash 155 | #umount除disk1的所有其他挂载点 156 | dev@ubuntu:~/disks$ sudo umount ./disk2/disk4 157 | dev@ubuntu:~/disks$ sudo umount /home/dev/disks/bind1 158 | dev@ubuntu:~/disks$ sudo umount /home/dev/disks/bind2 159 | dev@ubuntu:~/disks$ sudo umount /home/dev/disks/disk2 160 | #确认只剩disk1 161 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 162 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 163 | 164 | #分别显式的用shared和slave的方式bind disk1 165 | dev@ubuntu:~/disks$ sudo mount --bind --make-shared ./disk1 ./bind1 166 | dev@ubuntu:~/disks$ sudo mount --bind --make-slave ./bind1 ./bind2 167 | 168 | #164、173和176都属于同一个peer group, 169 | #master:105表示/home/dev/disks/bind2是peer group 105的slave 170 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 171 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 172 | 173 24 7:1 / /home/dev/disks/bind1 rw,relatime shared:105 173 | 176 24 7:1 / /home/dev/disks/bind2 rw,relatime master:105 174 | 175 | #mount disk3到disk1的子目录disk3下 176 | dev@ubuntu:~/disks$ sudo mount ./disk3.img ./disk1/disk3/ 177 | #其他两个目录bin1和bind2里面也挂载成功,说明master发生变化的时候,slave会跟着变化 178 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 179 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 180 | 173 24 7:1 / /home/dev/disks/bind1 rw,relatime shared:105 181 | 176 24 7:1 / /home/dev/disks/bind2 rw,relatime master:105 182 | 179 164 7:2 / /home/dev/disks/disk1/disk3 rw,relatime shared:109 183 | 181 176 7:2 / /home/dev/disks/bind2/disk3 rw,relatime master:109 184 | 180 173 7:2 / /home/dev/disks/bind1/disk3 rw,relatime shared:109 185 | 186 | #umount disk3,然后mount disk3到bind2目录下 187 | dev@ubuntu:~/disks$ sudo umount ./disk1/disk3/ 188 | dev@ubuntu:~/disks$ sudo mount ./disk3.img ./bind2/disk3/ 189 | 190 | #由于bind2的propagation type是slave,所以disk1和bind1两个挂载点下面不会挂载disk3 191 | #从179的类型可以看出,当父挂载点176是slave类型时,默认情况下其子挂载点179是private类型 192 | dev@ubuntu:~/disks$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 193 | 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 194 | 173 24 7:1 / /home/dev/disks/bind1 rw,relatime shared:105 195 | 176 24 7:1 / /home/dev/disks/bind2 rw,relatime master:105 196 | 179 176 7:2 / /home/dev/disks/bind2/disk3 rw,relatime - 197 | ``` 198 | 199 | ## 结束语 200 | 如果用到了bind mount和mount namespace,在挂载设备的时候就需要注意一下父挂载点是否和其他挂载点有peer group关系,如果有且父挂载点是shared,就说明你挂载的设备除了在当前挂载点可以看到,在父挂载点的peer group的下面也可以看到。 201 | 202 | ## 参考 203 | * [kernel:Shared Subtrees](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) 204 | * [lwn:Shared subtrees](https://lwn.net/Articles/159077/) 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /linux/007_linux_oom_killer.md: -------------------------------------------------------------------------------- 1 | # Linux OOM killer 2 | 3 | 作为Linux下的程序员,有时不得不面对一个问题,那就是系统内存被用光了,这时当进程再向内核申请内存时,内核会怎么办呢?程序里面调用的malloc函数会返回null吗? 4 | 5 | 为了处理内存不足时的问题,Linux内核发明了一种机制,叫OOM(Out Of Memory) killer,通过配置它可以控制内存不足时内核的行为。 6 | 7 | ## OOM killer 8 | 当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核将触发OOM killer,其行为如下: 9 | 10 | 1.检查文件/proc/sys/vm/panic_on_oom,如果里面的值为2,那么系统一定会触发panic 11 | 2.如果/proc/sys/vm/panic_on_oom的值为1,那么系统有可能触发panic(见后面的介绍) 12 | 3.如果/proc/sys/vm/panic_on_oom的值为0,或者上一步没有触发panic,那么内核继续检查文件/proc/sys/vm/oom_kill_allocating_task 13 | 3.如果/proc/sys/vm/oom_kill_allocating_task为1,那么内核将kill掉当前申请内存的进程 14 | 4.如果/proc/sys/vm/oom_kill_allocating_task为0,内核将检查每个进程的分数,分数最高的进程将被kill掉(见后面介绍) 15 | 16 | 进程被kill掉之后,如果/proc/sys/vm/oom_dump_tasks为1,且系统的rlimit中设置了core文件大小,将会由/proc/sys/kernel/core_pattern里面指定的程序生成core dump文件,这个文件里将包含 17 | pid, uid, tgid, vm size, rss, nr_ptes, nr_pmds, swapents, oom_score_adj 18 | score, name等内容,拿到这个core文件之后,可以做一些分析,看为什么这个进程被选中kill掉。 19 | 20 | 这里可以看看ubuntu默认的配置: 21 | ```bash 22 | #OOM后不panic 23 | dev@ubuntu:~$ cat /proc/sys/vm/panic_on_oom 24 | 0 25 | 26 | #OOM后kill掉分数最高的进程 27 | dev@ubuntu:~$ cat /proc/sys/vm/oom_kill_allocating_task 28 | 0 29 | 30 | #进程由于OOM被kill掉后将生成core dump文件 31 | dev@ubuntu:~$ cat /proc/sys/vm/oom_dump_tasks 32 | 1 33 | 34 | #默认max core file size是0, 所以系统不会生成core文件 35 | dev@ubuntu:~$ prlimit|grep CORE 36 | CORE max core file size 0 unlimited blocks 37 | 38 | #core dump文件的生成交给了apport,相关的设置可以参考apport的资料 39 | dev@ubuntu:~$ cat /proc/sys/kernel/core_pattern 40 | |/usr/share/apport/apport %p %s %c %P 41 | ``` 42 | 参考:[apport](https://wiki.ubuntu.com/Apport) 43 | 44 | ### panic_on_oom 45 | 正如上面所介绍的那样,该文件的值可以取0/1/2,0是不触发panlic,2是一定触发panlic,如果为1的话就要看[mempolicy](https://www.kernel.org/doc/Documentation/vm/numa_memory_policy.txt)和[cpusets](https://www.kernel.org/doc/Documentation/cgroup-v1/cpusets.txt),这篇不介绍这方面的内容。 46 | 47 | panic后内核的默认行 48 | 为是死在那里,目的是给开发人员一个连上去debug的机会。但对于大多数应用层开发人员来说没啥用,倒是希望它赶紧重启。为了让内核panic后重启,可以修改文件/proc/sys/kernel/panic,里面表示的是panic多少秒后系统将重启,这个文件的默认值是0,表示永远不重启。 49 | ```bash 50 | #设置panic后3秒重启系统 51 | dev@ubuntu:~$ sudo sh -c "echo 3 > /proc/sys/kernel/panic" 52 | ``` 53 | 54 | ### 调整分数 55 | 当oom_kill_allocating_task的值为0时(系统默认配置),系统会kill掉系统中分数最高的那个进程,这里的分数是怎么来的呢?该值由内核维护,并存储在每个进程的/proc//oom_score文件中。 56 | 57 | 每个进程的分数受多方面的影响,比如进程运行的时间,时间越长表明这个程序越重要,所以分数越低;进程从启动后分配的内存越多,表示越占内存,分数会越高;这里只是列举了一两个影响分数的因素,实际情况要复杂的多,需要看内核代码,这里有篇文章可以参考:[Taming the OOM killer](https://lwn.net/Articles/317814/) 58 | 59 | 由于分数计算复杂,比较难控制,于是内核提供了另一个文件用来调控分数,那就是文件/proc//oom_adj,这个文件的默认值是0,但它可以配置为-17到15中间的任何一个值,内核在计算了进程的分数后,会和这个文件的值进行一个计算,得到的结果会作为进程的最终分数写入/proc//oom_score。计算方式大概如下: 60 | 61 | * 如果/proc//oom_adj的值为正数,那么分数将会被乘以2的n次方,这里n是文件里面的值 62 | * 如果/proc//oom_adj的值为负数,那么分数将会被除以2的n次方,这里n是文件里面的值 63 | 64 | 由于进程的分数在内核中是一个16位的整数,所以-17就意味着最终进程的分数永远是0,也即永远不会被kill掉。 65 | 66 | 当然这种控制方式也不是非常精确,但至少比没有强多了。 67 | 68 | ### 修改配置 69 | 上面的这些文件都可以通过下面三种方式来修改,这里以panic_on_oom为例做个示范: 70 | 71 | * 直接写文件(重启后失效) 72 | ```bash 73 | dev@ubuntu:~$ sudo sh -c "echo 2> /proc/sys/vm/panic_on_oom" 74 | ``` 75 | 76 | * 通过控制命令(重启后失效) 77 | ```bash 78 | dev@dev:~$ sudo sysctl vm.panic_on_oom=2 79 | ``` 80 | 81 | * 修改配置文件(重启后继续生效) 82 | ```bash 83 | #通过编辑器将vm.panic_on_oom=2添加到文件sysctl.conf中(如果已经存在,修改该配置项即可) 84 | dev@dev:~$ sudo vim /etc/sysctl.conf 85 | 86 | #重新加载sysctl.conf,使修改立即生效 87 | dev@dev:~$ sudo sysctl -p 88 | ``` 89 | 90 | ### 日志 91 | 一旦OOM killer被触发,内核将会生成相应的日志,一般可以在/var/log/messages里面看到,如果配置了syslog,日志可能在/var/log/syslog里面,这里是ubuntu里的日志样例 92 | ```bash 93 | dev@dev:~$ grep oom /var/log/syslog 94 | Jan 23 21:30:29 dev kernel: [ 490.006836] eat_memory invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0 95 | Jan 23 21:30:29 dev kernel: [ 490.006871] [] oom_kill_process+0x202/0x3c0 96 | ``` 97 | 98 | ## cgroup的OOM killer 99 | 除了系统的OOM killer之外,如果配置了memory cgroup,那么进程还将受到自己所属memory cgroup的限制,如果超过了cgroup的限制,将会触发cgroup的OOM killer,cgroup的OOM killer和系统的OOM killer行为略有不同,详情请参考[Linux Cgroup系列(04):限制cgroup的内存使用](/container/cgroup/004_cgroup_memeory.md)。 100 | 101 | ## malloc 102 | malloc是libc的函数,C/C++程序员对这个函数应该都很熟悉,它里面实际上调用的是内核的[sbrk](http://man7.org/linux/man-pages/man2/brk.2.html)和[mmap](http://man7.org/linux/man-pages/man2/mmap.2.html),为了避免频繁的调用内核函数和优化性能,它里面在内核函数的基础上实现了一套自己的内存管理功能。 103 | 104 | 既然内存不够时有OOM killer帮我们kill进程,那么这时调用的malloc还会返回NULL给应用进程吗?答案是不会,因为这时只有两种情况: 105 | 106 | 1. 当前申请内存的进程被kill掉:都被kill掉了,返回什么都没有意义了 107 | 2. 其它进程被kill掉:释放出了空闲的内存,于是内核就能给当前进程分配内存了 108 | 109 | 那什么时候我们调用malloc的时候会返回NULL呢,从malloc函数的[帮助文件](http://man7.org/linux/man-pages/man3/malloc.3.html)可以看出,下面两种情况会返回NULL: 110 | 111 | * 使用的虚拟地址空间超过了RLIMIT_AS的限制 112 | * 使用的数据空间超过了RLIMIT_DATA的限制,这里的数据空间包括程序的数据段,BSS段以及heap 113 | 114 | 关于虚拟地址空间和heap之类的介绍请参考[Linux进程的内存使用情况](006_process_memory.md),这两个参数的默认值为unlimited,所以只要不修改它们的默认配置,限制就不会被触发。有一种极端情况需要注意,那就是代码写的有问题,超过了系统的虚拟地址空间范围,比如32位系统的虚拟地址空间范围只有4G,这种情况下不确定系统会以一种什么样的方式返回错误。 115 | 116 | ## rlimit 117 | 上面提到的RLIMIT_AS和RLIMIT_DATA都可以通过函数[getrlimit和setrlimit](http://man7.org/linux/man-pages/man2/getrlimit.2.html)来设置和读取,同时linux还提供了一个[prlimit](http://man7.org/linux/man-pages/man1/prlimit.1.html)程序来设置和读取rlimit的配置。 118 | 119 | prlimit是用来替代 120 | [ulimit](http://man7.org/linux/man-pages/man3/ulimit.3.html)的一个程序,除了能设置上面的那两个参数之外,还有其它的一些参数,比如core文件的大小。关于prlimit的用法请参考它的[帮助文件](http://man7.org/linux/man-pages/man1/prlimit.1.html)。 121 | 122 | ```bash 123 | #默认情况下,RLIMIT_AS和RLIMIT_DATA的值都是unlimited 124 | dev@dev:~$ prlimit |egrep "DATA|AS" 125 | AS address space limit unlimited unlimited bytes 126 | DATA max data size unlimited unlimited bytes 127 | ``` 128 | 129 | ## 测试代码 130 | C语言的程序会受到libc的影响,可能在触发OOM killer之前就触发了segmentfault错误,如果要用C语言程序来测试触发OOM killer,一定要注意malloc的行为受MMAP_THRESHOLD影响,一次申请分配太多内存的话,malloc会调用mmap映射内存,从而不一定触发OOM killer,具体细节目前还不太清楚。这里是一个触发oom killer的例子,供参考: 131 | ```bash 132 | #include 133 | #include 134 | #include 135 | #include 136 | 137 | #define M (1024 * 1024) 138 | #define K 1024 139 | 140 | int main(int argc, char *argv[]) 141 | { 142 | char *p; 143 | int size =0; 144 | while(1) { 145 | p = (char *)malloc(K); 146 | if (p == NULL){ 147 | printf("memory allocate failed!\n"); 148 | return -1; 149 | } 150 | memset(p, 0, K); 151 | size += K; 152 | if (size%(100*M) == 0){ 153 | printf("%d00M memory allocated\n", size/(100*M)); 154 | sleep(1); 155 | } 156 | } 157 | 158 | return 0; 159 | } 160 | ``` 161 | 162 | ## 结束语 163 | 对一个进程来说,内存的使用受多种因素的限制,可能在系统内存不足之前就达到了rlimit和memory cgroup的限制,同时它还可能受不同编程语言所使用的相关内存管理库的影响,就算系统处于内存不足状态,申请新内存也不一定会触发OOM killer,需要具体问题具体分析。 164 | 165 | ## 参考 166 | * [sysctl/vm.txt](https://www.kernel.org/doc/Documentation/sysctl/vm.txt) 167 | * [How to Configure the Linux Out-of-Memory Killer](http://www.oracle.com/technetwork/articles/servers-storage-dev/oom-killer-1911807.html) 168 | * [When Linux Runs Out of Memory](http://www.linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html?page=2) -------------------------------------------------------------------------------- /linux/008_cpu.md: -------------------------------------------------------------------------------- 1 | # Linux CPU使用率 2 | 3 | 4 | 在Linux下面,可以通过top命令看到CPU的负载情况,其输出大概如下(只摘录CPU部分): 5 | ``` 6 | top - 01:24:41 up 6 min, 2 users, load average: 0.00, 0.03, 0.00 7 | %Cpu(s): 2.5 us, 1.8 sy, 3.1 ni, 90.5 id, 1.7 wa, 0.0 hi, 0.4 si, 0.0 st 8 | ``` 9 | 10 | 这里的load average以及缩写的us、sy、ni、id、wa、hi、si、st都是些什么意思呢?这些值在一个什么样的区间比较合理呢?如果值超过了合理区间,应该怎么处理呢?这篇将来聊聊这些问题。 11 | 12 | ## load average 13 | load average代表CPU的平均负载值,上面示例中的```load average: 0.00, 0.03, 0.00```分别表示当前CPU在1分钟、5分钟和15分钟内的平均负载。这些负载值是怎么来的呢? 14 | 15 | 这些数据来自于文件/proc/loadavg,内核会负责统计出这些数据。 16 | 17 | top和uptime命令显示的内容就来自于这个文件,那么这里所谓的平均负载是个什么概念? 根据proc的[帮助文件](http://man7.org/linux/man-pages/man5/proc.5.html)可知,这里的值就是单位时间内处于运行状态以及等待disk I/O状态的平均job数量。这里的运行状态和job都是内核的概念,这里简单澄清一下: 18 | 19 | * 对内核来说,进程和线程都是job 20 | * job处于运行状态指job处于内核的运行队列中,正在或等待被CPU调度(用户空间的进程正在运行不代表需要被CPU调度,有可能在等待I/O,也有可能在sleep等等) 21 | 22 | 因为某一刻(瞬间)等待调度的进程多少并不能反映系统的整体压力,所以这里取了1,5和15分钟的平均值。 23 | 24 | 那么这个值的大小反映系统什么样的一个压力状态呢?这里以单核CPU为例 25 | 26 | * 小于1: 说明平均每次只有不到一个job在忙,对于单核的CPU来说,完全能处理过来 27 | * 等于1: 说明平均每次刚好有一个job在忙,对于单核的CPU来说,刚好能处理过来 28 | * 大于1: 说明平均每次有多于一个job在忙,对于单核的CPU来说,由于一次只能处理一个任务,所以肯定有任务在等待,说明系统负载较大,调度不过来,有job需要等待 29 | 30 | 从上面可以看出,一旦大于1,就说明job得不到及时调度,系统性能将受影响。对于多核来说,由于一次可以调度多个job,所以大于1不一定有问题,以4核CPU为例,该值大于4才说明CPU忙不过来。 31 | 32 | 那这个平均负载保持在多少比较合适呢?其实没有一个标准值,但一般的做法是预留一定的空间来应对系统负载的波动,建议控制在“0.7\*核数”以内,比如4核,那么0.7\*4=2.8比较合适,一旦超过这个值,需要分析原因并着手解决。 33 | 34 | ## %Cpu(s) 35 | load average通过统计等待运行的平均job数量来推断CPU的繁忙程度,而%Cpu(s)则直接统计CPU处于不同状态的时间,比上面的load average更直观,所以在实际上也被使用的更多。 36 | 37 | 总体来说,CPU会处于下面三种状态中的一种: 38 | 39 | * Idle: 处于空闲状态,没有任务需要调度 40 | * User space: 正在运行user space的代码(处于用户态) 41 | * Kernel: 正在运行内核的代码(处于内核态) 42 | 43 | 对上面这三种状态,内核又进一步细分为很多状态,这里以上面输出的8种状态为例进行说明: 44 | 45 | * **2.5 us** : 表示CPU有2.5%的时间在运行用户态代码(即在运行用户态程序) 46 | 47 | * **1.8 sy** : 表示CPU有1.8%的时间在运行内核态代码。内核负责管理系统的所有进程和硬件资源,所有的内核代码都运行在内核态,当用户态进程需要访问硬件资源时,如分配内存,读写I/O等,也需要通过系统调用进入内核态运行内核代码。%sy高说明内核占用太多资源,或者用户进程发起了太多的系统调用。 48 | 49 | * **3.1 ni** : 表示CPU有3.1%的时间在运行niceness不为0的进程代码。默认情况下,进程的niceness值都为0,但可以通过命令[nice](http://man7.org/linux/man-pages/man1/nice.1.html)来启动一个进程并指定其niceness值,niceness的取值范围是-20到19,值越小,表示优先级越高,越优先被内核调度。 50 | 51 | * **90.5 id** : 表示CPU有90.5%的时间处于空闲状态 52 | 53 | * **1.7 wa** : 表示CPU有1.7%的时间处于I/O等待状态。通常情况下,当CPU遇到一个I/O操作时,会先触发I/O操作,然后去干别的,等I/O操作完成后,CPU再接着继续工作,但如果这时系统比较空闲,CPU没有别的事情可以做,那么CPU将处于等待状态,这种处于等待状态的时间将会被统计进I/O wait,也就是说CPU处于I/O wait状态即CPU闲着没事干在等I/O操作结束,和idle几乎是一样的。这个值高说明CPU闲且I/O操作多或者I/O操作慢,但低并不能说明没有I/O操作或者I/O操作快,有可能是CPU在忙别的,所以这只是一个参考值,需要和其他的统计项一起来分析。 54 | 55 | * **0.0 hi & 0.4 si** : 这两个值反映了CPU有多少时间花在了中断处理上,hi(hardware interrupts)是硬件中断,si(softirqs)是软件中断。硬件中断一般由I/O设备引起,如网卡、磁盘等,发生硬件中断后,CPU需要立即处理,当硬件中断中需要处理的事情很多时,内核会生成相应的软中断,然后将耗时且不需要立即处理完成的操作放在软中断中执行,比如当网卡收到网络包时,需要CPU立即把数据拷贝到内存中去,因为网卡自带的缓存较小,如果不及时处理的话后面的数据包就进不来,导致丢包,当数据拷贝到内存中之后,就不需要那么着急的处理了,这时候可以将处理数据包(协议栈)的代码放在软中断中执行。本人不是内核专家,关于软中断的部分请参考[Understanding the Linux Kernel, 3rd Edition](https://www.safaribooksonline.com/library/view/understanding-the-linux/0596005652/ch04s07.html) 56 | 57 | * **0.0 st** : %st和虚拟机有关,当系统运行在虚拟机中时,当前虚拟机就会和宿主机以及其它的虚拟机共享CPU,%st就表示当前虚拟机在等待CPU为它服务的时间。该值越大,表示物理CPU被宿主机和其它虚拟机占用的时间越长,导致当前虚拟机得不到充足的CPU资源。如果%st长时间大于0,说明CPU资源得不到满足,这时可以考虑将虚拟机移到其它机器上,或者减少当前机器运行的虚拟机数量。 58 | 59 | 上面这些统计项的总和等于100%,除了%idle之外,其它的任何一项数值过高都代表系统有问题,需要具体问题具体分析。 60 | 61 | ## 问题处理 62 | 63 | * **%us过高** : 表示有用户态进程占用了过多的CPU,通过top命令可以很清楚的看到是哪个进程,如果这不是预期的行为,可以通过kill命令杀死相应的进程或者重启它 64 | 65 | * **%sy过高** : 如果只是偶尔过高的话,不用担心,但如果是持续走高的话,就需要重视,有可能是某些进程的系统调用太频繁,比如进程不停的往控制台输出日志,但如果用户态的进程都没有问题,那可能是内核里面的代码出现了问题,尤其是代码写的不好的驱动模块 66 | 67 | * **%ni过高** : 说明有人用nice程序运行了比较耗CPU的进程。如果niceness值大于0的话,就没什么好担心的,因为它的优先级比默认优先级要低,不会影响CPU性能,但最好还是确认一下该进程不会抢占系统的其它资源,如内存、磁盘I/O等,避免对系统整体性能造成影响。如果niceness值小于0的话,表示该进程优先级高且占用CPU资源多,需要确保该进程占用的CPU资源是符合预期的,如果不是,可以用top命令把它找出来并kill掉或者重启。 68 | 69 | * **%wa过高** : 意味着系统中有进程在做大量的I/O操作,或者在读写速度比较慢的I/O设备,比如频繁的读写磁盘,这时可以通过[iotop](http://guichaz.free.fr/iotop/)命令来查看是哪些进程占I/O,然后再针对不同的进程做相应的处理;还有一种情况就是系统在频繁的使用交换分区,这时需要解决的就是内存的问题,而不是I/O的问题。 70 | 71 | * **%hi或者%si过高** : %hi过高一般是硬件出问题了,%si过高一般是内核里面的代码出问题了 72 | 73 | * **%st 过高** : 正如上面介绍介绍的那样,%st过高表示当前虚拟机得不到足够的CPU资源。这时可以考虑将当前虚拟机搬迁到其它的主机上,或者想办法降低当前主机的负载,比如关掉一些其它的虚拟机。 74 | 75 | ## 结束语 76 | load average和%Cpu(s)以不同的方式给出了当前主机的CPU负载情况,通过%Cpu(s)我们可以看到系统当前的实时负载,现在很多监控系统每隔一段时间都会采集一次%Cpu(s),然后存储起来以图形的方式展示出来,这样就能很直观的看到CPU负载的变化,当然如果没有这样的监控系统的话,通过load average也能大概的知道最近一段时间内的平均负载(最长15分钟)。 77 | 78 | ## 参考 79 | * [Understanding the Load Average on Linux and Other Unix-like Systems](http://www.howtogeek.com/194642/understanding-the-load-average-on-linux-and-other-unix-like-systems/) 80 | * [Understanding Linux CPU Load](http://blog.scoutapp.com/articles/2009/07/31/understanding-load-averages) 81 | * [Understanding Linux CPU stats](http://blog.scoutapp.com/articles/2015/02/24/understanding-linuxs-cpu-stats) -------------------------------------------------------------------------------- /linux/010_file_system_comparison.md: -------------------------------------------------------------------------------- 1 | # Linux下常见文件系统对比 2 | 3 | 本文将对Linux下常见的几种文件系统进行对比,包括ext2、ext3、ext4、XFS和Btrfs,希望能帮助大家更好的选择合适的文件系统。 4 | 5 | >内容来自于网上找的资料以及自己的一些经验,能力有限,错误在所难免,仅供参考 6 | 7 | ## 历史 8 | 9 | | 文件系统 | 创建者 | 创建时间 |最开始支持的平台| 10 | | :--- | :--- | :--- | :--- | 11 | |[ext2](https://en.wikipedia.org/wiki/Ext2)|[Rémy Card](https://en.wikipedia.org/wiki/R%C3%A9my_Card)|1993|Linux,[Hurd](https://en.wikipedia.org/wiki/GNU_Hurd)| 12 | |[XFS](https://en.wikipedia.org/wiki/XFS)|[SGI](https://en.wikipedia.org/wiki/Silicon_Graphics)| 1994|[IRIX](https://en.wikipedia.org/wiki/IRIX), Linux, FreeBSD| 13 | |[ext3](https://en.wikipedia.org/wiki/Ext3)|[Dr. Stephen C. Tweedie](https://en.wikipedia.org/wiki/Stephen_Tweedie)| 1999|Linux| 14 | |[ZFS](https://en.wikipedia.org/wiki/ZFS)|Sun| 2004|Solaris| 15 | |[ext4](https://en.wikipedia.org/wiki/Ext4)|众多开发者| 2006|Linux| 16 | |[Btrfs](https://en.wikipedia.org/wiki/Btrfs)|Oracle| 2007|Linux| 17 | 18 | 从创建时间可以看出他们所处的不同时代,因为Btrfs的实现借鉴自ZFS,所以这里也将ZFS列出来作为参考。 19 | 20 | ## 大小限制 21 | 22 | | 文件系统 | 最大文件名长度 | 最大文件大小 |最大分区大小| 23 | | :--- | :--- | :--- | :--- | 24 | |ext2|255 bytes |2 TB |16 TB| 25 | |ext3|255 bytes| 2 TB |16 TB| 26 | |ext4|255 bytes| 16 TB| 1 EB| 27 | |XFS|255 bytes |8 EB| 8 EB| 28 | |Btrfs|255 bytes| 16 EB| 16 EB| 29 | 30 | 最大文件和分区大小受格式化分区时所采用的块大小(block size)所影响,块越大,所支持的最大文件和分区越大,也越可能浪费磁盘空间,上表列出的数据基于4K的块大小。 31 | 32 | ## 代码规模 33 | 从代码规模可以看出文件系统的功能丰富程度以及复杂度,下面列出的数据来自于kernel-4.1-rc8,只是简单的用wc -l来统计,没有过滤空行、注释等。 34 | 35 | | 文件系统 | 源文件(.c) | 头文件(.h) | 36 | | :--- | :--- | :--- | 37 | |ext2 |8363 |1016| 38 | |ext3|16496 |1567| 39 | |ext4|44650| 4522| 40 | |XFS|89605 |15091| 41 | |Btrfs|105254 |7933| 42 | 43 | * Btrfs还在快速的开发过程中,代码行数可能还有比较大的变化 44 | * XFS和Btrfs都使用了B-tree 45 | 46 | ## ext2 47 | ext的优点是比较简单,文件比较少时性能较好,比较适合文件少的场景,主要缺点如下 48 | 49 | * inode的数量是固定不变的,在格式化分区的时候可以指定inode和数据块所占空间的比例,但一旦格式化好,后续就没法再改变了 50 | * 当块大小为4K时,单个文件大小不能超过2TB,分区大小不能超过16TB(目前硬盘大小一般都只有几TB,所以也不是什么大问题,) 51 | * 一个目录下最多只能有32000个子目录 52 | * 由于目录里面存储的文件和子目录都是以线性方式来组织的,所以遍历目录效率不高,尤其当目录下文件个数达到10K以上规模的时候,速度会明显的变慢 53 | * 当底层的磁盘分区空间变大时(使用LVM时很常见),ext2没法动态的扩展来使用增加的空间 54 | * 没有日志(Journal)功能,所以数据的安全性不高 55 | 56 | ## ext3 57 | ext3在ext2的基础上实现了下面几个功能,其它的都保持不变,即ext2的缺点ext3也有 58 | 59 | * 支持日志(Journal)功能,数据的安全性较ext2有很大的提高 60 | * 当底层的分区空间变大时,ext3可以自动扩展来使用增加的空间 61 | * 使用HTree来组织目录里面的文件和子目录,使目录下的文件和子目录数不再受性能限制(数量超过10K也不会有性能问题) 62 | 63 | ## ext4 64 | ext4借鉴了当前成熟的一些文件系统技术,在ext3上增加了一些功能,并且对性能做了一些改进,主要变化如下 65 | 66 | * 当块大小为4K时,支持的最大文件和最大分区大小分别达到了16TB和1EB 67 | * 不再受32000个子目录数的限制,支持不限数量的子目录个数 68 | * 支持Extents,提高了大文件的操作性能 69 | * 内部实现上支持一次分配多个数据块,较ext3的性能有所提高 70 | * 支持延时分配(即支持fallocate函数)(fallocate是libc的函数,在不支持该功能的文件系统上,libc会创建一个占用磁盘空间文件) 71 | * 支持在线快速扫描 72 | * 支持在线碎片整理(单个文件或者整个分区) 73 | * 日志(Journal)支持校验码(checksum),数据的安全性进一步提高 74 | * 支持无日志(No Journaling)模式(ext3不支持该功能),这样就和ext2一样,消除了写日志对性能的影响 75 | * 支持纳秒级的时间戳 76 | * 记录了文件的创建时间,由于相关的应用层工具还不支持,所以只能通过debug的方式看到文件的创建时间 77 | 78 | 这里是一个查看文件/etc/fstab创建时间的例子(文件存在/dev/sda1分区上): 79 | ```bash 80 | dev@ubuntu:~$ ls -i /etc/fstab 81 | 10747906 /etc/fstab 82 | dev@ubuntu:~$ sudo debugfs -R 'stat <10747906>' /dev/sda1 83 | Inode: 10747906 Type: regular Mode: 0644 Flags: 0x80000 84 | Links: 1 Blockcount: 8 85 | ctime: 0x5546dc54:6e6bc80c -- Sun May 3 22:41:24 2015 86 | atime: 0x55d1b014:8bcf7b44 -- Mon Aug 17 05:57:40 2015 87 | mtime: 0x5546dc54:6e6bc80c -- Sun May 3 22:41:24 2015 88 | crtime: 0x5546dc54:6e6bc80c -- Sun May 3 22:41:24 2015 89 | Size of extra inode fields: 28 90 | EXTENTS: (0):46712815 91 | ``` 92 | 93 | 94 | **Extents:** 在最开始的ext2文件系统中,数据块都是一个一个单独管理的,inode中存有指向数据块的指针,文件占用了多少个数据块,inode里面就有多少个指针(多级),想象一下一个1G的文件,4K的块大小,那么需要(1024 * 1024)/4=262144个数据块,即需要262144个指针,创建文件的时候需要初始化这些指针,删除文件的时候需要回收这些指针,影响性能。现代的文件系统都支持Extents的功能,简单点说,Extent就是数据块的集合,以前一次分配一个数据块,现在可以一次分配一个Extent,里面包含很多数据块,同时inode里面只需要分配指向Extent的指针就可以了,从而大大减少了指针的数量和层级,提高了大文件操作的性能。 95 | 96 | **inode数量固定:** 在ext2/3/4系列的文件系统中,inode的数量都是固定的,坏处是如果存很多小文件的话,有可能造成inode被用光,但磁盘还有很多剩余空间无法被使用的情况,不过它也有一个好处,就是一旦磁盘损坏,恢复起来要相对简单些,因为数据在磁盘上布局相对要固定简单。 97 | 98 | ## xfs 99 | 和ext4相比,xfs不支持下面这些功能 100 | 101 | * 不支持日志(Journal)校验码 102 | * 不支持无日志(No Journaling)模式 103 | * 不支持文件创建时间 104 | * 不支持数据日志(data journal),只有元数据日志(metadata journal) 105 | 106 | 但xfs有下面这些特性 107 | 108 | * 支持的最大文件和分区都达到了8EB 109 | * inode动态分配,从而不受inode数量的限制,再也不用担心存储大量小文件导致inode不够用的问题了。 110 | * [更大的xattr(extended attributes)](https://en.wikipedia.org/wiki/XFS#Extended_attributes)空间,ext2/3/4及btrfs都限制xattr的长度不能超过一个块(一般是4K),而xfs可以达到64K 111 | * 内部采用Allocation groups机制,各个group之间没有依赖,支持并发操作,在多核环境的某些场景下性能表现不错 112 | * 提供了原生的dump和restore工具,并且支持在线dump 113 | 114 | ## btrfs 115 | btrfs是一个和ZFS类似的文件系统,支持的功能非常多,据说将来会替换ext4成为Linux下的默认文件系统。这里列举一些重要的功能 116 | 117 | * 支持的最大文件和分区达到了16EB 118 | * 支持COW(copy on write) 119 | * 针对小文件和SSD做了优化 120 | * inode动态分配 121 | * 支持子分区(Subvolumes),子分区可以单独挂载 122 | * 支持元数据和数据的校验(crc32) 123 | * 支持压缩,去重 124 | * 支持多个磁盘和分区,可动态扩展 125 | * 支持LVM,RAID的功能(有了btrfs,就不再需要lvm和软raid了) 126 | * 增量备份和恢复 127 | * 支持快照 128 | * 将ext2/3/4转换成btrfs(反过来不行) 129 | 130 | btrfs最大的缺点就是由于其COW的实现方式,导致碎片化问题比较严重,不太适合频繁写的场景,比如数据库、虚拟机的磁盘文件等。不过大部分场合不需要担心,btrfs有在线的碎片整理工具。 131 | 132 | ## 如何选择 133 | 下表仅供参考 134 | 135 | |文件系统 |适用场景 |原因| 136 | | :--- | :--- | :--- | 137 | |ext2| U盘 |U盘一般不会存很多文件,且U盘的文件在电脑上有备份,安全性要求没那么高,由于ext2不写日志(journal),所以写U盘性能比较好。当然由于ext2的兼容性没有fat好,目前大多数U盘格式还是用fat| 138 | |ext3 |对稳定性要求高的地方 |有了ext4后,好像没什么原因还要用ext3,ext4现在的问题是出来时间不长,还需要一段时间变稳定| 139 | |ext4 |小文件较少 |ext系列的文件系统都不支持inode动态分配,所以如果有大量小文件需要存储的话,不建议用ext4| 140 | |xfs |小文件多或者需要大的xttr空间,如[openstack swift](https://docs.openstack.org/developer/swift/)将数据文件的元数据放在了xttr里面 |xfs支持inode动态分配,所以不存在inode不够的情况,并且xttr的最大长度可以达到64K| 141 | |btrfs |没有频繁的写操作,且需要btrfs的一些特性 |btrfs虽然还不稳定,但支持众多的功能,如果你需要这些功能,且不会频繁的写文件,那么选择btrfs| 142 | 143 | 另外,ext系列文件系统内部结构相对简单一些,出问题后恢复相对容易。 144 | 145 | ## 结束语 146 | 本篇没有比较它们的性能,在通常情况下,他们之间没有太大的性能差别,只有在特定的场景下,才能看出区别,如果对性能比较敏感,建议根据自己的使用场景来测试不同的文件系统,然后根据结果来选择。 147 | -------------------------------------------------------------------------------- /linux/011_aufs.md: -------------------------------------------------------------------------------- 1 | # Linux文件系统之aufs 2 | aufs的全称是advanced multi-layered unification filesystem,主要功能是把多个文件夹的内容合并到一起,提供一个统一的视图,主要用于各个Linux发行版的livecd中,以及docker里面用来组织image。 3 | 4 | 据说由于aufs代码的可维护性不好(代码可读性和注释不太好),所以一直没有被合并到Linux内核的主线中去,不过有些发行版的kernel里面维护的有该文件系统,比如在ubuntu 16.04的内核代码中,就有该文件系统。 5 | 6 | >本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过 7 | 8 | ## 检查系统是否支持aufs 9 | 在使用aufs之前,可以通过下面的命令确认当前系统是否支持aufs,如果不支持,请自行根据相应发行版的文档安装 10 | ```bash 11 | #下面的命令如果没有输出,表示内核不支持aufs 12 | #由于ubuntu 16.04的内核中已经将aufs编译进去了,所以默认就支持 13 | dev@ubuntu:~$ grep aufs /proc/filesystems 14 | nodev aufs 15 | #这里nodev表示该文件系统不需要建在设备上 16 | ``` 17 | 18 | >注意:有些Linux发行版可能将aufs编译成了模块,所以虽然这里显示内核不支持,但其实后面的命令都能正常运行 19 | 20 | ## 挂载aufs 21 | 选择好相应的参数(参考[帮助文档](http://manpages.ubuntu.com/manpages/xenial/en/man5/aufs.5.html)),调用mount命令即可,示例如下 22 | ``` 23 | # mount -t aufs -o br=./Branch-0:./Branch-1:./Branch-2 none ./MountPoint 24 | ``` 25 | 26 | * -t aufs: 指定挂载类型为aufs 27 | * -o br=./Branch-0:./Branch-1:./Branch-2: 表示将当前目录下的Branch-0,Branch-1,Branch-2三个文件夹联合到一起 28 | * none:aufs不需要设备,只依赖于-o br指定的文件夹,所以这里填none即可 29 | * ./MountPoint:表示将最后联合的结果挂载到当前的MountPoint目录下,然后我们就可以往这个目录里面读写文件了 30 | 31 | 假设Branch-0里面有文件001.txt、003.txt,Branch-1里面有文件001.txt、003.txt、004.txt,Branch-2里面有文件002.txt、003.txt。 32 | 33 | mount完成后,得到的结果将会如下图所示 34 | ``` 35 | /*001.txt(b0)表示Branch-0的001.txt文件,其它的以此类推*/ 36 | +-------------+-------------+-------------+-------------+ 37 | MountPoint | 001.txt(b0) | 002.txt(b2) | 003.txt(b0) | 004.txt(b1) | 38 | +-------------+-------------+-------------+-------------+ 39 | ↑ ↑ ↑ ↑ 40 | | | | | 41 | +-------------+-------------+-------------+-------------+ 42 | Branch-0 | 001.txt | | 003.txt | | 43 | +-------------+-------------+-------------+-------------+ 44 | Branch-1 | 001.txt | | 003.txt | 004.txt | 45 | +-------------+-------------+-------------+-------------+ 46 | Branch-2 | | 002.txt | 003.txt | | 47 | +-------------+-------------+-------------+-------------+ 48 | ``` 49 | 50 | 联合之后,在MountPoint下将会看到四个文件,分别是Branch-0下的001.txt、003.txt,Branch-1下的04.txt,以及Branch-2下的002.txt。 51 | 52 | * branch是aufs里面的概念,其实一个branch就是一个目录,所以上面的Branch-0,1,2就是三个目录 53 | * branch是有index的,index越小的branch会放在最上面,如果多个branch里面有同样的文件,只有index最小的那个branch下的文件才会被访问到 54 | * MountPoint就是最后这三个目录联合后挂载到的位置,访问这个目录下的文件都会经过aufs文件系统,换句话说,直接访问Branch-0,1,2这三个目录的话,aufs是不知道的 55 | 56 | >注意:并不是所有文件系统里的目录都能作为aufs的branch,目前aufs不支持的有:btrfs aufs eCryptfs 57 | 58 | 59 | 读这些文件的时候访问的是最上层的文件,但如果要写这些文件呢?或者在挂载点下创建新的文件呢?请看下面的示例 60 | 61 | ## 只读挂载 62 | 挂载时,可以指定每个branch的读写权限,如果不指定的话,第一个目录将会是可写的,其它的目录是只读的,在实际使用时,最好是显示的指定每个branch的读写属性,这样大家都一眼就能看懂。这里先演示一下只读挂载: 63 | 64 | ```bash 65 | #准备相应的目录和文件 66 | dev@ubuntu:~$ mkdir /tmp/aufs && cd /tmp/aufs 67 | dev@ubuntu:/tmp/aufs$ mkdir dir0 dir1 root 68 | dev@ubuntu:/tmp/aufs$ echo dir0 > dir0/001.txt 69 | dev@ubuntu:/tmp/aufs$ echo dir0 > dir0/002.txt 70 | dev@ubuntu:/tmp/aufs$ echo dir1 > dir1/002.txt 71 | dev@ubuntu:/tmp/aufs$ echo dir1 > dir1/003.txt 72 | #最后用tree命令来看看最终的目录结构 73 | dev@ubuntu:/tmp/aufs$ tree 74 | . 75 | ├── dir0 76 | │   ├── 001.txt 77 | │   └── 002.txt 78 | ├── dir1 79 | │   ├── 002.txt 80 | │   └── 003.txt 81 | └── root 82 | 83 | #通过指定ro参数来让两个branch都为只读 84 | dev@ubuntu:/tmp/aufs$ sudo mount -t aufs -o br=./dir0=ro:./dir1=ro none ./root 85 | #联合后最终的root目录下将看到三个文件 86 | dev@ubuntu:/tmp/aufs$ ls root/ 87 | 001.txt 002.txt 003.txt 88 | #其中002.txt的内容是dir0中的002.txt的内容,说明dir0的index要比dir1的index小 89 | dev@ubuntu:/tmp/aufs$ cat root/002.txt 90 | dir0 91 | 92 | #由于是只读挂载,所以touch失败 93 | dev@ubuntu:/tmp/aufs$ touch root/001.txt 94 | touch: cannot touch 'root/001.txt': Read-only file system 95 | dev@ubuntu:/tmp/aufs$ touch root/003.txt 96 | touch: cannot touch 'root/003.txt': Read-only file system 97 | 98 | #但是我们可以跳过root目录来修改001.txt和003.txt, 99 | #因为跳过了root目录,所以就不受aufs控制 100 | dev@ubuntu:/tmp/aufs$ touch dir0/001.txt 101 | dev@ubuntu:/tmp/aufs$ touch dir1/003.txt 102 | 103 | #我们还能在下面的目录中创建新的文件 104 | dev@ubuntu:/tmp/aufs$ touch dir1/004.txt 105 | #新创建的文件能及时的反应到挂载点上去 106 | dev@ubuntu:/tmp/aufs$ ls ./root/ 107 | 001.txt 002.txt 003.txt 004.txt 108 | 109 | #删除该文件,以免影响后续的演示 110 | dev@ubuntu:/tmp/aufs$ rm ./dir1/004.txt 111 | ``` 112 | 113 | 从上面的演示可以看出,我们可以跳过挂载点直接读写底层的目录,这样就不受aufs的控制,但我们修改的内容(dir1里面创建的004.txt)还是能在挂载点下看到,这是因为aufs在访问文件时,默认的做法是如果最上层目录里面没这个文件,就一层一层的往下找,所以下层有变动的话,aufs会自动发现。控制这种行为的参数为“udba”,有兴趣可以参考[帮助文档](http://manpages.ubuntu.com/manpages/xenial/en/man5/aufs.5.html) 114 | 115 | >由于访问一个文件时需要一级一级往下找,所以如果联合的目录(层级)过多的话,会影响性能 116 | 117 | ## 读写挂载 118 | 如果联合的文件夹有写的权限,那么所有的修改都会写入可写的那个文件夹,如果可写的文件夹有多个,那么写入哪个文件夹就依赖于相应的策略,有round-robin、最多剩余空间等,详情请参考[帮助文档](http://manpages.ubuntu.com/manpages/xenial/en/man5/aufs.5.html)中的“create”参数,这里不做介绍。 119 | 120 | ```bash 121 | dev@ubuntu:/tmp/aufs$ sudo umount ./root 122 | #dir0具有读写权限,dir1为只读权限 123 | dev@ubuntu:/tmp/aufs$ sudo mount -t aufs -o br=./dir0=rw:./dir1=ro none ./root 124 | dev@ubuntu:/tmp/aufs$ echo "root->write" >> ./root/001.txt 125 | dev@ubuntu:/tmp/aufs$ echo "root->write" >> ./root/002.txt 126 | dev@ubuntu:/tmp/aufs$ echo "root->write" >> ./root/003.txt 127 | dev@ubuntu:/tmp/aufs$ echo "root->write" >> ./root/005.txt 128 | 129 | #跟开始前相比,dir0目录下多了003.txt和005.txt,其它的保持不变 130 | dev@ubuntu:/tmp/aufs$ ls ./root/ 131 | 001.txt 002.txt 003.txt 005.txt 132 | dev@ubuntu:/tmp/aufs$ ls ./dir0/ 133 | 001.txt 002.txt 003.txt 005.txt 134 | dev@ubuntu:/tmp/aufs$ ls ./dir1/ 135 | 002.txt 003.txt 136 | 137 | #再来看看内容,dir1里面的内容保持不变 138 | dev@ubuntu:/tmp/aufs$ cat ./dir1/002.txt 139 | dir1 140 | dev@ubuntu:/tmp/aufs$ cat ./dir1/003.txt 141 | dir1 142 | 143 | #dir0下的文件内容都变了 144 | dev@ubuntu:/tmp/aufs$ cat ./dir0/001.txt 145 | dir0 146 | root->write 147 | dev@ubuntu:/tmp/aufs$ cat ./dir0/002.txt 148 | dir0 149 | root->write 150 | dev@ubuntu:/tmp/aufs$ cat ./dir0/003.txt 151 | dir1 152 | root->write 153 | dev@ubuntu:/tmp/aufs$ cat ./dir0/005.txt 154 | root->write 155 | ``` 156 | 157 | * 当创建一个新文件的时候,新的文件会写入具有rw权限的那个目录,如果有多个目录具有rw权限,那么依赖于挂载时配置的的创建策略 158 | * 当修改一个具有rw权限目录下的文件时,直接修改该文件 159 | * 当修改一个只有ro权限目录下的文件时,aufs会先将该文件拷贝到一个rw权限的目录里面,然后在上面进行修改,这就是所谓的COW(copy on write),拷贝的速度依赖于底层branch所在的文件系统。 160 | 161 | 从上面可以看出,COW对于大文件来说,性能还是很低的,同时也会占用很多的空间,但由于只需要在第一次修改的时候拷贝一次,所以很多情况下还是能接受。 162 | 163 | ## 删除文件 164 | 删除文件时,如果该文件只在rw目录下有,那就直接删除rw目录下的该文件,如果该文件在ro目录下有,那么aufs将会在rw目录里面创建一个.wh开头的文件,标识该文件已被删除 165 | 166 | ```bash 167 | #通过aufs删除所有文件 168 | dev@ubuntu:/tmp/aufs$ rm ./root/001.txt ./root/002.txt ./root/003.txt ./root/005.txt 169 | 170 | #dir0下的文件全被删除了,但dir1目录下的文件没动 171 | dev@ubuntu:/tmp/aufs$ tree 172 | . 173 | ├── dir0 174 | ├── dir1 175 | │   ├── 002.txt 176 | │   └── 003.txt 177 | └── root 178 | 179 | #通过-a参数来看看dir0目录下的内容 180 | #可以看到aufs为002.txt和003.txt新建了两个特殊的以.wh开头的文件, 181 | #用来表示这两个文件已经被删掉了 182 | #这里其他.wh开头的文件都是aufs用到的一些属性文件 183 | dev@ubuntu:/tmp/aufs$ ls ./dir0/ -a 184 | . .. .wh.002.txt .wh.003.txt .wh..wh.aufs .wh..wh.orph .wh..wh.plnk 185 | ``` 186 | 187 | ## 结束语 188 | 这里只介绍了aufs的基本功能,其它的高级配置项没有涉及,比如动态的增加和删除branch等。 189 | 190 | 使用aufs时,建议参考livecd及docker的使用方式,就是将所有的目录都以只读的方式和一个支持读写的空目录联合起来,这样所有的修改都会存到那个指定的空目录中,不用之后删除掉那个目录就可以了,并且在使用的过程中不要绕过aufs直接操作底层的branch,也不要动态的增加和删除branch,如果把使用场景弄得太复杂,由于aufs里面的细节很多,很有可能会由于对aufs的理解不深而踩坑。 191 | 192 | ## 参考 193 | * [aufs](http://aufs.sourceforge.net/) 194 | * [aufs manual](http://manpages.ubuntu.com/manpages/xenial/en/man5/aufs.5.html) 195 | * [Linux AuFS Examples](http://www.thegeekstuff.com/2013/05/linux-aufs/) -------------------------------------------------------------------------------- /linux/015_network_sending_data.md: -------------------------------------------------------------------------------- 1 | # Linux网络 - 数据包的发送过程 2 | 3 | 继上一篇介绍了[数据包的接收过程](014_network_receiving_data.md)后,本文将介绍在Linux系统中,数据包是如何一步一步从应用程序到网卡并最终发送出去的。 4 | 5 | 如果英文没有问题,强烈建议阅读后面参考里的文章,里面介绍的更详细。 6 | 7 | >本文只讨论以太网的物理网卡,并且以一个UDP包的发送过程作为示例,由于本人对协议栈的代码不熟,有些地方可能理解有误,欢迎指正 8 | 9 | ## socket层 10 | ``` 11 | +-------------+ 12 | | Application | 13 | +-------------+ 14 | | 15 | | 16 | ↓ 17 | +------------------------------------------+ 18 | | socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) | 19 | +------------------------------------------+ 20 | | 21 | | 22 | ↓ 23 | +-------------------+ 24 | | sendto(sock, ...) | 25 | +-------------------+ 26 | | 27 | | 28 | ↓ 29 | +--------------+ 30 | | inet_sendmsg | 31 | +--------------+ 32 | | 33 | | 34 | ↓ 35 | +---------------+ 36 | | inet_autobind | 37 | +---------------+ 38 | | 39 | | 40 | ↓ 41 | +-----------+ 42 | | UDP layer | 43 | +-----------+ 44 | 45 | 46 | ``` 47 | 48 | * **socket(...):** 创建一个socket结构体,并初始化相应的操作函数,由于我们定义的是UDP的socket,所以里面存放的都是跟UDP相关的函数 49 | * **sendto(sock, ...):** 应用层的程序(Application)调用该函数开始发送数据包,该函数数会调用后面的inet_sendmsg 50 | * **inet_sendmsg:** 该函数主要是检查当前socket有没有绑定源端口,如果没有的话,调用inet_autobind分配一个,然后调用UDP层的函数 51 | * **inet_autobind:** 该函数会调用socket上绑定的get_port函数获取一个可用的端口,由于该socket是UDP的socket,所以get_port函数会调到UDP代码里面的相应函数。 52 | 53 | ## UDP层 54 | ``` 55 | | 56 | | 57 | ↓ 58 | +-------------+ 59 | | udp_sendmsg | 60 | +-------------+ 61 | | 62 | | 63 | ↓ 64 | +----------------------+ 65 | | ip_route_output_flow | 66 | +----------------------+ 67 | | 68 | | 69 | ↓ 70 | +-------------+ 71 | | ip_make_skb | 72 | +-------------+ 73 | | 74 | | 75 | ↓ 76 | +------------------------+ 77 | | udp_send_skb(skb, fl4) | 78 | +------------------------+ 79 | | 80 | | 81 | ↓ 82 | +----------+ 83 | | IP layer | 84 | +----------+ 85 | 86 | ``` 87 | 88 | * **udp_sendmsg:** udp模块发送数据包的入口,该函数较长,在该函数中会先调用ip_route_output_flow获取路由信息(主要包括源IP和网卡),然后调用ip_make_skb构造skb结构体,最后将网卡的信息和该skb关联。 89 | * **ip_route_output_flow:** 该函数会根据路由表和目的IP,找到这个数据包应该从哪个设备发送出去,如果该socket没有绑定源IP,该函数还会根据路由表找到一个最合适的源IP给它。 如果该socket已经绑定了源IP,但根据路由表,从这个源IP对应的网卡没法到达目的地址,则该包会被丢弃,于是数据发送失败,sendto函数将返回错误。该函数最后会将找到的设备和源IP塞进flowi4结构体并返回给udp_sendmsg 90 | * **ip_make_skb:** 该函数的功能是构造skb包,构造好的skb包里面已经分配了IP包头,并且初始化了部分信息(IP包头的源IP就在这里被设置进去),同时该函数会调用__ip_append_dat,如果需要分片的话,会在__ip_append_data函数中进行分片,同时还会在该函数中检查socket的send buffer是否已经用光,如果被用光的话,返回ENOBUFS 91 | * **udp_send_skb(skb, fl4)** 主要是往skb里面填充UDP的包头,同时处理checksum,然后调用IP层的相应函数。 92 | 93 | ## IP层 94 | ``` 95 | | 96 | | 97 | ↓ 98 | +-------------+ 99 | | ip_send_skb | 100 | +-------------+ 101 | | 102 | | 103 | ↓ 104 | +-------------------+ +-------------------+ +---------------+ 105 | | __ip_local_out_sk |------>| NF_INET_LOCAL_OUT |------>| dst_output_sk | 106 | +-------------------+ +-------------------+ +---------------+ 107 | | 108 | | 109 | ↓ 110 | +------------------+ +----------------------+ +-----------+ 111 | | ip_finish_output |<-------| NF_INET_POST_ROUTING |<------| ip_output | 112 | +------------------+ +----------------------+ +-----------+ 113 | | 114 | | 115 | ↓ 116 | +-------------------+ +------------------+ +----------------------+ 117 | | ip_finish_output2 |----->| dst_neigh_output |------>| neigh_resolve_output | 118 | +-------------------+ +------------------+ +----------------------+ 119 | | 120 | | 121 | ↓ 122 | +----------------+ 123 | | dev_queue_xmit | 124 | +----------------+ 125 | ``` 126 | 127 | 128 | * **ip_send_skb:** IP模块发送数据包的入口,该函数只是简单的调用一下后面的函数 129 | * **__ip_local_out_sk:** 设置IP报文头的长度和checksum,然后调用下面netfilter的钩子 130 | * **NF_INET_LOCAL_OUT:** netfilter的钩子,可以通过iptables来配置怎么处理该数据包,如果该数据包没被丢弃,则继续往下走 131 | * **dst_output_sk:** 该函数根据skb里面的信息,调用相应的output函数,在我们UDP IPv4这种情况下,会调用ip_output 132 | * **ip_output:** 将上面udp_sendmsg得到的网卡信息写入skb,然后调用NF_INET_POST_ROUTING的钩子 133 | * **NF_INET_POST_ROUTING:** 在这里,用户有可能配置了SNAT,从而导致该skb的路由信息发生变化 134 | * **ip_finish_output:** 这里会判断经过了上一步后,路由信息是否发生变化,如果发生变化的话,需要重新调用dst_output_sk(重新调用这个函数时,可能就不会再走到ip_output,而是走到被netfilter指定的output函数里,这里有可能是xfrm4_transport_output),否则往下走 135 | * **ip_finish_output2:** 根据目的IP到路由表里面找到下一跳(nexthop)的地址,然后调用__ipv4_neigh_lookup_noref去arp表里面找下一跳的neigh信息,没找到的话会调用__neigh_create构造一个空的neigh结构体 136 | * **dst_neigh_output:** 在该函数中,如果上一步ip_finish_output2没得到neigh信息,那么将会走到函数neigh_resolve_output中,否则直接调用neigh_hh_output,在该函数中,会将neigh信息里面的mac地址填到skb中,然后调用dev_queue_xmit发送数据包 137 | * **neigh_resolve_output:** 该函数里面会发送arp请求,得到下一跳的mac地址,然后将mac地址填到skb中并调用dev_queue_xmit 138 | 139 | ## netdevice子系统 140 | ``` 141 | | 142 | | 143 | ↓ 144 | +----------------+ 145 | +----------------| dev_queue_xmit | 146 | | +----------------+ 147 | | | 148 | | | 149 | | ↓ 150 | | +-----------------+ 151 | | | Traffic Control | 152 | | +-----------------+ 153 | | loopback | 154 | | or +--------------------------------------------------------------+ 155 | | IP tunnels ↓ | 156 | | ↓ | 157 | | +---------------------+ Failed +----------------------+ +---------------+ 158 | +----------->| dev_hard_start_xmit |---------->| raise NET_TX_SOFTIRQ |- - - - >| net_tx_action | 159 | +---------------------+ +----------------------+ +---------------+ 160 | | 161 | +----------------------------------+ 162 | | | 163 | ↓ ↓ 164 | +----------------+ +------------------------+ 165 | | ndo_start_xmit | | packet taps(AF_PACKET) | 166 | +----------------+ +------------------------+ 167 | ``` 168 | 169 | * **dev_queue_xmit:** netdevice子系统的入口函数,在该函数中,会先获取设备对应的qdisc,如果没有的话(如loopback或者IP tunnels),就直接调用dev_hard_start_xmit,否则数据包将经过[Traffic Control](http://tldp.org/HOWTO/Traffic-Control-HOWTO/intro.html)模块进行处理 170 | * **Traffic Control:** 这里主要是进行一些过滤和优先级处理,在这里,如果队列满了的话,数据包会被丢掉,详情请参考[文档](http://tldp.org/HOWTO/Traffic-Control-HOWTO/intro.html),这步完成后也会走到dev_hard_start_xmit 171 | * **dev_hard_start_xmit: ** 该函数中,首先是拷贝一份skb给“packet taps”,tcpdump就是从这里得到数据的,然后调用ndo_start_xmit。如果dev_hard_start_xmit返回错误的话(大部分情况可能是NETDEV_TX_BUSY),调用它的函数会把skb放到一个地方,然后抛出软中断NET_TX_SOFTIRQ,交给软中断处理程序net_tx_action稍后重试(如果是loopback或者IP tunnels的话,失败后不会有重试的逻辑) 172 | * **ndo_start_xmit: ** 这是一个函数指针,会指向具体驱动发送数据的函数 173 | 174 | ## Device Driver 175 | ndo_start_xmit会绑定到具体网卡驱动的相应函数,到这步之后,就归网卡驱动管了,不同的网卡驱动有不同的处理方式,这里不做详细介绍,其大概流程如下: 176 | 177 | 1. 将skb放入网卡自己的发送队列 178 | 2. 通知网卡发送数据包 179 | 3. 网卡发送完成后发送中断给CPU 180 | 4. 收到中断后进行skb的清理工作 181 | 182 | 在网卡驱动发送数据包过程中,会有一些地方需要和netdevice子系统打交道,比如网卡的队列满了,需要告诉上层不要再发了,等队列有空闲的时候,再通知上层接着发数据。 183 | 184 | ## 其它 185 | 186 | * **SO_SNDBUF:** 从上面的流程中可以看出来,对于UDP来说,没有一个对应send buffer存在,SO_SNDBUF只是一个限制,当这个socket分配的skb占用的内存超过这个值的时候,会返回ENOBUFS,所以说只要不出现ENOBUFS错误,把这个值调大没有意义。从sendto函数的帮助文件里面看到这样一句话:(Normally, this does not occur in Linux. Packets are just silently dropped when a device queue overflows.)。这里的device queue应该指的是Traffic Control里面的queue,说明在linux里面,默认的SO_SNDBUF值已经够queue用了,疑问的地方是,queue的长度和个数是可以配置的,如果配置太大的话,按道理应该有可能会出现ENOBUFS的情况。 187 | 188 | * **txqueuelen:** 很多地方都说这个是控制qdisc里queue的长度的,但貌似只是部分类型的qdisc用了该配置,如linux默认的pfifo_fast。 189 | 190 | * **hardware RX:** 一般网卡都有一个自己的ring queue,这个queue的大小可以通过ethtool来配置,当驱动收到发送请求时,一般是放到这个queue里面,然后通知网卡发送数据,当这个queue满的时候,会给上层调用返回NETDEV_TX_BUSY 191 | 192 | * **packet taps(AF_PACKET):** 当第一次发送数据包和重试发送数据包时,都会经过这里,如果发生重试的情况的话,不确定tcpdump是否会抓到两次包,按道理应该不会,可能是我哪里没看懂 193 | 194 | ## 参考 195 | 196 | * [Monitoring and Tuning the Linux Networking Stack: Sending Data](https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data/) 197 | * [queueing in the linux network stack](https://www.coverfire.com/articles/queueing-in-the-linux-network-stack/) -------------------------------------------------------------------------------- /linux/020_session_and_job.md: -------------------------------------------------------------------------------- 1 | # Linux session和进程组概述 2 | 3 | 在[上一篇](019_tty.md)中介绍了tty的相关原理,这篇将介绍跟tty密切相关的session和进程组。 4 | 5 | >本篇主要目的是澄清一些概念,不涉及细节 6 | 7 | ## session 8 | session就是一组进程的集合,session id就是这个session中leader的进程ID。 9 | 10 | ### session的特点 11 | session的主要特点是当session的leader退出后,session中的所有其它进程将会收到SIGHUP信号,其默认行为是终止进程,即session的leader退出后,session中的其它进程也会退出。 12 | 13 | 如果session和tty关联的话,它们之间只能一一对应,一个tty只能属于一个session,一个session只能打开一个tty。当然session也可以不和任何tty关联。 14 | 15 | ### session的创建 16 | session可以在任何时候创建,调用setsid函数即可,session中的第一个进程即为这个session的leader,leader是不能变的。常见的创建session的场景是: 17 | 18 | * 用户登录后,启动shell时将会创建新的session,shell会作为session的leader,随后shell里面运行的进程都将属于这个session,当shell退出后,所有该用户运行的进程将退出。这类session一般都会和一个特定的tty关联,session的leader会成为tty的控制进程,当session的前端进程组发生变化时,控制进程负责更新tty上关联的前端进程组,当tty要关闭的时候,控制进程所在session的所有进程都会收到SIGHUP信号。 19 | * 启动deamon进程,这类进程需要和父进程划清界限,所以需要启动一个新的session。这类session一般不会和任何tty关联。 20 | 21 | ## 进程组 22 | 进程组(process group)也是一组进程的集合,进程组id就是这个进程组中leader的进程ID。 23 | 24 | ### 进程组的特点 25 | 进程组的主要特点是可以以进程组为单位通过函数[killpg](http://man7.org/linux/man-pages/man3/killpg.3.html)发送信号 26 | 27 | ### 进程组的创建 28 | 进程组主要用在shell里面,shell负责进程组的管理,包括创建、销毁等。(这里shell就是session的leader) 29 | 30 | * 对大部分进程来说,它自己就是进程组的leader,并且进程组里面就只有它自己一个进程 31 | * shell里面执行类似```ls|more```这样的以管道连接起来的命令时,两个进程就属于同一个进程组,ls是进程组的leader。 32 | * shell里面启动一个进程后,一般都会将该进程放到一个单独的进程组,然后该进程fork的所有进程都会属于该进程组,比如多进程的程序,它的所有进程都会属于同一个进程组,当在shell里面按下CTRL+C时,该程序的所有进程都会收到SIGINT而退出。 33 | 34 | ### 后台进程组 35 | shell中启动一个进程时,默认情况下,该进程就是一个前端进程组的leader,可以收到用户的输入,并且可以将输出打印到终端,只有当该进程组退出后,shell才可以再响应用户的输入。 36 | 37 | 但我们也可以将该进程组运行在后台,这样shell就可以继续相应用户的输入,常见的方法如下: 38 | 39 | * 启动程序时,在后面加&,如```sleep 1000 &```,进程将会进入后台继续运行 40 | * 程序启动后,可以按CTRL+Z让它进入后台,和后面加&不同的是,进程会被暂停执行 41 | 42 | 对于后台运行的进程组,在shell里面体现为job的概念,即一个后台进程组就是一个job,job有如下限制: 43 | 44 | * 默认情况下,只要后台进程组的任何一个进程读tty,将会使整个进程组的所有进程暂停 45 | * 默认情况下,只要后台进程组的任何一个进程写tty,将有可能会使整个进程组的所有进程暂停(依赖于tty的配置,请参考[TTY/PTS概述](019_tty.md)) 46 | 47 | 所有后台运行的进程组可以通过jobs命令查看到,也可以通过fg命令将后台进程组切换到前端,这样就可以继续接收用户的输入了。这两个命令的具体用法请参考它们的帮助文件,这里只给出一个简单的例子: 48 | ```bash 49 | #通常情况下,sleep命令会一直等待在那里,直到指定的时间过去后才退出。 50 | #shell启动sleep程序时,就将sleep放到了一个新的进程组, 51 | #并且该进程组为前端进程组,虽然sleep不需要输入,也没有输出, 52 | #但当前session的标准输入和输出还是归它,别人用不了, 53 | #只有我们按下CTRL+C使sleep进程退出后,shell自己重新变成了前端进程组, 54 | #于是shell重新具备了响应输入以及输出能力 55 | dev@debian:~$ sleep 1000 56 | ^C 57 | 58 | #我们可以在命令行的后面加上&符号,shell还是照样会创建新的进程组, 59 | #并且sleep进程就是新进程组的leader, 60 | #但是shell会将sleep进程组放到后端,让它成为后台进程组 61 | #这里[1]是job id,1627是进程组的ID,即sleep进程的id 62 | dev@debian:~$ sleep 1000 & 63 | [1] 1627 64 | 65 | #可以通过jobs命令看到当前有哪些后台进程组(job) 66 | dev@debian:~$ jobs 67 | [1]+ Running sleep 1000 & 68 | 69 | #使用fg命令带上job id,即可让后端进程组回到前端, 70 | #然后我们使用CTRL+Z命令可以让它再次回到后端,并暂停进程的执行 71 | #CTRL+Z和&不一样的地方就是CTRL+Z会让进程暂停执行,而&不会 72 | dev@debian:~$ fg 1 73 | sleep 1000 74 | ^Z 75 | [1]+ Stopped sleep 1000 76 | #Stopped状态表示进程在后台已经暂停执行了 77 | dev@debian:~$ jobs 78 | [1]+ Stopped sleep 1000 79 | 80 | ``` 81 | 82 | ## session和进程组的关系 83 | deamon程序虽然也是一个session的leader,但一般它不会创建新的进程组,也没有job的管理功能,所以这种情况下一个session就只有一个进程组,所有的进程都属于同样的进程组和session。 84 | 85 | 我们这里看一下shell作为session leader的情况,假设我们在shell里面执行了这些命令: 86 | ```bash 87 | dev@debian:~$ sleep 1000 & 88 | [1] 1646 89 | dev@debian:~$ cat | wc -l & 90 | [2] 1648 91 | dev@debian:~$ jobs 92 | [1]- Running sleep 1000 & 93 | [2]+ Stopped cat | wc -l 94 | ``` 95 | 96 | 下面这张图标明了这种情况下它们之间的关系: 97 | ``` 98 | +--------------------------------------------------------------+ 99 | | | 100 | | pg1 pg2 pg3 pg4 | 101 | | +------+ +-------+ +-----+ +------+ | 102 | | | bash | | sleep | | cat | | jobs | | 103 | | +------+ +-------+ +-----+ +------+ | 104 | | session leader | wc | | 105 | | +-----+ | 106 | | | 107 | +--------------------------------------------------------------+ 108 | session 109 | ``` 110 | 111 | >pg = process group(进程组) 112 | 113 | * bash是session的leader,sleep、cat、wc和jobs这四个进程都由bash fork而来,所以他们也属于这个session 114 | * bash也是自己所在进程组的leader 115 | * bash会为自己启动的每个进程都创建一个新的进程组,所以这里sleep和jobs进程属于自己单独的进程组 116 | * 对于用管道符号“|”连接起来的命令,bash会将它们放到一个进程组中 117 | 118 | ## nohup 119 | nohup是咋回事呢?nohup干了这么几件事: 120 | 121 | * 将stdin重定向到/dev/null,于是程序读标准输入将会返回EOF 122 | * 将stdout和stderr重定向到nohup.out或者用户通过参数指定的文件,程序所有输出到stdout和stderr的内容将会写入该文件(有时在文件中看不到输出,有可能是程序没有调用flush) 123 | * 屏蔽掉SIGHUP信号 124 | * 调用exec启动指定的命令(nohup进程将会被新进程取代,但进程ID不变) 125 | 126 | 从上面nohup干的事可以看出,通过nohup启动的程序有这些特点: 127 | 128 | * nohup程序不负责将进程放到后台,这也是为什么我们经常在nohup命令后面要加上符号“&”的原因 129 | * 由于stdin、stdout和stderr都被重定向了,nohup启动的程序不会读写tty 130 | * 由于stdin重定向到了/dev/null,程序读stdin的时候会收到EOF返回值 131 | * nohup启动的进程本质上还是属于当前session的一个进程组,所以在当前shell里面可以通过jobs看到nohup启动的程序 132 | * 当session leader退出后,该进程会收到SIGHUP信号,但由于nohup帮我们忽略了该信号,所以该进程不会退出 133 | * 由于session leader已经退出,而nohup启动的进程属于该session,于是出现了一种情况,那就是通过nohup启动的这个进程组所在的session没有leader,这是一种特殊的情况,内核会帮我们处理这种特殊情况,这里就不再深入介绍 134 | 135 | 通过nohup,我们最后达到了就算session leader(一般是shell)退出后,进程还可以照常运行的目的。 136 | 137 | ## deamon 138 | 通过nohup,就可以实现让进程在后台一直执行的功能,为什么我们还要写deamon进程呢? 139 | 140 | 从上面的nohup的介绍中可以看出来,虽然进程是在后台执行,但进程跟当前session还是有着千丝万缕的关系,至少其父进程还是被session管着的,所以我们还是需要一个跟任何session都没有关系的进程来实现deamon的功能。实现deamon进程的大概步骤如下: 141 | 142 | * 调用fork生成一个新进程,然后原来的进程退出,这样新进程就变成了孤儿进程,于是被init进程接收,这样新进程就和调用进程没有父子关系了。 143 | * 调用setsid,创建新的session,新进程将成为新session的leader,同时该新session不和任何tty关联。 144 | * 切换当前工作目录到其它地方,一般是切换到根目录,这样就取消了对原工作目录的引用,如果原工作目录是某个挂载点下面的目录,这样就不会影响该挂载点的卸载。 145 | * 关闭一些从父进程继承过来而自己不需要的fd,避免不小心读写这些fd。 146 | * 重定向stdin、stdout和stderr,避免读写它们出现错误。 147 | 148 | ## 参考 149 | 150 | * [Processes](https://www.win.tue.nl/~aeb/linux/lk/lk-10.html) 151 | * [Job Control](https://www.gnu.org/savannah-checkouts/gnu/libc/manual/html_node/Job-Control.html) 152 | * [那些永不消逝的进程](https://www.ibm.com/developerworks/cn/linux/1702_zhangym_demo/index.html) -------------------------------------------------------------------------------- /linux/021_network_tun-tap.md: -------------------------------------------------------------------------------- 1 | # 虚拟网络设备之tun/tap 2 | 3 | 在现在的云时代,到处都是虚拟机和容器,它们背后的网络管理都离不开虚拟网络设备,所以了解虚拟网络设备有利于我们更好的理解云时代的网络结构。从本篇开始,将介绍Linux下的虚拟网络设备。 4 | 5 | ## 虚拟设备和物理设备的区别 6 | 7 | 在[Linux网络数据包的接收过程](014_network_receiving_data.md)和[数据包的发送过程](015_network_sending_data.md)这两篇文章中,介绍了数据包的收发流程,知道了Linux内核中有一个网络设备管理层,处于网络设备驱动和协议栈之间,负责衔接它们之间的数据交互。驱动不需要了解协议栈的细节,协议栈也不需要了解设备驱动的细节。 8 | 9 | 对于一个网络设备来说,就像一个管道(pipe)一样,有两端,从其中任意一端收到的数据将从另一端发送出去。 10 | 11 | 比如一个物理网卡eth0,它的两端分别是内核协议栈(通过内核网络设备管理模块间接的通信)和外面的物理网络,从物理网络收到的数据,会转发给内核协议栈,而应用程序从协议栈发过来的数据将会通过物理网络发送出去。 12 | 13 | 那么对于一个虚拟网络设备呢?首先它也归内核的网络设备管理子系统管理,对于Linux内核网络设备管理模块来说,虚拟设备和物理设备没有区别,都是网络设备,都能配置IP,从网络设备来的数据,都会转发给协议栈,协议栈过来的数据,也会交由网络设备发送出去,至于是怎么发送出去的,发到哪里去,那是设备驱动的事情,跟Linux内核就没关系了,所以说虚拟网络设备的一端也是协议栈,而另一端是什么取决于虚拟网络设备的驱动实现。 14 | 15 | ## tun/tap的另一端是什么? 16 | 先看图再说话: 17 | ``` 18 | +----------------------------------------------------------------+ 19 | | | 20 | | +--------------------+ +--------------------+ | 21 | | | User Application A | | User Application B |<-----+ | 22 | | +--------------------+ +--------------------+ | | 23 | | | 1 | 5 | | 24 | |...............|......................|...................|.....| 25 | | ↓ ↓ | | 26 | | +----------+ +----------+ | | 27 | | | socket A | | socket B | | | 28 | | +----------+ +----------+ | | 29 | | | 2 | 6 | | 30 | |.................|.................|......................|.....| 31 | | ↓ ↓ | | 32 | | +------------------------+ 4 | | 33 | | | Newwork Protocol Stack | | | 34 | | +------------------------+ | | 35 | | | 7 | 3 | | 36 | |................|...................|.....................|.....| 37 | | ↓ ↓ | | 38 | | +----------------+ +----------------+ | | 39 | | | eth0 | | tun0 | | | 40 | | +----------------+ +----------------+ | | 41 | | 10.32.0.11 | | 192.168.3.11 | | 42 | | | 8 +---------------------+ | 43 | | | | 44 | +----------------|-----------------------------------------------+ 45 | ↓ 46 | Physical Network 47 | ``` 48 | 49 | 上图中有两个应用程序A和B,都在用户层,而其它的socket、协议栈(Newwork Protocol Stack)和网络设备(eth0和tun0)部分都在内核层,其实socket是协议栈的一部分,这里分开来的目的是为了看的更直观。 50 | 51 | tun0是一个Tun/Tap虚拟设备,从上图中可以看出它和物理设备eth0的差别,它们的一端虽然都连着协议栈,但另一端不一样,eth0的另一端是物理网络,这个物理网络可能就是一个交换机,而tun0的另一端是一个用户层的程序,协议栈发给tun0的数据包能被这个应用程序读取到,并且应用程序能直接向tun0写数据。 52 | 53 | 这里假设eth0配置的IP是10.32.0.11,而tun0配置的IP是192.168.3.11. 54 | 55 | > 这里列举的是一个典型的tun/tap设备的应用场景,发到192.168.3.0/24网络的数据通过程序B这个隧道,利用10.32.0.11发到远端网络的10.33.0.1,再由10.33.0.1转发给相应的设备,从而实现VPN。 56 | 57 | 下面来看看数据包的流程: 58 | 59 | 1. 应用程序A是一个普通的程序,通过socket A发送了一个数据包,假设这个数据包的目的IP地址是192.168.3.1 60 | 2. socket将这个数据包丢给协议栈 61 | 3. 协议栈根据数据包的目的IP地址,匹配本地路由规则,知道这个数据包应该由tun0出去,于是将数据包交给tun0 62 | 4. tun0收到数据包之后,发现另一端被进程B打开了,于是将数据包丢给了进程B 63 | 5. 进程B收到数据包之后,做一些跟业务相关的处理,然后构造一个新的数据包,将原来的数据包嵌入在新的数据包中,最后通过socket B将数据包转发出去,这时候新数据包的源地址变成了eth0的地址,而目的IP地址变成了一个其它的地址,比如是10.33.0.1. 64 | 6. socket B将数据包丢给协议栈 65 | 7. 协议栈根据本地路由,发现这个数据包应该要通过eth0发送出去,于是将数据包交给eth0 66 | 8. eth0通过物理网络将数据包发送出去 67 | 68 | 10.33.0.1收到数据包之后,会打开数据包,读取里面的原始数据包,并转发给本地的192.168.3.1,然后等收到192.168.3.1的应答后,再构造新的应答包,并将原始应答包封装在里面,再由原路径返回给应用程序B,应用程序B取出里面的原始应答包,最后返回给应用程序A 69 | 70 | > 这里不讨论Tun/Tap设备tun0是怎么和用户层的进程B进行通信的,对于Linux内核来说,有很多种办法来让内核空间和用户空间的进程交换数据。 71 | 72 | 从上面的流程中可以看出,数据包选择走哪个网络设备完全由路由表控制,所以如果我们想让某些网络流量走应用程序B的转发流程,就需要配置路由表让这部分数据走tun0。 73 | 74 | ## tun/tap设备有什么用? 75 | 从上面介绍过的流程可以看出来,tun/tap设备的用处是将协议栈中的部分数据包转发给用户空间的应用程序,给用户空间的程序一个处理数据包的机会。于是比较常用的数据压缩,加密等功能就可以在应用程序B里面做进去,tun/tap设备最常用的场景是VPN,包括tunnel以及应用层的IPSec等,比较有名的项目是[VTun](http://vtun.sourceforge.net),有兴趣可以去了解一下。 76 | 77 | ## tun和tap的区别 78 | 用户层程序通过tun设备只能读写IP数据包,而通过tap设备能读写链路层数据包,类似于普通socket和raw socket的差别一样,处理数据包的格式不一样。 79 | 80 | ## 示例 81 | 82 | ### 示例程序 83 | 这里写了一个程序,它收到tun设备的数据包之后,只打印出收到了多少字节的数据包,其它的什么都不做,如何编程请参考后面的参考链接。 84 | ``` 85 | #include 86 | #include 87 | #include 88 | #include 89 | #include 90 | #include 91 | #include 92 | #include 93 | #include 94 | 95 | int tun_alloc(int flags) 96 | { 97 | 98 | struct ifreq ifr; 99 | int fd, err; 100 | char *clonedev = "/dev/net/tun"; 101 | 102 | if ((fd = open(clonedev, O_RDWR)) < 0) { 103 | return fd; 104 | } 105 | 106 | memset(&ifr, 0, sizeof(ifr)); 107 | ifr.ifr_flags = flags; 108 | 109 | if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) { 110 | close(fd); 111 | return err; 112 | } 113 | 114 | printf("Open tun/tap device: %s for reading...\n", ifr.ifr_name); 115 | 116 | return fd; 117 | } 118 | 119 | int main() 120 | { 121 | 122 | int tun_fd, nread; 123 | char buffer[1500]; 124 | 125 | /* Flags: IFF_TUN - TUN device (no Ethernet headers) 126 | * IFF_TAP - TAP device 127 | * IFF_NO_PI - Do not provide packet information 128 | */ 129 | tun_fd = tun_alloc(IFF_TUN | IFF_NO_PI); 130 | 131 | if (tun_fd < 0) { 132 | perror("Allocating interface"); 133 | exit(1); 134 | } 135 | 136 | while (1) { 137 | nread = read(tun_fd, buffer, sizeof(buffer)); 138 | if (nread < 0) { 139 | perror("Reading from interface"); 140 | close(tun_fd); 141 | exit(1); 142 | } 143 | 144 | printf("Read %d bytes from tun/tap device\n", nread); 145 | } 146 | return 0; 147 | } 148 | ``` 149 | 150 | ### 演示 151 | ``` 152 | #--------------------------第一个shell窗口---------------------- 153 | #将上面的程序保存成tun.c,然后编译 154 | dev@debian:~$ gcc tun.c -o tun 155 | 156 | #启动tun程序,程序会创建一个新的tun设备, 157 | #程序会阻塞在这里,等着数据包过来 158 | dev@debian:~$ sudo ./tun 159 | Open tun/tap device tun1 for reading... 160 | Read 84 bytes from tun/tap device 161 | Read 84 bytes from tun/tap device 162 | Read 84 bytes from tun/tap device 163 | Read 84 bytes from tun/tap device 164 | 165 | #--------------------------第二个shell窗口---------------------- 166 | #启动抓包程序,抓经过tun1的包 167 | # tcpdump -i tun1 168 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 169 | listening on tun1, link-type RAW (Raw IP), capture size 262144 bytes 170 | 19:57:13.473101 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 1, length 64 171 | 19:57:14.480362 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 2, length 64 172 | 19:57:15.488246 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 3, length 64 173 | 19:57:16.496241 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 4, length 64 174 | 175 | #--------------------------第三个shell窗口---------------------- 176 | #./tun启动之后,通过ip link命令就会发现系统多了一个tun设备, 177 | #在我的测试环境中,多出来的设备名称叫tun1,在你的环境中可能叫tun0 178 | #新的设备没有ip,我们先给tun1配上IP地址 179 | dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun1 180 | 181 | #默认情况下,tun1没有起来,用下面的命令将tun1启动起来 182 | dev@debian:~$ sudo ip link set tun1 up 183 | 184 | #尝试ping一下192.168.3.0/24网段的IP, 185 | #根据默认路由,该数据包会走tun1设备, 186 | #由于我们的程序中收到数据包后,啥都没干,相当于把数据包丢弃了, 187 | #所以这里的ping根本收不到返回包, 188 | #但在前两个窗口中可以看到这里发出去的四个icmp echo请求包, 189 | #说明数据包正确的发送到了应用程序里面,只是应用程序没有处理该包 190 | dev@debian:~$ ping -c 4 192.168.3.12 191 | PING 192.168.3.12 (192.168.3.12) 56(84) bytes of data. 192 | 193 | --- 192.168.3.12 ping statistics --- 194 | 4 packets transmitted, 0 received, 100% packet loss, time 3023ms 195 | 196 | ``` 197 | 198 | ## 结束语 199 | 平时我们用到tun/tap设备的机会不多,不过由于其结构比较简单,拿它来了解一下虚拟网络设备还不错,为后续理解Linux下更复杂的虚拟网络设备(比如网桥)做个铺垫。 200 | 201 | ## 参考 202 | 203 | * [Universal TUN/TAP device driver](https://www.kernel.org/doc/Documentation/networking/tuntap.txt) 204 | * [Tun/Tap interface tutorial](http://backreference.org/2010/03/26/tuntap-interface-tutorial/) -------------------------------------------------------------------------------- /linux/022_network_veth.md: -------------------------------------------------------------------------------- 1 | # 虚拟网络设备之veth 2 | 3 | 有了上一篇关于[tun/tap的介绍](021_network_tun-tap.md)之后,大家应该对虚拟网络设备有了一定的了解,本篇将接着介绍另一种虚拟网络设备veth。 4 | 5 | ## veth设备的特点 6 | 7 | * veth和其它的网络设备都一样,一端连接的是内核协议栈。 8 | * veth设备是成对出现的,另一端两个设备彼此相连 9 | * 一个设备收到协议栈的数据发送请求后,会将数据发送到另一个设备上去。 10 | 11 | 下面这张关系图很清楚的说明了veth设备的特点: 12 | ``` 13 | +----------------------------------------------------------------+ 14 | | | 15 | | +------------------------------------------------+ | 16 | | | Newwork Protocol Stack | | 17 | | +------------------------------------------------+ | 18 | | ↑ ↑ ↑ | 19 | |..............|...............|...............|.................| 20 | | ↓ ↓ ↓ | 21 | | +----------+ +-----------+ +-----------+ | 22 | | | eth0 | | veth0 | | veth1 | | 23 | | +----------+ +-----------+ +-----------+ | 24 | |192.168.1.11 ↑ ↑ ↑ | 25 | | | +---------------+ | 26 | | | 192.168.2.11 192.168.2.1 | 27 | +--------------|-------------------------------------------------+ 28 | ↓ 29 | Physical Network 30 | ``` 31 | 上图中,我们给物理网卡eth0配置的IP为192.168.1.11, 而veth0和veth1的IP分别是192.168.2.11和192.168.2.1。 32 | 33 | ## 示例 34 | 我们通过示例的方式来一步一步的看看veth设备的特点。 35 | 36 | ### 只给一个veth设备配置IP 37 | 先通过ip link命令添加veth0和veth1,然后配置veth0的IP,并将两个设备都启动起来 38 | ```bash 39 | dev@debian:~$ sudo ip link add veth0 type veth peer name veth1 40 | dev@debian:~$ sudo ip addr add 192.168.2.11/24 dev veth0 41 | dev@debian:~$ sudo ip link set veth0 up 42 | dev@debian:~$ sudo ip link set veth1 up 43 | ``` 44 | 这里不给veth1设备配置IP的原因就是想看看在veth1没有IP的情况下,veth0收到协议栈的数据后会不会转发给veth1。 45 | 46 | ping一下192.168.2.1,由于veth1还没配置IP,所以肯定不通 47 | ```bash 48 | dev@debian:~$ ping -c 4 192.168.2.1 49 | PING 192.168.2.1 (192.168.2.1) 56(84) bytes of data. 50 | From 192.168.2.11 icmp_seq=1 Destination Host Unreachable 51 | From 192.168.2.11 icmp_seq=2 Destination Host Unreachable 52 | From 192.168.2.11 icmp_seq=3 Destination Host Unreachable 53 | From 192.168.2.11 icmp_seq=4 Destination Host Unreachable 54 | 55 | --- 192.168.2.1 ping statistics --- 56 | 4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3015ms 57 | pipe 3 58 | ``` 59 | 60 | 但为什么ping不通呢?是到哪一步失败的呢? 61 | 62 | 先看看抓包的情况,从下面的输出可以看出,veth0和veth1收到了同样的ARP请求包,但没有看到ARP应答包: 63 | ```bash 64 | dev@debian:~$ sudo tcpdump -n -i veth0 65 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 66 | listening on veth0, link-type EN10MB (Ethernet), capture size 262144 bytes 67 | 20:20:18.285230 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 68 | 20:20:19.282018 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 69 | 20:20:20.282038 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 70 | 20:20:21.300320 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 71 | 20:20:22.298783 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 72 | 20:20:23.298923 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 73 | 74 | dev@debian:~$ sudo tcpdump -n -i veth1 75 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 76 | listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes 77 | 20:20:48.570459 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 78 | 20:20:49.570012 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 79 | 20:20:50.570023 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 80 | 20:20:51.570023 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 81 | 20:20:52.569988 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 82 | 20:20:53.570833 ARP, Request who-has 192.168.2.1 tell 192.168.2.11, length 28 83 | ``` 84 | 为什么会这样呢?了解ping背后发生的事情后就明白了: 85 | 86 | 1. ping进程构造ICMP echo请求包,并通过socket发给协议栈, 87 | 2. 协议栈根据目的IP地址和系统路由表,知道去192.168.2.1的数据包应该要由192.168.2.11口出去 88 | 3. 由于是第一次访问192.168.2.1,且目的IP和本地IP在同一个网段,所以协议栈会先发送ARP出去,询问192.168.2.1的mac地址 89 | 4. 协议栈将ARP包交给veth0,让它发出去 90 | 5. 由于veth0的另一端连的是veth1,所以ARP请求包就转发给了veth1 91 | 6. veth1收到ARP包后,转交给另一端的协议栈 92 | 7. 协议栈一看自己的设备列表,发现本地没有192.168.2.1这个IP,于是就丢弃了该ARP请求包,这就是为什么只能看到ARP请求包,看不到应答包的原因 93 | 94 | ### 给两个veth设备都配置IP 95 | 96 | 给veth1也配置上IP 97 | ```bash 98 | dev@debian:~$ sudo ip addr add 192.168.2.1/24 dev veth1 99 | ``` 100 | 101 | 再ping 192.168.2.1成功(由于192.168.2.1是本地IP,所以默认会走lo设备,为了避免这种情况,这里使用ping命令带上了-I参数,指定数据包走指定设备) 102 | ```bash 103 | dev@debian:~$ ping -c 4 192.168.2.1 -I veth0 104 | PING 192.168.2.1 (192.168.2.1) from 192.168.2.11 veth0: 56(84) bytes of data. 105 | 64 bytes from 192.168.2.1: icmp_seq=1 ttl=64 time=0.032 ms 106 | 64 bytes from 192.168.2.1: icmp_seq=2 ttl=64 time=0.048 ms 107 | 64 bytes from 192.168.2.1: icmp_seq=3 ttl=64 time=0.055 ms 108 | 64 bytes from 192.168.2.1: icmp_seq=4 ttl=64 time=0.050 ms 109 | 110 | --- 192.168.2.1 ping statistics --- 111 | 4 packets transmitted, 4 received, 0% packet loss, time 3002ms 112 | rtt min/avg/max/mdev = 0.032/0.046/0.055/0.009 ms 113 | ``` 114 | 115 | > 注意:对于非debian系统,这里有可能ping不通,主要是因为内核中的一些ARP相关配置导致veth1不返回ARP应答包,如ubuntu上就会出现这种情况,解决办法如下: 116 | > root@ubuntu:~# echo 1 > /proc/sys/net/ipv4/conf/veth1/accept_local 117 | > root@ubuntu:~# echo 1 > /proc/sys/net/ipv4/conf/veth0/accept_local 118 | > root@ubuntu:~# echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter 119 | > root@ubuntu:~# echo 0 > /proc/sys/net/ipv4/conf/veth0/rp_filter 120 | > root@ubuntu:~# echo 0 > /proc/sys/net/ipv4/conf/veth1/rp_filter 121 | 122 | 再来看看抓包情况,我们在veth0和veth1上都看到了ICMP echo的请求包,但为什么没有应答包呢?上面不是显示ping进程已经成功收到了应答包吗? 123 | ```bash 124 | dev@debian:~$ sudo tcpdump -n -i veth0 125 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 126 | listening on veth0, link-type EN10MB (Ethernet), capture size 262144 bytes 127 | 20:23:43.113062 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24169, seq 1, length 64 128 | 20:23:44.112078 IP 192.168.2.11 129 | > 192.168.2.1: ICMP echo request, id 24169, seq 2, length 64 130 | 20:23:45.111091 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24169, seq 3, length 64 131 | 20:23:46.110082 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24169, seq 4, length 64 132 | 133 | 134 | dev@debian:~$ sudo tcpdump -n -i veth1 135 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 136 | listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes 137 | 20:24:12.221372 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24174, seq 1, length 64 138 | 20:24:13.222089 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24174, seq 2, length 64 139 | 20:24:14.224836 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24174, seq 3, length 64 140 | 20:24:15.223826 IP 192.168.2.11 > 192.168.2.1: ICMP echo request, id 24174, seq 4, length 64 141 | ``` 142 | 看看数据包的流程就明白了: 143 | 144 | 1. ping进程构造ICMP echo请求包,并通过socket发给协议栈, 145 | 2. 由于ping程序指定了走veth0,并且本地ARP缓存里面已经有了相关记录,所以不用再发送ARP出去,协议栈就直接将该数据包交给了veth0 146 | 3. 由于veth0的另一端连的是veth1,所以ICMP echo请求包就转发给了veth1 147 | 4. veth1收到ICMP echo请求包后,转交给另一端的协议栈 148 | 5. 协议栈一看自己的设备列表,发现本地有192.168.2.1这个IP,于是构造ICMP echo应答包,准备返回 149 | 6. 协议栈查看自己的路由表,发现回给192.168.2.11的数据包应该走lo口,于是将应答包交给lo设备 150 | 7. lo接到协议栈的应答包后,啥都没干,转手又把数据包还给了协议栈(相当于协议栈通过发送流程把数据包给lo,然后lo再将数据包交给协议栈的接收流程) 151 | 8. 协议栈收到应答包后,发现有socket需要该包,于是交给了相应的socket 152 | 9. 这个socket正好是ping进程创建的socket,于是ping进程收到了应答包 153 | 154 | 155 | 抓一下lo设备上的数据,发现应答包确实是从lo口回来的: 156 | ```bash 157 | dev@debian:~$ sudo tcpdump -n -i lo 158 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 159 | listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes 160 | 20:25:49.590273 IP 192.168.2.1 > 192.168.2.11: ICMP echo reply, id 24177, seq 1, length 64 161 | 20:25:50.590018 IP 192.168.2.1 > 192.168.2.11: ICMP echo reply, id 24177, seq 2, length 64 162 | 20:25:51.590027 IP 192.168.2.1 > 192.168.2.11: ICMP echo reply, id 24177, seq 3, length 64 163 | 20:25:52.590030 IP 192.168.2.1 > 192.168.2.11: ICMP echo reply, id 24177, seq 4, length 64 164 | ``` 165 | ### 试着ping下其它的IP 166 | ping 192.168.2.0/24网段的其它IP失败,ping一个公网的IP也失败: 167 | ```bash 168 | dev@debian:~$ ping -c 1 -I veth0 192.168.2.2 169 | PING 192.168.2.2 (192.168.2.2) from 192.168.2.11 veth0: 56(84) bytes of data. 170 | From 192.168.2.11 icmp_seq=1 Destination Host Unreachable 171 | 172 | --- 192.168.2.2 ping statistics --- 173 | 1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms 174 | 175 | dev@debian:~$ ping -c 1 -I veth0 baidu.com 176 | PING baidu.com (111.13.101.208) from 192.168.2.11 veth0: 56(84) bytes of data. 177 | From 192.168.2.11 icmp_seq=1 Destination Host Unreachable 178 | 179 | --- baidu.com ping statistics --- 180 | 1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms 181 | ``` 182 | 183 | 从抓包来看,和上面第一种veth1没有配置IP的情况是一样的,ARP请求没人处理 184 | ```bash 185 | dev@debian:~$ sudo tcpdump -i veth1 186 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 187 | listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes 188 | 02:25:23.223947 ARP, Request who-has 192.168.2.2 tell 192.168.2.11, length 28 189 | 02:25:24.224352 ARP, Request who-has 192.168.2.2 tell 192.168.2.11, length 28 190 | 02:25:25.223471 ARP, Request who-has 192.168.2.2 tell 192.168.2.11, length 28 191 | 02:25:27.946539 ARP, Request who-has 123.125.114.144 tell 192.168.2.11, length 28 192 | 02:25:28.946633 ARP, Request who-has 123.125.114.144 tell 192.168.2.11, length 28 193 | 02:25:29.948055 ARP, Request who-has 123.125.114.144 tell 192.168.2.11, length 28 194 | ``` 195 | 196 | ## 结束语 197 | 从上面的介绍中可以看出,从veth0设备出去的数据包,会转发到veth1上,如果目的地址是veth1的IP的话,就能被协议栈处理,否则连ARP那关都过不了,IP forward啥的都用不上,所以不借助其它虚拟设备的话,这样的数据包只能在本地协议栈里面打转转,没法走到eth0上去,即没法发送到外面的网络中去。 198 | 199 | 下一篇将介绍Linux下的网桥,到时候veth设备就有用武之地了。 200 | 201 | ## 参考 202 | 203 | * [Linux Switching – Interconnecting Namespaces](http://www.opencloudblog.com/?p=66) 204 | -------------------------------------------------------------------------------- /python/001_i18n.md: -------------------------------------------------------------------------------- 1 | # Python多国语言支持 2 | 3 | 本篇介绍如何在Debian 8.10下让Python 3程序支持多国语言。 4 | 5 | >其它Linux发行版应该也是差不多的操作 6 | 7 | ## 代码支持多国语言 8 | 想让程序支持多国语言,首先需要在代码上做一些修改,对需要翻译的字符串做一些记号。这里以一个简单的helloword.py为例,介绍如何修改代码。 9 | 10 | 原来的代码: 11 | 12 | ```python 13 | print("Hello World!") 14 | ``` 15 | 16 | 修改后的代码: 17 | ```python 18 | import gettext 19 | import os 20 | 21 | cwd = os.path.dirname(os.path.abspath(__file__)) #获取当前脚本所在的目录 22 | locale_dir = os.path.join(cwd, "locale") #配置locale相关文件存放的目录,路径和名字可以随意,这里使用当前目录下的"locale"子目录 23 | 24 | gettext.install("helloworld", locale_dir) #helloworld是domain名称(可以是任意名称),domain名称可以理解为一个用来标识翻译结果文件的名称,install会根据当前环境的语言,加载locale_dir目录下的helloworld.mo文件,并且安装_()函数到全局空间,这样当前进程的所有python代码都能访问_()函数 25 | 26 | print(_("Hello World!")) #将想要翻译的字符串用_()包含起来,相当于调用了一下该函数 27 | ``` 28 | 29 | 上面只有“Hello World!”需要输出给人看,所以只将它用_()包含起来,最后将上面的代码保存为helloworld.py, 30 | 31 | ## 提取要翻译的文本 32 | 下一步需要将里面的字符串提取出来以便翻译,这里会用到python自带的pygettxt命令: 33 | 34 | ```bash 35 | $ mkdir locale #这里目录的路径可以随便取,只需要跟上面代码里面的locale_dir保持一致就行 36 | 37 | #-p指定输出目录,-d指定domain名称,domain名称可以随便取,只需要和上面gettext.install的第一个参数保持一致即可 38 | $ pygettext3 -p ./locale/ -d helloworld ./helloworld.py 39 | 40 | $ ls ./locale/ #locale目录下生成了一个和domain名字一样的.pot文件 41 | helloworld.pot 42 | $ cat ./locale/helloworld.pot #文件中包含了提取出来的待翻译的文本,示例如下 43 | ...... 44 | #: ./helloworld.py:9 45 | msgid "Hello World!" 46 | msgstr "" 47 | ``` 48 | 49 | ## 翻译 50 | 51 | 上一步生成的.pot是一个模板,怎么翻译呢?很简单,拷贝一份该文件,重命名为.po,然后编辑该.po文件,填上msgstr即可,这里将其翻译成中文 52 | 53 | ```bash 54 | $ cp ./locale/helloworld.pot ./locale/helloworld.po #先拷贝,然后编辑.po文件进行翻译,这里省略编辑过程 55 | $ cat ./locale/helloworld.po #这里是修改后的内容 56 | ...... 57 | #: ./helloworld.py:9 58 | msgid "Hello World!" 59 | msgstr "你好,世界!" 60 | ``` 61 | 62 | ## 编译 63 | 翻译好了之后需要将该文件编译成对代码友好的格式,这里需要用到msgfmt这个命令: 64 | ```bash 65 | $ msgfmt -o ./locale/helloworld.mo ./locale/helloworld.po #这里-o指定的输出文件的名称必须要和上面代码中的domain名称一致 66 | ``` 67 | 68 | >如果系统中没有msgfmt命令,可以通过```sudo apt-get install gettext```安装 69 | 70 | ## 应用 71 | 有了mo文件后,需要将它放到指定位置,这样程序才能在启动的时候自动加载它。 72 | 73 | 上面代码中用```gettext.install("helloworld", locale_dir)```来加载mo文件, gettext.install会根据当前系统的语言来去相应目录找mo文件,这个位置就是locale_dir/language/LC_MESSAGES/domain.mo,我们翻译的是中文,所以language是zh,LC_MESSAGES是固定目录,保持不变,我们的domian是helloworld,所以这里示例的完整路径就是./locale/zh/LC_MESSAGES/hellworld.mo. 74 | ```bash 75 | $ mkdir -p ./locale/zh/LC_MESSAGES #创建相应的目录 76 | $ mv ./locale/helloworld.mo ./locale/zh/LC_MESSAGES/ #将mo文件放到相应目录下 77 | ``` 78 | 79 | ## 测试 80 | 一切都准备好了,只差测试了,gettext.install判断当前系统语言的方式是依次读取环境变量LANGUAGE,LC_ALL,LC_MESSAGES和LANG,找到第一个非空的值 81 | 82 | ```bash 83 | $ export LANGUAGE=en_US:en 84 | $ python3 ./helloworld.py 85 | Hello World! 86 | $ export LANGUAGE=zh_CN:zh #gettext会处理这里的冒号,相当于会依次搜索locale目录下的zh_CN和zh目录,所以上面创建目录的时候使用./locale/zh_CN/LC_MESSAGES也可以 87 | $ python3 ./helloworld.py 88 | 你好,世界! 89 | ``` 90 | 91 | 如果运行过程中出现如下错误 92 | ``` 93 | UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128) 94 | ``` 95 | 96 | 极有可能是系统不支持zh_CN,需要修改文件```/etc/locale.gen```,将```zh_CN.UTF-8 UTF-8```前面的注释去掉,然后运行```sudo locale-gen```生成zh_CN相关的文件。 97 | 98 | ## 高级功能 99 | 100 | ### 有多个源文件 101 | 在一个python项目中,存在多个python源文件,这时候应该怎么办? 首先程序的main入口文件里面应该跟上面的helloworld.py一样,调用gettext.install安装_()函数,在其他python文件中只需要直接使用_()即可,不需要再次调用gettext.install。 102 | 103 | 提取文件的时候可以使用下面的命令: 104 | ```bash 105 | $ find ./ -name "*.py"|xargs pygettext3 -p ./locale/ -d hellowrold #前面的find命令可以根据需要进行修改来过滤出想要的文件, ./locale为输出目录,hellowrold为输出的文件名称 106 | ``` 107 | 108 | ### 如何增量翻译 109 | 翻译肯定不是做过一次之后再也不做了,正常情况一般是翻译过一次之后,又对源代码进行了修改,增/删/改了一些字符串,这时候就需要将新生成的pot文件和原来翻译的结果文件进行合并: 110 | ```bash 111 | $ pygettext3 -p ./locale/ -d helloworld ./helloworld.py #重新扫描源代码,提取待翻译的字符串,生成pot模板文件 112 | 113 | #将新的pot和原来老的翻译好的po文件进行合并,生成新的hellowrold_new.po,然后编辑hellowrold_new.po进行翻译 114 | $ msgmerge -N ./locale/hellowrold.po ./locale/hellowrold.pot -o ./locale/hellowrold_new.po 115 | 116 | $ msgfmt -o ./locale/zh/LC_MESSAGES/helloworld.mo ./locale/hellowrold_new.po #将新的po编译成mo文件 117 | ``` 118 | 119 | 上面的命令只会保留新的hellowrold.pot中存在的字符串的翻译内容,也即如果源代码中删除了一个字符串,那么合并之后的hellowrold_new.po就不包含删除的那个字符串的翻译,如果后续又将这个字符串加了进来,那就需要对它重新进行翻译。针对这种情况,可以考虑使用--previous选项```msgmerge -N --previous ./locale/hellowrold.po ./locale/hellowrold.pot -o ./locale/hellowrold_new.po```,这样旧的翻译只是在生成的hellowrold_new.po中被临时的注释掉,待下次再需要时,msgmerge会将其注释去掉。 120 | 121 | ## 参考 122 | 123 | * [python library: gettext](https://docs.python.org/3/library/gettext.html) 124 | * [man msgfmt](http://www.man7.org/linux/man-pages/man1/msgfmt.1.html) -------------------------------------------------------------------------------- /raspberrypi/001_introduction.md: -------------------------------------------------------------------------------- 1 | resize partition:raspi-config --expand-rootfs http://elinux.org/RPi_raspi-config 2 | 3 | deb http://mirrors.aliyun.com/raspbian/raspbian/ jessie main contrib non-free rpi 4 | 5 | https://www.pine64.org/ 6 | 7 | 8 | # how to install raspbian on raspberry 9 | 10 | ## download 11 | https://www.raspberrypi.org/downloads/raspbian/ 12 | 13 | ## install 14 | https://www.raspberrypi.org/documentation/installation/installing-images/mac.md 15 | 16 | diskutil list 17 | diskutil unmountDisk /dev/disk2 18 | diskutil list 19 | sudo dd bs=1m if=/Users/wuyangchun/Downloads/2019-04-08-raspbian-stretch-lite.zip/2019-04-08-raspbian-stretch-lite.img of=/dev/rdisk2 conv=sync 20 | 21 | 22 | ## enbale sshd 23 | touch /Volumes/boot/ssh 24 | 25 | ## login 26 | pi/raspberry 27 | 28 | ## edit source list 29 | https://mirrors.aliyun.com/raspbian/raspbian/ 30 | 31 | ## setup wifi 32 | pi@raspberrypi:~ $ sudo cat /etc/wpa_supplicant/wpa_supplicant.conf 33 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 34 | update_config=1 35 | 36 | network={ 37 | ssid="CMCC-venus" 38 | psk="sa_1234567" 39 | } 40 | 41 | ## add user 42 | sudo adduser dev 43 | sudo adduser dev sudo 44 | sudo deluser pi 45 | 46 | ## Expand Filesystem 47 | sudo raspi-config 48 | advanced options -> Expand Filesystem 49 | 50 | ## support xfs, exfat 51 | sudo apt-get install xfsprogs exfat-utils --------------------------------------------------------------------------------