精华内容
下载资源
问答
  • 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,理论上会快些。

    更多相关内容
  • 进程地址空间

    千次阅读 多人点赞 2022-03-31 20:01:15
    我们一起来看一看C/C++程序地址空间布局: 问题:这个C/C++程序地址空间是内存吗?

    我们一起来看一看C/C++程序地址空间布局:
    在这里插入图片描述
    问题:这个C/C++程序地址空间是内存吗?

    答案:这个不是内存!

    那么它究竟是什么呢?

    进程虚拟地址空间!

    我们来一段代码感受一下布局:

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<stdlib.h>
    #include<string.h>
    int g_unval;//为初始化全局数据
    int g_val=100;//已初始化全局数据
    int main()
    {
      const char *s="hello world";
      printf("code addr:%p\n",main);//栈区
      printf("string rdonly addr:%p\n",s);//字符常量区
      printf("init addr:%p\n",&g_val);//已初始化化全局区
      printf("uninit addr:%p\n",&g_unval);//未初始化全局区
      
      char *heap=(char*)malloc(10);
      char *heap1=(char*)malloc(10);
      char *heap2=(char*)malloc(10);
      char *heap3=(char*)malloc(10);
      printf("heap addr:%p\n",heap);//验证堆向上生长
      printf("heap1 addr:%p\n",heap1);
      printf("heap2 addr:%p\n",heap2);
      printf("heap3 addr:%p\n",heap3);
      
      printf("stack addr:%p\n",&s);//验证栈向下生长
      printf("stack addr:%p\n",&heap);
    
      int a=10;
      int b=20;
      printf("a addr:%p\n",&a);
      printf("b addr:%p\n",&b);
      return 0;
    }
    
    

    结果展示:

    [sjj@VM-20-15-centos 2022_4_1]$ ./myproc
    code addr:0x40057d
    string rdonly addr:0x400770
    init addr:0x60103c
    uninit addr:0x601044
    heap addr:0xaf9010
    heap1 addr:0xaf9030
    heap2 addr:0xaf9050
    heap3 addr:0xaf9070
    stack addr:0x7ffce118bc00
    stack addr:0x7ffce118bbf8
    a addr:0x7ffce118bbf4
    b addr:0x7ffce118bbf0
    

    可以看到确实是按照我们的规则排布的!
    我们再来一段代码感受一下:

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<stdlib.h>
    #include<string.h>
    int g_val=100;//全局变量,被父子进程共享
    int main()
    {
      pid_t id=fork();
      if(id==0)
      {
        //chlid
        int cnt=5;
        while(cnt)
        {
          printf("I am child! times:%d,g_val=%d,&g_val=%p\n",cnt,g_val,&g_val);
          cnt--;
          sleep(1);
          if(cnt==3)//三秒后修改数据
          {
            printf("#############child更改数据####################\n");
            g_val=200;
            printf("#############child更改数据完成################\n");
          }
        }
      }
      else
      {
          while(1)
          {
          printf("I am father! g_val=%d,&g_val=%p\n",g_val,&g_val);
          sleep(1);
          }
       }
      return 0;
    }
    
    

    演示效果:
    在这里插入图片描述
    父子进程打印出来的地址怎么能也没有变化呢?

     如果C/C++打印出来的是物理地址,那么这种现象是不可能存在的!所以我们这里,所使用的地址绝对不是物理地址!而被称为虚拟地址

     一般而言我们在语言层面上用到的地址,都是虚拟地址!
     我们系统中存在多个进程,每个进程都有一个地址空间,都认为自己独占整个物理内存!!!我们要管理地址空间,之前就已经提到过了,就得用先描述再组织的方式!描述:进程地址空间再内核中是一个数据结构类型,含有描述进程的地址空间变量,Linux下进程地址空间用一个结构体来描述——mm_struct
    在这里插入图片描述
     说明:虽然这里只有start和end,但是每个进程都可以认为mm_ struct 代表整个内存且所有的地址为0x00000000到0xFFFFFFFF,我们就把这串地址,叫做虚拟地址,我们可以将进程地址空间想象成一把有刻度的直尺,从0开始到全F,每一个划分的区间可以对应不同的区域,每一个刻度对应一个虚拟地址,也叫作线性地址。
    在这里插入图片描述
    可是数据总归是要存储在物理内存上的啊,那么虚拟地址与物理内存地址是怎么建立起联系的呢?那就是通过页表+MMU(MMU内存管理单元,用来查页表的)映射实现的!
    在这里插入图片描述
     页表:操作系统通过页表将虚拟地址转换为物理地址,进而再去访问代码和数据。
     为什么我们要有页表映射这一中间层?通过PCB直接访问物理内存不是更加方便吗?

    原因:那是为了我们的所有操作都在操作系统的约束下进行,加了中间的虚拟地址和页表映射能够更好的管理约束进程!

    在这里插入图片描述
    为什么要有进程虚拟地址空间?
    先来看几个例子,得出我们的结论!
     例1:如果没有虚拟地址空间,PCB就能直接访问物理内存的数据和代码,甚至能够修改里面的信息,这是很不安全的所以我们要加一个中间软件层——虚拟地址空间和页表
     例2:如果要申请1000字节,我们能马上用它吗?答案是不一定,我们可能会用,也可能不用。站在OS操作系统的角度看,空间如果马上给你,是不是意味着有一部分空间要给别的进程使用,但是现在却给了你闲置着。所以我们可以通过虚拟地址空间,先把内存同意申请给你,到你真正要用它的时候,OS会立刻通过页表建议映射关系,马上到物理内存上给你分配空间,但是反馈给上层(如PCB它是并不清楚底层的情况的)的确实分配到了相应的内存空间!
     例3:我们的CPU如果没有虚拟地址空间和页表映射,那么内存中本来就有着多个进程,那么每次CPU都要到物理内存中去找相应的代码和数据,每次寻找的地址都不一样,这对于CPU来说,效率就很降低,那么我们通过虚拟地址空间和页表映射,我们CPU统一将每个进程都看做一样的进程地址划分,main函数每次开始都是一样的虚拟地址,return也是同样的虚拟地址,即使每个进程在内存中的布局很混乱,通过页表的映射关系,总是能够找到对应的代码和数据。

    结论:
     1.通过添加一 层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全!
     2.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件的解耦和分离!
     3.站在CPU和应用层的角度, 进程统一可以看做统一使用4GB空间(32位), 而且每个空间区域的相对位置是比较确定的!

    操作系统这样设计的目的:让每个进程认为自己独占系统资源!!!

    知道上面这些结论我们就能解释开头的那个现象了!
     本质上我们的父子进程的&g_val的值是一样的,这个地址就是虚拟地址。
    每个进程都有自己的虚拟地址空间和页表, fork创建子进程后,子进程也是用于自己的虚拟地址空间和页表,同一个变量,地址相同,其实就是虚拟地址相同,内容不同其实是被映射到了不同的物理地址中!3秒时修改g_val值的时候,发生了写时拷贝,将子进程在物理空间上重新拷贝一份到新的物理地址上面,然后再重新在页表上面建立新的映射关系!
      ps:进程的特点:子进程以父进程为模板,进程间是具有独立性的!父子进程的代码一般是共享的!所以一般的只读代码,一般可以只有一份(也是从代码区到物理空间,通过页表建立映射关系),因为这样操作系统的代价才是最低的!
    在这里插入图片描述
    我们是不是也可以打印一下命令行参数呢?

    int main(int argc,char* argv[],char* env[])
    {
    	for (int i = 0; argv[i]; i++)
    	{
    		printf("argv[%d]:%p\n",%d,argv[i]);
    	}
    	for (int i = 0; env[i]; i++)
    	{
    		printf("argv[%d]:%p\n", %d, env[i]);
    	}
    	return 0;
    }
    

    结果展示:
    在这里插入图片描述
     对比之前的进程地址空间,我们能发现,这些地址全部都是在栈的上面,这也完美印证了命令行参数和环境变量在我们栈上面!
    总结
     现在对于进程的理解我们又提升了一个台阶:一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建!

    展开全文
  • 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将这个内存区映射到本进程的虚拟地址空间。


    展开全文
  • 这种思想理解起来并不难,操作系统保证不同进程的地址空间映射到物理地址空间中不同的区域上,这样每个进程最终访问到的。 物理地址空间都是彼此分开的。通过这种方式,就实现了进程间的地址隔离。还是以实例...

    00. 目录

    01. 早期的内存分配机制

    在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。

    那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存 的呢?下面通过实例来说明当时的内存分配方法:

    某台计算机总的内存大小是 128M ,现在同时运行两个程序 A 和 B , A 需占用内存 10M , B 需占用内存 110 。计算机在给程序分配内存时会采取这样的方法:先将内存中的前 10M 分配给程序 A ,接着再从内存中剩余的 118M 中划分出 110M 分配给程序 B 。这种分配方法可以保证程序 A 和程序 B 都能运行,但是这种简单的内存分配策略问题很多。
    在这里插入图片描述

    早期的内存分配方法

    问题 1 :进程地址空间不隔离。由于程序都是直接访问物理内存,所以恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。有些非恶意的,但是有 bug 的程序也可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。这种情况对用户来说是无法容忍的,因为用户希望使用计算机的时候,其中一个任务失败了,至少不能影响其它的任务。

    问题 2 :内存使用效率低。在 A 和 B 都运行的情况下,如果用户又运行了程序 C,而程序 C 需要 20M 大小的内存才能运行,而此时系统只剩下 8M 的空间可供使用,所以此时系统必须在已运行的程序中选择一个将该程序的数据暂时拷贝到硬盘上,释放出部分空间来供程序 C 使用,然后再将程序 C 的数据全部装入内存中运行。可以想象得到,在这个过程中,有大量的数据在装入装出,导致效率十分低下。

    问题 3 :程序运行的地址不确定。当内存中的剩余空间可以满足程序 C 的要求后,操作系统会在剩余空间中随机分配一段连续的 20M 大小的空间给程序 C 使用,因为是随机分配的,所以程序运行的地址是不确定的。

    02. 分段

    为 了解决上述问题,人们想到了一种变通的方法,就是增加一个中间层,利用一种间接的地址访问方法访问物理内存。按照这种方法,程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,就可以达到内存地址空间隔离的效果。

    当创建一个进程时,操作系统会为该进程分配一个 4GB 大小的虚拟进程地址空间。之所以是 4GB ,是因为在 32 位的操作系统中,一个指针长度是 4 字节,而 4 字节指针的寻址能力是从 0x00000000~0xFFFFFFFF,最大值 0xFFFFFFFF 表示的即为 4GB 大小的容量。与虚拟地址空间相对的,还有一个物理地址空间,这个地址空间对应的是真实的物理内存。如果你的计算机上安装了 512M 大小的内存,那么这个物理地址空间表示的范围是 0x00000000~0x1FFFFFFF 。当操作系统做虚拟地址到物理地址映射时,只能映射到这一范围,操作系统也只会映射到这一范围。当进程创建时,每个进程都会有一个自己的 4GB 虚拟地址空间。要注意的是这个 4GB 的地址空间是“虚拟”的,并不是真实存在的,而且每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数据,通过这种方法实现了进程间的地址隔离。那是不是这 4GB 的虚拟地址空间应用程序可以随意使用呢?很遗憾,在 Windows 系统下,这个虚拟地址空间被分成了 4 部分: NULL 指针区、用户区、 64KB 禁入区、内核区。

    1)NULL指针区 (0x00000000~0x0000FFFF): 如果进程中的一个线程试图操作这个分区中的数据,CPU就会引发非法访问。他的作用是,调用 malloc 等内存分配函数时,如果无法找到足够的内存空间,它将返回 NULL。而不进行安全性检查。它只是假设地址分配成功,并开始访问内存地址 0x00000000(NULL)。由于禁止访问内存的这个分区,因此会发生非法访问现象,并终止这个进程的运行。

    2)用户模式分区 ( 0x00010000~0xBFFEFFFF):这个分区中存放进程的私有地址空间。一个进程无法以任何方式访问另外一个进程驻留在这个分区中的数据 (相同 exe,通过 copy-on-write 来完成地址隔离)。(在windows中,所有 .exe 和动态链接库都载入到这一区域。系统同时会把该进程可以访问的所有内存映射文件映射到这一分区)。

    2)隔离区 (0xBFFF0000~0xBFFFFFFF):这个分区禁止进入。任何试图访问这个内存分区的操作都是违规的。微软保留这块分区的目的是为了简化操作系统的现实。

    3)内核区 (0xC0000000~0xFFFFFFFF):这个分区存放操作系统驻留的代码。线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序代码都在这个分区加载。这个分区被所有进程共享。

    应用程序能使用的只是用户区而已,大约 2GB 左右 ( 最大可以调整到 3GB) 。内核区为 2GB ,内核区保存的是系统线程调度、内存管理、设备驱动等数据,这部分数据供所有的进程共享,但应用程序是不能直接访问的。

    人们之所以要创建一个虚拟地址空间,目的是为了解决进程地址空间隔离的问题。但程序要想执行,必须运行在真实的内存上,所以,必须在虚拟地址与物理地址间建立一种映射关系。这样,通过映射机制,当程序访问虚拟地址空间上的某个地址值时,就相当于访问了物理地址空间中的另一个值。人们想到了一种分段(Sagmentation) 的方法,它的思想是在虚拟地址空间和物理地址空间之间做一一映射。比如说虚拟地址空间中某个 10M 大小的空间映射到物理地址空间中某个 10M 大小的空间。这种思想理解起来并不难,操作系统保证不同进程的地址空间被映射到物理地址空间中不同的区域上,这样每个进程最终访问到的。

    物理地址空间都是彼此分开的。通过这种方式,就实现了进程间的地址隔离。还是以实例说明,假设有两个进程 A 和 B ,进程 A 所需内存大小为 10M ,其虚拟地址空间分布在 0x00000000 到 0x00A00000 ,进程 B 所需内存为 100M ,其虚拟地址空间分布为 0x00000000 到 0x06400000 。那么按照分段的映射方法,进程 A 在物理内存上映射区域为 0x00100000 到 0x00B00000 ,,进程 B 在物理内存上映射区域为0x00C00000 到 0x07000000 。于是进程 A 和进程 B 分别被映射到了不同的内存区间,彼此互不重叠,实现了地址隔离。从应用程序的角度看来,进程 A 的地址空间就是分布在 0x00000000 到 0x00A00000 ,在做开发时,开发人员只需访问这段区间上的地址即可。应用程序并不关心进程 A 究竟被映射到物理内存的那块区域上了,所以程序的运行地址也就是相当于说是确定的了。 下图显示的是分段方式的内存映射方法:

    在这里插入图片描述

    分段方式的内存映射方法

    这种分段的映射方法虽然解决了上述中的问题一和问题三,但并没能解决问题二,即内存的使用效率问题。在分段的映射方法中,每次换入换出内存的都是整个程序, 这样会造成大量的磁盘访问操作,导致效率低下。所以这种映射方法还是稍显粗糙,粒度比较大。实际上,程序的运行有局部性特点,在某个时间段内,程序只是访问程序的一小部分数据,也就是说,程序的大部分数据在一个时间段内都不会被用到。基于这种情况,人们想到了粒度更小的内存分割和映射方法,这种方法就是分页 (Paging)

    03. 分页

    分页的基本方法是,将地址空间分成许多的页。每页的大小由 CPU 决定,然后由操作系统选择页的大小。目前 Inter 系列的 CPU 支持 4KB 或 4MB 的页大小,而 PC上目前都选择使用 4KB 。按这种选择, 4GB 虚拟地址空间共可以分成 1048576 页, 512M 的物理内存可以分为 131072 个页。显然虚拟空间的页数要比物理空间的页数多得多。

    在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。

    下面通过介绍一个可执行文件的装载过程来说明分页机制的实现方法。一个可执行文件 (PE 文件 ) 其实就是一些编译链接好的数据和指令的集合,它也会被分成很多页,在 PE 文件执行的过程中,它往内存中装载的单位就是页。当一个 PE 文件被执行时,操作系统会先为该程序创建一个 4GB 的进程虚拟地址空间。前面介绍过,虚拟地址空间只是一个中间层而已,它的功能是利用一种映射机制将虚拟地址空间映射到物理地址空间,所以,创建 4GB 虚拟地址空间其实并不是要真的创建空间,只是要创建那种映射机制所需要的数据结构而已,这种数据结构就是页目和页表。

    当创建完虚拟地址空间所需要的数据结构后,进程开始读取 PE 文件的第一页。在PE 文件的第一页包含了 PE 文件头和段表等信息,进程根据文件头和段表等信息,将 PE 文件中所有的段一一映射到虚拟地址空间中相应的页 (PE 文件中的段的长度都是页长的整数倍 ) 。这时 PE 文件的真正指令和数据还没有被装入内存中,操作系统只是据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系而已。当 CPU 要访问程序中用到的某个虚拟地址时,当 CPU 发现该地址并没有相相关联的物理地址时, CPU 认为该虚拟地址所在的页面是个空页面, CPU 会认为这是个页错误 (Page Fault) , CPU 也就知道了操作系统还未给该 PE 页面分配内存,CPU 会将控制权交还给操作系统。操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。

    分页方法的核心思想就是当可执行文件执行到第 x 页时,就为第 x 页分配一个内存页 y ,然后再将这个内存页添加到进程虚拟地址空间的映射表中 , 这个映射表就相当于一个 y=f(x) 函数。应用程序通过这个映射表就可以访问到 x 页关联的 y 页了。

    04. 地址比较

    逻辑地址、线性地址、物理地址和虚拟地址的区别

    逻辑地址(Logical Address) 是指由程式产生的和段相关的偏移地址部分。例如,你在进行 C 语言指针编程中,能读取指针变量本身值( &操作 ),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在 Intel 实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。应用程式员仅需和逻辑地址打交道,而分段和分页机制对你来说是完全透明的,仅由系统编程人员涉及。应用程式员虽然自己能直接操作内存,那也只能在操作系统给你分配的内存段操作。

    线性地址(Linear Address) 是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386 的线性地址空间容量为 4G(2的32次方即32根地址总线寻址)。

    物理地址(Physical Address) 是指出目前 CPU 外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

    虚拟内存(Virtual Memory) 是指计算机呈现出要比实际拥有的内存大得多的内存量。因此他允许程式员编制并运行比实际系统拥有的内存大得多的程式。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个非常恰当的比喻是:你不必非常长的轨道就能让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就能完成这个任务。采取的方法是把后面的铁轨即时铺到火车的前面,只要你的操作足够快并能满足需求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的任务。在 Linux0.11 内核中,给每个程式(进程)都划分了总容量为 64MB 的虚拟内存空间。因此程式的逻辑地址范围是 0x0000000 到 0x4000000。有时我们也把逻辑地址称为 虚拟地址。因为和虚拟内存空间的概念类似,逻辑地址也是和实际物理内存容量无关的。逻辑地址和物理地址的“差距”是 0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。机理逻辑地址(或称为虚拟地址)到线性地址是由CPU的段机制自动转换的。如果没有开启分页管理,则线性地址就是物理地址。如果开启了分页管理,那么系统程式需要参和线性地址到物理地址的转换过程。具体是通过设置页目录表和页表项进行的。

    05. 附录

    5.1 博客:进程地址空间与虚拟存储空间的理解

    展开全文
  • ⭐️这篇博客就要和大家介绍进程地址空间相关内容,学完这个部分,我们会对进程的地址空间有一个全选的了解 目录程序地址空间进程地址空间 程序地址空间 先看厦门下面一张图,在之前C/C+博客的内存管理中放过这...
  • Linux下进程地址空间(初学者必备)

    千次阅读 多人点赞 2022-04-03 15:38:18
    进程地址空间 一.程序地址空间 首先我们先通过一张图回顾一下c/c++中的程序地址空间: 下面简单的介绍一个这几个区域: 1.堆区: 堆数据区即heap区,在C程序中,该区域的分配和回收由malloc和free进行。...
  • Linux进程地址空间进程的内存分布

    万次阅读 多人点赞 2018-05-15 20:13:18
    原网址为:https://blog.csdn.net/yusiguyuan/article/details/45155035一 进程空间分布概述 对于一个进程,其空间分布如下图所示: 程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。初始化过的...
  • 看到内核详解上说不管什么进程一旦进入内核就进入了系统空间都有相同的页面映射,内核就不用cr3吗?实在搞不明白,希望大虾详细讲解一下 | 内核当然会用cr3,在每个进程的页目录项中,都把最后3-4G的页目录项映射...
  • 这篇博文可以在你基本了解逻辑地址空间和物理地址空间的概念后,为增强理解可通过我画的示意图来理解,本文会深入一些概念,以达到全面掌握该映射关系的目的。画图不易鸭,点个赞再走呗(✿◡‿◡) 逻辑地址空间及...
  • Linux用户空间与内核空间内存映射

    千次阅读 2017-05-25 15:42:06
    Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户...进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存
  • 进程地址空间(虚拟地址 | 物理内存)

    千次阅读 多人点赞 2021-03-17 20:45:37
    文章目录前言一、简单理解地址空间二、虚拟地址现象解释三、三个问题搞懂地址空间1. 什么是地址空间?2. 为什么要有地址空间?3. 地址空间是如何工作的?四、一些补充 前言 在之前的学习中,我们只学习了图中的下...
  • 进程地址空间浅谈

    千次阅读 2022-03-16 23:31:35
    在谈对地址空间的理解之前先看这样一个问题: 首先我们在vim中创建这样一组代码。 主函数上定义全局变量num,并赋值为1。 之后在主函数内由 fork() 函数创建出子进程。 在父子进程中各自轮换打印出num值和num...
  • 进程空间内存分配详解

    千次阅读 2019-08-20 11:50:00
    进程空间的内存分配 从高地址到低地址如图所示: 名称 操作系统内核区 用户不可见 用户栈 栈指针,向下扩展 动态堆 向上扩展 全局区(静态区) .data初始化.bss未初始化 文字常量区(只读数据) ...
  • 进程地址空间的分布,虚拟内存和物理内存的映射关系,页表
  • linux进程用户内存空间和内核空间

    千次阅读 2020-06-15 20:02:52
    每个进程的页面目录就分成了两部分,第一部分为“用户空间”,用来映射其整个进程空间(0x0000 0000-0xBFFF FFFF)即3G字节的虚拟地址;第二部分为“系统空间”,用来映射(0xC000 0000-0xFFFF FFFF)1G字节的...
  • 内核空间和用户空间  现代操作...为了保证用户进程不能直接操作内核,保证内核安全,操作系统将虚拟空间划分为两部分,一部分是内核空间,一部分是用户空间。针对Linux操作系统,将最高的1G字节(从虚拟地址0xC0...
  • vb端先新建内存映射文件,再调用C++对图片进行处理,处理后的结果写入vb建好的内存映射文件,然后vb端再读取内存映射文件。这样处理就可以不用在磁盘上进行读写操作了,节约了IO资源。 内存映射文件包含虚拟内存中...
  • mm_struct这个结构体描述出了虚拟地址空间,页表记录了虚拟地址空间与物理地址空间之间的转换关系。其中*mm指向内存区描述符的指针,*mm结构体中的pgd指向页全局目录,*mmap指向线性区对象的链表头,通过页表进行...
  • 用内存映射文件实现进程间通信

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

    万次阅读 2018-11-05 11:38:37
    DLL和进程的地址空间一,MT和MD的区别二,显示链接与隐式链接三,DLL和进程的地址空间 DLL是Windows开发人员经常使用到的一种技术,比如我们经常会把相同功能的代码封装到一个模块中,然后供其他需要使用该模块的...
  • 进程基本概念、进程地址空间

    千次阅读 2018-03-31 17:54:14
    强调内容今天来谈一谈进程的一些基本概念,认识一些进程状态,重新认识一下程序地址空间进程地址空间),进程调度算法,环境变量等属性。 一、进程 1.什么是进程? 程序的一个执行实例,正在执行的程序,是一个...
  • mmap() 是一个系统调用函数,本质是一种进程虚拟内存的映射方法,可以将一个文件、一段物理内存或者其它对象映射进程的虚拟内存地址空间。实现这样的映射关系后,进程就可以采用指针的方式来读写操作这一段内存,...
  • 文章目录子进程与父进程信号管道消息队列共享内存 子进程与父进程进程继承父进程的 用户号UIDs和用户组号GIDs 环境Environment 堆栈 共享内存 打开文件的描述符 执行时关闭(Close-on-exec)标志 信号(Signal...
  • 虚拟地址空间映射到物理地址空间

    千次阅读 2018-09-07 09:06:45
    虚拟地址空间映射到物理地址空间参考如下  当处理器读或写入内存位置时,它会使用虚拟地址。作为读或写操作的一部分,处理器将虚拟地址转换为物理地址。通过虚拟地址访问内存有以下优势:  程序可以使用一系列...
  • QT 进程间通信——文件映射

    千次阅读 2019-04-25 16:47:46
    在我们处理较大文件的时候,通常需要使用文件映射,即将物理地址中的文件数据映射进程的虚拟地址中。通过文件映射之后,可以像操作内存一样去直接操作文件,而不需要再调用文件读写方法了。内存映射文件可以用于这...
  • 【Linux】进程的地址空间

    千次阅读 2020-05-07 21:57:19
    进程的虚拟地址空间进程虚拟地址空间的含义:进程的虚拟地址空间的作用:进程之间为什么能通过虚拟地址空间空间实现数据独有?三种内存管理方式 再讲进程的虚拟地址空间之前,我们先来了解一下程序的地址空间是什么...
  • Binder是安卓中实现IPC(进程间通信的)常用手段,四大组件之间的跨进程通信也是利用Binder实现的,Binder是学习四大组件工作原理的的一个重要基础,是Android提供的一套进程间相互通信框架。用来多进程间发送消息,...
  • 正如其名(Memory Map),mmap 可以将某个设备或者文件映射到应用进程的内存空间中。通过直接的内存操作即可完成对设备或文件的读写。. 通过映射同一块物理内存,来实现共享内存,完成进程间的通信。由于减少了数据...
  • 浅谈进程地址空间与虚拟存储空间

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

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 224,598
精华内容 89,839
关键字:

关于进程映射空间