Interrupts -2- (irq chip)

 

IRQ Chip

  • 여러 제조사의 인터럽트 컨트롤러들은 각 인터럽트 라인의 통제 방법이 각각 다르다.
  • 리눅스 IRQ 코어 레이어에서 irq chip은 다양한 인터럽트 컨트롤러의 라인별 제어 액션을 통일하여 처리하도록 구현되었다.
    • 인터럽트 라인마다  마스킹 또는 set/clear 동작을 수행하는 서비스를 제공한다.
    • irq_chip에 있는 여러 개의 후크 포인터에 연결된 콜백 함수를 통해 처리한다.
      • irq_mask
      • irq_unmask
      • irq_set_type
    • 각 제조사의 인터럽트 컨트롤러 디바이스 드라이버에는 irq_chip의 후크에 연결될 처리 함수들을 제공해야 한다.
  • 인터럽트 라인의 마스킹 및 set/clear 처리 유형 등의 처리가 각각 다른 경우 irq_chip을 분리하여 구성할 수 있다.
  • 인터럽트 라인 각각 마다 1개의 irq_chip을 지정할 수 있다.
    • irq_set_chip()

 

 

Device Tree로 hierarchy irq domain 구성

샘플 보드인 rpi2를 대상으로 설명하였기에 소스는 rpi의 커널 v4.4 이상을 대상으로한다.

다음 그림은 rpi2에서 설정되는 5개의 irq_chip을 보여준다.

  • 실제 rpi2 드라이버는 hierarchy irq domain을 아직 지원하지 않고 irq chip만 부모 관계가 있다.
  • hierarchy irq domain 구현 사례
    • apic를 사용하는 x86
    • arm에서 gic, gic v2m, gic v3, nvic 등

3 개의 인터럽트 컨트롤러

  • gpio pinctrl 디바이스
    • gpio를 사용하는 pinctrl 디바이스 드라이버에 구현되어 있다.
      • 인터럽트 컨트롤러 디바이스 드라이버가 아니므로 IRQCHIP_DECLARE()를 사용하지 않았으므로 __chip_id_of_table 섹션에 인터럽트 컨트롤러 엔트리가 등록되지 않는다.
        • 인터럽트 초기화 루틴 irqchip_init()에서 초기화 함수를 호출하지 않는다.
    • DT 스크립트에 “gpio-controller;” 및 “interrupt-controller;” 두 가지 기능의 컨트롤러가 동작한다.
      • 메인 기능이 gpio를 사용하는 pinctrl 디바이스이다.
      • 추가 기능으로 gpio에서 인터럽트 라인 컨트롤 기능을 수행할 수 있다.
        • gpio pin 들을 인터럽트 라인으로 사용할 수 있어서 이들에 대한 set/clear 등의 마스킹 동작을 위해 irq_chip이 구현되어 있다.
  • GPU 인터럽트 컨트롤러
    • 0 ~ 2번 뱅크를 입력으로 받아들이며 각각 최대 32개의 인터럽트 라인을 가지고 있다.
    •  rpi에서도 사용하는 인터럽트 컨트롤러
  • LOCAL 인터럽트 컨트롤러
    • rpi2에서 SMP core 간 IPI 등의 소프트 인터럽트 처리 등을 위해 별도로 추가되었다.
    • 3 개의 irq_chip으로 구성
      • gpu 인터럽트를 처리하기 위한 irq_chip
      • 4개의 timer 인터럽트를 처리하기 위한 irq_chip
      • 1개의 pmu 인터럽트를 처리하기 위한 irq_chip

 

Device Tree 기반의 인터럽트 컨트롤러 초기화

irqchip_init()

drivers/irqchip/irqchip.c

void __init irqchip_init(void)
{
        of_irq_init(__irqchip_of_table);
}

__irqchip_of_table 섹션에 있는 of_device_id 구조체들을 검사하여 매치되는 인터럽트 컨트롤러 초기화 함수를 찾아 호출한다.

  • rpi2:
    • 2 개의 인터럽트 컨트롤러 드라이버 호출
      • bcm2836_arm_irqchip_l1_intc_of_init()
      • bcm2835_armctrl_of_init()

 

of_irq_init()

drivers/of/irq.c

/**
 * of_irq_init - Scan and init matching interrupt controllers in DT
 * @matches: 0 terminated array of nodes to match and init function to call
 *
 * This function scans the device tree for matching interrupt controller nodes,
 * and calls their initialization functions in order with parents first.
 */
void __init of_irq_init(const struct of_device_id *matches)
{
        struct device_node *np, *parent = NULL;
        struct intc_desc *desc, *temp_desc;
        struct list_head intc_desc_list, intc_parent_list;

        INIT_LIST_HEAD(&intc_desc_list);
        INIT_LIST_HEAD(&intc_parent_list);

        for_each_matching_node(np, matches) {
                if (!of_find_property(np, "interrupt-controller", NULL) ||
                                !of_device_is_available(np))
                        continue;
                /*
                 * Here, we allocate and populate an intc_desc with the node
                 * pointer, interrupt-parent device_node etc.
                 */
                desc = kzalloc(sizeof(*desc), GFP_KERNEL);
                if (WARN_ON(!desc))
                        goto err;

                desc->dev = np;
                desc->interrupt_parent = of_irq_find_parent(np);
                if (desc->interrupt_parent == np)
                        desc->interrupt_parent = NULL;
                list_add_tail(&desc->list, &intc_desc_list);
        }

Device Tree에 있는 인터럽트 컨트롤러와 매치되는 인터럽트 컨트롤러의 초기화 함수를 호출한다. 단 2개 이상의 인터럽트 컨트롤러를 사용하는 경우 상위 인터럽트 컨트롤러부터 초기화한다.

  • 코드 라인 14~15에서 임시로 사용되는 리스트 2개를 초기화한다.
    • intc_desc_list:
      • irqchip 테이블의 이름과 Device Tree의 인터럽트 컨트롤러 드라이버명(compatible)으로 매치되어 구성된 desc 구조체 리스트
        • 보통 ARM 임베디드 시스템은 보통 1 ~ 2개의 인터럽트 컨트롤러를 사용한다.
    • intc_parent_list:
      • 초기화 성공한 인터럽트 컨트롤러의 desc 구조체 리스트로 최상위 인터럽트 컨트롤러가 가장 앞으로 구성된다.
  • 코드 라인 17~20에서 DTB 트리의 모든 노드에서 irqchip 테이블의 이름 과 Device Tree의 인터럽트 컨트롤러 드라이버명(compatible)으로 일치한 노드들에서 “interrupt-controller” 속성을 못찾았거나 “status”가 “ok”가 아닌 경우 skip 한다.
  • 코드 라인 25~27에서 desc 구조체를 할당받아 온다.
  • 코드 라인 29에서 desc->dev에 인터럽트 컨트롤러 노드를 가리키게한다.
  • 코드 라인 30~32에서 desc->interrupt_parent에 상위 인터럽트 컨트롤러 노드를 가리키게 한다. 만일 상위 인터럽트 컨트롤러가 자기 자신을 가리키면 null을 대입한다.
    • 상위 인터럽트 컨트롤러를 찾는 법
      • “interrupt-parent” 속성이 위치한 노드가 가리키는 phandle 값을 알아온 후 그 값으로 노드를 검색하여 알아온다.
  • 코드 라인 33에서 intc_desc_list 임시 리스트의 후미에 desc를 추가한다.

 

        /*
         * The root irq controller is the one without an interrupt-parent.
         * That one goes first, followed by the controllers that reference it,
         * followed by the ones that reference the 2nd level controllers, etc.
         */
        while (!list_empty(&intc_desc_list)) {
                /*
                 * Process all controllers with the current 'parent'.
                 * First pass will be looking for NULL as the parent.
                 * The assumption is that NULL parent means a root controller.
                 */
                list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
                        const struct of_device_id *match;
                        int ret;
                        of_irq_init_cb_t irq_init_cb;

                        if (desc->interrupt_parent != parent)
                                continue;

                        list_del(&desc->list);
                        match = of_match_node(matches, desc->dev);
                        if (WARN(!match->data,
                            "of_irq_init: no init function for %s\n",
                            match->compatible)) {
                                kfree(desc);
                                continue;
                        }

                        pr_debug("of_irq_init: init %s @ %p, parent %p\n",
                                 match->compatible,
                                 desc->dev, desc->interrupt_parent);
                        irq_init_cb = (of_irq_init_cb_t)match->data;
                        ret = irq_init_cb(desc->dev, desc->interrupt_parent);
                        if (ret) {
                                kfree(desc);
                                continue;
                        }

                        /*
                         * This one is now set up; add it to the parent list so
                         * its children can get processed in a subsequent pass.
                         */
                        list_add_tail(&desc->list, &intc_parent_list);
                }

                /* Get the next pending parent that might have children */
                desc = list_first_entry_or_null(&intc_parent_list,
                                                typeof(*desc), list);
                if (!desc) {
                        pr_err("of_irq_init: children remain, but no parents\n");
                        break;
                }
                list_del(&desc->list);
                parent = desc->dev;
                kfree(desc);
        }

        list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
                list_del(&desc->list);
                kfree(desc);
        }
err:
        list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
                list_del(&desc->list);
                kfree(desc);
        }
}
  • 코드 라인 6에서 intc_desc_list 리스트의 모든 엔트리가 없어질 때까지 루프를 돈다.
  • 코드 라인 12에서 intc_desc_list 리스트의 desc 엔트리에 대해 루프를 돈다.
  • 코드 라인 17~18에서 남은 인터럽트 컨트롤러 중 최상위가 아닌 경우 skip 한다.
  • 코드 라인 20에서 initc_desc_list에서 하나를 제거한다.
  • 코드 라인 21~27에서 __irqchip_of_table 에서 해당 드라이버와 매치되는 of_device_id를 가져오고 매치 되지 않는 경우 “of_irq_init: no init function for %s”라는 경고 메시지를 출력하고 skip 한다.
  • 코드 라인 29~31에서 매치되는 인터럽트 컨트롤러 드라이버 이름과 주소 그리고 부모 인터럽트 컨트롤러 주소 정보를 출력한다.
    • “of_irq_init: init <driver_name> @ <addr>, parent <addr>”
  • 코드 라인 32에서 매치된 of_device_id 구조체의 data에서 irq 초기화를 위한 콜백함수를 알아온다.
  • 코드 라인 33~37에서 IRQ 초기화 함수를 호출한다.  만일 초기화가 실패하면 skip 한다.
  • 코드 라인 43에서 초기화 성공한 인터럽트 컨트롤러의 desc 구조체 정보를 intc_parent_list 라는 이름의 임시 리스트에 추가한다.
    • 부모 없는 child 인터럽트 컨트롤러가 있는지 오류 체크를 위함
  • 코드 라인 47~52에서 초기화 성공한 인터럽트 컨트롤러가 없으면 “of_irq_init: children remain, but no parents” 메시지를 출력하고 처리를 중단한다.
    • A: 조부모 IC <- B: 부모 IC, C: 아들 IC 구성 시
      • A-> B -> C 순서대로 초기화되어야 한다.
  • 코드 라인 53~55에서 임시 변수 parent에 처리한 인터럽트 컨트롤러 노드를 가리키게 하고 intc_desc_list 라는 이름의 임시 리스트에서 제거한다.
  • 코드 라인 58~66에서 임시로 사용했던 intc_parent_list 및 intc_desc_list에 등록된 desc 구조체를 모두 할당 해제한다.

 

다음 그림은 2개의 인터럽트 컨트롤러에 대해 초기화 함수가 호출되는 것을 보여준다.

 

irqchip 테이블

__irqchip_of_table

  • .init.data 섹션에서 .dtb.init.rodata 바로 뒤에 위치한다.
  • 엔트리들은 of_device_id 구조체로 구성된다.
  • __irqchip_of_table = .; *(__irqchip_of_table) *(__irqchip_of_table_end)

 

드라이버 내에서 irqchip 초기화 선언

rpi2) irqchip 초기화 함수가 등록되는 예

drivers/irqchip/irq-bcm2835.c

IRQCHIP_DECLARE(bcm2835_armctrl_ic, "brcm,bcm2835-armctrl-ic", armctrl_of_init);
  • __irqchip_of_table 섹션 위치에 of_device_id 구조체가 다음과 같이 정의 된다.
    • .compatible = “brcm,bcm2835-armctrl-ic”
    • .data = armctrl_of_init()

 

IRQCHIP_DECLARE()

drivers/irqchip/irqchip.h

/*
 * This macro must be used by the different irqchip drivers to declare
 * the association between their DT compatible string and their
 * initialization function.
 *      
 * @name: name that must be unique accross all IRQCHIP_DECLARE of the
 * same file.
 * @compstr: compatible string of the irqchip driver
 * @fn: initialization function
 */
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)

irq_chip 디바이스 드라이버를 구성할 때 사용하는 선언문으로 여기서 만들어진 of_device_id 구조체는 __irqchip_of_table 섹션에 등록된다.

 

include/linux/of.h

#define OF_DECLARE_2(table, name, compat, fn) \
                _OF_DECLARE(table, name, compat, fn, of_init_fn_2)

 

include/linux/of.h

#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  }

 

Chip 정보 설정

irq_set_chip()

kernel/irq/chip.c

/**
 *      irq_set_chip - set the irq chip for an irq
 *      @irq:   irq number
 *      @chip:  pointer to irq chip description structure
 */
int irq_set_chip(unsigned int irq, struct irq_chip *chip)
{
        unsigned long flags;
        struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);

        if (!desc)
                return -EINVAL;

        if (!chip)
                chip = &no_irq_chip;

        desc->irq_data.chip = chip;
        irq_put_desc_unlock(desc, flags);
        /*
         * For !CONFIG_SPARSE_IRQ make the irq show up in
         * allocated_irqs.
         */
        irq_mark_irq(irq);
        return 0;
}
EXPORT_SYMBOL(irq_set_chip);

irq 디스크립터에 해당 irq를 마스킹 또는 set/clear 동작을 시킬 수 있는 irq chip 정보를 설정한다.

  • 코드 라인 14~15에서 만일 인터럽트 컨트롤러 chip을 사용하지 않은 경우  no_irq_chip(name=”none”) 구조체 정보를 대신 대입한다.
  • 코드 라인 17에서 irq 디스크립터의 chip_data에 chip 정보를 기록한다.
  • 코드 라인 23에서 Sparse IRQ를 사용하지 않는 경우allocated_irqs 비트맵에 요청 irq에 해당하는 비트를 mark하여 irq가 사용됨을 표시한다.

 

irq_mark_irq()

kernel/irq/irqdesc.c

void irq_mark_irq(unsigned int irq)
{       
        mutex_lock(&sparse_irq_lock);
        bitmap_set(allocated_irqs, irq, 1);
        mutex_unlock(&sparse_irq_lock);
}

Sparse IRQ를 사용하지 않는 경우 전역 allocated_irqs 비트맵을 사용하여 irq 번호에 해당하는 비트를 설정하여 irq가 사용됨을 표시한다.

 

irq 핸들러 설정

__irq_set_handler()

kernel/irq/chip.c

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);

        if (!desc)
                return;

        if (!handle) {
                handle = handle_bad_irq;
        } else {
                struct irq_data *irq_data = &desc->irq_data;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
                /*
                 * With hierarchical domains we might run into a
                 * situation where the outermost chip is not yet set
                 * up, but the inner chips are there.  Instead of
                 * bailing we install the handler, but obviously we
                 * cannot enable/startup the interrupt at this point.
                 */
                while (irq_data) {
                        if (irq_data->chip != &no_irq_chip)
                                break;
                        /*
                         * Bail out if the outer chip is not set up
                         * and the interrrupt supposed to be started
                         * right away.
                         */
                        if (WARN_ON(is_chained))
                                goto out;
                        /* Try the parent */
                        irq_data = irq_data->parent_data;
                }
#endif
                if (WARN_ON(!irq_data || irq_data->chip == &no_irq_chip))
                        goto out;
        }

        /* Uninstall? */
        if (handle == handle_bad_irq) {
                if (desc->irq_data.chip != &no_irq_chip)
                        mask_ack_irq(desc);
                irq_state_set_disabled(desc);
                desc->depth = 1;
        }
        desc->handle_irq = handle;
        desc->name = name;

        if (handle != handle_bad_irq && is_chained) {
                irq_settings_set_noprobe(desc);
                irq_settings_set_norequest(desc);
                irq_settings_set_nothread(desc);
                irq_startup(desc, true);
        }
out:
        irq_put_desc_busunlock(desc, flags);
}
EXPORT_SYMBOL_GPL(__irq_set_handler);

irq descriptor에 handle과 이름을 대입하고 activate 한다. activate 시 관련 콜백 함수들을 호출한다.

  • 코드 라인 6~10에서 irq 디스크립터에 buslock을 걸고 가져온다. 만일 실패하는 경우 함수를 빠져나간다.
  • 코드 라인 12~13 인수 handle이 지정되지 않은 경우 handle_bad_irq() 함수를 대입한다.
  • 코드 라인 14~39에서 커널이 irq hierarchy를 지원하는 경우 irq_data 체인을 루프를 돌며 처음 발견되는 chip 정보를 찾는다.
    • 하나의 irq가 2 개 이상의 인터럽트 컨틀롤러 칩을 거치는 경우 irq_data가 parent irq_data에 연결되어 구성된다.
  • 코드 라인 42~47에서 지정된 핸들러 함수가 없는 경우 irq 상태를 disable로 변경하고 depth=1로 설정한다. 만일 chip 정보가 지정되지 않은 경우 irq를 mask 상태로 바꾼다.
    • irq_desc->irq_data->state_use_accessors에 IRQD_IRQ_DISABLED 추가 (no handler)
    • irq_desc->irq_data->state_use_accessors에 IRQD_IRQ_MASKED 추가 (no chip)
  • 코드 라인 48~49에서 핸들러 함수와 이름을 대입한다.
  • 코드 라인 51~56에서 핸들러 함수가 지정되었고 chain 걸린 경우 irq 상태를 noprobe, norequest, nothread로 바꾸고 irq를 enable하고 activate 한다.
    • irq_desc->irq_data->state_use_accessors에 _IRQ_NOPROBE 추가
    • irq_desc->irq_data->state_use_accessors에 _IRQ_NOREQUEST 추가
    • irq_desc->irq_data->state_use_accessors에 _IRQ_NOTHREAD 추가

 

chip 및 IRQ 핸들러 설정

irq_set_chip_and_handler()

include/linux/irq.h

static inline void irq_set_chip_and_handler(unsigned int irq, struct irq_chip *chip,
                                            irq_flow_handler_t handle)
{
        irq_set_chip_and_handler_name(irq, chip, handle, NULL);
}

irq 디스크립터에 chip, handle 정보를 대입한다. 단 이름(name)에는 null을 대입한다.

 

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);
}
EXPORT_SYMBOL_GPL(irq_set_chip_and_handler_name);

irq 디스크립터에 chip 정보를 대입한다. 그리고 핸들러와 이름을 대입하다.

 

per-cpu 기반 디바이스 및 IRQ 핸들러 설정

irq_set_percpu_devid()

kernel/irq/irqdesc.c

int irq_set_percpu_devid(unsigned int irq)
{
        struct irq_desc *desc = irq_to_desc(irq);

        if (!desc)
                return -EINVAL;

        if (desc->percpu_enabled) 
                return -EINVAL;
        
        desc->percpu_enabled = kzalloc(sizeof(*desc->percpu_enabled), GFP_KERNEL);
                        
        if (!desc->percpu_enabled)
                return -ENOMEM;

        irq_set_percpu_devid_flags(irq);
        return 0;
}

요청 irq를 per-cpu(코어별로 라우팅되는) 처리 용도로 준비한다.

  • 코드 라인 3~6에서 요청 irq번호에 맞는 irq 디스크립터가 없는 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 8~9에서 이미 per-cpu로 설정된 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 11~14에서 cpu 비트맵을 할당하여 percpu_enabled에 대입한다. 할당이 실패한 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 16에서 irq 상태로 noautoen, per_cpu, nothread, noprobe, per_cpu_devid 플래그를 설정한다.

참고: genirq: Add support for per-cpu dev_id interrupts 

 

irq_set_percpu_devid_flags()

include/linux/irq.h

static inline void irq_set_percpu_devid_flags(unsigned int irq)
{
        irq_set_status_flags(irq,
                             IRQ_NOAUTOEN | IRQ_PER_CPU | IRQ_NOTHREAD |
                             IRQ_NOPROBE | IRQ_PER_CPU_DEVID);
}

irq 상태로 noautoen, per_cpu, nothread, noprobe, per_cpu_devid 플래그를 설정한다.

 

인터럽트 핸들러 호출

인터럽트 호출 시 단계

  • 핸들러 설정
    • 핸들러 함수를 전역 handle_arch_irq 함수 포인터에 저장
  • irq exception 시
    • arch/arm/entry-armv.S – irq_handler 매크로 함수가 전역 handle_arch_irq 함수 포인터에 저장된 함수를 호출한다.
  • 여러 형태의 핸들러 함수 들
    • handle_percpu_irq()
      • per-cpu용 인터럽트 핸들러
      • ack -> handle_irq_event_percpu() 호출 -> eoi
    • handle_level_irq()
      • 레벨 타입 트리거 설정된 경우 사용할 인터럽트 핸들러
      • handle_irq_event() 호출 -> cond-unmask
    • handle_simple_irq()
      • handle_irq_event() 호출
    • handle_fasteoi_irq()
      • transparent 컨트롤러들을 위한 핸들러
      • oneshot일때 mask -> preflow -> handle_irq_event() 호출 -> cond-unmask eoi
    • handle_edge_irq()
      • 엣지 타입 트리거 설정된 경우 사용할 인터럽트 핸들러
      • ack -> handle_irq_event() 호출
    • handle_edge_eoi_irq()
      • 엣지 eoi 타입 트리거 설정된 경우 사용할 인터럽트 핸들러
      • handle_irq_event() 호출 -> eoi
    •  handle_nested_irq()
      •  irq thread로부터의 nested irq 핸들러
      • ack -> action->thread_fn() 호출 -> eoi
    • handle_percpu_devid_irq()
      • per-cpu용 인터럽트 핸들러
      • ack -> action->handler() 호출 -> eoi

 

다음 그림은 타이머 인터럽트 핸들러가 호출되는 과정을 보여준다.

 

핸들러 함수 -1-

handle_level_irq()

kernel/irq/chip.c

/**
 *      handle_level_irq - Level type irq handler
 *      @irq:   the interrupt number
 *      @desc:  the interrupt description structure for this irq
 *
 *      Level type interrupts are active as long as the hardware line has
 *      the active level. This may require to mask the interrupt and unmask
 *      it after the associated handler has acknowledged the device, so the
 *      interrupt line is back to inactive.
 */
void    
handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
        raw_spin_lock(&desc->lock);
        mask_ack_irq(desc);

        if (!irq_may_run(desc))
                goto out_unlock;

        desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
        kstat_incr_irqs_this_cpu(irq, desc);

        /*
         * If its disabled or no action available
         * keep it masked and get out of here
         */ 
        if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
                desc->istate |= IRQS_PENDING;
                goto out_unlock;
        }                               
                                        
        handle_irq_event(desc);
                        
        cond_unmask_irq(desc);

out_unlock:                
        raw_spin_unlock(&desc->lock);
}               
EXPORT_SYMBOL_GPL(handle_level_irq);

레벨 타입의 트리거를 가진 인터럽트 핸들러로 인터럽트 처리 전에 mask와 ack 처리를 수행하고 인터럽트 처리 후에 조건부로 unmask를 수행한다.

  • 코드 라인 15에서 인터럽트 컨트롤러를 통해 mask와 ack 처리를 한다. irqd는 masked 상태로 설정한다.
  • 코드 라인 17~18에서 irq의 실행이 곧 준비되지 않을 것 같으면 인터럽트 처리를 포기하고 빠져나간다.
  • 코드 라인 20에서 istate에서 replay 및 waiting 플래그를 제거한다.
  • 코드 라인 21에서 해당 인터럽트 카운터를 증가시킨다.
  • 코드 라인 27~30에서 irq 디스크립터에 action이 없거나 disable 설정된 경우 istate에 pending 플래그를 추가하고 함수를 빠져나간다.
  • 코드 라인 32에서 irq 디스크립터의 istate에서 pending 플래그를 제거하고, 모든 action에 등록한 인터럽트 핸들러들을 호출하여 수행한다. 수행 중인 동안에는 irq 진행중 상태가 유지된다.
  • 코드 라인 34에서 조건에 따라 unmask 처리를 수행한다.
    • 다음의 조건에서 unmask 처리를 수행하지 않는다.
      • 이미 인터럽트가 disable된 상태
      • 이미 인터럽트가 mask된 상태
      • oneshot 인터럽트의 경우
        • mask 상태를 유지하기 위해 unmask를 수행하지 않는다.

 

핸들러 함수 -2-

handle_percpu_devid_irq()

kernel/irq/chip.c

/**
 * handle_percpu_devid_irq - Per CPU local irq handler with per cpu dev ids
 * @irq:        the interrupt number
 * @desc:       the interrupt description structure for this irq
 *
 * Per CPU interrupts on SMP machines without locking requirements. Same as
 * handle_percpu_irq() above but with the following extras:
 *
 * action->percpu_dev_id is a pointer to percpu variables which
 * contain the real device id for the cpu on which this handler is
 * called
 */
void handle_percpu_devid_irq(unsigned int irq, struct irq_desc *desc) 
{
        struct irq_chip *chip = irq_desc_get_chip(desc);
        struct irqaction *action = desc->action;
        void *dev_id = raw_cpu_ptr(action->percpu_dev_id); 
        irqreturn_t res;

        kstat_incr_irqs_this_cpu(irq, desc);

        if (chip->irq_ack)
                chip->irq_ack(&desc->irq_data); 

        trace_irq_handler_entry(irq, action); 
        res = action->handler(irq, dev_id);
        trace_irq_handler_exit(irq, action, res);
        
        if (chip->irq_eoi)
                chip->irq_eoi(&desc->irq_data);
}

요청 irq의 action에 등록한 핸들러 함수를 호출 시 irq의 현재 cpu에 대한 dev_id를 인수로 전달한다. 핸들러 함수 호출 전후로 ack 및 eoi 처리를 한다.

  • 코드 라인 17에서 요청한 irq 디스크립터의 action에 등록한 per-cpu dev_id 값을 알아온다.
  • 코드 라인 20에서 현재 cpu에 대한 kstat 카운터를 증가시킨다.
  • 코드 라인 22~23에서 핸들러 함수 호출 전에 irq_chip의 irq_ack 콜백이 등록된 경우 호출한다.
    • 인터럽트 호출되기 전에 컨틀롤러에서 항상 처리해야 할 일이 있는 경우 사용한다.
  • 코드 라인 26에서 등록한 핸들러 함수를 호출할 때 irq 번호와 dev_id를 인수로 넘겨준다.
  • 코드 라인 29~30에서 핸들러 함수 호출 후에 irq_chip의 irq_eoi 콜백이 등록된 경우 호출한다.
    • 인터럽트 호출후에 컨틀롤러에서 항상 처리해야 할 일이 있는 경우 사용한다.

 

핸들러 공용 함수들

irq_may_run()

kernel/irq/chip.c

static bool irq_may_run(struct irq_desc *desc)
{
        unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED;

        /*
         * If the interrupt is not in progress and is not an armed
         * wakeup interrupt, proceed.
         */
        if (!irqd_has_set(&desc->irq_data, mask))
                return true;

        /*
         * If the interrupt is an armed wakeup source, mark it pending
         * and suspended, disable it and notify the pm core about the
         * event.
         */
        if (irq_pm_check_wakeup(desc))
                return false;

        /*
         * Handle a potential concurrent poll on a different core.
         */
        return irq_check_poll(desc);
}

irq의 실행이 곧 준비될 것 같으면 기다린다. 만일 처리가 불가능하면 false를 반환하고, 인터럽트 처리가 가능한 경우 true를 반환한다.

  • 코드 라인 9~10에서 irq 처리가 진행중이거나 armed wakeup 인터럽트가 아닌 경우 true를 반환하여 인터럽트가 계속 진행되게 한다.
  • 코드 라인 17~18에서 CONFIG_PM_SLEEP 커널 옵션을 사용하는 경우 Power Management 기능을 사용할 수 있는데 suspend 하지 않은 경우 true를 반환한다.
    • armed wakeup 인터럽트인 경우 인터럽트 디스크립터의 istate 상태에 suspended 및 pending을 추가하고 깨울 때까지 대기한다.
  • 코드 라인 23에서 irq polling을 중단 설정하지 않은 경우 다른 cpu에서 irq polling을 수행하는 중이면 완료될 때까지 기다린다.

 

핸들러 최종 호출 단계들

handle_irq_event()

kernel/irq/handle.c

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
        struct irqaction *action = desc->action;
        irqreturn_t ret;

        desc->istate &= ~IRQS_PENDING;
        irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
        raw_spin_unlock(&desc->lock);

        ret = handle_irq_event_percpu(desc, action);

        raw_spin_lock(&desc->lock);
        irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
        return ret;
}

irq 디스크립터의 istate에서 pending 플래그를 제거하고 모든 action에 등록한 인터럽트 핸들러들을 호출하여 수행한다. 수행 중인 동안에는 irq 진행중 상태가 유지된다.

 

handle_irq_event_percpu()

kernel/irq/handle.c

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;

        do {
                irqreturn_t res;

                trace_irq_handler_entry(irq, action);
                res = action->handler(irq, action->dev_id);
                trace_irq_handler_exit(irq, action, res);

                if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
                              irq, action->handler))
                        local_irq_disable();

                switch (res) {
                case IRQ_WAKE_THREAD:
                        /*
                         * Catch drivers which return WAKE_THREAD but
                         * did not set up a thread function
                         */
                        if (unlikely(!action->thread_fn)) {
                                warn_no_thread(irq, action);
                                break;
                        }

                        __irq_wake_thread(desc, action);

                        /* Fall through to add to randomness */
                case IRQ_HANDLED:
                        flags |= action->flags;
                        break;

                default:
                        break;
                }

                retval |= res;
                action = action->next;
        } while (action);

        add_interrupt_randomness(irq, flags);

        if (!noirqdebug)
                note_interrupt(irq, desc, retval);
        return retval;
}

irq 디스크립터의 모든 action에 등록한 인터럽트 핸들러들을 호출하여 수행한다.

  • 코드 라인 11에서 action에 등록한 인터럽트 핸들러들을 호출하여 수행한다.
  • 코드 라인 14~16에서 핸들러 수행 후 local irq가 disable 상태를 유지하지 못한 경우 경고 메시지를 출력하고 local irq를 disable 한다.
    • “irq N handler <function> enabled interrupts” 경고 메시지
  • 코드 라인 18~29에서 처리 결과가 IRQ_WAKE_THREAD인 경우 thread를 깨우고 계속 다음 행을 수행한다.
  • 코드 라인 23~29에서 처리 결과가 IRQ_HANDLED인 경우 flags에 action->flags를 추가하고, 그 외의 처리 결과는 skip 한다.
  • 코드 라인 32~34에서 retval에 처리 결과를 누적시키고 더 처리할 action이 있는 경우 루프를 돈다.
  • 코드 라인 44에서 시스템에서 더 확실한 난수 처리를 위해 인터럽트 발생 간격을 이용한다.

 

Chip hook 관련

IRQ startup & shutdown

irq_startup()

kernel/irq/chip.c

int irq_startup(struct irq_desc *desc, bool resend)
{
        int ret = 0;

        irq_state_clr_disabled(desc);
        desc->depth = 0;

        irq_domain_activate_irq(&desc->irq_data);
        if (desc->irq_data.chip->irq_startup) {
                ret = desc->irq_data.chip->irq_startup(&desc->irq_data);
                irq_state_clr_masked(desc);
        } else {
                irq_enable(desc);
        }
        if (resend)
                check_irq_resend(desc, desc->irq_data.irq);
        return ret;
}

요청 irq에 대해 enable 시키고 activate 처리를 수행하며 관련 콜백 함수를 호출한다.

  • 코드 라인 5~6에서 irq 상태에서 disabled를 클리어하고 depth=0으로 한다.
  • 코드 라인 8에서 요청 irq에 대해 irq_data 체인에 구성된 activate 핸들러 함수를 모두 호출한다.
  • 코드 라인 9~11에서 irq_startup 후크에 등록된 함수가 있는 경우 이를 호출하고 masked 상태를 클리어한다.
    • irq_desc->irq_data->state_use_accessors에서_IRQD_IRQ_MASKED 제거
  • 코드 라인 12~14에서 irq_startup 후크에 비어 있는 경우 irq_enable() 함수를 호출하여
  • 코드 라인 15~16에서 resend 요청이 있는 경우 pending된 irq를 다시 호출하게 tasklet을 리스케쥴하다.

 

irq_shutdown()

kernel/irq/chip.c

void irq_shutdown(struct irq_desc *desc)
{
        irq_state_set_disabled(desc);
        desc->depth = 1;
        if (desc->irq_data.chip->irq_shutdown)
                desc->irq_data.chip->irq_shutdown(&desc->irq_data);
        else if (desc->irq_data.chip->irq_disable)
                desc->irq_data.chip->irq_disable(&desc->irq_data);
        else
                desc->irq_data.chip->irq_mask(&desc->irq_data);
        irq_domain_deactivate_irq(&desc->irq_data);
        irq_state_set_masked(desc);
}

요청 irq에 대해 disable 시키고 deactivate 처리를 수행하며 관련 콜백 함수를 호출한다.

  • 코드 라인 3~4에서 irq 상태에서 disabled를 설정하고 depth=1로 한다.
  • 코드 라인 5~10에서 irq_shutdown 후크에 등록된 함수를 호출하고, 등록되지 않은 경우 irq_disable 후크에 등록된 함수를 호출한다. irq_disable 후크 마저 등록되지 않은 경우 irq_mask() 후크에 등록된 함수를 호출한다.
  • 코드 라인 11에서 최상위 irq domain까지 deactivate 후크에 등록한 콜백 함수를 호출한다.
    • irq domain의 deactivate 후크에 등록한 콜백 함수를 호출한다.
    • 재귀 호출로 parent domain에 대해서 반복한다.
  • 코드 라인 12에서 irq_startup 후크에 등록된 함수가 있는 경우 이를 호출하고 masked 상태를 클리어한다.
    • irq_desc->irq_data->state_use_accessors에_IRQD_IRQ_MASKED 설정 추가

 

IRQ Enable & Disable

irq_enable()

kernel/irq/chip.c

void irq_enable(struct irq_desc *desc)
{
        irq_state_clr_disabled(desc);
        if (desc->irq_data.chip->irq_enable)
                desc->irq_data.chip->irq_enable(&desc->irq_data);
        else
                desc->irq_data.chip->irq_unmask(&desc->irq_data);
        irq_state_clr_masked(desc);
}

요청 irq를 enable 한다.

  • 코드 라인 3에서 disabled 상태를 클리어한다.
    • irq_desc->irq_data->state_use_accessors에서IRQD_IRQ_DISABLED 제거
  • 코드 라인 4~5에서 chip에 irq_enable 후크가 등록된 경우 이를 호출한다.
  • 코드 라인 6~7에서 chip에 irq_enable 후크가 등록되지 않은 경우 irq_unmask() 콜백 함수를 호출한다.
  • 코드 라인 8에서 masked 상태를 클리어한다.

 

irq_disable()

kernel/irq/chip.c

/**
 * irq_disable - Mark interrupt disabled
 * @desc:       irq descriptor which should be disabled
 *
 * If the chip does not implement the irq_disable callback, we
 * use a lazy disable approach. That means we mark the interrupt
 * disabled, but leave the hardware unmasked. That's an
 * optimization because we avoid the hardware access for the
 * common case where no interrupt happens after we marked it
 * disabled. If an interrupt happens, then the interrupt flow
 * handler masks the line at the hardware level and marks it
 * pending.
 */
void irq_disable(struct irq_desc *desc)
{
        irq_state_set_disabled(desc);
        if (desc->irq_data.chip->irq_disable) {
                desc->irq_data.chip->irq_disable(&desc->irq_data);
                irq_state_set_masked(desc);
        }
}

요청 irq를 disable 한다.

  • 코드 라인 16에서 disabled 플래그를 설정한다.
    • irq_desc->irq_data->state_use_accessors에IRQD_IRQ_DISABLED 플래그 설정
  • 코드 라인 17~20에서 chip->irq_disable 후크에 등록한 콜백함수를 호출하고 masked 플래그를 설정한다.

 

IRQ mask & unmask

mask_irq()

kernel/irq/chip.c

void mask_irq(struct irq_desc *desc)
{
        if (desc->irq_data.chip->irq_mask) {
                desc->irq_data.chip->irq_mask(&desc->irq_data);
                irq_state_set_masked(desc);
        }
}

요청 irq를 마스크한다.

  • 코드 라인 3~6에서 chip->irq_mask 후크에 등록된 콜백 함수를 호출하고 masked 플래그를 설정한다.

 

unmask_irq()

kernel/irq/chip.c

void unmask_irq(struct irq_desc *desc)
{
        if (desc->irq_data.chip->irq_unmask) {
                desc->irq_data.chip->irq_unmask(&desc->irq_data);
                irq_state_clr_masked(desc);
        }
}

요청 irq를 언마스크한다.

  • 코드 라인 3~6에서 chip->irq_unmask 후크에 등록된 콜백 함수를 호출하고 masked 플래그를 클리어한다.

 

cond_unmask_irq()

kernel/irq/chip.c

/*
 * Called unconditionally from handle_level_irq() and only for oneshot
 * interrupts from handle_fasteoi_irq()
 */
static void cond_unmask_irq(struct irq_desc *desc)
{
        /*
         * We need to unmask in the following cases:
         * - Standard level irq (IRQF_ONESHOT is not set)
         * - Oneshot irq which did not wake the thread (caused by a
         *   spurious interrupt or a primary handler handling it
         *   completely).
         */
        if (!irqd_irq_disabled(&desc->irq_data) &&
            irqd_irq_masked(&desc->irq_data) && !desc->threads_oneshot)
                unmask_irq(desc);
}

조건에 따라 요청 irq를 언마스크한다.

  • 다음의 조건에서 unmask 처리를 수행하지 않는다.
    • 이미 인터럽트가 disable된 상태
    • 이미 인터럽트가 mask된 상태
    • oneshot 인터럽트의 경우
      • mask 상태를 유지하기 위해 unmask를 수행하지 않는다.

 

IRQ mask_ack & unmask_eoi

mask_ack_irq()

kernel/irq/chip.c

static inline void mask_ack_irq(struct irq_desc *desc)
{
        if (desc->irq_data.chip->irq_mask_ack)
                desc->irq_data.chip->irq_mask_ack(&desc->irq_data);
        else {
                desc->irq_data.chip->irq_mask(&desc->irq_data);
                if (desc->irq_data.chip->irq_ack)
                        desc->irq_data.chip->irq_ack(&desc->irq_data);
        }
        irq_state_set_masked(desc);
}

요청 irq에 대해 mask와 ack 처리를 하고 masked 플래그를 설정한다.

  • 보통 같은 인터럽트에 대해 계속 진입되는 것을 방지가힉 위해 인터럽트 핸들러 초입에서 호출되며  마스크 처리와 응답 처리를 같이 수행하게 한다.

 

  • 코드 라인 3~4에서 인터럽트 컨트롤러의 irq_mask_ack 후크에 등록한 콜백 함수를 호출한다.
  • 코드 라인 5~8에서 등록되지 않은 경우 irq_mask 및 irq_ack 후크에 등록한 콜백 함수를 연속해서 호출한다.
  • 코드 라인 10에서 irqd를 masked 상태로 설정한다.

 

cond_unmask_eoi_irq()

kernel/irq/chip.c

static void cond_unmask_eoi_irq(struct irq_desc *desc, struct irq_chip *chip)
{
        if (!(desc->istate & IRQS_ONESHOT)) {
                chip->irq_eoi(&desc->irq_data);
                return;
        }
        /*
         * We need to unmask in the following cases:
         * - Oneshot irq which did not wake the thread (caused by a
         *   spurious interrupt or a primary handler handling it
         *   completely).
         */
        if (!irqd_irq_disabled(&desc->irq_data) &&
            irqd_irq_masked(&desc->irq_data) && !desc->threads_oneshot) {
                chip->irq_eoi(&desc->irq_data);
                unmask_irq(desc);
        } else if (!(chip->flags & IRQCHIP_EOI_THREADED)) {
                chip->irq_eoi(&desc->irq_data);
        }
}

요청 irq의 eoi 처리를 하고 조건에 따라 unmask 처리한다.

  • 코드 라인 3~6에서 인터럽트가 oneshot 설정되지 않은 경우 eoi 처리 후 함수를 빠져나간다.
  • 코드 라인 13~16에서 irq가 enable된 상태에서 mask되었고 threads oneshot이 아닌 경우 eoi 처리 및 unmask 처리를 한다.
  • 코드 라인 17~19에서 eoi 스레드 플래그가 설정되지 않은 경우 eoi 처리만 한다.

 

기타

irq_domain_activate_irq()

kernel/irq/irqdomain.c

/**
 * irq_domain_activate_irq - Call domain_ops->activate recursively to activate
 *                           interrupt
 * @irq_data:   outermost irq_data associated with interrupt
 *
 * This is the second step to call domain_ops->activate to program interrupt
 * controllers, so the interrupt could actually get delivered.
 */
void irq_domain_activate_irq(struct irq_data *irq_data)
{
        if (irq_data && irq_data->domain) {
                struct irq_domain *domain = irq_data->domain;
                        
                if (irq_data->parent_data)
                        irq_domain_activate_irq(irq_data->parent_data);
                if (domain->ops->activate)
                        domain->ops->activate(domain, irq_data);
        }
}

irq domain에 구성된 최상위까지 activate 후크에 등록한 콜백 함수를 호출한다.

  • parent_data가 상위 irq_data를 가리키는 경우 재귀 호출된다.

 

irq_domain_deactivate_irq()

kernel/irq/irqdomain.c

/**             
 * irq_domain_deactivate_irq - Call domain_ops->deactivate recursively to
 *                             deactivate interrupt 
 * @irq_data: outermost irq_data associated with interrupt
 *                                 
 * It calls domain_ops->deactivate to program interrupt controllers to disable
 * interrupt delivery.
 */
void irq_domain_deactivate_irq(struct irq_data *irq_data)
{
        if (irq_data && irq_data->domain) {
                struct irq_domain *domain = irq_data->domain;

                if (domain->ops->deactivate)
                        domain->ops->deactivate(domain, irq_data);
                if (irq_data->parent_data)
                        irq_domain_deactivate_irq(irq_data->parent_data);
        }                                
}

irq domain에 구성된 최상위까지 deactivate 후크에 등록한 콜백 함수를 호출한다.

  • parent_data가 상위 irq_data를 가리키는 경우 재귀 호출된다.

 

DT용 인터럽트 컨트롤러 초기화 – rpi2

 

부모 컨트롤러 Part

bcm2836_arm_irqchip_l1_intc_of_init()

drivers/irqchip/irq-bcm2836.c

static int __init bcm2836_arm_irqchip_l1_intc_of_init(struct device_node *node,
                                                      struct device_node *parent)
{
        intc.base = of_iomap(node, 0);
        if (!intc.base) {
                panic("%s: unable to map local interrupt registers\n",
                        node->full_name);
        }

        bcm2835_init_local_timer_frequency();

        intc.domain = irq_domain_add_linear(node, LAST_IRQ + 1,
                                            &bcm2836_arm_irqchip_intc_ops,
                                            NULL);
        if (!intc.domain)
                panic("%s: unable to create IRQ domain\n", node->full_name);

        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTPSIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTPNSIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTHPIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTVIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_GPU_FAST,
                                         &bcm2836_arm_irqchip_gpu);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_PMU_FAST,
                                         &bcm2836_arm_irqchip_pmu);

        bcm2836_arm_irqchip_smp_init();

        set_handle_irq(bcm2836_arm_irqchip_handle_irq);
        return 0;
}

bcm2836 로컬 인터럽트에 해당하는 1개의  irq domain 과 3 개의 irq chip을 구성하고 사용하는 인터럽트들을 매핑한다.

  • 코드 라인 4~8에서 인수로 전달받은 인터럽트 컨트롤러 노드의 base 레지스터를 IO 매핑하여 가상주소를 알아온다. 만일 매핑이 실패하면 panic 처리를 한다.
    • rpi2: 0xf400_0000
  • 코드 라인 10~14에서 10개의 irq로 리니어 domain을 추가한다. domain의 변환 함수가 ops에 등록되어 운용된다. 만일 irq domain이 추가되지 않는 경우 panic 처리를 한다.
    • .xlate = irq_domain_xlate_onecell
  • 코드 라인 18~25에서 4개의 타이머 인터럽트를 timer용 irq chip에 구성하고 매핑한다.
  • 코드 라인 26~27에서 1개의 gpu 인터럽트를 gpu용 irq chip에 구성하고 매핑한다.
  • 코드 라인 28~29에서 1개의 pmu 인터럽트를 pmu용 irq chip에 구성하고 매핑한다.
  • 코드 라인 31에서 cpu on/off 등 상태 변경에 따른 notify를 받기 위해 연동하고, ipi 호출을 위해 연동함수를 등록한다.
  • 코드 라인 33에서 irq exception 처리 루틴에서 호출하도록 irq 핸들러 함수를 전역 handle_arch_irq에 등록한다.

 

자식 컨트롤러 Part

bcm2836_armctrl_of_init()

drivers/irqchip/irq-bcm2835.c

static int __init bcm2836_armctrl_of_init(struct device_node *node,
                                          struct device_node *parent)
{
        return armctrl_of_init(node, parent, true);
}

처리할 인터럽트 컨트롤러에 대한 디바이스 노드와 상위 인터럽트 컨트롤러에 대한 디바이스 노드 정보를 갖고 armctrl_of_init() 함수를 호출한다.

 

armctrl_of_init()

drivers/irqchip/irq-bcm2835.c

static int __init armctrl_of_init(struct device_node *node,
                                  struct device_node *parent,
                                  bool is_2836)
{
        void __iomem *base;
        int irq, b, i;

        base = of_iomap(node, 0);
        if (!base)
                panic("%s: unable to map IC registers\n",
                        node->full_name);

        intc.base = base;
        intc.domain = irq_domain_add_linear(node, NUMBER_IRQS * 2,
                                            &armctrl_ops, NULL);
        if (!intc.domain)
                panic("%s: unable to create IRQ domain\n", node->full_name);

        for (b = 0; b < NR_BANKS; b++) { 
                intc.pending[b] = base + reg_pending[b];
                intc.enable[b] = base + reg_enable[b];
                intc.disable[b] = base + reg_disable[b];

                for (i = 0; i < bank_irqs[b]; i++) {
                        irq = irq_create_mapping(intc.domain, MAKE_HWIRQ(b, i));
                        BUG_ON(irq <= 0);
                        irq_set_chip_and_handler(irq, &armctrl_chip,
                                handle_level_irq);
                        irq_set_probe(irq);
                }
        }

        if (is_2836) {
                int parent_irq = irq_of_parse_and_map(node, 0);

                if (!parent_irq) {
                        panic("%s: unable to get parent interrupt.\n",
                              node->full_name);
                }
                irq_set_chained_handler(parent_irq, bcm2836_chained_handle_irq);
        } else {
                set_handle_irq(bcm2835_handle_irq);
        }

        if (is_2836) {
                intc.local_regmap =
                        syscon_regmap_lookup_by_compatible("brcm,bcm2836-arm-local");
                if (IS_ERR(intc.local_regmap)) {
                        pr_err("Failed to get local register map. FIQ is disabled for cpus > 1\n");
                        intc.local_regmap = NULL;
                }
        }

        /* Make a duplicate irq range which is used to enable FIQ */
        for (b = 0; b < NR_BANKS; b++) {
                for (i = 0; i < bank_irqs[b]; i++) {
                        irq = irq_create_mapping(intc.domain,
                                        MAKE_HWIRQ(b, i) + NUMBER_IRQS);
                        BUG_ON(irq <= 0); 
                        irq_set_chip(irq, &armctrl_chip);
                        irq_set_probe(irq);
                }
        }
        init_FIQ(FIQ_START);

        return 0;
}

 

bcm2836 gpu 인터럽트에 해당하는 irq domain 과 irq chip을 구성하고 사용하는 인터럽트들을 매핑한다.

  • 코드 라인 8~13에서 인수로 전달받은 인터럽트 컨트롤러 노드의 base 레지스터를 IO 매핑하여 가상주소를 알아온다. 만일 매핑이 실패하면 panic 처리를 한다.
    • rpi2: 0xf300_b200
  • 코드 라인 14~17에서 3개 뱅크(각 32개 irq)가 연결되는 96개 irq + 96개 fiq에 대응하도록 irq domain을 구성하고 매핑한다.
    • irq domain의 ops
      • .xlate =armctrl_xlate
  • 코드 라인 19~22에서 각 뱅크의 pending 레지스터와 enable 및 disable을 레지스터의 가상 주소를 대입한다.
  • 코드 라인 24~25에서 각 뱅크에서 처리할 hw 인터럽트 수 만큼을 irq domain과 irq 디스크립터에 매핑한다.
    • 3 개의 각 뱅크에서 매핑할 hwirq 들
      • bank#0
        • hwirq=0~7까지 8개
        • irq=22~29
      • bank#1
        • hwirq=32~63까지 32개
        • irq=32~63
      • bank#2
        • hwirq=64~95까지 32개
        • irq=62~93
  • 코드 라인 27~30에서 “ARMCTRL-level” 이라는 이름의 irq chip을 설정하고 핸들러로 handle_level_irq()를 사용한다.
  • 코드 라인 33~40에서 이 드라이버 코드는 bcm2835(rpi)와 혼용하여 사용하므로 bcm2836(rpi2)인 경우 부모 인터럽트 컨트롤의 hw irq를 알아와서 이 인터럽트 핸들러로 bcm2836_chained_handle_irq() 함수를 대입한다. 만일 Device Tree 스크립트에서 parent irq 번호를 알아오지 못한 경우 panic 처리한다.
    • 모든 gpu 인터럽트들 중 하나라도 인터럽트가 발생하면 부모 인터럽트 컨트롤러의 8번 hwirq도 인터럽트가 발생한다.
  • 코드 라인 41~43에서 bcm2835(rpi)에서 호출한 경우 핸들러로 bcm2835_handle_irq() 함수를 대입한다.
  • 코드 라인 45~52에서 Device Tree에서 System Control Register 정보를 읽어서 base 주소를 대입한다. 못 읽어오는 경우 메시지를 출력한다. 이 때 2 번째 cpu 부터 fiq 인터럽트를 수신할 수 없게된다.
    • rpi2: 물리 주소=0x4000_0000, size=0x100
  • 코드 라인 55~58에서 각 뱅크에서 처리할 hw 인터럽트 수 만큼을 irq domain과 irq 디스크립터에 매핑한다. 이 인터럽트들은 fiq에 해당한다.
    • 3 개의 각 뱅크에서 매핑할 hwirq 들
      • bank#0
        • hwirq=96~103까지 8개
        • irq=94~101
      • bank#1
        • hwirq=128~159까지 32개
        • irq=102~133
      • bank#2
        • hwirq=160~191까지 32개
        • irq=134~165
  • 코드 라인 60~62에서 “ARMCTRL-level” 이라는 이름의 irq chip을 설정하고 probe를 설정한다.
  • 코드 라인 64에서 default FIQ 인터럽트 처리기를 백업하기 위해 fiq 벡터 값과 fiq 모드에서 사용하는 레지스터들 일부(r8 ~ r12, sp, lr)를 백업해둔다.
    • fiq 사용 시 멀티플 레지스터 로드 및 저장 시 깨질 수 있기에 복원을 위해 초기 핸들러를 백업해 둔다.

 

irq_set_chained_handler()

include/linux/irq.h

/*
 * Set a highlevel chained flow handler for a given IRQ.
 * (a chained handler is automatically enabled and set to
 *  IRQ_NOREQUEST, IRQ_NOPROBE, and IRQ_NOTHREAD) 
 */
static inline void
irq_set_chained_handler(unsigned int irq, irq_flow_handler_t handle)
{
        __irq_set_handler(irq, handle, 1, NULL);
}

요청 irq에 chained 핸들러를 설정한다.

  • rpi2: 로컬 인터럽트 컨트롤러가 부모이고 hwirq=8에 chained 핸들러를 설정한다.
    • chained handler: bcm2836_chained_handle_irq()

 

GPIO 컨트롤러 Part

인터럽트 컨트롤러에서 초기화하지 않고 GPIO의 pinctrl 드라이버에서 초기화된다.  초기화 과정의 소스는 여기서 설명하지 않고 아래 그림만 참고한다.

 

IRQ exception 처리 후 컨트롤러가 설정한 핸들러 함수 호출

부모 컨트롤러  Part

bcm2836_arm_irqchip_handle_irq()

static void
__exception_irq_entry bcm2836_arm_irqchip_handle_irq(struct pt_regs *regs)
{
        int cpu = smp_processor_id();
        u32 stat;

        stat = readl_relaxed(intc.base + LOCAL_IRQ_PENDING0 + 4 * cpu);
        if (stat & BIT(LOCAL_IRQ_MAILBOX0)) {
#ifdef CONFIG_SMP
                void __iomem *mailbox0 = (intc.base +
                                          LOCAL_MAILBOX0_CLR0 + 16 * cpu);
                u32 mbox_val = readl(mailbox0);
                u32 ipi = ffs(mbox_val) - 1;

                writel(1 << ipi, mailbox0);
                handle_IPI(ipi, regs);
#endif
        } else if (stat) {
                u32 hwirq = ffs(stat) - 1;

                handle_IRQ(irq_linear_revmap(intc.domain, hwirq), regs);
        }
}

mailbox0 레지스터를 읽어 값이 있는 경우 IPI 핸들러를 호출하고 그렇지 않은 경우 IRQ 핸들러를 호출한다.

  • rpi2: 부모 인터럽트 컨틀롤러에서 발생한 irq들은 4개의 timer와 pmu 인터럽트이다.

 

자식 컨트롤러  Part

bcm2836_chained_handle_irq()

drivers/irqchip/irq-bcm2835.c

 

static void bcm2836_chained_handle_irq(struct irq_desc *desc)
{
        u32 hwirq;

        while ((hwirq = get_next_armctrl_hwirq()) != ~0)
                generic_handle_irq(irq_linear_revmap(intc.domain, hwirq));
}

gpu pending 레지스터 값을 읽어서 hwirq 값을 irq로 변환한 후 generic_handle_irq() 함수를 호출한다.

  • 변환한 irq에 등록한 핸들러 함수를 호출한다.
    • rpi2: handle_level_irq()

 

get_next_armctrl_hwirq()

drivers/irqchip/irq-bcm2835.c

static u32 get_next_armctrl_hwirq(void)
{
        u32 stat = readl_relaxed(intc.pending[0]) & BANK0_VALID_MASK;

        if (stat == 0)
                return ~0;
        else if (stat & BANK0_HWIRQ_MASK)
                return MAKE_HWIRQ(0, ffs(stat & BANK0_HWIRQ_MASK) - 1);
        else if (stat & SHORTCUT1_MASK)
                return armctrl_translate_shortcut(1, stat & SHORTCUT1_MASK);
        else if (stat & SHORTCUT2_MASK)
                return armctrl_translate_shortcut(2, stat & SHORTCUT2_MASK);
        else if (stat & BANK1_HWIRQ)
                return armctrl_translate_bank(1);
        else if (stat & BANK2_HWIRQ)
                return armctrl_translate_bank(2);
        else
                BUG();
}

gpu pending 레지스터 값을 읽어서 hwirq로 변환한다.

  • 코드 라인 3에서 로컬 pending  레지스터 값을 읽어서 관련 비트들만 마스크한다.
    • timer 4개, mailbox 4개, gpu 매핑 1개, pmu 매핑 1개, shortcut 11개 비트들
  • 코드 라인 5~6에서 pending 인터럽트가 없는 경우 ~0(호출 함수에서 루프의 끝을 의미)을 반환한다.
  • 코드 라인 7~8에서 local 인터럽트 8개(bit7:0) 중 하나에 속한 경우 0번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
  • 코드 라인 9~10에서 shortcut1에 해당하는 인터럽트가 발생한 경우 1번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
    • bit10 ~ bit14 -> <7, 9, 10, 18, 19> + 32(bank1)
  • 코드 라인 11~12에서 shortcut2에 해당하는 인터럽트가 발생한 경우 2번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
    • bit15 ~ bit20 -> <21, 22, 23, 24, 25, 30> + 64(bank2)
  • 코드 라인 13~14에서 bank1에 해당하는 인터럽트인 경우 gpu#1 pending 레지스터를 읽어 가장 우측 비트(lsb)가 1로 설정된 인터럽트에 해당하는 hwirq로 변환하여 반환한다.
    • bit0 ~ bit31 -> 30 ~ 61
  • 코드 라인 15~16에서 bank2에 해당하는 인터럽트인 경우 gpu#2 pending 레지스터를 읽어 가장 우측 비트(lsb)가 1로 설정된 인터럽트에 해당하는 hwirq로 변환하여 반환한다.
    • bit0 ~ bit31 -> 62 ~ 93

 

armctrl_translate_shortcut()

drivers/irqchip/irq-bcm2835.c

static u32 armctrl_translate_shortcut(int bank, u32 stat)
{
        return MAKE_HWIRQ(bank, shortcuts[ffs(stat >> SHORTCUT_SHIFT) - 1]);
}

shortcut에 해당하는 인터럽트가 발생한 경우 해당 뱅크에 해당하는 hwirq로 변환하여 반환한다.

  • bit10 ~ bit14 -> <7, 9, 10, 18, 19> + 32(bank1)
  • bit15 ~ bit20 -> <21, 22, 23, 24, 25, 30> + 64(bank2)

 

armctrl_translate_bank()

drivers/irqchip/irq-bcm2835.c

/*
 * Handle each interrupt across the entire interrupt controller.  This reads the
 * status register before handling each interrupt, which is necessary given that
 * handle_IRQ may briefly re-enable interrupts for soft IRQ handling.
 */

static u32 armctrl_translate_bank(int bank)
{
        u32 stat = readl_relaxed(intc.pending[bank]);

        return MAKE_HWIRQ(bank, ffs(stat) - 1);
}

gpu#1 또는 gpu#2 pending 레지스터에서 읽은 stat 값에서 가장 우측 비트에 해당하는 hwirq를 반환한다.

  • bit0 ~ bit31 -> 30 ~ 61
  • bit0 ~ bit31 -> 62 ~ 93

 

drivers/irqchip/irq-bcm2835.c

/* Put the bank and irq (32 bits) into the hwirq */
#define MAKE_HWIRQ(b, n)        ((b << 5) | (n))
#define HWIRQ_BANK(i)           (i >> 5)
#define HWIRQ_BIT(i)            BIT(i & 0x1f)

 

generic_handle_irq()

kernel/irq/irqdesc.c

/**
 * 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;
}
EXPORT_SYMBOL_GPL(generic_handle_irq);

요청한 irq에 해당하는 핸들러 함수를 호출한다.

 

generic_handle_irq_desc()

include/linux/irqdesc.h

/*
 * 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);
}

요청한 irq 디스크립터의 등록한 핸들러 함수를 호출한다.

 

irq chip의 콜백 함수 – rpi2

ARMCTRL-level용

armctrl_chip

drivers/irqchip/irq-bcm2835.c

static struct irq_chip armctrl_chip = {
        .name = "ARMCTRL-level",
        .irq_mask = armctrl_mask_irq,
        .irq_unmask = armctrl_unmask_irq
};

bcm2835 인터럽트용 irq_chip 콜백 함수들

 

armctrl_mask_irq()

drivers/irqchip/irq-bcm2835.c

static void armctrl_mask_irq(struct irq_data *d)
{
        if (d->hwirq >= NUMBER_IRQS)
                writel_relaxed(REG_FIQ_DISABLE, intc.base + REG_FIQ_CONTROL);
        else
                writel_relaxed(HWIRQ_BIT(d->hwirq),
                               intc.disable[HWIRQ_BANK(d->hwirq)]);
}

요청한 인터럽트(hwirq) 라인을 마스크하여 인터럽트 진입을 허용하지 않게 한다.

  • 코드 라인 3~4에서 hwirq가 NUMBER_IRQS(96) 이상인 경우 FIQ 전체에 대한 인터럽트 마스크를 수행한다.
  • 코드 라인 5~7에서 요청 IRQ에 대한 마스크를 수행한다.
    • 1개 뱅크당 32개의 인터럽트 라인을 제어할 수 있다.

 

armctrl_unmask_irq()

drivers/irqchip/irq-bcm2835.c

static void armctrl_unmask_irq(struct irq_data *d)
{
        if (d->hwirq >= NUMBER_IRQS) {
                if (num_online_cpus() > 1) {
                        unsigned int data;
                        int ret;

                        if (!intc.local_regmap) {
                                pr_err("FIQ is disabled due to missing regmap\n");
                                return;
                        }

                        ret = regmap_read(intc.local_regmap,
                                          ARM_LOCAL_GPU_INT_ROUTING, &data);
                        if (ret) {
                                pr_err("Failed to read int routing %d\n", ret);
                                return;
                        }

                        data &= ~0xc;
                        data |= (1 << 2);
                        regmap_write(intc.local_regmap,
                                     ARM_LOCAL_GPU_INT_ROUTING, data);
                }

                writel_relaxed(REG_FIQ_ENABLE | hwirq_to_fiq(d->hwirq),
                               intc.base + REG_FIQ_CONTROL);
        } else {
                writel_relaxed(HWIRQ_BIT(d->hwirq),
                               intc.enable[HWIRQ_BANK(d->hwirq)]);
        }
}

요청한 인터럽트(hwirq) 라인을 언마스크하여 인터럽트 진입을 허용하게 한다.

  • 코드 라인 3에서 hwirq가 NUMBER_IRQS(96) 이상인 경우
  • 코드 라인 4~24에서 online cpu가 1개를 초과하는 경우 2개 이상인 경우 bcm2836에 있는 local routing 레지스터를 설정할 수 있고, 여기에서 irq cpu 라우팅을 fiq cpu 라우팅으로 변경한다.
    • local routing  레지스터 값은 하위 3비트만 유효하다.
      • 최하위 비트 2개는 cpu core 번호
      • bit2는 0=irq, 1=fiq
    • 이 조작으로 영향을 받는 인터럽트는 core 전용이 아닌 공통 인터럽트들로 아래와 같다.
      • GPU IRQ
      • Local Timer
      • AXI error
      • 미 사용 15개의 local peripheral 인터럽트들
    • 이 조작으로 영향을 받지 않는 인터럽트들은 core 인터럽트로 아래와 같다.
      • 4개의 코어 타이머 인터럽트들 x 4 cpus
      • 1개의 pm 인터럽트 x 4 cpus
      • 4개의 mailbox 인터럽트들 x 4 cpus
  • 코드 라인 26~27에서 FIQ Control 레지스터의 8번 비트를 켜고 bit0~bit7의 irq(fiq) source를 지정한다.
    • FIQ Control 레지스터: 버스(VC) 주소=0x7e00_b20c, ARM 물리 주소=0x3f00_b20c, ARM 가상 주소=0xf300_b20c
    • 1개의 irq 소스를 fiq로 라우팅할 수 있다.
      • rpi2: 고석 처리가 요구되는 usb를 fiq로 라우팅하여 사용한다.
  • 코드 라인 28~31에서 요청 IRQ를 enable을 시킨다.
    • 3개의 뱅크에서 각 뱅크당 최대 32개의 인터럽트 라인을 제어할 수 있다.

 

GPIO pinctrl-bcm2835용

rpi 및 rpi2에는 2개의 뱅크에 나뉘어 총 54개의 gpio 라인이 제공되고 이를 28개, 18개, 8개로 나누어 81~83번의 hw 인터럽트를 발생시킨다.

 

bcm2835_gpio_irq_chip

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static struct irq_chip bcm2835_gpio_irq_chip = {
        .name = MODULE_NAME,
        .irq_enable = bcm2835_gpio_irq_enable,
        .irq_disable = bcm2835_gpio_irq_disable,
        .irq_set_type = bcm2835_gpio_irq_set_type,
};

bcm2835의 gpio 인터럽트용 irq_chip 콜백 함수들

 

bcm2835_gpio_irq_enable()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static void bcm2835_gpio_irq_enable(struct irq_data *data)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);
        set_bit(offset, &pc->enabled_irq_map[bank]);
        bcm2835_gpio_irq_config(pc, gpio, true);
        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);
}

gpio irq를 enable 한다. (hwirq=0~53)

  • 코드 라인 3에서 irq_chip->chip_data에 저장해둔 pc(pin control)를 알아온다.
  • 코드 라인 4에서 gpio hwirq 번호를 알아온다.
  • 코드 라인 5~6에서 hwirq에 대한 bank와 offset을 구한다.
    • 예) hwirq=53인 경우 bank=1, offset=21
  • 코드 라인 10에서 hwirq에 해당하는 enable 비트를 설정한다.
  • 코드 라인 11에서 hwirq의 트리거 타입에 따른 gpio 레지스터를 조작하여 enable 설정을 한다.

 

bcm2835_gpio_irq_disable()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static void bcm2835_gpio_irq_disable(struct irq_data *data)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);
        bcm2835_gpio_irq_config(pc, gpio, false);
        /* Clear events that were latched prior to clearing event sources */
        bcm2835_gpio_set_bit(pc, GPEDS0, gpio);
        clear_bit(offset, &pc->enabled_irq_map[bank]);
        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);
}

gpio irq를 disable 한다. (hwirq=0~53)

  • 코드 라인 3에서 irq_chip->chip_data에 저장해둔 pc(pin control)를 알아온다.
  • 코드 라인 4에서 hwirq 번호를 알아온다.
    • rpi2: 81 ~ 83
  • 코드 라인 5~6에서 hwirq에 대한 bank와 offset을 구한다.
    • 예) hwirq=81인 경우 bank=2, offset=17
  • 코드 라인 10에서 hwirq의 트리거 타입에 따른 gpio 레지스터를 조작하여 disable 설정을 한다.
  • 코드 라인 12에서 2 개의 GPEDS(GPIO Event Detect Status) Register 중 요청 hwirq에 해당하는 비트를 1로 기록하여 이벤트 발생 비트를 클리어한다.
  • 코드 라인 13에서 hwirq에 해당하는 enable 비트를 클리어한다.

 

bcm2835_gpio_irq_set_type()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static int bcm2835_gpio_irq_set_type(struct irq_data *data, unsigned int type)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;
        int ret;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);

        if (test_bit(offset, &pc->enabled_irq_map[bank]))
                ret = __bcm2835_gpio_irq_set_type_enabled(pc, gpio, type);
        else 
                ret = __bcm2835_gpio_irq_set_type_disabled(pc, gpio, type);

        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);

        return ret;
}

gpio irq에 해당하는 트리거 타입을 설정한다. (hwirq=0~53)

  • 타입 설정 전에 요청 irq에 대해 disable 한 후 트리거 타입을 바꾼다. 그런 후 enable 비트가 설정된 경우 해당 irq에 대해 enable 시킨다.

 

bcm2836-gpu용

bcm2836_arm_irqchip_gpu

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_gpu = {
        .name           = "bcm2836-gpu",
        .irq_mask       = bcm2836_arm_irqchip_mask_gpu_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_gpu_irq,
};

bcm2836의 GPU 인터럽트용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_gpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_gpu_irq(struct irq_data *d)
{
}

1개 gpu 인터럽트의 mask 처리 요청에 대해 아무것도 수행하지 않는다.

 

bcm2836_arm_irqchip_unmask_gpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_gpu_irq(struct irq_data *d)
{
}

1개 gpu 인터럽트의 unmask 처리 요청에 대해 아무것도 수행하지 않는다.

 

bcm2836-pmu용

bcm2836_arm_irqchip_pmu

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_pmu = {
        .name           = "bcm2836-pmu",
        .irq_mask       = bcm2836_arm_irqchip_mask_pmu_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_pmu_irq,
};

bcm2836의 pmu 인터럽트용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_pmu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_pmu_irq(struct irq_data *d)
{
        writel(1 << smp_processor_id(), intc.base + LOCAL_PM_ROUTING_CLR);
}

1개 pmu 인터럽트의 mask 처리 요청을 수행한다.

 

bcm2836_arm_irqchip_unmask_pmu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_pmu_irq(struct irq_data *d)
{
        writel(1 << smp_processor_id(), intc.base + LOCAL_PM_ROUTING_SET);
}

1개 pmu 인터럽트의 unmask 처리 요청을 수행한다.

 

bcm2836-timer용

bcm2836_arm_irqchip_timer

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_timer = {
        .name           = "bcm2836-timer",
        .irq_mask       = bcm2836_arm_irqchip_mask_timer_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_timer_irq,
};

bcm2836의 각 코어에 구성된 4개 local 타이머용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_timer_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_timer_irq(struct irq_data *d)
{
        bcm2836_arm_irqchip_mask_per_cpu_irq(LOCAL_TIMER_INT_CONTROL0,
                                             d->hwirq - LOCAL_IRQ_CNTPSIRQ,
                                             smp_processor_id());
}

4개의 local timer 인터럽트는 cpu별로 동작하는 per-cpu 타입인데 이 중 현재 cpu에 요청한 타이머 인터럽트(irq_data->hwirq(0~3))를 마스크 처리한다.

  • cpu별 LOCAL_TIMER_INT_CONTROL 레지스터
    • 0x4000_0040 (#0번 core) ~ 0x4000_004c(#3번 core)
  • 4 개 timer 인터럽트 중 요청한 인터럽트(0 ~ 3번 중 하나)를 마스크한다.

 

bcm2836_arm_irqchip_mask_per_cpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_per_cpu_irq(unsigned int reg_offset,
                                                 unsigned int bit,
                                                 int cpu)
{
        void __iomem *reg = intc.base + reg_offset + 4 * cpu;

        writel(readl(reg) & ~BIT(bit), reg);
}

bcm2836 local 인터럽트 컨트롤러에서 per-cpu 인터럽트에 대한 마스크를 수행한다.

  • rpi2:
    • intc.base는 0x4000_0000
    • reg_offset는 0x40

 

bcm2836_arm_irqchip_unmask_timer_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_timer_irq(struct irq_data *d)
{
        bcm2836_arm_irqchip_unmask_per_cpu_irq(LOCAL_TIMER_INT_CONTROL0,
                                               d->hwirq - LOCAL_IRQ_CNTPSIRQ,
                                               smp_processor_id());
}

4개의 local timer 인터럽트는 cpu별로 동작하는 per-cpu 타입인데 이 중 현재 cpu에 요청한 타이머 인터럽트(irq_data->hwirq(0~3))를 언마스크 처리한다.

  • cpu별 LOCAL_TIMER_INT_CONTROL 레지스터
    • 0x4000_0040 (#0번 core) ~ 0x4000_004c(#3번 core)
  • 4 개 timer 인터럽트 중 요청한 인터럽트(0 ~ 3번 중 하나)를 클리어한다.

 

bcm2836_arm_irqchip_unmask_per_cpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_per_cpu_irq(unsigned int reg_offset,
                                                 unsigned int bit,
                                                 int cpu)
{
        void __iomem *reg = intc.base + reg_offset + 4 * cpu;

        writel(readl(reg) | BIT(bit), reg);
}

bcm2836 local 인터럽트 컨트롤러에서 per-cpu 인터럽트에 대한 언마스크를 수행한다.

 

IRQ 설정

각 디바이스 드라이버에서 irq를 설정할 때 사용하는 함수는 다음 그림과 같다.

 

구조체

irq_chip 구조체

include/linux/irq.h

/*
 * @name:               name for /proc/interrupts
 * @irq_startup:        start up the interrupt (defaults to ->enable if NULL)
 * @irq_shutdown:       shut down the interrupt (defaults to ->disable if NULL)
 * @irq_enable:         enable the interrupt (defaults to chip->unmask if NULL)
 * @irq_disable:        disable the interrupt
 * @irq_ack:            start of a new interrupt
 * @irq_mask:           mask an interrupt source
 * @irq_mask_ack:       ack and mask an interrupt source
 * @irq_unmask:         unmask an interrupt source
 * @irq_eoi:            end of interrupt
 * @irq_set_affinity:   set the CPU affinity on SMP machines
 * @irq_retrigger:      resend an IRQ to the CPU
 * @irq_set_type:       set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
 * @irq_set_wake:       enable/disable power-management wake-on of an IRQ
 * @irq_bus_lock:       function to lock access to slow bus (i2c) chips
 * @irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips
 * @irq_cpu_online:     configure an interrupt source for a secondary CPU
 * @irq_cpu_offline:    un-configure an interrupt source for a secondary CPU
 * @irq_suspend:        function called from core code on suspend once per chip
 * @irq_resume:         function called from core code on resume once per chip
 * @irq_pm_shutdown:    function called from core code on shutdown once per chip
 * @irq_calc_mask:      Optional function to set irq_data.mask for special cases
 * @irq_print_chip:     optional to print special chip info in show_interrupts
 * @irq_request_resources:      optional to request resources before calling
 *                              any other callback related to this irq
 * @irq_release_resources:      optional to release resources acquired with
 *                              irq_request_resources
 * @irq_compose_msi_msg:        optional to compose message content for MSI
 * @irq_write_msi_msg:  optional to write message content for MSI
 * @flags:              chip specific flags
 */
struct irq_chip {
        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);

        void            (*irq_cpu_online)(struct irq_data *data);
        void            (*irq_cpu_offline)(struct irq_data *data);

        void            (*irq_suspend)(struct irq_data *data);
        void            (*irq_resume)(struct irq_data *data);
        void            (*irq_pm_shutdown)(struct irq_data *data);

        void            (*irq_calc_mask)(struct irq_data *data);

        void            (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
        int             (*irq_request_resources)(struct irq_data *data);
        void            (*irq_release_resources)(struct irq_data *data);

        void            (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
        void            (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);

        unsigned long   flags;
}
  • name
    • /proc/interrupts에 출력할 이름
  • irq_startup
    • 인터럽트 startup
    • core – irq_startup() 함수에서 호출하는데 이 후크가 지정되지 않으면 core – irq_enable() 함수를 호출한다.
  • irq_shutdown
    • 인터럽트 shutdown
    • core – irq_shutdown() 함수에서 호출하는데 이 후크가 지정되지 않으면 core – irq_disable() 함수를 호출한다.
  • irq_enable
    • 인터럽트 enable
    • core – irq_enable() 함수에서 호출하는데 이 후크가 지정되지 않으면 chip->irq_unmask() 후크를 호출한다.
  • irq_disable
    • 인터럽트 disable
    • core – irq_disable() 함수에서 호출한다.
  • irq_ack
    • 인터럽트 핸들러에서 새 인터럽트가 들어온 경우 호출한다. (새 인터럽트의 시작)
  • irq_mask
    • 인터럽트 소스 마스크
    • c0re – mask_irq() 함수에서 호출한다.
  • irq_mask_ack
    • 인터럽트 핸들러에서 새 인터럽트가 들어온 경우 호출하며 irq 마스크도 같이 수행한다.
    • core – mask_ack_irq() 함수에서 호출하는데 이 후크가 지정되지 않으면 chip->irq_mask() 후크와 chip->irq_ack() 후크를 따로 하나씩 호출한다.
  • irq_unmask
    • 인터럽트 소스 언마스크
    • c0re – unmask_irq() 함수에서 호출한다.
  • irq_eoi
    • 인터럽트 핸들러 처리의 종료 시 호출한다.
  • irq_set_affinity
    • SMP 머신에서 irq 소스를 허용할 cpu affinity를 설정한다.
    • core – irq_do_set_affinity() 함수에서 호출한다.
  • irq_retrigger
    • cpu로 irq를 재 전송
  • irq_set_type
    • irq 라인 트리거 타입을 설정한다.
  • irq_set_wake
    • irq의 pm wake-on의 enable/disable 여부 설정
    • core – irq_set_irq_wake() -> set_irq_kake_real() 함수에서 호출한다.
  • irq_bus_lock
    • slow bus chip에 대한 lock으로 인터럽트를 구성하고 해제할 때 lock을 사용 한다.
      • 사용 함수
        • setup_irq()
        • free_irq()
        • request_threaded_irq()
        • setup_percpu_irq()
        • free_percpu_irq()
        • request_percpu_irq()
        • irq_finalize_oneshot()
  • irq_bus_sync_unlock
    • slow bus chip에 대한 sync와 unlock
  • irq_cpu_online
    • cpu가 online될 때 core – irq_cpu_online() 함수에서 호출한다.
  • irq_cpu_offline
    • cpu가 offline될 때 core – irq_cpu_offline() 함수에서 호출한다.
  • irq_suspend
    • PM(Power Management) 기능을 사용하는 경우 core가 suspend 되는 경우 호출되어 irq chip도같이 suspend 동작을 알려 동작을 멈추게 한다.
  • irq_resume
    • PM(Power Management) 기능을 사용하는 경우 suspend된 irq chip이 깨어나 다시 동작하게 한다.
  • irq_pm_shutdown
    • irq에 대한 전원 소비를 없애기 위해 shutdown 한다.
  • irq_calc_mask
    • irq_data.mask의 설정을 사용한 특별한 옵션 기능
  • irq_print_chip
    • show interruptes 시 추가 옵션 칩 정보를 출력
  • irq_request_resources
    • irq와 관련된 모든 콜백 함수를 호출하기 전에 리소스를 요청하기 위한 옵션
  • irq_release_resources
    • irq_request_resources에서 얻은 리소스를 해제
  • irq_compose_msi_msg
    • MSI를 위한 메시지 구성
  • irq_write_msi_msg
    • MSI를 위한 메시지 기록 옵션
  • flags
    • chip 관련 플래그들

 

참고

 

Interrupts -3- (irq domain)

 

IRQ Domain

각 제조사의 인터럽트 컨트롤러에 고정 매핑 또는 프로그래머블 매핑 기능이 있는 경우가 많이 있다. 그리고 리눅스 인터럽트에 대해 매핑되기 전 실제 하드웨어 인터럽트 번호를 알아내기 위해 리버스 매핑(리눅스 irq -> hw irq) 기능을 필요하게 되었고 이를 구현하기 위해 다음과 같은 방법들을 고려하여 몇 가지의 구현 방법을 준비하였다.

  • 리버스 맵 할당 방법
    • Linear(연속된) 고정 할당
    • Sparse Dynamic 할당
  • 매핑
    • 매핑 변경 가능
    • 매핑 변경 불가능
    • 매핑 불가능

 

매핑 구현 방법

다음과 같은 여러 가지 구현 모델에 대해 “irq_domain_add_”로 시작하는 함수를 제공한다. irq domain 들은 전역

  • Linear
    • irq와 hwirq의 매핑이 자유롭게 동작해야 하는 시스템에서 사용한다.
    • 리니어 리버스 매핑 테이블을 구성하여 사용한다. (hwirq -> irq)
      • hwirq 번호가 수백번 이하인 경우 컴파일 타임에 static 배열을 사용하므로 구현이 간단하다.
    • linear 방식의 irq domain이 추가된 후 부터 dynamic 하게 매핑 함수를 통해 만들고 매핑할 수 있다.
      • irq_create_mapping() 함수 등..
    • 구현
      • irq_domain_add_linear() 함수
  • Tree
    • linear 방식과 동일하게 irq와 hwirq의 매핑이 자유롭게 동작해야 하는 시스템에서 사용한다.
    • 리버스 매핑 테이블을 Radix Tree에 구성하여 할당 관리한다. (hwirq -> irq)
      • hwirq 번호가 매우 큰 경우 dynamic 하게 필요한 hwirq에 대해서만 할당 구성하여 메모리를 낭비하지 않게 할 때 사용한다.
      • arm64의 default 구성이다.
    • tree 방식의 irq domain이 추가된 후 부터 dynamic 하게 매핑 함수를 통해 를 만들고 매핑할 수 있다.
      • irq_create_mapping() 함수 등..
    • 구현
      • irq_domain_add_tree() 함수
  • Nomap
    • irq와 hwirq가 항상 동일하여 매핑을 전혀 필요하지 않는 시스템에서 사용하다.
      • 리버스 매핑 테이블도 만들지 않는다.
    • irq domain을 추가하기 전에 irq 디스크립터들이 미리 구성되어 있어야 한다.
    •  구현
      • irq_domain_add_nomap() 함수
  • Legacy
    • irq와 hwirq의 매핑을 고정한 채로 동작해야 하는 시스템에서 사용한다.
    • 리니어 리버스 매핑 테이블 역시 구성하여 사용하되 중간에 변경될 수 없다. (hwirq -> irq)
    • legacy 방식으로 irq domain을 추가하기 전에 irq 디스크립터들이 미리 할당되어 있어야 한다.
      • rpi2: early_irq_init() 함수에서 irq 디스크립터들이 처음 할당된다.
    • 구현
      • irq_domain_add_legacy() 함수
  • Legacy ISA
    • Legacy와 동일하나 irq 수가 16개로 제한된다.
    • 구현
      • irq_domain_add_legacy_isa()  함수

 

다음 그림은 3개의 인터럽트 컨트롤로에 대응해 nomap, linear, tree 등 3가지 방법을 섞어서 irq_domain을 구성한 예를 보여준다.

 

IRQ Domain hierarchy

2 개 이상의 인터럽트 컨틀롤러가 PM(파워 블럭 관리) 통제를 받는 경우 상위 인터럽트 컨트롤러에 파워가 공급되지 않는 경우 하위 인터럽트 컨트롤러도 인지하여 대응하는 구조가 필요하다. 이러한 경우를 위해 irq domain 하이라키 구조가 도입되었고 구현 시 기존 irq domain 에 parent 필드를 추가하여 사용할 수 있도록 수정하였다.

다음 그림은 인터럽트 컨트롤러가 2개 이상 중첩되는 경우와 한 개의 컨트롤러만 사용하는 경우를 비교하였다.

  • gic가 parent 인터럽트 컨트롤러이며 먼저 초기화된다.

 

IRQ Domain  추가 함수들

irq_domain_add_linear()

kernel/irq/irqdomain.c

/**
 * irq_domain_add_linear() - Allocate and register a linear revmap irq_domain.
 * @of_node: pointer to interrupt controller's device tree node.
 * @size: Number of interrupts in the domain.
 * @ops: map/unmap domain callbacks
 * @host_data: Controller private data pointer
 */
static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
                                         unsigned int size,
                                         const struct irq_domain_ops *ops,
                                         void *host_data)
{
        return __irq_domain_add(of_node, size, size, 0, ops, host_data);
}

size 만큼의 선형 리버스 매핑 블럭을 사용할 계획으로 도메인을 할당받아 구성한 후 추가한다.

  • 연속된 리버스 매핑 공간에서 자유로운 인터럽트 매핑 방식이 필요한 인터럽트 컨트롤러 드라이버에 사용된다.
    • 예) 약 30개 드라이버에서 사용한다.
      • arm,gic
      • arm,nvic
      • bcm,bcm2835
      • bcm,bcm2836
      • exynos-combiner
      • samsung,s3c24xx
      • armada-370-xp

 

__irq_domain_add()

kernel/irq/irqdomain.c

/**
 * __irq_domain_add() - Allocate a new irq_domain data structure
 * @of_node: optional device-tree node of the interrupt controller
 * @size: Size of linear map; 0 for radix mapping only
 * @hwirq_max: Maximum number of interrupts supported by controller
 * @direct_max: Maximum value of direct maps; Use ~0 for no limit; 0 for no
 *              direct mapping
 * @ops: domain callbacks
 * @host_data: Controller private data pointer
 *
 * Allocates and initialize and irq_domain structure.
 * Returns pointer to IRQ domain, or NULL on failure.
 */
struct irq_domain *__irq_domain_add(struct device_node *of_node, int size,
                                    irq_hw_number_t hwirq_max, int direct_max,
                                    const struct irq_domain_ops *ops,
                                    void *host_data)
{
        struct irq_domain *domain;

        domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size),
                              GFP_KERNEL, of_node_to_nid(of_node));
        if (WARN_ON(!domain))
                return NULL;

        /* Fill structure */
        INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
        domain->ops = ops;
        domain->host_data = host_data;
        domain->of_node = of_node_get(of_node);
        domain->hwirq_max = hwirq_max;
        domain->revmap_size = size;
        domain->revmap_direct_max_irq = direct_max;
        irq_domain_check_hierarchy(domain);

        mutex_lock(&irq_domain_mutex);
        list_add(&domain->link, &irq_domain_list);
        mutex_unlock(&irq_domain_mutex);

        pr_debug("Added domain %s\n", domain->name);
        return domain;
}
EXPORT_SYMBOL_GPL(__irq_domain_add);

하나의 irq 도메인을 추가하고 전달된 인수에 대해 다음과 같이 준비한다.

  • 인수 size가 주어지는 경우 size 만큼의 리니어 리버스 맵을 준비한다.
  • 인수 hwirq_max는 현재 도메인에서 최대 처리할 하드웨어 인터럽트 수이다.
  • 인수 direct_max가 주어지는 경우 해당 범위는 리버스 맵을  사용하지 않는다.
  • 인수 ops는 리버스 매핑과 변환 함수에 대한 콜백 후크를 제공한다.
  • 인수 host_data는 private 데이터 포인터를 전달할 수 있다.

 

  • 코드 라인 21~24에서 irq domain과 리버스 맵 배열을 준비하고 할당이 실패하는 경우 에러 값으로 null을 반환한다.
  • 코드 라인 27에서 tree 형태로 domain을 사용할 수 있도록 radix tree를 초기화한다.
  • 코드 라인 28~33에서 전달받은 인수를 domain에 설정한다.
    • domain->of_node는 인수로 전달받은 디바이스 노드와 연결된다.
  • 코드 라인 34에서 CONFIG_IRQ_DOMAIN_HIERARCHY 커널 옵션을 사용하여 domain이 hierarchy를 지원하는 경우 현재 domain에 HIERARCHY 플래그를 추가한다.
  • 코드 라인 36~38에서 전역 irq_domain_list의 선두에 현재 domain을 추가한다.

 

irq_domain_add_nomap()

kernel/irq/irqdomain.c

static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
                                         unsigned int max_irq,
                                         const struct irq_domain_ops *ops,
                                         void *host_data)
{
        return __irq_domain_add(of_node, 0, max_irq, max_irq, ops, host_data);
}

nomape 만큼의 선형 리버스 매핑 블럭을 사용할 계획으로 도메인을 할당받아 구성한 후 추가한다.

인터럽트 매핑이 불가능한 인터럽트 컨트롤러에 필요하다. 따라서 연속된 리버스 매핑 공간이 필요치 않다.

  • 대부분의 아키텍처에서 사용하지 않는 방식이다. 현재 4 개의 powerpc 아키텍처용 드라이버에서 사용한다.

 

irq_domain_add_legacy()

kernel/irq/irqdomain.c

/**
 * irq_domain_add_legacy() - Allocate and register a legacy revmap irq_domain.
 * @of_node: pointer to interrupt controller's device tree node.
 * @size: total number of irqs in legacy mapping
 * @first_irq: first number of irq block assigned to the domain
 * @first_hwirq: first hwirq number to use for the translation. Should normally
 *               be '0', but a positive integer can be used if the effective
 *               hwirqs numbering does not begin at zero.
 * @ops: map/unmap domain callbacks
 * @host_data: Controller private data pointer
 *
 * Note: the map() callback will be called before this function returns
 * for all legacy interrupts except 0 (which is always the invalid irq for
 * a legacy controller).
 */  
struct irq_domain *irq_domain_add_legacy(struct device_node *of_node,
                                         unsigned int size,
                                         unsigned int first_irq,
                                         irq_hw_number_t first_hwirq,
                                         const struct irq_domain_ops *ops,
                                         void *host_data)
{
        struct irq_domain *domain;

        domain = __irq_domain_add(of_node, first_hwirq + size,
                                  first_hwirq + size, 0, ops, host_data);
        if (domain)
                irq_domain_associate_many(domain, first_irq, first_hwirq, size);

        return domain;
}
EXPORT_SYMBOL_GPL(irq_domain_add_legacy);

size 만큼의 이미 만들어놓은 선형 리버스 매핑 블럭을 사용할 계획으로 도메인을 할당받아 구성한다. 매핑 후 변경이 필요 없는 구성에서 사용한다.

  • 미리 구성해 놓은 연속된 리버스 매핑 공간을 사용한 인터럽트 매핑 방식이 필요한 인터럽트 컨트롤러 드라이버에 사용된다.
    • 예) 약 10 여개 드라이버에서 사용한다.
      • arm,gic
      • mmp
      • samsung,s3c24xx
      • hip04

 

 

irq_domain_add_legacy_isa()

kernel/irq/irqdomain.c

static inline struct irq_domain *irq_domain_add_legacy_isa(
                                struct device_node *of_node,
                                const struct irq_domain_ops *ops,
                                void *host_data)
{
        return irq_domain_add_legacy(of_node, NUM_ISA_INTERRUPTS, 0, 0, ops,
                                     host_data);
}
  • 고정된 16(legacy ISA 하드웨어가 사용하는 최대 인터럽트 수)개 인터럽트 수 만큼의 이미 만들어놓은 선형 리버스 매핑 블럭을 사용할 계획으로 도메인을 할당받아 구성한다. 매핑 후 변경이 필요 없는 구성에서 사용한다.
    • 대부분의 아키텍처에서 사용하지 않는 방식이다. 현재 2 개의 powerpc 아키텍처용 드라이버에서 사용한다.

 

irq_domain_add_tree()

kernel/irq/irqdomain.c

static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
                                         const struct irq_domain_ops *ops,
                                         void *host_data)
{
        return __irq_domain_add(of_node, 0, ~0, 0, ops, host_data);
}

Radix tree로 관리되는 리버스 매핑 블럭을 사용할 계획으로 도메인을 할당받아 구성한다. 구성된 후 자유롭게 매핑/해제를 할 수 있다.

  • 하드웨어 인터럽트 번호가 매우 큰 시스템에서 메모리 낭비 없이 자유로운 인터럽트 매핑 방식이 필요한 인터럽트 컨트롤러 드라이버에 사용된다.
    • 예) arm에서는 최근 버전의 gic 컨트롤러 드라이버 3개가 사용하고, 다른 몇 개의 아키텍처에서도 사용한다.
      • arm,gic-v2m
      • arm,gic-v3
      • arm,gic-v3-its

 

irq_domain_add_hierarchy()

kernel/irq/irqdomain.c

/**
 * irq_domain_add_hierarchy - Add a irqdomain into the hierarchy
 * @parent:     Parent irq domain to associate with the new domain
 * @flags:      Irq domain flags associated to the domain
 * @size:       Size of the domain. See below
 * @node:       Optional device-tree node of the interrupt controller
 * @ops:        Pointer to the interrupt domain callbacks
 * @host_data:  Controller private data pointer
 *
 * If @size is 0 a tree domain is created, otherwise a linear domain.
 *
 * If successful the parent is associated to the new domain and the
 * domain flags are set.
 * Returns pointer to IRQ domain, or NULL on failure.
 */
struct irq_domain *irq_domain_add_hierarchy(struct irq_domain *parent,
                                            unsigned int flags,
                                            unsigned int size,
                                            struct device_node *node,
                                            const struct irq_domain_ops *ops,
                                            void *host_data)
{
        struct irq_domain *domain;

        if (size)
                domain = irq_domain_add_linear(node, size, ops, host_data);
        else
                domain = irq_domain_add_tree(node, ops, host_data);
        if (domain) {
                domain->parent = parent;
                domain->flags |= flags;
        }

        return domain;
}

주어진 인수 size에 따라 선형 또는 Radix tree로 관리되는 리버스 매핑 블럭을 사용할 계획으로 하이라키 도메인을 구성 시 사용한다. 구성된 후 자유롭게 매핑/해제를 할 수 있다.

  • domain 간 부모관계를 설정하고자 할 때 사용하며, 현재 몇 개의 arm 및 다른 아키텍처 드라이버에서 사용한다.
      • tegra
      • imx-gpcv2
      • mtk-sysirq
      • vf610-mscm-ir
      • crossbar

 

irq_domain_add_simple()

kernel/irq/irqdomain.c

/**
 * irq_domain_add_simple() - Register an irq_domain and optionally map a range of irqs
 * @of_node: pointer to interrupt controller's device tree node.
 * @size: total number of irqs in mapping
 * @first_irq: first number of irq block assigned to the domain,
 *      pass zero to assign irqs on-the-fly. If first_irq is non-zero, then
 *      pre-map all of the irqs in the domain to virqs starting at first_irq.
 * @ops: domain callbacks
 * @host_data: Controller private data pointer
 *
 * Allocates an irq_domain, and optionally if first_irq is positive then also
 * allocate irq_descs and map all of the hwirqs to virqs starting at first_irq.
 *
 * This is intended to implement the expected behaviour for most
 * interrupt controllers. If device tree is used, then first_irq will be 0 and
 * irqs get mapped dynamically on the fly. However, if the controller requires
 * static virq assignments (non-DT boot) then it will set that up correctly.
 */
struct irq_domain *irq_domain_add_simple(struct device_node *of_node,
                                         unsigned int size,
                                         unsigned int first_irq,
                                         const struct irq_domain_ops *ops,
                                         void *host_data)
{
        struct irq_domain *domain;

        domain = __irq_domain_add(of_node, size, size, 0, ops, host_data);
        if (!domain)
                return NULL;

        if (first_irq > 0) {
                if (IS_ENABLED(CONFIG_SPARSE_IRQ)) {
                        /* attempt to allocated irq_descs */
                        int rc = irq_alloc_descs(first_irq, first_irq, size,
                                                 of_node_to_nid(of_node));
                        if (rc < 0)
                                pr_info("Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
                                        first_irq);
                }
                irq_domain_associate_many(domain, first_irq, 0, size);
        }

        return domain;
}
EXPORT_SYMBOL_GPL(irq_domain_add_simple);

 

first_irq 번호 부터 0번 hwirq에 1:1로 size 만큼의 선형 리버스 매핑 블럭을 할당하고 도메인을 구성한다. 단 first_irq가 0인 경우 매핑 없이 domain만 추가한다.

  • offset 만큼의 irq 번호와 hwirq를 매핑하면 되는 간단한 시스템 드라이버에서 사용하며 약 10여개 드라이버에서 사용한다.
      • arm,vic
      • versatile-fpga
      • sa11x0

 

IRQ Domain  제거 함수

irq_domain_remove()

kernel/irq/irqdomain.c

/**
 * irq_domain_remove() - Remove an irq domain.
 * @domain: domain to remove
 *
 * This routine is used to remove an irq domain. The caller must ensure
 * that all mappings within the domain have been disposed of prior to
 * use, depending on the revmap type.
 */
void irq_domain_remove(struct irq_domain *domain)
{
        mutex_lock(&irq_domain_mutex);

        /*
         * radix_tree_delete() takes care of destroying the root
         * node when all entries are removed. Shout if there are
         * any mappings left.
         */
        WARN_ON(domain->revmap_tree.height);

        list_del(&domain->link);

        /*
         * If the going away domain is the default one, reset it.
         */
        if (unlikely(irq_default_domain == domain))
                irq_set_default_host(NULL);

        mutex_unlock(&irq_domain_mutex);

        pr_debug("Removed domain %s\n", domain->name);

        of_node_put(domain->of_node);
        kfree(domain);
}
EXPORT_SYMBOL_GPL(irq_domain_remove);

전역 irq_domain_list에서 요청한 irq domain을 제거하고 할당 해제한다.

  • 코드 라인 20에서 전역 irq_domain_list에서 요청한 irq domain을 제거한다.
  • 코드 라인 25~26에서 낮은 확률로 요청 domain이 irq_default_domain인 경우 irq_default_domain을 null로 설정한다.
    • arm이 아닌 하드 코딩된 몇 개의 아키텍처 드라이버에서 irq domain을 구현하지 않고 인터럽트 번호를 사용할 목적으로 irq_create_mapping() 함수에서 domain 대신 NULL을 인자로 사용한다.
  • 코드 라인 32에서 디바이스 노드의 참조를 해제하고 domain을 할당 해제한다.

 

리눅스 irq와 hw irq 매핑 생성

dynamic 매핑 방법을 사용하는 linear, tree 및 hierarchy 방식의 irq domain에서 사용한다.

irq_create_mapping()

kernel/irq/irqdomain.c

/**
 * irq_create_mapping() - Map a hardware interrupt into linux irq space
 * @domain: domain owning this hardware interrupt or NULL for default domain
 * @hwirq: hardware irq number in that domain space
 *
 * Only one mapping per hardware interrupt is permitted. Returns a linux
 * irq number.
 * If the sense/trigger is to be specified, set_irq_type() should be called
 * on the number returned from that call.
 */
unsigned int irq_create_mapping(struct irq_domain *domain,
                                irq_hw_number_t hwirq)
{
        int virq;

        pr_debug("irq_create_mapping(0x%p, 0x%lx)\n", domain, hwirq);

        /* Look for default domain if nececssary */
        if (domain == NULL)
                domain = irq_default_domain;
        if (domain == NULL) {
                WARN(1, "%s(, %lx) called with NULL domain\n", __func__, hwirq);
                return 0;
        }
        pr_debug("-> using domain @%p\n", domain);

        /* Check if mapping already exists */
        virq = irq_find_mapping(domain, hwirq);
        if (virq) {
                pr_debug("-> existing mapping on virq %d\n", virq);
                return virq;
        }
vim 
        /* Allocate a virtual interrupt number */
        virq = irq_domain_alloc_descs(-1, 1, hwirq,
                                      of_node_to_nid(domain->of_node));
        if (virq <= 0) {
                pr_debug("-> virq allocation failed\n");
                return 0;
        }

        if (irq_domain_associate(domain, virq, hwirq)) {
                irq_free_desc(virq);
                return 0;
        }

        pr_debug("irq %lu on domain %s mapped to virtual irq %u\n",
                hwirq, of_node_full_name(domain->of_node), virq);

        return virq;
}
EXPORT_SYMBOL_GPL(irq_create_mapping);

요청한 hwirq로 리눅스 irq를 배정 받아 매핑하고 그 번호를 반환한다.  hwirq를 음수로 지정하는 경우 리눅스 irq를 배정한다.

  • 코드 라인19~20에서 domain이 null로 지정된 경우 default domain을 선택한다.
  • 코드 라인 21~24에서 domain이 여전히 null인 경우 경고 메시지 출력과 함께 0을 반환한다.
  • 코드 라인 28~32에서 domain에서 hwirq 에 매핑된 값이 있는지 알아온다. 만일 이미 매핑이 존재하는 경우 경고 메시지 출력과 함께 0을 반환한다.
  • 코드 라인 35~40에서 domain에서 hwirq에 해당하는 리버스 매핑을 할당한다. 만일 할당하지 못한 경우 0을 반환한다.
  • 코드 라인 42~45에서 domain에서 virq와 hwirq의 매핑을 수행한다. 매핑 실패 시 0을 반환한다.

 

irq_create_direct_mapping()

kernel/irq/irqdomain.c

/**
 * irq_create_direct_mapping() - Allocate an irq for direct mapping
 * @domain: domain to allocate the irq for or NULL for default domain
 *
 * This routine is used for irq controllers which can choose the hardware
 * interrupt numbers they generate. In such a case it's simplest to use
 * the linux irq as the hardware interrupt number. It still uses the linear
 * or radix tree to store the mapping, but the irq controller can optimize
 * the revmap path by using the hwirq directly.
 */
unsigned int irq_create_direct_mapping(struct irq_domain *domain)
{
        unsigned int virq;

        if (domain == NULL)
                domain = irq_default_domain;

        virq = irq_alloc_desc_from(1, of_node_to_nid(domain->of_node));
        if (!virq) {
                pr_debug("create_direct virq allocation failed\n");
                return 0;
        }
        if (virq >= domain->revmap_direct_max_irq) {
                pr_err("ERROR: no free irqs available below %i maximum\n",
                        domain->revmap_direct_max_irq);
                irq_free_desc(virq);
                return 0;
        }
        pr_debug("create_direct obtained virq %d\n", virq);

        if (irq_domain_associate(domain, virq, virq)) {
                irq_free_desc(virq);
                return 0;
        }

        return virq;
}
EXPORT_SYMBOL_GPL(irq_create_direct_mapping);

irq 디스크립터를 하나 할당받은 후 1:1 직접 매핑 방법으로 hwirq 번호와 연결한다.

  • 코드 라인 15~16에서 domain이 주어지지 않은 경우 default domain을 선택한다.
  • 코드 라인 18~22에서 1개의 irq 디스크립터를 할당받아온다. 만일 할당이 실패한 경우 0을 반환한다.
  • 코드 라인 23~28에서 할당 받은 irq가 no 매핑 방법으로 사용할 수 있는 범위를 초과하는 경우에러 메시지를 출력하고 할당받은 irq 디스크립터를 다시 할당 해제한 후 0을 반환한다.
  • 코드 라인 31~34에서 할당받은 irq 번호로 hwirq 번호를 연결한다. 그런 후 Radix tree로 관리하는 리버스 맵에 hwirq와 irq가 동일하게 추가한다.

 

irq_create_strict_mappings()

kernel/irq/irqdomain.c

/**
 * irq_create_strict_mappings() - Map a range of hw irqs to fixed linux irqs
 * @domain: domain owning the interrupt range
 * @irq_base: beginning of linux IRQ range
 * @hwirq_base: beginning of hardware IRQ range
 * @count: Number of interrupts to map
 *
 * This routine is used for allocating and mapping a range of hardware
 * irqs to linux irqs where the linux irq numbers are at pre-defined
 * locations. For use by controllers that already have static mappings
 * to insert in to the domain.
 *
 * Non-linear users can use irq_create_identity_mapping() for IRQ-at-a-time
 * domain insertion.
 *
 * 0 is returned upon success, while any failure to establish a static
 * mapping is treated as an error.
 */
int irq_create_strict_mappings(struct irq_domain *domain, unsigned int irq_base,
                               irq_hw_number_t hwirq_base, int count)
{
        int ret;

        ret = irq_alloc_descs(irq_base, irq_base, count,
                              of_node_to_nid(domain->of_node));
        if (unlikely(ret < 0))
                return ret;

        irq_domain_associate_many(domain, irq_base, hwirq_base, count);
        return 0;
}
EXPORT_SYMBOL_GPL(irq_create_strict_mappings);

irq_base 번호부터 count 수 만큼 irq 디스크립터를 할당받은 후 hwirq_base 부터 count 수 만큼 매핑 한다.

  • 코드 라인 24~27에서 irq_base 번호부터 count 만큼의 irq 디스크립터를 할당받아온다. 만일 할당이 실패하는 경우 함수를 빠져나가낟.
  • 코드 라인 29에서 irq_base 번호와 hwirq_base 번호를 count 수 만큼 매핑한다.

 

irq_create_of_mapping()

kernel/irq/irqdomain.c

unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
        struct irq_domain *domain;
        irq_hw_number_t hwirq;
        unsigned int type = IRQ_TYPE_NONE;
        int virq;

        domain = irq_data->np ? irq_find_host(irq_data->np) : irq_default_domain;
        if (!domain) {
                pr_warn("no irq domain found for %s !\n",
                        of_node_full_name(irq_data->np));
                return 0;
        }

        /* If domain has no translation, then we assume interrupt line */
        if (domain->ops->xlate == NULL)
                hwirq = irq_data->args[0];
        else {
                if (domain->ops->xlate(domain, irq_data->np, irq_data->args,
                                        irq_data->args_count, &hwirq, &type))
                        return 0;
        }

        if (irq_domain_is_hierarchy(domain)) {
                /*
                 * If we've already configured this interrupt,
                 * don't do it again, or hell will break loose.
                 */
                virq = irq_find_mapping(domain, hwirq);
                if (virq)
                        return virq;

                virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, irq_data);
                if (virq <= 0)
                        return 0;
        } else {
                /* Create mapping */
                virq = irq_create_mapping(domain, hwirq);
                if (!virq)
                        return virq;
        }

        /* Set type if specified and different than the current one */
        if (type != IRQ_TYPE_NONE &&
            type != irq_get_trigger_type(virq))
                irq_set_irq_type(virq, type);
        return virq;
}
EXPORT_SYMBOL_GPL(irq_create_of_mapping);

Device Tree 스크립트에서 인터럽트 컨르롤러와 연결되는 디바이스에 대해 매핑을 수행한다.

  • 코드 라인 8에서 phandle이 가리키는 디바이스 노드로 irq domain을 알아온다. 없는 경우 default  domain을 사용한다.
  • 코드 라인 9~13에서 domain이 없는 경우 경고 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 16~17에서 도메인에 등록한 xlate 후크 함수가 없는 경우 첫 번째 인자값을 hwirq로 사용한다.
  • 코드 라인 18~22에서 도메인에 등록한 xlate 후크에 등록한 함수를 호출하여 hwirq 값과 타입을 구해온다. 만일 실패하는 경우 함수를 빠져나간다.
    • Device Tree의 인터럽트를 사용하는 각 장치의 “interrupts = { a1, a2 }”에서 인수 2 개로 hwirq 값을 알아온다.
  • 코드 라인 24~31에서 hierarch를 지원하는 irq domain인 경우이미 hwirq 값으로 매핑된 리눅스 irq 값이 있는 경우 해당 irq 값을 반환한다.
  • 코드 라인 33~35에서 매핑을 못찾은 경우 하나의 irq 디스크립터를 할당받아온다. 만일 할당이 실패하는 경우 0을 반환한다.
  • 코드 라인 36~41에서 디스크립터를 하나 할당받은 후 hwirq에 매핑한 후 할당한 irq 번호를 알아온다. 만일 매핑 생성이 실패하는 경우 0을 반환한다.
  • 코드 라인 44~47에서 해당 irq 번호에 대한 트리거 타입이 지정된 경우 인터럽트 컨트롤러 드라이버(chip)의 후크 함수를 통해 트리거 타입을 설정한다.
    • 해당 인터럽트 컨트롤러는 트리거 컨트롤러 레지스터를 설정하여 해당 인터럽트 라인의 트리거 타입을 설정한다.

 

예) irq2의 uart 디바이스

                uart0: uart@7e201000 {
                        compatible = "arm,pl011", "arm,primecell";
                        reg = <0x7e201000 0x1000>;
                        interrupts = <2 25>;
  • uart의 인터럽트의 인자는 2개로 <2 25> -> bank #2, bit 25 인터럽트
  • hwirq로 변환 -> 57
  • 할당한 irq 디스크립터의 irq=83이라고 가정하면 이 irq(83)과 hwirq(57)을 매핑한다.

 

트리거 타입

플래그에서 4 bit를 트리거 타입에 사용한다.

  • IRQ_TYPE_NONE = 0x00000000,
    • 지정되지 않은 트리거 타입
  • IRQ_TYPE_EDGE_RISING = 0x00000001,
    • 상승 시 트리거
  • IRQ_TYPE_EDGE_FALLING = 0x00000002,
    • 하강 시 트리거
  • IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING),
    • 상승 및 하강 시 둘다 트리거
  • IRQ_TYPE_LEVEL_HIGH = 0x00000004,
    • 레벨이 high에 있는 상태에서 트리거
  • IRQ_TYPE_LEVEL_LOW = 0x00000008,
    • 레벨이 low에 있는 상태에서 트리거

 

irq_find_host()

kernel/irq/irqdomain.c

/**
 * irq_find_host() - Locates a domain for a given device node
 * @node: device-tree node of the interrupt controller
 */
struct irq_domain *irq_find_host(struct device_node *node)
{
        struct irq_domain *h, *found = NULL;
        int rc;

        /* We might want to match the legacy controller last since
         * it might potentially be set to match all interrupts in
         * the absence of a device node. This isn't a problem so far
         * yet though...
         */
        mutex_lock(&irq_domain_mutex);
        list_for_each_entry(h, &irq_domain_list, link) {
                if (h->ops->match)
                        rc = h->ops->match(h, node);
                else
                        rc = (h->of_node != NULL) && (h->of_node == node);

                if (rc) {
                        found = h;
                        break;
                }
        }
        mutex_unlock(&irq_domain_mutex);
        return found;
}
EXPORT_SYMBOL_GPL(irq_find_host);

주어진 디바이스 노드에 해당하는 irq domain을 찾아온다.

  • 코드 라인 16에서 irq_domain_list에 등록한 모든 엔트리에 대해 루프를 돈다.
  • 코드 라인 17~18에서 irq_domain의 ops->match 후크에 등록된 함수가 있으면 해당 irq_domain을 반환한다.
    • 실제 match 후크 함수를 사용한 사례를 못 찾았다.
  • 코드 라인 19~20에서 irq_domain에 등록한 디바이스 노드와 동일한 경우 해당 irq_domain을 반환한다.

 

리버스 매핑 검색 (hwirq -> irq)

irq_find_mappings()

kernel/irq/irqdomain.c

/**
 * irq_find_mapping() - Find a linux irq from an hw irq number.
 * @domain: domain owning this hardware interrupt
 * @hwirq: hardware irq number in that domain space
 */
unsigned int irq_find_mapping(struct irq_domain *domain,
                              irq_hw_number_t hwirq)
{
        struct irq_data *data;

        /* Look for default domain if nececssary */
        if (domain == NULL)
                domain = irq_default_domain;
        if (domain == NULL)
                return 0;

        if (hwirq < domain->revmap_direct_max_irq) {
                data = irq_domain_get_irq_data(domain, hwirq);
                if (data && data->hwirq == hwirq)
                        return hwirq;
        }

        /* Check if the hwirq is in the linear revmap. */
        if (hwirq < domain->revmap_size)
                return domain->linear_revmap[hwirq];

        rcu_read_lock();
        data = radix_tree_lookup(&domain->revmap_tree, hwirq);
        rcu_read_unlock();
        return data ? data->irq : 0;
}
EXPORT_SYMBOL_GPL(irq_find_mapping);

irq domain 내에서 hwirq에 매핑된 irq 번호를 검색한다. 매핑되지 않은 경우 0을 반환한다.

  • 코드 라인 12~13에서 domain이 지정되지 않은 경우 default domain을 사용한다.
  • 코드 라인 14~15에서 여전히 domain이 null인 경우 0을 반환한다.
  • 코드 라인 17~21에서 no 매핑 구간에서 irq(=hwirq) 값을 알아온다.
    • hwirq가 리버스 매핑이 필요 없는 구간인 경우 hwirq 값으로 irq_data를 구해온다. 만일 data->hwirq 값과 동일한 경우에 그 대로 hwirq 값을 반환한다.
  • 코드 라인 24~25에서 리니어 매핑 구간에서 hwirq에 해당하는 irq 값을 알아온다.
    • hwirq가 리버스 매핑 범위 이내인 경우 리니어 리버스 매핑된 값을 반환한다.
  • 코드 라인 27~30에서 tree 매핑 구간에서 hwirq로 검색하여 irq 값을 알아온다.
    • radix tree에서 hwirq로 검색하여 irq_data를 알아온 다음 data->irq를 반환한다.

 

irq domain에 연결할 irq 디스크립터 할당과 해제

irq_domain_alloc_descs()

kernel/irq/irqdomain.c

static int irq_domain_alloc_descs(int virq, unsigned int cnt,
                                  irq_hw_number_t hwirq, int node)
{               
        unsigned int hint;

        if (virq >= 0) {
                virq = irq_alloc_descs(virq, virq, cnt, node);
        } else {
                hint = hwirq % nr_irqs;
                if (hint == 0)
                        hint++;
                virq = irq_alloc_descs_from(hint, cnt, node);
                if (virq <= 0 && hint > 1)
                        virq = irq_alloc_descs_from(1, cnt, node);
        }

        return virq;
}

고정 irq 번호를 요청한 경우 그 번호부터 cnt 수 만큼 irq 디스크립터를 할당하고 현재 모듈을 irq 디스크립터의 owner로 설정한다. 단 고정 irq 번호를 요청한 경우가 아니면 다음과 같은 방식으로 irq 번호를 배정한다.

  • hwirq 번호를 irq 번호로 할당 시도한다. 단 hwirq 번호가 0인 경우 hwirq + 1을 irq 번호로 사용한다.
  • 할당이 실패한 경우 hwirq 번호가 2 이상이면 irq 번호를 1부터 다시 한 번 시도한다.

 

hwirq 번호를 irq 번호(hwirq 번호가 0부터 시작하는 경우 irq 번호는 1부터)로 cnt 수 만큼 할당받는다. 단 hwirq + 1을 사용하여 할당 실패 시 hwirq를 1로 다시 시도한다.

  • 코드 라인 6~7에서 고정 irq 번호를 요청한 경우 그 번호부터 cnt 수 만큼 irq 디스크립터를 할당하고 현재 모듈을 irq 디스크립터의 owner로 설정한다.
  • 코드 라인 8~12에서 hwirq 번호부터 cnt 수 만큼 irq 디스크립터를 할당하고 현재 모듈을 irq 디스크립터의 owner로 설정한다. 만일 hwirq가 0부터 시작하는 경우 irq 번호는 1부터 시작 한다.
  • 코드 라인 13~14에서 만일 할당이 실패한 경우 hwirq 번호가 2 이상이면 irq 번호를 1부터 다시 한 번 시도한다.

 

irq_alloc_descs()

include/linux/irq.h

#define irq_alloc_descs(irq, from, cnt, node)   \
        __irq_alloc_descs(irq, from, cnt, node, THIS_MODULE)

요청 irq 번호부터 cnt 수 만큼 irq 디스크립터를 할당하고  현재 모듈을 irq 디스크립터의 owner로 설정한다.

 

irq_alloc_descs_from()

include/linux/irq.h

#define irq_alloc_descs_from(from, cnt, node)   \
        irq_alloc_descs(-1, from, cnt, node)

새로운 irq 번호로 cnt 수 만큼 irq 디스크립터를 할당하고  현재 모듈을 irq 디스크립터의 owner로 설정한다.

 

__irq_alloc_descs()

kernel/irq/irqdesc.c

/**     
 * irq_alloc_descs - allocate and initialize a range of irq descriptors
 * @irq:        Allocate for specific irq number if irq >= 0
 * @from:       Start the search from this irq number
 * @cnt:        Number of consecutive irqs to allocate.
 * @node:       Preferred node on which the irq descriptor should be allocated
 * @owner:      Owning module (can be NULL)
 *
 * Returns the first irq number or error code
 */
int __ref
__irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node,
                  struct module *owner)
{
        int start, ret;

        if (!cnt)
                return -EINVAL;

        if (irq >= 0) {
                if (from > irq)
                        return -EINVAL;
                from = irq;
        } else {
                /*
                 * For interrupts which are freely allocated the
                 * architecture can force a lower bound to the @from
                 * argument. x86 uses this to exclude the GSI space.
                 */
                from = arch_dynirq_lower_bound(from);
        }

        mutex_lock(&sparse_irq_lock);

        start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS,
                                           from, cnt, 0);
        ret = -EEXIST;
        if (irq >=0 && start != irq)
                goto err;

        if (start + cnt > nr_irqs) {
                ret = irq_expand_nr_irqs(start + cnt);
                if (ret)
                        goto err; 
        }

        bitmap_set(allocated_irqs, start, cnt);
        mutex_unlock(&sparse_irq_lock);
        return alloc_descs(start, cnt, node, owner);

err:
        mutex_unlock(&sparse_irq_lock);
        return ret;
}
EXPORT_SYMBOL_GPL(__irq_alloc_descs);

고정 irq 번호를 요청한 경우 그 번호부터 cnt 수 만큼 irq 디스크립터를 할당하고 irq 디스크립터의 owner를 설정한다. 단 고정 irq 번호를 요청한 경우가 아니면 from 번호부터 배정한다.

  • 코드 라인 17~18에서 cnt가 지정되지 않은 경우 -EINVAL 에러로 반환한다.
  • 코드 라인 20~23에서 고정 irq 번호(0번 이상)를 요청한 경우 from에 고정 irq 번호를 대입한다. 단 from이 고정 irq 번호보다 큰 경우 -EINVAL 에러로 반환한다.
  • 코드 라인 24~31에서 새 irq 번호를 발부받으려 하는 경우 from 부터 시작하도록 한다. 단 아키텍처에 따라 from 값이 바뀔 수 있다.
    • x86에서는 GSI 공간을 피해야 한다.
  • 코드 라인 35~36에서 인터럽트 할당 비트맵에서 from 비트부터 연속된 cnt 수 만큼의 할당되지 않은 비트를 찾는다.
    • 참고: Bitmap Operations | 문c
    • 예) 0b0000_0000_1111_0000_1111_0000
      • from=5, cnt=2 -> start=8
      • from=5, cnt=5 -> start=16
  • 코드 라인 37~39에서 고정 irq 번호를 요청하였으면서 찾은 시작 irq 번호와 다른 경우 -EEXIST 에러를 반환한다.
  • 코드 라인 41~45에서 할당할 공간이 부족한 경우 irq 공간을 필요한 수 만큼 확장한다. 확장이 불가능한 경우 -EEXIST 에러를 반환한다.
  • 코드 라인 47에서 인터럽트 할당 비트맵에 start 비트부터 cnt 수 만큼 1로 설정하여 할당되었음을 표시한다.
  • 코드 라인 49에서start irq 디스크립터 부터 cnt 수 만큼 모듈 owner를 설정하고 start irq 번호를 반환한다.
    • 만일 SPARSE IRQ 설정된 경우는 해당 irq 디스크립터를 할당도 한다.

 

alloc_descs() – FLAT IRQ 용

kernel/irq/irqdesc.c

static inline int alloc_descs(unsigned int start, unsigned int cnt, int node,
                              struct module *owner)
{
        u32 i;

        for (i = 0; i < cnt; i++) {
                struct irq_desc *desc = irq_to_desc(start + i);

                desc->owner = owner;
        }
        return start;
}

start irq 디스크립터 부터 cnt 수 만큼 모듈 owner를 설정하고 start irq 번호를 반환한다.

 

alloc_descs() – SPARSE IRQ 용

kernel/irq/irqdesc.c

static int alloc_descs(unsigned int start, unsigned int cnt, int node,
                       struct module *owner)
{
        struct irq_desc *desc;
        int i;

        for (i = 0; i < cnt; i++) {
                desc = alloc_desc(start + i, node, owner);
                if (!desc)
                        goto err;
                mutex_lock(&sparse_irq_lock);
                irq_insert_desc(start + i, desc);
                mutex_unlock(&sparse_irq_lock);
        }
        return start;

err:
        for (i--; i >= 0; i--)
                free_desc(start + i);

        mutex_lock(&sparse_irq_lock);
        bitmap_clear(allocated_irqs, start, cnt);
        mutex_unlock(&sparse_irq_lock);
        return -ENOMEM;
}

start irq 디스크립터 부터 cnt 수 만큼 irq 디스크립터를 할당하고 모듈 owner를 설정한 후 radix tree에 추가한다. 그런 후 start irq 번호를 반환한다.

  • 코드 라인 7~10에서 start 번호부터 시작하여 cnt 수 만큼 irq 디스크립터를 할당하고 모듈 owner를 설정한다.  만일 할당이 실패하면 err 레이블로 이동한다.
  • 코드 라인 11~15에서 할당한 irq 디스크립터를 radix tree에 추가한다. 그런 후 start irq 번호를 반환한다.
  • 코드 라인 17~24에서 이 루틴에서 할당한 모든 irq 디스크립터를 해제하고 allocated_irqs 비트맵에서 해당하는 비트들을 클리어한 후 -ENOMEM 에러를 반환한다.

 

리눅스 irq와 hw irq 연결

irq_domain_associate()

kernel/irq/irqdomain.c

int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
                         irq_hw_number_t hwirq)
{
        struct irq_data *irq_data = irq_get_irq_data(virq);
        int ret;

        if (WARN(hwirq >= domain->hwirq_max,
                 "error: hwirq 0x%x is too large for %s\n", (int)hwirq, domain->name))
                return -EINVAL;
        if (WARN(!irq_data, "error: virq%i is not allocated", virq))
                return -EINVAL;
        if (WARN(irq_data->domain, "error: virq%i is already associated", virq))
                return -EINVAL;

        mutex_lock(&irq_domain_mutex);
        irq_data->hwirq = hwirq;
        irq_data->domain = domain;
        if (domain->ops->map) {
                ret = domain->ops->map(domain, virq, hwirq);
                if (ret != 0) {
                        /*
                         * If map() returns -EPERM, this interrupt is protected
                         * by the firmware or some other service and shall not
                         * be mapped. Don't bother telling the user about it.
                         */
                        if (ret != -EPERM) {
                                pr_info("%s didn't like hwirq-0x%lx to VIRQ%i mapping (rc=%d)\n",
                                       domain->name, hwirq, virq, ret);
                        }
                        irq_data->domain = NULL;
                        irq_data->hwirq = 0;
                        mutex_unlock(&irq_domain_mutex);
                        return ret;
                }

                /* If not already assigned, give the domain the chip's name */
                if (!domain->name && irq_data->chip)
                        domain->name = irq_data->chip->name;
        }

        if (hwirq < domain->revmap_size) {
                domain->linear_revmap[hwirq] = virq;
        } else {
                mutex_lock(&revmap_trees_mutex);
                radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
                mutex_unlock(&revmap_trees_mutex);
        }
        mutex_unlock(&irq_domain_mutex);

        irq_clear_status_flags(virq, IRQ_NOREQUEST);

        return 0;
}
EXPORT_SYMBOL_GPL(irq_domain_associate);

irq 디스크립터와 hwirq를 매핑한다. 도메인의 ops->map 후크에 등록한 콜백 함수가 있는 경우 해당 컨트롤러를 통해 매핑을 설정한다.

  • 코드 라인 4에서 irq 디스크립터의 irq_data를 알아온다.
  • 코드 라인 7~13에서 다음과 같이 에러가 있는 경우 경고 메시지를 출력하고 -EINVAL 에러를 반환한다.
    • hwirq 번호가 도메인의 hwirq_max 이상인 경우
    • irq 디스크립터가 할당되어 있지 않은 경우
    • 이미 domain에 연결되어 있는 경우
  • 코드 라인 16~17에서 irq_data의 hwirq와 domain 정보를 설정한다.
  • 코드 라인 18~19에서 irq domain의 map 후크에 핸들러 함수가 연결된 경우 호출하여 컨트롤러로 하여금 매핑을 수행하게 한다.
  • 코드 라인 20~34에서 만일 매핑이 실패한 경우 허가되지 않은 irq인 경우 단순 메시지만 출력하고 irq_data의 domain과 hwirq에 null과 0을 설정후 함수를 빠져나간다.
  • 코드 라인 37에서 domain에 이름이 설정되지 않은 경우 컨트롤러 이름을 대입한다.
  • 코드 라인 39~40에서 linear 방식에서 사용되는 irq인 경우 리니어용 리버스 맵 테이블에서 hwirq에 대응하는 곳에 irq를 설정한다.
  • 코드 라인 41~45에서 tree 방식에서 사용되는 irq인 경우 radix tree용 리버스 맵에 hwirq를 추가하고 irq_data(컨트롤러 기준 hwirq 번호)를 대입한다.
  • 코드 라인 48에서 irq 라인 상태에에서 norequest 플래그를 클리어한다.
    • IRQ_NOREQUEST
      • 현재 irq 라인에 요청된 인터럽트가 없다.

 

irq_domain_associate_many()

kernel/irq/irqdomain.c

void irq_domain_associate_many(struct irq_domain *domain, unsigned int irq_base,
                               irq_hw_number_t hwirq_base, int count)
{
        int i;

        pr_debug("%s(%s, irqbase=%i, hwbase=%i, count=%i)\n", __func__,
                of_node_full_name(domain->of_node), irq_base, (int)hwirq_base, count);

        for (i = 0; i < count; i++) {
                irq_domain_associate(domain, irq_base + i, hwirq_base + i);
        }
}
EXPORT_SYMBOL_GPL(irq_domain_associate_many);

irq_base로 시작하는 irq 디스크립터를 count 수 만큼 hwirq 번호부터 매핑한다. 해당 도메인의 ops->map 후크에 등록한 콜백 함수가 있는 경우 해당 컨트롤러를 통해 각각의 매핑을 설정한다.

 

리눅스 irq와 hw irq 연결 해제

irq_domain_disassociate()

kernel/irq/irqdomain.c

void irq_domain_disassociate(struct irq_domain *domain, unsigned int irq)
{
        struct irq_data *irq_data = irq_get_irq_data(irq);
        irq_hw_number_t hwirq;

        if (WARN(!irq_data || irq_data->domain != domain,
                 "virq%i doesn't exist; cannot disassociate\n", irq))
                return;

        hwirq = irq_data->hwirq;
        irq_set_status_flags(irq, IRQ_NOREQUEST);

        /* remove chip and handler */
        irq_set_chip_and_handler(irq, NULL, NULL);

        /* Make sure it's completed */
        synchronize_irq(irq);

        /* Tell the PIC about it */
        if (domain->ops->unmap)
                domain->ops->unmap(domain, irq);
        smp_mb();

        irq_data->domain = NULL;
        irq_data->hwirq = 0;

        /* Clear reverse map for this hwirq */
        if (hwirq < domain->revmap_size) {
                domain->linear_revmap[hwirq] = 0;
        } else {
                mutex_lock(&revmap_trees_mutex);
                radix_tree_delete(&domain->revmap_tree, hwirq);
                mutex_unlock(&revmap_trees_mutex);
        }
}

요청 irq에 대해 매핑된 hwirq와의 매핑을 해제한다. 도메인의 ops->unmap 후크에 등록한 콜백 함수가 있는 경우 해당 컨트롤러를 통해 매핑 해제를 설정한다.

 

arch/arm/mach-bcm2709/armctrl.c

static struct irq_domain_ops armctrl_ops = {
        .xlate = armctrl_xlate
};

bcm2708 및 bcm2709는 리눅스 irq와 hw irq의 변환은 인터럽트 컨트롤러에 고정되어 있어 sw로 매핑 구성을 변경할 수 없다. 따라서 매핑 및 언매핑용 함수는 제공되지 않고 argument -> hwirq 변환 함수만 제공한다.

  • argument
    • “interrupts = < x, … > 값

 

armctrl_xlate()

arch/arm/mach-bcm2709/armctrl.c

/* from drivers/irqchip/irq-bcm2835.c */
static int armctrl_xlate(struct irq_domain *d, struct device_node *ctrlr,
        const u32 *intspec, unsigned int intsize,
        unsigned long *out_hwirq, unsigned int *out_type)
{
        if (WARN_ON(intsize != 2))
                return -EINVAL;

        if (WARN_ON(intspec[0] >= NR_BANKS))
                return -EINVAL;

        if (WARN_ON(intspec[1] >= IRQS_PER_BANK))
                return -EINVAL;

        if (WARN_ON(intspec[0] == 0 && intspec[1] >= NR_IRQS_BANK0))
                return -EINVAL;

        if (WARN_ON(intspec[0] == 3 && intspec[1] > 3 && intspec[1] != 5 && intspec[1] != 9))
                return -EINVAL;

        if (intspec[0] == 0) 
                *out_hwirq = ARM_IRQ0_BASE + intspec[1];
        else if (intspec[0] == 1)
                *out_hwirq = ARM_IRQ1_BASE + intspec[1];
        else if (intspec[0] == 2)
                *out_hwirq = ARM_IRQ2_BASE + intspec[1];
        else
                *out_hwirq = ARM_IRQ_LOCAL_BASE + intspec[1];

        /* reverse remap_irqs[] */
        switch (*out_hwirq) {
        case INTERRUPT_VC_JPEG:
                *out_hwirq = INTERRUPT_JPEG;
                break;
        case INTERRUPT_VC_USB:
                *out_hwirq = INTERRUPT_USB;
                break;
        case INTERRUPT_VC_3D:
                *out_hwirq = INTERRUPT_3D;
                break;
        case INTERRUPT_VC_DMA2:
                *out_hwirq = INTERRUPT_DMA2;
                break;
        case INTERRUPT_VC_DMA3:
                *out_hwirq = INTERRUPT_DMA3;
                break;
        case INTERRUPT_VC_I2C:
                *out_hwirq = INTERRUPT_I2C;
                break;
        case INTERRUPT_VC_SPI:
                *out_hwirq = INTERRUPT_SPI;
                break;
        case INTERRUPT_VC_I2SPCM:
                *out_hwirq = INTERRUPT_I2SPCM;
                break;
        case INTERRUPT_VC_SDIO:
                *out_hwirq = INTERRUPT_SDIO;
                break;
        case INTERRUPT_VC_UART:
                *out_hwirq = INTERRUPT_UART;
                break;
        case INTERRUPT_VC_ARASANSDIO:
                *out_hwirq = INTERRUPT_ARASANSDIO;
                break;
        }

        *out_type = IRQ_TYPE_NONE;
        return 0;
}

DT 스크립트에 설정한 argumnet 값으로 hwirq 번호를 알아온다.

  • 인터럽트를 사용하는 디바이스는 “interrupts = { bank, irq }” 속성 값을 전달한다.
    • bcm2708(rpi) 및 bcm2709(rpi2)에서는 2 개의 인수를 받는데 처음 것은 bank 이고, 두 번째 것은 각 뱅크에 해당하는 개별 인터럽트 번호(0~31번 )이다.
    • rpi2 예) out_hwirq = 0~85, 96~99, 101, 105

 

  • 코드 라인 6~7에서 DT 스크립트에서 설정한 “interrupts = { bank, irq }” 형태로 인수가 2개가 아닌 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 9~10에서 bank 값이 4 이상인 경우 -EINVAL 에러를 반환한다.
    • rpi: 0~2까지 가능
    • rpi2: 0~3까지 가능
  • 코드 라인 12~13에서 irq 값이 bank당 최대 값 수(32) 이상인 경우 -EINVAL 에러를 반환한다.
    • rpi & rpi2: bank당 0~31까지 가능
  • 코드 라인 15~16에서 0번 bank를 사용하는 irq 값이 bank #0 최대 인터럽트 수 이상인 경우 -EIVAL 에러를 반환한다.
    • rpi & rpi2: bank당 0~20까지 가능
  • 코드 라인 18~19에서 3번 bank의 경우 irq 번호가 0~3, 5 및 9번이 아닌 경우 -EINVAL 에러를 반환한다.
    • 0~3: core timer, 5=mail box #1, 9=pmu
  • 코드 라인 21~22에서 bank 0번의 경우 64번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 23~24에서 bank 1번의 경우 0번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 25~26에서 bank 2번의 경우 32번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 27~28에서 bank 3번의 경우 96번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 41~65에서 Video Core에서 사용하는 인터럽트들(#0 ~ #64) 중 11개가 공유되어 Local ARM에 라우팅(#74~#84) 된다.
    • 예) 7(vc_usb)  -> 74(arm_usb)
  • 코드 라인 67~68에서 에러가 없는 경우 out_type에 IRQ_TYPE_NONE을 대입한후 0을 반환한다.

 

기타 함수

  • irq_set_default_host()
  • irq_domain_insert_irq()
  • irq_domain_remove_irq()
  • irq_domain_insert_irq_data()
  • irq_domain_free_irq_data()
  • irq_domain_alloc_irq_data()
  • irq_domain_get_irq_data()
  • irq_domain_set_hwirq_and_chip()
  • irq_domain_set_info()
  • irq_domain_reset_irq_data()
  • irq_domain_free_irqs_common()
  • irq_domain_free_irqs_top()
  • irq_domain_is_auto_recursive()
  • irq_domain_free_irqs_recursive()
  • irq_domain_alloc_irqs_recursive()
  • __irq_domain_alloc_irqs()
  • irq_domain_free_irqs()
  • irq_domain_alloc_irqs_parent()
  • irq_domain_free_irqs_parent()
  • irq_domain_activate_irq()
  • irq_domain_deactivate_irq()
  • irq_domain_check_hierarchy()
  • irq_domain_get_irq_data()

 

FS 디버깅이 가능한 커널에서 irq domain 구성 참고

/sys/kernel/debug# cat irq_domain_mapping 
 name              mapped  linear-max  direct-max  devtree-node
 IR-IO-APIC            24          24           0  
 DMAR-MSI               2           0           0  
 IR-PCI-MSI             6           0           0  
 (null)             65563       65536           0  
 IR-PCI-MSI             1           0           0  
 (null)             65536       65536           0  
 (null)                 0           0           0  
 (null)                 0           0           0  
*(null)                31           0           0  
irq    hwirq    chip name        chip data           active  type            domain
    1  0x00001  IR-IO-APIC       0xffff8802158a6f80     *    LINEAR          IR-IO-APIC
    3  0x00003  IR-IO-APIC       0xffff880214972180          LINEAR          IR-IO-APIC
    4  0x00004  IR-IO-APIC       0xffff880214972280          LINEAR          IR-IO-APIC
    5  0x00005  IR-IO-APIC       0xffff880214972380          LINEAR          IR-IO-APIC
    6  0x00006  IR-IO-APIC       0xffff880214972480          LINEAR          IR-IO-APIC
    7  0x00007  IR-IO-APIC       0xffff880214972580          LINEAR          IR-IO-APIC
    8  0x00008  IR-IO-APIC       0xffff880214972680     *    LINEAR          IR-IO-APIC
    9  0x00009  IR-IO-APIC       0xffff880214972780     *    LINEAR          IR-IO-APIC
   10  0x0000a  IR-IO-APIC       0xffff880214972880          LINEAR          IR-IO-APIC
   11  0x0000b  IR-IO-APIC       0xffff880214972980          LINEAR          IR-IO-APIC
   12  0x0000c  IR-IO-APIC       0xffff880214972a80     *    LINEAR          IR-IO-APIC
   13  0x0000d  IR-IO-APIC       0xffff880214972b80          LINEAR          IR-IO-APIC
   14  0x0000e  IR-IO-APIC       0xffff880214972c80          LINEAR          IR-IO-APIC
   15  0x0000f  IR-IO-APIC       0xffff880214972d80          LINEAR          IR-IO-APIC
   16  0x00010  IR-IO-APIC       0xffff8800d3e4d940     *    LINEAR          IR-IO-APIC
   17  0x00011  IR-IO-APIC       0xffff8800d3e4da40          LINEAR          IR-IO-APIC
   18  0x00012  IR-IO-APIC       0xffff8800d3e4db40          LINEAR          IR-IO-APIC
   19  0x00013  IR-IO-APIC       0xffff8802147d3bc0          LINEAR          IR-IO-APIC
   20  0x00014  IR-IO-APIC       0xffff8800350411c0          LINEAR          IR-IO-APIC
   22  0x00016  IR-IO-APIC       0xffff88020f9e8e40          LINEAR          IR-IO-APIC
   23  0x00017  IR-IO-APIC       0xffff880035670000     *    LINEAR          IR-IO-APIC
   24  0x00000  DMAR-MSI                     (null)     *     RADIX          DMAR-MSI
   25  0x00001  DMAR-MSI                     (null)     *     RADIX          DMAR-MSI
   26  0x50000  IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI
   27  0x7d000  IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI
   28  0x58000  IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI
   29  0x08000  IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI
   30  0x6c000  IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI
   31  0x64000  IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI
   32  0x180000 IR-PCI-MSI                   (null)     *     RADIX          IR-PCI-MSI

 

irq_domain 구조체

include/linux/irqdomain.h

/**
 * struct irq_domain - Hardware interrupt number translation object
 * @link: Element in global irq_domain list.
 * @name: Name of interrupt domain
 * @ops: pointer to irq_domain methods
 * @host_data: private data pointer for use by owner.  Not touched by irq_domain
 *             core code.
 * @flags: host per irq_domain flags
 *
 * Optional elements
 * @of_node: Pointer to device tree nodes associated with the irq_domain. Used
 *           when decoding device tree interrupt specifiers.
 * @gc: Pointer to a list of generic chips. There is a helper function for
 *      setting up one or more generic chips for interrupt controllers
 *      drivers using the generic chip library which uses this pointer.
 * @parent: Pointer to parent irq_domain to support hierarchy irq_domains
 *
 * Revmap data, used internally by irq_domain
 * @revmap_direct_max_irq: The largest hwirq that can be set for controllers that
 *                         support direct mapping
 * @revmap_size: Size of the linear map table @linear_revmap[]
 * @revmap_tree: Radix map tree for hwirqs that don't fit in the linear map
 * @linear_revmap: Linear table of hwirq->virq reverse mappings
 */
struct irq_domain {
        struct list_head link;
        const char *name;
        const struct irq_domain_ops *ops;
        void *host_data;
        unsigned int flags;

        /* Optional data */
        struct device_node *of_node;
        struct irq_domain_chip_generic *gc;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
        struct irq_domain *parent;
#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_list에 연결될 때 사용하다.
  • name
    • irq domain 명
  • ops
    • irq domain 메소드들의 주소가 담긴다.
  • host_data
    • private data 포인터 주소(irq domain core 코드에서 사용하지 않는다)
  • flags
    • irq domain의 플래그
      • IRQ_DOMAIN_FLAG_HIERARCHY
        • tree 구조 irq domain
      • IRQ_DOMAIN_FLAG_AUTO_RECURSIVE
        • tree 구조에서 recursive 할당/할당해제를 사용하는 경우
      • IRQ_DOMAIN_FLAG_NONCORE
        • generic 코어 코드를 사용하지 않고 직접 irq domain에 대해 specific한 처리를 한 경우 사용
  • of_node
    • 연결된 device tree 노드
  • gc
    • 연결된 generic chip 리스트 (인터럽트 컨트롤러 리스트)
  • parent
    • 상위 irq domain
    • irq domain 하이라키를 지원하는 경우 사용
    • arm64는 기본 사용
  • revmap_direct_max_irq
    • 직접 매핑을 지원하는 컨트롤러들에서 설정되는 최대 hw irq 번호
  • revmap_size
    • 리니어 맵 테이블 사이즈
  • revmap_tree
    • hwirq를 위한 radix tree 맵
  • linear_revmap
    • hwirq -> virq에 리버스 매핑에 대한 리니어 테이블

 

irq_domain_ops 구조체

include/linux/irqdomain.h

/**
 * struct irq_domain_ops - Methods for irq_domain objects
 * @match: Match an interrupt controller device node to a host, returns
 *         1 on a match
 * @map: Create or update a mapping between a virtual irq number and a hw
 *       irq number. This is called only once for a given mapping.
 * @unmap: Dispose of such a mapping
 * @xlate: Given a device tree node and interrupt specifier, decode
 *         the hardware irq number and linux irq type value.
 *
 * Functions below are provided by the driver and called whenever a new mapping
 * is created or an old mapping is disposed. The driver can then proceed to
 * whatever internal data structures management is required. It also needs
 * to setup the irq_desc when returning from map().
 */
struct irq_domain_ops {
        int (*match)(struct irq_domain *d, struct device_node *node);
        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);

#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
        /* extended V2 interfaces to support hierarchy irq_domains */
        int (*alloc)(struct irq_domain *d, unsigned int virq,
                     unsigned int nr_irqs, void *arg);
        void (*free)(struct irq_domain *d, unsigned int virq,
                     unsigned int nr_irqs);
        void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
        void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
#endif
};
  • match
    • 호스와 인터럽트 컨트롤러 디바이스 노드와 매치 여부를 수행하는 함수 포인터. 매치되면 1을 반환한다
  • map
    • 가상 irq와 하드웨어 irq 번호끼리의 매핑을 만들거나 업데이트 하는 함수 포인터.
  • unmap
    • 매핑을 해제하는 함수 포인터.
  • xlate
    • 하드웨어 irq와 리눅스 irq와의 변환을 수행하는 함수 포인터.
  • alloc
    • irq domain의 tree 구조 지원시 irq 할당 함수 포인터
  • free
    • irq domain의 tree 구조 지원시 irq 할당 해제 함수 포인터
  • activate
    • irq domain의 tree 구조 지원시 irq activate 함수 포인터
  • deactivate
    • irq domain의 tree 구조 지원시 irq deactivate 함수 포인터

 

참고

init_IRQ()

이 루틴은 해당 시스템의 인터럽트 컨트롤러를 초기화하고 각각의 인터럽트 번호에 따른 핸들러들을 준비하는 과정이다. 인터럽트 컨트롤러는 시스템마다 다른 각각의 하드웨어를 사용하므로 분석을하고자 하는 시스템의 하드웨어에 대한 지식이 필요한다. 따라서 각 시스템에서 사용하는 인터럽트 컨틀롤러의 데이터 시트를 참고하기 바란다.

 

리눅스 커널에서 구현 방법은 크게 다음과 같이 두 가지로 나뉜다.

  • 아키텍처 전용 머신 코드를 수행하여 인터럽트 컨트롤러를 초기화하고 핸들러를 구성하는 방법
    • arch/arm/mach-로 시작하는 디렉토리
    • 대부분의 임베디드 시스템에서 처음 사용된 방식으로 곧장 각 머신의 인터럽트 컨트롤러 설정 등이 구현되어 있다.
      • rpi 및 rp2 예)
        • 내부 인터럽트 초기화 함수는 Device Tree를 사용한다.
        • 커널 v4.4 부터는 지원되지 않는다.
  • Device Tree에 설정된 내용을 분석하여 해당 드라이버를 구동하고 인터럽트 컨트롤러 하드웨어를 초기화하고 핸들러를 구성하는 방법
    • kernel/chip.c (헬퍼) -> drivers/irqchip 디렉토리의 각 인터럽트 컨트롤러 드라이버
      • irq_domain 등을 사용하여 리눅스 irq와 hw irq의 매핑을 지원하는 구조로 복잡해졌지만 점차 시스템들이 이 방향으로 구성하고 있다.
      • arm64도 Device Tree 구성을 읽어 인터럽트 초기화 함수들을 호출한다.
      • rpi 및 rpi2 예)
        • 커널 v4.4 부터 지원하기 시작하였다.

 

IRQ 초기화

 

init_IRQ()

arch/arm/kernel/irq.c

void __init init_IRQ(void)
{
        int ret;

        if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
                irqchip_init();
        else
                machine_desc->init_irq();

        if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
            (machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
                if (!outer_cache.write_sec)
                        outer_cache.write_sec = machine_desc->l2c_write_sec;
                ret = l2x0_of_init(machine_desc->l2c_aux_val,
                                   machine_desc->l2c_aux_mask);
                if (ret)
                        pr_err("L2C: failed to init: %d\n", ret);
        }
}

인터럽트 처리 핸들러를 위해 인터럽트 컨트롤러의 준비 및 필요한 설정 수행한다. 시스템 구성에 따라 머신 디스크립터 또는 DTB 둘 중 하나의 방법을 사용하여 초기화 한다.

  • 코드 라인 5~8에서 시스템이 Device Tree를 지원하면서 머신 디스크립터에서 init_irq 후크가 설정되어 있지 않으면 Device Tree를 위해 irqchip_init() 함수를 호출하고, 그렇지 않은 경우 머신 디스크립터에 준비한 init_irq 후크에 설정된 콜백 함수를 호출한다.
    • rpi2 예)
      • 머신의 init_irq에 등록한 bcm2709_init_irq() 함수를 호출한다.
    • 참고: A new generic IRQ layer | LWN.net
  • 코드 라인 10~18에서 outer 캐시를 가진 시스템에 대해 초기화를 수행한다.
    • DTB를 사용할 수 있는 커널에서 L2X0 캐시 컨트롤러를 사용하는 시스템이고 머신에 l2c_aux_mask 또는 l2_aux_val이 설정된 경우 outer 캐시에 대한 콜백함수를 준비하고 초기화 함수를 호출한다.

 

arch/arm64/kernel/irq.c – arm64 참고

void __init init_IRQ(void)
{
        irqchip_init();
        if (!handle_arch_irq)
                panic("No interrupt controller found.");
}

arm64에서는 DTB 만을 지원하므로 irqchip_init() 함수를 곧바로 호출한다.

 

인터럽트 컨트롤러 초기화

시스템에 따라 다양한 종류의 인터럽트 컨트롤러를 사용하는데 rpi2에서 사용하는 bcm2709용 인터럽트 컨트롤러 초기화 함수를 호출하기로 한다.

  • Device Tree 미 사용시 (CONFIG_OF=n)
    • rpi
      • arch/arm/mach-bcm2708/bcm2708.c – bcm2708_init_irq()
        • 내부 초기화 함수에서는 device tree를 사용한다.
    • rp2
      • arch/arm/mach-bcm2709/bcm2709.c – bcm2709_init_irq()
        • 내부 초기화 함수에서는 device tree를 사용한다.
  • Device Tree 사용 시
    • of_irq_init() 함수를 통해 Device Tree에 있는 인터럽트 컨트롤러 정보를 읽어오고 해당 드라이버의 초기화 함수들을 호출한다.
    • GIC
      • drivers/irqchip/irq-gic.c – gic_of_init()
    • Exynos Combiner
      • drivers/irqchip/exynos-combiner.c – combiner_of_init()
    • rpi
      • drivers/irqchip/irq-bcm2835.c – armctrl_of_init()
    • rpi2
      • 두 개의 드라이버를 사용한다. (커널 v4.4 부터 적용)
        • drivers/irqchip/irq-bcm2835.c – bcm2836_armctrl_of_init()
        • drivers/irqchip/irq-bcm2836.c – bcm2836_arm_irqchip_l1_intc_of_init()

 

머신 specific 인터럽트 컨트롤러 초기화

bcm2708_init_irq() & bcm2709_init_irq()

arch/arm/mach-bcm2708/bcm2708.c

void __init bcm2708_init_irq(void)
{
        armctrl_init(__io_address(ARMCTRL_IC_BASE), 0, 0, 0);
}

인터럽트 컨트롤러의 base 가상 주소를 인수로 인터럽트 컨트롤러를 초기화한다.

 

arch/arm/mach-bcm2709/bcm2709.c

void __init bcm2709_init_irq(void)
{
        armctrl_init(__io_address(ARMCTRL_IC_BASE), 0, 0, 0);
}

인터럽트 컨트롤러의 base 가상 주소를 인수로 인터럽트 컨트롤러를 초기화한다.

 

rpi vs rpi2 ARM 인터럽트 컨트롤러 주소

BCM2708을 사용하는 rpi와 BCM2709를 사용하는 rpi2는 내부에서 사용하는 디바이스들의 구성은 거의 유사하나 peripheral base 물리 주소가 다르고 이에 따른 가상 주소도 다르다.

  • peripheral base 물리 주소 -> 가상 주소
    • rpi: 0x2000_0000 -> 0xf200_0000
    • rpi2: 0x3f00_0000 -> 0xf300_0000

 

rpi 물리 주소

#define BCM2708_PERI_BASE        0x20000000
#define ARM_BASE                 (BCM2708_PERI_BASE + 0xB000)    /* BCM2708 ARM control block */
#define ARMCTRL_IC_BASE          (ARM_BASE + 0x200)           /* ARM interrupt controller */
  • rpi의 인터럽트 컨트롤러 물리 주소 = 0x2000_b200
  • rpi의 인터럽트 컨트롤러 가상 주소 = 0xf200_b200

 

rpi2 물리 주소

#define BCM2708_PERI_BASE        0x3F000000
#define ARM_BASE                 (BCM2708_PERI_BASE + 0xB000)    /* BCM2708 ARM control block */
#define ARMCTRL_IC_BASE          (ARM_BASE + 0x200)           /* ARM interrupt controller */
  • rpi2의 인터럽트 컨트롤러 물리 주소 = 0x3f00_b200
  • rpi2의 인터럽트 컨트롤러 가상 주소 = 0xf300_b200

 

armctrl_init() – bcm2709 용

arch/arm/mach-bcm2709/armctrl.c

/**
 * armctrl_init - initialise a vectored interrupt controller
 * @base: iomem base address
 * @irq_start: starting interrupt number, must be muliple of 32
 * @armctrl_sources: bitmask of interrupt sources to allow
 * @resume_sources: bitmask of interrupt sources to allow for resume
 */
int __init armctrl_init(void __iomem * base, unsigned int irq_start,
                        u32 armctrl_sources, u32 resume_sources)
{
        unsigned int irq;

        for (irq = 0; irq < BCM2708_ALLOC_IRQS; irq++) {
                unsigned int data = irq;
                if (irq >= INTERRUPT_JPEG && irq <= INTERRUPT_ARASANSDIO)
                        data = remap_irqs[irq - INTERRUPT_JPEG];
                if (irq >= IRQ_ARM_LOCAL_CNTPSIRQ && irq <= IRQ_ARM_LOCAL_TIMER) {
                        irq_set_percpu_devid(irq);
                        irq_set_chip_and_handler(irq, &armctrl_chip, handle_percpu_devid_irq);
                        set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN);
                } else {
                        irq_set_chip_and_handler(irq, &armctrl_chip, handle_level_irq);
                        set_irq_flags(irq, IRQF_VALID | IRQF_PROBE | IRQF_DISABLED);
                }
                irq_set_chip_data(irq, (void *)data);
        }

        armctrl_pm_register(base, irq_start, resume_sources);
        init_FIQ(FIQ_START);
        armctrl_dt_init();
        return 0;
}

bcm2709용 인터럽트 컨트롤러를 초기화하고 각 인터럽트에 대한 핸들러들을 준비한다.

  • 코드 라인 13에서 인터럽트 컨트롤러에 할당된 irq 수 만큼 루프를 돈다.
    • rpi: SPARE_ALLOC_IRQS=394
    • rpi2: SPARE_ALLOC_IRQS=480
  • 코드 라인 15~16에서 GPU용 리눅스 irq를 hwirq로 변환한다.
    • irq 번호가 INTERRUPT_JPEG(74) ~ INTERRUPT_ARASANSDIO(84) 번까지의 irq는 GPU(VC)가 ARM용으로 routing 한 리눅스 irq 번호이므로 기존 GPU(VC)용 hwirq 번호를 알아낸 후 data에 대입한다.
    • GPU(VC) hardware irq 중 11개의 irq는 ARM에서 같이 공유하여 사용하기 위해 ARM pending 레지스터로 라우팅(변환)하여 사용한다.
    • remap_irqs[]
      • INTERRUPT_VC_JPEG(7)
      • INTERRUPT_VC_USB(9)
      • (..생략..)
      • INTERRUPT_VC_ARASANSDIO(62) 까지 총 11개가 등록되어 있다.
  • 코드 라인 17~20에서 irq 번호가 IRQ_ARM_LOCAL_CNTPSIRQ(96) ~ IRQ_ARM_LOCAL_TIMER(107)번 사이의 local 인터럽트인 경우 core별로 전용 사용되므로 per-cpu 디바이스를 취급하는 핸들러를 설정한다.
  • 코드 라인 21~24에서 그 외의 irq 번호인 경우 일반 핸들러를 설정한다.
  • 코드 라인 25에서 hwirq 번호로 chip_data를 구성한다.
  • 코드 라인 28에서 PM(Power Management)를 위해 armctrl_info 구조체에 인터럽트 정보를 대입해둔다.
  • 코드 라인 29에서 FIQ 인터럽트 핸들러를 준비한다.
  • 코드 라인 30에서 Device Tree 스크립트를 통해 해당 인터럽트 컨트롤러 정보를 읽어 irq_domain을 구성한다.

 

FIQ 초기화

init_FIQ()

arch/arm/kernel/fiq.c

void __init init_FIQ(int start)
{
        unsigned offset = FIQ_OFFSET;
        dfl_fiq_insn = *(unsigned long *)(0xffff0000 + offset);
        get_fiq_regs(&dfl_fiq_regs);
        fiq_start = start;
}

default FIQ 인터럽트 처리기를 백업하기 위해 fiq 벡터 값과 fiq 모드에서 사용하는 레지스터들 일부(r8 ~ r12, sp, lr)를 백업해둔다.

  • fiq가 필요한 owner task에서 핸들러를 준비하여 요청/해제등을 수행 시 파괴된 레지스터와 fiq 벡터들을 복원시 사용하기 위해 백업해둔다.
    • STM 및 LDM 수행은 보통 atomic 하게 수행되지만 cpu가 low interrupt latency 모드를 사용하는 경우 STM과 LDM 명령이 atomic하게 이루어지지 않아 STM 도중에 fiq exception 되는 경우 완전하게 스택에 기록하지 않는 문제가 발생하고 fiq 인터럽트 서비스 루틴 수행 후 다시 원래 모드 루틴으로 복귀할 때 LDM으로 읽은 값(최종 값이 보통 pc)에 가베지가 로드되어 문제가 발생할 수 있다. 이런 경우를 위해 fiq 모드에서 사용하였던 default 인터럽트 핸들러용 레지스터들을 백업해두고 이를 복원하는 용도로 사용한다.
    • fiq는 irq에 비해 약 10 ~ 20여배 latency가 작다. (10 ~ 20 ns vs 수 백 ns)
    • 참고: [ARM] 3256/1: Make the function-returning ldm’s use sp as the base register
  • rpi2 예)
    • “usb_fiq” 라는 이름의 usb 드라이버에서 fiq를 사용한다.
    • hcd_init_fiq() – drivers/usb/host/dwc_otg/dwc_otg_hcd_linux.c

 

  • 코드 라인 3~4에서 fiq에 해당하는 exception 벡터에 기록된 명령을 읽어와서 전역 변수 dfl_fiq_insn에 저장한다.
  • 코드 라인 5에서 fiq 모드로 진입한 후 레지스터 백업 장소 dfl_fiq_regs->r8 위치에 레지스터 r8 ~ r12, sp, lr을 저장하고 다시 svc 모드로 돌아온다.
  • 코드 라인 6에서 fiq_start에 hw irq 시작 번호를 대입한다.
    • rpi: 85
    • rpi2: 128

 

ENTRY(__get_fiq_regs)
        mov     r2, #PSR_I_BIT | PSR_F_BIT | FIQ_MODE
        mrs     r1, cpsr
        msr     cpsr_c, r2      @ select FIQ mode
        mov     r0, r0          @ avoid hazard prior to ARMv4
        stmia   r0!, {r8 - r12}
        str     sp, [r0], #4
        str     lr, [r0]
        msr     cpsr_c, r1      @ return to SVC mode 
        mov     r0, r0          @ avoid hazard prior to ARMv4
        ret     lr
ENDPROC(__get_fiq_regs)

FIQ 모드로 진입한 후 인수 r0 레지스터가 가리키는 주소에 현재 레지스터 r8 ~ r12, sp, lr을 저장하고 다시 SVC 모드로 돌아온다.

 

Device Tree에서 인터럽트 컨트롤러 노드 정보를 읽어 irq domain 생성

armctrl_dt_init()

arch/arm/mach-bcm2709/armctrl.c

void __init armctrl_dt_init(void)
{
        struct device_node *np;
        struct irq_domain *domain;

        np = of_find_compatible_node(NULL, NULL, "brcm,bcm2708-armctrl-ic");
        if (!np)
                return;

        domain = irq_domain_add_legacy(np, BCM2708_ALLOC_IRQS,
                                        IRQ_ARMCTRL_START, 0,
                                        &armctrl_ops, NULL);
        WARN_ON(!domain);
}

디바이스 트리 스크립트에 설정된 인터럽트 컨트롤러 명이 “brcm,bcm2708-armctrl-ic”인 경우 irq_domain을 추가하고 legacy 방법으로 domain을 최대 할당 인터럽트 수 만큼 지정한다.

  • 최대 할당 인터럽트 수
    • rpi: 394
    • rpi2: 480

 

Device Tree 방식으로 인터럽트 초기화

아래 irq chip에 대한 글에서 rpi2를 대상으로 설명한다. 다만 커널 v4.4이상을 요구한다.

 

참고

Interrupt -1- (Interrupt Controller)

 

아키텍처별 인터럽트 컨트롤러 H/W 구성

  • 아키텍처마다 인터럽트 컨트롤러 구성이 다른데 간략히 아래 그림과 같이 다른 처리 방법을 갖는 것을 이해하자
    • x86 with PIC
      • 초기 CPU를 하나만 사용하는 X86 시스템에서는 8259 PIC(Programmable Interrupt Controller)를 하나 이상 사용하여 구성하였다.
    • x86 with APIC
      • x86에서도 SMP 구성을 하면서 APIC(Advanced Programmble Interrupt Controller)를 두 개의 칩에 나누어 하나는 cpu가 있는 코어 프로세서칩에 사용하고 I/O를 담당하는 south 칩셋에 APIC를 구성하였다.
    • ARM SoC
      • ARM은 무척 구성이 다양하다. SoC를 제작하는 회사마다 소유한 인터럽트 컨트롤러 로직을 SoC의 IP 블럭으로 구성하여 사용하는데 점차적으로 ARM사가 디자인한 GIC(Generic Interrupt Controller)를 사용하게 되는 경향이 있다.
        • GIC v1
          • 1020개 까지 인터럽트 소스 처리 가능
          • ARM Cortex-A5, A9 등에서 사용
        • GIC v2
          • 가상화(virtualization) 기능 추가
          • ARM Cortex-A7, A15, A53, A57 등에서 사용
        • GIC v3
          • 시그널이 아니라 메시지로 처리
          • non-secure와 secure 그룹에 따라 인터럽트 처리를 분리하는 진화된 security 기능 추가
          • ARM Cortex-A53, A57, A72 등에서 사용
        • GICv4
          • 가상 인터럽트의 직접 주입(injection) 기능
          • ARM Cortex-A53, A57, A72 등에서 사용

 

ARM의 다양한 인터럽트 컨트롤러 들

  • ARM社 design
    • gic
      • gic-400
      • arm11mp-gic
      • arm1176jzf-devchip-gic
      • cortex-a15-gic
      • cortex-a9-gic
      • cortex-a7-gic
      • qcom,msm-8660-qgic
      • qcom,msm-qgic2
    • gic-v2m
      • arm,gic-v2m-frame
    • gic-v3
      • arm,gic-v3
      • gic-v3-its
        arm,gic-v3-its
    • nvic
      • arm,armv7m-nvic
    • vic
      • arm,pl190-vic
      • arm,pl192-vic
      • arm,versatile-vic
  • Broadcom社 design
    • bcm2835-ic
      • rpi는 ARM의 GIC를 사용하지 않고 broadcom 인터럽트 컨트롤러를 사용한다.
        • 커널 소스 참고: https://github.com/raspberrypi/linux
        • 2가지 구현
          • architecture specific 인터럽트 핸들러로 구현 (머신 디스크립터 정의)
            • arch/arm/mach-bcm2708/bcm2708.c
          • DTB 기반 irq-domain 인터럽트 핸들러로 구현
            • drivers/irqchip/irq-bcm2835.c
    • bcm2836-ic
      • rpi2 역시 broadcom 사의 인터럽트 컨트롤러를 사용하는데 4개의 armv7 코어를 사용하면서 2835에 비해 더 복잡해졌다.
        • 커널 소스 참고: https://github.com/raspberrypi/linux
        • 2가지 구현 (커널 v4.4 이상)
          • architecture specific 인터럽트 핸들러로 구현 (머신 디스크립터 정의)
            • arch/arm/mach-bcm2709/bcm2709.c
          • DTB 기반 per-cpu irq-domain 인터럽트 핸들러로 구현
            • drivers/irqchip/irq-bcm2836.c
  • 그 외 제조사 custom 디자인 IP 블럭
    • exynos-combiner
    • armada-mpic
    • atmel-aic
    • hip04
    • keystone
    • omap-intc
    • xtensa-pic
    • (수십 종류라 생략…)

 

 

1) GIC(Generic Interrupt Controller)

GIC 블럭다이아그램

  • GIC는 CPU Interface 레지스터들과 Distributor 레지스트들을 통해 제어한다.
  • CPU Interface 레지스터
    • core로 전달된 irq들 전체를 한꺼번에 통제한다.
    • core로 전달될 irq들에 대해 설정한 priority mask 값을 사용하여 통과시킨다.
      • priority 레벨링 (16 ~ 256 단계)
  • Distributor 레지스터
    • 진입된 인터럽트의 종류는 다음과 같이 구분된다.
      • PPIs(Private Peripherals Interrupts)
        • 각 core에 전용으로 사용될 수 있는 IRQ 들로 0 ~ 31 까지의 id를 사용한다.
      • SPIs(Shared Peripherals Interrupts)
        • core에 공용으로 사용될 수 있는 IRQ 들로 32 ~ 1020까지의 id를 사용한다.
      • SGIs(Software Generated Interrupts)
        • 각 core에 전용으로 사용되며, 외부 인터럽트가 아니라 소프트웨어에서 발생시킬 수 있는 내부 IRQ 들로 0 ~ 15 까지의 id를 사용한다.
    • secure 상태에 따라 group 0/1의 방향을 선택하게 한다. (irq/fiq)
      • non-secure extension 사용시 group은 항상 fiq로 forward된다.
      • secure extension 사용시 group-0은 fiq, group-1은 irq로 forward 된다.
    • Pending 인터럽트를 forward할 지 설정할 수 있다.
    • IRQ 별로 forward를 설정(Set-enable) 및 클리어(Set-Clear) 요청할 수 있다.
    • IRQ 별로 priority를 설정할 수 있다.
    • IRQ 별로 개별 cpu interface로의 전달여부를 비트마스크로 설정할 수 있다. (1=forward, 0=drop)
    • 하드웨어 IRQ 별로 인터럽트 트리거 타입을 설정할 수 있다.
      • 0=level sensitive (전압의 레벨이 지속되는 동안)
      • 1=edge trigger (high나 low로 바뀌는 순간만)
        • SGI는 edge 트리거를 사용한다.

GICv1 레지스터들 (GIC-390)

CPU Interface Registers

 

  • GICC_CTRL (CPU Interface Control Register)
    • Enable=1로 설정하여 인터럽트를 forward 한다.
    • secure mode 별로 레지스터가 bank 되어 사용된다.
  • GICC_PMR (Interrupt Priority Mask Register)
    • core로 전달될 irq들에 대해 설정한 priority 마스크와 and 연산하여 forward한다.
    • priority 레벨링 (16 ~ 256 단계)
      • 0xff: 256 단계
      • 0xfe: 128 단계
      • 0xfc: 64 단계
      • 0xf8: 32단계
      • 0xf0: 16 단계
  • GICC_BPR (Binary Point Register)
    • priority 값을 group/sub와 같이 두 그룹으로 나누어 그 중 group에 해당하는 priority를 가진 인터럽트에 대해 preemption interrupt로 분류한다.
  • GICC_IAR  (Interrupt Acknowledge Register)
    • 인터럽트 수신 시 이 레지스터를 읽어 인터럽트 id를 읽어오는 것으로 인터럽트 처리 루틴이 ack 처리되었음을 gic에 알린다.
  • GICC_EOIR  (End of Interrupt Register)
    • ISR(Interrupt Service Routine)에서 인터럽트 처리 완료 시 해당 인터럽트 id를 기록하여 eoi 처리하였음을 gic에 알린다.
  • GICC_RPR (Running Priority Register)
    • 읽기 전용으로 현재 동작하고 있는 인터럽트의 priority 값을 알아온다. idle 상태인 경우는 0xff
    • non-secure에서 읽은 경우 0x80을 mask 하여사용한다.
  • GICC_HPIR (Highest Pending Interrupt Register)
    • 읽기 전용으로 현재 하드웨어 인터럽트 (PPI&SPI)들 중 pending되고 있는 가장 높은 priority irq 번호와 cpu를 읽어온다.
    • 소프트 인터럽트(SGI)의 경우 cpu 값만 읽어올 수 있다.
  • GICC_IIDR (CPU Interface Identification Register)
    • 읽기 전용으로 GIC 제품 ID, 아키텍처 버전, 리비전, 구현회사 등의 정보를 읽어올 수 있다.

 

Distributor Registers

  • GICD_CTRL (Distributor Control Register)
    • Pending 인터럽트에 대해 CPU interface로 올려보낼지 여부를 통제한다.
    • secure extension을 사용할 때 non-secure 모드에서는 group1에 대해서만 통제할 수 있다.
      • 1=forward enable
    • security extension 사용 시 bank 된다.
  • GICD_TYPER (Interrupt Controller Type Register)
    • 읽기 전용으로 Security Extension을 사용 시 최대 지원 cpu 수와 인터럽트 수를 알아낸기 위해 사용한다.
      • Lockable SPI
        • 최대 지원 lockable SPI 수
      • SecurityExtn (bit:10)
        • 1=security extension 사용
        • 0=security extension 사용하지 않음
      • CPU Number
        • 최대 지원 CPU 수
      • ITLinesNumber
        • 최대 지원 인터럽트 수
  • GICD_IIDR (Distributor Implementer Identification Register)
    • 읽기 전용으로 GIC implementor 정보를 알아온다.
  • GICD_IGROUPR (Interrupt Group Registers)
    • secure extension을 사용하는 경우 인터럽트별로 group 0와 group 1을 지정한다.
      • 0=group 0, 1=group 1
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ISER (Interrupt Set Enable Registers)
    • IRQ 별로 통과(Set-Enable) 여부를 설정할 수 있다.
      • 1=set 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICER (Interrupt Clear Enable Registers)
    • IRQ 별로 통제(Clear-Enable) 여부를 설정할 수 있다.
      • 1=clear 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ISPENDR (Interrupt Set PENDing Registers)
    • IRQ 별로 pending 상태를 설정할 수 있다.
      • 1=set 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICPENDR (Interrupt Clear PENDing Registers)
    • IRQ 별로 pending 상태 해제를 요청할 수 있다.
      • 1=clear 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_IPRIORITYR (Interrupt PRIORITY Registers)
    • IRQ 별로 priority를 설정할 수 있다.
      • 0~255 priority 설정
    • IRQ #0~31을 위해 첫 8개의 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ITARGETR (Interrupt processor TARGET Registers)
    • IRQ 별로 개별 cpu interface로의 전달여부를 비트로 마스크 설정할 수 있다.
    • bit0=cpu#0 제어, bit1=cpu#1 제어…
      • 0=disable forward, 1=enable forward
    • IRQ #0~31을 위해 첫 8개의 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICFGR (Interrupt ConFiGuration Registers)
    • IRQ 별로 인터럽트 트리거 방향을 설정할 수 있다.
      • 0=상향 시 트리거, 1=하향 시 트리거
    • IRQ #160~31을 위해 두 번째 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_SGIR (Software Generated Interrupt Register)
    • TLF (Target List Filter)
      • 0=white list 제어 방식, CPU Target List에 설정된 cpu로만 forward
      • 1=black list 제어 방식, CPU Target List에 설정된 cpu만 drop
      • 2=위의 ACL 방식 제어를 사용하지 않는다.
      • 3=reserved
    • CPU Target List
      • 0~7개의 cpu bit
    • NSATT (Non-Secure
      • 0=SGI#에 대해 group-0으로 설정된 경우 CPU Target List로 forward
      • 1=SGI#에 대해 group-1로 설정된 경우 CPU Target List로 forward

 

GIC 인터럽트 상태

  • Inactive
    • 인터럽트가 발생하지 않은 상태
  • Pending
    • 하드웨어 또는 소프트웨어에서 인터럽트가 발생하였으나 타깃 프로세스에게 전달되지 않음
  • Active
    • 인터럽트가 발생하여 프로세스에게 전달되었고 처리 중이며 완료되지 않은 상태
  • Active & Pending
    • 인터럽트가 발생하여 프로세스에게 전달되었고, 동일한 인터럽트가 발생하여 보류중임

인터럽트 트리거

  • Level Sensitive
  • Edge Trigger

 

Prioritization

Preemption Interrupt

  • Binary Point
    • 3개의 비트를 사용한 0~7의 값으로 각 하드웨어 인터럽트에 설정된 priority 값을 두 그룹으로 분리한다.
    • 분리한 두 개의 그룹
      • Group Priority
        • fast interrupt 처리가 가능하도록 현재 처리하고 있는 인터럽트가 있다하더라도 더 높은 우선 순위의 인터럽트 발생 시 이를 먼저 처리하도록 한다.
      • Sub Priority
        • 현재 처리하고 있는 인터럽트의 우선 순위보다 더 높은 우선 순위의 인터럽트 발생 시 먼저 처리하지 않는다.

 

Priority Leveling

  • Priority Mask
    • 8개의 비트를 사용한다.
    • priority 값과 Priority Mask 값을 and 연산을 취한 값을 priority 값으로 사용하는 것으로 우선순위를 레벨링한다.
    • 최하 16 단계부터 256단계 까지 사용한다.

 

2) BCM2708(RPI) 인터럽트 컨트롤러

BCM2708 인터럽트 컨트롤러

arm irq에 해당하는 박스에 색상이 있는 항목들은 실제 ARM 커널 드라이버에서 처리된다.

 

인터럽트 소스 구분

RPI에서 처리하는 인터럽트 소스는 다음과 같다.

  • Pending IRQ1:
    • GPU peripherals 인터럽트
    • irqnr = #0~31
      • 7, 9, 10, 18, 19번은 ARM과 share
  • Pending IRQ2:
    • GPU peripherals 인터럽트
    • irqnr = #32~63
      • 53~57, 62번은 ARM과 share
  • Pending Base IRQ0:
    • ARM peripherals 인터럽트
    • irqnr = #64~95
      • 64~71번은 timer, doorbell #0~1, mailbox, …
      • 72-73번은 pending #1-#2 레지스터 소스
      • 74~85번은 VideoCore와 share

 

 

관련 레지스터

  • 0x7E00_0000으로 시작하는 주소는 GPU(VC)에 연결된 I/O peripherals 기본 물리 주소이다. 따라서 ARM 리눅스에서 사용하는 물리주소와 가상주소로 변환하여 사용한다.
    • rpi (ARM 기본 주소):
      • GPU 물리 주소: 0x7e00_0000 (DTB에 구성된 주소로 ARM 관점에서 BUS 주소로 본다.)
      • ARM 물리 주소: 0x2000_0000
      • ARM 가상 주소: 0xf200_0000
    • rpi2 (ARM 기본 주소):
      • GPU 물리 주소: 0x7e00_0000 (DTB에 구성된 주소로 ARM 관점에서 BUS 주소로 본다.)
      • ARM 물리 주소: 0x3f00_0000
      • ARM 가상 주소: 0xf300_0000

 

3) BCM2709(RPI2) 인터럽트 컨트롤러

BCM2709 인터럽트 컨트롤러

arm irq에 해당하는 박스에 색상이 있는 항목들은 실제 ARM 커널 드라이버에서 처리된다.

인터럽트 소스 구분

RPI2에서 처리하는 인터럽트 소스는 다음과 같다. Machine Specific 함수에서 초기화한 경우와 Device Tree로 초기화할 때의 구성이 다르다.

  • HARD IRQs
    • GPU Pending IRQ1:
      • GPU peripherals 인터럽트
      • Machine Specific irqnr = #0~31
        • 7, 9, 10, 18, 19번은 ARM과 share
      • Device Tree virq=30~61, hwirq=32~63
    • GPU Pending IRQ2:
      • GPU peripherals 인터럽트
      • Machine Specific irqnr = #32~63
        • 53~57, 62번은 ARM과 share
      • Device Tree virq=62~93, hwirq=64~95
    • ARM Pending Base IRQ0:
      • ARM peripherals 인터럽트
      • irqnr = #64~95
        • 64~71번은 timer, doorbell #0~1, mailbox, …
        • 72-73번은 pending #1-#2 레지스터 소스
        • 74~85번은 VideoCore와 share
      • Device Tree virq=22~29, hwirq=0~7
    • LOCAL:
      • 인터럽트 소스로 해당 비트가 설정되는 경우 각각의 레지스터를 통해 해당 IRQ를 얻어낸다.
      • irqnr = #96~127
        • 100~103번은 mailbox0~3 interrupt controller 레지스터 소스
        • 104 번은 pending #0 레지스터 소스
        • 105번은 PMU 레지스터 소스
      • Device Tree virq=16~21, hwirq=0~9
  • Soft IRQs:
    • IPI (mailbox #0)
      • ipinr = #0~31
        • 8~31번은 리눅스에서 사용하지 않는다.
    • mailbox #1~3, doorbell, timer, …

 

BCM2709 인터럽트 컨트롤러 레지스터 (Machine Specific)

관련 레지스터

BCM2708의 10개의 (IRQ basic pending ~ ) 기본 레지스터들과 동일하고 BCM2709는 ARM 전용으로 다음과 같이 추가되었다.

이 레지스터들은 GPU(VC)에서 사용하지 않고 ARM 전용으로 사용된다.

  • rpi2 (ARM LOCAL 기본 주소):
    • ARM 물리 주소: 0x4000_0000
    • ARM 가상 주소: 0xf400_0000

RPI2 인터럽트 확인 (Machine-Specific)

rpi2를 machine specific 코드 기반으로 초기화된 인터럽트 정보이다.

  • 보여주는 정보 순서
    • irq 번호
    • cpu별 인터럽트 통계
    • irq_chip.name
    • irq_domain.hwirq
      • irq_domain이 설정되지 않아 출력되지 않음
    • 트리거 레벨
      • CONFIG_GENERIC_IRQ_SHOW_LEVEL이 설정되지 않아 출력되지 않음
    • irq name
      • 이름이 설정되지 않아 아무것도 출력되지 않음
    • action list
      • 1개 이상은 컴마로 분리됨
$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
 16:          0          0          0          0   ARMCTRL  bcm2708_fb dma
 24:      12845          0          0          0   ARMCTRL  DMA IRQ
 25:       2980          0          0          0   ARMCTRL  DMA IRQ
 32:  128848277          0          0          0   ARMCTRL  dwc_otg, dwc_otg_pcd, dwc_otg_hcd:usb1
 52:          0          0          0          0   ARMCTRL  BCM2708 GPIO catchall handler
 65:         11          0          0          0   ARMCTRL  bcm2708_vcio
 66:          2          0          0          0   ARMCTRL  VCHIQ doorbell
 75:          1          0          0          0   ARMCTRL
 83:          2          0          0          0   ARMCTRL  uart-pl011
 84:     497931          0          0          0   ARMCTRL  mmc0
 99:    4675934    2099819     568663      90331   ARMCTRL  arch_timer
FIQ:              usb_fiq
IPI0:          0          0          0          0  CPU wakeup interrupts
IPI1:          0          0          0          0  Timer broadcast interrupts
IPI2:     137529      40597      36229      25588  Rescheduling interrupts
IPI3:         14         17         11         15  Function call interrupts
IPI4:          1          5          1          3  Single function call interrupts
IPI5:          0          0          0          0  CPU stop interrupts
IPI6:          0          0          0          0  IRQ work interrupts
IPI7:          0          0          0          0  completion interrupts
Err:

 

RPI2 인터럽트 확인 (Device Tree)

Device Tree로 구성한 인터럽트 정보이다..

  • 보여주는 정보 순서
    • irq 번호
    • cpu별 인터럽트 통계
    • irq_chip.name
    • irq_domain.hwirq
    • 트리거 레벨
    • irq name
      • 이름이 설정되지 않아 아무것도 출력되지 않음
    • action list
      • 1개 이상은 컴마로 분리됨
$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
 16:          0          0          0          0  bcm2836-timer   0 Edge      arch_timer
 17:   37228140   22280702  154598435   50544784  bcm2836-timer   1 Edge      arch_timer
 23:      24375          0          0          0  ARMCTRL-level   1 Edge      3f00b880.mailbox
 24:        216          0          0          0  ARMCTRL-level   2 Edge      VCHIQ doorbell
 39:          1          0          0          0  ARMCTRL-level  41 Edge
 46:          0          0          0          0  ARMCTRL-level  48 Edge      bcm2708_fb dma
 48:     545428          0          0          0  ARMCTRL-level  50 Edge      DMA IRQ
 50:          0          0          0          0  ARMCTRL-level  52 Edge      DMA IRQ
 62:  925102072          0          0          0  ARMCTRL-level  64 Edge      dwc_otg, dwc_otg_pcd, dwc_otg_hcd:usb1
 79:          0          0          0          0  ARMCTRL-level  81 Edge      3f200000.gpio:bank0
 80:          0          0          0          0  ARMCTRL-level  82 Edge      3f200000.gpio:bank1
 86:     540806          0          0          0  ARMCTRL-level  88 Edge      mmc0
 87:       5053          0          0          0  ARMCTRL-level  89 Edge      uart-pl011
 92:    9308589          0          0          0  ARMCTRL-level  94 Edge      mmc1
FIQ:              usb_fiq
IPI0:          0          0          0          0  CPU wakeup interrupts
IPI1:          0          0          0          0  Timer broadcast interrupts
IPI2:    1160150    6582642    1541946    1893722  Rescheduling interrupts
IPI3:          3          5          8          8  Function call interrupts
IPI4:        114         91         79         97  Single function call interrupts
IPI5:          0          0          0          0  CPU stop interrupts
IPI6:          0          0          0          0  IRQ work interrupts
IPI7:          0          0          0          0  completion interrupts
Err:          0

 

참고

 

MM(Mapped Memory) Fault Handler

 

 

 

handle_mm_fault()

mm/memory.c

/*
 * By the time we get here, we already hold the mm semaphore
 *
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 */
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                    unsigned long address, unsigned int flags)
{
        int ret;            

        __set_current_state(TASK_RUNNING);

        count_vm_event(PGFAULT); 
        mem_cgroup_count_vm_event(mm, PGFAULT);

        /* do counter updates before entering really critical section. */
        check_sync_rss_stat(current);

        /*
         * Enable the memcg OOM handling for faults triggered in user
         * space.  Kernel faults are handled more gracefully.
         */
        if (flags & FAULT_FLAG_USER)
                mem_cgroup_oom_enable();

        ret = __handle_mm_fault(mm, vma, address, flags);

        if (flags & FAULT_FLAG_USER) {
                mem_cgroup_oom_disable();
                /*
                 * The task may have entered a memcg OOM situation but
                 * if the allocation error was handled gracefully (no
                 * VM_FAULT_OOM), there is no need to kill anything.
                 * Just clean up the OOM state peacefully.
                 */   
                if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
                        mem_cgroup_oom_synchronize(false);
        }

        return ret; 
}
EXPORT_SYMBOL_GPL(handle_mm_fault);

vma 매핑된 메모리에 대해 fault시 처리할 핸들러로서 요청한 가상 주소에 물리 페이지를 할당하여 매핑해온다. 해당 페이지가 swapping되었었거나 file 캐시되었었던 경우 다시 사용할 수 있도록 관련 페이지들을 불러 읽어온다.

  • 코드 라인 12에서 현재 태스크를 TASK_RUNNING 상태로 설정한다.
  • 코드 라인 14에서 PGFAULT vm 카운터를 증가시킨다.
  • 코드 라인 15에서 현재 태스크가 memcg 제어에 있는 경우 MEM_CGROUP_EVENTS_PGFAULT memcg 카운터를 증가시킨다.
  • 코드 라인 18에서 현재 태스크의 rss_stat.events를 증가시킨다. 만일 TASK_RS_EVENTS_THRESH(64)값을 초과한 경우 태스크의 RSS 카운터 값을 메모리 디스크립터로 옮겨 동기화한다.
  • 코드 라인 24~25에서 user 플래그가 설정된 경우 memcg OOM 기능을 할 수 있도록 설정한다.
  • 코드 라인 27에서 __handle_mm_fault() 함수를 호출하여 새로운 물리 페이지를 할당하고 매핑한다. 해당 페이지가 swapping되엇었거나 file 캐시되엇된 경우 다시 사용할 수 있도록 관련 페이지들을 불러 읽어온다.
  • 코드 라인 29~39에서 user 플래그가 설정된 경우 memcg OOM 기능을 하지 못하게 클리어한다.  태스크가 memcg oom 설정되었고 결과가 fault OOM이 아닌 경우 OOM 동기화를 수행한다.

 

__handle_mm_fault()

mm/memory.c

/*
 * By the time we get here, we already hold the mm semaphore
 *
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 */
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                             unsigned long address, unsigned int flags)
{
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *pte;

        if (unlikely(is_vm_hugetlb_page(vma)))
                return hugetlb_fault(mm, vma, address, flags);

        pgd = pgd_offset(mm, address);
        pud = pud_alloc(mm, pgd, address);
        if (!pud)
                return VM_FAULT_OOM;
        pmd = pmd_alloc(mm, pud, address);
        if (!pmd)
                return VM_FAULT_OOM;
        if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) {
                int ret = VM_FAULT_FALLBACK;
                if (!vma->vm_ops)
                        ret = do_huge_pmd_anonymous_page(mm, vma, address,
                                        pmd, flags);
                if (!(ret & VM_FAULT_FALLBACK))
                        return ret;
        } else {
                pmd_t orig_pmd = *pmd;
                int ret;

                barrier();
                if (pmd_trans_huge(orig_pmd)) {
                        unsigned int dirty = flags & FAULT_FLAG_WRITE;

                        /*
                         * If the pmd is splitting, return and retry the
                         * the fault.  Alternative: wait until the split
                         * is done, and goto retry.
                         */
                        if (pmd_trans_splitting(orig_pmd))
                                return 0;

                        if (pmd_protnone(orig_pmd))
                                return do_huge_pmd_numa_page(mm, vma, address,
                                                             orig_pmd, pmd);

                        if (dirty && !pmd_write(orig_pmd)) {
                                ret = do_huge_pmd_wp_page(mm, vma, address, pmd,
                                                          orig_pmd);
                                if (!(ret & VM_FAULT_FALLBACK))
                                        return ret;
                        } else {
                                huge_pmd_set_accessed(mm, vma, address, pmd,
                                                      orig_pmd, dirty);
                                return 0;
                        }
                }
        }

        /*
         * Use __pte_alloc instead of pte_alloc_map, because we can't
         * run pte_offset_map on the pmd, if an huge pmd could
         * materialize from under us from a different thread.
         */
        if (unlikely(pmd_none(*pmd)) &&
            unlikely(__pte_alloc(mm, vma, pmd, address)))
                return VM_FAULT_OOM;
        /* if an huge pmd materialized from under us just retry later */
        if (unlikely(pmd_trans_huge(*pmd)))
                return 0;
        /*
         * A regular pmd is established and it can't morph into a huge pmd
         * from under us anymore at this point because we hold the mmap_sem
         * read mode and khugepaged takes it in write mode. So now it's
         * safe to run pte_offset_map().
         */
        pte = pte_offset_map(pmd, address);

        return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}

 

pud, pmd 및 pte 테이블이 필요한 경우 할당하여 연결하고 할당한 물리 페이지를 pte에 연결한다. (pmd huge page가 구성가능한 경우에 pmd에서 huge 페이지를 할당받아 연결한다.)

  • 코드 라인 15~16에서 작은 확률로 vma가 huge 페이지인 경우 hugetlb_fault() 함수에게 처리를 인계한다.
  • 코드 라인 18~21에서 요청 가상 주소로 pgd 엔트리를 찾아오고 새로운 pud 테이블을 할당해온다. 만일 할당이 실패한 경우 VM_FAULT_OOM을 반환한다.
  • 코드 라인 22~24에서 요청 가상 주소로 새로운 pmd 엔트리를 할당해온다. 만일 할당이 실패한 경우 VM_FAULT_OOM을 반환한다.
  • 코드 라인 25~31에서 pmd 엔트리가 비어있고 vma 영역에 transparent huge 페이지가 설정된 경우 pmd 단위의 huge page를 할당해온다. 만일 VM_FAULT_FALLBACK이 결과 코드에 없는 경우 그 결과를 반환한다.
    • fallback 설정이 없는 경우 할당하여 성공하든 할당이 실패하든 그 결과를 바로 반환한다.
    • fallback 설정된 경우 pmd 단위의 huge page는 포기하고 아래 루틴에서 pte 테이블을 할당하려 시도 한다.
  • 코드 라인 32~46에서 pmd 엔트리가 huge 페이지 설정되었고 split된 pmd huge 페이지 설정된 경우 0으로 반환한다.
    • split 중이므로 처리를 유보 시킨다. (또 exception 되겠지…^^;)
  • 코드 라인 48~50에서 pmd 엔트리가 매핑 되지 않은 경우 numa 통계를 위해 fault 카운터를 증가시키고 잘못된 위치에 놓인 trans huge 페이지를 이주시켜 교정한다.
  • 코드 라인 52~56에서 write 요청이 있으면서 write가 허가되지 않은 pmd 매핑일 때 write 허가로 바꾼다. 이 때 매핑이 2군데 이상이 있는 경우 pmd huge 페이지를 복사한 후 write 속성으로 매핑한다. 결과에 fallback 요청이 없으면 곧바로 결과 값을 반환한다.
  • 코드 라인 57~61에서 pmd huge mapping을 write 가능하게 바꾼다.
  • 코드 라인 70~72에서 pmd 매핑이 안된 경우 pte 테이블을 할당해온다.  만일 할당이 실패한 경우 VM_FAULT_OOM을 반환한다.
  • 코드 라인 74~75에서 작은 확률로 pmd 엔트리가 transparent huge 페이지 설정된 경우 0을 반환한다.
    • retry되도록 그냥 0을 반환한다.
  • 코드 라인 82에서 요청 가상 주소로 pte 엔트리 주소를 알아온다.
  • 코드 라인 84에서 pud와 pmd 엔트리까지 연결고리는 다 처리했으므로 마지막 pte fault를 처리하기 위해 handle_pte_fault() 함수를 호출한다.

 

handle_pte_fault()

mm/memory.c

/*
 * These routines also need to handle stuff like marking pages dirty
 * and/or accessed for architectures that don't do it in hardware (most
 * RISC architectures).  The early dirtying is also good on the i386.
 *
 * There is also a hook called "update_mmu_cache()" that architectures
 * with external mmu caches can use to update those (ie the Sparc or
 * PowerPC hashed page tables that act as extended TLBs).
 *
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with pte unmapped and unlocked.
 *
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 */
static int handle_pte_fault(struct mm_struct *mm,
                     struct vm_area_struct *vma, unsigned long address,
                     pte_t *pte, pmd_t *pmd, unsigned int flags)
{
        pte_t entry;
        spinlock_t *ptl;

        /*
         * some architectures can have larger ptes than wordsize,
         * e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
         * so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
         * The code below just needs a consistent view for the ifs and
         * we later double check anyway with the ptl lock held. So here
         * a barrier will do.
         */
        entry = *pte;
        barrier();
        if (!pte_present(entry)) {
                if (pte_none(entry)) {
                        if (vma->vm_ops) {
                                if (likely(vma->vm_ops->fault))
                                        return do_fault(mm, vma, address, pte,
                                                        pmd, flags, entry);
                        }
                        return do_anonymous_page(mm, vma, address,
                                                 pte, pmd, flags);
                }
                return do_swap_page(mm, vma, address,
                                        pte, pmd, flags, entry);
        }

        if (pte_protnone(entry))
                return do_numa_page(mm, vma, address, entry, pte, pmd);

        ptl = pte_lockptr(mm, pmd);
        spin_lock(ptl);
        if (unlikely(!pte_same(*pte, entry)))
                goto unlock;
        if (flags & FAULT_FLAG_WRITE) {
                if (!pte_write(entry))
                        return do_wp_page(mm, vma, address,
                                        pte, pmd, ptl, entry);
                entry = pte_mkdirty(entry);
        }
        entry = pte_mkyoung(entry);
        if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
                update_mmu_cache(vma, address, pte);
        } else {
                /*
                 * This is needed only for protection faults but the arch code
                 * is not yet telling us if this is a protection fault or not.
                 * This still avoids useless tlb flushes for .text page faults
                 * with threads.
                 */
                if (flags & FAULT_FLAG_WRITE)
                        flush_tlb_fix_spurious_fault(vma, address);
        }
unlock:
        pte_unmap_unlock(pte, ptl);
        return 0;
}

요청한 가상 주소에서 fault된 경우 해당 주소에 새 페이지를 할당하고 매핑한다. 할당한 새 페이지는 다음의 상황으로 구분된다.

  • file 매핑 타입으로 사용되는 vma 영역에서 fault된 경우 새 페이지를 file로부터 읽는다.
  • anon 타입으로 사용되는 vma 영역에서 fault된 경우 새 페이지를 reverse anon에 추가한다.
  • swap된 anon 페이지에서 fault된 경우 새 페이지를 swap 파일로부터 읽는다.
  • write protect 매핑되어 있는 공유메모리에 write 하려다가 fault된 경우 해당 페이지를 새 페이지에 복사한 후 write 매핑한다. (COW)

 

  • 코드 라인 34~43에서 엔트리가 none이면서 vma가 file 매핑을 사용하는 경우 file로부터 페이지를 읽어들이기 위해 do_fault() 함수를 호출한다.
  • 코드 라인 34~43에서 엔트리가 none이면서 vma가 file 매핑을 사용하는 경우가 아니면 새 페이지를 할당하고 reverse anon으로 추가하기 위해 do_anonymous_page() 함수를 호출한다.
  • 코드 라인 44~45에서 엔트리가 none이 아니면서 present 설정되지 않은 경우 swap 된 페이지를 불러오기 위해 do_swap_page()를 호출한다.
  • 코드 라인 48~49에서 NUMA 밸런싱을 위해 엔트리가 protnone인 경우 do_num_page() 함수를 호출한다.
    • protnone:
      • NUMA 시스템에서 해당 페이지를 읽을 때 accesss 권한 실패로 인해 abort exception이 발생되어 fault된 후 해당 페이지를 사용하는 태스크의 migration을 고려하는 Automatic NUMA balancing을 위해 사용된다.
      • x86: none비트만 설정되고 present는 없는 상태
      • powerpc: present 설정되고 user는 없는 상태
      • NUMA 밸런싱을 사용하지 않는 UMA 시스템에서는 항상 0을 반환한다.
  • 코드 라인 51~52에서 pmd 엔트리 값에 해당하는, 즉 pte 페이지 테이블에 대한 페이지의 ptl(page table lock) 값을 가져오고 lock을 수행한다.
  • 코드 라인 53~54에서 작은 확률로 pte 값과 entry 값이 다른 경우 unlock 레이블로 이동한다.
  • 코드 라인 55~60에서 write 요청이 있는 경우 엔트리 값에 dirty 설정을 한다. 단 write protect된 pte 매핑 엔트리인 경우 기존 페이지를 새 페이지에 복사하기 위해 do_wp_page() 함수를 호출한다.
    • 유저가 공유메모리에 대해 write 요청을 하는 경우 기존 페이지를 새 페이지에 COW(Copy-On-Write) 한다.
  • 코드 라인 61에서 엔트리 값에 young 설정을 추가한다.
  • 코드 라인 62~73에서 pte 값과 엔트리 값이 다른 경우 pte 테이블 엔트리 값을 업데이트한다. 실제 업데이트가 이루어진 경우 캐시도 업데이트한다. 만일 값이 같아 pte 테이블 엔트리 값을 업데이트 하지 않아도 될 때 write 요청이 있으면 tlb flush를 진행한다.
    • arm 아키텍처 v6 이상에서는 아무런 동작을 하지 않아도 된다.

 

파일 매핑된 주소에서의 fault 처리

do_fault()

mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults).
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 */
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags, pte_t orig_pte)
{
        pgoff_t pgoff = (((address & PAGE_MASK)
                        - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

        pte_unmap(page_table);
        if (!(flags & FAULT_FLAG_WRITE))
                return do_read_fault(mm, vma, address, pmd, pgoff, flags,
                                orig_pte);
        if (!(vma->vm_flags & VM_SHARED))
                return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
                                orig_pte);
        return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}

file 매핑 타입으로 사용되는 vma 영역에서 fault된 경우 다음 3가지 유형으로 처리한다.

  • write 권한 요청이 없는 경우 페이지 캐시에 있는 페이지를 읽어와 매핑한다. 만일 페이지 캐시에 없으면 file로 부터 읽어 다시 페이지 캐시에 저장한다.
  • read only 비 공유 파일에 대해 write 권한 요청이 있는 경우 file로 부터 읽어 페이지 캐시에 저장한 후 새 페이지에 복사하고 그 페이지에 write 권한을 부여하여 anon 매핑한다.
  • read only 공유 파일에 대해 write 권한 요청이 있는 경우 write 권한을 설정한다.

 

  • 코드 라인 11~12에서 offset 페이지 값을 산출한다.
    • 예) vma->vm_start=0x1000_0000, address=0x1234_5678, vma->vm_pgoff=0x1000
      • pgoff=0x12345 – 0x10000 + 0x1000 = 0x3345
  • 코드 라인 14에서 CONFIG_HIGHPTE 커널 옵션을 사용한 경우 highmem에 매핑한 page_table 엔트리를 unmap한다.
  • 코드 라인 15~17에서 write 요청이 없는 경우 매핑된 file로부터 페이지를 읽기 위해 do_read_fault() 함수를 호출한다.
  • 코드 라인 18~20에서 vma 영역이 공유 설정된 경우 매핑된 file로부터 페이지를 읽은 후에 새 페이지에 복사하고 write 설정하기 위해 do_cow_fault() 함수를 호출한다.
  • 코드 라인 21에서 그 외의 경우 매핑된 file로부터 페이지를 읽은 후 write 설정하기 위해 do_shared_fault() 함수를 호출한다.

do_read_fault()

mm/memory.c

static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pmd_t *pmd,
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
        struct page *fault_page;
        spinlock_t *ptl;
        pte_t *pte;
        int ret = 0;

        /*
         * Let's call ->map_pages() first and use ->fault() as fallback
         * if page by the offset is not ready to be mapped (cold cache or
         * something).
         */
        if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
                pte = pte_offset_map_lock(mm, pmd, address, &ptl);
                do_fault_around(vma, address, pte, pgoff, flags);
                if (!pte_same(*pte, orig_pte))
                        goto unlock_out;
                pte_unmap_unlock(pte, ptl);
        }

        ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                return ret;

        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*pte, orig_pte))) {
                pte_unmap_unlock(pte, ptl);
                unlock_page(fault_page);
                page_cache_release(fault_page);
                return ret;
        }
        do_set_pte(vma, address, fault_page, pte, false, false);
        unlock_page(fault_page);
unlock_out:
        pte_unmap_unlock(pte, ptl);
        return ret;
}

파일 시스템의 vm_ops의 map_pages 핸들러를 통해 페이지 캐시로부터 exception된 가상 주소의 주변(readahead 기능) 페이지들을 읽어오는데 만일 그러한 핸들러가 필요 없는 블럭 디바이스(FS_DAX)이거나 페이지 캐시로 부터 읽어오지 못한 경우 fallback되어 vm_ops의 fault 핸들러를 통해 파일에서 해당 페이지에 대해서 read I/O 요청을 한다.

  • 코드 라인 15~21에서 vma에 map_pages 핸들러가 있고 fault_around_bytes가 2 페이지 이상인 경우 pte 테이블 페이지에 대한 ptl lock을 걸고 fault 주소 주변의 몇 개 페이지들을 불러 온다. pte 값이 변경된 경우 unlock_out 레이블로 이동하여 ptl 락을 풀고 함수를 빠져나간다.
    • 이렇게 미리 주변 페이지를 불러오는 경우 fault 처리를 줄이고자 하는데 목적이 있다.
    • ext2나 ext4 파일 시스템에서 generic 함수인 filemap_map_pages() 함수가 사용된다.
    • DAX(Direct Access eXicting) 기능에서는 앞으로 더 읽을 것이라고 예측하여 주변 페이지를 미리 더 읽어오게 하는 readahead 기능을 사용하지 않으므로 map_pages에 해당하는 핸들러는 제공하지 않는다. 따라서 map_pages 핸들러에서 항상 fallback된다.
    • fault_around_bytes
      • default  값으로 65536이며 CONFIG_DEBUG_FS 커널 옵션이 사용될 때 “/sys/kernel/debug/fault_around_bytes” 파일을 통해 설정 값을 바꿀 수 있다. (2의 차수 단위가 적용되며 최소 페이지 사이즈이다)
      • 참고: [PATCH] mm: make fault_around_bytes configurable | LKML.org
  • 코드 라인 23~25에서 페이지 캐시를 통해 페이지들을 가져올 수 없어 fallback된 경우 해당 페이지 들을 파일에서 읽기 위해 __do_fault() 함수를 호출한다. 만일 작은 확률로 errror, nopage 또는 retry 결과를 얻게되면 바로 그 결과를 반환한다.
  • 코드 라인 27~33에서 다시 pte 테이블 페이지에 대한 ptl lock을 건다. 만일 pte 값이 orig_pte 값과 다른 경우 fault 페이지를 할당 해제하고 함수를 빠져나간다.
  • 코드 라인 34~35 fault 페이지에 대한 pte 매핑을 하고 fault 페이지에 대한 락(PG_locked) 플래그를 클리어한다.
  • 코드 라인 37~38에서 pte 테이블 페이지의 ptl 락을 풀고 함수를 빠져나간다.

 

do_fault_around()

mm/memory.c

/*
 * do_fault_around() tries to map few pages around the fault address. The hope
 * is that the pages will be needed soon and this will lower the number of
 * faults to handle.
 *
 * It uses vm_ops->map_pages() to map the pages, which skips the page if it's
 * not ready to be mapped: not up-to-date, locked, etc.
 *
 * This function is called with the page table lock taken. In the split ptlock
 * case the page table lock only protects only those entries which belong to
 * the page table corresponding to the fault address.
 *
 * This function doesn't cross the VMA boundaries, in order to call map_pages()
 * only once.
 *
 * fault_around_pages() defines how many pages we'll try to map.
 * do_fault_around() expects it to return a power of two less than or equal to
 * PTRS_PER_PTE.
 *
 * The virtual address of the area that we map is naturally aligned to the
 * fault_around_pages() value (and therefore to page order).  This way it's
 * easier to guarantee that we don't cross page table boundaries.
 */
static void do_fault_around(struct vm_area_struct *vma, unsigned long address,
                pte_t *pte, pgoff_t pgoff, unsigned int flags)
{
        unsigned long start_addr, nr_pages, mask;
        pgoff_t max_pgoff;
        struct vm_fault vmf;
        int off;

        nr_pages = ACCESS_ONCE(fault_around_bytes) >> PAGE_SHIFT;
        mask = ~(nr_pages * PAGE_SIZE - 1) & PAGE_MASK;

        start_addr = max(address & mask, vma->vm_start);
        off = ((address - start_addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
        pte -= off;
        pgoff -= off;

        /*
         *  max_pgoff is either end of page table or end of vma
         *  or fault_around_pages() from pgoff, depending what is nearest.
         */
        max_pgoff = pgoff - ((start_addr >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) +
                PTRS_PER_PTE - 1;
        max_pgoff = min3(max_pgoff, vma_pages(vma) + vma->vm_pgoff - 1,
                        pgoff + nr_pages - 1);

        /* Check if it makes any sense to call ->map_pages */
        while (!pte_none(*pte)) {
                if (++pgoff > max_pgoff)
                        return;
                start_addr += PAGE_SIZE;
                if (start_addr >= vma->vm_end)
                        return;
                pte++;
        }

        vmf.virtual_address = (void __user *) start_addr;
        vmf.pte = pte;
        vmf.pgoff = pgoff;
        vmf.max_pgoff = max_pgoff;
        vmf.flags = flags;
        vma->vm_ops->map_pages(vma, &vmf);
}

파일에 대해 radix tree로 관리되는 페이지 캐시로부터 요청 가상 주소를 기준으로 fault_around_bytes(64K) 단위로 정렬된 범위 만큼의 페이지를 가져와 매핑한다.

  • 코드 라인 32~33에서 fault_around_bytes(default: 64K) 를 페이지 단위로 바꾸고, 이에 대한 mask 값을 산출한다.
    • 예 1) fault_around_bytes=64K, PAGE_SIZE=4K
      • -> nr_pages=16, mask=0xffff_0000
  • 코드 라인 35에서 요청 가상 주소를 mask하여 절삭 한 값과 vma 영역의 시작 주소 중 가장 큰 주소를 start_addr에 대입한다.
  • 코드 라인 36~38에서 start_addr를 기준으로 요청 했던 주소와 차이나는 페이지 수를 산출한 후 off에 대입하고, pte와 pgoff에서 그 차이를 각각 뺀다.
  • 코드 라인 44~47에서 start_addr를 기준으로 pte 테이블의 마지막 엔트리에 해당하는 가상 주소에 대한 pgoff 값을 max_pgoff에 대입한다. 그 후 max_pgoff, vma 페이지의 마지막 pgoff 및 fault_around_bytes 단위로 읽고자 하는 마지막 페이지에 대한 pgoff 중 가장 작은 수를 max_pgoff로 대입한다.
    • 예) 아래 그림 중 처음 fault된 경우: address=0x1000_5000, pgoff=5
      • -> nr_pages=4, mask=0xffff_c000, off=1, 최종 pgoff=4, max_pgoff=7=min(255, 12, 7)
  • 코드 라인 50~57에서 이미 매핑된 페이지들은 skip한다.  pgoff 부터 max_pgoff를 벗어나거나 페이지가 vma의 영역의 끝 주소를 넘어가는 경우 이미 다 매핑이 된 경우로 함수를 빠져나간다.
  • 코드 라인 59~64에서  매핑되지 않은 pte 엔트리에 대한 가상 주소 start_addr와 이 주소에 대한 pgoff와 max_pgoff 등을 가지고 vm_fault 구조체를 채운 후 마운팅된 파일 시스템의 map_pages 핸들러 함수를 호출한다.
    • ext2 및 ext4 파일 시스템에서는 generic 코드인 mapfiles_map_pages() 함수를 호출한다.

 

아래 그림과 같이 예를 들어 vma 영역이 13개 페이지가 있을 때 매핑된 file에 대해 0x1000_5000 ~ 0x1000_9fff까지 5개 페이지를 읽으려 할 때 2 번의 fault가 발생하는 모습을 보여준다.

 

__do_fault()

mm/memory.c

/*
 * The mmap_sem must have been held on entry, and may have been
 * released depending on flags and vma->vm_ops->fault() return value.
 * See filemap_fault() and __lock_page_retry().
 */
static int __do_fault(struct vm_area_struct *vma, unsigned long address,
                        pgoff_t pgoff, unsigned int flags,
                        struct page *cow_page, struct page **page)
{
        struct vm_fault vmf;
        int ret;

        vmf.virtual_address = (void __user *)(address & PAGE_MASK);
        vmf.pgoff = pgoff;
        vmf.flags = flags;
        vmf.page = NULL;
        vmf.cow_page = cow_page;

        ret = vma->vm_ops->fault(vma, &vmf);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                return ret;
        if (!vmf.page)
                goto out;

        if (unlikely(PageHWPoison(vmf.page))) {
                if (ret & VM_FAULT_LOCKED)
                        unlock_page(vmf.page);
                page_cache_release(vmf.page);
                return VM_FAULT_HWPOISON;
        }

        if (unlikely(!(ret & VM_FAULT_LOCKED)))
                lock_page(vmf.page);
        else
                VM_BUG_ON_PAGE(!PageLocked(vmf.page), vmf.page);

 out:
        *page = vmf.page;
        return ret;
}

vm_fault 구조체 정보를 채워 vma의 fault 핸들러를 호출한다. 출력 인수 page에 파일로 부터 읽어들인 페이지 정보를 전달한다.

  • 코드 라인 13~19에서 인수로 받은 정보를 vm_struct 구조체에 대입하고 vma에 등록한 fault 핸들러 함수를 호출한다.
    • 예) ext2 및 ext4 파일 시스템을 사용하는 경우 등록하여 사용하는 핸들러는 다음 둘 중 하나를 사용한다.
      • generic mmap fault 핸들러: mm/filemap.c – filemap_fault()
      • CONFIG_FS_DAX 커널 옵션이 사용되는 경우 dax mmap fault 핸들러: fs/dax.c – dax_fault()
        • DAX(Direct Access eXiciting)
          • 블럭 디바이스에 RAM이 장착되어 사용되므로, 리눅스에서 별도로 버퍼를 만들어 핸들링 하지 않도록 한다.
          • 실제 마운트 옵션에서 -o dax 옵션을 주어 사용한다.
          • arm, mips, sparc 아키텍처에서는 사용하지 않는다.
          • 참고: DAX: Page cache bypass for filesystems on memory storage | LWN.net
  • 코드 라인 20~21에서 작은 확률로 error, nopage, retry 등의 결과를 얻게되면 그대로 반환한다.
  • 코드 라인 22~23에서 vfm.page에 읽어들인 정보를 할당받은 페이지를 가리키는데 지정되지 않은 경우 함수를 빠져나간다.
  • 코드 라인 25~30에서 작은 확률로 vmf.page가 hwpoison 된 경우 페이지 할당을 해제한 후 hwpoison 에러를 반환한다.
    • HWPOISON
      • 메모리 에러를  검출할 수 있는 하드웨어와 이를 지원하는 시스템에서 사용된다. 주로 ECC 메모리에 대한 에러 교정에 사용된다.
      • 참고: HWPOISON | LWN.net
  • 코드 라인 32~33에서 작은 확률로 locked 가 설정되지 않은 경우 다시 lock을 수행한다.

 

do_cow_fault()

mm/memory.c

static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pmd_t *pmd,
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
        struct page *fault_page, *new_page;
        struct mem_cgroup *memcg;
        spinlock_t *ptl;
        pte_t *pte;
        int ret;

        if (unlikely(anon_vma_prepare(vma)))
                return VM_FAULT_OOM;

        new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
        if (!new_page) 
                return VM_FAULT_OOM;

        if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) {
                page_cache_release(new_page);
                return VM_FAULT_OOM;
        }

        ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                goto uncharge_out;

        if (fault_page)
                copy_user_highpage(new_page, fault_page, address, vma);
        __SetPageUptodate(new_page);

        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*pte, orig_pte))) {
                pte_unmap_unlock(pte, ptl);
                if (fault_page) {
                        unlock_page(fault_page);
                        page_cache_release(fault_page);
                } else {
                        /*
                         * The fault handler has no page to lock, so it holds
                         * i_mmap_lock for read to protect against truncate.
                         */
                        i_mmap_unlock_read(vma->vm_file->f_mapping);
                }
                goto uncharge_out;
        }
        do_set_pte(vma, address, new_page, pte, true, true);
        mem_cgroup_commit_charge(new_page, memcg, false);
        lru_cache_add_active_or_unevictable(new_page, vma);
        pte_unmap_unlock(pte, ptl);
        if (fault_page) {
                unlock_page(fault_page);
                page_cache_release(fault_page);
        } else {
                /*
                 * The fault handler has no page to lock, so it holds
                 * i_mmap_lock for read to protect against truncate.
                 */
                i_mmap_unlock_read(vma->vm_file->f_mapping);
        }
        return ret;
uncharge_out:
        mem_cgroup_cancel_charge(new_page, memcg);
        page_cache_release(new_page);
        return ret;
}

파일 시스템의 vm_ops의 map_pages 핸들러를 통해 페이지 캐시로부터 exception된 가상 주소에 대한 페이지를 읽어와서 새 페이지에 복사하고 write 권한으로 anon 매핑한다.

  • 코드 라인 11~12에서 요청 vma에 anon_vma가 준비되지 않은 경우 할당하고 준비를 해온다. 만일 준비가 안된 경우 가상 메모리 부족(VM_FAULT_OOM) 에러를 반환한다.
  • 코드 라인 14~16에서 하나의 새 유저 페이지를 할당받는다. 만일 할당되지 않으면 가상 메모리 부족 에러를 반환한다.
    • highmem이 있는 경우 가능하면 highmem에서 movable 타입으로 한 개 페이지를 할당 받는다.
  • 코드 라인 18~21에서 가상 페이지 수가 memcg 설정된 commit 할당량을 벗어난 경우 가상 메모리 부족 에러를 반환한다.
    • 주로 memory control group을 사용하여 지정한 태스크의 메모리 사용량을 제어하기 위해 사용한다.
  • 코드 라인 23~25에서 파일로부터 읽어온다. 만일 결과가 error, nopage 또는 retry 에러인 경우 uncharge_out 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 27~29에서 파일로 부터 요청한 페이지 fault_page를 읽어온 경우 새 페이지에 복사하고 새 페이지에 대해 PG_uptodate 플래그를 설정한다.
  • 코드 라인 31~45에서 출력 인수로 받아온 pte와 orig_pte가 다른 경우 매핑 없이 uncharge_out 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 46에서 새 페이지를 write 속성으로 매핑하고 anon reverse map에 페이지를 추가한다.
  • 코드 라인 47에서 memcg에 한 페이지가 추가되었음을 commit 한다.
  • 코드 라인 48에서 새 페이지를 active 설정하고 lru 캐시에 추가한다.
    • lru_add_pvec에 추가한다.
  • 코드 라인 50~52에서 fault_page에 대해 참조 카운터를 감소시키고 0이되면 할당 해제도 한다.

 

do_shared_fault()

mm/memory.c

static int do_shared_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pmd_t *pmd,
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
        struct page *fault_page;
        struct address_space *mapping;
        spinlock_t *ptl;
        pte_t *pte;
        int dirtied = 0;
        int ret, tmp;

        ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                return ret;

        /*
         * Check if the backing address space wants to know that the page is
         * about to become writable
         */
        if (vma->vm_ops->page_mkwrite) {
                unlock_page(fault_page);
                tmp = do_page_mkwrite(vma, fault_page, address);
                if (unlikely(!tmp ||
                                (tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
                        page_cache_release(fault_page);
                        return tmp;
                }
        }

        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*pte, orig_pte))) {
                pte_unmap_unlock(pte, ptl);
                unlock_page(fault_page);
                page_cache_release(fault_page);
                return ret;
        }
        do_set_pte(vma, address, fault_page, pte, true, false);
        pte_unmap_unlock(pte, ptl);

        if (set_page_dirty(fault_page))
                dirtied = 1;
        /*
         * Take a local copy of the address_space - page.mapping may be zeroed
         * by truncate after unlock_page().   The address_space itself remains
         * pinned by vma->vm_file's reference.  We rely on unlock_page()'s
         * release semantics to prevent the compiler from undoing this copying.
         */
        mapping = fault_page->mapping;
        unlock_page(fault_page);
        if ((dirtied || vma->vm_ops->page_mkwrite) && mapping) {
                /*
                 * Some device drivers do not set page.mapping but still
                 * dirty their pages
                 */
                balance_dirty_pages_ratelimited(mapping);
        }

        if (!vma->vm_ops->page_mkwrite)
                file_update_time(vma->vm_file);

        return ret;
}

공유 파일 매핑 영역에서 write 요청으로 fault된 경우 해당 페이지의 매핑을 write로 변경한다. 만일 write로 변경을 할 수 없는 상황인 경우 포기한다.

  • 코드 라인 12~14에서 파일 캐시로 부터 fault된 주소의 페이지를 읽어온다. 만일 결과가 error, nopage 또는 retry 에러인 경우 포기하고 그 결과를 반환한다.
  • 코드 라인 20~28에서 vma의 오퍼레이션 핸들러 중 page_mkwrite 후크 함수가 설정된 경우 write 설정을 위해 do_page_mkwrite() 함수를 호출한다. 만일 실패하는 경우 fault_page 를 release 하고 함수를 빠져나간다.
  • 코드 라인 30~36에서 페이지 테이블 락을 획득해온다. 만일 락 획득 과정에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 포기하고 빠져나간다.
  • 코드 라인 37에서 pte 엔트리에 대한 매핑을 write로 변경한다.
  • 코드 라인 40~41에서 fault_page가 dirty 설정이 되어 있는지 여부를 알아온다.
    • dirty 설정이 된 경우 페이지 캐시의 내용이 파일에 기록이 되지 않았음을 알 수 있다.
  • 코드 라인 48~56에서 fault_page의 매핑과 vma의 page_mkwrite 핸들러가 설정되었거나 dirty 된 경우 balance_dirty_pages_ratelimited() 함수를 호출한다.
    • dirty 페이지가 제한치를 초과하는 경우 밸런싱을 맞추기 위해 블록디바이스가 백그라운드에서 writeback을 수행할 수 있게 요청한다.
  • 코드 라인 58~59에서 vma의 page_mkwrite 핸들러가 설정되지 않은 경우 이 루틴에서 파일의 시간을 업데이트 한다.
    • inode의 mtime(수정 시각)과 ctime(작성 시각)을 현재 시간으로 업데이트 한다.

 

anon 매핑된 주소에서의 fault 처리

anon 매핑 주소에서의 fault 처리

do_anonymous_page()

mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with mmap_sem still held, but pte unmapped and unlocked.
 */
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags)
{
        struct mem_cgroup *memcg;
        struct page *page;
        spinlock_t *ptl;
        pte_t entry;

        pte_unmap(page_table);

        /* Check if we need to add a guard page to the stack */
        if (check_stack_guard_page(vma, address) < 0)
                return VM_FAULT_SIGSEGV;

        /* Use the zero-page for reads */
        if (!(flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(mm)) {
                entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
                                                vma->vm_page_prot));
                page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
                if (!pte_none(*page_table))
                        goto unlock;
                goto setpte;
        }

        /* Allocate our own private page. */
        if (unlikely(anon_vma_prepare(vma)))
                goto oom;
        page = alloc_zeroed_user_highpage_movable(vma, address);
        if (!page)
                goto oom;
        /*
         * The memory barrier inside __SetPageUptodate makes sure that
         * preceeding stores to the page contents become visible before
         * the set_pte_at() write.
         */
        __SetPageUptodate(page);

        if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg))
                goto oom_free_page;

        entry = mk_pte(page, vma->vm_page_prot);
        if (vma->vm_flags & VM_WRITE)
                entry = pte_mkwrite(pte_mkdirty(entry));

        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (!pte_none(*page_table))
                goto release;

        inc_mm_counter_fast(mm, MM_ANONPAGES);
        page_add_new_anon_rmap(page, vma, address);
        mem_cgroup_commit_charge(page, memcg, false);
        lru_cache_add_active_or_unevictable(page, vma);
setpte:
        set_pte_at(mm, address, page_table, entry);

        /* No need to invalidate - it was non-present before */
        update_mmu_cache(vma, address, page_table);
unlock:
        pte_unmap_unlock(page_table, ptl);
        return 0;
release:
        mem_cgroup_cancel_charge(page, memcg);
        page_cache_release(page);
        goto unlock;
oom_free_page:
        page_cache_release(page);
oom:
        return VM_FAULT_OOM;
}

anon 매핑된 영역에서 exception된 가상 주소에 대한 새 페이지를 할당하고 anon 매핑한다.

  • 코드 라인 15에서 pte 테이블이 fixmap에 임시로 매핑되어 있는 경우 매핑을 해제한다.
  • 코드 라인 18~19에서 영역이 스택이고  확장 불가능한 경우 VM_FAULT_SIGSEGV 에러를 반환한다.
  • 코드 라인 22~29에서 write 요청이 아니고 zero 페이지를 사용할 수 있는 경우 별도로 페이지를 할당하지 않고 기존 zero 페이지를 매핑하도록 준비하고 setpte 레이블로 이동한다.
  • 코드 라인 32~33에서 요청 vma에 anon_vma가 준비되지 않은 경우 할당하고 준비를 해온다. 만일 준비가 안된 경우 가상 메모리 부족(VM_FAULT_OOM) 에러를 반환한다.
  • 코드 라인 34~36에서 하나의 새 유저 페이지를 할당받는다. 만일 할당되지 않으면 가상 메모리 부족 에러를 반환한다.
    • highmem이 있는 경우 가능하면 highmem에서 movable 타입으로 한 개 페이지를 할당 받는다.
  • 코드 라인 42에서 새 페이지에 대해 PG_uptodate 플래그를 설정한다.
  • 코드 라인 44~45에서 가상 페이지 수가 memcg 설정된 commit 할당량을 벗어난 경우 가상 메모리 부족 에러를 반환한다.
    • 주로 memory control group을 사용하여 지정한 태스크의 메모리 사용량을 제어하기 위해 사용한다.
  • 코드 라인 47에서 vma 영역에 설정된 페이지 테이블 속성을 사용하여 pte 엔트리의 매핑을 준비한다.
  • 코드 라인 48~49에서 vma 영역에 write 속성이 있는 경우 pte 엔트리의 dirty 플래그를 추가하고 read only 플래그를 제거한다.
  • 코드 라인 51~53에서 해당 가상주소에 대한 pte 테이블 페이지의 lock을 걸고 pte 엔트리를 알아온다. 만일 엔트리가 이미 설정되어 있는 경우 release 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 55에서 MM_ANONPAGES 카운터를 증가시킨다.
  • 코드 라인 56에서 anon rmap(reverse map)에 페이지를 추가한다.
  • 코드 라인 57에서 memcg에 한 페이지가 추가되었음을 commit 한다.
  • 코드 라인 58에서 새 페이지를 active 설정하고 lru 캐시에 추가한다.
    • lru_add_pvec에 추가한다.
  • 코드 라인 60에서 pte 엔트리에 매핑한다.
  • 코드 라인 63에서 tlb 캐시를 업데이트(flush) 한다.
    • armv6 이전 아키텍처는 tlb 캐시를 flush한다.

 

 

anon 매핑에서 swap 페이지의 fault 처리

do_swap_page()

mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with pte unmapped and unlocked.
 *
 * We return with the mmap_sem locked or unlocked in the same cases
 * as does filemap_fault().
 */
static int do_swap_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags, pte_t orig_pte)
{
        spinlock_t *ptl;
        struct page *page, *swapcache;
        struct mem_cgroup *memcg;
        swp_entry_t entry;
        pte_t pte;
        int locked;
        int exclusive = 0;
        int ret = 0;

        if (!pte_unmap_same(mm, pmd, page_table, orig_pte))
                goto out;

        entry = pte_to_swp_entry(orig_pte);
        if (unlikely(non_swap_entry(entry))) {
                if (is_migration_entry(entry)) {
                        migration_entry_wait(mm, pmd, address);
                } else if (is_hwpoison_entry(entry)) {
                        ret = VM_FAULT_HWPOISON;
                } else {
                        print_bad_pte(vma, address, orig_pte, NULL);
                        ret = VM_FAULT_SIGBUS;
                }
                goto out;
        }
        delayacct_set_flag(DELAYACCT_PF_SWAPIN);
        page = lookup_swap_cache(entry);
        if (!page) {
                page = swapin_readahead(entry,
                                        GFP_HIGHUSER_MOVABLE, vma, address);
                if (!page) {
                        /*
                         * Back out if somebody else faulted in this pte
                         * while we released the pte lock.
                         */
                        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
                        if (likely(pte_same(*page_table, orig_pte)))
                                ret = VM_FAULT_OOM;
                        delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
                        goto unlock;
                }

                /* Had to read the page from swap area: Major fault */
                ret = VM_FAULT_MAJOR;
                count_vm_event(PGMAJFAULT);
                mem_cgroup_count_vm_event(mm, PGMAJFAULT);
        } else if (PageHWPoison(page)) {
                /*
                 * hwpoisoned dirty swapcache pages are kept for killing
                 * owner processes (which may be unknown at hwpoison time)
                 */
                ret = VM_FAULT_HWPOISON;
                delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
                swapcache = page;
                goto out_release;
        }

        swapcache = page;
        locked = lock_page_or_retry(page, mm, flags);

        delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
        if (!locked) {
                ret |= VM_FAULT_RETRY;
                goto out_release;
        }

swap 되어 있는 페이지에 접근했을 때 fault 되어 진입한 경우 swap 캐시 또는 swap file로 부터 페이지를 읽어들인다.

  • 코드 라인 22~23에서 pte 엔트리 값과 orig_pte 엔트리 값이 다른 경우 out 레이블로 이동한 후 함수를 빠져나간다.
  • 코드 라인 24에서 orig_pte 엔트리에 대한 swap 엔트리를 구해온다.
  • 코드 라인 25~36에서 작은 확률로 swap 엔트리가 아닌 경우 다음을 처리한 후 out 레이블로 이동하고 함수를 빠져나간다.
    • 코드 라인 27~28에서 migration 엔트리인 경우 해당 페이지가 unlock될 때까지 기다린다.
      • 해당 페이지의 PG_locked가 클리어될 때까지 대기한다.
    • 코드 라인 29~30에서 엔트리가 hwpoison 엔트리인 경우 에러 코드로 hwpoison을 담는다.
    • 코드 라인 31~34에서 pte 정보를 출력하고 에러 코드로 sigbus를 담는다.
  • 코드 라인 37에서 현재 태스크의 delay 플래그에 delay accounting을 위해 DELAYACCT_PF_SWAPIN 플래그를 설정한다.
  • 코드 라인 38에서 swap 엔트리로 swap 캐시 페이지를 알아온다.
  • 코드 라인 39~41에서 페이지를 알아올 수 없으면 swapin_readahead() 함수를 호출하여 swap file로 부터 요청 페이지를 포함한 일정량의 페이지를 읽어온다.
  • 코드 라인 42~52에서 그래도 페이지를 가져올 수 없으면 설정해둔 DELAYACCT_PF_SWAPIN 플래그를 제거하고 unlock 레이블을 통해 함수를 빠져나간다.
    • swap file을 블럭 디바이스에서 비동기로 읽어들여 바로 가져오지 못할 확률이 크다. 그래서 delay를 발생시킬 목적으로 그냥 함수를 빠져나간다.
  • 코드 라인 55~57에서 에러 코드로 major fault로 대입하고, PGMAJFAULT 카운터를 증가시키고 memcg 설정된 경우에서도 PGMAJFAULT 카운터를 증가시킨다.
    • fault된 페이지가 swap 캐시에서 검색되지 않은 경우 swap file에서 페이지를 읽어온 경우 pgmajfault 카운터가 증가된다.
  • 코드 라인 58~67에서 hwpoison 페이지인 경우 hwpoison 에러 코드를 설정하고 설정해둔 DELAYACCT_PF_SWAPIN 플래그도 제거하며 out_release 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 69~76에서 가져온 페이지에 대해 lock을 시도하고 안되면 retry 에러를 추가하여 out_release 레이블을 통해 함수를 빠져나간다.

 

.       /*
         * Make sure try_to_free_swap or reuse_swap_page or swapoff did not
         * release the swapcache from under us.  The page pin, and pte_same
         * test below, are not enough to exclude that.  Even if it is still
         * swapcache, we need to check that the page's swap has not changed.
         */
        if (unlikely(!PageSwapCache(page) || page_private(page) != entry.val))
                goto out_page;

        page = ksm_might_need_to_copy(page, vma, address);
        if (unlikely(!page)) {
                ret = VM_FAULT_OOM;
                page = swapcache;
                goto out_page;
        }

        if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg)) {
                ret = VM_FAULT_OOM;
                goto out_page;
        }

        /*
         * Back out if somebody else already faulted in this pte.
         */
        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*page_table, orig_pte)))
                goto out_nomap;

        if (unlikely(!PageUptodate(page))) {
                ret = VM_FAULT_SIGBUS;
                goto out_nomap;
        }

        /*
         * The page isn't present yet, go ahead with the fault.
         *
         * Be careful about the sequence of operations here.
         * To get its accounting right, reuse_swap_page() must be called
         * while the page is counted on swap but not yet in mapcount i.e.
         * before page_add_anon_rmap() and swap_free(); try_to_free_swap()
         * must be called after the swap_free(), or it will never succeed.
         */

        inc_mm_counter_fast(mm, MM_ANONPAGES);
        dec_mm_counter_fast(mm, MM_SWAPENTS);
        pte = mk_pte(page, vma->vm_page_prot);
        if ((flags & FAULT_FLAG_WRITE) && reuse_swap_page(page)) {
                pte = maybe_mkwrite(pte_mkdirty(pte), vma);
                flags &= ~FAULT_FLAG_WRITE;
                ret |= VM_FAULT_WRITE;
                exclusive = 1;
        }
        flush_icache_page(vma, page);
        if (pte_swp_soft_dirty(orig_pte))
                pte = pte_mksoft_dirty(pte);
        set_pte_at(mm, address, page_table, pte);
        if (page == swapcache) {
                do_page_add_anon_rmap(page, vma, address, exclusive);
                mem_cgroup_commit_charge(page, memcg, true);
        } else { /* ksm created a completely new copy */
                page_add_new_anon_rmap(page, vma, address);
                mem_cgroup_commit_charge(page, memcg, false);
                lru_cache_add_active_or_unevictable(page, vma);
        }
  • 코드 라인 7~8에서 작은 확률로 swap 캐시 페이지가 아니거나 private 페이지의 private 값이 entry 값과 다른 경우 out_page 레이블로 이동하여 함수를 빠져나간다.
  • 코드 라인 10~15에서 ksm 페이지인 경우 새 페이지를 할당 받고 기존 ksm 페이지로 부터 복사하고, 그렇지 않으면 기존 페이지를 그대로 반환한다. 만일 새 페이지 할당이 실패하는 경우 oom 에러를 반환한다.
  • 코드 라인 17~20에서 가상 페이지 수가 memcg 설정된 commit 할당량을 벗어난 경우 가상 메모리 부족 에러를 반환한다.
    • 주로 memory control group을 사용하여 지정한 태스크의 메모리 사용량을 제어하기 위해 사용한다.
  • 코드 라인 25~27에서 해당 가상주소에 대한 pte 테이블 페이지의 lock을 걸고 pte 엔트리를 알아와서 orig_pte와 다른 경우 누군가 이미 매핑한 경우이므로 내버려 두고 함수를 빠져나간다. 다시 fault될지 모르지만…
  • 코드 라인 29~32에서 작은 확률로 uptodate 플래그가 설정되지 않은 경우 sigbus 에러를 반환한다.
  • 코드 라인 44~45에서 anon 페이지가 추가되었고 swap 페이지가 감소되었으므로 MM_ANONPAGES 카운터를 증가시키고 MM_SWAPENTS 카운터를 감소시킨다.
  • 코드 라인 46에서 해당 페이지에 대해 vma 영역에 설정된 권한 속성을 더해 pte 엔트리를 만든다.
  • 코드 라인 47~52에서 write 요청이 있으면서 swap 캐시를 재 사용할 수 있는 상황인 경우 COW 없이 그냥 swap 캐시에서 제거하고 그 페이지를 그대로 anon 페이지로 전환한다. 그런 후 pte 속성 에서 read only를 제거하고 dirty를 설정하여 준비하고 write 플래그는 제거한다. 반환 값에 VM_FAULT_WRITE를 추가하고 exclusive를 1로 설정한다.
  • 코드 라인 53에서 해당 페이지에 대해 instruction 캐시를 flush 한다.
    • 특정 아키텍처에서 사용되며, arm, arm64, x86 아키텍처 등에서는 아무런 동작도 하지 않는다.
  • 코드 라인 54~55에서 orig_pte 값에 soft-dirty 플래그가 설정된 경우 pte 엔트리에도 soft-dirty 설정을 한다.
    • CONFIG_MEM_SOFT_DIRTY
      • 메모리 변경 시 트래킹을 하기 위해 사용하며 현재는 x86 아키텍처에만 구현되어 있는 커널 옵션이다.
      • 참고: SOFT-DIRTY PTEs – vm/soft-dirty.txt
  • 코드 라인 56에서 page_table 엔트리를 pte 값으로 설정하여 매핑한다.
  • 코드 라인 57~59에서 swap 캐시페이지를 anon 페이지로 그대로 활용한 경우 그 페이지를 anon reverse map에 추가하고 memcg에 에 페이지가 추가되었음을 commit 한다.
  • 코드 라인 60~64에서 swap 캐시 페이지를 새 페이지에 복사하여 사용하게 된 경우 anon reverse map에 새 페이지를 추가하고 memcg에 에 한 페이지가 추가되었음을 commit 하며 lru 캐시에 추가한다.
    • 페이지는 PG_SwapBacked 플래그 설정하고 _mapcount=0(-1부터 시작)으로 초기화한다.

 

.       swap_free(entry);
        if (vm_swap_full() || (vma->vm_flags & VM_LOCKED) || PageMlocked(page))
                try_to_free_swap(page);
        unlock_page(page);
        if (page != swapcache) {
                /*
                 * Hold the lock to avoid the swap entry to be reused
                 * until we take the PT lock for the pte_same() check
                 * (to avoid false positives from pte_same). For
                 * further safety release the lock after the swap_free
                 * so that the swap count won't change under a
                 * parallel locked swapcache.
                 */
                unlock_page(swapcache);
                page_cache_release(swapcache);
        }

        if (flags & FAULT_FLAG_WRITE) {
                ret |= do_wp_page(mm, vma, address, page_table, pmd, ptl, pte);
                if (ret & VM_FAULT_ERROR)
                        ret &= VM_FAULT_ERROR;
                goto out;
        }

        /* No need to invalidate - it was non-present before */
        update_mmu_cache(vma, address, page_table);
unlock:
        pte_unmap_unlock(page_table, ptl);
out:
        return ret;
out_nomap:
        mem_cgroup_cancel_charge(page, memcg);
        pte_unmap_unlock(page_table, ptl);
out_page:
        unlock_page(page);
out_release:
        page_cache_release(page);
        if (page != swapcache) {
                unlock_page(swapcache);
                page_cache_release(swapcache);
        }
        return ret;
}
  • 코드 라인 1에서 엔트리를 swap 캐시에서 제거한다.
  • 코드 라인 2~3에서 swap 캐시가 50% 이상 가득 차거나 locked vma 영역이거나 mlock 설정된 페이지인 경우 좀 더 swap 캐시를 비우려고 시도한다.
  • 코드 라인 4에서 페이지를 unlock한다.
  • 코드 라인 5~16에서 swap 캐시페이지를 새 페이지에 COW한 경우 swapcache 페이지도 unlock하고 release 한다.
  • 코드 라인 18~23에서 write 요청이 있는 경우 새 페이지를 할당받고 COW 하기 위해 do_wp_page() 함수를 호출하고 빠져나간다.
  • 코드 라인 26에서 tlb 캐시를 업데이트(flush) 한다.
    • armv6 이전 아키텍처는 tlb 캐시를 flush한다.
  • 코드 라인 28~30에서 page_table 페이지에 대한 ptl 언락을 하고 함수를 마친다.

 

 

 

swapin_readahead()

mm/swap_state.c

/**
 * swapin_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vma: user vma this address belongs to
 * @addr: target address for mempolicy
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * Primitive swap readahead code. We simply read an aligned block of
 * (1 << page_cluster) entries in the swap area. This method is chosen
 * because it doesn't cost us any seek time.  We also make sure to queue
 * the 'original' request together with the readahead ones...
 *
 * This has been extended to use the NUMA policies from the mm triggering
 * the readahead.
 *
 * Caller must hold down_read on the vma->vm_mm if vma is not NULL.
 */
struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
                        struct vm_area_struct *vma, unsigned long addr)
{
        struct page *page;
        unsigned long entry_offset = swp_offset(entry);
        unsigned long offset = entry_offset;
        unsigned long start_offset, end_offset;
        unsigned long mask;
        struct blk_plug plug;

        mask = swapin_nr_pages(offset) - 1;
        if (!mask)
                goto skip;

        /* Read a page_cluster sized and aligned cluster around offset. */
        start_offset = offset & ~mask;
        end_offset = offset | mask;
        if (!start_offset)      /* First page is swap header. */
                start_offset++;

        blk_start_plug(&plug);
        for (offset = start_offset; offset <= end_offset ; offset++) {
                /* Ok, do the async read-ahead now */
                page = read_swap_cache_async(swp_entry(swp_type(entry), offset),
                                                gfp_mask, vma, addr);
                if (!page)
                        continue;
                if (offset != entry_offset)
                        SetPageReadahead(page);
                page_cache_release(page);
        }
        blk_finish_plug(&plug);

        lru_add_drain();        /* Push any new pages onto the LRU now */
skip:
        return read_swap_cache_async(entry, gfp_mask, vma, addr);
}

swap 파일로 부터 swap 캐시로 요청 offset 페이지에 대한 readahead(한 번에 미리 읽어올 적정 페이지 수)페이지들을 읽어오도록 비동기 요청한다. 단 해당 offset 페이지는 읽혀올 때까지 블럭된다.

  • 코드 라인24에서 swap 엔트리 값에서 offset을 추출한다.
  • 코드 라인 30~32에서 swap 파일에서 swap 캐시로 읽어올 readahead(한 번에 미리 읽어올 적정 페이지 수) 페이지 수에서 -1을 하여 mask 값을 구한다. 만일 mask 값이 0인 경우 skip 레이블로 이동한다.
  • 코드 라인 35~36에서 offset을 mask를 사용하여 시작 offset과 끝 offset을 구한다.
    • 예) mask=7, offset=30
      • start_offset=24, end_offset=31
  • 코드 라인 37~38에서 start_offset이 0인 경우 1부터 시작하게 한다.
    • 특별히 0에는 swap 헤더가 존재한다.
  • 코드 라인 40에서 태스크에서 blk_plug를 초기화 하고 pending I/O에 의한 데드락 트래킹하도록 구성한다.
  • 코드 라인 41~50에서 swap 파일에서 해당 offset에 대한 페이지를 읽어오도록 비동기 요청을 한다. 이미 요청 offset이 아닌 다른 offset 페이지에는 PG_readahead 플래그를 설정한다.
  • 코드 라인 51에서 태스크에 설정해 둔 blk_plug를 flush한다.
  • 코드 라인 53~55에서 per cpu lru들을 lruvec으로 이동시키고 다시 swap 파일에서 해당 offset에 대한 페이지를 읽어오도록 비동기 요청을 한다.
    • 두 번째 호출 시 내부에서는 swap file로 부터 해당 페이지를 불러오는데 완료될 때까지 블럭된다.

 

swap 캐시용 raadahead 페이지 산출

swapin_nr_pages()

mm/swap_state.c

static unsigned long swapin_nr_pages(unsigned long offset)
{
        static unsigned long prev_offset;
        unsigned int pages, max_pages, last_ra;
        static atomic_t last_readahead_pages;

        max_pages = 1 << ACCESS_ONCE(page_cluster);
        if (max_pages <= 1)
                return 1;

        /*
         * This heuristic has been found to work well on both sequential and
         * random loads, swapping to hard disk or to SSD: please don't ask
         * what the "+ 2" means, it just happens to work well, that's all.
         */
        pages = atomic_xchg(&swapin_readahead_hits, 0) + 2;
        if (pages == 2) {
                /*
                 * We can have no readahead hits to judge by: but must not get
                 * stuck here forever, so check for an adjacent offset instead
                 * (and don't even bother to check whether swap type is same).
                 */
                if (offset != prev_offset + 1 && offset != prev_offset - 1)
                        pages = 1;
                prev_offset = offset;
        } else {
                unsigned int roundup = 4;
                while (roundup < pages)
                        roundup <<= 1;
                pages = roundup;
        }

        if (pages > max_pages)
                pages = max_pages;

        /* Don't shrink readahead too fast */
        last_ra = atomic_read(&last_readahead_pages) / 2;
        if (pages < last_ra)
                pages = last_ra;
        atomic_set(&last_readahead_pages, pages);

        return pages;
}

swap 파일에서 swap 캐시로 읽어올 readahead(한 번에 미리 읽어올 적정 페이지 수) 페이지 수를 산출한다.

  • readahead 페이지 수는 swap 캐시 hit 를 기반으로 huristic 하게 관리된다.
  • readahead 수는 유지, 증가, 감소가 가능하되 증가 시엔 항상 2배씩 증가하고, 감소 시에는 절반씩 감소한다.
    • 범위는 최소 1 ~ 최대 클러스터 당 페이지 수이며 2의 차수 단위이다.
      • 예) 4 -> 8(증가) -> 8(유지) -> 16(증가) -> 32(증가) -> 16(감소) -> 16(유지) -> 8(감소) -> 4(감소) -> 2(감소) -> 1(감소)

 

  • 코드 라인 7~9에서 2^page_cluster 값을 max_pages에 대입한다.
    • 클러스터 단위가 최대 처리할 수 있는 페이지이다.
  • 코드 라인 16에서 swapin_readahead_hits  +2를 하여 pages에 대입하고 swapin_readahead_hits 값은 0으로 초기화한다.
    • swapin_readahead_hits
      • default 값은 4부터 출발하고 통계 기반으로 적중률이 높아지면 증가된다.
        • swap 캐시가 hit되면 1씩 증가한다.
        • swap 캐시가 miss되어 이 함수가 호출되면 0으로 리셋된다.
  • 코드 라인 17~25에서 기존 swapin_readahead_hits 값이 0인 경우 즉, 이전에도 swap 캐시에서 페이지 lookup이 실패한 경우 이전 offset 요청이 현재 offset 요청과 +1/-1 차이로 연속된 번호가 아닌 경우 pages에 1을 대입한다. 그리고 현재 offset을 기억해둔다.
  • 코드 라인 26~31에서 초기화 전 swapin_readahead_hits 값이 0이 아닌 경우 즉, 이전에 swap 캐시에서 페이지 lookup이 실패하지 않았던 경우 pages 값을 최소 4부터 시작하여 2의 차수 단위로 올림 정렬한다.
  • 코드 라인 33~34에서 결정된 pages 값이 max_pages를 초과하는 경우 max_pages로 변경한다.
  • 코드라인 37~42에서 페이지가 마지막에 읽었던 readhead 페이지의 절반보다 작으면 그 절반 값을 last_readahead_pages에 대입하고 반환한다.
    •  last_readahead_pages
      • 가장 마지막에 사용한 swap 사용 시 readahead 페이지 수를 보관한다.

 

radix tree로 구성된 swap 캐시 검색

lookup_swap_cache()

mm/swap_state.c

/*
 * Lookup a swap entry in the swap cache. A found page will be returned
 * unlocked and with its refcount incremented - we rely on the kernel
 * lock getting page table operations atomic even if we drop the page
 * lock before returning.
 */
struct page * lookup_swap_cache(swp_entry_t entry)
{
        struct page *page;

        page = find_get_page(swap_address_space(entry), entry.val);

        if (page) {
                INC_CACHE_INFO(find_success);
                if (TestClearPageReadahead(page))
                        atomic_inc(&swapin_readahead_hits);
        }

        INC_CACHE_INFO(find_total);
        return page;
}

swap 엔트리로 radix tree로 구성된 swap 캐시에서 페이지를 구해 반환한다.

  • 코드 라인 11에서 swap 엔트리로 radix tree로 구성된 swap 캐시에서 페이지를 구해 반환한다.
    • #define swap_address_space(entry) (&swapper_spaces[swp_type(entry)])
  • 코드 라인 13~17에서 만일 페이지가 발견되면 swap_cache_info.find_success 카운터를 증가시킨다. 그리고 readahead 플래그가 설정되어 있으면 클리어하고 swapin_readahead_hits 값을 증가시킨다.
    • 이 값은 swap 캐시에서의 lookup이 계속 성공할 때마다 증가되고 실패하는 순간 0으로 초기화된다.
  • 코드 라인 19에서 swap_cache_info.find_total 카운터를 증가시킨다.

 

anon 매핑에서 protnone 페이지의 fault 처리 (numa migration for automatic numa balancing)

do_numa_page()

mm/memory.c

static int do_numa_page(struct mm_struct *mm, struct vm_area_struct *vma,
                   unsigned long addr, pte_t pte, pte_t *ptep, pmd_t *pmd)
{
        struct page *page = NULL;
        spinlock_t *ptl;
        int page_nid = -1;
        int last_cpupid;
        int target_nid;
        bool migrated = false;
        bool was_writable = pte_write(pte);
        int flags = 0;

        /* A PROT_NONE fault should not end up here */
        BUG_ON(!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE)));

        /*
        * The "pte" at this point cannot be used safely without
        * validation through pte_unmap_same(). It's of NUMA type but
        * the pfn may be screwed if the read is non atomic.
        *
        * We can safely just do a "set_pte_at()", because the old
        * page table entry is not accessible, so there would be no
        * concurrent hardware modifications to the PTE.
        */
        ptl = pte_lockptr(mm, pmd);
        spin_lock(ptl);
        if (unlikely(!pte_same(*ptep, pte))) {
                pte_unmap_unlock(ptep, ptl);
                goto out;
        }

        /* Make it present again */
        pte = pte_modify(pte, vma->vm_page_prot);
        pte = pte_mkyoung(pte);
        if (was_writable)
                pte = pte_mkwrite(pte);
        set_pte_at(mm, addr, ptep, pte);
        update_mmu_cache(vma, addr, ptep);

        page = vm_normal_page(vma, addr, pte);
        if (!page) {
                pte_unmap_unlock(ptep, ptl);
                return 0;
        }

        /*
         * Avoid grouping on RO pages in general. RO pages shouldn't hurt as
         * much anyway since they can be in shared cache state. This misses
         * the case where a mapping is writable but the process never writes
         * to it but pte_write gets cleared during protection updates and
         * pte_dirty has unpredictable behaviour between PTE scan updates,
         * background writeback, dirty balancing and application behaviour.
         */
        if (!(vma->vm_flags & VM_WRITE))
                flags |= TNF_NO_GROUP;

        /*
         * Flag if the page is shared between multiple address spaces. This
         * is later used when determining whether to group tasks together
         */
        if (page_mapcount(page) > 1 && (vma->vm_flags & VM_SHARED))
                flags |= TNF_SHARED;

        last_cpupid = page_cpupid_last(page);
        page_nid = page_to_nid(page);
        target_nid = numa_migrate_prep(page, vma, addr, page_nid, &flags);
        pte_unmap_unlock(ptep, ptl);
        if (target_nid == -1) {
                put_page(page);
                goto out;
        }

        /* Migrate to the requested node */
        migrated = migrate_misplaced_page(page, vma, target_nid);
        if (migrated) {
                page_nid = target_nid;
                flags |= TNF_MIGRATED;
        } else
                flags |= TNF_MIGRATE_FAIL;

out:
        if (page_nid != -1)
                task_numa_fault(last_cpupid, page_nid, 1, flags);
        return 0;
}

참고: Automatic NUMA Balancing | redhat – 다운로드 pdf

 

write protect 페이지의 fault 처리

do_wp_page()

mm/memory.c

/*
 * This routine handles present pages, when users try to write
 * to a shared page. It is done by copying the page to a new address
 * and decrementing the shared-page counter for the old page.
 *
 * Note that this routine assumes that the protection checks have been
 * done by the caller (the low-level page fault routine in most cases).
 * Thus we can safely just mark it writable once we've done any necessary
 * COW.
 *
 * We also mark the page dirty at this point even though the page will
 * change only once the write actually happens. This avoids a few races,
 * and potentially makes it more efficient.
 *
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), with pte both mapped and locked.
 * We return with mmap_sem still held, but pte unmapped and unlocked.
 */
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                spinlock_t *ptl, pte_t orig_pte)
        __releases(ptl)
{
        struct page *old_page, *new_page = NULL;
        pte_t entry;
        int ret = 0;
        int page_mkwrite = 0;
        bool dirty_shared = false;
        unsigned long mmun_start = 0;   /* For mmu_notifiers */
        unsigned long mmun_end = 0;     /* For mmu_notifiers */
        struct mem_cgroup *memcg;

        old_page = vm_normal_page(vma, address, orig_pte);
        if (!old_page) {
                /*
                 * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
                 * VM_PFNMAP VMA.
                 *
                 * We should not cow pages in a shared writeable mapping.
                 * Just mark the pages writable as we can't do any dirty
                 * accounting on raw pfn maps.
                 */
                if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
                                     (VM_WRITE|VM_SHARED))
                        goto reuse;
                goto gotten;
        }

write 권한이 없어 fault된 페이지에 대해 해당 페이지 또는 COW(Copy On Write) anon 페이지에 write 권한 매핑을 한다.

  • 코드 라인 33~47에서 special 페이지인 경우 vma가 공유 write 매핑된 경우 재사용하기 위해 reuse: 레이블로 이동하고, 그렇지 않은 경우 COW를 위해 gotten: 레이블로 이동한다.
    • null을 반환하는 경우 VM_PFMMAP 또는 VM_MIXEDMAP에서 사용하는 special 페이지이다.

 

.       /*
         * Take out anonymous pages first, anonymous shared vmas are
         * not dirty accountable.
         */
        if (PageAnon(old_page) && !PageKsm(old_page)) {
                if (!trylock_page(old_page)) {
                        page_cache_get(old_page);
                        pte_unmap_unlock(page_table, ptl);
                        lock_page(old_page);
                        page_table = pte_offset_map_lock(mm, pmd, address,
                                                         &ptl);
                        if (!pte_same(*page_table, orig_pte)) {
                                unlock_page(old_page);
                                goto unlock;
                        }
                        page_cache_release(old_page);
                }
                if (reuse_swap_page(old_page)) {
                        /*
                         * The page is all ours.  Move it to our anon_vma so
                         * the rmap code will not search our parent or siblings.
                         * Protected against the rmap code by the page lock.
                         */
                        page_move_anon_rmap(old_page, vma, address);
                        unlock_page(old_page);
                        goto reuse;
                }
                unlock_page(old_page);
        } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
                                        (VM_WRITE|VM_SHARED))) {
                page_cache_get(old_page);
                /*
                 * Only catch write-faults on shared writable pages,
                 * read-only shared pages can get COWed by
                 * get_user_pages(.write=1, .force=1).
                 */
                if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
                        int tmp;

                        pte_unmap_unlock(page_table, ptl);
                        tmp = do_page_mkwrite(vma, old_page, address);
                        if (unlikely(!tmp || (tmp &
                                        (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
                                page_cache_release(old_page);
                                return tmp;
                        }
                        /*
                         * Since we dropped the lock we need to revalidate
                         * the PTE as someone else may have changed it.  If
                         * they did, we just return, as we can count on the
                         * MMU to tell us if they didn't also make it writable.
                         */
                        page_table = pte_offset_map_lock(mm, pmd, address,
                                                         &ptl);
                        if (!pte_same(*page_table, orig_pte)) {
                                unlock_page(old_page);
                                goto unlock;
                        }
                        page_mkwrite = 1;
                }

                dirty_shared = true;
  • 코드 라인 5에서 anon 페이지이면서 KSM(Kernel Same Merge) 페이지가 아닌 경우
  • 코드 라인 6~17에서 페이지의 PG_locked를 획득(설정)하고 페이지를 release 한다.  만일 락 획득 과정에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 포기하고 빠져나간다.
  • 코드 라인 18~27에서 swap 페이지를  재사용할 수 있는 경우 PG_dirty 설정을 하고 기존 reserve map을 재배치하고 PG_locked를 해제한다. 그리고 계속 reuse: 레이블을 진행한다.
    • swap 캐시가 참조되지 않았으면서 PG_writeback이 없는 경우 swap 캐시를 지우고 PG_dirty 설정을 한다.
  • 코드 라인 28에서 페이지의 PG_locked를 해제한다.
  • 코드 라인 29~31에서 낮은 확률로 vma가 공유 write 설정된 경우(file) old 페이지의 참조 카운터를 증가시킨다.
  • 코드 라인 37~46에서 vma의 ops 핸들러에 page_mkwrite가 설정된 경우 pte 엔트리가 있는 페이지 테이블 락을 해제하고 do_page_mkwrite()를 수행하여 write 설정을 한다.  이 과정에서 mkwrite 수행 시 에러가 있는 경우 페이지를 release하고 빠져나간다.
  • 코드 라인 53~58에서 다시 페이지 테이블 락을  얻는다. 이 과정에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 포기하고 빠져나간다.
  • 코드 라인 59에서 page_mkwrite를 설정하여 아래 reuse: 레이블의 코드에서 vma의 ops 핸들러에 page_mkwrite 핸들러가 없는 경우에만 동작시킬 코드를 구분하기 위한 플래그이다.
  • 코드 라인 62에서 아래 reuse: 레이블의 코드를 3군데에서 호출하여 사용하는데, 특별히 위의 조건에서만 진행할 코드를 수행할 수 있도록 이를 판단하기 위해 dirty_shared를 설정한다.

 

reuse:
                /*
                 * Clear the pages cpupid information as the existing
                 * information potentially belongs to a now completely
                 * unrelated process.
                 */
                if (old_page)
                        page_cpupid_xchg_last(old_page, (1 << LAST_CPUPID_SHIFT) - 1);

                flush_cache_page(vma, address, pte_pfn(orig_pte));
                entry = pte_mkyoung(orig_pte);
                entry = maybe_mkwrite(pte_mkdirty(entry), vma);
                if (ptep_set_access_flags(vma, address, page_table, entry,1))
                        update_mmu_cache(vma, address, page_table);
                pte_unmap_unlock(page_table, ptl);
                ret |= VM_FAULT_WRITE;

                if (dirty_shared) {
                        struct address_space *mapping;
                        int dirtied;

                        if (!page_mkwrite)
                                lock_page(old_page);

                        dirtied = set_page_dirty(old_page);
                        VM_BUG_ON_PAGE(PageAnon(old_page), old_page);
                        mapping = old_page->mapping;
                        unlock_page(old_page);
                        page_cache_release(old_page);

                        if ((dirtied || page_mkwrite) && mapping) {
                                /*
                                 * Some device drivers do not set page.mapping
                                 * but still dirty their pages
                                 */
                                balance_dirty_pages_ratelimited(mapping);
                        }

                        if (!page_mkwrite)
                                file_update_time(vma->vm_file);
                }

                return ret;
        }
  • 코드 라인 7~8에서 old 페이지의 플래그 중 cpupid 정보를 제거하기 위해 모든 해당 비트를 1로 설정한다.
  • 코드 라인 10에서 해당 페이지에 대한 캐시를 각 아키텍처의 고유한 방법으로 flush한다.
  • 코드 라인 11~14에서 young, dirty, write 비트를 설정하고 pte 엔트리를 업데이트한다. 또한 해당 페이지에 대한 캐시를 각 아키텍처의 고유한 방법으로 update 한다.
  • 코드 라인 15에서 페이지 테이블 락(pte 페이지 테이블에 대한 page->ptl)을 해제한다.
  • 코드 라인 16에서 결과 코드에 VM_FAULT_WRITE 플래그를 설정한다.
  • 코드 라인 18~23에서 dirty_shared가 설정된 경우 (vma가 공유 파일 write 설정)  vma의 page_mkwrite 핸들러가 별도로 지정되지 않은 경우 페이지의 락을 획득한다. (PG_locked)
  • 코드 라인 25~29에서 페이지에 PG_dirty 설정을하고 페이지 락을 다시 해제한 후 페이지를 release 한다.
  • 코드 라인 31~37에서 file 매핑된 페이지가 기존에 PG_dirty 설정이 있었거나 vma에 page_mkwrite 핸들러가 있으면 balance_dirty_pages_ratelimited() 함수를 호출한다.
    • dirty 페이지가 제한치를 초과하는 경우 밸런싱을 맞추기 위해 블록디바이스가 백그라운드에서 writeback을 수행할 수 있게 요청한다.
  • 코드 라인 39~40에서 vma에 page_mkwrite 핸들러 함수가 없는 경우 file_update_time() 함수를 호출하여 파일에 update 시간을 기록한다.

 

        /*
         * Ok, we need to copy. Oh, well..
         */
        page_cache_get(old_page);
gotten:
        pte_unmap_unlock(page_table, ptl);

        if (unlikely(anon_vma_prepare(vma)))
                goto oom;

        if (is_zero_pfn(pte_pfn(orig_pte))) {
                new_page = alloc_zeroed_user_highpage_movable(vma, address);
                if (!new_page)
                        goto oom;
        } else {
                new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
                if (!new_page)
                        goto oom;
                cow_user_page(new_page, old_page, address, vma);
        }
        __SetPageUptodate(new_page);

        if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg))
                goto oom_free_new;

        mmun_start  = address & PAGE_MASK;
        mmun_end    = mmun_start + PAGE_SIZE;
        mmu_notifier_invalidate_range_start(mm, mmun_start, mmun_end);
  • 코드 라인 4에서 기존 페이지를 복사하기 위해 페이지 참조 카운터를 증가시킨다.
  • 코드 라인 6에서 페이지 테이블 페이지의 ptl 락을 해제한다.
  • 코드 라인 8~9에서 낮은 확률로  메모리가 부족하여 anon_vma를 준비하지 못하면 oom 레이블로 이동한다.
  • 코드 라인 11~14에서 zero 페이지가 매핑되어 있었던 경우 0으로 초기화된 새로운 페이지를 준비한다. 메모리 부족으로 새 페이지가 준비되지 않으면 oom 레이블로 이동한다.
  • 코드 라인 15~20에서 zero 페이지가 아닌 경우 새로운 페이지를 준비하고 기존 페이지로부터 복사한다. 만일 메모리 부족으로 새 페이지가 준비되지 않으면 oom 레이블로 이동한다.
  • 코드 라인 21에서 새 페이지의 PG_uptodate 플래그를 설정한다.
  • 코드 라인 23~24에서 memcg에 새 페이지의 할당 한계가 초과되었는지 알아보고 초과한 경우 oom_free_new 레이블로 이동한다.
  • 코드 라인 26~28에서 새 가상 주소 페이지의 주소 범위를 인수로 mmu notifier 리스트에 등록한 invalidate_range_start 콜백 함수들을 호출한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 invalidate_range_start 콜백 함수들
      • virt/kvm/kvm_main.c – kvm_mmu_notifier_invalidate_range_start()
      • drivers/gpu/drm/radeon/radeon_mn.c – radeon_mn_invalidate_range_start()
      • drivers/infiniband/core/umem_odp.c – ib_umem_notifier_invalidate_range_start()
      • drivers/misc/mic/scif/scif_dma.c – scif_mmu_notifier_invalidate_range_start()
    • mmu notifier
      • CONFIG_MMU_NOTIFIER 커널 옵션을 사용하는 경우 CONFIG_SRCU 커널 옵션이 자동 선택되며 read-side 크리티컬 섹션에서 슬립이 가능한 SRCU(Sleepable RCU)를 사용할 수 있다.
      • mm이 변경되는 경우 notify 하는 메커니즘을 제공하는데 리스트  탐색 시 SRCU를 사용한다.

 

.       /*
         * Re-check the pte - we dropped the lock
         */
        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (likely(pte_same(*page_table, orig_pte))) {
                if (old_page) {
                        if (!PageAnon(old_page)) {
                                dec_mm_counter_fast(mm, MM_FILEPAGES);
                                inc_mm_counter_fast(mm, MM_ANONPAGES);
                        }
                } else
                        inc_mm_counter_fast(mm, MM_ANONPAGES);
                flush_cache_page(vma, address, pte_pfn(orig_pte));
                entry = mk_pte(new_page, vma->vm_page_prot);
                entry = maybe_mkwrite(pte_mkdirty(entry), vma);
                /*
                 * Clear the pte entry and flush it first, before updating the
                 * pte with the new entry. This will avoid a race condition
                 * seen in the presence of one thread doing SMC and another
                 * thread doing COW.
                 */
                ptep_clear_flush_notify(vma, address, page_table);
                page_add_new_anon_rmap(new_page, vma, address);
                mem_cgroup_commit_charge(new_page, memcg, false);
                lru_cache_add_active_or_unevictable(new_page, vma);
                /*
                 * We call the notify macro here because, when using secondary
                 * mmu page tables (such as kvm shadow page tables), we want the
                 * new page to be mapped directly into the secondary page table.
                 */
                set_pte_at_notify(mm, address, page_table, entry);
                update_mmu_cache(vma, address, page_table);
                if (old_page) {
                        /*
                         * Only after switching the pte to the new page may
                         * we remove the mapcount here. Otherwise another
                         * process may come and find the rmap count decremented
                         * before the pte is switched to the new page, and
                         * "reuse" the old page writing into it while our pte
                         * here still points into it and can be read by other
                         * threads.
                         *
                         * The critical issue is to order this
                         * page_remove_rmap with the ptp_clear_flush above.
                         * Those stores are ordered by (if nothing else,)
                         * the barrier present in the atomic_add_negative
                         * in page_remove_rmap.
                         *
                         * Then the TLB flush in ptep_clear_flush ensures that
                         * no process can access the old page before the
                         * decremented mapcount is visible. And the old page
                         * cannot be reused until after the decremented
                         * mapcount is visible. So transitively, TLBs to
                         * old page will be flushed before it can be reused.
                         */
                        page_remove_rmap(old_page);
                }

                /* Free the old page.. */
                new_page = old_page;
                ret |= VM_FAULT_WRITE;
        } else
                mem_cgroup_cancel_charge(new_page, memcg);
  • 코드 라인 4~5에서 페이지 테이블 페이지의 ptl 락을 획득한 후에도 pte 엔트리 값이 변경된 적이 없으면 해당 pte 엔트리에 대해 다른 cpu와의 경쟁이 없었거나 경쟁 상황에서 이긴 경우이다.
  • 코드 라인 6~12에서 기존 페이지가 anon 페이지가 아닌 경우 MM_FILEPAGES 카운터를 줄이고 MM_ANONPAGES 카운터를 증가시킨다. 기존 페이지가 없었던 경우는 MM_ANONPAGES만 증가시킨다.
  • 코드 라인 13에서 orig_pte에 해당하는 페이지 영역을 캐시 flush 한다.
  • 코드 라인 14~15에서 새 페이지에 해당하는 pte 엔트리 속성을 준비하고 L_PTE_DIRTY를 추가하고 vma에 write 설정된 경우 L_PTE_RDONLY를 제거한다.
  • 코드 라인 22에서 pte 엔트리를 언매핑(0) 한 후 아키텍처에 따라 해당 페이지에 대해 TLB 캐시를 flush한다. 그런 후 mmu notifier 리스트에 등록한 invalidate_range 콜백 함수들을 호출한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 invalidate_range 콜백 함수들
      • drivers/infiniband/hw/mlx5/odp.c – mlx5_ib_invalidate_range()
      • drivers/net/ethernet/adi/bfin_mac.c – blackfin_dcache_invalidate_range()
      • drivers/iommu/amd_iommu_v2.c – mn_invalidate_range()
  • 코드 라인 23~25에서 anon reverse map에 새 페이지를 추가하고, memcg에 새 페이지 추가를 commit 한 후 lru 캐시에 추가한다.
  • 코드 라인 31~32에서 mmu notifier 리스트에 등록한 change_pte 콜백 함수들을 호출한 후 pte 엔트리를 변경한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 change_pte 콜백 함수
      • virt/kvm/kvm_main.c – kvm_mmu_notifier_change_pte()
  • 코드 라인 33~57에서 기존 페이지가 있는 경우 anon reverse map에서 제거한다.
  • 코드 라인 62~63에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 memcg에 새 페이지를 commit 계량하지 않게 취소 요청한다.

 

        if (new_page)
                page_cache_release(new_page);
unlock:
        pte_unmap_unlock(page_table, ptl);
        if (mmun_end > mmun_start)
                mmu_notifier_invalidate_range_end(mm, mmun_start, mmun_end);
        if (old_page) {
                /*
                 * Don't let another task, with possibly unlocked vma,
                 * keep the mlocked page.
                 */
                if ((ret & VM_FAULT_WRITE) && (vma->vm_flags & VM_LOCKED)) {
                        lock_page(old_page);    /* LRU manipulation */
                        munlock_vma_page(old_page);
                        unlock_page(old_page);
                }
                page_cache_release(old_page);
        }
        return ret;
oom_free_new:
        page_cache_release(new_page);
oom:
        if (old_page)
                page_cache_release(old_page);
        return VM_FAULT_OOM;
}
  • 코드 라인 1~2에서 새 페이지가 있는 경우 페이지 참조를 release 한다.
  • 코드 라인 4에서 페이지 테이블 락을 해제한다.
    • pte 엔트리가 있는 페이지 테이블 페이지의 ptl 락을 해제한다.
  • 코드 라인 5~6에서  mmu notifier 리스트에 등록한 invalidate_range_end 콜백 함수들을 호출한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 invalidate_range_end 콜백 함수들
      • virt/kvm/kvm_main.c – kvm_mmu_notifier_invalidate_range_end()
      • drivers/misc/sgi-gru/grutlbpurge.c – gru_invalidate_range_end()
      • drivers/infiniband/core/umem_odp.c – ib_umem_notifier_invalidate_range_end()
  • 코드 라인 7~18에서 old 페이지가 있는 경우 페이지의 참조 카운터를 감소시키고 0이되는 경우 할당 해제한다. 만일 mlocked 페이지에서 COW 하였었던 경우 기존 페이지에 lock을 걸고 PG_mlocked 을 제거하고 NR_MLOCK 카운터를 감소시킨다. 또한 lru 페이지인 경우 PG_lru를 제거하고, lru 리스트에서도 제거한다.  마지막으로 다시 old 페이지에서 lock을 해제한다.(PG_locked 해제)

 

 

빠른 RSS 카운팅을 위해 분리

SPLIT_RSS_COUNTING

include/linux/mm_types.h

#if USE_SPLIT_PTE_PTLOCKS && defined(CONFIG_MMU)
#define SPLIT_RSS_COUNTING
/* per-thread cached information, */
struct task_rss_stat {
        int events;     /* for synchronization threshold */
        int count[NR_MM_COUNTERS]; 
};
#endif /* USE_SPLIT_PTE_PTLOCKS */

USE_SPLIT_PTE_PTLOCKS와 MMU가 설정된 경우 빠른 RSS 카운팅을 위해 RSS 카운팅을 이원화 시킨다.

  • 2개의 rss_stat으로 나뉘어 운영한다.
    • mm->rss_stat.count[] (RSS 통계값 원본)
    • task->rss_stat.count[] (이원화할 때 빠른 카운터로 추가)
  • sync는 task->stat_envents를 증가시키다 TASK_RSS_EVENTS_THRESH(64) 값에 도달하면 sync를 위해 task의 카운터들을 mm에 옮긴다.

 

USE_SPLIT_PTE_PTLOCKS

include/linux/mm_types.h

#define USE_SPLIT_PTE_PTLOCKS   (NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS)

USE_SPLIT_PTE_PTLOCKS은 CONFIG_SPLIT_PTLOCK_CPUS이상의 NR_CPUS가 설정된 경우 동작한다.

  •  rpi2: NR_CPUS=4, CONFIG_SPLIT_PTLOCK_CPUS=4로 설정되어 USE_SPLIT_PTE_PTLOCKS가 운용된다.

 

check_sync_rss_stat()

mm/memory.c

static void check_sync_rss_stat(struct task_struct *task)
{
        if (unlikely(task != current))
                return;
        if (unlikely(task->rss_stat.events++ > TASK_RSS_EVENTS_THRESH))
                sync_mm_rss(task->mm);
}

요청 태스크가 현재 태스크인 경우에 한해 task의 sync 카운터인 rss_stat.events를 증가시킨다. 만일 sync 카운터가 스레졸드(64)를 초과하는 경우 다시 0으로 설정하고 이원화된 rss 카운터를 동기화하도록 요청 한다.

 

sync_mm_rss()

mm/memory.c

void sync_mm_rss(struct mm_struct *mm)
{               
        int i;

        for (i = 0; i < NR_MM_COUNTERS; i++) {
                if (current->rss_stat.count[i]) {
                        add_mm_counter(mm, i, current->rss_stat.count[i]);
                        current->rss_stat.count[i] = 0;
                }
        }
        current->rss_stat.events = 0;
}

현재 태스크의 rss_stat.count[]의 값들을 모두 원래 카운터 값인 메모리 디스크립터의 rss_stat.count[]에 옮기고 sync 카운터를 다시 0으로 한다.

 

참고