アドレス空間とI / Oアドレス空間でのLinux

現在最も広くこのより技術文書を議論する80X86アーキテクチャの使用CPUアーキテクチャとの素晴らしい関係を、達成するためのアドレス空間。これに基づいてもあります。

メモリアドレスの「Linuxカーネルの深い理解」によると、以下の3つに分割されています。

論理アドレス(論理アドレス) 

       これは、機械語命令または命令のオペランドを指定するために使用されるアドレスが含まれています。セグメント化された建設知られているこのアドレッシング80×86は、それがセグメントに窓をプログラムするプログラマを奨励し、特に具体的でした。各論理アドレスはセグメントで構成され、組成物をオフセット、実際のアドレスとの間の距離セグメントの開始からのオフセットを示しています。

(また、仮想アドレスの仮想アドレスとして知られている)リニアアドレス(リニアアドレス)

       32ビットの符号なし整数である、アドレスの4ギガバイト(2に対処するため、すなわち、32番目のアドレスバス32)まで表現するために使用することができます。線形アドレスは、通常、16進数で表され、値は0x00000000のから0xFFFFFFFFの範囲です。

物理アドレス(物理アドレス)

       クラスメモリは、メモリチップに対処するための手段。彼らは、ピンは、マイクロプロセッサに対応するアドレスバスからメモリへ電気信号を送信押します。32ビットの物理アドレスまたは36ビットの符号なし整数で表されます。

これらの3つのアドレス間の変換:

論理アドレス - >(セグメント) - >リニアアドレス - >(タブ) - >物理アドレス

セグメントを実現します:

図1に示すように、セグメント・セレクタ現在のセグメントを知って、T1 = 0又は1を参照して、各レジスタによると、セクションのGDTまたはLDTに変換すると、そのアドレスとサイズを得ることです。私たちは、の配列を持っています。

それはベースアドレスを知っているベース、すなわちだように、図2に示すように、前部セグメント・セレクタのうち13がアレイであってもよく、対応するセグメント記述子が、発見されました。

図3に示すように、ベース+オフセットは、線形アドレスを変換することです。

2.6のLinuxバージョンでのみ80×86の構成のセグメンテーションを使用する必要があることに留意すべきです。すべてのプロセスのグローバルGDT、独自のプロセスと各LDT--が、LinuxとIntelの意向によると、命令とデータのために対処するために、同じセグメントを使用します。すなわち、ユーザデータセグメント、ユーザコードセグメント、カーネルに対応するカーネル・コード・セグメントのカーネルデータセクション。論理アドレスは、(内側部分のセグメント識別子+オフセット)線形アドレス(リニアアドレスフィールドベース+ =記述子オフセット内側部分)セグメントが、セグメント識別子0で構成されているように、特別なソフトウェアによって実現のLinuxベース記述子フィールド0であるので、アドレスは、論理アドレス=線形、すなわち、Linuxで論理と線形アドレスは同じです。

ページングの実装:

まず、いくつかの基本的な概念を理解します:

ページ:リニアアドレスは、ページと呼ばれる固定長の単位にグループ分けされています。

フレーム:すべてのRAMメモリ領域を固定長に分割され、また、物理的なページと呼ばれます。各フレームは、ページとページフレームの長さと一致しているページが含まれています。

ページフレームがメモリ領域であり、ページはデータのブロックであり、ページは、任意のブロックと、ディスクに格納することができます。

ページテーブル:データ構造の線形アドレスを物理アドレスにマッピングします。

共通するのは、32ビットと64ビットの両方のシステム用のLinuxのページングモデルを使用しています。システムは、2つのタブ32を用いるシステム64は、複数のページング・レベルを必要とする、十分です。バージョン2.6.10までは、Linuxはバージョン2.6.11から、3つのページングモデルを使用して、4つのページング・モデルを使用し始めました。

図示のように:

PGD​​ページグローバルディレクトリ

親ディレクトリページのPUD

中級ページディレクトリPMD

ページテーブルPT

32ビットシステムの場合、両者は十分であろう。Linuxと親ディレクトリのページディレクトリページ中央のビット0に達成します。

切り替え処理が発生した場合、Linuxは、以前の記述子に格納された処理内容を実行する制御レジスタ(グローバルディレクトリ格納されたページのアドレス)CR3、その後、次のレジスタCR3にロードプロセス記述子の値を実行します。だから、新しいプロセスがページテーブルの正しいセットに単位ポイントをページング、CPU上で実行を再開したとき。

疑問のアドレス空間(A)-Linux

一連の質問は、あなたを悩ませているかどうか、これがあります。

1.ユーザプログラムがどの程度にアドレス空間を形成するように接続されてコンパイルされましたか?

2.どの程度にカーネルアドレス空間をコンパイル?

周辺、I / Oアドレス空間にアクセスするには3は何ですか?
まず最初の質問に答えます。Linuxは、最も一般的な実行可能ファイル形式のELF(実行可能ファイルとリンク可能フォーマット)です。実行可能コードのELF形式では、常にこのような各プログラムの0x8000000スケジューラ「コードセグメント」から開始LD。プログラムの実行、メモリマップされ、そのコアによって特定のアドレスに応じて、割り当てられた物理メモリ・ページを作成する仮割当て時のメモリの実際の物理アドレスとして。
私たちは、そのアドレス範囲のように認識して、あなたのプログラムを分解するために、Linuxのobjdumpのためのユーティリティを使用することができます。
例えば:私たちは単純なCプログラムhello.cがあるとし

 
  1. # include <stdio.h>

  2. greeting ( )

  3. {

  4. printf(“Hello,world!\n”);

  5. }

  6. main()

  7. {

  8. greeting();

  9. }

このような単純なプログラムは2つの機能を書いた理由は、転送処理の指示を説明するために。私たちは、gccとldはコンパイルされ、ハロー実行可能コードを取得するためにリンクされます。次に、その有用性をobjdumpのための使用のLinuxを分解:
$ objdumpのこんにちは-d
主なセグメントは以下の通りであったが。

 
  1. 08048568 <greeting>:

  2. 8048568: pushl %ebp

  3. 8048569: movl %esp, %ebp

  4. 804856b: pushl $0x809404

  5. 8048570: call 8048474 <_init+0x84>

  6. 8048575: addl $0x4, %esp

  7. 8048578: leave

  8. 8048579: ret

  9. 804857a: movl %esi, %esi

  10. 0804857c <main>:

  11. 804857c: pushl %ebp

  12. 804857d: movl %esp, %ebp

  13. 804857f: call 8048568 <greeting>

  14. 8048584: leave

  15. 8048585: ret

  16. 8048586: nop

  17. 8048587: nop

これは、このアドレス08048568のように、私たちは多くの場合、仮想アドレスは、(アドレスが存在しないが、物理アドレスがあるので、ベール「仮想」のように見える)ことを言います。

 

仮想メモリ、カーネル空間とユーザ空間

   Linux仮想メモリサイズは2 ^ 32(のx86マシン32上で)、これらのコア4Gバイト空間は、2つの部分に分割されます。1Gカーネル用(仮想アドレス0xC0000000から0xFFFFFFFFのに)最高のバイトは、と呼ばれる「カーネル空間。」3G(仮想アドレス0x00000000の0xBFFFFFFFからの)下位バイト一方は、各プロセスに使用される、と呼ばれる「ユーザ空間」。各プロセスは、システムコールを通じてカーネルに入ることができるので、それゆえ、Linuxカーネル空間は、システム内のすべてのプロセスで共有します。したがって、特定のプロセスの観点から、各プロセスは、(仮想メモリとしても知られる)の仮想アドレス空間の4Gバイトを有することができます。
   
    各プロセスは、独自のプライベートユーザ空間(0〜3G)は、システム内の他のプロセスのために、このスペースは表示されません。最高1GBのカーネル空間は、すべてのプロセスとカーネルによって共有されていました。また、また、物語の裏にある「アドレス空間」と呼ばれる「ユーザ空間」のプロセスは、私たちはもはや、これら二つの用語を区別しません。


    ユーザースペースは、共有のプロセスが、分離のプロセスではありません。各プロセスは、3ギガバイトの最大ユーザー空間を持つことができます。アドレスのいずれかへのプロセスへのアクセスは、同じアドレスにアクセスするために他のプロセスと競合することはありません。例えば、ユーザのアドレス空間からプロセス0x1234ABCD 8の整数を読み、プロセス空間のアドレスから他のことができるが、独自のロジックの処理に応じて、ユーザが20の整数を読み取ることができる0x1234ABCD。
    どれでも1時間、CPU上で実行されている唯一のプロセス。このCPUのために懸念しているので、この時点で、システム全体が唯一の4ギガバイトの仮想アドレス空間、プロセスの仮想アドレス空間があります。プロセスを切り替えるときは、スイッチと仮想アドレス空間を発生します。その仮想アドレス空間は、それが唯一のCPUに知られていた実行するときに、このプロセスは、実行され、各プロセスが独自の仮想アドレス空間を持つ、見ることができます。またある時には、CPUのための仮想アドレス空間は、不可知です。だから、それぞれのプロセスは、仮想アドレス空間の4ギガバイトを持つことができますが、しかし、CPUの目には、一つだけの仮想アドレス空間があります。スイッチングプロセスの変化と仮想アドレス空間の変更、。
     以上のことから、我々はプログラムの後に形成されたアドレス空間は、仮想アドレス空間を接続しているコンパイルされていることを知っているが、最終的にあなたは、物理メモリ内のプログラムを実行したいです。したがって、任意の所与のアプリケーションの仮想アドレスは、最終的に仮想アドレス空間は、物理メモリ空間にマッピングされなければならない、物理アドレスに変換する必要があり、マッピング関係は、所定のデータ構造のハードウェアアーキテクチャによって確立される必要があります。これは、私たちが主にページ・テーブルによってマップのLinux、セグメント記述子やページテーブルと呼ぶものです。
     だから、我々は結論を出す異なるページ・テーブルを指定した場合、その後、CPUは異なるだろうアドレスに物理アドレスへの仮想アドレス空間になります。だから我々は、プロセスごとにそのページテーブルを確立している、プロセスごとの仮想アドレス空間は、自分のニーズに応じて物理アドレス空間にマッピングされています。切り替え処理が発生する唯一のプロセスが実行されているCPU、上のいくつかの時点から、ページテーブルは実装することができ、プロセスの対応するページテーブルを用いて置換される各プロセスは、独自の仮想アドレスを有していますスペースとは、お互いに影響を与えます。だから、いつでも、CPUのために、彼らだけが変換の物理アドレスへの仮想アドレスを実現することができ、現在のプロセスのページテーブルを持っている必要があります。

图1 进程地址空间的分布.

内核空间到物理内存的映射 

   内核空间对所有的进程都是共享的,其中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据,不管是内核程序还是用户程序,它们被编译和连接以后,所形成的指令和符号地址都是虚地址(参见2.5节中的例子),而不是物理内存中的物理地址。
虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始的,如图4.2所示,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中,3GB(0xC0000000)就是物理地址与虚拟地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
                   
我们来看一下在include/asm/i386/page.h头文件中对内核空间中地址映射的说明及定义:

#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()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。


解惑-Linux内核空间(二)

  从前一讲我们知道,内核空间为3GB~4GB,这1GB的空间分为如下几部分,如图1所示:

  

图2  从PAGE_OFFSET开始的1GB地址空间 

 

 先说明图中符号的含义:

PAGE_OFFSET:0XC0000000,即3GB

high_memory:这个变量的字面含义是高端内存,到底什么是高端内存,Linux内核规定,RAM的前896为所谓的低端内存,而896~1GB共128MB为高端内存。

如果你的内存是512M,那么high_memory是多少?是3GB+512,也就是说,物理地址x<=896M,就有内核地址0xc0000000+x,否则,high_memory=0xc0000000+896M 
或者说high_memory最大值为0xc0000000+896M ,实际值为0xc0000000+x

在源代码中函数mem_init中,有这样一行:

high_memory = (void *) __va(max_low_pfn * PAGE_SIZE);
其中,max_low_pfn为物理内存的最大页数。

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

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

 

VMALLOC_START:非连续区的的起始地址。

VMALLOC_END:非连续区的的末尾地址

在非连续区中,物理内存映射的末端与第一个VMalloc之间有一个8MB的安全区,目的是为了“捕获”对内存的越界访问。处于同样的理由,插入其他4KB的安全区来隔离非连续区。

 

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

 

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

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

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

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

vmalloc()函数用起来比较简单:

char *buf;

buf = vmalloc(16*PAGE_SIZE);  /*获得16页*/

if (!buf)

    /* 错误!不能分配内存*/

在使用完分配的内存之后,一定要释放它:

vfree(buf);

 

图3 内存分配API调用关系

解惑-驱动开发中的I/O地址空间(三)

1.I/O端口和I/O内存
设备驱动程序要直接访问外设或其接口卡上的物理电路,这部分通常都是以寄存器的形式出现。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外设寄存器的不同方式,可以把CPU分成两大类。一类CPU(如M68K,Power PC等)把这些寄存器看作内存的一部分,寄存器参与内存统一编址,访问寄存器就通过访问一般的内存指令进行,所以,这种CPU没有专门用于设备I/O的指令。这就是所谓的“I/O内存”方式。另一类CPU(典型地如X86)将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的” I/O端口方式 。但是,用于I/O指令的“地址空间”相对来说是很小的。事实上,现在x86的I/O地址空间已经非常拥挤。
但是,随着计算机技术的发展,单纯的I/O端口方式无法满足实际需要了,因为这种方式只能对外设中的几个寄存器进行操作。而实际上,需求在不断发生变化,例如,在PC上可以插上一块图形卡,有2MB的存储空间,甚至可能还带有ROM,其中装有可执行代码。自从PCI总线出现后,不管是CPU的设计采用I/O端口方式还是I/O内存方式,都必须将外设卡上的存储器映射到内存空间,实际上是采用了虚存空间的手段,这样的映射是通过ioremap()来建立的。
2.  访问I/O端口
   in、out、ins和outs汇编语言指令都可以访问I/O端口。内核中包含了以下辅助函数来简化这种访问:

inb( )、inw( )、inl( )
分别从I/O端口读取1、2或4个连续字节。后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32位)。
inb_p( )、inw_p( )、inl_p( )
分别从I/O端口读取1、2或4个连续字节,然后执行一条“哑元(dummy,即空指令)”指令使CPU暂停。
outb( )、outw( )、outl( )
分别向一个I/O端口写入1、2或4个连续字节。
outb_p( )、outw_p( )、outl_p( )
分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“哑元”指令使CPU暂停。

insb( )、insw( )、insl( )
分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。
outsb( )、outsw( )、outsl( )
分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。

虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了,对基于ISA总线的系统来说更是如此。通常,I/O设备驱动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用“资源”来记录分配给每个硬件设备的I/O端口。
资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在这里,资源表示I/O端口地址的一个范围。每个资源对应的信息存放在resource数据结构中:

 
  1. struct resource {

  2. resource_size_t start;

  3. resource_size_t end;

  4. const char *name;

  5. unsigned long flags;

  6. struct resource *parent, *sibling, *child;

  7. };


其字段如表1所示。所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;例如,表示I/O端口地址范围的所有资源都包括在一个根节点为ioport_resource的树中。

表1: resource数据结构中的字段

 

 

类型 字段 描述
const char * name 资源拥有者的名字
unsigned long start 资源范围的开始
unsigned long end 资源范围的结束
unsigned long flags 各种标志
struct resource * parent 指向资源树中父亲的指针
struct resource * sibling 指向资源树中兄弟的指针
struct resource * child 指向资源树中第一个孩子的指针

节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。

为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘使用0xf000 到 0xf007的子范围,从盘使用0xf008 到 0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到 0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。

任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:

request_resource( )
把一个给定范围分配给一个I/O设备。

allocate_resource(  )
在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。

release_resource(  )
释放以前分配给I/O设备的给定范围。

内核也为以上函数定义了一些应用于I/O端口的快捷函数:request_region( )分配I/O端口的给定范围,release_region( )释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。
3.把I/O端口映射到内存空间-访问I/O端口的另一种方式
映射函数的原型为:
void *ioport_map(unsigned long port, unsigned int count);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。
但请注意,在进行映射前,还必须通过request_region( )分配I/O端口。

当不再需要这种映射时,需要调用下面的函数来撤消:
void ioport_unmap(void *addr);

  在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用Linux内核的如下一组函数来完成访问I/O内存:·读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. 访问I/O内存
  Linux内核也提供了一组函数申请和释放某一范围的I/O内存:
  struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
   这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。注意,。如果分配成功,则返回非NULL,否则,返回NULL。
另外,可以通过/proc/iomem查看系统给各种设备的内存范围。

  要释放所申请的I/O内存,应当使用release_mem_region()函数:
  void release_mem_region(unsigned long start, unsigned long len)

  申请一组I/O内存后,  调用ioremap()函数:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
其中三个参数的含义为:
phys_addr:与requset_mem_region函数中参数start相同的I/O物理地址;
size:要映射的空间的大小;
flags:要映射的IO空间的和权限有关的标志;
功能: 将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)

おすすめ

転載: blog.csdn.net/ll148305879/article/details/94405076