irq_desc操作
本博客轉載自:http://rock3.info/blog/2013/11/17/irq_desc數組的初始化過程/
這篇博客解析非常精辟,再次表達對原作者的敬意。
irq_desc[]數組是linux內核中用于維護IRQ資源的管理單元,它存儲了某IRQ號對應的哪些處理函數,屬于哪個PIC管理、來自哪個設備、IRQ自身的屬性、資源等,是內核中斷子系統的一個核心數組,習慣上稱其為“irq數組”(個人愛好,下標就irq號)。本篇博客著重學習irq_desc[]數組的一些操作的過程和方法,如初始化、中斷處理、中斷號申請、中斷線程等,而對于輔助性的8259A和APIC等設備的初始化過程,不詳細討論,對于某些圖片或代碼,也將其省略掉了。
本文中出現的irq_desc->和desc->均表示具體的irq數組變量,稱其中的一個個體為irq_desc[]數組元素,描述個體時也直接時用字符串desc。為了區別PIC的handle和driver的handle,將前者稱為中斷處理函數(對應desc->handle_irq,實際上對應handle_xxx_irq()),而將后者稱為中斷處理操作(對應desc->action)。本文中將以irq_descp[]數組為操作對象的層稱為irq層。本文使用的內核代碼版本為3.10.9。一篇好的博客應該是盡量多的說明,配少量的核心代碼,這里偷懶了,很多部分實際是代碼分析的過程,也沒有省略掉。本篇博客耗時48小時。一、irq_desc結構和irq_desc[]數組
irq_desc[]數組,在kernel/irq/irqdesc.c中聲明,用于內核管理中斷請求,例如中斷請求來自哪個設備,使用什么函數處理,同步資源等:
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 … NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
整體上,關于irq_desc結構體,如下圖所示:irq_desc
action指針指向具體的設備驅動提供的中斷處理操作,就是所為的ISR,action本身是一個單向鏈表結構體,由next指針指向下一個操作,因此action實際上是一個操作鏈,可以用于共享IRQ線的情況。
handle_irq是irq_desc結構中與PIC相關的中斷處理函數的接口,通常稱作”hard irq handler“。此函數對應了PIC中的handle_xxx_irq()系列函數(xxx代表觸發方式),do_IRQ()就會調用該函數,此函數最終會執行desc->action。
irq_data用于描述PIC方法使用的數據,irq_data下面有兩個比較重要的結構:chip和state_use_accessors,前者表示此irq_desc[]元素時用的PIC芯片類型,其中包含對該芯片的基本操作方法的指針;后者表示該chip的狀態和屬性,其中有些用于判斷irq_desc本身應該所處的狀態。
lock用于SMP下不同core下的同步。
depth表示中斷嵌套深度,也即一個中斷打斷了幾個其他中斷。
istate表示該desc目前的狀態,將在“六、istate狀態”中描述。
struct irq_desc {
struct irq_data irq_data;
irq_flow_handler_t handle_irq;
…
struct irqaction action; / IRQ action list /
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; / nested irq disables */
raw_spinlock_t lock;
…
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
這里還是看一下irqaction結構體,action的handler是具體的中斷服務程序,next指針用于指向同一個鏈上的后一個的irqaction,thread_fn用于描述軟中斷處理函數。
typedef irqreturn_t (*irq_handler_t)(int, void *);
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
這意味著所有的驅動在寫中斷處理函數時,必須以irqreturn_t為類型:
// intel e1000
static irqreturn_t e1000_intr(int irq, void *data);
// acpi
static irqreturn_t acpi_irq(int irq, void *dev_id)
// hd
static irqreturn_t hd_interrupt(int irq, void *dev_id)
// ac97
static irqreturn_t atmel_ac97c_interrupt(int irq, void *dev)
在這里,很容易產生一個問題,就是驅動程序處理的數據在哪?總要有些數據要處理,是從void參數嗎?那么這個數據怎么獲取的?handle_irq_event_percpu()函數里有具體的action的調用方式:
1
res = action->handler(irq, action->dev_id);
那么,void *參數來自action->dev_id,而dev_id是驅動程序注冊時,調用request_irq()函數傳遞給內核的。而這個dev_id通常指向一個device設備,驅動程序就通過該device設備將需要的數據接收上來,并進行處理。
二、irq_desc[]的初始化——8259A
irq_desc[]數組是內核維護中斷請求資源的核心數組,它必須在合適的時機予以初始化。內核起動后,有步驟的初始化內核各個子系統,init_IRQ()函數主要負責完成內核中斷子系統的主要初始化。irq_desc[]數組伴隨著init_IRQ()函數的執行而完成其一部分的初始化。
init_IRQ()函數的調用路徑為main()->…->start_kernel()->init_IRQ()->native_init_IRQ()。init_IRQ()函數與irq_desc[]數組初始化或者IDT、interrupt[]數組的設置有關的函數或過程,關于init_IRQ的內部調用關系,如下圖所示:
init_IRQ
下面是具體的代碼分析過程:從init_IRQ()函數開始分析,init_IRQ在arch/x86/kernel/irqinit.c中定義:void __init init_IRQ(void)
{
int i;
}
x86_add_irq_domains()直接略過。這里的注釋還時很有用的,這里說開始時使用8259A注冊這些中斷向量號,如果系統使用IO APIC,將覆蓋這些中斷向量號,并且能夠動態的重新使用。vector_irq為在arch/x86/include/asm/hw_irq.h中定義的per_cpu整形數組,長度為256,用于描述每個CPU的中斷向量號,即vector_irq[](vector_irq[]元素初始化時被賦值為-1)中存儲著系統可以使用的中斷向量號。這里需要注意,vector_irq[]數組時PER_CPU的。
struct legacy_pic default_legacy_pic = {
.nr_legacy_irqs = NR_IRQS_LEGACY,
.chip = &i8259A_chip,
.mask = mask_8259A_irq,
.unmask = unmask_8259A_irq,
.mask_all = mask_8259A,
.restore_mask = unmask_8259A,
.init = init_8259A,
.irq_pending = i8259A_irq_pending,
.make_irq = make_8259A_irq,
};
struct legacy_pic *legacy_pic = &default_legacy_pic;
a) native_inti_IRQ()
init_IRQ()將vector_irq[]逐個賦值(就賦值了16個,從0x30到0x39)。x86_init為x86架構初始化時的一個全局變量,記錄了各個子系統(irq,paging,timer,iommu,pci等)初始化使用的具體函數。而實際的x86_init.irqs.intr_init指針指向native_init_IRQ()函數(arch/x86/kernel/irqinit.c):
void __init native_init_IRQ(void)
{
int i;
#ifdef CONFIG_X86_32
irq_ctx_init(smp_processor_id());
#endif
}
x86_init.irqs.pre_vector_init指針指向init_ISA_irqs()函數,主要完成8259A/Local APIC的初始化,apic_intr_init()函數主要完成apic相關的中斷的初始化。接著,native_init_IRQ()函數將調用set_intr_gate()函數設置中斷門,將interrupt[]數組設置的地址設置到相應的中斷門。注意,這里只是對沒有used_vectors進行set_intr_gate()的賦值,并不是從FIRST_EXTERNAL_VECTOR到NR_VECTORS全部賦值,因為有些特殊情況會預留(關于used_vectors和vector_irq的關系,詳見“七、中斷向量、鎖和CPU”)。余下的兩個接口處理了一些特殊情況,這里不展開了。
實際上init_IRQ()主要調用了native_init_IRQ(),除了使用set_intr_gate()來初始化Interrupt describptor外,后者主要干了兩件事:init_ISA_irqs()和apic_intr_init()。先從簡單的看起,apic_intr_init()函數實際上是一系列的set_intr_gate,但不通過interrupt[]數組,也不通過irq_desc[](這就是native_init_IRQ()函數中所為的“特殊情況”,屬于used_vectors的范圍):
static void __init apic_intr_init(void)
{
smp_intr_init();
#ifdef CONFIG_X86_THERMAL_VECTOR
alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
…
#ifdef CONFIG_HAVE_KVM
/* IPI for KVM to deliver posted interrupt */
alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif
…
}
而smp_intr_init()函數如下執行apic_intr_intr()函數類似的操作,也通過set_intr_gate()函數設置了一些中斷門。
[rock3@e4310 linux-stable]$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
…
44: 66 80 77 72 PCI-MSI-edge snd_hda_intel
45: 14948296 0 0 0 PCI-MSI-edge iwlwifi
NMI: 1539 19912 17314 17232 Non-maskable interrupts
LOC: 45133746 42836772 33584448 33666542 Local timer interrupts
SPU: 0 0 0 0 Spurious interrupts
PMI: 1539 19912 17314 17232 Performance monitoring interrupts
IWI: 641572 409182 330064 302186 IRQ work interrupts
然后看比較復雜的init_ISA_irqs()函數,代碼如下:
void __init init_ISA_irqs(void)
{
struct irq_chip *chip = legacy_pic->chip;
const char *name = chip->name;
int i;
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
init_bsp_APIC();
#endif
legacy_pic->init(0);
}
legacy_pic->init指針指向init_8259A()函數,因此init_ISA_irqs執行了init_8259A(0)。irq_set_chip_and_handler_name()函數用于設置irq_desc[]數組的handle_irq、name、chip等成員。因此init_ISA_irqs()函數做了三件事:init_bsp_APIC()、init_8259A()、irq_set_chip_and_handler_name()。此時legacy_pic->nr_legacy_irqs為16。
init_8259A(0)為對8259A的某種初始化操作,與Irq_desc[]數組的初始化無關,不討論了。
irq_set_chip_and_handler_name()函數如下(kernel/irq/chip.c):
void
irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
irq_flow_handler_t handle, const char name)
{
irq_set_chip(irq, chip);
__irq_set_handler(irq, handle, 0, name);
}
irq_set_chip()將irq_descp[]數組的action的chip成員,主要是__irq_set_handler()函數(kernel/irq/chip.c),看下__irq_set_handler()函數都設置了irq_desc[]數組的什么成員:
void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
const char *name)
{
unsigned long flags;
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
out:
irq_put_desc_busunlock(desc, flags);
}
主要就設置了兩個成員:handle_irq全部設置為handle_level_irq,name設置為“XT-PIC”(8259A)。而irq_desc[]數組中的handle_irq成員在do_IRQ()中被調用來執行具體的ISA。這個位置使用了buslock,也即desc->irq_data.chip->irq_bus_lock,而不是desc->lock。buslock用于中斷控制器的操作,desc->lock用于IRQ中斷處理函數的操作。
三、irq_desc[]的初始化——IO APIC
IO APIC的handle_irq通過setup_IO_APIC_irqs()函數初始化。調用過程是start_kernel()->rest_init()->kernel_thread(kernel_init)->kernel_init_freeable()->smp_init()->APIC_init_uniprocessor()->setup_IO_APIC()->setup_IO_APIC_irqs()->__io_apic_setup_irqs()->io_apic_setup_irq_pin()->setup_ioapic_irq()->ioapic_register_intr()->irq_set_chip_and_handler_name()。中間經過了太多的過程,其中主要的入口有setup_IO_APIC()用于初始化IO APIC,在初始化IO APIC的過程中完成了對IO APIC的irq_desc[]數組的初始化(setup_IO_APIC_irqs()),最終調用了irq_set_chip_and_handler_name()函數,完成了對irq_desc[]數組的初始化,其初始化desc->handle默認為handle_fasteoi_irq()或handle_edge_irq(),desc->name分別對應fasteoi或edge。
當然,這個過程在8259A調用irq_set_chip_and_handler_name()之后,那么根據__irq_set_handler()的實現,handle_irq可以更新,因此后注冊的IO APIC替代了先前的8259A。
這里還有個IRQ個數的問題,8259A初始化了16個irq_desc[]數組(0x30到0x39),而APIC應該時224個,但是實際上在setup_IO_APIC_irqs()函數執行時,輪詢了系統偵測到的所有的IO APIC,對每個IO APIC在__io_apic_setup_irqs()函數中,又輪詢該IO APIC上注冊的所有的設備,對于每個注冊者,執行io_apic_setup_irq_pin()->setup_ioapic_irq()->ioapic_register_intr()->irq_set_chip_and_handler_name()的過程,而對于每個IO APIC上的每個注冊者,對應的irq號,就通過pin_2_irq()接口確認。這意味著,IO APIC要將哪些irq_desc[]數組初始化。
IO APIC的hanle_irq有兩種,分別是handle_fasteoi_irq()和handle_edge_irq(),最終也都調用了handle_irq_event()->handle_irq_event_percpu(),在irq_desc[]初始化上與8259A一致,在“四、desc->handle_irq”部分會詳細分析。
這樣就存在一個問題,因為IO APIC并非每個irq_desc[]數組元素都去初始化,而是只初始化那些連接有設備的,那么如何能保證這些irq號就是驅動申請的irq號那?
四、desc->handle_irq
經過“irq_desc[]的初始化”部分的描述desc->handle_irq已經初始化完畢,而desc->handle_irq接口實際上可以掛接幾個函數(8259A和IO APIC):
void handle_level_irq(unsigned int irq, struct irq_desc *desc);(8259A)
void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc);(IO APIC)
void handle_edge_irq(unsigned int irq, struct irq_desc *desc);(IO APIC)
? 字面意思說明三種處理函數分別代表電平觸發、xxx觸發、邊沿觸發,他們之間各有不同但最終均調用了handle_irq_event()。本文中將以以上函數稱為handle_xxx_irq()系列函數(還有其他幾個handle_xxx_irq()函數,也屬于此系列,但是不是x86平臺時用或者不是8259A或IO APIC時用)。他們三者之間在何時屏蔽IRQ線、是否要響應發出中斷信號的硬件等處有微小的區別。
void
handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc);
out_unlock:
raw_spin_unlock(&desc->lock);
}
首先,raw_spin_lock先獲取鎖(關于desc->lock將在“七、中斷向量、鎖和CPU“部分詳細介紹),退出前釋放鎖,然后按照下列次序進行處理:
用mask_irq_ack()函數向產生中斷的硬件發出ACK響應,并暫時屏蔽該中斷線(handle_level_irq()獨有操作)。
用irqd_irq_inprogress()判斷中斷是否處于inprogress階段,如果處于inprogress階段,則校驗并等待“偽中斷”輪詢完畢(關閉“偽中斷”輪詢,詳見“六、istate狀態”)。
去掉desc->istate中的IRQS_REPLAY和IRQS_WAITING標志(詳見“六、istate狀態”)。
用kstat_incr_irqs_this_cpu()函數更新desc關于cpu的統計數據。
如果desc->action為空或者desc->irq_data處于DISABLE狀態,則將該irq_desc[]元素掛起(desc->istate置位IRQS_PENDING)并返回。
執行handle_irq_event(desc),循環調用desc->action。
用cond_unmask_irq()函數恢復IRQ線。
handle_irq_event()代碼如下(kernel/irq/handle.c):
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
irqreturn_t ret;
}
handle_irq_event()函數首先將irq_desc[]數組的istate清除IRQ_PENDING標志,然后設置desc->irq_data->state_use_accessors增加IRQD_IRQ_INPROGRESS,然后執行handle_irq_event_percpu()函數,逐個cpu執行action,執行完畢后,清除desc->irq_data->state_use_accessors的IRQD_IRQ_INPROGRESS標志,說明IRQD_IRQ_INPROGRESS標志表示正在執行某個具體中斷處理操作,也即正在執行action。注意此處鎖的位置,更新desc->irq_data->state_use_accessors的標志時,鎖,執行action的時候不鎖,進入handle_irq_event()時,就已經鎖住了。下面來看handle_irq_event_percpu()函數:
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
irqreturn_t retval = IRQ_NONE;
unsigned int flags = 0, irq = desc->irq_data.irq;
}
trace_irq_handler_entry()函數和trace_irq_handler_entry()函數用于找出action中那些不合法的(網上有人這么說,沒找到,具體不詳)并強行disble他們。具體中斷處理調用了action->handler(),反復執行將整個chain上的所有action都執行一遍,所有的返回值或起來作為總的返回值。針對IRQ_WAKE_THREAD,說明驅動程序使用了軟斷的方式(設置了thread_fn),那么就要調用irq_wake_thread(),嘗試在調度器中激活action->thread_fn。關于irq_wake_thread(),詳見“八、中斷線程”部分。最后,在默認情況下,通過note_interrupt()接口更新desc->action的結果,并決定是否觸發“偽中斷”輪詢函數poll_spurious_irq()。
desc->handle_irq,負責與PIC觸發方式相關的操作,并作desc->istate相關校驗、置位;
handle_irq_event(),負責設置desc->irq_data以及解鎖避免其他CPU忙等。
handle_irq_event_percpu(),負責具體的在CPU上運行desc->action以及轉向軟中斷、轉向“偽中斷”輪詢的過程。
五、中斷申請函數request_irq()
從上述描述可以看出,irq_desc[]數組的name,handle_irq,irq_data->chip伴隨著8259A和IO APIC的初始化而初始化,而irq_desc[]數組的其他部分,如actions還沒有注冊,actions是驅動程序通過request_irq()函數項內核的IRQ系統申請一個IRQ號,并初始化該號的irq_desc[]數組元素中的action(include/linux/interrupt.h):
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);
}
request_threaded_irq()函數代碼真不少,刪除參數校驗和調試的部分,主要完成了申請action內存,并初始化之,然后掛接action和irq_desc[]:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
…
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
}
自此,irq_desc[]數組的初始化基本完成。
六、istate狀態
Linux內核中的中斷子系統有四種狀態、特性,分別是:
用于描述irq_desc的istate。
用于描述irq_desc->action->handler的flags。
用于描述irq_desc->action->thread_fn的thread_flags。
用于描述irq_desc->irq_data的state_use_accessors。
此處重點介紹istate,但首先還是irq_desc->action->flags。include/linux/interrupt.h文件中定義了handling routines的一些標志(并沒有完全列出,還有一些IRQF_TRIGGER_的表示觸發方式的標志):
/*
- These flags used only by the kernel as part of the
- irq handling routines.
- IRQF_DISABLED - keep irqs disabled when calling the action handler.
- DEPRECATED. This flag is a NOOP and scheduled to be removed
- IRQF_SHARED - allow sharing the irq among several devices
- IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
- IRQF_TIMER - Flag to mark this interrupt as timer interrupt
- IRQF_PERCPU - Interrupt is per cpu
- IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
- IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
- registered first in an shared interrupt is considered for
- performance reasons)
- IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
- Used by threaded interrupts which need to keep the
- irq line disabled until the threaded handler has been run.
- IRQF_NO_SUSPEND - Do not disable this IRQ during suspend
- IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
- IRQF_NO_THREAD - Interrupt cannot be threaded
- IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
- resume time.
*/
以上標志如果非得從“狀態”和“特性”中選擇一個的話,應該是“特性”,其前綴為IRQF_,表示IRQ Flags。在request_irq()函數執行時,需要填寫flags變量,也就是這些標志,這些標志用于說明你申請的handler的特性,request_irq的flags總是帶有IRQF_DISABLED的標志,其他特性讓其他IRQ操作函數按不同的路徑執行。如IRQ_SHARED標志表示可以共享IRQ號,IRQF_NO_THREAD標志表示中斷處理函數不能被線程化。
/*
-
Bit masks for desc->state
-
IRQS_AUTODETECT - autodetection in progress
-
IRQS_SPURIOUS_DISABLED - was disabled due to spurious interrupt
- detection
-
IRQS_POLL_INPROGRESS - polling in progress
-
IRQS_ONESHOT - irq is not unmasked in primary handler
-
IRQS_REPLAY - irq is replayed
-
IRQS_WAITING - irq is waiting
-
IRQS_PENDING - irq is pending and replayed later
-
IRQS_SUSPENDED - irq is suspended
*/
enum {
IRQS_AUTODETECT = 0x00000001,
IRQS_SPURIOUS_DISABLED = 0x00000002,
IRQS_POLL_INPROGRESS = 0x00000008,
IRQS_ONESHOT = 0x00000020,
IRQS_REPLAY = 0x00000040,
IRQS_WAITING = 0x00000080,
IRQS_PENDING = 0x00000200,
IRQS_SUSPENDED = 0x00000800,
};
這些狀態存儲在irq_desc->istate,也就是core_internal_state__do_not_mess_with_it。從字面意思看,有些狀態不好理解,通過他們在linux內核中的調用關系搞清楚來龍去脈:IRQS_AUTODETECT狀態和IRQS_WAITING狀態
? AUTODETECT狀態在函數probe_irq_on()中被開啟,在probe_ifq_off()函數或probe_ifq_mask()函數中被關閉。而probe_irq_on()和probe_irq_off()是驅動與內核申請可用的irq號的調用函數,一個驅動程序可以通過request_irq()向內核申請注冊中斷處理函數到desc->action,此時需要攜帶irq號作為參數,而irq號可以自是預先設定(Intel Enternet drvier),也可以通過一種叫自動偵測(auto probe)的過程來完成:
首先,其驅動程序調用probe_irq_on()接口,開始auto probe的過程。probe_irq_on()函數將返回一個32bits的irq_desc是否可用的掩碼mask(此處因為返回值類型為int,所以只能返回32位的掩碼)。probe_irq_on()接口首先選出所有可以被偵測(desc->irq_data->status_use_accessors是否包含IRQD_IRQ_NOPROBE標志)、并且沒有action函數的irq_desc[],并將這些irq_desc->istate設置IRQS_AUTODETECT標志和IRQS_WAITING標志;然后過濾掉所有的偽中斷irq_desc[],是否可用通過判斷desc->istate包含IRQS_WAITING狀態(偽中斷經過handle_xxx_irq()處理將清除IRQS_WAITING狀態)。上面的工作對于auto probe都是輔助性的,但最重要的是probe_irq_on()函數在開始時就調用了async_synchronize_full()接口,來同步內核中所有異步調用的函數(這樣就不存在發生了中斷,但沒有被記錄的情況)。
其次,驅動程序收到mask掩碼后(當然也可以利用mask做一些事情,但并非所有的驅動都這樣做),主動產生一次硬件中斷。
然后,驅動程序調用probe_irq_off(mask),后者偵測所有的irq_desc[],查看到底是哪些irq_desc[]元素發生了硬件中斷,如果只發生了一次硬件中斷,那么就返該irq_desc[]的下標,也即irq號;如果沒有偵測到中斷發生,則返回0;如果偵測到多次中斷發生,則返回一個負數(中斷發生次數的相反數)。probe_irq_off()函數通過desc->istate是否包含IRQS_WAITING標志來判斷是否發生了一次硬件中斷(包含就沒發生,不包含就發生了)。
最后,驅動程序通過probe_irq_off()的返回值,來判斷是否是一個可用的irq號,如果可用,則通過request_irq()來申請注冊中斷處理函數。
? 如果再扣的細一點,還有兩個問題:
問題1,async_synchronize_full()函數能夠使所有的irq_desc[]的istate都去掉IRQS_WAITING標志,這是為什么?
問題2,probe_irq_off()函數為什么可以使用帶有IRQS_WAITING標志來判斷哪個irq描述符發生了硬件中斷?
對問題1,涉及到IRQS_WAITING標志的含義,IRQS_WAITING標志在probe_irq_on()接口中被設置,在執行irq_desc->handle_irq,也即具體的handle_xxx_irq()接口時被清除,猜測async_synchronize_full()接口可能會等待所有的中斷處理函數執行完畢吧(此處還有問題,如何等待源源不斷的中斷處理執行完成?那得看async_synchronize_full的細節了。)
對問題2,在init_8259A()函數或者setup_IO_APIC_irqs()函數中,都調用了irq_set_chip_and_handler_name()函數,后者將desc->irq_data.chip和desc->handle_irq以及desc->name給予初始化,這意味著與irq_desc[]關聯的PIC已經完全驅動起來,并且與irq_desc[]建立了關聯關系,當硬件產生中斷信號時,PIC可以接收到該信號,并通知CPU,CPU查找到IDT里的中斷處理程序后,就執行了do_IRQ()函數,然后調用了handle_IRQ_event()接口,然后到具體的irq_desc->handle_irq,如handle_level_irq(),而在具體的handle_xxx_irq()接口中,就會清除掉IRQS_WAITING標志。
總結一下,IRQS_AUTODETECT表示某個irq_desc[]變量處于自動偵測狀態,通過probe_irq_on()函數設置此狀態,通過probe_irq_off()清除此狀態。IRQS_WAITING表示某個irq_desc[]變量處于等待狀態,也即等待被處理,等待irq_desc->handle_irq的執行,此狀態通過probe_irq_on()設置,通過irq_desc->handle_irq,也即handle_xxx_irq()系列函數清除。IRQS_SPURIOUS_DISABLED狀態前面說IRQS_AUTODETECT狀態時,提到“偽中斷”,這個IRQS_SPURIOUS_DISABLED就是指這種情況。根據wiki上的說法:“一類不希望被產生的硬件中斷。發生的原因有很多種,如中斷線路上電氣信號異常,或是中斷請求設備本身有問題。”,猜測是哪些PIC上確實偵聽到了中斷信號,但實際上沒有發生中斷,或者沒有找到中斷處理函數的情況。Linux內核將某個irq_desc[]元素發生了10萬次中斷,卻有9.9萬次沒有處理的情況,視為“偽中斷”,會將其置位,意味著因為“偽中斷”而被禁用(緊接著會執行irq_disable(desc))。中斷發生次數統計通過desc->irq_count,中斷發生卻沒有處理的統計通過desc->irq_unhandled。內核在note_interrupt()函數中處理此情況。setup_irq()函數用于驅動程序將具體的中斷處理函數掛接到desc->action下,該函數執行時,將清除IRQS_SPURIOUS_DISABLED標志。對于“偽中斷”,內核并不是扔掉不管,而是有一套自己的處理方法,有人還對其做過優化。目前,內核采用poll_spurious_irqs()的方法來處理被IRQS_SPURIOUS_DISABLED的desc。poll_spurious_irqs()函數將輪詢所有的“偽中斷”,并嘗試在本地core上執行一次(通過try_one_irq()函數)。try_one_irq()函數有選擇的執行handle_irq_event(),(有些情況的偽中斷不予執行,比如PER_CPU的、嵌套的等等)。poll_spurious_irqs()函數并不清除IRQS_SPURIOUS_DISABLED標志,而是嘗試輪詢并執行他們一次。? 總結一下,IRQS_SPURIOUS_DISABLED標志意味著某irq_desc[]元素被視為“偽中斷”,并被禁用。該標志被note_interrupt()函數設置,被setup_irq()函數清除。對于哪些偽中斷,系統嘗試時用poll_spurious_irqs()函數在本地CPU上輪詢并執行他們一次(在中斷線被PIC禁用時,仍可以執行中斷處理函數,即是中斷處理函數執行完畢,也不清除此標志)。
IRQS_POLL_INPROGRESS狀態? in progress表示正處于過程當中,poll in progress字面意思就是中斷處理函數目前正在以poll的方式執行。硬件中斷處理函數通常是立即執行,而軟中斷才留在后面執行。
前面提到的try_one_irq()函數,在其執行handle_irq_event()函數前,將設置此標志表示中斷處理函數正在以poll的方式被執行,在其執行完畢handle_irq_event()后清除此標志,表示中斷處理函數執行poll完畢。而調用try_one_irq()函數的還由misrouted_irq()函數(用于嘗試執行一次可能時misrouted的情況,硬件產生了中斷信號,內核卻將其對應到錯誤的irq_desc[]元素上的情況)函數中被調用。這是對于“偽中斷”的輪詢,但對正常的中斷處理,并沒有采用poll的方法(NAPI采用了,另說),而是在具體的handle_xxx_irq()函數中需要執行irq_check_poll()方法,等待中斷處理函數poll完畢,因為驅動實現的中斷處理函數未必是可重入的。總結一下,IRQS_POLL_INPROGRESS,表示一個irq_desc[]元素正處于輪詢調用action鏈的階段。此標志只在try_one_irq()函數(被poll_spurious_irqs()函數和misrouted_irq()函數調用)調用handle_irq_event()前被設置,在其調用handle_irq_event()后被清除。由于具體的中斷處理函數(desc->action)的設計未必是可重入的,因此desc->handle_irq,如handle_xxx_irq()需要等待其上的輪詢完畢后才能執行。這意味著,僅僅“偽中斷”才會被輪詢,并且一個中斷處理函數可以同時被“偽中斷”輪詢執行,也可以正常執行,但必須排隊執行。
這個地方還是不理解,既然被定為“偽中斷”,那么就會被irq_disable()——從硬件上屏蔽該中斷線,怎么還會接收到中斷、并執行中斷處理函數那?這可能就是“偽中斷”神奇的地方。
IRQS_ONESHOT狀態?開一槍狀態?應該是個不太重要的狀態。該狀態表示irq_desc[]元素的主處理函數不是非屏蔽的(直接說mask不就完了,難道除了mask,unmask還有半mask?)。在handle_fasteoi_irq()函數(其他handle_xxx_irq()中沒有,fasteoi一定是一種比較特殊的情況)中,如果該標志設置,就需要mask_irq()執行一下,然后就是硬件操作了。此處的mask應該是這樣的,mask意味著屏蔽中斷,也就是在中斷處理函數執行的時后,從CPU的角度短暫的禁止該中斷(應該是中斷向量,通過設置CPU的中斷屏蔽寄存器IMR,來完成此過程)。而是否需要mask中斷,是中斷處理函數的本身的需要,因此,也就是應該是desc->action->flags里的設置(IRQF_ONESHOT標志),而IRQS_ONESHOT狀態應該是源于IRQF_ONESHOT標志的,在setup_irq()函數被執行用來為desc添加action的時候,在非共享的方式下,如果發現action->flags中設置了IRQF_ONESHOT標志,則為desc->istate設置此狀態。在irq_finalize_oneshot()函數中,將會執行unmask_irq(),并清除該標志。irq_finalize_oneshot()函數在one shot中斷處理函數運行完畢(action->thread_fn)時被調用。總結,one shot,字面意思開一槍,這槍肯定是早期的步槍,沒能實現自動填裝彈藥。IRQS_ONESHOT更像一種屬性,而非狀態(在其被設置時,并不是中斷被屏蔽的時刻,而時表示此action是one shot類型的),在setup_irq()的時后被條件設置,在handle_fasteoi_irq()執行mask_irq()操作,在irq_finalize_oneshot()中執行unmask_irq()操作,并清除此標志。IRQS_REPLAY狀態?從字面意思上講,應該是重新發生一次中斷的意思。該標志在handle_xxx_irq()函數中的開始部分就被清除(避免多個core同時執行),在check_irq_resend()函數中,若判斷desc->istate包含IRQS_PENDING標志,則設置該狀態。IRQS_REPLAY狀態僅在check_irq_resend()函數中被設置,check_irq_resend()函數通過重新激發中斷控制器上的中斷信號來完成此過程(中斷硬件將不發送中斷信號,僅僅由PIC重新向CPU發送),對handle_level_irq()函數觸發的中斷,不予重新發送(不知道原因)。而check_irq_resend()函數又被__enable_irq()和irq_startup()所調用,最終被irq_set_chip_and_handler_name()和irq_set_handler()調用,他們的調用關系如下圖所示:resend
總結,IRQS_REPLAY狀態表示需要重新激發一次中斷信號(正在重新發送IRQ信號),它在desc->handle_irq被初始化最后時刻被設置(被irq_set_chip_and_handler_name()函數設置):完成desc->handle_irq的掛接后要由PIC自動產生一次中斷,利用重新注冊action的機會,對“掛起”(IRQS_PENDING)狀態的中斷再觸發一遍,然后由handle_xxx_irq來查看中斷處理函數是否正常,該標志在handle_xxx_irq()時被清除。IRQS_PENDING狀態字面意思“正在掛起”,為什么要掛起?在什么情況下掛起?在handle_xxx_irq()函數(handle_ege_irq()函數和handle_edge_eoi_irq()函數比較特殊)中,如果發現以下情況中的一種,則掛起,掛起后直接釋放鎖并推出,并不執行handle_irq_event()函數:desc->action = NULL,也即無具體的中斷處理函數;
desc->irq_data->state_use_accessors包含IRQD_IRQ_DISABLED,也即中斷控制器被禁用;
還有一種情況會“掛起”中斷處理,在probe_irq_on()時,對于所有的沒有分配action并且可以被PROBE的desc,需要重新irq_startup()一下,清掉此前mask it self的longstanding interrupt。在irq_startup()函數中,硬件會嘗試掛起該中斷向量,如果成功的話,desc->istate也需要置位IRQS_PENDING。
另一種需要“掛起”的情況是try_one_irq()函數中,如果desc->irq_data->state_use_accessors包含IRQD_IRQ_INPROGRESS標志(表示IRQ不處于),則為desc->istate置掛起標志并推出。
以下是清除IRQS_PENDING的場景:handle_irq_event()函數開始處即清除IRQS_PENDING標志;
check_irq_resend()函數,會在PIC重新觸發、向CPU產生中斷信號前,清除掉IRQS_PENDING標志;
? 以下是校驗IRQS_PENDING的場景:
try_one_irq()函數中,如果desc->irq_data->state_use_accessors并不包含IRQD_IRQ_INPROGRESS標志,但是desc->istate卻包含IRQS_PENDING標志,并且desc->action不為空,則返回執行action鏈上的函數。
check_wakeup_irq()函數中,對所有的desc,如果desc->depth==1并且置位IRQS_PENDING標志,則返回-EBUSY(這個地方一定有特殊含義,就不細研究了)。
? 總結,IRQS_PENDING表示中斷處理被掛起,通常是因為沒有中斷處理函數或者中斷控制器被禁用。通常由handle_xxx_irq()設置,由handle_irq_event()清除。
在__disable_irq()函數中,如果參數suspend設置為true,則置位IRQS_SUSPENDED狀態。suspend_device_irqs()函數調用__disable_irq()函數時,會設置此標志。
? 清除IRQS_SUSPENDED狀態場景:
在__enable_irq()函數中,如果參數resume為ture,則清除IRQS_SUSPENDED狀態。resume_device_irqs()函數調用__enable_irq()函數時,清除此標志。
校驗IRQS_SUSPENDED狀態的場景:
在__enable_irq()函數中,如果desc->depth=1,則校驗IRQS_SUSPENDED狀態,如果置位,則退出,不執行irq_enable()。
在check_wakeup_irqs()函數中,具備IRQS_SUSPENDED狀態成為是否執行mask_irq()的一個條件。
在suspend_device_irqs()函數,對所有具備IRQS_SUSPENDED狀態的desc執行 synchronize_irq()。
? 總結,IRQS_SUSPENDED標志表示所有中斷線均被disable,通過suspend_device_irqs()函數置位,通過resume_device_irqs()函數清除。
istate
七、中斷向量、鎖和CPU
中斷向量這個詞經常說,那么到底什么是中斷向量那?內核中有兩個概念,一個叫vector,另一個叫irq。vector指的就是“中斷向量”,也就是PIC向CPU傳遞的用于表示發生中斷的IRQ線的一種編號。而irq,也就是常說的irq號,是內核維護的中斷請求的數組下標。以下說法或變量是等價的(x86架構):
中斷向量號
PIC向CPU傳遞的關于中斷向量號
IDT表的索引號
interrupt[]數組的下標+0x20
irq_desc[]數組的下標+0x20
irq_desc->action->irq+0x20
vector_irq[]數組的下標
irq號+0x20
硬件維護的中斷向量號,在x86平臺上,本質上是PIC向CPU傳遞的用于表示發生中斷的IRQ線的一種向量編號,CPU通過該向量找到IDT表中的對應的中斷門描述符。由于Intel保留了前0x20個中斷、異常號,所以內核就沒必要再維護這塊了,所以內核中的數組下標從0開始,就對應了中斷向量的0x20號。但是vector_irq[]數組比較特殊,它的下標代表vector,而內容代表irq,實際上就是irq到vector的一個影射:
typedef int vector_irq_t[NR_VECTORS];
DECLARE_PER_CPU(vector_irq_t, vector_irq);
通過setup_vector_irq()函數初始化:
void setup_vector_irq(int cpu)
{
#ifndef CONFIG_X86_IO_APIC
int irq;
…
for (irq = 0; irq < legacy_pic->nr_legacy_irqs; irq++)
per_cpu(vector_irq, cpu)[IRQ0_VECTOR + irq] = irq;
#endif
}
前文中提到在init_IRQ()函數中,會首先初始化vector_irq[]數組,而且vector_irq[]數組是PER_CPU的。vector_irq[]在聲明時被全部初始化為-1,-1表示未被使用。在init_IRQ()函數中,16個(0x30~0x3f,為什么是0x30開始,就不討論了)被初始化為0到15。
某CPU在處理某中斷,此刻發生了相同的中斷,要相同的CPU來處理;丟中斷;
某CPU在處理某中斷,此刻發生了相同的中斷,要不同的CPU來處理;避免這種情況;
某CPU在處理某中斷,此刻發生了不同的中斷,要相同的CPU來處理;中斷嵌套;
某CPU在處理某中斷,此刻發生了不同的中斷,要不同的CPU來處理;正常處理;
對于第一種情況,起碼要有一種機制保證正常的丟失中斷,而不是總被自己打斷,而無法完成正常的中斷處理。而對第二種情況,應該盡量避免,因為中斷處理函數不一定是可重入的,因此必須順序執行,一次沒處理完,不能并發處理下一次。Linux內核通過自旋鎖來完成這個工作,也即在handle_xxx_irq()系列函數開始時,獲取鎖,退出時,釋放鎖。Linux內核中斷子系統中至少使用到了4種鎖用于同步不同的資源,分別是:
desc->irq_data.chip->irq_bus_lock
desc->lock
irq_domain_mutex
gc_lock
前面說的鎖就是desc->lock的用處。irq_desc->lock類型為raw_spinlock_t,是一個自選鎖。首先,得看一下自旋鎖的特點和操作。自旋鎖有以下特點:
自旋,同時只能被一個進程持有,只能由持有該鎖的進程解鎖;
忙等,如果其他進程想要獲取鎖,則會一致處于等待狀態而無法做其他事情。
遞歸死鎖,以上兩條說明會有這個現象。
自旋鎖可以時用下列方法去操作:
spin_lock()和spin_unlock()
raw_spin_lock()和raw_spin_unlock()
raw_spin_lock_irq()和raw_spin_unlock_irq()
raw_spin_lock_irqsave()和raw_spin_unlock_irqrestore()
內核在desc->handle_irq到handle_irq_event()接口的過程中,通常這樣處理:
handle_xxx_irq
為什么要著要么做?避免同時對調用handle_irq_event(),奇怪的是在handle_irq_event()里,在irqd_set(IRQD_IRQ_INPROGRESS)后,又解鎖,這是為什么?顯然,內核并不是害怕兩個CPU同時執行了desc->action(除了handle_xxx_irq()系列函數,內核還有其他位置調用了desc->action),而是害怕在鎖與解鎖之間的操作被交叉操作,這些操作包括(以handle_level_irq()為例,上圖中沒有標出):mask_ack_irq(desc),向發出中斷的硬件ACK,并且暫時屏蔽該中斷;
如果irqd_irq_inprogress(),則執行irq_check_poll(),這表示該中斷可能正在“偽中斷”輪詢函數輪詢。(不知道有沒有這種情況,偽中斷輪詢函數沒有輪詢該中斷,desc->irq_data狀態卻不是IN_PROGRESS)
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
kstat_incr_irqs_this_cpu(irq, desc)更新cpu相關的中斷統計;
如果desc->action為空,或者desc->irq_data處于DISABLE的狀態,執行desc->istate |= IRQS_PENDING;
看樣子,desc->lock只保護desc->istate狀態變化的操作,硬件中斷處理中PIC對中斷線的一些操作,并不保護desc->action。這意味著
八、中斷線程
Linux內核的硬中斷處理程序(desc->handle_irq)將處理中斷信號發生后需要立即完成的事情,如網卡數據包的拷貝,響應中斷等,而將不太緊急的工作留給軟中斷去完成。在“六、istate狀態”一節中有提到一種ONESHOT狀態,就與中斷線程相關。使用ps aux | grep ksoftirqd命令,會發現系統中正運行著幾個(core總個數)ksoftirq進程:
[rock3@e4310 linux-stable]$ ps aux | grep ksoftirqd
root 3 0.0 0.0 0 0 ? S 11月12 0:37 [ksoftirqd/0]
root 13 0.0 0.0 0 0 ? S 11月12 0:34 [ksoftirqd/1]
root 18 0.0 0.0 0 0 ? S 11月12 0:23 [ksoftirqd/2]
root 23 0.0 0.0 0 0 ? S 11月12 0:22 [ksoftirqd/3]
這些進程就是內核啟動的中斷線程,他是一種內核線程,每個core都有一個,用于處理不太緊急的中斷事務,本節就討論從硬中斷處理轉向軟中斷處理的過程,這得從setup_irq()函數(用于設置desc->action)說起。先看下調用關系:softirq
當然創建的線程是irq_thread,不過irq_thread()函數將new->thread_fn又包裹了一層:irq_thread()通過irq_thread_fn()函數或者irq_forced_thread_fn()函數來調用new->thread_fn,而irq_thread_fn/irq_forced_thread_fn的補充操作為irq_finalize_oneshot(),即根據ONESHOT狀態來執行unmaks_irq()。irq_thread()函數還有不少其他操作,這里就不分析了。
通過kthread_create()創建內核線程,到wake_up_process()喚醒它,中斷子系統進入了軟中斷(softirq)的階段,該階段以內核線程的方式來處理中斷,被調度器調度。軟中斷通常有tasklet、工作隊列等具體方式。具體原理請詳見“軟中斷原理”一文。九、總結
本篇博客實際上是逆向學習Linux中斷子系統的過程,本意為看下irq_desc[]數組初始化的過程,結果有很多不明白的地方,就一路跟蹤過來,雖然也學到了一些內核中斷子系統的東西,但整體上對框架和細節的把握并不到位,估計以后還需要重新系統學習:
帶著問題學,逆向思維很重要。
逆向學習的過程應該是先縱向、再橫向,但本篇博客中很多地方是先縱橫不分,類似籃球里的“盯球不盯人”,不好。
假設一定要驗證,“我以為”是失敗之母;
用20%的時間學會整體框架,而不是用80%的時間了解細節。
從別人那學,會快,但不深刻。
不要學究。
十、遺留問題
1、關于鎖的問題,還是沒想明白,從找到interrupt[i]的內容,執行do_IRQ()函數,到handle_xxx_irq(),再到handle_irq_event(),在handle_xxx_irq()開始處加鎖,而在desc->action處解鎖,那么說ISR并不是臨界區,可以在兩個CPU上執行,而desc->lock只保護desc->istate變化以及PIC的一些操作,而不保護ISR。這個問題很重要,涉及到SMP架構下對中斷的處理,涉及到如何與軟中斷配合。
2、轉向軟中斷的一些細節。
參考資料:
1、Understanding Linux Kernel
2、Professional Linux Kernel Architecture
總結
以上是生活随笔為你收集整理的irq_desc操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: latex插入参考文献小技巧
- 下一篇: Chrome插件实现GitHub代码离线