??xml version="1.0" encoding="utf-8" standalone="yes"?>
做法Q?br>1、将牛肉z净切块入锅Q锅里加水。煮开以后Q撇LQ沫,转小火慢炖两个小时左叟?br>
2、土豆、洋葱切块,胡萝卜去皮切块,芹菜斜着切段Q圆白菜切大片?br>
3、将所有的蔬菜都放入锅里,大火烧开后{火接着炖?br>
4、西U柿切丁Q另起一锅热油,倒入西红柿丁和西U柿q炒至西红柿丁熟烂以后Q倒入
汤锅里,加盐调味Q再炖一个小时左叛_可?
]]>
声明Q本文可以不l作者同意Q意{载,但Q何对本文的引用都L明作者、出处及此声明信息。谢谢!Q?br>
要了解此文章中引用的本人写的另一文章,请到以下地址Q?br>http://m.shnenglu.com/zjl-1026-2001/archive/2009/08/15/93427.html
以上的这文章是早在d的时候写的了Q当时正在作休闲q_Q一直在想着如何实现一个可扩充的支持百万h在线的游戏^収ͼ后来思\有了Q就写了那篇ȝ。文章的意思,重点在于阐述一个百万在线的系l是如何实施的,倒没真正认真地考察qQQ游戏到底是不是那样实现的?br>
q日在与业内人士讨论Ӟ提到QQ游戏的实现方式ƈ不是我原来所想的那样Q于是,今天又认真抓了一下QQ游戏的包Q结果确如这位兄弟所aQQQ游戏的架构与我当初所设想的那个架构相差确实不。下面,我重新给出QQ百万U在U的技术实现方案,q以此展开Q谈谈大型在U系l中的负载均衡机制的设计?br>
从QQ游戏的登录及游戏q程来看QQQ游戏中,也至分Zc?a class=mykeyword title=http://www.doserver.net/tag.php?tag=%E6%9C%8D%E5%8A%A1%E5%99%A8 target=_blank>服务?/font>。它们是Q?br> W一层:登陆/账号服务?Login Server)Q负责验证用戯n份、向客户端传送初始信息,从QQ聊天软g的封包常识来看,q些初始信息可能包括“会话密钥”此类的信息,以后客户端与后箋服务器的通信׃用此会话密钥q行w䆾验证和信息加密;
W二层:大厅服务?Cq么叫吧, Game Hall Server)Q负责向客户端传递当前游戏中的所有房间信息,q些戉K信息包括Q各戉K的连接IPQPORTQ各戉K的当前在Uh敎ͼ戉K名称{等?br> W三层:游戏逻辑服务?Game Logic Server)Q负责处理房间逻辑及房间内的桌子逻辑?br>
从静态的表述来看Q以上的三层l构g与我以前写的那篇文章相比q没有太大的区别Q事实上Q重Ҏ它的工作程QQQ游戏的通信程与我以前的设惛_谓大相径庭,其设计思想和技术水q确实非怼U。具体来_QQ游戏的通信q程是这LQ?br>
1.由Client向Login Server发送̎号及密码{登录消息,Login ServerҎ校验l果q回相应信息。可以设想的是,如果Login Server通过了Client的验证,那么它会通知其它Game Hall Server或将通过验证的消息以及会话密钥放在Game Hall Server也可以取到的地方。MQLogin Server与Game Hall Server之间是可以共享这个校验成功消息的。一旦Client收到了Login Serverq回成功校验的消息后QLogin Server会主动断开与Client的连接,以腾出socket资源。Login Server的IP信息Q是存放在QQGame\config\QQSvrInfo.ini里的?br>
2.Client收到Login Server的校验成功等消息后,开始根据事先选定的游戏大厅入口登录游戏大厅,各个游戏大厅Game Hall Server的IP及Port信息Q是存放在QQGame\Dirconfig.ini里的。Game Hall Server收到客户端Client的登录消息后Q会Ҏ一定的{略军_是否接受Client的登录,如果当前的Game Hall Server已经C上限或暂时不能处理当前玩家登录消息,则由Game Hall Server发消息给ClientQ以让Client重定向到另外的Game Hall Serverd。重定向的IP及端口信息,本地没有保存Q是通过数据包或一定的法得到的。如果当前的Game Hall Server接受了该玩家的登录消息后Q会向该Client发送房间目录信息,q些信息的内Ҏ上面已经提到。目录等消息发送完毕后QGame Hall Serverx开与Client的连接,以腾出socket资源。在此后的时间里QClient每隔30分钟会重新连接Game Hall Serverq向其烦要最新的戉K目录信息及在UhC息?br>
3.ClientҎ列出的房间列表,选择某个戉Kq入游戏。根据我的抓包结果分析,QQ游戏Qƈ不是l每一个游戏房间都分配了一个单独的端口q行处理。在QQ游戏里,有很多房间是q的同一个IP和同一个端口。比如,在斗C一区,?0个房_用的都是同一个IP和Port信息。这意味着Q这些房_在QQ游戏的服务器上,事实上,可能是同一个程序在处理Q!QQQ游戏戉K的hC限是400人,不难推算QQQ游戏单个服务器程序的用户承蝲量是2万,即QQ的一个游戏逻辑服务器程序最多可同时?万个玩家保持TCPq接q保证游戏效率和品质Q更重要的是Q这样可以ؓ腾讯省多money呀Q!Q哇哦!QQ实很牛。以2万的在线数还能保持这么好的游戏品质,实不容易!QQ游戏的单个服务器E序Q管理的不再只是逻辑意义上的单个戉KQ而可能是许多逻辑意义上的戉K。其实,对于服务器而言Q它是一个大区服务器或大区服务器的一部分Q我们可以把它理解ؓ一个庞大的游戏地图Q它实现的也是分块处理。而对于每一张桌子上的打牌逻辑Q则是有一个统一的处理流E,50个房间的50Q?00张桌子全p一个服务器E序q行处理(我不知道QQ游戏的具体打牌逻辑是如何设计的Q我惛_有可能也是分区域的,分块?。当Ӟ以上q些只是服务器作的事Q针对于客户端而言Q客L只是在表CQ将一个个戉K单独|列了出来,q样作,是ؓ便于玩家q行游戏以及减少服务器的开销Q把q个大区中的?00人放在一个集合内q行处理Q比如聊天信息,“?00人广?#8221;?#8220;?万hq播”Q这是完全不同的两个概念Q?br>
4.需要特别说明的一炏V进入QQ游戏戉K后,直到点击某个位置坐下打开另一个程序界面,客户端的E序Q没有再创徏新的socketQ而仍然用原来大厅房间客L跟游戏逻辑服务器交互用的socket。也是_q是两个q程q的同一个socketQ不要小看这一炏V如果你在创建桌子客LE序后又新徏了一个新的socket与游戏逻辑服务器进行通信Q那么由此带来的玩家q入、退出、逃跑{消息会带来非常ȝ的数据同步问题,俺在刚开始的时候就深受其害。而一旦共用了同一个socket后,你如果退出桌子,服务器不涉及释放socket的问题,所以,q里少了很多的数据同步问题。关于多个进E如何共享同一个socket的问题,请去google以下内容QWSADuplicateSocket?br>
以上便是我根据最新的QQ游戏抓包l果分析得到的QQ游戏的通信程Q当Ӟq个程更多的是客户端如何与服务器之间交互的Q却没有涉及到服务器彼此之间是如何通信和作数据同步的。关于服务器之间的通信程Q我们只能基于自ql验和猜惻I得出以下xQ?br>
1.Login Server与Game Hall Server之前的通信问题。Login Server是负责用户验证的Q一旦验证通过之后Q它要设法让Game Hall Server知道q个消息。它们之前实C息交的途径Q我惛_能有q样几条Qa. Login Server通过验证的用户存攑ֈ临时数据库中Qb. Login Server验证通过的用户存攑֜内存中,当然Q这个信息,应该是全局可访问的Q就是说所有QQ的Game Hall Server都可以通过服务器之间的数据包通信去获得这L信息?br>
2.Game Hall Server的最新房间目录信息的取得。这个信息,是全局的,也就是整个游戏中Q只保留一个目录。它的信息来源,可以由底层的戉K服务器逐报上来,报给谁?我认为就如保存的全局d列表一P它报l保存全局d列表的那个服务器或数据库?br>
3.在QQ游戏中,同一cd的游戏,无法打开两上以上的游戏房间。这个信息的判定Q可以根据全局信息来判定?br>
以上关于服务器之间如何通信的内容,均属于个人猜惻IQQ到底怎么作的Q恐怕只有等大家中的某一位进了腾讯之后才知道了。呵c不q,有一Ҏ可以肯定的,在整个服务器架构中,应该有一个地Ҏ专门保存了全局的登录玩家列表,只有q样才能保证玩家不会重复d以及q入多个相同cd的房间?br>
在前面的描述中,我曾l提到过一个问题:当登录当前Game Hall Server不成功时QQQ游戏服务器会选择让客L重定向到另位的服务器ȝ录,事实上,QQ聊天服务器和MSN服务器的d也是cM的,它也存在d重定向问题?br>
那么Q这引Z另外的问题,p来作q个{略选择Q以及由谁来提供q样的选择资源Q这L处理Q便是负责负载均衡的服务器的处理范围了。由QQ游戏的通信q程分析z出来的针对负责均衡及百万U在U系l的更进一步讨论,在下篇文章中l?br>
在此Q特别感谢网友tilly及某位不侉K露姓名的网友的讨论Q是你们让我军_认真再抓一ơ包探个I竟?br>
<未完待箋>
]]>
ADSL是一U?a title=异步传输模式 href="http://m.shnenglu.com/zh-cn/%E5%BC%82%E6%AD%A5%E4%BC%A0%E8%BE%93%E6%A8%A1%E5%BC%8F">异步传输模式QATMQ?/p>
在电信服务提供商端,需要将每条开通ADSL业务的电话线路连接在数字用户U\讉K多\复用?/em>QDSLAMQ上。而在用户端,用户需要用一?em>ADSLl端Q因为和传统?a title=调制解调?href="http://m.shnenglu.com/zh-cn/%E8%B0%83%E5%88%B6%E8%A7%A3%E8%B0%83%E5%99%A8">调制解调?/font>QModemQ类|所以也被称?#8220;?#8221;Q来q接电话U\。由于ADSL使用高频信号Q所以在两端q都要?em>ADSL信号分离?/em>ADSL数据信号和普通音频电话信号分d来,避免打电话的时候出现噪韛_扰?/p>
通常的ADSLl端有一个电话Line-InQ一?a title=以太|?href="http://m.shnenglu.com/zh-cn/%E4%BB%A5%E5%A4%AA%E7%BD%91">以太|?/font>口,有些l端集成了ADSL信号分离器,q提供一个连接的Phone接口?/p>
某些ADSL调制解调器用USB接口与电脑相q,需要在电脑上安装指定的软g以添加虚拟网卡来q行通信?/p>
׃受到传输高频信号的限ӞADSL需要电信服务提供商端接入设备和用户l端之间的距M能超q?千米Q也是用户的电话线q到电话局的距M能超q?千米?/p>
ADSL讑֤在传输中需要遵循以下标准之一Q?/p>
q有一些更快更新的标准Q但是目前还很少有电信服务提供商使用Q?/p>
当电信服务提供商的设备端和用L端之间距d?.3千米的时候,q可以用速率更高?a title=VDSL href="http://m.shnenglu.com/zh-cn/VDSL">VDSLQ它的速率可以辑ֈ下行55.2MbpsQ上?9.2Mbps?/p>
ADSL通常提供三种|络d方式Q?/p>
后两U通常不提供静态IPQ而是动态的l用户分配网l地址?/p>
?上一文?/font>l束Ӟ我描qC一个比较特D的NQ如果线E正在等待某个特定条件发生,它应该如何处理这U情况?它可以重复对互斥对象锁定和解锁,每次都会查共享数据结构,以查找某个倹{但q是在浪Ҏ间和资源Q而且q种J忙查询的效率非怽。解册个问题的最x法是使用 pthread_cond_wait() 调用来等待特D条件发生? 了解 pthread_cond_wait() 的作用非帔R?-- 它是 POSIX U程信号发送系l的核心Q也是最难以理解的部分?/p>
首先Q让我们考虑以下情况Q线Eؓ查看已链接列表而锁定了互斥对象Q然而该列表恰y是空的。这一特定U程什么也q不?-- 其设计意图是从列表中除去节点Q但是现在却没有节点。因此,它只能: 锁定互斥对象ӞU程调?pthread_cond_wait(&mycond,&mymutex)。pthread_cond_wait() 调用相当复杂Q因此我们每ơ只执行它的一个操作?/p>
pthread_cond_wait() 所做的W一件事是同时对互斥对象解锁(于是其它U程可以修改已链接列表)Qƈ{待条g mycond 发生Q这样当 pthread_cond_wait() 接收到另一个线E的“信号”Ӟ它将苏醒Q。现在互斥对象已被解锁,其它U程可以讉K和修改已链接列表Q可能还会添加项?/p>
此时Qpthread_cond_wait() 调用q未q回。对互斥对象解锁会立卛_生,但等待条?mycond 通常是一个阻塞操作,q意味着U程睡眠,在它苏醒之前不会消?CPU 周期。这正是我们期待发生的情c线E将一直睡眠,直到特定条g发生Q在q期间不会发生Q何浪?CPU 旉的繁忙查询。从U程的角度来看,它只是在{待 pthread_cond_wait() 调用q回?/p>
现在l箋说明Q假讑֏一个线E(UC“2 LE?#8221;Q锁定了 mymutex q对已链接列表添加了一V在对互斥对象解锁之后,2 LE会立即调用函数 pthread_cond_broadcast(&mycond)。此操作之后Q? LE将使所有等?mycond 条g变量的线E立卌醒。这意味着W一个线E(仍处?pthread_cond_wait() 调用中)现在苏醒?/p>
现在Q看一下第一个线E发生了什么。您可能会认为在 2 LE调?pthread_cond_broadcast(&mymutex) 之后Q? LE的 pthread_cond_wait() 会立卌回。不是那P实际上,pthread_cond_wait() 执行最后一个操作:重新锁定 mymutex。一?pthread_cond_wait() 锁定了互斥对象,那么它将q回q允?1 LEl执行。那Ӟ它可以马上检查列表,查看它所感兴的更改?/p>
那个q程非常复杂Q因此让我们先来回顾一下。第一个线E首先调用: 然后Q它查了列表。没有找到感兴趣的东西,于是它调用: 然后Qpthread_cond_wait() 调用在返回前执行许多操作Q?/p>
它对 mymutex 解锁Q然后进入睡眠状态,{待 mycond 以接?POSIX U程“信号”。一旦接收到“信号”Q加引号是因为我们ƈ不是在讨Zl的 UNIX 信号Q而是来自 pthread_cond_signal() ?pthread_cond_broadcast() 调用的信PQ它׃苏醒。但 pthread_cond_wait() 没有立即q回 -- 它还要做一件事Q重新锁?mutexQ?/p>
pthread_cond_wait() 知道我们在查?mymutex “背后”的变化,因此它l操作,为我们锁定互斥对象,然后才返回?/p>
现在已回了 pthread_cond_wait() 调用Q您应该了解了它的工作方式。应该能够叙q?pthread_cond_wait() 依次执行的所有操作。尝试一下。如果理解了 pthread_cond_wait()Q其余部分就相当ҎQ因此请重新阅读以上部分Q直到记住ؓ止。好Q读完之后,能否告诉我在调用 pthread_cond_wait() ??/em>Q互斥对象必d于什么状态?pthread_cond_wait() 调用q回之后Q互斥对象处于什么状态?q两个问题的{案都是“锁定”。既然已l完全理解了 pthread_cond_wait() 调用Q现在来l箋研究更简单的东西 -- 初始化和真正的发送信号和q播q程。到那时Q我们将会对包含了多U程工作队列?C 代码了如指掌? 条g变量是一个需要初始化的真实数据结构。以下就初始化的Ҏ。首先,定义或分配一个条件变量,如下所C: 然后Q调用以下函数进行初始化Q?/p>
瞧,初始化完成了Q在释放或废弃条件变量之前,需要毁坏它Q如下所C: 很简单吧。接着讨论 pthread_cond_wait() 调用?/p>
一旦初始化了互斥对象和条g变量Q就可以{待某个条gQ如下所C: h意,代码在逻辑上应该包?mycond ?mymutex。一个特定条件只能有一个互斥对象,而且条g变量应该表示互斥数据“内部”的一U特D的条g更改。一个互斥对象可以用许多条g变量Q例如,cond_empty、cond_full、cond_cleanupQ,但每个条件变量只能有一个互斥对象?/p>
对于发送信号和q播Q需要注意一炏V如果线E更Ҏ些共享数据,而且它想要唤醒所有正在等待的U程Q则应?pthread_cond_broadcast 调用Q如下所C: 在某些情况下Q活动线E只需要唤醒第一个正在睡眠的U程。假设您只对队列d了一个工作作业。那么只需要唤醒一个工作程序线E(再唤醒其它线E是不礼貌的Q)Q?/p>
此函数只唤醒一个线E。如?POSIX U程标准允许指定一个整敎ͼ可以让您唤醒一定数量的正在睡眠的线E,那就更完了。但是很可惜Q我没有被邀请参加会议?/p>
我将演示如何创徏多线E工作组。在q个Ҏ中,我们创徏了许多工作程序线E。每个线E都会检?wqQ?#8220;工作队列”Q,查看是否有需要完成的工作。如果有需要完成的工作Q那么线E将从队列中除去一个节点,执行q些特定工作Q然后等待新的工作到达?/p>
与此同时Q主U程负责创徏q些工作E序U程、将工作d到队列,然后在它退出时攉所有工作程序线E。您会遇到许多 C 代码Q好好准备吧Q?/p>
需要队列是Z两个原因。首先,需要队列来保存工作作业。还需要可用于跟踪已终止线E的数据l构。还记得前几文章(请参阅本文结֤?参考资?/font>Q中Q我曾提到过需要用带有特定进E标识的 pthread_join 吗?使用“清除队列”Q称?"cq"Q可以解x法等?M已终止线E的问题Q稍后将详细讨论q个问题Q。以下是标准队列代码。将此代码保存到文g queue.h ?queue.cQ? 我编写的q不是线E安全的队列例程Q事实上我创Z一?#8220;数据包装”?#8220;控制”l构Q它可以是Q何线E支持的数据l构。看一?control.hQ?/p>
现在您看C data_control l构定义Q以下是它的视觉表示Q?/p>
囑փ中的锁代表互斥对象,它允许对数据l构q行互斥讉K。黄色的星代表条件变量,它可以睡眠,直到所讨论的数据结构改变ؓ止。on/off 开兌C整?"active"Q它告诉U程此数据是否是zd的。在代码中,我用整?active 作ؓ标志Q告诉工作队列何时应该关闭。以下是 control.cQ?/p>
在开始调试之前,q需要一个文件。以下是 dbug.hQ?/p>
此代码用于处理工作组代码中的不可U正错误?/p>
说到工作l代码,以下是Q?/p>
现在来快速初排代码。定义的W一个结构称?"wq"Q它包含?data_control 和队列头。data_control l构用于仲裁Ҏ个队列的讉KQ包括队列中的节炏V下一步工作是定义实际的工作节炏V要使代码符合本文中的示例,此处所包含的都是作业号?/p>
接着Q创建清除队列。注释说明了它的工作方式。好Q现在让我们跌 threadfunc()、join_threads()、create_threads() ?initialize_structs() 调用Q直接蟩?main()。所做的W一件事是初始化结?-- q包括初始化 data_controls 和队列,以及Ȁzd作队列?/p>
现在初始化线E。如果看一?create_threads() 调用Q似乎一切正?-- 除了一件事。请注意Q我们正在分配清除节点,以及初始化它的线E号?TID lg。我们还清除节点作为初始自变量传递给每一个新的工作程序线E。ؓ什么这样做Q?/p>
因ؓ当某个工作程序线E退出时Q它会将其清除节点连接到清除队列Q然后终止。那ӞȝE会在清除队列中到q个节点Q利用条件变量)Qƈ这个节点移出队列。因?TIDQ线E标识)存储在清除节点中Q所以主U程可以切知道哪个U程已终止了。然后,ȝE将调用 pthread_join(tid)Qƈ联接适当的工作程序线E。如果没有做记录Q那么主U程需要按L序联接工作E序U程Q可能是按它们的创徏序。由于线E不一定按此顺序终止,那么ȝE可能会在已l联接了十个U程Ӟ{待联接另一个线E。您能理解这U设计决{是如何使关闭代码加速的吗(其在用几百个工作E序U程的情况下Q? 我们已启动了工作E序U程Q它们已l完成了执行 threadfunc()Q稍后将讨论此函敎ͼQ现在主U程开始将工作节点插入工作队列。首先,它锁?wq 的控制互斥对象,然后分配 16000 个工作包Q将它们逐个插入队列。完成之后,调?pthread_cond_broadcast()Q于是所有正在睡眠的U程会被唤醒Qƈ开始执行工作。此ӞȝE将睡眠两秒钟,然后释放工作队列Qƈ通知工作E序U程l止zd。接着Q主U程会调?join_threads() 函数来清除所有工作程序线E?/p>
现在来讨?threadfunc()Q这是所有工作程序线E都要执行的代码。当工作E序U程启动Ӟ它会立即锁定工作队列互斥对象Q获取一个工作节点(如果有的话)Q然后对它进行处理。如果没有工作,则调?pthread_cond_wait()。您会注意到q个调用在一个非常紧凑的 while() 循环中,q是非常重要的。当?pthread_cond_wait() 调用中苏醒时Q决不能认ؓ条g肯定发生?-- ?可能发生了,也可能没有发生。如果发生了q种情况Q即错误地唤醒了U程Q而列表是I的Q那?while 循环再ơ调?pthread_cond_wait()? 如果有一个工作节点,那么我们只打印它的作业号Q释攑֮q出。然而,实际代码会执行一些更实质性的操作。在 while() 循环l尾Q我们锁定了互斥对象Q以便检?active 变量Q以及在循环剙查新的工作节炏V如果执行完此代码,׃发现如果 wq.control.active ?0Qwhile 循环׃l止Qƈ会执?threadfunc() l尾处的清除代码?/p>
工作E序U程的清除代码部仉常有。首先,׃ pthread_cond_wait() q回了锁定的互斥对象Q它会对 work_queue 解锁。然后,它锁定清除队列,d清除代码Q包含了 TIDQ主U程用此 TID 来调?pthread_join()Q,然后再对清除队列解锁。此后,它发信号l所?cq {待?(pthread_cond_signal(&cq.control.cond))Q于是主U程q道有一个待处理的新节点。我们不使用 pthread_cond_broadcast()Q因为没有这个必?-- 只有一个线E(ȝE)在等待清除队列中的新节点。当它调?join_threads() Ӟ工作E序U程打印关闭消息,然后l止Q等待主U程发出?pthread_join() 调用?/p>
如果要查看关于如何用条件变量的单示例,请参?join_threads() 函数。如果还有工作程序线E,join_threads() 会一直执行,{待清除队列中新的清除节炏V如果有新节点,我们会将此节点移出队列、对清除队列解锁Q从而工作E序可以d清除节点Q、联接新的工作程序线E(使用存储在清除节点中?TIDQ、释放清除节炏V减?#8220;现有”U程的数量,然后l箋?/p>
现在已经C“POSIX U程详解”pd的尾壎ͼ希望您已l准备好开始将多线E代码添加到您自q应用E序中。有兌l信息,请参?参考资?/font>部分Q这部分内容q包含了本文中用的所有源码的 tar 文g。下一个系列中再见Q? Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQGentoo 目的总设计师QMacMillan 出版书籍的撰E作者,他的著作有: Caldera OpenLinux Unleashed, SuSE Linux Unleashed, ?Samba Unleashed。Daniel 自二q起就与计机某些领域l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和新出生的女?Hadassah 一起共度时光。可通过 drobbins@gentoo.org?Daniel 联系? Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQGentoo 目的总设计师QMacMillan 出版书籍的撰E作者,他的著作有: Caldera OpenLinux Unleashed, SuSE Linux Unleashed, ?Samba Unleashed。Daniel 自二q起就与计机某些领域l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和新出生的女?Hadassah 一起共度时光。可通过 drobbins@gentoo.org?Daniel 联系? Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQGentoo 目的总设计师QMacMillan 出版书籍的撰E作者,他的著作有: Caldera OpenLinux Unleashed, SuSE Linux Unleashed, ?Samba Unleashed。Daniel 自二q起就与计机某些领域l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和新出生的女?Hadassah 一起共度时光。可通过 drobbins@gentoo.org?Daniel 联系? Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQGentoo 目的总设计师QMacMillan 出版书籍的撰E作者,他的著作有: Caldera OpenLinux Unleashed, SuSE Linux Unleashed, ?Samba Unleashed。Daniel 自二q起就与计机某些领域l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和新出生的女?Hadassah 一起共度时光。可通过 drobbins@gentoo.org?Daniel 联系? ?前一文章中 Q谈C会导致异常结果的U程代码。两个线E分别对同一个全局变量q行了二十次加一。变量的值最后应该是 40Q但最l值却?21。这是怎么回事呢?因ؓ一个线E不停地“取消”了另一个线E执行的加一操作Q所以生这个问题。现在让我们来查看改正后的代码,它?互斥对象(mutex)来解册问题Q? 如果这D代码与 前一文?/font> 中给出的版本作一个比较,׃注意到增加了 pthread_mutex_lock() ?pthread_mutex_unlock() 函数调用。在U程E序中这些调用执行了不可或缺的功能。他们提供了一U?怺排斥的方法(互斥对象即由此得名)。两个线E不能同时对同一个互斥对象加锁? 互斥对象是这样工作的。如果线E?a 试图锁定一个互斥对象,而此时线E?b 已锁定了同一个互斥对象时Q线E?a 将q入睡眠状态。一旦线E?b 释放了互斥对象(通过 pthread_mutex_unlock() 调用Q,U程 a p够锁定这个互斥对象(换句话说Q线E?a 将?pthread_mutex_lock() 函数调用中返回,同时互斥对象被锁定)。同样地Q当U程 a 正锁定互斥对象时Q如果线E?c 试图锁定互斥对象的话Q线E?c 也将临时q入睡眠状态。对已锁定的互斥对象上调?pthread_mutex_lock() 的所有线E都进入睡眠状态,q些睡眠的线E将“排队”讉Kq个互斥对象?/p>
通常使用 pthread_mutex_lock() ?pthread_mutex_unlock() 来保护数据结构。这是_通过U程的锁定和解锁Q对于某一数据l构Q确保某一时刻只能有一个线E能够访问它。可以推到Q当U程试图锁定一个未加锁的互斥对象时QPOSIX U程库将同意锁定Q而不会ɾU程q入睡眠状态?/p>
图中Q锁定了互斥对象的线E能够存取复杂的数据l构Q而不必担心同时会有其它线E干扰。那个数据结构实际上?#8220;ȝ”了,直到互斥对象被解锁ؓ止。pthread_mutex_lock() ?pthread_mutex_unlock() 函数调用Q如?#8220;在施工中”标志一P正在修改和d的某一特定׃n数据包围h。这两个函数调用的作用就是警告其它线E,要它们l睡眠ƈ{待轮到它们对互斥对象加锁。当Ӟ除非?每个 对特定数据结构进行读写操作的语句前后Q都分别放上 pthread_mutex_lock() ?pthread_mutext_unlock() 调用Q才会出现这U情c?br> 听上d有趣Q但I竟Z么要让线E睡眠呢Q要知道Q线E的主要优点不就是其h独立工作、更多的时候是同时工作的能力吗Q是的,实是这栗然而,每个重要的线E程序都需要用某些互斥对象。让我们再看一下示例程序以便理解原因所在?/p>
L thread_function()Q@环中一开始就锁定了互斥对象,最后才它解锁。在q个CZE序中,mymutex 用来保护 myglobal 的倹{仔l查?thread_function()Q加一代码?myglobal 复制C个局部变量,对局部变量加一Q睡眠一U钟Q在q之后才把局部变量的g回给 myglobal。不使用互斥对象Ӟ即ȝE在 thread_function() U程睡眠一U钟期间内对 myglobal 加一Qthread_function() 苏醒后也会覆盖主U程所加的倹{用互斥对象能够保证这U情形不会发生。(您也怼惛_Q我增加了一U钟延迟以触发不正确的结果。把局部变量的Dl?myglobal 之前Q实际上没有什么真正理p?thread_function() 睡眠一U钟。)使用互斥对象的新E序产生了期望的l果Q?/p>
Zq一步探索这个极为重要的概念Q让我们看一看程序中q行加一操作的代码:
标准Q?br>
传输标准
|络d标准
]]>
本文?POSIX U程三部曲系列的最后一部分QDaniel 详l讨论如何用条件变量。条件变量是 POSIX U程l构Q可以让您在遇到某些条g?#8220;唤醒”U程。可以将它们看作是一U线E安全的信号发送。Daniel 使用目前您所学到的知识实C一个多U程工作l应用程序,本文围l着q一CZ而进行讨论?/blockquote>
pthread_mutex_lock(&mymutex);
pthread_cond_wait(&mycond, &mymutex);
pthread_mutex_unlock(&mymutex);
pthread_mutex_lock(&mymutex);
pthread_cond_t mycond;
pthread_cond_init(&mycond,NULL);
pthread_cond_destroy(&mycond);
pthread_cond_wait(&mycond, &mymutex);
pthread_cond_broadcast(&mycond);
pthread_cond_signal(&mycond);
queue.h
/* queue.h
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
*/
typedef struct node {
struct node *next;
} node;
typedef struct queue {
node *head, *tail;
} queue;
void queue_init(queue *myroot);
void queue_put(queue *myroot, node *mynode);
node *queue_get(queue *myroot);
queue.c
/* queue.c
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
**
** This set of queue functions was originally thread-aware. I
** redesigned the code to make this set of queue routines
** thread-ignorant (just a generic, boring yet very fast set of queue
** routines). Why the change? Because it makes more sense to have
** the thread support as an optional add-on. Consider a situation
** where you want to add 5 nodes to the queue. With the
** thread-enabled version, each call to queue_put() would
** automatically lock and unlock the queue mutex 5 times -- that's a
** lot of unnecessary overhead. However, by moving the thread stuff
** out of the queue routines, the caller can lock the mutex once at
** the beginning, then insert 5 items, and then unlock at the end.
** Moving the lock/unlock code out of the queue functions allows for
** optimizations that aren't possible otherwise. It also makes this
** code useful for non-threaded applications.
**
** We can easily thread-enable this data structure by using the
** data_control type defined in control.c and control.h. */
#include <stdio.h>
#include "queue.h"
void queue_init(queue *myroot) {
myroot->head=NULL;
myroot->tail=NULL;
}
void queue_put(queue *myroot,node *mynode) {
mynode->next=NULL;
if (myroot->tail!=NULL)
myroot->tail->next=mynode;
myroot->tail=mynode;
if (myroot->:head==NULL)
myroot->head=mynode;
}
node *queue_get(queue *myroot) {
//get from root
node *mynode;
mynode=myroot->head;
if (myroot->head!=NULL)
myroot->head=myroot->head->next;
return mynode;
}
control.h
#include
typedef struct data_control {
pthread_mutex_t mutex;
pthread_cond_t cond;
int active;
} data_control;
所使用?data_control l构
control.c
/* control.c
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
**
** These routines provide an easy way to make any type of
** data-structure thread-aware. Simply associate a data_control
** structure with the data structure (by creating a new struct, for
** example). Then, simply lock and unlock the mutex, or
** wait/signal/broadcast on the condition variable in the data_control
** structure as needed.
**
** data_control structs contain an int called "active". This int is
** intended to be used for a specific kind of multithreaded design,
** where each thread checks the state of "active" every time it locks
** the mutex. If active is 0, the thread knows that instead of doing
** its normal routine, it should stop itself. If active is 1, it
** should continue as normal. So, by setting active to 0, a
** controlling thread can easily inform a thread work crew to shut
** down instead of processing new jobs. Use the control_activate()
** and control_deactivate() functions, which will also broadcast on
** the data_control struct's condition variable, so that all threads
** stuck in pthread_cond_wait() will wake up, have an opportunity to
** notice the change, and then terminate.
*/
#include "control.h"
int control_init(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_init(&(mycontrol->mutex),NULL))
return 1;
if (pthread_cond_init(&(mycontrol->cond),NULL))
return 1;
mycontrol->active=0;
return 0;
}
int control_destroy(data_control *mycontrol) {
int mystatus;
if (pthread_cond_destroy(&(mycontrol->cond)))
return 1;
if (pthread_cond_destroy(&(mycontrol->cond)))
return 1;
mycontrol->active=0;
return 0;
}
int control_activate(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_lock(&(mycontrol->mutex)))
return 0;
mycontrol->active=1;
pthread_mutex_unlock(&(mycontrol->mutex));
pthread_cond_broadcast(&(mycontrol->cond));
return 1;
}
int control_deactivate(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_lock(&(mycontrol->mutex)))
return 0;
mycontrol->active=0;
pthread_mutex_unlock(&(mycontrol->mutex));
pthread_cond_broadcast(&(mycontrol->cond));
return 1;
}
dbug.h
#define dabort() \
{ printf("Aborting at line %d in source file %s\n",__LINE__,__FILE__); abort(); }
workcrew.c
#include <stdio.h>
#include <stdlib.h>
#include "control.h"
#include "queue.h"
#include "dbug.h"
/* the work_queue holds tasks for the various threads to complete. */
struct work_queue {
data_control control;
queue work;
} wq;
/* I added a job number to the work node. Normally, the work node
would contain additional data that needed to be processed. */
typedef struct work_node {
struct node *next;
int jobnum;
} wnode;
/* the cleanup queue holds stopped threads. Before a thread
terminates, it adds itself to this list. Since the main thread is
waiting for changes in this list, it will then wake up and clean up
the newly terminated thread. */
struct cleanup_queue {
data_control control;
queue cleanup;
} cq;
/* I added a thread number (for debugging/instructional purposes) and
a thread id to the cleanup node. The cleanup node gets passed to
the new thread on startup, and just before the thread stops, it
attaches the cleanup node to the cleanup queue. The main thread
monitors the cleanup queue and is the one that performs the
necessary cleanup. */
typedef struct cleanup_node {
struct node *next;
int threadnum;
pthread_t tid;
} cnode;
void *threadfunc(void *myarg) {
wnode *mywork;
cnode *mynode;
mynode=(cnode *) myarg;
pthread_mutex_lock(&wq.control.mutex);
while (wq.control.active) {
while (wq.work.head==NULL && wq.control.active) {
pthread_cond_wait(&wq.control.cond, &wq.control.mutex);
}
if (!wq.control.active)
break;
//we got something!
mywork=(wnode *) queue_get(&wq.work);
pthread_mutex_unlock(&wq.control.mutex);
//perform processing...
printf("Thread number %d processing job %d\n",mynode->threadnum,mywork->jobnum);
free(mywork);
pthread_mutex_lock(&wq.control.mutex);
}
pthread_mutex_unlock(&wq.control.mutex);
pthread_mutex_lock(&cq.control.mutex);
queue_put(&cq.cleanup,(node *) mynode);
pthread_mutex_unlock(&cq.control.mutex);
pthread_cond_signal(&cq.control.cond);
printf("thread %d shutting down...\n",mynode->threadnum);
return NULL;
}
#define NUM_WORKERS 4
int numthreads;
void join_threads(void) {
cnode *curnode;
printf("joining threads...\n");
while (numthreads) {
pthread_mutex_lock(&cq.control.mutex);
/* below, we sleep until there really is a new cleanup node. This
takes care of any false wakeups... even if we break out of
pthread_cond_wait(), we don't make any assumptions that the
condition we were waiting for is true. */
while (cq.cleanup.head==NULL) {
pthread_cond_wait(&cq.control.cond,&cq.control.mutex);
}
/* at this point, we hold the mutex and there is an item in the
list that we need to process. First, we remove the node from
the queue. Then, we call pthread_join() on the tid stored in
the node. When pthread_join() returns, we have cleaned up
after a thread. Only then do we free() the node, decrement the
number of additional threads we need to wait for and repeat the
entire process, if necessary */
curnode = (cnode *) queue_get(&cq.cleanup);
pthread_mutex_unlock(&cq.control.mutex);
pthread_join(curnode->tid,NULL);
printf("joined with thread %d\n",curnode->threadnum);
free(curnode);
numthreads--;
}
}
int create_threads(void) {
int x;
cnode *curnode;
for (x=0; x<NUM_WORKERS; x++) {
curnode=malloc(sizeof(cnode));
if (!curnode)
return 1;
curnode->threadnum=x;
if (pthread_create(&curnode->tid, NULL, threadfunc, (void *) curnode))
return 1;
printf("created thread %d\n",x);
numthreads++;
}
return 0;
}
void initialize_structs(void) {
numthreads=0;
if (control_init(&wq.control))
dabort();
queue_init(&wq.work);
if (control_init(&cq.control)) {
control_destroy(&wq.control);
dabort();
}
queue_init(&wq.work);
control_activate(&wq.control);
}
void cleanup_structs(void) {
control_destroy(&cq.control);
control_destroy(&wq.control);
}
int main(void) {
int x;
wnode *mywork;
initialize_structs();
/* CREATION */
if (create_threads()) {
printf("Error starting threads... cleaning up.\n");
join_threads();
dabort();
}
pthread_mutex_lock(&wq.control.mutex);
for (x=0; x<16000; x++) {
mywork=malloc(sizeof(wnode));
if (!mywork) {
printf("ouch! can't malloc!\n");
break;
}
mywork->jobnum=x;
queue_put(&wq.work,(node *) mywork);
}
pthread_mutex_unlock(&wq.control.mutex);
pthread_cond_broadcast(&wq.control.cond);
printf("sleeping...\n");
sleep(2);
printf("deactivating work queue...\n");
control_deactivate(&wq.control);
/* CLEANUP */
join_threads();
cleanup_structs();
}
]]>
POSIX U程是提高代码响应和性能的有力手Dc在此三部分pd文章的第二篇中,Daniel Robbins 说明,如何使用被称Z斥对象的灵y玩意,来保护线E代码中׃n数据l构的完整性?/blockquote>
thread3.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;
void *thread_function(void *arg) {
int i,j;
for ( i=0; i<20; i++) {
pthread_mutex_lock(&mymutex);
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
pthread_mutex_unlock(&mymutex);
}
return NULL;
}
int main(void) {
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
printf("error creating thread.");
abort();
}
for ( i=0; i<20; i++) {
pthread_mutex_lock(&mymutex);
myglobal=myglobal+1;
pthread_mutex_unlock(&mymutex);
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) ) {
printf("error joining thread.");
abort();
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}
Lq幅L的O画,四个精灵重C最q一?pthread_mutex_lock() 调用的一个场面?/strong>
$ ./thread3
o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooo
myglobal equals 40
thread_function() 加一代码Q?
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
ȝE加一代码Q?
myglobal=myglobal+1;
如果代码是位于单U程E序中,可以预期 thread_function() 代码完整执行。接下来才会执行ȝE代码(或者是以相反的序执行Q。在不用互斥对象的U程E序中,代码可能Q几乎是Q由于调用了 sleep() 的缘故)以如下的序执行Q?/p>
thread_function() U程 ȝE? j=myglobal; j=j+1; printf("."); fflush(stdout); sleep(1); myglobal=myglobal+1; myglobal=j; |
当代码以此特定顺序执行时Q将覆盖ȝE对 myglobal 的修攏V程序结束后Q就得C正确的倹{如果是在操U|针的话,可能生段错误。注意到 thread_function() U程按顺序执行了它的所有指令。看来不象是 thread_function() 有什么次序颠倒。问题是Q同一旉内,另一个线E对同一数据l构q行了另一个修攏V?/p>
在解释如何确定在何处使用互斥对象之前Q先来深入了解一下线E的内部工作机制。请看第一个例子:
假设ȝE将创徏三个新线E:U程 a、线E?b 和线E?c。假定首先创建线E?aQ然后是U程 bQ最后创建线E?c?/p>
pthread_create( &thread_a, NULL, thread_function, NULL); pthread_create( &thread_b, NULL, thread_function, NULL); pthread_create( &thread_c, NULL, thread_function, NULL); |
在第一?pthread_create() 调用完成后,可以假定U程 a 不是已存在就是已l束q停止。第二个 pthread_create() 调用后,ȝE和U程 b 都可以假定线E?a 存在Q或已停止)?/p>
然而,在W二?create() 调用q回后,ȝE无法假定是哪一个线E(a ?bQ会首先开始运行。虽然两个线E都已存在,U程 CPU 旉片的分配取决于内核和U程库。至于谁首先运行,q没有严格的规则。尽线E?a 更有可能在线E?b 之前开始执行,但这q无保证。对于多处理器系l,情况更是如此。如果编写的代码假定在线E?b 开始执行之前实际上执行U程 a 的代码,那么Q程序最l正运行的概率?99%。或者更p糕Q程序在您的机器?100% 地正运行,而在您客L四处理器服务器上正确q行的概率却是零?/p>
从这个例子还可以得知Q线E库保留了每个单独线E的代码执行序。换句话_实际上那三个 pthread_create() 调用按它们出现的顺序执行。从ȝE上来看Q所有代码都是依ơ执行的。有Ӟ可以利用q一Ҏ优化部分U程E序。例如,在上例中Q线E?c 可以假定线E?a 和线E?b 不是正在q行是已经l止。它不必担心存在q没有创建线E?a 和线E?b 的可能性。可以用这一逻辑来优化线E程序?/p>
现在来看另一个假想的例子。假设有许多U程Q他们都正在执行下列代码Q?/p>
myglobal=myglobal+1; |
那么Q是否需要在加一操作语句前后分别锁定和解锁互斥对象呢Q也许有Z?#8220;?#8221;。编译器极有可能把上q赋D句编译成一条机器指令。大安知道Q不可能"半?中断一条机器指令。即使是g中断也不会破坏机器指令的完整性。基于以上考虑Q很可能們于完全省?pthread_mutex_lock() ?pthread_mutex_unlock() 调用。不要这样做?/p>
我在说废话吗Q不完全是这栗首先,不应该假定上q赋D句一定会被编译成一条机器指令,除非亲自验证了机器代码。即使插入某些内嵌汇~语句以保加一操作的完整执行――甚臻I即是自己动手写~译器!-- 仍然可能有问题?/p>
{案在这里。用单条内嵌汇~操作码在单处理器系l上可能不会有什么问题。每个加一操作都将完整地进行,q且多半会得到期望的l果。但是多处理器系l则截然不同。在?CPU 机器上,两个单独的处理器可能会在几乎同一时刻Q或者,在同一时刻Q执行上q赋D句。不要忘了,q时对内存的修改需要先?L1 写入 L2 高速缓存、然后才写入d。(SMP 机器q不只是增加了处理器而已Q它q有用来仲裁?RAM 存取的特D硬件。)最l,Ҏ无法搞清在写入主存的竞争中,哪个 CPU 会"胜出"。要产生可预的代码Q应使用互斥对象。互斥对象将插入一?内存兛_"Q由它来保对主存的写入按照U程锁定互斥对象的顺序进行?/p>
考虑一U以 32 位块为单位更C存的 SMP 体系l构。如果未使用互斥对象对一?64 位整数进行加一操作Q整数的最?4 位字节可能来自一?CPUQ而其?4 个字节却来自另一 CPU。糟p吧Q最p糕的是Q用差劲的技术,您的E序在重要客Lpȝ上有可能不是很长旉才崩溃一ơ,是早上三点钟就崩溃。David R. Butenhof 在他的《POSIX U程~程》(请参阅本文末 参考资?/font>部分Q一书中Q讨Z׃未用互斥对象而将产生的种U情c?
如果攄了过多的互斥对象Q代码就没有什么ƈ发性可aQ运行v来也比单U程解决Ҏ慢。如果放|了q少的互斥对象,代码出现奇怪和令h尬的错误。幸q的是,有一个中间立场。首先,互斥对象是用于串行化存取*׃n数据*。不要对非共享数据用互斥对象,q且Q如果程序逻辑保M时候都只有一个线E能存取特定数据l构Q那么也不要使用互斥对象?/p>
其次Q如果要使用׃n数据Q那么在诅R写׃n数据旉应用互斥对象。用 pthread_mutex_lock() ?pthread_mutex_unlock() 把读写部分保护v来,或者在E序中不固定的地斚wZ用它们。学会从一个线E的角度来审视代码,q确保程序中每一个线E对内存的观炚w是一致和合适的。ؓ了熟悉互斥对象的用法Q最初可能要花好几个时来编写代码,但是很快׃习惯q且*?不必多想p够正用它们?/p>
现在该来看看使用互斥对象的各U不同方法了。让我们从初始化开始。在 thread3.c CZ 中,我们使用了静态初始化Ҏ。这需要声明一?pthread_mutex_t 变量Qƈ赋给它常?PTHREAD_MUTEX_INITIALIZERQ?
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER; |
很简单吧。但是还可以动态地创徏互斥对象。当代码使用 malloc() 分配一个新的互斥对象时Q用这U动态方法。此Ӟ静态初始化Ҏ是行不通的Qƈ且应当用例E?pthread_mutex_init()Q?/p>
int pthread_mutex_init( pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr) |
正如所C,pthread_mutex_init 接受一个指针作为参C初始化ؓ互斥对象Q该指针指向一块已分配好的内存区。第二个参数Q可以接受一个可选的 pthread_mutexattr_t 指针。这个结构可用来讄各种互斥对象属性。但是通常q不需要这些属性,所以正常做法是指定 NULL?/p>
一旦?pthread_mutex_init() 初始化了互斥对象Q就应?pthread_mutex_destroy() 消除它。pthread_mutex_destroy() 接受一个指?pthread_mutext_t 的指针作为参敎ͼq攑ֈZ斥对象时分配l它的Q何资源。请注意Q?pthread_mutex_destroy() 不会 释放用来存储 pthread_mutex_t 的内存。释放自q内存完全取决于您。还必须注意一点,pthread_mutex_init() ?pthread_mutex_destroy() 成功旉q回零?
pthread_mutex_lock(pthread_mutex_t *mutex) |
pthread_mutex_lock() 接受一个指向互斥对象的指针作ؓ参数以将光定。如果碰巧已l锁定了互斥对象Q调用者将q入睡眠状态。函数返回时Q将唤醒调用者(昄Qƈ且调用者还保留该锁。函数调用成功时q回Ӟp|时返回非零的错误代码?/p>
pthread_mutex_unlock(pthread_mutex_t *mutex) |
pthread_mutex_unlock() ?pthread_mutex_lock() 盔R合,它把U程已经加锁的互斥对象解锁。始l应该尽快对已加锁的互斥对象q行解锁Q以提高性能Q。ƈ且绝对不要对您未保持锁的互斥对象q行解锁操作Q否则,pthread_mutex_unlock() 调用失败ƈ带一个非零的 EPERM q回|?/p>
pthread_mutex_trylock(pthread_mutex_t *mutex) |
当线E正在做其它事情的时候(׃互斥对象当前是锁定的Q,如果希望锁定互斥对象Q这个调用就相当方便。调?pthread_mutex_trylock() 时将试锁定互斥对象。如果互斥对象当前处于解锁状态,那么您将获得该锁q且函数返回零。然而,如果互斥对象已锁定,q个调用也不会阻塞。当Ӟ它会q回非零?EBUSY 错误倹{然后可以l做其它事情Q稍后再试锁定?/p>
互斥对象是线E程序必需的工P但它们ƈ非万能的。例如,如果U程正在{待׃n数据内某个条件出玎ͼ那会发生什么呢Q代码可以反复对互斥对象锁定和解锁,以检查值的M变化。同Ӟq要快速将互斥对象解锁Q以便其它线E能够进行Q何必需的更攏V这是一U非常可怕的ҎQ因为线E需要在合理的时间范围内频繁地@环检变化?/p>
在每ơ检查之_可以让调用线E短暂地q入睡眠Q比如睡眠三U钟Q但是因此线E代码就无法最快作出响应。真正需要的是这样一U方法,当线E在{待满某些条g时ɾU程q入睡眠状态。一旦条件满Iq需要一U方法以唤醒因等待满特定条件而睡眠的U程。如果能够做到这一点,U程代码是非常高效的,q且不会占用宝贵的互斥对象锁。这正是 POSIX 条g变量能做的事Q?/p>
?POSIX 条g变量是我下一文章的主题Q其中将说明如何正确使用条g变量。到那时Q您拥有了创徏复杂U程E序所需的全部资源,那些U程E序可以模拟工作人员、装配线{等。既然您已经来熟悉线E,我将在下一文章中加快q度。这P在下一文章的l尾p放上一个相对复杂的U程E序。说到等到条件生,下次再见Q?/p>
![]() |
||
|
![]() |
Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQ?Gentoo 目的总设计师Q多?MacMillan 出版书籍的作者,包括Q?Caldera OpenLinux Unleashed?SuSE Linux Unleashed?Samba Unleashed 。Daniel 自小学二q起就与计机l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和刚出生的女?Hadassah 一起共渡时光。可通过 drobbins@gentoo.org ?Daniel 取得联系? |
Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQ?Gentoo 目的总设计师Q多?MacMillan 出版书籍的作者,包括Q?Caldera OpenLinux Unleashed?SuSE Linux Unleashed?Samba Unleashed 。Daniel 自小学二q起就与计机l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和刚出生的女?Hadassah 一起共渡时光。可通过 drobbins@gentoo.org ?Daniel 取得联系?
[原文地址]http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/
POSIXQ可UL操作pȝ接口Q线E是提高代码响应和性能的有力手Dc在本系列中QDaniel Robbins 向您_地展C在~程中如何用线E。其中还涉及大量q后l节Q读完本pd文章Q您完全可以q用 POSIX U程创徏多线E程序?/p>
了解如何正确q用U程是每一个优UE序员必备的素质。线E类gq程。如同进E,U程由内核按旉分片q行理。在单处理器pȝ中,内核使用旉分片来模拟线E的q发执行Q这U方式和q程的相同。而在多处理器pȝ中,如同多个q程Q线E实际上一样可以ƈ发执行?/p>
那么Z么对于大多数合作性Q务,多线E比多个独立的进E更优越呢?q是因ؓQ线E共享相同的内存I间。不同的U程可以存取内存中的同一个变量。所以,E序中的所有线E都可以L写声明过的全局变量。如果曾?fork() ~写q重要代码,׃认识到这个工L重要性。ؓ什么呢Q虽?fork() 允许创徏多个q程Q但它还会带来以下通信问题: 如何让多个进E相互通信Q这里每个进E都有各自独立的内存I间。对q个问题没有一个简单的{案。虽然有许多不同U类的本?IPC (q程间通信Q,但它们都遇到两个重要障碍Q?/p>
双重坏事: 开销和复杂性都非好事。如果曾lؓ了支?IPC 而对E序大动q戈q,那么您就会真正欣赏线E提供的单共享内存机制。由于所有的U程都驻留在同一内存I间QPOSIX U程无需q行开销大而复杂的长距调用。只要利用简单的同步机制Q程序中所有的U程都可以读取和修改已有的数据结构。而无需数据经由文件描q符转储或挤入紧H的׃n内存I间。仅此一个原因,p以让您考虑应该采用单进E?多线E模式而非多进E?单线E模式?/p>
不仅如此。线E同栯是非常快L。与标准 fork() 相比Q线E带来的开销很小。内核无需单独复制q程的内存空间或文g描述W等{。这p省了大量?CPU 旉Q得线E创建比新进E创建快上十C癑ր。因一点,可以大量使用U程而无需太过于担心带来的 CPU 或内存不뀂?fork() 时导致的大量 CPU 占用也不复存在。这表示只要在程序中有意义,通常可以创建线E?/p>
当然Q和q程一PU程利用多 CPU。如果Y件是针对多处理器pȝ设计的,q就真的是一大特性(如果软g是开放源码,则最l可能在不少q_上运行)。特定类型线E程序(其?CPU 密集型程序)的性能随pȝ中处理器的数目几乎线性地提高。如果正在编?CPU 非常密集型的E序Q则l对惌法在代码中用多U程。一旦掌握了U程~码Q无需使用J琐?IPC 和其它复杂的通信机制Q就能够以全新和创造性的Ҏ解决~码N。所有这些特性配合在一起得多U程~程更有、快速和灉|?/p>
如果熟悉 Linux ~程Q就有可能知?__clone() pȝ调用。__clone() cM?fork()Q同时也有许多线E的Ҏ。例如,使用 __clone()Q新的子q程可以有选择地共享父q程的执行环境(内存I间Q文件描q符{)。这是好的一面。但 __clone() 也有不之处。正如__clone() 在线帮助指出Q?/p>
“__clone 调用是特定于 Linux q_的,不适用于实现可UL的程序。欲~写U程化应用程序(多线E控制同一内存I间Q,最好用实?POSIX 1003.1c U程 API 的库Q例?Linux-Threads 库。参?pthread_create(3thr)?#8221;
虽然 __clone() 有线E的许多Ҏ,但它是不可移植的。当然这q不意味着代码中不能用它。但在Y件中考虑使用 __clone() 时应当权衡这一事实。值得庆幸的是Q正?__clone() 在线帮助指出Q有一U更好的替代ҎQPOSIX U程。如果想~写 可移植的 多线E代码,代码可运行于 Solaris、FreeBSD、Linux 和其它^収ͼPOSIX U程是一U当然之选?/p>
下面是一?POSIX U程的简单示例程序:
#include <pthread.h> #include <stdlib.h> #include <unistd.h> void *thread_function(void *arg) { int i; for ( i=0; i<20; i++) { printf("Thread says hi!\n"); sleep(1); } return NULL; } int main(void) { pthread_t mythread; if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { printf("error creating thread."); abort(); } if ( pthread_join ( mythread, NULL ) ) { printf("error joining thread."); abort(); } exit(0); } |
要编译这个程序,只需先将E序存ؓ thread1.cQ然后输入:
$ gcc thread1.c -o thread1 -lpthread |
q行则输入:
$ ./thread1 |
thread1.c 是一个非常简单的U程E序。虽然它没有实现什么有用的功能Q但可以帮助理解U程的运行机制。下面,我们一步一步地了解q个E序是干什么的。main() 中声明了变量 mythreadQ类型是 pthread_t。pthread_t cd?pthread.h 中定义,通常UCؓ“U程 id”Q羃写ؓ "tid"Q。可以认为它是一U线E句柄?/p>
mythread 声明后(C mythread 只是一?"tid"Q或是将要创建的U程的句柄)Q调?pthread_create 函数创徏一个真实活动的U程。不要因?pthread_create() ?"if" 语句内而受其迷惑。由?pthread_create() 执行成功时返回零而失败时则返回非零|?pthread_create() 函数调用攑֜ if() 语句中只是ؓ了方便地失败的调用。让我们查看一?pthread_create 参数。第一个参?&mythread 是指?mythread 的指针。第二个参数当前?NULLQ可用来定义U程的某些属性。由于缺省的U程属性是适用的,只需该参数设ؓ NULL?/p>
W三个参数是新线E启动时调用的函数名。本例中Q函数名?thread_function()。当 thread_function() q回Ӟ新线E将l止。本例中Q线E函数没有实现大的功能。它仅将 "Thread says hi!" 输出 20 ơ然后退出。注?thread_function() 接受 void * 作ؓ参数Q同时返回值的cd也是 void *。这表明可以?void * 向新U程传递Q意类型的数据Q新U程完成时也可返回Q意类型的数据。那如何向线E传递一个Q意参敎ͼ很简单。只要利?pthread_create() 中的W四个参数。本例中Q因为没有必要将M数据传给微不道?thread_function()Q所以将W四个参数设?NULL?/p>
您也许已推测刎ͼ?pthread_create() 成功q回之后Q程序将包含两个U程。等一{, 两个 U程Q我们不是只创徏了一个线E吗Q不错,我们只创Z一个进E。但是主E序同样也是一个线E。可以这L解:如果~写的程序根本没有?POSIX U程Q则该程序是单线E的Q这个单U程UCؓ“?#8221;U程Q。创Z个新U程之后E序d有两个U程了?
我想此时您至有两个重要问题。第一个问题,新线E创Z后主U程如何q行。答案,ȝE按序l箋执行下一行程序(本例中执?"if (pthread_join(...))"Q。第二个问题Q新U程l束时如何处理。答案,新线E先停止Q然后作为其清理q程的一部分Q等待与另一个线E合q或“q接”?/p>
现在Q来看一?pthread_join()。正?pthread_create() 一个线E拆分ؓ两个Q?pthread_join() 两个线E合qؓ一个线E。pthread_join() 的第一个参数是 tid mythread。第二个参数是指?void 指针的指针。如?void 指针不ؓ NULLQpthread_join 线E的 void * q回值放|在指定的位|上。由于我们不必理?thread_function() 的返回|所以将其设?NULL.
您会注意?thread_function() ׃ 20 U才完成。在 thread_function() l束很久之前Q主U程已l调用了 pthread_join()。如果发生这U情况,ȝE将中断Q{向睡眠)然后{待 thread_function() 完成。当 thread_function() 完成? pthread_join() 返回。这时程序又只有一个主U程。当E序退出时Q所有新U程已经使用 pthread_join() 合ƈ了。这是应该如何处理在程序中创徏的每个新U程的过E。如果没有合q一个新U程Q则它仍然对pȝ的最大线E数限制不利。这意味着如果未对U程做正的清理Q最l会D pthread_create() 调用p|?/p>
如果使用q?fork() pȝ调用Q可能熟悉父q程和子q程的概c当?fork() 创徏另一个新q程Ӟ新进E是子进E,原始q程是父q程。这创徏了可能非常有用的层次关系Q尤其是{待子进E终止时。例如,waitpid() 函数让当前进E等待所有子q程l止。waitpid() 用来在父q程中实现简单的清理q程?/p>
?POSIX U程更有意思。您可能已经注意到我一直有意避免?#8220;父线E?#8221;?#8220;子线E?#8221;的说法。这是因?POSIX U程中不存在q种层次关系。虽然主U程可以创徏一个新U程Q新U程可以创徏另一个新U程QPOSIX U程标准它们视为等同的层次。所以等待子U程退出的概念在这里没有意义。POSIX U程标准不记录Q?#8220;家族”信息。缺家族信息有一个主要含意:如果要等待一个线E终止,必dU程?tid 传递给 pthread_join()。线E库无法为您断定 tid?/p>
对大多数开发者来说这不是个好消息Q因会有多个线E的E序复杂化。不q不要ؓ此担忧。POSIX U程标准提供了有效地理多个U程所需要的所有工兗实际上Q没有父/子关p这一事实却ؓ在程序中使用U程开辟了更创造性的Ҏ。例如,如果有一个线E称为线E?1Q线E?1 创徏了称为线E?2 的线E,则线E?1 自己没有必要调用 pthread_join() 来合q线E?2Q程序中其它MU程都可以做到。当~写大量使用U程的代码时Q这可能允许发生有的事情。例如,可以创徏一个包含所有已停止U程的全局“ȝE列?#8221;Q然后让一个专门的清理U程专等停止的线E加到列表中。这个清理线E调?pthread_join() 刚停止的线E与自己合ƈ。现在,仅用一个线E就巧妙和有效地处理了全部清理?br>
现在我们来看一些代码,q些代码做了一些意想不到的事情。thread2.c 的代码如下:
#include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> int myglobal; void *thread_function(void *arg) { int i,j; for ( i=0; i<20; i++) { j=myglobal; j=j+1; printf("."); fflush(stdout); sleep(1); myglobal=j; } return NULL; } int main(void) { pthread_t mythread; int i; if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { printf("error creating thread."); abort(); } for ( i=0; i<20; i++) { myglobal=myglobal+1; printf("o"); fflush(stdout); sleep(1); } if ( pthread_join ( mythread, NULL ) ) { printf("error joining thread."); abort(); } printf("\nmyglobal equals %d\n",myglobal); exit(0); } |
如同W一个程序,q个E序创徏一个新U程。主U程和新U程都将全局变量 myglobal 加一 20 ơ。但是程序本w生了某些意想不到的结果。编译代码请输入Q?/p>
$ gcc thread2.c -o thread2 -lpthread |
q行误入:
$ ./thread2 |
输出Q?/p>
$ ./thread2 ..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o myglobal equals 21 |
非常意外吧!因ؓ myglobal 从零开始,ȝE和新线E各自对其进行了 20 ơ加一, E序l束?myglobal 值应当等?40。由?myglobal 输出l果?21Q这其中肯定有问题。但是究竟是什么呢Q?/p>
攑ּ吗?好,让我来解释是怎么一回事。首先查看函?thread_function()。注意如何将 myglobal 复制到局部变?"j" 了吗? 接着?j 加一, 再睡眠一U,然后到这时才新?j 值复制到 myglobalQ这是关键所在。设想一下,如果ȝE就在新U程?myglobal 值复制给 j ?/strong> 立即?myglobal 加一Q会发生什么??thread_function() ?j 的值写?myglobal Ӟp盖了ȝE所做的修改?
当编写线E程序时Q应避免产生q种无用的副作用Q否则只会浪Ҏ_当然Q除了编写关?POSIX U程的文章时有用Q。那么,如何才能排除q种问题呢?
׃是将 myglobal 复制l?j q且{了一U之后才写回时生问题,可以试避免使用临时局部变量ƈ直接?myglobal 加一。虽然这U解x案对q个特定例子适用Q但它还是不正确。如果我们对 myglobal q行相对复杂的数学运,而不是简单的加一Q这U方法就会失效。但是ؓ什么呢Q?/p>
要理解这个问题,必须CU程是ƈ发运行的。即使在单处理器pȝ上运行(内核利用旉分片模拟多Q务)也是可以的,从程序员的角度,惛_两个U程是同时执行的。thread2.c 出现问题是因?thread_function() 依赖以下论据Q在 myglobal 加一之前的大U一U钟期间不会修改 myglobal。需要有些途径让一个线E在?myglobal 做更Ҏ通知其它U程“不要靠近”。我在下一文章中讲解如何做到q一炏V到时候见?/p>
![]() |
||
|
![]() |
Daniel Robbins 居住在新墨西哥州?Albuquerque。他?Gentoo Technologies, Inc. 的总裁?CEOQ?Gentoo 目的总设计师Q多?MacMillan 出版书籍的作者,包括Q?Caldera OpenLinux Unleashed?SuSE Linux Unleashed?Samba Unleashed 。Daniel 自小学二q起就与计机l下不解之缘Q那时他首先接触的是 Logo E序语言Qƈ沉h?Pac-Man 游戏中。这也许是他至今仍担Q SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻?Mary 和刚出生的女?Hadassah 一起共渡时光。可通过 drobbins@gentoo.org ?Daniel Robbins 取得联系? |
集群q不是一个全新的概念Q其实早在七十年代计机厂商和研I机构就开始了寚w系l的研究和开发。由于主要用于科学工E计,所以这些系lƈ不ؓ大家所熟知。直到Linux集群的出玎ͼ集群的概忉|得以qؓ传播。集系l主要分为高可用(High Availability)集群,U?HA 集群Q和高性能计算(High Perfermance Computing)集群Q简U?HPC 集群?/p>
通过下面q篇文章我们可以Ҏ面面了解 Linux 集群涉及的硬件和软g?br>
哪种集适合您?
2000 q?5 ?01 ?/p>
Rawn Shah 作ؓ专家Q在 Linux 现有的开放源码和闭源码集群解决Ҏ斚w为您指点qh|?/blockquote>计算 Linux 中集项目的数量p计算谷中创业公司的数量一栗不?Windows NT 已经受其自n的封闭环境阻,Linux 有大量的集群pȝ可供选择Q适合于不同的用途和需要。但定应该使用哪一个集的工作却没有因此变得简单?/p>
问题的部分原因在于术语集用于不同场合。IT l理可能兛_如何使服务器q行旉更长Q或使应用程序运行得更快Q而数学家可能更关心在服务器上q行大规模数D。两者都需要群集,但是各自需要不同特性的集?/p>
本文调查了不同Ş式的集群以及许多实现中的一部分Q这些实现可以买刎ͼ也可以免费Y件Ş式获得。尽列出的所有解x案ƈ不都是开放源码,但是大多数Y仉遵@分发 Linux 源码的公共惯例,特别是由于那些实现集的常常希望调整pȝ性能Q以满需要?/p>
集群L涉及到机器之间的gq接。在C大多数情况下Q这只是?#8220;快速以太网”|卡和集U器。但在尖端科学领域中Q有许多专ؓ集群设计的网l接口卡?/p>
它们包括 Myricom ?Myrinet、Giganet ?cLAN ?IEEE 1596 标准可~一致接?(SCI)。那些卡的功能不但在集的节点之间提供高带宽Q而且q减gq(发送消息所用的旉Q。对于在节点间交换状态信息以使其操作保持同步情况Q那些gq是臛_重要的?/p>
Myricom 提供|卡和交换机Q其单向互连速度最高可辑ֈ 1.28 Gbps。网卡有两种形式Q铜U型和光U型。铜U型 LAN 可以?10 英尺距离内以全速进行通信Q而在长达 60 英尺距离内以半速进行操作。光U型 Myrinet 可以?6.25 英里长的单模光纤或?340 英尺长的多模光纤上全速运行。Myrinet 只提供直接点到点、基于集U器或基于交换机的网l配|,但在可以q接CL交换光纤数量斚w没有限制。添加交换光U只会增加节炚w的gq。两个直接连接的节点之间的^均gq是 5 ?18 微秒Q比以太|快得多?/p>
最常见的三U群集类型包括高性能U学集、负载均衡群集和高可用性群集?/p>
通常Q第一U涉及ؓ集开发ƈ行编E应用程序,以解军_杂的U学问题。这是ƈ行计的基础Q尽它不用专门的q行计算机,q种计算机内部由十至上万个独立处理器l成。但它却使用商业pȝQ如通过高速连接来链接的一l单处理器或双处理器 PCQƈ且在公共消息传递层上进行通信以运行ƈ行应用程序。因此,您会常常听说又有一U便宜的 Linux 计算机问世了。但它实际是一个计机集Q其处理能力与真的超U计机相等Q通常一套象L集配置开销要超q?$100,000。这对一般h来说g是太贵了Q但与hg百万元的专用超U计机相比q算是便宜的?/p>
负蝲均衡集Z业需求提供了更实用的pȝ。如名称所暗示的,该系l负蝲可以在计机集中尽可能q_地分摊处理。该负蝲可能是需要均衡的应用E序处理负蝲或网l流量负载。这Lpȝ非常适合于运行同一l应用程序的大量用户。每个节炚w可以处理一部分负蝲Qƈ且可以在节点之间动态分配负载,以实现^衡。对于网l流量也是如此。通常Q网l服务器应用E序接受了太多入|流量,以致无法q速处理,q就需要将量发送给在其它节点上q行的网l服务器应用。还可以Ҏ每个节点上不同的可用资源或网l的Ҏ环境来进行优化?/p>
高可用性群集的出现是ؓ了ɾ集的整体服务尽可能可用Q以便考虑计算g和Y件的易错性。如果高可用性群集中的主节点发生了故障,那么q段旉内将由次节点代替它。次节点通常是主节点的镜像,所以当它代替主节点Ӟ它可以完全接其w䆾Qƈ且因此ɾpȝ环境对于用户是一致的?/p>
在群集的q三U基本类型之_l常会发生合与交杂。于是,可以发现高可用性群集也可以在其节点之间均衡用户负蝲Q同时仍试图l持高可用性程度。同P可以从要~入应用E序的群集中扑ֈ一个ƈ行群集,它可以在节点之间执行负蝲均衡。尽集系l本w独立于它在使用的Y件或gQ但要有效运行系l时Q硬件连接将起关键作用?/p>
Giganet 是用?Linux q_的虚拟接?(VI) 体系l构卡的W一家供应商Q提?cLAN 卡和交换机。VI 体系l构是独立于q_的Y件和gpȝQ它?Intel 开发,用于创徏集。它使用自己的网l通信协议在服务器之间直接交换数据Q而不是?IPQƈ且它q不打算成ؓ WAN 可\qpȝ。现在,VI 的未来取决于正在q行?#8220;pȝ I/O l?#8221;的工作,q个组本是 Intel 领导?#8220;下一?I/O”组?IBM ?Compaq 领导?#8220;未来 I/O 组”的合q。Giganet 产品当前可以在节点之间提?1 Gbps 单向通信Q最gqؓ 7 微秒?/p>
IEEE 标准 SCI 的gq更(低于 2.5 微秒Q,q且其单向速度可达?400 MBQ秒 (3.2 Gbps)。SCI 是基于环拓扑的网l系l,不像以太|是星Ş拓扑。这在较大规模的节点之间通信速度更快。更有用的是环面拓扑|络Q它在节点之间有许多环Şl构。两l环面可以用 n ?m 的网DC,其中在每一行和每一列都有一个环形网l。三l环面也cMQ可以用三维立体节点|格表示Q每一层上有一个环形网l。密集超U计ƈ行系l用环面拓扑网l,为成百上千个节点之间的通信提供相对最快的路径?/p>
大多数操作系l的限制因素不是操作pȝ或网l接口,而是服务器的内部 PCI ȝpȝ。几乎所有台?PC 通常有基?32-位,33-MHz PCIQƈ且大多数低端服务器只提供 133 MBQ秒 (1 Gbps)Q这限制了那些网卡的能力。一些昂늚高端服务器,?Compaq Proliant 6500 ?IBM Netfinity 7000 pdQ都?64-位, 66-MHz |卡Q它们能够以四倍速度q行。不q地是,矛盾是更多公怋用低端的pȝQ因此大多数供应商最l生产和销售更多低?PCI |卡。也有专门的 64-位,66-MHz PCI |卡Q但h要贵许多。例如,Intel 提供了这U类型的“快速以太网”|卡Qh格约 $400 ?$500Q几乎是普?PCI 版本h?5 倍?/p>
某些q行集pȝ可以辑ֈ如此高的带宽和低延迟Q其原因是它们通常l过使用|络协议Q如 TCP/IP。虽然网际协议对于广域网很重要,但它包含了太多的开销Q而这些开销在节点相互已知的闭|络集中是不必要的。其实,那些pȝ中有一部分可以在节点之间用直接内存访?(DMA)Q它cM于图形卡和其它外围设备在一台机器中的工作方式。因此横跨群集,可以通过M节点上的M处理器直接访问一UŞ式的分布式共享内存。它们也可以使用低开销的消息传递系l,在节点之间进行通信?/p>
消息传递接?(MPI) 是ƈ行群集系l间消息传递层的最常见实现。MPI 存在几种衍生版本Q但在所有情况下Q它为开发者访问ƈ行应用程序提供了一个公?APIQ这样开发者就不必手工解决如何在群集的节点之间分发代码Dc其中一个,Beowulf pȝ首先?MPI 用作公共~程接口?/p>
很难军_使用哪种高性能集群包。许多都提供cM服务Q但计算的具体要求才是决定性因素。很多情况下Q在那些pȝ中的研究工作只是解决需求的一半,而且使用那些软g需要集包开发者的Ҏ帮助和合作?/p>
当谈?Linux 集群Ӟ许多人的W一反映?Beowulf。那是最著名?Linux U学软g集群pȝ。没有一个包叫做 Beowulf。实际上Q它是一个术语,适用于在 Linux 内核上运行的一l公pY件工兗其中包括流行的软g消息传?APIQ如“消息传送接?#8221;(MPI) ?#8220;q行虚拟?#8221;(PVM)Q对 Linux 内核的修改,以允许结合几个以太网接口、高性能|络驱动器,对虚拟内存管理器的更改,以及分布式进E间通信 (DIPC) 服务。公共全局q程标识I间允许使用 DIPC 机制从Q何节点访问Q何进E。Beowulf q在节点间支持一pdgq通性选g?/p>
Beowulf 可能是考虑 Linux 时注意到的第一个高性能集群pȝQ这只是因ؓ它的q泛使用和支持。关于这个主题,有许多文档和书籍。Beowulf 与以下一些科学集系l之间的差异可以是实际的Q或者只是在产品名称中有差异。例如,管名称不同QAlta Technologies ?AltaCluster 是一?Beowulf pȝ。某些供应商Q如 ParTec AGQ一家d国公司,提供?Beowulf 模型的衍生版本,以包括其它管理接口和通信协议?/p>
Giganet 提供了一U定制的Zg的解x案,它用非 IP 协议在一个科学群集的节点间进行通信。如前所qͼ“虚拟接口”协议通过除去不少协议的开销Q如 IPQ以支持服务器间更快的通信。另外,gpȝ可按千兆比特速度q行Qƈ且gq很短,使它非常适合构徏最多达 256 个节点的U学集。该供应商支?MPIQ这栯多ƈ行应用程序就可以在类似的pȝQ如 BeowulfQ上q行?/p>
它也?Beowulf 的缺点,即不能用作网l负载共享系l,除非惌~写应用E序来监控和分发在服务器间传送的|络包?br>
Legion 试图构徏一个真正的多计机pȝ。这是一个群集,其中每个节点都是一个独立系l,但在用户看来Q整个系l只是一台计机。Legion 设计成支持一C界范围的计算机,׃百万个主Z及数以万亿计的Y件对象组成。在 Legion 中,用户可以创立他们自己的合作小l?/p>
Legion 提供了高性能q行、负载均衡、分布式数据理和容错性?/p>
Legion 提供了高性能q行、负载均衡、分布式数据理和容错性。它通过其容错管理和成员节点间的动态重新配|来支持高可用性。它q有一个可扩充核心Q该核心可以在出现新的改q和q展时动态替换或升。系lƈ不是只接受单一控制Q而是可以׃Q意数量的l织理Q而每个组l都支持整体的自治部分。Legion API 通过其内|的q行性提供了高性能计算?/p>
Legion 需要用特别编写的软gQ以使它可以使用?API 库。它位于用户计算机操作系l之上,协调本地资源和分布式资源。它自动处理资源调度和安全性,q管理上下文I间以描q和讉K整个pȝ中上亿种可能之外的对象。然而,在每个节点上q行Ӟ不需要用系l管理员ҎQƈ且可以用无Ҏ的用户帐可行工作。这增加加?Legion 的节点和用户的灵zL?/p>
Sandia National Lab 中的 Computational Plant 是一个大规模整体q行集Q用于实?TeraFLOPQ万亿次点q算Q计ƈ构徏在商业组件上。整个系l由“可~单?#8221;l成Q这?#8220;可~单?#8221;可以划分成适合不同目的Q计、磁?I/O、网l?I/O、服务管理)。群集中的每个节炚w是一?Linux pȝQ带有专门开发的、提供分区服务的内核U模块。每个分区的功能可以通过装入和卸载内核模块来修攏V?/p>
目分三个阶D完成,开始阶D|原型Q有 128 个基?433-MHz DEC Alpha 21164 的系l,其中每个都有 192 MB RAM ?2 GB 驱动器,怺之间?Myrinet |卡?8-端口?SAN 交换接。第 1 阶段它扩充?400 个基?21164 的工作站Q这些工作站的运行速度?500 MHzQ有 192 MB RAMQ没有存储器Q用 16-端口?SAN 交换Z立方体l构q接hQƈ且运?Red Hat 5.1。当前的W?2 阶段?592 台基?DEC 21264 的机器,它们的运行速度?500 MHzQ有 256 MB RAMQ没有驱动器。每个节炚w使用 64-位,33-MHz PCI Myrinet |卡Qƈ且仍使用 16-端口交换Z立方体l构q接?/p>
?Cplant 上运行的应用E序包括解决E疏线性系l、流体力学和l构力学中计系l的优化、分子力学的模拟、线性结构力学的有限元分析,以及q行应用E序的动态负载均衡库?/p>
香港大学的系l研I小l有一个基?Java 的群集,叫做支持 Java 的单pȝ映像计算体系l构 (JESSICA)Q它作ؓ一个中间g层以完成单系l映像的qL。该层是每个使用分布式共享内?(DSM) pȝq行通信的节点上q行的所有线E的一个全局U程I间。该目使用 ThreadMark DSMQ但最l将用他们自己创建的 JiaJia Using Migrating-home Protocol (JUMP)。他们用定制的Z Java ?ClusterProbe 软g来管理群集的 50 个节炏V?/p>
法国?IRISA 研究所?#8220;大规模数字模拟应用程序的~程q行和分布式pȝ”(PARIS) 目提供了几U用于创?Linux 服务器群集的工具。该目׃部分l成Q群集的资源理软g、ƈ行编E语a的运行时环境Q以及分布式数字模拟的Y件工兗?/p>
资源理软g包括用于׃n内存、磁盘和处理器资源的 Globelins 分布式系l,及其 Dupleix ?Mome 分布式共享内存系l?/p>
负蝲均衡集在多节点之间分发|络或计处理负载。在q种情况下,区别在于~少跨节点运行的单ƈ行程序。大多数情况下,那种集中的每个节点都是q行单独软g的独立系l。但是,不管是在节点之间q行直接通信Q还是通过中央负蝲均衡服务器来控制每个节点的负载,在节点之间都有一U公共关pR通常Q用特定的法来分发该负蝲?/p>
|络量负蝲均衡是一个过E,它检查到某个集的入|流量,然后流量分发到各个节点以进行适当处理。它最适合大型|络应用E序Q如 Web ?FTP 服务器。负载均衡网l应用服务要求群集Y件检查每个节点的当前负蝲Qƈ定哪些节点可以接受新的作业。这最适合q行如数据分析等串行和批处理作业。那些系l还可以配置成关注某特定节点的硬件或操作pȝ功能Q这P集中的节点没有必要是一致的?/p>
“Linux 虚拟服务?#8221;目已经实现了许多内核补丁,它们为入|?TCP/IP 量创徏了负载均衡系l。LVS 软g查入|流量,然后Ҏ负蝲均衡法Q将量重定向到一l充当群集的服务器。这允许|络应用E序Q如 Web 服务器,在节点群集上q行以支持大量用戗?/p>
LVS 支持作ؓ负蝲均衡服务器直接连接到同一?LAN 的群集节点,但它q能够以通道传?IP 包的方式q接到远E服务器。后一U方法包括压~?IP 包中的均衡请求,q些 IP 信息包从负蝲均衡服务器直接发送到q程集节点。尽?LVS 可以q程支持|站的负载均衡,但它使用的负载均衡算法现在对于虚拟群集中的广?Web 服务器仍无效。因此,如果 Web 服务器都在同一?LAN 中,LVS 最好当作负载均衡服务器使用?/p>
负蝲均衡pȝ的几U硬件实现比在通用操作pȝQ如 LinuxQ上q行得更快。它们包括来?Alteon ?Foundry 的硬Ӟ其硬仉辑和最操作系l可以在g中执行流量管理,q且速度比纯软g快。它们的h也很高,通常都在 $10,000 以上。如果需要简单和便宜的解x案,一个有很多内存 (256 MB) 的中{?Linux pȝ会是一个好的负载均衡系l?br>
TurboLinux TurboCluster ?enFuzion
TurboLinux 有一个品叫 TurboClusterQ它最初以“Linux 虚拟服务?#8221;目开发的内核补丁为基。因此,它可以得到大部分优点Q但它的~点也与原来的项目一栗TurboLinux 为此q开发了一些工P用于监控增加产品实用性的集行ؓ。一家主要供应商的商业支持也使它对于大型|站更具吸引力?/p>
EnFuzion 支持在节点之间实现自动负载均衡和资源׃nQ而且可以自动重新安排p|的作业?/p>
EnFuzion ?TurboLinux 卛_推出的科学群集品,它ƈ不基?Beowulf。但是,它可以支持上百个节点以及许多不同的非 Linux q_Q包?Solaris、Windows NT、HP-UX、IBM AIX、SGI Irix ?Tru64。EnFuzion 非常有趣Q因为它q行所有现有YӞq且不需要ؓ环境~写定制的ƈ行应用程序。它支持在节炚w实现自动负蝲均衡和资源共享,而且可以自动重新安排p|的作业?/p>
Platform Computing ?LSF 批处?/span>
Platform Computing 是群集计领域的老手Q现在提供了 Linux q_上的“负蝲均衡设施 (LSF) 批处?#8221;软g。LSF 批处理允怸央控制器安排作业在群集中L数量的节点上q行。在概念上,它类g TurboLinux enFuzion 软gQƈ且支持在节点上运行Q何类型的应用E序?/p>
q种Ҏ对于集大小是非常灵zȝQ因为可以明选择节点的数量,甚至是运行应用程序的节点。于是,可以?64 个节点的集分成更小的逻辑集Q每个逻辑集都运行自q批处理应用程序。而且Q如果应用程序或节点p|Q它可以在其它服务器上重新安排作业?/p>
Platform 的品在主要 Unix pȝ?Windows NT 上运行。目前,只有它们?LSF 批处理品已l移植到 Linux 上。最l,LSF Suite lg的其余部分也紧随其后移植到 Linux 上?br>
Resonate 有一U基于Y件的负蝲均衡ҎQ类g Linux 虚拟服务器。但是,它支持更多特性,以及一些更好的负蝲均衡法。例如,使用 ResonateQ可以在每个集节点装入一个代理,以确定该节点当前的系l负载。然后,负蝲均衡服务器检查每个节点的代理Q以定哪个节点的负载最,q且新的流量发送给它。另外,Resonate q可以用它?Global Dispatch 产品更有效地支持地区性分布式服务器?/p>
Resonate 已经?Red Hat Linux 上彻底测试了该YӞ怿它也可以在其它发行版上运行。Resonate 的Y件还可以在其它各U^Cq行Q包?Solaris、AIX、Windows NTQƈ且它q可以在混合环境中进行负载均衡?/p>
MOSIX 使用 Linux 内核新版本来实现q程负蝲均衡集群pȝ。该集中,M服务器或工作站可以按指定加入或离开Q即d到群集的d理能力,或从中除厅R根据其文档QMOSIX 使用自适应q程负蝲均衡和内存引导算法整体性能最大化。应用程序进E可以在节点之间抢先q移Q以利用最好的资源Q这cM于对U多处理器系l可以在各个处理器之间切换应用程序?/p>
MOSIX 在应用层是完全透明的,q且不需要重新编译或者重新链接到新的库,因ؓ所有一切都发生在内核上。可以有几种Ҏ它配置成多用户׃n环境集。所有服务器可以׃n一个池Q系l可以是集的一部分Q或者群集可以动态地分成几个子群集,每种Ҏ都有不同的用途。Linux 工作站还可以是群集的一部分Q可以是固定的,也可以是临时的,或者只是作为批处理作业提交者。作Z时群集节点,工作站可以在其空闲时用于增加集处理能力。也允许只以批处理方式用群集,在这U方式中Q群集被配置成通过队列接受批处理作业。然后,守护E序取走作业q将它们发送到集节点q行处理?/p>
MOSIX 的不利之处是它更?Linux 内核行ؓ的一些核心部分,于是pȝU应用程序将不会按期望运行?/p>
除了高性能U学计算QMOSIX 提供了一个有的选项Q用于以共同讄创徏集群环境。通过使用服务器和工作站上的闲|资源,它可以更快更有效地创建和q行应用E序。由于访问了多台服务器,q且可以动态调整群集大和更改负蝲均衡规则Q它q可以提供高度的服务器可用性。MOSIX 的不利之处是它更?Linux 内核行ؓ的一些核心部分,于是pȝU应用程序将不会按期望运行。要使用|络应用E序Ӟ而该E序使用Z单个服务器地址的套接字q接QMOSIX 通常也会受到限制。这意味着|络应用E序在一个服务器节点上开始运行时Q如?IP 地址与套接字l定Q那么它必须l箋在该节点上运行。显ӞMOSIX q正在开始迁Ud接字Q因此这很快变成了争论的焦炏V?/p>
高可用?(HA) 集致力于服务器系l的q行速度和响应速度可能快。它们经怋用在多台机器上运行的冗余节点和服务,用来怺跟踪。如果某个节点失败,它的替补在几秒钟或更短旉内接它的职责。因此,对于用户而言Q群集永q不会停机?/p>
某些 HA 集也可以维护节炚w冗余应用E序。因此,用户的应用程序将l箋q行Q即使他或她使用的节点出了故障。正在运行的应用E序会在几秒之内q移到另一个节点,而所有用户只会察觉到响应E微慢了一炏V但是,q种应用E序U冗余要求将软g设计成具有群集意识的Qƈ且知道节点失败时应该做什么。但对于 LinuxQ大多数现在q做不到。因?Linux pȝ没有 HA 集群标准Qƈ且也没有公共 API 可供应用E序开发者构建有集意识的Y件?/p>
HA 集可以执行负蝲均衡Q但通常L务器q行作业Q而系l辅助服务器保持闲|。辅助服务器通常是主服务器操作系l设|的镜像Q尽硬件本w稍有不同。辅助节点对L务器q行zd监控或心跌察,以查看它是否仍在q行。如果心跌时器没有接收C服务器的响应Q则辅助节点接网l和pȝw䆾Q如果是 Linux pȝQ则?IP L名和地址Q?/p>
但是QLinux 在这一领域仍有一点忽略。好消息是有一家著名的供应商正在努力尽快研刉可用性群集,因ؓ它是企业U服务器都必需的功能?/p>
高可用?Linux 目Q根据其目标声明Q旨在ؓ Linux 提供高可用性解x案,以通过C开发成果提高可靠性、可用性和服务能力。Linux 辑ֈ高可用性集时Q这是一U试囄?Linux 与先q的 Unix pȝQ如 Solaris、AIX ?HP/UXQ一样具有竞争力的特性。因此,目的目标是?2001 q之前达?Unix 集群比较报告 ( http://www.sun.com/clusters/dh.brown.pdf) 中分析专家组 D. H. Brown 特定功能性别?
目中有可以l护节点间心跛_ƈ接管p|节点?IP 地址的Y件。如果一个节点失败,它?#8220;伪造冗?IP”软g包将p|节点的地址d到工作节点以承担它的职责。于是,可以在几毫秒旉内自动替换失败的节点。实际用中Q心跳通常在几U范围内Q除非在节点之间有专用网l链接。因此,p|pȝ中的用户应用E序仍需要在新的节点上重新启动?/p>
对于 LinuxQ有许多集群pȝ可供选用。同Ӟ那些目中有几个是非商业性的Q甚x实验性质的。虽然对学术界和某些l织q也没有形成问题Q但大公叔R常首选著名供应商的商业支持^台。供应商Q如 IBM、SGI、HP ?SunQ提供了用于?Linux 中构建科学群集的产品和服务,因ؓ集很流行,q且可以销售大量的服务器设备。一旦商业机构认为其它Ş式的集群是可靠的Q那些相同的服务器供应商或许会围l着开放源码集解x案创q产品?/p>
Linux 作ؓ服务器^台的重要性依赖于支持大型服务器和服务器群集的能力。这׃它可以与 Sun、HP、IBM 和其它公司的 UNIX 服务器在更高层面上竞争。虽?Windows NT ?2000 不支?Linux 能够支持的集范_但是 HA 集群正规Ҏ的可用性以及用于构建有集意识?API 也它能够参与竞争?/p>
如果正在考虑构徏一个群集,那么您应当仔l检查那些可能性,q将它们与您的需求做比较。您也许会发现想要实现的目标q不能成Z个完整的解决ҎQ或怼发现已经有了现成的解x案。不是哪种情况Q请怿许多现有公司他们的应用E序托付l进行深度计ƈ提供大量|页?Linux pȝ集。集是一U企业系l服务,已经?Linux 下成功测试过。尽新的集将出现Q但选择的多h正?Linux 过其它pȝQ如 Windows NTQ的优势?/p>
Rawn Shah 是居住在亚利桑那州图市的一位独立顾问。他多年来与多^台问题打交道q撰写相x章,但常o他不解的是很有人知道有用的pȝ工具?/p>
]]>
非本Z?因非常经?所以收归旗?与众人阅?原作者不?
二、堆和栈的理论知?nbsp;
2.1甌方式
stack:
ql自动分配?nbsp;例如Q声明在函数中一个局部变?nbsp;int b; pȝ自动在栈中ؓb开辟空?nbsp;
heap:
需要程序员自己甌Qƈ指明大小Q在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用newq算W?nbsp;
如p2 = (char *)malloc(10);
但是注意p1、p2本n是在栈中的?nbsp;
2.2
甌后系l的响应
栈:只要栈的剩余I间大于所甌I间Q系l将为程序提供内存,否则报异常提示栈溢出?nbsp;
堆:首先应该知道操作pȝ有一个记录空闲内存地址的链表,当系l收到程序的甌Ӟ
会遍历该链表Q寻扄一个空间大于所甌I间的堆l点Q然后将该结点从I闲l点链表中删除,q将该结点的I间分配l程序,另外Q对于大多数pȝQ会在这块内存空间中的首地址处记录本ơ分配的大小Q这P代码中的delete语句才能正确的释放本内存I间。另外,׃扑ֈ的堆l点的大不一定正好等于申L大小Q系l会自动的将多余的那部分重新攑օI闲链表中?nbsp;
2.3甌大小的限?nbsp;
栈:在Windows?栈是向低地址扩展的数据结构,是一块连l的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是pȝ预先规定好的Q在WINDOWS下,栈的大小?MQ也有的说是1MQM是一个编译时q定的常数Q,如果甌的空间超q栈的剩余空间时Q将提示overflow。因此,能从栈获得的I间较小?nbsp;
堆:堆是向高地址扩展的数据结构,是不q箋的内存区域。这是由于系l是用链表来存储的空闲内存地址的,自然是不q箋的,而链表的遍历方向是由低地址向高地址。堆的大受限于计算机系l中有效的虚拟内存。由此可见,堆获得的I间比较灉|Q也比较大?nbsp;
2.4甌效率的比较:
栈由pȝ自动分配Q速度较快。但E序员是无法控制的?nbsp;
堆是由new分配的内存,一般速度比较慢,而且Ҏ产生内存片,不过用v来最方便.
另外Q在WINDOWS下,最好的方式是用VirtualAlloc分配内存Q他不是在堆Q也不是在栈是直接在q程的地址I间中保留一快内存,虽然用v来最不方ѝ但是速度快,也最灉|?nbsp;
2.5堆和栈中的存储内?nbsp;
栈: 在函数调用时Q第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句Q的地址Q然后是函数的各个参敎ͼ在大多数的C~译器中Q参数是由右往左入栈的Q然后是函数中的局部变量。注意静态变量是不入栈的?nbsp;
当本ơ函数调用结束后Q局部变量先出栈Q然后是参数Q最后栈指针指向最开始存的地址Q也是dC的下一条指令,E序p点l运行?nbsp;
堆:一般是在堆的头部用一个字节存攑֠的大。堆中的具体内容有程序员安排?nbsp;
2.6存取效率的比?nbsp;
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在q行时刻赋值的Q?nbsp;
而bbbbbbbbbbb是在~译时就定的;
但是Q在以后的存取中Q在栈上的数l比指针所指向的字W串(例如?快?nbsp;
比如Q?nbsp;
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇~代?nbsp;
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
W一U在d时直接就把字W串中的元素d寄存器cl中,而第二种则要先把指针D到edx中,在根据edxd字符Q显然慢了?nbsp;
2.7结Q?nbsp;
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜Q发出申P、付钱、和吃(使用Q,吃饱了就赎ͼ不必理会切菜、洗菜等准备工作和洗、刷锅等扫尾工作Q他的好处是快捷Q但是自由度?nbsp;
使用堆就象是自己动手做喜Ƣ吃的菜_比较ȝQ但是比较符合自q口味Q而且自由度大?nbsp;
windowsq程中的内存l构
在阅L文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基知识?nbsp;
接触q编E的人都知道Q高U语a都能通过变量名来讉K内存中的数据。那么这些变量在内存中是如何存放的呢Q程序又是如何用这些变量的呢?下面׃Ҏq行深入的讨论。下文中的C语言代码如没有特别声明,默认都用VC~译的release版?nbsp;
首先Q来了解一?nbsp;C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变?Local)Q静态变?Static)、寄存器变量(Regeister)。每U变量都有不同的分配方式。先来看下面q段代码Q?nbsp;
#include <stdio.h>
int g1=0, g2=0, g3=0;
int main()
{
static int s1=0, s2=0, s3=0;
int v1=0, v2=0, v3=0;
//打印出各个变量的内存地址
printf("0x%08x\n",&v1); //打印各本地变量的内存地址
printf("0x%08x\n",&v2);
printf("0x%08x\n\n",&v3);
printf("0x%08x\n",&g1); //打印各全局变量的内存地址
printf("0x%08x\n",&g2);
printf("0x%08x\n\n",&g3);
printf("0x%08x\n",&s1); //打印各静态变量的内存地址
printf("0x%08x\n",&s2);
printf("0x%08x\n\n",&s3);
return 0;
}
~译后的执行l果是:
0x0012ff78
0x0012ff7c
0x0012ff80
0x004068d0
0x004068d4
0x004068d8
0x004068dc
0x004068e0
0x004068e4
输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量Qs1,s2,s3是静态变量。你可以看到q些变量在内存是q箋分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连l的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的l果。对于一个进E的内存I间而言Q可以在逻辑上分?个部份:代码区,静态数据区和动态数据区。动态数据区一般就?#8220;堆栈”?#8220;?stack)”?#8220;?heap)”是两U不同的动态数据区Q栈是一U线性结构,堆是一U链式结构。进E的每个U程都有U有?#8220;?#8221;Q所以每个线E虽然代码一P但本地变量的数据都是互不q扰。一个堆栈可以通过“基地址”?#8220;栈顶”地址来描q。全局变量和静态变量分配在静态数据区Q本地变量分配在动态数据区Q即堆栈中。程序通过堆栈的基地址和偏U量来访问本地变量?nbsp;
├———————┤低端内存区域
?nbsp;…… ?nbsp;
├———————┤
?nbsp;动态数据区 ?nbsp;
├———————┤
?nbsp;…… ?nbsp;
├———————┤
?nbsp;代码?nbsp;?nbsp;
├———————┤
?nbsp;静态数据区 ?nbsp;
├———————┤
?nbsp;…… ?nbsp;
├———————┤高端内存区域
堆栈是一个先q后出的数据l构Q栈地址L于{于栈的基地址。我们可以先了解一下函数调用的q程Q以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定Q这些因素有参数的压入规则和堆栈的^衡。windows API的调用规则和ANSI C的函数调用规则是不一LQ前者由被调函数调整堆栈Q后者由调用者调整堆栈。两者通过“__stdcall”?#8220;__cdecl”前缀区分。先看下面这D代码:
#include <stdio.h>
void __stdcall func(int param1,int param2,int param3)
{
int var1=param1;
int var2=param2;
int var3=param3;
printf("0x%08x\n",¶m1); //打印出各个变量的内存地址
printf("0x%08x\n",¶m2);
printf("0x%08x\n\n",¶m3);
printf("0x%08x\n",&var1);
printf("0x%08x\n",&var2);
printf("0x%08x\n\n",&var3);
return;
}
int main()
{
func(1,2,3);
return 0;
}
~译后的执行l果是:
0x0012ff78
0x0012ff7c
0x0012ff80
0x0012ff68
0x0012ff6c
0x0012ff70
├———————┤<—函数执行时的栈ӞESPQ、低端内存区?nbsp;
?nbsp;…… ?nbsp;
├———————┤
?nbsp;var 1 ?nbsp;
├———————┤
?nbsp;var 2 ?nbsp;
├———————┤
?nbsp;var 3 ?nbsp;
├———————┤
?nbsp;RET ?nbsp;
├———————┤<?#8220;__cdecl”函数q回后的栈顶QESPQ?nbsp;
?nbsp;parameter 1 ?nbsp;
├———————┤
?nbsp;parameter 2 ?nbsp;
├———————┤
?nbsp;parameter 3 ?nbsp;
├———————┤<?#8220;__stdcall”函数q回后的栈顶QESPQ?nbsp;
?nbsp;…… ?nbsp;
├———————┤<—栈底(基地址 EBPQ、高端内存区?nbsp;
上图是函数调用q程中堆栈的样子了。首先,三个参数以从又到左的ơ序压入堆栈Q先?#8220;param3”Q再?#8220;param2”Q最后压?#8220;param1”Q然后压入函数的q回地址(RET)Q接着跌{到函数地址接着执行Q这里要补充一点,介绍UNIX下的~冲溢出原理的文章中都提到在压入RET后,l箋压入当前EBPQ然后用当前ESP代替EBP。然而,有一介lwindows下函数调用的文章中说Q在windows下的函数调用也有q一步骤Q但Ҏ我的实际调试Qƈ未发现这一步,q还可以从param3和var1之间只有4字节的间隙这点看出来Q;W三步,栈?ESP)减去一个数Qؓ本地变量分配内存I间Q上例中是减?2字节(ESP=ESP-3*4Q每个int变量占用4个字?Q接着初始化本地变量的内存空间。由?#8220;__stdcall”调用p调函数调整堆栈,所以在函数q回前要恢复堆栈Q先回收本地变量占用的内?ESP=ESP+3*4)Q然后取回地址Q填入EIP寄存器,回收先前压入参数占用的内?ESP=ESP+3*4)Ql执行调用者的代码。参见下列汇~代码:
;--------------func 函数的汇~代?------------------
:00401000 83EC0C sub esp, 0000000C //创徏本地变量的内存空?nbsp;
:00401003 8B442410 mov eax, dword ptr [esp+10]
:00401007 8B4C2414 mov ecx, dword ptr [esp+14]
:0040100B 8B542418 mov edx, dword ptr [esp+18]
:0040100F 89442400 mov dword ptr [esp], eax
:00401013 8D442410 lea eax, dword ptr [esp+10]
:00401017 894C2404 mov dword ptr [esp+04], ecx
……………………Q省略若q代码)
:00401075 83C43C add esp, 0000003C ;恢复堆栈Q回收本地变量的内存I间
:00401078 C3 ret 000C ;函数q回Q恢复参数占用的内存I间
;如果?#8220;__cdecl”的话Q这里是“ret”Q堆栈将p用者恢?nbsp;
;-------------------函数l束-------------------------
;--------------ȝ序调用func函数的代?-------------
:00401080 6A03 push 00000003 //压入参数param3
:00401082 6A02 push 00000002 //压入参数param2
:00401084 6A01 push 00000001 //压入参数param1
:00401086 E875FFFFFF call 00401000 //调用func函数
;如果?#8220;__cdecl”的话Q将在这里恢复堆栈,“add esp, 0000000C”
聪明的读者看到这里,差不多就明白~冲溢出的原理了。先来看下面的代码:
#include <stdio.h>
#include <string.h>
void __stdcall func()
{
char lpBuff[8]="\0";
strcat(lpBuff,"AAAAAAAAAAA");
return;
}
int main()
{
func();
return 0;
}
~译后执行一下回怎么P哈,“"0x00414141"指o引用?0x00000000"内存。该内存不能?read"?#8221;Q?#8220;非法操作”喽!"41"是"A"?6q制的ASCII码了Q那明显是strcatq句出的问题了?lpBuff"的大只?字节Q算q结\0Q那strcat最多只能写??A"Q但E序实际写入?1?A"外加1个\0。再来看看上面那q图Q多出来?个字节正好覆盖了RET的所在的内存I间Q导致函数返回到一个错误的内存地址Q执行了错误的指令。如果能_ֿ构造这个字W串Q它分成三部分Q前一部䆾仅仅是填充的无意义数据以辑ֈ溢出的目的,接着是一个覆盖RET的数据,紧接着是一DshellcodeQ那只要着个RET地址能指向这Dshellcode的第一个指令,那函数返回时p执行shellcode了。但是Y件的不同版本和不同的q行环境都可能媄响这Dshellcode在内存中的位|,那么要构造这个RET是十分困隄。一般都在RET和shellcode之间填充大量的NOP指oQ得exploit有更强的通用性?nbsp;
├———————┤<—低端内存区?nbsp;
?nbsp;…… ?nbsp;
├———————┤<—由exploit填入数据的开?nbsp;
?nbsp;?nbsp;
?nbsp;buffer ?lt;—填入无用的数据
?nbsp;?nbsp;
├———————┤
?nbsp;RET ?lt;—指向shellcodeQ或NOP指o的范?nbsp;
├———————┤
?nbsp;NOP ?nbsp;
?nbsp;…… ?lt;—填入的NOP指oQ是RET可指向的范围
?nbsp;NOP ?nbsp;
├———————┤
?nbsp;?nbsp;
?nbsp;shellcode ?nbsp;
?nbsp;?nbsp;
├———————┤<—由exploit填入数据的结?nbsp;
?nbsp;…… ?nbsp;
├———————┤<—高端内存区?nbsp;
windows下的动态数据除了可存放在栈中,q可以存攑֜堆中。了解C++的朋友都知道QC++可以使用new关键字来动态分配内存。来看下面的C++代码Q?nbsp;
#include <stdio.h>
#include <iostream.h>
#include <windows.h>
void func()
{
char *buffer=new char[128];
char bufflocal[128];
static char buffstatic[128];
printf("0x%08x\n",buffer); //打印堆中变量的内存地址
printf("0x%08x\n",bufflocal); //打印本地变量的内存地址
printf("0x%08x\n",buffstatic); //打印静态变量的内存地址
}
void main()
{
func();
return;
}
E序执行l果为:
0x004107d0
0x0012ff04
0x004068c0
可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC~译器是通过windows下的“?heap)”来实现new关键字的内存动态分配。在?#8220;?#8221;之前Q先来了解一下和“?#8221;有关的几个API函数Q?nbsp;
HeapAlloc 在堆中申请内存空?nbsp;
HeapCreate 创徏一个新的堆对象
HeapDestroy 销毁一个堆对象
HeapFree 释放甌的内?nbsp;
HeapWalk 枚D堆对象的所有内存块
GetProcessHeap 取得q程的默认堆对象
GetProcessHeaps 取得q程所有的堆对?nbsp;
LocalAlloc
GlobalAlloc
当进E初始化Ӟpȝ会自动ؓq程创徏一个默认堆Q这个堆默认所占内存的大小?M。堆对象ql进行管理,它在内存中以铑ּl构存在。通过下面的代码可以通过堆动态申请内存空_
HANDLE hHeap=GetProcessHeap();
char *buff=HeapAlloc(hHeap,0,8);
其中hHeap是堆对象的句柄,buff是指向申L内存I间的地址。那q个hHeapI竟是什么呢Q它的值有什么意义吗Q看看下面这D代码吧Q?nbsp;
#pragma comment(linker,"/entry:main") //定义E序的入?nbsp;
#include <windows.h>
_CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf
/*---------------------------------------------------------------------------
写到q里Q我们顺便来复习一下前面所讲的知识Q?nbsp;
(*?printf函数是C语言的标准函数库中函敎ͼVC的标准函数库由msvcrt.dll模块实现?nbsp;
由函数定义可见,printf的参C数是可变的,函数内部无法预先知道调用者压入的参数个数Q函数只能通过分析W一个参数字W串的格式来获得压入参数的信息,׃q里参数的个数是动态的Q所以必ȝ调用者来q堆栈Q这里便使用了__cdecl调用规则。BTWQWindowspȝ的API函数基本上是__stdcall调用形式Q只有一个API例外Q那是wsprintfQ它使用__cdecl调用规则Q同printf函数一Pq是׃它的参数个数是可变的~故?nbsp;
---------------------------------------------------------------------------*/
void main()
{
HANDLE hHeap=GetProcessHeap();
char *buff=HeapAlloc(hHeap,0,0x10);
char *buff2=HeapAlloc(hHeap,0,0x10);
HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");
printf=(void *)GetProcAddress(hMsvcrt,"printf");
printf("0x%08x\n",hHeap);
printf("0x%08x\n",buff);
printf("0x%08x\n\n",buff2);
}
执行l果为:
0x00130000
0x00133100
0x00133118
hHeap的值怎么和那个buff的值那么接q呢Q其实hHeapq个句柄是指向HEAP首部的地址。在q程的用户区存着一个叫PEB(q程环境?的结构,q个l构中存攄一些有兌E的重要信息Q其中在PEB首地址偏移0x18处存攄ProcessHeap是q程默认堆的地址Q而偏U?x90处存放了指向q程所有堆的地址列表的指针。windows有很多API都用进E的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中甌内存来{换ANSI字符串到Unicode字符串的。对一个堆的访问是序q行的,同一时刻只能有一个线E访问堆中的数据Q当多个U程同时有访问要求时Q只能排队等待,q样侉K成E序执行效率下降?nbsp;
最后来说说内存中的数据寚w。所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存v始地址能被4除尽QWORD数据的内存v始地址能被2除尽Qx86 CPU能直接访问对齐的数据Q当他试图访问一个未寚w的数据时Q会在内部进行一pd的调_q些调整对于E序来说是透明的,但是会降低运行速度Q所以编译器在编译程序时会尽量保证数据对齐。同样一D代码,我们来看看用VC、Dev-C++和lcc三个不同~译器编译出来的E序的执行结果:
#include <stdio.h>
int main()
{
int a;
char b;
int c;
printf("0x%08x\n",&a);
printf("0x%08x\n",&b);
printf("0x%08x\n",&c);
return 0;
}
q是用VC~译后的执行l果Q?nbsp;
0x0012ff7c
0x0012ff7b
0x0012ff80
变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)?nbsp;
q是用Dev-C++~译后的执行l果Q?nbsp;
0x0022ff7c
0x0022ff7b
0x0022ff74
变量在内存中的顺序:c(4字节)-中间盔R3字节-b(?字节)-a(4字节)?nbsp;
q是用lcc~译后的执行l果Q?nbsp;
0x0012ff6c
0x0012ff6b
0x0012ff64
变量在内存中的顺序:同上?nbsp;
三个~译器都做到了数据对齐,但是后两个编译器昄没VC“聪明”Q让一个char占了4字节Q浪费内存哦?nbsp;
基础知识Q?nbsp;
堆栈是一U简单的数据l构Q是一U只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈Ӟ另一端称为栈底,对堆栈的插入和删除操作被UCؓ入栈和出栈。有一lCPU指o可以实现对进E的内存实现堆栈讉K。其中,POP指o实现出栈操作QPUSH指o实现入栈操作。CPU的ESP寄存器存攑ֽ前线E的栈顶指针QEBP寄存器中保存当前U程的栈底指针。CPU的EIP寄存器存放下一个CPU指o存放的内存地址Q当CPU执行完当前的指o后,从EIP寄存器中d下一条指令的内存地址Q然后l执行?nbsp;
参考:《Windows下的HEAP溢出及其利用》by: isno
《windows核心~程》by: Jeffrey Richter
摘要Q?nbsp;讨论常见的堆性能问题以及如何防范它们。(?nbsp;9 )
前言
您是否是动态分配的 C/C++ 对象忠实且幸q的用户Q您是否在模块间的往q通信中频J地使用?#8220;自动?#8221;Q您的程序是否因堆分配而运行v来很慢?不仅仅您遇到q样的问题。几乎所有项目迟早都会遇到堆问题。大安惌Q?#8220;我的代码真正好,只是堆太?#8221;。那只是部分正确。更深入理解堆及其用法、以及会发生什么问题,是很有用的?/p>
什么是堆?
Q如果您已经知道什么是堆,可以跛_“什么是常见的堆性能问题Q?#8221;部分Q?/p>
在程序中Q用堆来动态分配和释放对象。在下列情况下,调用堆操作:
事先不知道程序所需对象的数量和大小?/p>
对象太大而不适合堆栈分配E序?br>堆用了在运行时分配l代码和堆栈的内存之外的部分内存。下囄Z堆分配程序的不同层?br>
GlobalAlloc/GlobalFreeQMicrosoft Win32 堆调用,q些调用直接与每个进E的默认堆进行对话?/p>
LocalAlloc/LocalFreeQWin32 堆调用(Z?nbsp;Microsoft Windows NT 兼容Q,q些调用直接与每个进E的默认堆进行对话?/p>
COM ?nbsp;IMalloc 分配E序Q或 CoTaskMemAlloc / CoTaskMemFreeQ:函数使用每个q程的默认堆。自动化E序使用“lg对象模型 (COM)”的分配程序,而申LE序使用每个q程堆?/p>
C/C++ q行?nbsp;(CRT) 分配E序Q提供了 malloc() ?nbsp;free() 以及 new ?nbsp;delete 操作W。如 Microsoft Visual Basic ?nbsp;Java {语a也提供了新的操作Wƈ使用垃圾攉来代替堆。CRT 创徏自己的私有堆Q驻留在 Win32 堆的剙?/p>
Windows NT 中,Win32 堆是 Windows NT q行时分配程序周围的薄层。所?nbsp;API 转发它们的请求给 NTDLL?/p>
Windows NT q行时分配程序提?nbsp;Windows NT 内的核心堆分配程序。它由具?nbsp;128 个大从 8 ?nbsp;1,024 字节的空闲列表的前端分配E序l成。后端分配程序用虚拟内存来保留和提交页?/p>
在图表的底部?#8220;虚拟内存分配E序”Q操作系l用它来保留和提交c所有分配程序用虚拟内存进行数据的存取?/p>
分配和释攑֝不就那么单吗Qؓ何花费这么长旉Q?/p>
堆实现的注意事项
传统上,操作pȝ和运行时库是与堆的实现共存的。在一个进E的开始,操作pȝ创徏一个默认堆Q叫?#8220;q程?#8221;。如果没有其他堆可用,则块的分配?#8220;q程?#8221;。语aq行时也能在q程内创建单独的堆。(例如QC q行时创建它自己的堆。)除这些专用的堆外Q应用程序或许多已蝲入的动态链接库 (DLL) 之一可以创徏和用单独的堆。Win32 提供一整套 API 来创建和使用U有堆。有兛_函数Q英文)的详指|请参?nbsp;MSDN?/p>
当应用程序或 DLL 创徏U有堆时Q这些堆存在于进E空_q且在进E内是可讉K的。从l定堆分配的数据在同一个堆上释放。(不能从一个堆分配而在另一个堆释放。)
在所有虚拟内存系l中Q堆ȝ在操作系l的“虚拟内存理?#8221;的顶部。语aq行时堆也驻留在虚拟内存剙。某些情况下Q这些堆是操作系l堆中的层,而语aq行时堆则通过大块的分配来执行自己的内存管理。不使用操作pȝ堆,而用虚拟内存函数更利于堆的分配和块的用?/p>
典型的堆实现由前、后端分配程序组成。前端分配程序维持固定大块的空闲列表。对于一ơ分配调用,堆尝试从前端列表扑ֈ一个自由块。如果失败,堆被q从后端Q保留和提交虚拟内存Q分配一个大块来满h。通用的实现有每块分配的开销Q这耗费执行周期Q也减少了可使用的存储空间?/p>
Knowledge Base 文章 Q10758Q?#8220;?nbsp;calloc() ?nbsp;malloc() 理内存” Q搜索文章编P, 包含了有兌些主题的更多背景知识。另外,有关堆实现和设计的详l讨Z可在下列著作中找刎ͼ“Dynamic Storage Allocation: A Survey and Critical Review”Q作?nbsp;Paul R. Wilson、Mark S. Johnstone、Michael Neely ?nbsp;David BolesQ?#8220;International Workshop on Memory Management”, 作?nbsp;Kinross, Scotland, UK, 1995 q?nbsp;9 ?http://www.cs.utexas.edu/users/oops/papers.html)Q英文)?/p>
Windows NT 的实玎ͼWindows NT 版本 4.0 和更新版本) 使用?nbsp;127 个大从 8 ?nbsp;1,024 字节?nbsp;8 字节寚w块空闲列表和一?#8220;大块”列表?#8220;大块”列表Q空闲列表[0]Q?nbsp;保存大于 1,024 字节的块。空闲列表容U了用双向链表链接在一L对象。默认情况下Q?#8220;q程?#8221;执行攉操作。(攉是将盔RI闲块合q成一个大块的操作。)攉耗费了额外的周期Q但减少了堆块的内部片?/p>
单一全局锁保护堆Q防止多U程式的使用。(请参?#8220;Server Performance and Scalability Killers”中的W一个注意事? George Reilly 所著,?nbsp;“MSDN Online Web Workshop”上(站点Q?img src="" align=absMiddle border=0>http://msdn.microsoft.com/workshop/server/iis/tencom.aspQ英文)。)单一全局锁本质上是用来保护堆数据l构Q防止跨多线E的随机存取。若堆操作太频繁Q单一全局锁会Ҏ能有不利的影响?/p>
什么是常见的堆性能问题Q?br>以下是您使用堆时会遇到的最常见问题Q?nbsp;
分配操作造成的速度减慢。光分配p费很长旉。最可能Dq行速度减慢原因是空闲列表没有块Q所以运行时分配E序代码会耗费周期L较大的空闲块Q或从后端分配程序分配新块?/p>
释放操作造成的速度减慢。释放操作耗费较多周期Q主要是启用了收集操作。收集期_每个释放操作“查找”它的盔R块,取出它们q构造成较大块,然后再把此较大块插入I闲列表。在查找期间Q内存可能会随机到Q从而导致高速缓存不能命中,性能降低?/p>
堆竞争造成的速度减慢。当两个或多个线E同时访问数据,而且一个线El进行之前必ȝ待另一个线E完成时发生竞争。竞争LDȝQ这也是目前多处理器pȝ遇到的最大问题。当大量使用内存块的应用E序?nbsp;DLL 以多U程方式q行Q或q行于多处理器系l上Q时导致速度减慢。单一锁定的用—常用的解决Ҏ—意味着使用堆的所有操作是序列化的。当{待锁定时序列化会引LE切换上下文。可以想象交叉\口闪烁的U灯处走走停停导致的速度减慢?nbsp;
竞争通常会导致线E和q程的上下文切换。上下文切换的开销是很大的Q但开销更大的是数据从处理器高速缓存中丢失Q以及后来线E复zL的数据重建?/p>
堆破坏造成的速度减慢。造成堆破坏的原因是应用程序对堆块的不正确使用。通常情Ş包括释放已释攄堆块或用已释放的堆块,以及块的界重写{明N题。(破坏不在本文讨论范围之内。有兛_存重写和泄漏{其他细节,请参?nbsp;Microsoft Visual C++(R) 调试文档 。)
频繁的分配和重分配造成的速度减慢。这是用脚本语a旉常普遍的现象。如字符串被反复分配Q随重分配增长和释放。不要这样做Q如果可能,量分配大字W串和用缓冲区。另一U方法就是尽量少用连接操作?br>竞争是在分配和释放操作中D速度减慢的问题。理x况下Q希望用没有竞争和快速分?释放的堆。可惜,现在q没有这L通用堆,也许来会有?/p>
在所有的服务器系l中Q如 IIS、MSProxy、DatabaseStacks、网l服务器?nbsp;Exchange 和其他), 堆锁定实在是个大瓉。处理器数越多,竞争p会恶化?/p>
量减少堆的使用
现在您明白用堆时存在的问题了,N您不x有能解决q些问题的超U魔吗Q我可希望有。但没有法能堆运行加快—因此不要期望在产品之前的最后一星期能够大ؓ改观。如果提前规划堆{略Q情况将会大大好转。调整用堆的方法,减少对堆的操作是提高性能的良斏V?/p>
如何减少使用堆操作?通过利用数据l构内的位置可减堆操作的次数。请考虑下列实例Q?/p>
struct ObjectA {
// objectA 的数?nbsp;
}
struct ObjectB {
// objectB 的数?nbsp;
}
// 同时使用 objectA ?nbsp;objectB
//
// 使用指针
//
struct ObjectB {
struct ObjectA * pObjA;
// objectB 的数?nbsp;
}
//
// 使用嵌入
//
struct ObjectB {
struct ObjectA pObjA;
// objectB 的数?nbsp;
}
//
// 集合 – 在另一对象内?nbsp;objectA ?nbsp;objectB
//
struct ObjectX {
struct ObjectA objA;
struct ObjectB objB;
}
避免使用指针兌两个数据l构。如果用指针关联两个数据结构,前面实例中的对象 A ?nbsp;B 被分别分配和释放。这会增加额外开销—我们要避免q种做法?/p>
把带指针的子对象嵌入父对象。当对象中有指针Ӟ则意味着对象中有动态元素(癑ֈ之八十)和没有引用的C|。嵌入增加了位置从而减了q一步分?释放的需求。这提高应用程序的性能?/p>
合ƈ对象Ş成大对象Q聚合)。聚合减分配和释放的块的数量。如果有几个开发者,各自开发设计的不同部分Q则最l会有许多小对象需要合q。集成的挑战是要找到正的聚合边界?/p>
内联~冲够满百分之八十的需要(aka 80-20 规则Q。个别情况下Q需要内存缓冲区来保存字W串/二进制数据,但事先不知道d节数。估计ƈ内联一个大能满癑ֈ之八十需要的~冲区。对剩余的百分之二十Q可以分配一个新的缓冲区和指向这个缓冲区的指针。这P减分配和释放调用q增加数据的位置I间Q从Ҏ上提高代码的性能?/p>
在块中分配对象(块化Q。块化是以组的方式一ơ分配多个对象的Ҏ。如果对列表的项q箋跟踪Q例如对一?nbsp;{名称Q值} 对的列表Q有两种选择Q选择一是ؓ每一?#8220;名称-?#8221;对分配一个节点;选择二是分配一个能容纳Q如五个Q?#8220;名称-?#8221;对的l构。例如,一般情况下Q如果存储四对,可减少节点的数量,如果需要额外的I间数量Q则使用附加的链表指针?nbsp;
块化是友好的处理器高速缓存,特别是对?nbsp;L1-高速缓存,因ؓ它提供了增加的位|?nbsp;—不用说对于块分配,很多数据块会在同一个虚拟页中?/p>
正确使用 _amblksiz。C q行?nbsp;(CRT) 有它的自定义前端分配E序Q该分配E序从后端(Win32 堆)分配大小?nbsp;_amblksiz 的块。将 _amblksiz 讄高的D潜在地减对后端的调用次数。这只对q泛使用 CRT 的程序适用?br>使用上述技术将获得的好处会因对象类型、大及工作量而有所不同。但总能在性能和可升羃性方面有所收获。另一斚wQ代码会有点ҎQ但如果l过深思熟虑,代码q是很容易管理的?/p>
其他提高性能的技?br>下面是一些提高速度的技术:
使用 Windows NT5 ?nbsp;
׃几个同事的努力和辛勤工作Q?998 q初 Microsoft Windows(R) 2000 中有了几个重大改q:
改进了堆代码内的锁定。堆代码Ҏ堆一个锁。全局锁保护堆数据l构Q防止多U程式的使用。但不幸的是Q在高通信量的情况下,堆仍受困于全局锁,D高竞争和低性能。Windows 2000 中,锁内代码的界区竞争的可能性减到最?从而提高了可~性?/p>
使用 “Lookaside”列表。堆数据l构对块的所有空闲项使用了大在 8 ?nbsp;1,024 字节Q以 8-字节递增Q的快速高速缓存。快速高速缓存最初保护在全局锁内。现在,使用 lookaside 列表来访问这些快速高速缓存空闲列表。这些列表不要求锁定Q而是使用 64 位的互锁操作Q因此提高了性能?/p>
内部数据l构法也得到改q?br>q些改进避免了对分配高速缓存的需求,但不排除其他的优化。?nbsp;Windows NT5 堆评估您的代码;它对于 1,024 字节 (1 KB) 的块Q来自前端分配程序的块)是最佳的。GlobalAlloc() ?nbsp;LocalAlloc() 建立在同一堆上Q是存取每个q程堆的通用机制。如果希望获得高的局部性能Q则使用 Heap(R) API 来存取每个进E堆Q或为分配操作创q堆。如果需要对大块操作Q也可以直接使用 VirtualAlloc() / VirtualFree() 操作?/p>
上述改进已在 Windows 2000 beta 2 ?nbsp;Windows NT 4.0 SP4 中用。改q后Q堆锁的竞争率显著降低。这使所?nbsp;Win32 堆的直接用户受益。CRT 堆徏立于 Win32 堆的剙Q但它用自q块堆,因而不能从 Windows NT 改进中受益。(Visual C++ 版本 6.0 也有改进的堆分配E序。)
使用分配高速缓?nbsp;
分配高速缓存允讔R速缓存分配的块,以便来重用。这能够减少对进E堆Q或全局堆)的分?释放调用的次敎ͼ也允许最大限度的重用曄分配的块。另外,分配高速缓存允许收集统计信?以便较好地理解对象在较高层次上的使用?/p>
典型圎ͼ自定义堆分配E序在进E堆的顶部实现。自定义堆分配程序与pȝ堆的行ؓ很相伹{主要的差别是它在进E堆的顶部ؓ分配的对象提供高速缓存。高速缓存设计成一套固定大(?nbsp;32 字节?4 字节?28 字节{)。这一个很好的{略Q但q种自定义堆分配E序丢失与分配和释放的对象相关的“语义信息”?nbsp;
与自定义堆分配程序相反,“分配高速缓?#8221;作ؓ每类分配高速缓存来实现。除能够提供自定义堆分配E序的所有好处之外,它们q能够保留大量语义信息。每个分配高速缓存处理程序与一个目标二q制对象兌。它能够使用一套参数进行初始化Q这些参数表Cƈ发别、对象大和保持在空闲列表中的元素的数量{。分配高速缓存处理程序对象维持自qU有I闲实体池(不超q指定的阀|q用私有保护锁。合在一P分配高速缓存和U有锁减了与主pȝ堆的通信量,因而提供了增加的ƈ发、最大限度的重用和较高的可~性?/p>
需要用清理程序来定期查所有分配高速缓存处理程序的zd情况q回收未用的资源。如果发现没有活动,释攑ֈ配对象的池,从而提高性能?/p>
可以审核每个分配/释放zd。第一U信息包括对象、分配和释放调用的L。通过查看它们的统计信息可以得出各个对象之间的语义关系。利用以上介l的许多技术之一Q这U关pd以用来减内存分配?/p>
分配高速缓存也起到了调试助手的作用Q帮助您跟踪没有完全清除的对象数量。通过查看动态堆栈返回踪q和除没有清除的对象之外的签名,甚至能够扑ֈ切的失败的调用者?/p>
MP ?nbsp;
MP 堆是对多处理器友好的分布式分配的E序包,?nbsp;Win32 SDKQWindows NT 4.0 和更新版本)中可以得到。最初由 JVert 实现Q此处堆抽象建立?nbsp;Win32 堆程序包的顶部。MP 堆创建多?nbsp;Win32 堆,q试囑ְ分配调用分布C同堆Q以减少在所有单一锁上的竞争?/p>
本程序包是好的步?nbsp;—一U改q的 MP-友好的自定义堆分配程序。但是,它不提供语义信息和缺乏统计功能。通常?nbsp;MP 堆作?nbsp;SDK 库来使用。如果用这?nbsp;SDK 创徏可重用组Ӟ您将大大受益。但是,如果在每?nbsp;DLL 中徏立这?nbsp;SDK 库,增加工作设|?/p>
重新思考算法和数据l构
要在多处理器机器上~,则算法、实现、数据结构和g必须动态~。请看最l常分配和释攄数据l构。试问,“我能用不同的数据l构完成此工作吗Q?#8221;例如Q如果在应用E序初始化时加蝲了只读项的列表,q个列表不必是线性链接的列表。如果是动态分配的数组非常好。动态分配的数组减内存中的堆块和片Q从而增强性能?/p>
减少需要的对象的数量减少堆分配程序的负蝲。例如,我们在服务器的关键处理\径上使用五个不同的对象,每个对象单独分配和释放。一起高速缓存这些对象,把堆调用从五个减到一个,显著减少了堆的负载,特别当每U钟处理 1,000 个以上的h时?/p>
如果大量使用“Automation”l构Q请考虑从主U代码中删除“Automation BSTR”Q或臛_避免重复?nbsp;BSTR 操作。(BSTR q接Dq多的重分配和分?释放操作。)
摘要
Ҏ有^台往往都存在堆实现Q因此有巨大的开销。每个单独代码都有特定的要求Q但设计能采用本文讨论的基本理论来减堆之间的相互作用?nbsp;
评h您的代码中堆的用?/p>
改进您的代码Q以使用较少的堆调用Q分析关键\径和固定数据l构?/p>
在实现自定义的包装程序之前用量化堆调用成本的方法?/p>
如果Ҏ能不满意,误?nbsp;OS l改q堆。更多这c请求意味着Ҏq堆的更多关注?/p>
要求 C q行时组针对 OS 所提供的堆制作y的分配包装程序。随着 OS 堆的改进QC q行时堆调用的成本将减小?/p>
操作pȝQWindows NT 家族Q正在不断改q堆。请随时x和利用这些改q?br>Murali Krishnan ?nbsp;Internet Information Server (IIS) l的首席软g设计工程师。从 1.0 版本开始他p?nbsp;IISQƈ成功发行?nbsp;1.0 版本?nbsp;4.0 版本。Murali l织q?nbsp;IIS 性能l三q?nbsp;(1995-1998), 从一开始就影响 IIS 性能。他拥有威斯h?nbsp;Madison 大学?nbsp;M.S.和印?nbsp;Anna 大学?nbsp;B.S.。工作之外,他喜Ƣ阅诅R打排球和家庭烹饪?br>http://community.csdn.net/Expert/FAQ/FAQ_Index.asp?id=172835
我在学习对象的生存方式的时候见CU是在堆?stack)之中Q如?nbsp;
CObject object;
q有一U是在堆(heap)?nbsp; 如下
CObject* pobject=new CObject();
请问
Q?Q这两种方式有什么区别?
Q?Q堆栈与堆有什么区别?Q?nbsp;
---------------------------------------------------------------
1) about stack, system will allocate memory to the instance of object automatically, and to the
heap, you must allocate memory to the instance of object with new or malloc manually.
2) when function ends, system will automatically free the memory area of stack, but to the
heap, you must free the memory area manually with free or delete, else it will result in memory
leak.
3)栈内存分配运内|于处理器的指o集中Q效率很高,但是分配的内存容量有限?nbsp;
4Q堆上分配的内存可以有我们自己决定,使用非常灉|?nbsp;
---------------------------------------------------------------
注意不同~译器会插入自己的汇~代码以提供~译的通用性,但是大体代码如此。其中在函数开始处保留esp到ebp中,在函数结束恢复是~译器常用的Ҏ?/p>
从函数调用看Q??依次被pushq堆栈,而在函数中又通过相对于ebp(卛_q函数时的堆栈指针)的偏U量存取参数。函数结束后Qret 8表示清理8个字节的堆栈Q函数自己恢复了堆栈?br>
׃不同的编译器产生栈的方式不尽相同Q调用者就不一定能够正常的完成堆栈的清除工作,但函数本w自己可以解x除工作,所以,在跨q_的程序开发中的函数调用,我们通常都用__stdcallU定Qwindows下的l大多数函数也都是stdcall调用。既然如此,Z么还需要__cdecl呢?别着急,接着往下看?br>
二、cdecl调用U定
cdeclQ也可写作__cdeclQ又UCؓC调用U定Q是C/C++语言和MFCE序默认~省的调用约定,它的定义语法是:
采用__cdeclU定Ӟ函数参数按照从右到左的顺序入栈,q且p用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能用该调用U定。由于这U变化,C调用U定允许函数的参数的个数是不固定的,q也是C语言的一大特艌Ӏ同Ӟ׃每一个用__cdeclU定的函数都要包含清理堆栈的代码Q所以生的可执行文件大会比较大。__cdecl可以写成_cdecl?
对于前面的function函数Q用cdecl后的汇编码变成:
调用?br> push 1
push 2
call function
add esp, 8 // 注意Q这里调用者在恢复堆栈
被调用函数_function?br> push ebp // 保存ebp寄存器,该寄存器用来保存堆栈的栈顶指针Q可以在函数退出时恢复
mov ebp,esp // 保存堆栈指针
mov eax,[ebp + 8H] // 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] // 堆栈中ebp + 12处保存了b
mov esp,ebp // 恢复esp
pop ebp
ret // 注意Q这里没有修改堆?/p>
不写了,累得慌,呵呵 转蝲两篇文章?br>
__stdcall,__cdecl,_cdecl,_stdcall,。__fastcall,_fastcall 区别?nbsp;
1.
今天写线E函数时Q发现msdn中对ThreadProc的定义有要求QDWORD WINAPI ThreadProc(LPVOID lpParameter);
不解Z么要用WINAPI宏定义,查了后发C面的定义。于是乎需要区别__stdcall和__cdecl两者的区别Q?#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
几乎我们写的每一个WINDOWS API函数都是__stdcallcd的,首先Q需要了解两者之间的区别Q?WINDOWS的函数调用时需要用到栈QSTACKQ一U先入后出的存储l构Q。当函数调用完成后,栈需要清楚,q里是问题的关键,如何清除Q? 如果我们的函C用了_cdeclQ那么栈的清除工作是p用者,用COM的术语来讲就是客h完成的。这样带来了一个棘手的问题Q不同的~译器生栈的方式不相同,那么调用者能否正常的完成清除工作呢?{案是不能?如果使用__stdcallQ上面的问题p决了Q函数自px除工作。所以,在跨Q开发)q_的调用中Q我们都使用__stdcallQ虽然有时是以WINAPI的样子出玎ͼ。那么ؓ什么还需要_cdecl呢?当我们遇到这L函数如fprintf()它的参数是可变的Q不定长的,被调用者事先无法知道参数的长度Q事后的清除工作也无法正常的q行Q因此,q种情况我们只能使用_cdecl。到q里我们有一个结论,如果你的E序中没有涉及可变参敎ͼ最好用__stdcall关键字?/p>
2.
__cdecl,__stdcall是声明的函数调用协议.主要是传参和Ҏ斚w的不?一般c++用的是__cdecl,windows里大都用的是__stdcall(API)
__cdecl是C/C++和MFCE序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdeclU定Ӟ函数参数按照从右到左的顺序入栈,q且p用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能用该调用U定。由于每一个用__cdeclU定的函数都要包含清理堆栈的代码Q所以生的可执行文件大会比较大。__cdecl可以写成_cdecl?
__stdcall调用U定用于调用Win32 API函数。采用__stdcallU定Ӟ函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函C本n知道传进来的参数个数Q因此被调用的函数可以在q回前用一条ret n指o直接清理传递参数的堆栈。__stdcall可以写成_stdcall?
__fastcallU定用于Ҏ能要求非常高的场合。__fastcallU定函数的从左边开始的两个大小不大?个字节(DWORDQ的参数分别攑֜ECX和EDX寄存器,其余的参C旧自叛_左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall
3.
__stdcall:
_stdcall 调用U定相当?6位动态库中经怋用的PASCAL调用U定?/p>
_stdcall是PascalE序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC函数编译后会在函数名前面加上下划线前缀Q在函数名后加上"@"和参数的字节数?/p>
_cdecl:
_cdecl c调用U定, 按从双左的序压参数入栈,p用者把参数弹出栈。对于传送参数的内存栈是p用者来l护的(正因为如此,实现可变参数的函数只能用该调用U定Q。另外,在函数名修饰U定斚w也有所不同?/p>
_cdecl是C和CQ+E序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以生的可执行文件大会比调用_stdcall函数的大。函数采用从叛_左的压栈方式。VC函数编译后会在函数名前面加上下划线前缀。是MFC~省调用U定?/p>
__fastcall:
__fastcall调用U定??如其名,它的主要特点是快,因ؓ它是通过寄存器来传送参数的Q实际上Q它用ECX和EDX传送前两个双字QDWORDQ或更小的参敎ͼ剩下的参C旧自叛_左压栈传送,被调用的函数在返回前清理传送参数的内存栈)Q在函数名修饰约定方面,它和前两者均不同?/p>
_fastcall方式的函数采用寄存器传递参敎ͼVC函数编译后会在函数名前面加?@"前缀Q在函数名后加上"@"和参数的字节数?/p>
thiscall:
thiscall仅仅应用?C++"成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定?/p>
naked call:
采用1-4的调用约定时Q如果必要的话,q入函数时编译器会生代码来保存ESIQEDIQEBXQEBP寄存器,退出函数时则生代码恢复这些寄存器的内宏V?/p>
naked call不生这L代码。naked call不是cd修饰W,故必d_declspec共同使用?/p>
另附:
关键?__stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在~译环境的Setting...\C/C++ \Code Generationw择。当加在输出函数前的关键字与~译环境中的选择不同Ӟ直接加在输出函数前的关键字有效。它们对应的命o行参数分别ؓ/Gz?Gd?Gr。缺省状态ؓ/GdQ即__cdecl?/p>
要完全模仿PASCAL调用U定首先必须使用__stdcall调用U定Q至于函数名修饰U定Q可以通过其它Ҏ模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏Q它可以出函数译成适当的调用约定,在WIN32中,它被定义为__stdcall。用WINAPI宏可以创qAPIs?/p>
名字修饰U定
1、修饰名(Decoration name)
“C”或?#8220;C++”函数在内部(~译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字W串。有些情况下使用函数的修饰名是必要的Q如在模块定义文仉头指定输?#8220;C++”重蝲函数、构造函数、析构函敎ͼ又如在汇~代码里调用“C””?#8220;C++”函数{?
修饰名由函数名、类名、调用约定、返回类型、参数等共同军_?
2、名字修饰约定随调用U定和编译种c?C或C++)的不同而变化。函数名修饰U定随编译种cd调用U定的不同而不同,下面分别说明?
a、C~译时函数名修饰U定规则Q?
__stdcall调用U定在输出函数名前加上一个下划线前缀Q后面加上一?#8220;@”W号和其参数的字节数Q格式ؓ_functionname@number?
__cdecl调用U定仅在输出函数名前加上一个下划线前缀Q格式ؓ_functionname?/p>
__fastcall调用U定在输出函数名前加上一?#8220;@”W号Q后面也是一?#8220;@”W号和其参数的字节数Q格式ؓ@functionname@number?
它们均不改变输出函数名中的字W大写Q这和PASCAL调用U定不同QPASCALU定输出的函数名无Q何修C全部大写?
b、C++~译时函数名修饰U定规则Q?
__stdcall调用U定Q?
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面?#8220;@@YG”标识参数表的开始,后跟参数表;
3、参数表以代可C:
X--void Q?
D--charQ?
E--unsigned charQ?
F--shortQ?
H--intQ?
I--unsigned intQ?
J--longQ?
K--unsigned longQ?
M--floatQ?
N--doubleQ?
_N--boolQ?
....
PA--表示指针Q后面的代号表明指针cdQ如果相同类型的指针q箋出现Q以“0”代替Q一?#8220;0”代表一ơ重复;
4、参数表的第一ؓ该函数的q回值类型,其后依次为参数的数据cd,指针标识在其所指数据类型前Q?
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数Q则?#8220;Z”标识l束?
其格式ؓ“?functionname@@YG*****@Z”?#8220;?functionname@@YG*XZ”Q例?
int Test1Qchar *var1,unsigned longQ?----“?Test1@@YGHPADK@Z”
void Test2Q) -----“?Test2@@YGXXZ”
__cdecl调用U定Q?
规则同上面的_stdcall调用U定Q只是参数表的开始标识由上面?#8220;@@YG”变ؓ“@@YA”?
__fastcall调用U定Q?
规则同上面的_stdcall调用U定Q只是参数表的开始标识由上面?#8220;@@YG”变ؓ“@@YI”?
VC++对函数的省缺声明?#8220;__cedcl“,只能被C/C++调用.
CB在输出函数声明时使用4U修饰符?
//__cdecl
cb的默认|它会在输出函数名前加_Qƈ保留此函数名不变Q参数按照从叛_左的序依次传递给栈,也可以写成_cdecl和cdecl形式?
//__fastcall
她修饰的函数的参数将肯呢感C用寄存器来处理,其函数名前加@Q参数按照从左到右的序压栈Q?
//__pascal
它说明的函数名用Pascal格式的命名约定。这时函数名全部大写。参数按照从左到右的序压栈Q?
//__stdcall
使用标准U定的函数名。函数名不会改变。用__stdcall修饰时。参数按照由叛_左的序压栈Q也可以是_stdcallQ?/p>
VC++对函数的省缺声明?__cedcl",只能被C/C++调用.
注意Q?/p>
1、_beginthread需要__cdecl的线E函数地址Q_beginthreadex和CreateThread需要__stdcall的线E函数地址?/p>
2、一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义:
#define CALLBACK __stdcall
#define WINAPI __stdcall
3、extern "C" _declspec(dllexport) int __cdecl Add(int a, int b);
typedef int (__cdecl*FunPointer)(int a, int b);
修饰W的书写序如上?/p>
4、extern "C"的作用:如果Add(int a, int b)是在c语言~译器编译,而在c++文g使用Q则需要在c++文g中声明:extern "C" Add(int a, int b)Q因为c~译器和c++~译器对函数名的解释不一Pc++~译器解释函数名的时候要考虑函数参数Q这h了方便函数重载,而在c语言中不存在函数重蝲的问题)Q用extern "C"Q实质就是告诉c++~译器,该函数是c库里面的函数。如果不使用extern "C"则会出现链接错误?/p>
一般象如下使用Q?/p>
#ifdef _cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C extern
#endif
#ifdef _cplusplus
extern "C"{
#endif
EXTERN_C int func(int a, int b);
#ifdef _cplusplus
}
#endif
5、MFC提供了一些宏Q可以用AFX_EXT_CLASS来代替__declspec(DLLexport)Qƈ修饰cdQ从而导出类QAFX_API_EXPORT来修饰函敎ͼAFX_DATA_EXPORT来修饰变?/p>
AFX_CLASS_IMPORTQ__declspec(DLLexport)
AFX_API_IMPORTQ__declspec(DLLexport)
AFX_DATA_IMPORTQ__declspec(DLLexport)
AFX_CLASS_EXPORTQ__declspec(DLLexport)
AFX_API_EXPORTQ__declspec(DLLexport)
AFX_DATA_EXPORTQ__declspec(DLLexport)
AFX_EXT_CLASSQ?ifdef _AFXEXT
AFX_CLASS_EXPORT
#else
AFX_CLASS_IMPORT
6、DLLMain负责初始?Initialization)和结?Termination)工作Q每当一个新的进E或者该q程的新的线E访问DLLӞ或者访问DLL的每一个进E或者线E不再用DLL或者结束时Q都会调用DLLMain。但是,使用TerminateProcess或TerminateThreadl束q程或者线E,不会调用DLLMain?/p>
7、一个DLL在内存中只有一个实?/p>
DLLE序和调用其输出函数的程序的关系Q?/p>
1)、DLL与进E、线E之间的关系
DLL模块被映到调用它的q程的虚拟地址I间?/p>
DLL使用的内存从调用q程的虚拟地址I间分配Q只能被该进E的U程所讉K?/p>
DLL的句柄可以被调用q程使用Q调用进E的句柄可以被DLL使用?/p>
DLLDLL可以有自q数据D,但没有自q堆栈Q用调用进E的栈,与调用它的应用程序相同的堆栈模式?/p>
2)、关于共享数据段
DLL定义的全局变量可以被调用进E访问;DLL可以讉K调用q程的全局数据。用同一DLL的每一个进E都有自qDLL全局变量实例。如果多个线Eƈ发访问同一变量Q则需要用同步机Ӟ对一个DLL的变量,如果希望每个使用DLL的线E都有自q|则应该用线E局部存?TLSQThread Local Strorage)?br>
在C语言中,假设我们有这L一个函敎ͼ
int function(int a,int b)
调用时只要用result = function(1,2)q样的方式就可以使用q个函数。但是,当高U语a被编译成计算机可以识别的机器码时Q有一个问题就凸现出来Q在CPU中,计算机没有办法知道一个函数调用需要多个、什么样的参敎ͼ也没有硬件可以保存这些参数。也是_计算Z知道怎么l这个函C递参敎ͼ传递参数的工作必须由函数调用者和函数本n来协调。ؓ此,计算机提供了一U被UCؓ栈的数据l构来支持参C递?/p>
栈是一U先q后出的数据l构Q栈有一个存储区、一个栈指针。栈指针指向堆栈中W一个可用的数据(被称为栈Ӟ。用户可以在栈顶上方向栈中加入数据,q个操作被称为压?Push)Q压栈以后,栈顶自动变成新加入数据项的位|,栈顶指针也随之修攏V用户也可以从堆栈中取走栈顶Q称为弹出栈(pop)Q弹出栈后,栈顶下的一个元素变成栈Ӟ栈顶指针随之修改?/p>
函数调用Ӟ调用者依ơ把参数压栈Q然后调用函敎ͼ函数被调用以后,在堆栈中取得数据Qƈq行计算。函数计结束以后,或者调用者、或者函数本w修改堆栈,使堆栈恢复原装?/p>
在参C递中Q有两个很重要的问题必须得到明确说明Q?br>
当参C数多于一个时Q按照什么顺序把参数压入堆栈
函数调用后,p来把堆栈恢复原装
在高U语a中,通过函数调用U定来说明这两个问题。常见的调用U定有:
stdcall
cdecl
fastcall
thiscall
naked call
stdcall调用U定
stdcall很多时候被UCؓpascal调用U定Q因为pascal是早期很常见的一U教学用计算机程序设计语aQ其语法严}Q用的函数调用U定是stdcall。在Microsoft C++pd的C/C++~译器中Q常常用PASCAL宏来声明q个调用U定Q类似的宏还有WINAPI和CALLBACK?/p>
stdcall调用U定声明的语法ؓ(以前文的那个函数ZQ:
int __stdcall function(int a,int b)
stdcall的调用约定意味着Q?Q参C叛_左压入堆栈,2Q函数自w修改堆?3)函数名自动加前导的下划线Q后面紧跟一个@W号Q其后紧跟着参数的尺?/p>
以上q这个函Cؓ例,参数b首先被压栈,然后是参数aQ函数调用function(1,2)调用处翻译成汇编语言变成:
push 2 W二个参数入?br> push 1 W一个参数入?br> call function 调用参数Q注意此时自动把cs:eip入栈
而对于函数自w,则可以翻译ؓQ?
push ebp 保存ebp寄存器,该寄存器用来保存堆栈的栈顶指针Q可以在函数退出时恢复
mov ebp, esp 保存堆栈指针
mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp, cs:eip, a, b, ebp +8指向a
add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b
mov esp, ebp 恢复esp
pop ebp
ret 8
而在~译Ӟq个函数的名字被译?a href="mailto:_function@8">_function@8
注意不同~译器会插入自己的汇~代码以提供~译的通用性,但是大体代码如此。其中在函数开始处保留esp到ebp中,在函数结束恢复是~译器常用的Ҏ?/p>
从函数调用看Q??依次被pushq堆栈,而在函数中又通过相对于ebp(卛_q函数时的堆栈指针)的偏U量存取参数。函数结束后Qret 8表示清理8个字节的堆栈Q函数自己恢复了堆栈?/p>
cdecl调用U定
cdecl调用U定又称为C调用U定Q是C语言~省的调用约定,它的定义语法是:
int function (int a ,int b) //不加修饰是C调用U定
int __cdecl function(int a,int b)//明确指出C调用U定
在写本文ӞZ我的意料Q发现cdecl调用U定的参数压栈顺序是和stdcall是一LQ参数首先由叛_左压入堆栈。所不同的是Q函数本w不清理堆栈Q调用者负责清理堆栈。由于这U变化,C调用U定允许函数的参数的个数是不固定的,q也是C语言的一大特艌Ӏ对于前面的function函数Q用cdecl后的汇编码变成:
调用?br> push 1
push 2
call function
add esp, 8 注意Q这里调用者在恢复堆栈
被调用函数_function?br> push ebp 保存ebp寄存器,该寄存器用来保存堆栈的栈顶指针Q可以在函数退出时恢复
mov ebp,esp 保存堆栈指针
mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 注意Q这里没有修改堆?/p>
MSDN中说Q该修饰自动在函数名前加前导的下划线Q因此函数名在符可中被记录为_functionQ但是我在编译时g没有看到q种变化?/p>
׃参数按照从右向左序压栈Q因此最开始的参数在最接近栈顶的位|,因此当采用不定个数参数时Q第一个参数在栈中的位|肯定能知道Q只要不定的参数个数能够ҎW一个后者后l的明确的参数确定下来,可以用不定参敎ͼ例如对于CRT中的sprintf函数Q定义ؓQ?
int sprintf(char* buffer,const char* format,...)
׃所有的不定参数都可以通过format定Q因此用不定个数的参数是没有问题的?/p>
fastcall
fastcall调用U定和stdcallcMQ它意味着Q?
函数的第一个和W二个DWORD参数Q或者尺寸更的Q通过ecx和edx传递,其他参数通过从右向左的顺序压?
被调用函数清理堆?
函数名修改规则同stdcall
其声明语法ؓQint fastcall function(int a, int b)
thiscall
thiscall是唯一一个不能明指明的函数修饰Q因为thiscall不是关键字。它是C++cL员函数缺省的调用U定。由于成员函数调用还有一个this指针Q因此必ȝD处理,thiscall意味着Q?
参数从右向左入栈
如果参数个数定Qthis指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈Z说明q个调用U定Q定义如下类和用代码:
class A
{
public:
int function1(int a,int b);
int function2(int a,...);
};
int A::function1 (int a,int b)
{
return a+b;
}
#include <stdarg.h>
int A::function2(int a,...)
{
va_list ap;
va_start(ap,a);
int i;
int result = 0;
for(i = 0 ; i < a ; i ++)
{
result += va_arg(ap,int);
}
return result;
}
void callee()
{
A a;
a.function1(1, 2);
a.function2(3, 1, 2, 3);
}
callee函数被翻译成汇编后就变成Q?
//函数function1调用
00401C1D push 2
00401C1F push 1
00401C21 lea ecx,[ebp-8]
00401C24 call function1 注意Q这里this没有被入?/p>
//函数function2调用
00401C29 push 3
00401C2B push 2
00401C2D push 1
00401C2F push 3
00401C31 lea eax, [ebp-8] q里引入this指针
00401C34 push eax
00401C35 call function2
00401C3A add esp, 14h
可见Q对于参C数固定情况下Q它cM于stdcallQ不定时则类似cdecl
naked call
q是一个很见的调用约定,一般程序设计者徏议不要用。编译器不会l这U函数增加初始化和清理代码,更特D的是,你不能用returnq回q回|只能用插入汇~返回结果。这一般用于实模式驱动E序设计Q假讑֮义一个求和的加法E序Q可以定义ؓQ?
__declspec(naked) int add(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret
}
注意Q这个函数没有显式的returnq回|q回通过修改eax寄存器实玎ͼ而且q退出函数的ret指o都必L式插入。上面代码被译成汇~以后变成:
mov eax,[ebp+8]
add eax,[ebp+12]
ret 8
注意q个修饰是和__stdcall及cdecll合使用的,前面是它和cdecll合使用的代码,对于和stdcalll合的代码,则变成:
__declspec(naked) int __stdcall function(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret 8 //注意后面?
}
至于q种函数被调用,则和普通的cdecl及stdcall调用函数一致?/p>
函数调用U定D的常见问?br> 如果定义的约定和使用的约定不一_则将D堆栈被破坏,D严重问题Q下面是两种常见的问题:
函数原型声明和函C定义不一?
DLL导入函数时声明了不同的函数约?
以后者ؓ例,假设我们在dllU声明了一U函CؓQ?
__declspec(dllexport) int func(int a,int b);//注意Q这里没有stdcallQ用的是cdecl
使用时代码ؓQ?
typedef int (*WINAPI DLLFUNC)func(int a,int b);
hLib = LoadLibrary(...);
DLLFUNC func = (DLLFUNC)GetProcAddress(...)//q里修改了调用约?br> result = func(1,2);//D错误
׃调用者没有理解WINAPI的含义错误的增加了这个修饎ͼ上述代码必然D堆栈被破坏,MFC在编译时插入的checkesp函数告诉你Q堆栈被破坏
特地拿来与大家分享,希望能给像我q样的菜鸟们一些帮助,O(∩_∩)O
【第一步】创qdll
1.打开vs2005Q选择菜单【File-New-Project】,在弹出对话框中选择[Visual C++]下的[Win32]-[Win32 Console Application]Q输入工E名后确认?/pre>2.在弹出的对话框中选择[next]Q在Application Settiongs中选择Application type为DllQAdditional options选择Empty projectQ然后点Finish?/pre>q时创Z一个空的可以生成dll文g的工E?/pre>3.在工E中d一个头文g(q里为dll_test.h)Q在头文件中写入如下内容Q?/pre>1 #ifndef _DLL_TUTORIAL_H
2 #define _DLL-TUTORIAL_H
3
4 #include<iostream>
5
6 #if defined DLL_EXPORT
7 #define DECLDIR _declspec(dllexport)
8 #else
9 #define DECLDIR _declspec(dllimport)
10 #endif
11
12 extern "C"
13 {
14 DECLDIR int Add(int a, int b);
15 DECLDIR void Function(void);
16 }
17
18 #endifq里要说明的是:在VC中有两个Ҏ来导出dll中定义的函数Q?/pre>(1) 使用__declspec,q是一个Microsoft定义的关键字?/pre>(2) 创徏一个模板定义文?Module-Definition FileQ即.DEF)?/pre>W一U方法稍E比W二U方法简单,在这里我们用的是第一U方法?/pre>__declspec(dllexport)函数的作用是导出函数W号到在你的Dll中的一个存储类里去?/pre>当下面一行被定义时我定义DECLDIR宏来q行q个函数?/pre>#define DLL_EXPORT在此情况下你导出函数Add(int a,int b)和Function().4.创徏一个源文g(名字为dll_test.cpp)Q内容如下:1 #include <iostream>
2 #define DLL_EXPORT
3 #include "dll_test.h"
4
5 extern "C"
6 {
7 // 定义了(DLL中的Q所有函?/span>
8 DECLDIR int Add( int a, int b )
9 {
10 return( a + b );
11 }
12
13 DECLDIR void Function( void )
14 {
15 std::cout << "DLL Called!" << std::endl;
16 }
17 }
18【第二步】用创建好的DLL
现在已经创徏了DLLQ那么如何在一个应用程序中使用它呢Q?/pre>当DLL被生成后Q它创徏了一?dll文g和一?libQ这两个都是使用dll旉要用到的?/pre>在具体介l之前先看一下dll的链接方式?/pre>(1)隐式q接q里有两个方法来载入一个DLLQ一个方法是只链接到.lib文gQƈ?dll文g攑ֈ要用这个DLL的项目\径中?/pre>因此Q创Z个新的空的Win32控制台项目ƈd一个源文g。将我们创徏好的DLL攑օ与新目相同的目录下。同时我们还必须链接到dll_test.lib文g?/pre>可以在项目属性中讄Q也可以在源E序中用下面的语句来链接Q? #pragma comment(lib, "dll_test.lib")最后,我们q要在新的win32控制台项目中包含前面的dll_test.h头文件。可以把q个头文件放到新建win32控制台项目的目录中然后在E序中加入语句: #include "dll_test.h"新项目代码如下:#include<iostream>#include "DLLTutorial.h"int main(){Function();std::cout<< Add(32, 56)<< endl;return 0;}(2)昄链接E微复杂一点的加蝲DLL的方法需要用到函数指针和一些Windows函数。但是,通过q种载入DLL的方法,不需要DLL?lib文g或头文gQ而只需要DLL卛_?/pre>下面列出一些代码:/****************************************************************/ #include <iostream> #include <windows.h> typedef int (*AddFunc)(int,int); typedef void (*FunctionFunc)();int main(){ AddFunc _AddFunc;FunctionFunc _FunctionFunc;HINSTANCE hInstLibrary = LoadLibrary("DLL_Tutorial.dll");if (hInstLibrary == NULL) { FreeLibrary(hInstLibrary); }_AddFunc = (AddFunc)GetProcAddress(hInstLibrary, "Add");_FunctionFunc = (FunctionFunc)GetProcAddress(hInstLibrary, "Function");if ((_AddFunc == NULL) || (_FunctionFunc == NULL)) { FreeLibrary(hInstLibrary); }std::cout << _AddFunc(23, 43) << std::endl;_FunctionFunc();std::cin.get();FreeLibrary(hInstLibrary);return(1); } /*******************************************************************/首先可以看到Q这里包括进了windows.h头文Ӟ同时L了对dll_test.h头文件的包含。原因很单:因ؓwindows.h包含了一些Windows函数Q?/pre>它也包含了一些将会用到的Windows特定变量。可以去掉DLL的头文gQ因为当使用q个Ҏ载入DLL时ƈ不需要其头文件?/pre>下面你会看到Q以下面形式的一块古灵_怪的代码:typedef int (*AddFunc)(int,int); typedef void (*FunctionFunc)();q是函数指针。因是一个关于DLL的自学指南,深入探究函数指针出了本指南的范_因此Q现在我们只把它们当作DLL包含的函数的别名?/pre>我喜Ƣ在N?#8220;Func”命名之?int,int)部分是这个函数的参数部分Q比如,Add函数要获得两个整敎ͼ因此Q你需要它?/pre>Q译注:?int,int)部分Q作为函数指针的参数。Function函数没有参数Q因此你让它为空。main()部分中的前面两行是声明函数指针以使得你可以认为它们等同于DLL内部的函数。我只是喜欢预先定义它们?/pre>一个HINSTANCE是一个Windows数据cdQ是一个实例的句柄Q在此情况下Q这个实例将是这个DLL。你可以通过使用函数LoadLibrary()获得DLL?/pre>实例Q它获得一个名UC为参数?/pre>在调用LoadLibrary函数后,你必需查看一下函数返回是否成功。你可以通过查HINSTANCE是否{于NULLQ在Windows.h中定义ؓ0或Windows.h?/pre>含的一个头文gQ来查看其是否成功。如果其{于NULLQ该句柄是无效的,q且你必需释放q个库。换句话_你必需释放DLL获得的内存?/pre>如果函数q回成功Q你的HINSTANCE包含了指向DLL的句柄。一旦你获得了指向DLL的句柄,你现在可以从DLL中重新获得函数?/pre>Zq样作,你必M用函数GetProcAddress()Q它DLL的句柄(你可以用HINSTANCEQ和函数的名UC为参数。你可以让函数指针获得由GetProcAddress()q回的|同时你必需GetProcAddress()转换为那个函数定义的函数指针。D个例子,对于Add()函数Q你必需GetProcAddress()转换为AddFuncQ这是它知道参数及q回值的原因。现在,最好先定函数指针是否{于NULL以及它们拥有DLL的函数?/pre>q只是一个简单的if语句Q如果其中一个等于NULLQ你必需如前所q释攑ֺ。一旦函数指针拥有DLL的函敎ͼ你现在就可以使用它们了,但是q里有一?/pre>需要注意的地方Q你不能使用函数的实际名Uͼ你必需使用函数指针来调用它们。在那以后,所有你需要做的是释放库如此而已?/pre>现在你知道了DLL的一些基本知识。你知道如何创徏它们Q你也知道如何用两种不同的方法链接它们。这里仍然有更多的东襉K要我们学习,但我把它们留l你们自己探索了和更的作者来写了?
]]>