3.2.1 Linux 内存绑定在局部存储器的实现总体步骤
总体步骤:
l 采用方案三,在在原来分析的基础上,以及已知Linux系统内存的初始化的情况,对内核代码进行修改,主要包括确定新区的范围,建立新区,重新对分配内存的分配机制进行设置。
l 新区划分后,对新建的两个区进行一定程度上的延迟;
l 建立系统调用,系统调用将提供用户进行手动设置访问方式。
l 对内核进行配置,并进行相关调式。
l 用户程序进行最终的测试,并验证相关结论。
3.2.2 Linux 内核绑定在局部存储器代码实现
(一)、修改代码:
1) 在init/main.c这个文件中,在这一段代码
#ifdef CONFIG_X86_LOCAL_APIC
#include <asm/smp.h>
#endif
的下面增加这段代码:
unsigned long max_normal_low_pfn=0;
EXPORT_SYMBOL(max_normal_low_pfn);
这里使用EXPORT_SYMBOL是声明max_normal_low_pfn是全局变量,所有的文件都可以使用,它的用处是为了标记新区ZONE_NORMAL_LOW的最大可用的页框号。
2) arch/x86/kernel/setup.c这个文件:
l 在struct boot_params boot_params;
#endif之后添加,声明该变量已经在别的文件有定义了
extern unsigned long max_normal_low_pfn;
l 在setup_arch()这个函数中,在find_low_pfn_range()这个之后,增加这一句:
max_normal_low_pfn = max_low_pfn/2;
该句用于计算新区(ZONE_NORMAL_LOW)的最大可用页框号。
3) 在arch/x86/mm/init_32.c文件
l 在unsigned long max_low_pfn_mapped; unsigned long max_pfn_mapped;之后添加extern unsigned long max_normal_low_pfn;
l 在static void __init zone_sizes_init(void)这个函数中增加这句代码
max_zone_pfns[ZONE_NORMAL_LOW]=max_normal_low_pfn;最好放在max_zone_pfns[ZONE_NORMAL]=max_low_pfn; max_zone_pfns是用于记录管理区的 开始和最后的页框号。
4) 在include/linux/mmzone.h这个文件中
l 在enum zone_type这个枚举类型中,一定要在
#endif
ZONE_NORMAL,一定要在两个之间添加ZONE_NORMAL_LOW,这一句。
在这里添加,该文件中所有的关于管理区的定义类型是在zone_type这个枚举类型中,使用的是枚举类中是有关变量的默认值,如无定义,是从0开始的,如果有定义,就从有赋值的那个变量依次增加。
l 并找到这段代码,修改
#if MAX_NR_ZONES < 2
#define ZONES_SHIFT 0
#elif MAX_NR_ZONES <= 2
#define ZONES_SHIFT 1
#elif MAX_NR_ZONES <= 4
#define ZONES_SHIFT 2
在这里增添这句代码: /*这是在使用相关位进行管理区的标志*/
#elif MAX_NR_ZONES <= 8
#define ZONES_SHIFT 3
#else
#error ZONES_SHIFT -- too many zones configured adjust calculation
#endif
l 在mm/page_alloc.c这个文件
在#include "internal.h"的下面增加这几句:
static int zone_flag=0;
static int task_mode=0;
static unsigned long total_number=1000000;
task_mode是用于标志在那种工作方式的,如果为0,则按照原来的策略分配管理 区,如果为1,则是通过设置方式,当zone_flag=0时,分配到新区中的ZONE_NORMAL_LOW;如果zone_flag=1时,分配到新区中的ZONE_NORMAL;total_number是用于延迟的数据。
l 把sysctl_lowmem_reserve_ratio和const zone_names这两个数组修改成:
int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES-1] = {
#ifdef CONFIG_ZONE_DMA
256,
#endif
#ifdef CONFIG_HIGHMEM
32,
#endif
32,
32,
};
static char * const zone_names[MAX_NR_ZONES] = {
#ifdef CONFIG_ZONE_DMA
"DMA",
#endif
#ifdef CONFIG_ZONE_DMA32
"DMA32",
#endif
"Normal_Low",
"Normal",
#ifdef CONFIG_HIGHMEM
"HighMem",
#endif
"Movable",
};
这里主要是对管理区的各个名称重新定义,但同时需要注意,给这些名称的定义与zone_type想对应,不然的话,在某些地方,它采用的是我们不知道的某种方式进行访问的时候,就会出错,结果在启动的时候,启动不了,显示出现在的堆栈段错误信息。
l 把__rmqueue_smallest函数修改成:
static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order,i;
struct free_area * area;
struct page *page;
/* Find a page of the appropriate size in the preferred list */
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
if (list_empty(&area->free_list[migratetype]))
continue;
page = list_entry(area->free_list[migratetype].next,
struct page, lru);
list_del(&page->lru);
rmv_page_order(page);
area->nr_free--;
__mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
expand(zone, page, order, current_order, area, migratetype);
if(task_mode==0)
{
printk("alloc page pfn=%lu with %lu pages base on %s,start_pfn=%lu task_mode=%d/n",(long unsigned int)page_to_pfn(page),(1UL<<(long unsigned int)order),zone->name,zone->zone_start_pfn,task_mode);
}
else if(task_mode==1)
{
if(zone_flag==0)
{
for(i=0; i<total_number; i++) ;
// printk("This is in test memory!/n");
}
printk("alloc page pfn=%lu with %lu pages base on %s,start_pfn=%lu task_mode=1 zone_flag=%d/n",(long unsigned int)page_to_pfn(page),(1UL<<(long unsigned int)order),zone->name,zone->zone_start_pfn,zone_flag);
}
return page;
}
return NULL;
}
在这个函数中,主要增加了对检查系统访问方式,如果task_mode==1的话,就判断采用新的设置方式,否则,还是随机方式,设置后,再判断要在那个新区进行物理内存的分配,如果是ZONE_NORMAL_LOW,将进行延迟操作,以区分这两个新区,达到最后模拟的效果。
l 在__alloc_pages_internal这个函数之前增加这几个函数,用于系统调用访问本文件的变量。
void set_flag(int value1,int value2)
{
task_mode = value1;
zone_flag = value2;
}
void clean_flag(int value)
{
task_mode = value;
}
void change_total_number(int value)
{
total_number = value;
}
其中set_flag是用于设置访问管理区的工作方式,而工作方式的选择在5)中已做解释,
同理clean_flag用于清除管理区的工作方式的标志。
change_total_number是用于修改total_number的值,该值是用于延迟管理区。
l 修改__alloc_pages_internal()这个函数,在
If (should_fail_alloc_page(gfp_mask, order))
return NULL; 之后添加这段代码:
if(task_mode==0)
{
if(high_zoneidx == ZONE_NORMAL&&zone_flag==0)
{
high_zoneidx = ZONE_NORMAL_LOW;
zone_flag=1;
}
else if(high_zoneidx == ZONE_NORMAL&&zone_flag==1)
{
zone_flag=0;
}
}
else if(task_mode==1)
{
printk("max_normal_low_pfn = %lu,max_low_pfn = %lu and max_pfn=%lu, task_mode=%d zone_flag=%d total_number=%ld/n",max_normal_low_pfn,max_low_pfn,max_pfn,task_mode,zone_flag,total_number);
if(zone_flag==0)
{
high_zoneidx = ZONE_NORMAL_LOW;
for(i=0; i<total_number; i++)
{
if(i==(total_number-1))
printk("OK! i=%d/n",i);
}
}
else if(zone_flag==1)
{
high_zoneidx = ZONE_NORMAL;
}
}
关于这段代码的功能,在上面讲述task_mode和zone_flag中已经做了解释。这里就不重述。
3.2.3 新增系统调用
(1)系统调用:
linux内核中设置了一组用于实现系统功能的子程序,叫做系统调用。
系统调用号:标识系统调用,在系统中,其值只能是唯一。
系统调用处理程序:接收用户态的信息,并调用其服务例程。
系统调用服务例程:内核态进程,实现系统调用的功能。
(2)系统调用的意义
Linux系统调用函数与通常的库函数用法是非常接相似的。都是提供使用的接口,不过系统的调用是由操作系统核心提供的,是核心态进程,而通常的库函数是由函数库和用户自己构建的,运行于用户态。
一般,用户进程是不可访问内核,读取内核数据或者修改内核数据的。如果那样的话,整个系统的设计是不可以取的。当应用程序要访问硬件设备和其他的操作系统的资源时,这个时候,对于用户进程是不能使用的,所以在操作系统中,内核提供了一些接口,该接口可以访问上述所需要的资源。这中接口专门提供给应用程序使用的,使之能够访问内核空间或调用相关函数,实现所需要的功能。
这其实也可以理解为在用户和硬件之间再加上一层中间层,该层向上提供用户空间进程的接口,向下发送信息到硬件。这样做的好处有以下几个方面:
l 提供系统的安全性和稳定性
l 避免恶意的应用程序对系统的攻击
l 提供应用程序的接口,但应用程序不知道系统调用对内核的处理,这个就形成很好的封装。
(3)系统调用的执行过程
当用户空间进程调用系统调用时,CPU会切换到内核态,并开始处理内核函数。
在80x86体系结构中,提供了两种方式进行调用系统调用。
l 一种是使用INT $0x80这个端口进入汇编语言的入口地址;
l 第二种使用的是system_call()这个函数。
相对应的系统调用的退出方式有:
l 执行iret这条汇编指令返回到原来的状态(类似与return)
系统调用的执行过程:
l 在内核态的堆栈中保留寄存器的信息,实现入栈功能(中断中的保留现场)
l 调用系统服务例程的相关c函数来处理系统调用,实现其系统功能
l 退出系统调用的处理程序,并把原来保留在内核栈中的信息恢复(中断中的恢复现场),然后由内核态返回用户态
图1.10 系统调用的执行过程
(4)建立系统调用
建立新的系统调用,对系统调用的入口方式采用的是system_call()这个函数,这个函数接收系统调用号和参数,提供6种的参数方式(最多可以传入6个参数),然后它自动会根据你所传入的参数进行选择对应了6中参数方式的那种,并根据系统调用号,进入系统服务例程。
下面是关于建立系统调用,该系统名称分别为set_memory_flag和clean_memory_flag,主要有以下步骤:
l 修改arch/x86/include/asm/unistd_32.h,分别增加了333和334这个两个系统调用号,最后写成的系统调用名是set_memory_falg和clean_memory_flag:
图:1.10 增加系统调用号
l 修改arch/86/kernel/syscall_table_32.S,增加sys_creat_syscall的中断向量处理函数入口,分别增加sys_set_memory_flag和sys_clean_memroy_flag。
图1.11 中断向量处理函数入口
l 在arch/x86/kernel/sys_i386_32.c中增添sys_set_memory_flag和sys_clean_memrory_falg的系统服务例程。具体如下:
extern void set_flag(int value1,int value2);
asmlinkage int sys_set_memory_flag(int value1,int value2)
{
set_flag(value1,value2);
return 0;
}
extern void clean_flag(int value);
asmlinkage int sys_clean_memory_flag(int value)
{
clean_flag(value);
return 0;
}
extern void change_total_number(int value);
asmlinkage int sys_total_number(int value)
{
change_total_number(value);
return 0;
}
注意:在写系统服务例程时,如果不在是arch/x86/kernel/sys_i386_32.c中的函数或者是变量的话,要通过其他方式才能够访问。如果是函数或全局变量,只需在sys_i386_32.c标识函数或者全局变量已经在别的文件中了;如果是局部变量,别的文件是不能直接访问的,所以在那个文件中在增加对这些变量的处理,形成封装,然后在要对该变量访问的文件声明extern即可。
如何定义整个系统的全局变量,而不是只对于当前文件的全局变量。只须定义新变量(不能是static变量),然后EXPORT_SYMBOL(变量)既可;
上面的服务例程就采用了封装机制,在page_alloc.c中使用函数set_flag对task_mode,zone_flag进行封装,在arch/x86/kernel/sys_i386_32.c中要改变task_mode和zone_flag,对set_flag声明为extern即可。
3.2.4 Linux内核的配置
Linux内核的编译的主要步骤
1) 从www.kernel.org这里找到新的内核版本,这里我们使用的是linux-2.6.28这个内核版本,找到linux-2.6.28.tar.gz这个文件,放到/usr/src/这个目录下面,并解压到当前目录,进入到/usr/src/linux-2.6.28/这个目录。
2) 生成Makefile文件,这个可以通过内核提供的功能去生成,主要有:
make menuconfig: 纯文本模式,不用启动X Window,还可以远程登陆,进行内核参数的选择;
make xconfig: 利用X Window的功能来进行选择,是图形界面;
make gconfig: 利用GDK函数库德图形界面进行内核参数的选择;
这里我使用的是make menuconfig进行内核参数的选择,在/usr/src/linux-2.6.28/下执行该命令(make menuconfig),关于内核参数的选择,可以参考[15],这里不做说明。
3) 接下来进行编译,执行make命令即可;(会等很长的时间)
4) 执行make bzImage生成内核的主要文件;
5) 执行make modules命令,这个命令生成的内核所需要的内核模块;
6) 把生成的内核模块安装到/lib/modules/这个目录,执行make modules_install;
注意:如果你是重新编译已有的内核,这个时候你最好把/lib/modules/2.6.28这个目录删除或者进行重命名,否则会出错。
7) 命令: mkinitrd –f /boot/initrd-2.6.28 2.6.28
生成initrd文件,这是启动时存在于内存的文件系统。Initrd的最初目的是为了把kernel的启动分成两个阶段:在内核中保留最少的最基本的启动代码,然后把对各种各样硬件设备的支持以模块的方式放在initrd中,这样就在启动中从initrd所mount的根文件系统中装载需要的模块。Mkinitrd –f是为了如果在/boot中已经存在上次的initrd,所要操作的是强制覆盖。
8) 把生成的bzImage和System.map拷贝到/boot这个目录下,具体如下:
cp /usr/src/linux-2.6.28/arch/x86/boot/bzImage /boot/vmlinuz-2.6.28
(注:x86是针对我自己的机器,对各个机器的路径不同,要看具体情况)
9) 修改/boot/grub/menu.lst,增添如下:
图1.3 menu.lst添加新内核
因为在本次设计中多次编译内核,所以把上述的步骤写成了shell脚本,具体可参考另外提供的shell文件。在usr/src/linux-2.6.28这个目录下创建名make.sh的shell脚本,具体代码如下:
make
make bzImage
make modules
rm -rf /lib/modules/2.6.28
make modules_install
mkinitrd -f /boot/vmlinuz-2.6.28 2.6.28
cp -f arch/x86/boot/bzImage /boot/vmlinuz-2.6.28
cp -f System.map /boot/System.map-2.6.28
注意:创建的make.sh的权限需要可执行的,不然执行chmod 777 make.sh即可。
3.2.5 仿真结果的性能测试
l 测试程序如下:
#include<stdio.h>
#include<stdlib.h>
#include<linux/unistd.h>
#include<time.h>
#define __NR_set_memory_flag 333 /*set_memory_flag系统调用号*/
#define __NR_clean_memory_flag 334 /*clean_memory_flag系统调用号*/
/*set_memory_flag这个函数,接收两个参数,并调用系统调用,syscall(),里面的参数顺序是调用号,参数1,参数2*/
int set_memory_flag(int value1,int value2)
{
printf("task_mode=%d and zone_flag=%d/n",value1,value2);
return syscall(__NR_set_memory_flag,value1,value2);
}
/*clean_memory_flag将是对系统调用的封装,只是传入系统调用号,和参数1,用于清除管理区标志*/
int clean_memory_flag(int value)
{
return syscall(__NR_clean_memory_flag,value);
}
/*接收命令终端的输入参数,只接收三个参数,如果不是的话,就输入错误信息*/
int main(int argc,char *argv[])
{
int i,j;
int *p;
clock_t start,end;
if(argc!=3)
{
printf("Input Error!");
return 0;
}
start=clock(); /*开始时间*/
/*调用这个set_memrory_flag,这个函数中将使用系统调用进行有关内核参数的设置,改变管理区的分配方式。*/
set_memory_flag(argv[1][0]-'0',argv[2][0]-'0');
sleep(5);
/*申请分配空间*/
for(i=0; i<2000; i++)
{
p=(int *)malloc(sizeof(int)*1024*1024*256);
if(p==NULL)
{
printf("ERROR!/n");
return -1;
}
free(p);
}
/*清除管理中的标志,该标志的表示方式详见上述对task_mode和zone_flag的解释*/
clean_memory_flag(0);
end = clock();
/输入进行运行的时间*/
printf("Execution time is %lf s./n",(double)(end-start)/CLOCKS_PER_SEC);
return 0;
}
注明:查看管理区的状态,是通过查看/var/log/messages这个日志文件里面的信息,这是因为我们修改内核代码过程中,输出有关内核信息出来,而这些信息是写入到这个文件的。而查看管理页的状态这个过程是动态的,所以可以通过使用这个命令动态的查看:tail –f /var/log/messages。当然你也可以使用echo > /var/log/messages对这个文件进行清空,以防止文件过大。
l 运行前管理区的状态:
May 9 14:05:57 localhost kernel: alloc page pfn=476042 with 1 pages base on HighMem,start_pfn=24096 task_mode=0
May 9 14:05:57 localhost kernel: alloc page pfn=102357 with 1 pages base on Normal_Low,start_pfn=4096 task_mode=0
May 9 14:05:58 localhost kernel: alloc page pfn=102384 with 1 pages base on Normal_Low,start_pfn=4096 task_mode=0
May 9 14:05:58 localhost kernel: alloc page pfn=213676 with 1 pages base on Normal,start_pfn=113151 task_mode=0
May 9 14:05:58 localhost kernel: alloc page pfn=213677 with 1 pages base on Normal,start_pfn=113151 task_mode=0
对上面输出结果的解释:pfn=213677就是分配的物理页面的页框号为213677,而且只分配1个页面,这个页框号是在Normal这个管理区分配的,start_pfn就是代表每个管理区的起始页框号,task_mode=0代表的是工作方式,如果task_mode=0的话,就按照原来的分配方案进行管理区的分配,如果task_mode=1,分配管理区则按着本文设计中的方案进行那个分配,当zone_flag=0就在ZONE_NORMAL_LOW这个区中分配,当zone_flag=1时在ZONE_NORMAL中分配。
l 运行结果1:
[root@localhost sys_call]# ./allocate 1 0
task_mode=1 and zone_flag=0
Execution time is 3.890000 s.
l 运行过程中管理区的状态:
May 9 18:15:32 localhost kernel: alloc page pfn=89597 with 1 pages base on Normal_Low,start_pfn=4096 task_mode=1 zone_flag=0
May 9 18:15:32 localhost kernel: alloc page pfn=89598 with 1 pages base on Normal_Low,start_pfn=4096 task_mode=1 zone_flag=0
l 运行结果2:
[root@localhost sys_call]# ./allocate 1 1
task_mode=1 and zone_flag=1
Execution time is 0.040000 s.
l 运行过程中管理区的状态:
May 9 18:18:11 localhost kernel: alloc page pfn=216895 with 1 pages base on Normal,start_pfn=113151 task_mode=1 zone_flag=1
May 9 18:18:11 localhost kernel: alloc page pfn=203688 with 1 pages base on Normal,start_pfn=113151 task_mode=1 zone_flag=1
多次数据记录情况 表1:
| 访问ZONE_NORMAL_LOW的时间 (s) | 访问ZONE_NORMAL的时间(s) |
1 | 3.930000 | 0.050000 |
2 | 3.880000 | 0.040000 |
3 | 3.900000 | 0.050000 |
4 | 3.880000 | 0.050000 |
5 | 3.890000 | 0.040000 |
6 | 3.890000 | 0.040000 |
7 | 3.890000 | 0.040000 |
8 | 3.890000 | 0.040000 |
9 | 3.880000 | 0.040000 |
10 | 3.900000 | 0.040000 |
11 | 3.900000 | 0.040000 |
12 | 3.900000 | 0.040000 |
13 | 3.900000 | 0.050000 |
14 | 3.900000 | 0.040000 |
15 | 3.910000 | 0.050000 |
16 | 3.920000 | 0.050000 |
17 | 3.890000 | 0.040000 |
18 | 3.900000 | 0.040000 |
19 | 3.900000 | 0.050000 |
20 | 3.900000 | 0.040000 |
平均时间 | 3.897500 | 0.043500 |
从上表中可知,访问的时间上有明显的差异,所以仿真过程中,访问远程(ZONE_NORMAL_LOW)的要比本地的(ZONE_NORMAL)的慢,该仿真的测试结果符合要求。