71|0

504

帖子

0

TA的资源

纯净的硅(初级)

楼主
 

《Linux内核深度解析》--内核互斥技术 [复制链接]

本帖最后由 dirty 于 2025-1-22 12:06 编辑

      本篇以笔记形式记录学习内核互斥技术,并做一定拓展学习。

      在Linux内核中,可能出现多个进程、进程和硬中断、进程和软中断、多个处理器访问同一个对象等现象,我们需要使用互斥技术,确保在给定的时刻只有一个主体可以进入临界区访问对象。

      如果临界区的执行时间比较长或者可能睡眠,可以使用下面这些互斥技术。

●信号量,大多数情况下我们使用互斥信号量。
●读写信号量。
●互斥锁。
●实时互斥锁。

      如果临界区的执行时间很短,并且不会睡眠,可以使用下面这些互斥技术。

●原子变量。
●自旋锁。
●读写自旋锁,它是对自旋锁的改进,允许多个读者同时进入临界区。
●顺序锁,它是对读写自旋锁的改进,读者不会阻塞写者。

      进程还可以使用下面的互斥技术。
●禁止内核抢占,防止被当前处理器上的其他进程抢占,实现和当前处理器上的他进程互斥。
●禁止软中断,防止被当前处理器上的软中断抢占,实现和当前处理器上的软中互斥。
●禁止硬中断,防止被当前处理器上的硬中断抢占,实现和当前处理器上的硬中互斥。

 

信号量

      信号量(semaphore) 是进程和线程间控制共享资源访问的重要机制, 用于同步操作。信号量本质上是一个计数器, 用来跟踪资源的可用数量, 并通过增减信号量的值来控制对共享资源的访问权。 信号量的值可以理解为资源的数量, 信号量为 0 时表示资源已被占用, 当信号量为正数时表示资源可用。

      内核使用的信号量结构体如下

include/linux/semaphore.h

struct semaphore {
	raw_spinlock_t		lock;		//自旋锁,用来保护信号量的其他成员。
	unsigned int		count;		//计数值,表示还可以允许多少个进程进入临界区
	struct list_head	wait_list;	//等待进入临界区的进程链表。
};

      初始化静态信号量的方法:

(1)SEMAPHORE INITIALIZER(name,n):指定名称和计数值,允许n个进程同时进入临界区。
(2)DEFINE SEMAPHORE(name):初始化一个互斥信号量。

      在运行时动态初始化信号量的方法:

static inline void sema init(struct semaphore *sem, int val); //参数 val指定允许同时进入临界区的进程数量

      获取信号量函数如下:

void down(struct semaphore *sem); //获取信号量,如果计数值是0,进程深度睡眠
int down_interruptible(struct semaphore *sem);//获取信号量,如果计数值是0,进程轻度睡眠。
int down_killable(struct semaphore *sem);//获取信号量,如果计数值是0,进程中度睡眠。
int down_trylock(struct semaphore *sem);//获取信号量,如果计数值是0,进程不等待。
int down_timeout(struct semaphore *sem, long jiffes),//获取信号量,指定等待的时间。
void up(struct semaphore*sem);//释放信号量

 

读写信号量

      读写信号量是对互斥信号量的改进,允许多个读者同时进入临界区,读者和写者互斥猪和写者互斥,适合在以读为主的情况使用。

      初始化静态读写信号量的方法:

DECLARE RWSEM(name);

      在运行时动态初始化读写信号量的方法:

init rwsem(sem);

      申请读锁的函数如下:

void down_read(struct rw semaphore *sem);//申请读锁,如果写者占有写锁或者正在等待写锁,那么进程深度睡眠。
int down_read_trylock(struct rw semaphore *sem);//尝试申请读锁,不会等待。如果申请成功,返回1;否则返回0。
void up_read(struct rwsemaphore *sem);//释放读锁

      申请写锁的函数如下:

void down_write(struct rw_semaphore *sem);//申请写锁,如果写者占有写锁或者读者占有读锁,那么进程深度睡眼
int down_write_killable(struct rw_semaphore *sem);//申请写锁,如果写者占有写锁或者读者占有读锁,那么进程中度睡眠
int down_write_trylock(struct rw_semaphore *sem);//尝试申请写锁,不会等待。如果申请成功,返回1;否则返回0.
void downgrade_write(struct rw_semaphore *sem);//占有写锁以后,可以把写锁降级为读锁
void up_write(struct rw semaphore *sem);//释放写锁函数

 

互斥锁

      互斥锁只允许一个进程进入临界区,适合保护比较长的临界区。尽管可以把二值信号量当作互斥锁使用,但是内核单独实现了互斥锁。      申请互斥锁的函数如下:

void mutex_lock(struct mutex *lock);//申请互斥锁,如果锁被占有,进程深度睡眠
int mutex_lock_interruptible(struct mutex *lock);//申请互斥锁,如果锁被占有,进程轻度睡眠。
int mutex_lock_killable(struct mutex *lock)//申请互斥锁,如果锁被占有,进程中度睡眠。
int mutex_tryock(struct mutex *lock);//申请互斥锁,如果申请成功,返回1:如果锁被其他进程占有,那么进程不等待,返回0.
void mutex_unlock(struct mutex *lock);//释放互斥锁

 

实时互斥锁

      实时互斥锁是对互斥锁的改进,实现了优先级继承(priorityinheritance),解决了优先级反转(priority inversion)问题。

      如果需要使用实时互斥锁,编译内核时需要开启配置宏CONFIGRTMUTEXES.

      初始化静态实时互斥锁的方法:

DEFINE RT MUTEX(mutexname);

      在运行时动态初始化实时互斥锁的方法:

rt mutex init(mutex);

 

      申请实时互斥锁的函数如下:

void rt_mutex_lock(struct rt mutex *lock);//申请实时互斥锁,如果锁被占有,进程深度睡眠
int rt_mutex_lock_interruptible(struct rt mutex *lock);//申请实时互斥锁,如果锁被占有,进程轻度睡眠。
int rt_mutex_timed_lock(struct rt_mutex *lock, struct hrtimer sleeper *timeou//申请实时互斥锁,如果锁被占有,进程睡眠等待一段时间。
int rt_mutex_trylock(struct rt mutex *lock);//申请实时互斥锁,如果申请成功,返回1;如果锁被其他进程占有,进程不等待回 0.
void rt_mutex_unlock(struct rt mutex*lock);//释放实时互斥锁

 

原子变量用来实现对整数的互斥访问,通常用来实现计数器。

 

自旋锁

      自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待(也称为忙等待)。进程、软中断和硬中断都可以使用自旋锁。

      定义并且初始化静态自旋锁的方法如下:

DEFINE _SPINLOCK(x);

      在运行时动态初始化自旋锁的方法如下:

spin_lock init(x);


      申请自旋锁的函数如下:

void spin_lock(spinlock t *lock);//当前处理器自旋等待申请自旋锁,如果锁被其他处理器占有,
void spin_lock bh(spinlock t *lock);//申请自旋锁,并且禁止当前处理器的软中断。
void spin_lock irq(spinlock t *lock);//申请自旋锁,并且禁止当前处理器的硬中断。
spin_lock_irgsave(lock, flags);//申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断
int spin_trylock(spinlock t *lock);//申请自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回 0。

      释放自旋锁的函数如下:

(1)void spin unlock(spinlock t *lock);
(2)void spin unlock bh(spinlock t *lock);
释放自旋锁,并且开启当前处理器的软中断。
(3)void spin unlock irq(spinlock t *lock);
释放自旋锁,并且开启当前处理器的硬中断。
(4)void spin unlock irqrestore(spinlock t *lock, unsigned long flags);释放自旋锁,并且恢复当前处理器的硬中断状态。

 

读写自旋锁
      读写自旋锁(通常简称读写锁)是对自旋锁的改进,区分读者和写者,允许多个读者同时进入临界区,读者和写者互斥,写者和写者互斥。如果读者占有读锁,写者申请写锁的时候自旋等待。如果写者占有写锁,读者申请读锁的时候自旋等待。

 

顺序锁
      顺序锁区分读者和写者,和读写自旋锁相比,它的优点是不会出现写者饿死的情况。读者不会阻塞写者,读者读数据的时候写者可以写数据。顺序锁有序列号,写者把序列号加1,如果读者检测到序列号有变化,发现写者修改了数据,将会重试,读者的代价比较高。顺序锁支持两种类型的读者。

(1)顺序读者(sequencereaders):不会阻塞写者,但是如果读者检测到序列号有变化发现写者修改了数据,读者将会重试。
(2)持锁读者(lockingreaders):如果写者或另一个持锁读者正在访问临界区,持锁读者将会等待。持锁读者也会阻塞写者。这种情况下顺序锁退化为自旋锁。

      如果使用顺序读者,那么互斥访问的资源不能是指针,因为写者可能使指针失效,读者访问失效的指针会出现致命的错误。

 

禁止内核抢占
      内核抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,编译内核时需要打开配置宏 CONFIG PREEMPT。

如果变量只会被本处理器上的进程访问,比如每处理器变量,可以使用禁止内核抢占的方法来保护,代价很低。如果变量可能被其他处理器上的进程访问,应该使用锁保护。

 

进程和软中断互斥
      如果进程和软中断可能访问同一个对象,那么进程和软中断需要互斥,进程需要禁止软中新。

      如果进程只需要和本处理器的软中断互斥,那么进程只需要禁止本处理器的软中断:如果进程要和所有处理器的软中断互斥,那么进程需要禁止本处理器的软中断,还要使用自旋锁和其他处理器的软中断互斥。

 

进程和硬中断互斥
      如果进程和硬中断可能访问同一个对象,那么进程和硬中断需要互斥,进程需要禁止硬中断。如果进程只需要和本处理器的硬中断互斥,那么进程只需要禁止本处理器的硬中断;如果进程要和所有处理器的硬中断互斥,那么进程需要禁止本处理器的硬中断,还要使用自旋锁和其他处理器的硬中断互斥。

 

每处理器变量
      在多处理器系统中,每处理器变量为每个处理器生成一个变量的副本,每个处理器访问自己的副本,从而避免了处理器之间的互斥和处理器缓存之间的同步,提高了程序的执行速度。每处理器变量分为静态和动态两种

 

内存屏障
      内存屏障(memorybarrier)是一种保证内存访问顺序的方法,用来解决下面这些内存访问乱序问题。

      内核支持3种内存屏障。

(1)编译器屏障。
(2)处理器内存屏障。
(3)内存映射I/O(Memory Mappping I/O,MMIO)写屏障。

 

RCU
      RCU(Read-Copy Update)的意思是读-复制更新,它是根据原理命名的。写者修改对象的过程是:首先复制生成一个副本,然后更新这个副本,最后使用新的对象替换旧的对象。在写者执行复制更新的时候读者可以读数据。写者删除对象,必须等到所有访问被删除对象的读者访问结束,才能执行销毁操作。

      RCU 的优点是读者没有任何同步开销:不需要获取何锁,不需要执行原子指令,(在除了阿尔法以外的处理器上)不需要执行内存屏障。但是写者的同步开销比较大,写者需要延迟对象的释放,复制被修改的对象,写者之间必须使用锁互斥。

      RCU 根据数据结构可以分为以下两种:树型 RCU(tree RCU),微型 RCU(tinyRCU)。


死锁检测工具 lockdep
      常见的死锁有以下4种情况。

(1)进程重复申请同一个锁,称为AA 死锁。例如,重复申请同一个自旋锁;使用读写锁,第一次申请读锁,第二次申请写锁。
(2)进程申请自旋锁时没有禁止硬中断,进程获取自旋锁以后,硬中断抢占,申请同一个自旋锁。这种 AA 死锁很隐蔽,人工审查很难发现。
(3)两个进程都要获取锁L1和L2,进程1持有锁L1,再去获取锁L2,如果这个时候进程2持有锁L2并且正在尝试获取锁L1,那么进程1和进程2就会死锁,称为AB-BA 死锁。
(4)在一个处理器上进程1持有锁L1,再去获取锁L2,在另一个处理器上进程2持有锁 L2,硬中断抢占进程2以后获取锁L1。这种AB-BA死锁很隐蔽,人工审查很难发现。

      内核提供的死锁检测工具1ockdep用来发现内核的死锁风险。使用方法:死锁检测工具 lockdep 的配置宏如下:

(1)CONFIG_LOCKDEP:在配置菜单中看不到这个配置宏,打开配置宏CONFIGPROVE_LOCKING 或 CONFIG DEBUG LOCK ALLOC 的时候会自动打开这个配置宏。
(2)CONFIG PROVE LOCKING:允许内核报告死锁问题。
(3)CONFIG DEBUGLOCK ALLOC:检查内核是否错误地释放被持有的锁。
(4)CONFIG DEBUG LOCKING APISELFTESTS:内核在初始化的过程中运行一小段自我测试程序,自我测试程序检查调试机制是否可以发现常见的锁缺陷。

 

拓展

信号量的主要作用
      控制共享资源的访问: 信号量充当标志, 控制对资源的并发访问, 保证只有一个进程或线程能占用资源。
      进程与线程同步: 信号量可以协调进程或线程的执行顺序, 确保资源的访问按指定顺序进行。
      Linux 中的信号量也分为 System V 和 POSIX 两种标准, 两者在用法和特性上有所不同; System V信号量提供了一个信号量集, 可以通过一个信号量标识符( semid) 来管理多个信号量。 其典型操作函数在 sys/sem.h 头文件中声明。 System V 信号量适用于需要多进程共享多个信号量的情况, 使用较为复杂,适合更细粒度的控制。 POSIX 信号量在 Linux 2.6 之后引入, 提供了单个信号量对象管理。 POSIX 信号量分为 命名信号量(可以在不同进程之间共享, 通过信号量的名称来标识) 和非命名信号量(只能在同一进程的线程间共享, 不支持跨进程) 两种。

System V 信号量的操作
      创建和初始化信号量: 通过 semget 创建或获取信号量集。
      信号量的 P(wait) 操作: 通过 semop 减少信号量的值, 如果信号量为 0, 则等待直到信号量大于0。
      信号量的 V(signal) 操作: 通过 semop 增加信号量的值, 释放对资源的访问权。
      控制和删除信号量: 通过 semctl 设置或获取信号量的值, 或删除信号量。

POSIX 信号量操作
      相对而言, POSIX 信号量的操作更为简单, 主要使用以下函数:
      创建和初始化信号量:

sem_open: 用于创建或打开命名信号量。
sem_init: 用于初始化非命名信号量。

      信号量的 P(wait) 操作:

sem_wait: 等待信号量, 若信号量值为 0, 则阻塞。

      信号量的 V(signal) 操作:

sem_post: 释放信号量, 增加信号量的值。

      控制和删除信号量:

sem_close: 关闭信号量。
sem_unlink: 删除命名信号量。

 

      本篇的内容特别概念原理是比较偏多较生涩,章节通过学习笔记了解内核互斥技术里所包含的术语定义、功能、原理以及内核实现函数,有一个较系统全面的轮廓。拓展是结合了实际工程应用需要一个点述。内核学习也是一个长期过程,也需要在实际工程应用中不断去学习领悟。

 

点赞 关注
个人签名

保持热爱


回复
举报
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/8 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表