参考 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;
}复制代码
加入
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复制代码
// 若是隔离了 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复制代码
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
#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
一个挂载状态可能为以下的其中一种:
挂载的过程是经过mount系统调用完成的,它有两个参数:一个是已存在的普通文件名,一个是能够直接访问的特殊文件,一个是特殊文件的名字。这个特殊文件通常用来关联一些存储卷,这个存储卷能够包含本身的目录层级和文件系统结构。mount所达到的效果是:像访问一个普通的文件同样访问位于其余设备上文件系统的根目录,也就是将该设备上目录的根节点挂到了另一个文件系统的页节点上,达到了给这个文件系统扩充容量的目的。
能够经过/proc文件系统查看一个进程的挂载信息,具体作法以下:
cat /proc/$pid/mountinfo复制代码
绑定挂载
的引入使得mount的其中一个参数不必定要是一个特殊文件,也能够是该文件系统上的一个普通文件目录。Linux中绑定挂载的用法以下:
mount --bind /home/work /home/qiniu
mount -o bind /home/work /home/qiniu复制代码
要把容器中的uid和真实系统的uid给映射在一块儿,须要修改 /proc//uid_map 和 /proc//gid_map 这两个文件。这两个文件的格式为:ID-inside-ns ID-outside-ns length
User namespace主要隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户ID、用户组ID、root目录、key(指密钥)以及特殊权限。说得通俗一点,一个普通用户的进程经过clone()建立的新进程在新user namespace中能够拥有不一样的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,可是他建立的容器进程却属于拥有全部权限的超级用户,这个技术为容器提供了极大的自由。
User Namespace除了隔离用户ID和用户组ID以外,还对每一个Namespace进行了Capability的隔离和控制,能够经过添加和删除相应的Capability来控制新Namespace中进程所拥有的权限,好比为新的Namespace中增长CAP_CHOWN权限,那么在这个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而变成#号的提示符。复制代码
在Linux下,咱们通常用ip命令建立Network Namespace
通常状况下,物理网络设备都分配在最初的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复制代码
cgroups能够限制、记录、隔离进程组所使用的物理资源(包括:CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。
主要提供了以下功能:
对开发者来讲,cgroups有以下四个有趣的特色:
本质上来讲,cgroups是内核附加在程序上的一系列钩子(hooks),经过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。
术语
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)复制代码
// 虚拟机操做,会影响系统
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复制代码
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复制代码
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复制代码
你们在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。
图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是能够的。
规则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中,就会出现矛盾。
规则4: 进程(task)在fork自身时建立的子任务(child task)默认与原task在同一个cgroup中,可是child task容许被移动到不一样的cgroup中。即fork完成后,父子进程间是彻底独立的。以下图4中,小圈中的数字表示task 出现的时间顺序,当httpd刚fork出另外一个httpd时,在同一个hierarchy中的同一个cgroup中。可是随后若是PID为4840的httpd须要移动到其余cgroup也是能够的,由于父子任务间已经独立。总结起来就是:初始化时子任务与父任务在同一个cgroup,可是这种关系随后能够改变。
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?
除了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:
docker privileged能够设置,可是有些参数是系统级别的,没有隔离,改了会影响别的容器。后来版本docker作了限制,只能改一些whitelisted sysctls。
Only namespaced kernel parameters can be modified
k8s里面的设置github.com/kubernetes/…