加菲猫窝

房价日涨,生活艰难
随笔 - 42, 评论 - 504, 引用 - 61

导航

关于

标签

每月存档

最新留言

广告

【第1页/共6页,101条】
首页
前页
1
2007年12月07日

《TCP/IP详解》(TCP/IP Illustrated) 很多人都在看,<卷2:实现>  里面参考的代码是 4.4bsd-lite 的 source code,这个代码也有不少人找,因为书上给的下载url早就不能用了:(

从网上下载来,给大家共享一下:

4.4BSD-lite.tar.gz  http://www.mybloop.com/go/rXjhH7

4.4BSD-lite2.tar.gz  http://www.mybloop.com/go/n272eK

顺便缅怀一下 W.Richard Stevens ,太牛了。

一个人写本书容易,难得是几本书都好得不能再好,万人景仰,这才是最难最难的啊!

把那几本书翻烂先!

posted on 2007-12-07 12:03:00 by peon  评论(0) 阅读(2139)

 
2007年12月02日

     前些天发现自己的网站无法访问,询问机房这边,说是机器最近常死机,我就把网站迁移到一个朋友的主机上, 结果没过几天机器又挂了,问朋友的机房那边说是硬件防火墙被攻击了而死掉了,详细情况不知。看来不是硬件问题,多半是被SYN FLOOD或者CC攻击了。恰好原来的机房说最近购买了新的防火墙,我又放了回去。

      既然不是硬件问题而可能是攻击,我就开始检查IIS log了,发现 IIS 里面很多Timer_ConnectionIdle和Timer_MinBytesPerSecond的错误,到网络上google了一下,常见说法是说错误是因为IIS的设置不当引起的,是因为连接超时时间设置太小,解决方法是设置连接超时为600秒,把MinFileBytesPerSec的设置从240修改到0(相当于关掉该设置)。觉得这些解决方法都有问题,假如车辆防盗警报经常响,正确的解决方法是看看有谁常来打你车子的主意,或者把车子放在更安全的地方,而绝对不是关掉警报。

      因为HTTP服务需要占用TCP连接,而TCP连接时是需要占用系统资源的,而且IIS为每个连接也需要分配相应的资源(至少一个FSM是要给的)。目前的主机能够处理上万的连接就可以说是软硬件设计都很不错了(可以参见C10K )。假如恶意人员通过一台或者多台机器发起大量的连接,而不请求内容(这样不需要消耗多少攻击机器的带宽),就可以大量消耗服务器资源而达到拒绝服务的目的。

     所以 IIS 需要关闭长时间非活动的连接,这个就是Timer_ConnectionIdle 的错误由来。

     既然盾牌改进了,当然矛也要发展一下,攻击者可以给服务器故意缓慢的发送和接收内容而消耗服务器的资源,这样可以避免服务器对于Timer_ConnectionIdle 的保护,相应的IIS的防范就是 MinFileBytesPerSec 设置,MinFileBytesPerSec 属性通过以最小的数据量保持连接,来禁止恶意的或软件工作不正常的客户端消耗资源。如果吞吐量低于 MinFileBytesPerSec 设置的值,则终止连接。LOG里面就会显示Timer_MinBytesPerSecond错误(一些Timer_MinBytesPerSecond错误是因为 windows 2003 的http.sys错误引起的,解决方式是打上最新 ServicePack : http://support.microsoft.com/kb/919797   http://support.microsoft.com/kb/919797/en-us )

      所以说这些设置都是用来保护IIS服务器的,可以一定程度上抵御一些恶意的行为消耗服务器的资源,所以我反而将IIS连接超时从原来的600秒改到了30秒,就让攻击来得更加猛烈些吧!!不过我还是很纳闷: 网站不大,所以我不可能去财消灾。无怨无仇的,谁这么无聊啊?!

 

最后发现结果挺搞笑的,欢迎看后继文章。(最近挺忙, 续集暂时不会出来。希望解决这种错误的同志:假如不是攻击的话,打上微软的补丁包就好了)

posted on 2007-12-02 12:17:00 by peon  评论(3) 阅读(6731)

 
2007年09月05日

看了蝈蝈俊.net的《理解缓存》,觉得真的是一个对于web applcation 缓存应用的好文,难得的是覆盖了冰山海面下的部分。我现在做的应用可以说和缓存打的交道也不少(不过不是web应用),也写些东西来分享给大家。

1.缓存是什么?

在我看来,缓存是通过存储中间结果,缩短访问路径来减少开销,提高性能的方法。这个概括未必最科学全面,也不够具体。我们来看看一个http 动态页面访问的例子:

访问路径是 : 数据库->应用数据集->内存对象->动态页面->HTTP服务器->用户浏览器

一个简单的访问,中间经过了多个环节,我们称这些环节为访问路径,我们来看看哪些地方可以采用缓存:

  1. HTTP服务器->用户浏览器,大家都知道浏览器都有本地缓存,浏览过的页面图片脚本等都会根据http header还有html的相关指令临时保存在本地硬盘里面,假如再次访问,访问路径就变成了"本地硬盘缓存->用户浏览器", 浏览的环节大幅度减少,性能也提高了。在这个环节,经常还使用带缓存的代理服务器来提高性能。
  2. 动态页面->HTTP服务器,这里有多种方式, 比如动态页面静态化,目前大量的大型网站使用这种方式。还有WEB服务器根据一定规则缓存整个动态页面,比如asp.net的Page Cache。这里的访问路径变成了"缓存页面->HTTP服务器->用户浏览器"
  3. 本地数据集->内存对象->动态页面。常见的就是缓存数据集还有对象,这个是ASP.NET cache里面相对浓墨重彩的部分,也是Memcached发力的侧重点, 也就不多说了。
  4. 数据库->应用数据集,不少数据库实现都有查询缓存

这里缓存都在访问路径中的环节存储了中间结果,用来减少相应的开销

2.缓存本身的开销

缓存本身也是有性能开销的,一种是将内存存储到缓存中开销,一种是将内容取出来的开销。另外,缓存往往还要付出空间上的开销。另外还要付出系统复杂度的开销,这增加了开发和维护成本。

大家也听说过IE缓存太大了或者是文件系统碎片太多以后,可能相反会拖累浏览速度,测试我倒是没有做过,但是的确是完全可能的。也就是说,缓存的开销可能会不缓存而是直接访问还要大,这就是大家不想看到的了。

3. 缓存的目的

其实前面已经说过缓存是为了减少开销,提高性能,这不就是缓存的目的吗?这倒是没错,但是也不尽然。

因为开销是一个很笼统的词,具体点有CPU开销,磁盘IO开销,网络开销,数据库访问开销等,缓存对于性能的优化,除了一些大众化的优化措施以外,还得有的放矢。

以前学习写程序,大家一定都听说过什么时间换空间,空间换时间,到底什么时候要拿空间换时间,什么时候要拿时间换空间,只能看具体应用了。前面说过缓存也有开销,其实缓存就是拿某些开销换取其他开销的下降而已。比如说动态页面静态化是一些大型网站常见的优化方法,他付出了磁盘的空间和读写开销,来换取更低的CPU消耗(不用解析动态页面)和数据库访问。有些网站每天访问量没有多少,却频繁生成和更新静态页面,同时还在服务器上做下载,本来磁盘就不堪重负,这下更加是雪上加霜,可以说是缓存优化的反面例子。或者是本来内存不大,磁盘swap很多影响性能,但是却使用大量内存做页面和对象缓存,也是反面的例子。

所以说不能盲目的进行缓存优化,一个系统,性能出现了问题,或者将来可能出现问题,性能总会有一个或者若干个瓶颈,我们要做的就是平衡或者削弱这个瓶颈,缓存是重要的手段。

所以缓存的目的是针对几个主要指标,兼顾若干个其他指标,来尽量实现低开销。

比如,数据库的CPU较高,那么一般是复杂的查询或者是存储过程导致的,在前面的各个环节进行缓存优化,比如缓存数据集和内存对象,都是好的解决方法,缓存整个页面也是个好方法,但是缓存页面要付出更多的空间开销,在某些情况下,缓存数据集或者内存对象已经够了。

假如WEB服务器的CPU较高,往往是因为动态页面处理造成的,找出开销大的处理,将处理的结果对象缓存,或者是页面静态化是不错的方法,而缓存数据库结果集往往收效不大。

4. 啥样的缓存才是好缓存?

蝈蝈俊认为是命中率最高的缓存最好。我做的领域是streaming server的磁盘IO缓存和CDN的网络边缘缓存,瓶颈就是磁盘或者网络IO,这种时候,命中率就是硬道理。

但是对于web服务器来讲,影响性能的因素很多,不同的内容访问开销相差很大,什么是好缓存,虽然命中率是极为重要的指标,但是还得要综合缓存的开销,原始的访问路径/开销和性能瓶颈来综合评价。也就是说不同的应用侧重不同,跑的硬件条件和瓶颈也不一样,很难有一个简单的指标。

比如缓存一个命中率稍低,但是原始访问开销很大的对象(比如要经过复杂查询和处理的对象) 比一个命中率较高,但是原始访问开销很小的对象要划得来。我觉得假如有一个"加权命中率"会更好,原始访问开销大的对象,要给与更高的权值,再进行命中率的计算。


假如一个缓存策略可以减少某个方面的性能开销,但是却带来了新的性能瓶颈,那么它也不是个好的缓存实现。比如磁盘IO紧张,页面更新非常频繁的情况下,静态页面缓存往往就不是一个好方法(好像没有用静态化页面做聊天室的吧:->)。

到底什么是好的缓存实现呢?前面也讲过了缓存的目的,我觉得充分利用了软硬件条件,消灭了性能瓶颈的就是好缓存。

5. 如何进行缓存优化

进行缓存优化,第一是要找到性能瓶颈,第二是找到瓶颈有关的应用部分。

性能瓶颈一般还是好找,系统有那么多性能计数器,数据库n多的调优工具,查询优化分析器。好好对照厂商的技术资料和google大法,很容易找到是CPU 内存还是IO限制了性能提高。

然后就是应用针对性能瓶颈的优化,有一个2/8定律,就是说80%的性能都消耗在20%的处理中,这20%也分为两种,一种是访问比例很高的,一种是开销贼大的,当然两个都占了,就更加没说的了。我们的任务就是集中火力提升这20%的性能,缓存往往是重要的方法。

寻找开销大的操作是个细活。我们在软件设计阶段可以预见瓶颈的部位,在出现问题的时候可以猜测出现瓶颈的部位,但是除非对行业模型,相关的架构性能还有一些性能细节非常了解(也就是说你实践过n次了),否则当初的预见和猜测都可能有较大偏差的,性能优化没有银弹,实践出真知。要多多使用工具进行分析,假如在系统里本身就有一些性能计数,可以在线或者离线提取就更好了。对于成熟的运营系统,性能统计和分析往往是其必不可少的功能。



在代码里寻找性能瓶颈太过细节,就不废话了,剩下的事情就是需要分析用户行为,根据用户对内容的访问频度实现不同的缓存策略。

6. 用户行为分析

用户行为主要有两种,一种是时间行为分布,就是一个内容的访问随时间变化的规律,不同的内容常常不同,比如新闻和音乐肯定是不一样的。
另外一种是空间分布,就是用户对于不同内容的偏好程度,大家常常说80/20规律,就是指大量的访问往往集中于少量内容,80/20只是一个定性的规律,这里一般适用的规律是zipf分布,我统计过一些系统的行为,和zipf分布的吻合度还是很好的,大家可以看看我的一篇文章:http://blog.joycode.com/peon/archive/2006/08/19/80885.aspx

大型的系统,缓存所有的内容肯定是不切实际的,缓存访问比率高的部分内容大概能达到多少缓存命中率呢?我根据zipf公式做了计算,结果可以看看下面的图:

y轴为命中率,x轴为缓存的内容占所有内容的比率,不同类型的内容有不同的a值(什么是a值, 请看http://blog.joycode.com/peon/archive/2006/08/19/80885.aspx),上面除了一条lg函数曲线外,其余的是各个a值对应的命中率曲线。可以看到a=0.95的时候,刚好符合20/80的定律。
光有理论的不够,进行实际统计永远是必须的,会议室里面的讨论永远代替不了实际的统计,log分析,性能计数器都是必要的技术。

7.关于分布式缓存

缓存会增加应用的复杂度,假如应用是分布式的而缓存不是分布式的,这个复杂度将会会平方。但是分布式缓存肯定比本机缓存效率低,所以是否选择分布式缓存实现,哪些内容使用分布式缓存哪些使用本机缓存,是第一步就得考虑的。

后记:
本文的着重点在性能,但是有些东西写的虚了点,有些想写的东西又没有写出来,算是薄积薄发献丑了:>
另外推荐一篇文章: 《从LiveJournal后台发展看大规模网站性能优化方法



posted on 2007-09-05 12:31:00 by peon  评论(10) 阅读(8644)

 
2007年07月21日

     发现很多情况下,msn传输文件比qq要慢,倒不是说msn没有快的时候,但是大部分的时候是真的比QQ慢,连我这种神经比较大条的人都注意到了,google了一下,早就有人做了解答,基本上就是说msn传输文件是使用TCP,而QQ使用UDP,剩下的事情就是论证TCP传输文件没有UDP快。大概的就是下面的几个观点:

1. TCP是可靠的,需要验证数据是否到达和是否正确,而UDP是不可靠的,少做了很多事情,所以MSN的文件传输比QQ慢。

我看了当时就想笑,UDP传输不可靠,但是应用层肯定会写代码作些和TCP的可靠传输差不多的事情。也用了QQ不少时日了,从来也没有发现传输文件有问题的,用UDP作协议也很久了,不做应用层验证重传的代码,我还真不敢写。这个理由,失败。

2. TCP建立连接需要3次握手,而UDP不需要,所以TCP慢。

3次握手这个事实倒是千真万确,还好我没有那么容易被忽悠,两个人谈话之前要握握手的确要稍微费上几秒钟,但是这个关谈话的语速啥事情?假如网络的ping值达到300ms,各位看官喜欢网络游戏的,估计都不会玩了,否则垂死的boss会高兴的发现你忽然变成了木偶可以随便殴打不还手,最后你只能骂骂电信网通然后复活几分钟后又是一条好汉。但是对于TCP,这样的ping值,3次握手一般都不需要1秒钟,把这个定为文件慢慢传的罪魁祸首,似乎太不靠谱了,这个理由还是失败。

3. TCP一旦建立链接,路由就确定了,而UDP是不确定的路由方式,谁速度快走谁的线路。

这样说就说明没有作者好好理解TCP/IP协议了,TCP的链路只是一个逻辑的,又没有建立物理链路,下面跑的还是IP包,这个包走这条路,那个包完全可能走另外的路,这点TCP和UDP没有两样。理由继续失败。

4. msn服务器在国外?

有些道理,但是我听一个美国的朋友说他也喜欢用QQ传文件的。

 

     那到底是怎么回事呢?是因为微软没有做好?(题外话,个人的确觉得MSN相比QQ的飞速进步而显得动作迟缓)QQ的Fans开始摩拳擦掌,一些不那么喜欢M$的估计就要开始丢板砖了。不管立场如何,事实还是要探寻一下,本着不求甚解,薄积薄发,浅入浅出的精神,我认为有几个可能原因:

1. 两个传文件客户端都在NAT后面的时候 (你不知道NAT啥意思?比如多个人通过路由器共享一个猫上网,那么你们一般就是在NAT后面了),从技术实现上讲,TCP在这种情况下穿越NAT比UDP麻烦得多。UDP只要开始几个穿越NAT的协商包需要服务器转一下,后面的文件数据可以两个客户端之间直接传输搞定,但是一般TCP就只能全程由服务器中转了,你说哪一个会比较快? 为什么TCP需要服务器中转?先看看NAT吧,听说有高人可以用raw sock搞定,反正我没有中间服务器搞不定。

 

2. 但是即使上面的条件不成立,msn还是一般比QQ慢的。问题还是在出在前面提到的验证数据可靠性上面,TCP是可靠的,UDP是不可靠的,但是用UDP做传输文件这档子事情,肯定要在应用层写一个验证的协议,否则传过来的文件缺胳膊少腿,会被用户骂死的。说是协议,其实也不难,打个比方吧:

    long long ago,贾宝玉在北京,林黛玉在长沙,怎么互诉衷肠呢,派家丁送信!路途遥远,怎么知道信收到没有?打电话问?那时候发明了这个就不用送信了,只能看家丁是否带了回信来了。假如发现一个家丁一个月还没有回,那就多半迷路堵车遭遇山贼或者开小差到扬州花差快活去了,再派一个人送吧! TCP就是这么做的,UDP在应用层协议一般也需要这么做,但是实现上有时候往往有区别。

    北京到长沙之间的路,并不是只有一个人跑的,常常会发生拥堵,假如发现家丁好久没有回了,TCP版的贾宝玉再派人送信是要的,但是他会比较识大体,他会少写信,降低发送速度,原来一天一封,现在可能一周一封了。他想,大家假如都这样,路就不会那么拥挤了。这做法很有道理,假如塞车了大家都想见缝插针,只能越来越塞,最后大家都动不了,还不如彼此容让慢慢排队。而UDP版本的贾宝玉假如也这么集体主义,那么他就叫做TCP友好流,就假如它只管自己拼命挤,就是非TCP友好的。

    所有的TCP协议实现假如发现拥塞,都会马上降低自己的发送速度。假如基于UDP的协议不这么做的话,原来拥塞的IP包加上重发的包,网路只会越来越拥塞,最后所有的坚持集体主义的TCP流都会做出牺牲,把带宽让给一些非TCP友好的UDP流。所以除非网络状况非常好,否则TCP是拼不过非TCP友好的协议的。

    我怀疑(仅仅是怀疑而已,我也没有条件和时间验证),QQ的文件传输机制是不那么TCP友好的,它抢带宽更加"我能",这样虽然对于整个网络负荷不是什么好事,但是对于单个用户,就见仁见智了,就好像大家看待多线程下载或者p2p一样。

posted on 2007-07-21 11:59:00 by peon  评论(30) 阅读(10922)

 
2007年05月26日

本文含有图片,无法贴上,请下载WORD文档阅读下载

有时你想这样管理某些对象,要让某种类型的对象能够自我销毁,也就是能够“delete this.” 很明显这种管理方式需要此类型对象要被分配在堆中。而其它一些时候你想获得一种保障:“不在堆中分配对象,从而保证某种类型的类不会发生内存泄漏。”如果你在嵌入式系统上工作,就有可能遇到这种情况,发生在嵌入式系统上的内存泄漏是极其严重的,其堆空间是非常珍贵的。有没有可能编写出代码来要求或禁止在堆中产生对象(heap-based object)呢?通常是可以的,不过这种代码也会把“on the heap”的概念搞得比你脑海中所想的要模糊。

要求在堆中建立对象

让我们先从必须在堆中建立对象开始说起。为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到。非堆对象(non-heap object)在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的构造函数和析构函数,就可以实现这种限制。

把这些调用变得不合法的一种最直接的方法是把构造函数和析构函数声明为private。这样做副作用太大。没有理由让这两个函数都是private。最好让析构函数成为private,让构造函数成为public。处理过程与条款26相似,你可以引进一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。

例如,如果我们想仅仅在堆中建立代表unlimited precision numbers无限精确度数字的对象,可以这样做

class UPNumber {

public:

  UPNumber();

  UPNumber(int initValue);

  UPNumber(double initValue);

  UPNumber(const UPNumber& rhs);

 

  // 伪析构函数 (一个const 成员函数, 因为

  // 即使是const对象也能被释放。)

  void destroy() const { delete this; }

 

  ...

 

private:

  ~UPNumber();

};

然后客户端这样进行程序设计:

UPNumber n;                          // 错误! (在这里合法,但是

                                     // 当它的析构函数被隐式地

                                     // 调用时,就不合法了)

 

UPNumber *p = new UPNumber;          //正确

 

...

 

delete p;                            // 错误! 试图调用

                                     // private 析构函数

 

p->destroy();                        // 正确

另一种方法是把全部的构造函数都声明为private。这种方法的缺点是一个类经常有许多构造函数,类的作者必须记住把它们都声明为private。否则如果这些函数就会由编译器生成,构造函数包括拷贝构造函数,也包括缺省构造函数;编译器生成的函数总是public(参见Effecitve C++ 条款45)。因此仅仅声明析构函数为private是很简单的,因为每个类只有一个析构函数。

通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象,但是在条款26已经说过,这种方法也禁止了继承和包容(containment):

class UPNumber { ... };              // 声明析构函数或构造函数

                                     // private

 

class NonNegativeUPNumber:

  public UPNumber { ... };           // 错误! 析构函数或

                                     //构造函数不能编译

 

class Asset {

private:

  UPNumber value;

  ...                                // 错误! 析构函数或

                                     //构造函数不能编译

};

这些困难不是不能克服的。通过把UPNumber的析构函数声明为protected(同时它的构造函数还保持public)就可以解决继承的问题,需要包含UPNumber对象的类可以修改为包含指向UPNumber的指针:

class UPNumber { ... };              // 声明析构函数为protected

 

class NonNegativeUPNumber:

  public UPNumber { ... };           // 现在正确了; 派生类

                                     // 能够访问

                                     // protected 成员

 

class Asset {

public:

  Asset(int initValue);

  ~Asset();

  ...

 

private:

  UPNumber *value;

};

 

Asset::Asset(int initValue)

: value(new UPNumber(initValue))      // 正确

{ ... }

 

Asset::~Asset()

{ value->destroy(); }                 // 也正确

判断一个对象是否在堆中

如果我们采取这种方法,我们必须重新审视一下“在堆中”这句话的含义。上述粗略的类定义表明一个非堆的NonNegativeUPNumber对象是合法的:

NonNegativeUPNumber n;                // 正确

那么现在NonNegativeUPNumber对象n中的UPNumber部分也不在堆中,这样说对么?答案要依据类的设计和实现的细节而定,但是让我们假设这样说是不对的,所有UPNumber对象 —即使是做为其它派生类的基类—也必须在堆中。我们如何能强制执行这种约束呢?

没有简单的办法。UPNumber的构造函数不可能判断出它是否做为堆对象的基类而被调用。也就是说对于UPNumber的构造函数来说没有办法侦测到下面两种环境的区别:

NonNegativeUPNumber *n1 =

  new NonNegativeUPNumber;            // 在堆中

 

NonNegativeUPNumber n2;               //不再堆中

不过你可能不相信我。也许你想你能够在new操作符、operator newnew 操作符调用的构造函数的相互作用中玩些小把戏(参见条款8)。可能你认为你比他们都聪明,可以这样修改UPNumber,如下所示:

class UPNumber {

public:

  // 如果建立一个非堆对象,抛出一个异常

  class HeapConstraintViolation {};

 

  static void * operator new(size_t size);

 

  UPNumber();

  ...

 

private:

  static bool onTheHeap;                 //在构造函数内,指示

                                         // 对象是否被构造在

  ...                                    // 堆上

 

};

 

// obligatory definition of class static

bool UPNumber::onTheHeap = false;

 

void *UPNumber::operator new(size_t size)

{

  onTheHeap = true;

  return ::operator new(size);

}

 

UPNumber::UPNumber()

{

  if (!onTheHeap) {

    throw HeapConstraintViolation();

  }

 

  proceed with normal construction here;

 

  onTheHeap = false;                    // 为下一个对象清除标记

}

如果不再深入研究下去,就不会发现什么错误。这种方法利用了这样一个事实:“当在堆上分配对象时,会调用operator new来分配raw memory”,operator new设置onTheHeaptrue,每个构造函数都会检测onTheHeap,看对象的raw memory是否被operator new所分配。如果没有,一个类型为HeapConstraintViolation的异常将被抛出。否则构造函数如通常那样继续运行,当构造函数结束时,onTheHeap被设置为false,然后为构造下一个对象而重置到缺省值。

这是一个非常好的方法,但是不能运行。请考虑一下这种可能的客户端代码:

UPNumber *numberArray = new UPNumber[100];

第一个问题是为数组分配内存的是operator new[],而不是operator new,不过(倘若你的编译器支持它)你能象编写operator new一样容易地编写operator new[]函数。更大的问题是numberArray100个元素,所以会调用100次构造函数。但是只有一次分配内存的调用,所以100个构造函数中只有第一次调用构造函数前把onTheHeap设置为true。当调用第二个构造函数时,会抛出一个异常,你真倒霉。

即使不用数组,bit-setting操作也会失败。考虑这条语句:

UPNumber *pn = new UPNumber(*new UPNumber);

这里我们在堆中建立两个UPNumber,让pn指向其中一个对象;这个对象用另一个对象的值进行初始化。这个代码有一个内存泄漏,我们先忽略这个泄漏,这有利于下面对这条表达式的测试,执行它时会发生什么事情:

new UPNumber(*new UPNumber)

它包含new 操作符的两次调用,因此要调用两次operator new和调用两次UPNumber构造函数(参见条款8)。程序员一般期望这些函数以如下顺序执行:

调用第一个对象的operator new

调用第一个对象的构造函数

调用第二个对象的operator new

调用第二个对象的构造函数

但是C++语言没有保证这就是它调用的顺序。一些编译器以如下这种顺序生成函数调用:

调用第一个对象的operator new

调用第二个对象的operator new

调用第一个对象的构造函数

调用第二个对象的构造函数

编译器生成这种代码丝毫没有错,但是在operator newset-a-bit的技巧无法与这种编译器一起使用。因为在第一步和第二步设置的bit,第三步中被清除,那么在第四步调用对象的构造函数时,就会认为对象不再堆中,即使它确实在。

这些困难没有否定让每个构造函数检测*this指针是否在堆中这个方法的核心思想,它们只是表明检测在operator new(operator new[])里的bit set不是一个可靠的判断方法。我们需要更好的方法进行判断。

如果你陷入了极度绝望当中,你可能会沦落进不可移植的领域里。例如你决定利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展:

在以这种方法管理程序内存的系统里(很多系统都是,但是也有很多不是这样),你可能会想能够使用下面这个函数来判断某个特定的地址是否在堆中:

// 不正确的尝试,来判断一个地址是否在堆中

bool onHeap(const void *address)

{

  char onTheStack;                   // 局部栈变量

 

  return address < &onTheStack;

}

这个函数背后的思想很有趣。在onHeap函数中onTheSatck是一个局部变量。因此它在堆栈上。当调用onHeap时,它的栈框架(stack frame(也就是它的activation record)被放在程序栈的顶端,因为栈在结构上是向下扩展的(趋向低地址),onTheStack的地址肯定比任何栈中的变量或对象的地址小。如果参数address的地址小于onTheStack的地址,它就不会在栈上,而是肯定在堆上。

posted on 2007-05-26 09:27:00 by peon  评论(0) 阅读(658)

 

到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。静态对象是那些在程序运行时仅能初始化一次的对象。静态对象不仅仅包括显示地声明为static的对象,也包括在全局和命名空间里的对象(参见条款47)。这些对象肯定位于某些地方,而这些地方既不是栈也不是堆。

它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。先前内存管理的图片到讲述的是事实,不过是很多系统都具有的事实,但是没有告诉我们这些系统全部的事实,加上静态变量后,这幅图片如下所示:

onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别:

void allocateSomeObjects()

{

  char *pc = new char;               // 堆对象: onHeap(pc)

                                     // 将返回true

 

  char c;                            // 栈对象: onHeap(&c)

                                     // 将返回false

 

  static char sc;                    // 静态对象: onHeap(&sc)

                                     // 将返回true

  ...

 

}

现在你可能不顾一切地寻找区分堆对象与栈对象的方法,在走头无路时你想在可移植性上打主意,但是你会这么孤注一掷地进行一个不能获得正确结果的交易么?绝对不会。我知道你会拒绝使用这种虽然诱人但是不可靠的“地址比对”技巧。

令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。如果你实在非得必须判断一个地址是否在堆上,你必须使用完全不可移植的方法,其实现依赖于系统调用,只能这样做了。因此你最好重新设计你的软件,以便你可以不需要判断对象是否在堆中。

如果你发现自己实在为对象是否在堆中这个问题所困扰,一个可能的原因是你想知道对象是否能在其上安全调用delete。这种删除经常采用“delete this”这种声明狼籍的形式。不过知道“是否能安全删除一个指针”与“只简单地知道一个指针是否指向堆中的事物”不一样,因为不是所有在堆中的事物都能被安全地delete。再考虑包含UPNumber对象的Asset对象:

class Asset {

private:

  UPNumber value;

  ...

 

};

 

Asset *pa = new Asset;

很明显*pa(包括它的成员value)在堆上。同样很明显在指向pa->value上调用delete是不安全的,因为该指针不是被new返回的。

幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易。因为对于前者我们只需要一个operator new返回的地址集合。因为我们能自己编写operator new函数(参见Effective C++条款8—条款10),所以构建这样一个集合很容易。如下所示,我们这样解决这个问题:

void *operator new(size_t size)

{

  void *p = getMemory(size);         //调用一些函数来分配内存,

                                     //处理内存不够的情况

 

  p加入到一个被分配地址的集合;

 

  return p;

 

}

 

void operator delete(void *ptr)

{

  releaseMemory(ptr);                // return memory to

                                     // free store

 

  从被分配地址的集合中移去ptr;

}

 

bool isSafeToDelete(const void *address)

{

  返回address是否在被分配地址的集合中;

}

这很简单,operator new在地址分配集合里加入一个元素,operator delete从集合中移去项目,isSafeToDelete在集合中查找并确定某个地址是否在集合中。如果operator new operator delete函数在全局作用域中,它就能适用于所有的类型,甚至是内建类型。

在实际当中,有三种因素制约着对这种设计方式的使用。第一是我们极不愿意在全局域定义任何东西,特别是那些已经具有某种含义的函数,象operator newoperator delete正如我们所知,只有一个全局域,只有一种具有正常特征形式(也就是参数类型)的operator newoperator delete 。这样做会使得我们的软件与其它也实现全局版本的operator new 和operator delete的软件(例如许多面向对象数据库系统)不兼容。

 

我们考虑的第二个因素是效率:如果我们不需要这些,为什么还要为跟踪返回的地址而负担额外的开销呢?

最后一点可能有些平常,但是很重要。实现isSafeToDelete让它总能够正常工作是不可能的。难点是多继承下来的类或继承自虚基类的类有多个地址,所以无法保证传给isSafeToDelete的地址与operator new 返回的地址相同,即使对象在堆中建立。有关细节参见条款24和条款31

我们希望这些函数提供这些功能时能够不污染全局命名空间,没有额外的开销,没有正确性问题。幸运的是C++使用一种抽象mixin基类满足了我们的需要。

抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(mix in)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容(参见Effective C++条款7)。这种类几乎都是抽象类。因此我们能够使用抽象混合(mixin)基类给派生类提供判断指针指向的内存是否由operator new分配的能力。该类如下所示:

class HeapTracked {                  // 混合类; 跟踪

public:                              // operator new返回的ptr

 

  class MissingAddress{};            // 异常类,见下面代码

 

  virtual ~HeapTracked() = 0;