整体框架
:mbr.S->loader.S:当os镜像被加载之后,cpu转去main的起止位置开始执行。(到目前为止,控制权已经掌握在自己手里)
接下来就要一步一步搭建起内核模块:
一:实现基本的输入输出功能(lib/kernel/print.h 以及print.S)
接口:put_char() ; put_str() ; put_int() ; set_cursor() ; cls_screen()
实现的语言:汇编。
实现的原理:操纵显存以及显卡端口即可。需要储备显卡相关知识。
二:构建中断框架:操作端口的库的实现(lib/kernel/io.h 用汇编实现)中断实现(kernel/interrupt.h interrupt.c kernel.S)
理论:
搭建流程:
数据结构:
//中断门描述符struct gate_desc {uint16_t func_offset_low_word;uint16_t selector;uint8_t dcount; uint8_t attribute;uint16_t func_offset_high_word;
};struct gate_desc idt[IDT_DESC_CNT]//idt中断描述符表,在中断初始化的手加载的就是这个
char* intr_name[IDT_DESC_CNT]//保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]//真正的中断处理函数,在kernel.S中调用,kernel.S中是中断入口
intr_handler intr_entry_table[IDT_DESC_CNT]//中断处理函数入口(地址)数组
接口:
enum intr_status { // 中断状态
INTR_OFF, // 中断关闭
INTR_ON // 中断打开
};enum intr_status intr_get_status(void);//获得中断状态并
enum intr_status intr_set_status (enum intr_status);//设置新状态并且返回旧状态
enum intr_status intr_enable (void);//打开中断
enum intr_status intr_disable (void);//关闭中断
void register_handler(uint8_t vector_no, intr_handler function);//中断处理函数的注册
中断模块的初始化:
三:构建内存管理模块:字符串库的实现(lib/string.c string.h) 位图库的实现(lib/kernel/bitmap.c bitmap.h)(kernel/memory.c memory.h)
搭建流程
理论知识:
1、物理内存的管理:内核物理内存管理 ,用户物理内存管理
两者采用相同的管理方式:
(1)分配内存大小:4KB 以及小于 1024bytes
(2)管理方法:位图(bitmap)
2、虚拟内存管理:内核线程虚拟地址管理,用户态进程虚拟地址管理
(虚拟内存并不需要分配实际的空间,只需要管理每一个进程虚拟空间即可,动态与物理内存管理交互)
管理方法:位图(bitmap)
数据结构:
**内核中只需要持有两个内存池,一个是给内核分配物理内存的,一个是给用户分配内存的**
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理 内存uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址uint32_t pool_size; // 本内存池字节容量
struct lock lock; // 申请内存时互斥
};**此结构位于每一个页的开头**
struct arena {struct mem_block_desc* desc; // 此arena关联的mem_block_desc
/* large为ture时,cnt表示的是页框数。* 否则cnt表示空闲mem_block数量 */uint32_t cnt;bool large;
};/* 用于虚拟地址管理 */
**用于管理内核虚拟地址以及为每一个用户进程创建一个该结构来管理用户虚拟地址空间**
struct virtual_addr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单 位。*/struct bitmap vaddr_bitmap;
/* 管理的虚拟地址 */uint32_t vaddr_start;
};/* 内存块 */
struct mem_block {struct list_elem free_elem;
};/* 内存块描述符 */
struct mem_block_desc {uint32_t block_size; // 内存块大小uint32_t blocks_per_arena; // 本arena中可容纳此mem_block的数量.struct list free_list; // 目前可用的mem_block链表
};struct mem_block_desc k_block_descs[DESC_CNT]; // 内核内存块描述符数组
struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址
有关小于1024bytes字节空间分配数据结构的关系示意图:
两个内存池:
接口:
void mem_init(void);//内存管理初始化
void malloc_init(void);
void block_desc_init(struct mem_block_desc* desc_array);void* get_kernel_pages(uint32_t pg_cnt);//从内核池中获取一个物理页,并返回虚拟地址
void* get_user_pages(uint32_t pg_cnt);//从用户池中获得一个物理页,并返回虚拟地址
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);//从用户池或者内核池中获取一个物理页,并返回虚拟地址。uint32_t* pte_ptr(uint32_t vaddr);//获取pte的物理地址
uint32_t* pde_ptr(uint32_t vaddr);//获取pde的物理地址uint32_t addr_v2p(uint32_t vaddr);//从虚拟地址转换到物理地址void* get_a_page(enum pool_flags pf, uint32_t vaddr);从pf中获得一个物理页并映射到vaddrvoid* sys_malloc(uint32_t size);
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt);
void pfree(uint32_t pg_phy_addr);
void sys_free(void* ptr);
void* get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr);
void free_a_phy_page(uint32_t pg_phy_addr);
内存管理模块初始化:
四:线程模块的实现:链表库的实现(lib/kernel/list.c list.h) 线程库的实现(thread/thread.c thread.h switch.S)
理论:线程或者进程的调度算法:
搭建过程:
理论知识:
数据结构:
内核空间维持的两个进程/线程队列:
struct list thread_all_list;
struct list thread_ready_listpid号分配池中断栈intr_stack
struct intr_stack {uint32_t vec_no; // kernel.S 宏VECTOR中push %1压入的中断号uint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy; // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;/* 以下由cpu从低特权级进入高特权级时压入 */uint32_t err_code; // err_code会被压入在eip之后void (*eip) (void);uint32_t cs;uint32_t eflags;void* esp;uint32_t ss;
};线程栈thread_stack:struct thread_stack {uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;/* 线程第一次执行时,eip指向待调用的函数kernel_thread
其它时候,eip是指向switch_to的返回地址*/void (*eip) (thread_func* func, void* func_arg);/***** 以下仅供第一次被调度上cpu时使用 ****//* 参数unused_ret只为占位置充数为返回地址 */void (*unused_retaddr);thread_func* function; // 由Kernel_thread所调用的函数名void* func_arg; // 由Kernel_thread所调用的函数所需的参数
};
接口:
/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */void thread_create(struct task_struct* pthread, thread_func function, void* func_arg);//伪造好线程环境:thread_stack:其中包括待执行函数的指针,但调度器调度切换到该进程的时候便可以执行该函数,即伪造的断点。struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);//初始化线程相关信息void init_thread(struct task_struct* pthread, char* name, int prio);
以上两个函数完成了创造线程的工作:内核布局如下图所示:
//获取当前进程的task_struct结构struct task_struct* running_thread(void);//任务调度器void schedule(void);//线程模块初始化void thread_init(void);//阻塞进程void thread_block(enum task_status stat);//唤醒进程void thread_unblock(struct task_struct* pthread);//出让调度器但不阻塞当前进程,只是让出处理机,进入就绪队列,等待下次调度void thread_yield(void);pid_t fork_pid(void);void sys_ps(void);//线程退出,回收相关资源void thread_exit(struct task_struct* thread_over, bool need_schedule);struct task_struct* pid2thread(int32_t pid);void release_pid(pid_t pid);
初始化:
五:同步模块以及键盘等外设驱动模块的实现:(thread/sync.c sync,h)(device/*)
理论:
个人理解:在实现多线程,多进程的系统中,一个重要的模块或者说功能当然是同步与互斥。不管是单核还是多核,再面对共享资源的情况下,可能需要同步操作或者互斥访问。这就需要有信号量和锁这样的机制(注意不要把信号量和锁两个概念相混淆。锁可以通过信号量来实现,也可以用其它方式,如硬件提供的锁。再用信号量实现锁的情况下,锁相当于信号量的值为1的情况。而信号量同时也用于实现同步问题。当用于同步问题的时候,值的大小往往代表了资源的数量。)
我们通过运用开关中断的方式来实现不同线程或者进程之间的同步和互斥问题。
数据结构:
/* 信号量结构 */
struct semaphore {uint8_t value;struct list waiters;
};/* 锁结构 */
struct lock {struct task_struct* holder; // 锁的持有者struct semaphore semaphore; // 用二元信号量实现锁uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数
};
接口:
//信号量初始化
void sema_init(struct semaphore* psema, uint8_t value);
//信号量的p操作
void sema_down(struct semaphore* psema);
//信号量的v操作
void sema_up(struct semaphore* psema);
//锁的初始化
void lock_init(struct lock* plock);
//获得锁
void lock_acquire(struct lock* plock);
//释放锁
void lock_release(struct lock* plock);
六:用户进程以及更细粒度内存管理模块的实现:(userprog/process.c process.h tss.c tss.h)(kernel/memory.c memory.h)
个人理解:实现用户进程是在线程的基础上去实现的,首先我们伪造好一个进程环境,然后通过一个关键的指令:iret返回到用户态。伪造用户进程环境的过程如下:创建进程的过程如下如所示:
调用的主要函数有:init_thread() ; thread_create() ; create_page_dir() ; block_desc_init()
结果process_execute()函数创建好环境后,当调度到这个任务的时候,会执行start_process()函数,该函数会完全伪造好进入中断后的进程环境,并在最后执行iret真正返回到用户态。
接口:
void process_execute(void* filename, char* name);
void start_process(void* filename_);
void process_activate(struct task_struct* p_thread);//below three function is about page dir and vaddr//activate process page dir
void page_dir_activate(struct task_struct* p_thread);uint32_t* create_page_dir(void);//creat user process vaddr bitmap(each user process has a vaddr bitmap)
void create_user_vaddr_bitmap(struct task_struct* user_prog);
七:系统调用以及标准io的实现的实现:(userporog/syscall-init.h)(lib/user/syscall.c syscall.h) (lib/stdio.h)
先从整体上看一系统调用过程:
注意:早在中断模块初始化的时候就已经初始化好中断描述符当中系统调用的入口,入口如下(文件在kernel.S当中):
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境push 0 ; 压入0, 使栈中格式统一push dspush espush fspush gspushad ; PUSHAD指令压入32位寄存器,其入栈顺序是:; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式;2 为系统调用子功能传入参数push edx ; 系统调用中第3个参数push ecx ; 系统调用中第2个参数push ebx ; 系统调用中第1个参数;3 调用子功能处理函数call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数;4 将call调用后的返回值存入待当前内核栈中eax的位置mov [esp + 8*4], eaxjmp intr_exit ; intr_exit返回,恢复上下文
从代码中可以看到在系统调用的公共部分调用了call[syscall_table + eax*4] ,其中syscall_table是一个数组的起始地址,他是保存对应系统调用处理函数的入口地址的数组。该数组的初始化如下:(各个函数由相应的模块去实现)。
/* 初始化系统调用 */
void syscall_init(void) {put_str("syscall_init start\n");syscall_table[SYS_GETPID] = sys_getpid;syscall_table[SYS_WRITE] = sys_write;syscall_table[SYS_MALLOC] = sys_malloc;syscall_table[SYS_FREE] = sys_free;syscall_table[SYS_FORK] = sys_fork;syscall_table[SYS_READ] = sys_read;syscall_table[SYS_PUTCHAR] = sys_putchar;syscall_table[SYS_CLEAR] = cls_screen;syscall_table[SYS_GETCWD] = sys_getcwd;syscall_table[SYS_OPEN] = sys_open;syscall_table[SYS_CLOSE] = sys_close;syscall_table[SYS_LSEEK] = sys_lseek;syscall_table[SYS_UNLINK] = sys_unlink;syscall_table[SYS_MKDIR] = sys_mkdir;syscall_table[SYS_OPENDIR] = sys_opendir;syscall_table[SYS_CLOSEDIR] = sys_closedir;syscall_table[SYS_CHDIR] = sys_chdir;syscall_table[SYS_RMDIR] = sys_rmdir;syscall_table[SYS_READDIR] = sys_readdir;syscall_table[SYS_REWINDDIR] = sys_rewinddir;syscall_table[SYS_STAT] = sys_stat;syscall_table[SYS_PS] = sys_ps;syscall_table[SYS_EXECV] = sys_execv;syscall_table[SYS_EXIT] = sys_exit;syscall_table[SYS_WAIT] = sys_wait;syscall_table[SYS_PIPE] = sys_pipe;syscall_table[SYS_FD_REDIRECT] = sys_fd_redirect;syscall_table[SYS_HELP] = sys_help;put_str("syscall_init done\n");
}
八:硬盘驱动模块实现:(device/ide.c ide.h)
个人理解:首先,对硬盘的操作就是读(read),写(write)。那么我们手里应该有什么信息呢,或者说内核应该持有什么样的信息才能满足我们对操作硬盘的需要。首先我们要知道硬盘的结构,如下图所示:
其中每一个分区都有一个独立的文件系统,而每一个文件系统最重要的当然是元信息,这些元信息包括超级块,空闲块位图,inode位图,inode数组;而我们再内核中,只需要抽象出一个表示分区的数据结构即可(struct partiton),把这些信息用这个结构存起来,当要操作这个分区里的文件时,捏着这个数据结构便能解决大部分的事情。
原理:
数据结构:
/* 分区结构 */
struct partition {uint32_t start_lba; // 起始扇区uint32_t sec_cnt; // 扇区数struct disk* my_disk; // 分区所属的硬盘struct list_elem part_tag; // 用于队列中的标记char name[8]; // 分区名称struct super_block* sb; // 本分区的超级块struct bitmap block_bitmap; // 块位图struct bitmap inode_bitmap; // i结点位图struct list open_inodes; // 本分区打开的i结点队列
};
/* 硬盘结构 */
struct disk {char name[8]; // 本硬盘的名称,如sda等struct ide_channel* my_channel; // 此块硬盘归属于哪个ide通道uint8_t dev_no; // 本硬盘是主0还是从1struct partition prim_parts[4]; // 主分区顶多是4个struct partition logic_parts[8]; // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};/* ata通道结构 */
struct ide_channel {char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。uint16_t port_base; // 本通道的起始端口号uint8_t irq_no; // 本通道所用的中断号struct lock lock;bool expecting_intr; // 向硬盘发完命令后等待来自硬盘的中断struct semaphore disk_done; // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从
};
接口:
初始化:
个人理解:(注:读者在阅读该部分的时候需要先熟悉通道,硬盘这些数据结构的嵌套关系)首先,初始化硬盘在我看来就是准备好一切数据,为操作服务罢了。整个硬盘的初始化,就是调用partition_scan去扫描整个硬盘的主分区以及逻辑分区,然后初始化好表示通道的数据结构channel,以及初始化好链表partition_list(所有partiton都用整个链表关联起来,用到哪个分区就从这里获得,而不必再次去硬盘中去获得相应分区的信息)
九:文件系统的实现:(fs/*)
个人理解:在硬盘初始化好后,我们能够得到的信息是全局的channel结构,以及一个partition_list链表,其中包含了80hd.img中所有分区的信息。
那么文件系统初始化的工作就是将每一个分区的元信息填入到硬盘相应的分区上去,这些元信息包括:超级块,inode位图,块位图等。这项工作是由函数partition_format()来实现的。其次我们还要挂载默认的分区(本质就是将cur_part指向需要操作的partiton对象),挂载完成后,需要打开该分区的根目录,以便后续操作。
设计理论:
从全局上一览数据结构之间的关系:黄色代表初始化完成后内核持有的全局变量
原理:
数据结构:
struct file {uint32_t fd_pos; // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1uint32_t fd_flag;struct inode* fd_inode;
};
/* 目录结构 */
struct dir {struct inode* inode;uint32_t dir_pos; // 记录在目录内的偏移uint8_t dir_buf[512]; // 目录的数据缓存
};
/* 目录项结构 */
struct dir_entry {char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称uint32_t i_no; // 普通文件或目录对应的inode编号enum file_types f_type; // 文件类型
};
/* inode结构 */
struct inode {uint32_t i_no; // inode编号/* 当此inode是文件时,i_size是指文件大小,
若此inode是目录,i_size是指该目录下所有目录项大小之和*/uint32_t i_size;uint32_t i_open_cnts; // 记录此文件被打开的次数bool write_deny; // 写文件不能并行,进程写文件前检查此标识/* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */uint32_t i_sectors[13];struct list_elem inode_tag;
};
接口:
初始化:
十:shell的实现以及系统调用的进一步丰富:(shell/) (userprog/)