kk Blog —— 通用基础

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

监控skb释放

skb_probe.c

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

#include <linux/net.h>
#include <linux/socket.h>
#include <linux/sockios.h>
#include <linux/in.h>
#include <linux/inet.h>
#include <linux/inetdevice.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>

#include <net/snmp.h>
#include <net/ip.h>
#include <net/protocol.h>
#include <net/route.h>
#include <linux/skbuff.h>
#include <net/sock.h>
#include <net/arp.h>
#include <net/icmp.h>
#include <net/raw.h>
#include <net/checksum.h>
#include <linux/netfilter_ipv4.h>
#include <net/xfrm.h>
#include <linux/mroute.h>
#include <linux/netlink.h>

int count = 0;

struct ctl_table_header *ctl_header = NULL;
static struct ctl_table debug_table[] = { 
	{
		.procname       = "pr_count",
		.data           = &count,
		.maxlen         = sizeof(count),
		.mode           = 0644,
		.proc_handler   = &proc_dointvec, },
	{ },
};

static struct ctl_table ipv4_dir_table[] = {
	{
		.procname    = "ipv4",
		.mode        = 0555,
		.child       = debug_table, },
	{ },
};

static ctl_table net_dir_table[] = {
	{ 
		.procname    = "net",
		.mode        = 0555,
		.child        = ipv4_dir_table, },
	{ },
};

int dump_stack_skb(void)
{
	if (count > 0) {
		dump_stack();
		count--;
	}
	return 0;
}

/*
// ip_rcv call skb_orphan, skb_orphan will reset skb->destructor
int j_ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	skb->destructor = (void*)dump_stack_skb;
	jprobe_return();
	return 0;
}

static struct jprobe jp_ip_rcv = {
	.entry = j_ip_rcv,
	.kp = {
		.symbol_name  = "ip_rcv",
	}
};
*/

int j_ip_rcv_finish(struct sk_buff *skb)
{
	skb->destructor = (void*)dump_stack_skb;
	jprobe_return();
	return 0;
}

static struct jprobe jp_ip_rcv_finish = {
	.entry = j_ip_rcv_finish,
	.kp = {
		.symbol_name  = "ip_rcv_finish",
	}
};


static int __init kprobe_init(void)
{
	int ret;
	ctl_header = register_sysctl_table(net_dir_table);
	if(!ctl_header){
		printk(KERN_ERR"SYNPROXY: sp_sysctl_init() calls failed.");
		return -1;
	}

//    ret = register_jprobe(&jp_ip_rcv);
	ret = register_jprobe(&jp_ip_rcv_finish);
	if (ret < 0) {
		unregister_sysctl_table(ctl_header);
		printk(KERN_INFO "register_jprobe failed, returned %d\n", ret);
		return -1;
	}
//    printk(KERN_INFO "Planted jprobe at %p, handler addr %p\n", jp_ip_rcv.kp.addr, jp_ip_rcv.entry);
	printk(KERN_INFO "Planted jprobe at %p, handler addr %p\n", jp_ip_rcv_finish.kp.addr, jp_ip_rcv_finish.entry);
	return 0;
}

static void __exit kprobe_exit(void)
{
	if (ctl_header)
		unregister_sysctl_table(ctl_header);

//    unregister_jprobe(&jp_ip_rcv);
//    printk(KERN_INFO "kprobe at %p unregistered\n", jp_ip_rcv.kp.addr);
	unregister_jprobe(&jp_ip_rcv_finish);
	printk(KERN_INFO "kprobe at %p unregistered\n", jp_ip_rcv_finish.kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

Makefile

1
2
3
4
5
6
7
8
9
10
11
obj-m := skb_probe.o

KDIR:=/lib/modules/`uname -r`/build
PWD=$(shell pwd)

KBUILD_FLAGS += -w

all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	make -C $(KDIR) M=$(PWD) clean

运行

打印10次释放

1
echo 10 > /proc/sys/net/ipv4/pr_count

error, forbidden warning

编译时出现类似的情况:把Warning当做Error,当没有加-Werror选项

1
2
3
4
5
6
7
8
  CC [M]  /home/kk/udp_probe/udp_probe.o
/home/kk/udp_probe/udp_probe.c: In function 'kp_init':
/home/kk/udp_probe/udp_probe.c:36:18: warning: assignment makes pointer from integer without a cast [enabled by default]
error, forbidden warning: udp_probe.c:36
make[2]: *** [/home/kk/udp_probe/udp_probe.o] 错误 1
make[1]: *** [_module_/home/kk/udp_probe] 错误 2
make[1]:正在离开目录 `/media/000617990000DB90/403a/source/kernel'
make: *** [all] 错误 2

修改 scripts/gcc-wrapper.py 去掉 interpret_warning 函数中的如下部分

1
2
3
4
5
6
7
	# If there is a warning, remove any object if it exists.
	if ofile:
		try:
			os.remove(ofile)
		except OSError:
			pass
	sys.exit(1)

若直接加 -w gcc选项,则会直接不显示Warning

cpuset子系统

http://www.cnblogs.com/lisperl/archive/2012/05/02/2478817.html

cpuset子系统为cgroup 中的任务分配独立 CPU(在多核系统)和内存节点。Cpuset子系统为定义了一个叫cpuset的数据结构来管理cgroup中的任务能够使用的cpu和内存节点。Cpuset定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct cpuset {
	struct cgroup_subsys_state css;
 
	unsigned long flags; /* "unsigned long" so bitops work */
	cpumask_var_t cpus_allowed; /* CPUs allowed to tasks in cpuset */
	nodemask_t mems_allowed; /* Memory Nodes allowed to tasks */
 
	struct cpuset *parent; /* my parent */
 
	struct fmeter fmeter; /* memory_pressure filter */
 
	/* partition number for rebuild_sched_domains() */
	int pn;
 
	/* for custom sched domain */
	int relax_domain_level;
 
	/* used for walking a cpuset heirarchy */
	struct list_head stack_list;
};

其中css字段用于task或cgroup获取cpuset结构。

cpus_allowed和mems_allowed定义了该cpuset包含的cpu和内存节点。

Parent字段用于维持cpuset的树状结构,stack_list则用于遍历cpuset的层次结构。

Pn和relax_domain_level是跟Linux 调度域相关的字段,pn指定了cpuset的调度域的分区号,而relax_domain_level表示进行cpu负载均衡寻找空闲cpu的策略。

除此之外,进程的task_struct结构体里面还有一个cpumask_t cpus_allowed成员,用以存储进程的cpus_allowed信息;一个nodemask_t mems_allowed成员,用于存储进程的mems_allowed信息。

Cpuset子系统的实现是通过在内核代码加入一些hook代码。由于代码比较散,我们逐条分析。

在内核初始化代码(即start_kernel函数)中插入了对cpuset_init调用的代码,这个函数用于cpuset的初始化。

下面我们来看这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __init cpuset_init(void)
{
	int err = 0;
 
	if (!alloc_cpumask_var(&top_cpuset.cpus_allowed, GFP_KERNEL))
		BUG();
 
	cpumask_setall(top_cpuset.cpus_allowed);
	nodes_setall(top_cpuset.mems_allowed);
 
	fmeter_init(&top_cpuset.fmeter);
	set_bit(CS_SCHED_LOAD_BALANCE, &top_cpuset.flags);
	top_cpuset.relax_domain_level = -1;
 
	err = register_filesystem(&cpuset_fs_type);
	if (err < 0)
		return err;
 
	if (!alloc_cpumask_var(&cpus_attach, GFP_KERNEL))
		BUG();
 
	number_of_cpusets = 1;
	return 0;
}

cpumask_setall和nodes_setall将top_cpuset能使用的cpu和内存节点设置成所有节点。紧接着,初始化fmeter,设置top_cpuset的load balance标志。最后注册cpuset文件系统,这个是为了兼容性,因为在cgroups之前就有cpuset了,不过在具体实现时,对cpuset文件系统的操作都被重定向了cgroup文件系统。

除了这些初始化工作,cpuset子系统还在do_basic_setup函数(此函数在kernel_init中被调用)中插入了对cpuset_init_smp的调用代码,用于smp相关的初始化工作。

下面我们看这个函数:

1
2
3
4
5
6
7
8
9
10
11
void __init cpuset_init_smp(void)
{
	cpumask_copy(top_cpuset.cpus_allowed, cpu_active_mask);
	top_cpuset.mems_allowed = node_states[N_HIGH_MEMORY];
 
	hotcpu_notifier(cpuset_track_online_cpus, 0);
	hotplug_memory_notifier(cpuset_track_online_nodes, 10);
 
	cpuset_wq = create_singlethread_workqueue("cpuset");
	BUG_ON(!cpuset_wq);
}

首先,将top_cpuset的cpu和memory节点设置成所有online的节点,之前初始化时还不知道有哪些online节点所以只是简单设成所有,在smp初始化后就可以将其设成所有online节点了。然后加入了两个hook函数,cpuset_track_online_cpus和cpuset_track_online_nodes,这个两个函数将在cpu和memory热插拔时被调用。

cpuset_track_online_cpus函数中调用scan_for_empty_cpusets函数扫描空的cpuset,并将其下的进程移到其非空的parent下,同时更新cpuset的cpus_allowed信息。cpuset_track_online_nodes的处理类似。

那cpuset又是怎么对进程的调度起作用的呢?

这个就跟task_struct中cpu_allowed字段有关了。首先,这个cpu_allowed和进程所属的cpuset的cpus_allowed保持一致;其次,在进程被fork出来的时候,进程继承了父进程的cpuset和cpus_allowed字段;最后,进程被fork出来后,除非指定CLONE_STOPPED标记,都会被调用wake_up_new_task唤醒,在wake_up_new_task中有:

1
2
cpu = select_task_rq(rq, p, SD_BALANCE_FORK, 0);
set_task_cpu(p, cpu);

即为新fork出来的进程选择运行的cpu,而select_task_rq会调用进程所属的调度器的函数,对于普通进程,其调度器是CFS,CFS对应的函数是select_task_rq_fair。在select_task_rq_fair返回选到的cpu后,select_task_rq会对结果和cpu_allowed比较:

1
2
3
if (unlikely(!cpumask_test_cpu(cpu, &p->cpus_allowed) ||
     !cpu_online(cpu)))
cpu = select_fallback_rq(task_cpu(p), p);

这就保证了新fork出来的进程只能在cpu_allowed中的cpu上运行。

对于被wake up的进程来说,在被调度之前,也会调用select_task_rq选择可运行的cpu。

这就保证了进程任何时候都只会在cpu_allowed中的cpu上运行。

最后说一下,如何保证task_struct中的cpus_allowd和进程所属的cpuset中的cpus_allowed一致。首先,在cpu热插拔时,scan_for_empty_cpusets会更新task_struct中的cpus_allowed信息,其次对cpuset下的控制文件写入操作时也会更新task_struct中的cpus_allowed信息,最后当一个进程被attach到其他cpuset时,同样会更新task_struct中的cpus_allowed信息。

在cpuset之前,Linux内核就提供了指定进程可以运行的cpu的方法。通过调用sched_setaffinity可以指定进程可以运行的cpu。Cpuset对其进行了扩展,保证此调用设定的cpu仍然在cpu_allowed的范围内。在sched_setaffinity中,插入了这样两行代码:

1
2
cpuset_cpus_allowed(p, cpus_allowed);
cpumask_and(new_mask, in_mask, cpus_allowed);

其中cpuset_cpus_allowed返回进程对应的cpuset中的cpus_allowed,cpumask_and则将cpus_allowed和调用sched_setaffinity时的参数in_mask相与得出进程新的cpus_allowed。

通过以上代码的嵌入,Linux内核实现了对进程可调度的cpu的控制。下面我们来分析一下cpuset对memory节点的控制。

Linux中内核分配物理页框的函数有6个:alloc_pages,alloc_page,get_free_pages,get_free_page,get_zeroed_page,get_dma_pages,这些函数最终都通过alloc_pages实现,而alloc_pages又通过alloc_pages_nodemask实现,在__alloc_pages_nodemask中,调用get_page_from_freelist从zone list中分配一个page,在get_page_from_freelist中调用cpuset_zone_allowed_softwall判断当前节点是否属于mems_allowed。通过附加这样一个判断,保证进程从mems_allowed中的节点分配内存。

Linux在cpuset出现之前,也提供了mbind, set_mempolicy来限定进程可用的内存节点。Cpuset子系统对其做了扩展,扩展的方法跟扩展sched_setaffinity类似,通过导出cpuset_mems_allowed,返回进程所属的cupset允许的内存节点,对mbind,set_mempolicy的参数进行过滤。

最后让我们来看一下,cpuset子系统最重要的两个控制文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
	.name = "cpus",
	.read = cpuset_common_file_read,
	.write_string = cpuset_write_resmask,
	.max_write_len = (100U + 6 * NR_CPUS),
	.private = FILE_CPULIST,
},
 
{
	.name = "mems",
	.read = cpuset_common_file_read,
	.write_string = cpuset_write_resmask,
	.max_write_len = (100U + 6 * MAX_NUMNODES),
	.private = FILE_MEMLIST,
},

通过cpus文件,我们可以指定进程可以使用的cpu节点,通过mems文件,我们可以指定进程可以使用的memory节点。

这两个文件的读写都是通过cpuset_common_file_read和cpuset_write_resmask实现的,通过private属性区分。

在cpuset_common_file_read中读出可用的cpu或memory节点;在cpuset_write_resmask中则根据文件类型分别调用update_cpumask和update_nodemask更新cpu或memory节点信息。

memory子系统

http://www.cnblogs.com/lisperl/archive/2012/04/28/2474872.html

memory 子系统可以设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告。memory子系统是通过linux的resource counter机制实现的。下面我们就先来看一下resource counter机制。

resource counter是内核为子系统提供的一种资源管理机制。这个机制的实现包括了用于记录资源的数据结构和相关函数。Resource counter定义了一个res_counter的结构体来管理特定资源,定义如下:

1
2
3
4
5
6
7
8
9
struct res_counter {
	unsigned long long usage;
	unsigned long long max_usage;
	unsigned long long limit;
	unsigned long long soft_limit;
	unsigned long long failcnt; /*
	spinlock_t lock;
	struct res_counter *parent;
};

Usage用于记录当前已使用的资源,max_usage用于记录使用过的最大资源量,limit用于设置资源的使用上限,进程组不能使用超过这个限制的资源,soft_limit用于设定一个软上限,进程组使用的资源可以超过这个限制,failcnt用于记录资源分配失败的次数,管理可以根据这个记录,调整上限值。Parent指向父节点,这个变量用于处理层次性的资源管理。

除了这个关键的数据结构,resource counter还定义了一系列相关的函数。下面我们来看几个关键的函数。

1
2
3
4
5
6
7
void res_counter_init(struct res_counter *counter, struct res_counter *parent)
{
	spin_lock_init(&counter->lock);
	counter->limit = RESOURCE_MAX;
	counter->soft_limit = RESOURCE_MAX;
	counter->parent = parent;
}

这个函数用于初始化一个res_counter。

第二个关键的函数是int res_counter_charge(struct res_counter *counter, unsigned long val, struct res_counter **limit_fail_at)。当资源将要被分配的时候,资源就要被记录到相应的res_counter里。这个函数作用就是记录进程组使用的资源。在这个函数中有:

1
2
3
4
5
6
7
8
9
for (c = counter; c != NULL; c = c->parent) {
	spin_lock(&c->lock);
	ret = res_counter_charge_locked(c, val);
	spin_unlock(&c->lock);
	if (ret < 0) {
		*limit_fail_at = c;
		goto undo;
	}
}

在这个循环里,从当前res_counter开始,从下往上逐层增加资源的使用量。我们来看一下res_counter_charge_locked这个函数,这个函数顾名思义就是在加锁的情况下增加使用量。实现如下:

1
2
3
4
5
6
7
8
9
10
11
{
	if (counter->usage + val > counter->limit) {
		counter->failcnt++;
		return -ENOMEM;
	}
 
	counter->usage += val;
	if (counter->usage > counter->max_usage)
		counter->max_usage = counter->usage;
	return 0;
}

首先判断是否已经超过使用上限,如果是的话就增加失败次数,返回相关代码;否则就增加使用量的值,如果这个值已经超过历史最大值,则更新最大值。

第三个关键的函数是void res_counter_uncharge(struct res_counter *counter, unsigned long val)。当资源被归还到系统的时候,要在相应的res_counter减轻相应的使用量。这个函数作用就在于在于此。实现如下:

1
2
3
4
5
for (c = counter; c != NULL; c = c->parent) {
	spin_lock(&c->lock);
	res_counter_uncharge_locked(c, val);
	spin_unlock(&c->lock);
}

从当前counter开始,从下往上逐层减少使用量,其中调用了res_counter_uncharge_locked,这个函数的作用就是在加锁的情况下减少相应的counter的使用量。

有这些数据结构和函数,只需要在内核分配资源的时候,植入相应的charge函数,释放资源时,植入相应的uncharge函数,就能实现对资源的控制了。

介绍完resource counter,我们再来看memory子系统是利用resource counter实现对内存资源的管理的。

memory子系统定义了一个叫mem_cgroup的结构体来管理cgroup相关的内存使用信息,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct mem_cgroup {
	struct cgroup_subsys_state css;
	struct res_counter res;
	struct res_counter memsw;
	struct mem_cgroup_lru_info info;
	spinlock_t reclaim_param_lock;
	int prev_priority;
	int last_scanned_child;
	bool use_hierarchy;
	atomic_t oom_lock;
	atomic_t refcnt;
	unsigned int swappiness;
	int oom_kill_disable;
	bool memsw_is_minimum;
	struct mutex thresholds_lock;
	struct mem_cgroup_thresholds thresholds;
	struct mem_cgroup_thresholds memsw_thresholds;
	struct list_head oom_notify;
	unsigned long  move_charge_at_immigrate;
	struct mem_cgroup_stat_cpu *stat;
};

跟其他子系统一样,mem_cgroup也包含了一个cgroup_subsys_state成员,便于task或cgroup获取mem_cgroup。

mem_cgroup中包含了两个res_counter成员,分别用于管理memory资源和memory+swap资源,如果memsw_is_minimum为true,则res.limit=memsw.limit,即当进程组使用的内存超过memory的限制时,不能通过swap来缓解。

use_hierarchy则用来标记资源控制和记录时是否是层次性的。

oom_kill_disable则表示是否使用oom-killer。

oom_notify指向一个oom notifier event fd链表。

另外memory子系统还定义了一个叫page_cgroup的结构体:

1
2
3
4
5
6
struct page_cgroup {
	unsigned long flags;
	struct mem_cgroup *mem_cgroup;
	struct page *page;
	struct list_head lru; /* per cgroup LRU list */
};

此结构体可以看作是mem_map的一个扩展,每个page_cgroup都和所有的page关联,而其中的mem_cgroup成员,则将page与特定的mem_cgroup关联起来。

我们知道在linux系统中,page结构体是用来管理物理页框的,一个物理页框对应一个page结构体,而每个进程中的task_struct中都有一个mm_struct来管理进程的内存信息。每个mm_struct知道它属于的进程,进而知道所属的mem_cgroup,而每个page都知道它属于的page_cgroup,进而也知道所属的mem_cgroup,而内存使用量的计算是按cgroup为单位的,这样以来,内存资源的管理就可以实现了。

memory子系统既然是通过resource counter实现的,那肯定会在内存分配给进程时进行charge操作的。下面我们就来看一下这些charge操作:

1.page fault发生时,有两种情况内核需要给进程分配新的页框。一种是进程请求调页(demand paging),另一种是copy on write。内核在handle_pte_fault中进行处理。其中,do_linear_fault处理pte不存在且页面线性映射了文件的情况,do_anonymous_page处理pte不存在且页面没有映射文件的情况,do_nonlinear_fault处理pte存在且页面非线性映射文件的情况,do_wp_page则处理copy on write的情况。其中do_linear_fault和do_nonlinear_fault都会调用do_fault来处理。Memory子系统则do_fault、do_anonymous_page、do_wp_page植入mem_cgroup_newpage_charge来进行charge操作。

2.内核在handle_pte_fault中进行处理时,还有一种情况是pte存在且页又没有映射文件。这种情况说明页面之前在内存中,但是后面被换出到swap空间了。内核用do_swap_page函数处理这种情况,memory子系统在do_swap_page加入了mem_cgroup_try_charge_swapin函数进行charge。mem_cgroup_try_charge_swapin是处理页面换入时的charge的,当执行swapoff系统调用(关掉swap空间),内核也会执行页面换入操作,因此mem_cgroup_try_charge_swapin也被植入到了相应的函数中。

3.当内核将page加入到page cache中时,也需要进行charge操作,mem_cgroup_cache_charge函数正是处理这种情况,它被植入到系统处理page cache的add_to_page_cache_locked函数中。

4.最后mem_cgroup_prepare_migration是用于处理内存迁移中的charge操作。

除了charge操作,memory子系统还需要处理相应的uncharge操作。下面我们来看一下uncharge操作:

1.mem_cgroup_uncharge_page用于当匿名页完全unmaped的时候。但是如果该page是swap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。

2.mem_cgroup_uncharge_cache_page用于page cache从radix-tree删除的时候。但是如果该page是swap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。

3.mem_cgroup_uncharge_swapcache用于swap cache从radix-tree删除的时候。Charge的资源会被算到swap_cgroup,如果mem+swap controller被禁用了,就不需要这样做了。

4.mem_cgroup_uncharge_swap用于swap_entry的引用数减到0的时候。这个函数主要在mem+swap controller可用的情况下使用的。

5.mem_cgroup_end_migration用于内存迁移结束时相关的uncharge操作。

Charge函数最终都是通过调用mem_cgroup_try_charge来实现的。在mem_cgroup_try_charge函数中,调用res_counter_charge(&mem->res, csize, &fail_res)对memory进行charge,调用res_counter_charge(&mem->memsw, csize, &fail_res)对memory+swap进行charge。

Uncharge函数最终都是通过调用do_uncharge来实现的。在do_uncharge中,分别调用res_counter_uncharge(&mem->res,PAGE_SIZE)和res_counter_uncharge(&mem->memsw, PAGE_SIZE)来uncharge memory和memory+swap。

跟其他子系统一样,memory子系统也实现了一个cgroup_subsys。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct cgroup_subsys mem_cgroup_subsys = {
	.name = "memory",
	.subsys_id = mem_cgroup_subsys_id,
	.create = mem_cgroup_create,
	.pre_destroy = mem_cgroup_pre_destroy,
	.destroy = mem_cgroup_destroy,
	.populate = mem_cgroup_populate,
	.can_attach = mem_cgroup_can_attach,
	.cancel_attach = mem_cgroup_cancel_attach,
	.attach = mem_cgroup_move_task,
	.early_init = 0,
	.use_id = 1,
};

Memory子系统中重要的文件有

1
2
3
4
5
6
7
memsw.limit_in_bytes
{
	.name = "memsw.limit_in_bytes",
	.private = MEMFILE_PRIVATE(_MEMSWAP, RES_LIMIT),
	.write_string = mem_cgroup_write,
	.read_u64 = mem_cgroup_read,
},

这个文件用于设定memory+swap上限值。

Limit_in_bytes

1
2
3
4
5
6
{
	.name = "limit_in_bytes",
	.private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
	.write_string = mem_cgroup_write,
	.read_u64 = mem_cgroup_read,
},

这个文件用于设定memory上限值。

ns子系统

http://www.cnblogs.com/lisperl/archive/2012/04/26/2471776.html

ns子系统是一个比较特殊的子系统。特殊在哪儿呢,首先ns子系统没有自己的控制文件,其次ns子系统没有属于自己的状态信息,这点从ns子系统的ns_cgroup的定义可以看出:

1
2
3
struct ns_cgroup {
	struct cgroup_subsys_state css;
};

它只有一个cgroup_subsys_state成员。

最后ns子系统的实现也比较简单,只是提供了一个ns_cgroup_clone函数,在copy_process和unshare_nsproxy_namespaces被调用。而ns_cgroup_clone函数本身的实现也很简单,只是在当前的cgroup下创建了一个子cgroup,该子cgroup完全clone了当前cgroup的信息,然后将当前的进程移到新建立的cgroup中。

这样看来,好像ns子系统没什么意义,其实不然。要想了解ns子系统的意义,就要分析一下ns_cgroup_clone被调用的时机了。我们来看copy_process中的代码:

1
2
3
4
5
if (current->nsproxy != p->nsproxy) {
	retval = ns_cgroup_clone(p, pid);
	if (retval)
		goto bad_fork_free_pid;
}

copy_process是在do_fork中被调用的,作用在于为子进程复制父进程的相关信息。这段意思就是当前进程(即父进程)和子进程的命名空间不同时,调用ns_cgroup_clone。这样以来,ns子系统的作用就清楚了,ns子系统实际上是提供了一种同命名空间的进程聚类的机制。具有相同命名空间的进程会在相同cgroup中。

那什么时候,父进程fork出的子进程会拥有不同的命名空间呢,这就设计到了Linux的命名空间的机制了,在这里就不详细讲了。简单说来就是,在调用fork时,加入了特殊flag(比如NEWPID,NEWNS)时,内核会为子进程创建不同的命令空间。

除了这种情况外,ns_cgroup_clone在unshare_nsproxy_namespaces用到了。unshare_nsproxy_namespaces函数被sys_unshare调用,实际上是对unshare系统调用的实现。当指定相应标记时,unshare系统调用会为调用的进程创建不同的命名空间,因此调用ns_cgroup_clone为其创建新的cgroup。