内核与应用分离

简介: 内核与应用分离

引言

  • 思考:在当前设计中,内核与应用间的界限是否清晰?
  • 很显然,一点也不清晰,内核程序与应用程序统一被编译在一起,应用程序也被当做内核中的一部分
  • 所以本章节我们的重点工作就是将应用程序与内核拆分开来

拆分应用程序

  • 我们来重新设计一下架构,把应用程序从内核中拆分出去

  • 回顾之前的系统启动流程

  • 拆分应用程序后的流程如下

拆分应用程序之代码重构

  • 代码见:app.capp.htask.ctask.hschedule.cmain.c
  • 前面的实验中,我们把任务测试代码都写在了 “main.c” 文件中,这显然是不合理的,现在,我们就来把它从 “main.c” 中移出去
  • 创建 “app” 文件夹,其中新增 “app.c” 文件,创建对应的 “app.h” 文件并放到 “include” 文件夹下,那么最外层 “BUILD.json” 配置文件需要在 “dir” 中增加 “app” 文件夹名,同时在 “app” 文件夹下也要新增一个 “BUILD.json” 配置文件负责管理其下的工程源文件
  • 首先,我们把原本在 “main.c” 中的任务相关代码移到 “app.c” 中
U08 taskA_stack[512];   // 任务私有栈
void TaskAFunc(void)    // 任务执行函数
{
    static U32 count = 0;
    while(1)
    {
        if(count++ % 10000 == 0)
        {
            static U32 j = 0;
            asm volatile("cli");
            SetCursorPos(0, 6);
            printk("TASK A: %d\n", j++); 
            asm volatile("sti");
        } 
    }
}
... 省略任务 B C D
  • 把 app 和 task 分层,即 app 中不直接调用 task 内容,而是把数据传递到 task,task 负责处理这些数据,我们用 APP_INFO 这个数据结构来传递数据,
typedef struct APP_INFO
{
    void        (*pfunc)(void);
    U08*        stackAddr;
    U16         stackSize;
    U08*        name;
    E_APP_PRI   priority;
} APP_INFO;
  • 传递过来的数据放到 TASK_OOP taskOop[MAX_TASK_NUM] 数组中,TASK_OOP 类型定义如下:
typedef struct TASK_OOP
{
    QUEUE_NODE      QueueNode;
    U08             active;
    TASK            task;
} TASK_OOP;
  • 在 app 模块中,我们将应用程序相关数据写到 appInfo 这个数组中
APP_INFO appInfo[MAX_APP_NUM] = {0};
U16 appNum = 0;
E_RET AppRegister(APP_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name, E_APP_PRI priority)
{
    if(appNum >= MAX_APP_NUM)
        return E_ERR;
    appInfo[appNum].pfunc = pFunc;
    appInfo[appNum].stackAddr = stackAddr;
    appInfo[appNum].stackSize = stackSize;
    appInfo[appNum].name = name;
    appInfo[appNum].priority = priority;
    appNum++;
    return E_OK;
}
void AppInit(void)
{
    AppRegister(TaskAFunc, taskA_stack, 512, "TASK A", E_APP_PRI5);
    AppRegister(TaskBFunc, taskB_stack, 512, "TASK B", E_APP_PRI7);
    AppRegister(TaskCFunc, taskC_stack, 512, "TASK C", E_APP_PRI9);
    AppRegister(TaskDFunc, taskD_stack, 512, "TASK D", E_APP_PRI11);
}
  • 在 task 模块中,我们将 appInfo 中的应用数据转移到 task 自己的任务数据区 TASK_OOP taskOop[MAX_TASK_NUM] 数组中,并根据这些数据创建任务并加入就绪任务队列
#include <app.h>
extern APP_INFO appInfo[MAX_APP_NUM];
extern U16 appNum;
void TaskInit(void)
{
    U16 index = 0;
    U16 taskNum = 0;
    QueueInit(&TASK_READY_QUEUE);                  // 就绪任务队列初始化
    QueueInit(&TASK_WAIT_QUEUE);                   // 等待任务队列初始化
    // 创建第一个任务(空闲任务)
    TaskCreat(&taskIdle.task, TaskIdleFunc, taskIdle_stack, IDLE_STACK_SIZE, "Idle", E_TASK_PRI15);
    // 将空闲任务节点添加到就绪任务队列中
    QueueAdd(&TASK_READY_QUEUE, (QUEUE_NODE *)&taskIdle);
    for(index = 0; index < MAX_TASK_NUM && taskNum < appNum; index++)
    {
        if(0 == taskOop[index].active)
        {
            taskOop[index].active = 1;
            taskOop[index].task.name = appInfo[taskNum].name;
            taskOop[index].task.stack_addr = appInfo[taskNum].stackAddr;
            taskOop[index].task.stack_size = appInfo[taskNum].stackSize;
            taskOop[index].task.task_entry = appInfo[taskNum].pfunc;
            taskOop[index].task.priority = appInfo[taskNum].priority;
            TaskCreat(&taskOop[index].task, taskOop[index].task.task_entry, taskOop[index].task.stack_addr, taskOop[index].task.stack_size, taskOop[index].task.name, taskOop[index].task.priority);
            QueueAdd(&TASK_READY_QUEUE, (QUEUE_NODE *)&taskOop[index]);
            taskNum++; 
        }
    }
}
  • 注意:由于任务节点改变, “schedule.c” 中 schedule 函数中也要稍微修改一下
void schedule(void)
{
    ...
    // current_task = (volatile TASK *)((TASK_QUEUE_NODE *)QUEUE_NODE(node, TASK_QUEUE_NODE, QueueNode)->task);
    current_task = (volatile TASK *)&(((TASK_OOP *)QUEUE_NODE(node, TASK_OOP, QueueNode))->task);
    ...
}
  • 注意别忘了任务销毁也要改动
E_RET TaskDestory(void)
{ 
    ...
    ((TASK_OOP *)QUEUE_NODE(node, TASK_OOP, QueueNode))->active = 0;
    ...
}

拆分应用程序之单独编译 app

  • 上面哗啦哗啦写了一大堆,然而实际上应用程序和内核还是编译在一起,现在我们把它们分开,单独编译出应用程序
  • 我们可以单独实现一个编译脚本放到 “app” 文件夹下,专门用于对应用程序部分代码进行编译管理,目前 “app” 文件下目录结构如下:
app
 |--- AppBuild.py
 |--- BUILD.json
 |--- aentry.asm
 |--- app.c
  • 参照 “Build.py”, 实现 “AppBuild.py” ,与 “Build.py” 区别是 “AppBuild.py” 仅实现对 app 的编译和链接工作,见:AppBuild.py
  • 由于使用 “AppBuild.py” 进行编译,所以 “BUILD.json” 配置文件稍微做一下修改,“inc” 项中头文件路径改变,这个是 app 下的头文件路径
{
    "dir" : [
        ],
    "src" : [
        "aentry.asm",
        "app.c"
        ],
    "inc" : [
        "../include"
        ]
}
  • 可以参照 “kentry.asm” 文件实现 “aentry.asm” 文件,作为应用程序入口,其内容如下:
[section .text]
global _start
extern AppInit
_start:
AppEntry:
    push ebp
    mov ebp, esp
    call AppInit
    leave
    ret
  • 注意了,虽然该修改的代码都已经修改了,但是链接时需要用到 “print.o” 这个编译中间文件,由于脚本原因,只链接 “output/app” 下的 “.o” 文件,我们可以手动把 “output” 文件夹下的 “print.o” 文件复制到 “output/app”,这样子再执行脚本就不会出问题了
  • 切换到 “app” 目录下,执行命令:“python AppBuild.py”, 最终生成 “app.bin” 文件

拆分应用程序之内核与应用之间的数据交互

  • 应用程序已经被分离出去了,于是,又产生了新的问题,内核与应用之间的数据又该如何交互呢?比如内核 “task.c” 中就需要获得 app 中下面的两个数据

APP_INFOappInfo[MAX_APP_NUM];

U16appNum;

  • 原本是统一编译成一个程序,直接使用 “extern” 关键字即可,现在内核与应用分离,肯定不能用 “extern” 关键字了。
  • 解决办法:共享内存
  • 既然说到共享内存了,那么顺便也将 app 程序的加载地址也考虑一下,重新规划一下内存使用,把内核与应用间共享数据定义在 0xA800 位置,把 app 加载到地址 0x80000,注意这个地址不要大于 1M,因为在实模式下 x86 处理器能访问的最大地址就是 0xFFFFF(1M内),我一开始就规划过大,发现始终无法将 app 程序加载到那个内存地址处
  • 注意 “AppBuild.py” 中链接地址也要修改为 0x80000

  • 先来考虑内核与应用之间共享内存的使用
  • 在 “app.c” 中往贡献内存中写数据
void AppInit(void)
{
    ...
    // 把应用数据放入共享内存 0xA800 处
    *((volatile U32*)0xA800) = appInfo;
    *((volatile U32*)0xA804) = appNum;
    ...
}
• 在内核 “task.c” 中读共享内存数据
void TaskInit(void)
{
    ...
    APP_INFO* pAppInfo = (APP_INFO *)(*(U32 *)APP_INFO_ADDR);
    U32 appNum = *((U32*)(APP_NUM_ADDR));
    ...
}
  • 想要内核能跳转到应用程序中执行,我们可以使用函数指针的形式实现

typedefvoid(*APP_INIT)(void);

APP_INITAppInit=(APP_INIT)0x80000;   // 0x80000: 应用程序加载地址

AppInit();                              // 应用程序模块初始化

  • 注意:由于内核与应用分离,根目录下的 “BUILD.json” 配置文件中 “dir” 项应去掉 “app”,因为 app 相关工程代码已经在前面单独编译了
  • 代码见:app.ctask.cshare.hmain.c

拆分应用程序之加载应用程序

  • 目前我们已经能利用 “AppBuild.py” 成功编译出 app.bin 程序了,接下来就要把 “app.bin” 写到 “a.img” 中,前面已经实现了将 boot、loader 以及 kernel 写入 “a.img” 中,现在写 app 也是类似的,具体就不再详细介绍了,代码实现见:Build.py
  • “a.img” 制作成功之后,接下来就是在 “loader.asm” 中将 app 数据读到内存 0x80000 地址处,这里有个需要说明的就是 0x80000 地址超过了 16 位寄存器范围,而 rd_disk_to_mem 函数中使用 bx 寄存器传参,想要访问地址 0x80000 ,于是将 ds 段基址寄存器赋值为 0x8000, mov [bx], ax 这条指令其实相当于 mov [ds:bx], ax,ds:bx 这种表示方法其实就等同于 ds*16+bx,ds*16 就等于 ds 左移 4 位,0x8000 左移 4 位即 0x80000
; 将硬盘扇中 app 数据读入到内存  0x80000 处 
mov ax, [0x700]     ; loader 所占扇区数
add ax, [0x702]     ; + kernel 扇区数
add ax, 2           ; + 2 得到 app.bin 起始扇区 
mov cx, [0x704]
; 因为 app 加载地址 0x80000 超过 0xFFFF,通过改动段基址 ds = 0x8000 实现访问内存地址 0x80000
mov dx, 0x8000
mov ds, dx
mov bx, 0x0000
call rd_disk_to_mem
; 需恢复 ds=0, 下面的程序需要 ds 为 0
mov dx, 0x0
mov ds, dx
  • 到这里程序就已经完成了,为了测试,我们稍微修改一下 “aentry.asm”
_start:
AppEntry:
    mov eax, 0x8899      ; 仅用于调试
    mov ebx, 0x5566      ; 仅用于调试
    jmp $                ; 仅用于调试
    push ebp
    mov ebp, esp
    call AppInit
    leave
    ret
  • 好了,跑起来看一看,当然,啥也看不见,程序死在了上面 app 入口的 jmp $ 处
  • 使用 Ctrl+C 退出,使用 “reg” 指令,得到:
eax: 0x00008899 34969
ecx: 0x000007f8 2040
edx: 0x00000000 0
ebx: 0x00005566 21862
esp: 0x00007bdc 31708
ebp: 0x00007be8 31720
esi: 0x00000b04 2820
edi: 0x000009ac 2476
eip: 0x0008000a
eflags 0x00003002: id vip vif ac vm rf nt IOPL=3 of df if tf sf zf af pf cf
  • 从中我们可以看出,eax,ebx 的值已被成功修改为我们想要的值,这说明目前程序已经能从 kernel 跳转到 app 执行了
  • 代码见:loader.asmaentry.asm

异常处理

  • 去掉 “aentry.asm” 中的调试代码,跑起来
_start:
AppEntry:
    push ebp
    mov ebp, esp
    call AppInit
    leave
    ret
  • 果然没有想象中的美好,总要出点问题,貌似只有空闲任务运行,其它任务都没运行起来

  • 从 app 入口开始往里查,程序从 AppEntry 执行到 AppInit,我们在 AppInit 函数开头放个打印信息看看程序有没有进来执行注册 app 数据
void AppInit(void)
{
    printk("AppInit\n");
    ...
}
  • 这回运行一下,现象更奇特了,注意了,注意了,不仔细看差点没发现,打印第一行 “Boot...” 消失了

  • 初步排除 boot 部分代码问题,因为成功加载了 loader,左思右想,那就是打印第一行被其它打印覆盖了,那么肯定怀疑刚加的 AppInit 函数中的打印
  • 继续思考,app 中调用的 print 打印相关函数跟 kernel 中是调用同一个函数吗?
  • 很显然,由于我们独立编译了 app,链接时还复制了 print.o 文件,这说明内核 print 相关函数与 app 的 print 相关函数用的不是同一个地址,仅仅只是名字相同而已,kernel 和 app 中各有一份
  • 于是在 app 中初始化一下,增加打印颜色和设置光标位置,因为我怀疑默认的打印颜色正好是黑色,所以打印出来的字符串看不到
void AppInit(void)
{
    SetCursorPos(0, 3);                     // 设置光标位置: (0, 3)
    SetFontColor(E_FONT_WHITE);             // 设置打印字体颜色: 白色
    printk("AppInit\n");
    ...
}
  • 再次运行看一下现象,果然如上面猜想的一样,这回成功打印出 “AppInit” 字符串了,但是其它几个任务依旧没有运行起来

  • 顺着程序运行往下查呗,接下来就是把应用数据放入共享内存 0xA800 处,这个代码并没有什么问题,那么就是 “task.c” 中获取应用数据出问题了
  • 一看,果然,共享内存地址定义出错了
// #define APP_INFO_ADDR           (SHARE_START_ADDR + 0xA800)
// #define APP_NUM_ADDR            (SHARE_START_ADDR + 0xA804)
#define APP_INFO_ADDR           (SHARE_START_ADDR + 0x800)
#define APP_NUM_ADDR            (SHARE_START_ADDR + 0x804)
  • 修改完成,再次编译运行,哈哈哈,这回终于 OK 了

  • 本次异常问题解决相关代码见:aentry.asmapp.cshare.h
目录
相关文章
|
2月前
|
虚拟化
操作系统体系结构和内存分层
操作系统体系结构和内存分层
17 0
|
4月前
|
Linux API
Linux驱动的软件架构(三):主机驱动与外设驱动分离的设计思想
Linux驱动的软件架构(三):主机驱动与外设驱动分离的设计思想
39 0
|
5月前
|
存储 监控 算法
【操作系统】—处理机调度的概念以及层次
【操作系统】—处理机调度的概念以及层次
【操作系统】—处理机调度的概念以及层次
|
6月前
|
存储 安全 API
3.5 Windows驱动开发:应用层与内核层内存映射
在上一篇博文`《内核通过PEB得到进程参数》`中我们通过使用`KeStackAttachProcess`附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。
61 0
3.5 Windows驱动开发:应用层与内核层内存映射
|
6月前
|
存储 缓存 算法
解密Linux中的通用块层:加速存储系统,提升系统性能
本文探讨了Linux操作系统中的通用块层和存储系统I/O软件分层的优化策略。通用块层作为文件系统和磁盘驱动之间的接口,通过排队和调度I/O请求,提高磁盘的读写效率和可靠性。存储系统的I/O软件分层包括文件系统层、通用块层和设备层,它们相互协作,实现对存储系统的高效管理和操作。本文旨在深入了解通用块层和其他I/O软件层的功能和作用,分析优化存储系统的管理和操作,提升系统性能和可靠性。
解密Linux中的通用块层:加速存储系统,提升系统性能
|
9月前
|
Linux
Linux驱动入门(5)LED驱动---驱动分层和分离,平台总线模型
Linux驱动入门(5)LED驱动---驱动分层和分离,平台总线模型
63 0
|
10月前
|
缓存 前端开发 调度
根据Nehalem架构了解CPU内部细节
根据Nehalem架构了解CPU内部细节
199 0
|
存储 架构师 算法
架构设计的本质:系统与子系统、模块与组件、框架与架构
在软件研发这个领域,程序员的终极目标都是想成为一名合格的架构师。然而梦想很美好,但现实却很曲折。
架构设计的本质:系统与子系统、模块与组件、框架与架构
|
存储 缓存 安全
操作系统—底层工作的整体认识(一)
操作系统—底层工作的整体认识(一)
190 0
操作系统—底层工作的整体认识(一)