加入星計劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • Linux 中斷管理機(jī)制
    • 中斷下半部之 workqueue
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

吐血整理 | 肝翻 Linux中斷所有知識點

10/09 13:55
641
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

Linux 中斷管理機(jī)制

GIC 硬件原理

GIC,Generic Interrupt Controller。是ARM公司提供的一個通用的中斷控制器。主要作用為:接受硬件中斷信號,并經(jīng)過一定處理后,分發(fā)給對應(yīng)的CPU進(jìn)行處理。

當(dāng)前GIC 有四個版本,GIC v1~v4, 本文主要介紹GIC v3控制器。

GIC v3中斷類別

GICv3定義了以下中斷類型:

    SGI (Software Generated Interrupt):軟件觸發(fā)的中斷。軟件可以通過寫 GICD_SGIR 寄存器來觸發(fā)一個中斷事件,一般用于核間通信,內(nèi)核中的 IPI:inter-processor interrupts 就是基于 SGI。
    PPI (Private Peripheral Interrupt):私有外設(shè)中斷。這是每個核心私有的中斷。PPI會送達(dá)到指定的CPU上,應(yīng)用場景有CPU本地時鐘。
    SPI (Shared Peripheral Interrupt):公用的外部設(shè)備中斷,也定義為共享中斷。中斷產(chǎn)生后,可以分發(fā)到某一個CPU上。比如按鍵觸發(fā)一個中斷,手機(jī)觸摸屏觸發(fā)的中斷。
    LPI (Locality-specific Peripheral Interrupt):LPI 是 GICv3 中的新特性,它們在很多方面與其他類型的中斷不同。LPI 始終是基于消息的中斷,它們的配置保存在表中而不是寄存器。比如 PCIe 的 MSI/MSI-x 中斷。
中斷類型 硬件中斷號
SGI 0-15
PPI 16-31
SPI 32-1019
reserved ......
LPI 8192-MAX

GIC v3 組成

GICv3 控制器由以下三部分組成:

    Distributor:SPI 中斷的管理,將中斷發(fā)送給 Redistributor
    打開或關(guān)閉每個中斷。Distributor對中斷的控制分成兩個級別。一個是全局中斷的控制(GIC_DIST_CTRL)。一旦關(guān)閉了全局的中斷,那么任何的中斷源產(chǎn)生的中斷事件都不會被傳遞到 CPU interface。另外一個級別是對針對各個中斷源進(jìn)行控制(GIC_DIST_ENABLE_CLEAR),關(guān)閉某一個中斷源會導(dǎo)致該中斷事件不會分發(fā)到 CPU interface,但不影響其他中斷源產(chǎn)生中斷事件的分發(fā)??刂茖?dāng)前優(yōu)先級最高的中斷事件分發(fā)到一個或者一組 CPU interface。當(dāng)一個中斷事件分發(fā)到多個 CPU interface 的時候,GIC 的內(nèi)部邏輯應(yīng)該保證只 assert 一個CPU。優(yōu)先級控制。interrupt屬性設(shè)定。設(shè)置每個外設(shè)中斷的觸發(fā)方式:電平觸發(fā)、邊緣觸發(fā);interrupt group的設(shè)定。設(shè)置每個中斷的 Group,其中 Group0 用于安全中斷,支持 FIQ 和 IRQ,Group1 用于非安全中斷,只支持 IRQ;
    Redistributor:SGI,PPI,LPI 中斷的管理,將中斷發(fā)送給 CPU interface
    啟用和禁用 SGI 和 PPI。設(shè)置 SGI 和 PPI 的優(yōu)先級。將每個 PPI 設(shè)置為電平觸發(fā)或邊緣觸發(fā)。將每個 SGI 和 PPI 分配給中斷組??刂?SGI 和 PPI 的狀態(tài)。內(nèi)存中數(shù)據(jù)結(jié)構(gòu)的基址控制,支持 LPI 的相關(guān)中斷屬性和掛起狀態(tài)。電源管理支持。
    CPU interface:傳輸中斷給 Core
    打開或關(guān)閉 CPU interface 向連接的 CPU assert 中斷事件。對于 ARM,CPU interface 和 CPU 之間的中斷信號線是 nIRQCPU 和 nFIQCPU。如果關(guān)閉了中斷,即便是 Distributor 分發(fā)了一個中斷事件到 CPU interface,也不會 assert 指定的 nIRQ 或者 nFIQ 通知 Core。中斷的確認(rèn)。Core 會向 CPU interface 應(yīng)答中斷(應(yīng)答當(dāng)前優(yōu)先級最高的那個中斷),中斷一旦被應(yīng)答,Distributor 就會把該中斷的狀態(tài)從 pending 修改成 active 或者 pending and active(這是和該中斷源的信號有關(guān),例如如果是電平中斷并且保持了該 asserted 電平,那么就是 pending and active)。ack 了中斷之后,CPU interface 就會 deassert nIRQCPU 和 nFIQCPU 信號線。中斷處理完畢的通知。當(dāng) interrupt handler 處理完了一個中斷的時候,會向?qū)?CPU interface 的寄存器通知 GIC CPU 已經(jīng)處理完該中斷。做這個動作一方面是通知 Distributor 將中斷狀態(tài)修改為 deactive,另外一方面,CPU interface 會 priority drop,從而允許其他的 pending 的中斷向 CPU 提交。為 CPU 設(shè)置中斷優(yōu)先級掩碼。通過 priority mask,可以 mask 掉一些優(yōu)先級比較低的中斷,這些中斷不會通知到 CPU。設(shè)置 CPU 的中斷搶占(preemption)策略。在多個中斷事件同時到來的時候,選擇一個優(yōu)先級最高的通知 CPU。

GICv3 控制器內(nèi)部模塊和各中斷類型的關(guān)系如下圖所示:

中斷路由

GICv3 使用 hierarchy 來標(biāo)識一個具體的 core, 如下圖是一個四層的結(jié)構(gòu)(aarch64):

用 <affinity level 3>.<affinity level 2>.<affinity level 1>.<affinity level 0> 的形式組成一個 PE 的路由。每一個 core 的 affnity 值可以通過 MPDIR_EL1 寄存器獲取, 每一個 affinity 占用8bit。配置對應(yīng) core 的 MPIDR 值,可以將中斷路由到該 core 上。

各個 affinity 的定義是根據(jù) SOC 自己的定義,比如:

<group?of?groups>.?<group?of?processors>.<processor>.<core>
<group?of?processors>.<processor>.<core>.<thread>

中斷親和性的設(shè)置的通用函數(shù)為 irq_set_affinity,后面會做詳細(xì)介紹。

中斷狀態(tài)機(jī)

中斷處理的狀態(tài)機(jī)如下圖:

    Inactive:無中斷狀態(tài),即沒有 Pending 也沒有 Active。Pending:硬件或軟件觸發(fā)了中斷,該中斷事件已經(jīng)通過硬件信號通知到 GIC,等待 GIC 分配的那個 CPU 進(jìn)行處理,在電平觸發(fā)模式下,產(chǎn)生中斷的同時保持 Pending 狀態(tài)。Active:CPU 已經(jīng)應(yīng)答(acknowledge)了該中斷請求,并且正在處理中。Active and pending:當(dāng)一個中斷源處于 Active 狀態(tài)的時候,同一中斷源又觸發(fā)了中斷,進(jìn)入 pending 狀態(tài)。

中斷處理流程

    外設(shè)發(fā)起中斷,發(fā)送給 DistributorDistributor 將該中斷,分發(fā)給合適的 RedistributorRedistributor 將中斷信息,發(fā)送給 CPU interfaceCPU interface 產(chǎn)生合適的中斷異常給處理器處理器接收該異常,并且軟件處理該中斷

GIC 驅(qū)動

這里主要分析 linux kernel 中 GIC v3 中斷控制器的代碼(drivers/irqchip/irq-gic-v3.c)。

設(shè)備樹

先來看下一個中斷控制器的設(shè)備樹信息:

gic:?interrupt-controller@51a00000?{
????????compatible?=?"arm,gic-v3";
????????reg?=?<0x0?0x51a00000?0?0x10000>,?/*?GIC?Dist?*/
??????????????<0x0?0x51b00000?0?0xC0000>,?/*?GICR?*/
??????????????<0x0?0x52000000?0?0x2000>,??/*?GICC?*/
??????????????<0x0?0x52010000?0?0x1000>,??/*?GICH?*/
??????????????<0x0?0x52020000?0?0x20000>;?/*?GICV?*/
????????#interrupt-cells?=?<3>;
????????interrupt-controller;
????????interrupts?=?<GIC_PPI?9
????????????????(GIC_CPU_MASK_SIMPLE(6)?|?IRQ_TYPE_LEVEL_HIGH)>;
????????interrupt-parent?=?<&gic>;
};
    compatible:用于匹配GICv3驅(qū)動reg :GIC的物理基地址,分別對應(yīng)GICD,GICR,GICC…#interrupt-cells:這是一個中斷控制器節(jié)點的屬性。它聲明了該中斷控制器的中斷指示符(interrupts)中 cell 的個數(shù)interrupt-controller: 表示該節(jié)點是一個中斷控制器interrupts:分別代表中斷類型,中斷號,中斷類型, PPI中斷親和, 保留字段

關(guān)于設(shè)備數(shù)的各個字段含義,詳細(xì)可以參考 Documentation/devicetree/bindings 下的對應(yīng)信息。

初始化

1. irq chip driver 的聲明:

IRQCHIP_DECLARE(gic_v3,?"arm,gic-v3",?gic_of_init);

定義 IRQCHIP_DECLARE 之后,相應(yīng)的內(nèi)容會保存到 __irqchip_of_table 里邊:

#define?IRQCHIP_DECLARE(name,?compat,?fn)?OF_DECLARE_2(irqchip,?name,?compat,?fn)

#define?OF_DECLARE_2(table,?name,?compat,?fn)??
????????_OF_DECLARE(table,?name,?compat,?fn,?of_init_fn_2)

#define?_OF_DECLARE(table,?name,?compat,?fn,?fn_type)?????????????
????static?const?struct?of_device_id?__of_table_##name?????????
????????__used?__section(__##table##_of_table)?????????????
?????????=?{?.compatible?=?compat,?????????????????
?????????????.data?=?(fn?==?(fn_type)NULL)???fn?:?fn??}

__irqchip_of_table 在鏈接腳本 vmlinux.lds 里,被放到了 __irqchip_begin 和 __irqchip_of_end 之間,該段用于存放中斷控制器信息:

#ifdef?CONFIG_IRQCHIP
????#define?IRQCHIP_OF_MATCH_TABLE()????????????????????
????????.?=?ALIGN(8);???????????????????????????
????????VMLINUX_SYMBOL(__irqchip_begin)?=?.;????????????????
????????*(__irqchip_of_table)???????????????????????
????????*(__irqchip_of_end)
#endif

在內(nèi)核啟動初始化中斷的函數(shù)中,of_irq_init 函數(shù)會去查找設(shè)備節(jié)點信息,該函數(shù)的傳入?yún)?shù)就是 __irqchip_of_table 段,由于 IRQCHIP_DECLARE 已經(jīng)將信息填充好了,of_irq_init 函數(shù)會根據(jù) “arm,gic-v3” 去查找對應(yīng)的設(shè)備節(jié)點,并獲取設(shè)備的信息。or_irq_init 函數(shù)中,最終會回調(diào) IRQCHIP_DECLARE 聲明的回調(diào)函數(shù),也就是 gic_of_init,而這個函數(shù)就是 GIC 驅(qū)動的初始化入口。

2. gic_of_init 流程:

static?int?__init?gic_of_init(struct?device_node?*node,?struct?device_node?*parent)
{
??......
?dist_base?=?of_iomap(node,?0);???????????????????????????????????????????------(1)
?if?(!dist_base)?{
??pr_err("%pOF:?unable?to?map?gic?dist?registersn",?node);
??return?-ENXIO;
?}

?err?=?gic_validate_dist_version(dist_base);??????????????????????????????------(2)
?if?(err)?{
??pr_err("%pOF:?no?distributor?detected,?giving?upn",?node);
??goto?out_unmap_dist;
?}

?if?(of_property_read_u32(node,?"#redistributor-regions",?&nr_redist_regions))??------(3)
??nr_redist_regions?=?1;

?rdist_regs?=?kzalloc(sizeof(*rdist_regs)?*?nr_redist_regions,?GFP_KERNEL);
?if?(!rdist_regs)?{
??err?=?-ENOMEM;
??goto?out_unmap_dist;
?}

?for?(i?=?0;?i?<?nr_redist_regions;?i++)?{????????????????????????????????------(4)
??struct?resource?res;
??int?ret;

??ret?=?of_address_to_resource(node,?1?+?i,?&res);
??rdist_regs[i].redist_base?=?of_iomap(node,?1?+?i);
??if?(ret?||?!rdist_regs[i].redist_base)?{
???pr_err("%pOF:?couldn't?map?region?%dn",?node,?i);
???err?=?-ENODEV;
???goto?out_unmap_rdist;
??}
??rdist_regs[i].phys_base?=?res.start;
?}
?
?if?(of_property_read_u64(node,?"redistributor-stride",?&redist_stride))??------(5)
??redist_stride?=?0;

?err?=?gic_init_bases(dist_base,?rdist_regs,?nr_redist_regions,???????????------(6)
????????redist_stride,?&node->fwnode);
?if?(err)
??goto?out_unmap_rdist;

?gic_populate_ppi_partitions(node);???????????????????????????????????????------(7)
?gic_of_setup_kvm_info(node);
?return?0;
??......
?return?err;
}
    映射 GICD 的寄存器地址空間。驗證 GICD 的版本是 GICv3 還是 GICv4(主要通過讀GICD_PIDR2寄存器bit[7:4]. 0x1代表GICv1, 0x2代表GICv2…以此類推)。通過 DTS 讀取 redistributor-regions 的值。為一個 GICR 域分配基地址。通過 DTS 讀取 redistributor-stride 的值。下面詳細(xì)介紹。設(shè)置一組 PPI 的親和性。
static?int?__init?gic_init_bases(void?__iomem?*dist_base,
?????struct?redist_region?*rdist_regs,
?????u32?nr_redist_regions,
?????u64?redist_stride,
?????struct?fwnode_handle?*handle)
{
??......
?typer?=?readl_relaxed(gic_data.dist_base?+?GICD_TYPER);????????????????------(1)
?gic_data.rdists.id_bits?=?GICD_TYPER_ID_BITS(typer);
?gic_irqs?=?GICD_TYPER_IRQS(typer);
?if?(gic_irqs?>?1020)
??gic_irqs?=?1020;
?gic_data.irq_nr?=?gic_irqs;

?gic_data.domain?=?irq_domain_create_tree(handle,?&gic_irq_domain_ops,??------(2)
???????&gic_data);
?gic_data.rdists.rdist?=?alloc_percpu(typeof(*gic_data.rdists.rdist));
?gic_data.rdists.has_vlpis?=?true;
?gic_data.rdists.has_direct_lpi?=?true;
??......
?set_handle_irq(gic_handle_irq);????????????????????????????????????????------(3)

?gic_update_vlpi_properties();??????????????????????????????????????????------(4)

?if?(IS_ENABLED(CONFIG_ARM_GIC_V3_ITS)?&&?gic_dist_supports_lpis())
??its_init(handle,?&gic_data.rdists,?gic_data.domain);??????????????????------(5)

?gic_smp_init();????????????????????????????????????????????????????????------(6)
?gic_dist_init();???????????????????????????????????????????????????????------(7)
?gic_cpu_init();????????????????????????????????????????????????????????------(8)
?gic_cpu_pm_init();?????????????????????????????????????????????????????------(9)

?return?0;
??......
}
    確認(rèn)支持 SPI 中斷號最大的值為多少。向系統(tǒng)中注冊一個 irq domain 的數(shù)據(jù)結(jié)構(gòu),irq_domain 主要作用是將硬件中斷號映射到 irq number,后面會做詳細(xì)的介紹。設(shè)定 arch 相關(guān)的 irq handler。gic_irq_handle 是內(nèi)核 gic 中斷處理的入口函數(shù),后面會做詳細(xì)的介紹。gic 虛擬化相關(guān)的內(nèi)容。初始化 ITS。設(shè)置 SMP 核間交互的回調(diào)函數(shù),用于 IPI,回到函數(shù)為 gic_raise_softir。初始化 Distributor。初始化 CPU interface。初始化 GIC 電源管理。

中斷的映射

當(dāng)早期的系統(tǒng)只存在一個中斷控制器,而且中斷數(shù)目也不多的時候,一個很簡單的做法就是一個中斷號對應(yīng)到中斷控制器的一個號,可以說是簡單的線性映射:

但當(dāng)一個系統(tǒng)中有多個中斷控制器,而且中斷號也逐漸增加的時候。linux 內(nèi)核為了應(yīng)對此問題,引入了 irq_domain 的概念。

irq_domain 的引入相當(dāng)于一個中斷控制器就是一個 irq_domain。這樣一來所有的中斷控制器就會出現(xiàn)級聯(lián)的布局。利用樹狀的結(jié)構(gòu)可以充分的利用 irq 數(shù)目,而且每一個 irq_domain 區(qū)域可以自己去管理自己 interrupt 的特性。

每一個中斷控制器對應(yīng)多個中斷號, 而硬件中斷號在不同的中斷控制器上是會重復(fù)編碼的, 這時僅僅用硬中斷號已經(jīng)不能唯一標(biāo)識一個外設(shè)中斷,因此 linux kernel 提供了一個虛擬中斷號的概念。

接下來我們看下硬件中斷號是如何映射到虛擬中斷號的。

數(shù)據(jù)結(jié)構(gòu)

在看硬件中斷號映射到虛擬中斷號之前,先來看下幾個比較重要的數(shù)據(jù)結(jié)構(gòu)。

struct irq_desc 描述一個外設(shè)的中斷,稱之中斷描述符。

struct?irq_desc?{
?struct?irq_common_data?irq_common_data;
?struct?irq_data??irq_data;??
?unsigned?int?__percpu?*kstat_irqs;
?irq_flow_handler_t?handle_irq;??
??......
?struct?irqaction?*action;?
?......
}?____cacheline_internodealigned_in_smp;
    irq_data:中斷控制器的硬件數(shù)據(jù)handle_irq:中斷控制器驅(qū)動的處理函數(shù),指向一個 struct irqaction 的鏈表,一個中斷源可以多個設(shè)備共享,所以一個 irq_desc 可以掛載多個 action,由鏈表結(jié)構(gòu)組織起來action:設(shè)備驅(qū)動的處理函數(shù)

struct irq_data 包含中斷控制器的硬件數(shù)據(jù)。

struct?irq_data?{
?u32???mask;
?unsigned?int??irq;
?unsigned?long??hwirq;
?struct?irq_common_data?*common;
?struct?irq_chip??*chip;
?struct?irq_domain?*domain;
#ifdef?CONFIG_IRQ_DOMAIN_HIERARCHY
?struct?irq_data??*parent_data;
#endif
?void???*chip_data;
};
    irq:虛擬中斷號hwirq:硬件中斷號chip:對應(yīng)的 irq_chip 數(shù)據(jù)結(jié)構(gòu)domain:對應(yīng)的 irq_domain 數(shù)據(jù)結(jié)構(gòu)

struct irq_chip 用于對中斷控制器的硬件操作。

struct?irq_chip?{
?struct?device?*parent_device;
?const?char?*name;
?unsigned?int?(*irq_startup)(struct?irq_data?*data);
?void??(*irq_shutdown)(struct?irq_data?*data);
?void??(*irq_enable)(struct?irq_data?*data);
?void??(*irq_disable)(struct?irq_data?*data);

?void??(*irq_ack)(struct?irq_data?*data);
?void??(*irq_mask)(struct?irq_data?*data);
?void??(*irq_mask_ack)(struct?irq_data?*data);
?void??(*irq_unmask)(struct?irq_data?*data);
?void??(*irq_eoi)(struct?irq_data?*data);

?int??(*irq_set_affinity)(struct?irq_data?*data,?const?struct?cpumask?*dest,?bool?force);
?int??(*irq_retrigger)(struct?irq_data?*data);
?int??(*irq_set_type)(struct?irq_data?*data,?unsigned?int?flow_type);
?int??(*irq_set_wake)(struct?irq_data?*data,?unsigned?int?on);

?void??(*irq_bus_lock)(struct?irq_data?*data);
?void??(*irq_bus_sync_unlock)(struct?irq_data?*data);
?......
};
    parent_device:指向父設(shè)備name:/proc/interrupts 中顯示的名字irq_startup:啟動中斷,如果設(shè)置成 NULL,則默認(rèn)為 enableirq_shutdown:關(guān)閉中斷,如果設(shè)置成 NULL,則默認(rèn)為 disableirq_enable:中斷使能,如果設(shè)置成 NULL,則默認(rèn)為 chip->unmaskirq_disable:中斷禁止irq_ack:開始新的中斷irq_mask:中斷源屏蔽irq_mask_ack:應(yīng)答并屏蔽中斷irq_unmask:解除中斷屏蔽irq_eoi:中斷處理結(jié)束后調(diào)用irq_set_affinity:在 SMP 中設(shè)置 CPU 親和力irq_retrigger:重新發(fā)送中斷到 CPUirq_set_type:設(shè)置中斷觸發(fā)類型irq_set_wake:使能/禁止電源管理中的喚醒功能irq_bus_lock:慢速芯片總線上的鎖irq_bus_sync_unlock:同步釋放慢速總線芯片的鎖

struct irq_domain 與中斷控制器對應(yīng),完成硬件中斷號 hwirq 到 virq 的映射。

struct?irq_domain?{
?struct?list_head?link;
?const?char?*name;
?const?struct?irq_domain_ops?*ops;
?void?*host_data;
?unsigned?int?flags;
?unsigned?int?mapcount;

?/*?Optional?data?*/
?struct?fwnode_handle?*fwnode;
?enum?irq_domain_bus_token?bus_token;
?struct?irq_domain_chip_generic?*gc;
#ifdef?CONFIG_IRQ_DOMAIN_HIERARCHY
?struct?irq_domain?*parent;
#endif
#ifdef?CONFIG_GENERIC_IRQ_DEBUGFS
?struct?dentry??*debugfs_file;
#endif

?/*?reverse?map?data.?The?linear?map?gets?appended?to?the?irq_domain?*/
?irq_hw_number_t?hwirq_max;
?unsigned?int?revmap_direct_max_irq;
?unsigned?int?revmap_size;
?struct?radix_tree_root?revmap_tree;
?unsigned?int?linear_revmap[];
};
    link:用于將 irq_domain 連接到全局鏈表 irq_domain_list 中name:irq_domain 的名稱ops:irq_domain 映射操作函數(shù)集mapcount:映射好的中斷的數(shù)量fwnode:對應(yīng)中斷控制器的 device nodeparent:指向父級 irq_domain 的指針,用于支持級聯(lián) irq_domainhwirq_max:該 irq_domain 支持的中斷最大數(shù)量linear_revmap[]:hwirq->virq 反向映射的線性表

struct irq_domain_ops 是 irq_domain 映射操作函數(shù)集。

struct?irq_domain_ops?{
?int?(*match)(struct?irq_domain?*d,?struct?device_node?*node,
???????enum?irq_domain_bus_token?bus_token);
?int?(*select)(struct?irq_domain?*d,?struct?irq_fwspec?*fwspec,
????????enum?irq_domain_bus_token?bus_token);
?int?(*map)(struct?irq_domain?*d,?unsigned?int?virq,?irq_hw_number_t?hw);
?void?(*unmap)(struct?irq_domain?*d,?unsigned?int?virq);
?int?(*xlate)(struct?irq_domain?*d,?struct?device_node?*node,
???????const?u32?*intspec,?unsigned?int?intsize,
???????unsigned?long?*out_hwirq,?unsigned?int?*out_type);
?......
};
    match:用于中斷控制器設(shè)備與 irq_domain 的匹配map:用于硬件中斷號與 Linux 中斷號的映射xlate:通過 device_node,解析硬件中斷號和觸發(fā)方式

struct irqaction 主要是用來存設(shè)備驅(qū)動注冊的中斷處理函數(shù)。

struct?irqaction?{
?irq_handler_t??handler;?
?void???*dev_id;??
??......
?unsigned?int??irq;??
?unsigned?int??flags;??
??......
?const?char??*name;???
?struct?proc_dir_entry?*dir;
}?____cacheline_internodealigned_in_smp;
    handler:設(shè)備驅(qū)動里的中斷處理函數(shù)dev_id:設(shè)備 idirq:中斷號flags:中斷標(biāo)志,注冊時設(shè)置,比如上升沿中斷,下降沿中斷等name:中斷名稱,產(chǎn)生中斷的硬件的名字dir:指向 /proc/irq/ 相關(guān)的信息

這里,我們用一張圖來匯總下上面的數(shù)據(jù)結(jié)構(gòu):

上面的結(jié)構(gòu)體 struct irq_desc 是設(shè)備驅(qū)動加載的過程中完成的,讓設(shè)備樹中的中斷能與具體的中斷描述符 irq_desc 匹配,其中 struct irqaction 保存著設(shè)備的中斷處理函數(shù)。右邊框內(nèi)的結(jié)構(gòu)體主要是在中斷控制器驅(qū)動加載的過程中完成的,其中 struct irq_chip 用于對中斷控制器的硬件操作,struct irq_domain 用于硬件中斷號到 Linux irq 的映射。

下面我們結(jié)合代碼看下中斷控制器驅(qū)動和設(shè)備驅(qū)動是如何創(chuàng)建這些結(jié)構(gòu)體,并且硬中斷和虛擬中斷號是如何完成映射的。

中斷控制器注冊 irq_domain

通過 __irq_domain_add 初始化 irq_domain 數(shù)據(jù)結(jié)構(gòu),然后把 irq_domain 添加到全局的鏈表 irq_domain_list 中。

外設(shè)的驅(qū)動創(chuàng)建硬中斷和虛擬中斷號的映射關(guān)系

設(shè)備的驅(qū)動在初始化的時候可以調(diào)用 irq_of_parse_and_map 這個接口函數(shù)進(jìn)行該 device node 中和中斷相關(guān)的內(nèi)容的解析,并建立映射關(guān)系

    of_irq_parse_one 函數(shù)用于解析DTS文件中設(shè)備定義的屬性,如"reg", “interrupt”irq_find_matching_fwspec 遍歷 irq_domain_list 鏈表,找到 device node 匹配的 irq_domaingic_irq_domain_translate 解析出中斷信息,比如硬件中斷號 hwirq,中斷觸發(fā)方式irq_domain_alloc_descs 分配一個虛擬的中斷號 virq,分配和初始化中斷描述符 irq_descgic_irq_domain_alloc 為 hwirq 和 virq 創(chuàng)建映射關(guān)系。內(nèi)部會通過 irq_domain_set_info 調(diào)用 irq_domain_set_hwirq_and_chip,然后通過 virq 獲取 irq_data 結(jié)構(gòu)體,并將 hwirq 設(shè)置到 irq_data->hwirq 中, 最終完成 hwirq 到 virq 的映射irq_domain_set_info 根據(jù)硬件中斷號的范圍設(shè)置 irq_desc->handle_irq 的指針,共享中斷入口為 handle_fasteoi_irq,私有中斷入口為 handle_percpu_devid_irq

最后,我們可以通過 /proc/interrupts 下的值來看下它們的關(guān)系:

現(xiàn)在,我們已經(jīng)知道內(nèi)核為硬件中斷號與 Linux 中斷號做了映射,相關(guān)數(shù)據(jù)結(jié)構(gòu)的綁定及初始化,并且設(shè)置了中斷處理函數(shù)執(zhí)行的入口。接下來我們再看下設(shè)備的中斷是怎么來注冊的?

中斷的注冊

設(shè)備驅(qū)動中,獲取到了 irq 中斷號后,通常就會采用 request_irq/request_threaded_irq 來注冊中斷,其中 request_irq 用于注冊普通處理的中斷。request_threaded_irq 用于注冊線程化處理的中斷,線程化中斷的主要目的把中斷上下文的任務(wù)遷移到線程中,減少系統(tǒng)關(guān)中斷的時間,增強(qiáng)系統(tǒng)的實時性。

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

其中 irq 是 linux 中斷號,handler 是中斷處理函數(shù),flags 是中斷標(biāo)志位,name 是中斷的名字。在講具體的注冊流程前,先看一下主要的中斷標(biāo)志位:

#define?IRQF_SHARED??0x00000080??????????????//多個設(shè)備共享一個中斷號,需要外設(shè)硬件支持
#define?IRQF_PROBE_SHARED?0x00000100??????????????//中斷處理程序允許sharing?mismatch發(fā)生
#define?__IRQF_TIMER??0x00000200???????????????//時鐘中斷
#define?IRQF_PERCPU??0x00000400???????????????//屬于特定CPU的中斷
#define?IRQF_NOBALANCING?0x00000800???????????????//禁止在CPU之間進(jìn)行中斷均衡處理
#define?IRQF_IRQPOLL??0x00001000??????????????//中斷被用作輪訓(xùn)
#define?IRQF_ONESHOT??0x00002000??????????????//一次性觸發(fā)的中斷,不能嵌套,1)在硬件中斷處理完成后才能打開中斷;2)在中斷線程化中保持關(guān)閉狀態(tài),直到該中斷源上的所有thread_fn函數(shù)都執(zhí)行完
#define?IRQF_NO_SUSPEND??0x00004000??????//系統(tǒng)休眠喚醒操作中,不關(guān)閉該中斷
#define?IRQF_FORCE_RESUME?0x00008000??????????????//系統(tǒng)喚醒過程中必須強(qiáng)制打開該中斷
#define?IRQF_NO_THREAD??0x00010000??????//禁止中斷線程化
#define?IRQF_EARLY_RESUME?0x00020000??????//系統(tǒng)喚醒過程中在syscore階段resume,而不用等到設(shè)備resume階段
#define?IRQF_COND_SUSPEND?0x00040000??????//與NO_SUSPEND的用戶共享中斷時,執(zhí)行本設(shè)備的中斷處理函數(shù)

創(chuàng)建完成后,通過 ps 命令可以查看系統(tǒng)中的中斷線程,注意這些線程是實時線程 SCHED_FIFO:

#?ps?-A?|?grep?"irq/"
root??????????1749?????2???????0??????0?irq_thread??????????0?S?[irq/433-imx_drm]
root??????????1750?????2???????0??????0?irq_thread??????????0?S?[irq/439-imx_drm]
root??????????1751?????2???????0??????0?irq_thread??????????0?S?[irq/445-imx_drm]
root??????????1752?????2???????0??????0?irq_thread??????????0?S?[irq/451-imx_drm]
root??????????2044?????2???????0??????0?irq_thread??????????0?S?[irq/279-isl2902]
root??????????2192?????2???????0??????0?irq_thread??????????0?S?[irq/114-mmc0]
root??????????2199?????2???????0??????0?irq_thread??????????0?S?[irq/115-mmc1]
root??????????2203?????2???????0??????0?irq_thread??????????0?S?[irq/322-5b02000]
root??????????2361?????2???????0??????0?irq_thread??????????0?S?[irq/294-4-0051]

中斷的處理

當(dāng)完成中斷的注冊后,所有結(jié)構(gòu)的組織關(guān)系都已經(jīng)建立好,剩下的工作就是當(dāng)信號來臨時,進(jìn)行中斷的處理工作。這里我們站在前面知識點的基礎(chǔ)上,把中斷觸發(fā),中斷處理等整個流程走一遍。

假設(shè)當(dāng)前在 EL0 運(yùn)行一個應(yīng)用程序,觸發(fā)了一個 EL0 的 irq 中斷,則處理器會做如下的操作:

先會跳到 arm64 對應(yīng)的異常向量表:

/*
?*?Exception?vectors.
?*/
????????.pushsection?".entry.text",?"ax"

????????.align??11
SYM_CODE_START(vectors)
????????......
????????
????????kernel_ventry???1,?sync?????????????????????????//?el1?下的同步異常,例如指令執(zhí)行異常、缺頁中斷等
????????kernel_ventry???1,?irq??????????????????????????// el1 下的異步異常,硬件中斷。1代表異常等級
????????kernel_ventry???1,?fiq_invalid??????????????????//?FIQ?EL1h
????????kernel_ventry???1,?error????????????????????????//?Error?EL1h

????????kernel_ventry???0,?sync?????????????????????????//?el0?下的同步異常,例如指令執(zhí)行異常、缺頁中斷(跳轉(zhuǎn)地址或者取地址)、系統(tǒng)調(diào)用等
????????kernel_ventry???0,?irq??????????????????????????// el0?下的異步異常,硬件中斷。0代表異常等級
????????kernel_ventry???0,?fiq_invalid??????????????????//?FIQ?64-bit?EL0
????????kernel_ventry???0,?error????????????????????????//?Error?64-bit?EL0

????????......
#endif
SYM_CODE_END(vectors)

arm64 的異常向量表 vectors 中設(shè)置了各種異常的入口。kernel_ventry 展開后,可以看到有效的異常入口有兩個同步異常 el0_sync,el1_sync 和兩個異步異常 el0_irq,el1_irq,其他異常入口暫時都 invalid。中斷屬于異步異常。

通過上圖,我們可以看出中斷的處理分為三個部分,保護(hù)現(xiàn)場,中斷處理,恢復(fù)現(xiàn)場。其中 el0_irq 和 el1_irq 的具體實現(xiàn)略有不同,但處理流程大致是相同的。接下來我們以 el0_irq 為例對上面三個步驟進(jìn)行梳理。

保護(hù)現(xiàn)場

kernel_entry 0,其中 kernel_entry 是一個宏,此宏會將 CPU 寄存器按照 pt_regs 結(jié)構(gòu)體的定義將第一現(xiàn)場保存到棧上。

.macro??kernel_entry,?el,?regsize?=?64
.if?????regsize?==?32
mov?????w0,?w0??????????????????????????//?zero?upper?32?bits?of?x0
.endif
stp?????x0,?x1,?[sp,?#16?*?0]
stp?????x2,?x3,?[sp,?#16?*?1]
stp?????x4,?x5,?[sp,?#16?*?2]
stp?????x6,?x7,?[sp,?#16?*?3]
stp?????x8,?x9,?[sp,?#16?*?4]
stp?????x10,?x11,?[sp,?#16?*?5]
stp?????x12,?x13,?[sp,?#16?*?6]
stp?????x14,?x15,?[sp,?#16?*?7]
stp?????x16,?x17,?[sp,?#16?*?8]
stp?????x18,?x19,?[sp,?#16?*?9]
stp?????x20,?x21,?[sp,?#16?*?10]
stp?????x22,?x23,?[sp,?#16?*?11]
stp?????x24,?x25,?[sp,?#16?*?12]
stp?????x26,?x27,?[sp,?#16?*?13]
stp?????x28,?x29,?[sp,?#16?*?14]

.if?????el?==?0
clear_gp_regs
mrs?????x21,?sp_el0
ldr_this_cpu????tsk,?__entry_task,?x20
msr?????sp_el0,?tsk

enable_da_f 是關(guān)閉中斷。

/*?IRQ?is?the?lowest?priority?flag,?unconditionally?unmask?the?rest.?*/
.macro?enable_da_f
msr?????daifclr,?#(8?|?4?|?1)
.endm

總之,保存現(xiàn)場主要是下面三個操作:

    保存 PSTATE 到 SPSR_ELx 寄存器將 PSTATE 中的 D A I F 全部屏蔽保存 PC 寄存器的值到 ELR_ELx 寄存器

中斷處理

保存過現(xiàn)場后,即將跳入中斷處理 irq_handler。

/*
?*?Interrupt?handling.
?*/
????????.macro??irq_handler
????????ldr_l???x1,?handle_arch_irq
????????mov?????x0,?sp
????????irq_stack_entry??????//進(jìn)入中斷棧
????????blr?????x1???????????//執(zhí)行?handle_arch_irq
????????irq_stack_exit???????//退出中斷棧
????????.endm

這里主要做了三個動作:

    進(jìn)入中斷棧執(zhí)行中斷控制器的 handle_arch_irq退出中斷棧

中斷棧用來保存中斷的上下文,中斷發(fā)生和退出的時候調(diào)用 irq_stack_entry 和 irq_stack_exit 來進(jìn)入和退出中斷棧。中斷棧是在內(nèi)核啟動時就創(chuàng)建好的,內(nèi)核在啟動過程中會去為每個 CPU 創(chuàng)建一個 per cpu 的中斷棧:start_kernel->init_IRQ->init_irq_stacks

那中斷控制器的 handle_arch_irq 又指向哪里呢?其實上面我們有講到,在內(nèi)核啟動過程中初始化中斷控制器時,設(shè)置了具體的 handler,gic_init_bases->set_handle_irq 將 handle_arch_irq 指針指向 gic_handle_irq 函數(shù)。代碼如下:

void?__init?set_handle_irq(void?(*handle_irq)(struct?pt_regs?*))
{
?if?(handle_arch_irq)
??return;

?handle_arch_irq?=?handle_irq;
}

static?int?__init?gic_init_bases(void?__iomem?*dist_base,
?????struct?redist_region?*rdist_regs,
?????u32?nr_redist_regions,
?????u64?redist_stride,
?????struct?fwnode_handle?*handle)
{
?set_handle_irq(gic_handle_irq);
}

所以,中斷處理最終會進(jìn)入 gic_handle_irq:

static?asmlinkage?void?__exception_irq_entry?gic_handle_irq(struct?pt_regs?*regs)
{
?u32?irqnr;

?do?{
??irqnr?=?gic_read_iar();?????????????????????????????????????------(1)

??if?(likely(irqnr?>?15?&&?irqnr?<?1020)?||?irqnr?>=?8192)?{??------(2)
???int?err;

???if?(static_key_true(&supports_deactivate))
????gic_write_eoir(irqnr);
???else
????isb();

???err?=?handle_domain_irq(gic_data.domain,?irqnr,?regs);????------(3)
???if?(err)?{
????WARN_ONCE(true,?"Unexpected?interrupt?received!n");
????if?(static_key_true(&supports_deactivate))?{
?????if?(irqnr?<?8192)
??????gic_write_dir(irqnr);
????}?else?{
?????gic_write_eoir(irqnr);
????}
???}
???continue;
??}
??if?(irqnr?<?16)?{??????????????????????????????????????????------(4)
???gic_write_eoir(irqnr);
???if?(static_key_true(&supports_deactivate))
????gic_write_dir(irqnr);
#ifdef?CONFIG_SMP
???/*
????*?Unlike?GICv2,?we?don't?need?an?smp_rmb()?here.
????*?The?control?dependency?from?gic_read_iar?to
????*?the?ISB?in?gic_write_eoir?is?enough?to?ensure
????*?that?any?shared?data?read?by?handle_IPI?will
????*?be?read?after?the?ACK.
????*/
???handle_IPI(irqnr,?regs);????????????????????????????????------(5)
#else
???WARN_ONCE(true,?"Unexpected?SGI?received!n");
#endif
???continue;
??}
?}?while?(irqnr?!=?ICC_IAR1_EL1_SPURIOUS);
}
    讀取中斷控制器的寄存器GICC_IAR,并獲取 hwirq外設(shè)觸發(fā)的中斷。硬件中斷號 0-15 表示 SGI 類型的中斷,15-1020 表示外設(shè)中斷(SPI或PPI類型),8192-MAX 表示 LPI 類型的中斷中斷控制器中斷處理的主體軟件觸發(fā)的中斷核間交互觸發(fā)的中斷

中斷控制器中斷處理的主體,如下所示:

int?__handle_domain_irq(struct?irq_domain?*domain,?unsigned?int?hwirq,
???bool?lookup,?struct?pt_regs?*regs)
{
?struct?pt_regs?*old_regs?=?set_irq_regs(regs);????????
?unsigned?int?irq?=?hwirq;
?int?ret?=?0;

?irq_enter();???????????????????????????????------(1)

#ifdef?CONFIG_IRQ_DOMAIN
?if?(lookup)
??irq?=?irq_find_mapping(domain,?hwirq);????------(2)
#endif

?/*
??*?Some?hardware?gives?randomly?wrong?interrupts.??Rather
??*?than?crashing,?do?something?sensible.
??*/
?if?(unlikely(!irq?||?irq?>=?nr_irqs))?{
??ack_bad_irq(irq);
??ret?=?-EINVAL;
?}?else?{
??generic_handle_irq(irq);??????????????????------(3)
?}

?irq_exit();????????????????????????????????------(4)
?set_irq_regs(old_regs);
?return?ret;
}
    進(jìn)入中斷上下文根據(jù) hwirq 去查找 linux 中斷號通過中斷號找到全局中斷描述符數(shù)組 irq_desc[NR_IRQS] 中的一項,然后調(diào)用 generic_handle_irq_desc,執(zhí)行該 irq 號注冊的 action退出中斷上下文
static?inline?void?generic_handle_irq_desc(struct?irq_desc?*desc)
{
?desc->handle_irq(desc);????????????
}

調(diào)用 desc->handle_irq 指向的回調(diào)函數(shù)

irq_domain_set_info 根據(jù)硬件中斷號的范圍設(shè)置 irq_desc->handle_irq 的指針,共享中斷入口為 handle_fasteoi_irq,私有中斷入口為 handle_percpu_devid_irq。如下所示:

handle_percpu_devid_irq:處理私有中斷處理,在這個過程中會分別調(diào)用中斷控制器的處理函數(shù)進(jìn)行硬件操作,該函數(shù)調(diào)用 action->handler() 來進(jìn)行中斷處理

handle_fasteoi_irq:處理共享中斷,并且遍歷 irqaction 鏈表,逐個調(diào)用 action->handler() 函數(shù),這個函數(shù)正是設(shè)備驅(qū)動程序調(diào)用 request_irq/request_threaded_irq 接口注冊的中斷處理函數(shù),此外如果中斷線程化處理的話,還會調(diào)用 __irq_wake_thread 喚醒內(nèi)核線程。

恢復(fù)現(xiàn)場

SYM_CODE_START_LOCAL(ret_to_user)
????????disable_daif??????????????????????//D?A?I?F?分別為PSTAT中的四個異常屏蔽標(biāo)志位,此處屏蔽這4中異常
????????gic_prio_kentry_setup?tmp=x3
#ifdef?CONFIG_TRACE_IRQFLAGS
????????bl??????trace_hardirqs_off
#endif
????????ldr?????x19,?[tsk,?#TSK_TI_FLAGS]??//獲取?thread_info?中的flags變量的值
????????and?????x2,?x19,?#_TIF_WORK_MASK
????????cbnz????x2,?work_pending
finish_ret_to_user:
????????user_enter_irqoff
????????/*?Ignore?asynchronous?tag?check?faults?in?the?uaccess?routines?*/
????????clear_mte_async_tcf
????????enable_step_tsk?x19,?x2
#ifdef?CONFIG_GCC_PLUGIN_STACKLEAK
????????bl??????stackleak_erase
#endif
????????kernel_exit?0??????????????????????//恢復(fù)?pt_regs?中的寄存器上下文

主要分三步:

    disable 中斷檢查在退出中斷前有沒有需要處理事情,如調(diào)度、信號處理等將之前壓棧的 pt_regs 彈出,恢復(fù)現(xiàn)場

總結(jié)

上面講了中斷控制器和設(shè)備驅(qū)動的初始化。包括從設(shè)備樹獲取中斷源信息的解析,硬件中斷號到 Linux 中斷號的映射關(guān)系,irq_desc 等各個結(jié)構(gòu)的分配及初始化、中斷的注冊等等,總而言之,就是完成靜態(tài)關(guān)系創(chuàng)建,為中斷處理做好準(zhǔn)備。

當(dāng)外設(shè)觸發(fā)中斷信號時,中斷控制器接收到信號并發(fā)送到處理器,此時處理器進(jìn)行異常模式切換,如果涉及到中斷線程化,則還需要進(jìn)行中斷內(nèi)核線程的喚醒操作,最終完成中斷處理函數(shù)的執(zhí)行。

最后,用一張圖來匯總中斷控制器和設(shè)備驅(qū)動的來龍去脈:

中斷下半部之 workqueue

workqueue 是除了 softirq 和 tasklet 以外最常用的下半部機(jī)制之一。workqueue 的本質(zhì)是把 work 交給一個內(nèi)核線程,在進(jìn)程上下文調(diào)度的時候執(zhí)行。因為這個特點,所以 workqueue 允許重新調(diào)度和睡眠,這種異步執(zhí)行的進(jìn)程上下文,能解決因為 softirq 和 tasklet 執(zhí)行時間長而導(dǎo)致的系統(tǒng)實時性下降等問題。

當(dāng)驅(qū)動程序在進(jìn)程上下文中有異步執(zhí)行的工作任務(wù)時,可以用 work 來描述工作任務(wù)。把 work 添加到一個鏈表 worklist 中,然后由一個內(nèi)核線程 worker 遍歷鏈表,串行地執(zhí)行掛入 worklist 中的所有 work。如果 worklist 中沒有 work,那么內(nèi)核線程 worker 就會變成 IDLE 狀態(tài);如果有 work,則執(zhí)行 work 中的回調(diào)函數(shù)。

workqueue 相關(guān)的數(shù)據(jù)結(jié)構(gòu)

關(guān)于 workqueue 中幾個概念都是 work 相關(guān)的數(shù)據(jù)結(jié)構(gòu)非常容易混淆,大概可以這樣來理解:

work_struct :

工作。初始化一個 work 并添加到工作隊列后,將會將其傳遞到合適的內(nèi)核線程來進(jìn)行處理,它是用于調(diào)度的最小單位。

struct?work_struct?{
?atomic_long_t?data;?????
?struct?list_head?entry;?
?work_func_t?func;???????
#ifdef?CONFIG_LOCKDEP
?struct?lockdep_map?lockdep_map;
#endif
};
    data:低比特存放狀態(tài)位,高比特存放 worker_pool 的ID或者 pool_workqueue 的指針entry:用于添加到其他隊列上func:工作任務(wù)的處理函數(shù),在內(nèi)核線程中回調(diào)

workqueue_struct :

工作的集合。workqueue 和 work 是一對多的關(guān)系。內(nèi)核中工作隊列分為兩種:

    bound:綁定處理器的工作隊列,每個 worker 創(chuàng)建的內(nèi)核線程綁定到特定的 CPU 上運(yùn)行。unbound:不綁定處理器的工作隊列,創(chuàng)建的時候需要指定 WQ_UNBOUND 標(biāo)志,內(nèi)核線程可以在處理器間遷移。
struct?workqueue_struct?{
?struct?list_head?pwqs;??/*?WR:?all?pwqs?of?this?wq?*/???
?struct?list_head?list;??/*?PR:?list?of?all?workqueues?*/??

?struct?list_head?maydays;?/*?MD:?pwqs?requesting?rescue?*/????
?struct?worker??*rescuer;?/*?I:?rescue?worker?*/??

?struct?pool_workqueue?*dfl_pwq;?/*?PW:?only?for?unbound?wqs?*/

?char???name[WQ_NAME_LEN];?/*?I:?workqueue?name?*/

?/*?hot?fields?used?during?command?issue,?aligned?to?cacheline?*/
?unsigned?int??flags?____cacheline_aligned;?/*?WQ:?WQ_*?flags?*/
?struct?pool_workqueue?__percpu?*cpu_pwqs;?/*?I:?per-cpu?pwqs?*/????
?struct?pool_workqueue?__rcu?*numa_pwq_tbl[];?/*?PWR:?unbound?pwqs?indexed?by?node?*/????//Per-Node創(chuàng)建pool_workqueue
????...
};
    pwqs:所有的 pool_workqueue 都添加到本鏈表中l(wèi)ist:用于將工作隊列添加到全局鏈表 workqueues 中maydays:rescue狀態(tài)下的 pool_workqueue 添加到本鏈表中rescuer:rescuer 內(nèi)核線程,用于處理內(nèi)存緊張時創(chuàng)建工作線程失敗的情況cpu_pwqs:Per-CPU 創(chuàng)建 pool_workqueuenuma_pwq_tbl[]:Per-Node 創(chuàng)建 pool_workqueue

pool_workqueue:

中間人 / 中介,負(fù)責(zé)建立起 workqueue 和 worker_pool 之間的關(guān)系。workqueue 和 pool_workqueue 是一對多的關(guān)系。

struct?pool_workqueue?{
?struct?worker_pool?*pool;??/*?I:?the?associated?pool?*/????
?struct?workqueue_struct?*wq;??/*?I:?the?owning?workqueue?*/???

?int???nr_active;?/*?L:?nr?of?active?works?*/????
?int???max_active;?/*?L:?max?active?works?*/???
?struct?list_head?delayed_works;?/*?L:?delayed?works?*/?????
?struct?list_head?pwqs_node;?/*?WR:?node?on?wq->pwqs?*/????
?struct?list_head?mayday_node;?/*?MD:?node?on?wq->maydays?*/???//用于添加到workqueue鏈表中
????...
}?__aligned(1?<<?WORK_STRUCT_FLAG_BITS);
    pool:指向 worker_poolwq:指向所屬的 workqueuenr_active:活躍的 work 數(shù)量max_active:活躍的最大 work 數(shù)量delayed_works:延遲執(zhí)行的 work 掛入本鏈表pwqs_node:用于添加到 workqueue 鏈表中mayday_node:用于添加到 workqueue 鏈表中

worker_pool:

工人的集合。pool_workqueue 和 worker_pool 是一對一的關(guān)系,worker_pool 和 worker 是一對多的關(guān)系。

    bound 類型的工作隊列:worker_pool 是 Per-CPU 創(chuàng)建,每個 CPU 都有兩個 worker_pool,對應(yīng)不同的優(yōu)先級,nice 值分別為 0 和 -20。unbound 類型的工作隊列:worker_pool 創(chuàng)建后會添加到 unbound_pool_hash 哈希表中。
struct?worker_pool?{
?spinlock_t??lock;??/*?the?pool?lock?*/
?int???cpu;??/*?I:?the?associated?cpu?*/?????
?int???node;??/*?I:?the?associated?node?ID?*/?
?int???id;??/*?I:?pool?ID?*/
?unsigned?int??flags;??/*?X:?flags?*/

?unsigned?long??watchdog_ts;?/*?L:?watchdog?timestamp?*/

?struct?list_head?worklist;?/*?L:?list?of?pending?works?*/??
?int???nr_workers;?/*?L:?total?number?of?workers?*/???

?/*?nr_idle?includes?the?ones?off?idle_list?for?rebinding?*/
?int???nr_idle;?/*?L:?currently?idle?ones?*/

?struct?list_head?idle_list;?/*?X:?list?of?idle?workers?*/??
?struct?timer_list?idle_timer;?/*?L:?worker?idle?timeout?*/
?struct?timer_list?mayday_timer;?/*?L:?SOS?timer?for?workers?*/

?/*?a?workers?is?either?on?busy_hash?or?idle_list,?or?the?manager?*/
?DECLARE_HASHTABLE(busy_hash,?BUSY_WORKER_HASH_ORDER);???/*?L:?hash?of?busy?workers?*/

?/*?see?manage_workers()?for?details?on?the?two?manager?mutexes?*/
?struct?worker??*manager;?/*?L:?purely?informational?*/
?struct?mutex??attach_mutex;?/*?attach/detach?exclusion?*/
?struct?list_head?workers;?/*?A:?attached?workers?*/???
?struct?completion?*detach_completion;?/*?all?workers?detached?*/

?struct?ida??worker_ida;?/*?worker?IDs?for?task?name?*/

?struct?workqueue_attrs?*attrs;??/*?I:?worker?attributes?*/
?struct?hlist_node?hash_node;?/*?PL:?unbound_pool_hash?node?*/????
????...
}?____cacheline_aligned_in_smp;
    cpu:綁定到 CPU 的 workqueue,代表 CPU IDnode:非綁定類型的 workqueue,代表內(nèi)存 Node IDworklist:pending 狀態(tài)的 work 添加到本鏈表nr_workers:worker 的數(shù)量idle_list:處于 IDLE 狀態(tài)的 worker 添加到本鏈表busy_hash:工作狀態(tài)的 worker 添加到本哈希表中workers:worker_pool 管理的 worker 添加到本鏈表中hash_node:用于添加到 unbound_pool_hash 中

worker :

工人。在代碼中 worker 對應(yīng)一個 work_thread() 內(nèi)核線程。

struct?worker?{
?/*?on?idle?list?while?idle,?on?busy?hash?table?while?busy?*/
?union?{
??struct?list_head?entry;?/*?L:?while?idle?*/?????
??struct?hlist_node?hentry;?/*?L:?while?busy?*/?
?};

?struct?work_struct?*current_work;?/*?L:?work?being?processed?*/??
?work_func_t??current_func;?/*?L:?current_work's?fn?*/????????????????
?struct?pool_workqueue?*current_pwq;?/*?L:?current_work's?pwq?*/???

?struct?list_head?scheduled;?/*?L:?scheduled?works?*/???
??
?/*?64?bytes?boundary?on?64bit,?32?on?32bit?*/

?struct?task_struct?*task;??/*?I:?worker?task?*/???
?struct?worker_pool?*pool;??/*?I:?the?associated?pool?*/???
??????/*?L:?for?rescuers?*/
?struct?list_head?node;??/*?A:?anchored?at?pool->workers?*/??//添加到worker_pool->workers鏈表中
??????/*?A:?runs?through?worker->node?*/
????...
};
    entry:用于添加到 worker_pool 的空閑鏈表中hentry:用于添加到 worker_pool 的忙碌列表中current_work:當(dāng)前正在處理的 workcurrent_func:當(dāng)前正在執(zhí)行的 work 回調(diào)函數(shù)current_pwq:指向當(dāng)前 work 所屬的 pool_workqueuescheduled:所有被調(diào)度執(zhí)行的 work 都將添加到該鏈表中task:指向內(nèi)核線程pool:該 worker 所屬的 worker_poolnode:添加到 worker_pool->workers 鏈表中

可以用下圖來總結(jié):

workqueue 的初始化

內(nèi)核在啟動的時候會對 workqueue 做初始化,workqueue 的初始化包含兩個階段,分別是 workqueue_init_early 和 workqueue_init。

workqueue_init_early

    分配 worker_pool,并且對該結(jié)構(gòu)中的字段進(jìn)行初始化操作分配 workqueue_struct,并且對該結(jié)構(gòu)中的字段進(jìn)行初始化操作alloc_and_link_pwqs:分配 pool_workqueue,將 workqueue_struct 和 worker_pool 關(guān)聯(lián)起來

workqueue_init

這里主要完成的工作是給之前創(chuàng)建好的 worker_pool,添加一個初始的 worker,然后利用函數(shù) create_worker,創(chuàng)建名字為 kworker/XX:YY 或者 kworker/uXX:YY 的內(nèi)核線程。其中 XX 表示 worker_pool 的編號,YY 表示 worker 的編號,u 表示unbound。

    分配 worker,并且對該結(jié)構(gòu)中的字段進(jìn)行初始化操作為 worker 創(chuàng)建內(nèi)核線程 worker_thread將 worker 添加到 worker_pool 中worker 進(jìn)入 IDLE 狀態(tài)

經(jīng)過上面兩個階段的初始化,workqueue 子系統(tǒng)基本就已經(jīng)將數(shù)據(jù)結(jié)構(gòu)的關(guān)聯(lián)建立好了,當(dāng)有 work 來進(jìn)行調(diào)度的時候,就可以進(jìn)行處理了。

使用 workqueue

內(nèi)核推薦驅(qū)動開發(fā)者使用默認(rèn)的 workqueue,而不是新建 workqueue。要使用系統(tǒng)默認(rèn)的 workqueue,首先需要初始化 work,內(nèi)核提供了相應(yīng)的宏 INIT_WORK。

初始化 work

#define?INIT_WORK(_work,?_func)??????
?__INIT_WORK((_work),?(_func),?0)
??
#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)

初始化 work 后,就可以調(diào)用 shedule_work 函數(shù)把 work 掛入系統(tǒng)默認(rèn)的 workqueue 中。

work 調(diào)度

    將 work 添加到系統(tǒng)的 system_wq 工作隊列中。判斷 workqueue 的類型,如果是 bound 類型,根據(jù) CPU 來獲取 pool_workqueue。如果是 unbound 類型,通過 node 號來獲取 pool_workqueue。判斷 pool_workqueue 活躍的 work 數(shù)量,少于最大限值則將 work 加入到 pool->worklist 中,否則加入到 pwq->delayed_works 鏈表中。如果 __need_more_worker 判斷沒有 worker 在執(zhí)行,則通過 wake_up_worker 喚醒 worker 內(nèi)核線程。

worker_thread

worker 內(nèi)核線程的執(zhí)行函數(shù)是 worker_thread。

    設(shè)置標(biāo)志位 PF_WQ_WORKER,調(diào)度器在進(jìn)行調(diào)度處理時會對 task 進(jìn)行判斷,針對 workerqueue worker 有特殊的處理。worker 被喚醒的時候,跳轉(zhuǎn)到 woke_up 執(zhí)行。woke_up 中,如果此時 worker 是需要銷毀的,那就進(jìn)行清理工作并返回。否則,離開 IDLE 狀態(tài),并進(jìn)入 recheck 模塊執(zhí)行。recheck 中,判斷是否需要更多的 worker 來處理,如果沒有任務(wù)處理,跳轉(zhuǎn)到 sleep 地方進(jìn)行睡眠。如果有任務(wù)需要處理時,遍歷工作鏈表,對鏈表中的每個節(jié)點調(diào)用 process_one_work 來執(zhí)行 work 的回調(diào)函數(shù),即 INIT_WORK 里的回調(diào)函數(shù)。

    sleep 中,沒有任務(wù)處理時,worker 進(jìn)入空閑狀態(tài),并將當(dāng)前的內(nèi)核線程設(shè)置成睡眠狀態(tài),讓出 CPU。

總結(jié)

相關(guān)推薦

電子產(chǎn)業(yè)圖譜

針對嵌入式人工智能,物聯(lián)網(wǎng)等專業(yè)技術(shù)分享和交流平臺,內(nèi)容涉及arm,linux,android等各方面。

Arm64 棧回溯
  • Linux BSP實戰(zhàn)課(中斷篇):中斷控制器的驅(qū)動實現(xiàn)
  • 查看更多