Norlit OS —— 自制操作系统 第8章 新的挑战

8  新的挑战

8.1         APIC内存映射

要知道,编程时保证程序清晰有条理是非常重要的。因此,我本着程序和定义分离的原则,把所有的数据结构都提取到type.h里面去了,把所有全局变量都提到global.h里面去了。笔者还整理了一下定义。Loader加载的PagingInfoFlags被我删掉了,因为这些在KERNEL中也可以轻易检测。这些更改就不贴出来了。

另外,不知道为什么,这么一整理以后kernel.bin大小瞬间减小了很多。

我们在挑战APIC的时候,我们需要先了解什么是APICAPIC(高级可编程中断控制器)可以用来代替我们之前设置的PIT8259A,能够更好地控制硬件中断和处理器之间的协调。

APIC与别的接口不同,APIC的操作不通过寄存器,不通过端口,而直接通过内存操作。对FEE00000H或者更高地址的操作会被认为是操作APIC寄存器。我们得把这些地址通过页表映射在内核的地址上。

         为了防止这部分地址被普通内存映射,我们把MAX_MAPPING改成0x30000000

         查阅了Intel大叔的文档,我们知道了APIC寄存器的范围是FEE0 0000H ~ FEE0 04001KB的空间,那么我们只要映射一个页面就够了。

         我们来计算应该怎么映射页表

FEE0 0000H =1111111011 1000000000 000000000000B

1111111011B =3FBH

1000000000B =200H

11111110111000000000B = FEE00H

由于flat_ptes只负责一般内存映射,我们需要另外分配一个页面,把这个页设置为存在、可写、系统页、不可缓存(0b11011)。

然后,我们把FEC0 0000H ~ FF00 0000H在内核PDE上指向这个页上,从而把这部分线性地址原封不动地映射到物理地址(va=pa)。

    u_addr* apic_ptes=ALLOC_PAGE();

    memset(apic_ptes,0, PAGE_SIZE);

    apic_ptes[APIC_REG_ADDR/0x1000-(APIC_REG_ADDR/0x1000/1024)*1024]=APIC_REG_ADDR+0b11011;

    kernel_pde[APIC_REG_ADDR/0x1000/1024]=va2pa(apic_ptes)+0b11011;

    __asm__ __volatile__("movl%0,%%cr3"::"a"(va2pa(kernel_pde)));

代码8.1.1异常简单的代码(chapter8/a/kernel/apic/apic.c)

 

我们刚才的代码在大多数电脑上是可用的,但是我们缺乏检验。

我们先检验APIC是否存在。这里笔者编写了几个宏,用起来很方便:

#define BIT_TEST(x,bit) ((x)&(1<<(bit)))

#define assert(assertion,str)do{if(!(assertion)){puts(str);io_clihlt();}}while(0)

#define GET_CPUID(id, reg) ({u_addr ret;__asm____volatile__("cpuid":"=" #reg(ret):"a"(id));ret;})

#define READ_MSR(reg) ({u64ret;__asm__ __volatile__("rdmsr":"=A"(ret):"c"(reg));ret;})

代码8.1.2常用宏(chapter8/a/kernel/include/proto.h)

这里面唯一一个比较复杂的是GET_CPUID宏。这个宏的第一个参数传调用cpuid时的eax,第二个参数传需要获取的寄存器(如果是eax就是aedx就是d等等,如果获得edx:eax64位值则传A)。

现在开始写新的init_apic

    assert(BIT_TEST(GET_CPUID(1,d),9),":-( APIC is not supported.");

   

    u64 msr=READ_MSR(IA32_APIC_BASE);

   

    assert(BIT_TEST(msr,8),":-( AP called init_apic.");

    assert(BIT_TEST(msr,11),":-( APIC is disabled");

    assert(!(msr&0xF00000000),":-( PAE is not supported");

   

    apic_base=msr&0xFFFFFF000;

    assert(apic_base>=PAGE_OFFSET+MAX_MAPPING,":-( APIC address is less than PAGE_OFFSET +MAX_MAPPING.");

 

    u32* apic_ptes=ALLOC_PAGE();

    memset(apic_ptes,0, PAGE_SIZE);

    apic_ptes[va2pte(apic_base)]=apic_base+0b11011;

    kernel_pde[va2pde(apic_base)]=va2pa(apic_ptes)+0b11011;

__asm__ __volatile__("movl %0,%%cr3"::"a"(va2pa(kernel_pde)));

代码8.1.3汰渍init_apic(chapter8/a/kernel/apic/apic.c)

我们把常量地址改为从IA32_APIC_BASE MSR中获得,并且确认这个地址是在32位模式下能够企及的。顺便,我们检查了msr的第8位(BSPAP)位和第11位(启用、禁用位)。这两位肯定是开着的。我们还要确认APIC的地址在我们能够映射的范围内,不会和一般页表冲突。对了,va2pteva2pde是两个宏,也定义在了proto.h里面。不过,这么一想,我们为什么不把APIC映射在固定位置呢!这样的话我们也就可以避免和一般页表冲突了!

u32* apic_ptes=ALLOC_PAGE();

    memset(apic_ptes,0, PAGE_SIZE);

    apic_ptes[va2pte(APIC_REG_ADDR)]=(u32)(msr&0xFFFFFF000)+0b11011;

kernel_pde[va2pde(APIC_REG_ADDR)]=va2pa(apic_ptes)+0b11011;

代码8.1.4改版(chapter8/a/kernel/apic/apic.c)

8.2         APIC基本操作

APIC的基本操作包括读寄存器和写寄存器。这两个操作我们用两个宏完成

static INLINE void apic_write(u_addr reg, u32 v)

{

   (*((volatile u32*)(APIC_REG_ADDR+(u_addr)(reg)))= v);

}

 

static INLINE u32 apic_read(u_addr reg)

{

   return*((volatile u32*)(APIC_REG_ADDR+reg));

}

代码8.2.1内联函数(chapter8/a/kernel/include/apic.h)

 

我们先做点最简单的操作,获取一下本地APIC ID

8.2.1 APIC ID

我们用dispInt(apic_read(APIC_ID_REG)>>24);打印一下ID,作为唯一的处理器,APICID应该是0。嗯,没错,的确是。APIC还包含很多东西,这些我们就以后再讲了,我们之前设置了一个wall_clock变量吧,我们这次要利用一下这个变量。

 

8.3         时间的存储和显示

时间在计算机中肯定不是像我们日常中用的这种格式存储。事实上,在计算机中,时间是用当前时间到一个固定时间点的秒数或者毫秒数表示的。我们采用了64位的时钟,那么我们为了更好地利用这么多位,我们当然要用毫秒数表示。顺便,这样的话我们也不需要记录jiffies了。在Norlit OS里面,我们采用当前时间到公元元年11000秒的毫秒数作为时间的表示。

Linux中产生这个时间的代码非常神奇,我们借鉴一下,写出了自己的代码:

/** This function's idea was borrowed from

       linux but modified tosuit Norlit OS **/

FASTCALL u64 mktime(u32 year, u8 mon, u8 day, u8 hour, u8 min, u8 sec){

   if((s8)(mon-=2)<=0){

       mon+=12;

       year-=1; 

   } 

   return((((u64)(year/4-year/100+year/400+367*mon/12+day)

      +year*365-337)*24+hour)*60+min)*60+sec;

}

代码8.3.1产生式(chapter8/a/kernel/lib.c)

由于要考虑闰年的因素,我们首先把1月和2月当作上一年的13月和14月,使得多出来这一天变成一年的最后一天,方便计算。

year/4-year/100+year/400则计算之前一共有多少个闰年,367*mon/12刚好可以产生大小月排列的序列,得到这个月第一天距离第一个月的天数。day则是当月的日期。然后加上每年的365天,这样下来,我们可以精确地得到一到已经过去的天数值。但是为什么要-337呢?这是因为我们之前减去了两个月,所以需要加回来,我们算月数的时候又多算了一个月(当月还没过去),也多算了一年。这样下来,我们需要一个修正。这个修正的值其实就是year=1mon=1day=1时,也就是我们的起点,计算所得的(year/4-year/100+year/400+367*mon/12+day)+year*365的值。然后换算成毫秒。真是完美的代码!但是怎么还原成显示用的格式呢?

还原算法是我自己想的,所以很长又不精致:

FASTCALLvoid gmtime(u64 time,struct tm*ret){

   static u16 mday[]={0,31,59,90,120,151,181,212,243,273,304,334,365};

    u64 mod;

    time=do_divmod64(time,1000,&mod);

    ret->milli=mod;

    time=do_divmod64(time,60,&mod);

    ret->second=mod;

    time=do_divmod64(time,60,&mod);

    ret->minute=mod;

    time=do_divmod64(time,24,&mod);

    ret->hour=mod;

    do_divmod64(time+1,7,&mod);

    ret->weekday=mod;

    u32 year=do_divmod64(time,365,&time);

    s32 days=time-year/4+year/100-year/400+1;

   while(days<=0){

       days+=365+((!(year%4)&&(year%100))||!(year%400));

       year--;

   }

    ret->year=++year;

   if((!(year%4)&&(year%100))||!(year%400)){

      if(days==60){

           ret->month=2;

           ret->day=29;

          return;

      }elseif(days>60)days--;

   }

    u8 bmon=days/30;

    u8 mon=days/31;

   if(bmon!=mon){

      if(days>mday[bmon])mon=bmon;

   }

    ret->month=mon+1;

    ret->day=days-mday[mon];

}

代码8.3.2还原式(chapter8/a/kernel/lib.c)

struct tm 结构是定义在type.h里的,成员在这段代码中看的一清二楚,我就不贴了。你可能会有疑问为什么我用了do_divmod64函数。这个函数用来完成除法和取余。由于32位的处理器上原生不支持64位除法和取余,所以我们需要自己完成。前面的代码很简单,分离出天数。求星期几是用(天数+1)取余7的得到的(0代表周日),因为公元元年元旦是周一。

然后我们先除365得到年数,然后修正闰年,最后获取月份。月份我稍微优化了一下,因为1天所在的月份可能是days/30,也可能是days/31

do_divmod64移植自libgcc,我就不讲解了,如果大家有兴趣的话可以自行研究计算机代数系统。

/** These three function was borrowed from libgcc **/

FASTCALL u64 do_divmod64(u64 num, u64 den, u64 *rem_p){

    u64 quot=0, qbit=1;

   if(den==0){

      return1/((u_addr)den);/* Intentional divide by zero,without

                             triggeringa compiler warning which

                             wouldabort the build */

   }

   /* Left-justifydenominator and count shift */

   while((s64)den>=0){

       den<<=1;

       qbit<<=1;

   }

   while(qbit){

      if(den<=num){

           num-=den;

           quot+=qbit;

      }

       den>>=1;

       qbit>>=1;

   }

   if(rem_p)*rem_p=num;

   return quot;

}

 

FASTCALL u64 do_mod64(u64 num, u64 den){

    u64 v;

    do_divmod64(num, den,&v);

   return v;

}

 

FASTCALL u64 do_div64(u64 num, u64 den){

   return do_divmod64(num, den,NULL);

}

代码8.3.3高效除法(chapter8/a/kernel/lib.c)

8.4         CMOSRTC时钟

刚才我们好不容易才搞完时间,不用太可惜了。我们知道主板里面有CMOS元件,里面自带时钟。我们现在就来读取一下。

CMOS操作时,先将地址写入0x70端口,然后从0x71中读写数据。

Address

Description

0x00 

Current second

0x01 

Alarm second

0x02 

Current minute

0x03 

Alarm minute

0x04 

Current hour

0x05 

Alarm hour

0x06 

Current day of week 

0x07 

Current date of month

0x08 

Current month

0x09 

Current year

0x0a 

RTC Status Register A 

0x0b 

RTC Status Register B 

0x0c 

RTC Status Register C 

0x0d 

RTC Status Register D 

8.4.1 RTC寄存器

要注意的时,RTC时钟是一直在走的,如果在读取过程中刚好更新了怎么办?很简单,我们先读取RTC状态寄存器A的第七位保证没在更新,然后开始读取。为了防止读取时更新,我们必须再读一遍,直至读到相同的值为止。写的时候,我们先写秒为0,这样我们就有充足的时间来写其他寄存器了。最后再把秒写入。读写时,状态寄存器B的第一位表示12小时(0hour寄存器第7位表示AM0)或PM1))或者24小时(1)。状态寄存器B的第二位表示寄存器的值使用BCD码(0)或者二进制(1)存储。

FASTCALL u8 CMOS_read(u_addr reg){

    io_out8(CMOS_ADDRESS_REG, reg);

   return io_in8(CMOS_DATA_REG);

}

 

FASTCALLvoid CMOS_write(u_addr reg, u8 value){

    io_out8(CMOS_ADDRESS_REG, reg);

    io_out8(CMOS_DATA_REG, value);

}

 

FASTCALLvoid write_RTC(u64 wall_clock){

   struct tm tmm;

    gmtime(wall_clock,&tmm);

    u8 registerB=CMOS_read(0x0B);

    u8 hour_correction=0;

   if(!BIT_TEST(registerB,1)){

      if(tmm.hour==0){

           tmm.hour=12;

           hour_correction=0x80;

      }elseif(tmm.hour>12){

           tmm.hour-=12;

           hour_correction=0x80;

      }

   }

   if(!BIT_TEST(registerB,2)){

       tmm.second=BIN2BCD(tmm.second);

       tmm.minute=BIN2BCD(tmm.minute);

       tmm.hour=BIN2BCD(tmm.hour);

       tmm.day=BIN2BCD(tmm.day);

       tmm.month=BIN2BCD(tmm.month);

       tmm.year=BIN2BCD(tmm.year%100);

   }

    tmm.hour|=hour_correction;

   while(BIT_TEST(CMOS_read(0x0A),7));

    CMOS_write(0x00,0);

   while(BIT_TEST(CMOS_read(0x0A),7));

    CMOS_write(0x02, tmm.minute);

    CMOS_write(0x04, tmm.hour);

    CMOS_write(0x06, tmm.weekday);

    CMOS_write(0x07, tmm.day);

    CMOS_write(0x08, tmm.month);

    CMOS_write(0x09, tmm.year);

    CMOS_write(0x00, tmm.second);

}

 

FASTCALL u64 read_RTC(){

    u8 last_second, last_minute, last_hour, last_day, last_month, last_year;

    u8 second, minute, hour, day, month;

    u8 hour_correction=0;

    u8 registerB;

    u16 year;

   

   while(BIT_TEST(CMOS_read(0x0A),7));

    second=CMOS_read(0x00);

    minute=CMOS_read(0x02);

    hour=CMOS_read(0x04);

    day=CMOS_read(0x07);

    month=CMOS_read(0x08);

    year=CMOS_read(0x09);

   do{

       last_second= second;

       last_minute= minute;

       last_hour= hour;

       last_day= day;

       last_month= month;

       last_year= year;

      while(BIT_TEST(CMOS_read(0x0A),7));

       second=CMOS_read(0x00);

       minute=CMOS_read(0x02);

       hour=CMOS_read(0x04);

       day=CMOS_read(0x07);

       month=CMOS_read(0x08);

       year=CMOS_read(0x09);

   }while((last_second!=second)||(last_minute!=minute)||(last_hour!=hour)||

      (last_day!=day)||(last_month!=month)||(last_year!=year));

   

    registerB=CMOS_read(0x0B);

   

   if((hour&0x80)&&(!BIT_TEST(registerB,1))){

       hour=hour&~0x80;

       hour_correction=12;

   }

   

   if(!BIT_TEST(registerB,2)){

       second=BCD2BIN(second);

       minute=BCD2BIN(minute);

       hour=BCD2BIN(hour);

       day=BCD2BIN(day);

       month=BCD2BIN(month);

       year=BCD2BIN(year);

   }

   

    hour=(hour+hour_correction)%24;

    year+=2000;

   

   return mktime(year,month,day,hour,minute,second)*1000;

}

8.4.2 RTC驱动程序(chapter8/a/kernel/driver/rtc.c)

代码很好懂,BCD2BINBIN2BCD是定义在lib.c里的函数。

现在我们在初始化timer的时候加一句:startup_time=wall_clock=read_RTC();

startup_time是我们定义的用来存放开机时间的变量。我们现在可以利用wall_clock来写一个延迟用的程序:

FASTCALLvoid delay(u_addr millisec){

    u64 end=wall_clock+millisec;

   while(wall_clock<end);

}

代码8.4.1 delay(chapter8/a/kernel/lib.c)

我们再来写个小时钟:

ASMLINKAGEvoid testClock(){

   struct tm tmm;

   while(1){

       gmtime(wall_clock,&tmm);

dispByte(BIN2BCD(tmm.month));puts("/");dispByte(BIN2BCD(tmm.day));puts(" ");

dispByte(BIN2BCD(tmm.hour));puts(":");dispByte(BIN2BCD(tmm.minute));puts(":");dispByte(BIN2BCD(tmm.second));puts(" ");

       delay(1000);

       puts("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");

   }

}

代码8.4.2时钟(chapter8/a/kernel/process/proc.c)

我们巧妙利用一下BIN2BCD来显示10进制。

好了,我们看一下效果吧!阿勒?怎么不动了?

这个问题其实是GCC的优化太彻底了。GCC认为wall_clock变量是不会变化的,一旦陷入while,就永远不会离开了。我们在wall_clock前加入volatile就不会有问题了。以后也是这样。所有可能会在程序外修改的变量都要加volatile。重新Make

8.4.1计时器工作了!

 

太棒了!我们的操作系统又向实用迈出了一大步!

 

8.5         ACPI(高级电源管理)

我其实并不想这么早就搞高级电源管理。但是没办法,为了实现I/O APIC。我们必须这样做。

ACPI提供了一组表格。在这组表格里,我们能找到很多东西。现在我们就要获取I/O APIC的地址。(话说ACPIAPIC好难搞清楚啊)。

按照标准的说法,ACPI0E0000h0FFFFFhBIOS只读数据区或者在EBDAExtended BIOS Data Area)的数据区里。EBDA数据区的地址可以在0x40E处获得(获得的是实模式下的段)。

在这些地址,16字节对齐的区域,如果我们发现了一个RSD PTR结构的话,就宣告ACPI入口的发现。

struct ACPI_RSDP{

    u8 signature[8];//RSD PTR

    u8 checksum;

    u8 oem_id[6];

    u8 revision;

    u32 rsdt;

    u32 length;

    u64 xsdt;

    u8 ext_checksum;

    u8 reserved[3];

};

代码8.5.1 RSD PTR(chapter8/b/kernel/include/type.h)

         这个结构的特点是以RSD PTR打头,并且所有字段之和为0checksum)。

我们下面来找找。

INITIALIZERstatic u_addr acpi_check_rsdp(struct ACPI_RSDP* rsdp){

   /* Refers to thepage 112 of the ACPI Specificaton 4.0a

     * We check if RSDP's checksum iscorrect */

   if(checksum(rsdp,20)!=0)return0;

   /* We check ifXSDT existed */

   if(rsdp->revision!=0){

      if(checksum(rsdp, rsdp->length)!=0)return0;

   }else{

      /*If XSDT is not existed, we turn to use DSDT */

       rsdp->length=20;

   }

   return1;

}

 

INITIALIZERstaticstructACPI_RSDP* acpi_find_rsdp(){

   /* Refers to thepage 111 of the ACPI Specificaton 4.0a */

    u8* sig;

   /* ACPI's RSDPcan be located in the BIOS read-only

     *  memory space between 0E0000hand 0FFFFFh. */

   for(sig=(u8*)0xC00E0000;sig<(u8*)0xC00FFFFF;sig+=16)

      /*We check if we found a RSDP */

      if(memcmp(sig,"RSD PTR ",8)==0&&acpi_check_rsdp((struct ACPI_RSDP*)sig))

          return(struct ACPI_RSDP*)sig;

   /* * We firstfound out the EBDA(Extended BIOS Data Area)'s Address */

    u_addr ebda=*((u16*)(0x40E+ PAGE_OFFSET))*0x10+PAGE_OFFSET;  

   /* ACPI's RSDPcan also be located in,

     * the first 1 KB of the EBDA. */

   for(sig=(u8*)ebda;sig<(u8*)(ebda+0x3FF);sig+=16)

      /*We check if the signature is RSD PTR */

      if(memcmp(sig,"RSD PTR ",8)==0&&acpi_check_rsdp((struct ACPI_RSDP*)sig))

          return(struct ACPI_RSDP*)sig;

   returnNULL;

}

代码8.5.2寻找你(chapter8/b/kernel/driver/acpi.c)

 

Checksum检查的代码过于简单就不写了。找到了RSDP以后,我们要提取I/O APIC的位置。怎么办呢?很简单,找找MADTMultiple APIC Description Table)。

我们首先通过RSD PTR找到RSDT。在RSDT里面有一个数组用来存储指向各表的指针。

INITIALIZER FASTCALLvoid init_acpi(){

   {

       BootParam* bp=(BootParam*)BOOT_PARAM_POS;

       u32 bplen;

       ARDSItem* ai=bp->items;

      for(bplen=bp->len;bplen>0;bplen--,ai++){

          switch(ai->type){

          case3:{

              assert((ai->base+ai->limit<=MAX_MAPPING),":-( ACPIReallocation Fails.");

          }

          case4:

             break;

          }

      }

   }

   struct ACPI_RSDP* rsdp=acpi_find_rsdp();

    assert(rsdp!=NULL,":-( No ACPI Support.");

   struct ACPI_RSDT* rsdt=(struct ACPI_RSDT*)(rsdp->rsdt+PAGE_OFFSET);

   assert(rsdt->header.signature==0x54445352,":-(Failed to initialize ACPI.");

      //0x54445352="RSDT"

    u_addr max_entries=(rsdt->header.length-offsetof(struct ACPI_RSDT, entry))/4;

    u_addr index=0;

    puts("ACPI Found.\r\n");

   for(index=0;index<max_entries;index++){

      struct ACPI_HEADER*entry=(struct ACPI_HEADER*)(rsdt->entry[index]+PAGE_OFFSET);

      switch(entry->signature){

      case0x43495041:{ //0x43495041="MADT"

           acpi_apic((struct ACPI_MADT*)entry);

          break;

      }

      }

   }

    puts("\r\n");

    assert(acpi_fadt!=0,":-( Unabled to find FADT.");

}

 

struct ACPI_HEADER{

    u32 signature;

    u32 length;

    u8 revision;

    u8 checksum;

    u8 oem_id[6];

    u8 oem_table_id[8];

    u32 oem_revision;

    u32 creator_id;

    u32 creator_revision;

};

 

struct ACPI_RSDT{

   struct ACPI_HEADER header;

    u32 entry[0];

};

代码8.5.3主初始化代码(chapter8/b/kernel/driver/acpi.c)

你可以看到我们首先确认每一个ACPI内存块都是被映射的(如果不是的话很麻烦哎,我懒得想)。接着,我们Callacpi_find_rsdp。我们从结构里提取出主表,然后我原来是打算检查一下signature的,不过如果硬件坑我我也没办法,不检查了,省点空间。

我们可以从RSDTlength计算出来数组的长度,并且对一个表一个表解析。如果找到了MADT,我们就进一步分析。

INITIALIZERstaticvoidacpi_apic(struct ACPI_MADT* madt){

    u32 length=madt->header.length-offsetof(struct ACPI_MADT,apic_str);

    u8 pros=0;

   

    u32 io_apic_base=0;

   

   struct ACPI_APIC_HEADER* header=madt->apic_str;

   while(length){

      switch(header->type){

      case0:{

           pros++;

          break;

      }

      case1:{

          struct ACPI_IO_APIC* apic=(struct ACPI_IO_APIC*)header;

           assert(io_apic_base==0,":-( Mulitiple I/OAPIC Not Supported.");

           io_apic_base=apic->io_apic_addr;

          break;

      }

      case2:{

          struct ACPI_INT_OVERRIDE* irq=(struct ACPI_INT_OVERRIDE*)header;

           IRQOverride[irq->source]=irq->interrupt;

          break;

      }

      }

       length-=header->length;

       header=(struct ACPI_APIC_HEADER*)((u_addr)header+header->length);

   }

    assert(io_apic_base!=0,"No I/O APIC Not Supported.");

    init_apic(io_apic_base);

   

   if(pros>1)puts(":-) MultipleProcessor Detected!\r\n");

}

代码8.5.4解析MADT(chapter8/b/kernel/driver/apic.c)

你现在还不知道MADT的结构长怎么样。你是不是很想看呢?

妈妈说猴急的人是会被讨厌的哦。

struct ACPI_APIC_HEADER{

    u8 type;

    u8 length;

};

 

struct ACPI_MADT{

   struct ACPI_HEADER header;

    u32 apic_address;

    u32 flag;

   struct ACPI_APIC_HEADER apic_str[0];

};

 

struct ACPI_IO_APIC{

    u8 type;

    u8 length;

    u8 io_apic_id;

    u8 reserved;

    u32 io_apic_addr;

    u32 int_base;

};

 

struct ACPI_INT_OVERRIDE{

    u8 type;

    u8 length;

    u8 bus;/* Always 0 */

    u8 source;

    u32 interrupt;

    u8 flags;

};

代码8.5.5 MADT长这鬼样(chapter8/b/kernel/include/type.h)

这些ACPI_INT_OVERRIDE等等的结构被直接附在MADT后面。我们还得判断length来看看有没有读完。Type字段表示类型,0表示本地APIC,我们目前不用,就看看处理器个数,1表示I/O APIC,里面有基地址。2表示重载ISA总线IRQ重载,我们用一个IRQOverride的重定向表记录下来,以后要用。

得到了I/O APIC的基地址以后就爽了,我们可以开始初始化外部中断了。

对了,APIC初始化的入口我放在ACPI里了。顺便加了一个参数传刚读的基地址。

8.6         I/O APIC

I/O APIC的资料很难找,是一个叫Intel 2093AA I/OADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (IOAPIC)PDF规范。

读写I/O APIC时,往基地址写寄存器号码,然后往基地址+10h的地方读写数据。这两个地址我们叫做IOREGSELIOWIN

我们学习本地 APIC时的做法,先映射,然后直接往固定地址读写数据。

读写I/O APIC的函数是这样的:

static INLINE u32io_apic_read(u32reg){

   *((volatile u32*)APIC_IOREGSEL)=reg;

    asm_mfence();

   return*((volatile u32*)APIC_IOWIN);

}

 

static INLINE u32io_apic_write(u32reg, u32 v){

   *((volatile u32*)APIC_IOREGSEL)=reg;

    asm_sfence();

   *((volatile u32*)APIC_IOWIN)=v;

}

代码8.6.1读写I/O APIC(chapter8/b/kernel/include/apic.h)

         很简单吧。对了,mfencesfence是用来保证读写顺序不被流水线干扰的指令。接下来我们要让I/OAPIC把中断映射到本地APIC上面。

8.6.1重定向表

8.6.2属性说明(推送模式被我简化了,只剩fixed了)

按照这张表,我们应该在高位写上我们得到的本地APIC ID,然后在把它Mask掉,禁用所有的外部中断。

好了,I/O APIC初始化完了!我们写一个注册函数吧。

 

u8 apic_id=apic_read(APIC_ID_REG)>>24;

    u32 id;

   for(id=0;id<16;id++){

       io_apic_write(IO_APIC_RED(id)+1,apic_id<<(56-32));

       io_apic_write(IO_APIC_RED(id),0x10000+0x20+id);

   }

   for(;id<24;id++){

       io_apic_write(IO_APIC_RED(id)+1,apic_id<<(56-32));

       io_apic_write(IO_APIC_RED(id),0x10000);

}

我是一条华丽的分割线//

FASTCALLvoid registerIRQ(u8 irq, int_handler inth){

    handlerTable[irq]=inth;

}

 

ASMLINKAGEvoid enableIRQ(u8 irq){

    u32 adr=IO_APIC_RED(IRQOverride[irq]);

    io_apic_write(adr,io_apic_read(adr)&(~0x10000));

}

 

ASMLINKAGEvoid disableIRQ(u8 irq){

    u32 adr=IO_APIC_RED(IRQOverride[irq]);

    io_apic_write(adr,io_apic_read(adr)|0x10000);

}

8.6.3 IRQ重定向(chapter8/b/kernel/driver/apic.c)

这个handlerTable是什么呢?这是我们存中断服务程序的表。我们捕捉到中断以后call表里的处理程序。

int_handler handlerTable[16]={

    unknown_irq,unknown_irq,unknown_irq,unknown_irq,

    unknown_irq,unknown_irq,unknown_irq,unknown_irq,

    unknown_irq,unknown_irq,unknown_irq,unknown_irq,

    unknown_irq,unknown_irq,unknown_irq,unknown_irq};

8.6.4定义(chapter8/b/kernel/include/global.h)

         Interrupt.asm也做了大的修改。

%macro HWINTHandler 1

hwint%1:

   call   save

   push  %1

   call  [handlerTable+%1*4]

   add   esp,4

   mov   eax,0xFEE000B0

   mov   dword[eax],0

   ret

%endmacro

HWINTHandler00

HWINTHandler01

HWINTHandler02

HWINTHandler03

HWINTHandler04

HWINTHandler05

HWINTHandler06

HWINTHandler07

HWINTHandler08

HWINTHandler09

HWINTHandler10

HWINTHandler11

HWINTHandler12

HWINTHandler13

HWINTHandler14

HWINTHandler15

 

unknown_irq:

   push  21

exception:

   call   exception_handler

   add   esp,4*2

   hlt

8.6.5宏展开的中断处理(chapter8/b/kernel/include/global.h)

 

对了,我们发送EOI到本地APIC表示中断已经完成受理。

现在只要把init_8260A里的所有IRQ Mask掉,再用I/O APIC注册一下时钟中断就好了。

registerIRQ(0, irq0_handler);

enableIRQ(0);

至此,我们成功地远离了PIC,投入了I/OAPIC的怀抱。

8.7         原子操作

这里的原子操作指的肯定不是超能力。原子操作是保证多处理器或者多进程、多线程程序运行时安全而被设计出来的。这些操作是不可中断的(或者中断也不会出问题的)。

Intel大叔保证的原子操作有:读写字节、读写对齐的字、读写对齐的双字。但是我们还有很多操作需要保证同时只有一个进程访问怎么办呢?

解决方案是临界区,互斥区中的代码或数据同一时刻只有一个进程可以访问,也就是说,使用互斥的手段来防止这种冲突。然而,怎么互斥呢?我们有这些手段:

1.      关中断

这种方案只能保证单个CPU有效,而且还不能让用户进程使用,对于内核来说可能很方便,但肯定不是通用的互斥机制。

2.      锁变量

用一个锁变量可以有效防止临界区的冲突。但是,操作一个锁的过程中有可能会被中断。怎么办呢?我们引入了自旋锁。

 

8.8         自旋锁

lock前缀可以显式要求处理器锁总线,防止出现多个处理器抢占一个锁的情况。

我们来看一下典型的自旋锁代码:

spin_lock:

   mov   edx,[esp+4]

.spin:

   mov   eax,1

   xchg  eax,[edx]

   or    eax,eax

jz     .ret

   pause

   jmp    .spin

.ret:

  ret

 

spin_unlock:

   mov   edx,[esp+4]

   mov   [edx],0

ret

代码8.8.1典型的自旋锁(chapter8/b/kernel/process/proc.asm)

加锁的时候,edx里面存放锁的地址,eax存放1xchg eax,[edx]会自动锁总线,于是[edx]里面会存放1eax会存放原[edx]的值。如果锁已被获得,那么eax会变成1,这时候我们需要循环重新获得锁。如果我们获得了锁,那么eax0,我们就可以返回了。

释放锁的时候,我们只要简单地把锁变量归零就可以了。

现在系统中就有自旋锁了,如果多进程共享数据的话就可以加锁。

下面一章,我要开始挑战输入(键盘、鼠标)了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值