回到start_kernel,559行,boot_cpu_init函数,跟start_kernel位于同一文件:
494static void __init boot_cpu_init(void) 495{ 496 int cpu = smp_processor_id(); 497 /* Mark the boot cpu "present", "online" etc for SMP and UP case */ 498 set_cpu_online(cpu, true); 499 set_cpu_active(cpu, true); 500 set_cpu_present(cpu, true); 501 set_cpu_possible(cpu, true); 502} |
496行,第一次见到smp_processor_id宏。由于没有配置CONFIG_DEBUG_PREEMPT,所以其等价于调用宏raw_smp_processor_id,其意义在于SMP的情况下,获得当前CPU的ID。如果不是SMP,那么就返回0。那么在CONFIG_X86_32_SMP的情况下:
#define raw_smp_processor_id() (percpu_read(cpu_number))
千万要注意,这里cpu_number来自arch/x86/kernel/setup_percpu.c的30行:
30DEFINE_PER_CPU(int, cpu_number);
31EXPORT_PER_CPU_SYMBOL(cpu_number);
这个东西不像是c语言全局变量,而是通过两个宏来定义的。要读懂这两个宏,必须对每CPU变量这个概念非常了解,如果还不是很清楚的同学请查阅一下博客“每CPU变量”http://blog.csdn.net/yunsongice/archive/2010/05/18/5605239.aspx。
其中DEFINE_PER_CPU来自文件include/linux/percpu-defs.h:
#define DEFINE_PER_CPU(type, name) /
DEFINE_PER_CPU_SECTION(type, name, "")
该宏静态分配一个每CPU数组,数组名为name,结构类型为type。由于我们没有设置CONFIG_DEBUG_FORCE_WEAK_PER_CPU编译选项,所以DEFINE_PER_CPU_SECTION又被定义为:
#define DEFINE_PER_CPU_SECTION(type, name, sec) /
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES /
__typeof__(type) name
其中,__PCPU_ATTRS(sec)在include/linux/percpu-defs.h中定义:
#define __PCPU_ATTRS(sec) /
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) /
PER_CPU_ATTRIBUTES
__percpu是个编译扩展类型,大家可以去看看include/linux/compile.h这个文件,里面的__percpu是空的。而传进来的sec也是空的,PER_CPU_ATTRIBUTES也是空的,而上面PER_CPU_DEF_ATTRIBUTES还是空代码,可能都是留给将来内核代码扩展之用的吧,所以DEFINE_PER_CPU(int, cpu_number)展开就是:
__attribute__((section(PER_CPU_BASE_SECTION))) /
__typeof__(int) cpu_number
所以现在只关注PER_CPU_BASE_SECTION,来自include/asm-generic/percpu.h
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data.percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
好了,介绍一下GCC的一些扩展,首先如何使用typeof来支持一些“genericprogramming”。gcc支持一种叫做类型识别的技术,通过typeof(x)关键字,获得x的数据类型。而如果是在一个要被一些c文件包含的头文件中获得变量的数据类型,就需要用__typeof__而不是typeof关键字了,比如说我们这里。最后,这里就是声明一个int类型的cpu_number指针,编译的时候把他指向.data.percpu段的开始位置。
当然,我们自己写程序的时候没有这么地generic programming,所以没必要这么做,不过想要成为一个内核达人,掌握这些技术还是很有必要的。DEFINE_PER_CPU_SECTION宏又是什么意思呢?定义在arch/x86/kernel/percpu-defs.h的147行:
#define EXPORT_PER_CPU_SYMBOL(var) EXPORT_SYMBOL(var)
EXPORT_SYMBOL是什么东西啊?还记得我们在编译内核时有个kallsyms目标么?这里就用到了。所以我说过,内核的编译过程很重要,请大家务必掌握!这个对象对应于“/proc/kallsyms”文件,该文件对应着内核符号表,记录了符号以及符号所在的内存地址。模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名)
导出的符号可以被其他模块使用,不过使用之前一定要声明一下。EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。所以,EXPORT_SYMBOL(cpu_number)就是把cpu_number变量声明出去。接下来重点介绍一下percpu_read这个宏,来自arch/x86/include/asm/percpu.h:
#define percpu_read(var) percpu_from_op("mov", var, "m" (var))
看到mov,我们就知道可能要调用汇编语言了,所以这个宏调用percpu_from_op("mov", cpu_number, "m" (cpu_number)),这个宏位于同一个文件:
164#define percpu_from_op(op, var, constraint) / 165({ / 166 typeof(var) pfo_ret__; / 167 switch (sizeof(var)) { / 168 case 1: / 169 asm(op "b "__percpu_arg(1)",%0" / 170 : "=q" (pfo_ret__) / 171 : constraint); / 172 break; / 173 case 2: / 174 asm(op "w "__percpu_arg(1)",%0" / 175 : "=r" (pfo_ret__) / 176 : constraint); / 177 break; / 178 case 4: / 179 asm(op "l "__percpu_arg(1)",%0" / 180 : "=r" (pfo_ret__) / 181 : constraint); / 182 break; / 183 case 8: / 184 asm(op "q "__percpu_arg(1)",%0" / 185 : "=r" (pfo_ret__) / 186 : constraint); / 187 break; / 188 default: __bad_percpu_size(); / 189 } / 190 pfo_ret__; / 191}) |
当然,我们为了获得CPU号,cpu_number用一个字节就够了,所以上面代码翻译过来就是:
asm("movb "__percpu_arg(1)",%0" /
: "=q" (pfo_ret__) /
: constraint);
其中,__percpu_arg(1)是这样定义的:
#ifdef CONFIG_SMP
#define __percpu_arg(x) "%%"__stringify(__percpu_seg)":%P" #x
#ifdef CONFIG_X86_64
#define __percpu_seg gs
#define __percpu_mov_op movq
#else
#define __percpu_seg fs
#define __percpu_mov_op movl
#endif
所以__percpu_arg(x)翻译过来就是:
"%%"__stringify(fs)":%P" #x
请注意,这是大家第一次遇到这样的定义方式,跟我们传统的C语言宏不一样了。不错,这里要学习新知识了,首先讲讲关于#和##的知识。
在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。比如__percpu_arg宏:
#define __percpu_arg(x) "%%"__stringify(__percpu_seg)":%P" #x
而x,我们传入的是1;__percpu_seg,我们传入的是fs,所以__percpu_arg展开:
%%"__stringify(fs)":%P" "1"
而##,虽然这里没有用到,但是我还是一并讲讲,供大家扩充c语言知识。##被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。比如:
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d
typedef struct record_type LINK_MULTIPLE(name,company,position,salary);
这里这个语句将展开为:
typedef struct record_type name_company_position_salary;
再来一个新知识,可变参数宏,于1999年的ISO C标准中定义,也就十年前而已。我们看到这里用到了__stringify宏,其定义是这样的:
#define __stringify(x...) __stringify_1(x)
其中…为可变参数,什么意思?就是这个宏除x以外,还可以有额外的多个参数。另外,如果是可变参数宏,那么可变参数x可能会展开为多个参数,那么比如上面也可定义为:
#define __stringify(...) my__stringify(y,z) 0
不过如果指定了参数名x,则后者必须包含一个x:
#define __stringify(x...) my__stringify(x,y,z)
那么我要问了,如果我定义成上面三个参数的形式,但是给的却又只有两个参数怎么办?比如执行一个__stringify(a,b)语句,会不会出错?当然会!因为既然是可变参数的宏,那么传递进去一个空参数也是对的啊,只不过你得有多余的逗号啊,也就是__stringify(,a,b)这样。
不过GCC还有一个东西可以避免这样的“bug”,那就是##符号。如果我们定义成:
#define __stringify(x...) my__stringify(##x,y,z)
这时,##这个连接符号充当的作用就是当x为空的时候,消除前面的那个逗号,这样就不会有上面的bug了。
所以宏:
#define __stringify_1(x...) #x
我们就可以看懂了吧,
所以__percpu_arg(1)最终翻译过来就是:
"%%" "fs:%P" "1"
因此上边的汇编代码翻译过来就是:
asm("movb %%fs:%P1, %0" /
: "=q" (pfo_ret__) /
: "m" (var));
其中pfo_ret__是输出部%0,q,表示寄存器eax、ebx、ecx或edx中的一个,并且变量pfo_ret__存放在这个寄存器中。var就是刚才我们建立的那个临时的汇编变量cpu_number,作为输入部%1。
还记得“加载全局/中断描述符表”中把__KERNEL_PERCPU段选择子赋给了fs了吗,不错,fs: cpu_number就获得了当前存放在__KERNEL_PERCPU段中cpu_number偏移的内存中,最后把结果返回给pfo_ret__。所以这个宏最后的结果就是pfo_ret__的值,其返回的是CPU的编号,把它赋给boot_cpu_init函数的内部变量cpu。
那么这个偏移cpu_number到底是多少呢?众里寻他千百度,猛回首,这个偏移在生成的vmlinux.lds文件的504行被我发现了:
__per_cpu_load = .; .data.percpu 0
不错,刚才.data.percpu处被编译成0,所以内部cpu的值就是0。我为什么把这一部分讲得这么详细呢,因为这部分内容包含了很多内核代码的细节,以后遇到同样的问题我们就照这样的方法来分析,举一反三。随后的四个函数set_cpu_online、set_cpu_active、set_cpu_present和set_cpu_possible我不想多说了,就是激活当前CPU的cpu_present_bits中四个标志位online、active、present和possible,感兴趣的同学可以通过上面学到的方法去详细分析。