DragonFly BSDの新機能「kpmap」を読んでみる

DragonFly BSD 4.0.1がリリースされたという記事を呼んでリリースノートを見ていたら「New device files /dev/upmap and /dev/kpmap have been added」なんてのがあって、興味が出たのでソースを読んでみました。
この機能はざっくり言ってしまうとLinuxにおけるVDSOみたいなもので、一部のシステムコールに関しては一定回数以上呼ばれたらシステムコールを呼ぶのではなくメモリにマップしたデータを読み書きして返すというものです。このために2個のデバイスファイルが追加されました。 * /dev/upmap * /dev/kpmap

kpmapはカーネル単位(なので全プロセスに共通)のデータを読むためのもので、Read Onlyとなっています。 upmapはプロセス単位でこちらはR/Wとなります。

ここではkpmapの方を見ていきます。

まずは実際に使っているところから見ていきます。見る関数は__clock_gettime(clock_gettime(2)の実装)でファイルはlib/libc/upmap/ukp_clock.cでlibcの関数です。

最初のif文でfast_clockのチェックをしていますが、これは初期値0の変数です。条件によってい__kpmap_map()内で1が設定されます。次のチェックはfast_countが10>=かの部分でこの変数も初期値は0になります。 このfast_countが10になるとシステムコールの呼び出してはなくて/dev/kmapからデータを読んで返すようになります。

static int fast_clock;
static int fast_count;
static int *upticksp;
static struct timespec *ts_uptime;
static struct timespec *ts_realtime;

int
__clock_gettime(clockid_t clock_id, struct timespec *ts)
{
    int res;
    int w;

    if (fast_clock == 0 && fast_count++ >= 10) {
        __kpmap_map(&upticksp, &fast_clock, KPTYPE_UPTICKS);
        __kpmap_map(&ts_uptime, &fast_clock, KPTYPE_TS_UPTIME);
        __kpmap_map(&ts_realtime, &fast_clock, KPTYPE_TS_REALTIME);
        __kpmap_map(NULL, &fast_clock, 0);
    }

次はfast_clock変数が0以上の場合で、これは/dev/kmapからデータを読める場合の処理です。switch文のcaseを見るとclock_idの値次第で直接値を読みだして返すかシステムコールを呼ぶかが変わります。fast_clockが0の場合もシステムコールを呼びます。

 if (fast_clock > 0) {
        switch(clock_id) {
        case CLOCK_UPTIME_FAST:
        case CLOCK_MONOTONIC_FAST:
            do {
                w = *upticksp;
                cpu_lfence();
                *ts = ts_uptime[w & 1];
                cpu_lfence();
                w = *upticksp - w;
            } while (w > 1);
            res = 0;
            break;
        case CLOCK_REALTIME_FAST:
        case CLOCK_SECOND:
            do {
                w = *upticksp;
                cpu_lfence();
                *ts = ts_realtime[w & 1];
                cpu_lfence();
                w = *upticksp - w;
            } while (w > 1);

            if (clock_id == CLOCK_SECOND)
                ts->tv_nsec = 0;
            res = 0;
            break;
        default:
            res = __sys_clock_gettime(clock_id, ts);
            break;
        }
    } else {
        res = __sys_clock_gettime(clock_id, ts);
    }
    return res;
}

では次に__kpmap_map()を見てみます。呼び出しはこのような感じですね。

static int *upticksp;
static struct timespec *ts_uptime;
static struct timespec *ts_realtime;

int
__clock_gettime(clockid_t clock_id, struct timespec *ts)
{
〜略〜

    if (fast_clock == 0 && fast_count++ >= 10) {
        __kpmap_map(&upticksp, &fast_clock, KPTYPE_UPTICKS);
        __kpmap_map(&ts_uptime, &fast_clock, KPTYPE_TS_UPTIME);
        __kpmap_map(&ts_realtime, &fast_clock, KPTYPE_TS_REALTIME);
        __kpmap_map(NULL, &fast_clock, 0);
    }

実装はこのようになってます。ファイルはlib/libc/upmap/upmap.cです。 最初はスレッドかどうかの確認で、スレッドだったらロックをとっています。

void
__kpmap_map(void *datap, int *state, uint16_t type)
{
    ukpheader_t *head;

    if (__isthreaded)
        _pthread_mutex_lock(&ukpmap_lock);

kpmap_okは初期値0の変数です。ということで、if文の中に入ります。そして、最初にまたkpmap_okのチェックがあります。

 if (kpmap_ok <= 0) {
        int fd;

        if (kpmap_ok < 0)
            goto failed;

特に問題がなければデバイスファイル(/dev/kpmap)を開いてmmap()してファイルを閉じるという普通の処理です。

     fd = _open("/dev/kpmap", O_RDONLY);
        if (fd < 0) {
            kpmap_ok = -1;
            goto failed;
        }
        kpmap_headers = mmap(NULL, KPMAP_MAPSIZE,
                     PROT_READ, MAP_SHARED,
                     fd, 0);
        _close(fd);
        if ((void *)kpmap_headers == MAP_FAILED) {
            kpmap_ok = -1;
            goto failed;
        }
        kpmap_ok = 1;
    }

kpmap_headersはファイルの先頭の方で下記のように宣言されています。

static ukpheader_t *kpmap_headers;

ukpheader_tはsys/sys/upmap.hでこのように定義されています。

typedef struct ukpheader {
    uint16_t   type;       /* element type */
    uint16_t   offset;     /* offset from map base, max 65535 */
} ukpheader_t;

次のチェックですが、typeはKPTYPE_UPTICKSとかですが、clock_gettime()ではkpmap_map()の一連の呼び出しの最後にkpmap_map(NULL, &fast_clock, 0);としてkpmap_map()を呼んでいました。この3番目の引数が0だった場合の処理がここになります。そして、stateが0の場合(lib/libc/upmap/ukp_clock.cでfast_clockが0だった場合)に1が設定されます。typeが0だった場合は読みだすデータは特にないのでここで終了になります。

    /*
    * Special case to finalize state
    */
    if (type == 0) {
        if (*state == 0)
            *state = 1;
        if (__isthreaded)
            _pthread_mutex_unlock(&ukpmap_lock);
        return;
    }

ここからはtypeが0以外で何かしら意味のあるデータを取得する場合の処理です。kpmap_headersは先ほどのmmap()の戻り値です。ここからデータを辿って行って、ヘッダの内容が欲しい情報のヘッダだった場合に値が設定されてreturnします。何も見つからなければfailedのラベル以下の行が実行されて終了です。

 /*
    * Look for type.
    */
    for (head = kpmap_headers; head->type; ++head) {
        if (head->type == type) {
            *(void **)datap = (char *)kpmap_headers + head->offset;
            if (__isthreaded)
                _pthread_mutex_unlock(&ukpmap_lock);
            return;
        }
    }
failed:
    *state = -1;
    if (__isthreaded)
        _pthread_mutex_unlock(&ukpmap_lock);
}

ここまでが使っているところで、次は初期化部分を見てみます。こちらはlibcではなくてkernelでsys/kern/init_main.cに初期化処理があります。これはまあ見たとおりですね。

static void
kpmap_init(const void *udata __unused)
{
    kpmap = kmalloc(roundup2(sizeof(*kpmap), PAGE_SIZE),
            M_TEMP, M_ZERO | M_WAITOK);

    kpmap->header[0].type = UKPTYPE_VERSION;
    kpmap->header[0].offset = offsetof(struct sys_kpmap, version);
    kpmap->header[1].type = KPTYPE_UPTICKS;
    kpmap->header[1].offset = offsetof(struct sys_kpmap, upticks);
    kpmap->header[2].type = KPTYPE_TS_UPTIME;
    kpmap->header[2].offset = offsetof(struct sys_kpmap, ts_uptime);
    kpmap->header[3].type = KPTYPE_TS_REALTIME;
    kpmap->header[3].offset = offsetof(struct sys_kpmap, ts_realtime);
    kpmap->header[4].type = KPTYPE_TSC_FREQ;
    kpmap->header[4].offset = offsetof(struct sys_kpmap, tsc_freq);
    kpmap->header[5].type = KPTYPE_TICK_FREQ;
    kpmap->header[5].offset = offsetof(struct sys_kpmap, tick_freq);
    kpmap->version = KPMAP_VERSION;
}
SYSINIT(kpmapinit, SI_BOOT1_POST, SI_ORDER_FIRST, kpmap_init, NULL)

kpmapはグローバル変数で型はstruct sys_kpmapです。

struct sys_kpmap {
    ukpheader_t header[64];
    int32_t        version;
    int32_t        upticks;    /* userland reads ts_*[upticks & 1] */
    struct timespec    ts_uptime[2];  /* mono uptime @ticks (uncompensated) */
    struct timespec ts_realtime[2];   /* realtime @ticks resolution */
    int64_t        tsc_freq;   /* (if supported by cpu) */
    int32_t        tick_freq;  /* scheduler tick frequency */
};

kpmap_init()でheaderのtypeとoffsetに設定していた内容が__kpmap_map()の↓で見たところですね。

 for (head = kpmap_headers; head->type; ++head) {
        if (head->type == type) {
            *(void **)datap = (char *)kpmap_headers + head->offset;
            if (__isthreaded)
                _pthread_mutex_unlock(&ukpmap_lock);
            return;
        }
    }

さて、kpmap_init()では構造体に値を設定しただけなので/dev/kpmapにデータを設定する必要があります。kpmap、upmapに限らずmemとかnullみたいなファイルを作っているのはmem_drvinit()です。

static void
mem_drvinit(void *unused)
{

    /* Initialise memory range handling */
    if (mem_range_softc.mr_op != NULL)
        mem_range_softc.mr_op->init(&mem_range_softc);

    make_dev(&mem_ops, 0, UID_ROOT, GID_KMEM, 0640, "mem");
    make_dev(&mem_ops, 1, UID_ROOT, GID_KMEM, 0640, "kmem");
    make_dev(&mem_ops, 2, UID_ROOT, GID_WHEEL, 0666, "null");
    make_dev(&mem_ops, 3, UID_ROOT, GID_WHEEL, 0644, "random");
    make_dev(&mem_ops, 4, UID_ROOT, GID_WHEEL, 0644, "urandom");
    make_dev(&mem_ops, 5, UID_ROOT, GID_WHEEL, 0666, "upmap");
    make_dev(&mem_ops, 6, UID_ROOT, GID_WHEEL, 0444, "kpmap");
    zerodev = make_dev(&mem_ops, 12, UID_ROOT, GID_WHEEL, 0666, "zero");
    make_dev(&mem_ops, 14, UID_ROOT, GID_WHEEL, 0600, "io");
}

SYSINIT(memdev,SI_SUB_DRIVERS,SI_ORDER_MIDDLE+CDEV_MAJOR,mem_drvinit,NULL)

このファイルの中でkpmap/upmapのデータを見ていそうなのはmemuksmap()でした。えーと、memory user kernel space mappingとかそんな意味かな?

 case 5:
    case 6:
        /*
        * minor device 5 is /dev/upmap (see sys/upmap.h)
        * minor device 6 is /dev/kpmap (see sys/upmap.h)
        */
        result = 0;
        error = user_kernel_mapping(minor(dev),
                        ptoa(fake->pindex), &result);
        fake->phys_addr = result;
        break;

で、user_kernel_mapping()ですが、実際にデータ(kpmap)を扱っているのはこの辺です。pmap_kextract()は仮想アドレスから物理アドレスに変換する関数みたいです。

 switch(num) {
    case 5:
        /*
        * /dev/upmap - maps RW per-process shared user-kernel area.
        */
        if (p->p_upmap == NULL)
            proc_usermap(p, invfork);
        else if (invfork)
            p->p_upmap->invfork = invfork;

        if (p->p_upmap &&
            offset < roundup2(sizeof(*p->p_upmap), PAGE_SIZE)) {
            /* only good for current process */
            *resultp = pmap_kextract((vm_offset_t)p->p_upmap +
                         offset);
            error = 0;
        }
        break;
    case 6:
        /*
        * /dev/kpmap - maps RO shared kernel global page
        */
        if (kpmap &&
            offset < roundup2(sizeof(*kpmap), PAGE_SIZE)) {
            *resultp = pmap_kextract((vm_offset_t)kpmap +
                         offset);
            error = 0;
        }
        break;

/dev/kpmap辺りの初期化はだいたいこれくらいで、あとはシステムコール固有の処理があります。と言いつつシステムコール固有じゃないけど初期化処理としてはkpmap構造体のtsc_freq、tick_freqに値を設定するinitclocks()があります。ファイルはsys/kern/kern_clock.cです。

static void
initclocks(void *dummy)
{
    /*psratio = profhz / stathz;*/
    initclocks_pcpu();
    clocks_running = 1;
    if (kpmap) {
        kpmap->tsc_freq = (uint64_t)tsc_frequency;
        kpmap->tick_freq = hz;
    }
}

このファイルでは他にhardclock()がkpmap構造体にデータを設定します。

     /*
        * Update kpmap on each tick.  TS updates are integrated with
        * fences and upticks allowing userland to read the data
        * deterministically.
        */
        if (kpmap) {
        int w;

        w = (kpmap->upticks + 1) & 1;
        getnanouptime(&kpmap->ts_uptime[w]);
        getnanotime(&kpmap->ts_realtime[w]);
        cpu_sfence();
        ++kpmap->upticks;
        cpu_sfence();
        }
    }

ここで__clock_gettime()の処理を振り返ってみます。clock_idがCLOCK_REALTIME_FASTの場合、ts_realtimeを呼んでますね。ts_realtimeに値を設定しているのが↑ですね。このhardclock()はタイマー割り込みのタイミングで呼ばれるようです。

         do {
                w = *upticksp;
                cpu_lfence();
                *ts = ts_realtime[w & 1];
                cpu_lfence();
                w = *upticksp - w;
            } while (w > 1);

ということでDragonFly BSDの/dev/kpmapの処理を読んでみました。