linux内核同步之信号量、顺序锁、RCU、完成量、关闭中断

一、信号量

1.信号量的概念

信号量也是一种锁,当信号量不可用时,尝试获取信号量的任务将挂起直到它拿到了信号量。由于尝试获取信号量的任务可能挂起,因而中断服务程序以及可延迟函数不能使用信号量。

对于信号量来说需要注意:

  1. 只有对信号量计数值的操作是原子的
  2. 信号量的自旋锁只用于保护信号量的等待队列
  3. 信号量是比较特殊的,其up操作不是必须由down操作的调用者发起。如果把信号量也看作是一把锁,则该锁是很特殊的,它不一定由持有锁的任务释放,任何其它任务都可以做释放动作即调用up
因此信号量的down和up是可以并发执行的。但是由于保护了信号量的计数值和等待队列,因而这种并发并不会导致up和down本身出问题。
因为down操作可能导致调用者休眠,因而不能休眠的场景是不允许调用该函数的,比如中断上下文,而up可以在任意上下文调用。如果要在中断上下文调用可以使用down_trylock,它尝试获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0。

2.信号量的数据结构和相关API

1.数据结构

信号量用数据结构semaphore表示, 它包含以下域:
  • count:受该信号狼保护的资源的计数值,如果大于0表示资源可用;否则表示资源不可用。对它的操作必须是原子的。如果存在检查并更新的操作,这两个操作的组合必须也是原子的,比如down中的比较并减1。
  • wait_list:等待该信号量保护的资源可用的任务队列的链表。
  • lock:保护等待任务链表的自旋锁。

2.初始化

init_MUTEX( ) 将信号量的count域初始化为1,表示资源当前可用
init_MUTEX_LOCKED( ) 将信号量的count域初始化为0 ,表示资源当前不可用 
DECLARE_MUTEX 完成和 init_MUTEX类似的操作,但是它还多一个静态分配一个信号量的动作
DECLARE_MUTEX_LOCKED 和init_MUTEX_LOCKED类似,但是它还多一个静态分配一个信号量的动作
当然也可以将信号量的初始者设置为其它正值。

3.获取和释放信号量

up()函数用于释放信号量,如果当前信号量的等待队列为空,即没有任务在等待该信号量被释放,则它增加信号量的count值,然后返回,否则它唤醒等待队列上的第一个任务。
down用于获取信号量,如果信号量的值大于0,则它将count的值减1,并返回;否则调用者将被添加到等待队列的尾部,并等待直到被唤醒即该任务获得资源。
void down(struct semaphore *sem)
int down_trylock(struct semaphore *sem)//尝试获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
int down_interruptible(struct semaphore *sem)//获取信号量,但是在等待信号量的时候可以被打断,如果在等待过程中被打断,则返回-EINTR
int down_timeout(struct semaphore *sem, long jiffies) //用于获取信号量,但是最多等待jiffies长的时间,如果在指定的时间期限内没有获取信号量,则就返回-ETIME。
void up(struct semaphore *sem)

3. 读写信号量的概念

读写信号量类似于读写自旋锁,它是为读多写少的场景做了优化的信号量,不同于自旋锁的是,在无法获得信号量时,它挂起而不是自旋。
可以有多个任务并发的为读获取读写信号量,但是同一时间点只能有一个任务可以为写获得读写信号量。因此只有没有任何任务为读或写持有该信号量时,新的为写获取信号量的操作才能成功。
内核FIFO的方式存储等待队列中的任务:
  1. 如果读者或写者无法获取读写信号量,就会被添加到等待队列的尾部
  2. 当信号量被释放时,就唤醒等待队列中的第一个任务(先唤醒谁取决于实现采取的策略,代码最能说明问题,最好查阅实际的代码)
  3. 如果唤醒的第一个任务是写,则其它任务继续睡眠,如果唤醒的第一个任务是读,则会唤醒它之后的所有读任务直到碰到了一个写任务,该写任务以及在它之后的所有任务都继续睡眠
由于它还是信号量,因此其应用场景的限制和信号量相同。

4.读写信号量的数据结构

rw_semaphore数据结构用于表示读写信号量,它包含如下的域:
  • activity:0表示没有任务在读或者写,大于0表示有任务正在读,-1表示有一个任务正在写
  • wait_list:存放等待该信号量的任务的链表
  • wait_lock:用于保护等待任务链表的自旋锁
可以用init_rwsem(sem)初始化读写信号量,也可以用DECLARE_RWSEM(name)声明并初始化一个信号量
void down_read(struct rw_semaphore *sem)
int down_read_trylock(struct rw_semaphore *sem)//尝试为读获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
void down_write(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)//尝试为读获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
void up_read(struct rw_semaphore *sem)  
void up_write(struct rw_semaphore *sem)  
void downgrade_write(struct rw_semaphore *sem)//它将一个写信号量降格为读信号量,并唤醒等待队列上的读任务。因此它的调用者应该持有写信号量

二、顺序锁

1.顺序锁的概念

使用读写锁时,读锁和写锁的优先级是一样的。2.6内核引入的顺序锁和读写自旋锁相同,区别在于它给写者赋予了更高的优先级:在使用顺序锁时即便读者正在进行读操作,写者也可以进行写动作。读写锁的优点在于写者永远不会由于有读者正在进行读而等待,其缺点在于读者可能需要尝试读好多次才能读到合法的数据。

不是所有的数据类型都能用顺序锁来保护,如果要使用顺序锁,以下原则必须被遵循:

  1. 被保护的数据结构不能包含由写者保护而由读者释放的指针
  2. 读者临界区的代码不能有副作用
  3. 读者临界区应该很小,而且写者应该尽量少获取顺序锁,否则重复的读会造成一些性能损伤

2.数据结构

顺序锁使用数据结构seqlock_t表示,它包含两个域:
  • 自旋锁lock
  • 整数序列号
序列号作为顺序计数器存在。每个读者必须至少读这个值两次,一次在读数据之前,一次在读数据之后,如果两次读获取的序列号相同,则读到了一个合法的值,否则说明在读的过程中有写者更新了数据,因此读者需要重新读取。
有两种方法可以初始化顺序锁:
  1. seqlock_t lock1 = SEQLOCK_UNLOCKED;
  2. seqlock_t lock2; seqlock_init(&lock2);
这两种方法都会把顺序锁初始化未上锁状态。

1.写操作

写者必须先获取锁,再操作,然后再释放锁。
write_seqlock:用于为写获取顺序锁,它会获取顺序锁中的自旋锁,然后将顺序锁的序列号加1
write_sequnlock:用于释放顺序锁,它也会增加顺序锁的序列号,然后释放顺序锁中的自旋锁
这种设计确保了写者正在写数据并且没有完成时序列号为奇数,没有写者在修改数据时,序列号为偶数。

2.读操作

对于读者来说,它需要采取下列形式的操作序列:
    unsigned int seq;
    do {
        seq = read_seqbegin(&seqlock);
        /* ... CRITICAL REGION ... */
    } while (read_seqretry(&seqlock, seq));
read_seqbegin:返回顺序锁当前序列号的值
read_seqretry:如果指定的值和顺序锁的序列号的值不等或者顺序锁的序列号为奇数,则返回1。
需要注意的是,读者并不会关闭内核抢占,而由于写者获取了顺序锁中的自旋锁,因而它会禁止内核抢占。

三、Read-Copy Update (RCU)

RCU是另外一种被设计用来在SMP环境下保护主要操作是读操作的数据同步技术。RCU允许多个读者和多个写者同时并发操作。RCU没使用任何锁也没使用任何由多个CPU共享的计数器;相比于读写自旋锁和顺序锁,这是一个极大地优势。
RCU有极大地优势,但是也有很大的限制,它限制了它所能保护的数据结构:
  • 被保护的资源应当是动态分配的、通过指针来存取的, 并且所有对这些资源的引用必须由原子代码持有
  • 当进入由RCU保护的临界区时不能休眠
RCU操作包括读操作,写操作,以及释放旧版本的操作组成。

1.写操作

它的实现原理是:当数据结构需要改变时,写线程做一个拷贝,改变这个拷贝(这里需要一个内存屏障,以保证更新能被其它CPU看到),接着使相关的指针指向新的版本,当内核确认没有CPU还在引用旧版本时旧的版本就可以被释放.

2.读操作

当内核代码想要读取一个由RCU保护的数据结构时,它
  1. 调用rcu_read_lock(相当于preempt_disable)
  2. 进行读访问
  3. 调用rcu_read_unlock(相当于preempt_enable)
这里需要注意的是在1和3之间的代码不允许休眠。

3.释放旧的版本

在RCU中关键的是旧版本何时释放。由于其它处理器上的代码可能还有对旧的数据的引用,因而不能立即释放它。内核必须在它确保已经没有任何指向旧版本的引用时才能释放旧版本。实际上,只有当所有读者都调用了rcu_read_unlock后才能释放旧的拷贝。内核要求每个读者在开始下列动作之前调用rcu_read_unlock宏:
  • CPU进行进程切换
  • CPU开始在向用户模式转变
  • CPU开始执行idle进程
call_rcu函数由写者调用以清除旧的数据结构。该函数原型如下:
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
  • head:其中head是rcu_head类型的数据结构指针,它通常嵌入在受保护的数据结构中。
  • func:在可以释放RCU数据结构时会被调用以释放旧的数据结构
call_rcu函数会在rcu_head描述符中存放回调函数的地址和它的参数,然后将描述符插入到一个每CPU回调链表中。内核会定期检查是否可以释放RCU数据结构了,如果是的话,就会由一个tasklet调用回调函数。

四、完成量(Completions)

1.完成量的概念

内核中常见的一种场景是在当前任务启动另外一个任务,然后等待该任务完成做完某个事情。考虑使用信号量来完成这个工作:
struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);
当外部完成我们期望的动作时,它调用up(&sem)。
但是信号量并不是特别适合这种场景,信号量对“可用”的情况做了优化,即使用信号量时期望在大部分情况下它是可用的。然而在上述场景中很显然down必然走的是信号量不可用的分支。
completion是用来解决这种问题的一种机制。completion允许一个任务告诉另一个任务工作已经完成。

2.数据结构和相关API

内核使用数据结构completion来表示completion
void init_completion(struct completion *x)
可以使用该函数完成completion的初始化或者通过DECLARE_COMPLETION(work)声明并初始化一个completion,INIT_COMPLETION(x)宏用户初始化completion x。
void wait_for_completion(struct completion *c);
该函数在c上等待,并且不可打断。如果调用了wait_for_completion而没有任何任务调用complete,则wait_for_completion的将永远等待。
wait_for_completion_interruptible(struct completion *x)
该函数在x上等待,但是可能在等待过程中被打断,当由于被打断而返回时,返回值为-ERESTARTSYS;否则返回0
void complete(struct completion *c);
void complete_all(struct completion *c);
这两个函数可用于唤醒在c上等待的任务。区别在于complete只唤醒一个等待的任务,而complete_all唤醒所有的。
completion 机制的典型使用是在模块退出时与内核线程的终止一起. 在这个原型例子里,
void complete_and_exit(struct completion *c, long retval);
唤醒在c上等待的任务,并且以retval退出本任务。

五、关闭本地中断

关闭中断是一种设置临界区的方法。当关闭了中断时,即便是硬件中断也无法打断代码的运行,因而它可以保护同时被中断服务程序访问的数据结构。但是需要注意的是关闭本地中断并不能保护可能被多个CPU访问的数据结构。因此在SMP架构下,往往需要用关闭中断和自旋锁想结合的方式来保护被中断服务程序使用的共享资源。
local_irq_disable() 宏用来在本地CPU关闭中断
local_irq_enable() 宏用来在本地CPU打开中断, which makes use of the of the sti assembly language instruction, enables them. As stated in the 使用这两个函数的问题在于,在需要的时候我们可以简单的关闭中断,但是简单粗暴的打开中断不一定是正确的,因为在我们关闭中断时,中断可能已经是关闭的,这时如果我们简单的打开了中断就可能导致问题。
local_irq_save:关闭中断并且保存中断状态字
local_irq_restore:以指定的中断状态字恢复中断
这两个宏就很好的解决了问题,因为local_irq_restore只是将中断恢复到了我们调用local_irq_save时的状态。

六、使能和关闭可延迟函数

由于可延迟函数在不可预期的时间点被执行,因而被可延迟函数访问的数据结构也需要进行保护。
禁止可延迟函数执行的最简单的办法是关闭本地中断,因为这样中断服务程序就没办法执行了,也就没办法启动可延迟函数。
由于软中断在处于中断状态时不会执行,而tasklet是基于软中断实现的,因而只要禁止本地的软中断就可以在本地CPU上禁止可延迟函数。
local_bh_disable宏用于将本地CPU的软中断计数加1,因而就禁止了本地的软中断。
local_bh_enable宏用于将本地CPU的软中断计数减1,用于打开本地软中断
这两个函数可以都可以被重复调用,但是调用了多少次local_bh_disable,就要相应的调用多少次local_bh_enable才能打开软中断

七、选择同步技术

有很多技术都可用于避免访问共享的数据时出现竞态的手段。但是各种手段对系统性能的影响是不同的。但是作为一条原则,应该使用在该场景下能获得最大并发等级或者说并发数量的技术手段。系统的并发等级取决于:

  • 可以并发操作的I/O设备数
  • 做有效工作的CPU数
为了最大化I/O吞度量,应该使得关闭中断的时间尽可能短。

为了提高CPU效率,应该尽可能避免使用自旋锁。因为它不仅导致自旋的CPU处于忙等状态,而且会对高速缓存造成不利的影响。

取决于访问共享数据的内核任务的类型,需要采用的同步技术也会有区别:

任务单处理器环境下使用的同步技术多处理器环境下使用的额外的同步技术
可休眠任务(内线线程,系统调用)信号量不需要额外的同步技术
中断关闭本地中断自旋锁
可延迟函数不需要不需要或者使用自旋锁(取决于不同的tasklet是否会访问相同的数据结构)
可休眠任务+中断关闭本地中断自旋锁
可休眠任务+可延迟函数关闭本地软中断自旋锁
中断+可延迟函数关闭本地中断自旋锁
可休眠任务+中断+可延迟函数关闭本地中断自旋锁



  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
共计8个压缩包 本压缩包是:part01.rar 出版社:人民邮电出版社 ·页码:368 页 ·出版日期:2008年 ·ISBN:7115187118/9787115187116 ·条形码:9787115187116 ·包装版本:1版 ·装帧:平装 ·开本:16 ·中文:中文 ·附带品描述:附光盘一张 ·市场价格:49元 内容简介 Linux内核Linux操作系统中最核心的部分,用于实现对硬件部件的编程控制和接口操作。《Linux2.6内核标准教程》深入、系统地讲解了 Linux内核的工作原理,对Linux内核的核心组件逐一进行深入讲解。 全书共8章,首先讲解Linux系统的引导过程;然后对Linux内核的3大核心模块——内存管理、进程管理、中断和异常处理进行了深入的分析; 在此基础上,对时间度、系统调用进行了分析和讨论;最后讲解了Linux内核中常见的同步机制,使读者掌握每处理器变RCU这两种新的 同步机制。 《Linux2.6内核标准教程》适合Linux内核爱好者、Linux驱动开发人员、Linux系统工程师参考使用,也可以作为计算机及相关专业学生深入学 习操作系统的参考书。 引用: 目录 第1章 Linux内核学习基础 1 1.1 为什么研究Linux内核 2 1.1.1 Linux的历史来源 2 1.1.2 Linux的发展现状 3 1.1.3 Linux的前景展望 3 1.2 选择什么版本进行研究 3 1.3 内核基本结构 4 1.3.1 内核在操作系统中的地位 4 1.3.2 Linux 2.6内核源代码目录树简介 5 1.3.3 Linux 2.6内核的新特性 8 1.4 如何阅读本书 9 1.4.1 内核探索工具 10 1.4.2 推荐阅读方法 12 第2章 引导过程分析 14 2.1 内核镜像的构建过程 15 2.1.1 编译内核的步骤及分析 15 2.1.2 内核镜像构建过程分析 16 2.2 系统引导过程分析 18 2.2.1 傀儡引导扇区 18 2.2.2 探测系统资源 21 2.2.3 解压内核镜像 35 2.2.4 进入保护模式 40 2.2.5 系统最终初始化 47 2.3 系统引导过程总结 47 第3章 内存管理 50 3.1 基础知识 51 3.1.1 存储器地址 51 3.1.2 分段机制 52 3.1.3 分页机制 59 3.2 内核页表的初始化过程 65 3.2.1 启用分页机制 65 3.2.2 构建内核页表 68 3.3 物理内存的描述方法 76 3.3.1 内存节点 77 3.3.2 内存区域 81 3.3.3 物理页框 85 3.4 物理内存的初始化过程 86 3.4.1 探测系统物理内存 87 3.4.2 初始化内存分配器 89 3.5 物理内存的分配与回收 101 3.5.1 伙伴分配算法 101 3.5.2 对象缓冲技术 103 3.6 内核地址空间 105 3.6.1 常规映射地址空间 105 3.6.2 固定映射地址空间 107 3.6.3 长久内核映射空间 109 3.6.4 临时内核映射空间 116 3.6.5 非连续映射地址空间 119 第4章 进程管理 128 4.1 进程与线程的概念 129 4.1.1 程序与进程 129 4.1.2 进程与线程 129 4.2 进程描述符 131 4.2.1 进程标识符 132 4.2.2 进程的状态 132 4.2.3 进程上下文 134 4.2.4 当前进程 139 4.3 进程的组织形式 143 4.3.1 进程标识符构成的哈希表 143 4.3.2 所有进程构成的双向链表 148 4.3.3 执行态进程组成的运行队列 149 4.3.4 阻塞态进程组成的等待队列 152 4.4 进程的创建过程 155 4.4.1 进程创建的接口函数 156 4.4.2 进程创建的处理过程 162 4.5 进程调度算法 177 4.5.1 进程的分类 178 4.5.2 进程优先级 178 4.5.3 时间片分配 181 4.5.4 进程调度时机 182 4.6 进程切换过程分析 183 4.6.1 选取合适进程 183 4.6.2 完成上下文切换 184 4.7 空闲进程的初始化 187 4.7.1 空闲进程的内核态栈 187 4.7.2 空闲进程的内存描述符 188 4.7.3 空闲进程的硬件上下文 190 4.7.4 空闲进程的任务状态段 190 第5章 中断和异常 192 5.1 基础知识 193 5.1.1 中断和异常的定义 193 5.1.2 中断和异常的分类 193 5.1.3 中断和异常的对比 194 5.2 处理机制 195 5.2.1 IA32架构下的处理机制 195 5.2.2 Linu
Linux 内核RCU(Read-Copy Update)机制在内存管理方面有一些特殊的考虑和实现。下面是关于 Linux 内核 RCU 内存管理的详细介绍: 1. RCU 内存回收:RCU 机制允许在没有的情况下进行并发读取,这意味着在进行内存回收时需要特殊处理。内核使用了一种称为“延迟回收”的技术来处理内存回收。当某个对象不再被引用时,RCU 并不立即释放它,而是延迟一段时间,以确保没有正在执行的读取操作仍然可以访问该对象。只有在延迟期间所有的读取操作都完成后,该对象才会被安全地释放。 2. RCU 保护的数据结构:在使用 RCU 时,需要特殊的数据结构来管理共享数据。常见的结构包括 rcu_head、struct rcu_head、rcu_node 等。这些结构用于追踪需要进行延迟回收的对象,并在适当的时机释放它们。 3. RCU 的内存屏障:为了确保并发读取的正确性,RCU 引入了内存屏障(memory barrier)来保证读取操作的顺序。在读取共享数据之前和之后,需要使用适当的内存屏障指令来确保读取操作的正确顺序。 4. RCU 的内存分配:在使用 RCU 时,需要特殊的内存分配函数来分配 RCU 保护的数据结构。Linux 内核提供了一些 RCU 特定的内存分配函数,例如 rcu_alloc() 和 rcu_free(),这些函数确保分配和释放的对象能够正确地与 RCU 机制配合使用。 5. RCU 的内存同步:在使用 RCU 时,需要进行适当的内存同步操作来确保数据的一致性。常见的同步操作包括 synchronize_rcu() 和 call_rcu()。synchronize_rcu() 用于等待所有当前正在执行的 RCU 读取操作完成,而 call_rcu() 用于注册一个回调函数,在所有当前正在执行的读取操作完成后执行。 总体而言,Linux 内核RCU 机制在内存管理方面提供了一种高效且无的并发读取解决方案。通过合理使用延迟回收、特殊数据结构、内存屏障和内存分配函数,以及适当进行内存同步操作,RCU 确保了并发读取的正确性和性能。这些特性使得 RCU 成为内核中重要的并发编程机制之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值