您的位置:首页 > 其它

关于内存的思考

2013-09-24 21:47 260 查看
1.虚拟内存

实际上,由于物理内存的限制,会限制应用程序在物理内存上的运行。因此,很早的时候,采用廉价但缓慢的磁盘来扩充昂贵但快速的内存。在任意时刻,程序被载入到物理内存中,有一段时间未使用的物理内存,可能会被移往硬盘中,而因此节省下来的物理内存载入其他的数据。但在早期,这个数据从内存的移出移进是由程序员操作的,必须花费大量的精力去追踪任一时刻那些数据在物理内存中,那些时刻在硬盘中,并根据需要做切换。

因此,虚拟内存实际上是这种多层存储的扩充。使用磁盘来保存进程的映像,SunOS中进程执行于32位地址空间,OS负责具体细节,使每个进程都以为自己拥有整个地址空间的独家访问权。所有进程共享机器的物理内存,当内存用完时就用硬盘保存数据。内存管理硬件MMU把虚拟地址翻译成物理地址,并让一个进程始终运行于系统的真正内存中。



虚拟内存通过页的形式组织,页就是操作系统在硬盘和内存之间移来移去或进行保护的单位,一般为几KB。从潜在的可能性上来说,与进程有关的所有内存都将被系统所使用,如果该进程因为优先级,状态等因素导致不在运行,OS可以暂时收回所有分配给该进程的物理内存资源,将该进程的所有相关信息都备份到磁盘上。在磁盘上有一个特殊的交换区,用于保存这些进程的状态信息。在一台机器中,交换区一般是物理内存的几倍。Sunos常驻于内存中,只有用户进程才会被换进换出。

进程只能操作位于物理内存的页面,当进程引用一个不在物理内存中的页面时,MMU将产生一个页错误,内核对此事件做出响应,并判断该引用是否有效,如果有效,则从磁盘取回该页,换入到内存,否则,发出一个段违规信号,一旦页面进入内存,进程便被解锁,可以重新运行,SunOS对于硬盘的文件系统和主存使用相同的底层数据结构,因此虚拟内存的移进移出,是在文件区域到内存区域的映射。

帽子层,驱动MMU的硬件地址翻译软件。

2.Cache 存储器

Cache 是多层存储概念的更深扩展,其特点是:容量小,速度极快,价格高,位于CPU和内存之间,是一种极快的存储缓冲区,从MMU的角度看,有些机器的Cache属于CPU,此时,Cache使用的是虚拟地址,在每次进程切换时,它的内容必须刷新。有的Cache从MMU单元看,是属于物理内存一侧的,此时Cache使用的是物理地址,这就容易使得多个CPU共享同一个Cache。



当数据从内存读入时,整“行”的数据被装入Cache,一般是16B或32B,如果程序具有良好的地址引用局部性,那么CPU以后对邻近数据的引用就可以从快速的Cache读取,而不用从缓慢的内存中读取。Cache的操作周期和系统一样。在典型情况下,主存的存取速度可能只有它的四分之一,但因为其体积很大,成本很高,因此常作为附加存储系统。

Cache包含一个地址的列表以及它们的内容,随着处理器不断引用新的内存地址,Cache的地址列表也一直处于变化中,所有对内存的读写,都要经过Cache

当处理器需要从一个指定地址提取数据时,这个请求先交给cache,如果数据已经存在于cache中,则它立即被提取,否则,cache请求内存,内存的读取以行为单位,在读取的同时,也装入cache.这样,CPU下次访问临近数据的时候,可以直接从cache中获取,也就是命中cache.如果你的程序每次都无法命中cache,那么程序的性能比不采用cache还要差。大多数CPU对cache的命中在90%以上。cache有两种类型:

全写----->每次写入cache时,同时写入内存。使两者保持统一。

写回---->当第一次写入时,只写入cache,如果已经写入的cache行再次需要写入,此时第一次写入的结果尚未保存,所以要先把它写入到内存中。当内核切换进程时,Cache中的所有数据也要都写入内存中。

两种情况下,一旦对cache的访问结束,指令流都将继续执行,不用等待缓慢的内存操作全部完成。

#define DUMBCOPY for (i = 0; i < 65536; ++i) \

destination[i] = source[i]

#define SMARTCOPY memcpy(destination, source, 65536)

两种拷贝方式,性能差距在7倍左右。。。。。原因如下:

因为src和dest的大小正好是cache容量的整数倍,由于cache行的填充算法,导致src和dest不可能同时出现在cache中,但此时他们使用相同的cache行,导致每次对内存的引用都无法命中cache,使CPU的利用率大大下降,因为它不得不等待常规的内存操作完成,而memcpy函数经过特别优化,它把先读取一个cache行,再对它进行写入这个循环分开来,大大提高性能。

cache行:

就是对cache进行访问的单位,每行由两部分组成:一个数据部分以及一个标签,用于指定它所代表的地址。

cache块:即行中的数据部分,保存来回移动于cache行和内存之间的字节数据,一个典型的块为32B、

一个cache行的内容代表特定的内存块,如果处理器试图访问属于该块地址范围的内存,它就会做出反应,自然比内存快。

一个cache由许多行组成。

3. 数据段和堆

堆是数据段中可自动增长的区域,通过指针访问,堆中所有东西都是匿名的,所以只能用指针间接访问。从堆中获取内存的唯一方法是malloc等库函数。

calloc与malloc类似,但分配内存后,清零。

relloc可以改变空间大小。

对内存的回收不必与其他分配的顺序一致,甚至可以不回收。堆的末端有一个称为break的指针来标示。当堆管理器需要更多的内存时,brk,sbrk可以移动break指针。一般用户不需要主动调用。

另外,被分配的内存总是经过对齐的,以适合机器上最大尺寸的原子访问,一个malloc请求申请的内存大小为方便起见一般被圆整为2的乘方,回收的内存可供重新使用,但没有(方便的)办法把它从你的进程中移除交换给操作系统。

关于内存泄露:

之所以称为内存泄露,是因为一种稀有的资源正在被一个进程榨干。因为发生泄露的进程,在换进换出的时,会消耗更大的资源,也更有可能被换出。即使泄露的内存本身并不被引用,但它仍可能存在于页面中,内容自然是垃圾,这样就增加了进程的工作页数量,降低了性能。另外,泄露的内存往往比忘记释放的数据结构要大,因为malloc函数在分配空间的时候会圆整为2的乘方,在资源有限的情况下,即使引起内存泄露的进程并不运行,整个系统的运行速度也会被拖慢。从理论上说,进程的空间有个上限值,实际上,在进程空间还没远未达到上限值时,磁盘的交换区早被消耗尽。

4.关于总线错误和段错误

实际上,总线错误几乎都是由于未对齐的读写操作引起的。之所以称为总线错误,是因为出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。也就是说数据项只能存储在地址是数据项大小的整数倍的内存位置上。RISC架构下,都需要数据对齐,否则由于额外逻辑使得整个系统变慢。通过迫使每个内存访问都局限在一个cache行或者一个单独的内存页,可加速cache和内存管理单元等这些硬件。其实,也就是说,用内存对齐这个说法,表达了禁止内存跨页访问这个意思。页和cache的大小是经过精心设计的,只要遵循对其规则,就可以保证一个原子数据项不会跨越一个页或者cache的边界。

给个例子:

union

{

char a[10];

int i;

}u;

int *p = (int *)&(u.a[1]);

*p = 17;

这将导致一个总线错误(其实跟CPU有关,一般的PC,该程序正常)。因为,联合u确保了数组a是按照int类型的4字节对齐的,所以,a[1]的地址肯定没有按int对齐,然后,我们试图往这个地址写4个字节的数据。但这个访问只是按照单字节对齐的。这个就 违反了规则。

在内存中编译器自动分配和填充来进行对齐,但在硬盘和磁带上,并没有这样的要求,所以可以任意使用,但当将char转换成int指针时,就会出现总线错误。

段错误或者段违规:

段错误是由于内存管理单元的异常所致,该异常通常是由于解除引用一个未初始化或非法值的指针引起的。

如果指针引用一个不在地址空间中的地址,OS就会干涉,通常导致段错误的原因有:

1.解除引用一个包含非法值的指针。

2.解除引用一个空指针。

常常由于从系统程序中返回空指针,未经检查就使用。

3.在未得到正确的权限时进行访问。如试图往一个只读的文本段存储值就会引起段错误。

4.用完了堆栈或者堆空间。

另外,如果未初始化的指针恰好有未对其的值,它将会产生总线错误,而不是段错误。

在绝大多数架构,绝大多数情况下,总线错误意味着CPU对进程引用内存的一些不满,段错误则是MMU对进程引用内存的不满。

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

1.坏指针值错误

例如未赋值就引用内存,向库函数传递坏指针,以及释放后又访问指针内容,可以在释放后,将其置空,即:free(p);p = NULL;这样,如果在指针释放后继续使用指针,至少程序能在终止之前进行信息转储。

2.改写错误

数组越界,动态内存两边写入数据。

3.指针释放引起的错误

例如释放两次,释放未使用malloc函数分配的内存,或释放正在使用的内存,或释放一个无效指针。

在遍历链表时正确释放元素的方法是使用临时变量存储下一个元素的地址,这样就可以安全的在任何时候释放当前元素,不必担心在取下一个元素的地址时还要引用它,代码如下:

struct node *p, *start, *tmp;

for (p = start; p; p = tmp)

{

tmp = p->next;

free(p);

}


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: