NUMA -1- (ARM64 초기화)

<kernel v5.0>

NUMA -1- (ARM64 초기화)

arm64_numa_init()

arch/arm64/mm/numa.c

/**
 * arm64_numa_init - Initialize NUMA
 *
 * Try each configured NUMA initialization method until one succeeds.  The
 * last fallback is dummy single node config encomapssing whole memory.
 */
void __init arm64_numa_init(void)
{
        if (!numa_off) {
                if (!acpi_disabled && !numa_init(arm64_acpi_numa_init))
                        return;
                if (acpi_disabled && !numa_init(of_numa_init))
                        return;
        }

        numa_init(dummy_numa_init);
}

ARM64 시스템에서 ACPI 테이블 또는 디바이스 트리를 통해 NUMA 시스템 구성을 시도한다. 그러한 구성이 없으면 노드가 한 개인 dummy 누마 초기화를 수행한다.

 

numa_init()

arch/arm64/mm/numa.c

static int __init numa_init(int (*init_func)(void))
{
        int ret;

        nodes_clear(numa_nodes_parsed);
        nodes_clear(node_possible_map);
        nodes_clear(node_online_map);

        ret = numa_alloc_distance();
        if (ret < 0)
                return ret;

        ret = init_func();
        if (ret < 0)
                goto out_free_distance;

        if (nodes_empty(numa_nodes_parsed)) {
                pr_info("No NUMA configuration found\n");
                ret = -EINVAL;
                goto out_free_distance;
        }

        ret = numa_register_nodes();
        if (ret < 0)
                goto out_free_distance;

        setup_node_to_cpumask_map();

        return 0;
out_free_distance:
        numa_free_distance();
        return ret;
}

NUMA 시스템을 초기화한다.

  • 코드 라인 5~7에서 NUMA 상태를 관리하는 비트맵들을 모두 초기화한다.
  • 코드 라인 9~11에서 numa_disatance[] 배열을 필요한 만큼 할당하고 초기 값들을 지정한다.
  • 코드 라인 13~15에서 인자로 받은 함수 @init_func을 실행시켜 설정한다.
  • 코드 라인 17~21에서 노드 정보가 하나도 없는 경우 “No NUMA configuration found” 메시지를 출력하고 빠져나간다.
  • 코드 라인 23~25에서 노드 데이터(struct pglist_data)를 할당하고 기본 노드 정보를 설정한다.
  • 코드 라인 27에서 노드 -> cpu 맵을 설정한다.

 

아래와 같이 ACPI 또는 디바이스 트리를 통해 NUMA 설정을 찾을 수 없는 경우 “No NUMA configuration found” 메시지가 출력되는 것을 볼 수 있다. 그리고 fallback되어 dymmy NUMA 초기화 루틴을 통해 “Faking a node at …” 메시지가 출력되는 것도 볼 수 있다.

[    0.000000] NUMA: No NUMA configuration found
[    0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000] NUMA: NODE_DATA [mem 0x7ddf4840-0x7ddf5fff]
[    0.000000] Zone ranges:
[    0.000000]   DMA32    [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000]   Normal   empty
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000040000000-0x000000007fffffff]

 

numa_alloc_distance()

arch/arm64/mm/numa.c

static int __init numa_alloc_distance(void)
{
        size_t size;
        u64 phys;
        int i, j;

        size = nr_node_ids * nr_node_ids * sizeof(numa_distance[0]);
        phys = memblock_find_in_range(0, PFN_PHYS(max_pfn),
                                      size, PAGE_SIZE);
        if (WARN_ON(!phys))
                return -ENOMEM;

        memblock_reserve(phys, size);

        numa_distance = __va(phys);
        numa_distance_cnt = nr_node_ids;

        /* fill with the default distances */
        for (i = 0; i < numa_distance_cnt; i++)
                for (j = 0; j < numa_distance_cnt; j++)
                        numa_distance[i * numa_distance_cnt + j] = i == j ?
                                LOCAL_DISTANCE : REMOTE_DISTANCE;

        pr_debug("Initialized distance table, cnt=%d\n", numa_distance_cnt);

        return 0;
}

노드 수 * 노드 수만큼 numa_distance[] 배열을 만들어 할당한다.

  • 코드 라인 7에서 할당할 사이즈로 노드 수 * 노드 수를 사용한다.
  • 코드 라인 8~11에서 memblock의 빈 공간에서 size 만큼의 공간을 페이지 단위로 알아온다.
  • 코드 라인 13에서 size 영역을 memblock에 reserve 한다.
  • 코드 라인 15~16에서 할당한 물리 주소를 가상 주소로 변환하여 numa_distance에 대입하고, 노드 수를 지정한다.
  • 코드 라인 19~22에서 numa_distance[]는 논리적으로 이중 배열을 표현하였다. 디폴트 numa_distance[] 값으로 같은 노드를 의미하는 경우에만 LOCAL_DISTANCE(10)를 지정하고 그 외의 경우는 REMOTE_DISTANCE(20) 값을 지정해둔다.
    • 예) nr_node_ids=2인 경우 numa_distance[]를 알아본다.
      • numa_distance[0] = 10
      • numa_distance[1] = 20
      • numa_distance[2] = 20
      • numa_distance[3] = 10

 

numa_register_nodes()

arch/arm64/mm/numa.c

static int __init numa_register_nodes(void)
{
        int nid;
        struct memblock_region *mblk;

        /* Check that valid nid is set to memblks */
        for_each_memblock(memory, mblk)
                if (mblk->nid == NUMA_NO_NODE || mblk->nid >= MAX_NUMNODES) {
                        pr_warn("Warning: invalid memblk node %d [mem %#010Lx-%#010Lx]\n",
                                mblk->nid, mblk->base,
                                mblk->base + mblk->size - 1);
                        return -EINVAL;
                }

        /* Finally register nodes. */
        for_each_node_mask(nid, numa_nodes_parsed) {
                unsigned long start_pfn, end_pfn;

                get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
                setup_node_data(nid, start_pfn, end_pfn);
                node_set_online(nid);
        }

        /* Setup online nodes to actual nodes*/
        node_possible_map = numa_nodes_parsed;

        return 0;
}

노드 데이터(struct pglist_data)를 할당하고 기본 노드 정보를 설정한다.

  • 코드 라인 7~13에서 memblock에 노드가 지정되지 않은 경우 경고 메시지를 출력하고 에러 -EINVAL 값을 반환한다.
  • 코드 라인 16~22에서 노드를 순회하며 노드 데이터( struct pglist_data)를 할당하고 노드의 기본 정보를 설정한다.
  • 코드 라인 23에서 해당 노드를 online 상태로 설정한다.
  • 코드 라인 27에서 online 노드들을 possible 맵에 지정한다.

 

setup_node_to_cpumask_map()

arm64/mm/numa.c

/*
 * Allocate node_to_cpumask_map based on number of available nodes
 * Requires node_possible_map to be valid.
 *
 * Note: cpumask_of_node() is not valid until after this is done.
 * (Use CONFIG_DEBUG_PER_CPU_MAPS to check this.)
 */
static void __init setup_node_to_cpumask_map(void)
{
        int node;

        /* setup nr_node_ids if not done yet */
        if (nr_node_ids == MAX_NUMNODES)
                setup_nr_node_ids();

        /* allocate and clear the mapping */
        for (node = 0; node < nr_node_ids; node++) {
                alloc_bootmem_cpumask_var(&node_to_cpumask_map[node]);
                cpumask_clear(node_to_cpumask_map[node]);
        }

        /* cpumask_of_node() will now work */
        pr_debug("Node to cpumask map for %d nodes\n", nr_node_ids);
}

노드 -> cpu 맵을 설정한다.

  • 코드 라인 6~7에서 nr_node_ids가 아직 설정되지 않은 경우 possible 노드 수로 지정한다.
  • 코드 라인 10~13에서 노드 수만큼 순회하며 node_to_cpumask_map[node] 비트맵을 할당하고 초기화한다.

 

dummy_numa_init()

arch/arm64/mm/numa.c

/**
 * dummy_numa_init - Fallback dummy NUMA init
 *
 * Used if there's no underlying NUMA architecture, NUMA initialization
 * fails, or NUMA is disabled on the command line.
 *
 * Must online at least one node (node 0) and add memory blocks that cover all
 * allowed memory. It is unlikely that this function fails.
 */
static int __init dummy_numa_init(void)
{
        int ret;
        struct memblock_region *mblk;

        if (numa_off)
                pr_info("NUMA disabled\n"); /* Forced off on command line. */
        pr_info("Faking a node at [mem %#018Lx-%#018Lx]\n",
                memblock_start_of_DRAM(), memblock_end_of_DRAM() - 1);

        for_each_memblock(memory, mblk) {
                ret = numa_add_memblk(0, mblk->base, mblk->base + mblk->size);
                if (!ret)
                        continue;

                pr_err("NUMA init failed\n");
                return ret;
        }

        numa_off = true;
        return 0;
}

NUMA 설정이 없는 경우 한 개의 노드로 구성된 dummy NUMA 구성으로 초기화한다.

  • 코드 라인 6~7에서 “numa=off” 커널 파라미터가 지정된 경우 “NUMA disabled” 메시지를 출력한다.
  • 코드 라인 8~9에서 “Faking a node at …” 메시지 정보를 통해 한 개의 메모리 정보를 출력한다.
  • 코드 라인 11~18에서 memory memblock 모두에 0번 노드를 지정한다.
  • 코드 라인 20에서 NUMA가 disable 되었음을 나타낸다.

 

numa_add_memblk()

arch/arm64/mm/numa.c

/**
 * numa_add_memblk - Set node id to memblk
 * @nid: NUMA node ID of the new memblk
 * @start: Start address of the new memblk
 * @end:  End address of the new memblk
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init numa_add_memblk(int nid, u64 start, u64 end)
{
        int ret;

        ret = memblock_set_node(start, (end - start), &memblock.memory, nid);
        if (ret < 0) {
                pr_err("memblock [0x%llx - 0x%llx] failed to add on node %d\n",
                        start, (end - 1), nid);
                return ret;
        }

        node_set(nid, numa_nodes_parsed);
        return ret;
}

memblock 영역에 노드 id를 지정하고  numa_nodes_parsed 비트맵에 현재 노드를 설정한다.

 

“numa” Early 커널 파라미터

numa_parse_early_param()

arch/arm64/mm/numa.c

static __init int numa_parse_early_param(char *opt)
{
        if (!opt)
                return -EINVAL;
        if (!strncmp(opt, "off", 3))
                numa_off = true;

        return 0;
}
early_param("numa", numa_parse_early_param);

“numa=off” 커널 파라미터가 지정된 경우 전역 변수 numa_off에 true를 대입한다.

 


디바이스 트리를 통한 NUMA 시스템 초기화

다음과 같이 4개의 NUMA 노드를 가진 시스템 구성을 참고한다.

  • 각  노드마다 4개의 클러스터로 구성하여 총 16개의 클러스터를 사용한다.
  • 각 클러스터마다 4개의 코어를 구성하여 총 64개의 코어를 사용한다.
  • 메모리 노드의 누마 id는 UEFI 펌웨어가 인식하여 전달되므로 아래는 노드 0번 정보만 기록되어 있다.
#include "hip07.dtsi"

/ {
        model = "Hisilicon Hip07 D05 Development Board";
        compatible = "hisilicon,hip07-d05";

        /* the mem node will be updated by UEFI. */
        memory@0 {
                device_type = "memory";
                reg = <0x0 0x00000000 0x0 0x40000000>;
                numa-node-id = <0>;
        };

        distance-map {
                compatible = "numa-distance-map-v1";
                distance-matrix = <0 0 10>,
                                  <0 1 15>,
                                  <0 2 20>,
                                  <0 3 25>,
                                  <1 0 15>,
                                  <1 1 10>,
                                  <1 2 25>,
                                  <1 3 30>,
                                  <2 0 20>,
                                  <2 1 25>,
                                  <2 2 10>,
                                  <2 3 15>,
                                  <3 0 25>,
                                  <3 1 30>,
                                  <3 2 15>,
                                  <3 3 10>;

 

arch/arm64/boot/dts/hisilicon/hip07.dtsi

                cpu-map {
                        cluster0 {
                                core0 {
                                        cpu = <&cpu0>;
                                };
                                core1 {
                                        cpu = <&cpu1>;
                                };
                                core2 {
                                        cpu = <&cpu2>;
                                };
                                core3 {
                                        cpu = <&cpu3>;
                                };
                        };

                        ...

                        cluster15 {
                                core0 {
                                        cpu = <&cpu60>;
                                };
                                core1 {
                                        cpu = <&cpu61>;
                                };
                                core2 {
                                        cpu = <&cpu62>;
                                };
                                core3 {
                                        cpu = <&cpu63>;
                                };
                        };
                };

                cpu0: cpu@10000 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a72", "arm,armv8";
                        reg = <0x10000>;
                        enable-method = "psci";
                        next-level-cache = <&cluster0_l2>;
                        numa-node-id = <0>;
                };

                ...

                cpu63: cpu@70303 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a72", "arm,armv8";
                        reg = <0x70303>;
                        enable-method = "psci";
                        next-level-cache = <&cluster15_l2>;
                        numa-node-id = <3>;
                };

                cluster0_l2: l2-cache0 {
                        compatible = "cache";
                };

                ...

                cluster15_l2: l2-cache15 {
                        compatible = "cache";
                };
        };

 

of_numa_init()

drivers/of/of_numa.c

int __init of_numa_init(void)
{
        int r;

        of_numa_parse_cpu_nodes();
        r = of_numa_parse_memory_nodes();
        if (r)
                return r;
        return of_numa_parse_distance_map();
}

디바이스 트리를 통해 cpu 노드와 memory 노드에서 누마 id를 읽어 설정하고, distance 맵을 파싱해온다.

 

of_numa_parse_cpu_nodes()

drivers/of/of_numa.c

/*
 * Even though we connect cpus to numa domains later in SMP
 * init, we need to know the node ids now for all cpus.
*/
static void __init of_numa_parse_cpu_nodes(void)
{
        u32 nid;
        int r;
        struct device_node *np;

        for_each_of_cpu_node(np) {
                r = of_property_read_u32(np, "numa-node-id", &nid);
                if (r)
                        continue;

                pr_debug("CPU on %u\n", nid);
                if (nid >= MAX_NUMNODES)
                        pr_warn("Node id %u exceeds maximum value\n", nid);
                else
                        node_set(nid, numa_nodes_parsed);
        }
}

cpu 노드 수만큼 순회하며 노드를 파싱한다.

  • “numa-node-id” 속성이 1 이상인 경우 numa_nodes_parsed 비트맵에 노드를 설정한다.

 

of_numa_parse_memory_nodes()

drivers/of/of_numa.c

static int __init of_numa_parse_memory_nodes(void)
{
        struct device_node *np = NULL;
        struct resource rsrc;
        u32 nid;
        int i, r;

        for_each_node_by_type(np, "memory") {
                r = of_property_read_u32(np, "numa-node-id", &nid);
                if (r == -EINVAL)
                        /*
                         * property doesn't exist if -EINVAL, continue
                         * looking for more memory nodes with
                         * "numa-node-id" property
                         */
                        continue;

                if (nid >= MAX_NUMNODES) {
                        pr_warn("Node id %u exceeds maximum value\n", nid);
                        r = -EINVAL;
                }

                for (i = 0; !r && !of_address_to_resource(np, i, &rsrc); i++)
                        r = numa_add_memblk(nid, rsrc.start, rsrc.end + 1);

                if (!i || r) {
                        of_node_put(np);
                        pr_err("bad property in memory node\n");
                        return r ? : -EINVAL;
                }
        }

        return 0;
}

memory 노드 수만큼 순회하며 노드를 파싱하여 memblock에 노드를 기록한다.

  • “numa-node-id” 속성이 있는 메모리 노드인 경우 그 영역의 memblock에 노드 정보를 기록한다.

 

of_numa_parse_distance_map()

drivers/of/of_numa.c

static int __init of_numa_parse_distance_map(void)
{
        int ret = 0;
        struct device_node *np;

        np = of_find_compatible_node(NULL, NULL,
                                     "numa-distance-map-v1");
        if (np)
                ret = of_numa_parse_distance_map_v1(np);

        of_node_put(np);
        return ret;
}

compatible = “numa-distance-map-v1” 속성 값을 가진 노드에서 distance 맵을 파싱하여 온다.

 

of_numa_parse_distance_map_v1()

drivers/of/of_numa.c

static int __init of_numa_parse_distance_map_v1(struct device_node *map)
{
        const __be32 *matrix;
        int entry_count;
        int i;

        pr_info("parsing numa-distance-map-v1\n");

        matrix = of_get_property(map, "distance-matrix", NULL);
        if (!matrix) {
                pr_err("No distance-matrix property in distance-map\n");
                return -EINVAL;
        }

        entry_count = of_property_count_u32_elems(map, "distance-matrix");
        if (entry_count <= 0) {
                pr_err("Invalid distance-matrix\n");
                return -EINVAL;
        }

        for (i = 0; i + 2 < entry_count; i += 3) {
                u32 nodea, nodeb, distance;

                nodea = of_read_number(matrix, 1);
                matrix++;
                nodeb = of_read_number(matrix, 1);
                matrix++;
                distance = of_read_number(matrix, 1);
                matrix++;

                if ((nodea == nodeb && distance != LOCAL_DISTANCE) ||
                    (nodea != nodeb && distance <= LOCAL_DISTANCE)) {
                        pr_err("Invalid distance[node%d -> node%d] = %d\n",
                               nodea, nodeb, distance);
                        return -EINVAL;
                }

                numa_set_distance(nodea, nodeb, distance);

                /* Set default distance of node B->A same as A->B */
                if (nodeb > nodea)
                        numa_set_distance(nodeb, nodea, distance);
        }

        return 0;
}

distance 맵을 파싱하여 온다.

  • “distance-matrix” 속성 값을 읽어 numa_distance[] 배열에 값을 지정한다.

 

numa_set_distance()

arch/arm64/mm/numa.c

/**
 * numa_set_distance - Set inter node NUMA distance from node to node.
 * @from: the 'from' node to set distance
 * @to: the 'to'  node to set distance
 * @distance: NUMA distance
 *
 * Set the distance from node @from to @to to @distance.
 * If distance table doesn't exist, a warning is printed.
 *
 * If @from or @to is higher than the highest known node or lower than zero
 * or @distance doesn't make sense, the call is ignored.
 *
 */
void __init numa_set_distance(int from, int to, int distance)
{
        if (!numa_distance) {
                pr_warn_once("Warning: distance table not allocated yet\n");
                return;
        }

        if (from >= numa_distance_cnt || to >= numa_distance_cnt ||
                        from < 0 || to < 0) {
                pr_warn_once("Warning: node ids are out of bound, from=%d to=%d distance=%d\n",
                            from, to, distance);
                return;
        }

        if ((u8)distance != distance ||
            (from == to && distance != LOCAL_DISTANCE)) {
                pr_warn_once("Warning: invalid distance parameter, from=%d to=%d distance=%d\n",
                             from, to, distance);
                return;
        }

        numa_distance[from * numa_distance_cnt + to] = distance;
}

numa distace를 설정한다.

  • numa_distance[from * numa_distance_cnt + to] 배열에 distance 값을 지정한다.

 

참고

 

 

bootmem_init() – ARM64

<kernel v5.0>

부트 메모리 초기화

bootmem_init()

arch/arm64/mm/init.c

void __init bootmem_init(void)
{
        unsigned long min, max;

        min = PFN_UP(memblock_start_of_DRAM());
        max = PFN_DOWN(memblock_end_of_DRAM());

        early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);

        max_pfn = max_low_pfn = max;

        arm64_numa_init();
        /*
         * Sparsemem tries to allocate bootmem in memory_present(), so must be
         * done after the fixed reservations.
         */
        arm64_memory_present();

        sparse_init();
        zone_sizes_init(min, max);

        memblock_dump_all();
}

부트 메모리 초기화 루틴에서는 각 노드의 각 존별 초기화를 수행한다.

  • 코드 라인 5~8에서 “memtest=<count>” 커널 파라메터가 주어진 경우 메모리에 대한 패턴 테스트를 <count> 수 만큼 수행한다.
  • 코드 라인 10에서 arm64에는 highmem이 없다. 따라서 highmem 경계를 나타내는 max_low_pfn은 max 값과 동일하다.
  • 코드 라인 12에서 NUMA 시스템인 경우 초기화를 수행한다. ACPI 또는 디바이스 트리를 통해 초기화를 수행한다. ACPI 또는 디바이스 트리를 통해 NUMA 초기화를 수행하지 못하는 경우이거나,  “numa=off” 커널 파라메터가 주어진 경우 NUMA disable 상태로 초기화를 수행한다.
  • 코드 라인 17에서 각 메모리 모델의 관리를 위해 필요한 데이터를 초기화한다.
    • sparse memory 모델의 경우 memblock에 등록된 모든 메모리 블록에 대해 섹션별로 메모리가 존재하는 곳의 mem_section을 초기화한다.
  • 코드 라인 19에서 Sparse memory 모델을 사용하는 시스템을 위해 관리 영역을 할당받고 매핑 초기화한다.
  • 코드 라인 20에서 존별로 메모리 영역을 지정하고 초기화한다.
  • 코드 라인 22에서 “memblock=debug” 커널 파라메터가 주어진 경우 memblock 상태를 덤프한다.

 

다음은 4G RAM을 가진 rock960 보드의 memblock 상태를 덤프하여 보여준다.

$ cat /sys/kernel/debug/memblock/memory
   0: 0x0000000000200000..0x00000000f7ffffff
$ cat /sys/kernel/debug/memblock/reserved
   0: 0x0000000002080000..0x00000000033b5fff
   1: 0x00000000ef400000..0x00000000f5dfffff
   2: 0x00000000f5eef000..0x00000000f5f01fff
   3: 0x00000000f6000000..0x00000000f7bfffff
   4: 0x00000000f7df4000..0x00000000f7df4fff
   5: 0x00000000f7df5e00..0x00000000f7df5fff
   6: 0x00000000f7e74000..0x00000000f7f61fff
   7: 0x00000000f7f62600..0x00000000f7f6265f
   8: 0x00000000f7f62680..0x00000000f7f626df
   9: 0x00000000f7f62700..0x00000000f7f6282f
  10: 0x00000000f7f62840..0x00000000f7f62857
  11: 0x00000000f7f62880..0x00000000f7f62887
  12: 0x00000000f7f648c0..0x00000000f7f6492b
  13: 0x00000000f7f64940..0x00000000f7f649ab
  14: 0x00000000f7f649c0..0x00000000f7f64a2b
  15: 0x00000000f7f64a40..0x00000000f7f64a47
  16: 0x00000000f7f64a64..0x00000000f7f64aea
  17: 0x00000000f7f64aec..0x00000000f7f64b1a
  18: 0x00000000f7f64b1c..0x00000000f7f64b4a
  19: 0x00000000f7f64b4c..0x00000000f7f64b7a
  20: 0x00000000f7f64b7c..0x00000000f7f64baa
  21: 0x00000000f7f64bac..0x00000000f7fcdff7
  22: 0x00000000f7fce000..0x00000000f7ffffff

 


 

zone_sizes_init() – NUMA

arch/arm64/mm/init.c

static void __init zone_sizes_init(unsigned long min, unsigned long max)
{
        unsigned long max_zone_pfns[MAX_NR_ZONES]  = {0};

        if (IS_ENABLED(CONFIG_ZONE_DMA32))
                max_zone_pfns[ZONE_DMA32] = PFN_DOWN(max_zone_dma_phys());
        max_zone_pfns[ZONE_NORMAL] = max;

        free_area_init_nodes(max_zone_pfns);
}

메모리 모델에 따른 노드, 존의 초기화를 다룬다.

  • 코드 라인 5~6에서 dma32 존의 끝(pfn)을 지정한다.
    • 실제 끝 pfn보다 1 추가된 값을 가진다. (예: 0xf_ffff -> 0x10_0000)
  • 코드 라인 7에서 normal 존의 끝(pfn)을 지정한다.
    • 메모리가 dma32 존에 들어갈 정도로 작은 경우 dma32 존과 normal 존에 대한 max_zone_pfns[] 값은 동일하다.
    • 예) 1G 메모리
      • max_zone_pfns[] = { 0x8_0000, 0x8_0000, 0 }
  • 코드 라인 9에서 max_zone_pfns[] 배열 정보를 사용하여 빈 페이지들을 초기화한다.

 

다음 그림은 CONFIG_ZONE_DMA32 및 CONFIG_NUMA 커널 옵션을 사용한 경우 max_zone_pfns[] 값을 산출하는 과정이다.

  • DMA 영역은 물리 공간에서 최대 4G 영역을 사용할 수 있으며, 4G 단위의 영역 경계를 넘어가지 못한다.

 

zone_sizes_init() – UMA

arch/arm64/mm/init.c

static void __init zone_sizes_init(unsigned long min, unsigned long max)
{
        struct memblock_region *reg;
        unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
        unsigned long max_dma = min;

        memset(zone_size, 0, sizeof(zone_size));

        /* 4GB maximum for 32-bit only capable devices */
#ifdef CONFIG_ZONE_DMA32
        max_dma = PFN_DOWN(arm64_dma_phys_limit);
        zone_size[ZONE_DMA32] = max_dma - min;
#endif
        zone_size[ZONE_NORMAL] = max - max_dma;

        memcpy(zhole_size, zone_size, sizeof(zhole_size));

        for_each_memblock(memory, reg) {
                unsigned long start = memblock_region_memory_base_pfn(reg);
                unsigned long end = memblock_region_memory_end_pfn(reg);

                if (start >= max)
                        continue;

#ifdef CONFIG_ZONE_DMA32
                if (start < max_dma) {
                        unsigned long dma_end = min(end, max_dma);
                        zhole_size[ZONE_DMA32] -= dma_end - start;
                }
#endif
                if (end > max_dma) {
                        unsigned long normal_end = min(end, max);
                        unsigned long normal_start = max(start, max_dma);
                        zhole_size[ZONE_NORMAL] -= normal_end - normal_start;
                }
        }

        free_area_init_node(0, zone_size, min, zhole_size);
}

메모리 모델에 따른 노드, 존의 초기화를 다룬다.

  • 코드 라인 10~13에서 dma32 존 사이즈를 지정한다.
  • 코드 라인 14에서 dma32 존 사이즈를 제외한 메모리로 normal 존 사이즈를 지정한다.
  • 코드 라인 16에서 zone_size[ ]를 zhole_size[ ]에 복사한다.
  • 코드 라인 18~23에서 memory memblock을 루프를 돌며 메모리의 끝 주소 이상인 영역은 skip 한다.
  • 코드 라인 25~30에서 max_dma 아래에 위치한 영역에 대해 dma32 존에 대한 zhole_size를 구한다.
  • 코드 라인 31~35에서 max_dma를 초과하는 영역에 대해 normal 존에 대한 zhole_size를 구한다.
  • 코드 라인 38에서 0번 노드에 대해 zone_size[ ]와, zhole_size[ ] 정보를 사용하여 빈 페이지들을 초기화한다.

 

다음 그림은 CONFIG_ZONE_DMA32 커널 옵션을 사용한 경우 zone_size[] 및 zhole_size[] 값을 산출하는 과정이다.

  • DMA 영역은 물리 공간에서 최대 4G 영역을 사용할 수 있으며, 4G 단위의 영역 경계를 넘어가지 못한다.

 

max_zone_dma_phys()

arch/arm64/mm/init.c

/*
 * Return the maximum physical address for ZONE_DMA32 (DMA_BIT_MASK(32)). It
 * currently assumes that for memory starting above 4G, 32-bit devices will
 * use a DMA offset.
 */
static phys_addr_t __init max_zone_dma_phys(void)
{
        phys_addr_t offset = memblock_start_of_DRAM() & GENMASK_ULL(63, 32);
        return min(offset + (1ULL << 32), memblock_end_of_DRAM());
}

DMA 가능한 최대 물리 메모리의 끝 주소를 반환한다. 단 DMA 가능한 물리 메모리의 끝 주소는 시작 주소를 4G 단위로 내림 정렬한 후 4G를 더한 값을 초과하지 않는다. DMA 영역은 물리 공간에서 최대 4G 영역을 사용할 수 있으며, 4G 단위의 영역 경계를 넘어가지 못한다.

  • 예) DRAM 시작 주소=0x8_8000_0000, DRAM 끝 주소=0x8_f000_0000
    • offset=0x8_0000_0000, 반환 값=0x8_f000_0000
  • 예) DRAM 시작 주소=0x8_8000_0000, DRAM 끝 주소=0x9_800_0000
    • offset=0x8_0000_0000, 반환 값=0x9_0000_0000

 

GENMASK_ULL()

include/linux/bits.h

#define GENMASK_ULL(h, l) \
        (((~0ULL) - (1ULL << (l)) + 1) & \
         (~0ULL >> (BITS_PER_LONG_LONG - 1 - (h))))

high 비트 번호 @h 부터 low 비트 번호 @l 까지 1로 설정한 64비트 값을 반환한다.

  • 예) h=63, l=32
    • 0xffff_ffff_0000_0000

 

참고

setup_arch() – ARM64

 

다음 그림은 ARM64 아키텍처가 setup_arch() 함수내에서 준비하는 과정 중 메모리 할당자 및 매핑 시스템에 대한 전개 과정을 보여준다.

  • 부트 업 순간 memblock_add() 및 memblock_reserve() 등의 API를 사용할 수 있다. 그러나 메모리에 대한 매핑이 완료되지 않은 상태에서는 memblock_alloc() 함수를 사용할 수 없다.
  • paging_init()이 완료된 이후 부터는 early 메모리 할당자인 memblock을 사용하여 메모리 영역 중 빈 영역을 할당할 수 있다.
  • setup_arch() 및 mm_init()이 완료된 이후 부터는 정규 메모리 할당자 들이 사용될 수 있다.

 

setup_arch()

arch/arm64/kernel/setup.c

void __init setup_arch(char **cmdline_p)
{
        init_mm.start_code = (unsigned long) _text;
        init_mm.end_code   = (unsigned long) _etext;
        init_mm.end_data   = (unsigned long) _edata;
        init_mm.brk        = (unsigned long) _end;

        *cmdline_p = boot_command_line;

        early_fixmap_init();
        early_ioremap_init();

        setup_machine_fdt(__fdt_pointer);

        parse_early_param();

        /*
         * Unmask asynchronous aborts and fiq after bringing up possible
         * earlycon. (Report possible System Errors once we can report this
         * occurred).
         */
        local_daif_restore(DAIF_PROCCTX_NOIRQ);

        /*
         * TTBR0 is only used for the identity mapping at this stage. Make it
         * point to zero page to avoid speculatively fetching new entries.
         */
        cpu_uninstall_idmap();

        xen_early_init();
        efi_init();
        arm64_memblock_init();

        paging_init();

        acpi_table_upgrade();

        /* Parse the ACPI tables for possible boot-time configuration */
        acpi_boot_table_init();

        if (acpi_disabled)
                unflatten_device_tree();

        bootmem_init();

        kasan_init();

        request_standard_resources();

        early_ioremap_reset();

        if (acpi_disabled)
                psci_dt_init();
        else
                psci_acpi_init();

        cpu_read_bootcpu_ops();
        smp_init_cpus();
        smp_build_mpidr_hash();


        /* Init percpu seeds for random tags after cpus are set up. */
        kasan_init_tags();

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
        /*
         * Make sure init_thread_info.ttbr0 always generates translation
         * faults in case uaccess_enable() is inadvertently called by the init
         * thread.
         */
        init_task.thread_info.ttbr0 = __pa_symbol(empty_zero_page);
#endif

#ifdef CONFIG_VT
        conswitchp = &dummy_con;
#endif
        if (boot_args[1] || boot_args[2] || boot_args[3]) {
                pr_err("WARNING: x1-x3 nonzero in violation of boot protocol:\n"
                        "\tx1: %016llx\n\tx2: %016llx\n\tx3: %016llx\n"
                        "This indicates a broken bootloader or old kernel\n",
                        boot_args[1], boot_args[2], boot_args[3]);
        }
}

ARM64 아키텍처에 초기화를 진행한다. 주로 커널 및 메모리 매핑과 reserve 영역들을 등록한다.

  • 코드 라인 3~6에서 커널 코드 영역과, 데이터 영역(컴파일 타임에 초기화된 데이터 및 초기화되지 않은 데이터 영역)의 가상 주소를 지정한다.
  • 코드 라인 10에서 fixmap을 사용할 수 있도록 페이지 테이블에 매핑해둔다.
  • 코드 라인 11에서 fixmap 가상 주소 영역을 사용하여 7개 디바이스까지 각 디바이스 당 최대 256K 영역을 지원하는 임시(early) 매핑을 할 수 있도록 준비한다.
  • 코드 라인 13에서 디바이스 트리를 스캔하여 해당 머신에 대한 메모리 정보 및 커멘드 라인 파라메터 정보를 알아온다.
  • 코드 라인 15에서 커멘드 라인 파라메터들 중 early 파라메터에 대한 파싱 및 이에 대응하는 함수를 동작 시킨다.
  • 코드 라인 22에서 현재 cpu의 DAIF 플래그를 복구한다.
    • irq는 현재 시점에서 허용하지 않지만, asynchronous aborts와 fiq는 earlycon 이후로 동작될 수도 있다.
  • 코드 라인 28에서 유저용 페이지 테이블을 가리키는 ttbr0는 부트 업 현재 시점까지 1:1 매핑 영역 페이지 테이블에 연결되어 사용되었다. 여기에서는 현재 cpu가 이러한 1:1 identity 매핑 테이블을 사용하지 않게 한다.
  • 코드 라인 30에서 ARM64의 경우 xen 하이퍼바이저를 위해 따로 준비한 셋업 코드가 없다.
  • 코드 라인 31에서 최근 ARM64 서버에서 EFI 펌웨어를 지원한다. 이의 사용을 위해 준비한다. 서버를 제외한 임베디드에서는 대부분 지원하지 않는다.
  • 코드 라인 32에서 memblock 영역에 reserve할 영역을 등록해둔다.
  • 코드 라인 35에서 커널 영역과 메모리 영역을 모두 정규 매핑한다.
  • 코드 라인 37~40에서 서버 등에서 받은 ACPI 테이블 데이터를 조회하여 관련 메모리 영역 등을 memblock에 reserve 한다.
  • 코드 라인 42~43에서 acpi를 사용하지 않는 시스템의 경우 DTB(FDT)형태로 로드된 디바이스 트리를 객체 형태로 바꾼다.
    • 객체 형태로 바꾸기 전에는 fdt_*() API를 사용하고, 바꾼 후에는 of_*() API도 사용할 수 있다.
  • 코드 라인 45에서 zone 영역을 설정하고, 초기화한다.
  • 코드 라인 47에서 커널 메모리 디버그용 KASAN(Kernel Address SANitizer)을 준비한다.
  • 코드 라인 49에서 memblock에 등록된 reserve 영역들과 메모리 영역을 리소스에 등록해둔다.
  • 코드 라인 51에서 정규 페이징이 동작하는 경우 fixmap을 사용한 early_ioremap()을 사용할 필요가 없으므로 after_paging_init 전역 변수에 1을 대입해둔다.
    • ARM64 아키텍처의 경우에는 정규 페이징이 동작하더라도 early_ioremap()의 사용을 허락한다.
  • 코드 라인 53~56에서 디바이스 트리 또는 ACPI 정보를 통해 절전 관련 ops를 초기화한다.
  • 코드 라인 58에서 디바이스 트리 또는 ACPI 정보를 통해 “enable-method”가 지정된 경우 cpu online/offline을 관리할 ops를 초기화한다.
  • 코드 라인 59에서 디바이스 트리 또는 ACPI 정보를 통해 cpu 노드 정보를 읽어온 후 cpu의 셋업 코드를 수행한다.
  • 코드 라인 60에서 mpidr 레지스터로부터 mpidr 해시를 생성해둔다.
    • cpu의 suspend 진입 시 어셈블리 코드에서 빠르게 이 해시를 사용하여 cpu 로지컬 id를 분류한 후 cpu 레지스터 등의 context 정보를 저장할 때 사용한다.
  • 코드 라인 63에서 KASAN에서 사용할 per-cpu prng_state를 초기화한다.
  • 코드 라인 65~72에서 보안 목적으로 커널이 ttbr0를 통해 유저 영역의 메모리에 직접 접근하지 못하도록 ttbr0 멤버에 zero 페이지를 연결해둔다.
  • 코드 라인 74~76에서 VT(Vertual Terminal)을 지원하는 경우 일단 전역 변수에 더미 콘솔을 지정해둔다.
  • 코드 라인 77~82에서 처음 커널의 head.S에 진입 시 boot_args[]에 저장해둔 x0~x3 레지스터 값들 중 x1 ~ x3 레지스터 값에 0이 아닌 값이 있는 경우 에러 메시지를 출력한다.

 

ARM64 페이지 테이블 -3- (API)

<kernel v5.0>

ARM64 리눅스에서 사용하는 페이지 테이블 관련 명령(pgd, pud, pmd, pte)을 알아본다.

  • ARM64는 2~4 레벨 페이지 테이블을 사용한다.
    • 4 레벨 사용 시: pgd -> pud -> pmd -> pte
    • 3 레벨 사용 시: pgd(pud) -> pmd -> pte
    • 2 레벨 사용 시: pgd(pud, pmd) -> pte
  • 페이지 테이블은 4K 바이트를 사용하고, 각 엔트리는 8바이트이다.

 

엔트리 타입

다음 4 가지 테이블 엔트리들이 사용하는 타입 들(pgdval_t, pudval_t, pmdval_t, pteval_t)은 모두 8바이트를 사용하는 u64 타입이다.

  • pgd_t
    • typedef struct { pgdval_t pgd; } pgd_t;
  • pud_t
    • typedef struct { pudval_t pud; } pud_t;
  • pmd_t
    • typedef struct { pmdval_t pmd; } pmd_t;
  • pte_t
    • typedef struct { pteval_t pte; } pte_t;

 

주요 매크로 상수

다음은 ARM64 커널에서 사용되는 각종 페이지 테이블 관련 매크로 상수 값이다.

 


 

APIs

엔트리 값

p*d_val()

arch/arm64/include/asm/pgtable-types.h

#define pgd_val(x)      ((x).pgd)
#define pud_val(x)      ((x).pud)
#define pmd_val(x)      ((x).pmd)
#define pte_val(x)      ((x).pte)

pgd | pud | pmd | pte 엔트리 값을 반환한다.

 

다음 그림은 p*d_val() 함수의 처리 과정을 보여준다.

 

엔트리 인덱스

p*d_index()

arch/arm64/include/asm/pgtable.h

/* to find an entry in a page-table-directory */
#define pgd_index(addr)         (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
/* Find an entry in the frst-level page table. */
#define pud_index(addr)         (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1))
/* Find an entry in the second-level page table. */
#define pmd_index(addr)         (((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
/* Find an entry in the third-level page table. */
#define pte_index(addr)         (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

주어진 가상 주소에서 pgd | pud | pmd | pte 인덱스 값을 추출하여 리턴한다.

 

다음 그림은 4K 페이지, VA_BITS=39 커널 옵션을 가진 시스템에서 p*d_index() 함수의 처리 과정을 보여준다.


오프셋 (엔트리 포인터)

다음 그림은 엔트리 포인터를 알아오는 다양한 offset API들이 동작하는 과정을 보여준다.

 

pgd_offset()

arch/arm64/include/asm/pgtable.h

#define pgd_offset(mm, addr)    (pgd_offset_raw((mm)->pgd, (addr)))

@mm이 가리키는 pgd 테이블에서 가상 주소 @addr에 해당하는 pgd 엔트리 포인터를 알아온다.

 

pgd_offset_k()

arch/arm64/include/asm/pgtable.h

#define pgd_offset_k(addr)      pgd_offset(&init_mm, addr)

“_k” suffix(접미) 는 커널이라는 뜻이다. 즉 커널이 사용하는 pgd 테이블에서 가상 주소 @addr에 해당하는 pgd 엔트리 포인터를 알아온다.

 

pgd_offset_raw()

arch/arm64/include/asm/pgtable.h

#define pgd_offset_raw(pgd, addr)       ((pgd) + pgd_index(addr))

@pgd 테이블에서 가상 주소 @addr에 해당하는 pgd 엔트리 포인터를 알아온다.

 

pud_offset()

arch/arm64/include/asm/pgtable.h

#define pud_offset(dir, addr)           ((pud_t *)__va(pud_offset_phys((dir), (addr))))

pgd 엔트리 포인터 @dir이 가리키는 pud 테이블에서 @addr에 해당하는 pud 엔트리 포인터를 알아온다.

 

pud_offset_phys()

arch/arm64/include/asm/pgtable.h

#define pud_offset_phys(dir, addr)      (pgd_page_paddr(READ_ONCE(*(dir))) + pud_index(addr) * sizeoo
f(pud_t))

pgd 엔트리 포인터 @dir이 가리키는 pud 테이블에서 @addr에 해당하는 pud 엔트리의 물리 주소를 알아온다.

 

pmd_offset()

arch/arm64/include/asm/pgtable.h

#define pmd_offset(dir, addr)           ((pmd_t *)__va(pmd_offset_phys((dir), (addr))))

pud 엔트리 포인터 @dir이 가리키는 pmd 테이블에서 @addr에 해당하는 pmd 엔트리 포인터를 알아온다.

 

pmd_offset_phys()

arch/arm64/include/asm/pgtable.h

#define pmd_offset_phys(dir, addr)      (pud_page_paddr(READ_ONCE(*(dir))) + pmd_index(addr) * sizeoo
f(pmd_t))

pud 엔트리 포인터 @dir이 가리키는 pmd 테이블에서 @addr에 해당하는 pmd 엔트리의 물리 주소를 알아온다.

 

pte_offset_kernel()

arch/arm64/include/asm/pgtable.h

#define pte_offset_kernel(dir,addr)     ((pte_t *)__va(pte_offset_phys((dir), (addr))))

pmd 엔트리 포인터 @dir이 가리키는 pte 테이블에서 @addr에 해당하는 pte 엔트리 포인터를 알아온다.

 

pte_offset_phys()

arch/arm64/include/asm/pgtable.h

#define pte_offset_phys(dir,addr)       (pmd_page_paddr(READ_ONCE(*(dir))) + pte_index(addr) * sizeoo
f(pte_t))

pmd 엔트리 포인터 @dir이 가리키는 pte 테이블에서 @addr에 해당하는 pte 엔트리의 물리 주소를 알아온다.

 

pte_offset_kimg()

arch/arm64/include/asm/pgtable.h

#define pte_offset_kimg(dir,addr)       ((pte_t *)__phys_to_kimg(pte_offset_phys((dir), (addr))))

static하게 할당된 커널 이미지용 pmd 엔트리 포인터 @dir이 가리키는 pte 테이블에서 @addr에 해당하는 pte 엔트리 포인터를 알아온다.


페이지 변환

다음과 같이 pgd, pud, pmd 및 pte 엔트리 포인터가 가리키는 다음 레벨의 테이블, 블록 및 페이지에 해당하는 페이지 디스크립터를 알아온다.

 

pgd_page()

arch/arm64/include/asm/pgtable.h

#define pgd_page(pgd)           pfn_to_page(__phys_to_pfn(__pgd_to_phys(pgd)))

@pgd 엔트리 포인터에서 읽은 물리 주소에 해당하는 페이지 디스크립터를 알아온다.

  • pgd 엔트리 포인터 -> pgd 엔트리 물리 주소 -> pfn -> page 포인터

 

__pgd_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pgd_to_phys(pgd)      __pte_to_phys(pgd_pte(pgd))

@pgd 엔트리 포인터에서 읽은 pgd 엔트리 값을 물리 주소로 변환해온다.

 

pgd_pte()

arch/arm64/include/asm/pgtable.h

static inline pte_t pgd_pte(pgd_t pgd)
{
        return __pte(pgd_val(pgd));
}

pgd_t 타입을 pte_t 타입으로 변환한다.

 

__pte_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pte_to_phys(pte)      (pte_val(pte) & PTE_ADDR_MASK)

@pgd 엔트리 포인터에서 읽은 pgd 엔트리 값에서 물리 주소 값만을 읽어온다.

  • 위 소스는 52비트 미만의 물리 주소를 사용하는 경우에 한정한다.

 

pud_page()

arch/arm64/include/asm/pgtable.h

#define pud_page(pud)           pfn_to_page(__phys_to_pfn(__pud_to_phys(pud)))

@pud 엔트리 포인터에서 읽은 물리 주소에  해당하는 페이지 디스크립터를 알아온다.

 

__pud_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pud_to_phys(pud)      __pte_to_phys(pud_pte(pud))

@pud 엔트리 포인터에서 읽은 pud 엔트리 값에서 물리 주소 값만을 읽어온다.

 

pud_pte()

arch/arm64/include/asm/pgtable.h

static inline pte_t pud_pte(pud_t pud)
{
        return __pte(pud_val(pud));
}

pud_t 타입을 pte_t 타입으로 변환한다.

 

pmd_page()

arch/arm64/include/asm/pgtable.h

#define pmd_page(pmd)           pfn_to_page(__phys_to_pfn(__pmd_to_phys(pmd)))

@pmd 엔트리 포인터에서 읽은 물리 주소에 해당하는 페이지 디스크립터를 알아온다.

 

__pmd_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pmd_to_phys(pmd)      __pte_to_phys(pmd_pte(pmd))

@pmd 엔트리 포인터에서 읽은 pmd 엔트리 값에서 물리 주소 값만을 읽어온다.

 

pmd_pte()

arch/arm64/include/asm/pgtable.h

static inline pte_t pmd_pte(pmd_t pmd)
{
        return __pte(pmd_val(pmd));
}

pmd_t 타입을 pte_t 타입으로 변환한다.

 

pte_page()

arch/arm64/include/asm/pgtable.h

#define pte_page(pte)           (pfn_to_page(pte_pfn(pte)))

@pte 엔트리 포인터에서 읽은 물리 주소에 해당하는 페이지 디스크립터를 알아온다.

 

pte_pfn()

arch/arm64/include/asm/pgtable.h

#define pte_pfn(pte)            (__pte_to_phys(pte) >> PAGE_SHIFT)

@pte 엔트리 포인터에서 읽은 pte 물리 주소에 해당하는 pfn 값을 반환한다.

 


테이블 엔트리의 범위

pgd_addr_end()

include/asm-generic/pgtable.h

/*
 * When walking page tables, get the address of the next boundary,
 * or the end address of the range if that comes earlier.  Although no
 * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
 */

#define pgd_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;  \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

가상 주소 @addr가 주어질 때 @end 범위내에서 pgd  엔트리가 담당하는 사이즈 단위로 정렬된 끝 주소를 반환한다. 반환 값이 @end를 넘어가면 @end 주소를 반환한다.

 

다음 그림은 4K 페이지, VA_BITS=48 커널 옵션을 사용한 경우 pgd_addr_end() 함수가 차례로 호출될 때 반환되는 값을 보여준다.

  • @addr=0x1234_5678_9000, @end=0x1280_0000_0000
    • 결과는 0x1280_0000_0000
  • @addr=0x1280_0000_0000, @end=0x1300_0000_0000
    • 결과는 0x1300_0000_0000
  • @addr=0x1300_0000_0000, @end=0x1334_5678_9000
    • 결과는 0x1334_5678_9000

 

pud_addr_end()

include/asm-generic/pgtable.h

#define pud_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PUD_SIZE) & PUD_MASK;      \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

가상 주소 @addr가 주어질 때 @end 범위내에서 pud  엔트리가 담당하는 사이즈 단위로 정렬된 끝 주소를 반환한다. 반환 값이 @end를 넘어가면 @end 주소를 반환한다.

 

pmd_addr_end()

include/asm-generic/pgtable.h

#define pmd_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PMD_SIZE) & PMD_MASK;      \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

가상 주소 @addr가 주어질 때 @end 범위내에서 pmd  엔트리가 담당하는 사이즈 단위로 정렬된 끝 주소를 반환한다. 반환 값이 @end를 넘어가면 @end 주소를 반환한다.

 

참고

 

ARM64 페이지 테이블 -2- (매핑)

<kernel v5.0>

ARM64 페이지 테이블 -2- (매핑)

 

ARM64 커널 메모리 맵

ARM64 커널에서 커널용 가상 주소 영역을 좀 더 자세히 알아보면, 그 영역의 절반을 잘라서
2개로 나누어 이용한다. 상위 절반은 물리 메모리를 1:1로 미리 매핑하여 사용한다. 하위 절반은 fixmap, vmalloc, vmemmap, pci-iomap 영역 및 커널 이미지 영역으로 나누어 사용한다.

 

그중에서 fixmap과 vmalloc 영역 동적으로 매핑하여 사용할 수 있는 공간으로 사용한다. 커널 주소 공간을 제외한 그 외 매핑 영역별 용도는 다음과 같다.

  • fixmap: 컴파일 타임에 목적에 따라 가상 주소 공간이 이미 결정된 매핑 영역이다.
  • vmalloc: 런타임에 연속된 가상 주소 공간을 자유롭게 매핑할 수 있는 영역이다.
  • vmalloc( ) 함수가 vmap( ) 함수를 통해 이용하는 곳이고, ioremap( ) 함수 역시 사용하는 영역이다.
  • pci-iomap: PCI 디바이스의 메모리 맵 I/O 영역으로 사용하는 영역이다. pci-ioremap 영역은 PCI 디바이스만 사용하는 데 반해 일반 ioremap 함수를 통해 매핑하는 곳은 vmalloc 영역임을 주의해야 한다.
  • vmemmap: 빠른 접근을 위해 분산된 page 구조체들을 이 영역에 매핑하기 위해 사용한다.

 

다음 그림은 커널용 가상 주소 공간을 반으로 나누어 하위 절반을 각각의 용도로 나눈 모습을 보여준다. 가상 주소 공간의 상위 절반은 물리 메모리를 1:1로 매핑하여 이용하는 영역임을 확인하자. 그리고 그 이하 4개의 매핑 영역 위치도 확인해둔다.

 

페이징 초기화

페이징에 사용하는 커널용 페이지 테이블을 초기화한다. 아직 커널 메모리 영역이 매핑되지 않은 상태이므로 memblock을 사용하여 페이지를 할당받아도 이에 해당하는 가상 주소가 없으므로 곧바로 할당받은 페이지에 접근할 수 없다. 커널의 부트업 과정 초반에는 이렇게 메모리 관리 및 매핑 관리가 동작하기 전 상태이므로 고정 매핑(fixmap) 주소 공간을 활용한 early 매핑에 대한 방법도 제공한다. 이 과정이 완료되면 정규 메모리 할당은 아직 동작하지 않지만, memblock을 이용한 early 메모리 할당이 가능해진다.

 

paging_init() – ARM64

arch/arm64/mm/mmu.c

/*
 * paging_init() sets up the page tables, initialises the zone memory
 * maps and sets up the zero page.
 */
void __init paging_init(void)
{
        pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir));

        map_kernel(pgdp);
        map_mem(pgdp);

        pgd_clear_fixmap();

        cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
        init_mm.pgd = swapper_pg_dir;

        memblock_free(__pa_symbol(init_pg_dir),
                      __pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));

        memblock_allow_resize();
}

커널 코드 및 데이터 영역, DRAM 영역을 커널용 페이지 테이블을 가리키는 swapper_pg_dir에 매핑하고 메모리를 관리하기 위한 메모리 맵(mem_map)을 구성한다.  그런 후 swapper_pg_dir 페이지 테이블 체제를 활성화시킨다.

  • 코드 라인 3에서 커널 페이지 테이블로 사용할 pgd 테이블로 컴파일 타임에 static하게 생성한 &swapper_pg_dir 테이블을 사용하여 fixmap 영역에서 pgd 엔트리에 해당하는 주소에 매핑한다.
  • 코드 라인 5~6에서 커널(코드 및 데이터) 영역과 메모리 영역을 매핑한다.
  • 코드 라인 8에서 fixmap 영역의 pgd 엔트리에 매핑한 pgd 테이블을 매핑 해제한다.
  • 코드 라인 10~11에서 커널용 ttbr1이 swapper_pg_dir을 가리키도록 한다.
  • 코드 라인 13~14에서 임시로 사용한 init_pg_dir 페이지 테이블 영역을 memblock에 할당 해제 한다.
  • 코드 라인 16에서 지금부터 memblock이 확장될 수 있게 설정한다.

 

다음 그림은 커널이 부트 타임 pgd를 사용하다 fixmap을 이용하여 커널과 메모리를 매핑 후 정규 pgd로 전환하는 과정이다.

 

커널 매핑

map_kernel()

arch/arm64/mm/mmu.c

/*
 * Create fine-grained mappings for the kernel.
 */
static void __init map_kernel(pgd_t *pgdp)
{
        static struct vm_struct vmlinux_text, vmlinux_rodata, vmlinux_inittext,
                                vmlinux_initdata, vmlinux_data;

        /*
         * External debuggers may need to write directly to the text
         * mapping to install SW breakpoints. Allow this (only) when
         * explicitly requested with rodata=off.
         */
        pgprot_t text_prot = rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;

        /*
         * Only rodata will be remapped with different permissions later on,
         * all other segments are allowed to use contiguous mappings.
         */
        map_kernel_segment(pgdp, _text, _etext, text_prot, &vmlinux_text, 0,
                           VM_NO_GUARD);
        map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,
                           &vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD);
        map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,
                           &vmlinux_inittext, 0, VM_NO_GUARD);
        map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,
                           &vmlinux_initdata, 0, VM_NO_GUARD);
        map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data, 0, 0);

        if (!READ_ONCE(pgd_val(*pgd_offset_raw(pgdp, FIXADDR_START)))) {
                /*
                 * The fixmap falls in a separate pgd to the kernel, and doesn't
                 * live in the carveout for the swapper_pg_dir. We can simply
                 * re-use the existing dir for the fixmap.
                 */
                set_pgd(pgd_offset_raw(pgdp, FIXADDR_START),
                        READ_ONCE(*pgd_offset_k(FIXADDR_START)));
        } else if (CONFIG_PGTABLE_LEVELS > 3) {
                /*
                 * The fixmap shares its top level pgd entry with the kernel
                 * mapping. This can really only occur when we are running
                 * with 16k/4 levels, so we can simply reuse the pud level
                 * entry instead.
                 */
                BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
                pud_populate(&init_mm,
                             pud_set_fixmap_offset(pgdp, FIXADDR_START),
                             lm_alias(bm_pmd));
                pud_clear_fixmap();
        } else {
                BUG();
        }

        kasan_copy_shadow(pgdp);
}

커널 코드를 의도하지 않은 수정으로부터 보호하고 실행 영역과 비실행 영역 또한 보호하기 위해 커널 코드와 데이터의 읽기 전용 영역과 읽고 쓰기 영역을 나누어 각각의 적절한 매핑 속성으로 매핑한다. 매핑할 페이지 테이블은 요청한 pgd 테이블의 포인터 @pgdp 이며, 이곳에 매핑을 수행한다.

  • 코드 라인 11에서 external 디버거를 사용 시 sw 브레이크 포인터를 사용하여 커널 코드가 매핑된 영역에 직접 기록할 수 있다. 이러한 경우 커널 파라메터에 “rodata=off”를 사용하여야 커널 영역을 read only로 하지 않고 기록도 가능하게 매핑할 수 있다.
  • 코드 라인 17~18에서 커널 이미지의 일반 코드 영역을 커널 실행 페이지 타입으로 매핑한다.
  • 코드 라인 19~20에서 커널 이미지의 읽기 전용 데이터 영역을 커널 페이지 타입으로 매핑한다.
  • 코드 라인 21~22에서 커널 이미지의 초기화 코드 영역을 커널 실행 페이지 타입으로 매핑한다.
  • 코드 라인 23~24에서 커널 이미지의 초기화 데이터 영역을 커널 페이지 타입으로 매핑한다.
  • 코드 라인 25에서 커널 이미지의 일반 데이터 영역을 커널 페이지 타입으로 매핑한다.
  • 코드 라인 27~34에서 요청한 pgd 테이블에 fixmap 주소 영역이 아직 매핑되어 있지 않았다면 커널 페이지 테이블의 fixmap에 해당하는 엔트리를 읽어 매핑한다.
  • 코드 라인 35~46에서 pgd 테이블에 fixmap 주소 영역이 엔트리가 설정된 경우이면서 변환 레벨이 3을 초과한다면 fixmap은 커널용 pgd 테이블에 위치하지 않고 bm_pud[ ]에 위치해 있다. 따라서 fixmap에 pud 테이블을 매핑한 후 pud 엔트리에 bm_pmd[] 테이블을 연결한다. 그런 후 pud 테이블의 매핑을 해제한다.

 

다음과 같이 커널 이미지를 덤프하여 head.text 섹션에 위치한 head.S 어셈블리 커널 코드가 시작됨을 알 수 있다. 또한 .text 섹션은 커널 코드가 담기는 영역이며 .rodata 섹션에는 읽기 전용 데이터가 담긴다.

$ aarch64-linux-gnu-objdump -x vmlinux 
vmlinux:     file format elf64-littleaarch64
vmlinux
architecture: aarch64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0xffff000010080000

Program Header:
    LOAD off    0x0000000000010000 vaddr 0xffff000010080000 paddr 0xffff000010080000 align 2**16
         filesz 0x0000000000a7c7c0 memsz 0x0000000000a7c7c0 flags r-x
    LOAD off    0x0000000000a90000 vaddr 0xffff000010b00000 paddr 0xffff000010b00000 align 2**16
         filesz 0x0000000000831a00 memsz 0x00000000008d91a8 flags rwx
    NOTE off    0x0000000000fc8598 vaddr 0xffff000011038598 paddr 0xffff000011038598 align 2**2
         filesz 0x000000000000003c memsz 0x000000000000003c flags r--
   STACK off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
         filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
private flags = 0:

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .head.text    00001000  ffff000010080000  ffff000010080000  00010000  2**12
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .text         00a7b7c0  ffff000010081000  ffff000010081000  00011000  2**11
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .rodata       004e78a0  ffff000010b00000  ffff000010b00000  00a90000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  3 .pci_fixup    00002170  ffff000010fe78a0  ffff000010fe78a0  00f778a0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 __ksymtab     00009920  ffff000010fe9a10  ffff000010fe9a10  00f79a10  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 __ksymtab_gpl 0000b2c0  ffff000010ff3330  ffff000010ff3330  00f83330  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 __ksymtab_strings 00033801  ffff000010ffe5f0  ffff000010ffe5f0  00f8e5f0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 __param       00003660  ffff000011031df8  ffff000011031df8  00fc1df8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 __modver      00000ba8  ffff000011035458  ffff000011035458  00fc5458  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 __ex_table    00002598  ffff000011036000  ffff000011036000  00fc6000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .notes        0000003c  ffff000011038598  ffff000011038598  00fc8598  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 11 .init.text    000649a4  ffff000011040000  ffff000011040000  00fd0000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  (...생략...)
 29 .debug_loc    00c6b58f  0000000000000000  0000000000000000  0d5146a0  2**0

 

map_kernel_segment()

arch/arm64/mm/mmu.c

static void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,
                                      pgprot_t prot, struct vm_struct *vma,
                                      int flags, unsigned long vm_flags)
{
        phys_addr_t pa_start = __pa_symbol(va_start);
        unsigned long size = va_end - va_start;

        BUG_ON(!PAGE_ALIGNED(pa_start));
        BUG_ON(!PAGE_ALIGNED(size));

        __create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,
                             early_pgtable_alloc, flags);

        if (!(vm_flags & VM_NO_GUARD))
                size += PAGE_SIZE;

        vma->addr       = va_start;
        vma->phys_addr  = pa_start;
        vma->size       = size;
        vma->flags      = VM_MAP | vm_flags;
        vma->caller     = __builtin_return_address(0);

        vm_area_add_early(vma);
}

요청 가상 주소 범위를 가상 주소에 해당하는 물리 주소에 prot 메모리 타입으로 매핑하고, 이 영역은 vm_struct 구조체에 담아 전역 vmlist에 담아둔다.

  • 코드 11~12에서 요청 가상 주소 범위 @va_start ~ @va_end에 해당하는 물리 주소에 prot 메모리 타입으로 매핑한다.
  • 코드 라인 14~23에서 구성된 vm_struct 구조체를 전역 vmlist에 담아둔다. 담아둔 리스트는 나중에 슬랩 메모리 할당자가 활성화된 이후에 vmalloc_init( ) 함수가 호출되면서 이를 활용한다.

 

메모리 매핑

map_mem()

arch/arm64/mm/mmu.c

static void __init map_mem(pgd_t *pgdp)
{
        phys_addr_t kernel_start = __pa_symbol(_text);
        phys_addr_t kernel_end = __pa_symbol(__init_begin);
        struct memblock_region *reg;
        int flags = 0;

        if (rodata_full || debug_pagealloc_enabled())
                flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;

        /*
         * Take care not to create a writable alias for the
         * read-only text and rodata sections of the kernel image.
         * So temporarily mark them as NOMAP to skip mappings in
         * the following for-loop
         */
        memblock_mark_nomap(kernel_start, kernel_end - kernel_start);
#ifdef CONFIG_KEXEC_CORE
        if (crashk_res.end)
                memblock_mark_nomap(crashk_res.start,
                                    resource_size(&crashk_res));
#endif

        /* map all the memory banks */
        for_each_memblock(memory, reg) {
                phys_addr_t start = reg->base;
                phys_addr_t end = start + reg->size;

                if (start >= end)
                        break;
                if (memblock_is_nomap(reg))
                        continue;

                __map_memblock(pgdp, start, end, PAGE_KERNEL, flags);
        }

        /*
         * Map the linear alias of the [_text, __init_begin) interval
         * as non-executable now, and remove the write permission in
         * mark_linear_text_alias_ro() below (which will be called after
         * alternative patching has completed). This makes the contents
         * of the region accessible to subsystems such as hibernate,
         * but protects it from inadvertent modification or execution.
         * Note that contiguous mappings cannot be remapped in this way,
         * so we should avoid them here.
         */
        __map_memblock(pgdp, kernel_start, kernel_end,
                       PAGE_KERNEL, NO_CONT_MAPPINGS);
        memblock_clear_nomap(kernel_start, kernel_end - kernel_start);

#ifdef CONFIG_KEXEC_CORE
        /*
         * Use page-level mappings here so that we can shrink the region
         * in page granularity and put back unused memory to buddy system
         * through /sys/kernel/kexec_crash_size interface.
         */
        if (crashk_res.end) {
                __map_memblock(pgdp, crashk_res.start, crashk_res.end + 1,
                               PAGE_KERNEL,
                               NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS);
                memblock_clear_nomap(crashk_res.start,
                                     resource_size(&crashk_res));
        }
#endif
}

memory memblock에 등록된 각 메모리 영역을 pgd에 매핑한다.

  • 코드 라인 8~9에서 “rodata=full” 커멘드 라인 파라메터가 주어지거나 CONFIG_DEBUG_PAGEALLOC 커널 옵션과 “debug_pagealloc=”이 설정된 경우 커널  영역은 writable 페이지가 된다. 이러한 경우 블럭(huge: pmd 타입 2M 섹션, pud 타입 1G 섹션) 매핑과 리니어 매핑을 하지 못하도록 플래그를 설정한다.
    • CONFIG_DEBUG_PAGEALLOC 및 “debug_pagealloc=”
      •  KASAN(Kernel Address Sanitizer)보다는 빠른 페이지 메모리 할당에 대한 디버그를 사용하게 한다.
      • 참고: Debugging | kernel.org
    • NO_BLOCK_MAPPINGS
      • 블럭(huge) 매핑을 하지 못하게 제한하는 플래그
        • pmd 타입은 2M 섹션
        • pud 타입은 1G 섹션
    • NO_CONT_MAPPINGS
      • 연속된 물리 페이지의 매핑 시 TLB 엔트리의 contiguous 비트를 설정하여 TLB 엔트리를 절약할 수 있는데 이를 못하게 제한하는 플래그
  • 코드 라인 17에서 임시로 커널 memblock 영역을 nomap 플래그를 설정하여 아래 루프에서 매핑하지 못하도록 한다.
  • 코드 라인 18~22에서 crash kernel 영역도 nomap 플래그를 설정한다.
  • 코드 라인 25~30에서 memory memblock에 등록된 각 영역에 대해 루프를 돌며 영역의 시작 주소와 끝 주소를 구한다.
  • 코드 라인 31~32에서 노매핑 플래그가 설정된 영역은 매핑하지 않는다.
  • 코드 라인 34에서 memblock의 start~end 영역을 커널 페이지 속성으로 페이지 테이블에 매핑한다.
  • 코드 라인 47~48에서 커널 영역을 커널 페이지 속성으로 매핑하되 리니어 매핑을 허용하지 않는다.
  • 코드 라인 49에서 커널 memblock 영역의 nomap 플래그를 제거한다.
  • 코드 라인 51~64에서 crash kernel 영역을 커널 페이지로 매핑하되 블럭 매핑과 리니어 매핑을 하지 않는다. 그런 후 memblock에 임시로 설정한 nomap 플래그를 제거한다.

 

다음 그림은 물리 주소의 메모리와 커널이 가상 주소에 각각 매핑되는 것을 알 수 있다.

 

_ _map_memblock()

arch/arm64/mm/mmu.c

static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start,
                                  phys_addr_t end, pgprot_t prot, int flags)
{
        __create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start,
                             prot, early_pgtable_alloc, flags);
}

pgd 페이지 테이블을 가리키는 포인터 @pgdp에 물리 주소 범위 @start ~ @end를 @flags 값을 사용하여 @prot 속성으로 매핑한다.  커널 부트업 타임에 버디 시스템 등의 정규 페이지 할당자가 아직 활성화되지 않았을 때 early 메모리 할당자인 memblock을 사용하여 페이지 테이블을 할당한다.

 

early_pgtable_alloc()

arch/arm64/mm/mmu.c

static phys_addr_t __init early_pgtable_alloc(void)
{
        phys_addr_t phys;
        void *ptr;

        phys = memblock_phys_alloc(PAGE_SIZE, PAGE_SIZE);

        /*
         * The FIX_{PGD,PUD,PMD} slots may be in active use, but the FIX_PTE
         * slot will be free, so we can (ab)use the FIX_PTE slot to initialise
         * any level of table.
         */
        ptr = pte_set_fixmap(phys);

        memset(ptr, 0, PAGE_SIZE);

        /*
         * Implicit barriers also ensure the zeroed page is visible to the page
         * table walker
         */
        pte_clear_fixmap();

        return phys;
}

페이지 테이블 용도로 사용할 싱글 페이지를 할당하여 0으로 초기화한 후 페이지의 물리 주소를 리턴한다. 0으로 초기화할 때 커널이 사용할 메모리들은 아직 매핑되어 있지 않으므로 접근할 수가 없다. 따라서 임시로 가상 주소를 사용하여 0으로 초기화하기 위해 fixmap 영역의 FIX_PTE 주소에 임시로 매핑하는 방법을 사용한다

  • 코드 라인 6에서 커널 페이지 테이블로 사용할 하나의 페이지를 memblock으로부터 할당받는다.
  • 코드 라인 13~23에서 fixmap의 FIX_PTE 주소에 매핑한 후 해당 페이지를 0으로 클리어하고, 다시 매핑 해제한 후 물리 주소를 리턴한다.

 


페이지 테이블 매핑

pgd 테이블에 매핑

create_pgd_mapping()

arch/arm64/mm/mmu.c

void __init create_pgd_mapping(struct mm_struct *mm, phys_addr_t phys,
                               unsigned long virt, phys_addr_t size,
                               pgprot_t prot, bool page_mappings_only)
{
        int flags = 0;

        BUG_ON(mm == &init_mm);

        if (page_mappings_only)
                flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;

        __create_pgd_mapping(mm->pgd, phys, virt, size, prot,
                             pgd_pgtable_alloc, flags);
}

유저 페이지 테이블의 가상 주소 @virt에서 @size에 해당하는 엔트리에 물리 주소 @phy를 @prot 속성으로 매핑한다. 인자로 주어진 @page_mappings_only가 true인 경우 개별 페이지 매핑만 지원한다. 즉 블럭(섹션) 매핑과 리니어 연속 매핑을 지원하지 않는다.

 

__create_pgd_mapping()

arch/arm64/mm/mmu.c

static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
                                 unsigned long virt, phys_addr_t size,
                                 pgprot_t prot,
                                 phys_addr_t (*pgtable_alloc)(void),
                                 int flags)
{
        unsigned long addr, length, end, next;
        pgd_t *pgdp = pgd_offset_raw(pgdir, virt);

        /*
         * If the virtual and physical address don't have the same offset
         * within a page, we cannot map the region as the caller expects.
         */
        if (WARN_ON((phys ^ virt) & ~PAGE_MASK))
                return;

        phys &= PAGE_MASK;
        addr = virt & PAGE_MASK;
        length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

        end = addr + length;
        do {
                next = pgd_addr_end(addr, end);
                alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
                               flags);
                phys += next - addr;
        } while (pgdp++, addr = next, addr != end);
}

페이지 테이블 @pgdir에서 가상 주소 @virt 부터 @size 만큼에 해당하는 pgd 테이블 엔트리에 물리 주소 @phys를 매핑한다. pgd 매핑을 수행하면 그 뒤에 따르는 pmd 및 pte 테이블까지 각 엔트리를 매핑하고 연결하게 된다. 연결되는 하위 페이지 테이블을 할당받아야 할 때 인자로 전달받은 pgtable_alloc( ) 함수를 호출하여 페이지를 할당한다.

  • 코드 라인 8에서 pgd 페이지 테이블 @pgdir 에서 가상 주소 @virt에 해당하는 pgd 엔트리 포인터를 알아온다.
  • 코드 라인 17~21에서 물리 주소를 페이지 단위로 내림 정렬하고, 가상 주소를 페이지 단위로 내림 정렬하여 addr에 설정한다. 그리고 length에 매핑할 페이지 바이트 수를 계산하여 담고, end에 매핑의 끝 가상 주소를 담는다.
  • 코드 라인 22~23에서 다음 처리할 pgd 엔트리를 구해둔다
    • 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pgd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 24~25에서 가상 주소(addr)에 해당하는 pgd 엔트리가 없으면 pud 테이블을 생성하고 이를 가리키게 한다.
  • 코드 라인 26에서 루프를 순회하기 위해 다음에 매핑할 pgd 엔트리의 물리 주소를 구한다.
  • 코드 라인 27에서 다음 pgd 엔트리를 가리키도록 포인터를 증가시키고, 처리할 가상 주소(addr)에 next를 설정한다. 그런 후 매핑이 아직 다 완료되지 않았으면 루프를 돈다.

 

다음 그림은 4K 페이지, VA_BITS = 39를 사용한 구성으로 3 레벨의 페이지 테이블을 사용한다. __create_pgd_mapping() 함수를 통해 virt 주소부터 size만큼 pgd 엔트리를 구성하고 각 pgd 엔트리마다 alloc_init_pud( ) 함수를 호출하는 모습을 보여준다.

  • pgd 엔트리는 pud 엔트리와 동일하며 하나당 1G 가상 공간을 관리한다.

 

다음 그림은 4K 페이지, VA_BITS = 48을 사용한 구성으로 4레벨의 페이지 테이블을 사용한다. __create_pgd_mapping() 함수를 통해 virt 주소부터 size만큼 pgd 엔트리를 구성하고 각 pgd 엔트리마다 alloc_init_pud( ) 함수를 호출하는 모습을 보여준다.

  • pgd 엔트리 하나당 512G 가상 공간을 관리한다.

 

pud 테이블 할당 및 초기화

alloc_init_pud()

arch/arm64/mm/mmu.c

static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
                           phys_addr_t phys, pgprot_t prot,
                           phys_addr_t (*pgtable_alloc)(void),
                           int flags)
{
        unsigned long next;
        pud_t *pudp;
        pgd_t pgd = READ_ONCE(*pgdp);

        if (pgd_none(pgd)) {
                phys_addr_t pud_phys;
                BUG_ON(!pgtable_alloc);
                pud_phys = pgtable_alloc();
                __pgd_populate(pgdp, pud_phys, PUD_TYPE_TABLE);
                pgd = READ_ONCE(*pgdp);
        }
        BUG_ON(pgd_bad(pgd));

        pudp = pud_set_fixmap_offset(pgdp, addr);
        do {
                pud_t old_pud = READ_ONCE(*pudp);

                next = pud_addr_end(addr, end);

                /*
                 * For 4K granule only, attempt to put down a 1GB block
                 */
                if (use_1G_block(addr, next, phys) &&
                    (flags & NO_BLOCK_MAPPINGS) == 0) {
                        pud_set_huge(pudp, phys, prot);

                        /*
                         * After the PUD entry has been populated once, we
                         * only allow updates to the permission attributes.
                         */
                        BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),
                                                      READ_ONCE(pud_val(*pudp))));
                } else {
                        alloc_init_cont_pmd(pudp, addr, next, phys, prot,
                                            pgtable_alloc, flags);

                        BUG_ON(pud_val(old_pud) != 0 &&
                               pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));
                }
                phys += next - addr;
        } while (pudp++, addr = next, addr != end);

        pud_clear_fixmap();
}

물리 주소(phys)를 size만큼 가상 주소(addr)에 해당하는 pud 테이블 엔트리에 매핑한다. pud 매핑을 수행하면 그 뒤에 따르는 pmd 및 pte 테이블까지 각 엔트리를 매핑하고 연결하게 된다. 연결되는 하위 페이지 테이블을 할당받아야 할 때 인자로 전달받은 pgtable_alloc 함수를 호출하여 페이지를 할당한다. 만일 매핑할 주소 범위가 1G 단위에 해당하고 pud 섹션 매핑이 가능한 상태라면 pmd 테이블을 할당받아 연결하지 않고 직접 pud 섹션 페이지 매핑을 수행한다.

  • 코드 라인 8~16에서 pgd 엔트리가 매핑되어 있지 않아 NULL인 경우에는 pud 테이블을 할당받아 연결한다.
  • 코드 라인 19에서 fixmap에 pud 테이블을 매핑한다.
    • fixmap에서 pgd, pud, pmd, pte 테이블용으로 각각의 페이지가 준비되어 있는데, 할당받은 페이지 테이블이 memblock으로부터 막 할당받아 아직 가상 주소에 매핑되어 사용하지 않는 경우 임시로 가상 주소에 매핑시켜 사용할 수 있도록 준비된 페이지로 페이지 테이블의 첫 구성 시에 사용한다.
  • 코드 라인 20~23에서 처리할 pud 엔트리에 대한 범위를 알아온다. 그 다음 주소는 next에 반환한다.
    • 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pud 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 28~37에서 4K 페이지 테이블을 사용하면서 addr, next, phys가 1G 단위로 정렬된 경우 pud 타입 섹션 매핑을 설정한다. 한 번에 1G 페이지가 매핑된다.
  • 코드 라인 38~44에서 가상 주소(addr)에 해당하는 pud 엔트리가 없으면 pmd 테이블을 생성하고 이를 가리키게 한다.
  • 코드 라인 45~46에서 다음 가상 주소 addr에 해당하는 pud 엔트리를 처리하기 위해 루프를 돈다.
  • 코드 라인 48에서 pud용 fixmap 페이지를 매핑 해제한다.

 

다음 그림은 4K 페이지, VA_BITS =39 커널 옵션을 사용한 경우 alloc_init_pud( ) 함수를 통해 addr~end 가상 주소 범위에 해당하는 pud 섹션 페이지가 매핑되거나 alloc_init_pmd( ) 함수를 호출하는 과정을 보여준다(최종 pmd 및 pte 테이블 구성은 생략)

 

다음 그림은 4K 페이지, VA_BITS =48 커널 옵션을 사용한 경우 alloc_init_pud( ) 함수를 통해 addr~end 가상 주소 범위에 해당하는 pud 섹션 페이지가 매핑되거나 alloc_init_pmd( ) 함수를 호출하는 과정을 보여준다(최종 pmd 및 pte 테이블 구성은 생략)

 

pmd 테이블 할당 및 초기화

alloc_init_cont_pmd()

arch/arm64/mm/mmu.c

static void alloc_init_cont_pmd(pud_t *pudp, unsigned long addr,
                                unsigned long end, phys_addr_t phys,
                                pgprot_t prot,
                                phys_addr_t (*pgtable_alloc)(void), int flags)
{
        unsigned long next;
        pud_t pud = READ_ONCE(*pudp);

        /*
         * Check for initial section mappings in the pgd/pud.
         */
        BUG_ON(pud_sect(pud));
        if (pud_none(pud)) {
                phys_addr_t pmd_phys;
                BUG_ON(!pgtable_alloc);
                pmd_phys = pgtable_alloc();
                __pud_populate(pudp, pmd_phys, PUD_TYPE_TABLE);
                pud = READ_ONCE(*pudp);
        }
        BUG_ON(pud_bad(pud));

        do {
                pgprot_t __prot = prot;

                next = pmd_cont_addr_end(addr, end);

                /* use a contiguous mapping if the range is suitably aligned */
                if ((((addr | next | phys) & ~CONT_PMD_MASK) == 0) &&
                    (flags & NO_CONT_MAPPINGS) == 0)
                        __prot = __pgprot(pgprot_val(prot) | PTE_CONT);

                init_pmd(pudp, addr, next, phys, __prot, pgtable_alloc, flags);

                phys += next - addr;
        } while (addr = next, addr != end);
}

가상 주소(addr)에 해당하는 pmd 테이블 엔트리에 물리 주소(phys)를 size만큼 매핑한다. 만일 매핑할 주소 범위가 섹션 단위에 해당하는 pmd 섹션 매핑이 가능한 상태라면, pte 테이블을 할당받아 연결하지 않고 직접 pmd 섹션 페이지 매핑을 수행한다.

  • 코드 라인 13~19에서 pud 엔트리가 매핑되어 있지 않아 NULL이거나 pud 섹션 페이지 매핑(64K 페이지가 아니면서 3레벨 이상의 변환 테이블에서만 유효)된 경우 pmd 테이블을 할당받아 연결한다.
  • 코드 라인 22~25에서 처리할 pmd 엔트리에 대한 범위를 알아온다. 그 다음 주소는 next에 반환한다.
    • 루프를 돌며 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pmd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 28~30에서 addr, next, phys가 섹션 단위로 정렬된 경우 PMD 타입 섹션 매핑을 하도록 PTE_CONT 플래그를 설정한다.
  • 코드 라인 32에서 pmd 엔트리 아래에 연결된 pte 테이블을 생성하고 이를 가리키게 한다.
  • 코드 라인 34~35에서 다음 가상 주소 addr에 해당하는 pmd 엔트리를 처리하기 위해 루프를 돈다.

 

다음 그림은 4K 페이지, VA_BITS =39 커널 옵션을 사용한 경우 alloc_init_cont_pmd( ) 함수를 통해 addr~end 가상 주소에 해당하는 pmd 엔트리들에 pmd 섹션 페이지가 매핑되거나 alloc_init_pte( ) 함수를 호출하는 과정을 보여준다(최종 pte 테이블 구성은 생략).

 

다음 그림은 4K 페이지, VA_BITS =48 커널 옵션을 사용한 경우 alloc_init_cont_pmd( ) 함수를 통해 addr~end 가상 주소에 해당하는 pmd 엔트리들에 pmd 섹션 페이지가 매핑되거나 alloc_init_pte( ) 함수를 호출하는 과정을 보여준다(최종 pte 테이블 구성은 생략).

 

init_pmd()

arch/arm64/mm/mmu.c

static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end,
                     phys_addr_t phys, pgprot_t prot,
                     phys_addr_t (*pgtable_alloc)(void), int flags)
{
        unsigned long next;
        pmd_t *pmdp;

        pmdp = pmd_set_fixmap_offset(pudp, addr);
        do {
                pmd_t old_pmd = READ_ONCE(*pmdp);

                next = pmd_addr_end(addr, end);

                /* try section mapping first */
                if (((addr | next | phys) & ~SECTION_MASK) == 0 &&
                    (flags & NO_BLOCK_MAPPINGS) == 0) {
                        pmd_set_huge(pmdp, phys, prot);

                        /*
                         * After the PMD entry has been populated once, we
                         * only allow updates to the permission attributes.
                         */
                        BUG_ON(!pgattr_change_is_safe(pmd_val(old_pmd),
                                                      READ_ONCE(pmd_val(*pmdp))));
                } else {
                        alloc_init_cont_pte(pmdp, addr, next, phys, prot,
                                            pgtable_alloc, flags);

                        BUG_ON(pmd_val(old_pmd) != 0 &&
                               pmd_val(old_pmd) != READ_ONCE(pmd_val(*pmdp)));
                }
                phys += next - addr;
        } while (pmdp++, addr = next, addr != end);

        pmd_clear_fixmap();
}

요청한 가상 주소  @addr ~ @end 범위의 pmd 엔트리에 대해 초기화를 수행한다. 필요시 pmd 섹션 블럭이나 pte 테이블을 생성하여 연결한다.

  • 코드 라인 8에서 처리할 pmd 테이블을 fixmap에 매핑하고 가상주소 @addr에 해당하는 pmd 엔트리 포인터를 알아온다.
  • 코드 라인 9~10에서 루프를 돌며 pmd 엔트리 값을 읽어 old_pmd에 저장해둔다.
  • 코드 라인 12에서 다음 처리할 pmd 엔트리를 구해둔다.
    • 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pmd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 15~24에서 addr, next, phys가 섹션 단위로 정렬된 경우 PMD 타입 섹션 매핑을 하도록 PTE_CONT 플래그를 설정한다.
  • 코드 라인 25~31에서 pte 테이블을 할당하고 범위 내의 페이지들을 매핑한다.
  • 코드 라인 32~33에서 다음 처리할 pmd 엔트리를 위해 루프를 돈다.
  • 코드 라인 35에서 pmd 테이블을 fixmap에서 해제한다.

 

pte 테이블 할당 및 초기화

alloc_init_cont_pte()

arch/arm64/mm/mmu.c

static void alloc_init_cont_pte(pmd_t *pmdp, unsigned long addr,
                                unsigned long end, phys_addr_t phys,
                                pgprot_t prot,
                                phys_addr_t (*pgtable_alloc)(void),
                                int flags)
{
        unsigned long next;
        pmd_t pmd = READ_ONCE(*pmdp);

        BUG_ON(pmd_sect(pmd));
        if (pmd_none(pmd)) {
                phys_addr_t pte_phys;
                BUG_ON(!pgtable_alloc);
                pte_phys = pgtable_alloc();
                __pmd_populate(pmdp, pte_phys, PMD_TYPE_TABLE);
                pmd = READ_ONCE(*pmdp);
        }
        BUG_ON(pmd_bad(pmd));

        do {
                pgprot_t __prot = prot;

                next = pte_cont_addr_end(addr, end);

                /* use a contiguous mapping if the range is suitably aligned */
                if ((((addr | next | phys) & ~CONT_PTE_MASK) == 0) &&
                    (flags & NO_CONT_MAPPINGS) == 0)
                        __prot = __pgprot(pgprot_val(prot) | PTE_CONT);

                init_pte(pmdp, addr, next, phys, __prot);

                phys += next - addr;
        } while (addr = next, addr != end);
}

가상 주소(addr)에 해당하는 pte 테이블 엔트리에 물리 주소(phys)를 size만큼 매핑한다. 만일 매핑할 주소 범위에 따라 연속 pte 매핑이 기능도 사용할 수 있다.

  • 코드 라인 8~17에서 pmd 엔트리가 매핑되어 있지 않아 NULL인 경우 pte 테이블을 할당받아 연결한다.
  • 코드 라인 20~23에서 루프를 돌며 다음 처리할 pmd 엔트리를 구해둔다
    • 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pmd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 26~28에서 addr, next, phys가 다음과 같은 연속 pte 단위로 정렬된 경우 연속 페이지 매핑을 할 수 있도록 PTE_CONT 플래그를 설정한다.
    • 4K 페이지를 사용하는 경우 4K 페이지 * 16 = 64KB 단위
    • 16K 페이지를 사용하는 경우 16K 페이지 * 128 = 2MB 단위
    • 64K 페이지를 사용하는 경우 64K 페이지 * 32 = 2MB 단위
  • 코드 라인 30에서 pte 엔트리와 해당 범위의 페이지들을 매핑한다.
  • 코드 라인 32~33에서 다음 처리할 pmd 엔트리를 위해 루프를 돈다.

 

다음 그림은 4K 페이지, VA_BITS =39 커널 옵션을 사용한 경우 alloc_init_pte( ) 함수를 통해 addr~end 가상 주소에 해당하는 pte 엔트리를 구성하고 최종 페이지와 매핑하는 것을 보여준다.

 

다음 그림은 4K 페이지, VA_BITS =48 커널 옵션을 사용한 경우 alloc_init_pte( ) 함수를 통해 addr~end 가상 주소에 해당하는 pte 엔트리를 구성하고 최종 페이지와 매핑하는 것을 보여준다.

 

init_pte()

arch/arm64/mm/mmu.c

static void init_pte(pmd_t *pmdp, unsigned long addr, unsigned long end,
                     phys_addr_t phys, pgprot_t prot)
{
        pte_t *ptep;

        ptep = pte_set_fixmap_offset(pmdp, addr);
        do {
                pte_t old_pte = READ_ONCE(*ptep);

                set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot));

                /*
                 * After the PTE entry has been populated once, we
                 * only allow updates to the permission attributes.
                 */
                BUG_ON(!pgattr_change_is_safe(pte_val(old_pte),
                                              READ_ONCE(pte_val(*ptep))));

                phys += PAGE_SIZE;
        } while (ptep++, addr += PAGE_SIZE, addr != end);

        pte_clear_fixmap();
}

요청한 가상 주소  @addr ~ @end 범위의 pte 엔트리를 페이지와 매핑한다.

  • 코드 라인 6에서 처리할 pte 테이블을 fixmap에 매핑하고 가상주소 @addr에 해당하는 pte 엔트리 포인터를 알아온다.
  • 코드 라인 7~9에서 루프를 돌며 pte 엔트리 값을 읽어 old_pte에 저장해둔다.
  • 코드 라인 11에서 pte 엔트리에 매핑할 페이지에 해당하는 물리 주소와 속성을 기록하여 매핑한다.
  • 코드 라인 19~20에서 다음 처리할 pmd 엔트리를 위해 루프를 돈다.
  • 코드 라인 22에서 pte 테이블을 fixmap에서 해제한다.

 


 

테이블 연결(population)

pgd_populate()

arch/arm64/include/asm/pgalloc.h

static inline void pgd_populate(struct mm_struct *mm, pgd_t *pgdp, pud_t *pudp)
{
        __pgd_populate(pgdp, __pa(pudp), PUD_TYPE_TABLE);
}

pgd 페이지 테이블 엔트리 포인터 @pgdp에 다음 레벨의 pud 테이블을 연결한다.

 

__pgd_populate()

arch/arm64/include/asm/pgalloc.h

static inline void __pgd_populate(pgd_t *pgdp, phys_addr_t pudp, pgdval_t prot)
{
        set_pgd(pgdp, __pgd(__phys_to_pgd_val(pudp) | prot));
}

pgd 페이지 테이블 엔트리 포인터 @pgdp에 다음 레벨의 pud 테이블을 연결할 때 @prot 속성을 사용한다.

 

pud_populate()

arch/arm64/include/asm/pgalloc.h

static inline void pud_populate(struct mm_struct *mm, pud_t *pudp, pmd_t *pmdp)
{
        __pud_populate(pudp, __pa(pmdp), PMD_TYPE_TABLE);
}

pud 페이지 테이블 엔트리 포인터 @pudp에 다음 레벨의 pmd 테이블을 연결한다.

 

__pud_populate()

arch/arm64/include/asm/pgalloc.h

static inline void __pud_populate(pud_t *pudp, phys_addr_t pmdp, pudval_t prot)
{
        set_pud(pudp, __pud(__phys_to_pud_val(pmdp) | prot));
}

pud 페이지 테이블 엔트리 포인터 @pudp에 다음 레벨의 pmd 테이블을 연결할 때 @prot 속성을 사용한다.

 

pmd_populate()

arch/arm64/include/asm/pgalloc.h

static inline void
pmd_populate(struct mm_struct *mm, pmd_t *pmdp, pgtable_t ptep)
{
        __pmd_populate(pmdp, page_to_phys(ptep), PMD_TYPE_TABLE);
}

pmd 페이지 테이블 엔트리 포인터 @pmdp에 다음 레벨의 pte 테이블을 연결한다.

 

__pmd_populate()

arch/arm64/include/asm/pgalloc.h

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t ptep,
                                  pmdval_t prot)
{
        set_pmd(pmdp, __pmd(__phys_to_pmd_val(ptep) | prot));
}

pmd 페이지 테이블 엔트리 포인터 @pmdp에 다음 레벨의 pte 테이블을 연결할 때 @prot 속성을 사용한다.

 


테이블 엔트리 매핑/해제

테이블 엔트리 매핑

set_pgd()

arch/arm64/include/asm/pgtable.h

static inline void set_pgd(pgd_t *pgdp, pgd_t pgd)
{
        if (in_swapper_pgdir(pgdp)) {
                set_swapper_pgd(pgdp, pgd);
                return;
        }

        WRITE_ONCE(*pgdp, pgd);
        dsb(ishst);
}

pgd 테이블 엔트리 포인터 @pgdp에 @pgd 값을 기록하여 매핑한다.

  • 커널 영역은 내부 공유(inner share) 영역에 포함된 cpu들이 공유하여 사용 중인 영역이다. 그러므로 이 cpu들을 한꺼번에 TLB 및 인스트럭션 캐시 등에 대해 동기화하는 작업이 필요하다. 따라서 TLB 캐시 작업이 완료될 때까지 기다리도록 dsb 배리어를 수행한다.

 

set_swapper_pgd()

arch/arm64/mm/mmu.c

void set_swapper_pgd(pgd_t *pgdp, pgd_t pgd)
{
        pgd_t *fixmap_pgdp;

        spin_lock(&swapper_pgdir_lock);
        fixmap_pgdp = pgd_set_fixmap(__pa_symbol(pgdp));
        WRITE_ONCE(*fixmap_pgdp, pgd);
        /*
         * We need dsb(ishst) here to ensure the page-table-walker sees
         * our new entry before set_p?d() returns. The fixmap's
         * flush_tlb_kernel_range() via clear_fixmap() does this for us.
         */
        pgd_clear_fixmap();
        spin_unlock(&swapper_pgdir_lock);
}

 

set_pud()

arch/arm64/include/asm/pgtable.h

static inline void set_pud(pud_t *pudp, pud_t pud)
{
#ifdef __PAGETABLE_PUD_FOLDED
        if (in_swapper_pgdir(pudp)) {
                set_swapper_pgd((pgd_t *)pudp, __pgd(pud_val(pud)));
                return;
        }
#endif /* __PAGETABLE_PUD_FOLDED */

        WRITE_ONCE(*pudp, pud);

        if (pud_valid(pud))
                dsb(ishst);
}

pud 테이블 엔트리 포인터 @pudp에 @pud 값을 기록하여 매핑한다.

 

set_pmd()

arch/arm64/include/asm/pgtable.h

static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
{
#ifdef __PAGETABLE_PMD_FOLDED
        if (in_swapper_pgdir(pmdp)) {
                set_swapper_pgd((pgd_t *)pmdp, __pgd(pmd_val(pmd)));
                return;
        }
#endif /* __PAGETABLE_PMD_FOLDED */

        WRITE_ONCE(*pmdp, pmd);

        if (pmd_valid(pmd))
                dsb(ishst);
}

pmd 테이블 엔트리 포인터 @pmdp에 @pmd 값을 기록하여 매핑한다.

 

set_pte()

arch/arm64/include/asm/pgtable.h

static inline void set_pte(pte_t *ptep, pte_t pte)
{
        WRITE_ONCE(*ptep, pte);

        /*
         * Only if the new pte is valid and kernel, otherwise TLB maintenance
         * or update_mmu_cache() have the necessary barriers.
         */
        if (pte_valid_not_user(pte))
                dsb(ishst);
}

pte 테이블 엔트리 포인터 @ptep에 @pte 값을 기록하여 매핑한다.

 

테이블 엔트리 매핑 해제

pgd_clear()

arch/arm64/include/asm/pgtable.h

static inline void pgd_clear(pgd_t *pgdp)
{
        set_pgd(pgdp, __pgd(0));
}

@pgdp 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pud_clear()

arch/arm64/include/asm/pgtable.h

static inline void pud_clear(pud_t *pudp)
{
        set_pud(pudp, __pud(0));
}

@pudp 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pmd_clear()

arch/arm64/include/asm/pgtable.h

static inline void pmd_clear(pmd_t *pmdp)
{
        set_pmd(pmdp, __pmd(0));
}

@pmdp 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pte_clear()

arch/arm64/include/asm/pgtable.h

#define pte_clear(mm,addr,ptep) set_pte(ptep, __pte(0))

@ptet 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pte 엔트리 활성화 시 메모리 타입 속성

set_pte( ) 함수에서 물리 주소를 매핑할 때 메모리 타입 속성을 추가하여 매핑한 페이지에 대해 캐시 속성을 지정할 수 있다.

ARM64에서 캐시 속성은 ARM에서와 달리 단순하게 normal 메모리 타입과 device 타입을 두었다.

  • Normal 타입
    • 메모리 타입에 사용하고, 캐시 policy를 적용할 수 있다.
  • Device 타입
    • 예측 접근을 허용하지 않고, 캐시를 사용하지 않는 특징이 있다.

 

다음과 같은 타입의 매크로 상수 값을 사용한다.

  • MT_DEVICE_nGnRnE
    • ARM에서의 strongly-ordered 같은 타입으로 버퍼 및 캐시를 사용할 수 없어 디바이스에서 가장 느린 특성을 갖고 있다.
  • MT_DEVICE_nGnRE
    • ARM에서의 device 타입처럼 버퍼 및 캐시를 사용하지 않는다.
  • MT_DEVICE_GRE
    • ARMv8에서 새로 소개된 device 타입으로, 예측(predict) 접근을 허용하지 않고, 캐시를 사용하지 않는 normal 메모리와 유사하다.
  • MT_NORMAL_NC
    • 버퍼만 사용하고 캐시는 사용하지 않는 타입이다.
  • MT_NORMAL
    • 버퍼 및 캐시를 사용하는 타입이다.
  • MT_NORMAL_WT
    • 버퍼 및 일부 기능 제한된 캐시(write-through)를 사용한다.

 

디바이스 타입에 사용하는 세 가지 타입을 알아본다. n이 앞에 붙으면 non을 의미한다.

  • G(Gather)
    •  성능을 높이기 위해 다중 액세스를 병합하여 하나의 버스 트랜잭션으로 처리하는 것을 허용한다.
    • 명시적으로 요청한 사이즈와 횟수만큼의 데이터 기록을 위해 버스에 전달될 때 한 번에 요청할 수 있다.
      • 예) 1 바이트 문자를 이어진 주소에서 연달아 4번 기록하라고 했고, 쓰기 버퍼에서 이러한 요청을 4 바이트 워드로 병합하여 한 번에 처리한다.
  • R(Reorder)
    • 성능을 높이기 위해 버스를 통해 같은 디바이스에 요청될 때 프로그램 순서가 재정렬되는 것을 허용한다.
  • E(Early Write Acknowledgement)
    • 성능을 높이기 위해 버스에 정상 요청한 경우 응답을 기다리지 않고 다음 처리를 위해 완료한다.
      • 예) 높은 신뢰성을 가진 버스와 디바이스에서 사용되는 방법이다.

 

참고