Linux 中斷管理機(jī)制
GIC 硬件原理
GIC,Generic Interrupt Controller。是ARM公司提供的一個(gè)通用的中斷控制器。主要作用為:接受硬件中斷信號(hào),并經(jīng)過(guò)一定處理后,分發(fā)給對(duì)應(yīng)的CPU進(jìn)行處理。
當(dāng)前GIC 有四個(gè)版本,GIC v1~v4, 本文主要介紹GIC v3控制器。
GIC v3中斷類別
GICv3定義了以下中斷類型:
- SGI (Software Generated Interrupt):軟件觸發(fā)的中斷。軟件可以通過(guò)寫 GICD_SGIR 寄存器來(lái)觸發(fā)一個(gè)中斷事件,一般用于核間通信,內(nèi)核中的 IPI:inter-processor interrupts 就是基于 SGI。
- PPI (Private Peripheral Interrupt):私有外設(shè)中斷。這是每個(gè)核心私有的中斷。PPI會(huì)送達(dá)到指定的CPU上,應(yīng)用場(chǎng)景有CPU本地時(shí)鐘。
- SPI (Shared Peripheral Interrupt):公用的外部設(shè)備中斷,也定義為共享中斷。中斷產(chǎn)生后,可以分發(fā)到某一個(gè)CPU上。比如按鍵觸發(fā)一個(gè)中斷,手機(jī)觸摸屏觸發(fā)的中斷。
- LPI (Locality-specific Peripheral Interrupt):LPI 是 GICv3 中的新特性,它們?cè)诤芏喾矫媾c其他類型的中斷不同。LPI 始終是基于消息的中斷,它們的配置保存在表中而不是寄存器。比如 PCIe 的 MSI/MSI-x 中斷。
中斷類型 | 硬件中斷號(hào) |
---|---|
SGI | 0-15 |
PPI | 16-31 |
SPI | 32-1019 |
reserved | ...... |
LPI | 8192-MAX |
GIC v3 組成
GICv3 控制器由以下三部分組成:
- Distributor:SPI 中斷的管理,將中斷發(fā)送給 Redistributor
- 打開或關(guān)閉每個(gè)中斷。Distributor對(duì)中斷的控制分成兩個(gè)級(jí)別。一個(gè)是全局中斷的控制(GIC_DIST_CTRL)。一旦關(guān)閉了全局的中斷,那么任何的中斷源產(chǎn)生的中斷事件都不會(huì)被傳遞到 CPU interface。另外一個(gè)級(jí)別是對(duì)針對(duì)各個(gè)中斷源進(jìn)行控制(GIC_DIST_ENABLE_CLEAR),關(guān)閉某一個(gè)中斷源會(huì)導(dǎo)致該中斷事件不會(huì)分發(fā)到 CPU interface,但不影響其他中斷源產(chǎn)生中斷事件的分發(fā)??刂茖?dāng)前優(yōu)先級(jí)最高的中斷事件分發(fā)到一個(gè)或者一組 CPU interface。當(dāng)一個(gè)中斷事件分發(fā)到多個(gè) CPU interface 的時(shí)候,GIC 的內(nèi)部邏輯應(yīng)該保證只 assert 一個(gè)CPU。優(yōu)先級(jí)控制。interrupt屬性設(shè)定。設(shè)置每個(gè)外設(shè)中斷的觸發(fā)方式:電平觸發(fā)、邊緣觸發(fā);interrupt group的設(shè)定。設(shè)置每個(gè)中斷的 Group,其中 Group0 用于安全中斷,支持 FIQ 和 IRQ,Group1 用于非安全中斷,只支持 IRQ;
- Redistributor:SGI,PPI,LPI 中斷的管理,將中斷發(fā)送給 CPU interface
- 啟用和禁用 SGI 和 PPI。設(shè)置 SGI 和 PPI 的優(yōu)先級(jí)。將每個(gè) PPI 設(shè)置為電平觸發(fā)或邊緣觸發(fā)。將每個(gè) 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 中斷事件。對(duì)于 ARM,CPU interface 和 CPU 之間的中斷信號(hào)線是 nIRQCPU 和 nFIQCPU。如果關(guān)閉了中斷,即便是 Distributor 分發(fā)了一個(gè)中斷事件到 CPU interface,也不會(huì) assert 指定的 nIRQ 或者 nFIQ 通知 Core。中斷的確認(rèn)。Core 會(huì)向 CPU interface 應(yīng)答中斷(應(yīng)答當(dāng)前優(yōu)先級(jí)最高的那個(gè)中斷),中斷一旦被應(yīng)答,Distributor 就會(huì)把該中斷的狀態(tài)從 pending 修改成 active 或者 pending and active(這是和該中斷源的信號(hào)有關(guān),例如如果是電平中斷并且保持了該 asserted 電平,那么就是 pending and active)。ack 了中斷之后,CPU interface 就會(huì) deassert nIRQCPU 和 nFIQCPU 信號(hào)線。中斷處理完畢的通知。當(dāng) interrupt handler 處理完了一個(gè)中斷的時(shí)候,會(huì)向?qū)?CPU interface 的寄存器通知 GIC CPU 已經(jīng)處理完該中斷。做這個(gè)動(dòng)作一方面是通知 Distributor 將中斷狀態(tài)修改為 deactive,另外一方面,CPU interface 會(huì) priority drop,從而允許其他的 pending 的中斷向 CPU 提交。為 CPU 設(shè)置中斷優(yōu)先級(jí)掩碼。通過(guò) priority mask,可以 mask 掉一些優(yōu)先級(jí)比較低的中斷,這些中斷不會(huì)通知到 CPU。設(shè)置 CPU 的中斷搶占(preemption)策略。在多個(gè)中斷事件同時(shí)到來(lái)的時(shí)候,選擇一個(gè)優(yōu)先級(jí)最高的通知 CPU。
GICv3 控制器內(nèi)部模塊和各中斷類型的關(guān)系如下圖所示:
中斷路由
GICv3 使用 hierarchy 來(lái)標(biāo)識(shí)一個(gè)具體的 core, 如下圖是一個(gè)四層的結(jié)構(gòu)(aarch64):
用 <affinity level 3>.<affinity level 2>.<affinity level 1>.<affinity level 0> 的形式組成一個(gè) PE 的路由。每一個(gè) core 的 affnity 值可以通過(guò) MPDIR_EL1 寄存器獲取, 每一個(gè) affinity 占用8bit。配置對(duì)應(yīng) core 的 MPIDR 值,可以將中斷路由到該 core 上。
各個(gè) affinity 的定義是根據(jù) SOC 自己的定義,比如:
<group?of?groups>.?<group?of?processors>.<processor>.<core>
<group?of?processors>.<processor>.<core>.<thread>
中斷親和性的設(shè)置的通用函數(shù)為 irq_set_affinity,后面會(huì)做詳細(xì)介紹。
中斷狀態(tài)機(jī)
中斷處理的狀態(tài)機(jī)如下圖:
- Inactive:無(wú)中斷狀態(tài),即沒(méi)有 Pending 也沒(méi)有 Active。Pending:硬件或軟件觸發(fā)了中斷,該中斷事件已經(jīng)通過(guò)硬件信號(hào)通知到 GIC,等待 GIC 分配的那個(gè) CPU 進(jìn)行處理,在電平觸發(fā)模式下,產(chǎn)生中斷的同時(shí)保持 Pending 狀態(tài)。Active:CPU 已經(jīng)應(yīng)答(acknowledge)了該中斷請(qǐng)求,并且正在處理中。Active and pending:當(dāng)一個(gè)中斷源處于 Active 狀態(tài)的時(shí)候,同一中斷源又觸發(fā)了中斷,進(jìn)入 pending 狀態(tài)。
中斷處理流程
- 外設(shè)發(fā)起中斷,發(fā)送給 DistributorDistributor 將該中斷,分發(fā)給合適的 RedistributorRedistributor 將中斷信息,發(fā)送給 CPU interfaceCPU interface 產(chǎn)生合適的中斷異常給處理器處理器接收該異常,并且軟件處理該中斷
GIC 驅(qū)動(dòng)
這里主要分析 linux kernel 中 GIC v3 中斷控制器的代碼(drivers/irqchip/irq-gic-v3.c)。
設(shè)備樹
先來(lái)看下一個(gè)中斷控制器的設(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ū)動(dòng)reg :GIC的物理基地址,分別對(duì)應(yīng)GICD,GICR,GICC…#interrupt-cells:這是一個(gè)中斷控制器節(jié)點(diǎn)的屬性。它聲明了該中斷控制器的中斷指示符(interrupts)中 cell 的個(gè)數(shù)interrupt-controller: 表示該節(jié)點(diǎn)是一個(gè)中斷控制器interrupts:分別代表中斷類型,中斷號(hào),中斷類型, PPI中斷親和, 保留字段
關(guān)于設(shè)備數(shù)的各個(gè)字段含義,詳細(xì)可以參考 Documentation/devicetree/bindings 下的對(duì)應(yīng)信息。
初始化
1. irq chip driver 的聲明:
IRQCHIP_DECLARE(gic_v3,?"arm,gic-v3",?gic_of_init);
定義 IRQCHIP_DECLARE 之后,相應(yīng)的內(nèi)容會(huì)保存到 __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)核啟動(dòng)初始化中斷的函數(shù)中,of_irq_init 函數(shù)會(huì)去查找設(shè)備節(jié)點(diǎn)信息,該函數(shù)的傳入?yún)?shù)就是 __irqchip_of_table 段,由于 IRQCHIP_DECLARE 已經(jīng)將信息填充好了,of_irq_init 函數(shù)會(huì)根據(jù) “arm,gic-v3” 去查找對(duì)應(yīng)的設(shè)備節(jié)點(diǎn),并獲取設(shè)備的信息。or_irq_init 函數(shù)中,最終會(huì)回調(diào) IRQCHIP_DECLARE 聲明的回調(diào)函數(shù),也就是 gic_of_init,而這個(gè)函數(shù)就是 GIC 驅(qū)動(dòng)的初始化入口。
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 的寄存器地址空間。驗(yàn)證 GICD 的版本是 GICv3 還是 GICv4(主要通過(guò)讀GICD_PIDR2寄存器bit[7:4]. 0x1代表GICv1, 0x2代表GICv2…以此類推)。通過(guò) DTS 讀取 redistributor-regions 的值。為一個(gè) GICR 域分配基地址。通過(guò) 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 中斷號(hào)最大的值為多少。向系統(tǒng)中注冊(cè)一個(gè) irq domain 的數(shù)據(jù)結(jié)構(gòu),irq_domain 主要作用是將硬件中斷號(hào)映射到 irq number,后面會(huì)做詳細(xì)的介紹。設(shè)定 arch 相關(guān)的 irq handler。gic_irq_handle 是內(nèi)核 gic 中斷處理的入口函數(shù),后面會(huì)做詳細(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)只存在一個(gè)中斷控制器,而且中斷數(shù)目也不多的時(shí)候,一個(gè)很簡(jiǎn)單的做法就是一個(gè)中斷號(hào)對(duì)應(yīng)到中斷控制器的一個(gè)號(hào),可以說(shuō)是簡(jiǎn)單的線性映射:
但當(dāng)一個(gè)系統(tǒng)中有多個(gè)中斷控制器,而且中斷號(hào)也逐漸增加的時(shí)候。linux 內(nèi)核為了應(yīng)對(duì)此問(wèn)題,引入了 irq_domain 的概念。
irq_domain 的引入相當(dāng)于一個(gè)中斷控制器就是一個(gè) irq_domain。這樣一來(lái)所有的中斷控制器就會(huì)出現(xiàn)級(jí)聯(lián)的布局。利用樹狀的結(jié)構(gòu)可以充分的利用 irq 數(shù)目,而且每一個(gè) irq_domain 區(qū)域可以自己去管理自己 interrupt 的特性。
每一個(gè)中斷控制器對(duì)應(yīng)多個(gè)中斷號(hào), 而硬件中斷號(hào)在不同的中斷控制器上是會(huì)重復(fù)編碼的, 這時(shí)僅僅用硬中斷號(hào)已經(jīng)不能唯一標(biāo)識(shí)一個(gè)外設(shè)中斷,因此 linux kernel 提供了一個(gè)虛擬中斷號(hào)的概念。
接下來(lái)我們看下硬件中斷號(hào)是如何映射到虛擬中斷號(hào)的。
數(shù)據(jù)結(jié)構(gòu)
在看硬件中斷號(hào)映射到虛擬中斷號(hào)之前,先來(lái)看下幾個(gè)比較重要的數(shù)據(jù)結(jié)構(gòu)。
struct irq_desc 描述一個(gè)外設(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ū)動(dòng)的處理函數(shù),指向一個(gè) struct irqaction 的鏈表,一個(gè)中斷源可以多個(gè)設(shè)備共享,所以一個(gè) irq_desc 可以掛載多個(gè) action,由鏈表結(jié)構(gòu)組織起來(lái)action:設(shè)備驅(qū)動(dòng)的處理函數(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:虛擬中斷號(hào)hwirq:硬件中斷號(hào)chip:對(duì)應(yīng)的 irq_chip 數(shù)據(jù)結(jié)構(gòu)domain:對(duì)應(yīng)的 irq_domain 數(shù)據(jù)結(jié)構(gòu)
struct irq_chip 用于對(duì)中斷控制器的硬件操作。
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:?jiǎn)?dòng)中斷,如果設(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 與中斷控制器對(duì)應(yīng),完成硬件中斷號(hào) 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:對(duì)應(yīng)中斷控制器的 device nodeparent:指向父級(jí) irq_domain 的指針,用于支持級(jí)聯(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:用于硬件中斷號(hào)與 Linux 中斷號(hào)的映射xlate:通過(guò) device_node,解析硬件中斷號(hào)和觸發(fā)方式
struct irqaction 主要是用來(lái)存設(shè)備驅(qū)動(dòng)注冊(cè)的中斷處理函數(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ū)動(dòng)里的中斷處理函數(shù)dev_id:設(shè)備 idirq:中斷號(hào)flags:中斷標(biāo)志,注冊(cè)時(shí)設(shè)置,比如上升沿中斷,下降沿中斷等name:中斷名稱,產(chǎn)生中斷的硬件的名字dir:指向 /proc/irq/ 相關(guān)的信息
這里,我們用一張圖來(lái)匯總下上面的數(shù)據(jù)結(jié)構(gòu):
上面的結(jié)構(gòu)體 struct irq_desc 是設(shè)備驅(qū)動(dòng)加載的過(guò)程中完成的,讓設(shè)備樹中的中斷能與具體的中斷描述符 irq_desc 匹配,其中 struct irqaction 保存著設(shè)備的中斷處理函數(shù)。右邊框內(nèi)的結(jié)構(gòu)體主要是在中斷控制器驅(qū)動(dòng)加載的過(guò)程中完成的,其中 struct irq_chip 用于對(duì)中斷控制器的硬件操作,struct irq_domain 用于硬件中斷號(hào)到 Linux irq 的映射。
下面我們結(jié)合代碼看下中斷控制器驅(qū)動(dòng)和設(shè)備驅(qū)動(dòng)是如何創(chuàng)建這些結(jié)構(gòu)體,并且硬中斷和虛擬中斷號(hào)是如何完成映射的。
中斷控制器注冊(cè) irq_domain
通過(guò) __irq_domain_add 初始化 irq_domain 數(shù)據(jù)結(jié)構(gòu),然后把 irq_domain 添加到全局的鏈表 irq_domain_list 中。
外設(shè)的驅(qū)動(dòng)創(chuàng)建硬中斷和虛擬中斷號(hào)的映射關(guān)系
設(shè)備的驅(qū)動(dòng)在初始化的時(shí)候可以調(diào)用 irq_of_parse_and_map 這個(gè)接口函數(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 解析出中斷信息,比如硬件中斷號(hào) hwirq,中斷觸發(fā)方式irq_domain_alloc_descs 分配一個(gè)虛擬的中斷號(hào) virq,分配和初始化中斷描述符 irq_descgic_irq_domain_alloc 為 hwirq 和 virq 創(chuàng)建映射關(guān)系。內(nèi)部會(huì)通過(guò) irq_domain_set_info 調(diào)用 irq_domain_set_hwirq_and_chip,然后通過(guò) virq 獲取 irq_data 結(jié)構(gòu)體,并將 hwirq 設(shè)置到 irq_data->hwirq 中, 最終完成 hwirq 到 virq 的映射irq_domain_set_info 根據(jù)硬件中斷號(hào)的范圍設(shè)置 irq_desc->handle_irq 的指針,共享中斷入口為 handle_fasteoi_irq,私有中斷入口為 handle_percpu_devid_irq
最后,我們可以通過(guò) /proc/interrupts 下的值來(lái)看下它們的關(guān)系:
現(xiàn)在,我們已經(jīng)知道內(nèi)核為硬件中斷號(hào)與 Linux 中斷號(hào)做了映射,相關(guān)數(shù)據(jù)結(jié)構(gòu)的綁定及初始化,并且設(shè)置了中斷處理函數(shù)執(zhí)行的入口。接下來(lái)我們?cè)倏聪略O(shè)備的中斷是怎么來(lái)注冊(cè)的?
中斷的注冊(cè)
設(shè)備驅(qū)動(dòng)中,獲取到了 irq 中斷號(hào)后,通常就會(huì)采用 request_irq/request_threaded_irq 來(lái)注冊(cè)中斷,其中 request_irq 用于注冊(cè)普通處理的中斷。request_threaded_irq 用于注冊(cè)線程化處理的中斷,線程化中斷的主要目的把中斷上下文的任務(wù)遷移到線程中,減少系統(tǒng)關(guān)中斷的時(shí)間,增強(qiáng)系統(tǒng)的實(shí)時(shí)性。
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 中斷號(hào),handler 是中斷處理函數(shù),flags 是中斷標(biāo)志位,name 是中斷的名字。在講具體的注冊(cè)流程前,先看一下主要的中斷標(biāo)志位:
#define?IRQF_SHARED??0x00000080??????????????//多個(gè)設(shè)備共享一個(gè)中斷號(hào),需要外設(shè)硬件支持
#define?IRQF_PROBE_SHARED?0x00000100??????????????//中斷處理程序允許sharing?mismatch發(fā)生
#define?__IRQF_TIMER??0x00000200???????????????//時(shí)鐘中斷
#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)喚醒過(guò)程中必須強(qiáng)制打開該中斷
#define?IRQF_NO_THREAD??0x00010000??????//禁止中斷線程化
#define?IRQF_EARLY_RESUME?0x00020000??????//系統(tǒng)喚醒過(guò)程中在syscore階段resume,而不用等到設(shè)備resume階段
#define?IRQF_COND_SUSPEND?0x00040000??????//與NO_SUSPEND的用戶共享中斷時(shí),執(zhí)行本設(shè)備的中斷處理函數(shù)
創(chuàng)建完成后,通過(guò) ps 命令可以查看系統(tǒng)中的中斷線程,注意這些線程是實(shí)時(shí)線程 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)完成中斷的注冊(cè)后,所有結(jié)構(gòu)的組織關(guān)系都已經(jīng)建立好,剩下的工作就是當(dāng)信號(hào)來(lái)臨時(shí),進(jìn)行中斷的處理工作。這里我們站在前面知識(shí)點(diǎn)的基礎(chǔ)上,把中斷觸發(fā),中斷處理等整個(gè)流程走一遍。
假設(shè)當(dāng)前在 EL0 運(yùn)行一個(gè)應(yīng)用程序,觸發(fā)了一個(gè) EL0 的 irq 中斷,則處理器會(huì)做如下的操作:
先會(huì)跳到 arm64 對(duì)應(yīng)的異常向量表:
/*
?*?Exception?vectors.
?*/
????????.pushsection?".entry.text",?"ax"
????????.align??11
SYM_CODE_START(vectors)
????????......
????????
????????kernel_ventry???1,?sync?????????????????????????//?el1?下的同步異常,例如指令執(zhí)行異常、缺頁(yè)中斷等
????????kernel_ventry???1,?irq??????????????????????????// el1 下的異步異常,硬件中斷。1代表異常等級(jí)
????????kernel_ventry???1,?fiq_invalid??????????????????//?FIQ?EL1h
????????kernel_ventry???1,?error????????????????????????//?Error?EL1h
????????kernel_ventry???0,?sync?????????????????????????//?el0?下的同步異常,例如指令執(zhí)行異常、缺頁(yè)中斷(跳轉(zhuǎn)地址或者取地址)、系統(tǒng)調(diào)用等
????????kernel_ventry???0,?irq??????????????????????????// el0?下的異步異常,硬件中斷。0代表異常等級(jí)
????????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 展開后,可以看到有效的異常入口有兩個(gè)同步異常 el0_sync,el1_sync 和兩個(gè)異步異常 el0_irq,el1_irq,其他異常入口暫時(shí)都 invalid。中斷屬于異步異常。
通過(guò)上圖,我們可以看出中斷的處理分為三個(gè)部分,保護(hù)現(xiàn)場(chǎng),中斷處理,恢復(fù)現(xiàn)場(chǎng)。其中 el0_irq 和 el1_irq 的具體實(shí)現(xiàn)略有不同,但處理流程大致是相同的。接下來(lái)我們以 el0_irq 為例對(duì)上面三個(gè)步驟進(jìn)行梳理。
保護(hù)現(xiàn)場(chǎng)
kernel_entry 0,其中 kernel_entry 是一個(gè)宏,此宏會(huì)將 CPU 寄存器按照 pt_regs 結(jié)構(gòu)體的定義將第一現(xiàn)場(chǎng)保存到棧上。
.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)場(chǎng)主要是下面三個(gè)操作:
- 保存 PSTATE 到 SPSR_ELx 寄存器將 PSTATE 中的 D A I F 全部屏蔽保存 PC 寄存器的值到 ELR_ELx 寄存器
中斷處理
保存過(guò)現(xiàn)場(chǎng)后,即將跳入中斷處理 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
這里主要做了三個(gè)動(dòng)作:
- 進(jìn)入中斷棧執(zhí)行中斷控制器的 handle_arch_irq退出中斷棧
中斷棧用來(lái)保存中斷的上下文,中斷發(fā)生和退出的時(shí)候調(diào)用 irq_stack_entry 和 irq_stack_exit 來(lái)進(jìn)入和退出中斷棧。中斷棧是在內(nèi)核啟動(dòng)時(shí)就創(chuàng)建好的,內(nèi)核在啟動(dòng)過(guò)程中會(huì)去為每個(gè) CPU 創(chuàng)建一個(gè) per cpu 的中斷棧:start_kernel->init_IRQ->init_irq_stacks
那中斷控制器的 handle_arch_irq 又指向哪里呢?其實(shí)上面我們有講到,在內(nèi)核啟動(dòng)過(guò)程中初始化中斷控制器時(shí),設(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);
}
所以,中斷處理最終會(huì)進(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ā)的中斷。硬件中斷號(hào) 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 中斷號(hào)通過(guò)中斷號(hào)找到全局中斷描述符數(shù)組 irq_desc[NR_IRQS] 中的一項(xiàng),然后調(diào)用 generic_handle_irq_desc,執(zhí)行該 irq 號(hào)注冊(cè)的 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ù)硬件中斷號(hào)的范圍設(shè)置 irq_desc->handle_irq 的指針,共享中斷入口為 handle_fasteoi_irq,私有中斷入口為 handle_percpu_devid_irq。如下所示:
handle_percpu_devid_irq:處理私有中斷處理,在這個(gè)過(guò)程中會(huì)分別調(diào)用中斷控制器的處理函數(shù)進(jìn)行硬件操作,該函數(shù)調(diào)用 action->handler() 來(lái)進(jìn)行中斷處理
handle_fasteoi_irq:處理共享中斷,并且遍歷 irqaction 鏈表,逐個(gè)調(diào)用 action->handler() 函數(shù),這個(gè)函數(shù)正是設(shè)備驅(qū)動(dòng)程序調(diào)用 request_irq/request_threaded_irq 接口注冊(cè)的中斷處理函數(shù),此外如果中斷線程化處理的話,還會(huì)調(diào)用 __irq_wake_thread 喚醒內(nèi)核線程。
恢復(fù)現(xiàn)場(chǎng)
SYM_CODE_START_LOCAL(ret_to_user)
????????disable_daif??????????????????????//D?A?I?F?分別為PSTAT中的四個(gè)異常屏蔽標(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 中斷檢查在退出中斷前有沒(méi)有需要處理事情,如調(diào)度、信號(hào)處理等將之前壓棧的 pt_regs 彈出,恢復(fù)現(xiàn)場(chǎng)
總結(jié)
上面講了中斷控制器和設(shè)備驅(qū)動(dòng)的初始化。包括從設(shè)備樹獲取中斷源信息的解析,硬件中斷號(hào)到 Linux 中斷號(hào)的映射關(guān)系,irq_desc 等各個(gè)結(jié)構(gòu)的分配及初始化、中斷的注冊(cè)等等,總而言之,就是完成靜態(tài)關(guān)系創(chuàng)建,為中斷處理做好準(zhǔn)備。
當(dāng)外設(shè)觸發(fā)中斷信號(hào)時(shí),中斷控制器接收到信號(hào)并發(fā)送到處理器,此時(shí)處理器進(jìn)行異常模式切換,如果涉及到中斷線程化,則還需要進(jìn)行中斷內(nèi)核線程的喚醒操作,最終完成中斷處理函數(shù)的執(zhí)行。
最后,用一張圖來(lái)匯總中斷控制器和設(shè)備驅(qū)動(dòng)的來(lái)龍去脈:
中斷下半部之 workqueue
workqueue 是除了 softirq 和 tasklet 以外最常用的下半部機(jī)制之一。workqueue 的本質(zhì)是把 work 交給一個(gè)內(nèi)核線程,在進(jìn)程上下文調(diào)度的時(shí)候執(zhí)行。因?yàn)檫@個(gè)特點(diǎn),所以 workqueue 允許重新調(diào)度和睡眠,這種異步執(zhí)行的進(jìn)程上下文,能解決因?yàn)?softirq 和 tasklet 執(zhí)行時(shí)間長(zhǎng)而導(dǎo)致的系統(tǒng)實(shí)時(shí)性下降等問(wèn)題。
當(dāng)驅(qū)動(dòng)程序在進(jìn)程上下文中有異步執(zhí)行的工作任務(wù)時(shí),可以用 work 來(lái)描述工作任務(wù)。把 work 添加到一個(gè)鏈表 worklist 中,然后由一個(gè)內(nèi)核線程 worker 遍歷鏈表,串行地執(zhí)行掛入 worklist 中的所有 work。如果 worklist 中沒(méi)有 work,那么內(nèi)核線程 worker 就會(huì)變成 IDLE 狀態(tài);如果有 work,則執(zhí)行 work 中的回調(diào)函數(shù)。
workqueue 相關(guān)的數(shù)據(jù)結(jié)構(gòu)
關(guān)于 workqueue 中幾個(gè)概念都是 work 相關(guān)的數(shù)據(jù)結(jié)構(gòu)非常容易混淆,大概可以這樣來(lái)理解:
work_struct :
工作。初始化一個(gè) work 并添加到工作隊(duì)列后,將會(huì)將其傳遞到合適的內(nèi)核線程來(lá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:用于添加到其他隊(duì)列上func:工作任務(wù)的處理函數(shù),在內(nèi)核線程中回調(diào)
workqueue_struct :
工作的集合。workqueue 和 work 是一對(duì)多的關(guān)系。內(nèi)核中工作隊(duì)列分為兩種:
- bound:綁定處理器的工作隊(duì)列,每個(gè) worker 創(chuàng)建的內(nèi)核線程綁定到特定的 CPU 上運(yùn)行。unbound:不綁定處理器的工作隊(duì)列,創(chuàng)建的時(shí)候需要指定 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:用于將工作隊(duì)列添加到全局鏈表 workqueues 中maydays:rescue狀態(tài)下的 pool_workqueue 添加到本鏈表中rescuer:rescuer 內(nèi)核線程,用于處理內(nèi)存緊張時(shí)創(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 是一對(duì)多的關(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 是一對(duì)一的關(guān)系,worker_pool 和 worker 是一對(duì)多的關(guān)系。
- bound 類型的工作隊(duì)列:worker_pool 是 Per-CPU 創(chuàng)建,每個(gè) CPU 都有兩個(gè) worker_pool,對(duì)應(yīng)不同的優(yōu)先級(jí),nice 值分別為 0 和 -20。unbound 類型的工作隊(duì)列:worker_pool 創(chuàng)建后會(huì)添加到 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 對(duì)應(yīng)一個(gè) 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 鏈表中
可以用下圖來(lái)總結(jié):
workqueue 的初始化
內(nèi)核在啟動(dòng)的時(shí)候會(huì)對(duì) workqueue 做初始化,workqueue 的初始化包含兩個(gè)階段,分別是 workqueue_init_early 和 workqueue_init。
workqueue_init_early
- 分配 worker_pool,并且對(duì)該結(jié)構(gòu)中的字段進(jìn)行初始化操作分配 workqueue_struct,并且對(duì)該結(jié)構(gòu)中的字段進(jìn)行初始化操作alloc_and_link_pwqs:分配 pool_workqueue,將 workqueue_struct 和 worker_pool 關(guān)聯(lián)起來(lái)
workqueue_init
這里主要完成的工作是給之前創(chuàng)建好的 worker_pool,添加一個(gè)初始的 worker,然后利用函數(shù) create_worker,創(chuàng)建名字為 kworker/XX:YY 或者 kworker/uXX:YY 的內(nèi)核線程。其中 XX 表示 worker_pool 的編號(hào),YY 表示 worker 的編號(hào),u 表示unbound。
- 分配 worker,并且對(duì)該結(jié)構(gòu)中的字段進(jìn)行初始化操作為 worker 創(chuàng)建內(nèi)核線程 worker_thread將 worker 添加到 worker_pool 中worker 進(jìn)入 IDLE 狀態(tài)
經(jīng)過(guò)上面兩個(gè)階段的初始化,workqueue 子系統(tǒng)基本就已經(jīng)將數(shù)據(jù)結(jié)構(gòu)的關(guān)聯(lián)建立好了,當(dāng)有 work 來(lái)進(jìn)行調(diào)度的時(shí)候,就可以進(jìn)行處理了。
使用 workqueue
內(nèi)核推薦驅(qū)動(dòng)開發(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 工作隊(duì)列中。判斷 workqueue 的類型,如果是 bound 類型,根據(jù) CPU 來(lái)獲取 pool_workqueue。如果是 unbound 類型,通過(guò) node 號(hào)來(lái)獲取 pool_workqueue。判斷 pool_workqueue 活躍的 work 數(shù)量,少于最大限值則將 work 加入到 pool->worklist 中,否則加入到 pwq->delayed_works 鏈表中。如果 __need_more_worker 判斷沒(méi)有 worker 在執(zhí)行,則通過(guò) 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)度處理時(shí)會(huì)對(duì) task 進(jìn)行判斷,針對(duì) workerqueue worker 有特殊的處理。worker 被喚醒的時(shí)候,跳轉(zhuǎn)到 woke_up 執(zhí)行。woke_up 中,如果此時(shí) worker 是需要銷毀的,那就進(jìn)行清理工作并返回。否則,離開 IDLE 狀態(tài),并進(jìn)入 recheck 模塊執(zhí)行。recheck 中,判斷是否需要更多的 worker 來(lái)處理,如果沒(méi)有任務(wù)處理,跳轉(zhuǎn)到 sleep 地方進(jìn)行睡眠。如果有任務(wù)需要處理時(shí),遍歷工作鏈表,對(duì)鏈表中的每個(gè)節(jié)點(diǎn)調(diào)用 process_one_work 來(lái)執(zhí)行 work 的回調(diào)函數(shù),即 INIT_WORK 里的回調(diào)函數(shù)。
- sleep 中,沒(méi)有任務(wù)處理時(shí),worker 進(jìn)入空閑狀態(tài),并將當(dāng)前的內(nèi)核線程設(shè)置成睡眠狀態(tài),讓出 CPU。