[Kernel] Interrupt — Top Half

Brian Pan
5 min readDec 21, 2021
Linux interrupt management
- layers of interrupt management:
Hardware: Hardware <-> General Interrupt Controller
Process Arch Management: CPU interrupt
Interrupt Controller Mangement: IRQ Interrupt # mapping
- General Interrupt Controller workflow
1. When GIC detects an interrupt, mark the status of it into pending
2. Distributor Unit will assign the interrupt to target CPU
3. For each CPU, the Distributor Unit will pick up an interrupt based on the priority, and assign the interrupt to the CPU Interface module
4. CPU Interface module decides whether to dispatch the interrupt from a signal once some criteria fulfilled
5. CPU gets into interrupt and loads the hardware interrupt ID from register (GICC_IAR in ARM arch)
6.After processor finished the interrupt, it dispatches a End of Interrupt(EOI) signal to General Interrupt Controller
- /proc/interupts look into interrut stats
- request_irq/request_threaded_irq uses software interrupt Number
- struct irq_domain framework to support several interrupt controllers
- each GIC registers a irq_domain struct

Mapping between Software/Hardware interrupt number

Definition of the irq_domain

[include/linux/irqdomain.h]struct irq_domain {
struct list_head link; // links to global link list irq_domain_list
const char *name;
const struct irq_domain_ops *ops; // set of operation
void *host_data;
unsigned int flags;
// optional
struct device_node *of_node;
struct irq_domain_chip_generic *gc;
// reverse map data
irq_hw_number_t hwirq_max;
unsigned int revmap_direct_max_irq;
unsigned int revmap_size; // size of linear mapping
struct radis_tree_root revmap_tree;
unsigned int linear_revmap[];
}

Register the irq_domain

irq_domain_add_linear(node, gic_irqs, ops, gic);

Map of the hardware interrupt # into Linux Kernel IRQ

  • irq_of_parse_and_map(struct device_node *dev, int index)
  • - of_urq_parse_one(struct device_node *dev, int index, struct of_phandle_args *oirq): put hardware interrupt # into args[1]
  • - irq_create_of_mapping(struct of_phandle_args *oirq)
  • — gic_irq_domain_xlate(domain, irq_data->np, irq_data->args, irq_data->args_count, &hwirq, &type): hardware interrupt # translation
  • — irq_domain_alloc_irqs(domain, nr_irq, NUMA_NO_NODE, irq_data): 1. find empty slot in bitmap(alloated_irqs). 2. allocate struct irq_desc(where the mapping happens)
  • — — irq_domain_alloc_irqs_resurive(domain, virq, nr_irqs, arg): map the physical IRQ # with software IRQ #

Some structs during mapping

struct irq_desc {
struct irq_data irq_data;
const char *name;
irq_flow_handler_t handle_irq;
//..
}
struct irq_data {
unsigned int irq; // software irq #
unsigned long hwirq; // hardware irq #
struct irq_chip *chip; // a list of operations of interrupt controller
struct irq_domain *domain;
//..
}
// struct of the interrupt controller listing the set of the operations
struct irq_chip {
//...
}

Register Interrupt

When an interrupt from device happens, kernel executes interrupt handler to deal with it within interrupt context

Register function : static inline int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)/request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t thread_fn, unsigned long irqflags, const char *devname void *dev_id)

  • irq: software IRQ number
  • handler: primary handler
  • thread_fn: Function called from the IRQ handler thread If NULL, no IRQ thread is created
  • devname: name of the interrupt
  • dev_id: argument to interrupt handler. also, being used as the identifier of the devices sharing the interrupt

Interrupt context is when interrupt occurs goes interrupt handler and current process state saves into stack until kernel completes the interrupt. During the interrupt context the CPU can’t be slept nor preempted by other processes

IRQ Flag category

  • IRQF_ : describe the trait of the interrupt(IRQF_IRQPOLL, IRQF_ONESHOT)
  • IRQS_: locate in the member istate in struct irq_desc:(IRQS_ONESHOT, IRQS_PENDING)
  • IRQD_:show the status of the interrupt (IRQD_DISABLED,IRQD_INPROGRESS)

Useful functions during interrupt registration

irq_to_desc(irq) // using irq number to retreive struct *irq_desv 

structs during interrupt registration

//descriptor of the interrupt
struct irqaction{
irq_handler_t handler;
void *dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread; // interrupt thread struct
unsigned int irq;
unsigned int flags; // flags with prefix IRQF_
unsigned long thread_flags;
unsigned long thread_masl;
const char *name;
} ____cacheline_internodealigned_in_smp;

What are the steps during the registration

  • Get struct *irq_desc from software IRQ number
  • Initialize struct irqaction
  • Check if interrupt controller is valid (struct *irq_chip)
  • Verify nested case is available (irq_settings_is_nested_thread)
  • Create a kernel thread (irq-<irq>-<interrupt name> and schedule policy: SCHED_FIFO) while the interrupt is not nested and thread_fn isn’t NULL
  • Handle the case about sharing the interrupt: in IRQF_ONESHOT type, use thread_mask to manage the sharing interrupt
  • Deal with !shared case: set IRQ type, clear IRQD_IRQ_INPROGRESS
  • In shared interrupt case, insert the current irqaction into the tail of the link list(action) in struct irq_desc
  • Wake up the threading process through wake_up_process(new->thread) once thread_fn isn’t NULL

Low level interrupt workflow


Supervisor (svc) mode: A privileged mode entered whenever the CPU is reset or when an SVC instruction is executed.
Hardware does
-
Save Reg CPSR to Reg SPSR_irq
-
Modify CPSR and processor enters into processor mode
- Hardware automatically stops IRQ or FIQ
- Save Return Address into LR_irq
-
Load LR_irq into PC and return to the next instruction of interrupt point
Software view
- Vector table
mapping in arch/arm/kernel/vmlinux.lds.S
- __vector_(start|end), __stubs_(start|end) : abnormal vec stub
- early_trap_init copies 2 pages size of vector table(__vector, __stubs) from what defined in linker-script into the memory
- svc_entry macro saves regs information into Kernel stack and turns into SVC mode
- irq_handler: deal with actual interrupt process
- svc_exit: SPSR_irq loaded into SPSR. ldmia restores 15 regs val. SPSR loaded into CPSR. Return from the interrupt

IRQ handler

ARM processor responds to interrupt

.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
adr lr, BSYM(9997f)
ldr pc, [r1] # load pc to handle_arch_irq
#else
arch_irq_handler_default
#endif
9997:
.endm
  • handle_arch_irq points to gic_handle_irq in ARM arch
  • gic_handle_irq based on types of the interrupt from irqnr to dispatch corresponding functions

irqnr > 15 && irqnr < 1021 : handler_domain_irq(foc->domain, irqnr, regs) for externel devices

irqnr < 16: SGI interrupt

  • handle_irq executes irq_enter() to enter into interrupt context and increment the counter HARDWAREIRQ from preempt_count. Then, generic_handle_irq(irq) to continue interrupt process.
  • generic_handle_irq calls desc->handle_irq callback from struct irq_desc
  • this callback function is handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)in this case.
  • handle_fasteoi_irq calls handle_irq_event(irq) which clears pending flag and set flag IRQD_IRQ_INPROGRESS
  • Next, handle_irq_event(irq) continues calling handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action). In this function, there’s a while loop to iterate the rrqaction linklist.
do {
irqreturn_t res;
res = action->handler(irq, action->dev_id);
//....
switch(res) {
case IRQ_WAKE_THREAD:
// ...
__irq_wake_thread(irq, action); // wake up thread here
case IRQ_HANDLED:
flags |= action->flags;
default:
break;
}
retval |= res;
action = action->next; // iterate the link list
} while(action);
  • So far, we know how interrupts are dispatched. We shall look into execution function irq_thread(void *data)
struct callback_head on_exit_head on_exit_work;
struct irqaction *action = data;
struct irq_desc *desc = irq_to_desc(action->irq);
irqreturn_t (*handler_fn)(struct irq_desc *desc, struct irqaction *action);handler_fn = irq_thread_fn;init_task_work(&on_exit_work, irq_thread_dtor);
task_work_add(curent, &on_exit_work, false);
while(!irq_wait_for_interrupt(action)) {
irqreturn_t action_ret;

action_ret = handler_fn(desc, action);
if(action_ret == IRQ_HANDLED)
atomic_inc(&desc->threads_handled);
wake_threads_waitq(desc); // will call schedule() function to let cpu switch into other tasks
}
task_work_cancel(current, irq_thread_dtor);
  • After that, irq_exit() is executed to decrement the counter HARDWAREIRQ from preempt_count

References

--

--