?? (ldd) ch12-加載塊設備驅動程序(轉載).htm
字號:
color=#ffffff size=3>
<P>當前緩沖區頭是CURRENT->bh,而數據塊是CURRENT->bh->b_data。后一個指針為了象sbu<BR>ll一類忽略集簇的驅動程序緩沖在CURRENT->buffer中。<BR> <BR>請求集簇在drivers/block/ll_rw_block.c的函數make_request中實現。不過,如上所說<BR>,集簇只對幾個驅動程序有效(軟驅,IDE,和SCSI),以其主設備號為準。我曾通過以<BR>major=34裝載sbull看到過集簇是如何工作的,因為34是IDE3_MAJOR,而我的系統中沒有<BR>第三個IDE控制器。<BR> <BR>下面列表總結了當掃描一個集簇請求時應做的事項。bh是被處理的緩沖區頭——列表的<BR>第一項。對列表中的每個緩沖區頭,驅動程序要完成下面一系列操作:<BR> <BR>l 傳送位于地址bh->b_data,大小為bh->b_size字節數據塊。數據傳送的方向通<BR>常由CURRENT->cmd指出。<BR> <BR>l 從列表中找出下一個緩沖區頭:bh->b_request。接著通過將b_request置為0<BR>,把剛傳送過的緩沖區從列表中摘下。b_reqnext指向你剛找出的新緩沖區。<BR> <BR>l 通過調用mark_buffer_uptodate(bh,1),unlock_buffer(bh),告訴核心你已<BR>完成對上個緩沖區的操作。這些調用保證緩沖高速緩存保持正確,不致有錯誤指向的指<BR>針。mark_buffer_uptodate中參數“1”表示傳送成功,若傳送失敗,則換為0。<BR> <BR>l 循環回到開始,傳送下一個相鄰塊。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>當你做完了集簇請求,CURRENT->bh必須被更新以指向“已經被處理但未被解鎖”的第一<BR>個緩沖區。如果列表中所有的緩沖區都已被處理和解鎖,CURRENT->bh可被置為NULL。<BR> <BR>此時,驅動程序可以調用end_request。如果CURRENT->bh是有效的,那么這個函數在轉<BR>到下一個緩沖之前對其進行解鎖——這是非集簇操作所發生的情況,此時由end_request<BR>照管所有的事情。如果指針為空,這個函數直接轉到下一個請求。<BR> <BR>全功能的集簇實現出現在driver/block/floppy.c,而要求的所有操作出現在blk.h的end<BR>_request中。floppy.c和blk.h都不容易理解,不過建議先從后者開始。<BR> <BR>安裝(Mounting)是如何工作的<BR> <BR>塊設備與字符設備及一般文件的不同在于它們可以被安裝到計算機的文件系統上。這與<BR>一般的訪問方式不同。一般的訪問方式通過結構file進行,這個結構與特定的進程相關<BR>聯,并且只在open到close之間存在。當一個文件系統被安裝后,沒有進程擁有一個filp<BR>。<BR> <BR>當核心把一個設備安裝到文件系統上,它調用一般的open方法來訪問驅動程序。然而,<BR>這種情況下open的參數filp是個虛的變量,幾乎只是為了占個地方,它唯一有意義的域<BR>是f_mode。其它域含任意值并不使用。f_mode的值是告訴驅動程序設備是以只讀(f_mod<BR>e==FMODE_READ)還是讀寫(f_mode==(FMODE_READ|FMOD_WRITE))方式被安裝。使用<BR>一個虛變量而不是file結構的原因是因為實際的結構file在進程結束時將被釋放,而被<BR></P></FONT><FONT
color=#ffffff size=3>
<P>一個虛變量而不是file結構的原因是因為實際的結構file在進程結束時將被釋放,而被<BR>安裝的文件系統在mount命令完成后仍然存在。<BR> <BR>在安裝時,驅動程序唯一調用的是open方法。當磁盤被安裝后,核心調用設備中的read<BR>和write 方法(被映射到request_fn)來管理文件系統中的文件。驅動程序并不知道req<BR>uest_fn服務的是一個進程(象fsck)還是核心中的文件系統層。<BR> <BR>至于umount,它只是刷新緩沖高速緩存并調用驅動程序的release(close)方法。由于<BR>沒有有意義的filp可以傳遞給fop->realse,核心使用NULL。<BR> <BR>因此,當你實現release時,你應將驅動程序設為能處理為NULL的filp指針。不然,如果<BR>你用了filp,你可能運行mkfs和fsck,它們都使用filp來訪問設備,你也可能mount這個<BR>設備,但umount將無法運行,原因就是NULL指針。<BR> <BR>由于一個塊設備驅動程序的release實現不能用filp->private_data來訪問設備信息,它<BR>采用inode->i_rdev來區分設備。這里是release的sbull實現:<BR> <BR>(代碼249)<BR> <BR>其它的驅動程序函數并不關心filp問題,因為它們與安裝的文件系統無關。例如,一個<BR>顯示地open這個設備的進程只發出ioctl。<BR> <BR>ioctl方法<BR></P></FONT><FONT
color=#ffffff size=3>
<P>ioctl方法<BR> <BR>如字符設備一樣,塊設備也可以通過ioctl系統調用進行操作。兩者之間相對不一樣的地<BR>方在于塊設備驅動程序有大量驅動程序都要支持的ioctl命令。<BR> <BR>塊設備驅動程序經常要處理的命令如下所示,它們在<linux/fs.h>中被聲明。<BR> <BR>BLKGETSIZE<BR> <BR>獲取當前設備的大小,以扇區數表示。由系統調用傳遞的 數值arg是一個指向long數值<BR>的指針,用來將大小拷貝到一個用戶空間的變量中。這個ioctl命令可以被mkfs用來獲知<BR>產生的文件系統的大小。<BR> <BR>BLKFLSBUF<BR> <BR>字面上的意思是“刷新緩沖區”。這個命令的實現對每個設備都是一樣的,我們將在后<BR>面整個ioctl方法的示例代碼中給出來。<BR> <BR>BLKRAGET<BR> <BR>用來為設備取得當前提前讀的值。當前數值應該用在參數arg中傳遞給ioctl的指針寫進<BR>一個long類型的用戶空間變量。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>BLKRASET<BR> <BR>設置提前讀的值。用戶進程在arg中傳遞這個新值。<BR> <BR>BLKRRPART<BR> <BR>重讀分區表。這個命令只對可分區設備有意義,將在后面“可分區設備”中介紹。<BR> <BR>BLKROSET<BR> <BR>BLKROGET<BR> <BR>這些命令用來改變和檢查設備的只讀標志。因為代碼是設備無關的,它們由宏RO_IOCTLS<BR>(kdev_tdev,unsigned long where)來實現。這個宏在blk.h中定義。<BR> <BR>HDIO_GETGEO<BR> <BR>在<linux/hdreg.h>中定義,用來獲得磁盤的幾何參數。這個參數應被寫入用戶空間的結<BR>構hd_geometry中,它也在hdreg.h中定義。sbull顯示了這個命令的一般實現。<BR> <BR>HDIO_GETGEO是<linux/hdreg.h>中定義的一系列HDIO命令中最常用的一個。感興趣的讀<BR>者可以查看ide.c和hd.c以獲得這些命令的更多信息。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>者可以查看ide.c和hd.c以獲得這些命令的更多信息。<BR> <BR>這里列出的這些命令的一個主要缺點是它們是以“老”方法定義的(是第五章“增強的<BR>字符設備驅動程序操作”中“選擇ioctl命令”一節),因此無法使用位域的宏來減化代<BR>碼——每個命令要實現它自己的verify_area。不過,如果一個驅動程序需要定義它自己<BR>的命令來利用設備的一些特殊特點,你可以自由地使用“新”方法來定義命令。<BR> <BR>sbull設備只支持上面的通用命令,因為實現設備特定的命令與實現字符設備驅動程序的<BR>命令沒有什么不同。sbull的ioctl實現如下所示,它將有助于你理解上面列出的命令。<BR> <BR>(代碼250)<BR> <BR>(代碼251)<BR> <BR>函數開始的PDEBUG語句被留出,這樣當你編譯這個模塊時,你可以打開調試(debugging<BR>)來看看設備上調用了哪個ioctl命令。<BR> <BR>例如,對于顯示的ioctl命令,你可以在sbull上使用fdisk。下面是在我自己系統上的一<BR>個示例執行過程:<BR> <BR>(代碼252 1#)<BR> <BR>在會話過程中下面的消息出現在我的系統日志中:<BR></P></FONT><FONT
color=#ffffff size=3>
<P>在會話過程中下面的消息出現在我的系統日志中:<BR> <BR>(代碼252 2#)<BR> <BR>第一個ioctl是HDIO_GETGEO,它在fdisk啟動時被調用;第二個是BLKRRPART。對后一個<BR>命令的sbull實現僅僅是調用一下revalidate函數,它則在打印輸出中打印最后的消息(<BR>見本章后面的“revalidate”)。<BR> <BR>可拆卸的設備<BR> <BR>在我們討論字符設備驅動程序時,我們忽略了fops結構中的最后兩個文件操作,因為它<BR>們只是為可拆卸塊設備而設的。現在是看看它們的時候了。sbull并不真是可拆卸的,但<BR>它假裝是,因此它實現了這些方法。<BR> <BR>我所說的操作是check_media-_change和revalidate。前者用來發現設備自上次訪問以來<BR>是否改變過,后者則在磁盤變動之后重新初始化驅動程序的狀態。<BR> <BR>至于sbull,與設備相聯的數據區在使用計數下降為零后半分鐘要釋放。待這個設備處于<BR>未安裝狀態(或關閉狀態)足夠長的時間以模擬一次磁盤的改變,下一次對設備的訪問<BR>分配一個新的內存區域。<BR> <BR>這一類的“時間到期”通過一個核心計數器來實現。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>check_media_change<BR>這個檢查函數接收到kev_t做為一個確定設備的參數。如果介質被改變了返回值為1,否<BR>則為0。如果一塊設備驅動程序不支持可拆卸設備,可以通過置fops->check_media_chan<BR>ge為NULL來避免這個聲明函數。<BR> <BR>有趣的是要注意,當一個設備是可拆卸的,但卻無法判斷它是否改變了,這時,返回1是<BR>個安全選擇。事實上,IDE驅動程序在處理可 鸚洞排淌 就是這么做的。<BR> <BR>sbull的實現是這樣的,當由于計數器超時,設備已經從內存中刪除時就返回1,如果數<BR>據仍然有效則返回0。如果設置了調試,它同時向系統日志打印一條消息,這樣用戶就可<BR>以檢查核心什么時候調用了這個方法。<BR> <BR>(代碼253 1#)<BR> <BR>revalidate<BR> <BR>這個有效化函數是在檢測到一個磁盤的改變時被調用。它也被在核心的2.1版中實現的各<BR>種stat系統調用。返回值目前不做使用;為安全起見,返回0表示成功,出錯時返回一個<BR>負的錯誤代碼。<BR> <BR>revalidate執行的動作是設備特定的,但revalidate通常更新一些內部狀態信息以反映<BR>新的設備。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>新的設備。<BR> <BR>在sbull中,revalidate方法在沒有一個有效區域的情況下試圖分配一塊新的數據區域。<BR> <BR> <BR>(代碼253 2#)<BR> <BR>(代碼254 1#)<BR> <BR>特別注意<BR> <BR>當可拆卸設備已經打開時,驅動程序也應該檢查是否有磁盤的改變;在mount時核心自動<BR>調用它的check-_disk_change函數,但在open時,并不這樣做。<BR> <BR>不過,有些程序直接訪問磁盤數據而不安裝這個設備,fsck,mcopy和fdisk都是這類程<BR>序的例子。如果驅動程序在內存中保存可拆卸設備的狀態信息,它應在設備第一次打開<BR>時調用check_disk_change函數。這個核心函數還要依賴驅動程序方法(check_media_ch<BR>ange和revalidate),因此在open里不須實現任何特別的東西。<BR> <BR>這里是open的sbull實現,它關注了發生磁盤改變的情況:<BR> <BR>(代碼254 2#)<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>在驅動程序中不需對磁盤的改變做任何別的。如果一個磁盤被改變了,而它的打開計數<BR>大于零,那么數據會被破壞。防止這種情況發生的唯一方法是讓利用在物理上支持的設<BR>備使用計數控制門鎖。open和close可以在合適的時候關閉或打開鎖。<BR> <BR>可分區設備<BR> <BR>如果你想用fdisk生成分區,你會發現它們有一些問題。fdisk程序稱這些分區為/dev/sb<BR>ull01,/dev/sbull02以 此類推,但文件系統上并不存在這些名字。的確,基本的sbull<BR>設備是一個字節陣列,不存在提供訪問數據區域的子區域的入口點,因此想對sbull進行<BR>分區是行不通的。<BR> <BR>為了能對設備分區,我們必須給每個物理設備分配幾個次設備號。一個數字用來訪問整<BR>個設備(如/dev/hda),其它的用來訪問不同的分區(如/dev/hda1)。由于fdisk產生<BR>分區名的辦法是在全盤設備名后加一個數字后綴,我們將在后面的塊設備驅動程序中遵<BR>循同樣的命名規則。<BR> <BR>在本節中我將要介紹的設備叫spull,因此它是一個“簡單的可分區工具(Simple<BR>Partitionable Utility)”。這個設備位于spull目錄,完全與sbull無關,盡管它們共<BR>享很多代碼。<BR> <BR>在字符設備驅動程序scull中,不同的次設備號可以實現不同的行為,因此一個驅動程序<BR>可以顯示幾種不同的實現。而按照次設備號區分塊設備是不可行的,這就是為什么sbull<BR></P></FONT><FONT
color=#ffffff size=3>
<P>可以顯示幾種不同的實現。而按照次設備號區分塊設備是不可行的,這就是為什么sbull<BR>和spull被分離開。這種無能為力是塊設備驅動程序的一個基本特征,因為幾個數據結構<BR>和宏只是作為主設備號的函數定義的。<BR> <BR>關于移植,需要注意的是可分區模塊不能被加載到核心的1.2版,因為符號resetup_one_<BR>dev(在本節后面介紹)沒有被引出到模塊。在對SCSI盤的支持模塊化之前,沒有人會考<BR>慮可分區的模塊。<BR> <BR>我要介紹的設備結點被稱做pd,表示“可分區磁盤(partitionable disk)”。四個完<BR>整的設備(又稱“單元”)被稱做/dev/pda直到/dev/pdd;每個設備最多支持15個分區<BR>。次設備號有下面的含義:低四位表示分區號(0為完整的設備),高四位表示單元號。<BR>這個規則在源文件中由下面的宏表達:<BR> <BR>(代碼255)<BR> <BR>普通硬盤<BR> <BR>每個可分區設備需要知道它是如何分區的。這個信息可以從分區表中得到。初始化進程<BR>的一部分包括解碼分區表,并更新內部數據結構以反映分區信息。<BR> <BR>這個解碼并不容易。不過幸運的是,核心提供可被所有塊設備驅動程序使用的“普通硬<BR>盤”支持,它顯著地減少了處理分區驅動程序的代碼。這個普通支持的另一個好處是驅<BR>動程序的作者不必理解分區是如何完成的,而不需要修改驅動程序的代碼就可以在核心<BR></P></FONT><FONT
color=#ffffff size=3>
<P>動程序的作者不必理解分區是如何完成的,而不需要修改驅動程序的代碼就可以在核心<BR>中支持新的分區方式。<BR> <BR>想要支持分區的塊設備驅動程序要包含<linux/genhd.h>,并聲明結構gendisk。所有這<BR>樣的結構被組織在一個鏈表中,它的頭是全局指針gendisk_head。<BR> <BR>在我們進行下一步之前,讓我們先看看結構gendisk的域。你為了利用普通設備支持就需<BR>要理解它們。<BR> <BR>int major<BR> <BR>確定這個結構所指的設備驅動程序的主設備號。<BR> <BR>const char*major_name<BR> <BR>屬于這個主設備號的設備的基本名。每個設備名是通過在這個名字后為每個單元加一個<BR>字母并為每個分區加一個數字得到。例如,“hd”是用來構成/dev/hda1和/dev/hda3的<BR>基本名。基本名最多5個字符長,因為add_partition在一個8字節的緩沖區中構造全名,<BR>它要附加上一個確定單元的字母,分區號和一個終止符‘\0’。spull所用的名字是pd(<BR>“可分區磁盤(partitionable disk)”)。<BR> <BR>int minor_shift<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>從設備的次設備號中獲取驅動器號要進行移位的次數。在spull中這個數是4。這個域中<BR>的值應與宏DEVICE_NR(device)中的定義一致(見本章前面的“頭文件blk.h”)。spull<BR>中的宏擴展為device>>4。<BR> <BR>int max_p<BR> <BR>分區的最大數目。在我們的例子中,max_p1是16,或更一般地,是<<minor_shift。<BR> <BR>int max-_nr<BR> <BR>單元的最大數目。在spull中,這個數字是4。單元最大數目在移位minor_shift次后的結<BR>果應匹配次設備號的可能的范圍,目前是0-255。IDE驅動程序可以同時支持很多驅動器<BR>和每一個驅動器很多分區,因為它注冊了幾個主設備號,從而繞過了次設備號范圍小的<BR>問題。<BR> <BR>void(*init)(struct gendisk*)<BR> <BR>驅動程序的初始化函數,它在初始化設備后和分區檢查執行前被調用。我將在下面介紹<BR>這個函數更多的細節。<BR> <BR>struct hd_struct *part<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>設備的解碼后的分區表。驅動程序用這一項確定通過每個次設備號哪些范圍的磁盤扇區<BR>是可以訪問的。大多數驅動程序實現max_nr<<minor_shift個結構的靜態數值,并負責數<BR>組的分配和釋放。在核心解碼分區表之前驅動程序應將數組初始化為零。<BR> <BR>int *sizes<BR> <BR>這個域指向一個整數數組。這個數組保持著與blk_size同樣的信息。驅動程序負責分配<BR>和釋放該數據區域。注意設備的分區檢查把這個指針拷貝到blk_size,因此處理可分區<BR>設備的驅動程序不必分配這后一個數組。<BR> <BR>int nr_real<BR> <BR>存在的真實設備(單元)的個數。這個數字必須小于等于max_nr。<BR> <BR>void *real_devices<BR> <BR>這個指針被那些需要保存一些額外私有信息的驅動程序內部使用(這與filp->private_d<BR>ata類似)。<BR> <BR>void struct gendisk *next<BR> <BR>在普通硬盤列表中的一根鏈。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>在普通硬盤列表中的一根鏈。<BR> <BR>分區檢查的設計最適合那些直接鏈入核心映象的驅動程序,因此我將從介紹核心代碼的<BR>基本結構開始。以后我將介紹spull模塊處理它的分區的方法。<BR> <BR>核心中的分區檢測<BR> <BR>在引導時,init/main.c調用了各種各樣的初始化函數。其中一個是start_kernel,它通<BR>過調用device_setup來初始化所有的驅動程序。這個函數又調用blk_dev_init,接著檢<BR>查所有注冊的普通硬盤的分區信息。任何一個塊設備驅動程序,如果它找到至少一個它<BR>的設備,就將這個驅動程序的genhd結構注冊到核心列表中,這樣它的分區便可以被正確<BR>地檢測出來。<BR> <BR>因此,一個可分區的驅動程序應該聲明它自己的結構genhd。這個結構看起來如下:<BR> <BR>(代碼258)<BR> <BR>于是,在這個驅動程序的初始化函數中,這個結構被排隊在可分區設備的主列表中。<BR> <BR>被鏈入核心的驅動程序的初始化函數與init_module等價,即使它被調用的方式不同。這<BR>個函數一定包含如下兩行,它們用來將結構排隊:<BR> <BR>my_gendisk.next=gendisk_head;<BR></P></FONT><FONT
color=#ffffff size=3>
<P>my_gendisk.next=gendisk_head;<BR> <BR>gendisk_head=my_gendisk;<BR> <BR>通過將結構插入鏈表,這簡單的兩行便是驅動程序入口點為所有的分區正確地識別和配<BR>置所需要的所有內容。<BR> <BR>額外的設置通過my_geninit完成。在上面的例子中,這個函數填充“單元數”域來反映<BR>計算機系統的實際硬件設置。在my_geninit結束后,gendisk.c為所有的盤(單元)執行<BR>實際的分區檢測。你可以看到系統啟動時被檢測的分區,因為gendisk.c在系統控制臺上<BR>打印分區檢查Partition check:,后面跟隨它在可得的普通硬盤上找到的所有分區。<BR> <BR>你可以修改前面的代碼,推遲my_sizes和my_partitions的分配直到my_geninit函數。這<BR>可以節省少量的核心內存,因為這些數組可以小到nr_real<<minor_shift,而竟態數組<BR>則必須為max_nr<<minor_shift字節長。不過,典型的數值是每個物理單元節省幾百個字<BR>節。<BR> <BR>模塊中的分區檢測<BR> <BR>一個模塊化的驅動程序和鏈接到核心的驅動程序的區別在于它無法受益于集體中的初始<BR>化。相反,它需要處理它自己的設置。由于沒有為模塊的兩步初始化,所以spull的gend<BR>isk結構在它的init函數指針中有一個NULL指針:<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>(代碼259 1#)<BR> <BR>同時也不必在普通硬盤的全局鏈表里注冊gendisk結構。<BR> <BR>通過引出函數resetup_one_dev,文件gendisk.c被準備用來處理象模塊需要一類“晚的<BR>”初始化。resetup_one_dev為單個物理設備掃描分區。其原型是:<BR> <BR>boid resetup_one_dev(struct gendisk *dev,int drive);<BR> <BR>從這個函數名字你可以看出來它是要改變一個設備的設置信息。這個函數被設計為由ioc<BR>tl里BLKRRPART實現調用,但他也可以被用來完成一個模塊的初始設置。<BR> <BR>當一個模塊被初始化后,它應該為每個它將要訪問的物理設備調用resetup_one_dev,從<BR>而將分區信息貯存my_gendisk->part中。分區信息會被設備的request_fn函數使用。<BR> <BR>在spull中,init_module函數除了通常的指令外還包含了下面的代碼。它分配分區檢測<BR>所需的數組并初始化數組中完整磁盤的項目。<BR> <BR>(代碼259 2#)<BR> <BR>(代碼260 1#)<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>有趣的是注意到resetup_one_dev通過重復調用下面函數打印分區信息:<BR> <BR>printk(“%s:”,disk_name(hd,minor,buf));<BR> <BR>這就是為什么spull要打印一個引導串。它意味著要為塞進系統日志的信息增加一些上下<BR>文。<BR> <BR>當一個可分區的模塊被卸載時,驅動程序應該通過為每個支持的主/次對調用fsync_dev<BR>來安排所有的分區刷新。而且,如果結構gendisk被插在全局鏈表中,它應該被刪除——<BR>注意spull并未自己插入它,原因上面提到過。<BR> <BR>spull的清除函數是:<BR> <BR>(代碼260 2#)<BR> <BR>(代碼261)<BR> <BR>使用Initrd進行分區檢測<BR> <BR>如果你想從一個設備上安裝你的根文件系統,而這個設備的驅動程序只有模塊化的形式<BR>,你就必須使用由現代Linux核心提供的Initrd工具。我不想在這里介紹Initrd,這一小<BR>節是針對那些了解Initrd并想知道它是如何影響塊設備驅動程序的讀者的。<BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>使用Initrd進行分區檢測<BR> <BR>如果你想從一個設備上安裝你的根文件系統,而這個設備的驅動程序只有模塊化的形式<BR>,你就必須使用由現代Linux核心提供的Initrd工具。我不想在這里介紹Initrd,這一小<BR>節是針對那些了解Initrd并想知道它是如何影響塊設備驅動程序的讀者的。<BR> <BR>當你用Initrd引導一個核心時,它會在安裝真正的根文件系統之前建立一個暫時的運行<BR>環境。模塊通常是從被用作臨時根文件系統的ramdisk中裝載。<BR> <BR> <BR>--<BR><FONT
color=#00ff00>※ 來源:.華南網木棉站 bbs.gznet.edu.cn.[FROM: 202.38.196.234]</FONT><BR>--<BR><FONT
color=#00ffff>※ 轉寄:.華南網木棉站 bbs.gznet.edu.cn.[FROM: 211.80.41.106]</FONT><BR>--<BR><FONT
color=#0000ff>※ 轉寄:.華南網木棉站 bbs.gznet.edu.cn.[FROM: 211.80.41.106]</FONT><BR>--<BR><FONT
color=#ffff00>※ 轉載:.南京大學小百合站 bbs.nju.edu.cn.[FROM: 211.80.41.106]</FONT><BR>--<BR><FONT
color=#ff0000>※ 轉載:·飲水思源 bbs.sjtu.edu.cn·[FROM: 211.80.41.106]</FONT><BR></P></FONT>
<P align=center><A
href="http://211.71.69.201/joyfire/lsdp/index.htm"><FONT color=#ffffff
size=2>目錄頁</FONT></A> | <A
href="http://211.71.69.201/joyfire/lsdp/14.htm"><FONT color=#ffffff
size=2>上一頁</FONT></A> | <A
href="http://211.71.69.201/joyfire/lsdp/16.htm"><FONT color=#ffffff
size=2>下一頁</FONT></A></P></SPAN></TD></TR></TBODY></TABLE>
<TABLE cellSpacing=0 cellPadding=0 width="90%" align=center border=0>
<TBODY>
<TR>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -