驱动程序中的并发与控制(二)

news/2024/7/6 6:32:31

驱动程序中的并发与控制(一)
驱动程序中的并发与控制(二)
驱动程序中的并发与控制(三)

自旋锁spin_lock

说明:下面的源码分析来自内核版本4.9.88

设计自旋锁的最初目的是在多处理器系统中提供对共享数据的保护,其名称来源于它的工作方式。

为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量。由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”。

不同的处理器上有不同的指令用以实现上述的原子操作,所以spin_lock的相关代码在不同体系架构上有不同的实现,下面以ARM处理器上的实现为例,仔细考察spin_lock的幕后行为。

下面是Linux源码中提供给设备驱动程序等内核模块使用的spin_lock接口函数的定义:

static __always_inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}

spinlock_t的定义如下:

/* include/linux/spinlock_types.h */
typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

//如果定义了CONFIG_DEBUG_LOCK_ALLOC
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
		struct {
			u8 __padding[LOCK_PADSIZE];
			struct lockdep_map dep_map;
		};
#endif
	};
} spinlock_t;


/* include/linux/spinlock_types.h */
typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;

#ifdef CONFIG_GENERIC_LOCKBREAK
	unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} raw_spinlock_t;

arch_spinlock_t的定义不同的架构可能有不同的实现,对于ARM架构则为:

/* arch/arm/include/asm/spinlock_types.h */
typedef struct {
	union {
		u32 slock;
		struct __raw_tickets {
#ifdef __ARMEB__
			u16 next;
			u16 owner;
#else
			u16 owner;
			u16 next;
#endif
		} tickets;
	};
} arch_spinlock_t;

上述的__raw_tickets结构体中有owner、next两个成员,这是在SMP系统中实现spinlock的关键。

spin_lock函数中调用的raw_spin_lock是个宏,它的定义如下:

/* include/linux/spinlock.h */

#define raw_spin_lock(lock)	_raw_spin_lock(lock)

_raw_spin_lock的定义不同的CPU系统中实现不一致,在UP(单CPU) 系统中实现如下:

/* include/linux/spinlock_api_up.h */
#define _raw_spin_lock(lock)			__LOCK(lock)

/* include/linux/spinlock_api_up.h */
#define __LOCK(lock) \
  do { preempt_disable(); ___LOCK(lock); } while (0)
       //禁止抢占

/* include/linux/spinlock_api_up.h */
#define ___LOCK(lock) \
  do { __acquire(lock); (void)(lock); } while (0)

/* include/linux/compiler.h */
# define __acquire(x) (void)0

从以上代码可知,在UP系统中spin_lock()就退化为preempt_disable(),如果用的内核不支持preempt,那么 spin_lock()什么事都不用做。
为什么需要关闭系统的可抢占性呢?在一个支持抢占特性的Linux系统中,一个在内核态执行的进程也有可能被切换岀处理器,典型地,比如当前进程正在内核态执行某一系统调用时,发生了一个外部中断,当中断处理函数返回时,因为内核的可抢占性,此时将会出现一个调度点,如果CPU的运行队列中出现了一个比当前被中断进程优先级更高的进程,那么被中断的进程将会被换出处理器,即便此时它正运行在内核态。单处理器上的这种因为内核的可抢占性所导致的两个不同进程并发执行的情形,非常类似于SMP系统上运行在不同处理器上的进程之间的并发,因此为了保护共享的资源不会受到破坏,必须在进入临界区前关闭内核的可抢占性。对于不开启的抢占Linux系统中,只要在内核态,就不会发生进程调度。

_raw_spin_lock的定义在SMP系统中定义如下:

/* kernel/locking/spinlock.c */
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}

/* include/linux/spinlock_api_smp.h */
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	//关闭抢占
	preempt_disable();  

	//定义了CONFIG_DEBUG_LOCK_ALLOC相关的,不是重点就不分析了
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);


	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

/* include/linux/lockdep.h 
在未定义CONFIG_LOCK_STAT的时候,LOCK_CONTENDED为:
*/
#define LOCK_CONTENDED(_lock, try, lock) \
	lock(_lock) /* 即do_raw_spin_lock(_lock) */


而do_raw_spin_lock代码如下:

/* include/linux/spinlock.h */
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
	__acquire(lock);
	arch_spin_lock(&lock->raw_lock);
}

arch_spin_lock的实现就与CPU架构相关了,在真正讨论底层实现之前,先来说说一个问题。假如有 CPU0、 CPU1、 CPU2都调用spin_lock想获得临界资源,那么谁先得到这个锁呢?这里要确保保证公平,先到先得,谁先申请谁先获得。
举个现实中的例子。餐厅里只有一个座位,去吃饭的人都得先取号、等叫号。注意,有2个动作:顾客从取号机取号,电子叫号牌叫号。
① 一开始取号机待取号码为0;

② 顾客A从取号机得到号码0,电子叫号牌显示0,顾客A上座;

取号机显示下一个待取号码为1。

③ 顾客B从取号机得到号码1,电子叫号牌还显示为 0,顾客B等待;

取号机显示下一个待取号码为2。

④ 顾客C从取号机得到号码2,电子叫号牌还显示为0,顾客C等待;

取号机显示下一个待取号码为3。

⑤ 顾客A吃完离座,电子叫号牌显示为1,顾客B的号码等于1,他上座;

⑥ 顾客B吃完离座,电子叫号牌显示为2,顾客C的号码等于2,他上座;

在这个例子中有3个号码:取号机显示的“下一个号码”,顾客取号后它会自动加1;电子叫号牌显示“当前号码”,顾客离座后它会自动加1。某个客户手上拿到的号码等于电子叫号牌的号码时,该客户上座。在这个过程中,即使顾客 B、 C 同时到店,只要保证他们从取号机上得到的号码不同,他们就不会打架。所以,关键点在于:取号机的号码发放,必须互斥,保证客户的号码互不相同。而电子叫号牌上号码的变动不需要保护,只有顾客离开后它才会变化,没人争抢它。
在SMP系统中,linux自旋锁实现就如上述描述的例子。在ARM平台上,spinlock_t和arch_spin_lock的实现如下:
在这里插入图片描述
owner就相当于电子叫号牌,现在谁在吃饭。 next就当于于取号机,下一个号码是什么。每一个 CPU从取号机上取到的号码保存在spin_lock函数中的局部变量里。

/* arch/arm/include/asm/spinlock.h */
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned long tmp;
	u32 newval;
	arch_spinlock_t lockval;

	prefetchw(&lock->slock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%3]\n"  //1.取号,lockval = lock->slock,owner和next都读出来
"	add	%1, %0, %4\n"    //2.取号机的号码加1,newval = lockval + (1<<TICKET_SHIFT),即next++
"	strex	%2, %1, [%3]\n"//3.新号码写回取号机,lock->slock = newval,不一定写成功 
"	teq	%2, #0\n"        //4.如果123过程中被人先取号了,我的写回操作失败,那就重新取号
"	bne	1b"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
	: "cc");
  
  	//5.手上的号码不等于电子叫号牌的话
	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe(); //6.就原地休息一会,cpu低功耗运行
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); //7.时不时看看电子叫号牌
	}
  	//8.能运行到这里,表示我手中的号码等于电子叫号牌了
	smp_mb();
}

与spin_lock相对的是spin_unlock函数,这是一个应该在离开临界区时调用的函数,用来释放此前获得的自旋锁。其外部接口定义如下:

/* include/linux/spinlock.h */
static __always_inline void spin_unlock(spinlock_t *lock)
{
	raw_spin_unlock(&lock->rlock);
}

#define raw_spin_unlock(lock)		_raw_spin_unlock(lock)

在UP系统上,_raw_spin_unlock的实现为:

/* include/linux/spinlock_api_up.h */
#define _raw_spin_unlock(lock)			__UNLOCK(lock)

#define __UNLOCK(lock) \
  do { preempt_enable(); ___UNLOCK(lock); } while (0)

在单CPU的系统上,spin_unlock即为关闭抢占。

在SMP系统上,_raw_spin_unlock的实现为:

#define _raw_spin_unlock(lock) __raw_spin_unlock(lock)

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	preempt_enable();  //开启抢占
}

/* include/linux/spinlock.h */
static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
{
	arch_spin_unlock(&lock->raw_lock);  //不同CPU架构实现有所不同
	__release(lock);
}

//对于ARM架构的CPU,arch_spin_unlock实现如下:
/* arch/arm/include/asm/spinlock.h */
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	smp_mb();
	lock->tickets.owner++;  //即叫号牌+1,轮到下一个排队的顾客
	dsb_sev();
}

spin_lock的变体

在前面讨论spin_lock函数时,spin_lock对多处理器系统中这种进程间真正的并发执行引起的竞态问题解决得很好,但是考虑下图所示这样的一个场景:
在这里插入图片描述
处理器上的当前进程A因为要对某一全局性的链表g_list进行操作,所以在操作前通过调用spin_lock来进入临界区(图中标号1所示),当它正处于临界区中时,进程A所在的处理器上发生了一个外部硬件中断,此时系统必须暂停当前进程A的执行转而去处理该中断(图中标号2所示),假设该中断的处理例程中恰好也要操作g_list,因为这是一个共享的全局变量,所以在操作之前也要调用spin_lock函数来对该共享变量进行保护(图中标号3所示),当中断处理例程中的spin_lock试图去获得自旋锁slock时,因为被它中断的进程A之前已经获得该锁,于是将导致中断处理例程进入自旋状态。在中断处理例程中出现一个自旋状态是非常致命的,因为中断处理例程必须在尽可能短的时间内返回,而此时它却必须自旋。同时被它中断的进程A因中断处理函数不能返回而无法恢复执行,也就不可能释放锁,所以将导致中断处理例程中的spin_lock一直自旋下去,导致死锁。

对这种问题的解决导致了spin_lock函数其他变体的出现。因处理外部的中断而引发spin_lock缺陷的例子,于是出现了spin_lock_irq和spin_lock_irqsave函数,对比正常spin_lock函数,这两个除了关闭抢占还会关闭本CPU的中断。spin_lock_irq函数定义如下所示:

/* include/linux/spinlock.h */
static __always_inline void spin_lock_irq(spinlock_t *lock)
{
	raw_spin_lock_irq(&lock->rlock);
}

#define raw_spin_lock_irq(lock)		_raw_spin_lock_irq(lock)

/* kernel/locking/spinlock.c */
void __lockfunc _raw_spin_lock_irq(raw_spinlock_t *lock)
{
	__raw_spin_lock_irq(lock);
}

/* include/linux/spinlock_api_smp.h */
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
	local_irq_disable();  //关闭中断
	preempt_disable();    //关闭抢占
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

关闭本地处理器响应外部中断的能力,这样在获取一个锁时就可以确保不会发生中断,从而避免上面提到的死锁问题。local_irq_disable只能用来关闭本地处理器的中断,当一个通过调用spin_lock_irq拥有自旋锁V的进程在处理器A上执行时,虽然在处理器A上中断被关闭了, 但是外部中断依然有机会发送到处理器B上,如果处理器B上的中断处理函数也试图去获得锁V,情况会怎样呢?因为此时处理器A上的进程可以继续执行,在它离开临界区时将释放锁,这样处理器B上的中断处理函数就可以结束此前的自旋状态。这从一个侧面说明通过自旋锁进入的临界区代码必须在尽可能短的时间内执行完毕,因为它执行的时间越长,别的处理器就越需要自旋以等待更长的时间(尤其是这种自旋发生在中断处理函数中),最糟糕的情况是进程在临界区中因为某种原因被换出处理器。所以作为使用自旋锁时一条确定的规则,任何拥有自旋锁的代码都必须是原子的,不能休眠。

如此,当知道一个自旋锁在中断处理的上下文中有可能会被使用到时,应该使用spin_lock_irq函数,而不是spin_lock,后者只有在能确定中断上下文中不会使用到自旋锁的情形下才能使用。

spin_lock_irq对应的释放锁函数为spin_unlock_irq,这里就不细说了,无非就是开中断、抢占。

与spin_lock_irq类似的还有一个spin_lock_irqsave宏,它与spin_lock_irq函数最大的区别是,在关闭中断前会将处理器当前的中断状态寄存器的值保存在一个变量中,当调用对应的spin_unlock_irqrestore来释放锁吋,会将spin_lock_irqsave中保存的中断状态值重新写冋到寄存器中。


http://www.niftyadmin.cn/n/3657317.html

相关文章

驱动程序中的并发与控制(三)

驱动程序中的并发与控制&#xff08;一&#xff09; 驱动程序中的并发与控制&#xff08;二&#xff09; 驱动程序中的并发与控制&#xff08;三&#xff09; 信号量(semaphore) 相对于自旋锁&#xff0c;信号量的最大特点是允许调用它的线程进入睡眠状态。 信号量的定义与初…

Asp.net 2.0的异常处理需要考虑的问题

在.NET 1.1, 只有主线程中未处理的异常才会终止应用程序的运行,其他的线程的异常.CLR会处理,因此你的应用程序可以正常运行.在 .NET 2.0, 任何线程上的未处理的异常都可能终止应用程序的运行 (具体信息参看Exceptions In Managed Threads ). 这对于Asp.net开发者来说,特别在将1…

Linux i2c驱动框架分析 (一)

Linux i2c驱动框架分析 &#xff08;一&#xff09; Linux i2c驱动框架分析 &#xff08;二&#xff09; Linux i2c驱动框架分析 &#xff08;三&#xff09; 通用i2c设备驱动分析 Linux的i2c体系结构 Linux的i2c体系结构分为3个组成部分。 (1) i2c核心 i2c核心提供了i2c总线…

Linux i2c驱动框架分析 (二)

Linux i2c驱动框架分析 &#xff08;一&#xff09; Linux i2c驱动框架分析 &#xff08;二&#xff09; Linux i2c驱动框架分析 &#xff08;三&#xff09; 通用i2c设备驱动分析 i2c core i2c核心&#xff08;drivers/i2c/i2c-core.c&#xff09;中提供了一组不依赖于硬件平…

BOOT,FAT16结构

以下资料仅供参考:----------------------------------------------------------------------------------------目录项(Directory Entries)文件属性字节(File attribute byte)FAT16结构(FAT16 structure)磁盘引导记录结构(BOOT record layout)目录项(Directory Entries)offset…

Tip - SQL报表打印的空白页问题

SQL报表中一个常见问题是&#xff1a;在HTML格式中报表看起来还不错&#xff0c;但是打印出来&#xff08;或者在PDF格式中&#xff09;却发现每一个页面后面都跟着一个空白页。这是因为报表的设计尺寸超过了打印页面的物理尺寸。那么如何设置报表的尺寸适合打印呢&#xff1f;…

Linux i2c驱动框架分析 (三)

Linux i2c驱动框架分析 &#xff08;一&#xff09; Linux i2c驱动框架分析 &#xff08;二&#xff09; Linux i2c驱动框架分析 &#xff08;三&#xff09; 通用i2c设备驱动分析 i2c适配器驱动 i2c适配器驱动加载与卸载 i2c总线驱动模块的加载函数要完成两个工作。 初始化…

.NET 格式字符串速查

.NET编程及SQL报表中常用到数字或者日期的格式转化&#xff0c;那些格式字符总是记不住&#xff0c;索性列出来备查&#xff1a;Standard numeric format strings: http://msdn2.microsoft.com/en-us/library/aa720653.aspxCustom numeric format strings: http://msdn2.micros…