cgroup 介绍(1)

January 14th, 2015 by JasonLe's Tech 1,393 views

cgroups (abbreviated from control groups) is a Linux kernel feature that limits, accounts for and isolates the resource usage (CPU, memory, disk I/O, network, etc.) of a collection of processes.

cgroup 在内核的发展是从从2007年 2.6.24内核引入的机制。经过这些年的发展,已经发展成为docker /LXC 中间力量

cgroup定义,很多中文资料已经说明非常详细了,我这里就过多赘述。

值得注意的是,cgroup最初是依靠sysfs作为与用户的控制接口,因为会存在很多hierarchy。

vfs在cgroup这里受到一些滥用,因此cgroup也积累的很多的问题,在vfs中可能会出现重名死锁。
在3.14的版本后,也就是2013之后,kernel对cgroup进行了巨大的修改。很多的数据结构都有了巨大的变化。其中提出来一种 Unified hierarchy 。

缺点[1][2]:

1)虽然这种树型结构可以带来一定的灵活性,但是在实际应用中可能存在一些问题,比如每个子系统在一个hierarchy只能有一个实例,显然freezer也只能有一个实例,freezer不得不移动去控制其他进程。
2)所有子系统绑定在hierarchy中,一旦hierarchy与具体pid绑定,控制granularity不好把握。
3)因为是树型结构,所以树的深度是无限的,这就使得在一个有限的资源中,子hierarchy可能会分配出有限资源外的资源。也使得管理更加复杂。
4)对多hierarchy支持限制了cgroup的使用,因为我们可以知道我们可以有12个hierarchy绑定12个子系统,也可以只有一个hierarhy绑定12个子统统。

在kernel 3.16中开发了Unified hierarchy。它的目的是通过使用其他的结构,同时解决了上述缺点,保持足够的灵活性,对于大多数用例

这个特性目前没有在本文中讨论。

简单提一点就是:这种新的unified hierarchy不允许remount/rename

——————-

由于从3.14后cgroup大改,放弃使用sysfs,创建一个kernfshttp://en.wikipedia.org/wiki/Kernfs_%28Linux%29

这种方式说白了就是抽出了sysfs的核心,创建了一种新的文件系统,可以进行复用。但是这个kernfs目前只是由cgroup在使用

https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=2bd59d48ebfb3df41ee56938946ca0dd30887312

这个里面对于3.14之后的更改说的比较清晰。因为使用kernfs,vfs被移除,也就是说super_block dentry, inode 和 bdi 被移除,使用cgroup_mount()直接挂载根由vfs管理的注册结构体转移到 kernfs_ops and kernfs_syscall_ops。

具体的子系统,我这里简单的提一下:

  • blkio — 这个子系统为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等
    等)。
  • cpu — 这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问。说白了就是时间片大小!
  • cpuacct — 这个子系统自动生成 cgroup 中任务所使用的 CPU 报告。
  • cpuset — 这个子系统为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点。
  • devices — 这个子系统可允许或者拒绝 cgroup 中的任务访问设备。
  • freezer — 这个子系统挂起或者恢复 cgroup 中的任务。
  • memory — 这个子系统设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的
    内存资源报告。
  • net_cls — 这个子系统使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程
    序(tc)识别从具体 cgroup 中生成的数据包。
  • ns — 名称空间子系统。

比如我们要创建一个cpu核数和memory大小受限的cgroup:

1)创建一个目录mkdir

2)挂载新的文件系统,如果我们在/sys/fs/cgroup挂载,我们要先挂载一个tmpfs,因为sysfs下不允许创建文件夹,其他允许创建文件夹的文件系统,不需要挂载tmpfs

3)$mount -t cgroup -o cpuset,memory cgroup_cpu_mem  /mnt/cgroup_cpu_mem

4))我们挂载到root_cgroup的文件系统成功

5)

[lzz@localhost cgroup_cpu_mem]$ ls
cgroup.clone_children   cpuset.memory_pressure_enabled   memory.kmem.slabinfo                memory.pressure_level
cgroup.event_control    cpuset.memory_spread_page        memory.kmem.tcp.failcnt             memory.soft_limit_in_bytes
cgroup.procs            cpuset.memory_spread_slab        memory.kmem.tcp.limit_in_bytes      memory.stat
cgroup.sane_behavior    cpuset.mems                      memory.kmem.tcp.max_usage_in_bytes  memory.swappiness
cgroup_test             cpuset.sched_load_balance        memory.kmem.tcp.usage_in_bytes      memory.usage_in_bytes
cpuset.cpu_exclusive    cpuset.sched_relax_domain_level  memory.kmem.usage_in_bytes          memory.use_hierarchy
cpuset.cpus             memory.failcnt                   memory.limit_in_bytes               notify_on_release
cpuset.mem_exclusive    memory.force_empty               memory.max_usage_in_bytes           release_agent
cpuset.mem_hardwall     memory.kmem.failcnt              memory.move_charge_at_immigrate     tasks
cpuset.memory_migrate   memory.kmem.limit_in_bytes       memory.numa_stat
cpuset.memory_pressure  memory.kmem.max_usage_in_bytes   memory.oom_control

我们发现在这个root cgroup里面,包括两个系统的相关设置,分别以cpuset/memory开头,上面就是建立一个hierarchy.是一个cpusset与memory的资源集合。

6)在这个hierarchy下面,建立文件夹,就是建立新的cgroup,进入cgroup_test

7)然后这个cgroup_test中的设置都为0

8)具体每个字段含义,可以查看document https://www.kernel.org/doc/Documentation/cgroups/

9)我们简单的限定一下:

$echo 16M  > memory.memory.limit_in_bytes
$echo 1  > cpuset.cpus
$echo 0  > cpus.mems

10)这个cgroup就可以往里面添加进程了,我们可以先添加一个shell终端 echo $$ > tasks ,然后通过这个终端启动的都会添加到该cgroup!

 

 

 

[1] http://lwn.net/Articles/606699/
[2] https://www.kernel.org/doc/Documentation/cgroups/unified-hierarchy.txt

 

Python 快速学习教程

January 7th, 2015 by JasonLe's Tech 1,194 views

» Read more: Python 快速学习教程

内存管理3个层次的关系:物理内存管理(2)

January 4th, 2015 by JasonLe's Tech 1,233 views

综述 http://www.lizhaozhong.info/archives/690

之前我们在编写模块时候,对内核地址空间有一个粗略的了解,最近结合内存管理代码的学习,使得我对内核中内存管理有了更清晰的认识!

首先我们看一下http://www.lizhaozhong.info/archives/1003

这个图对kernel中kmalloc、vmalloc有比较详细的说明。

下面我们阐述一下Linux 物理内存管理

在内核中,内核认为一旦有内核函数申请内存,那么就必须立刻满足该申请内存的请求,并且这个请求一定是正确合理的。相反,对于用户态申请内存的请求,内核总是尽量延后分配物理内存,用户进程总是先获得一个虚拟内存区的使用权,最终通过缺页异常获得一块真正的物理内存。

我们之前通过代码看到,linux将内核分为三个区域DMA,NORMAL,HIGHMEM

我们要明白的是只是针对内核有这三个区域,用户空间没有!

这三个区域与体系结构相关,比如有的DMA所占用的物理内存区域是0-16M,有的是0-4GB。这个我们要结合具体的体系结构来看。

从物理内存布局来说DMA 0~16M     NORMAL  16M~896M    HIGHMEM 大于896MB

 

我们拿IA32举例:

kernel可以直接将低于896MB大小的物理内存(即ZONE_DMA,ZONE_NORMAL)映射到内核地址空间,但超出896MB大小的page frame 并不映射在内核线性地址空间的第4个GB。

1.高端内存不能全部映射到内核空间,也就是说这些物理内存没有对应的线性地址。内核此时可以使用alloc_pages()和alloc_page()来分配高端内存,因为这些函数返回页框描述符的线性地址。

2.内核地址空间的后128MB专门用于映射高端内存,否则,没有线性地址的高端内存不能被内核所访问。这些高端内存的内核映射显然是暂时映射的,否则也只能映射128MB的高端内存。当内核需要访问高端内存时就临时在这个区域进行地址映射,使用完毕之后再用来进行其他高端内存的映射。

The lower 896 MB, from 0xC0000000 to 0xF7FFFFFF, is directly mapped to the kernel physical address space, and the remaining 128 MB, from 0xF8000000 to 0xFFFFFFFF, is used on demand by the kernel to be mapped to high memory. When inuser mode, translations are only effective for the first region, thus protecting the kernel from user programs, but when in kernel mode, translations are effective for both regions, thus giving the kernel an easy way to refer to the buffers of processes—it just uses the process’ own mappings.[1]

由于要进行高端内存的内核映射,因此直接能够映射的物理内存大小只有896MB,该值保存在high_memory中。内核地址空间的线性地址区间如下图所示:

phy_addr

 

 

从这个图,我们可以清晰看出来从high_memory到4GB这段内存区域,系统可以使用vmalloc,也可以临时映射到用户空间的某一段内存。

总的来说高端内存映射有三种方式:

1)因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面 (VMALLOC_START~VMALLOC_END)

2)持久映射:这个空间和其它空间使用同样的页目录表,通过内核页目录表方式达到内存映射

3)固定映射:

这块空间具有如下特点:
(1)每个 CPU 占用一块空间
(2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在map_types.h 中的 km_type 中。

当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现临时映射。

 

http://blog.csdn.net/trochiluses/article/details/9297311

Linux 中Physical Memory 数据结构分析

January 4th, 2015 by JasonLe's Tech 1,589 views

Kernel在在管理内存时将物理内存从逻辑上划分为节点(node),内存管理区(zone),页框(frame page)三级结构。我们都知道frame page是管理内存单元的最小单位,这个frame page在代码中就是struct page。

而node是与cpu数量相关的!默认在NUMA存在多个cpu,则每个cpu都存在一个struct pglist_data 类型的节点。而一个struct pglist_data下又把当前管理的内存区域划分为3部分:这个就是由zone定义的。

zone将内存区域划分为三种类型:1)DMA  2)NORMAL 3)HIGHEM

引入这种node管理方式的根本原因是为了兼容UMA架构的计算机,Kernel对于内存的管理主要存在NUMA/UMA两种形式

关于这两种形式的解释,请查阅wiki:

http://en.wikipedia.org/wiki/Non-uniform_memory_access

http://en.wikipedia.org/wiki/Uniform_memory_access

下面来说明这三种结构体:

1.struct page (include/linux/mm_types.h)

page结构体描述一个物理页框,每个物理页框都关联一个这样的结构体。但是page结构仅用来描述一个页框的属性,它不包含该页框中的任何数据。我们知道一个物理页框大小通常是4096 byte (4KB) 而sizeof(struct page)远远小于4096byte,其他空间用于存储数据。

内核在定义page结构时使用了许多union,这样做的目的是保证page结构尽可能的小。虽然每个page结构体占很少内存,但是由于实际系统中页框总数量巨大,因此所有页框对应的page结构所占用的内存量也很庞大。

2.struct zone(include/linux/mmzone.h)

http://lxr.free-electrons.com/source/include/linux/mmzone.h

内核将内存划分为几个连续的区域,每个区域页框都是连续的。kernel使用枚举zone_type方式定义每个内存区域。

enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
	__MAX_NR_ZONES
};

struct zone -> struct per_cpu_pageset -> struct per_cpu_pages 可以记录每个内存管理区中page使用情况。

 内存管理区是一个逻辑上的概念,它的存在是因为计算机中硬件访问物理内存时有一些限制。因此,每个内存管理区的实际分布是与体系结构相关的。

下面来解释这三个主要的区域:

ZONE_DMA:某些设备通过DMA方式访问内存时,不能访问到所有的物理内存,此时只能为它们单独划分一块内存管理区。ZONE_DMA的范围根据体系结构而改变。

ZONE_NORMAL:这个区域包含的都是能够正常映射的页框。通过源码中的定义可以发现,所有的体系架构都包含这个区域。但是并不是每个架构下该区都能对应到实际的物理内存,根据上面所述,某些架构下ZONE_DMA32会占据整个4G的物理内存,因此该区域为空。

ZONE_HIGHMEM:这个区域代表超出内核空间大小的物理内存,这部分内存也被成为高端内存(与之对应ZONE_DMA和ZONE_NORMAL成为低端内存)。在32位的x86系统中,高端内存即为大于896MB的物理内存。而在64位的系统中,高端内存总为空。

__MAX_NR_ZONES:它用来标记内存管理区的数量。在UMA架构下,该常量为1.

3.struct pglist_data

下面我们来看这个struct pglist_data

节点这个概念是由于NUMA(非一致内存访问)模型而诞生的,该模型只存在于多处理器计算机中。NUMA根据CPU数量将整个物理内存分为几个大块,每块内存即为每个CPU的的本地内存。这样的划分使每个CPU都能以较快的速度访问本地内存,当然每个CPU也可以访问其他CPU的内存只不过速度比较慢而已。上述的每块物理内存对应一个pg_data_t数据结构,每块物理内存即为一个节点,所以的结点形成一个双链表。

NUMA_Memory

struct bootmem_data;
typedef struct pglist_data {
        struct zone node_zones[MAX_NR_ZONES];
        struct zonelist node_zonelists[MAX_ZONELISTS];
        int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
        struct page *node_mem_map;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
        struct page_cgroup *node_page_cgroup;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
        struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
        spinlock_t node_size_lock;
#endif
        unsigned long node_start_pfn;
        unsigned long node_present_pages; /* total number of physical pages */
        unsigned long node_spanned_pages; /* total size of physical page
                                             range, including holes */
        int node_id;
        wait_queue_head_t kswapd_wait;
        struct task_struct *kswapd;
        int kswapd_max_order;
} pg_data_t;

这个结构体里面的 struct page *node_mem_map;是指向page实例数组指针,用于描述节点的所有物理内存页。他包含了节点这个node所有的page!

node_zones:当前节点中内存管理区描述符数组。这个数组的大小使用__MAX_NR_ZONES来定义。

node_zonelists:它是zonelist结构的数组,长度为MAX_ZONELISTS。如果内核未配置NUMA,则长度为1,否则,长度为2。该数组中0号元素指定了备用的内存管理区链表,也就是当前系统中所有的zone。1号元素指定了当前节点中的管理区链表。除非分配内存时指定了GFP_THISNODE标志而采用本地内存节点上的zonelist,一般均采用备用zonelist。

unsigned long node_start_pfn; 上面结构体里的成员变量是该pg_data_t结构体的第一个pfn!三种内存区域都可能是存在的!

understand-html001

上面这个图比较老,那个zone_mem_map结构已经不存在,而是由unsigned long zone_start_pfn 地址来代替,通过这种方式查找出来的pfn是某个内存区域特有的!比如是ZONE_DMA/ZONE_NORMAL/ZONE_HIGHMEM的第一个pfn号码!

总的来说,主要是通过struct pglist_data -> struct zone(node_zones 三个不同的区域)->struct zone 下的unsigned long zone_start_pfn 找到某个特定的page!pfn全kernel唯一!

 

 

https://www.kernel.org/doc/gorman/html/understand/understand005.html

http://lxr.free-electrons.com/source/include/linux/mmzone.h

tools/vm/page-types.c 解析使用

December 9th, 2014 by JasonLe's Tech 1,145 views

page-types.c这个代码位于kernel源码下的tool/vm/page-type.c

我们可以通过使用makefile编译这个程序,通过这个程序我们可以查找每个特定进程的page frame number  记住这个pfn只是一个index而已,我们如果想取得真实的物理地址需要使用pfn*page_size,当然这个是page frame 的物理起始地址。

这个程序是位于用户态,通过查找/proc/pid/pagemap 和 /proc/pid/maps来找到pfn的。

这个程序的功能比较强大,我们使用的仅仅是指定一个$./page-type -p pid

首先mian()中先要解析参数,然后通过parse_number()来获取用户输入的pid,然后我们知道当指定了一个pid,也就意味着我们打开一个正在运行的pid的maps

[root@localhost 13352]# ls
attr       clear_refs       cpuset   fd       limits    mountinfo   ns         oom_score_adj  root       stack   syscall
autogroup  cmdline          cwd      fdinfo   loginuid  mounts      numa_maps  pagemap        sched      stat    task
auxv       comm             environ  gid_map  maps      mountstats  oom_adj    personality    sessionid  statm   uid_map
cgroup     coredump_filter  exe      io       mem       net         oom_score  projid_map     smaps      status  wchan
00400000-00401000 r-xp 00000000 fd:02 4330673                            /home/lzz/mca-ras/src/tools/simple_process/simple_process
00600000-00601000 r--p 00000000 fd:02 4330673                            /home/lzz/mca-ras/src/tools/simple_process/simple_process
00601000-00602000 rw-p 00001000 fd:02 4330673                            /home/lzz/mca-ras/src/tools/simple_process/simple_process
019b8000-019d9000 rw-p 00000000 00:00 0                                  [heap]
33c1a00000-33c1a20000 r-xp 00000000 fd:01 2622088                        /usr/lib64/ld-2.18.so
33c1c1f000-33c1c20000 r--p 0001f000 fd:01 2622088                        /usr/lib64/ld-2.18.so
33c1c20000-33c1c21000 rw-p 00020000 fd:01 2622088                        /usr/lib64/ld-2.18.so
33c1c21000-33c1c22000 rw-p 00000000 00:00 0
33c1e00000-33c1fb4000 r-xp 00000000 fd:01 2622743                        /usr/lib64/libc-2.18.so
33c1fb4000-33c21b3000 ---p 001b4000 fd:01 2622743                        /usr/lib64/libc-2.18.so
33c21b3000-33c21b7000 r--p 001b3000 fd:01 2622743                        /usr/lib64/libc-2.18.so
33c21b7000-33c21b9000 rw-p 001b7000 fd:01 2622743                        /usr/lib64/libc-2.18.so
33c21b9000-33c21be000 rw-p 00000000 00:00 0
7f3fe081a000-7f3fe081d000 rw-p 00000000 00:00 0
7f3fe083c000-7f3fe083e000 rw-p 00000000 00:00 0
7fff87571000-7fff87592000 rw-p 00000000 00:00 0                          [stack]
7fff875fe000-7fff87600000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

我们可以看到这个进程每个段,包括BSS CODE HEAP STACK等。然后哦page-types.c中的parse_pid读取这个表里面的信息。
关于这个表每个列的含义看这个http://stackoverflow.com/questions/1401359/understanding-linux-proc-id-maps

我们主要关注头两行,vm_start vm_end。这时我们要获取虚拟地址的page number:

pg_start[nr_vmas] = vm_start / page_size;
pg_end[nr_vmas] = vm_end / page_size;

至于这个page_size可以通过page_size = getpagesize();获取。这里我们要注意unsigned long的取值范围是0-2^64-1,-1也就是2^64-1

然后我们进入walk_task()函数,因为process可能会存在匿名映射,所以我们需要使用filebacked_path[i]判断文件地址。

进入walk_vma()后。
这时我们需要读取/proc/pid/pagemap,这个我们用常规打不开,所以我们只好通过vm index读取的方式获得pages和pfn的相关信息。

详细我们查看

http://stackoverflow.com/questions/17021214/decode-proc-pid-pagemap-entry
https://www.kernel.org/doc/Documentation/vm/pagemap.txt

关注

* Bits 0-54 page frame number (PFN) if present
* Bit 63 page present
下面就是通过编程的方式获取这个的0-54位。总的来所就是使用位操作:

#define PFN_MASK (((1LL)<<55)-1)  //意味着0-54位都为1
*phy=(buf&PFN_MASK)*page_size+vir%page_size

核心函数:

        for (i = 0; i < pages; i++) {
//          printf("%lx\n",buf[i]);
            pfn = pagemap_pfn(buf[i]);
            if (pfn) {
                printf("%lx", pfn);
                printf("\t0x%lx",pfn*page_size);
                walk_pfn(index + i, pfn, 1, buf[i]);
            }
        }

我们可以看到pfn只是一个index而已,真正的物理地址需要pfn*page_size的方式,后面的各种属性是通过读取/proc/kpageflags获得。结果如下:

/home/lzz/mca-ras/src/tools/simple_process/simple_process
93a12	0x93a12000	referenced,uptodate,lru,active,mmap,private

/home/lzz/mca-ras/src/tools/simple_process/simple_process
54d07	0x54d07000	uptodate,lru,active,mmap,anonymous,swapbacked

/home/lzz/mca-ras/src/tools/simple_process/simple_process
9aa43	0x9aa43000	uptodate,lru,active,mmap,anonymous,swapbacked

[heap]
91f74	0x91f74000	uptodate,lru,active,mmap,anonymous,swapbacked
60fb3	0x60fb3000	uptodate,lru,active,mmap,anonymous,swapbacked
...............

 

我们看到物理地址只是在后面添加了3个0,其实也就是12位,正好是一个4k page的页内offset。