一个极简操作系统的代码实现

一个极简操作系统的代码实现

在网上看的demo OS实现时,发现一个名为Hurlex的demo OS project,实现精简,麻雀虽小五脏俱全,挺适合对OS实现进行代码级别的快速粗略了解一下的。

当然,高校也有类似的很不错的教学项目,比如MIT Xv6 OS哈工大李治军Linux-0.11 OS实验,这些有更加详细的配套文档和项目代码,更加适合深入的学习。

上面的demo OS都可以通过QEMU仿真器直接跑起来并且能调试,可以看用QEMU调Linux:
使用QEMU调试ARM64 Linux内核v6.0.9

Hurlex OS简介

Hurlex是一个支持IA32(x86架构)demo操作系统,项目相关信息如下:

项目仓库: https://github.com/hurley25/Hurlex-II
文档仓库:https://github.com/hurley25/hurlex-doc
在线文档: https://wiki.0xffffff.org/

看了下代码规模,也就8k出头的代码规模:

 ~/Hurlex-II $ find -name *.[chs] | xargs wc -l
    ...
    8218 total

OS如何启动

一个OS要启动,仅仅是OS本身还不够,还需要BIOS/Bootloader等,它们的核心作用就引导操作系统系统去启动。

打开电源后,BIOS开机自检,确定启动设备,安装启动设备启动设备上面安装的GRUB开始引导Linux,Linux首先先进行内核引导,通过跟切换,执行init程序,init程序确定启动级别,根据启动级别进行系统初始化和运行的服务,然后返回init启动终端,用户通过验证成功登陆Shell,这就是一个从开机到登陆的启动过程。

一个简化的Bootloader的伪代码流程示例,用于展示整个Bootloader的执行过程:

// Bootloader代码开始执行
bootloader_entry():
    hardware_init() // 初始化硬件和设备
    load_config_file() // 加载配置文件
    display_boot_menu() // 显示启动菜单
    wait_for_user_input() // 等待用户选择
    load_kernel_to_memory(selected_kernel) // 根据用户选择,加载相应的内核文件到内存
    jump_to_kernel_entry() // 跳转到内核入口点,启动内核

Hurlex OS实现

内核初始化

内核运行首先也是初始化部分,刚开始还没有栈,也是汇编代码执行,栈构造完毕进入到C代码部分。

这块看 Hurlex相关的源码如下:

1. 初始化(汇编阶段)

主要是就是构造了栈,然后让PC寄存器跳转到内核入口的C函数部分。

arch/i386/init/init_s.s  // <-- 初始化(汇编代码)
    [GLOBAL start] //内核代码入口,此处提供该声明给 ld 链接器
start:
    mov [mboot_ptr_tmp], ebx        // 将 ebx 中的指针存入 mboot_ptr_tmp
    mov esp, STACK_TOP              // 设置内核栈地址
    and esp, 0FFFFFFF0H             // 栈地址按照 16 字节对齐
    mov ebp, 0                      // 帧指针修改为 0

    call kern_entry  // 进入内核入口函数(C代码)
2. 初始化(C程序入口)

这个内核入口,进入C函数部分。

虚拟的页面每页占据4KB,按页为单位进行管理。物理内存也被分页管理,按照4KB分为一个个物理页框。虚拟地址到物理地址通过由页目录和页表组成的二级页表映射,页目录的地址放置在CR3寄存器里

arch/i386/init/init.c // 初始化(C程序)
kern_entry:
    mmap_tmp_page // 映射临时页表
        // 指定物理地址写
        // 映射 0x00000000-0x01000000 的物理地址到虚拟地址 0xC0000000-0xC1000000
        // 设置临时页表, 设置 cr3 寄存器
        __asm__ volatile ("mov %0, %%cr3" : : "r" (pgd_tmp));
    enable_paging  // 启用分页
    // 切换临时内核栈到分页后的新栈
    __asm__ volatile ("mov %0, %%esp\n\t" "xor %%ebp, %%ebp" : : "r" (kern_stack_top));
    // 更新全局 multiboot_t 指针指向
    glb_mboot_ptr = (multiboot_t *)((uint32_t)mboot_ptr_tmp + PAGE_OFFSET);
    kern_init() // <-- kmain.c
        arch_init
            gdt_init   // 初始化全局描述符表
            idt_init   // 初始化中断描述符表
            clock_init // 注册时间相关的处理函数, 时间片超时函数
        mm_init
            pmm_init  // 物理内存页初始化
                phy_pages_init
            vmm_init  // virtual memory init
                // 注册页错误中断的处理函数
                // 构造页目录
                // 构造页表映射,内核 0xC0000000~0xF8000000 映射到物理 0x00000000~0x38000000
            slob_init // slob 分配器初始化
        task_init // task管理 -- process/thread
            *idle_task = (struct task_struct *)kern_stack;
            idle_task->pid = alloc_pid();
            idle_task->need_resched = true;
            idle_task->stack = (void *)kern_stack_top;
            list_add(&idle_task->list, &task_list);
            register_interrupt_handler(0x80, syscall_handler); // 注册系统调用中断
        fs_init
            for(;;) 
                cpu_hlt(); //  __asm__ volatile ("hlt"); , cpu 空载

初始化函数完成,这个有点像一个进程main函数执行完毕,后面就是循环执行 hlt 让CPU空载,后续都是时钟中断等来触发调度,来执行可以执行的task了。调度的时机也有很多:时钟中断、任务主动让出exit或者sleep、内核空间返回用户空间根据条件判断等。

3. 时钟中断

内核初始化后,后续就会有时钟中断来定期触发调度了,去寻找可执行的任务来运行等。

clock_init
    register_interrupt_handler(IRQ0, clock_callback); // 注册时间片中断
||    
clock_callback
    schedule // 触发调度
    // 找到可运行的任务,并递增时间片

任务管理

OS通常这么说进程和线程:进程是资源管理(内存权限等)的最小单位,线程是调度的最小单位。Linux早起也只有进程,后面才有了线程的支持。

线程本质是一个执行流,硬件资源对应占用一个cpu core的硬线程,有一组寄存器和栈,加上对应的地址空间,即可执行。多个执行流,可以共享一部分地址空间资源,从而减少切换开销。

内核线程是内核中的一个执行流,共享内核空间全局内存信息,特权等级工作在内核态。

schedule

任务调度:主要就是找到下一个可执行的task,然后切换 上下文(Context switch),主要切换栈和PC,关键寄存器值修改一下即可,就可以执行新的任务了。

// 触发调度
schedule()  // <-- 时间片超时会调用到, important
    // find task
    task_next->runs_time++; // 时间片增加
    if (task_next != current)
        task_run(task_next);
            // 切进程页表
            switch_to(&prev->context, &next->context);
            // 执行栈切换
            // 这个一定是汇编代码,因为切换函数调用栈,是无法用C语言表达出来的

图示:
ctx-switch

fork

任务创建:新建task_struct结构体 (PCB, Process Control Block) , 申请进程PID, 设置地址空间和状态,设置task状态等。

// arch/i386/task/task.c
do_fork
    task_struct *task = alloc_task_struct()
    copy_mm(clone_flags, task)
    copy_thread(task, pt_regs); // copy register
    // interrupt proc
    task->pid = alloc_pid();
    list_add(&task->list, &task_list);
    // interrupt proc
    wakeup_task(task);
        task->state = TASK_RUNNABLE;

exit

任务退出:释放资源,PCB/PID等,让出CPU,触发一次新的调度。比如,用户态程序main函数执行完毕就会调用到exit调用。

do_exit
    current->state = TASK_ZOMBIE; // 进程状态
    current->need_resched = true;
    free_pid(current->pid);
    cpu_idle()
        if (current->need_resched)
            schedule()

内存管理

物理地址

物理内存管理: Physical Memory Management. 通常物理内存按页来管理,物理地址也是全局空间固定的。

// arch/i386/mm/pmm.c
struct pmm_manager {
    const char *name;                                // 管理算法的名称
    void (*page_init)(page_t *pages, uint32_t n);    // 初始化
    uint32_t (*alloc_pages)(uint32_t n);             // 申请物理内存页(n为字节数)
    void (*free_pages)(uint32_t addr, uint32_t n);   // 释放内存页
    uint32_t (*free_pages_count)(void);              // 返回当前可用内存页
};
pmm_manager *pmm_manager = &ff_mm_manager;  // First-Fit 算法内存管理

First-Fit 算法是一种常见的连续内存分配算法,用于管理可用的物理内存空间。核心思想是将进程的内存需求分配给满足要求的第一个合适的可用内存块,基本工作流程:

  1. 初始化内存空间:将整个物理内存空间划分为多个可用的内存块,每个内存块都有一个大小和状态(已分配或未分配)。
  2. 请求内存:请求分配一定大小的内存时,First-Fit算法会从头开始遍历可用内存块列表,寻找第一个满足大小要求的空闲块。
  3. 分配内存:找到满足要求的空闲块后,算法将该块标记为已分配,并将所需的内存分配给进程。
  4. 更新内存块状态:根据实际分配情况,可能需要更新相应的内存块状态信息,例如更新块的大小和剩余空间。
  5. 返回分配结果:将分配的内存块的起始地址返回给进程,进程可以使用该地址来访问已分配的内存。

在实际应用中,First-Fit 算法常用于简单的内存管理系统或具有较少内存碎片问题的场景。对于需要更高内存利用率和更好的碎片管理的系统,可以考虑其他算法如最佳适应算法(Best Fit)或 伙伴算法(Buddy Algorithm)

虚拟地址

虚拟地址管理: Virtual Memory Management. 虚拟地址映射物理地址,申请物理页面,缺页处理等。

关键数据结构:

// 任务虚拟内存区间
struct mm_struct {
    pgd_t *pgdir;
    int vma_count;
    struct list_head vma_list;
};

struct vma_struct {
    struct mm_struct *mm;
    uint32_t vm_start;
    uint32_t vm_end;
    uint32_t vm_flags;
    struct list_head list;
};

关键处理:

//arch/i386/mm/vmm.c
vmm_init
    register_interrupt_handler(INT_PAGE_FAULT, &do_page_fault); // 注册页错误中断的处理函数
    // 页表数组指针
    // 构造页目录(MMU需要的是物理地址,此处需要减去偏移)
    // 构造页表映射
    // switch_pgd((uint32_t)ka_to_pa(pgd_kern));

// 物理地址转换内核虚拟地址
static inline void *pa_to_ka(void *pa)
    return (void *)((uint32_t)pa + KERNBASE);

map(pgd_t *pgd_now, uint32_t va, uint32_t pa, uint32_t flags)
    pte_idx = PTE_INDEX(va);
    pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
    if (!pte)
        pte = (pte_t *)alloc_pages(1); // 物理地址管理
        pte = (pte_t *)pa_to_ka(pte);
    tlb_reload_page(va);
        __asm__ volatile ("invlpg (%0)" : : "a" (va)); // CPU 更新页表缓存

文件系统

VFS(Virtual File System)是操作系统中的一个抽象层,用于统一管理不同文件系统的访问和操作。它提供了一套通用的接口和数据结构,使应用程序和操作系统能够以统一的方式访问和操作各种不同类型的文件系统,如ext4、NTFS、FAT等。VFS提供的主要通用接口和数据结构如下:

  1. 文件操作函数接口:打开、文件、写入、关闭等操作函数接口,用于对文件进行操作和访问。
  2. 文件结构体:表示打开的文件,包含与文件相关的信息,如关联的iNode、当前文件偏移量等。
  3. iNode(索引节点)结构体:表示文件或目录的元数据,包含文件模式、所有者信息、时间戳、文件类型等。
  4. 目录项结构体:表示目录中的一个条目,包含目录项名称和关联的 iNode。
  5. 超级块结构体:表示文件系统的元数据,包含根目录的 iNode、文件系统参数等。
  6. 文件系统操作函数接口:包括文件系统的挂载、卸载等操作函数接口,用于管理文件系统。
  7. 路径解析:提供函数用于解析文件路径,将路径转换为对应的iNode。

通过这些接口和数据结构,应用程序可以通过统一的方式对文件系统进行操作,无论是访问普通文件、目录还是其他文件类型。VFS 将底层文件系统的差异隐藏在后端,使应用程序能够以统一的方式与不同文件系统交互。

//fs/vfs.c
vfs_init
    init_mount_tree(mount); // 初始化VFS目录树
        add_filesystem(&fs_ramfs); // 添加根文件系统
        super_block *sb = alloc_super_block(); // 获取根文件系统的超级块结构
        inode *inode = alloc_inode(); // 初始化根结点 inode
        dentry *dentry = alloc_dentry(); // 初始化根结点 dentry

总结

想深刻理解操作系统而不是仅仅停留在概念,需要对应硬件体系结构有一定了解,然后才能明白OS很多核心部分到底是做什么的,为什么那么设计。比如:汇编指令集架构,函数调用约定,中断控制器,MMU,关键系统寄存器作用,特权或异常等级等。了解了这些,才能对OS课本上介绍的各种术语概念有更加深刻的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
响应式极简新闻发布系统是基于ASP ACCESS/MSSQL开发的响应式网站系统,可同时兼容PC端及移动端,也可以通过安装插件来关闭手机端。可以在后台安装大量插件与模板,一键安装实现网站的花样功能与扩展。 响应式极简新闻发布系统 v4.8 更新日志 优化热门文章的排序逻辑 响应式极简新闻发布系统功能 程序支持子目录,支持放到站点的下级,或多级目录中 1、后台服务器信息查看功能能够快速、全面的查看服务器的软硬件状况。 2、站点基本信息设置,包括全局关键词,开通关闭网站,统计代码,logo上传,后台登录免验证码次数等。 3、网站联系方式设置,包括QQ、电话、传真、联系地址等。 4、会员注册设置,包括是否允许注册,注册会员是否需要什么,注册会员初始积分,注册会员页面显示的提示信息等。 5、留言发文功能设置,留言、评论、发表文章的功能开启,以及上述功能的后台管理员审核等。 6、管理员管理,可设定管理员的权限范围及是具备管理权限还是只有查看权限。 7、数据库管理,可备份与还原数据库,同时进行了常见的上传木马漏洞过滤,安全性较高。 8、广告管理,可在现有广告位添加广告,同时针对相对路径不同层级的路径进行了优化,广告管理页面有详细的介绍。 9、友情链接管理,可设置图片或文字类型的友链。 10、后台操作日志管理,记录了后台所有的操作记录,可进行批量删除。 11、栏目管理,支持无限分类,就是说支持栏目有无限多个下级栏目及无限多个同级栏目。同时可以控制栏目开关、栏目是图片还是文字类型、是否在导航栏显示,是否在首页板块显示,控制导航栏木首页板块显示顺序等。 12、文章管理,支持发布图文形式的文章功能,同时能够上传图片,选择服务器已上传的图片,批量上传图片,并可设置多种形式的图片、文字前台展示效果。同时支持文章查看权限设置,能够细化为限制会员查看,限制会员级别查看。 13、评论管理,可以在文章编辑页面中进入以该文章的评论进行专门管理,也可以直接管理系统的全部评论,可删除、审核、回复。 14、留言管理,功能同评论管理。 15、会员管理,管理员可在后台添加会员,此功能与前台会员注册的效果相同。同时也可以管理会员、审核会员、设定会员等级积分等。 16、系统支持文章投稿,会员可在个人中心进行投稿,并获得管理员设定的积分。 17、会员前台注册、登录、会员投稿、签到、管理投稿、以及其他多种形式的互动功能,例如游客留言、游客评论等功能。 18、采集功能,可实现采集规则编写,采集规则复制,采集规则删除,采集规则编辑,批量采集,批量入库,批量删除已采集数据,批量删除历史记录,采集图片本地化等等。 19、整站生成静态html功能,可生成首页、列表页、内容页的静态功能,自定义静态存储的路径,同时兼容会员积分、权限查看、评论等功能。 20、整站伪静态功能的支持,此功能需要在服务器端或虚拟主机端安装伪静态组件,配合伪静态组件就可以实现整站伪静态功能了。 21、动态版功能依然保留,动态版、静态版、伪静态版可以进行无缝切换,即时切换即时生效实现你的多种要求。 22、seo功能,所有页面都支持后台自定义title,keywords,description,全局内链关键词,整站静态化,静态主路径自定义,外网文章批量采集,以及标题h标签优化,链接ul/li/a链接优化等seo功能。 23、分享功能,可实现将网站中的所有内容,文字、图片、网址分享到其他网站或平台,整站都有此功能,内容页文章底部有横排图标形式的,支持自定义后台管理。 24、手机版功能,手机版与电脑版统一后台管理,所有数据都是同步的,不需要额外对手机版进行单独的数据录入等操作,同时手机版的功能与电脑版一样齐全,兼容性强,简单明了。 25、手机版与电脑版可根据用户的设备进行无缝切换,例如使用手机浏览电脑版页面时就会自动跳转到手机版的对应页面,而不是像其他系统只是简单的跳转到首页,而是智能跳转。 26、手机版同样可切换为动态版、静态版、伪静态版,操作方便,一键控制。 27、可免费用于个人非商业用途,除此以外的用途均需要获得官方版权,天人响应式极简新闻发布系统的强大功能与完善的系统配置比市面上几百元的商业版源码更好,更安全。 28、一键操作,安装插件、模板、升级包全都是一键操作,不需要人为手动修改文件、复制文件,一律自动完成。 29、应用中心,可通过后台的应用中心安装应用(插件、模板、升级包),也可以离线安装,傻瓜操作,简单至极。 响应式极简新闻发布系统前台截图 响应式极简新闻发布系统后台截图 相关阅读 同类推荐:站长常用源码

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值