• 正文
    • Linux 中斷管理機制
    • 中斷下半部之 workqueue
  • 相關推薦
申請入駐 產業(yè)圖譜

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

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

Linux 中斷管理機制

GIC 硬件原理

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

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

GIC v3中斷類別

GICv3定義了以下中斷類型:

    SGI (Software Generated Interrupt):軟件觸發(fā)的中斷。軟件可以通過寫 GICD_SGIR 寄存器來觸發(fā)一個中斷事件,一般用于核間通信,內核中的 IPI:inter-processor interrupts 就是基于 SGI。
    PPI (Private Peripheral Interrupt):私有外設中斷。這是每個核心私有的中斷。PPI會送達到指定的CPU上,應用場景有CPU本地時鐘。
    SPI (Shared Peripheral Interrupt):公用的外部設備中斷,也定義為共享中斷。中斷產生后,可以分發(fā)到某一個CPU上。比如按鍵觸發(fā)一個中斷,手機觸摸屏觸發(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
    打開或關閉每個中斷。Distributor對中斷的控制分成兩個級別。一個是全局中斷的控制(GIC_DIST_CTRL)。一旦關閉了全局的中斷,那么任何的中斷源產生的中斷事件都不會被傳遞到 CPU interface。另外一個級別是對針對各個中斷源進行控制(GIC_DIST_ENABLE_CLEAR),關閉某一個中斷源會導致該中斷事件不會分發(fā)到 CPU interface,但不影響其他中斷源產生中斷事件的分發(fā)??刂茖斍皟?yōu)先級最高的中斷事件分發(fā)到一個或者一組 CPU interface。當一個中斷事件分發(fā)到多個 CPU interface 的時候,GIC 的內部邏輯應該保證只 assert 一個CPU。優(yōu)先級控制。interrupt屬性設定。設置每個外設中斷的觸發(fā)方式:電平觸發(fā)、邊緣觸發(fā);interrupt group的設定。設置每個中斷的 Group,其中 Group0 用于安全中斷,支持 FIQ 和 IRQ,Group1 用于非安全中斷,只支持 IRQ;
    Redistributor:SGI,PPI,LPI 中斷的管理,將中斷發(fā)送給 CPU interface
    啟用和禁用 SGI 和 PPI。設置 SGI 和 PPI 的優(yōu)先級。將每個 PPI 設置為電平觸發(fā)或邊緣觸發(fā)。將每個 SGI 和 PPI 分配給中斷組??刂?SGI 和 PPI 的狀態(tài)。內存中數(shù)據(jù)結構的基址控制,支持 LPI 的相關中斷屬性和掛起狀態(tài)。電源管理支持。
    CPU interface:傳輸中斷給 Core
    打開或關閉 CPU interface 向連接的 CPU assert 中斷事件。對于 ARM,CPU interface 和 CPU 之間的中斷信號線是 nIRQCPU 和 nFIQCPU。如果關閉了中斷,即便是 Distributor 分發(fā)了一個中斷事件到 CPU interface,也不會 assert 指定的 nIRQ 或者 nFIQ 通知 Core。中斷的確認。Core 會向 CPU interface 應答中斷(應答當前優(yōu)先級最高的那個中斷),中斷一旦被應答,Distributor 就會把該中斷的狀態(tài)從 pending 修改成 active 或者 pending and active(這是和該中斷源的信號有關,例如如果是電平中斷并且保持了該 asserted 電平,那么就是 pending and active)。ack 了中斷之后,CPU interface 就會 deassert nIRQCPU 和 nFIQCPU 信號線。中斷處理完畢的通知。當 interrupt handler 處理完了一個中斷的時候,會向寫 CPU interface 的寄存器通知 GIC CPU 已經處理完該中斷。做這個動作一方面是通知 Distributor 將中斷狀態(tài)修改為 deactive,另外一方面,CPU interface 會 priority drop,從而允許其他的 pending 的中斷向 CPU 提交。為 CPU 設置中斷優(yōu)先級掩碼。通過 priority mask,可以 mask 掉一些優(yōu)先級比較低的中斷,這些中斷不會通知到 CPU。設置 CPU 的中斷搶占(preemption)策略。在多個中斷事件同時到來的時候,選擇一個優(yōu)先級最高的通知 CPU。

GICv3 控制器內部模塊和各中斷類型的關系如下圖所示:

中斷路由

GICv3 使用 hierarchy 來標識一個具體的 core, 如下圖是一個四層的結構(aarch64):

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

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

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

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

中斷狀態(tài)機

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

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

中斷處理流程

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

GIC 驅動

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

設備樹

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

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驅動reg :GIC的物理基地址,分別對應GICD,GICR,GICC…#interrupt-cells:這是一個中斷控制器節(jié)點的屬性。它聲明了該中斷控制器的中斷指示符(interrupts)中 cell 的個數(shù)interrupt-controller: 表示該節(jié)點是一個中斷控制器interrupts:分別代表中斷類型,中斷號,中斷類型, PPI中斷親和, 保留字段

關于設備數(shù)的各個字段含義,詳細可以參考 Documentation/devicetree/bindings 下的對應信息。

初始化

1. irq chip driver 的聲明:

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

定義 IRQCHIP_DECLARE 之后,相應的內容會保存到 __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

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

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 的值。下面詳細介紹。設置一組 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;
??......
}
    確認支持 SPI 中斷號最大的值為多少。向系統(tǒng)中注冊一個 irq domain 的數(shù)據(jù)結構,irq_domain 主要作用是將硬件中斷號映射到 irq number,后面會做詳細的介紹。設定 arch 相關的 irq handler。gic_irq_handle 是內核 gic 中斷處理的入口函數(shù),后面會做詳細的介紹。gic 虛擬化相關的內容。初始化 ITS。設置 SMP 核間交互的回調函數(shù),用于 IPI,回到函數(shù)為 gic_raise_softir。初始化 Distributor。初始化 CPU interface。初始化 GIC 電源管理。

中斷的映射

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

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

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

每一個中斷控制器對應多個中斷號, 而硬件中斷號在不同的中斷控制器上是會重復編碼的, 這時僅僅用硬中斷號已經不能唯一標識一個外設中斷,因此 linux kernel 提供了一個虛擬中斷號的概念。

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

數(shù)據(jù)結構

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

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

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:中斷控制器驅動的處理函數(shù),指向一個 struct irqaction 的鏈表,一個中斷源可以多個設備共享,所以一個 irq_desc 可以掛載多個 action,由鏈表結構組織起來action:設備驅動的處理函數(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:對應的 irq_chip 數(shù)據(jù)結構domain:對應的 irq_domain 數(shù)據(jù)結構

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:指向父設備name:/proc/interrupts 中顯示的名字irq_startup:啟動中斷,如果設置成 NULL,則默認為 enableirq_shutdown:關閉中斷,如果設置成 NULL,則默認為 disableirq_enable:中斷使能,如果設置成 NULL,則默認為 chip->unmaskirq_disable:中斷禁止irq_ack:開始新的中斷irq_mask:中斷源屏蔽irq_mask_ack:應答并屏蔽中斷irq_unmask:解除中斷屏蔽irq_eoi:中斷處理結束后調用irq_set_affinity:在 SMP 中設置 CPU 親和力irq_retrigger:重新發(fā)送中斷到 CPUirq_set_type:設置中斷觸發(fā)類型irq_set_wake:使能/禁止電源管理中的喚醒功能irq_bus_lock:慢速芯片總線上的鎖irq_bus_sync_unlock:同步釋放慢速總線芯片的鎖

struct irq_domain 與中斷控制器對應,完成硬件中斷號 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:對應中斷控制器的 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:用于中斷控制器設備與 irq_domain 的匹配map:用于硬件中斷號與 Linux 中斷號的映射xlate:通過 device_node,解析硬件中斷號和觸發(fā)方式

struct irqaction 主要是用來存設備驅動注冊的中斷處理函數(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ù)dev_id:設備 idirq:中斷號flags:中斷標志,注冊時設置,比如上升沿中斷,下降沿中斷等name:中斷名稱,產生中斷的硬件的名字dir:指向 /proc/irq/ 相關的信息

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

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

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

中斷控制器注冊 irq_domain

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

外設的驅動創(chuàng)建硬中斷和虛擬中斷號的映射關系

設備的驅動在初始化的時候可以調用 irq_of_parse_and_map 這個接口函數(shù)進行該 device node 中和中斷相關的內容的解析,并建立映射關系

    of_irq_parse_one 函數(shù)用于解析DTS文件中設備定義的屬性,如"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)建映射關系。內部會通過 irq_domain_set_info 調用 irq_domain_set_hwirq_and_chip,然后通過 virq 獲取 irq_data 結構體,并將 hwirq 設置到 irq_data->hwirq 中, 最終完成 hwirq 到 virq 的映射irq_domain_set_info 根據(jù)硬件中斷號的范圍設置 irq_desc->handle_irq 的指針,共享中斷入口為 handle_fasteoi_irq,私有中斷入口為 handle_percpu_devid_irq

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

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

中斷的注冊

設備驅動中,獲取到了 irq 中斷號后,通常就會采用 request_irq/request_threaded_irq 來注冊中斷,其中 request_irq 用于注冊普通處理的中斷。request_threaded_irq 用于注冊線程化處理的中斷,線程化中斷的主要目的把中斷上下文的任務遷移到線程中,減少系統(tǒ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 是中斷標志位,name 是中斷的名字。在講具體的注冊流程前,先看一下主要的中斷標志位:

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

中斷的處理

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

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

先會跳到 arm64 對應的異常向量表:

/*
?*?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í)行異常、缺頁中斷(跳轉地址或者取地址)、系統(tǒng)調用等
????????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 中設置了各種異常的入口。kernel_ventry 展開后,可以看到有效的異常入口有兩個同步異常 el0_sync,el1_sync 和兩個異步異常 el0_irq,el1_irq,其他異常入口暫時都 invalid。中斷屬于異步異常。

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

保護現(xiàn)場

kernel_entry 0,其中 kernel_entry 是一個宏,此宏會將 CPU 寄存器按照 pt_regs 結構體的定義將第一現(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 是關閉中斷。

/*?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??????//進入中斷棧
????????blr?????x1???????????//執(zhí)行?handle_arch_irq
????????irq_stack_exit???????//退出中斷棧
????????.endm

這里主要做了三個動作:

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

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

那中斷控制器的 handle_arch_irq 又指向哪里呢?其實上面我們有講到,在內核啟動過程中初始化中斷控制器時,設置了具體的 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);
}

所以,中斷處理最終會進入 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外設觸發(fā)的中斷。硬件中斷號 0-15 表示 SGI 類型的中斷,15-1020 表示外設中斷(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ù) hwirq 去查找 linux 中斷號通過中斷號找到全局中斷描述符數(shù)組 irq_desc[NR_IRQS] 中的一項,然后調用 generic_handle_irq_desc,執(zhí)行該 irq 號注冊的 action退出中斷上下文
static?inline?void?generic_handle_irq_desc(struct?irq_desc?*desc)
{
?desc->handle_irq(desc);????????????
}

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

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

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

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

恢復現(xiàn)場

SYM_CODE_START_LOCAL(ret_to_user)
????????disable_daif??????????????????????//D?A?I?F?分別為PSTAT中的四個異常屏蔽標志位,此處屏蔽這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??????????????????????//恢復?pt_regs?中的寄存器上下文

主要分三步:

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

總結

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

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

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

中斷下半部之 workqueue

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

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

workqueue 相關的數(shù)據(jù)結構

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

work_struct :

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

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:工作任務的處理函數(shù),在內核線程中回調

workqueue_struct :

工作的集合。workqueue 和 work 是一對多的關系。內核中工作隊列分為兩種:

    bound:綁定處理器的工作隊列,每個 worker 創(chuàng)建的內核線程綁定到特定的 CPU 上運行。unbound:不綁定處理器的工作隊列,創(chuàng)建的時候需要指定 WQ_UNBOUND 標志,內核線程可以在處理器間遷移。
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 內核線程,用于處理內存緊張時創(chuàng)建工作線程失敗的情況cpu_pwqs:Per-CPU 創(chuàng)建 pool_workqueuenuma_pwq_tbl[]:Per-Node 創(chuàng)建 pool_workqueue

pool_workqueue:

中間人 / 中介,負責建立起 workqueue 和 worker_pool 之間的關系。workqueue 和 pool_workqueue 是一對多的關系。

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 是一對一的關系,worker_pool 和 worker 是一對多的關系。

    bound 類型的工作隊列:worker_pool 是 Per-CPU 創(chuàng)建,每個 CPU 都有兩個 worker_pool,對應不同的優(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,代表內存 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 對應一個 work_thread() 內核線程。

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:當前正在處理的 workcurrent_func:當前正在執(zhí)行的 work 回調函數(shù)current_pwq:指向當前 work 所屬的 pool_workqueuescheduled:所有被調度執(zhí)行的 work 都將添加到該鏈表中task:指向內核線程pool:該 worker 所屬的 worker_poolnode:添加到 worker_pool->workers 鏈表中

可以用下圖來總結:

workqueue 的初始化

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

workqueue_init_early

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

workqueue_init

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

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

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

使用 workqueue

內核推薦驅動開發(fā)者使用默認的 workqueue,而不是新建 workqueue。要使用系統(tǒng)默認的 workqueue,首先需要初始化 work,內核提供了相應的宏 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 后,就可以調用 shedule_work 函數(shù)把 work 掛入系統(tǒng)默認的 workqueue 中。

work 調度

    將 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 內核線程。

worker_thread

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

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

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

總結

相關推薦

登錄即可解鎖
  • 海量技術文章
  • 設計資源下載
  • 產業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

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