一、TSS
TSS是Task State Segment的缩写,即任务状态段,早在简述特权级的时候我们就讲过了一点 手写简易操作系统(八),现在我们讲一下这些保存的寄存器是干嘛的。
这一部分需要讲点历史,硬件与软件的关系是相互促进的,最初的CPU只支持单任务,但是后面随着多任务的需求越来越急迫,在软件上面实现多任务不仅存在效率不高的问题,而且容易出bug,于是CPU的厂商想办法来解决这个问题。
单核CPU要想实现多任务,唯一的方案就是多个任务共享同一个CPU,也就是只能让CPU在多个任务间轮转,让所有任务轮流使用CPU,除此之外还真别无他法。
CPU执行任务时,需要把任务运行所需要的数据加载到寄存器、栈和内存中。CPU只能直接处理这些资源中的数据,这是CPU在设计之初时工程师们决定的。于是问题来了,任务的数据和指令是CPU的处理对象,任务的执行要占用一套存储资源,如寄存器和内存,这些存储资源中装的是任务的数据和指令,但是CPU不愿意直接使用内存中的数据,他更喜欢寄存器这个高速的缓存,因此内存中的数据往往被加载到高速的寄存器后再处理,处理完成后,再将结果回写到低速的内存中,所以,任何时候,寄存器中的内容才是任务的最新状态。采取轮流使用CPU的方式运行多任务,当前任务在被换下CPU时,任务的最新状态,也就是寄存器中的内容应该找个地方保存起来,以便下次重新将此任务调度到CPU上时可以恢复此任务的最新状态,这样任务才能继续执行,否则就出错了。
CPU的厂商也是这么想的,于是硬件厂商的想法是给每个任务一个任务状态段TSS。这里有一套完整的寄存器映像,恢复这些寄存器映像就能恢复任务状态。TSS是由程序员提供的,其实就是提供了一个内存地址,告诉了CPU和操作系统去哪里找相应任务的TSS。但是确实CPU在维护的,CPU自动的就可以使用这段内存保存任务的上下文以及寄存器。
CPU又如何知晓该切换TSS了呢,在CPU中有一个专门存储TSS信息的寄存器,这就是TR寄存器,它始终指向当前正在运行的任务,任务切换的实质就是TR寄存器指向不同的TSS。但是在Linux中并未这样做,因为切换TSS的开销是很大的,不如TR寄存器只指向一个TSS,我们直接替换TSS中的寄存器映像。这样也间接的实现了切换任务,且开销较小。
1.1、TSS描述符
TSS和其他段一样,本质上是一片存储数据的内存区域,Intel打算用这片内存区域保存任务的最新状态(也就是任务运行时占用的寄存器组等),因此它也像其他段那样,需要用某个描述符结构来“描述”它,这就是TSS描述符,TSS描述符也要在GDT中注册,
TSS描述符属于系统段描述符,因此S为0,在S为0的情况下,TYPE的值为10B1。我们这里关注一下B位,B表示busy位,B位为0时,表示任务不繁忙,B位为1时,表示任务繁忙。
任务繁忙有两方面的含义,一方面就是指此任务是否为当前正在CPU上运行的任务。另一方面是指此任务嵌套调用了新的任务,CPU正在执行新任务,此任务暂时挂起,等新任务执行完成后CPU会回到此任务继续执行,所以此任务马上就会被调度执行了。这种有嵌套调用关系的任务数不只两个,可以很多,比如任务A调用了任务A.1,任务A.1又调用了任务A.1.1等,为维护这种嵌套调用的关联,CPU把新任务TSS中的B位置为1,并且在新任务的TSS中保存了上一级旧任务的TSS指针(还要把新任务标志寄存器eflags中NT位的值置为1),新老任务的调用关系形成了调用关系链。
当任务刚被创建时,此时尚未上CPU执行,因此,此时的B位为0,TYPE的值为1001。当任务开始上CPU执行时,处理器自动地把B位置为1,此时TYPE的值为1011。当任务被换下CPU时,处理器把B位置0。注意,B位是由CPU来维护的,不需要干预。
B位存在的意义可不是单纯为了表示任务忙不忙,而是为了给当前任务打个标记,目的是避免当前任务调用自己,也就是说任务是不可重入的。
1.2、TSS结构
TSS中的字段基本上全是寄存器名称,这些寄存器就是任务运行中的最新状态。可见TSS的主要作用就是保存任务的快照。
除了一般的寄存器外,TSS中还有“I/O位图”和“上一个任务的TSS指针”,分别位于TSS结构图的左上角和右下角。I/O位图可以在单个端口的粒度上进行IO特权控制,
另外要说的就是TSS中有三组栈:SS0和esp0,SS1和esp1,SS2和esp2。之前已经介绍过,除了从中断和调用门返回外,CPU不允许从高特权级转向低特权级。没有特权级会跳到特权级3,所以不会有特权级3的栈。
Linux只用到了0特权级和3特权级,用户进程处于3特权级,内核位于0特权级,因此对于Linux来说只需要在TSS中设置SS0和esp0,咱们也效仿它,只设置SS0和esp0的值就够了。
TSS是CPU原生支持的数据结构,因此CPU能够直接、正确识别其中的所有字段。当任务被换下CPU时,CPU会自动将当前寄存器中的值存储到TSS中的对应位置,当有新任务上CPU运行时,CPU会自动从新任务的TSS中找到相应的寄存器值加载到对应的寄存器中。
和LDT一样,CPU对TSS的处理也采取了类似的方式,它提供了一个寄存器来存储TSS的起始地址及偏移大小。但也许让人有点意外,这个寄存器不叫TSSR,而是称为TR(Task Register),也许是称为 TR,
将tss加载到寄存器TR的指令是ltr,其指令格式为:
ltr “16位通用寄存器”或“16位内存单元”
有了TSS后,任务在被换下CPU时,由CPU自动地把当前任务的资源状态(所有寄存器、必要的内存结构,如栈等)保存到该任务对应的TSS中(由寄存器TR指定)。CPU通过新任务的TSS选择子加载新任务时,会把该TSS中的数据载入到CPU的寄存器中,同时用此TSS描述符更新寄存器TR。注意啦,以上动作是CPU自动完成的,不需要干预。这就是硬件级别的原生支持。
1.3、现代操作系统支持的任务切换方式
Linux为每个CPU创建一个TSS,在各个CPU上的所有任务共享同一个TSS,各CPU的TR寄存器保存各CPU上的TSS,在用ltr指令加载TSS后,该TR寄存器永远指向同一个TSS,之后再也不会重新加载TSS。在进程切换时,只需要把TSS中的SS0及esp0更新为新任务的内核栈的段地址及栈指针。
实际上Linux对TSS的操作是一次性加载TSS到TR,之后不断修改同一个TSS的内容,不再进行重复加载操作。Linux在TSS中只初始化了SS0、esp0和I/O位图字段,除此之外TSS便没用了,就是个空架子,不再做保存任务状态之用。
那任务的状态信息保存在哪里呢?当CPU由低特权级进入高特权级时,CPU会“自动”从TSS中获取对应高特权级的栈指针(TSS是CPU内部框架原生支持的嘛,当然是自动从中获取新的栈指针)。Linux只用到了特权3级和特权0级,因此CPU从3特权级的用户态进入0特权级的内核态时(比如从用户进程进入中断),CPU自动从当前任务的TSS中获取SS0和esp0字段的值作为0特权级的栈,然后Linux“手动”执行一系列的push指令将任务的状态的保存在0特权级栈中,也就是TSS中SS0和esp0所指向的栈。
Intel当初是打算让TR寄存器指向不同任务的TSS以实现任务切换的,Linux这里只换了TSS中的部分内容,而TR本身没换,还是指向同一个TSS,这种“自欺欺人”的好处是任务切换的开销更小了,因为和修改TSS中的内容所带来的开销相比,在TR中加载TSS的开销要大得多。
每次切换任务都要用ltr指令重新加载新任务的TSS到寄存器TR,TSS是位于内存中的,随着任务数量一多,这种频繁重复加载的开销就更为“可观”。另外,Linux中任务切换不使用call和jmp指令,这也避免了任务切换的低效。
二、定义并实现TSS
首先看一下tss的结构,和上图的顺序是一样的
/* 任务状态段tss结构 */
struct tss {
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t(*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint32_t trace;
uint32_t io_base;
};
其初始化和更新程序为
/* 更新tss中esp0字段的值为pthread的0级线 */
void update_tss_esp(struct task_struct* pthread) {
tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}
/* 创建gdt描述符 */
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) {
uint32_t desc_base = (uint32_t)desc_addr;
struct gdt_desc desc;
desc.limit_low_word = limit & 0x0000ffff;
desc.base_low_word = desc_base & 0x0000ffff;
desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
desc.attr_low_byte = (uint8_t)(attr_low);
desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
desc.base_high_byte = desc_base >> 24;
return desc;
}
/* 在gdt中创建tss并重新加载gdt */
void tss_init() {
put_str("tss_init start\n");
uint32_t tss_size = sizeof(tss);
memset(&tss, 0, tss_size);
tss.ss0 = SELECTOR_K_STACK;
tss.io_base = tss_size;
/* 在gdt中添加dpl为0的TSS描述符,以及特权级为3的用户段 */
*((struct gdt_desc*)(0xc0000603 + 8*4)) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
*((struct gdt_desc*)(0xc0000603 + 8*5)) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
*((struct gdt_desc*)(0xc0000603 + 8*6)) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
/* gdt段基址为0x603,把tss放到第4个位置,也就是0x602+0x20的位置 */
uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000603 << 16));
asm volatile ("lgdt %0" : : "m" (gdt_operand));
asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
put_str("tss_init and ltr done\n");
}
我们先仿真一下
可以看到所有的gdt,已经设置正确了。
三、实现用户进程
我们的目标是在现有线程的基础上实现进程,先回忆下,我们创建线程是通过thread_start进行的,其内部实现的流程如下图所示
如果要基于线程实现进程,我们把function替换为创建进程的新函数就可以啦,先把控制权拿到手再说,进程相关的具体工作再由新函数完成。
3.2、用户进程的虚拟地址
进程与内核线程最大的区别是进程有单独的 4GB 空间,这指的是虚拟地址,物理地址空间可未必有那么大,看似无限的虚拟地址经过分页机制之后,最终要落到有限的物理页中。
每个进程都拥有4GB的虚拟地址空间,虚拟地址连续而物理地址可以不连续,这就是保护模式下分页机制的优势。为此我们需要为每个进程维护一个虚拟地址池,用此地址池来记录该进程的虚拟中,哪些已被分配,哪些可以分配。
为此task_struct
结构杯修改以下
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 线程或者进程内核栈的栈顶,就是pcb的高位
enum task_status status; // 线程的运行状态
char name[16]; // 线程名,最多16个字母
uint8_t priority; // 线程优先级
uint8_t ticks; // 每次在处理器上的执行时间的滴答数
uint32_t elapsed_ticks; // 这个任务总的滴答数
struct list_elem general_tag; // 线程在一段队列中的节点
struct list_elem all_tag;// 线程在所有任务队列中的节点
uint32_t* pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址池
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
pgdir用于存放进程页目录表的虚拟地址,这将在为进程创建页表时为其赋值。也许您感到迷惑,按理说页表寄存器cr3中的应该是页目录表的物理地址,但成员pgdir是虚拟地址,这是什么原因?原因是页目录表本身也要占用内存来存储,我们在为进程创建页目录表时,必然要为其申请内存,但内存管理系统返回的地址肯定都是虚拟地址,不可能返回物理地址,因为返回物理地址也没用,在分页机制下,引用的任何地址都被当作虚拟地址,该“物理地址”也要再次被转换成别的物理地址,这就错了。因此在往寄存器cr3中加载页目录地址时,我们会将pgdir转换成物理地址,这部分将在下节介绍。
3.3、为进程创建页表和3级特权栈
进程与线程的区别是进程拥有独立的地址空间,不同的地址空间就是不同的页表,因此我们在创建进程的过程中需要为每个进程单独创建一个页表。我们这里所说的页表是“页目录表+页表”,页目录表用来存放页目录项PDE,每个PDE又指向不同的PTE。
为此我们需要修改以下memory.c
,增加当用户申请内存时的程序。
首先为内存池结构添加锁
/* 内存池结构 */
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
struct lock lock; // 申请内存时互斥
};
vaddr_get
函数中添加对用户进程的支持
/* 在pf表示的虚拟地址池中同时取出pg_cnt个连续的页,成功返回虚拟地址,失败返回NULL */
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL) {
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
put_str("vaddr_get error: kernel vaddr error!\n");
return NULL;
}
while (cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else {
// 获得当前的运行的线程
struct task_struct* cur = running_thread();
// 获得pg_cnt个连续的虚拟地址
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
// 如果没有这么大空间返回空
if (bit_idx_start == -1) {
put_str("vaddr_get error: User vaddr error!\n");
return NULL;
}
// 获取相应的物理地址
while (cnt < pg_cnt) {
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
// 拿到物理地址的起始
vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
return (void*)vaddr_start;
}
添加申请用户物理内存池的函数
/* 从用户物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_user_pages(uint32_t pg_cnt) {
void* vaddr = malloc_page(PF_USER, pg_cnt);
if (vaddr != NULL) {
memset(vaddr, 0, pg_cnt * PG_SIZE);
}
else {
put_str("get_user_pages error: vaddr error!\n");
}
return vaddr;
}
添加将地址vaddr与pf池中的物理地址关联的函数
/* 将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配 */
void* get_a_page(enum pool_flags pf, uint32_t vaddr) {
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
// 相应的物理内存池加锁
lock_acquire(&mem_pool->lock);
// 先将虚拟地址对应的位图置1
struct task_struct* cur = running_thread();
// 获得虚拟地址位图多少位有空
int32_t bit_idx = -1;
// 若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图
if (cur->pgdir != NULL && pf == PF_USER) {
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
// 如果是内核线程申请内核内存,就修改内核虚拟地址位图
else if (cur->pgdir == NULL && pf == PF_KERNEL) {
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);
}
// 其他情况就是出错了
else {
PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page");
}
// 获得申请的物理地址,
void* page_phyaddr = palloc(mem_pool);
// 没拿到返回空
if (page_phyaddr == NULL) { return NULL; }
// 页表中添加虚拟地址和物理地址之间的映射
page_table_add((void*)vaddr, page_phyaddr);
// 解锁
lock_release(&mem_pool->lock);
// 返回虚拟地址
return (void*)vaddr;
}
得到的虚拟地址映射到物理地址
/* 得到虚拟地址映射到的物理地址 */
uint32_t addr_v2p(uint32_t vaddr) {
uint32_t* pte = pte_ptr(vaddr);
// (*pte)的值是页表所在的物理页框地址, 去掉其低12位的页表项属性+虚拟地址vaddr的低12位
return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}
3.4、进入特权级3
用户进程工作在特权级3,但是目前为止,我们都在特权级0下工作,除了中断,特权级之间不能随意跳转,高特权级更不能跳转到低特权级,除了中断返回,但是现在还没有中断啊,怎么中断返回呢,还是之前的思路,反正不管是中断还是中断返回,都是从栈中调用数据,那么我们直接从栈中把要返回的地址和参数给系统准备好,系统直接返回不就行了。
从中断返回肯定要用到iretd指令,iretd指令会用到栈中的数据作为返回地址,还会加载栈中eflags的值到eflags寄存器,如果栈中cs.rpl若为更低的特权级,处理器的特权级检查通过后,会将栈中cs载入到CS寄存器,栈中ss载入SS寄存器,随后处理器进入低特权级。
在中断发生时,我们在中断入口函数“intr%1entry”中通过一系列的push操作来保存任务的上下文,因此在intr_exit中恢复任务上下文要通过一系列的pop操作,这属于“intr%1entry”的逆过程。任务的上下文信息被保存在任务pcb中的struct intr_stack中,注意啦,struct intr_stack并不要求有固定的位置,它只是一种保存任务上下文的格式结构。
在汇编函数intr_exit里面的一系列的pop操作是为了恢复任务的上下文,我们也可以利用它,借一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
当执行完intr_exit中的iretd指令后,CPU便恢复了任务中断前的状态:中断前是哪个特权级就进入哪个特权级。
CPU是如何知道从中断退出后要进入哪个特权级呢?这是由栈中保存的CS选择子中的RPL决定的,我们知道,CS.RPL就是CPU的CPL,当执行iretd时,在栈中保存的CS选择子要被加载到代码段寄存器CS中,因此栈中CS选择子中的RPL便是从中断返回后CPU的新的CPL。
既然用户进程的特权级为3,操作系统不能辜负CPU的委托,它有责任把用户进程所有段选择子的RPL都置为3,因此,在RPL=CPL=3的情况下,用户进程只能访问DPL为3的内存段,即代码段、数据段、栈段。我们前面的工作中已经准备好了DPL为3的代码段及数据段,由此我们得出关键点4,栈中段寄存器的选择子必须指向DPL为3的内存段。
对于可屏蔽中断来说,任务之所以能进入中断,是因为标志寄存器eflags中的IF位为1,退出中断后,还得保持IF位为1,继续响应新的中断。
用户进程属于最低的特权级,对于IO操作,不允许用户进程直接访问硬件,只允许操作系统有直接的硬件控制。这是由标志寄存器eflags中IOPL位决定的,必须使其值为0。
所以我们得到六个关键点
1、从中断返回,必须要经过intr_exit
2、必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息,借一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
3、在栈中存储的CS选择子,其RPL必须为3。
4、栈中段寄存器的选择子必须指向DPL为3的内存段。
5、必须使栈中eflags的IF位为1。
6、必须使栈中eflags的IOPL位为0。
3.5、创建用户进程的流程
我们看一下如何创建的进程
/* 创建用户进程 */
void process_execute(void* filename, char* name) {
// pcb是操作系统的数据,由操作系统来维护
struct task_struct* thread = get_kernel_pages(1);
// 初始化线程
init_thread(thread, name);
// 完善pcb中的用户虚拟地址位图,这也是线程是内核线程还是用户进程的判别
create_user_vaddr_bitmap(thread);
// 创建线程
thread_create(thread, start_process, filename);
// 创建用户进程的页目录表,用户进程有自己的页目录表,这样就实现了进程的隔离
thread->pgdir = create_page_dir();
// 将当前线程加入多级反馈优先队列
mlfq_new(thread);
}
1、申请一页内核地址作为用户进程的pcb
2、初始用户进程的pcb
3、用户进程中有用户进程的虚拟地址位图,初始化这个位图
4、创建进程,其中要执行的函数是
start_process
, 传入的参数是filename
5、 创建用户进程的页目录表,用户进程有自己的页目录表,这样才能实现了进程的隔离,用户进程的页目录表的高3G的地址是复制的内核线程的,这样也实现了不同进程高1GB地址的通用
6、将当前进程加入多级反馈有限队列等待调用。
发现了嘛,进程和内核线程几乎是一样的,只是用户进程不能访问硬件资源,用户进程有自己的页目录表,页目录表的高1G地址还是复制的内核页表。我们看一下 start_process
这个函数,毕竟创建进程后就等待调用了,一旦 schedule()
函数调用了这个进程,那么就开始执行 start_process(filename)
这个函数
/* 构建用户进程初始上下文信息 */
void start_process(void* filename_) {
// 后面我们是要从硬盘中读取用户程序的,所以这里是一个filename,不是function
void* function = filename_;
struct task_struct* cur = running_thread();
cur->self_kstack += sizeof(struct thread_stack); //跨过thread_stack,指向intr_stack
struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;//可以不用定义成结构体指针
proc_stack->edi = 0;
proc_stack->esi = 0;
proc_stack->ebp = 0;
proc_stack->esp_dummy = 0;
proc_stack->ebx = 0;
proc_stack->edx = 0;
proc_stack->ecx = 0;
proc_stack->eax = 0;
proc_stack->gs = 0; // 不允许用户态直接访问硬件显存
proc_stack->ds = SELECTOR_U_DATA;
proc_stack->es = SELECTOR_U_DATA;
proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function; // 待执行的用户程序地址
proc_stack->cs = SELECTOR_U_CODE;
proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE) ;
proc_stack->ss = SELECTOR_U_DATA;
asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
}
首先就是获取当前的线程,当前的线程其实就是我们的用户进程了,为什么这么讲,我们在 schedule()
中执行 switch_to
函数以后当前线程就已经了新创建的用户进程了,直到通过栈执行到 start_process
。
看一下上面的C代码,可以发现只是填写中断栈,填写中断栈的作用只有一个中断返回,没有进入中断不要紧,我们构造一个中断栈,中断栈内压入要执行的用户程序地址作为中断返回地址,然后执行中断返回,从此就实现了内核程序向用户程序的跳转。
四、仿真
可以看到现在的打印还有问题
这个问题就好分析了,虽然我们对控制台上了锁,但是我们的打印是分三次进行的,在打印出 0x
后,进入中断,任务调换了,再次进入任务a时才打印后面的数字以及回车符。
结束语
本节我们创建了用户程序,但是用户程序连打印都做不到,需要内核级线程的帮助才能打印
正常的Linux系统是通过 0x80
中断来实现低特权级向高特权级跳转的,也就是通过 0x80
中断调用系统服务。下节我们实现 0x80
中断。
老规矩,本节的代码地址:https://github.com/lyajpunov/os