Interrupts -3- (irq domain)

<kernel v5.4>

IRQ Domain

각 제조사의 인터럽트 컨트롤러에는 신호 제어 또는 기능이 다른 인터럽트들을 몇 개의 그룹으로 나누어 관리하기도 한다. 리눅스 커널은 전체 인터럽트를 하나의 그룹 또는 기능별로 나뉜 그룹과 동일하게 IRQ 도메인으로 나누어 관리할 수 있다. 즉 인터럽트들을 IRQ 도메인으로 나누어 관리할 수 있다.

 

IRQ 도메인은 다음과 같은 특징이 있다.

  • hwirq는 도메인내에서 중복되지 않는다.
  • 리버스 매핑을 사용하여 구현하였다.

 

정방향 매핑(리눅스 irq -> hwirq)이 아닌 리버스 매핑(hwirq -> 리눅스 irq)을 사용하는 이유는 인터럽트 발생 시 리눅스 irq를 찾는 동작을 더 빨리 지원하기 위함이다.

 

다음 그림은 인터럽트 발생 시 hwirq를 통해  irq 디스크립터를 찾아 연결된 핸들러 함수가 호출되는 과정을 보여준다.

 

도메인 생성 타입

여러 가지 성격의 도메인을 빠르게 구현하기 위해 다음과 같은 방법들을 고려하여 몇 가지의 도메인 생성 방법을 준비하였다.

  • 리버스 맵 할당 방법
    • Linear
      • 연속 할당 매핑 공간
      • 장점: hwirq가 수 백번 이하의 번호를 사용하는 경우에 한하여 부트업 타임에 한꺼번에 리니어 매핑 배열을 만들어도 메모리 부담이 크지 않으므로 구현이 간단하다
    • Tree
      • Radix Tree를 사용한 Dynamic 할당 공간
      • 장점: hwirq 번호가 매우 큰 경우 dynamic 하게 필요한 hwirq에 대해서만 할당 구성하여 메모리를 낭비하지 않게 할 때 사용한다.
    • No-Map
      • 매핑을 위한 공간이 필요 없음 (동일 번호로 자동 매핑)
  • 매핑 조건
    • 매핑 공간 할당 시 같이 매핑하여 사용한다.
    • 매핑 공간 할당 후 추후 매핑하여 사용한다.

 

매핑 구현 방법

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

  • Linear
    • 요청 size 만큼의 리니어 매핑 공간만 만들고 매핑 연결은 하지 않는다.
    • 추후에 매핑 API 등을 사용하여 irq 디스크립터들을 생성하고 hwirq에 매핑하여 사용한다.
    • 구현
      • irq_domain_add_linear() 함수
  • Tree
    • 사이즈 제한 없이 Radix Tree를 초기화 시켜 사용한다. 매핑 공간 및 매핑 연결은 하지 않는다.
    • 추후에 매핑 API 등을 사용하여 irq 디스크립터들을 생성하고 hwirq에 매핑하여 사용한다.
    • 구현
      • irq_domain_add_tree() 함수
  • Nomap
    • irq와 hwirq가 항상 동일하여 매핑을 전혀 필요하지 않는 시스템에서 사용하다.
      • 리니어 매핑 테이블을 만들지도 않고, Radix Tree도 사용하지 않는다.
    • irq domain을 추가하기 전에 irq 디스크립터들이 미리 구성되어 있어야 한다.
    •  구현
      • irq_domain_add_nomap() 함수
  • Legacy
    • 요청 size + first_irq만큼의 리니어 매핑 공간을 만들고, first_hw_irq 매핑 번호부터 first_irq 번부터 자동으로 고정 매핑하여 사용한다.
    • legacy 방식으로 irq domain을 추가하기 전에 irq 디스크립터들이 미리 할당되어 있어야 한다.
    • 구현
      • irq_domain_add_legacy() 함수
  • Legacy ISA
    • Legacy와 동일하나 자동으로 size=16, first_irq=0, first_hw_irq=0과 같이 동작한다. (irq 수가 16개로 제한)
    • 구현
      • irq_domain_add_legacy_isa()  함수
  • Simple
    • 요청 size 만큼의 리니어 매핑 공간을 만들고, hwirq=0번부터, 그리고 irq=first_irq 번부터 irq 디스크립터들을 생성해 연결하고 순서대로 고정 매핑하여 사용한다.
    • 구현
      • irq_domain_add_simple() 함수

 

다음 그림은 여러 유형의 도메인 생성 방법을 보여준다.

 

다음 그림은 도메인 매핑 구현 모델에 따라 매핑에 대한 자료 구조, irq 디스크립터 생성 및 매핑 여부를 보여준다.

 

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

 

IRQ Domain hierarchy

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

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

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

 


IRQ Domain  생성(추가) 및 삭제

도메인 추가 -1- (linear)

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_to_fwnode(of_node), size, size, 0, ops, host_data);
}

size 만큼의 선형 리버스 매핑 블럭을 사용할 계획으로 도메인을 할당한다. 이 함수에서는 매핑을 하지 않으므로, 매핑은 추후 irq_create_mapping() 함수를 사용하여 다이나믹하게 매핑할 수 있다.

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

 

도메인 추가 -2- (nomap)

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_to_fwnode(of_node), 0, max_irq, max_irq, ops, host_data);
}

리니어 매핑 공간 또는 Radix Tree를 사용한 매핑 공간을 사용하지 않고, max_irq 수 만큼 irq 도메인을 생성한다. 매핑을 하지 않으므로 생성한 개수만큼 hwirq 번호는 irq 번호와 동일하게 사용한다.

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

 

도메인 추가 -3- (legacy)

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_to_fwnode(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 + first_irq만큼의 리니어 매핑 공간을 만들고, first_hw_irq 매핑 번호부터 first_irq 번호를 자동으로 고정 매핑하여 사용한다. 미리 매핑 후 변경이 필요 없는 구성에서 사용한다.

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

 

 

도메인 추가 -4- (legacy, isa)

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 아키텍처용 드라이버에서 사용한다.

 

도메인 추가 -5- (tree)

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_to_fwnode(of_node), 0, ~0, 0, ops, host_data);
}

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

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

 

도메인 추가 -6- (simple)

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

요청 size 만큼의 리니어 매핑 공간을 만들고 hwirq=0번부터, irq=first_irq 번호부터 순서대로 자동으로 고정 매핑하여 사용한다.

  • 간단하게 irq 도메인을 만들고 매핑하기 위한 시스템 드라이버에서 사용하며 약 10여개 드라이버에서 사용한다.
      • arm,vic
      • versatile-fpga
      • sa11x0

 

hierarchy 구성을 위한 도메인 추가

irq_domain_add_hierarchy()

include/linux/irqdomain.h

static inline 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)
{
        return irq_domain_create_hierarchy(parent, flags, size,
                                           of_node_to_fwnode(node),
                                           ops, host_data);
}

하이라키 irq 도메인을 추가한다.

 

irq_domain_create_hierarchy()

kernel/irq/irqdomain.c

/**
 * irq_domain_create_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_create_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_create_linear(node, size, ops, host_data);
        else
                domain = irq_domain_create_tree(node, ops, host_data);
        if (domain) {
                domain->parent = parent;
                domain->flags |= flags;
        }

        return domain;
}
EXPORT_SYMBOL_GPL(irq_domain_create_hierarchy);

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

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

 

irq 도메인 추가 공통 함수

__irq_domain_add()

kernel/irq/irqdomain.c -1/2-

/**
 * __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 fwnode_handle *fwnode, int size,
                                    irq_hw_number_t hwirq_max, int direct_max,
                                    const struct irq_domain_ops *ops,
                                    void *host_data)
{
        struct device_node *of_node = to_of_node(fwnode);
        struct irqchip_fwid *fwid;
        struct irq_domain *domain;

        static atomic_t unknown_domains;

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

        if (fwnode && is_fwnode_irqchip(fwnode)) {
                fwid = container_of(fwnode, struct irqchip_fwid, fwnode);

                switch (fwid->type) {
                case IRQCHIP_FWNODE_NAMED:
                case IRQCHIP_FWNODE_NAMED_ID:
                        domain->fwnode = fwnode;
                        domain->name = kstrdup(fwid->name, GFP_KERNEL);
                        if (!domain->name) {
                                kfree(domain);
                                return NULL;
                        }
                        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
                        break;
                default:
                        domain->fwnode = fwnode;
                        domain->name = fwid->name;
                        break;
                }
#ifdef CONFIG_ACPI
        } else if (is_acpi_device_node(fwnode)) {
                struct acpi_buffer buf = {
                        .length = ACPI_ALLOCATE_BUFFER,
                };
                acpi_handle handle;

                handle = acpi_device_handle(to_acpi_device_node(fwnode));
                if (acpi_get_name(handle, ACPI_FULL_PATHNAME, &buf) == AE_OK) {
                        domain->name = buf.pointer;
                        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
                }

                domain->fwnode = fwnode;
#endif
        } else if (of_node) {
                char *name;

                /*
                 * DT paths contain '/', which debugfs is legitimately
                 * unhappy about. Replace them with ':', which does
                 * the trick and is not as offensive as '\'...
                 */
                name = kasprintf(GFP_KERNEL, "%pOF", of_node);
                if (!name) {
                        kfree(domain);
                        return NULL;
                }

                strreplace(name, '/', ':');

                domain->name = name;
                domain->fwnode = fwnode;
                domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
        }

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

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

 

  • 코드 라인 12~15에서 irq 도메인 구조체를 할당한다. 구조체의 마지막에 위치한 linear_revmap[]을 위해 @size 만큼의 정수 배열이 추가된다.
  • 코드 라인 17~70에서 irq 도메인에 fwnode와 이름 정보를 저장한다. 이름 정보가 저장된 경우 IRQ_DOMAIN_NAME_ALLOCATED 플래그를 설정한다.
    • 디바이스 트리 노드가 주어진 경우 노드명으로 이름을 지정하되 ‘:’ 문자열은 ‘/’로 변경된다.

 

kernel/irq/irqdomain.c -2/2-

        if (!domain->name) {
                if (fwnode)
                        pr_err("Invalid fwnode type for irqdomain\n");
                domain->name = kasprintf(GFP_KERNEL, "unknown-%d",
                                         atomic_inc_return(&unknown_domains));
                if (!domain->name) {
                        kfree(domain);
                        return NULL;
                }
                domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
        }

        of_node_get(of_node);

        /* Fill structure */
        INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
        mutex_init(&domain->revmap_tree_mutex);
        domain->ops = ops;
        domain->host_data = host_data;
        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);
        debugfs_add_domain_dir(domain);
        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);
  • 코드 라인 1~11에서 도메인 명이 없는 경우 “unknown-<순번>” 명칭으로 사용한다.
  • 코드 라인 16~17에서 tree 형태로 domain을 사용할 수 있도록 radix tree를 초기화한다.
  • 코드 라인 18~22에서 전달받은 인수를 domain에 설정한다.
  • 코드 라인 23에서 hierarchy 구성인 경우 IRQ_DOMAIN_FLAG_HIERARCHY  플래그를 추가한다.
  • 코드 라인 26에서 debugfs에 도메인 디버깅을 위해 디렉토리를 생성한다.
  • 코드 라인 27에서 전역 irq_domain_list에 생성한 도메인을 추가한다.
  • 코드 라인 30에서 만들어진 도메인 정보를 디버그 출력한다.
  • 코드 라인 31에서 성공적으로 생성한 도메인을 반환한다.

 

irq_domain_check_hierarchy()

kernel/irq/irqdomain.c

static void irq_domain_check_hierarchy(struct irq_domain *domain)
{
        /* Hierarchy irq_domains must implement callback alloc() */
        if (domain->ops->alloc)
                domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;
}

도메인이hierarchy 구성인 경우 IRQ_DOMAIN_FLAG_HIERARCHY  플래그를 추가한다.

 

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을 제거하고 할당 해제한다.

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

 


IRQ 매핑 및 해제

다음은 도메인내 매핑 연결 및 해제 API들이다.

  • irq_domain_associate()
    • irq 디스크립터와 <–> hwirq를 매핑한다.
  • irq_domain_associate_many()
    • 위의 API 기능을 복수로 수행한다.
  • irq_domain_disassociate()
    • irq 디스크립터와 <–> hwirq를 매핑 해제한다.

 

매핑 연결 -1- (single)

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

        domain->mapcount++;
        irq_domain_set_mapping(domain, hwirq, irq_data);
        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~34에서 irq domain의 map 후크에 핸들러 함수가 연결된 경우 호출하여 컨트롤러로 하여금 매핑을 수행하게 한다. 만일 매핑이 실패한 경우 허가되지 않은 irq인 경우 단순 메시지만 출력하고 irq_data의 domain과 hwirq에 null과 0을 설정후 함수를 빠져나간다.
  • 코드 라인 37~38에서 domain에 이름이 설정되지 않은 경우 컨트롤러 이름을 대입한다.
  • 코드 라인 42에서요청한 도메인에서 @hwirq와 irq 디스크립터를 매핑한다.
  • 코드 라인 45에서 irq 라인 상태에에서 norequest 플래그를 클리어한다.
    • IRQ_NOREQUEST
      • 현재 irq 라인에 요청된 인터럽트가 없다.

 

irq_domain_set_mapping()

kernel/irq/irqdomain.c

static void irq_domain_set_mapping(struct irq_domain *domain,
                                   irq_hw_number_t hwirq,
                                   struct irq_data *irq_data)
{
        if (hwirq < domain->revmap_size) {
                domain->linear_revmap[hwirq] = irq_data->irq;
        } else {
                mutex_lock(&domain->revmap_tree_mutex);
                radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
                mutex_unlock(&domain->revmap_tree_mutex);
        }
}

요청한 도메인에서 @hwirq와 irq 디스크립터를 매핑한다.

  • 코드 라인 5~6에서 linear 방식에서 사용되는 irq인 경우 리니어용 리버스 맵 테이블에서 hwirq에 대응하는 곳에 irq 디스크립터와 매핑한다.
  • 코드 라인 7~11에서 tree 방식에서 사용되는 irq인 경우 radix tree용 리버스 맵에 hwirq를 추가하고 irq 디스크립터와 매핑한다.

 

매핑 연결 -2- (many)

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_base 번호부터 매핑한다. 해당 도메인의 ops->map 후크에 등록한 콜백 함수가 있는 경우 해당 컨트롤러를 통해 각각의 매핑을 설정한다.

 

매핑 해제

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, … > 값

 


IRQ 매핑 생성 및 삭제

 

다음은 도메인내 dynamic하게 매핑을 생성(연결 포함) 및 삭제할 수 있는 API들이다. 이들은 dynamic 매핑 방법을 사용하는 linear, tree 및 hierarchy 방식의 irq domain에서 사용한다.

  • irq_create_mapping()
    • irq 디스크립터를 할당하고 지정한 hwirq에 매핑한다.
  • irq_create_of_mapping()
    • irq 디스크립터를 할당하고 디바이스 트리 노드를 통해 알아온 phandle argument 값을 파싱하여 변환한 hwirq에 매핑한다.
  • irq_create_fwspec_mapping()
    • irq 디스크립터를 할당하고 인자로 전달받은 fwspec 구조체를 통해 변환한 hwirq에 매핑한다.
  • irq_create_identity_mapping()
    • hwirq와 irq가 동일한 irq 디스크립터를 할당하고 hwirq에 매핑한다.
  • irq_create_strict_mapping()
    • 복수개의 irq 디스크립터들을 할당하고 지정한 irq 및 hwirq 번호들에 순서대로 매핑한다.
  • irq_create_direct_mapping()
    • irq 디스크립터를 하나 할당받은 후 hwirq 번호와 동일하게 1:1 직접 매핑 방법으로 매핑한다. (hwirq = irq)
  • irq_dispose_mapping()
    • 매핑을 해제하고 irq 디스크립터를 삭제한다.

 

다음 그림은 irq 도메인내에서 irq <–> hwirq 매핑과 irq 디스크립터와의 연동을 수행하는 몇 가지 API들을 보여준다.

 

매핑 생성 -1- (Dynamic)

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

        of_node = irq_domain_get_of_node(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;
        }

        /* Allocate a virtual interrupt number */
        virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(domain->of_node), NULL);
        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를 배정한다.

  • 코드 라인 9~10에서 domain이 null로 지정된 경우 default domain을 선택한다.
  • 코드 라인 11~14에서 domain이 여전히 null인 경우 경고 메시지 출력과 함께 0을 반환한다.
  • 코드 라인 17에서 도메인에 대한 디바이스 노드 정보를 알아온다.
  • 코드 라인 19~23에서 domain에서 hwirq 에 매핑된 값이 있는지 알아온다. 만일 이미 매핑이 존재하는 경우 디버그 메시지 출력과 함께 0을 반환한다.
  • 코드 라인 26~30에서 irq 디스크립터를 할당한다.
  • 코드 라인 32~35에서 할당한 irq 디스크립터를 hwirq에 매핑한다.
  • 코드 라인 37~40에서 성공한 매핑에 대해 디버그 메시지를 출력하고, virq 값을 반환한다.

 

다음 그림은 irq 디스크립터를 할당하여 hwirq에 매핑을 생성하는 모습을 보여준다.

 

매핑 생성 -2- (Device Tree)

irq_create_of_mapping()

kernel/irq/irqdomain.c

unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
        struct irq_fwspec fwspec;

        of_phandle_args_to_fwspec(irq_data->np, irq_data->args,
                                  irq_data->args_count, &fwspec);

        return irq_create_fwspec_mapping(&fwspec);
}
EXPORT_SYMBOL_GPL(irq_create_of_mapping);

irq 디스크립터를 할당하고 디바이스 트리 노드를 통해 알아온 phandle argument 값을 파싱하여 변환한 hwirq에 매핑한다.

 

예) 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)을 매핑한다.

 

매핑 생성 -3- (fwspec)

irq_create_fwspec_mapping()

kernel/irq/irqdomain.c -1/2-

unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
        struct irq_domain *domain;
        struct irq_data *irq_data;
        irq_hw_number_t hwirq;
        unsigned int type = IRQ_TYPE_NONE;
        int virq;

        if (fwspec->fwnode) {
                domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
                if (!domain)
                        domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);
        } else {
                domain = irq_default_domain;
        }

        if (!domain) {
                pr_warn("no irq domain found for %s !\n",
                        of_node_full_name(to_of_node(fwspec->fwnode)));
                return 0;
        }

        if (irq_domain_translate(domain, fwspec, &hwirq, &type))
                return 0;

        /*
         * WARN if the irqchip returns a type with bits
         * outside the sense mask set and clear these bits.
         */
        if (WARN_ON(type & ~IRQ_TYPE_SENSE_MASK))
                type &= IRQ_TYPE_SENSE_MASK;

        /*
         * 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) {
                /*
                 * If the trigger type is not specified or matches the
                 * current trigger type then we are done so return the
                 * interrupt number.
                 */
                if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))
                        return virq;

                /*
                 * If the trigger type has not been set yet, then set
                 * it now and return the interrupt number.
                 */
                if (irq_get_trigger_type(virq) == IRQ_TYPE_NONE) {
                        irq_data = irq_get_irq_data(virq);
                        if (!irq_data)
                                return 0;

                        irqd_set_trigger_type(irq_data, type);
                        return virq;
                }

                pr_warn("type mismatch, failed to map hwirq-%lu for %s!\n",
                        hwirq, of_node_full_name(to_of_node(fwspec->fwnode)));
                return 0;
        }

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

  • 코드 라인 9~15에서 @fwspce을 통해 fwnode가 존재하면 해당 도메인을 알아오고, 찾지 못한 경우 default  domain을 사용한다.
  • 코드 라인 17~21에서 domain이 없는 경우 경고 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 23~24에서 도메인에 등록한 변환 함수인 (*xlate) 후크 함수를 호출하여 hwirq 값과 타입을 구해온다.
    • 예) Device Tree의 경우 인터럽트를 사용하는 각 장치의 “interrupts = { a1, a2 }”에서 인수 2 개로 hwirq 값을 알아온다.
  • 코드 라인 30~31에서 IRQ_TYPE_SENSE_MASK에 해당하는 타입만 허용한다.
  • 코드 라인 37에서 도메인 변환을 통해 찾아온 hwirq에 해당하는 매핑을 통해 virq를 알아온다.
  • 코드 라인 38~63에서 virq가 이미 매핑된 경우 함수를 빠져나간다. 단 함수 타입 체크를 수행하여 다른 경우 경고 메시지를 출력한다.

 

kernel/irq/irqdomain.c -2/2-

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

        irq_data = irq_get_irq_data(virq);
        if (!irq_data) {
                if (irq_domain_is_hierarchy(domain))
                        irq_domain_free_irqs(virq, 1);
                else
                        irq_dispose_mapping(virq);
                return 0;
        }

        /* Store trigger type */
        irqd_set_trigger_type(irq_data, type);

        return virq;
}
EXPORT_SYMBOL_GPL(irq_create_fwspec_mapping);
  • 코드 라인 1~10에서 hierarch를 지원하는 irq domain인 경우 irq 디스크립터만 할당해오고, 그럲지 않은 경우 irq 디스크립터를 할당해서 hwirq에 매핑한다.
  • 코드 라인 12~19에서 irq 디스크립터가 할당되지 않은 경우 매핑을 취소하고 함수를 빠져나간다.
  • 코드 라인 22에서 트리거 타입을 지정한다.
  • 코드 라인 24에서 성공적으로 할당받은 virq를 반환한다.

 

매핑 생성 -4- (Identity)

irq_create_identity_mapping()

kernel/irq/irqdomain.c

static inline int irq_create_identity_mapping(struct irq_domain *host,
                                              irq_hw_number_t hwirq)
{
        return irq_create_strict_mappings(host, hwirq, hwirq, 1);
}

@hwirq 번호와 동일한 irq를 사용하는 irq 디스크립터를 할당받은 후 @hwirq 번호에 매핑 한다.

 

매핑 생성 -5- (Strict)

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)
{
        struct device_node *of_node;
        int ret;

        of_node = irq_domain_get_of_node(domain);
        ret = irq_alloc_descs(irq_base, irq_base, count,
                              of_node_to_nid(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 수 만큼 매핑 한다.

  • 코드 라인 7~11에서 irq_base 번호부터 count 만큼의 irq 디스크립터를 할당 받아온다.
  • 코드 라인 13에서 irq_base 번호에 해당하는 irq 디스크립터들을 hwirq_base 번호부터 @count 수 만큼 매핑한다.
  • 코드 라인 14에서 성공 값 0을 반환한다.

 

매핑 생성 -6- (Direct)

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)
{
        struct device_node *of_node;
        unsigned int virq;

        if (domain == NULL)
                domain = irq_default_domain;

        of_node = irq_domain_get_of_node(domain);
        virq = irq_alloc_desc_from(1, of_node_to_nid(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 디스크립터를 하나 할당받은 후 hwirq 번호와 동일하게 1:1 직접 매핑 방법으로 매핑한다.

  • 코드 라인 6~7에서 domain이 주어지지 않은 경우 default domain을 선택한다.
  • 코드 라인 9~14에서 1개의 irq 디스크립터를 할당 받아온다.
  • 코드 라인 15~20에서 할당 받은 irq가 no 매핑 방법으로 사용할 수 있는 범위를 초과하는 경우 에러 메시지를 출력하고 할당받은 irq 디스크립터를 다시 할당 해제한 후 0을 반환한다.
  • 코드 라인 21에서 매핑한 디버그 정보를 로그로 출력한다.
  • 코드 라인 23~26에서 할당받은 irq 번호로 hwirq 번호를 매핑한다.
  • 코드 라인 28에서 성공한 경우 할당받은 virq 번호를 반환한다.

 

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

irq_find_mapping()

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을 반환한다.

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

 

다음 그림은 irq_find_mapping() 함수가 hwirq에 매핑된 irq 번호를 찾는 과정 3 가지를 보여준다.

 


도메인용 irq 디스크립터 할당과 해제

도메인용 irq 디스크립터 할당

irq_domain_alloc_descs()

kernel/irq/irqdomain.c

int irq_domain_alloc_descs(int virq, unsigned int cnt, irq_hw_number_t hwirq,
                           int node, const struct irq_affinity_desc *affinity)
{
        unsigned int hint;

        if (virq >= 0) {
                virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE,
                                         affinity);
        } else {
                hint = hwirq % nr_irqs;
                if (hint == 0)
                        hint++;
                virq = __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE,
                                         affinity);
                if (virq <= 0 && hint > 1) {
                        virq = __irq_alloc_descs(-1, 1, cnt, node, THIS_MODULE,
                                                 affinity);
                }
        }

        return virq;
}

irq 디스크립터를 @cnt 수만큼 @node에 할당한다. @affinity로 특정 cpu들을 대상으로 지정할 수 있다. @virq를 지정할 수도 있고, 지정하지 않은 경우(-1) 다음과 같은 기준으로 생성한다.

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

 

  • 코드 라인 6~8에서 @virq 번호를 지정한 경우 그 번호부터 @cnt 수 만큼 irq 디스크립터를 @node에 할당하고 @affinity를 지정한다. 그리고 현재 모듈을 irq 디스크립터의 owner로 설정한다.
  • 코드 라인 9~14에서 @virq 번호를 지정하지 않은 경우 hwirq 번호(0인 경우 1)부터 @cnt 수 만큼 irq 디스크립터를 @node에 할당하고 @affinity를 지정한다. 그리고 현재 모듈을 irq 디스크립터의 owner로 설정한다.
  • 코드 라인 15~18에서 만일 할당이 실패한 경우 hwirq 번호가 2 이상이면 irq 번호를 1부터 다시 한 번 시도한다.
  • 코드 라인 21에서 할당한 irq 디스크립터에 대한 virq 번호를 반환한다.

 


IRQ 도메인 검색

디바이스 노드에 해당하는 도메인 검색

irq_find_host()

include/linux/irqdomain.h

static inline struct irq_domain *irq_find_host(struct device_node *node)
{
        struct irq_domain *d;

        d = irq_find_matching_host(node, DOMAIN_BUS_WIRED);
        if (!d)
                d = irq_find_matching_host(node, DOMAIN_BUS_ANY);

        return d;
}

디바이스 트리의 @node 정보를 사용하여 일치하는 irq domain을 찾아온다.

  • DOMAIN_BUS_WIRED 토큰을 먼저 사용하고, 찾을 수 없는 경우 DOMAIN_BUS_ANY를 사용하여 버스 제한 없이 검색한다.

 

irq_find_matching_host()

include/linux/irqdomain.h

static inline struct irq_domain *irq_find_matching_host(struct device_node *node,
                                                        enum irq_domain_bus_token bus_token)
{
        return irq_find_matching_fwnode(of_node_to_fwnode(node), bus_token);
}

디바이스 트리의 @node와 @bus_token 정보를 사용하여 일치하는 irq domain을 찾아온다.

 

irq_find_matching_fwnode()

include/linux/irqdomain.h

static inline
struct irq_domain *irq_find_matching_fwnode(struct fwnode_handle *fwnode,
                                            enum irq_domain_bus_token bus_token)
{
        struct irq_fwspec fwspec = {
                .fwnode = fwnode,
        };

        return irq_find_matching_fwspec(&fwspec, bus_token);
}

인자 @fwnode와 @bus_token 정보를 사용하여 일치하는 irq domain을 찾아온다.

 

irq_find_matching_fwspec()

kernel/irq/irqdomain.c

/**
 * irq_find_matching_fwspec() - Locates a domain for a given fwspec
 * @fwspec: FW specifier for an interrupt
 * @bus_token: domain-specific data
 */
struct irq_domain *irq_find_matching_fwspec(struct irq_fwspec *fwspec,
                                            enum irq_domain_bus_token bus_token)
{
        struct irq_domain *h, *found = NULL;
        struct fwnode_handle *fwnode = fwspec->fwnode;
        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...
         *
         * bus_token == DOMAIN_BUS_ANY matches any domain, any other
         * values must generate an exact match for the domain to be
         * selected.
         */
        mutex_lock(&irq_domain_mutex);
        list_for_each_entry(h, &irq_domain_list, link) {
                if (h->ops->select && fwspec->param_count)
                        rc = h->ops->select(h, fwspec, bus_token);
                else if (h->ops->match)
                        rc = h->ops->match(h, to_of_node(fwnode), bus_token);
                else
                        rc = ((fwnode != NULL) && (h->fwnode == fwnode) &&
                              ((bus_token == DOMAIN_BUS_ANY) ||
                               (h->bus_token == bus_token)));

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

인자 @fwspce 정보와 @bus_token 정보를 사용하여 일치하는 irq domain을 찾아온다.

  • irq_domain_list에 등록한 모든 엔트리에 대해 루프를 돌며 다음 3가지 중 하나로 도메인을 찾는다.
    • 첫 번째, (*select) 후크에 등록된 함수
    • 두 번째, (*match) 후크에 등록된 함수
    • 세 번째, fwnode가 동일하고, @bus_token이 일치하는 경우

 


기타 함수

주요 APIs

다음은 본문에서 설명한 API들이다.

  • irq_domain_add_linear()
  • irq_domain_add_tree()
  • irq_domain_add_legacy()
  • irq_domain_add_legacy_isa()
  • irq_domain_add_simple()
  • __irq_domain_add()
  • irq_domain_remove()
  • irq_domain_create_hierarchy()
  • irq_find_mapping()
  • irq_find_matching_fwspec()
  • irq_domain_associate()
  • irq_domain_associate_many()
  • irq_domain_disassociate()
  • irq_create_mapping()
  • irq_create_of_mapping()
  • irq_create_fwspec_mapping()
  • irq_create_direct_mapping()
  • irq_create_identity_mapping()
  • irq_create_strict_mappings()
  • irq_dispose_mapping()
  • irq_domain_free_fwnode()
  • irq_domain_set_hwirq_and_chip()
  • irq_domain_alloc_descs()
  • irq_find_matching_fwspec()
  • __irq_domain_alloc_fwnode()

 

기타 APIs

  • irq_set_default_host()
  • irq_get_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_linear_revmap()
  • 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()
  • irq_domain_check_msi_remap()
  • irq_domain_update_bus_token()
  • irq_domain_push_irq()
  • irq_domain_pop_irq()
  • irq_domain_hierarchical_is_msi_remap()
  • irq_domain_xlate_onecell()
  • irq_domain_xlate_twocell()
  • irq_domain_xlate_onetwocell()
  • irq_domain_translate_twocell()

 

인터럽트 목록 확인

다음은 rock960 보드에서 QEMU/KVM을 사용하여 Guest OS를 동작시켜 확인한 인터럽트 목록들이다.

$ cat /proc/interrupts
           CPU0       CPU1
 12:       2653       2704     GICv3  27 Level     arch_timer
 47:       1495          0     GICv3  78 Edge      virtio0
 48:         32          0     GICv3  79 Edge      virtio1
 50:          0          0     GICv3  34 Level     rtc-pl031
 51:        181          0     GICv3  33 Level     uart-pl011
 52:          0          0  GICv3-23   0 Level     arm-pmu
 53:          0          0  GICv3-23   1 Level     arm-pmu
 54:          0          0  9030000.pl061   3 Edge      GPIO Key Poweroff
IPI0:       987       1260       Rescheduling interrupts
IPI1:         5        611       Function call interrupts
IPI2:         0          0       CPU stop interrupts
IPI3:         0          0       CPU stop (for crash dump) interrupts
IPI4:         0          0       Timer broadcast interrupts
IPI5:         0          0       IRQ work interrupts
IPI6:         0          0       CPU wake-up interrupts
Err:          0

 

irq 별 sysfs 속성 확인

arch_timer를 담당하는 인터럽트 번호를 찾고, “cd /sys/kernel/irq/<irq>”를 타이핑하여 해당 인터럽트 디렉토리로 진입하여 다음 속성들을 확인한다.

$ cd /sys/kernel/irq/12
$ ls
actions  chip_name  hwirq  name  per_cpu_count  type  wakeup
$ cat actions
arch_timer
$ cat chip_name
GICv3
$ cat hwirq
27
$ cat per_cpu_count
2390,2251
$ cat type
level
$ cat wakeup
disabled

 

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

$ cd /sys/kernel/debug/irq

$ ls
domains irqs

$ ls domains/
default  gic400@40041000  gpio@7e200000  unknown-1

$ ls irqs/
1   11  13  15  17  19  20  22  24  26  28  3   31  33  35  37  39  5  7  9
10  12  14  16  18  2   21  23  25  27  29  30  32  34  36  38  4   6  8

$ cat /irqs/3
handler:  handle_percpu_devid_irq
device:   (null)
status:   0x00031708
            _IRQ_NOPROBE
            _IRQ_NOTHREAD
            _IRQ_NOAUTOEN
            _IRQ_PER_CPU_DEVID
istate:   0x00000000
ddepth:   1
wdepth:   0
dstate:   0x02032a08
            IRQ_TYPE_LEVEL_LOW
            IRQD_LEVEL
            IRQD_ACTIVATED
            IRQD_IRQ_DISABLED
            IRQD_IRQ_MASKED
            IRQD_PER_CPU
node:     0
affinity: 0-3
effectiv:
domain:  gic400@40041000
 hwirq:   0x1e
 chip:    GICv2
  flags:   0x15
             IRQCHIP_SET_TYPE_MASKED
             IRQCHIP_MASK_ON_SUSPEND
             IRQCHIP_SKIP_SET_WAKE

 

/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
 * @mapcount: The number of mapped interrupts
 *
 * Optional elements
 * @fwnode: Pointer to firmware node associated with the irq_domain. Pretty easy
 *          to swap it for the of_node via the irq_domain_get_of_node accessor
 * @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
 * @debugfs_file: dentry for the domain debugfs file
 *
 * 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;
        unsigned int mapcount;

        /* Optional data */
        struct fwnode_handle *fwnode;
        enum irq_domain_bus_token bus_token;
        struct irq_domain_chip_generic *gc;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
        struct irq_domain *parent;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
        struct dentry           *debugfs_file;
#endif

        /* reverse map data. The linear map gets appended to the irq_domain */
        irq_hw_number_t hwirq_max;
        unsigned int revmap_direct_max_irq;
        unsigned int revmap_size;
        struct radix_tree_root revmap_tree;
        struct mutex revmap_tree_mutex;
        unsigned int linear_revmap[];
};

리눅스 인터럽트와 하드웨어 인터럽트 번호를 매핑 및 변환하기 위한 테이블 관리와 함수를 제공한다.

  •  link
    • 전역 irq_domain_list에 연결될 때 사용하다.
  • *name
    • irq domain 명
  • *ops
    • irq domain operations 구조체를 가리킨다.
  • *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한 처리를 한 경우 사용
  • fwnode
    • 연결된 펌에어 노드 핸들 (디바이스 트리, ACPI, …)
  • bus_token
    • 도메인이 사용되는 목적
      • DOMAIN_BUS_ANY (0)
      • DOMAIN_BUS_WIRED (1)
      • DOMAIN_BUS_GENERIC_MSI (2)
      • DOMAIN_BUS_PCI_MSI (3)
      • DOMAIN_BUS_PLATFORM_MSI (4)
      • DOMAIN_BUS_NEXUS (5)
      • DOMAIN_BUS_IPI (6)
      • DOMAIN_BUS_FSL_MC_MSI (7)
      • DOMAIN_BUS_TI_SCI_INTA_MSI (8)
  • *gc
    • 연결된 generic chip 리스트 (인터럽트 컨트롤러 리스트)
  • *parent
    • 상위 irq domain
    • irq domain 하이라키를 지원하는 경우 사용
  • hwirq_max
    • 지원가능한 최대 hwirq 번호
  • 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,
                     enum irq_domain_bus_token bus_token);
        int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
                      enum irq_domain_bus_token bus_token);
        int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
        void (*unmap)(struct irq_domain *d, unsigned int virq);
        int (*xlate)(struct irq_domain *d, struct device_node *node,
                     const u32 *intspec, unsigned int intsize,
                     unsigned long *out_hwirq, unsigned int *out_type);
#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);
        int (*activate)(struct irq_domain *d, struct irq_data *irqd, bool reserve);
        void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
        int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
                         unsigned long *out_hwirq, unsigned int *out_type);
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
        void (*debug_show)(struct seq_file *m, struct irq_domain *d,
                           struct irq_data *irqd, int ind);
#endif
};
  • (*match)
    • 호스와 인터럽트 컨트롤러 디바이스 노드와 매치 여부를 수행하는 함수 포인터. 매치되면 1을 반환한다
  • (*select)
    • 도메인 선택을 위한 함수 포인터
  • (*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 함수 포인터
  • (*translate)
    • CONFIG_IRQ_DOMAIN_HIERARCHY 구성된 도메인에서 (*xlate)를 대체하여 사용하는 함수 포인터

 

참고

 

Interrupts -1- (Interrupt Controller)

<kernel v5.4>

아키텍처별 인터럽트 컨트롤러 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)를 사용하게 되는 경향이 있다.

 

Single & Hierarchical 구성

다음 그림과 같이 2 개 이상의 인터럽트 컨트롤러들을 연결하여 구성할 수도 있다.

 

ARM에 사용되는 인터럽트 컨트롤러들

arm64 시스템에서 인터럽트 컨트롤러로 GIC v1 ~ GIC v4가 주로 사용되고 있다. 그리고 arm32 시스템에선 각사에서 디자인한 다양한 인터럽트 컨트롤러들이 채택되어 사용되고 있음을 알 수 있다.

  • ARM社 design
    • gic
      • arm,pl390
      • 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
    • (수십 종류라 생략…)

 


GIC(Generic Interrupt Controller)

GIC 버전별 주요 특징

  • GIC v1
    • 8개까지 PE 지원
    • 1020개 까지 인터럽트 소스 처리 가능
    • Security extension 지원
    • ARM Cortex-A5, A9, R7 등에서 사용
  • GIC v2
    • v1 기능을 모두 포함
    • 가상화(virtualization) 기능 추가
    • ARM Cortex-A7, A15, A53, A57, … 등에서 사용
    • GIC v2m은 MSI(Message based Interrupt) 기능을 추가한 모델
  • GIC v3
    • v2 기능을 모두 포함
    • 클러스터당 8개 PE씩 최대 128개 PE 지원
    • 1020개 초과 인터럽트 소스 처리 가능
    • 시그널(v1 및 v2에서 사용하는 legacy) 방식 뿐만 아니라 MSI 방식 지원
      • MSI는 SPI와 LPI에서 사용할 수 있다.
    • non-secure와 secure 그룹에 따라 인터럽트 처리를 분리하는 진화된 security 기능 추가
    • SPI, PPI & SGI 이외에 LPI(Locality specific Peripheral Interrupt) 지원
    • affinity routing 기능 지원
    • ARM Cortex-A53, A57, A72, … 등에서 사용
  • GICv4
    • v3 기능을 모두 포함
    • 가상 인터럽트의 직접 주입(injection) 기능
    • ARM Cortex-A53, A57, A72, … 등에서 사용

 

GIC 블럭 다이어그램

다음 그림은 GIC v1(390), v2(400), v3(500)에 대한 간단한 내부 블록 다이어그램이다.

 


GIC v2

security extension 지원

GIC v1부터 security extension이 있는 GIC는 인터럽트마다 secure 및 non-secure 지정을 할 수 있다. (칩 구현에 따라 security extension 지원 여부가 결정된다)

  • secure 인터럽트
    • irq 및 fiq를 secure 펌웨어에 전달할 수 있다.
    • irq보다 더 높은 우선 순위인 fiq는 cpu에서 기존 irq를 처리하는 도중에도 preemption되어 fiq를 처리할 수 있다.
  • non-secure 인터럽트
    • irq만 non-secure 커널에 전달할 수 있다.

 

IRQ vs FIQ

ARM 아키텍처는 IRQ와 FIQ 두 가지 인터럽트 소스를 위해 물리적인 각각의 시그널 라인을 사용한다. 인터럽트가 발생되면 ARM 아키텍처는 비동기(Async) Exception에 해당하는 각각의 벡터로 점프된다. FIQ는 IRQ에 비해 더 빠른 처리가 필요할 때 사용되지만 매우 많은 제약을 가지고 있다. 이러한 특징을 조금 더 알아본다.

  • IRQ 보다 더 높은 우선 순위를 FIQ에 부여하기 때문에 cpu가 IRQ 처리 중에도 preemption되어 FIQ에 따른 ISR을 우선 처리할 수 있다.
  • 32비트 ARM 아키텍처
    • FIQ용 벡터 엔트리가 가장 마지막에 위치해서 jump 코드가 아닌 필요한 코드를 곧바로 사용할 수 있어, 수 사이클이라도 줄일 수 있다.
    • IRQ와 다르게 2 개 이상의 워드를 처리하는 긴 명령 사이클 중간에도 인터럽트된다.
    • IRQ 전환 시 저장(Store)하지 않고 즉시 사용 가능한 뱅크된 레지스터는 r13과 r14 두 개를 사용한다. 그러나 FIQ 전환시에는 이보다 더 많은 r8~r14까지 총 7개를 사용할 수 있고 이 중 link register로 돌아가야 할 주소를 담아 사용하는 r14를 제외하면 6개를 사용할 수 있다.
      • 보통 DRAM access를 최소화(캐시된 DRAM access)하고 6개의 레지스터만을 사용하여 FIQ에 대한 ISR을 수행하므로 대단히 빠른 처리를 할 수 있다.
        • 캐시되지 않은 DRAM access를 ISR 내부에서 자주하는 경우 캐시되지 않은 한 번의 DRAM access당 수십~수백 사이클이 소요되므로 FIQ를 사용하는 의미가 거의 없어진다.
      • Exception 레벨 전환에 따른 Store/Restore해야 할 레지스터의 수가 더 적어 빠르게 처리할 수 있다. (context복원)
        • 레벨 전환에 Store/Restore할 레지스터가 더 적어 빠르다 하더라도 보통 FIQ에서는 IRQ에서 ISR 구현하는 것처럼 레지스터들을 백업하고 복구하는 루틴을 사용하지 않는다. 이는 매우 현명하지 않은 사용예일 뿐이다.
  • 64비트 ARM 아키텍처
    • 32비트 ARM과 다르게 뱅크 레지스터를 사용하지 않으므로 사용할 레지스터들은 Store/Release 루틴을 사용하여야 한다.
      • SRAM 등의 처리빠른 전용 메모리 장치를 사용하지 않는 경우 리눅스 커널에선 더욱 사용할 필요가 없다
  • FIQ 루틴은 IRQ보다 수십 나노 세컨드 또는 수십 사이클을 더 빠르게 처리하는 장점을 활용하기 위해 보통 수 개 이하의 입력 소스만 FIQ로 처리하도록 어셈블리 언어를 직접 사용한다.
    • IRQ 같이 여러 인터럽트 소스를 FIQ로 처리하려면 irq domain 등 분기 처리루틴이 추가되면서 많은 소요 시간이 추가되므로 FIQ를 사용하는 장점이 없어진다.
  • 특별한 사용자 변경 없이는 ARM 리눅스 커널은 IRQ를 처리하고, 시큐어 펌웨어에서 FIQ를 처리한다.
    • 리눅스 커널에서는 여러 인터럽트 소스를 사용하므로 대부분 FIQ를 사용하지 않는다.
    • latency 및 ISR의 처리 시간 단축 문제로 IRQ에서 처리하기 힘든 특정 ISR에서만 FIQ를 사용하도록 해야 한다.
  • 빠른 access 성능을 갖춘 전용 SRAM 등이 있는 하드웨어 환경인 경우 FIQ를 사용하면 레지스터들의 백업과 복구를 DRAM이 아닌 이러한 SRAM 등을 사용하여 처리하면 위의 처리 성능을 대폭 줄일 수 있다.
  • 참고: FIQ Handlers in the ARM Linux Kernel | Bootlin

 

인터럽트 전송 타입

디바이스에서 발생한 인터럽트를 아래 두 가지 형태로 신호 또는 메시지로 GIC에 전달한다. GIC는 관련 cpu에 irq 또는 fiq 형태로 시그널을 전달한다.

  • Legacy Interrupt
    • 시그널 기반 전송 인터럽트는 디바이스가 인터럽트를 위한 별도의 인터럽트 시그널 라인을 통해 직접 GIC에 시그널(1과 0의 표현)로 전달한다.
  • MSI(Message based Interrupt)
    • 메시지 기반 전송 인터럽트는 디바이스가  인터럽트를 위한 별도의 회로를 사용하지 않고 데이터 통신을 수행하는 interconnect 버스(arm 및 arm64에서는 AXI 버스)를 통해 메시지 형태로 GIC에 전달한다.
    • 장점
      • Legacy 인터럽트에 비해 별도의 라인을 사용하지 않아 인터럽트에 사용하는 많은 라인들을 줄일 수 있다.
      • Legacy  인터럽트는 1~4개의 인터럽트를 사용하지만 MSI 방식에서는 디바이스마다 더 많은 인터럽트를 정의하여 사용할 수 있다.
    • 단점
      • interconnect에 직접 연결되는 디바이스만 가능한다. 대표적으로 PCIe 컨트롤러가 있다.
        • PCIe 버스에 연결된 PCIe 디바이스의 경우 PCIe 버스를 경유하고 AXI 버스를 통해 GIC에 인터럽트가 전달된다.
      • interconnect 버스를 통해야 하므로 추가적인 지연(latency)이 발생한다.
        • ITS를 사용하는 경우 DRAM 테이블을 사용하므로 더 큰 지연이 발생하지만, 높은 우선 순위 및 빈도 높은 인터럽트등은 캐시되어 사용되므로 지연을 최대한 줄일 수 있다.

 

다음 그림은 Signal 기반과 Message 기반의 인터럽트 전송 방법을 비교하여 보여준다.

 

다음 그림은 Signal 기반과 Message 기반이 실제 PCIe 디바이스에 구현된 사례를 보여준다.

  • 현재 PCIe 규격은 legacy 인터럽트와 MSI 둘 다 지원한다.

 

GIC 인터럽트 상태

SGI, PPI, SPI 들은 각각의 인터럽트에 대해 아래와 같이 4개의 상태(state)로 구분할 수 있다. 단 LPI는 inactive와 pending 상태로만 구분된다.

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

 

GIC 인터럽트 타입

인터럽트 타입은 다음과 같다. (INTID(인터럽트 ID) 1020~1022번은 특수 목적으로 예약되었다. 예: 1023=no pending interrupt)

    • SGIs(Software Generated Interrupts)
      • 각 core에 전용으로 사용되며, 외부 인터럽트가 아니라 소프트웨어에서 발생시킬 수 있는 내부 IRQ 들로 0 ~ 15 까지의 id를 사용한다.
      • INTID0~7번은 non-secure, INTID8~15번은 secure로 사용하도록 arm사가 적극 권장한다.
    • PPIs(Private Peripherals Interrupts)
      • 각 core에 전용으로 사용될 수 있는 IRQ 들로 INTID16 ~ 31 까지 사용한다.
      • GIC에서 사용하는 클럭과 연동된다.
    • SPIs(Shared Peripherals Interrupts)
      • core에 공용으로 사용될 수 있는 IRQ 들로 INTID32 ~ 1019까지 사용한다.
      • GIC에서 사용하는 클럭과 연동된다.
    • LPI(Locality-specific Peripheral Interrupt)
      • GICv3 이상에서 새롭게 지원하는 타입의 인터럽트로 legacy 시그널 형태는 지원하지 않고 메시지 방식인 MSI 형태만 지원하며 INTID8192 이상을 사용한다.
      • non-secure group 1 인터럽트로만 사용되며 edge trigger 방식으로 동작한다.
      • 지정된 PE에 연동된 redistributtor로 전달되지만, 옵션 장치인 ITS(Interrupt Translation Service)를 사용하면, 특정 PE의 redistributor로 변환하여 라우팅 서비스를 사용할 수 있다.

 

ARM 권장 PPI의 INTIDs

다음은 ARM사에서 권장하는 PPI의 INTID들이다. 그럼에도 불구하고 일부 arm64 SoC 제작사에서는 아래와 다른 번호를 가진 시스템도 있다.

  • 30 : EL1 physical timer
  • 29 : EL3 physical timer
  • 28: Non-Secure EL2 virtual timer (ARMv8.1)
  • 27: EL1 Virtual timer
  • 26: Non-Secure EL2 physical timer
  • 25: Virtual CPU Interface Maintenence interrupt
  • 24: Cross Trigger Interface interrupt
  • 23: Performance Monitor Counter Overflow interrupt
  • 22: Debug Communcations Channel interrupt
  • 20: Secure EL2 Physical Timer (ARMv8.4)
  • 19: Secure EL2 Virtual Timer (ARMv8.4)

 

인터럽트 트리거

인터럽트 트리거 모드는 다음과 같이 두가지 타입이 있고, high 또는 low 신호 방향 중 하나를 가진다.

  • Level Sensitive
  • Edge Trigger

 

다음 그림은 high activate 시그널로 동작하는 두 가지 인터럽트 트리거 모드에 대한 인터럽트 상태 변화를 보여준다.

 

우선 순위 (Priority)

인터럽트별로 0~255까지 우선 순위를 줄 수 있다.

  • 0=가장 높은 우선 순위
  • 255=가장 낮은 우선 순위

 

우선 순위 Filter

  • 설정된 우선 순위 filter 값보다 높은 우선 순위의 인터럽트를 전달하고, 그 외는 block 한다.
    • 설정된 우선 순위 값보다 작은 숫자만 인터럽트를 전달한다.
    • 예) priority filter = 0xf0인 경우
      • 우선 순위 값이 0x00 ~ 0xef 범위인 인터럽트만 허용하고, 나머지는 허용하지 않는다.

 

Prioritization

Preemption Interrupt

  • Binary Point
    • 3개의 비트를 사용한 0~7의 값으로 각 하드웨어 인터럽트에 설정된 priority 값을 두 그룹으로 분리한다.
    • 분리한 두 개의 그룹
      • Group Priority
        • irq preemption: fast interrupt 처리가 가능하도록 현재 처리하고 있는 인터럽트가 있다하더라도 더 높은 우선 순위의 인터럽트 발생 시  먼저 처리하도록 한다.
      • Sub Priority
        • 현재 처리하고 있는 인터럽트의 우선 순위보다 더 높은 우선 순위의 인터럽트 발생 시 먼저 처리하지 않는다.
    • 리눅스 커널은 GIC v3 드라이버에서 Binary Point 값으로 0을 기록하여 리셋시켜 사용한다. (리셋 후 값은 칩 구현에 따라 다르다)

 

다음 그림은 인터럽트 그루핑을 보여준다.

 

Priority Leveling

GIC는 칩 구현에 따라 security extension을 사용하는 경우 최소 32~256 레벨을 사용할 수 있다. (GIC v3 이상에서  two security states에서는 32~256단계를 사용하고, single security states에서는 16~256단계를 사용한다.)

  • GICD_IPRIORITYRn 레지스터는 인터럽트 n번의 우선순위를 기록하는데 사용한다.
    • GICD_IPRIORITYRn에 설정된 우선 순위는 GIC가 사용하는 단계에 맞춰 mask되어 사용된다.
    • 예) 칩이 32단계로 구현된 경우 0xff를 기록하더라도 mask 값 0xf8과 and하여 뒤의 3 비트는 무시되어 0으로 기록된다.
  • GIC v3를 사용하는 경우 칩이 사용중인 레벨은 ICC_CTLR_EL1.pribits 값을 읽어 알아올 수 있다. (Read Only)
    • 수식 = 2^(N+1) 단계(레벨)
    • 3=16 단계
    • 4=32 단계
    • 5=64 단계
    • 6=128 단계
    • 7=256 단계
  • 리눅스 커널은 모든 irq에 대해 한 가지 우선 순위를 사용하여 preemption이 발생하지 않는다. 단 최근 추가된 Pesudo-NMI 기능을 지원하는 경우 nmi를 사용하는 인터럽트는 더 높은 우선 순위로 설정한다.

 

Secure/Non-Secure에서 priority 값 변환

GIC v1~v2에서 security extension을 사용하는 경우 group 1 인터럽트는 non-secure로 사용되고, group 0 인터럽트는 secure로 사용된다.(참고로 GIC v3부터는 group 1 인터럽트도 non-secure와 secure로 분리할 수 있다) 그리고 group 1 인터럽트가 group 0 인터럽트를 선점하지 않도록 ARM 제작사는 다음 내용을 강력히 권장하고 있다.

  • group 1 인터럽트는 0x80 ~ 0xff 범위를 사용한다.
  • group 0 인터럽트는 0x00 ~ 0x7f 범위를 사용한다.
  • group 0 인터럽트는 group 1의 모든 인터럽트들보다 항상 우선 순위를 더 높게 강제한다.
    • 이는 priority 비트들 중 최상위 bit7에 따라 group 0와 group 1이 나뉘는 것을 의미한다.
      • bit7=0 -> group 0
      • bit7=1 -> group 1

 

secure states에 따라 다음과 같은 규칙을 갖는다.

  • secure states
    • group 0에 해당하는 0x00~0x7f 우선 순위와 group 1에 해당하는 0x80~0xff 우선 순위 모두 설정할 수 있고 그 값을 그대로 읽을 수 있다.
  • non-secure states
    • group 1에 해당하는 우선 순위만 기록하고 읽을 수 있다. (group 0에 해당하는 우선 순위는 접근할 수 없다.)
    • non-secure states에서는 secure states에서 사용하는 우선 순위 값과 다르게 표현된다. secure states에서 사용하는 값을 좌측으로 1 시프트한 값을 사용한다.
    • 반대로 non-secure states에서 기록한 값은 secure states 또는 distributor 입장에서 우측으로 1 시프트하고 bit7을 1로 설정한 값을 사용한다.

 

다음 그림과 같이 non-secure에서 사용하는 priority 값과 secure states(distributor와 동일)에서 priority 값은 변환되는 관계를 보여준다.

  • non-secure로 동작하는 리눅스 커널에서 특정 인터럽트의 우선 순위 값을 0x02로 설정하는 경우, GIC의 distributor는 이를 0x81 우선 순위로 인식하여 사용하는 것을 알 수 있다.

 

Virtualization 지원

다음 그림은 하이퍼바이저가 수신한 인터럽트를 Guest OS에 가상 인터럽트로 전달하는 과정을 보여준다.

 

GIC v2 주요 레지스터들

  • GIC는 CPU Interface 레지스터들과 Distributor 레지스트들을 통해 제어한다.
  • CPU Interface 레지스터
    • core로 전달된 group 1 irq들 전체를 한꺼번에 통제한다.
    • core로 전달될 irq들에 대해 설정한 priority mask 값을 사용하여 통과시킨다.
      • priority 레벨링 (16 ~ 256 단계)
  • Distributor 레지스터
    • secure 상태에 따라 group 0/1의 방향을 선택하게 한다. (irq/fiq)
      • non-secure extension 사용시 group은 항상 fiq로 forward된다.
      • secure extension 사용시 group-0은 fiq, group-1은 irq로 forward 된다.
    • IRQ 별로 forward를 설정(Set-enable) 및 클리어(Set-Clear) 요청할 수 있다.
    • IRQ 별로 priority를 설정할 수 있다.
    • IRQ 별로 개별 cpu interface로의 전달여부를 비트마스크로 설정할 수 있다. (1=forward, 0=drop)
    • PPI & SPI에 해당하는 하드웨어 IRQ 별로 인터럽트 트리거 타입을 설정할 수 있다.
      • 0=level sensitive (전압의 레벨이 지속되는 동안)
      • 1=edge trigger (high나 low로 바뀌는 순간만)
        • SGI는 edge 트리거를 사용한다.

 

GIC v2의 AArch64 시스템용 레지스터들은 다음과 같은 그룹으로 나눌 수 있다. (AArch32는 생략)

  • 메모리 맵드 레지스터들
    • GICC, CPU Interface Registers
    • GICD, Distributor Registers
    • GICH, Virtual CPU Interface Control Registers
      • 생략
    • GICV, Virtual CPU Interface Registers
      • 생략

 

1) GICC, CPU Interface Registers

  • GICC_CTLR (CPU Interface Control Register)
    • secure mode 별로 레지스터가 bank 되어 사용된다.
    • Enable
      • group 1 인터럽트의 forward 여부 (0=disable, 1=enable)
    • EnableGrp1
      • group 1 인터럽트의 forward 여부 (0=disable, 1=enable)
    • FIQBypDisGrp1 & IRQBypDisGrp1
      • cpu 인터페이스 비활성화시 PE에 인터럽트 바이패스 Disable 여부 (0=bypass, 1=bypass disable)
    • EOImodeNS
      • EOI 모드 운영 방법 선택
        • 0=GICC_EOIR & GICC_AEOIR을 사용하여 priority drop 기능과 비활성화 기능 지원
        • 1=GICC_EOIR & GICC_AEOIR을 사용하여 priority drop 기능만 지원, 인터럽트 비활성화 기능은 별도로 GICC_DIR 사용해야 한다.
  • GICC_PMR (Interrupt Priority Mask Register)
    • core로 전달될 irq들에 대해 설정한 priority 마스크 미만만 통과시킨다.
        • 리눅스 커널의 GIC v1~v4 드라이버는 디폴트 값으로 0xf0을 사용한다.
    • 0 값을 기록하는 경우 모든 인터럽트가 전달되지 않는다.
  • GICC_BPR (Binary Point Register)
    • priority 값을 group/sub와 같이 두 그룹으로 나누어 그 중 group에 해당하는 priority를 가진 인터럽트에 대해 preemption interrupt로 분류한다.
      • 리눅스 커널의 경우 GIC v3 부터 이 필드에 0 값을 기록하여 리셋시킨다. 설정되는 값은 구현에 따라 다르다.
  • 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_HPPIR (Highest Priority Pending Interrupt Register)
    • 읽기 전용으로 현재 하드웨어 인터럽트 (PPI&SPI)들 중 pending되고 있는 가장 높은 priority irq 번호와 cpu를 읽어온다.
    • 소프트 인터럽트(SGI)의 경우 cpu 값만 읽어올 수 있다.
  • GICC_IIDR (CPU Interface Identification Register)
    • 읽기 전용으로 GIC 제품 ID, 아키텍처 버전, 리비전, 구현회사 등의 정보를 읽어올 수 있다.
  • GICC_ABPR (Aliased Binary Point Register)
  • GICC_APRn (Active Priorities Register)
  • GICC_AEOIR (Aliased End of Interrupt Register)
  • GICC_AIAR (Aliased Interrupt Acknowledge Register)
  • GICC_AHPPIR (Aliased Highest Priority Pending Interrupt Register)
  • GICC_DIR (Deactivate Interrupt Register)
  • GICC_NSAPRn (Non-Secure Active Priorities Register)

 

2) GICD, 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_IGROUPRn (Interrupt Group Registers)
    • secure extension을 사용하는 경우 인터럽트별로 secure group 0와 non-secure group 1을 지정한다.
      • 0=group 0, 1=group 1
    • secure extension을 사용하는 경우 리셋시 0으로(RAS) 읽히며, 모든 인터럽트가 재설정되기 전엔 모두 group 0 상태가된다.
      • SMP 시스템에서 secure extension을 지원하고, non-secure 프로세서들에 대해서는 뱅크된 GICD_IGROUPR0은 group 1 상태로 리셋한다.
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ISENABLERn (Interrupt Set-Enable Registers)
    • IRQ 별로 통과(Set-Enable) 여부를 설정할 수 있다.
      • 1=set 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICENABLERn (Interrupt Clear-Enable Registers)
    • IRQ 별로 통제(Clear-Enable) 여부를 설정할 수 있다.
      • 1=clear 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ISPENDRn (Interrupt Set PENDing Registers)
    • IRQ 별로 pending 상태를 설정할 수 있다.
      • 1=set 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICPENDRn (Interrupt Clear PENDing Registers)
    • IRQ 별로 pending 상태 해제를 요청할 수 있다.
      • 1=clear 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_IPRIORITYRn (Interrupt PRIORITY Registers)
    • IRQ 별로 priority를 설정할 수 있다.
      • 0~255 priority 설정
    • IRQ #0~31을 위해 첫 8개의 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ITARGETRn (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_ICFGRn (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

 

다음 그림은 GICv2 주요 레지스터들에 대해 GICv1 명칭과 비교하여 보여준다.

 


GIC v3

Security States

GIC v2까지 적용되었던 Security Extension 용어를 사용하지 않고, GIC v3 부터는 다음과 같이 두 가지 security states를 가지고 운영한다.

  • Single Security States
    • GIC에서 Secure와 Non-Secure를 나누어 사용하지 않는다.
    • EL0~3 Exception 레벨에 Secure와 Non-Secure 구분하지 않고 인터럽트를 전달한다.
  • Two Security States
    • GIC에서 Secure와 Non-Secure로 나누어 사용된다.
    • EL0~2 Exception 레벨에 Secure와 Non-Secure로 나누어 인터럽트를 전달한다. EL3의 경우는 Secure 상태만 사용한다.
    • 특정 레지스터는 Secure와 Non-Secure에 따라 뱅크되어 운영된다. (같은 이름을 사용하지만 각각 운영된다)

 

인터럽트 라이프 사이클

다음 그림과 같이 각 인터럽트의 발생과 종료에 대한 라이프 사이클을 보여준다.

 

ACK(Acknowledge) 처리 방법

ack 처리는 다음 읽기 전용 레지스터를 읽는 것만으로 완료된다.

  • ICC_IAR0_EL1
  • ICC_IAR1_EL1
  • GICC_IAR
  • GICC_AIAR

 

EOI(End Of Interrupt) 처리 방법

eoi 처리는 다음 쓰기 전용 레지스터를 기록하는 것만으로 완료된다.

  • ICC_EOIR0_EL1
  • ICC_EOIR1_EL1
  • GICC_EOIR
  • GICC_AEOIR

 

deactivation 처리는 다음 쓰기 전용 레지스터를 기록하는 것만으로 완료된다.

  • ICC_DIR_EL1
  • GICC_DIR

 

GIC v3 이상부터 eoi 처리 방법이 다음과 같이 두 개로 나뉜다.

  • priority drop & deactivation 통합 처리
    • GICD_CTLR.EOImode=0 설정 사용
    • eoi 명령 사용 시 priority drop과 deactivation이 자동으로 처리된다.
    • GIC v1 & v2의 경우 eoi 처리 시 priority drop과 deactivation을 둘 다 처리하였다.
  • priority drop & deactivation 분리 처리
    • GICD_CTLR.EOImode=1 설정 사용
    • eoi 명령 사용 시 priority drop만 처리한다.
      • 이 때 running priority에는 idle priority(0xff)가 설정되어 다른 우선 순위를 가진 인터럽트가 preempt되는 것을 허용한다.
      • 단 deactivation 명령 처리 전까지 현재 완료되지 않은 인터럽트 번호는 진입을 허용하지 않는다.
    • 이 모드는 virtualization을 지원하기 위해 채용되었다.
      • 리눅스 커널이 EL2에서 부팅한 경우에만 Host OS 역할로 동작할 때 이렇게 분리된 모드를 사용한다.
      • 리눅스 커널은 v4.3-rc1부터 EOImode=1을 지원한다.

 

인터럽트 수신 Exception 레벨

arm64 시스템에서 커널과 secure firmware를 동작시켜 인터럽트를 수신하는 경우 커널에서는 non-secure group 1에 대해 irq를 수신받고, secure firmware에서는 secure group 0에 대해 irq 보다 더 높은 우선 순위인 fiq를 수신받아 처리하였다. 최근 곧 출시할 ARMv8.4 아키텍처와 GIC v3 등의 최신 아키텍처로 무장한 시스템을 사용하여 linaro 측에서는 secure 처리에 대해 하나의 secure firmware로만 관리하지 않고 secure application과 secure OS를 각 Exception 레벨에서 분리하여 운영할 계획을 가지고 있다. 추가로 Hypervisor도 Secure와 Non-Secure를 분리하여 처리할 계획이 있다.

 

다음 그림은 각 Exception 레벨에서 운영할 Application과 OS등을 보여준다.

  • 각 Exception 레벨에서 인터럽트를 각각 할당하여 처리하여야한다.
  • 상위 EL2나 EL3 Exception 레벨에서는 하위 Exception 레벨을 대신 emulation 하기도 한다. (해당 OS가 직접받아 처리하는 것이 가장 빠르다)
  • ARMv8.4 아키텍처부터 Secure EL2가 동작 가능하다.

 

인터럽트별 group 및 secure 상태 분류

각 Exception 레벨의 Secure 및 Non-Secure 에 인터럽트를 전달하기 위한 경로를 3개로 나누어 처리하는 것을 보여준다.

  • 기존 GIC v2 까지는 single security states 처럼 처리하였다.
    • 인터럽트별로 group을 지정한다.
  • GIC v3부터는 single security states도 지원하지만, two security states를 사용하는 경우 인터럽트별로 group을 지정하고 이어서 group modifier 레지스터를 적용하여 다음과 같이 3가지 상태로 변환하여 처리할 수 있다.
    • G1NS(non-secure group 1)
    • G1S(secure group 1)
    • G0(secure group 0) 또는 G0S

 

IRQ 및 FIQ 분리

GIC v3 이전에는 인터럽트별로 설정된 group 0은 secure 상태로 FIQ로 전달되고, group 1은 non-secure 상태로 IRQ로 전달되었다. 따라서 non-secure 상태로 동작하는 리눅스 커널은 IRQ를 수신하여 처리하고, Secure Firmware는 FIQ를 수신하여 처리하는 것으로 간단히 분리하였다. 그러나 GIC v3 부터는 group modifier 레지스터를 통해 group + secure 상태로 변환되어 조금 더 복잡하게 처리한다.

  • 설정에 의해 각 secure/non-secure Exceptin 레벨로 IRQ 및 FIQ를 보낼 수 있게 하였다.

 

다음 그림은 리눅스 커널이 secure 펌웨어 등과 같이 동작할 때 single 및 two security state에 대해 동작하는 일반적인 사례를 보여준다.

 

다음 그림은 휴대폰으로 전화가 온 경우 secure에서 받은 fiq 인터럽트를 리눅스 커널까지 라우팅하는 두 가지 사례를 보여준다.

  • 1)번 그림의 경우 (1) cpu가 L1 secure state에 있을 때 non-secure group 1으로 지정된 인터럽트가 발생하였다. 이 인터럽트는 non-secure용이기 대문에 Rich OS에 전달해야 하는데 이 인터럽트는 fiq로 변환되어 일단 trust OS 또는 Secure Monitor로 보내게 된다. 왜냐면 SCR_EL3.FIQ=1로 설정되어 있으므로 fiq 인터럽트는 EL3 Secure Monitor로 향하게 된다. Secure Monitor에서는 특수 용도의 인터럽트 번호인 1021번으로 진입하여 fiq 벡터로 수신되는데, 이는 “non-secure로 전환하여 인터럽트를 보내라는 의미“이다. (2) 따라서 non-secure에서 동작하는 Rich OS로 인터럽트를 보내기 위해 non-secure EL1으로 context switching을 하면 (3) 자동으로 non-secure 인터럽트가 non-secure EL1에서 발생하여 Rich OS의 irq 벡터에서 수신할 수 있게 된다.
  • 2)번 그림의 경우는 1)번 그림과 유사하지만 (1) secure EL1에서 동작하는 Trusted OS로 fiq가 전달된다. 왜냐면 SCR_EL3.FIQ가 0으로 설정되었기 때문이다. 그런후 (2) smc 명령을 사용하여 EL3로 context switching을 수행한다. 그 이후 흐름은 1)번 그림과 같다.

 

Pseudo-NMI 구현

arm 및 arm64 시스템에는 NMI 기능이 지원되지 않는다. 그러나 최근 arm64 시스템에서 인터럽트 컨트롤러로 GIC v3 이상을 사용하는 경우 ICC_PMR(Prioirty  Mask Register) 기능으로 irq preemption 기법을 적용하여, x86 시스템의 NMI와 유사한 동작을 수행하도록 구현하였다.

  • local_irq_disable() 루틴의 동작
    • 기존 방법은 “msr daifset, #2” 명령으로 PSTATE.I를 설정하는 것으로 local irq를 disable하였다.
    • 새로운 방법은 PMR을 사용하여 IRQOFF를 위해 0x60 미만의 우선 순위만을 허용하게 설정하여 디폴트 우선 순위 값 0xa0을 사용하는 irq를 차단하지만, NMI용 우선 순위 값 0x20을 사용하는 irq는 허용한다.
  • local_irq_enable() 루틴의 동작
    • 기존 방법은 “msr daifclr, #2” 명령으로 PSTATE.I를 클리어하는 것으로 local irq를 enable하였다.
    • 새로운 방법은 PMR을 사용하여 IRQON을 위해 0xe0 미만의 우선 순위만을 허용하게 설정하여 디폴트 우선 순위 값 0xa0을 사용하는 irq 및 NMI용 우선 순위 값 0x20을 사용하는 irq 둘 다 허용한다.
  • PMR 관련 priority mask 상수 값
    • GIC_PRIO_IRQON (0xe0)
    • GIC_PRIO_IRQOFF (0x60) <- 위의 값 중 bit7 클리어
  • irq 라인별 우선 순위 상수 값
    • GICD_INT_DEF_PRI (0xa0)
    • GICD_INT_NMI_PRI (0x20) <- 위의 값 중 bit7 클리어
  • 사용 방법
    • CONFIG_ARM64_PSEUDO_NMI=y로 설정해야 한다.
    • 커널 파라미터 “irqchip_gicv3_pseudo_nmi=1” 설정
    • APIs
      • request_nmi() & free_nmi()
      • request_percpu_nmi() & free_percpu_nmi()
  • 제약 조건

 

다음은 irq를 disable 방법에 대해 일반적인 방법과 priority mask를 사용하는 방법을 비교하여 보여준다.

 

저전력 지원

절전 기능을 사용하는 경우 priority mask에 의한 인터럽트 마스킹이 아니라 일반 마스킹을 사용한 절전 방법을 보여준다.

  • core를 정지하는 wfi 명령을 사용한 후 수신된 인터럽트로 인해 깨어나서 해당 인터럽트를 처리하는 과정을 보여준다.

 

Affinity 라우팅 지원

SMP로 동작하는 ARM 아키텍처에는 core마다 각 레벨의 affinity 정보가 기록되어 있다. (각 레벨의 affinity 값은 최대 8비트를 사용한다)

  • AArch32에서는 3 레벨로 운영이된다. (MPIDR에 기록되어 있다.)
  • AArch64에서는 3 또는 4 레벨로 운영이된다. (MPIDR_EL1에 기록되어 있다.)

GIC v3의 Affinity 라우팅은 다음과 같이 운영된다.

  • Affnity 라우팅 사용 여부는 다음 레지스터를 사용한다.
    • GICD_CTLR.ARE_NS
    • GICD_CTLR.ARE_S
  • 칩에 설정된 Affinity 라우팅 레벨은 다음 레지스터에서 확인할 수 있다.
    • ICC_CTLR_EL3.A3V
    • ICC_CTLR_EL1.A3V
    • GICD_TYPER.A3V
  • AArch32에서는 3 레벨로 운영이된다.
  • AArch64에서는 3 레벨 또는 4 레벨로 운영된다.
    • 0.b.c.d (3 레벨 운영)
    • a.b.c.d (4 레벨 운영)

 

Affinity 라우팅 for SPI

각 SPI에 대한 Affinity 라우팅 설정은GICD_IROUTERn을 사용하여 기록한다.

  • 리눅스 커널의 GIC v3에서 모든 SPI 인터럽트들은 디폴트로 boot cpu에 라우팅되도록 boot cpu의 AFFx 값들을 읽어 이 레지스터에 기록하여 사용한다.
  • 예) 어떤 SPI 인터럽트를 1번 cpu의 2번 core로 라우팅하려면 aff3=0, aff2=0, aff1=1, aff0=2로 설정한다.

 

Affinity 라우팅 for SGI

각 SGI에 대한 인터럽트를 만들어 발생할 때 Affinity 라우팅은 다음과 같이 동작한다.

  • 관련 레지스터는 다음과 같다.
    • ICC_SGI0R_EL1
    • ICC_SGI1R_EL1
    • ICC_ASGI1R_EL1. 설정은 GICD_IROUTERn을 사용하여 기록한다.
  • SGI에서는 1개의 Aff3~Aff1까지를 지정하고 Aff0의 경우 16비트로 구성된 TargetList를 사용하여 최대 16개 까지 cpu들에 복수 라우팅이 가능하다.
  • 예) 어떤 SGI 인터럽트를 1번 cpu의 0~3번 core에 동시에 라우팅하려면 aff3=0, aff2=0, aff1=1, TargetList=0x000f로 설정한다.

 

Affinity 라우팅 Legacy 지원 옵션

GIC v3가 legacy (GIC v2로 작성된 드라이버 소프트웨어)를 지원하기 위해 GICD_CTRL 레지스터의 Affinity Routing Enable (ARE) 비트를 사용하여 결정한다.

  • 0: affinity 라우팅 비활성화 (legacy operation).
  • 1: affinity 라우팅 활성화

 

다음 그림은 two security states를 사용하면서 각 states에서 legacy 지원 여부를 각각 결정하는 모습을 보여준다.

 

Redistributor

Redistributor는 다음과 같이 프로그래밍 인터페이스를 제공한다.

  • SGI 및 PPI 활성화 또는 비활성화
  • SGI 및 PPI의 우선 순위 설정
  • 각 PPI를 레벨 감지 또는 에지 트리거로 설정
  • 각 SGI 및 PPI를 인터럽트 그룹에 할당
  • SGI 및 PPI의 보류(Pending) 상태 제어
  • SGI 및 PPI의 활성(Active) 상태 제어
  • 연결된 PE에 대한 전원 관리 지원
  • LPI가 지원되는 경우 관련 인터럽트 속성 및 보류 상태를 지원하는 메모리의 데이터 구조에 대한 기본 주소 제어
  • GICv4가 지원되는 경우 관련 가상 인터럽트 속성 및 보류 상태를 지원하는 메모리의 데이터 구조에 대한 기본 주소 제어

 

 

GIC v3 레지스터들

GIC v3에는 기존 GIC v2에는 없었던 3 가지 시스템 레지스터 그룹(ICC, ICV, ICH)들과 2 개의 메모리 맵드 레지스터 그룹(GICR, GITS)들이 추가되었다. GIC v3의 AArch64 시스템용 레지스터들은 다음과 같은 그룹으로 나눌 수 있다. (AArch32는 생략)

  • 시스템 레지스터들
    • ICC, Physical GIC CPU interface Systerm Registers (GICC를 대체)
    • ICV, Virtual GIC CPU interface Systerm Registers (GICV를 대체)
    • ICH, Virtual Interface Control System Registers (GICH를 대체)
  • 메모리 맵드 레지스터들
    • GICC, CPU Interface Registers
    • GICD, Distributor Registers
    • GICH, Virtual CPU Interface Control Registers
    • GICR, Redistributor Registers
    • GICV, Virtual CPU Interface Registers
    • GITS, ITS Registers

 

시스템 레지스터들과 메모리 맵드 레지스터간의 관계

GIC 아키텍처는 동일한 레지스터를 메모리 맵드 레지스터와 동등한 시스템 레지스터간에 공유 할 수 있도록 허용하지만 필수는 아니다. 다음과 같은 조건으로 하나를 선택하여 사용한다.

  • a) Legacy 미지원
    • 리눅스 커널의 GIC v3 드라이버가 사용한다. 이 경우 GIC v2와 호환하지 않는다.
    • ICC_SRE_ELx.SRE==1
    • 시스템 레지스터를 사용하여 메모리 맵드 레지스터보다 빠르다.
  • b) Legacy 지원
    • GIC v2와 호환 코드를 사용하기 위해 메모리 맵드 레지스터를 사용한다.
    • ICC_SRE_ELx.SRE==0
    • 메모리 맵드 레지스터에 액세스 한 경우 시스템 레지스터가 수정 될 수 있음을 의미한다

 

다음 그림은 레지스터 인터페이스의 Legacy의 호환 여부에 따른 비교를 보여준다.

 

1-1) ICC, Physical GIC CPU interface System Registers

  • ICC_CTLR_EL1 (Interrupt Controller Control Register EL1)
  • ICC_CTLR_EL3 (Interrupt Controller Control Register EL3)
    • CBPR (Common Binary Point Register)
      • 0=preemption 그룹으로 group 0만 사용
      • 1=preemption 그룹으로 group 0과 group 1 모두 사용
    • EOImode (End Of Interrupt Mode)
      • 0=ICC_EOIR_EL1 & ICC_AEOIR_EL1을 사용하여 priority drop 기능과 비활성화 기능 지원
      • 1=ICC_EOIR_EL1 & ICC_AEOIR_EL1을 사용하여 priority drop 기능만 지원, 인터럽트 비활성화 기능은 별도로 ICC_DIR_EL1 사용
    • PMHE (Priority Mask Hint Enable)
      • ICC_PMR_EL1을 힌트로 사용하는 것의 유무
      • 0=disable, 1=enable
    • PRIbists (Priority bits)
      • 몇 개의 priority 비트를 지원하는지를 알아온다. (Read Only)
        • 2개의 security state를 지원하는 시스템의 경우 최소 32단계(5 priority bits) 이상이어야 한다.
        • 1개의 security state를 지원하는 시스템의 경우 최소 16단계(4 priority bits) 이상이어야 한다.
    • IDbits (Identifier bits)
      • 인터럽트 id로 몇 비트를 사용하는지 알아온다. (Read Only)
        • 000=16bits, 001=24bits
    • SEIS (SEI Support)
      • CPU 인터페이스가 local SEI들의 generation을 지원하는지 여부 (Read Only)
        • 0=not support, 1=support
    • A3V (Affinity 3 Valid)
      • CPU 인터페이스가 SGI generation시 affinity 3의 사용을 지원하는지 여부(Read Only)
        • 0=zero values, 1=non-zero values
    • RSS (Range Select Support)
      • SGI 사용 시 Targeted Affinity 레벨 0 값으로 허용하는 범위
        • 0=0~15까지 허용한다. (0~15 cpu)
        • 1=0~255까지 허용한다. (0~255 cpu)
    • ExtRange
      • 확장 SPI 지원 여부
        • 0=INTID 1024 to 8191까지 사용하지 않는다.
        • 1=INTID 1024 to 8191까지 사용 가능하다.

 

  • ICC_PMR_EL1 (Interrupt Controller Interrupt Priority Mask Register EL1)
    • Priority Mask
      • priority mask 값으로 이 값보다 작은 값(수치가 작을 수록 가장 높은 우선 순위이다)의 priority를 가진 인터럽트를 PE(Processing Element)에 전달하고, 그 외의 인터럽트는 block 한다.
      • 참고로 BIT(8 – IIC_CTRL.pribits) 값을 이 필드에 저장하고 다시 읽었을 때 0인 값이 읽히면 group 0를 사용하지 못하는 상태를 알 수 있다.
      • Pesudo-NMI 구현 시 이 값으로 GIC_PRIO_IRQON(0xe0)과 GIC_PRIO_IRQOFF(0x60)을 기록하여 사용한다.
      • 0 값을 기록하는 경우 모든 인터럽트가 전달되지 않는다.
  • ICC_BPR0_EL1 (Interrupt Controller Interrupt Binary Point Register 0 EL1)
  • ICC_BPR1_EL1 (Interrupt Controller Interrupt Binary Point Register 1 EL1)
    • Binary Point
      • priority 값을 group priority(g) 필드와 subpriority(s) 필드로  나누는 기준 값을 설정한다.  group priority 필드는 인터럽트 preemption을 의미한다.
        • 0=ggggggg.s
        • 1=gggggg.ss
        • 2=ggggg.sss
        • 3=gggg.ssss
        • 4=ggg.sssss
        • 5=gg.ssssss
        • 6=g.sssssss
        • 7=ssssssss (no preemption)
      • 이 필드에 0 값을 기록하여 리셋시킬 수 있다. 리셋된 후의 값은 구현에 따라 다르다.
    • security 사용 여부에 따라 다음과 같이 처리한다.
      • Security Extension을 사용하지 않는 경우 다음과 같이 처리한다.
        • group 0 인터럽트 또는 GICC_CTLR.CBPR==1인 경우 GICC_BPR을 사용한다.
        • group 1 인터럽트 또는 GICC_CTLR.CBPR==0인 경우 GICC_ABPR을 사용한다.
      • Security Extension을 사용하는 경우 secure/non-secure별 뱅크된 GICC_BPR을 사용한다.
  • ICC_IAR0_EL1 (Interrupt Controller Interrupt Acknowledge Register 0 EL1)
  • ICC_IAR1_EL1 (Interrupt Controller Interrupt Acknowledge Register 1 EL1)
    • 인터럽트 수신 시 이 레지스터를 읽어 인터럽트 id를 읽어오는 것으로 인터럽트 처리 루틴이 ack 처리되었음을 gic에 알린다. (읽기 전용)
  • ICC_EOIR0_EL1 (Interrupt Controller Interrupt End Of Interrupt Register 0 EL1)
  • ICC_EOIR1_EL1 (Interrupt Controller Interrupt End Of Interrupt Register 1 EL1)
    • ISR(Interrupt Service Routine)에서 인터럽트 처리 완료 시 해당 인터럽트 id를 기록하여 eoi 처리하였음을 gic에 알린다. (쓰기 전용)
  • ICC_RPR_EL1 (Interrupt Controller Interrupt Running Priority Register EL1)
    • 읽기 전용으로 현재 동작하고 있는 인터럽트의 priority 값을 알아온다.
    • idle 상태인 경우는 0xff
  • ICC_HPPIR0_EL1 (Interrupt Controller Interrupt Highest Priority Pending Interrupt Register 0 EL1)
  • ICC_HPPIR1_EL1 (Interrupt Controller Interrupt Highest Priority Pending Interrupt Register 1 EL1)
    • 현재 하드웨어 인터럽트들 중 pending되고 있는 가장 높은 priority irq 번호를 읽어온다. (읽기 전용)
  • ICC_SRE_EL1 (Interrupt Controller Interrupt System Register Enable Register EL1)
  • ICC_SRE_EL2 (Interrupt Controller Interrupt System Register Enable Register EL2)
  • ICC_SRE_EL3 (Interrupt Controller Interrupt System Register Enable Register EL3)
    • SRE (System Register Enable)
      • 0=반드시 메모리 맵드 인터페이스가 사용되야 한다.
      • 1=현재 security state의 시스템 레지스터 인터페이스를 enable 한다.
        • 리눅스 커널의 GIC v3 드라이버는 시스템 레지스터를 사용한다.
    • DFB (Disable FIQ Bypass)
      • 0=FIQ bypass enable
      • 1=FIQ bypass disable
    • DIB (Disable IRQ Bypass)
      • 0=IRQ bypass enable
      • 1=IRQ bypass disable
  • ICC_DIR_EL1 (Interrupt Controller Interrupt Deactivate Interrupt Register EL1)
    • INTID에 해당하는 인터럽트를 비활성화시킨다. (쓰기 전용)
  • ICC_IGRPEN0_EL1 (Interrupt Controller Interrupt Group 0 Enable Register EL1)
  • ICC_IGRPEN1_EL1 (Interrupt Controller Interrupt Group 1 Enable Register EL1)
  • ICC_IGRPEN1_EL3 (Interrupt Controller Interrupt Group 1 Enable Register EL3)
    • Group 0(1) 인터럽트의 활성화를 결정한다.
      • 0=disable
      • 1=enable
  • ICC_AP0Rn_EL1 (Interrupt Controller Interrupt Active Priorities Group 0 Registers EL1)
  • ICC_AP1Rn_EL1 (Interrupt Controller Interrupt Active Priorities Group 1 Registers EL1)
    • group 0(1)의 active priorities 정보를 제공한다. (칩 구현에 따라 다르다.)

 

  • ICC_SGI0R_EL1 (Interrupt Contorller Software Generated Interrupt Group 0 Register EL1)
  • ICC_SGI1R_EL1 (Interrupt Contorller Software Generated Interrupt Group 1 Register EL1)
  • ICC_ASGI1R_EL1 (Interrupt Contorller Alias Software Generated Interrupt Group 1 Register EL1)
    • SGI를 발생시킨다. (쓰기 전용)
    • 리눅스 커널에서는 IPI(Inter Process Interrupt)라고 불린다.
    • TargetList
      • SGI를 발생시킬 aff0 레벨의 cpu들(최대 16개)
    • INTID
      • 발생시킬 SGI 번호
    • Aff1~3
      • SGI 인터럽트가 전달될 cpu affinity를 지정한다.

 

1-2) ICV, Virtual GIC CPU interface Systerm Registers

  • ICV_AP0Rn_EL1 (Interrupt Controller Virtual Active Priorities Group 0 Registers EL1)
  • ICV_AP1Rn_EL1 (Interrupt Controller Virtual Active Priorities Group 1 Registers EL1)
  • ICV_BPR0_EL1 (Interrupt Controller Virtual Binary Point Register 0 EL1)
  • ICV_BPR1_EL1 (Interrupt Controller Virtual Binary Point Register 1 EL1)
  • ICV_CTLR_EL1 (Interrupt Controller Virtual Control Register EL1)
  • ICV_DIR_EL1 (Interrupt Controller Deactivate Virtual Interrupt Register EL1)
  • ICV_EOIR0_EL1 (Interrupt Controller Virtual End Of Interrupt Register 0 EL1)
  • ICV_EOIR1_EL1 (Interrupt Controller Virtual End Of Interrupt Register 1 EL1)
  • ICV_HPPIR0_EL1 (Interrupt Controller Virtual Highest Priority Pending Interrupt Register 0 EL1)
  • ICV_HPPIR1_EL1 (Interrupt Controller Virtual Highest Priority Pending Interrupt Register 1 EL1)
  • ICV_IAR0_EL1 (Interrupt Controller Virtual Interrupt Acknowledge Register 0 EL1)
  • ICV_IAR1_EL1 (Interrupt Controller Virtual Interrupt Acknowledge Register 1 EL1)
  • ICV_IGRPEN0_EL1 (Interrupt Controller Virtual Interrupt Group 0 Enable register EL1)
  • ICV_IGRPEN1_EL1 (Interrupt Controller Virtual Interrupt Group 1 Enable register EL1)
  • ICV_PMR_EL1 (Interrupt Controller Virtual Interrupt Priority Mask Register EL1)
  • ICV_RPR_EL1 (Interrupt Controller Virtual Running Priority Register EL1)

 

1-3) ICH, Virtualization Control System Registers

  • ICH_AP0Rn_EL2 (Interrupt Controller Hyp Active Priorities Group 0 Registers EL2)
  • ICH_AP1Rn_EL2 (Interrupt Controller Hyp Active Priorities Group 1 Registers EL2)
  • ICH_HCR_EL2 (Interrupt Controller Hyp Control Register EL2)
  • ICH_VTR_EL2 (Interrupt Controller VGIC Type Register EL2)
  • ICH_MISR_EL2 (Interrupt Controller Maintenance Interrupt State Register EL2)
  • ICH_EISR_EL2 (Interrupt Controller End of Interrupt Status Register EL2)
  • ICH_ELRSR_EL2 (Interrupt Controller Empty List Register Status Register EL2)
  • ICH_VMCR_EL2 (Interrupt Controller Virtual Machine Control Register EL2)
  • ICH_LRn_EL2 (Interrupt Controller List Registers EL2)

 

2-1) GICC, CPU interface Registers

  • GICC_CTRL (CPU Interface Control Register)
    • GIC v2와 다르게 3가지 모드에서 사용방법이 다르고, 더 많은 제어 비트를 가진다.
      • 1) GICD_CTRL.DS=0 & Non-Secure
        • 리눅스 커널에서 GIC v3  드라이버가 이 모드를 사용하여 설정한다.
      • 2) GICD_CTRL.DS=0 & Secure
        • 시큐어 OS가 사용하는 모드이다.
      • 3) GICD_CTRL.DS=1
    • EnableGrp0~1
      • Group 0와 Group 1에 대한 인터럽트 시그널의 enable 설정이다. (0=disable, 1=enable)
    • FIQEn
      • group 0 인터럽트의 FIQ 시그널 처리 여부
        • 0=disable(IRQ로 처리), 1=enable(FIQ로 처리)
    • CBPR
      • 인터럽트 preemption 지원 방법
        • 0=GICC_BPR을 사용하여 group 0 인터럽트들의 preemption을 지원하고, group 1 인터럽트는 GICC_ABPR 레지스터를 사용하여 지원
        • 1=GICC_BPR을 사용하여 group 0 및 group 1 인터럽트들의 preemption을 지원한다.
    • FIQBypDisGrp0~1 & IRQBypDisGrp0~1
      • cpu 인터페이스 비활성화시 PE에 인터럽트 바이패스 Disable 여부 (0=bypass, 1=bypass disable)
    • EOImode (EOImodeS & EOImodeNS)
      • 0=GICC_EOIR & GICC_AEOIR을 사용하여 priority drop 기능과 비활성화 기능 지원
      • 1=GICC_EOIR & GICC_AEOIR을 사용하여 priority drop 기능만 지원, 인터럽트 비활성화 기능은 별도로 GICC_DIR 사용

 

  • GICC_PMR (Interrupt Priority Mask Register)
    • core로 전달될 irq들에 대해 설정한 priority 마스크 값보다 작은 priority 값(값이 작을 수록 우선 순위가 높다)을 cpu로 전달한다.
      • 리눅스 커널의 GIC v1~v4 드라이버는 0xf0을 설정하여 사용한다.
    • 0 값을 기록하는 경우 모든 인터럽트가 전달되지 않는다.
  • GICC_BPR (Binary Point Register)
    • priority 값을 group/sub와 같이 두 그룹으로 나누어 그 중 group에 해당하는 priority를 가진 인터럽트에 대해 preemption interrupt로 분류한다.
      • 0=ggggggg.s
      • 1=gggggg.ss
      • 2=ggggg.sss
      • 3=gggg.ssss
      • 4=ggg.sssss
      • 5=gg.ssssss
      • 6=g.sssssss
      • 7=ssssssss (no preemption)
    • 리눅스 커널의 경우 GIC v3 부터 이 필드에 0 값을 기록하여 리셋시킨 후 사용한다. 리셋 후의 값은 칩 구현에 따라 다르다.
    • two security state를 사용하는 경우 secure/non-secure별 뱅크된 GICC_BPR을 사용한다.
    • single security state를 사용하는 경우 group 0만 인터럽트 preemption을 지원한다.
  • 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_HPPIR (Highest Priority Pending Interrupt Register)
    • 읽기 전용으로 현재 하드웨어 인터럽트들 중 pending되고 있는 가장 높은 priority irq 번호를 읽어온다.
  • GICC_IIDR (CPU Interface Identification Register)
    • 읽기 전용으로 GIC 제품 ID, 아키텍처 버전, 리비전, 구현회사 등의 정보를 읽어올 수 있다.
  • GICC_ABPR (Aliased Binary Point Register)
  • GICC_APRn (Active Priorities Register)
  • GICC_NSAPRn (Non-Secure Active Priorities Register)
    • active priorities 정보를 제공한다. (칩 구현에 따라 다르다)
  • GICC_AEOIR (Aliased End of Interrupt Register)
  • GICC_AIAR (Aliased Interrupt Acknowledge Register)
  • GICC_AHPPIR (Aliased Highest Priority Pending Interrupt Register)
  • GICC_DIR (Deactivate Interrupt Register)
    • INTID에 해당하는 인터럽트를 비활성화시킨다. (쓰기 전용)

 

2-2) GICD, Distributor Registers

  • GICD_CTRL (Distributor Control Register)
    • EnableGrp0~1
      • Group 0(1) 인터럽트의 활성화 여부 (EnableGrp1NS=non-secure에서, EnableGrp1S=secure에서)
        • 0=disable, 1=enable
      • Pending 인터럽트에 대해 CPU interface로 올려보낼지 여부를 통제한다.
        • secure extension을 사용할 때 non-secure 모드에서는 group1에 대해서만 통제할 수 있다.
        • security extension 사용 시 bank 된다.
    • ARE (Affinity Routing Enable)
      • affinitiny routing의 활성화 여부 (ARE_NS=non-secure에서, ARE_S=secure에서)
        • 0=disable, 1=enable
    • DS (Disable Security)
      • non-secure 에서 group 0 인터럽트들에 대한 레지스터들에 access를 허용 여부
        • 0=access 금지, 1=access 허용
    • E1NWF (Enable 1 of N Wakeup Functionality)
      • N 개 중 하나의 인터럽트를 선택하여 PE를 깨어나게 하는 기능
        • 0=disable, 1=enabel
    • RWP (Register Write Pending)
      • 이 레지스터의 기록이 진행중인지 여부. 진행 중에는 기록하지 못한다. (Read Only)
        • 0=Write 가능
        • 1=Write 진행중

 

  • GICD_TYPER (Interrupt Controller Type Register)
    • 읽기 전용으로 Security Extension 사용 시 최대 지원 cpu 수와 인터럽트 수 등을 알아내기 위해 사용한다.
    • ESPI_range
      • GICD_TYPER.ESPI==1로 설정된 경우 최대 확장 SPI INTID 수로 다음 식과 같다.
        • = (32 * (ESPI_range + 1) + 4095)
    • RSS (Range Select Support)
      • affinity level 0에서 IRI 지원 SGI 대상 범위 선택
        • 0=0~15
        • 1=0~255
    • No1N (No support 1 Of N)
      • 1 of N SPI 인트럽트 비지원 여부
        • 0=supported, 1=not supported
    • A3V
      • Affinity 3 valid 여부
        • 0=not support (all zero values), 1=support
    • IDBits
      • 지원하는 인터럽트 수에 대한 비트 표현-1
      • 수식 = 2^(N+1)
        • 예) 0b01001 -> 1024 인터럽트
    • DVIS (Direct Virtual LPI Injection Support)
      • Direct Virtual LPI 인젝션의 지원 여부
        • 0=not support, 1=support
    • LPIS (LPI Support)
      • LPI 지원 여부
        • 0=not support, 1=support
    • MBIS (Message Based Interrupts Support)
      • 메시지 기반 인터럽트의 지원 여부
        • 0=not support, 1=support
    • num_LPIs
      • 지원 가능한 LPI 수 (GICD_TYPER.IDbits를 초과할 수 없다)
        • =2^(num_LPIs+1) .
          • LPI용 INITID 범위: 8192 ~ (8192 + 2^(num_LPIs+1) – 1)
        • 0인 경우 GICD_TYPER.IDbits 로 산출된 수와 동일
        • ◦ This field cannot indicate a maximum LPI INTID greater than that indicated byWhen the supported INTID width is less than 14 bits, this field is RES0 and no LPIs are supported.
    • SecurityExtn
      • Two Security 지원 여부
        • 0=not support (single security states), 1=support (two security states)
    • ESPI (Extended SPI range)
      • Extended SPI range 지원 여부
        • 0=미지원 1=지원
    • CPU Number
      • Affinity Routing 사용 시 최대 지원 CPU 수에 대한 비트 표현-1(0=ARE 미지원)
        • 수식 = 2^(N+1)
        • 예) 0b001 -> 4개 cpu
    • ITLinesNumber
      • 지원 SPI 인터럽트 수(최대 수=1020)
      • 수식 = 32 * (N+1)
        • 예) 0b00011 -> 32 * (3 + 1) = 128
        • 예) 0b11111 -> 32 * (31 + 1) = 1024이나 최대 수인 1020개 제한
  • GICD_IIDR (Distributor Implementer Identification Register)
    • 읽기 전용으로 GIC implementor 정보를 알아온다.

 

  • GICD_IGROUPRn (Interrupt Group Registers)
    • secure extension을 사용하는 경우 인터럽트별로 secure group 0와 non-secure group 1을 지정한다.
      • 0=secure group 0, 1=non-secure group 1
    • secure extension을 사용하는 경우 리셋시 0으로(RAS) 읽히며, 모든 인터럽트가 재설정되기 전엔 모두 secure group 0 상태가된다.
      • SMP 시스템에서 secure extension을 지원하고, non-secure 프로세서들에 대해서는 뱅크된 GICD_IGROUPR0은 non-secure group 1 상태로 리셋한다.
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
    • 최종 secure 상태를 결정하기 위해 GICD_IGRPMODRn 과 같이 사용된다.
    • two security states를 사용할 때에는 반드시 secure에서 설정을 해야 유효하다. non-secure에서 동작하는 리눅스 커널에서의 설정은 의미가 없다.
    • 리눅스 커널의 GIC v3 드라이버에서는 single security state를 위해 모든 SPI 인터럽트들을 G1NS(non-secure group 1)으로 설정한다.
  • GICD_IGRPMODRn (Interrupt Group Modifier Registers)
    • two security 모드로 동작 시 GICD_IGROUPRn와 같이 사용하여 최종 그룹과 secure 상태를 결정한다.
      • 0=secure group 0 -> G0S(secure group 0), non-secure group 1 -> G1NS(non-secure group 1)
      • 1=secure group 0 -> G1S(secure group 1), non-secure group 1 -> G1NS(non-secure group 1, reserved)
    • two security states를 사용할 때에는 반드시 secure에서 설정을 해야 유효하다. non-secure에서 동작하는 리눅스 커널에서의 설정은 의미가 없다.
  • GICD_ISENABLERn (Interrupt Set-Enable Registers)
    • IRQ 별로 통과(Set-Enable) 여부를 설정할 수 있다.
      • 1=set 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICENABLERn (Interrupt Clear-Enable Registers)
    • IRQ 별로 통제(Clear-Enable) 여부를 설정할 수 있다.
      • 1=clear 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ISPENDRn (Interrupt Set PENDing Registers)
    • IRQ 별로 pending 상태를 설정할 수 있다.
      • 1=set 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ICPENDRn (Interrupt Clear PENDing Registers)
    • IRQ 별로 pending 상태 해제를 요청할 수 있다.
      • 1=clear 요청
    • IRQ #0~31을 위해 첫 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_IPRIORITYRn (Interrupt PRIORITY Registers)
    • IRQ 별로 priority를 설정할 수 있다.
      • 0~255 priority 설정
    • IRQ #0~31을 위해 첫 8개의 레지스트가 cpu별로 bank 되어 사용된다.
    • 리눅스 커널의 GIC v3 드라이버는 디폴트 값으로 0xa0 값을 사용한다.  단 최근 커널에서 pesudo-nmi를 지원하는 경우 nmi 모드를 위해서는 0x20 값을 사용한다.
      • non-secure에서 설정한 값이므로 우선순위 값은 다음과 같이 변환되어 사용된다.
        • (0xa0 >> 1 | 0x80) = 0xd0
        • (0x20 >> 1 | 0x80) = 0x90
  • GICD_ITARGETRn (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_ICFGRn (Interrupt ConFiGuration Registers)
    • IRQ 별로 인터럽트 트리거 방향을 설정할 수 있다.
      • 0=상향 시 트리거, 1=하향 시 트리거
    • IRQ #160~31을 위해 두 번째 레지스트가 cpu별로 bank 되어 사용된다.
  • GICD_ISACTIVERn (Interrupt Set-Active Registers)
    • IRQ 별로 활성화 요청한다.
      • 1=활성화 요청
  • GICD_ICACTIVERn (Interrupt Clear-Active Registers)
    • IRQ 별로 비활성화 요청한다.
      • 1=비활성화 요청
  • GICD_NSACRn (Non-secure Access Control Registers)
    • non-secure 소프트웨어만 설정할 수 있다.
    • 특정 PE의 non-secure 그룹 0 인터럽트에 대한 제어 활성화 여부
      • 00=not permit
      • 01=set-pending 허용, secure SGI 생성 허용, clear-pending 허용 가능(구현에 따라)
      • 10=01에 추가하여 clear-pending 허용, set-active 및 clear-active 허용
      • 11=10에 추가하여 target cpu 지정 및 affinity 라우팅 정보 사용 가능

 

  • 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
  • GICD_CPENDSGIRn (SGI Clear-Pending Registers)
    • Pending SGI 인터럽트 상태 클리어 (1=clear 요청)
  • GICD_SPENDSGIRn (SGI Set-Pending Registers)
    • Pending SGI 인터럽트 상태 설정 (1=set 요청)
  • GICD_IROUTERn (Interrupt Routing Registers)
    • affinity 라우팅(ARE)이 활성화된 경우 SPI에 대한 라우팅 정보를 제공한다.
    • Aff0~3
      • affinity 레벨별 cpu 마스크 정보
        • 리눅스 커널의 GIC v3 드라이버는 boot cpu의 aff3.aff2.aff1.aff0 정보를 기록하여 boot cpu로 라우팅하도록 디폴트로 설정하여 운영한다.
    • IRM (Interrupt Routing Mode)
      • 0=인터럽트가 aff3,aff2.aff1.aff0으로 라우팅 (정보를 사용하여 Affinity Routing)
        • 리눅스 커널의 GIC v3 드라이버가 이 모드를 디폴트 설정하여 운영한다.
      • 1=인터럽트가 협력하는 노드의 모든 PE로 라우팅 (정보를 사용하지 않고 노드내 모든 PR로 Affinity Routing)

 

2-3) GICH, Virtual CPU Interface Control Registers

  • GICH_APR (Active Priorities Register)
  • GICH_EISRn (End of Interrupt Status Registers)
  • GICH_ELRSRn (Empty List Register Status Registers)
  • GICH_HCR (Hypervisor Control Register)
  • GICH_LRn (List Registers)
  • GICH_MISR (Maintenance Interrupt Status Register)
  • GICH_VMCR (Virtual Machine Control Register)
  • GICH_VTR (VGIC Type Register)

 

2-4) GICR, Redistributor Registers

  • GICR_CLRLPIR (Clear LPI Pending Register)
  • GICR_CTLR (Redistributor Control Register)
  • GICR_ICACTIVER0 (Interrupt Clear-Active Register 0)
  • GICR_ICENABLER0 (Interrupt Clear-Enable Register 0)
  • GICR_ICFGR0 (Interrupt Configuration Register 0)
  • GICR_ICFGR1 (Interrupt Configuration Register 1)
  • GICR_ICPENDR0 (Interrupt Clear-Pending Register 0)
  • GICR_IGROUPR0 (Interrupt Group Register 0)
  • GICR_IGRPMODR0 (Interrupt Group Modifier Register 0)
  • GICR_IIDR (Redistributor Implementer Identification Register)
  • GICR_INVALLR (Redistributor Invalidate All Register)
  • GICR_INVLPIR (Redistributor Invalidate LPI Register)
  • GICR_IPRIORITYRn (Interrupt Priority Registers)
  • GICR_ISACTIVER0 (Interrupt Set-Active Register 0)
  • GICR_ISENABLER0 (Interrupt Set-Enable Register 0)
  • GICR_ISPENDR0 (Interrupt Set-Pending Register 0)
  • GICR_NSACR (Non-secure Access Control Register)
  • GICR_PENDBASER (Redistributor LPI Pending Table Base Address Register)
  • GICR_PROPBASER (Redistributor Properties Base Address Register)
  • GICR_SETLPIR (Set LPI Pending Register)
  • GICR_STATUSR (Error Reporting Status Register)
  • GICR_SYNCR (Redistributor Synchronize Register)
  • GICR_TYPER (Redistributor Type Register)
  • GICR_VPENDBASER (Virtual Redistributor LPI Pending Table Base Address Register)
  • GICR_VPROPBASER (Virtual Redistributor Properties Base Address Register)
  • GICR_WAKER (Redistributor Wake Register)

 

2-5) GICV, Virtual CPU Interface Registers

  • GICV_ABPR (VM Aliased Binary Point Register)
  • GICV_AEOIR (VM Aliased End of Interrupt Register)
  • GICV_AHPPIR (VM Aliased Highest Priority Pending Interrupt Register)
  • GICV_AIAR (VM Aliased Interrupt Acknowledge Register)
  • GICV_APRn (VM Active Priorities Registers)
  • GICV_BPR (VM Binary Point Register)
  • GICV_CTLR (Virtual Machine Control Register)
  • GICV_EOIR (VM End of Interrupt Register)
  • GICV_HPPIR (VM Highest Priority Pending Interrupt Register)
  • GICV_IAR (VM Interrupt Acknowledge Register)
  • GICV_PMR (VM Priority Mask Register)
  • GICV_RPR (VM Running Priority Register)
  • GICV_IIDR (VM CPU Interface Identification Register)
  • GICV_DIR (VM Deactivate Interrupt Register)

 

2-6) GITS, ITS Registers

  • GITS_BASERn (ITS Translation Table Descriptors)
  • GITS_CBASER (ITS Command Queue Descriptor)
  • GITS_CREADR (ITS Read Register)
  • GITS_CTLR (ITS Control Register)
  • GITS_CWRITER (ITS Write Register)
  • GITS_IIDR (ITS Identification Register)
  • GITS_TRANSLATER (ITS Translation Register)
  • GITS_TYPER (ITS Type Register)

 


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

 


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

 

참고

 

 

Exception -6- (MM Fault Handler)

 

메모리 Fault 핸들러

 

유저 태스크에서 sys_mmap()이나 sys_brk() 등을 통해 커널에 메모리를 요청하면 커널은 곧바로 해당 태스크에 메모리를 할당하여 매핑하지 않는다. 커널은 요청을 받으면 요청한 유저 태스크의 rb 트리(로 관리하는 vma 정보에 메모리 range 및 플래그 등의 정보만을 기록(추가 또는 변경)한다. 그런 후 유저 태스크에서 실제 해당 페이지에 접근하려할 때 fault가 발생하도록 유도한다. fault 핸들러는 이러한 요구에 대한 처리를 수행한다. 요청한 페이지가 처음 0으로 초기화된 메모리를 요청하는 경우 zero 페이지에 매핑을 하거나, 다른 태스크에서 사용중인 페이지로 매핑하여 페이지를 공유하게 하는 방법을 사용한다. 이렇게 처리하게 되면 다음 몇 가지의 장점을 얻게된다.

  • 유저 태스크가 요청한 메모리를 할당 받자마자 모두 다 사용하는 것이 아니므로 필요한 메모리에 접근할 때에만 물리 메모리를 할당하면 실제 메모리의 사용을 절약할 수 있다.
    • anon 매핑: 파일과 관계 없는 유저 태스크가 요구하는 heap, stack  메모리 요청
    • file 매핑: 유저가 파일을 가상 주소 공간에 매핑(memory-mapped 파일) 요청 – mmap 메모리 요청
  • 유저 태스크가 요청한 메모리에 대한 처리를 커널이 빠르게 수행할 수 있다. – COW(Copy On Write) 방법
    • 부모 태스크가 child 태스크의 생성을 위해 fork 또는 clone등을 수행할 때 child 태스크가 사용할 메모리 할당 요청에 대해 즉각적으로 물리 페이지를 할당하지 않고 부모 태스크가 사용하던 메모리 페이지 테이블을 복사하여 사용한다. 이렇게 하여 빠른 태스크의 생성이 가능해진다.
  • h/w 아키텍처가 실제 메모리의 접근에 대한 모니터링을 지원하지 못하는 경우에도 이러한 fault 처리를 통해 리눅스가 알아낼 수 있도록 표식을 남길 수 있다. (young 플래그)

 

MMU가 TLB를 통해 Table walk를 수행하다 매핑되지 않은 페이지이거나  페이지 fault 핸들러인 do_page_fault() -> __do_page_fault() 함수를 통해 handle_mm_fault() 함수가 호출되었다. 다음 그림에서 그 이후 함수 호출 흐름을 보여준다.

 

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().
 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
                unsigned int flags)
{
        vm_fault_t ret;

        __set_current_state(TASK_RUNNING);

        count_vm_event(PGFAULT);
        count_memcg_event_mm(vma->vm_mm, PGFAULT);

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

        if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
                                            flags & FAULT_FLAG_INSTRUCTION,
                                            flags & FAULT_FLAG_REMOTE))
                return VM_FAULT_SIGSEGV;

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

        if (unlikely(is_vm_hugetlb_page(vma)))
                ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
        else
                ret = __handle_mm_fault(vma, address, flags);

        if (flags & FAULT_FLAG_USER) {
                mem_cgroup_exit_user_fault();
                /*
                 * 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가 발생하여 진입한 핸들러이다. vma 영역 상태에 따라 처리 방법이 다르다.

  • 코드 라인 6에서 현재 태스크를 TASK_RUNNING 상태로 설정한다.
  • 코드 라인 8~9에서 PGFAULT vm 카운터를 증가시기고, 현재 태스크가 memcg 통제를 받는 경우 이에 대해서도 증가시킨다.
  • 코드 라인 12에서 해당 태스크에서 TASK_RSS_EVENTS_THRESH(64)번 만큼 fault가 발생한 경우 per-cpu rss 관련 메모리 통계를 글로벌에 갱신한다.
  • 코드 라인 14~17에서 코드 영역에 기록을 수행하려 할 때 특정 아키텍처가 지원하지 않는 경우 VM_FAULT_SIGSEGV를 반환한다.
    • arch_vma_access_permitted() 함수는 x86, powerpc에 해당 코드를 지원하며 그 밖의 경우 항상 true를 반환한다.
  • 코드 라인 23~24에서 유저 영역에 대한 fault인 경우 memcg OOM 기능을 동작하도록 enable 한다.
  • 코드 라인 26~29에서 hugetlb를 사용하는 영역 여부에 따라 각각의 fault 함수를 호출한다.
  • 코드 라인 31~41에서 유저 영역에 대한 fault인 경우 memcg OOM 기능을 disable 한다. 만일 태스크가 memcg oom을 진행중이고 fault 핸들러 결과가 fault OOM이 아닌 경우 oom 동기화도 수행한다.

 

__handle_mm_fault()

mm/memory.c -1/2-

/*
 * 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 vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
                unsigned long address, unsigned int flags)
{
        struct vm_fault vmf = {
                .vma = vma,
                .address = address & PAGE_MASK,
                .flags = flags,
                .pgoff = linear_page_index(vma, address),
                .gfp_mask = __get_fault_gfp_mask(vma),
        };
        unsigned int dirty = flags & FAULT_FLAG_WRITE;
        struct mm_struct *mm = vma->vm_mm;
        pgd_t *pgd;
        p4d_t *p4d;
        vm_fault_t ret;

        pgd = pgd_offset(mm, address);
        p4d = p4d_alloc(mm, pgd, address);
        if (!p4d)
                return VM_FAULT_OOM;

        vmf.pud = pud_alloc(mm, p4d, address);
        if (!vmf.pud)
                return VM_FAULT_OOM;
        if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
                ret = create_huge_pud(&vmf);
                if (!(ret & VM_FAULT_FALLBACK))
                        return ret;
        } else {
                pud_t orig_pud = *vmf.pud;

                barrier();
                if (pud_trans_huge(orig_pud) || pud_devmap(orig_pud)) {

                        /* NUMA case for anonymous PUDs would go here */

                        if (dirty && !pud_write(orig_pud)) {
                                ret = wp_huge_pud(&vmf, orig_pud);
                                if (!(ret & VM_FAULT_FALLBACK))
                                        return ret;
                        } else {
                                huge_pud_set_accessed(&vmf, orig_pud);
                                return 0;
                        }
                }
        }

p4d, pud, pmd 및 pte 테이블이 필요한 경우 할당하여 연결(population)하고 루틴의 마지막에서 handle_pte_fault() 함수를 호출하여 pte 테이블에서의 fault 처리를 수행한다.

pud population
  • 코드 라인 4~10에서 fault 처리를 위해 여러 함수가 사용되는데 필요한 인자가 많아서 이를 쉽게 전달할 구조체 vm_fault에 담는다. 주요 정보로 다음들을 담는다.
    • .vma
      • fault 발생한 vma 영역
    • .address
      • fault 발생한 가상 주소 페이지의 시작 주소
    • .flags
    • .pgoff
      • vma 영역내에서 fault 발생한 가상 주소 페이지의 offset 페이지 번호
    • .gfp_mask
      • 파일 매핑된 경우 fs, io를 포함한 매핑에 사용한 gfp 플래그를 사용하고, 그 외의 경우 GFP_KERNEL에 해당하는 gfp 플래그를 사용한다.
  • 코드 라인 11에서 write 중에 fault가 발생했는지 여부를 dirty에 대입한다.
  • 코드 라인 17에서 fault 가상 주소에 해당하는 pgd 엔트리를 알아온다.
  • 코드 라인 18~20에서 pgd 엔트리가 빈 경우 연결을 위해 다음 레벨인 p4d 테이블을 할당한다.
  • 코드 라인 22~24에서 p4d 엔트리가 빈 경우 연결을 위해 다음 레벨인 pud 테이블을 할당한다.
  • 코드 라인 25~28에서 pud 엔트리가 비어있고, pud 단위의 블록 매핑을 사용할 조건을 만족하면 huge pud를 할당한다.
    • ARM64의 경우 pud 단위의 블럭 매핑은 1G에 해당한다.
  • 코드 라인 29~46에서 pud_trans_huge() 함수에선 pud 단위의 thp가 가능한 경우에 한해 다음과 같이 처리한다.
    • pud 엔트리가 읽기 전용 매핑되었고, write 시도하여 fault가 발생한 경우 파일 매핑된 파일 시스템이 (*huge_fault) 후크를 지원하는 경우 이를 실행한다. 그 외 anonymous 등은 아직 지원하지 않아 VM_FAULT_FALLBACK을 반환받아 오므로 계속 코드를 진행한다.
    • 그 외의 경우 pud 엔트리에 읽었음을 의미하는 young 플래그를 기록한다. 또한 write 시도였던 경우 dirty 플래그도 기록한다.

 

DAX(Direct Access)와 huge pud fault

DAX를 지원하는 파일 시스템이 (*huge_fault) 후크 함수를 지원하면, 이를 통해 이 영역에 접근 하여 fault가 발생하는 경우 pte 단위(4K) 보다 더 큰 pmd(2M) 단위 또는 pud 단위(ARM64에서 1G )로 크게 매핑하면 그 만큼 fault 발생 횟수를 줄이므로 성능을 향상시킨다.

  • 현재 리눅스에서 DAX를 지원하는 파일 시스템과 지원하는 fault 매핑 크기는 다음과 같다.
    • ext2의 경우 pte(4K) 단위의 fault 매핑을 지원한다.
    • ext4의 경우 pte(4K) 및 pmd(2M) 단위의 huge fault 매핑을 지원한다.
    • xfs의 경우 pte(4K), pmd(2M) 및 pud(1G) 단위의 huge fault 매핑을 지원한다.
  • MS 윈도우 ntfs의 경우에도 dax를 지원하나, 리눅스에서는 아직 지원하지 않는다.
  • 참고: Persistent Memory & DAX | 문c

 

mm/memory.c -2/2-

        vmf.pmd = pmd_alloc(mm, vmf.pud, address);
        if (!vmf.pmd)
                return VM_FAULT_OOM;
        if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
                ret = create_huge_pmd(&vmf);
                if (!(ret & VM_FAULT_FALLBACK))
                        return ret;
        } else {
                pmd_t orig_pmd = *vmf.pmd;

                barrier();
                if (unlikely(is_swap_pmd(orig_pmd))) {
                        VM_BUG_ON(thp_migration_supported() &&
                                          !is_pmd_migration_entry(orig_pmd));
                        if (is_pmd_migration_entry(orig_pmd))
                                pmd_migration_entry_wait(mm, vmf.pmd);
                        return 0;
                }
                if (pmd_trans_huge(orig_pmd) || pmd_devmap(orig_pmd)) {
                        if (pmd_protnone(orig_pmd) && vma_is_accessible(vma))
                                return do_huge_pmd_numa_page(&vmf, orig_pmd);

                        if (dirty && !pmd_write(orig_pmd)) {
                                ret = wp_huge_pmd(&vmf, orig_pmd);
                                if (!(ret & VM_FAULT_FALLBACK))
                                        return ret;
                        } else {
                                huge_pmd_set_accessed(&vmf, orig_pmd);
                                return 0;
                        }
                }
        }

        return handle_pte_fault(&vmf);
}
pmd population
  • 코드 라인 1~3에서 pud 엔트리가 빈 경우 연결을 위해 다음 레벨인 pmd 테이블을 할당한다.
  • 코드 라인 4~7에서 pmd 엔트리가 비어있고, pmd 단위의 블록 매핑을 사용할 조건을 만족하면 huge pmd를 할당한다.
    • ARM64의 경우 pmd 단위의 블럭 매핑은 2M에 해당한다.
  • 코드 라인 8~18에서 pmd 엔트리가 존재하는 경우이다. 낮은 확률로 pmd 엔트리가 swap 된 상태에서 fault가 발생한 경우 swap 영역으로 부터 로딩 중이다. 잠시 기다렸다 성공 값 0을 반환한다.
  • 코드 라인 19~32에서 pmd_trans_huge() 함수에선 pmd 단위의 thp가 가능하거나 pmd 단위의 디바이스 메모리의 맵이 가능한 경우에 한해 다음과 같이 처리한다.
    • vma 영역에 접근이 가능한 상태이면 do_huge_pmd_numa_page() 함수를 통해 huge pmd에 대한 처리를 수행한다.
    • pmd 엔트리가 읽기 전용 매핑되었고, write 시도하여 fault가 발생한 경우 파일 매핑된 파일 시스템이 (*huge_fault) 후크를 지원하는 경우 이를 실행한다. 그 외 anonymous 등은 아직 지원하지 않아 VM_FAULT_FALLBACK을 반환받아 오므로 계속 코드를 진행한다.
    • 그 외의 경우 pmd 엔트리에 읽었음을 의미하는 young 플래그를 기록한다. 또한 write 시도였던 경우 dirty 플래그도 기록한다.
pte fault 처리
  • 코드 라인 34에서  마지막 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).
 *
 * The mmap_sem may have been released depending on flags and our return value.
 * See filemap_fault() and __lock_page_or_retry().
 */
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
        pte_t entry;

        if (unlikely(pmd_none(*vmf->pmd))) {
                /*
                 * Leave __pte_alloc() until later: because vm_ops->fault may
                 * want to allocate huge page, and if we expose page table
                 * for an instant, it will be difficult to retract from
                 * concurrent faults and from rmap lookups.
                 */
                vmf->pte = NULL;
        } else {
                /* See comment in pte_alloc_one_map() */
                if (pmd_devmap_trans_unstable(vmf->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().
                 */
                vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
                vmf->orig_pte = *vmf->pte;

                /*
                 * some architectures can have larger ptes than wordsize,
                 * e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and
                 * CONFIG_32BIT=y, so READ_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.
                 */
                barrier();
                if (pte_none(vmf->orig_pte)) {
                        pte_unmap(vmf->pte);
                        vmf->pte = NULL;
                }
        }

        if (!vmf->pte) {
                if (vma_is_anonymous(vmf->vma))
                        return do_anonymous_page(vmf);
                else
                        return do_fault(vmf);
        }

        if (!pte_present(vmf->orig_pte))
                return do_swap_page(vmf);

        if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
                return do_numa_page(vmf);

        vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);
        spin_lock(vmf->ptl);
        entry = vmf->orig_pte;
        if (unlikely(!pte_same(*vmf->pte, entry)))
                goto unlock;
        if (vmf->flags & FAULT_FLAG_WRITE) {
                if (!pte_write(entry))
                        return do_wp_page(vmf);
                entry = pte_mkdirty(entry);
        }
        entry = pte_mkyoung(entry);
        if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
                                vmf->flags & FAULT_FLAG_WRITE)) {
                update_mmu_cache(vmf->vma, vmf->address, vmf->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 (vmf->flags & FAULT_FLAG_WRITE)
                        flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
        }
unlock:
        pte_unmap_unlock(vmf->pte, vmf->ptl);
}

요청한 가상 주소에서 fault된 경우 마지막 pte에 엔트리에 대한 처리이다. fault에 의해 처리되는 주요 항목은 다음과 같다.

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

 

  • 코드 라인 5~12에서 마지막 pte 테이블을 가리키는 pmd 엔트리가 비어있는 경우이다. vmf->pte에 null을 대입하여 이어지는 루틴에서 이에 대한 처리를 하도록 한다.
  • 코드 라인 13~39에서 pmd 엔트리가 존재하는 경우이다. vmf->pte에 pte 엔트리를 알아온다. 단 pmd 단위로 동작하는 디바이스 메모리이거나 pmd 단위로 동작하는 thp인 경우 더 이상 진행할 필요 없으므로 0을 반환한다.
fault에 의해 처리되는 주요 항목들
  • 코드 라인 41~46에서 테이블을 pte 엔트리가 매핑되어 있지 않은 경우 fault된 vma 영역이 file 매핑 또는 anon 매핑인지 구분하여 해당 처리 함수를 호출한다.
    • anon 매핑을 사용하는 경우 do_anonymous_page() 함수를 호출하여 새로운 페이지를 할당받아 매핑한다. 이 방법을 lazy 페이지 할당이라고 한다.
    • file 매핑을 사용하는 경우 file로부터 페이지를 읽어들이기 위해 do_fault() 함수를 호출한다.
  • 코드 라인 48~49에서 swap 엔트리인 경우 swap 된 페이지를 불러오기 위해 do_swap_page()를 호출한다.
  • 코드 라인 51~52에서 NUMA 밸런싱을 위해 fault가 발생한 경우 페이지를 migration하기 위해 do_numa_page() 함수를 호출한다.
  • 코드 라인 54~58에서 지금부터 pte 엔트리 값을 변경하기 위해 페이지 테이블 락을 획득한다.
    • pmd 엔트리 값에 해당하는, 즉 pte 페이지 테이블에 대한 페이지의 ptl(page table lock) 값을 가져오고 lock을 수행한다.
  • 코드 라인 59~63에서 페이지에 write 요청을 한 경우 엔트리 값에 dirty 설정을 한다. 또한 write protect 엔트리 즉 read-only 공유 페이지에 write 요청이 온 경우 기존 공유페이지를 새 페이지에 복사하기 위해 do_wp_page() 함수를 호출한다.
    • 유저가 공유메모리에 대해 write 요청을 하는 경우 기존 공유 페이지를 새 페이지에 COW(Copy-On-Write) 한다.
pte 엔트리 갱신
  • 코드 라인 64에서 엔트리 값에 현재 페이지에 acceess 하였음을 표시하는 young 비트 설정을 한다.
  • 코드 라인 65~77에서 pte 값과 엔트리 값이 다른 경우 pte 테이블 엔트리 값을 갱신한다. 실제 업데이트가 이루어진 경우 캐시도 업데이트한다. 만일 값이 같아 pte 테이블 엔트리 값을 업데이트 하지 않더라도 write 요청이 있었으면 tlb flush를 진행해야한다.
    • update_mmu_cache() 함수
      • arm 아키텍처 v6 이상 및 arm64에서는 아무런 동작을 하지 않아도 된다.

 

Page Translation Fault

ARM 및 ARM64의 경우 페이지 테이블 단계별로 엔트리에 접근하는데 이 때 해당 가상 주소에 대한 각 단계별 페이지 테이블의 엔트리 값중 bit0가 0인 경우 fault가 발생한다.

 

Lazy Page Allocation

유저 프로세스가 malloc() 함수를 호출할 때 Heap 매니저가 메모리가 부족 시 커널로 메모리 할당을 요청하는데 이 때 커널은 물리메모리는 할당하지 않고 가상 주소 영역(vma)만 지정한 후 페이지 테이블에는 null 매핑을 해둔다. 이렇게 비어있는 pte 엔트리를 비워두고 유저가 이 공간에 접근할 때 매핑되지 않은 공간이므로 fault가 발생하게 되는데 이 때에 실제 물리 메모리를 할당하고 해당 가상 주소에 매핑하는 방식을 사용한다.

 

swap 엔트리
  • pte 엔트리 값의 bit0 값이 0이면 매핑되지 않아서 fault가 발생한다. 그 엔트리의 다른 비트들에 어떠한 값이 존재하면 그 pte 엔트리는 swap 엔트리 값으로 사용한다.
  • swap 엔트리의 관리 메모리를 절약하기 위해 pte 엔트리를 사용한다.

 

NUMA 밸런싱 & protnone()
  • NUMA 시스템에서 해당 페이지를 읽을 때 accesss 권한 실패로 인해 abort exception이 발생되어 fault된 후 해당 페이지를 사용하는 태스크의 migration을 고려하는 Automatic NUMA balancing을 위해 사용된다. NUMA 밸런싱을 사용하지 않는 UMA 시스템에서는 항상 0을 반환한다.
  • ARM64
    • return (pte_val(pte) & (PTE_VALID | PTE_PROT_NONE)) == PTE_PROT_NONE;
    • none 비트만 설정되고 valid 비트는 없는 상태
  • x86
    • return (pmd_flags(pmd) & (_PAGE_PROTNONE | _PAGE_PRESENT)) == _PAGE_PROTNONE;
    • none 비트만 설정되고 present 비트는 없는 상태

 


파일 매핑(mmap)된 주소에서의 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().
 * If mmap_sem is released, vma may become invalid (for example
 * by other thread calling munmap()).
 */
static vm_fault_t do_fault(struct vm_fault *vmf)
{
        struct vm_area_struct *vma = vmf->vma;
        struct mm_struct *vm_mm = vma->vm_mm;
        vm_fault_t ret;

        /*
         * The VMA was not fully populated on mmap() or missing VM_DONTEXPAND
         */
        if (!vma->vm_ops->fault) {
                /*
                 * If we find a migration pmd entry or a none pmd entry, which
                 * should never happen, return SIGBUS
                 */
                if (unlikely(!pmd_present(*vmf->pmd)))
                        ret = VM_FAULT_SIGBUS;
                else {
                        vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
                                                       vmf->pmd,
                                                       vmf->address,
                                                       &vmf->ptl);
                        /*
                         * Make sure this is not a temporary clearing of pte
                         * by holding ptl and checking again. A R/M/W update
                         * of pte involves: take ptl, clearing the pte so that
                         * we don't have concurrent modification by hardware
                         * followed by an update.
                         */
                        if (unlikely(pte_none(*vmf->pte)))
                                ret = VM_FAULT_SIGBUS;
                        else
                                ret = VM_FAULT_NOPAGE;

                        pte_unmap_unlock(vmf->pte, vmf->ptl);
                }
        } else if (!(vmf->flags & FAULT_FLAG_WRITE))
                ret = do_read_fault(vmf);
        else if (!(vma->vm_flags & VM_SHARED))
                ret = do_cow_fault(vmf);
        else
                ret = do_shared_fault(vmf);

        /* preallocated pagetable is unused: free it */
        if (vmf->prealloc_pte) {
                pte_free(vm_mm, vmf->prealloc_pte);
                vmf->prealloc_pte = NULL;
        }
        return ret;
}

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

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

 

  • 코드 라인 10~35에서 매핑된 디바이스나 파일 시스템에 vm_ops->fault 후크 함수가 지원되지 않는 경우의 처리이다.
    • pte 엔트리의 unmap을 처리한다.
      • 32비트 시스템에서는 pte 엔트리를 highmem에 매핑하는 옵션을 사용할 수 있다. 이러한 경우에 한해 pte 테이블을 사용하지 않을 때 unmap 처리를 해야 한다.
      • 64비트 시스템에서는 pte 엔트리가 항상 매핑 상태를 유지하는 normal 메모리를 사용하므로 별도로 언매핑 처리하지 않는다.
      • CONFIG_HIGHPTE 커널 옵션을 사용하면 pte 엔트리를 highmem에 할당하여 사용할 수 있다.
    • 다음과 같이 fault 처리한다.
      • pmd 엔트리가 준비되어 있지 않으면 VM_FAULT_SIGBUS fault를 반환한다.
      • fault 주소에 대응하는 pte 엔트리 값이 없으면 VM_FAULT_SIGBUS, 있으면 VM_FAULT_NOPAGE를 반환한다.
  • 코드 라인 36~37에서 write 요청이 없는 경우 매핑된 file로부터 페이지를 읽기 위해 do_read_fault() 함수를 호출한다.
  • 코드 라인 38~39에서 vma 영역이 공유 설정된 경우 매핑된 file로부터 페이지를 읽은 후에 새 페이지에 복사하고 write 설정하기 위해 do_cow_fault() 함수를 호출한다.
  • 코드 라인 40~41에서 그 외의 경우 매핑된 file로부터 페이지를 읽은 후 write 설정하기 위해 do_shared_fault() 함수를 호출한다.
  • 코드 라인 44~48에서 사전에 준비한 사용되지 않는 pte 페이지 테이블을 할당해제 한다.

 

file 매핑
  • 디바이스 또는 파일을 vma 공간에 매핑하여 사용한다.
  • 페이지 캐시를 검색하여 없으면 새 페이지를 file로부터 읽은 후 페이지 캐시로 등록한다. 페이지 캐시가 검색되는 경우 이 페이지를 가상 공간에 매핑하여 사용한다.
  • DAX를 지원하는 파일시스템의 경우 페이지 캐시를 사용하지 않고, 파일 시스템에 연결된 persistent 메모리 등을 직접 가상 공간에 매핑하여 사용한다.

 

다음 그림은 file 또는 디바이스(pmem 등)를 가상 주소에 mmap() 매핑한 후 해당 영역에서 fault가 발생한 함수 처리 경로를 보여준다.

 

 

—커널 5.4 코드로 수정 중—

 

 

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에서 실제 swap 정보로 구성된 swap 엔트리가 아니라 migration 정보가 담긴 swap 엔트리인 경우 해당 페이지의 migration이 완료되어 page가 unlock될 때까지 기다린다.
      • 해당 페이지의 migration이 완료되어 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으로 한다.

 

참고

 

 

Swap 엔트리

<kernel v5.0>

Swap 엔트리

리눅스 커널의 Swap 엔트리 관리는 커널 v4.20-rc1에서 Radix Tree Exceptional 방식에서 XArray를 사용하는 방식으로 변경되었다.

 

swap_entry_t 구조

다음 그림은 32비트 시스템에서 운영하는 swap 엔트리 구조를 보여준다.

  • 이 값은 arm h/w pte 엔트리가 아니라 arm 리눅스 pte 엔트리에 저장되며 이 페이지가 swap 엔트리임을 의미한다.

 

다음 그림은 64비트 시스템에서 운영하는 swap 엔트리 구조를 보여준다.

 

swap_entry_t 구조체

include/linux/mm_types.h

 /*
  * A swap entry has to fit into a "unsigned long", as the entry is hidden
  * in the "index" field of the swapper address space.
  */
typedef struct {
        unsigned long val;
} swp_entry_t;

 

아키텍처 독립 Swap 엔트리

swp_entry()

include/linux/swapops.h

/*
 * Store a type+offset into a swp_entry_t in an arch-independent format
 */
static inline swp_entry_t swp_entry(unsigned long type, pgoff_t offset)
{
        swp_entry_t ret;

        ret.val = (type << SWP_TYPE_SHIFT(ret)) | (offset & SWP_OFFSET_MASK);
        return ret;
}

offset 값과 type 값으로 swap 엔트리를 구성한다.

 

swp_type()

include/linux/swapops.h

/*
 * Extract the `type' field from a swp_entry_t.  The swp_entry_t is in
 * arch-independent format
 */
static inline unsigned swp_type(swp_entry_t entry)
{
        return (entry.val >> SWP_TYPE_SHIFT);
}

swap 엔트리에서 type 값을 반환한다.

 

swp_offset()

include/linux/swapops.h

/*
 * Extract the `offset' field from a swp_entry_t.  The swp_entry_t is in
 * arch-independent format
 */
static inline pgoff_t swp_offset(swp_entry_t entry)
{
        return entry.val & SWP_OFFSET_MASK;
}

swap 엔트리에서 offset 값을 반환한다.

 

SWP_TYPE_SHIFT() & SWP_OFFSET_MASK()

include/linux/swapops.h

/*
 * swapcache pages are stored in the swapper_space radix tree.  We want to
 * get good packing density in that tree, so the index should be dense in
 * the low-order bits.
 *
 * We arrange the `type' and `offset' fields so that `type' is at the seven
 * high-order bits of the swp_entry_t and `offset' is right-aligned in the
 * remaining bits.  Although `type' itself needs only five bits, we allow for
 * shmem/tmpfs to shift it all up a further two bits: see swp_to_radix_entry().
 *
 * swp_entry_t's are *never* stored anywhere in their arch-dependent format.
 */
#define SWP_TYPE_SHIFT       (BITS_PER_XA_VALUE - MAX_SWAPFILES_SHIFT)
#define SWP_OFFSET_MASK      ((1UL << SWP_TYPE_SHIFT) - 1)

 

매크로 상수

include/linux/swap.h

/*
 * MAX_SWAPFILES defines the maximum number of swaptypes: things which can
 * be swapped to.  The swap type and the offset into that swap type are
 * encoded into pte's and into pgoff_t's in the swapcache.  Using five bits
 * for the type means that the maximum number of swapcache pages is 27 bits
 * on 32-bit-pgoff_t architectures.  And that assumes that the architecture packs
 * the type/offset into the pte as 5/27 as well.
 */
#define MAX_SWAPFILES_SHIFT     5

 

include/linux/swap.h

/*
 * NUMA node memory migration support
 */
#ifdef CONFIG_MIGRATION
#define SWP_MIGRATION_NUM 2
#define SWP_MIGRATION_READ      (MAX_SWAPFILES + SWP_HWPOISON_NUM)
#define SWP_MIGRATION_WRITE     (MAX_SWAPFILES + SWP_HWPOISON_NUM + 1)
#else
#define SWP_MIGRATION_NUM 0
#endif

 

ARM32 Swap 엔트리

__swp_entry()

arch/arm/include/asm/pgtable.h

#define __swp_entry(type,offset) ((swp_entry_t) { ((type) << __SWP_TYPE_SHIFT) | ((offset) << __SWP_OFFSET_SHIFT) })

offset 값과 type 값으로 arm swap 엔트리를 구성한다.

 

__swp_type()

arch/arm/include/asm/pgtable.h

#define __swp_type(x)           (((x).val >> __SWP_TYPE_SHIFT) & __SWP_TYPE_MASK)

arm swap 엔트리에서 type 값을 반환한다.

 

__swp_offset()

arch/arm/include/asm/pgtable.h

#define __swp_offset(x)         ((x).val >> __SWP_OFFSET_SHIFT)

arm swap 엔트리에서 offset 값을 반환한다.

 

매크로 상수

arch/arm/include/asm/pgtable.h

/*      
 * Encode and decode a swap entry.  Swap entries are stored in the Linux
 * page tables as follows:
 *
 *   3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
 *   1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
 *   <--------------- offset ------------------------> < type -> 0 0
 *
 * This gives us up to 31 swap files and 128GB per swap file.  Note that
 * the offset field is always non-zero.
 */
#define __SWP_TYPE_SHIFT        2
#define __SWP_TYPE_BITS         5
#define __SWP_TYPE_MASK         ((1 << __SWP_TYPE_BITS) - 1)
#define __SWP_OFFSET_SHIFT      (__SWP_TYPE_BITS + __SWP_TYPE_SHIFT)

 

ARM64 Swap 엔트리

__swp_entry()

arch/arm64/include/asm/pgtable.h

#define __swp_entry(type,offset) ((swp_entry_t) { ((type) << __SWP_TYPE_SHIFT) | ((offset) << __SWP_OFFSET_SHIFT) })

offset 값과 type 값으로 arm64 swap 엔트리를 구성한다.

 

__swp_type()

arch/arm64/include/asm/pgtable.h

#define __swp_type(x)           (((x).val >> __SWP_TYPE_SHIFT) & __SWP_TYPE_MASK)

arm64 swap 엔트리에서 type 값을 반환한다.

 

__swp_offset()

arch/arm64/include/asm/pgtable.h

#define __swp_offset(x)         (((x).val >> __SWP_OFFSET_SHIFT) & __SWP_OFFSET_MASK)

arm64 swap 엔트리에서 50bit offset 값을 반환한다.

 

매크로 상수

arch/arm64/include/asm/pgtable.h

/*
 * Encode and decode a swap entry:
 *      bits 0-1:       present (must be zero)
 *      bits 2-7:       swap type
 *      bits 8-57:      swap offset
 *      bit  58:        PTE_PROT_NONE (must be zero)
 */
#define __SWP_TYPE_SHIFT        2
#define __SWP_TYPE_BITS         6
#define __SWP_OFFSET_BITS       50
#define __SWP_TYPE_MASK         ((1 << __SWP_TYPE_BITS) - 1)
#define __SWP_OFFSET_SHIFT      (__SWP_TYPE_BITS + __SWP_TYPE_SHIFT)
#define __SWP_OFFSET_MASK       ((1UL << __SWP_OFFSET_BITS) - 1)


Swap PTE 엔트리 식별

is_swp_pte()

include/linux/swapops.h

/* check whether a pte points to a swap entry */
static inline int is_swap_pte(pte_t pte)
{
        return !pte_none(pte) && !pte_present(pte);
}

swap된 pte 엔트리인지 여부를 반환한다.

  • PTE가 NONE 설정이 아니면서 PRESENT 설정도 없는 경우가 swap 상태이다.

 

Swap 캐시

backing storage에 예약된 슬롯을 가진 공유 페이지는 swap 캐시로 간주된다. swap 캐시는 파일 캐시와 다음 2 가지가 다르다.

  • page->mapping 이 &swapper_space[]를 사용한다. (address_space)
  • add_to_page_cache() 대신 add_to_swap_cache() 함수를 사용한다.