cgroup 内存管理之 tmpfs

本文内核代码版本:https://elixir.bootlin.com/linux/v4.14.281/source/mm/shmem.c

1. tmpfs 内存简介

tmpfs 文件系统是 pod 中常见的一种“存储”介质,也叫 ram disk,都是一个东西

tmpfs 的特殊的地方在于:

  1. 首先它是个文件系统
  2. 但是它的文件数据是完全存放在内存里面的,不在磁盘上

所以要讲 tmpfs 的话,就得把这两部分都讲清楚,一个是文件系统的实现,一个是底层“持久化”层内存的管理

通常应用程序之间会通过 tmpfs 文件系统来实现高效的数据共享

/dev/shm 就是一个最典型的 tmpfs 文件系统,是操作系统为了解决大多数程序数据共享而默认挂在的一个 tmpfs

2. tmpfs 文件系统的实现

我们知道 file 是linux内核最重要的设计,一切皆文件

除了普通的文件,平时我们接触到的,unix管道、socket、proc、cgroup 等等,都是基于文件的实现

为了实现灵活可扩展的文件系统架构,Linux设计了 virtual file system 抽象层,简称 vfs,对用户台程序屏蔽了所有具体的底层文件系统的实现细节,提供统一的文件系统接口

2.1. virtual file system 接口定义

https://www.kernel.org/doc/html/latest/filesystems/vfs.html

vfs 属于一个专题,我们这里不讲那么复杂,有时间可以专门展开讲

vfs 定义了文件系统实现最关键的2个接口:

  1. 一个是 struct file_operations:文件读写的接口
  2. 一个是 struct inode_operations:inode操作接口

inode_opertions 定义如下:

struct inode_operations {
    int (*create)(struct user_namespace*, struct inode*, struct dentry*, umode_t, bool);
    int (*symlink)(struct user_namespace*, struct inode*, struct dentry*, const char*);
    int (*mkdir)(struct user_namespace*, struct inode*, struct dentry*, umode_t);
    int (*rmdir)(struct inode*, struct dentry*); /* 省略一万字 */
};

是不是很熟悉?

file_operations 的定义如下:

struct file_operations {
    int (*open)(struct inode*, struct file*);
    loff_t (*llseek)(struct file*, loff_t, int);
    ssize_t (*read)(struct file*, char __user*, size_t, loff_t*);
    ssize_t (*write)(struct file*, const char __user*, size_t, loff_t*); /* 省略一万字 */
};

是不是也很熟悉?

内核 tmpfs 文件系统的源码:mm/shmem.c

tmpfs 其实并没有实现 vfs 中的所有接口,主要原因是因为由于绝大部分的 file system 底层实现其实都是一样的,没区别,所以内核为了简化文件系统的开发,把这些通用的实现都抽象出来,变成 generic_file_read/write 之类的通用实现

static const struct file_operations shmem_file_operations = { /* 普通文件的读、写、seek、fsync */ .mmap = shmem_mmap, .get_unmapped_area = shmem_get_unmapped_area, #ifdef CONFIG_TMPFS .llseek = shmem_file_llseek, .read_iter = shmem_file_read_iter, .write_iter = generic_file_write_iter, .fsync = noop_fsync, .splice_read = generic_file_splice_read, .splice_write = iter_file_splice_write, .fallocate = shmem_fallocate, #endif };
static const struct inode_operations shmem_inode_operations = { /* 这个是针对普通文件的 inode 操作 */ .getattr = shmem_getattr, .setattr = shmem_setattr, };
static const struct inode_operations shmem_dir_inode_operations = { /* 这个是针对目录的 inode 操作 */ #ifdef CONFIG_TMPFS .create = shmem_create, .lookup = simple_lookup, .link = shmem_link, .unlink = shmem_unlink, .symlink = shmem_symlink, .mkdir = shmem_mkdir, .rmdir = shmem_rmdir, .mknod = shmem_mknod, .rename = shmem_rename2, .tmpfile = shmem_tmpfile, #endif };

2.2. 文件创建过程

创建一个 tmpfs 数据文件

内核代码:shmem_create -> shmem_mknod

static int shmem_create(struct inode* dir, struct dentry* dentry, umode_t mode, bool excl) {
    return shmem_mknod(dir, dentry, mode | S_IFREG,
                       0); /* S_IFREG 表明是创建的文件,如果是目录,那就是 S_IFDIR */
}

再来看下 shmem_mknode() 函数的实现,非常简单

/* * File creation. Allocate an inode, and we're done.. */ static int shmem_mknod(struct inode* dir,
        struct dentry* dentry, umode_t mode, dev_t dev) {
    struct inode* inode;
    int error =
        -ENOSPC; /* 这里先 new 一个新的 inode,然后挂到 sb 里面管理起来,sb的数据也完全是在内存里面 */
    inode = shmem_get_inode(dir->i_sb, dir, mode, dev, VM_NORESERVE);

    if (inode) {
        error = simple_acl_create(dir, inode);

        if (error) {
            goto out_iput;
        }

        error = security_inode_init_security(inode, dir, &dentry->d_name, shmem_initxattrs, NULL);

        if (error && error != -EOPNOTSUPP) {
            goto out_iput;
        }

        error = 0; /* 目录的 size,其实就是目录下所有 inode 空间的总和 * 注意:目录也是一个特殊的文件,文件的内容存储的是子目录的索引 */
        dir->i_size += BOGO_DIRENT_SIZE;
        dir->i_ctime = dir->i_mtime = current_time(dir); /* 把新建的 inode 和 dentry 关联起来 */
        d_instantiate(dentry, inode);
        dget(dentry); /* Extra count - pin the dentry in core */
    }

    return error;
out_iput:
    iput(inode);
    return error;
}

2.3. 文件读写过程

用户台的文件读写,默认情况下都是 buffer 模式,也就是写 page cache,再由内核把 dirty page 刷到磁盘上

只有在真正的把文件落盘时,我们才需要真正的理解 file system 的底层结构,才需要知道把文件数据具体写到哪个磁盘 block 上

由于 tmpfs 数据不是存在物理磁盘上的,完全在内存,因此 tmpfs 的文件读写,完全是一个读写 page cache 的过程

内核为 tmpfs 构造了一个特殊的 address_space,写 page cache 就通过这个 address_space 来完成

1)读过程

内核调用栈:

shmem_file_read_iter

-> shmem_getpage -> shmem_getpage_gfp -> find_lock_entry -> find_get_entry:从 address_space 的 cache 里获取 page 缓存页

-> copy_page_to_iter:拷贝到用户空间

static ssize_t shmem_file_read_iter(struct kiocb* iocb, struct iov_iter* to) {
    struct file* file = iocb->ki_filp;
    struct inode* inode = file_inode(file);
    struct address_space* mapping = inode->i_mapping; /* ... */ index = *ppos >> PAGE_SHIFT;
    offset = *ppos & ~PAGE_MASK;

    for (;;) {
        /* ... */ error = shmem_getpage(inode, index, &page, sgp);

        if (error) {
            if (error == -EINVAL) {
                error = 0;
            }

            break;
        } /* ... */ /* * Ok, we have the page, and it's up-to-date, so * now we can copy it to user space... */
        ret = copy_page_to_iter(page, offset, nr, to); /* ... */
    } /* ... */
}

2)写过程

xxx

3. tmpfs 内存的内核态管理

3.1. tmpfs 内存限制

tmpfs 内存受2个地方限制

一个是 mount 挂载文件系统的时候,会指定 tmpfs 的大小,如果超过这个限制,会写失败

第二个限制是,由于 tmpfs 的底层就是内存空间,不是磁盘,如果一个容器设置了 memory.max,硬限,那也会触发这个限制。当然,这个限制是间接的

3.2. tmpfs 内存统计

由于 tmpfs 数据并不直接落物理磁盘,所有数据都是在内存中以 page cache 的形式存在的,因此,容器中 tmpfs 的内存占用会统计到2个地方:

  1. page cache
  2. shmem

如下:如果我写一个100m的文件到 tmpfs 下面,cgroup 会把这个空间统计到 cache 和 shmem 里面

cat / sys / fs / cgroup / memory / test / memory.stat cache 104890368 rss 12288 rss_huge 0 shmem
104755200 mapped_file 0 dirty 0 writeback 0 pgpgin 25905 pgpgout 284

从 mm/memcontrol.c 代码中,我们知道 shmem 这项内存是保存在 stat[NR_SHMEM] 结构中的

static int memory_stat_show(struct seq_file* m, void* v) {
    /* ... */ seq_printf(m, "anon %llu\n", (u64)stat[MEMCG_RSS] * PAGE_SIZE);
    seq_printf(m, "file %llu\n", (u64)stat[MEMCG_CACHE] * PAGE_SIZE);
    seq_printf(m, "shmem %llu\n", (u64)stat[NR_SHMEM] *
               PAGE_SIZE); // 这里 seq_printf(m, "file_mapped %llu\n", (u64)stat[NR_FILE_MAPPED] * PAGE_SIZE); seq_printf(m, "file_dirty %llu\n", (u64)stat[NR_FILE_DIRTY] * PAGE_SIZE); seq_printf(m, "file_writeback %llu\n", (u64)stat[NR_WRITEBACK] * PAGE_SIZE); /* ... */ }

mm/shmem.c 文件读写的时候,什么时候会把文件大小计数在这里呢?

shmem_add_to_page_cache 这个函数

/* * Like add_to_page_cache_locked, but error if expected item has gone. */ static int
shmem_add_to_page_cache(struct page* page, struct address_space* mapping, pgoff_t index,
                        void* expected) {

    int error, nr = hpage_nr_pages(page); /* ... */ if (!error) {
        mapping->nrpages += nr;

        if (PageTransHuge(page)) {
            __inc_node_page_state(page, NR_SHMEM_THPS);
        }

        __mod_node_page_state(page_pgdat(page), NR_FILE_PAGES, nr);
        __mod_node_page_state(page_pgdat(page), NR_SHMEM,
                              nr); /* 这里 */ spin_unlock_irq(&mapping->tree_lock);
    } else {
        page->mapping = NULL;
        spin_unlock_irq(&mapping->tree_lock);
        page_ref_sub(page, nr);
    }

    return error;
}

同样,当 tmpfs 文件被删除时,shmem_delete_from_page_cache 函数会把这个计数器减掉被删除的文件大小

3.3. tmpfs 内存的分配和回收(page cache回收)

除非开了 swap 的情况下,swap 一般生产环境线上服务器是不打开的,因此这里不讨论

tmpfs 的内存虽然是 page cache,但是永远不会被回收,这是怎么做到的?

1)先说说分配过程

tmpfs 文件的 page cache 在分配的时候,会打上1个特殊的标记:__SetPageSwapBacked(),对应 PG_SwapBacked

__SetPageLocked() 这个函数不用管它

static struct page* shmem_alloc_and_acct_page(gfp_t gfp, struct inode* inode, pgoff_t index,
        bool huge) {
    struct shmem_inode_info* info = SHMEM_I(inode);
    struct page* page;
    int nr;
    int err = -ENOSPC;

    if (!IS_ENABLED(CONFIG_TRANSPARENT_HUGE_PAGECACHE)) {
        huge = false;
    }

    nr = huge ? HPAGE_PMD_NR : 1;

    if (!shmem_inode_acct_block(inode, nr)) {
        goto failed;
    }

    if (huge) {
        page = shmem_alloc_hugepage(gfp, info, index);
    } else {
        page = shmem_alloc_page(gfp, info, index);
    }

    if (page) {
        /* 这里 */ __SetPageLocked(page);
        __SetPageSwapBacked(page);
        return page;
    }

    err = -ENOMEM;
    shmem_inode_unacct_blocks(inode, nr);
failed:
    return ERR_PTR(err);
}

除此之外,一旦这个 page 被写入任何数据,这个 page 就会被 vfs 标记为 PG_dirty

有了 PG_dirty 和 PG_SwapBacked 这2个标记之后,如果没有开启 swap 分区,tmpfs 的文件是没法回收的,必须常驻内存(可以理解)

(特别注意,也就是说 tmpfs 的 page 在内核里面,永远都是 dirty 状态

2)再看看 page cache 回收的过程

内核代码 mm/vmscan.c

函数 shrink_page_list

pageout() 函数写交换分区,这个函数有4个返回值:

  1. PAGE_KEEP:写page失败
  2. PAGE_ACTIVATE:表示page需要迁移回到活跃LRU链表中
  3. PAGE_SUCCESS:表示 page 已经成功写入存储设备
  4. PAGE_CLEAN:表示 page 已经是干净的,可以释放

page 回收的过程是:

shrink_page_list -> if (PageDirty(page)) { … } -> pageout() -> shmem_writepage() -> get_swap_page(),由于机器 swap 分区关闭,所以 get_swap_page 失败,返回 PAGE_ACTIVATE

4. tmpfs 内存的 Pod 共享

xx

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注