DPDKにおけるメモリ管理

DPDKにおいてどのようにメモリが使用されるかをここにまとめたいと思います. なお,ここに書いてあるものはあくまで私がコードを読んで解釈した結果になります. DPDKのバージョンは17.05,OSはLinuxを対象としています.

全体像

DPDKでは独自のメモリアロケータを利用してメモリを管理しています. メモリ管理のために以下のようなデータ構造が利用されます.

  • memconfig : 作成したmemsegやmemzoneなどの情報を保持
  • memseg : 連続した物理メモリ領域情報を保持
  • heap : メモリの空きブロックの管理 / 割り当て.memsegのメモリ領域を使用する.
  • memzone : 連続した物理メモリ領域を名前をつけて割り当て.内部でheapを利用
  • mempool : 固定長メモリアロケータ.内部でmemzoneを利用

前提知識: queue(3)

本題に入る前に,DPDKでは内部で複数のリストを保持していますが,このリストには queue(3)を利用しています. queueについて知っておいた方がコードが読みやすいです.

queueにはSLIST,STAILQおよびLISTとTAILQの4種類があります. SLISTとSTAILQが単方向リスト,LISTとTAILQが連結リストで,TAILQの場合は末尾への挿入が可能です.queueは全てマクロとして実装されています.

TAILQの簡単な使用例は以下のようになります.

struct entry{
    TAILQ_ENTRY(entry) tq;
    int data;
} e1, e2, e3;

TAILQ_HEAD(entry_head, entry);

int main(){
    struct entry_head head;
    TAILQ_INIT(&head);

    e1.data = 1;
    e2.data = 2;
    e3.data = 3;

    TAILQ_INSERT_HEAD(&head, &e1, tq);
    TAILQ_INSERT_HEAD(&head, &e2, tq);
    TAILQ_INSERT_TAIL(&head, &e3, tq);

    struct entry* p = TAILQ_FIRST(&head);
    while(p != NULL){
        printf("%d\n", p->data);
        p = TAILQ_NEXT(p, tq);
    }
}

初期化部分がやや分かりにくいですが,TAILQ_ENTRY()TAILQ_HEAD()が以下のよ うに展開されることが理解できれば納得できると思います.

#define _TAILQ_HEAD(name, type, qual)                   \
struct name {                               \
    qual type *tqh_first;       /* first element */     \
    qual type *qual *tqh_last;  /* addr of last next element */ \
}
#define TAILQ_HEAD(name, type)  _TAILQ_HEAD(name, struct type,)

#define _TAILQ_ENTRY(type, qual)                    \
struct {                                \
    qual type *tqe_next;        /* next element */      \
    qual type *qual *tqe_prev;  /* address of previous next element */\
}
#define TAILQ_ENTRY(type)   _TAILQ_ENTRY(struct type,)

基本的にマクロにはポインタを渡します. queueの実装は全てヘッダファイルに記述してあるので,見てみると参考になると思います.

glibcの実装: https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=misc/sys/queue.h;hb=HEAD

ちなみに,TAILQの実装を読む際は,

  • tqe_prev は 前の要素の tqe_next のアドレスを保持している
    • これを利用して要素を挿入した際に前の要素のtqe_nextを書き換える
  • ある要素の前の要素を取得する際は,前の要素のteq_prevを使う
    • 実質的に前の要素のtqe_nextを参照していることになる

ということを抑えておけば理解が容易だと思います.

memconfig

rte_mem_configは,EALの中でメモリ全体の情報を保持しているデータ構造です. 具体的には作成したmemsegやmemzone,heap等(後述)を保持しています. EALの中ではrte_eal_get_configuration()->mem_configからmemconfigを取得できます.

librte_eal/common/rte_eal_memconfig.h:

struct rte_mem_config {
    volatile uint32_t magic;   /**< Magic number - Sanity check. */

    /* memory topology */
    uint32_t nchannel;    /**< Number of channels (0 if unknown). */
    uint32_t nrank;       /**< Number of ranks (0 if unknown). */

    /**
     * current lock nest order
     *  - qlock->mlock (ring/hash/lpm)
     *  - mplock->qlock->mlock (mempool)
     * Notice:
     *  *ALWAYS* obtain qlock first if having to obtain both qlock and mlock
     */
    rte_rwlock_t mlock;   /**< only used by memzone LIB for thread-safe. */
    rte_rwlock_t qlock;   /**< used for tailq operation for thread safe. */
    rte_rwlock_t mplock;  /**< only used by mempool LIB for thread-safe. */

    uint32_t memzone_cnt; /**< Number of allocated memzones */

    /* memory segments and zones */
    struct rte_memseg memseg[RTE_MAX_MEMSEG];    /**< Physmem descriptors. */
    struct rte_memzone memzone[RTE_MAX_MEMZONE]; /**< Memzone descriptors. */

    struct rte_tailq_head tailq_head[RTE_MAX_TAILQ]; /**< Tailqs for objects */

    /* Heaps of Malloc per socket */
    struct malloc_heap malloc_heaps[RTE_MAX_NUMA_NODES];

    /* address of mem_config in primary process. used to map shared config into
     * exact same address the primary process maps it.
     */
    uint64_t mem_cfg_addr;
} __attribute__((__packed__));

初期化

DPDKのプログラムで最初にrte_eal_init()を呼ぶと様々な初期化処理がおこなわれますが, その中でも rte_eal_memory_init()でmemsegの初期化,rte_eal_memzone_init()で memzone及びheapの初期化がおこなわれます.

memseg

memsegは連続した物理アドレス空間の情報を保持するデータ構造です.

librte_eal/common/include/rte_memory.h:

struct rte_memseg {
    phys_addr_t phys_addr;      /**< Start physical address. */
    RTE_STD_C11
    union {
        void *addr;         /**< Start virtual address. */
        uint64_t addr_64;   /**< Makes sure addr is always 64 bits */
    };
    size_t len;               /**< Length of the segment. */
    uint64_t hugepage_sz;       /**< The pagesize of underlying memory */
    int32_t socket_id;          /**< NUMA socket ID. */
    uint32_t nchannel;          /**< Number of channels. */
    uint32_t nrank;             /**< Number of ranks. */
#ifdef RTE_LIBRTE_XEN_DOM0
     /**< store segment MFNs */
    uint64_t mfn[DOM0_NUM_MEMBLOCK];
#endif
}

phys_addrに先頭の物理アドレスが,lenに長さが格納されています. memsegの初期化は rte_eal_init() => rte_eal_memory_init() => rte_eal_hugepage_init() の中でおこなわれます.

具体的な物理アドレス確保の方法は前回のエントリに書いた通りです : DPDKにおけるhugepageの初期化部分. なるべく物理的に連続した領域を確保しようとします.

heap

連続したメモリの空きブロックの管理にはheapを利用します.

librte_eal/common/malloc_heap.h:

struct malloc_heap {
     rte_spinlock_t lock;
     LIST_HEAD(, malloc_elem) free_head[RTE_HEAP_NUM_FREELISTS];
     unsigned alloc_count;
     size_t total_size;
 } __rte_cache_aligned;

struct malloc_elem {
    struct malloc_heap *heap;
    struct malloc_elem *volatile prev;      /* points to prev elem in memseg */
    LIST_ENTRY(malloc_elem) free_list;      /* list of free elements in heap */
    const struct rte_memseg *ms;
    volatile enum elem_state state;
    uint32_t pad;
    size_t size;
#ifdef RTE_LIBRTE_MALLOC_DEBUG
    uint64_t header_cookie;         /* Cookie marking start of data */
                                    /* trailer cookie at start + size */
#endif
} __rte_cache_aligned;

malloc_elemが一つのメモリブロックで,それはqueue(3)のLISTの一要素となっています. LISTの先頭はmalloc_heap構造体のfree_headからアクセス可能です. フリーリストはメモリブロックのサイズの大きさごとに分けられています(後述のmalloc_elem_free_list_index()参照). またmalloc_heap自体はmemconfigのmalloc_heapsからアクセスできます.

heapの初期化はrte_eal_memory_init()のあとに呼ばれるrte_eal_memzone_init() の中のrte_malloc_heap_init()でおこなわれます.

rte_malloc_heap_init()ではmalloc_heap_add_memseg()を呼び,各memsegをheapに 追加します.ここでは一つのmemseg全体が一つのmalloc_elemとして追加されます.

librte_eal/common/malloc_heap.c:

static void
malloc_heap_add_memseg(struct malloc_heap *heap, struct rte_memseg *ms)
{
    /* allocate the memory block headers, one at end, one at start */
    struct malloc_elem *start_elem = (struct malloc_elem *)ms->addr;
    struct malloc_elem *end_elem = RTE_PTR_ADD(ms->addr,
            ms->len - MALLOC_ELEM_OVERHEAD);
    end_elem = RTE_PTR_ALIGN_FLOOR(end_elem, RTE_CACHE_LINE_SIZE);
    const size_t elem_size = (uintptr_t)end_elem - (uintptr_t)start_elem;

    malloc_elem_init(start_elem, heap, ms, elem_size);
    malloc_elem_mkend(end_elem, start_elem);
    malloc_elem_free_list_insert(start_elem);

    heap->total_size += elem_size;
}

librte_eal/common/malloc_elem.c:

void
malloc_elem_init(struct malloc_elem *elem,
        struct malloc_heap *heap, const struct rte_memseg *ms, size_t size)
{
    elem->heap = heap;
    elem->ms = ms;
    elem->prev = NULL;
    memset(&elem->free_list, 0, sizeof(elem->free_list));
    elem->state = ELEM_FREE;
    elem->size = size;
    elem->pad = 0;
    set_header(elem);
    set_trailer(elem);
}

ここでは各memsegの先頭と末尾にmalloc_elem用の領域を確保してそれを初期化しています. 末尾にもmalloc_elemを用意するのはオーバフロー防止のためのようです. malloc_elemのsizeはmalloc_elem構造体自身を含んだサイズのようです.

malloc_elem_free_list_insert()で作成したmalloc_elemmaloc_heapfree_headで先頭が示されるフリーリストに追加します. このフリーリストはメモリブロックの大きさごとに分かれており,どこのフリーリスト に入れるかはmalloc_elem_free_list_index()で決めています. コメントを読む限り,

heap->free_head[0] - (0   , 2^8]
heap->free_head[1] - (2^8 , 2^10]
heap->free_head[2] - (2^10 ,2^12]
heap->free_head[3] - (2^12, 2^14]
...

というような感じに分けられて格納されるようです.

memzone

memzoneは連続した物理アドレス空間に名前をつけて保持するためのデータ構造です. 取得したmemzoneはmemconfig.memzoneに保持されます. memzoneの確保の際に内部的にはheapからメモリを確保します.

librte_eal/common/include/rte_memzone.h:

struct rte_memzone {

#define RTE_MEMZONE_NAMESIZE 32       /**< Maximum length of memory zone name.*/
    char name[RTE_MEMZONE_NAMESIZE];  /**< Name of the memory zone. */

    phys_addr_t phys_addr;            /**< Start physical address. */
    RTE_STD_C11
    union {
        void *addr;                   /**< Start virtual address. */
        uint64_t addr_64;             /**< Makes sure addr is always 64-bits */
    };
    size_t len;                       /**< Length of the memzone. */

    uint64_t hugepage_sz;             /**< The page size of underlying memory */

    int32_t socket_id;                /**< NUMA socket ID. */

    uint32_t flags;                   /**< Characteristics of this memzone. */
    uint32_t memseg_id;               /**< Memseg it belongs. */
} __attribute__((__packed__));

rte_eal_memzone_init()の中ではmemzoneは空に初期化されるだけです.

heapからのメモリの割り当て

heapからの割り当てはmalloc_heap_alloc()からおこないます.

librte_eal/common/malloc_heap.c:

void *
malloc_heap_alloc(struct malloc_heap *heap,
        const char *type __attribute__((unused)), size_t size, unsigned flags,
        size_t align, size_t bound)
{
    struct malloc_elem *elem;

    size = RTE_CACHE_LINE_ROUNDUP(size);
    align = RTE_CACHE_LINE_ROUNDUP(align);

    rte_spinlock_lock(&heap->lock);

    elem = find_suitable_element(heap, size, flags, align, bound);
    if (elem != NULL) {
        elem = malloc_elem_alloc(elem, size, align, bound);
        /* increase heap's count of allocated elements */
        heap->alloc_count++;
    }
    rte_spinlock_unlock(&heap->lock);

    return elem == NULL ? NULL : (void *)(&elem[1]);
}

ここではまずfind_suitable_element()を利用して,ヒープのフリーリストからズの空きブロックを見つけます. やっていることはフリーリストを先頭から順に辿って行って,十分な大きさの空きブロックが見つかればそれを返します.ただしこの際指定したhugepage要件を満たすかチェックしています.

librte_eal/common/malloc_elem.c:

static struct malloc_elem *
find_suitable_element(struct malloc_heap *heap, size_t size,
        unsigned flags, size_t align, size_t bound)
{
    size_t idx;
    struct malloc_elem *elem, *alt_elem = NULL;

    for (idx = malloc_elem_free_list_index(size);
            idx < RTE_HEAP_NUM_FREELISTS; idx++) {
        for (elem = LIST_FIRST(&heap->free_head[idx]);
                !!elem; elem = LIST_NEXT(elem, free_list)) {
            if (malloc_elem_can_hold(elem, size, align, bound)) {
                if (check_hugepage_sz(flags, elem->ms->hugepage_sz))
                    return elem;
                if (alt_elem == NULL)
                    alt_elem = elem;
            }
        }
    }

    if ((alt_elem != NULL) && (flags & RTE_MEMZONE_SIZE_HINT_ONLY))
        return alt_elem;

    return NULL;
}

その後実際のmalloc_elem_alloc()で割り当てを行います.

struct malloc_elem *
malloc_elem_alloc(struct malloc_elem *elem, size_t size, unsigned align,
        size_t bound)
{
    struct malloc_elem *new_elem = elem_start_pt(elem, size, align, bound);
    const size_t old_elem_size = (uintptr_t)new_elem - (uintptr_t)elem;
    const size_t trailer_size = elem->size - old_elem_size - size -
        MALLOC_ELEM_OVERHEAD;

    elem_free_list_remove(elem);

割り当てはelemの末尾から指定したサイズ分だけ取得して割り当てます. まずelem_start_pt()で新しいオブジェクトの先頭のアドレスを取得します.

    if (trailer_size > MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) {
        /* split it, too much free space after elem */
        struct malloc_elem *new_free_elem =
                RTE_PTR_ADD(new_elem, size + MALLOC_ELEM_OVERHEAD);

        split_elem(elem, new_free_elem);
        malloc_elem_free_list_insert(new_free_elem);
    }

あんまり詳しく説明していませんが,割り当ての際に指定した境界に合うようにアラインメントしているので,その際にnew_elemの後にMIN_DATA_SIZE以上(自分の環境では64byte)の領域ができたらそれをフリーリストに追加しています*1

    if (old_elem_size < MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) {
        /* don't split it, pad the element instead */
        elem->state = ELEM_BUSY;
        elem->pad = old_elem_size;


        /* put a dummy header in padding, to point to real element header */
        if (elem->pad > 0){ /* pad will be at least 64-bytes, as everything
                             * is cache-line aligned */
            new_elem->pad = elem->pad;
            new_elem->state = ELEM_PAD;
            new_elem->size = elem->size - elem->pad;
            set_header(new_elem);
        }

        return new_elem;
    }

分割した際に,空きスペースがあまりにも小さい場合はそれをフリーリストに追加せ ず,ただ単にELEM_BUSYとするようです.

    /* we are going to split the element in two. The original element
     * remains free, and the new element is the one allocated.
     * Re-insert original element, in case its new size makes it
     * belong on a different list.
     */
    split_elem(elem, new_elem);
    new_elem->state = ELEM_BUSY;
    malloc_elem_free_list_insert(elem);

    return new_elem;
}

通常の場合はこのsplit_elem()で領域が分割され,利用しない前半部分はまたフリーリストに追加されます.

static void
split_elem(struct malloc_elem *elem, struct malloc_elem *split_pt)
{
    struct malloc_elem *next_elem = RTE_PTR_ADD(elem, elem->size);
    const size_t old_elem_size = (uintptr_t)split_pt - (uintptr_t)elem;
    const size_t new_elem_size = elem->size - old_elem_size;

    malloc_elem_init(split_pt, elem->heap, elem->ms, new_elem_size);
    split_pt->prev = elem;
    next_elem->prev = split_pt;
    elem->size = old_elem_size;
    set_trailer(elem);
}

図にすると以下のような感じだと思います.

f:id:mm_i:20170524154902p:plain

malloc_heap_allog()では最後(void *)(&elem[1])を返します.つまり,返されたアドレスはmalloc_elemのヘッダ部分を除いたアドレスになります.

memzoneの確保

メモリ領域を名前をつけて管理する場合はmemzoneを利用します. memzoneの確保はrte_memzone_reserve()からおこないます.

rte_memzone_reserve()を呼ぶと,最終的にmemzone_reserve_aligned_thread_unsafe()が呼ばれます. memzone_reserve_aligned_thread_unsafe()はそこそこ長いですが,主要部のみ抜き 出すと以下のようになります.

static const struct rte_memzone *
memzone_reserve_aligned_thread_unsafe(const char *name, size_t len,
        int socket_id, unsigned flags, unsigned align, unsigned bound)
{
    struct rte_memzone *mz;
    struct rte_mem_config *mcfg;
    size_t requested_len;
    int socket, i;

    ...

    /* get pointer to global configuration */
    mcfg = rte_eal_get_configuration()->mem_config;

    ...
    /* allocate memory on heap */
    void *mz_addr = malloc_heap_alloc(&mcfg->malloc_heaps[socket], NULL,
            requested_len, flags, align, bound);
    ...

    const struct malloc_elem *elem = malloc_elem_from_data(mz_addr);

    /* fill the zone in config */
    mz = get_next_free_memzone();
    ...

    mcfg->memzone_cnt++;
    snprintf(mz->name, sizeof(mz->name), "%s", name);
    mz->phys_addr = rte_malloc_virt2phy(mz_addr);
    mz->addr = mz_addr;
    mz->len = (requested_len == 0 ? elem->size : requested_len);
    mz->hugepage_sz = elem->ms->hugepage_sz;
    mz->socket_id = elem->ms->socket_id;
    mz->flags = 0;
    mz->memseg_id = elem->ms - rte_eal_get_configuration()->mem_config->memseg;

    return mz;
}

malloc_heap_alloc()からメモリを確保したあと,その情報をmemconfig.memzone[]に登録しています.

mempool

mempoolは固定長サイズのオブジェクトのアロケータです. フリーのオブジェクトはring構造で保持されます. mempoolでは各コアが占有して利用できるキャッシュを提供する機能などもあります. mempoolを作成する際,確保したいオブジェクトのサイズ及びその個数を指定しますが, 内部的にはそのオブジェクトが割り当てられるだけのmemzoneを取得します.

struct rte_mempool {
    /*
     * Note: this field kept the RTE_MEMZONE_NAMESIZE size due to ABI
     * compatibility requirements, it could be changed to
     * RTE_MEMPOOL_NAMESIZE next time the ABI changes
     */
    char name[RTE_MEMZONE_NAMESIZE]; /**< Name of mempool. */
    RTE_STD_C11
    union {
        void *pool_data;         /**< Ring or pool to store objects. */
        uint64_t pool_id;        /**< External mempool identifier. */
    };
    void *pool_config;               /**< optional args for ops alloc. */
    const struct rte_memzone *mz;    /**< Memzone where pool is alloc'd. */
    int flags;                       /**< Flags of the mempool. */
    int socket_id;                   /**< Socket id passed at create. */
    uint32_t size;                   /**< Max size of the mempool. */
    uint32_t cache_size;
    /**< Size of per-lcore default local cache. */

    uint32_t elt_size;               /**< Size of an element. */
    uint32_t header_size;            /**< Size of header (before elt). */
    uint32_t trailer_size;           /**< Size of trailer (after elt). */

    unsigned private_data_size;      /**< Size of private data. */
    /**
     * Index into rte_mempool_ops_table array of mempool ops
     * structs, which contain callback function pointers.
     * We're using an index here rather than pointers to the callbacks
     * to facilitate any secondary processes that may want to use
     * this mempool.
     */

    int32_t ops_index;

    struct rte_mempool_cache *local_cache; /**< Per-lcore local cache */

    uint32_t populated_size;         /**< Number of populated objects. */
    struct rte_mempool_objhdr_list elt_list; /**< List of objects in pool */
    uint32_t nb_mem_chunks;          /**< Number of memory chunks */
    struct rte_mempool_memhdr_list mem_list; /**< List of memory chunks */

#ifdef RTE_LIBRTE_MEMPOOL_DEBUG
    /** Per-lcore statistics. */
    struct rte_mempool_debug_stats stats[RTE_MAX_LCORE];
#endif
}  __rte_cache_aligned;

実際にmempoolで確保したオブジェクトはelt_listで先頭が示されるSTAILQで保持されます. また,確保した連続したメモリ領域はmem_listで先頭が示されるSTAILQで保持されます.

mempoolの初期化

mempoolの確保は以下のようにしておこなわれます.

  1. mempool構造体自体を格納する領域を一つのmemzoneとして取得
  2. 固定長のオブジェクトをn個格納するための領域を取得. この際領域は複数のmemzoneにまたがることがある.

mempoolの作成はrte_mempool_create()からおこないます. rte_mampool_create()は以下のようになっています.

librte_mempool/rte_mempool.c:

struct rte_mempool *
rte_mempool_create(const char *name, unsigned n, unsigned elt_size,
    unsigned cache_size, unsigned private_data_size,
    rte_mempool_ctor_t *mp_init, void *mp_init_arg,
    rte_mempool_obj_cb_t *obj_init, void *obj_init_arg,
    int socket_id, unsigned flags)
{
    int ret;
    struct rte_mempool *mp;

    mp = rte_mempool_create_empty(name, n, elt_size, cache_size,
        private_data_size, socket_id, flags);
    if (mp == NULL)
        return NULL;

    /*
     * Since we have 4 combinations of the SP/SC/MP/MC examine the flags to
     * set the correct index into the table of ops structs.
     */
    if ((flags & MEMPOOL_F_SP_PUT) && (flags & MEMPOOL_F_SC_GET))
        ret = rte_mempool_set_ops_byname(mp, "ring_sp_sc", NULL);
    else if (flags & MEMPOOL_F_SP_PUT)
        ret = rte_mempool_set_ops_byname(mp, "ring_sp_mc", NULL);
    else if (flags & MEMPOOL_F_SC_GET)
        ret = rte_mempool_set_ops_byname(mp, "ring_mp_sc", NULL);
    else
        ret = rte_mempool_set_ops_byname(mp, "ring_mp_mc", NULL);

    if (ret)
        goto fail;

    /* call the mempool priv initializer */
    if (mp_init)
        mp_init(mp, mp_init_arg);

    if (rte_mempool_populate_default(mp) < 0)
        goto fail;

    /* call the object initializers */
    if (obj_init)
        rte_mempool_obj_iter(mp, obj_init, obj_init_arg);

    return mp;

 fail:
    rte_mempool_free(mp);
    return NULL;
}

nameがmempoolの名前,nが割り当てたいオブジェクトの数,elt_sizeが各オブジェクトのサイズになります. 基本的にはrte_mempool_create_emtpy()で空のmempoolを作成し,その後rte_mempool_populate_default()で実際にオブジェクト分のメモリを確保します. また,rte_mempool_set_ops_byname()のところは,mempoolでオブジェクトを確保するスレッドが一つかどうかと解放するスレッドが一つかどうか(SC: Single Consumer, SP: Single Producer)のフラグによって,mempoolからオブジェクトを取得/解放する関数を切り替えて登録しています.

rte_mempool_create_empty()の主要部は以下のようになっています.

librte_mempool/rte_mempool.c:

/* create an empty mempool */
struct rte_mempool *
rte_mempool_create_empty(const char *name, unsigned n, unsigned elt_size,
    unsigned cache_size, unsigned private_data_size,
    int socket_id, unsigned flags)
{
    ...

    mempool_list = RTE_TAILQ_CAST(rte_mempool_tailq.head, rte_mempool_list);

    ...

    mempool_size = MEMPOOL_HEADER_SIZE(mp, cache_size);
    mempool_size += private_data_size;
    mempool_size = RTE_ALIGN_CEIL(mempool_size, RTE_MEMPOOL_ALIGN);

    ret = snprintf(mz_name, sizeof(mz_name), RTE_MEMPOOL_MZ_FORMAT, name);
    if (ret < 0 || ret >= (int)sizeof(mz_name)) {
        rte_errno = ENAMETOOLONG;
        goto exit_unlock;
    }
    mz = rte_memzone_reserve(mz_name, mempool_size, socket_id, mz_flags);
    if (mz == NULL)
        goto exit_unlock;

    /* init the mempool structure */
    mp = mz->addr;

    memset(mp, 0, MEMPOOL_HEADER_SIZE(mp, cache_size));
    ret = snprintf(mp->name, sizeof(mp->name), "%s", name);
    if (ret < 0 || ret >= (int)sizeof(mp->name)) {
        rte_errno = ENAMETOOLONG;
        goto exit_unlock;
    }
    mp->mz = mz;
    mp->size = n;
    mp->flags = flags;
    mp->socket_id = socket_id;
    mp->elt_size = objsz.elt_size;
    mp->header_size = objsz.header_size;
    mp->trailer_size = objsz.trailer_size;
    /* Size of default caches, zero means disabled. */
    mp->cache_size = cache_size;
    mp->private_data_size = private_data_size;
    STAILQ_INIT(&mp->elt_list);
    STAILQ_INIT(&mp->mem_list);

    ...
    return mp;
}

mempool自体もTAILQで管理されています.mempool_listはそのTAILQのheadを指すデータ構造です. rte_mempool_create_empty()では,mempoool構造体自体を格納するためのメモリ領域をrte_memzone_reserve()で確保し,初期化します. この時点ではまだオブジェクトのメモリ領域は確保していません.

実際にオブジェクトのメモリを確保するのはrte_mempool_populate_default()になります.

librte_mempool/rte_mempool.c:

int
rte_mempool_populate_default(struct rte_mempool *mp)
{
    int mz_flags = RTE_MEMZONE_1GB|RTE_MEMZONE_SIZE_HINT_ONLY;
    char mz_name[RTE_MEMZONE_NAMESIZE];
    const struct rte_memzone *mz;
    size_t size, total_elt_sz, align, pg_sz, pg_shift;
    phys_addr_t paddr;
    unsigned mz_id, n;
    int ret;

    /* mempool must not be populated */
    if (mp->nb_mem_chunks != 0){
        RTE_LOG(ERR, MBUF, "a\n");
        return -EEXIST;
    }

    if (rte_xen_dom0_supported()) {
        pg_sz = RTE_PGSIZE_2M;
        pg_shift = rte_bsf32(pg_sz);
        align = pg_sz;

    } else if (rte_eal_has_hugepages()) {
        pg_shift = 0; /* not needed, zone is physically contiguous */
        pg_sz = 0;
        align = RTE_CACHE_LINE_SIZE;
    } else {
        pg_sz = getpagesize();
        pg_shift = rte_bsf32(pg_sz);
        align = pg_sz;
    }

    total_elt_sz = mp->header_size + mp->elt_size + mp->trailer_size;
    for (mz_id = 0, n = mp->size; n > 0; mz_id++, n -= ret) {
        size = rte_mempool_xmem_size(n, total_elt_sz, pg_shift);

        ret = snprintf(mz_name, sizeof(mz_name),
            RTE_MEMPOOL_MZ_FORMAT "_%d", mp->name, mz_id);
        if (ret < 0 || ret >= (int)sizeof(mz_name)) {
            RTE_LOG(ERR, MBUF, "b\n");
            ret = -ENAMETOOLONG;
            goto fail;
        }

        mz = rte_memzone_reserve_aligned(mz_name, size,
            mp->socket_id, mz_flags, align);
        /* not enough memory, retry with the biggest zone we have */
        if (mz == NULL)
            mz = rte_memzone_reserve_aligned(mz_name, 0,
                mp->socket_id, mz_flags, align);
        if (mz == NULL) {
            RTE_LOG(ERR, MBUF, "c\n");
            ret = -rte_errno;
            goto fail;
        }

        if (mp->flags & MEMPOOL_F_NO_PHYS_CONTIG)
            paddr = RTE_BAD_PHYS_ADDR;
        else
            paddr = mz->phys_addr;

        if (rte_eal_has_hugepages() && !rte_xen_dom0_supported())
            ret = rte_mempool_populate_phys(mp, mz->addr,
                paddr, mz->len,
                rte_mempool_memchunk_mz_free,
                (void *)(uintptr_t)mz);
        else
            ret = rte_mempool_populate_virt(mp, mz->addr,
                mz->len, pg_sz,
                rte_mempool_memchunk_mz_free,
                (void *)(uintptr_t)mz);
        if (ret < 0) {
            RTE_LOG(ERR, MBUF, "d\n");
            rte_memzone_free(mz);
            goto fail;
        }
    }

    return mp->size;

 fail:
    rte_mempool_free_memchunks(mp);
    return ret;

重要なのはforループの内部の処理で,rte_memzone_reserve_aligned()を利用して確 保したいメモリ(オブジェクトサイズ ×個数分のメモリ)をmemzoneから確保しようとします. このときもし確保できなかった場合(それだけ連続するメモリがなかった場合)は, memzoneから最も大きな連続する領域を取得します. この後通常はrte_eal_has_hugepages() = 1, rte_xen_dom0_supported() = 0なのでrte_mempool_populate_phys()を呼び,確保したメモリ上でオブジェクトのブロックを作成します. rte_mempool_populate_phys()の戻り値が確保できたオブジェクトの数です. 目標のオブジェクト数だけ確保できるまでこの操作を繰り返します.

rte_mempool_popoulate_phys()の中では,確保したメモリ領域からオブジェクトを切り出し,それをmempool_add_elem()を使ってmempool.elt_listに入れ,また空きオブジェクトを管理するring bufferに入れています.また,確保したメモリ(memzone)をmempool.mem_listに登録します.

int
rte_mempool_populate_phys(struct rte_mempool *mp, char *vaddr,
    phys_addr_t paddr, size_t len, rte_mempool_memchunk_free_cb_t *free_cb,
    void *opaque)
{
    unsigned total_elt_sz;
    unsigned i = 0;
    size_t off;
    struct rte_mempool_memhdr *memhdr;
    int ret;

    /* create the internal ring if not already done */
    if ((mp->flags & MEMPOOL_F_POOL_CREATED) == 0) {
        ret = rte_mempool_ops_alloc(mp);
        if (ret != 0)
            return ret;
        mp->flags |= MEMPOOL_F_POOL_CREATED;
    }

    /* mempool is already populated */
    if (mp->populated_size >= mp->size)
        return -ENOSPC;

    total_elt_sz = mp->header_size + mp->elt_size + mp->trailer_size;

    memhdr = rte_zmalloc("MEMPOOL_MEMHDR", sizeof(*memhdr), 0);
    if (memhdr == NULL)
        return -ENOMEM;

    memhdr->mp = mp;
    memhdr->addr = vaddr;
    memhdr->phys_addr = paddr;
    memhdr->len = len;
    memhdr->free_cb = free_cb;
    memhdr->opaque = opaque;

    if (mp->flags & MEMPOOL_F_NO_CACHE_ALIGN)
        off = RTE_PTR_ALIGN_CEIL(vaddr, 8) - vaddr;
    else
        off = RTE_PTR_ALIGN_CEIL(vaddr, RTE_CACHE_LINE_SIZE) - vaddr;

    while (off + total_elt_sz <= len && mp->populated_size < mp->size) {
        off += mp->header_size;
        if (paddr == RTE_BAD_PHYS_ADDR)
            mempool_add_elem(mp, (char *)vaddr + off,
                RTE_BAD_PHYS_ADDR);
        else
            mempool_add_elem(mp, (char *)vaddr + off, paddr + off);
        off += mp->elt_size + mp->trailer_size;
        i++;
    }

    /* not enough room to store one object */
    if (i == 0)
        return -EINVAL;

    STAILQ_INSERT_TAIL(&mp->mem_list, memhdr, next);
    mp->nb_mem_chunks++;
    return i;
}
static void
mempool_add_elem(struct rte_mempool *mp, void *obj, phys_addr_t physaddr)
{
    struct rte_mempool_objhdr *hdr;
    struct rte_mempool_objtlr *tlr __rte_unused;

    /* set mempool ptr in header */
    hdr = RTE_PTR_SUB(obj, sizeof(*hdr));
    hdr->mp = mp;
    hdr->physaddr = physaddr;
    STAILQ_INSERT_TAIL(&mp->elt_list, hdr, next);
    mp->populated_size++;

#ifdef RTE_LIBRTE_MEMPOOL_DEBUG
    hdr->cookie = RTE_MEMPOOL_HEADER_COOKIE2;
    tlr = __mempool_get_trailer(obj);
    tlr->cookie = RTE_MEMPOOL_TRAILER_COOKIE;
#endif

    /* enqueue in ring */
    rte_mempool_ops_enqueue_bulk(mp, &obj, 1);
}

rte_mempool_ops_enqueue_bulk()では,最終的には次で説明するenqueue()が呼ばれます.

mempoolの操作

rte_create_mempol_empty()のところで説明したとおり,初期化時のフラグによって mempoolからオブジェクトを取得/解放する際の関数を切り替えています. 関数の定義は drivers/mempool/ring/rte_mempool_ring.cに書いてあります.

static const struct rte_mempool_ops ops_mp_mc = {
    .name = "ring_mp_mc",
    .alloc = common_ring_alloc,
    .free = common_ring_free,
    .enqueue = common_ring_mp_enqueue,
    .dequeue = common_ring_mc_dequeue,
    .get_count = common_ring_get_count,
};

static const struct rte_mempool_ops ops_sp_sc = {
    .name = "ring_sp_sc",
    .alloc = common_ring_alloc,
    .free = common_ring_free,
    .enqueue = common_ring_sp_enqueue,
    .dequeue = common_ring_sc_dequeue,
    .get_count = common_ring_get_count,
};

static const struct rte_mempool_ops ops_mp_sc = {
    .name = "ring_mp_sc",
    .alloc = common_ring_alloc,
    .free = common_ring_free,
    .enqueue = common_ring_mp_enqueue,
    .dequeue = common_ring_sc_dequeue,
    .get_count = common_ring_get_count,
};


static const struct rte_mempool_ops ops_sp_mc = {
    .name = "ring_sp_mc",
    .alloc = common_ring_alloc,
    .free = common_ring_free,
    .enqueue = common_ring_sp_enqueue,
    .dequeue = common_ring_mc_dequeue,
    .get_count = common_ring_get_count,
};

MEMPOOL_REGISTER_OPS(ops_mp_mc);
MEMPOOL_REGISTER_OPS(ops_sp_sc);
MEMPOOL_REGISTER_OPS(ops_mp_sc);
MEMPOOL_REGISTER_OPS(ops_sp_mc);

singleかmultiかによって,rte_ringを操作する関数を切り替えています.

基本的にはrte_mempool_get()でmempoolから空きオブジェクトを取得し,rte_mempool_put()でオブジェクトを再びring bufferに追加します.

DPDKでパケットを格納するために利用するmbufは内部でmempolを利用しています. mbufに関してはまた後で書こうと思います.

*1:たぶん

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なしでもうまくいけば環境によっては動作するのかもしれません.

/proc/pid/pagemapの話

前回 /proc/pid/pagemap について簡単に触れたので,実際にpagemapを読み取るときどういうことが起きるか少し追ってみようと思います.

/proc/pid/pagemapに関する操作はカーネルfs/proc/task_mmu.c内でproc_pagemap_operationsに定義してあります.

https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1463

const struct file_operations proc_pagemap_operations = {
    .llseek     = mem_lseek, /* borrow this */
    .read       = pagemap_read,
    .open       = pagemap_open,
    .release    = pagemap_release,
};

要するに,/proc/pid/pagemapをreadする場合はpagemap_read()が呼ばれるということです. pagemap_read()の処理を抜粋すると以下のようになります.

https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1351

static ssize_t pagemap_read(struct file *file, char __user *buf,
               size_t count, loff_t *ppos)
{
   struct mm_struct *mm = file->private_data;
   struct pagemapread pm;
   struct mm_walk pagemap_walk = {};
   unsigned long src;
   unsigned long svpfn;
   unsigned long start_vaddr;
   unsigned long end_vaddr;
...
   pagemap_walk.pmd_entry = pagemap_pmd_range;
   pagemap_walk.pte_hole = pagemap_pte_hole;
#ifdef CONFIG_HUGETLB_PAGE
   pagemap_walk.hugetlb_entry = pagemap_hugetlb_range;
#endif
   pagemap_walk.mm = mm;
   pagemap_walk.private = &pm;

   src = *ppos;
   svpfn = src / PM_ENTRY_BYTES;
   start_vaddr = svpfn << PAGE_SHIFT;
   end_vaddr = mm->task_size;
...
   while (count && (start_vaddr < end_vaddr)) {
        ...
       down_read(&mm->mmap_sem);
       ret = walk_page_range(start_vaddr, end, &pagemap_walk);
       up_read(&mm->mmap_sem);
       start_vaddr = end;

       len = min(count, PM_ENTRY_BYTES * pm.pos);
       if (copy_to_user(buf, pm.buffer, len)) {
           ret = -EFAULT;
           goto out_free;
       }
       copied += len;
       buf += len;
       count -= len;
   }

この関数ではread()のオフセットから求めたい仮想アドレスを計算し(start_vaddr)その後 walk_page_range() 関数を呼びます.このwalk_page_range()関数は https://github.com/torvalds/linux/blob/master/mm/pagewalk.c#L283 に定義されており,指定したページ範囲を探索し,必要に応じて設定したコールバック関数を呼ぶ関数です.

今回の場合,基本的には walk_page_range() => __walk_page_range() => walk_pgd_range() => walk_pud_range() => walk_pmd_range() と探索していったのち,walk->pmd_entry() が呼ばれます.このwalk->pmd_entry()pagemap_read()の前半部分で設定したコールバック関数で,実体はpagemap_pmd_range()です.

pagemap_pmd_range()の主要部を抜き出すと以下のようになっています. https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1205

static int pagemap_pmd_range(pmd_t *pmdp, unsigned long addr, unsigned long end,
                 struct mm_walk *walk)
{
    ...
    orig_pte = pte = pte_offset_map_lock(walk->mm, pmdp, addr, &ptl);
    for (; addr < end; pte++, addr += PAGE_SIZE) {
        pagemap_entry_t pme;

        pme = pte_to_pagemap_entry(pm, vma, addr, *pte);
        err = add_to_pagemap(addr, &pme, pm);
        if (err)
            break;
    }
    ....
}

また,add_to_pagemap()は以下のようになっています. https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1121

static int add_to_pagemap(unsigned long addr, pagemap_entry_t *pme,
              struct pagemapread *pm)
{
    pm->buffer[pm->pos++] = *pme;
    if (pm->pos >= pm->len)
        return PM_END_OF_BUFFER;
    return 0;
}

やっていることはpmdからpteを求め,そのptepte_to_pagemap_entryでpagemapの64bitのエントリに変換し,add_to_pagemapでそれをバッファに保存します.こうして得られたpagemapのエントリを最終的にpagemap_read()のい中でcopy_to_user()でコピーしています.

ということで,pagemapをreadするとページテーブルを探索してそれの結果を返すという,そりゃそうだろうという話ですがこうなっているという話でした.

仮想アドレスから物理アドレスを求める

××な理由で仮想アドレスではなく実際の物理アドレスを求めたいことがあると思います.

linuxの場合,カーネルからならvirt2phys()等使えますが,ユーザランドからはそのような関数はありません. その代わり, /proc/self/pagemap を参照して解決することができます.

pagemap ファイルは各仮想ページがどの物理ページに対応しているかを保持している特殊ファイルです.一つのエントリの長さが64bitで,一つのエントリが一つの仮想ページの情報を保持しています.物理アドレスを求めたい場合はまず仮想アドレスページ番号をインデックスとしてpagemapのエントリを読み取り,そこから物理ページのアドレスを求めます.物理ページのアドレスが分かればそこにオフセットを足せば実際の物理アドレスが求まります.エントリの63bit目がページがメモリ上にあるかどうかを示すフラグになります.

DPDKにあるrte_mem_virt2phy() という関数を参考に*1以下のような関数で解決できました. http://dpdk.org/browse/dpdk/tree/lib/librte_eal/linuxapp/eal/eal_memory.c?h=v17.05-rc2#n159

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
#include <inttypes.h>

#define PFN_MASK_SIZE 8

unsigned long
virt2phy(const void *virtaddr)
{
    unsigned long page, physaddr, virt_pfn;
    off_t offset;
    int page_size = sysconf(_SC_PAGESIZE);
    int fd, retval;

    fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        printf("error: open");
        return 0;
    }

    virt_pfn = (unsigned long)virtaddr / page_size;
    offset = sizeof(uint64_t) * virt_pfn;
    if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {
        printf("error: lseek\n");
        close(fd);
        return 0;
    }

    retval = read(fd, &page, PFN_MASK_SIZE);
    close(fd);
    if (retval < 0) {
        printf("error: read\n");
        return 0;
    } else if (retval != PFN_MASK_SIZE) {
        printf("error: read2\n");
        return 0;
    }

    if ((page & 0x7fffffffffffffULL) == 0){
        printf("pfn == 0\n");
        printf("page = %016lX\n", page);
        return 0;
    }

    physaddr = ((page & 0x7fffffffffffffULL) * page_size)
        + ((unsigned long)virtaddr % page_size);

    return physaddr;
}

実のところ,最初は以下のstack overflowの回答をみつけ,この関数を試してみたのですが,malloc()で求めたアドレスには正しく動くものの,mmap()やローカル変数のアドレスにはなぜか動きませんでした.fread()が失敗しているようですが何が原因でしょうか..

c - How to find the physical address of a variable from user-space in Linux? - Stack Overflow

*1:というかほぼそのまま

I/O APICについて

前回LAPICについて書きました.LAPICは今ではCPU内に埋め込まれており,IntelのSDMにその使用方法が書いてあります.一方でI/O APICは今ではチップセットに埋め込まれているため,I/O APICに関して知りたい場合はチップセットのデータシートを見ることになります.

もともとはLAPICもI/O APICもCPUとは別の独立したチップとして実装されていました.特に,Intel 82093AAが最初のI/O APICチップです.例によって基本的に製品の互換性が保たれているため,I/O APICについて知りたい場合は82093AAのデータシートも参考になります.巨大なチップセットのデータシートと比べコンパクトにまとまっています.

文献

I/O APICの基本

f:id:mm_i:20170327201320p:plain (Intel SDMより)

図に示したように,I/O APICは外部からの割り込みを受け取り,それを各LAPICへ送ります.外部からの割り込みをどのLAPICへ送るかはRemaping Tableで設定します.I/O APICを使用するデバイスは,タイマやキーボードコントローラ(i8042)などです./proc/interruptsから何がI/O APICを利用しているか確認できます.

% cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
  0:         16          0          0          0          0          0          0          0   IO-APIC   2-edge      timer
  1:          1          0          0          0          1          0          0          0   IO-APIC   1-edge      i8042
  8:          0          0          0          0          0          1          0          0   IO-APIC   8-edge      rtc0
  9:          0          0          0          0          4          0          0          0   IO-APIC   9-fasteoi   acpi
 12:          3          0          0          0          1          0          0          0   IO-APIC  12-edge      i8042
 16:        188          0          0          0        187         48          0          0   IO-APIC  16-fasteoi   ehci_hcd:usb1
 18:          0          0          0          0          0          2          0          0   IO-APIC  18-fasteoi   firewire_ohci
 23:         28          0          0          0          2          3          0          0   IO-APIC  23-fasteoi   ehci_hcd:usb4
...

なお,PCIeデバイスMSIを利用する場合は,I/O APICを介さずに直接割り込みがLAPICへ送られます.

I/O APICレジスタ

LAPICではアドレスfee00000h以下にLAPIC レジスタがmemory-mappedされていました(x2APICの場合はMSRを使います).I/O APICの場合,memory-mappedされているレジスタを利用して,I/O APICレジスタへアクセスします.以下にmemory-mappedされているレジスタを示します.

f:id:mm_i:20170409131928p:plain (9-series datasheetより)

具体的には,Indexレジスタ(IOREGSEL レジスタ),Dataレジスタ(IOWINレジスタ)の2つを使います*1.この2つのレジスタはmemory-mappedされており,IndexでI/O APICレジスタのインデックス(下図参照)を指定し,Dataレジスタでデータの読み書きをおこないます.間接的にアクセスできるレジスタは以下の通りです.

f:id:mm_i:20170409132110p:plain (9-series datasheetより)

IndexレジスタやDataレジスタがどこにmemory-mappedされているかはACPIから取得します.linuxの場合,iaslを利用して以下のようにACPIの情報が取得できます.

% sudo apt-get install iasl
% sudo cp /sys/firmware/acpi/tables/APIC .
% iasl -d APIC

これを実行すると,APIC.dslというファイルが作成されます.このファイルを見ると,以下のようにI/O APICについて書かれた部分が見つかります.

...
[06Ch 0108   1]                Subtable Type : 01 [I/O APIC]
[06Dh 0109   1]                       Length : 0C
[06Eh 0110   1]                  I/O Apic ID : 02
[06Fh 0111   1]                     Reserved : 00
[070h 0112   4]                      Address : FEC00000
[074h 0116   4]                    Interrupt : 00000000
...

これより,Indexレジスタが fec00000hにマッピングされていることがわかります.また,Dataレジスタはfec00010hにマッピングされています.

I/O APICレジスタの読み出し

カーネルモジュールからI/O APICレジスタを読み出してみます.読み出し方法は,まずIndexレジスタにoffsetを書き込み,その後Data Registerを読み出すことでおこないます(書き込みたい場合は,Data Registerに直接書き込みます).これらのレジスタのアクセスは全て32bit単位でおこないます.

オフセット0にあるI/O APIC IDと,オフセット1にあるI/O APIC Versionは以下のようにして読み出すことができます.

static int hello_init(){
    void *addr = ioremap(0xFEC00000, 0x10);

    writel(0, addr);
    pr_info("I/O APIC ID : %08x\n", readl(addr+0x10));

    writel(1, addr);
    pr_info("I/O APIC VER : %08x\n", readl(addr+0x10));

    iounmap(addr);

    return 0;
}
% sudo insmode hello.ko && rmmod helo && dmesg | tail -n2
[663870.752166] I/O APIC ID : 02000000
[663870.752185] I/O APIC VER : 00170020

IDは以下に示すように27-24bit目に格納されており,その値は02です.これはACPIで得られたI/O APIC IDと一致します.

f:id:mm_i:20170409132150p:plain (9-series datasheetより)

また,I/O APIC VERは7-0bit目がバージョン,23-16bit目がRedirection Tableのエントリインデックスの最大値です.したがって,このマシンではI/O APICのバージョンは0x20,Redirection Tableのエントリ数の最大(サポートする割り込みの数)は0x17+1 (=24) になります.どのI/O APICでもRedirection Tableの最大エントリ数は24のようです. → 200 seriesのデータシート見たらエントリ数の最大は120でした.

f:id:mm_i:20170409132217p:plain (9-series datasheetより)

Redirection Table

I/O APICレジスタの中でも重要なのが,Redirection Table Registerです.Redirectoin Table Registerは64bitのレジスタで,24個のRedirection Table RegisterそれぞれにはIndexレジスタで10-11h, 12-13h, …, 3e-3fhを指定することでアクセスします..

Redirection Table Registerの構造は以下の通りです.

f:id:mm_i:20170409132231p:plain f:id:mm_i:20170409132238p:plain (9-series datasheetより)

63-56bitでDestinationを決めます.LAPICと同様にDestinationにはLogical modeとPhysical modeがあって,11bit目でどちらのモードかを指定します.下位8bitが割り込みベクタ番号,10-8bit目がDelivery modeになっています.

各Redirection Table Registerと実際の割り込みの対応付けは以下のようになっています.

f:id:mm_i:20170409132320p:plain f:id:mm_i:20170409132328p:plain (9-series datasheetより)

Redirection Tableの読み出し

実際にRedirection Tableを読み出して見ます. 以下のようなカーネルモジュールで読み出すことができます.

long read_redirection_table(int idx){
    void *addr = ioremap_nocache(0xFEC00000, 0x14);
    int t1,t2;
    writel(0x10+idx*2, addr);
    t1 = readl(addr+0x10);

    writel(0x11+idx*2, addr);
    t2 = readl(addr+0x10);
    iounmap(addr);

    return ((long)(t2) << 32) | (long)(t1);
}

void read_rt(){
    int i = 0;
    for(i = 0; i < 24; i++){
        long reg = read_redirection_table(i);
        pr_info("RT Entry%2d : %016lx\n", i, reg);
    }
 }

実行結果:

[869496.874591] RT Entry 0 : 0000000000010000
[869496.874623] RT Entry 1 : ff00000000000931
[869496.874655] RT Entry 2 : ff00000000000930
[869496.874686] RT Entry 3 : ff00000000000933
[869496.874717] RT Entry 4 : ff00000000000934
[869496.874750] RT Entry 5 : ff00000000000935
[869496.874781] RT Entry 6 : ff00000000000936
[869496.874812] RT Entry 7 : ff00000000000937
[869496.874844] RT Entry 8 : ff00000000000938
[869496.874874] RT Entry 9 : ff00000000008939
[869496.874906] RT Entry10 : ff0000000000093a
[869496.874937] RT Entry11 : ff0000000000093b
[869496.874968] RT Entry12 : ff0000000000093c
[869496.875946] RT Entry13 : ff0000000000093d
[869496.876926] RT Entry14 : ff0000000000093e
[869496.877858] RT Entry15 : ff0000000000093f
[869496.878775] RT Entry16 : ff0000000000a971
[869496.879704] RT Entry17 : 0000000000010000
[869496.880615] RT Entry18 : ff0000000000a9a1
[869496.881520] RT Entry19 : 0000000000010000
[869496.882421] RT Entry20 : 0000000000010000
[869496.883306] RT Entry21 : 0000000000010000
[869496.884220] RT Entry22 : 0000000000010000
[869496.885118] RT Entry23 : ff0000000000a973

これだとちょっと分かりにくいので,以下の様な関数で表示してみます.

void print_rt(long reg){
    int dest      = (reg >> 56) & 0xff;
    int edid      = (reg >> 48) & 0xff;
    int mask      = (reg >> 16) & 0x1;
    int trigger   = (reg >> 15) & 0x1;
    int irr       = (reg >> 14) & 0x1;
    int polarity  = (reg >> 13) & 0x1;
    int status    = (reg >> 12) & 0x1;
    int dest_mode = (reg >> 11) & 0x1;
    int delv_mode = (reg >>  8) & 0x3;
    int vector = reg & 0xff;

    pr_info("  Dest           : %02X\n",  dest);
    pr_info("  EDID           : %02X\n",  edid);
    pr_info("  Mask           : %s\n",  mask ? "masked" : "not masked");
    pr_info("  Trigger mode   : %s\n",  trigger ? "level" : "edge");
    pr_info("  Remote IRR mode: %d\n",  irr);
    pr_info("  Polarity       : %s\n",  polarity ? "active low" : "active high");
    pr_info("  Delivery status: %s\n",  status ? "pending" : "idle");
    pr_info("  Dest mode      : %s\n",  dest_mode ? "logical" : "physical");
    pr_info("  Delivery mode  : %03x\n",  delv_mode);
    pr_info("  vector         : %02x\n",  vector);
}

たとえばエントリ16に関しては以下の様になります.

[865418.043710] RT Entry16 : ff0000000000a971
[865418.044335]   Dest           : FF
[865418.044967]   EDID           : 00
[865418.045572]   Mask           : not masked
[865418.046177]   Trigger mode   : level
[865418.046779]   Remote IRR mode: 0
[865418.047381]   Polarity       : active low
[865418.047989]   Delivery status: idle
[865418.048612]   Dest mode      : logical
[865418.049224]   Delivery mode  : 001
[865418.049806]   vector         : 71

ちなみに,ここでdestination modeがlogical, delivery modeは001 (lowest priority), destinationはffですが,LAPICの時と同様自分の環境では割り込みは散りませんでした.. 何故でしょうか..

x2APICの場合

x2APICを利用する場合は,MSIの場合と同様にInterrupt Remappingを使うことになります.下図のように,Redirection Tableの63-49bit目でInterrupt Rmeppaing Tableのインデックスを指定します.

f:id:mm_i:20170409132351p:plain (Intel® Virtualization Technology for Directed I/Oより)

*1:82093AAではIOREGSELとIOWINという名前ですが,9 seriesのデータシートではIndexとDataという名前になっています

per cpu data シンボルのアドレス

kallsymsを見てみると,メモリ先頭のユーザ空間のアドレスにあるシンボルが複数存在していることが確認できます.

% sudo cat /proc/kallsyms | head
0000000000000000 A irq_stack_union
0000000000000000 A __per_cpu_start
0000000000004000 A exception_stacks
0000000000009000 A gdt_page
000000000000a000 A espfix_waddr
000000000000a008 A espfix_stack
000000000000a020 A cpu_llc_id
000000000000a028 A cpu_llc_shared_map
000000000000a030 A cpu_core_map
000000000000a038 A cpu_sibling_map

なんかおかしいなと思ってましたが,__per_cpu_start__per_cpu_endに囲まれた領域にあるシンボルはコアごとに存在するper cpu dataのシンボルのようです.

ソースは追ってませんが,ドキュメントをみると,

DEFINE_PER_CPU(int, x);
int z;

z = this_cpu_read(x);

このコードは,

mov ax, gs:[x]

のようにgsセグメントを介したアクセスになります. x86_64だとgsはIA32_GS_BASE_MSRに設定します(GDTは32bitしか対応してないため).

簡単なカーネルモジュールでIA32_GS_BASE_MSRの確認してみました.

#define MSR_GS_BASE             0xc0000101

static unsigned long long rdmsr(int msr)
{
    unsigned long msrl = 0, msrh = 0;

    asm volatile ("rdmsr" : "=a"(msrl), "=d"(msrh) : "c"(msr));

    return ((unsigned long long)msrh << 32) | msrl;
}

static int hello_init(){
    long long gs = rdmsr(MSR_GS_BASE);
    pr_info("gs: %llx\n", gs);
    return 0;
}
% for i in `seq 0 1 7`; sudo taskset -c $i insmod hello.ko && sudo rmmod hello && dmesg|tail -n1
[35721.154116] gs: ffff8a729f200000
[35721.207429] gs: ffff8a729f240000
[35721.267899] gs: ffff8a729f280000
[35721.323081] gs: ffff8a729f2c0000
[35721.374967] gs: ffff8a729f300000
[35721.432406] gs: ffff8a729f340000
[35721.479421] gs: ffff8a729f380000
[35721.532804] gs: ffff8a729f3c0000

コアごとに0x40000 (=256KB)の領域が確保されているようです. またこの空間はメモリマップ的にはdirect mapping領域内に存在しています.

X540のMSI-X設定

実際のデバイスMSI-Xの設定の確認をしてみます. 今手元にX540があるので,それで実際に確認してみます.

文献

X540の仕様は以下から入手できます.

7.3章にX540におけるMSI-Xについて書いてあります.また,9.3章にPCIeのConfiguration Spaceの構造が書いてあります.

Configuration Space及びMSI-X Tableの確認

lspci -vvvとすると,デバイスのConfiguration Spaceの情報を表示してくれます(Capabilityを表示するには,管理者権限が必要です).

% sudo lspci -vvv -s 04:00.0
04:00.0 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 01)
        Subsystem: Intel Corporation Ethernet Converged Network Adapter X540-T2
        Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
        Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
        Latency: 0, Cache Line Size: 64 bytes
        Interrupt: pin B routed to IRQ 17
        Region 0: Memory at f0200000 (64-bit, prefetchable) [size=2M]
        Region 4: Memory at f0404000 (64-bit, prefetchable) [size=16K]
        Expansion ROM at f0480000 [disabled] [size=512K]
        Capabilities: [40] Power Management version 3
                Flags: PMEClk- DSI+ D1- D2- AuxCurrent=0mA PME(D0+,D1-,D2-,D3hot+,D3cold-)
                Status: D0 NoSoftRst- PME-Enable- DSel=0 DScale=1 PME-
        Capabilities: [50] MSI: Enable- Count=1/1 Maskable+ 64bit+
                Address: 0000000000000000  Data: 0000
                Masking: 00000000  Pending: 00000000
        Capabilities: [70] MSI-X: Enable+ Count=64 Masked-
                Vector table: BAR=4 offset=00000000
                PBA: BAR=4 offset=00002000
... (省略) ...

この結果より,X540はMSIMSI-Xともに対応しており,またMSI-Xが有効(Enable+)になっていることが分かります.Count=64なので,デバイスには64個の割り込みが登録されています.またMSI-X TableはBAR 4のoffset 0から,PBAはBAR 4 のoffset 02000hからあることが分かります.BAR 4のアドレスは,Region 4: Memory at f0404000のアドレスになります.

lspci -xxx を利用すると,configuration spaceのダンプが見れるので,こちらも一応確認してみます.

% sudo lspci -xxx -s 04:00.0
04:00.0 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 01)
00: 86 80 28 15 06 04 10 00 01 00 00 02 10 00 80 00
10: 0c 00 20 f0 00 00 00 00 00 00 00 00 00 00 00 00
20: 0c 40 40 f0 00 00 00 00 00 00 00 00 86 80 01 00
30: 00 00 88 f7 40 00 00 00 00 00 00 00 0a 02 00 00
40: 01 50 23 48 00 20 00 00 00 00 00 00 00 00 00 00
50: 05 70 80 01 00 00 00 00 00 00 00 00 00 00 00 00
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70: 11 a0 3f 80 04 00 00 00 04 20 00 00 00 00 00 00
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
a0: 10 00 02 00 c2 8c 00 10 2f 28 09 00 82 fc 42 10
b0: 00 00 82 10 00 00 00 00 00 00 00 00 00 00 00 00
c0: 00 00 00 00 1f 00 00 00 00 00 00 00 00 00 00 00
d0: 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

まず,Configuration Spaceの0x34がCapability Pointerで,その部分は40です(ちなみに,Configuration Spaceはリトルエンディアンです).そこで,40h番地を見てみるとCapability IDが01h, Next Cap Ptrが50hです.Capability ID 01hはMSIでもMSI-Xでもないので,次に50h番地を見てみると,Capability IDが05h, Next Cap Ptrが70hです.Capability IDが05hなので,ここがMSIのCapabilityに相当します.また,次の70h番地を見てみると,Capability IDが11hで,ここがMSI-XのCapabilityです.この領域をよくみると,MSI-X Table BIRとMSI-X PBA BIRが4で,またTable Offsetが0,PBA Offsetが2000hであることが確認できます.

PCIバイスのBAR領域にアクセスするには,/sys/bus/pci/devices/<bus>:<device>:<num>:.<function num>/resource<BAR番号> のファイルをmmapすればアクセスすることができます.そのためのソフトウェアとして,pcimemがあります.pcimemを利用してX540のBAR4の領域にアクセスしてみると以下のようになりました.

% for j in `seq 0 1 64`; do; for i in `seq $((($j+1)*16-4)) -4 $(($j*16))`; sudo ./pcimem  /sys/bus/pci/devices/0000:04:00.0/resource4 $i w | tail -n1 | cut -d':' -f 2 | tr '\n' ' '; echo ; done
 0x0  0x41A2  0x0  0xFEE8000C
 0x0  0x41B2  0x0  0xFEE0400C
 0x0  0x41C2  0x0  0xFEE4000C
 0x0  0x41D2  0x0  0xFEE8000C
 0x0  0x41E2  0x0  0xFEE4000C
 0x0  0x4123  0x0  0xFEE1000C
 0x0  0x4143  0x0  0xFEE4000C
 0x0  0x4153  0x0  0xFEE1000C
 0x0  0x4163  0x0  0xFEEFF00C
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0xDEADBEAF  0xDEADBEAF  0xDEADBEAF  0xDEADBEAF

これは各行につきMSI-X Tableのエントリ一つで,左からControl Vector, Message Data Register, Message Upper Address, Message Addressです.まず,今回Configuration Spaceで設定されたエントリ数は64でしたが,実際には9つのエントリしか有効になっていないことが分かります(Control Vector=1の場合,その割り込みはマスクされる).また,65番目以降のエントリにアクセスすると0xDEADBEAFが返ってきます(これは仕様です).ixgbeのドライバでは基本的にコア数分だけキューを用意して,割り込みはキュー間で分散させるよう設定するのだと思います(このあたり,また後で詳しく調べたい).

Message Address Registerを見てみると,下位4bitがC0であることから,RH=1, DM=1であることが分かります.Data Registerの値は下位8bitの割り込みベクタ番号のみが違い,割り込みベクタはそれぞれA2h, B2h, C2h, D2h, E2h, 23h, 43h, 53h, 63hとなっています.Local APICでは割り込みベクタの上位4bitで割り込みの優先度が決まりますが,それぞれの割り込みで同じ優先度にならないように割り込みベクタを割り振っているようです.

割り込みベクタ番号と,linuxカーネル内で利用されるirq番号の対応はvector_irq という名のstruct irq_desc*の配列に入っています.以下のようなカーネルモジュールでベクタ番号とirq番号の確認ができます(vector_irqEXPORT_SYMBOL()されていないので,kallsyms_lookup_nameを使う必要があります).

int hello_init(){
    vector_irq_t* vector_irq = (vector_irq_t*)kallsyms_lookup_name("vector_irq");
    int vector_num[] = {0xA2, 0xB2, 0xC2, 0xD2, 0xE2, 0x23, 0x43, 0x53, 0x63};
    int i = 0;
    for (i = 0; i < 9; i++) {
        struct irq_desc* desc = __this_cpu_read((*vector_irq)[vector_num[i]]);
        if (IS_ERR_OR_NULL(desc)){
            continue;
        }
        struct irq_data* data = irq_desc_get_irq_data(desc);
        pr_info("%x : %d\n", vector_num[i], data->irq);
    }

    return 0;
}

実行結果:

% sudo insmod hello5.ko && sudo rmmod hello5 && dmesg | tail -n9
[91213.998593] a2 : 33
[91213.999377] b2 : 34
[91214.000155] c2 : 35
[91214.000933] d2 : 36
[91214.001710] e2 : 37
[91214.002555] 23 : 38
[91214.003359] 43 : 39
[91214.004136] 53 : 40
[91234.077288] 63 : 41

これより,各割り込みはこの環境ではirq番号33-41に対応していることが分かります.

割り込み状況の確認

/proc/interruptsで割り込み状況を確認してみます.

% cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
...
 33:         52       1158          0          0          0          0          0      44844   PCI-MSI 2097152-edge      enp4s0f0-TxRx-0
 34:          1         45      45965          0          0          0          0          0   PCI-MSI 2097153-edge      enp4s0f0-TxRx-1
 35:          1          0         45          0          0          0      46211          0   PCI-MSI 2097154-edge      enp4s0f0-TxRx-2
 36:          1          0          0         45          0          0          0      45965   PCI-MSI 2097155-edge      enp4s0f0-TxRx-3
 37:          0       1150          0          1         45          0      44872          0   PCI-MSI 2097156-edge      enp4s0f0-TxRx-4
 38:          0       1145          0          0      44816         51          0          0   PCI-MSI 2097157-edge      enp4s0f0-TxRx-5
 39:          1          0          0          0          0          0      46066          0   PCI-MSI 2097158-edge      enp4s0f0-TxRx-6
 40:          0          0          0          0      45969          0          0         45   PCI-MSI 2097159-edge      enp4s0f0-TxRx-7
 41:          1          0          0          0          0          0          0          0   PCI-MSI 2097160-edge      enp4s0f0
...

irqのsmp_affinityを確認すると,以下のようになっています.

% for i in `seq 33 1 41`; cat /proc/irq/$i/smp_affinity
80
04
40
80
40
10
40
10
08

この値をよく見てみると,ベクタ番号41を除き,smp_affinityの値はMSI Address Registerの12-19bit目と一致していることが分かります.また,それぞれのビットが立っている位置とCPU番号を対応付けて /proc/interrupts を見てみると,そのビットが立っているCPUに割り込みが多く入っていることが分かります.ということは,今現在MSI-Xのlogical apic IDはflat modeで運用されていると予想されます.flat modeかどうかはLocal APICDestination Fromat Register (DFR)の値によります.また,Logical APIC IDはLogical Destination Register (LDR)に格納されています.LDRは0fee000d0h, FDRは0fee000e0h 番地にmemory-mappedされています.

以下のようなカーネルモジュールを書いて確認してみます(CPUはi7 3770Kです).

int hello_init(){
    void *addr = ioremap(0xfee000D0, 32);
    pr_info("LDR,DFR : %08x, %08x \n", readl(addr), readl(addr+16));
    iounmap(addr);
    return 0;
}

実行結果:

% for i in `seq 0 1 7`; sudo taskset -c $i insmod hello.ko && dmesg | tail -n1 && sudo rmmod hello
[81324.591351] LDR,DFR : 01000000, ffffffff
[81324.651546] LDR,DFR : 02000000, ffffffff
[81324.702683] LDR,DFR : 04000000, ffffffff
[81324.758692] LDR,DFR : 08000000, ffffffff
[81324.802557] LDR,DFR : 10000000, ffffffff
[81324.855086] LDR,DFR : 20000000, ffffffff
[81324.905689] LDR,DFR : 40000000, ffffffff
[81324.945385] LDR,DFR : 80000000, ffffffff

DFRの31-28bit目が全て1なので,これはfalt modelです.また,各CPUコアごとに異なるビットがLogical APIC IDとして振られていることが分かります.この結果は/proc/interrupts の結果と矛盾しません(なんでデフォルトでMSI-Xの各エントリが各コアに対応していないのかはよく分かりませんが..).

smp_affintyを変化させた場合

smp_affinityを変更して各キューに一つのCPUが対応するようにしてみます.

% for i in `seq 0 1 7`;  echo "obase=16; $((1<<i))" | bc | sudo tee /proc/irq/$((33+i))/smp_affinity

ちゃんと変更されちるかsmp_affinityを確認してみます.

% for i in `seq 0 1 8`; cat /proc/irq/$((33+i))/smp_affinity
01
02
04
08
10
20
40
80

で,これで割り込みがコアごとに分散されるかと思ったのですが,しばらく/proc/interrupts観測を続けても特に変化なし..

おかしいと思ってMSI-X Tableを確認してみると,何も変わっていませんでした.

% for j in `seq 0 1 8`; do; for i in `seq $((($j+1)*16-4)) -4 $(($j*16))`; sudo ./pcimem  /sys/bus/pci/devices/0000:04:00.1/resource4 $i w | tail -n1 | cut -d':' -f 2 | tr '\n' ' '; echo ; done
 0x0  0x4183  0x0  0xFEE1000C
 0x0  0x4193  0x0  0xFEE4000C
 0x0  0x41A3  0x0  0xFEE0400C
 0x0  0x41B3  0x0  0xFEE0800C
 0x0  0x41C3  0x0  0xFEE1000C
 0x0  0x41D3  0x0  0xFEE4000C
 0x0  0x41E3  0x0  0xFEE0400C
 0x0  0x4124  0x0  0xFEE8000C
 0x0  0x4144  0x0  0xFEEFF00C

何が原因なのか分かりませんが,仕方ないので直接MSI-X TableのAddress RegisterのDestination Fieldを書き換えてみます.

% for i in `seq 0 1 7`; sudo ./pcimem /sys/bus/pci/devices/0000:04:00.1/resource4 $((i*16)) w "0xFEE"`echo "$((1<<i))"| awk '{printf "%02x",$1}'`"00C"

以下のように書き換わりました.

% for j in `seq 0 1 8`; do; for i in `seq $((($j+1)*16-4)) -4 $(($j*16))`; sudo ./pcimem  /sys/bus/pci/devices/0000:04:00.1/resource4 $i w | tail -n1 | cut -d':' -f 2 | tr '\n' ' '; echo ; done
 0x0  0x4183  0x0  0xFEE0100C
 0x0  0x4193  0x0  0xFEE0200C
 0x0  0x41A3  0x0  0xFEE0400C
 0x0  0x41B3  0x0  0xFEE0800C
 0x0  0x41C3  0x0  0xFEE1000C
 0x0  0x41D3  0x0  0xFEE2000C
 0x0  0x41E3  0x0  0xFEE4000C
 0x0  0x4124  0x0  0xFEE8000C
 0x0  0x4144  0x0  0xFEEFF00C

これで,割り込みがコアごとに分散されるはずです. しばらくpingを動かしてみると,以下のようになりました.

 33:       1130       1158          0          0          0          0          0      45552   PCI-MSI 2097152-edge      enp4s0f0-TxRx-0
 34:          1       1102      46673          0          0          0          0          0   PCI-MSI 2097153-edge      enp4s0f0-TxRx-1
 35:          1          0       1109          0          0          0      46923          0   PCI-MSI 2097154-edge      enp4s0f0-TxRx-2
 36:          1          0          0       1102          0          0          0      46673   PCI-MSI 2097155-edge      enp4s0f0-TxRx-3
 37:          0       1150          0          1       1805          0      45580          0   PCI-MSI 2097156-edge      enp4s0f0-TxRx-4
 38:          0       1145          0          0      45524       1108          0          0   PCI-MSI 2097157-edge      enp4s0f0-TxRx-5
 39:          1          0          0          0          0          0      47832          0   PCI-MSI 2097158-edge      enp4s0f0-TxRx-6
 40:          0          0          0          0      46677          0          0       1102   PCI-MSI 2097159-edge      enp4s0f0-TxRx-7
 41:          1          0          0          0          0          0          0          0   PCI-MSI 2097160-edge      enp4s0f0

数分後:

 33:       1343       1158          0          0          0          0          0      45552   PCI-MSI 2097152-edge      enp4s0f0-TxRx-0
 34:          1       1314      46673          0          0          0          0          0   PCI-MSI 2097153-edge      enp4s0f0-TxRx-1
 35:          1          0       1323          0          0          0      46923          0   PCI-MSI 2097154-edge      enp4s0f0-TxRx-2
 36:          1          0          0       1314          0          0          0      46673   PCI-MSI 2097155-edge      enp4s0f0-TxRx-3
 37:          0       1150          0          1       2017          0      45580          0   PCI-MSI 2097156-edge      enp4s0f0-TxRx-4
 38:          0       1145          0          0      45524       1320          0          0   PCI-MSI 2097157-edge      enp4s0f0-TxRx-5
 39:          1          0          0          0          0          0      48044          0   PCI-MSI 2097158-edge      enp4s0f0-TxRx-6
 40:          0          0          0          0      46677          0          0       1314   PCI-MSI 2097159-edge      enp4s0f0-TxRx-7
 41:          1          0          0          0          0          0          0          0   PCI-MSI 2097160-edge      enp4s0f0

この二つの差分をとると,以下のようになります.

33: 213        0        0        0        0        0        0        0
34:   0      212        0        0        0        0        0        0
35:   0        0      214        0        0        0        0        0
36:   0        0        0      212        0        0        0        0
37:   0        0        0        0      212        0        0        0
38:   0        0        0        0        0      212        0        0
39:   0        0        0        0        0        0      212        0
40:   0        0        0        0        0        0        0        212

目標通り,割り込みが分散されていることが確認できます.

smp_affinityを変更してもAddress Registerが書き変わらないのはixgbeドライバの 問題でしょうか.要調査ですね..