CSDN开源夏令营 - 第七周工作总结
这一周在实现了“set trace-buffer-size”的simple mode(即舍弃Ring Buffer中原有的trace frames)基础之上,试图完成normal mode,因为用户有的时候是在trace过程中发现buffer不够大,此时暂停trace,待扩大buffer空间后继续trace,如果此时再使用simple mode,显然不符合用户的预期了。因此,normal mode需要处理用户原有trace frames的数据搬移,分配一套新的Ring Buffer后,拷贝原有Ring
Buffer的数据过去,然后再把旧的Ring Buffer释放。如果用户把buffer size设小了,则需要考虑舍弃一部分数据,这个策略也要根据用户需求进行设计。
normal mode目前没有实现,这周主要分析下gtp_rb.c的实现,以及KGTP中涉及到的Linux Kernel编程的知识点吧。
1. SMP、NUMA、MPP体系结构介绍和对比
为啥会想着去看这些东西呢?因为KGTP的代码中充斥着per_cpu,这显然跟SMP相关了,然后再一扩展,就引出了SMP、NUMA、MPP三种体系结构了。
这三者的全称分别是,对称多处理器结构 (SMP : Symmetric Multi-Processor) ,非一致存储访问结构 (NUMA : Non-Uniform Memory Access) ,以及海量并行处理结构 (MPP : Massive Parallel Processing) ,下面说一下他们的basic idea。
SMP,所谓对称多处理器结构,是指服务器中多个 CPU 对称工作,无主次或从属关系。所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。SMP 服务器的主要特征是共享,系统中所有资源
(CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。
NUMA,由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA 就是这种努力下的结果之一。NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存
( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。但 NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。
MPP,和 NUMA 不同, MPP 提供了另外一种进行系统扩展的方式,它由多个 SMP 服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务,从用户的角度来看是一个服务器系统。其基本特征是由多个 SMP 服务器 ( 每个 SMP 服务器称节点 ) 通过节点互联网络连接而成,每个节点只访问自己的本地资源 ( 内存、存储等 ) ,是一种完全无共享 (Share Nothing) 结构,因而扩展能力最好,理论上其扩展无限制,目前的技术可实现 512 个节点互联,数千个 CPU 。在 MPP 系统中,每个
SMP 节点也可以运行自己的操作系统、数据库等。但和 NUMA 不同的是,它不存在异地内存访问的问题。换言之,每个节点内的 CPU 不能访问另一个节点的内存。节点之间的信息交互是通过节点互联网络实现的,这个过程一般称为数据重分配 (Data Redistribution) 。但是 MPP 服务器需要一种复杂的机制来调度和平衡各个节点的负载和并行处理过程。
以上信息摘抄于[1],更多信息大家可以移步那里,里面的配图非常直观。
2. Linux和SMP
这里我们重点关注SMP,着眼其在Linux Kernel中是如何被支持,且给开发者提供了哪些数据结构和接口。关于Linux SMP,可以看看这篇短文[2],可以看其英文版[3]。关于Linux是如何支持SMP的,参考[4]。这个ppt是HUST的一位前辈做的,从Linux启动,进程调度,中断三个方面来说明Linux对SMP的支持,简单明了!
好,从数据结构和接口来看,即是per_cpu变量了,头文件包含在2.6的内核中是<linux/percpu.h>,在目前最新的是<asm-generic/percpu.h>,还有<linux/percpu-defs.h>。
先看看SMP的相关宏定义,
#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#endif
可以看到如果有SMP的支持,则相应的section是.data..percpu。
编译期间分配:
DEFINE_PER_CPU(type, name);
避免进程在访问一个per-CPU变量时被切换到另外一个处理器上运行或被其它进程抢占:
get_cpu_var(变量)++;
put_cpu_var(变量);
访问其他处理器的变量副本用这个宏:
per_cpu(变量,int cpu_id);
动态分配与释放:
动态分配per-CPU变量:
void * alloc_percpu(type);
void * __alloc_percpu(size_t size, size_t align); //可以做特定的内存对齐
释放动态分配的per-CPU变量:
free_percpu();
访问动态分配的per-CPU变量的访问通过per_cpu_ptr完成:
per_cpu_ptr(void * per_cpu_var, int cpu_id);
要想阻塞抢占,使用get_cpu()与put_cpu()即可:
int cpu = get_cpu();
ptr = per_cpu_ptr(per_cpu_var, cpu);
put_cpu();
导出per_cpu变量给模块:
EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);
要在模块中访问这样一个变量,应该这样做声明:
DECLARE_PER_CPU(type, name);
3. gtp_rb.c的分析
KGTP自己实现的Ring Buffer,让我们看一看具体的代码实现。
gtp_rb_s是一个结构体,作为指向一个Ring Buffer的指针,
struct gtp_rb_s {
spinlock_t lock;
/* Pointer to the prev frame entry head.
*/
void *prev_frame;
/* When write, this is the next address to be write.
When read, this is the end of read. */
void *w;
/* When alloc memory from rb, record prev value W to PREV_W.
When this memory doesn't need, set W back to PREV_W to release
this memroy. */
void *prev_w;
/* Point to the begin of ring buffer. Read will begin from R. */
void *r;
/* Point to the trace frame entry head of current read. */
void *rp;
/* This the id of rp point to.
0 means rp doesn't point to a trace frame entry.
So it need call gtp_rb_walk first. */
u64 rp_id;
/* The cpu id. */
int cpu;
};
可以看到,用到了自旋锁,一个读指针,一个写指针,同时写还保存了上一次的写指针,可以方便释放内存。
再看相关变量,
static struct gtp_rb_s __percpu *gtp_rb;
#if defined(CONFIG_ARM) && (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,34))
static atomic_t gtp_rb_count;
#else
static atomic64_t gtp_rb_count;
#endif
static unsigned int gtp_rb_page_count;
static atomic_t gtp_rb_discard_page_number;
看到了吧,其中__percpu是一个宏定义,
# define __percpu __attribute__((noderef, address_space(3)))
另外定义了一些全局的计数器。
下面分析核心的函数实现。
(1)初始化Ring Buffer
主要分配各自CPU的Ring Buffer的gtp_rb指针,不实际分配Ring Buffer的内存。
static int
gtp_rb_init(void)
{
int cpu;
gtp_rb = alloc_percpu(struct gtp_rb_s);
if (!gtp_rb)
return -ENOMEM;
for_each_possible_cpu(cpu) {
struct gtp_rb_s *rb
= (struct gtp_rb_s *)per_cpu_ptr(gtp_rb, cpu);
memset(rb, 0, sizeof(struct gtp_rb_s));
rb->lock = __SPIN_LOCK_UNLOCKED(rb->lock);
rb->cpu = cpu;
}
gtp_rb_page_count = 0;
atomic_set(&p_rb_discard_page_number, 0);
return 0;
}
这里重点是:
<linux/percpu.h>中的宏定义以及<mm/percpu.c>的实现,
<linux/cpumask.h>中的宏定义,
也可以关注下,spinlock是如何定义以及atomic_set。
#define alloc_percpu(type) \
(typeof(type) __percpu *)__alloc_percpu(sizeof(type), __alignof__(type))
/**
836 * __alloc_percpu - allocate dynamic percpu area
837 * @size: size of area to allocate in bytes
838 * @align: alignment of area (max PAGE_SIZE)
839 *
840 * Allocate zero-filled percpu area of @size bytes aligned at @align.
841 * Might sleep. Might trigger writeouts.
842 *
843 * CONTEXT:
844 * Does GFP_KERNEL allocation.
845 *
846 * RETURNS:
847 * Percpu pointer to the allocated area on success, NULL on failure.
848 */
849 void __percpu *__alloc_percpu(size_t size, size_t align)
850 {
851 return pcpu_alloc(size, align, false);
852 }
853 EXPORT_SYMBOL_GPL(__alloc_percpu);
#define for_each_possible_cpu(cpu) for_each_cpu((cpu), cpu_possible_mask)
#define for_each_online_cpu(cpu) for_each_cpu((cpu), cpu_online_mask)
#define for_each_present_cpu(cpu) for_each_cpu((cpu), cpu_present_mask)
也可以关注下,spinlock是如何定义以及atomic_set。
(2)释放Ring Buffer
实际上是释放per_cpu变量gtp_rb,即每个CPU都有gtp_rb的一个副本。
static void
gtp_rb_release(void)
{
if (gtp_rb) {
free_percpu(gtp_rb);
gtp_rb = NULL;
}
}
(3)重置Ring Buffer的指针
初始化gtp_rb的读写指针,以及重置各个计数器。
static void
gtp_rb_reset(void)
{
int cpu;
for_each_possible_cpu(cpu) {
struct gtp_rb_s *rb
= (struct gtp_rb_s *)per_cpu_ptr(gtp_rb, cpu);
rb->w = GTP_RB_DATA(rb->w);
rb->r = rb->w;
rb->rp = NULL;
rb->rp_id = 0;
rb->prev_frame = NULL;
}
#if defined(CONFIG_ARM) && (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,34))
atomic_set(&p_rb_count, 0);
#else
atomic64_set(&p_rb_count, 0);
#endif
atomic_set(&p_rb_discard_page_number, 0);
}
这里我们可以看到per_cpu_ptr获取各个CPU变量副本的指针。
(4)计数器原子增
static inline u64
gtp_rb_clock(void)
{
u64 ret;
re_inc:
#if defined(CONFIG_ARM) && (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,34))
ret = (u64)atomic_inc_return(>p_rb_count);
#else
ret = atomic64_inc_return(>p_rb_count);
#endif
if (ret == 0)
goto re_inc;
return ret;
}
有个goto在这里,直到成功,函数再退出。
有个相关判断Ring Buffer是否为空的宏,
#define GTP_RB_PAGE_IS_EMPTY (gtp_rb_page_count == 0)
(5)分配内存页给Ring Buffer
核心部分了,分配页内存。
static int
gtp_rb_page_alloc(int size)
{
int cpu;
for_each_possible_cpu(cpu) {
struct gtp_rb_s *rb
= (struct gtp_rb_s *)per_cpu_ptr(gtp_rb, cpu);
void *last = NULL, *next = NULL;
struct page *page;
int current_size;
gtp_rb_page_count = 0;
current_size = size;
while (1) {
if (current_size > 0)
current_size -= PAGE_SIZE;
else
break;
page = alloc_pages_node(cpu_to_node(cpu),
GFP_KERNEL, 0);
if (!page)
return -1;
gtp_rb_page_count++;
rb->w = GTP_RB_DATA(page_address(page));
GTP_RB_NEXT(rb->w) = next;
if (next)
GTP_RB_PREV(next) = rb->w;
next = rb->w;
if (!last)
last = rb->w;
}
GTP_RB_NEXT(last) = next;
GTP_RB_PREV(next) = last;
rb->r = rb->w;
GTP_RB_NEXT(last) = next;
GTP_RB_PREV(next) = last;
rb->r = rb->w;
if (gtp_rb_page_count < 3)
return -1;
}
return 0;
}
传入的参数size为可分配空间的大小,单位是byte。首先使用for_each_possible_cpu遍历CPU,然后通过per_cpu_ptr得到每个gtp_rb指针,之后使用alloc_pages_node分配PAGE,最后会得到一个双链表Ring Buffer。
让我们重点看alloc_pages_node,
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
313 unsigned int order)
314 {
315 /* Unknown node is current node */
316 if (nid < 0)
317 nid = numa_node_id();
318
319 return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
320 }
再看__alloc_pages,
static inline struct page *
306 __alloc_pages(gfp_t gfp_mask, unsigned int order,
307 struct zonelist *zonelist)
308 {
309 return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
310 }
继续追踪,__alloc_pages_nodemask,
/*
2722 * This is the 'heart' of the zoned buddy allocator.
2723 */
2724 struct page *
2725 __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
2726 struct zonelist *zonelist, nodemask_t *nodemask)
2727 {
2728 enum zone_type high_zoneidx = gfp_zone(gfp_mask);
2729 struct zone *preferred_zone;
2730 struct zoneref *preferred_zoneref;
2731 struct page *page = NULL;
2732 int migratetype = allocflags_to_migratetype(gfp_mask);
2733 unsigned int cpuset_mems_cookie;
2734 int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
2735 int classzone_idx;
2736
2737 gfp_mask &= gfp_allowed_mask;
2738
2739 lockdep_trace_alloc(gfp_mask);
2740
2741 might_sleep_if(gfp_mask & __GFP_WAIT);
2742
2743 if (should_fail_alloc_page(gfp_mask, order))
2744 return NULL;
2745
2746 /*
2747 * Check the zones suitable for the gfp_mask contain at least one
2748 * valid zone. It's possible to have an empty zonelist as a result
2749 * of GFP_THISNODE and a memoryless node
2750 */
2751 if (unlikely(!zonelist->_zonerefs->zone))
2752 return NULL;
2753
2754 retry_cpuset:
2755 cpuset_mems_cookie = read_mems_allowed_begin();
2756
2757 /* The preferred zone is used for statistics later */
2758 preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
2759 nodemask ? : &cpuset_current_mems_allowed,
2760 &preferred_zone);
2761 if (!preferred_zone)
2762 goto out;
2763 classzone_idx = zonelist_zone_idx(preferred_zoneref);
2764
2765 #ifdef CONFIG_CMA
2766 if (allocflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
2767 alloc_flags |= ALLOC_CMA;
2768 #endif
2769 retry:
2770 /* First allocation attempt */
2771 page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
2772 zonelist, high_zoneidx, alloc_flags,
2773 preferred_zone, classzone_idx, migratetype);
2774 if (unlikely(!page)) {
2775 /*
2776 * The first pass makes sure allocations are spread
2777 * fairly within the local node. However, the local
2778 * node might have free pages left after the fairness
2779 * batches are exhausted, and remote zones haven't
2780 * even been considered yet. Try once more without
2781 * fairness, and include remote zones now, before
2782 * entering the slowpath and waking kswapd: prefer
2783 * spilling to a remote zone over swapping locally.
2784 */
2785 if (alloc_flags & ALLOC_FAIR) {
2786 reset_alloc_batches(zonelist, high_zoneidx,
2787 preferred_zone);
2788 alloc_flags &= ~ALLOC_FAIR;
2789 goto retry;
2790 }
2791 /*
2792 * Runtime PM, block IO and its error handling path
2793 * can deadlock because I/O on the device might not
2794 * complete.
2795 */
2796 gfp_mask = memalloc_noio_flags(gfp_mask);
2797 page = __alloc_pages_slowpath(gfp_mask, order,
2798 zonelist, high_zoneidx, nodemask,
2799 preferred_zone, classzone_idx, migratetype);
2800 }
2801
2802 trace_mm_page_alloc(page, order, gfp_mask, migratetype);
2803
2804 out:
2805 /*
2806 * When updating a task's mems_allowed, it is possible to race with
2807 * parallel threads in such a way that an allocation can fail while
2808 * the mask is being updated. If a page allocation is about to fail,
2809 * check if the cpuset changed during allocation and if so, retry.
2810 */
2811 if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
2812 goto retry_cpuset;
2813
2814 return page;
2815 }
2816 EXPORT_SYMBOL(__alloc_pages_nodemask);
唔,需要再深入了解一下buddy system了,关于以上函数,可以参考[6]。
(6)页内存的释放
static void
gtp_rb_page_free(void)
{
int cpu;
for_each_possible_cpu(cpu) {
struct gtp_rb_s *rb
= (struct gtp_rb_s *)per_cpu_ptr(gtp_rb, cpu);
void *need_free = NULL;
int is_first = 1;
for (rb->r = rb->w = GTP_RB_DATA(rb->w);
is_first || rb->w != rb->r;
rb->w = GTP_RB_NEXT(rb->w)) {
if (need_free)
free_page((unsigned long)need_free);
need_free = GTP_RB_HEAD(rb->w);
is_first = 0;
}
if (need_free)
free_page((unsigned long)need_free);
}
gtp_rb_page_count = 0;
}
嗯,跟上面的分配对着看吧。
(7)一些宏定义
/* Following macros is for page of ring buffer. */
#define ADDR_SIZE sizeof(size_t)
#define GTP_RB_HEAD(addr) ((void *)((size_t)(addr) & PAGE_MASK))
#define GTP_RB_DATA(addr) (GTP_RB_HEAD(addr) + ADDR_SIZE)
#define GTP_RB_END(addr) (GTP_RB_HEAD(addr) + PAGE_SIZE - ADDR_SIZE)
#define GTP_RB_PREV(addr) (*(void **)GTP_RB_HEAD(addr))
#define GTP_RB_NEXT(addr) (*(void **)GTP_RB_END(addr))
#define GTP_RB_DATA_MAX (PAGE_SIZE - ADDR_SIZE - ADDR_SIZE - FID_SIZE \
- sizeof(u64))
#define GTP_RB_LOCK(r) spin_lock(&r->lock)
#define GTP_RB_UNLOCK(r) spin_unlock(&r->lock)
#define GTP_RB_LOCK_IRQ(r, flags) spin_lock_irqsave(&r->lock, flags)
#define GTP_RB_UNLOCK_IRQ(r, flags) spin_unlock_irqrestore(&r->lock, flags)
#define GTP_RB_RELEASE(r) (r->w = r->prev_w)
(8)一些utilites
static inline void *
gtp_rb_prev_frame_get(struct gtp_rb_s *rb)
{
return rb->prev_frame;
}
static inline void
gtp_rb_prev_frame_set(struct gtp_rb_s *rb, void *prev_frame)
{
rb->prev_frame = prev_frame;
}
/* Return the max size for next gtp_rb_alloc. */
static inline size_t
gtp_rb_alloc_max(struct gtp_rb_s *rb)
{
return GTP_RB_END(rb->w) - rb->w;
}
(9)页内分配内存
static void *
gtp_rb_alloc(struct gtp_rb_s *rb, size_t size, u64 id)
{
void *ret;
size = FRAME_ALIGN(size);
if (size > GTP_RB_DATA_MAX) {
printk(KERN_WARNING "gtp_rb_alloc: The size %zu is too big"
"for the KGTP ring buffer. "
"The max size that KGTP ring buffer "
"support is %lu (Need sub some size for "
"inside structure).\n", size, GTP_RB_DATA_MAX);
return NULL;
}
rb->prev_w = rb->w;
if (rb->w + size > GTP_RB_END(rb->w)) {
/* Don't have enough size in current page, insert a
FID_PAGE_END and try to get next page. */
if (GTP_RB_END(rb->w) - rb->w >= FID_SIZE)
FID(rb->w) = FID_PAGE_END;
if (GTP_RB_HEAD(GTP_RB_NEXT(rb->w)) == GTP_RB_HEAD(rb->r)) {
if (gtp_circular) {
rb->r = GTP_RB_NEXT(rb->r);
atomic_inc(>p_rb_discard_page_number);
} else
return NULL;
}
rb->w = GTP_RB_NEXT(rb->w);
if (id) {
/* Need insert a FID_PAGE_BEGIN. */
FID(rb->w) = FID_PAGE_BEGIN;
*((u64 *)(rb->w + FID_SIZE)) = id;
rb->w += FRAME_ALIGN(GTP_FRAME_PAGE_BEGIN_SIZE);
}
}
ret = rb->w;
rb->w += size;
return ret;
}
/* GTP_FRAME_SIZE must align with FRAME_ALIGN_SIZE if use GTP_FRAME_SIMPLE. */
#define GTP_FRAME_SIZE 5242880 // 5*1024*1024 = 5MB
#define GTP_FRAME_SIZE_MIN 12288 // 12*1024 = 12KB = 3 PAGES
#if defined(GTP_FRAME_SIMPLE) || defined(GTP_RB)
#define FRAME_ALIGN_SIZE sizeof(unsigned int)
#define FRAME_ALIGN(x) ((x + FRAME_ALIGN_SIZE - 1) \
& (~(FRAME_ALIGN_SIZE - 1)))
#endif
4. 需要理解的几个问题
通过对以上代码的分析,需要理解以下几个问题:(1)内存对齐
(2)Linux内存管理,页分配机制
(3)Linux SMP的支持,per_cpu的使用
(4)双链表Ring Buffer的实现
(5)原子操作
5. 参考链接
[1] http://www.cnblogs.com/yubo/archive/2010/04/23/1718810.html
[2] http://www.ibm.com/developerworks/cn/linux/l-linux-smp/
[3] http://www.ibm.com/developerworks/library/l-linux-smp/
[4] grid.hust.edu.cn/chc/download/Linux_smp.ppt
[5] http://blog.csdn.net/nancygreen/article/details/8788394
[6] http://www.makelinux.net/ldd3/chp-8-sect-3