3.0内核代码 linux

2013-06-21 21:09:38 lhf_tiger 阅读数 1349

 

1       内核同步
1.1 内存屏障

Paul曾经讲过:在建造大桥之前,必须得明白力学的原理。要理解内存屏障,首先得明白计算机硬件体系结构,特别是硬件是如何管理缓存的。缓存在多核上的一致性问题是如何产生的。

要深入理解内存屏障,建议大家首先阅读以下资料:

1、《深入理解并行编程》,下载地址是:http://xiebaoyou.download.csdn.net

2、内核自带的文档documentation/memory-barriers.txt.

内存屏障是如此难此理解也难以使用,为什么还需要它呢?硬件工程师为什么不给软件开发者提供一种程序逻辑一致性的内存视图呢?归根结底,这个问题受到光速的影响。在1.8G的主频系统中,在一个时钟周期内,光在真空中的传播距离只有几厘米,电子的传播距离更短,根本无法传播到整个系统中。

Linux为开发者实现了以下内存屏障:

名称

函数名

作用

读写屏障

mb

在多核和IO内存、缓存之间设置一个完全读写屏障

读屏障

rmb

在多核和IO内存、缓存之间设置一个读屏障

写屏障

wmb

在多核和IO内存、缓存之间设置一个写屏障

读依赖屏障

read_barrier_depends

在多核和IO内存、缓存之间设置一个读依赖屏障

多核读写屏障

Smp_mb

在多核之间设置一个完全读写屏障

多核读屏障

Smp_rmb

在多核之间设置一个读屏障

多核写屏障

Smp_wmb

在多核之间设置一个写屏障

多核读依赖屏障

Smp_read_barrier_depends

在多核之间设置一个读依赖屏障

 

按照linux设计,mb、rmb、wmb、read_barrier_depends主要用于CPU与外设IO之间。在arm及其他一些RISC系统中,通常将外设IO地址映射为一段内存地址。虽然这样的内存是非缓存的,但是仍然受到内存读写乱序的影响。例如,我们要读写一个外部IO端口的数据时,可能会先向某个寄存器写入一个要读写的端口号,再读取另一个端口得到其值。如果要读取值之前,设置的端口号还没有到达外设,那么通常读取的数据是不可靠的,有时甚至会损坏硬件。这种情况下,需要在读寄存器前,设置一个内存屏障,保证二次操作外部端口之间没有乱序。

Smp_mb、smp_rmb、smp_wmb仅仅用于SMP系统,它解决的是多核之间内存乱序的问题。其具体用法及原理,请参阅《深入理解并行编程》。

read_barrier_depends和smp_ read_barrier_depends是读依赖屏障。除了在DEC alpha架构外,linux支持的其他均不需要这个屏障。Alpha需要它,是因为alpha架构中,使用的缓存是split cache.所谓split cache,简单的说就是一个核的缓存不止一个.在arm架构下,我们可以简单的忽略这个屏障。

虽然linux分读写屏障、读屏障、写屏障,但是在ARM中,它们的实现都是一样的,没有严格区别不同的屏障。

内存屏障也隐含了编译屏障的作用。所谓编译屏障,是为了解决编译乱序的问题。这个问题的根源在于:在发明编译器的时候,多核还未出现。编译器开发者认为编译出来的二进制代码只要在单核上运行正确就可以了。甚至,只要保证单线程内的程序逻辑正确性即可。例如,我们有两句赋值语句:

A = 1;

B = 2;

编译器并不保证生成的汇编是按照C语句的顺序。为了效率或者其他原因,它生成的汇编语句可能与下面的C代码是一致的:

B = 2;

A = 1;

要防止编译乱序,可以使用编译屏障指令barrier();

 

1.2 不是题外话的题外话

在描述原子变量和每CPU变量、其他内核同步方法之前,我们先看一段代码。假设有两个线程A和线程B,它们的执行代码分别是foo_a、foo_b,它们都操作一个全局变量g_a,如下:

Unsigned long g_a;

Int stoped = 0;

Void foo_a(void *unused)

{

         While (stopped == 0)

         {

G_a++;

}

}

 

Void foo_b(void *unused)

{

         While (stopped == 0)

         {

G_a++;

}

}

 

假设当stopped被设置为1后,线程A和线程B执行了count_a、count_b次,您会认为g_a的值等于count_a + count_b吗?

恩,当您在一台真实的计算上测试这个程序的时候,也许您的直觉是对的,g_a的值确实等于count_a + count_b。

但是,请您:

1、将测试程序运行的时间运行得久一点

2、或者将程序放到arm、powerpc或者mips上运行

3、或者找一台运行linux的多核x86机器运行。

g_a的值还会等于count_a + count_b吗?

 

答案是不会。

 

原因是什么呢?

产生这个问题的根本原因是:

1、             在多核上,一个CPU在向内存写入数据时,它并不知道其他核在向同样的内存地址写入。某一个核写入的数据可能会覆盖其他核写入的数据。假说g_a当前值是0,那么线程A和线程B同时读取它的值,当内存中的值放入总线上后,两个线程都认为其值是0.并同时将其值加1后提交给总线并向内存中写入1.其中一个线程对g_a的递增被丢失了。

2、             Arm、powerpc、mips这些体系结构都是存储/加载体系结构,它们不能直接对内存中的值进行操作。而必须将内存中的值加载到寄存器中后,将寄存器中的值加1后,再存储到内存中。如果两个线程都读取0值到寄存器中,并将寄存器的值递增为1后存储到内存,那么也会丢失一次递增。

3、             即使在x86体系结构中,允许直接对内存进行递增操作。也会由于编译器的原因,将内存中的值加载到内存,同第二点,也可能造成丢失一次递增。

 

怎么解决这个问题呢?

聪明的读者会说了:是不是需要这样声明g_a?

Unsigned long volatile g_a;

 

更聪明的读者会说,在写g_a时还需要锁住总线,使用汇编语句并在汇编前加lock前缀。

锁总线是正确的,但是也必须将g_a声明为valatile类型的变量。可是,在我们分析的ARM多核上,应该怎么办?

1.3 原子变量

原子变量就是为了解决我们遇到的问题:如果在共享内存的多核系统上正确的修改共享变量的计数值。

首先,我们看一下老版本是如何定义原子变量的:

/**

 * 将counter声明成volatile是为了防止编译器优化,强制从内存中读取counter的值

 */

typedef struct { volatile int counter; } atomic_t;

在linux3.0中,已经有所变化:

typedef struct {

         int counter;

} atomic_t;

已经没有volatile来定义counter了。难道不需要禁止编译优化了吗?答案不是的。这是因为linux3.0已经修改了原子变量相关的函数。

 

Linux中的基本原子操作

宏或者函数

说明

Atomic_read

返回原子变量的值

Atomic_set

设置原子变量的值。

Atomic_add

原子的递增计数的值。

Atomic_sub

原子的递减计数的值。

atomic_cmpxchg

原子比较并交换计数值。

atomic_clear_mask

原子的清除掩码。

 

除此以外,还有一组操作64位原子变量的变体,以及一些位操作宏及函数。这里不再罗列。

/**

 * 返回原子变量的值。

 * 这里强制将counter转换为volatile int并取其值。目的就是为了避免编译优化。

 */

#define atomic_read(v)   (*(volatile int *)&(v)->counter)

/**

 * 设置原子变量的值。

 */

#define atomic_set(v,i)    (((v)->counter) = (i))

 

原子递增的实现比较精妙,理解它的关键是需要明白ldrex、strex这一对指令的含义。

/**

 * 原子的递增计数的值。

 */

static inline void atomic_add(int i, atomic_t *v)

{

         unsigned long tmp;

         int result;

 

         /**

          * __volatile__是为了防止编译器乱序。与"#define atomic_read(v)          (*(volatile int *)&(v)->counter)"中的volatile类似。

          */

         __asm__ __volatile__("@ atomic_add\n"

         /**

          * ldrex是arm为了支持多核引入的新指令,表示"排它性"加载。与mips的ll指令一样的效果。

          * 它与"排它性"存储配对使用。

          */

"1:    ldrex         %0, [%3]\n"

         /**

          * 原子变量的值已经加载到寄存器中,这里对寄存器中的值减去指定的值。

          */

"       add  %0, %0, %4\n"

         /**

          * strex是"排它性"的存储寄存器的值到内存中。类似于mips的sc指令。

          */

"       strex         %1, %0, [%3]\n"

         /**

          * 关键代码是这里的判断。如果在ldrex和strex之间,其他核没有对原子变量变量进行加载存储操作,

          * 那么寄存器中值就是0,否则非0.

          */

"       teq   %1, #0\n"

         /**

          * 如果其他核与本核冲突,那么寄存器值为非0,这里跳转到标号1处,重新加载内存的值并递增其值。

          */

"       bne  1b"

         : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)

         : "r" (&v->counter), "Ir" (i)

         : "cc");

}

 

atomic_add_return递增原子变量的值,并返回它的新值。它与atomic_add的最大不同,在于在原子递增前后各增加了一句:smp_mb();

这是由linux原子操作函数的语义规定的:所有对原子变量的操作,如果需要向调用者返回结果,那么就需要增加多核内存屏障的语义。通俗的说,就是其他核看到本核对原子变量的操作结果时,本核在原子变量前的操作对其他核也是可见的。

 

理解了atomic_add,其他原子变量的实现也就容易理解了。这里不再详述。

 

1.4 每CPU变量

 

原子变量是不是很棒?无论有多少个核,每个核都可以修改共享内存变量,并且这样的修改可以被其他核立即看到。多核编程原来so easy!

不过还是不能太高兴了,原子变量虽然不是毒瘤,但是也差不多了。我曾经遇到一个兄弟,工作十多年了吧,得意的吹嘘:“我写的代码精细得很,统计计数都是用的汇编实现的,汇编加法指令还用了lock前缀。”呜呼,这个兄弟完全没有意识到在x86体系结构中,这个lock前缀对性能的影响。

不管哪种架构,原子计数(包含原子比较并交换)都是极耗CPU的。与单纯的加减计数指令相比,它消耗的CPU周期要高一到两个数量级。原因是什么呢?还是光信号(电信号)的传播速度问题。要让某个核上的修改被其他核发现,需要信号在整个系统中进行传播。这在几个核的系统中,可能还不是大问题,但是在1024个核以上的系统中呢?比如我们熟知的天河系统。

为了解决这个问题,内核引用入了每CPU变量。

 

可以将它理解为数据结构的数组。系统的每个CPU对应数组中的一个元素。每个CPU都只访问本CPU对应的数组元素。

每CPU数组中,确保每一个数组元素都位于不同的缓存行中。假如您有一个int型的每CPU数组,那么每个int型都会占用一个缓存行(很多系统中一个缓存行是32个字节),这看起来有点浪费。这样做的原因是:

ü  对每CPU数组的并发访问不会导致高速缓存行的失效。避免在各个核之间引起缓存行的抖动。

ü  这也是为了避免出现多核之间数据覆盖的情况。对这一点,可能您暂时不能理解。也许您在内核领域实际工作几年,也会觉得这有点难于理解。不过,现在您只需要知道有这么一个事实存在就行了。

关于第二个原因,您可以参考一个内核补丁:

99dcc3e5a94ed491fbef402831d8c0bbb267f995。据提交补丁的兄弟讲,这个补丁表面是一个性能优化的措施。但是,它实际上是一个BUG。该故障会引起内核内存分配子系统的一个BUG,最终会引起内存分配子系统陷入死循环。我实际的遇到了这个故障,可怜了我的两位兄弟,为了解决这个故障,花了近两个月时间,今天终于被我搞定了。

 

每CPU变量的主要目的是对多CPU并发访问的保护。但是它不能防止同一核上的中断的影响。我们曾经讲过,在arm、mips等系统中,++、--这样的简单计数操作,都需要几条汇编语句来完成。如果在从内存中加载数据到寄存器后,还没有将数据保存到内存中前,有中断将操作过程打断,并在中断处理函数中对同样的计数值进行操作,那么中断中的操作将被覆盖。

不管在多CPU还是单CPU中,内核抢占都可能象中断那样破坏我们对计数的操作。因此,应当在禁用抢占的情况下访问每CPU变量。内核抢占是一个大的话题,我们在讲调度的时候再提这个事情。

相关宏和函数:

宏或者函数

说明

DEFINE_PER_CPU

静态定义一个每CPU变量数组

per_cpu

获得每CPU数组中某个CPU对应的元素

__this_cpu_ptr

获得当前CPU在数组中的元素的指针。

__get_cpu_var

获得当前CPU在数组中的元素的值。

get_cpu_ptr

关抢占,并获得CPU对应的元素指针。

put_cpu_var

开抢占,与get_cpu_ptr配对使用。

 

看到这里,也许大家会觉得,用每CPU变量来代替原子变量不是很好么?不过,存在的东西就必然在存在的理由,因为每CPU变量用于计数有一个致使的弊端:它是不精确的。我们设想:有32个核的系统,每个核更新自己的CPU计数,如果有一个核想知道计数总和怎么办?简单的用一个循环将计数加起来吗?这显然是不行的。因为某个核修改了自己的计数变量时,其他核不能立即看到它对这个核的计数进行的修改。这会导致计数总和不准。特别是某个核对计数进行了大的修改的时候,总计数看起来会严重不准。

为了使总和大致可信,内核又引入了另一种每CPU变量:percpu_counter。

percpu_counter的详细实现在percpu_counter.c中。有兴趣的同学可以研究一下。下面我们讲一个主要的函数,希望起个抛砖引玉的作用:

/**

* 增加每CPU变量计数

*            fbc:            要增加的每CPU变量

*            amount:   本次要增加的计数值

*            batch:       当本CPU计数超过此值时,要确保其他核能及时看到。                                     

*/

void __percpu_counter_add(struct percpu_counter *fbc, s64 amount, s32 batch)

{

         s64 count;

 

         /**

* 为了避免当前任务飘移到其他核上,或者被其他核抢占,导致计数丢失

* 这里需要关抢占。

*/

         preempt_disable();

         /**

     * 获得本CPU计数值并加上计数值。

     */

         count = __this_cpu_read(*fbc->counters) + amount;

         if (count >= batch || count <= -batch) {/* 本次修改的值较大,需要同步到全局计数中 */

                   spin_lock(&fbc->lock);/* 获得自旋锁,这样可以避免多核同时更新全局计数。 */

                   fbc->count += count;/* 修改全局计数,并将本CPU计数清0 */

                   __this_cpu_write(*fbc->counters, 0);

                   spin_unlock(&fbc->lock);

         } else {

                   __this_cpu_write(*fbc->counters, count);/* 本次修改的计数较小,仅仅更新本CPU计数。 */

         }

         preempt_enable();/* 打开抢占 */

}

 

 

2013-01-17 14:38:17 beyondioi 阅读数 1861

原文地址:《LINUX3.0内核源代码分析》第三章:内核同步(1) 作者:xiebaoyou

摘要:本文主要讲述linux如何处理ARM cortex A9多核处理器的内核同步部分。主要包括其中的内存屏障、原子变量、每CPU变量。

自旋锁、信号量、complete、读写自旋锁、读写信号量、顺序锁、RCU放在后文介绍。

法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友(scxby@163.com)发表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。

本连载文章并不是为了形成一本适合出版的书籍,而是为了向有一定内核基本的读者提供一些linux3.0源码分析。因此,请读者结合《深入理解LINUX内核》第三版阅读本连载。

1       内核同步
1.1 内存屏障

Paul曾经讲过:在建造大桥之前,必须得明白力学的原理。要理解内存屏障,首先得明白计算机硬件体系结构,特别是硬件是如何管理缓存的。缓存在多核上的一致性问题是如何产生的。

要深入理解内存屏障,建议大家首先阅读以下资料:

1、《深入理解并行编程》,下载地址是:http://xiebaoyou.download.csdn.net

2、内核自带的文档documentation/memory-barriers.txt.

内存屏障是如此难此理解也难以使用,为什么还需要它呢?硬件工程师为什么不给软件开发者提供一种程序逻辑一致性的内存视图呢?归根结底,这个问题受到光速的影响。在1.8G的主频系统中,在一个时钟周期内,光在真空中的传播距离只有几厘米,电子的传播距离更短,根本无法传播到整个系统中。

Linux为开发者实现了以下内存屏障:

名称

函数名

作用

读写屏障

mb

在多核和IO内存、缓存之间设置一个完全读写屏障

读屏障

rmb

在多核和IO内存、缓存之间设置一个读屏障

写屏障

wmb

在多核和IO内存、缓存之间设置一个写屏障

读依赖屏障

read_barrier_depends

在多核和IO内存、缓存之间设置一个读依赖屏障

多核读写屏障

Smp_mb

在多核之间设置一个完全读写屏障

多核读屏障

Smp_rmb

在多核之间设置一个读屏障

多核写屏障

Smp_wmb

在多核之间设置一个写屏障

多核读依赖屏障

Smp_read_barrier_depends

在多核之间设置一个读依赖屏障

按照linux设计,mb、rmb、wmb、read_barrier_depends主要用于CPU与外设IO之间。在arm及其他一些RISC系统中,通常将外设IO地址映射为一段内存地址。虽然这样的内存是非缓存的,但是仍然受到内存读写乱序的影响。例如,我们要读写一个外部IO端口的数据时,可能会先向某个寄存器写入一个要读写的端口号,再读取另一个端口得到其值。如果要读取值之前,设置的端口号还没有到达外设,那么通常读取的数据是不可靠的,有时甚至会损坏硬件。这种情况下,需要在读寄存器前,设置一个内存屏障,保证二次操作外部端口之间没有乱序。

Smp_mb、smp_rmb、smp_wmb仅仅用于SMP系统,它解决的是多核之间内存乱序的问题。其具体用法及原理,请参阅《深入理解并行编程》。

read_barrier_depends和smp_ read_barrier_depends是读依赖屏障。除了在DEC alpha架构外,linux支持的其他均不需要这个屏障。Alpha需要它,是因为alpha架构中,使用的缓存是split cache.所谓split cache,简单的说就是一个核的缓存不止一个.在arm架构下,我们可以简单的忽略这个屏障。

虽然linux分读写屏障、读屏障、写屏障,但是在ARM中,它们的实现都是一样的,没有严格区别不同的屏障。

内存屏障也隐含了编译屏障的作用。所谓编译屏障,是为了解决编译乱序的问题。这个问题的根源在于:在发明编译器的时候,多核还未出现。编译器开发者认为编译出来的二进制代码只要在单核上运行正确就可以了。甚至,只要保证单线程内的程序逻辑正确性即可。例如,我们有两句赋值语句:

A = 1;

B = 2;

编译器并不保证生成的汇编是按照C语句的顺序。为了效率或者其他原因,它生成的汇编语句可能与下面的C代码是一致的:

B = 2;

A = 1;

要防止编译乱序,可以使用编译屏障指令barrier();

1.2 不是题外话的题外话

在描述原子变量和每CPU变量、其他内核同步方法之前,我们先看一段代码。假设有两个线程A和线程B,它们的执行代码分别是foo_a、foo_b,它们都操作一个全局变量g_a,如下:

Unsigned long g_a;

Int stoped = 0;

Void foo_a(void *unused)

{

         While (stopped == 0)

         {

G_a++;

}

}

Void foo_b(void *unused)

{

         While (stopped == 0)

         {

G_a++;

}

}

假设当stopped被设置为1后,线程A和线程B执行了count_a、count_b次,您会认为g_a的值等于count_a + count_b吗?

恩,当您在一台真实的计算上测试这个程序的时候,也许您的直觉是对的,g_a的值确实等于count_a + count_b。

但是,请您:

1、将测试程序运行的时间运行得久一点

2、或者将程序放到arm、powerpc或者mips上运行

3、或者找一台运行linux的多核x86机器运行。

g_a的值还会等于count_a + count_b吗?

答案是不会。

原因是什么呢?

产生这个问题的根本原因是:

1、             在多核上,一个CPU在向内存写入数据时,它并不知道其他核在向同样的内存地址写入。某一个核写入的数据可能会覆盖其他核写入的数据。假说g_a当前值是0,那么线程A和线程B同时读取它的值,当内存中的值放入总线上后,两个线程都认为其值是0.并同时将其值加1后提交给总线并向内存中写入1.其中一个线程对g_a的递增被丢失了。

2、             Arm、powerpc、mips这些体系结构都是存储/加载体系结构,它们不能直接对内存中的值进行操作。而必须将内存中的值加载到寄存器中后,将寄存器中的值加1后,再存储到内存中。如果两个线程都读取0值到寄存器中,并将寄存器的值递增为1后存储到内存,那么也会丢失一次递增。

3、             即使在x86体系结构中,允许直接对内存进行递增操作。也会由于编译器的原因,将内存中的值加载到内存,同第二点,也可能造成丢失一次递增。

怎么解决这个问题呢?

聪明的读者会说了:是不是需要这样声明g_a?

Unsigned long volatile g_a;

更聪明的读者会说,在写g_a时还需要锁住总线,使用汇编语句并在汇编前加lock前缀。

锁总线是正确的,但是也必须将g_a声明为valatile类型的变量。可是,在我们分析的ARM多核上,应该怎么办?

1.3 原子变量

原子变量就是为了解决我们遇到的问题:如果在共享内存的多核系统上正确的修改共享变量的计数值。

首先,我们看一下老版本是如何定义原子变量的:

/**

* 将counter声明成volatile是为了防止编译器优化,强制从内存中读取counter的值

*/

typedef struct { volatile int counter; } atomic_t;

在linux3.0中,已经有所变化:

typedef struct {

         int counter;

} atomic_t;

已经没有volatile来定义counter了。难道不需要禁止编译优化了吗?答案不是的。这是因为linux3.0已经修改了原子变量相关的函数。

Linux中的基本原子操作

宏或者函数

说明

Atomic_read

返回原子变量的值

Atomic_set

设置原子变量的值。

Atomic_add

原子的递增计数的值。

Atomic_sub

原子的递减计数的值。

atomic_cmpxchg

原子比较并交换计数值。

atomic_clear_mask

原子的清除掩码。

除此以外,还有一组操作64位原子变量的变体,以及一些位操作宏及函数。这里不再罗列。

/**

* 返回原子变量的值。

* 这里强制将counter转换为volatile int并取其值。目的就是为了避免编译优化。

*/

#define atomic_read(v)   (*(volatile int *)&(v)->counter)

/**

* 设置原子变量的值。

*/

#define atomic_set(v,i)    (((v)->counter) = (i))

原子递增的实现比较精妙,理解它的关键是需要明白ldrex、strex这一对指令的含义。

/**

* 原子的递增计数的值。

*/

static inline void atomic_add(int i, atomic_t *v)

{

         unsigned long tmp;

         int result;

         /**

          * __volatile__是为了防止编译器乱序。与"#define atomic_read(v)          (*(volatile int *)&(v)->counter)"中的volatile类似。

          */

         __asm__ __volatile__("@ atomic_addn"

         /**

          * ldrex是arm为了支持多核引入的新指令,表示"排它性"加载。与mips的ll指令一样的效果。

          * 它与"排它性"存储配对使用。

          */

"1:    ldrex         %0, [%3]n"

         /**

          * 原子变量的值已经加载到寄存器中,这里对寄存器中的值减去指定的值。

          */

"       add  %0, %0, %4n"

         /**

          * strex是"排它性"的存储寄存器的值到内存中。类似于mips的sc指令。

          */

"       strex         %1, %0, [%3]n"

         /**

          * 关键代码是这里的判断。如果在ldrex和strex之间,其他核没有对原子变量变量进行加载存储操作,

          * 那么寄存器中值就是0,否则非0.

          */

"       teq   %1, #0n"

         /**

          * 如果其他核与本核冲突,那么寄存器值为非0,这里跳转到标号1处,重新加载内存的值并递增其值。

          */

"       bne  1b"

         : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)

         : "r" (&v->counter), "Ir" (i)

         : "cc");

}

atomic_add_return递增原子变量的值,并返回它的新值。它与atomic_add的最大不同,在于在原子递增前后各增加了一句:smp_mb();

这是由linux原子操作函数的语义规定的:所有对原子变量的操作,如果需要向调用者返回结果,那么就需要增加多核内存屏障的语义。通俗的说,就是其他核看到本核对原子变量的操作结果时,本核在原子变量前的操作对其他核也是可见的。

理解了atomic_add,其他原子变量的实现也就容易理解了。这里不再详述。

1.4 每CPU变量

原子变量是不是很棒?无论有多少个核,每个核都可以修改共享内存变量,并且这样的修改可以被其他核立即看到。多核编程原来so easy!

不过还是不能太高兴了,原子变量虽然不是毒瘤,但是也差不多了。我曾经遇到一个兄弟,工作十多年了吧,得意的吹嘘:“我写的代码精细得很,统计计数都是用的汇编实现的,汇编加法指令还用了lock前缀。”呜呼,这个兄弟完全没有意识到在x86体系结构中,这个lock前缀对性能的影响。

不管哪种架构,原子计数(包含原子比较并交换)都是极耗CPU的。与单纯的加减计数指令相比,它消耗的CPU周期要高一到两个数量级。原因是什么呢?还是光信号(电信号)的传播速度问题。要让某个核上的修改被其他核发现,需要信号在整个系统中进行传播。这在几个核的系统中,可能还不是大问题,但是在1024个核以上的系统中呢?比如我们熟知的天河系统。

为了解决这个问题,内核引用入了每CPU变量。

可以将它理解为数据结构的数组。系统的每个CPU对应数组中的一个元素。每个CPU都只访问本CPU对应的数组元素。

每CPU数组中,确保每一个数组元素都位于不同的缓存行中。假如您有一个int型的每CPU数组,那么每个int型都会占用一个缓存行(很多系统中一个缓存行是32个字节),这看起来有点浪费。这样做的原因是:

ü  对每CPU数组的并发访问不会导致高速缓存行的失效。避免在各个核之间引起缓存行的抖动。

ü  这也是为了避免出现多核之间数据覆盖的情况。对这一点,可能您暂时不能理解。也许您在内核领域实际工作几年,也会觉得这有点难于理解。不过,现在您只需要知道有这么一个事实存在就行了。

关于第二个原因,您可以参考一个内核补丁:

99dcc3e5a94ed491fbef402831d8c0bbb267f995。据提交补丁的兄弟讲,这个补丁表面是一个性能优化的措施。但是,它实际上是一个BUG。该故障会引起内核内存分配子系统的一个BUG,最终会引起内存分配子系统陷入死循环。我实际的遇到了这个故障,可怜了我的两位兄弟,为了解决这个故障,花了近两个月时间,今天终于被我搞定了。

每CPU变量的主要目的是对多CPU并发访问的保护。但是它不能防止同一核上的中断的影响。我们曾经讲过,在arm、mips等系统中,++、--这样的简单计数操作,都需要几条汇编语句来完成。如果在从内存中加载数据到寄存器后,还没有将数据保存到内存中前,有中断将操作过程打断,并在中断处理函数中对同样的计数值进行操作,那么中断中的操作将被覆盖。

不管在多CPU还是单CPU中,内核抢占都可能象中断那样破坏我们对计数的操作。因此,应当在禁用抢占的情况下访问每CPU变量。内核抢占是一个大的话题,我们在讲调度的时候再提这个事情。

相关宏和函数:

宏或者函数

说明

DEFINE_PER_CPU

静态定义一个每CPU变量数组

per_cpu

获得每CPU数组中某个CPU对应的元素

__this_cpu_ptr

获得当前CPU在数组中的元素的指针。

__get_cpu_var

获得当前CPU在数组中的元素的值。

get_cpu_ptr

关抢占,并获得CPU对应的元素指针。

put_cpu_var

开抢占,与get_cpu_ptr配对使用。

看到这里,也许大家会觉得,用每CPU变量来代替原子变量不是很好么?不过,存在的东西就必然在存在的理由,因为每CPU变量用于计数有一个致使的弊端:它是不精确的。我们设想:有32个核的系统,每个核更新自己的CPU计数,如果有一个核想知道计数总和怎么办?简单的用一个循环将计数加起来吗?这显然是不行的。因为某个核修改了自己的计数变量时,其他核不能立即看到它对这个核的计数进行的修改。这会导致计数总和不准。特别是某个核对计数进行了大的修改的时候,总计数看起来会严重不准。

为了使总和大致可信,内核又引入了另一种每CPU变量:percpu_counter。

percpu_counter的详细实现在percpu_counter.c中。有兴趣的同学可以研究一下。下面我们讲一个主要的函数,希望起个抛砖引玉的作用:

/**

* 增加每CPU变量计数

*            fbc:            要增加的每CPU变量

*            amount:   本次要增加的计数值

*            batch:       当本CPU计数超过此值时,要确保其他核能及时看到。                                     

*/

void __percpu_counter_add(struct percpu_counter *fbc, s64 amount, s32 batch)

{

         s64 count;

         /**

* 为了避免当前任务飘移到其他核上,或者被其他核抢占,导致计数丢失

* 这里需要关抢占。

*/

         preempt_disable();

         /**

     * 获得本CPU计数值并加上计数值。

     */

         count = __this_cpu_read(*fbc->counters) + amount;

         if (count >= batch || count <= -batch) {/* 本次修改的值较大,需要同步到全局计数中 */

                   spin_lock(&fbc->lock);/* 获得自旋锁,这样可以避免多核同时更新全局计数。 */

                   fbc->count += count;/* 修改全局计数,并将本CPU计数清0 */

                   __this_cpu_write(*fbc->counters, 0);

                   spin_unlock(&fbc->lock);

         } else {

                   __this_cpu_write(*fbc->counters, count);/* 本次修改的计数较小,仅仅更新本CPU计数。 */

         }

         preempt_enable();/* 打开抢占 */

}

大家现在觉得多核编程有那么一点难了吧?一个简单的计数都可以搞得这么复杂。

复杂的东西还在后面。接下来我们新开一帖,讨论内核同步的其他技术:自旋锁、信号量、RCU、无锁编程。

来源:http://blog.chinaunix.net/uid-26303633-id-3013598.html

2018-07-15 16:02:05 hzgdiyer 阅读数 773

https://blog.csdn.net/ekenlinbing/article/details/7613334
摘要:本章主要介绍了LINUX3.0内存寻址方面的内容,重点对follow_page函数进行注释,以帮助读者大致了解ARM A9的页表组织。 读者需要理解一些基本概念:虚拟地址、物理地址、MPU、MMU、ARM中的二级页表、cache、TLB。

法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友(scxby@163.com)发表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。

本连载文章并不是为了形成一本适合出版的书籍,而是为了向有一定内核基本的读者提供一些linux3.0源码分析。因此,请读者结合《深入理解LINUX内核》第三版阅读本连载。

本系列文章分析ARM A9的linux3.0代码实现。因此,需要读者有一定的ARM体系硬件知识。推荐阅读《ARM嵌入式系统开发-软件设计与优化》。另外,读者最好对内核有所了解,推荐阅读《深入理解LINUX内核》第三版。

读者需要理解一些基本概念:虚拟地址、物理地址、MPU、MMU、ARM中的二级页表、cache、TLB。

1.1 基本函数
Linux3.0将分页抽象为四级:

名称 数据结构 备注
页全局目录 Pgd_t
页上级目录 Pud_t A9未用
页中间目录 Pmd_t A9未用
页表 Pte_t
/**

 * 对A9来说,只支持4K大小的页,因此PAGE_SHIFT定义为12.它表示一个虚拟地址的页内偏移量的位数。

 * 根据它计算出来的页大小PAGE_SIZE为4K,PAGE_MASK为0xffff000。

 */

#define PAGE_SHIFT           12

#define PAGE_SIZE              (_AC(1,UL) << PAGE_SHIFT)

#define PAGE_MASK           (~(PAGE_SIZE-1))



/**

 * 对A9来说,没有PMD和PUD,因此,PMD_SHIFT和PUD_SHIFT的值与PGDIR_SHIFT是一样的,都是21.

 * 21表示一个页全局目录项代表了2^20即1M的地址空间。

 */

#define PMD_SHIFT            21

#define PGDIR_SHIFT                  21



/**

 * 分别代表一个页表、页中间目录、页全局目录表中表项的个数。

 */

#define PTRS_PER_PTE               512

#define PTRS_PER_PMD             1

#define PTRS_PER_PGD              2048



/**

 * 将pte\pmd\pud\pgd\pgprot转换为整型值

 */

#define pte_val(x)      (x)

#define pmd_val(x)      (x)

#define pgd_val(x)      ((x)[0])

#define pgprot_val(x)   (x)



/**

 * 将整型值转换为pte\pmd\pud\pgd\pgprot

 */

#define __pte(x)        (x)

#define __pmd(x)        (x)

#define __pgprot(x)     (x)

1.1.1 判断页表项标志的函数

/**

 * 页表项是否为0

 */

#define pte_none(pte)                 (!pte_val(pte))

/**

 * 页表项是否可用。当页在内存中但是不可读写时置此标志。典型的用途是写时复制。

 */

#define pte_present(pte)  (pte_val(pte) & L_PTE_PRESENT)

/**

 * 页表项是否有可写标志

 */

#define pte_write(pte)                (!(pte_val(pte) & L_PTE_RDONLY))

/**

 * 页表项是否为脏

 */

#define pte_dirty(pte)                 (pte_val(pte) & L_PTE_DIRTY)

/**

 * 页表项是否表示最近没有被访问过

 */

#define pte_young(pte)               (pte_val(pte) & L_PTE_YOUNG)

/**

 * 页表项是否有可执行标志

 */

#define pte_exec(pte)                 (!(pte_val(pte) & L_PTE_XN))

#define pte_special(pte)    (0)



/**

 * 清除页表项的值。

 */

#define pte_clear(mm,addr,ptep)     set_pte_ext(ptep, __pte(0), 0)



/**

 * 向一个页表项中写入指定的值。

 */

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)



/**

 * 判断两个页表项是否指向相同的页并且有相同的访问权限

 */

static inline int pte_same(pte_t pte_a, pte_t pte_b)

{

   return pte_val(pte_a) == pte_val(pte_b);

}



/**

 * 检查页中间目录项是否指向不可用的页表。

 */

#define pmd_bad(pmd)               (pmd_val(pmd) & 2)



/**

 * 页表项是否可用。当页在内存中但是不可读写时置此标志。典型的用途是写时复制。

 */

#define pte_present(pte)  (pte_val(pte) & L_PTE_PRESENT)

1.1.2 页表项操作函数

/**

 * 虚拟地址在页全局目录中索引

 */

#define pgd_index(addr)             ((addr) >> PGDIR_SHIFT)



/**

 * 计算一个进程用户态地址对应的页全局目录项地址。

 * 计算内核态地址的页全局目录项地址应当使用pgd_offset_k

 */

#define pgd_offset(mm, addr)  ((mm)->pgd + pgd_index(addr))



/* to find an entry in a kernel page-table-directory */

/**

 * 计算一个内核态地址的页全局目录项地址。

 */

#define pgd_offset_k(addr)        pgd_offset(&init_mm, addr)

/**

 * 获得页全局目录项所指向的页面。对A9来说,就是pmd_page

 */

#define pgd_page(pgd)                                  (pud_page((pud_t){ pgd }))

/**

 * 获得页全局目录项的虚拟地址。

 */

#define pgd_page_vaddr(pgd)                     (pud_page_vaddr((pud_t){ pgd }))



/**

 * 在页全局目录表中,查找一个虚拟地址对应的页上级目录位置。

 * 对二级页表来说,页上级目录就是页全局目录,因此直接返回页全局目录。

 */

#define pud_offset(pgd, start)           (pgd)

/**

 * 获得页上级目录页面。

 */

#define pud_page(pud)                         pgd_page(pud)

/**

 * 获得页上级目录页面的虚拟地址。

 */

#define pud_page_vaddr(pud)            pgd_page_vaddr(pud)



/**

 * 获得一个虚拟地址的页中间目录中的地址。对二级页表来说,没有pmd,直接返回页全局目录地址即可。

 */

#define pmd_offset(dir, addr)    ((pmd_t *)(dir))

/**

 * 获得页中间目录指向的页表页面。

 */

#define pmd_page(pmd)             pfn_to_page(__phys_to_pfn(pmd_val(pmd)))

/**

 * 获得一个线性地址对应的页表项在页表中的索引

 */

#define pte_index(addr)              (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

/**

 * 在主内核页表中定位内核地址对应的页表项的虚拟地址。

 */

#define pte_offset_kernel(pmd,addr)        (pmd_page_vaddr(*(pmd)) + pte_index(addr))

/**

 * 在进程页表中定位线性地址对应的页表项的地址。如果页表保存在高端内存中,那么还为页表建立一个临时内核映射。

 */

#define pte_offset_map(pmd,addr)  (__pte_map(pmd) + pte_index(addr))

/**

 * 如果页表在高端内存中,不解除由pte_offset_map建立的临时内核映射。

 */

#define pte_unmap(pte)                      __pte_unmap(pte)

/**

 * 获取页表项中的页帧号。

 */

#define pte_pfn(pte)           (pte_val(pte) >> PAGE_SHIFT)

/**

 * 根据页帧号和页面属性,合成页表项。

 */

#define pfn_pte(pfn,prot)  __pte(__pfn_to_phys(pfn) | pgprot_val(prot))

/**

 * 从页表项中提取页帧号,并定位该页帧号对应的页框。

 */

#define pte_page(pte)                 pfn_to_page(pte_pfn(pte))

/**

 * 根据页框和页面属性,合成页表项。

 */

#define mk_pte(page,prot)        pfn_pte(page_to_pfn(page), prot)

/**

 * 当页表项映射到文件,并且没有装载进内存时,从页表项中提取文件页号。

 */

#define pte_to_pgoff(x)              (pte_val(x) >> 3)

/**

 * 将页面映射的页号存放到页表项中

 */

#define pgoff_to_pte(x)              __pte(((x) << 3) | L_PTE_FILE)

1.1.3 页表分配相关的函数

/**

 * 为页全局目录分配内存

 */

pgd_t *pgd_alloc(struct mm_struct *mm)

/**

 * 释放页全局目录项

 */

void pgd_free(struct mm_struct *mm, pgd_t *pgd_base)

/**

 * 分配页上级目录,在二级页表中,此函数什么也不做。

 */

#define pud_alloc(mm, pgd, address)        (pgd)

/**

 * 释放页上级目录,在二级页表中,这个函数什么也不做

 */

#define pud_free(mm, x)                               do { } while (0)



Pmd_alloc、pmd_free、pte_alloc_map、pte_free等宏或函数与此类似。

1.2 刷新cache和TLB
Cache是CPU与内存之间的缓存,而TLB是CPU与MMU之间缓存。

当外部硬件通过DMA修改了内存中的数据时,需要使cache中的数据失效,强制CPU从内存中装载数据。当CPU向缓存中写入数据后,为了通过DMA将数据传送到外部硬件,则需要将缓存中的数据强制写入内存。

当页表项映射的页面发生变化后,也需要将页面缓存的内容写入内存。

同理,当修改了页表项后,为了避免TLB中缓存的项进行错误的MMU转换,也需要使TLB中缓存的项失效。

1.3 follow_page函数
follow_page函数是从进程的页表中搜索特定地址对应的页面对象。这个函数对于理解LINUX内核页表管理有帮助。

struct page *follow_page(struct vm_area_struct *vma, unsigned long address,

                           unsigned int flags)

{

        pgd_t *pgd;

        pud_t *pud;

        pmd_t *pmd;

        pte_t *ptep, pte;

        spinlock_t *ptl;

        struct page *page;

        struct mm_struct *mm = vma->vm_mm;



        /**

         * 对ARM A9来说,没有配置巨页功能,follow_huge_addr实际上是空处理。

         */

        page = follow_huge_addr(mm, address, flags & FOLL_WRITE);

        if (!IS_ERR(page)) {

                 BUG_ON(flags & FOLL_GET);

                 goto out;

        }



        page = NULL;

        /**

         * 在一级目录项中,查找地址对应的一级目录索引项。

         */

        pgd = pgd_offset(mm, address);

        /**

         * 该地址对应的一级目录项无效。对ARM来说,pgd_none总返回0,真正的判断是在pmd_none。

         */

        if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))

                 goto no_page_table;



        /**

         * 查找地址对应的页上级目录项。这对4级目录的分组体系来说才有效。ARM不存在页上级目录和页中间目录。

         * pud总是返回pgd。

         */

        pud = pud_offset(pgd, address);

        /**

         * pud_none总是返回0,因此下面的判断是无用。真正有用的判断在后面的pmd_none

         */

        if (pud_none(*pud))

                 goto no_page_table;

        if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {

                 BUG_ON(flags & FOLL_GET);

                 page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);

                 goto out;

        }

        if (unlikely(pud_bad(*pud)))

                 goto no_page_table;



        /**

         * 取页中间目录,对ARM来说,pmd直接返回pud,即pgd。

         */

        pmd = pmd_offset(pud, address);

        /**

         * 判断pmd是否为0,即ARM一级目录是否有效。对pgd,pud的判断都是无用的,真正的判断在这里。

         */

        if (pmd_none(*pmd))

                 goto no_page_table;

        /**

         * 判断pmd是否是一个巨页,以及用户虚拟地址空间段是否是一个巨页段,略过。

         */

        if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {

                 BUG_ON(flags & FOLL_GET);

                 /**

                  * 查找巨页地址映射的物理页面。

                  */

                 page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);

                 goto out;

        }

        /**

         * 透明巨页处理,对某些体系结构,如mips来说,这个功能是有效的。但是虽然ARM硬件支持巨页(1M页)

         * 目前的内核还不支持ARM巨页,略过。

         */

        if (pmd_trans_huge(*pmd)) {

                 if (flags & FOLL_SPLIT) {

                           split_huge_page_pmd(mm, pmd);

                           goto split_fallthrough;

                 }

                 spin_lock(&mm->page_table_lock);

                 if (likely(pmd_trans_huge(*pmd))) {

                           if (unlikely(pmd_trans_splitting(*pmd))) {

                                    spin_unlock(&mm->page_table_lock);

                                    wait_split_huge_page(vma->anon_vma, pmd);

                           } else {

                                    page = follow_trans_huge_pmd(mm, address,

                                                                     pmd, flags);

                                    spin_unlock(&mm->page_table_lock);

                                    goto out;

                           }

                 } else

                           spin_unlock(&mm->page_table_lock);

                 /* fall through */

        }

split_fallthrough:

        /**

         * 判断pmd是否有效。

         */

        if (unlikely(pmd_bad(*pmd)))

                 goto no_page_table;



        /**

         * 在二级页表中找到地址对应的pte。并将pte指针返回。

         * 注意,这里获取了进程的内存页表锁。以防止内核其他路径修改进程页表,使得ptep指向的pte产生变化。

         * ptl是内存页表锁。

         * 如果内核支持将pte表放到高端内存,那么还需要调用kmap_atomic将页表到内核地址空间中。

         */

        ptep = pte_offset_map_lock(mm, pmd, address, &ptl);



        pte = *ptep;

        /**

         * 这里判断页表项是否有效。

         * 有时,页面在内存中,但是不允许访问。比如写时复制。

         * 当页完全不在内存中时,页表项也没有效。

         */

        if (!pte_present(pte))

                 goto no_page;

        /**

         * 希望搜索一个可写的页面,但是页表项没有写权限。

         */

        if ((flags & FOLL_WRITE) && !pte_write(pte))

                 goto unlock;



        /**

         * 根据pte中保存的页帧号,找到该页帧号对应的page结构。

         */

        page = vm_normal_page(vma, address, pte);

        if (unlikely(!page)) {/* 根据页帧号无法找到page结构,可能是一些特殊情况。如驱动自行管理的pte出了问题。 */

                 if ((flags & FOLL_DUMP) || /* 不允许返回0页 */

                     !is_zero_pfn(pte_pfn(pte))) /* 不是0页 */

                           goto bad_page;

                 page = pte_page(pte);/* 向上层返回0页 */

        }



        /**

         * 调用者要求获取页面引用,则增加页面引用计数。

         */

        if (flags & FOLL_GET)

                 get_page(page);

        if (flags & FOLL_TOUCH) {/* 调用者希望设置访问标志,可能是随后会写页面 */

                 if ((flags & FOLL_WRITE) &&/* 获取写引用 */

                     !pte_dirty(pte) && !PageDirty(page))/* 页面和pte的脏标志都还没有设置,则强制设置脏标志 */

                           set_page_dirty(page);

                 /*

                  * pte_mkyoung() would be more correct here, but atomic care

                  * is needed to avoid losing the dirty bit: it is easier to use

                  * mark_page_accessed().

                  */

                 /**

                  * 标记页面访问标志。

                  */

                 mark_page_accessed(page);

        }

        /**

         * 调用者想将页面锁在内存中。

         */

        if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {

                 /*

                  * The preliminary mapping check is mainly to avoid the

                  * pointless overhead of lock_page on the ZERO_PAGE

                  * which might bounce very badly if there is contention.

                  *

                  * If the page is already locked, we don't need to

                  * handle it now - vmscan will handle it later if and

                  * when it attempts to reclaim the page.

                  */

                 if (page->mapping && trylock_page(page)) {/* 锁住页面,不交换到外部存储器中 */

                           lru_add_drain();  /* push cached pages to LRU */

                          /*

                            * Because we lock page here and migration is

                            * blocked by the pte's page reference, we need

                            * only check for file-cache page truncation.

                            */

                           if (page->mapping)

                                    mlock_vma_page(page);

                           unlock_page(page);

                 }

        }

unlock:

        /**

         * 释放进程页面锁,同时,如果支持将页表放到高端内存,就解除对页表的映射。

         */

        pte_unmap_unlock(ptep, ptl);

out:

        return page;



bad_page:

        pte_unmap_unlock(ptep, ptl);

        return ERR_PTR(-EFAULT);



no_page:

        pte_unmap_unlock(ptep, ptl);

        if (!pte_none(pte))

                 return page;



no_page_table:

        /*

         * When core dumping an enormous anonymous area that nobody

         * has touched so far, we don't want to allocate unnecessary pages or

         * page tables.  Return error instead of NULL to skip handle_mm_fault,

         * then get_dump_page() will return NULL to leave a hole in the dump.

         * But we can only make this optimization where a hole would surely

         * be zero-filled if handle_mm_fault() actually did handle it.

         */

        if ((flags & FOLL_DUMP) &&

            (!vma->vm_ops || !vma->vm_ops->fault))

                 return ERR_PTR(-EFAULT);

        return page;
2016-08-02 14:12:44 Xiongzhizhu 阅读数 2539
开发板:FL2440      内核:linux-3.0       交叉编译器:buildroot-2012.08

1. Linux内核基础知识

1.1 关于Linux版本

查看内核版本

[zzx@localhost ~]$ uname -a
Linux localhost.localdomain 2.6.32-573.el6.i686 #1 SMP Thu Jul 23 12:37:35 UTC 2015 i686 i686 i386 GNU/Linux
[zzx@localhost ~]$ cat /proc/version 
Linux version 2.6.32-573.el6.i686 (mockbuild@c6b9.bsys.dev.centos.org) (gcc version 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC) ) #1 SMP Thu Jul 23 12:37:35 UTC 2015
[zzx@localhost ~]$  

Linux内核的版本号可以从源代码的顶层目录下的Makefile中看到,比如2.6.28.1内核的Makefile中:

VERSION       =  2

PATCHLEVEL   =  6

SUBLEVEL     =  28

EXTRAVERSION =  .1

其中的“VERSION”和”PATCHLEVEL”组成主版本号,比如2.42.52.6等。稳定的版本号用偶数表示(用偶数版最好),开发中的测试版本用奇数表示。但是3.0以后均为稳定版。“SUBLEVEL”成为版本号,它不分奇偶,顺序递增,每隔1~2个月发布一个稳定版本。“EXTRAVERSION”扩展版本号,不分奇偶,顺序递增。

1.2 Linux内核启动过程概述

一个嵌入式Linux系统从软件角度看可以分为四个部分:引导加载程序(Bootloader),Linux内核,根文件系统,应用程序。 

其中Bootloader是系统启动或复位以后执行的第一段代码,它主要用来初始化处理器及外设,然后调用Linux内核,linux内核在完成系统的初始化之后需要挂载某个文件系统作为根文件系统(rootfs),根文件系统是linux系统的核心组成部分,也是内核启动时首先要挂载的就是跟文件系统。它可以作为linux系统中文件和数据的存储区域,通常它还包括系统配置文件和运行应用程序需要的库,保证了人和内核的信息交互。应用程序则是嵌入式系统所要达到的目标,一切的准备都是为了实现程序。若是没有应用程序,硬件上再精良嵌入式系统都没有实用意义。

Bootloader在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。Bootloader在它完成CPU和相关硬件的初始化之后,再将操作系统映像或固化的嵌入式应用程序装在到内存中然后跳转到操作系统所在的空间,启动操作系统运行它最根本的功能就是为了启动linux内核.为了启动内核Bootloader要依次完成初始化RAM初始化串口检测处理器类型设置Linux启动参数调用LInux内核镜像。

文件系统Linux文件系统中的文件是数据的集合,文件系统不仅包含着文件中的数据而且还有文件系统的结构,所有Linux用户和程序看到的文件、目录、软连接及文件保护信息等都存储在其中。


2. Linux内核移植与启动

2.1下载并解压缩linux-3.0.tar.bz2 源代码
首先到 Linux Kernel 官方网站www.kernel.org下载得到 linux-3.0.tar.bz2 文件,把它放到一个你有权限读写的文件夹,运行下面命令解压缩并进入解压后的文件夹:
[zzx@localhost kernel]$ tar -xjf linux-3.0.tar.bz2

[zzx@localhost kernel]$ cd linux-3.0


2.2配置内核编译参数

1. 首先修改顶层目录的Makefile文件

这里首要便是交叉编译器环境变量和选择开发平台类型,然后要将生成的zimage拷贝到当前目录,再手动增加mkimage工具来自动把Zimage生成可执行的.bin文件。
注:内核编译之后会生成两个文件。一个Image,一个Zimage。image为内核映像文件,而Zimage为内核映像压缩文件。

修改arch和交叉编译器

[zzx@localhost linux-3.0]$ vim Makefile  

  


添加zimage

 560     cp arch/arm/boot/zImage . -f
 561     mkimage -A arm -O linux -T kernel -C none -a 30008040 -n "Linux Kernel" -     d zImage linuxrom-s3c2440.bin
 562     rm -f zImage 

 

其中的mkimage在编译好的u-boot下的tools中,找到mkimage,将其放到/bin目录下。


修改distclean添加删除linuxrom-s3c2440.bin

 

下面是补丁文件。



2.  修改晶振频率

因为自己使用的是FL2440的开发板,根据datesheet可知晶振频率为12M。所以下一步修改晶振频率。

[zzx@localhost linux-3.0]$ vim arch/arm/mach-s3c2440/mach-smdk2440.c 



3. 修改驱动的串口

[zzx@localhost linux-3.0]$ vim drivers/tty/serial/samsung.c 

  


4.参考mini2440.config文件作为我们的默认.config文件

 


5.切换vt100模式,make menuconfig进行内核配置

打开文本窗口来选定即将要编译的模块,保存后会将里面的信息保存到同时生成的.config配置文件中。
另外:make menuconfig时出来的文本窗口中的信息是通过读取在内核源代码目录下的Kconfig文件来配置的。根据Makefile文件来编译,而Makefile文件中的变量则通过.config来进行赋值操作。仅仅在Kconfig中添加选项。

[zzx@localhost linux-3.0]$ vt100

[zzx@localhost linux-3.0]$ make menuconfig

  

 

 


6.修改MACHINE ID设备编号

同样因为是做FL2440的内核,所以我们选择三星SMDK2440这个开发板。因为我U-boot中使用的machine id为1999,而且自己使用的是s3c2440这个板子。将mini2440和和s3c2440的machineID对调。

[zzx@localhost linux-3.0]$ vim arch/arm/tools/mach-types

 

 


7.对nandflash进行分区

[zzx@localhost linux-3.0]$ vim arch/arm/plat-s3c24xx/common-smdk.c 


板子上nandflash是256M,给bootloader1M用来启动,内核15M以及48M的根文件系统。其他自由分配。

注意:此时若要再进行分区就要选择相应的文件系统。Nandflash比较常用的有yaffs2,ubifs,cramfs以及initramfs等等。


8.make

[zzx@localhost linux-3.0]$ make

。。。。。。。


此时能编译成功,但是生成的linux-s3c2440.bin文件依旧不能在板子上跑起来。因为内核启动时首先挂载的是根文件系统,我们还没有做根文件系统给予内核支持,系统会出错而退出启动。根文件系统制作请看接下来的几篇。

OK!!!


2015-10-24 00:17:25 sanshiguan 阅读数 479

        内核移植

1下载内核为linux-3.0版本的内核,解压缩 下载网址:www.kenel.org

2进入liunx-3.0更改makefile 1cup改为arm2)把交叉编译器改为自己的



3修改这个地方,生成内核的bin文件


 

4修改distclean


@符号表示 不显示命令本身

5在对源代码进行修改在arch/arm/mach-s3c2440/mach-smdks3c2440.c这个c文件进行修改将晶 振改为12MHz



6对串口的驱动进行修改,文件路径名字

修改的地方是,改为:


在下面的结构体中把.dricername =ttys

7make menuconfig我们参考是arch/arm/configs/mini2440_defconfig文件 将这个文件拷贝到过来并名为为.config

cp arch/arm/configs/mini2440_deconfig  .config

8将默认的mini2440修改为smdk2440




 

8;改设备的编号 u-boot里边使用的是1999 要把s3c240的改为1999 mini2440的改为s3c2440

修改的路径和文件:

s3c244改为1999


 

再把mini 2440的修改为362


 

 

 

修改之后make

 

 

make结束的时候并没有成功而是提醒出现了错误

/bin/sh: mkimage: command not found

百度之后  解决方法将u-boot   t ools文件夹下mkimage放在、usr/bin

最后make成功!!把生成的linuxrom.bin烧录到自己的fl2440的开发板上就大功告成!注意此时并没有将文件系统添加上去!!

 

 

 

 

 

 

 

 

 

 

 

9  接下来把生的linuxrom-s3c2440.bin放到自己的开发板上

  (1)把linuxrom-s3c2440.bin放在自己的/tftp目录下

  (2)开发板开机把linuxrom-s3c2440.bin下载到30008000的位置tftp 30008000 linuxrom-s3c2440.binbootm 30008000