BlogJava-xiaomage234-隨筆分類-c/c++http://www.fpcwrs.live/xiaomage234/category/28353.html生命本就是一次凄美的漂流,記憶中放不下的,永遠是孩提時代的那一份浪漫與純真!zh-cnMon, 16 Mar 2015 17:28:00 GMTMon, 16 Mar 2015 17:28:00 GMT6013 款開源的全文搜索引擎[轉]http://www.fpcwrs.live/xiaomage234/archive/2015/03/16/423495.html小馬歌小馬歌Mon, 16 Mar 2015 10:37:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2015/03/16/423495.htmlhttp://www.fpcwrs.live/xiaomage234/comments/423495.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2015/03/16/423495.html#Feedback1http://www.fpcwrs.live/xiaomage234/comments/commentRss/423495.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/423495.html

本文轉載自xum2008的博客,主要介紹13款現有的開源搜索引擎,你可以將它們用在你的項目中以實現檢索功能。 


1.  Lucene 

Lucene的開發語言是Java,也是Java家族中最為出名的一個開源搜索引擎,在Java世界中已經是標準的全文檢索程序,它提供了完整的查詢引擎和索引引擎,沒有中文分詞引擎,需要自己去實現,因此用Lucene去做一個搜素引擎需要自己去架構.另外它不支持實時搜索,但linkedin和twitter有分別對Lucene改進的實時搜素. 其中Lucene有一個C++移植版本叫CLucene,CLucene因為使用C++編寫,所以理論上要比lucene快. 

官方主頁:http://lucene.apache.org/ 

CLucene官方主頁:http://sourceforge.net/projects/clucene/ 

2.  Sphinx 

Sphinx是一個用C++語言寫的開源搜索引擎,也是現在比較主流的搜索引擎之一,在建立索引的事件方面比Lucene快50%,但是索引文件比Lucene要大一倍,因此Sphinx在索引的建立方面是空間換取事件的策略,在檢索速度上,和lucene相差不大,但檢索精準度方面Lucene要優于Sphinx,另外在加入中文分詞引擎難度方面,Lucene要優于Sphinx.其中Sphinx支持實時搜索,使用起來比較簡單方便. 

官方主頁:http://sphinxsearch.com/about/sphinx/ 

3.  Xapian 

Xapian是一個用C++編寫的全文檢索程序,它的api和檢索原理和lucene在很多方面都很相似,算是填補了lucene在C++中的一個空缺. 

官方主頁:http://xapian.org/ 

4.  Nutch 

Nutch是一個用java實現的開源的web搜索引擎,包括爬蟲crawler,索引引擎,查詢引擎. 其中Nutch是基于Lucene的,Lucene為Nutch提供了文本索引和搜索的API. 

對于應該使用Lucene還是使用Nutch,應該是如果你不需要抓取數據的話,應該使用Lucene,最常見的應用是:你有數據源,需要為這些數據提供一個搜索頁面,在這種情況下,最好的方式是直接從數據庫中取出數據,并用Lucene API建立索引. 

官方主頁:http://nutch.apache.org/ 

5.  DataparkSearch 

DataparkSearch是一個用C語言實現的開源的搜索引擎. 其中網頁排序是采用神經網絡模型.  其中支持HTTP,HTTPS,FTP,NNTP等下載網頁.包括索引引擎,檢索引擎和中文分詞引擎(這個也是唯一的一個開源的搜索引擎里有中文分詞引擎).能個性化定制搜索結果,擁有完整的日志記錄. 

官方主頁:http://www.dataparksearch.org/ 

6.  Zettair 

Zettair是根據Justin Zobel的研究成果為基礎的全文檢索實驗系統.它是用C語言實現的. 其中Justin Zobel在全文檢索領域很有名氣,是業界第一個系統提出倒排序索引差分壓縮算法的人,倒排列表的壓縮大大提高了檢索和加載的性能,同時空間膨脹率也縮小到相當優秀的水平. 由于Zettair是源于學術界,代碼是由RMIT University的搜索引擎組織寫的,因此它的代碼簡潔精煉,算法高效,是學習倒排索引經典算法的非常好的實例. 其中支持linux,windows,mac os等系統. 

官方主頁:http://www.seg.rmit.edu.au/zettair/about.html 

7.  Indri 

Indri是一個用C語言和C++語言寫的全文檢索引擎系統,是由University of Massachusetts和Carnegie Mellon University合作推出的一個開源項目. 特點是跨平臺,API接口支持Java,PHP,C++. 

官方主頁:http://www.lemurproject.org/indri/ 

8.  Terrier 

Terrier是由School of Computing Science,Universityof Glasgow用java開發的一個全文檢索系統. 

官方主頁:http://terrier.org/ 

9.  Galago 

Galago是一個用java語言寫的關于文本搜索的工具集. 其中包括索引引擎和查詢引擎,還包括一個叫TupleFlow的分布式計算框架(和google的MapReduce很像).這個檢索系統支持很多Indri查詢語言. 

官方主頁:http://www.galagosearch.org/ 

10.  Zebra 

Zebra是一個用C語言實現的檢索程序,特點是對大數據的支持,支持EMAIL,XML,MARC等格式的數據. 

官方主頁:https://www.indexdata.com/zebra 

11.  Solr 

Solr是一個用java開發的獨立的企業級搜索應用服務器,它提供了類似于Web-service的API接口,它是基于Lucene的全文檢索服務器,也算是Lucene的一個變種,很多一線互聯網公司都在使用Solr,也算是一種成熟的解決方案. 

官方主頁:http://lucene.apache.org/solr/ 

12.  Elasticsearch 

Elasticsearch是一個采用java語言開發的,基于Lucene構造的開源,分布式的搜索引擎. 設計用于云計算中,能夠達到實時搜索,穩定可靠. Elasticsearch的數據模型是JSON. 

官方主頁:http://www.elasticsearch.org/ 

13.  Whoosh 

Whoosh是一個用純python寫的開源搜索引擎. 

官方主頁:https://bitbucket.org/mchaput/whoosh/wiki/Home  

  1. 增加一個,SolrCloud是基于Solr和Zookeeper的分布式搜索方案,是正在開發中的Solr4.0的核心組件之一,它的主要思想是使用Zookeeper作為集群的配置信息中心。它有幾個特色功能:1)集中式的配置信息 2)自動容錯 3)近實時搜索 4)查詢時自動負載均衡


小馬歌 2015-03-16 18:37 發表評論
]]>
WebSocket協議分析http://www.fpcwrs.live/xiaomage234/archive/2014/04/19/412684.html小馬歌小馬歌Sat, 19 Apr 2014 07:16:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2014/04/19/412684.htmlhttp://www.fpcwrs.live/xiaomage234/comments/412684.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2014/04/19/412684.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/412684.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/412684.html

內容不斷更新,目前包括協議中握手和數據幀的分析

 

1.1 背景

1.2 協議概覽

協議包含兩部分:握手,數據傳輸。

客戶端的握手如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服務端的握手如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
客戶端和服務端都發送了握手,并且成功,數據傳輸即可開始。
 
1.3 發起握手
發起握手是為了兼容基于HTTP的服務端程序,這樣一個端口可以同時處理HTTP客戶端和WebSocket客戶端
因此WebSocket客戶端握手是一個HTTP Upgrade請求:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
握手中的域的順序是任意的。
 
5 數據幀
5.1 概述
WebScoket協議中,數據以幀序列的形式傳輸。
考慮到數據安全性,客戶端向服務器傳輸的數據幀必須進行掩碼處理。服務器若接收到未經過掩碼處理的數據幀,則必須主動關閉連接。
服務器向客戶端傳輸的數據幀一定不能進行掩碼處理。客戶端若接收到經過掩碼處理的數據幀,則必須主動關閉連接。
針對上情況,發現錯誤的一方可向對方發送close幀(狀態碼是1002,表示協議錯誤),以關閉連接。
5.2 幀協議
WebSocket數據幀結構如下圖所示:
      0                   1                   2                   3       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1      +-+-+-+-+-------+-+-------------+-------------------------------+      |F|R|R|R| opcode|M| Payload len |    Extended payload length    |      |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |      |N|V|V|V|       |S|             |   (if payload len==126/127)   |      | |1|2|3|       |K|             |                               |      +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +      |     Extended payload length continued, if payload len == 127  |      + - - - - - - - - - - - - - - - +-------------------------------+      |                               |Masking-key, if MASK set to 1  |      +-------------------------------+-------------------------------+      | Masking-key (continued)       |          Payload Data         |      +-------------------------------- - - - - - - - - - - - - - - - +      :                     Payload Data continued ...                :      + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +      |                     Payload Data continued ...                |      +---------------------------------------------------------------+
 
FIN:1位
表示這是消息的最后一幀(結束幀),一個消息由一個或多個數據幀構成。若消息由一幀構成,起始幀即結束幀。
 
RSV1,RSV2,RSV3:各1位
MUST be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the negotiated extensions defines the meaning of such a nonzero value, the receiving endpoint MUST _Fail the WebSocket Connection_.
這里我翻譯不好,大致意思是如果未定義擴展,各位是0;如果定義了擴展,即為非0值。如果接收的幀此處非0,擴展中卻沒有該值的定義,那么關閉連接。
 
OPCODE:4位
解釋PayloadData,如果接收到未知的opcode,接收端必須關閉連接。
0x0表示附加數據幀
0x1表示文本數據幀
0x2表示二進制數據幀
0x3-7暫時無定義,為以后的非控制幀保留
0x8表示連接關閉
0x9表示ping
0xA表示pong
0xB-F暫時無定義,為以后的控制幀保留
 
MASK:1位
用于標識PayloadData是否經過掩碼處理。如果是1,Masking-key域的數據即是掩碼密鑰,用于解碼PayloadData。客戶端發出的數據幀需要進行掩碼處理,所以此位是1。
 
Payload length:7位,7+16位,7+64位
PayloadData的長度(以字節為單位)。
如果其值在0-125,則是payload的真實長度。
如果值是126,則后面2個字節形成的16位無符號整型數的值是payload的真實長度。注意,網絡字節序,需要轉換。
如果值是127,則后面8個字節形成的64位無符號整型數的值是payload的真實長度。注意,網絡字節序,需要轉換。
長度表示遵循一個原則,用最少的字節表示長度(我理解是盡量減少不必要的傳輸)。舉例說,payload真實長度是124,在0-125之間,必須用前7位表示;不允許長度1是126或127,然后長度2是124,這樣違反原則。
Payload長度是ExtensionData長度與ApplicationData長度之和。ExtensionData長度可能是0,這種情況下,Payload長度即是ApplicationData長度。
 
 
WebSocket協議規定數據通過幀序列傳輸。
客戶端必須對其發送到服務器的所有幀進行掩碼處理。
服務器一旦收到無掩碼幀,將關閉連接。服務器可能發送一個狀態碼是1002(表示協議錯誤)的Close幀。
而服務器發送客戶端的數據幀不做掩碼處理,一旦客戶端發現經過掩碼處理的幀,將關閉連接。客戶端可能使用狀態碼1002。
 

消息分片

分片目的是發送長度未知的消息。如果不分片發送,即一幀,就需要緩存整個消息,計算其長度,構建frame并發送;使用分片的話,可使用一個大小合適的buffer,用消息內容填充buffer,填滿即發送出去。

分片規則:

1.一個未分片的消息只有一幀(FIN為1,opcode非0)

2.一個分片的消息由起始幀(FIN為0,opcode非0),若干(0個或多個)幀(FIN為0,opcode為0),結束幀(FIN為1,opcode為0)。

3.控制幀可以出現在分片消息中間,但控制幀本身不允許分片。

4.分片消息必須按次序逐幀發送。

5.如果未協商擴展的情況下,兩個分片消息的幀之間不允許交錯。

6.能夠處理存在于分片消息幀之間的控制幀

7.發送端為非控制消息構建長度任意的分片

8.client和server兼容接收分片消息與非分片消息

9.控制幀不允許分片,中間媒介不允許改變分片結構(即為控制幀分片)

10.如果使用保留位,中間媒介不知道其值表示的含義,那么中間媒介不允許改變消息的分片結構

11.如果協商擴展,中間媒介不知道,那么中間媒介不允許改變消息的分片結構,同樣地,如果中間媒介不了解一個連接的握手信息,也不允許改變該連接的消息的分片結構

12.由于上述規則,一個消息的所有分片是同一數據類型(由第一個分片的opcode定義)的數據。因為控制幀不允許分片,所以一個消息的所有分片的數據類型是文本、二進制、opcode保留類型中的一種。

需要注意的是,如果控制幀不允許夾雜在一個消息的分片之間,延遲會較大,比如說當前正在傳輸一個較大的消息,此時的ping必須等待消息傳輸完成,才能發送出去,會導致較大的延遲。為了避免類似問題,需要允許控制幀夾雜在消息分片之間。

控制幀

 

 

根據官方文檔整理,官方文檔參考http://datatracker.ietf.org/doc/rfc6455/?include_text=1



小馬歌 2014-04-19 15:16 發表評論
]]>
Websocket協議簡介http://www.fpcwrs.live/xiaomage234/archive/2014/04/19/412683.html小馬歌小馬歌Sat, 19 Apr 2014 07:00:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2014/04/19/412683.htmlhttp://www.fpcwrs.live/xiaomage234/comments/412683.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2014/04/19/412683.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/412683.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/412683.html
今天@julyclyde 在微博上問我websocket的細節。但是這個用70個字是無法說清楚的,所以就整理在這里吧。恰好我最近要重構年前寫的websocket的代碼。
眾所周知,HTTP是一種基于消息(message)的請求(request )/應答(response)協議。當我們在網頁中點擊一條鏈接(或者提交一個表單)的時候,瀏覽器給服務器發一個request message,然后服務器算啊算,答復一條response message。主動發起TCP連接的是client,接受TCP連接的是server。HTTP消息只有兩種:request和response。client只能發送request message,server只能發送response message。一問一答,因此按HTTP協議本身的設計,服務器不能主動的把消息推給客戶端。而像微博、網頁聊天、網頁游戲等都需要服務器主動給客戶端推東西,現在只能用long polling等方式模擬,很不方便。
 
OK,來看看internet的另一邊,網絡游戲是怎么工作的?
我之前在一個游戲公司工作。我們做游戲的時候,普遍采用的模式是雙向、異步消息模式。
首先通信的最基本單元是message。(這點和HTTP一樣)
其次,是雙向的。client和server都可以給對方發消息(這點和HTTP不一樣)
最后,消息是異步的。我給服務器發一條消息出去,然后可能有一條答復,也可能有多條答復,也可能根本沒有答復。無論如何,調用完send方法我就不管了,我不會傻乎乎的在這里等答復。服務器和客戶端都會有一個線程專門負責read,以及一個大大的switch… case,根據message id做相應的action。
while( msg=myconnection.readMessage()){
switch(msg.id()){
case LOGIN: do_login(); break;
case TALK: do_talk(); break;
}
}
Websocket就是把這樣一種模式,搬入到HTTP/WEB的框架內。它主要解決兩個問題:
從服務器給客戶端主動推東西。
HTTP協議傳輸效率低下的問題。這一點在web service中尤為突出。每個請求和應答都得帶上很長的http header!
websocket協議在RFC 6455中定義,這個RFC在上個月(2011年12月)才終于定稿、提交。所以目前沒有任何一個瀏覽器是能完全符合這個RFC的最終版的。Google是websocket協議的主力支持者,目前主流的瀏覽器中,對websocket支持最好的就是chrome。chrome目前的最新版本是16,支持的是RFC 6455的draft 13,http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-13 。IE9則是完全不支持websocket。而server端,只有jetty和Node.js對websocket的支持比較好。
 
Websocket協議可以分為兩個階段,一個是握手階段,一個是數據傳輸階段。
在建立TCP連接之后,首先是websocket層的握手。這階段很簡單,client給server發一個http request,server給client一個http response。這個階段,所有數據傳輸都是基于文本的,和現有的HTTP/1.1協議保持兼容。
這是一個請求的例子:
Connection:Upgrade
Host:echo.websocket.org
Origin:http://websocket.org
Sec-WebSocket-Key:ov0xgaSDKDbFH7uZ1o+nSw==
Sec-WebSocket-Version:13
Upgrade:websocket
 
(其中Host和Origin不是必須的)
Connection是HTTP/1.1中常用的一個header,以前經常填的是keepalive或close。這里填的是Upgrade。在設計HTTP/1.1的時候,委員們就想到一個問題,假如以后出HTTP 2.0了,那么現有的這套東西怎么辦呢?所以HTTP協議中就預定義了一個header叫Upgrade。如果客戶端發來的請求中有這個,那么意思就是說,我支持某某協議,并且我更偏向于用這個協議,你看你是不是也支持?你要是支持,咱們就換新協議吧!
然后就是websocket協議中所定義的兩個特殊的header,Sec-WebSocket-Key和Sec-WebSocket-Version。
其中Sec-WebSocket-Key是客戶端生的一串隨機數,然后base64之后填進去的。Sec-WebSocket-Version是指協議的版本號。這里的13,代表draft 13。下面給出,我年前寫的發送握手請求的JAVA代碼:
// 生一個隨機字符串,作為Sec-WebSocket-Key
?View Code JAVA
        byte[] nonce = new byte[16];        rand.nextBytes(nonce);        BASE64Encoder encode = new BASE64Encoder();        String chan = encode.encode(nonce);         HttpRequest request = new BasicHttpRequest("GET", "/someurl");        request.addHeader("Host", host);        request.addHeader("Upgrade", "websocket");        request.addHeader("Connection", "Upgrade");        request.addHeader("Sec-WebSocket-Key", chan); // 生的隨機串         request.addHeader("Sec-WebSocket-Version", "13");        HttpResponse response;        try {            conn.sendRequestHeader(request);            conn.flush();            request.toString();            response = conn.receiveResponseHeader();        } catch (HttpException ex) {            throw new RuntimeException("handshake fail", ex);        }
 
服務器在收到握手請求之后需要做相應的答復。消息的例子如下:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Date:Sun, 29 Jan 2012 18:05:49 GMT
Sec-WebSocket-Accept:7vI97qQ5QRxq6lD6E5RRX36mOBc=
Server:jetty
Upgrade:websocket
(其中Date、Server都不是必須的)
第一行是HTTP的Status-Line。注意,這里的Status Code是101。很少見吧!Sec-WebSocket-Accept字段是一個根據client發來的Sec-WebSocket-Key得到的計算結果。
算法為:
把客戶端發來的key作為字符串,與” 258EAFA5-E914-47DA-95CA-C5AB0DC85B11″這個字符串連接起來,然后做sha1 Hash,將計算結果base64編碼。注意,用來和” 258EAFA5-E914-47DA-95CA-C5AB0DC85B11″做連接操作的字符串,是base64編碼后的。也就是說,客戶端發來什么樣就是什么樣,不要試圖去做base64解碼。
示例代碼如下:
?View Code CPP
    std::string value=request.get("Sec-WebSocket-Key");    value+="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";    unsigned char hash[20];    sha1::calc(value.c_str(),value.length(),hash);    std::string res=base64_encode(hash,sizeof(hash));    std::ostringstream oss;    oss<<"HTTP/1.1 101 Switching Protocols\r\n"        "Upgrade: websocket\r\n"        "Connection: Upgrade\r\n"        "Sec-WebSocket-Accept: "<<res<<"\r\n";                   connection.send(oss.str());
握手成功后,進入數據流階段。這個階段就和http協議沒什么關系了。是在TCP流的基礎上,把數據分成frame而已。首先,websocket的一個message,可以被分成多個frame。從邏輯上來看,frame的格式如下
isFinal
opcode
isMasked
Data length
Mask key
Data(可以是文本,也可以是二進制)
 
isFinal:
每個frame的第一個字節的最高位,代表這個frame是不是該message的最后一個frame。1代表是最后一個,0代表后面還有。
opcode:
指明這個frame的類型。目前定義了這么幾類continuation、text 、binary 、connection close、ping、pong。對于應用而言,最關心的就是,這個message是binary的呢,還是text的?因為html5中,對于這兩種message的接口有細微不一樣。
isMasked:
客戶端發給服務器的消息,要求加擾之后發送。加擾的方式是:客戶端生一個32位整數(mask key),然后把data和這32位整數做異或。
mask key:
前面已經說過了,就是用來做異或的隨機數。
Data:
這才是我們真正要傳輸的數據啊!!
發送frame時加擾的代碼如下:
        java.util.Random rand ;
        ByteBuffer buffer;
        byte[] dataToSend;
        …
        
        byte[] mask = new byte[4];
        rand.nextBytes(mask);
        buffer.put(mask);
        int oldpos = buffer.position();        
        buffer.put(data);
        int newpos = buffer.position();
        // 按位異或
        for (int i = oldpos; i != newpos; ++i) {
            int maskIndex = (i – oldpos) % mask.length;
            buffer.put(i, (byte) (buffer.get(i) ^ (byte) mask[maskIndex]));
        }
下面討論一下這個協議的某些設計:
為什么要做這個異或操作呢?
說來話長。首先從Connection:Upgrade這個header講起。本來它是留給TLS用的。就是,假如我從80端口去連接一個服務器,然后通過發送Connection:Upgrade,仿照上面所說的流程,把http協議”升級”成https協議。但是實際上根本沒人這么用。你要用https,你就去連接443。我80端口根本不處理https。由于這個header只是出現在rfc中,并未實際使用,于是大多數cache server看不懂這個header。這樣的結果是,cache server可能以為后面的傳輸數據依然是普通的http協議,然后按照原來的規則做cache。那么,如果這個client和server都已經被黑客很好的操控,他就可以往這個cache server上投毒。比如,從client發送一個websocket frame,但是偽裝成普通的http GET請求,指向一個JS文件。但是這個GET請求的目的地未必是之前那個websocket server,可能是另外一臺web server。然后他再去操控這個web server,做好配合,給一個看起來像http response的答復(實際是websocket frame),里面放的是被修改過的js文件。然后cache server就會把這個js文件錯誤的緩存下來,以后發給其他人。
首先,client是誰?是瀏覽器。它在一個不很安全的環境中,很容易受到XSS或者流氓插件的攻擊。假如我們的頁面遭到了xss,使得攻擊者可以利用JS從受害者的頁面上發送任意字符串給服務器,如果沒有這個異或操作,那么他就可以控制什么樣的二進制數據出現在信道上,從而實現上述攻擊。但是我還是覺得有點問題。proxy server一般都會對目的地做嚴格的限制,比如,sina的squid肯定不會幫new.163.com做cache。那么既然你已經控制了一個web server,為什么不讓js直接這么做呢?那篇paper的名字叫《Talking to Yourself for Fun and Pro?t》,有空我繼續看。貌似是中國人寫的。
還有,為什么要把message分成frame呢? 因為HTTP協議有chunk功能,可以讓服務器一邊生數據,一邊發。而websocket協議也考慮到了這點。如果沒有framing功能,那么我必須知道整個message的長度之后,才能開始發送message的data。


小馬歌 2014-04-19 15:00 發表評論
]]>
如何將Int轉String? (C/C++) (C)http://www.fpcwrs.live/xiaomage234/archive/2014/04/11/412286.html小馬歌小馬歌Fri, 11 Apr 2014 03:50:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2014/04/11/412286.htmlhttp://www.fpcwrs.live/xiaomage234/comments/412286.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2014/04/11/412286.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/412286.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/412286.htmlC/C++並沒有提供內建的int轉string函數,這裡提供幾個方式達到這個需求。

1.若用C語言,且想將int轉char *,可用sprintf(),sprintf()可用類似printf()參數轉型。

 1/* 
 2(C) OOMusou 2007 http://oomusou.cnblogs.com
 3
 4Filename    : int2str_sprintf.cpp
 5Compiler    : Visual C++ 8.0 / ANSI C
 6Description : Demo the how to convert int to const char *
 7Release     : 01/06/2007 1.0
 8*/

 9#include "stdio.h"
10
11void int2str(int , char *);
12
13int main() {
14  int i = 123;
15  char s[64];
16  int2str(i, s);
17  puts(s);
18}

19
20void int2str(int i, char *s) {
21  sprintf(s,"%d",i);
22}


2.若用C語言,還有另外一個寫法,使用_itoa(),Microsoft將這個function擴充成好幾個版本,可參考MSDN Library。

 1/* 
 2(C) OOMusou 2007 http://oomusou.cnblogs.com
 3
 4Filename    : int2str_itoa.cpp
 5Compiler    : Visual C++ 8.0 / ANSI C
 6Description : Demo the how to convert int to const char *
 7Release     : 01/06/2007 1.0
 8*/

 9#include "stdio.h"  // puts()
10#include "stdlib.h" // _itoa()
11
12void int2str(int , char *);
13
14int main() {
15  int i = 123;
16  char s[64];
17  int2str(i, s);
18  puts(s);
19}

20
21void int2str(int i, char *s) {
22  _itoa(i, s, 10);
23}


3.若用C++,stringstream是個很好用的東西,stringstream無論是<<或>>,都會自動轉型,要做各型別間的轉換,stringstream是個很好的媒介。

 1/* 
 2(C) OOMusou 2007 http://oomusou.cnblogs.com
 3
 4Filename    : int2str_sstream.cpp
 5Compiler    : Visual C++ 8.0 / ISO C++
 6Description : Demo the how to convert int to string
 7Release     : 01/06/2007 1.0
 8*/

 9
10#include <iostream>
11#include <string>
12#include <sstream>
13
14using namespace std;
15
16string int2str(int &);
17
18int main(void{
19  int i = 123;
20  string s;
21  s = int2str(i);
22
23  cout << s << endl;
24}

25
26string int2str(int &i) {
27  string s;
28  stringstream ss(s);
29  ss << i;
30
31  return ss.str();
32}


4.若用C++,據稱boost有更好的方法,不過我還沒有裝boost,所以無從測試



小馬歌 2014-04-11 11:50 發表評論
]]>
linux使用msgpack及測試http://www.fpcwrs.live/xiaomage234/archive/2014/04/10/412214.html小馬歌小馬歌Thu, 10 Apr 2014 05:43:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2014/04/10/412214.htmlhttp://www.fpcwrs.live/xiaomage234/comments/412214.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2014/04/10/412214.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/412214.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/412214.html在網絡程序的開發中,免不了會涉及到服務器與客戶端之間的協議交互,由于客戶端與服務器端的平臺的差異性(有可能是windows,android,linux等等),以及網絡字節序等問題,通信包一般會做序列化與反序列化的處理,也就是通常說的打包解包工作。google的protobuf是一個很棒的東西,它不僅提供了多平臺的支持,而且直接支持從配置文件生成代碼。但是這么強大的功能,意味著它的代碼量以及編譯生成的庫文件等等都不會小,如果相對于手機游戲一兩M的安裝包來說,這個顯然有點太重量級了(ps:查了一下protobuf2.4.1的jar的包大小有400k左右),而且在手機游戲客戶端與服務器的交互過程中,很多時候基本的打包解包功能就足夠了。

今天經同事推薦,查閱了一下msgpack相關的資料。msgpack提供快速的打包解包功能,官網上給出的圖號稱比protobuf快4倍,可以說相當高效了。最大的好處在與msgpack支持多語言,服務器端可以用C++,android客戶端可以用java,能滿足實際需求。

在實際安裝msgpack的過程中,碰到了一點小問題,因為開發機器是32位機器,i686的,所以安裝完后跑一個簡單的sample時,c++編譯報錯,錯誤的表現為鏈接時報錯:undefined reference to `__sync_sub_and_fetch_4',后來參考了下面的博客,在configure時指定CFLAGS="-march=i686"解決,注意要make clean先。

msgpack官網地址:http://msgpack.org/

安裝過程中參考的博客地址:http://blog.csdn.net/xiarendeniao/article/details/6801338



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

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

補上近期簡單的測試結果

測試機器,cpu 4核 Dual-Core AMD Opteron(tm) Processor 2212,單核2GHz,1024KB cache, 內存4G。


1. 簡單包測試

  1. struct ProtoHead  
  2. {  
  3.     uint16_t uCmd;      // 命令字  
  4.     uint16_t uBodyLen;  // 包體長度(打包之后的字符串長度)  
  5.     uint32_t uSeq;      //消息的序列號,唯一標識一個請求  
  6.     uint32_t uCrcVal;           // 對包體的crc32校驗值(如果校驗不正確,服務器會斷開連接)  
  7. };  
  8.   
  9. struct ProtoBody  
  10. {  
  11.     int num;  
  12.     std::string str;  
  13.     std::vector<uint64_t> uinlst;  
  14.     MSGPACK_DEFINE(num, str, uinlst);  
  15. };  

測試中省略了包頭本地字節序與網絡字節序之間的轉化,只有包體做msgpack打包處理.

vector數組中元素數量為16個,每次循環做一次打包與解包,并驗證前后數據是否一致,得到的測試結果如下:

總耗時(s)

循環次數

平均每次耗時(ms)

0.004691

100

0.04691

0.044219

1000

0.044219

0.435725

10000

0.043573

4.473818

100000

0.044738

總結:基本每次耗時0.045ms左右,每秒可以打包解包22k次,速度很理想。


2. 復雜包測試(vector嵌套)

  1. struct test_node  
  2. {  
  3.     std::string str;  
  4.     std::vector<uint32_t> idlst;  
  5.   
  6.     test_node()  
  7.     {  
  8.         str = "it's a test node";  
  9.   
  10.         for (int i = 0; i++; i < 10)  
  11.         {  
  12.             idlst.push_back(i);  
  13.         }  
  14.     }  
  15.   
  16.     bool operator == (const test_node& node) const  
  17.     {  
  18.         if (node.str != str)  
  19.         {  
  20.             return false;  
  21.         }  
  22.   
  23.         if (node.idlst.size() != idlst.size())  
  24.         {  
  25.             return false;  
  26.         }  
  27.   
  28.         for (int i = 0; i < idlst.size(); i++)  
  29.         {  
  30.             if (idlst[i] != node.idlst[i])  
  31.             {  
  32.                 return false;  
  33.             }  
  34.         }  
  35.         return true;  
  36.     }  
  37.   
  38.     MSGPACK_DEFINE(str, idlst);  
  39. };  
  40.   
  41. struct ProtoBody  
  42. {  
  43.     int num;  
  44.     std::string str;  
  45.     std::vector<uint64_t> uinlst;  
  46.     std::vector<test_node> nodelst;  
  47.   
  48.     MSGPACK_DEFINE(num, str, uinlst, nodelst);  
  49. };  
每個nodelst中插入16個node,每個node中的idlst插入16個id,同1中的測試方法,得到測試結果如下:

總耗時(s)

循環次數

平均每次耗時(ms)

0.025401

100

0.25401

0.248396

1000

0.248396

2.533385

10000

0.253339

25.823562

100000

0.258236

基本上每次打包解包一次要耗時0.25ms,每秒估算可以做4k次打包解包,速度還是不錯的。


3. 加上crc校驗

如果每個循環中,打包過程加上crc的計算,解包過程中加上crc校驗,得到測試結果如下:

總耗時(s)

循環次數

平均每次耗時(ms)

0.025900

100

0.25900

0.260424

1000

0.260424

2.649585

10000

0.264959

26.855452

100000

0.268555

基本上每次打包解包耗時0.26ms左右,與沒有crc差別不大;



小馬歌 2014-04-10 13:43 發表評論
]]>
對象序列化類庫MsgPack介紹http://www.fpcwrs.live/xiaomage234/archive/2014/04/10/412213.html小馬歌小馬歌Thu, 10 Apr 2014 05:42:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2014/04/10/412213.htmlhttp://www.fpcwrs.live/xiaomage234/comments/412213.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2014/04/10/412213.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/412213.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/412213.htmlMessagePack(以下簡稱MsgPack)一個基于二進制高效的對象序列化類庫,可用于跨語言通信。它可以像JSON那樣,在許多種語言之間交換結構對象;但是它比JSON更快速也更輕巧。支持Python、Ruby、Java、C/C++等眾多語言。比Google Protocol Buffers還要快4倍。

代碼:
> require ‘msgpack’
> msg = [1,2,3].to_msgpack  #=> “\x93\x01\x02\x03″
> MessagePack.unpack(msg)   #=> [1,2,3]

以上摘自oschina介紹。

msgpack官方主頁:http://msgpack.org/

github主頁:https://github.com/msgpack/msgpack

因我只使用C++版本,故只下載了CPP部分,大家請按需下載。

源碼安裝msgpack

打開終端下載msgpac 4 cpp最新版本0.5.7

wget http://msgpack.org/releases/cpp/msgpack-0.5.7.tar.gz

解壓

tar zxvf msgpack-0.5.7.tar.gz

進入解壓后的文件夾中進行安裝

cd msgpack-0.5.7 ./configure make sudo make install

當然了,你也可以使用git和svn直接抓取源代碼進行編譯,不過需要安裝版本控制工具。

自動安裝msgpack
apt-get install libmsgpack-dev

(安裝過程中會將頭文件拷貝到 /usr/local/include/ 庫文件拷貝到/usr/local/lib/)

安裝好了,我們直接使用用它看看效果。

直接包含msgpack.hpp即可使用。

simple using
#include <msgpack.hpp> #include <vector> #include <string> #include <iostream>  int main() { 	std::vector<std::string> _vecString; 	_vecString.push_back("Hello"); 	_vecString.push_back("world");  	// pack 	msgpack::sbuffer _sbuffer; 	msgpack::pack(_sbuffer, _vecString); 	std::cout << _sbuffer.data() << std::endl;  	// unpack 	msgpack::unpacked msg; 	msgpack::unpack(&msg, _sbuffer.data(), _sbuffer.size()); 	msgpack::object obj = msg.get(); 	std::cout << obj << std::endl;  	// convert 	std::vector<std::string> _vecRString; 	obj.convert(&_vecRString);  	// print 	for(size_t i = 0; i < _vecRString.size(); ++i) 	{ 		std::cout << _vecRString[i] << std::endl; 	}      return 0; }

結果就不貼了,大家自己運行下便知。

using stream
#include <msgpack.hpp> #include <vector> #include <string> #include <iostream>  int main() { 	// msgpack stream  	// use msgpack::packer to pack multiple objects. 	msgpack::sbuffer buffer_; 	msgpack::packer pack_(&buffer_); 	pack_.pack(std::string("this is 1st string")); 	pack_.pack(std::string("this is 2nd string")); 	pack_.pack(std::string("this is 3th string"));  	// use msgpack::unpacker to unpack multiple objects. 	msgpack::unpacker unpack_; 	unpack_.reserve_buffer(buffer_.size()); 	memcpy(unpack_.buffer(), buffer_.data(), buffer_.size()); 	unpack_.buffer_consumed(buffer_.size());  	msgpack::unpacked result_; 	while (unpack_.next(&result_)) 	{ 		std::cout << result_.get() << std::endl; 	}  	return 0; }

使用sbuffer stream序列化多個對象。

如何序列化自定義數據結構

msgpack支持序列化/反序列化自定義數據結構,只需要簡單的使用MSGPACK_DEFINE宏即可。

##include <msgpack.hpp> #include <vector> #include <string>  class my_class { private: 	std::string my_string; 	std::vector vec_int; 	std::vector vec_string; public: 	MSGPACK_DEFINE(my_string, vec_int, vec_string); };  int main() { 	std::vector<my_class> my_class_vec;  	// add some data  	msgpack::sbuffer buffer; 	msgpack::pack(buffer, my_class_vec);  	msgpack::unpacked msg; 	msgpack::unpack(&msg, buffer.data(), buffer.size());  	msgpack::object obj = msg.get(); 	std::vector<my_class> my_class_vec_r; 	obj.convert(&my_class_vec_r);  	return 0; }

這樣我們就可以在網絡通訊等地方可以使用msgpack來序列化我們的數據結構,完全可以做到安全高效,并且可以在接收方使用別的語言來處理結構做邏輯。完全是 多種語言-多種語言,現在支持的語言如下:

Ruby Perl Python C/C++ Java PHP JS OC C# Lua Scala D Haskell Erlang Ocaml Smallalk GO LabVIEW

完全夠我們使用了,當然了,如果沒有你要的語言,建議看源代碼模仿一個。

關于性能測試結果可以查看:linux使用msgpack及測試 



小馬歌 2014-04-10 13:42 發表評論
]]>
thrift rpc 使用常見問題解答和經驗http://www.fpcwrs.live/xiaomage234/archive/2014/02/14/409868.html小馬歌小馬歌Fri, 14 Feb 2014 08:42:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2014/02/14/409868.htmlhttp://www.fpcwrs.live/xiaomage234/comments/409868.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2014/02/14/409868.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/409868.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/409868.html原文:http://blog.rushcj.com/2010/08/21/try-thrift/

Thrift是一個非常棒的工具,是Facebook的開源項目,目前的開發非常的活躍,由Apache管理,所以用的是Apache Software License,這非常重要,因為可以放心的對其修改并用到自己的項目中。

談到修改Thrift,這非常重要。因為我覺得如果要嚴肅的使用Thrift,不可避免的要深入了解它,并幾乎都要修改Thrift的代碼。一個通信框架,它不可能幫你做到所有的事情,也不可能在不了解的情況下就貿然的使用。

1.Thrift 的Java Server/Client有個較為嚴重的bug(https://issues.apache.org/jira/browse/THRIFT-601 ),隨機向thrift  sever的監聽端口發些數據,可能會導致Server OutOfMemory,細細看看代碼,這個bug有點土。

2.Thrift Client線程不安全,多線程下使用可能導致Server和客戶端程序崩潰。Client的每次調用遠程方法其實是有多次Socket寫操作,因此每個線程中使用的Client要保證獨立,如果多個線程混用同一個Client(其實是用同一個Socket),可能會導致傳輸的字節順序混亂,使得Server OutOfMemory(參考1)

3.Thrift定義數據結構時,盡量避免用map, 或者set。在cpp下, map被對應為std::map(rb tree)和std::set,thrift生成的類不會重載”<”,因此需要手動修改生成類,否則link沒法通過。較為麻煩。

4.如果Client端基于效率考慮,要緩存Socket,需要重新實現其TTransport類,以支持 Socket緩存池。當然,這個實現其實跟thrift沒多大關系,算是2次開發。但一般都要這么做的吧?

5.如果Client基于效率考慮,緩存了Socket,那么thrift Server端的模式選擇就較為重要了。如果使用同步的TThreadPoolServer,那么無可避免的,客戶端緩存1個Socket,Server端就會有一個線程一直處于Server狀態,等待peek這個Socket上的數據。這個線程就不能用于其它請求了。所以,及時清理Client端的Socket及控制Socket池的大小是非常必要的。

6.聽同事說CPP Thrift Server的Epoll NonBlocking模式有效率問題。其實,并發要求不高的Server用LT模式的EPoll其實很方便的,當然,這個要自己給Thrfit Server做patch了,不過也不麻煩。開發起來也是很方便的。我想給我們的Server加個EPOLLONESHOT的同步EPoll實現。

7.CPP下的 TThreadPoolServer和TThreadServer由一個有趣的問題,如果有客戶端維護長連接,那么對這個Server實例做析構的時候會堵塞(前面說過了,在peek中…)。

8.用valgrind看,thrift cpp似乎有一些內存問題。沒細看。

9.無論是Java,還是CPP,Server端都無法通過合法的方式獲取Client的ip, port。可以通過編寫ThriftServerEventHandler可以處理這件事情。如果想要獲取Client ip, port的話,可以看看這個東西。



小馬歌 2014-02-14 16:42 發表評論
]]>
從Lua中調用C函數http://www.fpcwrs.live/xiaomage234/archive/2013/09/17/404165.html小馬歌小馬歌Tue, 17 Sep 2013 04:30:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2013/09/17/404165.htmlhttp://www.fpcwrs.live/xiaomage234/comments/404165.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2013/09/17/404165.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/404165.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/404165.html閱讀全文

小馬歌 2013-09-17 12:30 發表評論
]]>
更好的內存管理-jemalloc http://www.fpcwrs.live/xiaomage234/archive/2013/09/12/403984.html小馬歌小馬歌Thu, 12 Sep 2013 04:05:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2013/09/12/403984.htmlhttp://www.fpcwrs.live/xiaomage234/comments/403984.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2013/09/12/403984.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/403984.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/403984.htmlhttp://wangkaisino.blog.163.com/blog/static/1870444202011431112323846/


今年年初由于facebook而火起jemalloc人之,但殊不知,malloc界里面很早就出名了。Jemalloc始人Jason Evans也是在FreeBSD很有名的開發。此人就在2006提高低性能的mallocjemallocJemalloc2007始以FreeBSD準引進來的。件技革新很多是FreeBSD起的。在FreeBSD用廣泛的技術會慢慢入到linux

目前jemallocfirefox中也在使用。在firefox2中出存碎片問題之后,便在firefox3中使用了jemalloc。在safarichrome中使用的是googletcmalloc

Jemalloc的技特性

Jemalloc聚集了malloc的使用程中所驗證的很多技。忽略細節著眼,最出色的部分仍是arenathread cache。(事上,這兩個與tcmalloc的架幾乎相同。Jemalloc only的部分將會在另一次posting繼續

Arena

其像malloc集中管理一整塊內存,不如其分成塊來分而治之。此小便稱為arena想象一下,小朋友一圖紙們隨意地點。果可想而知,他肯定相互方而不敢肆意地synchronization),而影響畫圖效率。但是如果老事先在大圖紙分好每人的域,小朋友就可以又快又準地在各自地域上畫圖這樣念就是arena

Thread cache

如果是辟小塊內存,使不arena而直接malloc各自的thread cache域。此ideagoogletcmalloc的核心部分,亦在jemalloc中體

再拿上面的例子,小朋友除了一圖紙外,再各自A4這樣,小朋友在不大面的點,只在自己的A4上心情地即可(no arena seeking)。可以在自己手上的或涂(using thread cache),完全不用人(no synchronization, no locking),迅速有效地

jemalloc的核心layout。看著復雜,其都是上面明的部分。

更好的內存管理-jemalloc - Alex - wangkaisino的博客
 

實際jemalloc的性能呢?

更好的內存管理-jemalloc - Alex - wangkaisino的博客
 

最左的就是glibcmalloc,最右的就是jemalloc從圖表上可以看出,jemalloc的性能有glibc倍以上。非常倒性的性能差。因此,使用了jemalloc用程序自然快很多。Jemalloc的就是tcmallocTcmalloc的性能其相差甚微,低jemalloc2.1.04.5%上和tcmalloc1.4版本,而如今到了1.6版本,因此實際這兩應該是不相仲伯的。Jemalloc始人jason evans也意一點,cpu core 8以上的算機上jemalloc效率更高。

程序的最后的免午餐 – kth分布式技lab      

2005表了一篇文章免費午餐的時代結束了在之前,程序就算不用費腦子,cpu時鐘速度增加,程序性能自己就上去。但在不同,cpu時鐘趨定,而核地增加。程序需要適應這樣的多程多程的境,開發出適合的程序。文章的大這樣容。

6年之后的如今,篇文章完全現實了。事cpu時鐘停留在3GHz,而核不上升。在程序要適程多程的分布式算,速度才能上升。但是這樣的程序很

在在多程的境下,程序員們的最后一道午餐便是tcmallocjemalloc這樣malloc library于使用多程的程序而言,性能提高%

共享一下我本人的經驗。我本人在kth術研究所分布式技lab中承擔iLock(分布式同步工具,請參googlechubby)。在iLock中用了googletcmalloc果,性能提升了18~22%

最大的點就是不需要做任何復雜的工作便可得到這樣的效果。不需要代編譯。只需在行二制之前,在cmd窗口中

$ LD_PRELOAD=tcmalloc所設置的文件夾/libtcmalloc.so

這樣在之后行的用程序使用tcmallocjemalloc而代替glibcmallocptmalloc)。置此,我便可得到性能20%的提升,這真是送的最后的免午餐。

如今,在分布式技lab中使用googletcmalloc。原因在于性能上者差不多,但googletcmalloc所提供的程序分析工具非常(heap profiler, cpu profiler)豐富。所以tcmalloc可能更方便一些。

一定要使用最新的malloc?一定要的!



小馬歌 2013-09-12 12:05 發表評論
]]>
Darts: Double-ARray Trie System [zz]http://www.fpcwrs.live/xiaomage234/archive/2013/08/13/402750.html小馬歌小馬歌Tue, 13 Aug 2013 08:45:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2013/08/13/402750.htmlhttp://www.fpcwrs.live/xiaomage234/comments/402750.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2013/08/13/402750.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/402750.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/402750.htmlDarts 是用于構建雙數組 Double-Array [Aoe 1989] 的簡單的 C++ Template Library . 雙數組 (Double-Array) 是用于實現 Trie 的一種數據結構, 比其它的類 Trie 實現方式(Hash-Tree, Digital Trie, Patricia Tree, Suffix Array) 速度更快。 原始的 Double-Array 使能夠支持動態添加刪除 key, 但是 Darts 只支持把排好序的詞典文件轉換為靜態的 Double-Array.

Darts 既可以像 Hash 一樣作為簡單的詞典使用,也能非常高效的執行分詞詞典中必須的 Common Prefix Search 操作。

自2003年7月起, 兩個開源的日語分詞系統 MeCabChaSen 都使用了 Darts .

下載

  • Darts 是自由軟件.遵循 LGPL(Lesser GNU General Public License) 和 BSD 協議, 可以修改后重新發布.

Source

  • darts-0.32.tar.gz: HTTP

安裝

% ./configure 
% make
% make check
% make install
然后在程序中 include /usr/local/include/darts.h

使用方法

Darts 只提供了 darts.h 這個 C++ 模板文件。每次使用的時候 include 該文件即可.
使用這樣的發布方式是希望通過內聯函數實現高效率。

類接口

namespace Darts {

template <class NodeType, class NodeUType class ArrayType,
class ArrayUType, class LengthFunc = Length<NodeType> >

class DobuleArrayImpl
{
public:
typedef ArrayType result_type;
typedef NodeType key_type;

DoubleArrayImpl();
~DoubleArrayImpl();

int set_array(void *ptr, size_t = 0);
void *array();
void clear();
size_t size ();
size_t unit_size ();
size_t nonzero_size ();
size_t total_size ();

int build (size_t key_size,
key_type **key,
size_t *len = 0,
result_type *val = 0,
int (*pg)(size_t, size_t) = 0);

int open (const char *file,
const char *mode = "rb",
size_t offset = 0,
size_t _size = 0);

int save (const char *file,
const char *mode = "wb",
size_t offset = 0);

result_type exactMatchSearch (const key_type *key,
size_t len = 0,
size_t node_pos = 0)

size_t commonPrefixSearch (const key_type *key,
result_type *result,
size_t result_size,
size_t len = 0,
size_t node_pos = 0)

result_type traverse (const key_type *key,
size_t &node_pos,
size_t &key_pos,
size_t len = 0)
};

typedef Darts::DoubleArrayImpl<char, unsigned char,
int, unsigned int> DoubleArray;
};

模板參數說明

 

NodeTypeTrie 節點類型, 對普通的 C 字符串檢索, 設置為 char 型即可.
NodeUTypeTrie 節點轉為無符號整數的類型, 對普通的 C 字符串檢索, 設置為 unsigned char 型即可.
ArrayTypeDouble-Array 的 Base 元素使用的類型, 通常設置為有符號 32bit 整數
ArrayUTypeDouble-Array 的 Check 元素使用的類型, 通常設置為無符號 32bit 整數
LengthFunc使用 NodeType 數組的時候,使用該函數對象獲取數組的大小, 在該函數對象中對 operator() 進行重載. 
NodeType 是 char 的時候, 缺省使用 strlen 函數, 否則以 0 作為數組結束標志計算數組的大小 .

typedef 說明

模板參數類型的別名. 在外部需要使用這些類型的時候使用 .

key_type待檢索的 key 的單個元素的類型. 等同于 NodeType.
result_type單個結果的類型. 等同于 ArrayType .

方法說明

int Darts::DoubleArrayImpl::build(size_t size, const key_type **str, const size_t *len = 0, const result_type *val = 0, int (*progress_func)(size_t, size_t) = 0)
構建 Double Array .

size 詞典大小 (記錄的詞條數目),
str 指向各詞條的指針 (共 size個指針)
len 用于記錄各個詞條的長度的數組(數組大小為 size)
val 用于保存各詞條對應的 value 的數組 (數組大小為 size)
progress_func 構建進度函數.

str 的各個元素必須按照字典序排好序.
另外 val 數組中的元素不能有負值.
len, val, progress_func 可以省略,
省略的時候, len 使用 LengthFunc 計算,
val 的各元素的值為從 0 開始的計數值。 


構建成功,返回 0; 失敗的時候返回值為負.
進度函數 progress_func 有兩個參數.
第一個 size_t 型參數表示目前已經構建的詞條數 
第二個 size_t 型參數表示所有的詞條數 

result_type Darts::DoubleArrayImpl::exactMatchSearch(const key_type *key, size_t len = 0, size_t node_pos = 0)
進行精確匹配(exact match) 檢索, 判斷給定字符串是否為詞典中的詞條.

key 待檢索字符串,
len 字符串長度,
node_pos 指定從 Double-Array 的哪個節點位置開始檢索.

len, 和 node_pos 都可以省略, 省略的時候, len 缺省使用 LengthFunc 計算,
node_pos 缺省為 root 節點.

檢索成功時, 返回 key 對應的 value 值, 失敗則返回 -1. 

size_t Darts::DoubleArrayImpl::commonPrefixSearch (const key_type *key, result_type *result, size_t result_size, size_t len = 0, size_t node_pos = 0)
執行 common prefix search. 檢索給定字符串的哪些的前綴是詞典中的詞條

key 待檢索字符串,
result 用于保存多個命中結果的數組,
result_size 數組 result 大小,
len 待檢索字符串長度,
node_pos 指定從 Double-Array 的哪個節點位置開始檢索.

len, 和 node_pos 都可以省略, 省略的時候, len 缺省使用 LengthFunc 計算,
node_pos 缺省為 root 節點.

函 數返回命中的詞條個數. 對于每個命中的詞條, 詞條對應的 value 值存依次放在 result 數組中. 如果命中的詞條個數超過 result_size 的大小, 則 result 數組中只保存 result_size 個結果。函數的返回值為實際的命中詞條個數, 可能超過 result_size 的大小。 

result_t Darts::DoubleArrayImpl::traverse (const key_type *key, size_t &node_pos, size_t &key_pos, size_t len = 0)
traverse Trie, 檢索當前字符串并記錄檢索后到達的位置 

key 待檢索字符串,
node_pos 指定從 Double-Array 的哪個節點位置開始檢索.
key_pos 從待檢索字符串的哪個位置開始檢索
len 待檢索字符串長度,

該函數和 exactMatchSearch 很相似. traverse 過程是按照檢索串 key 在 TRIE 的節點中進行轉移.
但是函數執行后, 可以獲取到最后到達的 Trie 節點位置,最后到達的字符串位置 . 這和 exactMatchSearch 是有區別的. 

node_pos 通常指定為 root 位置 (0) . 函數調用后, node_pos 的值記錄最后到達的 DoubleArray 節點位置。 
key_pos 通常指定為 0. 函數調用后, key_pos 保存最后到達的字符串 key 中的位置。 

檢索失敗的時候, 返回 -1 或者 -2 .
-1 表示再葉子節點失敗, -2 表示在中間節點失敗,.
檢索成功的時候, 返回 key 對應的 value. 

int Darts::DoubleArrayImpl::save(const char *file, const char *mode = "wb", size_t offset = 0)
把 Double-Array 保存為文件.

file 保存文件名,
mode 文件打開模式 
offset 保存的文件位置偏移量, 預留將來使用, 目前沒有實現 .

成功返回 0 , 失敗返回 -1 

int Darts::DoubleArrayImpl::open (const char *file, const char *mode = "rb", size_t offset = 0, size_t size = 0)
讀入 Double-Array 文件.

file 讀取文件名,
mode 文件打開模式 
offset 讀取的文件位置偏移量 

size 為 0 的時候, size 使用文件的大小 .

成功返回 0 , 失敗返回 -1 

size_t Darts::DoubleArrayImpl::size()
返回 Double-Array 大小. 

size_t Darts::DoubleArrayImpl::unit_size()
Double-Array 一個元素的大小(byte).

size() * unit_size() 是, 存放 Double-Array 所需要的內存(byte) 大小. 

size_t Darts::DoubleArrayImpl::nonzero_size()
Double-Array 的所有元素中, 被使用的元素的數目, .
nonezero_size()/size() 用于計算壓縮率. 

例子程序

從靜態詞典構建雙數組 Double-Array.

#include <iostream>
#include <darts.h>

int main (int argc, char **argv)
{
using namespace std;

Darts::DoubleArray::key_type *str[] = { "ALGOL", "ANSI", "ARCO", "ARPA", "ARPANET", "ASCII" }; // same as char*
Darts::DobuleArray::result_type val[] = { 1, 2, 3, 4, 5, 6 }; // same as int

Darts::DoubleArray da;
da.build (6, str, 0, val);

cout << da.exactMatchSearch("ALGOL") << endl;
cout << da.exactMatchSearch("ANSI") << endl;
cout << da.exactMatchSearch("ARCO") << endl;;
cout << da.exactMatchSearch("ARPA") << endl;;
cout << da.exactMatchSearch("ARPANET") << endl;;
cout << da.exactMatchSearch("ASCII") << endl;;
cout << da.exactMatchSearch("APPARE") << endl;

da.save("some_file");
}

執行結果
1
2
3
4
5
6
-1

從標準輸入讀取字符串, 對 Double-Array 執行 Common Prefix Search

#include <iostream>
#include <string>
#include <algorithm>
#include <darts.h>

int main (int argc, char **argv)
{
using namespace std;

Darts::DoubleArray da;
if (da.open("some_file") == -1) return -1;

Darts::DoubleArray::result_type r [1024];
Darts::DoubleArray::key_type buf [1024];

while (cin.getline (buf, 1024)) {
size_t result = da.commonPrefixSearch(buf, r, 1024);
if (result == 0) {
cout << buf << ": not found" << endl;
} else {
cout << buf << ": found, num=" << result << " ";
copy (r, r + result, ostream_iterator<Darts::DoubleArray::result_type>(cout, " "));
cout << endl;
}
}

return 0;
}

付屬程序說明

mkdarts

% ./mkdarts DictionaryFile DoubleArrayFile 
把排序好的詞典 DictionaryFile 轉換為 DoubleArrayFile

darts

% ./darts DoubleArrayFile 

使用 DoubleArrayFile 做 common prefix search .

使用例子

% cd tests
% head -10 linux.words
ALGOL
ANSI
ARCO
ARPA
ARPANET
ASCII
..

% ../mkdarts linux.words dar
Making Double Array: 100% |*******************************************|
Done!, Compression Ratio: 94.6903 %

% ../darts dar
Linux
Linux: found, num=2 3697 3713
Windows
Windows: not found
LaTeX
LaTeX: found, num=1 3529

參考文獻, 鏈接



小馬歌 2013-08-13 16:45 發表評論
]]>
棧和堆的區別【總結】http://www.fpcwrs.live/xiaomage234/archive/2013/05/20/399513.html小馬歌小馬歌Mon, 20 May 2013 08:40:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2013/05/20/399513.htmlhttp://www.fpcwrs.live/xiaomage234/comments/399513.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2013/05/20/399513.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/399513.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/399513.html

1.1內存分配方面

:一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式是類似于鏈表。可能用到的關鍵字如下:new、malloc、delete、free等等。

:由編譯器(Compiler)自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。

1.2申請方式方面:

:需要程序員自己申請,并指明大小。在c中malloc函數如p1 = (char *)malloc(10);在C++中用new運算符,但是注意p1、p2本身是在棧中的。因為他們還是可以認為是局部變量。

:由系統自動分配。 例如,聲明在函數中一個局部變量 int b;系統自動在棧中為b開辟空間。

1.3系統響應方面:

:操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣代碼中的delete語句才能正確的釋放本內存空間。另外由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。

:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。

1.4大小限制方面:

:是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。

:在Windows下, 棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是固定的(是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。

1.5效率方面:

:是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便,另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。

:由系統自動分配,速度較快。但程序員是無法控制的。

1.6存放內容方面:

:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。

:在函數調用時第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧,然后是函數中的局部變量。 注意: 靜態變量是不入棧的。當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。

1.7存取效率方面:

:char *s1 = "Hellow Word";是在編譯時就確定的;

:char s1[] = "Hellow Word"; 是在運行時賦值的;用數組比用指針速度要快一些,因為指針在底層匯編中需要用edx寄存器中轉一下,而數組在棧上直接讀取。


小結

1、靜態變量不入棧。 
2、棧由編譯器自動分配和釋放。棧中存放局部變量和參數,函數調用結束后,局部變量先出棧,然后是參數。 
3、數組比用指針速度要快一些,因為指針在底層匯編中需要用edx寄存器中轉一下,而數組在棧上直接讀取。 
4、堆是由程序員通過new、malloc、free、delete等指令進行分配和釋放。如果程序員沒有進行釋放,程序結束時可能有OS回收。 
5、堆是由new分配的內存,速度較慢;棧是由系統自動分配,速度較快。 
6、比如存放在棧里面的數組,是在運行時賦值。而存在堆里面的指針數據,是在編譯時就確定的。

附:

一. 在c中分為這幾個存儲區

1.棧 - 由編譯器自動分配釋放
2.堆 - 一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收
3.全局區(靜態區),全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。- 程序結束釋放
4.另外還有一個專門放常量的地方。- 程序結束釋放
                                                                                                                                              
在函數體中定義的變量通常是在棧上,用malloc, calloc, realloc等分配內存的函數分配得到的就是在堆上。在所有函數體外定義的是全局量,加了static修飾符后不管在哪里都存放在全局區(靜態區),在所有函數體外定義的static變量表示在該文件中有效,不能extern到別的文件用,在函數體內定義的static表示只在該函數體內有效。另外,函數中的"adgfdf"這樣的字符串存放在常量區。比如:

int a = 0//全局初始化區
char *p1; //全局未初始化區
void main()
{
    int b; //
    char s[] = "abc"; //
    char *p2; //
    char *p3 = "123456"; //123456{post.content}在常量區,p3在棧上
    static int c = 0; //全局(靜態)初始化區
     p1 = (char *)malloc(10); //分配得來得10字節的區域在堆區
     p2 = (char *)malloc(20); //分配得來得20字節的區域在堆區
     strcpy(p1, "123456");
    //123456{post.content}放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一塊
}


二.在C++中,內存分成5個區,他們分別是堆、棧、自由存儲區、全局/靜態存儲區和常量存儲區
1.棧,
就是那些由編譯器在需要的時候分配,在不需要的時候自動清楚的變量的存儲區。里面的變量通常是局部變量、函數參數等。
2.堆,就是那些由new分配的內存塊,他們的釋放編譯器不去管,由我們的應用程序去控制,一般一個new就要對應一個delete。如果程序員沒有釋放掉,那么在程序結束后,操作系統會自動回收。
3.自由存儲區,就是那些由malloc等分配的內存塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。
4.全局/靜態存儲區,全局變量和靜態變量被分配到同一塊內存中,在以前的C語言中,全局變量又分為初始化的和未初始化的,在C++里面沒有這個區分了,他們共同占用同一塊內存區。
5.常量存儲區,這是一塊比較特殊的存儲區,他們里面存放的是常量,不允許修改(當然,你要通過非正當手段也可以修改)

三. 談談堆與棧的關系與區別
具體地說,現代計算機(串行執行機制),都直接在代碼底層支持棧的數據結構。這體現在,有專門的寄存器指向棧所在的地址,有專門的機器指令完成數據入棧出棧的操作。這種機制的特點是效率高,支持的數據有限,一般是整數,指針,浮點數等系統直接支持的數據類型,并不直接支持其他的數據結構。因為棧的這種特點,對棧的使用在程序中是非常頻繁的。對子程序的調用就是直接利用棧完成的。機器的call指令里隱含了把返回地址推入棧,然后跳轉至子程序地址的操作,而子程序中的ret指令則隱含從堆棧中彈出返回地址并跳轉之的操作。C/C++中的自動變量是直接利用棧的例子,這也就是為什么當函數返回時,該函數的自動變量自動失效的原因。 

和棧不同,堆的數據結構并不是由系統(無論是機器系統還是操作系統)支持的,而是由函數庫提供的。基本的malloc/realloc/free 函數維護了一套內部的堆數據結構。當程序使用這些函數去獲得新的內存空間時,這套函數首先試圖從內部堆中尋找可用的內存空間,如果沒有可以使用的內存空間,則試圖利用系統調用來動態增加程序數據段的內存大小,新分配得到的空間首先被組織進內部堆中去,然后再以適當的形式返回給調用者。當程序釋放分配的內存空間時,這片內存空間被返回內部堆結構中,可能會被適當的處理(比如和其他空閑空間合并成更大的空閑空間),以更適合下一次內存分配申請。這套復雜的分配機制實際上相當于一個內存分配的緩沖池(Cache),使用這套機制有如下若干原因:
1. 系統調用可能不支持任意大小的內存分配。有些系統的系統調用只支持固定大小及其倍數的內存請求(按頁分配);這樣的話對于大量的小內存分類來說會造成浪費。
2. 系統調用申請內存可能是代價昂貴的。系統調用可能涉及用戶態和核心態的轉換。
3. 沒有管理的內存分配在大量復雜內存的分配釋放操作下很容易造成內存碎片。

堆和棧的對比
從以上知識可知,棧是系統提供的功能,特點是快速高效,缺點是有限制,數據不靈活;而棧是函數庫提供的功能,特點是靈活方便,數據適應面廣泛,但是效率有一定降低。棧是系統數據結構,對于進程/線程是唯一的;堆是函數庫內部數據結構,不一定唯一。不同堆分配的內存無法互相操作。棧空間分靜態分配和動態分配兩種。靜態分配是編譯器完成的,比如自動變量(auto)的分配。動態分配由alloca函數完成。棧的動態分配無需釋放(是自動的),也就沒有釋放函數。為可移植的程序起見,棧的動態分配操作是不被鼓勵的!堆空間的分配總是動態的,雖然程序結束時所有的數據空間都會被釋放回系統,但是精確的申請內存/ 釋放內存匹配是良好程序的基本要素。

    1.碎片問題:對于堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。對于棧來講,則不會存在這個問題,因為棧是先進后出的隊列,他們是如此的一一對應,以至于永遠都不可能有一個內存塊從棧中間彈出,在他彈出之前,在他上面的后進的棧內容已經被彈出,詳細的可以>參考數據結構,這里我們就不再一一討論了。
    2.生長方向:對于堆來講,生長方向是向上的,也就是向著內存地址增加的方向;對于棧來講,它的生長方向是向下的,是向著內存地址減小的方向增長。
    3.分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
    4.分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很復雜的,例如為了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然后進行返回。顯然,堆的效率比棧要低得多。

    明確區分堆與棧:
    在bbs上,堆與棧的區分問題,似乎是一個永恒的話題,由此可見,初學者對此往往是混淆不清的,所以我決定拿他第一個開刀。
    首先,我們舉一個例子:

void f()

    int* p=new int[5];
}

這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們分配了一塊堆內存,那么指針p呢?他分配的是一塊棧內存,所以這句話的意思就是:在棧內存中存放了一個指向一塊堆內存的指針p。在程序會先確定在堆中分配內存的大小,然后調用operator new分配內存,然后返回這塊內存的首地址,放入棧中,他在VC6下的匯編代碼如下:
    00401028    push         14h
    0040102A    call            operator new (00401060)
    0040102F    add          esp,4
    00401032    mov          dword ptr [ebp-8],eax
    00401035    mov          eax,dword ptr [ebp-8]
    00401038    mov          dword ptr [ebp-4],eax
    這里,我們為了簡單并沒有釋放內存,那么該怎么去釋放呢?是delete p么?澳,錯了,應該是delete []p,這是為了告訴編譯器:我刪除的是一個數組,VC6就會根據相應的Cookie信息去進行釋放內存的工作。
    好了,我們回到我們的主題:堆和棧究竟有什么區別?
    主要的區別由以下幾點:
    1、管理方式不同;
    2、空間大小不同;
    3、能否產生碎片不同;
    4、生長方向不同;
    5、分配方式不同;
    6、分配效率不同;
    管理方式:對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產生memory leak。
    空間大小:一般來講在32位系統下,堆內存可以達到4G的空間,從這個角度來看堆內存幾乎是沒有什么限制的。但是對于棧來講,一般都是有一定的空間大小的,例如,在VC6下面,默認的棧空間大小是1M(好像是,記不清楚了)。當然,我們可以修改:
    打開工程,依次操作菜單如下:Project->Setting->Link,在Category 中選中Output,然后在Reserve中設定堆棧的最大值和commit。
注意:reserve最小值為4Byte;commit是保留在虛擬內存的頁文件里面,它設置的較大會使棧開辟較大的值,可能增加內存的開銷和啟動時間。
    堆和棧相比,由于大量new/delete的使用,容易造成大量的內存碎片;由于沒有專門的系統支持,效率很低;由于可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址,EBP和局部變量都采用棧的方式存放。所以,我們推薦大家盡量用棧,而不是用堆。

另外對存取效率的比較:
代碼:

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在運行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快
比如:

void main()
{
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
     a = c[1];
     a = p[1];
    return;
}

對應的匯編代碼
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
第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,在根據edx讀取字符,顯然慢了.
    無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因為越界的結果要么是程序崩潰,要么是摧毀程序的堆、棧結構,產生以想不到的結果,就算是在你的程序運行過程中,沒有發生上面的問題,你還是要小心,說不定什么時候就崩掉,編寫穩定安全的代碼才是最重要的



小馬歌 2013-05-20 16:40 發表評論
]]>
Linux下GDB調試 http://www.fpcwrs.live/xiaomage234/archive/2012/12/12/392852.html小馬歌小馬歌Wed, 12 Dec 2012 04:24:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2012/12/12/392852.htmlhttp://www.fpcwrs.live/xiaomage234/comments/392852.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2012/12/12/392852.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/392852.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/392852.html

Linux下GDB調試



本文寫給主要工作在Windows操作系統下而又需要開發一些跨平臺軟件的程序員朋友,以及程序愛好者。

GDB是一個由GNU開源組織發布的、UNIX/LINUX操作系統下的、基于命令行的、功能強大的程序調試工具。

GDB中的命令固然很多,但我們只需掌握其中十個左右的命令,就大致可以完成日常的基本的程序調試工作。

命令解釋示例
file <文件名>加載被調試的可執行程序文件。
因為一般都在被調試程序所在目錄下執行GDB,因而文本名不需要帶路徑。
(gdb) file gdb-sample
rRun的簡寫,運行被調試的程序。
如果此前沒有下過斷點,則執行完整個程序;如果有斷點,則程序暫停在第一個可用斷點處。
(gdb) r
cContinue的簡寫,繼續執行被調試程序,直至下一個斷點或程序結束。(gdb) c
b <行號>
b <函數名稱>
b *<函數名稱>
b *<代碼地址>

d [編號]

b: Breakpoint的簡寫,設置斷點。兩可以使用“行號”“函數名稱”“執行地址”等方式指定斷點位置。
其中在函數名稱前面加“*”符號表示將斷點設置在“由編譯器生成的prolog代碼處”。如果不了解匯編,可以不予理會此用法。

d: Delete breakpoint的簡寫,刪除指定編號的某個斷點,或刪除所有斷點。斷點編號從1開始遞增。

(gdb) b 8
(gdb) b main
(gdb) b *main
(gdb) b *0x804835c

(gdb) d

s, ns: 執行一行源程序代碼,如果此行代碼中有函數調用,則進入該函數;
n: 執行一行源程序代碼,此行代碼中的函數調用也一并執行。

s 相當于其它調試器中的“Step Into (單步跟蹤進入)”;
n 相當于其它調試器中的“Step Over (單步跟蹤)”。

這兩個命令必須在有源代碼調試信息的情況下才可以使用(GCC編譯時使用“-g”參數)。

(gdb) s
(gdb) n
si, nisi命令類似于s命令,ni命令類似于n命令。所不同的是,這兩個命令(si/ni)所針對的是匯編指令,而s/n針對的是源代碼。(gdb) si
(gdb) ni
p <變量名稱>Print的簡寫,顯示指定變量(臨時變量或全局變量)的值。(gdb) p i
(gdb) p nGlobalVar
display ...

undisplay <編號>

display,設置程序中斷后欲顯示的數據及其格式。
例如,如果希望每次程序中斷后可以看到即將被執行的下一條匯編指令,可以使用命令
“display /i $pc”
其中 $pc 代表當前匯編指令,/i 表示以十六進行顯示。當需要關心匯編代碼時,此命令相當有用。

undispaly,取消先前的display設置,編號從1開始遞增。

(gdb) display /i $pc

(gdb) undisplay 1

iInfo的簡寫,用于顯示各類信息,詳情請查閱“help i”。(gdb) i r
qQuit的簡寫,退出GDB調試環境。(gdb) q
help [命令名稱]GDB幫助命令,提供對GDB名種命令的解釋說明。
如果指定了“命令名稱”參數,則顯示該命令的詳細說明;如果沒有指定參數,則分類顯示所有GDB命令,供用戶進一步瀏覽和查詢。
(gdb) help display

/add************************************/

j命令              回跳

ret               設置返回值        (例ret 0/ret -1)

/add************************************/

廢話不多說,下面開始實踐。gdb-sample.c

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#include <</font>stdio.h>

int nGlobalVar = 0;

int tempFunction(int a, int b)
{
    printf("tempFunction is called, a = %d, b = %d \n", a, b);
    return (a + b);
}

int main()
{
    int n;
        n = 1;
        n++;
        n--;

        nGlobalVar += 100;
        nGlobalVar -= 12;

    printf("n = %d, nGlobalVar = %d \n", n, nGlobalVar);

        n = tempFunction(1, 2);
    printf("n = %d", n);

    return 0;
}

 

gcc gdb-sample.c -o gdb-sample -g

使用參數 -g 表示將源代碼信息編譯到可執行文件中。如果不使用參數 -g,會給后面的GDB調試造成不便。當然,如果我們沒有程序的源代碼,自然也無從使用 -g 參數,調試/跟蹤時也只能是匯編代碼級別的調試/跟蹤。

下面“gdb”命令啟動GDB,將首先顯示GDB說明,不管它:

GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu".
(gdb)

上面最后一行“(gdb) ”為GDB內部命令引導符,等待用戶輸入GDB命令。

下面使用“file”命令載入被調試程序 gdb-sample(這里的 gdb-sample 即前面 GCC 編譯輸出的可執行文件):

(gdb) file gdb-sample
Reading symbols from gdb-sample...done.

上面最后一行提示已經加載成功。

下面使用“r”命令執行(Run)被調試文件,因為尚未設置任何斷點,將直接執行到程序結束:

(gdb) r
Starting program: /home/liigo/temp/test_jmp/test_jmp/gdb-sample
n = 1, nGlobalVar = 88
tempFunction is called, a = 1, b = 2
n = 3
Program exited normally.

下面使用“b”命令在 main 函數開頭設置一個斷點(Breakpoint):

(gdb) b main
Breakpoint 1 at 0x804835c: file gdb-sample.c, line 19.

上面最后一行提示已經成功設置斷點,并給出了該斷點信息:在源文件 gdb-sample.c 第19行處設置斷點;這是本程序的第一個斷點(序號為1);斷點處的代碼地址為 0x804835c(此值可能僅在本次調試過程中有效)。回過頭去看源代碼,第19行中的代碼為“n = 1”,恰好是 main 函數中的第一個可執行語句(前面的“int n;”為變量定義語句,并非可執行語句)。

再次使用“r”命令執行(Run)被調試程序:

(gdb) r
Starting program: /home/liigo/temp/gdb-sample

Breakpoint 1, main () at gdb-sample.c:19
19 n = 1;

程序中斷在gdb-sample.c第19行處,即main函數是第一個可執行語句處。

上面最后一行信息為:下一條將要執行的源代碼為“n = 1;”,它是源代碼文件gdb-sample.c中的第19行。

下面使用“s”命令(Step)執行下一行代碼(即第19行“n = 1;”):

(gdb) s
20 n++;

上面的信息表示已經執行完“n = 1;”,并顯示下一條要執行的代碼為第20行的“n++;”。

既然已經執行了“n = 1;”,即給變量 n 賦值為 1,那我們用“p”命令(Print)看一下變量 n 的值是不是 1 :

(gdb) p n
$1 = 1

果然是 1。($1大致是表示這是第一次使用“p”命令——再次執行“p n”將顯示“$2 = 1”——此信息應該沒有什么用處。)

下面我們分別在第26行、tempFunction 函數開頭各設置一個斷點(分別使用命令“b 26”“b tempFunction”):

(gdb) b 26
Breakpoint 2 at 0x804837b: file gdb-sample.c, line 26.
(gdb) b tempFunction
Breakpoint 3 at 0x804832e: file gdb-sample.c, line 12.

使用“c”命令繼續(Continue)執行被調試程序,程序將中斷在第二 個斷點(26行),此時全局變量 nGlobalVar 的值應該是 88;再一次執行“c”命令,程序將中斷于第三個斷點(12行,tempFunction 函數開頭處),此時tempFunction 函數的兩個參數 a、b 的值應分別是 1 和 2:

(gdb) c
Continuing.

Breakpoint 2, main () at gdb-sample.c:26
26 printf("n = %d, nGlobalVar = %d \n", n, nGlobalVar);
(gdb) p nGlobalVar
$2 = 88
(gdb) c
Continuing.
n = 1, nGlobalVar = 88

Breakpoint 3, tempFunction (a=1, b=2) at gdb-sample.c:12
12 printf("tempFunction is called, a = %d, b = %d \n", a, b);
(gdb) p a
$3 = 1
(gdb) p b
$4 = 2

上面反饋的信息一切都在我們預料之中,哈哈~~~

再一次執行“c”命令(Continue),因為后面再也沒有其它斷點,程序將一直執行到結束:

(gdb) c
Continuing.
tempFunction is called, a = 1, b = 2
n = 3
Program exited normally.

 

有時候需要看到編譯器生成的匯編代碼,以進行匯編級的調試或跟蹤,又該如何操作呢?

這就要用到display命令“display /i $pc”了(此命令前面已有詳細解釋):

(gdb) display /i $pc
(gdb)

此后程序再中斷時,就可以顯示出匯編代碼了:

(gdb) r
Starting program: /home/liigo/temp/test_jmp/test_jmp/gdb-sample

Breakpoint 1, main () at gdb-sample.c:19
19 n = 1;
1: x/i $pc 0x804835c : movl $0x1,0xfffffffc(?p)

看到了匯編代碼,“n = 1;”對應的匯編代碼是“movl $0x1,0xfffffffc(?p)”。

并且以后程序每次中斷都將顯示下一條匯編指定(“si”命令用于執行一條匯編代碼——區別于“s”執行一行C代碼):

(gdb) si
20 n++;
1: x/i $pc 0x8048363 : lea 0xfffffffc(?p),?x
(gdb) si
0x08048366 20 n++;
1: x/i $pc 0x8048366 : incl (?x)
(gdb) si
21 n--;
1: x/i $pc 0x8048368 : lea 0xfffffffc(?p),?x
(gdb) si
0x0804836b 21 n--;
1: x/i $pc 0x804836b : decl (?x)
(gdb) si
23 nGlobalVar += 100;
1: x/i $pc 0x804836d : addl $0x64,0x80494fc

 

接下來我們試一下命令“b *<函數名稱>”。

為了更簡明,有必要先刪除目前所有斷點(使用“d”命令——Delete breakpoint):

(gdb) d
Delete all breakpoints? (y or n) y
(gdb)

當被詢問是否刪除所有斷點時,輸入“y”并按回車鍵即可。

下面使用命令“b *main”在 main 函數的 prolog 代碼處設置斷點(prolog、epilog,分別表示編譯器在每個函數的開頭和結尾自行插入的代碼):

 

(gdb) b *main
Breakpoint 4 at 0x804834c: file gdb-sample.c, line 17.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/liigo/temp/test_jmp/test_jmp/gdb-sample

Breakpoint 4, main () at gdb-sample.c:17
17 {
1: x/i $pc 0x804834c : push ?p
(gdb) si
0x0804834d 17 {
1: x/i $pc 0x804834d : mov %esp,?p
(gdb) si
0x0804834f in main () at gdb-sample.c:17
17 {
1: x/i $pc 0x804834f : sub $0x8,%esp
(gdb) si
0x08048352 17 {
1: x/i $pc 0x8048352 : and $0xfffffff0,%esp
(gdb) si
0x08048355 17 {
1: x/i $pc 0x8048355 : mov $0x0,?x
(gdb) si
0x0804835a 17 {
1: x/i $pc 0x804835a : sub ?x,%esp
(gdb) si
19 n = 1;
1: x/i $pc 0x804835c : movl $0x1,0xfffffffc(?p)

 

此時可以使用“i r”命令顯示寄存器中的當前值———“i r”即“Infomation Register”:

 

(gdb) i r
eax 0xbffff6a4 -1073744220
ecx 0x42015554 1107383636
edx 0x40016bc8 1073834952
ebx 0x42130a14 1108544020
esp 0xbffff6a0 0xbffff6a0
ebp 0xbffff6a8 0xbffff6a8
esi 0x40015360 1073828704
edi 0x80483f0 134513648
eip 0x8048366 0x8048366
eflags 0x386 902
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x33 51

 

當然也可以顯示任意一個指定的寄存器值:

 

(gdb) i r eax
eax 0xbffff6a4 -1073744220

 

最后一個要介紹的命令是“q”,退出(Quit)GDB調試環境:



小馬歌 2012-12-12 12:24 發表評論
]]>
How to Detect Memory Leaks Using Valgrind memcheck Tool for C / C++ http://www.fpcwrs.live/xiaomage234/archive/2012/11/05/390792.html小馬歌小馬歌Mon, 05 Nov 2012 03:22:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2012/11/05/390792.htmlhttp://www.fpcwrs.live/xiaomage234/comments/390792.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2012/11/05/390792.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/390792.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/390792.htmlhttp://www.thegeekstuff.com/2011/11/valgrind-memcheck/


One major aspect of system programming is to handle memory related issues effectively. The more you work close to the system, the more memory related issues you need to face.

Sometimes these issues are very trivial while many times it becomes a nightmare to debug memory related issues. So, as a practice many tools are used for debugging memory related issues.

In this article, we will discuss the most popular open source memory management framework VALGRIND.

From Valgrind.org :

Valgrind is an instrumentation framework for building dynamic analysis tools. It comes with a set of tools each of which performs some kind of debugging, profiling, or similar task that helps you improve your programs. Valgrind’s architecture is modular, so new tools can be created easily and without disturbing the existing structure.

A number of useful tools are supplied as standard.

  1. Memcheck is a memory error detector. It helps you make your programs, particularly those written in C and C++, more correct.
  2. Cachegrind is a cache and branch-prediction profiler. It helps you make your programs run faster.
  3. Callgrind is a call-graph generating cache profiler. It has some overlap with Cachegrind, but also gathers some information that Cachegrind does not.
  4. Helgrind is a thread error detector. It helps you make your multi-threaded programs more correct.
  5. DRD is also a thread error detector. It is similar to Helgrind but uses different analysis techniques and so may find different problems.
  6. Massif is a heap profiler. It helps you make your programs use less memory.
  7. DHAT is a different kind of heap profiler. It helps you understand issues of block lifetimes, block utilisation, and layout inefficiencies.
  8. SGcheck is an experimental tool that can detect overruns of stack and global arrays. Its functionality is complementary to that of Memcheck: SGcheck finds problems that Memcheck can’t, and vice versa..
  9. BBV is an experimental SimPoint basic block vector generator. It is useful to people doing computer architecture research and development.

There are also a couple of minor tools that aren’t useful to most users: Lackey is an example tool that illustrates some instrumentation basics; and Nulgrind is the minimal Valgrind tool that does no analysis or instrumentation, and is only useful for testing purposes.

Here in this article we will focus on the tool ‘memcheck’.

Using Valgrind Memcheck

The memcheck tool is used as follows :

valgrind --tool=memcheck ./a.out

As clear from the command above, the main binary is ‘Valgrind’ and the tool which we want to use is specified by the option ‘–tool’. The ‘a.out’ above signifies the executable over which we want to run memcheck.

This tool can detect the following memory related problems :

  • Use of uninitialized memory
  • Reading/writing memory after it has been freed
  • Reading/writing off the end of malloc’d blocks
  • Memory leaks
  • Mismatched use of malloc/new/new[] vs free/delete/delete[]
  • Doubly freed memory

Note : The above list is not exhaustive but contains the popular problems detected by this tool.

Lets discuss the above scenarios one by one:

Note : All the test code described below should be compiled using gcc with -g option(to generate line numbers in memcheck output) enabled. As we discussed earlier for a C program to get compiled into an executable, it has to go through 4 different stages.

1. Use of uninitialized memory

Code :

#include <stdio.h> #include <stdlib.h>   int main(void) {     char *p;       char c = *p;       printf("\n [%c]\n",c);       return 0; }

In the above code, we try to use an uninitialized pointer ‘p’.

Lets run memcheck and see the result.

$ valgrind --tool=memcheck ./val ==2862== Memcheck, a memory error detector ==2862== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==2862== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info ==2862== Command: ./val ==2862== ==2862== Use of uninitialised value of size 8 ==2862==    at 0x400530: main (valgrind.c:8) ==2862==  [#] ==2862== ==2862== HEAP SUMMARY: ==2862==     in use at exit: 0 bytes in 0 blocks ==2862==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated ==2862== ==2862== All heap blocks were freed -- no leaks are possible ==2862== ==2862== For counts of detected and suppressed errors, rerun with: -v ==2862== Use --track-origins=yes to see where uninitialized values come from ==2862== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

As seen from the output above, Valgrind detects the uninitialized variable and gives a warning(see the lines in bold above).

2. Reading/writing memory after it has been freed

Code :

#include <stdio.h> #include <stdlib.h>   int main(void) {     char *p = malloc(1);     *p = 'a';       char c = *p;       printf("\n [%c]\n",c);       free(p);     c = *p;     return 0; }

In the above piece of code, we have freed a pointer ‘p’ and then again we have tried to access the value help by the pointer.

Lets run memcheck and see what Valgrind has to offer for this scenario.

$ valgrind --tool=memcheck ./val ==2849== Memcheck, a memory error detector ==2849== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==2849== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info ==2849== Command: ./val ==2849==    [a] ==2849== Invalid read of size 1 ==2849==    at 0x400603: main (valgrind.c:30) ==2849==  Address 0x51b0040 is 0 bytes inside a block of size 1 free'd ==2849==    at 0x4C270BD: free (vg_replace_malloc.c:366) ==2849==    by 0x4005FE: main (valgrind.c:29) ==2849== ==2849== ==2849== HEAP SUMMARY: ==2849==     in use at exit: 0 bytes in 0 blocks ==2849==   total heap usage: 1 allocs, 1 frees, 1 bytes allocated ==2849== ==2849== All heap blocks were freed -- no leaks are possible ==2849== ==2849== For counts of detected and suppressed errors, rerun with: -v ==2849== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

As seen in the output above, the tool detects the invalid read and prints the warning ‘Invalid read of size 1′.

On a side note, to debug a c program use gdb.

3. Reading/writing off the end of malloc’d blocks

Code :

#include <stdio.h> #include <stdlib.h>   int main(void) {     char *p = malloc(1);     *p = 'a';       char c = *(p+1);       printf("\n [%c]\n",c);       free(p);     return 0; }

In the above piece of code, we have allocated 1 byte for ‘p’ but we access the the address p+1 while reading the value into ‘c’.

Now we run Valgrind on this piece of code :

$ valgrind --tool=memcheck ./val ==2835== Memcheck, a memory error detector ==2835== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==2835== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info ==2835== Command: ./val ==2835== ==2835== Invalid read of size 1 ==2835==    at 0x4005D9: main (valgrind.c:25) ==2835==  Address 0x51b0041 is 0 bytes after a block of size 1 alloc'd ==2835==    at 0x4C274A8: malloc (vg_replace_malloc.c:236) ==2835==    by 0x4005C5: main (valgrind.c:22) ==2835==    [] ==2835== ==2835== HEAP SUMMARY: ==2835==     in use at exit: 0 bytes in 0 blocks ==2835==   total heap usage: 1 allocs, 1 frees, 1 bytes allocated ==2835== ==2835== All heap blocks were freed -- no leaks are possible ==2835== ==2835== For counts of detected and suppressed errors, rerun with: -v ==2835== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

Again, this tool detects the invalid read done in this case.

4. Memory leaks

Code:

#include <stdio.h> #include <stdlib.h>   int main(void) {     char *p = malloc(1);     *p = 'a';       char c = *p;       printf("\n [%c]\n",c);       return 0; }

In this code, we have malloced one byte but haven’t freed it. Now lets run Valgrind and see what happens :

$ valgrind --tool=memcheck --leak-check=full ./val ==2888== Memcheck, a memory error detector ==2888== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==2888== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info ==2888== Command: ./val ==2888==    [a] ==2888== ==2888== HEAP SUMMARY: ==2888==     in use at exit: 1 bytes in 1 blocks ==2888==   total heap usage: 1 allocs, 0 frees, 1 bytes allocated ==2888== ==2888== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==2888==    at 0x4C274A8: malloc (vg_replace_malloc.c:236) ==2888==    by 0x400575: main (valgrind.c:6) ==2888== ==2888== LEAK SUMMARY: ==2888==    definitely lost: 1 bytes in 1 blocks ==2888==    indirectly lost: 0 bytes in 0 blocks ==2888==      possibly lost: 0 bytes in 0 blocks ==2888==    still reachable: 0 bytes in 0 blocks ==2888==         suppressed: 0 bytes in 0 blocks ==2888== ==2888== For counts of detected and suppressed errors, rerun with: -v ==2888== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

The lines (in bold above) shows that this tool was able to detect the leaked memory.

Note: In this case we added an extra option ‘–leak-check=full’ to get verbose details of the memory leak.

5. Mismatched use of malloc/new/new[] vs free/delete/delete[]

Code:

#include <stdio.h> #include <stdlib.h> #include<iostream>   int main(void) {     char *p = (char*)malloc(1);     *p = 'a';       char c = *p;       printf("\n [%c]\n",c);     delete p;     return 0; }

In the above code, we have used malloc() to allocate memory but used delete operator to delete the memory.

Note : Use g++ to compile the above code as delete operator was introduced in C++ and to compile c++ code, g++ tool is used.

Lets run this tool and see :

$ valgrind --tool=memcheck --leak-check=full ./val ==2972== Memcheck, a memory error detector ==2972== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==2972== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info ==2972== Command: ./val ==2972==    [a] ==2972== Mismatched free() / delete / delete [] ==2972==    at 0x4C26DCF: operator delete(void*) (vg_replace_malloc.c:387) ==2972==    by 0x40080B: main (valgrind.c:13) ==2972==  Address 0x595e040 is 0 bytes inside a block of size 1 alloc'd ==2972==    at 0x4C274A8: malloc (vg_replace_malloc.c:236) ==2972==    by 0x4007D5: main (valgrind.c:7) ==2972== ==2972== ==2972== HEAP SUMMARY: ==2972==     in use at exit: 0 bytes in 0 blocks ==2972==   total heap usage: 1 allocs, 1 frees, 1 bytes allocated ==2972== ==2972== All heap blocks were freed -- no leaks are possible ==2972== ==2972== For counts of detected and suppressed errors, rerun with: -v ==2972== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

We see from the output above (see lines in bold), the tool clearly states ‘Mismatched free() / delete / delete []‘

You can try and use the combination ‘new’ and ‘free’ in a test code and see what result this tool gives.

6. Doubly freed memory

Code :

#include <stdio.h> #include <stdlib.h>   int main(void) {     char *p = (char*)malloc(1);     *p = 'a';       char c = *p;     printf("\n [%c]\n",c);     free(p);     free(p);     return 0; }

In the above peice of code, we have freed the memory pointed by ‘p’ twice. Now, lets run the tool memcheck :

$ valgrind --tool=memcheck --leak-check=full ./val ==3167== Memcheck, a memory error detector ==3167== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==3167== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info ==3167== Command: ./val ==3167==    [a] ==3167== Invalid free() / delete / delete[] ==3167==    at 0x4C270BD: free (vg_replace_malloc.c:366) ==3167==    by 0x40060A: main (valgrind.c:12) ==3167==  Address 0x51b0040 is 0 bytes inside a block of size 1 free'd ==3167==    at 0x4C270BD: free (vg_replace_malloc.c:366) ==3167==    by 0x4005FE: main (valgrind.c:11) ==3167== ==3167== ==3167== HEAP SUMMARY: ==3167==     in use at exit: 0 bytes in 0 blocks ==3167==   total heap usage: 1 allocs, 2 frees, 1 bytes allocated ==3167== ==3167== All heap blocks were freed -- no leaks are possible ==3167== ==3167== For counts of detected and suppressed errors, rerun with: -v ==3167== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

As seen from the output above(lines in bold), the tool detects that we have called free twice on the same pointer.

In this article, we concentrated on memory management framework Valgrind and used the tool memcheck (provided by this framework) to describe how it makes life easy for a developer working close to memory. This tool can detect many memory related problems that are very hard to find manually.



小馬歌 2012-11-05 11:22 發表評論
]]>
Chrome源碼剖析 下《轉》http://www.fpcwrs.live/xiaomage234/archive/2012/02/16/370123.html小馬歌小馬歌Thu, 16 Feb 2012 09:10:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2012/02/16/370123.htmlhttp://www.fpcwrs.live/xiaomage234/comments/370123.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2012/02/16/370123.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/370123.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/370123.html

【四】Chrome的UI繪制

1. Chrome的窗口控件

Chrome提供了自己的一個UI控件庫,相關文檔可以參見這里。用Chrome自己的話來說,我覺得市面上的七葷八素的圖形控件庫都不好用,于是自己倒騰倒騰實現了一套。。。
廣告雖如此說,不過,Chrome的圖形控件結構,我還未發現有啥非常非常特別的地方。Chrome的窗口、按鈕、菜單之類的控件,都直接或間接派生自View,這個是控件基類。Chrome的View具有樹形結構,其內部有一個子View數組,由此構成一個控件常用的組合模式。。。
有一個比較特殊的View子類,叫做RootView,顧名思義,它是整個View控件樹的根,在Chrome中,一個正確的樹形的控件結構,必須由RootView作為根。之所以要這樣設計,是因為RootView有一個比較特殊的功能,那就是分發消息。。。
我們知道,一般的Windows控件,都有一個HWND,用與占據一塊屏幕,捕獲系統消息。Chrome中的View只是保存控件相關信息和繪制控件,里面沒有HWND句柄,因此不能夠捕獲系統消息。在Chrome中,完整的控件架構是這樣的,首先需要有一個ViewContainer,它里面包含一個RootView。ViewContainer是一個抽象類,在Window中的一個子類是HWNDViewContainer,同時,HWNDViewContainer還是MessageLoopForUI::Observer的子類。如果你看過本文第一部分描述的線程通信的內容的話,你就應該還記得,Observer是用于監聽本線程內系統消息的東東。。。
當有系統消息進入此線程消息循環 后,HWNDViewContainer會監聽到這個情況,如果和View相關的消息,它就會調用RootView的相關方法,傳遞給控件。在 RootView的內部,會遍歷整個控件樹上的控件,將消息傳遞給各個控件。當然,有的消息是可以獨占的,比如鼠標移動發送在某個View所管轄的范圍 內,它會告知RootView(通過方法的返回值...),這個消息我要了,那么RootView會停止遍歷。。。
在設計的時候,View對消息的處理,采取的是大而全的接口模式。 就是說在View內部,提供了所有可能的消息處理接口,并提供了默認實現,所有子類只需要覆蓋自己需要的消息處理函數即可。如果對MFC的消息映射有了解 的話,可以知道兩者的區別。MFC在設計的時候,覺得無法提供大而全的接口,因為消息總類實在太多,而且還是可擴展的,于是就有了消息映射著一套繁瑣的 宏。但Chrome的圖形框架,顯然沒有做一個通用的Framework的打算,因此,可以采用這樣的策略,使得子類的派生變得簡單而自然。。。
每一個View的子類控件,比如Button之類的,會存儲一些數據,根據消息做一些行為,并且繪制出自己。在Chrome中,畫圖的東西是ChromeCanvas這個類,在其內部,通過Skia和GDI實現繪制。 Skia是Android團隊開發的一個跨平臺的圖形引擎,在Chrome中負責除了文字之外,所有內容的繪制;而文字繪制的重擔,在Windows中交 到了GDI的手上。這樣的設計會給跨平臺帶來一些困難,估計是由Skia實現文本繪制會比較繁瑣,才會帶出如此一個設計的模式。。。
另外一個歷史遺留產物,就是在Windows下的圖形控件,還有一些是原生的,就是說帶有HWND那種傳統的控件,這是Chrome身上不多的趕工期的痕跡,隨著時間的寬裕,這樣的原生控件會被淘汰進歷史的垃圾箱,而全部變為從View派生的控件。。。
其實,對于Chrome這套控件架構我還沒算摸得很熟悉,估計等到做一次插件之后會了解的更透徹,因此,只說了點皮毛,聊表心意。。。

2. Chrome的頁面加載和繪制

上面這些UI控件,都是用在窗口上的(比如瀏 覽器的外框,菜單,對話框之類的...)。我們在瀏覽器中看到的大部分內容,是網頁頁面。頁面的繪制(繪制,就是把一個HTML文件變成一個活靈活現的頁 面展示的過程...),只有一半輪子是Chrome自己做的,還有一部分來自于WebKit,這個Apple打造的Web渲染器。。。
之所以說是一半輪子來源于WebKit,是因為WebKit本身包含兩部分主要內容,一部分是做Html渲染的,另一部分是做JavaScript解析的。在Chrome中,只有Html的渲染采用了WebKit的代碼,而在JavaScript上,重新搭建了一個NB哄哄的V8引擎。目標是,用WebKit + V8的強強聯手,打造一款上網沖浪的法拉利,從效果來看,還著實做的不錯。。。
不過,雖說Chrome和WebKit都是開 源的,并聯手工作。但是,Chrome還是刻意的和WebKit保持了距離,為其始亂終棄埋下了伏筆。Chrome在WebKit上封裝了一層,稱為 WebKit Glue。Glue層中,大部分類型的結構和接口都和WebKit類似,Chrome中依托WebKit的組件,都只是調用WebKit Glue層的接口,而不是直接調用WebKit中的類型。按照Chrome自己文檔中的話來說,就是,雖然我們再用WebKit實現頁面的渲染,但通過這 個設計(加一個間接層...)已經從某種程度大大降低了與WebKit的耦合,使得可以很容易將WebKit換成某個未來可能出現的更好的渲染引擎。。。

重用
在《夢斷代碼》中,有一坨調侃重用的文字。他覺著軟件重用的困難一方面來自于場景本身很多變,很難設計出一套包羅萬象的東西;另一方面來自于人,程序員總是瞅著別人寫的代碼不順眼,總喜歡自己寫一套。。。
于是,解決重用這個問題也就只有兩種,寫最NB人見人服無所不能的代碼,或者是有很多很多NB代碼共君任選。Google無疑在這兩個方面做得都不 錯,Map/Reduce,Big Table之類的一套東西,強大到可以適合太多的場景,大大簡化了N多上層應用的開發。而對開源的利用使用,使得其可以隨意挑一個巨人站到他肩膀上跳舞,每看到這種場景,MS估計都會氣得拍著胸口吐血。。。
Google本身在服務端的基礎底層,有很深積累,隨著Chrome,Android等等客戶端應用的開發,客戶端的積累也逐步提升,也許,擁抱開源才是MS的正道?。。。

當你鍵入一個Url并敲下回車后,Chrome會在Browser進程中下載Url對應的頁面資源(包括Web頁面和Cookie),而 不是直接將Url發送給Render進程讓它們自行下載(你會越來越發現,Render進程絕對是100%的名符其實,除了繪制,幾乎啥多余的事情都不會 干的...)。與各個Render進程各自為站,各自管好自己所需的資源相比,這種策略仿佛會增加大量的進程間通信。之所以采用,按照這篇文檔的 解釋,主要有三個優點,一個是避免子進程與網絡通信,從而將網絡通信的權限牢牢握在主進程手中,Render進程能力弱了,想造反干壞事的可能性就降低了 (可以更好控制各個Render進程的權限...);另一個是有利于Cookie等持久化資源在不同頁面中的共享,否則在不同Render進程中傳遞 Cookie這樣的事情,做起來更麻煩;還有一點很重要的,是可以控制與網絡建立HTTP連接的數量,以Browser為代表與網絡各方進行通信,各種優 化策略都比較好開展(比如池化)。。。
當然,在Browser進程中進行統一的資源管理,也就意味著不再方便用WebKit進行資源下載(WebKit當然有此能力,不過再次被Chrome拋棄了...),而是依托WinHTTP來做的。WinHTTP在接受數據的過程中,會不停的把數據和相關的消息通過IPC,發送給負責繪制此頁面的Render進程中對應的RenderView。在這里,路由消息中的那個ID值起了關鍵的作用,系統依照此ID,能夠準確的將相關的消息發送到相關的View頭上,這玩意發錯了地方還真不是和有人把錢錯到你賬戶上一樣,因為錯收的進程基本上無福消受這個意外來客,輕者頁面顯示混亂,重者消化不良直接噎死。。。
RenderView接收到頁面信息,會一邊 繪制一邊等待更多的資源到來,在用戶看來,所請求的頁面正在一點一點顯示出來。當然,如果是一個通知傳輸開始、傳輸結束這樣的消息,通過序列化到消息參數 里面,經由IPC發過來,代價還是可以承受的,但是,想資源內容這樣大段大段的字節流,如果通過消息發過來,浪費兩邊進程大量空間和時間,就不合適了。于 是這里用到了共享內存。Browser進程將下載到的資源寫到共享 內存中,并將共享內存的句柄和共享區域的大小序列化在消息中發送給Render進程。Render進程拿到這個句柄,就可以通過它訪問到共享內存相關的區 域,讀取信息并進行繪制。通過這樣的方式,即享用到了統一資源管理的優點,由避免了很高的進程通信開銷,左右逢源,好不快活。。。

3. Chrome頁面的消息響應

Render進程是一個嬌生慣養的進程,這一點從上面一段已經可以看出來了。它自己的資源它自己都不下載,而是由Browser進程來幫忙。不過Render進程也許比你想象的還要懶惰一些,它不但不自己下載資源,甚至,連自己的系統消息都不接收。。。
Render進程中不包含HWND,當你鼠標 在頁面上劃來劃去,點上點下,這些消息其實都發到了Browser進程,它們擁有頁面呈現部分的HWND。Browser會將這些消息轉手通過IPC發送 給對應的Render進程中的RenderView,很多時候WebKit會處理此類消息,當它發現出現了某種值得告訴Browser進程的事情,它會組 個報回贈給Browser進程。舉個例子,你打開一個頁面,然后拿鼠標在頁面上亂晃。Browser這時候就像一個碎嘴大嬸,不厭其煩的告訴Render 進程,“鼠標動了,鼠標動了”。如果Render對這個信息無所謂,就會很無聊的應答著:“哦,哦”(發送一個回包...)。但是,當鼠標劃過鏈接的時 候,矜持的Render進程坐不住了,會大聲告訴Browser進程:“換鼠標,換鼠標~~”,Browser聽到后,會將鼠標從箭頭狀換成手指狀,然后 繼續以上過程。。。
比較麻煩的是Paint消息,重新繪制頁面是 一個太頻繁發生的事情,不可能重繪一次就序列化一坨字節流過去。于是策略也很清楚了,就是依然用共享內存讀寫,用消息發句柄。在Render進程中,會有 一個共享內存池(默認值為2...),以size為key,以共享內存為值,簡單的先入先出淘汰算法,利用局部性的特征,避免反復的創建和銷毀共享內存 (這和資源傳遞不一樣,因為資源傳遞可以開一塊固定大小的共享內存...)。Render進程從共享內存池中拿起一塊(二維字節數組...),就好像拿著 一塊屏幕似的,拼了命往上繪制,為了讓Render安心覺著有成就感,Browser會偷偷幫Render把這些內容繪制到屏幕上,造成Render進程 直接繪制屏幕的假象。這可就苦了屏幕取詞的工具們,因為在HWND上壓根就沒啥字符信息,全部就是一坨圖像而已,啥也取不著。于是Google金山詞霸, 網易有道詞霸各自發揮智慧,另辟蹊徑,也算是都利用Chrome做了一把廣告。。。
為什么不讓Render進程自己擁有HWND,自己管理自己的消息,既快捷又便利。在Chrome的官方Blog上,有一篇解釋的文章, 基本上是這個意思,速度是必須快的發指的,但是為了用戶響應,放棄一些速度是必要的,畢竟,沒有人喜歡總假死的瀏覽器。在Browser進程中,基本上是 杜絕任何同步Render進程的工作,所有操作都是異步完成。因為Render進程是不靠譜的,隨時可能犧牲掉,同步它們往往導致主進程停止響應,從而導 致整個瀏覽器停下來甚至掛掉,這個代價是不可以容忍的。但是,Windows有一個惡習,喜歡往整個HWND繼承體系中發送同步消息(我不是很清楚這個狀 況,有人能解釋么?...),這時候,如果HWND在Render進程中,就務必會導致主進程與Render進程的同步,Chrome無法控制 Windows,于是,它們只能夠控制Render,把它們的HWND搬到主進程中,避免同步操作,換取用戶響應的速度。。。

4. 結論

整個Chrome的UI架構,就是一個權責分 配的問題。可以把Browser進程看成是一個類似于朱元璋般的勤勞皇帝(詳見《明朝那些事 一》...),把大多數的權利都牢牢把握在手中,這樣,雖然Browser很操勞,但是整體上的協調和同步,都進行的非常順暢。Render進程就是皇帝 手下的傀儡宰相們,只負責自己的一畝三分地,聽從皇帝的調配即可。這這樣的環境下,Render進程的生死變得無足輕重,Render的死亡,只是少了一 個繪制頁面的工具而已,其他一切如故。通過控制權力,換取天下太平,這招在coding界,同樣是一個不錯的策略,但是,唯一的意外來自于Plugin。 按照規范,Chrome的Plugin是可以創立窗口的(HWND),這必然導致同步問題,Chrome沒有辦法通過控制權力的方式解決這個問題,只能想 些別的亡羊補牢的招來搞定。。。


【五】 Chrome的插件模型

1. NPAPI

為了緊密的與各個開源瀏覽器團結起來,共同抗擊IE的壟斷,Chrome的插件,也遵循了NPAPI(Netscape Plugin Application Programming Interface)標準,支持這個標準的瀏覽器需要實現一組規定的API供插件調用,這組API形如NPN_XXX,比如NPN_GetURL,插件可以利用這些API進行二次開發。而NPAPI插件以一個Dll之類的作為物理載體(windows下dll,linux下是so...)進行提供,里面同樣也實現了一組規定的API。形式包括NP_XXXNPP_XXX,NP_XXX是系統需要默認調用的方法,用于認知這個插件,比如NP_Initialize, 而NPP_XXX是用于插件完成一些實際功能,比如NPP_New。。。
所有的插件dll都需要放置在指定目錄下(根 據操作系統的不同而不同...),每個插件可以處理一種或多種MIME格式的數據,比如application/pdf,說明該插件可以處理pdf相關的 文檔。在Chrome中鍵入about:plugins,可以查看當前Chrome中具有的插件信息。。。
NPAPI是一個很經典的插件方案,用dll進行注入,用協定的API進行通信,用字符串描述插件能力。 插件宿主(在這里就是瀏覽器...),會根據能力描述,動態加載插件,并負責插件調用的流程和生命周期管理。而插件中,負責真實邏輯的處理,并可以構造 UI與用戶交流。以此類方式實現的插件系統,往往是處理的邏輯比較固定適用范圍一般(用API寫死了邏輯...),但可擴展性不錯(用字符串描述能力,可 無限擴展...)。。。
在Chrome中nphostapi.h中,定義了所有NPAPI相關的函數指針和結構,這個文件放置在glue目錄下,如果看過前面碰過的文章就知道,在WebKit內肯定也有一套相同的東西;在npapi.h/.cc中,提供了Chrome瀏覽器端的NPN_XXX系列函數的實現;每一個插件物理實例,用PluginLib類來表示,而每一個插件的邏輯實例,用PluginInstance類 來表示。這個概念牽強附會的可以用windows中的句柄來類比,當你想操作一個內核對象,你需要獲得一個內核對象的句柄,每個進程中的句柄肯定不相同, 但后面的內核對象卻是同一個,內核對象的生命周期通過句柄的計數來控制,有人用則或,無人用則死(當然這個類比相當的牽強,主要是想說明引用計數和邏輯與 物理的關系,但一個關鍵性的區別在于,PluginLib與PluginInstance都是在一個進程內的,不能跨越進程邊界...)。在Chrome中,PluginLib負責加載和銷毀一個dll,拿到所有導出函數的函數指針,PluginInstance對這些東西進行了封裝,可以更好的來調用。。。
關于NPAPI的更多細節,Chrome并沒有提供任何文檔,但是,各個先驅的瀏覽器們都提供了大量豐富的文檔。比如,你可以到這里,查看firefox中的NPAPI文檔,基本通用。。。

2. Chrome的多進程插件模型

Chrome的插件模型,與早先的瀏覽器的最大不同,是它采用了多進程的 方式,每一個插件,都有一個單獨的進程來承載(Shift + Esc打開Chrome進程管理器,可以看到現在已經加載的插件進程...)。當WebKit進行頁面渲染的時候,發現了未知的MIME類型數據,它會告 知給Browser進程,召喚它提供一個插件來解析。如果該插件還未加載,Browser會在指定目錄中搜尋出具有此實力的插件(如果沒有此類人才只能作 罷...),并為它創建一個進程,讓它負責所有的該插件相關的任務,然后建立起一個IPC通路,與它“保持通話”。這套流程一定不會太陌生,因為它與 Render進程的創建大同小異換湯不換藥。。。
Plugin進程與Render進程最大的區 別在于,Render需要與Browser進程大量通信,因為它的HWND歸Browser老大掌管著,相關所有內容都需要通信完成。但Plugin不需 要與Browser頻繁聯系,它大部分的通信都是與Render進程發生的。如果Plugin與Render之間的通信,還需要走Browser中轉一 下,這就顯得有些脫褲子放屁了,雖然Browser是大頭,但不是冤大頭,它不會干這種吃力不討好的事情。他只是做了一回Render與Plugin間的 媒婆而已。當Plugin與Browser建立好了IPC通路后,它會讓Render建立一個新IPC通路,用以與Plugin通信,IPC的有名管道 名,經由Browser通知給Plugin。完成名字協商后,Render與Plugin的通信關系就建立好了,它們之間就可以直接進行通信了。。。
整個通信模式,可以看這里。這是一個很標準的代理模式的應用,稍有了解的都可以跳過我后面會做的一段羅嗦的描述,一看官方文檔中的圖便能知曉。在Render進程端,WebPluginImplWebPlugin的一個子類,WebPlugin是供Webkit進行調用的一個接口,利用依賴倒置,實現了擴展。在Plugin進程端,實現了一個WebPluginDelegateImpl類, 該類會調用PluginInstance的相關接口實現真實的插件功能。這樣的話,只需要WebPluginImpl調用 WebPluginDelegateImpl中的相應方法,就可以實現功能。但問題是WebPluginImpl與 WebPluginDelegateImpl天各一方各處于一個進程,很顯然,這里需要一個代理模式。這里沿用了COM的架構,Delegate + Stub + Proxy。WebPluginImpl調用代理WebPluginDelegateProxy,該代理會將調用轉換成消息,通過IPC發送給Plugin進程,在Plugin端,通過WebPluginDelegateStub監聽消息,并轉換成對真實WebPluginDelegateImpl的調用,從而完成了跨進程的一個調用,反之亦然。。。

3. Chrome的可擴展性

總所周知,firefox通過三種方式進行自定義,插件、擴展和皮膚。其中,插件是使得瀏覽器能用,不會出現一大塊一大塊的無法顯示的區域;擴展是使得瀏覽器好用,可以簡單方便的進行功能的定制和個性化配置;皮膚是幫助瀏覽器變得好看,畢竟羅卜白菜,給有所愛。。。
與之對比,來看Chrome。Chrome有 了插件,有了皮膚,但是沒有擴展。這就意味著,你很難為Chrome定制一些特色的功能。目前,所有對Chrome的功能擴展,都是通過書簽抑或是修改內 核來實現的。前者能力太弱,后者開發起來太麻煩,容易出錯不提,還必須要與時俱進,跟上版本的變化,并且還不能自由的選擇或關閉。因此,這都不是長遠之 計,Chrome提供一套類似于firefox的擴展機制,也許才是正道。據傳說,Chrome團隊正在琢磨這件事,不知道最終會出來個怎么樣的結果,是 盡力接近firefox降低移植成本,還是另立門戶特立獨行,我想可以拭目以待一把。。。
在多進程模式下,Chrome的插件還有一個 問題,前面提到過,就是關于UI控件的。由于NPAPI的標準,是允許插件創建HWND窗口的,這就使得當Plugin繁忙,且Browser進程發起 HWND的同步的時候,主進程被掛起,這個瀏覽器停滯。在Render進程中,解決這個問題的思路是控制權限,不然Render創建HWND,到了 Plugin中,這招不能使用,只能夠使用另一招,就是監管。不停的檢查Plugin是否太繁忙,無法響應,一旦發現,立即殺死該Plugin及其所處的 頁面。這就好比你想解決奶中有三氯氰胺的問題,要么控制奶源,不從奶站購買全部用自家的,要么加強監管,提高檢查力度防止隱患。兩種策略的優缺點一眼便 知,依照不同環境采取不同策略即可。。。
總體說來,Chrome的可擴展性著實一般,不過Chrome還處于Beta中,我們可以繼續期待。。。


小馬歌 2012-02-16 17:10 發表評論
]]>
Chrome源碼剖析 上《轉》http://www.fpcwrs.live/xiaomage234/archive/2012/02/16/370122.html小馬歌小馬歌Thu, 16 Feb 2012 09:08:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2012/02/16/370122.htmlhttp://www.fpcwrs.live/xiaomage234/comments/370122.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2012/02/16/370122.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/370122.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/370122.html

原著:duguguiyu。
整理:July。
時間:二零一一年四月二日。
出處:http://blog.csdn.net/v_JULY_v
說明:此Chrome源碼剖析很大一部分編輯整理自此博客:http://flyvenus.net/。我對寫原創文章的作者向來是以最大的尊重的。近期想好好研究和學習下Chrome源碼,正巧看到了此duguguiyu兄臺的源碼剖析,處于學習的目的,就不客氣的根據他的博客整理了此文。若有諸多冒犯之處,還望海涵。



--------------------------------


前言:

1、之所以整理此文,有倆個目的:一是為了供自己學習研究之用;二是為了備份,以作日后反復研究。除此之外,無它。
2、此文的形式其實是有點倆不像的,既不是個人首創即原創,又非單純的轉載(有加工),無奈之下,權且稱作翻譯吧。有不妥之處,還望原作者,及讀者見諒。

    文中加入了我自己的一些見解,請自行辨別。順便再說一句,duguguiyu寫的這個Chrome源碼剖析,真不錯,勾起了偶對源碼剖析的莫大興趣。

    順便透露下:在此份Chrome源碼剖析之后,互聯網上即將,首次出現sgi stl v3.3版的源碼剖析拉。作者:本人July。是的,本人最近在研究sgi stl v3.3版的源碼,正在做源碼剖析,個人首創,敬請期待。

    在具體針對源碼剖析之前,再粗略回答一下網友可能關心的問題:chrome速度維護如此之快?據網上資料顯示:有幾個主要的關鍵技術:DNS預解析、 Google自主開發的V8 Javacript引擎、DOM綁定技術以及多進程架構等等。但這不是本文的重點,所以略過不談。

    ok,激動人心的Chrome源碼剖析旅程,即刻開始。


Chrome源碼剖析【序】

此序成于08年末,Chrome剛剛推出之際。

    duguguiyu:“有的人一看到Chrome用到多進程就說垃圾廢物肯定低能。拜托,大家都是搞技術的,你知道多進程的缺點,Google也知道,他 們不是政客,除了搞個噱頭扯個蛋就一無所知了,人家也是有臉有皮的,寫一坨屎一樣的開源代碼放出來遭世人恥笑難道會很開心?所謂技術的優劣,是不能一概而 論的,同樣的技術在不同場合不同環境不同代碼實現下,效果是有所不同的。....”

Chrome對我來說,有吸引力的地方在于(排名分先后…):
  1、它是如何利用多進程(其實也會有多線程一起)做并發的,又是如何解決多進程間的一些問題的,比如進程間通信,進程的開銷;
  2、做為一個后來者,它的擴展能力如何,如何去權衡對原有插件的兼容,提供怎么樣的一個插件模型;
  3、它的整體框架是怎樣,有沒有很NB的架構思想;
  4、它如何實現跨平臺的UI控件系統;
  5、傳說中的V8,為啥那么快。
    但Chrome是一個跨平臺的瀏覽器,其Linux和Mac版本正在開發過程中,所以我把所有的眼光都放在了windows版本中,所有的代碼剖析都是基于windows版本的。有錯誤請指正。


    關于Chrome的源碼下載和環境配置,大家可自行查找資料,強調一點,一定要嚴格按照說明來配置環境,特別是vs2005的補丁和windows SDK的安裝,否則肯定是編譯不過的。

    最后,寫這部分唯一不是廢話的內容,請記住以下這幅圖,這是Chrome最精華的一個縮影:

圖1 Chrome的線程和進程模型


Chrome源碼剖析【一】—— 多線程模型

【一】 Chrome的多線程模型
0. Chrome的并發模型
    如果你仔細看了前面的圖,對Chrome的線程和進程框架應該有了個基本的了解。Chrome有一個主進程,稱為Browser進程,它是老大,管理 Chrome大部分的日常事務;其次,會有很多Renderer進程,它們圈地而治,各管理一組站點的顯示和通信(Chrome在宣傳中一直宣稱一個 tab對應一個進程,其實是很不確切的…),它們彼此互不搭理,只和老大說話,由老大負責權衡各方利益。它們和老大說話的渠道,稱做IPC(Inter- Process Communication),這是Google搭的一套進程間通信的機制,基本的實現后面自會分解。

Chrome的進程模型
Google 在宣傳的時候一直都說,Chrome是one tab one process的模式,其實,這只是為了宣傳起來方便如是說而已,基本等同廣告,實際療效,還要從代碼中來看。實際上,Chrome支持的進程模型遠比宣 傳豐富,簡單的說,Chrome支持以下幾種進程模型:

1.Process-per-site-instance:就是你打開一個網站,然后從這個網站鏈開的一系列網站都屬于一個進程。這是Chrome的默認模式。
2.Process-per-site:同域名范疇的網站放在一個進程,比如www.google.com由于此文形成于08年,所以無法訪問,你懂的)和www.google.com/bookmarks就屬于一個域名內(google有自己的判定機制),不論有沒有互相打開的關系,都算作是一個進程中。用命令行–process-per-site開啟。
3.Process-per-tab:這個簡單,一個tab一個process,不論各個tab的站點有無聯系,就和宣傳的那樣。用–process-per-tab開啟。
4.Single Process:這個很熟悉了吧,即傳統瀏覽器的模式:沒有多進程只有多線程,用–single-process開啟。

關于各種模式的優缺點,官方有官方的說法,大家自己也會有自己的評述。不論如何,至少可以說明,Google不是由于白癡而采取多進程的策略,而是實驗出來的效果。

大家可以用Shift+Esc觀察各模式下進程狀況,至少我是觀察失敗了(每種都和默認的一樣…),原因待跟蹤。 

    不論是Browser進程還是Renderer進程,都不只是光桿司令,它們都有一系列的線程為自己打理各種業務。對于Renderer進程,它們通常有兩個線程:一個是Main thread,它負責與老大進行聯系,有一些幕后黑手的意思;另一個是Render thread,它們負責頁面的渲染和交互,一看就知道是這個幫派的門臉級人物。
    相比之下,Browser進程既 然是老大,小弟自然要多一些,除了大腦般的Main thread,和負責與各Renderer幫派通信的IO thread,其實還包括負責管文件的file thread,負責管數據庫的db thread等等,它們各盡其責,齊心協力為老大打拼。它們和各Renderer進程的之間的關系不一樣,同一個進程內的線程,往往需要很多的協同工作, 這一坨線程間的并發管理,是Chrome最出彩的地方之一了。

閑話并發
單進程單線程的編程是最愜意的事情,所看即所得,一維的思考即可。但程序員的世界總是沒有那么美好,在很多的場合,我們都需要有多線程、多進程、多機器攜起手來一齊上陣共同完成某項任務,統稱:并發(非官方版定義…)。在我看來,需要并發的場合主要是要兩類:

1.為了更好的用戶體驗。有的事情處理起來太慢,比如 數據庫讀寫、遠程通信、復雜計算等等,如果在一個線程一個進程里面來做,往往會影響用戶感受,因此需要另開一個線程或進程轉到后臺進行處理。它之所以能夠 生效,仰仗的是單CPU的分時機制,或者是多CPU協同工作。在單CPU的條件下,兩個任務分成兩撥完成的總時間,是大于兩個任務輪流完成的,但是由于彼 此交錯,給人的感覺更自然一些。

2.為了加速完成某項工作。大名鼎鼎的 Map/Reduce,做的就是這樣的事情,它將一個大的任務,拆分成若干個小的任務,分配個若干個進程去完成,各自收工后,再匯集在一起,更快地得到最 后的結果。為了達到這個目的,只有在多CPU的情形下才有可能,在單CPU的場合(單機單CPU…),是無法實現的。
第二種場合下,我們會自然而然的關注數據的分離,從而很好的利用上多CPU的能力;而在第一種場合,我們習慣了單CPU的模式,往往不注重數據與行為的對應關系,導致在多CPU的場景下,性能不升反降。


1. Chrome的線程模型
    仔細回憶一下我們大部分時候是怎么來用線程的,在我足夠貧瘠的多線程經歷中,往往都是這樣用的:起一個線程,傳入一個特定的入口函數,看一下這個函數是否 是有副作用的(Side Effect),如果有,并且還會涉及到多線程的數據訪問,仔細排查,在可疑地點上鎖伺候。

    Chrome的線程模型走的是另一個路子,即,極力規避鎖的存在。 換更精確的描述方式來說,Chrome的線程模型,將鎖限制了極小的范圍內(僅僅在將Task放入消息隊列的時候才存在…),并且使得上層完全不需要關心 鎖的問題(當然,前提是遵循它的編程模型,將函數用Task封裝并發送到合適的線程去執行…),大大簡化了開發的邏輯。

    不過,從實現來說,Chrome的線程模型并沒有什么神秘的地方,它用到了消息循環的手段。每一個Chrome的線程,入口函數都差不多,都是啟動一個消息循環(參見MessagePump類),等待并執行任務。
    而其中,唯一的差別在于,根據線程處理事務類別的不同,所起的消息循環有所不同。比如處理進程間通信的線程(注意,在Chrome中,這類線程都叫做IO 線程)啟用的是MessagePumpForIO類,處理UI的線程用的是MessagePumpForUI類,一般的線程用到的是 MessagePumpDefault類(只討論windows)。
    不同的消息循環類,主要差異有兩個,一是消息循環中需要處理什么樣的消息和任務,第二個是循環流程(比如是死循環還是阻塞在某信號量上…)。下圖是一個完 整版的Chrome消息循環圖,包含處理Windows的消息,處理各種Task(Task是什么,稍后揭曉,敬請期待),處理各個信號量觀察者 (Watcher),然后阻塞在某個信號量上等待喚醒。

圖2 Chrome的消息循環


    當然,不是每一個消息循環類都需要跑那么一大圈的,有些線程,它不會涉及到那么多的事情和邏輯,白白浪費體力和時間,實在是不可饒恕的。因此,在實際中,不同的MessagePump類,實現是有所不同的,詳見下表:


2. Chrome中的Task
    從上面的表不難看出,不論是哪一種消息循環,必須處理的,就是Task(暫且遺忘掉系統消息的處理和Watcher,以后,我們會緬懷它們的…)。刨去其 它東西的干擾,只留下Task的話,我們可以這樣認為:Chrome中的線程從實現層面來看沒有任何區別,它的區別只存在于職責層面,不同職責的線程,會 處理不同的Task。最后,在鋪天蓋地西紅柿來臨之前,我說一下啥是Task。

    簡單的看,Task就是一個類,一個包含了void Run()抽象方法的類(參見Task類…)。一個真實的任務,可以派生Task類,并實現其Run方法。每個MessagePump類中,會有一個 MessagePump::Delegate的類的對象(MessagePump::Delegate的一個實現,請參見MessageLoop類…), 在這個對象中,會維護若干個Task的隊列。當你期望,你的一個邏輯在某個線程內執行的時候,你可以派生一個Task,把你的邏輯封裝在Run方法中,然 后實例一個對象,調用期望線程中的PostTask方法,將該Task對象放入到其Task隊列中去,等待執行。我知道很多人已經抄起了板磚,因為這種手 法實在是太常見了,就不是一個簡單的依賴倒置,在線程池,Undo\Redo等模塊的實現中,用的太多了。

    但,我想說的是,雖說誰家過年都是吃頓餃子,這餃子好不好吃還是得看手藝,不能一概而論。在Chrome中,線程模型是統一且唯一的,這就相當于有了一套 標準,它需要滿足在各個線程上執行的幾十上百種任務的需求,因此,必須在靈活行和易用性上有良好的表現,這就是設計標準的難度。為了滿足這些需 求,Chrome在底層庫上做了足夠的功夫:
  1.它提供了一大套的模板封裝(參見task.h),可以將Task擺脫繼承結構、函數名、函數參數等限制(就是基于模板的偽function實現,想要更深入了解,建議直接看鼻祖《Modern C++》和它的Loki庫…);
  2.同時派生出CancelableTask、ReleaseTask、DeleteTask等子類,提供更為良好的默認實現;
  3.在消息循環中,按邏輯的不同,將Task又分成即時處理的Task、延時處理的Task、Idle時處理的Task,滿足不同場景的需求;
  4.Task派生自tracked_objects::Tracked,Tracked是為了實現多線程環境下的日志記錄、統計等功能,使得Task天生就有良好的可調試性和可統計性;
這一套七葷八素的都搭建完,這才算是一個完整的Task模型,由此可知,這餃子,做的還是很費功夫的。


3. Chrome的多線程模型
    工欲善其事,必先利其器。Chrome之所以費了老鼻子勁去磨底層框架這把刀,就是為了面對多線程這坨怪獸的時候殺的更順暢一些。在Chrome的多線程 模型下,加鎖這個事情只發生在將Task放入某線程的任務隊列中,其他對任何數據的操作都不需要加鎖。當然,天下沒有免費的午餐,為了合理傳遞Task, 你需要了解每一個數據對象所管轄的線程,不過這個事情,與紛繁的加鎖相比,真是小兒科了不知道多少倍。

圖3 Task的執行模型


    如果你熟悉設計模式,你會發現這是一個Command模式,將創建于執行的環境相分離,在一個線程中創建行為,在另一個線程中執行行為。Command模 式的優點在于,將實現操作與構造操作解耦,這就避免了鎖的問題,使得多線程與單線程編程模型統一起來,其次,Command還有一個優點,就是有利于命令 的組合和擴展,在Chrome中,它有效統一了同步和異步處理的邏輯。

Command模式
Command 模式,是一種看上去很酷的模式,傳統的面向對象編程,我們封裝的往往都是數據,在Command模式下,我們希望封裝的是行為。這件事在函數式編程中很正 常,封裝一個函數作為參數,傳來傳去,稀疏平常的事兒;但在面向對象的編程中,我們需要通過繼承、模板、函數指針等手法,才能將其實現。

應用Command模式,我們是期望這個行為能到一個不同于它出生的環境中去執行,簡而言 之,這是一種想生不想養的行為。我們做Undo/Redo的時候,會把在任一一個環境中創建的Command,放到一個隊列環境中去,供統一的調度;在 Chrome中,也是如此,我們在一個線程環境中創建了Task,卻把它放到別的線程中去執行,這種寄居蟹似的生活方式,在很多場合都是有用武之地的。

    在一般的多線程模型中,我們需要分清楚啥是同步啥是異步,在同步模式下,一切看上去和單線程沒啥區別,但同時也喪失了多線程的優勢(淪落成為多線程串 行…)。而如果采用異步的模式,那寫起來就麻煩多了,你需要注冊回調,小心管理對象的生命周期,程序寫出來是嗷嗷惡心。在Chrome的多線程模型下,同 步和異步的編程模型區別就不復存在了,如果是這樣一個場景:A線程需要B線程做一些事情,然后回到A線程繼續做一些事情;在Chrome下你可以這樣來 做:生成一個Task,放到B線程的隊列中,在該Task的Run方法最后,會生成另一個Task,這個Task會放回到A的線程隊列,由A來執行。如此 一來,同步異步,天下一統,都是Task傳來傳去,想不會,都難了。

圖4 Chrome的一種異步執行的解決方案


4. Chrome多線程模型的優缺點
    一直在說Chrome在規避鎖的問題,那到底鎖是哪里不好,犯了何等滔天罪責,落得如此人見人嫌恨不得先殺而后快的境地。《代碼之美》的第二十四章“美麗 的并發”中,Haskell設計人之一的Simon Peyton Jones總結了一下用鎖的困難之處,如下:

1.鎖少加了,導致兩個線程同時修改一個變量;
2.鎖多加了,輕則妨礙并發,重則導致死鎖;
3.鎖加錯了,由于鎖和需要鎖的數據之間的聯系,只存在于程序員的大腦中,這種事情太容易發生了;
4.加鎖的順序錯了,維護鎖的順序是一件困難而又容易出錯的問題;
5.錯誤恢復;
6.忘記喚醒和錯誤的重試;
7. 而最根本的缺陷,是鎖和條件變量不支持模塊化的編程。比如一個轉賬業務中,A賬戶扣了100元錢,B賬戶增加了100元,即使這兩個動作單獨用鎖保護維持 其正確性,你也不能將兩個操作簡單的串在一起完成一個轉賬操作,你必須讓它們的鎖都暴露出來,重新設計一番。好好的兩個函數,愣是不能組在一起用,這就是 鎖的最大悲哀;

    通過這些缺點的描述,也就可以明白Chrome多線程模型的優點。它解決了鎖的最根本缺陷,即,支持模塊化的編程,你只需要維護對象和線程之間的職能關系即可,這個攤子,比之鎖的那個爛攤子,要簡化了太多。對于程序員來說,負擔一瞬間從泰山降成了鴻毛。

    而Chrome多線程模型的一個主要難點,在于線程與數據關系的設計上,你需要良好的劃分各個線程的職責,如果有一個線程所管轄的數據,幾乎占據了大半部分的Task,那么它就會從多線程淪為單線程,Task隊列的鎖也將成為一個大大的瓶頸。

設計者的職責
一 個底層結構設計是否成功,這個設計者是否稱職,我一直覺得是有一個很簡單的衡量標準的。你不需要看這個設計人用了多少NB的技術,你只需要關心,他的設 計,是否給其他開發人員帶來了困難。一個NB的設計,是將所有困難都集中在底層搞定,把其他開發人員換成白癡都可以工作的那種;一個SB的設計,是自己弄 了半天,只是為了給其他開發人員一個長達250條的注意事項,然后很NB的說,你們按照這個手冊去開發,就不會有問題了。

    從根本上來說,Chrome的線程模型解決的是并發中的用戶體驗問題而不是聯合工作的問題(參見我前面噴的“閑話并發”),它不是和Map/Reduce 那樣將關注點放在數據和執行步驟的拆分上,而是放在線程和數據的對應關系上,這是和瀏覽器的工作環境相匹配的。設計總是和所處的環境相互依賴的,畢竟,在 客戶端,不會和服務器一樣,存在超規模的并發處理任務,而只是需要盡可能的改善用戶體驗,從這個角度來說,Chrome的多線程模型,至少看上去很美。

 

Chrome源碼剖析【二】—— 進程通信

【二】Chrome的進程間通信
1. Chrome進程通信的基本模式
    進程間通信,叫做IPC(Inter-Process Communication)。Chrome最主要有三類進程,一類是Browser主進程,我們一直尊稱它老人家為老大;還有一類是各個Render進 程,前面也提過了;另外還有一類一直沒說過,是Plugin進程,每一個插件,在Chrome中都是以進程的形式呈現,等到后面說插件的時候再提罷了。 Render進程和Plugin進程都與老大保持進程間的通信,Render進程與Plugin進程之間也有彼此聯系的通路,唯獨是多個Render進程 或多個Plugin進程直接,沒有互相聯系的途徑,全靠老大協調。

    進程與進程間通信,需要仰仗操作系統的特性,能玩的花著實不多,在Chrome中,用到的就是有名的管道(Named Pipe),只不過,它用一個IPC::Channel類,封裝了具體的實現細節。Channel可以有兩種工作模式,一種是Client,一種是 Server,Server和Client分屬兩個進程,維系一個共同的管道名,Server負責創建該管道,Client會嘗試連接該管道,然后雙發往 各自管道緩沖區中讀寫數據(在Chrome中,用的是二進制流,異步IO…),完成通信。

管道名字的協商
在 Socket中,我們會事先約定好通信的端口,如果不按照這個端口進行訪問,走錯了門,會被直接亂棍打出門去的。與之類似,有名管道期望在兩個進程間游 走,就需要拿一個兩個進程都能接受的進門暗號,這個就是有名管道的名字。在Chrome中(windows下…),有名管道的名字格式都是:\\.\pipe\chrome.ID。其中的ID,自然是要求獨一無二,比如:進程ID.實例地址.隨機數。通常,這個ID是由一個Process生成(往往是Browser Process),然后在創建另一個進程的時候,作為命令行參數傳進去,從而完成名字的協商。

如果不了解并期待了解有關Windows下有名管道和信號量的知識,建議去看一些專業的書 籍,比如圣經級別的《Windows核心編程》和《深入解析Windows操作系統》,當然也可以去查看SDK,你需要了解的API可能包 括:CreateNamedPipe, CreateFile, ConnectNamedPipe, WaitForMultipleObjects, WaitForSingleObject, SetEvent, 等等。

    Channel中,有三個比較關鍵的角色,一個是Message::Sender,一個是Channel::Listener,最后一個是 MessageLoopForIO::Watcher。Channel本身派生自Sender和Watcher,身兼兩角,而Listener是一個抽象 類,具體由Channel的使用者來實現。顧名思義,Sender就是發送消息的接口,Listener就是處理接收到消息的具體實現,但這個 Watcher是啥?如果你覺得Watcher這東西看上去很眼熟的話,我會激動的熱淚盈眶的,沒錯,在前面(第一部分第一小節…)說消息循環的時候,從 那個表中可以看到,IO線程(記住,在Chrome中,IO指的是網絡IO,*_*)的循環會處理注冊了的Watcher。其實Watcher很簡單,可 以視為一個信號量和一個帶有OnObjectSignaled方法對象的對,當消息循環檢測到信號量開啟,它就會調用相應的 OnObjectSignaled方法。

圖5 Chrome的IPC處理流程圖

    一圖解千語,如上圖所示,整個Chrome最核心的IPC流程都在圖上了,期間,刨去了一些錯誤處理等邏輯,如果想看原汁原味的,可以自查Channel 類的實現。當有消息被Send到一個發送進程的Channel的時候,Channel會把它放在發送消息隊列中,如果此時還正在發送以前的消息(發送端被 阻塞…),則看一下阻塞是否解除(用一個等待0秒的信號量等待函數…),然后將消息隊列中的內容序列化并寫道管道中去。操作系統會維護異步模式下管道的這 一組信號量,當消息從發送進程緩沖區寫到接收進程的緩沖區后,會激活接收端的信號量。當接收進程的消息循環,循到了檢查Watcher這一步,并發現有信 號量激活了,就會調用該Watcher相應的OnObjectSignaled方法,通知接受進程的Channel,有消息來了!Channel會嘗試從 管道中收字節,組消息,并調用Listener來解析該消息。

    從上面的描述不難看出,Chrome的進程通信,最核心的特點,就是利用消息循環來檢查信號量,而不是直接讓管道阻塞在某信號量上。這樣就與其多線程模型 緊密聯系在了一起,用一種統一的模式來解決問題。并且,由于是消息循環統一檢查,線程不會隨便就被阻塞了,可以更好的處理各種其他工作,從理論上講,這是 通過增加CPU工作時間,來換取更好的體驗,頗有資本家的派頭。

溫柔的消息循環
其實,Chrome的很多消息循環,也不是都那么霸道,也是會被阻塞在某些信號量或者某種場景上的,畢竟客戶端不是它家的服務器,CPU不能被全部歸在它家名下。

比如IO線程,當沒有消息來到,又沒有信號量被激活的時候,就會被阻塞,具體實現可以去看MessagePumpForIO的WaitForWork方法。

不過這種阻塞是集中式的,可隨時修改策略的,比起Channel直接阻塞在信號量上,停工的時間更短。


2. 進程間的跨線程通信和同步通信
    在Chrome中,任何底層的數據都是線程非安全的,Channel不是太上老君(抑或中國足球?…),它也沒有例外。在每一個進程中,只能有一個線程來 負責操作Channel,這個線程叫做IO線程(名不符實真是一件悲涼的事情…)。其它線程要是企圖越俎代庖,是會出大亂子的。

    但是有時候(其實是大部分時候…),我們需要從非IO線程與別的進程相通信,這該如何是好?如果,你有看過我前面寫的線程模型,你一定可以想到,做法很簡 單,先將對Channel的操作放到Task中,將此Task放到IO線程隊列里,讓IO線程來處理即可。當然,由于這種事情發生的太頻繁,每次都人肉做 一次頗為繁瑣,于是有一個代理類,叫做ChannelProxy,來幫助你完成這一切。

    從接口上看,ChannelProxy的接口和Channel沒有大的區別(否則就不叫Proxy了…),你可以像用Channel一樣,用 ChannelProxy來Send你的消息,ChannelProxy會辛勤的幫你完成剩余的封裝Task等工作。不僅如此,ChannelProxy 還青出于藍勝于藍,在這個層面上做了更多的事情,比如:發送同步消息。

    不過能發送同步消息的類不是ChannelProxy,而是它的子類,SyncChannel。在Channel那里,所有的消息都是異步的(在 Windows中,也叫Overlapped…),其本身也不支持同步邏輯。為了實現同步,SyncChannel并沒有另造輪子,而只是在 Channel的層面上加了一個等待操作。當ChannelProxy的Send操作返回后,SyncChannel會把自己阻塞在一組信號量上,等待回 包,直到永遠或超時。從外表上看同步和異步沒有什么區別,但在使用上還是要小心,在UI線程中使用同步消息,是容易被發指的。


3. Chrome中的IPC消息格式
    說了半天,還有一個大頭沒有提過,那就是消息包。如果說,多線程模式下,對數據的訪問開銷來自于鎖,那么在多進程模式下,大部分的額外開銷都來自于進程間 的消息拆裝和傳遞。不論怎么樣的模式,只要進程不同,消息的打包,序列化,反序列化,組包,都是不可避免的工作。

    在Chrome中,IPC之間的通信消息,都是派生自IPC::Message類的。對于消息而言,序列化和反序列化是必須要支持的,Message的基 類Pickle,就是干這個活的。Pickle提供了一組的接口,可以接受int,char,等等各種數據的輸入,但是在Pickle內部,所有的一切都 沒有區別,都轉化成了一坨二進制流。這個二進制流是32位齊位的,比如你只傳了一個bool,也是最少占32位的,同時,Pickle的流是有自增邏輯的 (就是說它會先開一個Buffer,如果滿了的話,會加倍這個Buffer…),使其可以無限擴展。Pickle本身不維護任何二進制流邏輯上的信息,這 個任務交到了上級處理(后面會有說到…),但Pickle會為二進制流添加一個頭信息,這個里面會存放流的長度,Message在繼承Pickle的時 候,擴展了這個頭的定義,完整的消息格式如下:


                                          圖6 Chrome的IPC消息格式

    其中,黃色部分是包頭,定長96個bit,綠色部分是包體,二進制流,由payload_size指明長度。從大小上看這個包是很精簡的了,除了 routing位在消息不為路由消息的時候會有所浪費。消息本身在有名管道中是按照二進制流進行傳輸的(有名管道可以傳輸兩種類型的字符流,分別是二進制 流和消息流…),因此由payload_size + 96bits,就可以確定是否收了一個完整的包。

    從邏輯上來看,IPC消息分成兩類,一類是路由消息(routed message),還有一類是控制消息(control message)。路由消息是私密的有目的地的,系統會依照路由信息將消息安全的傳遞到目的地,不容它人窺視;控制消息就是一個廣播消息,誰想聽等能夠聽 得到。

消息的序列化
前不久讀了Google Protocol Buffers的源碼,是用在服務器端,用做內部機器通信協議的標準、代碼生成工具和框架。它主要的思想是揉合了key/value的內容到二進制中,幫助生成更為靈活可靠的二進制協議。

在Chrome中,沒有使用這套東西,而是用到了純二進制流作為消息序列化的方式。我想這 是由于應用場景不同使然。在服務端,我們更關心協議的穩定性,可擴展性,并且,涉及到的協議種類很多。但在一個Chrome中,消息的格式很統一,這方面 沒有擴展性和靈活性的需求,而在序列化上,雖然key/value的方式很好很強大,但是在Chrome中需要的不是靈活性而是精簡性,因此寧可不用 Protocol Buffers造好的輪子,而是另立爐灶,花了好一把力氣提供了一套純二進制的消息機制。
 

4. 定義IPC消息
    如果你寫過MFC程序,對MFC那里面一大堆宏有所忌憚的話,那么很不幸,在Chrome中的IPC消息定義中,你需要再吃一點苦頭了,甚至,更苦大仇深 一些;如果你曾經領教過用模板的特化偏特化做Traits、用模板做函數重載、用編譯期的Tuple做變參數支持,之類機制的種種麻煩的話,那么,同樣很 遺憾,在Chrome中,你需要再感受一次。。。

    不過,先讓我們忘記宏和模板,看人肉一個消息,到底需要哪些操作。一個標準的IPC消息定義應該是類似于這樣的:

class SomeMessage: public IPC::Message
{
  public:
    enum { ID = …; }
    SomeMessage(SomeType & data)
    : IPC::Message(MSG_ROUTING_CONTROL, ID, ToString(data))
    {…}
    …
};

    大概意思是這樣的,你需要從Message(或者其他子類)派生出一個子類,該子類有一個獨一無二的ID值,該子類接受一個參數,你需要對這個參數進行序列化。兩個麻煩的地方看的很清楚,如果生成獨一無二的ID值?如何更方便的對任何參數可以自動的序列化?。

    在Chrome中,解決這兩個問題的答案,就是宏 + 模板。Chrome為每個消息安排了一種ID規格,用一個16bits的值來表示,高4位標識一個Channel,低12位標識一個消息的子id,也就是 說,最多可以有16種Channel存在不同的進程之間,每一種Channel上可以定義4k的消息。目前,Chrome已經用掉了8種 Channel(如果A、B進程需要雙向通信,在Chrome中,這是兩種不同的Channel,需要定義不同的消息,也就是說,一種雙向的進程通信關 系,需要耗費兩個Channel種類…),他們已經覺得,16bits的ID格式不夠用了,在將來的某一天,估計就被擴展成了32bits的。書歸正 傳,Chrome是這么來定義消息ID的,用一個枚舉類,讓它從高到低往下走,就像這樣:

enum SomeChannel_MsgType
{
  SomeChannelStart = 5 << 12,
  SomeChannelPreStart = (5 << 12) – 1,
  Msg1,
  Msg2,
  Msg3,
  …
  MsgN,
  SomeChannelEnd
};


    這是一個類型為5的Channel的消息ID聲明,由于指明了最開始的兩個值,所以后續枚舉的值會依次遞減,如此,只要維護Channel類型的唯一性, 就可以維護所有消息ID的唯一性了(當然,前提是不能超過消息上限…)。但是,定義一個ID還不夠,你還需要定義一個使用該消息ID的Message子 類。這個步驟不但繁瑣,最重要的,是違反了DIY原則,為了添加一個消息,你需要在兩個地方開工干活,是可忍孰不可忍,于是Google祭出了宏這顆原子 彈,需要定義消息,格式如下:

IPC_BEGIN_MESSAGES(PluginProcess, 3)
IPC_MESSAGE_CONTROL2(PluginProcessMsg_CreateChannel,
int /* process_id */,
HANDLE /* renderer handle */)
IPC_MESSAGE_CONTROL1(PluginProcessMsg_ShutdownResponse,
bool /* ok to shutdown */)
IPC_MESSAGE_CONTROL1(PluginProcessMsg_PluginMessage,
std::vector<uint8> /* opaque data */)
IPC_MESSAGE_CONTROL0(PluginProcessMsg_BrowserShutdown)
IPC_END_MESSAGES(PluginProcess)

    這是Chrome中,定義PluginProcess消息的宏,我挖過來放在這了,如果你想添加一條消息,只需要添加一條類似與 IPC_MESSAGE_CONTROL0東東即可,這說明它是一個控制消息,參數為0個。你基本上可以這樣理解,IPC_BEGIN_MESSAGES 就相當于完成了一個枚舉開始的聲明,然后中間的每一條,都會在枚舉里面增加一個ID,并聲明一個子類。這個一宏兩吃,直逼北京烤鴨兩吃的高超做法,可以參 看ipc_message_macros.h,或者看下面一宏兩吃的一個舉例。

多次展開宏的技巧
這是Chrome中用到的一個技巧,定義一次宏,展開多段代碼,我孤陋寡聞,第一次見,一個類似的例子,如下:

首先,定義一個macro.h,里面放置宏的定義:
#undef SUPER_MACRO
#if defined(FIRST_TIME)
#undef FIRST_TIME
#define SUPER_MACRO(label, type) \

enum IDs { \
  label##__ID = 10 \
};

#elif defined(SECOND_TIME)
#undef SECOND_TIME

#define SUPER_MACRO(label, type) \
class TestClass \
{ \
  public: \
    enum {ID = label##__ID}; \
    TestClass(type value) : _value(value) {} \
    type _value; \
};
#endif

可以看到,這個頭文件是可重入的,每一次先undef掉之前的定義,然后判斷進行新的定義。然后,你可以創建一個use_macro.h文件,利用這個宏,定義具體內容:

#include “macros.h”
SUPER_MACRO(Test, int)

這個頭文件在利用宏的部分不需要放到ifundef…define…這樣的頭文件保護中,目的就是為了可重入。在主函數中,你可以多次define + include,實現多次展開的目的:

#define FIRST_TIME
#include “use_macro.h”
#define SECOND_TIME
#include “use_macro.h”
#include <iostream>
int _tmain(int argc, _TCHAR* argv[])
{
  TestClass t(5);
  std::cout << TestClass::ID << std::endl;
  std::cout << t._value << std::endl;
  return 0;
}

這樣,你就成功的實現,一次定義,生成多段代碼了。

此外,當接收到消息后,你還需要處理消息。接收消息的函數,是 IPC::Channel::Listener子類的OnMessageReceived函數。在這個函數中,會放置一坨的宏,這一套宏,一定能讓你想起 MFC的Message Map機制(關于此消息機制原理更具體的介紹,可參考侯捷的深入淺出MFC一書。):

IPC_BEGIN_MESSAGE_MAP_EX(RenderProcessHost, msg, msg_is_ok)

IPC_MESSAGE_HANDLER(ViewHostMsg_PageContents, OnPageContents)
IPC_MESSAGE_HANDLER(ViewHostMsg_UpdatedCacheStats,
OnUpdatedCacheStats)
IPC_MESSAGE_UNHANDLED_ERROR()
IPC_END_MESSAGE_MAP_EX()

 

    這個東西很簡單,展開后基本可以視為一個Switch循環,判斷消息ID,然后將消息,傳遞給對應的函數。與MFC的Message Map比起來,做的事情少多了。

    通過宏的手段,可以解決消息類聲明和消息的分發問題,但是自動的序列化還不能支持(所謂自動的序列化,就是不論你是什么類型的參數,幾個參數,都可以直接 序列化,不需要另寫代碼…)。在C++這種語言中,所謂自動的序列化,自動的類型識別,自動的XXX,往往都是通過模板來實現的。這些所謂的自動化,其實 就是通過事前的大量人肉勞作,和模板自動遞推來實現的,如果說.Net或Java中的自動序列化是過山軌道,這就是那挑夫的驕子,雖然最后都是兩腿不動到 了山頂,這底下費得力氣真是天壤之別啊。具體實現技巧,有興趣的看看侯捷的《STL源碼剖析》,或者是《C++新思維》,或者Chrome中的 ipc_message_utils.h,這要說清楚實在不是一兩句的事情。

    總之通過宏和模板,你可以很簡單的聲明一個消息,這個消息可以傳入各式各樣的參數(這里用到了夸張的修辭手法,其實,只要是模板實現的自動化,永遠都是有 限制的,在Chrome的模板實現中,參數數量不要超過5個,類型需要是基本類型、STL容器等,在不BT的場合,應該夠用了…),你可以調用 Channel、ChannelProxy、SyncChannel之類的Send方法,將消息發送給其他進程,并且,實現一個Listener類,用 Message Map來分發消息給對應的處理函數。如此,整個IPC體系搭建完成。

苦力的宏和模板
不論是宏還是模板,為了實現這套機制,都需要寫大量的類似代碼,比如為了支持0~N個參數的Control消息,你就需要寫N+1個類似的宏;為了支持各種基礎數據結構的序列化,你就需要寫上十來個類似的Write函數和Traits。

之所以做如此苦力的活,都是為了用這些東西的人能夠盡可能的簡單方便,符合DIY原則。規 約到之前說的設計者的職責上來,這是一個典型的苦了我一個幸福千萬人的負責任的行為。在Chrome中,如此的代碼隨處可見,光Tuple那一套拳法,我 現在就看到了使了不下三次(我曾經做過一套,直接吐血…),如此兢兢業業,真是可歌可泣啊。
 

【三】 Chrome的進程模型
1. 基本的進程結構
    Chrome是一個多進程的架構,不過所有的進程都會由老大,Browser進程來管理,走的是集中化管理的路子。在Browser進程中,有 xxxProcessHost,每一個host,都對應著一個Process,比如RenderProcessHost對應著 RenderProcess,PluginProcessHost對應著PluginProcess,有多少個host的實例,就有多少個進程在運行。

    這是一個比較典型的代理模式,Browser對Host的操作,都會被Host封裝成IPC消息,傳遞給對應的Process來處理,對于大部分上層的類,也就隔離了多進程細節。


2. Render進程
    先不扯Plugin的進程,只考慮Render進程。前面說了,一個Process一個tab,只是廣告用語,實際上,每一個web頁面內容(包括在 tab中的和在彈出窗口中的…),在Chrome中,用RenderView表示一個web頁面,每一個RenderView可以寄宿在任一一個 RenderProcess中,它只是依托RenderProcess幫助它進行通信。每一個RenderProcess進程都可以有1到N個 RenderView實例。

    Chrome支持不同的進程模型,可以一個tab一個進程,一個site instance一個進程等等。但基本模式都是一致的,當需要創建一個新的RenderView的時候,Chrome會嘗試進行選擇或者是創建進程。比 如,在one site one process的模式下,如果存在此site,就會選擇一個已有的RenderProcessHost,讓它管理這個新的RenderView,否則,會 創建一個RenderProcessHost(同時也就創建了一個Process),把RenderView交給它。

    在默認的one site instance one process的模式中,Chrome會為每個新的site instance創建一個進程(從一個頁面鏈開來的頁面,屬于同一個site instance),但,Render進程總數是有個上限的。這個上限,根據內存大小的不同而異,比如,在我的機器上(2G內存),最多可以容納20個 Render進程,當達到這個上限后,你再開新的網站,Chrome會隨機為你選擇一個已有的進程,把這個網站對應的RenderView給扔進去。。。

    每一次你新輸入一個站點信息,在默認模式下,都必然導致一個進程的誕生,很可能,伴隨著另一個進程的死亡(如果這個進程沒有其他承載的 RenderView的話,他就自然死亡了,RenderView的個數,就相當于這個進程的引用計數…)。比如,你打開一個新標簽頁的時候,系統為你創 造了一個進程來承載這個新標簽頁,你輸入http://www.baidu.com/,于是新標簽頁進程死亡,承載http://www.baidu.com/的進程誕生。你用baidu搜索了一下,毫無疑問,你基本對它的搜索結果很失望,于是你重新輸入http://www.google.com.hk/, 老的承載baidu的進程死亡,承載google的進程被構建出來。這時候你想回退到之前baidu的搜索結果,樂呵樂呵的話,一個新的承載baidu的 進程被創造,之前Google的進程死亡。同樣,你再次點擊前進,又來到Google搜索結果的時候,一個新的進程有取代老的進程出現了。

    以上現象,你都可以自己來檢驗,通過觀察about:memory頁面的信息,你可以了解整個過程(記得每做一步,需要刷新一下about:memory 頁面)。我唧唧歪歪說了半天,其實想表達的是,Chrome并沒有像我YY的一樣做啥進程池之類的特殊機制,而是簡單的履行有就創建、沒有就銷毀的策略。 我并不知道有沒有啥很有效的多進程模型,這方面一點都沒玩過,猜測Chrome之所以采取這樣的策略,是經過琢磨的,覺得進程生死的代價可以承受,比較可 行。


3. 進程開銷控制算法
    說開銷無外乎兩方面的內容,一為時間,二則空間。Chrome沒有在進程創建和銷毀上做功夫,但是當進程運行起來后,還是做了一些工作的。

    節約工作首先從CPU耗時上做起,優先級越高的進程中的線程,越容易被調度,從而耗費CPU時間,于是,當一個頁面不再直接面對用戶的時候,Chrome 會將它的進程優先級切到Below Normal的級別,反之,則切回Normal級別。通過這個步驟,小節約了一把時間。

進程的優先級
在 windows中,進程是有優先級的,當然,這個優先級不是真實的調度優先級,而是該進程中,線程優先級計算的基準。在《Windows via C/C++》(也就是《windows核心編程》的第五版)中,有一張詳細的表,表述了線程優先級和進程優先級的具體對應關系,感覺設計的很不錯,在此就 不再贅述了,有興趣的自行動手翻書。


    當然這只是一道開胃小菜,滿漢全席是控制進程的工作集大小,以達到降低進程實際內存消耗的目的(Chrome為了體現它對內存的節約,用了“更為精確”的 內存消耗計算方法…)。提到這一點,Chrome頗為自豪,在文檔中,順著道把單進程的模式鄙視了一下,基本意思是:在多進程的模式下,各個頁面實際占用 的內存數量,更容易被控制,而在單進程的模式下,幾乎是不能作出控制的,所以,很多時候,多進程模式耗費的內存,是會小于多線程模式的。這個說法靠不靠 譜,大家心里都有譜,就不多說了。

    具體說來,Chrome對進程工作集的控制算法還是比較簡單的。首先,在進程啟動的時候,需要指明進程工作的內存環境,是高內存,低內存,還是中等內存, 默認模式下,是中等內存(我以為Chrome會動態計算的,沒想到竟然是啟動時指定…)。在高內存模式,不存在對工作集的調整,使勁用就完事了;在低內存 的模式下,調整也很簡單,一旦一個進程不再有頁面面對觀眾了,嘗試釋放其所有工作集。相比來說,中等模式下,算法相對復雜一些,當一個進程從直接面對觀 眾,淪落到切換到后臺的悲慘命運,其工作集會縮減,算法為: TargetWorkingSetSize = (LastWorkingSet/2 + CurrentWorkingSet) /2;其中,TargetWorkingSetSize指的是預期降到的工作集大小,CurrentWorkingSet指的是進程當前的工作集(在 Chrome中,工作集的大小,包含私有的和可共享的兩部分內存,而不包含已經共享了的內存空間…),LastWorkingSet,等于上一次的 CurrentWorkingSet除以DampingFactor,默認的DampingFactor為2。而反之,當一個進程從幕后走向臺前,它的工 作集會被放大為 LastWorkingSet * DampingFactor * 2,了解過LastWorkingSet的含義,你已經知道,這就是將工作集放大兩倍的另類版寫法。

    Chrome的Render進程工作集調整,除了發生在tab切換(或新頁面建立)的時候,還會發生在整個Chrome的idle事件觸發后。 Chrome有個計時器,統計Chrome空閑的時長,當時長超過30s后(此工作會反復進行…),Chrome會做一系列工作,其中就包括,調整進程的 工作集。被調整的進程,不僅僅是Render進程,還包括Plugin進程和Browser進程,換句話描述,就是所有Chrome進程。

    這個算法導致一個很悲涼的狀況,當你去蹲了個廁所回到電腦前,切換了一個Chrome頁面,你發現頁面一片慘白,一陣硬盤的騷動過后,好不容易恢復了原 貌。如果再切,相同的事情又會發生,孜孜不倦,直到你切過每一個進程。這個慘案發生的主要原因,就是由于所有Chrome進程的工作集都被釋放了,頁面的 重載和Render需要不少的一坨時間,這就大大影響了用戶感受,畢竟,總看到慘白的畫面,容易產生不好的情緒。強烈感覺這個不算一個很出色的策略,應該 有一個工作集切換的底限,或者是在Chrome從idle中被激活的時候,偷偷摸摸的統一擴大工作集,發幾個事件刺激一下,把該加載的東西加載起來。

    整體感覺,Chrome對進程開銷的控制,并不像想象中的有非常精妙絕倫的策略在里面,通過工作集這總手段并不算華麗,而且,如果想很好的工作的話,有一 個非常非常重要的前提,就是被切換的頁面,很少再被繼續瀏覽。個人覺得這個假設并不是十分可靠,這就使得在某些情況下,產生非常不好的用戶體驗,也許 Chrome需要進一步在這個地方琢磨點方法的。

本文Chrome源碼剖析、上,完。



小馬歌 2012-02-16 17:08 發表評論
]]>
用C/C++擴展你的PHP[轉,很全面很具體]http://www.fpcwrs.live/xiaomage234/archive/2009/09/01/293433.html小馬歌小馬歌Tue, 01 Sep 2009 04:21:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2009/09/01/293433.htmlhttp://www.fpcwrs.live/xiaomage234/comments/293433.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2009/09/01/293433.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/293433.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/293433.html

 

· 作者:laruence(http://www.laruence.com/)
· 本文地址: http://www.laruence.com/2009/04/28/719.html
· 轉載請注明出處
翻譯:taft at wjl.cn
校對:laruence at yahoo.com.cn
最后更新日期:2009/04/29

簡 介

PHP取得成功的一個主要原因之一是她擁有大量的可用擴展。web開發者無論有何種需求,這種需求最有可能在PHP發行包里找到。PHP發行包包括支持各種數據庫,圖形文件格式,壓縮,XML技術擴展在內的許多擴展。

擴展API的引入使PHP3取得了巨大的進展,擴展API機制使PHP開發社區很容易的開發出幾十種擴展。現在,兩個版本過去了,API仍然和PHP3時的非常相似。擴展主要的思想是:盡可能的從擴展編寫者那里隱藏PHP的內部機制和腳本引擎本身,僅僅需要開發者熟悉API。

有兩個理由需要自己編寫PHP擴展。第一個理由是:PHP需要支持一項她還未支持的技術。這通常包括包裹一些現成的C函數庫,以便提供PHP接口。例如,如果一個叫FooBase的數據庫已推出市場,你需要建立一個PHP擴展幫助你從PHP里調用FooBase的C函數庫。這個工作可能僅由一個人完成,然后被整個PHP社區共享(如果你愿意的話)。第二個不是很普遍的理由是:你需要從性能或功能的原因考慮來編寫一些商業邏輯。

如果以上的兩個理由都和你沒什么關系,同時你感覺自己沒有冒險精神,那么你可以跳過本章。

本章教你如何編寫相對簡單的PHP擴展,使用一部分擴展API函數。對于大多數打算開發自定義PHP擴展開發者而言,它含概了足夠的資料。學習一門編程課程的最好方法之一就是動手做一些極其簡單的例子,這些例子正是本章的線索。一旦你明白了基礎的東西,你就可以在互聯網上通過閱讀文擋、原代碼或參加郵件列表新聞組討論來豐富自己。因此,本章集中在讓你如何開始的話題。在UNIX下一個叫ext_skel的腳本被用于建立擴展的骨架,骨架信息從一個描述擴展接口的定義文件中取得。因此你需要利用UNIX來建立一個骨架。Windows開發者可以使用Windows ext_skel_win32.php代替ext_skel。

然而,本章關于用你開發的擴展編譯PHP的指導僅涉及UNIX編譯系統。本章中所有的對API的解釋與UNIX和Windows下開發的擴展都有聯系。

當你閱讀完這章,你能學會如何

  • 建立一個簡單的商業邏輯擴展。
  • 建議個C函數庫的包裹擴展,尤其是有些標準C文件操作函數比如fopen()

快速開始

本節沒有介紹關于腳本引擎基本構造的一些知識,而是直接進入擴展的編碼講解中,因此不要擔心你無法立刻獲得對擴展整體把握的感覺。假設你正在開發一個網站,需要一個把字符串重復n次的函數。下面是用PHP寫的例子:

  1. function self_concat($string, $n){
  2.     $result = "";
  3. for($i = 0; $i < $n; $i++){
  4.     $result .= $string;
  5. }
  6.     return $result;
  7. }
  8.  
  9. self_concat("One", 3) returns "OneOneOne".
  10.  
  11. self_concat("One", 1) returns "One".

假設由于一些奇怪的原因,你需要時常調用這個函數,而且還要傳給函數很長的字符串和大值n。這意味著在腳本里有相當巨大的字符串連接量和內存重新分配過程,以至顯著地降低腳本執行速度。如果有一個函數能夠更快地分配大量且足夠的內存來存放結果字符串,然后把$string重復n次,就不需要在每次循環迭代中分配內存。

為擴展建立函數的第一步是寫一個函數定義文件,該函數定義文件定義了擴展對外提供的函數原形。該例中,定義函數只有一行函數原形self_concat() :

  1. string self_concat(string str, int n)

函數定義文件的一般格式是一個函數一行。你可以定義可選參數和使用大量的PHP類型,包括: bool, float, int, array等。

保存為myfunctions.def文件至PHP原代碼目錄樹下。

該是通過擴展骨架(skeleton)構造器運行函數定義文件的時機了。該構造器腳本叫ext_skel,放在PHP原代碼目錄樹的ext/目錄下(PHP原碼主目錄下的README.EXT_SKEL提供了更多的信息)。假設你把函數定義保存在一個叫做myfunctions.def的文件里,而且你希望把擴展取名為myfunctions,運行下面的命令來建立擴展骨架

  1. ./ext_skel --extname=myfunctions --proto=myfunctions.def

這個命令在ext/目錄下建立了一個myfunctions/目錄。你要做的第一件事情也許就是編譯該骨架,以便編寫和測試實際的C代碼。編譯擴展有兩種方法:

  • 作為一個可裝載模塊或者DSO(動態共享對象)
  • 靜態編譯到PHP
PHP擴展開發導圖

PHP擴展開發導圖

因為第二種方法比較容易上手,所以本章采用靜態編譯。如果你對編譯可裝載擴展模塊感興趣,可以閱讀PHP原代碼根目錄下的README.SELF-CONTAINED_EXTENSIONS文件。為了使擴展能夠被編譯,需要修改擴展目錄ext/myfunctions/下的config.m4文件。擴展沒有包裹任何外部的C庫,你需要添加支持–enable-myfunctions配置開關到PHP編譯系統里(–with-extension 開關用于那些需要用戶指定相關C庫路徑的擴展)。可以去掉自動生成的下面兩行的注釋來開啟這個配置。

  1. ./ext_skel --extname=myfunctions --proto=myfunctions.def
  2. PHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support,
  3.  
  4. [ --enable-myfunctions                Include myfunctions support])

 

現在剩下的事情就是在PHP原代碼樹根目錄下運行./buildconf,該命令會生成一個新的配置腳本。通過查看./configure –help輸出信息,可以檢查新的配置選項是否被包含到配置文件中。現在,打開你喜好的配置選項開關和–enable-myfunctions重新配置一下PHP。最后的但不是最次要的是,用make來重新編譯PHP。

ext_skel應該把兩個PHP函數添加到你的擴展骨架了:打算實現的self_concat()函數和用于檢測myfunctions 是否編譯到PHP的confirm_myfunctions_compiled()函數。完成PHP的擴展開發后,可以把后者去掉。

  1. <?php
  2. print confirm_myfunctions_compiled("myextension");
  3. ?>

運行這個腳本會出現類似下面的輸出:

  1. "Congratulations! You have successfully modified ext/myfunctions
  2.  
  3. config.m4. Module myfunctions is now compiled into PHP."

另外,ext_skel腳本生成一個叫myfunctions.php的腳本,你也可以利用它來驗證擴展是否被成功地編譯到PHP。它會列出該擴展所支持的所有函數。

現在你學會如何編譯擴展了,該是真正地研究self_concat()函數的時候了。
下面就是ext_skel腳本生成的骨架結構:

  1. /* {{{ proto string self_concat(string str, int n)
  2.  
  3. */
  4.  
  5. PHP_FUNCTION(self_concat)
  6.  
  7. }
  8.  
  9. char *str = NULL;
  10.  
  11. int argc = ZEND_NUM_ARGS();
  12.  
  13. int str_len;
  14.  
  15. long n;
  16.  
  17. if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
  18.  
  19. return;
  20.  
  21. php_error(E_WARNING, "self_concat: not yet implemented");
  22.  
  23. }
  24.  
  25. /* }}} */

 

自動生成的PHP函數周圍包含了一些注釋,這些注釋用于自動生成代碼文檔和vi、Emacs等編輯器的代碼折疊。函數自身的定義使用了宏PHP_FUNCTION(),該宏可以生成一個適合于Zend引擎的函數原型。邏輯本身分成語義各部分,取得調用函數的參數和邏輯本身。

為了獲得函數傳遞的參數,可以使用zend_parse_parameters()API函數。下面是該函數的原型:

  1. zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);

第一個參數是傳遞給函數的參數個數。通常的做法是傳給它ZEND_NUM_ARGS()。這是一個表示傳遞給函數參數總個數的宏。第二個參數是為了線程安全,總是傳遞TSRMLS_CC宏,后面會講到。第三個參數是一個字符串,指定了函數期望的參數類型,后面緊跟著需要隨參數值更新的變量列表。因為PHP采用松散的變量定義和動態的類型判斷,這樣做就使得把不同類型的參數轉化為期望的類型成為可能。例如,如果用戶傳遞一個整數變量,可函數需要一個浮點數,那么zend_parse_parameters()就會自動地把整數轉換為相應的浮點數。如果實際值無法轉換成期望類型(比如整形到數組形),會觸發一個警告。

下表列出了可能指定的類型。我們從完整性考慮也列出了一些沒有討論到的類型。

類型指定符 對應的C類型 描述
l long 符號整數
d double 浮點數
s char *, int 二進制字符串,長度
b zend_bool 邏輯型(1或0)
r zval * 資源(文件指針,數據庫連接等)
a zval * 聯合數組
o zval * 任何類型的對象
O zval * 指定類型的對象。需要提供目標對象的類類型
z zval * 無任何操作的zval

為了容易地理解最后幾個選項的含義,你需要知道zval是Zend引擎的值容器[1]。無論這個變量是布爾型,字符串型或者其他任何類型,其信息總會包含在一個zval聯合體中。本章中我們不直接存取zval,而是通過一些附加的宏來操作。下面的是或多或少在C中的zval, 以便我們能更好地理解接下來的代碼。

  1. typedef union _zval{
  2. long lval;
  3. double dval;
  4. struct {
  5. char *val;
  6. int len;
  7. }str;
  8.  
  9. HashTable *ht;
  10. zend_object_value obj;
  11.  
  12. }zval;

 

在我們的例子中,我們用基本類型調用zend_parse_parameters(),以本地C類型的方式取得函數參數的值,而不是用zval容器。

為了讓zend_parse_parameters()能夠改變傳遞給它的參數的值,并返回這個改變值,需要傳遞一個引用。仔細查看一下self_concat():

  1. if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
  2. return;

注意到自動生成的代碼會檢測函數的返回值FAILUER(成功即SUCCESS)來判斷是否成功。如果沒有成功則立即返回,并且由zend_parse_parameters()負責觸發警告信息。因為函數打算接收一個字符串l和一個整數n,所以指定 ”sl” 作為其類型指示符。s需要兩個參數,所以我們傳遞參考char * 和 int (str 和 str_len)給zend_parse_parameters()函數。無論什么時候,記得總是在代碼中使用字符串長度str_len來確保函數工作在二進制安全的環境中。不要使用strlen()和strcpy(),除非你不介意函數在二進制字符串下不能工作。二進制字符串是包含有nulls的字符串。二進制格式包括圖象文件,壓縮文件,可執行文件和更多的其他文件。”l” 只需要一個參數,所以我們傳遞給它n的引用。盡管為了清晰起見,骨架腳本生成的C變量名與在函數原型定義文件中的參數名一樣;這樣做不是必須的,盡管在實踐中鼓勵這樣做。

回到轉換規則中來。下面三個對self_concat()函數的調用使str, str_len和n得到同樣的值:

  1. self_concat("321", 5);
  2.  
  3. self_concat(321, "5");
  4.  
  5. self_concat("321", "5");
  6.  
  7. str points to the string "321", str_len equals 3, and n equals 5.
  8.  
  9. str 指向字符串"321",str_len等于3,n等于5。

 

在我們編寫代碼來實現連接字符串返回給PHP的函數前,還得談談兩個重要的話題:內存管理、從PHP內部返回函數值所使用的API。

內存管理

用于從堆中分配內存的PHP API幾乎和標準C API一樣。在編寫擴展的時候,使用下面與C對應(因此不必再解釋)的API函數:

emalloc(size_t size);

efree(void *ptr);

ecalloc(size_t nmemb, size_t size);

erealloc(void *ptr, size_t size);

estrdup(const char *s);

estrndup(const char *s, unsigned int length);

在這一點上,任何一位有經驗的C程序員應該象這樣思考一下:“什么?標準C沒有strndup()?”是的,這是正確的,因為GNU擴展通常在Linux下可用。estrndup()只是PHP下的一個特殊函數。它的行為與estrdup()相似,但是可以指定字符串重復的次數(不需要結束空字符),同時是二進制安全的。這是推薦使用estrndup()而不是estrdup()的原因。

在幾乎所有的情況下,你應該使用這些內存分配函數。有一些情況,即擴展需要分配在請求中永久存在的內存,從而不得不使用malloc(),但是除非你知道你在做什么,你應該始終使用以上的函數。如果沒有使用這些內存函數,而相反使用標準C函數分配的內存返回給腳本引擎,那么PHP會崩潰。

這些函數的優點是:任何分配的內存在偶然情況下如果沒有被釋放,則會在頁面請求的最后被釋放。因此,真正的內存泄漏不會產生。然而,不要依賴這一機制,從調試和性能兩個原因來考慮,應當確保釋放應該釋放的內存。剩下的優點是在多線程環境下性能的提高,調試模式下檢測內存錯誤等。

還有一個重要的原因,你不需要檢查這些內存分配函數的返回值是否為null。當內存分配失敗,它們會發出E_ERROR錯誤,從而決不會返回到擴展。

從PHP函數中返回值

擴展API包含豐富的用于從函數中返回值的宏。這些宏有兩種主要風格:第一種是RETVAL_type()形式,它設置了返回值但C代碼繼續執行。這通常使用在把控制交給腳本引擎前還希望做的一些清理工作的時候使用,然后再使用C的返回聲明 ”return” 返回到PHP;后一個宏更加普遍,其形式是RETURN_type(),他設置了返回類型,同時返回控制到PHP。下表解釋了大多數存在的宏。

設置返回值并且結束函數 設置返回值 宏返回類型和參數
RETURN_LONG(l) RETVAL_LONG(l) 整數
RETURN_BOOL(b) RETVAL_BOOL(b) 布爾數(1或0)
RETURN_NULL() RETVAL_NULL() NULL
RETURN_DOUBLE(d) RETVAL_DOUBLE(d) 浮點數
RETURN_STRING(s, dup) RETVAL_STRING(s, dup) 字符串。如果dup為1,引擎會調用estrdup()重復s,使用拷貝。如果dup為0,就使用s
RETURN_STRINGL(s, l, dup) RETVAL_STRINGL(s, l, dup) 長度為l的字符串值。與上一個宏一樣,但因為s的長度被指定,所以速度更快。
RETURN_TRUE RETVAL_TRUE 返回布爾值true。注意到這個宏沒有括號。
RETURN_FALSE RETVAL_FALSE 返回布爾值false。注意到這個宏沒有括號。
RETURN_RESOURCE(r) RETVAL_RESOURCE(r) 資源句柄。

完成self_concat()

現在你已經學會了如何分配內存和從PHP擴展函數里返回函數值,那么我們就能夠完成self_concat()的編碼:

  1. /* {{{ proto string self_concat(string str, int n)
  2.  
  3. */
  4.  
  5. PHP_FUNCTION(self_concat)
  6.  
  7. }
  8.  
  9. char *str = NULL;
  10.  
  11. int argc = ZEND_NUM_ARGS();
  12.  
  13. int str_len;
  14.  
  15. long n;
  16.  
  17. char *result; /* Points to resulting string */
  18.  
  19. char *ptr; /* Points at the next location we want to copy to */
  20.  
  21. int result_length; /* Length of resulting string */
  22.  
  23. if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
  24.  
  25. return;
  26.  
  27. /* Calculate length of result */
  28.  
  29. result_length = (str_len * n);
  30.  
  31. /* Allocate memory for result */
  32.  
  33. result = (char *) emalloc(result_length + 1);
  34.  
  35. /* Point at the beginning of the result */
  36.  
  37. ptr = result;
  38.  
  39. while (n--) {
  40.  
  41. /* Copy str to the result */
  42.  
  43. memcpy(ptr, str, str_len);
  44.  
  45. /* Increment ptr to point at the next position we want to write to */
  46.  
  47. ptr += str_len;
  48.  
  49. }
  50.  
  51. /* Null terminate the result. Always null-terminate your strings
  52.  
  53. even if they are binary strings */
  54.  
  55. *ptr = '\0';
  56.  
  57. /* Return result to the scripting engine without duplicating it*/
  58.  
  59. RETURN_STRINGL(result, result_length, 0);
  60.  
  61. }
  62.  
  63. /* }}} */

現在要做的就是重新編譯一下PHP,這樣就完成了第一個PHP函數。

讓我門檢查函數是否真的工作。在最新編譯過的PHP樹下執行[2]下面的腳本:

  1. <?php
  2. for ($i = 1; $i <= 3; $i++){
  3.     print self_concat("ThisIsUseless", $i);
  4.     print "\n";
  5. }
  6. ?>

你應該得到下面的結果:

  1. ThisIsUseless
  2.  
  3. ThisIsUselessThisIsUseless
  4.  
  5. ThisIsUselessThisIsUselessThisIsUseless

 

實例小結

你已經學會如何編寫一個簡單的PHP函數。回到本章的開頭,我們提到用C編寫PHP功能函數的兩個主要的動機。第一個動機是用C實現一些算法來提高性能和擴展功能。前一個例子應該能夠指導你快速上手這種類型擴展的開發。第二個動機是包裹三方函數庫。我們將在下一步討論。

包裹第三方的擴展

本節中你將學到如何編寫更有用和更完善的擴展。該節的擴展包裹了一個C庫,展示了如何編寫一個含有多個互相依賴的PHP函數擴展。

動機

也許最常見的PHP擴展是那些包裹第三方C庫的擴展。這些擴展包括MySQL或Oracle的數據庫服務庫,libxml2的 XML技術庫,ImageMagick 或GD的圖形操縱庫。

在本節中,我們編寫一個擴展,同樣使用腳本來生成骨架擴展,因為這能節省許多工作量。這個擴展包裹了標準C函數fopen(), fclose(), fread(), fwrite()和 feof().

擴展使用一個被叫做資源的抽象數據類型,用于代表已打開的文件FILE*。你會注意到大多數處理比如數據庫連接、文件句柄等的PHP擴展使用了資源類型,這是因為引擎自己無法直接“理解”它們。我們計劃在PHP擴展中實現的C API列表如下:

FILE *fopen(const char *path, const char *mode);

int fclose(FILE *stream);

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

int feof(FILE *stream);

 

我們實現這些函數,使它們在命名習慣和簡單性上符合PHP腳本。如果你曾經向PHP社區貢獻過代碼,你被期望遵循一些公共習俗,而不是跟隨C庫里的API。并不是所有的習俗都寫在PHP代碼樹的CODING_STANDARDS文件里。這即是說,此功能已經從PHP發展的很早階段即被包含在PHP中,并且與C庫API類似。PHP安裝已經支持fopen(), fclose()和更多的PHP函數。

以下是PHP風格的API:

resource file_open(string filename, string mode)

file_open() //接收兩個字符串(文件名和模式),返回一個文件的資源句柄。

bool file_close(resource filehandle)

file_close() //接收一個資源句柄,返回真/假指示是否操作成功。

string file_read(resource filehandle, int size)

file_read() //接收一個資源句柄和讀入的總字節數,返回讀入的字符串。

bool file_write(resource filehandle, string buffer)

file_write()   //接收一個資源句柄和被寫入的字符串,返回真/假指示是否操作成功。

bool file_eof(resource filehandle)

file_eof() //接收一個資源句柄,返回真/假指示是否到達文件的尾部。

因此,我們的函數定義文件——保存為ext/目錄下的myfile.def——內容如下:

resource file_open(string filename, string mode)

bool file_close(resource filehandle)

string file_read(resource filehandle, int size)

bool file_write(resource filehandle, string buffer)

bool file_eof(resource filehandle)

 

下一步,利用ext_skel腳本在ext./ 原代碼目錄執行下面的命令:

./ext_skel --extname=myfile --proto=myfile.def

然后,按照前一個例子的關于編譯新建立腳本的步驟操作。你會得到一些包含FETCH_RESOURCE()宏行的編譯錯誤,這樣骨架腳本就無法順利完成編譯。為了讓骨架擴展順利通過編譯,把那些出錯行[3]注釋掉即可。

資源

資源是一個能容納任何信息的抽象數據結構。正如前面提到的,這個信息通常包括例如文件句柄、數據庫連接結構和其他一些復雜類型的數據。

使用資源的主要原因是因為:資源被一個集中的隊列所管理,該隊列可以在PHP開發人員沒有在腳本里面顯式地釋放時可以自動地被釋放。

舉個例子,考慮到編寫一個腳本,在腳本里調用mysql_connect()打開一個MySQL連接,可是當該數據庫連接資源不再使用時卻沒有調用mysql_close()。在PHP里,資源機制能夠檢測什么時候這個資源應當被釋放,然后在當前請求的結尾或通常情況下更早地釋放資源。這就為減少內存泄漏賦予了一個“防彈”機制。如果沒有這樣一個機制,經過幾次web請求后,web服務器也許會潛在地泄漏許多內存資源,從而導致服務器當機或出錯。

注冊資源類型

如何使用資源?Zend引擎讓使用資源變地非常容易。你要做的第一件事就是把資源注冊到引擎中去。使用這個API函數:

int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number)

這個函數返回一個資源類型id,該id應當被作為全局變量保存在擴展里,以便在必要的時候傳遞給其他資源API。ld:該資源釋放時調用的函數。pld用于在不同請求中始終存在的永久資源,本章不會涉及。type_name是一個具有描述性類型名稱的字符串,module_number為引擎內部使用,當我們調用這個函數時,我們只需要傳遞一個已經定義好的module_number變量。

回到我們的例子中來:我們會添加下面的代碼到myfile.c原文件中。該文件包括了資源釋放函數的定義,此資源函數被傳遞給zend_register_list_destructors_ex()注冊函數(資源釋放函數應該提早添加到文件中,以便在調用zend_register_list_destructors_ex()時該函數已被定義):

  1. static void myfile_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC){
  2. FILE *fp = (FILE *) rsrc->ptr;
  3. fclose(fp);
  4. }

把注冊行添加到PHP_MINIT_FUNCTION()后,看起來應該如下面的代碼:

  1. PHP_MINIT_FUNCTION(myfile){
  2. /* If you have INI entries, uncomment these lines
  3. ZEND_INIT_MODULE_GLOBALS(myfile, php_myfile_init_globals,NULL);
  4.  
  5. REGISTER_INI_ENTRIES();
  6. */
  7.  
  8. le_myfile = zend_register_list_destructors_ex(myfile_dtor,NULL,"standard-c-file", module_number);
  9.  
  10. return SUCCESS;
  11. }

l 注意到le_myfile是一個已經被ext_skel腳本定義好的全局變量。

PHP_MINIT_FUNCTION()是一個先于模塊(擴展)的啟動函數,是暴露給擴展的一部分API。下表提供可用函數簡要的說明。

函數聲明宏 語義
PHP_MINIT_FUNCTION() 當PHP被裝載時,模塊啟動函數即被引擎調用。這使得引擎做一些例如資源類型,注冊INI變量等的一次初始化。
PHP_MSHUTDOWN_FUNCTION() 當PHP完全關閉時,模塊關閉函數即被引擎調用。通常用于注銷INI條目
PHP_RINIT_FUNCTION() 在每次PHP請求開始,請求前啟動函數被調用。通常用于管理請求前邏輯。
PHP_RSHUTDOWN_FUNCTION() 在每次PHP請求結束后,請求前關閉函數被調用。經常應用在清理請求前啟動函數的邏輯。
PHP_MINFO_FUNCTION() 調用phpinfo()時模塊信息函數被呼叫,從而打印出模塊信息。

新建和注冊新資源 我們準備實現file_open()函數。當我們打開文件得到一個FILE *,我們需要利用資源機制注冊它。下面的主要宏實現注冊功能:

  1. ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type);

參考表格對宏參數的解釋

ZEND_REGISTER_RESOURCE 宏參數

宏參數 參數類型
rsrc_result zval *, which should be set with the registered resource information. zval * 設置為已注冊資源信息
rsrc_pointer Pointer to our resource data. 資源數據指針
rsrc_type The resource id obtained when registering the resource type. 注冊資源類型時獲得的資源id

文件函數

現在你知道了如何使用ZEND_REGISTER_RESOURCE()宏,并且準備好了開始編寫file_open()函數。還有一個主題我們需要講述。

當PHP運行在多線程服務器上,不能使用標準的C文件存取函數。這是因為在一個線程里正在運行的PHP腳本會改變當前工作目錄,因此另外一個線程里的腳本使用相對路徑則無法打開目標文件。為了阻止這種錯誤發生,PHP框架提供了稱作VCWD (virtual current working directory 虛擬當前工作目錄)宏,用來代替任何依賴當前工作目錄的存取函數。這些宏與被替代的函數具備同樣的功能,同時是被透明地處理。在某些沒有標準C函數庫平臺的情況下,VCWD框架則不會得到支持。例如,Win32下不存在chown(),就不會有相應的VCWD_CHOWN()宏被定義。

VCWD列表

標準C庫 VCWD宏
getcwd() VCWD_GETCWD()
fopen() VCWD_FOPEN
open() VCWD_OPEN() //用于兩個參數的版本
open() VCWD_OPEN_MODE() //用于三個參數的open()版本
creat() VCWD_CREAT()
chdir() VCWD_CHDIR()
getwd() VCWD_GETWD()
realpath() VCWD_REALPATH()
rename() VCWD_RENAME()
stat() VCWD_STAT()
lstat() VCWD_LSTAT()
unlink() VCWD_UNLINK()
mkdir() VCWD_MKDIR()
rmdir() VCWD_RMDIR()
opendir() VCWD_OPENDIR()
popen() VCWD_POPEN()
access() VCWD_ACCESS()
utime() VCWD_UTIME()
chmod() VCWD_CHMOD()
chown() VCWD_CHOWN()

編寫利用資源的第一個PHP函數

實現file_open()應該非常簡單,看起來像下面的樣子:

  1. PHP_FUNCTION(file_open){
  2. char *filename = NULL;
  3. char *mode = NULL;
  4. int argc = ZEND_NUM_ARGS();
  5. int filename_len;
  6. int mode_len;
  7. FILE *fp;
  8.  
  9. if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename,&filename_len, &mode, &mode_len) == FAILURE) {
  10. return;
  11. }
  12.  
  13. fp = VCWD_FOPEN(filename, mode);
  14.  
  15. if (fp == NULL) {
  16. RETURN_FALSE;
  17. }
  18.  
  19. ZEND_REGISTER_RESOURCE(return_value, fp, le_myfile);
  20. }

 

你可能會注意到資源注冊宏的第一個參數return_value,可此地找不到它的定義。這個變量自動的被擴展框架定義為zval * 類型的函數返回值。先前討論的、能夠影響返回值的RETURN_LONG() 和RETVAL_BOOL()宏確實改變了return_value的值。因此很容易猜到程序注冊了我們取得的文件指針fp,同時設置return_value為該注冊資源。

訪問資源 需要使用下面的宏訪問資源(參看表對宏參數的解釋)

ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type);

ZEND_FETCH_RESOURCE 宏參數

參數 含義
rsrc 資源值保存到的變量名。它應該和資源有相同類型。
rsrc_type rsrc的類型,用于在內部把資源轉換成正確的類型
passed_id 尋找的資源值(例如zval **)
default_id 如果該值不為-1,就使用這個id。用于實現資源的默認值。
resource_type_name 資源的一個簡短名稱,用于錯誤信息。
resource_type 注冊資源的資源類型id

使用這個宏,我們現在能夠實現file_eof():

  1. PHP_FUNCTION(file_eof){
  2. int argc = ZEND_NUM_ARGS();
  3. zval *filehandle = NULL;
  4. FILE *fp;
  5.  
  6. if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) ==FAILURE) {
  7. return;
  8. }
  9.  
  10. ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-c-file",le_myfile);
  11.  
  12. if (fp == NULL){
  13. RETURN_FALSE;
  14. }
  15.  
  16. if (feof(fp) <= 0) {
  17. /* Return eof also if there was an error */
  18. RETURN_TRUE;
  19. }
  20.  
  21. RETURN_FALSE;
  22. }

 

 

刪除一個資源

通常使用下面這個宏刪除一個資源:

int zend_list_delete(int id)

傳遞給宏一個資源id,返回SUCCESS或者FAILURE。如果資源存在,優先從Zend資源列隊中刪除,該過程中會調用該資源類型的已注冊資源清理函數。因此,在我們的例子中,不必取得文件指針,調用fclose()關閉文件,然后再刪除資源。直接把資源刪除掉即可。

使用這個宏,我們能夠實現file_close():

  1. PHP_FUNCTION(file_close){
  2. int argc = ZEND_NUM_ARGS();
  3. zval *filehandle = NULL;
  4.  
  5. if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) {
  6. return;
  7. }
  8.  
  9. if (zend_list_delete(Z_RESVAL_P(filehandle)) == FAILURE) {
  10. RETURN_FALSE;
  11. }
  12.  
  13. RETURN_TRUE;
  14. }

 

你肯定會問自己Z_RESVAL_P()是做什么的。當我們使用zend_parse_parameters()從參數列表中取得資源的時候,得到的是zval的形式。為了獲得資源id,我們使用Z_RESVAL_P()宏得到id,然后把id傳遞給zend_list_delete()。
有一系列宏用于訪問存儲于zval值(參考表的宏列表)。盡管在大多數情況下zend_parse_parameters()返回與c類型相應的值,我們仍希望直接處理zval,包括資源這一情況。

Zval訪問宏

訪問對象 C 類型
Z_LVAL, Z_LVAL_P, Z_LVAL_PP 整型值 long
Z_BVAL, Z_BVAL_P, Z_BVAL_PP 布爾值 zend_bool
Z_DVAL, Z_DVAL_P, Z_DVAL_PP 浮點值 double
Z_STRVAL, Z_STRVAL_P, Z_STRVAL_PP 字符串值 char *
Z_STRLEN, Z_STRLEN_P, Z_STRLEN_PP 字符串長度值 int
Z_RESVAL, Z_RESVAL_P,Z_RESVAL_PP 資源值 long
Z_ARRVAL, Z_ARRVAL_P, Z_ARRVAL_PP 聯合數組 HashTable *
Z_TYPE, Z_TYPE_P, Z_TYPE_PP Zval類型 Enumeration (IS_NULL, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_BOOL, IS_RESOURCE)
Z_OBJPROP, Z_OBJPROP_P, Z_OBJPROP_PP 對象屬性hash(本章不會談到) HashTable *
Z_OBJCE, Z_OBJCE_P, Z_OBJCE_PP 對象的類信息 zend_class_entry

用于訪問zval值的宏

所有的宏都有三種形式:一個是接受zval s,另外一個接受zval *s,最后一個接受zval **s。它們的區別是在命名上,第一個沒有后綴,zval *有后綴_P(代表一個指針),最后一個 zval **有后綴_PP(代表兩個指針)。
現在,你有足夠的信息來獨立完成 file_read()和 file_write()函數。這里是一個可能的實現:

  1. PHP_FUNCTION(file_read){
  2. int argc = ZEND_NUM_ARGS();
  3. long size;
  4. zval *filehandle = NULL;
  5. FILE *fp;
  6. char *result;
  7. size_t bytes_read;
  8.  
  9. if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle,&size) == FAILURE) {
  10. return;
  11. }
  12.  
  13. ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);
  14.  
  15. result = (char *) emalloc(size+1);
  16.  
  17. bytes_read = fread(result, 1, size, fp);
  18.  
  19. result[bytes_read] = '\0';
  20.  
  21. RETURN_STRING(result, 0);
  22. }
  23.  
  24. PHP_FUNCTION(file_write){
  25. char *buffer = NULL;
  26. int argc = ZEND_NUM_ARGS();
  27. int buffer_len;
  28. zval *filehandle = NULL;
  29. FILE *fp;
  30.  
  31. if (zend_parse_parameters(argc TSRMLS_CC, "rs", &filehandle,&buffer, &buffer_len) == FAILURE) {
  32. return;
  33. }
  34.  
  35. ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);
  36.  
  37. if (fwrite(buffer, 1, buffer_len, fp) != buffer_len) {
  38. RETURN_FALSE;
  39. }
  40.  
  41. RETURN_TRUE;
  42. }

測試擴展

你現在可以編寫一個測試腳本來檢測擴展是否工作正常。下面是一個示例腳本,該腳本打開文件test.txt,輸出文件類容到標準輸出,建立一個拷貝test.txt.new。

  1. <?php
  2. $fp_in = file_open("test.txt", "r") or die("Unable to open input file\n");
  3.  
  4. $fp_out = file_open("test.txt.new", "w") or die("Unable to open output file\n");
  5.  
  6. while (!file_eof($fp_in)) {
  7.     $str = file_read($fp_in, 1024);
  8.     print($str);
  9.     file_write($fp_out, $str);
  10. }
  11.  
  12. file_close($fp_in);
  13. file_close($fp_out);
  14. ?>

全局變量

你可能希望在擴展里使用全局C變量,無論是獨自在內部使用或訪問php.ini文件中的INI擴展注冊標記(INI在下一節中討論)。因為PHP是為多線程環境而設計,所以不必定義全局變量。PHP提供了一個創建全局變量的機制,可以同時應用在線程和非線程環境中。我們應當始終利用這個機制,而不要自主地定義全局變量。用一個宏訪問這些全局變量,使用起來就像普通全局變量一樣。

用于生成myfile工程骨架文件的ext_skel腳本創建了必要的代碼來支持全局變量。通過檢查php_myfile.h文件,你應當發現類似下面的被注釋掉的一節,

  1. ZEND_BEGIN_MODULE_GLOBALS(myfile)
  2.  
  3. int global_value;
  4. char *global_string;
  5.  
  6. ZEND_END_MODULE_GLOBALS(myfile)

你可以把這一節的注釋去掉,同時添加任何其他全局變量于這兩個宏之間。文件后部的幾行,骨架腳本自動地定義一個MYFILE_G(v)宏。這個宏應當被用于所有的代碼,以便訪問這些全局變量。這就確保在多線程環境中,訪問的全局變量僅是一個線程的拷貝,而不需要互斥的操作。

為了使全局變量有效,最后需要做的是把myfile.c:

ZEND_DECLARE_MODULE_GLOBALS(myfile)

注釋去掉。

你也許希望在每次PHP請求的開始初始化全局變量。另外,做為一個例子,全局變量已指向了一個已分配的內存,在每次PHP請求結束時需要釋放內存。為了達到這些目的,全局變量機制提供了一個特殊的宏,用于注冊全局變量的構造和析構函數(參考表對宏參數的說明):

ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)

表 ZEND_INIT_MODULE_GLOBALS 宏參數

參數 含義
module_name 與傳遞給ZEND_BEGIN_MODULE_GLOBALS()宏相同的擴展名稱。
globals_ctor 構造函數指針。在myfile擴展里,函數原形與void php_myfile_init_globals(zend_myfile_globals *myfile_globals)類似
globals_dtor 析構函數指針。例如,php_myfile_init_globals(zend_myfile_globals *myfile_globals)

你可以在myfile.c里看到如何使用構造函數和ZEND_INIT_MODULE_GLOBALS()宏的示例。

添加自定義INI指令

INI文件(php.ini)的實現使得PHP擴展注冊和監聽各自的INI條目。如果這些INI條目由php.ini、Apache的htaccess或其他配置方法來賦值,注冊的INI變量總是更新到正確的值。整個INI框架有許多不同的選項以實現其靈活性。我們涉及一些基本的(也是個好的開端),借助本章的其他材料,我們就能夠應付日常開發工作的需要。

通過在PHP_INI_BEGIN()/PHP_INI_END()宏之間的STD_PHP_INI_ENTRY()宏注冊PHP INI指令。例如在我們的例子里,myfile.c中的注冊過程應當如下:

  1. PHP_INI_BEGIN()
  2.  
  3. STD_PHP_INI_ENTRY("myfile.global_value", "42", PHP_INI_ALL, OnUpdateInt, global_value, zend_myfile_globals, myfile_globals)
  4.  
  5. STD_PHP_INI_ENTRY("myfile.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_myfile_globals, myfile_globals)
  6.  
  7. PHP_INI_END()

 

除了STD_PHP_INI_ENTRY()其他宏也能夠使用,但這個宏是最常用的,可以滿足大多數需要(參看表對宏參數的說明):

STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr)

STD_PHP_INI_ENTRY 宏參數表

參數 含義
name INI條目名
default_value 如果沒有在INI文件中指定,條目的默認值。默認值始終是一個字符串。
modifiable 設定在何種環境下INI條目可以被更改的位域。可以的值是:
• PHP_INI_SYSTEM. 能夠在php.ini或http.conf等系統文件更改
• PHP_INI_PERDIR. 能夠在 .htaccess中更改
• PHP_INI_USER. 能夠被用戶腳本更改
• PHP_INI_ALL. 能夠在所有地方更改
on_modify 處理INI條目更改的回調函數。你不需自己編寫處理程序,使用下面提供的函數。包括:
• OnUpdateInt
• OnUpdateString
• OnUpdateBool
• OnUpdateStringUnempty
• OnUpdateReal
property_name 應當被更新的變量名
struct_type 變量駐留的結構類型。因為通常使用全局變量機制,所以這個類型自動被定義,類似于zend_myfile_globals。
struct_ptr 全局結構名。如果使用全局變量機制,該名為myfile_globals。

最后,為了使自定義INI條目機制正常工作,你需要分別去掉PHP_MINIT_FUNCTION(myfile)中的REGISTER_INI_ENTRIES()調用和PHP_MSHUTDOWN_FUNCTION(myfile)中的UNREGISTER_INI_ENTRIES()的注釋。

訪問兩個示例全局變量中的一個與在擴展里編寫MYFILE_G(global_value) 和MYFILE_G(global_string)一樣簡單。

如果你把下面的兩行放在php.ini中,MYFILE_G(global_value)的值會變為99。

; php.ini – The following line sets the INI entry myfile.global_value to 99.
myfile.global_value = 99

 

線程安全資源管理宏

現在,你肯定注意到以TSRM(線程安全資源管理器)開頭的宏隨處使用。這些宏提供給擴展擁有獨自的全局變量的可能,正如前面提到的。

當編寫PHP擴展時,無論是在多進程或多線程環境中,都是依靠這一機制訪問擴展自己的全局變量。如果使用全局變量訪問宏(例如MYFILE_G()宏),需要確保TSRM上下文信息出現在當前函數中。基于性能的原因,Zend引擎試圖把這個上下文信息作為參數傳遞到更多的地方,包括PHP_FUNCTION()的定義。正因為這樣,在PHP_FUNCTION()內當編寫的代碼使用訪問宏(例如MYFILE_G()宏)時,不需要做任何特殊的聲明。然而,如果PHP函數調用其他需要訪問全局變量的C函數,要么把上下文作為一個額外的參數傳遞給C函數,要么提取上下文(要慢點)。

在需要訪問全局變量的代碼塊開頭使用TSRMLS_FETCH()來提取上下文。例如:

  1. void myfunc(){
  2. TSRMLS_FETCH();
  3.  
  4. MYFILE_G(myglobal) = 2;
  5. }

如果希望讓代碼更加優化,更好的辦法是直接傳遞上下文給函數(正如前面敘述的,PHP_FUNCTION()范圍內自動可用)。可以使用TSRMLS_C(C表示調用Call)和TSRMLS_CC(CC邊式調用Call和逗號Comma)宏。前者應當用于僅當上下文作為一個單獨的參數,后者應用于接受多個參數的函數。在后一種情況中,因為根據取名,逗號在上下文的前面,所以TSRMLS_CC不能是第一個函數參。

在函數原形中,可以分別使用TSRMLS_D和TSRMLS_DC宏聲名正在接收上下文。

下面是前一例子的重寫,利用了參數傳遞上下文。

  1. void myfunc(TSRMLS_D){
  2. MYFILE_G(myglobal) = 2;
  3. }
  4.  
  5. PHP_FUNCTION(my_php_function)
  6. {
  7. myfunc(TSRMLS_C);
  8. }

 

總 結

現在,你已經學到了足夠的東西來創建自己的擴展。本章講述了一些重要的基礎來編寫和理解PHP擴展。Zend引擎提供的擴展API相當豐富,使你能夠開發面向對象的擴展。幾乎沒有文檔談幾許多高級特性。當然,依靠本章所學的基礎知識,你可以通過瀏覽現有的原碼學到很多。

更多關于信息可以在PHP手冊的擴展PHP章節http://www.php.net/manual/en/zend.php中找到。另外,你也可以考慮加入PHP開發者郵件列表internals@ lists.php.net,該郵件列表圍繞開發PHP 本身。你還可以查看一下新的擴展生成工具——PECL_Gen(http://pear.php.net/package/PECL_Gen),這個工具正在開發之中,比起本章使用的ext_skel有更多的特性。

此外你還可以關注風雪之隅, 會有更多相關知識更新.

詞匯表

binary safe 二進制安全
context 上下文
extensions 擴展
entry 條目
skeleton 骨架

Thread-Safe Resource Manager TSRM 線程安全資源管理器

Contact info:
Email: taft at wjl.cn / laruence at yahoo.com.cn
http://www.laruence.com

——————————————————————————–
[1] 可參考譯者寫的
[2] 譯者:可以使用phpcli程序在控制臺里執行php文件。
[3] 譯者:可以查看到生成的FETCH_RESOURCE()宏參數是一些’???’。



小馬歌 2009-09-01 12:21 發表評論
]]>
Linux下C++實現PHP擴展中級應用 (轉)http://www.fpcwrs.live/xiaomage234/archive/2009/08/31/293353.html小馬歌小馬歌Mon, 31 Aug 2009 09:57:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2009/08/31/293353.htmlhttp://www.fpcwrs.live/xiaomage234/comments/293353.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2009/08/31/293353.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/293353.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/293353.html(一):
此篇文章準備分2個部分來講述:
第一部分主要詳細講述一下怎么構建一個完成的C++應用擴展模塊;
第二部分主要講述在PHP及Zend框架下怎么使用Zend API和C++語言來實現自己所要的功能以及項目的開發;
此篇文章所運用的環境在Linux 2.4.21-4.ELsmp(Red Hat Linux 3.2.3-20),Apache/2.2.8,gcc version 3.2.3 20030502,PHP 5.2.5 (cli),Zend Engine v2.2.0下進行。

一、前言
以前寫過一些使用C語言來擴展PHP的應用[1]。在淘寶使用C++做PHP的擴展做項目的過程中,遇到了一些問題,從Google中查找,使用C++來開發PHP的中文文章少之又少,而且沒有一個手冊來告訴用戶怎么寫m4[2]文件,怎么使用zend[3]引擎的一套api函數去寫相關PHP的接口,這里就怎么用C++語言來開發PHP的一些心得介紹給大家,希望有心人能夠有所收獲;
二、為什么要用C++開發PHP
使用C++比用C語言開發PHP主要有2個好處:
使用C++能夠很方便的操作string類型,本身的一些容器和模板[4]、以及面對對象的功能讓開發者能夠節省大量開發的時間,這是比較重要的一點;
C++可以直接使用C的庫,只需要extern “C” {}將其C的頭文件和庫定義包含起來就可以,不需要太多的移植工作,可以重復利用前人的代碼或者庫進行后續的工作;
用C++開發PHP是快速、迅捷的,熟悉了相關的定義以及語法,相信開發PHP不是難事。

三、書寫config文件

config.m4[5]或config.w32[6]文件是編譯基礎中最核心的文件,這個文件主要是用autoconf[7]來產生configure[8]配置文件,繼而自動生成大家所熟悉的Makefile文件,以Linux系統為例:

你可以自己書寫config.m4文件,也可以由Shell腳本 ext_skel[9] 來生成樣板:
[cnangel@localhost ~]$wget http://docs.php.net/get/php-5.2.5.tar.bz2/from/cn.php.net/mirror
[cnangel@localhost ~]$tar -jxf php-5.2.5.tar.bz2
[cnangel@localhost ~]$cd php-5.2.6/ext
[cnangel@localhost ext]./ext_skel –extname=extern_name

接著你會發現在ext目錄下多了一個叫extern_name的目錄。進入該目錄,會發現目錄下有幾個文件:
[cnangel@localhost ext_name]$ls -l
總計 32
-rw-r–r– 1 cnangel cnangel 2103 06-29 19:00 config.m4
-rw-r–r– 1 cnangel cnangel 310 06-29 19:00 config.w32
-rw-r–r– 1 cnangel cnangel 8 06-29 19:00 CREDITS
-rw-r–r– 1 cnangel cnangel 0 06-29 19:00 EXPERIMENTAL
-rw-r–r– 1 cnangel cnangel 5305 06-29 19:00 ext_name.c
-rw-r–r– 1 cnangel cnangel 508 06-29 19:00 ext_name.php
-rw-r–r– 1 cnangel cnangel 2766 06-29 19:00 php_ext_name.h
drwxr-xr-x 2 cnangel cnangel 4096 06-29 19:00 tests

然后可以根據提示來修改config.m4文件,這里有幾個重要的宏命令如下:
dnl 是注釋;
PHP_ARG_WITH 或者 PHP_ARG_ENABLE 指定了PHP擴展模塊的工作方式,前者意味著不需要第三方庫,后者正好相反;
PHP_REQUIRE_CXX 用于指定這個擴展用到了C++;
PHP_ADD_INCLUDE 指定PHP擴展模塊用到的頭文件目錄;
PHP_CHECK_LIBRARY 指定PHP擴展模塊PHP_ADD_LIBRARY_WITH_PATH定義以及庫連接錯誤信息等;
PHP_ADD_LIBRARY(stdc++,”",EXTERN_NAME_LIBADD)用于將標準C++庫鏈接進入擴展
PHP_SUBST(EXTERN_NAME_SHARED_LIBADD) 用于說明這個擴展編譯成動態鏈接庫的形式;
PHP_NEW_EXTENSION 用于指定有哪些源文件應該被編譯,文件和文件之間用空格隔開;

ext_skel默認生成的模塊框架是針對C的,我們要使用C++進行PHP擴展, 那除以上的PHP_REQUIRE_CXX, PHP_ADD_LIBRARY兩個宏必需外,還要把extern_name.c改名成extern_name.cpp。

需要注意的是,在config.m4里面可以使用類似的Makefile語法,片段如下:
PHP_REQUIRE_CXX()
INCLUDES=”$INCLUDES `mysql_config –cflags`”
PHP_ADD_LIBRARY(stdc++, “”, EXTRA_LDFLAGS)
EXTRA_LDFLAGS=”$EXTRA_LDFLAGS `mysql_config –libs` -lmemcached”
AC_CHECK_HEADERS([mysql/mysql.h])
CPPFILE=”ext_name.cpp antiForbitWord.cpp antiBaseDict.cpp Trie.cpp Logger.cpp antiEncodeConverter.cpp strnormalize.cpp”
PHP_NEW_EXTENSION(ext_name, $CPPFILE, $ext_shared)
四、書寫.h文件

這里指修改php_ext_name.h這個頭文件。

由于TSRM.h這個文件所包含的函數和類都是用純C語言寫的,故應該使用extern來說明如下:
extern “C” {
#ifdef ZTS
#include “TSRM.h”
#endif
}

如果該php_ext_name.h頭文件或者ext_name.cpp文件用到了C++語言中的一些容器或者一些函數,則需要在頭文件中包含相應的c++庫的頭文件,否則會出現找不到相應的C++函數錯誤。
五、書寫.cpp文件

這里指修改ext_name.cpp這個cpp文件。

由于config.h、php.h、php_ini.h和ext/standard/info.h中包含的函數和類如TSRM.h一樣,都是用純C語言寫的,所以也需要用extern說明如下:
extern “C” {
#ifdef HAVE_CONFIG_H
#include “config.h”
#endif
#include “php.h”
#include “php_ini.h”
#include “ext/standard/info.h”
}

而 #include “php_ext_name.h” 這句則已經不需要包含在extern “C”內,另外,ZEND_GET_MODULE這個宏命令也是需要特別申明如下:
#ifdef COMPILE_DL_EXT_NAME
BEGIN_EXTERN_C()
ZEND_GET_MODULE(ext_name)
END_EXTERN_C()
#endif

總之,把一些C寫的庫或轟用兼容的方式給解決。
六、初步執行

這里需要用到一個命令:phpize[10],命令如下:
[cnangel@localhost ext_name]$phpize
[cnangel@localhost ext_name]$./configure
[cnangel@localhost ext_name]$make

注意:可以使用用phpize生成configure執行文件后,可以使用./configure –help查看幫助信息,修改config.m4文件可以修改configure的幫助信息。每次修改了config.m4文件,需要使用清除臨時文件 命令phpize –clean來完成消除configure。
七、初步應用

怎么應用到php上,把剛才的擴展模塊當作一個普通的php函數調用呢?簡單的應用直接使用命令:
[cnangel@localhost ext_name]$sudo make install

如果有多個php版本,則尋找擴展庫目錄顯得沒有那么好找了,比如,你的php執行文件的路徑在/usr/local/php/bin/目錄下,想知道php擴展模塊所在的目錄的話,那么執行(PHP5.0以上):
[cnangel@localhost ext_name]$/usr/local/php/bin/php-config | grep extension-dir | sed ’s/.*\[\(.*\)]/\1/’`

PHP5.0以下執行:
[cnangel@localhost ext_name]$/usr/local/php/bin/php-config –extension-dir

這樣你可以發現你的擴展庫的路徑:
/usr/local/php/lib/php/extensions/no-debug-non-zts-20060613

當然,你可以修改php.ini,找到php安裝的配置文件,修改extension_dir的值為你想要的一個路徑另外,需要將你的擴展寫入php.ini,像這樣:

extension=ext_name.so

最后,找到擴展庫的路徑后,將modules下面的extern_name.so文件復制到擴展庫的目錄下,重新啟動一下Apache進程:
[cnangel@localhost ext_name]$which httpd
/usr/bin/httpd
[cnangel@localhost ext_name]$sudo /usr/bin/httpd -k stop
[cnangel@localhost ext_name]$sudo /usr/bin/httpd -k start

把這個樣例ext_name.php復制到web路徑上去,看看是否好使啦?下一節我們將詳細講一些Zend API的宏在ext_name.cpp中的一些復雜應用。

——————————————————————————-
(二)

這里主要講述在PHP及Zend框架下怎么使用Zend API和C++語言來實現自己所要的功能以及項目的開發。
此篇文章所運用的環境在Linux 2.4.21-4.ELsmp(Red Hat Linux
3.2.3-20),Apache/2.2.8,gcc version 3.2.3 20030502,PHP 5.2.5 (cli),Zend
Engine v2.2.0下進行。
前言

上次我們說到使用c++寫一個完整的php擴展,這里以ext_name模塊為例復習一下:

首先仍然修改config.m4文件,由于沒有引用外面的模塊或者相關庫,所以不需要使用PHP_ARG_WITH的方式,使用PHP_ARG_ENABLE方式。找到
PHP_NEW_EXTENSION(ext_name, ext_name.c, $ext_shared)

修改成
PHP_REQUIRE_CXX()
PHP_ADD_LIBRARY(stdc++, “”, EXTRA_LDFLAGS)
PHP_NEW_EXTENSION(ext_name, ext_name.cpp, $ext_shared)

并將ext_name.c重新命名為ext_name.cpp,接著修改其內容,將
#include “php.h”
#include “php_ini.h”
#include “ext/standard/info.h”

用extern “C”將其用大括號括起來,修改
ZEND_GET_MODULE(ext_name)


BEGIN_EXTERN_C()
ZEND_GET_MODULE(ext_name)
END_EXTERN_C()

到此為止,這就是我們第一章內容,第二章比較龐大,這里還是分節來敘述吧。
概述

概述里面主要簡單介紹PHP擴展中的一些大致結構和需要注意的事項,做過C擴展PHP的都會知道 PHP_FE是一個宏把這個宏標識的函數,例如:helloworld,這個函數可以直接作用于PHP解釋器,比如
< ?php
helloworld();
?>

安裝ext_name樣板后,系統會自動有一個函數confirm_ext_name_compiled,這個函數是可以自行修改的,當然,PHP_FE可以定義多個函數,這些函數都必須在之前進行申明,一般在php_ext_name.h頭文件進行申明。

我們還知道,僅僅有頭文件和PHP_FE宏來申明這個函數是不行的,這個函數還沒有內容,怎么編寫這個函數的內容呢?這個在接下來會講到。

其實,稍微細心的人看了ext_name.cpp就知道,去掉注釋后,還有很多的宏命令,比如zend_module_entry、ZEND_GET_MODULE、PHP_MINIT_FUNCTION等等,讀者不要著急,下面會一一道來。

關于ext_name.cpp文件中一些變量的命名,通常是PHP模塊名(eg:ext_name)前面或者后面有一串字符,比如 le_ext_name、ext_name_functions、這是一種習慣,最好我們在書寫的時候遵循這種習慣,這樣寫出來的代碼不僅僅讓你自己明 白,讓其他的開發人員也能夠很快熟悉你的代碼。通常一些定義的常量會大寫,比如要定義這個模塊的名字和版本,可以在頭文件中添加:
#define PHP_EXT_NAME_EXTNAME “ext_name”
#define PHP_EXT_NAME_VERSION “0.1″

然后修改ext_name_module_entry的內容,將”ext_name”和”0.1″分別用PHP_EXT_NAME_EXTNAME和PHP_EXT_NAME_VERSION來替換,這樣具有方便且通用。

如果你可能在代碼中可能需要用到stl之類的或者c++的一些庫,那么你可以在ext_name.cpp文件中添加
#ifndef __APP_CPP__
#define __APP_CPP__
#include
#include
#include
/*
#include
#include #include
#include
#include
#include
#include
*/
#endif
PHP 與 Zend API

引用一句經典的原文來說明PHP和Zend API之間的關系
PHP的核心由兩部分組成。最底層是Zend引擎(ZE)。ZE把人類易讀的腳本解析成機器可讀的符號,
然后在進程空間內執行這些符號。ZE也處理內存管理、變量作用域及調度程序調用。另一部分是PHP內核,
它綁定了SAPI層(Server Application Programming Interface,通常涉及主機環境,如Apache,IIS,CLI,CGI等),
并處理與它的通信。它同時對safe_mode和open_basedir的檢測提供一致的控制層,就像流層將fopen()、fread()和
fwrite()等用戶空間的函數與文件和網絡I/O聯系起來一樣。
模塊信息

模塊信息主要體現在ext_name_module_entry結構上,它包含了

1, 標準模塊的頭

通常用 “STANDARD_MODULE_HEADER” 來填充,它指定了模塊的四個成員:
標識整個模塊結構大小的 size
值為 ZEND_MODULE_API_NO 常量的 zend_api
標識是否為調試版本(使用 ZEND_DEBUG 進行編譯)的 zend_debug
還有一個用來標識是否啟用了 ZTS (Zend 線程安全,使用 ZTS 或USING_ZTS 進行編譯)的 zts。

2, 模塊名稱

模塊名稱這個名字就是使用 phpinfo() 函數后在“Additional Modules”部分所顯示的名稱。

3, PHP擴展可用到的函數或類

zend函數塊的指針

4, 模塊啟動函數

5, 模塊關閉函數

6, 請求啟動函數

7, 請求關閉函數

8, 模塊信息函數

9, 模塊的版本號

10, 其它結構元素

原文:http://my.huhoo.net/archives/2008/02/php_ip.html#1
參考:
快速開發一個PHP擴展:http://blog.csdn.net/heiyeshuwu/archive/2008/12/05/3453854.aspx
如何編寫PHP擴展:http://blog.csdn.net/taft/archive/2006/02/10/596291.aspx



小馬歌 2009-08-31 17:57 發表評論
]]>
用C語言寫PHP擴展的步驟[轉]http://www.fpcwrs.live/xiaomage234/archive/2009/08/31/293352.html小馬歌小馬歌Mon, 31 Aug 2009 09:55:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2009/08/31/293352.htmlhttp://www.fpcwrs.live/xiaomage234/comments/293352.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2009/08/31/293352.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/293352.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/293352.html用C語言,php的擴展的書寫格式(ZEND API)寫PHP擴展的步驟:
我用的php環境 php 5.2.5,完成最基本功能 helloword

cd /php5.2.5/ext

生成骨架 ./ext_skel --extname=cltest 會在當前目錄建立一個cltest的目錄

進入該目錄 cd cltest

修改 配置文件config.m4
vi cltest/config.m4 

注釋3個dnl [dnl代表注釋,對應shell的#]
PHP_ARG_WITH(my_module, for my_module support,
Make sure that the comment is aligned:
[ --with-my_module Include my_module support])

修改完畢。

vi cltest.c
改成這樣:
function_entry my_module_functions[] = {
PHP_FE(say_hello, NULL) /* ?添加這一行代碼   注冊函數say_hello() */
PHP_FE(confirm_my_module_compiled, NULL) /* For testing, remove later. */
{NULL, NULL, NULL} /* Must be the last line in my_module_functions[] */
};

另外在文件的最后添加下列代碼 函數程序主體
PHP_FUNCTION(say_hello)
{
        RETURN_STRINGL("hello world",100,1);
}

修改完畢。

 

vi php_cltest.h

在文件中PHP_FUNCTION(confirm_my_module_compiled);一行前面添加下面的代碼 函數定義
PHP_FUNCTION(say_hello);

修改完畢。

 

找到這個執行文件phpize ,在cltest目錄下執行命令,用于加載動態鏈接庫

/usr/local/php/bin/phpize
./configure --enable-cltest --with-apxs2=/usr/local/apache2/bin/apxs --with-php-config=/usr/local/php/bin/php-config

make
會在當前的目錄下生成一個目錄叫modules他的下面就放著你要的cltest.so文件

make install
會cp modules/cltest.so /usr/local/php/include/php/ext/
其余的就是修改 php.ini加載該.so webserver要生效需要重啟。



小馬歌 2009-08-31 17:55 發表評論
]]>
Linux靜態/動態鏈接庫的創建和使用 [轉]http://www.fpcwrs.live/xiaomage234/archive/2009/07/10/286216.html小馬歌小馬歌Fri, 10 Jul 2009 03:54:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2009/07/10/286216.htmlhttp://www.fpcwrs.live/xiaomage234/comments/286216.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2009/07/10/286216.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/286216.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/286216.htmlfrom : http://hi.baidu.com/xiaochongs/blog/item/23a70b5592e662c4b645aef2.html

Windows系統一樣Linux也有靜態/動態鏈接庫,下面介紹創建和使用方法: 
   

假設有下面幾個文件: 
頭文件String.h,聲明相關函數原形,內容如下: 
Strlen.c
:函數Strlen的實現,獲取給定字符串的長度,內容如下: 
Strlnen.c
:函數StrNlen的實現,獲取給定字符串的長度,如果輸入字符串的長度大于指定的最大長度,則返回最大長度,否者返回字符串的實際長度,內容如下: 
生成靜態庫: 
 
利用GCC生成對應目標文件: 
gcc –c Strlen.c Strnlen.c 
如果對應的文件沒有錯誤,gcc會對文件進行編譯生成Strlen.oStrnlen.o兩個目標文件(相當于windows下的obj文件)。然后用ar創建一個名字為libstr.a的庫文件,并把Strlen.o Strnlen.o的內容插入到對應的庫文件中。,相關命令如下: 
ar –rc libstr.a Strlen.o Strnlen.o 
命令執行成功以后,對應的靜態庫libstr.a已經成功生成。 

/*********************************** 
Filename : String.h 
Description : 
Author   : HCJ 
Date     : 2006-5-7 
************************************/ 

int Strlen(char *pStr); 
int StrNlen(char *pStr, unsigned long ulMaxLen); 



/************************************** 
Filename    : get string length 
Description  :  
Author      : HCJ 
Date        : 2006/5/7 
**************************************/ 
#include<stdio.h> 
#include<assert.h> 
int Strlen(char *pStr) 

    unsigned long ulLength; 
    assert(NULL != pStr); 
    ulLength = 0; 
    while(*pStr++) 
    { 
        ulLength++; 
    } 
    return ulLength; 




********************************************** 
Fileneme: mystrnlen.c 
Description: get input string length,if string large 
             max length input return max length, 
             else real length 
Author: HCJ 
Date  : 2006-5-7 
**********************************************/ 
#include<stdio.h> 
#include<assert.h> 
int StrNlen(char *pStr, unsigned long ulMaxLen) 

    unsigned long ulLength; 
    assert(NULL != pStr); 
    if(ulMaxLen <= 0) 
    { 
        printf("Wrong Max Length!\n"); 
        return -1; 
    } 
    ulLength = 0; 
    while(*pStr++ &&  ulLength < ulMaxLen) 
    { 
        ulLength++; 
    } 
    return ulLength; 
}




生成動態鏈接庫: 
 gcc  -fpic -shared -o libstr.so  Strlen.c Strnlen.c 
-fpic 
使輸出的對象模塊是按照可重定位地址方式生成的。 
-shared
指定把對應的源文件生成對應的動態鏈接庫文件libstr.so文件。 
對應的鏈接庫已經生成,下面看一下如何使用對應的鏈接庫。 
靜態庫的使用: 
假設有下面的文件要使用對應的的靜態庫
編譯生成對應的目標文件: 
gcc -c -I/home/hcj/xxxxxxxx main.c  
生成可執行文件: 
gcc -o main1 -L/home/hcj/xxxxxxxx main.o libstr.a  
其中-I/home/hcj/xxxxxxxx-L/home/hcj/xxxxxxxx是通過-I-L指定對應的頭文件和庫文件的路徑。libstr.a是對應的靜態庫的名稱。這樣對應的靜態庫已經編譯到對應的可執行程序中。執行對應的可執行文件便可以對應得函數調用的結果。 

/***************************************** 
FileName: main.c 
Description: test static/dynamic library 
Author: HCJ 
Date  : 2005-5-7 
******************************************/ 
#include<stdio.h> 
#include <String.h>   //
靜態庫對應函數的頭文件 
int main(int argc, char* argv[]) 

    char str[] = {"hello world"}; 
    unsigned long ulLength = 0; 
    printf("The string is : %s\n", str); 
    ulLength = Strlen(str); 
    printf("The string length is : %d(use Strlen)\n", ulLength); 
    ulLength = StrNlen(str, 10); 
    printf("The string length is : %d(use StrNlen)\n", ulLength); 
    return 0; 




動態庫的分為隱式調用和顯式調用兩種調用方法: 
隱式調用的使用使用方法和靜態庫的調用差不多,具體方法如下: 
gcc -c -I/home/hcj/xxxxxxxx main.c  
gcc -o main1 -L/home/hcj/xxxxxxxx main.o libstr.so  //
這里是*.so 
在這種調用方式中,需要維護動態鏈接庫的配置文件/etc/ld.so.conf來讓動態鏈接庫為系統所使用,通常將動態鏈接庫所在目錄名追加到動態鏈接庫配置文件中。否則在執行相關的可執行文件的時候就會出現載入動態鏈接庫失敗的現象。在編譯所引用的動態庫時,可以在gcc采用 –l-L選項或直接引用所需的動態鏈接庫方式進行編譯。在Linux里面,可以采用ldd命令來檢查程序依賴共享庫。 
顯式調用: 

/***************************************** 
FileName: main2.c 
Description: test static/dynamic library 
Author: HCJ 
Date  : 2005-5-7 
******************************************/ 
#include<stdio.h> 
#include<dlfcn.h> 
int main(int argc, char* argv[]) 

    //define function pointor 
    int (*pStrlenFun)(char* pStr);     //
聲明對應的函數的函數指針 
    int (*pStrnlenFun)(char* pStr, int ulMaxLen); 
    char str[] = {"hello world"}; 
    unsigned long ulLength = 0; 
    void *pdlHandle; 
    char *pszErr; 
    pdlHandle = dlopen("./libstr.so", RTLD_LAZY);  //
加載鏈接庫/libstr.so 
    if(!pdlHandle) 
    { 
        printf("Failed load library\n"); 
    } 
    pszErr = dlerror(); 
    if(pszErr != NULL) 
    { 
        printf("%s\n", pszErr); 
        return 0; 
    } 
    //get function from lib 
    pStrlenFun = dlsym(pdlHandle, "Strlen"); //
獲取函數的地址 
    pszErr = dlerror(); 
    if(pszErr != NULL) 
    { 
        printf("%s\n", pszErr); 
        return 0; 
    } 
    pStrnlenFun = dlsym(pdlHandle, "StrNlen"); 
    pszErr = dlerror(); 
    if(pszErr != NULL) 
    { 
        printf("%s\n", pszErr); 
        return 0; 
    } 
    printf("The string is : %s\n", str); 
    ulLength = pStrlenFun(str);   //
調用相關的函數 
    printf("The string length is : %d(use Strlen)\n", ulLength); 
    ulLength = pStrnlenFun(str, 10); 
    printf("The string length is : %d(use StrNlen)\n", ulLength); 
 dlclose(pdlHandle); 
    return 0; 





gcc -o mian2 -ldl main2.c 

gcc編譯對應的源文件生成可執行文件,-ldl選項,表示生成的對象模塊需要使用共享庫。執行對應得文件同樣可以得到正確的結果。 
相關函數的說明如下: 
(1)dlopen() 
第一個參數:指定共享庫的名稱,將會在下面位置查找指定的共享庫。 
-環境變量LD_LIBRARY_PATH列出的用分號間隔的所有目錄。 
-文件/etc/ld.so.cache中找到的庫的列表,用ldconfig維護。 
-目錄usr/lib 
-目錄/lib 
-當前目錄。 
第二個參數:指定如何打開共享庫。 
RTLD_NOW:將共享庫中的所有函數加載到內存 
RTLD_LAZY:會推后共享庫中的函數的加載操作,直到調用dlsym()時方加載某函數 
(2)dlsym() 
調用dlsym時,利用dlopen()返回的共享庫的phandle以及函數名稱作為參數,返回要加載函數的入口地址。 
(3)dlerror() 
該函數用于檢查調用共享庫的相關函數出現的錯誤。 
 
這樣我們就用簡單的例子說明了在Linux下靜態/動態庫的創建和使用。                                                                                                                                                                                                                                                      



小馬歌 2009-07-10 11:54 發表評論
]]>
GCC精彩之旅--轉帖http://www.fpcwrs.live/xiaomage234/archive/2008/09/19/229938.html小馬歌小馬歌Fri, 19 Sep 2008 06:55:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2008/09/19/229938.htmlhttp://www.fpcwrs.live/xiaomage234/comments/229938.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2008/09/19/229938.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/229938.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/229938.html
在為Linux開發應用程序時,絕大多數情況下使用的都是C語言,因此幾乎每一位Linux程序員面臨的首要問題都是如何靈活運用C編譯器。目前Linux下最常用的C語言編譯器是GCC(GNU Compiler Collection),它是GNU項目中符合ANSI C標準的編譯系統,能夠編譯用C、C++和Object C等語言編寫的程序。GCC不僅功能非常強大,結構也異常靈活。最值得稱道的一點就是它可以通過不同的前端模塊來支持各種語言,如Java、Fortran、Pascal、Modula-3和Ada等。

開放、自由和靈活是Linux的魅力所在,而這一點在GCC上的體現就是程序員通過它能夠更好地控制整個編譯過程。在使用GCC編譯程序時,編譯過程可以被細分為四個階段:

◆ 預處理(Pre-Processing)

◆ 編譯(Compiling)

◆ 匯編(Assembling)

◆ 鏈接(Linking)

Linux程序員可以根據自己的需要讓GCC在編譯的任何階段結束,以便檢查或使用編譯器在該階段的輸出信息,或者對最后生成的二進制文件進行控制,以便通過加入不同數量和種類的調試代碼來為今后的調試做好準備。和其它常用的編譯器一樣,GCC也提供了靈活而強大的代碼優化功能,利用它可以生成執行效率更高的代碼。

GCC提供了30多條警告信息和三個警告級別,使用它們有助于增強程序的穩定性和可移植性。此外,GCC還對標準的C和C++語言進行了大量的擴展,提高程序的執行效率,有助于編譯器進行代碼優化,能夠減輕編程的工作量。

GCC起步

在學習使用GCC之前,下面的這個例子能夠幫助用戶迅速理解GCC的工作原理,并將其立即運用到實際的項目開發中去。首先用熟悉的編輯器輸入清單1所示的代碼:

清單1:hello.c

 #include <stdio.h>
 int main(void)
 {
  printf ("Hello world, Linux programming!\n");
  return 0;
 }
 
然后執行下面的命令編譯和運行這段程序:

 # gcc hello.c -o hello # ./hello Hello world, Linux programming!
 
從程序員的角度看,只需簡單地執行一條GCC命令就可以了,但從編譯器的角度來看,卻需要完成一系列非常繁雜的工作。首先,GCC需要調用預處理程序cpp,由它負責展開在源文件中定義的宏,并向其中插入“#include”語句所包含的內容;接著,GCC會調用ccl和as將處理后的源代碼編譯成目標代碼;最后,GCC會調用鏈接程序ld,把生成的目標代碼鏈接成一個可執行程序。

為了更好地理解GCC的工作過程,可以把上述編譯過程分成幾個步驟單獨進行,并觀察每步的運行結果。第一步是進行預編譯,使用-E參數可以讓GCC在預處理結束后停止編譯過程:

 # gcc -E hello.c -o hello.i
 
此時若查看hello.cpp文件中的內容,會發現stdio.h的內容確實都插到文件里去了,而其它應當被預處理的宏定義也都做了相應的處理。下一步是將hello.i編譯為目標代碼,這可以通過使用-c參數來完成:

 # gcc -c hello.i -o hello.o
 
GCC默認將.i文件看成是預處理后的C語言源代碼,因此上述命令將自動跳過預處理步驟而開始執行編譯過程,也可以使用-x參數讓GCC從指定的步驟開始編譯。最后一步是將生成的目標文件鏈接成可執行文件:

 # gcc hello.o -o hello
 
在采用模塊化的設計思想進行軟件開發時,通常整個程序是由多個源文件組成的,相應地也就形成了多個編譯單元,使用GCC能夠很好地管理這些編譯單元。假設有一個由foo1.c和foo2.c兩個源文件組成的程序,為了對它們進行編譯,并最終生成可執行程序foo,可以使用下面這條命令:

 # gcc foo1.c foo2.c -o foo
 
如果同時處理的文件不止一個,GCC仍然會按照預處理、編譯和鏈接的過程依次進行。如果深究起來,上面這條命令大致相當于依次執行如下三條命令:

 # gcc -c foo1.c -o foo1.o # gcc -c foo2.c -o foo2.o # gcc foo1.o foo2.o -o foo
 
在編譯一個包含許多源文件的工程時,若只用一條GCC命令來完成編譯是非常浪費時間的。假設項目中有100個源文件需要編譯,并且每個源文件中都包含10000行代碼,如果像上面那樣僅用一條GCC命令來完成編譯工作,那么GCC需要將每個源文件都重新編譯一遍,然后再全部連接起來。很顯然,這樣浪費的時間相當多,尤其是當用戶只是修改了其中某一個文件的時候,完全沒有必要將每個文件都重新編譯一遍,因為很多已經生成的目標文件是不會改變的。要解決這個問題,關鍵是要靈活運用GCC,同時還要借助像Make這樣的工具。

警告提示功能

GCC包含完整的出錯檢查和警告提示功能,它們可以幫助Linux程序員寫出更加專業和優美的代碼。先來讀讀清單2所示的程序,這段代碼寫得很糟糕,仔細檢查一下不難挑出很多毛病:

◆main函數的返回值被聲明為void,但實際上應該是int;

◆使用了GNU語法擴展,即使用long long來聲明64位整數,不符合ANSI/ISO C語言標準;

◆main函數在終止前沒有調用return語句。

清單2:illcode.c

 #include <stdio.h>
 void main(void)
 {
  long long int var = 1;
  printf("It is not standard C code!\n");
 }

下面來看看GCC是如何幫助程序員來發現這些錯誤的。當GCC在編譯不符合ANSI/ISO C語言標準的源代碼時,如果加上了-pedantic選項,那么使用了擴展語法的地方將產生相應的警告信息:

 # gcc -pedantic illcode.c -o illcode illcode.c: In function `main': illcode.c:9: ISO C89 does not support `long long' illcode.c:8: return type of `main' is not `int'

需要注意的是,-pedantic編譯選項并不能保證被編譯程序與ANSI/ISO C標準的完全兼容,它僅僅只能用來幫助Linux程序員離這個目標越來越近。或者換句話說,-pedantic選項能夠幫助程序員發現一些不符合ANSI/ISO C標準的代碼,但不是全部,事實上只有ANSI/ISO C語言標準中要求進行編譯器診斷的那些情況,才有可能被GCC發現并提出警告。

除了-pedantic之外,GCC還有一些其它編譯選項也能夠產生有用的警告信息。這些選項大多以-W開頭,其中最有價值的當數-Wall了,使用它能夠使GCC產生盡可能多的警告信息:

 # gcc -Wall illcode.c -o illcode illcode.c:8: warning: return type of `main' is not `int' illcode.c: In function `main': illcode.c:9: warning: unused variable `var'

GCC給出的警告信息雖然從嚴格意義上說不能算作是錯誤,但卻很可能成為錯誤的棲身之所。一個優秀的Linux程序員應該盡量避免產生警告信息,使自己的代碼始終保持簡潔、優美和健壯的特性

在處理警告方面,另一個常用的編譯選項是-Werror,它要求GCC將所有的警告當成錯誤進行處理,這在使用自動編譯工具(如Make等)時非常有用。如果編譯時帶上-Werror選項,那么GCC會在所有產生警告的地方停止編譯,迫使程序員對自己的代碼進行修改。只有當相應的警告信息消除時,才可能將編譯過程繼續朝前推進。執行情況如下:

 # gcc -Wall -Werror illcode.c -o illcode cc1: warnings being treated as errors illcode.c:8: warning: return type of `main' is not `int' illcode.c: In function `main': illcode.c:9: warning: unused variable `var'

對Linux程序員來講,GCC給出的警告信息是很有價值的,它們不僅可以幫助程序員寫出更加健壯的程序,而且還是跟蹤和調試程序的有力工具。建議在用GCC編譯源代碼時始終帶上-Wall選項,并把它逐漸培養成為一種習慣,這對找出常見的隱式編程錯誤很有幫助。

庫依賴

在Linux下開發軟件時,完全不使用第三方函數庫的情況是比較少見的,通常來講都需要借助一個或多個函數庫的支持才能夠完成相應的功能。從程序員的角度看,函數庫實際上就是一些頭文件(.h)和庫文件(.so或者.a)的集合。雖然Linux下的大多數函數都默認將頭文件放到/usr/include/目錄下,而庫文件則放到/usr/lib/目錄下,但并不是所有的情況都是這樣。正因如此,GCC在編譯時必須有自己的辦法來查找所需要的頭文件和庫文件。

GCC采用搜索目錄的辦法來查找所需要的文件,-I選項可以向GCC的頭文件搜索路徑中添加新的目錄。例如,如果在/home/xiaowp/include/目錄下有編譯時所需要的頭文件,為了讓GCC能夠順利地找到它們,就可以使用-I選項:

 # gcc foo.c -I /home/xiaowp/include -o foo

同樣,如果使用了不在標準位置的庫文件,那么可以通過-L選項向GCC的庫文件搜索路徑中添加新的目錄。例如,如果在/home/xiaowp/lib/目錄下有鏈接時所需要的庫文件libfoo.so,為了讓GCC能夠順利地找到它,可以使用下面的命令:

 # gcc foo.c -L /home/xiaowp/lib -lfoo -o foo

值得好好解釋一下的是-l選項,它指示GCC去連接庫文件libfoo.so。Linux下的庫文件在命名時有一個約定,那就是應該以lib三個字母開頭,由于所有的庫文件都遵循了同樣的規范,因此在用-l選項指定鏈接的庫文件名時可以省去lib三個字母,也就是說GCC在對-lfoo進行處理時,會自動去鏈接名為libfoo.so的文件。

Linux下的庫文件分為兩大類分別是動態鏈接庫(通常以.so結尾)和靜態鏈接庫(通常以.a結尾),兩者的差別僅在程序執行時所需的代碼是在運行時動態加載的,還是在編譯時靜態加載的。默認情況下,GCC在鏈接時優先使用動態鏈接庫,只有當動態鏈接庫不存在時才考慮使用靜態鏈接庫,如果需要的話可以在編譯時加上-static選項,強制使用靜態鏈接庫。例如,如果在/home/xiaowp/lib/目錄下有鏈接時所需要的庫文件libfoo.so和libfoo.a,為了讓GCC在鏈接時只用到靜態鏈接庫,可以使用下面的命令:

 # gcc foo.c -L /home/xiaowp/lib -static -lfoo -o foo
 
代碼優化

代碼優化指的是編譯器通過分析源代碼,找出其中尚未達到最優的部分,然后對其重新進行組合,目的是改善程序的執行性能。GCC提供的代碼優化功能非常強大,它通過編譯選項-On來控制優化代碼的生成,其中n是一個代表優化級別的整數。對于不同版本的GCC來講,n的取值范圍及其對應的優化效果可能并不完全相同,比較典型的范圍是從0變化到2或3。

編譯時使用選項-O可以告訴GCC同時減小代碼的長度和執行時間,其效果等價于-O1。在這一級別上能夠進行的優化類型雖然取決于目標處理器,但一般都會包括線程跳轉(Thread Jump)和延遲退棧(Deferred Stack Pops)兩種優化。選項-O2告訴GCC除了完成所有-O1級別的優化之外,同時還要進行一些額外的調整工作,如處理器指令調度等。選項-O3則除了完成所有-O2級別的優化之外,還包括循環展開和其它一些與處理器特性相關的優化工作。通常來說,數字越大優化的等級越高,同時也就意味著程序的運行速度越快。許多Linux程序員都喜歡使用-O2選項,因為它在優化長度、編譯時間和代碼大小之間,取得了一個比較理想的平衡點。

下面通過具體實例來感受一下GCC的代碼優化功能,所用程序如清單3所示。

清單3:optimize.c

 #include <stdio.h>
 int main(void)
 {
  double counter;
  double result;
  double temp;
  
  for (counter = 0; counter < 2000.0 * 2000.0 * 2000.0 / 20.0 + 2020; counter += (5 - 1) / 4)
  {
    temp = counter / 1979; result = counter;
  }
  printf("Result is %lf\n", result); return 0;
 }

首先不加任何優化選項進行編譯:

 # gcc -Wall optimize.c -o optimize
 
借助Linux提供的time命令,可以大致統計出該程序在運行時所需要的時間:

 # time ./optimize Result is 400002019.000000 real 0m14.942s user 0m14.940s sys 0m0.000s
 
接下去使用優化選項來對代碼進行優化處理:

 # gcc -Wall -O optimize.c -o optimize
 
在同樣的條件下再次測試一下運行時間:

 # time ./optimize Result is 400002019.000000 real 0m3.256s user 0m3.240s sys 0m0.000s
 
對比兩次執行的輸出結果不難看出,程序的性能的確得到了很大幅度的改善,由原來的14秒縮短到了3秒。這個例子是專門針對GCC的優化功能而設計的,因此優化前后程序的執行速度發生了很大的改變。盡管GCC的代碼優化功能非常強大,但作為一名優秀的Linux程序員,首先還是要力求能夠手工編寫出高質量的代碼。如果編寫的代碼簡短,并且邏輯性強,編譯器就不會做更多的工作,甚至根本用不著優化。

優化雖然能夠給程序帶來更好的執行性能,但在如下一些場合中應該避免優化代碼:

◆ 程序開發的時候 優化等級越高,消耗在編譯上的時間就越長,因此在開發的時候最好不要使用優化選項,只有到軟件發行或開發結束的時候,才考慮對最終生成的代碼進行優化。

◆ 資源受限的時候 一些優化選項會增加可執行代碼的體積,如果程序在運行時能夠申請到的內存資源非常緊張(如一些實時嵌入式設備),那就不要對代碼進行優化,因為由這帶來的負面影響可能會產生非常嚴重的后果。

◆ 跟蹤調試的時候 在對代碼進行優化的時候,某些代碼可能會被刪除或改寫,或者為了取得更佳的性能而進行重組,從而使跟蹤和調試變得異常困難。

調試

一個功能強大的調試器不僅為程序員提供了跟蹤程序執行的手段,而且還可以幫助程序員找到解決問題的方法。對于Linux程序員來講,GDB(GNU Debugger)通過與GCC的配合使用,為基于Linux的軟件開發提供了一個完善的調試環境。

默認情況下,GCC在編譯時不會將調試符號插入到生成的二進制代碼中,因為這樣會增加可執行文件的大小。如果需要在編譯時生成調試符號信息,可以使用GCC的-g或者-ggdb選項。GCC在產生調試符號時,同樣采用了分級的思路,開發人員可以通過在-g選項后附加數字1、2或3來指定在代碼中加入調試信息的多少。默認的級別是2(-g2),此時產生的調試信息包括擴展的符號表、行號、局部或外部變量信息。級別3(-g3)包含級別2中的所有調試信息,以及源代碼中定義的宏。級別1(-g1)不包含局部變量和與行號有關的調試信息,因此只能夠用于回溯跟蹤和堆棧轉儲之用。回溯跟蹤指的是監視程序在運行過程中的函數調用歷史,堆棧轉儲則是一種以原始的十六進制格式保存程序執行環境的方法,兩者都是經常用到的調試手段。

GCC產生的調試符號具有普遍的適應性,可以被許多調試器加以利用,但如果使用的是GDB,那么還可以通過-ggdb選項在生成的二進制代碼中包含GDB專用的調試信息。這種做法的優點是可以方便GDB的調試工作,但缺點是可能導致其它調試器(如DBX)無法進行正常的調試。選項-ggdb能夠接受的調試級別和-g是完全一樣的,它們對輸出的調試符號有著相同的影響。

需要注意的是,使用任何一個調試選項都會使最終生成的二進制文件的大小急劇增加,同時增加程序在執行時的開銷,因此調試選項通常僅在軟件的開發和調試階段使用。調試選項對生成代碼大小的影響從下面的對比過程中可以看出來:

 # gcc optimize.c -o optimize # ls optimize -l -rwxrwxr-x 1 xiaowp xiaowp 11649 Nov 20 08:53 optimize (未加調試選項) # gcc -g optimize.c -o optimize # ls optimize -l -rwxrwxr-x 1 xiaowp xiaowp 15889 Nov 20 08:54 optimize (加入調試選項)
 
雖然調試選項會增加文件的大小,但事實上Linux中的許多軟件在測試版本甚至最終發行版本中仍然使用了調試選項來進行編譯,這樣做的目的是鼓勵用戶在發現問題時自己動手解決,是Linux的一個顯著特色。

下面還是通過一個具體的實例說明如何利用調試符號來分析錯誤,所用程序見清單4所示。

清單4:crash.c

 #include <stdio.h>
 int main(void)
 {
  int input =0;
  
  printf("Input an integer:");
  scanf("%d", input);
  printf("The integer you input is %d\n", input);
  return 0;
 }
 
編譯并運行上述代碼,會產生一個嚴重的段錯誤(Segmentation fault)如下:

 # gcc -g crash.c -o crash # ./crash Input an integer:10 Segmentation fault
 
為了更快速地發現錯誤所在,可以使用GDB進行跟蹤調試,方法如下:

 # gdb crash GNU gdb Red Hat Linux (5.3post-0.20021129.18rh) …… (gdb)
 
當GDB提示符出現的時候,表明GDB已經做好準備進行調試了,現在可以通過run命令讓程序開始在GDB的監控下運行:

 (gdb) run Starting program: /home/xiaowp/thesis/gcc/code/crash Input an integer:10 Program received signal SIGSEGV, Segmentation fault. 0x4008576b in _IO_vfscanf_internal () from /lib/libc.so.6
 
仔細分析一下GDB給出的輸出結果不難看出,程序是由于段錯誤而導致異常中止的,說明內存操作出了問題,具體發生問題的地方是在調用_IO_vfscanf_internal ( )的時候。為了得到更加有價值的信息,可以使用GDB提供的回溯跟蹤命令backtrace,執行結果如下:

 (gdb) backtrace #0 0x4008576b in _IO_vfscanf_internal () from /lib/libc.so.6 #1 0xbffff0c0 in ?? () #2 0x4008e0ba in scanf () from /lib/libc.so.6 #3 0x08048393 in main () at crash.c:11 #4 0x40042917 in __libc_start_main () from /lib/libc.so.6
 
跳過輸出結果中的前面三行,從輸出結果的第四行中不難看出,GDB已經將錯誤定位到crash.c中的第11行了。現在仔細檢查一下:

 (gdb) frame 3 #3 0x08048393 in main () at crash.c:11 11 scanf("%d", input);
 
使用GDB提供的frame命令可以定位到發生錯誤的代碼段,該命令后面跟著的數值可以在backtrace命令輸出結果中的行首找到。現在已經發現錯誤所在了,應該將

 scanf("%d", input); 改為 scanf("%d", &input);
 
完成后就可以退出GDB了,命令如下:

 (gdb) quit
 
GDB的功能遠遠不止如此,它還可以單步跟蹤程序、檢查內存變量和設置斷點等。

調試時可能會需要用到編譯器產生的中間結果,這時可以使用-save-temps選項,讓GCC將預處理代碼、匯編代碼和目標代碼都作為文件保存起來。如果想檢查生成的代碼是否能夠通過手工調整的辦法來提高執行性能,在編譯過程中生成的中間文件將會很有幫助,具體情況如下:

 # gcc -save-temps foo.c -o foo # ls foo* foo foo.c foo.i foo.s
 
GCC支持的其它調試選項還包括-p和-pg,它們會將剖析(Profiling)信息加入到最終生成的二進制代碼中。剖析信息對于找出程序的性能瓶頸很有幫助,是協助Linux程序員開發出高性能程序的有力工具。在編譯時加入-p選項會在生成的代碼中加入通用剖析工具(Prof)能夠識別的統計信息,而-pg選項則生成只有GNU剖析工具(Gprof)才能識別的統計信息。

最后提醒一點,雖然GCC允許在優化的同時加入調試符號信息,但優化后的代碼對于調試本身而言將是一個很大的挑戰。代碼在經過優化之后,在源程序中聲明和使用的變量很可能不再使用,控制流也可能會突然跳轉到意外的地方,循環語句有可能因為循環展開而變得到處都有,所有這些對調試來講都將是一場噩夢。建議在調試的時候最好不使用任何優化選項,只有當程序在最終發行的時候才考慮對其進行優化。

上次的培訓園地中介紹了GCC的編譯過程、警告提示功能、庫依賴、代碼優化和程序調試六個方面的內容。這期是最后的一部分內容。

加速

在將源代碼變成可執行文件的過程中,需要經過許多中間步驟,包含預處理、編譯、匯編和連接。這些過程實際上是由不同的程序負責完成的。大多數情況下GCC可以為Linux程序員完成所有的后臺工作,自動調用相應程序進行處理。

這樣做有一個很明顯的缺點,就是GCC在處理每一個源文件時,最終都需要生成好幾個臨時文件才能完成相應的工作,從而無形中導致處理速度變慢。例如,GCC在處理一個源文件時,可能需要一個臨時文件來保存預處理的輸出、一個臨時文件來保存編譯器的輸出、一個臨時文件來保存匯編器的輸出,而讀寫這些臨時文件顯然需要耗費一定的時間。當軟件項目變得非常龐大的時候,花費在這上面的代價可能會變得很沉重。

解決的辦法是,使用Linux提供的一種更加高效的通信方式—管道。它可以用來同時連接兩個程序,其中一個程序的輸出將被直接作為另一個程序的輸入,這樣就可以避免使用臨時文件,但編譯時卻需要消耗更多的內存。

在編譯過程中使用管道是由GCC的-pipe選項決定的。下面的這條命令就是借助GCC的管道功能來提高編譯速度的:

 # gcc -pipe foo.c -o foo
 
在編譯小型工程時使用管道,編譯時間上的差異可能還不是很明顯,但在源代碼非常多的大型工程中,差異將變得非常明顯。

文件擴展名

在使用GCC的過程中,用戶對一些常用的擴展名一定要熟悉,并知道其含義。為了方便大家學習使用GCC,在此將這些擴展名羅列如下:

.c C原始程序;

.C C++原始程序;

.cc C++原始程序;

.cxx C++原始程序;

.m Objective-C原始程序;

.i 已經過預處理的C原始程序;

.ii 已經過預處理之C++原始程序;

.s 組合語言原始程序;

.S 組合語言原始程序;

.h 預處理文件(標頭文件);

.o 目標文件;

.a 存檔文件。

GCC常用選項

GCC作為Linux下C/C++重要的編譯環境,功能強大,編譯選項繁多。為了方便大家日后編譯方便,在此將常用的選項及說明羅列出來如下:

-c 通知GCC取消鏈接步驟,即編譯源碼并在最后生成目標文件;

-Dmacro 定義指定的宏,使它能夠通過源碼中的#ifdef進行檢驗;

-E 不經過編譯預處理程序的輸出而輸送至標準輸出;

-g3 獲得有關調試程序的詳細信息,它不能與-o選項聯合使用;

-Idirectory 在包含文件搜索路徑的起點處添加指定目錄;

-llibrary 提示鏈接程序在創建最終可執行文件時包含指定的庫;

-O、-O2、-O3 將優化狀態打開,該選項不能與-g選項聯合使用;

-S 要求編譯程序生成來自源代碼的匯編程序輸出;

-v 啟動所有警報;

-Wall 在發生警報時取消編譯操作,即將警報看作是錯誤;

-Werror 在發生警報時取消編譯操作,即把報警當作是錯誤;

-w 禁止所有的報警。

小結

GCC是在Linux下開發程序時必須掌握的工具之一。本文對GCC做了一個簡要的介紹,主要講述了如何使用GCC編譯程序、產生警告信息、調試程序和加快GCC的編譯速度。對所有希望早日跨入Linux開發者行列的人來說,GCC就是成為一名優秀的Linux程序員的起跑線。



小馬歌 2008-09-19 14:55 發表評論
]]>
Linux上安裝GCC編譯器過程(zz)http://www.fpcwrs.live/xiaomage234/archive/2008/09/19/229937.html小馬歌小馬歌Fri, 19 Sep 2008 06:53:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2008/09/19/229937.htmlhttp://www.fpcwrs.live/xiaomage234/comments/229937.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2008/09/19/229937.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/229937.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/229937.html

2004年4月20日最新版本的GCC編譯器3.4.0發布了。目前,GCC可以用來編譯C/C++、FORTRAN、JAVA、OBJC、ADA等語言的程序,可根據需要選擇安裝支持的語言。GCC 3.4.0比以前版本更好地支持了C++標準。本文以在Redhat Linux上安裝GCC3.4.0為例,介紹了GCC的安裝過程。

  安裝之前,系統中必須要有cc或者gcc等編譯器,并且是可用的,或者用環境變量CC指定系統上的編譯器。如果系統上沒有編譯器,不能安裝源代碼形式的GCC 3.4.0。如果是這種情況,可以在網上找一個與你系統相適應的如RPM等二進制形式的GCC軟件包來安裝使用。本文介紹的是以源代碼形式提供的GCC軟件包的安裝過程,軟件包本身和其安裝過程同樣適用于其它Linux和Unix系統。

  系統上原來的GCC編譯器可能是把gcc等命令文件、庫文件、頭文件等分別存放到系統中的不同目錄下的。與此不同,現在GCC建議我們將一個版本的GCC安裝在一個單獨的目錄下。這樣做的好處是將來不需要它的時候可以方便地刪除整個目錄即可(因為GCC沒有uninstall功能);缺點是在安裝完成后要做一些設置工作才能使編譯器工作正常。在本文中我采用這個方案安裝GCC 3.4.0,并且在安裝完成后,仍然能夠使用原來低版本的GCC編譯器,即一個系統上可以同時存在并使用多個版本的GCC編譯器。

  按照本文提供的步驟和設置選項,即使以前沒有安裝過GCC,也可以在系統上安裝上一個可工作的新版本的GCC編譯器。

  1. 下載

  在GCC網站上(http://gcc.gnu.org/)或者通過網上搜索可以查找到下載資源。目前GCC的最新版本為 3.4.0。可供下載的文件一般有兩種形式:gcc-3.4.0.tar.gz和gcc-3.4.0.tar.bz2,只是壓縮格式不一樣,內容完全一致,下載其中一種即可。

  2. 解壓縮

  根據壓縮格式,選擇下面相應的一種方式解包(以下的“%”表示命令行提示符):

  % tar xzvf gcc-3.4.0.tar.gz
  或者
  % bzcat gcc-3.4.0.tar.bz2 | tar xvf -

  新生成的gcc-3.4.0這個目錄被稱為源目錄,用${srcdir}表示它。以后在出現${srcdir}的地方,應該用真實的路徑來替換它。用pwd命令可以查看當前路徑。

  在${srcdir}/INSTALL目錄下有詳細的GCC安裝說明,可用瀏覽器打開index.html閱讀。

  3. 建立目標目錄
 
  目標目錄(用${objdir}表示)是用來存放編譯結果的地方。GCC建議編譯后的文件不要放在源目錄${srcdir]中(雖然這樣做也可以),最好單獨存放在另外一個目錄中,而且不能是${srcdir}的子目錄。

  例如,可以這樣建立一個叫 gcc-build 的目標目錄(與源目錄${srcdir}是同級目錄):

  % mkdir gcc-build
  % cd gcc-build

  以下的操作主要是在目標目錄 ${objdir} 下進行。

  4. 配置
 
  配置的目的是決定將GCC編譯器安裝到什么地方(${destdir}),支持什么語言以及指定其它一些選項等。其中,${destdir}不能與${objdir}或${srcdir}目錄相同。

  配置是通過執行${srcdir}下的configure來完成的。其命令格式為(記得用你的真實路徑替換${destdir}):

  % ${srcdir}/configure --prefix=${destdir} [其它選項]

  例如,如果想將GCC 3.4.0安裝到/usr/local/gcc-3.4.0目錄下,則${destdir}就表示這個路徑。

  在我的機器上,我是這樣配置的:

  % ../gcc-3.4.0/configure --prefix=/usr/local/gcc-3.4.0 --enable-threads=posix --disable-checking --enable--long-long --host=i386-redhat-linux --with-system-zlib --enable-languages=c,c++,java

  將GCC安裝在/usr/local/gcc-3.4.0目錄下,支持C/C++和JAVA語言,其它選項參見GCC提供的幫助說明。

  5. 編譯

  % make

  這是一個漫長的過程。在我的機器上(P4-1.6),這個過程用了50多分鐘。

  6. 安裝

  執行下面的命令將編譯好的庫文件等拷貝到${destdir}目錄中(根據你設定的路徑,可能需要管理員的權限):

  % make install

  至此,GCC 3.4.0安裝過程就完成了。

  6. 其它設置

  GCC 3.4.0的所有文件,包括命令文件(如gcc、g++)、庫文件等都在${destdir}目錄下分別存放,如命令文件放在bin目錄下、庫文件在lib下、頭文件在include下等。由于命令文件和庫文件所在的目錄還沒有包含在相應的搜索路徑內,所以必須要作適當的設置之后編譯器才能順利地找到并使用它們。

  6.1 gcc、g++、gcj的設置

  要想使用GCC 3.4.0的gcc等命令,簡單的方法就是把它的路徑${destdir}/bin放在環境變量PATH中。我不用這種方式,而是用符號連接的方式實現,這樣做的好處是我仍然可以使用系統上原來的舊版本的GCC編譯器。

  首先,查看原來的gcc所在的路徑:

  % which gcc

  在我的系統上,上述命令顯示:/usr/bin/gcc。因此,原來的gcc命令在/usr/bin目錄下。我們可以把GCC 3.4.0中的gcc、g++、gcj等命令在/usr/bin目錄下分別做一個符號連接:

  % cd /usr/bin
  % ln -s ${destdir}/bin/gcc gcc34
  % ln -s ${destdir}/bin/g++ g++34
  % ln -s ${destdir}/bin/gcj gcj34

  這樣,就可以分別使用gcc34、g++34、gcj34來調用GCC 3.4.0的gcc、g++、gcj完成對C、C++、JAVA程序的編譯了。同時,仍然能夠使用舊版本的GCC編譯器中的gcc、g++等命令。

  6.2 庫路徑的設置

  將${destdir}/lib路徑添加到環境變量LD_LIBRARY_PATH中,最好添加到系統的配置文件中,這樣就不必要每次都設置這個環境變量了。

  例如,如果GCC 3.4.0安裝在/usr/local/gcc-3.4.0目錄下,在RH Linux下可以直接在命令行上執行或者在文件/etc/profile中添加下面一句:

  setenv LD_LIBRARY_PATH /usr/local/gcc-3.4.0/lib:$LD_LIBRARY_PATH

  7. 測試
 
  用新的編譯命令(gcc34、g++34等)編譯你以前的C、C++程序,檢驗新安裝的GCC編譯器是否能正常工作。

  8. 根據需要,可以刪除或者保留${srcdir}和${objdir}目錄。



小馬歌 2008-09-19 14:53 發表評論
]]>
怎樣用GDB調試程序http://www.fpcwrs.live/xiaomage234/archive/2008/06/19/209218.html小馬歌小馬歌Thu, 19 Jun 2008 09:55:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2008/06/19/209218.htmlhttp://www.fpcwrs.live/xiaomage234/comments/209218.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2008/06/19/209218.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/209218.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/209218.html簡單的可以這樣:
1,首先要打開core文件限制:ulimit -c unlimited
2,gdb app corefile
3,bt,顯示堆棧信息


有兩個關于如何調試的詳細頁面:
用GDB調試程序(zt) 
http://www.linuxsir.org/bbs/showthread.php?t=171156
http://www.trucy.org/blog/archives/eoiae/000087.html


小馬歌 2008-06-19 17:55 發表評論
]]>
google ctemplate庫簡介http://www.fpcwrs.live/xiaomage234/archive/2007/12/24/170174.html小馬歌小馬歌Mon, 24 Dec 2007 14:32:00 GMThttp://www.fpcwrs.live/xiaomage234/archive/2007/12/24/170174.htmlhttp://www.fpcwrs.live/xiaomage234/comments/170174.htmlhttp://www.fpcwrs.live/xiaomage234/archive/2007/12/24/170174.html#Feedback0http://www.fpcwrs.live/xiaomage234/comments/commentRss/170174.htmlhttp://www.fpcwrs.live/xiaomage234/services/trackbacks/170174.html linux下的web開發,動態頁面生成很費周折,通常是 利用fastcgi接受請求,然后返回頁面給請求端。
代碼邏輯和顯示邏輯寫在一起,是一件很痛苦的事情,c++里也有一個類似java中velocity的東東。
它的名字叫 ctemplate,出自大名鼎鼎的google。目前最新版本是 0.8.
它有四種變量表達方式:
1,簡單的值替換;
2,<#tag>和</tag>式的循環以及內嵌;
3,">file"式的include文件;
4,"!"開頭的注釋說明。

在c++里有了這個工具,能很大程度提高開發效率,方便不少。

小馬歌 2007-12-24 22:32 發表評論
]]>
魔法糖果闯关 老11选5快彩乐 体彩天津11选5 进哪里的工厂最赚钱 快乐扑克派 天天乐棋牌送现金 中彩票的号码 贵州11选5任三技巧 广西11选5走 游戏推广代理赚钱吗 福彩中奖查询 陕西11选5不开奖 做苦力赚钱难 浙江十一选五前三直遗漏一定牛 上证指数 七星体育 上海新11选5