CPU 虚拟化机制——受限直接执行 (LDE)

1. 引言:CPU虚拟化的核心问题

让多个进程看似同时运行在一个物理CPU上。核心思想是时分共享 (time sharing) CPU。为了实现高效且可控的时分共享,本章介绍了一种关键机制,称为受限直接执行 (Limited Direct Execution, LDE)

1.1 LDE的基本思想

  1. 直接执行 (Direct Execution): 为了性能,让用户程序尽可能直接在硬件CPU上运行。
  2. 受限 (Limited): 操作系统必须施加限制,以确保它能保持对系统的控制权,并且用户程序不能执行危险或非授权的操作。

LDE基本思想

1.2 问题推导

问题推导

LDE协议有两个阶段:

  • 第一阶段(在系统引导时):内核初始化陷阱表,并且CPU记住它的位置以供随后使用。内核通过特权指令来执行此操作。
  • 第二阶段(运行进程时):在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容(例如,在进程列表中分配一个节点,分配内存)。这会将CPU切换到用户模式并开始运行该进程。

2. 问题1:如何执行受限制的操作?

2.1 背景

直接执行时,如果进程需要执行I/O等特权操作怎么办?不能让用户进程随意操作硬件。

2.2 解决方案:引入特权级别 (Processor Modes)

  • 用户模式 (User Mode): 运行用户程序,权限受限,不能执行特权指令(如I/O)。
  • 内核模式 (Kernel Mode): 运行操作系统内核,拥有最高权限,可以执行任何指令,访问任何硬件。

2.3 解决方案:受保护的控制权转移 (Protected Control Transfer)

受保护的控制权转移

  • 系统调用 (System Calls): 用户程序通过执行特殊的trap指令,陷入(trap)到内核。
  • 陷入过程: trap指令会提升CPU权限到内核模式,并跳转到操作系统预设的代码地址。
  • 陷阱表 (Trap Table): 操作系统在启动时设置一个陷阱表,告诉硬件在发生特定事件时应该跳转到内核中的哪个处理程序。
  • 返回: 内核完成工作后,执行return-from-trap指令,降低CPU权限回用户模式。

3. 问题2:如何在进程之间切换?

3.1 背景

实现了LDE后,当一个进程在CPU上运行时,操作系统本身并没有运行。那么操作系统如何夺回控制权以切换到另一个进程,实现时分共享呢?

3.2 子问题:如何重获CPU的控制权?

方案1 (协作式 Cooperative):

  • 依赖进程"自觉"
  • 进程通过发起系统调用或产生错误将控制权交还给OS
  • 缺陷: 如果进程陷入死循环且不进行系统调用或出错,OS将失去控制权

方案2 (非协作式/抢占式 Preemptive):

  • 硬件支持——时钟中断 (Timer Interrupt)

时钟中断机制

  • OS在启动时设置并启动一个硬件时钟
  • 时钟会定期产生中断信号,强制打断当前运行的进程
  • CPU控制权转移给OS预设的中断处理程序

3.3 机制:上下文切换 (Context Switch)

当OS决定切换进程时,执行上下文切换:

  1. 保存当前进程的状态到其进程结构(如PCB)或内核栈中
  2. 加载下一个要运行进程的状态
  3. 切换内核栈指针
  4. 通过return-from-trap指令返回,CPU开始执行新加载的进程

4. 潜在问题:并发问题

提出担忧: 如果在处理系统调用或中断时,又发生了一个中断怎么办?

初步解答: 这是并发问题,将在后续章节详细讨论。常用方法包括在处理中断时禁止中断 (disable interrupts),或使用复杂的**锁 (locking)**机制来保护内核数据结构。

5. 深入理解Trap和Trap Table

5.1 Trap (陷阱)

Trap机制说明

在操作系统和计算机体系结构的语境下,"陷阱"是一种机制,它允许将处理器的控制权从用户模式安全地转移到内核模式。

目的:

  • 执行受限操作: 通过系统调用实现
  • 处理异常和错误: 如除零、访问无效内存
  • 响应硬件中断: 如时钟中断、I/O完成信号

过程:

  1. 触发:trap指令、CPU错误/异常或硬件中断触发
  2. 硬件动作: 保存状态、提升权限、查询陷阱表
  3. 内核执行: 跳转到陷阱处理程序
  4. 返回: 通过return-from-trap指令返回用户模式

5.2 Trap Table (陷阱表)

陷阱表是一个由操作系统内核创建和维护的数据结构,通常是一个数组。

内容: 每一项包含内核中特定处理程序代码的内存地址

设置: 操作系统在启动过程中初始化陷阱表

作用: 指导硬件在发生事件时应该跳转到哪里

重要性: 确保控制权转移的安全性和可控性

6. 陷阱表的技术实现

6.1 陷阱表概述

陷阱表结构

  • 陷阱表本质上是一个数组结构
  • 数组的索引对应中断/异常/陷阱编号
  • 数组的每个元素包含处理该事件的内核代码地址控制信息

6.2 x86架构的实现:中断描述符表(IDT)

在x86架构中,陷阱表被称为中断描述符表(IDT),最多包含256个条目。

门描述符结构:

struct idt_entry {uint16_t offset_low;    // 处理程序地址低16位uint16_t selector;      // 段选择子uint8_t  ist;           // IST索引和保留位uint8_t  type_attr;     // 类型和属性uint16_t offset_mid;    // 处理程序地址中16位uint32_t offset_high;   // 处理程序地址高32位uint32_t zero;          // 保留位
} __attribute__((packed));

6.3 IDT初始化代码示例

void initialize_idt() {// 设置除零错误处理程序set_idt_gate(0, (uint64_t)&divide_by_zero_handler, KERNEL_CS, 0x8E, 0);// 设置缺页错误处理程序set_idt_gate(14, (uint64_t)&page_fault_handler, KERNEL_CS, 0x8E, 0);// 设置时钟中断处理程序set_idt_gate(32, (uint64_t)&timer_interrupt_handler, KERNEL_CS, 0x8E, 0);// 设置系统调用处理程序(注意DPL=3)set_idt_gate(128, (uint64_t)&system_call_handler, KERNEL_CS, 0xEE, 0);// 设置IDT指针idt_pointer.limit = sizeof(idt_table) - 1;idt_pointer.base  = (uint64_t)&idt_table;// 加载IDT寄存器load_idt(&idt_pointer);
}

6.4 处理程序实现

C语言处理函数:

void timer_interrupt_handler_c() { /* 时钟中断逻辑 */ }
uint64_t system_call_handler_c(uint64_t syscall_num, uint64_t arg1, ...) { /* 处理系统调用 */ }

汇编包装器:

timer_interrupt_handler_asm:pushaq                      ; 保存所有通用寄存器call timer_interrupt_handler_c  ; 调用C函数popaq                       ; 恢复所有通用寄存器iretq                       ; 中断返回

7. 总结

通过LDE机制解释了如何虚拟化CPU的核心思想是让程序直接运行,但预先设置好硬件限制(用户/内核模式、陷阱处理、时钟中断),就像给房间做"宝宝防护 (baby proofing)"一样。这样既保证了效率,又维持了OS的控制权。

关键机制包括:

  • 特权级别分离
  • 受保护的控制权转移
  • 时钟中断
  • 上下文切换
  • 陷阱表

实验:测量上下文调度时间

创建两个进程。

创建两个管道 (pipe) 用于这两个进程间双向通信。

管道 1:进程 A -> 进程 B

管道 2:进程 B -> 进程 A

关键: 将这两个进程绑定到同一个 CPU 核心上运行。这可以使用 sched_setaffinity() (Linux) 或类似系统调用。这是为了确保测量的是单个 CPU 上的上下文切换,而不是因为进程在不同 CPU 间迁移。

进行乒乓通信:

进程 A 向管道 1 写入少量数据(例如 1 字节)。

进程 A 尝试从管道 2 读取数据。由于管道 2 是空的,进程 A 会阻塞 (block)。

操作系统检测到进程 A 阻塞,执行上下文切换,调度进程 B 运行。

进程 B 尝试从管道 1 读取数据(读取 A 写入的数据)。

进程 B 向管道 2 写入少量数据。

进程 B 尝试从管道 1 读取数据。由于管道 1 现在是空的,进程 B 会阻塞。

操作系统检测到进程 B 阻塞,执行上下文切换,调度进程 A 运行(此时 A 可以读到 B 写入的数据,解除阻塞)。

这样完成了一个来回 (round trip)。

重复这个来回很多次。

记录总时间,除以总的来回次数,得到平均每次来回的时间。

注意: 一个完整的来回包含两次上下文切换(A->B 和 B->A)。所以,用平均来回时间除以 2,得到单次上下文切换的估算成本。

2.2 工具
pipe(): 创建 UNIX 管道。

fork(): 创建子进程。

write(), read(): 通过管道读写数据。

sched_setaffinity(): (Linux) 将进程绑定到指定 CPU 核心。需要包含 <sched.h> 并可能需要定义 _GNU_SOURCE。

wait() 或 waitpid(): 父进程等待子进程结束。

计时函数: 使用 clock_gettime() 。

#define _GNU_SOURCE // 需要这个宏才能使用 sched_setaffinity
#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sched.h>
#include <time.h>
#include <string.h> // For memset#define ITERATIONS 100000 // 上下文切换测量的迭代次数
#define CPU_TO_BIND 0      // 绑定到 CPU 核心 0,确保你的系统有这个核心// Helper function to calculate time difference in nanoseconds
long long timespec_diff_ns(struct timespec start, struct timespec end) {return (end.tv_sec - start.tv_sec) * 1000000000LL + (end.tv_nsec - start.tv_nsec);
}int main() {int pipe1[2]; // Pipe: Parent -> Childint pipe2[2]; // Pipe: Child -> Parentpid_t child_pid;struct timespec start_time, end_time;long long total_elapsed_ns;double avg_round_trip_ns, avg_context_switch_ns;char buffer = 'x'; // 用于在管道中传输的单个字节// 创建两个管道if (pipe(pipe1) == -1 || pipe(pipe2) == -1) {perror("pipe creation failed");return 1;}// 创建子进程child_pid = fork();if (child_pid == -1) {perror("fork failed");return 1;}// 设置 CPU 亲和性 (Affinity)cpu_set_t cpu_set;CPU_ZERO(&cpu_set);CPU_SET(CPU_TO_BIND, &cpu_set);if (sched_setaffinity(0, sizeof(cpu_set_t), &cpu_set) == -1) {perror("sched_setaffinity failed");// 不一定是致命错误,但测量结果可能不准}if (child_pid == 0) {// --- 子进程代码 ---close(pipe1[1]); // 关闭 pipe1 的写端close(pipe2[0]); // 关闭 pipe2 的读端for (long i = 0; i < ITERATIONS; ++i) {// 从父进程读if (read(pipe1[0], &buffer, 1) != 1) {perror("Child read failed");exit(1);}// 写回给父进程if (write(pipe2[1], &buffer, 1) != 1) {perror("Child write failed");exit(1);}}close(pipe1[0]);close(pipe2[1]);exit(0);} else {// --- 父进程代码 ---close(pipe1[0]); // 关闭 pipe1 的读端close(pipe2[1]); // 关闭 pipe2 的写端// 获取开始时间 (在循环开始前)if (clock_gettime(CLOCK_MONOTONIC, &start_time) == -1) {perror("clock_gettime start failed");return 1;}// 执行乒乓通信循环for (long i = 0; i < ITERATIONS; ++i) {// 写给子进程if (write(pipe1[1], &buffer, 1) != 1) {perror("Parent write failed");exit(1);}// 从子进程读 (会阻塞,触发 A->B 切换)if (read(pipe2[0], &buffer, 1) != 1) {perror("Parent read failed");exit(1);}// 读完后,子进程会阻塞,触发 B->A 切换}// 获取结束时间if (clock_gettime(CLOCK_MONOTONIC, &end_time) == -1) {perror("clock_gettime end failed");return 1;}// 等待子进程结束wait(NULL);close(pipe1[1]);close(pipe2[0]);// 计算总耗时total_elapsed_ns = timespec_diff_ns(start_time, end_time);// 计算平均每次来回耗时avg_round_trip_ns = (double)total_elapsed_ns / ITERATIONS;// 计算平均每次上下文切换耗时 (来回时间 / 2)avg_context_switch_ns = avg_round_trip_ns / 2.0;printf("Measured %ld round trips between two processes on CPU %d.\n", (long)ITERATIONS, CPU_TO_BIND);printf("Total time: %lld ns\n", total_elapsed_ns);printf("Average round trip time: %.2f ns\n", avg_round_trip_ns);printf("Average context switch cost: %.2f ns\n", avg_context_switch_ns);}return 0;
}// 编译: gcc -o measure_ctxsw measure_ctxsw.c -lrt -D_GNU_SOURCE
// 运行: ./measure_ctxsw

在这里插入图片描述

8. 参考资料

  1. 陷阱、中断、异常、信号
  2. OSTEP

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/76980.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux 中断子系统链式中断编程

直接贴代码了&#xff1a; 虚拟中断控制器代码&#xff0c;chained_virt.c #include<linux/kernel.h> #include<linux/module.h> #include<linux/clk.h> #include<linux/err.h> #include<linux/init.h> #include<linux/interrupt.h> #inc…

容器修仙传 我的灵根是Pod 第10章 心魔大劫(RBAC与SecurityContext)

第四卷&#xff1a;飞升之劫化神篇 第10章 心魔大劫&#xff08;RBAC与SecurityContext&#xff09; 血月当空&#xff0c;林衍的混沌灵根正在异变。 每道经脉都爬满黑色纹路&#xff0c;神识海中回荡着蛊惑之音&#xff1a;"破开藏经阁第九层禁制…夺取《太古弑仙诀》……

基于c#,wpf,ef框架,sql server数据库,音乐播放器

详细视频: 【基于c#,wpf,ef框架,sql server数据库&#xff0c;音乐播放器。-哔哩哔哩】 https://b23.tv/ZqmOKJ5

精益数据分析(21/126):剖析创业增长引擎与精益画布指标

精益数据分析&#xff08;21/126&#xff09;&#xff1a;剖析创业增长引擎与精益画布指标 大家好&#xff01;在创业和数据分析的探索道路上&#xff0c;我一直希望能和大家携手共进&#xff0c;共同学习。今天&#xff0c;我们继续深入研读《精益数据分析》&#xff0c;剖析…

Spark-streaming核心编程

1.导入依赖‌&#xff1a; <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-streaming-kafka-0-10_2.12</artifactId> <version>3.0.0</version> </dependency> 2.编写代码‌&#xff1a; 创建Sp…

Kafka的ISR机制是什么?如何保证数据一致性?

一、Kafka ISR机制深度解析 1. ISR机制定义 ISR&#xff08;In-Sync Replicas&#xff09;是Kafka保证数据一致性的核心机制&#xff0c;由Leader副本&#xff08;复杂读写&#xff09;和Follower副本(负责备份)组成。当Follower副本的延迟超过replica.lag.time.max.ms&#…

Docker 基本概念与安装指南

Docker 基本概念与安装指南 一、Docker 核心概念 1. 容器&#xff08;Container&#xff09; 容器是 Docker 的核心运行单元&#xff0c;本质是一个轻量级的沙盒环境。它基于镜像创建&#xff0c;包含应用程序及其运行所需的依赖&#xff08;如代码、库、环境变量等&#xf…

数据库监控 | MongoDB监控全解析

PART 01 MongoDB&#xff1a;灵活、可扩展的文档数据库 MongoDB作为一款开源的NoSQL数据库&#xff0c;凭借其灵活的数据模型&#xff08;基于BSON的文档存储&#xff09;、水平扩展能力&#xff08;分片集群&#xff09;和高可用性&#xff08;副本集架构&#xff09;&#x…

OpenFeign和Gateway

OpenFeign和Gateway 一.OpenFeign介绍二.快速上手1.引入依赖2.开启openfeign的功能3.编写客户端4.修改远程调用代码5.测试 三.OpenFeign参数传递1.传递单个参数2.多个参数、传递对象和传递JSON字符串3.最佳方式写代码继承的方式抽取的方式 四.部署OpenFeign五.统一服务入口-Gat…

spark-streaming(二)

DStream创建&#xff08;kafka数据源&#xff09; 1.在idea中的 pom.xml 中添加依赖 <dependency><groupId>org.apache.spark</groupId><artifactId>spark-streaming-kafka-0-10_2.12</artifactId><version>3.0.0</version> </…

JAVA聚焦OutOfMemoryError 异常

个人主页 文章专栏 在正文开始前&#xff0c;我想多说几句&#xff0c;也就是吐苦水吧…最近这段时间一直想写点东西&#xff0c;停下来反思思考一下。 心中万言&#xff0c;真正执笔时又不知先写些什么。通常这个时候&#xff0c;我都会随便写写&#xff0c;文风极像散文&…

如何在Spring Boot中配置自定义端口运行应用程序

Spring Boot 应用程序默认在端口 8080 上运行嵌入式 Web 服务器&#xff08;如 Tomcat、Jetty 或 Undertow&#xff09;。然而&#xff0c;在开发、测试或生产环境中&#xff0c;开发者可能需要将应用程序配置为在自定义端口上运行&#xff0c;例如避免端口冲突、适配微服务架构…

linux嵌入式(进程与线程1)

Linux进程 进程介绍 1. 进程的基本概念 定义&#xff1a;进程是程序的一次执行过程&#xff0c;拥有独立的地址空间、资源&#xff08;如内存、文件描述符&#xff09;和唯一的进程 ID&#xff08;PID&#xff09;。 组成&#xff1a; 代码段&#xff1a;程序的指令。 数据…

智驭未来:NVIDIA自动驾驶安全白皮书与实验室创新实践深度解析

一、引言&#xff1a;自动驾驶安全的范式革新 在当今数字化浪潮的推动下&#xff0c;全球自动驾驶技术正大步迈入商业化的深水区。随着越来越多的自动驾驶车辆走上道路&#xff0c;其安全性已成为整个行业乃至社会关注的核心命题。在这个关键的转折点上&#xff0c;NVIDIA 凭借…

多模态大模型 Qwen2.5-VL 的学习之旅

Qwen-VL 是阿里云研发的大规模视觉语言模型&#xff08;Large Vision Language Model, LVLM&#xff09;。Qwen-VL 可以以图像、文本、检测框作为输入&#xff0c;并以文本和检测框作为输出。Qwen-VL 系列模型性能强大&#xff0c;具备多语言对话、多图交错对话等能力&#xff…

Redis 与 Memcache 全面对比:功能、性能与应用场景解析

Redis 和 Memcache 都是常用的内存数据库&#xff0c;以下是它们在多个方面的能力比较&#xff1a; 一、数据类型 Redis&#xff1a;支持丰富的数据类型&#xff0c;如字符串&#xff08;String&#xff09;、哈希&#xff08;Hash&#xff09;、列表&#xff08;List&#x…

Oracle--PL/SQL编程

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 PL/SQL&#xff08;Procedural Language/SQL&#xff09;是Oracle数据库中的一种过程化编程语言&#xff0c;构建于SQL之上&#xff0c;允许编写包含S…

新增优惠券

文章目录 概要整体架构流程技术细节小结 概要 接口分析 一个基本的新增接口&#xff0c;按照Restful风格设计即可&#xff0c;关键是请求参数。之前表分析时已经详细介绍过这个页面及其中的字段&#xff0c;这里不再赘述。 需要特别注意的是&#xff0c;如果优惠券限定了使…

力扣面试经典150题(第二十三题)- KMP算法

问题 给你两个字符串 haystack 和 needle &#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。如果 needle 不是 haystack 的一部分&#xff0c;则返回 -1 。 示例 1&#xff1a; 输入&#xff1a;haysta…

PostgreSQL 的 MVCC 机制了解

PostgreSQL 的 MVCC 机制了解 PostgreSQL 使用多版本并发控制(MVCC)作为其核心并发控制机制&#xff0c;这是它与许多其他数据库系统的关键区别之一。MVCC 允许读操作不阻塞写操作&#xff0c;写操作也不阻塞读操作&#xff0c;从而提供高度并发性。 一 MVCC 基本原理 1.1 M…