Linux内核抢占调度调用链分析
Linux内核__local_bh_enable_ip到preempt_schedule函数的调用链
在 Linux 内核中,从 __local_bh_enable_ip()
到 preempt_schedule()
的调用链涉及底半部(Bottom Half)启用和内核抢占调度机制。以下是详细的调用链分析(基于 Linux 5.x 内核版本):
核心调用链
1 | __local_bh_enable_ip() |
详细步骤分析
1. __local_bh_enable_ip()
- 位置:
kernel/softirq.c
- 功能:启用底半部处理(软中断/Tasklet),并处理未决的软中断。
- 关键逻辑:
- 减少当前任务的软中断禁用计数(
preempt_count
中的软中断部分)。 - 检查是否需要执行未决的软中断。
- 调用
__local_bh_enable()
继续处理。
- 减少当前任务的软中断禁用计数(
1 | void __local_bh_enable_ip(unsigned long ip, unsigned int cnt) |
2. preempt_check_resched()
- 位置:
include/linux/preempt.h
- 功能:检查是否需要抢占当前任务。
- 关键逻辑:
- 检查
TIF_NEED_RESCHED
标志(是否需要重新调度)。 - 如果允许抢占(
preempt_count() == 0
),则调用preempt_schedule()
。
- 检查
1 |
3. preempt_schedule()
- 位置:
kernel/sched/core.c
- 功能:主动触发内核抢占调度。
- 关键逻辑:
- 增加抢占计数器(标记进入抢占上下文)。
- 调用
__schedule()
执行实际的任务切换。 - 减少抢占计数器。
1 | asmlinkage __visible void __sched preempt_schedule(void) |
关键机制说明
底半部启用:
local_bh_disable()
/local_bh_enable()
用于禁用/启用软中断。- 启用时(
__local_bh_enable_ip()
),内核需检查是否有未决的软中断待处理。
抢占触发条件:
- 在启用底半部后,通过
preempt_check_resched()
检查调度标志。 - 若满足以下条件,则触发
preempt_schedule()
:TIF_NEED_RESCHED
被设置(例如:高优先级任务就绪)。- 抢占未被禁用(
preempt_count() == 0
)。
- 在启用底半部后,通过
抢占调度:
preempt_schedule()
是内核主动调度的入口,最终调用__schedule()
切换任务。
场景示例
假设以下事件序列:
- 任务 A 调用
local_bh_disable()
禁用底半部。 - 中断触发,唤醒高优先级任务 B(设置
TIF_NEED_RESCHED
)。 - 任务 A 调用
local_bh_enable()
→ 进入__local_bh_enable_ip()
。 - 在
preempt_check_resched()
中检测到需调度,调用preempt_schedule()
。 - 任务 A 被抢占,任务 B 开始执行。
总结
- 调用链:
__local_bh_enable_ip()
→preempt_check_resched()
→preempt_schedule()
- 意义:
在启用底半部时主动检查抢占,确保高优先级任务能及时响应,提升系统实时性。
Linux内核中中断和抢占是否有关联
是的,在 Linux 内核中,中断和抢占有密切的关联。它们共同构成了内核响应性和实时性的基础机制,主要通过以下方式相互影响:
1. 中断是抢占的主要触发源
中断处理程序(ISR)是设置抢占标志的主要场景:
1 | // 典型中断处理流程 |
- 高优先级任务就绪:当中断唤醒高优先级任务时,会设置当前任务的
TIF_NEED_RESCHED
标志 - 时间片耗尽:定时器中断(
scheduler_tick()
)检查任务时间片,耗尽时设置抢占标志
2. 中断返回路径触发抢占
内核在从中断返回的临界点检查抢占:
1 | // 中断返回路径 (arch/x86/entry/entry_64.S) |
- 如果
TIF_NEED_RESCHED
被设置且抢占未被禁用,触发preempt_schedule_irq()
- 这是内核抢占(Preemption)发生的关键时机
3. 中断上下文与抢占的互斥
内核通过 preempt_count
实现三层保护:
1 | struct thread_info { |
- 进入中断时:
irq_enter()
增加硬中断计数 → 隐式禁止抢占 - 退出中断时:
irq_exit()
减少计数,当计数归零时触发软中断和抢占检查
4. 底半部处理与抢占
软中断/Tasklet 启用时是抢占触发点:
1 | void __local_bh_enable_ip(unsigned long ip, unsigned int cnt) |
- 如您之前提到的调用链:
local_bh_enable()
→preempt_check_resched()
→preempt_schedule()
5. 抢占对中断的影响
- 中断延迟:当内核处于不可抢占区域(如自旋锁临界区)时:
1
2
3
4spin_lock(&mylock);
preempt_disable(); // 实际在 spin_lock 中隐含
// 此期间即使有中断发生,抢占也会延迟到解锁后
spin_unlock(&mylock); // 解锁时检查抢占 - 实时性保障:
CONFIG_PREEMPT_RT
补丁通过将中断线程化,使得中断处理程序自身也可被抢占
关键关联总结
机制 | 中断 → 抢占 | 抢占 → 中断 |
---|---|---|
触发方向 | 中断是抢占的主要触发源 | 抢占状态影响中断延迟 |
关键标志 | TIF_NEED_RESCHED 由中断设置 |
preempt_count 禁止抢占 |
执行点 | 中断返回路径、软中断启用点 | 抢占临界区内延迟中断响应 |
实时性影响 | 决定调度响应速度 | 决定最坏情况中断延迟(WCET) |
典型场景
硬件中断:网卡收包中断
- ISR 快速处理 → 唤醒软中断
NET_RX_SOFTIRQ
- 中断返回前触发
preempt_schedule_irq()
- 若网络处理线程优先级高,立即抢占当前任务
- ISR 快速处理 → 唤醒软中断
定时器中断:
1
2
3
4
5
6scheduler_tick() // 由定时器中断调用
{
task_tick_fair(); // 检查时间片
if (should_resched(prev))
set_tsk_need_resched(prev); // 标记需抢占
}自旋锁临界区:
1
2
3
4spin_lock(&db_lock);
// 此期间即使发生中断,抢占也会被延迟
update_database();
spin_unlock(&db_lock); // 解锁时立即触发抢占检查
结论
中断和抢占是 Linux 内核中深度耦合的核心机制:
- 中断是抢占的触发引擎(通过设置调度标志)
- 抢占是中断的执行结果(在中断返回路径实现)
- 通过
preempt_count
实现原子性保护,协调两者关系 - 共同保障系统的响应性(中断)和公平性(抢占)
这种紧密关联是 Linux 实现实时性、高吞吐量的基础,尤其在服务器、实时系统和嵌入式领域至关重要。
Linux内核中设置schedule_timeout_uninterruptible后调用软中断
在 Linux 内核中,当设置了 schedule_timeout_uninterruptible()
后调用软中断,涉及到任务状态转换、超时机制和底半部处理的复杂交互。以下是详细分析:
核心流程
1 | graph TD |
关键阶段分析
1. schedule_timeout_uninterruptible()
的作用
1 | // kernel/time/timer.c |
- 任务状态:设置为
TASK_UNINTERRUPTIBLE
,表示不响应信号的睡眠 - 超时机制:添加内核定时器,超时后自动唤醒任务
- 调度行为:调用
schedule()
主动让出 CPU
2. 睡眠期间软中断触发
1 | // 典型中断处理流程 |
irq_exit()
关键路径:1
2
3
4
5void irq_exit(void)
{
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); // 执行软中断!
}
3. 软中断执行与任务唤醒
在软中断处理中可能唤醒睡眠任务:
1 | // 例如网络收包软中断 |
三种可能的唤醒场景
场景1: 软中断先于超时唤醒任务
1 | sequenceDiagram |
场景2: 超时先于软中断唤醒
1 | sequenceDiagram |
场景3: 软中断修改超时时间
某些驱动可能在软中断中动态调整超时:
1 | // 示例伪代码 |
关键注意事项
状态保护:
1
2
3
4set_current_state(TASK_UNINTERRUPTIBLE);
if (!condition)
schedule_timeout(timeout);
// 唤醒后需重新检查条件!软中断并发控制:
- 软中断可能在不同 CPU 上并发执行
- 对共享数据的访问需要锁保护:
1
2
3spin_lock_bh(&lock); // 同时禁止软中断
/* 访问共享数据 */
spin_unlock_bh(&lock);
唤醒的原子性:
1
2
3
4
5
6// 正确写法:防止唤醒丢失
set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&queue, &wait); // 先加入等待队列
if (!condition)
schedule_timeout(timeout);
remove_wait_queue(&queue, &wait);
性能影响
延迟抖动:
- 软中断执行可能延迟超时唤醒
- 实时系统需监控
ksoftirqd
线程负载
负载均衡:
1
2# 监控软中断负载
watch -n1 'cat /proc/softirqs'优先级反转风险:
- 高优先级任务等待低优先级的软中断处理
CONFIG_PREEMPT_RT
通过线程化软中断缓解此问题
最佳实践
短睡眠设计:
1
2
3// 避免长超时阻塞软中断
schedule_timeout_uninterruptible(SHORT_TIMEOUT);混合等待机制:
1
2
3
4
5
6while (!condition) {
set_current_state(TASK_UNINTERRUPTIBLE);
if (schedule_timeout(timeout)) // 超时返回非0
break; // 处理超时逻辑
// 否则被软中断唤醒,重试
}软中断优化:
- 将耗时操作转移到工作队列
- 使用
tasklet_hi_schedule()
赋予更高优先级
总结
当 schedule_timeout_uninterruptible()
与软中断交互时:
阶段 | 行为 | 风险 | 缓解措施 |
---|---|---|---|
睡眠设置 | 设置不可中断状态+定时器 | 死锁(若条件永远不满足) | 设置合理超时 |
软中断执行 | 可能在任意CPU异步执行 | 唤醒竞争条件 | 使用等待队列屏障 |
唤醒路径 | 软中断/定时器均可唤醒 | 虚假唤醒 | 循环检查条件 |
超时处理 | 超时后强制唤醒 | 可能错过有效事件 | 记录超时标志 |
这种机制广泛用于:
- 设备驱动等待硬件响应(如磁盘I/O)
- 网络协议栈等待数据包重组
- 同步原语实现(如信号量超时等待)
理解其交互原理对开发可靠的内核代码至关重要,尤其在实时系统和性能敏感场景中。
Linux内核中计算sha256哈希值的接口
在 Linux 内核中计算 SHA-256 哈希值主要通过内核的加密 API 实现。以下是完整的接口使用指南和实现细节:
1. 核心接口和头文件
1 |
2. 关键数据结构
1 | struct crypto_shash *tfm; // 转换对象(算法实例) |
3. 计算 SHA-256 的完整流程
步骤 1: 分配算法实例
1 | tfm = crypto_alloc_shash("sha256", 0, 0); |
步骤 2: 准备操作描述符
1 | desc = kmalloc(sizeof(*desc) + crypto_shash_descsize(tfm), GFP_KERNEL); |
步骤 3: 初始化哈希操作
1 | ret = crypto_shash_init(desc); |
步骤 4: 更新哈希状态(处理数据)
1 | // 处理单个连续缓冲区 |
步骤 5: 获取最终哈希值
1 | u8 hash[SHA256_DIGEST_SIZE]; // 32字节缓冲区 |
步骤 6: 资源清理
1 | out_free_desc: |
4. 完整示例函数
1 |
|
5. 优化技巧
单步计算(适合小数据)
1 | int one_step_sha256(const u8 *data, size_t len, u8 *hash) |
重用算法实例(高性能场景)
1 | // 模块初始化时 |
6. 内核版本差异处理
内核版本 | 变化 | 兼容性处理 |
---|---|---|
< 4.0 | shash_desc 直接使用 |
标准方法 |
≥ 4.0 | 引入 SHASH_DESC_ON_STACK |
使用栈分配减少内存开销 |
≥ 5.11 | crypto_shash_ 函数原型更新 |
检查返回值处理 |
兼容性包装示例:
1 |
|
7. 使用场景
文件系统完整性校验:
1
2// fs/verity.c
fsverity_calc_hash(struct inode *inode, const u8 *data, u8 *hash)内核模块签名验证:
1
2// kernel/module/signing.c
mod_verify_sig(const void *mod, unsigned long *_len)网络协议安全:
1
2// net/ipv4/tcp.c
tcp_v4_md5_hash_skb(char *md5_hash, struct tcp_md5sig_key *key, ...)硬件加密引擎集成:
1
2// drivers/crypto/ccree/cc_hash.c
cc_hash_update(struct ahash_request *req)
8. 调试与验证
检查算法可用性:
1
2
3
4
5$ cat /proc/crypto | grep -A 3 sha256
name : sha256
driver : sha256-generic
module : kernel
priority : 100测试哈希计算:
1
2
3
4
5
6
7u8 test_data[] = "Hello, Kernel SHA-256!";
u8 hash[SHA256_DIGEST_SIZE];
ret = calculate_sha256(test_data, sizeof(test_data)-1, hash);
// 预期结果:
// b7c76f4e... (使用外部工具验证)性能分析:
1
2
3ktime_t start = ktime_get();
// 多次调用哈希函数
ktime_t delta = ktime_sub(ktime_get(), start);
9. 常见错误处理
错误代码 | 原因 | 解决方案 |
---|---|---|
-ENOENT | 算法未注册 | 检查内核配置 CONFIG_CRYPTO_SHA256=y |
-EINVAL | 无效参数 | 检查缓冲区对齐和数据长度 |
-ENOMEM | 内存不足 | 减少并发操作或使用 SHASH_DESC_ON_STACK |
-ENOSYS | 函数未实现 | 检查内核版本兼容性 |
10. 替代方法(已弃用)
旧版方法(避免使用):
1 | // 已弃用:HMAC 接口 |
迁移指南:
- 替换
crypto_hash_
为crypto_shash_
- 转换
struct hash_desc
到struct shash_desc
- 使用分散列表代替直接缓冲区
总结
Linux 内核中 SHA-256 计算的核心接口:
1 | graph LR |
最佳实践:
- 使用
crypto_shash_
系列函数 - 重用算法实例提升性能
- 检查所有返回值
- 使用
SHASH_DESC_ON_STACK
减少内存分配 - 处理内核版本兼容性问题
这些接口广泛应用于内核的加密子系统、文件系统完整性保护、网络协议安全等核心功能,是内核安全机制的基础设施之一。