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

分类: LINUX

#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:yuxu9710108@163.com

版权声明:版权保留。本文用作其它用途当经作者本人允许,转载请注明作者姓名

All Rights Reserved. If for other use,must Agreed By the writer.Citing this text,please claim the writer's name.

Copyright (C) 2005 YuXu

*************************************************************

内存屏障是Linux Kernel中常要遇到的问题,这里专门来对其进行研究。一者查阅网上现有资料。进行整理汇集;二者翻阅Linux内核方面的指导书。从中提炼观点。最后,自己加以综合分析,提出自己的看法。以下将对个问题进行专题分析。

*****************************************************************************

------------------------------------------------------ 专题研究:内存屏障--------------------------------



---------------------------------------------------------论坛众人资料汇集分析---------------------------

set_current_state(),__set_current_state(),set_task_state(),__set_task_state(),rmb(),wmb(),mb()的源码中的相关疑难问题及众人的论坛观点:

-----------------------------------------------------------------------------------------------------------------

1.--->ymons 在www.linuxforum.net Linux内核技术论坛发贴问:

set_current_state和__set_current_state的差别?



#define __set_current_state(state_value) \

do { current->state = (state_value); } while (0)



#define set_current_state(state_value) \

set_mb(current->state, (state_value))



#define set_mb(var, value) do { var = value; mb(); } while (0)

#define mb() __asm__ __volatile__ ("" : : : "memory")



在linux的源码中常常有这样的设置当前进程状态的代码,但我搞不清楚这两种使用方法的不同?有哪位大虾指点一二。必将感谢不尽!

------------------

2.---> chyyuu(chenyu-tmlinux@hpclab.cs.tsinghua.edu.cn)
www.linuxforum.net的Linux内核技术上发贴问:



在kernel.h中有一个define

/* Optimization barrier */

/* The "volatile" is due to gcc bugs */

#define barrier() __asm__ __volatile__("": : :"memory")

在内核很多地方被调用。不知究竟是生成什么汇编指令????

请教!

!!

--------------------

3.--->tigerl 02-12-08 10:57 在www.linuxforum.net的Linux内核技术提问:



这一句(include/asm-i386/system.h中)定义是什么意思?

#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")



4.--->jackcht 01-03-02 10:55 在www.linuxforum.net Linux内核技术
:

各位大虾,我在分析linux的时候发现有一个古怪的函数,就是barrier,俺愣是不知道它是干嘛用的,帮帮我这菜鸟吧,感谢感谢!

还有就是以下这句中的("":::"memory")是什么意思呀,我苦!

# define barrier() _asm__volatile_("": : :"memory")



***********************************众人的观点*******************************

ANSWER:

1.jkl Reply:这就是所谓的内存屏障。前段时间以前讨论过。CPU越过内存屏障后。将刷新自已对存储器的缓冲状态。

这条语句实际上不生成不论什么代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。



2.wheelz发帖指出:

#define __set_task_state(tsk, state_value) \

do { (tsk)->state = (state_value); } while (0)



#define set_task_state(tsk, state_value) \

set_mb((tsk)->state, (state_value))



set_task_state()带有一个memory barrier,__set_task_state()则没有,当状态state是RUNNING时。由于scheduler可能訪问这个state。因此此时要变成其它状态(如INTERRUPTIBLE)。就要用set_task_state()而当state不是RUNNING时,由于没有其它人会訪问这个state,因此能够用__set_task_state()反正用set_task_state()肯定是安全的,但 __set_task_state()可能会快些。



自己分析:

wheelz解说非常清楚。尤其是指出了__set_task_state()速度会快于set_task_state()。这一点。非常多贴子忽略了,这里有独到之处。

在此,作者专门强调之。



3.自己分析:

1)set_mb(),mb(),barrier()函数追踪究竟,就是__asm__ __volatile__("":::"memory"),而这行代码就是内存屏障。

2)__asm__用于指示编译器在此插入汇编语句

3)__volatile__用于告诉编译器。严禁将此处的汇编语句与其他的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。

4)memory强制gcc编译器如果RAM全部内存单元均被汇编指令改动,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在须要的时候又一次读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去訪问内存。

5)"":::表示这是个空指令。barrier()不用在此插入一条串行化汇编指令。在后文将讨论什么叫串行化指令。

6)__asm__,__volatile__,memory在前面已经解释

7)lock前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu的一个内存屏障。

8)addl $0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。

加上一个0,esp寄存器的数值依旧不变。即这是一条没用的汇编指令。在此利用这条无价值的汇编指令来配合lock指令,在__asm__,__volatile__,memory的作用下。用作cpu的内存屏障。

9)set_current_state()和__set_current_state()差别就不难看出。

10)至于barrier()就非常易懂了。

11)作者注明:作者在回答这个问题时候,參考了《深入理解LINUX内核》一书,陈莉君译,中国电力出版社,P174



4.xshell 发贴指出:

#include 

"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。

2012-05-30 18:10 1959人阅读 评论(0)  举报
Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP 分类:
linux命令(21) Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP mips(11) Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP

版权声明:本文为博主原创文章,未经博主同意不得转载。

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

linux实现共享内存同步的四种方法

Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP
(2014-02-07 10:28:00)

作者:冯老师,华清远见嵌入式学院讲师。

本文主要对实现共享内存同步的四种方法进行了介绍。

共享内存是一种最为高效的进程间通信方式,进程能够直接读写内存。而不须要不论什么数据的拷贝。它是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;

读、敲代码流程例如以下图所看到的。

Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP

Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP

方法三、利用System V的信号灯实现共享内存的同步

System V的信号灯是一个或者多个信号灯的一个集合。

当中的每个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯

System V 信号灯由内核维护,主要函数semget。semop,semctl 。

一个进程写。还有一个进程读。信号灯集中有两个信号灯,下标0代表是否能读,初始化为0。 下标1代表是否能写。初始为1。

程序流程例如以下:

Linux Barrier I/O 实现分析与barrier内存屏蔽 总结-LMLPHP

写的流程和前边的类似。

方法四、利用信号实现共享内存的同步

信号是在软件层次上对中断机制的一种模拟。是一种异步通信方式。

利用信号也能够实现共享内存的同步。

思路:

reader和writer通过信号通信必须获取对方的进程号。可利用共享内存保存两方的进程号。

reader和writer执行的顺序不确定,可约定先执行的进程创建共享内存并初始化。

利用pause, kill, signal等函数能够实现该程序(流程和前边类似)。

分类: LINUX

一般会说:

进程之间的地址空间是独享的,而线程是共享进程的地址空间,线程的资源比进程小,创建线程比创建进程快,线程间切换快,线程间通信快,线程资源利用率好.



以下做个补充:

1,线程挂则可能导致进程挂,稳定性差。对长时间执行的serve程序,这一点尤为重要。所以为了兼顾稳定性和性能,非常多程序中採用multi-process +multi-thread.

2,线程受进程资源的限制,比方:ulimit -a能够看到的若干。

3,线程在同一进程内,方便的共享内存。进程则需做内存映射,相关的相互排斥量等也须要设置为进程共享。

4,同步相对复杂。不利于资源的管理和维护。



在开发经验中你会懂得:

1)webserver都支持master-worker多进程架构

2)apache还在worker进程里採取主线程。监听线程。线程池的架构。

3)  lighttpd/nginx用epoll I/O复用代替Apache的多线程架构,线程切换少了,进程稳定了。

4)  I/O复用接口效率高。并发量不再受线程数限制了。

5)  多进程更有利于权限控制。通常webserver的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, 等等,反正用途比較小。看情况用。

04-25 23:20