linux指導(dǎo)內(nèi)核編程如果第一個(gè)程序員是山頂洞人它在山洞壁第一臺(tái)計(jì)算機(jī)上鑿出的_第1頁
linux指導(dǎo)內(nèi)核編程如果第一個(gè)程序員是山頂洞人它在山洞壁第一臺(tái)計(jì)算機(jī)上鑿出的_第2頁
linux指導(dǎo)內(nèi)核編程如果第一個(gè)程序員是山頂洞人它在山洞壁第一臺(tái)計(jì)算機(jī)上鑿出的_第3頁
linux指導(dǎo)內(nèi)核編程如果第一個(gè)程序員是山頂洞人它在山洞壁第一臺(tái)計(jì)算機(jī)上鑿出的_第4頁
linux指導(dǎo)內(nèi)核編程如果第一個(gè)程序員是山頂洞人它在山洞壁第一臺(tái)計(jì)算機(jī)上鑿出的_第5頁
已閱讀5頁,還剩66頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

第1 o,如果第一個(gè)程序員是一個(gè)山頂洞人,它在山洞壁(第一臺(tái)計(jì)算機(jī))上鑿出的第一個(gè)程序應(yīng)該是用羚羊圖案構(gòu)成的一個(gè)字符串“o,orldSalut,Mundi”開始的。我不知道如果打破這個(gè)傳統(tǒng)會(huì)帶來什么,至少我還沒有勇氣去做第一個(gè)吃螃蟹的人。init_module和cleanup_module。第一個(gè)函數(shù)是在把模塊插init_module可以為內(nèi)核的某些東西一個(gè)處理程序,或者也可以用自身的代碼來取代某個(gè)內(nèi)核函數(shù)(通常是先干點(diǎn)別的什么事,然后再調(diào)用原來的函數(shù))。函數(shù)cleanup_module的任務(wù)是清除掉init_module所做的一切,這樣,這個(gè)模塊就可以安全地卸載了。146第二部分Linux內(nèi)核模塊編程指 內(nèi)核模塊并不是一個(gè)獨(dú)立的可執(zhí)行文件,而是一個(gè)對(duì)象文件,在運(yùn)行時(shí)內(nèi)核模塊被到內(nèi)核中。因此,應(yīng)該使用-c命令參數(shù)來編譯它們。還有一點(diǎn)需要注意,在編譯所有內(nèi)核模塊時(shí),都將需要定義好某些特定的符號(hào)。__KERNEL__—這個(gè)符號(hào)告訴頭文件:這個(gè)程序代碼將在內(nèi)核模式下運(yùn)行,而不要作為用戶進(jìn)程的一部分來執(zhí)行。LINUX—從技術(shù)的角度講,這個(gè)符號(hào)不是必需的。然而,如果程序員想要編寫一個(gè)重要的內(nèi)核模塊,而且這個(gè)內(nèi)核模塊需要在多個(gè)操作系統(tǒng)上編譯,在這種情況下,程序員將會(huì)很高興自己定義了LINUX這個(gè)符號(hào)。這樣一來,在那些依賴于操作系統(tǒng)的部分,這個(gè)符號(hào)就可以提供條件編譯了。還有其它的一些符號(hào),是否包含它們要取決于在編譯內(nèi)核時(shí)使用了哪些命令參數(shù)。如果/usr/include/linux/config.h。__SMP__—對(duì)稱多處理。如果編譯內(nèi)核的目的是為了支持對(duì)稱多處理,在編譯時(shí)就需要定義這個(gè)符號(hào)(即使內(nèi)核只是在一個(gè)CPU上運(yùn)行也需要定義它)。當(dāng)然,如果用戶使(參見第12章)。CONFIG_MODVERSIONS—如果CONFIG_MODVERSIONS可用,那么在編譯內(nèi)核模塊時(shí)就需要定義它,并且包含頭文件/usr/include/linux/modversions.h。還可以用代碼自身來完成這個(gè)任務(wù)。完成了以上這些任務(wù)以后,剩下唯一要做的事就是切換到根用戶下(你不是以root編譯),然后根據(jù)自己的需要插入或刪除o模塊。在執(zhí)行完順便提一下,Makefile建議用戶不要從X執(zhí)行insmod命令的原因在于,當(dāng)內(nèi)核有個(gè)消息需要使用printk命令打印出來時(shí),內(nèi)核會(huì)把該消息發(fā)送給控制臺(tái)。當(dāng)用戶沒有使用X時(shí),該消息第第1 將發(fā)送到用戶正在使用的虛擬終端(用戶可以用Alt-F<n>來選擇當(dāng)前終端),然后用戶就可以看到這個(gè)消息了。而另一方面,當(dāng)用戶使用X時(shí),存在兩種可能性。一種情況是用戶用命令xterm-C打開了一個(gè)控制臺(tái),這時(shí)輸出將被發(fā)送到那個(gè)控制臺(tái);另一種情況是用戶沒有打開控制臺(tái),這時(shí)輸出將送往虛擬終端7—被X所“覆蓋”的一個(gè)虛擬終端。X的用戶更有可能取得調(diào)試信息。如果沒有使用X,printk中printxerm-P當(dāng)服務(wù)器獲得CPU時(shí)間時(shí),它將顯示該消息—潰或者重新啟動(dòng),所以用戶不希望推遲錯(cuò)誤信息顯示的時(shí)間,因?yàn)樵撔畔⒖赡軙?huì)向用戶解釋什么方了題如示時(shí)晚系重的刻用將錯(cuò)個(gè)要信。除了一個(gè)源文件以外,在其它所有源文件中加入一行#define__NO_VERSION__。這點(diǎn)module.h中通常會(huì)包含有kernel_version的定義(kernel_version是一個(gè)全局變量,它)。如果用戶需要version.h文件,那么用戶必須自己把它包含在源文件中,因?yàn)樵诙x了__NO_VERSION__module.h是不會(huì)為用戶完成這個(gè)任務(wù)的。把所有的對(duì)象文件組合進(jìn)一個(gè)文件中。 x86下,可以使用命令ld-melf_i386-r-o〈模塊名稱〉.o第一個(gè)源文件).o第二個(gè)源文件).o來完成這個(gè)任務(wù)。148第二部分Linux內(nèi)核模塊編程指 第2 我們現(xiàn)在就可以吹牛說自己是內(nèi)核程序員了。雖然我們所寫的內(nèi)核模塊還什么也干不了,但我們?nèi)匀粸樽约焊械津湴粒喼笨梢苑Q得上趾高氣揚(yáng)。但是,有時(shí)候在某種程度上我們也會(huì)感到缺少點(diǎn)什么,簡單的模塊并不是太有趣。內(nèi)核模塊主要通過兩種法與進(jìn)程打交道法通過設(shè)備文件(例如在/dev的文件),另法是使用proc文件系統(tǒng)。既然編寫內(nèi)核模塊的主要原因之一就是支持某些類型的硬件設(shè)備,那么就讓我們從設(shè)備文件開始吧。設(shè)備文件最初的用途是使進(jìn)程與內(nèi)核中的設(shè)備驅(qū)動(dòng)程序通信,并且通過設(shè)備驅(qū)動(dòng)程序再與物理設(shè)備(調(diào)制解調(diào)器、終端等等)通信。下面我們要講述實(shí)現(xiàn)這一任務(wù)的方法。/proc/devices中找到驅(qū)動(dòng)程序以及它們對(duì)應(yīng)的主編號(hào)的列表。由設(shè)備驅(qū)動(dòng)程序管理的每個(gè)物理設(shè)備都被賦予一個(gè)從編號(hào)。這些設(shè)備中的每一個(gè),不管是否真正安裝在計(jì)算機(jī)系統(tǒng)上,都將/dev例如,如果執(zhí)行命令ls-l/dev/hd[ab]*,用戶將可以看到與一個(gè)計(jì)算機(jī)相連接的所有的IDE3,但是從編號(hào)卻各不相同。需要強(qiáng)調(diào)的是,這里假設(shè)用戶使用的是PC體系結(jié)構(gòu)。我并不知道在其它體系結(jié)構(gòu)上運(yùn)行的在安裝了系統(tǒng)以后,所有的設(shè)備文件都由命令mknod創(chuàng)建出來。從技術(shù)的角度上講,并沒有什么特別的原因一定要把這些設(shè)備文件放在 /dev中,這只不過是一個(gè)有用的傳統(tǒng)習(xí)慣而已。如果讀者創(chuàng)建設(shè)備文件的目的只不過是為了試試看,就像本章的練樣,那么把該設(shè)備文件放置在編譯內(nèi)核模塊的中可能會(huì)更有意義一些。設(shè)備一般分為兩種類型:字符設(shè)備和塊設(shè)備。它們的區(qū)別在于塊設(shè)備具有一個(gè)請(qǐng)求緩沖區(qū),所以塊設(shè)備可以選擇按照何種順序來響應(yīng)這些請(qǐng)求。這對(duì)于設(shè)備來說是很重要的。在設(shè)備中,讀或?qū)懴噜彽纳葏^(qū)速度要快一些,而讀寫相互之間離得較遠(yuǎn)的扇區(qū)則要慢得(塊的大小根據(jù)設(shè)備類型的變化而有所不同),而字符設(shè)備則可以隨心所欲地使用任意數(shù)目的字節(jié)。當(dāng)前大多數(shù)設(shè)備都是字符設(shè)備,因?yàn)樗鼈兗炔恍枰撤N形式的緩沖,也不需要按照固定的塊大小來進(jìn)行操作。如ls-l,查看一下該命bc的是字符設(shè)備。模塊分為兩個(gè)獨(dú)立的部分:模塊部分和設(shè)備驅(qū)動(dòng)程序部分。前者用于設(shè)備。函數(shù)init_module調(diào)用module_register_chrdev,把該設(shè)備驅(qū)動(dòng)程序加入到內(nèi)核的字符設(shè)備驅(qū)動(dòng)程序表中,它還會(huì)返回供驅(qū)程序所使用的主號(hào)。函數(shù)cleanup_module則取消該設(shè)備的。某設(shè)備和取消它的是這兩個(gè)函數(shù)最基本的功能。內(nèi)核中的東西并不是按照它們自己的意愿主動(dòng)開始運(yùn)行的,就像進(jìn)程一樣,而是由進(jìn)程通過系統(tǒng)調(diào)用來調(diào)用,或者由硬件設(shè)備通過中斷來調(diào)用,或者由內(nèi)核的其它部分調(diào)用只需調(diào)用特定的函數(shù),它們才會(huì)執(zhí)行。因此,如果用戶往內(nèi)核中加入了代碼,就必須把它作為某種特定類型的處理程序進(jìn)行注;除碼戶消。設(shè)備驅(qū)動(dòng)程序一般是由四個(gè)device_<action>函數(shù)所組成的,如果用戶需要處理具有對(duì)應(yīng)file_operations結(jié)構(gòu)Fops內(nèi)核可以知道調(diào)用哪些函數(shù)。因?yàn)樵摻Y(jié)構(gòu)的值是在設(shè)備時(shí)給定的,它包含了指向這四個(gè)函數(shù)的指針。在這里我們還需要記住的一點(diǎn)是:無論如何不能亂刪內(nèi)核模塊。原因在于如果設(shè)備文件是由進(jìn)程打開的,而我們刪去了該內(nèi)核模塊,那么使用該文件就將導(dǎo)致對(duì)正確的函數(shù)(讀/寫)原來所處的位置的調(diào)用。如果我們走運(yùn),那里沒有裝入什么其它的代碼,那我們至多得到一些難看的錯(cuò)誤信息,而如果我們不走運(yùn),在原來的同一位置已經(jīng)裝入了另一個(gè)內(nèi)核模塊,這就意味著跳轉(zhuǎn)到了內(nèi)核中另一個(gè)函數(shù)的中間,這樣做的是設(shè)想的,起碼不會(huì)是令(一個(gè)負(fù)數(shù))。而對(duì)cleanup_module來說這是不可能的,因?yàn)樗且粋€(gè)void函數(shù)。一旦調(diào)用了cleanup_module,這個(gè)模塊就死了。然而,還有一個(gè)計(jì)數(shù)器記錄了有多少個(gè)其它的內(nèi)核模塊正在使用該內(nèi)核模塊,這個(gè)計(jì)數(shù)器稱為計(jì)數(shù)器(就是位于文件/proc/modules信息行中的最后那個(gè)數(shù)值)。如果這個(gè)數(shù)值不為零, rmmod將失敗。模塊的計(jì)數(shù)值可以從變量mod_use_count_中得到。因?yàn)橛行┖晔菍iT為處理這個(gè)變量而定義的(如MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT),我們寧愿使用這些宏,也不愿直接對(duì)mod_use_count_進(jìn)行操作,這樣一來,如果將來實(shí)現(xiàn)方法有所變化,我們也會(huì)很安全。可能會(huì)加入新的系統(tǒng)調(diào)用,但是老的系統(tǒng)調(diào)用是不會(huì)改變的。這主要是為了提供向后兼容性的需要—新的內(nèi)核版本不應(yīng)該使原來工作正常的進(jìn)程出現(xiàn)問題。在大多數(shù)情況下,設(shè)備文件也應(yīng)該保持不變。另一方面,內(nèi)核里面的內(nèi)部接口則可以變化,并且也確實(shí)隨著內(nèi)核版本的變化而改變了。Linux內(nèi)核的版本可以劃分為穩(wěn)定版本(n.<偶數(shù)>.m)和開發(fā)版本(n.<奇數(shù)>.m)兩種。開發(fā)版本中包括所有最酷的思想,當(dāng)然其中也可能有一些將來會(huì)被認(rèn)為是餿主意的錯(cuò)誤,有些()(帶數(shù)字m的版本)而認(rèn)為接口是保持不變的。這個(gè)MPG版本包含對(duì)Linu內(nèi)核版本2.0.x和版本2.2.x的支持。因?yàn)檫@兩個(gè)版本之間存在差異,這就要求用戶根據(jù)內(nèi)核版本號(hào)來進(jìn)行條件編譯。為了做到這一點(diǎn),可以使用宏LINUX_VERSION_CODE。在內(nèi)核版本a.b.c中,該宏的值將會(huì)是216a+8b+c。為了獲取特定內(nèi)核版本的值,我們可以使用宏KERNEL_VERSION。在2.0.35中該宏沒有定義,如果需要的話我們可以自己定義它。第3 /proc文件系統(tǒng)。最初/proc文件系統(tǒng)是為了可以輕松有關(guān)進(jìn)程的信息而設(shè)計(jì)的(這就是它的名稱的由來),現(xiàn)在每一個(gè)內(nèi)核部分只要有些信息需要報(bào)告,都可以使用/proc/proc/modules/proc/meminfo使用/proc文件系統(tǒng)的方法其實(shí)與使用設(shè)備驅(qū)動(dòng)程序的方法是非常類似的—用戶需要?jiǎng)?chuàng)建一個(gè)結(jié)構(gòu),該結(jié)構(gòu)包含了/proc文件所需要的所有信息,包括指向任意處理程序函數(shù)的指針(/proc文件時(shí)將調(diào)用這個(gè)函數(shù))。然后,ni_mdue將向內(nèi)核這個(gè)結(jié)構(gòu),ceaup_odul將取它的。在程序中之所以需要使用proc_register_dynamic,是因?yàn)槲覀儾幌胧孪扰袛辔募褂玫乃饕?jié)點(diǎn)編號(hào),而是讓內(nèi)核去決定,這樣可以避免編號(hào)。一般文件系統(tǒng)都是位于磁盤上的,而不是僅僅存在于內(nèi)存中(/proc是存在于內(nèi)存中的),在那種情況下,索引節(jié)點(diǎn)編號(hào)是一(簡寫為inode)。索引節(jié)點(diǎn)包含該文件的有關(guān)信息,例如文件的權(quán)限,以及指向某個(gè)或者某些磁盤位置的指針,在這個(gè)或者這些磁盤位置中,存放著文件的數(shù)據(jù)。因?yàn)樵谖募蜷_或者關(guān)閉時(shí)該模塊不會(huì)被調(diào)用,所以我們無需在該模塊中使用MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT。如果文件打開以后模塊被刪除了,沒有任何措施可以避免這一。在下一章中,學(xué)習(xí)到一種比較難于實(shí)現(xiàn),但卻相對(duì)方便的方法,可以用于處理/proc文件,我們也可以使用那個(gè)方法來防止這個(gè)問題。160第二部分Linux內(nèi)核模塊編程指 第4 到目前為止,可以通過兩種方法從核模塊產(chǎn)生輸:我們可以一個(gè)設(shè)備驅(qū)動(dòng)程序,并使用mknod/proc文件。這樣內(nèi)核模塊就可以告訴我們各種各樣的信息?,F(xiàn)在唯一的問題是我們沒有辦法來回答它。如果要把輸入信息發(fā)/proc文件中。因?yàn)榫帉憄roc文件系統(tǒng)的主要目的是為了讓內(nèi)核可以把它的狀態(tài)報(bào)告給進(jìn)程,對(duì)于輸入并沒有提供相應(yīng)的特別措施。結(jié)構(gòu)proc_dir_entry中并不包含指向輸入函數(shù)的指針,它只包含指向輸出函數(shù)的指針。如果需要輸入,為了把信息寫到/proc文件中,用戶需要使用標(biāo)準(zhǔn)的文件系統(tǒng)機(jī)制。Linux為文件系統(tǒng)提了一個(gè)標(biāo)準(zhǔn)的機(jī)制。因個(gè)文件系統(tǒng)都必須具有自己的函數(shù)Linux提供了一個(gè)特殊的結(jié)構(gòu)inode_operationsfile_operation的指針。在/proc中,無論何個(gè)新件,戶都以指使用個(gè)inode_operations結(jié)構(gòu)來它。這就是我們所使用的機(jī)制。結(jié)構(gòu)inode_operations包含指向結(jié)構(gòu)file_operations的指針,而結(jié)構(gòu)file_operations又包含指向module_input和module_output函數(shù)的指針。注意 在內(nèi)核讀和的標(biāo)角色互換,讀數(shù)用輸,而寫數(shù)則于輸,記住這點(diǎn)很重要。之所以會(huì)這樣,是因?yàn)樽x和寫實(shí)際上是站在用戶的觀點(diǎn)來說的—如果一個(gè)進(jìn)程從內(nèi)核讀信,內(nèi)核需要做的輸出這些息,而如果進(jìn)程向內(nèi)核信息內(nèi)核當(dāng)會(huì)把當(dāng)作入來收。這里另外一個(gè)有趣的地方是module_permission函數(shù)。只要進(jìn)程試圖對(duì)/pro文件干點(diǎn)什么,這個(gè)函數(shù)就將被調(diào)用,它可以判斷是允許對(duì)文件進(jìn)行,還是這次。目前這種判斷還只是基于操作本身以及當(dāng)前所使用的uid來作出(當(dāng)前所使用的uid可以從current得到,current),但是函數(shù)module_permission還可以基于用戶所選擇的任意條件來作出允許或是的判斷,例如其它還有什么進(jìn)程正在使用這個(gè)文件、日期和時(shí)間、或者我們最近接收到的輸入。在程序中我們之所以使用put_user和get_user,主要是因?yàn)長inux內(nèi)存是分段的(在In結(jié)構(gòu)下;有些其它的處理器可能會(huì)有所不同)。這就意味著一個(gè)指針并不能指向內(nèi)存中的某個(gè)唯一的位置,而只能指向一個(gè)內(nèi)存段。為了能夠使用指針,用戶必須知道它指向的是哪個(gè)內(nèi)存段。內(nèi)核只對(duì)應(yīng)一個(gè)內(nèi)存段,且每個(gè)進(jìn)程都對(duì)應(yīng)一個(gè)內(nèi)存段。進(jìn)程所能的唯一的內(nèi)存段就是它自己的內(nèi)存段,所以在寫將要當(dāng)作進(jìn)程來運(yùn)行的常規(guī)程序時(shí),程序員不需要考慮有關(guān)分段的問題,而當(dāng)用戶編寫內(nèi)核模塊時(shí),通常用戶需要訪問內(nèi)核的內(nèi)存段,該內(nèi)存段是由系統(tǒng)自動(dòng)處理的。然而,如果內(nèi)存緩沖區(qū)中的內(nèi)容需要在當(dāng)前運(yùn)行的進(jìn)程和內(nèi)核之間傳送,內(nèi)核函數(shù)將會(huì)接收到一個(gè)指針,該指針指向進(jìn)程段中的內(nèi)存緩沖區(qū)。宏put_uer和get_user使用戶可以那塊內(nèi)存。第5 設(shè)備文件一般代表物理設(shè)備,而大多數(shù)物理設(shè)備既可用于輸出,也可用于輸入,所以Linux必須提供一些機(jī)制,以便內(nèi)核中的設(shè)備驅(qū)動(dòng)程序可以從進(jìn)程獲得輸出信息,并把它發(fā)送到設(shè)備。要做到這一點(diǎn),可以為輸出打開設(shè)備文件,并且向它寫信息,就像寫普通的文件一樣。在下面的例子中,這些任務(wù)是由device_writeCPU角度來看,它還是由一個(gè)與調(diào)制解調(diào)器相連的串)。用戶將會(huì)做的最自然的事情是用備件信寫調(diào)解器(調(diào)制解調(diào)器命令或者數(shù)據(jù)將會(huì)通過線來傳送),并且利用設(shè)備文件從制解調(diào)器讀信息命令的應(yīng)或者數(shù)據(jù)也是通過線接收的)然而,這會(huì)帶來一個(gè)很明顯的問題:如果用戶需要與串行口本身交換信息的話,用戶該怎么在Unix中,可以使用一個(gè)稱為ioctl(ioctl是輸入輸出控制的英文縮寫)。每個(gè)設(shè)備都有屬于自己的ioctl命令,可以是讀ioctl(從進(jìn)程把到內(nèi)核)、寫ioctl(把信息返回到進(jìn)程)、都有或者都沒有。調(diào)用ioctl函數(shù)必須帶上三個(gè)參數(shù):適當(dāng)?shù)脑O(shè)備文件的文件描述符,ioctl編號(hào)以及另外一個(gè)長整型的參數(shù),用戶可以使用這個(gè)長整型參數(shù)來傳送任何信息。ioctl編號(hào)是由主設(shè)備編號(hào)、ioctl類型、命令以及參數(shù)的類型這幾者編碼而成。這個(gè)ioctl編號(hào)通常是由一個(gè)頭文件中的宏調(diào)用(取決于類型的不同,可以是_IO、_IOR、_IOW或者_(dá)IOWR)來創(chuàng)建的。然后,將要使用ioctl的程序以及內(nèi)核模塊都必須通過#include命令包含這個(gè)頭文件。前者包含這個(gè)頭文件是為了生成適當(dāng)?shù)膇octl,而后者是為了能理解它。在下面的如果用戶希望在自己的內(nèi)核模塊中使用ioctl,最好是接受正式的ioctl的約定,這樣如果偶想知道的信息,可以查詢 ation/ioctl-number.txt下的內(nèi)核源代碼樹。第6 啟動(dòng)參面所給出的許多例子中,我們不得不把一些東西硬塞進(jìn)內(nèi)核模塊中,如/proc文件的文件名或者設(shè)備的主設(shè)備編號(hào),這樣我們就可以使用該設(shè)備的ioctl命令。但這是與Unix和Linux的背道而馳的,Unix和Linux提倡編寫用戶所習(xí)慣的易于使用的程序。在程序或者內(nèi)核模塊開始工作之前,如果希望告訴它一些它所需要的信息,可以使用命argc和agv—相反,我們還有更好的選擇。我inmod命令,它將替我們給這些變量賦值。在下面這個(gè)內(nèi)核模塊中,我們定義了兩個(gè)全局變量:str1和str2。用戶所需做的全部工作就是編譯該內(nèi)核模塊,然后運(yùn)行insmodstr1=xxxstr2=yyy。當(dāng)調(diào)用init_module時(shí),str1將指向字符串“xxx”,而str2將指向字符串“yyy”。在版本2中,對(duì)這些參數(shù)不進(jìn)行類型檢查。如果str1或者str2的第一個(gè)字符是一個(gè)數(shù)字,則內(nèi)核將用該整數(shù)的值填充這個(gè)變量,而不會(huì)用指向字符串的指針去填充它。如果用戶對(duì)此不太確定,那么就必須親自去檢查一下。 第6章啟動(dòng)參 第7 系統(tǒng)調(diào)到現(xiàn)在為止,我們所做的唯一的工作就使用一個(gè)義好的內(nèi)核機(jī)制來/proc件和設(shè)備處理程序。如果用戶僅僅希望做一個(gè)內(nèi)核程序員份內(nèi)的工作,例如編寫設(shè)備驅(qū)動(dòng)程序,那么以前我們所學(xué)的知識(shí)已經(jīng)足夠了。但是如果用戶想做一些不平凡的事,比如在某些方面,在某種程度上改變一下系統(tǒng)的行為,那應(yīng)該怎么辦呢?答案是,幾乎全部要靠自己。這就是內(nèi)核編程之所以的原因。在編寫下面的例題時(shí),我關(guān)掉了系統(tǒng)調(diào)用open將意味著我不能打開任何文件,不能運(yùn)行任何程序,甚至不能關(guān)閉計(jì)算機(jī)。我只能把電源開關(guān)拔掉。幸運(yùn)的是,我沒有刪除掉任何文件。為了保證自己也不丟失任何文件,請(qǐng)讀者在執(zhí)行insmod和rmmod之前先運(yùn)行syn。現(xiàn)在讓我們忘記/proc文件,忘記設(shè)備文件,他們只不過是無關(guān)大雅的細(xì)節(jié)問題。實(shí)現(xiàn)內(nèi)核通信機(jī)制的“真正”進(jìn)程是系統(tǒng)調(diào)用,它是被所有進(jìn)程所使用的進(jìn)程。當(dāng)某進(jìn)程向內(nèi)核請(qǐng)求服務(wù)時(shí)(例如打開文件、生一個(gè)新進(jìn)程或者請(qǐng)的內(nèi)存),它所使用的機(jī)制就是系統(tǒng)調(diào)用。如果用戶希望以一種有趣的方式改變內(nèi)核的行為,也需要依靠系統(tǒng)調(diào)用。順便說一下,strace<command><arguments>。一般來說,進(jìn)程是不能內(nèi)核的。它不能內(nèi)核,它也不能調(diào)用內(nèi)核函數(shù)。CPU的硬件保證了這一點(diǎn)(那就為什稱之“保模式”原因)。系統(tǒng)調(diào)用是這條通用規(guī)則的一個(gè)特例。在進(jìn)行系統(tǒng)調(diào)用時(shí),進(jìn)程以適當(dāng)?shù)闹堤畛涑绦?,然后調(diào)用一條特殊的指令,而這條指令是跳轉(zhuǎn)到以前定義好的內(nèi)核中的某個(gè)位置(當(dāng)然,用戶進(jìn)程可以讀那個(gè)位置,但卻不能對(duì)它進(jìn)行寫的操作)。在In CPU下,以上任務(wù)是通過中斷0x80來完成的。硬件知道一旦跳轉(zhuǎn)到這個(gè)位置,用戶的進(jìn)程就不再是在受限制的用戶模式下運(yùn)行了,而是作為操作系統(tǒng)內(nèi)核來運(yùn)行—于是用戶就被允許干所有他想干的事。進(jìn)程可以跳轉(zhuǎn)到的那個(gè)內(nèi)核中的位置稱為system_call。那個(gè)位置上的過程檢查系統(tǒng)調(diào)用編號(hào)(系統(tǒng)調(diào)用編號(hào)可以告訴內(nèi)核進(jìn)程所請(qǐng)求的是什么服務(wù))。然后,該過程查看系統(tǒng)調(diào)用表(sys_call_table),找出想要調(diào)用的內(nèi)核函數(shù)的地址,然后調(diào)用那個(gè)函數(shù)。在函數(shù)返回之后,該(或者如果進(jìn)程時(shí)間已用完,則返回到另一個(gè)進(jìn)程)。如果讀者想讀這段代碼,可以查看源文件arch/<architecture>/kernel/entr.S,它就在ENTRY(system_call)那一行的后面。這樣看來,如果我們想要改變某個(gè)系統(tǒng)調(diào)用的工作方式,我們需要編寫自己的函數(shù)來實(shí)現(xiàn)它(通常是加入一點(diǎn)自己的代碼,然后再調(diào)用原來的函數(shù)),然后改變sys_call_table表中的指針,使它指向我們的函數(shù)。因?yàn)槲覀兊暮瘮?shù)將來可能會(huì)被刪除掉,而我們不想使系統(tǒng)處于一個(gè)不穩(wěn)定的狀態(tài),所以必須用cleanup_module使sys_call_table表恢復(fù)到它的原始狀態(tài),這是很重要的。本章的源代碼就是這樣一個(gè)內(nèi)核模塊的例子。我們想要“偵聽”某個(gè)特定的用戶。無論printk打印出一個(gè)消息。為了做到這一點(diǎn),our_sys_ope。該函數(shù)查看當(dāng)前進(jìn)程的uid(用戶的ID),如果它就是我們想要偵聽的uid,它就調(diào)用printk,顯示出將要打開的文件的名稱。接下來,不管當(dāng)前進(jìn)程的uid是不是想要偵聽的uid,該函數(shù)都使用同樣的參數(shù)調(diào)用原來的open函數(shù),真正地打開那個(gè)文件。init_module函數(shù)替換sys_call_table表中相應(yīng)的位置,并把原來的指針存放在一個(gè)變量中;而cleanup_module函數(shù)則使用那個(gè)變量把一切都恢復(fù)成原來正常的狀態(tài)。這種方法是具有一定的性的,因?yàn)榭赡苡袃蓚€(gè)內(nèi)核模塊都修改同一個(gè)系統(tǒng)調(diào)用。現(xiàn)在假設(shè)有兩個(gè)內(nèi)核模塊A和B。A模塊打開文件的系統(tǒng)調(diào)用是A_open,而B的系統(tǒng)調(diào)用是B_open。當(dāng)把A插入到內(nèi)核中時(shí),系統(tǒng)調(diào)用被換成了A_open,它在被調(diào)用時(shí)將會(huì)調(diào)用原來的sys_open。接下來,當(dāng)BB_open,它在被調(diào)用時(shí),將會(huì)調(diào)用它自以為是原始系統(tǒng)調(diào)用的那個(gè)系統(tǒng)調(diào)用,即A_open。現(xiàn)在假設(shè)B先被刪除,那么一切都將正?!到y(tǒng)調(diào)用將被恢復(fù)成A_open,而A_open會(huì)調(diào)用原始的系統(tǒng)調(diào)用。然而,如果A先被刪除然后B被刪除,系統(tǒng)將會(huì)。刪除A時(shí)系統(tǒng)調(diào)用被恢復(fù)成原始的sys_open,B_openB時(shí),B將把系統(tǒng)調(diào)用恢復(fù)成它自認(rèn)為是原始的系統(tǒng)調(diào)用,即A_open,而A_open已經(jīng)不再位于內(nèi)存中了。乍一看上去,好象(這樣在刪除B時(shí)它就不會(huì)改變系統(tǒng)調(diào)用),似乎這樣可以避免問題的發(fā)生。但這樣做將會(huì)導(dǎo)致一個(gè)更為嚴(yán)重的問題。當(dāng)AB_open,而不再指向A_open,所以A在從內(nèi)存中刪除前不會(huì)將系統(tǒng)調(diào)用恢復(fù)為sys_open。不幸的是,B_open仍將試圖調(diào)用A_open,而A_open已不內(nèi)存了,樣,至還到刪除B時(shí)系統(tǒng)就將。我認(rèn)為有兩種方法可以解決這個(gè)問題。第一個(gè)方法是把系統(tǒng)調(diào)用恢復(fù)為原始的值:sys_opensys_open不是/proc/ksyms中內(nèi)核系統(tǒng)表的一部分,所以我們不能它。另一個(gè)方法是一旦裝入了模塊,馬上設(shè)立一個(gè)計(jì)數(shù)器,以防止根用戶把它rmmod掉對(duì)于產(chǎn)品模塊來說,這樣做是很好的,但卻不適合于作為教學(xué)的例子—這就是我沒有在這里實(shí)現(xiàn)它的原因。第8 阻塞處如果有人想讓你做一些目前無法做到的事,你會(huì)怎么處理呢?如果打擾你的是某個(gè)人的做一些目前它無法處理的事,內(nèi)核模塊卻可以有另一種處理方法。內(nèi)核可以讓進(jìn)程睡眠,直(這就是多個(gè)進(jìn)程看上去好象同時(shí)在一個(gè)CPU上運(yùn)行的道理)。(名為/proc/sleep)每次只能被一個(gè)進(jìn)程所打module_interruptible_sleep_on。這個(gè)函數(shù)把任務(wù)的狀態(tài)改為ASK_INTERRUPTIBLE(任務(wù)是內(nèi)核數(shù)據(jù)結(jié)構(gòu),它包含著有關(guān)它所處的進(jìn)程和系統(tǒng)調(diào)用的信息,如果存在系統(tǒng)調(diào)用的話),把它加入到aitQ當(dāng)中,這就意味著任務(wù)在被喚醒之前將不會(huì)運(yùn)行。aitQ是等待文件的任務(wù)隊(duì)。然后,函數(shù)調(diào)用下文調(diào)度程,切換到另一個(gè)擁有CPU時(shí)間的進(jìn)程。在進(jìn)程結(jié)束了文件操作后,進(jìn)程將關(guān)閉件,并調(diào)用module_close。該函數(shù)將喚醒隊(duì)列中的所有進(jìn)程(沒有只喚醒一個(gè)進(jìn)程的機(jī)制),然后函數(shù)返回,剛剛關(guān)閉文件的那個(gè)進(jìn)程就可以繼續(xù)運(yùn)行了。如果那個(gè)程的時(shí)間片用完,則調(diào)度度將會(huì)及時(shí)判斷出這一,并將 CP控制權(quán)交給另一個(gè)進(jìn)程最后,等待隊(duì)列的某個(gè)進(jìn)將會(huì)被調(diào)度程序授予CPU的控制權(quán)。該進(jìn)程將從緊接著調(diào)用module_interruptible_sleep_on后面的那個(gè)點(diǎn)開始執(zhí)行。然后它會(huì)設(shè)置一個(gè)全局變量,告訴其它所有進(jìn)程文件依然打開著,然后該進(jìn)程將繼續(xù)執(zhí)行。當(dāng)其它進(jìn)程獲得CPU時(shí)間片時(shí),它們將看到那個(gè)全局變量,于是繼續(xù)睡眠。更為有趣的是,并不是有module_close才能喚醒那些等待文件的進(jìn)程。一個(gè)信號(hào),例如Ctrl+C(SIGINT)也可以喚醒進(jìn)程。在那種情況下,我們希望立刻用EINTR返回。這是很重要的,用戶才能在進(jìn)程接收到文件之前殺死那個(gè)進(jìn)程。還需要記住一點(diǎn),有時(shí)候進(jìn)程并不想睡眠;它們希望或者立刻拿到它們想要的東西,或者直接告訴它們這是不能的。這樣的進(jìn)在打開文時(shí)使用標(biāo)志O_NONBLOCK。內(nèi)核在進(jìn)行該作時(shí)將會(huì)回錯(cuò)代碼EAGAIN來作響應(yīng),否則該操作就將阻塞,就像下面例子中的打開文件操作一樣。本章的源 中有一個(gè)程序 cat_noblock,可以用于帶O_NONBLOCK標(biāo)志打開一個(gè)文件。第9 在第1章,我曾經(jīng)提到過X編程和內(nèi)核模塊編程不能混為一談。這句話在開發(fā)內(nèi)核模塊時(shí)是正確的。但是在實(shí)際應(yīng)用中,用戶可能希望向運(yùn)行tty命令的那個(gè)模塊發(fā)送消息。在釋放內(nèi)核模塊以后,這對(duì)于識(shí)別錯(cuò)誤是非常重要的。因?yàn)樵搩?nèi)核模塊將會(huì)被所有使用tty所使用。通過使用current可以做到這一點(diǎn),current是一個(gè)指向當(dāng)前正在運(yùn)行的任務(wù)的指針,通過使用它可以獲得當(dāng)前任務(wù)的tt結(jié)構(gòu)。然后查看tty結(jié)構(gòu),可以找到指向?qū)懽址暮瘮?shù)的指針,我們就是用這個(gè)指針向tty寫字符串的。 第9章替換 第10 任務(wù)調(diào)常常有一些“內(nèi)務(wù)處理”任務(wù)需要我們定時(shí)或者經(jīng)常去做。如果任務(wù)是由進(jìn)程去完成的,我們可以把它放在cronta文件中。如果任務(wù)要由內(nèi)核模塊來完成,那么兩種選擇。第案是把一個(gè)進(jìn)放在crontab文件中,該進(jìn)程在需要的時(shí)候通過系統(tǒng)調(diào)用喚醒模塊。crontab中運(yùn)行一個(gè)新進(jìn)程,把一個(gè)新的可執(zhí)行程序讀入內(nèi)存,而這一切都只是為了喚醒一個(gè)早已經(jīng)位于內(nèi)存中的內(nèi)核模塊這樣做效率是非常低的。除了以上這兩種方法以外,我們還可以創(chuàng)建一個(gè)函數(shù),在每次時(shí)鐘中斷時(shí)調(diào)用一次那個(gè)函數(shù)。為了做到這一點(diǎn),需要?jiǎng)?chuàng)建一個(gè)任務(wù),存放在結(jié)構(gòu)tq_struct中,而該結(jié)構(gòu)將存放指向函數(shù)的指針。接著,我們使用queue_task把那個(gè)任務(wù)放置到一個(gè)稱為tq_timer的任務(wù)列表中,該任務(wù)列表中的任務(wù)都將在下次時(shí)鐘中斷時(shí)執(zhí)行。由于我們希望該函數(shù)能繼續(xù)執(zhí)行下去,無論該函數(shù)何時(shí)被調(diào)用,要把它放回到tq_timer中,這樣在下一次時(shí)中斷時(shí)它還以執(zhí)行。在這里還需要記住一點(diǎn)。當(dāng)使用rmmod命令刪除一個(gè)模塊時(shí),首先需要檢查它的計(jì)數(shù)器值。如果計(jì)數(shù)器的值為0,則調(diào)用module_cleanup。然后,該模塊以及它的所有函數(shù)就被從內(nèi)存中刪除掉了。沒有人會(huì)記得檢查一下看看時(shí)鐘的任務(wù)列表中是否碰巧包含了一個(gè)指向(這是從計(jì)算),內(nèi)核發(fā)生了一次時(shí)鐘中斷,并試圖調(diào)用任務(wù)列表中的函數(shù)。不幸的是,該函數(shù)早已經(jīng)不在那里了。在大多數(shù)情況下,該函數(shù)原來所在的內(nèi)存頁還沒有被使用,這時(shí)用戶得到的將是一段難看的錯(cuò)誤信息。但如果那個(gè)內(nèi)存位置已經(jīng)存放了一些新的其它的代碼,事情就變得非常糟糕了。不幸的是,我們還沒有一種簡便的方法可以從任務(wù)列表中取消一個(gè)任務(wù)的。由于cleanup_module不能返回錯(cuò)誤代碼(它是一個(gè)void函數(shù)),解決的方法是根本不讓它返回,相反,它調(diào)用sleep_on或者module_sleep_on,把rmmod進(jìn)程置為睡眠狀態(tài)。而在此之前,它會(huì)設(shè)置一個(gè)全局變量,通知那些將要在時(shí)鐘中斷時(shí)調(diào)用的函數(shù)停止連接。然后,在下一次時(shí)鐘中斷發(fā)生時(shí),rmmod進(jìn)程將被喚醒,我們的函數(shù)已經(jīng)不在隊(duì)列中了,這時(shí)就可以安全地刪除模塊。第11 除了第10章以外,到目前為止我們在內(nèi)核中所做的所有工作都是為了響應(yīng)進(jìn)程的請(qǐng)求,或者是處理一個(gè)特殊的文件,發(fā)送ioctl,或者是發(fā)出一個(gè)系統(tǒng)調(diào)用。但是內(nèi)核的任務(wù)并不僅僅是為了響應(yīng)進(jìn)程請(qǐng)求。另一個(gè)任務(wù)也是同樣重要的,那就是內(nèi)核還需要和與機(jī)器相連接的硬件進(jìn)行通信。在CPUCPU向硬件發(fā)出命令,另一種是硬件需要告訴CPU一些事情。第二種方式稱之為中斷,相比較而言,中斷要難實(shí)現(xiàn)CPU方便的時(shí)候去處理。一般RAM,如果在可以讀它們的信息的時(shí)候用戶沒有去讀,則這些信息將丟失。在Linux下,硬件中斷稱為IRQ(即InterruptRequest的簡稱,中斷請(qǐng)求)。IRQ分為兩種類型:短的和長的,短IRQ塞,并且不再處理其它的中斷。而長IRQ是指需要的時(shí)間相對(duì)長一些,在這段時(shí)間內(nèi)也可能會(huì)發(fā)生別的中斷(但是同一個(gè)設(shè)備上不會(huì)再產(chǎn)生中斷)。如果有可能的話,中斷處理程序還是處理長IRQ要好一些。當(dāng)CPU(除非它正在處理一個(gè)更為重要的中斷,在那種情況下,只有處理完那個(gè)更重要的中斷以后,CPU才會(huì)去處理這個(gè)中斷),在棧中保存某些特定的參數(shù),并調(diào)用中斷處理程序。這就意味著在中斷處理程序內(nèi)部有些特定的事情是不允許的,因?yàn)橄到y(tǒng)處于一個(gè)未知的狀態(tài)下。為了解決這個(gè)問題,中斷處理程序應(yīng)該做那些需要立刻去做的事情,通常是從硬件讀一些信息或者向硬件發(fā)送一些信息,在稍后的某時(shí)刻再調(diào)度去做新信息的處理工作()并返回。內(nèi)核必須保證盡快調(diào)用底半處理—在進(jìn)行底半處理工作時(shí),內(nèi)核模塊中允許做的所有工作都可以做。要實(shí)現(xiàn)這一點(diǎn),可以調(diào)用request_irq,這樣一來,在接收到相關(guān)IRQ時(shí),就可以調(diào)用用戶的中斷處理程序(在In平臺(tái)上共有16種IRQ)。request_irq接收IRQ編號(hào)、函數(shù)名稱、標(biāo)志、/pro/iterupts的名稱以及傳送給中斷處理函數(shù)的一個(gè)參數(shù)。這里提到的標(biāo)志包括_IR和_ITERRT,前者表明用戶愿意與其它中斷處理程序IR(通常是為同個(gè)IR上有多個(gè)硬件設(shè)備);而后者表明這是個(gè)高速中斷。只有當(dāng)這個(gè)IR上還沒有處理程序,或者兩者都愿意共享這個(gè)IR時(shí),rquet_ir接下來,我們從中斷處理程序內(nèi)部與硬件進(jìn)行通信,并使用 tq_immediate和mark_bh(BH_IMMEDIATE)調(diào)用queue_task_irq,以調(diào)度底半處理。在版本2中我們不能使用標(biāo)準(zhǔn)的queue_task,這是因?yàn)橹袛嘤锌赡苷冒l(fā)生在其它的queue_task中間。我們之所以需要使用mark_bh,是因?yàn)橐郧鞍姹镜腖inux只有一個(gè)由32個(gè)底半處理所組成的隊(duì)列,而現(xiàn)在它們中的一個(gè)(BH_IMMEDIATE)已經(jīng)被用作底半處理的列表,這是為那些沒有得到分配給它們的底半處理的驅(qū)動(dòng)程序所準(zhǔn)備的。 在寫本章的示例代碼時(shí)我遇到了一個(gè)問題。一方面,為了讓所給出的例子能有用,它必須可以在所有人的計(jì)算機(jī)上運(yùn)行,且能得到有意義的結(jié)果。但是另一方面,內(nèi)核中早已經(jīng)為所有的通用設(shè)備準(zhǔn)備了設(shè)備驅(qū)動(dòng)程序,而那些設(shè)備驅(qū)動(dòng)程序與我將要編寫的驅(qū)動(dòng)程序是不能共存的。最后我找到了解決的辦法,就是為鍵盤中斷寫驅(qū)動(dòng)程序,并且首先關(guān)閉常規(guī)的鍵盤(特別是指drivers/char/keyboard.c)中的靜態(tài)符號(hào),insmod命令插入這個(gè)代碼以前,請(qǐng)先在另一個(gè)終端上執(zhí)行l(wèi)eep120;reboot。該代碼把它自己綁定在IRQ1上,在In體系結(jié)構(gòu)下,IRQ1是控制鍵盤的IRQ。這樣,當(dāng)它接收到鍵盤中斷時(shí),它讀鍵盤的狀態(tài)(在程序中使用inb(0x64)來實(shí)現(xiàn)),并查看代碼,該代碼是由鍵盤所返回的值。然后在內(nèi)核認(rèn)為適合的時(shí)候,它立刻運(yùn)行g(shù)ot_char,該函數(shù)將給出所使用的鍵的代碼(即查看到的代碼的前七位),并給出該鍵是被按下(如果第八位為0)還是被松開 第11章中斷處理程 第12 要提高硬件的性能,最簡單的(同時(shí)也是最便宜的)方法是把多個(gè)CPU放到一個(gè)主板上。實(shí)CPU執(zhí)行不同的任務(wù)即非對(duì)稱多處理)些CPU并行運(yùn)行,執(zhí)行同樣的任務(wù)(即對(duì)稱多處理,簡寫為SMP)。進(jìn)行非對(duì)稱多處理非常需要對(duì)計(jì)算機(jī)將要執(zhí)行的任務(wù)有特別的了解,而在像Linux這樣的操作系統(tǒng)中,這是不可能的。另一方面,對(duì)稱多處理相對(duì)要容易實(shí)現(xiàn)一些。這里所說的“相對(duì)容易”就是相對(duì)的意思—并不是指它真的容易實(shí)現(xiàn)。在對(duì)稱多處理環(huán)境中,CPU共享同一個(gè)內(nèi)存,這樣做的是一個(gè)CPU中運(yùn)行的代碼可能會(huì)影響另一個(gè)CPU所使用的內(nèi)存。用戶不再可以肯定自己面一行中設(shè)置了值的那個(gè)變量仍然保持著那個(gè)值—另一個(gè)CPU可能在用戶不注意的時(shí)候?qū)δ莻€(gè)變量進(jìn)行了處理。很顯然,要編出這樣的程序是不可能的。在進(jìn)程編程時(shí),一般來說上面這個(gè)問題就不是什么問題了,因?yàn)檫M(jìn)程在某個(gè)時(shí)刻一般是只在一個(gè)CPUCPU上的不同進(jìn)程所調(diào)用。在版本2.0.x中,這個(gè)也不是什么問題,因?yàn)檎麄€(gè)內(nèi)核就是一個(gè)大的自旋鎖(spinlock)。這意CPU必須等待,直到第一個(gè)CPU處理完,這使得LinuxSMP比較安全,但是效率卻相當(dāng)?shù)?。在已?jīng)就SMP的問題向其它高手求助了,希望在本書的下一個(gè)版本中將會(huì)包含的信息第13 常見錯(cuò)在讀者躊躇滿志地準(zhǔn)備動(dòng)手編寫內(nèi)核模塊以前,我還要提醒大家注意一些事情。如果是因?yàn)槲覜]有警告過你而發(fā)生了不愉快的情況,請(qǐng)遇到的問題告訴我。我將買此書所付的錢全部還給你。使用標(biāo)準(zhǔn)庫不能這樣做,在內(nèi)核模塊中用戶只能使用內(nèi)核函數(shù),也就是可以在關(guān)閉中斷 用戶可能需要在短時(shí)間內(nèi)暫時(shí)關(guān)閉中斷,那沒什么問題,但是事后如果忘了打開它們,系統(tǒng)將會(huì)癱瘓,用戶將不得不把電源拔掉。無視的存 可能不需要提醒讀者,但是最后我還是要說,只是為了以防萬一 實(shí)際上我對(duì)整個(gè)內(nèi)核了解得并不是很透徹,沒有透徹到能夠列出所有

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論