Linux开发学习第七天——虚拟内存和物理内存
一、虚拟内存
1.概念
内核给进程编造的独立地址空间,每个进程都以为自己独占内存。
它们靠MMU(硬件) + 页表(内核)做映射。
每个进程都独有一份,就是之前提到的进程的内存模型。
2.作用
- 隔离进程:A 进程不能直接访问 B 进程内存。
- 扩大地址空间:32 位最多 4G,64 位极大。
- 延迟分配 / 写时复制:
fork、malloc先只建虚拟表,不真占物理页。 - swap 置换:把不常用页换到磁盘,腾出物理内存。
3.映射关系
- 虚拟内存按页(Page,4KB)划分
- 物理内存按页帧(Page Frame,4KB)划分
- 页表保存:
虚拟页号 → 物理页帧号 + 权限
4.关键机制
4.1写时复制
fork时:只复制页表,不复制物理页- 父子只读共享同一物理页
- 一旦要写 → 触发页异常 → 复制一个新物理页→ 既快又省内存
4.2缺页中断
- 访问的虚拟页没有对应物理页
- 内核:
- 分配物理页帧
- 建立映射
- 重新执行指令
malloc很大内存时,真正分配物理内存就是在缺页中断时。
4.3Swap交换
- 物理内存紧张时,内核把不活跃的匿名页换到磁盘
- 释放物理内存给更需要的进程
- 再次访问时,再从 swap 读回(会变慢)
5.相关(联系之前内容)
- PCB (task_struct) → mm_struct → 页表每个进程有自己的页表,所以有独立虚拟空间。
- fork= 复制虚拟空间 + 共享物理页(COW)
- malloc= 先分配虚拟地址,真正用的时候才分配物理页
- 栈帧 / RBP都在虚拟地址的 stack 区,由内核映射到物理页
6.总结
- 虚拟内存:进程看到的 “假地址”,负责隔离、扩容、延迟分配。
- 物理内存:真实内存条,按页帧管理。
- 页表 + MMU:把虚拟地址翻译成物理地址。
- 核心价值:进程隔离 + 高效利用内存 + 支持多进程并发。
二、中断与异常
1.定义
1.1. 中断(Interrupt)
- 来自外部硬件的信号(网卡、磁盘、键盘、时钟)
- 异步发生(与 CPU 指令流无关)
- 内核处理时处于中断上下文(不能睡眠、不能调度)
- 必须快速处理 → 分为上半部 + 下半部
1.2. 异常(Exception)
- CPU 执行指令时发现的错误 / 特殊条件
- 同步发生(必须立刻处理)
- 分为三类:
- Fault(故障):可修复,修复后重新执行原指令例:缺页异常 #PF
- Trap(陷阱):指令执行后触发,执行下一条例:系统调用、断点
- Abort(终止):不可恢复,系统崩溃例:双故障、硬件错误
所有中断/异常最终都会进入内核C语言处理函数。
2.内核核心数据结构
2.1保存所有上下文:
中断 / 异常发生时,硬件 + 内核汇编代码会把全部 CPU 寄存器保存到内核栈上。这个结构在 C 语言里就是:
struct pt_regs { unsigned long r15, r14, r13, r12, rbp, rbx; unsigned long r11, r10, r9, r8; unsigned long rax, rcx, rdx, rsi, rdi; unsigned long orig_rax; // 中断向量号 / 系统调用号 // 硬件自动保存 unsigned long rip; // 被打断的指令地址 unsigned long cs; // 代码段 unsigned long eflags; // 标志位 unsigned long rsp; // 被打断时的栈 unsigned long ss; // 栈段 };所有中断 / 异常处理函数的第一个参数都是struct pt_regs *regs
2.2. 中断描述符:irq_desc
内核管理每一个中断的核心结构体:
struct irq_desc { irq_flow_handler_t handle_irq; // 中断流处理函数 struct irq_chip *chip; // 中断控制器(APIC) struct irqaction *action; // 驱动注册的处理函数链表 void *handler_data; unsigned int irq; // IRQ号 unsigned int state_use_access; };2.3. 驱动注册的中断处理:irqaction
struct irqaction { irq_handler_t handler; // 上半部(硬件处理) irq_handler_t thread_fn; // 下半部(线程) struct task_struct *thread; // 中断线程 unsigned int flags; void *dev_id; char *name; struct irqaction *next; };3.中断处理完整流程(纯 C 语言层)
第一步:硬件发送中断 → CPU
- 外部设备(网卡 / 键盘)发送电信号
- 中断控制器(IO-APIC)转发给 CPU
- CPU暂停当前进程,进入内核态
第二步:硬件 + 汇编保存上下文(一句话)
CPU 自动将rip, cs, rflags, rsp, ss压栈,内核汇编把其余寄存器压栈,形成pt_regs,然后跳转到 C 语言总入口。
第三步:C 语言中断总入口do_IRQ()
void do_IRQ(struct pt_regs *regs) { // 1. 获取中断向量号 int vector = get_vector(regs); // 2. 根据向量号找到中断描述符 struct irq_desc *desc = irq_to_desc(vector); // 3. 执行中断处理流程 generic_handle_irq_desc(desc); }第四步:执行中断流函数handle_edge_irq
void handle_edge_irq(struct irq_desc *desc) { // 屏蔽中断 mask_irq(desc); // 发送中断结束应答(EOI) desc->chip->irq_eoi(desc); // 执行驱动注册的处理函数 handle_irq_event(desc); // 解除屏蔽 unmask_irq(desc); }第五步:执行驱动的中断处理函数(上半部)
irqreturn_t handle_irq_event(struct irq_desc *desc) { struct irqaction *action; for_each_action(action, desc) { // 执行驱动的硬中断 handler irqreturn_t ret = action->handler(irq, action->dev_id); // 如果需要线程化处理,则唤醒线程 if (ret == IRQ_WAKE_THREAD) wake_up_process(action->thread); } return IRQ_HANDLED; }第六步:中断返回
- 恢复
pt_regs - 回到用户态 / 内核态被中断的代码
4.中断的「上半部」与「下半部」(内核核心设计)
4.1. 上半部(Hard IRQ)
- 运行在中断上下文
- 不能睡眠、不能调度、不能调用可能阻塞的函数
- 只做:
- 清除中断标志
- 读取硬件状态
- 启动 DMA
触发下半部
4.2. 下半部(Softirq / Tasklet / Threaded IRQ)
- 可以睡眠、调度、使用 mutex、kmalloc
- 处理:
- 网络包处理
- 磁盘数据拷贝
- 数据封装、协议解析
现代 Linux 推荐:线程化中断(threaded irq)
驱动注册方式:
request_threaded_irq(irq, hard_handler, thread_handler, IRQF_ONESHOT, name, dev);5、异常处理完整流程(以缺页异常 #PF 为例)
异常和中断流程几乎一样,只是处理函数不同。
第一步:CPU 执行指令 → 发现异常
例如:
- 访问未映射的虚拟地址
- 权限错误
- 除零
第二步:硬件 + 汇编保存上下文 → 跳转到 C 函数
一句话:保存寄存器 → 跳转到异常处理 C 函数。
第三步:缺页异常 C 语言入口do_page_fault()
void do_page_fault(struct pt_regs *regs, unsigned long hw_error) { // 1. 读取触发缺页的虚拟地址 unsigned long addr = read_cr2(); // 2. 判断:用户态/内核态、读/写、是否缺页 int user = user_mode(regs); int write = hw_error & PF_WRITE; int present = hw_error & PF_PROT; // 3. 获取当前进程的内存空间 struct mm_struct *mm = current->mm; // 4. 查找虚拟内存区域 VMA struct vm_area_struct *vma = find_vma(mm, addr); // 5. 处理缺页(核心函数) handle_mm_fault(mm, vma, addr, write ? FAULT_FLAG_WRITE : 0); }第四步:真正处理缺页handle_mm_fault()
vm_fault_t handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long addr, unsigned int flags) { // 1. 查找页表 pgd_t *pgd = pgd_offset(mm, addr); pud_t *pud = pud_offset(pgd, addr); pmd_t *pmd = pmd_offset(pud, addr); pte_t *pte = pte_offset_map(pmd, addr); // 2. 处理三种情况: // a. 从未分配过物理页 → 分配新页 // b. 页面被交换到swap → 从swap读回 // c. 写时复制(COW)→ 复制物理页 return do_fault(mm, vma, pgoff, flags, pgd); }第五步:返回
- 对于 Fault 类异常(#PF):重新执行触发异常的指令
- 程序继续正常运行
6、中断上下文 vs 进程上下文(超级重点)
1. 进程上下文
- 运行进程代码
current指向当前进程- 可以睡眠、调度、使用 mutex、kmalloc
2. 中断上下文
- 运行中断 / 异常处理代码
- 不属于任何进程
- 绝对不能睡眠、不能调度、不能使用可能阻塞的函数
- 只能使用:
spin_lockkmalloc(GFP_ATOMIC)- 原子操作
判断是否在中断上下文:
in_interrupt(); // 硬中断/软中断中 → 返回非0 in_irq(); // 硬中断中 → 返回非07、内核中中断 / 异常的关键 API(C 语言)
1. 驱动注册中断
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);2. 注册线程化中断
int request_threaded_irq(unsigned int irq, irq_handler_t hard_fn, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);3. 释放中断
void free_irq(unsigned int irq, void *dev);4. 全局禁止 / 开启中断
local_irq_disable(); // 关中断 local_irq_enable(); // 开中断8、完整总结(深入内核必须理解透)
中断处理流程
硬件中断 → CPU 暂停 → 保存寄存器 →do_IRQ()→ 执行中断流 → 驱动 handler → 触发下半部 → 恢复现场 → 返回
异常处理流程
指令执行错误 → 保存寄存器 →do_*_fault()→ 修复错误(如分配物理页) → 恢复现场 → 重新执行指令
核心特点
- 中断是异步、外部、分上下半部
- 异常是同步、内部、必须立即修复
- 都保存在 pt_regs
- 中断上下文不能睡眠
- 缺页异常是虚拟内存的基石
- 时钟中断是进程调度的基石
三、扩展
1.进程切换
1.1.进程上下文:
(1)进程上文:其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
(2)进程下文:其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。
1.2.中断上下文:
(1)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。
(2)中断下文:执行在内核空间的中断服务程序。
2.系统调用
系统调用就是一种异常——陷入(用户态->内核态)
