以上两题基本一模一样,想了好久才搞懂。开始只会用回溯法,总是超时。标准解法是动态规划。
这两题都是求可能的方案总数。我认为其突破口在于两点:
这部分内容看得感觉很有收获,解决了一些长久以来的疑惑。
昨天学过虚拟内存的表示,其实就是一些结构体,这些结构体将整个虚拟内存划分成了一个一个区域。现在,所谓内存映射,就是将这些区域与磁盘中的某些位置进行关联,当然也可以关联二进制零。这里的关键是“与磁盘中的位置”进行关联,而不是内存,所以不要混淆了内存映射和地址翻译。
具体如何关联(映射)的,csapp只是介绍了几个用户级的函数,可以实现映射的功能,例如mmap函数和munmap函数,分别实现创建虚拟内存区域并应映射和销毁虚拟内存的功能。
如果磁盘中的某个对象被作为共享对象,为多个进程引用,那么每个进程的虚拟内存中,都会有与之关联的互相独立的区域,但是!这些独立的虚拟内存区域,最终都映射到了同一个物理内存位置(页帧号),从而每个进程对该区域的操作,对其他进程而言都是可见的,并且,对该对象的写操作会反应到磁盘中。这,就叫做共享内存,共享对象。
刚才说的是共享对象的概念,而在Linux中,Copy On Write 问题是针对自己独有的私有对象而提出的。当虚拟内存关联一个对象并且希望将其作为私有的时候,他并不会在引用内存的时候着急在物理内存中复制一份自己专属的拷贝。而是只有当其希望对虚拟内存对象进行写的时候,才会单独拷贝一份出来,作为自己私有的对象进行写,而写的结果也不会反映到磁盘中。也就是说,再此之前,虽然逻辑上我们认为进程私有了一个对象,但是其实物理上还是共享的。这就类似于深拷贝和浅拷贝的概念。
当进程执行fork函数的时候,内核只是进行了一次“浅拷贝”,尽管我们认为子进程是拥有和父进程独立的虚拟地址空间的。父子进程共享物理内存,直到其中一方打算对共享的内存进行写操作,那么该操作会被中断,进入写时拷贝处理程序,也就是先拷贝一份,修改相应页表,然后再回过来进行写操作。这样的“延迟”拷贝可以尽可能的减少拷贝操作,最大限度的增加内存的共享程度,节约物理内存资源。
该函数通过3个步骤来实现程序的加载和运行:
由此可见,所谓加载的过程中,并没有实际代码、数据、共享库的“从磁盘到内存的拷贝”,仅仅是一些关联工作, 只有当cpu请求某个虚拟页,并且引起缺页中断的时候,才会真正从磁盘中将相应的页换入以及可能的换出。
前面一直提到堆,其实堆就是一个由动态分配器维护的虚拟内存区域。动态分配器也就是一段代码,他是通过维护一些数据结构来维护虚拟内存区域的。动态分配器的底层用到了前面讲过的mmap函数,也就是说,动态分配器也是用来进行虚拟内存的创建、映射、回收的。程序员通常会使用动态分配器而不是mmap函数。
动态分配器主要为程序员提供了两种接口,分配内存和销毁内存。动态分配器的设计根据“是否需要手动释放内存”分为两种风格:
对于两种风格的动态分配器,其分配内存的操作都必须是显式的。
隐式分配器为什么不需要手动释放呢?因为这些语言对指针的使用有着很严格的限制,所以分配器可以精确地维护可达图。所谓可达图,就是分配器眼中的堆。
所以Java语言可以对那些已经不可达的内存进行自动回收,称为垃圾回收,而具有垃圾回收功能的动态分配器也成为垃圾收集器(Garbage Collector)。