Day 29:IRQ (Part 3) - 这是核心执行绪的味道!Threaded IRQ

这篇文章以实验观察 threaded IRQ 与传统 IRQ 的不同。

关於这一切 IRQ 行为不同,可以参考 2016 年 ELCE 中,Alison Chaiken 的演讲 IRQs: the Hard, the Soft, the Threaded and the Preemptible,里面介绍了比如 threaded IRQ 这类机制与传统的 IRQ 有什麽不同。除此之外,interrupt controller 的驱动程序架构,可以参考也是 2016 年的 ELCE 中,How Dealing with Modern Interrupt Architectures can Affect Your Sanity 演讲,听他聊聊现代的电脑中,中断相关周边硬体是如何让人失去理智的(?)。

实验方面,今 (2020) 年的 OSS NA 中,ftrace 的维护者 Steven Rostedt 亲自上阵,在 Finding Sources of Latency on your Linux System 中,提到如何用 ftrace 追踪 IRQ,并且量测 IRQ 带来的延迟 (以及 ftrace 相关的最新进展)(14:36 开始的部分)。而他本人也有写一篇关於这个主题的文章

程序

关於 threaded IRQ 的用法,文件可以在 kernel/irq/manage.c 的注解中找到。如果就 API 来说的话,threaded IRQ 用起来很简单:凑到 request_threaded_irq 需要的参数:

int request_threaded_irq(unsigned int irq, 
    irq_handler_t handler,
    irq_handler_t thread_fn, 
    unsigned long irqflags,
    const char *devname, void *dev_id)

这主要的参数大致上跟 request_irq 一样,除了现在要传进去两个 irq_handler_t 以外。这两个分别是:

  1. handler:上半部的函式,会在 atomic context 下执行。
  2. thread_fn:下半部 ,会在 process context 下执行。

提供完之後,用这个函数注册他们。

这两个函式不总是两个都要:如果你觉得上半部就可以处理完,那麽下半部的函式可以传进 NULL; 类似地,如果觉得不需要上半部,那麽上半部也可以传进 NULL。只要两个当中其中一个有回传 IRQ_HANDLED 就好。只要提供两个函数,不用再烦恼是不是要 Workqueue 设计新的资料结构等等。

这一系列函数的基本款是 request_threaded_irq,但也有其他的变形,比如说这边使用到的 devm_request_threaded_irq

Step 1:实作上半部

这个函数是上半部的函数,必须在 atomic context 中执行。如果 IRQ 不需要下半部,那麽要在这里回传 IRQ_HANDLED; 但如果有任务要给下半部处理,则要回传 IRQ_WAKE_THREAD。这边为了展示使用方法,所以就什麽都不做直接回传 IRQ_WAKE_THREAD,把任务丢给下半部。

static irqreturn_t minirq_top_half(int irq, void *p)
{
    return IRQ_WAKE_THREAD;
}

事实上可以在 request_threaded_irq 系列的函数中,把上半部函数的位置填进 NULL 来做到一样的事情,而不用多此一举写一个「只会回传 IRQ_WAKE_THREAD」的函数。但这边为了展示使用方法,所以故意多此一举。

Step 2:实作下半部

也就是参数中的 thread_fn,会在 process context 下执行。这边做的事情跟昨天一样,就是印一个通知:

static irqreturn_t minirq_bottom_half(int irq, void *p)
{
    struct minirq_dev *minirq = p;
    dev_info(minirq->dev, "Rising edge detected!\n");
    return IRQ_HANDLED;
}

Step 3:request_threaded_irq

最後在 probe 中注册:

static int minirq_probe(struct platform_device *pdev)
{
    ...
    ret = devm_request_threaded_irq(dev, minirq->irq, 
        minirq_top_half, minirq_bottom_half, 
        IRQF_TRIGGER_RISING, "minirq", minirq);
    ...
}

实验方法

硬体的配置跟昨天一样:Raspberry Pi 的 GPIO17 透过 Logic Level Shifter 接到 Arduino 的 A0。而程序方面也跟昨天一样。为了避免 interrupt 太多,建议可以是当调整时间间隔。而 Arduino 使用的程序如下:

void setup() {
    pinMode(A0, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    digitalWrite(A0, LOW);
    delay(500);
    digitalWrite(A0, LOW);
    delay(500);
}

观察工具

这边使用两个工具来观察:ftraceps

ps -aux

使用 ps -aux 的目的是看看有没有新的 kernel thread 产生。在实验开始之前,看起来是这个样子:

$ ps -aux | grep irq
root         9  0.0  0.0      0     0 ?        S    09:29   0:00 [ksoftirqd/0]
root        15  0.0  0.0      0     0 ?        S    09:29   0:00 [ksoftirqd/1]
root        20  0.0  0.0      0     0 ?        S    09:29   0:00 [ksoftirqd/2]
root        25  0.0  0.0      0     0 ?        S    09:29   0:01 [ksoftirqd/3]
pi        2642  0.0  0.0   7348   516 pts/0    S+   10:29   0:00 grep --color=auto irq

ftrace

ftrace 目的是哪个执行单元执行了 minirq_* 这些函数,这样就可以知道处理这个 IRQ 时的路径是什麽。而不管是 threaded IRQ 或是传统的 IRQ,这边使用的指令都一样:用 function 这个 tracer,并且用 -func-stack 选项令他印出 stack trace

$ sudo trace-cmd record \
    -p function \
    -T \
    --func-stack \
    -l minirq_*

实验:ps -aux

观察:ps -aux (传统 IRQ)

如果没有用 threaded IRQ,模组安装上去之後,ps -aux 的结果还是一样:

$ sudo insmod minirq.ko 
$ ps -aux | grep irq
 root         9  0.0  0.0      0     0 ?        S    09:29   0:00 [ksoftirqd/0]
 root        15  0.0  0.0      0     0 ?        S    09:29   0:00 [ksoftirqd/1]
 root        20  0.0  0.0      0     0 ?        S    09:29   0:00 [ksoftirqd/2]
 root        25  0.0  0.0      0     0 ?        S    09:29   0:01 [ksoftirqd/3]
 pi        2697  0.0  0.0   7348   492 pts/0    S+   10:29   0:00 grep --color=auto irq

(USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND)

观察:ps -aux (Threaded IRQ)

而如果是 threaded IRQ 的版本,如果安装上模组之後,立刻执行,就会发现在差不多时间多出了一个名为 irq/167-minirq 的核心执行绪:

$ sudo insmod minirq.ko 
$ ps -aux | grep irq
 root         9  0.0  0.0      0     0 ?        S    14:40   0:00 [ksoftirqd/0]
 root        15  0.0  0.0      0     0 ?        S    14:40   0:00 [ksoftirqd/1]
 root        20  0.0  0.0      0     0 ?        S    14:40   0:00 [ksoftirqd/2]
 root        25  0.0  0.0      0     0 ?        S    14:40   0:00 [ksoftirqd/3]
+root      2851  0.0  0.0      0     0 ?        S    16:40   0:00 [irq/167-minirq]
 pi        2922  0.0  0.0   7348   492 pts/0    S+   16:41   0:00 grep --color=auto irq
 
(USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND)

实验:ftrace (上半部)

接着用 ftrace 去看看上半部,也就是 minirq_top_half 这个函数。为了帮变识别,昨天的模组中,IRQ Handler 的名称从 irq_top_half 也改成 minira_top_half。改动过的程序码也会附在最後面。

观察:呼叫堆叠 (传统 IRQ)

<idle>-0     [000]  7169.646594: function:             minirq_top_half
<idle>-0     [000]  7169.646610: kernel_stack:         <stack trace>
=> ftrace_graph_call (801111c4)
=> __handle_irq_event_percpu (80186c78)
=> handle_irq_event_percpu (80186e90)
=> handle_irq_event (80186f38)
=> handle_edge_irq (8018bbc4)
=> generic_handle_irq (80185c64)
=> bcm2835_gpio_irq_handle_bank (8059f6f4)
=> bcm2835_gpio_irq_handler (8059f7a0)
=> generic_handle_irq (80185c64)
=> bcm2836_chained_handle_irq (80597aac)
=> generic_handle_irq (80185c64)
=> __handle_domain_irq (801863c4)
=> bcm2836_arm_irqchip_handle_irq (80102220)
=> __irq_svc (80101a3c)
=> arch_cpu_idle (80109b18)
=> default_idle_call (808f09a0)
=> do_idle (80158d8c)
=> cpu_startup_entry (801590a8)
=> rest_init (808e9d08)
=> arch_call_rest_init (80c00b68)
=> start_kernel (80c010a0)
=> 0

不管是传统的 IRQ,还是 threaded IRQ,这两者上半部的呼叫堆叠大致相同:

观察:呼叫堆叠 (Threaded IRQ)

<idle>-0     [000]  7222.508310: function:             minirq_top_half
<idle>-0     [000]  7222.508328: kernel_stack:         <stack trace>
=> ftrace_graph_call (801111c4)
=> __handle_irq_event_percpu (80186c78)
=> handle_irq_event_percpu (80186e90)
=> handle_irq_event (80186f38)
=> handle_edge_irq (8018bbc4)
=> generic_handle_irq (80185c64)
=> bcm2835_gpio_irq_handle_bank (8059f6f4)
=> bcm2835_gpio_irq_handler (8059f7a0)
=> generic_handle_irq (80185c64)
=> bcm2836_chained_handle_irq (80597aac)
=> generic_handle_irq (80185c64)
=> __handle_domain_irq (801863c4)
=> bcm2836_arm_irqchip_handle_irq (80102220)
=> __irq_svc (80101a3c)
=> arch_cpu_idle (80109b18)
=> default_idle_call (808f09a0)
=> do_idle (80158d8c)
=> cpu_startup_entry (801590a8)
=> rest_init (808e9d08)
=> arch_call_rest_init (80c00b68)
=> start_kernel (80c010a0)
=> 0

其实也不意外。虽然发展脉络来说,是先有传统的 IRQ,再有 threaded IRQ。但实际上不管是 devm_request_irq ,还是 request_irq ,他们其实都是用 threaded IRQ 的版本去实作的。以 request_irq 为例:

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

所以会有一样的呼叫路径其实一点也不意外。

实验:ftrace (下半部)

下半部就会有明显的差别。在原先的 IRQ 中,是在上半部 (也就是 minirq_top_half 函数中),把一个 struct work_struct 排进全域的 Workqueue 中,所以执行下半部的会是处理 Workqueue 的执行绪; 而 threaded IRQ 的版本中,则是由前面 ps -aux 所看到的,新的 irq/167-minirq 执行绪专门去执行。

观察:下半部呼叫堆叠 (传统 IRQ)

可以发现是 worker_thread 在做事情。看起来也很合理,因为这就是在处理 Workqueue 的那个行程:

kworker/0:3-2704  [000]  7169.646820: function:             minirq_bottom_half
kworker/0:3-2704  [000]  7169.646826: kernel_stack:         <stack trace>
=> ftrace_graph_call (801111c4)
=> process_one_work (801413bc)
=> worker_thread (8014173c)
=> kthread (80148aac)
=> ret_from_fork (801010ac)
=> 0

观察:下半部呼叫堆叠 (Threaded IRQ)

这时可以发现:执行下半部的就是专门的 kernel thread,也就是刚刚在 ps 时所观察到的,新的执行绪:

irq/167-minirq-2851  [003]  7222.508489: function:             minirq_bottom_half
irq/167-minirq-2851  [003]  7222.508495: kernel_stack:         <stack trace>
=> ftrace_graph_call (801111c4)
=> irq_thread_fn (801881fc)
=> irq_thread (80187fdc)
=> kthread (80148aac)
=> ret_from_fork (801010ac)
=> 0

顺带一提,如果去使用其他 ftrace 的选项,比如:

$ sudo trace-cmd record -p function_graph \
    -e irq_handler_entry

会发现这个执行绪跟原先的执行绪交错出现的状况:

...
  irq/167-minirq-1179  [000]   283.217546: funcgraph_entry:                   |                  __up_console_sem.constprop.17() {
       trace-cmd-1193  [003]   283.217547: funcgraph_entry:                   |                            rcu_all_qs() {
  irq/167-minirq-1179  [000]   283.217547: funcgraph_entry:        0.937 us   |                    __printk_safe_enter();
       trace-cmd-1193  [003]   283.217548: funcgraph_exit:         1.718 us   |                            }
  irq/167-minirq-1179  [000]   283.217549: funcgraph_entry:                   |                    up() {
       trace-cmd-1193  [003]   283.217550: funcgraph_exit:         5.208 us   |                          }
  irq/167-minirq-1179  [000]   283.217550: funcgraph_entry:        1.145 us   |                      _raw_spin_lock_irqsave();
       trace-cmd-1193  [003]   283.217552: funcgraph_entry:                   |                          should_failslab() {
  irq/167-minirq-1179  [000]   283.217552: funcgraph_entry:        0.989 us   |                      _raw_spin_unlock_irqrestore();
       trace-cmd-1193  [003]   283.217554: funcgraph_exit:         1.823 us   |                          }
  irq/167-minirq-1179  [000]   283.217554: funcgraph_exit:         5.104 us   |                    }
  irq/167-minirq-1179  [000]   283.217555: funcgraph_entry:                   |                    __printk_safe_exit() {
       trace-cmd-1193  [003]   283.217555: funcgraph_exit:       + 12.187 us  |                        }
  irq/167-minirq-1179  [000]   283.217556: funcgraph_exit:         0.937 us   |                    }
  irq/167-minirq-1179  [000]   283.217557: funcgraph_exit:       + 11.979 us  |                  }
       trace-cmd-1193  [003]   283.217557: funcgraph_entry:                   |                        start_this_handle() {
  irq/167-minirq-1179  [000]   283.217558: funcgraph_entry:                   |                  _raw_spin_lock() {
...

如果是使用这个选项,仅仅数秒就会产生大量资料 (约 100 MB 左右),也会带来大幅度的延迟。所以请谨慎实验。

幕後花絮

如果使用 ply 的话

kprobe:minirq_top_half
{
    @minirq_top_half[stack, arg0] = count();
}
kprobe:minirq_bottom_half
{
    @minitq_bottom_half[stack, arg0] = count();
}

会发现有不同的路径:

@minirq_top_half:
{ 
+	snd_info_done+45544
+	handle_irq_event_percpu+60
+	handle_irq_event+84
+	handle_edge_irq+204
+	generic_handle_irq+52
+	bcm2835_gpio_irq_handle_bank+148
+	bcm2835_gpio_irq_handler+116
+	generic_handle_irq+52
+	bcm2836_chained_handle_irq+72
+	generic_handle_irq+52
+	__handle_domain_irq+108
+	bcm2836_arm_irqchip_handle_irq+96
	__irq_usr+76
, 167 }: 1
{ 
+	snd_info_done+45544
+	handle_irq_event_percpu+60
+	handle_irq_event+84
+	handle_edge_irq+204
+	generic_handle_irq+52
+	bcm2835_gpio_irq_handle_bank+148
+	bcm2835_gpio_irq_handler+116
+	generic_handle_irq+52
+	bcm2836_chained_handle_irq+72
+	generic_handle_irq+52
+	__handle_domain_irq+108
+	bcm2836_arm_irqchip_handle_irq+96
	__irq_svc+92
	default_idle_call+64
	do_idle+292
	cpu_startup_entry+40
	rest_init+180
	parse_early_param+128
	start_kernel+1212
, 167 }: 69

从 ARM 的文件 可以知道,这是处理不同的 interrupt 进入点。

暴走的 ftrace

一开始我是想使用 irq_handler_exit 这个选项:

$ sudo trace-cmd list -e | grep irq
 irq:softirq_raise
 irq:softirq_exit
 irq:softirq_entry
 irq:irq_handler_exit
 irq:irq_handler_entry
 preemptirq:irq_enable
 preemptirq:irq_disable
 rtc:rtc_alarm_irq_enable
 rtc:rtc_irq_set_state
 rtc:rtc_irq_set_freq

所以本来是用像这样的指令:

$ sudo trace-cmd record -p function_graph \
    -e irq_handler_entry

不过发现仅仅数秒就会跑出将近 100 MB 的资料,而且非常混乱。所以就决定换成仅用 function 这个 tracer 了。

附录:完整程序

装置树跟昨天的实验一样,Makefile 亦同。

传统 IRQ

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>

struct minirq_dev {
    struct gpio_desc *gpiod;
    struct work_struct work;
    int irq;
    struct device *dev;
};

void minirq_bottom_half(struct work_struct *work)
{
    struct minirq_dev *minirq;
    minirq = container_of(work, struct minirq_dev, work);
    dev_info(minirq->dev, "Rising edge detected!\n");
    return;
}

static irqreturn_t minirq_top_half(int irq, void *p)
{
    struct platform_device *pdev = p;
    struct minirq_dev *minirq;

    minirq = platform_get_drvdata(pdev);
    schedule_work(&(minirq -> work));
    return IRQ_HANDLED;
}

static int minirq_probe(struct platform_device *pdev)
{
    struct device *dev = &(pdev-> dev);
    struct minirq_dev *minirq;
    int ret = 0;

    minirq = devm_kzalloc(dev, sizeof(struct minirq_dev), GFP_KERNEL);
    if (!minirq) {
        dev_err(dev, "Failed to allocate memory.\n");
	    return -ENOMEM;
    }
    platform_set_drvdata(pdev, minirq);
    minirq->dev = dev;

    minirq->gpiod = devm_gpiod_get_index(dev, "minirq", 0, GPIOD_IN);
    if (IS_ERR(minirq->gpiod)) {
        dev_err(dev, "Failed to get gpio descriptor.\n");
        return PTR_ERR(minirq -> gpiod);
    }
    
    ret = gpiod_to_irq(minirq->gpiod);
    if (ret < 0) {
        dev_err(dev, "Failed to get irq from gpiod.\n");
        return ret;
    }
    minirq->irq = ret;

    INIT_WORK(&minirq->work, minirq_bottom_half);

    ret = devm_request_irq(dev, minirq->irq, minirq_top_half, 
            IRQF_TRIGGER_RISING, "minirq", pdev);
    if (ret < 0) {
        dev_err(dev, "Failed to request IRQ.\n");
        return ret;
    }

    return 0;
}

static const struct of_device_id minirq_ids[] = {
    {.compatible = "minirq",},
    {}
};

static struct platform_driver minirq_driver = {
    .driver = {
        .name = "minirq",
	    .of_match_table = minirq_ids,
    },
    .probe = minirq_probe
};
MODULE_LICENSE("GPL");
module_platform_driver(minirq_driver);

Threaded IRQ

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>

struct minirq_dev {
    struct gpio_desc *gpiod;
    struct work_struct work;
    int irq;
    struct device *dev;
};


static irqreturn_t minirq_top_half(int irq, void *p)
{
    return IRQ_WAKE_THREAD;
}

static irqreturn_t minirq_bottom_half(int irq, void *p)
{
    struct minirq_dev *minirq = p;
    dev_info(minirq->dev, "Rising edge detected!\n");
    return IRQ_HANDLED;
}

static int minirq_probe(struct platform_device *pdev)
{
    struct device *dev = &(pdev-> dev);
    struct minirq_dev *minirq;
    int ret = 0;

    minirq = devm_kzalloc(dev, sizeof(struct minirq_dev), GFP_KERNEL);
    if (!minirq) {
        dev_err(dev, "Failed to allocate memory.\n");
	    return -ENOMEM;
    }
    minirq->dev = dev;

    minirq->gpiod = devm_gpiod_get_index(dev, "minirq", 0, GPIOD_IN);
    if (IS_ERR(minirq->gpiod)) {
        dev_err(dev, "Failed to get gpio descriptor.\n");
        return PTR_ERR(minirq -> gpiod);
    }
    
    ret = gpiod_to_irq(minirq->gpiod);
    if (ret < 0) {
        dev_err(dev, "Failed to get irq from gpiod.\n");
        return ret;
    }
    minirq->irq = ret;

    ret = devm_request_threaded_irq(dev, minirq->irq, 
            minirq_top_half, minirq_bottom_half, 
            IRQF_TRIGGER_RISING, "minirq", minirq);
    if (ret < 0) {
        dev_err(dev, "Failed to request IRQ.\n");
        return ret;
    }

    return 0;
}

static const struct of_device_id minirq_ids[] = {
    {.compatible = "minirq",},
    {}
};

static struct platform_driver minirq_driver = {
    .driver = {
        .name = "minirq",
	    .of_match_table = minirq_ids,
    },
    .probe = minirq_probe
};
MODULE_LICENSE("GPL");
module_platform_driver(minirq_driver);

<<:  29. 建立路由

>>:  这些日子我学到的JavaScript:Day26- BOM

#12 JavaScript Introduction

What is JavaScript? HTML is like the structure/bon...

【没钱买ps,PyQt自己写】Day 12 - 建立一个可以缩放图片大小的显示器 (基於 QImage 使用 OpenCV)

看完这篇文章你会得到的成果图 zoom in zoom out 前言 我们接下来的讨论,会基於读者已...

那些被忽略但很好用的 Web API / Selection

选你所爱,爱你所选。 在浏览网站时,反白(或称反蓝)其实是一个非常常见的动作,不管是要强调目前的阅...

【Day08】for 回圈在硬体的使用及该注意的那些事

for-loop 在 C/C++ 语言中,我们经常用到 for 回圈语句,但在 Verilog 中 ...

30天轻松学会unity自制游戏-敌机反击

敌机要反击跟Player设定差不了多少,首先也让敌机自动发射个子弹,找到素材把Enemy Bulle...