一直以來。I/O順序問題一直困擾着我。事實上這個問題是一個比較綜合的問題,它涉及的層次比較多,從VFS page cache到I/O調度算法,從i/o子系統到存儲外設。而Linux I/O barrier就是當中重要的一部分。php
可能很是多人以爲,在作了文件寫操做後,調用fsycn就能保證數據可靠地寫入磁盤。大多數狀況下,確實如此。html
但是,因爲緩存的存在。fsycn這些同步操做。並不能保證存儲設備把數據寫入非易失性介質。linux
假設此時存儲設備發生掉電或者硬件錯誤。此時存儲緩存中的數據將會丟失。這對於像日誌文件系統中的日誌這種數據。其後果多是很是嚴重的。因爲日誌文件系統中,數據的寫入和日誌的寫入存在前後順序。假設順序發生錯亂,則可能破壞文件系統。所以必須要有一種方式,來知道寫入的數據是否真的被寫入到外部存儲的非易失性介質,比便文件系統依據寫入狀況來進行下一步的操做。假設把fsycn理解成OS級別同步的話,那麼對於Barrier I/O,個人理解就是硬件級別的同步。詳細Linux Barrier I/O的介紹,參考」Linux Barrier I/O」。本文主要分析Linux Barrier I/O的實現以及其它塊設備驅動對它的影響。ios
Barrier I/O的目的是使其以前的I/O在其以前寫入存儲介質,以後的I/O需要等到其寫入完畢後才幹獲得運行。nginx
爲了實現這個要求。咱們最多需要運行2次flush(刷新)操做。web
(注意。這兒所說的flush,指的是刷新存儲設備的緩存。但並不是所有存儲設備都支持flush操做,因此不是所有設備都支持barrier I/O。支持依據這個要求,需要在初始化磁盤設備的請求隊列時,顯式的代表該設備支持barrier I/O的類型並實現prepare flush 方法,參見」Linux Barrier I/O」。算法
)第一次flush是把barrier I/O以前的所有數據刷新。當刷新成功,也就是這些數據被存儲設備告知確實寫入其介質後,提交Barrier I/O所在的請求。而後運行第二次刷新,此次刷新的是Barrier I/O所攜帶的數據。shell
固然,假設Barrier I/O沒有攜帶不論什麼數據,則第二次刷新可以省略。此外。假設存儲設備支持FUA。則可以在提交Barrier I/O所攜帶的數據時,使用FUA命令。這樣可以直接知道Barrier I/O所攜帶的數據是否寫入成功,從而省略掉第二次刷新。express
經過對Barrier I/O的處理過程。咱們可以看到,當中最核心的是兩次刷新操做和中間的Barrier I/O。爲了表示這兩次刷新操做以及該Barrier I/O,在Linux Barrier I/O的實現中。引入了3個輔助request: pre_flush_rq, bar_rq, post_flush_rq. 它們包括在磁盤設備的request_queue中。每當通用塊層接收到上面發下來的Barrier I/O請求,就會把該請求複製到bar_rq,並把這3個請求依次增長請求隊列,造成flush->barrier->flush請求序列。這樣,在處理請求時,便能實現barrier I/O所要求的功能。apache
固然,並不是所有設備都必須使用以上序列中的所有操做。詳細要使用那些操做,是有設備自身特色決定的。爲了標示設備所需要採取的操做序列,Linux Barrier I/O中定義了下面標誌:
QUEUE_ORDERED_BY_DRAIN = 0x01,
QUEUE_ORDERED_BY_TAG = 0x02,
QUEUE_ORDERED_DO_PREFLUSH = 0x10,
QUEUE_ORDERED_DO_BAR = 0x20,
QUEUE_ORDERED_DO_POSTFLUSH = 0x40,
QUEUE_ORDERED_DO_FUA = 0x80,
QUEUE_ORDERED_NONE = 0x00,
QUEUE_ORDERED_DRAIN = QUEUE_ORDERED_BY_DRAIN |
QUEUE_ORDERED_DO_BAR,
QUEUE_ORDERED_DRAIN_FLUSH = QUEUE_ORDERED_DRAIN |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_POSTFLUSH,
QUEUE_ORDERED_DRAIN_FUA = QUEUE_ORDERED_DRAIN |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_FUA,
QUEUE_ORDERED_TAG = QUEUE_ORDERED_BY_TAG |
QUEUE_ORDERED_DO_BAR,
QUEUE_ORDERED_TAG_FLUSH = QUEUE_ORDERED_TAG |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_POSTFLUSH,
QUEUE_ORDERED_TAG_FUA = QUEUE_ORDERED_TAG |
QUEUE_ORDERED_DO_PREFLUSH |
QUEUE_ORDERED_DO_FUA,
不一樣的標誌決定了不一樣的操做序列。此外。爲了標示操做序列的運行狀態。Linux Barrier I/O中定義了下面標誌,它們表示了處理Barrier I/O過程當中,運行操做序列的狀態:
QUEUE_ORDSEQ_STARTED = 0x01, /* flushing in progress */
QUEUE_ORDSEQ_DRAIN = 0x02, /* waiting for the queue to be drained */
QUEUE_ORDSEQ_PREFLUSH = 0x04, /* pre-flushing in progress */
QUEUE_ORDSEQ_BAR = 0x08, /* original barrier req in progress */
QUEUE_ORDSEQ_POSTFLUSH = 0x10, /* post-flushing in progress */
QUEUE_ORDSEQ_DONE = 0x20,
整個Barrier I/O的處理流程,就是依據操做序列標誌肯定操做序列。而後運行操做序列並維護其狀態的過程。如下將詳細分析其代碼實現。
1.提交Barrier I/O
提交Barrier I/O最直接的方法是設置該i/o的標誌爲barrier。當中主要有兩個標誌:WRITE_BARRIER和BIO_RW_BARRIER。WRITE_BARRIER定義在fs.h。其定義爲:#define WRITE_BARRIER ((1 << BIO_RW) | (1 << BIO_RW_BARRIER)),而BIO_RW_BARRIER定義在bio.h。這兩個標誌都可以直接做用於bio。
此外,在更上一層,如buffer_header,它有個BH_Ordered位,假設該位設置,並且爲去寫方式爲WRITE,則在submit_bh中會初始化bio的標誌爲WRITE_BARRIER。當中,在buffer_head.h中定義了操做BH_Ordered位的函數:set_buffer_ordered。buffer_ordered。
if (buffer_ordered(bh) && (rw & WRITE))
rw |= WRITE_BARRIER;
帶有barrier i/o標誌的bio經過submit_bio提交後。則需要爲其生成request。在生成request的過程當中。會依據該barrier i/o來設置request的一些標誌。比方在__make_request->init_request_from_bio中有:
if (unlikely(bio_barrier(bio)))
req->cmd_flags |= (REQ_HARDBARRIER | REQ_NOMERGE);
這兩個標誌告訴elevator。該request包括barrier i/o。不需要合併。
所以內核elevator在增長該request的時候會對其作專門的處理。
2.barrier request增長elevator調度隊列
咱們把包括barrier i/o的request叫作barrier request。Barrier request不一樣於通常的request,所以在將其增長elevator調度隊列時。需要作專門處理。
void __elv_add_request(struct request_queue *q, struct request *rq, int where,
int plug)
{
if (q->ordcolor)
rq->cmd_flags |= REQ_ORDERED_COLOR;
if (rq->cmd_flags & (REQ_SOFTBARRIER | REQ_HARDBARRIER)) {
/*
* toggle ordered color
*/
if (blk_barrier_rq(rq))
q->ordcolor ^= 1;
/*
* barriers implicitly indicate back insertion
*/
if (where == ELEVATOR_INSERT_SORT)
where = ELEVATOR_INSERT_BACK;
/*
* this request is scheduling boundary, update
* end_sector
*/
if (blk_fs_request(rq)) {
q->end_sector = rq_end_sector(rq);
q->boundary_rq = rq;
}
} else if (!(rq->cmd_flags & REQ_ELVPRIV) &&
where == ELEVATOR_INSERT_SORT)
where = ELEVATOR_INSERT_BACK;
…
elv_insert(q, rq, where);
}
爲了標明調度隊列中兩個barrier request的界限。request引入了order color。
經過這兩句話,把兩個barrier request以前的request「填上」不一樣的顏色:
if (q->ordcolor)
rq->cmd_flags |= REQ_ORDERED_COLOR;
if (blk_barrier_rq(rq))
q->ordcolor ^= 1;
比方:
Rq1 re2 barrrier1 req3 req4 barrier2
0 0 0 1 1 1
因爲以後,在處理barrier request時。會爲其生成一個request序列,當中可能包括3個request。經過着色,可以區分不一樣barrier request的處理序列。
另外,對barrier request的特殊處理就是設置其插入elevator調度隊列的方向爲ELEVATOR_INSERT_BACK。
一般,咱們插入調度隊列的方向是ELEVATOR_INSERT_SORT。其含義是依照request所含的數據塊的盤塊順序插入。在elevator調度算法中,每每會插入盤塊順序的紅黑樹中,如deadline調度算法。以後調度算法在往設備請求隊列中分發request的時候,大體會依照這個順序分發(有可能發生回掃。飢餓。操做等)。
所以這這樣的插入方式不適合barrier request。Barrier request必須插到所有request的最後。這樣,才幹把以前的request 都」flush」下去。
選擇好插入方向後,如下就是調用elv_insert來詳細插入一個barrier request:
void elv_insert(struct request_queue *q, struct request *rq, int where)
{
rq->q = q;
switch (where) {
case ELEVATOR_INSERT_FRONT:
rq->cmd_flags |= REQ_SOFTBARRIER;
list_add(&rq->queuelist, &q->queue_head);
break;
case ELEVATOR_INSERT_BACK:
rq->cmd_flags |= REQ_SOFTBARRIER;
elv_drain_elevator(q);
list_add_tail(&rq->queuelist, &q->queue_head);
/*
* We kick the queue here for the following reasons.
* - The elevator might have returned NULL previously
* to delay requests and returned them now. As the
* queue wasn't empty before this request, ll_rw_blk
* won't run the queue on return, resulting in hang.
* - Usually, back inserted requests won't be merged
* with anything. There's no point in delaying queue
* processing.
*/
blk_remove_plug(q);
q->request_fn(q);
break;
case ELEVATOR_INSERT_SORT:
…
case ELEVATOR_INSERT_REQUEUE:
/*
* If ordered flush isn't in progress, we do front
* insertion; otherwise, requests should be requeued
* in ordseq order.
*/
rq->cmd_flags |= REQ_SOFTBARRIER;
/*
* Most requeues happen because of a busy condition,
* don't force unplug of the queue for that case.
*/
if (q->ordseq == 0) {
list_add(&rq->queuelist, &q->queue_head);
break;
}
ordseq = blk_ordered_req_seq(rq);
list_for_each(pos, &q->queue_head) {
struct request *pos_rq = list_entry_rq(pos);
if (ordseq <= blk_ordered_req_seq(pos_rq))
break;
}
list_add_tail(&rq->queuelist, pos);
break;
..
}
if (unplug_it && blk_queue_plugged(q)) {
int nrq = q->rq.count[READ] + q->rq.count[WRITE]
- q->in_flight;
if (nrq >= q->unplug_thresh)
__generic_unplug_device(q);
}
}
前面分析了,正常狀況下。barrier request的插入方向是ELEVATOR_INSERT_BACK。在把barrier request增長設備請求隊列末尾以前,需要調用elv_drain_elevator把調度算法中的請求隊列中的request都「排入」設備的請求隊列。注意,elv_drain_elevator調用的是while (q->elevator->ops->elevator_dispatch_fn(q, 1));。它設置了force dispatch,所以elevator調度算法必須強制把所有緩存在本身調度隊列中的request都分發到設備的請求隊列。比方AS調度算法,假設在force dispatch狀況下,它會終止預測和batching。這樣。當前barrier request一定是插入設備請求隊列的最後一個request。
否則,假設AS可能出於預測狀態,它可能延遲request的處理,即緩存在調度算法隊列中的request排不乾淨。現在。barrier request和以前的request都到了設備的請求隊列,如下就是調用設備請求隊列的request_fn來處理每個請求。(blk_remove_plug(q)以前的注視不是很是明確,需要進一步分析)
上面是request barrier正常的插入狀況。但是,假設在barrier request的處理序列中,某個request可能出現錯誤。比方設備繁忙。沒法完畢flush操做。這個時候,經過錯誤處理。該barrier request處理序列中的request需要requeue: ELEVATOR_INSERT_REQUEUE。
因爲一個barrier request的處理序列存在preflush,barrier,postflush這個順序,因此噹噹中一個request發生requeue時,需要考慮barrier request處理序列當前的順序。看到底運行到了哪一步了。而後依據當前的序列,查找對應的request並增長隊尾。
3.處理barrier request
現在,barrier request以及其前面的request都被排入了設備請求隊列。處理過程詳細是在request_fn中,調用__elv_next_request來進行處理的。
static inline struct request *__elv_next_request(struct request_queue *q)
{
struct request *rq;
while (1) {
while (!list_empty(&q->queue_head)) {
rq = list_entry_rq(q->queue_head.next);
if (blk_do_ordered(q, &rq))
return rq;
}
if (!q->elevator->ops->elevator_dispatch_fn(q, 0))
return NULL;
}
}
__elv_next_request每次取出設備請求隊列中隊首request。並進行blk_do_ordered操做。
該函數詳細處理barrier request。blk_do_ordered的第二個參數爲輸入輸出參數。它表示下一個要運行的request。
Request從設備隊列頭部取出。交由blk_do_ordered推斷,假設是通常的request。則該request會被直接返回給設備驅動,進行處理。假設是barrier request,則該request會被保存,rq會被替換成barrier request運行序列中對應的request,從而開始運行barrier request的序列。
int blk_do_ordered(struct request_queue *q, struct request **rqp)
{
struct request *rq = *rqp;
const int is_barrier = blk_fs_request(rq) && blk_barrier_rq(rq);
if (!q->ordseq) {
if (!is_barrier)
return 1;
if (q->next_ordered != QUEUE_ORDERED_NONE) {
*rqp = start_ordered(q, rq);
return 1;
} else {
…
}
}
/*
* Ordered sequence in progress
*/
/* Special requests are not subject to ordering rules. */
if (!blk_fs_request(rq) &&
rq != &q->pre_flush_rq && rq != &q->post_flush_rq)
return 1;
if (q->ordered & QUEUE_ORDERED_TAG) {
/* Ordered by tag. Blocking the next barrier is enough. */
if (is_barrier && rq != &q->bar_rq)//the next barrier i/o
*rqp = NULL;
} else {
/* Ordered by draining. Wait for turn. */
WARN_ON(blk_ordered_req_seq(rq) < blk_ordered_cur_seq(q));
if (blk_ordered_req_seq(rq) > blk_ordered_cur_seq(q))
*rqp = NULL;
}
return 1;
}
blk_do_ordered分爲兩部分。首先,假設barrier request請求序列還沒開始。也就是尚未開始處理barrier request。則調用start_ordered來初始化barrier request處理序列。此外,假設當前正處於處理序列中。則依據處理序列的階段來處理當前request。
初始化處理序列start_ordered
static inline struct request *start_ordered(struct request_queue *q,
struct request *rq)
{
q->ordered = q->next_ordered;
q->ordseq |= QUEUE_ORDSEQ_STARTED;
/*
* Prep proxy barrier request.
*/
blkdev_dequeue_request(rq);
q->orig_bar_rq = rq;
rq = &q->bar_rq;
blk_rq_init(q, rq);
if (bio_data_dir(q->orig_bar_rq->bio) == WRITE)
rq->cmd_flags |= REQ_RW;
if (q->ordered & QUEUE_ORDERED_FUA)
rq->cmd_flags |= REQ_FUA;
init_request_from_bio(rq, q->orig_bar_rq->bio);
rq->end_io = bar_end_io;
/*
* Queue ordered sequence. As we stack them at the head, we
* need to queue in reverse order. Note that we rely on that
* no fs request uses ELEVATOR_INSERT_FRONT and thus no fs
* request gets inbetween ordered sequence. If this request is
* an empty barrier, we don't need to do a postflush ever since
* there will be no data written between the pre and post flush.
* Hence a single flush will suffice.
*/
if ((q->ordered & QUEUE_ORDERED_POSTFLUSH) && !blk_empty_barrier(rq))
queue_flush(q, QUEUE_ORDERED_POSTFLUSH);
else
q->ordseq |= QUEUE_ORDSEQ_POSTFLUSH;
elv_insert(q, rq, ELEVATOR_INSERT_FRONT);
if (q->ordered & QUEUE_ORDERED_PREFLUSH) {
queue_flush(q, QUEUE_ORDERED_PREFLUSH);
rq = &q->pre_flush_rq;
} else
q->ordseq |= QUEUE_ORDSEQ_PREFLUSH;
if ((q->ordered & QUEUE_ORDERED_TAG) || q->in_flight == 0)
q->ordseq |= QUEUE_ORDSEQ_DRAIN;
else
rq = NULL;
return rq;
}
start_ordered爲處理barrier request準備整個request序列。它主要完畢下面工做(1)保存原來barrier request,用設備請求隊列中所帶的bar_rq來替換該barrier request。
當中包括從該barreir request複製request的各類標誌以及bio。
(2)依據設備聲明的所支持的barrier 序列,初始化request序列。
該序列最多可能包括三個request:pre_flush_rq, bar_rq, post_flush_rq。
它們被倒着插入對頭,這樣就可以一次運行它們。固然。這三個request並不是必須得。
比方,假設設備支持的處理序列僅爲QUEUE_ORDERED_PREFLUSH,則僅僅會把pre_flush_rq和bar_qr插入隊列。
又比方,假設barrier reques沒有包括不論什麼數據,則post flush可以省略。所以,也僅僅會插入上面兩個request。
注意。pre_flush_rq和post_flush_rq都是在插入以前。調用queue_flush初始化得。除了插入請求序列包括的request,同一時候還需要依據請求序列的設置來設置當前進行的請求序列:q->ordseq。現在請求序列尚未開始處理,怎麼在這兒設置當前的請求序列呢?那是因爲,依據設備的特性,三個request不必定都包括。而每個request表明了一個序列的處理階段。
這兒,咱們依據設備的聲明安排了整個請求序列,所以知道那些請求,也就是那些處理階段不需要。在這兒把這些階段的標誌置位,表示這些階段已經運行完成。是爲了省略對應的處理階段。現在,設備請求隊列中的請求以及處理序列例如如下所看到的:
Rq1 re2 pre_flush_rq bar_rq post_flush_rq
0 0 0 0 0
QUEUE_ORDSEQ_DRAIN QUEUE_ORDERED_PREFLUSH QUEUE_ORDSEQ_BAR QUEUE_ORDERED_POSTFLUSH
分類: LINUX
3)memory強制gcc編譯器若是RAM所有內存單元均被彙編指令改動,這樣cpu中的registers和cache中已緩存的內存單元中的數據將做廢。cpu將不得不在需要的時候又一次讀取內存中的數據。這就阻止了cpu又將registers,cache中的數據用於去優化指令。而避免去訪問內存。
4)"":::表示這是個空指令。
*****************************************************************************
------------------------------------------------------ 專題研究:內存屏障--------------------------------
---------------------------------------------------------論壇衆人資料聚集分析---------------------------
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
"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
mfence
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( 都是空宏。
**************************************************************************
在本文的代碼中有很多下劃線的keyword,特此做一研究:
--------------------------------------------------------雙下劃線的解釋--------------------------------------
--->摘自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'選項編譯時候,一些keyword,比方:asm、typeof、inline就不能再用了,在這個編譯選項下。這此keyword被關閉。
因此用有雙下劃線的keyword。如:__asm__、__typeof__、__inline__。這些編譯器一般支持這些帶有雙下劃線的宏。這能替換這些會產生編譯問題的keyword,使程序能正常經過編譯。
2。假設是用其它的編譯器。可能不認這些帶有雙下劃線的宏,就用下面宏來轉換:
#ifndef __GNUC__
#define __asm__ asm
#endif
這種話,這些其它的編譯器未定義__GUNUC__,也不支持__asm__,__inline__,__typeof__等宏,因此必會,運行#define __asm__ asm等。這樣。用__asm__,__inline__,__typeof__所編寫的程序代碼。仍能宏展開爲asm,inline,typeof,而這此keyword這些其它的編譯器支持。因此程序能正常編譯。
-----------------------------------------------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。
版權聲明:本文爲博主原創文章,未經博主贊成不得轉載。
1. linux下sync命令
在busybox-1.14.3中sync命令相關代碼很easy。
int sync_main(int argc, char **argv UNUSED_PARAM)
{
/* coreutils-6.9 compat */
bb_warn_ignoring_args(argc - 1);
sync();
return EXIT_SUCCESS;
}
2. sync系統調用
在fs/sync.c中
/*
* sync everything. Start out by waking pdflush, because that writes back
* all queues in parallel.
*/
SYSCALL_DEFINE0(sync)
{
wakeup_flusher_threads(0);
sync_filesystems(0);
sync_filesystems(1);
if (unlikely(laptop_mode))
laptop_sync_completion();
return 0;
}
值得注意的是sync函數僅僅是將所有改動過的塊緩衝區排入寫隊列,而後它就返回。它並不等待實際寫磁盤操做結束,幸運的是,一般成爲update的系統守護進程會週期(30s)調用sync函數,這就保證了按期沖洗內核的塊緩衝區。因此咱們在linux上更新一個文件後,不要着急從新啓動server,最好等待實際的磁盤寫操做完畢,避免數據丟失。
3. mips芯片的sync指令
防止不需要的亂序運行。
• SYNC affects only uncached and cached coherent loads and stores. The loads and stores that occur before the SYNC must be completed before the loads and stores after the SYNC are allowed to start.
• Loads are completed when the destination register is written. Stores are completed when the stored value is visible to every other processor in the system.
• SYNC is required, potentially in conjunction with SSNOP, to guarantee that memory reference results are visible
across operating mode changes. For example, a SYNC is required on some implementations on entry to and exit
from Debug Mode to guarantee that memory affects are handled correctly.
Detailed Description:
• When the stype field has a value of zero, every synchronizable load and store that occurs in the instruction stream
before the SYNC instruction must be globally performed before any synchronizable load or store that occurs after the
SYNC can be performed, with respect to any other processor or coherent I/O module.
• SYNC does not guarantee the order in which instruction fetches are performed. The stype values 1-31 are reserved
for future extensions to the architecture. A value of zero will always be defined such that it performs all defined
synchronization operations. Non-zero values may be defined to remove some synchronization operations. As such,
software should never use a non-zero value of the stype field, as this may inadvertently cause future failures if
non-zero values remove synchronization operations
基於mips架構的linux下barrier就是使用sync指令:
在文件 arch/mips/include/asm/barrier.h 中
#ifdef CONFIG_CPU_HAS_WB
#include <asm/wbflush.h>
#define wmb() fast_wmb()
#define rmb() fast_rmb()
#define mb() wbflush()
#define iob() wbflush()
#else /* !CONFIG_CPU_HAS_WB */
#define wmb() fast_wmb()
#define rmb() fast_rmb()
#define mb() fast_mb()
#define iob() fast_iob()
#endif /* !CONFIG_CPU_HAS_WB */
咱們所處的平臺沒有CONFIG_CPU_HAS_WB,因此是紅色的定義。
當中的fast_wmb/fast_rmb/fast_mb等定義可參考同一個文件的代碼:
#ifdef CONFIG_CPU_HAS_SYNC
#define __sync() \
__asm__ __volatile__( \
".set push\n\t" \
".set noreorder\n\t" \
".set mips2\n\t" \
"sync\n\t" \
".set pop" \
: /* no output */ \
: /* no input */ \
: "memory")
#else
#define __sync() do { } while(0)
#endif
#define __fast_iob() \
__asm__ __volatile__( \
".set push\n\t" \
".set noreorder\n\t" \
"lw $0,%0\n\t" \
"nop\n\t" \
".set pop" \
: /* no output */ \
: "m" (*(int *)CKSEG1) \
: "memory")
#ifdef CONFIG_CPU_CAVIUM_OCTEON
# define OCTEON_SYNCW_STR ".set push\n.set arch=octeon\nsyncw\nsyncw\n.set pop\n"
# define __syncw() __asm__ __volatile__(OCTEON_SYNCW_STR : : : "memory")
# define fast_wmb() __syncw()
# define fast_rmb() barrier()
# define fast_mb() __sync()
# define fast_iob() do { } while (0)
#else /* ! CONFIG_CPU_CAVIUM_OCTEON */
# define fast_wmb() __sync()
# define fast_rmb() __sync()
# define fast_mb() __sync()
# ifdef CONFIG_SGI_IP28
# define fast_iob() \
__asm__ __volatile__( \
".set push\n\t" \
".set noreorder\n\t" \
"lw $0,%0\n\t" \
"sync\n\t" \
"lw $0,%0\n\t" \
".set pop" \
: /* no output */ \
: "m" (*(int *)CKSEG1ADDR(0x1fa00004)) \
: "memory")
# else
# define fast_iob() \
do { \
__sync(); \
__fast_iob(); \
} while (0)
# endif
#endif /* CONFIG_CPU_CAVIUM_OCTEON */
可看到沒有CONFIG_CPU_CAVIUM_OCTEON時,
# define fast_wmb() __sync()
# define fast_rmb() __sync()
# define fast_mb() __sync()
都調用了__sync宏。
當中CONFIG_CPU_HAS_SYNC可參考arch/mips/Kconfig
config CPU_HAS_SYNC
bool
depends on !CPU_R3000
default y
做者:馮老師,華清遠見嵌入式學院講師。
本文主要對實現共享內存同步的四種方法進行了介紹。
共享內存是一種最爲高效的進程間通訊方式,進程可以直接讀寫內存。而不需要不論什麼數據的拷貝。它是IPC對象的一種。
爲了在多個進程間交換信息,內核專門留出了一塊內存區,可以由需要訪問的進程將其映射到本身的私有地址空間。進程就可以直接讀寫這一內存區而不需要進行數據的拷貝,從而大大提升的效率。
同步(synchronization)指的是多個任務(線程)依照約定的順序相互配合完畢一件事情。由於多個進程共享一段內存。所以也需要依靠某種同步機制。如相互排斥鎖和信號量等 。
信號燈(semaphore)。也叫信號量。它是不一樣進程間或一個給定進程內部不一樣線程間同步的機制。信號燈包含posix有名信號燈、 posix基於內存的信號燈(無名信號燈)和System V信號燈(IPC對象)
方法1、利用POSIX有名信號燈實現共享內存的同步
有名信號量既可用於線程間的同步,又可用於進程間的同步。
兩個進程。對同一個共享內存讀寫。可利用有名信號量來進行同步。一個進程寫,還有一個進程讀,利用兩個有名信號量semr, semw。semr信號量控制是否能讀。初始化爲0。 semw信號量控制是否能寫。初始爲1。
讀共享內存的程序演示樣例代碼例如如下
semr = sem_open("mysem_r", O_CREAT | O_RDWR , 0666, 0);