一直以来。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);