linux namespace and cgroup

namespace

參考 php

簡介

Linux Namespace是Linux提供的一種內核級別環境隔離的方法。
提供了對UTS、IPC、mount、PID、network、User等的隔離機制。html

分類

分類 系統調用參數 相關內核版本 隔離內容
Mount namespaces CLONE_NEWNS Linux 2.4.19 掛載點(文件系統)
UTS namespaces CLONE_NEWUTS Linux 2.6.19 主機名與域名,影響uname(hostname, domainname)
IPC namespaces CLONE_NEWIPC Linux 2.6.19 信號量、消息隊列和共享內存, inter-process communication,有全局id
PID namespaces CLONE_NEWPID Linux 2.6.24 進程編號
Network namespaces CLONE_NEWNET 始於Linux 2.6.24 完成於 Linux 2.6.29 網絡設備、網絡棧、端口等等
User namespaces CLONE_NEWUSER 始於 Linux 2.6.23 完成於 Linux 3.8) 用戶和用戶組

三個系統調用

調用 做用
clone() 實現線程的系統調用,用來建立一個新的進程,並能夠經過設計上述參數達到隔離。
unshare() 使某進程脫離某個namespace
setns() 把某進程加入到某個namespace

詳解

測試代碼node

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定義一個給 clone 用的棧,棧大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    printf("Container - inside the container!\n");
    /* 直接執行一個shell,以便咱們觀察這個進程空間裏的資源是否被隔離了 */
    sethostname("container",10); /* 設置hostname */

    execv(container_args[0], container_args); 
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent - start a container!\n");
    /* 調用clone函數,其中傳出一個函數,還有一個棧空間的(爲何傳尾指針,由於棧是反着的) */
    // int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL);
    // int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | SIGCHLD, NULL); /*啓用CLONE_NEWUTS Namespace隔離 */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);

    /* 等待子進程結束 */
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}複製代碼

UTS Namespace

加入 
sethostname("container",10); /* 設置hostname */

int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | SIGCHLD, NULL); /*啓用CLONE_NEWUTS Namespace隔離 */

root@container:~/testnamespace# uname -a
Linux container 4.4.0-96-generic #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
root@container:~/testnamespace# hostname
container複製代碼

IPC Namespace

// 若是隔離了 ipcs -q 看不到外面的,不然能看到

root@kube-master:~/testnamespace# ipcs -a

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x10d91bac 0          root       644        0            0
0xb92f99fd 32769      root       644        0            0
0xfcebd528 65538      root       644        0            0

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 0          root       644        80         2
0x00000000 32769      root       644        16384      2
0x00000000 65538      root       644        280        2

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x000000a7 0          root       600        1複製代碼

PID Namespace

int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); 

沒有隔離
root@container:~/testnamespace# ps -a
  PID TTY          TIME CMD
10079 pts/0    00:00:00 a.out

隔離以後
root@container:~# echo $$
1

可是 ps -a沒有變化,這是由於ps, top這些命令會去讀/proc文件系統,因此,由於/proc文件系統在父進程和子進程都是同樣的,因此這些命令顯示的東西都是同樣的複製代碼

pid 1 是一個特殊的pid須要有進程監控和資源回收的能力, docker 1.13 引入了一個 --init 參數解決這個問題
--init false Run an init inside the container that forwards signals and reaps processes
參考 blog.phusion.nl/2015/01/20/…python

➜  ke git:(alb) ✗ docker run  alpine  ps
PID   USER     TIME   COMMAND
    1 root       0:00 ps
➜  ke git:(alb) ✗ docker run --init  alpine  ps
PID   USER     TIME   COMMAND
    1 root       0:00 /dev/init -- ps
    5 root       0:00 ps複製代碼

unshare()和setns()系統調用對PID Namespace的處理不太相同,當unshare PID namespace時,調用進程會爲它的子進程分配一個新的PID Namespace,可是調用進程自己不會被移到新的Namespace中。並且調用進程第一個建立的子進程在新Namespace中的PID爲1,併成爲新Namespace中的init進程。爲何建立其餘的Namespace時unshare()和setns()會直接進入新的Namespace,而惟獨PID Namespace不是如此呢?由於調用getpid()函數獲得的PID是根據調用者所在的PID Namespace而決定返回哪一個PID,進入新的PID namespace會致使PID產生變化。而對用戶態的程序和庫函數來講,他們都認爲進程的PID是一個常量,PID的變化會引發這些進程奔潰。換句話說,一旦程序進程建立之後,那麼它的PID namespace的關係就肯定下來了,進程不會變動他們對應的PID namespace。linux

Mount Namespace

#include <stdlib.h>
system("mount -t proc proc /proc");
 /* 啓用Mount Namespace - 增長CLONE_NEWNS參數 */
int container_pid = clone(container_main, container_stack+STACK_SIZE, 
        CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);

// 這時候 ps就乾淨多了
root@vm-master:~/testnamespace# ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  20036  3868 pts/1    S    12:24   0:00 /bin/bash
root        15  0.0  0.0  36084  3228 pts/1    R+   12:24   0:00 ps -aux複製代碼

關於mount命令git

模仿Docker的Mount Namespace。
先要作一個rootfs文件夾
hchen@ubuntu:~/rootfs$ ls
bin  dev  etc  home  lib  lib64  mnt  opt  proc  root  run  sbin  sys  tmp  usr  var

// 拷貝必要的命令
hchen@ubuntu:~/rootfs$ ls ./bin ./usr/bin

./bin:
bash   chown  gzip      less  mount       netstat  rm     tabs  tee      top       tty
cat    cp     hostname  ln    mountpoint  ping     sed    tac   test     touch     umount
chgrp  echo   ip        ls    mv          ps       sh     tail  timeout  tr        uname
chmod  grep   kill      more  nc          pwd      sleep  tar   toe      truncate  which

./usr/bin:
awk  env  groups  head  id  mesg  sort  strace  tail  top  uniq  vi  wc  xargs


// 拷貝命令要用的sso
hchen@ubuntu:~/rootfs$ ls ./lib64 ./lib/x86_64-linux-gnu/

./lib64:
ld-linux-x86-64.so.2

./lib/x86_64-linux-gnu/:
libacl.so.1      libmemusage.so         libnss_files-2.19.so    libpython3.4m.so.1
libacl.so.1.1.0  libmount.so.1          libnss_files.so.2       libpython3.4m.so.1.0
libattr.so.1     libmount.so.1.1.0      libnss_hesiod-2.19.so   libresolv-2.19.so
libblkid.so.1    libm.so.6              libnss_hesiod.so.2      libresolv.so.2
libc-2.19.so     libncurses.so.5        libnss_nis-2.19.so      libselinux.so.1
libcap.a         libncurses.so.5.9      libnss_nisplus-2.19.so  libtinfo.so.5
libcap.so        libncursesw.so.5       libnss_nisplus.so.2     libtinfo.so.5.9
libcap.so.2      libncursesw.so.5.9     libnss_nis.so.2         libutil-2.19.so
libcap.so.2.24   libnsl-2.19.so         libpcre.so.3            libutil.so.1
libc.so.6        libnsl.so.1            libprocps.so.3          libuuid.so.1
libdl-2.19.so    libnss_compat-2.19.so  libpthread-2.19.so      libz.so.1
libdl.so.2       libnss_compat.so.2     libpthread.so.0
libgpm.so.2      libnss_dns-2.19.so     libpython2.7.so.1
libm-2.19.so     libnss_dns.so.2        libpython2.7.so.1.0

// 拷貝必要的配置文件
hchen@ubuntu:~/rootfs$ ls ./etc
bash.bashrc  group  hostname  hosts  ld.so.cache  nsswitch.conf  passwd  profile  
resolv.conf  shadow

// 供掛載用的配置文件
hchen@ubuntu:~$ ls ./conf
hostname     hosts     resolv.conf

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
    "/bin/bash",
    "-l",
    NULL
};

int container_main(void* arg)
{
    printf("Container [%5d] - inside the container!\n", getpid());

    //set hostname
    sethostname("container",10);

    //remount "/proc" to make sure the "top" and "ps" show container's information if (mount("proc", "rootfs/proc", "proc", 0, NULL) !=0 ) { perror("proc"); } if (mount("sysfs", "rootfs/sys", "sysfs", 0, NULL)!=0) { perror("sys"); } if (mount("none", "rootfs/tmp", "tmpfs", 0, NULL)!=0) { perror("tmp"); } if (mount("udev", "rootfs/dev", "devtmpfs", 0, NULL)!=0) { perror("dev"); } if (mount("devpts", "rootfs/dev/pts", "devpts", 0, NULL)!=0) { perror("dev/pts"); } if (mount("shm", "rootfs/dev/shm", "tmpfs", 0, NULL)!=0) { perror("dev/shm"); } if (mount("tmpfs", "rootfs/run", "tmpfs", 0, NULL)!=0) { perror("run"); } /* * 模仿Docker的從外向容器裏mount相關的配置文件 * 你能夠查看:/var/lib/docker/containers/<container_id>/目錄, * 你會看到docker的這些文件的。 */ if (mount("conf/hosts", "rootfs/etc/hosts", "none", MS_BIND, NULL)!=0 || mount("conf/hostname", "rootfs/etc/hostname", "none", MS_BIND, NULL)!=0 || mount("conf/resolv.conf", "rootfs/etc/resolv.conf", "none", MS_BIND, NULL)!=0 ) { perror("conf"); } /* 模仿docker run命令中的 -v, --volume=[] 參數乾的事 */ if (mount("/tmp/t1", "rootfs/mnt", "none", MS_BIND, NULL)!=0) { perror("mnt"); } /* chroot 隔離目錄 */ if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){ perror("chdir/chroot"); } execv(container_args[0], container_args); perror("exec"); printf("Something's wrong!\n"); return 1; } int main() { printf("Parent [%5d] - start a container!\n", getpid()); int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL); waitpid(container_pid, NULL, 0); printf("Parent - container stopped!\n"); return 0; }複製代碼

進程在建立mount namespace時,會把當前的文件結構複製給新的namespace。新namespace中的全部mount操做都隻影響自身的文件系統,而對外界不會產生任何影響。這樣作很是嚴格地實現了隔離,可是某些狀況可能並不適用。好比父節點namespace中的進程掛載了一張CD-ROM,這時子節點namespace拷貝的目錄結構就沒法自動掛載上這張CD-ROM,由於這種操做會影響到父節點的文件系統。github

2006 年引入的掛載傳播(mount propagation)解決了這個問題,掛載傳播定義了掛載對象(mount object)之間的關係,系統用這些關係決定任何掛載對象中的掛載事件如何傳播到其餘掛載對象(參考自:www.ibm.com/developerwo…docker

進程在建立Mount Namespace時,會把當前的文件結構複製給新的Namespace,新的Namespace中的全部mount操做僅影響自身的文件系統。但隨着引入掛載傳播的特性,Mount Namespace變得並非徹底意義上的資源隔離,這種傳播特性使得多Mount Namespace之間的掛載事件能夠相互影響。shell

掛載傳播定義了掛載對象之間的關係,系統利用這些關係來決定掛載對象中的掛載事件對其餘掛載對象的影響。其中掛載對象之間的關係描述以下:ubuntu

  • 共享關係(MS_SHARED):一個掛載對象的掛載事件會跨Namespace共享到其餘掛載對象。
  • 從屬關係(MS_SLAVE): 傳播的方向是單向的,即只能從Master傳播到Slave方向。
  • 私有關係(MS_PRIVATE): 不一樣Namespace的掛載事件是互不影響的(默認選項)。
  • 不可綁定關係(MS_UNBINDABLE): 一個不可綁定的私有掛載,與私有掛載相似,可是不能執行掛載操做

一個掛載狀態可能爲以下的其中一種:

  • 共享掛載(shared)
  • 從屬掛載(slave)
  • 共享/從屬掛載(shared and slave)
  • 私有掛載(private)
  • 不可綁定掛載(unbindable)

image
image

掛載的過程是經過mount系統調用完成的,它有兩個參數:一個是已存在的普通文件名,一個是能夠直接訪問的特殊文件,一個是特殊文件的名字。這個特殊文件通常用來關聯一些存儲卷,這個存儲卷能夠包含本身的目錄層級和文件系統結構。mount所達到的效果是:像訪問一個普通的文件同樣訪問位於其餘設備上文件系統的根目錄,也就是將該設備上目錄的根節點掛到了另一個文件系統的頁節點上,達到了給這個文件系統擴充容量的目的。

能夠經過/proc文件系統查看一個進程的掛載信息,具體作法以下:

cat /proc/$pid/mountinfo複製代碼

綁定掛載的引入使得mount的其中一個參數不必定要是一個特殊文件,也能夠是該文件系統上的一個普通文件目錄。Linux中綁定掛載的用法以下:

mount --bind /home/work /home/qiniu  
mount -o bind /home/work /home/qiniu複製代碼

User Namespace

要把容器中的uid和真實系統的uid給映射在一塊兒,須要修改 /proc//uid_map 和 /proc//gid_map 這兩個文件。這兩個文件的格式爲:
ID-inside-ns ID-outside-ns length

  • 第一個字段ID-inside-ns表示在容器顯示的UID或GID,
  • 第二個字段ID-outside-ns表示容器外映射的真實的UID或GID。
  • 第三個字段表示映射的範圍,通常填1,表示一一對應。

User namespace主要隔離了安全相關的標識符(identifiers)和屬性(attributes),包括用戶ID、用戶組ID、root目錄、key(指密鑰)以及特殊權限。說得通俗一點,一個普通用戶的進程經過clone()建立的新進程在新user namespace中能夠擁有不一樣的用戶和用戶組。這意味着一個進程在容器外屬於一個沒有特權的普通用戶,可是他建立的容器進程卻屬於擁有全部權限的超級用戶,這個技術爲容器提供了極大的自由。
User Namespace除了隔離用戶ID和用戶組ID以外,還對每一個Namespace進行了Capability的隔離和控制,能夠經過添加和刪除相應的Capability來控制新Namespace中進程所擁有的權限,好比爲新的Namespace中增長CAP_CHOWN權限,那麼在這個Namespace的進程擁有改變文件屬主的權限。

  • user namespace被建立後,第一個進程被賦予了該namespace中的所有權限,這樣這個init進程就能夠完成全部必要的初始化工做,而不會因權限不足而出現錯誤。
  • 咱們看到namespace內部看到的UID和GID已經與外部不一樣了,默認顯示爲65534,表示還沒有與外部namespace用戶映射。咱們須要對user namespace內部的這個初始user和其外部namespace某個用戶創建映射,這樣能夠保證當涉及到一些對外部namespace的操做時,系統能夠檢驗其權限(好比發送一個信號或操做某個文件)。一樣用戶組也要創建映射。
  • 還有一點雖然不能從輸出中看出來,可是值得注意。用戶在新namespace中有所有權限,可是他在建立他的父namespace中不含任何權限。就算調用和建立他的進程有所有權限也是如此。因此哪怕是root用戶調用了clone()在user namespace中建立出的新用戶在外部也沒有任何權限。
  • 最後,user namespace的建立實際上是一個層層嵌套的樹狀結構。最上層的根節點就是root namespace,新建立的每一個user namespace都有一個父節點user namespace以及零個或多個子節點user namespace,這一點與PID namespace很是類似。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
    "/bin/bash",
    NULL
};

int pipefd[2];

void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}

void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

int container_main(void* arg)
{

    printf("Container [%5d] - inside the container!\n", getpid());

    printf("Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());

    /* 等待父進程通知後再往下執行(進程間的同步) */
    char ch;
    close(pipefd[1]);
    read(pipefd[0], &ch, 1);

    printf("Container [%5d] - setup hostname!\n", getpid());
    //set hostname
    sethostname("container",10);

    //remount "/proc" to make sure the "top" and "ps" show container's information mount("proc", "/proc", "proc", 0, NULL); execv(container_args[0], container_args); printf("Something's wrong!\n"); return 1; } int main() { const int gid=getgid(), uid=getuid(); printf("Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n", (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid()); pipe(pipefd); printf("Parent [%5d] - start a container!\n", getpid()); int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL); printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid); //To map the uid/gid, // we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent //The file format is // ID-inside-ns ID-outside-ns length //if no mapping, // the uid will be taken from /proc/sys/kernel/overflowuid // the gid will be taken from /proc/sys/kernel/overflowgid set_uid_map(container_pid, 0, uid, 1); set_gid_map(container_pid, 0, gid, 1); printf("Parent [%5d] - user/group mapping done!\n", getpid()); /* 通知子進程 */ close(pipefd[1]); waitpid(container_pid, NULL, 0); printf("Parent - container stopped!\n"); return 0; } 上面的程序,咱們用了一個pipe來對父子進程進行同步,爲何要這樣作?由於子進程中有一個execv的系統調用,這個系統調用會把當前子進程的進程空間給所有覆蓋掉,咱們但願在execv以前就作好user namespace的uid/gid的映射,這樣,execv運行的/bin/bash就會由於咱們設置了uid爲0的inside-uid而變成#號的提示符。複製代碼

Network Namespace

在Linux下,咱們通常用ip命令建立Network Namespace

image
image

通常狀況下,物理網絡設備都分配在最初的root namespace(表示系統默認的namespace,在PID namespace中已經說起)中。可是若是你有多塊物理網卡,也能夠把其中一塊或多塊分配給新建立的network namespace。須要注意的是,當新建立的network namespace被釋放時(全部內部的進程都終止而且namespace文件沒有被掛載或打開),在這個namespace中的物理網卡會返回到root namespace而非建立該進程的父進程所在的network namespace。

在創建起veth pair以前,新舊namespace該如何通訊呢?答案是pipe(管道)。咱們以Docker Daemon在啓動容器dockerinit的過程爲例。Docker Daemon在宿主機上負責建立這個veth pair,經過netlink調用,把一端綁定到docker0網橋上,一端連進新建的network namespace進程中。創建的過程當中,Docker Daemon和dockerinit就經過pipe進行通訊,當Docker Daemon完成veth-pair的建立以前,dockerinit在管道的另外一端循環等待,直到管道另外一端傳來Docker Daemon關於veth設備的信息,並關閉管道。dockerinit才結束等待的過程,並把它的「eth0」啓動起來。整個效果相似下圖所示。

// docker 網絡本質作的事就是 1. 建立網橋  2. 建立veth 虛擬網卡,一頭在docker ns1,一頭插在網橋上 3. 設置ip,路由規則,nat,讓docker 網絡能通過bridge 出去  外部訪問容器網絡 也是在本地的 iptable 的 nat 表中添加相應的規則 https://yeasy.gitbooks.io/docker_practice/content/advanced_network/port_mapping.html
calico 也是相似實現,沒有用bridge模式

## 首先,咱們先增長一個網橋lxcbr0,模仿docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #爲網橋設置IP地址

## 接下來,咱們要建立一個network namespace - ns1

# 增長一個namesapce 命令爲 ns1 (使用ip netns add命令)
ip netns add ns1 

# 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1來操做ns1中的命令)
ip netns exec ns1   ip link set dev lo up 

## 而後,咱們須要增長一對虛擬網卡

# 增長一個pair虛擬網卡,注意其中的veth類型,其中一個網卡要按進容器中
# VETH 設備老是成對出現,送到一端請求發送的數據老是從另外一端以請求接受的形式出現。該設備不能被用戶程序直接操做,但使用起來比較簡單。建立並配置正確後,向其一端輸入數據,VETH 會改變數據的方向並將其送入內核網絡核心,完成數據的注入。在另外一端能讀到此數據。

ip link add veth-ns1 type veth peer name lxcbr0.1

# 把 veth-ns1 按到namespace ns1中,這樣容器中就會有一個新的網卡了
ip link set veth-ns1 netns ns1

# 把容器裏的 veth-ns1更名爲 eth0 (容器外會衝突,容器內就不會了)
ip netns exec ns1  ip link set dev veth-ns1 name eth0 

# 爲容器中的網卡分配一個IP地址,並激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up


# 上面咱們把veth-ns1這個網卡按到了容器中,而後咱們要把lxcbr0.1添加上網橋上
brctl addif lxcbr0 lxcbr0.1

# 爲容器增長一個路由規則,讓容器能夠訪問外面的網絡
ip netns exec ns1     ip route add default via 192.168.10.1

# 在/etc/netns下建立network namespce名稱爲ns1的目錄,
# 而後爲這個namespace設置resolv.conf,這樣,容器內就能夠訪問域名了
mkdir -p /etc/netns/ns1
echo "nameserver 8.8.8.8" > /etc/netns/ns1/resolv.conf複製代碼

CGroup

cgroups能夠限制、記錄、隔離進程組所使用的物理資源(包括:CPU、memory、IO等),爲容器實現虛擬化提供了基本保證,是構建Docker等一系列虛擬化管理工具的基石。

主要提供了以下功能:

  • Resource limitation: 限制資源使用,好比內存使用上限以及文件系統的緩存限制。
  • Prioritization: 優先級控制,好比:CPU利用和磁盤IO吞吐。
  • Accounting: 一些審計或一些統計,主要目的是爲了計費。
  • Control: 掛起進程,恢復執行進程。

對開發者來講,cgroups有以下四個有趣的特色:

  • cgroups的API以一個僞文件系統的方式實現,即用戶能夠經過文件操做實現cgroups的組織管理。
  • cgroups的組織管理操做單元能夠細粒度到線程級別,用戶態代碼也能夠針對系統分配的資源建立和銷燬cgroups,從而實現資源再分配和管理。
  • 全部資源管理的功能都以「subsystem(子系統)」的方式實現,接口統一。
  • 子進程建立之初與其父進程處於同一個cgroups的控制組。

本質上來講,cgroups是內核附加在程序上的一系列鉤子(hooks),經過程序運行時對資源的調度觸發相應的鉤子以達到資源追蹤和限制的目的。

術語

  • task(任務):cgroups的術語中,task就表示系統的一個進程。
  • cgroup(控制組):cgroups 中的資源控制都以cgroup爲單位實現。cgroup表示按某種資源控制標準劃分而成的任務組,包含一個或多個子系統。一個任務能夠加入某個cgroup,也能夠從某個cgroup遷移到另一個cgroup。
  • subsystem(子系統):cgroups中的subsystem就是一個資源調度控制器(Resource Controller)。好比CPU子系統能夠控制CPU時間分配,內存子系統能夠限制cgroup內存使用量。
  • hierarchy(層級樹):hierarchy由一系列cgroup以一個樹狀結構排列而成,每一個hierarchy經過綁定對應的subsystem進行資源調度。hierarchy中的cgroup節點能夠包含零或多個子節點,子節點繼承父節點的屬性。整個系統能夠有多個hierarchy
hchen@ubuntu:~$ mount -t cgroup #或者使用lssubsys -m命令: # lscgroup 查詢
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,relatime,blkio)
cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,net_prio)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,relatime,hugetlb)複製代碼

cgroups的使用方法簡介

查詢cgroup及子系統掛載狀態

  • 查看全部的cgroup:lscgroup
  • 查看全部支持的子系統:lssubsys -a
  • 查看全部子系統掛載的位置: lssubsys –m
  • 查看單個子系統(如memory)掛載位置:lssubsys –m memory

建立hierarchy層級並掛載子系統

// 虛擬機操做,會影響系統
mount -t tmpfs cgroups /sys/fs/cgroup
mkdir /sys/fs/cgroup/cg1
// mount -t cgroup -o subsystems name /cgroup/name
mount –t cgroup –o cpu,memory cpu_and_mem /sys/fs/cgroup/cg1複製代碼

CPU 限制

root@container:~# mkdir -p /sys/fs/cgroup/cpu/wanglei
root@container:~# cat /sys/fs/cgroup/cpu/wanglei/cpu.cfs_quota_us
-1

測試程序
int main(void)
{
    int i = 0;
    for(;;) i++;
    return 0;
}

top->
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6121 root      20   0    4224    684    612 R 100.0  0.0   0:05.89 a.out

開始限制,6121查到是測試程序的pid
root@container:~/testcgroup# echo 20000 > /sys/fs/cgroup/cpu/wanglei/cpu.cfs_quota_us
root@container:~/testcgroup# echo 6121 >> /sys/fs/cgroup/cpu/wanglei/tasks

top->
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6121 root      20   0    4224    684    612 R  20.3  0.0   2:31.16 a.out複製代碼

下面的代碼是一個線程的示例

#define _GNU_SOURCE /* See feature_test_macros(7) */

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>


const int NUM_THREADS = 5;

void *thread_main(void *threadid)
{
    /* 把本身加入cgroup中(syscall(SYS_gettid)爲獲得線程的系統tid) */
    char cmd[128];
    sprintf(cmd, "echo %ld >> /sys/fs/cgroup/cpu/haoel/tasks", syscall(SYS_gettid));
    system(cmd); 
    sprintf(cmd, "echo %ld >> /sys/fs/cgroup/cpuset/haoel/tasks", syscall(SYS_gettid));
    system(cmd);

    long tid;
    tid = (long)threadid;
    printf("Hello World! It's me, thread #%ld, pid #%ld!\n", tid, syscall(SYS_gettid));

    int a=0; 
    while(1) {
        a++;
    }
    pthread_exit(NULL);
}
int main (int argc, char *argv[])
{
    int num_threads;
    if (argc > 1){
        num_threads = atoi(argv[1]);
    }
    if (num_threads<=0 || num_threads>=100){
        num_threads = NUM_THREADS;
    }

    /* 設置CPU利用率爲50% */
    mkdir("/sys/fs/cgroup/cpu/haoel", 755);
    system("echo 50000 > /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us");

    mkdir("/sys/fs/cgroup/cpuset/haoel", 755);
    /* 限制CPU只能使用#2核和#3核 */
    system("echo \"2,3\" > /sys/fs/cgroup/cpuset/haoel/cpuset.cpus");

    pthread_t* threads = (pthread_t*) malloc (sizeof(pthread_t)*num_threads);
    int rc;
    long t;
    for(t=0; t<num_threads; t++){
        printf("In main: creating thread %ld\n", t);
        rc = pthread_create(&threads[t], NULL, thread_main, (void *)t);
        if (rc){
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }

    /* Last thing that main() should do */
    pthread_exit(NULL);
    free(threads);
}複製代碼

內存使用限制

測試一個耗盡內存的程序,限制內存,能夠看到程序會被kill

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    int size = 0;
    int chunk_size = 512;
    void *p = NULL;

    while(1) {

        if ((p = malloc(chunk_size)) == NULL) {
            printf("out of memory!!\n");
            break;
        }
        memset(p, 1, chunk_size);
        size += chunk_size;
        printf("[%d] - memory is allocated [%8d] bytes \n", getpid(), size);
        sleep(1);
    }
    return 0;
}複製代碼
root@container:~/testcgroup# mkdir /sys/fs/cgroup/memory/wanglei
root@container:~/testcgroup# echo 64k > /sys/fs/cgroup/memory/wanglei/memory.limit_in_bytes
root@container:~/testcgroup# echo [pid] > /sys/fs/cgroup/memory/haoel/tasks^C複製代碼

磁盤I/O限制

root@container:~/testcgroup# dd if=/dev/vda of=/dev/null

iotop->
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
15660 be/4 root       73.81 M/s    0.00 B/s  0.00 % 82.47 % dd if=/dev/vda of=/dev/null

root@container:~/testcgroup# mkdir /sys/fs/cgroup/blkio/wanglei
root@container:~/testcgroup# ls -l /dev/vda
brw-rw---- 1 root disk 253, 0 Sep 25 12:49 /dev/vda
root@container:~/testcgroup# echo "253:0 1048576" > /sys/fs/cgroup/blkio/wanglei/blkio.throttle.read_bps_device
root@container:~/testcgroup# echo 16221 > /sys/fs/cgroup/blkio/wanglei/tasks

iotop-> 限制得不是很準
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
16221 be/4 root      978.21 K/s    0.00 B/s  0.00 % 95.28 % dd if=/dev/vda of=/dev/null複製代碼

CGroup的子系統

  • blkio: 這個subsystem能夠爲塊設備設定輸入/輸出限制,好比物理驅動設備(包括磁盤、固態硬盤、USB等)。
  • cpu: 這個subsystem使用調度程序控制task對CPU的使用。
  • cpuacct: 這個subsystem自動生成cgroup中task對CPU資源使用狀況的報告。
  • cpuset: 這個subsystem能夠爲cgroup中的task分配獨立的CPU(此處針對多處理器系統)和內存。
  • devices 這個subsystem能夠開啓或關閉cgroup中task對設備的訪問。
  • freezer 這個subsystem能夠掛起或恢復cgroup中的task。
  • memory 這個subsystem能夠設定cgroup中task對內存使用量的限定,而且自動生成這些task對內存資源使用狀況的報告。
  • perfevent 這個subsystem使用後使得cgroup中的task能夠進行統一的性能測試。{![perf: Linux CPU性能探測器,詳見perf.wiki.kernel.org/index.php/M…
  • *net_cls 這個subsystem Docker沒有直接使用,它經過使用等級識別符(classid)標記網絡數據包,從而容許 Linux 流量控制程序(TC:Traffic Controller)識別從具體cgroup中生成的數據包。

組織結構與基本規則

你們在namespace技術的講解中已經瞭解到,傳統的Unix進程管理,其實是先啓動init進程做爲根節點,再由init節點建立子進程做爲子節點,而每一個子節點由能夠建立新的子節點,如此往復,造成一個樹狀結構。而cgroups也是相似的樹狀結構,子節點都從父節點繼承屬性。

它們最大的不一樣在於,系統中cgroup構成的hierarchy能夠容許存在多個。若是進程模型是由init做爲根節點構成的一棵樹的話,那麼cgroups的模型則是由多個hierarchy構成的森林。這樣作的目的也很好理解,若是隻有一個hierarchy,那麼全部的task都要受到綁定其上的subsystem的限制,會給那些不須要這些限制的task形成麻煩。

瞭解了cgroups的組織結構,咱們再來了解cgroup、task、subsystem以及hierarchy四者間的相互關係及其基本規則{![參照自:access.redhat.com/documentati…

規則1: 同一個hierarchy能夠附加一個或多個subsystem。以下圖1,cpu和memory的subsystem附加到了一個hierarchy。

image
image

圖1 同一個hierarchy能夠附加一個或多個subsystem

規則2: 一個subsystem能夠附加到多個hierarchy,當且僅當這些hierarchy只有這惟一一個subsystem。以下圖2,小圈中的數字表示subsystem附加的時間順序,CPU subsystem附加到hierarchy A的同時不能再附加到hierarchy B,由於hierarchy B已經附加了memory subsystem。若是hierarchy B與hierarchy A狀態相同,沒有附加過memory subsystem,那麼CPU subsystem同時附加到兩個hierarchy是能夠的。

image
image

圖2 一個已經附加在某個hierarchy上的subsystem不能附加到其餘含有別的subsystem的hierarchy上

規則3: 系統每次新建一個hierarchy時,該系統上的全部task默認構成了這個新建的hierarchy的初始化cgroup,這個cgroup也稱爲root cgroup。對於你建立的每一個hierarchy,task只能存在於其中一個cgroup中,即一個task不能存在於同一個hierarchy的不一樣cgroup中,可是一個task能夠存在在不一樣hierarchy中的多個cgroup中。若是操做時把一個task添加到同一個hierarchy中的另外一個cgroup中,則會從第一個cgroup中移除。在下圖3中能夠看到,httpd進程已經加入到hierarchy A中的/cg1而不能加入同一個hierarchy中的/cg2,可是能夠加入hierarchy B中的/cg3。實際上不容許加入同一個hierarchy中的其餘cgroup野生爲了防止出現矛盾,如CPU subsystem爲/cg1分配了30%,而爲/cg2分配了50%,此時若是httpd在這兩個cgroup中,就會出現矛盾。

image
image

圖3 一個task不能屬於同一個hierarchy的不一樣cgroup

規則4: 進程(task)在fork自身時建立的子任務(child task)默認與原task在同一個cgroup中,可是child task容許被移動到不一樣的cgroup中。即fork完成後,父子進程間是徹底獨立的。以下圖4中,小圈中的數字表示task 出現的時間順序,當httpd剛fork出另外一個httpd時,在同一個hierarchy中的同一個cgroup中。可是隨後若是PID爲4840的httpd須要移動到其餘cgroup也是能夠的,由於父子任務間已經獨立。總結起來就是:初始化時子任務與父任務在同一個cgroup,可是這種關係隨後能夠改變。

image
image

圖4 剛fork出的子進程在初始狀態與其父進程處於同一個cgroup

補充

systemd

kuberlet有個systemd文檔這麼說:
This document describes how the node should be configured, and a set of enhancements that should be made to the kubelet to better integrate with these distributions independent of container runtime.

The Kernel direction for cgroup management is to promote a single-writer model rather than allowing multiple processes to independently write to parts of the file-system.In distributions that run systemd as their init system, the cgroup tree is managed by systemd by default since it implicitly interacts with the cgroup tree when starting units. Manual changes made by other cgroup managers to the cgroup tree are not guaranteed to be preserved unless systemd is made aware. systemd can be told to ignore sections of the cgroup tree by configuring the unit to have the Delegate= option.

是說再linux上就推薦用systemd來管理cgroup?並且這樣還能不依賴docker?

sysctl

除了cgroup作資源限制,對於系統級別的資源限制相關的還有一個sysctl命令
sysctl命令被用於在內核運行時動態地修改內核的運行參數,可用的內核參數在目錄/proc/sys中。它包含一些TCP/ip堆棧和虛擬內存系統的高級選項, 這可讓有經驗的管理員提升引人注目的系統性能。用sysctl能夠讀取設置超過五百個系統變量。
Parameters are available via /proc/sys/ virtual process file system. The parameters cover various subsystems such as:

  • kernel (common prefix: kernel.)
  • networking (common prefix: net.)
  • virtual memory (common prefix: vm.)
  • MDADM (common prefix: dev.)

docker privileged能夠設置,可是有些參數是系統級別的,沒有隔離,改了會影響別的容器。後來版本docker作了限制,只能改一些whitelisted sysctls。
Only namespaced kernel parameters can be modified
k8s裏面的設置github.com/kubernetes/…

相關文章
相關標籤/搜索