roto11fonの日記

技術系の話を書いたりしてます。

VPS for FreeBSDの内部実装 -構造体-

VPS for FreeBSD

皆さんは VPS for FreeBSDを知っていますか?

よくクラウドとかレンタルサーバーで聞くVPS(Virtual Private Server)とは違います。

Virtual Private System for FreeBSD (link)です。 VPS for FreeBSDは EuroBSDCon2010 で Klaus P. Ohrhallinger が発表したものです。 FreeBSDのコンテナであるJailをライブマイグレーション可能にしたシステムと言われています。 カーネルに手が加えられていますが、競合はあまり起こらないように実装されているらしいです。

今回はコードから見たJailとVPSの違いを、構造体に着目して書いていきたいと思います。 結論から言いますと、VPSはJail実装をベースとして拡張したものです。 Jailに似た部分が多いので、Jailの実装を見た後にVPSを見て行きます。

途中から説明が雑になっていると思います。いい言い回しとか、そもそも間違っているとか、意味わからないとかあったら遠慮なくコメントしてください。

FreeBSD Jailの用語

FreeBSD Jailはjailとかprisonとかいろんな用語をつかいます。 linkから用語をそのまま引用します。

  • jail [監獄]
    • FreeBSDのjail機能そのものを指したり、同機能を用いて構築された(仮想)システム[=hosted/prisoner]を指したりする。要はサンドボックス
  • hosted
    • jailシステムで隔離されたシステム、プロセス、サービスを表す。
  • host
    • jailシステムが動いているシステム、プロセス、サービスを表す。名前の通りの意味。
  • prisoner [囚人]
    • hostedに同じ
  • jailer [看守]
    • hostに同じ

コンテナ内部を呼ぶときはhosted/prisoner、コンテナ外部を呼ぶときはhost/jailerと呼びます。 また別の視点(公式のドキュメント)から見ると、 ユーザ空間からコンテナをJailと呼び、カーネル空間からコンテナをPrisonと呼んでいます。 実装レベルでも同じように分けて実装されています。

FreeBSD Jailの実装

まずは、Jailの構造体を見たいと思います。

/* sys/sys/jail.h */

struct jail {
    uint32_t   version;
    char       *path;
    char       *hostname;
    char       *jailname;
    uint32_t   ip4s;
    uint32_t   ip6s;
    struct in_addr *ip4;
    struct in6_addr    *ip6;
};

非常にシンプルですね。jail構造体には、jlsをした時に見える情報、Jailに関する基本的な情報が格納されています。 基本的にはユーザ空間とカーネル空間のインタフェースのような扱いで使われます。 例えば、jail(2)の実行時にカーネルで呼ばれるsys_jailで使われます。

/* sys/kern/kern_jail.c */

int
sys_jail(struct thread *td, struct jail_args *uap)
{
    uint32_t version;
    int error;
    struct jail j;

    error = copyin(uap->jail, &version, sizeof(uint32_t));
    if (error)
        return (error);

    switch (version) {
    case 0:
    {
        struct jail_v0 j0;

        /* FreeBSD single IPv4 jails. */
        bzero(&j, sizeof(struct jail));
        error = copyin(uap->jail, &j0, sizeof(struct jail_v0));
        if (error)
            return (error);
        j.version = j0.version;
        j.path = j0.path;
        j.hostname = j0.hostname;
        j.ip4s = htonl(j0.ip_number);   /* jail_v0 is host order */
        break;
    }

    case 1:
        /*
        * Version 1 was used by multi-IPv4 jail implementations
        * that never made it into the official kernel.
        */
        return (EINVAL);

    case 2:  /* JAIL_API_VERSION */
        /* FreeBSD multi-IPv4/IPv6,noIP jails. */
        error = copyin(uap->jail, &j, sizeof(struct jail));
        if (error)
            return (error);
        break;

    default:
        /* Sci-Fi jails are not supported, sorry. */
        return (EINVAL);
    }
    return (kern_jail(td, &j));
}

この関数では、jail(2)の引数をjail構造体にコピーして使っています。 上の方でインタフェースと表現したように、これはコンテナの実体ではありません。

次に、FreeBSDにおけるコンテナの実体であるprison構造体を見ます。

/* sys/sys/jail.h */

struct prison {
    TAILQ_ENTRY(prison) pr_list;            /* (a) all prisons */
    int         pr_id;             /* (c) prison id */
    int         pr_ref;            /* (p) refcount */
    int         pr_uref;           /* (p) user (alive) refcount */
    unsigned    pr_flags;          /* (p) PR_* flags */
    LIST_HEAD(, prison) pr_children;        /* (a) list of child jails */
    LIST_ENTRY(prison) pr_sibling;          /* (a) next in parent's list */
    struct prison  *pr_parent;         /* (c) containing jail */
    struct mtx  pr_mtx;
    struct task     pr_task;           /* (c) destroy task */
    struct osd  pr_osd;            /* (p) additional data */
    struct cpuset  *pr_cpuset;         /* (p) cpuset */
    struct vnet    *pr_vnet;           /* (c) network stack */
    struct vnode   *pr_root;           /* (c) vnode to rdir */
    int         pr_ip4s;           /* (p) number of v4 IPs */
    int         pr_ip6s;           /* (p) number of v6 IPs */
    struct in_addr *pr_ip4;            /* (p) v4 IPs of jail */
    struct in6_addr    *pr_ip6;            /* (p) v6 IPs of jail */
    struct prison_racct *pr_prison_racct;      /* (c) racct jail proxy */
    void       *pr_sparep[3];
    int         pr_childcount;         /* (a) number of child jails */
    int         pr_childmax;           /* (p) maximum child jails */
    unsigned    pr_allow;          /* (p) PR_ALLOW_* flags */
    int         pr_securelevel;        /* (p) securelevel */
    int         pr_enforce_statfs;     /* (p) statfs permission */
    int         pr_devfs_rsnum;        /* (p) devfs ruleset */
    int         pr_spare[3];
    int         pr_osreldate;          /* (c) kern.osreldate value */
    unsigned long  pr_hostid;         /* (p) jail hostid */
    char        pr_name[MAXHOSTNAMELEN];   /* (p) admin jail name */
    char        pr_path[MAXPATHLEN];       /* (c) chroot path */
    char        pr_hostname[MAXHOSTNAMELEN];   /* (p) jail hostname */
    char        pr_domainname[MAXHOSTNAMELEN]; /* (p) jail domainname */
    char        pr_hostuuid[HOSTUUIDLEN];  /* (p) jail hostuuid */
    char        pr_osrelease[OSRELEASELEN];    /* (c) kern.osrelease value */
};

大きめな構造体になっていますね。prison構造体はリストになっています。
pr_childcountやpr_childmaxといったコンテナのネスト数なども管理してます。 メンバ変数のpr_prison_racctは、hosted/prisonerが使っているリソース(cpuやメモリなど)の情報を含んでおり、主にRACCT/RCTLで使われます。
prison構造体は、スレッドが保持するucred構造体に格納され、td->td_ucred->pr_prisonという使われ方をします。 host/jailer(コンテナホスト)のスレッドのpr_prisonには特別な値prison0が代入されます。prison0はゼロ値(っぽい値)が入ったprison構造体です。

/* sys/sys/kern_jail.c */

struct prison prison0 = {
    .pr_id      = 0,
    .pr_name    = "0",
    .pr_ref     = 1,
    .pr_uref    = 1,
    .pr_path    = "/",
    .pr_securelevel = -1,
    .pr_devfs_rsnum = 0,
    .pr_childmax    = JAIL_MAX,
    .pr_hostuuid    = DEFAULT_HOSTUUID,
    .pr_children    = LIST_HEAD_INITIALIZER(prison0.pr_children),
#ifdef VIMAGE
    .pr_flags   = PR_HOST|PR_VNET|_PR_IP_SADDRSEL,
#else
    .pr_flags   = PR_HOST|_PR_IP_SADDRSEL,
#endif
    .pr_allow   = PR_ALLOW_ALL_STATIC,
};

ここまでのFreeBSD Jailの情報をまとめてみます。

  • jail構造体はユーザ空間とカーネル空間とのインタフェースのような扱い
  • コンテナの実体はprison
  • スレッドごとにprison構造体を持つ
  • コンテナホストのスレッドはprison0が代入される

FreeBSD VPSの実装

まず、vps構造体を見ましょう。

/* sys/vps/vps2.h */

struct vps {

    struct vnet        *vnet;

        LIST_ENTRY(vps)     vps_all;
        LIST_ENTRY(vps)     vps_sibling;
        LIST_HEAD(, vps)    vps_child_head;
        struct vps     *vps_parent;

    struct sx      vps_lock;
    char           *vps_lock_name;

    u_int           vps_id;
    char           vps_name[MAXHOSTNAMELEN];
    u_char          vps_status;

    u_int           vps_refcnt;
    struct mtx     vps_refcnt_lock;
#ifdef INVARIANTS
    TAILQ_HEAD(, vps_ref)   vps_ref_head;
#endif
    struct timeout_task    vps_task;

        u_char          priv_allow_set[PRIV_SET_SIZE];
        u_char          priv_impl_set[PRIV_SET_SIZE];

    struct vps_arg_ip4 *vps_ip4;
    struct vps_arg_ip6 *vps_ip6;
    u_int16_t       vps_ip4_cnt;
    u_int16_t       vps_ip6_cnt;

    u_int           vps_flags;

    int            restore_count;

    int64_t            suspend_time;

    struct vps_acc     *vps_acc;   /* XXX do inline */

    struct vnode       *consolelog;
    struct tty     *console_tty;
    struct file        *console_fp_ma;
    int            consolelog_refcnt;
    int            console_flags;

    struct ucred       *vps_ucred;

    struct devfs_rule  *devfs_ruleset;

        struct vnode       *_rootvnode;
        char           _rootpath[MAXPATHLEN];
};

prison構造体と似た作りになっていますが、違う情報も扱っていることがわかります。 prison構造体とは全く関係がないのかというとそうではなくて、ucred構造体であるvps_ucredを持っています。 上のprison構造体の説明で書きましたが、ucred構造体はprison構造体を含んでいます。 vps構造体はprison構造体を含んでいる。つまり、vps構造体はprison構造体の拡張になります。

/* sys/vps/vps_core.c */

struct vps *
vps_alloc(struct vps *vps_parent, struct vps_param *vps_pr,
    char *vps_name, int *errorval)
{
    struct vps *vps;
    struct vps *vps_save;
    struct nameidata nd;
    char *path;
    int error;
    char *tmpstr = NULL;
    struct thread *td = curthread;

~~~~~~~~~中略~~~~~~~~~~
        vps_prison_alloc(vps_parent, vps);
        prison_hold(VPS_VPS(vps, prison0));
        prison_free(vps->vps_ucred->cr_prison);
        vps->vps_ucred->cr_prison = VPS_VPS(vps, prison0);

~~~~~~~~~~~~~~~~~~~~~~
}

static int
vps_prison_alloc(struct vps *vps_parent, struct vps *vps)
{
    struct prison *pp, *np;

~~~~~~~~~中略~~~~~~~~~~

    VPS_VPS(vps, prison0) = malloc(sizeof(struct prison),
        M_VPS_CORE, M_WAITOK|M_ZERO);
    np = VPS_VPS(vps, prison0);
    pp = VPS_VPS(vps_parent, prison0);

~~~~~~~~~~~~~~~~~~~~~~
}

vps_alloc関数はvpsctlでコンテナを実行した後、vps構造体を作る関数です。 vpsを作る中で、vps_prison_alloc関数を呼び、prison構造体を作成しています。

/* sys/vps/vps_core.c */

static void
vps_init(void *unused)
{

    LIST_INIT(&vps_head);

    /* init all the global locks here */
    sx_init(&vps_all_lock, "lock of all vps instances");

    sx_xlock(&vps_all_lock);
    vps0 = vps_alloc(NULL, NULL, "", NULL);
    sx_xunlock(&vps_all_lock);

    curthread->td_vps = vps0;

    vps_console_init();

    DBGCORE("WARNING: VPS VIRTUAL PRIVATE SYSTEMS ENABLED. "
        "HIGHLY EXPERIMENTAL!\n");
}

OS起動時にはhost/jailerに対して、ゼロ値で初期化されたvps構造体を作成しています。
prison0も一緒に作成されますが、VPSの場合はtd->td_vps->vps_ucred->cr_prisonに代入されます。

ここまでのVPS for FreeBSDの情報をまとめてみます。

  • VPSではprisonを拡張している
  • vps構造体はjail構造体よりも管理する情報が多い
  • vps構造体はprison構造体を保持できるucredまで管理する
  • vps0もprison0も作成する

まとめ

FreeBSD JailとVPS for FreeBSDでは、似ている部分としてprisonを使ってコンテナを作成していることが挙げられます。 しかし、jailはユーザ空間とカーネル空間のインタフェース的な存在なのに対し、vpsはprisonそのもののような役割を持っています。