kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

CentOS 6 使用 docker

docker bridge 设置

1
2
docker network create --subnet=192.168.3.0/24 --gateway=192.168.3.1 nett
docker run -i -t --net nett --ip 192.168.3.2 49f7960eb7e4 /bin/bash

http://www.linuxidc.com/Linux/2014-01/95513.htm

一、禁用selinux

由于Selinux和LXC有冲突,所以需要禁用selinux。编辑/etc/selinux/config,设置两个关键变量。

1
2
SELINUX=disabled
SELINUXTYPE=targeted

二、配置Fedora EPEL源

1
sudo yum install http://ftp.riken.jp/Linux/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm

三、添加hop5.repo源

1
2
cd /etc/yum.repos.d
sudo wget http://www.hop5.in/yum/el6/hop5.repo

四、安装Docker

1
sudo yum install docker-io

http://www.server110.com/docker/201411/11105.html

启动docker服务

1
2
3
[root@localhost /]# service docker start
Starting cgconfig service:                                 [  OK  ]
Starting docker:                                           [  OK  ]

基本信息查看

docker version:查看docker的版本号,包括客户端、服务端、依赖的Go等

1
2
3
4
5
6
7
8
9
[root@localhost /]# docker version
Client version: 1.0.0
Client API version: 1.12
Go version (client): go1.2.2
Git commit (client): 63fe64c/1.0.0
Server version: 1.0.0
Server API version: 1.12
Go version (server): go1.2.2
Git commit (server): 63fe64c/1.0.0

docker info :查看系统(docker)层面信息,包括管理的images, containers数等

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost /]# docker info
Containers: 16
Images: 40
Storage Driver: devicemapper
 Pool Name: docker-253:0-1183580-pool
 Data file: /var/lib/docker/devicemapper/devicemapper/data
 Metadata file: /var/lib/docker/devicemapper/devicemapper/metadata
 Data Space Used: 2180.4 Mb
 Data Space Total: 102400.0 Mb
 Metadata Space Used: 3.4 Mb
 Metadata Space Total: 2048.0 Mb
Execution Driver: lxc-0.9.0
Kernel Version: 2.6.32-431.el6.x86_64

5 镜像的获取与容器的使用

镜像可以看作是包含有某些软件的容器系统,比如ubuntu就是一个官方的基础镜像,很多镜像都是基于这个镜像“衍生”,该镜像包含基本的ubuntu系统。再比如,hipache是一个官方的镜像容器,运行后可以支持http和websocket的代理服务,而这个镜像本身又基于ubuntu。

搜索镜像

1
docker search <image>:在docker index中搜索image
1
2
3
4
5
6
7
[root@localhost /]# docker search ubuntu12.10
NAME                        DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
mirolin/ubuntu12.10                                                         0
marcgibbons/ubuntu12.10                                                     0
mirolin/ubuntu12.10_redis                                                   0
chug/ubuntu12.10x32         Ubuntu Quantal Quetzal 12.10 32bit  base i...   0
chug/ubuntu12.10x64         Ubuntu Quantal Quetzal 12.10 64bit  base i...   0

下载镜像

1
docker pull <image> :从docker registry server 中下拉image
1
[root@localhost /]# docker pull chug/ubuntu12.10x64

查看镜像

1
2
3
4
docker images: 列出images
docker images -a :列出所有的images(包含历史)
docker images --tree :显示镜像的所有层(layer)
docker rmi  <image ID>: 删除一个或多个image

使用镜像创建容器

1
2
[root@localhost /]# docker run chug/ubuntu12.10x64  /bin/echo hello world
hello world

交互式运行

1
2
[root@localhost /]# docker run -i -t chug/ubuntu12.10x64  /bin/bash
root@2161509ff65e:/#

运行Container

1
2
3
$ docker run --name shell -i -t chug/ubuntu12.10x64 /bin/bash 

$ docker run -t -i efd1e7457182 /bin/bash 

两个参数,-t表示给容器tty终端,-i表示可以interactive,可以交互。

查看容器

1
2
3
4
docker ps :列出当前所有正在运行的container
docker ps -l :列出最近一次启动的container
docker ps -a :列出所有的container(包含历史,即运行过的container)
docker ps -q :列出最近一次运行的container ID

再次启动容器

1
2
3
4
5
6
7
8
docker start/stop/restart <container> :开启/停止/重启container
docker start [container_id] :再次运行某个container (包括历史container)
docker attach [container_id] :连接一个正在运行的container实例(即实例必须为start状态,可以多个窗口同时attach 一个container实例)
docker start -i <container> :启动一个container并进入交互模式(相当于先start,在attach)

docker run -i -t <image> /bin/bash :使用image创建container并进入交互模式, login shell是/bin/bash
docker run -i -t -p <host_port:contain_port> :映射 HOST 端口到容器,方便外部访问容器内服务,host_port 可以省略,省略表示把 container_port 映射到一个动态端口。
注:使用start是启动已经创建过得container,使用run则通过image开启一个新的container。

删除容器

1
2
3
docker rm <container...> :删除一个或多个container
docker rm `docker ps -a -q` :删除所有的container
docker ps -a -q | xargs docker rm :同上, 删除所有的container

6 持久化容器与镜像

6.1 通过容器生成新的镜像

运行中的镜像称为容器。你可以修改容器(比如删除一个文件),但这些修改不会影响到镜像。不过,你使用docker commit 命令可以把一个正在运行的容器变成一个新的镜像。

1
docker commit <container> [repo:tag] 将一个container固化为一个新的image,后面的repo:tag可选。
1
2
3
4
5
6
7
8
9
[root@localhost /]# docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
chug/ubuntu12.10x64   latest              0b96c14dafcd        4 months ago        270.3 MB
[root@localhost /]# docker commit d0fd23b8d3ac chug/ubuntu12.10x64_2
daa11948e23d970c18ad89c9e5d8972157fb6f0733f4742db04219b9bb6d063b
[root@localhost /]# docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
chug/ubuntu12.10x64_2   latest              daa11948e23d        6 seconds ago       270.3 MB
chug/ubuntu12.10x64     latest              0b96c14dafcd        4 months ago        270.3 MB
6.2 持久化容器

export命令用于持久化容器

1
docker export <CONTAINER ID> > /tmp/export.tar
6.3 持久化镜像

Save命令用于持久化镜像

1
docker save 镜像ID > /tmp/save.tar
6.4 导入持久化container

删除container 2161509ff65e

导入export.tar文件

1
2
3
4
5
6
7
[root@localhost /]# cat /tmp/export.tar | docker import - export:latest
af19a55ff0745fb0a68655392d6d7653c29460d22d916814208bbb9626183aaa
[root@localhost /]# docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
export                  latest              af19a55ff074        34 seconds ago      270.3 MB
chug/ubuntu12.10x64_2   latest              daa11948e23d        20 minutes ago      270.3 MB
chug/ubuntu12.10x64     latest              0b96c14dafcd        4 months ago        270.3 MB
6.5 导入持久化image

删除image daa11948e23d

导入save.tar文件

1
[root@localhost /]# docker load < /tmp/save.tar

对image打tag

1
[root@localhost /]# docker tag daa11948e23d load:tag
6.6 export-import与save-load的区别

导出后再导入(export-import)的镜像会丢失所有的历史,而保存后再加载(save-load)的镜像没有丢失历史和层(layer)。这意味着使用导出后再导入的方式,你将无法回滚到之前的层(layer),同时,使用保存后再加载的方式持久化整个镜像,就可以做到层回滚。(可以执行docker tag 来回滚之前的层)。

6.7 一些其它命令
1
2
3
4
5
6
7
docker logs $CONTAINER_ID #查看docker实例运行日志,确保正常运行
docker inspect $CONTAINER_ID #docker inspect <image|container> 查看image或container的底层信息

docker build <path> 寻找path路径下名为的Dockerfile的配置文件,使用此配置生成新的image
docker build -t repo[:tag] 同上,可以指定repo和可选的tag
docker build - < <dockerfile> 使用指定的dockerfile配置文件,docker以stdin方式获取内容,使用此配置生成新的image
docker port <container> <container port> 查看本地哪个端口映射到container的指定端口,其实用docker ps 也可以看到

7 一些使用技巧

7.1 docker文件存放目录

Docker实际上把所有东西都放到/var/lib/docker路径下了。

1
2
[root@localhost docker]# ls -F
containers/  devicemapper/  execdriver/  graph/  init/  linkgraph.db  repositories-devicemapper  volumes/

containers目录当然就是存放容器(container)了,graph目录存放镜像,文件层(file system layer)存放在graph/imageid/layer路径下,这样我们就可以看看文件层里到底有哪些东西,利用这种层级结构可以清楚的看到文件层是如何一层一层叠加起来的。

7.2 查看root密码

docker容器启动时的root用户的密码是随机分配的。所以,通过这种方式就可以得到容器的root用户的密码了。

1
docker logs 5817938c3f6e 2>&1 | grep 'User: ' | tail -n1

http://www.tuicool.com/articles/7V7vYn

Docker常用命令

1. 查看docker信息(version、info)

1
2
3
4
5
# 查看docker版本
$docker version

# 显示docker系统的信息
$docker info

2. 对image的操作(search、pull、images、rmi、history)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 检索image
$docker search image_name

# 下载image
$docker pull image_name

# 列出镜像列表; -a, --all=false Show all images; --no-trunc=false Don't truncate output; -q, --quiet=false Only show numeric IDs
$docker images

# 删除一个或者多个镜像; -f, --force=false Force; --no-prune=false Do not delete untagged parents
$docker rmi image_name

# 显示一个镜像的历史; --no-trunc=false Don't truncate output; -q, --quiet=false Only show numeric IDs
$docker history image_name

3. 启动容器(run)

docker容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源,包括文件系统、系统类库、shell 环境等等。但这个沙盒默认是不会运行任何程序的。你需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程,所以当该进程结束的时候,容器也会完全的停止。

1
2
3
4
5
6
7
8
9
# 在容器中运行"echo"命令,输出"hello word"
$docker run image_name echo "hello word"

# 交互式进入容器中
$docker run -i -t image_name /bin/bash


# 在容器中安装新的程序
$docker run image_name apt-get install -y app_name

Note: 在执行apt-get 命令的时候,要带上-y参数。如果不指定-y参数的话,apt-get命令会进入交互模式,需要用户输入命令来进行确认,但在docker环境中是无法响应这种交互的。apt-get 命令执行完毕之后,容器就会停止,但对容器的改动不会丢失。

4. 查看容器(ps)

1
2
3
4
5
6
# 列出当前所有正在运行的container
$docker ps
# 列出所有的container
$docker ps -a
# 列出最近一次启动的container
$docker ps -l

5. 保存对容器的修改(commit)

当你对某一个容器做了修改之后(通过在容器中运行某一个命令),可以把对容器的修改保存下来,这样下次可以从保存后的最新状态运行该容器。

1
2
# 保存对容器的修改; -a, --author="" Author; -m, --message="" Commit message
$docker commit ID new_image_name

Note: image相当于类,container相当于实例,不过可以动态给实例安装新软件,然后把这个container用commit命令固化成一个image。

6. 对容器的操作(rm、stop、start、kill、logs、diff、top、cp、restart、attach)

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
# 删除所有容器
$docker rm `docker ps -a -q`

# 删除单个容器; -f, --force=false; -l, --link=false Remove the specified link and not the underlying container; -v, --volumes=false Remove the volumes associated to the container
$docker rm Name/ID

# 停止、启动、杀死一个容器
$docker stop Name/ID
$docker start Name/ID
$docker kill Name/ID

# 从一个容器中取日志; -f, --follow=false Follow log output; -t, --timestamps=false Show timestamps
$docker logs Name/ID

# 列出一个容器里面被改变的文件或者目录,list列表会显示出三种事件,A 增加的,D 删除的,C 被改变的
$docker diff Name/ID

# 显示一个运行的容器里面的进程信息
$docker top Name/ID

# 从容器里面拷贝文件/目录到本地一个路径
$docker cp Name:/container_path to_path
$docker cp ID:/container_path to_path

# 重启一个正在运行的容器; -t, --time=10 Number of seconds to try to stop for before killing the container, Default=10
$docker restart Name/ID

# 附加到一个运行的容器上面; --no-stdin=false Do not attach stdin; --sig-proxy=true Proxify all received signal to the process
$docker attach ID

Note: attach命令允许你查看或者影响一个运行的容器。你可以在同一时间attach同一个容器。你也可以从一个容器中脱离出来,是从CTRL-C。

7. 保存和加载镜像(save、load)

当需要把一台机器上的镜像迁移到另一台机器的时候,需要保存镜像与加载镜像。

1
2
3
4
5
6
7
8
9
# 保存镜像到一个tar包; -o, --output="" Write to an file
$docker save image_name -o file_path
# 加载一个tar包格式的镜像; -i, --input="" Read from a tar archive file
$docker load -i file_path

# 机器a
$docker save image_name > /home/save.tar
# 使用scp将save.tar拷到机器b上,然后:
$docker load < /home/save.tar

8、 登录registry server(login)

1
2
# 登陆registry server; -e, --email="" Email; -p, --password="" Password; -u, --username="" Username
$docker login

9. 发布image(push)

1
2
# 发布docker镜像
$docker push new_image_name

10. 根据Dockerfile 构建出一个容器

1
2
3
4
5
6
#build
	  --no-cache=false Do not use cache when building the image
	  -q, --quiet=false Suppress the verbose output generated by the containers
	  --rm=true Remove intermediate containers after a successful build
	  -t, --tag="" Repository name (and optionally a tag) to be applied to the resulting image in case of success
$docker build -t image_name Dockerfile_path

CentOS 6 使用 LXC

http://purplegrape.blog.51cto.com/1330104/1343766/

LXC 自kernel 2.6.27 加入linux 内核,依赖Linux 内核的cgroup和namespace功能而实现,非常轻量级,设计用于操作系统内部应用级别的隔离。

不同于vmware,kvm等虚拟化技术,它是一种类似chroot的容器技术,非常的轻量级。

与传统的硬件虚拟化技术相比有以下优势:

a、更小的虚拟化开销。Linux内核本身是一个很好的硬件资源调度器,LXC的诸多特性基本由内核提供,而内核实现这些特性只有极少的花费,CPU,内存,硬盘都是直接使用。

b、更快的启动速度。lxc容器技术将操作系统抽象到了一个新的高度。直接从init启动,省去了硬件自检、grub引导、加载内核、加载驱动等传统启动项目,因此启动飞速。

c、更快速的部署。lxc与带cow特性的后端文件系统相结合,一旦建好了模板,利用快照功能,半秒钟即可实现克隆一台lxc虚拟机。LXC虚拟机本质上只是宿主机上的一个目录,这也为备份和迁移提供了极大便利。

d、更高内存使用效率。普通虚拟机一般会独占一段内存,即使闲置,其他虚拟机也无法使用,例如KVM。而容器可以只有一个内存上限,没有下限。如果它只使用1MB内存,那么它只占用宿主机1MB内存。宿主机可以将富余内存作为他用。

LXC 目前已经比较成熟,官方在2014年2月推出1.0版本后就开始了长期维护,目前最新版本已经是1.07,CentOS 从6.5 开始支持LXC技术。

将LXC投入生产环境完全没有问题,因为LXC并不是什么新技术,而是重新聚合了已经成熟了的技术。

环境CentOS 6.5 x64

1、安装LXC

1
2
3
yum install libcgroup lxc lxc-templates --enablerepo=epel
/etc/init.d/cgconfig start
/etc/init.d/lxc start

2、检查环境

1
lxc-checkconfig

输出如下即是OK

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
Kernel configuration not found at /proc/config.gz; searching...
Kernel configuration found at /boot/config-2.6.32-431.1.2.0.1.el6.x86_64
--- Namespaces ---
Namespaces: enabled
Utsname namespace: enabled
Ipc namespace: enabled
Pid namespace: enabled
User namespace: enabled
Network namespace: enabled
Multiple /dev/ptsinstances: enabled
--- Control groups---
Cgroup: enabled
Cgroup namespace: enabled
Cgroup device: enabled
Cgroup sched: enabled
Cgroup cpu account: enabled
Cgroup memory controller: enabled
Cgroup cpuset: enabled
--- Misc ---
Veth pair device: enabled
Macvlan: enabled
Vlan: enabled
File capabilities: enabled
Note : Before booting a new kernel, you can check its configuration
usage : CONFIG=/path/to/config/usr/bin/lxc-checkconfig

/usr/share/lxc/templates/ 自带了常用的模板可供选择,debian/ubuntu,centos/redhat 都有。

3、使用模板安装一个centos 6 虚拟机

1
lxc-create -n vm01 -t centos

或者

1
lxc-create -n vm01 -t download -- -d centos -r 6 -a amd64

安装后,虚拟机默认位于/var/lib/lxc/vm01/rootfs,配置文件为/var/lib/lxc/vm01/config

a、如果你系统里恰好有个lvm VG 名字叫做lxc,那么lxc会识别到,加上一个参数 -B lvm,创建的虚拟机配置文件依然是/var/lib/lxc/vm01/config,但是lxc镜像会在/dev/lxc/vm01 这个LV 上 (默认500M大小);

示例:

1
lxc-create -n vm01 -t centos -B lvm --thinpool --fssize 250G --fstype xfs

上面的命令将会在lvm上创建一个lv,名为vm01,最大容量250G(因为加了thinpool参数,用多少占多少),文件系统是xfs。

b、如果你的/var 单独分区,恰好使用的是btrfs文件系统,lxc也会识别,创建lxc容器时自动创建子卷,并将容器镜像放在里面;

4、lxc容器

打开lxc容器并进入开机console,

1
lxc-start -n vm01

在后台运行虚拟机,并通过console连接过去 (使用ctrl+a+q退出console)

1
2
lxc-start -n vm01 -d
lxc-console -n vm01

直接连上虚拟机,不需要密码,连上后passwd设置root密码

1
lxc-attach -n vm01

查看lxc容器相关信息(名称、是否在运行、PID,CPU使用、IO使用、内存使用、IP地址、网络吞吐量)

1
lxc-info -n vm01

监视lxc容器的资源使用

1
lxc-top

5、配置虚拟机网络,

新版lxc自带一个桥接lxcbr0 (10.0.3.1),物理网卡通过NAT桥接到lxcbr0 ,网段为10.0.3.0/24。

如果上面新创建的虚拟机启动失败,很可能是lxcbr0 没有启动。

编辑文件/var/lib/lxc/vm01/config,确保文件包含一下内容

1
2
3
4
5
6
lxc.network.type= veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.name = eth0
lxc.network.ipv4 = 10.0.3.2/24
lxc.network.ipv4.gateway = 10.0.3.1

如果需要第二块网卡,则继续在/var/lib/lxc/vm01/config添加一组配置

1
2
3
4
5
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.name = eth1
lxc.network.ipv4 = 10.0.3.3/24

虚拟机网络默认由dnsmasq分配,如果没有在lxc中指定,则由虚拟机内部dhcp获得。

veth依赖网卡桥接,且可以与任何机器(宿主机,其他虚拟机,局域网其他机器)通讯。

在网络层,可以采取下面的方式加固安全:

如果要隔绝虚拟机与宿主机的通讯(虚拟机之间可以通信,与局域网其他机器也可以通信),网卡可选择macvlan中的bridge模式

1
2
3
4
lxc.network.type = macvlan
lxc.network.macvlan.mode = bridge
lxc.network.flags = up
lxc.network.link = eth0

如果要进一步隔离同一宿主机上不同虚拟机之间的通讯(仅可与局域网其他机器通信),网卡还要选择macvlan中的vepa模式

1
2
3
4
lxc.network.type = macvlan
lxc.network.macvlan.mode = vepa
lxc.network.flags = up
lxc.network.link = eth0

下面是三种特殊的网络

1
lxc.network.type = none

none表示停用网络空间的namespace,复用宿主机的网络。

据说关闭容器也会关闭宿主机,ubuntu phone通过lxc里的安卓容器,使用网络复用达到兼容安卓应用的目的。(个人没有测试通过)

1
lxc.network.type = empty

empty表示容器没有网卡,仅有一个回环lo,无法通过网络层与外部通信。用于某些特殊的场合。比如将宿主机的某个图片目录挂载到容器里,容器利用有限的资源对图片进行处理,如果放在宿主机上处理,图片处理占用的资源可能不好控制,影响整体性能。

1
lxc.network.type = vlan

这种模式需要上联的物理交换机支持,用不同的vlan id 隔离容器与宿主机之间的通信。

6、控制虚拟机的资源

虚拟机默认与宿主机共享硬件资源,CPU,内存,IO等,也可以用cgroup实现资源隔离。

1
2
3
4
5
6
7
8
#设置虚拟机只使用0,1两个CPU核心
lxc-cgroup -n centos cpuset.cpus 0,1
#设置虚拟机可用内存为512M
lxc-cgroup -n centos memory.limit_in_bytes 536870912
#设置虚拟机消耗的CPU时间
 lxc-cgroup -n centos cpu.shares 256
#设置虚拟机消耗的IO权重
 lxc-cgroup -n centos blkio.weight 500

另一种限制资源的方法是将具体的限制写入虚拟机的配置文件,可选的参数如下:

1
2
3
4
5
6
7
8
9
10
#设置虚拟机只使用0,1两个CPU核心
lxc.cgroup.cpuset.cpus  = 0,1
#设置虚拟机消耗的CPU时间
lxc.cgroup.cpu.shares  = 256
#设置虚拟机可用内存为512M
lxc.cgroup.memory.limit_in_bytes = 512M
#限制虚拟机可用的内存和swap空间一共1G
lxc.cgroup.memory.memsw.limit_in_bytes = 1G
#设置虚拟机可使用的IO权重
lxc.cgroup.blkio.weight=500

7、安装ubuntu 12.04

LXC强大到有点变态,在centos上运行ubuntu?没错,因为内核对于LInux发行版来说是通用的。

1
lxc-create -n ubuntu -t ubuntu -- -r precise

或者加上MIRROR参数(仅适用于ubuntu,用于选择较近的软件源)

1
MIRROR="http://cn.archive.ubuntu.com/ubuntu"  lxc-create -n ubuntu-test -t ubuntu -- -r precise

点到为止,不深入。

8、容器克隆

你可以创建一个标准化的lxc容器作为模板,然后对它进行克隆,避免重新安装,实现横向扩展和环境的标准化。下面以基于lvm卷的容器为例

1
lxc-clone vm01 webserver01 -B lvm

克隆后的容器,是一个独立的lvm逻辑卷,默认与原来的大小一致(也可以指定大小),仅仅会改变mac地址和主机名。

如果你想节约空间,克隆时带上 -s (–snapshot) 参数,可以创建一个源容器的可读写快照,它几乎不占用空间,使得在一个机器上运行成百上千个容器成为可能,仅支持lvm和btrfs,因为它们都有cow功能 。-L 参数可以指定快照的大小。更多参数详见 man lxc-clone 。

1
lxc-clone vm01 webserver01 -s -B lvm

9、lxc容器的系统安全

lxc容器里的系统完全可以不需要用到root密码和ssh,可以设置空密码或者超级长的密码,openssh服务可以不必启动甚至不必安装。因为从宿主机运行下面的命令可以直接获得root shell,相当于chroot

1
lxc-attach -n webserver01

如果是应用容器,则更简单,因为容器里只有应用进程,比如httpd,连init 都木有。具体实现参考模板lxc-sshd 。

lxc 1.0还支持非特权容器,利用uidmap映射技术,将容器里的root映射为宿主机上的普通用户,允许以普通用户身份运行LXC容器,大大提高了宿主机的安全性。

使用方法省略,见我的另一篇文章。《ubuntu 14.04 体验LXC非特权容器》

http://purplegrape.blog.51cto.com/1330104/1528503

relay 数据传输

https://www.ibm.com/developerworks/cn/linux/l-cn-relay/

Relay 要解决的问题

对于任何在内核工作的程序而言,如何把大量的调试信息从内核空间传输到用户空间都是一个大麻烦,对于运行中的内核更是如此。特别是对于哪些用于调试内核性能的工具,更是如此。

对于这种大量数据需要在内核中缓存并传输到用户空间需求,很多传统的方法都已到达了极限,例如内核程序员很熟悉的 printk() 调用。此外,如果不同的内核子系统都开发自己的缓存和传输代码,造成很大的代码冗余,而且也带来维护上的困难。

这些,都要求开发一套能够高效可靠地将数据从内核空间转发到用户空间的系统,而且这个系统应该独立于各个调试子系统。

这样就诞生了 RelayFS。

Relay的发展历史

Relay 的前身是 RelayFS,即作为 Linux 的一个新型文件系统。2003年3月,RelayFS的第一个版本的代码被开发出来,在7月14日,第一个针对2.6内核的版本也开始提供下载。经过广泛的试用和改进,直到2005年9月,RelayFS才被加入mainline内核(2.6.14)。同时,RelayFS也被移植到2.4内核中。在2006年2月,从2.6.17开始,RelayFS不再作为单独的文件系统存在,而是成为内核的一部分。它的源码也从fs/目录下转移到kernel/relay.c中,名称中也从RelayFS改成了Relay。

RelayFS目前已经被越来越多的内核工具使用,包括内核调试工具SystemTap、LTT,以及一些特殊的文件系统例如DebugFS。

Relay的基本原理

总的说来,Relay提供了一种机制,使得内核空间的程序能够通过用户定义的relay通道(channel)将大量数据高效的传输到用户空间。

一个relay通道由一组和CPU一一对应的内核缓冲区组成。这些缓冲区又被称为relay缓冲区(buffer),其中的每一个在用户空间都用一个常规文件来表示,这被叫做relay文件(file)。内核空间的用户可以利用relay提供的API接口来写入数据,这些数据会被自动的写入当前的CPU id对应的那个relay缓冲区;同时,这些缓冲区从用户空间看来,是一组普通文件,可以直接使用read()进行读取,也可以使用mmap()进行映射。Relay并不关心数据的格式和内容,这些完全依赖于使用relay的用户程序。Relay的目的是提供一个足够简单的接口,从而使得基本操作尽可能的高效。

Relay将数据的读和写分离,使得突发性大量数据写入的时候,不需要受限于用户空间相对较慢的读取速度,从而大大提高了效率。Relay作为写入和读取的桥梁,也就是将内核用户写入的数据缓存并转发给用户空间的程序。这种转发机制也正是Relay这个名称的由来。

下面这个图给出了Relay的基本结构和典型操作:

Relay的基本结构和典型操作

可以看到,这里的relay通道由四个relay缓冲区(kbuf0到kbuf3)组成,分别对应于系统中的cpu0到cpu1。每个CPU上的代码调用relay_write()的时候将数据写入自己对应的relay缓冲区内。每个relay缓冲区称一个relay文件,即/cpu0到/cpu3。当文件系统被mount到/mnt/以后,这个relay文件就被映射成映射到用户空间的地址空间。一旦数据可用,用户程序就可以把它的数据读出来写入到硬盘上的文件中,即cpu0.out到cpu3.out。

Relay的主要API

前面提到的 relay_write() 就是 relay API 之一。除此以外,Relay 还提供了更多的 API来支持用户程序完整的使用 relay。这些 API,主要按照面向用户空间和面向内核空间分为两大类,下面我们来分别进行介绍。

面向用户空间的 API

这些 Relay 编程接口向用户空间程序提供了访问 relay 通道缓冲区数据的基本操作的入口,包括:

1
2
3
4
5
6
open() - 允许用户打开一个已经存在的通道缓冲区
mmap() - 使通道缓冲区被映射到位于用户空间的调用者的地址空间。要特别注意的是,我们不能仅对局部区域进行映射。也就是说,必须映射整个缓冲区文件,其大小是 CPU的个数和单个 CPU 缓冲区大小的乘积
read() - 读取通道缓冲区的内容。这些数据一旦被读出,就意味着他们被用户空间的程序消费掉了,也就不能被之后的读操作看到
sendfile() - 将数据从通道缓冲区传输到一个输出文件描述符。其中可能的填充字符会被自动去掉,不会被用户看到
poll() - 支持 POLLIN/POLLRDNORM/POLLERR 信号。每次子缓冲区的边界被越过时,等待着的用户空间程序会得到通知
close() - 将通道缓冲区的引用数减1。当引用数减为0时,表明没有进程或者内核用户需要打开它,从而这个通道缓冲区被释放。
面向内核空间的 API

这些API接口向位于内核空间的用户提供了管理relay通道、数据写入等功能。下面介绍其中主要的部分,完整的API接口列表请参见这里。

1
2
3
4
relay_open() - 创建一个relay通道,包括创建每个CPU对应的relay缓冲区。
relay_close() - 关闭一个relay通道,包括释放所有的relay缓冲区,在此之前会调用relay_switch()来处理这些relay缓冲区以保证已读取但是未满的数据不会丢失
relay_write() - 将数据写入到当前CPU对应的relay缓冲区内。由于它使用了local_irqsave()保护,因此也可以在中断上下文中使用。
relay_reserve() - 在relay通道中保留一块连续的区域来留给未来的写入操作。这通常用于那些希望直接写入到relay缓冲区的用户。考虑到性能或者其它因素,这些用户不希望先把数据写到一个临时缓冲区中,然后再通过relay_write()进行写入。

Relay的例子

我们用一个最简单的例子来介绍怎么使用Relay。这个例子由两部分组成:一部分是位于内核空间将数据写入relay文件的程序,使用时需要作为一个内核模块被加载;另一部分是位于用户空间从relay文件中读取数据的程序,使用时作为普通用户态程序运行。

内核空间的程序主要操作是:
加载模块时,打开一个relay通道,并且往打开的relay通道中写入消息;
卸载模块时,关闭relay通道。

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
#include <linux/module.h>
#include <linux/relay.h>
#include <linux/debugfs.h>

static struct dentry *create_buf_file_handler(const char *filename, struct dentry *parent, int mode, struct rchan_buf *buf, int *is_global)
{
	return debugfs_create_file(filename, mode, parent, buf, &relay_file_operations);
}

static int remove_buf_file_handler(struct dentry *dentry)
{
	debugfs_remove(dentry);
	return 0;
}

static struct rchan_callbacks relay_callbacks =
{
	.create_buf_file = create_buf_file_handler,
	.remove_buf_file = remove_buf_file_handler,
};

static struct rchan *hello_rchan;
struct dentry *dir;

int init_module(void)
{
	const char *msg="Hello world\n";
	dir = debugfs_create_dir("test", NULL);
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,32))
	hello_rchan = relay_open("cpu", dir, 8192, 2, &relay_callbacks, NULL);
#else   
	hello_rchan = relay_open("cpu", dir, 8192, 2, &relay_callbacks);
#endif  
	if(!hello_rchan){
		printk("relay_open() failed.\n");
		return -ENOMEM;
	}
	relay_write(hello_rchan, msg, strlen(msg));
	return 0;
}

查看输出

1
2
mount -t debugfs debugfs /media
cat /media/test/cpu*


http://www.cnblogs.com/hoys/archive/2011/04/10/2011270.html

用户空间与内核空间数据交换的方式(4)——relayfs

relayfs是一个快速的转发(relay)数据的文件系统,它以其功能而得名。它为那些需要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制。

Channel是relayfs文件系统定义的一个主要概念,每一个channel由一组内核缓存组成,每一个CPU有一个对应于该channel 的内核缓存,每一个内核缓存用一个在relayfs文件系统中的文件文件表示,内核使用relayfs提供的写函数把需要转发给用户空间的数据快速地写入当前CPU上的channel内核缓存,用户空间应用通过标准的文件I/O函数在对应的channel文件中可以快速地取得这些被转发出的数据mmap 来。写入到channel中的数据的格式完全取决于内核中创建channel的模块或子系统。

relayfs的用户空间API:

relayfs实现了四个标准的文件I/O函数,open、mmap、poll和close.

open(),打开一个channel在某一个CPU上的缓存对应的文件。

mmap(),把打开的channel缓存映射到调用者进程的内存空间。

read (),读取channel缓存,随后的读操作将看不到被该函数消耗的字节,如果channel的操作模式为非覆盖写,那么用户空间应用在有内核模块写时仍 可以读取,但是如果channel的操作模式为覆盖式,那么在读操作期间如果有内核模块进行写,结果将无法预知,因此对于覆盖式写的channel,用户 应当在确认在channel的写完全结束后再进行读。

poll(),用于通知用户空间应用转发数据跨越了子缓存的边界,支持的轮询标志有POLLIN、POLLRDNORM和POLLERR。

close(),关闭open函数返回的文件描述符,如果没有进程或内核模块打开该channel缓存,close函数将释放该channel缓存。

注意:用户态应用在使用上述API时必须保证已经挂载了relayfs文件系统,但内核在创建和使用channel时不需要relayfs已经挂载。下面命令将把relayfs文件系统挂载到/mnt/relay。

1
mount -t relayfs relayfs /mnt/relay

relayfs内核API:

relayfs提供给内核的API包括四类:channel管理、写函数、回调函数和辅助函数。

Channel管理函数包括:

1
2
3
4
5
6
7
8
relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks)
relay_close(chan)
relay_flush(chan)
relay_reset(chan)
relayfs_create_dir(name, parent)
relayfs_remove_dir(dentry)
relay_commit(buf, reserved, count)
relay_subbufs_consumed(chan, cpu, subbufs_consumed)

写函数包括:

1
2
3
relay_write(chan, data, length)
__relay_write(chan, data, length)
relay_reserve(chan, length)

回调函数包括:

1
2
3
subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf)
buf_mapped(buf, filp)
buf_unmapped(buf, filp)

辅助函数包括:

1
2
relay_buf_full(buf)
subbuf_start_reserve(buf, length)

前面已经讲过,每一个channel由一组channel缓存组成,每个CPU对应一个该channel的缓存,每一个缓存又由一个或多个子缓存组成,每一个缓存是子缓存组成的一个环型缓存。

函数relay_open用于创建一个channel并分配对应于每一个CPU的缓存,用户空间应用通过在relayfs文件系统中对应的文件可以 访问channel缓存,参数base_filename用于指定channel的文件名,relay_open函数将在relayfs文件系统中创建 base_filename0..base_filenameN-1,即每一个CPU对应一个channel文件,其中N为CPU数,缺省情况下,这些文件将建立在relayfs文件系统的根目录下,但如果参数parent非空,该函数将把channel文件创建于parent目录下,parent目录使 用函数relay_create_dir创建,函数relay_remove_dir用于删除由函数relay_create_dir创建的目录,谁创建的目录,谁就负责在不用时负责删除。参数subbuf_size用于指定channel缓存中每一个子缓存的大小,参数n_subbufs用于指定 channel缓存包含的子缓存数,因此实际的channel缓存大小为(subbuf_size x n_subbufs),参数overwrite用于指定该channel的操作模式,relayfs提供了两种写模式,一种是覆盖式写,另一种是非覆盖式 写。使用哪一种模式完全取决于函数subbuf_start的实现,覆盖写将在缓存已满的情况下无条件地继续从缓存的开始写数据,而不管这些数据是否已经 被用户应用读取,因此写操作决不失败。在非覆盖写模式下,如果缓存满了,写将失败,但内核将在用户空间应用读取缓存数据时通过函数 relay_subbufs_consumed()通知relayfs。如果用户空间应用没来得及消耗缓存中的数据或缓存已满,两种模式都将导致数据丢失,唯一的区别是,前者丢失数据在缓存开头,而后者丢失数据在缓存末尾。一旦内核再次调用函数relay_subbufs_consumed(),已满的缓存将不再满,因而可以继续写该缓存。当缓存满了以后,relayfs将调用回调函数buf_full()来通知内核模块或子系统。当新的数据太大无法写 入当前子缓存剩余的空间时,relayfs将调用回调函数subbuf_start()来通知内核模块或子系统将需要使用新的子缓存。内核模块需要在该回调函数中实现下述功能:

初始化新的子缓存;

如果1正确,完成当前子缓存;

如果2正确,返回是否正确完成子缓存切换;

在非覆盖写模式下,回调函数subbuf_start()应该如下实现:

1
2
3
4
5
6
7
8
9
10
11
static int subbuf_start(struct rchan_buf *buf, void *subbuf, void *prev_subbuf, unsigned intprev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;

	if (relay_buf_full(buf))
		return 0;

	subbuf_start_reserve(buf, sizeof(unsigned int));
	return 1;
}

如果当前缓存满,即所有的子缓存都没读取,该函数返回0,指示子缓存切换没有成功。当子缓存通过函数relay_subbufs_consumed ()被读取后,读取者将负责通知relayfs,函数relay_buf_full()在已经有读者读取子缓存数据后返回0,在这种情况下,子缓存切换成 功进行。

在覆盖写模式下, subbuf_start()的实现与非覆盖模式类似:

1
2
3
4
5
6
7
8
9
static int subbuf_start(struct rchan_buf *buf, void *subbuf, void *prev_subbuf, unsigned int prev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;

	subbuf_start_reserve(buf, sizeof(unsigned int));

	return 1;
}

只是不做relay_buf_full()检查,因为此模式下,缓存是环行的,可以无条件地写。因此在此模式下,子缓存切换必定成功,函数 relay_subbufs_consumed() 也无须调用。如果channel写者没有定义subbuf_start(),缺省的实现将被使用。 可以通过在回调函数subbuf_start()中调用辅助函数subbuf_start_reserve()在子缓存中预留头空间,预留空间可以保存任 何需要的信息,如上面例子中,预留空间用于保存子缓存填充字节数,在subbuf_start()实现中,前一个子缓存的填充值被设置。前一个子缓存的填 充值和指向前一个子缓存的指针一道作为subbuf_start()的参数传递给subbuf_start(),只有在子缓存完成后,才能知道填充值。 subbuf_start()也被在channel创建时分配每一个channel缓存的第一个子缓存时调用,以便预留头空间,但在这种情况下,前一个子 缓存指针为NULL。

内核模块使用函数relay_write()或__relay_write()往channel缓存中写需要转发的数据,它们的区别是前者失效了本 地中断,而后者只抢占失效,因此前者可以在任何内核上下文安全使用,而后者应当在没有任何中断上下文将写channel缓存的情况下使用。这两个函数没有 返回值,因此用户不能直接确定写操作是否失败,在缓存满且写模式为非覆盖模式时,relayfs将通过回调函数buf_full来通知内核模块。

函数relay_reserve()用于在channel缓存中预留一段空间以便以后写入,在那些没有临时缓存而直接写入channel缓存的内核 模块可能需要该函数,使用该函数的内核模块在实际写这段预留的空间时可以通过调用relay_commit()来通知relayfs。当所有预留的空间全 部写完并通过relay_commit通知relayfs后,relayfs将调用回调函数deliver()通知内核模块一个完整的子缓存已经填满。由于预留空间的操作并不在写channel的内核模块完全控制之下,因此relay_reserve()不能很好地保护缓存,因此当内核模块调用 relay_reserve()时必须采取恰当的同步机制。

当内核模块结束对channel的使用后需要调用relay_close() 来关闭channel,如果没有任何用户在引用该channel,它将和对应的缓存全部被释放。

函数relay_flush()强制在所有的channel缓存上做一个子缓存切换,它在channel被关闭前使用来终止和处理最后的子缓存。

函数relay_reset()用于将一个channel恢复到初始状态,因而不必释放现存的内存映射并重新分配新的channel缓存就可以使用channel,但是该调用只有在该channel没有任何用户在写的情况下才可以安全使用。

回调函数buf_mapped() 在channel缓存被映射到用户空间时被调用。

回调函数buf_unmapped()在释放该映射时被调用。内核模块可以通过它们触发一些内核操作,如开始或结束channel写操作。

在源代码包中给出了一个使用relayfs的示例程序relayfs_exam.c,它只包含一个内核模块,对于复杂的使用,需要应用程序配合。该模块实现了类似于文章中seq_file示例实现的功能。

当然为了使用relayfs,用户必须让内核支持relayfs,并且要mount它,下面是作者系统上的使用该模块的输出信息:

1
2
3
4
5
6
$ mkdir -p /relayfs
$ insmod ./relayfs-exam.ko
$ mount -t relayfs relayfs /relayfs
$ cat /relayfs/example0
$

relayfs是一种比较复杂的内核态与用户态的数据交换方式,本例子程序只提供了一个较简单的使用方式,对于复杂的使用,请参考relayfs用例页面http://relayfs.sourceforge.net/examples.html%E3%80%82

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//kernel module: relayfs-exam.c
#include <linux/module.h>
#include <linux/relayfs_fs.h>
#include <linux/string.h>
#include <linux/sched.h>

#define WRITE_PERIOD (HZ * 60)
static struct rchan * chan;
static size_t subbuf_size = 65536;
static size_t n_subbufs = 4;
static char buffer[256];

void relayfs_exam_write(unsigned long data);

static DEFINE_TIMER(relayfs_exam_timer, relayfs_exam_write, 0, 0);

void relayfs_exam_write(unsigned long data)
{
	int len;
	task_t * p = NULL;

	len = sprintf(buffer, "Current all the processes:\n");
	len += sprintf(buffer + len, "process name\t\tpid\n");
	relay_write(chan, buffer, len);

	for_each_process(p) {
		len = sprintf(buffer, "%s\t\t%d\n", p->comm, p->pid);
		relay_write(chan, buffer, len);
	}
	len = sprintf(buffer, "\n\n");
	relay_write(chan, buffer, len);

	relayfs_exam_timer.expires = jiffies + WRITE_PERIOD;
	add_timer(&relayfs_exam_timer);
}


/*
* subbuf_start() relayfs callback.
*
* Defined so that we can 1) reserve padding counts in the sub-buffers, and
* 2) keep a count of events dropped due to the buffer-full condition.
*/
static int subbuf_start(struct rchan_buf *buf,
				void *subbuf,
				void *prev_subbuf,
				unsigned int prev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;

	if (relay_buf_full(buf))
		return 0;

	subbuf_start_reserve(buf, sizeof(unsigned int));

	return 1;
}

/*
* relayfs callbacks
*/
static struct rchan_callbacks relayfs_callbacks =
{
	.subbuf_start = subbuf_start,
};

/**
* module init - creates channel management control files
*
* Returns 0 on success, negative otherwise.
*/
static int init(void)
{

	chan = relay_open("example", NULL, subbuf_size,
	n_subbufs, &relayfs_callbacks);

	if (!chan) {
		printk("relay channel creation failed.\n");
		return 1;
	}
	relayfs_exam_timer.expires = jiffies + WRITE_PERIOD;
	add_timer(&relayfs_exam_timer);

	return 0;
}

static void cleanup(void)
{
	del_timer_sync(&relayfs_exam_timer);
	if (chan) {
		relay_close(chan);
		chan = NULL;
	}
}

module_init(init);
module_exit(cleanup);
MODULE_LICENSE("GPL");