zoukankan      html  css  js  c++  java
  • 深入理解计算机系统9——虚拟存储器

    虚拟存储器

    一个系统中的进程是与其他进程共享CPU和主存资源的。共享主存会形成一些特殊的挑战。

    随着对CPU需求的增长,进程以某种合理的平滑方式慢了下来。

    但是如果太多的进程需要太多的存储器,那么它们中的一些根本就无法运行。

    存储器还很容易被破坏。如果某个进程不小心写了另一个进程使用的存储器,它就可能以某种完全和程序逻辑无关的令人迷惑的方式失败。

    为了更加有效管理存储器并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟存储器(VM)。 

    虚拟存储器为每个进程提供了一个大的、一致的和私有的地址空间。

    通过一个很清晰的机制,虚拟存储器提供了三个重要能力:

      1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。

      2)它为每个进程提供了一致的地址空间,从而简化了存储器管理;

      3)保护了每个进程的地址空间不被其他进程破坏;

    虚拟存储器是计算机系统最重要的概念之一。它的成功在于它是沉默地、自动地工作着,不需要应用程序员的任何干涉。

    既然虚拟存储器在幕后工作得如此之好,为什么程序员还要理解它,原因有以下几个:

    1)虚拟存储器是中心的

      虚拟存储器遍及计算机系统的所有层面,理解虚拟存储器将帮助更好地理解系统通常是如何工作的。

    2)虚拟存储器是强大的

      虚拟存储器给予应用程序强大的能力。可以创建和销毁存储器的片(chunk),将存储器映射到磁盘文件的某个部分,以及与其他进程共享存储器。

    3)虚拟存储器是危险的

      每次应用程序引用一个变量、间接引用一个指针,或者调用一个诸如malloc这样的动态分配程序时,它就会和虚拟存储器发生交互。如果虚拟存储器使用不当,应用将遇到复杂危险的与存储器有关的错误。例如,一个带有错误指针的程序可以立即崩溃于“段错误”或者“保护错误”,它可能在崩溃之前还默默地运行了几个小时,或者是最令人惊慌地,运行完成却产生不正确的结果。

    这篇文章主要讲述两方面

    1)虚拟存储器是如何工作的;

    2)应用程序如何使用和管理虚拟存储器;

    ===================================================

    1、物理和虚拟寻址

    计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。

    每字节都有唯一的物理地址(Physical Address)PA

    第一个字节的地址为0,接下来的字节地址为1,以此类推。

     

    CPU访问存储器最自然的方式就是使用物理地址。这种寻址方式被称为物理寻址

    当CPU执行一条加载指令时,它会生成一个有效的物理地址,通过存储器总线,把它传递给主存。

    主存取出从物理地址4处开始的4字节的字,并将它返回给CPU,CPU会将它存放在一个寄存器中。

     

    现代处理器使用的是一种称为虚拟寻址的寻址方式。 

    使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。

    将一个虚拟地址转换成物理地址的任务叫做地址翻译。CPU芯片上叫做MMU(Memory Management Unit)存储器管理单元的专用硬件会进行这个任务。

    地址翻译的任务需要硬件和操作系统紧密配合,MMU会利用放在主存上的查询表来动态翻译虚拟地址,该表的内容由操作系统来管理。

    ===================================================

    2、地址空间

    地址空间是一个非负整数地址的有序集合:{0,1,2,...}

    如果地址空间中的整数是连续的,那么我们说它是线性地址空间

    为了简化讨论,我们总是假设地址空间是线性地址空间。

    在带虚拟存储器的系统中,CPU从一个有N=2^n 个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。 

    一个地址空间的大小是由表示最大地址所需要的位数来描述的。

    例如,一个包含N=2^n个地址的虚拟地址空间叫做一个n位地址空间。

    现代系统典型地支持32位或者64位虚拟地址空间

    一个系统还有物理地址空间,它与系统中物理存储器的M个字节相对应:{0,1,2,...,M-1}

    M不要求是2的幂,但为了简化讨论,一般假设M=2^m。

    地址空间的概念非常重要。

    它清楚地区分了数据对象(字节)和它们的属性(地址)。

    主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

    ===================================================

    3、虚拟存储器作为缓存的工具

    虚拟存储器(VM)被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。

    每个字节都有唯一的地址。

    VM系统通过将虚拟存储器分割为称为虚拟页(Virtual Page VP)的大小固定的块来处理这个问题。

    每个虚拟页的大小为P=2^p个字节。 

    类似的,物理存储器被分割为物理页(Physical Page PP),大小也为P字节(物理页页被称为页帧(page frame))

     

    任意时刻,虚拟页面的集合分为三个不相交的子集。

    未分配的:未分配(或创建)的页没有绑定物理存储器上的块;不占用任何磁盘空间;

    缓存的:已经缓存在物理存储器中的已分配的页;

    未缓存的:已分配的页,但是没有缓存在物理存储器中;

    DRAM缓存的组织结构

    术语SRAM缓存来表示位于CPU和主存之间的L1、L2、L3高速缓存;高速缓存

    术语DRAM缓存来表示虚拟存储器系统的缓存,它在主存中缓存虚拟页;主存缓存

    SRAM比DRAM块大约10倍,DRAM比磁盘块大约100000倍;

    DRAM不命中,要由本地磁盘服务;

    SRAM不命中,要由DRAM服务;

    页表

     页表将虚拟页映射到物理页;

    每次地址翻译硬件将虚拟地址转换为物理地址时都会读取页表。

    操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。

    通过页表,虚拟存储器系统可以判定一个虚拟页是否存放在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页放在哪个物理页中。

    如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置。

    在物理存储中选择一个牺牲页,并将虚拟页从磁盘拷贝到DRAM中,替换这个牺牲页

    这些功能是由许多软硬件联合提供的,包括操作系统软件、MMU(存储器管理单元)中的地址翻译硬件和一个存放在物理存储器中的叫做页表的数据结构。

    页表是页表条目(PTE)的一个数组;

    每个PTE是由一个有效位和一个n位地址字段构成的。

    有效位表明当前虚拟页是否缓存在DRAM中。

    地址字段表明DRAM中相应的物理页起始位置。

    如果没有设置有效位,空地址表示虚拟页还没有被分配;否则这个地址指向虚拟页在磁盘上的起始位置。(虚拟页映射到磁盘中不叫缓存)

    关键词:分配、缓存、命中、虚拟、物理

    命中:是针对虚拟页有没有缓存而言(虚拟页有没有映射物理页到DRAM或SRAM);

    缓存:虚拟页映射到DRAM或SRAM中的物理页;

    分配:未分配的虚拟页不映射任何物理页(磁盘,DRAM、SRAM);

    页命中:

    地址翻译硬件将虚拟地址作为一个索引来定位PTE2,并从存储器中读取它。

    因为设置了有效位,那么地址翻译硬件就知道VP2是缓存在DRAM中

    它使用PTE中的物理存储器地址,构造出这个字的物理地址。

    缺页

    DRAM缓存不命中,称为缺页(page default)

    CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。

    地址翻译硬件从存储器中读取PT3,从有效位推断出VP3未被缓存,从而触发一个缺页异常。

    缺页异常调用内核中的缺页异常处理程序。该程序会选择一个牺牲页。

    这个牺牲页就是存放在PP3中的VP4。

    如果VP4被修改了,内核就会将PP3中的内容重新拷贝回磁盘。

    另外内核会修改VP4的页表条目,反映出VP4不在DRAM中缓存。

    被称为

    在磁盘和存储器(DRAM和SRAM)中传送页的活动叫做交换 或者 页面调度

    页从磁盘换入DRAM(页面调入)和从DRAM换出(页面调出)磁盘。

     

    直到有不命中发生时,才换入页面的策略叫做按需页面调度

    所有现代系统都是使用按需页面调度的方式。

    局部性

    一开始对于虚拟存储器的第一印象是,效率应该非常低。因为不命中的处罚很大,因为担心页面调度会破坏程序的性能。

    实际上虚拟存储器工作得相当好,这是得益于局部性。

    程序往往在一个较小的活动页面集合上工作,这个集合叫做工作集,或者叫常驻集。 

    初始开销完成后,后续的开销很小。

    但是有的情况是如果工作集的大小超过了物理存储器的大小,那么程序将产生一种不幸的状态,叫做颠簸

    可以利用Unix的GETrusage函数监测缺页的数量。

    ===================================================

    4、虚拟存储器作为存储器管理的工具

    虚拟地址是一个有用的机制,因为它大大地简化了存储器的管理,并提供了一种自然的保护存储器的方法

    实际上,操作系统为每个进程都提供了一个独立的页表,因为也就是一个独立的虚拟地址空间。

    按需页面调度独立的虚拟地址空间的结合,对系统中的存储器的使用和管理产生了深远的影响。

    VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。

    简化链接

      独立的地址空间允许进程的存储器映像使用相同的格式,而不需要管代码和数据存放在物理存储器的位置。

      这样大大简化了链接器的设计和实现,允许链接器生成全链接的可执行文件,这些可执行文件是独立于物理存储器中代码和数据的最终位置。

    简化加载

      虚拟存储器还使得容易想存储器中加载可执行文件和可共享对象文件。

    简化共享

      一般情况下,每个进程都有自己的私有的代码、数据和堆以及栈区域,是不和其他进程共享的。

      有些情况下,进程之间还是要共享代码和数据的。操作系统通过将不同的进程中适当的虚拟页面映射到相同的物理页面中,从而安排多个进程共享这部分代码的一个拷贝,而不需要每个进程包含独立的多份共享代码。

    简化存储器分配

       虚拟存储器向用户进程提供一个简单的分配额外存储器的机制。

      当一个运行的用户进程需要额外的堆空间时,操作系统分配一个适当的k个连续的虚拟存储器页面。然后将虚拟存储器页面映射到物理存储器的任意位置的k个任意的物理页面,物理页面可以不连续,可以随机分散在物理存储器中。

    ===================================================

    5、虚拟存储器作为存储器保护的工具

    任何现代系统都必须为操作系统提供手段来控制对存储器系统的访问。

    不应该允许一个用户进程修改它的只读文本段。

    不应该允许它读或修改任何内核中的代码和数据结构。

    不应该允许它读或者写其他进程的私有存储器。

    不应该允许它修改任何与其他进程共享的虚拟存储器页面。 

     

    提供独立的地址空间使得分离不同进程的私有存储器变得容易。

    但是地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制

    通过在PTE上添加额外的许可位来控制对一个虚拟页面的内容的访问。

    有三个许可位:SUP、WRITE、READ;

    SUP许可位用于表示进程是否必须运行在内核(超级用户)模式下;

    运行在内核模式下的进程可以访问任何页面,运行在用户模式中的进程只允许访问那些SUP为0的页面;

    READ位WRITE位用于控制对页面的读和写访问。

    如果一条指令违反了这些许可条件,那么CPU就触发一个一般保护故障,将控制权传递给一个内核中的异常处理程序。

    Unix外壳一般讲这种异常报告为“段错误”(segmentation fault)。

    ===================================================

    6、地址翻译

    地址翻译是介绍硬件在支持虚拟存储器中所扮演的角色;

    这里不展开描述;

    ===================================================

    7、案例研究:Intel Core i7/Linux存储器系统

     暂时省略

    ===================================================

    8、存储器映射

    Linux通过将一个虚拟存储区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程就叫做存储器映射(memory mapping)

    虚拟存储器区域可以映射到两种类型的对象中的一种:

    1)Unix文件系统中的普通文件

      一个区域可以映射到一个普通磁盘文件的连续部分,例如:一个可执行目标文件;

      文件区(section)被分成页大小的片;

      每一片包含一个虚拟页面的初始内容,因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理存储器(指的是高速缓存和主存)。

    2)匿名文件

      一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零;

      CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理存储器中找到一个合适的牺牲页面

      如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表。

      注意,磁盘和存储器之间并没有实际的数据传送。

    无论哪种情况下,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。

    交换文件也叫做交换空间(swap space),或者交换区域(swap area)

    交换空间限制着当前运行着的进程能够分配的虚拟页面的总数。

    交换空间是现代 Linux 系统中的第二种内存类型。交换空间的主要功能是当全部的 RAM 被占用并且需要更多内存时,用磁盘空间代替 RAM 内存。

    Linux 计算机中的内存总量是 RAM + 交换分区,交换分区被称为虚拟内存

    //这里统一一下术语,磁盘/硬盘是一回事;存储器指的是主存/内存DRAM,高速缓存SRAM;存储器分为虚拟存储器、物理存储器;

    共享对象

    如果虚拟存储器系统可以集成到传统的文件系统中,那么就能提供一种简单而高效的把程序和数据加载到存储器中的方法。

    对于许多程序都需要访问的代码,如果每个进程都保持一份该代码的拷贝,这是非常浪费资源的。

    幸运的是,存储器映射给我们一种清晰的机制,用来控制多个进程如何共享对象。

    一个对象被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象

     一个进程将一个共享对象映射到虚拟存储器的一个区域内,那么这个进程对于这个区域的任何写操作,对于那些也罢共享对象映射到它们的虚拟存储器的其他进程来说也是可见的。而且,这些变化也会反映在磁盘的原始对象中

    另一方面,对一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中

    一个映射到共享对象的虚拟存储器区域叫做共享区域。类似地,也有私有区域。

    私有对象,只有在写的时候才创建一份私有的物理拷贝副本;

    只读的话,就像共享对象一样,不同进程使用同一份物理拷贝;

    这个技术就叫做写时拷贝技术。

    只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障

    保护故障程序会会在物理存储器中创建这个页面的新拷贝,更新页表条目以指向这个新的拷贝,然后恢复这个页面的可写权限。

    然后在新创建的页面上,这个写操作就可以正常执行了。

    fork函数

    接下来理解fork函数是如何创建一个带有自己独立虚拟地址空间的新进程。

    当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。

    为了给这个新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表原样的拷贝。

    它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时拷贝

    当fork在新进程中返回时,新进场现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。

    当这两个进程中的任一个后来进行写操作时,写时拷贝机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

    execve函数

    虚拟存储器和存储器映射在将程序加载到存储器的过程中扮演着关键的角色。

    接下来理解execve函数实际上是如何加载和执行程序的。

    假设运行在当前进程的程序执行了如下的调用:

    Execve("a.out", NULL, NULL);

    execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:

    1)删除已存在的用户区域

      删除当前进程虚拟地址的用户部分中的已存在的区域结构。

    2)映射私有区域

      为新程序的文本、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时拷贝的。

      文本和数据区域被映射到a.out文件中的文本和数据区。

      bss区域是请求二进制零的,映射到匿名文件的。

      栈和堆区域也是请求二进制零的,且初始长度为零。

    3)映射共享区域

      如果a.out程序与共享对象(目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

    4)设置程序计数器

      execve做的最后一件事是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。

    使用mmap函数的用户级存储器映射

    Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。

    void mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

    mmap函数的工作过程

      mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片(chunk)映射到这个新的区域。

      连续的对象片大小为length字节,从距文件开始偏移量为offset字节的地方开始。start地址仅仅是一个暗示,通常被定义为NULL。

      就是将文件描述符fd指定的磁盘文件映射到进程的虚拟存储器区域中。 

    munmap函数删除虚拟存储器的区域:

    int munmap(void *start, size_t length); //若成功返回0,若失败返回-1;

    ===================================================

    9、动态存储器分配

    虽然可以使用低级的mmap和munmap函数来创建和删除虚拟存储器区域。

    但是C程序员还是觉得需要额外的虚拟存储器时,用动态存储器分配器会更加方便,也具有更好的移植性。

     

    动态存储器分配器维护这一个进程的虚拟存储器区域,成为。(就是对虚拟页的分配和未分配)

    假设堆是一个请求二进制零的区域,它紧接在未初始化的bss区域后开始,并向上生长。

    对于每个进程,内核维护着一个变量brk,它指向堆得顶部。

     

    分配器将视为 不同大小的块(block)的集合 来维护的。

    //术语统一,块和页其实是一回事,但是略有不同,块用在内存的语境中,页用在程序的语境中。

    页page、块block、片chunk

    程序 内存
    逻辑地址 物理地址
    页号 块号
    页内地址 块内地址
    页长(页面大小) 块长(块大小)

    每个块(页)就是一个连续的虚拟存储器的片(chunk)。

    要么是已分配的,要么是空闲的未分配的;

    空闲块保持空闲,直到它显式地被应用所分配。

    一个已分配的块保持已分配的状态,知道它被释放;

    这种释放要么是应用程序显式执行的,要么是存储器分配器自身隐式执行的。

     

    显式分配器(explicit allocator)

      要求应用显式地释放任何已分配的块。

      C标准库提供一种叫做malloc程序包的显式分配器。

      C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。

      C++中的new和delete操作符与之相当。

    隐式分配器(implicit allocator)

      要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。

      隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集

      一些高级语言依靠垃圾收集来释放已分配的块。

    接下来主要讨论显式分配器的设计和实现

    存储器分配是一个普遍的概念。

    malloc和free函数

    程序通过调用malloc函数从堆中分配块:

    void * malloc (size_t size); 

    该函数返回一个指针,指向大小为size字节的存储器块。

    在Unix系统上,malloc返回一个8字节(双字)边界对齐的块。

    32位系统的字,即字长位32位,即4个字节。因此8字节=双字。

    64位系统而言,8字节=单字;

    字对应多大的空间,就看这个系统是多少位系统。

    如果malloc函数遇到问题,那么它就返回NULL,并设置errno。

    malloc不初始化它返回的存储器。

    此外还有sbrk函数,这里不展开介绍。

    程序通过调用free函数来释放已分配的堆块。

    void free(void * ptr);

    为什么要用动态存储器分配

    程序使用动态存储器分配的最重要原因是因为经常直到程序实际运行时,它们才知道某些数据结构的大小。

    如果使用硬编码来分配数组通常不是好想法。

    动态存储器分配是一种有用而重要的编程技术。

    11节中会讨论因为不正确使用分配器所导致的错误。

    分配器的要求和目标

     显式分配器在一些非常严格的约束条件下工作:

      处理任意请求序列

      立即响应请求

      只使用堆

      对齐块

      不修改已分配的块

    目标:

      最大化吞吐率

      最大化存储器利用率

      

    碎片

    造成堆利用率很低的原因是一种称为碎片的现象。 

    当虽然有未使用存储器但不能用来满足分配请求时,就会发生这种现象。

    有两种形式的碎片:内部碎片,外部碎片。

    内部碎片:是已分配块大小和它们有效载荷大小之差的和。

    外部碎片:没有一个单独的空闲块足够大可以来处理这个请求时发生的。

    实现问题

    实际的分配器要在吞吐率和利用率之间把握好平衡,就必须考虑下面问题:

        空闲块组织 :如何记录空闲块

      放置 :我们如何选择一个合适的空闲块来放置一个新分配的块

      分割 : 在我们将一个新分配的块放置到某个空闲块之后,我们如何处理这个空闲块中的剩余部分

      合并 :如何处理一个刚刚被释放的块

    //后面的问题都是在讨论分配器的实现,暂时不展开讨论;

    隐式空闲链表

    放置已分配的块

    分割空闲块

    获取额外的堆存储器

    合并空闲块

    带边界标记的合并

    实现一个简单的分配器

    显式空闲链表

    分离的空闲链表

    ===================================================

    10、垃圾收集

    未能释放已分配的块是一个常见的编程错误。

    垃圾收集齐是一种动态存储分配器,它自动释放程序不再需要的已分配块。

    这些块被称为垃圾

    自动回收堆存储的过程叫作垃圾收集

    垃圾收集器定期识别垃圾块,并相应地调用free,将这些块放回到空闲链表中。

    有大量文献描述了垃圾收集的方法,本文的讨论仅局限于Mark&Sweep(标记&擦除)算法。 

     

    //以下的可达图算法、标记清除算法也是面试常问的内容;

    垃圾收集的基本知识

    可达图:

    可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

    根节点堆节点

    Java语言的垃圾收集器能够维护可达图的一种精确表示,因此能够回收所有垃圾; 

    收集器可以按需提供它们的服务,或者它们可以作为一个和应用并行的独立线程,不断地更新可达图和回收垃圾。

    关键思想就是收集器代替应用去调用free

     

    Mark&Sweep垃圾收集器

    标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,

    标记完毕后,再扫描整个空间中未被标记的对象,进行回收,

    如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

    C程序的保守Mark&Sweep

    ===================================================

    11、C程序中常见的与存储器有关的错误

    间接引用坏指针

    scanf("%d", &val);

    但是对于粗心的人来说,可能写成了scanf("%d", val); 很容易传递val的内容,而不是它的地址,之后scanf把val的内容解释为一个地址,并试图将一个字写到这个位置。

    这会导致操作系统以段异常终止我们的程序。 

    读未初始化的存储器

    bss存储器位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆存储器却并不是这样的。

    一个常见的错误是假设堆存储器被初始化为零。

    int *matvec(int **A, int *x, int n)

    {

      int i , j;

      int *y = (int*) Malloc(n *sizeof(int));

      

      for(i=0;i<n; i++)

        for (j=0; j<n; j++)

          y[i] +=A[i][j] * x[j];

      return y;

    错误地方在于程序员不假思索地将y[i]视为0,正确的方式是将y[i]显式地设置为零;

    允许栈缓冲区溢出

    如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误

    void bufoverflow()

    {

      char buf[64];

      gets(buf);

      return;

    }

    假设指针和它们指向的悐是相同大小的

    造成错位错误

    引用指针,而不是它所指向的对象

    误解指针运算

    引用不存在的变量

    int * stackref()

    {

      int val;

      return &val;

    }

    val是局部变量,此时返回局部变量的引用是不对的;因为局部变量会不存在的。

    引用空闲堆块中的数据

    引起存储器泄漏

    调用malloc,而忘记使用free,会缓慢地,隐性地使堆里充满了垃圾。 

    ====================================================

    12、小结

    虚拟存储器是对主存的抽象;

    虚拟存储器简化了存储器管理,简化了存储器保护;

    分配器有两种类型:显式分配器(malloc和free),隐式分配器(垃圾回收);

    对于程序员来说,管理和使用虚拟存储器是一件困难和容易出错的事情;

    知识点

    关注一下垃圾回收算法:标记-擦除的实现、可达图算法;

    分配器的设计和实现;

    局部性,命中缓存;

  • 相关阅读:
    关于数据库主键和外键
    数据库建立索引常用原则
    恭喜!Apache Hudi社区新晋多位Committer
    触宝科技基于Apache Hudi的流批一体架构实践
    轻快好用的Docker版云桌面(不到300M、运行快、省流量)
    实时视频
    通讯-- 通讯录
    通讯-- 总指挥部
    右侧菜单-- 事件面板
    应急救援预案选择逻辑
  • 原文地址:https://www.cnblogs.com/grooovvve/p/10596090.html
Copyright © 2011-2022 走看看