一、中斷機制
1、實現中斷響應和中斷返回
當CPU收到中斷請求后,能根據具體情況決定是否響應中斷,如果CPU沒有更急、更重要的工作,則在執行完當前指令后響應這一中斷請求。CPU中斷響應過程如下:首先,將斷點處的PC值(即下一條應執行指令的地址)推入堆棧保留下來,這稱為保護斷點,由硬件自動執行。然后,將有關的寄存器內容和標志位狀態推入堆棧保留下來,這稱為保護現場,由用戶自己編程完成。保護斷點和現場后即可執行中斷服務程序,執行完畢,CPU由中斷服務程序返回主程序,中斷返回過程如下:首先恢復原保留寄存器的內容和標志位的狀態,這稱為恢復現場,由用戶編程完成。然后,再加返回指令RETI,RETI指令的功能是恢復PC值,使CPU返回斷點,這稱為恢復斷點。恢復現場和斷點后,CPU將繼續執行原主程序,中斷響應過程到此為止。
2、實現優先權排隊
通常,系統中有多個中斷源,當有多個中斷源同時發出中斷請求時,要求計算機能確定哪個中斷更緊迫,以便首先響應。為此,計算機給每個中斷源規定了優先級別,稱為優先權。這樣,當多個中斷源同時發出中斷請求時,優先權高的中斷能先被響應,只有優先權高的中斷處理結束后才能響應優先權低的中斷。計算機按中斷源優先權高低逐次響應的過程稱優先權排隊,這個過程可通過硬件電路來實現,亦可通過軟件查詢來實現。
3、實現中斷嵌套
當CPU響應某一中斷時,若有優先權高的中斷源發出中斷請求,則CPU能中斷正在進行的中斷服務程序,並保留這個程序的斷點(類似於子程序嵌套),響應高級中斷,高級中斷處理結束以后,再繼續進行被中斷的中斷服務程序,這個過程稱為中斷嵌套。如果發出新的中斷請求的中斷源的優先權級別與正在處理的中斷源同級或更低時,CPU不會響應這個中斷請求,直至正在處理的中斷服務程序執行完以后才能去處理新的中斷請求。
二、ANSI C 中斷處理機制分析
對於一般的C語言愛好者而言,就如何在C中使用中斷例程這一問題應該已經非常熟悉,例如,我們可以通過int86()函數調用13H號中斷直接對磁盤物理扇區進行操作,也可以通過INT86 ( )函數調用33H號中斷在屏幕上顯示鼠標光標等。其實,13H號也好,33H號也好,它們只不過就是一些函數,這些函數的參數通過CPU的寄存器傳遞。中 斷號也只不過是間接地指向函數體的起始內存單元,說它是間接的,也就是說,函數的起始段地址和偏移量是由中斷號通過一種方法算得的(具體如何操作,下面會作解釋)。如此一來,程序員不必要用太多的時間去寫操作硬件的程序了,只要在自己的程序中設置好參數,再調用BIOS或DOS提供的中斷服務程序就可以 了,大大減小了程序開發難度,縮短了程序開發周期。那么中斷既然是函數,就可以由用戶任意的調用、由用戶任意地編寫。
計算機內存的前1024個字節(偏移量00000H到003FFH)保存着256個中斷向量,每個中斷向量占4個字節,前兩個字節保存着中斷服務程序的入 口地址偏移量,后兩個字節保存着中斷程序的入口段地址,使用時,只要將它們分別調入寄存器IP及CS中,就可以轉入中斷服務程序實現中斷調用。每當中斷發 生時,CPU將中斷號乘以4,在中斷向量表中得到該中斷向量地址,進而獲得IP及CS值,從而轉到中斷服務程序的入口地址,調用中斷。這就是中斷服務程序通過中斷號調用的基本過程。在計算機啟動的時候,BIOS將基本的中斷填入中斷向量表,當DOS得到系統控制權后,它又要將一些中斷向量填入表中,還要修 改一部分BIOS的中斷向量。有一部分中斷向量是系統為用戶保留的,如60H到67H號中斷,用戶可以將自己的中斷服務程序寫入這些中斷向量中。不僅如此,用戶還可以自己更改和完善系統已有的中斷向量。
在C語言中,提供了一種新的函數類型interrupt,專門用來定義中斷服務程序,比如我們可以寫如下的中斷服務程序:
/*例1:中斷服務程序*/
- <span style="font-size:18px;"> void interrupt int60()
- {
- puts("This is an example");
- } </span>
該中斷的功能就是顯示一個字符串,為什么不用printf ( )函數呢?這就牽涉到DOS的重入問題,后面將作一些介紹。
一個簡單的中斷服務程序寫好了,如何把它的函數入口地址填寫到中斷向量表中,以便在產生中斷的時候能轉入中斷服務程序去執行呢?這里要用到setvect ( )和getvect ( )函數。setvect ( )有兩個參數:中斷號和函數的入口地址,其功能是將指定的函數安裝到指定的中斷向量中,getvect ( )函數有一個參數:中斷號,返回值是該中斷的入口地址。在安裝中斷以前,最好用disable ( )函數關閉中斷,以防止在安裝過程中又產生新的中斷而導致程序運行混亂,待安裝完成后,再用enable ( )函數開放中斷,使程序正常運行。現在我們可以把上面的例子再豐富一下:
/*例2:中斷服務程序的編寫、安裝和使用*/
- <span style="font-size:18px;"> #include <dos.h>
- #include <stdio.h>
- #ifdef __cplusplus
- #define __ARGU ...
- #else
- #define __ARGU
- #endif
- void interrupt int60 (__ARGU) /*中斷服務函數*/
- {
- puts("This is an example");
- }
- void install (void interrupt (*fadd)(__ARGU),int num) /*安裝中斷*/
- {
- disable(); /*關閉中斷*/
- setvect(num, fadd); /*設置中斷*/
- enable(); /*開放中斷*/
- }
- void main()
- {
- install (int60,0x60);/*將int60函數安裝到0x60中斷*/
- geninterrupt (0x60); /*人為產生0x60號中斷*/
- }
- </span>
有一定經驗的讀者很容易得到該程序的執行結果:在屏幕上顯示“This is an example!”。
編寫、安裝中斷服務程序的方法就介紹這些。下面再淺談一下內存駐留程序(TSR)的編寫和使用。在C語言中,可以用keep ( )函數將程序駐留內存。這個函數有兩個參數:status和size。size為駐留內存長度,可以用size=_SS+_SP/16-_psp得到,當然這也是一種估算的方法,並不是精確值。函數執行完以后,出口狀態信息保存在status中。比如,對於上面的例子,將“geninterrupt (0x60);”改寫成“keep(0,_SS+_SP/16-_psp);”后再執行程序,這一段程序就被駐留,此后在其它的任何軟件或程序設計中,只要用到了60H號中斷,就會在屏幕上顯示“This is an example!”的字樣。要恢復系統對60H號中斷的定義,只能重新啟動計算機。
像上面的例子其實還很不完善,它沒有考慮DOS系統環境的狀態、沒有考慮程序是否已經駐留內存、沒有考慮退出內存駐留等問題。對於第二個問題還是很容易解決的:執行程序一開始就讀取某一函數中斷入口地址(如63H號中斷)判斷是否為空(NULL),如果為空就先將該地址置為非空再駐留內存,若為非空則表示 已經駐留並退出程序。這一步判斷非常重要,否則將會因為重復駐留占用過多內存空間而最后造成系統崩潰。至於其它兩個問題,在此不多作說明,有興趣的讀者可以參考一些有關書籍。
不僅如此,我們還可以通過在DOS下使用熱鍵(Hotkey)來調用內存駐留程序。比如將《希望漢字系統》自帶的《希望詞典》駐留內存后,在任意時刻按下 Ctrl+F11鍵,就能激活程序,出現詞典界面。微機的鍵盤中有一個微處理芯片,用來掃描和檢測每個按鍵的按下和釋放狀態。大多數按鍵都有一個掃描碼,告知CPU當前的狀態,但一些特殊的鍵如PrintScreen、Ctrl+Break等不會產生掃描碼,而直接產生中斷。正因為如此,我們可以將Ctrl+Break產生的中斷號指向我們自己寫好的程序入口地址,那么當按下Ctrl+Break后,系統就會調用我們自己的程序去執行,這實際上也就是修改了Ctrl+Break的中斷向量。至於其它按鍵激活程序則可以利用9H號鍵盤中斷捕獲的掃描碼來實現,在此不多作說明。例如,執行下面的程序后,退回DOS系統,在任意的時候按下Ctrl+Break后,屏幕的底色就會變成紅色。
/*例3:中斷服務程序編寫、安裝和使用,內存駐留*/
- <span style="font-size:18px;"> #include <dos.h>
- #include <conio.h>
- #ifdef __cplusplus
- #define __ARGU ...
- #else
- #define __ARGU
- #endif
- void interrupt newint(__ARGU); /*函數聲明*/
- void install (void interrupt (*fadd)(__ARGU), int num);
- int main()
- {
- install (newint,0x1b); /*Ctrl+Break中斷號:1BH*/
- keep(0,_SS+(_SP/16)-_psp); /*駐留程序*/
- return 0;
- }
- void interrupt newint(__ARGU)
- {
- textbackground(4); /*設置屏幕底色為紅色*/
- clrscr(); /*清除屏幕*/
- }
- void install (void interrupt (*fadd)(__ARGU), int num)
- {
- disable();
- setvect(num,fadd); /*設置中斷*/
- enable();
- }
- </span>
由於13H號中斷是BIOS提供的磁盤中斷服務程序,對於DOS下的應用程序,它們的存盤、讀盤功能都是通過調用這一中斷來實現的。有許多DOS下的病毒就喜歡修改13H號中斷來破壞系統,例如,修改13H號中斷服務程序,將其改成:
/*例4:病毒體程序偽代碼*/
- <span style="font-size:18px;"> void interrupt new13(__ARGU)
- {
- if (病毒發作條件成熟)
- { 修改入口參數指向病毒程序入口地址;
- 執行病毒代碼;
- }
- 調用原來的13H中斷;
- }
- </span>
只要當任一軟件(如EDIT.COM等)對磁盤有操作並且病毒發作條件成熟時,病毒就被激活。當然,這樣做會導致可用內存空
三、Linux中斷編程
在Linux下,硬件中斷被稱為IRQs[Interrupt Requests (這是Linux起源的Intel架構上的標准術語)的縮寫]。有兩種IRQs,短的和長的。一個短的IRQ預期占用非常短的一段時間,在那期間,機器的剩余部分被阻塞,沒有其他的中斷將被處理。長的IRQ占用的時間長些,在那期間其他中斷有可能發生(但不能是來自同一設備)。只要是可能的,聲明一個長中斷是較好的。
當CPU接收到一個中斷,它停止它正在做的任何事情(除非它正在處理一個更重要的中斷,在那種情況下,它將處理完那個中斷后才來處理現在的這個),在堆棧中保存某些參數並調用中斷處理程序。這意味着在中斷處理程序自身中有些東西是不能允許的,因為系統處於一種未知的狀態。解決的辦法是中斷處理程序馬上做完需要做的,通常是從硬件里面讀什么或向硬件發送什么然后安排處理稍后的新信息(這被稱為‘bottom half’)並返回。然后內核保證只要可能就調用bottom half --當這在運行,內核模塊中允許做的所有事情將被允許。
實現這個辦法是當接收到相關的IRQ(在 Intel 平台下有16個)時去調用request_irq以使中斷處理程序被調用。這個函數接收IRQ號,函數名,標志/proc/interrupts中的名字及一個傳送給中斷處理程序的參數作為其參數。標志可以包括SA_SHIRQ以指明你願意和其他的中斷處理程序分享那個IRQ(通常因為幾個硬件設備在同一IRQ)以及SA_INTERRUPT以指明這是一個快速中斷。這個函數只在那個IRQ上沒有處理程序的情況下成功,或者你願意兩者共享。
然后從中斷處理程序中我們和硬件通信,聯合tq_immediate使用queue_task_irq和mark_bh(BH_IMMEDIATE)調度bottom half。我們在2.0版中不使用標准的queue_task 的原因是中斷有可能在其他人的 queue_task(queue_task_irq從這被一個全局鎖保護--在2.2版中沒有queue_task_irq而queue_task被一個鎖保護。)中發生。我們需要 mark_bh 是因為Linux 的早期版本只能有32個 bottom half,而現在它們中的一個(BH_IMMEDIATE)用於還沒有得到bottom half入口的驅動程序的bottom half連接表。
Intel 架構鍵盤
警告: 這章剩下的內容都特別指定為完全的基於Intel架構。如果你不是在這個平台下運行,它沒有用。甚至不要試圖去編譯這里的代碼。
在為這章寫范例代碼的時候我有一個問題。一方面,對於一個有用的范例,它應該運行於每個人的計算機上且有意味深長的結果。另一方面,內核已經包含了所有的通用設備的驅動程序,並且那些設備驅動程序不能和我將要寫的共存。我發現的結果是寫一些鍵盤中斷的東西並且先關閉通常的鍵盤的中斷句柄。因為在內核源文件(明確的, drivers/char/keyboard.c)中它被定義為靜態符號,所以沒有辦法恢復它。如果你重視你的文件系統,在 insmod 這些代碼前,在另一個終端上sleep 120 ; reboot 。
這個代碼將自己綁定為 IRQ 1,這是Intel 架構下的鍵盤控制器的IRQ(中斷請求)。然后當它收到鍵盤中斷時它就讀鍵盤的狀態( 那就是inb(0x64)的目的)和掃描代碼,該代碼即是鍵盤的返回值。然后,內核一認為它是可行的它就運行給出鍵所使用的代碼(掃描代碼的前7位)和它是被按下(第8位為0)還是被釋放(第8位為1)的got_char函數。
范例 intrpt.c
- <span style="font-size:18px;">/* intrpt.c - 中斷句柄 */
- /* Copyright (C) 1998 by Ori Pomerantz */
- /* 必要頭文件 */
- /* 標准頭文件 */
- #include /* 內核工作 */
- #include /* 明確指定是模塊 */
- /* 處理 CONFIG_MODVERSIONS */
- #if CONFIG_MODVERSIONS==1
- #define MODVERSIONS
- #include
- #endif
- #include
- #include
- /* 我們想中斷 */
- #include
- #include
- /* 在 2.2.3 版/usr/include/linux/version.h 中包含這個宏,
- * 但 2.0.35 版不包含-因此在這加入以被需要 */
- #ifndef KERNEL_VERSION
- #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
- #endif
- /* Bottom Half - 一旦內核模塊認為它做任何事都是安全的時候這將被內核調用。 */
- static void got_char(void *scancode)
- {
- printk( "Scan Code %x %s.n ",
- (int) *((char *) scancode) & 0x7F,
- *((char *) scancode) & 0x80 ? "Released" : "Pressed ");
- }
- /* 這個函數為鍵盤中斷服務。它讀取來自鍵盤的相關信息然后安排當內核認為bottom half安全的時候讓它運行 */
- void irq_handler(int irq,
- void *dev_id,
- struct pt_regs *regs)
- {
- /* 這些變量是靜態的,因為它們需要對 bottom half 可見(通過指針)。 */
- static unsigned char scancode;
- static struct tq_struct task =
- {NULL, 0, got_char, &scancode};
- unsigned char status;
- /* Read keyboard status */
- status = inb(0x64);
- scancode = inb(0x60);
- /* 安排 bottom half 運行 */
- #if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
- queue_task(&task, &tq_immediate);
- #else
- queue_task_irq(&task, &tq_immediate);
- #endif
- mark_bh(IMMEDIATE_BH);
- }
- /* 初始化模塊--登記 IRQ 句柄 */
- int init_module()
- {
- /* 既然鍵盤的句柄不能和我們的共存,在我們做事情前我們不得不關閉它(釋放它的 IRQ)。
- * 因為我們不知道它在哪兒,所以以后沒有辦法恢復它--因此當我們做完時計算機將被重新啟動。
- */
- free_irq(1, NULL);
- /* 請求 IRQ 1,鍵盤的 IRQ,指向我們的 irq_handler。 */
- return request_irq(
- 1, /* PC上的鍵盤的 IRQ 號 */
- irq_handler, /* 我們的句柄 */
- SA_SHIRQ,
- /* SA_SHIRQ 意味着我們將另一個句柄用於這個 IRQ。
- *
- * SA_INTERRUPT 能使句柄為一個快速中斷。
- */
- "test_keyboard_irq_handler ", NULL);
- }
- /* 清除 */
- void cleanup_module()
- {
- /* 它在這兒只是為了完全。它是完全不相關的,因為我們沒有辦法恢復通常的鍵盤中斷因此計算機完全沒用 * 了,需要被重新啟動。 */
- free_irq(1, NULL);
- }
- </span>
四、Windows中斷編程
1、中斷機制
(1)實模式中斷
為了便於理解,我們先回顧實模式中斷。在實模式下,中斷向量表IVT起到相當重要的作用。無論來自外部硬件的中斷或是內部的軟中斷INTn,在CPU中都產生同樣的響應。
①CPU將當前的指令指針寄存器(IP)、代碼段寄存器(CS)、標志寄存器壓入堆棧。
②然后CPU使用 n值作為指向中斷向量表IVT的索引,在IVT中找出服務例程的遠地址。
③CPU將此遠地垃裝入CS:IP寄存器中,並開始執行服務例程。
④中斷例程總以IRET指令結束。此指令使存在堆棧中的三個值彈出並填入CS、IP和標志寄存器,CPU繼續執行原來的指令。
(2)保護模式中斷
保護模式中斷過程與實模式中斷過程類似,但它不再使用中斷向量表IVT,而使用中斷描述符表(IDT)。值得一提的是,Windows運行時IVT還存在,應用程序並不使用它,Windows仍然使用,但含義已不同。
IVT結構:IVT在RAM的 0000:0000之上,占據開始的1024字節。它仍然由 BIOS啟動例程設置,由DOS填充到RAM中。
①IDT中斷描述符表:保護模式下,Windows操作系統為實現中斷機制而建立的一個特殊表,即中斷描述符表IDT。該表被用來保存中斷服務例程的線性地址,它們是真正的24位或32位地址,沒有段:偏移值結構。中斷描述器表最多可含有256個例程說明。
②當中斷或異常發生時,處理過程與實模式類似當前的CS;IP值和標志寄存器值被存儲。保存的內容還包括CPU其他內部寄存器的值,以及目
前正在被執行的任務的有關信息(若必須發生任務切換的話)。CPU設法獲取中斷向量后,以它為索引值查找IDT中的服務例程遠地址,接着將控制轉移到該處的服務例程。這是與實模式轉移到IVT的不同所在。保護模式使用IDTR寄存器分配和定位內存中的IDT中斷描述符表。IDT在內存中是可移動的,與 IVT固定在內存中剛好相反。IDT中斷描述符表在Windows中起決定性的作用。理解了windows保護模式的中斷機制。有助於我們理解中斷服務程序的設計,它的關鍵就在於如何將服務例程的地址放入IDT中斷描述符表中。當中斷發生時,如何將斷點地址及CPU各寄存器值保護起來,中斷結束時,如何將保護的值恢復。windows系統本身並不提供實現上述功能的API,而DOS保護模式接口DPMI正具備了上述的功能。
2、Windows下中斷服務程序的設計
下面我們首先介紹DPMI接口,然后基於它實現Windows下中斷服務程序的設計。
(1)DOS保護模式接口DMPI
Windows除了標准服務外,還支持一組特殊的DOS服務,稱為DOS保護模式接口DPMI,由一些INT2FH和INT31H服務組成。它使應用程序能夠訪問 PC系列計算機的擴充內存,同時維護系統的保護功能。DPMI通過軟件中斷31h來定義了一個新的接口,使得保護模式的應用程序能夠用它作分配內存,修改描述符以及調用實模式軟件等工作。
Windows為應用程序提供DPMI服務。即Windows是DPMI的宿主(host),應用程序是DPMI的客戶(client),可通過INT31H調用得到DPMI服務。 INT 31H本身提供多功能。其中它的中斷管理服務允許保護模式用於攔截實模式中斷,並且掛住處理器異常。有些服務能夠和 DPMI宿主合作,以維護應用程序的虛擬中斷標志。
可以用INT31H來掛住保護模式中斷向量,以中斷方式處理外部實時事件。利用 INT 21H,功能0205H:設置保護模式中斷向量,將特定中斷的保護模式處理程序的地址置入中斷向量里。調用方式:
AX=0205H,BL=中斷號,CX:(E)DX=中斷處理程序選擇符:偏移值。返回:執行成功CF=清零,執行失敗CF,置位。
掛住/解掛中斷向量的時機很重要。主窗口第一次被創建時會傳送它WM—CREATE消息,這時是掛住中斷向量的最好時機。退出時需解掛向量,否則 Windows可能崩潰。上窗口接收到WM_DESTROY之后進行解掛工作,是最適合的。解掛向量可先用INT35H,0204H功能將老的中斷向量保存,退出時用INT35H,0205H恢復。
(2)編程實現
有了DPMI的支持,我們就可以很方便地處理數據采集、串行通信等工業過程中的實時事件。下面以Windows3.1平台下中斷方式實現的串行通信為例,說明中斷程序的編制和實現。為便於參考,給出了詳細的代碼。開發平台BC3.1/BC4.5,其本身支持0.9版的DPMI,無需運行其它支持DPMI的軟件。編程語言C,可與C++混合編譯。
初始化COM1,9600波特率,每字符8bits,1個停止位,中斷接收,查詢發送。
- <span style="font-size:18px;">//windows asy COmmunica60n
- //by Li Xiumi98
- //last modified on June25,1996
- #include <windows.h>
- #include <dos.h>
- void interrupt far DataReceive() ;
- void interrupt far( * old_vector)();
- unsigned char dataCom_r[1024],datacom_s[1024]:
- int inflag=0 ;
- unsigned int s8259;
- int InitCom1()
- {
- s8259=inportb(0x21);
- outportb(0x21,s8259&0xe8);
- outportb(0x3fb,0x83);
- outportb(0x3f8,0x0c);
- outportb(0x3f9,0x00);
- outportb(0x3fb,0x03);
- outportb(0x3fc,0x08);
- outportb(0x3f9,0x01);
- return 1;
- }
- void interrupt far DataReceive()
- {
- static int i=0 ;
- char rechar =0 ;
- rechar=inportb(0x3f8);
- if(inflag==0)
- {
- if(rechar!= 's '&&i==0)
- {
- i=0;
- goto l1;
- }
- datacom_r[i++]=rechar;
- if(rechar== 'e ')
- {
- inflag=1;
- i=0;
- }
- }
- l1:outportb(0x20,0x20);
- }
- void InitCom(void)
- {
- asm{
- cli;
- mov ax,204h
- mov bl,0ch
- int 31h
- sti
- }
- old_vector=MK_FP(_CX,_DX);
- asm{
- cli
- mov ax,205h
- mov bl,0ch
- mov cx,seg datareceive
- mov dx,offset datareceive
- int 31h
- sti
- }
- InitCom();
- }
- void restore_Comm(void)
- {
- outportb(0x21,s8259);
- asm{
- cli
- mov ax,205h
- mov bl,0ch
- mov cx,seg old_vector
- mov dx,offset old_vector
- int 31h
- sti
- }
- } </span>
在窗口第一次被創建時會傳送它WM_CREATE消息,這時調用initCom()即可。在主窗口關閉時,即主窗口中收到 WM_DESTROY消息時,調用Restore Comm()恢復原來的狀態。
這樣在對串口初始化,設置中斷服務例程后,通信事件發生時,會立即跳入中斷子程序中執行,越過系統的消息隊列,達到實時處理通信事件的目的。而數據處理模塊可通過全局標志f1,8訪問全局的數據通信緩沖區獲取實時數據。這種實現方式與基於消息機制的Windows通信API實現相比具有實時性強的的特點,因為它超過了Windows系統的兩極消息機制,上述程序已在實際系統中得到應用。在windows3.1支持下同時運行三個Windows任務,服務器SERVER(內有實時串行通信,多個網絡數據子服務),客戶CLIENT,FOXPRO數據庫系統。整個系統運行良好。切換到WIN95平台下,系統也運行良好。
- 2樓 automationer 2012-08-24 10:03發表 [回復]
-
- 每日一博!
- 1樓 tsinghua5201314 2012-08-24 09:51發表 [回復] [引用] [舉報]
-
- 我們學校在做C語言課程設計,請問博主是否能推薦幾本比較好的關於深入理解C語言運行機制的書呢?
- Re: automationer 2012-08-24 09:55發表 [回復] [引用] [舉報]
-
-
回復tsinghua5201314:《C語言高級程序設計》 王士元 清華大學出版社
《數據結構》 嚴蔚敏 清華大學出版社
《算法導論》 Thomas H.Cormen等 機械工業出版社
這些能讀懂了就夠了 - 轉載自:http://blog.csdn.net/zhengzhihust/article/details/7902423