作業系統級虛擬化
KVM、XEN等虛擬化技術允許各個虛擬機器擁有自己獨立的作業系統。與KVM、XEN等虛擬化技術不同,所謂作業系統級虛擬化,也被稱作容器化,是作業系統自身的一個特性,它允許多個相互隔離的使用者空間例項的存在。這些使用者空間例項也被稱作為容器。普通的程式可以看到計算機的所有資源而容器中的程式只能看到分配給該容器的資源。通俗來講,作業系統級虛擬化將作業系統所管理的計算機資源,包括程式、檔案、裝置、網路等分組,然後交給不同的容器使用。容器中執行的程式只能看到分配給該容器的資源。從而達到隔離與虛擬化的目的。
實現作業系統虛擬化需要用到Namespace及cgroups技術。
名稱空間(Namespace)
在程式語言中,引入名稱空間的概念是為了重用變數名或者服務例程名。在不同的名稱空間中使用同一個變數名而不會產生衝突。Linux系統引入名稱空間也有類似的作用。例如,在沒有作業系統級虛擬化的Linux系統中,使用者態程式從1開始編號(PID)。引入作業系統虛擬化之後,不同容器有著不同的PID名稱空間,每個容器中的程式都可以從1開始編號而不產生衝突。
目前,Linux中的名稱空間有6種型別,分別對應作業系統管理的6種資源:
- 掛載點(mount point) CLONE_NEWNS
- 程式(pid) CLONE_NEWPID
- 網路(net) CLONE_NEWNET
- 程式間通訊(ipc) CLONE_NEWIPC
- 主機名(uts) CLONE_NEWUTS
- 使用者(uid) CLONW_NEWUSER
將來還會引入時間、裝置等對應的namespace.
Linux 2.4.19版本引入了第一個名稱空間——掛載點,因為那時還沒有其他型別的名稱空間,所以clone系統呼叫中引入的flag就叫做CLONE_NEWNS
與名稱空間相關的三個系統呼叫(system calls)
下面3個系統呼叫用來操作名稱空間:
- clone() —— 用來建立新的程式及新的名稱空間,新的程式會被放到新的名稱空間中
- unshare() —— 建立新的名稱空間但並不建立新的子程式,之後建立的子程式會被放到新建立的名稱空間中去
- setns() —— 將程式加入到已經存在的名稱空間中
注意:這3個系統呼叫都不會改變呼叫程式(calling process)的pid名稱空間,而是會影響其子程式的pid名稱空間
名稱空間本身並沒用名字(囧),不同的名稱空間用不同的inode號來標識,這也符合Linux用檔案一統天下的慣例。可以在proc檔案系統中檢視一個程式所屬的名稱空間,例如,檢視PID為4123的程式所屬的名稱空間:
1 2 3 4 5 6 7 8 9 |
kelvin@desktop:~$ ls -l /proc/4123/ns/ 總用量 0 lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 ipc -> ipc:[4026531839] lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 mnt -> mnt:[4026531840] lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 net -> net:[4026531963] lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 pid -> pid:[4026531836] lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 user -> user:[4026531837] lrwxrwxrwx 1 kelvin kelvin 0 12月 26 16:28 uts -> uts:[4026531838] |
下面的程式碼演示瞭如何利用上述3個系統呼叫來操作程式的名稱空間:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#define _GNU_SOURCE #include <sys/types.h> #include <sys/wait.h> #include <sched.h> #include <signal.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define STACK_SIZE (10 * 1024 * 1024) char child_stack[STACK_SIZE]; int child_main(void* args) { pid_t child_pid = getpid(); printf("I'm child process and my pid is %d \n", child_pid); // 子程式會被放到clone系統呼叫新建立的pid名稱空間中, 所以其pid應該為1 sleep(300); // 名稱空間中的所有程式退出後該名稱空間的inode將會被刪除, 為後續操作保留它 return 0; } int main() { /* Clone */ pid_t child_pid = clone(child_main, child_stack + STACK_SIZE, \ CLONE_NEWPID | SIGCHLD, NULL); if(child_pid < 0) { perror("clone failed"); } /* Unshare */ int ret = unshare(CLONE_NEWPID); // 父程式呼叫unshare, 建立了一個新的名稱空間, //但不會建立子程式. 之後再建立的子程式將會被加入到新的名稱空間中 if (ret < 0) { perror("unshare failed"); } int fpid = fork(); if (fpid < 0) { perror("fork error"); } else if (fpid == 0) { printf("I am child process. My pid is %d \n", getpid()); // Fork後的子程式會被加入到unshare建立的名稱空間中, 所以pid應該為1 exit(0); } else { } waitpid(fpid, NULL, 0); /* Setns */ char path[80] = ""; sprintf(path, "/proc/%d/ns/pid", child_pid); int fd = open(path, O_RDONLY); if (fd == -1) perror("open error"); if (setns(fd, 0) == -1) // setns並不會改變當前程式的名稱空間, 而是會設定之後建立的子程式的名稱空間 perror("setns error"); close(fd); int npid = fork(); if (npid < 0) { perror("fork error"); } else if (npid == 0) { printf("I am child process. My pid is %d \n", getpid()); // 新的子程式會被加入到第一個子程式的pid名稱空間中, 所以其pid應該為2 exit(0); } else { } return 0; } |
執行結果:
1 2 3 4 |
$ sudo ./ns I'm child process and my pid is 1 I am child process. My pid is 1 I am child process. My pid is 2 |
控制組(Cgroups)
如果說名稱空間是從命名和編號的角度進行隔離,而控制組則是將程式進行分組,並真正的將各組程式的計算資源進行限制、隔離。控制組是一種核心機制,它可以對程式進行分組、跟蹤限制其使用的計算資源。對於每一類計算資源,控制組通過所謂的子系統(subsystem)來進行控制,現階段已有的子系統包括:
- cpusets: 用來分配一組CPU給指定的cgroup,該cgroup中的程式只等被排程到該組CPU上去執行
- blkio : 限制cgroup的塊IO
- cpuacct : 用來統計cgroup中的CPU使用
- devices : 用來黑白名單的方式控制cgroup可以建立和使用的裝置節點
- freezer : 用來掛起指定的cgroup,或者喚醒掛起的cgroup
- hugetlb : 用來限制cgroup中hugetlb的使用
- memory : 用來跟蹤限制記憶體及交換分割槽的使用
- net_cls : 用來根據傳送端的cgroup來標記資料包,流量控制器(traffic controller)會根據這些標記來分配優先順序
- net_prio : 用來設定cgroup的網路通訊優先順序
- cpu :用來設定cgroup中CPU的排程引數
- perf_event : 用來監控cgroup的CPU效能
與名稱空間不同,控制組並沒有增加系統呼叫,而是實現了一個檔案系統,通過檔案及目錄操作來管理控制組。下面通過一個例子來看一看cgroup是如何利用cpuset子系統來把程式繫結到指定的CPU上去執行的。
1. 建立一個一直執行的shell指令碼
1 2 3 4 5 6 7 |
#!/bin/bash x=0 while [ True ];do : done; |
2. 在後臺執行這個指令碼
1 2 |
# bash run.sh & [1] 20553 |
3. 檢視該指令碼在哪個CPU上執行
1 2 |
# ps -eLo ruser,lwp,psr,args | grep 20553 | grep -v grep root 20553 3 bash run.sh |
可以看到PID為20553的程式執行在編號為3的CPU上,下面利用cgroups將其繫結到編號為2的CPU上去執行
4. 掛載cgroups型別的檔案系統到一個新建立的目錄cgroups中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# mkdir cgroups # mount -t cgroup -o cpuset cgroups ./cgroups/ # ls cgroups/ cgroup.clone_children cpuset.memory_pressure_enabled cgroup.procs cpuset.memory_spread_page cgroup.sane_behavior cpuset.memory_spread_slab cpuset.cpu_exclusive cpuset.mems cpuset.cpus cpuset.sched_load_balance cpuset.effective_cpus cpuset.sched_relax_domain_level cpuset.effective_mems docker cpuset.mem_exclusive tasks cpuset.mem_hardwall notify_on_release cpuset.memory_migrate release_agent cpuset.memory_pressure |
5. 建立一個新的組group0
1 2 3 4 5 6 7 8 |
# mkdir group0 # ls group0/ cgroup.clone_children cpuset.mem_exclusive cpuset.mems cgroup.procs cpuset.mem_hardwall cpuset.sched_load_balance cpuset.cpu_exclusive cpuset.memory_migrate cpuset.sched_relax_domain_level cpuset.cpus cpuset.memory_pressure notify_on_release cpuset.effective_cpus cpuset.memory_spread_page tasks cpuset.effective_mems cpuset.memory_spread_slab |
6. 將上面的程式20553加入到新建的控制組中:
1 2 3 |
# echo 20553 >> group0/tasks # cat group0/tasks 20553 |
7. 限制該組的程式只能執行在編號為2的CPU上
1 2 3 |
# echo 2 > group0/cpuset.cpus # cat group0/cpuset.cpus 2 |
8. 檢視PID為20553的程式所執行的CPU編號
1 2 |
# ps -eLo ruser,lwp,psr,args | grep 20553 | grep -v grep root 20553 2 bash run.sh |
上面的例子簡單的展示瞭如何使用控制組。控制組通過檔案和目錄來操作,檔案系統又是樹形結構,因此如果不對cgroups的使用做一些限制的話,配置會變得異常複雜和混亂。因此,在新版的cgroups中做了一些限制。
小結
本文簡要介紹了作業系統虛擬化的概念,以及實現作業系統虛擬化的技術——名稱空間及控制組。並通過兩個簡單的例子演示了名稱空間及控制組的使用方法。