froghui homepage 2016-03-17T03:04:55+00:00 froghui2002@hotmail.com docker背后的存储之device mapper(四):dm-thin讨论 2016-02-29T20:33:25+00:00 froghui http://froghui.github.io/device-mapper4 我们知道,docker在centOS上背后使用的存储技术是device mapper。本文将从device mapper实现的细节来讨论device mapper在docker中是如何使用的。

1.关于blkdiscard

device mapper实际上是通过虚拟出一个硬盘设备来完成所谓的data mapping的。在虚拟磁盘设备dm-X上做磁盘操作,需要同时考虑到申请新的block和清除对block的占用。

对于申请新的block,需要向data space bitmap index申请新的可用的数据块,然后加入到 two level data mapping和detail tree中,同时会在data space bitmap index 中记录使用的ref count。

对于清除对block的占用,是通过产生block discard request来删除某块物理磁盘空间,具体到device mapper thin的实现,这时候一方面需要从 two level data mapping的那个map tree中对virtual block的映射删除,另一方面将virtual block实际对应的data block的磁盘 ref count计数减1。这样改data block在ref count == 0的时候就可用放入池中重新使用了。

blkdiscard对用户空间有两种暴露方式:

  • mount -o discard
  • ioctl discard (blkdiscard utils) 产生blockdiscard request

第一种方式由ext4文件系统自动的产生blkdiscard request。

mount -t ext4 -o nodiscard /dev/mapper/docker-xxx /tmp/xxx

man mount
discard/nodiscard
      Controls  whether  ext4 should issue discard/TRIM commands to  
      the underlying block device when blocks are freed.  This is     
      useful for SSD devices    
      and sparse/thinly-provisioned LUNs, but it is off by default     
      until sufficient testing has been done.  

第二种方式显示指定blkdiscard request。

NAME
       blkdiscard - discard sectors on a device

SYNOPSIS
       blkdiscard [-o offset] [-l length] [-s] [-v] device

DESCRIPTION
       blkdiscard  is  used  to  discard device sectors.  This is useful for solid-state drivers (SSDs) and thinly-provisioned storage.  Unlike fstrim(8) this
       command is used directly on the block device.

       By default, blkdiscard will discard all blocks on the device.  Options may be used to modify this behavior based on range or size, as explained  below.

       The device argument is the pathname of the block device.

       WARNING: All data in the discarded region on the device will be lost!

上述两种方式最终都会执行到process_prepared_discard。

static void process_prepared_discard(struct dm_thin_new_mapping *m)
{
    int r;
    struct thin_c *tc = m->tc;

    r = dm_thin_remove_block(tc->td, m->virt_block);
    if (r)
        DMERR_LIMIT("dm_thin_remove_block() failed");

    process_prepared_discard_passdown(m);
}

//删除该virtual_block对应的physical data block,以及<virtual_block, physical_block>映射数据
static int __remove(struct dm_thin_device *td, dm_block_t block)
{
    int r;
    struct dm_pool_metadata *pmd = td->pmd;
    dm_block_t keys[2] = { td->id, block };

    r = dm_btree_remove(&pmd->info, pmd->root, keys, &pmd->root);
    if (r)
        return r;

    td->mapped_blocks--;
    td->changed = 1;

    return 0;
}

所以在container的虚拟磁盘设备未被删除时,对磁盘空间的申请和撤销是通过make_requst这条线完成的(gendisk的request_queue)。

docker提供了参数可以对上述两种情形进行配置:

  • dm.mountopt是在container存活时,在container的文件系统中删除文件是否需要自动的blkdiscard, 如果设置为dm.mountopt=nodiscard,那么一个container的文件删除了,并不会通知device mapper有block需要回收。

  • dm.blkdiscard是在销毁容器时通知device mapper将该容器占有的data space返回到pool中以便其他container可以使用。 设置成dm.blkdiscard=ture,会通过ioctl发起blkdiscard操作。否则不发起。

当最后删除虚拟磁盘涉笔dm-X时,通过btree的删除会做两件事情; 一是删除detail_tree的映射数据; 二是删除two level data mapping 这个btree的第二层,对dev的映射,也会把对应的dm-X映射到的data block的ref count计数减1,达到清除data mapping的目的。和前面gendisk的blkdiscard request相比,这里是对该dev的所有<virtual_block, physical_block>进行处理。而不是对单个<virtual_block, phycical_block>进行处理。

    r = dm_btree_remove(&pmd->details_info, pmd->details_root,
                &key, &pmd->details_root);
    if (r)
        return r;

    r = dm_btree_remove(&pmd->tl_info, pmd->root, &key, &pmd->root);
    if (r)
        return r;

for each pointing data block:

    dm_sm_dec_block(sm, b);
    sm->dec(sm, block)

几个主要操作的总结

  • container里申请空间(dd) 由文件系统代表从data space bitmap index中申请新的数据块,将<virtual_block, physical_block>映射关系保存
  • container里删除磁盘空间(rm) 仅在配置dm.mountopt=discard的情况下才创建bio request, 通知data space bitmap index数据块phycical_block删除(refcount--),并删除data mapping<virtual_block, phycical_block>映射关系。
  • 删除container(dmsetup message delete) 将该container映射的所有phycical_block从data space bitmap index删除(refcount--),并且删除所有<virtual_block, phycical_block>映射关系。
  • 如果配置了dm.blkdiscard=true在dmsetup message delete之前会先调用ioctl interface(FITRIM)产生bio request,通知删除该命令所指定的virtual_block对应的phycical_block的data space bitmap index(refcount--),同时摘除<virtual_block,physical>映射关系。

2.关于COPY-ON-WRITE

这里的copy-on-write主要是指metadata数据,基于base device创建出来的snapshot device在刚刚建出来的时候,其mapping数据完全复用base device的那份。当有write请求落到任意一个设备上时(base deive或者snap child device)会引起copy-on-write,即分配新的metadata block数据,将data mapping数据在新的metadata block数据复制一份,同时在data bitmap index中对data block的计数ref count进行增加处理。这样相当于恰好有两个设备指向了同一个物理设备block,但是注意他们的metadata数据已经完全分开了。

]]>
docker背后的存储之device mapper(三):dm-thin设计实现 2016-02-12T20:33:25+00:00 froghui http://froghui.github.io/device-mapper3 我们知道,docker在centOS上背后使用的存储技术是device mapper。本文将从device mapper实现的细节来讨论device mapper在docker中是如何使用的。

虽然C并不是直接支持面向对象的编程,但是通过C的struct以及复杂的函数指针,还是可以“曲折”的实现面向对象的编程。下图显示了device mapper中persistent data中的主要结构(类)和成员。

1.dm_space_map

基础类dm_space_map定义了一系列接口,用来对metadata和data分区的space map相应管理。例如new_block就是向metadata或者data分区申请一块可以使用的block;get_count统计某个block被引用的个数;inc_block和dec_block用来通知某个block的引用计数增加和减少;extend告诉space map需要扩展管理的blocks的个数是多少。

/*
 * struct dm_space_map keeps a record of how many times each block in a device
 * is referenced.  It needs to be fixed on disk as part of the transaction.
 */
struct dm_space_map {
    void (*destroy)(struct dm_space_map *sm);

    /*
     * You must commit before allocating the newly added space.
     */
    int (*extend)(struct dm_space_map *sm, dm_block_t extra_blocks);

    /*
     * Extensions do not appear in this count until after commit has
     * been called.
     */
    int (*get_nr_blocks)(struct dm_space_map *sm, dm_block_t *count);

    /*
     * Space maps must never allocate a block from the previous
     * transaction, in case we need to rollback.  This complicates the
     * semantics of get_nr_free(), it should return the number of blocks
     * that are available for allocation _now_.  For instance you may
     * have blocks with a zero reference count that will not be
     * available for allocation until after the next commit.
     */
    int (*get_nr_free)(struct dm_space_map *sm, dm_block_t *count);

    int (*get_count)(struct dm_space_map *sm, dm_block_t b, uint32_t *result);
    int (*count_is_more_than_one)(struct dm_space_map *sm, dm_block_t b,
                      int *result);
    int (*set_count)(struct dm_space_map *sm, dm_block_t b, uint32_t count);

    int (*commit)(struct dm_space_map *sm);

    int (*inc_block)(struct dm_space_map *sm, dm_block_t b);
    int (*dec_block)(struct dm_space_map *sm, dm_block_t b);

    /*
     * new_block will increment the returned block.
     */
    int (*new_block)(struct dm_space_map *sm, dm_block_t *b);

    /*
     * The root contains all the information needed to fix the space map.
     * Generally this info is small, so squirrel it away in a disk block
     * along with other info.
     */
    int (*root_size)(struct dm_space_map *sm, size_t *result);
    int (*copy_root)(struct dm_space_map *sm, void *copy_to_here_le, size_t len);

    /*
     * You can register one threshold callback which is edge-triggered
     * when the free space in the space map drops below the threshold.
     */
    int (*register_threshold_callback)(struct dm_space_map *sm,
                       dm_block_t threshold,
                       dm_sm_threshold_fn fn,
                       void *context);
};

通过实现以上的函数,并且使用指针,在C代码中实现了C++面向对象的技术。 sm_bootstrap_ops主要用来对metadata block进行初始化分配,并不需要对index bitmap进行查找。

/*
 * When a new space map is created that manages its own space.  We use
 * this tiny bootstrap allocator.
 */
static void sm_bootstrap_destroy(struct dm_space_map *sm)
{
}

static int sm_bootstrap_extend(struct dm_space_map *sm, dm_block_t extra_blocks)
{
    DMERR("bootstrap doesn't support extend");

    return -EINVAL;
}

static int sm_bootstrap_get_nr_blocks(struct dm_space_map *sm, dm_block_t *count)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    return smm->ll.nr_blocks;
}

static int sm_bootstrap_get_nr_free(struct dm_space_map *sm, dm_block_t *count)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    *count = smm->ll.nr_blocks - smm->begin;

    return 0;
}

static int sm_bootstrap_get_count(struct dm_space_map *sm, dm_block_t b,
                  uint32_t *result)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    return b < smm->begin ? 1 : 0;
}

static int sm_bootstrap_count_is_more_than_one(struct dm_space_map *sm,
                           dm_block_t b, int *result)
{
    *result = 0;

    return 0;
}

static int sm_bootstrap_set_count(struct dm_space_map *sm, dm_block_t b,
                  uint32_t count)
{
    DMERR("bootstrap doesn't support set_count");

    return -EINVAL;
}

static int sm_bootstrap_new_block(struct dm_space_map *sm, dm_block_t *b)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    /*
     * We know the entire device is unused.
     */
    if (smm->begin == smm->ll.nr_blocks)
        return -ENOSPC;

    *b = smm->begin++;

    return 0;
}

static int sm_bootstrap_inc_block(struct dm_space_map *sm, dm_block_t b)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    return add_bop(smm, BOP_INC, b);
}

static int sm_bootstrap_dec_block(struct dm_space_map *sm, dm_block_t b)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    return add_bop(smm, BOP_DEC, b);
}

static int sm_bootstrap_commit(struct dm_space_map *sm)
{
    return 0;
}

static int sm_bootstrap_root_size(struct dm_space_map *sm, size_t *result)
{
    DMERR("bootstrap doesn't support root_size");

    return -EINVAL;
}

static int sm_bootstrap_copy_root(struct dm_space_map *sm, void *where,
                  size_t max)
{
    DMERR("bootstrap doesn't support copy_root");

    return -EINVAL;
}

sm_metadata_ops对metadata block进行管理,需要用到index bitmap。

static void sm_metadata_destroy(struct dm_space_map *sm)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    kfree(smm);
}

static int sm_metadata_get_nr_blocks(struct dm_space_map *sm, dm_block_t *count)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    *count = smm->ll.nr_blocks;

    return 0;
}

static int sm_metadata_get_nr_free(struct dm_space_map *sm, dm_block_t *count)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    *count = smm->old_ll.nr_blocks - smm->old_ll.nr_allocated -
         smm->allocated_this_transaction;

    return 0;
}

static int sm_metadata_get_count(struct dm_space_map *sm, dm_block_t b,
                 uint32_t *result)
{
    int r;
    unsigned i;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);
    unsigned adjustment = 0;

    /*
     * We may have some uncommitted adjustments to add.  This list
     * should always be really short.
     */
    for (i = smm->uncommitted.begin;
         i != smm->uncommitted.end;
         i = brb_next(&smm->uncommitted, i)) {
        struct block_op *op = smm->uncommitted.bops + i;

        if (op->block != b)
            continue;

        switch (op->type) {
        case BOP_INC:
            adjustment++;
            break;

        case BOP_DEC:
            adjustment--;
            break;
        }
    }

    r = sm_ll_lookup(&smm->ll, b, result);
    if (r)
        return r;

    *result += adjustment;

    return 0;
}

// b is the block no, for example, bitmap_root no 1
static int sm_metadata_count_is_more_than_one(struct dm_space_map *sm,
                          dm_block_t b, int *result)
{
    int r, adjustment = 0;
    unsigned i;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);
    uint32_t rc;

    /*
     * We may have some uncommitted adjustments to add.  This list
     * should always be really short.
     */
    for (i = smm->uncommitted.begin;
         i != smm->uncommitted.end;
         i = brb_next(&smm->uncommitted, i)) {

        struct block_op *op = smm->uncommitted.bops + i;

        if (op->block != b)
            continue;

        switch (op->type) {
        case BOP_INC:
            adjustment++;
            break;

        case BOP_DEC:
            adjustment--;
            break;
        }
    }

    if (adjustment > 1) {
        *result = 1;
        return 0;
    }

    r = sm_ll_lookup_bitmap(&smm->ll, b, &rc);
    if (r)
        return r;

    if (rc == 3)
        /*
         * We err on the side of caution, and always return true.
         */
        *result = 1;
    else
        *result = rc + adjustment > 1;

    return 0;
}

static int sm_metadata_set_count(struct dm_space_map *sm, dm_block_t b,
                 uint32_t count)
{
    int r, r2;
    enum allocation_event ev;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    if (smm->recursion_count) {
        DMERR("cannot recurse set_count()");
        return -EINVAL;
    }

    in(smm);
    r = sm_ll_insert(&smm->ll, b, count, &ev);
    r2 = out(smm);

    return combine_errors(r, r2);
}

static int sm_metadata_inc_block(struct dm_space_map *sm, dm_block_t b)
{
    int r, r2 = 0;
    enum allocation_event ev;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    if (recursing(smm))
        r = add_bop(smm, BOP_INC, b);
    else {
        in(smm);
        r = sm_ll_inc(&smm->ll, b, &ev);
        r2 = out(smm);
    }

    return combine_errors(r, r2);
}

static int sm_metadata_dec_block(struct dm_space_map *sm, dm_block_t b)
{
    int r, r2 = 0;
    enum allocation_event ev;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    if (recursing(smm))
        r = add_bop(smm, BOP_DEC, b);
    else {
        in(smm);
        r = sm_ll_dec(&smm->ll, b, &ev);
        r2 = out(smm);
    }

    return combine_errors(r, r2);
}

static int sm_metadata_new_block_(struct dm_space_map *sm, dm_block_t *b)
{
    int r, r2 = 0;
    enum allocation_event ev;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    // find one block who is not used
    r = sm_ll_find_free_block(&smm->old_ll, smm->begin, smm->old_ll.nr_blocks, b);
    if (r)
        return r;

    smm->begin = *b + 1;


    if (recursing(smm))
        r = add_bop(smm, BOP_INC, *b);
    else {
        in(smm);
        r = sm_ll_inc(&smm->ll, *b, &ev);
        r2 = out(smm);
    }

    if (!r)
        smm->allocated_this_transaction++;

    return combine_errors(r, r2);
}

static int sm_metadata_new_block(struct dm_space_map *sm, dm_block_t *b)
{
    dm_block_t count;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    int r = sm_metadata_new_block_(sm, b);
    if (r) {
        DMERR_LIMIT("unable to allocate new metadata block");
        return r;
    }

    r = sm_metadata_get_nr_free(sm, &count);
    if (r) {
        DMERR_LIMIT("couldn't get free block count");
        return r;
    }

    check_threshold(&smm->threshold, count);

    return r;
}

// call metadata_commit to refresh the bitmap root block
static int sm_metadata_commit(struct dm_space_map *sm)
{
    int r;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    r = sm_ll_commit(&smm->ll);
    if (r)
        return r;

    // why make the begin from 0 ??
    memcpy(&smm->old_ll, &smm->ll, sizeof(smm->old_ll));
    smm->begin = 0;
    smm->allocated_this_transaction = 0;

    return 0;
}

static int sm_metadata_register_threshold_callback(struct dm_space_map *sm,
                           dm_block_t threshold,
                           dm_sm_threshold_fn fn,
                           void *context)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    set_threshold(&smm->threshold, threshold, fn, context);

    return 0;
}

static int sm_metadata_root_size(struct dm_space_map *sm, size_t *result)
{
    *result = sizeof(struct disk_sm_root);

    return 0;
}

static int sm_metadata_copy_root(struct dm_space_map *sm, void *where_le, size_t max)
{
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);
    struct disk_sm_root root_le;

    root_le.nr_blocks = cpu_to_le64(smm->ll.nr_blocks);
    root_le.nr_allocated = cpu_to_le64(smm->ll.nr_allocated);
    root_le.bitmap_root = cpu_to_le64(smm->ll.bitmap_root);
    root_le.ref_count_root = cpu_to_le64(smm->ll.ref_count_root);

    if (max < sizeof(root_le))
        return -ENOSPC;

    memcpy(where_le, &root_le, sizeof(root_le));

    return 0;
}

static int sm_metadata_extend(struct dm_space_map *sm, dm_block_t extra_blocks);

sm_disk_ops对data space map进行管理。使用了btree来进行bitmap index block的管理,而不是sm_metadata_ops里面的静态bitmap index block数组。

static void sm_disk_destroy(struct dm_space_map *sm)
{
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    kfree(smd);
}

static int sm_disk_extend(struct dm_space_map *sm, dm_block_t extra_blocks)
{
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    return sm_ll_extend(&smd->ll, extra_blocks);
}

static int sm_disk_get_nr_blocks(struct dm_space_map *sm, dm_block_t *count)
{
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);
    *count = smd->old_ll.nr_blocks;

    return 0;
}

static int sm_disk_get_nr_free(struct dm_space_map *sm, dm_block_t *count)
{
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);
    *count = (smd->old_ll.nr_blocks - smd->old_ll.nr_allocated) - smd->nr_allocated_this_transaction;

    return 0;
}

static int sm_disk_get_count(struct dm_space_map *sm, dm_block_t b,
                 uint32_t *result)
{
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);
    return sm_ll_lookup(&smd->ll, b, result);
}

static int sm_disk_count_is_more_than_one(struct dm_space_map *sm, dm_block_t b,
                      int *result)
{
    int r;
    uint32_t count;

    r = sm_disk_get_count(sm, b, &count);
    if (r)
        return r;

    return count > 1;
}

static int sm_disk_set_count(struct dm_space_map *sm, dm_block_t b,
                 uint32_t count)
{
    int r;
    uint32_t old_count;
    enum allocation_event ev;
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    r = sm_ll_insert(&smd->ll, b, count, &ev);
    if (!r) {
        switch (ev) {
        case SM_NONE:
            break;

        case SM_ALLOC:
            /*
             * This _must_ be free in the prior transaction
             * otherwise we've lost atomicity.
             */
            smd->nr_allocated_this_transaction++;
            break;

        case SM_FREE:
            /*
             * It's only free if it's also free in the last
             * transaction.
             */
            r = sm_ll_lookup(&smd->old_ll, b, &old_count);
            if (r)
                return r;

            if (!old_count)
                smd->nr_allocated_this_transaction--;
            break;
        }
    }

    return r;
}

static int sm_disk_inc_block(struct dm_space_map *sm, dm_block_t b)
{
    int r;
    enum allocation_event ev;
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    r = sm_ll_inc(&smd->ll, b, &ev);
    if (!r && (ev == SM_ALLOC))
        /*
         * This _must_ be free in the prior transaction
         * otherwise we've lost atomicity.
         */
        smd->nr_allocated_this_transaction++;

    return r;
}

static int sm_disk_dec_block(struct dm_space_map *sm, dm_block_t b)
{
    enum allocation_event ev;
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    return sm_ll_dec(&smd->ll, b, &ev);
}

static int sm_disk_new_block(struct dm_space_map *sm, dm_block_t *b)
{
    int r;
    enum allocation_event ev;
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    /* FIXME: we should loop round a couple of times */
    r = sm_ll_find_free_block(&smd->old_ll, smd->begin, smd->old_ll.nr_blocks, b);
    if (r)
        return r;

    smd->begin = *b + 1;
    r = sm_ll_inc(&smd->ll, *b, &ev);
    if (!r) {
        BUG_ON(ev != SM_ALLOC);
        smd->nr_allocated_this_transaction++;
    }

    return r;
}

static int sm_disk_commit(struct dm_space_map *sm)
{
    int r;
    dm_block_t nr_free;
    struct sm_disk *smd = container_of(sm, struct sm_disk, sm);

    // nr_free shoule be 0
    r = sm_disk_get_nr_free(sm, &nr_free);
    if (r)
        return r;

    // call ll_commit to refresh the tow btree nodes
    r = sm_ll_commit(&smd->ll);
    if (r)
        return r;

    // now we can copy ll into old_ll, but why ??
    memcpy(&smd->old_ll, &smd->ll, sizeof(smd->old_ll));
    smd->begin = 0;
    smd->nr_allocated_this_transaction = 0;

    // nr_free shoule be 0 again
    r = sm_disk_get_nr_free(sm, &nr_free);
    if (r)
        return r;

    return 0;
}

2.disk_ll

基础接口disk_ll定义了一系列函数,用来对metadata和data分区的bitmap index block相应管理。例如load_ie_fn就是从metadata磁盘加载一个顺序号为index的bitmap index block, save_ie_fn就是将顺序号为index的bitmap index block存储到metadata磁盘上。 typedef int (*load_ie_fn)(struct ll_disk *ll, dm_block_t index, struct disk_index_entry *result); typedef int (*save_ie_fn)(struct ll_disk *ll, dm_block_t index, struct disk_index_entry *ie); typedef int (*init_index_fn)(struct ll_disk *ll); typedef int (*open_index_fn)(struct ll_disk *ll); typedef dm_block_t (*max_index_entries_fn)(struct ll_disk *ll); typedef int (*commit_fn)(struct ll_disk *ll);

metadata_ll是对metadata space以bitmap index数组为管理目标的实现。

// get the disk_metadata_index from memory
static int metadata_ll_load_ie(struct ll_disk *ll, dm_block_t index,
                   struct disk_index_entry *ie)
{
    memcpy(ie, ll->mi_le.index + index, sizeof(*ie));
    return 0;
}

// save the disk_metadata_index into memory
static int metadata_ll_save_ie(struct ll_disk *ll, dm_block_t index,
                   struct disk_index_entry *ie)
{
    ll->bitmap_index_changed = true;
    memcpy(ll->mi_le.index + index, ie, sizeof(*ie));
    return 0;
}

// this is used to create the bitmap_root
static int metadata_ll_init_index(struct ll_disk *ll)
{
    int r;
    struct dm_block *b;

    // call sm_bootstrap_ops to get the block number (it's 1) and begin++
    r = dm_tm_new_block(ll->tm, &index_validator, &b);
    if (r < 0)
        return r;

    // copy disk_metadata_index into block data (4K)
    memcpy(dm_block_data(b), &ll->mi_le, sizeof(ll->mi_le));
    // now bitmap_root points to the block_nr 1
    ll->bitmap_root = dm_block_location(b);

    return dm_tm_unlock(ll->tm, b);
}

static int metadata_ll_open(struct ll_disk *ll)
{
    int r;
    struct dm_block *block;

    r = dm_tm_read_lock(ll->tm, ll->bitmap_root,
                &index_validator, &block);
    if (r)
        return r;

    memcpy(&ll->mi_le, dm_block_data(block), sizeof(ll->mi_le));
    return dm_tm_unlock(ll->tm, block);
}

static dm_block_t metadata_ll_max_entries(struct ll_disk *ll)
{
    return MAX_METADATA_BITMAPS;
}

// refresh the bitmap root block, the data of bitmap root block comes from metadata_ll disck_metadata_index
static int metadata_ll_commit(struct ll_disk *ll)
{
    int r, inc;
    struct dm_block *b;

    r = dm_tm_shadow_block(ll->tm, ll->bitmap_root, &index_validator, &b, &inc);
    if (r)
        return r;

    // copy the metadata_index_data into the block (4K)
    memcpy(dm_block_data(b), &ll->mi_le, sizeof(ll->mi_le));
    // now bitmap_root points to the new block
    ll->bitmap_root = dm_block_location(b);

    return dm_tm_unlock(ll->tm, b);
}

disk_ll是对data space以btree为管理目标的实现。

static int disk_ll_load_ie(struct ll_disk *ll, dm_block_t index,
               struct disk_index_entry *ie)
{
    return dm_btree_lookup(&ll->bitmap_info, ll->bitmap_root, &index, ie);
}

static int disk_ll_save_ie(struct ll_disk *ll, dm_block_t index,
               struct disk_index_entry *ie)
{
    __dm_bless_for_disk(ie);
    return dm_btree_insert(&ll->bitmap_info, ll->bitmap_root,
                   &index, ie, &ll->bitmap_root);
}

static int disk_ll_init_index(struct ll_disk *ll)
{
    return dm_btree_empty(&ll->bitmap_info, &ll->bitmap_root);
}

static int disk_ll_open(struct ll_disk *ll)
{
    /* nothing to do */
    return 0;
}

static dm_block_t disk_ll_max_entries(struct ll_disk *ll)
{
    return -1ULL;
}

static int disk_ll_commit(struct ll_disk *ll)
{
    return 0;
}

3.metadata space map的初始化

// superblock is 0, nr_blocks is the total block numbers (block_size is 4096)
int dm_sm_metadata_create(struct dm_space_map *sm,
              struct dm_transaction_manager *tm,
              dm_block_t nr_blocks,
              dm_block_t superblock)
{
    int r;
    dm_block_t i;
    enum allocation_event ev;
    struct sm_metadata *smm = container_of(sm, struct sm_metadata, sm);

    // begin is from 1
    smm->begin = superblock + 1;
    smm->recursion_count = 0;
    smm->allocated_this_transaction = 0;
    brb_init(&smm->uncommitted);
    threshold_init(&smm->threshold);

    memcpy(&smm->sm, &bootstrap_ops, sizeof(smm->sm));

    // below is using sm_bootstrap_opts
    r = sm_ll_new_metadata(&smm->ll, tm);
    if (r)
        return r;

    if (nr_blocks > DM_SM_METADATA_MAX_BLOCKS)
        nr_blocks = DM_SM_METADATA_MAX_BLOCKS;
    r = sm_ll_extend(&smm->ll, nr_blocks);
    if (r)
        return r;

    // from now on will use sm_metadata_opts

    memcpy(&smm->sm, &ops, sizeof(smm->sm));

    /*
     * Now we need to update the newly created data structures with the
     * allocated blocks that they were built from.
     */
    for (i = superblock; !r && i < smm->begin; i++)
        r = sm_ll_inc(&smm->ll, i, &ev);

    if (r)
        return r;

    return sm_metadata_commit(sm);
}
]]>
docker背后的存储之device mapper(二):dm-thin概述 2016-01-25T20:33:25+00:00 froghui http://froghui.github.io/device-mapper2 我们知道,docker在centOS上背后使用的存储技术是device mapper。本文将从device mapper实现的细节来讨论device mapper在docker中是如何使用的。

1.metadata disk block format

使用device mapper时,需要指定一个metadata disk和一个data disk。前者主要用来存储元数据,对metadata disk和data disk做管理。具体的格式如下

  • super block:超级块保存了整个metadata disk format的重要数据,其中data_space_map_root保存了对数据分区映射的重要数据(nr_blocks描述了整个数据分区的大小,nr_allocated描述了已分配的数据分区的大小,bitmap_root记录了对数据分区进行映射的bitmap root在元数据分区上的块号,ref_count_root记录了对数据分区进行映射的ref count root在元数据分区上的块号);metadata_space_map_root保存了对元数据分区映射的重要数据(nr_blocks描述了整个元数据分区的大小,nr_allocated描述了已分配的元数据分区的大小,bitmap_root记录了对元数据分区进行映射的bitmap root在元数据分区上的块号,ref_count_root记录了对元数据分区进行映射的ref count root在元数据分区上的块号);data mapping root记录了数据分区映射的btree的起点;

  • metadata space: 整个元数据分区按照4K大小分成block,每个block是否已经分配使用,使用的个数是多少等信息对于元数据分区的管理是非常重要的。metadata space这几个block数据就是对应这部分工作的。首先需要一个metadata space map root,这个root block对于需要管理的index bitmap block做索引管理。比如可以通过disk_index_entry[15]知道第16个index bitmap block位于metadata分区中的位置(blocknr),该index还可以分配出多少个空闲的entry(这个entry其实对应的就是metadata block nr)。每个index bitmap 数据块中用两个bits来映射一个entry,这两个bits表示映射到的block对应的ref count。注意到ref count >2以后该entry表示的ref count进入ref count btree。ref count btree的key为该entry的block nr, value为实际的ref count。

  • data space: 和metadata space类似,不过由于data space没有大小限制,对index的查找是通过btree完成的,而不是通过metadata space里面的静态数组完成。data bitmap btree的key为后面的data index bitmap的index (1...),而value为对应的落在metadata数据分区上的block nr。通过这一层的btree管理,可以快速的得到第N个data index bitmap的信息,进而可以读出整个data index bitmap。同metadata space一样,。每个index bitmap 数据块中用两个bits来映射一个entry,这两个bits表示映射到的block对应的ref count。注意到ref count >2以后该entry表示的ref count进入第二个btree,即ref count btree。ref count btree的key为该entry的block nr, value为实际的ref count。

  • data mapping: 这是一个两层的btree,第一层的key为device id, value为device root信息(block_nr),这一层可以通过查询device id定位到device root这个block。第二层的key为virtual block nr,value为一个64bits值,其中40bits表示physical block nr, 24bits表示该physical block使用的时间。这一层可以通过查询bio request的virtual block,定位到具体的物理磁盘号。注意这里的物理磁盘块大小是API显示指定的,例如128表示的是64K。

  • data details mapping: 这个btree的key为virtual block nr,value为detail值,可以知道该virtual block的使用状况。

2.主要交互流程:

2.1 create pool

dmsetup create 
docker-8:1-1055230-pool 
--table 
"0 209715200   
thin-pool   
/dev/loop0 /dev/sda3
128 32768 1 "

初始化metadata device(/dev/loop0)以及data device(/dev/sda3)。其中最主要的是初始化pool,对metadata device做初始化分割。按照block_size=4K,依次分割成super_block,metadata space map, data space map等。做完划分以后,将数据写入到metadata device磁盘中。 注意到这里对metadata device的切割大小是device mapper根据inode(/dev/loop0)读出的磁盘大小来完成的;而对data device的分割大小是根据指定的len(209715200)来完成的。

来看代码实现,实际上上述调用会连续的往ioctl发三条task run,create table, load table以及resume。

2.2 create base thin and activate

dmsetup message 
/dev/mapper/docker-8:1-1055230-pool
0
"create_thin 0”

在这个步骤会首先向metadata分区申请一个block,获得的block号为dev_root, 然后将<key,value>=<dev_id,dev_root>加入到data mapping btree的第一层中。 最后在transaction结束时将<key, disk_entry>=<dev_id, 对应的disk细节信息>加入到data device detail btree中。

dmsetup create 
docker-8:1-1055230-base
—addnodeoncreate 
--table "20971520 
thin 
/dev/mapper/docker-8:2-1055230-pool
0”

dm在这一步会产生/dev/mapper/docker-8:1-1055230-base这个gendisk,该gendisk表示了pool大磁盘的一个分区 可以被mkfs, mount等。可以往该gendisk分区写文件。

随后,docker 在createBaseImage这一步中对base disk进行格式化 mkfs.ext4 -E nodiscard,lazy_itable_init=0,lazy_journal_init=0 /dev/mapper/docker-8:1-1055230-base

注意2.1&2.2是docker在初始化的时候会完成的事情。后续的步骤在device mapper初始化以后不会再进行,而是从metadata分区读出元数据。

2.3 create snapshot and activate

dmsetup message 
/dev/mapper/docker-8:1-1055230-pool
0
"create_snap <childDeviceId> <baseDeviceId>"

将<key,value>=<childDevId,dev_root>加入到data mapping btree的第一层中。 dev_root为指向原baseDeviceId的所指向的dev_root。这样可以看出snapshot完成是指向同样的二层map tree,所以所有的映射数据是和basedeviceId完全一样的。

最后在transaction结束时将<key, disk_entry>=<dev_id, 对应的disk细节信息>加入到data device detail btree中

dmsetup create 
docker-8:1-1055230-<uuid>
—addnodeoncreate 
--table "20971520 
thin 
/dev/mapper/docker-8:2-1055230-pool
<childDeviceId>”

dm在这一步会产生/dev/mapper/docker-8:1-1055230-#{uuid} 这个gendisk,该gendisk表示了pool大磁盘的一个分区 可以被mount,mount以后可以往该gendisk分区写文件。一旦有些文件就会引起Copy-on-Write过程,新的childDeviceId的第二层mapping tree会和原来的baseDeviceId的那个mapping tree完全独立开来,只不过对数据块的引用计数ref count需要+1。

2.4 deactivate device of the container

dmsetup remove  docker-8:1-1055230-<uuid>

这个命令相当于告诉device mapper将/dev/mapper/docker-8:1-1055230-这个gendisk从系统中删除,但是对data block等的占用并没有杰出,也就是说随时可以从和tablesize等数据(docker将这份数据保存在/devicemapper/metadata文件夹下)重新产生一个gendisk并且可以被mount。

这个动作对应的device mapper driver的Put操作.umount文件系统并且调用该命令删除系统gendisk。当然,如果是删除一个container,第一步就是需要Put操作,然后再调用下一个命令,彻底的删除数据。

// Put unmounts a device and removes it.
func (d *Driver) Put(id string) error {
    err := d.DeviceSet.UnmountDevice(id)
    if err != nil {
        logrus.Errorf("Error unmounting device %s: %s", id, err)
    }
    return err
}

2.5 delete device of the container

对应在删除一个container时,需要两步,第一步就是前面的deactive device。

第二步,是将该对应的space map所引用的每一个data block的ref count--,这样后续的新的docker container可以使用这些data space。另外删除整个childDeviceId在two level data mapping中的数据,以及device detail mapping数据。做完这一步,data block的数据会真正的删除。

dmsetup message 
/dev/mapper/docker-8:1-1055230-pool
0
delete <childDeviceId>”

docker在删除device之前,会根据配置参数发起一个blkDiscard操作,目的是通知device mapper回收所有的data block。但是这一步并不是必须的,原因是在后面发起的message delete ,device mapper的执行逻辑会将所有该device占有的data block做回收。

// Should be called with devices.Lock() held.
func (devices *DeviceSet) deleteDevice(info *devInfo, syncDelete bool) error {
    if devices.doBlkDiscard {
        devices.issueDiscard(info)
    }

    // Try to deactivate device in case it is active.
    if err := devices.deactivateDevice(info); err != nil {
        logrus.Debugf("Error deactivating device: %s", err)
        return err
    }

    //在这里发起dmsetup message delete <childDeviceId>命令
    if err := devices.deleteTransaction(info, syncDelete); err != nil {
        return err
    }

    devices.markDeviceIDFree(info.DeviceID)

    return nil
}
]]>
docker背后的存储之device mapper(一):模块和框架 2016-01-21T20:33:25+00:00 froghui http://froghui.github.io/device-mapper 我们知道,docker在centOS上背后使用的存储技术是device mapper。本文将从device mapper实现的细节来讨论device mapper在docker中是如何使用的。

1.模块

device mapper的实现是按照linux模块来做的,主要模块及其依赖如下。

主要模块的功能:

  • dm_mod: 这是最底层的一个模块,包括dm_target的接口,dm, dm_table, dm_io以及dm_ioctl。
  • dm_bufio: 操作metadata 底层IO的接口。
  • dm_pesistent_data: device mapper提供的一个metadata和data的持久层操作,包括dm_space_map完成block data的元数据管理,dm_btree完成btree的维护操作。
  • dm_thin_pool: 包括dm_thin和dm_thin_metadata,支持docker使用的thin pool和snapshot的功能。

device mapper对外暴露的API是通过暴露ioctl的达到的。具体来说,当使用dmsetup的command或者API时,由dm_mod模块的dm_ioctl对相应的API指令做出响应,dm_target汇聚了所有可能的target_type,其中docker使用的是dm_thin_pool module里面注册 的pool_target和thin_target。最重要的一个指令是dmsetup create创建mapped_device及其gendisk对象,这会在kernel中注册几个磁盘 如dm-0(pool), dm-1(docker 1), dm-2..。

2.框架及实现

linux block device主要通过暴露一个gendisk给系统,这个gendisk对象关联到一堆operations 函数,分别用来对该设备进行一些处理;另外通过产生bio request,最终bio request进入到和gendisk绑定的make_request_fn以及队列request_queue进行调度处理。其结构可以看下图:

3.debug环境搭建

在src目录建立一个run.sh如下,然后可以对drirvers/md下的文件打一些log,通过dmesg或者tail -f /var/log/messages来观察输出。

#!/bin/bash

#compile the module
make M=drivers/md

#copy the ko file
sudo cp drivers/md/dm-mod.ko /lib/modules/2.6.32-431.29.2.el6.x86_64/kernel/drivers/md/dm-mod.ko
sudo cp drivers/md/persistent-data/dm-persistent-data.ko /lib/modules/2.6.32-431.29.2.el6.x86_64/kernel/drivers/md/persistent-data/dm-persistent-data.ko
sudo cp drivers/md/dm-thin-pool.ko /lib/modules/2.6.32-431.29.2.el6.x86_64/kernel/drivers/md/dm-thin-pool.ko

#generate dependency
sudo depmod -a

#need to stop docker first so that we can remove modules
sudo service docker stop

#reload into the mormory, we need to remove the leaf modules from memory first, then install them
#modprobe will handle the module dependencies for us
sudo modprobe -r dm_thin_pool
sudo modprobe -r dm_mirror
sudo modprobe libcrc32c
sudo modprobe dm_mirror
sudo modprobe dm_thin_pool

sleep 2
#sudo service docker start
]]>
Linux内核的cgroup实现(一):Overview 2015-12-21T20:33:25+00:00 froghui http://froghui.github.io/cgroup-implementation 我们知道,docker背后实现的技术主要是linux namespace和linux cgroup(control group),本文就从内核的角度来分析一下linux cgroup究竟是如何实现的。本文研读的代码是linux kernel 2.6.32。

1.数据结构:

1.1 task_struct

每个进程的数据结构,通过css_set可以描述当前的进程和哪些cgroup关联,以及关联的子系统有哪些。对每个进程而言,每种类型的子系统最多可以关联一个cgroup。

1.2 css_set

css_set和一个或几个task相关联,描述一个任务属于哪些cgroup。通过该group内的task fork出来的child task也必然在该css_set内。如果某个task被放到/sys/cgroup/cpuset /sys/cgroup/mememry两个cgroup中,就使用一个css_set将这两个cgroup和css_set关联。 从kernel角度出发,一个task一定有一个css_set与之相关联。css_set是task和cgroup之间的桥梁。css_set可以有一个或多个子系统注册。

1.3 cg_cgroup_link

用来将css_set和cgroup做关联。

1.4 cgroup:

一个cgroup可以有一个或多个子系统注册,例如一个子系统cpu注册的cgroup: /cgroup/cpu/docker/{uuid}/
同时cpu, mememory注册的cgroup
/cgroup/cpu_and_mem/{uuid}/

每个cgroup对应一个mount到的cgroup文件系统,一个cgroup可以有多个子系统注册,多个子系统注册只需要在指定mount -t的时候带不同的子系统配置。

/cgroup/cpu (cpu)
/cgroup2/cpu (cpu)
/cgroup2/cpu_and_memory (cpu,memory)
/cgroup/cpu/docker (cpu)

在cgroup下通过mkdir新建的child group 如/cgroup/cpu/docker/{uid}也会创建新的cgroup。

1.5 cgroup_subsys_state

每个子系统的每次mount 都有一个cgroup_subsys_state与之对应,例如cpu和memory的cgroup_subsys_state

/cgroup/cpu (cpu state)
/cgroup/memory (memory state)
/cgroup2/cpu (cpu state)
/cgroup2/cpu_and_memery (cpu state)
/cgroup2/cpu_and_memery (memory state)

1.6 cgroup_subsys

内核可以支持的子系统。每个子系统必须附属于一个cgroupfs_root对象,一个唯一的subsys_id可以标示子系统。子系统提供相应的函数实现,必要的时候需要在内核中做适当的处理(例如cpuset需要在调度算法里按指定的cpu进行调度)

1.7 cgroupfs_root

cgroup的根对象,该对象在系统中mount完产生,其数目是有限的。比如 /cgroup/cpu, /cgroup/memory等2个cgroupfs_root。当然如果/cgroup/cpu_and_memery只有一个cgroupfs_root。通过cgroupfs_root可以关联到cgroup_subsys子系统,例如/cgroup/cpu关联到cpu的cgroup_subsys,而/cgroup/cpu_and_memery关联到cpu和memory两个cgroup_subsys。

struct cpuset {
    struct cgroup_subsys_state css;

    unsigned long flags;        /* "unsigned long" so bitops work */
    cpumask_var_t cpus_allowed;    /* CPUs allowed to tasks in cpuset */
    nodemask_t mems_allowed;    /* Memory Nodes allowed to tasks */

    struct cpuset *parent;        /* my parent */

    struct fmeter fmeter;        /* memory_pressure filter */

    /* partition number for rebuild_sched_domains() */
    int pn;

    /* for custom sched domain */
    int relax_domain_level;

    /* used for walking a cpuset heirarchy */
    struct list_head stack_list;
};

2.用户case

2.1初始化

向系统注册 cgroup类型的文件系统

//该函数初始化cgroup的树状结构,主要是init_css_set, rootnode, dummpytop等。
int __init cgroup_init_early(void)
{
    int i;
    atomic_set(&init_css_set.refcount, 1);
    INIT_LIST_HEAD(&init_css_set.cg_links);
    INIT_LIST_HEAD(&init_css_set.tasks);
    INIT_HLIST_NODE(&init_css_set.hlist);
    css_set_count = 1;
    init_cgroup_root(&rootnode);
    root_count = 1;
    init_task.cgroups = &init_css_set;

    init_css_set_link.cg = &init_css_set;
    init_css_set_link.cgrp = dummytop;
    list_add(&init_css_set_link.cgrp_link_list,
         &rootnode.top_cgroup.css_sets);
    list_add(&init_css_set_link.cg_link_list,
         &init_css_set.cg_links);

    for (i = 0; i < CSS_SET_TABLE_SIZE; i++)
        INIT_HLIST_HEAD(&css_set_table[i]);

    for (i = 0; i < CGROUP_SUBSYS_COUNT; i++) {
        struct cgroup_subsys *ss = subsys[i];

        BUG_ON(!ss->name);
        BUG_ON(strlen(ss->name) > MAX_CGROUP_TYPE_NAMELEN);
        BUG_ON(!ss->create);
        BUG_ON(!ss->destroy);
        if (ss->subsys_id != i) {
            printk(KERN_ERR "cgroup: Subsys %s id == %d\n",
                   ss->name, ss->subsys_id);
            BUG();
        }

        if (ss->early_init)
            cgroup_init_subsys(ss);
    }
    return 0;
}
```


```C
/**
 * cgroup_init - cgroup initialization
 *
 * Register cgroup filesystem and /proc file, and initialize
 * any subsystems that didn't request early init.
 */
int __init cgroup_init(void)
{
    int err;
    int i;
    struct hlist_head *hhead;

    err = bdi_init(&cgroup_backing_dev_info);
    if (err)
        return err;

    for (i = 0; i < CGROUP_SUBSYS_COUNT; i++) {
        struct cgroup_subsys *ss = subsys[i];
        if (!ss->early_init)
            cgroup_init_subsys(ss);
        if (ss->use_id)
            cgroup_subsys_init_idr(ss);
    }

    /* Add init_css_set to the hash table */
    hhead = css_set_hash(init_css_set.subsys);
    hlist_add_head(&init_css_set.hlist, hhead);
    BUG_ON(!init_root_id(&rootnode));
    err = register_filesystem(&cgroup_fs_type);
    if (err < 0)
        goto out;

    proc_create("cgroups", 0, NULL, &proc_cgroupstats_operations);

out:
    if (err)
        bdi_destroy(&cgroup_backing_dev_info);

    return err;
}

2.2 mount cgroup

如果在用户空间执行 mount -t cgroup -o cpu,cpuset,memory /sysgroup/cpu_and_mem 那么下面的函数将会被调用

static int cgroup_get_sb(struct file_system_type *fs_type,
             int flags, const char *unused_dev_name,
             void *data, struct vfsmount *mnt)

该函数首先初始化一个cgroupfs_root对象,cgroupfs_root结构里面包括了一个cgroup对象,初始化该cgroup对象使其成为一个top cgroup。所谓top cgroup就是最顶层的cgroup对象,该cgroup的top_cgroup指向自己。

static void init_cgroup_root(struct cgroupfs_root *root)
{
    struct cgroup *cgrp = &root->top_cgroup;
    INIT_LIST_HEAD(&root->subsys_list);
    INIT_LIST_HEAD(&root->root_list);
    root->number_of_cgroups = 1;
    cgrp->root = root;
    cgrp->top_cgroup = cgrp;
    init_cgroup_housekeeping(cgrp);
}

接下来根据指定的option对每个需要加载的subsys进行处理,将top cgroup里面的subsys指针初始化,并将对应的cgroup_subsys链接到cgroupfs_root中

            cgrp->subsys[i] = dummytop->subsys[i];
            cgrp->subsys[i]->cgroup = cgrp;
            list_move(&ss->sibling, &root->subsys_list);
            ss->root = root;

之后,将该top group 加入到root列表中; 紧接着,因为每个任务对应一个css_set,将每个任务对应的css_set和当前的root_cgroup做关联,也就算将每个任务默认的加入到root cgroup中 最后,在root dir下对每个cgroup_subsys按照该subsystem的规则生成控制文件

在2.6 kernel中cpu, cpuset, memory等subsystem仅可以加入一个group,不允许某个subsystem加入到多个group中。 例如下面试图将cpu加到两个cgroup就会失败

mount -t cgroup -o cpu,memory cpu_and_mem /tmp/cpu_and_mem
mount -t cgroup -o cpu cgroup /cgroup/cpu

2.3.mkdir group1 under mounted dentry

对应的是在root cgroup下创建子cgroup,函数

static int cgroup_mkdir(struct inode *dir, struct dentry *dentry, int mode)

负责处理该请求。

该函数首先对该cgroup下注册的的每个subsys创建cgroup_subsys_state对象,用来跟踪状态。cgroup_subsys_state对象是通过调用cgroup_subsys提供的函数create生成, 例如

struct cgroup_subsys_state *css = ss->create(ss, cgrp);

接着会创建新的cgroup对象,并建立新的cgroup和parent cgroup, cgroupfs_root等直接的层级关系。 最后调用new indoor在当前目录下创建cgroup.event_control cgroup.procs notify_on_release tasks release_agent等控制文件,同时调用cgroup_subsys的populate方法创建子系统额外的控制文件

2.4. echo $PID > tasks

这个操作会往当前cgroup中加入对应的task,具体的做法是通过调用函数

int cgroup_attach_task(struct cgroup *, struct task_struct *);

该函数首先对每个注册在当前cgroup下的cgroup_subsys,依次调用其提供的函数:

ss->can_attach
ss->can_attach_task(cgrp)
ss->pre_attach(cgrp);
ss->attach_task(cgrp,
ss->attach(ss, cgrp, oldcgrp, tsk, false);

来完成cgroup_subsys的加入,完成这一步之后标志着对该task的cgroup控制在内核开始生效。 接着对task_struct中的css_set数据进行处理,将该task_struct和对应的cgroup和cgroup_subsys_state做关联。 注意默认该task在top cgroup(cgroup_root)中,所以必定需要做一次cgroup migration, 将该task从一个css_set移到新的css_set,并和当前新的cgroup关联。

2.5. fork process from cgroup

和2.4类似,也是两个主要目的,一个是完成task_struct的关联工作;另一个是完成cgroup_subsys的回调将该forked task加入到cgroup中完成控制。

这几个目的是在fork系统调用中通过调用下面的三个函数来完成的。 先看第一步,将该task加入到对应的css_set中。task_struct的css_set指针指向父进程的css_set并增加引用计数。

void cgroup_fork(struct task_struct *child)
{
    task_lock(current);
    child->cgroups = current->cgroups;
    get_css_set(child->cgroups);
    task_unlock(current);
    INIT_LIST_HEAD(&child->cg_list);
}

第二步对每个cgroup_subsys调用fork操作,完成cgroup的控制。

void cgroup_fork_callbacks(struct task_struct *child)
{
    if (need_forkexit_callback) {
        int i;
        for (i = 0; i < CGROUP_SUBSYS_COUNT; i++) {
            struct cgroup_subsys *ss = subsys[i];
            if (ss->fork)
                ss->fork(ss, child);
        }
    }
}

第三步将task_struct加入到css_set的链表中,以便从css_set可以遍历所管理的tasks。

void cgroup_post_fork(struct task_struct *child)
{
    if (use_task_css_set_links) {
        write_lock(&css_set_lock);
        task_lock(child);
        if (list_empty(&child->cg_list))
            list_add(&child->cg_list, &child->cgroups->tasks);
        task_unlock(child);
        write_unlock(&css_set_lock);
    }
}

2.6. 具体实现

各个子系统在内核根据具体的代码逻辑有不同的实现,比如cpu和cpuset主要是在schedule过程中做相应的调度。这部分内容会在后续的系列文章中展开。

  • cpu: 按share调度cpu
  • cpuset: 限制某些cpu node可以使用
  • cpuacct: cpu使用统计
  • memory: 内存使用限制到bytes
  • blkio: 限制block device的读写速度。

2.7. umount /sys/cgroup/cpu_and_mem

static void cgroup_kill_sb(struct super_block *sb) {

]]>
Linux 2.6.32 进程调度 2015-11-22T20:33:25+00:00 froghui http://froghui.github.io/linux26-kernel 1.linux2.6.32使用的trap gate和interrupt gate

Intel X86体系中有中断和异常之分,

  • 中断发生在程序执行的任意时刻,一般来自外部硬件产生得事件,例如磁盘,网卡等I/O设备中断,中断一般和执行指令没有直接的关系,可以理解中断是Asynchronous(异步)的。
  • 异常一般是执行指令时由处理器捕捉到的错误,分为Fault, Trap和Aborts。另外一种异常是软中断/系统调用,用来和系统内核交互。 异常和执行指令有直接关系,比如执行了错误的指令,调用int系统调用等。从这层意义上理解可以说异常是同步(Synchronous)的。
    -- Faults — correctable; offending instruction is retried
    -- Traps — often for debugging; instruction is not retried
    -- Aborts — major error (hardware failure)

Linux2.6.32中主要使用了x86提供的中断门和陷阱们,按照linux2.6.32的使用方法,IDT中断描述符有3种

  • interupte_gate dpl:00(仅对内核态开放) type:01110(interrupt gate IF=0)

所有的IRQ(0x20-0x2f) 包括timer hd
几乎所有的异常(divide error, page fault)

  • system_intr_gate dpl:11(用户态开放) type:01110 (interrupt gate IF=0)

0x03 int3
0x04 overflow

  • system_trap_gate dpl:11(用户态开放) type:01111 (trap gate IF=1)

0x80 system_call

 arch/x86/kernel/irqinit.c
 arch/x86/kernel/traps.c

    set_intr_gate(0, &divide_error);
    set_intr_gate_ist(1, &debug, DEBUG_STACK);
    set_intr_gate_ist(2, &nmi, NMI_STACK);
    /* int3 can be called from all */
    set_system_intr_gate_ist(3, &int3, DEBUG_STACK);
    /* int4 can be called from all */
    set_system_intr_gate(4, &overflow);
    set_intr_gate(5, &bounds);
    set_intr_gate(6, &invalid_op);
    set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32
    set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
    set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
    set_intr_gate(9, &coprocessor_segment_overrun);
    set_intr_gate(10, &invalid_TSS);
    set_intr_gate(11, &segment_not_present);
    set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);
    set_intr_gate(13, &general_protection);
    set_intr_gate(14, &page_fault);
    set_intr_gate(15, &spurious_interrupt_bug);
    set_intr_gate(16, &coprocessor_error);
    set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCE
    set_intr_gate_ist(18, &machine_check, MCE_STACK);
#endif
    set_intr_gate(19, &simd_coprocessor_error);

    /* Reserve all the builtin and the syscall vector: */
    for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
        set_bit(i, used_vectors);

#ifdef CONFIG_IA32_EMULATION
    set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
    set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif

IF=0表示中断当前中断线上的屏蔽,会在所有处理器上屏蔽掉当前同一中断线,这样可以防止同一中断线上接收另一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。由此可见,同一个中断处理程序绝不会被同时调用产生同一个处理中断函数被嵌套的情况。而且,用这种方式也控制了整个中断处理栈的深度,内核最多可以支持15个IRQ同时在线,也就是说最大的内核中断处理栈的深度为15。

时钟片的任务切换(timer_interrupt)就是这样的一个中断,所以每次一定只有一个时间片处理中断发生,将CPU调度成功。

系统调用是通过trap_gate做的,很容易被中断掉,典型的是陷入系统调用以后被磁盘中断掉,注意Linux2.4之前都是内核非抢占的,陷于system_call的内核态系统调用不会被时钟中断调度出去。

2.关于中断/异常的嵌套执行

  • 首先,0.12中也是运行中断/异常的嵌套执行的,比如系统调用陷入内核,磁盘I/O完成转而去执行hd_interrupt。这样形成一个int 0x80 + hd_interrupt的内核控制路径。
  • 2.6.32当然也允许中断内核控制路径嵌套执行。

3.linux2.6.32和linux0.12主要的不同点

  • 除了软中断/系统调用以外的中断(IRQ0-IRQ15)和异常(0-32)都使用了中断门,也就是说针对这些中断和异常的处理都是仅有一个处理函数被允许,不可以嵌套同一个处理函数;而在0.12中仅仅是IRQ0-15是使用中断门的,异常(0-32)使用的是trap gate。
  • 抢占式内核的概念:运行在内核态的软中断/系统调用,会被中断处理程序打断,常见的是I/O设备如磁盘,软盘,网卡,这种打断在linux0.12中本身就是可以支持的,这是中断处理程序本身应有之意,并不是抢占式内核的含义。这里抢占式内核的含义是,当时钟中断来临时,检查当前运行在内核态的软中断/系统调用,如果配置可以被抢占,并且此时系统中确实有更高优先级的任务需要执行,则调用schedule()将当前进程保持在内核态的软中断/系统调用,而转而去执行另外的任务。等到下一次调度时候,本次被打断的任务继续在内核态执行。而在0.12中,时钟中断来临只会对任务进行计时,如果任务运行在内核态并不会做重新调度。
  • 引入了中断上下文,异常(包括软中断/系统调用)是在进程的上下文中执行,而硬件引起的中断是在中断上下文中执行,和进程无关。中断上下文也被称作原子上下文,该上下文中得执行代码不可阻塞,不可以进程切换。(问题: 时钟中断是如何进行进程切换的?)0.12中无论是异常还是中断都是在被打断的进程上下文中执行,
  • 引入上半部和下半部,将耗时比较长的任务放到下半部去执行。0.12没有下半部的概念,中断都是在一个处理函数中执行的。
  • 2.6.32抢占的情形
    用户抢占:
    1) 从系统调用(read/write/fork/exec等)返回用户空间时(注意是在中断处理程序执行完后进行schedule的)
    2) 从中断处理程序(hd/network/timer等)返回用户空间时(注意是在中断处理程序执行完后进行schedule的)

内核抢占:
1)中断处理程序(hd/network/timer等)正在执行,且返回内核空间之前;
2)释放锁的内核代码立即检查可否进行内核抢占;
3)显示调用schedule;
4)任务阻塞(调用schedule)

  • 0.12抢占的情形
    用户抢占:
    1)从系统调用(read/write/fork/exec等)返回用户空间时(注意是在中断处理程序执行完后进行schedule的)
    2)从时钟中断程序(timer)返回用户空间时(注意是在中断处理程序do_timer中进行schedule的)

内核抢占:
1)显示调用schedule;
2)任务阻塞(调用schedule) * 和0.12相比,用户抢占的情况基本相同,新的内核增加了除timer以外的其他中断处理程序返回用户空间时抢占的可能;在内核抢占方面增加了从中断处理程序返回内核空间时的抢占,以及内核代码中再一次具有抢占性时刻的抢占,从而使得内核态任务的抢占成为可能。

4.几个问题

问题1: 时钟中断是如何进行进程切换的?不是说中断处理是不可以阻塞,不可以进程切换的么? 回答: 时钟中断处理程序tick_periodic作为硬件中断的一种,并不进行进程切换,只进行计时操作。当中断程序执行完以后再根据情况进行schdule。这和0.12中直接在do_timer中做schedule是不同的。

问题2:软中断/系统调用+异常(do_no_page)最多两层嵌套?

]]>
centos sshpass+ssh无法执行remote shell 2015-11-16T12:33:25+00:00 froghui http://froghui.github.io/sshpass-ssh 问题

今天在一台物理机上执行一段远程bash,发现没有任何响应和输出。代码形如

sshpass -p password -p58422 root@10.101.10.19 "ifconfig"

该台机器的open-ssh和open-ssl版本如下

ssh -v
OpenSSH_5.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013

但是很奇怪的是在另一台物理机上执行同样的bash能成功。而且最为吊诡的是换成另一个目标ip在出问题的物理机上又成功了。

sshpass -p password -p58422 root@10.101.10.20 "ifconfig"

这是神马情况?考虑可能是open-ssh的bug,于是升级了open-ssh,然并卵!

最后发现,这是一个sshpass结合StrictHostKeyChecking出现的问题。sshpass就是一个简单的密码补齐工具,非常类似expect。 如果没有在/etc/ssh/ssh_config和~/.ssh/config等下面配置过StrictHostKeyChecking,然后使用ssh登陆,默认行为是这样的:

ssh -p58422 root@10.101.20.19
The authenticity of host '[10.101.20.19]:58422 ([10.101.20.19]:58422)' can't be established.
RSA key fingerprint is 4c:60:31:94:0e:50:39:af:2b:7f:b2:1f:08:9b:73:31.
Are you sure you want to continue connecting (yes/no)?

在这种情况下,捆绑sshpass和ssh使用,就会像石沉大海一样,没有任何反应。其实很好理解,sshpass希望是看到如下字符:

root@10.101.20.19's password:

以便很高兴的把密码填上,登陆进入。然而,对于这样yes/no的回答,它就傻眼了。

解决方法

解决的方法很简单,不要让StrictHostKeyChecking出现交互式回答。有两种方式:

  • 配置一下 /etc/ssh/ssh_config或者~/.ssh/config
    StrictHostKeyChecking no
  • 在ssh 调用时显示指定选项
    sshpass -p password -p12345 -o StrictHostKeyChecking=no root@10.101.10.19 "ifconfig"

现在明白为什么换一台物理机执行没问题了把,答案是第二台机器在~/.ssh/config配置了StrictHostKeyChecking no

而诡异的换一个IP可以的原因是10.101.20.20已经把RSA 加到了~/.ssh/knownhosts中。

关于StrictHostKeyChecking

如果试着在~/.ssh/config配置了StrictHostKeyChecking yes,再使用sshpass+ssh,这个时候ssh就会先报错,然后我们就知道发生了什么。

sshpass -p password ssh -p58422 root@10.101.20.19
No RSA host key is known for [10.101.20.19]:58422 and you have requested strict checking.
Host key verification failed.

所以,关于StrictHostKeyChecking 有至少三种模式

  • no 这种模式下显示warning,然后自动的加入到~/.ssh/knownhosts
Warning: Permanently added '[10.101.20.19]:58422' (RSA) to the list of known hosts.
  • yes 严格验证RSA
  • checking 需要用户交互式回答,这个是默认模式

显然,只有模式no能和sshpass一起配合使用。

]]>
git中无commit的push 2015-11-10T12:33:25+00:00 froghui http://froghui.github.io/git-push-without-commit 缘起

今天在看代码时,发现某位同事的某次代码commit,在主页面activities视图是可见的,但是到了Commits视图却怎么也看不到这个commit, git log也不能显示这个commit。后来猜测是这位同事发现该commit有问题,通过push -f的方式直接将这次commit从remote branch中取消了。这样看上去从历史活动页面有这个commit,但是从代码的Files/Commits视图都看不到这次commit。

验证

为了证实我的想法,我试着按以下步骤做了一下

git checkout -b testdev dev
git push origin testdev    (@0197802400)

echo "this is just a text" > test.txt
git commit -m "this is just a test"
git push origin testdev

git reset --hard  0197802400
git push -f origin testdev

证明确实如此,而且最后一次push -f的更新不会显示commit Id。

]]>
一步步打造一个迷你linux container 2015-09-10T11:33:25+00:00 froghui http://froghui.github.io/mini-container 概述

Linux container技术现在可谓是如火如荼,尤其以docker为代表(更早一点的有lxc, warden等)。虽然linux container是一种进程级别的技术,但由于其使用了linux kernel提供的资源隔离和限制的功能,可以“伪装”出一台小的"虚拟机",并且这台小虚拟机占用资源少,“虚拟”速度快,因而被一致的认为是很有前途的超越虚拟机的技术。

要理解这门技术,最快的方法就是从头自己build 一个mini container。本文的目的就是教你如何一步一步的创造出自己的linux container。( 全部code在source code of mini container)

总体上,我们需要一个父进程负载创建linux container子进程,子进程内执行必要的程序。父进程负载管理linux container的生命周期。

父进程

这里的父进程负责

  • 使用unicon fs技术准备rootfs,这样可以在子进程内使用pivot_root或者chroot方法将rootfs直接切换到准备好的rootfs中。
  • 调用clone方法,准备namespace NEWIPC NEWUTS NEWPID NEWNS NEWNET.
  • 在子进程创建之后设置cgroup
  • 在子进程创建之后设置主机的network,同时通知子进程去配置自己namespace中的network.
  • 等待子进程退出
    //使用一对pipe和子进程通信   
    pipe(pipes);
    printf(" parent pid:: [%5d] \n",getpid());
    //unicorn id for the container
    char * unicorn_id = malloc(11);
    random_string(unicorn_id, 10);

    //net configuration
    net_t *n = calloc(1, sizeof(net_t));;
    assert(n != NULL);
    n->unicorn_id = unicorn_id;
    char * hostname = malloc(20);
    sprintf(hostname,"unicorn-%s",unicorn_id);
    n->hostname = hostname;
    n->mtu=1500;
    n->ip=ip;
    n->netmask=mask;
    n->gateway=gw;
    //prepare auts and then clone
    //这里使用了aufs准备rootfs目录,同时调用clone方法准备命名空间
    prepare_rootfs(mount_base, unicorn_id, rootfs_base);
    int child_pid = clone(child_main, child_stack+STACK_SIZE,  CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, n);

    //prepare unicorn info
    unicorn_t *u = calloc(1, sizeof(unicorn_t));;
    assert(u != NULL);
    u->child_pid = child_pid;
    u->parent_pid = getpid();
    u->unicorn_id = unicorn_id;
    //配置cgroup
    cgroup_add(u);
    //配置主机上的network
    net_add(u);
    printf(" child pid from parent:: [%5d] \n",child_pid);
    sleep(1);
    //call child to prepare the inner ethenet
    close(pipes[1]);
    //wait to die
    waitpid(child_pid, NULL, 0);
    return 0;
    //prepare_rootfs主要使用aufs准备rootfs目录
    int prepare_rootfs(char * mount_base, char * unicorn_id, char * rootfs_base){  
    //use aufs to create union target rootfs
    char rootfs_path[100];
    check_result(sprintf(rootfs_path,"%s/%s",mount_base,unicorn_id), rootfs_path);
    check_mkdir(mkdir(rootfs_path, 0755), "mkdir root");
    check_result(sprintf(rootfs_path,"%s/rootfs",rootfs_path), rootfs_path);
    check_mkdir(mkdir(rootfs_path,0755), "mkdir rootfs");
    char mount_copy_on_write_dst[100];
    check_result(sprintf(mount_copy_on_write_dst,"%s/%s-init",mount_base,unicorn_id),mount_copy_on_write_dst);
    check_mkdir(mkdir(mount_copy_on_write_dst,0755),"mkdir copy_on_write dir");

    // mount -n -t aufs -o br:$mount_copy_on_write_dst=rw:$rootfs_base=ro+wh none rootfs_path
    char mount_data[100];
    check_result(sprintf(mount_data,"br:%s=rw:%s=ro+wh",mount_copy_on_write_dst, rootfs_base), mount_data);
    printf("rootfs_path:: %s mount_opt:: %s \n", rootfs_path, mount_data);
    check_result(mount("none",rootfs_path,"aufs",0, mount_data),"mount rootfs_base to rootfs dir");

    return 0;
//一旦子进程创建,根据子进程的pid,创建对应的cgroup,并将其加入。这样该进程以及该进程产生的新进程都会受到资源控制
char * const cgroup_base_dir="/tmp/cgroup";
char* cgroup_subsystems[] = {"cpu", "cpuset", "cpuacct", "memory", "blkio"};
int cgroup_subsystems_lengh = 5;

int cgroup_add(unicorn_t * u){
    int i;
    for (i=0; i< cgroup_subsystems_lengh; i++){
        char system_path[80];
        check_result(sprintf(system_path, "%s/%s", cgroup_base_dir, cgroup_subsystems[i]),system_path);

        char cmd[100];
        check_result(sprintf(cmd, "mkdir -p %s/%s", system_path, u->unicorn_id),cmd);
        check_result(system(cmd), "system call mkdir ");

        if( strcmp(cgroup_subsystems[i], "cpuset") == 0){
            check_result(sprintf(cmd, "cat %s/cpuset.mems > %s/%s/cpuset.mems", system_path, system_path, u->unicorn_id),cmd);
            check_result(system(cmd), "system call add cpuset.mems ");

            check_result(sprintf(cmd, "cat %s/cpuset.cpus > %s/%s/cpuset.cpus", system_path, system_path, u->unicorn_id),cmd);
            check_result(system(cmd), "system call add cpuset.cpus ");

        }
        check_result(sprintf(cmd, "echo %d > %s/%s/tasks", u->child_pid, system_path, u->unicorn_id),cmd);
        check_result(system(cmd), "system call add pid ");
    }

    return 0;
}
//网络方面的配置,使用了虚拟网卡对和bridge
int net_add(unicorn_t * u){
    //ip link add name u-${unicorn-id}-0 type veth peer name u-${unicorn-id}-1
    //ip link set u-${unicorn-id}-0 netns 1
    //ip link set u-${unicorn-id}-1 netns $PID
    //brctl addif ${bridge} u-${unicorn-id}-0   
    char cmd[100];
    check_result(sprintf(cmd, "ip link add name u-%s-0 type veth peer name u-%s-1", u->unicorn_id, u->unicorn_id),cmd);
    check_result(system(cmd), "system call ip link add ");

    check_result(sprintf(cmd, "ip link set u-%s-0 netns 1", u->unicorn_id),cmd);
    check_result(system(cmd), "system call ip link set ");


    check_result(sprintf(cmd, "ip link set u-%s-1 netns %d", u->unicorn_id, u->child_pid),cmd);
    check_result(system(cmd), "system call ip link set ");

    check_result(sprintf(cmd, "brctl addif br0 u-%s-0", u->unicorn_id),cmd);
    check_result(system(cmd), "system call addif br0 ");

    check_result(sprintf(cmd, "ifconfig u-%s-0 up", u->unicorn_id),cmd);
    check_result(system(cmd), "system call inconfig up ");


    return 0;

}

子进程

子进程所做的事情要简单一些,主要有:

  • 使用pivot_root或者choot切换到预先准备好的rootfs
  • mount其他需要的文件到rootfs,例如volums, /etc/hostname, /etc/hosts, /etc/reslov.conf,伪文件系统/sys,/proc, /dev/pts, /dev/shm等。
  • 配置网络,包括hostname以及interfaces(虚拟网卡对的另一端)。
  • 调用exec系统调用运行对应的入口程序如bash,start.sh等。(对应docker中的CMD或者ENTRYPOINT)
char* const child_args[] = {
    "/bin/bash",
    NULL
};

int child_main(void* arg)
{
    net_t * n = (net_t *)arg;
    //printf("in child main\n");
    char c;
    close(pipes[1]);   
    printf(" child pid from cloned child process:: [%5d] unicorn_id:: [%s] \n", getpid(), n->unicorn_id);

    pivot_move(mount_base, n->unicorn_id);

    //wait for the net peer to be setup
    read(pipes[0],&c,1);
    sethostname(n->hostname, 20);
    net_conf(n);

    check_result(execv(child_args[0], child_args), "execv");
    printf("return from chinld_main \n");
    return 1;
}

//这是子进程内最重要的一步,使用pivot_root切换rootfs目录,然后mount其他需要的目录,
//例如volums, /etc/hostname, /etc/hosts, /etc/reslov.conf,伪文件系统/sys,/proc, /dev/pts, /dev/shm等。
int pivot_move(char * mount_base, char * unicorn_id){
    char rootfs_path[100];
    check_result(sprintf(rootfs_path,"%s/%s/rootfs",mount_base,unicorn_id), rootfs_path);
    check_mkdir(mkdir(rootfs_path, 0755), "mkdir root");


    char pivot_old_dir[120];
    check_result(sprintf(pivot_old_dir,"%s/%s",rootfs_path,".pivot_old"),pivot_old_dir);
    check_mkdir(mkdir(pivot_old_dir,0755),"mkdir .pivot_old");
    check_result(chdir(rootfs_path),"chdir to rootfs_path");
    //printf("rootfs::%s pivot_old::%s \n", rootfs_path, pivot_old_dir);
    check_result(pivot_root(".", ".pivot_old"),"pivot_root");  
    check_result(chdir("/"),"chdir to /");
    check_result(umount2("/.pivot_old",MNT_DETACH), "umount /.pivot_old");
    check_result(rmdir(".pivot_old"),"remove .pivot_old");

    // mkdir -p /dev/pts
    //mount -t devpts -o newinstance,ptmxmode=0666 devpts /dev/pts
    //ln -sf pts/ptmx /dev/ptmx 
    check_mkdir(mkdir("/dev",0755),"mkdir /dev");
    check_mkdir(mkdir("/dev/pts",0755),"mkdir /dev/pts");
    check_result(mount("devpts","/dev/pts","devpts",0, "newinstance,ptmxmode=0666"),"mount /dev/pts");
    check_result(symlink("pts/ptmx","/dev/ptmx"),"ln -sf pts/ptmx /dev/ptmx ");

    //mkdir -p /dev/shm
    //mount -t tmpfs tmpfs /dev/shm
    //system("mkdir -p /dev/shm");
    check_mkdir(mkdir("/dev/shm",0755),"mkdir /dev/shm");
    check_result(mount("tmpfs","/dev/shm","tmpfs",0, NULL),"mount /dev/shm");

    //for top tool: mount -t proc proc /proc
    check_mkdir(mkdir("/proc",0755),"mkdir /proc");
    check_result(mount("proc","/proc","proc",0, NULL),"mount /proc");

    //for device data: mount -t proc proc /proc
    check_mkdir(mkdir("/sys",0755),"mkdir /sys");
    check_result(mount("sysfs","/sys","sysfs",0, NULL),"mount /sys");

    //export PS1="root@@\h:\w\$"

    return 0;
}

//子进程网络命名空间需要配置网卡
int net_conf(net_t * n){
    //ifconfig u-${unicorn-id}-1 $ip netmask $netmask mtu $mtu
    char cmd[100];
    check_result(sprintf(cmd, "ifconfig u-%s-1 %s netmask %s mtu %d", n->unicorn_id, n->ip, n->netmask, n->mtu),cmd);
    check_result(system(cmd), "system call ifconfig  ");

    return 0;
}

主要技术

总结一下,linux container主要用到了以下主要技术:

  • control group: 用来做资源限制,比如限制最大内存为4G,cpu使用为25%等(注意这里cpu的限制和完全公平的调度算法有关,换句话说,这里的cpu限制是控制整个contaier里面的进程们所占的所有cpu使用时间比率)
  • namespace: 用来做资源隔离,让container所在的所有进程只能感受到自己使用的资源,例如pid, network, uts, ipc, fs等等。
  • union fs: union fs的作用是将多层次的文件系统整合成一个单一的文件系统。
  • pivot_root或者chroot方法: 我们知道linux的rootfs是可以灵活挂载的,对于linux container而言,让其在自己独立的一个rootfs内,用到的技术正是pivot_root或者chroot。
]]>
docker背后的存储之aufs(一): Overview 2015-08-10T11:33:25+00:00 froghui http://froghui.github.io/docker-aufs 概述

docker将网络,存储,监控以及cgroup/namespace和docker本身抽象隔离开来,主要的driver有这么几种:

  • execdriver — lxc libcontianer 实现隔离(cgroup + namespace)
    基于cgroup, namespace将image运行起来,隔离多个进程,这方面包括uts, process, memory,network等等。docker支持lib container和lxc两种方式实现。
  • graphdriver — 实现image和container的存储 (rootfs) graph和aufs(device mapper)目录
  • networkdriver — docker vm的网络设置 (bridge+iptables) (bridge + mac switch) (ipvlan)

Docker image是一个抽象的概念,表示的是一个容器运行的rootfs环境。非常类似Cloud foundry里面的buildpack概念。
Dockerfile是docker的一种语法,用来build生成镜像。
Docker registry是image的管理。
docker image最为主要的概念是分层管理,Copy on Write,可以做到最小化image,而且可复用。

docker image分成两部分来管理

  • metadata 存储在/var/lib/docker/graph/{imageId},其中每个imageID代表一个layer, 是每次dockerfile里面动作的一个结果,例如ADD, RUN等。该文件目录下主要有两个文件, json存储了该image的metadata,例如创建时间, 对应的parent image, image产生的命令等等。另一个文件layersize是一个描述了的diff包的大小。
  • image tar diff。存储在/var/lib/docker/aufs目录。

aufs的目录主要分成三种 diff,layers和mnt。下面一一解释

/var/lib/docker/aufs/diff/{imageId}:  
/var/lib/docker/aufs/diff/{containerId}:  
/var/lib/docker/aufs/diff/{containerId-init}:  

存储每次该imageId指定的image相比其parent image的文件delta(diff)。例如ADD的一个文件, COPY的一个文件, RUN会改变的目录及文件等等。 另外也会存储每次产生container时需要的两层文件,{containerId}和{containerId-init}。这两层文件的作用后面会解释。

/var/lib/docker/aufs/layers/{imageId}:  
/var/lib/docker/aufs/layers/{containerId}:  
/var/lib/docker/aufs/layers/{containerId-init}:  

存储了当前image的parent image chain,便于快速构建image tree。当然,也存储了每次产生container时需要的两层文件所对应的文件层次,其实 {containerId}的parent就是{containerId-init}。我们可以先看一个image的例子。

root@vagrant-ubuntu-trusty-64:/var/lib/docker/aufs/layers# cat 60c018693d4c850166810f7f8104fa1b310bbad355eb1d43cbca02a31a0db9a8
18392d3874fbb2fcbfa861a81d5b6ea32c81c3293e9c0cda62143b599f186c4a
05a707a5c88538f5695ce679e1b61331b43a24faf3a755b12ff7873fd4e62003
8a09e63f6f5cca6b2b88f2a50f6a5c5399002f8394bf6dd44243ebec7277e632
...


/var/lib/docker/aufs/mnt/{containerId}:
/var/lib/docker/aufs/mnt/{containerId-init}:

存储了aufs将layed image经过union以后形成的最终可以读写的rootfs。

下面结合数据结构研究一下几个docker命令的实现

docker history [IMAGE]

根据image的repository:tag得到其image id; 一种是可以根据image id找到其/var/lib/docker/graph/{imageId}目录,找到其json数据,从而知道其parent ID; 循环 parentID until last 另一种是通过/var/lib/docker/aufs/layers/{imageId}找到其image tree.

这里tagStore 存储在/var/lib/docker/repositories-{graphDriver}

Repository map[string][]string
"ubuntu":
{"14.04":"d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6",
"14.04.2":"d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6",
"latest":"d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6",
"trusty":"d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6",
"trusty-20150630":"d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6"}

docker create and run

首先,创建containerId-init的layer, diff。并mount到/mnt/{containerId}-init

/var/lib/docker/aufs/diff/{containerId}-init  (RW)
/var/lib/docker/aufs/diff/{baseImageId}   (RO)
...
————>
/var/lib/docker/aufs/mnt/{containerId}-init

等价于下面的mount命令

mount -t aufs
br:/var/lib/docker/aufs/diff/{containerId}-init=rw
:/var/lib/docker/aufs/diff/{baseImageId}=ro+wh,xino=/dev/shm/aufs.xino
:/var/lib/docker/aufs/diff/{parentofBaseImageId}=ro+wh ...
/var/lib/docker/aufs/mnt/{conatainer}-init

其次准备container top-level 的一些文件,即对/var/lib/docker/aufs/mnt/{conatainer}-init写入一些top-level文件,也就是写到/var/lib/docker/aufs/diff/{containerId}-init中, 这是为了起到保护作用。这些文件主要是/dev/shm, /dev/console, /etc/hostname, /etc/hosts, /etc/resolv.conf等网络相关的配置等。这里要注意的是这里写到/diff/{containerId}-init中的都是空文件,而实际在container里面使用的这些文件或者目录,都会在子进程中重新mount到rootfs中。实际上,子进程不仅mount这些/dev/shm, /dev/console, /etc/hostname, /etc/hosts, /etc/resolv.conf, /proc, /sys, 而且会mount挂载的volumn。

root@vagrant-ubuntu-trusty-64:/var/lib/docker/aufs/diff/7e825e149f15854727c1a77f88d92726458197c11b52d6375664db1e4065e0ae-init/dev# ls -al
total 12
drwxr-xr-x 3 root root 4096 Jul 28 08:06 .
drwxr-xr-x 6 root root 4096 Jul 28 08:06 ..
-rwxr-xr-x 1 root root    0 Jul 28 08:06 console
drwxr-xr-x 2 root root 4096 Jul 28 08:06 shm
root@vagrant-ubuntu-trusty-64:/var/lib/docker/aufs/diff/7767c05d8bbf2214ad94ccb887b723342d7b3f3e63f811fa53f5320b83fcf2b8-init/etc# ll
total 8
drwxr-xr-x 2 root root 4096 Jul 28 08:06 ./
drwxr-xr-x 6 root root 4096 Jul 28 08:06 ../
-rwxr-xr-x 1 root root    0 Jul 28 08:06 hostname*
-rwxr-xr-x 1 root root    0 Jul 28 08:06 hosts*
lrwxrwxrwx 1 root root   12 Jul 28 08:06 mtab -> /proc/mounts
-rwxr-xr-x 1 root root    0 Jul 28 08:06 resolv.conf*

最后创建containerId的layer, diff,并mount到/mnt/{containerId}

/var/lib/docker/aufs/diff/{containerId}   (RW)
/var/lib/docker/aufs/diff/{containerId}-init  (RO)
/var/lib/docker/aufs/diff/{baseImageId}   (RO) ...
————>
/var/lib/docker/aufs/mnt/{containerId}

例如, 一个container

7767c05d8bbf docker.dp/tomcat/ngx4a-2:latest "/bin/sh -c /bin/sta “
root@vagrant-ubuntu-trusty-64:/var/lib/docker/aufs/layers# cat 7767c05d8bbf2214ad94ccb887b723342d7b3f3e63f811fa53f5320b83fcf2b8-init (init image)
60c018693d4c850166810f7f8104fa1b310bbad355eb1d43cbca02a31a0db9a8 (base image)
18392d3874fbb2fcbfa861a81d5b6ea32c81c3293e9c0cda62143b599f186c4a

root@vagrant-ubuntu-trusty-64:/var/lib/docker/aufs/layers# cat 7767c05d8bbf2214ad94ccb887b723342d7b3f3e63f811fa53f5320b83fcf2b8
7767c05d8bbf2214ad94ccb887b723342d7b3f3e63f811fa53f5320b83fcf2b8-init
60c018693d4c850166810f7f8104fa1b310bbad355eb1d43cbca02a31a0db9a8

docker build

首先分析出dockerfile每步需要做的动作,对每一步都会创建临时的一个container 该container的rootfs是来自两部分
1)/var/lib/docker/aufs/diff/{containerId}-init(此为当前container运行的init image,ro: read only), 注意/var/lib/docker/aufs/diff/{containerId}-init的parent image即为当前指定的base image(container从哪个image创建出来)。
2)/var/lib/docker/aufs/diff/{containerId}(rw: readwrite), 而mount的target为/var/lib/docker/aufs/mnt/{containerId},这样对于该container而言,对roots的写全部落入了/var/lib/docker/aufs/diff/{containerId}中。这样就可以得到当前步骤相对于parent image的diff。

/var/lib/docker/aufs/diff/{containerId} (RW)
/var/lib/docker/aufs/diff/{containerId}-init}  (RO)
/var/lib/docker/aufs/diff/{baseImageId}   (RO)
...
————>
/var/lib/docker/aufs/mnt/{containerId}

在该container RUN完之后,立即创建一个新的image对象保存metadata,并写到/var/lib/docker/graph/{imageId}中, 接下来调用driver.Diff(container.ID, initID)获取该container相对于init image的diff数据(tar包),并接着调用 driver.ApplyDiff(imageId, parentId, layerData archive.ArchiveReader)将数据写到 文件系统/var/lib/docker/aufs/diff/{imageId}中

aufs的作用 aufs 可以将每次对layed image所做的更改commit 成一个image(tar),然后可以merge在一起。像这样变成一个rootfs

/var/lib/docker/aufs/diff/{containerId} (RW)
/var/lib/docker/aufs/diff/{containerId}-init}  (RO)
/var/lib/docker/aufs/diff/{baseImageId}   (RO)
...
————>
/var/lib/docker/aufs/mnt/{containerId}
]]>