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

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

sysv共享内存的id和key  

2013-01-27 12:09:30|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、共享内存的标识
在linux的系统工具中,通过ipcs工具可以方便的看到系统中sysv机制下的共享资源,这包含了通常的消息对立,共享内存以及信号灯三种传统意义上的sysv进程通讯机制。在这些输出的显示中,每种资源的开始是有一个key,然后接下来是一个id,这两者在系统的输出中是同时存在的的,但是值的范围却相差比较大,那么它们两者的意义是什么,如何获得呢?
二、共享key的创建
如果这个资源需要在系统中共享,那么标准的做法就是通过ftok函数来返回一个键值,由于这个值需要在系统中唯一,所以这个函数就要能够返回一个系统级唯一的标识。在linux系统中,文件是一个比较唯一的设备,对于同一个文件来说,如果可以死确定它的设备号,再加上设备文件内的inode编号,则可以确定一个系统级的唯一整数,当前的glibc中该函数的实现方法为
key_t
ftok (pathname, proj_id)
     const char *pathname;
     int proj_id;
{
  struct stat64 st;
  key_t key;

  if (__xstat64 (_STAT_VER, pathname, &st) < 0)
    return (key_t) -1;

  key = ((st.st_ino & 0xffff) | ((st.st_dev & 0xff) << 16)
     | ((proj_id & 0xff) << 24));

  return key;
}
最低16bits为文件的inode编号,接着高8bits为设备号,最高8bits为传入的proj_id,这样同一个文件可以用来返回多个不同的key键值。对于一个可能使用到多个共享内存的工程来说,这种方法可以避免创建多个文件来生成键值。
如果我们知道一个文件的路径,根据这个函数的实现,就可以自己确定系统返回的key的具体数值,当然这种方法不能保证在所有的系统中可以使用,只是当前的glibc是这么实现的。好在linux有命令行下的stat命令,这个命令可以展示stat系统调用显示的大部分内容,所以我们可以通过该工具来展示一个文件的inode信息:
[root@Harry cgi-bin]# cat ftok.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>

int main (int argc ,char * argv[])
{
    printf("%x\n",ftok(argv[0],0x12));
}
[root@Harry cgi-bin]# gcc ftok.c -o ftok
[root@Harry cgi-bin]# ./ftok
12008637
[root@Harry cgi-bin]# stat ftok
  File: `ftok'
  Size: 5040          Blocks: 16         IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 427575      Links: 1
Access: (0775/-rwxrwxr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2013-01-27 11:23:57.428812730 +0800
Modify: 2013-01-27 11:23:53.835864267 +0800
Change: 2013-01-27 11:23:53.835864267 +0800
[root@Harry cgi-bin]# printf "%x\n" 427575
68637
可以看到这些内容拼接出了ftok返回的值。但是这里也要注意到一些问题:
新的系统中,device的编号不再是只有8bits,而是扩展到了16bits,从stat的输出中可以看到这个值为0xfd00,对于一个文件系统的inode,它的编号可能会大于16bits,也就是两个系统提供的内容都是可能出现被截断的,而截断之后是可能出现两个不同的文件对应相同的key值,当然这个概率是比较小的。
三、key和id的对应关系
对于shmget来说,不仅可以传给它一个系统级的key值,也可以让系统帮忙创建一个匿名的共享内存,此时的方法就是通过传入键值为IPC_PRIVATE的key值。关于这个特殊值,看一下man手册的说明
NOTES
       IPC_PRIVATE isn’t a flag field but a key_t type.  If this special value
       is used for key, the system call ignores everything but the least  sig-
       nificant  9  bits of shmflg and creates a new shared memory segment (on
       success).
内核中对于该内容的实现
if (key == IPC_PRIVATE) {
        err = newseg(ns, key, shmflg, size);
    } else if ((id = ipc_findkey(&shm_ids(ns), key)) == -1) {
        if (!(shmflg & IPC_CREAT))
            err = -ENOENT;
        else
            err = newseg(ns, key, shmflg, size);
    } else if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) {
        err = -EEXIST;

这里有一个非常重要的实现细节,就是对于IPC_PRIVATE类型的传入值,内核是不管三七二十一就创建一个,而没有进行任何检测文件是否存在;而对于非IPC_PRIVATE类型的输入值,内核首先会检测这个文件是否存在,如果没有存在并且传入了IPC_CREAT,那么会创建一个新的,否则只是根据key来找到这个已经存在的资源,并把id回传给用户。
这里就可以初步解释为什么需要一个key和一个id的原因了,因为对于相同的IPC_PRIVATE输入值来说,内核会返回不同的id;明显地,如果一个进程多次调用IPC_PRIVATE参数传入,那么此时返回的资源id是不同的,所以key值是不能作为资源标识的,而必须使用另一个id来表示该资源。
四、从key到id的转换
static inline int shm_addid(struct ipc_namespace *ns, struct shmid_kernel *shp)
{
    return ipc_addid(&shm_ids(ns), &shp->shm_perm, ns->shm_ctlmni);
}
int ipc_addid(struct ipc_ids* ids, struct kern_ipc_perm* new, int size)
{
    int id;

    size = grow_ary(ids,size);

    /*
     * rcu_dereference()() is not needed here since
     * ipc_ids.mutex is held
     */
    for (id = 0; id < size; id++) {
        if(ids->entries->p[id] == NULL)
            goto found;
    }
    return -1;
found:
    ids->in_use++;
……
    new->seq = ids->seq++;
    if(ids->seq > ids->seq_max)
        ids->seq = 0;

    spin_lock_init(&new->lock);
    new->deleted = 0;
    rcu_read_lock();
    spin_lock(&new->lock);
    ids->entries->p[id] = new;
    return id;这里的id返回的是一个数组的下标,而seq的编号则是单调递增的,直到序列号达到上限并折回
}
可以看到,这里对于一个未使用的id的查找使用了最为朴素的查找方法,就是遍历一个槽位数组,从头开始,如果找到第一个未使用的槽位,就可以使用这个id。这里的id是一个非常紧密的数组,也就是一个接一个的使用,这个id的分配和回收和一个进程的fd分配和回收使用的是相同的策略,也就是可被重复回收和利用的文件会被逐个使用,所以并不能保证后分配的资源的id一定在后面,因为随着系统中的资源的分配和回收,谁也不知道当一个分配进行的时候系统的状态是啥样子的,就像《阿甘正传》中汤哥所说的:人生就像一盒巧克力,你永远不知道下一块你会选到哪一颗。
对于文件描述符来说,他是一个文件内有效的资源标识符,所以一般不会出问题。相对来说,ipc的id是一个系统级的概念,所以如果直接连续分配,考虑到进程间的异步同步等问题,这个id应该表现的更加的松散,也就是它们的差别应该更加的明显。
在更上层的调用,这个id也并没有直接返回给用户态,
    shp->id = shm_buildid(ns, id, shp->shm_perm.seq);
#define shm_buildid(ns, id, seq)    \
    ipc_buildid(&shm_ids(ns), id, seq)
int ipc_buildid(struct ipc_ids* ids, int id, int seq)
{
    return SEQ_MULTIPLIER*seq + id;
}
#define SEQ_MULTIPLIER    (IPCMNI)
#define IPCMNI 32768  /* <= MAX_INT limit for ipc arrays (including sysctl changes) */
对于用户返回的id,这里将分配的序列号乘以一个常量之后加上一个id,这样对于同一个数组的下标(也就是相同的下标值),由于seq不同,所以返回给用户态的id也并不相同,所以即使id被回收之后分配给一个新的进程,返回给用户态的标示符依然是不相同的。
五、如何折回
1、从用户态shmid找到数组下标的方法
上面的操作有些令人眼花缭乱,但是转眼间问题就来了。就像小学课本中《跳水》中描写的小孩,在船上向着桅杆向上爬,很精彩,但是问题是如何再从桅杆上下来就是一个更加严峻而棘手的问题了,因为如果要操作这个资源,例如shmctl,此时传递的内核返回给用户态的虚拟id,但是这个id并不是物理存在的,所以如何通过这id还原出真实的数组下标并找到对应的资源就是一个问题了。所以需要继续看看这个翻转的实现,当然既然已经知道了正向的操作,那么逆向的操作也不会复杂
struct kern_ipc_perm* ipc_lock(struct ipc_ids* ids, int id)
{
    struct kern_ipc_perm* out;
    int lid = id % SEQ_MULTIPLIER;
    struct ipc_id_ary* entries;

    rcu_read_lock();
    entries = rcu_dereference(ids->entries);
    if(lid >= entries->size) {
        rcu_read_unlock();
        return NULL;
    }
    out = entries->p[lid];
    if(out == NULL) {
        rcu_read_unlock();
        return NULL;
    }
    spin_lock(&out->lock);
   
    /* ipc_rmid() may have already freed the ID while ipc_lock
     * was spinning: here verify that the structure is still valid
     */
    if (out->deleted) {
        spin_unlock(&out->lock);
        rcu_read_unlock();
        return NULL;
    }
    return out;
}
2、从key找到一个已经创建的共享资源
int ipc_findkey(struct ipc_ids* ids, key_t key)
{
    int id;
    struct kern_ipc_perm* p;
    int max_id = ids->max_id;

    /*
     * rcu_dereference() is not needed here
     * since ipc_ids.mutex is held
     */
    for (id = 0; id <= max_id; id++) {
        p = ids->entries->p[id];
        if(p==NULL)
            continue;
        if (key == p->key)
            return id;
    }
    return -1;
}
大家看到这里应该比较开心,这样的代码应该和大家写的代码水平一样的,简单的遍历循环,然后找到key相等的一个。
六、如何找到哪些进程在使用一个共享内存
1、理论基础
这个问题和如何找到所有的打开一个文件的进程一样,文件本身是不会保存谁打开了这些文件,因为这个打开文件承载了太多的责任。但是另一方面来说,如何知道所有打开这个文件的进程。这个只能从哪些真正打开这个文件的进程中来找到了。就好像一个人被枪击了,你去问他,谁射的子弹,它可能不知道,但是查找搜有有武器的人应该是一个比较好的入口。
回想一下如何查案一个文件被哪些进程打开,可以遍历proc文件系统下的所有文件,看文件的fd文件,然后看看哪些进程打开了文件。
对于shm共享内存,在fd文件中是不能看到的,但是好在shm的底层实现使用过内存文件系统实现的,也就是通过创建一个shm文件,然后执行mmap操作。或者可以说shmget等价于shm_open,而shmat则相当于mmap操作,只是前者是内核代替用户做的,而后者则是用户态直接做的。另外shmget并不占用系统的文件描述符,因为这个文件是内核默默创建的,用户态并不需要知道,并且这个文件是所有进程共享的,所以对用户透明。
2、实现方法
当进程执行shmat的时候,此时内核会讲一个文件mmap到进程的地址空间,而proc中可以查看文件的映射内存,并且文件的名字是特殊格式化的,因为在内存创建的时候
static int newseg (struct ipc_namespace *ns, key_t key, int shmflg, size_t size)

        sprintf (name, "SYSV%08x", key);
        file = shmem_file_setup(name, size, acctflag);
看一下系统中的实现
[root@Harry cgi-bin]# cat /proc/*/maps | grep SYSV
b57cb000-b582b000 rw-s 00000000 00:08 393225     /SYSV00000000 (deleted)
b5863000-b597d000 rw-s 00000000 00:08 1015828    /SYSV00000000 (deleted)
b597d000-b59dd000 rw-s 00000000 00:08 655376     /SYSV00000000 (deleted)
b59dd000-b5a3d000 rw-s 00000000 00:08 622607     /SYSV00000000 (deleted)
b5a3d000-b5a9d000 rw-s 00000000 00:08 458762     /SYSV00000000 (deleted)
这里后面的后缀为0,说明这些进程创建的时候都是进程内共享的一个文件。可以看到,之前还有一个数字,这个数字的意义为用户态可见的shmid,但是在一些版本中,这些的确是内存文件系统的inode编号,因为在2.6.21版本中就是这么实现的,但是在我使用的2.6.31内核中看到这个inode却是用户态可见的shmid。当时我也困惑了一下,不过看看内核的代码:
3、maps文件展示来源
linux-2.6.21\fs\proc\task_mmu.c

if (file) {
        struct inode *inode = vma->vm_file->f_path.dentry->d_inode;
        dev = inode->i_sb->s_dev;
        ino = inode->i_ino;
    }

    seq_printf(m, "%08lx-%08lx %c%c%c%c %08lx %02x:%02x %lu %n",
            vma->vm_start,
            vma->vm_end,
            flags & VM_READ ? 'r' : '-',
            flags & VM_WRITE ? 'w' : '-',
            flags & VM_EXEC ? 'x' : '-',
            flags & VM_MAYSHARE ? 's' : 'p',
            vma->vm_pgoff << PAGE_SHIFT,
            MAJOR(dev), MINOR(dev), ino, &len);
4、shm_setup中inode编号
struct file *shmem_file_setup(char *name, loff_t size, unsigned long flags)---->>>shmem_get_inode--->>new_inode
    static unsigned long last_ino;
……
        inode->i_ino = ++last_ino;
5、2.6.31内核实现
static int newseg (key_t key, int shmflg, size_t size)
    shp->id = shm_buildid(id,shp->shm_perm.seq);
    shp->shm_file = file;
    file->f_dentry->d_inode->i_ino = shp->id;
    file->f_op = &shm_file_operations;
    shm_tot += numpages;
    shm_unlock (id);
    return shp->id;
这里的shp->id就和用户态看到的id相同了
  评论这张
 
阅读(1515)| 评论(0)
推荐 转载

历史上的今天

评论

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

页脚

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