注册 登录
电子工程世界-论坛 返回首页 EEWORLD首页 频道 EE大学堂 下载中心 Datasheet 专题
oxlm_1的个人空间 https://home.eeworld.com.cn/space-uid-1076641.html [收藏] [复制] [分享] [RSS]
日志

《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 中断管理

已有 206 次阅读2024-3-31 23:07 |个人分类:读书笔记

       看完第一章并发与同步后,接下来便是 中断管理,这一章前面部分从硬件层面的中断出发逻辑到系统处理中断一路讲解下来。直到软中断和tasklet中才真正把中断处理讲解完毕。另外,补充了一个知识点,为解决中断的优先级过高导致多软中断处理时导致的系统调度受阻问题,引入了workqueue工作队列相关的知识。

        中断触发过程,从出发过程中看,其实arm64的硬件中断触发逻辑与cortex-m核的中断管理大致类似,都是各种中断源触发中断信号,处理器接收到中断信号后,通过查询寄存器判定是什么中断触发了,之后做一系列的中断处理。唯一有区别的是,arm64中断存在多核的场景(m核也是在近几年才出现多核异构的场景,但实际使用上,与linux的多核任务处理还是有明显差异),中断消息传递逻辑会更加复杂一些,另外中断源数相比于m核来说,完全不是一个数量级,因此存在多级中断的概念,以降低中断源判断的复杂度。

       之后是从arm64的角度分析linux内核中断处理过程,目前从应用角度上的总结信息如下:

中断注册:

request_irq() // 比较旧的接口函数
request_thread_irq() // 2.6.30内核新增的线程化中断注册函数
                     // 注意:
                     // 使用IRQ号(映射过的软件中断号),而不是硬件中断号。
                     // 主处理程序和thread_fn不能同时为NULL
                     // 当主处理程序为NULL且硬件中断控制器不支持硬件ONESHOT功能时,应该显式地设置IRQ_ONESHOT标志来确保不会产生中断风暴
                     // 若启用了中断线程化,那么primary handler应该返回IRQ_WAKE_THREAD来唤醒中断线程
setup_irq_thread()   // 创建一个实时线程,调度策略为SCHED_FIFO,优先级50

Linux中断调用流程:

//ARM64 Linux硬件中断注册流程
__git_init_bases()
    set_handle_irq(gic_handle_irq)// 使handle_arch_irq 指向 gic_handle_irq()

//ARM64 Linux中断流程
kernel_entery    // 保存中断上下文
    irq_handler  //中断处理
      irq_stack_entry  // 设置栈地址
      handle_arch_irq = gic_handle_irq()
        handle_domain_irq() //Linux内核中断处理入口
          irq_enter() //显式地告知内核现在进入中断上下文
            preempt_count_add(HARDIRQ_OFFSET)  // 增加当前进程的thread_infod中preempt_count成员里HARDIRQ域的值
          irq_find_mapping() //通过硬件中断号找到IRQ号
          generic_handle_irq() 
            handle_fasteoi_irq() // 如果没有指定action描述符或者该中断关闭了IRQD_IRQ_DISABLED,则设置中断状态为IRQ_PENDING,
                                 // 然后调用中断控制器的irq_chip中的irq_mask()回调函数屏蔽该终端
                                 // 如果该中断不支持中断嵌套,则调用mask_irq()函数关闭该中断源
              handle_irq_event()  // 中断处理的核心函数
                handle_irq_event_percpu() // 对应cpu的中断处理入口
                  __handle_irq_event_percpu() // 同上
                    action->handler() // 执行中断处理,若需要唤醒中断的内核线程,则返回IRQ_WAKE_THREAD
                    __irq_wake_thread() //若需要,则唤醒中断的内核线程
          irq_exit() //显式地告知内核现在退出中断上下文
            preempt_count_sub(HARDIRQ_OFFSET) //配对地递减preempt_count中HARDIRQ域地计数
            invoke_softirq()
              __do_softirq() // 处理软中断
      irq_stack_exit   // 从中断栈切换回中断进程地内核栈
kernel_exit // 恢复中断上下文并退出中断

// __irq_wake_thread()唤醒中断线程后的执行过程
irq_thread() //中断线程的执行函数,设置handler_fn()回调
  irq_thread_fn() // 执行注册中断时的thread_fn()函数
      atcion->thread_fn() //最终的中断处理入口
      irq_finalize_oneshot() //对IRQS_ONESHOT类型中断的处理

Linux软中断:

    由中断处理中可以发现,Linux的软中断处理实际上也是在中断调用流程中实现的。

open_softirq(int nr, void(*action)(struct softirq_action *))  //注册一个软中断,其中nr是软中断的序号

//触发软中断接口
#ifndef local_softirq_pending_ref
#define local_softirq_pending_ref irq_stat.__softirq_pending
#endif

#define local_softirq_pending() (__this_cpu_read(local_softirq_pending_ref))
#define set_softirq_pending(x)  (__this_cpu_write(local_softirq_pending_ref, (x)))
#define or_softirq_pending(x)   (__this_cpu_or(local_softirq_pending_ref, (x)))

// 设置irq_stat数据结构中__soft_pending成员的第nr位,nr表示中断序号。
// 在中断返回时,该CPU会检查此信息,若不为0,则说明有pending的软中断需处理
void __raise_softirq_irqoff(unsigned int nr)
{
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);
}

inline void raise_softirq_irqoff(unsigned int nr) // 主动关闭本地中断,允许在进程上下文中调用
{
    __raise_softirq_irqoff(nr);

    if (!in_interrupt())
        wakeup_softirqd();
}

void raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

Tasklet

Tasklet本质上是软中断的一个实现形式,运行在软中断的上下文中

//驱动中tasklet使用
// 定义
// 静态声明
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } // 初始化并默认使能
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } // 初始化并默认禁用

//动态声明
void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

// 使用Tasklet
//在驱动中调用tasklet_schedule()函数
// Tasklet执行,遵循软中断上下文逻辑,因此不是在调用__tasklet_schedule_common时就立即执行,而是需要等待调度时执行
static void __tasklet_schedule_common(struct tasklet_struct *t,
                      struct tasklet_head __percpu *headp,
                      unsigned int softirq_nr)
{
    struct tasklet_head *head;
    unsigned long flags;

    local_irq_save(flags);
    head = this_cpu_ptr(headp);
    t->next = NULL;
    *head->tail = t;
    head->tail = &(t->next);
    raise_softirq_irqoff(softirq_nr);
    local_irq_restore(flags);
}

void __tasklet_schedule(struct tasklet_struct *t)
{
    __tasklet_schedule_common(t, &tasklet_vec,
                  TASKLET_SOFTIRQ);
}

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}

       中断管理看完后,下一节的内容便是着重介绍工作队列(workqueue)部分。workqueue的引入是为了解决软中断和tasklet的天然硬伤,由于其处理优先级比系统调度要高,因此在软中断较多的场景下,系统调度的及时性会大大降低。而工作队列将任务交由特定线程完成,也就是说,工作队列的任务处理是在系统调度中实现,也就规避了系统调度被长时间阻塞的问题。

        工作队列核心代码的实现如下:

//工作队列初始化, 系统初始化时自动运行, 驱动开发者不需要操作
// 图像化的流程见附图2.10
start_kernel()
    workqueue_init_early() // 用于初始化默认的几个工作队列
    arch_call_rest_init()
        rest_init()
            kernel_thread(kernel_init, NULL, CLONE_FS);

kernel_init()
    kernel_init_freeable()
        workqueue_init()
            create_worker() // 创建工作队列
                worker_attach_to_pool() // 把刚分配的工作线程挂入worker_poolworker_pool中,并且设置这个工作线程允许运行的cpumask

// 创建工作队列
// 其中 __alloc_workqueue_key流程见附图2.11
#ifdef CONFIG_LOCKDEP
#define alloc_workqueue(fmt, flags, max_active, args...)        \
({                                  \
    static struct lock_class_key __key;             \
    const char *__lock_name;                    \
                                    \
    __lock_name = "(wq_completion)"#fmt#args;           \
                                    \
    __alloc_workqueue_key((fmt), (flags), (max_active),     \
                  &__key, __lock_name, ##args);     \
})
#else
#define alloc_workqueue(fmt, flags, max_active, args...)        \
    __alloc_workqueue_key((fmt), (flags), (max_active),     \
                  NULL, NULL, ##args)
#endif

#define alloc_ordered_workqueue(fmt, flags, args...)            \
    alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED |        \
            __WQ_ORDERED_EXPLICIT | (flags), 1, ##args)

#define create_workqueue(name)                      \
    alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
#define create_freezable_workqueue(name)                \
    alloc_workqueue("%s", __WQ_LEGACY | WQ_FREEZABLE | WQ_UNBOUND | \
            WQ_MEM_RECLAIM, 1, (name))
#define create_singlethread_workqueue(name)             \
    alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name)
    
//添加工作队列
#define ATOMIC_LONG_INIT(i) ATOMIC_INIT(i)
#define WORK_DATA_INIT()    ATOMIC_LONG_INIT((unsigned long)WORK_STRUCT_NO_POOL)

#ifdef CONFIG_LOCKDEP
#define __INIT_WORK(_work, _func, _onstack)             \
    do {                                \
        static struct lock_class_key __key;         \
                                    \
        __init_work((_work), _onstack);             \
        (_work)->data = (atomic_long_t) WORK_DATA_INIT();   \
        lockdep_init_map(&(_work)->lockdep_map, "(work_completion)"#_work, &__key, 0); \
        INIT_LIST_HEAD(&(_work)->entry);            \
        (_work)->func = (_func);                \
    } while (0)
#else
#define __INIT_WORK(_work, _func, _onstack)             \
    do {                                \
        __init_work((_work), _onstack);             \
        (_work)->data = (atomic_long_t) WORK_DATA_INIT();   \
        INIT_LIST_HEAD(&(_work)->entry);            \
        (_work)->func = (_func);                \
    } while (0)
#endif

#define INIT_WORK(_work, _func)                     \
    __INIT_WORK((_work), (_func), 0)

#define INIT_WORK_ONSTACK(_work, _func)                 \
    __INIT_WORK((_work), (_func), 1)


//调度工作队列
//__queue_work()为调度工作队列的核心实现,其大致实现流程见图2.12
//需注意,调度工作队列,实际上并未执行工作队列,工作队列的具体执行见工作队列初始化中的create_worker()函数中调用的worker_thread()函数
//简化后的worker_thread()函数实现如下:
worker_thread()
{
recheck:
    if(不需要更多的工作线程?)
        goto sleep;

    if(需要创建更多的工作线程? && 创建工作线程)
        goto recheck;
    do {
        处理工作;
    }(还有工作未完成 && 活跃的工作线程 <= 1)     

sleep:
    schedule();                                            
}

bool queue_work_on(int cpu, struct workqueue_struct *wq,
           struct work_struct *work)
{
    bool ret = false;
    unsigned long flags;

    local_irq_save(flags);

    if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
        __queue_work(cpu, wq, work);
        ret = true;
    }

    local_irq_restore(flags);
    return ret;
}

static inline bool queue_work(struct workqueue_struct *wq,
                  struct work_struct *work)
{
    return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}

static inline bool schedule_work(struct work_struct *work)
{
    return queue_work(system_wq, work);
}

//取消一个workqueue
cancel_work_sync()  // 取消一个work,但会等到work执行完毕
  __cancel_work_timer()
      try_to_grab_pending() // 尝试从线程池中拿到所需要取消的work,若未拿到,则继续执行__cancel_work_timer()
      __flush_work() // 等待work执行完毕

总结       

1. 或许是之前并未深入研究linux内核源码的缘故,阅读第二章的知识内容,仅仅从怎么使用的角度去理解,都有一种较为吃力的感觉,此感觉在workqueue部分特别明显,明显感觉需要反复对照源码才能理解逻辑。因此此章节的知识点还需要反复阅读学习,并配以项目实践来纠正和加深理解。

2. 在反复看前两章内容的过程中,明显感觉缺乏卷1内容的情况下直接看卷2,有不少知识点盲区。因此看此书时最好备有卷1,以便出现涉及到卷1内容时快速查阅。

3. CMWQ机制目前还没理解透, 还需继续深入阅读理解。

本文来自论坛,点击查看完整帖子内容。

评论 (0 个评论)

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 注册

热门文章