拼一个自己的操作系统(SnailOS 0.03的实现)

拼一个自己的操作系统 SnailOS 0.03的实现

拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-优快云下载

操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-优快云下载

SnailOS0.00-SnailOS0.00-其它文档类资源-优快云下载

https://pan.baidu.com/s/19tBHKyzOSKACX-mGxlvIeA?pwd=i889

 

读取fat32文件系统的文件。

在真正的讲fat32文件系统读取文件的内容前,让我们先把几个bmp文件复制到我们的虚拟硬盘上。并且创建了hello.txt的文本文件。

5aed52325ed864a329dfe8a4c1123692.png

 

看到了吧,都是大美女吧!继续写下去是不是好有兴致。

e4bca3b0d521fb8ec6f9faaaab7a77b5.png

 

 

时至今日,fat32文件系统已经被ntfs、ext?等其他文件系统甩了多少条街都说不好了。但有一点确定的是最近还有很多U盘用的是该文件系统。其实,仔细想来也没有什么特殊的原因,主要就是该文件系统还是非常好用的。但我们选择fat32文件系统的唯一目的就是它相对来说还是比较简单的。也就是能用比较少的代码就完成我们的目标。其实,我们的目标更是简单和直接,就是读取文件内容,然后做一些简单的操作,比如我们可以输出文本文件的内容,我们可以在屏幕上显示图片等等。

现在的问题就转化为,如何用上一章的硬盘驱动程序,把某个lba28扇区的内容读取到内存中供我们使用这么简单了。网上关于fat32文件系统的文章可以用多如牛毛来形容,其详解每个都有独到之处,所以笔者根本就不想拷贝那些信息在这里班门弄斧。我们的思路是通过二进制文件查看器,从目录项检索开始,逐步高清,该如何读取文件内容。首先让我们用UE打开temp.vhd的硬盘镜像文件。然后我们来一波骚操作,逐步摸清fat32的底数。下面的图中我们看到了一片16进制的数据(中间),左边是该数据对应的16进制地址,右边是数据对应的ascii码值。这是硬盘的开头,粗估一下应该是主引导扇区。因此我们要找到一个存在于512字节的标记也就是0xaa55,这样就比较确定了。那么我们往下看看哪个纯蓝色的部分正好是我们要找的地方。可是我们的目的可不是看这个让人迷糊的引导扇区,我是要找到我们的文件内容所在的扇区。因此按照fat32文件系统的安排,我们知道文件的目录项中含有该内容,同时目录项中也还有文件名称。干脆我们在此处搜索文件名看看能不能一眼就找到目录项,从而访问到文件内容。

91cbd7c546ac287a09f0cb03148e2bbe.png

 

 

说干就干,在下图大家看到了吧,一眼熟呀,可不是一个文件名我们熟悉,上一节我们拷贝的文件都在这里排队呢?这不是天上直接就掉下个林妹妹吗。同时看到下图我们用纯蓝色圈出的部分,在目录项中它是文件开始的簇号低16位,也就是0x30df,而往前4个字节处的0x0000处是簇号的高16位。而我们要的是文件的起始扇区,计算方法就是网上查到的

起始扇区= 根目录起始扇区 + (起始簇号 - 2)* 8

而我们现在缺少的是根目录起始扇区数。仔细一想这个并不难啊,我们稍微往前看看不就是根目录的起始扇区了吗。也就是说,我们的根目录根部没有几个文件吗。

 

4e0d8dffa7445e401a28e70254652a1e.png

 

 

看下面的图,不出所料的话,这就是根目录开始的位置。而这里是字节数,我们要转化为扇区数才能算计算出来了。用0x303000除以512就应该是扇区数吧。让我们来掰着手指头算一下好了。这个结果是0x1818,现在套入那个公式,0x1818 + (0x30df - 2) * 8 = 0x1f900;

 

1d9412d31b63d186c3d7d312041ba13f.png

 

 

接下来就是要查找文件了。看下图一打眼,是不是文件内容。我搜索的是Snail OS!...因此不是在文件开头,文件开头是一堆空格了。看看左边的地址,0x33e0000,这个是字节的,所以要除以512得出我们文件的起始扇区数。正是前面推算出的0x1f900这个数。至此查找一个简单txt文件的就大功告成了。

 

26041f9dce5335bf3043ef1c9b55da87.png

 

 

不过那个根目录起始扇区数可是我们手工蒙出来的,也就是说只可意授不可言传。这样可不行呀。

我们这里也就不兜圈子了,按照fat32的规定,根目录起始扇区在两个fat表之后,而两个fat表的起始扇区是通过bpb起始扇区加上bpb后的保留扇区数的到的。而这些信息除了bpb的起始扇区在mbr中获得外,其他的都在bpb中。因此我们现在要返回到mbr中得到bpb起始扇区数。然后通过bpb得到根目录起始扇区。bpb起始扇区在mbr分区表结构中的lba_start中,分区表在mbr第446字节处,也就是0x1be处,也就是lba_start在mbr的0x1c7处。

观察mbr的下图,纯蓝色的部分就是bpb起始扇区数,为0x800,乘以512(0x200)转化成字节数就是0x100000(1M)处。

 

858e8c5c546ba78f3b9d0225a7980355.png

 

 

看到下图大家就明白了吧,这么简单就找到了bpb,而我们所需要的所有信息都在这里了。对了这是通过转到地址0x100000处得到的。接下来大家就没什么疑问了吧,通过访问bpb中的关键数据,我们就能一步一步的最终访问文件内容了。

 

7b8176c3384d883ea09358f244fc3004.png

 

 

下面我们就给出完整的代码,相信大家学了这么久,对这些代码不会有什么问题了。

 

 

【fs.h】

// fs.h 作者:至强 创建时间:2022年12月

#ifndef __FS_H

#define __FS_H

 

struct partition_table_item {

unsigned char flags, head_start, sec_start, chs_start;

unsigned char fs_type, head_end, sec_end, chs_end;

unsigned int lba_start, sec_cnt;

}__attribute__((packed));

 

struct boot_sector {

unsigned char code_area[446];

struct partition_table_item parts[4];

unsigned short magic;

}__attribute__((packed));

 

struct bios_parameter_block {

unsigned char bpb_head[11];

unsigned short bytes_per_sector;

unsigned char sectors_per_cluster;

unsigned short reserved_sector;

unsigned char number_of_fat;

unsigned short root_entries;

unsigned short small_sector;

unsigned char media_descriptor;

unsigned short sectors_per_fat_16;

unsigned short sectors_per_track;

unsigned short number_of_head;

unsigned int hidden_sector;

unsigned int large_sector;

unsigned int sectors_per_fat_32;

unsigned short extended_flag;

unsigned short file_system_version;

unsigned int root_cluster_number;

unsigned short file_system_information_sector_number;

unsigned short backup_boot_sector;

unsigned char reserved[12];

unsigned char physical_drive_number;

unsigned char reserved_byte;

unsigned char extended_boot_signature;

unsigned int volume_serial_number;

unsigned char volume_label[11];

unsigned char system_id[8];

}__attribute__((packed));

 

struct dir_item {

char name[8], ext[3], attrib, empty, ticks;

unsigned short create_time, create_date, access_date;

unsigned short clust_no_h, modify_time, modify_date;

unsigned short clust_no_l;

unsigned int filesize;

};

 

struct fat32_krn_p {

int* fat1_p;

struct dir_item* root_dir_p;

unsigned int root_dir_start_sector;

};

 

void read_mbr_dbr(struct fat32_krn_p* fat32_p);

 

char* format_filename(const char* filename);

int find_file(struct fat32_krn_p* fat32_p, const char* filename);

void* file_read(const char* filename);

void hd_read_clust(struct fat32_krn_p* fat32_p, unsigned int start,

int mem_start);

 

#endif

 

【fs.c】

// fs.c 作者:至强 创建时间:2022年12月

#include "fs.h"

#include "memory.h"

#include "hd.h"

#include "string.h"

 

struct fat32_krn_p fat32_krn;

 

extern struct disk hds[2];

 

void read_mbr_dbr(struct fat32_krn_p* fat32_p) {

/*

为主引导扇区mbr分配内存,由于mbr仅有512字节,这里剩余的空间

实际是可以留作他用的。

*/

struct boot_sector* mbr = get_kernel_pages(1);

/*

用硬盘驱动把主引导扇区读入分配的内存中。

*/

hd_read_sub(&hds[0], 0, (void*)mbr, 1);

unsigned int dbr_lba_start;

/*

文件系统类型是fat32则bioa参数块的lba28模式其实扇区为主引导

扇区分区信息的lba_start成员。

*/

if(mbr->parts[0].fs_type == 0x0b) {

dbr_lba_start = mbr->parts[0].lba_start;

} else {

return;

}

/*

由于前面的mbr只使用的512字节,所以这里使用了接下来的512字节

作为bpb的内存地址,同样根据bpb在硬盘上的起始扇区,读取512字节

到内存中。

*/

struct bios_parameter_block* bpb = (struct bios_parameter_block* )

((unsigned int)mbr + 512);

hd_read_sub(&hds[0], dbr_lba_start, (void*)bpb, 1);

/*

得到fat32文件系统的几个重要信息,这个是每扇区的字节数,

这个数字估计是512。

*/

unsigned short bytes_per_sector = bpb->bytes_per_sector;

// printf_("^%x^", bytes_per_sector);

/*

这个是每簇扇区数,估计数值是8。也就是4096字节。

*/

unsigned char sectors_per_cluster = bpb->sectors_per_cluster;

// printf_("^%x^", sectors_per_cluster);

/*

bpb的保留扇区数,用于求解下面的fat表1的起始扇区。

*/

unsigned short reserved_sector = bpb->reserved_sector;

// printf_("^%x^", reserved_sector);

/*

这个是bpb的fat表的个数。此处的值应该是2。

*/

unsigned char number_of_fat = bpb->number_of_fat;

// printf_("^%x^", number_of_fat);

/*

每个fat表所占的扇区数。

*/

unsigned int sectors_per_fat_32 = bpb->sectors_per_fat_32;

// printf_("@%x@", sectors_per_fat_32);

/*

顺利地得到了fat表的起始扇区数。

*/

unsigned int fat_start_sector = dbr_lba_start +

reserved_sector;

// printf_("^%x^", fat_start_sector);

/*

我们的目标就在这里了,它将得到根目录的起始扇区数,也就是

fat表1的起始扇区数加上两个fat表的长度。

*/

unsigned int root_dir_start_sector = fat_start_sector +

sectors_per_fat_32 * number_of_fat;

// printf_("^%x^", root_dir_start_sector);

/*

根据fat表1的起始扇区数已经表的长度,将fat表1读入到内存中待用,

这个表将占用较大的内存。

*/

unsigned int* fat1_mem_start =

get_kernel_pages(up_pgs(sectors_per_fat_32 * bytes_per_sector));

hd_read_sub(&hds[0], fat_start_sector, (void*)fat1_mem_start,

sectors_per_fat_32);

// printf_("^%x^", fat1_mem_start);

/*

对于根目录我们这里仅仅读取了一页的内容,估计这样的情况对于我们

现在的系统也是足够用了。

*/

unsigned int* root_mem_start = get_kernel_pages(1);

hd_read_sub(&hds[0], root_dir_start_sector, (void*)root_mem_start, 8);

/*

将信息保存在全局结构中待用。

*/

fat32_p->fat1_p = (int*) fat1_mem_start;

fat32_p->root_dir_p = (struct dir_item*) root_mem_start;

fat32_p->root_dir_start_sector = root_dir_start_sector;

}

 

/*

这是简单的格式化fat32目录项中段格式文件名的函数,也就是尽量

正确的判断我们给出的文件名的正确性,然后存储到静态字符数组中

待用。这个函数肯定存在一些特殊的情况不能处理,因此,大家在根目录

中存储文件时,应该按照8+3的标准格式存储,否则就会有风险^_^。

*/

char* format_filename(const char* filename) {

int i = 0, j, k;

static char result[12];

char space = 0x20;

result[11] = 0;

if(strlen_(filename) > 11) {

return NULL;

}

for(j = 0; j < 11 && filename[j]; j++){

if(filename[j] == '.') {

i = 1;

}

}

if(i == 0) {

if(strlen_(filename) > 8) {

return NULL;

}

for(j = 0; j < 8 && filename[j]; j++) {

result[j] = filename[j] & 0xdf;

}

for(; j < 8 + 3; j++) {

result[j] = space;

}

return result;

} else {

if(filename[0] == '.') {

result[0] = '.';

if(filename[1] == '.') {

result[1] = '.';

for(j = 2; j < 8 + 3; j++) {

result[j] = space;

}

return result;

}

for(j = 1; j < 8 + 3; j++) {

result[j] = space;

}

return result;

}

}

for(j = 0; j < 8 && filename[j]; j++) {

result[j] = filename[j] & 0xdf;

if(filename[j] == '.') {

k = j + 1;

break;

}

}

for(; j < 8 + 3; j++) {

result[j] = space;

}

if(k < 11) {

for(j = 0; j < 3; j++) {

(result + 8)[j] = filename[k++] & 0xdf;

}

}

return result;

}

 

/*

整合上面的文件,从而在目录相中找到正确的文件。

*/

int find_file(struct fat32_krn_p* fat32_p, const char* filename) {

struct dir_item* root_dir_start = fat32_p->root_dir_p;

unsigned int file_start_clust;

unsigned int filesize;

unsigned int root_dir_start_sector = fat32_p->root_dir_start_sector;

 

char file[12] = {0};

strcpy_(file, format_filename(filename));

int k = 0;

int j;

/*

这个就是遍历目录项从而找到我们想要的文件。

*/

while((root_dir_start[k].name[0] != 0)) {

if(root_dir_start[k].name[0] == 0xe5) {

k++;

continue;

}

char f1 = 1, f2 = 1;

 

for(j = 0; j < 8; j++) {

if(root_dir_start[k].name[j] != file[j]) {

f1 = 0;

}

}

 

for(j = 0; j < 3; j++) {

if(root_dir_start[k].ext[j] != (file + 8)[j]) {

f2 = 0;

}

}

 

if(f1 && f2) {

file_start_clust = ((unsigned int)root_dir_start[k].clust_no_h <<

16)+ (unsigned int)root_dir_start[k].clust_no_l;

filesize = root_dir_start[k].filesize;

 

// printf_("*** %s %d %d %x %x ***", file, k + 1,

// filesize, file_start_clust, root_dir_start_sector);

return k;

} else {

// printf_("\n not found %s 第%d个文件", file, k + 1);

}

k++;

}

return -1;

}

 

/*

按簇来读取文件内容。每个簇有8个扇区,因此循环就是8次。

*/

void hd_read_clust(struct fat32_krn_p* fat32_p, unsigned int start, int mem_start) {

int i;

unsigned int data_start = fat32_p->root_dir_start_sector;

for(i = 0; i < 8; i++) {

/*

起始扇区的计算公式一个难以理解的东西,它是用根目录的起始扇区数

加上(起始簇数减去二)* 8来得到的,这个玩意简直太烧脑了,反正笔者也

不想弄明白了,只要记住公式就好了。

*/

hd_read_sub(&hds[0], data_start + (start - 2) * 8 + i,

(void*)mem_start + i * 512, 1);

}

}

 

/*

进一步整合就可以按文件名称读取相应的文件了。

*/

void* file_read(const char* filename) {

struct fat32_krn_p* fat32_p = &fat32_krn;

int index = find_file(fat32_p, filename);

if(index == -1) {

return NULL;

}

struct dir_item* root_dir_start = fat32_p->root_dir_p;

unsigned int* fat1_p = fat32_p->fat1_p;

unsigned int start_clust;

unsigned int filesize;

filesize = root_dir_start[index].filesize;

unsigned int root_dir_start_sector = fat32_p->root_dir_start_sector;

 

char* filep = get_kernel_pages(up_pgs(filesize));

char* fp = filep;

/*

起始簇数在根目录项中分两部分存储,因此要拼接出来。

*/

start_clust = ((unsigned int)root_dir_start[index].clust_no_h <<

16)+ (unsigned int)root_dir_start[index].clust_no_l;

/*

如果文件仅占用一个簇就够用了。我们就读取该簇就可以了。

*/

while(start_clust != 0x0fffffff) {

if(filesize <= 4096) {

hd_read_clust(fat32_p, start_clust, (int)filep);

return fp;

}

/*

如果文件大于一个簇则也要先把第一个簇的内容读入内存。

*/

hd_read_clust(fat32_p, start_clust, (int)filep);

filep += 4096;

filesize -= 4096;

/*

下一个簇的算法又是一个烧脑的玩意,居然在fat表中用上一个簇

下标得到的,笔者对于为什么会是这样也是不想弄懂了,只要记住公式

就好。

*/

start_clust = fat1_p[start_clust];

// printf_(" %x ", start_clust);

// printf_("|");

}

/*

到此便可返回得到文件内容的内存地址。

*/

return fp;

}

 

 

笔者在实验的过程中,发现仅仅使用二元信号量,对于一个资源多个线程共享的情况会很别扭,所以增加了一个多值信号量的操作。下面是它的代码。

 

【 semaphore.h】

// semaphore.h 作者:至强 创建时间:2022年12月

#ifndef _SEMAPHORE_H

#define _SEMAPHORE_H

 

/*

这是多值信号量的实现,通过多值信号量可以控制同一资源的多次

访问。也就是一种其他的同步关系。

*/

struct event {

void* msgptr;

int count;

unsigned int type;

struct double_linked_list waiters;

};

 

void event_init(struct event* event_, void* msg, int cnt, int type);

void event_array_init(struct event* event_array_);

struct event* get_event(struct event* event_array_);

void sema_pend(struct event* pevent);

void sema_post(struct event* pevent);

 

#endif

 

【 semaphore.c】

// semaphore.c 作者:至强 创建时间:2022年12月

#include "global.h"

#include "intr.h"

#include "thread.h"

#include "semaphore.h"

#include "memory.h"

 

struct event event_array[256];

struct event* pevent_;

 

/*

我们这里是按照事件来定义信号量结构的,所以这里是初始化事件。

*/

void event_init(struct event* event_, void* msg, int cnt, int type) {

unsigned int old_status = intr_disable();

event_->msgptr = msg;

event_->count = cnt;

event_->type = type;

double_linked_list_init(&event_->waiters);

set_intr_status(old_status);

}

 

/*

初始化最多256个信号量。

*/

void event_array_init(struct event* event_array_) {

int i;

for(i = 0; i < 256; i++) {

event_init(&event_array_[i], NULL, 0, -1);

}

}

 

/*

它实际上是获得一个信号量。

*/

struct event* get_event(struct event* event_array_) {

int i;

unsigned int old_status = intr_disable();

for(i = 0; i < 256; i++) {

if(event_array[i].type == -1) {

break;

}

}

set_intr_status(old_status);

return &event_array_[i];

}

 

/*

用于等待信号量,也就是说当信号量的值为0时,说明期待的事件

并未发生,这是线程需要阻塞自己,并进入等待队列。直到事件发生

后,完成事件的线程通过累加信号量唤醒线程。

*/

void sema_pend(struct event* pevent) {

struct task* cur;

unsigned int old_status = intr_disable();

if(pevent->count == 0) {

cur = running_thread();

double_linked_list_append(&pevent->waiters, &cur->general_tag);

cur->status = WAITING;

schedule();

set_intr_status(old_status);

} else {

pevent->count--;

set_intr_status(old_status);

}

}

 

/*

当一个事件完成后,完成该事件的线程会发送1个或者若干个信号,

(仅仅是对信号量的值进行累加)来唤醒在信号量的等待队列中

阻塞的线程。

*/

void sema_post(struct event* pevent) {

struct list_node* waiter;

unsigned int old_status = intr_disable();

if(!double_linked_list_is_empty(&pevent->waiters)) {

waiter = double_linked_list_pop(&pevent->waiters);

struct task* next = node2entry(struct task, general_tag, waiter);

thread_ready_list = &ready_list[next->level];

double_linked_list_append(thread_ready_list, waiter);

set_intr_status(old_status);

} else {

pevent->count++;

set_intr_status(old_status);

}

}

 

 

【kernel.c】

 

// kernel.c 创建者:至强 创建时间:2022年8月

#include "global.h"

#include "x.h"

#include "string.h"

#include "gdt_idt_init.h"

#include "memory.h"

#include "intr.h"

#include "debug.h"

#include "thread.h"

#include "ring_queue.h"

#include "screen.h"

#include "queue.h"

#include "mouse.h"

#include "timer.h"

#include "sheet.h"

#include "tss.h"

#include "process.h"

#include "syscall.h"

#include "hd.h"

#include "fs.h"

#include "semaphore.h"

 

 

unsigned long long limit;

void create_clock(int x, int y);

 

struct queue ins_queue;

 

void k_thread_a(void* arg);

void k_thread_b(void* arg);

void k_thread_c(void* arg);

void idle(void* arg);

 

void ua(void);

void ub(void);

void uc(void);

 

void instructions_switch(struct buf* e);

 

unsigned int mouse_bin[16 * 16];

unsigned int mouse_before[16 * 16];

 

extern void test();

 

extern unsigned int zzz;

 

struct fat32_krn_p fat32_krn;

struct semaphore fs_sema;

 

extern struct event event_array[256];

extern struct event* pevent_;

 

unsigned int* desktop;

 

void kernel_main(unsigned int magic, unsigned int addr) {

 

 

 

multiboot2_magic = magic;

multiboot2_addr = addr;

 

pos = 0;

 

int i, j;

 

// unsigned int* video = get_video_addr(magic, addr);

unsigned int* video = (unsigned int*)0xe0000000;

screen_init();

info_area_cls();

/*

fill_rectangle(video, 1024, 50, 50, 40, 40, 0x00ff0000);

fill_circle(video, 1024, 200, 200, 80, 0x00ffff00);

draw_line(video, 1024, 0, 0, 1023, 767, 0x00ffffff);

draw_circle(video, 1024, 200, 200, 100, 0x0000ffff);

draw_box(video, 1024, 40, 40, 20, 100, 0x0000ff00);

struct point a, b, c;

a.x = 300;

a.y = 300;

b.x = 200;

b.y = 400;

c.x = 500;

c.y = 450;

draw_triangle(video, 1024, &a, &b, &c, 0x000000ff);

a.x = 600;

a.y = 300;

b.x = 500;

b.y = 400;

c.x = 800;

c.y = 450;

fill_triangle(video, 1024, &a, &b, &c, 0x00bcbcbc);

 

char pic[16][16] =

{

{'*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','#','#','#','#','#','#','#','#','#','#','#','#','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','#','#','#','#','#','#','#','#','#','#','#','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*'},

};

 

unsigned int pic_data[16][16];

 

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

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

if(pic[j][i] == '#') {

pic_data[j][i] = 0x000000ff;

}

if(pic[j][i] == '*') {

pic_data[j][i] = 0x0000ff00;

}

}

}

 

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

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

draw_point(video + 1024 * (128 + 16 + 16),

1024, i + 512, j, pic_data[j][i]);

}

}

 

char font_A[16] = {

0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,

0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00

};

 

extern char font_A_;

 

make_ascii(video + 1024 * 128, 1024, 0, 0, 0x000000ff, (char*)&font_A_);

make_ascii(video + 1024 * (128 + 16), 1024, 0, 0, 0x0000ff00, font_A);

 

printf_("%s %d %x %b", "I love you SnailOS...!", 0x8, 8, 8);

 

*/

 

 

init();

sema_init(&fs_sema, 0);

event_array_init(event_array);

pevent_ = get_event(event_array);

event_init(pevent_, NULL, 0, 0);

/*

extern struct tss tss, tss1;

 

第1任务(主函数)的任务状态段只需要初始化页目录表,这

主要可能是因为处理器在保存进程的上文时不更新cr3。如果

不返回到主任务,甚至不用对它的任务状态段进行任何处理,

当然也可以没有。

 

// tss.eip = 0;

// tss.ss = tss.ds = tss.es = 0;

// tss.esp = 0;

tss.cr3 = 0x8000;

// tss.eflags = 0;

// tss.ss0 = 0;

// tss.esp0 = 0;

// tss.cs = 0;

// tss.ldt = 0;

*/

/*

简单的初始化第2个任务的任务状态段。

*/

/*

tss1.eip = (unsigned int)_0x70_handler;

tss1.ss = tss1.ds = tss1.es = 5 * 8 + 3;

tss1.esp = 0xa000;

tss1.cr3 = 0x8000;

tss1.eflags = 0x202;

tss1.ss0 = 2 * 8;

tss1.esp0 = 0x8000;

tss1.cs = 4 * 8 + 3;

tss1.ldt = 0;

 

 

// asm volatile ("jmp $6 * 8, $10000");

// asm volatile ("call $6 * 8, $0xabcd");

// asm volatile ("jmp $7 * 8, $10000");

// asm volatile ("call $7 * 8, $0xabcd");

// asm volatile ("cli;int $0x70");

 

asm volatile("pushl $5 * 8 + 3;\

pushl $5 * 8 + 3;\

pushfl;\

movl $0x202, (%esp);\

popfl;\

movl $6 * 8 , %eax;\

ltr %ax;\

popl %es;\

popl %ds;\

pushl $5 * 8 + 3;\

pushl $0xa000;\

pushfl;\

pushl $4 * 8 + 3;\

pushl $__0x70_handler;\

iret");

 

printf_("\n@@@task sucessful return!!!!!!");

 

while(1);

*/

// printf_("%x %x\n", pde_ptr(0x2000000), pte_ptr(0x2000000));

 

// 虚拟地址的页目录表项写入物理地址0x2000000 + 0x07,也就

// 是页表的首地址在0x2000000处。

// *((unsigned int*)pde_ptr(0x2000000)) = 0x2000000 + 0x07;

// 页表项的物理地址也写入0x2000000 + 0x07,也就是最终该

// 虚拟地址映射到物理地址一样的地方。

// *((unsigned int*)pte_ptr(0x2000000)) = 0x2000000 + 0x07;

// *((unsigned int*)pte_ptr(0x2000000)) = 0x2001000 + 0x07;

 

// 由于该地址已经映射了实际的物理内存,所以访问时不会出现

// 任何问题。需要注意的是,由于这里是页表第一项(从0开始)

// 是映射关系,所以如果把第一项也清零了,也会出现宕机的情况。

// 所以我们从第二项开始清零。

// for(i = 1; i < 1024; i++) {

// ((unsigned int*)0x2000000)[i] = 0;

// }

 

/*

当第一项清零的情况下,如果不加入此句是不会宕机的。是不是

非常的奇怪。这是因为只有执行了下面一句,处理器才按照内存

中实际的页表建立映射关系。不要忘了处理器还有自己的缓存。

下面的汇编语句是不是觉得有些怪怪的,是啊,他是AT&T的格式

格式的非扩展的嵌入式汇编语句,主要是函数调用得到形式要

切换到汇编文件中。注意了大伙, AT&T格式的汇编,源操作数和

目标操作数的顺序是相反的。

*/

// asm("mov %cr3, %eax;mov %eax, %cr3");

// printf_("\n%x %x\n", ((unsigned int*)0x2000000)[0],

// ((unsigned int*)0x2000000)[1]);

 

// i /= 0;

// asm("ud2");

// asm volatile ("movl $0xffffffff, %ebx;\

movl $0x0, %edx;\

movl $0xffffffff, %eax;\

mul %ebx;");

// asm volatile ("into");

// asm("int3");

// limit = 0xaaaa00000000;

// asm volatile("movl $0xaaaa, %eax;bound %eax, (_limit)");

// printf_("\n @@@@@@ NEW START! @@@@@@\n");

// asm volatile("movl $0xaaab, %eax;bound %eax, (_limit)");

 

 

/*

向主从芯片的数据端口写入数据,0xfb的二进制形式是11111011b,

也就是通过将第2位(从0计算)复位(清零),使向量0x22能够接收

中断,从而开启从芯片的所有中断。0xfe的二进制形式是11111110b,

正是从芯片的0x28向量对应的中断,该中断便是实时时钟中断。

*/

// out(0x21, 0xfb);

// out(0xa1, 0xfe);

 

struct bmp_buf_info bbi;

 

save_bmp((char*)&zzz, &bbi);

 

unsigned int* v = (unsigned int*)(0xe0000000 + 1024 * 128 * 4);

put_bmp_buf(v, 1024, 20, 20, &bbi);

/*

将表盘在这里描画。从而成为桌面图层的一部分。

*/

 

 

dial(930, 90);

 

/*

下面是鼠标位置坐标和鼠标控制结构。并对位置坐标进行了初始化。

*/

int mx = 200, my = 300;

struct mouse_dec mdec;

/*

把鼠标的缓冲区用我们预制的图形写好,以备图层管理函数使用。

*/

make_mouse_pointer(mouse_bin, 0x003098df);

 

// make_mouse_before(mouse_before, 0x003098df);

/*

在原始的没有图层的桌面上写了一串文字。

*/

put_gb2312_buf(video, 1024, 480, 400 - 16, 0x00d9d919,

"蜗牛");

put_gb2312_buf(video, 1024, 400, 400, 0x00d9d919,

"该不该搁下重重的壳");

put_gb2312_buf(video, 1024, 400, 416, 0x00d9d919,

"寻找到底哪里有蓝天");

put_gb2312_buf(video, 1024, 400, 432, 0x00d9d919,

"随着轻轻的风轻轻的飘");

put_gb2312_buf(video, 1024, 400, 448, 0x00d9d919,

"历经的伤都不感觉疼");

put_gb2312_buf(video, 1024, 400, 464, 0x00d9d919,

"我要一步一步往上爬");

put_gb2312_buf(video, 1024, 400, 480, 0x00d9d919,

"等待阳光静静看着它的脸");

put_gb2312_buf(video, 1024, 400, 496, 0x00d9d919,

"小小的天有大大的梦想");

put_gb2312_buf(video, 1024, 400, 512, 0x00d9d919,

"重重的壳裹着轻轻的仰望");

put_gb2312_buf(video, 1024, 400, 528, 0x00d9d919,

"我要一步一步往上爬");

put_gb2312_buf(video, 1024, 400, 544, 0x00d9d919,

"在最高点乘着叶片往前飞");

put_gb2312_buf(video, 1024, 400, 560, 0x00d9d919,

"小小的天留过的泪和汗");

put_gb2312_buf(video, 1024, 400, 576, 0x00d9d919,

"总有一天我有属于我的天");

/*

分别是桌面、鼠标、窗口0、窗口1的图层指针。

*/

struct SHEET* sht_back, * sht_mouse, *sht_win, *sht_win1;

/*

桌面图层缓冲区分配内存。

*/

void* buf_back = get_kernel_pages(up_pgs(1024 * (768 - 128) * 4));

desktop = buf_back;

/*

复制桌面当前内容当图层缓冲区,成为图层的一部分后,这些

东西上面图层将不能擦除。

*/

memcpy_((void*)buf_back, (void*)(video + 1024 * 128), 1024 *

(768 - 128) * 4);

/*

不通过拷贝直接往桌面图层中画一个黄色的实心圆形。

*/

// fill_circle(buf_back, 1024, 800, 300, 80, 0x00ffff00);

/*

分别是窗口0、窗口1的图层缓冲区内存分配、即颜色填充,同时画蛇添足

地往窗口中画了些图形、写了些文字。

*/

int* buf_win = get_kernel_pages(up_pgs(500 * 300 * sizeof(int)));

 

int* buf_win1 = get_kernel_pages(up_pgs(300 * 250 * sizeof(int)));

for(i = 0; i < 500 * 300; i++) {

buf_win[i] = 0x00cccccc;

}

for(i = 0; i < 300 * 250; i++) {

buf_win1[i] = 0x00cccccc;

}

fill_rectangle(buf_win1, 300, 180, 10, 80, 80, 0x00ff0000);

struct point a, b, c;

a.x = 80 + 200;

a.y = 5;

b.x = 20 + 200;

b.y = 200;

c.x = 150 + 200;

c.y = 210;

fill_triangle(buf_win, 500, &a, &b, &c, 0x00ffffff);

 

put_str_buf(buf_win, 500, 80, 80, 0x00ff0000,

"Hello, SnailOS. Good luck!...");

/*

下面是初始化、分配图层结构、设置图层基本信息、移动图层到

适当位置以及重置图层高度。

*/

 

shtctl = shtctl_init(video + 1024 * 128, 1024, 768 - 128);

sht_back = sheet_alloc(shtctl);

sht_mouse = sheet_alloc(shtctl);

sht_win = sheet_alloc(shtctl);

sht_win1 = sheet_alloc(shtctl);

sheet_setbuf(sht_back, buf_back, 1024, 768 - 128, -1);

sheet_setbuf(sht_mouse, mouse_bin, 16, 16, 0x003098df);

sheet_setbuf(sht_win, buf_win, 500, 300, -1);

sheet_setbuf(sht_win1, buf_win1, 300, 250, -1);

 

sheet_slide(sht_back, 0, 0);

sheet_slide(sht_win, 80, 200);

sheet_slide(sht_mouse, 300, 300);

sheet_slide(sht_win1, 2, 2);

sheet_updown(sht_back, 0);

sheet_updown(sht_mouse, 3);

sheet_updown(sht_win, 1);

sheet_updown(sht_win1, 2);

/*

这些变量将用于鼠标对图层的操作。

*/

int x, y, mmx = -1, mmy = -1;

struct SHEET* sht = NULL;

 

/*

既定定时器的设置。

*/

 

 

 

 

timer_set(t_m, 100, &main_thread->r, 0);

timer_set(t_m, 100, &main_thread->r, 1);

 

enable_extern_intr(0x28);

enable_extern_intr(0x20);

enable_extern_intr(0x21);

enable_extern_intr(0x2c);

enable_extern_intr(0x2e);

 

// out(0x21, 0xf8);

// out(0xa1, 0xae);

intr_enable();

 

 

thread_start("k_thread_a", 2, k_thread_a, " argA ", 1);

thread_start("k_thread_b", 2, k_thread_b, " argB ", 1);

thread_start("k_thread_c", 3, k_thread_c, " argC ", 1);

/*

创建空闲线程,把空闲线程放在运行级别的最低级上,则当除了主线程

之外没有其他线程或者进程运行时,空闲线程粉墨登场,从而避免了由于

主线程睡眠而造成的无进程运行的调试异常。

*/

thread_start("Idle", 1, idle, "idle ", 7);

 

/*

创建3个用户级进程,它们都运行在1的运行级上,且优先级为2(优先级

的设置归init_thread函数管)。

*/

 

process_execute(ua, "UA", 1);

process_execute(ub, "UB", 1);

process_execute(uc, "Uc", 1);

 

 

 

unsigned int keymap[0x80] = {

0,0,'1','2','3','4','5','6','7','8','9','0','-','=',0,0,

'q','w','e','r','t','y','u','i','o','p','[',']',0,0,'a','s',

'd','f','g','h','j','k','l',';','\'','`',0,'\\','z','x','c','v',

'b','n','m',',','.','/',0,'*',0,' ',0,0,0,0,0,0,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

};

 

while(1) {

asm("cli");

 

/*

遍历各个线程的循环缓冲区,统一向其中发送整数0。从而使

每个线程的缓冲区都有数据,让线程的循环中显示信息的条件

成立。

*/

struct list_node* n;

struct task* t;

/*

这里我们也要改变一下,因为已经不是遍历一个队列,所以要

做个大循环来对每个就绪队列遍历。

*/

for(i = 0; i < 8; i++) {

thread_ready_list = &ready_list[i];

if(!double_linked_list_is_empty(thread_ready_list)) {

n = thread_ready_list->head.next;

while(n != &thread_ready_list->tail) {

struct task* t =

node2entry(struct task, general_tag, n);

ring_queue_in(&t->r, 0);

n = n->next;

}

}

}

sheet_refresh(sht_back, 700, 0, 1020, 300);

/*

如果ins_queue指令缓冲区为空,则不做任何操作。否则赋值指令

及其参数,并执行该指令。注意指令是用整数表示的。

*/

/*

当且仅当两个缓冲区都为空时才直接恢复中断。

*/

if(is_empty(&ins_queue) && ring_queue_is_empty(&running_thread()->r)) {

thread_block(BLOCKED);

asm("sti;hlt");

} else {

struct buf e;

e = ins_queue.buffer[queue_out(&ins_queue)];

/*

这里我们要消费掉鼠标和键盘中断发向缓冲区的数据。

*/

int d = ring_queue_out(&main_thread->r);

asm("sti");

instructions_switch(&e);

/*

打印缓冲区元素的数目,因为一直往缓冲区中填入数据,没有人读出,所以

缓冲区空间逐步减小,知道填满。

*/

/*

if(ring_queue_is_full(&running_thread()->r)) {

printf_("...%d...", running_thread()->r.free);

} else {

printf_("___%d___", running_thread()->r.free);

}

*/

/*

用于测试定时器的部分,当既定的时刻(滴答数)到来时,经由时钟

中断处理程序删除这个定时器,并向主线程发送数据0,这里接收到数据后,

就重新添加一个定时器,拟发送的数据为1,当次定时器到期时,时钟中断

处理程序也删除它,同时发送数据0。因此,形成定时循环显示文字的效果。

*/

if(d == 0) {

put_gb2312_buf(video, 1024, 200, 300, 0x00ff0000,

"我爱你蜗牛操作系统!");

timer_set(t_m, 100, &main_thread->r, 1);

}

if(d == 1) {

put_gb2312_buf(video, 1024, 200, 300, 0x000000ff,

"我爱你蜗牛操作系统!");

timer_set(t_m, 100, &main_thread->r, 0);

}

 

/*

这里我们键盘中断处理程序发送过来的数据,由于在原始数据的基础

上加上了0x100,所以这里要恢复原貌。

*/

if((d >= 0x100) && (d < 0x100 + 0x100)) {

if((d - 0x100) & 0x80) {

 

} else {

/*

!!!请特别的注意这里,它将printf_函数改为了内核专用的kprintf_

函数。这只要是因为主线程暂时不能使用互斥机制,因该函数的唤醒,不

依赖于互斥机制,而是由中断和循环缓冲区异步唤醒的。大家可以尝试一下,

当改为互斥机制的printf_函数时,系统会宕机。

*/

kprintf_("%c", keymap[(d - 0x100) & 0x7f]);

}

}

/*

这里我们鼠标中断处理程序发送过来的数据。

*/

if((d >= 0x100 + 0x100) && (d < 0x100 + 0x100 + 0x100)) {

if(mouse_decode(&mdec, d - 0x100 - 0x100)) {

/*

在鼠标未动之前,把鼠标指针消隐。

*/

// draw_mouse_before(video + 1024 * 128,

// mx, my, 16, 16, mouse_before);

/*

鼠标数据中的横向和纵向数据其实是相对与当前的增减数据。

*/

mx += mdec.x;

my += mdec.y;

/*

对数据在画面的范围进行判断。也就是横轴和纵轴的极值。

*/

if(mx < 0) {

mx = 0;

}

if(my < 0) {

my = 0;

}

if(mx > 1024 - 1) {

mx = 1024 - 1;

}

if(my > 768 - 1 - 128) {

my = 768 - 1 - 128;

}

/*

移动后,根据新的坐标位置重新描画鼠标指针。

*/

sheet_slide(sht_mouse, mx, my);

int btn = mdec.btn;

/*

这里是鼠标左键的处理。

*/

if(btn & 1) {

/*

mmx和mmy是窗口被移动前的坐标信息,它同时也表示窗口是否

是移动状态。窗口被移动的最高前提条件时,鼠标左键被按下,

而是否按在了窗口上,需要在从高往低遍历图层,只有当窗口位于鼠标

坐标范围内且不是透明色,才把窗口置为次顶层。显然mmx<0是

未被移动的状态,窗口也只有这两种状态。这时便可以通过mmx、

mmy记住窗口未移动前的位置。因为已经左击了窗口,所以窗口

位置升高到鼠标的下层(次顶层)。当未处于移动状态时,仅仅

点击了鼠标左键,则窗口位置提升后,退出循环。

*/

if(mmx < 0) {

for(j = shtctl->top - 1; j > 0; j--) {

sht = shtctl->sheets[j];

x = mx - sht->vx0;

y = my - sht->vy0;

if(0 <= x && x < sht->bxsize &&

0 <= y && y < sht->bysize)

{

if(sht->buf[y * sht->bxsize + x]

!= sht->col_inv) {

sheet_updown(sht, shtctl->top - 1);

mmx = mx;

mmy = my;

}

break;

}

}

/*

仅仅是mmx和mmy大于0并不能引起窗口的移动,只有在按下鼠标后,鼠标位置

的值发生变化了,才可能移动窗口图层。它们的差值加上图层在画面中的初始

位置作为移动窗口图层函数的参数。同时,在鼠标未按下时置图层移动标志为

-1,即不会移动。

*/

} else {

x = mx - mmx;

y = my - mmy;

sheet_slide(sht, sht->vx0 + x, sht->vy0 + y);

mmx = mx;

mmy = my;

}

} else {

mmx = -1;

}

if(btn & 2) {

/*

这里是鼠标右键的处理,现在未作任何处理。

*/

}

if(btn & 4) {

/*

这里是鼠标中键的处理,现在未作任何处理。

*/

}

}

 

}

 

}

}

}

 

/*

选择要执行的指令,这里只是随意安排的,目前仅仅是画实心矩形、

实心圆以及空心圆。

*/

void instructions_switch(struct buf* e) {

switch(e->instruction) {

case 0:

fill_rectangle((unsigned int*)e->argv[0], e->argv[1], e->argv[2],

e->argv[3], e->argv[4], e->argv[5], e->argv[6]);

break;

case 1:

fill_circle((unsigned int*)e->argv[0], e->argv[1], e->argv[2],

e->argv[3], e->argv[4], e->argv[5]);

break;

case 5:

draw_circle((unsigned int*)e->argv[0], e->argv[1], e->argv[2],

e->argv[3], e->argv[4], e->argv[5]);

break;

default:

break;

}

}

 

extern unsigned int ticks;

 

void init(void) {

ticks = 0;

/*

全局描述符表的初始化。

*/

gdt_init();

/*

分页机制的初始化。

*/

page_init();

/*

中断描述符表的初始化。

*/

idt_init();

/*

可编程中断控制器的初始化。

*/

i8259_init();

/*

实时时钟的初始化。

*/

rtc_init();

/*

发生频率的初始化。

*/

i8253_init();

/*

内存管理模块的初始化。

*/

mem_init();

thread_init();

screen_init_();

/*

一定要初始化指令队列。

*/

queue_init(&ins_queue);

 

keyboard_init();

mouse_init();

timer_man_init();

tss_init();

syscall_init();

hd_init();

}

 

unsigned int uav = 0, ubv = 0, ucv = 0;

 

void k_thread_a(void* arg) {

/*

每个线程都通过入队指令向指令队列中插入指令。当插入指令时,

为了防止竞争条件的发生,果断的关闭中断。

*/

struct buf e;

/*

通过临时变量,构造一个符合指令格式的完整指令。

*/

e.instruction = 0;

e.argc = 7;

e.t = running_thread();

e.argv[0] = 0xe0000000;

e.argv[1] = 1024;

e.argv[2] = 400;

e.argv[3] = 400;

e.argv[4] = 30;

e.argv[5] = 50;

e.argv[6] = 0x00ffff00;

// asm("cli");

// queue_in(&ins_queue, &e);

// asm("sti");

 

e.instruction = 0;

e.argc = 7;

e.t = running_thread();

e.argv[0] = 0xe0000000;

e.argv[1] = 1024;

e.argv[2] = 100;

e.argv[3] = 100;

e.argv[4] = 70;

e.argv[5] = 70;

e.argv[6] = 0x0000ff00;

 

// asm("cli");

// queue_in(&ins_queue, &e);

// asm("sti");

 

struct fat32_krn_p fat32_p;

read_mbr_dbr(&fat32_p);

fat32_krn = fat32_p;

unsigned int i;

for(i = 0; i < 0xffff; i++){

sema_post(pevent_);

}

char* s = file_read("hello.txt");

printf_("!!!%x %x %x!!!", fat32_krn.fat1_p, fat32_krn.root_dir_p,

fat32_krn.root_dir_start_sector);

printf_(s);

 

while(1) {

printf_(" THREAD A ");

mtime_sleep1(8000);

}

/*

*/

}

 

void k_thread_b(void* arg) {

 

struct buf e;

e.instruction = 0;

e.argc = 7;

e.t = running_thread();

e.argv[0] = 0xe0000000;

e.argv[1] = 1024;

e.argv[2] = 600;

e.argv[3] = 200;

e.argv[4] = 80;

e.argv[5] = 80;

e.argv[6] = 0x00ff0000;

// asm("cli");

// queue_in(&ins_queue, &e);

// asm("sti");

 

 

e.instruction = 5;

e.argc = 6;

e.t = running_thread();

e.argv[0] = 0xe0000000;

e.argv[1] = 1024;

e.argv[2] = 300;

e.argv[3] = 300;

e.argv[4] = 100;

e.argv[5] = 0x00ffffff;

// asm("cli");

// queue_in(&ins_queue, &e);

// asm("sti");

sema_pend(pevent_);

///*

char* s = file_read("y.bmp");

struct bmp_buf_info bbi;

save_bmp((char*)s, &bbi);

unsigned int* v = (unsigned int*)(0xe0000000 + 1024 * 128 * 4);

put_bmp_buf(v, 1024, 700, 300, &bbi);

 

while(1) {

printf_(" THREAD B ");

mtime_sleep1(9188);

}

/*

*/

}

 

void k_thread_c(void* arg) {

 

struct buf e;

e.instruction = 0;

e.argc = 7;

e.t = running_thread();

e.argv[0] = 0xe0000000;

e.argv[1] = 1024;

e.argv[2] = 600;

e.argv[3] = 200;

e.argv[4] = 50;

e.argv[5] = 100;

e.argv[6] = 0x00ff00ff;

// asm("cli");

// queue_in(&ins_queue, &e);

// asm("sti");

 

e.instruction = 1;

e.argc = 6;

e.t = running_thread();

e.argv[0] = 0xe0000000;

e.argv[1] = 1024;

e.argv[2] = 200;

e.argv[3] = 400;

e.argv[4] = 50;

e.argv[5] = 0x000f00f0;

// asm("cli");

// queue_in(&ins_queue, &e);

// asm("sti");

 

sema_pend(pevent_);

///*

char* s = file_read("l.bmp");

struct bmp_buf_info bbi;

save_bmp((char*)s, &bbi);

unsigned int* v = (unsigned int*)(0xe0000000 + 1024 * 128 * 4);

put_bmp_buf(v, 1024, 500, 320, &bbi);

 

while(1) {

printf_(" THREAD C ");

mtime_sleep1(10088);

}

/*

*/

}

 

/*

三个进程的实体,它们仅仅是玩了命的自增三个不同的全局

变量。显示变量的工作由内核进程来完成。

*/

void ua(void) {

while(1) {

// uav++;

/*

之所以这里没用,printf_打印变量信息,主要是因为它在几层

调用后含有特权指令,所以会出现一般保护异常。我们暂时选择

用全局变量在线程中显示。

*/

write(" PORCESS A");

msleep(8000);

}

}

 

void ub(void) {

while(1) {

write(" PORCESS B");

msleep(8000);

}

}

 

void uc(void) {

while(1) {

write(" PORCESS C");

msleep(8000);

}

}

 

void idle(void* arg) {

unsigned char* s = arg;

while(1) {

printf_(s);

__asm__ __volatile__ ("sti; hlt;");

}

}

 

同时在最后给大家一个完整kernel.c,从而可以看到系统实际运行的简单原貌。下图大家可以看到,真是美女如云啊,至此,我们Snail OS开发的第一阶段也就是这样了。拼一个自己的操作系统真是不容易啊!。

 

58d9637d5bfcaa28267a94c7be99bd48.png

 

 

参考书目:

《操作系统真相还原》郑钢

《30天自制操作系统》川合秀实

《linux内核完全剖析》赵炯

《一个操作系统的实现》于渊

 

 

内容简介   本书从只有二十行的引导扇区代码出发,一步一步地向读者呈现一个操作系统框架的完成过程。书中不仅关注代码本身,同时关注完成这些代码的思路和过程。本书不同于其他的理论型书籍,而是提供给读者一个动手实践的路线图。读者可以根据路线图逐步完成各部分的功能,从而避免了一开始就面对整个操作系统数万行代码时的迷茫和挫败感。书中讲解了大量在开发操作系统中需注意的细节问题,这些细节不仅能使读者更深刻地认识操作系统的核心原理,而且使整个开发过程少走弯路。本书分上下两篇,共11章。其中每一章都以前一章的工作成果为基础,实现一项新的功能。而在章的内部,一项大的功能被分解成许多小的步骤,通过完成每个小的步骤,读者可以不断获得阶段性的成果,从而让整个开发过程变得轻松并且有趣。   本书适合各类程序员、程序开发爱好者阅读,也可作为高等院校操作系统课程的实践参考书。 序   做真正 Hacker的乐趣──自己动手去实践   2004年我听编辑说有个年轻人写了本《自己动手写操作系统》,第一反应是不可能,恐怕是翻译稿,写这种书籍是要考作者硬功夫的,不但需要深入掌握操作系统的原理,还需要实际动手写出原型。   历史上的 Linux就是这么产生的,Linus Torvalds当时是一名赫尔辛基大学计算机科学系的二年级学生,经常要用自己的电脑去访问大学主机上的新闻组和邮件,为了方便读写和下载文件,他自己编写了磁盘驱动程序和文件系统,这成为了 Linux一个内核的雏形。   我想中国有能力写出内核原型的程序员应该也有,但把这个题目写成一本书,感觉上不会有人愿意做这件事情,作者要花很多时间,加上主题比较硬,销售量不会太高,经济上回报有限。   但拿来文稿一看,整个编辑部大为惊艳,内容文笔俱佳,而且绝对原创,马上决定在《程序员》连载。2005年博文视点出版的第一版也广受好评。   不过有很多读者还是质疑:现在软件编程主要领域是框架和应用,还需要了解操作系统底层吗?   经过四年的磨练成长,于渊又拿出第二版的书稿《Orange'S:一个操作系统实现》,这本书是属于真正 Hacker的。我虽然已经有多年不写代码了,但看这本书的时候,让我又重新感受到做程序员的乐趣:用代码建设属于自己的系统,让电脑听从自己的指令,对系统的每个部分都了如指掌。   黑客(hacker)实际是褒义词,维基百科的解释是喜欢用智力通过创造性方法来挑战脑力极限的人,特别是他们所感兴趣的领域,例如软件编程或电气工程。个人电脑、软件和互联网等划时代的产品都是黑客创造出来的,如苹果的 Apple电脑、微软的 Basic解释器、互联网的 Mosaic浏览器。   回答前面读者的质疑,学软件编程并不需要看这本书,想成为优秀程序员和黑客的朋友,我强烈建议你花时间来阅读这本书,并亲自动手实践。正如于渊在本书结尾中所说“我们写自己的操作系统是出于一种好奇,或者说一种求知欲。我希望这样不停地‘过把瘾’能让这种好奇不停地延续”。   好奇心是动力的源泉,追究问题的本质是优秀黑客的必备素质,只有充分掌握了系统原理,才能在技术上游刃有余,才能有真正的创新和发展。中国需要更多真正的黑客,也希望更多的程序员能享受属于黑客的创造乐趣。   蒋涛   2009年 4月 作者自序   本书是《自己动手写操作系统》的第二版,通过一个具体的实例向读者呈现一个操作系统雏形的实现过程。有关操作系统的书籍资料可以找到很多,但是关注如何帮助读者实现一个试验性操作系统的书籍却不多见,本书便是从一个简单的引导扇区开始,讲述一个操作系统成长的故事,以作读者参考之用。   本书面向实践,通过具体实例教读者开发自己的操作系统。书中的步骤遵循由小到大、由浅入深的顺序,跟随这些步骤,读者可以由一个最简单的引导扇区开始,逐渐完善代码,扩充功能,最后形成一个小的操作系统。   本书不仅介绍操作系统的各要素,同时涉及开发操作系统需要的各个方面,比如如何建立开发环境、如何调试以及如何在虚拟机中运行等。书中的实例操作系统采用IA32作为默认平台,所以保护模式也作为必备知识储备收入书中,而这是传统的操作系统实践书籍经常忽略的。总之,只要是开发自己的操作系统中需要的知识,书中都尽量涉及,以便于读者参考。   众所周知,一个成型的操作系统往往非常复杂。如果考虑到操作系统作为软硬件桥梁的特殊地位,那么它可能看上去比一般的软件系统更难理解,因为其核心部分往往包含许多直接针对CPU、内存和 I/O端口的操作,它们夹杂在一片代码汪洋之中,显得更加晦涩。   我们有许多源代码公开的操作系统,可供随时下载和阅读,看上去好像让实现一个供自己把玩的微型操作系统变得容易很多,但事实往往不尽人意,因为这些代码动辄上万甚至几十几百万行,而且细节之间经常互相关联,要理解它们着实不易。我们有许多容易得到的操作系统教程,但读来好像总觉得跟我们有隔膜,不亲近。造成这些的根本原因,在于学习者一开始就面对一个完整的操作系统,或者面对前辈们积累了几十年的一系列理论成果。而无论作者多么擅长写作,读者多么聪明,或者代码多么优秀,要一个初学者理清其中的头绪都将是非常困难的。   我并非在此危言耸听,因为这曾经是我的亲身体会。当然,如果只是为了考试,几本操作系统理论书籍就足够了,你不需要对细节那么清楚。但如果是出于兴趣呢?如果你是想编写自己的操作系统呢?你会发现理论书籍好像一下子变得无用武之地,你会发现任何一个细节上的理解错误都可能导致自己辛辛苦苦编写的代码运行异常甚至崩溃。   我经历过这一切!我曾经翻遍了一本《操作系统:设计与实现》,也没有找到实现一个操作系统应该从何处着手。并不是这些书不好,也不是前人的代码不优秀,而是作为一无所知的初学者,我们所不了解的不仅是高居庙堂的理论知识,还有让我们举步维艰的实践细节。   可能在这些教科书作者的眼里,操作的细节不属于课程的一部分,或者这些细节看上去太容易,根本不值一提,甚至作者认为这些属于所谓“经验”的一部分,约定俗成是由读者本人去摸索的。但是实际情况往往是,这些书中忽略掉的内容恰恰占去了一个初学者大部分的时间,甚至影响了学习的热情。   我至今仍记得当我开始编写自己的操作系统时所遭受的挫败感,那是一种不知道如何着手的无助的感觉。还好我坚持了下来,克服了各种困难,并完成了自己的操作系统雏形。   进而我想到,一定不只是我一个人对编写自己的操作系统怀有兴趣,也一定不只是我一个人在实践时遇到困难。或许我应该把自己的经历写下来,从而可以帮助跟我相似的后来者,就这样,我编写了本书的第一版,也就是《自己动手写操作系统》。我相信,如果你也对神奇的计算机世界充满好奇,并且希望通过自己编写操作系统的方式来了解背后发生的故事,那么你一定可以在这本书中得到一些帮助。而假如你真的因为我的书而重新燃起实践的热情,从而开始一段操作系统旅程,我将会感到非常高兴。   不过我得坦白,在写作《自己动手写操作系统》的时候,我并不敢期待它能引起多少反响,一方面因为操作系统并不是时尚的话题,另一方面我也是走在学习的路上,或许只是比读者早走了一小步而已。然而出乎我的意料,它面世后重印多次,甚至一度登上销量排行榜的榜首,这让我觉得它的确有一定的参考价值,我要借此机会感谢所有支持我的读者。   在我写作《自己动手写操作系统》的时候,并没有想过今天会有一个第二版。原因在于,我希望这本书是用来填补空白的,而不是重复去做别人已经做得很好的事情。所谓填补空白,具体说就是让像我一样的操作系统爱好者在读完本书之后,能够有信心去读其他比较流行的开源的操作系统代码,有能力从零开始自己动手写操作系统,而这个任务第一版已经完成了。   那么为什么我又写作了第二版呢?原因有几个方面。第一,虽然第一版未曾涉及的进程间通信、文件系统等内容在许多书中都有讲解,但阅读的时候还是感觉有语焉不详的通病,作者本人可能很清楚原委,但写得太简略,以至于读者看来未必清晰。第二,我自己想把这个圈画圆。第一版的书虽然完成了它的使命,但毕竟到书的结尾,读者看到的不是一个真正的操作系统,它没有文件系统,没有内存管理,什么也干不了。在第二版中,你将会看到,你已经可以通过交叉编译的方式为我们的实验性 OS编写应用程序了,也就是说,它已经具备操作系统的基本功能,虽然仍然极其简陋,但第一个圈,毕竟是已经圆起来了。第三,实践类的操作系统书籍还是太少了,以至于你要想看看别人是怎么做的,除了读以《操作系统:设计与实现》为代表的极少数书籍之外,就是一头扎进源代码中,而结果有时相当令人气馁。我自己也气馁过,所以我在第二版中,仍然试图把话说细一点,把自己的经验拿出来分享。而且我选择我能想到的最精简的设计,以便让读者不至于陷入太多细节而无法看到全貌。我想这是本书可能具有的价值所在──简化的易懂的设计,还有尽量详细的文字。   在这一版中,内容被划分成上下两篇。上篇基本上是第一版的修订,只是做了一个调整,那便是在兼顾 WindowsLinux两方面用户的基础上,默认在Linux下建立开发环境来编写我们的操作系统。至于这样做的原因,在本书第 2章有比较详细的说明。当然,开发环境毕竟是第二位的,书中讲述的内容以及涉及的代码跟第一版都是一致的。本书的下篇全部都是新鲜内容,主要是增加了进程间通信、文件系统和内存管理。跟第一版的做法相同,下篇仍然不仅关注结果,更加致力于将形成一个结果的过程呈现出来。与此同时,由于本书旨在分享和引路,所以尽可能地简化了设计,以便将最重要的部分凸显出来。读者将看到,一个操作系统的文件系统和内存管理可以简陋到什么程度。简陋不是缺点,对于我们初学者而言,正是需要从简陋入手。换言之,如果你已经对实现一个操作系统有了一定的经验,那么这本书可能不适合你。这本书适合从来没有编写过操作系统的初学者。   本书的排版是我用L ATEX自己完成的。在排版中我花了一些工夫,因为我希望读者购买的首先是一本易于阅读且赏心悦目的书,其次才是编写操作系统的方法。另外,书中列出的代码均由我自己编写的程序自动嵌入L ATEX源文件,从而严格保证书和光盘的一致性,读者可以根据文件名和行号方便地找到光盘中   代码的准确位置。   此外,在第二版中还有一些小的变化。首先是操作系统的名字改变了,原因在于虽然我们的试验性   OS从前辈们那里借鉴了很多东西,但其各个部分的设计(比如文件系统和内存管理)往往有其独特之处,所以我将原先的 Tinix(本意为 TryMinix)改成了新名字Orange ’S(这个名字来自于我的妻子 ,),以表示它们的不同。另外,书中的代码风格,有些地方也做了调整。   我想,虽然第二版有着这样那样的变化,但有一点没有变,那就是本书试图将我在编写自己操作系统的过程中的经验尽可能地告诉读者,同时尽可能将我当初的思路和编码过程呈现出来。很可能读者比我更聪明,有更好的解决问题的方法,但无论如何,我认为我自己的经验可以为读者所借鉴。如果真是如   此,我将会非常欣慰。   在第二版的编写过程中,我同样要感谢许多人。感谢我的父母和爷爷对我的爱,并希望爷爷不要为我担心,写书是件辛苦的事,但同时也使我收获良多。爸爸在第二版的最后阶段帮我订正文字,这本书里有你的功劳。我要感谢博文视点的各位朋友,感谢郭老师的理解和支持,感谢李玲的辛勤工作,感谢江立和李冰,你们的高效让我非常钦佩。我还要感谢孟岩老师,你给我的鼓励我一直记在心里。我要感谢我的挚友郭洪桥,不仅仅因为你在技术上给我的帮助,更加因为你在精神上给我的支持。感谢我的同事和朋友张会昌,你在技术上的广度和深度总令我钦佩。另外,在第一版中帮助我的人,我要再次谢谢你们,因为没有第一版,也就没有第二版。   在所有人中我最应该感谢和最想感谢的,是我的妻子黄丹红,感谢你给我的所有建议,还有你帮我画的图。尤其是,当这本书在我预想的时间内没有完成的时候,当我遇到困难迟迟不能解决的时候,你总在一旁给我鼓励,在你那里,我从来都能感觉到一种温暖,我深知,如果没有你的支持,我无法坚持下来将书写完。谢谢你,这本书同样属于你。   跟第一版相比,这本书涉及的内容触及操作系统设计的更多方面,而由于笔者的水平实在有限,难免有纰漏甚至错误。如果读者有任何的问题、意见或建议,请登录http://www.osfromscratch.org,让我们共同探讨,共同进步。   本书导读   这本书适合谁   本书是一本操作系统实践的技术书籍。对于操作系统技术感兴趣,想要亲身体验编写操作系统过程的实践主义者,以及Minix、Linux源代码爱好者,都可以在本书中得到实践中所需的知识和思路。   本书以“动手写”为指导思想,只要是跟“动手写”操作系统有关的知识,都作为介绍对象加以讨论,所以,从开发环境的搭建,到保护模式,再到IBMPC中有关芯片的知识,最后到操作系统本身的设计实现,都能在本文中找到相应介绍。所以如果你也想亲身实践的话,本书可以省去你在书店和互联网寻找相应资料的过程,使你的学习过程事半功倍。在读完本书后,你不但可以获得对于操作系统初步的感性认识,并且对 IBMPC的接口、IA架构之保护模式,以及操作系统整体上的框架都将会有一定程度的了解。   笔者相信,当你读完本书之后,如果再读那些纯理论性的操作系统书籍,所获得的体验将会完全不同,因为那些对你而言不再是海市蜃楼。   对于想阅读 Linux源代码的操作系统爱好者,本书可以提供阅读前所必要的知识储备,而这些知识储备不但在本书中有完整的涉及,而且在很多 Linux书籍中是没有提到的。   特别要提到的是,对于想通过阅读 Andrew S. Tanenbaum和 Albert S. Woodhull的《操作系统:设计与实现》来学习操作系统的读者,本书尤其适合作为你的引路书籍,因为它翔实地介绍了初学者入门时所必需的知识积累,而这些知识在《操作系统:设计与实现》一书中是没有涉及的,笔者本人是把这本书作为写操作系统的主要参考书籍之一,所以在本书中对它多有借鉴。   你需要什么技术基础   在本书中所用到的计算机语言只有两种:汇编和 C语言。所以只要你具备汇编和 C语言的经验,就可以阅读本书。除对操作系统常识性的了解(比如知道中断、进程等概念)之外,本书不假定读者具备其他任何经验。   如果你学习过操作系统的理论课程,你会发现本书是对于理论的吻合和补充。它是从实践的角度为你展现一幅操作系统画面。   书中涉及了 Intel CPU保护模式、Linux命令等内容,到时候会有尽可能清晰的讲解,如果笔者认为某些内容可以通过其他教材系统学习,会在书中加以说明。   另外,本书只涉及 Intel x86平台。   统一思想——让我们在这些方面达成共识   道篇   让我们有效而愉快地学习   你大概依然记得在你亲自敲出第一个“Hello world”程序并运行成功时的喜悦,那样的成就感助燃了你对编写程序浓厚的兴趣。随后你不断地学习,每学到新的语法都迫不及待地在计算机上调试运行,在调试的过程中克服困难,学到新知,并获得新的成就感。   可现在请你设想一下,假如课程不是这样的安排,而是先试图告诉你所有的语法,中间没有任何实践的机会,试问这样的课程你能接受吗?我猜你唯一的感受将是索然寡味。   原因何在?只是因为你不再有因为不断实践而获得的源源不断的成就感。而成就感是学习过程中快乐的源泉,没有了成就感,学习的愉快程度将大打折扣,效果于是也将变得不容乐观。   每个人都希望有效而且愉快的学习过程,可不幸的是,我们见到的操作系统课程十之八九令我们失望,作者喋喋不休地讲述着进程管理存储管理I/O控制调度算法,可我们到头来也没有一点的感性认识。我们好像已经理解却又好像一无所知。很明显,没有成就感,一点也没有。笔者痛恨这样的学习过程,也决不会重蹈这样的覆辙,让读者获得成就感将是本书的灵魂。   其实这本书完全可以称作一本回忆录,记载了笔者从开始不知道保护模式为何物到最终形成一个小小   OS的过程,这样的回忆录性质保证了章节的安排完全遵从操作的时间顺序,于是也就保证了每一步的可操作性,毫无疑问,顺着这样的思路走下来,每一章的成果都需要努力但又尽在眼前,步步为营是我   们的战术,成就感是我们的宗旨。   我们将从二十行代码开始,让我们最简单的操作系统婴儿慢慢长大,变成一个翩翩少年,而其中的每一步,你都可以在书中的指导下自己完成,不仅仅是看到,而是自己做到!你将在不断的实践中获得不断的成就感,笔者真心希望在阅读本书的过程中,你的学习过程可以变得愉快而有效。   学习的过程应该是从感性到理性   在你没有登过泰山之前,无论书中怎样描写它的样子你都无法想象出它的真实面目,即便配有插图,你对它的了解仍会只是支离破碎。毫无疑问,一千本对泰山描述的书都比不上你一次登山的经历。文学家的描述可能是华丽而优美的,可这样的描述最终产生的效果可能是你非去亲自登泰山不可。反过来想呢,假如你已经登过泰山,这样的经历产生的效果会是你想读尽天下描述泰山的书而后快吗?可能事实恰恰相反,你可能再也不想去看那些文字描述。   是啊,再好的讲述,又哪比得上亲身的体验?人们的认知规律本来如此,有了感性的认识,才能上升为理性的理论。反其道而行之只能是事倍功半。   如果操作系统是一座这样的大山,本书愿做你的导游,引领你进入它的门径。传统的操作系统书籍仅仅是给你讲述这座大山的故事,你只是在听讲,并没有身临其境,而随着这本书亲身体验,则好像置身于山门之内,你不但可以看见眼前的每一个细节,更是具有了走完整座大山的信心。   值得说明的是,本书旨在引路,不会带领你走完整座大山,但是有兴趣的读者完全可以在本书最终形成的框架的基础上容易地实现其他操作系统书籍中讲到的各种原理和算法,从而对操作系统有个从感性到理性的清醒认识。   暂时的错误并不可怕   当我们对一件事情的全貌没有很好理解的时候,很可能会对某一部分产生理解上的误差,这就是所谓的断章取义。很多时候断章取义是难免的,但是,在不断学习的过程中,我们会逐渐看到更多,了解更多,对原先事物的认识也会变得深刻甚至不同。   对于操作系统这样复杂的东西来说,要想了解所有的细节无疑是非常困难的,所以在实践的过程中,可能在很多地方,会有一些误解发生。这都没有关系,随着了解的深入,这些误解总会得到澄清,到时你会发现,自己对某一方面已经非常熟悉了,这时的成就感,一定会让你感到非常愉悦。   本书内容的安排遵从的是代码编写的时间顺序,它更像是一本开发日记,所以在书中一些中间过程不完美的产物被有意保留了下来,并会在以后的章节中对它们进行修改和完善,因为笔者认为,一些精妙的东西背后,一定隐藏着很多中间的产物,一个伟大的发现在很多情况下可能不是天才们刹那间的灵光一闪,背后也一定有着我们没有看到的不伟大甚至是谬误。笔者很想追寻前辈们的脚步,重寻他们当日的足迹。做到这一点无疑很难,但即便无法做到,只要能引起读者的一点思索,也是本书莫大的幸事。   挡住了去路的,往往不是大树,而是小藤   如果不是亲身去做,你可能永远都不知道,困难是什么。   就好像你买了一台功能超全的微波炉回家,研究完了整本说明书,踌躇满志想要烹饪的时候,却突然发现家里的油盐已经用完。而当时已经是晚上十一点,所有的商店都已经关门,你气急败坏,简直想摸起铁勺砸向无辜的微波炉。   研究说明书是没有错的,但是在没开始之前,你永远都想不到让你无法烹饪的原因居然是十块钱一瓶的油和一块钱一袋的更加微不足道的盐。你还以为困难是微波炉面板上密密麻麻的控制键盘。   其实做其他事情也是一样的,比如写一个操作系统,即便一个很小的可能受理论家们讥笑的操作系统雏形,仍然可能遇到一大堆你没有想过的问题,而这些问题在传统的操作系统书籍中根本没有提到。所以唯一的办法,便是亲自去做,只有实践了,才知道是怎么回事。   术篇   用到什么再学什么   我们不是在考试,我们只是在为了自己的志趣而努力,所以就让我们忠于自己的喜好吧,不必为了考试而看完所有的章节,无论那是多么的乏味。让我们马上投入实践,遇到问题再图解决的办法。笔者非常推崇这样的学习方法:   实践 →遇到问题 →解决问题 →再实践   因为我们知道我们为什么学习,所以我们才会非常投入;由于我们知道我们的目标是解决什么问题,所以我们才会非常专注;由于我们在实践中学习,所以我们才会非常高效。而最有趣的是,最终你会发现你并没有因为选择这样的学习方法而少学到什么,相反,你会发现你用更少的时间学到更多的东西,并且格外的扎实。   只要用心,就没有学不会的东西   笔者还清楚地记得刚刚下载完 Intel Architecture Software Developer Manual那三个可怕的 PDF文件时的心情,那时心里暗暗嘀咕,什么时候才能把这些东西读懂啊!可是突然有一天,当这些东西真的已经被基本读完的时候,我想起当初的畏惧,时间其实并没有过去多少。   所有的道理都是相通的,没有什么真正可怕,尤其是,我们所做的并非创造性的工作,所有的问题前人都曾经解决,所以我们更是无所畏惧,更何况我们不仅有书店,而且有互联网,动动手脚就能找到需要的资料,我们只要认真研究就够了。   所以当遇到困难时,请静下心来,慢慢研究,因为只要用心,就没有学不会的东西。   适当地囫囵吞枣   如果囫囵吞枣仅仅是学习的一个过程而非终点,那么它并不一定就是坏事。大家都应该听说过鲁迅先生学习英语的故事,他建议在阅读的过程中遇到不懂的内容可以忽略,等到过一段时间之后,这些问题会自然解决。   在本书中,有时候可能先列出一段代码,告诉你它能完成什么,这时你也可以大致读过,因为下面会有对它详细的解释。第一遍读它的时候,你只要了解大概就够了。    本书的原则   1.宁可啰嗦一点,也不肯漏掉细节   在书中的有些地方,你可能觉得有些很“简单”的问题都被列了出来,甚至显得有些啰嗦,但笔者宁可让内容写得啰嗦点,因为笔者自己在读书的时候有一个体验,就是有时候一个问题怎么也想不通,经过很长时间终于弄明白的时候才发现原来是那么“简单”。可能作者认为它足够简单以至于可以跳过不提,但读者未必那么幸运一下子就弄清楚。   不过本书到后面的章节,如果涉及的细节是前面章节提到过的,就有意地略过了。举个非常简单的例子,开始时本书会提醒读者增加一个源文件之后不要忘记修改Makefile,到后来就假定读者已经熟悉了这个步骤,可能就不再提及了。   2.努力做到平易近人   笔者更喜欢把本书称作一本笔记或者学习日志,不仅仅是因为它基本是真实的学习过程的再现,而且笔者不想让它有任何居高临下甚至是晦涩神秘的感觉。如果有一个地方你觉得书中没有说清楚以至于你没有弄明白,请你告诉我,我会在以后做出改进。 3.代码注重可读性但不注重效率   本书的代码力求简单易懂,在此过程中很少考虑运行的效率。一方面因为书中的代码仅仅供学习之用,暂时并不考虑实际用途;另一方面笔者认为当我们对操作系统足够了解之后再考虑效率的问题也不迟。   本书附带光盘说明   本书附带光盘中有本书用到的所有源代码。值得一提的是,其中不止包含完整的操作系统代码,还包含各个步骤的中间产物。换句话说,开发中每一步骤的代码,都可在光盘中单独文件夹中找到。举例说明,书的开篇介绍引导扇区,读者在相应文件夹中就只看到引导扇区的代码;第 9章介绍文件系统,在相应文件夹中就不会包含第 10章内存管理的代码。在任何一个步骤对应的文件夹中,都包含一个完整可编译运行的代码树,以方便读者试验之用。这样在学习的任何一个阶段,读者都可彻底了解阶段性成果,且不必担心受到自己还未学习的内容的影响,从而使学习不留死角。   在书的正文中引用的代码会标注出出自哪个文件。以“chapter5/b/bar.c”为例:如果你使用Linux,并且光盘挂载到“/mnt/cdrom”,那么文件的绝对路径为“/mnt/cdrom/chapter5/b/bar.c”;如果你使用Windows,并且光盘是 X:盘,那么文件的绝对路径为“X:nchapter5nbnbar.c”。 目 录   上 篇   第1章 马上动手写一个最小的“操作系统” 2   1.1 准备工作 2   1.2 十分钟完成的操作系统 3   1.3 引导扇区 4   1.4 代码解释 4   1.5 水面下的冰山 6   1.6 回顾 7   第2章 搭建你的工作环境 8   2.1 虚拟计算机Bochs 8   2.1.1 Bochs初体验 8   2.1.2 Bochs的安装 9   2.1.3 Bochs的使用 10   2.1.4 用Bochs调试操作系统 12   2.2 QEMU 15   2.3 平台之争:Windows还是*nix 16   2.4 GNU/Linux下的开发环境 20   2.5 Windows下的开发环境 22   2.6 总结 23   第3章 保护模式(Protect Mode) 25   3.1 认识保护模式 25   3.1.1 保护模式的运行环境 29   3.1.2 GDT(Global Descriptor Table) 31   3.1.3 实模式到保护模式,不一般的jmp 33   3.1.4 描述符属性 35   3.2 保护模式进阶 38   3.2.1 海阔凭鱼跃 38   3.2.2 LDT(Local Descriptor Table) 44   3.2.3 特权级概述 48   3.2.4 特权级转移 51   3.2.5 关于“保护”二字的一点思考 65   3.3 页式存储 65   3.3.1 分页机制概述 66   3.3.2 编写代码启动分页机制 67   3.3.3 PDE和PTE 68   3.3.4 cr3 71   3.3.5 回头看代码 72   3.3.6 克勤克俭用内存 73   3.3.7 进一步体会分页机制 81   3.4 中断和异常 87   3.4.1 中断和异常机制 87   3.4.2 外部中断 90   3.4.3 编程操作8259A 91   3.4.4 建立IDT 94   3.4.5 实现一个中断 95   3.4.6 时钟中断试验 96   3.4.7 几点额外说明 98   3.5 保护模式下的I/O 100   3.5.1 IOPL 100   3.5.2 I/O许可位图(I/O Permission Bitmap) 100   3.6 保护模式小结 101   第4章 让操作系统走进保护模式 102   4.1 突破512字节的限制 102   4.1.1 FAT12 103   4.1.2 DOS可以识别的引导盘 108   4.1.3 一个最简单的Loader 108   4.1.4 加载Loader入内存 109   4.1.5 向Loader交出控制权 116   4.1.6 整理boot.asm 116   4.2 保护模式下的“操作系统” 117   第5章 内核雏形 119   5.1 在Linux下用汇编写Hello World 119   5.2 再进一步,汇编和C同步使用 120   5.3 ELF(Executable and Linkable Format) 123   5.4 从Loader到内核 127   5.4.1 用Loader加载ELF 127   5.4.2 跳入保护模式 131   5.4.3 重新放置内核 137   5.4.4 向内核交出控制权 142   5.5 扩充内核 143   5.5.1 切换堆栈和GDT 144   5.5.2 整理我们的文件夹 148   5.5.3 Makefile 149   5.5.4 添加中断处理 155   5.5.5 两点说明 168   5.6 小结 169   第6章 进程 171   6.1 迟到的进程 171   6.2 概述 171   6.2.1 进程介绍 172   6.2.2 未雨绸缪——形成进程的必要考虑 172   6.2.3 参考的代码 173   6.3 最简单的进程 174   6.3.1 简单进程的关键技术预测 175   6.3.2 第一步——ring0→ring1 178   6.3.3 第二步——丰富中断处理程序 189   6.4 多进程 200   6.4.1 添加一个进程体 200   6.4.2 相关的变量和宏 200   6.4.3 进程表初始化代码扩充 202   6.4.4 LDT 203   6.4.5 修改中断处理程序 203   6.4.6 添加一个任务的步骤总结 206   6.4.7 号外:Minix的中断处理 207   6.4.8 代码回顾与整理 212   6.5 系统调用 220   6.5.1 实现一个简单的系统调用 222   6.5.2 get_ticks的应用 227   6.6 进程调度 232   6.6.1 避免对称——进程的节奏感 232   6.6.2 优先级调度总结 240   第7章 输入/输出系统 242   7.1 键盘 242   7.1.1 从中断开始——键盘初体验 242   7.1.2 AT、PS/2键盘 243   7.1.3 键盘敲击的过程 244   7.1.4 用数组表示扫描码 248   7.1.5 键盘输入缓冲区 251   7.1.6 用新加的任务处理键盘操作 253   7.1.7 解析扫描码 254   7.2 显示器 263   7.2.1 初识TTY 264   7.2.2 基本概念 264   7.2.3 寄存器 267   7.3 TTY任务 270   7.3.1 TTY任务框架的搭建 272   7.3.2 多控制台 277   7.3.3 完善键盘处理 281   7.3.4 TTY任务总结 288   7.4 区分任务和用户进程 289   7.5 printf 291   7.5.1 为进程指定TTY 292   7.5.2 printf()实现 292   7.5.3 系统调用write() 294   7.5.4 使用printf() 296   下 篇   第8章 进程间通信 300   8.1 微内核还是宏内核 300   8.1.1 Linux系统调用 302   8.1.2 Minix的系统调用 303   8.1.3 我们的选择 305   8.2 IPC 306   8.3 实现IPC 306   8.3.1 assert()和panic() 309   8.3.2 msg_send()和msg_receive() 313   8.3.3 增加消息机制之后的进程调度 321   8.4 使用IPC来替换系统调用get_ticks 322   8.5 总结 324   第9章 文件系统 325   9.1 硬盘简介 325   9.2 硬盘操作的I/O 端口 326   9.3 硬盘驱动程序 327   9.4 文件系统 337   9.5 硬盘分区表 338   9.6 设备号 344   9.7 用代码遍历所有分区 347   9.8 完善硬盘驱动程序 352   9.9 在硬盘上制作一个文件系统 355   9.9.1 文件系统涉及的数据结构 356   9.9.2 编码建立文件系统 358   9.10 创建文件 366   9.10.1 Linux下的文件操作 366   9.10.2 文件描述符(file descriptor) 367   9.10.3 open() 369   9.11 创建文件所涉及的其他函数 377   9.11.1 strip_path() 377   9.11.2 search_file() 378   9.11.3 get_inode()和sync_inode() 379   9.11.4 init_fs() 381   9.11.5 read_super_block()和get_super_block() 382   9.12 关闭文件 383   9.13 查看已创建的文件 384   9.14 打开文件 386   9.15 读写文件 387   9.16 测试文件读写 390   9.17 文件系统调试 393   9.18 删除文件 395   9.19 插曲:奇怪的异常 401   9.20 为文件系统添加系统调用的步骤 403   9.21 将TTY纳入文件系统 404   9.22 改造printf 411   9.23 总结 413   第10章 内存管理 414   10.1 fork 414   10.1.1 认识fork 414   10.1.2 fork前要做的工作(为fork所做的准备) 417   10.1.3 fork()库函数 421   10.1.4 MM 421   10.1.5 运行 427   10.2 exit和wait 427   10.3 exec 432   10.3.1 认识exec 433   10.3.2 为自己的操作系统编写应用程序 434   10.3.3 “安装”应用程序 436   10.3.4 实现exec 442   10.4 简单的shell 447   10.5 总结 449   第11章 尾声 451   11.1 让mkfs()只执行一次 451   11.2 从硬盘引导 455   11.2.1 编写硬盘引导扇区和硬盘版loader 455   11.2.2 “安装”hdboot.bin和hdldr.bin 461   11.2.3 grub 461   11.2.4 小结 463   11.3 将OS安装到真实的计算机 465   11.3.1 准备工作 465   11.3.2 安装Linux 466   11.3.3 编译源代码 466   11.3.4 开始安装 467   11.4 总结 467   参考文献 470 解密《一个操作系统实现》这本书 5 月 18 日见到了《 Orange'S :一个操作系统实现》的样书,多少有些激动。想一想前一版本《自己动手写操作系统》是那么畅销,这一本一定不能含糊。整个出版过程我能看到作者于渊为此付出的努力,还在自己排版的过程有深入体会,通过于渊的讲座也让博文视点的员工分享到他在排版过程中的很多心得。 应该有几万个朋友读过《自己动手写操作系统》了,本书的第 2 版《 Orange'S :一个操作系统实现》出来肯定有非常多的朋友想问,这两本书到底有何区别呢?就此博文视点对本书作者于渊进行了简单的采访。 * 提问:《 Orange'S :一个操作系统实现》与《自己动手写操作系统》明显区别在哪些方面? * 于渊:作为《自己动手写操作系统》(以下简称《自》)的第二版,《 Orange'S :一个操作系统实现》(以下简称“新版”)主要有以下变化: 1. 书中示例操作系统的名字改为 Orange'S 2. 书名改为《 Orange'S :一个操作系统实现》 3. 增加了有关 IPC 、 FS 、 MM 等内容 4. 将默认开发平台改为 GNU/Linux ,同时兼顾 Windows 5. 更改了排版工具,并使用技术手段增加书的可读性,比如代码行号的运用 6. 建立专门网站以服务读者 7. 建立专门讨论区供读者交流 读过《自己动手写操作系统》的读者一定知道,其中默认使用 Windows 作为开发平台,同时使用虚拟机来编译及运行自己的 OS ,在新版中这一点发生了变化(如上述第 4 条所述),具体的变化原因在书中第二章有详细的叙述。虽然开发平台是第二位的事情,但书中的默认平台却不免影响到叙述细节,所以,如果读者基于自己的原因坚持在 Windows 上开发(可能的原因或许有对 Linux 不熟悉、需要边开发操作系统边登录某些网上银行等等),则可能对读到的内容进行一点点额外加工。当然,所需的额外加工是少量的,而且在第二章中也有专门的文字介绍如何在两种平台下搭建工作环境。此外,如果读者不介意花钱,还可以同时购买《自己动手写操作系统》和新版,相互参照阅读。 * 提问:《 Orange'S :一个操作系统实现》与《自己动手写操作系统》相比是否有所增加吗?增加了多少内容量呢? 于渊:新版的内容是有增加的,新增文字约占整本书的三分之一,《 Orange'S :一个操作系统实现》新增代码则是《自己动手写操作系统》中代码的数倍。这些新增的内容,读者只能从新版中获得。目前并未有将新增内容单独成书的打算,所以读者即便仅想阅读第八章以后的内容,也需要购买整本《 Orange'S :一个操作系统实现》。已经购买了《自己动手写操作系统》的读者可能觉得有点浪费,但事实并不如此,因为《自己动手写操作系统》的内容经过了重新排版、修订和编辑(比如代码格式进行了重排,更方便与光盘中的文件对照阅读,以及其中所有的矢量图都用 pgf/TikZ 重新绘制等)笔者倾注的心血使得新版的感官已经大为不同,读者一看便知。 * 提问:在《自己动手写操作系统》大卖的时候,您是否想过会有第二版出版呢? * 于渊:坦白讲,我在写作《自》的时候,并没有想过今天会有一个第二版。原因在于,我希望这本书是用来填补空白的,而不是重复去做别人已经做得很好的事情。所谓填补空白,具体说就是让像我一样的操作系统爱好者在读完本书之后,能够有信心去读其他比较流行的开源的操作系统代码,有能力从零开始自己动手写操作系统,而这个任务第一版已经完成了。 * 提问:那么为什么又写作了第二版呢? * 于渊:原因有几个方面。第一,虽然第一版未曾涉及的进程间通信、文件系统等内容在许多书中都有讲解,但阅读的时候还是感觉有语焉不详的通病,作者本人可能很清楚原委,但写得太简略,以至于读者看来未必清晰。第二,我自己想把这个圈画圆。第一版的书虽然完成了它的使命,但毕竟到书的结尾,读者看到的不是一个真正的操作系统,它没有文件系统,没有内存管理,什么也干不了。在第二版中,你将会看到,你已经可以通过交叉编译的方式为我们的实验性 OS 编写应用程序了,也就是说,它已经具备操作系统的基本功能,虽然仍然极其简陋,但第一个圈,毕竟是已经圆起来了。第三,实践类的操作系统书籍还是太少了,以至于你要想看看别人是怎么做的,除了读以《操作系统:设计与实现》为代表的极少数书籍之外,就是一头扎进源代码中,而结果有时相当令人气馁。我自己也气馁过,所以我在第二版中,仍然试图把话说细一点,把自己的经验拿出来分享。而且我选择我能想到的最精简的设计,以便让读者不至于陷入太多细节而无法看到全貌。我想这是本书可能具有的价值所在──简化的易懂的设计,还有尽量详细的文字。 * 提问:这本书为何不考虑用 WORD 排版? * 于渊:新版的排版是我用 LaTeX 自己完成的。在排版中我花了一些工夫,因为我希望读者购买的首先是一本易于阅读且赏心悦目的书,其次才是编写操作系统的方法。另外,书中列出的代码均由我自己编写的程序自动嵌入 LaTeX 源文件,从而严格保证书和光盘的一致性,读者可以根据文件名和行号方便地找到光盘中代码的准确位置。 * 提问:第二版还有哪些区别呢? Orange'S 这个名字很特别,有什么寓意吗? * 于渊:新版中还有一些小的变化。首先是操作系统的名字改变了,原因在于虽然我们的试验性 OS 从前辈们那里借鉴了很多东西,但其各个部分的设计(比如文件系统和内存管理)往往有其独特之处,所以我将原先的 Tinix (本意为 TryMinix )改成了新名字 Orange'S (这个名字来自于我的妻子),以表示它们的不同。另外,书中的代码风格,有些地方也做了调整。 新版中,原先的叙述风格都尽量地得以贯彻,而在表现形式上,新版用了更多心思,我相信读者能在其中发现这些特点:关注动手细节,探寻代码背后的故事,结果与过程兼顾,内容与形式并重。加上专门为本书建立的网站和讨论区,我相信读者能更容易地阅读,更轻松地学习。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_39410618

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值