导航:首页 > 操作系统 > linux内存屏障

linux内存屏障

发布时间:2024-03-02 23:42:45

‘壹’ 如何实现linux下多线程之间的互斥与同步

Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态,linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景。

Linux内核是多进程、多线程的操作系统,它提供了相当完整的内核同步方法。内核同步方法列表如下:
中断屏蔽
原子操作
自旋锁
读写自旋锁
顺序锁
信号量
读写信号量
BKL(大内核锁)
Seq锁
一、并发与竞态:
定义:
并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。
在linux中,主要的竞态发生在如下几种情况:
1、对称多处理器(SMP)多个CPU
特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
2、单CPU内进程与抢占它的进程
3、中断(硬中断、软中断、Tasklet、底半部)与进程之间
只要并发的多个执行单元存在对共享资源的访问,竞态就有可能发生。
如果中断处理程序访问进程正在访问的资源,则竞态也会会发生。
多个中断之间本身也可能引起并发而导致竞态(中断被更高优先级的中断打断)。

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。

访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护,中断屏蔽,原子操作,自旋锁,和信号量都是linux设备驱动中可采用的互斥途径。

临界区和竞争条件:
所谓临界区(critical regions)就是访问和操作共享数据的代码段,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行——也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样,如果两个执行线程有可能处于同一个临界区中,那么就是程序包含一个bug,如果这种情况发生了,我们就称之为竞争条件(race conditions),避免并发和防止竞争条件被称为同步。

死锁:
死锁的产生需要一定条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了,所有线程都在相互等待,但它们永远不会释放已经占有的资源,于是任何线程都无法继续,这便意味着死锁的发生。

二、中断屏蔽
在单CPU范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。
由于linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。
中断屏蔽的使用方法:
local_irq_disable()//屏蔽中断
//临界区
local_irq_enable()//开中断
特点:
由于linux系统的异步IO,进程调度等很多重要操作都依赖于中断,在屏蔽中断期间所有的中断都无法得到处理,因此长时间的屏蔽是很危险的,有可能造成数据丢失甚至系统崩溃,这就要求在屏蔽中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
中断屏蔽只能禁止本CPU内的中断,因此,并不能解决多CPU引发的竞态,所以单独使用中断屏蔽并不是一个值得推荐的避免竞态的方法,它一般和自旋锁配合使用。

三、原子操作
定义:原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
(原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令)
(它保证指令以“原子”的方式执行而不能被打断)
原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl (递减指令)为例,这是一个典型的"读-改-写"过程,涉及两次内存访问。
通俗理解:
原子操作,顾名思义,就是说像原子一样不可再细分。一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。
分类:linux内核提供了一系列函数来实现内核中的原子操作,分为整型原子操作和位原子操作,共同点是:在任何情况下操作都是原子的,内核代码可以安全的调用它们而不被打断。

原子整数操作:
针对整数的原子操作只能对atomic_t类型的数据进行处理,在这里之所以引入了一个特殊的数据类型,而没有直接使用C语言的int型,主要是出于两个原因:
第一、让原子函数只接受atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用,同时,这也确保了该类型的数据不会被传递给其它任何非原子函数;
第二、使用atomic_t类型确保编译器不对相应的值进行访问优化——这点使得原子操作最终接收到正确的内存地址,而不是一个别名,最后就是在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异。
原子整数操作最常见的用途就是实现计数器。
另一点需要说明原子操作只能保证操作是原子的,要么完成,要么不完成,不会有操作一半的可能,但原子操作并不能保证操作的顺序性,即它不能保证两个操作是按某个顺序完成的。如果要保证原子操作的顺序性,请使用内存屏障指令。
atomic_t和ATOMIC_INIT(i)定义
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }

在你编写代码的时候,能使用原子操作的时候,就尽量不要使用复杂的加锁机制,对多数体系结构来讲,原子操作与更复杂的同步方法相比较,给系统带来的开销小,对高速缓存行的影响也小,但是,对于那些有高性能要求的代码,对多种同步方法进行测试比较,不失为一种明智的作法。

原子位操作:
针对位这一级数据进行操作的函数,是对普通的内存地址进行操作的。它的参数是一个指针和一个位号。

为方便其间,内核还提供了一组与上述操作对应的非原子位函数,非原子位函数与原子位函数的操作完全相同,但是,前者不保证原子性,且其名字前缀多两个下划线。例如,与test_bit()对应的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已经用锁保护了自己的数据),那么这些非原子的位函数相比原子的位函数可能会执行得更快些。

四、自旋锁
自旋锁的引入:
如 果每个临界区都能像增加变量这样简单就好了,可惜现实不是这样,而是临界区可以跨越多个函数,例如:先得从一个数据结果中移出数据,对其进行格式转换和解 析,最后再把它加入到另一个数据结构中,整个执行过程必须是原子的,在数据被更新完毕之前,不能有其他代码读取这些数据,显然,简单的原子操作是无能为力 的(在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间),这就需要使用更为复杂的同步方法——锁来提供保护。

自旋锁的介绍:
Linux内核中最常见的锁是自旋锁(spin lock),自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个被争用(已经被持有)的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用,要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行,在任意时间,自旋锁都可以防止多于一个的执行线程同时进入理解区,注意同一个锁可以用在多个位置—例如,对于给定数据的所有访问都可以得到保护和同步。
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),所以自旋锁不应该被长时间持有,事实上,这点正是使用自旋锁的初衷,在短期间内进行轻量级加锁,还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它,这样处理器就不必循环等待,可以去执行其他代码,这也会带来一定的开销——这里有两次明显的上下文切换, 被阻塞的线程要换出和换入。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时,当然我们大多数人不会无聊到去测量上下文切换的耗时,所以我们让持 有自旋锁的时间应尽可能的短就可以了,信号量可以提供上述第二种机制,它使得在发生争用时,等待的线程能投入睡眠,而不是旋转。
自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠),在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在 当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经持有的自旋锁,这样以来,中断处理程序就会自旋, 等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕前不可能运行,这正是我们在前一章节中提到的双重请求死锁,注意,需要关闭的只是当前处理器上的中断,如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同处理器上)最终释放锁。

自旋锁的简单理解:
理解自旋锁最简单的方法是把它作为一个变量看待,该变量把一个临界区或者标记为“我当前正在运行,请稍等一会”或者标记为“我当前不在运行,可以被使用”。如果A执行单元首先进入例程,它将持有自旋锁,当B执行单元试图进入同一个例程时,将获知自旋锁已被持有,需等到A执行单元释放后才能进入。

自旋锁的API函数:

其实介绍的几种信号量和互斥机制,其底层源码都是使用自旋锁,可以理解为自旋锁的再包装。所以从这里就可以理解为什么自旋锁通常可以提供比信号量更高的性能。
自旋锁是一个互斥设备,他只能会两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。
“测试并设置”的操作必须以原子方式完成。
任何时候,只要内核代码拥有自旋锁,在相关CPU上的抢占就会被禁止。
适用于自旋锁的核心规则:
(1)任何拥有自旋锁的代码都必须使原子的,除服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
(2)拥有自旋锁的时间越短越好。

需 要强调的是,自旋锁别设计用于多处理器的同步机制,对于单处理器(对于单处理器并且不可抢占的内核来说,自旋锁什么也不作),内核在编译时不会引入自旋锁 机制,对于可抢占的内核,它仅仅被用于设置内核的抢占机制是否开启的一个开关,也就是说加锁和解锁实际变成了禁止或开启内核抢占功能。如果内核不支持抢 占,那么自旋锁根本就不会编译到内核中。
内核中使用spinlock_t类型来表示自旋锁,它定义在:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;

对于不支持SMP的内核来说,struct raw_spinlock_t什么也没有,是一个空结构。对于支持多处理器的内核来说,struct raw_spinlock_t定义为
typedef struct {
unsigned int slock;
} raw_spinlock_t;

slock表示了自旋锁的状态,“1”表示自旋锁处于解锁状态(UNLOCK),“0”表示自旋锁处于上锁状态(LOCKED)。
break_lock表示当前是否由进程在等待自旋锁,显然,它只有在支持抢占的SMP内核上才起作用。
自旋锁的实现是一个复杂的过程,说它复杂不是因为需要多少代码或逻辑来实现它,其实它的实现代码很少。自旋锁的实现跟体系结构关系密切,核心代码基本也是由汇编语言写成,与体协结构相关的核心代码都放在相关的目录下,比如。对于我们驱动程序开发人员来说,我们没有必要了解这么spinlock的内部细节,如果你对它感兴趣,请参考阅读Linux内核源代码。对于我们驱动的spinlock接口,我们只需包括头文件。在我们详细的介绍spinlock的API之前,我们先来看看自旋锁的一个基本使用格式:
#include
spinlock_t lock = SPIN_LOCK_UNLOCKED;

spin_lock(&lock);
....
spin_unlock(&lock);

从使用上来说,spinlock的API还很简单的,一般我们会用的的API如下表,其实它们都是定义在中的宏接口,真正的实现在中
#include
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned long flags)
spin_unlock_irqsace(spinlock_t *, unsigned long flags)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)

• 初始化
spinlock有两种初始化形式,一种是静态初始化,一种是动态初始化。对于静态的spinlock对象,我们用 SPIN_LOCK_UNLOCKED来初始化,它是一个宏。当然,我们也可以把声明spinlock和初始化它放在一起做,这就是 DEFINE_SPINLOCK宏的工作,因此,下面的两行代码是等价的。
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;

spin_lock_init 函数一般用来初始化动态创建的spinlock_t对象,它的参数是一个指向spinlock_t对象的指针。当然,它也可以初始化一个静态的没有初始化的spinlock_t对象。
spinlock_t *lock
......
spin_lock_init(lock);

• 获取锁
内核提供了三个函数用于获取一个自旋锁。
spin_lock:获取指定的自旋锁。
spin_lock_irq:禁止本地中断并获取自旋锁。
spin_lock_irqsace:保存本地中断状态,禁止本地中断并获取自旋锁,返回本地中断状态。

自旋锁是可以使用在中断处理程序中的,这时需要使用具有关闭本地中断功能的函数,我们推荐使用 spin_lock_irqsave,因为它会保存加锁前的中断标志,这样就会正确恢复解锁时的中断标志。如果spin_lock_irq在加锁时中断是关闭的,那么在解锁时就会错误的开启中断。

另外两个同自旋锁获取相关的函数是:
spin_trylock():尝试获取自旋锁,如果获取失败则立即返回非0值,否则返回0。
spin_is_locked():判断指定的自旋锁是否已经被获取了。如果是则返回非0,否则,返回0。
• 释放锁
同获取锁相对应,内核提供了三个相对的函数来释放自旋锁。
spin_unlock:释放指定的自旋锁。
spin_unlock_irq:释放自旋锁并激活本地中断。
spin_unlock_irqsave:释放自旋锁,并恢复保存的本地中断状态。

五、读写自旋锁
如 果临界区保护的数据是可读可写的,那么只要没有写操作,对于读是可以支持并发操作的。对于这种只要求写操作是互斥的需求,如果还是使用自旋锁显然是无法满 足这个要求(对于读操作实在是太浪费了)。为此内核提供了另一种锁-读写自旋锁,读自旋锁也叫共享自旋锁,写自旋锁也叫排他自旋锁。
读写自旋锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面,只能最多有一个写进程,在读操作方面,同时可以有多个读执行单元,当然,读和写也不能同时进行。
读写自旋锁的使用也普通自旋锁的使用很类似,首先要初始化读写自旋锁对象:
// 静态初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//动态初始化
rwlock_t *rwlock;
...
rw_lock_init(rwlock);

在读操作代码里对共享数据获取读自旋锁:
read_lock(&rwlock);
...
read_unlock(&rwlock);

在写操作代码里为共享数据获取写自旋锁:
write_lock(&rwlock);
...
write_unlock(&rwlock);

需要注意的是,如果有大量的写操作,会使写操作自旋在写自旋锁上而处于写饥饿状态(等待读自旋锁的全部释放),因为读自旋锁会自由的获取读自旋锁。

读写自旋锁的函数类似于普通自旋锁,这里就不一一介绍了,我们把它列在下面的表中。
RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)
六、顺序琐
顺序琐(seqlock)是对读写锁的一种优化,若使用顺序琐,读执行单元绝不会被写执行单元阻塞,也就是说,读执行单元可以在写执行单元对被顺序琐保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。
但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其它写执行单元必须自旋在哪里,直到写执行单元释放了顺序琐。
如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的,这种锁在读写同时进行的概率比较小时,性能是非常好的,而且它允许读写同时进行,因而更大的提高了并发性,
注意,顺序琐由一个限制,就是它必须被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致Oops。
七、信号量
Linux中的信号量是一种睡眠锁,如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠,这时处理器能重获自由,从而去执行其它代码,当持有信号量的进程将信号量释放后,处于等待队列中的哪个任务被唤醒,并获得该信号量。
信号量,或旗标,就是我们在操作系统里学习的经典的P/V原语操作。
P:如果信号量值大于0,则递减信号量的值,程序继续执行,否则,睡眠等待信号量大于0。
V:递增信号量的值,如果递增的信号量的值大于0,则唤醒等待的进程。

信号量的值确定了同时可以有多少个进程可以同时进入临界区,如果信号量的初始值始1,这信号量就是互斥信号量(MUTEX)。对于大于1的非0值信号量,也可称为计数信号量(counting semaphore)。对于一般的驱动程序使用的信号量都是互斥信号量。
类似于自旋锁,信号量的实现也与体系结构密切相关,具体的实现定义在头文件中,对于x86_32系统来说,它的定义如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};

信号量的初始值count是atomic_t类型的,这是一个原子操作类型,它也是一个内核同步技术,可见信号量是基于原子操作的。我们会在后面原子操作部分对原子操作做详细介绍。

信号量的使用类似于自旋锁,包括创建、获取和释放。我们还是来先展示信号量的基本使用形式:
static DECLARE_MUTEX(my_sem);
......
if (down_interruptible(&my_sem))

{
return -ERESTARTSYS;
}
......
up(&my_sem)

Linux内核中的信号量函数接口如下:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
• 初始化信号量
信号量的初始化包括静态初始化和动态初始化。静态初始化用于静态的声明并初始化信号量。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);

对于动态声明或创建的信号量,可以使用如下函数进行初始化:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)

显然,带有MUTEX的函数始初始化互斥信号量。LOCKED则初始化信号量为锁状态。
• 使用信号量
信号量初始化完成后我们就可以使用它了
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

down函数会尝试获取指定的信号量,如果信号量已经被使用了,则进程进入不可中断的睡眠状态。down_interruptible则会使进程进入可中断的睡眠状态。关于进程状态的详细细节,我们在内核的进程管理里在做详细介绍。

down_trylock尝试获取信号量, 如果获取成功则返回0,失败则会立即返回非0。

当退出临界区时使用up函数释放信号量,如果信号量上的睡眠队列不为空,则唤醒其中一个等待进程。

八、读写信号量
类似于自旋锁,信号量也有读写信号量。读写信号量API定义在头文件中,它的定义其实也是体系结构相关的,因此具体实现定义在头文件中,以下是x86的例子:
struct rw_semaphore {
signed long count;
spinlock_t wait_lock;
struct list_head wait_list;
};

‘贰’ LINUX设备驱动程序如何与硬件通信

LINUX设备驱动程序是怎么样和硬件通信的?下面将由我带大家来解答这个疑问吧,希望对大家有所收获!

LINUX设备驱动程序与硬件设备之间的通信

设备驱动程序是软件概念和硬件电路之间的一个抽象层,因此两方面都要讨论。到目前为止,我们已经讨论详细讨论了软件概念上的一些细节,现在讨论另一方面,介绍驱动程序在Linux上如何在保持可移植性的前提下访问I/O端口和I/O内存。

我们在需要示例的场合会使用简单的数字I/O端口来讲解I/O指令,并使用普通的帧缓冲区显存来讲解内存映射I/O。

I/O端口和I/O内存

计算机对每种外设都是通过读写它的寄存器进行控制的。大部分外设都有几个寄存器,不管是在内存地址空间还是在I/O地址空间,这些寄存器的访问地址都是连续的。

I/O端口就是I/O端口,设备会把寄存器映射到I/O端口,不管处理器是否具有独立的I/O端口地址空间。即使没有在访问外设时也要模拟成读写I/O端口。

I/O内存是设备把寄存器映射到某个内存地址区段(如PCI设备)。这种I/O内存通常是首先方案,它不需要特殊的处理器指令,而且CPU核心访问内存更有效率。

I/O寄存器和常规内存

尽管硬件寄存器和内存非常相似,但程序员在访问I/O寄存器的时候必须注意避免由于CPU或编译器不恰当的优化而改变预期的I/O动作。

I/O寄存器和RAM最主要的区别就是I/O操作具有边际效应,而内存操作则没有:由于内存没有边际效应,所以可以用多种 方法 进行优化,如使用高速缓存保存数值、重新排序读/写指令等。

编译器能够将数值缓存在CPU寄存器中而不写入内存,即使储存数据,读写操作也都能在高速缓存中进行而不用访问物理RAM。无论是在编译器一级或是硬件一级,指令的重新排序都有可能发生:一个指令序列如果以不同于程序文本中的次序运行常常能执行得更快。

在对常规内存进行这些优化的时候,优化过程是透明的,而且效果良好,但是对I/O操作来说这些优化很可能造成致命的错误,这是因为受到边际效应的干扰,而这却是驱动程序访问I/O寄存器的主要目的。处理器无法预料某些 其它 进程(在另一个处理器上运行,或在在某个I/O控制器中发生的操作)是否会依赖于内存访问的顺序。编译器或CPU可能会自作聪明地重新排序所要求的操作,结果会发生奇怪的错误,并且很难调度。因此,驱动程序必须确保不使用高速缓冲,并且在访问寄存器时不发生读或写指令的重新排序。

由硬件自身引起的问题很解决:只要把底层硬件配置成(可以是自动的或是由Linux初始化代码完成)在访问I/O区域(不管是内存还是端口)时禁止硬件缓存即可。

由编译器优化和硬件重新排序引起的问题的解决办法是:对硬件(或其他处理器)必须以特定顺序的操作之间设置内存屏障(memory barrier)。Linux提供了4个宏来解决所有可能的排序问题:

#include <linux/kernel.h>

void barrier(void)

这个函数通知编译器插入一个内存屏障,但对硬件没有影响。编译后的代码会把当前CPU寄存器中的所有修改过的数值保存到内存中,需要这些数据的时候再重新读出来。对barrier的调用可避免在屏障前后的编译器优化,但硬件完成自己的重新排序。

#include <asm/system.h>

void rmb(void);

void read_barrier_depends(void);

void wmb(void);

void mb(void);

这些函数在已编译的指令流中插入硬件内存屏障;具体实现方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作之前完成。wmb保证写操作不会乱序,mb指令保证了两者都不会。这些函数都是barrier的超集。

void smp_rmb(void);

void smp_read_barrier_depends(void);

void smp_wmb(void);

void smp_mb(void);

上述屏障宏版本也插入硬件屏障,但仅仅在内核针对SMP系统编译时有效;在单处理器系统上,它们均会被扩展为上面那些简单的屏障调用。

设备驱动程序中使用内存屏障的典型形式如下:

writel(dev->registers.addr, io_destination_address);

writel(dev->registers.size, io_size);

writel(dev->registers.operation, DEV_READ);

wmb();

writel(dev->registers.control, DEV_GO);

在这个例子中,最重要的是要确保控制某种特定操作的所有设备寄存器一定要在操作开始之前已被正确设置。其中的内存屏障会强制写操作以要求的顺序完成。

因为内存屏障会影响系统性能,所以应该只用于真正需要的地方。不同类型的内存屏障对性能的影响也不尽相同,所以最好尽可能使用最符合需要的特定类型。

值得注意的是,大多数处理同步的内核原语,如自旋锁和atomic_t操作,也能作为内存屏障使用。同时还需要注意,某些外设总线(比如PCI总线)存在自身的高速缓存问题,我们将在后面的章节中讨论相关问题。

在某些体系架构上,允许把赋值语句和内存屏障进行合并以提高效率。内核提供了几个执行这种合并的宏,在默认情况下,这些宏的定义如下:

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

#define set_wmb(var, value) do {var = value; wmb();} while 0

#define set_rmb(var, value) do {var = value; rmb();} while 0

在适当的地方,<asm/system.h>中定义的这些宏可以利用体系架构特有的指令更快的完成任务。注意只有小部分体系架构定义了set_rmb宏。

使用I/O端口

I/O端口是驱动程序与许多设备之间的通信方式——至少在部分时间是这样。本节讲解了使用I/O端口的不同函数,另外也涉及到一些可移植性问题。

I/O端口分配

下面我们提供了一个注册的接口,它允允许驱动程序声明自己需要操作的端口:

#include <linux/ioport.h>

struct resource *request_region(unsigned long first, unsigned long n, const char *name);

它告诉内核,我们要使用起始于first的n个端口。name是设备的名称。如果分配成功返回非NULL,如果失败返回NULL。

所有分配的端口可从/proc/ioports中找到。如果我们无法分配到我们要的端口集合,则可以查看这个文件哪个驱动程序已经分配了这些端口。

如果不再使用这些端口,则用下面函数返回这些端口给系统:

void release_region(unsigned long start, unsigned long n);

下面函数允许驱动程序检查给定的I/O端口是否可用:

int check_region(unsigned long first, unsigned long n);//不可用返回负的错误代码

我们不赞成用这个函数,因为它返回成功并不能确保分配能够成功,因为检查和其后的分配并不是原子操作。我们应该始终使用request_region,因为这个函数执行了必要的锁定,以确保分配过程以安全原子的方式完成。

操作I/O端口

当驱动程序请求了需要使用的I/O端口范围后,必须读取和/或写入这些端口。为此,大多数硬件都会把8位、16位、32位区分开来。它们不能像访问系统内存那样混淆使用。

因此,C语言程序必须调用不同的函数访问大小不同的端口。那些只支持映射的I/O寄存器的计算机体系架构通过把I/O端口地址重新映射到内存地址来伪装端口I/O,并且为了易于移植,内核对驱动程序隐藏了这些细节。Linux内核头文件中(在与体系架构相关的头文件<asm/io.h>中)定义了如下一些访问I/O端口的内联函数:

unsigned inb(unsigned port);

void outb(unsigned char byte, unsigned port);

字节读写端口。

unsigned inw(unsigned port);

void outw(unsigned short word, unsigned port);

访问16位端口

unsigned inl(unsigned port);

void outl(unsigned longword, unsigned port);

访问32位端口

在用户空间访问I/O端口

上面这些函数主要是提供给设备驱动程序使用的,但它们也可以用户空间使用,至少在PC类计算机上可以使用。GNU的C库在<sys/io.h>中定义了这些函数。如果要要用户空间使用inb及相关函数,则必须满足正下面这些条件:

编译程序时必须带有-O选项来强制内联函数的展开。

必须用ioperm(获取单个端口的权限)或iopl(获取整个I/O空间)系统调用来获取对端口进行I/O操作的权限。这两个函数都是x86平台特有的。

必须以root身份运行该程序才能调用ioperm或iopl。或者进程的祖先进程之一已经以root身份获取对端口的访问。

如果宿主平台没有以上两个系统调用,则用户空间程序仍然可以使用/dev/port设备文件访问I/O端口。不过要注意,该设备文件的含义与平台密切相关,并且除PC平台以处,它几乎没有什么用处。

串操作

以上的I/O操作都是一次传输一个数据,作为补充,有些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字、双字。这些指令称为串操作指令,它们执行这些任务时比一个C语言编写的循环语句快得多。下面列出的宏实现了串I/O:

void insb(unsigned port, void *addr, unsigned long count);

void outsb(unsigned port, void *addr, unsigned long count);从内存addr开始连续读/写count数目的字节。只对单一端口port读取或写入数据

void insw(unsigned port, void *addr, unsigned long count);

void outsw(unsigned port, void *addr, unsigned long count);对一个16位端口读写16位数据

void insl(unsigned port, void *addr, unsigned long count);

void outsl(unsigned port, void *addr, unsigned long count);对一个32位端口读写32位数据

在使用串I/O操作函数时,需要铭记的是:它们直接将字节流从端口中读取或写入。因此,当端口和主机系统具有不同的字节序时,将导致不可预期的结果。使用inw读取端口将在必要时交换字节,以便确保读入的值匹配于主机的字节序。然而,串函数不会完成这种交换。

暂停式I/O

在处理器试图从总线上快速传输数据时,某些平台(特别是i386)就会出现问题。当处理器时钟比外设时钟(如ISA)快时就会出现问题,并且在设备板上特别慢时表现出来。为了防止出现丢失数据的情况,可以使用暂停式的I/O函数来取代通常的I/O函数,这些暂停式的I/O函数很像前面介绍的那些I/O函数,不同之处是它们的名字用_p结尾,如inb_p、outb_p等等。在linux支持的大多数平台上都定义了这些函数,不过它们常常扩展为非暂停式I/O同样的代码,因为如果不使用过时的外设总线就不需要额外的暂停。

平台相关性

I/O指令是与处理器密切相关的。因为它们的工作涉及到处理器移入移出数据的细节,所以隐藏平台间的差异非常困难。因此,大部分与I/O端口相关的源代码都与平台相关。

回顾前面函数列表可以看到有一处不兼容的地方,即数据类型。函数的参数根据各平台体系架构上的不同要相应地使用不同的数据类型。例如,port参数在x86平台上(处理器只支持64KB的I/O空间)上定义为unsigned short,但在其他平台上定义为unsigned long,在这些平台上,端口是与内存在同一地址空间内的一些特定区域。

感兴趣的读者可以从io.h文件获得更多信息,除了本章介绍的函数,一些与体系架构相关的函数有时也由该文件定义。

值得注意的是,x86家族之外的处理器都不为端口提供独立的地址空间。

I/O操作在各个平台上执行的细节在对应平台的编程手册中有详细的叙述;也可以从web上下载这些手册的PDF文件。

I/O端口示例

演示设备驱动程序的端口I/O的示例代码运行于通用的数字I/O端口上,这种端口在大多数计算机平台上都能找到。

数字I/O端口最常见的一种形式是一个字节宽度的I/O区域,它或者映射到内存,或者映射到端口。当把数字写入到输出区域时,输出引脚上的电平信号随着写入的各位而发生相应变化。从输入区域读取到的数据则是输入引脚各位当前的逻辑电平值。

这类I/O端口的具体实现和软件接口是因系统而异的。大多数情况下,I/O引脚由两个I/O区域控制的:一个区域中可以选择用于输入和输出的引脚,另一个区域中可以读写实际的逻辑电平。不过有时情况简单些,每个位不是输入就是输出(不过这种情况下就不能称为“通用I/O"了);在所有个人计算机上都能找到的并口就是这样的非通用的I/O端口。

并口简介

并口的最小配置由3个8位端口组成。第一个端口是一个双向的数据寄存器,它直接连接到物理连接器的2~9号引脚上。第二个端口是一个只读的状态寄存器;当并口连接打印机时,该寄存器 报告 打印机状态,如是否是线、缺纸、正忙等等。第三个端口是一个只用于输出的控制寄存器,它的作用之一是控制是否启用中断。

如下所示:并口的引脚

示例驱动程序

while(count--) {

outb(*(ptr++), port);

wmb();

}

使用I/O内存

除了x86上普遍使的I/O端口之外,和设备通信的另一种主要机制是通过使用映射到内存的寄存器或设备内存,这两种都称为I/O内存,因为寄存器和内存的差别对软件是透明的。

I/O内存仅仅是类似RAM的一个区域,在那里处理器可以通过总线访问设备。这种内存有很多用途,比如存放视频数据或以太网数据包,也可以用来实现类似I/O端口的设备寄存器(也就是说,对它们的读写也存在边际效应)。

根据计算机平台和所使用总线的不同,i/o内存可能是,也可能不是通过页表访问的。如果访问是经由页表进行的,内核必须首先安排物理地址使其对设备驱动程序可见(这通常意味着在进行任何I/O之前必须先调用ioremap)。如果访问无需页表,那么I/O内存区域就非常类似于I/O端口,可以使用适当形式的函数读取它们。

不管访问I/O内存是否需要调用ioremap,都不鼓励直接使用指向I/O内存的指针。相反使用包装函数访问I/O内存,这一方面在所有平台上都是安全的,另一方面,在可以直接对指针指向的内存区域执行操作的时候,这些函数是经过优化的。并且直接使用指针会影响程序的可移植性。

I/O内存分配和映射

在使用之前,必须首先分配I/O区域。分配内存区域的接口如下(在<linux/ioport.h>中定义):

struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

该函数从start开始分配len字节长的内存区域。如果成功返回非NULL,否则返回NULL值。所有的I/O内存分配情况可从/proc/iomem得到。

不再使用已分配的内存区域时,使用如下接口释放:

void release_mem_region(unsigned long start, unsigned long len);

下面函数用来检查给定的I/O内存区域是否可用的老函数:

int check_mem_region(unsigned long start, unsigned long len);//这个函数和check_region一样不安全,应避免使用

分配内存之后我们还必须确保该I/O内存对内存而言是可访问的。获取I/O内存并不意味着可引用对应的指针;在许多系统上,I/O内存根本不能通过这种方式直接访问。因此,我们必须由ioremap函数建立映射,ioremap专用于为I/O内存区域分配虚拟地址。

我们根据以下定义来调用ioremap函数:

#include <asm/io.h>

void *ioremap(unsigned long phys_addr, unsigned long size);

void *ioremap_nocache(unsigned long phys_addr, unsigned long size);在大多数计算机平台上,该函数和ioremap相同:当所有I/O内存已属于非缓存地址时,就没有必要实现ioremap的独立的,非缓冲版本。

void iounmap(void *addr);

记住,由ioremap返回的地址不应该直接引用,而应该使用内核提供的accessor函数。

访问I/O内存

在某些平台上我们可以将ioremap的返回值直接当作指针使用。但是,这种使用不具有可移植性,访问I/O内存的正确方法是通过一组专用于些目的的函数(在<asm/io.h>中定义)。

从I/O内存中读取,可使用以下函数之一:

unsigned int ioread8(void *addr);

unsigned int ioread16(void *addr);

unsigned int ioread32(void *addr);

其中,addr是从ioremap获得的地址(可能包含一个整数偏移量);返回值是从给定I/O内存读取到的值。

写入I/O内存的函数如下:

void iowrite8(u8 value, void *addr);

void iowrite16(u16 value, void *addr);

void iowrite32(u32 value, void *addr);

如果必须在给定的I/O内存地址处读/写一系列值,则可使用上述函数的重复版本:

void ioread8_rep(void *addr, void *buf, unsigned long count);

void ioread16_rep(void *addr, void *buf, unsigned long count);

void ioread32_rep(void *addr, void *buf, unsigned long count);

void iowrite8_rep(void *addr, const void *buf, unsigned long count);

void iowrite16_rep(void *addr, const void *buf, unsigned long count);

void iowrite32_rep(void *addr, const void *buf, unsigned long count);

上述函数从给定的buf向给定的addr读取或写入count个值。count以被写入数据的大小为单位。

上面函数均在给定的addr处执行所有的I/O操作,如果我们要在一块I/O内存上执行操作,则可以使用下面的函数:

void memset_io(void *addr, u8 value, unsigned int count);

void memcpy_fromio(void *dest, void *source, unsigned int count);

void memcpy_toio(void *dest, void *source, unsigned int count);

上述函数和C函数库的对应函数功能一致。

像I/O内存一样使用I/O端口

某些硬件具有一种有趣的特性:某些版本使用I/O端口,而其他版本则使用I/O内存。导出给处理器的寄存器在两种情况下都是一样的,但访问方法却不同。为了让处理这类硬件的驱动程序更加易于编写,也为了最小化I/O端口和I/O内存访问这间的表面区别,2.6内核引入了ioport_map函数:

void *ioport_map(unsigned long port, unsigned int count);

该函数重新映射count个I/O端口,使其看起来像I/O内存。此后,驱动程序可在该函数返回的地址上使用ioread8及其相关函数,这样就不必理会I/O端口和I/O内存之间的区别了。

当不需要这种映射时使用下面函数一撤消:

void ioport_unmap(void *addr);

这些函数使得I/O端口看起来像内存。但需要注意的是,在重新映射之前,我们必须通过request_region来分配这些I/O端口。

为I/O内存重用short

前面介绍的short示例模块访问的是I/O端口,它也可以访问I/O内存。为此必须在加载时通知它使用I/O内存,另外还要修改base地址以使其指向I/O区域。

下例是在MIPS开发板上点亮调试用的LED:

mips.root# ./short_load use_mem=1 base = 0xb7ffffc0

mips.root# echo -n 7 > /dev/short0

下面代码是short写入内存区域时使用的循环:

while(count--) {

iowrite8(*ptr++, address);

wmb();

}

1MB地址空间之下的ISA内存

最广为人知的I/O内存区之一就是个人计算机上的ISA内存段。它的内存范围在64KB(0xA0000)到1MB(0x100000)之间,因此它正好出现在常规系统RAM的中间。这种地址看上去有点奇怪,因为这个设计决策是20世纪80年代早期作出的,在当时看来没有人会用到640KB以上的内存。

‘叁’ 什么是全栈工程师

前端工程师:在打开一个网站时,屏幕上的一切都属于前端。前端工程师负责创建用户界面背后的代码。这些工程师不仅要熟悉HTML、JavaScript与CSS,还要掌握很多框架。这些框架有Foundation、Angular JS、Ember JS、Backbone及Bootstrap等等。前端工程师要与设计师和其他专家协同工作,从而将网站从模型转换为可使用的模式。

后端工程师:在你每天所访问的网站背后有服务器网络、数据库以及各种应用,他们相互协作将你所请求的网页与数据传递给你。后端工程师负责构建这些组件。他们要擅长各种编程语言,如Python、.Net、Ruby、Java及PHP等等,从而通过这些语言来编写程序。他们还需要使用各种数据库程序如MySQL、SQL Server及Oracle等来操纵信息并将其传递给最终用户。后端工程师需要与组织中的各种经理及其他成员通力配合来完成工作。

全栈工程师:全栈工程师既要了解后端开发,也要了解前端开发。他们是“全方位”的工程师,熟悉服务端的同时又懂客户端用户体验。全栈工程师理解Web开发进程的每一个方面,同时又会就整体策略与最佳实践对相关干系人提出建议与指导。

‘肆’ C语言中Valatile关键字有什么用

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。下面举例说明。在DSP开发中,经常需要等待某个事件的触发,所以经常会写出这样的程序:
short flag;
void test()
{
do1();
while(flag==0);
do2();
}
这段程序等待内存变量flag的值变为1(怀疑此处是0,有点疑问,)之后才运行do2()。变量flag的值由别的程序更改,这个程序可能是某个硬件中断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断,在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是,编译器并不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化,那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。为了让程序每次都读取真正flag变量的值,就需要定义为如下形式:
volatile short flag;
需要注意的是,没有volatile也可能能正常运行,但是可能修改了编译器的优化级别之后就又不能正常运行了。因此经常会出现debug版本正常,但是release版本却不能正常的问题。所以为了安全起见,只要是等待别的程序修改某个变量的话,就加上volatile关键字。

volatile的本意是“易变的”
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:
static int i=0;
int main(void)
{
...
while (1)
{
if (i) do_something();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中断产生时,在main当中调用do_something函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致do_something永远也不会被调用。如果变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
二、volatile 的含义
volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,它有下面的作用:
1 不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。
2 不做常量合并、常量传播等优化,所以像下面的代码:
volatile int i = 1;
if (i > 0) ...
if的条件不会当作无条件真。
3 对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。
前面有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。
对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。
你可能不知道在Pentium及后续CPU中,下面两组指令
inc jiffies
;;
mov jiffies, %eax
inc %eax
mov %eax, jiffies
作用相同,但一条指令反而不如三条指令快。
三、编译器优化 → C关键字volatile → memory破坏描述符zz
“memory”比较特殊,可能是内嵌汇编中最难懂部分。为解释清楚它,先介绍一下编译器的优化知识,再看C关键字volatile。最后去看该描述符。
1、编译器优化介绍
内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。再看软件一级的优化:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏解决编译器的执行顺序问题。
void Barrier(void)
这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。
2、C语言关键字volatile
C语言关键字volatile(注意它是用来修饰变量而不是上面介绍的__volatile__)表明某个变量的值可能在外部被改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新存取。该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程,例如:
DWORD __stdcall threadFunc(LPVOID signal)
{
int* intSignal=reinterpret_cast<int*>(signal);
*intSignal=2;
while(*intSignal!=1)
sleep(1000);
return 0;
}
该线程启动时将intSignal 置为2,然后循环等待直到intSignal 为1 时退出。显然intSignal的值必须在外部被改变,否则该线程不会退出。但是实际运行的时候该线程却不会退出,即使在外部将它的值改为1,看一下对应的伪汇编代码就明白了:
mov ax,signal
label:
if(ax!=1)
goto label
对于C编译器来说,它并不知道这个值会被其他线程修改。自然就把它cache在寄存器里面。记住,C 编译器是没有线程概念的!这时候就需要用到volatile。volatile 的本意是指:这个值可能会在当前线程外部被改变。也就是说,我们要在threadFunc中的intSignal前面加上volatile关键字,这时候,编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取,所作的循环变为如下面伪码所示:
label:
mov ax,signal
if(ax!=1)
goto label
3、Memory
有了上面的知识就不难理解Memory修改描述符了,Memory描述符告知GCC:
1)不要将该段内嵌汇编指令与前面的指令重新排序;也就是在执行内嵌汇编代码之前,它前面的指令都执行完毕
2)不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存。
如果汇编指令修改了内存,但是GCC 本身却察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加“memory”,告诉GCC 内存已经被修改,GCC 得知这个信息后,就会在这段指令之前,插入必要的指令将前面因为优化Cache 到寄存器中的变量值先写回内存,如果以后又要使用这些变量再重新读取。
使用“volatile”也可以达到这个目的,但是我们在每个变量前增加该关键字,不如使用“memory”方便。

‘伍’ 为什么原子操作都起内存屏障的作用

(1) 进行零次或一次对齐内存访问的汇编指令是原子的。

(2) 如果在读操作之后、写操作之前没有其它处理器占用内存总线,那么从内存中读取数据、更新数据并把更新后的数据写回内存中的这些” 读—修改—写”汇编语言指令是原子的。当然,在单处理器系统中,永远都不会发生内存总线窃用的情况。

(3) 操作码前缘是lock字节的” 读—修改—写”汇编语言指令即使在多处理器系统中也是原子的。当控制单元检测到这个前缀时,就”锁定”内存总线,直到这条指令执行完成为止。因此,当加锁的指令执行时,其它处理器不能访问这个内存单元。

(4) 操作码前缀是一个rep字节的汇编语言指令不是原子的,这条指令强行让控制单元多次重复执行相同的指令。控制单元在执行新的循环之前要检查挂起的中断。

在你编写C代码程序时,并不能保证编译器会为a=a+1或甚至像a++这样的操作使用一个原子指令。因此,Linux内核提供了一个专门的atomic_c类型和一些专门的函数和宏(参见表5-4),这些函数和宏作用于atomic_t类型的变量,并当作单独的、原子的汇编语言指令来使用。在多处理器系统中,每条这样的指令都有一个lock字节的前缀。

阅读全文

与linux内存屏障相关的资料

热点内容
c窗口界面编程 浏览:21
hypermill编程能做模板吗 浏览:780
计算机网络最经典的算法 浏览:626
华为思科的配置命令 浏览:869
linux设置光盘启动 浏览:359
程序员俱乐部注册 浏览:17
洗洁精鸡蛋盘解压视频 浏览:291
企业内网加密app 浏览:49
腾讯云服务器怎么设置本地网络 浏览:623
日常程序员 浏览:513
哪个pe有bit加密功能 浏览:108
rust服务器是用什么ip 浏览:692
java随机字符串生成 浏览:551
数码宝贝编程人物 浏览:390
php多用户建站系统 浏览:624
加密防盗地上栓价位 浏览:671
比泽尔压缩机下载 浏览:420
深圳压缩机制造公司 浏览:883
如何给u盘单独文件加密码 浏览:285
恒温机故障源码 浏览:419