DPDKにおけるhugepageの初期化部分

DPDKはhugepageをデフォルトで利用しています.ここではDPDKにおけるhugepageの初期化部分についてまとめようと思います. OSはLinux , DPDKのバージョンはv17.05-rc2です.

初期化のおおまかな流れ

librte_eal/linuxapp/eal/eal.c:rte_eal_init()がDPDKの基本的な初期化処理をおこないますが,この中でhugepageに関してはまず librte_eal/linuxap/eal/eal_hugepages_info.c:eal_hugepage_info_init() を呼びます. ここでは /sys/kernel/mm/hugepages/ の中のファイルからhugepageの基礎情報を読み取ります.その後 librte_eal/common/eal_common_memory.c:rte_eal_memory_init()を呼びますが,この中でlibrte_eal/linuxapp/eal/eal_memory.c:rte_eal_hugepage_init()が呼ばれます. このrte_eal_hugepage_init() でhugepage領域の確保をおこないます.

rte_eal_hugepage_init()

rte_eal_hugepage_init()の最初に何をするか書いてあります.

/*
 * Prepare physical memory mapping: fill configuration structure with
 * these infos, return 0 on success.
 *  1. map N huge pages in separate files in hugetlbfs
 *  2. find associated physical addr
 *  3. find associated NUMA socket ID
 *  4. sort all huge pages by physical address
 *  5. remap these N huge pages in the correct order
 *  6. unmap the first mapping
 *  7. fill memsegs in configuration with contiguous zones
 */

このコメントの通りですが,要するに

  • 最初にhugetlb上にhugepageと同じサイズのN個のファイルを作成し,それをメモリ上にマップする
  • マップしたhugetlbファイルの物理アドレスを求める
  • 物理アドレスが連続のものを仮想アドレスが連続になるように再マッピングする

ということをおこないます.

rte_eal_hugepage_init()はエラーハンドリングであったり複数アーキテクチャ対応のため若干長いですが,主要部分だけ抜粋すると以下のようになります.

http://dpdk.org/browse/dpdk/tree/lib/librte_eal/linuxapp/eal/eal_memory.c?h=v17.05-rc2#n964

    ....
    pages_new = map_all_hugepages(&tmp_hp[hp_offset], hpi, 1);
    ...
    find_physaddrs(&tmp_hp[hp_offset], hpi);
    ...
    qsort(&tmp_hp[hp_offset], hpi->num_pages[0],
          sizeof(struct hugepage_file), cmp_physaddr);
    ...
    map_all_hugepages(&tmp_hp[hp_offset], hpi, 0);
    ...

ここで,

をしています.

まず,map_all_hugepages()は以下のようになっています.

static unsigned
map_all_hugepages(struct hugepage_file *hugepg_tbl,
        struct hugepage_info *hpi, int orig)
{
    int fd;
    unsigned i;
    void *virtaddr;
    void *vma_addr = NULL;
    size_t vma_len = 0;

    for (i = 0; i < hpi->num_pages[0]; i++) {
        uint64_t hugepage_sz = hpi->hugepage_sz;
        ...

        hugepg_tbl[i].file_id = i;
        hugepg_tbl[i].size = hugepage_sz;
        eal_get_hugefile_path(hugepg_tbl[i].filepath,
                sizeof(hugepg_tbl[i].filepath), hpi->hugedir,
                hugepg_tbl[i].file_id);
        hugepg_tbl[i].filepath[sizeof(hugepg_tbl[i].filepath) - 1] = '\0';
        ...

        fd = open(hugepg_tbl[i].filepath, O_CREAT | O_RDWR, 0600);
        ...

        virtaddr = mmap(vma_addr, hugepage_sz, PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_POPULATE, fd, 0);
        hugepg_tbl[i].orig_va = virtaddr;
        ...

        *(int *)virtaddr = 0;
        ...
        flock(fd, LOCK_SH | LOCK_NB);
            RTE_LOG(DEBUG, EAL, "%s(): Locking file failed:%s \n",
        ...
        vma_addr = (char *)vma_addr + hugepage_sz;
        vma_len -= hugepage_sz;
    }

    return i;
}

最初は orig=1 なので orig=1 の処理のみ抜粋しています(エラーハンドリングは省略). hpi->num_pages[0]sys/kernel/mm/hugepages/hugepages-xxx/nr_hugepagesの値が入っており,その数だけhugetlb上にファイルを作成し,mmap()マッピング,仮想アドレスを hugepg_tbl[i].orig_va に格納します.

その後,rte_eal_hugepage_init() では find_physaddrs() で仮想アドレスから物理アドレスをもとめ,さらにそれを物理アドレスでソートします.仮想アドレスから物理アドレスを求める方法は,前のエントリに書いたとおりです.

物理アドレスを求めソートが終わったら,もう一度 map_allhugepaegs()を呼びます.ただし,今回はorig=0です. orig=0の場合の処理を抜粋すると以下のようになります.

http://dpdk.org/browse/dpdk/tree/lib/librte_eal/linuxapp/eal/eal_memory.c?h=v17.05-rc2#n393

static unsigned
map_all_hugepages(struct hugepage_file *hugepg_tbl,
        struct hugepage_info *hpi, int orig)
{
    int fd;
    unsigned i;
    void *virtaddr;
    void *vma_addr = NULL;
    size_t vma_len = 0;

    for (i = 0; i < hpi->num_pages[0]; i++) {
        uint64_t hugepage_sz = hpi->hugepage_sz;

        if (vma_len == 0) {
             unsigned j, num_pages;

             /* reserve a virtual area for next contiguous
              * physical block: count the number of
              * contiguous physical pages. */
             for (j = i+1; j < hpi->num_pages[0] ; j++) {
                if (hugepg_tbl[j].physaddr !=
                     hugepg_tbl[j-1].physaddr + hugepage_sz)
                     break;
             }

            num_pages = j - i;
            vma_len = num_pages * hugepage_sz;

            /* get the biggest virtual memory area up to
             * vma_len. If it fails, vma_addr is NULL, so
             * let the kernel provide the address. */
            vma_addr = get_virtual_area(&vma_len, hpi->hugepage_sz);
            if (vma_addr == NULL)
            vma_len = hugepage_sz;
        }
        ...
        fd = open(hugepg_tbl[i].filepath, O_CREAT | O_RDWR, 0600);
        ...
        virtaddr = mmap(vma_addr, hugepage_sz, PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_POPULATE, fd, 0);
        ...
        hugepg_tbl[i].final_va = virtaddr;
        ...
        flock(fd, LOCK_SH | LOCK_NB) == -1)
        ...
        close(fd);
        ...

        vma_addr = (char *)vma_addr + hugepage_sz;
        vma_len -= hugepage_sz;
    }

    return i;
}

ループの中で,まず最初は vma_len == 0 の条件が成立し,if文の中が実行されます.ここで連続した物理アドレスの長さがvma_lenに設定されます.その後get_virtual_area()を使いvma_lenの長さ以上の使用可能な仮想アドレス空間を求め,仮想アドレスが正しく求められたらそれを hugepg_tbl[i].final_va に設定しています.

get_virtual_area()は以下のようになっています.

http://dpdk.org/browse/dpdk/tree/lib/librte_eal/linuxapp/eal/eal_memory.c?h=v17.05-rc2#n312

static void *
get_virtual_area(size_t *size, size_t hugepage_sz)
{
    ...
    fd = open("/dev/zero", O_RDONLY);
    ...
    addr = mmap(addr, (*size) + hugepage_sz, PROT_READ, MAP_PRIVATE, fd, 0);
    ...
    aligned_addr = (long)addr;
    aligned_addr += (hugepage_sz - 1);
    aligned_addr &= (~(hugepage_sz - 1));
    addr = (void *)(aligned_addr);
    ...
    return addr;
}

やっていることは単純で, 求めたいサイズ(をhugepage境界にアラインしたもの)だけのアドレス空間mmap()で取得し,成功したならそのアドレスを返します.

rte_eal_hugepage_init()では最終的に rte_mem_config構造体の mcfgにメモリ情報を格納しています.memseg[j]が一つの連続した領域を表します.

    mcfg->memseg[j].phys_addr = hugepage[i].physaddr;
    mcfg->memseg[j].addr = hugepage[i].final_va;
    mcfg->memseg[j].len = hugepage[i].size;
    mcfg->memseg[j].socket_id = hugepage[i].socket_id;
    mcfg->memseg[j].hugepage_sz = hugepage[i].size;

またrte_eal_hugepage_init()ではこの他にNUMA環境の場合の初期化処理等もおこなっています.

全体的に,少々泥臭いことをしているというか,このあたりカーネル側でもう少しサポートがあれば綺麗にかけそうです(まぁ普通はユーザ側で物理アドレスが必要なことなんてないわけですが).

hugepageを使わない場合

DPDK(というかEAL)では,--no-hugeというオプションを渡すと,内部のinternal_config.no_hugetlbfsが1になります. rte_eal_hugepage_init()では一番最初にこのオプションをチェックしていてhugetableが無効の場合は以下の処理をおこないます.

if (internal_config.no_hugetlbfs) {
    addr = mmap(NULL, internal_config.memory, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
    ...
    mcfg->memseg[0].phys_addr = (phys_addr_t)(uintptr_t)addr;
    mcfg->memseg[0].addr = addr;
    mcfg->memseg[0].hugepage_sz = RTE_PGSIZE_4K;
    mcfg->memseg[0].len = internal_config.memory;
    mcfg->memseg[0].socket_id = 0;
    return 0;
}

hugepageを使わない代わりにmmap()で一括でメモリを取得し,それをmemseg[0]に設定します.このときmemseg[0].phy_addr物理アドレスは格納していません (そもそも仮にphys_addrを求めて格納したとしても後続の仮想アドレス空間に対応する物理アドレスが連続している保証はどこにもありません). ということで,--no-hugeオプションを渡した場合はDPDKが正常に利用できないような気がします.少なくとも,自分の環境(X540+VFIO)では動きませんでした.

2017-05-19 追記 改めてソースを眺めていたらmempoolの初期化部分でhugepageでない場合はrte_mempool_popoiulate_virt()を使ってなるべく連続した領域を割り当てようとしていることに気づきました. http://dpdk.org/browse/dpdk/tree/lib/librte_mempool/rte_mempool.c?h=v17.05#n451 ということでhugepageなしでもうまくいけば環境によっては動作するのかもしれません.