========================================================================================================================================================================================================================================================================================
学生不才,这个虚拟地址与虚拟内存的问题困惑的我很长的时间,现在写一下最近看过一些资料后的理解吧,肯定存在不完整或者不正确的地方,希望大家可以纠正,如果能给你带来一些新知识,或者帮您复习了一下旧知识,我感到非常荣幸,下面开始,以32位的i386CPU看。
首先先来看看我一直以来认识的错误的地方。
在最开始看书的时候,我知道程序在运行时,不是全部加入内存当中的,当然,这个现在依旧正确,在有了虚拟地址空间这个概念以后,我一直认为是在硬盘中划分出一块虚拟内存出来,然后给每个程序4G的硬盘空间,作为这个程序的虚拟内存,然后就需要运行哪一块然后哪一块装入内存当中。后来发现这个是错误的。大家可以在我错误的地方来看看自己的理解,纠正自己的问题。
首先,虚拟地址空间是不等于虚拟内存的(我的一本Linux教材上写的是这两个概念一致的,我认为是错误的)。我们来看一下这几个概念:
虚拟内存:虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。
虚拟地址空间:在32位的i386CPU的地址总线的是32位的,也就是说可以寻找到4G的地址空间。我们的程序被CPU执行,就是在0x00000000到0xFFFFFFFF这一段地址中。高2G的空间为内核空间,由操作系统调用,低2G的空间为用户空间,由用户使用。
CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。因为只有程序的一部分加入到内存中,所以会出现所寻找的地址不在内存中的情况(CPU产生缺页异常),如果在内存不足的情况下,就会通过页面调度算法来将内存中的页面置换出来,然后将在外存中的页面加入到内存中,使程序继续正常运行。
可以看出,虚拟地址空间和虚拟内存的一个关键的因素是MMU(内存管理单元)。
现在一个程序的执行现在可以分为3部分:
1:CPU需要执行的语句的虚拟地址。
2:程序装入内存的部分
3:程序再外存中的部分
因为CPU是通过时间片轮转的方法使不同的程序并发执行的。所以在某一时刻只有一个程序占据CPU资源,CPU的最大的寻址空间为4G,所以说可以将每个程序可以看做独立占据4G的内存(只是可以看成,但是它并没有占据实际的4G内存)。而CPU是将虚拟地址空间里面的代码执行,如果在内存中寻找不到所需要的页面,就需要到外存中寻找,外存的这一部分,我们可以当成内存来使用,这也就是虚拟内存。虚拟地址空间不等于虚拟内存。虚拟地址空间是一个空间,不是真正存在的,只是通过CPU的寻址虚拟出来的一个范围。而虚拟内存是实实在在的硬盘的空间。
我看到了一个比较形象的比喻,假设4G个门牌号(4G的虚拟地址空间,并将这4G的虚拟空间进行分页),但是房子的数量(内存)少于门牌号的数量(4G的虚拟空间),那样就先把每个房子(内存)上挂一个门牌号(页),如果你要找一个门牌号(页),就需要查找每个房子(内存),如果这个门牌号没有挂(页还没有被加入到内存中),那么就将一个房子的门牌号(页面置换到外存)摘下来,把你找的那个门牌号挂上(将外存的页加入内存),这样就找到了需要的门牌号(页)。
这是我将这个比喻综合了一下。CPU只需要说找哪一个页面,MMU就将这个页面翻译成物理地址,再通过页面调度机制来讲不在内存中的页加入到内存中。
我认为计算机使用的是一种各司其职的方法。
CPU老大只需要要虚拟地址中的一页,范围在0x00000000到0xFFFFFFFF,因为他的地址总线是32位,4G是他最大的能力,然后他就把任务分配给他的手下,CPU不需要知道他的手下是如何找到这一页,他只负责去要这一页和执行这些代码,然后他就和他的手下说“有招想去,没招死去”,他的手下必须要能找到这一页,然后内存非常有限,而CPU不管这个,只需要你能找到这一页让我执行就好,所以CPU的手下就将硬盘中的一部分当做内存,然后拿来骗CPU说,“这是我从内存中找到的”,然后CPU就去运行。如果访问的地方实在是不能找到,或者是没有权限,那么这个程序就真死了。程序员在开发的时候,因为程序员所编写的代码最终是要让CPU去执行,所以程序员也理所应当认为我有4G的内存空间,程序员把程序交给CPU,CPU就交给他手下。
所以说,他们每一层都不需要管对方是如何办到的,他们在乎的只有结果。他们各司其职,CPU认为我有4G内存空间的原因是因为CPU的地址总线为32位,最大的寻址能力是4G,而内存没有这个大,所以它想出来的办法是将硬盘的一部分拿来骗CPU,并给这块骗人的地方起了一个好名字,虚拟内存。这就是我的理解中的虚拟内存空间和虚拟内存的概念。
自身水平有限,希望大家矫正,谢谢!
==================================================================================================================================================================================================================
学生不才,这个虚拟地址与虚拟内存的问题困惑的我很长的时间,现在写一下最近看过一些资料后的理解吧,肯定存在不完整或者不正确的地方,希望大家可以纠正,如果能给你带来一些新知识,或者帮您复习了一下旧知识,我感到非常荣幸,下面开始,以32位的i386CPU看。
首先先来看看我一直以来认识的错误的地方。
在最开始看书的时候,我知道程序在运行时,不是全部加入内存当中的,当然,这个现在依旧正确,在有了虚拟地址空间这个概念以后,我一直认为是在硬盘中划分出一块虚拟内存出来,然后给每个程序4G的硬盘空间,作为这个程序的虚拟内存,然后就需要运行哪一块然后哪一块装入内存当中。后来发现这个是错误的。大家可以在我错误的地方来看看自己的理解,纠正自己的问题。
首先,虚拟地址空间是不等于虚拟内存的(我的一本Linux教材上写的是这两个概念一致的,我认为是错误的)。我们来看一下这几个概念:
虚拟内存:虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。
虚拟地址空间:在32位的i386CPU的地址总线的是32位的,也就是说可以寻找到4G的地址空间。我们的程序被CPU执行,就是在0x00000000到0xFFFFFFFF这一段地址中。高2G的空间为内核空间,由操作系统调用,低2G的空间为用户空间,由用户使用。
CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。因为只有程序的一部分加入到内存中,所以会出现所寻找的地址不在内存中的情况(CPU产生缺页异常),如果在内存不足的情况下,就会通过页面调度算法来将内存中的页面置换出来,然后将在外存中的页面加入到内存中,使程序继续正常运行。
可以看出,虚拟地址空间和虚拟内存的一个关键的因素是MMU(内存管理单元)。
现在一个程序的执行现在可以分为3部分:
1:CPU需要执行的语句的虚拟地址。
2:程序装入内存的部分
3:程序再外存中的部分
因为CPU是通过时间片轮转的方法使不同的程序并发执行的。所以在某一时刻只有一个程序占据CPU资源,CPU的最大的寻址空间为4G,所以说可以将每个程序可以看做独立占据4G的内存(只是可以看成,但是它并没有占据实际的4G内存)。而CPU是将虚拟地址空间里面的代码执行,如果在内存中寻找不到所需要的页面,就需要到外存中寻找,外存的这一部分,我们可以当成内存来使用,这也就是虚拟内存。虚拟地址空间不等于虚拟内存。虚拟地址空间是一个空间,不是真正存在的,只是通过CPU的寻址虚拟出来的一个范围。而虚拟内存是实实在在的硬盘的空间。
我看到了一个比较形象的比喻,假设4G个门牌号(4G的虚拟地址空间,并将这4G的虚拟空间进行分页),但是房子的数量(内存)少于门牌号的数量(4G的虚拟空间),那样就先把每个房子(内存)上挂一个门牌号(页),如果你要找一个门牌号(页),就需要查找每个房子(内存),如果这个门牌号没有挂(页还没有被加入到内存中),那么就将一个房子的门牌号(页面置换到外存)摘下来,把你找的那个门牌号挂上(将外存的页加入内存),这样就找到了需要的门牌号(页)。
这是我将这个比喻综合了一下。CPU只需要说找哪一个页面,MMU就将这个页面翻译成物理地址,再通过页面调度机制来讲不在内存中的页加入到内存中。
我认为计算机使用的是一种各司其职的方法。
CPU老大只需要要虚拟地址中的一页,范围在0x00000000到0xFFFFFFFF,因为他的地址总线是32位,4G是他最大的能力,然后他就把任务分配给他的手下,CPU不需要知道他的手下是如何找到这一页,他只负责去要这一页和执行这些代码,然后他就和他的手下说“有招想去,没招死去”,他的手下必须要能找到这一页,然后内存非常有限,而CPU不管这个,只需要你能找到这一页让我执行就好,所以CPU的手下就将硬盘中的一部分当做内存,然后拿来骗CPU说,“这是我从内存中找到的”,然后CPU就去运行。如果访问的地方实在是不能找到,或者是没有权限,那么这个程序就真死了。程序员在开发的时候,因为程序员所编写的代码最终是要让CPU去执行,所以程序员也理所应当认为我有4G的内存空间,程序员把程序交给CPU,CPU就交给他手下。
所以说,他们每一层都不需要管对方是如何办到的,他们在乎的只有结果。他们各司其职,CPU认为我有4G内存空间的原因是因为CPU的地址总线为32位,最大的寻址能力是4G,而内存没有这个大,所以它想出来的办法是将硬盘的一部分拿来骗CPU,并给这块骗人的地方起了一个好名字,虚拟内存。这就是我的理解中的虚拟内存空间和虚拟内存的概念。
自身水平有限,希望大家矫正,谢谢!
==================================================================================================================================================================================================================
程序访问存储器所使用的逻辑地址称为虚拟地址,虚拟地址 (virtual address): 4G虚拟地址空间中的地址,程序中使用的都是虚拟地址。每一个进程都分配有一个4G的虚拟地址。通过虚拟地址访问内存的形式称为保护模式,因为它不允许直接访问内核空间,而对应的直接访问物理内存的方式称为实模式,现在已经很少使用。 比如 mov eax,004227b8h ,这是把地址004227b8h处的值赋给寄存器的汇编代码,004227b8这个地址就是虚拟址。CPU在执行这行代码时,发现寄存器中的分页标志位已经被设定,就自动完成虚拟地址到物理地址的转换,使用物理地址取出值,完成指令。
使用了分页机制之后,4G的地址空间被分成了固定大小的页,每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件中,或者没有映射任何东西。CPU用来把虚拟地址转换成物理地址的信息存放在叫做页目录和页表的结构里,使用cpu中的MMU(内存管理单元)转换。
由于每个进程有自己的页目录和页表,所以每个进程的地址空间映射的物理内存是不一样的。两个进程的同一个虚拟地址处(如果都有物理内存映射)的值一般是不同的,因为他们往往对应不同的物理页。
而虚拟内存和虚拟地址几乎没有关系,前者就是拿硬盘空间当做内存空间欺骗进程。
===============================================================================================================================================================================================================================================================================================================
32位机器,每个程序有4G的虚拟地址空间。大致分为4块,从低地址到高地址依次是:NULL区,用户区,隔离区,核心区。用户私有的数据都在用户区(当然这个区里又可以细分,其中也包括一部分可以共享的内容),系统内核等东西都在核心区。
====================================================================================================================================================================================================================================================================================================================================================================================================================
SWAP就是LINUX下的虚拟内存分区,它的作用是在物理内存使用完之后,将磁盘空间(也就是SWAP分区)虚拟成内存来使用.它和Windows系统的交换文件作用类似,但是它是一段连续的磁盘空间,并且对用户不可见。
===============================================================================================================================================================================================================================================================================================================
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如Windows家族的“虚拟内存”;Linux的“交换空间”等。
====================================================================================================================================================================================================================================================================================================================================================================================================================
如果函数接口有指针参数,既可以把指针所指向的数据传给函数使用(称为传入参数),也可以由函数填充指针所指的内存空间,传回给调用者使用(称为传出参数),例如strcpy的函数原型为
- char *strcpy(char *dest, const char *src);
其中src参数是传入参数,dest参数是传出参数。有些函数的指针参数同时担当了这两种角色,如select函数。其函数原型为:
- int select(int nfds, fd_set *readfds,fd_set *writefds,
- fd_set *exceptfds, struct timeval*timeout);
其中的fd_set *参数,既是传入参数又是传出参数,这称为Value-result参数。
传入参数示例:
假如我们实现一个函数,其参数通过地址来传入一个值,其原型如下:
- void func(const unit_t *p);
其调用者与实现者之间的协议如下:
调用者 | 实现者 |
|
1、分配p所指的内存空间; 2、在p所指的内存空间中保存数据; 3、调用函数; 4、由于有const限定符,调用者可以确信p所指的内存空间不会被改变。 |
1、规定指针参数的类型unit_t *; 2、读取p所指的内存空间。 |
传出参数示例:
假如我们实现一个函数,其参数通过地址传出一个值,其原型如下:
- void func(unit_t *p);
其调用者与实现者之间的协议如下:
调用者 | 实现者 |
|
1、分配p所指的内存空间 2、调用函数 3、读取p所指的内存空间 |
1、规定指针参数的类型unit_t * 2、在p所指的内存空间中保存数据
|
Value-result参数示例:
- void func(unit_t *p);
其调用者与实现者之间的协议如下:
调用者 | 实现者 |
|
1、分配p所指的内存空间 2、在p所指的内存空间保存数据 3、调用函数 4、读取p所指的内存空间 |
1、规定指针参数的类型unit_t * 2、读取p所指的内存空间 3、改写p所指的内存空间 |
注意:由于传出参数和Value-result参数的函数接口完全相同,应该在文档中说明是哪种参数。
很多系统函数对于指针参数是NULL的情况有特殊规定:如果传入参数是NULL表示取缺省值,例如pthread_create(3)的pthread_attr_t *参数,也可能表示不做特别处理,例如free的参数;如果传出参数是NULL表示调用者不需要传出值,例如time(2)的参数。这些特殊规定应该在文档中写清楚。
下面是一个传出参数的完整例子:
- /* populator.h */
- #ifndef POPULATOR_H
- #define POPULATOR_H
- typedef struct {
- int number;
- char msg[20];
- } unit_t;
- extern void set_unit(unit_t *);
- #endif
- /* populator.c */
- #include <string.h>
- #include "populator.h"
- void set_unit(unit_t *p)
- {
- if (p == NULL)
- return; /* ignore NULL parameter */
- p->number = 3;
- strcpy(p->msg, "Hello World!");
- }
- /* main.c */
- #include <stdio.h>
- #include "populator.h"
- int main(void)
- {
- unit_t u;
- set_unit(&u);
- printf("number: %d\nmsg: %s\n", u.number, u.msg);
- return 0;
- }
二级指针的参数:
二级指针也是指针,同样可以表示传入参数、传出参数或者Value-result参数,只不过该参数所指的内存空间应该解释成一个指针变量。用两层指针做传出参数的系统函数也很常见,比如pthread_join(3)的void **参数。下面看一个简单的例子。
二级指针做传出参数
- /* redirect_ptr.h */
- #ifndef REDIRECT_PTR_H
- #define REDIRECT_PTR_H
- extern void get_a_day(const char **);
- #endif
这里的参数指针是const char **,有const限定符,却不是传入参数而是传出参数。
- /* redirect_ptr.c */
- #include "redirect_ptr.h"
- static const char *msg[] ={"Sunday", "Monday", "Tuesday","Wednesday",
- "Thursday","Friday", "Saturday"};
- void get_a_day(const char **pp)
- {
- static int i = 0;
- *pp = msg[i%7];
- i++;
- }
- /* main.c */
- #include <stdio.h>
- #include "redirect_ptr.h"
- int main(void)
- {
- const char *firstday = NULL;
- const char *secondday = NULL;
- get_a_day(&firstday);
- get_a_day(&secondday);
- printf("%s\t%s\n", firstday, secondday);
- return 0;
- }
二级指针作为传出参数还有一种特别的用法,可以在函数中分配内存,调用者通过传出参数取得指向该内存的指针,一般来说,实现一个分配内存的函数就要实现一个释放内存的函数。
通过参数分配内存示例:
- void alloc_unit(unit_t **pp);
- void free_unit(unit_t *p);
其调用者与实现者之间的协议如下:
调用者 | 实现者 |
|
1、分配pp所指的指针变量的空间; 2、调用alloc_unit分配内存; 3、读取pp所指的指针变量,通过后者使用alloc_unit分配的内存; 4、调用free_unit释放内存。 |
1、规定指针参数的类型unit_t **; 2、alloc_unit分配unit_t的内存并初始化,为pp所指的指针变量赋值; 3、free_unit释放在alloc_unit中分配的内存 |
下面是一个通过二级指针参数分配内存的例子
- /* para_allocator.h */
- #ifndef PARA_ALLOCATOR_H
- #define PARA_ALLOCATOR_H
- typedef struct {
- int number;
- char *msg;
- } unit_t;
- extern void alloc_unit(unit_t **);
- extern void free_unit(unit_t *);
- #endif
- /* para_allocator.c */
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include "para_allocator.h"
- void alloc_unit(unit_t **pp)
- {
- unit_t *p = malloc(sizeof(unit_t));
- if(p == NULL) {
- printf("out of memory\n");
- exit(1);
- }
- p->number = 3;
- p->msg = malloc(20);
- strcpy(p->msg, "Hello World!");
- *pp = p;
- }
- void free_unit(unit_t *p)
- {
- free(p->msg);
- free(p);
- }
- /* main.c */
- #include <stdio.h>
- #include "para_allocator.h"
- int main(void)
- {
- unit_t *p = NULL;
- alloc_unit(&p);
- printf("number: %d\nmsg: %s\n", p->number, p->msg);
- free_unit(p);
- p = NULL;
- return 0;
- }
二级指针参数如果是传出的,可以有两种情况:
第一种情况,传出的指针指向静态内存(比如上面的例子),或者指向已分配的动态内存(比如指向某个链表的节点);
第二种情况是在函数中动态分配内存,然后传出的指针指向这块内存空间,这种情况下调用者应该在使用内存之后调用释放内存的函数,调用者的责任是请求分配和请求释放内存,实现者的责任是完成分配内存和释放内存的操作。由于这两种情况的函数接口相同,我们在撰写文档或添加注释时应该说明是哪一种情况。
返回值是指针的情况
返回值显然是传出的而不是传入的,如果返回值传出的是指针,和通过参数传出指针的情况类似,也分为两种情况:
第一种是传出指向静态内存或已分配的动态内存的指针;
第二种是在函数中动态分配内存并传出指向这块内存的指针,这种情况通常还要实现一个释放内存的函数,所以有和malloc对应的free。由于这两种情况的函数接口相同,应该在文档中说明是哪一种情况。
返回指向已分配内存的指针示例:
- unit_t *func(void);
其调用者与实现者之间的协议如下:
调用者 | 实现者 |
|
1、调用函数 2、将返回值保存下来以备后用 |
1、规定返回值指针的类型unit_t * 2、返回一个指针 |
下面的例子演示返回指向已分配内存的指针
- /* ret_ptr.h */
- #ifndef RET_PTR_H
- #define RET_PTR_H
- extern char *get_a_day(int idx);
- #endif
- /* ret_ptr.c */
- #include <string.h>
- #include "ret_ptr.h"
- static const char *msg[] = {"Sunday","Monday", "Tuesday", "Wednesday",
- "Thursday","Friday", "Saturday"};
- char *get_a_day(int idx)
- {
- return msg[idx];
- }
- /* main.c */
- #include <stdio.h>
- #include "ret_ptr.h"
- int main(void)
- {
- printf("%s %s\n", get_a_day(0));
- return 0;
- }
动态分配内存并返回指针示例:
- unit_t *alloc_unit(void); voidfree_unit(unit_t *p);
其调用者与实现者之间的协议如下:
调用者 | 实现者 |
|
1、调用alloc_unit分配内存; 2、将返回值保存下来以备后用; 3、调用free_unit释放内存。
|
1、规定返回值指针的类型unit_t *; 2、alloc_unit分配内存并返回指向该内存的指针; 3、free_unit释放由alloc_unit分配的内存。
|
以下是一个完整动态分配内存并返回指针的例子
- /* ret_allocator.h */
- #ifndef RET_ALLOCATOR_H
- #define RET_ALLOCATOR_H
- typedef struct {
- int number;
- char *msg;
- } unit_t;
- extern unit_t *alloc_unit(void);
- extern void free_unit(unit_t *);
- #endif
- /* ret_allocator.c */
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include "ret_allocator.h"
- unit_t *alloc_unit(void)
- {
- unit_t *p = malloc(sizeof(unit_t));
- if(p == NULL) {
- printf("out of memory\n");
- exit(1);
- }
- p->number = 3;
- p->msg = malloc(20);
- strcpy(p->msg, "Hello world!");
- return p;
- }
- void free_unit(unit_t *p)
- {
- free(p->msg);
- free(p);
- }
- /* main.c */
- #include <stdio.h>
- #include "ret_allocator.h"
- int main(void)
- {
- unit_t *p = alloc_unit();
- printf("number: %d\nmsg: %s\n", p->number, p->msg);
- free_unit(p);
- p = NULL;
- return 0;
- }