Linux Tasklet 深度剖析: 从设计思想到底层实现

Linux Tasklet 深度剖析: 从设计思想到底层实现

1. 引言: 为什么需要 Tasklet?

在深入技术细节之前, 让我们先思考一个根本问题: 为什么 Linux 内核需要 tasklet 这样的机制?

想象一下这样的场景: 你正在厨房做饭 (CPU 在执行主要任务) , 突然门铃响了 (硬件中断) . 你去开门并接收快递 (中断处理) , 但快递需要拆箱、整理物品 (耗时操作) . 明智的做法是先签收快递 (快速响应中断) , 然后回到厨房继续做饭, 等有空时再处理拆箱 (延迟处理耗时部分)

这个 "等有空时再处理" 的哲学, 正是 Linux 中断处理的核心思想. Linux 将中断处理分为两部分:

部分 名称 特点 类比
上半部 Top Half 紧急、快速、不可中断 快递签收
下半部 Bottom Half 延迟、可中断、较耗时 快递拆箱整理

Tasklet 正是众多 "下半部" 实现机制中的一种. 让我们通过一个Mermaid时序图来直观理解:
内核主线程 Tasklet系统 CPU中断处理 硬件设备 内核主线程 Tasklet系统 CPU中断处理 硬件设备 触发硬件中断 执行上半部(快速响应) 调度tasklet(标记为待处理) 中断响应完成 在适当时机触发 执行tasklet函数(下半部)

2. Tasklet 的设计哲学

2.1 核心设计原则

Tasklet 的设计体现了几个关键原则:

  1. 串行化执行: 同一 tasklet 在多个 CPU 上不会并发执行
  2. 原子性调度: tasklet 的调度是原子的, 避免竞争条件
  3. 轻量级: 相比内核线程, tasklet 的开销极小
  4. 确定性和可靠性: 设计简单, 行为可预测

2.2 与其他下半部机制的对比

理解 tasklet 最好的方式之一就是将其与其他机制对比:
Linux下半部机制
Softirq
:完全并行
:静态分配
:性能最高
:使用复杂
Tasklet
:基于软中断
:同类型串行
:动态创建
:简单易用
Workqueue
:进程上下文执行
:可睡眠
:可被调度
:开销较大
Threaded IRQ
:完全在线程上下文
:实时性要求低
:可优先级调度

表格形式对比更加清晰:

特性 软中断 (Softirq) Tasklet 工作队列 (Workqueue) 线程化中断
执行上下文 中断上下文 中断上下文 进程上下文 进程上下文
可睡眠
并发性 完全并行 同类型串行 取决于实现 线程调度
性能开销 最低 很低 较高 最高
使用难度 困难 简单 中等 简单
适用场景 网络、块设备等高性能需求 通用设备驱动 需要睡眠的操作 实时性要求不高的驱动

3. Tasklet 的核心数据结构

3.1 基础结构体

让我们深入内核源码, 看看 tasklet 是如何定义的:

c 复制代码
/* 位于 include/linux/interrupt.h */

struct tasklet_struct {
    struct tasklet_struct *next;      // 链表指针
    unsigned long state;              // 状态标志
    atomic_t count;                   // 引用计数器
    void (*func)(unsigned long);      // 实际的处理函数
    unsigned long data;               // 传递给函数的参数
};

这个看似简单的结构体, 却包含了 tasklet 的全部奥秘. 让我们逐一分析每个字段:

字段 类型 描述
next struct tasklet_struct * 指向下一个 tasklet, 用于链表管理
state unsigned long 状态标志, 控制 tasklet 生命周期
count atomic_t 原子计数器, 为0时tasklet才可执行
func void (*)(unsigned long) 实际要执行的回调函数
data unsigned long 传递给回调函数的参数

3.2 状态标志详解

state 字段是理解 tasklet 行为的关键. 它使用位掩码表示不同的状态:

c 复制代码
/* tasklet 状态标志 */
enum {
    TASKLET_STATE_SCHED,   /* Tasklet 已被调度, 等待执行 */
    TASKLET_STATE_RUN,     /* Tasklet 正在执行中 */
    TASKLET_STATE_PENDING  /* 已废弃, 旧版内核使用 */
};

我们可以通过一个状态转换图来理解 tasklet 的生命周期:
tasklet_schedule()
CPU选取执行
执行完成, 清除SCHED标志
空闲(Idle)
已调度(Scheduled) CPU开始执行
等待执行
执行中
执行中(Running) func()执行完毕
正在运行
执行完成
count == 0: 可被调度

count != 0: 被禁用

3.3 每个CPU的数据结构

Tasklet 的实现依赖于每个CPU的数据结构. 这是实现高效并行处理的关键:

c 复制代码
/* 每个CPU的tasklet链表 */
struct tasklet_head {
    struct tasklet_struct *head;
    struct tasklet_struct **tail;
};

/* 每个CPU有两个tasklet链表 */
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

这里有一个重要的设计细节: 两个优先级 . tasklet_vec 是普通优先级, tasklet_hi_vec 是高优先级. 这种设计允许紧急的 tasklet 优先执行

4. Tasklet 的实现机制深度解析

4.1 调度过程: tasklet_schedule()

让我们从调度开始, 理解 tasklet 的生命周期:

c 复制代码
void tasklet_schedule(struct tasklet_struct *t)
{
    /* 1. 检查tasklet是否已被调度 */
    if (test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        return; /* 已经在调度队列中, 直接返回 */
    
    /* 2. 添加到当前CPU的链表中 */
    __tasklet_schedule(t);
}
EXPORT_SYMBOL(tasklet_schedule);

实际的调度函数 __tasklet_schedule() 更加精彩:

c 复制代码
void __tasklet_schedule(struct tasklet_struct *t)
{
    unsigned long flags;
    
    /* 获取当前CPU的ID */
    local_irq_save(flags);  /* 保存中断状态并禁用本地中断 */
    
    /* 将tasklet添加到当前CPU的链表中 */
    t->next = NULL;
    *__this_cpu_read(tasklet_vec.tail) = t;
    __this_cpu_write(tasklet_vec.tail, &(t->next));
    
    /* 触发软中断 */
    raise_softirq_irqoff(TASKLET_SOFTIRQ);
    
    local_irq_restore(flags);  /* 恢复中断状态 */
}

这个过程可以用一个流程图清晰地表示:
已设置
未设置
tasklet_schedule被调用
检查状态位

TASKLET_STATE_SCHED
直接返回
设置状态位
保存中断状态并禁用中断
将tasklet添加到当前CPU链表
触发TASKLET_SOFTIRQ软中断
恢复中断状态

4.2 执行过程: tasklet_action()

当软中断被触发后, 最终会调用 tasklet_action() 来执行 tasklet:

c 复制代码
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;
    
    /* 1. 禁用本地中断并获取当前CPU的tasklet链表 */
    local_irq_disable();
    list = __this_cpu_read(tasklet_vec.head);
    __this_cpu_write(tasklet_vec.head, NULL);
    __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&empty_tasklet_vec.head));
    local_irq_enable();
    
    /* 2. 遍历链表执行所有tasklet */
    while (list) {
        struct tasklet_struct *t = list;
        list = list->next;
        
        /* 3. 检查tasklet是否可执行 (count == 0)  */
        if (tasklet_trylock(t)) {
            /* 4. 确保它仍被调度 (可能被tasklet_kill取消)  */
            if (!atomic_read(&t->count)) {
                /* 5. 清除调度状态 */
                clear_bit(TASKLET_STATE_SCHED, &t->state);
                
                /* 6. 设置运行状态并执行 */
                set_bit(TASKLET_STATE_RUN, &t->state);
                
                /* 执行用户提供的处理函数 */
                t->func(t->data);
                
                /* 7. 清除运行状态 */
                clear_bit(TASKLET_STATE_RUN, &t->state);
            }
            tasklet_unlock(t);
        }
        
        /* 8. 重新检查链表, 处理新添加的tasklet */
        local_irq_disable();
        t->next = NULL;
        *__this_cpu_read(tasklet_vec.tail) = t;
        __this_cpu_write(tasklet_vec.tail, &(t->next));
        list = __this_cpu_read(tasklet_vec.head);
        if (!list)
            __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&empty_tasklet_vec.head));
        __this_cpu_write(tasklet_vec.head, NULL);
        local_irq_enable();
    }
}

这个执行过程相当精巧, 有几个关键点需要注意:

  1. 原子性操作 : 使用 local_irq_disable/enable() 保护关键区域
  2. 锁机制 : tasklet_trylock() 确保同一 tasklet 不会在多个CPU上并发执行
  3. 重入处理: 在执行过程中可能新的 tasklet 被调度, 因此需要重新检查链表

4.3 禁止和启用: tasklet_disable() 和 tasklet_enable()

Tasklet 提供了简单的启用/禁用机制, 这是通过原子计数器实现的:

c 复制代码
void tasklet_disable(struct tasklet_struct *t)
{
    /* 原子增加计数器 */
    atomic_inc(&t->count);
    
    /*
     * 同步屏障: 确保在计数器增加后, 
     * 任何正在运行的tasklet都能看到这个变化
     */
    smp_mb__after_atomic();
    
    /*
     * 等待正在运行的tasklet完成
     * 这是一个忙等待, 但通常很快
     */
    while (test_bit(TASKLET_STATE_RUN, &(t->state)))
        cpu_relax();
}

void tasklet_enable(struct tasklet_struct *t)
{
    /*
     * 同步屏障: 确保在计数器减少前, 
     * 所有内存操作都已完成
     */
    smp_mb__before_atomic();
    
    /* 原子减少计数器 */
    atomic_dec(&t->count);
}

这种设计的巧妙之处在于:

  • 禁用时: 增加计数器并等待当前执行完成
  • 启用时: 只需减少计数器, 不需要等待
  • 执行时 : 只有在 count == 0 时才会执行

4.4 整体架构图解

现在让我们用 Mermaid 图来展示 tasklet 的整体架构:
中断处理流程
CPU 0 的详细结构
每个CPU的数据结构
CPU 0
CPU 1
CPU n
tasklet_vec

普通优先级链表
tasklet_hi_vec

高优先级链表
tasklet_struct
tasklet_struct
...
tasklet_struct
tasklet_struct
中断处理程序
调用 tasklet_schedule
tasklet加入CPU本地链表
触发软中断 TASKLET_SOFTIRQ
软中断调用 tasklet_action
执行tasklet的func函数

5. Tasklet 的典型使用场景和实例

5.1 何时使用 Tasklet?

Tasklet 特别适合以下场景:

  1. 中断处理的后半部分: 当上半部需要快速返回时
  2. 中小型数据处理: 数据量不大但需要及时处理的情况
  3. 设备驱动中的异步操作: 如完成 DMA 后的数据处理
  4. 定时器回调: 某些需要快速执行的定时任务

5.2 一个简单的字符设备驱动示例

让我们通过一个具体的例子来理解 tasklet 的用法. 假设我们有一个虚拟的字符设备, 当数据到达时触发中断, 我们使用 tasklet 来处理这些数据:

c 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

/* 定义我们的设备结构 */
struct my_device {
    struct cdev cdev;
    dev_t devno;
    struct tasklet_struct my_tasklet;
    char buffer[256];
    int data_ready;
};

static struct my_device my_dev;

/* Tasklet 处理函数 */
static void my_tasklet_handler(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;
    
    printk(KERN_INFO "Tasklet 执行: 处理缓冲区数据\n");
    
    /* 这里应该处理设备数据 */
    /* 例如: 解析数据、唤醒等待进程等 */
    
    /* 标记数据已处理 */
    dev->data_ready = 0;
}

/* 模拟的中断处理程序 */
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    
    printk(KERN_INFO "中断上半部: 接收数据\n");
    
    /* 模拟从硬件读取数据 */
    snprintf(dev->buffer, sizeof(dev->buffer), 
             "数据来自中断, 时间戳: %lld", ktime_get_ns());
    dev->data_ready = 1;
    
    /* 调度 tasklet 进行后续处理 */
    tasklet_schedule(&dev->my_tasklet);
    
    return IRQ_HANDLED;
}

/* 文件操作: 读取函数 */
static ssize_t my_read(struct file *filp, char __user *buf, 
                       size_t count, loff_t *f_pos)
{
    struct my_device *dev = filp->private_data;
    int ret;
    
    /* 等待数据就绪 */
    while (!dev->data_ready) {
        if (filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        /* 在实际驱动中, 这里应该使用等待队列 */
        msleep(10);
    }
    
    /* 将数据复制到用户空间 */
    if (count > sizeof(dev->buffer))
        count = sizeof(dev->buffer);
    
    ret = copy_to_user(buf, dev->buffer, count);
    if (ret)
        return -EFAULT;
    
    dev->data_ready = 0;
    
    return count;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .read = my_read,
};

/* 模块初始化 */
static int __init my_module_init(void)
{
    int ret;
    
    printk(KERN_INFO "初始化 Tasklet 示例模块\n");
    
    /* 初始化 tasklet */
    tasklet_init(&my_dev.my_tasklet, my_tasklet_handler, 
                 (unsigned long)&my_dev);
    
    /* 分配设备号 */
    ret = alloc_chrdev_region(&my_dev.devno, 0, 1, "my_tasklet_dev");
    if (ret < 0) {
        printk(KERN_ERR "无法分配设备号\n");
        return ret;
    }
    
    /* 初始化字符设备 */
    cdev_init(&my_dev.cdev, &my_fops);
    my_dev.cdev.owner = THIS_MODULE;
    
    ret = cdev_add(&my_dev.cdev, my_dev.devno, 1);
    if (ret < 0) {
        printk(KERN_ERR "无法添加字符设备\n");
        unregister_chrdev_region(my_dev.devno, 1);
        return ret;
    }
    
    /* 注册中断处理程序 (这里使用虚拟中断号)  */
    ret = request_irq(100, my_interrupt_handler, 0, 
                      "my_tasklet_irq", &my_dev);
    if (ret < 0) {
        printk(KERN_ERR "无法注册中断\n");
        cdev_del(&my_dev.cdev);
        unregister_chrdev_region(my_dev.devno, 1);
        return ret;
    }
    
    printk(KERN_INFO "模块初始化完成\n");
    return 0;
}

/* 模块清理 */
static void __exit my_module_exit(void)
{
    /* 禁用 tasklet */
    tasklet_disable(&my_dev.my_tasklet);
    
    /* 等待 tasklet 完成 */
    tasklet_kill(&my_dev.my_tasklet);
    
    /* 释放中断 */
    free_irq(100, &my_dev);
    
    /* 删除字符设备 */
    cdev_del(&my_dev.cdev);
    unregister_chrdev_region(my_dev.devno, 1);
    
    printk(KERN_INFO "模块卸载完成\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Kernel Expert");
MODULE_DESCRIPTION("Tasklet 使用示例");

5.3 执行流程分析

这个示例展示了典型的 tasklet 使用模式:

  1. 中断到来 : my_interrupt_handler 被调用
  2. 快速处理: 保存必要数据, 调度 tasklet
  3. tasklet 调度 : tasklet_schedule 将 tasklet 加入队列
  4. 异步执行 : 在软中断上下文中执行 my_tasklet_handler
  5. 完整处理: tasklet 处理耗时的数据操作

6. Tasklet 的高级主题和内部细节

6.1 锁机制和并发控制

Tasklet 的并发控制是其设计的精髓之一. 让我们深入理解其中的锁机制:

c 复制代码
/* tasklet_trylock 的实现 */
static inline int tasklet_trylock(struct tasklet_struct *t)
{
    return !test_and_set_bit(TASKLET_STATE_RUN, &t->state);
}

/* tasklet_unlock 的实现 */
static inline void tasklet_unlock(struct tasklet_struct *t)
{
    smp_mb__before_atomic();
    clear_bit(TASKLET_STATE_RUN, &t->state);
}

关键点:

  • test_and_set_bit: 原子操作, 同时测试和设置位
  • 内存屏障: 确保操作的顺序性
  • 自旋锁风格: 但不会真正自旋, 只是检查

6.2 Tasklet 的优先级系统

Linux 提供了两种优先级的 tasklet:

c 复制代码
/* 高优先级 tasklet 的调度 */
void tasklet_hi_schedule(struct tasklet_struct *t)
{
    /* 实现与 tasklet_schedule 类似, 但使用高优先级链表 */
}

/* 高优先级 tasklet 的处理 */
static void tasklet_hi_action(struct softirq_action *a)
{
    /* 与 tasklet_action 类似, 但处理高优先级链表 */
}

优先级差异体现在软中断的编号上:

软中断类型 软中断编号 优先级 用途
HI_SOFTIRQ 0 最高 高优先级 tasklet
TIMER_SOFTIRQ 1 定时器
NET_TX_SOFTIRQ 2 网络发送
NET_RX_SOFTIRQ 3 网络接收
BLOCK_SOFTIRQ 4 块设备
IRQ_POLL_SOFTIRQ 5 IRQ 轮询
TASKLET_SOFTIRQ 6 普通 tasklet
SCHED_SOFTIRQ 7 调度器
HRTIMER_SOFTIRQ 8 高精度定时器
RCU_SOFTIRQ 9 最低 RCU回调

6.3 与其他机制的交互

Tasklet 不是孤立的, 它与内核的其他部分密切交互:
其他系统
Tasklet 执行
软中断系统
内核中断处理
硬件层
硬件设备
硬件中断
中断服务程序
tasklet_schedule
软中断 TASKLET_SOFTIRQ
ksoftirqd 内核线程

在负载高时处理
tasklet_action
用户回调函数
可能唤醒内核模块
可能调度工作队列
可能通知用户空间

7. Tasklet 的调试和性能分析

7.1 调试工具和技术

调试 tasklet 问题需要专门的工具和技术:

7.1.1 Proc 文件系统接口
bash 复制代码
# 查看软中断统计信息
cat /proc/softirqs

输出示例:

复制代码
                    CPU0       CPU1       CPU2       CPU3       
          HI:          5          2          3          1
       TIMER:     123456     123450     123445     123440
      NET_TX:        100         95         90         85
      NET_RX:       1000        995        990        985
       BLOCK:         50         45         40         35
    IRQ_POLL:          0          0          0          0
     TASKLET:        200        195        190        185
       SCHED:       5000       4995       4990       4985
     HRTIMER:         10          8          6          4
         RCU:      30000      29995      29990      29985
7.1.2 Ftrace 跟踪
bash 复制代码
# 启用 tasklet 跟踪
echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_exit/enable

# 查看跟踪结果
cat /sys/kernel/debug/tracing/trace
7.1.3 动态打印调试

在驱动代码中添加调试信息:

c 复制代码
#include <linux/dynamic_debug.h>

/* 控制动态打印 */
static void debug_tasklet(struct tasklet_struct *t, const char *action)
{
    pr_debug("Tasklet %ps %s on CPU %d, state: 0x%lx, count: %d\n",
             t->func, action, smp_processor_id(), t->state,
             atomic_read(&t->count));
}

/* 在调度时调用 */
debug_tasklet(t, "scheduled");

7.2 常见问题和解决方案

问题现象 可能原因 解决方案
系统延迟增加 Tasklet 处理时间过长 1. 优化处理函数 2. 考虑使用工作队列
死锁 Tasklet 中使用了可能睡眠的函数 1. 检查所有函数调用 2. 使用工作队列替代
数据竞争 共享数据未正确保护 1. 使用原子操作 2. 使用自旋锁保护
Tasklet 不执行 count 不为零或未调度 1. 检查 tasklet_disable/enable 调用 2. 确认调度函数被调用
CPU 使用率过高 Tasklet 过于频繁调度 1. 合并处理请求 2. 增加调度延迟

7.3 性能优化技巧

  1. 批量处理: 合并多个小任务为一个大任务
  2. 延迟调度 : 使用 tasklet_hi_schedule 提高优先级
  3. CPU 亲和性: 绑定 tasklet 到特定 CPU
  4. 监控统计 : 使用 /proc/softirqs 监控性能

8. 实战案例分析: 网络驱动中的 Tasklet

让我们分析一个真实世界的例子: Linux 网络驱动中 tasklet 的使用

c 复制代码
/* 简化版的网络驱动 tasklet 处理 */
struct nic_private {
    struct net_device *dev;
    struct tasklet_struct tx_tasklet;
    struct tasklet_struct rx_tasklet;
    struct sk_buff_head tx_queue;
    struct sk_buff_head rx_queue;
};

/* 发送 tasklet 处理函数 */
static void tx_tasklet_handler(unsigned long data)
{
    struct nic_private *priv = (struct nic_private *)data;
    struct sk_buff *skb;
    
    /* 处理所有待发送的数据包 */
    while ((skb = skb_dequeue(&priv->tx_queue))) {
        if (nic_send_packet(priv, skb) < 0) {
            /* 发送失败, 重新排队 */
            skb_queue_head(&priv->tx_queue, skb);
            break;
        }
        dev_kfree_skb(skb);
    }
}

/* 接收 tasklet 处理函数 */
static void rx_tasklet_handler(unsigned long data)
{
    struct nic_private *priv = (struct nic_private *)data;
    struct sk_buff *skb;
    
    /* 处理所有接收到的数据包 */
    while ((skb = skb_dequeue(&priv->rx_queue))) {
        /* 传递给网络协议栈 */
        netif_receive_skb(skb);
    }
}

/* 中断处理程序 */
static irqreturn_t nic_interrupt(int irq, void *dev_id)
{
    struct nic_private *priv = dev_id;
    u32 status;
    
    /* 读取中断状态 */
    status = nic_read_status(priv);
    
    if (status & TX_COMPLETE) {
        /* 调度发送 tasklet */
        tasklet_schedule(&priv->tx_tasklet);
    }
    
    if (status & RX_READY) {
        /* 调度接收 tasklet */
        tasklet_hi_schedule(&priv->rx_tasklet); /* 使用高优先级 */
    }
    
    return IRQ_HANDLED;
}

这个例子展示了 tasklet 在网络驱动中的典型应用模式:

  1. 中断处理尽可能快: 只读取状态和调度 tasklet
  2. 批量处理: tasklet 处理队列中的所有数据包
  3. 优先级区分: 接收使用高优先级, 发送使用普通优先级

9. 总结和最佳实践

9.1 Tasklet 的核心要点总结

让我们用一张表格总结 tasklet 的关键特性:

特性类别 具体内容
设计目标 快速中断处理的延迟部分
执行上下文 软中断/中断上下文
调度方式 每个CPU的链表, 原子操作
并发特性 同类型tasklet串行执行
同步原语 原子计数器和状态位
优先级 普通和高优先级两种
生命周期 调度 -> 执行 -> 完成
调试支持 proc接口、tracepoint、动态调试

9.2 最佳实践指南

根据多年的内核开发经验, 我总结了以下最佳实践:

  1. 何时使用 Tasklet:

    • 处理时间在微秒级别
    • 不需要睡眠
    • 需要低延迟响应
    • 数据量适中
  2. 何时避免 Tasklet:

    • 处理时间超过100微秒
    • 需要调用可能睡眠的函数
    • 需要复杂的同步机制
    • 实时性要求不高
  3. 性能优化建议:

    c 复制代码
    /* 不好的做法: 频繁调度小任务 */
    for (i = 0; i < 100; i++) {
        tasklet_schedule(&small_task);
    }
    
    /* 好的做法: 批量处理 */
    void process_batch(unsigned long data) {
        for (i = 0; i < 100; i++) {
            process_item(i);
        }
    }
  4. 错误处理建议:

    c 复制代码
    /* 总是检查tasklet状态 */
    if (!test_bit(TASKLET_STATE_SCHED, &t->state)) {
        /* 安全地调度 */
        tasklet_schedule(t);
    }
    
    /* 在模块退出时正确清理 */
    static void __exit my_exit(void) {
        tasklet_disable(&my_tasklet);
        tasklet_kill(&my_tasklet);  /* 等待完成 */
        /* 其他清理工作 */
    }
相关推荐
dog2502 小时前
Linux 6.19 TCP 的两个极限拉扯
linux·运维·tcp/ip
Imxyk2 小时前
力扣:1553. 吃掉 N 个橘子的最少天数(记忆化搜索,Dijkstra解法)
算法
idcardwang2 小时前
esp32s3-idf使用smart_config一键配网
网络
新兴AI民工2 小时前
【Linux内核九】进程管理模块:list_head钩子构造双向列表和一些宏定义
linux·运维·list·linux内核
小周学学学2 小时前
ESXI故障处理-重启后数据存储丢失
linux·运维·服务器
googleccsdn2 小时前
ENSP Pro Lab笔记:配置BGP EVPN VXLAN双栈(2)
网络·笔记·网络协议
爱编码的傅同学2 小时前
【今日算法】Leetcode 581.最短无序连续子数组 和 42.接雨水
数据结构·算法·leetcode
min1811234562 小时前
HR人力资源招聘配置流程图制作教程
大数据·网络·人工智能·架构·流程图·求职招聘
Σίσυφος19002 小时前
线性与非线性 、齐次非齐次
算法