一直以来,I/O顺序问题一直困扰着我。其实这个问题是一个比较综合的问题,它涉及的层次比较多,从VFS page cache到I/O调度算法,从i/o子系统到存储外设。而Linux I/O barrier就是其中重要的一部分。可能很多人认为,在做了文件写操作后,调用fsycn就能保证数据可靠地写入磁盘。大多数情况下,确实如此。但是,由于缓存的存在,fsycn这些同步操作,并不能保证存储设备把数据写入非易失性介质。如果此时存储设备发生掉电或者硬件错误,此时存储缓存中的数据将会丢失。这对于像日志文件系统中的日志这样的数据,其后果可能是非常严重的。因为日志文件系统中,数据的写入和日志的写入存在先后顺序。如果顺序发生错乱,则可能破坏文件系统。因此必须要有一种方式,来知道写入的数据是否真的被写入到外部存储的非易失性介质,比便文件系统根据写入情况来进行下一步的操作。如果把fsycn理解成OS级别同步的话,那么对于Barrier I/O,我的理解就是硬件级别的同步。具体Linux Barrier I/O的介绍,参考”Linux Barrier I/O”。本文主要分析Linux Barrier I/O的实现以及其他块设备驱动对它的影响。
Barrier I/O的目的是使其之前的I/O在其之前写入存储介质,之后的I/O需要等到其写入完成后才能得到执行。为了实现这个要求,我们最多需要执行2次flush(刷新)操作。(注意,这儿所说的flush,指的是刷新存储设备的缓存。但并不是所有存储设备都支持flush操作,所以不是所有设备都支持barrier I/O。支持根据这个要求,需要在初始化磁盘设备的请求队列时,显式的表明该设备支持barrier I/O的类型并实现prepare flush 方法,参见”Linux Barrier I/O”。)第一次flush是把barrier I/O之前的所有数据刷新,当刷新成功,也就是这些数据被存储设备告知确实写入其介质后,提交Barrier I/O所在的请求。然后执行第二次刷新,这次刷新的是Barrier I/O所携带的数据。当然,如果Barrier I/O没有携带任何数据,则第二次刷新可以省略。此外,如果存储设备支持FUA,则可以在提交Barrier I/O所携带的数据时,使用FUA命令。这样可以直接知道Barrier I/O所携带的数据是否写入成功,从而省略掉第二次刷新。
通过对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所要求的功能。当然,并不是所有设备都必须使用以上序列中的所有操作,具体要使用那些操作,是有设备自身特点决定的。为了标示设备所需要采取的操作序列,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
一句话技术——barrier内存屏蔽 2010-12-20 12:32:57
#define barrier() __asm__ __volatile__("": : :"memory")
1)__asm__用于指示编译器在此插入汇编语句
2)__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
3)memory强制gcc编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。
4)"":::表示这是个空指令。
由这个话题引发的一个在cu上很经典的帖子,读了一点,暂时还不是完全理解,贴在下面,奇文共赏。
来自于: http://www.linuxforum.net/forum/ ... sts&Main=587989
存屏障机制及内核相关源代码分析
分析人:余旭
分析版本:Linux Kernel 2.6.14 来自于: www.kernel.org
分析开始时间:2005-11-17-20:45:56
分析结束时间:2005-11-21-20:07:32
编号:2-1 类别:进程管理-准备工作1-内存屏障
Email: [email protected]
版权声明:版权保留。本文用作其他用途当经作者本人同意,转载请注明作者姓名
All Rights Reserved. If for other use,must Agreed By the writer.Citing this text,please claim the writer's name.
Copyright (C) 2005 YuXu
*************************************************************
内存屏障是Linux Kernel中常要遇到的问题,这里专门来对其进行研究。一者查阅网上现有资料,进行整理汇集;二者翻阅Linux内核方面的指导书,从中提炼观点;最后,自己加以综合分析,提出自己的看法。下面将对个问题进行专题分析。
*****************************************************************************
------------------------------------------------------ 专题研究:内存屏障--------------------------------
---------------------------------------------------------论坛众人资料汇集分析---------------------------
set_current_state(),__set_current_state(),set_task_state(),__set_task_state(),rmb(),wmb(),mb()的源代码中的相关疑难问题及众人的论坛观点:
-----------------------------------------------------------------------------------------------------------------
1.--->ymons 在 www.linuxforum.net Linux内核技术论坛发贴问:
set_current_state和__set_current_state的区别?
#define __set_current_state(state_value) \
do { current->state = (state_value); } while (0)
#define set_current_state(state_value) \
set_mb(current->state, (state_value))
#define set_mb(var, value) do { var = value; mb(); } while (0)
#define mb() __asm__ __volatile__ ("" : : : "memory")
在linux的源代码中经常有这种设置当前进程状态的代码,但我搞不清楚这两种用法的不同?有哪位大虾指点一二,必将感谢不尽!
------------------
2.---> chyyuu( [email protected] ) 在 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 ( [email protected] )在 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文件的作用 作者:赵炯
[email protected]
作者说明:
1.OOPS和System.map文件密切相关。所以要研讨System.map文件。
2.本作者对所引用的文章内容进行了整理,删除了一些次要的部分,插入了一些内容,使文章更清晰。再者对一些内容进行了扩展说明。
--->符号表:
1.什么是符号(Symbols)?
在编程中,一个符号(symbol)是一个程序的创建块:它是一个变量名或一个函数名。如你自己编制的程序一样,内核具有各种符号也是不应该感到惊奇的。当然,区别在 于内核是一非常复杂的代码块,并且含有许多、许多的全局符号。
2.内核符号表(Kernel Symbol Table)是什么东西?
内核并不使用符号名。它是通过变量或函数的地址(指针)来使用变量或函数的,而 不是使用size_t BytesRead,内核更喜欢使用(例如)c0343f20来引用这个变量。
而另一方面,人们并不喜欢象c0343f20这样的名字。我们跟喜欢使用象 size_t BytesRead这样的表示。通常,这并不会带来什么问题。内核主要是用C语言写成的,所以在我们编程时编译器/连接程序允许我们使用符号名,并且使内核在运行时使用地址表示。这样大家都满意了。
然而,存在一种情况,此时我们需要知道一个符号的地址(或者一个地址对应的 符号)。这是通过符号表来做到的,与gdb能够从一个地址给出函数名(或者给出一个函数名的地址)的情况很相似。符号表是所有符号及其对应地址的一个列表。这里是 一个符号表例子:
c03441a0 B dmi_broken
c03441a4 B is_sony_vaio_laptop
c03441c0 b dmi_ident
c0344200 b pci_bios_present
c0344204 b pirq_table
c0344208 b pirq_router
c034420c b pirq_router_dev
c0344220 b ascii_buffer
c0344224 b ascii_buf_bytes
你可以看出名称为dmi_broken的变量位于内核地址c03441a0处。
--->;System.map文件与ksyms:
1.什么是System.map文件?
有两个文件是用作符号表的:
/proc/ksyms
System.map
这里,你现在可以知道System.map文件是干什么用的了。每当你编译一个新内核时,各种符号名的地址定会变化。
/proc/ksyms 是一个 "proc文件" 并且是在内核启动时创建的。实际上它不是一个真实的文件;它只是内核数据的简单表示形式,呈现出象一个磁盘文件似的。如果你不相信我,那么就试试找出/proc/ksyms的文件大小来。因此, 对于当前运行的内核来说,它总是正确的..
然而,System.map却是文件系统上的一个真实文件。当你编译一个新内核时,你原来的System.map中的符号信息就不正确了。随着每次内核的编译,就会产生一个新的 System.map文件,并且需要用该文件取代原来的文件。
--->OOPS:
1.什么是一个Oops?
在自己编制的程序中最常见的出错情况是什么?是段出错(segfault),信号11。
Linux内核中最常见的bug是什么?也是段出错。除此,正如你想象的那样,段出错的问题是非常复杂的,而且也是非常严重的。当内核引用了一个无效指针时,并不称其为段出错 -- 而被称为"oops"。一个oops表明内核存在一个bug,应该总是提出报告并修正该bug。
2.OOPS与段违例错的比较:
请注意,一个oops与一个段出错并不是一回事。你的程序并不能从段出错中恢复 过来,当出现一个oops时,并不意味着内核肯定处于不稳定的状态。Linux内核是非常健壮的;一个oops可能仅杀死了当前进程,并使余下的内核处于一个良好的、稳定的状态。
3.OOPS与panic的比较:
一个oops并非是内核死循环(panic)。在内核调用了panic()函数后,内核就不能继续运行了;此时系统就处于停顿状态并且必须重启。如果系统中关键部分遭到破坏那么一个oops也可能会导致内核进入死循环(panic)。例如,设备驱动程序中 出现的oops就几乎不会导致系统进行死循环。
当出现一个oops时,系统就会显示出用于调试问题的相关信息,比如所有CPU寄存器中的内容以及页描述符表的位置等,尤其会象下面那样打印出EIP(指令指针)的内容:
EIP: 0010:[<00000000>]
Call Trace: []
4.一个Oops与System.map文件有什么关系呢?
我想你也会认为EIP和Call Trace所给出的信息并不多,但是重要的是,对于内核开发人员来说这些信息也是不够的。由于一个符号并没有固定的地址, c010b860可以指向任何地方。
为了帮助我们使用oops含糊的输出,Linux使用了一个称为klogd(内核日志后台程序)的后台程序,klogd会截取内核oops并且使用syslogd将其记录下来,并将某些象c010b860信息转换成我们可以识别和使用的信息。换句话说,klogd是一个内核消息记录器 (logger),它可以进行名字-地址之间的解析。一旦klogd开始转换内核消息,它就使用手头的记录器,将整个系统的消息记录下来,通常是使用 syslogd记录器。
为了进行名字-地址解析,klogd就要用到System.map文件。我想你现在知道一个oops与System.map的关系了。
---------------------
作者补充图:
System.map文件
^
|
|
syslogd记录------->klogd解析名字-地址
^
|
|
内核出错----->OOPS
-----------------------
深入说明: klogd会执行两类地址解析活动:
1.静态转换,将使用System.map文件。 所以得知System.map文件只用于名字-地址的静态转换。
2.Klogd动态转换
动态转换,该方式用于可加载模块,不使用System.map,因此与本讨论没有关系,但我仍然对其加以简单说明。假设你加载了一个产生oops 的内核模块。于是就会产生一个oops消息,klogd就会截获它,并发现该oops发生在d00cf810处。由于该地址属于动态加载模块,因此在 System.map文件中没有对应条目。klogd将会在其中寻找并会毫无所获,于是断定是一个可加载模块产生了oops。此时klogd就会向内核查询该可加载模块输出的符号。即使该模块的编制者没有输出其符号,klogd也起码会知道是哪个模块产生了oops,这总比对一个oops一无所知要好。
还有其它的软件会使用System.map,我将在后面作一说明。
--------------
System.map应该位于什么地方?
System.map应该位于使用它的软件能够寻找到的地方,也就是说,klogd会在什么地方寻找它。在系统启动时,如果没有以一个参数的形式为klogd给出System.map的位置,则klogd将会在三个地方搜寻System.map。依次为:
/boot/System.map
/System.map
/usr/src/linux/System.map
System.map 同样也含有版本信息,并且klogd能够智能化地搜索正确的map文件。例如,假设你正在运行内核2.4.18并且相应的map文件位于 /boot/System.map。现在你在目录/usr/src/linux中编译一个新内核2.5.1。在编译期间,文件 /usr/src/linux/System.map就会被创建。当你启动该新内核时,klogd将首先查询/boot/System.map,确认它不是启动内核正确的map文件,就会查询/usr/src/linux/System.map, 确定该文件是启动内核正确的map文件并开始读取其中的符号信息。
几个注意点:
1.klogd未公开的特性:
在2.5.x系列内核的某个版本,Linux内核会开始untar成linux-version,而非只是linux(请举手表决--有多少人一直等待着这样做?)。我不知道klogd是否已经修改为在/usr/src/linux-version/System.map中搜索。TODO:查看 klogd源代码。
在线手册上对此也没有完整描述,请看:
# strace -f /sbin/klogd | grep 'System.map'
31208 open("/boot/System.map-2.4.18", O_RDONLY|O_LARGEFILE) = 2
显然,不仅klogd在三个搜索目录中寻找正确版本的map文件,klogd也同样知道寻找名字为 "System.map" 后加"-内核版本",象 System.map-2.4.18. 这是klogd未公开的特性。
2.驱动程序与System.map文件的关系:
有一些驱动程序将使用System.map来解析符号(因为它们与内核头连接而非glibc库等),如果没有System.map文件,它们将不能正确地工作。这与一个模块由于内核版本不匹配而没有得到加载是两码事。模块加载是与内核版本有关,而与即使是同一版本内核其符号表也会变化的编译后内核无关。
3.还有谁使用了System.map?
不要认为System.map文件仅对内核oops有用。尽管内核本身实际上不使用System.map,其它程序,象klogd,lsof,
satan# strace lsof 2>&1 1> /dev/null | grep System
readlink("/proc/22711/fd/4", "/boot/System.map-2.4.18", 4095) = 23
ps,
satan# strace ps 2>&1 1> /dev/null | grep System
open("/boot/System.map-2.4.18", O_RDONLY|O_NONBLOCK|O_NOCTTY) = 6
以及其它许多软件,象dosemu,需要有一个正确的System.map文件。
4.如果我没有一个好的System.map,会发生什么问题?
假设你在同一台机器上有多个内核。则每个内核都需要一个独立的System.map文件!如果所启动的内核没有对应的System.map文件,那么你将定期地看到这样一条信息:
System.map does not match actual kernel (System.map与实际内核不匹配)
不是一个致命错误,但是每当你执行ps ax时都会恼人地出现。有些软件,比如dosemu,可能不会正常工作。最后,当出现一个内核oops时,klogd或ksymoops的输出可能会不可靠。
5.我如何对上述情况进行补救?
方法是将你所有的System.map文件放在目录/boot下,并使用内核版本号重新对它们进行命名。
5-1.假设你有以下多个内核:
/boot/vmlinuz-2.2.14
/boot/vmlinuz-2.2.13
那么,只需对应各内核版本对map文件进行改名,并放在/boot下,如:
/boot/System.map-2.2.14
/boot/System.map-2.2.13
5-2.如果你有同一个内核的两个拷贝怎么办?
例如:
/boot/vmlinuz-2.2.14
/boot/vmlinuz-2.2.14.nosound
最佳解决方案将是所有软件能够查找下列文件:
/boot/System.map-2.2.14
/boot/System.map-2.2.14.nosound
但是说实在的,我并不知道这是否是最佳情况。我曾经见到搜寻"System.map-kernelversion",但是对于搜索 "System.map-kernelversion.othertext"的情况呢?我不太清楚。此时我所能做的就是利用这样一个事实: /usr/src/linux是标准map文件的搜索路径,所以你的map文件将放在:
/boot/System.map-2.2.14
/usr/src/linux/System.map (对于nosound版本)
你也可以使用符号连接:
System.map-2.2.14
System.map-2.2.14.sound
System.map -> System.map-2.2.14.sound
------------------------------------------------OOPS解释完毕----------------------------------------------
学习到这里,OOPS和system.map文件,已经有了较深刻的认识。回过头来继续对内存屏障的学习。
******************************************************************************
4. www.21icbbs.com 上的贴子
为了防止编译器对有特定时续要求的的硬件操作进行优化,系统提供了相应的办法:
1,对于由于数据缓冲(比如延时读写,CACHE)所引起的问题,可以把相应的I/O区设成禁用缓冲。
2,对于编译优化,可以用内存屏障来解决。如:void rmb(void),void wmb(void),void mb(void),分别是读,写,读写 屏障。和void barrier(void).
5.自己分析:
作者查阅了内核注释如下:
-----------------------------------------------asm-i386\system.h--------------------------------------
内核注释:
/*
* Force strict CPU ordering.
* And yes, this is required on UP too when we're talking
* to devices.
*
* For now, "wmb()" doesn't actually do anything, as all
* Intel CPU's follow what Intel calls a *Processor Order*,
* in which all writes are seen in the program order even
* outside the CPU.
*
* I expect future Intel CPU's to have a weaker ordering,
* but I'd also expect them to finally get their act together
* and add some real memory barriers if so.
*
* Some non intel clones support out of order store. wmb() ceases to be a
* nop for these.
*/
自己分析认为:
1.Intel CPU 有严格的“processor Order”,已经确保内存按序写,这里的wmb()所以定义的为空操作。
2.内核人员希望Intel CPU今后能采用弱排序技术,采用真正的内存屏障技术。
3.在非intel的cpu上,wmb()就不再为空操作了。
-----------------------------------------内核2.6.14完整的源代码----------------------------------
下面的源代码来自于Linux Kernel 2.6.14,开始对其进行一一的全面的分析:
-------------------------------------------\include\asm-i386\system.h----------------------------------
-----------------------------------------------------alternative()-----------------------------------------
/*
* Alternative instructions for different CPU types or capabilities.
*
* This allows to use optimized instructions even on generic binary kernels.
*
* length of oldinstr must be longer or equal the length of newinstr
* It can be padded with nops as needed.
*
* For non barrier like inlines please define new variants
* without volatile and memory clobber.
*/
#define alternative(oldinstr, newinstr, feature) \
asm volatile ("661:\n\t" oldinstr "\n662:\n" \
".section .altinstructions,\"a\"\n" \
" .align 4\n" \
" .long 661b\n" /* label */ \
" .long 663f\n" /* new instruction */ \
" .byte %c0\n" /* feature bit */ \
" .byte 662b-661b\n" /* sourcelen */ \
" .byte 664f-663f\n" /* replacementlen */ \
".previous\n" \
".section .altinstr_replacement,\"ax\"\n" \
"663:\n\t" newinstr "\n664:\n" /* replacement */ \
".previous" :: "i" (feature) : "memory")
自己分析:
1.alternative()宏用于在不同的cpu上优化指令。oldinstr为旧指令,newinstr为新指令,feature为cpu特征位。
2.oldinstr的长度必须>=newinstr的长度。不够将填充空操作符。
----------------------------------------------------------------------
/*
* Force strict CPU ordering.
* And yes, this is required on UP too when we're talking
* to devices.
*
* For now, "wmb()" doesn't actually do anything, as all
* Intel CPU's follow what Intel calls a *Processor Order*,
* in which all writes are seen in the program order even
* outside the CPU.
*
* I expect future Intel CPU's to have a weaker ordering,
* but I'd also expect them to finally get their act together
* and add some real memory barriers if so.
*
* Some non intel clones support out of order store. wmb() ceases * to be a nop for these.
*/
/*
* Actually only lfence would be needed for mb() because all stores done by the kernel should be already ordered. But keep a full barrier for now.
*/
自己分析:
这里的内核中的注释,在前面已经作了讲解,主要就是intel cpu采用Processor Order,对wmb()保证其的执行顺序按照程序顺序执行,所以wmb()定义为空操作。如果是对于对于非intel的cpu,这时wmb()就不能再是空操作了。
---------------------------mb()--rmb()--read_barrier_depends()--wmb()------------------
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#define read_barrier_depends() do { } while(0)
#ifdef CONFIG_X86_OOSTORE
/* Actually there are no OOO store capable CPUs for now that do SSE,but make it already an possibility. */
作者附注:(对内核注释中的名词的解释)
-->OOO:Out of Order,乱序执行。
-->SSE:SSE是英特尔提出的即MMX之后新一代(当然是几年前了)CPU指令集,最早应用在PIII系列CPU上。
本小段内核注释意即:乱序存储的cpu还没有问世,故CONFIG_X86_OOSTORE也就仍未定义的,wmb()当为后面空宏(在__volatile__作用下,阻止编译器重排顺序优化)。
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define wmb() __asm__ __volatile__ ("": : :"memory")
#endif
--------------------------
自己分析:
1.lock, addl $0,0(%%esp)在本文开始处已经解决。
lock前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu的一个内存屏障。addl $0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock指令,用作cpu的内存屏障。
2.mfence保证系统在后面的memory访问之前,先前的memory访问都已经结束。这是mfence是X86cpu家族中的新指令。详见后面。
3.新旧指令对比:
-------------------------------
以前的源代码:
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
__asm__用于指示编译器在此插入汇编语句
__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
-------------------
现在的源代码:
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
--------------------------
两者比较:
比起以前的源代码来少了__asm__和__volatile__。增加了alternative()宏和mfence指令。
-------------------------
而SFENCE指令(在Pentium III中引入)和LFENCE,MFENCE指令(在Pentium 4和Intel Xeon处理器中引入)提供了某些特殊类型内存操作的排序和串行化功能。sfence,lfence,mfence指令是在后继的cpu中新出现的的指令。
SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。
SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。
LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。
MFENCE——串行化发生在MFENCE指令之前的读写操作。
注意:SFENCE,LFENCE,MFENCE指令提供了比CPUID指令更灵活有效的控制内存排序的方式。
sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
其实这里是用mfence新指令来替换老的指令串:__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")。
mfence的执行效果就等效于__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")的执行效果。只不过,__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")是在以前的cpu平台上所设计的,借助于编译器__asm__,__volatile__,lock这些指令来实现内存屏障。而在 Pentium 4和Intel Xeon处理器中由于已经引入了mfence指令,无须再用这一套指令,直接调用这一条指令即ok。而alternative()宏就是用于这个优化指令的替换,用新的指令来替换老的指令串。
4.intel cpu已保证wmb()的顺序完成。wmb()此处定义为空操作。
5.X86_FEATURE_XMM的解释:
--------------------------------------asm-i386\cpufeature.h----------------------------------------
#define X86_FEATURE_XMM (0*32+25) /* Streaming SIMD Extensions */
************************************************************************
下面对SIMD进行解释:
--------------《计算机系统结构》--郑纬民编--清华大学出版社---------
1).指令流:(instruction stream)机器执行的指令序列
2).数据流:(data stream)指令调用的数据序列,包括输入数据和中间结果。
3)Flynn分类法:
(1)SISD(Single Instrution stream Single Datastream)
单指令流单数据流,对应为传统的顺序处理计算机。
(2)SIMD(Single Instrution stream Multiple Datastream)
单指令流多数据流,对应阵列处理机或并行处理机。
(3)MISD(Multiple Instrution stream Single Datastream)
多指令流单数据流,对应流水线处理机。
(4)MIMD(Multiple Instrution stream Multiple Datastream)
多指令流多数据流,对应多处理机。
*************************************************************************
由于以上几个指令牵涉到多处理器的管理,要彻底弄懂这些代码的原理,必须深入挖掘之,既然遇到了,就一口气吃掉。追根问底,清楚其来龙去脉。
***********************************************************************
----->来自Baidu快照,原网页打不开了:多处理器管理
说明:作者对此文进行了参考,由于文章太长,太专业化,作者对其进行了改动处理:
------------------------------------------------------------------------------------------------
1.IA-32体系的机制:总线加锁、cache一致性管理、串行化指令、高级可编程中断控制器、二级缓存、超线程技术:IA-32体系提供了几种机制来管理和提升连接到同一系统总线的多个处理器的性能。这些机制包括:
1)总线加锁、cache一致性管理以实现对系统内存的原子操作、串行化指令(serializing instructions。这些指令仅对pentium4,Intel Xeon, P6,Pentium处理器有效)。
2)处理器芯片内置的高级可编程中断控制器(APIC)。APIC是在Pentium处理器中被引入IA-32体系的。
3)二级缓存(level 2, L2)。对于Pentium4,Intel Xeon, P6处理器,L2 cache已经紧密的封装到了处理器中。而Pentium,Intel486提供了用于支持外部L2 cache的管脚。
4)超线程技术。这个技术是IA-32体系的扩展,它能够让一个处理器内核并发的执行两个或两个以上的指令流。
这些机制在对称多处理系统(symmetric-multiprocessing, SMP)中是极其有用的。然而,在一个IA-32处理器和一个专用处理器(例如通信,图形,视频处理器)共享系统总线的应用中,这些机制也是适用的。
-------------------------
2.多处理机制的设计目标是:
1)保持系统内存的完整性(coherency):
当两个或多个处理器试图同时访问系统内存的同一地址时,必须有某种通信机制或内存访问协议来提升数据的完整性,以及在某些情况下,允许一个处理器临时锁定某个内存区域。
2)保持高速缓存的一致性:
当一个处理器访问另一个处理器缓存中的数据时,必须要得到正确的数据。如果这个处理器修改了数据,那么所有的访问这个数据的处理器都要收到被修改后的数据。
3)允许以可预知的顺序写内存:
在某些情况下,从外部观察到的写内存顺序必须要和编程时指定的写内存顺序相一致。
4)在一组处理器中派发中断处理:
当几个处理器正在并行的工作在一个系统中时,有一个集中的机制是必要的,这个机制可以用来接收中断以及把他们派发到某一个适当的处理器。
5)采用现代操作系统和应用程序都具有的多线程和多进程的特性来提升系统的性能。
---------------------------
根据本文的需要,将重点讨论内存加锁,串行(serializing instructions)指令,内存排序,加锁的原子操作(locked atomic operations)。
3.系统内存加锁的原子操作:
32位IA-32处理器支持对系统内存加锁的原子操作。这些操作常用来管理共享的数据结构(例如信号量,段描述符,系统段页表)。两个或多个处理器可能会同时的修改这些数据结构中的同一数据域或标志。
处理器应用三个相互依赖的机制来实现加锁的原子操作:
1)可靠的原子操作(guaranteed atomic operations)。
2)总线加锁,使用LOCK#信号和LOCK指令前缀。
3)缓存完整性协议,保证原子操作能够对缓存中的数据结构执行;这个机制出现在Pentium4,IntelXeon,P6系列处理器中,这些机制以下面的形式相互依赖。
--->某些基本的内存事务(memory transaction)例如读写系统内存的一个字节)被保证是原子的。也就是说,一旦开始,处理器会保证这个操作会在另一个处理器或总线代理(bus agent)访问相同的内存区域之前结束。
--->处理器还支持总线加锁以实现所选的内存操作(例如在共享内存中的读-改-写操作),这些操作需要自动的处理,但又不能以上面的方式处理。因为频繁使用的内存数据经常被缓存在处理器的L1,L2高速缓存里,原子操作通常是在处理器缓存内部进行的,并不需要声明总线加锁。这里的处理器缓存完整性协议保证了在缓冲内存上执行原子操作时其他缓存了相同内存区域的处理器被正确管理。
注意到这些处理加锁的原子操作的机制已经像IA-32处理器一样发展的越来越复杂。于是,最近的IA-32处理器(例如Pentium 4, Intel Xeon, P6系列处理器)提供了一种比早期IA-32处理器更为精简的机制。
------------------------------------------------保证原子操作的情况------------------------------------
4.保证原子操作的情况
Pentium 4, Intel Xeon,P6系列,Pentium,以及Intel486处理器保证下面的基本内存操作总被自动的执行:
1)读或写一个字节
2)读或写一个在16位边界对齐的字
3)读或写一个在32位边界对齐的双字
Pentium 4, Intel Xeon,P6系列以及Pentium处理器还保证下列内存操作总是被自动执行:
1)读或写一个在64位边界对齐的四字(quadword)
2)对32位数据总线可以容纳的未缓存的内存位置进行16位方式访问
(16-bit accesses to uncached memory locations that fit within a 32-bit data bus)
P6系列处理器还保证下列内存操作被自动执行:
对32位缓冲线(cache line)可以容纳的缓存中的数据进行非对齐的16位,32位,64位访问.
对于可以被缓存的但是却被总线宽度,缓冲线,页边界所分割的内存区域,Pentium 4, Intel Xeon, P6 family,Pentium以及Intel486处理器都不保证访问操作是原子的。Pentium 4, Intel Xeon,P6系列处理器提供了总线控制信号来允许外部的内存子系统完成对分割内存的原子性访问;但是,对于非对齐内存的访问会严重影响处理器的性能,因此应该尽量避免。
--------------------------------------------------------------总线加锁------------------------------------------
5.总线加锁(Bus Locking)
1.Lock信号的作用:
IA-32处理器提供了LOCK#信号。这个信号会在某些内存操作过程中被自动发出。当这个输出信号发出的时候,来自其他处理器或总线代理的总线控制请求将被阻塞。软件能够利用在指令前面添加LOCK前缀来指定在其他情况下的也需要LOCK语义(LOCK semantics)。
在Intel386,Intel486,Pentium处理器中,直接调用加锁的指令会导致LOCK#信号的产生。硬件的设计者需要保证系统硬件中LOCK#信号的有效性,以控制多个处理对内存的访问。
--->注意:
对于Pentium 4, Intel Xeon,以及P6系列处理器,如果被访问的内存区域存在于处理器内部的高速缓存中,那么LOCK#信号通常不被发出;但是处理器的缓存却要被锁定。
--------------------------------------------------自动加锁(Automatic Locking)------- -------------------
6.自动加锁(Automatic Locking)
1.下面的操作会自动的带有LOCK语义:
1)执行引用内存的XCHG指令。
2)设置TSS描述符的B(busy忙)标志。在进行任务切换时,处理器检查并设置TSS描述符的busy标志。为了保证两个处理器不会同时切换到同一个任务。处理器会在检查和设置这个标志的时遵循LOCK语义。
3)更新段描述符时。在装入一个段描述符时,如果段描述符的访问标志被清除,处理器会设置这个标志。在进行这个操作时,处理器会遵循LOCK语义,因此这个描述符不会在更新时被其他的处理器修改。为了使这个动作能够有效,更新描述符的操作系统过程应该采用下面的方法:
(1)使用加锁的操作修改访问权字节(access-rights byte),来表明这个段描述符已经不存在,同时设置类型变量,表明这个描述符正在被更新。
(2)更新段描述符的内容。这个操作可能需要多个内存访问;因此不能使用加锁指令。
(3)使用加锁操作来修改访问权字节(access-rights byte),来表明这个段描述符存在并且有效。
注意,Intel386处理器总是更新段描述符的访问标志,无论这个标志是否被清除。Pentium 4, Intel Xeon,P6系列,Pentium以及Intel486处理器仅在该标志被清除时才设置这个标志。
4)更新页目录(page-directory)和页表(page-table)的条目。在更新页目录和页表的条目时,处理器使用加锁的周期(locked cycles)来设置访问标志和脏标志(dirty flag)。
5)响应中断。发生中断后,中断控制器可能会使用数据总线给处理器传送中断向量。处理器必须遵循LOCK语义来保证传送中断向量时数据总线上没有其他数据。
-------------------------------------------------软件控制的总线加锁----------------------------------------
7.软件控制的总线加锁
1)总述:
如果想强制执行LOCK语义,软件可以在下面的指令前使用LOCK前缀。当LOCK前缀被置于其他的指令之前或者指令没有对内存进行写操作(也就是说目标操作数在寄存器中)时,一个非法操作码(invalid-opcode)异常会被抛出。
2)可以使用LOCK前缀的指令:
1)位测试和修改指令(BTS, BTR, BTC)
2)交换指令(XADD, CMPXCHG, CMPXCHG8B)
3)XCHG指令自动使用LOCK前缀
4)单操作数算术和逻辑指令:INC, DEC, NOT, NEG
5)双操作数算术和逻辑指令:ADD, ADC, SUB, SBB, AND, OR, XOR
3)注意:
(1)一个加锁的指令会保证对目标操作数所在的内存区域加锁,但是系统可能会将锁定区域解释得稍大一些。
(2)软件应该使用相同的地址和操作数长度来访问信号量(一个用作处理器之间信号传递用的共享内存)。例如,如果一个处理器使用一个字来访问信号量,其他的处理器就不应该使用一个字节来访问这个信号量。
(3)总线加锁的完整性不受内存区域对齐的影响。在所有更新操作数的总线周期内,加锁语义一直持续。但是建议加锁访问能够在自然边界对齐,这样可以提升系统性能:
任何边界的8位访问(加锁或不加锁)
16位边界的加锁字访问。
32位边界的加锁双字访问。
64位边界的加锁四字访问。
(4)对所有的内存操作和可见的外部事件来说,加锁的操作是原子的。只有取指令和页表操作能够越过加锁的指令。
(5)加锁的指令能用于同步数据,这个数据被一个处理器写而被其他处理器读。
对于P6系列处理器来说,加锁的操作使所有未完成的读写操作串行化(serialize)(也就是等待它们执行完毕)。这条规则同样适用于Pentium4和Intel Xeon处理器,但有一个例外:对弱排序的内存类型的读入操作可能不会被串行化。
加锁的指令不应该用来保证写的数据可以作为指令取回。
--------------->自修改代码(self-modifying code)
(6)加锁的指令对于Pentium 4, Intel Xeon, P6 family, Pentium, and Intel486处理器,允许写的数据可以作为指令取回。但是Intel建议需要使用自修改代码(self-modifying code)的开发者使用另外一种同步机制。
处理自修改和交叉修改代码(handling self- and cross-modifying code)
处理器将数据写入当前的代码段以实现将该数据作为代码来执行的目的,这个动作称为自修改代码。IA-32处理器在执行自修改代码时采用特定模式的行为,具体依赖于被修改的代码与当前执行位置之间的距离。由于处理器的体系结构变得越来越复杂,而且可以在引退点(retirement point)之前推测性地执行接下来的代码(如:P4, Intel Xeon, P6系列处理器),如何判断应该执行哪段代码,是修改前地还是修改后的,就变得模糊不清。要想写出于现在的和将来的IA-32体系相兼容的自修改代码,必须选择下面的两种方式之一:
(方式1)
将代码作为数据写入代码段;
跳转到新的代码位置或某个中间位置;
执行新的代码;
(方式2)
将代码作为数据写入代码段;
执行一条串行化指令;(如:CPUID指令)
执行新的代码;
(在Pentium或486处理器上运行的程序不需要以上面的方式书写,但是为了与Pentium 4, Intel Xeon, P6系列处理器兼容,建议采用上面的方式。)
需要注意的是自修改代码将会比非自修改代码的运行效率要低。性能损失的程度依赖于修改的频率以及代码本身的特性。
--------------->交叉修改代码(cross-modifying code)
处理器将数据写入另外一个处理器的代码段以使得哪个处理器将该数据作为代码执行,这称为交叉修改代码(cross-modifying code)。像自修改代码一样,IA-32处理器采用特定模式的行为执行交叉修改代码,具体依赖于被修改的代码与当前执行位置之间的距离。要想写出于现在的和将来的IA-32体系相兼容的自修改代码,下面的处理器同步算法必须被实现:
;修改的处理器
Memory_Flag ← 0; (* Set Memory_Flag to value other than 1 *)
将代码作为数据写入代码段;
Memory_Flag ← 1;
;执行的处理器
WHILE (Memory_Flag ≠ 1)
等待代码更新;
ELIHW;
执行串行化指令; (* 例如, CPUID instruction *)
开始执行修改后的代码;
(在Pentium或486处理器上运行的程序不需要以上面的方式书写,但是为了与Pentium 4, Intel Xeon, P6系列处理器兼容,建议采用上面的方式。)
像自修改代码一样,交叉修改代码将会比非交叉修改代码的运行效率要低。性能损失的程度依赖于修改的频率以及代码本身的特性。
说明:作者读到这里时,也是对自修改代码和交叉修改代码稍懂一点,再要深入,也备感艰难。
-------------------------------------------------------缓存加锁--------------------------------------------
8.缓存加锁
1)加锁操作对处理器内部缓存的影响:
(1)对于Intel486和Pentium处理器,在进行加锁操作时,LOCK#信号总是在总线上发出,甚至锁定的内存区域已经缓存在处理器cache中的时候,LOCK#信号也从总线上发出。
(2)对于Pentium 4, Intel Xeon,P6系列处理器,如果加锁的内存区域已经缓存在处理器cache中,处理器可能并不对总线发出LOCK#信号,而是仅仅修改cache缓存中的数据,然后依赖cache缓存一致性机制来保证加锁操作的自动执行。这个操作称为"缓存加锁"。缓存一致性机制会自动阻止两个或多个缓存了同一区域内存的处理器同时修改数据。
-----------------------------------------------访存排序(memory ordering)-------- ---------------------
9.访存排序(memory ordering)
(1)编程排序(program ordering):
访存排序指的是处理器如何安排通过系统总线对系统内存访问的顺序。IA-32体系支持几种访存排序模型,具体依赖于体系的实现。例如, Intel386处理器强制执行"编程排序(program ordering)"(又称为强排序),在任何情况下,访存的顺序与它们出现在代码流中的顺序一致。
(2)处理器排序(processor ordering):
为了允许代码优化,IA-32体系在Pentium 4, Intel Xeon,P6系列处理器中允许强排序之外的另外一种模型——处理器排序(processor ordering)。这种排序模型允许读操作越过带缓存的写操作来提升性能。这个模型的目标是在多处理器系统中,在保持内存一致性的前提下,提高指令执行速度。
-----------------------------
10.Pentium和Intel 486处理器的访存排序:
1)普遍情况:
Pentium和Intel 486处理器遵循处理器排序访存模型;但是,在大多数情况下,访存操作还是强排序,读写操作都是以编程时指定的顺序出现在系统总线上。除了在下面的情况时,未命中的读操作可以越过带缓冲的写操作:
--->当所有的带缓冲的写操作都在cache缓存中命中,因此也就不会与未命中的读操作访问相同的内存地址。
2)I/O操作访存:
在执行I/O操作时,读操作和写操作总是以编程时指定的顺序执行。在"处理器排序"处理器(例如,Pentium 4, Intel Xeon,P6系列处理器)上运行的软件不能依赖Pentium或Intel486处理器的强排序。软件应该保证对共享变量的访问能够遵守编程顺序,这种编程顺序是通过使用加锁或序列化指令来完成的。
3)Pentium 4, Intel Xeon, P6系列处理器的访存排序
Pentium 4, Intel Xeon, P6系列处理器也是使用"处理器排序"的访存模型,这种模型可以被进一步定义为"带有存储缓冲转发的写排序"(write ordered with store-buffer forwarding)。这种模型有下面的特点:
---------单处理器系统中的排序规则
(1)在一个单处理器系统中,对于定义为回写可缓冲(write-back cacheable)的内存区域,下面的排序规则将被应用:
a.读能够被任意顺序执行。
b.读可以越过缓冲写,但是处理器必须保证数据完整性(self-consistent)。
c.对内存的写操作总是以编程顺序执行,除非写操作执行了CLFUSH指令以及利用非瞬时的移动指令(MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, MOVNTPD)来执行流存储操作(streamint stores)。
作者认为:CLFUSH--->CFLUSH,streamint--->streaming???是否原文有误。
d.写可以被缓冲。写不能够预先执行;它们只能等到其他指令执行完毕。
e.在处理器中,来自于缓冲写的数据可以直接被发送到正在等待的读操作。
f.读写操作都不能跨越I/O指令,加锁指令,或者序列化指令。
g.读操作不能越过LFENCE和MFENCE指令。
h.`写操作不能越过SFECE和MFENCE指令。
第二条规则(b)允许一个读操作越过写操作。然而如果写操作和读操作都是访问同一个内存区域,那么处理器内部的监视机制将会检测到冲突并且在处理器使用错误的数据执行指令之前更新已经缓存的读操作。
第六条规则(f)构成了一个例外,否则整个模型就是一个写排序模型(write ordered model)。
注意"带有存储缓冲转发的写排序"(在本节开始的时候介绍)指的是第2条规则和第6条规则的组合之后产生的效果。
---------------多处理器系统中的排序规则
(2)在一个多处理器系统中,下面的排序规则将被应用:
a.每个处理器使用同单处理器系统一样的排序规则。
b.所有处理器所观察到的某个处理器的写操作顺序是相同的。
c.每个处理器的写操作并不与其它处理器之间进行排序。
例如:在一个三处理器的系统中,每个处理器执行三个写操作,分别对三个地址A, B,C。每个处理器以编程的顺序执行操作,但是由于总线仲裁和其他的内存访问机制,三个处理器执行写操作的顺序可能每次都不相同。最终的A, B, C的值会因每次执行的顺序而改变。
-------------------
(3)本节介绍的处理器排序模型与Pentium Intel486处理器使用的模型是一样的。唯一在Pentium 4, Intel Xeon,P6系列处理器中得到加强的是:
a.对于预先执行读操作的支持。
b.存储缓冲转发,当一个读操作越过一个访问相同地址的写操作。
c.对于长串的存储和移动的无次序操作(out-of-Order Stores)Pentium 4,
--------------------
(4)快速串:
Intel Xeon, P6处理器对于串操作的无次序存储(Out-of-Order Stores)
Pentium 4, Intel
Xeon,P6处理器在进行串存储的操作(以MOVS和STOS指令开始)时,修改了处理器的动作,以提升处理性能。一旦"快速串"的条件满足了 (将在下面介绍),处理器将会在缓冲线(cache line)上以缓冲线模式进行操作。这会导致处理器在循环过程中发出对源地址的缓冲线读请求,以及在外部总线上发出对目标地址的写请求,并且已知了目标地址内的数据串一定要被修改。在这种模式下,处理器仅仅在缓冲线边界时才会相应中断。因此,目标数据的失效和存储可能会以不规则的顺序出现在外部总线上。
按顺序存储串的代码不应该使用串操作指令。数据和信号量应该分开。依赖顺序的代码应该在每次串操作时使用信号量来保证存储数据的顺序在所有处理器看来是一致的。
"快速串"的初始条件是:
在Pentium III 处理器中,EDI和ESI必须是8位对齐的。在Pentium4中,EDI必须是8位对齐的。
串操作必须是按地址增加的方向进行的。
初始操作计数器(ECX)必须大于等于64。
源和目的内存的重合区域一定不能小于一个缓冲线的大小(Pentium 4和Intel Xeon 处理器是64字节;P6 和Pentium处理器是 32字节)。
源地址和目的地址的内存类型必须是WB或WC。
----------------
11.加强和削弱访存排序模型(Strengthening or Weakening the Memory Ordering Model)
IA-32体系提供了几种机制用来加强和削弱访存排序模型以处理特殊的编程场合。这些机制包括:
1)I/O指令,加锁指令,LOCK前缀,以及序列化指令来强制执行"强排序"。
2)SFENCE指令(在Pentium III中引入)和LFENCE,MFENCE指令(在Pentium 4和Intel Xeon处理器中引入)提供了某些特殊类型内存操作的排序和串行化功能。
3)内存类型范围寄存器(memory type range registers (MTRRs))可以被用来加强和削弱物理内存中特定区域的访存排序模型。MTRRs只存在于Pentium 4, Intel Xeon, P6系列处理器。
4)页属性表可以被用来加强某个页或一组页的访存排序("页属性表"Page Attribute Table(PAT))。PAT只存在于Pentium 4, Intel Xeon,P6系列处理器。
这些机制可以通过下面的方式使用:
1)内存映射和其他I/O设备通常对缓冲区写操作的顺序很敏感。I/O指令(IN,OUT)以下面的方式对这种访问执行强排序。在执行一条I/O 指令之前,处理器等待之前的所有指令执行完毕以及所有的缓冲区都被写入了内存。只有取指令操作和页表查询(page table walk)能够越过I/O指令。后续指令要等到I/O指令执行完毕才开始执行。
2)一个多处理器的系统中的同步机制可能会依赖"强排序"模型。这里,一个程序使用加锁指令,例如XCHG或者LOCK前缀,来保证读-改-写操作是自动进行的。加锁操作像I/O指令一样等待所有之前的指令执行完毕以及缓冲区都被写入了内存。
3)程序同步可以通过序列化指令来实现。这些指令通常用于临界过程或者任务边界来保证之前所有的指令在跳转到新的代码区或上下文切换之前执行完毕。像I/O加锁指令一样,处理器等待之前所有的指令执行完毕以及所有的缓冲区写入内存后才开始执行序列化指令。
4)SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。
SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。
LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。
MFENCE——串行化发生在MFENCE指令之前的读写操作。
注意:SFENCE,LFENCE,MFENCE指令提供了比CPUID指令更灵活有效的控制内存排序的方式。
5)MTRRs在P6系列处理器中引入,用来定义物理内存的特定区域的高速缓存特性。下面的两个例子是利用MTRRs设置的内存类型如何来加强和削弱Pentium 4, Intel Xeon, P6系列处理器的访存排序:
(1)强不可缓冲(strong uncached,UC)内存类型实行内存访问的强排序模型:
这里,所有对UC内存区域的读写都出现在总线上,并且不能够被乱序或预先执行。这种内存类型可以应用于映射成I/O设备的内存区域来强制执行访存强排序。
(2)对于可以容忍弱排序访问的内存区域,可以选择回写(write back, WB)内存类型:
这里,读操作可以预先的被执行,写操作可以被缓冲和组合(combined)。对于这种类型的内存,锁定高速缓存是通过一个加锁的原子操作实现的,这个操作不会分割缓冲线,因此会减少典型的同步指令(如,XCHG在整个读-改-写操作周期要锁定数据总线)所带来的性能损失。对于WB内存,如果访问的数据已经存在于缓存cache中,XCHG指令会锁定高速缓存而不是数据总线。
(3)PAT在Pentium III中引入,用来增强用于存储内存页的缓存性能。PAT机制通常被用来与MTRRs一起来加强页级别的高速缓存性能。在Pentium 4, Intel Xeon,P6系列处理器上运行的软件最好假定是 "处理器排序"模型或者是更弱的访存排序模型。
Pentium 4, Intel Xeon,P6系列处理器没有实现强访存排序模型,除了对于UC内存类型。尽管Pentium 4, Intel Xeon,P6系列处理器支持处理器排序模型,Intel并没有保证将来的处理器会支持这种模型。为了使软件兼容将来的处理器,操作系统最好提供临界区 (critical region)和资源控制构建以及基于I/O,加锁,序列化指令的API,用于同步多处理器系统对共享内存区的访问。同时,软件不应该依赖处理器排序模型,因为也许系统硬件不支持这种访存模型。
(4)向多个处理器广播页表和页目录条目的改变:
在一个多处理器系统中,当一个处理器改变了一个页表或页目录的条目,这个改变必须要通知所有其它的处理器。这个过程通常称为"TLB shootdown"。广播页表或页目录条目的改变可以通过基于内存的信号量或者处理器间中断(interprocessor interrupts, IPI)。
例如一个简单的,但是算法上是正确的TLB shootdown序列可能是下面的样子:
a.开始屏障(begin barrier)——除了一个处理器外停止所有处理器;让他们执行HALT指令或者空循环。
b.让那个没有停止的处理器改变PTE or PDE。
c.让所有处理器在他们各自TLB中修改的PTE, PDE失效。
d.结束屏障(end barrier)——恢复所有的处理器执行。
(5)串行化指令(serializing instructions):
IA-32体系定义了几个串行化指令(SERIALIZING INSTRUCTIONS)。这些指令强制处理器完成先前指令对标志,寄存器以及内存的修改,并且在执行下一条指令之前将所有缓冲区里的数据写入内存。
===>串行化指令应用一:开启保护模式时的应用
例如:当MOV指令将一个操作数装入CR0寄存器以开启保护模式时,处理器必须在进入保护模式之前执行一个串行化操作。这个串行化操作保证所有在实地址模式下开始执行的指令在切换到保护模式之前都执行完毕。
-------------
串行化指令的概念在Pentium处理器中被引入IA-32体系。这种指令对于Intel486或更早的处理器是没有意义的,因为它们并没有实现并行指令执行。
非常值得注意的是,在Pentium 4, Intel Xeon,P6系列处理器上执行串行化指令会抑制指令的预执行(speculative execution),因为预执行的结果会被放弃掉。
-------------
下面的指令是串行化指令:
1.--->特权串行化指令——MOV(目标操作数为控制寄存器),MOV(目标操作数为调试存器),WRMSR, INVD, INVLPG, WBINVD, LGDT, LLDT, LIDT, LTR。
-------------------------作者补充------------------------------
作者:如果上述指令不熟,可以参考《80X86汇编语言程序设计教程》杨季文编,清华大学出版社。下面作些简单的介绍:以下作者对汇编指令的说明均参考引用了该书。
---->INVLPG指令:
使TLB(转换后援缓冲器:用于存放最常使用的物理页的页码)项无效。该指令是特权指令,只有在实方式和保护方式的特权级0下,才可执行该指令。
---------------------------------------------------------------
2.--->非特权串行化指令——CPUID, IRET, RSM。
3.--->非特权访存排序指令——SFENCE, LFENCE, MFENCE。
当处理器执行串行化指令的时候,它保证在执行下一条指令之前,所有未完成的内存事务都被完成,包括写缓冲中的数据。任何指令不能越过串行化指令,串行化指令也不能越过其他指令(读,写, 取指令, I/O)。
CPUID指令可以在任何特权级下执行串行化操作而不影响程序执行流(program flow),除非EAX, EBX, ECX, EDX寄存器被修改了。
SFENCE,LFENCE,MFENCE指令为控制串行化读写内存提供了更多的粒度。
在使用串行化指令时,最好注意下面的额外信息:
处理器在执行串行化指令的时候并不将高速缓存中已经被修改的数据写回到内存中。软件可以通过WBINVD串行化指令强制修改的数据写回到内存中。但是频繁的使用WVINVD(作者注:当为WBINVD,原文此处有误)指令会严重的降低系统的性能。
----------------作者补充:对WBINVAD的解释-----------------------
----->INVD指令:
INVD指令使片上的高速缓存无效,即:清洗片上的超高速缓存。但该指令并不把片上的超高速缓存中的内容写回主存。该指令是特权指令,只有在实方式和保护方式的特权级0下,才可执行该指令。
---->WBINVD指令:
WBINVD指令使片上的超高速缓存无效即:清洗片上的超高速缓存。但该指令将把片上的超高速缓存中更改的内容写回主存。该指令是特权指令,只有在实方式和保护方式的特权级0下,才可执行该指令。
****************************************************************
===>串行化指令应用二:改变了控制寄存器CR0的PG标志的应用
当一条会影响分页设置(也就是改变了控制寄存器CR0的PG标志)的指令执行时,这条指令后面应该是一条跳转指令。跳转目标应该以新的PG标志 (开启或关闭分页)来进行取指令操作,但跳转指令本身还是按先前的设置执行。Pentium 4, Intel Xeon,P6系列处理器不需要在设置CR0处理器之后放置跳转指令(因为任何对CR0进行操作的MOV指令都是串行化的)。但是为了与其他IA-32处理器向前和向后兼容,最好是放置一条跳转指令。
=========
作者说明:CR0的第31位为PG标志,PG=1:启用分页管理机制,此时线性地址经过分页管理机制后转换为物理地址;PG=0:禁用分页管理机制,此时线性地址直接作为物理地址使用。
****************************************************************
在允许分页的情况下,当一条指令会改变CR3的内容时,下一条指令会根据新的CR3内容所设置的转换表进行取指令操作。因此下一条以及之后的指令应该根据新的CR3内容建立映射。
=========
作者说明:CR3用于保存页目录表的起始物理地址,由于目录表是责对齐的,所以仅高20位有效,低12位无效。所以如果向CR3中装入新值,其低 12位当为0;每当用mov指令重置CR3的值时候,TLB中的内容会无效。CR3在实方式下也可以设置,以使分页机制初始化。在任务切换时候,CR3要被改变。但要是新任务的CR3的值==旧任务的CR3的值,则TLB的内容仍有效,不被刷新。
******************************************************************************
以上通过这篇文章资料对cpu的工作机制有了更深刻的了解,从而对我们的Linux Kernel的学习有极大的帮助。由此对加锁,各类排序,串行化,sfence,mfence,lfence指令的出现有了清楚的认识。再回头来读读源代码有更深刻的认识。
*****************************************************************************
------------------------------------------smp_mb()---smp_rmb()---smp_wmb()-------------------------
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif
#define set_wmb(var, value) do { var = value; wmb(); } while (0)
-----------------------------------------------\linux\compiler-gcc.h--------------------------------------
------------------------------------------------------barrier()-------------------------------------------------
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
自己分析:
1.如果定义的了CONFIG_SMP,也就是系统为对称多处理器系统。smp_mb(),smp_rmb(),smp_wmb()就是mb(),rmb(),wmb()。
由此可见,多处理器上的内存屏障与单处理器原理一样。
2.barrier()函数并无什么难点,与前面代码一样。
3.如果没有定义CONFIG_SMP,则smp_mb(), smp_rmb(), smp_wmb(), smp_read_barrier_depends( 都是空宏。
**************************************************************************
在本文的代码中有不少下划线的关键字,特此作一研究:
--------------------------------------------------------双下划线的解释--------------------------------------
--->摘自gcc手册
Alternate Keywords ‘-ansi’ and the various ‘-std’ options disable certain keywords。 This causes trouble when you want to use GNU C extensions, or a general-purpose header file that should be usable by all programs, including ISO C programs。 The keywords asm, typeof and inline are not available in programs compiled with ‘-ansi’ or ‘-std’ (although inline can be used in a program compiled with ‘-std=c99’)。 The ISO C99 keyword restrict is only available when ‘-std=gnu99’ (which will eventually be the default) or ‘-std=c99’ (or the equivalent ‘-std=iso9899:1999’) is used。The way to solve these problems is to put ‘__’ at the beginning and end of each problematical keyword。 For example, use __asm__ instead of asm, and __inline__ instead of inline。
Other C compilers won’t accept these alternative keywords; if you want to compile with another compiler, you can define the alternate keywords as macros to replace them with the customary keywords。 It looks like this:
#ifndef __GNUC__
#define __asm__ asm
#endif
‘-pedantic’(pedantic选项解释见下面) and other options cause warnings for many GNU C extensions。 You can prevent such warnings within one expression by writing __extension__ before the expression。__extension__ has no effect aside from this。
自己分析:
1。我们在程序中使用了很多的gnu风格,也就是GNU C extensions 或其他的通用的头文件。但是如果程序用'-ansi'或各种'-std'选项编译时候,一些关键字,比如:asm、typeof、inline就不能再用了,在这个编译选项下,这此关键字被关闭。所以用有双下划线的关键字,如:__asm__、__typeof__、__inline__,这些编译器通常支持这些带有双下划线的宏。这能替换这些会产生编译问题的关键字,使程序能正常通过编译。
2。如果是用其他的编译器,可能不认这些带有双下划线的宏,就用以下宏来转换:
#ifndef __GNUC__
#define __asm__ asm
#endif
这样的话,这些其他的编译器没有定义__GUNUC__,也不支持__asm__,__inline__,__typeof__等宏,所以必会,执行#define __asm__ asm等。这样,用__asm__,__inline__,__typeof__所编写的程序代码,仍能宏展开为asm,inline,typeof,而这此关键字这些其他的编译器支持。所以程序能正常编译。
-----------------------------------------------pedantic选项的解释----------------------------------
--->摘自gcc手册Download from www。gnu。org
Issue all the warnings demanded by strict ISO C and ISO C++; reject all programs that use forbidden extensions, and some other programs that do not follow ISO C and ISO C++。 For ISO C, follows the version of the ISO C standard specified by any ‘-std’ option used。 Valid ISO C and ISO C++ programs should compile properly with or without this option (though a rare few will require ‘-ansi’ or a ‘-std’ option specifying the required version of ISO C)。 However, without this option, certain GNU extensions and traditional C and C++ features are supported as well。 With this
option, they are rejected。 ‘-pedantic’ does not cause warning messages for use of the alternate keywords whose names begin and end with ‘__’。 Pedantic warnings are also disabled in the expression that follows __extension__。 However, only system header files should use these escape routes; application programs should avoid them。 See Section 5。38 [Alternate Keywords], page 271。
Some users try to use ‘-pedantic’ to check programs for strict ISO C conformance。They soon find that it does not do quite what they want: it finds some non-ISO practices, but not all—only those for which ISO C requires a diagnostic, and some others for which diagnostics have been added。 A feature to report any failure to conform to ISO C might be useful in some instances, but would require considerable additional work and would be quite different from ‘-pedantic’。 We don’t have plans to support such a feature in the near future。
sync
标签: filesystemsoutputreferencelinux磁盘input
2012-05-30 18:10 1959人阅读 收藏 举报
版权声明:本文为博主原创文章,未经博主允许不得转载。
1. linux下sync命令
在busybox-1.14.3中sync命令相关代码非常简单,
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上更新一个文件后,不要着急重启服务器,最好等待实际的磁盘写操作完成,避免数据丢失。
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
#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
linux实现共享内存同步的四种方法
(2014-02-07 10:28:00)
转载▼
标签: linux 共享内存 嵌入式 it |
分类: 嵌入式linux |
作者:冯老师,华清远见嵌入式学院讲师。
本文主要对实现共享内存同步的四种方法进行了介绍。
共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。它是IPC对象的一种。
为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情。由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等 。
信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制。信号灯包括posix有名信号灯、 posix基于内存的信号灯(无名信号灯)和System V信号灯(IPC对象)
方法一、利用POSIX有名信号灯实现共享内存的同步
有名信号量既可用于线程间的同步,又可用于进程间的同步。
两个进程,对同一个共享内存读写,可利用有名信号量来进行同步。一个进程写,另一个进程读,利用两个有名信号量semr, semw。semr信号量控制能否读,初始化为0。 semw信号量控制能否写,初始为1。
读共享内存的程序示例代码如下
semr = sem_open("mysem_r", O_CREAT | O_RDWR , 0666, 0);
if (semr == SEM_FAILED)
{
printf("errno=%d\n", errno);
return -1;
}
semw = sem_open("mysem_w", O_CREAT | O_RDWR, 0666, 1);
if (semw == SEM_FAILED)
{
printf("errno=%d\n", errno);
return -1;
}
if ((shmid = shmget(key, MAXSIZE, 0666 | IPC_CREAT)) == -1)
{
perror("semget");
exit(-1);
}
if ((shmadd = (char *)shmat(shmid, NULL, 0)) == (char *)(-1))
{
perror("shmat");
exit(-1);
}
while (1)
{
em_wait(semr);
printf("%s\n", shmadd);
sem_post(semw);
}
写共享内存的程序示例代码如下
。。。。。。
//同读的程序
while (1)
{
sem_wait(semw);
printf(">");
fgets(shmadd, MAXSIZE, stdin);
sem_post(semr);
}
方法二、利用POSIX无名信号灯实现共享内存的同步
POSIX无名信号量是基于内存的信号量,可以用于线程间同步也可以用于进程间同步。若实现进程间同步,需要在共享内存中来创建无名信号量。
因此,共享内存需要定义以下的结构体。
typedef struct
{
sem_t semr;
sem_t semw;
char buf[MAXSIZE];
}SHM;
读、写程序流程如下图所示。
方法三、利用System V的信号灯实现共享内存的同步
System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯
System V 信号灯由内核维护,主要函数semget,semop,semctl 。
一个进程写,另一个进程读,信号灯集中有两个信号灯,下标0代表能否读,初始化为0。 下标1代表能否写,初始为1。
程序流程如下:
写的流程和前边的类似。
方法四、利用信号实现共享内存的同步
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。利用信号也可以实现共享内存的同步。
思路:
reader和writer通过信号通信必须获取对方的进程号,可利用共享内存保存双方的进程号。
reader和writer运行的顺序不确定,可约定先运行的进程创建共享内存并初始化。
利用pause, kill, signal等函数可以实现该程序(流程和前边类似)。
多进程与多线程的优劣 与 共享内存的同步问题 2012-08-20 10:39:11
通常会说:
进程之间的地址空间是独享的,而线程是共享进程的地址空间,线程的资源比进程小,创建线程比创建进程快,线程间切换快,线程间通信快,线程资源利用率好.
下面做个补充:
1,线程挂则可能导致进程挂,稳定性差。对长时间运行的serve程序,这一点尤为重要。所以为了兼顾稳定性和性能,很多程序中采用multi-process +multi-thread.
2,线程受进程资源的限制,比如:ulimit -a可以看到的若干。
3,线程在同一进程内,方便的共享内存。进程则需做内存映射,相关的互斥量等也需要设置为进程共享。
4,同步相对复杂,不利于资源的管理和维护。
在开发经验中你会懂得:
1)web服务器都支持master-worker多进程架构
2)apache还在worker进程里采取主线程,监听线程,线程池的架构。
3) lighttpd/nginx用epoll I/O复用取代Apache的多线程架构,线程切换少了,进程稳定了。
4) I/O复用接口效率高,并发量不再受线程数限制了。
5) 多进程更有利于权限控制,通常web服务器的worker进程都会setuid到普通用户,避免拥有过高权限受到漏洞攻击,而master进程拥有root权限才能bind 80。
6) 多进程有利于架构级别的扩展,更利于部署,但多线程并不是影响这个问题的关键因素。
7) 如果功能点不会变动或者变动小,做在线程池里让功能更加内聚了。 但如果是一个通用模块,把它做成可扩展的独立进程,甚至通过程序架构设计动态库加载,可配置回调等等,让它为更多的项目服务,做成进程当然更合适。
另外一个问题:
在使用共享内存的时候,要注意什么?除了共享内存没有同步机制,使用共享内存的时候,程序员要自己实现同步,还有别的要注意么?
1,创建任何东西先带着CREAT | EXCL去创建,失败了则直接打开,这是原子性必备的。
2,共享内存初始化之前如何同步? 设置mode的X位后开始初始化共享内存,结束后取消X位,任何进程打开共享内存后stat轮询检查X位是否复位,复位后才可以开始操作。
3,进程共享的mutex, cond,你应该都会用,不会用看书或者man pthread.h找接口。
4,共享内存可以做成chunk list内存块链表,一般用于在共享内存中建立树型数据结构,或者建死的多级hash表,或者循环队列,都是可以做的。
5,其他同步机制,信号量,FIFO, 等等,反正用途比较小,看情况用。