2012年1月13日 星期五

MTU原理及相關問題分析

一、MTU的定義及相關概念:
Mtu即最大傳輸單元,全稱為Maximum Transmission Unit,是指通信協議的某一層上麵所能通過的最大數據包大小(以字節為單位)。由於定義的模糊性,在此也介紹幾個相關的名詞,MRU、PMTU、MSS和JUMBO FRAME,供大家甄別。
MRU即最大接收單元,全稱為Maximum Receive Unit,與MTU相對,稱為最大接收單元,目前也沒有權威的標準定義,但許多文章中有這個名詞。一台主機或路由器的MTU與MRU可以不一致。
PMTU,全稱為 path maximum transmission unit,即路徑MTU,把一條IP路徑上MTU的最小值稱為PMTU,PMTU是個理想化的概念,但目前業界沒有有效的手段來實現PMTU的發現和更新。`
MSS是OSI參考模型中四層的一個概念,即最大分段長度,全稱為TCP Maximum Segment Size,指TCP每次能夠傳輸的最大數據分段長度(以字節為單位),MSS一般比MTU小40字節。
Jumbo Frame(有些稱Giant Frame),網絡上會遇到jumbo frame的概念,cisco路由器的接口中也有這個參數,超過以太網標準長度1518字節的幀稱為jumbo frame。
二、MTU涉及主要原理:
1、常見網絡的MTU值:
IP網絡以包為單位進行信息傳遞,那麼,一次傳送多大的包合適、多大的包最高效就成為一個核心問題之一。MTU就是決定在什麼樣的物理網絡傳送多大數據包大的事實標準,不同類型網絡由於物理特性、發展階段不同,其MTU的默認值也不盡相同,以下是摘錄的各類網絡及其默認MTU值:
對於windows操作係統來講,其以太網網卡MTU默認為1500,但可以通過修改工具或修改注冊表進行修改,但隻能改小,不能改大,即隻能修改為小於或等於1500字節。
2、PMTU 發現過程:
對於一個基於網絡的應用來講,如果應用穿過網絡的MTU與PMTU相等,那麼應用穿過網絡的效率最高,或者說,應用通過主機網卡發出的最大數據包與PMTU越接近(指小於等於PMTU),應用穿過網絡的效率越高,原因是有效的避免了分片和重組。
為了達到這個目的,一些操作係統支持自動發現路徑MTU的功能,具體過程為:
路由器接口上收到一個報文長度大於本接口MTU值的報文,如果該報文被打上不分片的標記,將丟棄本報文,並且返回一個ICMP差錯報文,通知報文發起者丟棄原因。報文發起者將發送比較小的報文。通過多次上述報文協商,將得到對於某一個固定路徑上的最小Mtu值,這個過程叫做“Mtu Discovery”[詳見RFC1191]。
了解了MTU發現的原理,舉一個實例驗證PMTU變化過程:
在上圖所示實驗網絡中,由三層設備模擬PPPOE撥號,實現接入寬帶IP網。三層設備上行以太網口默認MTU為1482字節。抓包結果顯示如下:
將三層設備上行以太網口默認MTU改為1000字節。抓包結果顯示如下:
3、“PMTU”發現存在的問題:
由於互聯網上路由器或其它網絡設備的配置的無法統一規範,某些運營商或網站考慮到網絡安全和其它需要,有時會把ICMP報文過濾掉,此外,PMTU牽涉到主機、各類交換機、路由器、防火牆等網絡設備,這些主機和網絡設備沒有有效的手段實現PMTU的協商和交互,這樣Mtu Discovery不能正常運行,影響應用正常運行,即實質上目前沒有有效的手段來發現PMTU。
互聯網上的網絡設備,遇到MTU發現報文或必須將IP包分片但DF設置為1時,路由器可采用以下任一種方式(從網上摘錄):
發送符合 RFC 792 中最初定義的“ICMP Destination Unreachable-Fragmentation Needed and DF Set”消息,然後丟棄該包。原始消息格式中不包含有關轉發失敗的鏈路的 IP MTU 的信息。(導致PMTU無法正常發現)
? 發送符合 RFC 1191 中重新定義的“ICMP Destination Unreachable-Fragmentation Needed and DF Set”消息,然後丟棄該包。此新消息格式包含一個 MTU 字段,可指出轉發失敗的鏈路的 IP MTU。(PMTU可能會正常發現)

RFC 1191 定義了路徑 MTU (PMTU) 發現,它使得源和目的 TCP 對等方能夠動態地發現二者之間路徑的IP MTU,從而發現該路徑的 TCP MSS。一旦收到符合 RFC 1191 定義的“Destination Unreachable-Fragmentation Needed and DF Set”消息,TCP 就會將該連接的 MSS 調整為指定 IP MTU 減去 TCP 和 IP 報頭的大小。這樣,在該 TCP 連接上發送的後續包就不會超過最大大小,無需分段即可在該路徑上傳輸。
直接丟棄包。直接丟棄需分段但 DF 標記設置為 1 的包的路由器稱為 PMTU 黑洞路由器。
總之,PMTU的不可發現性,導致因MTU問題引起的應用係統無法正常運行情況時有發生。
4、超過MTU值的數據包分片、重組過程,:
IP包的格式如下:
IP包的分片重組牽涉到IP包頭的幾個重要字段,主要是標識符、標識位、偏移量,分別詳述如下:
標識符(Identification):在發送數據包前,發送主機給每個數據包一個ID值,放在16位的標識符字段中。此ID用於標識唯一的數據報或數據流。接收主機利用此ID對收到的數據報進行重組。當分片的IP數據報從源地址發送到目的地址的時候,由於網絡延遲或者不同的傳輸路徑的關係,在到達目的主機時,這些分片數據報並不總是有序的到達,而是處於一種無序狀態,因此,接收主機便用此ID判斷接收的這些分片數據報是否屬於同一個數據流,然後再進行重組。
標誌(Flags):第一個bit稱為R位,目前保留未用。第二個bit稱為DF位,Don't Fragment,“不分片”位,即如果將這一比特置1 ,表示上層應用不允許分片,IP層將不對數據報進行分片。第三個bit稱為MF位,More Fragment,“更多的片”。除了最後一個數據分片外,其他每個組成數據報的數據分片都要把該比特置1,表示有更多的分片。
偏移量(Framentation offset):13位的偏移量字段用來表示分段的數據報在整個數據流中的位置,即相當於分片數據報的順序號。發送主機對第一個數據報的偏移量置為0,而後續的分片數據報的偏移量則以網絡的MTU大小賦值。偏移量對於接收方進行數據重組的時候,這是一個唯一依據。對於分片的數據段(單位:字節)必須為8的整數倍,否則IP無法表達其偏移量。
了解了數據分片的幾個關鍵字段後,我們找一個實例說明分片過程(從網上摘錄):
在MTU為1500的以太網中,源主機如果需要通過UDP傳送3000字節的數據到目的主機,這時的分段情況如下所示(假定在同一網段):
此處需要注意的是對於分片1的報頭,相對於其他兩個分片的報頭要多8個字節UDP協議報頭的開銷。因此,在計算實際傳輸的數據淨載荷時,分片1要多減去8字節UDP報頭。最後,接收主機通過包ID、標識位、偏移量值將數據重組成完整的數據。
需要注意是,有些數據包在一次分片後,由於遇到更小MTU的網絡,還可能被繼續分片。為數據包分片和為數據包再次分片的區別在於網關處理MF位的不同。在一個網關為原來未分片的數據包分片時,除了末尾的數據包片,它將其餘所有分片上的MF位置為1,最後一片為0。然而,當網關為一個非末尾的數據包片再次分片時,它會把生成的所有子分片中的MF位全部調為1,因為所有這些子分片都不可能是整個數據包末尾的數據包。分片中標誌字段的M值取決於該分片是否是原始分組的最後一片,而片偏移量也是相對於原始分組的偏移量。
三、MTU對上層應用的影響:
1、TCP、UDP等上層應用傳輸效率的高低與PMTU有密切聯係。
對TCP來講,其傳輸效率與MSS的合適大小密切相關,而MTU是決定MSS大小的唯一因素。MSS的大小會在TCP連接建立階段進行協商,具體協商過程如下:
TCP client發出SYN報文,其中option選項填充的MSS字段一般為(MTU-IP頭大小-TCP頭大小),同樣TCP server收到SYN報文後,會發送SYN+ACK報文應答,option選項填充的mss字段也為(MTU-IP頭大小-TCP頭大小);協商雙方會比較SYN和SYN+ACK報文中MSS字段大小,選擇較小的MSS作為發送TCP分片的大小。
理論上,如果MSS與合適的MTU即PMTU(我認為合適的PMTU為路徑上可不分片通過的最大MTU)匹配,TCP傳輸效率最高,因為免去了分片、重組等工作。但由於PMTU發現過程無法保證,導致最終發現的MTU可能並不合適路徑傳輸,造成部分應用的分片和重組,降低了傳輸效率。
2、部分網絡設備對特定應用的分片報文處理能力弱,造成部分應用故障。
目前,國內寬帶IP網絡多數用戶采用PPPOE撥號方式實現業務接入。位於電信運營商內側的BAS設備用於終結PPP連接,實現用戶接入彙聚。實際應用中,部分BAS設備對VPDN等特定應用的分片、重組機製不健全,造成部分基於VPDN方式的業務速度緩慢。如:筆者曾發現過某廠商BAS設備ME60當TSU板未啟用時,其於L2TP隧道的VPDN撥號應用,經常出現業務訪問緩慢,甚至不通的情況,當啟用TSU板後,業務恢複正常(經了解,TSU板是專門用於基於隧道或其它協議數據包分片處理功能)。某廠商設備SE800,承載了基於L2TP隧道的VPDN撥號應用後,部分應用可以使用,部分應用無法正常運行。
3、部分網絡安全設備對分片的重組過程進一步影響業務應用。
原則上,當某個網絡應用業務在傳輸過程中發生分片後,隻有發送端和接收端會進行分片的重組,這已經影響到業務運行速度。當分過片的IP包經過網絡監測設備、安全係統等設備時,基於安全和其它特定目的,有些設備要求完成數據包的分片和重組,會進一步延緩應用的運行速度,甚至導致部分應用無法使用。
四、MTU常見問題小結(摘錄自網絡):
1、為何有些共享上網的路由器的網絡設備以太網口MTU值不是1500?
有些通過共享路由器PPPOE撥號上網的路由器上連口(與ADSL調製器互聯的以太網口)MTU為1492,因為PPP報文占據了8個字節,導致承載數據信息的IP報文大小變成了1492。
2、為什麼在思科路由器上GRE接口的默認MTU為1476?
因為GRE會重新封裝一個IP包頭,以及加上GRE的4字節頭部,一共是24個字節。這樣總的用於應用的IP包的長度為1476。
3、為什麼支持VLAN標記的接口MTU要大於1514字節?
由於VLAN的原理是在以太網的幀頭部加入了4個字節的VLAN TAG信息,因此在支持802.1q標記的鏈路的接口上要求MTU不小於1518字節,才能保證淨荷為1500的數據包順利通過該接口。
4、MTU和各種VPN的關係有無規律性?
因VPN的實現需要對原有的IP或者是TCP/UDP數據進行封裝,因此也就增加了數據包或數據幀的總長度,這樣也就導致了VPN 承載的數據淨荷值的減小,具體減少的數值與不同的VPN類型相關。
即:MTU=經VPN封裝後數據包長度-VPN封裝包頭長度
5、對於因MTU問題引起的應用係統故障現象有哪些?處理思路是什麼?
因MTU引的網絡問題表現各種各樣,如:有些遊戲,經常卡機。有的網站,部分網頁可以打開,部分網頁打不開。總的來講,如果網絡不丟包,而網絡應用時快時慢,或部分應用無法使用,很大程度上可以定位為MTU問題。
對於MTU引起的問題,處理思路為:要麼增加網絡中最細管道的MTU值,要麼減少終端應用發現的數據包大小。對於終端的MTU,會有各種各樣的工具或方法進行修改。


http://www.autooo.net/papers/paper/2011-10-25/81611.html

2012年1月12日 星期四

Token-pasting operator(##) in C

我們都知道C語言define前端處理假指令用來定義變數、字串或幾行的原始碼(統稱為巨集, Macro)。

當某一巨集被定義為參數帶入之巨集, 我們最常用的就是把該參數當作變數或指標來使用,如下所示:
#define INC_IDX(val, size) (++val % size) -> 以參數為變數之值帶入此巨集
#define GET_DATA(ptr) (ptr->data) -> 以參數為指標帶入此巨集

然而,我們可以把巨集所帶入的參數當作為識別子(Token)的一部分,如下所示:


#include 

int class_num = 9;
int class_stds = 10;

#define GET_MEMBER(postfix) class_ ## postfix

int main(void)
{
     printf("class_num: %d\n", GET_MEMBER(num));
     printf("class_stds: %d\n", GET_MEMBER(stds));
     return 0;
}


如上列所示,class_num可以經由GET_MEMBER巨集(帶入部份識別子, 也就是num)取得,此參數傳遞方法稱為Token-pasting operator,這是一個非常好用的方法, 提供給大家參考。

The Stringify Operator (#) in C

繼上次提到Token-pasting operator (##) in C文章後, 這次講一下在C語言define巨集參數使用的另一種方法, 在參數名稱前加一個#運算子, 此運算子代表所帶入的參數會被編譯器視為"字串", 所以#運算子又被稱為字串化運算子, 底下為一簡單的例子:


#define GET_RESULT(exp) printf(#exp "=%d\n", exp)

int main(void)
{
    GET_RESULT(3+2);
    return ;
}


ref:http://adrianhuang.blogspot.com/2009/05/gnu-c.html

2012年1月9日 星期一

mmap -- mapped memory

mapped memory 的行程間通訊機制,是透過「shared file」來做訊息的傳遞;將 shared file mapping 到 process address space 的 system call 為 'mmap()'。以下是其中一個簡單的程式範例


#include
#include
#include
#include
#include
#define FILE_LENGTH 0x400

int main(int argc, char *argv[])
{
   int fd;
   void *map_memory;

   /* Open a file to be mapped. */
   fd = open("/tmp/shared_file", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
   lseek(fd, FILE_LENGTH+1, SEEK_SET);
   write(fd, "", 1);
   lseek(fd, 0, SEEK_SET);

   /* Create map memory. */
   map_memory = mmap(0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd, 0);
   close(fd);

   /* Write to mapped memory. */
   if (strlen(argv[1]) < FILE_LENGTH)
      sprintf((char *)map_memory, "%s", argv[1]);

   sleep(10000);

   exit(0);
}
Linux允許將檔案對映到記憶體中。如此可以產生一個在檔案資料及記憶體資料一對一的對映。
當程式將檔案 mapping 到自己的記憶體空間(process address space)時,mmap system call 便會建立相對應的 VMA 區段;觀念上來說,只要透過 mmap system call 將檔案 mapping 到記憶體,便會產生對應的 VMA。
修改一下範例程式,讓程式在結束前暫停 10000 秒鍾,並做觀察
$ ./my_mmp hello&
[1]   xxxx
$ cat /proc/xxxx/maps
就可以看到結果



引用

2012年1月5日 星期四

printk printf color for console debug

color

30(黑色)、31(红色)32(绿色) 33(黄色)34(蓝色)35(洋红)36(青色)、37(白色)

#ifdef STH_DEBUG
#define sth_dbg_printstring(key, value) printf("\033[1;31m%s = %s\033[0m\n", key, value)
#define sth_dbg_printint(key, value) printf("\033[1;31m%s = %d\033[0m\n", key, value)
#else
#define sth_dbg_printstring(key, value)
#define sth_dbg_printint(key, value)
#endif

2011年12月29日 星期四

Linux下以pptp方式撥入VPN網路中


  1、下載pptp用戶端

  wget http://nchc.dl.sourceforge.net/sourceforge/pptpclient/pptp-1.7.1.tar.gz

  2、解壓

  tar zxvf pptp-1.7.1.tar.gz

  3、編譯和安裝

  make; make install

  4、編輯設定檔,設定撥號名為mypptp

  vim /etc/ppp/peers/mypptp

  內容如下:
remotename mypptp
linkname mypptp
ipparam mypptp
pty "pptp vpnserverip --nolaunchpppd "
name myaccount
usepeerdns
require-mppe
refuse-eap
noauth
file /etc/ppp/options.pptp

其中,myaccount為用戶名

  5、編輯/etc/ppp/chap-secrets,加入用戶名和帳號,這裡假設myaccount的密碼為mypassword

myaccount * mypassword *
  6、撥號,運行以下命令

/usr/sbin/pppd call mypptp logfd 1 updetach

  如果以上設定檔正確無誤,則可正常撥入虛擬網管的pptp VPN網路中了,此時如果用ifconfig查看連接情況,可以看到多了一條ppp連接,並能正確分到IP位址了。

  7、添加路由

  雖然已經撥號上來了,但此時,如果你要訪問你的虛擬區域網路資源,你必需添加一條路由才行,這裡假設你撥號上來的連接名為ppp0,並此你的虛擬區域網路的IP段為192.168.163.0,那麼,你需要加入以下命令:

route add -net 192.168.163.0 netmask 255.255.255.0 dev ppp0

  至此,在Linux系統下以pptp方式撥入虛擬網管的VPN網路中了。

  以上操作過程在Ubutun 8.1Fedora 78910下操作通過。

 PS:如果在撥號時報以下錯誤:

/usr/sbin/pppd:pty option precludes specifying device name

請檢查pppd的版本,不可低於2.3.7

檢查etc/ppp/optoins檔,該檔不能為空。


------------------------------------------------------------------------------------------------------------
若出現Couldn't set tty to PPP discipline: Invalid argument 錯誤訊息
則須在kernel 加入 ppp_mppe , ppp_async 這兩個module後即可



    Device Drivers --->
        Network device support --->
            <*>   PPP (point-to-point protocol) support
            <*>     PPP support for async serial ports
            <*>     PPP MPPE compression (encryption) (EXPERIMENTAL)

Linux下以l2tp方式撥入VPN網路

  1、下載l2tp用戶端

  wget ftp://ftp.xelerance.com/xl2tpd/xl2tpd-1.1.12.tar.gz

  2、解壓

  tar zxvf xl2tpd-1.1.12.tar.gz

  3、進入解壓目錄,並編譯和安裝

  cd xl2tpd-1.1.12

  make; make install

  4、在/etc目錄下建立xl2tpd目錄,並在該目錄下建立l2tp的設定檔xl2tpd.conf

  mkdir /etc/xl2tpd

  vim /etc/xl2tpd.conf

  xl2tpd.conf檔的內容如下:

[global]
port = 1701

[lac myvpn]
lns = vpnserver ip or domain name
refuse pap = yes
require authentication = no
name = myaccount
ppp debug = yes
pppoptfile = /etc/ppp/options.myvpn_l2tp

這裡假設你的vpn的帳號為myaccount

  5、在/etc/ppp目錄下建立options.myvpn_l2tp文件。

  vi /etc/ppp/options.myvpn_l2tp

該檔的內容如下:

asyncmap 0
noauth
crtscts
lock
hide-password
modem
netmask 255.255.255.0
proxyarp
lcp-echo-interval 30
lcp-echo-failure 4
ipcp-accept-local
ipcp-accept-remote

  6、在/etc/ppp/chap-secrets檔中加入myaccount的密碼:

myaccount * mypassword *

  7、運行xl2tpd程式。

  xl2tpd

  如果你需要看到程式的輸出資訊,可以在後面加上參數D,如:

  xl2tpd -D

此時輸出上可能會出現This binary does not support kernel L2TP. 不用理會它,沒什麼影響,但是如果報告:Open_controlfd: Unable to open /var/run/xl2tpd/l2tp-control for reading.的錯誤,則需要手動的在/var/run目錄下建立一個名為xl2tpd的目錄:

mkdir /var/run/xl2tpd

  8、新開一終端,使用以下方法開始撥號:

echo "c vpnlife" > /var/run/xl2tpd/l2tp-control

如果配置正確無誤,則可正常撥入虛擬網管的l2tp VPN網路中了,此時如果用ifconfig查看連接情況,可以看到多了一條ppp連接,並能正確分到IP位址了。

  9、添加路由

  雖然已經撥號上來了,但此時,如果你要訪問你的虛擬區域網路資源,你必需添加一條路由才行,這裡假設你撥號上來的連接名為ppp0,並此你的虛擬區域網路的IP段為192.168.163.0,那麼,你需要加入以下命令:

route add -net 192.168.163.0 netmask 255.255.255.0 dev ppp0

  至此,在Linux系統下以l2tp方式撥入虛擬網管的VPN網路中了。

  以上操作過程在Ubutun 8.1Fedora 78910下操作通過。

2011年12月24日 星期六

C/C++之指標 (pointer),參考 (reference) 觀念整理與常見問題



目錄
  1. 何謂指標 (pointer)? 何謂參考 (reference)?
  2. call by value? call by address (或call by pointer)? call by reference? -- swap(int* a, int* b) v.s. swap (int &a, int &b)
  3. pointer to pointer, reference to pointer (int** v.s. int*&)
  4. function pointer
  5. void ** (*d) (int &, char **(*)(char *, char **))....如何看懂複雜的宣告…
1. 何謂指標 (pointer)? 何謂參考 (reference)?
我們先談指標 (pointer)。指標,其實也只是一個變數,只是這個變數的意義是:指向某個儲存位址。很玄嗎? 一點也不。下面這張圖就可以輕易的看出指標為何物。

圖中,a, b, c, d, p1, p2都是一般的變數,儲存在記憶體 (memory) 中。其中,p1變數所記載的值是變數a的記憶體 (memory) 位址,而p2則記載著b的記憶體位址,像這樣的狀況,我們就稱p1是一個指向a的指標,相同的,p2是一個指向b的指標。
在C/C++中,我們用下面的式子來表示這個關係:
int *p1 = &a;
int *p2 = &b;

其中的&,稱為address of (取址)。即,p1 = address of a,p2 = address of b。
另一個符號*,代表的意義是指標。
int *p1
要由後往前閱讀來瞭解它的意義:p1 is a pointer points to an integer。因此,
int *p1 = &a;
這整行,我們可以看成:p1 is a pointer points to integer variable a,即:p1是一個指標,指向整數變數a。

且讓我們暫時打住指標的討論,轉頭看看參考 (reference)。
參考,可以想像成是一個變數或物件的別名 (alias)。通常,當函式 (function) 的參數 (parameter) 在函式中會被修改,而且要把這個修改結果讓呼叫函式的部份繼續使用,我們會用參考來當參數傳入函式中。
讓我們看看下面的例子:
void swap(int &a, int &b){
int tmp = a;
a = b;
b = tmp;
}

當其他程式呼叫此交換程式時,只要直接寫swap(x, y)就能交換x與y的值。在這裡,a和b為x與y的別名,即:a就是x,b就是y,如同美國國父就是華盛頓一樣。a和b不是x和y的複製品,任何做用在a與b上的動作都會反應在x與y上面,反之亦然。

指標和參考之所以難懂,有很大一部份的原因是符號上的陌生所致。加上&既能用於取址又能用於參考,容易造成初學者的混淆。下面我們提供幾個建議來幫助各位看懂這些符號。
  • 把int *p視為 int* p。
    把int和*連在一起看,當作是一種型態叫做 "指向整數之指標",要比int *p自然得多。同樣的方式也可以套在char* p或void* p等。但要注意的是下面的狀況:
    int* p, q;

    不要把這行誤解成p, q都是指向int之指標,事實上,q只是一個int變數。上面這行相當於
    int *p, q;

    int *p; int q;

    如果p, q都要宣告成指向int之指標,應寫成:
    int *p, *q

    或者干脆分兩行寫:
    int* p;
    int* q;
  • &前面有資料型態 (ex: int &),則為參考&前面有等號 (ex: int* p = &a),則為取址
    由於&同時具有多種意義,因此容易造成混淆。這裡列出的這個方法,可以幫助弄清楚每個&的意義。
2. call by value? call by address (或call by pointer)? call by reference? -- swap(int* a, int* b) v.s. swap (int &a, int &b)
JAVA中的reference與C++的reference意義上並不相同,卻使用同一個字,這也是reference容易造成混淆的原因。在此,我們暫不考慮JAVA中reference的觀念 (關於java中reference的觀念,請參考Reference in JAVA -- 淺談java的指標),純粹把主題放在C/C++上。
呼叫副函式時,call by value, address, 或reference是三種不同的參數傳遞方式。其意義如下:
  • call by value
    假設函式A呼叫函式B(p, q),則B中的p和q是「複製」自函式A所傳入的參數,B中對p, q所做的任何運算都不會影響到A中的p和q,因為B執行完後,並不會把複製的p, q存回到A中。這種參數傳遞方式,我們稱之為call by value。
    以swap這個常見的函式為例,若swap寫成下面的樣子:
    void swap(int a, int b){
    int tmp = a;
    a = b;
    b = tmp;
    }

    則呼叫
    swap(x, y)
    後,x和y的值並不會有變化。
  • call by address (或call by pointer)
    利用指標來做參數傳遞,這種方法骨子裡仍是call by value,只不過call by value的value,其資料型態為指標罷了。我們同樣看看用call by address來寫swap交換兩個integer的例子。
    void swap(int* a, int* b){
    int tmp = *a;
    *a = *b;
    *b = tmp;
    }

    呼叫swap時,要寫成swap(&x, &y)。呼叫swap時,x的指標 (x的儲存位置) 與y的指標 (y的儲存位置) 會被複製一份到swap中,然後把該位置內所記載的值做更換。swap結束後,&x (address of x) 和&y (address of y) 依然沒變,只是address of x所記錄之變數值與address of y所記錄之變數值交換了。因為&x 和&y 其實是利用call by value在傳,因此,call by address其實骨子裡就是call by value
  • call by reference
    這是C++才加進來的東西,C本身並沒有call by reference。call by reference基本上是把參數做個別名 (alias),以下面的swap為例:
    swap(int &a, int &b){
    int tmp = a;
    a = b;
    b = tmp;
    }

    未來使用時,只要呼叫swap(x, y),就可以讓x和y的值交換。在這個例子中,a 就是 x, b就是 y。這個觀念在上一節已經提過,在此不再贅述。
3. pointer to pointer, reference to pointer (int** v.s. int*&)
當我們用call by pointer (或address) 來傳遞參數時,被呼叫的函式複製一份pointer的值過去。但是,當我們想在函式內改變pointer的值 (而非pointer所指向之變數的值),而且改變的效果要能在函式外看得到時,call by pointer就不足夠用了。此時應該用的是"call by pointer to pointer"或"call by reference to pointer"。我們先看下面的例子:
int g_int = 0;
void changePtr(int* pInt){
pInt = &g_int;
}
void main(){
int localInt = 1;
int* localPInt = &localInt;
changePtr(localPInt);
printf("%d\n", *localPInt);
}

在這個例子中,印出來的數字仍然會是localInt的1,因為changPtr中的pInt是由localPInt「複製」過去的,對pInt做改變並不會反應到localPInt身上。
我們先用pointer to pointer對localPInt做改變,請看下例。
int g_int = 0;
void changePtr(int** pInt){
*pInt = &g_int;
}
void main(){
int localInt = 1;
int* localPInt = &localInt;
changePtr(&localPInt);
printf("%d\n", *localPInt);
}

本例中,印出來的數字會是g_int的0。changePtr函式中的pInt是由&localPInt複製所得,因此對pInt做改變並不會影響main中的&localPInt (資料型態:pointer to pointer to integer)。但在changePtr函式中我們改變的對象是pInt所指向的內容,因此這項改變在main中會顯示出來。

同樣的功能,我們也可改用reference to pointer來完成。但同樣切記,reference是C++才有的功能,因此reference to pointer也只能在支援C++的環境中使用。
int g_int = 0;
void changePtr(int* &refPInt){
refPInt = &g_int;
}
void main(){
int localInt = 1;
int* localPInt = &localInt;
changePtr(localPInt);
printf("%d\n", *localPInt);
}

這段程式印出來的數字會是0。因為在changePtr中,我們宣告的參數型態為int* &,即:reference to pointer to integer。因此,main中的localPInt與changePtr函式中的refPInt其實是「同一件東西」。

另一種常見的混淆是pointer array (指標陣列) 與pointer to pointers,因為兩種都可以寫成**的型式。如,int**可能是pointer to pointer to integer,也可能是integer pointer array。但pointer array的觀念相對來講要簡單且直觀許多,這裡我們就暫不花篇幅敘述。常見的例子:main(int argc, char** argv)其實應該是main(int argc, char* argv[])。

4. function pointer
變數的指標指向變數的位址,同樣的,function pointer (函式指標) 也是指向函式的位址的指標。
函式指標的加入,讓C/C++的符號更複雜,也使更多人望之而卻步。在說明函式指標的用途前,我們先直接由語法來看看函式指標該怎麼宣告、怎麼理解。
假設有個函式長成下面的樣子:
void func1(int int1, char char1);

我們想宣告一個能指向func1的指標,則寫成下面這樣:
void (*funcPtr1)(int, char);

這樣的寫法應理解成:funcPtr1是一個函數指標,它指向的函數接受int與char兩個參數並回傳void。如果今天有另一個函式長成
void func2(int int2, char char2);

則funcPtr1也能指向func2。
指標指向的方法,寫成下面這樣:
funcPtr1 = &func1;

取址符號省略亦可,效果相同:
funcPtr1 = func1;

若欲在宣告時就直接給予初值,則寫成下面這樣:
void (*funcPtr1)(int, char) = &func1;    //&亦可省略


stdlib.h中提供的qsort函式是函式指標最常見的應用之一。此函式之prototype長得如下:
void qsort(void* base, size_t n, size_t size, int (*cmp)(const void*, const void*));

其中的int (*cmp)(const void*, const void*) 就使用到函式指標。

函式指標常見的使用時機是multithread時。函數指標負責把函數傳進建立執行緒的API中。
另外,callback function也是常使用函式指標的地方。所謂callback function即:發生某事件時,自動執行某些動作。在event driven的環境中,便時常使用callback function來實現此機制。
事實上,函式指標還能讓C語言實作polymorphism。但礙於篇幅,在此不再詳述。

5. void ** (*d) (int &, char **(*)(char *, char **))....如何看懂複雜的宣告…
在這裡,我們介紹兩種方式來看懂複雜的宣告。第一種要判斷的是:常數與指標混合使用時,到底const修飾的是指標還是指標所指的變數? 第二種是面對如標題所示這種複雜的宣告時,我們要怎麼讀懂它。

5.1 常數與指標的讀法
const double *ptr;
double *const ptr;
double const* ptr;
const double *const ptr;

以上幾個宣告,到底const修飾的對象是指標,還是指標所指向的變數呢?
其實,關鍵在於:*與const的前後關係!
當*在const之前,則是常數指標,反之則為常數變數。因此,
const double *ptr;    // ptr指向常數變數
double *const ptr;    // ptr是常數指標
double const* ptr;    // ptr指向常數變數
const double *const ptr;    // 指向常數變數的常數指標

事實上,在The C++ Programming Language中有提到一個簡單的要訣:由右向左讀!!讓我們用這個要訣再來試一次。
const double *ptr;    // ptr is a pointer points to double, which is a constant
double *const ptr;    // ptr is a constant pointer points to double
double const* ptr;    // ptr is a pointer points to constant double
const double *const ptr;    // ptr is a constant pointer points to double, which is a constant

結果完全相同 :-)

5.2 複雜宣告的讀法 void ** (*d) (int &, char **(*)(char *, char **)).......
其實閱讀C/C++中複雜的宣告有點像是讀英文的長句子,看多了,自然知道句子是怎麼構造出來的。
但對於句子還不熟的人,難免得藉助文法來拆解一個句子。關於C語言複雜宣告的解析文法,最令我印象深刻的,莫過於印度工程師Vikram的"The right-left rule"。他是這麼說的:
「從最內層的括號讀起,變數名稱,然後往右,遇到括號就往左。當括號內的東西都解讀完畢了,就跳出括號繼續未完成的部份,重覆上面的步驟直到解讀完畢。」
舉個例子:void ** (*d) (int &, char*)依下面方式解讀:
1. 最內層括號的讀起,變數名稱: d
2. 往右直到碰到) : (空白)
3. 往左直到碰到( :是一個函數指標
4. 跳出括號,往右,碰到(int &, char*): 此函式接受兩個參數:第一個參數是reference to integer,第二個參數是character pointer。
5. 往左遇上void **: 此函式回傳的型態為pointer to pointer to void。
==> d是一個函式指標,指向的函式接受int&和char*兩個參數並回傳void**的型態。
如何,是不是好懂很多了呢?

標題中的void ** (*d) (int &, char **(*)(char *, char **))其實和上面的例子幾乎一樣,只是函式的第二個參數又是一個函式指標,接受char*和char**兩個參數並回傳char**的型態。

行文至此,把指標和參考的常見問題與混淆大致地提了一些。希望能讓使用C/C++的人在面對或使用指標、參考、或取址時,不再有疙瘩在心中。文中若有不足或錯誤之處,也請高手不吝指教囉。:-)

How to repair and clone disk with ddrescue

  ddrescue  is a tool that can be used to repair and clone disks on a  Linux system . This includes hard drives, partitions, DVD discs, flas...