军爷的附加实验(随便写版)

在探索Hello World程序的执行过程中,我们将深入了解操作系统的各个方面,从创建进程开始,一直到程序的执行结束。这个故事以操作系统的视角展开,记录了整个过程中涉及的关键知识点。
hello要做什么要做的效果就是,把你写的操作系统代码,也就是一堆 0101…组成的二进制数,写到硬盘上的某个位置,把这块硬盘插在电脑上,按下电脑的开机键,最终在黑黑的屏幕上输出一个 hello world 字符串。

磁盘的启动
概括为下面的流程
军爷的附加实验(随便写版)_第1张图片

创建进程

故事开始于一个用户决定运行Hello World程序的瞬间。用户敲击键盘,输入启动命令。这个命令被Shell捕获,Shell负责解释用户的命令并与操作系统进行交互。

当运行./hello时,操作系统会执行以下一系列步骤,从进程的角度来看:

  1. 加载程序: 操作系统会在内存中加载hello可执行文件。这涉及将可执行文件的代码段、数据段和其他必要的部分加载到进程的地址空间。

  2. 分配资源: 操作系统会为hello进程分配一些资源,包括内存空间、文件描述符、寄存器等。这确保了进程有足够的资源来执行。

  3. 建立环境: 操作系统会为hello进程设置运行环境,包括设置命令行参数、环境变量等。这些信息将对程序的执行产生影响。

  4. 创建进程控制块(PCB): 操作系统会为hello创建一个进程控制块,其中包含了进程的状态信息、程序计数器、寄存器的值等。PCB用于跟踪和管理进程的执行。

  5. 加载动态链接库(如果有的话): 如果hello依赖于某些动态链接库,操作系统将加载这些库,以便在运行时解析程序中的外部符号。

  6. 设置执行环境: 操作系统会为hello设置执行环境,包括清除寄存器、设置堆栈指针等,以确保程序执行的正确性。

  7. 执行程序: 操作系统将控制权转交给hello的入口点,开始执行程序。程序开始运行,执行其中的指令,可能涉及到对系统调用的调用,文件的读写等操作。

  8. 处理系统调用: 如果hello程序需要访问系统资源(如文件、网络等),它将通过系统调用请求操作系统提供相应的服务。操作系统会根据请求执行相应的内核功能。

  9. 处理中断: 在程序执行过程中,可能会发生中断事件(如硬件中断、时钟中断等)。操作系统会响应这些中断,执行相应的中断处理程序。

  10. 终止进程:hello程序执行完成或者出现错误时,操作系统会终止该进程。这可能涉及到释放分配给进程的资源、更新进程状态等操作。

总的来说,操作系统在执行可执行文件时扮演了很多角色,包括资源管理、环境设置、程序加载、中断处理等。这些步骤确保了程序能够在系统上正确运行并与其他进程协同工作。

# 伪代码示例

# 用户在Shell中输入字符,例如 ./hello,并按下Enter键
user_input = read_input_from_keyboard()

# 处理键盘输入中断
handle_keyboard_interrupt()

# Shell 解析用户输入
command = parse_user_input(user_input)

# 创建新进程
pid = fork()

if pid == 0:
    # 子进程执行 hello 程序
    exec("hello")

# 父进程等待子进程结束
wait(pid)

# 中断处理函数
def handle_keyboard_interrupt():
    # 从键盘缓冲区中读取字符
    char = read_char_from_keyboard_buffer()

    # 处理特殊字符
    if char == '\n':
        # 用户按下Enter键
        execute_command(command)

# 执行命令函数
def execute_command(command):
    if command == "./hello":
        # 加载并执行 hello 程序
        exec("hello")

军爷的附加实验(随便写版)_第2张图片
以下是详细步骤
这个函数的主要任务是执行一个可执行文件,以下是该函数主要步骤的另一种表达方式:

  1. 获取可执行文件的inode: 从文件系统中获取可执行文件的inode,以便后续的文件操作。

  2. 检查文件权限: 检查可执行文件的访问权限,确保当前用户有权执行该文件。

  3. 读取文件头部信息: 读取可执行文件的头部信息,以获取执行所需的关键信息。

  4. 处理脚本文件: 如果可执行文件是脚本文件,则执行相应的处理操作。

  5. 验证文件格式和大小: 检查可执行文件是否符合ZMAGIC格式,并确保文件大小正确,以避免错误执行。

  6. 复制参数和环境变量: 将命令行参数和环境变量复制到新进程的地址空间中,以便新进程能够正确访问这些信息。

  7. 更新文件描述符: 释放当前进程的可执行文件描述符,并为当前进程分配新的描述符。

  8. 信号处理程序重置: 清空当前进程的信号处理程序,确保新进程不会继承旧的信号处理状态。

  9. 文件关闭: 关闭当前进程打开的文件,以释放相关的系统资源。

  10. 页表释放: 释放当前进程的页表,以便为新进程创建新的地址映射。

  11. 协处理器状态重置: 重置当前进程的数学协处理器状态,确保新进程不会继承旧的数学协处理器状态。

  12. LDT创建: 创建新的局部描述符表(LDT),以确保新进程有自己独立的段描述符表。

  13. 堆栈和用户ID设置: 设置新进程的堆栈指针和用户ID,以确保正确的执行环境。

  14. 加载代码和数据: 从磁盘加载可执行文件的代码和数据到新进程的地址空间中。

  15. 设置入口点: 将新进程的入口点设置为可执行文件的入口点。

  16. 设置堆栈指针: 将新进程的堆栈指针设置为新进程的堆栈顶部。

  17. 成功返回: 返回0以表示执行成功。

内存管理

随着进程的创建,操作系统负责为Hello World程序分配内存空间。这包括代码段、数据段和堆栈。代码段存储程序的指令,数据段存储程序的静态数据,而堆栈则用于存储函数调用的信息。

在这个过程中,操作系统还会创建页表,用于将虚拟地址空间映射到物理内存。这有助于保护不同进程的内存空间,防止它们相互干扰。
军爷的附加实验(随便写版)_第3张图片
结合考试的一个页表翻译题
给了一个内存表如下图所示,采用二级页表 让你翻译0x00467FA H写出具体翻译过程。 一级页表的起始地址是从0开始(4分)

0x00H 十六个字节的数字
0x10H 十六个字节的数字
0X20H 我记得都是0
0x30H 我记得都是0
0x40H 一大堆数字

0xFF0 也是数字 到0x2040一大堆数字	

PS: 这个题应该是按照10 、10、12翻译的,如果没算错的话 一级页表那里是1,二级页表是6,地址偏移自己算(之前说的都是十进制),如果一级页表那里是4的话,应该是从0+4 * 4字节(页表项长度是4B),也就是从第二行开始读四个字节,我记得当时应该是小端法读,就比如是34 57 85 A4 B3,转过来应该是B3 A4 85 57 34,之后应该是可以在0FF0到2040找到一个数的作为二级页表的起始地址,之后再找到64B的位置 也就是从起始地址+64B的位置 读四个字节,这样获得了三十二位数字之后将后边的12位制成0之后+一开始算的偏移就是最后的物理地址。

军爷的附加实验(随便写版)_第4张图片
在Linux内核中,处理缺页异常的核心函数是 do_page_fault。当发生缺页异常时,内核会调用 do_page_fault 函数来处理。

以下是一个简化的代码示例,演示了当发生缺页异常时,内核可能调用的处理函数:

#include 
#include 

void handle_page_fault(struct pt_regs *regs)
{
    unsigned long address = read_cr2(); // 读取引发缺页异常的线性地址
    struct mm_struct *mm = current->mm;

    // 检查是否有空页面
    if (handle_mm_fault(mm, NULL, address, 0) == 0) {
        // 页面分配成功
        return;
    }

    // 处理无法分配页面的情况
    printk("Page fault at address: %lx\n", address);

    // 其他处理逻辑...
}

这里使用 handle_mm_fault 函数来尝试处理缺页异常,如果页面成功分配,函数返回0。如果分配失败,可能由于内存不足等原因,就需要进行其他处理逻辑。

IO与文件系统

Hello World程序在执行过程中可能需要进行输入输出操作。当程序尝试从标准输入读取数据或将数据写入标准输出时,操作系统介入并负责将数据传递给正确的设备。

同时,操作系统还会检索Hello World程序的可执行文件。这个文件可能存储在硬盘上的文件系统中。操作系统会根据文件系统的结构,将程序加载到内存中,并确保所有依赖的库和资源也被正确加载。

军爷的附加实验(随便写版)_第5张图片

第四章:进程的调度、释放和回收
hello进程的调度日志由下所示:
4 J 30793
4 R 30793
6 N 30793
6 J 30794
4 W 30794
6 R 30795
4 J 30797
6 E 30798
4 R 30798
当hello进程新建并就绪后,进程4也就是sh进入等待状态,之后hello进程开始运行,并在结束前唤醒
sh,之后hello进程退出sh继续运行。
军爷的附加实验(随便写版)_第6张图片
do_exit 函数实现了在进程退出时的一系列操作,以下是该函数主要功能的另一种表达方式:


int do_exit(long code)
{
    int i;
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
    for (i=0 ; i<NR_TASKS ; i++)
    if (task[i] && task[i]->father == current->pid) {
    task[i]->father = 1;
    if (task[i]->state == TASK_ZOMBIE)
    /*assumption task[1] is always init */
    (void) send_sig(SIGCHLD, task[1], 1);
    }
    for (i=0 ; i<NR_OPEN ; i++)
    if (current->filp[i])
    sys_close(i);
    iput(current->pwd);
    current->pwd=NULL;
    iput(current->root);
    current->root=NULL;
    iput(current->executable);
    current->executable=NULL;
    if (current->leader && current->tty >= 0)
    tty_table[current->tty].pgrp = 0;
    if (last_task_used_math == current)
    last_task_used_math = NULL;
    if (current->leader)
    kill_session();
    current->state = TASK_ZOMBIE;
    current->exit_code = code;
    fprintk(3,"%d\tE\t%d\n",current->pid,jiffies);
    tell_father(current->father);
    schedule();
    return (-1); /* just to suppress warnings*/
}

  1. 释放局部描述符表(LDT)页帧: 释放进程的局部描述符表页帧,确保释放了相关的系统资源。

  2. 查找子进程并更新父子关系: 遍历任务数组,查找当前进程的子进程,并将其父进程设置为init进程。同时,如果找到僵尸进程,向init进程发送 SIGCHLD 信号。

  3. 关闭所有打开的文件描述符: 遍历文件描述符数组,关闭当前进程打开的所有文件描述符。

  4. 释放进程相关计数: 释放当前进程的工作目录、根目录、可执行程序等引用计数,确保相关资源得到释放。

  5. 修改控制终端的组: 如果当前进程是会话领导进程,并且控制终端有效,则将控制终端的进程组设置为0。

  6. 清除数学协处理器标记: 如果当前进程是最后使用数学协处理器的进程,将全局变量 last_task_used_math 设置为 NULL。

  7. 清除会话(如果是会话领导进程): 如果当前进程是会话领导进程,清除整个会话,确保相关进程的关联关系被解除。

  8. 设置进程状态为僵尸状态: 将当前进程状态设置为僵尸状态,表示该进程已经退出。

  9. 记录退出代码和时间: 将退出代码和当前时间记录,以供后续查询或分析使用。

  10. 通知父进程: 向父进程发送通知,告知其子进程已经退出。

  11. 执行度量调度: 执行调度算法,将 CPU 的控制权切换到其他就绪进程上运行。

  12. 返回错误码-1: 函数最后返回错误码 -1,实际上,该返回语句可能永远不会被执行,因为函数中的 schedule() 调用将导致当前进程切换到其他进程。

通过这些操作,do_exit 函数确保了进程退出时释放了相关资源、更新了进程间关系,并通知了相关进程。这有助于系统的正常运作和资源管理。
第五章:执行Hello World

最终,Hello World程序准备好在其独立的进程中执行。操作系统通过调度算法决定何时运行这个进程。程序的指令被解释器加载到CPU中执行,而CPU的寄存器则保存了程序的状态和上下文信息。

在程序执行的过程中,操作系统会监视进程的状态,处理中断并确保进程能够安全地完成。当Hello World程序输出"Hello, World!"时,这一漫长而复杂的过程就算是结束了。

通过这个故事,我们深入了解了操作系统在Hello World程序执行过程中所扮演的关键角色,涉及到的进程管理、内存管理、IO、文件系统、缓冲、缺页中断等方方面面。这也让我们更加欣赏操作系统在计算机系统中的不可或缺的地位。

你可能感兴趣的:(linux,服务器,运维)