太累了,不想好好写了,就发个草稿了
前言:操作系统系列实验的blog比较抽象,如有需要可以直接去搬我上传的项目直接用
毕竟这几篇都是汇报检查是我打的草稿,内容很乱
链接如下:
可运行项目文件直达
geekos
键盘驱动
1 | //keyboard.c |
屏幕驱动?
1 | /* |
主函数
1 | // main.c |
参考:
7.5在屏幕上显示键盘输入
我们趁热打铁,完成GeekOS 的第一个项目。projectO 要求创建一个内核线程响应按键操作,并 将输入的字符显示到屏幕上,当遇到Ctrl+d时结束线程的运行。
7.5.1 实现路径
创建内核线程我们在第六章分析过了,调用函数 Start_Kernel_Thread 即可开始一个新的内核线 程。该函数的参数Thread_Start_Func startFunc就是我们要实现的显示键盘输入的函数。在kthread.h 中找到该参数类型的定义
82 typedef void (*Thread_Start_Func)( ulong_t arg);
原来Thread_Start_Func类型是一个函数指针,指向一个无返回值,带一个类型为ulong_t参数的 函数。所以我们实现的函数需要无返回值,带一个类型为ulong_t参数。
既然是接受键盘输入,那就先看看 GeekOS 给我们提供了哪些键盘接口。位 于./src/geekos/keyboard.h。
122 /*Public functions /
125 void Init_Keyboard(void);
126 bool Read_Key(Keycodekeycode);
Keycode Wait_For_Key(void );
Init_Keyboard在7.4节已经分析过了,我们来看看剩下两个。位于./src/geekos/keyboard.c。
bool Read_Key(Keycode*keycode)
{
bool result,iflag;
iflag =Begin_Int_Atomic(); result =!Is_Queue_Empty(); if (result){
*keycode =Dequeue_Keycode();
}
End_Int_Atomic(iflag);
内部资料请勿外传
302 return result;
303 }
Read_Key 负责从缓冲区队列中取出一个keycode。
310 Keycode Wait_For_Key(void ) 311 {
312 bool gotKey,iflag;
313 Keycode keycode =KEY_UNKNOWN;
315 iflag =Begin_Int_Atomic();
317 do {
318 gotKey =!Is_Queue_Empty();
319 if (gotKey)
320 keycode =Dequeue_Keycode();
321 else
322 Wait(&S_waitQueue); 323 }
324 while (!gotKey);
326 End_Int_Atomic(iflag);
328 return keycode; 329 }
注意,键盘缓冲区属于临界区,所以使用关中断来保护。
Wait_For_Key 也是从缓冲区取数据,如果gotKey为1,说明键盘缓冲区不空,则从缓冲区出一个 键盘码。若gotKey为0则调用Wait将线程置于等待队列s_waitQueue中。7.4节中已知在键盘中断函 数中会唤醒此等待队列。
综合比较这两个接口,明显Wait_For_Key更适合我们的任务。我们的线程只需要调用Wait_For_Key 获取 keycode,再显示出来即可。当没有键盘输入的时候,线程自动进入等待队列,当有键盘操作时, 线程被自动唤醒。
由7.4节可知,keycode是16位无符号数,其高6位是标志位,低8位是按键对应的键码,我们 输出时可以不管其高位,直接按字符输出其低8位即可。记得输出前检测keycode是否是Ctrl+d。
7 .5 .2实现参考
在 main.c文件中 main 函数前实现一个名为 print_key 的函数。根据线程的要求,该函数定义为 void print_key (ulong_t arg);。在函数中实现一个无限循环,循环中调用Wait_For_Key()取得键盘输入, 如果该输入等于’d’|KEY_CTRL_FLAG则跳出循环结束函数,否则将读取到的键盘输入使用Print函数输 入到屏幕上。
在main 函数中把宏TODO 注释掉,在该位置使用Start_Kernel_Thread 增加一个线程。 Start_Kernel_Thread(print_key,0,PRIORITY_NORMAL,true);
按第二章的方法,编译运行。
运行时发现按一个键会打印2个同样的字符,这是因为按下和松开都会产生扫描码,中断处理程 序都识别并压入了缓冲区。
个性化设计
设计了一个内核线程,该线程不仅会显示按键输入,还会统计每个字符的输入次数,并在屏幕上显示统计结果。当遇到 Ctrl + d 时,线程将退出并显示最终的统计结果。
1 |
|
思考题回答
根据提供的讲义内容,以下是针对第6章思考题的回答:
1. 系统的第一个线程是什么?初始化它的时候为何不调用Setup_Kernel_Thread?
回答:
- 系统的第一个线程是
Main
函数。 这个函数是内核的入口点,负责初始化各种子系统,并启动其他内核线程。 - 初始化
Main
线程时不调用Setup_Kernel_Thread
的原因:Main
函数是内核的第一个线程,它的上下文和堆栈已经在引导加载程序中设置好了。因此,不需要通过Setup_Kernel_Thread
来设置其上下文。- 在
Main
函数执行时,许多内核子系统(如内存管理、调度器等)还没有完全初始化。如果在这个阶段调用Setup_Kernel_Thread
,可能会导致依赖问题。 Main
函数直接控制了内核的启动流程,包括初始化各个子系统和创建其他内核线程。这种控制流确保了所有初始化步骤按顺序执行,避免了并发带来的复杂性。
2. 内核线程结构体中定义了三组指针,它们的作用是什么?
回答:
- 内核线程结构体
Kernel_Thread
中定义的三组指针及其作用:userContext
指针:指向用户态进程上下文。对于内核线程,这个指针为空;对于用户态进程,这个指针指向User_Context
结构体,用于存储用户态进程的相关信息。stackPage
指针:指向该线程的堆栈页。每个线程都有自己的堆栈空间,用于保存局部变量、函数参数和返回地址等。next
和prev
指针:用于将线程链接到链表中。这些指针使得线程可以被组织成链表结构,便于管理和调度。例如,s_runQueue
是一个运行队列,s_allThreadList
是所有线程的链表。
3. 区别 Setup_Kernel_Thread 中的 Push 函数与汇编指令 push
回答:
Push
函数:Push
函数是 C 语言中的一个函数,位于kthread.c
文件中。它通过操作线程结构体内部的esp
指针来模拟对线程堆栈的操作。- 具体实现如下:
1
2
3
4
5static inline void Push(struct Kernel_Thread *kthread, ulong_t value)
{
kthread->esp -= 4;
*((ulong_t *)kthread->esp) = value;
} Push
函数主要用于在设置新线程的上下文时,将一些初始值压入线程的堆栈中。
**汇编指令
push
**:push
是 x86 汇编语言中的一个指令,用于将数据压入当前执行线程的堆栈中。- 例如,在
Switch_To_Thread
函数中,push
指令用于保存当前线程的上下文信息,以便稍后恢复。
4. Switch_To_Thread 中调整堆栈的目的是什么?
回答:
- 调整堆栈的目的:
- 保存当前线程的上下文:在切换到新的线程之前,需要保存当前线程的上下文信息,包括寄存器状态、堆栈指针等。这通常是通过将这些信息压入堆栈来完成的。
- 为新线程准备上下文:调整堆栈以使新线程能够正确地开始执行。这包括将新线程的堆栈指针设置为正确的值,以及将新线程的上下文信息从堆栈中恢复。
- 模拟中断返回:
Switch_To_Thread
通过调整堆栈,使其看起来像是从一个中断返回。这样,当新线程开始执行时,它会从正确的地址继续执行,而不会丢失任何上下文信息。
具体来说,Switch_To_Thread
中的堆栈调整步骤包括:
- 保存当前线程的
eax
寄存器。 - 获取并调整返回地址。
- 将
eflags
和cs
寄存器的值压入堆栈。 - 保存通用寄存器的状态。
- 设置新线程的堆栈指针。
- 恢复新线程的上下文信息。
通过这些步骤,Switch_To_Thread
确保了线程切换过程中的上下文完整性,使得新线程能够顺利地从上次暂停的地方继续执行。
去牛么的实验分析:不想写。。。。。。
好的,根据你提供的实验报告和已完成的实验内容,我将帮助你完成第四部分的实验分析。以下是基于你的实验过程和结果的详细实验分析:
四、实验分析
1. 实验背景
- 项目简介:本实验基于 GeekOS 操作系统,旨在通过编写和调试内核代码来理解操作系统的基本原理和实现方法。
- 实验目标:
- 熟悉 GeekOS 实验环境,了解操作系统引导过程。
- 分析 GeekOS 中内核代码,掌握内核线程的实现原理和中断管理方法。
2. 实现细节
操作系统引导扇区代码:
- 编写了一个简单的引导扇区代码,该代码在启动时显示 “Hello, OS world!”。
- 使用 NASM 编译器编译生成引导扇区二进制文件,并将其写入软盘镜像文件中。
- 通过 Bochs 模拟器加载并运行软盘镜像文件,成功显示了预期的欢迎信息。
创建键盘输入处理线程:
- 定义了一个名为
print_key
的内核线程函数,用于从键盘接收按键并在屏幕上显示。 - 该线程函数使用
Wait_For_Key
函数等待键盘输入,并检查是否为松开事件或特殊组合键(如 Ctrl + d)。 - 如果是按下事件,则将按键转换为字符并在屏幕上显示。
- 定义了一个名为
创建两个打印字符串的线程:
- 创建了两个内核线程,分别打印不同的字符串信息。
- 通过
Start_Kernel_Thread
函数启动这两个线程,并观察它们在屏幕上的输出行为。
设计个性化的内核线程:
- 设计了一个内核线程,不仅会显示按键输入,还会统计每个字符的输入次数,并在屏幕上显示统计结果。
- 当遇到 Ctrl + d 时,线程将退出并显示最终的统计结果。
3. 遇到的问题及解决方案
问题1:每次按下按键都会输出打印两个同样的字符。
- 原因:键盘事件在按下和松开按键时都会产生扫描码,处理程序都识别并压入了缓冲区。
- 解决方案:通过添加检查是否为松开事件的代码部分解决了问题。具体代码如下:
1
2
3if (keycode & KEY_RELEASE_FLAG) {
continue;
}
问题2:个性化内核线程设计时,
make
报错。- 原因:
for
循环初始声明仅在 C99 模式下允许,但默认编译器模式不是 C99。 - 解决方案:修改代码将变量
i
声明提前,并使用-std=c99
或-std=gnu99
选项进行编译。
- 原因:
4. 实验结果
- 引导扇区代码:成功编译并运行了引导扇区代码,在 Bochs 模拟器中正确显示了 “Hello, OS world!”。
- 键盘输入处理线程:实现了从键盘接收按键并在屏幕上显示的功能,能够正确区分按下和松开事件。
- 打印字符串的线程:成功创建了两个内核线程,分别打印不同的字符串信息,观察到了多任务调度的效果。
- 个性化内核线程:设计的内核线程不仅能显示按键输入,还能统计每个字符的输入次数,并在退出时显示统计结果。
5. 总结与反思
收获:
- 深入理解了操作系统内核的基本结构和工作原理。
- 掌握了内核线程的创建和管理方法。
- 学习了如何处理键盘中断和屏幕输出。
- 了解了多任务调度的基本概念和实现方法。
不足之处:
- 在某些复杂功能的实现上还存在不足,需要进一步学习和实践。
- 对于一些底层硬件的操作还不够熟练,需要更多的实践和经验积累。
未来改进方向:
- 继续完善 GeekOS 的其他功能,如文件系统支持、进程调度等。
- 加深对操作系统底层机制的理解,提高调试和解决问题的能力。
通过这次实验,你不仅学到了许多关于操作系统内核的知识,还积累了宝贵的实践经验。希望你在未来的项目中能够继续提升自己的技能!
1 | // 线程1函数:打印字符串1 |
好的,你的代码创建了两个内核线程 print_thread1
和 print_thread2
,它们分别打印不同的字符串信息,并使用 Yield()
函数主动让出 CPU。下面是对你代码的详细分析以及可能的观察结果。
代码分析
线程函数定义
**
print_thread1
**:1
2
3
4
5
6void print_thread1(ulong_t arg) {
while (1) {
Print("Thread 1: Hello from thread 1!\n");
Yield(); // 主动让出CPU
}
}- 这个线程在无限循环中打印 “Thread 1: Hello from thread 1!”。
- 每次打印后调用
Yield()
函数,主动让出 CPU,使得其他线程有机会执行。
**
print_thread2
**:1
2
3
4
5
6void print_thread2(ulong_t arg) {
while (1) {
Print("Thread 2: Hello from thread 2!\n");
Yield(); // 主动让出CPU
}
}- 这个线程在无限循环中打印 “Thread 2: Hello from thread 2!”。
- 每次打印后调用
Yield()
函数,主动让出 CPU,使得其他线程有机会执行。
创建并启动线程
创建并启动线程1:
1
Start_Kernel_Thread(print_thread1, 0, PRIORITY_NORMAL, true);
创建并启动线程2:
1
Start_Kernel_Thread(print_thread2, 0, PRIORITY_NORMAL, true);
Start_Kernel_Thread
函数用于创建并启动一个新的内核线程。- 参数说明:
print_thread1
和print_thread2
:线程函数的入口点。0
:传递给线程函数的参数。PRIORITY_NORMAL
:线程的优先级。true
:表示线程是否应该立即开始执行。
观察与分析
预期行为
- 由于两个线程都具有相同的优先级
PRIORITY_NORMAL
,并且都调用了Yield()
函数主动让出 CPU,因此这两个线程应该会交替执行。 - 在屏幕上,你应该看到
Thread 1: Hello from thread 1!
和Thread 2: Hello from thread 2!
交替出现。
实际观察
交替输出:
- 如果调度器正常工作,你会看到以下输出(顺序可能会有所不同):
1
2
3
4
5Thread 1: Hello from thread 1!
Thread 2: Hello from thread 2!
Thread 1: Hello from thread 1!
Thread 2: Hello from thread 2!
...
- 如果调度器正常工作,你会看到以下输出(顺序可能会有所不同):
可能的非均匀分布:
- 由于调度器的具体实现和系统负载等因素,你可能会观察到某些情况下一个线程连续执行多次,然后切换到另一个线程。例如:
1
2
3
4
5Thread 1: Hello from thread 1!
Thread 1: Hello from thread 1!
Thread 2: Hello from thread 2!
Thread 2: Hello from thread 2!
...
- 由于调度器的具体实现和系统负载等因素,你可能会观察到某些情况下一个线程连续执行多次,然后切换到另一个线程。例如:
系统响应:
- 如果系统的其他部分(如中断处理、定时器等)也占用了 CPU 时间,可能会导致线程切换不完全均匀。
调试与验证
确保调度器正常工作:
- 检查
Yield()
函数的实现,确保它正确地将当前线程的状态设置为READY
并选择下一个可运行的线程。 - 检查调度器的实现,确保它能够公平地选择线程进行调度。
- 检查
检查日志输出:
- 可以在
Yield()
函数中添加一些调试信息,记录每次让出 CPU 的时间点,以便更好地理解调度行为。 - 例如,在
Yield()
中添加:1
Print("Yielding from thread %d\n", GetCurrentThreadId());
- 可以在
调整优先级:
- 尝试改变其中一个线程的优先级,观察其对调度行为的影响。例如:
1
2Start_Kernel_Thread(print_thread1, 0, PRIORITY_HIGH, true);
Start_Kernel_Thread(print_thread2, 0, PRIORITY_NORMAL, true);
- 尝试改变其中一个线程的优先级,观察其对调度行为的影响。例如:
总结
通过这个实验,你可以观察到多任务调度的基本行为。两个线程通过 Yield()
函数主动让出 CPU,实现了交替执行的效果。实际观察结果可能会因调度器的具体实现和其他系统因素而略有不同。通过调试和验证,可以进一步了解调度器的工作原理和优化方法。
希望这些分析对你有所帮助!如果你有更多具体的问题或需要进一步的帮助,请告诉我。