存屏障機制及內核相關源代碼分析
分析人:余旭
分析版本:Linux Kernel 2.6.14 來自于:www.kernel.org
分析開始時間:2005-11-17-20:45:56
分析結束時間:2005-11-21-20:07:32
編號:2-1 類別:進程管理-準備工作1-內存屏障
Email:yuxu9710108@163.com
版權聲明:版權保留。本文用作其他用途當經作者本人同意,轉載請注明作者姓名
All Rights Reserved. If for other use,must Agreed By the writer.Citing this text,please claim the writer's name.
Copyright (C) 2005 YuXu
*************************************************************
內存屏障是Linux Kernel中常要遇到的問題,這里專門來對其進行研究。一者查閱網上現有資料,進行整理匯集;二者翻閱Linux內核方面的指導書,從中提煉觀點;最后,自己加以綜合分析,提出自己的看法。下面將對個問題進行專題分析。
*****************************************************************************
------------------------------------------------------ 專題研究:內存屏障--------------------------------
---------------------------------------------------------論壇眾人資料匯集分析---------------------------
set_current_state(),__set_current_state(),set_task_state(),__set_task_state(),rmb(),wmb(),mb()的源代碼中的相關疑難問題及眾人的論壇觀點:
-----------------------------------------------------------------------------------------------------------------
1.--->ymons 在www.linuxforum.net Linux內核技術論壇發貼問:
set_current_state和__set_current_state的區別?
#define __set_current_state(state_value) /
do { current->state = (state_value); } while (0)
#define set_current_state(state_value) /
set_mb(current->state, (state_value))
#define set_mb(var, value) do { var = value; mb(); } while (0)
#define mb() __asm__ __volatile__ ("" : : : "memory")
在linux的源代碼中經常有這種設置當前進程狀態的代碼,但我搞不清楚這兩種用法的不同?有哪位大蝦指點一二,必將感謝不盡!
------------------
2.---> chyyuu(chenyu-tmlinux@hpclab.cs.tsinghua.edu.cn) 在www.linuxforum.net的Linux內核技術上發貼問:
在kernel.h中有一個define
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
在內核許多地方被調用,不知到底是生成什么匯編指令????
請教!!!
--------------------
3.--->tigerl 02-12-08 10:57 在www.linuxforum.net的Linux內核技術提問:
這一句(include/asm-i386/system.h中)定義是什么意思?
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
4.--->jackcht 01-03-02 10:55 在www.linuxforum.net Linux內核技術 :
各位大蝦,我在分析linux的時候發現有一個古怪的函數,就是barrier,俺愣是不知道它是干嘛用的,幫幫我這菜鳥吧,感謝感謝!
還有就是下面這句中的("":::"memory")是什么意思呀,我苦!
# define barrier() _asm__volatile_("": : :"memory")
***********************************眾人的觀點*******************************
ANSWER:
1.jkl Reply:這就是所謂的內存屏障,前段時間曾經討論過。CPU越過內存屏障后,將刷新自已對存儲器的緩沖狀態。這條語句實際上不生成任何代碼,但可使gcc在barrier()之后刷新寄存器對變量的分配。
2.wheelz發帖指出:
#define __set_task_state(tsk, state_value) /
do { (tsk)->state = (state_value); } while (0)
#define set_task_state(tsk, state_value) /
set_mb((tsk)->state, (state_value))
set_task_state()帶有一個memory barrier,__set_task_state()則沒有,當狀態state是RUNNING時,因為scheduler可能訪問這個state,因此此時要變成其他狀態(如INTERRUPTIBLE),就要用set_task_state()而當state不是RUNNING時,因為沒有其他人會訪問這個state,因此可以用__set_task_state()反正用set_task_state()肯定是安全的,但 __set_task_state()可能會快些。
自己分析:
wheelz講解很清楚,尤其是指出了__set_task_state()速度會快于set_task_state()。這一點,很多貼子忽略了,這里有獨到之處。在此,作者專門強調之。
3.自己分析:
1)set_mb(),mb(),barrier()函數追蹤到底,就是__asm__ __volatile__("":::"memory"),而這行代碼就是內存屏障。
2)__asm__用于指示編譯器在此插入匯編語句
3)__volatile__用于告訴編譯器,嚴禁將此處的匯編語句與其它的語句重組合優化。即:原原本本按原來的樣子處理這這里的匯編。
4)memory強制gcc編譯器假設RAM所有內存單元均被匯編指令修改,這樣cpu中的registers和cache中已緩存的內存單元中的數據將作廢。cpu將不得不在需要的時候重新讀取內存中的數據。這就阻止了cpu又將registers,cache中的數據用于去優化指令,而避免去訪問內存。
5)"":::表示這是個空指令。barrier()不用在此插入一條串行化匯編指令。在后文將討論什么叫串行化指令。
6)__asm__,__volatile__,memory在前面已經解釋
7)lock前綴表示將后面這句匯編語句:"addl $0,0(%%esp)"作為cpu的一個內存屏障。
8)addl $0,0(%%esp)表示將數值0加到esp寄存器中,而該寄存器指向棧頂的內存單元。加上一個0,esp寄存器的數值依然不變。即這是一條無用的匯編指令。在此利用這條無價值的匯編指令來配合lock指令,在__asm__,__volatile__,memory的作用下,用作cpu的內存屏障。
9)set_current_state()和__set_current_state()區別就不難看出。
10)至于barrier()就很易懂了。
11)作者注明:作者在回答這個問題時候,參考了《深入理解LINUX內核》一書,陳莉君譯,中國電力出版社,P174
4.xshell 發貼指出:
#include <asm/system.h>
"void rmb(void);"
"void wmb(void);"
"void mb(void);"
這些函數在已編譯的指令流中插入硬件內存屏障;具體的插入方法是平臺相關的。rmb(讀內存屏障)保證了屏障之前的讀操作一定會在后來的讀操作執行之前完成。wmb 保證寫操作不會亂序,mb 指令保證了兩者都不會。這些函數都是 barrier函數的超集。解釋一下:編譯器或現在的處理器常會自作聰明地對指令序列進行一些處理,比如數據緩存,讀寫指令亂序執行等等。如果優化對象是普通內存,那么一般會提升性能而且不會產生邏輯錯誤。但如果對I/O操作進行類似優化很可能造成致命錯誤。所以要使用內存屏障,以強制該語句前后的指令以正確的次序完成。其實在指令序列中放一個wmb的效果是使得指令執行到該處時,把所有緩存的數據寫到該寫的地方,同時使得wmb前面的寫指令一定會在wmb的寫指令之前執行。
5.Nazarite發貼指出:
__volatitle__是防止編譯器移動該指令的位置或者把它優化掉。"memory",是提示編譯器該指令對內存修改,防止使用某個寄存器中已經load的內存的值。lock 前綴是讓cpu的執行下一行指令之前,保證以前的指令都被正確執行。
再次發貼指出:
The memory keyword forces the compiler to assume that all memory locations in RAM have been changed by the assembly language instruction; therefore, the compiler cannot optimize the code by using the values of memory locations stored in CPU registers before the asm instruction.
6.bx bird 發貼指出:
cpu上有一根pin #HLOCK連到北橋,lock前綴會在執行這條指令前先去拉這根pin,持續到這個指令結束時放開#HLOCK pin,在這期間,北橋會屏蔽掉一切外設以及AGP的內存操作。也就保證了這條指令的atomic。
7.coldwind 發貼指出:
"memory",是提示編譯器該指令對內存修改,防止使用某個寄存器中已經load的內存的值,應該是告訴CPU內存已經被修改過,讓CPU invalidate所有的cache。
通過以上眾人的貼子的分析,自己綜合一下,這4個宏set_current_state(),__set_current_state(), set_task_state(),__set_task_state()和3個函數rmb(),wmb(),mb()的源代碼中的疑難大都被解決。此處只是匯集眾人精彩觀點,只用來解決代碼中的疑難,具體有序系統的源代碼將在后面給出。
--------------------------------------------------------------------------------------------------------------
mfence,mb(),wmb(),OOPS的疑難問題的突破
--------------------------------------------------------------------------------------------------------------
1.--->puppy love (zhou_ict@hotmail.com )在www.linuxforum.net CPU 與 編譯器 問: 在linux核心當中, mb(x86-64)的實現是 ("mfence":::"memory")
我查了一下cpu的manual,mfence用來同步指令執行的。而后面的memory clober好像是gcc中用來干擾指令調度的。但還是不甚了了,哪位能給解釋解釋嗎? 或者有什么文檔之類的可以推薦看看的?
ANSWER:
1.classpath 發貼指出:
mfence is a memory barrier supported by hardware, and it only makes sense for shared memory systems.
For example, you have the following codes
<codes1>
mfence
<codes2>
mfence or other memory barriers techniques disallows the code motion (load/store)from codes2 to codes1 done by _hardware_ . Some machines like P4 can move loads in codes 2 before stores in codes1, which is out-of-order.
Another memory barrier is something like
("":::"memory"),
which disallows the code motion done by _compiler_. But IMO memory access order is not always guaranteed in this case.
-----
2.canopy 發貼指出:
我稍微看了一下x86-64的手冊。mfence保證系統在后面的memory訪問之前,先前的memory訪問都已經結束。由于這條指令可能引起memory任意地址上內容的改變,所以需要用“memory” clobber告訴gcc這一點。這樣gcc就需要重新從memory中load寄存器來保證同一變量在寄存器和memory中的內容一致。
------------------
3.cool_bird Reply:
內存屏障
MB(memory barrier,內存屏障) :x86采用PC(處理機)內存一致性模型,使用MB強加的嚴格的CPU內存事件次序,保證程序的執行看上去象是遵循順序一致性(SC)模型,當然,即使對于UP,由于內存和設備見仍有一致性問題,這些Mb也是必須的。在當前的實現中,wmb()實際上是一個空操作,這是因為目前Intel的CPU系列都遵循“處理機一致性”,所有的寫操作是遵循程序序的,不會越過前面的讀寫操作。但是,由于Intel CPU系列可能會在將來采用更弱的內存一致性模型并且其他體系結構可能采用其他放松的一致性模型,仍然在內核里必須適當地插入wmb()保證內存事件的正確次序。
見頭文件include/asm/system.h
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
#define rmb() mb()
#define wmb() __asm__ __volatile__ ("": : :"memory")
此外,barrier實際上也是內存屏障。
include/linux/kernel.h:
#define barrier() __asm__ __volatile__("": : :"memory")
內存屏障也是一種避免鎖的技術。
它在進程上下文中將一個元素插入一個單向鏈表:
new->next=i->next;
wmb();
i->next=new;
同時,如果不加鎖地遍歷這個單向鏈表。或者在遍歷鏈表時已經可以看到new,或者new還不在該鏈表中。Alan Cox書寫這段代碼時就注意到了這一點,兩個內存寫事件的順序必須按照程序順序進行。否則可能new的next指針將指向一個無效地址,就很可能出現 OOPS!
不論是gcc編譯器的優化還是處理器本身采用的大量優化,如Write buffer, Lock-up free, Non-blocking reading, Register allocation, Dynamic scheduling, Multiple issues等,都可能使得實際執行可能違反程序序,因此,引入wmb內存屏障來保證兩個寫事件的執行次序嚴格按程序順序來執行。
作者說明:原貼子不太清楚,作者作了必要的調整。
**************************************************************************
作者讀到這里,不懂OOPS便又上網查找OOPS的資料學習如下,以期望搞懂OOPS后能更好的理解上面這段話。
------------------------------------------OOPS解釋--------------------------------------------------
1.網上第一個貼子:
--->殊途同歸 發表于 2005-7-26 16:40:00 :掌握 Linux 調試技術 來自中國教育人博客:www.blog.edu.cn/index.html
Oops 分析
Oops(也稱panic,慌張)消息包含系統錯誤的細節,如CPU寄存器的內容。在 Linux 中,調試系統崩潰的傳統方法是分析在發生崩潰時發送到系統控制臺的 Oops消息。一旦您掌握了細節,就可以將消息發送到ksymoops實用程序,它將試圖將代碼轉換為指令并將堆棧值映射到內核符號。在很多情況下,這些信息就足夠您確定錯誤的可能原因是什么了。請注意,Oops 消息并不包括核心文件。
2.網上第二個貼子:
--->www.plinux.org自由飛鴿 上的貼子:System.map文件的作用 作者:趙炯
gohigh@sh163.net
作者說明:
1.OOPS和System.map文件密切相關。所以要研討System.map文件。
2.本作者對所引用的文章內容進行了整理,刪除了一些次要的部分,插入了一些內容,使文章更清晰。再者對一些內容進行了擴展說明。
--->符號表:
1.什么是符號(Symbols)?
在編程中,一個符號(symbol)是一個程序的創建塊:它是一個變量名或一個函數名。如你自己編制的程序一樣,內核具有各種符號也是不應該感到驚奇的。當然,區別在 于內核是一非常復雜的代碼塊,并且含有許多、許多的全局符號。
2.內核符號表(Kernel Symbol Table)是什么東西?
內核并不使用符號名。它是通過變量或函數的地址(指針)來使用變量或函數的,而 不是使用size_t BytesRead,內核更喜歡使用(例如)c0343f20來引用這個變量。
而另一方面,人們并不喜歡象c0343f20這樣的名字。我們跟喜歡使用象 size_t BytesRead這樣的表示。通常,這并不會帶來什么問題。內核主要是用C語言寫成的,所以在我們編程時編譯器/連接程序允許我們使用符號名,并且使內核在運行時使用地址表示。這樣大家都滿意了。
然而,存在一種情況,此時我們需要知道一個符號的地址(或者一個地址對應的 符號)。這是通過符號表來做到的,與gdb能夠從一個地址給出函數名(或者給出一個函數名的地址)的情況很相似。符號表是所有符號及其對應地址的一個列表。這里是 一個符號表例子:
c03441a0 B dmi_broken
c03441a4 B is_sony_vaio_laptop
c03441c0 b dmi_ident
c0344200 b pci_bios_present
c0344204 b pirq_table
c0344208 b pirq_router
c034420c b pirq_router_dev
c0344220 b ascii_buffer
c0344224 b ascii_buf_bytes
你可以看出名稱為dmi_broken的變量位于內核地址c03441a0處。
--->;System.map文件與ksyms:
1.什么是System.map文件?
有兩個文件是用作符號表的:
/proc/ksyms
System.map
這里,你現在可以知道System.map文件是干什么用的了。每當你編譯一個新內核時,各種符號名的地址定會變化。
/proc/ksyms 是一個 "proc文件" 并且是在內核啟動時創建的。實際上它不是一個真實的文件;它只是內核數據的簡單表示形式,呈現出象一個磁盤文件似的。如果你不相信我,那么就試試找出/proc/ksyms的文件大小來。因此, 對于當前運行的內核來說,它總是正確的..
然而,System.map卻是文件系統上的一個真實文件。當你編譯一個新內核時,你原來的System.map中的符號信息就不正確了。隨著每次內核的編譯,就會產生一個新的 System.map文件,并且需要用該文件取代原來的文件。
--->OOPS:
1.什么是一個Oops?
在自己編制的程序中最常見的出錯情況是什么?是段出錯(segfault),信號11。
Linux內核中最常見的bug是什么?也是段出錯。除此,正如你想象的那樣,段出錯的問題是非常復雜的,而且也是非常嚴重的。當內核引用了一個無效指針時,并不稱其為段出錯 -- 而被稱為"oops"。一個oops表明內核存在一個bug,應該總是提出報告并修正該bug。
2.OOPS與段違例錯的比較:
請注意,一個oops與一個段出錯并不是一回事。你的程序并不能從段出錯中恢復 過來,當出現一個oops時,并不意味著內核肯定處于不穩定的狀態。Linux內核是非常健壯的;一個oops可能僅殺死了當前進程,并使余下的內核處于一個良好的、穩定的狀態。
3.OOPS與panic的比較:
一個oops并非是內核死循環(panic)。在內核調用了panic()函數后,內核就不能繼續運行了;此時系統就處于停頓狀態并且必須重啟。如果系統中關鍵部分遭到破壞那么一個oops也可能會導致內核進入死循環(panic)。例如,設備驅動程序中 出現的oops就幾乎不會導致系統進行死循環。
當出現一個oops時,系統就會顯示出用于調試問題的相關信息,比如所有CPU寄存器中的內容以及頁描述符表的位置等,尤其會象下面那樣打印出EIP(指令指針)的內容:
EIP: 0010:[<00000000>]
Call Trace: []
4.一個Oops與System.map文件有什么關系呢?
我想你也會認為EIP和Call Trace所給出的信息并不多,但是重要的是,對于內核開發人員來說這些信息也是不夠的。由于一個符號并沒有固定的地址, c010b860可以指向任何地方。
為了幫助我們使用oops含糊的輸出,Linux使用了一個稱為klogd(內核日志后臺程序)的后臺程序,klogd會截取內核oops并且使用syslogd將其記錄下來,并將某些象c010b860信息轉換成我們可以識別和使用的信息。換句話說,klogd是一個內核消息記錄器 (logger),它可以進行名字-地址之間的解析。一旦klogd開始轉換內核消息,它就使用手頭的記錄器,將整個系統的消息記錄下來,通常是使用 syslogd記錄器。
為了進行名字-地址解析,klogd就要用到System.map文件。我想你現在知道一個oops與System.map的關系了。
---------------------
作者補充圖:
System.map文件
^
|
|
syslogd記錄------->klogd解析名字-地址
^
|
|
內核出錯----->OOPS
-----------------------
深入說明: klogd會執行兩類地址解析活動:
1.靜態轉換,將使用System.map文件。 所以得知System.map文件只用于名字-地址的靜態轉換。
2.Klogd動態轉換
動態轉換,該方式用于可加載模塊,不使用System.map,因此與本討論沒有關系,但我仍然對其加以簡單說明。假設你加載了一個產生oops 的內核模塊。于是就會產生一個oops消息,klogd就會截獲它,并發現該oops發生在d00cf810處。由于該地址屬于動態加載模塊,因此在 System.map文件中沒有對應條目。klogd將會在其中尋找并會毫無所獲,于是斷定是一個可加載模塊產生了oops。此時klogd就會向內核查詢該可加載模塊輸出的符號。即使該模塊的編制者沒有輸出其符號,klogd也起碼會知道是哪個模塊產生了oops,這總比對一個oops一無所知要好。
還有其它的軟件會使用System.map,我將在后面作一說明。
--------------
System.map應該位于什么地方?
System.map應該位于使用它的軟件能夠尋找到的地方,也就是說,klogd會在什么地方尋找它。在系統啟動時,如果沒有以一個參數的形式為klogd給出System.map的位置,則klogd將會在三個地方搜尋System.map。依次為:
/boot/System.map
/System.map
/usr/src/linux/System.map
System.map 同樣也含有版本信息,并且klogd能夠智能化地搜索正確的map文件。例如,假設你正在運行內核2.4.18并且相應的map文件位于 /boot/System.map。現在你在目錄/usr/src/linux中編譯一個新內核2.5.1。在編譯期間,文件 /usr/src/linux/System.map就會被創建。當你啟動該新內核時,klogd將首先查詢/boot/System.map,確認它不是啟動內核正確的map文件,就會查詢/usr/src/linux/System.map, 確定該文件是啟動內核正確的map文件并開始讀取其中的符號信息。
幾個注意點:
1.klogd未公開的特性:
在2.5.x系列內核的某個版本,Linux內核會開始untar成linux-version,而非只是linux(請舉手表決--有多少人一直等待著這樣做?)。我不知道klogd是否已經修改為在/usr/src/linux-version/System.map中搜索。TODO:查看 klogd源代碼。
在線手冊上對此也沒有完整描述,請看:
# strace -f /sbin/klogd | grep 'System.map'
31208 open("/boot/System.map-2.4.18", O_RDONLY|O_LARGEFILE) = 2
顯然,不僅klogd在三個搜索目錄中尋找正確版本的map文件,klogd也同樣知道尋找名字為 "System.map" 后加"-內核版本",象 System.map-2.4.18. 這是klogd未公開的特性。
2.驅動程序與System.map文件的關系:
有一些驅動程序將使用System.map來解析符號(因為它們與內核頭連接而非glibc庫等),如果沒有System.map文件,它們將不能正確地工作。這與一個模塊由于內核版本不匹配而沒有得到加載是兩碼事。模塊加載是與內核版本有關,而與即使是同一版本內核其符號表也會變化的編譯后內核無關。
3.還有誰使用了System.map?
不要認為System.map文件僅對內核oops有用。盡管內核本身實際上不使用System.map,其它程序,象klogd,lsof,
satan# strace lsof 2>&1 1> /dev/null | grep System
readlink("/proc/22711/fd/4", "/boot/System.map-2.4.18", 4095) = 23
ps,
satan# strace ps 2>&1 1> /dev/null | grep System
open("/boot/System.map-2.4.18", O_RDONLY|O_NONBLOCK|O_NOCTTY) = 6
以及其它許多軟件,象dosemu,需要有一個正確的System.map文件。
4.如果我沒有一個好的System.map,會發生什么問題?
假設你在同一臺機器上有多個內核。則每個內核都需要一個獨立的System.map文件!如果所啟動的內核沒有對應的System.map文件,那么你將定期地看到這樣一條信息:
System.map does not match actual kernel (System.map與實際內核不匹配)
不是一個致命錯誤,但是每當你執行ps ax時都會惱人地出現。有些軟件,比如dosemu,可能不會正常工作。最后,當出現一個內核oops時,klogd或ksymoops的輸出可能會不可靠。
5.我如何對上述情況進行補救?
方法是將你所有的System.map文件放在目錄/boot下,并使用內核版本號重新對它們進行命名。
5-1.假設你有以下多個內核:
/boot/vmlinuz-2.2.14
/boot/vmlinuz-2.2.13
那么,只需對應各內核版本對map文件進行改名,并放在/boot下,如:
/boot/System.map-2.2.14
/boot/System.map-2.2.13
5-2.如果你有同一個內核的兩個拷貝怎么辦?
例如:
/boot/vmlinuz-2.2.14
/boot/vmlinuz-2.2.14.nosound
最佳解決方案將是所有軟件能夠查找下列文件:
/boot/System.map-2.2.14
/boot/System.map-2.2.14.nosound
但是說實在的,我并不知道這是否是最佳情況。我曾經見到搜尋"System.map-kernelversion",但是對于搜索 "System.map-kernelversion.othertext"的情況呢?我不太清楚。此時我所能做的就是利用這樣一個事實: /usr/src/linux是標準map文件的搜索路徑,所以你的map文件將放在:
/boot/System.map-2.2.14
/usr/src/linux/System.map (對于nosound版本)
你也可以使用符號連接:
System.map-2.2.14
System.map-2.2.14.sound
System.map -> System.map-2.2.14.sound
------------------------------------------------OOPS解釋完畢----------------------------------------------
學習到這里,OOPS和system.map文件,已經有了較深刻的認識。回過頭來繼續對內存屏障的學習。
******************************************************************************
4.www.21icbbs.com上的貼子
為了防止編譯器對有特定時續要求的的硬件操作進行優化,系統提供了相應的辦法:
1,對于由于數據緩沖(比如延時讀寫,CACHE)所引起的問題,可以把相應的I/O區設成禁用緩沖。
2,對于編譯優化,可以用內存屏障來解決。如:void rmb(void),void wmb(void),void mb(void),分別是讀,寫,讀寫 屏障。和void barrier(void).
5.自己分析:
作者查閱了內核注釋如下:
-----------------------------------------------asm-i386/system.h--------------------------------------
內核注釋:
/*
* Force strict CPU ordering.
* And yes, this is required on UP too when we're talking
* to devices.
*
* For now, "wmb()" doesn't actually do anything, as all
* Intel CPU's follow what Intel calls a *Processor Order*,
* in which all writes are seen in the program order even
* outside the CPU.
*
* I expect future Intel CPU's to have a weaker ordering,
* but I'd also expect them to finally get their act together
* and add some real memory barriers if so.
*
* Some non intel clones support out of order store. wmb() ceases to be a
* nop for these.
*/
自己分析認為:
1.Intel CPU 有嚴格的“processor Order”,已經確保內存按序寫,這里的wmb()所以定義的為空操作。
2.內核人員希望Intel CPU今后能采用弱排序技術,采用真正的內存屏障技術。
3.在非intel的cpu上,wmb()就不再為空操作了。
-----------------------------------------內核2.6.14完整的源代碼----------------------------------
下面的源代碼來自于Linux Kernel 2.6.14,開始對其進行一一的全面的分析:
-------------------------------------------/include/asm-i386/system.h----------------------------------
-----------------------------------------------------alternative()-----------------------------------------
/*
* Alternative instructions for different CPU types or capabilities.
*
* This allows to use optimized instructions even on generic binary kernels.
*
* length of oldinstr must be longer or equal the length of newinstr
* It can be padded with nops as needed.
*
* For non barrier like inlines please define new variants
* without volatile and memory clobber.
*/
#define alternative(oldinstr, newinstr, feature) /
asm volatile ("661:/n/t" oldinstr "/n662:/n" /
".section .altinstructions,/"a/"/n" /
" .align 4/n" /
" .long 661b/n" /* label */ /
" .long 663f/n" /* new instruction */ /
" .byte %c0/n" /* feature bit */ /
" .byte 662b-661b/n" /* sourcelen */ /
" .byte 664f-663f/n" /* replacementlen */ /
".previous/n" /
".section .altinstr_replacement,/"ax/"/n" /
"663:/n/t" newinstr "/n664:/n" /* replacement */ /
".previous" :: "i" (feature) : "memory")
自己分析:
1.alternative()宏用于在不同的cpu上優化指令。oldinstr為舊指令,newinstr為新指令,feature為cpu特征位。
2.oldinstr的長度必須>=newinstr的長度。不夠將填充空操作符。
----------------------------------------------------------------------
/*
* Force strict CPU ordering.
* And yes, this is required on UP too when we're talking
* to devices.
*
* For now, "wmb()" doesn't actually do anything, as all
* Intel CPU's follow what Intel calls a *Processor Order*,
* in which all writes are seen in the program order even
* outside the CPU.
*
* I expect future Intel CPU's to have a weaker ordering,
* but I'd also expect them to finally get their act together
* and add some real memory barriers if so.
*
* Some non intel clones support out of order store. wmb() ceases * to be a nop for these.
*/
/*
* Actually only lfence would be needed for mb() because all stores done by the kernel should be already ordered. But keep a full barrier for now.
*/
自己分析:
這里的內核中的注釋,在前面已經作了講解,主要就是intel cpu采用Processor Order,對wmb()保證其的執行順序按照程序順序執行,所以wmb()定義為空操作。如果是對于對于非intel的cpu,這時wmb()就不能再是空操作了。
---------------------------mb()--rmb()--read_barrier_depends()--wmb()------------------
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#define read_barrier_depends() do { } while(0)
#ifdef CONFIG_X86_OOSTORE
/* Actually there are no OOO store capable CPUs for now that do SSE,but make it already an possibility. */
作者附注:(對內核注釋中的名詞的解釋)
-->OOO:Out of Order,亂序執行。
-->SSE:SSE是英特爾提出的即MMX之后新一代(當然是幾年前了)CPU指令集,最早應用在PIII系列CPU上。
本小段內核注釋意即:亂序存儲的cpu還沒有問世,故CONFIG_X86_OOSTORE也就仍未定義的,wmb()當為后面空宏(在__volatile__作用下,阻止編譯器重排順序優化)。
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define wmb() __asm__ __volatile__ ("": : :"memory")
#endif
--------------------------
自己分析:
1.lock, addl $0,0(%%esp)在本文開始處已經解決。
lock前綴表示將后面這句匯編語句:"addl $0,0(%%esp)"作為cpu的一個內存屏障。addl $0,0(%%esp)表示將數值0加到esp寄存器中,而該寄存器指向棧頂的內存單元。加上一個0,esp寄存器的數值依然不變。即這是一條無用的匯編指令。在此利用這條無價值的匯編指令來配合lock指令,用作cpu的內存屏障。
2.mfence保證系統在后面的memory訪問之前,先前的memory訪問都已經結束。這是mfence是X86cpu家族中的新指令。詳見后面。
3.新舊指令對比:
-------------------------------
以前的源代碼:
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
__asm__用于指示編譯器在此插入匯編語句
__volatile__用于告訴編譯器,嚴禁將此處的匯編語句與其它的語句重組合優化。即:原原本本按原來的樣子處理這這里的匯編。
-------------------
現在的源代碼:
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
--------------------------
兩者比較:
比起以前的源代碼來少了__asm__和__volatile__。增加了alternative()宏和mfence指令。
-------------------------
而SFENCE指令(在Pentium III中引入)和LFENCE,MFENCE指令(在Pentium 4和Intel Xeon處理器中引入)提供了某些特殊類型內存操作的排序和串行化功能。sfence,lfence,mfence指令是在后繼的cpu中新出現的的指令。
SFENCE,LFENCE,MFENCE指令提供了高效的方式來保證讀寫內存的排序,這種操作發生在產生弱排序數據的程序和讀取這個數據的程序之間。
SFENCE——串行化發生在SFENCE指令之前的寫操作但是不影響讀操作。
LFENCE——串行化發生在SFENCE指令之前的讀操作但是不影響寫操作。
MFENCE——串行化發生在MFENCE指令之前的讀寫操作。
注意:SFENCE,LFENCE,MFENCE指令提供了比CPUID指令更靈活有效的控制內存排序的方式。
sfence:在sfence指令前的寫操作當必須在sfence指令后的寫操作前完成。
lfence:在lfence指令前的讀操作當必須在lfence指令后的讀操作前完成。
mfence:在mfence指令前的讀寫操作當必須在mfence指令后的讀寫操作前完成。
其實這里是用mfence新指令來替換老的指令串:__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")。
mfence的執行效果就等效于__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")的執行效果。只不過,__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")是在以前的cpu平臺上所設計的,借助于編譯器__asm__,__volatile__,lock這些指令來實現內存屏障。而在 Pentium 4和Intel Xeon處理器中由于已經引入了mfence指令,無須再用這一套指令,直接調用這一條指令即ok。而alternative()宏就是用于這個優化指令的替換,用新的指令來替換老的指令串。
4.intel cpu已保證wmb()的順序完成。wmb()此處定義為空操作。
5.X86_FEATURE_XMM的解釋:
--------------------------------------asm-i386/cpufeature.h----------------------------------------
#define X86_FEATURE_XMM (0*32+25) /* Streaming SIMD Extensions */
************************************************************************
下面對SIMD進行解釋:
--------------《計算機系統結構》--鄭緯民編--清華大學出版社---------
1).指令流:(instruction stream)機器執行的指令序列
2).數據流:(data stream)指令調用的數據序列,包括輸入數據和中間結果。
3)Flynn分類法:
(1)SISD(Single Instrution stream Single Datastream)
單指令流單數據流,對應為傳統的順序處理計算機。
(2)SIMD(Single Instrution stream Multiple Datastream)
單指令流多數據流,對應陣列處理機或并行處理機。
(3)MISD(Multiple Instrution stream Single Datastream)
多指令流單數據流,對應流水線處理機。
(4)MIMD(Multiple Instrution stream Multiple Datastream)
多指令流多數據流,對應多處理機。
*************************************************************************
由于以上幾個指令牽涉到多處理器的管理,要徹底弄懂這些代碼的原理,必須深入挖掘之,既然遇到了,就一口氣吃掉。追根問底,清楚其來龍去脈。
***********************************************************************
----->來自Baidu快照,原網頁打不開了:多處理器管理
說明:作者對此文進行了參考,由于文章太長,太專業化,作者對其進行了改動處理:
------------------------------------------------------------------------------------------------
1.IA-32體系的機制:總線加鎖、cache一致性管理、串行化指令、高級可編程中斷控制器、二級緩存、超線程技術:IA-32體系提供了幾種機制來管理和提升連接到同一系統總線的多個處理器的性能。這些機制包括:
1)總線加鎖、cache一致性管理以實現對系統內存的原子操作、串行化指令(serializing instructions。這些指令僅對pentium4,Intel Xeon, P6,Pentium處理器有效)。
2)處理器芯片內置的高級可編程中斷控制器(APIC)。APIC是在Pentium處理器中被引入IA-32體系的。
3)二級緩存(level 2, L2)。對于Pentium4,Intel Xeon, P6處理器,L2 cache已經緊密的封裝到了處理器中。而Pentium,Intel486提供了用于支持外部L2 cache的管腳。
4)超線程技術。這個技術是IA-32體系的擴展,它能夠讓一個處理器內核并發的執行兩個或兩個以上的指令流。
這些機制在對稱多處理系統(symmetric-multiprocessing, SMP)中是極其有用的。然而,在一個IA-32處理器和一個專用處理器(例如通信,圖形,視頻處理器)共享系統總線的應用中,這些機制也是適用的。
-------------------------
2.多處理機制的設計目標是:
1)保持系統內存的完整性(coherency):
當兩個或多個處理器試圖同時訪問系統內存的同一地址時,必須有某種通信機制或內存訪問協議來提升數據的完整性,以及在某些情況下,允許一個處理器臨時鎖定某個內存區域。
2)保持高速緩存的一致性:
當一個處理器訪問另一個處理器緩存中的數據時,必須要得到正確的數據。如果這個處理器修改了數據,那么所有的訪問這個數據的處理器都要收到被修改后的數據。
3)允許以可預知的順序寫內存:
在某些情況下,從外部觀察到的寫內存順序必須要和編程時指定的寫內存順序相一致。
4)在一組處理器中派發中斷處理:
當幾個處理器正在并行的工作在一個系統中時,有一個集中的機制是必要的,這個機制可以用來接收中斷以及把他們派發到某一個適當的處理器。
5)采用現代操作系統和應用程序都具有的多線程和多進程的特性來提升系統的性能。
---------------------------
根據本文的需要,將重點討論內存加鎖,串行(serializing instructions)指令,內存排序,加鎖的原子操作(locked atomic operations)。
3.系統內存加鎖的原子操作:
32位IA-32處理器支持對系統內存加鎖的原子操作。這些操作常用來管理共享的數據結構(例如信號量,段描述符,系統段頁表)。兩個或多個處理器可能會同時的修改這些數據結構中的同一數據域或標志。
處理器應用三個相互依賴的機制來實現加鎖的原子操作:
1)可靠的原子操作(guaranteed atomic operations)。
2)總線加鎖,使用LOCK#信號和LOCK指令前綴。
3)緩存完整性協議,保證原子操作能夠對緩存中的數據結構執行;這個機制出現在Pentium4,IntelXeon,P6系列處理器中,這些機制以下面的形式相互依賴。
--->某些基本的內存事務(memory transaction)例如讀寫系統內存的一個字節)被保證是原子的。也就是說,一旦開始,處理器會保證這個操作會在另一個處理器或總線代理(bus agent)訪問相同的內存區域之前結束。
--->處理器還支持總線加鎖以實現所選的內存操作(例如在共享內存中的讀-改-寫操作),這些操作需要自動的處理,但又不能以上面的方式處理。因為頻繁使用的內存數據經常被緩存在處理器的L1,L2高速緩存里,原子操作通常是在處理器緩存內部進行的,并不需要聲明總線加鎖。這里的處理器緩存完整性協議保證了在緩沖內存上執行原子操作時其他緩存了相同內存區域的處理器被正確管理。
注意到這些處理加鎖的原子操作的機制已經像IA-32處理器一樣發展的越來越復雜。于是,最近的IA-32處理器(例如Pentium 4, Intel Xeon, P6系列處理器)提供了一種比早期IA-32處理器更為精簡的機制。
------------------------------------------------保證原子操作的情況------------------------------------
4.保證原子操作的情況
Pentium 4, Intel Xeon,P6系列,Pentium,以及Intel486處理器保證下面的基本內存操作總被自動的執行:
1)讀或寫一個字節
2)讀或寫一個在16位邊界對齊的字
3)讀或寫一個在32位邊界對齊的雙字
Pentium 4, Intel Xeon,P6系列以及Pentium處理器還保證下列內存操作總是被自動執行:
1)讀或寫一個在64位邊界對齊的四字(quadword)
2)對32位數據總線可以容納的未緩存的內存位置進行16位方式訪問
(16-bit accesses to uncached memory locations that fit within a 32-bit data bus)
P6系列處理器還保證下列內存操作被自動執行:
對32位緩沖線(cache line)可以容納的緩存中的數據進行非對齊的16位,32位,64位訪問.
對于可以被緩存的但是卻被總線寬度,緩沖線,頁邊界所分割的內存區域,Pentium 4, Intel Xeon, P6 family,Pentium以及Intel486處理器都不保證訪問操作是原子的。Pentium 4, Intel Xeon,P6系列處理器提供了總線控制信號來允許外部的內存子系統完成對分割內存的原子性訪問;但是,對于非對齊內存的訪問會嚴重影響處理器的性能,因此應該盡量避免。
--------------------------------------------------------------總線加鎖------------------------------------------
5.總線加鎖(Bus Locking)
1.Lock信號的作用:
IA-32處理器提供了LOCK#信號。這個信號會在某些內存操作過程中被自動發出。當這個輸出信號發出的時候,來自其他處理器或總線代理的總線控制請求將被阻塞。軟件能夠利用在指令前面添加LOCK前綴來指定在其他情況下的也需要LOCK語義(LOCK semantics)。
在Intel386,Intel486,Pentium處理器中,直接調用加鎖的指令會導致LOCK#信號的產生。硬件的設計者需要保證系統硬件中LOCK#信號的有效性,以控制多個處理對內存的訪問。
--->注意:
對于Pentium 4, Intel Xeon,以及P6系列處理器,如果被訪問的內存區域存在于處理器內部的高速緩存中,那么LOCK#信號通常不被發出;但是處理器的緩存卻要被鎖定。
--------------------------------------------------自動加鎖(Automatic Locking)------- -------------------
6.自動加鎖(Automatic Locking)
1.下面的操作會自動的帶有LOCK語義:
1)執行引用內存的XCHG指令。
2)設置TSS描述符的B(busy忙)標志。在進行任務切換時,處理器檢查并設置TSS描述符的busy標志。為了保證兩個處理器不會同時切換到同一個任務。處理器會在檢查和設置這個標志的時遵循LOCK語義。
3)更新段描述符時。在裝入一個段描述符時,如果段描述符的訪問標志被清除,處理器會設置這個標志。在進行這個操作時,處理器會遵循LOCK語義,因此這個描述符不會在更新時被其他的處理器修改。為了使這個動作能夠有效,更新描述符的操作系統過程應該采用下面的方法:
(1)使用加鎖的操作修改訪問權字節(access-rights byte),來表明這個段描述符已經不存在,同時設置類型變量,表明這個描述符正在被更新。
(2)更新段描述符的內容。這個操作可能需要多個內存訪問;因此不能使用加鎖指令。
(3)使用加鎖操作來修改訪問權字節(access-rights byte),來表明這個段描述符存在并且有效。
注意,Intel386處理器總是更新段描述符的訪問標志,無論這個標志是否被清除。Pentium 4, Intel Xeon,P6系列,Pentium以及Intel486處理器僅在該標志被清除時才設置這個標志。
4)更新頁目錄(page-directory)和頁表(page-table)的條目。在更新頁目錄和頁表的條目時,處理器使用加鎖的周期(locked cycles)來設置訪問標志和臟標志(dirty flag)。
5)響應中斷。發生中斷后,中斷控制器可能會使用數據總線給處理器傳送中斷向量。處理器必須遵循LOCK語義來保證傳送中斷向量時數據總線上沒有其他數據。
-------------------------------------------------軟件控制的總線加鎖----------------------------------------
7.軟件控制的總線加鎖
1)總述:
如果想強制執行LOCK語義,軟件可以在下面的指令前使用LOCK前綴。當LOCK前綴被置于其他的指令之前或者指令沒有對內存進行寫操作(也就是說目標操作數在寄存器中)時,一個非法操作碼(invalid-opcode)異常會被拋出。
2)可以使用LOCK前綴的指令:
1)位測試和修改指令(BTS, BTR, BTC)
2)交換指令(XADD, CMPXCHG, CMPXCHG8B)
3)XCHG指令自動使用LOCK前綴
4)單操作數算術和邏輯指令:INC, DEC, NOT, NEG
5)雙操作數算術和邏輯指令:ADD, ADC, SUB, SBB, AND, OR, XOR
3)注意:
(1)一個加鎖的指令會保證對目標操作數所在的內存區域加鎖,但是系統可能會將鎖定區域解釋得稍大一些。
(2)軟件應該使用相同的地址和操作數長度來訪問信號量(一個用作處理器之間信號傳遞用的共享內存)。例如,如果一個處理器使用一個字來訪問信號量,其他的處理器就不應該使用一個字節來訪問這個信號量。
(3)總線加鎖的完整性不受內存區域對齊的影響。在所有更新操作數的總線周期內,加鎖語義一直持續。但是建議加鎖訪問能夠在自然邊界對齊,這樣可以提升系統性能:
任何邊界的8位訪問(加鎖或不加鎖)
16位邊界的加鎖字訪問。
32位邊界的加鎖雙字訪問。
64位邊界的加鎖四字訪問。
(4)對所有的內存操作和可見的外部事件來說,加鎖的操作是原子的。只有取指令和頁表操作能夠越過加鎖的指令。
(5)加鎖的指令能用于同步數據,這個數據被一個處理器寫而被其他處理器讀。
對于P6系列處理器來說,加鎖的操作使所有未完成的讀寫操作串行化(serialize)(也就是等待它們執行完畢)。這條規則同樣適用于Pentium4和Intel Xeon處理器,但有一個例外:對弱排序的內存類型的讀入操作可能不會被串行化。
加鎖的指令不應該用來保證寫的數據可以作為指令取回。
--------------->自修改代碼(self-modifying code)
(6)加鎖的指令對于Pentium 4, Intel Xeon, P6 family, Pentium, and Intel486處理器,允許寫的數據可以作為指令取回。但是Intel建議需要使用自修改代碼(self-modifying code)的開發者使用另外一種同步機制。
處理自修改和交叉修改代碼(handling self- and cross-modifying code)
處理器將數據寫入當前的代碼段以實現將該數據作為代碼來執行的目的,這個動作稱為自修改代碼。IA-32處理器在執行自修改代碼時采用特定模式的行為,具體依賴于被修改的代碼與當前執行位置之間的距離。由于處理器的體系結構變得越來越復雜,而且可以在引退點(retirement point)之前推測性地執行接下來的代碼(如:P4, Intel Xeon, P6系列處理器),如何判斷應該執行哪段代碼,是修改前地還是修改后的,就變得模糊不清。要想寫出于現在的和將來的IA-32體系相兼容的自修改代碼,必須選擇下面的兩種方式之一:
(方式1)
將代碼作為數據寫入代碼段;
跳轉到新的代碼位置或某個中間位置;
執行新的代碼;
(方式2)
將代碼作為數據寫入代碼段;
執行一條串行化指令;(如:CPUID指令)
執行新的代碼;
(在Pentium或486處理器上運行的程序不需要以上面的方式書寫,但是為了與Pentium 4, Intel Xeon, P6系列處理器兼容,建議采用上面的方式。)
需要注意的是自修改代碼將會比非自修改代碼的運行效率要低。性能損失的程度依賴于修改的頻率以及代碼本身的特性。
--------------->交叉修改代碼(cross-modifying code)
處理器將數據寫入另外一個處理器的代碼段以使得哪個處理器將該數據作為代碼執行,這稱為交叉修改代碼(cross-modifying code)。像自修改代碼一樣,IA-32處理器采用特定模式的行為執行交叉修改代碼,具體依賴于被修改的代碼與當前執行位置之間的距離。要想寫出于現在的和將來的IA-32體系相兼容的自修改代碼,下面的處理器同步算法必須被實現:
;修改的處理器
Memory_Flag ← 0; (* Set Memory_Flag to value other than 1 *)
將代碼作為數據寫入代碼段;
Memory_Flag ← 1;
;執行的處理器
WHILE (Memory_Flag ≠ 1)
等待代碼更新;
ELIHW;
執行串行化指令; (* 例如, CPUID instruction *)
開始執行修改后的代碼;
(在Pentium或486處理器上運行的程序不需要以上面的方式書寫,但是為了與Pentium 4, Intel Xeon, P6系列處理器兼容,建議采用上面的方式。)
像自修改代碼一樣,交叉修改代碼將會比非交叉修改代碼的運行效率要低。性能損失的程度依賴于修改的頻率以及代碼本身的特性。
說明:作者讀到這里時,也是對自修改代碼和交叉修改代碼稍懂一點,再要深入,也備感艱難。
-------------------------------------------------------緩存加鎖--------------------------------------------
8.緩存加鎖
1)加鎖操作對處理器內部緩存的影響:
(1)對于Intel486和Pentium處理器,在進行加鎖操作時,LOCK#信號總是在總線上發出,甚至鎖定的內存區域已經緩存在處理器cache中的時候,LOCK#信號也從總線上發出。
(2)對于Pentium 4, Intel Xeon,P6系列處理器,如果加鎖的內存區域已經緩存在處理器cache中,處理器可能并不對總線發出LOCK#信號,而是僅僅修改cache緩存中的數據,然后依賴cache緩存一致性機制來保證加鎖操作的自動執行。這個操作稱為"緩存加鎖"。緩存一致性機制會自動阻止兩個或多個緩存了同一區域內存的處理器同時修改數據。
-----------------------------------------------訪存排序(memory ordering)-------- ---------------------
9.訪存排序(memory ordering)
(1)編程排序(program ordering):
訪存排序指的是處理器如何安排通過系統總線對系統內存訪問的順序。IA-32體系支持幾種訪存排序模型,具體依賴于體系的實現。例如, Intel386處理器強制執行"編程排序(program ordering)"(又稱為強排序),在任何情況下,訪存的順序與它們出現在代碼流中的順序一致。
(2)處理器排序(processor ordering):
為了允許代碼優化,IA-32體系在Pentium 4, Intel Xeon,P6系列處理器中允許強排序之外的另外一種模型——處理器排序(processor ordering)。這種排序模型允許讀操作越過帶緩存的寫操作來提升性能。這個模型的目標是在多處理器系統中,在保持內存一致性的前提下,提高指令執行速度。
-----------------------------
10.Pentium和Intel 486處理器的訪存排序:
1)普遍情況:
Pentium和Intel 486處理器遵循處理器排序訪存模型;但是,在大多數情況下,訪存操作還是強排序,讀寫操作都是以編程時指定的順序出現在系統總線上。除了在下面的情況時,未命中的讀操作可以越過帶緩沖的寫操作:
--->當所有的帶緩沖的寫操作都在cache緩存中命中,因此也就不會與未命中的讀操作訪問相同的內存地址。
2)I/O操作訪存:
在執行I/O操作時,讀操作和寫操作總是以編程時指定的順序執行。在"處理器排序"處理器(例如,Pentium 4, Intel Xeon,P6系列處理器)上運行的軟件不能依賴Pentium或Intel486處理器的強排序。軟件應該保證對共享變量的訪問能夠遵守編程順序,這種編程順序是通過使用加鎖或序列化指令來完成的。
3)Pentium 4, Intel Xeon, P6系列處理器的訪存排序
Pentium 4, Intel Xeon, P6系列處理器也是使用"處理器排序"的訪存模型,這種模型可以被進一步定義為"帶有存儲緩沖轉發的寫排序"(write ordered with store-buffer forwarding)。這種模型有下面的特點:
---------單處理器系統中的排序規則
(1)在一個單處理器系統中,對于定義為回寫可緩沖(write-back cacheable)的內存區域,下面的排序規則將被應用:
a.讀能夠被任意順序執行。
b.讀可以越過緩沖寫,但是處理器必須保證數據完整性(self-consistent)。
c.對內存的寫操作總是以編程順序執行,除非寫操作執行了CLFUSH指令以及利用非瞬時的移動指令(MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, MOVNTPD)來執行流存儲操作(streamint stores)。
作者認為:CLFUSH--->CFLUSH,streamint--->streaming???是否原文有誤。
d.寫可以被緩沖。寫不能夠預先執行;它們只能等到其他指令執行完畢。
e.在處理器中,來自于緩沖寫的數據可以直接被發送到正在等待的讀操作。
f.讀寫操作都不能跨越I/O指令,加鎖指令,或者序列化指令。
g.讀操作不能越過LFENCE和MFENCE指令。
h.`寫操作不能越過SFECE和MFENCE指令。
第二條規則(b)允許一個讀操作越過寫操作。然而如果寫操作和讀操作都是訪問同一個內存區域,那么處理器內部的監視機制將會檢測到沖突并且在處理器使用錯誤的數據執行指令之前更新已經緩存的讀操作。
第六條規則(f)構成了一個例外,否則整個模型就是一個寫排序模型(write ordered model)。
注意"帶有存儲緩沖轉發的寫排序"(在本節開始的時候介紹)指的是第2條規則和第6條規則的組合之后產生的效果。
---------------多處理器系統中的排序規則
(2)在一個多處理器系統中,下面的排序規則將被應用:
a.每個處理器使用同單處理器系統一樣的排序規則。
b.所有處理器所觀察到的某個處理器的寫操作順序是相同的。
c.每個處理器的寫操作并不與其它處理器之間進行排序。
例如:在一個三處理器的系統中,每個處理器執行三個寫操作,分別對三個地址A, B,C。每個處理器以編程的順序執行操作,但是由于總線仲裁和其他的內存訪問機制,三個處理器執行寫操作的順序可能每次都不相同。最終的A, B, C的值會因每次執行的順序而改變。
-------------------
(3)本節介紹的處理器排序模型與Pentium Intel486處理器使用的模型是一樣的。唯一在Pentium 4, Intel Xeon,P6系列處理器中得到加強的是:
a.對于預先執行讀操作的支持。
b.存儲緩沖轉發,當一個讀操作越過一個訪問相同地址的寫操作。
c.對于長串的存儲和移動的無次序操作(out-of-Order Stores)Pentium 4,
--------------------
(4)快速串:
Intel Xeon, P6處理器對于串操作的無次序存儲(Out-of-Order Stores)
Pentium 4, Intel
Xeon,P6處理器在進行串存儲的操作(以MOVS和STOS指令開始)時,修改了處理器的動作,以提升處理性能。一旦"快速串"的條件滿足了 (將在下面介紹),處理器將會在緩沖線(cache line)上以緩沖線模式進行操作。這會導致處理器在循環過程中發出對源地址的緩沖線讀請求,以及在外部總線上發出對目標地址的寫請求,并且已知了目標地址內的數據串一定要被修改。在這種模式下,處理器僅僅在緩沖線邊界時才會相應中斷。因此,目標數據的失效和存儲可能會以不規則的順序出現在外部總線上。
按順序存儲串的代碼不應該使用串操作指令。數據和信號量應該分開。依賴順序的代碼應該在每次串操作時使用信號量來保證存儲數據的順序在所有處理器看來是一致的。
"快速串"的初始條件是:
在Pentium III 處理器中,EDI和ESI必須是8位對齊的。在Pentium4中,EDI必須是8位對齊的。
串操作必須是按地址增加的方向進行的。
初始操作計數器(ECX)必須大于等于64。
源和目的內存的重合區域一定不能小于一個緩沖線的大小(Pentium 4和Intel Xeon 處理器是64字節;P6 和Pentium處理器是 32字節)。
源地址和目的地址的內存類型必須是WB或WC。
----------------
11.加強和削弱訪存排序模型(Strengthening or Weakening the Memory Ordering Model)
IA-32體系提供了幾種機制用來加強和削弱訪存排序模型以處理特殊的編程場合。這些機制包括:
1)I/O指令,加鎖指令,LOCK前綴,以及序列化指令來強制執行"強排序"。
2)SFENCE指令(在Pentium III中引入)和LFENCE,MFENCE指令(在Pentium 4和Intel Xeon處理器中引入)提供了某些特殊類型內存操作的排序和串行化功能。
3)內存類型范圍寄存器(memory type range registers (MTRRs))可以被用來加強和削弱物理內存中特定區域的訪存排序模型。MTRRs只存在于Pentium 4, Intel Xeon, P6系列處理器。
4)頁屬性表可以被用來加強某個頁或一組頁的訪存排序("頁屬性表"Page Attribute Table(PAT))。PAT只存在于Pentium 4, Intel Xeon,P6系列處理器。
這些機制可以通過下面的方式使用:
1)內存映射和其他I/O設備通常對緩沖區寫操作的順序很敏感。I/O指令(IN,OUT)以下面的方式對這種訪問執行強排序。在執行一條I/O 指令之前,處理器等待之前的所有指令執行完畢以及所有的緩沖區都被寫入了內存。只有取指令操作和頁表查詢(page table walk)能夠越過I/O指令。后續指令要等到I/O指令執行完畢才開始執行。
2)一個多處理器的系統中的同步機制可能會依賴"強排序"模型。這里,一個程序使用加鎖指令,例如XCHG或者LOCK前綴,來保證讀-改-寫操作是自動進行的。加鎖操作像I/O指令一樣等待所有之前的指令執行完畢以及緩沖區都被寫入了內存。
3)程序同步可以通過序列化指令來實現。這些指令通常用于臨界過程或者任務邊界來保證之前所有的指令在跳轉到新的代碼區或上下文切換之前執行完畢。像I/O加鎖指令一樣,處理器等待之前所有的指令執行完畢以及所有的緩沖區寫入內存后才開始執行序列化指令。
4)SFENCE,LFENCE,MFENCE指令提供了高效的方式來保證讀寫內存的排序,這種操作發生在產生弱排序數據的程序和讀取這個數據的程序之間。
SFENCE——串行化發生在SFENCE指令之前的寫操作但是不影響讀操作。
LFENCE——串行化發生在SFENCE指令之前的讀操作但是不影響寫操作。
MFENCE——串行化發生在MFENCE指令之前的讀寫操作。
注意:SFENCE,LFENCE,MFENCE指令提供了比CPUID指令更靈活有效的控制內存排序的方式。
5)MTRRs在P6系列處理器中引入,用來定義物理內存的特定區域的高速緩存特性。下面的兩個例子是利用MTRRs設置的內存類型如何來加強和削弱Pentium 4, Intel Xeon, P6系列處理器的訪存排序:
(1)強不可緩沖(strong uncached,UC)內存類型實行內存訪問的強排序模型:
這里,所有對UC內存區域的讀寫都出現在總線上,并且不能夠被亂序或預先執行。這種內存類型可以應用于映射成I/O設備的內存區域來強制執行訪存強排序。
(2)對于可以容忍弱排序訪問的內存區域,可以選擇回寫(write back, WB)內存類型:
這里,讀操作可以預先的被執行,寫操作可以被緩沖和組合(combined)。對于這種類型的內存,鎖定高速緩存是通過一個加鎖的原子操作實現的,這個操作不會分割緩沖線,因此會減少典型的同步指令(如,XCHG在整個讀-改-寫操作周期要鎖定數據總線)所帶來的性能損失。對于WB內存,如果訪問的數據已經存在于緩存cache中,XCHG指令會鎖定高速緩存而不是數據總線。
(3)PAT在Pentium III中引入,用來增強用于存儲內存頁的緩存性能。PAT機制通常被用來與MTRRs一起來加強頁級別的高速緩存性能。在Pentium 4, Intel Xeon,P6系列處理器上運行的軟件最好假定是 "處理器排序"模型或者是更弱的訪存排序模型。
Pentium 4, Intel Xeon,P6系列處理器沒有實現強訪存排序模型,除了對于UC內存類型。盡管Pentium 4, Intel Xeon,P6系列處理器支持處理器排序模型,Intel并沒有保證將來的處理器會支持這種模型。為了使軟件兼容將來的處理器,操作系統最好提供臨界區 (critical region)和資源控制構建以及基于I/O,加鎖,序列化指令的API,用于同步多處理器系統對共享內存區的訪問。同時,軟件不應該依賴處理器排序模型,因為也許系統硬件不支持這種訪存模型。
(4)向多個處理器廣播頁表和頁目錄條目的改變:
在一個多處理器系統中,當一個處理器改變了一個頁表或頁目錄的條目,這個改變必須要通知所有其它的處理器。這個過程通常稱為"TLB shootdown"。廣播頁表或頁目錄條目的改變可以通過基于內存的信號量或者處理器間中斷(interprocessor interrupts, IPI)。
例如一個簡單的,但是算法上是正確的TLB shootdown序列可能是下面的樣子:
a.開始屏障(begin barrier)——除了一個處理器外停止所有處理器;讓他們執行HALT指令或者空循環。
b.讓那個沒有停止的處理器改變PTE or PDE。
c.讓所有處理器在他們各自TLB中修改的PTE, PDE失效。
d.結束屏障(end barrier)——恢復所有的處理器執行。
(5)串行化指令(serializing instructions):
IA-32體系定義了幾個串行化指令(SERIALIZING INSTRUCTIONS)。這些指令強制處理器完成先前指令對標志,寄存器以及內存的修改,并且在執行下一條指令之前將所有緩沖區里的數據寫入內存。
===>串行化指令應用一:開啟保護模式時的應用
例如:當MOV指令將一個操作數裝入CR0寄存器以開啟保護模式時,處理器必須在進入保護模式之前執行一個串行化操作。這個串行化操作保證所有在實地址模式下開始執行的指令在切換到保護模式之前都執行完畢。
-------------
串行化指令的概念在Pentium處理器中被引入IA-32體系。這種指令對于Intel486或更早的處理器是沒有意義的,因為它們并沒有實現并行指令執行。
非常值得注意的是,在Pentium 4, Intel Xeon,P6系列處理器上執行串行化指令會抑制指令的預執行(speculative execution),因為預執行的結果會被放棄掉。
-------------
下面的指令是串行化指令:
1.--->特權串行化指令——MOV(目標操作數為控制寄存器),MOV(目標操作數為調試存器),WRMSR, INVD, INVLPG, WBINVD, LGDT, LLDT, LIDT, LTR。
-------------------------作者補充------------------------------
作者:如果上述指令不熟,可以參考《80X86匯編語言程序設計教程》楊季文編,清華大學出版社。下面作些簡單的介紹:以下作者對匯編指令的說明均參考引用了該書。
---->INVLPG指令:
使TLB(轉換后援緩沖器:用于存放最常使用的物理頁的頁碼)項無效。該指令是特權指令,只有在實方式和保護方式的特權級0下,才可執行該指令。
---------------------------------------------------------------
2.--->非特權串行化指令——CPUID, IRET, RSM。
3.--->非特權訪存排序指令——SFENCE, LFENCE, MFENCE。
當處理器執行串行化指令的時候,它保證在執行下一條指令之前,所有未完成的內存事務都被完成,包括寫緩沖中的數據。任何指令不能越過串行化指令,串行化指令也不能越過其他指令(讀,寫, 取指令, I/O)。
CPUID指令可以在任何特權級下執行串行化操作而不影響程序執行流(program flow),除非EAX, EBX, ECX, EDX寄存器被修改了。
SFENCE,LFENCE,MFENCE指令為控制串行化讀寫內存提供了更多的粒度。
在使用串行化指令時,最好注意下面的額外信息:
處理器在執行串行化指令的時候并不將高速緩存中已經被修改的數據寫回到內存中。軟件可以通過WBINVD串行化指令強制修改的數據寫回到內存中。但是頻繁的使用WVINVD(作者注:當為WBINVD,原文此處有誤)指令會嚴重的降低系統的性能。
----------------作者補充:對WBINVAD的解釋-----------------------
----->INVD指令:
INVD指令使片上的高速緩存無效,即:清洗片上的超高速緩存。但該指令并不把片上的超高速緩存中的內容寫回主存。該指令是特權指令,只有在實方式和保護方式的特權級0下,才可執行該指令。
---->WBINVD指令:
WBINVD指令使片上的超高速緩存無效即:清洗片上的超高速緩存。但該指令將把片上的超高速緩存中更改的內容寫回主存。該指令是特權指令,只有在實方式和保護方式的特權級0下,才可執行該指令。
****************************************************************
===>串行化指令應用二:改變了控制寄存器CR0的PG標志的應用
當一條會影響分頁設置(也就是改變了控制寄存器CR0的PG標志)的指令執行時,這條指令后面應該是一條跳轉指令。跳轉目標應該以新的PG標志 (開啟或關閉分頁)來進行取指令操作,但跳轉指令本身還是按先前的設置執行。Pentium 4, Intel Xeon,P6系列處理器不需要在設置CR0處理器之后放置跳轉指令(因為任何對CR0進行操作的MOV指令都是串行化的)。但是為了與其他IA-32處理器向前和向后兼容,最好是放置一條跳轉指令。
=========
作者說明:CR0的第31位為PG標志,PG=1:啟用分頁管理機制,此時線性地址經過分頁管理機制后轉換為物理地址;PG=0:禁用分頁管理機制,此時線性地址直接作為物理地址使用。
****************************************************************
在允許分頁的情況下,當一條指令會改變CR3的內容時,下一條指令會根據新的CR3內容所設置的轉換表進行取指令操作。因此下一條以及之后的指令應該根據新的CR3內容建立映射。
=========
作者說明:CR3用于保存頁目錄表的起始物理地址,由于目錄表是責對齊的,所以僅高20位有效,低12位無效。所以如果向CR3中裝入新值,其低 12位當為0;每當用mov指令重置CR3的值時候,TLB中的內容會無效。CR3在實方式下也可以設置,以使分頁機制初始化。在任務切換時候,CR3要被改變。但要是新任務的CR3的值==舊任務的CR3的值,則TLB的內容仍有效,不被刷新。
******************************************************************************
以上通過這篇文章資料對cpu的工作機制有了更深刻的了解,從而對我們的Linux Kernel的學習有極大的幫助。由此對加鎖,各類排序,串行化,sfence,mfence,lfence指令的出現有了清楚的認識。再回頭來讀讀源代碼有更深刻的認識。
*****************************************************************************
------------------------------------------smp_mb()---smp_rmb()---smp_wmb()-------------------------
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif
#define set_wmb(var, value) do { var = value; wmb(); } while (0)
-----------------------------------------------/linux/compiler-gcc.h--------------------------------------
------------------------------------------------------barrier()-------------------------------------------------
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
自己分析:
1.如果定義的了CONFIG_SMP,也就是系統為對稱多處理器系統。smp_mb(),smp_rmb(),smp_wmb()就是mb(),rmb(),wmb()。
由此可見,多處理器上的內存屏障與單處理器原理一樣。
2.barrier()函數并無什么難點,與前面代碼一樣。
3.如果沒有定義CONFIG_SMP,則smp_mb(), smp_rmb(), smp_wmb(), smp_read_barrier_depends( 都是空宏。
**************************************************************************
在本文的代碼中有不少下劃線的關鍵字,特此作一研究:
--------------------------------------------------------雙下劃線的解釋--------------------------------------
--->摘自gcc手冊
Alternate Keywords ‘-ansi’ and the various ‘-std’ options disable certain keywords。 This causes trouble when you want to use GNU C extensions, or a general-purpose header file that should be usable by all programs, including ISO C programs。 The keywords asm, typeof and inline are not available in programs compiled with ‘-ansi’ or ‘-std’ (although inline can be used in a program compiled with ‘-std=c99’)。 The ISO C99 keyword restrict is only available when ‘-std=gnu99’ (which will eventually be the default) or ‘-std=c99’ (or the equivalent ‘-std=iso9899:1999’) is used。The way to solve these problems is to put ‘__’ at the beginning and end of each problematical keyword。 For example, use __asm__ instead of asm, and __inline__ instead of inline。
Other C compilers won’t accept these alternative keywords; if you want to compile with another compiler, you can define the alternate keywords as macros to replace them with the customary keywords。 It looks like this:
#ifndef __GNUC__
#define __asm__ asm
#endif
‘-pedantic’(pedantic選項解釋見下面) and other options cause warnings for many GNU C extensions。 You can prevent such warnings within one expression by writing __extension__ before the expression。__extension__ has no effect aside from this。
自己分析:
1。我們在程序中使用了很多的gnu風格,也就是GNU C extensions 或其他的通用的頭文件。但是如果程序用'-ansi'或各種'-std'選項編譯時候,一些關鍵字,比如:asm、typeof、inline就不能再用了,在這個編譯選項下,這此關鍵字被關閉。所以用有雙下劃線的關鍵字,如:__asm__、__typeof__、__inline__,這些編譯器通常支持這些帶有雙下劃線的宏。這能替換這些會產生編譯問題的關鍵字,使程序能正常通過編譯。
2。如果是用其他的編譯器,可能不認這些帶有雙下劃線的宏,就用以下宏來轉換:
#ifndef __GNUC__
#define __asm__ asm
#endif
這樣的話,這些其他的編譯器沒有定義__GUNUC__,也不支持__asm__,__inline__,__typeof__等宏,所以必會,執行#define __asm__ asm等。這樣,用__asm__,__inline__,__typeof__所編寫的程序代碼,仍能宏展開為asm,inline,typeof,而這此關鍵字這些其他的編譯器支持。所以程序能正常編譯。
-----------------------------------------------pedantic選項的解釋----------------------------------
--->摘自gcc手冊Download from www。gnu。org
Issue all the warnings demanded by strict ISO C and ISO C++; reject all programs that use forbidden extensions, and some other programs that do not follow ISO C and ISO C++。 For ISO C, follows the version of the ISO C standard specified by any ‘-std’ option used。 Valid ISO C and ISO C++ programs should compile properly with or without this option (though a rare few will require ‘-ansi’ or a ‘-std’ option specifying the required version of ISO C)。 However, without this option, certain GNU extensions and traditional C and C++ features are supported as well。 With this
option, they are rejected。 ‘-pedantic’ does not cause warning messages for use of the alternate keywords whose names begin and end with ‘__’。 Pedantic warnings are also disabled in the expression that follows __extension__。 However, only system header files should use these escape routes; application programs should avoid them。 See Section 5。38 [Alternate Keywords], page 271。
Some users try to use ‘-pedantic’ to check programs for strict ISO C conformance。They soon find that it does not do quite what they want: it finds some non-ISO practices, but not all—only those for which ISO C requires a diagnostic, and some others for which diagnostics have been added。 A feature to report any failure to conform to ISO C might be useful in some instances, but would require considerable additional work and would be quite different from ‘-pedantic’。 We don’t have plans to support such a feature in the near future。
-----------------------------------------------------------------------------------------------------------------