一、Linux操作系统分析 课程收获总结
1. 选课目的
本人自身的学习方向即为c/c++开发方向,所以学习 Linux 的相关知识是必须的,而《linux操作系统分析》这门课程是我很想要学习的一个方向,课程学习结束后,在数场面试中我都频繁的被询问到了很多课堂上或实验上所学到的内容,让我无比的庆幸此前向两位老师学习了此门课程。
2. 课程收获
通过本课程的学习,我深刻理解了 Linux 操作系统的基本原理和高级特性,收获颇丰。首先,通过孟老师对Linux操作系统概览的介绍,掌握了Linux系统的历史背景及其发展历程,认识到其在自由软件界的重要地位和影响力。
其次,在实验部分,实验一通过将简单的C程序汇编为不同架构的汇编代码,深入分析了存储程序计算机的基本原理,理解了函数调用堆栈在程序执行过程中的重要作用,为后续实验中的内核开发学习奠定了坚实的基础。 在构建极简内核及调试环境的实验二中,我通过孟老师给予的学习资源和实验参考,进行了学习 Linux 内核的基本构造和源码阅读的实践,学会了如何使用 QEMU 进行内核调试,增强自己对 Linux 内核架构的理解。通过vscode+gdb断点分析,了解了 Linux 内核的启动过程,初步掌握了系统引导和初始化流程的关键环节。
在后续对系统调用及进程管理等内容的逐步学习中,我提升了对操作系统如何管理计算资源,尤其是在进程调度和进程切换方面的认知,具体了解了如何通过系统调用实现进程的创建与执行。同时,掌握了ELF目标文件格式及其在程序编译、链接中的重要性,增强了对可执行程序工作原理的理解。
在李老师的课程中,我进一步学习了中断处理、设备驱动程序、字符设备等内容,在中断处理机制的学习中,深入理解了从硬件触发到内核响应的全流程。通过软中断机制的学习,明确了其作为硬中断 “下半部” 的特点。同时,还进行了字符设备驱动开发学习,此外,对 VFS 文件系统的超级块、索引节点、文件对象、目录项四大核心数据结构有了清晰认识,理解了其对不同文件系统的统一抽象机制。
本课程通过理论讲解、实验实操与代码分析的结合,使我在系统级思维、工程实践力和技术前瞻性等方面均得到显著提升。不仅建立了从汇编指令到内核模块的跨层认知,掌握了内核调试、驱动开发、中断分析等实操技能,还理解了容器、虚拟化等技术的内核支撑原理。
二、详解linux中断处理过程
1. 中断的基本过程
首先,以一个在近期本人面试中很常见、但也很难回答全面的问题来进行引入:linux系统是怎么进行网络包的接收的?以下是一个简化的此过程流程,描述了从网络包的接收至最终用户空间处理的整个过程:
当网络包到达计算机的网络接口(如以太网卡、无线网卡)时,网络接口硬件会将这个数据包传递给操作系统。网络接口的驱动程序负责与硬件通信。它会将接收到的网络包从硬件缓存复制到内存中,并将其排队到适当的内存缓冲区。接下来按照计算机网络的分层来逐层进行数据包的处理。
整个过程从linux系统的中断处理的视角来看,其实是由以下两个部分组成的:在网络包到达时,产生硬件中断的触发,硬件中断处理完毕后,后半部的软中断处理开始处理后续数据。
这里涉及到中断的概念,中断是一种重要的计算机系统机制,它允许CPU在执行程序时被外部事件(如硬件信号)打断,从而暂停当前的任务并即时处理其他重要的事件。
以下是中断处理的流程图:
当中断发生时,设备首先通过中断控制器向 CPU 发送中断请求,中断控制器会将 IRQ 映射为对应的中断向量号,并将该向量号传递给 CPU。
CPU 检测到中断信号后,会立即进入硬件响应阶段。首先,它会自动将当前程序的关键寄存器保存到内核栈中,这一操作确保了被中断程序的状态能够被完整记录。接着,CPU 会从用户态切换到内核态,获得更高的权限以执行内核代码。随后,CPU 根据接收到的中断向量号,从中断描述符表中查找对应的中断门,进而跳转到相应的中断处理入口函数。
进入内核中断处理阶段后,汇编层的入口函数会首先执行公共的中断处理代码。这之后,系统会将通用寄存器保存到栈中,从而完整保存当前的执行上下文。接下来,内核会根据中断号调用设备驱动预先注册的中断处理函数(例如网卡的中断处理函数),执行与该设备相关的特定操作。如果某些任务不需要立即处理,内核会触发中断下半部处理,将这些任务延迟执行,以提高系统的响应速度。
2. 中断过程中的重要概念解释
1.为什么需要中断?
中断机制是操作系统高效工作的基础,它通过快速响应外部事件、减少资源浪费、支持多任务处理等方式,使计算机能够高效、灵活地运行。没有中断机制,现代计算机将面临效率低下、响应迟缓和资源浪费等问题,因此中断是构建高效和可靠计算机系统的关键。
在没有中断机制的情况下,CPU需要不断地轮询(polling)外部设备以检查它们的状态,这会导致许多资源被浪费。中断允许设备在准备好数据时主动通知CPU,从而减少不必要的CPU周期浪费。
中断允许操作系统在发生重要事件时(如设备输入、定时器到期、网络数据到达等)快速响应。这种快速响应对于实时系统至关重要,例如在音视频处理、工业自动化和网络通信等领域。
中断机制使得操作系统可以有效地管理多任务的执行。当一个任务被中断,CPU可以切换到另一个任务或处理程序中,从而实现多任务处理。定时中断用于调度轮转任务,确保所有任务都有机会获得 CPU 时间。
中断提供了一种硬件和软件之间异步通信的方式,使得操作系统可以不必了解具体设备的状态。设备通过中断向操作系统报告完成操作或请求服务,从而将设备驱动程序与操作系统内核解耦,提高了系统的灵活性和可维护性。
2. 什么是软中断和硬中断?
硬中断是由外部硬件设备触发的信号。它是设备向CPU发送的一种请求,以告知CPU该设备需要处理的事件或状态变化。硬中断通常由外部设备(如键盘、鼠标、网络接口卡、磁盘驱动器等)通过中断请求线(IRQ)向CPU发送信号。当设备完成数据传输、发生错误或状态变化时会产生硬中断。以下为硬中断的特点:
-
异步: 硬中断是异步事件,不受当前正在执行的程序控制。
-
快速响应: 硬中断通常会被立即处理,以确保及时响应设备的请求。
-
优先级: 硬中断可以设置优先级,以便高优先级的中断可以中断低优先级的中断处理。
软中断是由程序或操作系统触发的中断。它通常是在执行某个软件指令时发起,允许程序请求操作系统执行特定的操作,如系统调用或服务请求。
软中断是通过程序中特定的中断指令来触发的。例如,在Linux中,一些系统调用可能通过软中断来切换模式,从用户态进入内核态,以便执行特权操作。以下为软中断的特点:
-
同步: 软中断通常是同步事件,发生在程序的控制流中。
-
可编程性: 软中断的触发是可以编程控制的,程序员可以在需要时明确调用。
-
常用于系统调用: 软中断被广泛用于实现系统调用,实现用户空间与内核空间之间的交互。
3. 中断过程中涉及到的核心数据结构
中断描述符表(IDT - Interrupt Descriptor Table)
-
作用:存储中断 / 异常的处理入口,每个条目描述一个中断门。
-
定义:在
arch/x86/include/asm/desc_defs.h
中定义。 -
初始化:内核启动时通过
trap_init()
(在arch/x86/kernel/traps.c
)填充 IDT。
中断请求描述符(struct irq_desc
)
-
作用:描述一个中断号的所有信息,包括处理函数和状态。
-
定义(简化版,位于
include/linux/irqdesc.h
):
struct irq_desc {
unsigned int irq;
struct irq_chip *chip;
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
unsigned int status;
unsigned int depth;
unsigned int wake_depth;
unsigned int irq_count;
unsigned int irqs_unhandled;
spinlock_t lock;
struct irqaction *action;
struct irq_desc *parent;
struct kobject kobj;
const char *name;
};
中断处理函数链表(struct irqaction
)
-
作用:管理同一中断号下的多个设备处理函数。
-
定义(位于
include/linux/interrupt.h
):
struct irqaction {
irq_handler_t handler;
void *dev_id;
unsigned long flags;
const char *name;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
void *thread_fn;
wait_queue_head_t wait;
struct task_struct *thread;
int irq_thread;
unsigned long thread_flags;
unsigned long thread_mask;
const char *thread_name;
};
寄存器上下文结构体(pt_regs
)
-
作用:在中断处理过程中,用于保存通用寄存器(EAX、EBX 等)的状态,形成完整的上下文,以便在中断返回时恢复现场。
-
定义(位于
arch/x86/include/asm/ptrace.h
):
struct pt_regs {
unsigned long ebx;
unsigned long ecx;
unsigned long edx;
unsigned long esi;
unsigned long edi;
unsigned long ebp;
unsigned long eax;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
unsigned long orig_eax;
unsigned long eip;
unsigned long cs;
unsigned long eflags;
unsigned long esp;
unsigned long ss;
};
软中断向量表(softirq_vec
)
-
作用:用于注册软中断处理函数,每个软中断号对应一个处理函数。
-
定义(位于
include/linux/interrupt.h
):
struct softirq_action {
void (*action)(struct softirq_action *);
};
extern struct softirq_action softirq_vec[NR_SOFTIRQS];
网络设备结构体(struct net_device
)
-
作用:在网络中断处理中,代表网络设备,包含设备的各种属性和操作函数指针。
-
定义(简化版,位于
include/linux/netdevice.h
)
4. 中断过程中堆栈指针的变化
-
保存现场信息:中断发生时,CPU 会暂停当前执行的任务,将当前任务的相关寄存器值保存到内核堆栈中。这包括通用寄存器(如 x86 架构中的 EAX、EBX 等)、程序计数器(PC)、状态寄存器等。这些寄存器的值记录了当前任务的执行状态,保存它们是为了在中断处理完成后能够恢复到中断发生前的状态,继续执行原任务。在 x86 架构中,中断处理程序入口会使用一系列push指令将寄存器值压入内核堆栈。
-
切换堆栈指针:进入中断处理程序前,CPU 会切换到内核堆栈进行操作。这是因为中断处理需要在内核态执行,且要保证与用户态进程的堆栈隔离,避免相互干扰。在 x86 架构下,会通过特定的寄存器(如ss和esp)切换到预先设置好的内核堆栈,确保中断处理程序有独立的堆栈空间用于数据存储和函数调用。
-
传递中断参数:部分中断相关信息会被压入内核堆栈,作为参数传递给中断处理程序。例如,中断号会被压入堆栈,中断处理程序可据此判断中断源,从而执行相应处理逻辑。不同类型的中断可能还会传递其他参数,如设备号(对于设备中断)等,帮助中断处理程序准确处理中断事件。
-
调用中断处理函数:内核堆栈保存信息和传递参数后,会调用对应的中断处理函数。该函数执行具体的中断处理任务,如处理设备 I/O 请求、更新系统时间(时钟中断)等。中断处理函数执行时,可能会在堆栈中保存局部变量、调用其他函数(其参数和返回地址也存于堆栈),导致堆栈进一步变化。
-
恢复现场信息:中断处理完成后,内核会从内核堆栈中恢复保存的寄存器值,使 CPU 状态回到中断发生前。这通过一系列pop指令实现,将堆栈中的寄存器值弹回相应寄存器。恢复程序计数器的值,让 CPU 继续执行原任务的下一条指令,完成中断处理并返回原任务执行流程。
5. 详细解读软中断
软中断是操作系统中一种特殊的中断机制,它利用软件方式模拟硬件中断,实现宏观上的异步执行效果,在系统任务处理中发挥着重要作用。以下从多个方面对软中断进行介绍:
-
概念:软中断是由软件(程序)触发的中断 ,通常是通过执行特定指令(如 int 指令)或系统调用产生。它与硬中断相对应,硬中断由硬件设备触发,而软中断一般是硬中断服务程序对内核的中断 ,用于处理一些不能被中断的工作或延迟执行的任务。在 Linux 系统中,软中断是 “底半处理” 的升级,用于处理内核级别的任务和事件,不依赖于外部设备的中断请求,由内核代码在适当的时机主动触发。
-
触发方式:由操作系统内核程序触发,具有确定性和可预测性。比如在 Linux 中,内核代码可以通过调用raise_softirq()函数来触发软中断。
-
处理流程
-
注册:内核模块或驱动程序首先需要在内核中注册软中断处理函数,并分配一个唯一的软中断号。在 Linux 内核中,使用open_softirq()函数来注册软中断的处理函数,在软中断向量表中为指定的软中断编号设置处理函数 。
-
触发:当特定的事件或条件发生时,调用相应的触发函数(如 Linux 中的raise_softirq() )触发软中断。
-
处理:一旦软中断被触发,内核会执行软中断处理函数。软中断处理函数通常会完成一些高优先级的任务,例如时间管理、网络数据包处理等。软中断处理分为上半部和下半部,上半部快速处理中断,通常会暂时关闭中断请求,主要负责处理与硬件紧密相关或时间敏感的任务;下半部延迟处理上半部未完成的工作,可以在多个 CPU 上并发执行。如果软中断处理函数需要执行耗时较长的操作,可以将一部分处理工作推迟到延迟处理阶段,通过任务队列或工作队列等机制实现 。
-
-
应用场景:主要用于程序请求操作系统服务或处理内部异常,如文件操作、网络通信、时间管理、进程调度等。例如,网络软中断用于处理网络数据包的接收和发送;定时器软中断用于处理系统的定时任务,如超时处理、周期性任务执行等 。
3. 实际的一些中断处理的例子
1. 网络包的到达(硬中断、软中断都发生)
1、硬件中断触发:网络包到达与中断产生
-
网络包到达网卡
当网络数据包通过物理链路(如网线)到达网卡(NIC)时,网卡将数据存入其内部缓冲区,并通过 DMA(直接内存访问)技术将数据搬运到系统内存的指定区域。 -
中断信号发送
网卡完成数据接收后,会向 CPU 发送一个硬件中断请求(IRQ),通知内核 “有新的网络包到达”。中断信号通过主板的中断控制器(如 APIC)传递至目标 CPU 核心。 -
CPU 响应中断
CPU 在执行当前指令周期结束后,检测到中断信号,会暂停当前任务,保存上下文(如寄存器状态),并根据中断向量表找到对应的中断处理程序入口。
2、顶半部与底半部处理
1. 顶半部:快速处理硬件事件
-
中断屏蔽与上下文切换
CPU 进入中断处理后,会先屏蔽同类型中断(避免嵌套),并切换至内核态。 -
调用网卡驱动的中断处理函数
内核根据中断向量找到网卡驱动注册的中断处理函数(如e1000_intr
),该函数会:-
读取网卡寄存器,确认中断来源(如接收完成中断);
-
从网卡缓冲区获取数据包描述符(Descriptor),记录数据包的内存地址、长度等信息;
-
清除网卡中断标志,允许后续中断再次触发。
-
-
调度底半部处理
顶半部不直接处理数据包内容,而是通过netif_rx()
或新接口netif_receive_skb()
将数据包提交给底半部处理,并快速返回,恢复被中断的任务。
2. 底半部:数据包解析与协议处理
底半部在顶半部返回后异步执行,特点是可中断、允许调度,负责完整的数据包处理。底半部通过软中断(类型为NET_RX_SOFTIRQ
)实现,由net_rx_action()
函数驱动。软中断可被任务调度打断,避免长时间占用 CPU。
-
数据包提取与初步处理
net_rx_action()
会从队列中取出数据包(skb 结构),并执行:-
校验与合法性检查:验证数据包长度、CRC 校验等,丢弃无效包;
-
RSS(Receive Side Scaling)负载均衡:若开启 RSS,根据数据包五元组(源 / 目 IP、端口等)将包分发到不同 CPU 核心的处理队列,提升多核效率。
-
-
协议栈逐层解析
数据包按网络协议栈逐层传递:-
链路层处理:解析以太网头部(如 MAC 地址、类型字段),确定上层协议(如 IPv4、IPv6);
-
网络层处理:解析 IP 头部,检查路由规则,决定数据包是转发还是提交给本地;
-
传输层处理:解析 TCP/UDP 头部,根据端口号找到对应的套接字(Socket),将数据放入接收队列。
-
值得一提的是,网络包持续到达不会一直触发硬中断。(面试常问)
Linux 内核在 2.6 版本引入了 NAPI(New API)机制。当有网络包到达时,网卡会通过 DMA 技术将网络包写入 Ring Buffer(环形缓冲区),然后向 CPU 发起硬件中断。CPU 收到中断后,会调用对应的中断处理函数,在中断处理函数中,会先暂时屏蔽中断,防止后续网络包不断触发中断。
接着,中断处理函数会触发一个软中断(如 NET_RX_SOFTIRQ)。内核中的 ksoftirqd 线程专门负责软中断的处理,它会从 Ring Buffer 中获取数据帧进行处理。在 ksoftirqd 线程处理期间,会以轮询的方式持续处理 Ring Buffer 中已有的网络包,直到处理完当前缓冲区中的所有数据包或达到一定的处理限制,才会重新开启中断,等待下一次网络包触发新的硬中断。
2. 软定时器(软中断)
一、软定时器的核心概念
软定时器是 Linux 内核基于软件实现的定时机制,通过系统时钟(如 jiffies)和软中断(softirq)实现延迟任务执行,不依赖硬件定时器。它允许内核模块或应用程序在指定时间间隔后触发回调函数,常用于超时处理、周期性任务调度等场景,具有轻量级、灵活配置的特点。
二、软定时器的处理流程
1. 初始化与设置阶段
-
注册软中断处理函数:内核启动时,
init_timers
函数会注册软定时器的核心处理函数run_timer_softirq
到软中断向量表(softirq_vec
),关联软中断类型TIMER_SOFTIRQ
。 -
定时器创建与时间计算:
-
当调用
schedule_timeout
等函数设置软定时器时,内核通过timeout + jiffies
计算到期时间expire
(jiffies
记录系统启动后的时钟节拍数)。 -
使用
setup_timer_on_stack
或setup_timer
初始化timer_list
结构体,设置回调函数、到期时间及参数。 -
通过
mod_timer
或add_timer
将定时器插入到系统定时器链表中,等待到期检测。
-
-
核心数据结构
timer_list
(位于include/linux/timer.h
):struct timer_list { struct list_head entry; // 定时器链表节点 unsigned long expires; // 到期时间(jiffies) void (*function)(unsigned long); // 回调函数 unsigned long data; // 传递给回调函数的参数 struct tvec_base *base; // 定时器基数(用于组织定时器) u32 flags; int slack; };
2. 检测与执行阶段
-
时钟中断触发检测:每次时钟中断(如周期性 Tick)处理结束后,内核会检查是否有软定时器到期。时钟中断处理函数(如
update_process_times
)会触发软中断TIMER_SOFTIRQ
。 -
软中断处理函数执行:
run_timer_softirq
函数作为软中断处理入口,会:-
获取当前 CPU 的定时器基数结构
tvec_base
,该结构维护了不同时间粒度的定时器链表(采用哈希 + 层次化设计,如 TVEC、TVEC_SZ)。 -
比较
jiffies
与base->timer_jiffies
,若jiffies
超过到期时间,则调用run_timers
处理所有到期定时器。
-
-
回调函数执行:
run_timers
遍历到期定时器链表,调用每个定时器的function
回调函数,并传递data
参数。若回调函数涉及进程唤醒(如process_timeout
),则会唤醒等待该定时器的进程。
3. 清理与移除阶段
-
定时器回调函数执行完毕或主动取消时,通过
del_timer
或del_timer_sync
移除定时器:-
del_timer
:非同步移除,若定时器正在其他 CPU 上执行则返回 0,否则从链表删除并释放资源。 -
del_timer_sync
:同步移除,等待所有 CPU 上的定时器操作完成后再删除,确保安全性。
-
-
对于栈上创建的定时器(如
setup_timer_on_stack
),需调用destroy_timer_on_stack
显式释放,避免内存泄漏。
三、结语
1.总结
通过本文,可以学习到中断的一系列概念、处理过程和相关的实例,了解重要的数据结构,对面试和学习起到一定的帮助。
非常感谢孟老师和李老师的精心教学,本课程内容丰富多样,贴近底层,对于学习linux系统十分有帮助,十分推荐后续学弟学妹选课学习。
2.参考资料
《庖丁解牛Linux操作系统分析》 linuxkernel: 庖丁解牛Linux操作系统分析