文章目录
专栏博客链接
相关查阅博客链接
本书中错误勘误
1、编写ide.c
的ide_init
函数 出现函数sprintf
本书中未定义这个函数 需要自己编写
2、编写ide.c
的busy_wait
函数 BIT_STAT_BSY
未定义过 根据上下文推测应该改成BIT_ALT_STAT_BSY
或者修改宏定义 下一行中的BIT_STAT_DRQ
也是
3、编写ide_init()
我在仔细看了这一章的关于的所有代码后 真的没找到对于struct list partition_list
的初始化代码 而这一错误还是我程序输出出现了问题 找了一个多小时才找到的
部分缩写熟知
yield
主动让出
idle
懈怠的
闲聊时刻
哈哈 不知不觉 已经来到了第十三章了啊 说点题外话 刚刚本来打算开始看书 结果被github
和git
吸走了注意 我忽然发现现在暑假还真的只有一个月了 感觉github
也应该是程序员必备的技能库之一啊 哈哈
我觉得还差最后的几章 写完之后 关于操作系统的学习之路也应该走的差不多了 后面我觉得我应该先把github
和git
搞懂 先把我们写的操作系统上传上去试试吧 哈哈 当然肯定是不会忘记这个地方的 CSDN
上面呢 我还是会照常的把所有代码在额外篇全部给出来 方便大家对比修改
其实我为什么坚持写这个系列 到现在还差最后的一点点就写完了呢 能坚持到现在 主要是觉得 这个系列应该之后还会被很多人看到 我相信很多人学习操作系统的时候 很多人会很懵
其实在学操作系统之前 我的寒假就在看CSAPP了 在此之前还看了汇编 其实对很多地方还是有个了解一二 在这样的情况下 我学操作系统都倍感吃力 甚至很多时候觉得是天马行空 只是朦朦胧胧有个概念在那里 但是感觉离我们太远了
我学习很多东西 尤其是计算机方面的东西 要是真的是这个样子 我其实是很崩溃的 于是各种找书来看 比如第一本入手的《现代操作系统》 之后还是觉得不得劲 倍感困难 转战哈工大操作系统 听言那里是理论+实战 哈哈 其实总得来说还可以 但是的话 还是不简单的 而且我觉得做完之后还是总感觉差了点什么 之后确实是没有办法了 才来做这个操作系统的
现在啊 其实真的对 进线程的切换啊 内核 用户态 锁 内存管理 分页分段这部分 我都能说出个一二 还能道出来个所以然来 因为上面的每一行代码都是我一个个敲上去的 基本上百分之九十八的代码我都是全部理解之后再敲上去的 很多还是按照我自己的想法敲上去的 这样子的情况下怎么可能不清楚是什么情况呢
哈哈 其实这种感觉还是相当不错的 感觉还是少说点废话了 昨天就是自己耽误了太多时间导致自己挺晚睡的 早做完早去吃晚饭 这部分就先告一段落 各位看官下面接着看~
提前需要准备编写的函数
这部分放到最前面来写吧
因为后面基本上和这个没什么关系 但是需要用到 考虑到连贯性和后面内容 就先提到最前面的准备区吧 哈哈 磨刀不误砍柴工
实现printk
其实在此之前书上面说的printk
是为了内核专用的函数printf
是为了用户进程专用的函数 我当时就寻思着 难道内核进程不能用printf
吗 还不是照用不误 但之后事后想了想 用户进程调用printf
不仅要通过中断陷入内核 还有一系列的push
pop
确实代价太高了
虽然内核不用切换tss
但是还是需要调用中断 切换上下文 代价还是挺大的 从节约资源的角度来说 我们还是实现一个吧 不费事
那代码来咯
路径lib/kernel/stdio-kernel.c
#include "stdio-kernel.h"
#include "stdint.h"
#include "stdio.h"
#include "console.h"
void printk(const char* format, ...)
{
va_list args;
va_start(args,format);
char buf[1024] = {0};
vsprintf(buf,format,args);
va_end(args);
console_put_str(buf);
}
路径lib/kernel/stdio-kernel.h
#ifndef __LIB__STDIO_KERNEL_H
#define __LIB__STDIO_KERNEL_H
void printk(const char* format, ...);
#endif
稍微修改一下makefile
OBJS 加了个 $(BUILD_DIR)/stdio-kernel.o
$(BUILD_DIR)/main.o 加了个 lib/kernel/stdio-kernel.h
$(BUILD_DIR)/stdio-kernel.o : lib/kernel/stdio-kernel.c lib/kernel/stdio-kernel.h \
lib/stdio.h device/console.h
$(CC) $(CFLAGS) $< -o $@
实现sprintf函数
书上没有写这部分 但是后面出现了这个函数 我们就需要自己写
刚开始还有点打脑壳 但是后面发现 哈哈 原理直接借用printf的就行了
修改文件的话还是修改stdio.c
哈哈 简不简单
uint32_t sprintf(char* _des,const char* format, ...)
{
va_list args;
uint32_t retval;
va_start(args,format); //args指向char* 的指针 方便指向下一个栈参数
retval = vsprintf(_des,format,args);
va_end(args);
return retval;
}
创建从盘
这部分呢 我还是倾情的把所有要打的代码下面打出来 图在下面
创建从盘的步骤
1、打开bochs文件夹 并打开终端
2、输入bin/bximage
进入下面图片界面
3、根据下面的引导一步步来就行了 版本不一样 输入的也不一样
4、根据我的版本如下图 依次输入1
回车默认
回车默认
80
hd80M.img
就ok了
5、接着你所打开的终端的目录下就出现了一个 hd80M.img
虚拟磁盘啦
修改后的bochsrc.disk
megs : 32
romimage: file=/home/cooiboi/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/cooiboi/bochs/share/bochs/VGABIOS-lgpl-latest
boot: disk
log: bochs.out
mouse:enabled=0
keyboard:keymap=/home/cooiboi/bochs/share/bochs/keymaps/x11-pc-us.map
ata0:enabled=1,ioaddr1=0x1f0,ioaddr2=0x3f0,irq=14
ata0-master: type=disk, path="hd60M.img", mode=flat,cylinders=121,heads=16,spt=63
ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63
#gdbstub:enabled=1,port=1234,text_base=0,data_base=0,bss_base=0
验证从盘安装成功
书上写的我们秉承拿来主义
那我们就用拿来主义来
我们安装的磁盘数在物理地址0x475
这是我们bios
做的事情 拿来用即可
发现现在由0x1
变到0x2
了 说明安装成功了 继续走起
创建磁盘分区表
在编辑博客在此之前 打了一个小时的游戏 感觉越打戾气越大 哎呀 简直人麻惨了 到最后打赢一把心里不舒服 打输了更不舒服 整挺好 确实戒网瘾
回到正题 哈哈 里面的文字就自己看看吧 感觉整理的还是挺好的 没什么多说的 下面一步步来
创建分区表步骤
1、开始键入fdisk ./hd80M.img
2、键入m
得到命令菜单
3、键入x
得到专家命令菜单
4、键入c
设置柱面 输入162
5、键入h
设置磁头 输入16
6、键入r
返回主菜单
7、键入n
创建分区
8、键入p
输入主扇区
9、键入1
开始创建第一个 之后由于bochs
版本不一样 我这边要求设置开始的扇区号 哈哈 而且扇区不能要求挨着 那没办法 后面也就都随行发挥了
10、紧紧跟着t
修改分区id 都改成0x66
即可 下面就是我扇区最后得到的 记得最后输入w
改写到磁盘上
下面的应该不会影响后面的程序 哈哈
实现硬盘驱动读入的思路
其实我觉得这里还是要写一写这一章究竟在干什么 其实就是提供了几个接口 有接口是读取硬盘扇区内容到指定地点 有的是指定硬盘扇区开始位置从我们指定的缓冲区读入 总而言之就是提供了几个接口
但在这里不是从这里说这方面的东西
我从上而下来写一些 这部分大致是怎么实现的
分区
首先先谈分区吧 我们刚开始的工作就是给从盘hd80M.img
让其分区管理 创建了分区表 分区表在哪里呢 除了硬盘中的第一个扇区的是MBR
后面的一个就是引导扇区EBR
分区表就在那里
EBR
的格式呢 兼容前面446
字节是引导内容 后面的64字节
是4条分区表项 每一项是16
字节 最后的2
字节是魔数 规定扇区最后的识别码 先暂时不说这个
既然分区表项的字节规定了 每一字节的位置是什么内容其实也是固定了的那我们怎么使用拓展分区呢 我们通过起始的lba位置+得到分区的ext_lba 就可以得到下一个分区的分区表信息 每个分区表都能装4个表项 其实这样就能起到无穷的拓展的效果了
硬盘的读入和写入
对于相关硬件的 寄存器读入和写入部分
其实在后面的代码 凡是关于硬件相关硬性规定的读入和写入的那种操作
我都是选择稍微看一看带过即可 并没有自己再去推一遍 研究研究 但是对于管理和写入的策略 流程和究竟是怎么写入的 每一行代码我都是仔细研究过 并且一个个敲上去的 所以觉得写这部分还是挺重要的
先不说策略吧 我们通过向硬盘指定的寄存器写入命令 然后再想指定的硬盘指定的窗口写入和读入的 哈哈 其实就是这么简单 但是还是展开来说一下吧
从硬盘读取内容到内存的流程如下
1、我们先向硬盘指定寄存器发送命令
2、等待硬盘把信息准备好(放到硬盘所在缓冲区中)
3、在等待硬盘把信息准备好期间还是有一定的时间 在期间硬盘准备好的时候 我们可以先阻塞自己的线程 因为cpu
时间珍贵 等到硬盘准备好的时候会发送中断(我们提前把硬盘引发的中断号注册了我们自己的中断处理函数) 我们再把我们之前阻塞的线程给唤醒 我们做一个事情的是交给了信号量 哈哈 没想到还有这个用
4、唤醒后继续做自己的事情了 在检测可读后 我们开始读取硬盘中的信息 但是注意 我们这个操作系统读取信息的时候 我们cpu
是无法抽开身的 一直监督着执行重复的outsw
的命令 没有实现用DMA
如果用DMA
的话 我们的cpu
就可以解放自己了 就这样一直读取
5、到最后任务结束 信息就被读到缓冲区了
总的流程就是上面那个样子了 只介绍了硬盘读取内容到内存 不用介绍缓冲区把内容放到硬盘吧 大同小异 在向硬盘的放入内容的时候cpu
也是一直在旁边督公 没办法跑的 跑的时候只是向硬盘写入命令 等待其准备好的时候才能跑
策略呢 好像上面都已经把阻塞自己的核心策略给说了 感觉核心的地方我都说的差不多了 哈哈 其实在通晓了原理之后 在写代码是一个很简单的事情 你只是把你的想法用代码的形式表示出来而已
哈哈 下面这段话是我在看完代码之后迫不及待记下来的话 各位看官看个乐即可 看完下面的话之后 代码就上菜咯
原来之前看《现代操作系统》上面说的 在读取硬盘上面的文件时 cpu可以腾出手来 去做其他的事情
哈哈 那个时候我只能凭空想象 现在一步步流程变成了代码 在我眼前鲜活的跳跃着
我的心情可想而知 就按照上一次我这么喜悦的时候是在搞懂分页页表的那个地方 说的一句话
此时此刻 真想来一个热情桑巴 哈哈哈 现在想起来自己的说法确实好搞笑哈 哈哈
但是做到后面 我发现我原来有个意识现在才逐渐意识过来 就是 硬盘也是需要大量时间去准备数据的
这个时间也是一个不可忽视的时间
而且其实 我们这个操作系统 采取的读磁盘 在把磁盘读取到内存的时候 我们cpu是没有休息的 一直在执行命令
唯一休息的时候就是在磁盘在读取到命令 准备的时候 那个时候我们才释放了cpu
编写硬盘驱动程序
修改后的thread.c
#include "thread.h" //函数声明 各种结构体
#include "stdint.h" //前缀
#include "string.h" //memset
#include "global.h" //不清楚
#include "memory.h" //分配页需要
#include "debug.h"
#include "interrupt.h"
#include "print.h"
#include "../userprog/process.h"
#include "../thread/sync.h"
struct task_struct* main_thread; //主线程main_thread的pcb
struct task_struct* idle_thread; //休眠线程
struct list thread_ready_list; //就绪队列
struct list thread_all_list; //总线程队列
struct lock pid_lock;
extern void switch_to(struct task_struct* cur,struct task_struct* next);
// 获取 pcb 指针
// 这部分我可以来稍微解释一下
// 我们线程所在的esp 肯定是在 我们get得到的那一页内存 pcb页上下浮动 但是我们的pcb的最起始位置是整数的 除去后面的12位
// 那么我们对前面的取 & 则可以得到 我们的地址所在地
pid_t allocate_pid(void)
{
static pid_t next_pid = 0; //约等于全局变量 全局性+可修改性
lock_acquire(&pid_lock);
++next_pid;
lock_release(&pid_lock);
return next_pid;
}
struct task_struct* running_thread(void)
{
uint32_t esp;
asm ("mov %%esp,%0" : "=g"(esp));
return (struct task_struct*)(esp & 0xfffff000);
}
void kernel_thread(thread_func* function,void* func_arg)
{
intr_enable(); //开中断 防止后面的时间中断被屏蔽无法切换线程
function(func_arg);
}
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg)
{
pthread->self_kstack -= sizeof(struct intr_stack); //减去中断栈的空间
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread; //地址为kernel_thread 由kernel_thread 执行function
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->ebx = kthread_stack->esi = 0; //初始化一下
return;
}
void init_thread(struct task_struct* pthread,char* name,int prio)
{
memset(pthread,0,sizeof(*pthread)); //pcb位置清0
strcpy(pthread->name,name);
if(pthread == main_thread)
pthread->status = TASK_RUNNING; //我们的主线程肯定是在运行的
else
pthread->status = TASK_READY; //放到就绪队列里面
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //刚开始的位置是最低位置 栈顶位置+一页得最顶部
//后面还要对这个值进行修改
pthread->pid = allocate_pid(); //提前分配pid
pthread->priority = prio;
pthread->ticks = prio; //和特权级 相同的时间片
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL; //线程没有单独的地址
pthread->stack_magic = 0x23333333; //设置的魔数 检测是否越界限
}
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg)
{
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread,name,prio);
thread_create(thread,function,func_arg);
ASSERT(!elem_find(&thread_ready_list,&thread->general_tag)); //之前不应该在就绪队列里面
list_append(&thread_ready_list,&thread->general_tag);
ASSERT(!elem_find(&thread_all_list,&thread->all_list_tag));
list_append(&thread_all_list,&thread->all_list_tag);
return thread;
}
//之前在loader.S的时候已经 mov esp,0xc0009f00
//现在的esp已经就在预留的pcb位置上了
void make_main_thread(void)
{
main_thread = running_thread(); //得到main_thread 的pcb指针
init_thread(main_thread,"main",31);
ASSERT(!elem_find(&thread_all_list,&main_thread->all_list_tag));
list_append(&thread_all_list,&main_thread->all_list_tag);
}
void schedule(void)
{
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread(); //得到当前pcb的地址
if(cur->status == TASK_RUNNING)
{
ASSERT(!elem_find(&thread_ready_list,&cur->general_tag)); //目前在运行的肯定ready_list是不在的
list_append(&thread_ready_list,&cur->general_tag); //加入尾部
cur->status = TASK_READY;
cur->ticks = cur->priority;
}
else
{}
if(list_empty(&thread_ready_list))
thread_unblock(idle_thread);
struct task_struct* thread_tag = list_pop(&thread_ready_list);
//书上面的有点难理解 代码我写了一个我能理解的
struct task_struct* next = (struct task_struct*)((uint32_t)thread_tag & 0xfffff000);
next->status = TASK_RUNNING;
process_activate(next);
switch_to(cur,next); //esp头顶的是 返回地址 +12是next +8是cur
}
void thread_block(enum task_status stat)
{
//设置block状态的参数必须是下面三个以下的
ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || stat == TASK_HANGING));
enum intr_status old_status = intr_disable(); //关中断
struct task_struct* cur_thread = running_thread();
cur_thread->status = stat; //把状态重新设置
//调度器切换其他进程了 而且由于status不是running 不会再被放到就绪队列中
schedule();
//被切换回来之后再进行的指令了
intr_set_status(old_status);
}
//由锁拥有者来执行的 善良者把原来自我阻塞的线程重新放到队列中
void thread_unblock(struct task_struct* pthread)
{
enum intr_status old_status = intr_disable();
ASSERT(((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING)));
if(pthread->status != TASK_READY)
{
//被阻塞线程 不应该存在于就绪队列中)
ASSERT(!elem_find(&thread_ready_list,&pthread->general_tag));
if(elem_find(&thread_ready_list,&pthread->general_tag))
PANIC("thread_unblock: blocked thread in ready_list\n"); //debug.h中定义过
//让阻塞了很久的任务放在就绪队列最前面
list_push(&thread_ready_list,&pthread->general_tag);
//状态改为就绪态
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}
void idle(void)
{
while(1)
{
thread_block(TASK_BLOCKED); //先阻塞后 被唤醒之后即通过命令hlt 使cpu挂起 直到外部中断cpu恢复
asm volatile ("sti;hlt" : : :"memory");
}
}
void thread_yield(void)
{
struct task_struct* cur = running_thread();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list,&cur->general_tag));
list_append(&thread_ready_list,&cur->general_tag); //放到就绪队列末尾
cur->status = TASK_READY; //状态设置为READY 可被调度
schedule();
intr_set_status(old_status);
}
void thread_init(void)
{
put_str("thread_init start!\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
make_main_thread();
idle_thread = thread_start("idle",10,idle,NULL); //创建休眠进程
put_str("thread_init done!\n");
}
修改后的thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"
#include "../kernel/memory.h"
#define PG_SIZE 4096
typedef int16_t pid_t;
extern struct list thread_ready_list,thread_all_list;
typedef void thread_func(void*); //这里有点不懂定义的什么意思 搜了搜博客 发现是函数声明
enum task_status
{
TASK_RUNNING, // 0
TASK_READY, // 1
TASK_BLOCKED, // 2
TASK_WAITING, // 3
TASK_HANGING, // 4
TASK_DIED // 5
};
/* intr_stack 用于处理中断被切换的上下文环境储存 */
/* 这里我又去查了一下 为什么是反着的 越在后面的参数 地址越高 */
struct intr_stack
{
uint32_t vec_no; //中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
uint32_t err_code;
void (*eip) (void); //这里声明了一个函数指针
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/* 线程栈 保护线程环境 */
struct thread_stack
{
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
void (*eip) (thread_func* func,void* func_arg); //和下面的相互照应 以ret 汇编代码进入kernel_thread函数调用
void (*unused_retaddr); //占位数 在栈顶站住了返回地址的位置 因为是汇编ret
thread_func* function; //进入kernel_thread要调用的函数地址
void* func_arg; //参数指针
};
struct task_struct
{
uint32_t* self_kstack; //pcb中的 kernel_stack 内核栈
pid_t pid;
enum task_status status; //线程状态
uint8_t priority; //特权级
uint8_t ticks; //在cpu 运行的滴答数 看ticks 来判断是否用完了时间片
uint32_t elapsed_ticks; //一共执行了多久
char name[16];
struct list_elem general_tag; //就绪队列中的连接节点
struct list_elem all_list_tag; //总队列的连接节点
uint32_t* pgdir; //进程自己页表的虚拟地址 线程没有
struct virtual_addr userprog_vaddr; //用户进程的虚拟空间
struct mem_block_desc u_block_desc[DESC_CNT]; //内存块描述符
uint32_t stack_magic; //越界检查 因为我们pcb上面的就是我们要用的栈了 到时候还要越界检查
};
struct task_struct* running_thread(void);
void kernel_thread(thread_func* function,void* func_arg);
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);
void init_thread(struct task_struct* pthread,char* name,int prio);
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);
void make_main_thread(void);
void schedule(void);
void thread_init(void);
void thread_block(enum task_status stat);
void thread_unblock(struct task_struct* pthread);
void idle(void);
void thread_yield(void);
#endif
修改后的timer.c
#include "timer.h"
#include "io.h"
#include "print.h"
#include "../kernel/interrupt.h"
#include "../thread/thread.h"
#include "debug.h"
#include "global.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define COUNTER0_PORT 0X40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_COUNTROL_PORT 0x43
#define mil_second_per_init 1000 / IRQ0_FREQUENCY
//自中断开启以来总的滴答数
uint32_t ticks;
void frequency_set(uint8_t counter_port ,uint8_t counter_no,uint8_t rwl,uint8_t counter_mode,uint16_t counter_value)
{
outb(PIT_COUNTROL_PORT,(uint8_t) (counter_no << 6 | rwl << 4 | counter_mode << 1));
outb(counter_port,(uint8_t)counter_value);
outb(counter_port,(uint8_t)counter_value >> 8);
return;
}
void intr_timer_handler(void)
{
struct task_struct* cur_thread = running_thread(); //得到pcb指针
ASSERT(cur_thread->stack_magic == 0x23333333); //检测栈是否溢出
++ticks;
++cur_thread->elapsed_ticks;
if(!cur_thread->ticks)
{
schedule();
}
else
--cur_thread->ticks;
return;
}
void timer_init(void)
{
put_str("timer_init start!\n");
frequency_set(COUNTER0_PORT,COUNTER0_NO,READ_WRITE_LATCH,COUNTER_MODE,COUNTER0_VALUE);
register_handler(0x20,intr_timer_handler); //注册时间中断函数 0x20向量号函数更换
put_str("timer_init done!\n");
return;
}
//休息n个时间中断期
void ticks_to_sleep(uint32_t sleep_ticks)
{
uint32_t start_tick = ticks;
while(ticks - start_tick < sleep_ticks)
thread_yield();
}
//毫秒为单位 通过毫秒的中断数来调用ticks_to_sleep 来达到休息毫秒的作用
void mtime_sleep(uint32_t m_seconds)
{
uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds,mil_second_per_init);
ASSERT(sleep_ticks > 0);
ticks_to_sleep(sleep_ticks);
}
修改后的timer.h
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "stdint.h"
void frequency_set(uint8_t counter_port ,uint8_t counter_no,uint8_t rwl,uint8_t counter_mode,uint16_t counter_value);
void intr_timer_handler(void);
void timer_init(void);
void mtime_sleep(uint32_t m_seconds);
void ticks_to_sleep(uint32_t sleep_ticks);
#endif
修改后的interrupt.c
这里只贴修改的函数位置嗷
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
outb (PIC_M_DATA, 0xf8);
outb (PIC_S_DATA, 0xbf);
put_str(" pic_init done\n");
}
编写完的ide.c
路径device/ide.c
#include "ide.h"
#include "stdint.h"
#include "debug.h"
#include "stdio-kernel.h"
#include "stdio.h"
#include "global.h"
#include "../thread/sync.h"
#include "io.h"
#include "timer.h"
#include "interrupt.h"
#include "memory.h"
#define reg_data(channel) (channel->port_base + 0)
#define reg_error(channel) (channel->port_base + 1)
#define reg_sect_cnt(channel) (channel->port_base + 2)
#define reg_lba_l(channel) (channel->port_base + 3)
#define reg_lba_m(channel) (channel->port_base + 4)
#define reg_lba_h(channel) (channel->port_base + 5)
#define reg_dev(channel) (channel->port_base + 6)
#define reg_status(channel) (channel->port_base + 7)
#define reg_cmd(channel) (reg_status(channel))
#define reg_alt_status(channel) (channel->port_base + 0x206)
#define reg_ctl(channel) reg_alt_status(channel)
#define BIT_STAT_BSY 0X80 //硬盘忙
#define BIT_STAT_DRDY 0X40 //驱动器准备好啦
#define BIT_STAT_DRQ 0x8 //数据传输准备好了
#define BIT_DEV_MBS 0XA0
#define BIT_DEV_LBA 0X40
#define BIT_DEV_DEV 0X10
#define CMD_IDENTIFY 0XEC //identify指令
#define CMD_READ_SECTOR 0X20 //读扇区指令
#define CMD_WRITE_SECTOR 0X30 //写扇区指令
#define max_lba ((80*1024*1024/512) - 1) //调试用
uint8_t channel_cnt; //通道数
struct ide_channel channels[2]; //两个ide通道
int32_t ext_lba_base = 0; //记录总拓展分区lba 初始为0
uint8_t p_no = 0,l_no = 0; //记录硬盘主分区下标 逻辑分区下标
struct list partition_list; //分区队列
//选择读写的硬盘
void select_disk(struct disk* hd)
{
uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
if(hd->dev_no == 1) //主盘0 从盘1
reg_device |= BIT_DEV_DEV;
outb(reg_dev(hd->my_channel),reg_device);
}
//向硬盘控制器写入起始扇区和读写的扇区数
void select_sector(struct disk* hd,uint32_t lba,uint8_t sec_cnt)
{
ASSERT(lba <= max_lba);
struct ide_channel* channel = hd->my_channel;
outb(reg_sect_cnt(channel),sec_cnt);
outb(reg_lba_l(channel),lba);
outb(reg_lba_m(channel),lba>>8);
outb(reg_lba_h(channel),lba>>16);
outb(reg_dev(channel),BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24);
}
//向通道channel发命令cmd
void cmd_out(struct ide_channel* channel,uint8_t cmd)
{
channel->expecting_intr = true;
outb(reg_cmd(channel),cmd);
}
//从扇区读数据
void read_from_sector(struct disk* hd,void* buf,uint8_t sec_cnt)
{
uint32_t size_in_byte;
if(sec_cnt == 0)
size_in_byte = 256*512;
else
size_in_byte = sec_cnt*512;
insw(reg_data(hd->my_channel),buf,size_in_byte/2); //读如数据到buf
}
//将 sec_cnt 扇区数据写入硬盘
void write2sector(struct disk* hd,void* buf,uint8_t sec_cnt)
{
uint32_t size_in_byte;
if(sec_cnt == 0) size_in_byte = 256*512;
else size_in_byte = sec_cnt * 512;
outsw(reg_data(hd->my_channel),buf,size_in_byte/2);
}
//最多等待三十秒 任务应该都在30秒内完成 读取硬盘相应给了整整30秒
bool busy_wait(struct disk* hd)
{
struct ide_channel* channel = hd->my_channel;
uint16_t time_limit = 30 * 1000;
while(time_limit -= 10 >= 0)
{
if(!(inb(reg_status(channel)) & BIT_STAT_BSY))
return (inb(reg_status(channel)) & BIT_STAT_DRQ);
else mtime_sleep(10);
}
return false;
}
//从硬盘读取sec_cnt扇区到buf
void ide_read(struct disk* hd,uint32_t lba,void* buf,uint32_t sec_cnt)
{
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire(&hd->my_channel->lock);
select_disk(hd);
uint32_t secs_op;
uint32_t secs_done = 0;
while(secs_done < sec_cnt)
{
if((secs_done + 256) <= sec_cnt) secs_op = 256;
else secs_op = sec_cnt - secs_done;
select_sector(hd,lba + secs_done, secs_op);
cmd_out(hd->my_channel,CMD_READ_SECTOR); //执行命令
/*在硬盘开始工作时 阻塞自己 完成读操作后唤醒自己*/
sema_down(&hd->my_channel->disk_done);
/*检测是否可读*/
if(!busy_wait(hd))
{
char error[64];
sprintf(error,"%s read sector %d failed!!!!\n",hd->name,lba);
PANIC(error);
}
read_from_sector(hd,(void*)((uint32_t)buf +secs_done * 512),secs_op);
secs_done += secs_op;
}
lock_release(&hd->my_channel->lock);
}
void ide_write(struct disk* hd,uint32_t lba,void* buf,uint32_t sec_cnt)
{
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire(&hd->my_channel->lock);
select_disk(hd);
uint32_t secs_op;
uint32_t secs_done = 0;
while(secs_done < sec_cnt)
{
if((secs_done + 256) <= sec_cnt) secs_op = 256;
else secs_op = sec_cnt - secs_done;
select_sector(hd,lba+secs_done,secs_op);
cmd_out(hd->my_channel,CMD_WRITE_SECTOR);
if(!busy_wait(hd))
{
char error[64];
sprintf(error,"%s write sector %d failed!!!!!!\n",hd->name,lba);
PANIC(error);
}
write2sector(hd,(void*)((uint32_t)buf + secs_done * 512),secs_op);
//硬盘响应期间阻塞
sema_down(&hd->my_channel->disk_done);
secs_done += secs_op;
}
lock_release(&hd->my_channel->lock);
}
//硬盘结束任务中断程序
void intr_hd_handler(uint8_t irq_no)
{
ASSERT(irq_no == 0x2e || irq_no == 0x2f);
uint8_t ch_no = irq_no - 0x20 - 0xe;
struct ide_channel* channel = &channels[ch_no];
ASSERT(channel->irq_no == irq_no);
if(channel->expecting_intr)
{
channel->expecting_intr = false;//结束任务了
sema_up(&channel->disk_done);
inb(reg_status(channel));
}
}
//将dst中len个相邻字节交换位置存入buf 因为读入的时候字节顺序是反的 所以我们再反一次即可
void swap_pairs_bytes(const char* dst,char* buf,uint32_t len)
{
uint8_t idx;
for(idx = 0;idx < len;idx += 2)
{
buf[idx+1] = *(dst++);
buf[idx] = *(dst++);
}
}
void partition_scan(struct disk* hd,uint32_t ext_lba)
{
struct boot_sector* bs = sys_malloc(sizeof(struct boot_sector));
ide_read(hd,ext_lba,bs,1);
uint8_t part_idx = 0;
struct partition_table_entry* p = bs->partition_table; //p为分区表开始的位置
while((part_idx++) < 4)
{
if(p->fs_type == 0x5) //拓展分区
{
if(ext_lba_base != 0)
{
partition_scan(hd,p->start_lba + ext_lba_base); //继续递归转到下一个逻辑分区再次得到表
}
else //第一次读取引导块
{
ext_lba_base = p->start_lba;
partition_scan(hd,ext_lba_base);
}
}
else if(p->fs_type != 0)
{
if(ext_lba == 0) //主分区
{
hd->prim_parts[p_no].start_lba = ext_lba + p->start_lba;
hd->prim_parts[p_no].sec_cnt = p->sec_cnt;
hd->prim_parts[p_no].my_disk = hd;
list_append(&partition_list,&hd->prim_parts[p_no].part_tag);
sprintf(hd->prim_parts[p_no].name,"%s%d",hd->name,p_no+1);
p_no++;
ASSERT(p_no<4); //0 1 2 3 最多四个
}
else //其他分区
{
hd->logic_parts[l_no].start_lba = ext_lba + p->start_lba;
hd->logic_parts[l_no].sec_cnt = p->sec_cnt;
hd->logic_parts[l_no].my_disk = hd;
list_append(&partition_list,&hd->logic_parts[l_no].part_tag);
sprintf(hd->logic_parts[l_no].name,"%s%d",hd->name,l_no+5); //从5开始
l_no++;
if(l_no >= 8) return; //只支持8个
}
}
++p;
}
sys_free(bs);
}
bool partition_info(struct list_elem* pelem,int arg)
{
struct partition* part = elem2entry(struct partition,part_tag,pelem);
printk(" %s start_lba:0x%x,sec_cnt:0x%x\n",part->name,part->start_lba,part->sec_cnt);
return false; //list_pop完
}
void identify_disk(struct disk* hd)
{
char id_info[512];
select_disk(hd);
cmd_out(hd->my_channel,CMD_IDENTIFY);
if(!busy_wait(hd))
{
char error[64];
sprintf(error,"%s identify failed!!!!!!\n");
PANIC(error);
}
read_from_sector(hd,id_info,1);//现在硬盘已经把硬盘的参数准备好了了 我们把参数读到自己的缓冲区中
char buf[64] = {0};
uint8_t sn_start = 10 * 2,sn_len = 20,md_start = 27*2,md_len = 40;
swap_pairs_bytes(&id_info[sn_start],buf,sn_len);
printk(" disk %s info: SN: %s\n",hd->name,buf);
swap_pairs_bytes(&id_info[md_start],buf,md_len);
printk(" MODULE: %s\n",buf);
uint32_t sectors = *(uint32_t*)&id_info[60*2];
printk(" SECTORS: %d\n",sectors);
printk(" CAPACITY: %dMB\n",sectors * 512 / 1024 / 1024);
}
//初始化硬盘数据
void ide_init(void)
{
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); //获取硬盘数量
ASSERT(hd_cnt > 0);
channel_cnt = DIV_ROUND_UP(hd_cnt,2); //两个硬盘一个ide 通过硬盘推ide
ASSERT(channel_cnt > 0);
struct ide_channel* channel;
uint8_t channel_no = 0,dev_no = 0;
list_init(&partition_list);
while(channel_no < channel_cnt)
{
channel = &channels[channel_no];
sprintf(channel->name,"ide%d",channel_no);
switch(channel_no)
{
case 0:
channel->port_base = 0x1f0; //ide0 起始端口号0x1f0
channel->irq_no = 0x2e; //8259a 中断引脚
break;
case 1:
channel->port_base = 0x170; //ide1 起始端口号0x170
channel->irq_no = 0x2f;
break;
}
register_handler(channel->irq_no,intr_hd_handler);
channel->expecting_intr = false; //不期待中断
lock_init(&channel->lock);
sema_init(&channel->disk_done,0);
while(dev_no < 2)
{
struct disk* hd = &channel->devices[dev_no];
hd->my_channel = channel;
hd->dev_no = dev_no;
sprintf(hd->name,"sd%c",'a' + channel_no * 2 + dev_no);
identify_disk(hd);
if(dev_no != 0)
partition_scan(hd,0);
p_no = 0,l_no = 0;
dev_no++;
}
dev_no = 0;
channel_no++;
}
printk("\n all partition info\n");
list_traversal(&partition_list,partition_info,(int)NULL);
printk("ide_init done\n");
}
编写完的ide.h
路径ide.h
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H
#include "stdint.h"
#include "bitmap.h"
#include "list.h"
#include "../thread/sync.h"
#include "list.h"
//分区结构
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; //本分区打开
};
struct partition_table_entry
{
uint8_t bootable; //是否可引导
uint8_t start_head; //开始磁头号
uint8_t start_sec; //开始扇区号
uint8_t start_chs; //起始柱面号
uint8_t fs_type; //分区类型
uint8_t end_head; //结束磁头号
uint8_t end_sec; //结束扇区号
uint8_t end_chs; //结束柱面号
uint32_t start_lba; //本分区起始的lba地址
uint32_t sec_cnt; //本扇区数目
} __attribute__ ((packed));
struct boot_sector
{
uint8_t other[446]; //446 + 64 + 2 446是拿来占位置的
struct partition_table_entry partition_table[4]; //分区表中4项 64字节
uint16_t signature; //最后的标识标志 魔数0x55 0xaa
} __attribute__ ((packed));
//硬盘
struct disk
{
char name[8]; //本硬盘的名称
struct ide_channel* my_channel; //这块硬盘归属于哪个ide通道
uint8_t dev_no; //0表示主盘 1表示从盘
struct partition prim_parts[4]; //主分区顶多是4个
struct partition logic_parts[8]; //逻辑分区最多支持8个
};
// ata 通道结构
struct ide_channel
{
char name[8]; //ata通道名称
uint16_t port_base; //本通道的起始端口号
uint8_t irq_no; //本通道所用的中断号
struct lock lock; //通道锁 一个硬盘一通道 不能同时
bool expecting_intr; //期待硬盘中断的bool
struct semaphore disk_done; //用于阻塞 唤醒驱动程序 和锁不一样 把自己阻塞后 把cpu腾出来
struct disk devices[2]; //一通道2硬盘 1主1从
};
void ide_init(void);
void select_disk(struct disk* hd);
void select_sector(struct disk* hd,uint32_t lba,uint8_t sec_cnt);
void cmd_out(struct ide_channel* channel,uint8_t cmd);
void read_from_sector(struct disk* hd,void* buf,uint8_t sec_cnt);
void write2sector(struct disk* hd,void* buf,uint8_t sec_cnt);
bool busy_wait(struct disk* hd);
void ide_read(struct disk* hd,uint32_t lba,void* buf,uint32_t sec_cnt);
void ide_write(struct disk* hd,uint32_t lba,void* buf,uint32_t sec_cnt);
void intr_hd_handler(uint8_t irq_no);
void swap_pairs_bytes(const char* dst,char* buf,uint32_t len);
void identify_disk(struct disk* fd);
void partition_scan(struct disk* hd,uint32_t ext_lba);
bool partition_info(struct list_elem* pelem,int arg);
#endif
修改后的makefile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/switch.o \
$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \
$(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o $(BUILD_DIR)/keyboard.o \
$(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o $(BUILD_DIR)/process.o \
$(BUILD_DIR)/syscall-init.o $(BUILD_DIR)/syscall.o $(BUILD_DIR)/stdio.o \
$(BUILD_DIR)/stdio-kernel.o $(BUILD_DIR)/ide.o
############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h lib/string.h kernel/memory.h \
thread/thread.h kernel/interrupt.h device/console.h \
device/keyboard.h device/ioqueue.h userprog/process.h \
lib/user/syscall.h userprog/syscall-init.h lib/stdio.h \
lib/kernel/stdio-kernel.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/stdint.h kernel/interrupt.h device/timer.h kernel/memory.h \
thread/thread.h device/console.h device/keyboard.h userprog/tss.h \
userprog/syscall-init.h device/ide.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h \
kernel/kernel.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/io.h lib/kernel/print.h \
kernel/interrupt.h thread/thread.h kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/debug.h kernel/global.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h kernel/global.h \
thread/sync.h thread/thread.h lib/kernel/list.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h kernel/global.h \
lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \
kernel/debug.h kernel/interrupt.h lib/kernel/print.h \
userprog/process.h thread/sync.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
kernel/interrupt.h lib/stdint.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \
lib/stdint.h thread/thread.h kernel/debug.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/console.o: device/console.c device/console.h \
lib/kernel/print.h thread/sync.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \
lib/kernel/print.h lib/kernel/io.h kernel/interrupt.h \
kernel/global.h lib/stdint.h device/ioqueue.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \
kernel/interrupt.h kernel/global.h kernel/debug.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \
kernel/global.h thread/thread.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/process.o: userprog/process.c userprog/process.h \
lib/string.h kernel/global.h kernel/memory.h lib/kernel/print.h \
thread/thread.h kernel/interrupt.h kernel/debug.h device/console.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/syscall-init.o: userprog/syscall-init.c userprog/syscall-init.h \
lib/user/syscall.h lib/stdint.h lib/kernel/print.h kernel/interrupt.h thread/thread.h \
kernel/memory.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/syscall.o: lib/user/syscall.c lib/user/syscall.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/stdio.o: lib/stdio.c lib/stdio.h lib/stdint.h lib/string.h lib/user/syscall.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/stdio-kernel.o: lib/kernel/stdio-kernel.c lib/kernel/stdio-kernel.h \
lib/stdio.h device/console.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/ide.o: device/ide.c device/ide.h lib/stdint.h kernel/debug.h \
lib/kernel/stdio-kernel.h lib/stdio.h kernel/global.h thread/sync.h \
lib/kernel/io.h device/timer.h kernel/interrupt.h lib/kernel/list.h
$(CC) $(CFLAGS) $< -o $@
############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/switch.o: thread/switch.S
$(AS) $(ASFLAGS) $< -o $@
############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/cooiboi/bochs/hd60M.img \
bs=512 count=200 seek=9 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
make all 验收成果
哈哈 其实每次写到这里都想笑 因为基本上没有几次是一次就把代码写对了的 每次都要修改很久很久 这篇博客是我昨天开始打算写的 代码我记得从9点写完 一直改到了12:30 澡都没有洗 终于把所有的错误改对了 真的不容易啊
不理解书上面的原理 连最基本的跟踪调试也不会 更不会知道哪里出错了 哈哈 事实就是这样的 一步步跟踪 一步步卡点 一个个函数尝试 输出尝试无数次 while(1);
卡点 路得一步步走啊 不说那么多了 第十三章也快结束了 那么开心的时刻就不说原来的辛酸史了 哈哈
调试了那么久终于出来了 我还是很欣慰的 那就 下一章见啦!