【OS Pintos】用户程序是如何工作的 | Pintos 运行原理 | 虚拟内存 | 页函数 | 系统调用

简介: 【OS Pintos】用户程序是如何工作的 | Pintos 运行原理 | 虚拟内存 | 页函数 | 系统调用

💭 写在前面

本篇博客将开始介绍 Pintos 的基本知识,为 Pintos 的 Project1 用户程序(User Program)做必要的只是铺垫,讲解 Pintos 运行原理、虚拟内存、页函数以及系统调用的实现。



0x00 背景介绍

Pintos 是一个简单的操作系统,它可以启动,允许应用程序,关机。

你可以试着在 Pintos 上运行应用程序  "echo" :

① 首先在 src/examples src/userprog 中 Makefile

  • cdsrc/examples 目录下,输入 make

  • cd  src/userprog 下,输入 make

② 在 /src/userprog 路径下输入运行它

  • 输入如下指令,利用 echo 打印 x (注意 -a echo 后面是两个 -)
~/pintos/src/userprog $ 
pintos --filesys-size=2 -p ../examples/echo -a echo -- -f -q run 'echo x'

熟悉的 "Powering off..." 这个 echo 程序看上去已经运行成功了,但是似乎没有出现结果。

0x01 用户程序是如何工作的(How User Program Works)

仔细观察前面指令的细节:

~/pintos/src/userprog $ 
pintos --filesys-size=2 -p ../examples/echo -a echo -- -f -q run 'echo x'
  • --filesys-size=2 生成大小为 2mb 的模拟 Pintos 磁盘(disk)。
  • -p ../examples/echo -a echo .../examples/echo复制到模拟磁盘,并将名称从 .../examples/echo 改为 echo
  • -- echo-f 之间:将 pintos 的选项和内核参数分离开。
  • -fPintos 对模拟磁盘进行格式化操作。
  • -q在用户执行完 echo 后 Pintos 将终止。
  • run 'echo x'Pintos 将以 x 作为参数,执行 echo

❓ 再次思考:我们为什么看不看到 echo 命令的结果?

这是因为在目前的 Pintos 中,系统调用(system call)、系统调用处理程序(system call handler)、参数传递(argument passing)和 用户栈(user stack)都没有实现!

  (这些都是要你动手操作的,加油)

可以说,目前的 Pintos 其实就是个空壳子,许多操作系统应具有的功能都没有,这些都是斯坦福专家们精心为你准备的练习,让你自己手糊一个操作系统玩玩。说得轻松,但是难度真的不容小觑!

0x02 前置知识:Pintos 运行原理

如果想明白的更透彻,我们这里可以了解一下 Pintos 的运行原理。

Pintos 可以加载和运行常规的 ELF(Executable & Linkable Format)可执行文件。

因此要运行一个用户程序,我们必须把用户程序复制到模拟磁盘上(可以参考刚才指令的细节)。

~/pintos/src/userprog $ 
pintos --filesys-size=2 -p ../examples/echo -a echo -- -f -q run 'echo x'
  • echo 是一个把参数写入标准输出(Standard Output)的应用程序。
  • 因此,echo 需要内核中的系统调用所提供的 功能
  • 并且还需要用到用户栈,在栈上存储参数并将它们传递给内核。
  • 但是,当前的 Pintos 没有实现 系统调用用户栈

这就是为什么我们看不到 echo x 结果的原因!

0x03 代码实现

溪云初起日沉阁,山雨欲来风满楼。

在这个项目中,我们需要让 Pintos 能够正确执行 "用户程序" ,我们应该在下列目录下完成,并修改下列文件:

代码级流程(Code Level Flow)

$ pintos --filesys-size=2 -p ../examples/echo -a echo -- -f -q run 'echo x'

/* 運行 */
run_actions(argv);
/* 終了 */
shutdown();
thread_exit();

💬 run_actions 函数:

run_actions(char** argv   )
{
    /* An action */
    struct action {
        char* name;
        int argc;
        void(*function) (char ** argv);
    };
    /* Table of supported actions. */
    static const struct action actions[] = {
        {"run", 2, run_task},
#ifdef FILESYS
        {"ls", 1, fsutil_ls},
while(*argv != NULL) {
        const struct action* a;
        int i;
        /* Find action name. */
        for (a = actions; ; a++)
            if (a->name == NULL)
                PANIC("unknow action");
        else if (!strcmp(*argv, a->name))
            break;
    /* Check for required arguments. */
    for (i = 0; i < a->argc; i++) 
        if (argv[i] == NULL)
            PANIC("action");
    /* Invoke action and advance. */
    a->function(argv);
    argv += a->argc;
}

💬 run_task 函数:

static void run_task (char** argv) 
{
    const char* task = argv[1];
    printf("Excuting '%s': \n", task);
#ifdef USERPROG
    process_wait (process_execute(task));
#else
    run_test(test);
#endif
    printf("Execution of '%s' complete.\n", task);
}

💬 process_execute 函数:

tid_t process_execute ( 
    const char* file_name
    ) 
{
    char* fn_copy;
    tid_t tid;
    /* Make a copy of FILE_NAME.
        Otherwise there's a race between the caller and load(). */
    fn_copy = palloc_get_page(0);
    if (fn_copy == NULL)
        return TID_ERROR;
    strlcpy(fn_copy, file_name, PGSIZE);
    /* Create a new thread to execute FILE_NAME. */
    tid = thread_create (file_name, PRI_DEFAULT, start_process, fn_copy);
    if (tid == TID_ERROR)
        palloc_free_page (fncopy);
    return tid;
}

pintos 工具选项:

$ pintos --filesys-size=2 -p ../examples/echo -a echo -- -f -q run 'echo x'
         👆               👆
        多字符选项       单字符选项
  • -  表示单字符选项
  • - - 表示多个字符的选项   (你可以通过执行 pintos --help 来阅读选项)

分隔符:

$ pintos --filesys-size=2 -p ../examples/echo -a echo -- -f -q run 'echo x'
                                                      👆
                                    [Pintos选项]     分隔符      [内核参数]
  • - -Pintos 选项和 Pintos 内核参数之间的分隔符

* 根据 80x86 调用约定设置用户栈。

0x04 虚拟内存(Virtual Memory)

Pintos 将内存分为两个区域,用户内存和内核内存。

如果我们直接使用这些内存区域,就很难管理内存,例如:

  • 每个进程都可以相互销毁
  • 进程可以破坏对运行操作系统至关重要的内核代码

为了防止这些问题,操作系统采用了 虚拟内存系统(virtual memory system)

由于虚拟内存的存在,每个进程都可以拥有自己专属的内存区域,并且能够使用它,就像该进程享用了整个内存一样。

而 Pintos 也是采用虚拟内存来管理内存区域的。

🔺 虚拟内存会被划分为两个区域:用户虚拟内存内核虚拟内存

虚拟内存:启动应用程序

内核虚拟内存是全局的,

Pintos 中的虚拟内存

  • 每个进程都有自己的用户虚拟内存。
  • Pintos分配给内核1GB作为全局内存。(PHYS_BASE (3 GB) ~ 4 GB的虚拟内存)
  • 内存单元在Pintos中是一个页,大小为4KB。
  • 用户程序可以通过页目录和页表翻译虚拟地址来访问物理内存。(参考上图 "页表")。

0x05 页函数(Functions for page)

threads/palloc.c

is_user_vaddr()     // 检查给定的虚拟地址是否是用户虚拟地址
is_kernel_vaddr()   // 检查给定的虚拟地址是否是内核虚拟地址
ptov()    // 将物理地址转换为内核虚拟地址
vtop()    // 将内核虚拟地址转换为物理地址

threads/palloc.c

palloc_get_page()    // 从用户或内核内存池中获取页

userprog/pagedir.c

pagedir_create()    // 创建页表
pagedir_get_page()  // 查询页面目录中与用户虚拟地址相对应的物理地址
pagedir_set_page()  // 在页面目录中添加从用户虚拟地址到物理页面的映射

0x06 系统调用(System Calls)

正如我们所见,Pintos 将内存分为 用户虚拟内存内核虚拟内存,以保护每个进程和内核代码。

随着虚拟内存的概念,操作系统防止用户程序访问包含核心功能的内核内存。

那么用户程序该如何使用内核的功能呢?

💡 操作系统提供了系统调用(System Calls)来解决这个问题!

  • 出于安全性考虑,操作系统提供了两种模式 —— 用户模式 内核模式
  • 当用户程序在用户模式下运行时,它不能访问内存或磁盘。
  • 这些操作是在 内核模式 下进行的。
  • 操作系统提供了进入内核模式的 系统调用

Pintos 在 lib/user/syscall.c  中提供了系统调用的用户级接口。

userprog/syscall.c 中提供了系统调用处理器的骨架。

Pintos 中的系统调用程序

  • 用户程序调用系统调用的函数
#include <stdio.h>
#include <syscall.h>
int main(int argc, char* argv[]) 
{
    bool success = true;
    int i;
    for (i = 1; i < argc; i++)
    {
        int fd = open(argv[i]);
        if (fd < 0) 
        {
            printf("%s: open failed\n", argv[i]);
            success = false;
            continue;
        }
  • 系统调用号 和 附加 参数 被压到调用者的栈中。
  • 通过使用 int $0x30 指令调用系统调用的中断
int open(const char* file)
{
    return syscall1 (SYS_OPEN, file);
}
/* Invokes syscall NUMBER, passing argument ARGO, and returns the 
   return value as and 'int'.  */
#define syscall1(NUMBER, ARG0)                                              \
   ( 
          int retval;                                                       \
          asm volatile
              ("pushl %[arg0]; pushl %[number]; int $0x30; addl $8, %%esp"  \
                                     // 从系统调用处理程序返回后,恢复栈指针。
                  : "=aa" (retval)
                  : [number] "i" (NUMBER),                                  \
                    [arg0] "g" (ARG0)                                       \
                  : "memory");                                              \
              retval;                                                       \
   } )

  • 为了能够中断,设置栈并调用中断处理程序
.func intr_entry
intr_entry:
    /* 保存调用者的寄存器 */
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal
    /* 设置内核环境 */
    cld                     // 字符串指令向上走
    mov $SEL_KDSEG, %eax    // 初始化段寄存器 
    mov %eax, %ds
    mov %eax, %es
    leal 56(%esp), %edp     // 设置框架指针
    /* 调用中断处理程序 */
    push %esp
.glovl intr_handler
    call intr_handler       // Call interrupt handler
    andl $4, %esp
.endfunc

*  source 代码位置: threads/intr-stubs.S

  • intr_handler()   调用中断处理程序
void intr_handler(struct intr_frame* frame)
{
    bool external;
    intr_handler_func* handler;
    /* 外部中断很特别
       我们一次只能处理一个(所以中断必须关闭)
       而且它们需要在PIC上被确认(见下文)
       一个外部中断处理程序无法休眠。  */
    external = frame->vec_no >= 0x20 && frame->vec_no < 0x30;
    if (external) 
    {
        ASSERT (intr_get_level() == INTR_OFF);
        ASSERT (!intr_context());
        in_external_intr = true;
        yield_on_retrun = false;
    }
    /* 启用中断处理程序 */
    handler = intr_handlers[frame->vec_no];
    if (handler != NULL)
        handler(frame);

在 Pintos 启动时,系统调用的中断处理程序就已经被注册了。

* 参考以下函数调用:

  1. main()in 'threads/init.c' calls syscall_init()which is in 'userprog/syscall.c'
  2. syscall_init() calls intr_register_int() in 'threads/interrupt.c'
  • syscall_handler()   得到控制权,它可以通过 intr_frame 结构的 'esp' 成员访问栈(在threads/interrupt.h。
  • 80x86 的惯例是将系统调用的返回值存储在 EAX 寄存器中,因此我们可以将返回值存储在intr_frame 结构的 'eax' 成员中。
static void syscall_handler(struct intr_frame* f UNUSED)
{
    printf("system call!\n");
    thread_exit();
}

* Pintos 提供了系统调用处理程序的骨架,我们将在这个项目中开发它。

struct intr_frame
{
    /* 由intr-stubs.S中的 intr_entry 推送,
       这些是保存被中断任务的寄存器。 */
    uint32_t ebx;       // 保存EBX
    unit32_t edx;       // 保存EDX
    unit32_t ecx;       // 保存ECX
    unit32_t eax;       // 保存EAX
    void* esp;          // 保存栈指针
    uint16_t ss, :16;   // esp的数据段
};

需要修改的:  syscall.c   process.c


📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.9.24
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces

A. Silberschatz, P. Galvin, and G. Gagne,

Operating System Concepts, 9th Edition, John Wiley & Sons, Inc., 2014, ISBN 978-1-118-09375-7.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

相关文章
|
3月前
|
缓存 固态存储 Windows
如何让内存发挥到最大效能?全面优化指南,提升电脑运行体验
电脑内存使用不合理会导致卡顿,本文教你如何优化内存性能。检查内存容量与主板支持上限,考虑升级或调整配置;关闭后台程序、管理浏览器标签、结束异常进程以释放内存;设置虚拟内存、调整视觉效果、定期重启提升效率;必要时增加内存条、选择高频内存、更换固态硬盘。避免盲目清理内存和依赖大内存忽视其他硬件瓶颈。只需合理设置,无需额外花钱,就能显著提升电脑速度。
|
6月前
|
机器学习/深度学习 存储 算法
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
反向传播算法虽是深度学习基石,但面临内存消耗大和并行扩展受限的问题。近期,牛津大学等机构提出NoProp方法,通过扩散模型概念,将训练重塑为分层去噪任务,无需全局前向或反向传播。NoProp包含三种变体(DT、CT、FM),具备低内存占用与高效训练优势,在CIFAR-10等数据集上达到与传统方法相当的性能。其层间解耦特性支持分布式并行训练,为无梯度深度学习提供了新方向。
219 1
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
|
5月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
161 0
|
3月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
242 15
|
8月前
|
人工智能 并行计算 安全
用户实操:如何以龙蜥操作系统为底座在 CPU 上运行 DeepSeek-R1
介绍如何在 CPU 上使用 llama.cpp 推理 671B 版本的 DeepSeek R1,以及实际效果。
|
9月前
|
监控 搜索推荐 开发工具
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
680 2
2025年1月9日更新Windows操作系统个人使用-禁用掉一下一些不必要的服务-关闭占用资源的进程-禁用服务提升系统运行速度-让电脑不再卡顿-优雅草央千澈-长期更新
|
8月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
342 0
|
10月前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
10月前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
134 4

推荐镜像

更多