-
2017-05-17 19:14:34
#Date: 2017/5/17
- 虚拟地址和物理地址的概念
物理地址示意图
如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址,如下图所示[1]。
虚拟地址示意图
MMU将虚拟地址映射到物理地址是以页(Page)为单位的,对于32位CPU通常一页为4K。例如,虚拟地址0xb700 1000~0xb700 1fff是一个页,可能被MMU映射到物理地址0x2000~0x2fff,物理内存中的一个物理页面也称为一个页框(Page Frame)。
内核也不能直接访问物理地址.但因为内核的虚拟地址和物理地址之间只是一个差值0xc0000000的区别,所以从物理地址求虚拟地址或从虚拟地址求物理地址很容易,+-这个差就行了
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
虚拟内存(virtual memory)
这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;
之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。
有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。
——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。2.理解
逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。
——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”。
线性地址(linear address)或也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
-------------------------------------------------------------
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。
这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。物理地址就是,机器内主存的地址,包括RAM和ROM
逻辑地址就是,程序运行在内存中,使用的地址。
虚拟地址就是,cpu支持的内存空间远远大于机器主存的大小,这些多出来的空间对于程序来说是可以用的,这个时候的所有地址都称为虚拟地址物理地址:最小系统下的存储器的实际地址,一般只是由CPU内存控制器(地址线)可以管理的容量为最大地址,而实际上这个容量(由地址产生的)远大于实际存在的容量;实际的存储器容量所需要的地址(内存)控制器管理的容量;它的大小一般由芯片决定
逻辑地址:相对程序员而言使用的地址,或说程序无需知道具体的实际地址管理数,而只要在系统(操作)允许范围内使用就行了(这时使用的是一种算法控制下的地址,实际上它只是借用地址概念产生的程序运行模式),它所要说明的是方便,也就是一个线性的(最好)的程序(指令)排列方式。它的大小一般由操作系统决定
虚拟地址:将具有存储功能的所有存储器(不仅仅是最小系统概念下的),进行“统一”编址,而不考虑存储器之间的差异(快慢等),这时的地址是一个比逻辑地址理会数学化的编号(地址),它的大小等往往由应用程序决定。更多相关内容 -
进程虚拟地址空间和内核空间的关系
2013-11-04 23:41:07Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux的虚拟地址空间也为0~4G.Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为"内核...Linux内核中,关于虚存管理的最基本的管理单元应该是struct vm_area_struct了,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。
下面是struct vm_area_struct结构体的定义:
vm_area_struct结构所描述的虚存空间以vm_start、vm_end成员表示,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。
通常,进程所使用到的虚存空间不连续,且各部分虚存空间的访问属性也可能不同。所以一个进程的虚存空间需要多个vm_area_struct结构来描述。在vm_area_struct结构的数目较少的时候,各个vm_area_struct按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct结构)。但是当vm_area_struct结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,vm_area_struct还添加了vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right(右子节点)三个成员来实现AVL树,以提高vm_area_struct的搜索速度。
假如该vm_area_struct描述的是一个文件映射的虚存空间,成员vm_file便指向被映射的文件的file结构,vm_pgoff是该虚存空间起始地址在vm_file文件里面的文件偏移,单位为物理页面。
一个程序可以选择MAP_SHARED或MAP_PRIVATE共享模式将一个文件的某部分数据映射到自己的虚存空间里面。这两种映射方式的区别在于:MAP_SHARED映射后在内存中对该虚存空间的数据进行修改会影响到其他以同样方式映射该部分数据的进程,并且该修改还会被写回文件里面去,也就是这些进程实际上是在共用这些数据。而MAP_PRIVATE映射后对该虚存空间的数据进行修改不会影响到其他进程,也不会被写入文件中。
来自不同进程,所有映射同一个文件的vm_area_struct结构都会根据其共享模式分别组织成两个链表。链表的链头分别是:vm_file->f_dentry->d_inode->i_mapping->i_mmap_shared,vm_file->f_dentry->d_inode->i_mapping->i_mmap。而vm_area_struct结构中的vm_next_share指向链表中的下一个节点;vm_pprev_share是一个指针的指针,它的值是链表中上一个节点(头节点)结构的vm_next_share(i_mmap_shared或i_mmap)的地址。
进程建立vm_area_struct结构后,只是说明进程可以访问这个虚存空间,但有可能还没有分配相应的物理页面并建立好页面映射。在这种情况下,若是进程执行中有指令需要访问该虚存空间中的内存,便会产生一次缺页异常。这时候,就需要通过vm_area_struct结构里面的vm_ops->nopage所指向的函数来将产生缺页异常的地址对应的文件数据读取出来。
vm_flags主要保存了进程对该虚存空间的访问权限,然后还有一些其他的属性。vm_page_prot是新映射的物理页面的页表项pgprot的默认值。
=======================================
原文:http://oss.org.cn/kernel-book/ch06/6.4.2.htm
6.4.2 进程的虚拟空间
如前所述,每个进程拥有3G字节的用户虚存空间。但是,这并不意味着用户进程在这3G的范围内可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用。
那么,内核怎样管理每个进程3G的虚存空间呢?概括地说,用户进程经过编译、链接后形成的映象文件有一个代码段和数据段(包括data段和bss段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,即全局变量和所有申明为static的局部变量,这些空间是进程所必需的基本要求,这些空间是在建立一个进程的运行映像时就分配好的。除此之外,堆栈使用的空间也属于基本要求,所以也是在建立进程时就分配好的,如图6.16所示:
进程虚拟空间(3G)
图6.16 进程虚拟空间的划分
由图可以看出,堆栈空间安排在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段则在低部,运行时并不向上延伸。从数据段的顶部到堆栈段地址的下沿这个区间是一个巨大的空洞,这就是进程在运行时可以动态分配的空间(也叫动态内存)。
进程在运行过程中,可能会通过系统调用mmap动态申请虚拟内存或释放已分配的内存,新分配的虚拟内存必须和进程已有的虚拟地址链接起来才能使用;Linux 进程可以使用共享的程序库代码或数据,这样,共享库的代码和数据也需要链接到进程已有的虚拟地址中。在后面我们还会看到,系统利用了请页机制来避免对物理内存的过分使用。因为进程可能会访问当前不在物理内存中的虚拟内存,这时,操作系统通过请页机制把数据从磁盘装入到物理内存。为此,系统需要修改进程的页表,以便标志虚拟页已经装入到物理内存中,同时,Linux 还需要知道进程虚拟空间中任何一个虚拟地址区间的来源和当前所在位置,以便能够装入物理内存。
由于上面这些原因,Linux 采用了比较复杂的数据结构跟踪进程的虚拟地址。在进程的 task_struct结构中包含一个指向 mm_struct 结构的指针。进程的mm_struct 则包含装入的可执行映象信息以及进程的页目录指针pgd。该结构还包含有指向 vm_area_struct 结构的几个指针,每个 vm_area_struct 代表进程的一个虚拟地址区间。
图6.17 进程虚拟地址示意图
图 6.17是某个进程的虚拟内存简化布局以及相应的几个数据结构之间的关系。从图中可以看出,系统以虚拟内存地址的降序排列 vm_area_struct。在进程的运行过程中,Linux 要经常为进程分配虚拟地址区间,或者因为从交换文件中装入内存而修改虚拟地址信息,因此,vm_area_struct结构的访问时间就成了性能的关键因素。为此,除链表结构外,Linux 还利用 红黑(Red_black)树来组织 vm_area_struct。通过这种树结构,Linux 可以快速定位某个虚拟内存地址。
当进程利用系统调用动态分配内存时,Linux 首先分配一个 vm_area_struct 结构,并链接到进程的虚拟内存链表中,当后续的指令访问这一内存区间时,因为 Linux 尚未分配相应的物理内存,因此处理器在进行虚拟地址到物理地址的映射时会产生缺页异常(请看请页机制),当 Linux 处理这一缺页异常时,就可以为新的虚拟内存区分配实际的物理内存。
在内核中,经常会用到这样的操作:给定一个属于某个进程的虚拟地址,要求找到其所属的区间以及vma_area_struct结构,这是由find_vma()来实现的,其实现代码在mm/mmap.c中:
* Look up the first VMA which satisfies addr< vm_end, NULL if none. */
struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* Check the cache first. */
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr&& vma->vm_start <= addr)) {
rb_node_t* rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node, structvm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node= rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
这个函数比较简单,我们对其主要点给予解释:
· 参数的含义:函数有两个参数,一个是指向mm_struct结构的指针,这表示一个进程的虚拟地址空间;一个是地址,表示该进程虚拟地址空间中的一个地址。
· 条件检查:首先检查这个地址是否恰好落在上一次(最近一次)所访问的区间中。根据代码作者的注释,命中率一般达到35%,这也是mm_struct结构中设置mmap_cache指针的原因。如果没有命中,那就要在红黑树中进行搜索,红黑树与AVL树类似。
· 查找节点:如果已经建立了红黑树结构(rb_rode不为空),就在红黑树中搜索。
· 如果找到指定地址所在的区间,就把mmap_cache指针设置成指向所找到的vm_area_struct结构。
· 如果没有找到,说明该地址所在的区间还没有建立,此时,就得建立一个新的虚拟区间,再调用insert_vm_struct()函数将新建立的区间插入到vm_struct中的线性队列或红黑树中。
=====================================================
原文:http://bbs.chinaunix.net/archiver/?tid-2058683.html
Linux sys_exec中可执行文件映射的建立及读取
1. 创建一个vm_area_struct;
2. 圈定一个虚用户空间,将其起始结束地址(elf段中已设置好)保存到vm_start和vm_end中;
3. 将磁盘file句柄保存在vm_file中;
4. 将对应段在磁盘file中的偏移值(elf段中已设置好)保存在vm_pgoff中;
5. 将操作该磁盘file的磁盘操作函数保存在vm_ops中;
6. 注意这里没有为对应的页目录表项创建页表,更不存在设置页表项了;
§ §
§ +------§->+--------------+
§ | § | Disk file |
§ | § | |
§ +----------------+ | +---§->|--------------|
§ | vm_area_struct | | | § | Seg Content |
§ |----------------| | | § |--------------|
+----------------+<-§-------- vm_start | | | § | |
| 圈定了一个未映 | § +----- vm_end | | | § | |
| 射到物理内存的 | § | | vm_file--------+ | § +--------------+
| vm_area_struct | § | | vm_pgoff ---------+ §
+----------------+<-§--+ | vm_ops --------+ §
§ | | | §
§ +----------------+ | §
§ | §
§ +----------------------+ §
§ | §
§ +->+-----------------------+ §
§ | file_private_map | §
§ |-----------------------| §
§ | nopage:filemap_nopage | §
§ | ..... | §
§ +-----------------------+ §
user space § kernel § disk
linux驱动程序一般工作在内核空间,但也可以工作在用户空间。下面我们将详细解析,什么是内核空间,什么是用户空间,以及如何判断他们。
Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux的虚拟地址空间也为0~4G.Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为"内核空间".而将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为"用户空间)。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。从图中可以看出(这里无法表示图),每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。
虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET.
内核空间和用户空间之间如何进行通讯?
内核空间和用户空间一般通过系统调用进行通信。
如何判断一个驱动是用户模式驱动还是内核模式驱动? 判断的标准是什么?
用户空间模式的驱动一般通过系统调用来完成对硬件的访问,如通过系统调用将驱动的io空间映射到用户空间等。因此,主要的判断依据就是系统调用。
内核空间和用户空间上不同太多了,说不完,比如用户态的链表和内核链表不一样;用户态用printf,内核态用printk;用户态每个应用程序空间是虚拟的,相对独立的,内核态中却不是独立的,所以编程要非常小心。等等。
还有用户态和内核态程序通讯的方法很多,不单单是系统调用,实际上系统调用是个不好的选择,因为需要系统调用号,这个需要统一分配。
可以通过ioctl、sysfs、proc等来完成。
在进行设备驱动程序,内核功能模块等系统级开发时,通常需要在内核和用户程序之间交换信息。Linux提供了多种方法可以用来完成这些任务。本文总结了各种常用的信息交换方法,并用简单的例子演示这些方法各自的特点及用法。其中有大家非常熟悉的方法,也有特殊条件下方可使用的手段。通过对比明确这些方法,可以加深我们对Linux内核的认识,更重要的是,可以让我们更熟练驾御linux内核级的应用开发技术。
作为一个Linux开发者,首先应该清楚内核空间和用户空间的区别。关于这个话题,已经有很多相关资料,我们在这里简单描述如下:
现代的计算机体系结构中存储管理通常都包含保护机制。提供保护的目的,是要避免系统中的一个任务访问属于另外的或属于操作系统的存储区域。如在IntelX86体系中,就提供了特权级这种保护机制,通过特权级别的区别来限制对存储区域的访问。 基于这种构架,Linux操作系统对自身进行了划分:一部分核心软件独立于普通应用程序,运行在较高的特权级别上,(Linux使用Intel体系的特权级3来运行内核。)它们驻留在被保护的内存空间上,拥有访问硬件设备的所有权限,Linux将此称为内核空间。
相对的,其它部分被作为应用程序在用户空间执行。它们只能看到允许它们使用的部分系统资源,并且不能使用某些特定的系统功能,不能直接访问硬件,不能直接访问内核空间,当然还有其他一些具体的使用限制。(Linux使用Intel体系的特权级0来运行用户程序。)
从安全角度讲将用户空间和内核空间置于这种非对称访问机制下是很有效的,它能抵御恶意用户的窥探,也能防止质量低劣的用户程序的侵害,从而使系统运行得更稳定可靠。但是,如果像这样完全不允许用户程序访问和使用内核空间的资源,那么我们的系统就无法提供任何有意义的功能了。为了方便用户程序使用在内核空间才能完全控制的资源,而又不违反上述的特权规定,从硬件体系结构本身到操作系统,都定义了标准的访问界面。关于X86系统的细节,请查阅参考资料1
一般的硬件体系机构都提供一种“门”机制。“门”的含义是指在发生了特定事件的时候低特权的应用程序可以通过这些“门”进入高特权的内核空间。对于IntelX86体系来说,Linux操作系统正是利用了“系统门”这个硬件界面(通过调用int $0x80机器指令),构造了形形色色的系统调用作为软件界面,为应用程序从用户态陷入到内核态提供了通道。通过“系统调用”使用“系统门”并不需要特别的权限,但陷入到内核的具体位置却不是随意的,这个位置由“系统调用”来指定,有这样的限制才能保证内核安全无虞。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实的坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。
出于效率和代码大小的考虑,内核程序不能使用标准库函数(当然还有其它的顾虑,详细原因请查阅参考资料2)因此内核开发不如用户程序开发那么方便。
内核空间和用户空间的相互作用现在,越来越多的应用程序需要编写内核级和用户级的程序来一起完成具体的任务,通常采用以下模式:首先,编写内核服务程序利用内核空间提供的权限和服务来接收、处理和缓存数据;然后编写用户程序来和先前完成的内核服务程序交互,具体来说,可以利用用户程序来配置内核服务程序的参数,提取内核服务程序提供的数据,当然,也可以向内核服务程序输入待处理数据。
比较典型的应用包括: Netfilter(内核服务程序:防火墙)VS Iptable(用户级程序:规则设置程序);IPSEC(内核服务程序:VPN协议部分)VS IKE(用户级程序:vpn密钥协商处理);当然还包括大量的设备驱动程序及相应的应用软件。这些应用都是由内核级和用户级程序通过相互交换信息来一起完成特定任务的。
信息交互方法用户程序和内核的信息交换是双向的,也就是说既可以主动从用户空间向内核空间发送信息,也可以从内核空间向用户空间提交数据。当然,用户程序也可以主动地从内核提取数据。下面我们就针对内核和用户交互数据的方法做一总结、归纳。
信息交互按信息传输发起方可以分为用户向内核传送/提取数据和内核向用户空间提交请求两大类,先来说说:
由用户级程序主动发起的信息交互。
(1)编写自己的系统调用从前文可以看出,系统调用是用户级程序访问内核最基本的方法。目前linux大致提供了二百多个标准的系统调用,并且允许我们添加自己的系统调用来实现和内核的信息交换。比如我们希望建立一个系统调用日志系统,将所有的系统调用动作记录下来,以便进行入侵检测。此时,我们可以编写一个内核服务程序。该程序负责收集所有的系统调用请求,并将这些调用信息记录到在内核中自建的缓冲里。我们无法在内核里实现复杂的入侵检测程序,因此必须将该缓冲里的记录提取到用户空间。最直截了当的方法是自己编写一个新系统调用实现这种提取缓冲数据的功能。当内核服务程序和新系统调用都实现后,我们就可以在用户空间里编写用户程序进行入侵检测任务了,入侵检测程序可以定时、轮训或在需要的时候调用新系统调用从内核提取数据,然后进行入侵检测(具体步骤和代码参见Linux内核之旅网站电子杂志第四期)。
(2)编写驱动程序Linux/UNIX的一个特点就是把所有的东西都看作是文件(every thing is a file)。系统定义了简洁完善的驱动程序界面,客户程序可以用统一的方法透过这个界面和内核驱动程序交互。而大部分系统的使用者和开发者已经非常熟悉这种界面以及相应的开发流程了。
驱动程序运行于内核空间,用户空间的应用程序通过文件系统中/dev/目录下的一个文件来和它交互。这就是我们熟悉的那个文件操作流程:open() —— read() —— write() —— ioctl() ——close()。(需要注意的是也不是所有的内核驱动程序都是这个界面,网络驱动程序和各种协议栈的使用就不大一致,比如说套接口编程虽然也有open()和close()等概念,但它的内核实现以及外部使用方式都和普通驱动程序有很大差异。)关于这部分的编程细节,请查阅参考资料3、4。
设备驱动程序在内核中要做的中断响应、设备管理、数据处理等等各种工作这篇文章不去关心,我们把注意力集中在它与用户级程序交互这一部分。操作系统为此定义了一种统一的交互界面,就是前面所说的open(), read(), write(), ioctl()和close()等等。每个驱动程序按照自己的需要做独立实现,把自己提供的功能和服务隐藏在这个统一界面下。客户级程序选择需要的驱动程序或服务(其实就是选择/dev/目录下的文件),按照上述界面和文件操作流程,就可以跟内核中的驱动交互了。其实用面向对象的概念会更容易解释,系统定义了一个抽象的界面(abstract interface),每个具体的驱动程序都是这个界面的实现(implementation)。
所以驱动程序也是用户空间和内核信息交互的重要方式之一。其实ioctl, read, write本质上讲也是通过系统调用去完成的,只是这些调用已被内核进行了标准封装,统一定义。因此用户不必像填加新系统调用那样必须修改内核代码,重新编译新内核,使用虚拟设备只需要通过模块方法将新的虚拟设备安装到内核中(insmod上)就能方便使用。关于此方面设计细节请查阅参考资料5,编程细节请查阅参考资料6。
在linux中,设备大致可分为:字符设备,块设备,和网络接口(字符设备包括那些必须以顺序方式,像字节流一样被访问的设备;如字符终端,串口等。块设备是指那些可以用随机方式,以整块数据为单位来访问的设备,如硬盘等;网络接口,就指通常网卡和协议栈等复杂的网络输入输出服务)。如果将我们的系统调用日志系统用字符型驱动程序的方式实现,也是一件轻松惬意地工作。我们可以将内核中收集和记录信息的那一部分编写成一个字符设备驱动程序。虽然没有实际对应的物理设备,但这并没什么问题:Linux的设备驱动程序本来就是一个软件抽象,它可以结合硬件提供服务,也完全可以作为纯软件提供服务(当然,内存的使用我们是无法避免的)。在驱动程序中,我们可以用open来启动服务,用read()返回处理好的记录,用ioctl()设置记录格式等,用close()停止服务,write()没有用到,那么我们可以不去实现它。然后在/dev/目录下建立一个设备文件对应我们新加入内核的系统调用日志系统驱动程序。
(3) 使用 proc 文件系统proc是Linux提供的一种特殊的文件系统,推出它的目的就是提供一种便捷的用户和内核间的交互方式。它以文件系统作为使用界面,使应用程序可以以文件操作的方式安全、方便的获取系统当前运行的状态和其它一些内核数据信息。
proc文件系统多用于监视、管理和调试系统,我们使用的很多管理工具如ps,top等,都是利用proc来读取内核信息的。除了读取内核信息,proc文件系统还提供了写入功能。所以我们也就可以利用它来向内核输入信息。比如,通过修改proc文件系统下的系统参数配置文件(/proc/sys),我们可以直接在运行时动态更改内核参数;再如,通过下面这条指令:
echo 1 > /proc/sys/net/ip_v4/ip_forward
开启内核中控制IP转发的开关,我们就可以让运行中的Linux系统启用路由功能。类似的,还有许多内核选项可以直接通过proc文件系统进行查询和调整。
除了系统已经提供的文件条目,proc还为我们留有接口,允许我们在内核中创建新的条目从而与用户程序共享信息数据。比如,我们可以为系统调用日志程序(不管是作为驱动程序也好,还是作为单纯的内核模块也好)在proc文件系统中创建新的文件条目,在此条目中显示系统调用的使用次数,每个单独系统调用的使用频率等等。我们也可以增加另外的条目,用于设置日志记录规则,比如说不记录open系统调用的使用情况等。关于proc文件系统得使用细节,请查阅参考资料7。
(4) 使用虚拟文件系统有些内核开发者认为利用ioctl()系统调用往往会似的系统调用意义不明确,而且难控制。而将信息放入到proc文件系统中会使信息组织混乱,因此也不赞成过多使用。他们建议实现一种孤立的虚拟文件系统来代替ioctl()和/proc,因为文件系统接口清楚,而且便于用户空间访问,同时利用虚拟文件系统使得利用脚本执行系统管理任务更家方便、有效。
我们举例来说如何通过虚拟文件系统修改内核信息。我们可以实现一个名为sagafs的虚拟文件系统,其中文件log对应内核存储的系统调用日志。我们可以通过文件访问特普遍方法获得日志信息:如
# cat /sagafs/log
使用虚拟文件系统——VFS实现信息交互使得系统管理更加方便、清晰。但有些编程者也许会说VFS 的API 接口复杂不容易掌握,不要担心2.5内核开始就提供了一种叫做libfs的例程序帮助不熟悉文件系统的用户封装了实现VFS的通用操作。有关利用VFS实现交互的方法看参考资料。
(5) 使用内存映像Linux通过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存。这样做的直观效果显而易见:内核在这块地址内存储变更的任何数据,用户可以立即发现和使用,根本无须数据拷贝。而在使用系统调用交互信息时,在整个操作过程中必须有一步数据拷贝的工作——或者是把内核数据拷贝到用户缓冲区,或只是把用户数据拷贝到内核缓冲区——这对于许多数据传输量大、时间要求高的应用,这无疑是致命的一击:许多应用根本就无法忍受数据拷贝所耗费的时间和资源。
我们曾经为一块高速采样设备开发过驱动程序,该设备要求在20兆采样率下以1KHz的重复频率进行16位实时采样,每毫秒需要采样、DMA和处理的数据量惊人,如果要使用数据拷贝的方法,根本无法达成要求。此时,内存映像成为唯一的选择:我们在内存中保留了一块空间,将其配置成环形队列供采样设备DMA输出数据。再把这块内存空间映射到在用户空间运行的数据处理程序上,于是,采样设备刚刚得到并传送到主机上的数据,马上就可以被用户空间的程序处理。
实际上,内存映射方式通常也正是应用在那些内核和用户空间需要快速大量交互数据的情况下,特别是那些对实时性要求较强的应用。X window系统的服务器的虚拟内存区域,就可以被看做是内存映像用法的一个典型例子:X服务器需要对视频内存进行大量的数据交换,相对于lseek/write来说,将图形显示内存直接映射到用户空间可以显著提高效能。
并不是任何类型的应用都适合mmap,比如像串口和鼠标这些基于流数据的字符设备,mmap就没有太大的用武之地。并且,这种共享内存的方式存在不好同步的问题。由于没有专门的同步机制可以让用户程序和内核程序共享,所以在读取和写入数据时要有非常谨慎的设计以保证不会产生干绕。
mmap完全是基于共享内存的观念了,也正因为此,它能提供额外的便利,但也特别难以控制。
由内核主动发起的信息交互在内核发起的交互中,我们最关心和感兴趣的应该是内核如何向用户程序发消息,用户程序又是怎样接收这些消息的,具体问题通常集中在下面这几个方面:内核可否调用用户程序?是否可以通过向用户进程发信号来告知用户进程事件发生?
前面介绍的交互方法最大的不同在于这些方式是由内核采取主动,而不是等系统调用来被动的返回信息的。
(1) 从内核空间调用用户程序。即使在内核中,我们有时也需要执行一些在用户级才提供的操作:如打开某个文件以读取特定数据,执行某个用户程序从而完成某个功能。因为许多数据和功能在用户空间是现有的或者已经被实现了,那么没有必要耗费大量的资源去重复。此外,内核在设计时,为了拥有更好的弹性或者性能以支持未知但有可能发生的变化,本身就要求使用用户空间的资源来配合完成任务。比如内核中动态加载模块的部分需要调用kmod。但在编译kmod的时候不可能把所有的内核模块都订下来(要是这样的话动态加载模块就没有存在意义了),所以它不可能知道在它以后才出现的那些模块的位置和加载方法。因此,模块的动态加载就采用了如下策略:加载任务实际上由位于用户空间的modprobe程序帮助完成——最简单的情形是modprobe用内核传过来的模块名字作为参数调用insmod。用这种方法来加载所需要的模块。
内核中启动用户程序还是要通过execve这个系统调用原形,只是此时的调用发生在内核空间,而一般的系统调用则在用户空间进行。如果系统调用带参数,那将会碰到一个问题:因为在系统调用的具体实现代码中要检查参数合法性,该检查要求所有的参数必须位于用户空间——地址处于0x0000000——0xC0000000之间,所以如果我们从内核传递参数(地址大于0xC0000000),那么检查就会拒绝我们的调用请求。为了解决这个问题,我们可以利用set_fs宏来修改检查策略,使得允许参数地址为内核地址。这样内核就可以直接使用该系统调用了。
例如:在kmod通过调用execve来执行modprobe的代码前需要有set_fs(KERNEL_DS):
......
set_fs(KERNEL_DS);
/* Go, go, go... */
if (execve(program_path, argv, envp) < 0)
return -errno;
上述代码中program_path 为"/sbin/modprobe",argv为{ modprobe_path, "-s", "-k", "--", (char*)module_name, NULL },envp为{ "HOME=/", "TERM=linux", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL }。
从内核中打开文件同样使用带参数的open系统调用,所需的仍是要先调用set_fs宏。
内核和用户空间传递数据主要是用get_user(ptr)和put_user(datum,ptr)例程。所以在大部分需要传递数据的系统调用中都可以找到它们的身影。可是,如果我们不是通过用户程序发起的系统调用——也就是说,没有明确的提供用户空间内的缓冲区位置——的情况下,如何向用户空间传递内核数据呢?
显然,我们不能再直接使用put_user()了,因为我们没有办法给它指定目的缓冲区。所以,我们要借用brk系统调用和当前进程空间:brk用于给进程设置堆空间的大小。每个进程拥有一个独立的堆空间,malloc等动态内存分配函数其实就是进程的堆空间中获取内存的。我们将利用brk在当前进程(current process)的堆空间上扩展一块新的临时缓冲区,再用put_user将内核数据导出到这个确定的用户空间去。
还记得刚才我们在内核中调用用户程序的过程吗?在那里,我们有一个跳过参数检查的操作,现在有了这种方法,可以另辟蹊径了:我们在当前进程的堆上扩展一块空间,把系统调用要用到的参数通过put_user()拷贝到新扩展得到的用户空间里,然后在调用execve的时候以这个新开辟空间地址作为参数,于是,参数检查的障碍不复存在了。
char * program_path = "/bin/ls" ;
/* 找到当前堆顶的位置*/
mmm=current->mm->brk;
/* 用brk在堆顶上原扩展出一块256字节的新缓冲区*/
ret = brk(*(void)(mmm+256));
/* 把execve需要用到的参数拷贝到新缓冲区上去*/
put_user((void*)2,program_path,strlen(program_path)+1);
/* 成功执行/bin/ls程序!*/
execve((char*)(mmm+2));
/* 恢复现场*/
tmp = brk((void*)mmm);这种方法没有一般性(具体的说,这种方法有负面效应吗),只能作为一种技巧,但我们不难发现:如果你熟悉内核结构,就可以做到很多意想不到的事情!
(3) 使用信号信号在内核里的用途主要集中在通知用户程序出现重大错误,强行杀死当前进程,这时内核通过发送SIGKILL信号通知进程终止,内核发送信号使用send_sign(pid,sig)例程,可以看到信号发送必须要事先知道进程序号(pid),所以要想从内核中通过发信号的方式异步通知用户进程执行某项任务,那么必须事先知道用户进程的进程号才可。而内核运行时搜索到特定进程的进程号是个费事的工作,可能要遍历整个进程控制块链表。所以用信号通知特定用户进程的方法很糟糕,一般在内核不会使用。内核中使用信号的情形只出现在通知当前进程(可以从current变量中方便获得pid)做某些通用操作,如终止操作等。因此对内核开发者该方法用处不大。
类似情况还有消息操作。这里不罗嗦了。
总结 由用户级程序主动发起的信息交互,无论是采用标准的调用方式还是透过驱动程序界面,一般都要用到系统调用。而由内核主动发起信息交互的情况不多。也没有标准的界面,操作大不方便。所以一般情况下,尽可能用本文描述的前几种方法进行信息交互。毕竟,在设计的根源上,相对于客户级程序,内核就被定义为一个被动的服务提供者。因此,我们自己的开发也应该尽量遵循这种设计原则。
-
【Linux操作系统】-- 程序地址空间
2022-04-08 14:40:162.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存过程,达到进程读写内存和OS内存管理操作,进行软件上面的分离。(物理空间滞后性开辟--写实拷贝是经典的之后开辟空间) 当...目录
虚拟地址的内部结构
这张图中可以看到内存地址中按从低地址到高地址排列的内存块,分别是代码区,字符常量区,数据区包括未初始化的和已初始化的,堆。而栈的地址是从高地址到低地址。
可以通过代码来验证一下(Linux环境下)
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("uninit addr:%p\n",&g_unval);//未初始化数据区 printf("init addr:%p\n",&g_val);//初始化数据区 char* heap =(char*)malloc(10); printf("heap addr:%p\n",heap);//堆 printf("stack addr_string:%p\n",&s);//栈--栈上的临时空间 printf("stack addr_heap:%p\n",&heap); int a=10,b=20; printf("stack addr_a:%p\n",&a);//验证栈区地址是由大到小 printf("stack addr_b:%p\n",&b); return 0; }
[wjy@VM-24-9-centos 407test]$ ./mycode code addr:0x40057d //代码区 string rdonly addr:0x400700 //字符常量区 uninit addr:0x601044 //未初始化数据区 init addr:0x60103c //初始化 heap addr:0x199a010 //堆 stack addr_string:0x7ffd05938bc8 //栈 stack addr_heap:0x7ffd05938bc0 stack addr_a:0x7ffd05938bbc stack addr_b:0x7ffd05938bb8
那么C/C++的程序地址空间,是内存吗?
验证一下,写一个mycode.c的C语言程序。定义一个全局变量,这样父子进程用到这个值都会是这个值。当fork()创建子进程,结果为0就是子进程,如果不为0,fork()值为子进程的pid,那么就是父进程。当两个进程一样的时候,二者共用一份代码,一份数据。当子进程有改变的时候,就会发生写时拷贝。
[wjy@VM-24-9-centos 407test]$ cat mycode.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int g_val=100; int main() { //数据是各自有一份(写时拷贝) if(fork()==0) { //child 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)//当cnt值为3改变g_val的数据 { printf("############### child changed data ################\n"); g_val=200; printf("############### child changed done ################\n"); } } exit(0); } else{ //parent while(1) { printf("I am parent,g_val=%d,&g_val=%p\n",g_val,&g_val); sleep(1); } } return 0; }
打印结果
[wjy@VM-24-9-centos 407test]$ ./mycode I am parent,g_val=100,&g_val=0x601054 I am child,times:5,g_val=100,&g_val:0x601054 I am parent,g_val=100,&g_val=0x601054 I am child,times:4,g_val=100,&g_val:0x601054 I am parent,g_val=100,&g_val=0x601054 ############### child changed data ################ ############### child changed done ################ I am child,times:3,g_val=200,&g_val:0x601054 I am parent,g_val=100,&g_val=0x601054 I am child,times:2,g_val=200,&g_val:0x601054 I am parent,g_val=100,&g_val=0x601054 I am child,times:1,g_val=200,&g_val:0x601054 I am parent,g_val=100,&g_val=0x601054 I am parent,g_val=100,&g_val=0x601054 I am parent,g_val=100,&g_val=0x601054
当cnt的值到3的时候,g_val的值发生了改变,此后子进程的所有g_val值都是200,但是父子进程并不干涉,因为发生了写时拷贝。但是我们发现,虽然父子进程的值已经不一样了,但是他们的地址竟然是一样的。为什么地址没有变化呢?
虚拟地址
如果C/C++打印出来的地址是物理内存地址,这种现象是不可能存在的。所以这里我们使用的地址不是物理地址,而是虚拟地址。所以上面提到的C/C++程序地址空间,实际上就是进程虚拟地址空间。
进程地址空间作为操作系统的地址空间,它是非常大的,进程在获取地址空间的时候,每个地址空间都认为自己独占这份进程地址空间,并且进程地址空间也给进程画了个大饼,告诉他们只有此进程独占进程地址空间。然而并非如此,因为进程地址空间非常大,他们不可能独占,而是让进程先描述,它能够干什么,需要从进程地址空间中拿走多少空间。然后再去申请空间,这也就是先描述再组织。
所以这里的先描述也是一个结构体,叫mm_struct,他是一个在内核中的数据结构类型,是一个具体进程的地址空间变量。这里包括了什么时候分配,如何分配等等。task_struct是描述进程信息的控制块,那么mm_struct就是描述进程分配地址空间信息的结构体。
但是还有一个疑问,会不会有这种情况,每个进程需要的空间大小和进程地址空间一样大?比如进程地址空间有10亿大小,进程需要10亿。这种情况是非常少的,几乎不存在。如果进程想要申请10亿,可能进程地址空间也不会给,申请会失败。
mm_struct虚拟地址结构体
地址空间本质是内核中一种数据类型。这个mm_struct可以看作很多区域的划分,里面划分了很多由start和end划分开来的数据。而这个mm_struct可以看作是操作系统的所有地址,因为每个进程都认为自己独占整个虚拟地址空间,在32位操作系统下,每个进程都认为地址空间的划分是按照4GB空间划分的,都是从0x00000000-0xFFFFFFFF。因为这些虚拟地址是连续的,所以也可以叫做线性地址。
struct mm_struct{ unsigned int code_start; unsigned int code_end; unsigned int init_data_start; unsigned int init_data_end; unsigned int uninit_data_start; unsigned int uninit_data_end; unsigned int heap_start; unsigned int heap_end; unsigned int stack_start; unsigned int stack_end; }
数据代码和虚拟地址的结合
那么通过上面的虚拟内存将空间地址划分,我们知道了将数据和代码放在哪,但这都是理论阶段。就像你逛某宝,上面的界面图已经将商品划分好,你知道怎么购买,但是这个商品还没有到你的手上。所以代码数据和虚拟地址的结合也需要某种介质来完成。
这个介质就是页表+MMU,MMU是一个硬件,是用来查页表的,一般MMU是集成在CPU当中的。页表是由操作系统维护进程的一张表,这个表可以看作左边是虚拟地址,右边是物理地址。
页表的作用:将虚拟地址转化为物理地址。这个页表就是一张映射表。那么mm_struct中的各个区域,代码区,字符常量区,数据区,栈区,堆区等等这些都是通过页表,先在页表中寻找虚拟地址,然后再根据页表中的映射找到对应的物理地址,从而找到对应的代码和数据。
那么为什么一定要加这个虚拟地址和页表呢?进程直接找物理地址不好吗?
这是因为加上虚拟地址和页表方便管理,并且如果直接访问物理地址可能会有一些非法操作和错误,当进程直接访问物理地址,我们通过指针访问,进程A的数据,但是如果指针知错了直接指向进程B的地址,那么本来要修改A的数据,直接把B的地址修改了,造成了非法访问。
就像我们有了压岁钱,但是需要交给妈妈保管,如果自己保管压岁钱,出去话可能被骗了,妈妈都不知道。所以进程也需要通过虚拟地址,让页表来保存,实际上,这个页表也就是操作系统的代名词,这个进程的虚拟地址还是交给操作系统管理的。
举个例子,这里有一个char* 类型的字符串,char* str="hello world";但是这里的str指向的内容不能随意被改变,*str='H'。这是因为本质上OS给字符常量区的权限只有读操作r,在页表中,硬件对应的页表指向的字符常量区只有r权限,如果进行了写w操作,那么系统直接将程序崩溃。
为什么要有地址空间?
1.通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了保护物理内存及各个进程的数据安全!(通过操作系统管理)
2.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存过程,达到进程读写内存和OS内存管理操作,进行软件上面的分离。(物理空间滞后性开辟--写实拷贝是经典的之后开辟空间)
当我们申请1000字节,我们不一定能立马使用全部字节,在OS角度,如果空间立马给你,那么整个申请的空间,,本来可以给别人立马用的,但是现在被你闲置着。这就意味着,虽然你有了空间,但是有的空间从来就没有被读写。所以操作系统会基于缺页中断进行物理内存,也就是当这部分闲置的空间没有立马被使用,操作系统不会先给你,如果你要使用的时候,操作系统再给你。但是你并不知道操作系统其实在这中间,还把内存给别人使用了。
那么当A进程要申请空间时候,物理内存已经满了,这时候操作系统还是可以申请到内存的,这就是内存管理算法。当磁盘满的时候,会将别的没有使用空间的进程置换到磁盘中,将空间开辟出来,申请给A进程使用。这时候A进程并不知道操作系统给你的空间是从哪里来的,是物理内存剩余申请过来的?还是物理内存已经满了,将别的进程与磁盘置换申请来的?这就是地址空间从中起到的重要作用。
3.站在CPU和应用层的角度,进程统一可以统一使用4GB空间,而且每个空间区域的相对应位置,是比较明确的。(CPU统一看待同一个区域的进程)
如果没有页表这样的地址空间,用main函数来说,每个程序都有main函数,CPU得每次都要对应各个main函数的物理地址空间,而且物理内存位置不能改变,改变的话CPU每次都要重新找,非常麻烦。即使位置没有改变,这样找的话也比较凌乱,因为还有其它区域的代码。所以操作系统建立页表,让CPU每次找main函数都从一个地址去找,这个地址就是所有main函数的地址,然后页表建立映射,找到各个进程的main函数,这样对CPU来说非常香,也很容易管理,main函数的物理内存也可以随便改变。
下图并不是一个虚拟地址能找两个物理地址,因为一个CPU只能运行一个进程,但是他们是通过时间片,交叉进行运作的。而这些物理地址都是可以通过同一个虚拟地址找到,从而达到CPU只访问一个地址就能找到main函数。
所以进程=PCB+地址空间+页表+代码和数据
所以回归上面的问题,为什么父子进程的结果已经不一样了,但是他们指向的空间还是一样的?
子进程的创建是以父进程为模板的,所以父子共用一份代码,它们所指向的物理地址也是一样的(红色实线和红色虚线),同样指向虚拟地址也是一样的。当子进程发生改变,进程具有独立性,发生了写时拷贝,这时候子进程指向的物理空间也发生改变(蓝色线),但是映射这些物理地址的页表中的虚拟地址没有发生改变。所以上面问题代码所指向的同一个地址是虚拟地址。
所有的只读数据,一般只有一份,比如定义两个指针,两个指针指向的内容都是一样的字符串,那么他们地址测试之后,也是一样的 。操作系统维护一份是成本最低的。
这也是缺页中断的表现。本来父子进程共享一份代码,会读取同一个物理地址,当子进程要进行改变的时候,操作系统会对子进程进行中断,让页表指向的物理地址变成一块新的空间之后,再让子进程继续运行。而在中断的这一过程,子进程并没有发觉操作系统让你中断了,就像时间静止。在这之后,子进程写时拷贝和父进程分开了,可以进行写操作了。
[wjy@VM-24-9-centos 0408test]$ cat mytest.c #include <stdio.h> int main() { char* str="hello world"; char* p="hello world"; printf("%p\n",str); printf("%p\n",p); } [wjy@VM-24-9-centos 0408test]$ ./mytest 0x400610 0x400610
虚拟地址的其他结构
除了上面的虚拟地址结构,还有命令行参数和环境变量,他们也有地址。
当我们在原有代码上增加了命令行参数和环境变量,并将它们的地址打印出来,发现命令行参数和环境变量的地址比栈要大,也就说明命令行参数和环境变量的地址更高
int g_unval; int g_val=100; int main(int argc,char* argv[],char* env[]) { const char* s="hello world"; printf("code addr:%p\n",main);//代码区 printf("string rdonly addr:%p\n",s);//(字符串)常量区 printf("uninit addr:%p\n",&g_unval);//未初始化数据区 printf("init addr:%p\n",&g_val);//初始化数据区 char* heap =(char*)malloc(10); printf("heap addr:%p\n",heap);//堆 printf("stack addr_string:%p\n",&s);//栈--栈上的临时空间 printf("stack addr_heap:%p\n",&heap); int a=10,b=20; printf("stack addr_a:%p\n",&a);//验证栈区地址是由大到小 printf("stack addr_b:%p\n",&b); for(int i=0;argv[i];i++) { printf("argv[%d]:%p\n",i,argv[i]); } for(int i=0;env[i];++i) { printf("env%d:%p\n",i,env[i]); } return 0; }
所以更准确的图是这样的。
-
Linux中的地址空间以及I/O地址空间
2019-07-01 19:33:44地址空间实现与cpu的体系结构有很大的关系,目前以应用最广的的80X86体系结构来论述这块的技术文档较多。这里也以此为基础。 根据《深入理解linux内核》所述,内存地址分为以下三种: 逻辑地址(Logical Address) ...地址空间实现与cpu的体系结构有很大的关系,目前以应用最广的的80X86体系结构来论述这块的技术文档较多。这里也以此为基础。
根据《深入理解linux内核》所述,内存地址分为以下三种:
逻辑地址(Logical Address)
包含在机器语言指令中用来指定一个操作数或一条指令的地址。这种寻址方式在80x86著名的分段结构中表现得尤为具体,它促使windows程序员把程序分成若干段。每个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址(linear address)(也称虚拟地址 virtual address)
是一个32位无符号整数,可以用来表示高达4GB的地址(2的32次方即32根地址总线寻址)。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff。
物理地址(physical address)
用于内存芯片级内存单元寻址。它们与从微处理器的地址引脚按发送到内存总线上的电信号相对应。物理地址由32位或36位无符号整数表示。
这三种地址之间的转换:
逻辑地址-->(分段)-->线性地址-->(分页)-->物理地址
分段的实现:
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它的Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
这里需要指出的是,2.6版的linux下只有在80x86结构下需要使用分段。按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。Linux通过特殊的软件实现使逻辑地址(段标识符+段内偏移量)组成的段标识符为0,同时使线性地址(线性地址=段描述符Base字段+段内偏移量)的段描述符Base字段为0,从而是逻辑地址=线性地址,即linux下逻辑地址和线性地址是一致的。
分页的实现:
先了解几个基本概念:
页:线性地址被分成以固定长度为单位的组,称为页。
页框:把所有RAM分成固定长度的内存区域,也叫物理页。每个页框包含一个页,也就是说页框的长度和页的长度一致。
页框是一个存储区域,页是一个数据块,可以存放在任何页框和磁盘中。
页表:把线性地址映射到物理地址的数据结构。
Linux采用了一种同时适用于32位和64位系统的普通分页模型。32位系统采用两级分页就足够了,而64位系统则需要更多的分页级别。直到2.6.10版本,Linux采用三级分页模型,从2.6.11版本开始采用四级分页模型。
如图所示:
页全局目录PGD
页上级目录PUD
页中间目录PMD
页表PT
对于32位系统,两级就足够了。Linux使页上级目录和页中间目录位为0来实现。
当进程发生切换时,Linux把cr3控制寄存器(保存页全局目录的地址)的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。
解惑—Linux中的地址空间(一)
有这么一系列的问题,是否在困扰着你:
1. 用户程序编译连接形成的地址空间在什么范围内?
2. 内核编译后地址空间在什么范围内?
3. 要对外设进行访问,I/O的地址空间又是什么样的?
先回答第一个问题。Linux最常见的可执行文件格式为elf(Executable and Linkable Format)。在elf格式的可执行代码中,ld总是从0x8000000开始安排程序的“代码段”,对每个程序都是这样。至于程序执行时在物理内存中的实际地址,则由内核为其建立内存映射时临时分配,具体地址取决于当时所分配的物理内存页面。
我们可以用Linux的实用程序objdump对你的程序进行反汇编,从而知晓其地址范围。
例如:假定我们有一个简单的C程序Hello.c-
# include <stdio.h>
-
greeting ( )
-
{
-
printf(“Hello,world!\n”);
-
}
-
main()
-
{
-
greeting();
-
}
之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用gcc和ld对其进行编译和连接,得到可执行代码hello。然后,用Linux的实用程序objdump对其进行反汇编:
$objdump –d hello
得到的主要片段为:-
08048568 <greeting>:
-
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 <main>:
-
804857c: pushl %ebp
-
804857d: movl %esp, %ebp
-
804857f: call 8048568 <greeting>
-
8048584: leave
-
8048585: ret
-
8048586: nop
-
8048587: nop
其中,像08048568这样的地址,就是我们常说的虚地址(这个地址实实在在的存在,只不过因为物理地址的存在,显得它是“虚”的罢了)。
虚拟内存、内核空间和用户空间
Linux虚拟内存的大小为2^32(在32位的x86机器上),内核将这4G字节的空间分为两部分。最高的1G字节(从虚地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而较低的3G字节(从虚地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此,Linux内核空间由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟地址空间(也叫虚拟内存)。
每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则为所有进程以及内核所共享。另外,进程的“用户空间”也叫“地址空间”,在后面的叙述中,我们对这两个术语不再区分。
用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有3GB的用户空间。一个进程对其中一个地址的访问,与其它进程对于同一地址的访问绝不冲突。比如,一个进程从其用户空间的地址0x1234ABCD处可以读出整数8,而另外一个进程从其用户空间的地址0x1234ABCD处可以读出整数20,这取决于进程自身的逻辑。
任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个4GB的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的CPU所知。在其它时刻,其虚拟地址空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4 GB的虚拟地址空间,但在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数据结构中:-
struct resource {
-
resource_size_t start;
-
resource_size_t end;
-
const char *name;
-
unsigned long flags;
-
struct resource *parent, *sibling, *child;
-
};
其字段如表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()申请到的) -
-
linux内存管理---虚拟地址、逻辑地址、线性地址、物理地址的区别(一)
2013-07-31 10:55:45虚拟地址 、物理地址 、线性地址 、逻辑地址 -
虚拟地址和物理地址的含义
2015-04-25 12:00:53CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有MMU(Memory Management Unit,内存管理单元),或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将直接传到CPU芯片的外部... -
linux 内存管理(虚拟地址到物理地址)
2008-11-23 16:58:36该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可... -
RTOS之UCOS(五)---存储管理与虚拟内存
2019-06-18 20:19:10我们使用C语言开发程序时经常涉及到存储空间的分配和释放,常用malloc/free两个函数完成相应的操作(C++语言则使用new/delete)。那么,我们分配和释放的到底是什么呢?有点计算机基础的应该知道,分配和释放的是... -
linux用户空间与内核空间关系
2020-03-13 10:51:26Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF... -
虚拟内存以及进程的虚拟内存分布(第六章)
2019-05-17 10:59:58在早期的计算机中,程序都是直接运行在物理内存上的,运行时访问的地址都是物理地址,而这要求程序使用的内存空间不超过物理内存的大小。在现代计算机操作系统中,计算机同时运行多个程序,为了提高CPU的利用率和... -
操作系统:进程地址空间
2018-08-20 21:26:13操作系统在管理内存时,每个进程都有一个独立的进程地址空间,进程地址空间的地址为虚拟地址,对于32位操作系统,该虚拟地址空间为2^32=4GB。其中0-3G是用户空间,3G-4G是内核空间。但4G的地址空间是不存在的,也... -
云手机怎么搭建服务器地址
2021-08-11 02:06:14云手机怎么搭建服务器地址 内容精选换一换欢迎使用云手机服务(Cloud Phone,CPH)。云手机是基于华为云裸金属服务器,虚拟出带有原生安卓操作系统,同时具有虚拟手机功能的云服务器。您可以使用本文档提供的API对云... -
【WPF】UI虚拟化之------自定义VirtualizingWrapPanel
2017-12-15 17:48:23前言前几天QA报了一个关于...一种用的是虚拟化的VirtualizingStackPanel,另一种没有考虑虚拟化用的是WrapPanel。所以当ListBox切换到第二种Template,而且有很多Item的时候,内存就爆掉然后直接挂了。然后就想着有没 -
面由 AI 生|虚拟偶像“捏脸”技术解析
2022-04-24 16:01:05针对“身份”、“沉浸感”、“低延迟”、“随时随地”这四个元宇宙核心基础,ZEGO 即构科技基于互动智能的业务逻辑,提出并落地了 ZegoAvatar 解决方案,将 AI 视觉技术应用至虚拟形象,完成了业务和技术的无缝衔接... -
物理地址和虚拟地址的概念
2010-06-02 10:18:00虚拟地址和物理地址的概念 CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有MMU(Memory Management Unit,内存管理单元),或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将... -
操作系统--虚拟存储器
2016-08-04 10:10:35为每个进程提供一个大的,一致的和 私有的地址空间。 提供了3个重要能力。 将主存看成磁盘地址空间的高速缓存。 只保留了活动区域,并根据需要在磁盘和主存间来回传送数据,高效使用主存。 -
qemu-kvm内存虚拟化的原理及其流程
2020-08-27 20:25:21非虚拟化环境,内存分配时逻辑地址需要转换为线性地址,然后由线性地址转换为物理地址。 逻辑地址 ==> 线性地址 ==> 物理地址 虚拟化环境下,由于qemu-kvm进程在宿主机上作为一个普通进程,那对于Guest而言,... -
地址空间、内核空间、IO地址空间
2014-11-26 20:27:40有这么一系列的问题,是否在困扰着你:用户程序编译连接形成的地址空间在什么范围内?内核编译后地址空间在什么范围内?要对外设进行访问,I/O的地址空间又是什么样的? 先 回答第一个问题。Linux最常见的可执行... -
操作系统-------------------内存空间的分配方式(连续分配和非连续分配和虚拟存储技术)
2020-10-03 10:57:59(PS:由于请求分页是虚拟内存的范畴,将会在迟下的时候讲) 总结两级页面(甚至多级页面)的优缺点: 优点:可以以更小的空间存储页表,例如有两级页表,一级页表肯定比二级页表小嘛,因为一级页表是二级页表的索引... -
内核地址空间与进程地址空间
2013-11-29 20:37:18一、内核地址空间 1)物理内存映射区: kmalloc,get_free_pages申请的内存位于物理内存映射区,在物理上连续,他们与真实的物理地址只有一个固定的偏移。 virt_to_phys() 可以实现内核虚拟地址转化为物理... -
虚拟化考点
2021-12-13 20:40:26计算机科学中的虚拟化包括平台虚拟化、存储虚拟化、网络虚拟化、设备虚拟化。狭义的虚拟化:在计算机上模拟运行多个操作系统平台; 3.计算机的服务层次结构:硬件(Hardware)--操作系统(OS)--框架库(Framework)--... -
深入探究Windows平台客户端安全问题-进程地址空间入侵和白加黑高阶利用
2016-05-28 13:31:06标 题: 深入探究Windows平台客户端安全问题-进程地址空间入侵和白加黑高阶利用 时 间: 2014-09-08,00:03:51 前言 为了避免被读者骂“标题党”,笔者在文章开头先澄清一下这个高大尚的“进程地址空间入侵”的可... -
GIS空间索引
2020-03-11 12:02:02在GIS系统中,空间索引技术就是通过更加有效的组织方式,抽取与空间定位相关的信息组成对原空间数据的索引,以较小的数据量管理大量数据的查询,从而提高空间查询的效率和空间定位的准确性。 常见的GIS空间索引 KD... -
vmware虚拟化故障虚拟磁盘丢失恢复办法
2017-03-14 16:06:17Dell R710系列服务器(用于VMware虚拟主机),Dell MD 3200系列存储(用于存放虚拟机文件),VMware ESXi 5.5版本,因意外断电,导致某台虚拟机不能正常启动,查看虚拟机的配置文件时发现此虚拟机的配置文件除了磁盘... -
[操作系统]内存管理 多级页表/虚拟内存
2020-08-27 15:09:02内存地址从0开始,每个地址对应一个存储单元 按字节编制:每个存储单元大小为1字节,即1B(8个二进制位) 装入的三种方式 将指令的逻辑地址转换为物理。确定物理地址 绝对装入 在编译时,如果知道程序将要放到内存... -
解析一个Java对象占用多少内存空间
2019-07-13 16:02:15说明: alignment, 对齐, 比如8字节的数据类型long, 在内存中的起始地址必须是8的整数倍。 padding, 补齐; 在对象所占据空间的末尾,如果有空白, 需要使用padding来补齐, 因为下一个对象的起始位置必须是4/8字节(32bit... -
深入理解Linux内核day08--进程线性地址空间
2016-05-05 10:45:31进程地址空间 内核中的函数以相当直截了当的方式获得动态内存: 1.__get_free_pages()和alloc_pages()从分区页框分配器中获得页框。 2.kmem_cache_alloc()和kmalloc()使用slab分配器为专门或通用对象分配快... -
操作系统复习提纲
2021-01-05 11:27:36进程调度信息 进程优先级 进程调度所需的其他信息 事件 进程状态 进程控制信息 程序和数据的地址 进程同步和通信机制 资源清单 链接指针 前趋图是一个有向无环图 (DAG-Directed Acyclic Graph),用于描述进程之间... -
一致性hash和虚拟节点
2019-04-20 16:32:00“虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。...