kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

cgroups 数据结构设计

http://www.cnblogs.com/lisperl/archive/2012/04/18/2455027.html

我们从进程出发来剖析cgroups相关数据结构之间的关系。 在Linux中,管理进程的数据结构是task_struct,其中与cgroups有关的:

1
2
3
4
5
6
#ifdef CONFIG_CGROUPS
	/* Control Group info protected by css_set_lock */
	struct css_set *cgroups;
	/* cg_list protected by css_set_lock and tsk->alloc_lock */
	struct list_head cg_list;
#endif

其中cgroups指针指向了一个css_set结构,而css_set存储了与进程相关的cgroups信息。Cg_list是一个嵌入的list_head结构,用于将连到同一个css_set的进程组织成一个链表。下面我们来看css_set的结构:

1
2
3
4
5
6
7
8
struct css_set {
	atomic_t refcount;
	struct hlist_node hlist;
	struct list_head tasks;
	struct list_head cg_links;
	struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
	struct rcu_head rcu_head;
};

其中refcount是该css_set的引用数,因为一个css_set可以被多个进程共用,只要这些进程的cgroups信息相同,比如:在所有已创建的层级里面都在同一个cgroup里的进程。

hlist是嵌入的hlist_node,用于把所有css_set组织成一个hash表,这样内核可以快速查找特定的css_set。

tasks指向所有连到此css_set的进程连成的链表。

cg_links指向一个由struct cg_cgroup_link连成的链表。

Subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。一个cgroup_subsys_state就是进程与一个特定子系统相关的信息。通过这个指针数组,进程就可以获得相应的cgroups控制信息了。

下面我们就来看cgroup_subsys_state的结构:

1
2
3
4
5
6
struct cgroup_subsys_state {
	struct cgroup *cgroup;
	atomic_t refcnt;
	unsigned long flags;
	struct css_id *id;
};

cgroup指针指向了一个cgroup结构,也就是进程属于的cgroup。进程受到子系统的控制,实际上是通过加入到特定的cgroup实现的,因为cgroup在特定的层级上,而子系统又是附加到曾经上的。通过以上三个结构,进程就可以和cgroup连接起来了:task_struct->css_set->cgroup_subsys_state->cgroup。

下面我们再来看cgroup的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct cgroup {
	unsigned long flags;       
	atomic_t count;
	struct list_head sibling;  
	struct list_head children; 
	struct cgroup *parent;     
	struct dentry *dentry;     
	struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
	struct cgroupfs_root *root;
	struct cgroup *top_cgroup;
	struct list_head css_sets;
	struct list_head release_list;
	struct list_head pidlists;
	struct mutex pidlist_mutex;
	struct rcu_head rcu_head;
	struct list_head event_list;
	spinlock_t event_list_lock;
};

sibling,children和parent三个嵌入的list_head负责将同一层级的cgroup连接成一颗cgroup树。

subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。这组指针指向了此cgroup跟各个子系统相关的信息,这个跟css_set中的道理是一样的。

root指向了一个cgroupfs_root的结构,就是cgroup所在的层级对应的结构体。这样以来,之前谈到的几个cgroups概念就全部联系起来了。

top_cgroup指向了所在层级的根cgroup,也就是创建层级时自动创建的那个cgroup。

css_set指向一个由struct cg_cgroup_link连成的链表,跟css_set中cg_links一样。

下面我们来分析一个css_set和cgroup之间的关系。我们先看一下 cg_cgroup_link的结构

1
2
3
4
5
6
struct cg_cgroup_link {
	struct list_head cgrp_link_list;
	struct cgroup *cgrp;
	struct list_head cg_link_list;
	struct css_set *cg;
};

cgrp_link_list连入到cgroup->css_set指向的链表,cgrp则指向此cg_cgroup_link相关的cgroup。

Cg_link_list则连入到css_set->cg_links指向的链表,cg则指向此cg_cgroup_link相关的css_set。

那为什么要这样设计呢?

那是因为cgroup和css_set是一个多对多的关系,必须添加一个中间结构来将两者联系起来,这跟数据库模式设计是一个道理。cg_cgroup_link中的cgrp和cg就是此结构体的联合主键,而cgrp_link_list和cg_link_list分别连入到cgroup和css_set相应的链表,使得能从cgroup或css_set都可以进行遍历查询。

那为什么cgroup和css_set是多对多的关系呢?

一个进程对应css_set,一个css_set就存储了一组进程(应该有可能被几个进程共享,所以是一组)跟各个子系统相关的信息,但是这些信息有可能不是从一个cgroup那里获得的,因为一个进程可以同时属于几个cgroup,只要这些cgroup不在同一个层级。举个例子:我们创建一个层级A,A上面附加了cpu和memory两个子系统,进程B属于A的根cgroup;然后我们再创建一个层级C,C上面附加了ns和blkio两个子系统,进程B同样属于C的根cgroup;那么进程B对应的cpu和memory的信息是从A的根cgroup获得的,ns和blkio信息则是从C的根cgroup获得的。因此,一个css_set存储的cgroup_subsys_state可以对应多个cgroup。另一方面,cgroup也存储了一组cgroup_subsys_state,这一组cgroup_subsys_state则是cgroup从所在的层级附加的子系统获得的。一个cgroup中可以有多个进程,而这些进程的css_set不一定都相同,因为有些进程可能还加入了其他cgroup。但是同一个cgroup中的进程与该cgroup关联的cgroup_subsys_state都受到该cgroup的管理(cgroups中进程控制是以cgroup为单位的)的,所以一个cgrouop也可以对应多个css_set。

那为什么要这样一个结构呢?

从前面的分析,我们可以看出从task到cgroup是很容易定位的,但是从cgroup获取此cgroup的所有的task就必须通过这个结构了。每个进程都会指向一个css_set,而与这个css_set关联的所有进程都会链入到css_set->tasks链表.而cgroup又通过一个中间结构cg_cgroup_link来寻找所有与之关联的所有css_set,从而可以得到与cgroup关联的所有进程。

最后让我们看一下层级和子系统对应的结构体。层级对应的结构体是cgroupfs_root:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct cgroupfs_root {
	struct super_block *sb;
	unsigned long subsys_bits;
	int hierarchy_id;
	unsigned long actual_subsys_bits;
	struct list_head subsys_list;
	struct cgroup top_cgroup;
	int number_of_cgroups;
	struct list_head root_list;
	unsigned long flags;
	char release_agent_path[PATH_MAX];
	char name[MAX_CGROUP_ROOT_NAMELEN];
};

sb指向该层级关联的文件系统超级块

subsys_bits和actual_subsys_bits分别指向将要附加到层级的子系统和现在实际附加到层级的子系统,在子系统附加到层级时使用

hierarchy_id是该层级唯一的id

top_cgroup指向该层级的根cgroup

number_of_cgroups记录该层级cgroup的个数

root_list是一个嵌入的list_head,用于将系统所有的层级连成链表

子系统对应的结构体是cgroup_subsys:

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
struct cgroup_subsys {
	struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss,
								struct cgroup *cgrp);
	int (*pre_destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
	void (*destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
	int (*can_attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
				struct task_struct *tsk, bool threadgroup);
	void (*cancel_attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
				struct task_struct *tsk, bool threadgroup);
	void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
			struct cgroup *old_cgrp, struct task_struct *tsk,
			bool threadgroup);
	void (*fork)(struct cgroup_subsys *ss, struct task_struct *task);
	void (*exit)(struct cgroup_subsys *ss, struct task_struct *task);
	int (*populate)(struct cgroup_subsys *ss,
			struct cgroup *cgrp);
	void (*post_clone)(struct cgroup_subsys *ss, struct cgroup *cgrp);
	void (*bind)(struct cgroup_subsys *ss, struct cgroup *root);
 
	int subsys_id;
	int active;
	int disabled;
	int early_init;
	bool use_id;
#define MAX_CGROUP_TYPE_NAMELEN 32
	const char *name;
	struct mutex hierarchy_mutex;
	struct lock_class_key subsys_key;
	struct cgroupfs_root *root;
	struct list_head sibling;
	struct idr idr;
	spinlock_t id_lock;
	struct module *module;
};

Cgroup_subsys定义了一组操作,让各个子系统根据各自的需要去实现。这个相当于C++中抽象基类,然后各个特定的子系统对应cgroup_subsys则是实现了相应操作的子类。类似的思想还被用在了cgroup_subsys_state中,cgroup_subsys_state并未定义控制信息,而只是定义了各个子系统都需要的共同信息,比如该cgroup_subsys_state从属的cgroup。然后各个子系统再根据各自的需要去定义自己的进程控制信息结构体,最后在各自的结构体中将cgroup_subsys_state包含进去,这样通过Linux内核的container_of等宏就可以通过cgroup_subsys_state来获取相应的结构体。

Linux内核的namespace机制分析

http://www.linuxidc.com/Linux/2015-02/113022.htm

1. Linux内核namespace机制

Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的Namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的。因此在操作系统层面上看,就会出现多个相同pid的进程。系统中可以同时存在两个进程号为0,1,2的进程,由于属于不同的namespace,所以它们之间并不冲突。而在用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。这样每个namespace看上去就像一个单独的Linux系统。

2 . Linux内核中namespace结构体

在Linux内核中提供了多个namespace,其中包括fs (mount), uts, network, sysvipc, 等。一个进程可以属于多个namesapce,既然namespace和进程相关,那么在task_struct结构体中就会包含和namespace相关联的变量。在task_struct 结构中有一个指向namespace结构体的指针nsproxy。

1
2
3
4
5
6
struct task_struct {
	……..
	/* namespaces */
	struct nsproxy *nsproxy;
	…….
}

再看一下nsproxy是如何定义的,在include/linux/nsproxy.h文件中,这里一共定义了5个各自的命名空间结构体,在该结构体中定义了5个指向各个类型namespace的指针,由于多个进程可以使用同一个namespace,所以nsproxy可以共享使用,count字段是该结构的引用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 'count' is the number of tasks holding a reference.
 * The count for each namespace, then, will be the number
 * of nsproxies pointing to it, not the number of tasks.
 * The nsproxy is shared by tasks which share all namespaces.
 * As soon as a single namespace is cloned or unshared, the
 * nsproxy is copied
*/
struct nsproxy {
	atomic_t count;
	struct uts_namespace *uts_ns;
	struct ipc_namespace *ipc_ns;
	struct mnt_namespace *mnt_ns;
	struct pid_namespace *pid_ns_for_children;
	struct net           *net_ns;
};

(1) UTS命名空间包含了运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System的简称。

(2) 保存在struct ipc_namespace中的所有与进程间通信(IPC)有关的信息。

(3) 已经装载的文件系统的视图,在struct mnt_namespace中给出。

(4) 有关进程ID的信息,由struct pid_namespace提供。

(5) struct net_ns包含所有网络相关的命名空间参数。

系统中有一个默认的nsproxy,init_nsproxy,该结构在task初始化是也会被初始化。

1
2
#define INIT_TASK(tsk)  \
{ .nsproxy  = &init_nsproxy,   }

其中init_nsproxy的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct kmem_cache *nsproxy_cachep;
 
struct nsproxy init_nsproxy = {
	.count                       = ATOMIC_INIT(1),
	.uts_ns                      = &init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
	.ipc_ns                        = &init_ipc_ns,
#endif
	.mnt_ns                      = NULL,
	.pid_ns_for_children         = &init_pid_ns,
#ifdef CONFIG_NET
	.net_ns                      = &init_net,
#endif
};

对于 .mnt_ns 没有进行初始化,其余的namespace都进行了系统默认初始。

3. 使用clone创建自己的Namespace

如果要创建自己的命名空间,可以使用系统调用clone(),它在用户空间的原型为

1
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)

这里fn是函数指针,这个就是指向函数的指针,, child_stack是为子进程分配系统堆栈空间,flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数也就是fn指向的函数参数。下面是flags可以取的值。这里只关心和namespace相关的参数。

CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask

CLONE_NEWNS 当clone需要自己的命名空间时设置这个标志,不能同时设置CLONE_NEWS和CLONE_FS。

Clone()函数是在libc库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏了clone系统条用。实现clone()系统调用的sys_clone()服务例程并没有fn和arg参数。封装函数把fn指针存放在子进程堆栈的每个位置处,该位置就是该封装函数本身返回地址存放的位置。Arg指针正好存放在子进程堆栈中的fn的下面。当封装函数结束时,CPU从堆栈中取出返回地址,然后执行fn(arg)函数。

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
/* Prototype for the glibc wrapper function */
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
			int flags, void *arg, ...
			/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

/* Prototype for the raw system call */
long clone(unsigned long flags, void *child_stack,
			void *ptid, void *ctid,
			struct pt_regs *regs);
我们在Linux内核中看到的实现函数,是经过libc库进行封装过的,在Linux内核中的fork.c文件中,有下面的定义,最终调用的都是do_fork()函数。

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
				int __user *, parent_tidptr,
				int, tls_val,
				int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
				int __user *, parent_tidptr,
				int __user *, child_tidptr,
				int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
				int, stack_size,
				int __user *, parent_tidptr,
				int __user *, child_tidptr,
				int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
				int __user *, parent_tidptr,
				int __user *, child_tidptr,
				int, tls_val)
#endif
{
	return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
3.1 do_fork函数

在clone()函数中调用do_fork函数进行真正的处理,在do_fork函数中调用copy_process进程处理。

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
long do_fork(unsigned long clone_flags,
			unsigned long stack_start,
			unsigned long stack_size,
			int __user *parent_tidptr,
			int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
 
	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;
 
		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}
 
	p = copy_process(clone_flags, stack_start, stack_size,
					child_tidptr, NULL, trace);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;
 
		trace_sched_process_fork(current, p);
 
		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);
 
		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);
 
		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}
 
		wake_up_new_task(p);
 
		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);
 
		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}
 
		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}
3.2 copy_process函数

在copy_process函数中调用copy_namespaces函数。

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
static struct task_struct *copy_process(unsigned long clone_flags,
						unsigned long stack_start,
						unsigned long stack_size,
						int __user *child_tidptr,
						struct pid *pid,
						int trace)
{
	int retval;
	struct task_struct *p;
	/*下面的代码是对clone_flag标志进行检查,有部分表示是互斥的,例如CLONE_NEWNS和CLONENEW_FS*/
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);
 
	if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
		return ERR_PTR(-EINVAL);
 
	if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
		return ERR_PTR(-EINVAL);
 
	if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
		return ERR_PTR(-EINVAL);
 
	if ((clone_flags & CLONE_PARENT) &&
					  current->signal->flags & SIGNAL_UNKILLABLE)
		return ERR_PTR(-EINVAL);

	……
	retval = copy_namespaces(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_mm;
	retval = copy_io(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_namespaces;
	retval = copy_thread(clone_flags, stack_start, stack_size, p);
	if (retval)
		goto bad_fork_cleanup_io;
	/*do_fork中调用copy_process函数,该函数中pid参数为NULL,所以这里的if判断是成立的。为进程所在的namespace分配pid,在3.0的内核之前还有一个关键函数,就是namespace创建后和cgroup的关系,
	if (current->nsproxy != p->nsproxy) {
		retval = ns_cgroup_clone(p, pid);
		if (retval)
			goto bad_fork_free_pid;
	但在3.0内核以后给删掉了,具体请参考remove the ns_cgroup*/
	if (pid != &init_struct_pid) {
		retval = -ENOMEM;
		pid = alloc_pid(p->nsproxy->pid_ns_for_children);
		if (!pid)
			goto bad_fork_cleanup_io;
	}…..
}

3.3 copy_namespaces 函数

在kernel/nsproxy.c文件中定义了copy_namespaces函数。

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
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
	struct nsproxy *old_ns = tsk->nsproxy;
	struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
	struct nsproxy *new_ns;
	/*首先检查flag,如果flag标志不是下面的五种之一,就会调用get_nsproxy对old_ns递减引用计数,然后直接返回0*/
	if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
				  CLONE_NEWPID | CLONE_NEWNET)))) {
		get_nsproxy(old_ns);
		return 0;
	}
	/*当前进程是否有超级用户的权限*/
	if (!ns_capable(user_ns, CAP_SYS_ADMIN))
		return -EPERM;
 
	/*
	 * CLONE_NEWIPC must detach from the undolist: after switching
	 * to a new ipc namespace, the semaphore arrays from the old
	 * namespace are unreachable.  In clone parlance, CLONE_SYSVSEM
	 * means share undolist with parent, so we must forbid using
	 * it along with CLONE_NEWIPC.
	   对CLONE_NEWIPC进行特殊的判断,*/
	if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) ==
		(CLONE_NEWIPC | CLONE_SYSVSEM)) 
		return -EINVAL;
	/*为进程创建新的namespace*/
	new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs);
	if (IS_ERR(new_ns))
		return  PTR_ERR(new_ns);
 
	tsk->nsproxy = new_ns;
	return 0;
}
3.4 create_new_namespaces函数

create_new_namespaces创建新的namespace

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
static struct nsproxy *create_new_namespaces(unsigned long flags,
		struct task_struct *tsk, struct user_namespace *user_ns,
		struct fs_struct *new_fs)
{
	struct nsproxy *new_nsp;
	int err;
	/*为新的nsproxy分配内存空间,并对其引用计数设置为初始1*/
	new_nsp = create_nsproxy();
	if (!new_nsp)
		return ERR_PTR(-ENOMEM);
	/*如果Namespace中的各个标志位进行了设置,则会调用相应的namespace进行创建*/
	new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);
	if (IS_ERR(new_nsp->mnt_ns)) {
		err = PTR_ERR(new_nsp->mnt_ns);
		goto out_ns;
	}
 
	new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
	if (IS_ERR(new_nsp->uts_ns)) {
		err = PTR_ERR(new_nsp->uts_ns);
		goto out_uts;
	}
 
	new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);
	if (IS_ERR(new_nsp->ipc_ns)) {
		err = PTR_ERR(new_nsp->ipc_ns);
		goto out_ipc;
	}
 
	new_nsp->pid_ns_for_children =
		copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);
	if (IS_ERR(new_nsp->pid_ns_for_children)) {
		err = PTR_ERR(new_nsp->pid_ns_for_children);
		goto out_pid;
	}
 
	new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
	if (IS_ERR(new_nsp->net_ns)) {
		err = PTR_ERR(new_nsp->net_ns);
		goto out_net;
	}
 
	return new_nsp;
 
out_net:
	if (new_nsp->pid_ns_for_children)
		put_pid_ns(new_nsp->pid_ns_for_children);
out_pid:
	if (new_nsp->ipc_ns)
		put_ipc_ns(new_nsp->ipc_ns);
out_ipc:
	if (new_nsp->uts_ns)
		put_uts_ns(new_nsp->uts_ns);
out_uts:
	if (new_nsp->mnt_ns)
		put_mnt_ns(new_nsp->mnt_ns);
out_ns:
	kmem_cache_free(nsproxy_cachep, new_nsp);
	return ERR_PTR(err);
}
3.4.1 create_nsproxy函数
1
2
3
4
5
6
7
8
9
static inline struct nsproxy *create_nsproxy(void)
{
	struct nsproxy *nsproxy;
 
	nsproxy = kmem_cache_alloc(nsproxy_cachep, GFP_KERNEL);
	if (nsproxy)
		atomic_set(&nsproxy->count, 1);
	return nsproxy;
}

例子1:namespace pid的例子

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
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sched.h>
#include <string.h>
 
static int fork_child(void *arg)
{
	int a = (int)arg;
	int i;
	pid_t pid;
	char *cmd  = "ps -el;
	printf("In the container, my pid is: %d\n", getpid());
	/*ps命令是解析procfs的内容得到结果的,而procfs根目录的进程pid目录是基于mount当时的pid namespace的,这个在procfs的get_sb回调中体现的。因此只需要重新mount一下proc, mount -t proc proc /proc*/
	mount("proc", "/proc", "proc", 0, "");
	for (i = 0; i <a; i++) {
		pid = fork();
		if (pid <0)
			return pid;
		else if (pid)
			printf("pid of my child is %d\n", pid);
		else if (pid == 0) {
			sleep(30);
			exit(0);
		}
	}
	execl("/bin/bash", "/bin/bash","-c",cmd, NULL);
	return 0;
}
int main(int argc, char *argv[])
{
	int cpid;
	void *childstack, *stack;
	int flags;
	int ret = 0;
	int stacksize = getpagesize() * 4;
	if (argc != 2) {
		fprintf(stderr, "Wrong usage.\n");
		return -1;
	}
	stack = malloc(stacksize);
	if(stack == NULL)
	{
		return -1;
	}
	printf("Out of the container, my pid is: %d\n", getpid());
	childstack = stack + stacksize;
	flags = CLONE_NEWPID | CLONE_NEWNS;
	cpid = clone(fork_child, childstack, flags, (void *)atoi(argv[1]));
	printf("cpid: %d\n", cpid);
	if (cpid <0) {
		perror("clone");
		ret = -1;
		goto out;
	}
	fprintf(stderr, "Parent sleeping 20 seconds\n");
	sleep(20);
	ret = 0;
out:
	free(stack);
	return ret;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@Ubuntu:~/c_program# ./namespace 7 
Out of the container, my pid is: 8684
cpid: 8685
Parent sleeping 20 seconds
In the container, my pid is: 1
pid of my child is 2
pid of my child is 3
pid of my child is 4
pid of my child is 5
pid of my child is 6
pid of my child is 7
pid of my child is 8
F S  UID  PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
4 R    0    1    0  0  80  0 -  1085 -      pts/0    00:00:00 ps
1 S    0    2    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace
1 S    0    3    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace
1 S    0    4    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace
1 S    0    5    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace
1 S    0    6    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace
1 S    0    7    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace
1 S    0    8    1  0  80  0 -  458 hrtime pts/0    00:00:00 namespace

例子2:UTS的例子

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
#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
						} while (0)
 
static int              /* Start function for cloned child */
childFunc(void *arg)
{
	struct utsname uts;
	/* Change hostname in UTS namespace of child */
	if (sethostname(arg, strlen(arg)) == -1)
		errExit("sethostname");
	/* Retrieve and display hostname */
	if (uname(&uts) == -1)
		errExit("uname");
	printf("uts.nodename in child:  %s\n", uts.nodename);
	/* Keep the namespace open for a while, by sleeping.
	 *              This allows some experimentation--for example, another
	 *                process might join the namespace. */
	sleep(200);
	return 0;          /* Child terminates now */
}
#define STACK_SIZE (1024 * 1024)    /* Stack size for cloned child */
 
int
main(int argc, char *argv[])
{
	char *stack;            /* Start of stack buffer */
	char *stackTop;                /* End of stack buffer */
	pid_t pid;
	struct utsname uts;
	if (argc < 2) {
		fprintf(stderr, "Usage: %s <child-hostname>\n", argv[0]);
		exit(EXIT_SUCCESS);
	}
	/* Allocate stack for child */
	stack = malloc(STACK_SIZE);
	if (stack == NULL)
		errExit("malloc");
	stackTop = stack + STACK_SIZE;  /* Assume stack grows downward */
	/* Create child that has its own UTS namespace;
	 *              child commences execution in childFunc() */
	pid = clone(childFunc, stackTop, CLONE_NEWUTS | SIGCHLD, argv[1]);
	if (pid == -1)
		errExit("clone");
	printf("clone() returned %ld\n", (long) pid);
	/* Parent falls through to here */
	sleep(1);          /* Give child time to change its hostname */
 
	/* Display hostname in parent's UTS namespace. This will be
	 *              different from hostname in child's UTS namespace. */
 
	if (uname(&uts) == -1)
		errExit("uname");
	printf("uts.nodename in parent: %s\n", uts.nodename);
	if (waitpid(pid, NULL, 0) == -1)    /* Wait for child */
		errExit("waitpid");
	printf("child has terminated\n");
	exit(EXIT_SUCCESS);
}
1
2
3
4
root@ubuntu:~/c_program# ./namespace_1 test
clone() returned 4101
uts.nodename in child:  test
uts.nodename in parent: ubuntu

Linux Namespaces机制

http://www.cnblogs.com/lisperl/archive/2012/05/03/2480316.html

Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于特定的Namespace。每个Namespace里面的资源对其他Namespace都是透明的。要创建新的Namespace,只需要在调用clone时指定相应的flag。Linux Namespaces机制为实现基于容器的虚拟化技术提供了很好的基础,LXC(Linux containers)就是利用这一特性实现了资源的隔离。不同container内的进程属于不同的Namespace,彼此透明,互不干扰。下面我们就从clone系统调用的flag出发,来介绍各个Namespace。

CLONE_NEWPID

当调用clone时,设定了CLONE_NEWPID,就会创建一个新的PID Namespace,clone出来的新进程将成为Namespace里的第一个进程。一个PID Namespace为进程提供了一个独立的PID环境,PID Namespace内的PID将从1开始,在Namespace内调用fork,vfork或clone都将产生一个在该Namespace内独立的PID。新创建的Namespace里的第一个进程在该Namespace内的PID将为1,就像一个独立的系统里的init进程一样。该Namespace内的孤儿进程都将以该进程为父进程,当该进程被结束时,该Namespace内所有的进程都会被结束。PID Namespace是层次性,新创建的Namespace将会是创建该Namespace的进程属于的Namespace的子Namespace。子Namespace中的进程对于父Namespace是可见的,一个进程将拥有不止一个PID,而是在所在的Namespace以及所有直系祖先Namespace中都将有一个PID。系统启动时,内核将创建一个默认的PID Namespace,该Namespace是所有以后创建的Namespace的祖先,因此系统所有的进程在该Namespace都是可见的。

CLONE_NEWIPC

当调用clone时,设定了CLONE_NEWIPC,就会创建一个新的IPC Namespace,clone出来的进程将成为Namespace里的第一个进程。一个IPC Namespace有一组System V IPC objects 标识符构成,这标识符有IPC相关的系统调用创建。在一个IPC Namespace里面创建的IPC object对该Namespace内的所有进程可见,但是对其他Namespace不可见,这样就使得不同Namespace之间的进程不能直接通信,就像是在不同的系统里一样。当一个IPC Namespace被销毁,该Namespace内的所有IPC object会被内核自动销毁。

PID Namespace和IPC Namespace可以组合起来一起使用,只需在调用clone时,同时指定CLONE_NEWPID和CLONE_NEWIPC,这样新创建的Namespace既是一个独立的PID空间又是一个独立的IPC空间。不同Namespace的进程彼此不可见,也不能互相通信,这样就实现了进程间的隔离。

CLONE_NEWNS

当调用clone时,设定了CLONE_NEWNS,就会创建一个新的mount Namespace。每个进程都存在于一个mount Namespace里面,mount Namespace为进程提供了一个文件层次视图。如果不设定这个flag,子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程。如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。该flag配合pivot_root系统调用,可以为进程创建一个独立的目录空间。

CLONE_NEWNET

当调用clone时,设定了CLONE_NEWNET,就会创建一个新的Network Namespace。一个Network Namespace为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPv4和IPv6协议栈,IP路由表,防火墙规则,sockets等等。一个Network Namespace提供了一份独立的网络环境,就跟一个独立的系统一样。一个物理设备只能存在于一个Network Namespace中,可以从一个Namespace移动另一个Namespace中。虚拟网络设备(virtual network device)提供了一种类似管道的抽象,可以在不同的Namespace之间建立隧道。利用虚拟化网络设备,可以建立到其他Namespace中的物理设备的桥接。当一个Network Namespace被销毁时,物理设备会被自动移回init Network Namespace,即系统最开始的Namespace。

CLONE_NEWUTS

当调用clone时,设定了CLONE_NEWUTS,就会创建一个新的UTS Namespace。一个UTS Namespace就是一组被uname返回的标识符。新的UTS Namespace中的标识符通过复制调用进程所属的Namespace的标识符来初始化。Clone出来的进程可以通过相关系统调用改变这些标识符,比如调用sethostname来改变该Namespace的hostname。这一改变对该Namespace内的所有进程可见。CLONE_NEWUTS和CLONE_NEWNET一起使用,可以虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机一样。

以上所有clone flag都可以一起使用,为进程提供了一个独立的运行环境。LXC正是通过在clone时设定这些flag,为进程创建一个有独立PID,IPC,FS,Network,UTS空间的container。一个container就是一个虚拟的运行环境,对container里的进程是透明的,它会以为自己是直接在一个系统上运行的。

一个container就像传统虚拟化技术里面的一台安装了OS的虚拟机,但是开销更小,部署更为便捷。

内存分配的原理--molloc/brk/mmap

http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/

内存分配的原理__进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

如何查看进程发生缺页中断的次数?

ps -o majflt,minflt -C program命令查看。

majflt代表major fault,中文名叫大错误,minflt代表minor fault,中文名叫小错误。这两个数值表示一个进程自启动以来所发生的缺页中断的次数。

发生缺页中断后,执行了那些操作?

当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:

1、检查要访问的虚拟地址是否合法

2、查找/分配一个物理页

3、填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)

4、建立映射关系(虚拟地址到物理地址)

重新执行发生缺页中断的那条指令

如果第3步,需要读取磁盘,那么这次缺页中断就是majflt,否则就是minflt。

内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

下面以一个例子来说明内存分配的原理:

情况一、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

1、进程启动的时候,其(虚拟)内存空间的初始布局如图1-(1)所示。

其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。

_edata指针(glibc里面定义)指向数据段的最高地址。

2、进程调用A=malloc(30K)以后,内存空间如图1-(2):

malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。

你可能会问:只要把_edata+30K就完成内存分配了?

事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。

3、进程调用B=malloc(40K)以后,内存空间如图1-(3)。

情况二、malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

4、进程调用C=malloc(200K)以后,内存空间如图2-(4):

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。

这样子做主要是因为::
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。

5、进程调用D=malloc(100K)以后,内存空间如图2-(5);

6、进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放如图2-(6)。

7、进程调用free(B)以后,如图3-(7)所示:

B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?

当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。

8、进程调用free(D)以后,如图3-(8)所示:

B和D连接起来,变成一块140K的空闲内存。

9、默认情况下:

当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图3-(9)所示。


mmap 样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<sys/mman.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>

int main()
{
	int i,j,k,l;
	char *mp;
	mp = (char*)mmap(NULL, 1000000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
	*mp = 'A';
	munmap(mp, 1000000);
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<sys/mman.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>

int main()
{
	int i,j,k,l;
	int fd;
	char *mp;

	fd = open("/tmp/mmap", O_CREAT|O_RDWR, 00777);
	lseek(fd, 100, SEEK_SET);
	write(fd, "", 1);
	mp = malloc(1000000);
	mp = (char*)mmap(NULL, 100, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
	i = *mp;
	*mp = 'A';
	munmap(mp, 100);
	return 0;
}

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