1. 程式人生 > >虛擬記憶體,頁表,快表,多級頁表,倒排頁表

虛擬記憶體,頁表,快表,多級頁表,倒排頁表

虛擬記憶體

儘管基址暫存器和界限暫存器可以用於建立地址空間的抽象,還有另一個問題需要解決:管理軟體的膨脹(bloatware)。雖然儲存器容量增長快速,但是軟體大小的增長更快。需要執行的程式往往大到記憶體無法容納,而且必然需要系統能夠支援多個程式同時執行,即使記憶體可以滿足其中單獨一個程式的需要,但總體來看,它們仍然超出了記憶體大小。交換技術(swapping)並不是一個有吸引力的解決方案(換入換出耗時),因為一個典型的SATA磁碟的峰值傳輸率最高達到100MB/s,這意味著至少需要10秒才能換出一個1GB的程式,並需要另一個10秒才能再將一個1GB的程式換入。

程式大於記憶體的問題早在計算時代開始就產生了。在20世紀60年代所採取的解決方法是:

把程式分割成許多片段,稱為覆蓋(overlay)。程式開始執行時,將覆蓋管理模組裝入記憶體,該管理模組立即裝入並執行覆蓋0。執行完成後,覆蓋0通知管理模組裝入覆蓋1,或者佔用覆蓋0的上方位置(如果有空間),或者佔用覆蓋0(如果沒有空間)。一些覆蓋系統非常複雜,允許多個覆蓋塊同時在記憶體中。覆蓋塊存放在磁碟上,在需要時由作業系統動態地換入換出

雖然由系統完成實際的覆蓋塊換入換出操作但是程式設計師必須把程式分割成多個片段把一個大程式分割成小的、模組化的片段是非常費時和枯燥的,並且易於出錯。很少程式設計師擅長使用覆蓋技術。因此,沒過多久就有人找到一個辦法,把全部工作都交給計算機去做。採用的這個方法稱為虛擬記憶體

(virtual memory)。虛擬記憶體的基本思想是:每個程式擁有自己的地址空間,這個空間被分割成多個每一塊稱作一頁或頁面(page)。每一頁有連續的地址範圍。這些頁被對映到實體記憶體,但並不是所有的頁都必須在記憶體中才能執行程式。當程式引用到一部分在實體記憶體中的地址空間時,由硬體立刻執行必要的對映。當程式引用到一部分不在實體記憶體中的地址空間(缺頁中斷)時,由作業系統負責將缺失的部分裝入實體記憶體(頁面置換)並重新執行失敗的指令

從某個角度來講,虛擬記憶體是對基址暫存器和界限暫存器的一種綜合。8088為正文和資料分離出專門的基址暫存器(但不包括界限暫存器)。而虛擬記憶體使得整個地址空間可以用相對較小的單元對映到實體記憶體,而不是為正文段和資料段分別進行重定位。

虛擬記憶體很適合在多道程式設計系統中使用,許多程式的片段同時儲存在記憶體中。當一個程式等待它的一部分讀入記憶體時,可以把CPU交給另一個程序使用。

分頁

大部分虛擬記憶體系統中都使用分頁技術。在任何一臺計算機上,程式引用了一組記憶體地址。當程式執行指令

  1. MOV REG,1000 

時,它把地址為1000的記憶體單元的內容複製到REG中(或者相反,這取決於計算機的型號)。地址可以通過索引、基址暫存器、段暫存器或其他方式產生。

由程式產生的這些地址稱為虛擬地址,它們構成了一個虛擬地址空間。在沒有虛擬記憶體的計算機上,系統直接將虛擬地址送到記憶體總線上,讀寫操作使用具有同樣地址的實體記憶體字;而在使用虛擬記憶體的情況下,虛擬地址不是被直接送到記憶體總線上,而是被送到記憶體管理單元(Memory Management Unit,MMU),MMU把虛擬地址對映為實體記憶體地址,如圖3-8所示。

圖3-9中一個簡單的例子說明了這種對映是如何工作的。在這個例子中,有一臺可以產生16位地址的計算機,地址範圍從0到64K,且這些地址是虛擬地址。然而,這臺計算機只有32KB的實體記憶體,因此,雖然可以編寫64KB的程式,但它們卻不能被完全調入記憶體執行。在磁碟上必須有一個可以大到64KB的程式核心映像的完整副本,以保證程式片段在需要時能被調入記憶體。

 

虛擬地址空間按照固定大小劃分成稱為頁面(page)的若干單元。在實體記憶體中對應的單元稱為頁框(page frame)。頁面和頁框的大小通常是一樣的,在本例中是4KB,現有的系統中常用的頁大小一般從512位元組到64KB。對應於64KB的虛擬地址空間和32KB的實體記憶體,我們得到16個虛擬頁面和8個頁框。RAM和磁碟之間的交換總是以整個頁面為單元進行的。

圖3-9中的標記符號如下:標記0K~4K的範圍表示該頁的虛擬地址或實體地址是0~4095。4K~8K的範圍表示地址4096~8191,等等。每一頁包含了4096個地址,起始於4096的整數倍位置,結束於4096倍數缺1。

當程式試圖訪問地址0時,例如執行下面這條指令

  1. MOV  REG,0 

將虛擬地址0送到MMU。MMU看到虛擬地址落在頁面0(0~4095),根據其對映結果,這一頁面對應的是頁框2(8192~12 287),因此MMU把地址變換為8192,並把地址8192送到總線上。記憶體對MMU一無所知,它只看到一個讀或寫地址8192的請求並執行它。MMU從而有效地把所有從0~4095的虛擬地址對映到了8192~12 287的實體地址。

同樣地,指令

  1. MOV  REG, 8192 

被有效地轉換為:

  1. MOV  REG, 24576 

因為虛擬地址8192(在虛擬頁面2中)被對映到實體地址24 567(在物理頁框6中)上。第三個例子,虛擬地址20 500在距虛擬頁面5(虛擬地址20 480~24 575)起始地址20位元組處,並且被對映到實體地址12 288+20=12 308。

通過恰當地設定MMU,可以把16個虛擬頁面對映到8個頁框中的任何一個。但是這並沒有解決虛擬地址空間比實體記憶體大的問題。在圖3-9中只有8個物理頁框,於是只有8個虛擬頁面被對映到了實體記憶體中,在圖3-9中用叉號表示的其他頁並沒有被對映。在實際的硬體中,用一個“在/不在”位(present/absent bit)記錄頁面在記憶體中的實際存在情況。

當程式訪問了一個未對映的頁面,例如執行指令

  1. MOV  REG,32780 

將會發生什麼情況呢?虛擬頁面8(從32 768開始)的第12個位元組所對應的實體地址是什麼呢?MMU注意到該頁面沒有被對映(在圖中用叉號表示),於是使CPU陷入到作業系統,這個陷阱稱為缺頁中斷(page fault)。作業系統找到一個很少使用的頁框且把它的內容寫入磁碟(如果它不在磁碟上)。隨後把需要訪問的頁面讀到剛才回收的頁框中,修改對映關係,然後重新啟動引起陷阱的指令。

例如,如果作業系統決定放棄頁框1,那麼它將把虛擬頁面8裝入實體地址4096,並對MMU對映做兩處修改。首先,它要標記虛擬頁面1表項為未對映,使以後任何對虛擬地址4096~8191的訪問都導致陷阱。隨後把虛擬頁面8的表項的叉號改為1,因此在引起陷阱的指令重新啟動時,它將把虛擬地址32780對映為實體地址4108(4096+12)。

MMU的內部結構以便了解它是怎麼工作的,以及瞭解為什麼選用的頁面大小都是2的整數次冪。在圖3-10中可以看到一個虛擬地址的例子,虛擬地址8196(二進位制是0010000000000100)用圖3-9所示的MMU對映機制進行對映,輸入的16位虛擬地址被分為4位的頁號和12位的偏移量。4位的頁號可以表示16個頁面,12位的偏移可以為一頁內的全部4096個位元組編址。

 

用頁號作為頁表(page table)的索引,以得出對應於該虛擬頁面的頁框號如果“在/不在”位是0,則將引起一個作業系統陷阱。如果該位是1,則將在頁表中查到的頁框號複製到輸出暫存器的高3位中再加上輸入虛擬地址中的低12位偏移量。如此就構成了15位的實體地址輸出暫存器的內容隨即被作為實體地址送到記憶體匯流排

頁表

虛擬地址到實體地址的對映虛擬地址被分成虛擬頁號(高位部分)和偏移量(低位部分)兩部分虛擬頁號可用做頁表的索引,以找到該虛擬頁面對應的頁表項。由頁表項可以找到頁框號(如果有的話)。然後把頁框號拼接到偏移量的高位端,以替換掉虛擬頁號,形成送往記憶體的實體地址

頁表的目的是把虛擬頁面對映為頁框

頁表項的結構

圖3-11中給出了頁表項的一個例子。不同計算機的頁表項大小可能不一樣,但32位是一個常用的大小。最重要的域是頁框號,其次是“在/不在”位,這一位是1時表示該表項是有效的,可以使用;0則表示該表項對應的虛擬頁面現在不在記憶體中,訪問該頁面會引起一個缺頁中斷。

 

“保護”指出一個頁允許什麼型別的訪問。最簡單的形式是這個域只有一位,0表示讀/寫,1表示只讀。一個更先進的方法是使用三位,各位分別對應是否啟用讀、寫、執行該頁面。

為了記錄頁面的使用狀況,引入了“修改”(modified)和“訪問”(referenced)位。在寫入一頁時由硬體自動設定修改位。該位在作業系統重新分配頁框時是非常有用的。如果一個頁面已經被修改過(即它是“髒”的),則必須把它寫回磁碟。如果一個頁面沒有被修改過(即它是“乾淨”的),則只簡單地把它丟棄就可以了,因為它在磁碟上的副本仍然是有效的。這一位有時也被稱為髒位(dirty bit),因為它反映了該頁面的狀態。

不論是讀還是寫,系統都會在該頁面被訪問時設定訪問位。它的值被用來幫助作業系統在發生缺頁中斷時選擇要被淘汰的頁面。不再使用的頁面要比正在使用的頁面更適合淘汰。這一位在即將討論的很多頁面置換演算法中都會起到重要的作用。

最後一位用於禁止該頁面被快取記憶體。對那些對映到裝置暫存器而不是常規記憶體的頁面而言,這個特性是非常重要的。假如作業系統正在緊張地迴圈等待某個I/O裝置對它剛發出的命令作出響應,保證硬體是不斷地從裝置中讀取資料而不是訪問一箇舊的被快取記憶體的副本是非常重要的。通過這一位可以禁止快取記憶體。具有獨立的I/O空間而不使用記憶體對映I/O的機器不需要這一位。

應該注意的是,若某個頁面不在記憶體時,用於儲存該頁面的磁碟地址不是頁表的一部分。原因很簡單,頁表只儲存把虛擬地址轉換為實體地址時硬體所需要的資訊。作業系統在處理缺頁中斷時需要把該頁面的磁碟地址等資訊儲存在作業系統內部的軟體表格中。硬體不需要它。

虛擬記憶體本質上是用來創造一個新的抽象概念—地址空間,這個概念是對實體記憶體的抽象,類似於程序是對物理機器(CPU)的抽象。虛擬記憶體的實現,是將虛擬地址空間分解成頁,並將每一頁對映到實體記憶體的某個頁框或者(暫時)解除對映。

加速分頁過程

在任何分頁式系統中,都需要考慮兩個主要問題:

1) 虛擬地址到實體地址的對映必須非常快。

2) 如果虛擬地址空間很大,頁表也會很大。

第一個問題是由於每次訪問記憶體,都需要進行虛擬地址到實體地址的對映。所有的指令最終都必須來自記憶體,並且很多指令也會訪問記憶體中的運算元。因此,每條指令進行一兩次或更多頁表訪問是必要的。如果執行一條指令需要1ns,頁表查詢必須在0.2ns之內完成,以避免對映成為一個主要瓶頸。

第二個問題來自現代計算機使用至少32位的虛擬地址,而且64位變得越來越普遍。假設頁長為4KB,32位的地址空間將有100萬頁,而64位地址空間簡直多到超乎你的想象。如果虛擬地址空間中有100萬個頁,那麼頁表必然有100萬條表項。另外請記住,每個程序都需要自己的頁表(因為它有自己的虛擬地址空間)。

對大而快速的頁對映的需求成為了構建計算機的重要約束。最簡單的設計(至少從概念上)是使用由一組“快速硬體暫存器”組成的單一頁表,每一個表項對應一個虛頁,虛頁號作為索引,如圖3-10所示。當啟動一個程序時,作業系統把儲存在記憶體中的程序頁表的副本載入到暫存器中。在程序執行過程中,不必再為頁表而訪問記憶體。這個方法的優勢是簡單並且在對映過程中不需要訪問記憶體。而缺點是在頁表很大時,代價高昂。而且每一次上下文切換都必須裝載整個頁表,這樣會降低效能

另一種極端方法是,整個頁表都在記憶體中。那時所需的硬體僅僅是一個指向頁表起始位置的暫存器。這樣的設計使得在上下文切換時,進行“虛擬地址到實體地址”的對映只需重新裝入一個暫存器。當然,這種做法的缺陷是在執行每條指令時,都需要一次或多次記憶體訪問,以完成頁表項的讀入,速度非常慢。

1. 轉換檢測緩衝區(快表

加速分頁機制和處理大的虛擬地址空間的實現方案,先介紹加速分頁問題。大多數優化技術都是從記憶體中的頁表開始的。這種設計對效率有著巨大的影響。例如,假設一條指令要把一個暫存器中的資料複製到另一個暫存器。在不分頁的情況下,這條指令只訪問一次記憶體,即從記憶體中取指令。有了分頁後,則因為要訪問頁表而引起更多次的訪問記憶體。由於執行速度通常被CPU從記憶體中取指令和資料的速度所限制,所以每次記憶體訪問必須進行兩次頁表訪問會降低一半的效能。在這種情況下,沒人會採用分頁機制。

多年以來,計算機的設計者已經意識到了這個問題,並找到了一種解決方案。這種解決方案的建立基於這樣一種現象:大多數程式總是對少量的頁面進行多次的訪問,而不是相反的。因此,只有很少的頁表項會被反覆讀取,而其他的頁表項很少被訪問。

上面提到的解決方案是為計算機設定一個小型的硬體裝置,將虛擬地址直接對映到實體地址,而不必再訪問頁表。這種裝置稱為轉換檢測緩衝區(Translation Lookaside Buffer,TLB),有時又稱為相聯儲存器(associate memory),如圖3-12所示。它通常在MMU中,包含少量的表項,在此例中為8個,在實際中很少會超過64個。每個表項記錄了一個頁面的相關資訊,包括虛擬頁號、頁面的修改位、保護碼(讀/寫/執行許可權)和該頁所對應的物理頁框。除了虛擬頁號(不是必須放在頁表中的),這些域與頁表中的域是一一對應的。另外還有一位用來記錄這個表項是否有效(即是否在使用)。

 

如果一個程序在虛擬地址19、20和21之間有一個迴圈,那麼可能會生成圖3-12中的TLB。因此,這三個表項中有可讀和可執行的保護碼。當前主要使用的資料(假設是個陣列)放在頁面129和頁面130中。頁面140包含了用於陣列計算的索引。最後,堆疊位於頁面860和頁面861。

現在看一下TLB是如何工作的。將一個虛擬地址放入MMU中進行轉換時,硬體首先通過將該虛擬頁號與TLB中所有表項同時(即並行)進行匹配,判斷虛擬頁面是否在其中。如果發現了一個有效的匹配並且要進行的訪問操作並不違反保護位,則將頁框號直接從TLB中取出而不必再訪問頁表。如果虛擬頁面號確實是在TLB中,但指令試圖在一個只讀頁面上進行寫操作,則會產生一個保護錯誤,就像對頁表進行非法訪問一樣

當虛擬頁號不在 TLB中時發生的事情值得討論。如果MMU檢測到沒有有效的匹配項時,就會進行正常的頁表查詢接著從TLB中淘汰一個表項,然後用新找到的頁表項代替它。這樣,如果這一頁面很快再被訪問,第二次訪問TLB時自然將會命中而不是不命中。當一個表項被清除出TLB時,將修改位複製到記憶體中的頁表項,而除了訪問位,其他的值不變。當頁表項中從頁表裝入到TLB中時,所有的值都來自記憶體。

2. 軟體TLB管理

到目前為止,已經假設每一臺具有虛擬記憶體的機器都具有由硬體識別的頁表,以及一個TLB。在這種設計中,對TLB的管理和TLB的失效處理都完全由MMU硬體來實現只有在記憶體中沒有找到某個頁面時,才會陷入到作業系統中

但是,許多現代的RISC機器幾乎所有的頁面管理都是在軟體中實現的。在這些機器上,TLB表項被作業系統顯式地裝載。當發生TLB訪問失效,不再是由MMU到頁表中查詢並取出需要的頁表項,而是生成一個TLB失效並將問題交給作業系統解決。系統必須先找到該頁面,然後從TLB中刪除一個項,接著裝載一個新的項,最後再執行先前出錯的指令。當然,所有這一切都必須在有限的幾條指令中完成,因為TLB失效比缺頁中斷髮生的更加頻繁。

如果TLB大(如64個表項)到可以減少失效率時,TLB的軟體管理就會變得足夠有效。這種方法的最主要的好處是獲得了一個非常簡單的MMU,這就在CPU晶片上為快取記憶體以及其他改善效能的設計騰出了相當大的空間。

改善使用軟體TLB管理的機器的效能:其中一種策略是在減少TLB失效的同時,又要在發生TLB失效時減少處理開銷。為了減少TLB失效,有時候作業系統能用“直覺”指出哪些頁面下一步可能會被用到並預先為它們在TLB中裝載表項。例如,當一個客戶程序傳送一條訊息給同一臺機器上的伺服器程序,很可能伺服器將不得不立即執行。瞭解了這一點,當執行處理send的陷阱時,系統也可以找到伺服器的內碼表、資料頁以及堆疊頁,並在有可能導致TLB失效前把它們裝載到TLB中。

無論是用硬體還是用軟體來處理TLB失效,常見方法都是找到頁表並執行索引操作以定位將要訪問的頁面。用軟體做這樣的搜尋的問題是,頁表可能不在TLB中,會導致處理過程中的額外的TLB失。可以通過在記憶體中的固定位置維護一個大的(如4KB)TLB表項的軟體快取記憶體(該快取記憶體的頁面總是被儲存在TLB中)來減少TLB失效通過首先檢查軟體快取記憶體,作業系統能夠實質性地減少TLB失效。

使用軟體TLB管理時,一個基本要求是要理解兩種不同的TLB失效的區別在哪裡。當一個頁面訪問在記憶體中而不在TLB中時,將產生軟失效(soft miss)。那麼此時所要做的就是更新一下TLB,不需要產生磁碟I/O。典型的處理需要10~20個機器指令並花費幾個納秒完成操作。相反,當頁面本身不在記憶體中(當然也不在TLB中)時,將產生硬失效。此刻需要一次磁碟存取以裝入該頁面,這個過程大概需要幾毫秒。硬失效的處理時間往往是軟失效的百萬倍。

針對大記憶體的頁表

在原有的記憶體頁表的方案之上,引入快表(TLB)可以用來加快虛擬地址到實體地址的轉換。不過這不是惟一需要解決的問題,另一個問題是怎樣處理巨大的虛擬地址空間

1. 多級頁表

第一種方法是採用多級頁表。一個簡單的例子如圖3-13所示。在圖3-13a中,32位的虛擬地址被劃分為10位的PT1域、10位的PT2域和12位的Offset(偏移量)域。因為偏移量是12位,所以頁面長度是4KB(2^12/1024),共有2^20個頁面。

引入多級頁表的原因是避免把全部頁表一直儲存在記憶體中。特別是那些從不需要的頁表就不應該保留。比如一個需要12MB記憶體的程序,其最底端是4MB的程式正文段,後面是4MB的資料段,頂端是4MB的堆疊段,在資料段上方和堆疊段下方之間是大量根本沒有使用的空閒區。

考察圖3-13b例子中的二級頁表是如何工作的。在左邊是頂級頁表,它具有1024個表項,對應於10位的PT1域。當一個虛擬地址被送到MMU時,MMU首先提取PT1域並把該值作為訪問頂級頁表的索引。因為整個4GB(32位)虛擬地址空間已經被分成1024個4MB的塊,所以這1024個表項中的每一個都表示4MB的虛擬地址空間。

 

由索引頂級頁表得到的表項中含有二級頁表的地址或頁框號。頂級頁表的表項0指向程式正文的頁表,表項1指向資料的頁表,表項1023指向堆疊的頁表,其他的表項(用陰影表示的)未用。現在把PT2域作為訪問選定的二級頁表的索引,以便找到該虛擬頁面的對應頁框號。

下面看一個示例,考慮32位虛擬地址0x00403004(十進位制4 206 596)位於資料部分12 292位元組處。它的虛擬地址對應PT1=1,PT2=3,Offset=4。MMU首先用PT1作為索引訪問頂級頁表得到表項1,它對應的地址範圍是4M~8M。然後,它用PT2作為索引訪問剛剛找到的二級頁表並得到表項3,它對應的虛擬地址範圍是在它的4M塊內的12 288~16 383(即絕對地址4 206 592~4 210 687)。這個表項含有虛擬地址0x00403004所在頁面的頁框號。如果該頁面不在記憶體中,頁表項中的“在/不在”位將是0,引發一次缺頁中斷。如果該頁面在記憶體中,從二級頁表中得到的頁框號將與偏移量(4)結合形成實體地址。該地址被放到總線上並送到記憶體中。

值得注意的是,雖然在圖3-13中虛擬地址空間超過100萬個頁面,實際上只需要四個頁表:頂級頁表以及0~4M(正文段)、4M~8M(資料段)和頂端4M(堆疊段)的二級頁表。頂級頁表中1021個表項的“在/不在”位都被設為0,當訪問它們時強制產生一個缺頁中斷。如果發生了這種情況,作業系統將注意到程序正在試圖訪問一個不希望被訪問的地址,並採取適當的行動,比如向程序發出一個訊號或殺死程序等。在這個例子中的各種長度選擇的都是整數,並且選擇PT1與PT2等長,但在實際中也可能是其他的值。

圖3-13所示的二級頁表可擴充為三級、四級或更多級。級別越多,靈活性就越大,但頁表超過三級會帶來更大的複雜性,這樣做是否值得令人懷疑。

2. 倒排頁表

對32位虛擬地址空間,多級頁表可以很好地發揮作用。但是,隨著64位計算機變得更加普遍,情況發生了徹底的變化。如果現在的地址空間是264位元組,頁面大小為4KB,我們需要一個有252個表項的頁表。如果每一個表項8個位元組,那麼整個頁表就會超過3000萬GB(30PB)。僅僅為頁表耗費3000萬GB不是個好主意(現在不是,可能以後幾年也不是)。因而,具有64位分頁虛擬地址空間的系統需要一個不同的解決方案。

解決方案之一就是使用倒排頁表(inverted page table)。在這種設計中,在實際記憶體中每一個頁框有一個表項,而不是每一個虛擬頁面有一個表項。例如,對於64位虛擬地址,4KB的頁,1GB的RAM,一個倒排頁表僅需要262 144(1GB/4KB)個頁表項。表項記錄哪一個(程序,虛擬頁面)對定位於該頁框。

雖然倒排頁表節省了大量的空間(至少當虛擬地址空間比實體記憶體大得多的時候是這樣的),但它也有嚴重的不足:從虛擬地址到實體地址的轉換會變得很困難。當程序n訪問虛擬頁面p時,硬體不再能通過把p當作指向頁表的一個索引來查詢物理頁框。取而代之的是,它必須搜尋整個倒排頁表來查詢某一個表項(n,p)。此外,該搜尋必須對每一個記憶體訪問操作都要執行一次,而不僅僅是在發生缺頁中斷時執行。每一次記憶體訪問操作都要查詢一個256K的表是不會讓你的機器執行得很快的。

走出這種兩難局面的辦法是使用TLB。如果TLB能夠記錄所有頻繁使用的頁面,地址轉換就可能變得像通常的頁表一樣快。但是,當發生TLB失效時,需要用軟體搜尋整個倒排頁表。一個可行的實現該搜尋的方法建立一張散列表用虛擬地址來雜湊當前所有在記憶體中的具有相同雜湊值的虛擬頁面被連結在一起,如圖3-14所示。如果散列表中的槽數與機器中物理頁面數一樣多,那麼散列表的衝突鏈的平均長度將會是1個表項,這將會大大提高對映速度。一旦頁框號被找到,新的(虛擬頁號,物理頁框號)對就會被裝載到TLB中。

  

倒排頁表在64位機器中很常見,因為在64位機器中即使使用了大頁面,頁表項的數量還是很龐大的。例如,對於4MB頁面和64位虛擬地址,需要242個頁表項。處理大虛存的其他方法可參見Talluri等人的論文(1995)。