Linux内核__local_bh_enable_ip到preempt_schedule函数的调用链

在 Linux 内核中,从 __local_bh_enable_ip()preempt_schedule() 的调用链涉及底半部(Bottom Half)启用和内核抢占调度机制。以下是详细的调用链分析(基于 Linux 5.x 内核版本):


核心调用链

1
2
3
4
5
__local_bh_enable_ip()
→ local_bh_enable()
→ __local_bh_enable()
→ preempt_check_resched()
→ preempt_schedule()

详细步骤分析

1. __local_bh_enable_ip()

  • 位置kernel/softirq.c
  • 功能:启用底半部处理(软中断/Tasklet),并处理未决的软中断。
  • 关键逻辑
    • 减少当前任务的软中断禁用计数(preempt_count 中的软中断部分)。
    • 检查是否需要执行未决的软中断。
    • 调用 __local_bh_enable() 继续处理。
1
2
3
4
5
6
7
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
// ... 更新 preempt_count ...
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq(); // 执行未决软中断
preempt_check_resched(); // → 触发抢占检查
}

2. preempt_check_resched()

  • 位置include/linux/preempt.h
  • 功能:检查是否需要抢占当前任务。
  • 关键逻辑
    • 检查 TIF_NEED_RESCHED 标志(是否需要重新调度)。
    • 如果允许抢占(preempt_count() == 0),则调用 preempt_schedule()
1
2
3
4
5
#define preempt_check_resched() \
do { \
if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
preempt_schedule(); \
} while (0)

3. preempt_schedule()

  • 位置kernel/sched/core.c
  • 功能:主动触发内核抢占调度。
  • 关键逻辑
    • 增加抢占计数器(标记进入抢占上下文)。
    • 调用 __schedule() 执行实际的任务切换。
    • 减少抢占计数器。
1
2
3
4
5
6
7
8
asmlinkage __visible void __sched preempt_schedule(void)
{
do {
preempt_disable();
__schedule(true); // 核心调度逻辑
preempt_enable();
} while (need_resched());
}

关键机制说明

  1. 底半部启用

    • local_bh_disable()/local_bh_enable() 用于禁用/启用软中断。
    • 启用时(__local_bh_enable_ip()),内核需检查是否有未决的软中断待处理。
  2. 抢占触发条件

    • 在启用底半部后,通过 preempt_check_resched() 检查调度标志。
    • 若满足以下条件,则触发 preempt_schedule()
      • TIF_NEED_RESCHED 被设置(例如:高优先级任务就绪)。
      • 抢占未被禁用(preempt_count() == 0)。
  3. 抢占调度

    • preempt_schedule() 是内核主动调度的入口,最终调用 __schedule() 切换任务。

场景示例

假设以下事件序列:

  1. 任务 A 调用 local_bh_disable() 禁用底半部。
  2. 中断触发,唤醒高优先级任务 B(设置 TIF_NEED_RESCHED)。
  3. 任务 A 调用 local_bh_enable() → 进入 __local_bh_enable_ip()
  4. preempt_check_resched() 中检测到需调度,调用 preempt_schedule()
  5. 任务 A 被抢占,任务 B 开始执行。

总结

  • 调用链
    __local_bh_enable_ip()preempt_check_resched()preempt_schedule()
  • 意义
    在启用底半部时主动检查抢占,确保高优先级任务能及时响应,提升系统实时性。

Linux内核中中断和抢占是否有关联

是的,在 Linux 内核中,中断和抢占有密切的关联。它们共同构成了内核响应性和实时性的基础机制,主要通过以下方式相互影响:


1. 中断是抢占的主要触发源

中断处理程序(ISR)是设置抢占标志的主要场景:

1
2
3
4
5
6
7
8
// 典型中断处理流程
irq_handler()
{
// 1. 处理硬件中断
// 2. 唤醒等待任务或设置调度标志
set_tsk_need_resched(current); // 设置 TIF_NEED_RESCHED
// 3. 退出中断处理
}
  • 高优先级任务就绪:当中断唤醒高优先级任务时,会设置当前任务的 TIF_NEED_RESCHED 标志
  • 时间片耗尽:定时器中断(scheduler_tick())检查任务时间片,耗尽时设置抢占标志

2. 中断返回路径触发抢占

内核在从中断返回的临界点检查抢占:

1
2
3
4
5
// 中断返回路径 (arch/x86/entry/entry_64.S)
ret_from_intr:
testl $X86_EFLAGS_IF, PT_EFLAGS(%rsp) // 检查是否开中断
jz ret_intr // 关中断则直接返回
call preempt_schedule_irq // → 抢占检查核心
  • 如果 TIF_NEED_RESCHED 被设置且抢占未被禁用,触发 preempt_schedule_irq()
  • 这是内核抢占(Preemption)发生的关键时机

3. 中断上下文与抢占的互斥

内核通过 preempt_count 实现三层保护:

1
2
3
4
5
6
struct thread_info {
unsigned long preempt_count; // 格式: [抢占计数|软中断|硬中断]
// 0x3000000 - 硬中断计数 (IRQ)
// 0x0FF0000 - 软中断计数 (SoftIRQ)
// 0x00000FF - 抢占计数 (Preempt)
};
  • 进入中断时irq_enter() 增加硬中断计数 → 隐式禁止抢占
  • 退出中断时irq_exit() 减少计数,当计数归零时触发软中断和抢占检查

4. 底半部处理与抢占

软中断/Tasklet 启用时是抢占触发点:

1
2
3
4
5
6
7
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
// ...
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq(); // 执行软中断
preempt_check_resched(); // → 检查抢占 !!!
}
  • 如您之前提到的调用链:local_bh_enable()preempt_check_resched()preempt_schedule()

5. 抢占对中断的影响

  • 中断延迟:当内核处于不可抢占区域(如自旋锁临界区)时:
    1
    2
    3
    4
    spin_lock(&mylock);
    preempt_disable(); // 实际在 spin_lock 中隐含
    // 此期间即使有中断发生,抢占也会延迟到解锁后
    spin_unlock(&mylock); // 解锁时检查抢占
  • 实时性保障CONFIG_PREEMPT_RT 补丁通过将中断线程化,使得中断处理程序自身也可被抢占

关键关联总结

机制 中断 → 抢占 抢占 → 中断
触发方向 中断是抢占的主要触发源 抢占状态影响中断延迟
关键标志 TIF_NEED_RESCHED 由中断设置 preempt_count 禁止抢占
执行点 中断返回路径、软中断启用点 抢占临界区内延迟中断响应
实时性影响 决定调度响应速度 决定最坏情况中断延迟(WCET)

典型场景

  1. 硬件中断:网卡收包中断

    • ISR 快速处理 → 唤醒软中断 NET_RX_SOFTIRQ
    • 中断返回前触发 preempt_schedule_irq()
    • 若网络处理线程优先级高,立即抢占当前任务
  2. 定时器中断

    1
    2
    3
    4
    5
    6
    scheduler_tick() // 由定时器中断调用
    {
    task_tick_fair(); // 检查时间片
    if (should_resched(prev))
    set_tsk_need_resched(prev); // 标记需抢占
    }
  3. 自旋锁临界区

    1
    2
    3
    4
    spin_lock(&db_lock);
    // 此期间即使发生中断,抢占也会被延迟
    update_database();
    spin_unlock(&db_lock); // 解锁时立即触发抢占检查

结论

中断和抢占是 Linux 内核中深度耦合的核心机制

  1. 中断是抢占的触发引擎(通过设置调度标志)
  2. 抢占是中断的执行结果(在中断返回路径实现)
  3. 通过 preempt_count 实现原子性保护,协调两者关系
  4. 共同保障系统的响应性(中断)和公平性(抢占)

这种紧密关联是 Linux 实现实时性、高吞吐量的基础,尤其在服务器、实时系统和嵌入式领域至关重要。

Linux内核中设置schedule_timeout_uninterruptible后调用软中断

在 Linux 内核中,当设置了 schedule_timeout_uninterruptible() 后调用软中断,涉及到任务状态转换、超时机制和底半部处理的复杂交互。以下是详细分析:


核心流程

1
2
3
4
5
6
7
8
9
10
11
graph TD
A[调用 schedule_timeout_uninterruptible] --> B[设置 TASK_UNINTERRUPTIBLE]
B --> C[添加定时器到队列]
C --> D[调用 schedule]
D --> E[任务进入睡眠]
E --> F[硬件中断发生]
F --> G[ISR 触发软中断]
G --> H[中断返回路径执行软中断]
H --> I[软中断处理中唤醒任务?]
I -->|是| J[修改任务状态为 TASK_RUNNING]
I -->|否| K[定时器超时唤醒]

关键阶段分析

1. schedule_timeout_uninterruptible() 的作用

1
2
3
4
5
6
// kernel/time/timer.c
signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
__set_current_state(TASK_UNINTERRUPTIBLE); // 设置不可中断状态
return schedule_timeout(timeout); // 设置定时器并调度
}
  • 任务状态:设置为 TASK_UNINTERRUPTIBLE,表示不响应信号的睡眠
  • 超时机制:添加内核定时器,超时后自动唤醒任务
  • 调度行为:调用 schedule() 主动让出 CPU

2. 睡眠期间软中断触发

1
2
3
4
5
6
7
8
9
10
11
12
// 典型中断处理流程
irq_handler()
{
/* 1. 硬件中断处理 */
handle_hardirq();

/* 2. 触发软中断 */
raise_softirq(NET_RX_SOFTIRQ); // 例如网卡收包

/* 3. 中断退出路径 */
irq_exit();
}
  • irq_exit() 关键路径
    1
    2
    3
    4
    5
    void irq_exit(void)
    {
    if (!in_interrupt() && local_softirq_pending())
    invoke_softirq(); // 执行软中断!
    }

3. 软中断执行与任务唤醒

在软中断处理中可能唤醒睡眠任务

1
2
3
4
5
6
7
8
9
10
11
12
// 例如网络收包软中断
static void net_rx_action(struct softirq_action *h)
{
while (!list_empty(&queue)) {
struct sk_buff *skb = dequeue_packet();
/* 处理数据包... */

// 可能唤醒等待数据的任务
if (skb->target_task->state == TASK_UNINTERRUPTIBLE)
wake_up_process(skb->target_task);
}
}

三种可能的唤醒场景

场景1: 软中断先于超时唤醒任务

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant Task
participant Timer
participant SoftIRQ

Task->>Timer: 设置超时(100ms)
Task->>Task: 进入UNINTERRUPTIBLE睡眠
Note over Task: 睡眠中...
SoftIRQ->>SoftIRQ: 执行(50ms时)
SoftIRQ->>Task: wake_up_process()
Task->>Task: 状态→RUNNING
Timer->>Timer: 超时(100ms)时任务已唤醒

场景2: 超时先于软中断唤醒

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant Task
participant Timer
participant SoftIRQ

Task->>Timer: 设置超时(100ms)
Task->>Task: 进入UNINTERRUPTIBLE睡眠
Note over Task: 睡眠中...
Timer->>Task: 超时唤醒(100ms)
Task->>Task: 状态→RUNNING
SoftIRQ->>SoftIRQ: 稍后执行(120ms)

场景3: 软中断修改超时时间

某些驱动可能在软中断中动态调整超时

1
2
3
4
5
6
7
// 示例伪代码
void softirq_handler()
{
if (need_more_time) {
mod_timer(&task_timer, jiffies + NEW_TIMEOUT); // 延长超时
}
}

关键注意事项

  1. 状态保护

    1
    2
    3
    4
    set_current_state(TASK_UNINTERRUPTIBLE);
    if (!condition)
    schedule_timeout(timeout);
    // 唤醒后需重新检查条件!
  2. 软中断并发控制

    • 软中断可能在不同 CPU 上并发执行
    • 对共享数据的访问需要锁保护:
      1
      2
      3
      spin_lock_bh(&lock); // 同时禁止软中断
      /* 访问共享数据 */
      spin_unlock_bh(&lock);
  3. 唤醒的原子性

    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);

性能影响

  1. 延迟抖动

    • 软中断执行可能延迟超时唤醒
    • 实时系统需监控 ksoftirqd 线程负载
  2. 负载均衡

    1
    2
    # 监控软中断负载
    watch -n1 'cat /proc/softirqs'
  3. 优先级反转风险

    • 高优先级任务等待低优先级的软中断处理
    • CONFIG_PREEMPT_RT 通过线程化软中断缓解此问题

最佳实践

  1. 短睡眠设计

    1
    2
    3
    // 避免长超时阻塞软中断
    #define SHORT_TIMEOUT (HZ/10) // 100ms
    schedule_timeout_uninterruptible(SHORT_TIMEOUT);
  2. 混合等待机制

    1
    2
    3
    4
    5
    6
    while (!condition) {
    set_current_state(TASK_UNINTERRUPTIBLE);
    if (schedule_timeout(timeout)) // 超时返回非0
    break; // 处理超时逻辑
    // 否则被软中断唤醒,重试
    }
  3. 软中断优化

    • 将耗时操作转移到工作队列
    • 使用 tasklet_hi_schedule() 赋予更高优先级

总结

schedule_timeout_uninterruptible() 与软中断交互时:

阶段 行为 风险 缓解措施
睡眠设置 设置不可中断状态+定时器 死锁(若条件永远不满足) 设置合理超时
软中断执行 可能在任意CPU异步执行 唤醒竞争条件 使用等待队列屏障
唤醒路径 软中断/定时器均可唤醒 虚假唤醒 循环检查条件
超时处理 超时后强制唤醒 可能错过有效事件 记录超时标志

这种机制广泛用于:

  1. 设备驱动等待硬件响应(如磁盘I/O)
  2. 网络协议栈等待数据包重组
  3. 同步原语实现(如信号量超时等待)

理解其交互原理对开发可靠的内核代码至关重要,尤其在实时系统和性能敏感场景中。

Linux内核中计算sha256哈希值的接口

在 Linux 内核中计算 SHA-256 哈希值主要通过内核的加密 API 实现。以下是完整的接口使用指南和实现细节:


1. 核心接口和头文件

1
2
3
#include <crypto/hash.h>      // 哈希操作核心头文件
#include <crypto/sha.h> // SHA 算法特定定义
#include <linux/scatterlist.h> // 分散/聚集列表支持

2. 关键数据结构

1
2
3
struct crypto_shash *tfm;      // 转换对象(算法实例)
struct shash_desc *desc; // 操作描述符
struct scatterlist sg; // 分散列表(用于处理非连续内存)

3. 计算 SHA-256 的完整流程

步骤 1: 分配算法实例

1
2
3
4
5
tfm = crypto_alloc_shash("sha256", 0, 0);
if (IS_ERR(tfm)) {
pr_err("Failed to allocate sha256: %ld\n", PTR_ERR(tfm));
return PTR_ERR(tfm);
}

步骤 2: 准备操作描述符

1
2
3
4
5
6
7
desc = kmalloc(sizeof(*desc) + crypto_shash_descsize(tfm), GFP_KERNEL);
if (!desc) {
ret = -ENOMEM;
goto out_free_tfm;
}

desc->tfm = tfm;

步骤 3: 初始化哈希操作

1
2
3
4
5
ret = crypto_shash_init(desc);
if (ret) {
pr_err("crypto_shash_init failed: %d\n", ret);
goto out_free_desc;
}

步骤 4: 更新哈希状态(处理数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 处理单个连续缓冲区
ret = crypto_shash_update(desc, data, data_len);
if (ret) {
pr_err("crypto_shash_update failed: %d\n", ret);
goto out_free_desc;
}

// 处理多个分散缓冲区(可选)
struct scatterlist sg[2];
sg_init_table(sg, 2);
sg_set_buf(&sg[0], data1, data1_len);
sg_set_buf(&sg[1], data2, data2_len);

ret = crypto_shash_digest(desc, sg, data1_len + data2_len, hash_result);

步骤 5: 获取最终哈希值

1
2
3
4
5
6
7
u8 hash[SHA256_DIGEST_SIZE]; // 32字节缓冲区

ret = crypto_shash_final(desc, hash);
if (ret) {
pr_err("crypto_shash_final failed: %d\n", ret);
goto out_free_desc;
}

步骤 6: 资源清理

1
2
3
4
out_free_desc:
kfree(desc);
out_free_tfm:
crypto_free_shash(tfm);

4. 完整示例函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <crypto/hash.h>
#include <crypto/sha.h>

int calculate_sha256(const u8 *data, size_t len, u8 *out_hash)
{
struct crypto_shash *tfm;
struct shash_desc *desc;
int ret;

// 1. 分配算法实例
tfm = crypto_alloc_shash("sha256", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);

// 2. 创建描述符
desc = kmalloc(sizeof(*desc) + crypto_shash_descsize(tfm), GFP_KERNEL);
if (!desc) {
ret = -ENOMEM;
goto out_tfm;
}

desc->tfm = tfm;

// 3. 初始化哈希操作
ret = crypto_shash_init(desc);
if (ret)
goto out_desc;

// 4. 更新哈希状态
ret = crypto_shash_update(desc, data, len);
if (ret)
goto out_desc;

// 5. 获取最终哈希值
ret = crypto_shash_final(desc, out_hash);

out_desc:
kfree(desc);
out_tfm:
crypto_free_shash(tfm);
return ret;
}

5. 优化技巧

单步计算(适合小数据)

1
2
3
4
5
6
7
8
9
int one_step_sha256(const u8 *data, size_t len, u8 *hash)
{
struct crypto_shash *tfm = crypto_alloc_shash("sha256", 0, 0);
if (IS_ERR(tfm)) return PTR_ERR(tfm);

int ret = crypto_shash_digest(tfm, data, len, hash);
crypto_free_shash(tfm);
return ret;
}

重用算法实例(高性能场景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模块初始化时
static struct crypto_shash *sha_tfm;

int mod_init(void)
{
sha_tfm = crypto_alloc_shash("sha256", 0, 0);
return PTR_ERR_OR_ZERO(sha_tfm);
}

// 每次计算时
int fast_sha256(const u8 *data, size_t len, u8 *hash)
{
SHASH_DESC_ON_STACK(desc, sha_tfm);
desc->tfm = sha_tfm;

int ret = crypto_shash_init(desc) ?:
crypto_shash_update(desc, data, len) ?:
crypto_shash_final(desc, hash);

return ret;
}

6. 内核版本差异处理

内核版本 变化 兼容性处理
< 4.0 shash_desc 直接使用 标准方法
≥ 4.0 引入 SHASH_DESC_ON_STACK 使用栈分配减少内存开销
≥ 5.11 crypto_shash_ 函数原型更新 检查返回值处理

兼容性包装示例

1
2
3
4
5
6
7
8
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
#define my_shash_update crypto_shash_update
#else
static inline int my_shash_update(struct shash_desc *desc, const u8 *data, unsigned int len)
{
return crypto_shash_update(desc, data, len);
}
#endif

7. 使用场景

  1. 文件系统完整性校验

    1
    2
    // fs/verity.c
    fsverity_calc_hash(struct inode *inode, const u8 *data, u8 *hash)
  2. 内核模块签名验证

    1
    2
    // kernel/module/signing.c
    mod_verify_sig(const void *mod, unsigned long *_len)
  3. 网络协议安全

    1
    2
    // net/ipv4/tcp.c
    tcp_v4_md5_hash_skb(char *md5_hash, struct tcp_md5sig_key *key, ...)
  4. 硬件加密引擎集成

    1
    2
    // drivers/crypto/ccree/cc_hash.c
    cc_hash_update(struct ahash_request *req)

8. 调试与验证

  1. 检查算法可用性

    1
    2
    3
    4
    5
    $ cat /proc/crypto | grep -A 3 sha256
    name : sha256
    driver : sha256-generic
    module : kernel
    priority : 100
  2. 测试哈希计算

    1
    2
    3
    4
    5
    6
    7
    u8 test_data[] = "Hello, Kernel SHA-256!";
    u8 hash[SHA256_DIGEST_SIZE];

    ret = calculate_sha256(test_data, sizeof(test_data)-1, hash);

    // 预期结果:
    // b7c76f4e... (使用外部工具验证)
  3. 性能分析

    1
    2
    3
    ktime_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
2
// 已弃用:HMAC 接口
struct crypto_hash *tfm = crypto_alloc_hash("sha256", 0, CRYPTO_ALG_ASYNC);

迁移指南

  1. 替换 crypto_hash_crypto_shash_
  2. 转换 struct hash_descstruct shash_desc
  3. 使用分散列表代替直接缓冲区

总结

Linux 内核中 SHA-256 计算的核心接口:

1
2
3
4
5
6
graph LR
A[crypto_alloc_shash] --> B[shash_desc分配]
B --> C[crypto_shash_init]
C --> D[crypto_shash_update]
D --> E[crypto_shash_final]
E --> F[资源释放]

最佳实践

  1. 使用 crypto_shash_ 系列函数
  2. 重用算法实例提升性能
  3. 检查所有返回值
  4. 使用 SHASH_DESC_ON_STACK 减少内存分配
  5. 处理内核版本兼容性问题

这些接口广泛应用于内核的加密子系统、文件系统完整性保护、网络协议安全等核心功能,是内核安全机制的基础设施之一。