引入系统调用

简介: 引入系统调用

引言

  • 回顾前面章节,我们成功的实现了多任务并行执行,然而这些任务都是一直循环执行,那么系统中的所有任务都是一直不停的执行的吗?

思考

  • 任务显然不是必须一直执行的,那么任务执行结束后,又返回到哪里呢?
  • 回顾前面所讲的恢复上下文示意图中 ③ eip 寄存器保存任务入口地址,然后通过 iret 指令跳转到任务入口执行

  • 很显然,在跳转前,我们并没有将跳转前程序执行地址入栈保存,所以 iret 跳转后是程序没办法返回的
  • 可见,当前我们已实现的方案是通过 iret 直接跳转到入口函数执行,并非函数调用,因此无法返回

如何实现返回

  • 为什么需要任务返回?
  • 任务结束后,我们需要释放任务占用资源,而如何知道任务执行结束呢?答案就是通过任务返回
  • 想要破局实现返回,我们可以构造出来一个函数调用,借助函数调用来实现返回
  • 听上去很抽象,别着急,我们一点一点具体实现
  • 先在 TASK 结构体中新增 TASK_FUNC task_entry 成员
typedef struct TASK
{
    U32             id;                 // 任务 id
    U08*            name;               // 任务名称
    U08*            stack_addr;         // 任务栈基址
    U16             stack_size;         // 任务栈大小
    REG             reg;                // 任务上下文,即任务执行状态下所有寄存器的值
    TASK_FUNC       task_entry;         // 任务函数入口
} TASK;
  • 在 “task.c” 中新增一个函数 TaskEntry
static void TaskEntry(void)
{
    if(current_task)
    {
        current_task->task_entry();
    }
    printk("%s end!!!\n", current_task->name); // 调试
    // 任务销毁工作
    while(1);
}
  • TaskEntry 函数具体是怎么使用的呢?见 TaskCreat 函数
E_RET TaskCreat(TASK* task, TASK_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name)
{
    ...
    // task->reg.eip = (U32)pFunc;
    task->reg.eip = (U32)TaskEntry;
    task->task_entry = pFunc;
    ...
}
  • 现在的程序逻辑就变成了所有任务的入口都是 TaskEntry 函数,在 TaskEntry 函数中再借助
current_task->task_entry(); 跳转到各自任务函数执行
  • 测试一下,让 TASK D 任务函数 TaskDFunc 循环几次后就跳出循环,测试代码见:task.ctask.hmain.c
void TaskDFunc(void)    // 任务执行函数
{
    static U32 count = 0;
    while(1)
    {
        if(count++ % 100000 == 0)
        {
            static U32 j = 0;
            asm volatile("cli");
            SetCursorPos(0, 12);
            printk("TASK D: %x\n", j++); 
            asm volatile("sti");
        }  
        if(count > 1000000) // 跳出循环
            break;
    }
}
  • 测试结果,TASK D 执行结束,其它任务仍在运行

引入系统调用

  • 回到 TaskEntry 函数,前面的实验中我们并没有具体实现任务销毁工作,仅仅只是写了个注释,然后 while(1) 循环
  • 在实现销毁任务前还需要深入思考一下,在什么情况下才能销毁任务呢?答案:0 特权级,也就是内核态才能执行销毁任务操作
  • 思考:TaskEntry 函数执行的特权级是多少? 0 还是 3
  • 显然,TaskEntry 是任务入口,那么其必定工作在 3 特权级,那么,其不具备销毁任务的操作权限
  • 问题又来了,怎么跳转到 0 特权级呢?
  • 答案,哈哈,终于说到点子上了,那就是通过中断跳转到 0 特权级,我们可以使用 0x80 号软中断实现跳转,于是,引入了系统调用

实现第一个系统调用

  • 系统调用是任务与内核之间的交互接口
  • 涉及特权级的转换(DPL3 --> DPL0)
  • 接下来我们将通过 0x80 号中断(软中断)实现系统调用
  • 代码见:syscall.csyscall.htask.cinterrupt.asm
  • 创建 “syscall.c” 和 “syscall.h” 文件,其用于系统调用相关代码,这个不用再多说了
  • 在 TaskEntry 函数中添加如下代码触发 0x80 号软中断

asmvolatile("int $0x80\n");

  • 不过,需要注意的是,在 “interrupt.asm” 的中断入口 Int0x80_Entry 也需要保存并恢复上下文
extern Int0x80Handle
Int0x80_Entry:
    ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存
    pushad                      ; 保存通用寄存器
    push ds
    push es
    push fs 
    push gs
    mov esp, KERNEL_STACK       ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 
    call Int0x80Handle          ; 中断逻辑功能
    mov esp, [current_reg]      ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
    ; 恢复上下文 
    pop gs
    pop fs
    pop es
    pop ds   
    popad                       ; 恢复通用寄存器
    iret
  • 保存上下文和恢复上下文的代码在 Int0x20_Entry 中断入口已经写过一遍了,本着代码不重复的原则,使用汇编宏优化一下代码
%macro BeginISR 0
    ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存
    pushad                      ; 保存通用寄存器
    push ds
    push es
    push fs 
    push gs
    mov esp, KERNEL_STACK       ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 
%endmacro
%macro EndISR 0
    mov esp, [current_reg]      ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
    ; 恢复上下文 
    pop gs
    pop fs
    pop es
    pop ds   
    popad                       ; 恢复通用寄存器
    iret
%endmacro
Int0x20_Entry:
    BeginISR
    call Int0x20Handle          ; 中断逻辑功能
    EndISR
...
Int0x80_Entry:
    BeginISR
    call Int0x80Handle          ; 中断逻辑功能
    EndISR
  • 中断都已经触发,然而对应的中断服务程序还没实现,我们把其放到 “syscall.c” 文件中
void Int0x80Handle(U16 ax)
{
    SetCursorPos(0, 14);
    printk("Int0x80Handle\n");
}
  • 万事具备,编译运行看看结果
  • 很不幸,程序崩溃了, bochs 提示如下:

interrupt(): soft_int && (gate.dpl < CPL)

  • 一看就是特权级的问题,查看一下 “loader.asm”, 发现其中中断描述符表 IDT 的属性特权级为 DA_DPL0,应改为 DA_DPL3
  • 再次编译运行,这回终于成功实现打印

  • 别着急,这还仅仅是调通了 0x80 号软中断,想要区分不同的系统调用就必须给 Int0x80Handle 函数传参,TaskEntry 函数中触发 0x80 号中断的代码就要改成如下方式,利用 ax 寄存器传参
asm volatile(
        "movw $0, %%ax\n"
        "int $0x80\n"
        : : : "ax" // 告诉 gcc 编译器,ax 寄存器被内嵌汇编使用,需要 gcc 自动添加保护和恢复操作(入栈和出栈)
    );
  • 根据 C与汇编混合编程 中的 C 调用约定,汇编作为主调者,要从右到左入栈,函数调用后还要负责恢复栈,于是 Int0x80_Entry 中断入口处代码如下
Int0x80_Entry:
    BeginISR
    push ax
    call Int0x80Handle          ; 中断逻辑功能
    add esp, 4
    EndISR
  • 中断服务程序逻辑代码 Int0x80Handle 中再以 ax 为参数执行不同的函数调用
void Int0x80Handle(U16 ax)
{
    if(0 == ax)
    {
        SetCursorPos(0, 14);
        printk("Kill Task\n");
    }
}
  • 嗯~,到最后我们都没有真正实现销毁任务代码,哈哈,只以打印字符串 "Kill Task" 替代,这是因为目前我们的主要目的是实现系统调用。关于任务销毁的具体实现,待后面章节实现
  • 最后看一下代码执行效果

目录
相关文章
|
3月前
|
监控 网络协议 Linux
彻底解密:select,poll底层系统调用的核心思想原理
彻底解密:select,poll底层系统调用的核心思想原理
|
API 开发工具
【Pintos】实现自定义 UserProg 系统调用 | 添加 syscall-nr 系统调用号 | 编写新的参数调用宏
【Pintos】实现自定义 UserProg 系统调用 | 添加 syscall-nr 系统调用号 | 编写新的参数调用宏
129 0
|
7月前
|
缓存 Linux 编译器
C/C++ 函数调用以及Linux中系统调用 开销介绍:介绍C/C函数调用以及Linux中系统调用的开销情况
C/C++ 函数调用以及Linux中系统调用 开销介绍:介绍C/C函数调用以及Linux中系统调用的开销情况
113 0
|
7月前
|
算法 Unix Linux
进程原理及系统调用
进程原理及系统调用
|
7月前
|
安全 Java 程序员
“系统调用”究竟是不是个函数?
- **系统调用**和普通**函数**有何区别? - 什么是**内核态** 和 **用户态**? - 操作系统如何让CPU切换状态? - 内中断、外中断、软中断、硬中断是什么意思? - 库函数和系统调
105 0
|
算法 Linux 调度
进程原理及其系统调用(上)
进程原理及其系统调用
138 0
|
Unix Linux
进程原理及其系统调用(下)
进程原理及其系统调用
85 0
|
缓存 数据格式 流计算
xawtv涉及的vivid系统调用分析
xawtv涉及的vivid系统调用分析
202 0
|
缓存 Linux API
系统编程之文件IO(七)——0,1,2三个文件描述符与库函数和系统调用的区别
系统编程之文件IO(七)——0,1,2三个文件描述符与库函数和系统调用的区别
113 0
系统编程之文件IO(七)——0,1,2三个文件描述符与库函数和系统调用的区别