C专家编程(学习笔记)_第7章 对内存的思考

1.Intel 80x86系列

     4004(4位)—>8008(8位)—>8080(8位)—>8085(8位)—>8086(16位)—>80186(16位)—>80286(32位)—>80386(32位)—>80486(32位)—>Pentium(32位)—>P6 64位?——>......

2.Intel 80x86内存模型以及它的工作原理

在UNIX中,段就是一块以二进制形式出现的相关内容。

在Intel 80x86内存模型中,段是内存模型设计的结果,在80x86的内存模型中,各处理器的地址空间并不一致(因为要保持兼容性),但它们都被分割成以64KB为单位的区域,每个这样的区域便称为段。作为80x86内存模型最基本的形式,8086中的段是一块64KB的内存区域,由一个段寄存器所指向。内存地址的形成经过是:取得段寄存器的值,左移4位(相当于乘上16),或者换种思路,把段寄存器的值看成是20位的,也就是在值的右边扩充4个0。然后就是16位偏移地址,它表示段内的地址。如果把段内寄存器的值(经过移位)加上偏移地址,就得到最终的地址。注意:正如两个数加起来等于24的例子有很多一样,不同的段地址加上偏移地址所形成的值可能指向同一个内存地址

     今天,计算机系统结构的真正挑战不在于内存容量,而是内存的速度。软件实际上受到磁盘和内存的等待时间(访问时间)的限制,准确的说在内存和CPU性能之间存在一道很深的鸿沟。在巨型地址空间的机器中,主存访问时间的重要性能将进一步凸显,当访问海量数据时,它所耗费的内存访问时间将左右软件的性能,我们只能寄希望未来看到Cache以及相关技术的更广泛使用。

PC的内存模型,Microsoft C认可下面几种内存模型:

small   所有的指针都是16位,代码和数据都限定在一个单一的段中,程序最大规模为128KB(代码段和数据段各64KB).
large   所有的指针都为32位,程序可以包含许多个64KB的段.
medium 函数指针为32位,所以代码可能段可能有多个,数据指针为16位,所以只有一个64KB的数据段.
compact medium的另一种形式:函数指针为16位,所以代码最多不超过64KB,数据指针为32位,所以数据可以占据多个段,但堆栈里的
数据仍限制在一个64KB的段内.

Microsoft C认可下面这些非标准的关键字,当它们应用于对象指针或函数指针时,只是覆盖相应类型的指针
__near 16位指针
__far  32位指针,但它所指向的对象必须全部位于同一个段中(所有的对象均不得超过64KB)
__huge 32位指针,上述所有对段的限制都不存在

3.虚拟内存

如果它存在,而且你能看见它————它是真实的(real)
如果它不存在,但你能看见它————它是虚拟的(virtual)
如果它存在,但你看不见它————它是透明的(transparent)
如果它不存在,而且你也看不见它————那肯定是你把它擦掉了
                           ————————IBM用于解释虚拟内存的张贴画,大约是在1978年

       让程序安装在机器上的物理内存数量的限制是非常不便的,很早的时候,在计算机领域中人们就提出了虚拟内存的概念,目的就是为了去除这个限制。它的基本思路是用廉价但缓慢的磁盘来扩充快速却昂贵的内存。在任一给定时刻,程序实际需要使用的虚拟内存区段的内容就被载入物理内存中。当物理内存中的数据有一段时间未被使用,它们就可能被转移到磁盘中,节省下来的物理内存空间用于载入需要使用的其它数据。

慢速访问<——————————————————————————————————快速访问
  磁带     磁盘     内存     Cache存储器    CPU寄存器
成本低、容量大——————————————————————————>成本高、容量小

       虚拟内存通过“页”的形式组织。页就是操作系统在磁盘和内存之间移来移去或进行保护的单位,一般为几K字节。可以通过键入/usr/ucb/pagesize来观察你的系统中的页面大小。当内存的映射在磁盘和物理内存间来回移动是,称它们是page in(移入内存)或page out(移到磁盘)。

       如果某进程可能不会马上运行(可能它的优先级低,也可能是它处于睡眠状态),操作系统可以暂时取回所有分配给它的物理内存资源,将该进程的所有相关信息都备份到磁盘上。这样,这个进程就被“换出”。在磁盘中有一个特殊的“交换区”,用于保存从内存中换出的进程。在一台机器中,交换区的大小一般是物理内存的几倍。只有用户进程才会被换进换出,SunOs内核常驻与于内存中。

       进程只能操作位于物理内存中的页面。当进程引用一个物理内存中的页面时,MMU就会产生一个页错误,内核对此事件作出响应,并判断该引用是否有效。SunOS对于磁盘的文件系统和主存有一种统一的观点,操作系统使用相同的底层数据结构(vnode,或称"虚拟节点")来操纵这两者。虚拟内存现已成为一项操作系统中不可或缺的技术,它允许多个进程运行于较小的物理内存中。

4.Cache存储器

       Cache存储器是多层存储概念的更深扩展,它的特点是容量小、价格高、速度快。Cache位于CPU和内存之间,是一种极快的存储缓冲区。从内存管理单元(MMU)的角度看,有些机器的Cache属于CPU一侧的,也有一些机器的Cache从MMU的角度看是属于物理内存一侧的。当数据从内存读入时,整“行”(一般16或32个字节)的数据被装入Cache,如果程序具有良好的地址引用局部性,那么CPU以后对邻近数据的引用就可以从快速的Cache读取,而不用从缓慢的内存中读取。

                   CPU
                    ↑
  虚拟地址————> Cache存储器
                    ↓
               内存管理单元(MMU)————>物理地址——》物理内存

              Cache存储器的基本知识

      如果你的程序的行为颇为怪异,以致每次都无法命中Cache,那么程序的性能比不采用Cache还要差,因为每次判断Cache是否命中的额外逻辑并不是免费的午餐,Sun当前使用两种类型的Cache:(1)全写法Cache——每次写入Cache时总是同时写入到内存中,使用内存和Cache保持一致;(2)写回法Cache——当第一次写入时,只对Cache进行写入。

Cache的组成
行(line)  行就是对Cache进行访问的单位,每行由两部分组成:一个数据部分以及一个标签
块(block) 一个Cache行内的数据被称为块,块保存来回移动与Cache行和内存之间的字节数据,一个典型的块为32字节
Cache     一个Cache由许多行组成,有时也使用相关硬件来加速对标签的访问。为了提高速度,Cache的位置离CPU很近且内存系统和总线经过高度优化

 5.数据段和堆

       就像堆栈段能够根据需要自动增长,数据段也包含了一个对象,用于完成这项工作,这就是堆(heap)。堆区域用于动态分配的存储,也就是通过malloc(内存分配)函数获得的内存,并通过指针访问。堆中的所有东西都是匿名——不能按照名字直接访问,只能通过指针间接访问。堆内存的回收不必与它所分配的顺序一致(它甚至可以不回收),所以无序的malloc/free最终会产生堆碎片。被分配的内存总是经过对齐,以适合机器上最大尺寸的原子访问,一个malloc请求申请的内存大小为方便起见一般被整为2的乘方。堆的末端由一个称为break的指针来标识,当堆管理器需要更多内存时,它可以通过调用brk和sbrk来移动break指针。用于内存管理的调用是:(1)malloc和free——从堆中获得内存以及把内存返回给堆;(2)brk和sbrk——调整数据段的大小至一个绝对值(通过某个增量);

       警告:你的程序可能无法同时调用malloc()和brk()。如果你使用malloc,malloc希望当你调用brk和sbrk时,它具有唯一的控制权。由于sbrk向进程提供了唯一的方法将数据段内存返回给系统内核,所以如果使用了malloc,就有效地防止了程序的数据段缩小的可能性。要想获得以后能够返回给系统内核的内存,可以使用mmap系统调用用来映射/dev/zero文件。需要返回这种内存时,可以使用munmap系统调用。

6.内存泄露

       堆经常会出现两种类型的问题:(1)释放或改写仍在使用的内存(称为"内存损坏");(2)未释放不再使用的内存(称为'"内存泄露")。

       内存泄露的主要可见症状就是罪魁进程的速度会减慢,原因是体积大的进程更有可能被系统换出,让别的进程运行,而且大的进程在换进换出时花费的时间也更多。

      观察内存泄露是一个两步骤的过程,首先使用swap命名观察还有多少可用的交换空间:/usr/sbin/swap -s;第二个步骤就是确定可疑进程,看看它是不是该为内存泄露负责;

      操作系统内核同时动态管理它的内存使用,内核中许多数据表是动态分配的,所以预先没有固定的限制,如果一个内核进程错误引起内存泄露,机器的速度便会慢下来,有时机器干脆挂起或甚至不知所措。

7.总线错误

       7.1总线错误

       总线错误几乎都是由于未对齐的读或写引起的。它之所以称为总线错误,是因为出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。对齐的意思就是数据项只能存储在地址是数据项大小的整数倍的内存位置上。编译器通过自动分配和填充数据(在内存中)来进行对齐。以前奇偶校验就有可能产生总线错误。

      7.2段错误

      段错误是由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常则通常是由于解除引用一个未初始化或非法值的指针引起的。

通常导致段错误的几个直接原因:
(1)解除引用一个包含非法值的指针;
(2)解除引用一个空指针;
(3)在未得到正确的权限时进行访问;
(4)用完了堆栈或堆空间(虚拟内存虽然巨大但绝非无限);
在绝大多数架构的绝大多数情况下,总线错误意味着CPU对进程引用内存的一些做法不满,而段错误则是MMU对进程引用内存的一些情况发出的抱怨.

已发生频率为序,最终可能导致段错误的常见编程错误是:

1.坏指针值错误:在指针赋值之前就用它来引用内存,或者向库函数传送一个坏指针。第三种可能导致坏指针的原因对指针进行释放之后再访问它的
内容。可以修改free语句,在指针释放之后再将它置为空值。
free(p);  p =NULL;
这样,如果再指针释放之后继续使用该指针,至少程序能终止之前进行信息存储。
2.改写(overwrite)错误:越过数组边界写入数据,在动态分配的内存两端之外写入数据,或改写一些堆管理数据结构.
p=malloc(256); p[-1]=0;p[256]=0;
3.指针释放引起的错误:释放同一个内存块两次,或释放一块未曾使用malloc分配的内存,或释放仍在使用中的内存,或释放一个无效的指针.

如何在链表中释放元素:

在遍历链表时正确释放元素的方法是使用临时变量存储下一个元素的地址:
struct node *p,*start,*tmp;
for(p=start;p;p=tmp){
    tmp=p->next;
    free(p);
}
规则:
1.每个进程用于几百万的“字节”;
2.字节存放于“页”中,每页4096个字节,位于同一页上的字节具有“本地引入”关系;
3.页可以存放在内存中,也可以存放在磁盘中,内存一般不够大,无法容纳所有的页;
4.总共只有一块内存,但可以有几个磁盘,所有进程共享内存和磁盘;
5.每个字节都有自己的“虚拟地址”;
6.进程可以对一个字节进行“引用操作”。每个进程轮流进行引用操作;
7.每个进程只能引用自己的字节,不能引用其进程的字节;
8.字节只有当它们位于内存中时才能被引用;
9.只有"虚拟内存管理器"知道某个字节位于内存还是位于磁盘;
10.一个字节不被引用的时间越长,它就被称为越“旧”;
11.进程必须通过虚拟内存管理器得到字节。它所给的字节数量是2的倍数或乘方数,它有助于减少开销;
12.进程引用字节的方法就是给出它的虚拟地址;
13.每个进程拥有的字节的虚拟地址与其他进程一样;
说明:
1.根据传统,虚拟内存管理器使用一张很大且分段的表,另外还有“页表”用于记住所有字节的位置以及它们的主人;
2.规则13的一个结果就是各次运行中每位进程的虚拟地址都类似,即使进程的数量有所变化;
3.虚拟内存管理器也拥有自己的一些自己,它们中的有些也和一般进程的字节一样在内存和磁盘中移来移去,但是它的有些字节使用频率非常之高,
所以常驻内存;
4.按照上述规则,经常被引用的字节更有可能被存放在内存中,而不太被引用的字节则更可能被存放在磁盘中,这可以提高内存的使用效率;

虚拟内存万岁!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值