setup_arch函数做一下平台相关的基本的初始化,先把setup_arch函数的源代码贴出来,然后再分析,代码如下:
282 void __init setup_arch(char **cmdline_p)
283 {
284 /* Initialize PROM console and command line. */
285 *cmdline_p = prom_getbootargs();
286 strcpy(boot_command_line, *cmdline_p);
287 parse_early_param();
288
289 boot_flags_init(*cmdline_p);
290 #ifdef CONFIG_EARLYFB
291 if (btext_find_display())
292 #endif
293 register_console(&prom_early_console);
294
295 if (tlb_type == hypervisor)
296 printk("ARCH: SUN4V\n");
297 else
298 printk("ARCH: SUN4U\n");
299
300 #ifdef CONFIG_DUMMY_CONSOLE
301 conswitchp = &dummy_con;
302 #endif
303
304 idprom_init();
305
306 if (!root_flags)
307 root_mountflags &= ~MS_RDONLY;
308 ROOT_DEV = old_decode_dev(root_dev);
309 #ifdef CONFIG_BLK_DEV_RAM
310 rd_image_start = ram_flags & RAMDISK_IMAGE_START_MASK;
311 rd_prompt = ((ram_flags & RAMDISK_PROMPT_FLAG) != 0);
312 rd_doload = ((ram_flags & RAMDISK_LOAD_FLAG) != 0);
313 #endif
314
315 task_thread_info(&init_task)->kregs = &fake_swapper_regs;
316
317 #ifdef CONFIG_IP_PNP
318 if (!ic_set_manually) {
319 int chosen = prom_finddevice ("/chosen");
320 u32 cl, sv, gw;
321
322 cl = prom_getintdefault (chosen, "client-ip", 0);
323 sv = prom_getintdefault (chosen, "server-ip", 0);
324 gw = prom_getintdefault (chosen, "gateway-ip", 0);
325 if (cl && sv) {
326 ic_myaddr = cl;
327 ic_servaddr = sv;
328 if (gw)
329 ic_gateway = gw;
330 #if defined(CONFIG_IP_PNP_BOOTP) || defined(CONFIG_IP_PNP_RARP)
331 ic_proto_enabled = 0;
332 #endif
333 }
334 }
335 #endif
336
337 /* Get boot processor trap_block[] setup. */
338 init_cur_cpu_trap(current_thread_info());
339
340 paging_init();
341 }
第285行,看函数的名字就知道,是调用函数prom_getbootargs得到内核的启动参数放到* cmdline_p中,我们看一看,prom_getbootargs函数。
31 prom_getbootargs(void)
32 {
33 /* This check saves us from a panic when bootfd patches args. */
34 if (bootstr_info.bootstr_valid)
35 return bootstr_info.bootstr_buf;
36 prom_getstring(prom_chosen_node, "bootargs",
37 bootstr_info.bootstr_buf, BARG_LEN);
38 bootstr_info.bootstr_valid = 1;
39 return bootstr_info.bootstr_buf;
40 }
第33行bootstr_info是一个定义来专门保存内核启动参数的变量,它第一如下:
struct {
int bootstr_len;
int bootstr_valid;
char bootstr_buf[BARG_LEN];
} bootstr_info = {
.bootstr_len = BARG_LEN,
#ifdef CONFIG_CMDLINE
.bootstr_valid = 1,
.bootstr_buf = CONFIG_CMDLINE,
#endif
};
从该变量的定义可知定义的时候已经被初始化过了,它的第一个成员是启动参数的最大长度,第二个成员是一个标志,表示当前是否已经有了有效的启动参数,如果为1,则表示已经有了,第三个成员则用来保存具体的启动参数了。从它的初始化可以看出如果定义了 CONFIG_CMDLINE ,则在这里就给它附了启动参数
我们接着看第33行,如果 bootstr_info.bootstr_valid有效,即定义了 CONFIG_CMDLINE,则直接返回内核配置选项中配置的启动参数(make menuconfig)
第36行到39行,如果内核配置时没有配置启动参数,则调用 prom_getstring函数得到bootloader即启动引导程序传递给内核的参数。它实际上是调用hv接口函数得到hv设备节点树 的 prom_chosen_node的bootargs属性,这个属性就是内核启动参数。至于hv接口,在head.S中已经分析过,在head.S中,曾调用prom_init把hv接口封装成了c语言函数,之后就 能调用 prom_getstring等相关函数了。
我们接着回到setup_arch函数,
第286行,把得到的 启动参数放到 boot_command_line变量中
第287行,调用parse_early_param()函数对启动参数做一个初步分析,这个函数,在setup_arch函数调用完成后还会在start_kernel函数中再次被调用,到时候我们再具体分析它.
第289行,这一行同样是对命令行进行分析,调用boot_flags_init函数,该函数定义如下:
126 static void __init boot_flags_init(char *commands)
127 {
128 while (*commands) {
129 /* Move to the start of the next "argument". */
130 while (*commands && *commands == ' ')
131 commands++;
132
133 /* Process any command switches, otherwise skip it. */
134 if (*commands == '\0')
135 break;
136 if (*commands == '-') {
137 commands++;
138 while (*commands && *commands != ' ')
139 process_switch(*commands++);
140 continue;
141 }
142 if (!strncmp(commands, "mem=", 4)) {
143 /*
144 * "mem=XXX[kKmM]" overrides the PROM-reported
145 * memory size.
146 */
147 cmdline_memory_size = simple_strtoul(commands + 4,
148 &commands, 0);
149 if (*commands == 'K' || *commands == 'k') {
150 cmdline_memory_size <<= 10;
151 commands++;
152 } else if (*commands=='M' || *commands=='m') {
153 cmdline_memory_size <<= 20;
154 commands++;
155 }
156 }
157 while (*commands && *commands != ' ')
158 commands++;
159 }
160 }
该函数比较简单,就不逐行分析了,第130,131行,如果碰到命令行中的空字符,则计数器加一,第136到140行,如果碰到形如”-x”这种形式的参数,则调用 process_switch,第 142到155行,取出命令行参数中指定的内存大小,即形如”mem=xxM”的参数,把指定的内存大小保存在 cmdline_memory_size变量中。
该函数只关心内核参数中的两种参数,即内存大小和形如”-x”的参数,对”-x”的参数使用
process_switch函数进行处理,该函数定义如下:
96 static void __init process_switch(char c)
97 {
98 switch (c) {
99 case 'd':
100 case 's':
101 break;
102 case 'h':
103 prom_printf("boot_flags_init: Halt!\n");
104 prom_halt();
105 break;
106 case 'p':
107 /* Just ignore, this behavior is now the default. */
108 break;
109 case 'P':
110 /* Force UltraSPARC-III P-Cache on. */
111 if (tlb_type != cheetah) {
112 printk("BOOT: Ignoring P-Cache force option.\n");
113 break;
114 }
115 cheetah_pcache_forced_on = 1;
116 add_taint(TAINT_MACHINE_CHECK);
117 cheetah_enable_pcache();
118 break;
119
120 default:
121 printk("Unknown boot switch (-%c)\n", c);
122 break;
123 }
124 }
看了这个函数,真正产生实际效果的是‘-P’参数,但是它只针对 tlb_type = cheetah的平 台,而我们的 tlb_type = hypervisor,所以这个函数对我们而言可以忽略。
显示控制台:
接着看setup_arch函数:
下面是初始化与显示器相关的显示控制台的。
第291行,初始化一个与显示器相关的控制台,这样就可以在显示器上打印信息了,当然这里的显示只能显示文本,不能显示图形,我们可以看看这个函数btext_find_display函数,该函数在arch/sparc/kernel/btext.c文件中,定义如下:
310 int __init btext_find_display(void)
311 {
312 unsigned int node;
313 char type[32];
314 int ret;
315
316 node = prom_inst2pkg(prom_stdout);
317 if (prom_getproperty(node, "device_type", type, 32) < 0)
318 return -ENODEV;
319 if (strcmp(type, "display"))
320 return -ENODEV;
321
322 ret = btext_initialize(node);
323 if (!ret) {
324 btext_clearscreen();
325 register_console(&btext_console);
326 }
327 return ret;
328 }
第316行,是prom前缀开头的函数,一看这种函数就是与OBP打交道的, 这类函数在prom_init函数后就可用了,是用来请求某个OBP服务的,这里就是根据 prom_stdout调用固件服务得到标准输出设备的节点号,prom_stdout是在prom_init函 数中赋值的,它实际上就是节点树的/chosen节点的”stdout”属性,对应的是标准输出 设备
第317行,调用OBP服务得到标准输出设备的设备类型,实际上就是设备节点 的”device_type”属性,如果没有就出错退出
第319行,判断标准输出设备是否是显示设备,如果不是,直接退出,因为这 里是要初始化显示控制台的,不显示设备就退出。
第320行,这个函数就是执行与显示相关的初始化,我们接下来看这个函数:
43 static int __init btext_initialize(unsigned int node)
44 {
45 unsigned int width, height, depth, pitch;
46 unsigned long address = 0;
47 u32 prop;
48
49 if (prom_getproperty(node, "width", (char *)&width, 4) < 0)
50 return -EINVAL;
51 if (prom_getproperty(node, "height", (char *)&height, 4) < 0)
52 return -EINVAL;
53 if (prom_getproperty(node, "depth", (char *)&depth, 4) < 0)
54 return -EINVAL;
55 pitch = width * ((depth + 7) / 8);
56
57 if (prom_getproperty(node, "linebytes", (char *)&prop, 4) >= 0 &&
58 prop != 0xffffffffu)
59 pitch = prop;
60
61 if (pitch == 1)
62 pitch = 0x1000;
63
64 if (prom_getproperty(node, "address", (char *)&prop, 4) >= 0)
65 address = prop;
66
67 /* FIXME: Add support for PCI reg properties. Right now, only
68 * reliable on macs
69 */
70 if (address == 0)
71 return -EINVAL;
72
73 g_loc_X = 0;
74 g_loc_Y = 0;
75 g_max_loc_X = width / 8;
76 g_max_loc_Y = height / 16;
77 dispDeviceBase = (unsigned char *)address;
78 dispDeviceRowBytes = pitch;
79 dispDeviceDepth = depth == 15 ? 16 : depth;
80 dispDeviceRect[0] = dispDeviceRect[1] = 0;
81 dispDeviceRect[2] = width;
82 dispDeviceRect[3] = height;
83
84 return 0;
85 }
第49到53行,分别得到显示设备的分辨率和色深;
第55到62行,得到显示一行像素点所需的字节数,第55行先通过计算得到一个初始值,然后调用OBP服务从OBP中取得,如果能够取到,则使用从OBP中得到的值,否则使用初始值。
第64行,调用OBP服务得到显示设备的’address’属性,这个属性实际上就是显示设备的frambuffer的基地址,当使用显示设备输出时,只需要把像素数据写到frambuffer地址就行了。
第73行到83行,初始化一些全局变量,当使用显示设备输出时会用到这些变量,其中第73,74行的g_loc_X ,g_loc_Y变量实际上表示的是屏幕上光标的位置,第75行,g_max_loc_X表示一行可以显示多少个字符,76行 g_max_loc_Y表示一列可以显示多少个字符,从这两个变量可以看出一个字符占有8x16个像素点, dispDeviceRect数组是用来计算给定的像素点在frambuffer中的位置的,其中 dispDeviceRect[0], dispDeviceRect[1]是第一个像素点的x,y坐标, dispDeviceRect[2], dispDeviceRect[3]是最后一个像素点的坐标。
我们回到btext_find_display函数中来,
324行,清空屏幕,btext_clearscreen函数有兴趣的自行分析,它实际上是把frambuffer空间清0
第325行,注册我们这里实现的显示控制台,该控制台定一如下:
static struct console btext_console = {
.name = "btext",
.write = btext_console_write,
.flags = CON_PRINTBUFFER | CON_ENABLED | CON_BOOT | CON_ANYTIME,
.index = 0,
};
当使用该控制台向显示设备输出字符时,会调用控制台定义中的write函数,它是在同一个文件中定义的,定义如下:
297 static void btext_console_write(struct console *con, const char *s,
298 unsigned int n)
299 {
300 btext_drawtext(s, n);
301 }
该函数调用函数btext_drawtext:
188 static void btext_drawtext(const char *c, unsigned int len)
189 {
190 while (len--)
191 btext_drawchar(*c++);
192 }
该函数调用 btext_drawchar输出字符串中的每一个字符:
141 void btext_drawchar(char c)
142 {
143 int cline = 0;
144 #ifdef NO_SCROLL
145 int x;
146 #endif
147 switch (c) {
148 case '\b':
149 if (g_loc_X > 0)
150 --g_loc_X;
151 break;
152 case '\t':
153 g_loc_X = (g_loc_X & -8) + 8;
154 break;
155 case '\r':
156 g_loc_X = 0;
157 break;
158 case '\n':
159 g_loc_X = 0;
160 g_loc_Y++;
161 cline = 1;
162 break;
163 default:
164 draw_byte(c, g_loc_X++, g_loc_Y);
165 }
166 if (g_loc_X >= g_max_loc_X) {
167 g_loc_X = 0;
168 g_loc_Y++;
169 cline = 1;
170 }
171 #ifndef NO_SCROLL
172 while (g_loc_Y >= g_max_loc_Y) {
173 scrollscreen();
174 g_loc_Y--;
175 }
176 #else
177 /* wrap around from bottom to top of screen so we don't
178 waste time scrolling each line. -- paulus. */
179 if (g_loc_Y >= g_max_loc_Y)
180 g_loc_Y = 0;
181 if (cline) {
182 for (x = 0; x < g_max_loc_X; ++x)
183 draw_byte(' ', x, g_loc_Y);
184 }
185 #endif
186 }
第148行到162行,如果是换行,回车等特殊字符的话值更新光标位置,而不把字符显示到屏幕上,
第164行,如果是常规字符,则调用draw_byte函数把字符显示到屏幕上:
194 static void draw_byte(unsigned char c, long locX, long locY)
195 {
196 unsigned char *base = calc_base(locX << 3, locY << 4);
197 unsigned char *font = &vga_font[((unsigned int)c) * 16];
198 int rb = dispDeviceRowBytes;
199
200 switch(dispDeviceDepth) {
201 case 24:
202 case 32:
203 draw_byte_32(font, (unsigned int *)base, rb);
204 break;
205 case 15:
206 case 16:
207 draw_byte_16(font, (unsigned int *)base, rb);
208 break;
209 case 8:
210 draw_byte_8(font, (unsigned int *)base, rb);
211 break;
212 }
213 }
第196行,计算要显示的像素点在frambuffer中的位置
第197行,得到要显示的字符所代表信息在vga_font数组中的位置,要理解vga数组的作用,就要对字符是怎么在屏幕上显示的有所了解了。每个字符占有8x16个像素点,每行有8个像素点,每列有16个像素点,当字符在屏幕上显示时,必须点亮这8x16个像素点中的多个像素点从而在屏幕上显示出字符,以8位色深为例,每个像素点在frambuffer占由8位即一个字节的空间,由于只显示文本,所以显示时不用考虑颜色,那么该字节中全1就代表熄灭像素点,但是点亮像素点却并非全0而是0xF0。但是要显示的字符在它的8x16个像素点中要点亮那些像素点呢,这就跟字体相关了,当然我们这里的显示控制台只能显示一种字体,每个字符在该字体下要点亮哪些像素点就可以到该字体的字体库中去查找了,这里的字体库就是我们这里的vga_font数组了,每个字符在该数组中占有8x16位的数据,即16个8字节的数据,其中这每个字节的数据中的每一位就代表每行8个像素点中的一个像素点是点亮还是熄灭,而每个字符在vga_font数组中是按照ascii编码顺序存放的。
第201到211行,根据色深来调用不同的函数把字符显示到屏幕上,我们只看其中的
draw_byte_8函数:
281 static void draw_byte_8(unsigned char *font, unsigned int *base, int rb)
282 {
283 int l, bits;
284 int fg = 0x0F0F0F0FUL;
285 int bg = 0x00000000UL;
286 unsigned int *eb = (int *)expand_bits_8;
287
288 for (l = 0; l < 16; ++l)
289 {
290 bits = *font++;
291 base[0] = (eb[bits >> 4] & fg) ^ bg;
292 base[1] = (eb[bits & 0xf] & fg) ^ bg;
293 base = (unsigned int *) ((char *)base + rb);
294 }
295 }
该函数的主体是一个for循环,该for循环的次数是16次,一看就知道每次循环就是显示该字符的每行8个像素点。
第190行,取出vga_font数组中代表该行的8个像素点的信息放到变量bits中。
第191行,从传入的参数可知,base(即要显示的像素点在framebuffer中的位置)变量是int型的指针,因此它每次能显示4个字节的信息,即每次显示4个像素点,每行8个像素点分两次显示完,即这里道德第291行和292行。每次显示4个像素点291行先显示前4个像素点,但是frambuffer里面存放的是每个像素点占有一个字节,这一个字节是全1或0xF0,那么4个字节的值是多少就可以计算出来了,但是计算出来显然比较麻烦,这里就引入了一个数组 expand_bits_8,4个像素点,其中每个像素点是点亮还是熄灭就2的4次放即16中情况,而 expand_bits_8就需要16个元素,每个元素就存放其中的一种情况,在 expand_bits_8数组中点亮像素点是按照全0来存放的,但是8位色深点量像素点却是0xF0,因此取出数组中的数据后需要进行一下处理。
第293行,把像素数据写入framebuffer中。
到这里一个像素点是怎么显示到屏幕上就解释清楚了,当然16位色深和32位色深的显示方法读者可自行分析,原理跟8位色深一样。这样draw_byte函数就分析完了。
我们再回到btext_drawchar函数中来,字符显示到屏幕上后,第166行到184行都是跟新光标位置的,其中有两中情况第166行到第169行是处理要换行的情况,后面的是要处理显示到屏幕上最后的位置要情况屏幕的情况。
到这里显示控制台就分析完了。
回到setup_arch函数中来。
我们看下一个函数idprom_init,它定义在arch/sparc/kernel/idprom.c文件中。定义如下:
88 void __init idprom_init(void)
89 {
90 prom_get_idprom((char *) &idprom_buffer, sizeof(idprom_buffer));
91
92 idprom = &idprom_buffer;
93
94 if (idprom->id_format != 0x01)
95 prom_printf("IDPROM: Warning, unknown format type!\n");
96
97 if (idprom->id_cksum != calc_idprom_cksum(idprom))
98 prom_printf("IDPROM: Warning, checksum failure (nvram=%x, calc=%x)!\n",
99 idprom->id_cksum, calc_idprom_cksum(idprom));
100
101 display_system_type(idprom->id_machtype);
102
103 printk(KERN_WARNING "Ethernet address: %pM\n", idprom->id_ethaddr);
104 }
这个函数的作用实际上是用来通过prom接口得到机器类型id的。
第90行,通过函数prom_get_idprom得到机器类型相关信息放到 idprom_buffer 结构变量中,这个函数就不具体分析了。这个函数就是取得节点树的根节的”idprom” 属性,然后放到idprom_buffer结构中。
第97行,对得到的idprom信息进行校验和校验。
第101行,显示机器类型信息。
我们接着看setup_arch函数:
第306,307行,root_flags变量在head_64.S文件中已经被初始化为1了,因此这条if分支是不会执行的。
第308行,ROOT_DEV初始化为0,ROOT_DEV是大写的,按照Linux的变量命名习惯,它看起来像是一个宏,但其实却是一个dev_t类型的变量,这个变量是个设备号,这里把它初始化为0。
第310行到312行,我们是没有定义的,不用去管。
第315行,把 init_task的thread_info结构的kregs成员赋值为&fake_swapper_regs,该成员是用来保存寄存器信息的。
第317行到335行,又是我们不用管的。
第338行,初始化当前CPU的trap_per_cpu变量,trap_per_cpu是每个virtual cpu都有的一个变量,里面保存有cpu处理trap时需要用到的一些信息。这里是把trap_per_cpu变量的thread成员置成当前进程的thread_info结构, 把pgd_paddr成员置0。
第340行,paging_init函数,该函数会为内核建立页表,并为设备资源建立设备节点树,我们看这个函数:
该函数是在文件arch/sparc/mm/init_64.c文件中定义的。
1683 void __init paging_init(void)
1684 {
1685 unsigned long end_pfn, shift, phys_base;
1686 unsigned long real_end, i;
1712
1713 kern_base = (prom_boot_mapping_phys_low >> 22UL) << 22UL;
1714 kern_size = (unsigned long)&_end - (unsigned long)KERNBASE;
1715
1716 /* Invalidate both kernel TSBs. */
1717 memset(swapper_tsb, 0x40, sizeof(swapper_tsb));
1718 #ifndef CONFIG_DEBUG_PAGEALLOC
1719 memset(swapper_4m_tsb, 0x40, sizeof(swapper_4m_tsb));
1720 #endif
1721
1722 if (tlb_type == hypervisor)
1723 sun4v_pgprot_init();
1724 else
1725 sun4u_pgprot_init();
1726
1727 if (tlb_type == cheetah_plus ||
1728 tlb_type == hypervisor)
1729 tsb_phys_patch();
1730
1731 if (tlb_type == hypervisor) {
1732 sun4v_patch_tlb_handlers();
1733 sun4v_ktsb_init();
1734 }
1735
1736 lmb_init();
1737
1746 read_obp_translations();
1747 read_obp_memory("reg", &pall[0], &pall_ents);
1748 read_obp_memory("available", &pavail[0], &pavail_ents);
1749 read_obp_memory("available", &pavail[0], &pavail_ents);
1750
1751 phys_base = 0xffffffffffffffffUL;
1752 for (i = 0; i < pavail_ents; i++) {
1753 phys_base = min(phys_base, pavail[i].phys_addr);
1754 lmb_add(pavail[i].phys_addr, pavail[i].reg_size);
1755 }
1756
1757 lmb_reserve(kern_base, kern_size);
1758
1759 find_ramdisk(phys_base);
1760
1761 lmb_enforce_memory_limit(cmdline_memory_size);
1762
1763 lmb_analyze();
1764 lmb_dump_all();
1765
1766 set_bit(0, mmu_context_bmap);
1767
1768 shift = kern_base + PAGE_OFFSET - ((unsigned long)KERNBASE);
1769
1770 real_end = (unsigned long)_end;
1771 num_kernel_image_mappings = DIV_ROUND_UP(real_end - KERNBASE, 1 << 22);
1772 printk("Kernel: Using %d locked TLB entries for main kernel image.\n",
1773 num_kernel_image_mappings);
1774
1775 /* Set kernel pgd to upper alias so physical page computations
1776 * work.
1777 */
1778 init_mm.pgd += ((shift) / (sizeof(pgd_t)));
1779
1780 memset(swapper_low_pmd_dir, 0, sizeof(swapper_low_pmd_dir));
1781
1782 /* Now can init the kernel/bad page tables. */
1783 pud_set(pud_offset(&swapper_pg_dir[0], 0),
1784 swapper_low_pmd_dir + (shift / sizeof(pgd_t)));
1785
1786 inherit_prom_mappings();
1787
1788 init_kpte_bitmap();
1789
1790 /* Ok, we can use our TLB miss and window trap handlers safely. */
1791 setup_tba();
1792
1793 __flush_tlb_all();
1794
1795 if (tlb_type == hypervisor)
1796 sun4v_ktsb_register();
1797
1798 prom_build_devicetree();
1799 of_populate_present_mask();
1800 #ifndef CONFIG_SMP
1801 of_fill_in_cpu_data();
1802 #endif
1803
1804 if (tlb_type == hypervisor) {
1805 sun4v_mdesc_init();
1806 mdesc_populate_present_mask(cpu_all_mask);
1807 #ifndef CONFIG_SMP
1808 mdesc_fill_in_cpu_data(cpu_all_mask);
1809 #endif
1810 }
1811
1812 /* Once the OF device tree and MDESC have been setup, we know
1813 * the list of possible cpus. Therefore we can allocate the
1814 * IRQ stacks.
1815 */
1816 for_each_possible_cpu(i) {
1817 /* XXX Use node local allocations... XXX */
1818 softirq_stack[i] = __va(lmb_alloc(THREAD_SIZE, THREAD_SIZE));
1819 hardirq_stack[i] = __va(lmb_alloc(THREAD_SIZE, THREAD_SIZE));
1820 }
1821
1822 /* Setup bootmem... */
1823 last_valid_pfn = end_pfn = bootmem_init(phys_base);
1824
1825 #ifndef CONFIG_NEED_MULTIPLE_NODES
1826 max_mapnr = last_valid_pfn;
1827 #endif
1828 kernel_physical_mapping_init();
1829
1830 {
1831 unsigned long max_zone_pfns[MAX_NR_ZONES];
1832
1833 memset(max_zone_pfns, 0, sizeof(max_zone_pfns));
1834
1835 max_zone_pfns[ZONE_NORMAL] = end_pfn;
1836
1837 free_area_init_nodes(max_zone_pfns);
1838 }
1839
1840 printk("Booting Linux...\n");
1841 }
建立内核页表:
第1713行,prom_boot_mapping_phys_low变量我们在head_64.S文件的分析中见过,该变量的值是内核映像的物理基地址,这里把它进行4M对齐的处理后存放在kern_base变量中。
第1714行,得到内核映像的大小。
第1717行,把swapper_tsb中的每个字节都置为0x40,swapper_tsb是在head_64.S文件中静态分配的,它的大小是32K,swapper_tsb是干什么用的呢?
CPU的每个虚拟处理器上都集成了MMU,MMU是用来把虚拟地址转换程物理地址的,具体到我们这里的sparc结构,在内核中,MMU是把虚拟地址转换程实地址的,但是为了表述的方便,在内核中我们一直把实地址称为物理地址的,实际上对内核而言,两者没区别,我们以后也还是以物理地址称之。MMU把虚拟地址转换成物理地址需要页表的帮助,即通过虚拟地址查找页表从而得到与该虚拟地址相关关联的物理地址,但是为了提高地址转换的速率,MMU实现上采用了缓存的机制,MMU在硬件上一般集成了TLB,最近最常使用的页表项都缓存在TLB上,这样地址转换时就可以直接使用TLB中缓存的表项来进行转换,当TLB没有与要转换地址相关的表项时(即TLB miss),如果直接从页表中查找相关表现再缓存到TLB中,效率仍然是不高的,因此在有些实现中,在TLB和页表直接存在这TSB,TSB也位于内存中,它可以看作是TLB与页表之间的告诉缓存,当TLB miss时,先从TSB中查找相关表现,如果发生TSB miss,再从页表中查找。当然这些机制不同的平台具有不同的具体实现,就T2处理器来说,它在内存中有多个TSB,当TLB miss时,可以同时在多个TSB中查找。我们这里不用关心T2 MMU实现的具体物理细节,因为内核中对MMU的操作都是通过hypervisor调用接口调用hypervisor代码来实现的,所以物理实现细节是hypervisor代码关心的事情。但是内核需要知道TSB表项的格式:
Bit Field Description
Tag-63:48 context_id The 16-bit context ID associated with the TTE
Tag– 47:42 — These bits must be zero for a tag match
Tag– 41:0 va 要转换的虚拟地址的63:22位
Data – 63 v 有效位,如果该位为0,则说明表项是无效的。
Data – 62 nfo 如果该位置位,则该表项所映射的物理页可以被无故障访问,无故障访问可以实现一些常规访问无法提供的功能,比如常规访问当访问空指针时就会产生一个错误,但无故障访问却可以安全的访问,利用这中特性,在程序调试和编译优化中是十分有效的,不过我们不用关心
Data – 61:56 soft2 这几位的作用由软件定义,MMU硬件是不关心的
Data – 55:13 taddr 即映射的物理地址
Data – 12 ie 大小端反转位,即如果当前系统访问内存采用大端格式,那么如果该位置位,则访问该表现所映射的页是会使用小端格式访问
Data – 11 e Side effect
Data – 10 cp cp和cv位标志该页是否可缓存(cachable)的,其中cp是cacheable-in-physically-indexed-cache
Data – 9 cv cacheable-in-virtually-
indexed-cache
Data – 8 p 如果该位置位,则只有privileged and hyperprivileged software
能够访问该页
Data – 7 ep 表示该页是否拥有可执行权限
Data – 6 w 表示该页是否是可写的
Data – 5:4 soft 这几位的作用由软件定义
Data – 3:0 size 页大小:
sz Page Size
0000 8 Kbyte
0001 64 Kbyte
0010 512 Kbyte
0011 4 Mbyte
010 32 Mbyte
0101 256 Mbyte
0110 2 Gbyte
0111 16 Gbyte
1000-111 Reserved
TSB中的每一个表项都有两个64位数据,分别是tag和data,其中tag是用来匹配的,如果某个内存访问匹配了tag字段,则它的地址转换就是通过该表现进行的,其中主要匹配两个字段即contex_id和虚拟地址的63:22位,其中contex_id是sparc体系结构中用来区分不同的虚拟地址空间的。 Data字段的内容则是TSB表项的主要目的了,它里面就存放了虚拟页所映射的物理页的物理地址,除了物理地址外,它还存放了一些所映射页的属性,包括是否可缓存,是否可写等。
这里1717行的swapper_tsb就是上面所说的TSB,不过T2中可以同时存在多个TSB,swapper_tsb是专门用来处理内核地址转换的TSB。
第1718到1720行,与debug相关的,我们不用管。
第1722到1725行,我们的系统里面走的是sun4v_pgprot_init分支,这个函数的内容我们就不贴出来了,这个函数就是初始化了一些变量,这些变量是干什么的呢,我们上面说页表项中不仅存有虚拟页所映射物理页的物理地址外,还有所映射页的访问属性。当然在不同的场合映射页具有不同的属性,比如内核应该具有可读可写可执行权限,且是可缓存的,再如系统中一些特殊的场合要求页是只读的。这里就是为这些不同场合的页属性预定义一些标志页属性的属性值放在protection_map数组中,当要为页建立页表时可直接使用。
第1729行,tsb_phys_patch()函数根据处理器的类型信息实现处理器相关的访问TSB的操作,相当于打patch,类似的情形我们在head_64.S文件的分析中遇到过,这里就不一行行分析了。
第1732行,sun4v_patch_tlb_handlers也相当于打patch,不过它实际上完成的工作是把处理器类型相关的与处理地址转换的trap的trap handler(trap处理函数)安装的privilged模式下的trap table中去。
第1733行,sun4v_ktsb_init初始化内核TSB,它定义于同一文件中:
1609 static void __init sun4v_ktsb_init(void)
1610 {
1611 unsigned long ktsb_pa;
1612
1613 /* First KTSB for PAGE_SIZE mappings. */
1614 ktsb_pa = kern_base + ((unsigned long)&swapper_tsb[0] - KERNBASE);
1615
1616 switch (PAGE_SIZE) {
1617 case 8 * 1024:
1618 default:
1619 ktsb_descr[0].pgsz_idx = HV_PGSZ_IDX_8K;
1620 ktsb_descr[0].pgsz_mask = HV_PGSZ_MASK_8K;
1621 break;
1622
1623 case 64 * 1024:
1624 ktsb_descr[0].pgsz_idx = HV_PGSZ_IDX_64K;
1625 ktsb_descr[0].pgsz_mask = HV_PGSZ_MASK_64K;
1626 break;
1627
1628 case 512 * 1024:
1629 ktsb_descr[0].pgsz_idx = HV_PGSZ_IDX_512K;
1630 ktsb_descr[0].pgsz_mask = HV_PGSZ_MASK_512K;
1631 break;
1632
1633 case 4 * 1024 * 1024:
1634 ktsb_descr[0].pgsz_idx = HV_PGSZ_IDX_4MB;
1635 ktsb_descr[0].pgsz_mask = HV_PGSZ_MASK_4MB;
1636 break;
1637 };
1638
1639 ktsb_descr[0].assoc = 1;
1640 ktsb_descr[0].num_ttes = KERNEL_TSB_NENTRIES;
1641 ktsb_descr[0].ctx_idx = 0;
1642 ktsb_descr[0].tsb_base = ktsb_pa;
1643 ktsb_descr[0].resv = 0;
1644
1659 }
可以看出这个函数并没有为 swapper_tsb的每一项赋值,而是初始化了一个struct hv_tsb_descr 结构体变量ktsb_descr,该结构是hypervisor用来配置MMU的TSB的,这个函数把内核TSB swapper_tsb与struct hv_tsb_descr结构关联起来,当初始化号内核页表后,就可以调用hypervisor服务通过struct hv_tsb_descr配置TSB了。
回到paging_init函数,
第1736行,初始化lmb,这里就要提到lmb机制了,lmb是系统启动时Linux提供的一种简单的内存管理机制,它维护了一个全局变量struct lmb lmb,里面包含有系统保留的和可用的物理内存,当然这些物理内存可能在地址上并不是连续的而是分成一片一片的,其中每一片的物理基地址和大小在lmb中都有保存,且他们是按照物理地址从小到达排序,lmb机制的实现在文件lib/lmb.c中,关于它的实现由于并不是体系结构相关的我们这里并不讲解,有兴趣的可以自行分析。这一行就是初始化lmb
第1746行,read_obp_translations函数,定义于同一文件中:
454 static void __init read_obp_translations(void)
455 {
456 int n, node, ents, first, last, i;
457
458 node = prom_finddevice("/virtual-memory");
459 n = prom_getproplen(node, "translations");
460 if (unlikely(n == 0 || n == -1)) {
461 prom_printf("prom_mappings: Couldn't get size.\n");
462 prom_halt();
463 }
464 if (unlikely(n > sizeof(prom_trans))) {
465 prom_printf("prom_mappings: Size %Zd is too big.\n", n);
466 prom_halt();
467 }
468
469 if ((n = prom_getproperty(node, "translations",
470 (char *)&prom_trans[0],
471 sizeof(prom_trans))) == -1) {
472 prom_printf("prom_mappings: Couldn't get property.\n");
473 prom_halt();
474 }
475
476 n = n / sizeof(struct linux_prom_translation);
477
478 ents = n;
479
480 sort(prom_trans, ents, sizeof(struct linux_prom_translation),
481 cmp_ptrans, NULL);
482
483 /* Now kick out all the non-OBP entries. */
484 for (i = 0; i < ents; i++) {
485 if (in_obp_range(prom_trans[i].virt))
486 break;
487 }
488 first = i;
489 for (; i < ents; i++) {
490 if (!in_obp_range(prom_trans[i].virt))
491 break;
492 }
493 last = i;
494
495 for (i = 0; i < (last - first); i++) {
496 struct linux_prom_translation *src = &prom_trans[i + first];
497 struct linux_prom_translation *dest = &prom_trans[i];
498
499 *dest = *src;
500 }
501 for (; i < ents; i++) {
502 struct linux_prom_translation *dest = &prom_trans[i];
503 dest->virt = dest->size = dest->data = 0x0UL;
504 }
505
506 prom_trans_ents = last - first;
507
508 if (tlb_type == spitfire) {
509 /* Clear diag TTE bits. */
510 for (i = 0; i < prom_trans_ents; i++)
511 prom_trans[i].data &= ~0x0003fe0000000000UL;
512 }
513 }
分析这个函数之前先解释一下这个函数的作用,在sparc体系结构下,hypervisor是运行于hyper-privilged模式下的,它负责实地址到物理地址的转换。而OBP(启动引导程序)和Linux是运行在privilged模式下的,他们使用的是虚拟地址,负责虚拟地址到实地址的转换。所以在系统启动进入Linux之前,OBP运行时也会使用虚拟地址,所以在进入Linux之前,有些物理地址已经被OBP映射到虚拟地址了,进入Linux后,这些已有的映射关系有的是不需要的,但有的需要保留,判断的标准就是映射的虚拟地址空间是否位于为OBP保留的虚拟地址范围内。
第458到475行,调用OBP服务得到OBP节点树的/virtual-memory节点的translations属性放到数组prom_trans中,这样OBP所维护的地址映射关系就保存到数组prom_trans中了。
prom_trans数组中每个元素的定义如下:
struct linux_prom_translation {
unsigned long virt;
unsigned long size;
unsigned long data;
};
其中virt成员代表映射的虚拟地址,size成员代表映射的内存块的大小,data成员代表映射的物理地址。
第476行,478行,得到prom_trans数组中元素的个数
第480行,把prom_trans数组元素排序,按其每个元素的virt成员由小到大排序。
第484行到493行,找出prom_trans数组中位于为OBP保留的虚拟地址范围内的映射关系。
第495到505行,把需要OBP保留的映射关系移到OBP保留的虚拟地址范围数组中的前几个元素中来,并把其他元素清0
第506行,把prom_trans数组的实际有效元素的个数存入变量prom_trans_ents中
回到paging_init函数中来,
第1747到1749行,这3行都是调用read_obp_memory函数,read_obp_memory函数就不贴出来一行行的分析了,看懂了上面的read_obp_translations函数,这个函数是很容易看懂的。
其中第1747行实际上是得到OBP节点树的”/memory“节点的“reg”属性放到数组pall中,第1748行和1749行得到OBP节点树的”/memory“节点的“available”属性放到数组pavail中,pall数组和pavil数组的元素都是struct linux_prom64_registers结构类型的,该结构定义如下:
struct linux_prom64_registers {
unsigned long phys_addr;
unsigned long reg_size;
};
该结构实际上就标志了一个内存块,其中第1747行通过”reg”属性得到的pall数组标志的内存块是已经被OBP或者hypervisor保留使用的,而通过“available”得到的pavail数组所标志的内存区域是Linux可以使用的内存,前面提到lmb管理的系统可用的物理内存实际上就是这里的pavail数组中的内存区域而不包括pall数组中的内存区域。
第1752到1755行,把pavail数组中的每一块内存区域都加入到lmb中去,并通过比较得到Linux可以使用的内存区域的最小物理地址保存到phys_base变量中。
1757行,把内核映像使用的内存加入的lmb维护的保留的内存中去。
第1759行,find_ramdisk函数:
697 static void __init find_ramdisk(unsigned long phys_base)
698 {
699 #ifdef CONFIG_BLK_DEV_INITRD
700 if (sparc_ramdisk_image || sparc_ramdisk_image64) {
701 unsigned long ramdisk_image;
702
703 /* Older versions of the bootloader only supported a
704 * 32-bit physical address for the ramdisk image
705 * location, stored at sparc_ramdisk_image. Newer
706 * SILO versions set sparc_ramdisk_image to zero and
707 * provide a full 64-bit physical address at
708 * sparc_ramdisk_image64.
709 */
710 ramdisk_image = sparc_ramdisk_image;
711 if (!ramdisk_image)
712 ramdisk_image = sparc_ramdisk_image64;
713
714 /* Another bootloader quirk. The bootloader normalizes
715 * the physical address to KERNBASE, so we have to
716 * factor that back out and add in the lowest valid
717 * physical page address to get the true physical address.
718 */
719 ramdisk_image -= KERNBASE;
720 ramdisk_image += phys_base;
721
722 numadbg("Found ramdisk at physical address 0x%lx, size %u\n",
723 ramdisk_image, sparc_ramdisk_size);
724
725 initrd_start = ramdisk_image;
726 initrd_end = ramdisk_image + sparc_ramdisk_size;
727
728 lmb_reserve(initrd_start, sparc_ramdisk_size);
729
730 initrd_start += PAGE_OFFSET;
731 initrd_end += PAGE_OFFSET;
732 }
733 #endif
734 }
ramdisk是一个内存文件系统镜像,它里面保存了许多系统启动时需要用到的模块,ramdisk的地址是在head_64.S文件中静态定义的,保存在变量sparc_ramdisk_image64中,
sparc_ramdisk_image64中保存的是虚拟地址。
第719,720行,根据虚拟地址计算计算处ramdisk的物理地址
第725,726行,把randisk的物理起始地址和结束地址保存在initrd_start和initrd_end变量中。
第718行,把ramdisk所使用的内存同样加入lmb所维护的保留内存中去。
第730,731行,计算处ramdisk的起始虚拟地址和结束地址更新到initrd_start和initrd_end变量中去。这里得到的虚拟地址和sparc_ramdisk_image64中保存的虚拟地址是不一样的。我们知道系统启动过程中内核所在的那段物理地址空间进行了两次虚拟地址映射,一次是映射到从0开始的4M大小的虚拟地址空间,一次是映射到Linux正常执行时的虚拟地址空间,在32位体系结构中该虚拟地址空间是3G到4G之间的虚拟地址空间。这里sparc_ramdisk_image64就是从0开始的那段4M的虚拟地址空间中的地址。
回到paging_init函数中去,
第1761行,有时候内核参数会指定使用的内存的大小,如果lmb中可用的内存大于内核参数中指定的内存大小的话,则把多出的部分截去不用
第1763行,计算出Linux可用内存的总大小
第1764行,打印处系统内存信息。
第1766行,把contex_id的位图的所有位置0,contex_id是用来区分不同的虚拟地址空间的,但是哪一个虚拟地址空间使用哪一个contex_id则是没有限制的,因此使用一个位图来管理contex_id的分配。
第1768行,计算处一个shift,该shift实际上是用来在系统启动时内核映像的两次虚拟地址映射的地址之间进行转换的,从0开始的4M大小的虚拟地址空间中的虚拟地址加上shift则得到Linux正常执行时相应物理地址的虚拟地址。
第1771行,要想理解num_kernel_image_mappings变量的作用,就又要解释一下背景知识了,在Linux的执行过程中,在内核空间同时存在者两中虚拟地址映射关系,我们还是以32位的4G大小的虚拟地址空间为例,一种映射关系是把从物理地址开始的896M的物理内存平滑的线性的映射到虚拟地址3G到3G+896M的地址空间中,这种映射关系是固定的,一段映射完就不再改变,叫做线性映射,另外一种是通过ioremap和vmalloc建立的映射关系,这种映射是不确定,经常需要改变。对于前一种映射关系,由于它固定不变的特定,可以对它做一些优化,比如可以使用比较大的内存页映射,这样可以减少页表项,由于其映射关系一直不变,可以把它的表项锁定到TLB中,保证它的表项一直存在于TLB中不被换出。
这里的num_kernel_image_mappings变量就是计算出内核映像可以映射的4M页的个数即表项数,同时把内核映像的映射关系锁定到TLB里,注意这里只是内核映像的线性地址映射关系,而不是整个896M的内核空间映射关系,主要原因是TLB空间很小,而且还要留出一部分缓存其他的地址映射,且内核空间中最常使用的就是内核映像,其他的地址空间很少使用,没必要缓存。DIV_ROUND_UP的宏的作用实际上就是把real_end - KERNBASE除以1 << 22得到内核映像可以分成多少个4M的大小。
第1778行,init_mm是struct mm_struct类型的变量,该类型的变量叫做内存描述符,每个进程都有一个内存描述符,它记录了进程内存使用的所用信息,其中它的pgd字段则保存了进程也表的页全局目录的地址,而init_mm则是0号进程init_task的内存描述符号,这条语句有什么作用,后面再说。
第1780行,把swapper_low_pmd_dir,数组清零
第1783行,该行的pud_set,pud_offset都是与页表项相关的宏,这些宏我们就不分析了,这条语句的实际效果就是swapper_pg_dir[0]=swapper_low_pmd_dir+ + (shift / sizeof(pgd_t))。其中的shift变量前面已经说过,它是在内核的两种虚拟地址空转换的,这行实际上是把swapper_pg_dir页全局目录的第0个表项只为 swapper_low_pmd_dir的虚拟地址作移11位,由此可见swapper_low_pmd_dir是页表的第二个目录。
第1786行,inherit_prom_mappings函数实际上是为内核映像重新映射虚拟地址空间的,它实际上是调用remap_kernel函数,我们看remap_kernel函数:
530 static void __init remap_kernel(void)
531 {
532 unsigned long phys_page, tte_vaddr, tte_data;
533 int i, tlb_ent = sparc64_highest_locked_tlbent();
534
535 tte_vaddr = (unsigned long) KERNBASE;
536 phys_page = (prom_boot_mapping_phys_low >> 22UL) << 22UL;
537 tte_data = kern_large_tte(phys_page);
538
539 kern_locked_tte_data = tte_data;
540
541 /* Now lock us into the TLBs via Hypervisor or OBP. */
542 if (tlb_type == hypervisor) {
543 for (i = 0; i < num_kernel_image_mappings; i++) {
544 hypervisor_tlb_lock(tte_vaddr, tte_data, HV_MMU_DMMU);
545 hypervisor_tlb_lock(tte_vaddr, tte_data, HV_MMU_IMMU);
546 tte_vaddr += 0x400000;
547 tte_data += 0x400000;
548 }
549 } else {
550 for (i = 0; i < num_kernel_image_mappings; i++) {
551 prom_dtlb_load(tlb_ent - i, tte_data, tte_vaddr);
552 prom_itlb_load(tlb_ent - i, tte_data, tte_vaddr);
553 tte_vaddr += 0x400000;
554 tte_data += 0x400000;
555 }
556 sparc64_highest_unlocked_tlb_ent = tlb_ent - i;
557 }
558 if (tlb_type == cheetah_plus) {
559 sparc64_kern_pri_context = (CTX_CHEETAH_PLUS_CTX0 |
560 CTX_CHEETAH_PLUS_NUC);
561 sparc64_kern_pri_nuc_bits = CTX_CHEETAH_PLUS_NUC;
562 sparc64_kern_sec_context = CTX_CHEETAH_PLUS_CTX0;
563 }
564 }
remap_kernel函数为内核映像建立虚拟地址映射实际上没有田重任何页表项或者TSB表项,它就是用我们前面提到的num_kernel_image_mappings把地址映射关系缩存到TLB中。
第535行,把内核虚拟基地址放到变量tte_vaddr 中,从这里可以看出这里映射的虚拟地址还是从0开始的虚拟地址而不是从3G开始的虚拟地址
第536行,tte_vaddr虚拟地址所对应的物理地址放到变量phys_page中,是内核映像的物理基地址,这里把它转换程4M对齐的地址,因为TLB锁存的映射关系使用的是4M大小的页。
第537行,设置TLB表项的data字段,放到tte_data变量中,实际上它是由phys_page和上文提到的页属性组成的,至于内核页有什么属性,请自行分析吧,可以确定的是页大小设置的是4M大小
第542到464行,的if分支语句我们只需要看hypervisor那个分支就行了,即543到547行的for循环,它是循环num_kernel_image_mappings次为内核映像的每个4M大小页建立映射关系并调用hypervisor服务进行TLB锁存。
该函数执行完后,内核映像就映射完了。
回到paging_init函数中来,
第1788行,这个函数就不分析了,它实际上是把前文提到的pall数组中的内存即不能被Linux使用的内存在kpte_linear_bitmap内存位图中置位,至于这个位图有什么作用kpte_linear_bitmap,等遇到的时候再说吧。
第1791行,setup_tba焊说是在head_64.S文件中定义的,当时分析这个文件时,并没用分析到这个函数,现在来看看这个函数:
786 .globl setup_tba
787 setup_tba:
788 save %sp, -192, %sp
789
790 /* The boot processor is the only cpu which invokes this
791 * routine, the other cpus set things up via trampoline.S.
792 * So save the OBP trap table address here.
793 */
794 rdpr %tba, %g7
795 sethi %hi(prom_tba), %o1
796 or %o1, %lo(prom_tba), %o1
797 stx %g7, [%o1]
798
799 call setup_trap_table
800 nop
801
802 ret
803 restore
804 sparc64_boot_end:
看这个函数的名字就知道这个函数是设置Linux的trap table的。
第788行,更新寄存器窗口,这一行对我们的理解不重要。
第194行,读取tba寄存器的值到g7中,该寄存器里面保存的privileged模式下trap table 的虚拟基地址,进入Linux之前,一直在OBP中执行,因此这里实际上是把OBP的trap table的虚拟地址保存的g7中
第795行到797行,把g7的值保存到变量 prom_tba
这几条语句实际上是把OBP的trap talbe的地址保存到变量 prom_tba,这是很好理解的,因为进入内核后要使用Linux自己的trap table,所以把OBP的trap talbe 的地址保存起来。
第799行,调用setup_trap_table
函数。
第802行,返回到paging_init函数中去。
可以看出setup_tba
后的主要工作是有函数 setup_trap_table
完成的。
678 .globl setup_trap_table
679 setup_trap_table:
680 save %sp, -192, %sp
681
682 /* Force interrupts to be disabled. */
683 rdpr %pstate, %l0
684 andn %l0, PSTATE_IE, %o1
685 wrpr %o1, 0x0, %pstate
686 rdpr %pil, %l1
687 wrpr %g0, PIL_NORMAL_MAX, %pil
上面这一段是禁止中断,就不细分析了
689 /* Make the firmware call to jump over to the Linux trap table. */
690 sethi %hi(is_sun4v), %o0
691 lduw [%o0 + %lo(is_sun4v)], %o0
692 brz,pt %o0, 1f
693 nop
这一段实际上判断 is_sun4v变量的值,如果为0,就跳到标记1处执行,显然我们这里是不会跳转的。
695 TRAP_LOAD_TRAP_BLOCK(%g2, %g3)
696 add %g2, TRAP_PER_CPU_FAULT_INFO, %g2
697 stxa %g2, [%g0] ASI_SCRATCHPAD
第695行的宏我们已经见过多次了,它就是得到当前虚拟处理器的trap_block结构的地址,放在g2中
第696,697行,把trap_block变量的fault_info成员存入虚拟处理器的地址为0的 SCRATCHPAD寄存器中。这样在以后需要的时候可以快速的从SCRATCHPAD寄存器中取出它的值,我们在中断处理中可以看到。
703 sethi %hi(KERNBASE), %g3
704 sub %g2, %g3, %g2
705 sethi %hi(kern_base), %g3
706 ldx [%g3 + %lo(kern_base)], %g3
707 add %g2, %g3, %o1
708 sethi %hi(sparc64_ttable_tl0), %o0
g2里面存放的是当前虚拟处理器trap_block变量的fault_info的虚拟地址
第703,704行,fault_info的虚拟地址减去 KERNBASE放到g2中
第705,706行,kern_base的值放到g3中
第707行,g3与g2相加放到%o0中
这几行所这些的操作实际上是%o1=fault_info- KERNBASE+ kern_base,实际上是把 fault_info的物理地址放到%o1中
第708行, 把变量 sparc64_ttable_tl0的值放在%o0中, sparc64_ttable_tl0看名字就知道是trap=0时,trap table的虚拟基地址
710 set prom_set_trap_table_name, %g2
711 stx %g2, [%sp + 2047 + 128 + 0x00]
712 mov 2, %g2
713 stx %g2, [%sp + 2047 + 128 + 0x08]
714 mov 0, %g2
715 stx %g2, [%sp + 2047 + 128 + 0x10]
716 stx %o0, [%sp + 2047 + 128 + 0x18]
717 stx %o1, [%sp + 2047 + 128 + 0x20]
718 sethi %hi(p1275buf), %g2
719 or %g2, %lo(p1275buf), %g2
720 ldx [%g2 + 0x08], %o1
721 call %o1
722 add %sp, (2047 + 128), %o0
这一段应该很熟系把,它实际上是调用OBP接口服务函数调用OBP服务,它实际上是把参数放到栈中,然后在OBP中会从栈中取处参数,在head_64.S的分析中已经分析过这种调用,这里就不分析了。它的执行效果相当于:
prom_set_trap_talbe(2,0,fault_info,sparc64_ttable_tl0);
很显然,服务函数完成后,privliged mode的trap talbe的虚拟基地址就设置成 sparc64_ttable_tl0了,这样再发生trap时,就要进入Linux处理了。其中第3个参数 fault_info的定义如下:
struct hv_fault_status {
unsigned long i_fault_type;
unsigned long i_fault_addr;
unsigned long i_fault_ctx;
unsigned long i_reserved[5];
unsigned long d_fault_type;
unsigned long d_fault_addr;
unsigned long d_fault_ctx;
unsigned long d_reserved[5];
};
该结构里面保存了指令MMU或数据MMU发生页故障时保存的故障类型地址,contex_id信息,这里设置后,当发生故障时,Linux就可一从里面取得这些信息
724 ba,pt %xcc, 2f
725 nop
第724行,跳转到2处执行
742 2: sethi %hi(sparc64_kern_pri_context), %g3
743 ldx [%g3 + %lo(sparc64_kern_pri_context)], %g2
744
745 mov PRIMARY_CONTEXT, %g1
这里就是标记2处了
第702,703行,把 sparc64_kern_pri_context的值放入g2中,看这个变量的名字就知道他是内核执行时标志内核虚拟地址空间的 的contex_id,这个变量在前面一直没有附过值,这里应该是0。
第745行,把虚拟处理器的 PRIMARY_CONTEXT寄存器的虚拟地址放入g1中
747 661: stxa %g2, [%g1] ASI_DMMU
748 .section .sun4v_1insn_patch, "ax"
749 .word 661b
750 stxa %g2, [%g1] ASI_MMU
751 .previous
752
753 membar #Sync
我们在前面的分析中看到了很多打patch的代码,这里就是其中的一个patch,在处理器执行到这里之前,patch已经打好了,所以这里真正执行的是750行的指令。
把g2寄存器的值存入g1寄存器所标志的地址中,这里实际上是把sparc64_kern_pri_context的值存入PRIMARY_CONTEXT中,contex_id是T2用来标志不同的虚拟地址空间的,当进程访问虚拟地址时,MMU就是用contex_id来匹配该虚拟地址属于哪一个进程从而进行地址转换。而在内核执行过程中进行虚拟地址到物理地址的转换时,使用PRIMARY_CONTEXT寄存器里面的值即contex_id=0来匹配TLB表项的tag字段进行地址转换。实际上在虚拟处理器的MMU还存在这另外一个寄存器叫做NUCLUS_CONTEXT,当trap_level>0时MMU使用该寄存器里面的值进行地址转换,该寄存器里面的值强制为0。在sparc的设计里面内核和trap_level>0的核心上下文(比如中断上下文)可以位于不同的虚拟地址空间,即可以具有不同的contex_id,从而两者具有相对独立的虚拟地址空间。但是在Linux里面,显然内核和核心上下文位于同一虚拟地址空间里的,因此通过把内核执行时的context_id置为0从而时内核与核心上下文处于同一虚拟地址空间。
753 membar #Sync
754
755 BRANCH_IF_SUN4V(o2, 1f)
第753行,进行指令流控制,保证前面的访存指令执行完。
第755行,该宏在head_64.S中分析过,跳到1处执行。
768 1:
769 sethi %hi(0x80000000), %o2
770 sllx %o2, 32, %o2
771 wr %o2, %asr25
第769,770行, 0x80000000左移32位放到%o2中
第771行,把%o2的值写入到asr25寄存器中
这一段的作用实际上是把asr25寄存器的第64位置1,asr25寄存器是System Tick Compare寄存器,它的第64位置1,则禁止时钟中断。
774 wrpr %g0, %g0, %wstate
775
776 call init_irqwork_curcpu
777 nop
778
779 /* Now we can restore interrupt state. */
780 wrpr %l0, 0, %pstate
781 wrpr %l1, 0x0, %pil
782
783 ret
784 restore
第774行,清零wstate寄存器
第776行,调用init_irqwork_curcpu,它实际上是把当前虚拟处理器的trap_block变量的irq_worklist_pa变量初始化为0,这个变量在中断处理中会使用,这里把它初始化为0,可参考中断处理的相关章节。
第779到781行,重新运行中断
第783,784行,退出
到这里这段汇编就分析完了,这段汇编除了做一些初始化设置外,最终要的是把当前虚拟处理器privilged mode的trap talbe从OBP切换到Linux中来。
我们回到paging_init函数中来,
第1793行,__flush_tlb_all函数就不一行行的分析了,它实际上是调用hypervisor服务刷新TLB缓存,它的刷新规则是如果TLB缓存的地址映射表项没有被锁存到TLB中,则取消该映射,执行了这个函数之后实际上OBP所做的虚拟地址映射全都取消了,只存在Linux的地址映射
第1796行,这个函数也是调用hypervisor服务,就不一行行分析了,它实际上就是把前面提到TSB通过struct hv_tsb_descr几个注册到hypervisor,hypervisor会设置虚拟处理器上的TSB,设置之后,当产生TLB miss后,就可以先到TSB中查询相应地址转换表项了。
到这里内核页表的初始化就完成了,它做了如下工作:
把内核映像的地址映射关系锁存到TLB中,准备了并设置了TSB,准备并初始化了页全局目录和页次级目录。
在这个过程中实际上真正虚拟地址到物理地址的映射关系的只有内核映像,内核其他部分都没有建立地址映射关系,他们是在内核执行的过程中动态建立的。因为这里已经初始化了Linux trap table,它访问到相应的虚拟地址时,如果TLB中没有表项,则会发生TLB miss ,查询TSB,TSB中没有会查询页全局目录和次级目录,如果也没有则会产生页故障,为该虚拟地址填充页表项建立地址映射关系,然后会依次缓存到TSB和TLB中。这样在内核执行的过程中,整个内核空间的地址映射关系都会建立起来。
为内核初始化页表是paging_init函数最主要的工作,这里就已经完成了
第1798行,建立设备节点树,机器启动后hypervisor会扫描机器上的所有硬件设备,并把相关硬件的资源信息使用一种叫做machine description机制保存起来,在OBP中,会根据machine description把为所有的硬件设备建立一种设备节点树,在节点树里面保存了资源信息,这里是通过根据OBP的节点树在Linux中建立一个资源节点树,保存硬件资源信息。Linux设备节点树的初始化过程我们在Linux T2 设备扫描的章节中会专门介绍,这里就不提了。
第1799和1801行,这两个函数在T2平台上没有做任何操作
第1806行,sun4v_mdesc_init函数是在linux中建立上文刚刚提到的hypervisor用来描述设备资源的machine description,刚在内核中已经基于OBP节点树在Linux建立设备节点树用来描述设备资源信息了,为什么这里还要把hypervisor描述的资源信息在Linux中重现呢,这时因为OBP设备节点树中虽然大部分信息是从machine description中得到的,但还是与machine description不同,它除了machine description的基础上添加一些OBP需要传递给内核的信息比如内核参数外,它并没有把machine description所有的信息都包括进来,因此内核需要通过machine description得到一些从设备节点树中不能得到的资源信息,不过内核主要还是使用设备节点树获得资源信息的。
machine description实现:
Linux中实现访问hypervisor的machine description的机制的代码主要在arch/sparc/kernel/mdesc.c文件中。
我们先描述一下machine description,然后再讲解Linux如何使用它。
machine description包括4个部分,他们在内存中是连续存放的,如下图所示:
header
node block
name block
data block
图1
header中每个每个filed的大小及描述如下表所示:
Byte offset Size in bytes Field name Description
0 4 transport_version Transport version number
4 4 node_blk_sz Size in bytes of node block
8 4 name_blk_sz Size in bytes of name block
12 4 data_blk_sz Size in bytes of data block
表1
描述machine description header的数据结构定义如下:
struct mdesc_hdr {
u32 version; /* Transport version */
u32 node_sz; /* node block size */
u32 name_sz; /* name block size */
u32 data_sz; /* data block size */
} __attribute__((aligned(16)));
name block紧跟这node block,它里面存放了许多的字符串,这些字符串都是以字符串结束符(‘\0’)结束,每个字符串都是node block中某个node或element的名称。
node block紧跟header,里面每个element占有16个字节,因此node block可以看作是每个元素占有16字节空间的数组,node block中每个element可以由它在该数组中的下标唯一标志。node block中每个element的16字节的各个filed描述如下表所示:
Byte offset Size in bytes Field name Description
0 1 tag 类型
1 1 name_len 名称的长度(name size),它的名称位于name block中
2 2 _reserved_field 保留
4 4 name_offset 名称字符串在name block中的位置
8 8 val 类型为“NODE”,“PROP_VAL”或“PROP_ARC”时的64位值
8 4 data_len 类型为“PROP_STR”或“PROP_DATA”时该成员在data block中数据的长度
12 4 data_offset 类型为“PROP_STR”或“PROP_DATA”该成员数据在data block中的位置
描述node block element的数据结构定义如下:
struct mdesc_elem {
u8 tag;
#define MD_LIST_END 0x00
#define MD_NODE 0x4e
#define MD_NODE_END 0x45
#define MD_NOOP 0x20
#define MD_PROP_ARC 0x61
#define MD_PROP_VAL 0x76
#define MD_PROP_STR 0x73
#define MD_PROP_DATA 0x64
u8 name_len;
u16 resv;
u32 name_offset;
union {
struct {
u32 data_len;
u32 data_offset;
} data;
u64 val;
} d;
};
data block里面则存放有node block element相关联的信息了,可以是硬件资源信息,也可以是其他的,这些数据具体有什么含义有node block element类型来定义。
machine description描述硬件资源使用情况是以NODE(节点)为基本单位的,一个NODE描述某一个硬件设备或虚拟设备所有资源信息,这些资源可能包括很多种,比如某一个外围设备可能使用既使用内存空间又使用I/O空间,内存空间资源和I/O空间资源就是两种相互独立的资源,由比如外围设备可能是PCI设备,也可能是USB设备,那么设备类型也可以看作一种资源,设备的资源可以使用property(属性)来描述,某一个属性描述了某一种资源的使用情况。因此一个NODE可以包含多个property,其中的每个property都描述了NODE的某一种资源的使用情况。
一个NODE在node block中是由多个连续相邻的node block elements组成的,其中node block中的NODE element(类型为NODE的element,见表3)标志了一个NODE的开始,
NODE_END element标志了一个NODE的结束,在NODE element和NODE_END element之间的element可以没有或者有多个elements,如果没有的话就说明该NODE没有property,如果有一个或多个elements的话,它们只能是PROP_VAL,PROP_STR,PROP_ARC或者PROP_DATA类型的element,这几种类型的element都标志了该NODE的一个property,其中如果该element类型为PROP_VAL,则该property描述的资源信息就存放在该element 16个字节中的val field中,而其他3中类型的element,其property描述的资源信息保存在data block中,由element 16个字节中的data_len 和data_offset filed指出该信息在data block中的位置和大小。所以data block中存放的就是所有NODE的这三种类型的property的资源信息。
node block中所有的elements都是有名称的,它们的名称都位于name block中,有element 的name_len 和name_offset field指出名称在name block的大小和位置。
一个NODE由在node block中由多个连续的相邻elements组成,其中标志NODE开始NODE类型的element的名称即是该NODE的名称,一个NODE包含多 个property,每个property的名称就是描述该peoperty的element的名称。
每个NODE都有一个唯一标志它的ID,它的ID其实就是该NODE在node block中的位置。
在node block可能存在这多个NODE,为了快速的找到每个NODE,每个标志NODE开始的NODE类型的elment的 value filed存放了下一个NODE在的node block中的位置,即下一个NODE的NODE ID,这样所有的NODE就组成了一个链表,链表的第一个元素就是nodeblock中的第一个NODE,叫做root NODE。
这些所有的NODE同时通过它们的名称组成了一个树形结构,用以描述不同NODE之间的父子关系,比如名称为”/cpu”的节点就是跟root 节点的子节点 /pci@0/pci@1的节点就是/pci@0节点的子节点。
machine description机制的讲解就到这里,下面看看Linux是怎么支持它的。
在machine description讲解中已经看到了Linux中为描述machine description的header block和node block element所定义的结构体,但这两个结构体并非Linux支持machine description的总的管理结构,总的管理结构定义如下:
struct mdesc_handle {
struct list_head list;
struct mdesc_mem_ops *mops;
void *self_base;
atomic_t refcnt;
unsigned int handle_size;
struct mdesc_hdr mdesc;
};
该结构的各个字段有什么含义,我们到用到的时候再说。
现在我们回过头来看sun4v_mdesc_init函数,它定义如下:
926 void __init sun4v_mdesc_init(void)
927 {
928 struct mdesc_handle *hp;
929 unsigned long len, real_len, status;
930
931 (void) sun4v_mach_desc(0UL, 0UL, &len);
932
933 printk("MDESC: Size is %lu bytes.\n", len);
934
935 hp = mdesc_alloc(len, &lmb_mdesc_ops);
936 if (hp == NULL) {
937 prom_printf("MDESC: alloc of %lu bytes failed.\n", len);
938 prom_halt();
939 }
940
941 status = sun4v_mach_desc(__pa(&hp->mdesc), len, &real_len);
942 if (status != HV_EOK || real_len > len) {
943 prom_printf("sun4v_mach_desc fails, err(%lu), "
944 "len(%lu), real_len(%lu)\n",
945 status, len, real_len);
946 mdesc_free(hp);
947 prom_halt();
948 }
949
950 cur_mdesc = hp;
951
952 report_platform_properties();
953 }
第931行,调用hyervisor服务得到machine description所占的内存空间的大小。
第935行,调用mdesc_alloc 函数struct mdesc_handle结构分配空间。 mdesc_alloc函数定义如下:
175 static struct mdesc_handle *mdesc_alloc(unsigned int mdesc_size,
176 struct mdesc_mem_ops *mops)
177 {
178 struct mdesc_handle *hp = mops->alloc(mdesc_size);
179
180 if (hp)
181 hp->mops = mops;
182
183 return hp;
184 }
可以看出该函数主要调用传入第二个参数分配空间的,并把该参数保存在 struct mdesc_handle结构变量的mops成员中,由此可见该成员是为machine description分配空间的,
该函数的第二个参数是kmalloc_mdesc_memops,它定义如下:
static struct mdesc_mem_ops kmalloc_mdesc_memops = {
.alloc = mdesc_kmalloc,
.free = mdesc_kfree,
};
所以分配空间的函数主要是 mdesc_kmalloc:
137 static struct mdesc_handle *mdesc_kmalloc(unsigned int mdesc_size)
138 {
139 unsigned int handle_size;
140 void *base;
141
142 handle_size = (sizeof(struct mdesc_handle) -
143 sizeof(struct mdesc_hdr) +
144 mdesc_size);
145
146 base = kmalloc(handle_size + 15, GFP_KERNEL | __GFP_NOFAIL);
147 if (base) {
148 struct mdesc_handle *hp;
149 unsigned long addr;
150
151 addr = (unsigned long)base;
152 addr = (addr + 15UL) & ~15UL;
153 hp = (struct mdesc_handle *) addr;
154
155 mdesc_handle_init(hp, handle_size, base);
156 return hp;
157 }
158
159 return NULL;
160 }
由于前面已经建立了一个简单的内存管理机制lmb,这里就使用lmb分配内存空间,可以看出它不光为struct mdesc_handle结构分配了空间,还为machine description分配了空间,这里,并调用 mdesc_handle_init函数初始化struct mdesc_handle结构,注意分配空间是之所以加上15,是为了保证分配的空间是16字节对齐的。mdesc_handle_init初始化包括把分配的空间清零,把分配的空间的16字节对齐的地址保存到struct mdesc_handle结构的self_base字段中,并把空间大小保存在handle_size字段中。
第941行,sun4v_mach_desc函数是调用hypervisor服务为刚刚分配的空间填充machine description,这样这个空间中就保存了hypervisror扫描的资源信息了。
第950行,把刚刚实现的Linux machine description保存到变量cur_mdesc中,以后就同过cur_mdesc变量使用它了。
第952行,通过得到的资源信息,打印一下系统信息。
这样Linux的machine description就初始化好了,那么我们怎么使用它呢,我们使用machine description最重要的目的就是找到我们所关心的NODE,以及NODE的property,因为他们描述的资源信息才是我们真正想要的东西。因此Linux里面提供了解析machine description硬件资源信息的函数,这些函数我们不会全部分析,只分析其中最常用的两个,其他的读者可自行分析。
这两个函数分别是通过NODE的名称找到该NODE,已经通过property名称得到资源信息。
392 u64 mdesc_node_by_name(struct mdesc_handle *hp,
393 u64 from_node, const char *name)
394 {
395 struct mdesc_elem *ep = node_block(&hp->mdesc);
396 const char *names = name_block(&hp->mdesc);
397 u64 last_node = hp->mdesc.node_sz / 16;
398 u64 ret;
399
400 if (from_node == MDESC_NODE_NULL) {
401 ret = from_node = 0;
402 } else if (from_node >= last_node) {
403 return MDESC_NODE_NULL;
404 } else {
405 ret = ep[from_node].d.val;
406 }
407
408 while (ret < last_node) {
409 if (ep[ret].tag != MD_NODE)
410 return MDESC_NODE_NULL;
411 if (!strcmp(names + ep[ret].name_offset, name))
412 break;
413 ret = ep[ret].d.val;
414 }
415 if (ret >= last_node)
416 ret = MDESC_NODE_NULL;
417 return ret;
418 }
419 EXPORT_SYMBOL(mdesc_node_by_name);
该函数的第二个参数from_node是一个NODE的ID,它的作用是通过它可以查找它后面的NODE,一般来说一个NODE的子NODE一定位于它的后面,这样就不用遍历整个NODE链表了,可以提高查找效率,当然如果该参数为空,则要查找整个链表。
第395行,node_block函数很简单就不分析了,它实际上计算得到node block的基地址,也就是elements数组的地址。
第396行,这个函数很简单,就就不分析了,它的作用是得到name block的基地址,放到names变量中。
第397行,得到node block中的elements的数量。
第401行,如果第一个from_node为空,则从第0个NODE开始查找。
第405行,得到下一个NODE的位置。
第408到414行的while循环则就是查找过程了,它会使用name block中该节点的名称匹配传入的名称,如果相等,则找到了该节点,把NODE ID 返回。
通过NODE得到其名称为name的property的函数为mdesc_get_property:
421 const void *mdesc_get_property(struct mdesc_handle *hp, u64 node,
422 const char *name, int *lenp)
423 {
424 const char *names = name_block(&hp->mdesc);
425 u64 last_node = hp->mdesc.node_sz / 16;
426 void *data = data_block(&hp->mdesc);
427 struct mdesc_elem *ep;
428
429 if (node == MDESC_NODE_NULL || node >= last_node)
430 return NULL;
431
432 ep = node_block(&hp->mdesc) + node;
433 ep++;
434 for (; ep->tag != MD_NODE_END; ep++) {
435 void *val = NULL;
436 int len = 0;
437
438 switch (ep->tag) {
439 case MD_PROP_VAL:
440 val = &ep->d.val;
441 len = 8;
442 break;
443
444 case MD_PROP_STR:
445 case MD_PROP_DATA:
446 val = data + ep->d.data.data_offset;
447 len = ep->d.data.data_len;
448 break;
449
450 default:
451 break;
452 }
453 if (!val)
454 continue;
455
456 if (!strcmp(names + ep->name_offset, name)) {
457 if (lenp)
458 *lenp = len;
459 return val;
460 }
461 }
462
463 return NULL;
464 }
第424行,得到name block的地址
第425行,得到node block中elements的数量。
第426行,得到data blockl的地址
第432行,根据得到的NODE Id从node block中得到其NODE elements(第一个element)。
第434的奥461行的循环遍历给NODE 的所有property,匹配其在name block中的名称,如果匹配,则如果property为PROP_VAL,则返回其element中val field,并把数据长度8存入最后一个参数中,如果是其他类型的property,则从data block中找到器信息位置返回并把数据长度存入最后一个参数中。
到这里machine description就讲完了,我们接着回到paging_init函数中来,
第1806行,cpu_all_mask是一个CPU位图,它应该标志了系统中所有的CPU的ID
,我们看这个函数的实现:
807 void __cpuinit mdesc_populate_present_mask(cpumask_t *mask)
808 {
809 if (tlb_type != hypervisor)
810 return;
811
812 ncpus_probed = 0;
813 mdesc_iterate_over_cpus(record_one_cpu, NULL, mask);
814 }
第812行,因为系统之前从未初始个过CPU位图,因此这里先把探测到的CPU的数量初始化为0;
第831行,调用mdesc_iterate_over_cpus函数:
768 static void * __cpuinit mdesc_iterate_over_cpus(void *(*func)(struct mdesc_handle *, u64, int, void *), void *arg, cpumask_t *mask)
769 {
770 struct mdesc_handle *hp = mdesc_grab();
771 void *ret = NULL;
772 u64 mp;
773
774 mdesc_for_each_node_by_name(hp, mp, "cpu") {
775 const u64 *id = mdesc_get_property(hp, mp, "id", NULL);
776 int cpuid = *id;
777
778 #ifdef CONFIG_SMP
779 if (cpuid >= NR_CPUS) {
780 printk(KERN_WARNING "Ignoring CPU %d which is "
781 ">= NR_CPUS (%d)\n",
782 cpuid, NR_CPUS);
783 continue;
784 }
785 if (!cpu_isset(cpuid, *mask))
786 continue;
787 #endif
788
789 ret = func(hp, mp, cpuid, arg);
790 if (ret)
791 goto out;
792 }
793 out:
794 mdesc_release(hp);
795 return ret;
796 }
第770行, mdesc_grab函数很简单,就不分析了。它就是通过cur_mdesc变量得到Linux中当前的machine description,有此可见我们是要利用machine description得到cpu系统的CPU相关信息。
第774行,mdesc_for_each_node_by_name函数也不分析了,看名字就知道他是变量node block中的所有NODE,找到其中所有名称包含”cpu”的NODE,从而形成一个循环。
第775到792行就是循环体的内容,它会得到cpu NODE 的“id” property,然后用该cpuid设置cpu_all_mask位图,其中第789行,执行的是函数record_one_cpu,这个函数也不分析了,它其实是把探测到的cpu的数量加一,然后设置cpu_present_mask位图。
这样,当 mdesc_populate_present_mask执行完后,就设置了两个cpu位图:
cpu_all_mask和cpu_present_mask,并且变量ncpus_probed就存放了系统中CPU的数量。
我们回到paging_init函数中来,
第1808行,mdesc_fill_in_cpu_data我们这里不执行,因为我们是SMP架构的,这里不执行的原因是这个函数要操作per_cpu域,而这里per_cpu域现在并没有初始化,因此在per_cpu域初始化后,这个函数到那时会被执行,可以参考Linux T2 SMP支持实现的章节。
第1816到1820行的循环为每个cpu建立了两个栈,这个栈分别软件中断和普通中断执行时使用的栈,他们保存在两个数组softirq_stack和hardirq_stack中。
Bootmem 内存分配器:
第1823行,前面的内存分配都是使用之前建立的简单临时的内存管理机制lmb,这里就要初始化另外一个内存管理机制:bootmem内存分配器。
bootmem与lmb一样也是一个临时的内存管理机器,与lmb不同的时,它对内存的管理是基于页框的,它会为所有物理页的使用情况建立一个位图。bootmem的主要目的是为Linux真正的内存管理机制做准备,在之后的真正的内存管理机制初始化时会在bootmem的基础上建立真正的内存管理机制。
bootmem的管理结构叫做bootmem_data_t,它定义如下:
typedef struct bootmem_data {
unsigned long node_min_pfn;
unsigned long node_low_pfn;
void *node_bootmem_map;
unsigned long last_end_off;
unsigned long hint_idx;
struct list_head list;
} bootmem_data_t;
我们知道计算机内存的组织结构分为两种,一种叫做NUMA结构,一种叫做UMA结构,我们并不准备讨论NUMA和UMA的特征和区别,只需要知道在NUMA结构下的CPU根据其内存及I/O资源分为了多个节点,那么每个节点都会有一个bootmem的管理结构bootmem_data_t,而在UMA结构下整个物理内存被看作一个节点,也就只存在一个bootmem管理结构了。因此针对NUMA和UMA,botmem的初始化是不同的,我们这里只关注UMA结构。
我们现在看看 bootmem_data_t中各成员的含义:
node_min_pfn:系统中第一个物理页的页帧号
node_low_pfn:该成员却不是系统中最后一个物理页的页帧号,而是ZONE_NOMAL区域中最后一个物理页的页帧号,在Linux中,每个内存节点都可以分为3个内存区域,分别是ZONE_DMA,ZONE_NOMAL已经ZONE_HIGH,其中在32位体系结构中ZONE_DMA和ZONE_NOMAL则覆盖了0到896M的物理地址范围,ZONE_HIGH就是高端内存了,他们是不被bootmem所管理的。不过我们这里是64位体系结构,内存空间很大,所以我们的内存应该都会位于ZONE_DMA和ZONE_NOMAL的区域内。
node_bootmem_map:该成员就是内存页框位图的地址
last_end_off:最近一次内存分配的分配地址在最后一个分配的页中的偏移
hint_idx:最近一次内存分配的页帧号
我们看bootmem_init函数:
1381 static unsigned long __init bootmem_init(unsigned long phys_base)
1382 {
1383 unsigned long end_pfn;
1384 int nid;
1385
1386 end_pfn = lmb_end_of_DRAM() >> PAGE_SHIFT;
1387 max_pfn = max_low_pfn = end_pfn;
1388 min_low_pfn = (phys_base >> PAGE_SHIFT);
1389
1390 if (bootmem_init_numa() < 0)
1391 bootmem_init_nonnuma();
1392
1393 /* XXX cpu notifier XXX */
1394
1395 for_each_online_node(nid)
1396 bootmem_init_one_node(nid);
1397
1398 sparse_init();
1399
1400 return end_pfn;
1401 }
第1386行,通过lmb得到系统中最后一个页框的页帧号放到end_pfn中
第1387行,max_pfn,pax_low_pfn都存着最后一个页框的页帧号
第1388行,系统中第一个物理页的页帧号
我们是uma结构,接下来就执行第1391行,我们看 bootmem_init_nonnuma函数:
1279 static void __init bootmem_init_nonnuma(void)
1280 {
1281 unsigned long top_of_ram = lmb_end_of_DRAM();
1282 unsigned long total_ram = lmb_phys_mem_size();
1283 unsigned int i;
1284
1285 numadbg("bootmem_init_nonnuma()\n");
1286
1287 printk(KERN_INFO "Top of RAM: 0x%lx, Total RAM: 0x%lx\n",
1288 top_of_ram, total_ram);
1289 printk(KERN_INFO "Memory hole size: %ldMB\n",
1290 (top_of_ram - total_ram) >> 20);
1291
1292 init_node_masks_nonnuma();
1293
1294 for (i = 0; i < lmb.memory.cnt; i++) {
1295 unsigned long size = lmb_size_bytes(&lmb.memory, i);
1296 unsigned long start_pfn, end_pfn;
1297
1298 if (!size)
1299 continue;
1300
1301 start_pfn = lmb.memory.region[i].base >> PAGE_SHIFT;
1302 end_pfn = start_pfn + lmb_size_pages(&lmb.memory, i);
1303 add_active_range(0, start_pfn, end_pfn);
1304 }
1305
1306 allocate_node_data(0);
1307
1308 node_set_online(0);
1309 }
第1291行,这个函数就不分析了,它初始化每个cpu内存节点的数组和位图,由于是uma结构都只有一个节点,所以所有cpu都存的是0号节点
第1294到1394行,是一个循环,这个循环里面的函数由于都是Linux里面的标准函数不是我们这里实现的体系结构相关的函数,就不分析了。它从lmb中解析处所有的可用的物理页的页帧号,放在在一个early_node_map的数组中
第1306行,函数allocate_node_data:
818 static void __init allocate_node_data(int nid)
819 {
820 unsigned long paddr, num_pages, start_pfn, end_pfn;
821 struct pglist_data *p;
822
835
836 p = NODE_DATA(nid);
837
838 get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
839 p->node_start_pfn = start_pfn;
840 p->node_spanned_pages = end_pfn - start_pfn;
841
842 if (p->node_spanned_pages) {
843 num_pages = bootmem_bootmap_pages(p->node_spanned_pages);
844
845 paddr = lmb_alloc_nid(num_pages << PAGE_SHIFT, PAGE_SIZE, nid,
846 nid_range);
847 if (!paddr) {
848 prom_printf("Cannot allocate bootmap for nid[%d]\n",
849 nid);
850 prom_halt();
851 }
852 node_masks[nid].bootmem_paddr = paddr;
853 }
第1306行,通过宏NODE_DATA得到 struct pglist_data结构,这个结构的细节就不讲另外,只要知道它的成员bdata成员就指向我们前面所说的bootmem的管理结构就行了。
第838行,get_pfn_range_for_nid函数实际上是从刚刚提到的early_node_map数组中得到系统中最大页帧号和最小页帧号分别赋值到struct pglist_data的两个成员中
第843行,bootmem_bootmap_pages是bootmem的标准函数,它计算出需要多少个页来存储当前系统中页框的位图
第845行,利用lmb分配位图
第852行,位图地址存到一个全局变量中
回到bootmem_init_nonnuma函数
第1308行,该函数注册一个节点
回到bootmem_init函数中来,刚刚分配了一个节点号为0的内存节点,
第1396行,初始化这个节点:
1349 static void __init bootmem_init_one_node(int nid)
1350 {
1351 struct pglist_data *p;
1352
1353 numadbg("bootmem_init_one_node(%d)\n", nid);
1354
1355 p = NODE_DATA(nid);
1356
1357 if (p->node_spanned_pages) {
1358 unsigned long paddr = node_masks[nid].bootmem_paddr;
1359 unsigned long end_pfn;
1360
1361 end_pfn = p->node_start_pfn + p->node_spanned_pages;
1362
1363 numadbg(" init_bootmem_node(%d, %lx, %lx, %lx)\n",
1364 nid, paddr >> PAGE_SHIFT, p->node_start_pfn, end_pfn);
1365
1366 init_bootmem_node(p, paddr >> PAGE_SHIFT,
1367 p->node_start_pfn, end_pfn);
1368
1369 numadbg(" free_bootmem_with_active_regions(%d, %lx)\n",
1370 nid, end_pfn);
371 free_bootmem_with_active_regions(nid, end_pfn);
1372
1373 trim_reserved_in_node(nid);
1374
1375 numadbg(" sparse_memory_present_with_active_regions(%d)\n",
1376 nid);
1377 sparse_memory_present_with_active_regions(nid);
1378 }
第1366行,这仍然是一个标准函数,它初始化bootmem的管理结构,包括我们刚刚分配的位图存放在它的 node_bootmem_map成员中,并把位图的所有为初始化为1。
第1371行,为early_node_map数组中记录的当前系统可用内存在页框位图中设置其代表位。
第1373行,trim_reserved_in_node,把lmb中保留的内存在页框位图中设置其代表位
第1377行,这一行做什么的我们这里就不用管了,它好像是把位图中为0的也框设置为active状态
到这里我们的位图就建立起来了,bootmem管理结构也同样初始化完了,该结构保存在 struct pglist_data结构的btext成员中。
我们的bootmem的初始化就分析到这里,回到paging_init函数中来。
第1826行,初始化max_mapnr变量为最后一个页帧号。
第1828行,这个函数在我们这里什么也不做。
第1831到1838行的语句就不细分析了,他们其实非常复杂,已经涉及到初始化Linux真正的内存管理机制了,它会根据系统中物理页资源初始化所有的页框结构和内存区域结构。
paging_init函数就分析完了。