精华内容
下载资源
问答
  • 用户态进程地址空间映射

    千次阅读 2019-06-12 18:32:27
    1、32位及64位地址空间分布 .../proc/pid/maps显示进程映射了的内存区域和访问权限。对应内核中的操作集为proc_pid_maps_op,具体的导出函数为show_map。内核中进程的一段地址空间用一个vm_area_struc...

    1、32位及64位地址空间分布
    地址空间
    2、proc maps文件解析
    查看用户进程地址映射可使用pmap命令,也可查看proc文件系统下的maps文件。下面是对proc下maps文件信息的解析。

    /proc/pid/maps显示进程映射了的内存区域和访问权限。对应内核中的操作集为proc_pid_maps_op,具体的导出函数为show_map。内核中进程的一段地址空间用一个vm_area_struct结构体表示,所有地址空间存储在task->mm->mmap链表中。

    一个文件可以映射到进程的一段内存区域中,映射的文件描述符保存在vm_area_struct->vm_file域中,这种内存区域叫做有名内存区域,相反,属于匿名映射内存区域。Vm_area_struct每项对应解析如下表所示:
    在这里插入图片描述
    下面一起看下一个proc maps的例子:

    cat /proc/19970/task/19970/maps
    001f7000-00212000 r-xp 00000000 fd:00 2719760    /lib/ld-2.5.so
    00212000-00213000 r-xp 0001a000 fd:00 2719760    /lib/ld-2.5.so
    00213000-00214000 rwxp 0001b000 fd:00 2719760    /lib/ld-2.5.so
    00214000-0036b000 r-xp 00000000 fd:00 2719767    /lib/libc-2.5.so
    0036b000-0036d000 r-xp 00157000 fd:00 2719767    /lib/libc-2.5.so
    0036d000-0036e000 rwxp 00159000 fd:00 2719767    /lib/libc-2.5.so
    0036e000-00371000 rwxp 0036e000 00:00 0
    0054f000-00565000 r-xp 00000000 fd:00 2719791    /lib/libpthread-2.5.so
    00565000-00566000 r-xp 00015000 fd:00 2719791    /lib/libpthread-2.5.so
    00566000-00567000 rwxp 00016000 fd:00 2719791    /lib/libpthread-2.5.so
    00567000-00569000 rwxp 00567000 00:00 0
    006f5000-006f6000 r-xp 006f5000 00:00 0          [vdso]
    08048000-08049000 r-xp 00000000 fd:00 3145810    /home/lijz/code/pthread
    08049000-0804a000 rw-p 00000000 fd:00 3145810    /home/lijz/code/pthread
    08c50000-08c71000 rw-p 08c50000 00:00 0          [heap]
    b75d7000-b75d8000 ---p b75d7000 00:00 0
    b75d8000-b7fda000 rw-p b75d8000 00:00 0
    b7fe4000-b7fe5000 rw-p b7fe4000 00:00 0
    bf987000-bf99c000 rw-p bffea000 00:00 0          [stack]
    

    进程的每段地址空间由struct vm_area_struct 描述。如上所示的每一行对应一个vm_area_struct结构体。一个文件可以映射到内存中,vm_area_struct的vm_file保存了文件描述符,这种映射称为有名映射,反之则为匿名映射。下面以第十四行为例,解释各例的内容。

    第一列:08049000-0804a000-----本段内存映射的虚拟地址空间范围,对应vm_area_struct中的vm_start和vm_end。

    第二列:rw-p----权限 r-读,w-写 x-可执行 p-私有,对应vm_flags。

    第三列:00000000----针对有名映射,指本段映射地址在文件中的偏移,对应vm_pgoff。对匿名映射而言,为vm_area_struct->vm_start。

    第四列:fd:00----所映射的文件所属设备的设备号,对应vm_file->f_dentry->d_inode->i_sb->s_dev。匿名映射为0。其中fd为主设备号,00为次设备号。

    第五列:3145810----文件的索引节点号,对应vm_file->f_dentry->d_inode->i_ino,与ls –i显示的内容相符。匿名映射为0。

    第六列:/home/lijz/code/pthread—所映射的文件名。对有名映射而言,是映射的文件名,对匿名映射来说,是此段内存在进程中的作用。[stack]表示本段内存作为栈来使用,[heap]作为堆来使用,其他情况则为无。

    经过上面的分析,proc maps中的每一列代表的意思已经非常清晰了。接下来看下proc每maps中每一行的解析。各共享库的代码段,存放着二进制可执行的机器指令,由kernel把该库ELF文件的代码段map到虚存空间;各共享库的数据段,存放着程序执行所需的全局变量,由kernel把ELF文件的数据段map到虚存空间;用户代码段,存放着二进制形式的可执行的机器指令,由kernel把ELF文件的代码段map到虚存空间;用户数据段,存放着程序执行所需的全局变量,由kernel把ELF文件的数据段map到虚存空间;堆(heap),当且仅当malloc调用时存在,由kernel把匿名内存map到虚存空间,堆则在程序中没有调用malloc的情况下不存在;栈(stack),作为进程的临时数据区,由kernel把匿名内存map到虚存空间,栈空间的增长方向是从高地址到低地址。

    pthread这个应用程序在maps中占用了两行,内容如下:

    08048000-08049000 r-xp 00000000 fd:00 3145810    /home/lijz/code/pthread
    08049000-0804a000 rw-p 00000000 fd:00 3145810    /home/lijz/code/pthread
    

    其中第一行的权限是只读,并且可执行,说明第一行是应用程序的代码段,而第二行的权限是可读可写,但是没有执行权限,说明该段是pthread的数据段。

    00c56000-00dad000 r-xp 00000000 fd:00 2719767    /lib/libc-2.5.so
    00dad000-00daf000 r-xp 00157000 fd:00 2719767    /lib/libc-2.5.so
    00daf000-00db0000 rwxp 00159000 fd:00 2719767    /lib/libc-2.5.so
    

    以上是libc-2.5共享库在maps文件中的记录,每个共享库在maps文件中对应着三行,分别是数据段与代码段。

    堆[heap]段。

    08c64000-08c85000 rw-p 08c64000 00:00 0          [heap]
    

    有些maps文件并不会出现该记录,这主要跟程序中有无使用malloc相关,如果主线程使用了malloc就会有该记录,否则就没有。在子线程中调用malloc,会产生另外的堆映射,但是并不会标记[heap]。例如,在子线程中动态分配1MB的内存空间,pthread2应用程序的执行结果如下所示:

        tid addr 0xbfd818f0 
        child thread run    
        stackbase 0xb7f4f3c0 
        stackaddr =0x7754e008----malloc分配的地址
        guardsize 4096
    

    对应的maps文件:

    08048000-08049000 r-xp 00000000 fd:00 3145811    /home/lijz/code/pthread2    
    08049000-0804a000 rw-p 00000000 fd:00 3145811    /home/lijz/code/pthread2    
    0945a000-0947b000 rw-p 0945a000 00:00 0          [heap]    
    7754e000-b754f000 rw-p 7754e000 00:00 0 -----------区间大小正是1MB    
    b754f000-b7550000 ---p b754f000 00:00 0    
    b7550000-b7f52000 rw-p b7550000 00:00 0    
    b7f5c000-b7f5d000 rw-p b7f5c000 00:00 0   
    bfd6e000-bfd83000 rw-p bffea000 00:00 0          [stack]
    

    maps文件中红色标注的行,从内容上看,本段内存大小是1MB,权限为读写私有,偏移为本段内存的开始地址,设备号和文件索引节点为0。可以看出本段内存是进程通过mmap映射的一段空间,是匿名映射。在pthread2程序中,正好用malloc分配了一个1MB的内存,能够与这段内存对应。同时,malloc分配的地址0x7754e008正落在该区间,并且偏向区间低地址部分,说明该区间是个堆地址空间。说明了这段1M的内存确实是进程调用malloc分配的,其中malloc又调用mmap系统调用匿名映射。

    栈段[stack],下面用几个例子来说明栈段。

    bfd50000-bfd65000 rw-p bffea000 00:00 0          [stack]
    

    对于单线程应用程序而言,只有一个[stack]段,对应多线程应用程序,[stack]段是主线程的栈空间,子线程的栈空间则用pthread库自动分配。

    例1,将一个单线程的应用的局部变量的地址打印出来,执行的结果如下所示:

    ./pthread2    
    tid addr 0xbfc73600
    

    对应的maps文件:

    08048000-08049000 r-xp 00000000 fd:00 3145811    /home/lijz/code/pthread2    
    08049000-0804a000 rw-p 00000000 fd:00 3145811    /home/lijz/code/pthread2    
    b7f7e000-b7f80000 rw-p b7f7e000 00:00 0    
    b7f8a000-b7f8b000 rw-p b7f8a000 00:00 0    
    bfc5f000-bfc74000 rw-p bffea000 00:00 0          [stack]
    

    局部变量的地址0xbfc73600在[stack]区间。

    例2:将一个拥有一个子线程的应用局部变量打印出来,执行的结果如下所示:

    tid addr 0xbfd64740---------主线程中打印的局部变量地址   
    child thread run    
    stackaddr   0xb7fc93c4--------子线程中打印的局部变量地址    
    guardsize 4096---------栈保护页大小
    

    对应的maps文件如下:

    08048000-08049000 r-xp 00000000 fd:00 3145811    /home/lijz/code/pthread2  
    08049000-0804a000 rw-p 00000000 fd:00 3145811    /home/lijz/code/pthread2    
    08c64000-08c85000 rw-p 08c64000 00:00 0          [heap]    
    b75c9000-b75ca000 ---p b75c9000 00:00 0---------pthread_create默认的栈溢出保护区    
    b75ca000-b7fcc000 rw-p b75ca000 00:000------------pthread_create创建的子线程的栈空间    
    b7fd6000-b7fd7000 rw-p b7fd6000 00:00 0------------------4KB应该也是通过mmap产生的匿名映射    
    bfd50000-bfd65000 rw-p bffea000 00:00 0          [stack]---------主进程的栈空间
    

    由上执行结果显示,主线程中局部变量地址0xbfd64740落在[stack]区间,而子线程局部变量地址0xb7fc93c4则落在b75ca000-b7fcc000 rw-p b75ca00区间,并且局部变量的地址从高地址开始分配,说明该VMA正是子线程的栈地址空间。另外,对栈空间,pthread默认设置了一个4KB的栈保护页,对应的区间为:b75c9000-b75ca000—p b75c9000,该区间不可读,不可写,也不能执行,通过这些属性信息的设置,可以达到栈溢出保护的作用。

    例3:在例2的基础上,多创建一个线程,pthread2程序的执行结果如下所示:

    ./pthread2    
    tid addr 0xbfc81610----------主线程局部变量地址    
    child thread run   
    stackaddr = 0xb7f183c0-------子线程1局部变量地址   
    guardsize 4096   
    child thread2 run    
    stackaddr =0xb75173c4 ----------子线程局部变量地址    
    guardsize 4096
    

    对应的maps文件:

    08048000-08049000 r-xp 00000000 fd:00 3145811    /home/lijz/code/pthread2
    08049000-0804a000 rw-p 00000000 fd:00 3145811    /home/lijz/code/pthread2
    092d6000-092f7000 rw-p 092d6000 00:00 0          [heap]
    76b16000-b6b17000 rw-p 76b16000 00:00 0 ----------mallocmmap
    b6b17000-b6b18000 ---p b6b17000 00:00 0
    b6b18000-b7518000 rw-p b6b18000 00:000---------pthread thread2 stack space
    b7518000-b7519000 ---p b7518000 00:00 0                                      
    b7519000-b7f1b000 rw-p b7519000 00:000----------pthread thread1 stack space
    b7f25000-b7f26000 rw-p b7f25000 00:00 0
    bfc6e000-bfc83000 rw-p bffea000 00:00 0          [stack]---main thread stack space
    

    从maps文件记录上看,增加一个子线程,在maps文件中就增加了两条记录,分别是子线程的栈空间和栈保护页的记录。默认情况下,pthread为子线程预留的栈空间大小为1MB,栈保护页为4KB(这主要跟页大小相关)。

    总之,proc maps文件可以查看进程的内存映射,每一段内存的权限属性等信息。

    转自:https://blog.csdn.net/lijzheng/article/details/23618365
    感谢原作者分析。

    展开全文
  • linux驱动-映射进程空间

    千次阅读 2017-02-12 10:11:44
    简述:内核映射进程空,就是由进程分配好空间(属于进程独占资源)后,将用户空间虚拟地址,传递到内核,然后内核映射成内核虚拟地址直接访问,此时内核访问的物理空间是位于用户空间。这样的好处是,不再是内核将...

    简述:

    内核映射进程空间,就是由进程分配好空间(属于进程独占资源)后,将用户空间虚拟地址,传递到内核,然后内核映射成内核虚拟地址直接访问,此时内核访问的物理空间是位于用户空间。这样的好处是,内核直接访问进程空间,减少copy动作。

    接口:

    • 接口要包含的头文件:
    #include <linux/mm.h>
    • 函数接口:
    long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
                unsigned long start, unsigned long nr_pages,
                int write, int force, struct page **pages,
                struct vm_area_struct **vmas);

    功能:内核用来映射进程内存空间
    第一个参数: tsk是进程控制块指针,这个参数几乎一直作为 current 传递。
    第二个参数: 一个内存管理结构的指针, 描述被映射的地址空间. mm_struct 结构是捆绑一个进程的虚拟地址空间所有部分. 对于驱动的使用, 这个参数应当一直是current->mm。
    第三个参数: start 是(页对齐的)用户空间缓冲的地址。要映射进程空间虚拟地址的起始地址。
    第四个参数: nr_pages是映射进程空间的大小,单位页。
    第五个参数: write是内核映射这段进程空间来读还是写,非0(一般就是1)表示用来写,当然,对于用户空间就只能是读了;为0表示用来读,此时用户空间就只能是写了。
    第六个参数: force 标志告知 get_user_pages 来覆盖在给定页上的保护, 来提供要求的
    权限; 驱动应当一直传递 0 在这里。
    第七个参数: pages是这个函数的输出参数,映射完成后,pages是指向进程空间的页指针数组,如pages[1],下标最大是nr_pages-1。注意,内核要用pages,还需要映射成内核虚拟地址,一般用kmap(),kmap_atomic()
    第八个参数: vmas也是个输出参数,vm_area_struct结构体,是linux用来管理虚拟内存的,映射完成后关于虚拟内存的信息就在这结构体里面。如果驱动不用,vmas可以是NULL。
    返回值: 返回实际映射的页数,只会小于等于nr_pages。

    映射:

    • 映射的时候要上进程读者锁:
      进程旗标(锁/信号量),通过current->mm->mmap_sem来获得。
      如:
    down_read(&current->mm->mmap_sem);
    result = get_user_pages(current, current->mm, ...);
    up_read(&current->mm->mmap_sem);

    如果内核映射的空间,用来写,写的时候要上进程写锁,在写的时候去读,就会被阻塞:

    down_write(current->mm->mmap_sem);
    ...
    //向映射的空间写数据
    //up_write(current->mm->mmap_sem);

    current->mm->page_table_lock.rlock也可以用这个自旋锁实现的读者写者,具体情况考虑。

    释放映射:

    if (! PageReserved(page))
        SetPageDirty(page);
    page_cache_release(struct page *page);

    PageReserved(): 判断是否为保留页,是保留页返回非0,不是返回0。在我们一般映射的页,没经过处理,都不是保留页,返回0。
    SetPageDirty(): 简单来说,就是告诉系统这页被修改。

    PageReserved(),SetPageDirty()定义在include/linux/page-flags.h,对他们的作用理解,兵不是很深,在一般的驱动中可有可无,安全起见,就按照上面形式放在哪里。

    page_cache_release()定义在include/linux/pagemap.h,只能是一次释放一个page,多个page用循环多次调用。

    思考:

    这种映射,主要是用在进程直接I/O,进程直接读写用户空间内存,就可以达到读写I/O。但是也要具体情况看,相对这种映射对系统开销,还是比较大的。
    get_user_pages的用法例子,可以看drivers/scsi/st.c

    用这种映射来实现读者写者,进程是读者,驱动是写者。
    在“linux驱动—file_operations异步读写aio_read、aio_write”这章,描述的异步例子基础上做,

    原始例子如下:

    struct kiocb *aki;
    struct iovec *aiov;
    loff_t aio_off = 0;
    struct workqueue *aiowq;
    
    void data_a(struct work_struct *work);
    DECLARE_DELAYED_WORK(aio_delaywork,data_a);
    
    ssize_t d_read(struct file *f, char __user *buf, size_t n, loff_t *off)
    {
    }
    void data_a(struct work_struct *work)
    {
        int ret = 0;
        ret = d_read(aki->ki_filp,aiov->iov->iov_base,n,&off);
        aio_complete(aki,ret ,0);
    }
    ssize_t d_aio_read(struct kiocb *ki, const struct iovec *iovs, unsigned long n, loff_t off)
    {
        if(is_sync_kiocb(ki))
            return d_read(ki->ki_filp,iovs->iov->iov_base,n,&off);
        else
        {
            aki = ki;
            aiov = iovs;
            aio_off = off;
            queue_delayed_work(aiowq,aio_delaywork,100);
            return -EIOCBQUEUED;//一般都返回这个,
        }
    }
    void init_aio()
    {
        aiowq= create_workqueue("aiowq");
    }

    用上get_user_pages()后,将变成:

    #include <linux/list.h>
    #include <linux/aio.h>
    #include <linux/workqueue.h>
    #include <linux/mm.h>
    
    struct kiocb *aki;
    //struct iovec *aiov;
    //loff_t aio_off = 0;
    struct workqueue *aiowq;
    struct page **aiopages;
    
    
    
    LIST_HEAD(custom_aa);
    
    struct custom_async{
        list_head list;
        task_struct *tsk;
        struct page **pg;
        long page_num;
        ssize_t size;   
    };
    
    void data_a(struct work_struct *work);
    DECLARE_DELAYED_WORK(aio_delaywork,data_a);
    DECLARE_DELAYED_WORK(aio_delaywork_c,async_d_writedata);
    
    struct file_operations {
        .owner = THIS_MODE,
        .read = d_read,
        .aio_read = d_aio_read,
        ...
        ..
    };
    
    
    ssize_t d_read(struct file *f, char __user *buf, size_t n, loff_t *off)
    {
    }
    ssize_t async_d_writedata(struct work_struct *work)
    {
        int i,n;
        void *p = NULL;
        struct page **temp = NULL;
        struct custom_async *t = NULL;
        struct list_head *tmp = NULL;
        list_for_each(custom_aa,tmp){
            t = container_of(tmp,custom_async,list);
            for(i=0;i<t->page_num;i++)
            {
                temp = t->pg;
                p = kmap(temp[i]);
                down_write(&t->tsk->mm->mmap_sem);
                ...
                //向p指定的空间写数据 ,   n为写了多少个字节数据
                ...
                t->size = n;
                up_write(&t->tsk->mm->mmap_sem);
                kunmap(aio_pages[i]);
            }
        }
        queue_delayed_work(aiowq,aio_delaywork_c,100);  
    }
    void data_a(struct work_struct *work)
    {
        int ret = 0;
        struct custom_async *t;
        //ret = d_read(aki->ki_filp,aiov->iov->iov_base,n,&off);
    
        t = container_of(custom_aa.next,custom_async,list);
        async_d_writedata();     
        ret = t->size;
        aio_complete(aki,ret ,0);
    }
    ssize_t d_aio_read(struct kiocb *ki, const struct iovec *iovs, unsigned long n, loff_t off)
    {   
        if(is_sync_kiocb(ki))
            return d_read(ki->ki_filp,iovs->iov->iov_base,n,&off);
        else
        {
            aki = ki;
            //aiov = iovs;
            //aio_off = off;
            struct custom_async *p;
    
            p = kmalloc(sizeof(struct custom_async));
            memset(p,0,sizeof(struct custom_async));
            INIT_LIST_HEAD(&p->list);
            list_add(&p->list,&custom_aa); 
            p->tsk = current;       
            down_read(&current->mm->mmap_sem);
            pagenum = get_user_pages(current,current->mm,iovs->iov->iov_base,
                            n/PAGE_SIZE+(n%PAGE_SIZE)?1:0,
                            1,0,p->pg,NULL);
            up_read(&current->mm->mmap_sem);       
            queue_delayed_work(aiowq,aio_delaywork,100);
            return -EIOCBQUEUED;//一般都返回这个,
        }
    }
    void init_aio()
    {
        aiowq= create_workqueue("aiowq");
        INIT_LIST_HEAD(custom_aa);
    }
    void exit()
    {
        int i;
        ...
        ..
        for(i=0;i<pagenum;i++)
        {
            if (! PageReserved(aiopages[i]))
                SetPageDirty(aiopages[i]);
            page_cache_release(aiopages[i]);
        }
        ...
        ..
    }

    上面的例子,进程都可以通过异步调用来申请映射,当进程收到一次异步调用完成后,不用再次异步调用,只需要读就可以,驱动会自动的向里面写数据。只要来异步调用申请过一次的进程,当有数据时,都会向每个进程空间写数据。典型的读者写者,进程是读者,驱动是写者。

    上面例子,只是表达了处理逻辑,没有编译过,模块退出时,释放不完全。

    对比一般的异步,内核是直接向进程空间写数据,不用在数据完成后,还需要一次copy,理论上会快些。

    展开全文
  • 进程地址空间和内存文件映射

    千次阅读 2014-02-03 00:13:50
    进程地址空间 每个进程都有自己的地址空间。对32位进程来说,由于32位指针可以表示从0x00000000到0xFFFFFFFF之间的任一值,地址空间的大小为4GB。对64位进程来说,由于64位指针可以表示从0x00000000'00000000到0...

    进程地址空间

    每个进程都有自己的地址空间。对32位进程来说,由于32位指针可以表示从0x00000000到0xFFFFFFFF之间的任一值,地址空间的大小为4GB。对64位进程来说,由于64位指针可以表示从0x00000000'00000000到0xFFFFFFFF'FFFFFFFF之间的任一值, 地址空间大小为16GB。

    其实这个地址空间是不存在的,也就是我们所说的进程虚拟内存空间

    linux操作系统每个进程的地址空间都是独立的,其实这里的独立说得是物理空间上得独立。那相同的虚拟地址,不同的物理地址,他们之间是怎样联系起来的呢?当一个程序被执行时,该程序的内容必须被放到进程的虚拟地址空间,对于可执行程序的共享库也是如此。可执行程序并非真正读到物理内存中,而只是链接到进程的虚拟内存中。

    在进程创建的过程中,程序内容被映射到进程的虚拟内存空间,为了让一个很大的程序在有限的物理内存空间运行,我们可以把这个程序的开始部分先加载到物理内存空间运行,因为操作系统处理的是进程的虚拟地址,如果在进行虚拟到物理地址的转换工程中,发现物理地址不存在时,这个时候就会发生缺页异常(nopage),接着操作系统就会把磁盘上还没有加载到内存中的数据加载到物理内存中,对应的进程页表进行更新。也许你会问,如果此时物理内存满了,操作系统将如何处理?

    当物理内存溢出时,linux使用"最近最少使用(LeastRecently Used ,LRU)"(很多缓存技术都是这个原则)页面调度技巧来公平地选择哪个页可以从系统中删除。

    内存映射文件

    内存映射文件是由一个文件到进程地址空间的映射,而不是真正意义上的映射内存。通过文件映射这种使磁盘文件的全部或部分内容与进程地址空间的某个区域建立映射关联的能力,可以直接对被映射的文件进行访问,而不必执行文件I/O操作也无需对文件内容进行缓冲处理。。内存文件映射的这种特性是非常适合于用来管理大尺寸文件的。

    使用内存映射文件的一般流程

    如果有十几GB乃至几十GB容量的巨型文件,显然不能一次将文件映像全部映射进来。对于这种情况只能依次将大文件的各个部分映射到进程中的一个较小的地址空间。这需要对上面的一般流程进行适当的更改:

    1)映射文件开头的映像。

    2)对该映像进行访问。

    3)取消此映像

    4)映射一个从文件中的一个更深的位移开始的新映像。

    5)重复步骤2,直到访问完全部的文件数据。

     

    展开全文
  • Linux进程间通信—— 内存映射

    千次阅读 2017-07-29 10:26:08
    两个不同进程A、B共享内存的意思是,同一块物理内存被映射进程A、B各自的进程地址空间进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁...

    Linux环境进程间通信(五): 共享内存(上)

    共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

    采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

    Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

    一、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面

    1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

    2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

    3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

    4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。 
    注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

    5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。 
    注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

    上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

    二、mmap()及其相关系统调用

    mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

    注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

    1、mmap()系统调用形式如下:

    void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset ) 
    参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

    2、系统调用mmap()用于共享内存的两种方式:

    (1)使用普通文件提供的内存映射:适用于任何进程之间; 此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

    	fd=open(name, flag, mode);
    if(fd<0)
    	...

    ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。

    (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间; 由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。 
    对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。

    3、系统调用munmap()

    int munmap( void * addr, size_t len ) 
    该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

    4、系统调用msync()

    int msync ( void * addr , size_t len, int flags) 
    一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

    三、mmap()范例

    下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

    范例1:两个进程通过映射普通文件实现共享内存通信

    范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。

    下面是两个程序代码:

    /*-------------map_normalfile1.c-----------*/
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
      char name[4];
      int  age;
    }people;
    main(int argc, char** argv) // map a normal file as shared mem:
    {
      int fd,i;
      people *p_map;
      char temp;
      
      fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
      lseek(fd,sizeof(people)*5-1,SEEK_SET);
      write(fd,"",1);
      
      p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
            MAP_SHARED,fd,0 );
      close( fd );
      temp = 'a';
      for(i=0; i<10; i++)
      {
        temp += 1;
        memcpy( ( *(p_map+i) ).name, &temp,2 );
        ( *(p_map+i) ).age = 20+i;
      }
      printf(" initialize over \n ");
      sleep(10);
      munmap( p_map, sizeof(people)*10 );
      printf( "umap ok \n" );
    }
    /*-------------map_normalfile2.c-----------*/
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
      char name[4];
      int  age;
    }people;
    main(int argc, char** argv)  // map a normal file as shared mem:
    {
      int fd,i;
      people *p_map;
      fd=open( argv[1],O_CREAT|O_RDWR,00777 );
      p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
           MAP_SHARED,fd,0);
      for(i = 0;i<10;i++)
      {
      printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
      }
      munmap( p_map,sizeof(people)*10 );
    }

    map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。

    map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。

    分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:

    initialize over
    umap ok

    在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):

    name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
    name: g	age 25;	name: h	age 26;	name: I	age 27;	name: j	age 28;	name: k	age 29;

    在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:

    name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
    name:	age 0;	name:	age 0;	name:	age 0;	name:	age 0;	name:	age 0;

    从程序的运行结果中可以得出的结论

    1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;

    2、 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。 
    注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

    3、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

    范例2:父子进程通过匿名映射实现共享内存

    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
      char name[4];
      int  age;
    }people;
    main(int argc, char** argv)
    {
      int i;
      people *p_map;
      char temp;
      p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
           MAP_SHARED|MAP_ANONYMOUS,-1,0);
      if(fork() == 0)
      {
        sleep(2);
        for(i = 0;i<5;i++)
          printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age);
        (*p_map).age = 100;
        munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。
        exit();
      }
      temp = 'a';
      for(i = 0;i<5;i++)
      {
        temp += 1;
        memcpy((*(p_map+i)).name, &temp,2);
        (*(p_map+i)).age=20+i;
      }
      sleep(5);
      printf( "parent read: the first people,s age is %d\n",(*p_map).age );
      printf("umap\n");
      munmap( p_map,sizeof(people)*10 );
      printf( "umap ok\n" );
    }

    考察程序的输出结果,体会父子进程匿名共享内存:

    child read: the 1 people's age is 20
    child read: the 2 people's age is 21
    child read: the 3 people's age is 22
    child read: the 4 people's age is 23
    child read: the 5 people's age is 24
    parent read: the first people,s age is 100
    umap
    umap ok

    四、对mmap()返回地址的访问

    前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:

    图 1

    注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
    	char name[4];
    	int  age;
    }people;
    main(int argc, char** argv)
    {
    	int fd,i;
    	int pagesize,offset;
    	people *p_map;
    	
    	pagesize = sysconf(_SC_PAGESIZE);
    	printf("pagesize is %d\n",pagesize);
    	fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
    	lseek(fd,pagesize*2-100,SEEK_SET);
    	write(fd,"",1);
    	offset = 0;	//此处offset = 0编译成版本1;offset = pagesize编译成版本2
    	p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);
    	close(fd);
    	
    	for(i = 1; i<10; i++)
    	{
    		(*(p_map+pagesize/sizeof(people)*i-2)).age = 100;
    		printf("access page %d over\n",i);
    		(*(p_map+pagesize/sizeof(people)*i-1)).age = 100;
    		printf("access page %d edge over, now begin to access page %d\n",i, i+1);
    		(*(p_map+pagesize/sizeof(people)*i)).age = 100;
    		printf("access page %d over\n",i+1);
    	}
    	munmap(p_map,sizeof(people)*10);
    }

    如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize*2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize*3的字节数。

    版本1的输出结果如下:

    pagesize is 4096
    access page 1 over
    access page 1 edge over, now begin to access page 2
    access page 2 over
    access page 2 over
    access page 2 edge over, now begin to access page 3
    Bus error		//被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面

    版本2的输出结果如下:

    pagesize is 4096
    access page 1 over
    access page 1 edge over, now begin to access page 2
    Bus error		//被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面

    结论:采用系统调用mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。内部实现机制区涉及到了linux存储管理以及文件系统等方面的内容,可以参考一下相关重要数据结构来加深理解。在本专题的后面部分,将介绍系统v共享内存的实现。


    Linux环境进程间通信(五): 共享内存(下)

    在共享内存(上)中,主要围绕着系统调用mmap()进行讨论的,本部分将讨论系统V共享内存,并通过实验结果对比来阐述两者的异同。系统V共享内存指的是把所有共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。

    系统调用mmap()通过映射一个普通文件实现共享内存。系统V则是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的),后面还将阐述。

    1、系统V共享内存原理

    进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

    注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

    struct shmid_kernel /* private to the kernel */
    {	
    	struct kern_ipc_perm	shm_perm;
    	struct file *		shm_file;
    	int			id;
    	unsigned long		shm_nattch;
    	unsigned long		shm_segsz;
    	time_t			shm_atim;
    	time_t			shm_dtim;
    	time_t			shm_ctim;
    	pid_t			shm_cprid;
    	pid_t			shm_lprid;
    };

    该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

    这里我们采用[1]中的图表给出与系统V共享内存相关数据结构:

    正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

    在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。

    2、系统V共享内存API

    对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。

    #include <sys/ipc.h>
    #include <sys/shm.h>

    shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出它们的调用方法。

    注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

    3、系统V共享内存限制

    在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

    在[2]中,给出了这些限制的测试方法,不再赘述。

    4、系统V共享内存范例

    本部分将给出系统V共享内存API的使用方法,并对比分析系统V共享内存机制与mmap()映射普通文件实现共享内存之间的差异,首先给出两个进程通过系统V共享内存通信的范例:

    /***** testwrite.c *******/
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>
    #include <unistd.h>
    typedef struct{
    	char name[4];
    	int age;
    } people;
    main(int argc, char** argv)
    {
    	int shm_id,i;
    	key_t key;
    	char temp;
    	people *p_map;
    	char* name = "/dev/shm/myshm2";
    	key = ftok(name,0);
    	if(key==-1)
    		perror("ftok error");
    	shm_id=shmget(key,4096,IPC_CREAT);	
    	if(shm_id==-1)
    	{
    		perror("shmget error");
    		return;
    	}
    	p_map=(people*)shmat(shm_id,NULL,0);
    	temp='a';
    	for(i = 0;i<10;i++)
    	{
    		temp+=1;
    		memcpy((*(p_map+i)).name,&temp,1);
    		(*(p_map+i)).age=20+i;
    	}
    	if(shmdt(p_map)==-1)
    		perror(" detach error ");
    }
    /********** testread.c ************/
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>
    #include <unistd.h>
    typedef struct{
    	char name[4];
    	int age;
    } people;
    main(int argc, char** argv)
    {
    	int shm_id,i;
    	key_t key;
    	people *p_map;
    	char* name = "/dev/shm/myshm2";
    	key = ftok(name,0);
    	if(key == -1)
    		perror("ftok error");
    	shm_id = shmget(key,4096,IPC_CREAT);	
    	if(shm_id == -1)
    	{
    		perror("shmget error");
    		return;
    	}
    	p_map = (people*)shmat(shm_id,NULL,0);
    	for(i = 0;i<10;i++)
    	{
    	printf( "name:%s\n",(*(p_map+i)).name );
    	printf( "age %d\n",(*(p_map+i)).age );
    	}
    	if(shmdt(p_map) == -1)
    		perror(" detach error ");
    }

    testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,先后执行./testwrite及./testread 则./testread输出结果如下:

    name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
    name: g	age 25;	name: h	age 26;	name: I	age 27;	name: j	age 28;	name: k	age 29;

    通过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通信,可以得出如下结论:

    1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。 注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。

    2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

    3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。 注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。

    结论:

    共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

    共享内存涉及到了存储管理以及文件系统等方面的知识,深入理解其内部机制有一定的难度,关键还要紧紧抓住内核使用的重要数据结构。系统V共享内存是以文件的形式组织在特殊文件系统shm中的。通过shmget可以创建或获得共享内存的标识符。取得共享内存标识符后,要通过shmat将这个内存区映射到本进程的虚拟地址空间。


    展开全文
  • 注意要设置一下vma->vm_pgoff为你要map的io空间的物理地址对应的页。arm IO/ 内存统一编址 所以#define io_remap_pfn_range(vma,from,pfn,size,prot) / remap_pfn_range(vma, from, pfn, size,
  •  近几天在研究windows编程中内存映射方面的知识,发现对于初学者来说,涉及到的很多概念比较难以... 关于内存映射文件  http://blog.csdn.net/gaoyugaoyugaoyu/archive/2010/01/20/5218015.aspx<br /> 
  • 应该就已经对malloc、mmap大致了解了,它们就是在堆中创建(或合并)所需虚拟地址的vma线性区,换句话说,就是达到进程地址空间中要有满足要求的vma,但不会给vma映射物理页(除非一定要求,即vma的flags标识了页锁定...
  • 在我们处理较大文件的时候,通常需要使用文件映射,即将物理地址中的文件数据映射进程的虚拟地址中。通过文件映射之后,可以像操作内存一样去直接操作文件,而不需要再调用文件读写方法了。内存映射文件可以用于这...
  • 为了使挂钩能够像它们在1 6位Wi n d o w s中那样工作,M i c r o s o f t不得不设计了一种方法,使得D L L能够插入另一个进程的地址空间中。 下面让我们来看一个例子。进程A(类似Microsoft Spy++的一个实用程序)...
  • 【C语言】【unix c】使用mmap将物理地址映射进程的虚拟地址空间
  • 看到内核详解上说不管什么进程一旦进入内核就进入了系统空间都有相同的页面映射,内核就不用cr3吗?实在搞不明白,希望大虾详细讲解一下 | 内核当然会用cr3,在每个进程的页目录项中,都把最后3-4G的页目录项映射...
  • 正如其名(Memory Map),mmap 可以将某个设备或者文件映射到应用进程的内存空间中。通过直接的内存操作即可完成对设备或文件的读写。. 通过映射同一块物理内存,来实现共享内存,完成进程间的通信。由于减少了数据...
  • 应用文件映射进行进程间通讯

    千次阅读 2014-01-28 10:33:48
    应用文件映射进行进程间通讯
  • Linux进程地址空间进程的内存分布

    万次阅读 多人点赞 2018-05-15 20:13:18
    原网址为:https://blog.csdn.net/yusiguyuan/article/details/45155035一 进程空间分布概述 对于一个进程,其空间分布如下图所示: 程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。初始化过的...
  • Linux内核空间映射到用户空间

    千次阅读 2017-07-21 16:26:49
    两个不同进程A、B共享内存的意思是,同一块物理内存被映射进程A、B各自的进程地址空间进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁...
  • 用内存映射文件实现进程间通信

    千次阅读 2017-01-21 18:05:04
    这可以作为进程通讯的基础,而且在Windows上,同一台机器上共享数据的最底层机制都是内存映射文件。  许多应用程序会在运行过程中创建一些数据,并需要将这些数据传输给其他进程,或与其他进程共享这些数据。如果...
  • 浅谈进程地址空间与虚拟存储空间

    万次阅读 多人点赞 2015-04-17 19:35:47
    操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面...
  • 进程的虚拟地址映射

    千次阅读 2015-12-04 13:20:01
    这篇文章介绍了32位系统下进程在内存中的映射关系,包括了内核,栈,堆,共享库的映射,保留区。以及链接后的文件的一些段的具体在内存虚拟映射中的具体位置。非常清晰明了。虽然是32位,不过映射方法是与64位几乎...
  • 进程间通信——内存映射

    千次阅读 2015-03-17 21:45:26
    进程1的数据有时需要发送给进程B,这可以通过内存映射实现。 主要用到的函数: 1.首先在一个进程里用CreateFileMapping创建一个新的文件映射内核对象。 HANDLE CreateFileMapping(  HANDLE hFile, //物理文件句柄,...
  • 这篇博文可以在你基本了解逻辑地址空间和物理地址空间的概念后,为增强理解可通过我画的示意图来理解,本文会深入一些概念,以达到全面掌握该映射关系的目的。画图不易鸭,点个赞再走呗(✿◡‿◡) 逻辑地址空间及...
  • Linux进程通信-内存映射

    千次阅读 2011-01-17 20:26:00
    与内存映射相关的另外一个概念就是共享内存,A,B进程共享内存的意思是将共享内存映射到A,B各自的进程地址空间中去,以后无论进程A或者是进程B对共享内存的读写,都彼此知道。 从功能上区分,内存映射是...
  • 进程地址空间

    千次阅读 2018-08-02 13:57:01
    操作系统内核为每个被创建的进程都建立一个PCB(进程控制块或进程描述符)来保存与其相关的信息,PCB存在于进程的高 1 G空间,也就是内核空间中。在具体linux内核实现中,使用一个名为task_struct的结构体来描述的,...
  • vb端先新建内存映射文件,再调用C++对图片进行处理,处理后的结果写入vb建好的内存映射文件,然后vb端再读取内存映射文件。这样处理就可以不用在磁盘上进行读写操作了,节约了IO资源。 内存映射文件包含虚拟内存中...
  • 程序、进程、内存映射

    千次阅读 2011-07-17 11:28:31
    程序如何产生的?源代码经过下面四个步骤:1、预编译2、编译3、汇编4、连接程序就产生了。...程序是存储在硬盘上的静态的二进制可执行代码,进程是在内存中运行,并不断发生变化的活的二进制执行代码。程序

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 199,218
精华内容 79,687
关键字:

关于进程映射空间