操作系统概念-内核同步-原子操作

       在同一个操作系统中,不同的进程经常需要相互协同工作,协同的方法一般有两种,一是直接共享逻辑地址空间,二是通过文件或消息共享数据。如果共享逻辑地址空间,则在进程执行的时候有可能会发生多个进程同时访问同一个数据的冲突问题,特别是在多处理器的情况下。对于这类冲突,内核采用了一些方法进行进程同步,例如原子操作、自旋锁、信号量等方法。接下来的四篇(包括本文)将分别介绍原子操作、自旋锁、信号量和死锁的一些概念,同时以Linux4.8.1版本的内核代码(x86架构部分)为例进行分析。

临界区

       临界区(critical-section)是解决进程协作的一个方法。将多个进程可能修改同一个共享变量的代码段设为临界区,当有进程进入临界区后,其他进程会被禁止进入,直到前一个进程离开临界区,其他进程才可以进入。即同一时刻只允许一个进程位于临界区内。伪代码形式可以表示为:

do{
	//进入区
	//临界区
	//退出区
	//剩余区
}while(TRUE)

临界区的实现需要满足以下三个条件:

1,   互斥,即同一时刻只能有一个进程位于临界区内;

2,   前进,当多个进程同时等待进入临界区的时候,会有一个进程被选择进入;

3,   有限等待,在进入区等待的进程必须在有限时间后进入临界区。

作为所有软件都可能会调用的操作系统,同一时刻内同一段代码可能会有多个进程在执行,而像文件读写、硬件调用等操作都是排他性的,因此操作系统更应该做好临界区的设置。

对于操作系统的临界区实现,要分为抢占内核和非抢占内核来讨论。显然,非抢占内核不存在竞争的问题,因为在临界区内的进程不会被打断,除非进程主动退出。对抢占内核来说,就需要硬件或者软件(算法)上的支持来实现临界区。

软件支持的一个例子是Peterson算法。Peterson算法的精髓在于用两个变量(或数组)来记录当前是否有进程位于临界区以及哪个进程位于临界区,这样通过在进入区检测并设置标记、退出区恢复标记可以实现临界区排他的特性。

硬件支持的方法是锁,从底层硬件的层面来看则是实现原子操作。进程在进入临界区前检测并申请锁,离开后释放锁。原子操作保证锁的正常运行。

原子操作

       原子操作指的是不可再分的指令操作,即在执行原子操作时不可能被打断,要么原子操作没有执行,要么已经执行完毕。

       原子操作的实现必须需要硬件的支持,操作系统仅仅是在硬件指令的基础之上进行一次封装。对于没有实现原子操作的硬件,则需要操作系统从软件算法层面进行支持。

linux下的实现

       linux下原子操作的数据结构是atomic_t,其定义放在下:

typedef struct {
	int counter;
}atomic_t;

可以看出原子操作仅支持int整形变量,为了与正常的整形区分,采用atomic_t来定义这个原子操作。

       原子操作的接口都在Atomic.h中列出(64位版本则在Atomic64.h)。包括:

原子操作接口

说明

int atomic_read(const atomic_t *v)

获取atomic_t类型变量的值

void atomic_set(atomic_t *v, int i)

设置atomic_t类型变量的值

void atomic_add(int i, atomic_t *v)

增加i

void atomic_sub(int i, atomic_t *v)

减少i

bool atomic_sub_and_test(int i, atomic_t *v)

减少i,如果结果为0则返回true

void atomic_inc(atomic_t *v)

等价于 atomic_add(1, v)

void atomic_dec(atomic_t *v)

等价于 atomic_sub(1, v)

bool atomic_dec_and_test(atomic_t *v)

atomic_sub(1, v),如果结果为0则返回true

bool atomic_inc_and_test(atomic_t *v)

atomic_add(1, v),如果结果为0则返回true

bool atomic_add_negative(int i, atomic_t *v)

增加i,如果结果为负则返回true

int atomic_add_return(int i, atomic_t *v)

增加i并返回结果

int atomic_sub_return(int i, atomic_t *v)

减少i并返回结果

atomic_inc_return(v)

宏,增加1并返回结果

atomic_dec_return(v)

宏,减少1并返回结果

int atomic_fetch_add(int i, atomic_t *v)

增加i,并返回原来的结果

int atomic_fetch_sub(int i, atomic_t *v)

减少i,并返回原来的结果

int atomic_cmpxchg(atomic_t *v, int old, int new)

比较old与v中的值,若相等则将new付给v,返回v原来的值

intatomic_xchg(atomic_t *v, int new)

将new付给v,并返回v原来的值

在这些原子操作中,为了调用底层架构实现的原子操作,加入了很多汇编语言及编译限制的关键字。比如 atomic_read中用到的READ_ONCE这个宏。

#defineREAD_ONCE(x) __READ_ONCE(x, 1)
#define__READ_ONCE(x, check)						\
({									\
	union { typeof(x) __val; char __c[1]; } __u;			\
	if (check)							\
		__read_once_size(&(x), __u.__c, sizeof(x));		\
	else								\
		__read_once_size_nocheck(&(x), __u.__c, sizeof(x));	\
	__u.__val;							\
})
void __read_once_size(constvolatilevoid *p, void *res, intsize)
{
	__READ_ONCE_SIZE;
}
void __read_once_size(constvolatilevoid *p, void *res, intsize)
{
	__READ_ONCE_SIZE;
}
#define__READ_ONCE_SIZE						\
({									\
	switch (size) {							\
	case 1: *(__u8 *)res = *(volatile __u8 *)p; break;		\
	case 2: *(__u16 *)res = *(volatile __u16 *)p; break;		\
	case 4: *(__u32 *)res = *(volatile __u32 *)p; break;		\
	case 8: *(__u64 *)res = *(volatile __u64 *)p; break;		\
	default:							\
		barrier();						\
		__builtin_memcpy((void *)res, (constvoid *)p, size);	\
		barrier();						\
	}								\
})

READ_ONCE宏是为了实现变量的一次性读。同理还有WRITE_ONCE宏。

还有一些操作用到了asm关键字内嵌汇编的做法,针对不同架构直接调用汇编语句,比如x86架构下的atomic_add这个函数:

static __always_inline void atomic_add(int i, atomic_t *v)
{
	asm volatile(LOCK_PREFIX "addl %1,%0"
		: "+m" (v->counter)
		: "ir" (i));
}

相当于用汇编语言执行

addl i, v->counter

关键字volatile确保了编译器不会对这个语句进行优化,即不会将这个语句拆分为多个汇编语句。在上面这个例子中,通过添加addl语句的前缀LOCK_PREFIX,保证多处理器的情形下调用x86架构下的原子addl汇编语句,而单处理器的情况下直接调用addl就足够了。顺便说一句,目前绝大多数架构的读-修改-写操作都是支持原子操作的。

LOCK_PREFIX前缀被定义为:

#ifdef CONFIG_SMP
#define LOCK_PREFIX_HERE \
		".pushsection .smp_locks,\"a\"\n"	\
		".balign 4\n"				\
		".long 671f - .\n"/* offset */		\
		".popsection\n"				\
		"671:"
#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
#else/* ! CONFIG_SMP */
#defineLOCK_PREFIX_HERE""
#defineLOCK_PREFIX""
#endif

即,在单处理器的情况下,LOCK_PREFIX为空,不需要前缀(因为单处理器的操作不会被其他处理器打断)。多处理器的情况下,LOCK_PREFIX是lock前缀。即调用x86架构的原子操作lockaddl。

64位的原子操作类似,只是在函数名上加了“64”,功能不变。

除了这些对整形的基本运算,还有原子位操作,接口在中。

原子位操作接口

说明

void set_bit(long nr, volatile unsigned long *addr)

设置addr地址的第nr位

void clear_bit(long nr, volatile unsigned long *addr)

清空addr地址的第nr位

void change_bit(long nr, volatile unsigned long *addr)

翻转addr地址的第nr位

bool test_and_set_bit(long nr, volatile unsigned long *addr)

设置addr地址的第nr位,并返回原来的值

bool test_and_set_bit_lock(long nr, volatile unsigned long *addr)

同上

bool test_and_clear_bit(long nr, volatile unsigned long *addr)

清空addr地址的第nr位,并返回原来的值

bool test_and_change_bit(long nr, volatile unsigned long *addr)

翻转addr地址的第nr位,并返回原来的值

test_bit(nr, addr)

宏,返回addr的第nr位是否为1


这些函数的实现方法同样是采用内嵌汇编调用底层架构的原子操作,思想与前面介绍的函数一致。

原子操作在操作系统中的使用。

       在中,互斥体mutex就用到了atomic_t。

structmutex {
	/* 1: unlocked, 0: locked, negative: locked, possible waiters */
	atomic_t		count;
	spinlock_t		wait_lock;
	structlist_head	wait_list;
#ifdefined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER)
	struct task_struct	*owner;
#endif
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
	struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
#ifdef CONFIG_DEBUG_MUTEXES
	void			*magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map	dep_map;
#endif
};

在这里,上锁标记位的操作必须是原子的,因此采用atomic_t类型的变量。

       在中,多功能设备(MFD)的结构体mdf_cell也用到了atomic_t。

structmfd_cell {
	constchar		*name;
	int			id;
	/* refcounting for multiple drivers to use a single cell */
	atomic_t		*usage_count;
	int(*enable)(structplatform_device *dev);
	int(*disable)(structplatform_device *dev);

	int(*suspend)(structplatform_device *dev);
	int(*resume)(structplatform_device *dev);
	//...
}

usage_count用于记录该cell被使用的情况。

除此之外,在devices、nfs等很多地方都用到了atomic_t,都是用来保证操作的原子性。下面介绍的自旋锁也同样用到了原子操作。在中:

extern int _atomic_dec_and_lock(atomic_t *atomic,spinlock_t *lock);

该函数的作用是将atomic原子地递减1,如果结果为0,将lock上锁并返回true,否则返回false。

你可能感兴趣的:(操作系统相关)