Linux内核深入学习 - 内核同步

目录

内核抢占

同步原语

per-CPU变量

API

Per CPU 变量的应用

per CPU 变量在多文件下的用法

原子操作

API

优化和内存屏障

自旋锁

自旋锁 API 函数

读写锁

API

RCU

信号量

API

1. 信号量的结构:

2. 初始化函数sema_init

3. 可中断获取信号量函数down_interruptible

4. 释放信号量函数 up

Reference


 在我们开始之前,先来看看内核抢占,他是我们为了解决的东西而引出来的内核同步!

内核抢占

在Linux中内核抢占比较复杂:

  • 无论在抢占内核还是在非抢占内核,运行在内核态的程序都可以自动放弃CPU,比如说:其原因可能是竞争,由于等待资源,而不得不进入睡眠状态。往往把这种状态称为计划性进程切换,但是抢占式内核在响应引起进程切换的异步事件,例如说唤醒高优先级进程的中断处理程序的方式上,与非抢占的内核是有差别的。我们把这种进程切换称之为强制性的进程切换

  • 所有的进程切换都由宏:switch_to来完成,在抢占式的内核和非抢占式的内核中,当进程的执行完某一些具有内核功能的线程,而且调度程序被调度后,就会发生进程切换!不过在非抢占内核中,当前进程是不可能被替换!除非他打算换到用户态上去!

因此抢占式内核的主要特点:是一个在内核态运行的进程可能在执行内核函数期间被另一个进程取代!

在Linux中当被current thread info宏所引用的thread_info描述符的preempt_count字段大于零时,就会禁止内核抢占他!

在如下任何一种情况发生时,取值都大于零:

  1. 内核正在执行内中断服务例程

  2. 可延迟函数被禁止

  3. 通过把抢占计数器设置为正数而显示的禁用内核抢占

上面的原则告诉我们只有当内核正在执行异常处理程序,而且内核抢占没有被显式地禁用的时候,才会抢占内核

那么什么时候同步是必须的呢:我们之前就有提到过刚计算的结果依赖于两个或两个以上的交叉内核控制路径的嵌套方式时,才有可能会引起竞争!(说白了就是两个执行流撞在一起,有多个进程同时执行同一段代码),临界区是一段代码,在其他的内核控制路径能够进入临界区前,进入临界区的内核控制路径必须全部执行完!这段临界区的代码交叉内核控制路径使内核开发的工作者变得复杂,他们必须小心地识别出异常处理程序,中断处理程序,可延迟函数和内核进程中的临界区。一旦临界区被确定,就必须对其采取一定的保护措施

那么什么时候同步是不必要的呢:

  • 所有中断处理程序响应来自pic的中断并且禁用了IRQ线,此外在中断处理程序的结束之前不允许产生相同的中断事件!

  • 中断处理程序,软中断,tasklet既不可以被抢占也不可能被阻塞,所以他们不可能长时间的处于挂起状态!即使在最坏的情况下它们的执行也只是有轻微的延迟,因为在其执行的过程中可能会发生其他中断执行!

  • 中断处理的内核控制路径不能被执行可延迟函数,或系统调用服务例程的内核控制路径所中断

  • 软中断和tasklet不能在一个给定的CPU上交错执行

  • 同一个tasklet不可能同时在几个CPU上执行!

以上的每一种设计选择都可以看作是一种约束,下面是一些可能简化了的例子

  • 已中断处理程序和tasklet不必编写成可重入的函数

  • 仅被软中断和task light访问的每CPU变量并不需要同步

  • 仅被一种tasklet访问的数据结构是不需要同步的

同步原语

下面来看看同步原语,内核中使用的同步技术

技术说明适用范围
每CPU变量在CPU中复制数据结构所有CPU
原子操作对一个计数器原子的读修改写的指令所有CPU
内存屏障避免指令重新排序本地CPU或者所有CPU
自旋锁加锁时忙等待所有CPU
信号量加锁是阻塞等待(睡眠)所有CPU
顺序锁基于访问计数器的锁所有CPU
本地中断的禁止禁止单个CPU上的中断处理本地CPU
本地软中断的禁止禁止单个CPU上的可延迟函数处理本地CPU
RCU通过指针而不是锁来访问共享数据结构所有CPU

per-CPU变量

最好的同步技术就是把不需要同步的内核放在首位正如我们将要看到事实上每一种显示的同步原语都会有不容忽视的性能开销最简单也是最重要的同步技术包括把内核变量声明为每CPU变量每CPU变量主要是数据结构的数组系统的每个CPU都对应数组的一个元素一个CPU不应该访问其他CPU对应的数组元素另外它可以随意读或修改他们自己的元素而不必担心竞争条件因为这是他唯一有资格这么做的CPU但是这也意味着每CPU变量基本上只能在特殊情况下才能够使用也就是当他确定这个系统上的CPU上的数据的逻辑上是独立的此外在单处理器和多处理器系统中内核抢占都可能会使每cpu变量产生竞争条件总的原则是内核控制路径应该禁用抢占的情况下去访问每cpu变量

API

为每个CPU定义一个变量的拷贝的宏定义在文件include/linux/percpu-defs.h中,如下:

#define DEFINE_PER_CPU(type, name) \DEFINE_PER_CPU_SECTION(type, name, "")

若我们使用DEFINE_PER_CPU(int, per_cpu_n)为每个CPU定义变量,其展开宏如下。

__attribute__((section(".data..percpu"))) int per_cpu_n
​
#define DEFINE_PER_CPU_SECTION(type, name, sec) \__PCPU_ATTRS(sec) __typeof__(type) name
#define __PCPU_ATTRS(sec)                        \__percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))    
​
#define PER_CPU_BASE_SECTION ".data..percpu"

在链接过程中,所有通过DEFINE_PER_CPU宏定义的变量都将链接到一起。在操作系统启动时,Linux 将为该段分配一段内存。 查看编译出的内核镜像可找到.data..percpu

# readelf -S vmlinux[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[21] .data..percpu     PROGBITS         0000000000000000  01000000000000000001d000  0000000000000000  WA       0     0     4096

per CPU 变量的访问通过宏get_cpu_var完成 。Linux内核是可抢占的,并且在访问访问 per cpu变量时我们需要知道当前代码运行在哪个CPU核上。 因此,在访问每个cpu变量时,应当不允许抢占当前代码并将其移至另一个CPU。例如,若在获取到 CPU id 为 1 后,该任务被抢占而移动到了 CPU 2上继续运行,这时访问的将仍然是 CPU 1的per cpu 变量。因此,在 get_cpu_var 宏中,首先要调用preempt_disable()函数禁止任务抢占。

// in  include/linux/percpu-defs.h
#define get_cpu_var(var)                        \
(*({                                    \preempt_disable();                        \this_cpu_ptr(&var);                        \
}))

我比较好奇的是 this_cpu_ptr是如何实现的。内核如何将该变量对应到属于该CPU的 per CPU 变量内存呢?

在初始化时,内核会使用一个数组__per_cpu_offset[cpu]记录每个CPU静态per cpu 变量的偏移地址。在ARM64架构下, OS 启动时将 per cpu 偏移地址写入到 TPDIR_EL1 和 TPDIR_EL2 寄存器中。

void __init setup_per_cpu_areas(void)
{unsigned long delta;unsigned int cpu;...delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;for_each_possible_cpu(cpu)__per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];
}
​
/* arch/arm64/include/asm/percpu.h */
static inline void set_my_cpu_offset(unsigned long off)
{asm volatile(ALTERNATIVE("msr tpidr_el1, %0","msr tpidr_el2, %0",ARM64_HAS_VIRT_HOST_EXTN):: "r" (off) : "memory");
}

this_cpu_ptr的宏展开如下:即相当于 percpu 变量指针 ptr 加上__my_cpu_offset。

#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
#define raw_cpu_ptr(ptr)                        \
({                                    \__verify_pcpu_ptr(ptr);                        \arch_raw_cpu_ptr(ptr);                        \
})
​
#define this_cpu_ptr(ptr)    raw_cpu_ptr(ptr)

__my_cpu_offset宏即是从当前cpu的tpidr_el1tpidr_el2寄存器中取出此前设置的__per_cpu_offset[cpu]值,实现如下:

static inline unsigned long __my_cpu_offset(void)
{unsigned long off;
​/** We want to allow caching the value, so avoid using volatile and* instead use a fake stack read to hazard against barrier().*/asm(ALTERNATIVE("mrs %0, tpidr_el1","mrs %0, tpidr_el2",ARM64_HAS_VIRT_HOST_EXTN): "=r" (off) :"Q" (*(const unsigned long *)current_stack_pointer));
​return off;
}

有时会需要指定某个 CPU 获取其某个 per cpu 变量的地址,通过宏per_cpu_ptr实现,源码如下:

#define SHIFT_PERCPU_PTR(__p, __offset)                    \RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset))
​
#define per_cpu_ptr(ptr, cpu)                        \
({                                    \__verify_pcpu_ptr(ptr);                        \SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu)));            \
})

user,kernel,safe,force等定义在compiler_type.h头文件中。看到两个很奇怪的现象,一个是只有在CHECKER宏打开的情况下,他们的定义才会被实现,否则他们的定义是空的。第二个是它们的attribute的定义,并不是gcc支持的属性。那到底是哪里使用到了呢?原来linux的作者们自己开发了一套编译期代码检查的工具Sparse,可以用于在编译阶段快速发现代码中隐含的问题。[1]

  • address_space 定义了指针能指向的内存的类型,0代表kernel space,1代表user space,2代表设备地址空间,3代表cpu局部的内存空间

  • safe 表示变量可以为空

  • force 表示变量可以强制类型转换

Per CPU 变量的应用

记录每个CPU 的 id 是 per CPU 变量的应用之一。

那么有了 Per CPU变量之后,如何获得当前执行代码的CPU 编号? 内核函数smp_processor_id()用来获取当前 CPU 的 id 。

CPU id 的存储依赖于 per CPU 变量(DEFINE_PER_CPU宏用来定义 cpu_number 变量)。

// 每个CPU的cpuid是放置在cpu_number这个percpu变量中
DEFINE_PER_CPU(int, cpu_number);

在内核初始化时,smp_prepare_cpus()函数执行per_cpu(cpu_number, cpu) = cpu;设定每个核的编号。

// in /arch/arm64/kernel/smp.c
void __init smp_prepare_cpus(unsigned int max_cpus)
{const struct cpu_operations *ops;int err;unsigned int cpu;unsigned int this_cpu;
​init_cpu_topology();
​this_cpu = smp_processor_id();
​for_each_possible_cpu(cpu) {// 设置 CPU idper_cpu(cpu_number, cpu) = cpu;// 确定在哪个核上执行的,若是本身则跳过。if (cpu == smp_processor_id())continue;
​ops = get_cpu_ops(cpu);if (!ops)continue;
​err = ops->cpu_prepare(cpu);if (err)continue;
​set_cpu_present(cpu, true);numa_store_cpu_info(cpu);}
}

smp_processer_id ()函数(定义在 include/linux/smp.h)展开如下。

# define smp_processor_id() __smp_processor_id()
#define __smp_processor_id(x) raw_smp_processor_id(x)

raw_smp_processor_id与处理器架构相关(下例为ARM64)的实现如下,raw_cpu_ptr 获取到 cpu_number 的地址,在解引用得到 cpu id。

#define raw_smp_processor_id() (*raw_cpu_ptr(&cpu_number))
per CPU 变量在多文件下的用法

声明一个 per cpu 变量并在另一个文件中引用,以获取当前 task_struct 为例(x86下 current 宏的实现)。

定义方式如下:

DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned =&init_task;
EXPORT_PER_CPU_SYMBOL(current_task);

在另一个文件中引用方式如下:

DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{return this_cpu_read_stable(current_task);
}
​
#define current get_current()

原子操作

若干汇编语言指令是具有RCU类型的,也就是说他们访问存储器单元两次!第一次读原值,第二次写新值

避免由于RCU指令引起的竞争条件的容易的办法,就是确保这样的操作在芯片级就是原子性的!

任何一个这样的操作都必须单个指令进行执行中间,是不允许中断的且避免其他的CPU访问统一存储器单元。这些很小的原子操作可以建立在其他更灵活的机制的基础之上创建临界区

让我们根据这样的分类来回顾一下8086的指令:

进行零次或一次对齐内存访问的汇编指令是原子的

如果在读操作之后写操作之前没有其他处理器占用内存总线,那么在从内存中读取数据,更新数据并且把更新后的数据写回内存中的这些RCU汇编语言指令是原子的!当然在单处理器系统中永远不会发生内存总线窃取的情况

操作码前缀是lock字节(0xf0)的汇编语言指令,即使在多处理器系统中,也是原子的!当控制单元检测到这个前缀时,就会锁定内存总线直到这条指令完成为止,因此在加速的指令执行时其他处理器是不能够访问这个内存单元的!

操作码前缀是reg字节(0xf2, 0xf3)的汇编语言指令不是原子的!这条指令强行让控制单元多次重复相同的指令控制单元,在执行新的循环之前要检查挂起的中断!

API
API含义
ATOMIC_INIT(int i)定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v)读取 v 的值,并且返回。
void atomic_set(atomic_t *v, int i)向 v 写入 i 值。
void atomic_add(int i, atomic_t *v)给 v 加上 i 值。
void atomic_sub(int i, atomic_t *v)从 v 减去 i 值。
void atomic_inc(atomic_t *v)给 v 加 1,也就是自增。
void atomic_dec(atomic_t *v)从 v 减 1,也就是自减
int atomic_dec_return(atomic_t *v)从 v 减 1,并且返回 v 的值。
int atomic_inc_return(atomic_t *v)给 v 加 1,并且返回 v 的值。
int atomic_sub_and_test(int i, atomic_t *v)从 v 减 i,如果结果为 0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v)从 v 减 1,如果结果为 0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v)给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v)给 v 加 i,如果结果为负就返回真,否则返回假

相应的也提供了 64 位原子变量的操作 API 函数,这里我们就不详细讲解了,和表 中的 API 函数有用法一样,只是将“atomic”前缀换为“atomic64”,将 int 换为 long long。如果使用的是 64 位的 SOC,那么就要使用 64 位的原子操作函数。

优化和内存屏障

当使用边缘优化的编译器时,它会重排汇编指令从而达到以最优,此外现代CPU通常会并行地执行若干条指令且可能重新安排内存访问这种重新排序可以极大地加速程序的执行然而当处理同步时则必须避免指令重新排序优化屏障源于保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言这些汇编语言指令在C中都有对应的语句在Linux中优化屏障就是barrier宏,它展开为:

asm volitile("":::"memory");

指令ASM告诉编译程序要插入汇编语言片段,volatile关键字禁止编译器把ASM指令与程序中的其他指令重新组合!memory关键字强制编译器假定RAM中的所有内存单元已经被汇编语言指令修改,因此编译器不能使用存放在CPU寄存器中的内存单元的值来优化ASM指令前的代码

注意优化屏障并不保证不使当前CPU把汇编语言指令混在一起执行,这是内存屏障的工作!

内存屏障源于保证在原语之后的操作开始之前,原语之前的操作已经完成。因此内存屏障类似于防火墙,让任何汇编语句指令都不能通过!在以下这些汇编指令在8086处理中是串行的!因为他们起到了内存屏障的作用:

对IO端口进行操作的所有指令

有lock前缀的所有指令

写控制寄存器系统寄存器或调试寄存器的所有指令

在奔腾4微处理器中引入的汇编指令lfence, sfence, mfence

关于内存屏障的汇编指令少数专门的汇编语言指令

Linux使用六个内存屏障原语,如下表所示:

内存屏障的宏定义功能说明
mb()适用于多处理器和单处理器的内存屏障。
rmb()适用于多处理器和单处理器的读内存屏障。
wmb()适用于多处理器和单处理器的写内存屏障。
smp_mb()适用于多处理器的内存屏障。
smp_rmb()适用于多处理器的读内存屏障。
smp_wmb()适用于多处理器的写内存屏障。

自旋锁

这是一种广泛使用的锁,关于锁,可以认为是对访问公共资源的一种限制。如果内核控制路径希望访问资源,就必须获取钥匙来打开这个锁!当且只当资源空闲时,也就是没有任何进程来访问这段资源的时候,它才能成功,然后持有这个锁!其他进程想要在这个进程处理。这个数据结构的时候必须等待这个进程处理完毕,释放掉这个锁之后,其他进程才能够接着访问这个数据结构!

严肃的版本:

自旋锁是用来在多处理器环境中工作的一种特殊的锁,如果内核控制路径发现自旋锁是开着的!那么获取锁,并且继续执行,相反,则会在周围旋转反复执行一条紧凑的循环指令,直到锁被释放!

自旋锁的循环指令表示忙等待,即使等待的内核控制路径是无事可做的。它也会在CPU上保持运行,不过自旋锁通常非常方便。因为很多内核资源只锁一毫秒的时间片段,所以说释放CPU和随后又获得CPU是不会消耗多少时间的!

这里有更加详细的自旋锁的文章,可以参看:

[Linux中的spinlock机制一] - CAS和ticket spinlock - 知乎 (zhihu.com)]

自旋锁 API 函数

最基本的自旋锁 API 函数如下表 所示:

API描述
DEFINE_SPINLOCK(spinlock_t lock)定义并初始化一个自选变量。
int spin_lock_init(spinlock_t *lock)初始化自旋锁。
void spin_lock(spinlock_t *lock)获取指定的自旋锁,也叫做加锁。
void spin_unlock(spinlock_t *lock)释放指定的自旋锁。
int spin_trylock(spinlock_t *lock)尝试获取指定的自旋锁,如果没有获取到就返回 0
int spin_is_locked(spinlock_t *lock)检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0

读写锁

读写自旋锁的引入是为了增加内核的并发能力,因为我们思考:只要没有内核控制路径对希望上锁的数据结构进行修改,我们就没有必要对这个数据结构进行上锁。只有当我们想要对这个结构进行写操作的时候那么我们才会对这个资源进行上锁。

API

与 spinlock 一样,Read/Write spinlock 有如下的 APIs:

接口API描述Read/Write Spinlock API
定义rw spin lock并初始化DEFINE_RWLOCK
动态初始化rw spin lockrwlock_init
获取指定的rw spin lockread_lock write_lock
获取指定的rw spin lock同时disable本CPU中断read_lock_irq write_lock_irq
保存本CPU当前的irq状态,disable本CPU中断并获取指定的rw spin lockread_lock_irqsave write_lock_irqsave
获取指定的rw spin lock同时disable本CPU的bottom halfread_lock_bh write_lock_bh
释放指定的spin lockread_unlock write_unlock
释放指定的rw spin lock同时enable本CPU中断read_unlock_irq write_unlock_irq
释放指定的rw spin lock同时恢复本CPU的中断状态read_unlock_irqrestore write_unlock_irqrestore
获取指定的rw spin lock同时enable本CPU的bottom halfread_unlock_bh write_unlock_bh
尝试去获取rw spin lock,如果失败,不会spin,而是返回非零值read_trylock write_trylock

RCU

RCU就是Read, Copy, Update机制,这是为了保护在多数情况下被多个CPU读的数据结构,而设计的一种同步技术!它允许多个读者和写者并发执行,而且它是不会使用锁的。就是说它不使用被所有CPU共享的锁或计数器

在这一点上与读写自旋锁与顺序锁相比,它具有更大的优势!它的关键思想在于限制RCP的范围:

  • RCU只会保护被动态分配,并通过指针引用的数据结构

  • 在被RCU保护的临界区中任何内核控制路径都不能睡眠

信号量

它本质上就是一个更加高级的锁,一个允许最大若干进程访问资源的锁。

Linux有两个信号量:内核信号量和IPC信号量,我们现在只关心前者

API
函数定义功能说明
sema_init(struct semaphore *sem, int val)初始化信号量,将信号量计数器值设置val。
down(struct semaphore *sem)获取信号量,不建议使用此函数,因为是 UNINTERRUPTABLE 的睡眠。
down_interruptible(struct semaphore *sem)可被中断地获取信号量,如果睡眠被信号中断,返回错误-EINTR。
down_killable (struct semaphore *sem)可被杀死地获取信号量。如果睡眠被致命信号中断,返回错误-EINTR。
down_trylock(struct semaphore *sem)尝试原子地获取信号量,如果成功获取,返回0,不能获取,返回1。
down_timeout(struct semaphore *sem, long jiffies)在指定的时间jiffies内获取信号量,若超时未获取,返回错误-ETIME。
up(struct semaphore *sem)释放信号量sem。

注意:down_interruptible 接口,在获取不到信号量的时候,该任务会进入 INTERRUPTABLE 的睡眠,但是 down() 接口会导致进入 UNINTERRUPTABLE 的睡眠,down 用的较少。

1. 信号量的结构:
struct semaphore {raw_spinlock_t      lock;unsigned int        count;struct list_head    wait_list;
};

信号量用结构semaphore描述,它在自旋锁的基础上改进而成,它包括一个自旋锁、信号量计数器和一个等待队列。用户程序只能调用信号量API函数,而不能直接访问信号量结构。

2. 初始化函数sema_init
#define __SEMAPHORE_INITIALIZER(name, n)                \
{                                   \.lock       = __RAW_SPIN_LOCK_UNLOCKED((name).lock),    \.count      = n,                        \.wait_list  = LIST_HEAD_INIT((name).wait_list),     \
}static inline void sema_init(struct semaphore *sem, int val)
{static struct lock_class_key __key;*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

初始化了信号量中的 spinlock 结构,count 计数器和初始化链表。

3. 可中断获取信号量函数down_interruptible
static noinline int __sched __down_interruptible(struct semaphore *sem)
{return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}int down_interruptible(struct semaphore *sem)
{unsigned long flags;int result = 0;raw_spin_lock_irqsave(&sem->lock, flags);if (likely(sem->count > 0))sem->count--;elseresult = __down_interruptible(sem);raw_spin_unlock_irqrestore(&sem->lock, flags);return result;
}

down_interruptible 进入后,获取信号量获取成功,进入临界区,否则进入 down_interruptible->down_common

static inline int __sched __down_common(struct semaphore *sem, long state,long timeout)
{struct semaphore_waiter waiter;list_add_tail(&waiter.list, &sem->wait_list);waiter.task = current;waiter.up = false;for (;;) {if (signal_pending_state(state, current))goto interrupted;if (unlikely(timeout <= 0))goto timed_out;__set_current_state(state);raw_spin_unlock_irq(&sem->lock);timeout = schedule_timeout(timeout);raw_spin_lock_irq(&sem->lock);if (waiter.up)return 0;}timed_out:list_del(&waiter.list);return -ETIME;interrupted:list_del(&waiter.list);return -EINTR;
}

加入到等待队列,将状态设置成为 TASK_INTERRUPTIBLE , 并设置了调度的 Timeout : MAX_SCHEDULE_TIMEOUT

在调用了 schedule_timeout,使得进程进入了睡眠状态。

4. 释放信号量函数 up
void up(struct semaphore *sem)
{unsigned long flags;raw_spin_lock_irqsave(&sem->lock, flags);if (likely(list_empty(&sem->wait_list)))sem->count++;else__up(sem);raw_spin_unlock_irqrestore(&sem->lock, flags);
}

如果等待队列为空,没有睡眠的进程期望获取这个信号量,则直接 count++,否则调用 __up:

static noinline void __sched __up(struct semaphore *sem)
{struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,struct semaphore_waiter, list);list_del(&waiter->list);waiter->up = true;wake_up_process(waiter->task);
}static noinline void __sched __up(struct semaphore *sem)
{struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,struct semaphore_waiter, list);list_del(&waiter->list);waiter->up = true;wake_up_process(waiter->task);
}

取出队列中的元素,进行唤醒操作。

Reference

Linux--原子操作(介绍及其操作函数集)_原子操作函数-CSDN博客

一文读懂优化屏障和内存屏障 - 知乎 (zhihu.com)

[Linux中的RCU机制一] - 原理与使用方法 - 知乎 (zhihu.com)

Linux 内核同步(五):信号量(semaphore)_sema_init-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3003746.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

iOS 实现视图遮罩效果

有时候&#xff0c;我们会遇到这种需求&#xff0c;只讲视图的某个部分展示出来 这时候&#xff0c;我们可以通过设置该视图layer.mask layerb来实现&#xff0c;需要注意的是&#xff0c;这里的layerb必须要设置backgroundColor&#xff0c;渐变layer有colors,否则达不到效果…

欧拉计划第804题:二次多项式计数

题目描述: 第一步,先根据题意直接求解 先利用一元二次方程求根的公式进行推导 x 2 + y x + 41 y 2 − n = 0 x^2 + yx + 41y^2 - n = 0

GPT3 终极指南(二)

原文&#xff1a;zh.annas-archive.org/md5/6de8906c86a2711a5a84c839bec7e073 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第五章&#xff1a;GPT-3 作为企业创新的下一步 当一个新的创新或技术转变发生时&#xff0c;大公司通常是最后一个采纳的。它们的等级结构…

如何定时打开网站

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具按下Ctrl3&#xff0c;切换到定时器模块&#xff0c;左侧右键&#xff0c;选择新建 2、标题叫百度&#xff0c;等下就让它打开百度&#xff0c…

Java | Leetcode Java题解之第64题最小路径和

题目&#xff1a; 题解&#xff1a; class Solution {public int minPathSum(int[][] grid) {if (grid null || grid.length 0 || grid[0].length 0) {return 0;}int rows grid.length, columns grid[0].length;int[][] dp new int[rows][columns];dp[0][0] grid[0][0]…

持续更新|UNIAPP适配APP遇到的问题以及解决方案

在使用UNIAPP开发APP的时候遇到的一些奇奇怪怪问题记录 组件样式丢失 问题&#xff1a;组件引入界面中&#xff0c;在小程序和H5环境下样式正常&#xff0c;而在APP中却出现高度异常问题 解决&#xff1a;增加view标签将组件包裹起来即可正常显示 解决前&#xff1a; 解决后…

Mysql的关联查询以及语句

一、mysql的连接查询 1、等值连接 这里是三张表的等值连接 select rp.role_id,rp.permission_id from role_permission rp, role r, permission p where rp.role_idr.id and rp.permission_idp.id 2、内连接&#xff1a; 角色&#xff1a;系统管理员 是否拥有权限&#xf…

并发编程之线程池的设计和原理

一、线程池 提前创建一系列的线程&#xff0c;保存在这个线程池中&#xff0c;有任务要执行的时候&#xff0c;从线程池中取出线程来执行。没有任务的时候&#xff0c;线程池放回去。 二、为什么要使用线程池 线程使用上的问题: 线程的频繁创建 和 销毁 线程的数量过多&…

51. 【Android教程】JSON 数据解析

在上一节我们学习了 xml 数据格式&#xff0c;如果你觉得 xml 的数据比较冗余&#xff0c;标签、属性等等定义过于复杂&#xff0c;那么这一节我们将继续学习另一种更精简、更高效的数据格式—— Json。它广泛的运用于数据持久化以及网络传输中&#xff0c;这一节我们一起学习 …

ios CI/CD 持续集成 组件化专题五-(自动发布私有库-组件化搭建)

一&#xff1a;手动发布私有库总结 手动发布pod私有库&#xff0c;需要进行如下几步操作&#xff1a; 1、修改完代码之后&#xff0c;需要提交代码push到git仓库。 2、给代码打tag。 3、修改podspec文件的version值&#xff0c;使其和设置的tag一直。 4、命令行执行pod repo…

【C 数据结构】深度优先搜索、广度优先搜索

文章目录 【 1. DFS 深度优先搜索 】1.1 基本原理1.2 C 实现 【 2. BFS 广度优先搜索 】2.1 基本原理2.2 C 实现 【 3. 深度优先生成树、广度优先生成树 】【 4. 深度优先生成森林、广度优先生成森林 】4.1 深度优先生成森林4.2 广度优先生成森林 对存储的图中的顶点进行遍历搜…

Linux专栏08:Linux基本指令之压缩解压缩指令

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Linux专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Linux基本指令之压缩解压缩指令 编号&#xff1a;08 文章目录 Linu…

【python】python标准化考试系统[单项选择题 简易版](源码)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

【数据结构】链表专题3

前言 本篇博客我们继续来讨论链表专题&#xff0c;今天的链表算法题是经典中的经典 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;数据结构 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 目录 1.判断链表是否…

springboot 自动配置源码解读

什么是自动装配 当我们程序依赖第三方功能组件时&#xff0c;不需要手动将这些组件类加载到IOC容器中。例如 当程序需要用到redis时&#xff0c;在pom.xml文件中引入依赖&#xff0c;然后使用依赖注入的方式直接从IOC容器中拿到相应RedisTemplate实例。 SpringBootApplication …

jvm面试题30问

什么是JVM的跨平台&#xff1f; 什么是JVM的语言无关性&#xff1f; 什么是JVM的解释执行 什么是JIT? JIT&#xff1a;在Java编程语言和环境中&#xff0c;即时编译器&#xff08;JIT compiler&#xff0c;just-in-time compiler&#xff09;是一个把Java的字节码&#xff08;…

为什么3D模型材质是透明的?---模大狮模型网

在进行3D建模和渲染过程中&#xff0c;正确的材质设置是保证模型外观逼真和渲染效果良好的关键之一。然而&#xff0c;有时您可能会遇到3D模型材质变成透明的情况&#xff0c;这可能会导致意想不到的效果和渲染结果。本文将探讨一些可能导致3D模型材质变成透明的原因&#xff0…

MySQL 运维篇

回顾基本语句&#xff1a; 数据定义语言(DDL) 这类语言用于定义和修改数据库的结构&#xff0c;包括创建、删除和修改数据库、 表、视图和索引等对象。 主要的语句关键字包括 CREATE 、 DROP 、 ALTER 、 RENAME 、 TRUNCATE 等。 create database 数据库 &#xff1b; cr…

嵌入式Linux 系统组成

三种系统组成图 嵌入式Linux系统和PC完整的操作系统的对比如下&#xff1a; // 可以看到我们嵌入式arm 上的 u-boot 同时替代了 BIOS 和 grub2&#xff08;bootmgr&#xff09;的功能 &#xff0c;下面我们来进行一些详细的介绍 1. BIOS和UEFI的作用&#xff1a; a. 进行硬件自…

【机器学习】机器学习在教育领域的应用场景探索

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…