深入学习XV6(3)

首先声明此处部分转载于:http://www.shaoqun.com/a/122274.aspx

在转载的基础上加上了一些注解和修改以便更好理解,红色部分为注解, 重新调整和加入源代码方便解读.

xv6是一个支持多处理器的Unix-like操作系统,近日阅读源码时发现xv6在记录当前CPU和进程状态时非常tricky

首先,上代码:

struct cpu {
  uchar id;                    // Local APIC ID; index into cpus[] below
  struct context *scheduler;   // swtch() here to enter scheduler
  struct taskstate ts;         // Used by x86 to find stack for interrupt
  struct segdesc gdt[NSEGS];   // x86 global descriptor table
  volatile uint started;       // Has the CPU started?
  int ncli;                    // Depth of pushcli nesting.
  int intena;                  // Were interrupts enabled before pushcli?
  
  // Cpu-local storage variables; see below
  struct cpu *cpu;
  struct proc *proc;           // The currently-running process.
};

extern struct cpu cpus[NCPU];
extern int ncpu;

// Per-CPU variables, holding pointers to the
// current cpu and to the current process.
// The asm suffix tells gcc to use "%gs:0" to refer to cpu
// and "%gs:4" to refer to proc.  seginit sets up the
// %gs segment register so that %gs refers to the memory
// holding those two variables in the local cpu's struct cpu.
// This is similar to how thread-local variables are implemented
// in thread libraries such as Linux pthreads.
extern struct cpu *cpu asm("%gs:0");       // &cpus[cpunum()]
extern struct proc *proc asm("%gs:4");     // cpus[cpunum()].proc

其中struct cpu是一个用来保存每个cpu运行状态的结构体,

代码第一行定义了结构体数组cpus[NCPU],NCPU对应cpu的总数(最大为8),也就是说cpus用来存储所有cpu的运行状态 

那么问题来了:上面的内核代码是运行于每个cpu之中的,那每个cpu如何知道自身的当前运行状态呢?

对于这个问题,我们可以通过lapic获取cpu自身编号,再利用编号对cpus寻址即可,

APIC分为IOAPIC和LAPIC, XV6实现在lapic.c和ioapic.c, 简单的介绍可见:

http://blog.csdn.net/hgf1011/article/details/5925661

更详细的介绍

http://blog.csdn.net/robinsongsog/article/details/39023467


也就是说,对于任意一个cpu,自身状态的存储位置可以这样获得: struct cpu *c = &cpus[cpunum()]; 

int
cpunum(void)
{
 // Cannot call cpu when interrupts are enabled:
 // result not guaranteed to last long enough to be used!
 // Would prefer to panic but even printing is chancy here:
 // almost everything, including cprintf and panic, calls cpu,
 // often indirectly through acquire and release.
 if(readeflags()&FL_IF){
   static int n;
   if(n++ == 0)
     cprintf("cpu called from %x with interrupts enabled\n",
       __builtin_return_address(0));
 }

 if(lapic)
   return lapic[ID]>>24;
 return 0;
}

然而,第二个问题来了:我们不可能每次引用cpu自身状态时都通过lapic获取编号啊,能不能弄一个全局变量把状态位置一次性存储下来呢?

像是这样, struct cpu *cpu; //全局变量,存储cpu自身状态 ,然后在初始化代码中 cpu = c ;

对于记录每个cpu正在运行的进程也有这样的问题,能不能写成: struct proc *proc; //全局变量,存储当前cpu正在运行的进程状态 

 

那么,第三个问题来了:每个cpu是独立并行的,在每个cpu上运行的内核代码都是一样的,页表也一样,这意味着全局变量cpu和proc的地址也是一样的,这样便不可以用来区分不同cpu的状态了。换句话说每个cpu上跑的内核代码都在相同的物理地址上, 虚拟地址也应该是一样的(有点不敢保证,求指教).

因此,我们需要一种方法,可以让我们在每个cpu中都用同一个符号记录状态,但这些符号却是映射到不同的地址。

既然页表一样,我们自然不能用一个绝对的数值来寻址啦,仔细想想,页表之上有什么?页表之上,还有段表啊。

所以我们需要用segment register来寻址,只要我们在建立段表时把该段都映射到不同的内存区域不就可以了,所以我们有了以下声明:用gs作为段寄存器,cpu指向[%gs],proc指向[%gs+4],

// Per-CPU variables, holding pointers to the
// current cpu and to the current process.
// The asm suffix tells gcc to use "%gs:0" to refer to cpu
// and "%gs:4" to refer to proc.  seginit sets up the
// %gs segment register so that %gs refers to the memory
// holding those two variables in the local cpu's struct cpu.
// This is similar to how thread-local variables are implemented
// in thread libraries such as Linux pthreads.
extern struct cpu *cpu asm("%gs:0");       // &cpus[cpunum()]
extern struct proc *proc asm("%gs:4");     // cpus[cpunum()].proc

其中为什么开头要用extern呢?我问过某大神,他说是因为gs段是在外部建立的,相当于外部定义的。。。

OK,最后一个问题来了,gs段应该指向哪,才能确保每个cpu的gs段都位于不同的区域?

最直观的想法当然是指向对应的cpus[num]内部啦,所以在struct cpu尾部增加两个域:

// Per-CPU state
struct cpu {
 uchar id;                    // Local APIC ID; index into cpus[] below
 struct context *scheduler;   // swtch() here to enter scheduler
 struct taskstate ts;         // Used by x86 to find stack for interrupt
 struct segdesc gdt[NSEGS];   // x86 global descriptor table
 volatile uint started;       // Has the CPU started?
 int ncli;                    // Depth of pushcli nesting.
 int intena;                  // Were interrupts enabled before pushcli?
 
 // Cpu-local storage variables; see below
 struct cpu *cpu;
 struct proc *proc;           // The currently-running process.
};


然后在建立段表时,增加gs段,并映射至尾部这两个域:

c->gdt[SEG_KCPU] = SEG(STA_W, &c->cpu, 8, 0);

完整实现在seginit函数中

// Set up CPU's kernel segment descriptors.
// Run once on entry on each CPU.
void
seginit(void)
{
 struct cpu *c;

 // Map "logical" addresses to virtual addresses using identity map.
 // Cannot share a CODE descriptor for both kernel and user
 // because it would have to have DPL_USR, but the CPU forbids
 // an interrupt from CPL=0 to DPL=3.
 c = &cpus[cpunum()];
 c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);
 c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);
 c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);
 c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);

 // Map cpu, and curproc
 c->gdt[SEG_KCPU] = SEG(STA_W, &c->cpu, 8, 0);

 lgdt(c->gdt, sizeof(c->gdt));
 loadgs(SEG_KCPU << 3);
 
 // Initialize cpu-local storage.
 cpu = c;
 proc = 0;
}

其实在这里cpu和proc变量跟线程局部存储的性质差不多,每个处理器都可以引用同一个变量,但这些变量都对应不同的存储区域。

有可能这种实现技巧跟TLS(线程局部存储)差不多,有空研究下TLS的实现看看是不是。


你可能感兴趣的:(cpu,信息,XV6)