• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 200, comments - 8, trackbacks - 0, articles - 0

            在Linux中,系統(tǒng)調(diào)用是用戶空間訪問內(nèi)核的唯一手段,它們是內(nèi)核唯一的合法入口。實際上,其他的像設(shè)備文件和/proc之類的方式,最終也還是要通過系統(tǒng)調(diào)用進行的。

                   一般情況下,應(yīng)用程序通過應(yīng)用編程接口(API)而不是直接通過系統(tǒng)調(diào)用來編程,而且這種編程接口實際上并不需要和內(nèi)核提供的系統(tǒng)調(diào)用對應(yīng)。一個API定義了一組應(yīng)用程序使用的編程接口。它們可以實現(xiàn)成一個系統(tǒng)調(diào)用,也可以通過調(diào)用多個系統(tǒng)調(diào)用來實現(xiàn),即使不使用任何系統(tǒng)調(diào)用也不存在問題。實際上,API可以在各種不同的操作系統(tǒng)上實現(xiàn),給應(yīng)用程序提供完全相同的接口,而它們本身在這些系統(tǒng)上的實現(xiàn)卻可能迥異。

                   在Unix世界中,最流行的應(yīng)用編程接口是基于POSIX標(biāo)準(zhǔn)的,Linux是與POSIX兼容的。

                   從程序員的角度看,他們只需要給API打交道就可以了,而內(nèi)核只跟系統(tǒng)調(diào)用打交道;庫函數(shù)及應(yīng)用程序是怎么使用系統(tǒng)調(diào)用不是內(nèi)核關(guān)心的。

                   系統(tǒng)調(diào)用(在linux中常稱作syscalls)通常通過函數(shù)進行調(diào)用。它們通常都需要定義一個或幾個參數(shù)(輸入)而且可能產(chǎn)生一些副作用。這些副作用通過一個long類型的返回值來表示成功(0值)或者錯誤(負(fù)值)。在系統(tǒng)調(diào)用出現(xiàn)錯誤的時候會把錯誤碼寫入errno全局變量。通過調(diào)用perror()函數(shù),可以把該變量翻譯成用戶可以理解的錯誤字符串。

                   系統(tǒng)調(diào)用的實現(xiàn)有兩個特別之處:1)函數(shù)聲明中都有asmlinkage限定詞,用于通知編譯器僅從棧中提取該函數(shù)的參數(shù)。2)系統(tǒng)調(diào)用getXXX()在內(nèi)核中被定義為sys_getXXX()。這是Linux中所有系統(tǒng)調(diào)用都應(yīng)該遵守的命名規(guī)則。

                   系統(tǒng)調(diào)用號:在linux中,每個系統(tǒng)調(diào)用都賦予一個系統(tǒng)調(diào)用號,通過這個獨一無二的號就可以關(guān)聯(lián)系統(tǒng)調(diào)用。當(dāng)用戶空間的進程執(zhí)行一個系統(tǒng)調(diào)用的時候,這個系統(tǒng)調(diào)用號就被用來指明到底要執(zhí)行哪個系統(tǒng)調(diào)用;進程不會提及系統(tǒng)調(diào)用的名稱。系統(tǒng)調(diào)用號一旦分配就不能再有任何變更(否則編譯好的應(yīng)用程序就會崩潰),如果一個系統(tǒng)調(diào)用被刪除,它所占用的系統(tǒng)調(diào)用號也不允許被回收利用。Linux有一個"未使用"系統(tǒng)調(diào)用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,這個錯誤號就是專門針對無效的系統(tǒng)調(diào)用而設(shè)的。雖然很罕見,但如果有一個系統(tǒng)調(diào)用被刪除,這個函數(shù)就要負(fù)責(zé)“填補空位”。

                   內(nèi)核記錄了系統(tǒng)調(diào)用表中所有已注冊過的系統(tǒng)調(diào)用的列表,存儲在sys_call_table中。它與體系結(jié)構(gòu)有關(guān),一般在entry.s中定義。這個表中為每一個有效的系統(tǒng)調(diào)用指定了唯一的系統(tǒng)調(diào)用號。

                   用戶空間的程序無法直接執(zhí)行內(nèi)核代碼。它們不能直接調(diào)用內(nèi)核空間的函數(shù),因為內(nèi)核駐留在受保護的地址空間上,應(yīng)用程序應(yīng)該以某種方式通知系統(tǒng),告訴內(nèi)核自己需要執(zhí)行一個系統(tǒng)調(diào)用,系統(tǒng)系統(tǒng)切換到內(nèi)核態(tài),這樣內(nèi)核就可以代表應(yīng)用程序來執(zhí)行該系統(tǒng)調(diào)用了。這種通知內(nèi)核的機制是通過軟中斷實現(xiàn)的。x86系統(tǒng)上的軟中斷由int$0x80指令產(chǎn)生。這條指令會觸發(fā)一個異常導(dǎo)致系統(tǒng)切換到內(nèi)核態(tài)并執(zhí)行第128號異常處理程序,而該程序正是系統(tǒng)調(diào)用處理程序,名字叫system_call().它與硬件體系結(jié)構(gòu)緊密相關(guān),通常在entry.s文件中通過匯編語言編寫。

                   所有的系統(tǒng)調(diào)用陷入內(nèi)核的方式都是一樣的,所以僅僅是陷入內(nèi)核空間是不夠的。因此必須把系統(tǒng)調(diào)用號一并傳給內(nèi)核。在x86上,這個傳遞動作是通過在觸發(fā)軟中斷前把調(diào)用號裝入eax寄存器實現(xiàn)的。這樣系統(tǒng)調(diào)用處理程序一旦運行,就可以從eax中得到數(shù)據(jù)。上述所說的system_call()通過將給定的系統(tǒng)調(diào)用號與NR_syscalls做比較來檢查其有效性。如果它大于或者等于NR_syscalls,該函數(shù)就返回-ENOSYS.否則,就執(zhí)行相應(yīng)的系統(tǒng)調(diào)用:call *sys_call_table(, %eax, 4);

                   由于系統(tǒng)調(diào)用表中的表項是以32位(4字節(jié))類型存放的,所以內(nèi)核需要將給定的系統(tǒng)調(diào)用號乘以4,然后用所得到的結(jié)果在該表中查詢器位置。如圖圖一所示:

                                                  結(jié)構(gòu) 

                 上面已經(jīng)提到,除了系統(tǒng)調(diào)用號以外,還需要一些外部的參數(shù)輸入。最簡單的辦法就是像傳遞系統(tǒng)調(diào)用號一樣把這些參數(shù)也存放在寄存器里。在x86系統(tǒng)上ebx,ecx,edx,esi和edi按照順序存放前5個參數(shù)。需要六個或六個以上參數(shù)的情況不多見,此時,應(yīng)該用一個單獨的寄存器存放指向所有這些參數(shù)在用戶空間地址的指針。給用戶空間的返回值也通過寄存器傳遞。在x86系統(tǒng)上,它存放在eax寄存器中。

                   系統(tǒng)調(diào)用必須仔細(xì)檢查它們所有的參數(shù)是否合法有效。系統(tǒng)調(diào)用在內(nèi)核空間執(zhí)行。如果任由用戶將不合法的輸入傳遞給內(nèi)核,那么系統(tǒng)的安全和穩(wěn)定將面臨極大的考驗。最重要的一種檢查就是檢查用戶提供的指針是否有效,內(nèi)核在接收一個用戶空間的指針之前,內(nèi)核必須要保證:

            1)指針指向的內(nèi)存區(qū)域?qū)儆谟脩艨臻g
            2)指針指向的內(nèi)存區(qū)域在進程的地址空間里
            3)如果是讀,讀內(nèi)存應(yīng)該標(biāo)記為可讀。如果是寫,該內(nèi)存應(yīng)該標(biāo)記為可寫。

                   內(nèi)核提供了兩種方法來完成必須的檢查和內(nèi)核空間與用戶空間之間數(shù)據(jù)的來回拷貝。這兩個方法必須有一個被調(diào)用。

            copy_to_user():向用戶空間寫入數(shù)據(jù),需要3個參數(shù)。第一個參數(shù)是進程空間中的目的內(nèi)存地址。第二個是內(nèi)核空間內(nèi)的源地址
                                 .第三個是需要拷貝的數(shù)據(jù)長度(字節(jié)數(shù))。
            copy_from_user():向用戶空間讀取數(shù)據(jù),需要3個參數(shù)。第一個參數(shù)是進程空間中的目的內(nèi)存地址。第二個是內(nèi)核空間內(nèi)的源地
                                 址.第三個是需要拷貝的數(shù)據(jù)長度(字節(jié)數(shù))。
            注意:這兩個都有可能引起阻塞。當(dāng)包含用戶數(shù)據(jù)的頁被換出到硬盤上而不是在物理內(nèi)存上的時候,這種情況就會發(fā)生。此時,進程就會休眠,直到缺頁處理程序?qū)⒃擁搹挠脖P重新?lián)Q回到物理內(nèi)存。

                   內(nèi)核在執(zhí)行系統(tǒng)調(diào)用的時候處于進程上下文,current指針指向當(dāng)前任務(wù),即引發(fā)系統(tǒng)調(diào)用的那個進程。在進程上下文中,內(nèi)核可以休眠(比如在系統(tǒng)調(diào)用阻塞或顯式調(diào)用schedule()的時候)并且可以被搶占。當(dāng)系統(tǒng)調(diào)用返回的時候,控制權(quán)仍然在system_call()中,它最終會負(fù)責(zé)切換到用戶空間并讓用戶進程繼續(xù)執(zhí)行下去。

                   給linux添加一個系統(tǒng)調(diào)用時間很簡單的事情,怎么設(shè)計和實現(xiàn)一個系統(tǒng)調(diào)用是難題所在。實現(xiàn)系統(tǒng)調(diào)用的第一步是決定它的用途,這個用途是明確且唯一的,不要嘗試編寫多用途的系統(tǒng)調(diào)用。ioctl則是一個反面教材。新系統(tǒng)調(diào)用的參數(shù),返回值和錯誤碼該是什么,這些都很關(guān)鍵。一旦一個系統(tǒng)調(diào)用編寫完成后,把它注冊成為一個正式的系統(tǒng)調(diào)用是件瑣碎的工作,一般下面幾步:

            1)在系統(tǒng)調(diào)用表(一般位于entry.s)的最后加入一個表項。從0開始算起,系統(tǒng)表項在該表中的位置就是它的系統(tǒng)調(diào)用號。如第
               10個系統(tǒng)調(diào)用分配到系統(tǒng)調(diào)用號為9
            2)任何體系結(jié)構(gòu),系統(tǒng)調(diào)用號都必須定義于include/asm/unistd.h中
            3)系統(tǒng)調(diào)用必須被編譯進內(nèi)核映像(不能編譯成模塊)。這只要把它放進kernel/下的一個相關(guān)文件就可以。

                   通常,系統(tǒng)調(diào)用靠C庫支持,用戶程序通過包含標(biāo)準(zhǔn)頭文件并和C庫鏈接,就可以使用系統(tǒng)調(diào)用(或者使用庫函數(shù),再由庫函數(shù)實際調(diào)用)。慶幸的是linux本身提供了一組宏用于直接對系統(tǒng)調(diào)用進行訪問。它會設(shè)置好寄存器并調(diào)用int $0x80指令。這些宏是_syscalln(),其中n的范圍是從0到6.代表需要傳遞給系統(tǒng)調(diào)用的參數(shù)個數(shù)。這是由于該宏必須了解到底有多少參數(shù)按照什么次序壓入寄存器。以open系統(tǒng)調(diào)用為例:

            open()系統(tǒng)調(diào)用定義如下是:
            long open(const char *filename, int flags, int mode)
            直接調(diào)用此系統(tǒng)調(diào)用的宏的形式為:
            #define NR_open 5
            _syscall3(long, open, const char *, filename, int , flags, int, mode)

                這樣,應(yīng)用程序就可以直接使用open().調(diào)用open()系統(tǒng)調(diào)用直接把上面的宏放置在應(yīng)用程序中就可以了。對于每個宏來說,都有2+2*n個參數(shù)。每個參數(shù)的意義簡單明了,這里就不詳細(xì)說明了。

            posted @ 2012-10-22 20:03 鑫龍 閱讀(320) | 評論 (0)編輯 收藏

            list_for_each()的定義:


            1. /**  
            2.  * list_for_each    -   iterate over a list  
            3.  * @pos:    the &struct list_head to use as a loop counter.  
            4.  * @head:   the head for your list.  
            5.  */  
            6. #define list_for_each(pos, head) \  
            7.     for (pos = (head)->next, prefetch(pos->next); pos != (head); \  
            8.         pos = pos->next, prefetch(pos->next))  

            list_for_each_safe()的定義:

            1. /**  
            2.  * list_for_each_safe   -   iterate over a list safe against removal of list entry  
            3.  * @pos:    the &struct list_head to use as a loop counter.  
            4.  * @n:      another &struct list_head to use as temporary storage  
            5.  * @head:   the head for your list.  
            6.  */  
            7. #define list_for_each_safe(pos, n, head) \  
            8.     for (pos = (head)->next, n = pos->next; pos != (head); \  
            9.         pos = n, n = pos->next)  

            由上面兩個對比來看,list_for_each_safe()函數(shù)比list_for_each()多了一個中間變量n

             

            當(dāng)在遍歷的過程中需要刪除結(jié)點時,來看一下會出現(xiàn)什么情況:

            list_for_each():list_del(pos)將pos的前后指針指向undefined state,導(dǎo)致kernel panic,另如果list_del_init(pos)將pos前后指針指向自身,導(dǎo)致死循環(huán)。

            list_for_each_safe():首先將pos的后指針緩存到n,處理一個流程后再賦回pos,避免了這種情況發(fā)生。


            因此之遍歷鏈表不刪除結(jié)點時,可以使用list_for_each(),而當(dāng)由刪除結(jié)點操作時,則要使用list_for_each_safe()。

            其他帶safe的處理也是基于這個原因。

             

            posted @ 2012-10-22 10:45 鑫龍 閱讀(1060) | 評論 (0)編輯 收藏

            一、 鏈表數(shù)據(jù)結(jié)構(gòu)簡介 

            鏈表是一種常用的組織有序數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu),它通過指針將一系列數(shù)據(jù)節(jié)點連接成一條數(shù)據(jù)鏈,是線性表的一種重要實現(xiàn)方式。相對于數(shù)組,鏈表具有更好的動態(tài)性,建立鏈表時無需預(yù)先知道數(shù)據(jù)總量,可以隨機分配空間,可以高效地在鏈表中的任意位置實時插入或刪除數(shù)據(jù)。鏈表的開銷主要是訪問的順序性和組織鏈的空間損失。

            通常鏈表數(shù)據(jù)結(jié)構(gòu)至少應(yīng)包含兩個域:數(shù)據(jù)域和指針域,數(shù)據(jù)域用于存儲數(shù)據(jù),指針域用于建立與下一個節(jié)點的聯(lián)系。按照指針域的組織以及各個節(jié)點之間的聯(lián)系形式,鏈表又可以分為單鏈表、雙鏈表、循環(huán)鏈表等多種類型,下面分別給出這幾類常見鏈表類型的示意圖:

            1. 單鏈表


            圖1 單鏈表
            圖1 單鏈表

            單鏈表是最簡單的一類鏈表,它的特點是僅有一個指針域指向后繼節(jié)點(next),因此,對單鏈表的遍歷只能從頭至尾(通常是NULL空指針)順序進行。

            2. 雙鏈表


            圖2 雙鏈表
            圖2 雙鏈表

            通過設(shè)計前驅(qū)和后繼兩個指針域,雙鏈表可以從兩個方向遍歷,這是它區(qū)別于單鏈表的地方。如果打亂前驅(qū)、后繼的依賴關(guān)系,就可以構(gòu)成"二叉樹";如果再讓首節(jié)點的前驅(qū)指向鏈表尾節(jié)點、尾節(jié)點的后繼指向首節(jié)點(如圖2中虛線部分),就構(gòu)成了循環(huán)鏈表;如果設(shè)計更多的指針域,就可以構(gòu)成各種復(fù)雜的樹狀數(shù)據(jù)結(jié)構(gòu)。

            3. 循環(huán)鏈表

            循環(huán)鏈表的特點是尾節(jié)點的后繼指向首節(jié)點。前面已經(jīng)給出了雙循環(huán)鏈表的示意圖,它的特點是從任意一個節(jié)點出發(fā),沿兩個方向的任何一個,都能找到鏈表中的任意一個數(shù)據(jù)。如果去掉前驅(qū)指針,就是單循環(huán)鏈表。

            在Linux內(nèi)核中使用了大量的鏈表結(jié)構(gòu)來組織數(shù)據(jù),包括設(shè)備列表以及各種功能模塊中的數(shù)據(jù)組織。這些鏈表大多采用在[include/linux/list.h]實現(xiàn)的一個相當(dāng)精彩的鏈表數(shù)據(jù)結(jié)構(gòu)。本文的后繼部分就將通過示例詳細(xì)介紹這一數(shù)據(jù)結(jié)構(gòu)的組織和使用。


            二、 Linux 2.6內(nèi)核鏈表數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)

            盡管這里使用2.6內(nèi)核作為講解的基礎(chǔ),但實際上2.4內(nèi)核中的鏈表結(jié)構(gòu)和2.6并沒有什么區(qū)別。不同之處在于2.6擴充了兩種鏈表數(shù)據(jù)結(jié)構(gòu):鏈表的讀拷貝更新(rcu)和HASH鏈表(hlist)。這兩種擴展都是基于最基本的list結(jié)構(gòu),因此,本文主要介紹基本鏈表結(jié)構(gòu),然后再簡要介紹一下rcu和hlist。

            鏈表數(shù)據(jù)結(jié)構(gòu)的定義很簡單(節(jié)選自[include/linux/list.h],以下所有代碼,除非加以說明,其余均取自該文件):

            struct list_head {
            	struct list_head *next, *prev;
            };
            

            list_head結(jié)構(gòu)包含兩個指向list_head結(jié)構(gòu)的指針prev和next,由此可見,內(nèi)核的鏈表具備雙鏈表功能,實際上,通常它都組織成雙循環(huán)鏈表。

            和第一節(jié)介紹的雙鏈表結(jié)構(gòu)模型不同,這里的list_head沒有數(shù)據(jù)域。在Linux內(nèi)核鏈表中,不是在鏈表結(jié)構(gòu)中包含數(shù)據(jù),而是在數(shù)據(jù)結(jié)構(gòu)中包含鏈表節(jié)點。

            在數(shù)據(jù)結(jié)構(gòu)課本中,鏈表的經(jīng)典定義方式通常是這樣的(以單鏈表為例):

            struct list_node {
            	struct list_node *next;
            	ElemType	data;
            };
            

            因為ElemType的緣故,對每一種數(shù)據(jù)項類型都需要定義各自的鏈表結(jié)構(gòu)。有經(jīng)驗的C++程序員應(yīng)該知道,標(biāo)準(zhǔn)模板庫中的<list>采用的是C++ Template,利用模板抽象出和數(shù)據(jù)項類型無關(guān)的鏈表操作接口。

            在Linux內(nèi)核鏈表中,需要用鏈表組織起來的數(shù)據(jù)通常會包含一個struct list_head成員,例如在[include/linux/netfilter.h]中定義了一個nf_sockopt_ops結(jié)構(gòu)來描述Netfilter為某一協(xié)議族準(zhǔn)備的getsockopt/setsockopt接口,其中就有一個(struct list_head list)成員,各個協(xié)議族的nf_sockopt_ops結(jié)構(gòu)都通過這個list成員組織在一個鏈表中,表頭是定義在[net/core/netfilter.c]中的nf_sockopts(struct list_head)。從下圖中我們可以看到,這種通用的鏈表結(jié)構(gòu)避免了為每個數(shù)據(jù)項類型定義自己的鏈表的麻煩。Linux的簡捷實用、不求完美和標(biāo)準(zhǔn)的風(fēng)格,在這里體現(xiàn)得相當(dāng)充分。


            圖3 nf_sockopts鏈表示意圖
            圖3 nf_sockopts鏈表示意圖

            三、 鏈表操作接口

            1. 聲明和初始化

            實際上Linux只定義了鏈表節(jié)點,并沒有專門定義鏈表頭,那么一個鏈表結(jié)構(gòu)是如何建立起來的呢?讓我們來看看LIST_HEAD()這個宏:

            #define LIST_HEAD_INIT(name) { &(name), &(name) }
            #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
            

            當(dāng)我們用LIST_HEAD(nf_sockopts)聲明一個名為nf_sockopts的鏈表頭時,它的next、prev指針都初始化為指向自己,這樣,我們就有了一個空鏈表,因為Linux用頭指針的next是否指向自己來判斷鏈表是否為空:

            static inline int list_empty(const struct list_head *head)
            {
            		return head->next == head;
            }
            

            除了用LIST_HEAD()宏在聲明的時候初始化一個鏈表以外,Linux還提供了一個INIT_LIST_HEAD宏用于運行時初始化鏈表:

            #define INIT_LIST_HEAD(ptr) do { \
            	(ptr)->next = (ptr); (ptr)->prev = (ptr); \
            } while (0)
            

            我們用INIT_LIST_HEAD(&nf_sockopts)來使用它。

            2. 插入/刪除/合并

            a) 插入

            對鏈表的插入操作有兩種:在表頭插入和在表尾插入。Linux為此提供了兩個接口:

            static inline void list_add(struct list_head *new, struct list_head *head);
            static inline void list_add_tail(struct list_head *new, struct list_head *head);
            

            因為Linux鏈表是循環(huán)表,且表頭的next、prev分別指向鏈表中的第一個和最末一個節(jié)點,所以,list_add和list_add_tail的區(qū)別并不大,實際上,Linux分別用

            __list_add(new, head, head->next);
            

            __list_add(new, head->prev, head);
            

            來實現(xiàn)兩個接口,可見,在表頭插入是插入在head之后,而在表尾插入是插入在head->prev之后。

            假設(shè)有一個新nf_sockopt_ops結(jié)構(gòu)變量new_sockopt需要添加到nf_sockopts鏈表頭,我們應(yīng)當(dāng)這樣操作:

            list_add(&new_sockopt.list, &nf_sockopts);
            

            從這里我們看出,nf_sockopts鏈表中記錄的并不是new_sockopt的地址,而是其中的list元素的地址。如何通過鏈表訪問到new_sockopt呢?下面會有詳細(xì)介紹。

            b) 刪除

            static inline void list_del(struct list_head *entry);
            

            當(dāng)我們需要刪除nf_sockopts鏈表中添加的new_sockopt項時,我們這么操作:

            list_del(&new_sockopt.list);
            

            被剔除下來的new_sockopt.list,prev、next指針分別被設(shè)為LIST_POSITION2和LIST_POSITION1兩個特殊值,這樣設(shè)置是為了保證不在鏈表中的節(jié)點項不可訪問--對LIST_POSITION1和LIST_POSITION2的訪問都將引起頁故障。與之相對應(yīng),list_del_init()函數(shù)將節(jié)點從鏈表中解下來之后,調(diào)用LIST_INIT_HEAD()將節(jié)點置為空鏈狀態(tài)。

            c) 搬移

            Linux提供了將原本屬于一個鏈表的節(jié)點移動到另一個鏈表的操作,并根據(jù)插入到新鏈表的位置分為兩類:

            static inline void list_move(struct list_head *list, struct list_head *head);
            static inline void list_move_tail(struct list_head *list, struct list_head *head);
            

            例如list_move(&new_sockopt.list,&nf_sockopts)會把new_sockopt從它所在的鏈表上刪除,并將其再鏈入nf_sockopts的表頭。

            d) 合并

            除了針對節(jié)點的插入、刪除操作,Linux鏈表還提供了整個鏈表的插入功能:

            static inline void list_splice(struct list_head *list, struct list_head *head);
            

            假設(shè)當(dāng)前有兩個鏈表,表頭分別是list1和list2(都是struct list_head變量),當(dāng)調(diào)用list_splice(&list1,&list2)時,只要list1非空,list1鏈表的內(nèi)容將被掛接在list2鏈表上,位于list2和list2.next(原list2表的第一個節(jié)點)之間。新list2鏈表將以原list1表的第一個節(jié)點為首節(jié)點,而尾節(jié)點不變。如圖(虛箭頭為next指針):


            圖4 鏈表合并list_splice(&list1,&list2)
            圖4 鏈表合并list_splice(&list1,&list2)

            當(dāng)list1被掛接到list2之后,作為原表頭指針的list1的next、prev仍然指向原來的節(jié)點,為了避免引起混亂,Linux提供了一個list_splice_init()函數(shù):

            static inline void list_splice_init(struct list_head *list, struct list_head *head);
            	

            該函數(shù)在將list合并到head鏈表的基礎(chǔ)上,調(diào)用INIT_LIST_HEAD(list)將list設(shè)置為空鏈。

            3. 遍歷

            遍歷是鏈表最經(jīng)常的操作之一,為了方便核心應(yīng)用遍歷鏈表,Linux鏈表將遍歷操作抽象成幾個宏。在介紹遍歷宏之前,我們先看看如何從鏈表中訪問到我們真正需要的數(shù)據(jù)項。

            a) 由鏈表節(jié)點到數(shù)據(jù)項變量

            我們知道,Linux鏈表中僅保存了數(shù)據(jù)項結(jié)構(gòu)中l(wèi)ist_head成員變量的地址,那么我們?nèi)绾瓮ㄟ^這個list_head成員訪問到作為它的所有者的節(jié)點數(shù)據(jù)呢?Linux為此提供了一個list_entry(ptr,type,member)宏,其中ptr是指向該數(shù)據(jù)中l(wèi)ist_head成員的指針,也就是存儲在鏈表中的地址值,type是數(shù)據(jù)項的類型,member則是數(shù)據(jù)項類型定義中l(wèi)ist_head成員的變量名,例如,我們要訪問nf_sockopts鏈表中首個nf_sockopt_ops變量,則如此調(diào)用:

            list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);
            

            這里"list"正是nf_sockopt_ops結(jié)構(gòu)中定義的用于鏈表操作的節(jié)點成員變量名。

            list_entry的使用相當(dāng)簡單,相比之下,它的實現(xiàn)則有一些難懂:

            #define list_entry(ptr, type, member) container_of(ptr, type, member)
            container_of宏定義在[include/linux/kernel.h]中:
            #define container_of(ptr, type, member) ({			\
                    const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
                    (type *)( (char *)__mptr - offsetof(type,member) );})
            offsetof宏定義在[include/linux/stddef.h]中:
            #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
            

            size_t最終定義為unsigned int(i386)。

            這里使用的是一個利用編譯器技術(shù)的小技巧,即先求得結(jié)構(gòu)成員在與結(jié)構(gòu)中的偏移量,然后根據(jù)成員變量的地址反過來得出屬主結(jié)構(gòu)變量的地址。

            container_of()和offsetof()并不僅用于鏈表操作,這里最有趣的地方是((type *)0)->member,它將0地址強制"轉(zhuǎn)換"為type結(jié)構(gòu)的指針,再訪問到type結(jié)構(gòu)中的member成員。在container_of宏中,它用來給typeof()提供參數(shù)(typeof()是gcc的擴展,和sizeof()類似),以獲得member成員的數(shù)據(jù)類型;在offsetof()中,這個member成員的地址實際上就是type數(shù)據(jù)結(jié)構(gòu)中member成員相對于結(jié)構(gòu)變量的偏移量。

            如果這么說還不好理解的話,不妨看看下面這張圖:


            圖5 offsetof()宏的原理
            圖5 offsetof()宏的原理

            對于給定一個結(jié)構(gòu),offsetof(type,member)是一個常量,list_entry()正是利用這個不變的偏移量來求得鏈表數(shù)據(jù)項的變量地址。

            b) 遍歷宏

            在[net/core/netfilter.c]的nf_register_sockopt()函數(shù)中有這么一段話:

            		……
            struct list_head *i;
            ……
            	list_for_each(i, &nf_sockopts) {
            		struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;
            		……
            	}
            	……
            	

            函數(shù)首先定義一個(struct list_head *)指針變量i,然后調(diào)用list_for_each(i,&nf_sockopts)進行遍歷。在[include/linux/list.h]中,list_for_each()宏是這么定義的:

                    	#define list_for_each(pos, head) \
            	for (pos = (head)->next, prefetch(pos->next); pos != (head); \
                    	pos = pos->next, prefetch(pos->next))
                    	

            它實際上是一個for循環(huán),利用傳入的pos作為循環(huán)變量,從表頭head開始,逐項向后(next方向)移動pos,直至又回到head(prefetch()可以不考慮,用于預(yù)取以提高遍歷速度)。

            那么在nf_register_sockopt()中實際上就是遍歷nf_sockopts鏈表。為什么能直接將獲得的list_head成員變量地址當(dāng)成struct nf_sockopt_ops數(shù)據(jù)項變量的地址呢?我們注意到在struct nf_sockopt_ops結(jié)構(gòu)中,list是其中的第一項成員,因此,它的地址也就是結(jié)構(gòu)變量的地址。更規(guī)范的獲得數(shù)據(jù)變量地址的用法應(yīng)該是:

            struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);
            

            大多數(shù)情況下,遍歷鏈表的時候都需要獲得鏈表節(jié)點數(shù)據(jù)項,也就是說list_for_each()和list_entry()總是同時使用。對此Linux給出了一個list_for_each_entry()宏:

            #define list_for_each_entry(pos, head, member)		……
            

            與list_for_each()不同,這里的pos是數(shù)據(jù)項結(jié)構(gòu)指針類型,而不是(struct list_head *)。nf_register_sockopt()函數(shù)可以利用這個宏而設(shè)計得更簡單:

            ……
            struct nf_sockopt_ops *ops;
            list_for_each_entry(ops,&nf_sockopts,list){
            	……
            }
            ……
            

            某些應(yīng)用需要反向遍歷鏈表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()來完成這一操作,使用方法和上面介紹的list_for_each()、list_for_each_entry()完全相同。

            如果遍歷不是從鏈表頭開始,而是從已知的某個節(jié)點pos開始,則可以使用list_for_each_entry_continue(pos,head,member)。有時還會出現(xiàn)這種需求,即經(jīng)過一系列計算后,如果pos有值,則從pos開始遍歷,如果沒有,則從鏈表頭開始,為此,Linux專門提供了一個list_prepare_entry(pos,head,member)宏,將它的返回值作為list_for_each_entry_continue()的pos參數(shù),就可以滿足這一要求。

            4. 安全性考慮

            在并發(fā)執(zhí)行的環(huán)境下,鏈表操作通常都應(yīng)該考慮同步安全性問題,為了方便,Linux將這一操作留給應(yīng)用自己處理。Linux鏈表自己考慮的安全性主要有兩個方面:

            a) list_empty()判斷

            基本的list_empty()僅以頭指針的next是否指向自己來判斷鏈表是否為空,Linux鏈表另行提供了一個list_empty_careful()宏,它同時判斷頭指針的next和prev,僅當(dāng)兩者都指向自己時才返回真。這主要是為了應(yīng)付另一個cpu正在處理同一個鏈表而造成next、prev不一致的情況。但代碼注釋也承認(rèn),這一安全保障能力有限:除非其他cpu的鏈表操作只有l(wèi)ist_del_init(),否則仍然不能保證安全,也就是說,還是需要加鎖保護。

            b) 遍歷時節(jié)點刪除

            前面介紹了用于鏈表遍歷的幾個宏,它們都是通過移動pos指針來達到遍歷的目的。但如果遍歷的操作中包含刪除pos指針?biāo)赶虻墓?jié)點,pos指針的移動就會被中斷,因為list_del(pos)將把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

            當(dāng)然,調(diào)用者完全可以自己緩存next指針使遍歷操作能夠連貫起來,但為了編程的一致性,Linux鏈表仍然提供了兩個對應(yīng)于基本遍歷操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它們要求調(diào)用者另外提供一個與pos同類型的指針n,在for循環(huán)中暫存pos下一個節(jié)點的地址,避免因pos節(jié)點被釋放而造成的斷鏈。


            四、 擴展

            1. hlist


            圖6 list和hlist
            圖6 list和hlist

            精益求精的Linux鏈表設(shè)計者(因為list.h沒有署名,所以很可能就是Linus Torvalds)認(rèn)為雙頭(next、prev)的雙鏈表對于HASH表來說"過于浪費",因而另行設(shè)計了一套用于HASH表應(yīng)用的hlist數(shù)據(jù)結(jié)構(gòu)--單指針表頭雙循環(huán)鏈表,從上圖可以看出,hlist的表頭僅有一個指向首節(jié)點的指針,而沒有指向尾節(jié)點的指針,這樣在可能是海量的HASH表中存儲的表頭就能減少一半的空間消耗。

            因為表頭和節(jié)點的數(shù)據(jù)結(jié)構(gòu)不同,插入操作如果發(fā)生在表頭和首節(jié)點之間,以往的方法就行不通了:表頭的first指針必須修改指向新插入的節(jié)點,卻不能使用類似list_add()這樣統(tǒng)一的描述。為此,hlist節(jié)點的prev不再是指向前一個節(jié)點的指針,而是指向前一個節(jié)點(可能是表頭)中的next(對于表頭則是first)指針(struct list_head **pprev),從而在表頭插入的操作可以通過一致的"*(node->pprev)"訪問和修改前驅(qū)節(jié)點的next(或first)指針。

            2. read-copy update

            在Linux鏈表功能接口中還有一系列以"_rcu"結(jié)尾的宏,與以上介紹的很多函數(shù)一一對應(yīng)。RCU(Read-Copy Update)是2.5/2.6內(nèi)核中引入的新技術(shù),它通過延遲寫操作來提高同步性能。

            我們知道,系統(tǒng)中數(shù)據(jù)讀取操作遠(yuǎn)多于寫操作,而rwlock機制在smp環(huán)境下隨著處理機增多性能會迅速下降(見參考資料4)。針對這一應(yīng)用背景,IBM Linux技術(shù)中心的Paul E. McKenney提出了"讀拷貝更新"的技術(shù),并將其應(yīng)用于Linux內(nèi)核中。RCU技術(shù)的核心是寫操作分為寫-更新兩步,允許讀操作在任何時候無阻訪問,當(dāng)系統(tǒng)有寫操作時,更新動作一直延遲到對該數(shù)據(jù)的所有讀操作完成為止。Linux鏈表中的RCU功能只是Linux RCU的很小一部分,對于RCU的實現(xiàn)分析已超出了本文所及,有興趣的讀者可以自行參閱本文的參考資料;而對RCU鏈表的使用和基本鏈表的使用方法基本相同。



            五、 示例

            附件中的程序除了能正向、反向輸出文件以外,并無實際作用,僅用于演示Linux鏈表的使用。

            為了簡便,例子采用的是用戶態(tài)程序模板,如果需要運行,可采用如下命令編譯:

            gcc -D__KERNEL__ -I/usr/src/linux-2.6.7/include pfile.c -o pfile
            

            因為內(nèi)核鏈表限制在內(nèi)核態(tài)使用,但實際上對于數(shù)據(jù)結(jié)構(gòu)本身而言并非只能在核態(tài)運行,因此,在筆者的編譯中使用"-D__KERNEL__"開關(guān)"欺騙"編譯器。


            參考資料

            1. 維基百科 http://zh.wikipedia.org,一個在GNU Documentation License下發(fā)布的網(wǎng)絡(luò)辭典,自由軟件理念的延伸,本文的"鏈表"概念即使用它的版本。
            2. 《Linux內(nèi)核情景分析》,毛德操先生的這本關(guān)于Linux內(nèi)核的巨著幾乎可以回答絕大部分關(guān)于內(nèi)核的問題,其中也包括內(nèi)核鏈表的幾個關(guān)鍵數(shù)據(jù)結(jié)構(gòu)。
            3. Linux內(nèi)核2.6.7源代碼,所有不明白的問題,只要潛心看代碼,總能清楚。
            4. Kernel Korner: Using RCU in the Linux 2.5 Kernel,RCU主要開發(fā)者Paul McKenney 2003年10月發(fā)表于Linux Journal上的一篇介紹RCU的文章。在 http://www.rdrop.com/users/paulmck/rclock/上可以獲得更多關(guān)于RCU的幫助。

            關(guān)于作者

            楊沙洲,目前在國防科技大學(xué)計算機學(xué)院攻讀軟件方向博士學(xué)位。對文中存在的技術(shù)問題,歡迎向 pubb@163.net質(zhì)疑。

            posted @ 2012-10-22 10:31 鑫龍 閱讀(388) | 評論 (0)編輯 收藏

            1.概述
                  CFS(completely fair schedule)是最終被內(nèi)核采納的調(diào)度器。它從RSDL/SD中吸取了完全公平的思想,不再跟蹤進程的睡眠時間,也不再企圖區(qū)分交互式進程。它將所有的進程都統(tǒng)一對待,這就是公平的含義。CFS的算法和實現(xiàn)都相當(dāng)簡單,眾多的測試表明其性能也非常優(yōu)越。

                  CFS 背后的主要想法是維護為任務(wù)提供處理器時間方面的平衡(公平性)。這意味著應(yīng)給進程分配相當(dāng)數(shù)量的處理器。分給某個任務(wù)的時間失去平衡時(意味著一個或多個任務(wù)相對于其他任務(wù)而言未被給予相當(dāng)數(shù)量的時間),應(yīng)給失去平衡的任務(wù)分配時間,讓其執(zhí)行。 

                  CFS拋棄了時間片,拋棄了復(fù)雜的算法,從一個新的起點開始了調(diào)度器的新時代,最開始的2.6.23版本,CFS提供一個虛擬的時鐘,所有進程復(fù)用這個虛擬時鐘的時間,CFS將時鐘的概念從底層體系相關(guān)的硬件中抽象出來,進程調(diào)度模塊直接和這個虛擬的時鐘接口 而不必再為硬件時鐘操作而操心,如此一來,整個進程調(diào)度模塊就完整了,從時鐘到調(diào)度算法,到不同進程的不同策略,全部都由虛擬系統(tǒng)提供,也正是在這個新的內(nèi)核,引入了調(diào)度類。因此新的調(diào)度器就是不同特性的進程在統(tǒng)一的虛擬時鐘下按照不同的策略被調(diào)度。

                  按照作者Ingo Molnar的說法:"CFS百分之八十的工作可以用一句話概括:CFS在真實的硬件上模擬了完全理想的多任務(wù)處理器"。在“完全理想的多任務(wù)處理器 “下,每個進程都能同時獲得CPU的執(zhí)行時間。當(dāng)系統(tǒng)中有兩個進程時,CPU的計算時間被分成兩份,每個進程獲得50%。然而在實際的硬件上,當(dāng)一個進程占用CPU時,其它進程就必須等待。這就產(chǎn)生了不公平。

            2.相關(guān)概念
            調(diào)度實體(sched entiy):就是調(diào)度的對象,可以理解為進程。
            虛擬運行時間(vruntime):即每個調(diào)度實體的運行時間。任務(wù)的虛擬運行時間越小, 意味著任務(wù)被允許訪問服務(wù)器的時間越短 — 其對處理器的需求越高。
            公平調(diào)度隊列(cfs_rq):采取公平調(diào)度的調(diào)度實體的運行隊列。

            3.CFS的核心思想
                  全公平調(diào)度器(CFS)的設(shè)計思想是:在一個真實的硬件上模型化一個理想的、精確的多任務(wù)CPU。該理想CPU模型運行在100%的負(fù)荷、在精確 平等速度下并行運行每個任務(wù),每個任務(wù)運行在1/n速度下,即理想CPU有n個任務(wù)運行,每個任務(wù)的速度為CPU整個負(fù)荷的1/n。
                  由于真實硬件上,每次只能運行一個任務(wù),這就得引入"虛擬運行時間"(virtual runtime)的概念,虛擬運行時間為一個任務(wù)在理想CPU模型上執(zhí)行的下一個時間片(timeslice)。實際上,一個任務(wù)的虛擬運行時間為考慮到運行任務(wù)總數(shù)的實際運行時間。 

                  CFS 背后的主要想法是維護為任務(wù)提供處理器時間方面的平衡(公平性)。CFS為了體現(xiàn)的公平表現(xiàn)在2個方面
            (1)進程的運行時間相等
                  CFS 在叫做虛擬運行時 的地方維持提供給某個任務(wù)的時間量。任務(wù)的虛擬運行時越小, 意味著任務(wù)被允許訪問服務(wù)器的時間越短 — 其對處理器的需求越高。
                        假設(shè)runqueue中有n個進程,當(dāng)前進程運行了 10ms。在“完全理想的多任務(wù)處理器”中,10ms應(yīng)該平分給n個進程(不考慮各個進程的nice值),因此當(dāng)前進程應(yīng)得的時間是(10/n)ms,但 是它卻運行了10ms。所以CFS將懲罰當(dāng)前進程,使其它進程能夠在下次調(diào)度時盡可能取代當(dāng)前進程。最終實現(xiàn)所有進程的公平調(diào)度。

            (2)睡眠的進程進行補償
                  CFS 還包含睡眠公平概念以便確保那些目前沒有運行的任務(wù)(例如,等待 I/O)在其最終需要時獲得相當(dāng)份額的處理器。 

                  CFS調(diào)度器的運行時間是O(logN),而以前的調(diào)度器的運行時間是O(1),這是不是就是說CFS的效率比O(1)的更差呢?
                  答案并不是那樣,我們知道 CFS調(diào)度器下的運行隊列是基于紅黑樹組織的,找出下一個進程就是截下左下角的節(jié)點,固定時間完成,所謂的O(logN)指的是插入時間,可是紅黑樹的統(tǒng) 計性能是不錯的,沒有多大概率真的用得了那么多時間,因為紅節(jié)點和黑節(jié)點的特殊排列方式既保證了樹的一定程度的平衡,又不至于花太多的時間來維持這種平 衡,插入操作大多數(shù)情況下都可以很快的完成,特別是對于組織得相當(dāng)好的數(shù)據(jù)。

            4.CFS的實現(xiàn)
            4.1 2.6.23 VS 2.6.25
                  在2.6.23內(nèi)核中,剛剛實現(xiàn)的CFS調(diào)度器顯得很淳樸,每次的時鐘滴答中都會將當(dāng)前進程先出隊,推進其虛擬時鐘和系統(tǒng)虛擬時鐘后再入隊,然后判斷紅黑 樹的左下角的進程是否還是當(dāng)前進程而抉擇是否要調(diào)度,這種調(diào)度器的key的計算是用當(dāng)前的虛擬時鐘減去待計算進程的等待時間,如果該計算進程在運行,那么其等待時間就是負(fù)值,這樣,等待越長的進程key越小,從而越容易被選中投入運行;
                  在2.6.25內(nèi)核以后實現(xiàn)了一種更為簡單的方式,就是設(shè)置一個運行隊列的虛擬時鐘,它單調(diào)增長并且跟蹤該隊列的最小虛擬時鐘的進程,key值由進程的vruntime和隊列的虛擬時鐘的差值計算,這種方式就是真正的追趕, 比2.6.23實現(xiàn)的簡單,但是很巧妙,不必在每次時鐘滴答中都將當(dāng)前進程出隊,入隊,而是根據(jù)當(dāng)前進程實際運行的時間和理想應(yīng)該運行的時間判斷是否應(yīng)該調(diào)度。

            4.2紅黑樹
                  與之前的 Linux 調(diào)度器不同,它沒有將任務(wù)維護在運行隊列中,CFS 維護了一個以時間為順序的紅黑樹(參見下圖)。 紅黑樹 是一個樹,具有很多有趣、有用的屬性。首先,它是自平衡的,這意味著樹上沒有路徑比任何其他路徑長兩倍以上。 第二,樹上的運行按 O(log n) 時間發(fā)生(其中 n 是樹中節(jié)點的數(shù)量)。這意味著您可以快速高效地插入或刪除任務(wù)。 


                  任務(wù)存儲在以時間為順序的紅黑樹中(由 sched_entity 對象表示),對處理器需求最多的任務(wù) (最低虛擬運行時)存儲在樹的左側(cè),處理器需求最少的任務(wù)(最高虛擬運行時)存儲在樹的右側(cè)。 為了公平,調(diào)度器先選取紅黑樹最左端的節(jié)點調(diào)度為下一個以便保持公平性。任務(wù)通過將其運行時間添加到虛擬運行時, 說明其占用 CPU 的時間,然后如果可運行,再插回到樹中。這樣,樹左側(cè)的任務(wù)就被給予時間運行了,樹的內(nèi)容從右側(cè)遷移到左側(cè)以保持公平。 因此,每個可運行的任務(wù)都會追趕其他任務(wù)以維持整個可運行任務(wù)集合的執(zhí)行平衡。 

            4.3 CFS內(nèi)部原理
                  Linux 內(nèi)的所有任務(wù)都由稱為 task_struct 的任務(wù)結(jié)構(gòu)表示。該結(jié)構(gòu)完整地描述了任務(wù)并包括了任務(wù)的當(dāng)前狀態(tài)、其堆棧、進程標(biāo)識、優(yōu)先級(靜態(tài)和動態(tài))等等。您可以在 ./linux/include/linux/sched.h 中找到這些內(nèi)容以及相關(guān)結(jié)構(gòu)。 但是因為不是所有任務(wù)都是可運行的,您在 task_struct 中不會發(fā)現(xiàn)任何與 CFS 相關(guān)的字段。 相反,會創(chuàng)建一個名為 sched_entity 的新結(jié)構(gòu)來跟蹤調(diào)度信息(參見下圖)。



                  樹的根通過 rb_root 元素通過 cfs_rq 結(jié)構(gòu)(在 ./kernel/sched.c 中)引用。紅黑樹的葉子不包含信息,但是內(nèi)部節(jié)點代表一個或多個可運行的任務(wù)。紅黑樹的每個節(jié)點都由 rb_node 表示,它只包含子引用和父對象的顏色。 rb_node 包含在 sched_entity 結(jié)構(gòu)中,該結(jié)構(gòu)包含 rb_node 引用、負(fù)載權(quán)重以及各種統(tǒng)計數(shù)據(jù)。最重要的是, sched_entity 包含 vruntime(64 位字段),它表示任務(wù)運行的時間量,并作為紅黑樹的索引。 最后,task_struct 位于頂端,它完整地描述任務(wù)并包含 sched_entity 結(jié)構(gòu)。 

                  CFS 調(diào)度函數(shù)非常簡單。 在 ./kernel/sched.c 中的 schedule() 函數(shù)中,它會先搶占當(dāng)前運行任務(wù)(除非它通過 yield() 代碼先搶占自己)。注意 CFS 沒有真正的時間切片概念用于搶占,因為搶占時間是可變的。 當(dāng)前運行任務(wù)(現(xiàn)在被搶占的任務(wù))通過對 put_prev_task 調(diào)用(通過調(diào)度類)返回到紅黑樹。 當(dāng) schedule 函數(shù)開始確定下一個要調(diào)度的任務(wù)時,它會調(diào)用 pick_next_task 函數(shù)。此函數(shù)也是通用的(在 ./kernel/sched.c 中),但它會通過調(diào)度器類調(diào)用 CFS 調(diào)度器。 CFS 中的 pick_next_task 函數(shù)可以在 ./kernel/sched_fair.c(稱為 pick_next_task_fair())中找到。 此函數(shù)只是從紅黑樹中獲取最左端的任務(wù)并返回相關(guān) sched_entity。通過此引用,一個簡單的 task_of() 調(diào)用確定返回的 task_struct 引用。通用調(diào)度器最后為此任務(wù)提供處理器。

            4.4 CFS的優(yōu)先級
                  CFS 不直接使用優(yōu)先級而是將其用作允許任務(wù)執(zhí)行的時間的衰減系數(shù)。 低優(yōu)先級任務(wù)具有更高的衰減系數(shù),而高優(yōu)先級任務(wù)具有較低的衰減系數(shù)。 這意味著與高優(yōu)先級任務(wù)相比,低優(yōu)先級任務(wù)允許任務(wù)執(zhí)行的時間消耗得更快。 這是一個絕妙的解決方案,可以避免維護按優(yōu)先級調(diào)度的運行隊列。

            posted @ 2012-10-16 16:30 鑫龍 閱讀(704) | 評論 (0)編輯 收藏

            進程在退出時,必須釋放它所擁有的資源,并通過某種方式告訴父進程。進程的退出一般是顯示或隱式地調(diào)用了eixt(),或者接受了某種信號。不過什么原因退出,最終都調(diào)用了do_exit。




            用于進程退出的系統(tǒng)調(diào)用有兩個exit和exit_group,exit只是終止某個進程,而exit_group整個線程中的進程。它們在內(nèi)核中的服務(wù)函數(shù)分別為sys_exit和sys_exit_group,它們又分別調(diào)用了do_exit和do_group_exit。而do_group最終又調(diào)用了do_exit。

             

            do_exit定義在kernel/exit.c中:

            僵死進程:僵死進程是一個進程已經(jīng)退出,它的內(nèi)存和資源已經(jīng)釋放掉了,但是位了時系統(tǒng)在它退出后能夠獲得它的退出狀態(tài)等信息,它的進程描述符仍然保留。

            一個進程退出時,它的父進程會接收到一個SIGCHLD信號,一般情況下這個信號的處理函數(shù)會執(zhí)行wait系列函數(shù)等待子進程的結(jié)束。從子進程退出到父進程調(diào)用wait(子進程結(jié)束)的這段時間,子進程稱為僵死進程。執(zhí)行ps –ef命令以“z”結(jié)尾的為僵死進程。

             

            僵死進程很特殊,它沒有任何可執(zhí)行代碼,不會被調(diào)度,只有一個進程描述符用來記錄退出等狀態(tài),除此之外不再占用其他任何資源。

             

            如果僵死進程的父進程沒有調(diào)用wait,則該進程會一直處于僵死狀態(tài)。如果父進程結(jié)束,內(nèi)核會在當(dāng)前線程組里為其找一個父進程,如果沒找到則把init作為其父進程,此時新的父進程將負(fù)責(zé)清楚其進程。如果父進程一直不結(jié)束,該進程會一直僵死。在root下用kill -9 也不能將其殺死。


            下面只對do_exit重點地方解析下: 


            1. struct task_struct *tsk = current;//獲取當(dāng)前要釋放進程的進程描述符   


              exit_signals(tsk);  /* sets PF_EXITING 以免內(nèi)和其他部分訪問該進程*/   

              exit_mm(tsk);  
            2.   
            3.     if (group_dead)  
            4.         acct_process();  
            5.     trace_sched_process_exit(tsk);  
            6.   
            7.     exit_sem(tsk);  
            8.     exit_files(tsk);  
            9.     exit_fs(tsk);  
            10.     check_stack_usage();  

            11. /*更新父子關(guān)系,并告訴父進程正在退出*/  
            12.     exit_notify(tsk, group_dead);
            13. /*切換到其他進程*/  
            14.     schedule(); 
            15.     exit_thread();  


            posted @ 2012-10-15 11:52 鑫龍 閱讀(501) | 評論 (0)編輯 收藏

            今天看到寫時拷貝這個概念,當(dāng)時一下沒有理解,后來查看一些網(wǎng)上的資料,找到了這篇文章,里面的那份個小程序能夠很好的說明進程創(chuàng)建寫時拷貝的概念。怕以后找不到就轉(zhuǎn)載了。嘿嘿。
            下面是那篇文章的原文:

            Linux進程創(chuàng)建,子進程對 父進程資源“寫時拷貝”的證明     傳統(tǒng)的fork()系統(tǒng)調(diào)用直接把所有的資源復(fù)制給新創(chuàng)建的進程。這種實現(xiàn)過于簡單并且效率低下,因為它拷貝的數(shù)據(jù)或許可以共享(This approach is significantly na?ve and inefficient in that it copies much data that might otherwise be shared.)。更糟糕的是,如果新進程打算立即執(zhí)行一個新的映像,那么所有的拷貝都將前功盡棄。
                Linux的fork()使用寫時拷貝 (copy- on-write)頁實現(xiàn)。寫時拷貝是一種可以推遲甚至避免拷貝數(shù)據(jù)的技術(shù)。內(nèi)核此 時并不復(fù)制整個進程的地址空間,而是讓父子進程共享同一個地址空間。只用在需要寫入的時候才會復(fù)制地址空間,從而使各個進行擁有各自的地址空間。也就是 說,資源的復(fù)制是在需要寫入的時候才會進行,在此之前,只有以只讀方式共享。這種技術(shù)使地址空間上的頁的拷貝被推遲到實際發(fā)生寫入的時候。在頁根本不會被 寫入的情況下---例如,fork()后立即執(zhí)行exec(),地址空間就無需被復(fù)制了。fork()的實際開銷就是復(fù)制父進程的頁表以及給子進程創(chuàng)建一 個進程描述符。下列程序可證明寫時拷貝:

            #include <stdio.h>

            #include <sched.h>

            int data = 10;

            int child_process()
            {
                printf("Child process %d, data %dn",getpid(),data);
                data = 20;
                printf("Child process %d, data %dn",getpid(),data);
                while(1);
            }

            int main(int argc, char* argv[])
            {
                if(fork()==0) {
                  child_process();    
                }else{
                    sleep(1);
                    printf("Parent process %d, data %dn",getpid(), data);
                    while(1);
                }
            }
            運行結(jié)果
            Child process 6427, data 10
            Child process 6427, data 20
            Parent process 6426, data 10 

                第1個Child process 6427, data 10是因為子進程創(chuàng)建時task_struct的mm直接拷貝自parent的mm;第2個Child process 6427, data 20是因為子進程進行了“寫時拷貝”,有了自己的dataa;第3個Parent process 6426, data 10輸出10是因為子進程的data和父進程的data不是同一份。
                如果把上述程序改為:

            #include <stdio.h>
            #include <sched.h>
            #include <stdlib.h>

            int data = 10;

            int child_process()
            {
                printf("Child process %d, data %dn",getpid(),data);
                data = 20;
                printf("Child process %d, data %dn",getpid(),data);
                while(1);
            }

            int main(int argc, char* argv[])
            {
                void **child_stack;
                child_stack = (void **) malloc(16384);
                clone(child_process, child_stack, CLONE_VM|CLONE_FILES|CLONE_SIGHAND, NULL);

                sleep(1);
                printf("Parent process %d, data %dn",getpid(), data);
                while(1);
            }

            運行結(jié)果將是
            Child process 6443, data 10
            Child process 6443, data 20
            Parent process 6442, data 20

                由于使用了CLONE_VM創(chuàng)建進程,子進程的mm實際直接指向父進程的mm,所以data是同一份。改變父子進程的data都會互相看到。 

            posted @ 2012-10-15 11:18 鑫龍 閱讀(203) | 評論 (0)編輯 收藏

            list_for_each遍歷子進程方法,順便分析下container_of宏的實現(xiàn)過程

            Linux系統(tǒng)中的每個進程都有一個父進程(init進程除外);每個進程還有0個或多個子進程。在進程描述符中parent指針指向其父進程,還有一個名為children的子進程鏈表(父進程task_struct中的children相當(dāng)于鏈表的表頭)。
            而我們可以使用list_for_each(/include/linux/list.h)來依次遍歷訪問子進程:
            1. struct task_struct *task;
            2. struct list_head *list;
            3. list_for_each(list, &current->children) {
            4.       task = list_entry(list, struct task_struct, sibling);
            5. }
            其中task即為某個子進程的地址
            首先需要說明一點task_struct中的children指針指向其某個子進程的進程描述符task_struct中children的地址,而非直接指向某個子進程的地址,也就是說子進程鏈表中存放的僅僅是各個task_struct成員children的地址。
            我們查看源文件找到list_for_each的定義:
            1. #define list_for_each(pos, head) \
            2.         for (pos = (head)->next; prefetch(pos->next), pos != (head); \
            3.                 pos = pos->next)
            從上可以看出list_for_each其實就是一個for循環(huán),在網(wǎng)上看到prefetch()是一個預(yù)抓取的函數(shù),我并不理解它(哪位大牛知道的講下哦),不過這個對for()并沒有多大的影響。for()實現(xiàn)的就是一個children鏈表的遍歷,而由children的地址如何取到task_struct的地址呢,它是由list_entry宏來實現(xiàn)的。
            我們先給出所需函數(shù)或宏的源代碼
            1. list_entry(/include/linux/list.h)
            2. #define list_entry(ptr, type, member) \
            3.         container_of(ptr, type, member)
            4. ---------------------------------------------------
            5. container_of(include/linux/kernel.h)
            6. #define container_of(ptr, type, member) ({                        \
            7.         const typeof( ((type *)0)->member ) *__mptr = (ptr);        \
            8.         (type *)( (char *)__mptr - offsetof(type,member) );})
            9. -------------------------------------------
            10. offsetof(/include/linux/stddef.h)
            11. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
            對于list_entry宏來說ptr在這里為指向children鏈表的指針,type為task_struct結(jié)構(gòu)體的類型,member為鏈表成員的變量名,即children。
            container_of()思路為先求出結(jié)構(gòu)體成員member(即children)在結(jié)構(gòu)體(即task_struct)中的偏移量,然后再根據(jù)member的地址(即ptr)來求出結(jié)構(gòu)體(即task_struct)的地址。

            哇哈哈  下面是我覺得最經(jīng)典的地方((type *)0)->member,他將地址0強制轉(zhuǎn)換為type類型的指針,然后再指向成員member,此時((type *)0)->member的地址即為member成員相對于結(jié)構(gòu)體的位移。
            其中typeof()相當(dāng)于C的sizeof(),(char *)__mptr這個強制轉(zhuǎn)換用來計算偏移字節(jié)量,size_t被定義為unsigned int 類型。



                   這樣這個過程就不難理解了吧


            PS:網(wǎng)上找到的list_entry宏定義的另一個版本(有人說是老版本kernel里面的),其實是一樣的,大家自己理解吧。^_^
            #define list_entry(ptr, type, member) \ 
            ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) 

            posted @ 2012-10-15 10:57 鑫龍 閱讀(365) | 評論 (0)編輯 收藏

            Linux通過slab分配器動態(tài)分配task_struct結(jié)構(gòu),該結(jié)構(gòu)定義在了<include/linux/sched.h>文件中,進程描述符中包含一個具體進程的所有信息,各個進程的task_struct存放在它們內(nèi)核棧的尾端。在棧底(對于向下增長的棧)或棧頂(對于向上增長的棧)創(chuàng)建一個新的結(jié)構(gòu)struct thread_info。利用這個新的機構(gòu)來迅速的找到task_struct的位置。
              下面是kernel2.6.32.10里task_struct的定義(對于x86類型的CPU來說文件位于:arch/x86/include/asm /include/asm/thread_info.h):
            1. struct thread_info {
            2.         struct task_struct        *task;                /* main task structure */
            3.         struct exec_domain        *exec_domain;        /* execution domain */
            4.         __u32                        flags;                /* low level flags */
            5.         __u32                        status;                /* thread synchronous flags */
            6.         __u32                        cpu;                /* current CPU */
            7.         int                        preempt_count;        /* 0 => preemptable, <0 => BUG */
            8.         mm_segment_t                addr_limit;
            9.         struct restart_block    restart_block;
            10.         void __user                *sysenter_return;
            11. #ifdef CONFIG_X86_32
            12.         unsigned long           previous_esp;   /* ESP of the previous stack in   case of nested (IRQ) stacks*/
            13.         __u8                        supervisor_stack[0];
            14. #endif
            15.         int                        uaccess_err;
            16. };
            其中的task的值就為task_struct的位置。
            kernel利用current宏尋找task_struct的位置,假設(shè)棧的大小為8k(13個二進制位),我們可以將進程棧的地址的后13位屏蔽掉,這樣得到的剛好就是進程棧的起始地址,而thread_info剛好就是位于進程棧的底部,所以進程棧的起始地址就是struct thread_info的地址,得到了thread_info的地址,我們就很容易找到task_struct的地址了。
            匯編實現(xiàn)過程為
            movl  %-8192 ,%eax
            andl   %esp ,%eax
            寄存器esp存放進程棧的當(dāng)前地址,eax最后存放的就是進程棧的起始地址。current使用current_thread_info來實現(xiàn)這個過程。
            kernel源碼(對于x86類型的CPU來說文件位于arch/x86/include/asm //include/asm/thread_info.h)
            1. /* how to get the current stack pointer from C */
            2. register unsigned long current_stack_pointer asm("esp") __used;

            3. /* how to get the thread information struct from C */
            4. static inline struct thread_info *current_thread_info(void)
            5. {
            6.         return (struct thread_info *)
            7.                 (current_stack_pointer & ~(THREAD_SIZE - 1));
            8. };
            其中current_stack_pointer為進程棧的當(dāng)前地址,THREAD_SIZE為進程棧的大小。
            所以current_thread_info()->task即為task_struct()的地址。 

            posted @ 2012-10-15 10:37 鑫龍 閱讀(582) | 評論 (0)編輯 收藏

            一、Observer模式的意圖:

                            在對象的內(nèi)部狀態(tài)發(fā)生變化時,自動通知外部對象進行響應(yīng)。

            二、Observer模式的構(gòu)成:

                           ·被觀察者:內(nèi)部狀態(tài)有可能被改變,而且又需要通知外部的對象

                           ·觀察者:需要對內(nèi)部狀態(tài)的改變做出響應(yīng)的對象

            三、Observer模式的Java實現(xiàn):

                            Java的API中已經(jīng)為我們提供了Observer模式的實現(xiàn)。具體由java.util.Observable類和java.util.Observer接口完成。

                            前者有兩個重要的方法:

                                   ·setChanged:設(shè)置內(nèi)部狀態(tài)為已改變

                                   ·notifyObservers(Object obj):通知觀察者所發(fā)生的改變,參數(shù)obj是一些改變的信息

                            后者有一個核心方法:

                                   ·update(Object obj):相應(yīng)被觀察者的改變,其中obj就是被觀察者傳遞過來的信息,該方法會在notifyObservers被調(diào)用時自動調(diào)用。

                          下面是Observer模式的實現(xiàn)過程:

                                 ·創(chuàng)建一個被觀察者,繼承java.util.Observable

                                 ·創(chuàng)建一個觀察者,實現(xiàn)java.util.Observer接口

                                · 注冊觀察著,調(diào)用addObserver(Observer observer)

                                ·在被觀察者改變對象內(nèi)部狀態(tài)的地方,調(diào)用setChanged()方法,然后調(diào)用notifyObservers(Object)方法,通知被觀察者

                               ·在觀察者的update(Object)方法中,對改變做出響應(yīng)。

            四、Observer模式的好處:

                             1.Observer模式的優(yōu)點:

                                   ·被觀察者只需要知道誰在觀察它,無需知道具體的觀察細(xì)節(jié)

                                   ·被觀察者一旦發(fā)生變化,只需要通過廣播的方式告知觀察者,至于消息如何到達則不需知道。這樣的話無疑消除了被觀察者和觀察者之間通信的硬編碼

                                   ·當(dāng)一個被觀察者同時被多個觀察著觀察時,觀察者可以只選擇自己感興趣的事件,而忽略其它的事件
               
                                  ·多個觀察者組合起來可以形成一個觀察鏈,如果一旦需要回滾多個操作,此時觀察鏈可以發(fā)揮作用

                                  ·觀察者可以實時對被觀察對象的變化做出響應(yīng),例如自動告警、中斷運行等


                            2.運用Observer模式可以

                                 ·屏蔽線程間的通信機制:例如兩個線程之間,主線程可以作為觀察者,執(zhí)行線程是被觀察者。彼此之間只知道對方存在,但不知道之間通信的細(xì)節(jié)

                                ·消除硬編碼:如果沒有Observer模式,則只能采用回調(diào)的模式,或者在代碼中顯示地調(diào)用觀察者

                                ·優(yōu)化異常機制:特別適合在異常發(fā)生時向頂層監(jiān)控,減少try-catch代碼量

            代碼:

             

            1. public class Observable {      
            2.     private boolean changed = false;      
            3.     private Vector obs;      
            4.          
            5.     //創(chuàng)建被觀察者時就創(chuàng)建一個它持有的觀察者列表,注意,這個列表是需要同步的。      
            6.     public Observable() {      
            7.     obs = new Vector();      
            8.     }      
            9.      
            10.     /**    
            11.      * 添加觀察者到觀察者列表中去    
            12.      */     
            13.     public synchronized void addObserver(Observer o) {      
            14.         if (o == null)      
            15.             throw new NullPointerException();      
            16.     if (!obs.contains(o)) {      
            17.         obs.addElement(o);      
            18.     }      
            19.     }      
            20.      
            21.     /**    
            22.      * 刪除一個觀察者    
            23.      */     
            24.     public synchronized void deleteObserver(Observer o) {      
            25.         obs.removeElement(o);      
            26.     }      
            27.      
            28.     /**    
            29.      * 通知操作,即被觀察者發(fā)生變化,通知對應(yīng)的觀察者進行事先設(shè)定的操作,不傳參數(shù)的通知方法    
            30.      */     
            31.     public void notifyObservers() {      
            32.     notifyObservers(null);      
            33.     }      
            34.      
            35.     /**    
            36.      * 與上面的那個通知方法不同的是,這個方法接受一個參數(shù),這個參數(shù)一直傳到觀察者里,以供觀察者使用    
            37.      */     
            38.     public void notifyObservers(Object arg) {      
            39.           
            40.         Object[] arrLocal;      
            41.      
            42.     synchronized (this) {      
            43.         if (!changed)      
            44.                 return;      
            45.             arrLocal = obs.toArray();      
            46.             clearChanged();      
            47.         }      
            48.      
            49.         for (int i = arrLocal.length-1; i>=0; i--)      
            50.             ((Observer)arrLocal[i]).update(this, arg);      
            51.     }      
            52. }     
            53. public interface Observer {      
            54.     /**    
            55.      * This method is called whenever the observed object is changed. An    
            56.      * application calls an <tt>Observable</tt> object's    
            57.      * <code>notifyObservers</code> method to have all the object's    
            58.      * observers notified of the change.    
            59.      *    
            60.      * @param   o     the observable object.    
            61.      * @param   arg   an argument passed to the <code>notifyObservers</code>    
            62.      *                 method.    
            63.      */     
            64.     void update(Observable o, Object arg);      
            65. }      
            66. }     
            67. public class MailObserver implements Observer{      
            68.      
            69.     /**    
            70.      * 這個類取名為MailObserver,顧名思義,她是一個用來發(fā)送郵件的觀察者    
            71.      */     
            72.     public void update(Observable o, Object arg) {      
            73.         System.out.println("發(fā)送郵件的觀察者已經(jīng)被執(zhí)行");      
            74.     }      
            75. }     
            76. public class JMSObserver implements Observer{      
            77.      
            78.     public void update(Observable o, Object arg) {      
            79.         System.out.println("發(fā)送消息給jms服務(wù)器的觀察者已經(jīng)被執(zhí)行");      
            80.     }      
            81. }     
            82. public class Subject extends Observable{      
            83.           
            84.     /**    
            85.      * 業(yè)務(wù)方法,一旦執(zhí)行某個操作,則通知觀察者    
            86.      */     
            87.     public void doBusiness(){      
            88.         if (true) {      
            89.             super.setChanged();      
            90.         }      
            91.         notifyObservers("現(xiàn)在還沒有的參數(shù)");      
            92.     }      
            93.      
            94.           
            95.     public static void main(String [] args) {      
            96.         //創(chuàng)建一個被觀察者      
            97.         Subject subject = new Subject();      
            98.               
            99.         //創(chuàng)建兩個觀察者      
            100.         Observer mailObserver = new MailObserver();      
            101.         Observer jmsObserver = new JMSObserver();      
            102.               
            103.         //把兩個觀察者加到被觀察者列表中      
            104.         subject.addObserver(mailObserver);      
            105.         subject.addObserver(jmsObserver);      
            106.               
            107.         //執(zhí)行業(yè)務(wù)操作      
            108.         subject.doBusiness();      
            109.     }      
            110. }    

             

             

            posted @ 2012-09-26 14:30 鑫龍 閱讀(322) | 評論 (0)編輯 收藏

            Java RMI 指的是遠(yuǎn)程方法調(diào)用 (Remote Method Invocation)。它是一種機制,能夠讓在某個 Java 虛擬機上的對象調(diào)用另一個 Java 虛擬機中的對象上的方法。可以用此方法調(diào)用的任何對象必須實現(xiàn)該遠(yuǎn)程接口。
             
            Java RMI不是什么新技術(shù)(在Java1.1的時代都有了),但卻是是非常重要的底層技術(shù)。
            大名鼎鼎的EJB都是建立在rmi基礎(chǔ)之上的,現(xiàn)在還有一些開源的遠(yuǎn)程調(diào)用組件,其底層技術(shù)也是rmi。
             
            在大力鼓吹Web Service、SOA的時代,是不是每個應(yīng)用都應(yīng)該選用笨拙的Web Service組件來實現(xiàn),通過對比測試后,RMI是最簡單的,在一些小的應(yīng)用中是最合適的。
             
            下面通過一個簡單的例子來說明RMI的原理和應(yīng)用,下面這個例子是一個簡單HelloWorld,但已涵蓋RMI的核心應(yīng)用與開發(fā)模式。
             
            /** 
            * Created by IntelliJ IDEA. 
            * User: leizhimin 
            * Date: 2008-8-7 21:50:02 
            * 定義一個遠(yuǎn)程接口,必須繼承Remote接口,其中需要遠(yuǎn)程調(diào)用的方法必須拋出RemoteException異常 
            */ 
            public interface IHello extends Remote { 

                /** 
                 * 簡單的返回“Hello World!"字樣 
                 * @return 返回“Hello World!"字樣 
                 * @throws java.rmi.RemoteException 
                 */ 
                public String helloWorld() throws RemoteException; 

                /** 
                 * 一個簡單的業(yè)務(wù)方法,根據(jù)傳入的人名返回相應(yīng)的問候語 
                 * @param someBodyName  人名 
                 * @return 返回相應(yīng)的問候語 
                 * @throws java.rmi.RemoteException 
                 */ 
                public String sayHelloToSomeBody(String someBodyName) throws RemoteException; 
            }
             
            /** 
            * Created by IntelliJ IDEA. 
            * User: leizhimin 
            * Date: 2008-8-7 21:56:47 
            * 遠(yuǎn)程的接口的實現(xiàn) 
            */ 
            public class HelloImpl extends UnicastRemoteObject implements IHello { 
                /** 
                 * 因為UnicastRemoteObject的構(gòu)造方法拋出了RemoteException異常,因此這里默認(rèn)的構(gòu)造方法必須寫,必須聲明拋出RemoteException異常 
                 * 
                 * @throws RemoteException 
                 */ 
                public HelloImpl() throws RemoteException { 
                } 

                /** 
                 * 簡單的返回“Hello World!"字樣 
                 * 
                 * @return 返回“Hello World!"字樣 
                 * @throws java.rmi.RemoteException 
                 */ 
                public String helloWorld() throws RemoteException { 
                    return "Hello World!"; 
                } 

                /** 
                 * 一個簡單的業(yè)務(wù)方法,根據(jù)傳入的人名返回相應(yīng)的問候語 
                 * 
                 * @param someBodyName 人名 
                 * @return 返回相應(yīng)的問候語 
                 * @throws java.rmi.RemoteException 
                 */ 
                public String sayHelloToSomeBody(String someBodyName) throws RemoteException { 
                    return "你好," + someBodyName + "!"; 
                } 
            }
             
            /** 
            * Created by IntelliJ IDEA. 
            * User: leizhimin 
            * Date: 2008-8-7 22:03:35 
            * 創(chuàng)建RMI注冊表,啟動RMI服務(wù),并將遠(yuǎn)程對象注冊到RMI注冊表中。 
            */ 
            public class HelloServer { 
                public static void main(String args[]) { 

                    try { 
                        //創(chuàng)建一個遠(yuǎn)程對象 
                        IHello rhello = new HelloImpl(); 
                        //本地主機上的遠(yuǎn)程對象注冊表Registry的實例,并指定端口為8888,這一步必不可少(Java默認(rèn)端口是1099),必不可缺的一步,缺少注冊表創(chuàng)建,則無法綁定對象到遠(yuǎn)程注冊表上 
                        LocateRegistry.createRegistry(8888); 

                        //把遠(yuǎn)程對象注冊到RMI注冊服務(wù)器上,并命名為RHello 
                        //綁定的URL標(biāo)準(zhǔn)格式為:rmi://host:port/name(其中協(xié)議名可以省略,下面兩種寫法都是正確的) 
                        Naming.bind("rmi://localhost:8888/RHello",rhello); 
            //            Naming.bind("http://localhost:8888/RHello",rhello); 

                        System.out.println(">>>>>INFO:遠(yuǎn)程IHello對象綁定成功!"); 
                    } catch (RemoteException e) { 
                        System.out.println("創(chuàng)建遠(yuǎn)程對象發(fā)生異常!"); 
                        e.printStackTrace(); 
                    } catch (AlreadyBoundException e) { 
                        System.out.println("發(fā)生重復(fù)綁定對象異常!"); 
                        e.printStackTrace(); 
                    } catch (MalformedURLException e) { 
                        System.out.println("發(fā)生URL畸形異常!"); 
                        e.printStackTrace(); 
                    } 
                } 
            }
             
            /** 
            * Created by IntelliJ IDEA. 
            * User: leizhimin 
            * Date: 2008-8-7 22:21:07 
            * 客戶端測試,在客戶端調(diào)用遠(yuǎn)程對象上的遠(yuǎn)程方法,并返回結(jié)果。 
            */ 
            public class HelloClient { 
                public static void main(String args[]){ 
                    try { 
                        //在RMI服務(wù)注冊表中查找名稱為RHello的對象,并調(diào)用其上的方法 
                        IHello rhello =(IHello) Naming.lookup("rmi://localhost:8888/RHello"); 
                        System.out.println(rhello.helloWorld()); 
                        System.out.println(rhello.sayHelloToSomeBody("熔巖")); 
                    } catch (NotBoundException e) { 
                        e.printStackTrace(); 
                    } catch (MalformedURLException e) { 
                        e.printStackTrace(); 
                    } catch (RemoteException e) { 
                        e.printStackTrace();   
                    } 
                } 
            }
             
            運行RMI服務(wù)端程序:
             
            運行RMI客戶端程序:
             
            總結(jié):
            從上面的過程來看,RMI對服務(wù)器的IP地址和端口依賴很緊密,但是在開發(fā)的時候不知道將來的服務(wù)器IP和端口如何,但是客戶端程序依賴這個IP和端口。
            這也是RMI的局限性之一。這個問題有兩種解決途徑:一是通過DNS來解決,二是通過封裝將IP暴露到程序代碼之外。
            RMI的局限性之二是RMI是Java語言的遠(yuǎn)程調(diào)用,兩端的程序語言必須是Java實現(xiàn),對于不同語言間的通訊可以考慮用Web Service或者公用對象請求代理體系(CORBA)來實現(xiàn)。

            posted @ 2012-09-26 14:07 鑫龍 閱讀(253) | 評論 (0)編輯 收藏

            僅列出標(biāo)題
            共20頁: First 12 13 14 15 16 17 18 19 20 
            国产精品久久久久乳精品爆| www亚洲欲色成人久久精品| 久久精品中文字幕有码| 韩国三级中文字幕hd久久精品 | 久久99精品国产麻豆宅宅| 久久久久国产精品嫩草影院| 精品久久8x国产免费观看| 99精品伊人久久久大香线蕉| 欧美一区二区久久精品| 国产精品久久久久影院色| 综合久久一区二区三区 | 合区精品久久久中文字幕一区| 国产精品久久久久久久人人看| 久久被窝电影亚洲爽爽爽| 美女久久久久久| 91精品国产91久久| 久久精品国产亚洲av麻豆小说 | 四虎国产永久免费久久| 久久99热这里只频精品6| 嫩草影院久久99| 无码人妻久久一区二区三区免费 | 久久精品国产一区二区| 国产精品美女久久久久| 久久只有这里有精品4| 久久久久成人精品无码 | 国产成人精品久久| 蜜桃麻豆www久久国产精品| 国产福利电影一区二区三区,免费久久久久久久精 | 伊人久久大香线蕉综合5g| 久久久久亚洲AV成人网人人软件| 丁香五月网久久综合| 久久精品无码专区免费东京热 | 国色天香久久久久久久小说| 思思久久99热免费精品6| 精品久久久久久国产免费了| 久久美女人爽女人爽| 亚洲国产精久久久久久久| 国产精品美女久久久久网| 久久精品国产免费一区| 一本大道加勒比久久综合| 丁香五月综合久久激情|