8 新的挑战
8.1 APIC内存映射
要知道,编程时保证程序清晰有条理是非常重要的。因此,我本着程序和定义分离的原则,把所有的数据结构都提取到type.h里面去了,把所有全局变量都提到global.h里面去了。笔者还整理了一下定义。Loader加载的PagingInfoFlags被我删掉了,因为这些在KERNEL中也可以轻易检测。这些更改就不贴出来了。
另外,不知道为什么,这么一整理以后kernel.bin大小瞬间减小了很多。
我们在挑战APIC的时候,我们需要先了解什么是APIC。APIC(高级可编程中断控制器)可以用来代替我们之前设置的PIT和8259A,能够更好地控制硬件中断和处理器之间的协调。
APIC与别的接口不同,APIC的操作不通过寄存器,不通过端口,而直接通过内存操作。对FEE00000H或者更高地址的操作会被认为是操作APIC寄存器。我们得把这些地址通过页表映射在内核的地址上。
为了防止这部分地址被普通内存映射,我们把MAX_MAPPING改成0x30000000。
查阅了Intel大叔的文档,我们知道了APIC寄存器的范围是FEE0 0000H ~ FEE0 0400这1KB的空间,那么我们只要映射一个页面就够了。
我们来计算应该怎么映射页表
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就是a,edx就是d等等,如果获得edx:eax的64位值则传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位(BSP、AP)位和第11位(启用、禁用位)。这两位肯定是开着的。我们还要确认APIC的地址在我们能够映射的范围内,不会和一般页表冲突。对了,va2pte和va2pde是两个宏,也定义在了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里面,我们采用当前时间到公元元年1月1日0时0分0秒的毫秒数作为时间的表示。
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=1,mon=1,day=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 CMOS和RTC时钟
刚才我们好不容易才搞完时间,不用太可惜了。我们知道主板里面有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小时(0,hour寄存器第7位表示AM(0)或PM(1))或者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)
代码很好懂,BCD2BIN和BIN2BCD是定义在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的地址。(话说ACPI和APIC好难搞清楚啊)。
按照标准的说法,ACPI在0E0000h到0FFFFFh的BIOS只读数据区或者在EBDA(Extended 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打头,并且所有字段之和为0(checksum)。
我们下面来找找。
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的位置。怎么办呢?很简单,找找MADT(Multiple 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内存块都是被映射的(如果不是的话很麻烦哎,我懒得想)。接着,我们Call了acpi_find_rsdp。我们从结构里提取出主表,然后我原来是打算检查一下signature的,不过如果硬件坑我我也没办法,不检查了,省点空间。
我们可以从RSDT的length计算出来数组的长度,并且对一个表一个表解析。如果找到了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的地方读写数据。这两个地址我们叫做IOREGSEL和IOWIN。
我们学习本地 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)
很简单吧。对了,mfence和sfence是用来保证读写顺序不被流水线干扰的指令。接下来我们要让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存放1,xchg eax,[edx]会自动锁总线,于是[edx]里面会存放1,eax会存放原[edx]的值。如果锁已被获得,那么eax会变成1,这时候我们需要循环重新获得锁。如果我们获得了锁,那么eax是0,我们就可以返回了。
释放锁的时候,我们只要简单地把锁变量归零就可以了。
现在系统中就有自旋锁了,如果多进程共享数据的话就可以加锁。
下面一章,我要开始挑战输入(键盘、鼠标)了。