2469|0

5979

帖子

8

TA的资源

版主

楼主
 

【转载】Linux-地址关系 [复制链接]

Linux-虚拟地址、物理地址、IO内存、IO端口、映射、内核空间、用户空间、IO空间、内存空间关系

转载请注明出处:http://blog.csdn.net/guolele2010

作者:guolele

这题目够长的,也是笔者想一次性弄好,现在开始吧!

讲基础知识之前,我们先了解一下一个编译的二进制程序的内容,这里引用《解惑—Linux中的地址空间》例如:假定我们有一个简单的C程序Hello.c
[cpp] view plaincopyprint?
  • # include
  • greeting ( )
  • {
  •             printf(“Hello,world!/n”);
  • }
  • main()
  • {
  •        greeting();
  • }

之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用gccld对其进行编译和连接,得到可执行代码hello。然后,用Linux的实用程序objdump对其进行反汇编:
$objdump –d hello
得到的主要片段为:
08048568 :
   8048568:     pushl  %ebp

   8048569:     movl  %esp, %ebp
   804856b:     pushl  $0x809404
   8048570:     call    8048474  <_init+0x84>
   8048575:     addl   $0x4, %esp
   8048578:     leave
   8048579:     ret
   804857a:     movl  %esi, %esi
   0804857c
:
   804857c:     pushl  %ebp
   804857d:     movl  %esp, %ebp
   804857f:     call    8048568  
   8048584:     leave
   8048585:     ret
   8048586:     nop
   8048587:     nop

08048568
这里的这个地址是虚拟地址,但大家都知道,真正的代码啊什么,是放在外设物理设备上,物理设备就有个叫物理地址。这是个应用程序,但如果是内核模块呢?那又涉及到内核地址,别蒙!慢慢来。
虚拟地址,物理地址,内核空间,用户空间,内核逻辑地址

在32位地址总线的CPU里,linux支持的虚拟内存大小为2^32=4G,其中又将这4G分为两部分。最高的1G字(从虚拟地址0xc0000000~0xffffffff)是内核的虚拟地址空间,称为“内核空间”,剩下的3G(0x0~0xbfffffff)为“用户空间”。因为用户空间又可以通过系统调用来进入内核空间,因此,内核空间是所有用户空间进程共享的,所以,从进程角度看,每个进程拥有4G的虚拟地址空间(也叫虚拟内存)。

    每个进程都拥有3G的虚拟空间,这个空间又对其它进程是不可见的,只有内核空间是共享的,进程的“用户空间”也叫“地址空间”。进程是隔离的,每个进程访问同一个地址(这里指虚拟地址),得到的数值可以是不一样的,如进程1在0x1读到1,进程2可以在0x1读到2,这就取决于它对应的数据存储的物理地址不一样。物理地址就是实实在在,地址总线直接取得的。

有一点特别注意,CPU只用虚拟地址,然后虚拟地址经过硬件MMU直接解映射为物理地址再送到总线上,然后再操作外设。

    CPU任一时刻只能有一个进程占用,那么对CPU来说,整个系统就只有一个4GB的虚拟地址空间,那切换进程时,虚拟空间也要跟着切换,因为每个进程都是进程隔离的,所以其中的3G就要地址切换,那么是如何切换的呢?答案在于段式管理与页式管理,其中linux是采用页式管理的。物理地址通过页式和段式管理(linux只有页式,后面只说页式),用户空间把物理地址转变成虚拟地址,内核空间是简单的线性映射关系(没涉及到页式管理)。而把虚拟地址链接到物理地址这个操作又叫做映射。进程具体操作就是CPU只需要切换页表就可以实现进程虚拟地址映射逻辑的变化了。

内核空间与物理内存的转换

    内核空间与物理内存的转换也是内核虚拟地址与物理地址的转换。内核空间对所有的进程都是共享的,其中存放的是内核代码和数据、堆栈、BSS等,而用户空间的就是用户空间的代码、数据、堆栈、BSS等。所有程序无论是在内核还是用户空间的,最终转换成的地址都是虚拟地址。虽然内核空间占据了虚拟空间中的最高1G字节,但映射到物理内存中却是从最低地址(0x0)开始,因为内核空间与物理内存之间建立的映射关系是简单的线性映射,就是移了一个位移量。其中3GB(0xc0000000)就是物理地址和虚拟地址之间的位移量,在linux里叫做PAGE_OFFSET。

#define __PAGE_OFFSET           (0xC0000000)
……
#define PAGE_OFFSET             ((unsigned long)__PAGE_OFFSET)
#define __pa(x)                 ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))
对于内核空间而言,给定一个虚地址x,其物理地址为“x- PAGE_OFFSET”,给定一个物理地址x,其虚地址为“x+ PAGE_OFFSET”。

这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。


这图描述的是内核空间的分布(更具体看下面内核memory.txt里的,这里的有点不太对),其中

PAGE_OFFSET是上面说的线性映射的位移量

High_memory是说高端内存,通常,我们把物理地址(物理内存)超过896M的区域称为高端内存。也就是说,MEM>896M,high_memory = 0XC0000000 + MEM – 896M,其中mem为所有的物理地址空间大小。一般少于896M就是0xc0000000 + mem。

在源代码(2.6.26)中函数mem_init中,

max_mapnr   = virt_to_page(high_memory) - mem_map;

先获得物理地址再映射虚拟地址,虚拟地址再到页

#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)

虚拟地址转换到物理地址,只要线性偏移一下就可以,其中PYHYS_OFFSET是物理内存的偏移,即比0x0地址多多少,简单的线性映射

#define __virt_to_phys(x)   ((x) - PAGE_OFFSET + PHYS_OFFSET)

就是物理地址映射到虚拟地址

__pa(kaddr) >> PAGE_SHIFT

下面这个函数的决定要看内核配置.config,这里是CONFIG_FLATMEM,所以调用这个,得到虚拟地址的页表

#define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET))

所以在图中,PAGE_OFFSET到high_memory 之间就是所谓的物理内存映射。只有这一段之间,物理地址与虚地址之间是简单的线性关系。

  还要说明的是,要在这段内存分配内存,则调用kmalloc()函数。反过来说,通过kmalloc()分配的内存,其物理页是连续的。

下面是内核关于memory安排的描述

Start      End    Use

--------------------------------------------------------------------------

ffff8000   ffffffff   copy_user_page / clear_user_page use.

                     For SA11xx and Xscale, this is used to

                     setup a minicache mapping.

ffff1000   ffff7fff   Reserved.

                     Platforms must not use this address range.

ffff0000   ffff0fff   CPU vector page.

                     The CPU vectors are mapped here if the

                     CPU supports vector relocation (control

                     register V bit.)

ffc00000   fffeffff   DMA memory mapping region.  Memory returned

                     by the dma_alloc_xxx functions will be

                     dynamically mapped here.DMA内存映射区域

ff000000   ffbfffff   Reserved for future expansion of DMA

                     mapping region.

VMALLOC_END   feffffff   Free for platform use, recommended.

                         VMALLOC_END must be aligned to a 2MB

                         boundary.

VMALLOC_START VMALLOC_END-1 vmalloc() / ioremap() space.

                         Memory returned by vmalloc/ioremap will

                         be dynamically placed in this region.

                         VMALLOC_START may be based upon the value

                         of the high_memory variable.

PAGE_OFFSET   high_memory-1 Kernel direct-mapped RAM region.

                                   内核直接映射内存区域

                            This maps the platforms RAM, and typically

                            maps all platform RAM in a 1:1 relationship.

TASK_SIZE  PAGE_OFFSET-1 Kernel module space内核模块空间

                         Kernel modules inserted via insmod are

                         placed here using dynamic mappings.

00001000   TASK_SIZE-1   User space mappings用户空间

                         Per-thread mappings are placed here via

                         the mmap() system call.

00000000   00000fff   CPU vector page / null pointer trap

                     CPUs which do not support vector remapping

                     place their vector page here.  NULL pointer

                     dereferences by both the kernel and user

                     space are also caught via this mapping.

非连续区的分配调用VMalloc()函数。

vmalloc()与 kmalloc()都是在内核代码中用来分配内存的函数,但二者有何区别?

   从前面的介绍已经看出,这两个函数所分配的内存都处于内核空间,即从3GB~4GB;但位置不同,kmalloc()分配的内存处于3GB~high_memory之间,这一段内核空间与物理内存的映射一一对应,而vmalloc()分配的内存在VMALLOC_START~4GB之间,这一段非连续内存区映射到物理内存也可能是非连续的。

   vmalloc()工作方式与kmalloc()类似, 其主要差别在于前者分配的物理地址无需连续,而后者确保页在物理上是连续的(虚地址自然也是连续的)。

尽管仅仅在某些情况下才需要物理上连续的内存块,但是,很多内核代码都调用kmalloc(),而不是用vmalloc()获得内存。这主要是出于性能的考虑。vmalloc()函数为了把物理上不连续的页面转换为虚拟地址空间上连续的页,必须专门建立页表项。还有,通过vmalloc()获得的页必须一个一个的进行映射(因为它们物理上不是连续的),这就会导致比直接内存映射大得多的缓冲区刷新。因为这些原因,vmalloc()仅在绝对必要时才会使用——典型的就是为了获得大块内存时。

上面在讲linux虚拟地址分布时,扯到一个VMALLOC_START VMALLOC_END-1这个区域,用于vmalloc和ioremap,对于ioremap,又要引入几个概念。

IO内存、IO端口、IO空间



看上面两图应该能大概明白,为什么有两个,第一个图目的是告诉你,外设上的寄存器或者内存连接在io空间或者内存空间是由硬件决定的,第二个图是告诉你并不只是寄存器能当端口,内存也可以是IO端口。

IO端口:当一寄存器或内存位于IO空间时,称为IO端口

IO内存:当一寄存器或内存位于内存空间时,称为IO内存

独立编址和统一编址

讲到IO端口和IO内存,其实跟独立编址和统一编址一样,因为有些CPU是IO空间与内存空间都有,如果X86,那它把Io端口独立开来,所以就叫独立编址,而一些设备如果ARM,只有内存空间,所以只能将所以都统编址。

讲完这些,就要讲IO端口IO内存与物理地址、虚拟地址之间的关系(linux)。

首先在linux中,IO端口与IO内存统一为IO资源,在linux里有两个文件显示IO端口跟IO内存,分别是/proc/ioport与/proc/iomem,这两个其实对于实际的硬件驱动作用不大,它只是在用户空间表现出来。下面分别介绍一下。

IO端口:

操作步骤:1、申请  2、访问  3、释放

1、申请

Request_region()这其实是在加入内核描述io端口的链表中,将在用的设备显示在/proc/ioport里

2、访问

inb和outb:
在Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
·  读写字节端口(8位宽)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
·  读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
·  读写长字端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
·  读写一串字节
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
·  insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存的count个字节连续地写入port开始的端口。
·  读写一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
·  读写一串长字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned。

3、释放

release_region()

check_region()检查



IO内存:

1、申请

申请用request_mem_region()显示在/proc/iomem里,这步只是告诉别人,我已经用了这段内存,你别用了。不是必要的。

2、映射

Ioremap()

非常重要,因为CPU操作的是虚拟地址,如果你直接给物理地址它,它就发疯了,所以你应该先将物理地址映射成虚拟地址,这步得到的地址就是可以操作的地址。

3、访问

读I/O内存
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
·  写I/O内存
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持):
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

4、释放

Release_mem_region()

把I/O端口映射到“内存空间”:
void *ioport_map(unsigned long port, unsigned int count);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。当不再需要这种映射时,需要调用下面的函数来撤消:
void ioport_unmap(void *addr);
实际上,分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口访问I/O端口。


点赞 关注(1)
个人签名生活就是油盐酱醋再加一点糖,快活就是一天到晚乐呵呵的忙
===================================
做一个简单的人,踏实而务实,不沉溺幻想,不庸人自扰

回复
举报
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/7 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表