实现用户进程-代码详解
本文前置内容:实现用户进程-进入用户态
本节对应分支:userprog
初始化TSS、C语言接管GDT
下面是 global.h 中添加的代码:
123456789101112131415161718192021222324252627282930313233343536373839//文件说明:global.h//=============用户进程的段选择子================#define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3)#define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3)#define SELECTOR_U_STACK SELECTOR_U_DATA// ===============GDT描述符属性=================#define DESC_G_4K 1#define DESC_D_32 1#define DESC_L 0 // 64位 ...
实现用户进程—进入用户态
本文前置内容(必看):TSS/LDT/GATE ,中断详解 ,进程的虚拟内存布局、《装载、链接与库》
本节对应代码讲解:实现用户进程-代码详解
概述
操作系统有三大核心功能:内存管理、进程管理、文件管理 。截至目前,我们已经完成了内存管理和进程管理的部分内容,对于内存管理,咋们还差内存回收机制;对于进程管理,由于线程是进程的基础,之前咋们实现了线程,所以进程也就完成了一半;文件管理将在不久后实现文件系统后再进行。
任务切换的原生方式
在 TSS/LDT/GATE 一文中,我们简单了解过 TSS 与 LDT 的作用,明白了 TSS 和 LDT 只是理想中的任务管理和切换的工具: Intel 建议用 TSS 来保存并恢复任务的状态,用 LDT 来保存任务的实体资源 。而考虑到效率问题,现代操作系统并未(完全)使用 TSS 和 LDT 来进行任务切换。至于为什么效率低下,看看其任务切换的具体过程便能体会到:
CPU 原生支持 的任务切换方式有两种:1)中断 + 任务门;2)call / jmp + 任务门;下面分别介绍这两种方式。
中断+任务门
既然是通过中断调用,那么调用方式只能通过 ...
锁机制—代码实现
本文前置内容:浅谈锁机制
本节对应分支:lock
在上节内容中我们提到,当线程申请锁时,如果该锁已经被其他线程拥有,则此线程必须在该锁上陷入睡眠,直到锁的拥有者将其叫醒。所以我们先实现进程的睡眠与觉醒。
123456789101112131415161718192021222324//thread.cvoid thread_block(enum task_status stat){ 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; schedule(); //将当前线程换下处理器 intr_set_status(old_status) ...
浅谈锁机制
前言
在上节线程 - 进阶 - 任务调度的末尾,笔者演示了当删去 put_str() 上下的 STI 和 CLI 后发生的错误情况(打印不规律,且发生 GP 异常)这是为什么呢?这里就不卖关子了,直接原因是 线程不同步 。这样说了也当白说,让我们仔细还原现场:
k_thread_a 在 put_char 中读取了字符打印的光标位置 p 。
当 k_thread_a 准备更新光标位置时,中断发生,切换到 k_thread_b 。
k_thread_b 读取光标位置,由于 k_thread_a 还未更新光标,所以此时光标值仍为 p 。
于是,k_thread_b 在相同地方打印字符,覆盖了 k_thread_a 的字符。
因此才出现少字符的情况。对于其他错误,比如一大串空格以及 GP 异常,就不详细说明原因了,只需明白,它们的罪魁祸首都是线程不同步造成的。为了进一步解释什么叫线程不同步,先来看看下面几个概念:
临界区: 是指包含有共享数据的一段 代码 ,这些代码可能被多个线程访问或修改。临界区的存在就是为了保证当有一个线程在临界区内执行的时候,不能有其他任何线程被允许在临界区执行。 ...
线程-进阶-任务调度
前置内容:线程-基础-加载线程
本节分支:thread-schedule
概览
任务链表
通常使用链表来维护任务队列。链表本身不是本节的重点,所以笔者将其放在文末。
任务调度基础
基于上节内容对 thread.c 和 thread.h 进行改进。
任务切换
改进时钟中断,添加任务调度器,开始任务切换。
任务调度基础
thread.h
1234567891011121314//thread.hstruct task_struct { uint32_t* self_kstack; enum task_status status; char name[16]; uint8_t priority; uint8_t ticks; uint32_t elapsed_ticks; struct list_elem general_tag; struct list_elem all_list_tag; uint32_t* pgdir; ...
内存管理-进阶-分配页内存
本节前置内容:内存管理-基础-初始化内存池
本节对应分支:memory-alloc
概述
对笔者而言,内存分配一直是操作系统最神秘的部分之一,从学习编程开始,就一直能在耳边听到这个词,所以这也是本人最期待的部分,不知读者是否也是如此呢?本节我们实现的内存分配是“整页分配”,这与 malloc 函数不同,后者能申请任意大小的内容,而前者的申请单位则是以页为计。不过,malloc 也是基于“整页分配”进行的,所以未来我们也会借助本节内容来实现 malloc 函数。
本节的函数逻辑也都很简单,只是它们的数量较多,关系稍显复杂,所以贴心的笔者(手动狗头^_^)献上一幅函数关系图以供大家参考:
上图就是内存申请的全过程,大括号中包含的函数即为括号所指函数中调用的函数,且从上到下依次调用。上图只是为了让大家稍微熟悉页分配的过程,具体过程咋们还是来看代码吧。
代码解析
123456789101112131415161718192021222324252627282930//memory.henum pool_flags { PF_KERNEL = 1, // 内核内存池 ...
详解字符串操作函数
本节对应分支:string
下节我们将要实现内存管理,这不可避免地要频繁使用到 memcpy、memset 等函数,有了内存操作函数就很容易实现字符串操作函数 strcpy、strcat 等。所以这节我们来实现内存操作和字符串操作函数。本节内容虽然简单,但有许多代码规范需要注意,还请读者不可掉以轻心。
memset
1234567void memset(void* dst, char var, unsigned int size){ assert(dst != NULL); unsigned char* tmp = dst; while((size--) > 0) *tmp++ = var;}
注意,void* 是无法直接作指针运算的,因为编译器无法确定其步长及其解释方式 。因此,需要定义 unsigned char* tmp 来代替 void* dst ,tmp 指针的步长即为 1 字节。以下同理。
什么是指针的步长?就是指 ++ 或 -- 时指针移动的字节数。
什么是解释方式?就是指定编译器如何去解释指针所指向的这个 ...
线程-基础-加载线程
本文参考:并发和并行,区分进程和线程,《操作系统真相还原》《操作系统哲学原理》
本节分支:thread
嚯,跨过千山万水,咋们终于要实现线程啦!这是既内存管理后,笔者最期待的部分。正式开干前,先让我们了解一下本节的学习框架。
概览
并发和并行、同步与异步
这是常见而又容易混淆的几种关于任务执行的概念,它们与线程、进程息息相关。
任务、进程、线程
任务是 CPU 的最小调度单元;任务既可以是线程,也可以是进程;线程是在进程基础上进行的第二次并发 。
PCB
进程/线程的身份证,用于存放进程/线程的管理和控制信息。
线程的内核态与用户态实现
粗略了解这两种方式的优缺点以及现代操作系统对线程的实现模型。
线程实现
初步实现线程,这是下节实现任务调度的基础。
并发与并行、同步与异步
并发和并行
并发又称“伪并行” ,并发的实质是一个物理 CPU 在若干道程序之间来回切换,每一刻都只有一个任务在 CPU 上执行 ,但因为切换任务的速度相当快,所以看上去是多个任务同时执行。
需要注意的是,伪并行降低的是任务的平均响应时间,也就是说,并发让执行时间短的任务可以不必等待那些执行时间长的任务 ...
内存管理-基础-初始化内存池
本节对应分支:memory
概述
操作系统内存管理可以说是操作系统中最重要的部分 ,它是未来所有工作的基础。内存管理相当复杂,大约有如下内容:
但本节并不会讨论以上全部内容,而是根据我们自制操作系统的需要来进行。我们当前的任务是完成操作系统的内存划分(本节)以及虚拟内存的申请(下节),即虚拟空间到物理内存的映射,其他内容咋们后续按需补充。本节内容如下:
通过 BIOS 中断获取内存容量
既然要分配内存,就一定需要知道系统的内存容量有多大,这通过 BIOS 中断来获取。
通过位图来管理内存
管理内存时,肯定需要知道哪些内存已经被使用,哪些还没有使用,这些信息通过我们自己维护的位图来获取。
规划内存池
管理内存前,当然还需要对内存做出规划,比如,哪些内存给内核使用,哪些内存又给用户使用。
向页表填写映射关系
我们早就实现了分页机制,就差向其中填入映射关系啦!笔者期待已久,让我们开始吧。
获取内存容量
获取内存容量的例程已经由操作系统厂商写好并存入了 BIOS 中,因此我们只需要调用 BIOS 中断即可。现在问题是,进入保护模式后,BIOS 中断无法再被调用,这怎么办呢?不得已,我 ...
C语言中的#和##
字符串化#
C 语言中,# 号可用于将宏参数转为字符串,如下:
1234567#define STR(x) #xint main(){ int age=18; printf(STR(age)); //输出字符串age,而非18 printf(STR(18)); //输出字符串18}
这种用法有什么使用场景呢?如下,有时候我们需要取得宏函数参数(表达式/变量)的字面内容:
12345678#define warn(expr) printf(#expr)int main(){ int a=0; int b=0; warn(a==0); //打印"a==0" return 0;}
这在某些情况下很有用,比如 assert 宏,参见assert剖析。
另外需要注意,当宏参数是另一个宏的时候,宏定义里有用 # 的地方宏参数不会再展开 ,比如:
123456#define warn(expr) printf(#expr)#define Π 3.14int main(){ w ...