30天自制操作系统(第7-9天)

7FIFO与鼠标控制

7.1 获取按键编码

#define PORT_KEYDAT 0x0060
void inthandler21(int *esp)
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    unsigned char data, s[4];
    /* void io_out8(int port,int data);将数据0x61写入端口PIC0_OCW2
        通知PIC"IRQ-01已经受理完毕" */
    io_out8(PIC0_OCW2, 0x61); 

    /* int io_in8(int port);从端口处读取8位数据,即什么操作导致了中断 */
    data = io_in8(PORT_KEYDAT);

    sprintf(s, "%02X", data);
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
    return;
}
0x0060 的设备号表示为键盘,发生了IRQ1中断,汇总信息得到数据0x61。

7.2 加快中断处理

创建一个结构体,该结构体中存有数据和缓冲区是否为空的标识。

struct KEYBUF {
        unsigned char data, flag;
};
判断当该结构体中的flag=0时,表示可以进行数据写入,则更新data,并将flag置为1。
for (;;) {
    io_cli();//屏蔽中断
    if (keybuf.flag == 0) {
        io_stihlt();//开放中断,并待机
    } else {
        i = keybuf.data;//读出缓冲区数据
        keybuf.flag = 0;//表示缓冲区可写入
        io_sti();//开放中断
        sprintf(s, "%02X", i);
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);//绘制矩形框
        putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);//打印字符串s
    }
}

7.3 制作FIFO缓冲区

在7.2节中,每次只能存储一个字节,并不方便。所以将KEYBUF结构体中的data改成数组类型。

struct KEYBUF {
    unsigned char data[32];//存储数据的缓冲区
    int next;//即将写入数据的位置下标
};
void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01已经受理完毕 */
    data = io_in8(PORT_KEYDAT);//读取按下键盘中的某一按键
    /// 当keybuf.next=32时,不再向缓冲区写入数据,也不是使keybuf.next增加
    if (keybuf.next < 32) {
        keybuf.data[keybuf.next] = data;
        keybuf.next++;
    }
    return;
}

for (;;) {
    io_cli();
    if (keybuf.next == 0) {
        io_stihlt();
    } else {
        i = keybuf.data[0];
        //keybuf.next是即将写入数据的位置下标,所以已经成功写入了keybuf.next-1个数据
        keybuf.next--;
        for (j = 0; j < keybuf.next; j++) {
            keybuf.data[j] = keybuf.data[j + 1];
        }
        io_sti();
        sprintf(s, "%02X", i);
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
        putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
    }
}

7.4 改善FIFO缓冲区

考虑到缓冲区只能写入32个字符,无法满足日常需求。如果只是单纯地扩大缓冲区,前方已被读出的缓冲区就浪费了,不再写入数据。所以采取链表形式,用 next_r、next_w 代表下一个将要读出、写入的位置。由于函数void inthandler21(int *esp) 功能为向缓冲区中写入数据,所以next_w递增,并当该值到达链表尾部(32)时转向链表头部(0),实现循环利用创建的缓冲区。并改写从缓冲区读出数据的程序,每次读出一个。

7.5 整理FIFO缓冲区

该节不再指定缓冲区的长度,并改写KEYBUF结构体为FIFO8结构体。

// p下一个数据写入地址  q下一个数据读出地址  size已经写入的数据个数
// free缓冲区里没有数据的字节数 flags该缓冲区是否可用
struct FIFO8 {
	unsigned char *buf;
	int p, q, size, free, flags;
};

/* fifo.c */
#define FLAGS_OVERRUN		0x0001
// 对FIFO8结构体中的元素进行初始化
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf){
	fifo->buf=buf;	
	fifo->p=0;		/* 下一个写入地址 */
	fifo->q=0;		/* 下一个读出地址 */
	fifo->size=size;/* 缓冲区大小 */
	fifo->free=size;/* 可用缓冲区大小 */
	fifo->flags=0;
	return;
}

// 向fifo中写入data,即更新缓冲区数据、p++和free--
int fifo8_put(struct FIFO8 *fifo, unsigned char data){
	if(fifo->free==0){/* 没有可用的缓冲区,设置flags并返回-1 */
		fifo->flags|=FLAGS_OVERRUN;
		return -1;
	}
	fifo->free--;
	fifo->buf[fifo->p]=data;
	fifo->p++;
	if(fifo->p == fifo->size)fifo->p=0;
	return 0;
}

// 从fifo读出一个数据,即更新缓冲区数据、q++和free++
int fifo8_get(struct FIFO8 *fifo){
	int data;
	if(fifo->free==fifo->size){/* 缓冲区内没有数据,直接返回-1 */
		return -1;
	}
	data = fifo->buf[fifo->q];
	fifo->q++;
	fifo->free++;
	if(fifo->q==fifo->size)fifo->q=0;
	return data;
}

/* 报告缓冲区内已存数据的个数 */
int fifo8_status(struct FIFO8 *fifo){
	return fifo->size - fifo->free;
}

7.6 总算讲到鼠标了

#define PORT_KEYDAT 			0x0060/* 数据端口 */
#define PORT_KEYSTA 			0x0064/* 端口状态 */
#define PORT_KEYCMD 			0x0064/* 命令端口 */
#define KEYSTA_SEND_NOTREADY 	0x02
#define KEYCMD_WRITE_MODE 		0x60/* 模式设定指令 */
#define KBC_MODE 				0x47/* 利用鼠标模式的模式号码是0x47 */
//等待键盘控制电路准备完毕
void wait_KBC_sendready(void){
	/*如果键盘控制电路可以接受CPU指令了,CPU从设备号码0x0064处所读
		取的数据的倒数第二位(从低位开始数的第二位)应该是0*/
	for(;;){
		if((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY)==0)
			break;
	}
	return;
}

void init_keyboard(void){
	wait_KBC_sendready();
	io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);//确认可否往命令端口发送指令信息
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, KBC_MODE);//向数据端口发送模式设定指令
	return;
}

7.7 从鼠标接收数据

/*	#define KEYCMD_SENDTO_MOUSE 0xd4
	#define MOUSECMD_ENABLE 0xf4*/
void enable_mouse(struct MOUSE_DEC *mdec){
	/* 激活鼠标,如果往键盘控制电路发送指令0xd4,下一个数据就会自动发送给鼠标*/
	wait_KBC_sendready();
	io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);//确认可否往命令端口发送指令信息
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);//向数据端口发送模式设定指令
	mdec->phase=0;/* 进入等待鼠标的0xfa的状态   enable_mouse()函数设置初始值MOUSECMD_ENABLE=0xfa */
	return; /* 顺利的话,键盘控制其会返送回ACK(0xfa)*/
}

8天 鼠标控制与32位模式切换

8.1 鼠标解读(1

unsigned char mouse_dbuf[3], mouse_phase;
enable_mouse();
mouse_phase = 0; /* 进入到等待鼠标的0xfa的状态 */
for (;;) {
    io_cli();
    if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {// keyfifo和mousefifo都没有数据
        io_stihlt();
    } else {// keyfifo、mousefifo中至少有一个有数据
        if (fifo8_status(&keyfifo) != 0) {//keyfifo有数据,即键盘有输入数据
            i = fifo8_get(&keyfifo);
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        } else if (fifo8_status(&mousefifo) != 0) {
            /*//mousefifo有数据,即鼠标有输入数据。把最初读到的0xfa舍弃掉,之后每次从鼠标那里送过来的数据都应该是3个字节一组的,所以要连续读取。*/
            i = fifo8_get(&mousefifo);
            io_sti();
            if (mouse_phase == 0) {
                /* 等待鼠标的0xfa的状态 */
                if (i == 0xfa) {
                    mouse_phase = 1;
                }
            } else if (mouse_phase == 1) {
                /* 等待鼠标的第一字节 */
                mouse_dbuf[0] = i;
                mouse_phase = 2;
            } else if (mouse_phase == 2) {
                /* 等待鼠标的第二字节 */
                mouse_dbuf[1] = i;
                mouse_phase = 3;
            } else if (mouse_phase == 3) {
                /* 等待鼠标的第三字节 */
                mouse_dbuf[2] = i;
                mouse_phase = 1;
                /* 鼠标的3个字节都齐了,显示出来 */
                sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
            }
        }
    }
}

8.2 稍事整理

将8.1节中的鼠标读取3字节的功能封装成函数int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat) 。

struct MOUSE_DEC {
//buf为鼠标的三元素 phase为鼠标的状态,分为0/1/2/3,0为等待状态,其他数字为等待写入第几个字节
    unsigned char buf[3], phase;
};

//dat为从鼠标缓冲区中读出的数据
//当读入三个数据时返回1,其他情形返回0
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
    if (mdec->phase == 0) {
        /* 等待鼠标的0xfa的阶段 */
        if (dat == 0xfa) {
            mdec->phase = 1;
        }
        return 0;
    }
    if (mdec->phase == 1) {
        /* 等待鼠标第一字节的阶段 */
        mdec->buf[0] = dat;
        mdec->phase = 2;
        return 0;
    }
    if (mdec->phase == 2) {
        /* 等待鼠标第二字节的阶段 */
        mdec->buf[1] = dat;
        mdec->phase = 3;
        return 0;
    }
    if (mdec->phase == 3) {
        /* 等待鼠标第二字节的阶段 */
        mdec->buf[2] = dat;
        mdec->phase = 1;
        return 1;
    }
    return -1; /* 应该不可能到这里来 */
}

8.3 鼠标解读(2

struct MOUSE_DEC{
	unsigned char buf[3], phase;
	int x, y, btn;/* x,y存储鼠标的坐标,btn为鼠标的按键状态信息 */
};


mdec->btn = mdec->buf[0] & 0x07;//鼠标键的状态,放在buf[0]的低3位
mdec->x = mdec->buf[1];
mdec->y = mdec->buf[2];
if ((mdec->buf[0] & 0x10) != 0) {// 参考注释
    mdec->x |= 0xffffff00;
}
if ((mdec->buf[0] & 0x20) != 0) {
    mdec->y |= 0xffffff00;
}
mdec->y = - mdec->y; /* 鼠标的y方向与画面符号相反 */
注释:x y ,基本上是直接使用 buf[1] buf[2] ,但是需要使用第一字节中对鼠标移动有 反应的几位(参考第一节的叙述)信息,将 x y 的第 8 位及第 8位以后全部都设成 1 ,或全部都保留为 0 。这样就能正确地解读 x y

8.4 移动鼠标指针

} else if (fifo8_status(&mousefifo) != 0) {
    i = fifo8_get(&mousefifo);
    io_sti();
    if (mouse_decode(&mdec, i) != 0) {
        /* 数据的3个字节都齐了,显示出来 */
        sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
        if ((mdec.btn & 0x01) != 0) {
            s[1] = 'L';
        }
        if ((mdec.btn & 0x02) != 0) {
            s[3] = 'R';
        }
        if ((mdec.btn & 0x04) != 0) {
            s[2] = 'C';
        }
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
        putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
        /* 鼠标指针的移动 */
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15); /* 隐藏鼠标 */
        //更新鼠标的坐标        
        mx += mdec.x;
        my += mdec.y;
        //防止越界
        if (mx < 0) mx = 0;
        if (my < 0) my = 0;
        if (mx > binfo->scrnx - 16) mx = binfo->scrnx - 16;
        if (my > binfo->scrny - 16) my = binfo->scrny - 16;

        sprintf(s, "(%3d, %3d)", mx, my);
        boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 隐藏坐标 */
        putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 显示坐标 */
        putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /* 描画鼠标 */
    }
}

9天 内存管理

9.1 内存容量检查(1

#define EFLAGS_AC_BIT 0x00040000
#define CR0_CACHE_DISABLE 0x60000000
unsigned int memtest(unsigned int start, unsigned int end){
	char flg486=0;
	unsigned int eflg, cr0, i;
	
	/* 确认CPU是386还是486以上的。如果是486以上,EFLAGS寄存器的第18位应该是所谓的AC标志位;
		如果CPU是386,那么就没有这个标志位,第18位一直是0。 */
	eflg = io_load_eflags();
	eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */
	io_store_eflags(eflg);
	eflg = io_load_eflags();
	
	if(eflg&EFLAGS_AC_BIT){/* 如果是386,即使设定AC=1,AC的值还会自动回到0 */
		flg486=1;
	}
	
	eflg&= ~EFLAGS_AC_BIT;/* 将AC标志位重置为0 */
	io_store_eflags(eflg);
	
	if(flg486){
		cr0=load_cr0();
		cr0|=CR0_CACHE_DISABLE;/*禁止缓存*/
		store_cr0(cr0);
	}
	// 调查从start地址到end地址的范围内,能够使用的内存的末尾地址
	i=memtest_sub(start, end);
	
	if(flg486){
		cr0=load_cr0();
		cr0&= ~CR0_CACHE_DISABLE;
		store_cr0(cr0);
	}
	return i;
}

unsigned int memtest_sub(unsigned int start, unsigned int end)的功能是从start地址到end地址的范围内,能够使用的内存的末尾地址,也就是确定在这个范围内能够使用的内存个数。确定的方法就是通过对地址内数据的两次反转,判断是否能恢复到初始值,如果可以则检查下一个,否则就返回当前地址。

9.2 内存容量检查(2

将函数memtest_sub改写成汇编语言,以加快处理速度。

;调查从start地址到end地址的范围内,能够使用的内存的末尾地址
_memtest_sub: 					; unsigned int memtest_sub(unsigned int start, unsigned int end)
		PUSH 	EDI 			; (由于还要使用EBX, ESI, EDI)
		PUSH 	ESI
		PUSH 	EBX
		MOV 	ESI,0xaa55aa55 	; pat0 = 0xaa55aa55;
		MOV 	EDI,0x55aa55aa 	; pat1 = 0x55aa55aa;
		MOV 	EAX,[ESP+12+4] 	; i = start;
mts_loop:
		MOV 	EBX,EAX
		ADD 	EBX,0xffc 		; p = i + 0xffc;
		MOV 	EDX,[EBX] 		; old = *p;
		MOV 	[EBX],ESI 		; *p = pat0;
		XOR 	DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
		CMP 	EDI,[EBX] 		; if (*p != pat1) goto fin;
		JNE 	mts_fin
		XOR 	DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
		CMP 	ESI,[EBX] 		; if (*p != pat0) goto fin;
		JNE 	mts_fin
		MOV 	[EBX],EDX 		; *p = old;
		ADD 	EAX,0x1000 		; i += 0x1000;
		CMP 	EAX,[ESP+12+8] 	; if (i <= end) goto mts_loop;
		JBE 	mts_loop
		POP 	EBX
		POP 	ESI
		POP 	EDI
		RET
mts_fin:
		MOV 	[EBX],EDX 		; *p = old;
		POP 	EBX
		POP 	ESI
		POP 	EDI
		RET

9.3 挑战内存管理

数据交换等操作对内存管理的要求较高,且使用并释放一个字节的内存比较费时费力,所以结合《深入理解Linux内核》采取页管理模式。

struct FREEINFO { /* 可用状况 */
    unsigned int addr, size;// addr页的首地址  size页大小
};
struct MEMMAN { /* 内存管理 */
    int frees;//可用的数量
    struct FREEINFO free[1000];
};
struct MEMMAN memman;
memman.frees = 1; /* 可用状况list中只有1件 */
memman.free[0].addr = 0x00400000; /* 从0x00400000号地址开始,有124MB可用 */
memman.free[0].size = 0x07c00000;/* 0x07c00000=7*16+12MB=124MB */

假设每页有4k内存,如果函数需要3226字节的内存,则从memman.free[0]中选一段连续的、未被使用的3226字节分配给函数( memman.free[0].addr+=3226; memman.free[0].size-=3226; ),函数用完内存后再释放给该页( memman.free[0].addr-=3226; memman.free[0].size+=3226; )。

首先就需要对结构体中的元素进行初始化,构造void memman_init(struct MEMMAN *man)函数。申请内存时需要考虑在哪个内存数组中申请多大的内存,确定函数的输入项为struct MEMMAN *man和size,接着就需要遍历man中所有的页,检查页中剩余内存能否满足size,如果可以满足则返回分配到的内存首地址,否则检查下一页内存。如果没有页满足要求,则返回0。

/* 在man分配内存:1.检索能放下size空间大小的内存,2.更新该块内存的信息,
   3.检查该内存是否还有剩余内存,没有则更新数组信息 */
unsigned int memman_alloc(struct MEMMAN *man, unsigned int size){
	unsigned int i, a;
	for(i=0;ifrees;i++){
		if(man->free[i].size>size){/* 找到了足够大的内存 */
			a=man->free[i].addr;
			man->free[i].addr+=size;
			man->free[i].size-=size;
			if(man->free[i].size==0){/* 如果free[i]变成了0,表明该区间没有可用内存,就减掉一条可用信息 */
				man->frees--;
				for(;ifrees;i++){/* 代入结构体,后续空间前移 */
					man->free[i] = man->free[i + 1]; 
				}
			}
			return a;
		}
	}
	return 0;/* 没有可用空间 */
}

内存释放就是确定从什么地址开始的多大内存释放到哪页内存中,由此得出函数的输入项为memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size)。思路为释放内存空间,如下有4种情况:
1.可以与前面相邻的内存空间进行合并(需要考虑是否前面存在内存空间);
2.可以与后面相邻空间进行合并(man->free数组需要移动);
3.不能与前后相邻内存空间进行合并(需要扩展man->free数组存放待释放的内存);
4.待释放的内存无法被释放。

int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
	int i, j;
	/* 为便于归纳内存,将free[]按照addr的顺序排列 */
	/* 所以,先决定应该放在哪里 */
	for (i = 0; i < man->frees; i++) {
    /* 表明addr在man->free[i].addr之前,即确定了释放空间位置 */
		if (man->free[i].addr > addr) {
			break;
		}
	}
	/* free[i - 1].addr < addr < free[i].addr */
	if (i > 0) {
		/* 前面有可用内存,即不是释放到整个内存空间头部 */
		if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
			/* 可以与前面的可用内存归纳到一起 */
			man->free[i - 1].size += size;
			if (i < man->frees) {
				/* 紧后也有可用内存 */
				if (addr + size == man->free[i].addr) {
					/* 也可以与后面的可用内存归纳到一起 */
					man->free[i - 1].size += man->free[i].size;
					/* man->free[i]删除 */
					/* free[i]变成0后归纳到前面去 */
					man->frees--;
					/* 由于下标i的内存与i-1内存进行了合并,所以后续空间前移 */
					for (; i < man->frees; i++) {
						man->free[i] = man->free[i + 1]; 
					}
				}
			}
			return 0; /* 成功完成 */
		}
	}
	/* 不能与前面的可用空间归纳到一起 */
	if (i < man->frees) {
		/* 后面还有,正好与下标为i的内存连接 */
		if (addr + size == man->free[i].addr) {
			/* 可以与后面的内容归纳到一起 */
			man->free[i].addr = addr;
			man->free[i].size += size;
			return 0; /* 成功完成 */
		}
	}
	/* 既不能与前面归纳到一起,也不能与后面归纳到一起 */
	if (man->frees < MEMMAN_FREES) {
		/* free[i]之后的,向后移动,腾出一个可用空间 */
		for (j = man->frees; j > i; j--) {
			man->free[j] = man->free[j - 1];
		}
		man->frees++;
		if (man->maxfrees < man->frees) {
			man->maxfrees = man->frees; /* 更新最大值 */
		}
		man->free[i].addr = addr;
		man->free[i].size = size;
		return 0; /* 成功完成 */
	}
	/* 不能往后移动 */
	man->losts++;
	man->lostsize += size;
	return -1; /* 失败 */
}

你可能感兴趣的:(其他)