寫在前面
宗旨:把話說清楚,把道理講透徹。
約定:所有代碼均來自Linux內核2.6.24版。
建議:本文介紹得十分詳細,但也略顯繁瑣,讀者可以先看“Ⅴ.總結”部分帶注釋的源碼,如果哪里不清楚,再回頭看詳細解釋。
正文
預備知識
位圖:
在Linux下,從數據結構上看,位圖本質上是一個數組,數組的每個元素都是long型的(即32bit或64bit)。
假設在32位系統下,某long型數組有128個元素,那么,從邏輯上看,這個數組就是一個128行×32列的bit陣列,就是所謂的位圖,見下面的示意圖。
上圖中的數字就是各個bit位的標號,即索引。
對於位圖的操作,也就是對位圖中bit位的操作。
從作用上說,位圖通常與其它數據相關聯,用位圖中的bit位對該數據進行統計或管理。
例如,在文件系統中,每個進程都有一個元素為file指針的數組(為表述方便,后面稱之為數組A),同時,也有一個位圖,位圖中有效bit位(何為有效bit位,后面會詳述)的個數與數組A中元素的個數相同。當進程打開一個文件時,要先在位圖中找一個為0的bit位,然后將該位置1,返回該bit位的索引fd。當內核創建了與要打開文件對應的file實例后,會使數組A中索引為fd的元素(注意,前邊說過,該元素是一個file類型的指針)指向該file實例。這里的索引fd,就是我們平常所說的文件描述符。
正題
Ⅰ.源碼
先給出find_next_bit的源碼(源碼來自內核2.6.24,位置:/lib/find_next_bit.c)
1 unsigned long find_next_bit(const unsigned long *addr, unsigned long size, 2 unsigned long offset) 3 { 4 const unsigned long *p = addr + BITOP_WORD(offset); 5 unsigned long result = offset & ~(BITS_PER_LONG-1); 6 unsigned long tmp; 7 8 if (offset >= size) 9 return size; 10 size -= result; 11 offset %= BITS_PER_LONG; 12 if (offset) { 13 tmp = *(p++); 14 tmp &= (~0UL << offset); 15 if (size < BITS_PER_LONG) 16 goto found_first; 17 if (tmp) 18 goto found_middle; 19 size -= BITS_PER_LONG; 20 result += BITS_PER_LONG; 21 } 22 while (size & ~(BITS_PER_LONG-1)) { 23 if ((tmp = *(p++))) 24 goto found_middle; 25 result += BITS_PER_LONG; 26 size -= BITS_PER_LONG; 27 } 28 if (!size) 29 return result; 30 tmp = *p; 31 32 found_first: 33 tmp &= (~0UL >> (BITS_PER_LONG - size)); 34 if (tmp == 0UL) 35 return result + size; 36 found_middle: 37 return result + __ffs(tmp); 38 }
Ⅱ.功能——參數——返回值
功能:在addr指向的位圖中,從索引為offset的bit位(包括該位)開始,找到第一個為1的bit位,返回該位的索引。
參數:
@addr:位圖(數組)的起始地址。
@size:位圖的大小,即位圖中有效bit位的個數。注意,Linux內核實際調用該函數時,該參數的值不一定是32的整數倍(32位系統下)。假設構成位圖的數組大小為3,即一共有96個bit,但函數調用時,參數size可 能是90,那么,從邏輯上說,數組最后一個元素的最后6位是不參與構成位圖的,即它們不是位圖的組成部分,是“無效”的;而前邊的90個bit共同構成了位圖,它們是“有效”的。注意,后面解釋中經常會用 到“有效位”和“無效位”的概念,對此,讀者一定要理解清楚。
@offset:查找起點。即從位圖中索引為offset的位(包括該位)開始,查找第一個為1的bit位,offset之前的bit位不在搜索范圍之內。“查找起點”這個概念在后面的敘述中經常會用到,希望讀者能理解清楚。
返回值:找到的bit位的索引。
Ⅲ.BITS_PER_LONG和BITOP_WORD
3.1 BITS_PER_LONG
顧名思義,BITS_PER_LONG是指一個long型數據中bit位的個數,看源碼
/*include/asm-x86/types.h*/ #ifdef CONFIG_X86_32 # define BITS_PER_LONG 32 #else # define BITS_PER_LONG 64 #endif
可見,32位系統下,它是32;64位系統下,它是64。
3.2 BITOP_WORD
不多說,直接看源碼
/*lib/find_next_bit.c*/ #define BITOP_WORD(nr) ((nr) / BITS_PER_LONG)
就是參數nr除以32或64。
Ⅳ.一句一句解釋
注:在后面的所有解釋中,我們按32位系統講解,即BITS_PER_LONG取32。
a.
const unsigned long *p = addr + BITOP_WORD(offset);
該句的作用是使p指向數組中索引為offset的bit位(即查找起點)所在的元素。
我們知道,位圖的0~31位在數組第0個元素中, 32~63位在第1個元素中,等等(如圖1)。假設offset=33,則BITOP_WORD(offset)就是33/32,結果為1,那addr + BITOP_WORD(offset)就是addr+1,即指向數組第1個元素,就是索引為33的bit位所在的元素。
b.
unsigned long result = offset & ~(BITS_PER_LONG-1);
BITS_PER_LONG-1就是31,二進制形式就是0001 1111(這里為了表述方便,采用8位二進制),再取反就是1110 0000,即低5位全0,剩下的都是1。假設offset=70D=0100 0110B,那么,
offset & ~(BITS_PER_LONG-1)就是0100 0110 & 1110 0000 =0100 0000=64。其實,就是將offset的低5位置0,高位保持不變。這個結果有什么意義呢?其實就是:索引為offset的bit位所在的元素前的元素中bit位的個數,如下圖所示。很顯然,這些bit位都位於查找起點的前面,即它們都不在搜索范圍之內,我們可以將它們理解為“已處理”的bit位。請讀者牢記,在find_next_bit函數中,result總是表示“已處理”的bit位總數,並且,很顯然,它一定是32的整數倍。
c.
if (offset >= size) return size;
如果查找的起始位置offset大於等於位圖的總大小,直接返回位圖大小。這里解釋一下問什么要有等於,假設offset=size=64,但實際上,這種情況下,合法的索引是0~63,所以,值為64的offset不合法。問題的根源在於offset是從0算起的,而size是從1算起的。另外,個人認為這兩句應該放在函數體的最前面,這樣一來,只要if條件成立,就直接返回,后面的工作就都不用做了。而按照源碼的寫法,是無論如何都要執行前兩句的,但如果此時if條件成立,前邊的工作就白做了,這不是一個高效的安排。
d.
size -= result;
offset %= BITS_PER_LONG;
注意,從這兩句開始,size和offset的含義和傳參時的含義就不同了。
size-=result就是size=size-result,前邊說過,此時的result表示查找起點所在數組元素前的元素中bit位的個數,就是“已處理”的bit位個數,我們要找的bit位一定在這后邊;而size表示位圖總位數。兩者相減的結果,就是“未處理的、待查找的”bit位個數。請讀者牢記,此后的代碼中,result表示“已處理”的bit位總數,而size表示“待處理”的bit位總數,兩者的加和,一定等於傳參時size的值。
offset %= BITS_PER_LONG執行之后的offset也不再表示查找起點的索引,而表示查找起點在它所在的元素中是第幾個。假設原來offset=32,則執行該句之后,offset的值變成了0,而從前面的示意圖中我們可以看到,索引為32的bit位正是在數組的第1個元素(從0算起)的第0位(從0算起)。所以,我們可以這樣說,代碼執行前,offset是查找起點在整個位圖中的索引;代碼執行后,offset是查找起點在它所在的數組元素中的索引。
e.
if (offset) { tmp = *(p++); tmp &= (~0UL << offset); if (size < BITS_PER_LONG) goto found_first; if (tmp) goto found_middle; size -= BITS_PER_LONG; result += BITS_PER_LONG; }
進入這個if的條件是offset不為0,通過前面的分析,就是查找起點不在所在數組元素的首位(我們前邊舉的例子就是在首位的情況)。好了,開始分析里面的代碼,看看這個if做了什么。
e_1
tmp = *(p++);
這句得到的是查找起點所在的數組元素的值,也就是那32個bit。tmp是unsigned long型的,在文章開頭給出的源碼的第6行定義。注意:
1.表達式(p++)得到的是p的值(后置++),之后指針p再自增1,指向下一個數組元素。
2.tmp只是構成位圖的數組元素的拷貝而不是數組元素本身,即,對tmp所進行的任何修改,都不會影響到原位圖。
e_2
tmp &= (~0UL << offset);
首先,0UL就是unsigned long型的數字0,從二進制的角度看,就是32位全0;然后,對它取反,就是32位全1;接着,再左移offset位,假設offset=3,移位后就是1111 1000(為表述方便,這里只寫8位);最后,再和tmp(即查找起點所在的數組元素的值)相與,結果就是tmp的低3位置0,其余位保持不變。為什么要這么做呢?因為offset=3,它前面的第2位、第1位、第0位其實都不在搜索范圍之內,所以,我們將它們置為0(當然,e_1中就說過,這只是在拷貝上操作,不會影響到原位圖),這主要是為后面的工作做准備(讀者看到e_4小節就會自然明了)。所以,這句代碼的用意是:在查找起點所在的元素中,將查找起點前的bit位置0。代碼之所以要將查找起點是否在數組元素的第0位區別對待,就是因為若查找起點不在數組元素首位,需要將查找起點前的bit位置0。
e_3
if (size < BITS_PER_LONG) goto found_first;
通過前面的分析,我們知道,此時的size表示的是“待處理”的bit位總數,而現在,這個數字小於32,這說明:
1.函數調用時傳入的參數不是32的整數倍,構成位圖的數組的最后一個元素中含有“無效位”,這一點,在講解函數參數時就詳細解釋過;
2.查找起點就在數組的最后一個元素中!
上面的圖2就展示了這種情況,假設函數調用時傳入的參數size是86,通過d中的分析,我們知道,此時size的值是86-64=22,滿足if中的條件,而顯然,這種情況下,查找起點(索引為70的bit位)正是在最后一個數組元素中。
在這種情況下,代碼要轉到found_first處。我們繼續跟蹤,看看found_first干了些什么。
e_4
found_first: tmp &= (~0UL >> (BITS_PER_LONG - size)); if (tmp == 0UL) return result + size;
首先,我們要知道,代碼能走到這里,存在兩個前提:
1.查找起點在數組的最后一個元素中
2.最后一個元素中存在“無效位”
通過e_3中的分析,我們知道,此時的size表示的是“待處理”的bit位的個數,同時,它也表示最后一個元素中“有效位”的個數。那么,很顯然,BITS_PER_LONG-size就是“無效位”的個數,為了表述方便,我們假設該值位3,那么~0UL >> (BITS_PER_LONG - size)得到的就是這樣的32個bit位:最左3位為0,其余位為1。然后,這32個bit位再與tmp(即最后一個數組元素的拷貝)相與,結果就是:將該元素中的“無效位”都置為0了(注意,對32位的long型數據來說,低位、索引小的位在右,高位、索引大的位在左,所以,“無效位“一定是在最左邊的)!
回顧一下e_2,在e_2中,將元素中查找起點之前的位都置成了0,而現在,又將“無效位”都置成了0,如下圖
那么,剩下的是什么呢?剩下的就是查找范圍,即我們要在上圖中的白色部分找第一個位1的bit位。
現在,讓我們思考這樣一個問題:如果此時最后一個數組元素,即tmp的值為0(即if (tmp == 0UL)),說明了什么呢?細思,極恐,這說明查找范圍內全是0!因為紅色部分和黃色部分早就置為0了,而現在整個元素的值為0,那結論只有一個——白色部分全是0!
而我們可能會馬上想到這樣一個不幸事實:這已經是位圖中的最后一個數組元素了!
於是,我們就會得到這樣一個令人絕望的結論:以函數參數offset為查找起點,在當前位圖中找到一個值為1的bit位,已經不可能了!
在這種情況下,函數只好無奈地return result + size了,d中說過,result + size的值,一定等於傳參時size的值,即數組的總大小。
這,就是e_4中代碼的含義。
那么,如果我們的運氣沒那么差,if (tmp == 0UL)沒有被執行,也就是說,白色部分有1存在!進而我們就會欣喜地想到:這樣,就一定能找到滿足條件的bit位!那么,具體怎么找呢?看了最初的源碼我們會發現,代碼走到了found_middle標簽下。好吧,讓我們來看看found_middle下有什么。
e_5
found_middle: return result + __ffs(tmp);
這里出現了一個新的函數__ffs,它定義在include/asm-generic/bitops/__fss.h中,我們先來看一下這個函數
1 /** 2 * __ffs - find first bit in word. 3 * @word: The word to search 4 * 5 * Undefined if no bit exists, so code should check against 0 first. 6 */ 7 static inline unsigned long __ffs(unsigned long word) 8 { 9 int num = 0; 10 11 /*如果BITS_PER_LONG等於64,要先對低32位進行檢查;否則(即BITS_PER_LONG等於32),直接對低16位進行檢查*/ 12 #if BITS_PER_LONG == 64 13 if ((word & 0xffffffff) == 0) { //如果與0xffffffff相與得0,說明word的低32位全為0 14 num += 32; //索引加32 15 word >>= 32; //word右移32位,把低32位的0移走 16 } 17 #endif 18 if ((word & 0xffff) == 0) { 19 num += 16; 20 word >>= 16; 21 } 22 if ((word & 0xff) == 0) { 23 num += 8; 24 word >>= 8; 25 } 26 if ((word & 0xf) == 0) { 27 num += 4; 28 word >>= 4; 29 } 30 if ((word & 0x3) == 0) { 31 num += 2; 32 word >>= 2; 33 } 34 if ((word & 0x1) == 0) 35 num += 1; 36 return num; 37 }
該函數的功能是在參數word中找到第一個值為1的bit位,返回該位的索引。可以看到,該函數沒有對參數word=0的情況(這意味着不可能在word中找到值為1的bit位)進行檢查,所以我們應該在調用該函數前對傳入的參數是否為0進行檢查,這就是上面代碼中第5行的英文注釋的意思,而該函數的調用者find_next_bit在調用該函數前已經進行過檢查了,正如我們在e_4中分析的那樣。
該函數的算法還是很巧妙的,它類似於“折半查找”。假設參數word是32位的,先執行word & 0xffff,析取低16位,這時可能出現兩種結果:
1.如果結果為0,說明低16位(0~15位)全為0,那么,可能為1的bit位最小是第16位,所以num += 16,並且,執行word >>= 16,將全0的低16位移走,這樣,可能有1存在的高16位變成了低16位;接着,如法炮制,word與0xff相與,對剩下的16位進行“折半查找”。
2.如果結果不為0,說明低16位中有1存在,由於我們是要找第一個為1的bit位,所以,高16位就不用看了,直接在低16位中尋找,所以,下一步就是執行word & 0xff,對低16位進行“折半查找”。
可見,無論word & 0xffff的結果是否為0,word & 0xff都是要執行的,只不過,如果word & 0xffff結果為0,需要進行增加計數和右移的工作。
按照上面的步驟,逐步進行“折半查找”,最終就能得到word中第一個為1的bit位的索引。讀者可以自己舉個例子,手動執行一下__ffs函數,就更加清楚了,這里不再贅述。
有一點大家要意識到,find_next_bit在調用__ffs時傳入的參數是tmp,而在e_4中我們看到,tmp中查找起點前的bit位和“無效位”都被置為0了,並且,也已經判斷出tmp不為0,所以,__ffs一定能找到為1的bit位且保證該bit位在合法的搜索空間(圖3的白色區域)內。
現在我們再來看found_middle下的那句return result + __ffs(tmp)。__ffs(tmp)得到的是tmp中自查找起點起第一個為1的bit位的索引,而result表示的是位圖中tmp之前的所有元素中bit位的總和,所以,兩者相加,就是我們要找的bit位在位圖中的索引,即find_next_bit的最終結果,於是,將這個結果return。
至此,find_next_bit的查找工作就結束了,但是,我們對find_next_bit的分析還沒結束。不知讀者是否還記得,我們是從e_3中if (size < BITS_PER_LONG)成立這個“路口”進入,一路追蹤,才走到這里來的。所以我們還要分析if (size < BITS_PER_LONG)不成立的情況,於是,讓我們再次回到最初的源碼……
e_6
if (tmp) goto found_middle;
如果if (size < BITS_PER_LONG)不成立,就會執行上面的代碼。if (size < BITS_PER_LONG)不成立,說明了什么呢?這說明tmp不是數組的最后一個元素,因此就不可能有“無效位”,也就不存在將“無效位”置為0的問題(即不用goto found_first了),又因為,在e_4中,已經將查找起點前的bit位都置0了,所以,這里直接判斷tmp是否為0,如不為0,說明在這個tmp中一定能找到為1的bit位,所以,轉到found_middle,找到第一個為1的bit位在位圖中的索引,然后返回結果。
那么,如果這里的tmp為0呢,該執行什么樣的代碼,我們往下看。
e_7
size -= BITS_PER_LONG;
result += BITS_PER_LONG;
如果tmp為0,說明在當前數組元素中不可能找到為1的bit位,於是需要在下一個數組元素中尋找,在此之前,將“未處理”bit位總數減去32,將“已處理”bit位總數增加32。有讀者可能會問:怎么沒見指針后移呢?不要忘了,在e_1中,這個工作已經做過了。
至此,整個e中的代碼就都分析完了 。但是,革命尚未成功,喘口氣,我們還得往下看。
f
while (size & ~(BITS_PER_LONG-1)) { if ((tmp = *(p++))) goto found_middle; result += BITS_PER_LONG; size -= BITS_PER_LONG;
}
首先要明白,有兩種情況代碼會走到這里:
1.這個while循環上面的if(offset)(就是e中的代碼)條件不成立(即查找起點位於數組元素的第0位),if中的語句體沒有被執行。
2.if(offset)的語句體被執行了,但該語句體內部的兩個if條件都不成立,這又分為兩種情況:
1.查找起點所在的數組元素不是數組的最后一個元素,並且,該元素中全是0,沒有1。
2.查找起點是數組最后一個元素,但該元素中沒有“無效位”,並且,該元素中全是0,沒有1。
在上面的兩種情況下,我們就要執行上面的while循環,在后面的數組元素中依次查找。
我們先來看一下循環進入條件
while (size & ~(BITS_PER_LONG-1))
我們將BITS_PER_LONG換成32,並轉換為二進制形式,上面的語句就變成了while(size & 1110 0000),這是什么呢?其實就是while(size>=32)!因為小於32(0010 0000B)的無符號數(注意size是unsigned long型,請看最初的源碼)與1110 0000相與的結果都是0000 0000,而大於等於32的無符號數與1110 0000相與的結果都將大於0010 0000。好了,現在我們知道了,上面那句故弄玄虛的while語句其實就是while(size>=32)。
好了,現在我們來看循環體。先看第一句
if ((tmp = *(p++)))
這句代碼依次做了三件事:
1.將p指向的數組元素(當然也是我們要進行查找的數組元素)復制到tmp
2.p指針自增,指向下一元素
3.判斷tmp是否為0
這里需要解釋一下:
1.如果查找起點在一個數組元素的第0位,那么,e中的if語句體就不會被執行,也就不會執行到if語句體中的tmp = *(p++),所以當第一次進入while循環並執行if ((tmp = *(p++)))時,tmp就是查找起點所在的數組元素的拷貝。
2.如果查找起點不在一個數組元素的第0位,e中的if語句體就會被執行,當第一次進入while循環並執行if ((tmp = *(p++)))時,tmp就是if語句體中那個tmp對應的元素的下一個元素的拷貝。
現在讓我們在整體看一下這個循環體
if ((tmp = *(p++))) goto found_middle; result += BITS_PER_LONG; size -= BITS_PER_LONG;
如果tmp不為0,說明我們要找的為1的bit位一定在這個tmp中,於是轉到found_middle,進行查找並返回結果,find_next_bit函數結束;如果tmp等於0,說明該元素中不含1,將“已處理”位數增加32,“待處理”位數減少32,然后判斷循環條件(“待查找”位數是否大於32),若條件成立,進入循環體,對下一個數組元素進行查找,否則,退出循環。
如果上面的while循環是正常退出(即由於size小於32而退出)的,那就說明整個整個while循環都沒找到為1的bit位(否則,代碼將從循環體轉到found_middle,while循環將不會正常退出)。如果是這種情況,接下來該怎么辦呢?我們還是繼續看源碼吧。
g
if (!size) return result; tmp = *p; found_first: tmp &= (~0UL >> (BITS_PER_LONG - size)); if (tmp == 0UL) return result + size; found_middle: return result + __ffs(tmp);
首先要明白,代碼走到這里,有四種情況:
1.查找起點位於數組的最后一個元素中,且位於該元素的第0位,同時,該元素含有“無效位”,即傳入的參數size不是32的整數倍,並且,執行完d中的代碼后,size小於32(這種情況容易被忽略)。在這種情況下,e中的if語句和f中的while循環都不會被執行,代碼直接從d處走到g處。
2.代碼執行完e中的if后不再滿足while循環的條件,直接走到g中代碼處。這種情況是:查找起點在倒數第二個元素中且不位於第0位且該元素32位全是0,同時,最后一個數組元素中有“無效位”(傳入參數size不是32的整數倍)。
3.while循環由於size=0而退出(傳入參數恰好是32的整數倍),這種情況下,整個位圖都查找過了且並沒有找到為1的bit位,換句話說,不可能再找到了。
4.while循環由於size介於1~31之間而退出,即數組的最后一個元素中有“無效位”(傳入參數size不是32的整數倍)。
下面讓我們看看,在這四種情況下,代碼是怎么做的:
首先,代碼先判斷size是否等於0,若等於0,直接返回result,這正對應了上面的情況3。注意,我們前面就說過,size+result的值總是等於傳參時size的值(位圖總大小),而此時size=0,所以,返回result,就是返回位圖總大小。
如果size不等於0,那就對應1、2、4三種情況,這三種情況的共同點是:都需要在數組的最后一個元素中查找,並且該元素中含有“無效位”。在這種情形下,find_next_bit函數的處理是:
1.將最后一個數組元素拷貝到tmp
2.在found_first中,將“無效位”置0,檢驗tmp是否為0,若是,返回位圖總大小,否則,進入found_middle
3.在found_middle中,找到tmp中第一個為1的bit位的索引,返回該位在位圖中的索引
至此,find_next_bit函數結束。
Ⅴ.總結
至此,我們對find_next_bit函數的分析就全部完成了。我們可以看到:
1.若函數查找成功,返回自查找起點起第一個為1的bit位的索引;若查找失敗,返回位圖總大小。
2.find_next_bit函數只是在位圖中進行查找,自始至終都沒有對位圖進行任何修改。
最后,我們給出find_next_bit函數源碼的簡要注釋版,作為最后的總結梳理
/* *@addr:位圖首地址 *@size:位圖總大小 *@offset:查找起點 */ unsigned long find_next_bit(const unsigned long *addr, unsigned long size, unsigned long offset) { /*找到查找起點所在數組元素的地址*/ const unsigned long *p = addr + BITOP_WORD(offset); /*計算查找起點所在數組元素前的數組元素中bit位的總個數,即“已處理”bit位個數*/ unsigned long result = offset & ~(BITS_PER_LONG-1); unsigned long tmp; /*如果查找起點大於等於位圖大小,返回位圖大小*/ if (offset >= size) return size; /*size:“未處理”bit位個數*/ size -= result; /*offset:查找起點在所在數組元素中的索引*/ offset %= BITS_PER_LONG; /*如果查找起點不在數組元素中的第0位*/ if (offset) { /*拷貝元素*/ tmp = *(p++); /*將查找起點前的bit位都置0*/ tmp &= (~0UL << offset); /*如果查找起點位於最后一個數組元素且該元素含有“無效位”*/ if (size < BITS_PER_LONG) goto found_first; /*如果tmp不為0,一定能找到*/ if (tmp) goto found_middle; /*“待查找”bit位總數減少32,“已查找”bit位總數增加32*/ size -= BITS_PER_LONG; result += BITS_PER_LONG; } /*while(size>=32)*/ while (size & ~(BITS_PER_LONG-1)) { /*拷貝元素,若不為0,一定能找到*/ if ((tmp = *(p++))) goto found_middle; /*“待查找”bit位總數減少32,“已查找”bit位總數增加32*/ result += BITS_PER_LONG; size -= BITS_PER_LONG; } /*若size=0,返回位圖總大小*/ if (!size) return result; /*否則,拷貝最后一個數組元素*/ tmp = *p; found_first: /*將元素中“無效位”都置為0*/ tmp &= (~0UL >> (BITS_PER_LONG - size)); /*若tmp為,不可能再找到,直接返回位圖總大小*/ if (tmp == 0UL) return result + size; found_middle: /*走到這里,就一定能找到,查找,返回結果*/ return result + __ffs(tmp); }
寫在后面
自認為寫得很詳細了,但在下才疏學淺,錯誤疏漏之處在所難免,懇請廣大讀者批評指正,您的批評指正是在下前進的不竭動力!