ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Interrupt - High-level flow irq handler
    Linux/kernel 2023. 12. 27. 13:42

    글의 참고

    - https://linux-kernel-labs.github.io/refs/heads/master/lectures/interrupts.html

    - https://docs.kernel.org/core-api/genericirq.html

    - http://www.wowotech.net/irq_subsystem/High_level_irq_event_handler.html


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Overview

    " peripherals 이 interrupt 를 발생시키면, 대략적인 interrupt handling flow 는 다음과 같다. 참고로, 이 글에서는 주로 (3) 단계에 대해 다룰 것이다.

    1. interrupt 가 발생하면, 현재 CPU architecture 에 따른 hardware context 정보들이 stack 의 특정 영역에 저장된다(이 내용은 CPU archiecture 마다 다르다). 그리고, CPU architecture 마다 정의되어 있는 interrupt vector table 로 이동해서  interrupt handler 를 수행한다(예전 리눅스 커널에서는 machine driver 라고 하면, CPU architecture 에 종속적인 driver 를 의미한다)

    2. machine driver 의 interrupt handler 는 peripheral interrupt 에 대응하는 hwirq 를 알아낸 뒤, 해당 hwirq 가 속한 irq domain 을 통해서 hwirq 를 IRQ number 로 translate 한다.

    3. 그리고, IRQ number 에 대응하는 `high-level irq flow handler` 를 호출한다. high-level handler 에서는 물리적인 interrupt controller 를 컨트롤함으로써, 전반적인 hardware level 의 interrupt handling 작업을 수행한다(예를 들면, interrupt nesting, preemption 등). 이후에, 실제 peripheral 이 발생시킨 interrupt 를 처리하기 위해서 interrupt descriptor 의 IRQ action list 에서 peripheral 이 등록한 driver irq handler 를 호출한다.      

    4. driver irq handler 까지 마무리되면, interrupt 가 발생하기 전에 저장 해놓았던 CPU architecture 에 따른 hardware context 를 복구한다.

     

    " high-level irq flow handler 라는 것은 무엇이며, 구체적으로 어떤 기능을 하는지 알아보자.

     

     

     

    - How to enter the high-level irq flow handler

    1. Interrupt handling process from CPU architecture to machine driver

    " 이 글에서 CPU architecture 관련 예시를 들 경우, CONFIG_MULTI_IRQ_HANDLER 컨피그가 설정되어 있으며, 기본적으로 ARMv7 architecture 인 점을 참고하자. 아래 코드는 ARMv7 의 interrupt handling process(CPU 레벨에서 처리하는 단계) 에서 machine interrupt driver(interrupt controller 레벨에서 처리하는 단계) 코드로 넘어가는 것을 보여준다.

    // arch/arm/kernel/entry-armv.S - v3.17
    /*
     * Interrupt handling.
     */
    	.macro	irq_handler
    #ifdef CONFIG_MULTI_IRQ_HANDLER
    	ldr	r1, =handle_arch_irq
    	mov	r0, sp
    	adr	lr, BSYM(9997f)
    	ldr	pc, [r1]
    #else
    	arch_irq_handler_default
    #endif
    9997:
    	.endm

     

     

    " CPU 의 interrupt handling process 에서 interrupt controller 의 interrupt handling(machine driver) process 을 거치지 않고, 직접적으로 kernel 의 interrupt handling process 로 jump 하는 것은 사실상 불가능하다. 왜냐면, 실제 interrupt handling process 는 interrupt controller 단계까지만 호출되는 것처럼 보이기 때문이다. 왜냐면, interrupt controller 단계에서 리눅스 커널의 interrupt handling module 이 제공하는 interrupt handling functions 들을 사용해서, generic interrupt handlers 를 호출하고 이 시점에 전반적인 interrupt handling process 가 완료되기 때문이다.

     

     

    " 여기서 `interrupt controller 단계` 라는 말은 hardware interrupt system design 과 관련이 있다. 예를 들어, 시스템에 몇 개의 interrupt controllers 가 존재하는지? 각 interrupt controller 들은 어떻게 컨트롤 해야 하는지? 이들은 어떤 cascading 방식으로 연결되어 있는지? 등이 있다. 위와 같은 내용들을 고려해서 만들어진 driver 를 `machine interrupt driver` 라고 부른다(device tree 가 나오기 전에 사용되던 소스 코드를 board, machine, arch, cpu 로 분리해서 관리하는 방식을 의미한다).

     

     

    " 위에 코드에서 `CONFIG_MULTI_IRQ_HANDLER` 컨피그가 설정되면, ARMv7 의 interrupt handling process 는 `handle_arch_irq` 로 바로 jump 한다. 그렇다면, ARMv7 에서 handle_arch_irq 의 정체는 뭘까? 시스템에 딱 한 종류의 interrupt controller 만 있으면(예를 들어, arm 같은 경우 GICs 들만 있어야 한다. GICs 가 여러 개 있는 건 상관없다. interrupt controller 의 종류가 중요하다. 예를 들어, broadcom 의 bcm2835 는 arm 을 사용하지만, interrupt controller 는 GIC 를 사용하지 않고, 자체 interrupt controller 를 사용하고 있는 것으로 보인다. 그래서, v3.17 /drivers/irqchip/irq-bcm2835.c 파일을 보면, 자체적으로 set_handle_irq(bcm2835_handle_irq) 함수를 통해서 자체 interrupt controller hanlder 를 등록한다. 혹은 broadcom 에서 GIC 를 사용하더라도 자체적으로 GIC 를 컨트롤하기 위한 interrupt controller hanlder 를 만든 것일 수 도 있다. 결론적으로, 한 시스템에 arm 과 broadcom 각각의 interrupt controller 가 공존하지 않는다면, set_handle_irq() 함수가 중복되서 호출될 일은 없을 것이다), interrupt controller(GICv2) 가 초기화 되는 시점에 handle_arch_irq(`gic_handle_irq`) 가 interrupt controller handler 로 등록된다. 코드는 다음과 같다.

    // drivers/irqchip/irq-gic.c - v3.17
    void __init gic_init_bases(unsigned int gic_nr, int irq_start,
    			   void __iomem *dist_base, void __iomem *cpu_base,
    			   u32 percpu_offset, struct device_node *node)
    {
    	....
    
    	if (gic_nr == 0) {
        	....
    		set_handle_irq(gic_handle_irq);
    	}
    
    	....
    }
    #ifdef CONFIG_MULTI_IRQ_HANDLER
    // arch/arm/kernel/irq.c - v3.17
    void __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
    {
    	if (handle_arch_irq)
    		return;
    
    	handle_arch_irq = handle_irq;
    }
    #endif

     

     

    " `gic_nr` 은 시스템에 존재하는 GICs 들의 개수를 나타낸다. GICv2 가 초기화 되는 시점에, 시스템에 다수의 GICs 가 존재할 경우, GIC 가 커널에게 발견되서 초기화가 완료될 때마다, gic_nr 가 1 씩 증가한다. 이 말은, 첫 번째 GIC 가 발견되는 시점까지는 gic_nr 이 0 임을 의미한다(첫 번째 GIC 의 초기화가 완료되어야 gic_nr 값이 증가한다). 함수 포인터 handle_arch_irq 는 per-CPU 변수가 아니기 때문에, 한 번만 설정해도 충분하다(interrupt controller 는 CPU 마다 존재하는 디바이스가 아니다. 그리고, handle_arch_irq 는 interrupt controller handler 이기 때문에, per-CPU 변수로 선언할 필요가 없다).

     

     

    " 만약, 시스템에 다양한 종류의 interrupt controllers 들이 사용될 경우(예를 들어, samsung s3c2451 SoC 에는 2 종류의 interrupt controllers 들이 사용된다. 첫 번째로, GPIO 를 interrupt 로 사용하는 interrupt controller(GPIO controller) 과 기존과 동일한 interrupt signal line 을 interrupt 로 사용하는 interrpt controller 가 있다), interrupt controller code 에서 handle_arch_irq 를 설정하는 것보다는 machine driver code(`setup_arch()` 함수) 에서 설정하는게 더 적합할 수 있다.

    // arch/arm/kernel/setup.c - v4.14
    void __init setup_arch(char **cmdline_p)
    {
    	const struct machine_desc *mdesc;
    
    	....
    	mdesc = setup_machine_fdt(__atags_pointer);
    	if (!mdesc)
    		mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
    	....
    
    #ifdef CONFIG_MULTI_IRQ_HANDLER
    	handle_arch_irq = mdesc->handle_irq;
    #endif
    
    	....
    }

     

     

    " CONFIG_MULTI_IRQ_HANDLER 컨피그는 run-time 에 interrupt controller handler 를 바꿀 수 있다는 것을 의미한다. 그런데, run-time 에 interrupt controller handler 를 변경해야 할 일이 생길까? 실제 applications 들이 동작할 때, 바뀌는 일은 없다고 보면 된다. 대신, 커널이 초기화 되는 시점에만 set_handle_irq() 함수를 통해 interrupt controller handler 가 동적으로 변경이 된다. 만약, interrupt controller handler 를 동적으로 변경할 수 없다면, kernel image 를 만들 때, interrupt controller handler 를 변경하는 코드가 하드 코딩 되어 있어야 한다. 예를 들어, exynos 는 자체 interrupt controller 를 위한 interrupt controller handler 등록 작업을 하드 코딩하고, broadcom 또한 마찬가지 일 것이다. 이 과정을 set_handle_irq() 함수를 제공함으로써, 커널 초기화 시점에 각 interupt controller 타입에 따라 동적으로 interrupt controller handler 를 변경할 수 있게 됬다. 

     

     

     

    2. interrupt controller handler

    " 아래 코드를 해석할 때는 시스템에 2 개의 GICs 가 존재하며, 2 개의 interrupt controllers 들은 cascading 방식으로 연결되어 있다고 가정한다.

    // drivers/irqchip/irq-gic.c - v3.17
    static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
    {
    	u32 irqstat, irqnr;
    	struct gic_chip_data *gic = &gic_data[0]; // root GIC 의 hardware description
    	void __iomem *cpu_base = gic_data_cpu_base(gic); // GIC registers 읽어야 하기 때문에, CPU address space 에 할당된 root GIC address 를 얻어냄
    
    	do {
    		irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK); // hwirq 를 추출
    		irqnr = irqstat & GICC_IAR_INT_ID_MASK;
    
    		if (likely(irqnr > 15 && irqnr < 1021)) { // GIC 의 PPIs & SPIs 를 나타냄.
    			irqnr = irq_find_mapping(gic->domain, irqnr); // hwirq 를 IRQ number 로 convert
    			handle_IRQ(irqnr, regs); // IRQ number 에 대한 interrupt handling
    			continue;
    		}
    		if (irqnr < 16) { // GIC SGIs 는 0 ~ 15 번에 할당됨. 즉, 아래 코드는 IPI interrupt handling 코드를 의미
    			writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
    #ifdef CONFIG_SMP
    			handle_IPI(irqnr, regs);
    #endif
    			continue;
    		}
    		break;
    	} while (1);
    }

     

     

    " GICv2 의 handle_irq() 함수와 관련된 더 많은 정보는 이 글을 참고하도록 하자.

    // arch/arm/kernel/irq.c - v3.17
    /*
     * handle_IRQ handles all hardware IRQ's.  Decoded IRQs should
     * not come via this function.  Instead, they should provide their
     * own 'handler'.  Used by platform code implementing C-based 1st
     * level decoding.
     */
    void handle_IRQ(unsigned int irq, struct pt_regs *regs)
    {
        ....
    	
    	/*
    	 * Some hardware gives randomly wrong interrupts.  Rather
    	 * than crashing, do something sensible.
    	 */
    	if (unlikely(irq >= nr_irqs)) { // maximum interrupt descriptors 개수보다 irq 가 클 경우, kenrel panic 보다는 경고 메시지 출력
    		if (printk_ratelimit())
    			printk(KERN_WARNING "Bad IRQ%u\n", irq);
    		ack_bad_irq(irq);
    	} else {
    		generic_handle_irq(irq); // 실제 IRQ number 처리 루틴
    	}
    	....
    }

     

     

     

    3. High-level irq flow handler

    " v3.17 에서는 high-level irq flow handler 를 호출하는 코드가 상당히 심플하다. 결국, high-level irq flow handler 의 실체는 `struct irq_desc->handle_irq` 라는 것을 알 수 있다.

    // kernel/irq/irqdesc.c - v3.17
    /**
     * generic_handle_irq - Invoke the handler for a particular irq
     * @irq:	The irq number to handle
     *
     */
    int generic_handle_irq(unsigned int irq)
    {
    	struct irq_desc *desc = irq_to_desc(irq);
    
    	if (!desc)
    		return -EINVAL;
    	generic_handle_irq_desc(irq, desc);
    	return 0;
    }
    // include/linux/irqdesc.h - v3.17
    /*
     * Architectures call this to let the generic IRQ layer
     * handle an interrupt. If the descriptor is attached to an
     * irqchip-style controller then we call the ->handle_irq() handler,
     * and it calls __do_IRQ() if it's attached to an irqtype-style controller.
     */
    static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
    {
    	desc->handle_irq(irq, desc);
    }

     

Designed by Tistory.