简要介绍了操作系统虚拟化的概念,以及实现操作系统虚拟化的技术

描述

操作系统级虚拟化

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的进程所属的命名空间:

kelvin@desktop:~$ls -l /proc/4123/ns/

总用量0

lrwxrwxrwx1kelvin kelvin012月2616:28cgroup -> cgroup:[4026531835]

lrwxrwxrwx1kelvin kelvin012月2616:28ipc -> ipc:[4026531839]

lrwxrwxrwx1kelvin kelvin012月2616:28mnt -> mnt:[4026531840]

lrwxrwxrwx1kelvin kelvin012月2616:28net -> net:[4026531963]

lrwxrwxrwx1kelvin kelvin012月2616:28pid -> pid:[4026531836]

lrwxrwxrwx1kelvin kelvin012月2616:28user -> user:[4026531837]

lrwxrwxrwx1kelvin kelvin012月2616:28uts -> uts:[4026531838]

下面的代码演示了如何利用上述3个系统调用来操作进程的命名空间:

#define _GNU_SOURCE

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define STACK_SIZE (10 * 1024 * 1024)

charchild_stack[STACK_SIZE];

intchild_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将会被删除, 为后续操作保留它

return0;

}

intmain(){

/* Clone */

pid_t child_pid = clone(child_main,child_stack + STACK_SIZE,\

CLONE_NEWPID | SIGCHLD,NULL);

if(child_pid < 0){

perror("clone failed");

}

/* Unshare */

intret = unshare(CLONE_NEWPID);// 父进程调用unshare, 创建了一个新的命名空间,

//但不会创建子进程. 之后再创建的子进程将会被加入到新的命名空间中

if(ret < 0){

perror("unshare failed");

}

intfpid = fork();

if(fpid < 0){

perror("fork error");

}elseif(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 */

charpath[80] = "";

sprintf(path,"/proc/%d/ns/pid",child_pid);

intfd = open(path,O_RDONLY);

if(fd == -1)

perror("open error");

if(setns(fd,0) == -1)

// setns并不会改变当前进程的命名空间, 而是会设置之后创建的子进程的命名空间

perror("setns error");

close(fd);

intnpid = fork();

if(npid < 0){

perror("fork error");

}elseif(npid == 0){

printf("I am child process. My pid is %d  \n",getpid());

// 新的子进程会被加入到第一个子进程的pid命名空间中, 所以其pid应该为2

exit(0);

}else{

}

return0;

}

运行结果:

$sudo./ns

I'mchildprocess andmy pid is1

Iam childprocess.My pid is1  

Iam childprocess.My pid is2

控制组(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脚本

#!/bin/bash

x=0

while[True];do

done;

2. 在后台执行这个脚本

# bash run.sh &

[1]20553

3. 查看该脚本在哪个CPU上运行

# ps -eLo ruser,lwp,psr,args | grep 20553 | grep -v grep

root     20553   3bash run.sh

可以看到PID为20553的进程运行在编号为3的CPU上,下面利用cgroups将其绑定到编号为2的CPU上去执行

4. 挂载cgroups类型的文件系统到一个新创建的目录cgroups中

# 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

# 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加入到新建的控制组中:

# echo 20553 >> group0/tasks

# cat group0/tasks

20553

7. 限制该组的进程只能运行在编号为2的CPU上

# echo 2 > group0/cpuset.cpus

# cat group0/cpuset.cpus

2

8. 查看PID为20553的进程所运行的CPU编号

# ps -eLo ruser,lwp,psr,args | grep 20553 | grep -v grep

root     20553   2bash run.sh

上面的例子简单的展示了如何使用控制组。控制组通过文件和目录来操作,文件系统又是树形结构,因此如果不对cgroups的使用做一些限制的话,配置会变得异常复杂和混乱。因此,在新版的cgroups中做了一些限制。

小结

本文简要介绍了操作系统虚拟化的概念,以及实现操作系统虚拟化的技术——命名空间及控制组。并通过两个简单的例子演示了命名空间及控制组的使用方法。
控制组

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分