标签 Unix 下的文章

学习虚存-自上而下

如果它不存在,但是你能看见它 — 它是虚拟的(IBM宣传虚拟内存之用语)。虚拟内存技术是计算机发展史上的一项重要的技术,它帮助应用程序摆脱了“体积”的限制。

记得上大学时,有一本书好像叫做“计算机网络 – 自顶向下”,全名记不太清了。书中从人们接触最多也最熟悉的“应用层”开始讲,一直讲到“物理层”,看完这本书后感觉效果不错。所以按照这种方法我也尝试着自上而下的去学习“虚存”,从我们最熟悉的C库接口调用说起,一直谈到底层的硬件支持设施。

1、初学者的疑惑
初学者往往都会写出以下这样的例子程序来学习malloc和free的使用。
int main() {
        int *p = malloc(10000);
        printf("p's address is 0x%p\n", p);
        free(p);
        return 0;
}
但往往结果让这些初学者们感到疑惑。比如上述的例子,在SUN SPARC 64编译后其输出如下:
p's address is 0x100100dc0
看到这样的结果,初学者往往心里嘀咕,“这台机器物理内存才4G,其地址空间总共才4294967296(dec),而0x100100dc0转换十进制为4296019392(dec),这个地址明显已经超出了我的物理内存的限制,这是怎么回事呢?”。其实这里的解释很简单:因为我们看到的都是“虚拟内存地址”。

2、“堆”为何物
malloc是个极其常见的内存分配接口函数,它主要负责运行时在“堆”上为程序动态分配内存空间。我们总是在口头上谈论着“堆”,那么“堆”到底为何物呢?我们已经知道了有“虚拟地址”这个东西的存在,想必“堆”和“虚拟地址”有着千丝万缕的联系^_^。我们来翻看一些经典书籍中的描述。在CSAPP[注1]中的描述是这样的:“堆是进程地址空间中的一段“虚拟地址”空间。在大多数的Unix系统中,堆是映射“二进制零区域(demand-zero)”实现的。其位置在bss段后,其增长方向为高地址方向”。

3、内存映射
前面谈到“demand-zero”这个新名词,那么什么叫“映射到demand-zero”呢?这里蕴含着一个极其重要的概念“内存映射”。内存映射好似一道桥梁,将放在物理磁盘上的对象和一段进程“虚拟地址”空间连接起来。磁盘上的对象,主要指的就是文件,在多数Unix的实现中支持两种文件的内存映射,分别为Regular File和匿名文件(如demand-zero)。映射的过程大致为将文件分成若干“虚拟内存基本单元(页)”大小存于“交换区”,直到CPU指令第一次访问到某个单元时,这个单元才真正被加载到物理内存中。

4、虚拟内存,何方神圣
看到这是不是有些“云里雾里”的感觉亚^_^。其实对于用户进程来说,它是看不到CPU和OS是如何相互配合完成内存管理的。它只认为它面前的是一个这样的情景:“一个完全被我拥有的CPU、一个从拥有M地址空间的物理内存(M = 2的n次方,n为地址总线宽度)…”。这里的用户进程眼中的“物理内存”实际就是“虚拟内存”。虚拟意味着假象,我们知道一个用户进程运行时可能仅仅占用的物理内存的一小部分。看来用户进程被欺骗了。而这个骗局是由操作系统和CPU共同布置的。为了让这个骗局一直维持下去,CPU和OS还是做了很多工作的,究竟有哪些工作呢?我们一一来看看。

1) 交换区(swap)
为了支持虚拟内存,操作系统在物理内存、磁盘之间交换数据的基本单元为“页”。页的大小是固定的,其因操作系统而异。这样一个用户进程在被加载之前首先要被分成若干个“页”,这些页存储在磁盘上。那么是不是进程启动后所有的页都被加载到物理内存中呢?答案是NO。在当前的Unix操作系统中,都有一个叫“交换区”的地方,“交换区”在磁盘上,它存储的是“已分配的虚拟内存页”。又有些糊涂是吧,什么叫已分配的页呢?一个进程虚拟内存页的加载流程大致是这样的:一旦用户进程一虚拟页需要被加载,则操作系统会在“交换区”中为该页分配一个页,一旦CPU访问的虚拟地址落入该页地址空间,则该页才被换入到物理内存中。在这个过程中虚拟页有多个状态,分别如下:
未分配的 - 进程虚拟页未得到加载指令,仍安静的待在磁盘上;
未缓存的 - OS为该进程虚拟页在交换区分配了一个空间,但是该虚拟页还未被引用;
已缓存的 - 该虚拟页被引用,被载入到物理内存中。

2) 换入换出
物理内存容量有限,当物理内存无空间存储新的内存页的时候,就需要将某些内存页从物理内存中移出以为新页腾出空间。这个过程对于那些被移出的页来说,就叫“换出”;相反对于那些新加入到物理内存中的页来说就叫做“换入”。

5、从缓存角度看虚存
现代计算机的存储体系是呈金字塔状的。越接近顶层,速度越快,容量越小,价格越贵;越接近底层,速度越慢,容量越大,价格越低。这样就形成了一个逐级缓存的机制。第K层设备永远是第K+1层设备的缓存。按照这种说法,在早期计算机中,主存是磁盘的缓存,CPU内的高级Cache是主存的缓存。现代计算机基本都支持虚拟内存机制,而虚存页是存储在磁盘上的,虚存页在主存中换入换出。按照缓存的概念,虚存属于容量大,速度慢的第K+1层,而处于第K层的主存就可以看作是虚拟内存的缓存。那么一切缓存理论就都可以应用在虚存和物理内存之间了,比如换入换出算法等。

6、硬件支持
在支持虚拟内存机制的计算机中,CPU都是以虚拟地址形式生成指令地址或者数据地址的,而这个虚拟地址对于物理内存来说是不可见的,那么是谁来屏蔽这个差异的呢?答案是MMU(Memory Management Unit)。MMU负责将CPU发出的虚拟地址转换成相应的物理内存地址。MMU不是孤立工作的,OS为其提供了很好的支持,OS在物理内存中为MMU维护着一张全局的页表,来帮助MMU找到正确地物理内存地址。

7、小结
这里简短而概要的对虚存进行了说明,虚存机制很复杂,不是一句两句能说清楚的,还需要慢慢探索^_^

[注1]
CS.APP – 《computer systems a programmer's perspective》 中文名:《深入理解计算机系统》。

tony说设计-实践后的体会

入司后连续做过几个项目。最近在做一个新的项目的设计的时候,突然想到是不是该把以前项目中一些好的设计想法应用到新的项目中,并且尽量减少在新的项目中遗留以前的不好的设计呢?那么以前的项目中哪些是值得我去借鉴,哪些又是应该去避免的呢?真的很遗憾,自己并没有系统的反思和总结过,这就是我写下这篇Blog的直接起因。

一直在Unix平台下做设计和开发,所以下面谈的内容可能都有些局限性。作为设计原则本身,某些可能具有很强的通用性,而还有一些可能局限于某个平台、某个领域。这里我想到了以下几个方面(仅仅提出一些观点,而没有太关注具体的解决方法,给大家一个想象的空间^_^):

1、扩展
扩展性在这里被我分为“性能扩展”和“功能扩展”两类。
1) 性能扩展
作为电信级系统,对其的性能要求肯定不会低。那么如何做性能扩展呢?有两种方法:提高单点处理能力(垂直扩展)和平行扩展。
垂直扩展 – 简单说就是一个进程不够,我再加一个进程做同样的处理。问题出现了:如何做进程间的通讯?使用共享内存(最快的IPC)还是其他IPC方式呢?还是一个权衡的过程。
水平扩展 – 简单说就是一台机器不够,我再加一台机器。咱们也时髦一把,弄个分布式。问题出现了:如何做分布式节点之间的通讯?目前流行soap,而且又有开源包,如gsoap,其唯一缺点也是致命缺点就是慢。所以我想大部分开发商还是使用自己的内部协议。另外分布式与钱还是挂钩的。分布式意味着需要更多的机器平台来承载我们的系统,机器是钱,对机器的服务也是钱。看来这也是大家都喜欢分布式的原因。

2) 功能扩展:
做电信软件,其面对的最大的问题可能就是“用户需求变化多端”这个问题,更有甚者就是“用户并不知道需求,需要你去引导用户”,这样就会给项目带来较大的风险。如何能在设计这一层来规避风险或者减小风险带来的损失呢?尽量划分清易变化的需求和较稳定的需求,采用面向接口编程的形式(记住面向接口并不是Java等语言的专利)。比如以动态链接的方式(有点plugin的意思)实现系统中那些易变化的功能模块。一旦用户需求改变,我们需要修改的只是一个动态链接库(即替换一个plugin)。

2、隔离
隔离是为了可测试和好维护。其缺点是可能带来性能上的缺失。
1) 可测试性
可测试性在当前可是衡量一个软件设计好坏的重要标准。大型程序,模块众多。首先应该想到的就是怎么做集成测试?集成测试可能需要把一个模块单独拿出来运行。这就需要我们在设计的时候使模块间的耦合性尽量小,比如我们可以采用文件或者MQ的方式来解除模块间的耦合。这样一旦模块A开发完毕,产生其输入数据的模块B还未完成,我们就可以使用模拟器来产生输入数据即可(生成文件或者手工写数据到MQ中)。

2) 维护性
软件脱离不了服务,服务也是钱,也算在软件的成本中。如果一个软件的维护成本过高,完全可能会使该项目赔本。可维护性高的一个很重要的指标就是能快速定位问题所在。隔离模块可以提高定位错误的效率。因为我们可以将某一模块从系统中拿出来,单独测试定位问题,一个一个的排查,而不是大海捞针般的在系统中胡乱撞。

隔离还有一点很重要,就是尽可能的让每个模块能单独可运行(并不一定是独立程序),而无须依赖其他模块。

3、灵活
在我看来体现一个系统是否灵活,最重要的一点就是其配置文件设计的灵活性和合理性。

1) 配置文件格式
现在Java世界的配置文件基本已经被xml格式所垄断,而在C这边仍旧使用着传统的“key – value”格式。xml的多级配置是传统“key – value”不能比拟的。
例如:

   
   

# in "key – value" format
[mqlist]
name = testmq1;testmq2

在传统配置文件中我们需要对name字串进行解析才能得到各个mq的名字。而且大多数读"key – value"的程序可能都有对value值长度的限制,也就是说我们不能无限制的增加mq的个数。在xml中不存在这样的问题。况且现在像expat这样的开源包对xml的支持也很好。建议在以后设计时向xml格式配置文件转移。

2) 配置方式
一个灵活的配置,会给系统维护和变更带来极大的方便。甚至可以通过修改配置来满足用户新的需求。另外集中配置和分散配置也是需要设计者考虑的问题。比如将整个系统做成一个大程序,并做集中配置,那么除非有动态配置更新程序,否则一旦配置更改,就需要重启整个系统。相反如果系统是一个小程序的集合,采取分散配置,这样针对每个小程序的配置修改只会影响到其自己,只需重启相应的程序即可。

3) 配置粒度
很难用定义解释这个问题,举个例子可能会有更好理解。比如按照一定的配置格式写一个文件(由若干行记录组成)。对文件格式的配置可能如下:
粒度粗的配置

               
               

粒度细的配置

               
               

可以看出“粒度粗的配置”只支持到区分记录间的对齐方式和填充字节的差异性;而“粒度细的配置”则支持到区分字段间的对齐方式和填充字节的差异性。一旦需求发生变化,要求每条记录的字段间的alignment和padding可以不同的话,那么“粒度粗的配置”则不能满足需求,而“粒度细的配置”仅仅通过改变配置即可满足这个需求。从这个例子可以看出配置粒度粗细选择某种程度上可能会影响程序的扩展性,一般来说配置粒度细的程序扩展性要更好些。

4、层次
做设计一定要考虑层次,这里体会不多也就不多说了,总之有一点就是“在做设计的时候心中一定要有层次的概念”。

5、小结
给我的感觉:设计是一门权衡的艺术,相信通过上面的一些文字也可以不充分的论证这一点。本文仅仅是我在做过一些项目后的一些体会,并没有很牢固的理论基础。自己也正在计划着读一些关于架构设计方面的书,来提高一下自己的理论水平^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats