我们都知道操作系统的基本功能就是对进程,任务进行管理,使用户在无感的情况下进程任务切换,本章节我们将在之前hello world程序基础上为我们的OS添加一个任务。
任务
任务是操作系统的基本管理单元,所有的进程切换,调度等都是以任务为单位的。任务需要管理的内容非常的多。包括栈、调度策略、VMA、任务运行时间等等。本次我们只添加基础的任务需要的元素,后续随着功能的添加我们将在其中添加更多的元素。
下边是task的数据结构和结构函数:
rdy_task_list用于管理已经就绪的任务,本次实验只有一个。
hf_task_create用于创建一个任务。
//任务的运行在状态,暂时未用到
#define HF_TASK_READY 0x1UL
#define HF_TASK_SUSPEND 0x2UL
#define HF_TASK_SLEEP 0x3UL
#define HF_TASK_DELETED 0x4UL
//任务的执行函数
typedef void *(*task_func)(void *arg);
//全局的任务列表
extern hf_list_t *rdy_task_list;
typedef struct hf_task {
void *task_stack; //栈地址,放在第一个位置上,方便汇编代码获取。
//这是个动态的指针会根据程序运行动态变化
void *stack_start; //记录栈的起始位置,不会变(栈向下增长,因此是个地址)
size_t stack_size; //栈的大小
char task_name[64]; //任务名
void *arg; //任务运行的函数参数
task_func func; //运行函数
unsigned long task_state; //当前任务的状态
hf_list_t task_list; //挂载的链表
} hf_task_t;
int hf_task_create(hf_task_t *task, const char *name, void *task_stak,
size_t stack_size, task_func func, void *arg);
int hf_task_delete(hf_task_t *task);
实现一个printk
为了方便内核中打印,我们增加了hf_printk.函数。类似于C库的printf函数。
在这个函数中定义了一个printk_output的函数指针,用于进行输出函数的保存。当系统初始化后就可以调用注册函数将uart的发送函数注册为输出函数,这样打印的信息就能通过串口输出了。
static printk_output_t printk_output = NULL;
/* 设置prink输出的函数 */
int hf_set_printk_output(void *output)
{
printk_output = output;
return 0;
}
/* 打印信息到出口 */
int hf_printk(const char *fmt, ...)
{
char buf[512] = {0};
va_list ap;
int ret = 0;
va_start(ap, fmt);
vsnprintf(buf, 512, fmt, ap);
va_end(ap);
if (printk_output) {
ret = printk_output(buf);
}
return ret;
}
添加常用的组件
双向链表:
双向链表在内核中是最常用的数据管理结构。通常用来将task等对象关联起来,方便查找。这里提供基础的插入、添加、删除等功能。
C库函数:
C库中有些比较常用的函数在内核中也用得到,因此我们会将部分函数移植到内核中来(目前自己写的大部分以功能为主,并没有开率效率的问题,后续有时间再做效率优化)。
ARMV8的异常处理:
接下来这一小节是ARMV8体系结构的内容。
这里以EL0为例,当异常发生的时候CPU会做如下的事情(自动执行,用户无感,此处仅描述寄存器相关的操作,更具体的查看手册):
1)把PSTATE寄存的值存储到SPSR_EL1中。
2)把返回地址(当前的PC)存储到ELR_EL1中。
当调用eret时,可以从异常处理中返回:
1)从ELR_EL1中恢复PC指针
2)从SPSR_EL1中恢复PSTATE的寄存器状态
任务加载
利用ARMV8的异常处理流程,我们可以进行任务的加载。模拟一次异常,将SPSR的值和ELR的值分别更填写上异常值和task->func,再将task->task_stack赋值给SP。最后调用eret指令,CPU就会将SPSR和ELR的值自动加载到PSTATE和PC指针中去。这样就实现了任务的加载。
.global arch_run_task
.type arch_run_task, function
.align 8
.text
arch_run_task:
msr elr_el1, x1 //task->func
mov x9, 0x05
msr spsr_el1, x9 //spsr = 0x5 = el1h
ldr x9, [x0]
mov sp, x9 //栈地址设置为task->task_stack
mov x0, x2 //task->arg放到X0里
eret
/*
//也可以实现跳转
ldr x9, [x0]
mov sp, x9
mov x0, x2
blr x1
*/
.end
问题总结
1)代码编译链接问题
编译的过程遇到了一个链接的问题,这里会提示找不到汇编的这个函数。
使用objdump查看是能够发现该函数的。
修改cmake的链接顺序后解决该问题。(需要查下camke的手册找下原因)
//原来的链接顺序
target_link_libraries(hfOS_${BOARD}.elf
PRIVATE -nostdlib
k_arch
k_lib
k_core
)
//修改后的链接顺序
target_link_libraries(hfOS_${BOARD}.elf
PRIVATE -nostdlib
k_lib
k_core
k_arch
)
2)arch_run_task汇编无法正常运行问题
调用eret后SP和PC的值都是0,无法正常跳转。使用GDB进行调试,观察调用eret之后的数值,发现SPSR_EL1和ELR_EL1数值都正常。但就是没有正常跳转。
问题分析思路:
既然没有跳转,那么需要分析跳转前后的系统状态,因此分别dump出来跳转前后的寄存器值进行对比,对比后发现eret之后SP_EL2的数值居然发生了变化,那是不是意味着我们当前运行在EL2呢?
有了EL2的怀疑之后在boot代码中添加了汇编代码去确认,最终确定确实运行在EL2。
解决办法:
既然是运行在EL2,需要我们将异常等级转换到EL1。参照异常的处理机制,同样使用eret实现异常等级的切换。在boot.s中添加汇编代码,最终解决该问题。
_el_process:
mrs x0, CurrentEL
ubfx x0, x0, 2, 2
cmp x0, 0x2
b.eq _el2_process
ret
_el2_process:
// (SPRS_DEBUG_MASK | SPRS_SERR_MASK | SPRS_IRQ_MASK | SPRS_FIQ_MASK | SPRS_M_AARCH64 | SPRS_M_EL1H)
mov x2, #0x3c5
msr spsr_el2, x2 //设置spsr_el2, eret返回后使用sp_el1
mov x0, #(1 << 31)
msr hcr_el2, x0 //设置EL1的运行状态为AArch64
isb
msr elr_el2, lr //设置跳转到EL1后的PC
eret
运行效果:
git clone https://gitee.com/genglufei/hfos.git
cd hfos/day2_task/hfOS/vendor
./build_hfos.sh qemu_a57
./run_hfos.sh
下一节,我们将尝试进行任务切换。