Linux in the address space and I / O address space

Address space to achieve a great relationship with the cpu architecture, currently the most widely used of 80X86 architecture to discuss this more technical documentation. There is also on this basis.

According to "in-depth understanding of linux kernel" of the memory address is divided into the following three:

The logical address (Logical Address) 

       It contains an address used to specify a machine language instruction or an instruction operand. This addressing 80x86 known segmented construction was particularly Specifically, it encourages programmers to program windows into segments. Each logical address consists of a segment and offset composition, indicates the offset from the start of the segment where the distance between the actual address.

Linear address (linear address) (also known as virtual address virtual address)

       Is a 32-bit unsigned integer, may be used to represent up to 4GB of address (i.e., 32 th address bus 32 to address 2). Linear address is usually represented by hexadecimal numbers, values ​​range from 0x00000000 to 0xffffffff.

Physical address (physical address)

       Class memory means for addressing the memory chip. They press the pin sends an electrical signal to the memory from the address bus corresponding to the microprocessor. Represented by a 32-bit physical address or 36-bit unsigned integer.

Conversion between these three addresses:

The logical address -> (segment) -> linear address -> (tab) -> physical address

Segment achieved:

1, the segment selector see T1 = 0 or 1, to know the current segment is to be converted in the GDT or LDT in section, according to the respective registers and then to obtain its address and size. We have an array of.

2, 13 out of the front segment selector may be in the array, the corresponding segment descriptor is found, so that it's Base, i.e. to know the base address.

3, the Base + offset, is to convert the linear address.

It should be noted that under the 2.6 linux version only requires the use of segmentation in the 80x86 configuration. According to Intel's intention, with global GDT, each with its own process LDT-- but the Linux for all processes use the same segment to address for instructions and data. I.e., the user data segment, the user code segments, corresponding to the kernel and the kernel data section of kernel code segments. Linux implemented by special software so that the logical address (inner section segment identifiers + offset) is composed of a segment identifier 0, while the linear address (linear address field Base + = descriptor offset inner section) segment Base descriptor field is 0, so the address is a logical address = linear, i.e. under linux logical and linear addresses are the same.

Paging implementation:

First understand a few basic concepts:

Page: linear addresses are divided into groups in units of fixed length, called pages.

Frames: all the RAM memory area is divided into fixed length, also called a physical page. Each frame contains a page that is consistent with the length of the page and page frame.

Page frame is a memory area, the page is a block of data, the page can be stored in any block and disks.

Page Table: mapping the linear address into a physical address of the data structure.

Common uses Linux paging model for both 32-bit and 64-bit systems. The system uses two tab 32 is sufficient, the system 64 requires more paging level. Until version 2.6.10, Linux uses three paging model, from version 2.6.11 began to use four paging model.

as the picture shows:

PGD ​​Page Global Directory

Parent directory page PUD

Intermediate page directory PMD

Page table PT

For 32-bit system, two will suffice. Linux and the parent directory page directory page middle bit to 0 to achieve.

When the switching process occurs, Linux cr3 the control register (global directory stored page address) to perform a process content stored in the previous descriptor, then the next to be executed process descriptor value loaded into register cr3. So when a new process to resume execution on CPU, paging unit points to the correct set of page tables.

Address space (a) of doubts -Linux

There are a series of questions so, whether bothering you:

1. The user program is compiled connected to form the address space to what extent?

2. Compile the kernel address space to what extent?

3. To access the peripheral, I / O address space is what?
First answer the first question. Linux is the most common executable file format elf (Executable and Linkable Format). In the executable code elf format, ld always start from 0x8000000 Scheduler "code segment" for each program like this. As for the actual physical address in memory during program execution, a temporary assignment when creating a memory-mapped by its core, depending on the specific address was allocated physical memory pages.
We can use the utility for Linux objdump to disassemble your program, so aware of its address range.
For example: Suppose we have a simple C program Hello.c

 
  1. # include <stdio.h>

  2. greeting ( )

  3. {

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

  5. }

  6. main()

  7. {

  8. greeting();

  9. }

The reason why such a simple program written two functions, to illustrate the transfer process instruction. We use gcc and ld be compiled and linked to get executable code hello. Then, disassemble use Linux for its utility objdump:
$ objdump -d the Hello
main segments were as follows:

 
  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

Which, like 08,048,568 this address, we often say that the virtual address (the address does exist, but because there is a physical address, it appears to be "virtual" Bale).

 

Virtual memory, kernel space and user space

   Linux virtual memory size is 2 ^ 32 (on an x86 machine 32), these core 4G bytes space is divided into two parts. 1G highest byte (from the virtual address 0xC0000000 to 0xFFFFFFFF) for kernel, referred to as "kernel space." 3G while the lower byte (from the virtual address 0x00000000 to 0xBFFFFFFF), used for each process, referred to as "user space." Because each process can enter the kernel through system calls, therefore, Linux kernel space shared by all processes within the system. So, from the point of view of specific processes, each process can have 4G bytes of virtual address space (also known as virtual memory).
   
    Each process has its own private user space (0 ~ 3G), this space for other processes in the system is not visible. The highest 1GB kernel space was shared by all processes and kernel. In addition, the process of "user space" also called "address space" in the back of the narrative, we no longer distinguish between these two terms.


    User space is not a process of sharing, but the process of isolation. Each process can have a maximum user space of 3GB. A process access to one of the addresses will not conflict with other processes to access the same address. For example, a process from the address space of the user 0x1234ABCD may read an integer of 8, and the other from a process space address 0x1234ABCD the user may read an integer of 20, depending on the process of its own logic.
    Any one time, only one process running on a CPU. So for this CPU is concerned, at this moment, the whole system there is only a 4GB virtual address space, the virtual address space for the process. When switching process occurs, the virtual address space with the switch. It can be seen, each process has its own virtual address space, this process runs only when its virtual address space to run it was only known to the CPU. At other times, its virtual address space for CPU, is unknowable. So, although each process can have 4 GB of virtual address space, but in the eyes of the CPU, there is only one virtual address space. Changes in the virtual address space, with the switching process changes.
     From the above we know that the address space formed after a program is compiled connection is a virtual address space, but eventually you want to run the program in physical memory. Thus, the virtual address of any given application must ultimately be converted into a physical address, the virtual address space must be mapped to the physical memory space, the mapping relationship needs to be established by the hardware architecture of a predetermined data structure. This is what we call segment descriptor and page tables, Linux mainly mapped by the page table.
     So, we come to a conclusion, if given a different page table, then the CPU will be a virtual address space to the physical address into the address would be different. So we have established that page table for each process, the virtual address space per process is mapped to the physical address space according to their needs. Since some point on a CPU only one process is running, when the switching process occurs, page tables will be replaced with the corresponding page table of the process, which can implement each process has its own virtual address space and affect each other. So, at any time, for a CPU, the only need to have the current process's page table, they can realize their virtual addresses to physical addresses of the conversion.

图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()申请到的)

Guess you like

Origin blog.csdn.net/ll148305879/article/details/94405076