注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

共享内存实现及应用  

2013-05-26 19:22:03|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、共享内存的创建方法
在用户态情况下,一般有两种接口方法来创建共享内存,一种是posix标准中描述的shm_open功能,另一种是早期SYSV IPC机制中的shmget创建。但是两者的实现是有区别的:
对于shm_open来说,它依赖于用户态文件系统tmpfs文件系统被挂在在系统中,这个系统默认是挂载在文件系统的/dev/tmpfs文件系统中,而该文件系统的内核实现要依赖于内核编译的时候使能了tmpfs文件系统的编译。
反过来看IPC机制中的shmget接口,它的实现并不依赖于内核构建时是否使能了tmpfs文件系统,更不需要该文件系统在用户态的挂载,所以使用shmXX函数簇的实现对底层的依赖更少(只要内核编译的时候使能了IPC机制即可)。
1、shmget的内核实现
newseg--->>>shmem_file_setup
    file = alloc_file(&path, FMODE_WRITE | FMODE_READ,
          &shmem_file_operations);
……
static const struct file_operations shmem_file_operations = {
    .mmap        = shmem_mmap,
#ifdef CONFIG_TMPFS
    .llseek        = generic_file_llseek,
    .read        = do_sync_read,
    .write        = do_sync_write,
    .aio_read    = shmem_file_aio_read,
    .aio_write    = generic_file_aio_write,
    .fsync        = noop_fsync,
    .splice_read    = generic_file_splice_read,
    .splice_write    = generic_file_splice_write,
#endif
};
可以看到的是,其中的mmap接口的编译不依赖于任何的文件系统的使能,即使没有使能TMPFS文件系统,该功能依然处于使能状态。只是在shmem之上增加了一层shm文件,它的主要作用就是为了区分共享匿名映射中的共享内存自动删除问题。由于对于匿名共享内存来说,它的底层实现也是依赖于shmem文件系统。和shm不同,这些匿名内存在所有进程的引用计数都为零之后会被系统自动回收,而对于shm内存来说,即使没有进程在引用该内存,只要没有被主动执行过ipcrm动作,那么这个共享内存将会一直存在。
中间插入的一个shm层(例如shm_file_operations,它和shmem名字非常类似)的目的就是为了处理这种文件的特殊生存期问题:如果该共享内存已经被删除,当文件的引用计数减少为零时,资源从系统中真正释放。
2、shm_open的实现
该接口不直接对应系统调用,而是C库实现的一组用户它接口
int
shm_open (const char *name, int oflag, mode_t mode)
{
  size_t namelen;
  char *fname;
  int fd;

  /* Determine where the shmfs is mounted.  */
  __libc_once (once, where_is_shmfs);
……
  /* And get the file descriptor.
     XXX Maybe we should test each descriptor whether it really is for a
     file on the shmfs.  If this is what should be done the whole function
     should be revamped since we can determine whether shmfs is available
     while trying to open the file, all in one turn.  */
  fd = open (fname, oflag | O_NOFOLLOW, mode);

where_is_shmfs (void)
  /* OK, do it the hard way.  Look through the /proc/mounts file and if
     this does not exist through /etc/fstab to find the mount point.  */
  fp = __setmntent ("/proc/mounts", "r");
  /* Now read the entries.  */
  while ((mp = __getmntent_r (fp, &resmem, buf, sizeof buf)) != NULL)
    /* The original name is "shm" but this got changed in early Linux
       2.4.x to "tmpfs".  */
    if (strcmp (mp->mnt_type, "tmpfs") == 0
#ifndef __ASSUME_TMPFS_NAME
    || strcmp (mp->mnt_type, "shm") == 0
#endif
    )
大致的思路就是从/proc/mounts中扫描tmpfs文件,然后找到tmpfs文件系统挂在位置,然后在该文件夹下创建制定的文件。所以该接口的实现要依赖于内核编译了TMPFS文件系统并且用户态已经将该文件系统挂载到系统中。
二、shm的特殊处理
1、创建及删除
shm创建之后,它的语义应该是该资源一直存在,直到被sys_shmctl的IPC_RMID参数删除掉。而对于tmpfs来说,当文件引用计数达到零时就会被自动删除,为了避免在何种情况,shm在shmem之上增加了又一层封装,在用户态shmget创建的文件被打开和关闭的时候执行自己的判断,判断此时该资源是不是已经被删除并且文件的引用计数已经降低为零,如果满足这个条件,则将shmem资源删除。
这个特殊的功能在shm_file_operation的mmap中安装内存区vma的vm_operations,而mmap在shmat时由内核调用:
do_shmat()
file->f_op = &shm_file_operations;
    file->private_data = sfd;
^
    user_addr = do_mmap (file, addr, size, prot, flags, 0);

static int shm_mmap(struct file * file, struct vm_area_struct * vma)
{
    struct shm_file_data *sfd = shm_file_data(file);
    int ret;

    ret = sfd->file->f_op->mmap(sfd->file, vma); 直接调用shmem的mmap功能先。
    if (ret != 0)
        return ret;
    sfd->vm_ops = vma->vm_ops;
    vma->vm_ops = &shm_vm_ops;
    shm_open(vma);

    return ret;
}
static struct vm_operations_struct shm_vm_ops = {
    .open    = shm_open,    /* callback for a new vm-area open */
    .close    = shm_close,    /* callback for when the vm-area is released */
    .nopage    = shm_nopage,
#if defined(CONFIG_NUMA)
    .set_policy = shm_set_policy,
    .get_policy = shm_get_policy,
#endif
};

/*
 * remove the attach descriptor vma.
 * free memory for segment if it is marked destroyed.
 * The descriptor has already been removed from the current->mm->mmap list
 * and will later be kfree()d.
 */
static void shm_close(struct vm_area_struct *vma)
{
    struct file * file = vma->vm_file;
    struct shm_file_data *sfd = shm_file_data(file);
    struct shmid_kernel *shp;
    struct ipc_namespace *ns = sfd->ns;

    mutex_lock(&shm_ids(ns).mutex);
    /* remove from the list of attaches of the shm segment */
    shp = shm_lock(ns, sfd->id);
    BUG_ON(!shp);
    shp->shm_lprid = current->tgid;
    shp->shm_dtim = get_seconds();
    shp->shm_nattch--;
    if(shp->shm_nattch == 0 &&
       shp->shm_perm.mode & SHM_DEST)
        shm_destroy(ns, shp);
    else
        shm_unlock(shp);
    mutex_unlock(&shm_ids(ns).mutex);
}
在关闭一个内存区时,如果该共享内存的引用计数已经降低为0并且已经被删除,则将共享内存删除,这也就是我们常说的延迟删除实现,在linux中非常常见。
2、延迟删除的执行实际
sys_exit--->>do_exit-->>exit_mm--->>>mmput--->>>exit_mmap--->>remove_vma
static struct vm_area_struct *remove_vma(struct vm_area_struct *vma)
{
    struct vm_area_struct *next = vma->vm_next;

    might_sleep();
    if (vma->vm_ops && vma->vm_ops->close)
        vma->vm_ops->close(vma);
    if (vma->vm_file)
        fput(vma->vm_file);
    mpol_free(vma_policy(vma));
    kmem_cache_free(vm_area_cachep, vma);
    return next;
}
三、关于文件系统中dentry及inode
1、目录项和inode的引用计数
在文件系统中,一个通常我们所说的dentry并不包含持久文件系统中的引用计数,我们看一下ext2文件系统中的dentry结构
struct ext2_dir_entry_2 {
    __le32    inode;            /* Inode number */
    __le16    rec_len;        /* Directory entry length */
    __u8    name_len;        /* Name length */
    __u8    file_type;
    char    name[EXT2_NAME_LEN];    /* File name */
};
可以看到其中没有该目录项的引用计数字段,反观ext2的inode节点信息
/*
 * Structure of an inode on the disk
 */
struct ext2_inode {
    __le16    i_mode;        /* File mode */
    __le16    i_uid;        /* Low 16 bits of Owner Uid */
    __le32    i_size;        /* Size in bytes */
    __le32    i_atime;    /* Access time */
    __le32    i_ctime;    /* Creation time */
    __le32    i_mtime;    /* Modification time */
    __le32    i_dtime;    /* Deletion Time */
    __le16    i_gid;        /* Low 16 bits of Group Id */
    __le16    i_links_count;    /* Links count */
这里就说明一个文件,目录项的删除是马上体现在持久存储介质上的,但是如果内存中的dentry还在被使用,则它会一直存在,并且导致inode无法被删除,因为inode的删除是在释放dentry的时候完成。
再具体的说,例如一个文件正在被自行,此时我们可以删除该文件对应的目录项,之后通过ls看不到该可执行文件的存在,但是该文件占用的inode节点在硬盘上并没有释放,之后当进程退出,所有对资源的引用全部释放之后才会在硬盘上体现。
引申出来的其它问题还有:
当一个目录项被删除之后,通过一个bit位就可以表示该文件已经被删除,这个标志位就是DCACHE_UNHASHED,如果内存中的dentry设置有该标志,那么说明它已经被删除,只是还没有在硬盘上释放inode信息。
符号链接本身不增加源文件的引用计数,也就是说,当源文件被删除之后,符号链接诶可能依然存在,但是无法打开文件内容,因为链接源已经被删除。
2、dentry的删除
sys_unlink--->>do_unlinkat--->>vfs_unlink--->>error = dir->i_op->unlink(dir, dentry)
也就是直接到到了具体文件系统的删除,此时磁盘上该目录项会被删除。在vfs_unlink之后会继续执行d_delete,该函数执行逻辑包括
    if (atomic_read(&dentry->d_count) == 1) {如果引用计数减少为1,可以直接删除目录项对应的inode,否则只是标志为UNHASHED
        dentry_iput(dentry);
        fsnotify_nameremove(dentry, isdir);

        /* remove this and other inotify debug checks after 2.6.18 */
        dentry->d_flags &= ~DCACHE_INOTIFY_PARENT_WATCHED;
        return;
    }

    if (!d_unhashed(dentry))
        __d_drop(dentry);

static inline void __d_drop(struct dentry *dentry)
{
    if (!(dentry->d_flags & DCACHE_UNHASHED)) {
        dentry->d_flags |= DCACHE_UNHASHED;
        hlist_del_rcu(&dentry->d_hash);
    }
}
再次读取文件夹下目录项时,由于文件系统中的dentry已经被删除,所以不会看到延迟删除的文件。
3、inode的延迟删除
以sys_close为例
filp_close--->>>fput--->>__fput--->>dput(dentry);
    /* Unreachable? Get rid of it */
     if (d_unhashed(dentry))
        goto kill_it;
……
kill_it: {
        struct dentry *parent;

        /* If dentry was on d_lru list
         * delete it from there
         */
          if (!list_empty(&dentry->d_lru)) {
              list_del(&dentry->d_lru);
              dentry_stat.nr_unused--;
          }
          list_del(&dentry->d_u.d_child);
        dentry_stat.nr_dentry--;    /* For d_free, below */
        /*drops the locks, at that point nobody can reach this dentry */
        dentry_iput(dentry);
        parent = dentry->d_parent;
        d_free(dentry);
        if (dentry == parent)
            return;
        dentry = parent;
        goto repeat;
    }
-->>>dentry_iput--->>iput(inode)-->>if (atomic_dec_and_lock(&inode->i_count, &inode_lock))  put_final(inode);--->>>generic_drop_inode
void generic_drop_inode(struct inode *inode)
{
    if (!inode->i_nlink)
        generic_delete_inode(inode);
    else
        generic_forget_inode(inode);
}
4、共享匿名映射
这个是在进程间共享的匿名共享映射,内核中同样使用shmem作为底层实现,
do_mmap_pgoff--->>shmem_zero_setup--->>shmem_file_setup("dev/zero", size, vma->vm_flags).
这里我们比较感兴趣的就是这个文件的自动释放问题,当所有的进程都退出时,该资源需要被自动释放,
d_alloc
    atomic_set(&dentry->d_count, 1);
    dentry->d_flags = DCACHE_UNHASHED;
    spin_lock_init(&dentry->d_lock);
    dentry->d_inode = NULL;
创建一个新的目录项时,资源的引用计数为1,并且处于删除的UNHASHED状态。同样是在exit_m-->>……-->>remove_vma
void fastcall fput(struct file *file)
{
    if (atomic_dec_and_test(&file->f_count))
        __fput(file);
}
/**
 * atomic_dec_and_test - decrement and test
 * @v: pointer of type atomic_t
 *
 * Atomically decrements @v by 1 and
 * returns true if the result is 0, or false for all other
 * cases.
 */
#define atomic_dec_and_test(v) (atomic_sub_return(1,(v)) == 0)
当shmem文件执行到这里之后,满足引用为零,并且unhashed,所以被删除。
5、新版本内核对shm inode的微调
在2.6.21之后的某一个版本(因为在2.6.21版本中还没有),shm中对于创建的shmem文件的inode做了调整,将inode编号调整为shmem的id,也就是通过ipcs看到的id内容,这样的好处就是可以在进程的/proc/$pid/maps文件中可以看到共享文件的确定编号。由于shm的共享内存的名字一般是"SYSV%08x"格式,所以在系统中还是比较容易看到的。
这里说了这么多就是有时候我们通过ipcs看到系统中一个shm的引用计数非零,而且已经被删除,但是它的attach进程数非零导致无法删除,所以需要遍历系统中所有进程的maps文件,为了减少搜索范围,这个唯一的id是一个非常重要的标识,也是搜索引用者的最强有力的办法。
shm文件名称
static int newseg (struct ipc_namespace *ns, key_t key, int shmflg, size_t size)
sprintf (name, "SYSV%08x", key);
而inode编号为
    /*
     * shmid gets reported as "inode#" in /proc/pid/maps.
     * proc-ps tools use this. Changing this will break them.
     */
    file->f_dentry->d_inode->i_ino = shp->shm_perm.id;
所以ipcs中看到的key和id都可以在maps中看到。
那为什么不直接使用key就可以了吗?
因为对于进程私有映射,key值为零,所以不是系统唯一的。
  评论这张
 
阅读(1376)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017