首页 > 系统相关 >Linux内核在RISC-V架构下的spinlock实现

Linux内核在RISC-V架构下的spinlock实现

时间:2023-12-22 12:33:51浏览次数:40  
标签:__ lock RISC raw arch Linux spin spinlock

riscv没有sev/wfe指令,当前无法在spinlock时省功耗,只能通过while循环不断检查条件。

本文分析linux内核下对于spinlock的实现,具体到RISC-V体系结构。由于RISC-V体系结构下目前只是简单的实现了一个基于TAS的最基本的spinlock,本文的另一个附加任务就是分析Linux内核为各个平台下实现spinlock搭建起来的通用框架。
这部分内容实质上与体系结构非常相关,属于非常底层的实现,了解这部分内容之前,建议:

  • 阅读RISC-V体系结构的A扩展,理解RISC-V体系结构提供的硬件支持
  • 阅读多线程相关书籍,我认为讲的非常好的一本是《Shared Memory Synchronization》作者是 Michael L. Scott
  • 理解内存模型与缓存一致性的实现

基本入口分析

对于内核通用代码来说,spinlock的入口自然是 include/linux/spinlock.h ,所以从这里入手。这个头文件开头的注释可以看到如下信息:

  • asm/spinlock_types.h 定义了 arch_spinlock_t 和 arch_rwlock_t
  • linux/spinlock_types.h 定义通用类型与其对应的初始化函数
  • asm/spinlock.h 定义 arch_spin_*() 函数
  • linux/spinlock.h 定义通用的接口
    当然以上定义都假定当前为SMP系统,很明显对于UP系统,spinlock是不需要的,全部定义为空函数即可。本文的一切分析假定为SMP系统。
    我们最开始肯定会碰到一个费解的概念: raw_spinlock_t ,这个概念需要从 rt-linux 时代说起。对于 rt-linux ,即 realtime linux ,一个对实时性能相当重要的优化就是将 spinning lock 替换为 sleeping lock 。将这个概念讲简单,就是将spinlock替换为mutex,但是很明显不可能完全进行替换,总有一部分spinlock依赖于spinlock特殊的语义。这时就引入了 raw_spinlock_t , raw_spinlock_t 就是真正的spinlock实现,而原先的 spinlock_t 的语义发生变化:在 PREEMPT_RT 条件下(即 rt-linux )为mutex实现,否则由 raw_spinlock_t 实现。归根节底,要进行分析的是 raw_spinlock_t 。

raw_spinlock_t

raw_spinlock_t的定义位于include/linux/spinlock_types.h,是真正意义上的通用代码实现:

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
#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;

可以看到,在关闭这两个调试选项之后,raw_spinlock_t本质上就剩下了一个arch_spinlock_t,很明显这就是spinlock的架构相关实现。对于这两个调试选项,后续进行单独分析。紧接着可以看到spinlock_t的定义:

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

#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;

前面提到了spinlock_t在PREEMPT_RT下应该定义为mutex,这里并没有体现,这是因为我们分析的主线内核没有打上rt补丁。spinlock_t的初始化macro平淡无奇,忽略。

内核中的raw_spinlock_t相关操作其实有两套实现,其主要区别在于spin状态下是否可以被抢占。内核提供了一个GENERIC_LOCKBREAK配置选项用以配置对应架构下对该特性的支持,当架构下该选项开启时,内核使用可以被抢占的spinlock实现。当内核开启部分spinlock相关调试选项时,也会使用可以被抢占的spinlock实现。

这里说明一下可以被抢占的正确性。我们知道一个CPU持有spinlock时,抢占是被关闭的。因此,不会出现一个CPU持有spinlock后,被抢占,新执行的任务又再次尝试持有该spinlock造成死锁的情况出现。

可抢占实现

该实现定义在kernel/locking/spinlock.c中,且内核使用如下方式切换两种实现:

#if !defined(CONFIG_GENERIC_LOCKBREAK) || defined(CONFIG_DEBUG_LOCK_ALLOC)
/*
 * The __lock_function inlines are taken from
 * spinlock : include/linux/spinlock_api_smp.h
 * rwlock   : include/linux/rwlock_api_smp.h
 */
#else
...

该实现的一个优点是可以利用架构相关的relax实现,优化相关操作:

#ifndef arch_read_relax
# define arch_read_relax(l)	cpu_relax()
#endif
#ifndef arch_write_relax
# define arch_write_relax(l)	cpu_relax()
#endif
#ifndef arch_spin_relax
# define arch_spin_relax(l)	cpu_relax()
#endif

在没有对应的实现时,使用cpu_relax()。BUILD_LOCK_OPS宏用于生成我们想要的实现,本质是一个代码模板。我们来分析这个模板:

#define BUILD_LOCK_OPS(op, locktype)					\
void __lockfunc __raw_##op##_lock(locktype##_t *lock)			\
{									\
	for (;;) {							\
		preempt_disable();					\
		if (likely(do_raw_##op##_trylock(lock)))		\
			break;						\
		preempt_enable();					\
									\
		arch_##op##_relax(&lock->raw_lock);			\
	}								\
}

上面是模板的一部分,生成形如__raw_spin_lock(raw_spinlock_t *lock)类似的函数。实现比较直观,注意到它使用了do_raw_spin_trylock类似的函数。irqsave版本类似:

unsigned long __lockfunc __raw_##op##_lock_irqsave(locktype##_t *lock)	\
{									\
	unsigned long flags;						\
									\
	for (;;) {							\
		preempt_disable();					\
		local_irq_save(flags);					\
		if (likely(do_raw_##op##_trylock(lock)))		\
			break;						\
		local_irq_restore(flags);				\
		preempt_enable();					\
									\
		arch_##op##_relax(&lock->raw_lock);			\
	}								\
									\
	return flags;							\
}

但是带bh后缀的要注意一下:

void __lockfunc __raw_##op##_lock_bh(locktype##_t *lock)		\
{									\
	unsigned long flags;						\
									\
	/*							*/	\
	/* Careful: we must exclude softirqs too, hence the	*/	\
	/* irq-disabling. We use the generic preemption-aware	*/	\
	/* function:						*/	\
	/**/								\
	flags = _raw_##op##_lock_irqsave(lock);				\
	local_bh_disable();						\
	local_irq_restore(flags);					\
}	\

注意这里关闭bottom half时,同样需要关闭硬件中断。再关注一下前缀的区别,即_raw_与__raw的区别。实际上_raw前缀的函数都是由对应__raw前缀的函数实现,但是可以通过对应的CONFIG控制是否进行内联操作:

#ifndef CONFIG_INLINE_SPIN_LOCK
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}
EXPORT_SYMBOL(_raw_spin_lock);
#endif

#ifdef CONFIG_INLINE_SPIN_LOCK
#define _raw_spin_lock(lock) __raw_spin_lock(lock)
#endi

注意这个__lockfunc前缀,其定义如下:
#define __lockfunc __attribute__((section(".spinlock.text")))
其实际用途就是将对应的函数放到名为.spinlock.text的section中,很明显这样同时阻止了编译器将其内联化。最后要注意上面是存在_raw_spin_lock这个导出的symbol的。

do_raw_spin_trylock其实也有两套实现,debug版本和非debug版本,这里只看非debug版本:

static inline int do_raw_spin_trylock(raw_spinlock_t *lock)
{
	int ret = arch_spin_trylock(&(lock)->raw_lock);

	if (ret)
		mmiowb_spin_lock();

	return ret;
}

这里注意mmiowb_spin_lock的作用,后续分析I/O操作时会看到其具体用途。之后,可能还有一个疑问,对应的unlock操作到哪里去了?很明显unlock不需要区分是否可以抢占,所以unlock的实现只有一份,在下面会提到。

不可抢占实现

前面看到这两个实现是不共存的,因此在spinlock_api_smp.h中可以看到不可抢占的实现:

#if !defined(CONFIG_GENERIC_LOCKBREAK) || defined(CONFIG_DEBUG_LOCK_ALLOC)
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
	unsigned long flags;

	local_irq_save(flags);
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	/*
	 * On lockdep we dont want the hand-coded irq-enable of
	 * do_raw_spin_lock_flags() code, because lockdep assumes
	 * that interrupts are not re-enabled during lock-acquire:
	 */
#ifdef CONFIG_LOCKDEP
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
#else
	do_raw_spin_lock_flags(lock, &flags);
#endif
	return flags;
}
...

RISC-V的实现

事实上,这套实现与lockdep框架深度集成。
目前RISC-V用的这套实现是相当简单的,基本就是拍脑门就能想出来的。内核中稍有复杂度的实现为ticket-based spinlock和queue spinlock,日后RISC-V采用了这些实现之后,可以进行分析。使用这个简单实现的spinlock,是因为目前的RISC-V处理器的实现似乎都是低性能,且核心数较少的实现,不需要过度考虑性能以及公平性带来的影响。相信后面RISC-V应用于PC或者服务器系统时,会采用高性能的qspinlock实现。

RISC-V实现的spinlock本质上是一个基于TAS(Test and Set)的spinlock,利用到了RISC-V提供的硬件原子操作指令。因此这个锁的定义是相当简单的,本质上就是一个整数:

typedef struct {
        volatile unsigned int lock;
} arch_spinlock_t;

#define __ARCH_SPIN_LOCK_UNLOCKED       { 0 }

当lock为0时,表示这个锁是可用的。
arch_spin_lock
该函数相当简单:

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
        while (1) {
                if (arch_spin_is_locked(lock))
                        continue;

                if (arch_spin_trylock(lock))
                        break;
        }
}

不过理解为什么这么写性能有优势则需要理解缓存的MESI协议。MESI协议中,一个内存位置的属性可以为E或者S,很明显将一个内存位置设置为E(Exclusive)的开销远远大于将其设置为S(Shared)的开销。因此函数中首先使用arch_spin_is_locked检测这个整数的值,如下:
#define arch_spin_is_locked(x) (READ_ONCE((x)->lock) != 0)
此时仅仅是请求其进入S状态,如果发现没有人已经拿到了该锁,才会正式使用原子指令对该内存地址进行E状态的访问,即Exclusive访问,这很明显减小了开销,因为在已有CPU持有锁的情况下其它CPU是不会发出原子指令的。随后使用amoswap指令进行swap操作,该操作将目标寄存器中保存地址指向的内存值取出,然后替换为一个新的值,并将原先取出的值作为返回值取出。也就是说arch_spin_trylock的操作本身就是:通过amoswap将1写入lock->lock,检查其原有的值是不是0,如果是0,则认为自己抢到了(因为它第一个写入了1),否则,认为自己没有抢到。当然,在这之后需要使用fence指令来一个acquire语义。

arch_spin_unlock

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
        smp_store_release(&lock->lock, 0);
}

Ref

https://crab2313.github.io/post/riscv-spinlock/#risc-v的实现
https://www.francisz.cn/2022/10/27/riscv-spinlock-rwlock/

标签:__,lock,RISC,raw,arch,Linux,spin,spinlock
From: https://www.cnblogs.com/lvzh/p/17921345.html

相关文章

  • linux下域名解析工具
    1、dig[root@hlcc_mastermysqlinit]#digwww.baidu.com;<<>>DiG9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.13<<>>www.baidu.com;;globaloptions:+cmd;;Gotanswer:;;->>HEADER<<-opcode:QUERY,status:NOERROR,id:723......
  • linux 如何删掉一些特殊字符的文件夹,比如带有空格,单引号,双引号的
    在Linux中删除包含特殊字符(如空格、单引号、双引号)的文件夹,你需要使用转义字符或引号来正确地指定文件夹名。以下是一些常用方法:使用反斜杠(\)转义特殊字符:如果文件夹名包含空格,比如MyFolder,你可以使用\来转义空格:rm-rMy\Folder/对于包含单引号或双引号的文件夹,同样......
  • 26k star!Linux不熟悉,快用这个帮助你
    作为一个IT从业者,熟练使用Linux是一项非常重要的能力,不过我想问问电脑前的你,你真的对Linux命令很熟悉吗?我想答案可不一定都是肯定的,说实话笔者就经常不记得改用什么命令或具体怎么用。今天我们推荐一款涵盖了大部分的Linux命令的开源项目,在GitHub已超过23KStar,它就是:linux-command......
  • 上手 Linux:快速入门 Vim
    Vi和VimVi是Linux操作系统默认的文本编辑器。Vim是Vi的加强版。不仅可以做简单的文字编辑,它也有颜色高亮,语法提示等特性,可以用作编程。现在大部分的Linux发行版都同时包含Vi和Vim。在Linux操作系统中,Vim是必须掌握的一个工具:作为命令行模式下的文本编辑器非常好用......
  • linux -bash: fork: retry: No child processes 解决
    Text.Text.Text.Text.[ubuntu@iZj6cc8f0831kkdffvlhjhZtools]$date-bash:fork:retry:Nochildprocesses-bash:fork:retry:Nochildprocesses-bash:fork:retry:NochildprocessesFriDec2209:56:49CST2023corefilesize(blocks,-c)......
  • Windows, Mac, Linux 上修改文件【修改时间】属性的方法(命令行方法)
    某些公司比较恶心,忘记打卡要补卡时需要提供工作记录,以证明你在下班时间点后还在公司的记录,一般是参考电脑文件属性里的【修改时间】。有时并不能找到带有合适修改时间的文件,毕竟,正经人谁下班之后还工作啊!这时,可以使用命令行修改【修改时间】这个属性。Windows上打开CMD,Mac/Linu......
  • Ubuntu18下实时Linux内核的编译安装记录(保姆级)
    本人系统是虚拟机上的ubuntu18,过程参考了以下3个链接:https://blog.csdn.net/huangjunsheng123/article/details/116202848https://blog.51cto.com/u_15899439/5907513https://kunaly.blog.csdn.net/article/details/101111502?spm=1001.2101.3001.6650.3&utm_medium=distribute......
  • Linux下安装Java环境三种方式(tar.gz、rpm、yum)
    一、Java软件包下载Java各版本下载地址;进入页面后我们首先看到的是Java最新版本的下载界面,但是在这我需要下载Java8的版本,所以得划到下面的界面,即界面如下:在这里我将下载:jdk-8u381-linux-x64.tar.gz(压缩包方式安装)、jdk-8u381-linux-x64.rpm(RPM方式安装)注:在下载界面的下面一......
  • Linux 定位服务器硬盘槽位的方法
    版权声明:原创作品,谢绝转载!否则将追究法律责任。—————作者:kirinLinux定位服务器硬盘槽位的方法1、安装sas3ircu工具2、获取磁盘SN号码2.1、使用smartctl命令获取smartctl-a/dev/sd*|grepSerial2.2、查看文件获取ll/dev/disk/by-id/|grepsd*3、通过硬盘S......
  • 工作中常用的一些Linux指令,简单易记还实用(三)
    成功路上最大的困难就是坚持,每天坚持看一道算法,每周坚持看一本好书!工作中,离不开Linux系统,很多刚步入职场的小白,往往对于Linux操作系统的使用都显得生疏,最主要的就是对一些常用的指令记忆不熟练,下面就罗列一些使用度比较高的一些Linux指令,一起来巩固学习一下。这一部分的指令就稍......