#if和#ifndef用法
借鉴:
C语言#if、##ifdef、#ifndef的用法详解,C语言条件编译详解 (biancheng.net)
在xv6的kernel/main.c中出现了这种用法,在其他多个.c文件里也出现了
为何要用这种用法:
假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。
Windows 有专有的宏_WIN32
,Linux 有专有的宏__linux__
,以现有的知识,我们很容易就想到了 if else,请看下面的代码:
#include <stdio.h>
int main(){
if(_WIN32){
system("color 0c");
printf("http://c.biancheng.net\n");
}else if(__linux__){
printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
}else{
printf("http://c.biancheng.net\n");
}
return 0;
}
但这段代码是错误的,在 Windows 下提示 linux 是未定义的标识符,在 Linux 下提示 _Win32 是未定义的标识符。对上面的代码进行改进:
#include <stdio.h>
int main(){
#if _WIN32
system("color 0c");
printf("http://c.biancheng.net\n");
#elif __linux__
printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
#else
printf("http://c.biancheng.net\n");
#endif
return 0;
}
这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。
#if 用法的一般格式为:它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。
#if 整型常量表达式1
程序段1
#elif 整型常量表达式2
程序段2
#elif 整型常量表达式3
程序段3
#else
程序段4
#endif
// #elif 和 #else 也可以省略 ,但是#if和#endif是一定要的
#ifdef 用法的一般格式为:如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
#ifdef 宏名
程序段1
#else
程序段2
#endif
// 也可以省略为:
#ifdef 宏名
程序段
#endif
// 如下面这段程序:
#include <stdio.h>
#include <stdlib.h>
int main(){
#ifdef _DEBUG
printf("正在使用 Debug 模式编译程序...\n");
#else
printf("正在使用 Release 模式编译程序...\n");
#endif
system("pause");
return 0;
}
#ifndef 用法的一般格式为:与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。
#ifndef 宏名
程序段1
#else
程序段2
#endif
最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
// 两个宏都存在时编译代码A,否则编译代码B:
#include <stdio.h>
#define NUM1 10
#define NUM2 20
int main(){
#if (defined NUM1 && defined NUM2)
//代码A
printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
#else
//代码B
printf("Error\n");
#endif
return 0;
}
宝图
一直用到这个图,所以取名宝图。
初始化freelist代码
这里的内容都看宝图中的右图。
首先在kernel/kernel开启debug,在main.c的kinit()处打断点:
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if(cpuid() == 0){
consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
statsinit();
#endif
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
// 打断点
kinit(); // physical page allocator
...
}
step into kinit(),进入kalloc.c:
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
跳过initlock()(看代码应该就是初始化锁的功能,和页表没啥关系),step into第二个函数,还是在kalloc.c中:
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
PGROUNDUP() :
这个函数的功能是向上对齐页大小,xv6中一页是4kb,也就是0x1000。假设现在pa_start是0x1020,为了保证每一页都是同等大小,就需要对齐。而这个函数就是用来这样做的,PGROUNDUP(pa_start) 会对齐为 0x2000。
pa_start && pa_end :
接着我们看一下freerange的参数。首先我们应该知道freerange这个函数的功能是把当前所有物理界面给分成一页一页,加入到 freelist中。
pa_start在kinit()中传入的是end,可以看到end的定义是: extern外部定义,在kernel.ld中定义。
extern char end[]; // first address after kernel.
// defined by kernel.ld.
转到kernel.ld查看:可以看到这里巴拉巴拉处理了一堆东西,存了又存。可以看宝图的0x80000000后面的kernel text部分。
OUTPUT_ARCH( "riscv" )
ENTRY( _entry )
SECTIONS
{
/*
* ensure that entry.S / _entry is at 0x80000000,
* where qemu's -kernel jumps.
*/
. = 0x80000000;
.text : {
/* 这里就是图中的0x800.. 之上的kernel text区域,比如现在是:0x80002555*/
*(.text .text.*)
. = ALIGN(0x1000); /* 对齐一页4kb(4096=0x1000),对齐后就是:0x80003000,0x3000 = 4096*3,ALIGN就是这个作用,就是说如果我们需要两页多来存储这些数据,我们就强行对齐成三页*/
/*trampoline分配空间并对齐一页*/
_trampoline = .;
*(trampsec) /*put*/
. = ALIGN(0x1000);
/*这里的意思是trampoline只能用一个页存,超过一个页就会报错*/
ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
PROVIDE(etext = .);
}
.rodata : {
. = ALIGN(16);
*(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
. = ALIGN(16);
*(.rodata .rodata.*)
}
.data : {
. = ALIGN(16);
*(.sdata .sdata.*) /* do not need to distinguish this from .data */
. = ALIGN(16);
*(.data .data.*)
}
.bss : {
. = ALIGN(16);
*(.sbss .sbss.*) /* do not need to distinguish this from .bss */
. = ALIGN(16);
*(.bss .bss.*)
}
/* 这个end就是在kalloc.c里面的那个end数组*/
PROVIDE(end = .);
}
所以,通过上面的kernel.ld我们可以看到,pa_start(也就是end)应该是大于0x80000000的,这也跟宝图对应上。我们查看gdb的pa_start地址,可以看到为:pa_start:0x80027020 。 也就是初始化上面那些东西用了27页多
然后再回到freerange的代码: 因为pa_start大小不一定是整数页,所以需要对齐,通过gdb我们可以看到 p = 0x80028000。进入for循环,再通过gdb可以看到 pa_end = 0x88000000。其实这个pa_end就是宝图的PHYSTOP(虽然比他小,但就是这个),这也说明了xv6其实只有128m的内存可以使用。
for循环就是从 p 开始,每次增长一页,一直到PHYSTOP-PAGESIZE(也就是宝图的freememory底部到顶部)。
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
然后转到kfree()查看:
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void
kfree(void *pa)
{
struct run *r;
// 确保pa是整数页倍且不越界
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
// 将每一页内容全初始化为1
memset(pa, 1, PGSIZE);
// 使得run本身就是pa的地址所在
r = (struct run*)pa;
acquire(&kmem.lock);
// add to the list front
// 将地址加入到freelist的头部
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
这就是整个初始化freelist的代码,结束完for的时候,我们的physical page就分配结束了。此时freelist的首页就是0x87fff000。
kvminit()函数
结束完kinit()就进入了kvminit()函数:
void
main()
{
if(cpuid() == 0){
consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
statsinit();
#endif
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
// 打断点
kvminit(); // create kernel page table
kvminit(),这个函数会设置好kernel的地址空间。kvminit的代码(在kernel/vm.c中):
kvmmap设置了虚拟地址va到物理地址pa的等价映射,看宝图的KernelBase以下部分
void
kvminit()
{
kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);
kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);
kvmmap(VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);
kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);
kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
kvmmap((uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}
然后我们看kalloc():
打一下kalloc()的地址可以发现他是在0x80000b20,因为我们刚刚在kernel.ld中把 0x8000000到0x80027020的地址给用了,所以kalloc()也已经被加载了。
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
// 分配一页空间
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
通过解读freelist那里我们已经知道,kmem的freelist里面存着所有空闲的物理地址,且都是一页一页的。所以kalloc要做的事就是检查是否还有内存,如果 r !=NULL说明还有页数,就可以把第一页分配给它。
通过gdb我们可以看到 kernel_pagetable的地址是0x87fff000,也就是freelist的第一页。
然后再看一下kvmmap():
void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)
panic("kvmmap");
}
mappages()的功能就是把va映射到pa,添加到页表中,还有赋予权限之类的:
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
// walk()函数模仿了硬件查找三级页表的过程,可以看我的4.11写的
// walk()函数是比较重要比较屌的,在lab也会用到,所以要看明白。
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc) // walk 模仿RISC-V分页硬件查找虚拟地址的PTE
{
if(va >= MAXVA)
panic("walk");
for(int level = 2; level > 0; level--) {
pte_t *pte = &pagetable[PX(level, va)];
if(*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
} else { // 如果PTE无效,那么所需的物理页还没有被分配
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) // 如果alloc参数被设置true,walk会分配一个新的页表页,并把它的物理地址放在PTE中
return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE(pagetable) | PTE_V;
}
}
return &pagetable[PX(0, va)]; // 返回PTE在树的最低层的地址
}
配合这个图会好理解很多,就是返回下图的第三级页表对应的pte。alloc=1代表如果不存在对应的page directory就分配一页给它。
kvminithart()函数
回到main,结束完kvminit()之后,就来到了kvminithart()函数:
首先就是w_satp(),这个函数使用了c语言的内嵌汇编,将kernel_pagetable的地址写入了satp寄存器,然后通过sfence_vma() flush TLB。每次更新satp寄存器都应该出现刷新TLB,这样才不会读到旧数据导致出错。
void
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma();
}
static inline void
w_satp(uint64 x)
{
asm volatile("csrw satp, %0" : : "r" (x));
}
// flush the TLB.
static inline void
sfence_vma()
{
// the zero, zero means flush all TLB entries.
asm volatile("sfence.vma zero, zero");
}
procinit()函数
void
main()
{
if(cpuid() == 0){
consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
statsinit();
#endif
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
.....
}
进入procinit():通过注释可以看到这个函数的功能应该是:在启动时初始化进程表。理解procinit()的功能对lab3也有帮助。
// param.h
#define NPROC 64 // maximum number of processes
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
// 给所有的进程分配kernel stack并添加到内核页表。所有的内核栈都在procinit中设置。
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;
}
kvminithart();
}