哈工大操作系统学习笔记五——内核级线程实现

哈工大os学习笔记五(内核级线程实现)



某个中断开始(fork)
内核级线程要在代码层面实现
fork()是系统调用,会引起中断,fork()创建进程(所以会有资源分配和线程的创建),所以fork()是一个非常好的切入点。能够非常清晰地看清
1.线程是怎么切换的
2.切换的五段论代码具体怎么实现的
3.核心级线程切换要具体做哪些事

一、 中断入口、中断出口(前后两段)

首先还是要从上次我画的那个图说起,从那个图中的中断开始。

1. 从int中断进入内核(中断入口第一段)

在这里插入图片描述

int 0x80执行的时候没进入内核,执行完毕后,才进入内核。
由于int 0x80没有执行完毕,所以当前的PC指向的是当前的(int 0x80的下一条)

_NR_fork 是系统调用号,存入%eax寄存器,后面会算出调用哪个函数

在这里插入图片描述

压入EFLAGS标志寄存器的状态,保存寄存器的状态,在保存寄存器里的内容
int 0x80中断,之后就是OS为我们打开的系统调用system _call,system_call还要压栈,因为我们刚进入内核,CPU这些寄存器仍然是用户态的,要保存寄存器的内容,将一些内容压栈,保护用户态执行的现场。

调用system_call 这个表,具体处理sys_fork,实现sys_fork真正的效果,执行sys_fork 的时候会引起切换。I/O阻塞,发生调度
在这里插入图片描述
在这里插入图片描述

执行完sys_fork 后,不停,又往下执行。
call 下面的代码就是判断是否引发中断的代码

_systemZ_call:
 push %d...%fs
 push %edx...
 call sys_fork //
 push1 %eax
————————————————————————————————
mov1 _current,%eax  //将当前的线程(当前线程一直是sys_fork)置给 eax
cmp1 $0,state(%eax) // state(%eax)是一段汇编代码,实际上是state加上eax
                    //current是PCB,就是看PCB中的state是不是0
                    //0是运行,非0表示阻塞
                    //如果不是0就要调度,也就是判断PCB队列的状态
jne reschedule      //跳转调度,跳到schedule完成内核 栈的切换
					//schedule五段论中间三段
——————————————————————————					
cmp1 $0, counter(%eax) //第五段判断current的counter是不是等于0
je reschedule          //等于0也进行调度,时间片是否为0
					   //时间片为0,时间片用完了为0,也要进行切换
ret_from_sys_call:	   //跳出去后,还要回来,这里是系统调用返回
					   //接下来就要执行中断返回的代码

——————————————————————
reschedule:			      //汇编的编号,实际上就是地址
						  //ret_from_sys_call的地址
 push1 $ret_from_sys_call //将地址压栈
 jmp _schedule			  /*调用_schedule,这个是C函数,他执行的时候
 							必然会遇到右大括号},执行}的时候就将弹栈,
 							返回的就上刚才压入的地址,
 							就会去ret_from_sys_call:执行*/

2.中断出口(最后一段)

在这里插入图片描述
在这里插入图片描述会一个一个的弹栈,最后将iret一起pop弹出。
切换出去后,他的下一个核心线程,也就是下一个进程的执行起来的用户栈,完成切换了。
在这里插入图片描述这里保存的都是用户态的内容

二、 其他三段

1.schedule()

schedule
next就是找下一个 线程,至于怎么找,后面会详细的讲。
next i进行调度,switch_to(next),next是下一个进程的TCB,
如果是线程那就是下一个核心级线程的TCB,switch_to 进行TCB切换就很简单,只要修改一下就ok

在这里插入图片描述核心是根据TCB完成核心栈的切换

2.switch_to()

TCB切换图
在这里插入图片描述
switch_to 具体代码实现:
下面李老师讲的这个代码(Linux0.11)是TSS实现切换的,而真正的用内核栈切完成切换的是实验四。TSS的介绍实验四有,是基于TSS来实现Kernel stack

TSS:task struct segment 任务结构段,英特尔给做出来了只用一句常跳转指令(ljmp %0\n\t)就可以做到,但是效率特别低,所以进行了改造
在这里插入图片描述
在这里插入图片描述
TR是CPU的寄存器,相当于GDT表
相当于一个快照,保存恢复。
next就是要进行改变的TR,新的TR找到新TSS描述符,在指向新的段,新的段保存了现场。
这种方式慢,由于快照要扣好多东西,而且由于是一条常指令,不能进行指令流水,是硬件设计效果不好,不能充分利用向现代CPU硬件的加速方法。而用栈,不用照好多东西只要压栈弹栈,而且可以进行流水。

3.ThreadCreate(创建好TSS)

创建线程,就是要创建出能够切换的样子,能够切换了,线程也就创建好了。所以下面的核心就是把TSS做好。当然TSS要做好首先要有PCB,TCB,内核栈,然后就可以做TSS。

下面 是代码实现:
在这里插入图片描述

继续执行fork()
_copy_process 拷贝父进程

下面是一段汇编
在这里插入图片描述
在这里插入图片描述
上面什么参数也没写,是因为都在内核栈里面。
因为父进程执行fork的时候,压了一堆东西,这些东西主要就是父进程在用户态执行时候的样子。这些样子(参数)需要传递给_copy_process,C语言执行的时候他的参数都从栈中弹出,当然这里已经进入内核执行了所以这里是内核栈。所以内核栈里面的内容全都作为参数,有了这些参数就能知道父进程长什么样子了。在_copy_process 基本能做出和父进程一样的叉子(进程)父进程和子进程只有一个地方的小差别。
这些参数包括esp等。在C函数中,越在后面的参数就越早压入栈中,所以位置越靠前的参数,越靠近栈顶。参数最长的函数。
esp>ss:sp esp j就是标记栈的
eip>ret=??1 int 0x80 要执行的下一句代码
在这里插入图片描述
要知道这些参数和栈的序列是怎么对应的
在这里插入图片描述
看下图:内核态代码get_free_page();获得一页内存,不能用malloc,因为malloc 是用户态的代码,在系统初始化的时候涉及到mem_map,mem_map将内存打成4K,4K,4K的一段一段,那个就叫一页一页 ,从那里找空闲页
就是mem_map等于0的那一页,找到mem_map等于0的那一页,把地址返回给p,并进行强制转换,这一页内存就用来做PCB。
PCB就有了
在这里插入图片描述
下面四行代码就是设置TSS,esp0就是内核栈,esp就是用户栈,这都是硬性规定好的。PAGE_SIZE(4k)p申请好的地址。
0x10 是内核数据段 ,这里也是用的内核堆栈段是一个段。
内核段有了,段偏移4k+p,下面是PCB,上面是内核栈。(PCB内核栈都做好了)

在这里插入图片描述

在这里插入图片描述
设置用户栈(copy_process那个贼多参数那个)
父进程传递下来的东西,父进程正在执行int0x80那个时候用到的用户栈,所以用了和父进程一样的栈。用户级可以用一样的,但是在内核级是两个不同的东西
在这里插入图片描述
在把TSS初始化就OK了,eip int 指令完成后的下一句话
在这里插入图片描述

4.小结

fork()子进程已经做好了

在这里插入图片描述
在这里插入图片描述

  1. 父进程阻塞,然后进入schedule调度

  2. schedule就要执行switch_to

  3. switch_to就切换到子进程
    在这里插入图片描述

  4. 子进程就会把刚才初始化好的TSS,直接扣到CPU。

在这里插入图片描述5. 其中包括eip等于int 0x80,eax等于0
父进程进去int创建一个子进程,返回的时候执行int的下一条,所以父进程也执行这条指令,父进程执行的时候eax≠0,所以父进程执行下面的
6. 执行

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
exec怎么进去的,从exec系统调用开始,进去后会执行实sys_execve这个函数,
老师提出的问题:在进入子进程之前,父进程子进程执行同样的代码。
好了现在调用exec,进入到内核态,一顿折腾后,再返回的时候执行的就不再是和父进程一样的代码,而是ls。
那么以上这些东西是怎么做的???
怎么做的:
在这里插入图片描述

中断返回的时候要做iret,iret 找到ss sp ret 一些的东西,将这些栈存储的东西赋给真正的寄存器,然后让PC执行。
在这里插入图片描述
接下来执行 call_do_execve这个函数,压栈,压栈是因为exec也是要有参数的,实际上压进去的实际是参数,参数是esp,eip。
如果能把ls的地址找到并且赋给ret,将来在执行iret的时候弹栈。EIP也就是一个偏移

esp,eip加起来赋给eax,eax进行压栈,EIP=0x1c 十进制是28,正好跳到28,也就是ret=???(eip)最后会指向PC。

在这里插入图片描述

在这里插入图片描述

eip[0]=ex.a_entry入口地址置给eip,就okl了。
而eip[3] 指向SS:SP
ls有自己的执行代码了有自己的栈了。
ex.a_entry哪里来的?可以读文件 ls是一个可执行命令 ls hello ,hello 是一个可执行程序,这个程序在磁盘上有,从磁盘上把这个程序读进来,就有一个文件头,这个文件头就有a_entry,a_entry是编译的是时候作为可执行文件写进去的。实际上是连接做为可执行文件程序写入的,写进去就放给ex.a_entry。在读文件读出来置给内核栈中,置好内核栈后,在中断返回iret时候就弹回去了,就执行hello的第一句话。

小结

做一个操作系统要知道好多内容:读写文件,链接,文件的格式,内核栈结构,内核栈位置,文件头赋给内核栈的相应位置,iret等等。 在这里插入图片描述

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
你好!感谢提问。下面是有关 K210 学习笔记中串口通信的一些内容: 在 K210 开发板上,串口是一种常见的通信接口,用于将数据从一个设备传输到另一个设备。串口通信通常使用 UART (Universal Asynchronous Receiver/Transmitter) 协议。 要在 K210 上进行串口通信,首先需要使用两个引脚:一个用于接收数据(RX),一个用于发送数据(TX)。这些引脚可以通过 GPIO 端口与外部设备连接。 在 K210 的开发环境中,可以使用 MaixPy 或者 Kendryte Standalone SDK 进行串口编程。 在 MaixPy 中,可以使用 `uart` 模块来进行串口通信。例如,要初始化一个串口对象并设置波特率为 115200,可以使用以下代码: ```python from machine import UART uart = UART(UART.UART1, 115200) ``` 然后,可以使用 `uart.write()` 函数发送数据,使用 `uart.read()` 函数接收数据。例如: ```python uart.write("Hello, world!\n") data = uart.read(10) ``` 在 Kendryte Standalone SDK 中,可以使用 `uart.h` 头文件中的函数来进行串口通信。例如,要初始化一个串口对象并设置波特率为 115200,可以使用以下代码: ```c #include "uart.h" uart_init(UART_DEVICE_1, 115200); ``` 然后,可以使用 `uart_send_data()` 函数发送数据,使用 `uart_receive_data()` 函数接收数据。例如: ```c uart_send_data(UART_DEVICE_1, "Hello, world!\n", 14); char buffer[10]; uart_receive_data(UART_DEVICE_1, buffer, 10); ``` 以上是关于 K210 学习笔记中串口通信的简要介绍。如果你有更具体的问题,请随时提问!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值