当前位置: 首页 > news >正文

Linux内核同步机制:确保系统稳定与高效

在复杂而庞大的 Linux 系统世界中,内核就如同一位有条不紊的指挥官,协调着各种任务和资源的分配。而其中,内核同步机制则是确保整个系统稳定与高效运行的关键要素。想象一下,众多的进程和线程在 Linux 内核的舞台上同时登场,它们都渴望访问共享的资源,如同千军万马奔向同一个目标。如果没有有效的同步机制,混乱和冲突将不可避免,系统可能陷入崩溃的边缘。

那么,Linux 内核同步机制究竟是如何发挥其神奇的魔力呢?它又是通过哪些巧妙的方式来保证不同的任务能够和谐共处,共同推动系统的高效运转呢?让我们一同深入探索 Linux 内核同步机制的奥秘,揭开它神秘的面纱,领略其在系统稳定性和高效性方面所展现出的强大力量。

一、引言

Linux 内核同步机制在多任务、多处理器环境下至关重要,它确保了并发操作的正确性和一致性。本文将深入探讨 Linux 内核同步机制的各种类型及其应用场景。

在当今复杂的计算机系统中,多任务和多处理器环境已成为常态。在这样的环境下,不同的任务和处理器可能同时访问共享资源,这就可能导致数据不一致和错误的结果。为了解决这个问题,Linux 内核提供了多种同步机制。这些同步机制可以分为不同的类型,每种类型都有其特定的应用场景。例如,自旋锁适用于保护短时间的临界区,当一个处理器试图获取已被其他处理器持有的自旋锁时,它会一直循环检查(旋转)直到锁变为可用。而互斥锁则用于保护代码段,确保同时只有一个线程可以执行。如果一个线程不能获得互斥锁,它会被挂起而不是忙等待。

读写锁则允许多个读操作同时进行,但写操作是排他的。这有助于提高并发性能,特别是当读操作远多于写操作时。信号量是一种计数器,用于控制对共享资源的访问。它可以用作互斥锁或同步多个线程。

此外,还有完成量、等待队列、屏障、原子操作、顺序锁、RCU 等同步机制。完成量用于等待某个事件或任务的完成,等待队列用于阻塞等待某个条件的发生,屏障用于同步一组线程,直到所有线程都到达屏障点,原子操作是一系列不可分割的操作,顺序锁是一种轻量级的锁机制,适用于读多写少的场景,RCU 是一种高效的无锁同步机制,允许多个线程并发读取,而写操作需要复制数据。

选择正确的同步机制对于提高系统性能和保证数据一致性至关重要。不同的同步机制适用于不同的并发场景和性能需求,因此在设计和实现 Linux 内核中的并发程序时,需要根据具体情况选择合适的同步机制。

为了弄清楚什么事同步机制,必需要弄明确下面三个问题:

  • 什么是相互排斥与同步?

  • 为什么须要同步机制?

  • Linux内核提供哪些方法用于实现相互排斥与同步的机制?

1.1什么是相互排斥与同步?(通俗理解)

  • 相互排斥与同步机制是计算机系统中,用于控制进程对某些特定资源的訪问的机制。

  • 同步是指用于实现控制多个进程依照一定的规则或顺序訪问某些系统资源的机制。

  • 相互排斥是指用于实现控制某些系统资源在随意时刻仅仅能同意一个进程訪问的机制。相互排斥是同步机制中的一种特殊情况。

  • 同步机制是linux操作系统能够高效稳定执行的重要机制。

1.2Linux为什么须要同步机制?

在操作系统引入了进程概念,进程成为调度实体后,系统就具备了并发运行多个进程的能力,但也导致了系统中各个进程之间的资源竞争和共享。另外,因为中断、异常机制的引入,以及内核态抢占都导致了这些内核运行路径(进程)以交错的方式运行。

对于这些交错路径运行的内核路径,如不採取必要的同步措施。将会对一些重要数据结构进行交错訪问和改动。从而导致这些数据结构状态的不一致,进而导致系统崩溃。

因此。为了确保系统高效稳定有序地运行,linux必需要採用同步机制。

1.3Linux内核提供了哪些同步机制?

在学习linux内核同步机制之前。先要了解下面预备知识:(临界资源与并发源)

在linux系统中,我们把对共享的资源进行訪问的代码片段称为临界区。把导致出现多个进程对同一共享资源进行訪问的原因称为并发源。

Linux系统下并发的主要来源有:

  • 中断处理:比如,当进程在訪问某个临界资源的时候发生了中断。随后进入中断处理程序,假设在中断处理程序中。也訪问了该临界资源。尽管不是严格意义上的并发,可是也会造成了对该资源的竞态。

  • 内核态抢占:比如。当进程在訪问某个临界资源的时候发生内核态抢占,随后进入了高优先级的进程。假设该进程也訪问了同一临界资源,那么就会造成进程与进程之间的并发。

  • 多处理器的并发:多处理器系统上的进程与进程之间是严格意义上的并发,每一个处理器都能够独自调度执行一个进程。在同一时刻有多个进程在同一时候执行 。

如前所述可知:採用同步机制的目的就是避免多个进程并发并发訪问同一临界资源。常用的 Linux 内核同步机制有原子操作、Per-CPU 变量、内存屏障、自旋锁、Mutex 锁、信号量和 RCU 等,后面几种锁实现会依赖于前三种基础同步机制。在正式开始分析具体的内核同步机制实现之前,需要先澄清一些基本概念。

二、基本概念

2.1 同步

既然是同步机制,那就首先要搞明白什么是同步。同步是指用于实现控制多个执行路径按照一定的规则或顺序访问某些系统资源的机制。所谓执行路径,就是在 CPU 上运行的代码流。我们知道,CPU 调度的最小单位是线程,可以是用户态线程,也可以是内核线程,甚至是中断服务程序。所以,执行路径在这里就包括用户态线程、内核线程和中断服务程序。执行路径、执行单元、控制路径等等,叫法不同,但本质都一样。那为什么需要同步机制呢?请继续往下看。

2.2 并发与竞态

并发是指两个以上的执行路径同时被执行,而并发的执行路径对共享资源(硬件资源和软件上的全局变量等)的访问则很容易导致竞态。例如,现在系统有一个 LED 灯可以由 APP 控制,APP1 控制灯亮一秒灭一秒,APP2 控制灯亮 500ms 灭 1500ms。

如果 APP1 和 APP2 分别在 CPU1 和 CPU2 上并发运行,LED 灯的行为会是什么样的呢?很有可能 LED 灯的亮灭节奏都不会如这两个 APP 所愿,APP1 在关掉 LED 灯时,很有可能恰逢 APP2 正要打开 LED 灯。很明显,APP1 和 APP2 对 LED 灯这个资源产生了竞争关系。竞态是危险的,如果不加以约束,轻则只是程序运行结果不符合预期,重则系统崩溃。

在操作系统中,更复杂、更混乱的并发大量存在,而同步机制正是为了解决并发和竞态问题。同步机制通过保护临界区(访问共享资源的代码区域)达到对共享资源互斥访问的目的,所谓互斥访问,是指一个执行路径在访问共享资源时,另一个执行路径被禁止去访问。关于并发与竞态,有个生活例子很贴切。

假如你和你的同事张小三都要上厕所,但是公司只有一个洗手间而且也只有一个坑。当张小三进入厕所关起门的那一刻起,你就无法进去了,只能在门外侯着。当小三哥出来后你才能进去解决你的问题。这里,公司厕所就是共享资源,你和张小三同时需要这个共享资源就是并发,你们对厕所的使用需求就构成了竞态,而厕所的门就是一种同步机制,他在用你就不能用了。

2.3 中断与抢占

中断本身的概念很简单,本文不予解释。当然,这并不是说 Linux 内核的中断部分也很简单。事实上,Linux 内核的中断子系统也相当复杂,因为中断对于操作系统来说实在是太重要了。以后有机会,笔者计划开专题再来介绍。对于同步机制的代码分析来说,了解中断的概念即可,不需要深入分析内核的具体代码实现。抢占属于进程调度的概念,Linux 内核从 2.6 版本开始支持抢占调度。

进程调度(管理)是 Linux 内核最核心的子系统之一,异常庞大,本文只简单介绍基本概念,对于同步机制的代码分析已然足够。通俗地说,抢占是指一个正愉快地运行在 CPU 上的 task(可以是用户态进程,也可以是内核线程) 被另一个 task(通常是更高优先级)夺去 CPU 执行权的故事。中断和抢占之间有着比较暧昧的关系,简单来说,抢占依赖中断。如果当前 CPU 禁止了本地中断,那么也意味着禁止了本 CPU 上的抢占。

但反过来,禁掉抢占并不影响中断。Linux 内核中用 preempt_enable() 宏函数来开启本 CPU 的抢占,用 preempt_disable() 来禁掉本 CPU 的抢占。这里,“本 CPU” 这个描述其实不太准确,更严谨的说法是运行在当前 CPU 上的 task。preempt_enable() 和 preempt_disable() 的具体实现展开来介绍的话也可以单独成文了,笔者没有深究过,就不班门弄斧了,感兴趣的读者可以去 RTFSC。不管是用户态抢占还是内核态抢占,并不是什么代码位置都能发生,而是有抢占时机的,也就是所谓的抢占点。抢占时机如下:

  • 用户态抢占:1、从系统调用返回用户空间时;2、从中断(异常)处理程序返回用户空间时。

  • 内核态抢占:1、当一个中断处理程序退出,返回到内核态时;2、task 显式调用 schedule();3、task 发生阻塞(此时由调度器完成调度)。

2.4 编译乱序与编译屏障

编译器(compiler)的工作就是优化我们的代码以提高性能。这包括在不改变程序行为的情况下重新排列指令。因为 compiler 不知道什么样的代码需要线程安全(thread-safe),所以 compiler 假设我们的代码都是单线程执行(single-threaded),并且进行指令重排优化并保证是单线程安全的。因此,当你不需要 compiler 重新排序指令的时候,你需要显式告诉 compiler,我不需要重排。否则,它可不会听你的。本篇文章中,我们一起探究 compiler 关于指令重排的优化规则。

注:测试使用 aarch64-linux-gnu-gcc 版本:7.3.0

编译器指令重排(Compiler Instruction Reordering)

compiler 的主要工作就是将对人们可读的源码转化成机器语言,机器语言就是对 CPU 可读的代码。因此,compiler 可以在背后做些不为人知的事情。我们考虑下面的 C语言代码:

int a, b;void foo(void)
{a = b + 1;b = 0;
}

使用 aarch64-linux-gnu-gcc 在不优化代码的情况下编译上述代码,使用 objdump 工具查看 foo() 反汇编结果:

<foo>:...ldr w0, [x0]       //load b to w0add w1, w0, #0x1...str w1, [x0]       //a = b + 1...str wzr, [x0]      //b = 0

我们应该知道 Linux 默认编译优化选项是 -O2,因此我们采用 -O2 优化选项编译上述代码,并反汇编得到如下汇编结果:

<foo>:...ldr w2, [x0]       //load b to w2str wzr, [x0]      //b = 0add w0, w2, #0x1str w0, [x1]       //a = b + 1

比较优化和不优化的结果,我们可以发现:在不优化的情况下,a 和 b 的写入内存顺序符合代码顺序(program order);但是 -O2 优化后,a 和 b 的写入顺序和 program order 是相反的。-O2 优化后的代码转换成 C 语言可以看作如下形式:

int a, b;void foo(void)
{register int reg = b;b = 0;a = reg + 1;
}

这就是 compiler reordering(编译器重排)。为什么可以这么做呢?对于单线程来说,a 和 b 的写入顺序,compiler 认为没有任何问题。并且最终的结果也是正确的(a == 1 && b == 0)。这种 compiler reordering 在大部分情况下是没有问题的。但是在某些情况下可能会引入问题。例如我们使用一个全局变量 flag 标记共享数据 data 是否就绪。由于 compiler reordering,可能会引入问题。考虑下面的代码(无锁编程):

int flag, data;void write_data(int value)
{data = value;flag = 1;
}

如果 compiler 产生的汇编代码是 flag 比 data 先写入内存,那么,即使是单核系统上,我们也会有问题。在 flag 置 1 之后,data 写 45 之前,系统发生抢占。另一个进程发现 flag 已经置 1,认为 data 的数据已经准备就绪。但是实际上读取 data 的值并不是 45。为什么 compiler 还会这么操作呢?因为,compiler 并不知道 data 和 flag 之间有严格的依赖关系。这种逻辑关系是我们人为强加的。我们如何避免这种优化呢?

显式编译器屏障(Explicit Compiler Barriers)

为了解决上述变量之间存在依赖关系导致 compiler 错误优化。compiler 为我们提供了编译器屏障(compiler barriers),可用来告诉 compiler 不要 reorder。我们继续使用上面的 foo() 函数作为演示实验,在代码之间插入 compiler barriers。

#define barrier() __asm__ __volatile__("": : :"memory")int a, b;void foo(void)
{a = b + 1;barrier();b = 0;
}

barrier() 就是 compiler 提供的屏障,作用是告诉 compiler 内存中的值已经改变,之前对内存的缓存(缓存到寄存器)都需要抛弃,barrier() 之后的内存操作需要重新从内存 load,而不能使用之前寄存器缓存的值。并且可以防止 compiler 优化 barrier() 前后的内存访问顺序。barrier() 就像是代码中的一道不可逾越的屏障,barrier() 前的 load/store 操作不能跑到 barrier() 后面;同样,barrier() 后面的 load/store 操作不能在 barrier() 之前。依然使用 -O2 优化选项编译上述代码,反汇编得到如下结果:

<foo>:...ldr w2, [x0]       //load b to w2add w2, w2, #0x1str w2, [x1]       //a = a + 1str wzr, [x0]      //b = 0...

我们可以看到插入 compiler barriers 之后,a 和 b 的写入顺序和 program order 一致。因此,当我们的代码中需要严格的内存顺序,就需要考虑 compiler barriers。

隐式编译器屏障(Implied Compiler Barriers)

除了显示的插入 compiler barriers 之外,还有别的方法阻止 compiler reordering。例如 CPU barriers 指令,同样会阻止 compiler reordering。后续我们再考虑 CPU barriers。除此以外,当某个函数内部包含 compiler barriers 时,该函数也会充当 compiler barriers 的作用。即使这个函数被 inline,也是这样。例如上面插入 barrier() 的 foo() 函数,当其他函数调用 foo() 时,foo() 就相当于 compiler barriers。考虑下面的代码:

int a, b, c;void fun(void)
{c = 2;barrier();
}void foo(void)
{a = b + 1;fun(); /* fun() call acts as compiler barriers */b = 0;
}

fun() 函数包含 barrier(),因此 foo() 函数中 fun() 调用也表现出 compiler barriers 的作用,同样可以保证 a 和 b 的写入顺序。如果 fun() 函数不包含 barrier(),结果又会怎么样呢?实际上,大多数的函数调用都表现出 compiler barriers 的作用。但是,这不包含 inline 的函数。

因此,fun() 如果被 inline 进 foo(),那么 fun() 就不具有 compiler barriers 的作用。如果被调用的函数是一个外部函数,其副作用会比 compiler barriers 还要强。因为 compiler 不知道函数的副作用是什么。它必须忘记它对内存所作的任何假设,即使这些假设对该函数可能是可见的。我们看一下下面的代码片段,printf() 一定是一个外部的函数。

int a, b;void foo(void)
{a = 5;printf("smcdef");b = a;
}

同样使用 -O2 优化选项编译代码,objdump 反汇编得到如下结果:

<foo>:...mov w2, #0x5              //#5str w2, [x19]             //a = 5bl 640 <__printf_chk@plt> //printf()ldr w1, [x19]             //reload a to w1...str w1, [x0]              //b = a

compiler 不能假设 printf() 不会使用或者修改 a 变量。因此在调用 printf() 之前会将 a 写 5,以保证 printf() 可能会用到新值。在 printf() 调用之后,重新从内存中 load a 的值,然后赋值给变量 b。重新 load a 的原因是 compiler 也不知道 printf() 会不会修改 a 的值。

因此,我们可以看到即使存在 compiler reordering,但是还是有很多限制。当我们需要考虑 compiler barriers 时,一定要显示的插入 barrier(),而不是依靠函数调用附加的隐式 compiler barriers。因为,谁也无法保证调用的函数不会被 compiler 优化成 inline 方式。

barrier() 除了防止编译乱序,还能做什么

barriers() 作用除了防止 compiler reordering 之外,还有什么妙用吗?我们考虑下面的代码片段:

int run = 1;void foo(void)
{while (run);
}

run 是个全局变量,foo() 在一个进程中执行,一直循环。我们期望的结果是 foo() 一直等到其他进程修改 run 的值为 0 才退出循环。实际 compiler 编译的代码和我们会达到我们预期的结果吗?我们看一下汇编代码:

0000000000000748 <foo>:
748: 90000080 adrp x0, 10000
74c: f947e800 ldr x0, [x0, #4048]
750: b9400000 ldr w0, [x0]            //load run to w0
754: d503201f nop
758: 35000000 cbnz w0, 758 <foo+0x10> //if (w0) while (1);
75c: d65f03c0 ret

汇编代码可以转换成如下的 C 语言形式:

int run = 1;void foo(void)
{register int reg = run;if (reg)while (1);
}

compiler 首先将 run 加载到一个寄存器 reg 中,然后判断 reg 是否满足循环条件,如果满足就一直循环。但是循环过程中,寄存器 reg 的值并没有变化。因此,即使其他进程修改 run 的值为 0,也不能使 foo() 退出循环。很明显,这不是我们想要的结果。我们继续看一下加入 barrier() 后的结果:

0000000000000748 <foo>:
748: 90000080 adrp x0, 10000
74c: f947e800 ldr x0, [x0, #4048]
750: b9400001 ldr w1, [x0]            //load run to w0
754: 34000061 cbz w1, 760 <foo+0x18>
758: b9400001 ldr w1, [x0]            //load run to w0
75c: 35ffffe1 cbnz w1, 758 <foo+0x10> //if (w0) goto 758
760: d65f03c0 ret

可以看到加入 barrier() 后的结果真是我们想要的。每一次循环都会从内存中重新 load run 的值。因此,当有其他进程修改 run 的值为 0 的时候,foo() 可以正常退出循环。为什么加入 barrier() 后的汇编代码就是正确的呢?因为 barrier() 作用是告诉 compiler 内存中的值已经变化,后面的操作都需要重新从内存 load,而不能使用寄存器缓存的值。因此,这里的 run 变量会从内存重新 load,然后判断循环条件。这样,其他进程修改 run 变量,foo() 就可以看得见了。

在 Linux kernel 中,提供了 cpu_relax() 函数,该函数在 ARM64 平台定义如下:

static inline void cpu_relax(void)
{asm volatile("yield" ::: "memory");
}

我们可以看出,cpu_relax() 是在 barrier() 的基础上又插入一条汇编指令 yield。在 kernel 中,我们经常会看到一些类似上面举例的 while 循环,循环条件是个全局变量。为了避免上述所说问题,我们就会在循环中插入 cpu_relax() 调用。

int run = 1;void foo(void)
{while (run)cpu_relax();
}

当然也可以使用 Linux 提供的 READ_ONCE()。例如,下面的修改也同样可以达到我们预期的效果。

int run = 1;void foo(void)
{while (READ_ONCE(run)) /* similar to while (*(volatile int *)&run) */;
}

当然你也可以修改 run 的定义为 volatile int run就会得到如下代码。同样可以达到预期目的。

volatile int run = 1;void foo(void)
{while (run);
}

2.5 执行乱序与内存屏障

不管是编译乱序还是执行乱序,都是为了提升 CPU 的性能。执行乱序是处理器运行时的行为,和 CPU 内部设计架构有关。而对于从事在 Linux 内核的程序员来说,要真正的理解透执行乱序所带来的软件方面的影响,首先需要搞清楚 cache 的概念。

三、主要同步机制介绍

3.1自旋锁(spinlock)

自旋锁是一种基本的同步机制,它使用忙等待的方式来实现互斥访问。当一个线程尝试获取自旋锁时,如果锁已被其他线程占用,该线程会一直循环等待,直到锁被释放。

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。

自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。

跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。

无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。自旋锁的API有:

  • spin_lock_init(x)该宏用于初始化自旋锁x。自旋锁在真正使用前必须先初始化。该宏用于动态初始化。

  • DEFINE_SPINLOCK(x)该宏声明一个自旋锁x并初始化它。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

  • SPIN_LOCK_UNLOCKED该宏用于静态初始化一个自旋锁。

  • DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKEDspin_is_locked(x)该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是,返回真,否则返回假。

  • spin_unlock_wait(x)该宏用于等待自旋锁x变得没有被任何执行单元保持,如果没有任何执行单元保持该自旋锁,该宏立即返回,否则将循环在那里,直到该自旋锁被保持者释放。

  • spin_trylock(lock)该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。

  • spin_lock(lock)该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放,这时,它获得锁并返回。总之,只有它获得锁才返回。

  • spin_lock_irqsave(lock, flags)该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。

  • spin_lock_irq(lock)该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。

  • spin_lock_bh(lock)该宏在得到自旋锁的同时失效本地软中断。

  • spin_unlock(lock)该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。

  • spin_unlock_irqrestore(lock, flags)该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。

  • spin_unlock_irq(lock)该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。

  • spin_unlock(lock)该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。

  • spin_unlock_irqrestore(lock, flags)该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。

  • spin_unlock_irq(lock)该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。

  • spin_unlock_bh(lock)该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。

  • spin_trylock_irqsave(lock, flags) 该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,并且失效本地中断,如果没有获得锁,它什么也不做。因此如果能够立即获得锁,它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。

  • spin_unlock_bh(lock)该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。

  • spin_trylock_irqsave(lock, flags) 该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,并且失效本地中断,如果没有获得锁,它什么也不做。因此如果能够立即获得锁,它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。

  • spin_can_lock(lock)该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。如果lock没有被锁,它返回真,否则,返回假。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。

如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。

当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。

如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。

如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。

timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。

如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。

如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。

如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。

如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。

因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。

因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。

当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。

需要特别提醒读者,spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。

  • 适用场景:行为时间很短的情况下,即将要读取或修改的数据只要一瞬间就能够可用的情况下使用。例如在一些对性能要求极高的场景中,如内核中的中断处理程序中,当需要快速响应并处理一些关键事件时,自旋锁可以确保在短时间内获取到所需的资源。

  • 单处理器下的自旋锁特点:单处理器上不需要自旋锁,编译时会将自旋锁简化为是否启用内核抢占的开关。在单处理器系统中,由于不存在真正的并行执行,所以自旋锁主要用于控制内核抢占。

  • Linux 的自旋锁不可重入:持有自旋锁的任务视图获取自己已经持有的锁,会陷入忙等待,导致死锁。这是因为自旋锁的设计初衷是为了在多处理器环境下快速实现互斥访问,而不是支持重入。

  • 与中断的关系:中断处理程序中如果需要加锁,可以使用自旋锁,但需要先禁止本地中断。这是为了防止在中断处理过程中被其他中断打断,从而导致数据不一致。例如,在处理硬件中断时,可能需要访问一些共享资源,此时使用自旋锁可以确保在中断处理期间这些资源不会被其他中断或进程修改。

3.2信号量(semaphore)

信号量是一种睡眠锁。如果任务想要获取一个不可用的信号量时,信号量会将任务推进一个队列,然后让这个任务睡眠。当该信号量可用后,处于等待队列的任务将被唤醒,并获得该信号量。Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干。

信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。

当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。信号量的API有:

  • DECLARE_MUTEX(name)该宏声明一个信号量name并初始化它的值为0,即声明一个互斥锁。

  • DECLARE_MUTEX_LOCKED(name)该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。

  • void sema_init (struct semaphore *sem, int val);该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。

  • void init_MUTEX (struct semaphore *sem);该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。

  • void init_MUTEX_LOCKED (struct semaphore *sem);该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

  • void down(struct semaphore * sem);该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。

  • int down_interruptible(struct semaphore * sem);该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。

  • int down_trylock(struct semaphore * sem);该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。

  • void up(struct semaphore * sem);该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

信号量在绝大部分情况下作为互斥锁使用,下面以console驱动系统为例说明信号量的使用。

在内核源码树的kernel/printk.c中,使用宏DECLARE_MUTEX声明了一个互斥锁console_sem,它用于保护console驱动列表console_drivers以及同步对整个console驱动系统的访问。

其中定义了函数acquire_console_sem来获得互斥锁console_sem,定义了release_console_sem来释放互斥锁console_sem,定义了函数try_acquire_console_sem来尽力得到互斥锁console_sem。这三个函数实际上是分别对函数down,up和down_trylock的简单包装。

需要访问console_drivers驱动列表时就需要使用acquire_console_sem来保护console_drivers列表,当访问完该列表后,就调用release_console_sem释放信号量console_sem。

函数console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要访问console_drivers,因此它们都使用函数对acquire_console_sem和release_console_sem来对console_drivers进行保护。

3.3读写信号量(rw_semaphore)

读写信号量是对普通信号量的扩展,区分了读操作和写操作。多个读操作可以同时获取到读锁,但是一旦有任务获取了写锁,则其他任务不允许再获取读锁和写锁。

读写信号量在以读为主的情况下非常有用,例如在一个数据库系统中,多个客户端可以同时读取数据,但是在写入数据时,需要独占访问。这样可以提高系统的并发性能,同时保证数据的一致性。

写信号量对访问者进行了细分,或者为读者,或者为写者,读者在保持读写信号量期间只能对该读写信号量保护的共享资源进行读访问,如果一个任务除了需要读,可能还需要写,那么它必须被归类为写者,它在对共享资源访问之前必须先获得写者身份,写者在发现自己不需要写访问的情况下可以降级为读者。读写信号量同时拥有的读者数不受限制,也就说可以有任意多个读者同时拥有一个读写信号量。

如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;否则,读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。因此,写者是排他性的,独占性的。

读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因此,增加新的架构不需要重新实现它,但缺点是性能低,获得和释放读写信号量的开销大;另一种是架构相关的,因此性能高,获取和释放读写信号量的开销小,但增加新的架构需要重新实现。在内核配置时,可以通过选项去控制使用哪一种实现。读写信号量的相关API有:

  • DECLARE_RWSEM(name)该宏声明一个读写信号量name并对其进行初始化。

  • void init_rwsem(struct rw_semaphore *sem);该函数对读写信号量sem进行初始化。

  • void down_read(struct rw_semaphore *sem);读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。

  • int down_read_trylock(struct rw_semaphore *sem);该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。

  • void down_write(struct rw_semaphore *sem);写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。

  • int down_write_trylock(struct rw_semaphore *sem);该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。

  • void up_read(struct rw_semaphore *sem);读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。

  • void up_write(struct rw_semaphore *sem);写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。

  • void downgrade_write(struct rw_semaphore *sem);该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。

在Linux中,每一个进程都用一个类型为task_t或struct task_struct的结构来描述,该结构的类型为struct mm_struct的字段mm描述了进程的内存映像,特别是mm_struct结构的mmap字段维护了整个进程的内存块列表,该列表将在进程生存期间被大量地遍利或修改。结构的mmap字段维护了整个进程的内存块列表,该列表将在进程生存期间被大量地遍利或修改。

因此mm_struct结构就有一个字段mmap_sem来对mmap的访问进行保护,mmap_sem就是一个读写信号量,在proc文件系统里有很多进程内存使用情况的接口,通过它们能够查看某一进程的内存使用情况,命令free、ps和top都是通过proc来得到内存使用信息的,proc接口就使用down_read和up_read来读取进程的mmap信息。

当进程动态地分配或释放内存时,需要修改mmap来反映分配或释放后的内存映像,因此动态内存分配或释放操作需要以写者身份获得读写信号量mmap_sem来对mmap进行更新。系统调用brk和munmap就使用了down_write和up_write来保护对mmap的访问。

3.4互斥锁

互斥锁是一种较为常用的同步机制,其原理是在多个进程或线程访问同一个资源之前先尝试获取互斥锁,如果该锁已经被占用,则进程或线程会阻塞等待。

互斥锁的底层原理涉及多个方面,包括硬件支持、原子操作、内核调度以及锁的实现方式。例如,在 Linux 操作系统中,互斥锁的底层实现依赖于硬件提供的原子操作,通过原子操作来确保操作的不可中断性。当一个线程尝试获取互斥锁但锁已被其他线程持有时,线程会进入休眠状态,并释放 CPU 资源。内核将在锁可用时选择一个线程唤醒并分配 CPU 时间,以允许其继续执行。

3.5原子操作

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。

原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。

原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。原子类型定义如下:

typedef struct { 
volatile int counter; 
} atomic_t;

volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。原子操作API包括:

  • tomic_read(atomic_t * v);该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。

  • atomic_set(atomic_t * v, int i);该函数设置原子类型的变量v的值为i。

  • void atomic_add(int i, atomic_t *v);该函数给原子类型的变量v增加值i。

  • atomic_sub(int i, atomic_t *v);该函数从原子类型的变量v中减去i。

  • int atomic_sub_and_test(int i, atomic_t *v);该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。

  • void atomic_inc(atomic_t *v);该函数对原子类型变量v原子地增加1。

  • void atomic_dec(atomic_t *v);该函数对原子类型的变量v原子地减1。

  • int atomic_dec_and_test(atomic_t *v);该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。

  • int atomic_inc_and_test(atomic_t *v);该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。

  • int atomic_add_negative(int i, atomic_t *v);该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。

  • int atomic_add_return(int i, atomic_t *v);该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。

  • int atomic_sub_return(int i, atomic_t *v);该函数从原子类型的变量v中减去i,并且返回指向v的指针。

  • int atomic_inc_return(atomic_t * v);该函数对原子类型的变量v原子地增加1并且返回指向v的指针。

  • int atomic_dec_return(atomic_t * v);该函数对原子类型的变量v原子地减1并且返回指向v的指针。

原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1。

当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放IP碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。

3.6等待队列

以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。

等待队列在 Linux 内核中广泛应用,例如在设备驱动程序中,当设备不可用时,进程可以将自己加入等待队列,等待设备变为可用。当设备变为可用时,驱动程序可以唤醒等待队列中的进程,通知它们设备已经可以使用。

3.7大内核锁

在早期的 Linux 内核中,BKL 是一个全局锁,用于保护整个内核。如今已被更细粒度的锁机制所取代。

大内核锁在早期的 Linux 内核中起到了重要的作用,但是随着内核的发展,它的存在成为了性能瓶颈。因为它是一个全局锁,所以在多处理器环境下,会导致大量的线程等待,降低系统的并发性能。因此,Linux 内核逐渐采用了更细粒度的锁机制,以提高系统的性能。

3.8读写锁

类似于读写信号量,允许任意数量的读取者共享资源,但只允许一个写入者独占资源。

读写锁在一些需要频繁读取但较少写入的场景中非常有用,例如在一个配置文件读取的程序中,多个线程可能同时需要读取配置文件,但是在写入配置文件时,需要独占访问。这样可以提高系统的并发性能,同时保证数据的一致性。

3.9大读者锁

优化了读取密集的场景,允许多个读取者同时访问,但只有一个写入者能获得锁。

大读者锁在读取密集的场景下表现出色,例如在一个 Web 服务器中,大量的客户端同时请求读取网页内容,但是在更新网页内容时,需要独占访问。这样可以提高系统的并发性能,同时保证数据的一致性。

3.10RCU(Read-Copy Update)

一种高效的无锁同步机制,通过延迟对象的删除来实现并发读取。

RCU 在一些对性能要求极高的场景中非常有用,例如在 Linux 内核中,RCU 被广泛应用于网络协议栈、文件系统等模块中。它允许多个线程同时读取共享数据,而在写入数据时,通过复制数据的方式来实现,避免了使用传统的锁机制带来的性能开销。

3.11顺序锁

轻量级的同步机制,用于保护数据结构免受破坏性的竞争条件影响。

顺序锁在一些需要轻量级同步机制的场景中非常有用,例如在一些小型的嵌入式系统中,资源有限,需要一种简单高效的同步机制来保护数据结构。顺序锁通过维护一个顺序计数器来实现,读取数据时,先读取顺序计数器的值,然后读取数据,最后再次读取顺序计数器的值,如果两次读取的值相同,则说明在读取过程中没有被写入操作修改,可以安全地使用读取到的数据。

3.12完成量(completion)

一个线程告诉另一个线程工作已完成,类似 Linux 应用的信号量,有静态宏创建和动态创建两种方式。

完成量在多线程编程中非常有用,例如在一个多线程的任务处理程序中,一个线程负责生成任务,另一个线程负责处理任务。当生成任务的线程完成任务的生成后,可以使用完成量来通知处理任务的线程开始处理任务。完成量可以通过静态宏创建或动态创建两种方式来创建,具体使用哪种方式取决于应用程序的需求。

四、同步机制的选择

不同的同步机制适用于不同的并发场景和性能需求。例如,自旋锁适用于短时间持有锁的场景;信号量适用于锁会被长时间持有的情况;读写锁在读取操作远多于写入操作的场景中特别有效。

自旋锁在需要快速获取锁且持有时间很短的情况下表现出色。比如在中断处理程序中,为了快速响应关键事件,自旋锁可以确保在短时间内获取到所需资源,避免因线程睡眠和唤醒带来的开销。但如果临界区执行时间较长,自旋锁会导致其他线程长时间忙等待,浪费 CPU 资源,此时就不适合使用自旋锁。

信号量则适用于锁会被长时间持有的情况。当一个任务想要获得已被占用的信号量时,信号量会将任务推进一个等待队列,然后让这个任务睡眠。当该信号量可用后,处于等待队列的任务将被唤醒,并获得该信号量。例如在一些耗时较长的任务中,如坐火车从南京到新疆需要 2 天时间,这种情况下使用信号量让任务进入睡眠状态是比较合适的,避免了一直占用 CPU 资源进行忙等待。

读写锁在读取操作远多于写入操作的场景中非常有用。读写锁允许任意数量的读取者共享资源,但只允许一个写入者独占资源。比如在一个数据库系统中,多个客户端可以同时读取数据,但是在写入数据时,需要独占访问。这样可以提高系统的并发性能,同时保证数据的一致性。

不同的同步机制各有其特点和适用场景,在实际应用中,需要根据具体的并发情况和性能要求来选择合适的同步机制,以提高系统的性能和稳定性。

五、全文总结

Linux 内核的同步机制在确保系统的正确性和性能方面起着至关重要的作用。不同的同步机制具有各自的特点和适用场景,开发人员需要根据实际情况进行选择。

如自旋锁适用于保护短时间的临界区,在中断处理等场景中能快速响应,但不可重入且在长时间持有锁时会浪费 CPU 资源。信号量则适用于锁会被长时间持有的情况,能让任务进入睡眠状态,避免占用 CPU 资源。读写信号量、读写锁和大读者锁在以读为主的场景中能提高并发性能,同时保证数据的一致性。互斥锁通过底层的硬件支持和内核调度实现互斥访问。原子操作用于保护共享数据,在资源计数等方面广泛应用。等待队列与进程调度机制紧密结合,可实现异步事件通知。大内核锁虽在早期有重要作用,但已被更细粒度的锁机制取代。RCU 是高效的无锁同步机制,适用于对性能要求极高的场景。顺序锁是轻量级的同步机制,用于保护数据结构。

相关文章:

Linux内核同步机制:确保系统稳定与高效

在复杂而庞大的 Linux 系统世界中&#xff0c;内核就如同一位有条不紊的指挥官&#xff0c;协调着各种任务和资源的分配。而其中&#xff0c;内核同步机制则是确保整个系统稳定与高效运行的关键要素。想象一下&#xff0c;众多的进程和线程在 Linux 内核的舞台上同时登场&#…...

firebase简介

Firebase 是一个由 Google 提供的移动应用开发平台&#xff0c;旨在帮助开发者快速构建和管理应用程序。它提供了一系列强大的工具和服务&#xff0c;特别适合用于开发和管理 Web 和移动应用。以下是 Firebase 的一些核心功能&#xff1a; 实时数据库&#xff1a;Firebase 提供…...

利用Termux在安卓手机中安装 PostgreSQL

利用Termux在安卓手机中安装 PostgreSQL ⬇️Termux下载 点击下载 在 Termux 中安装 PostgreSQL 可以按照以下步骤进行&#xff1a; 1. 更新 Termux 包管理器 先更新软件包列表和已安装的软件包&#xff1a; pkg update && pkg upgrade -y2. 安装 PostgreSQL 使…...

windows安装WSL完整指南

本文首先介绍WSL&#xff0c;然后一步一步安装WSL及Ubuntu系统&#xff0c;最后讲解如何在两个系统之间访问和共享文件信息。通过学习该完整指南&#xff0c;能帮助你快速安装WSL&#xff0c;解决安装和使用过程中的常见问题。 理解WSL&#xff08;Windows Subsystem for Linux…...

Windows Docker笔记-安装docker

安装环境 操作系统&#xff1a;Windows 11 家庭中文版 docker版本&#xff1a;Docker Desktop version: 4.36.0 (175267) 注意&#xff1a; Docker Desktop 支持以下Windows操作系统&#xff1a; 支持的版本&#xff1a;Windows 10&#xff08;家庭版、专业版、企业版、教育…...

ReactNative进阶(五十九):存量 react-native 项目适配 HarmonyOS NEXT

文章目录 一、前言二、ohos_react_native2.1 Fabric2.2 TurboModule2.2.1 ArkTSTurboModule2.2.2 cxxTurboModule&#xff1a; 三、拓展阅读 一、前言 2024年10月22日19:00&#xff0c;华为在深圳举办“原生鸿蒙之夜暨华为全场景新品发布会”&#xff0c;主题为“星河璀璨&…...

[x86 ubuntu22.04]进入S4失败

目录 1 问题描述 2 解决过程 2.1 查看内核日志 2.2 新建一个交换分区 2.3 指定交换分区的位置 1 问题描述 CPU&#xff1a;G6900E OS&#xff1a;ubuntu22.04 Kernel&#xff1a;6.8.0-49-generic 使用“echo disk > /sys/power/state”命令进入 S4&#xff0c;但是无法…...

Java面试题-MySQL数据库

文章目录 1.事务1.事务的特性 ACID2.并发事务问题3.undo log 和redo log的区别&#xff1f;4.事务的隔离性是如何保证的呢&#xff1f;解释一下MVCC&#xff1f; 2.索引1.如何定位慢查询&#xff1f;2.explain3.了解过索引吗&#xff1f;索引的底层数据结构B树和B树对比4.什么是…...

为什么Vue的data属性是函数而不是对象

Vue中data属性设计为函数而非对象的原因是解决组件复用时的数据隔离问题。确保每个实例维护独立的数据副本&#xff0c;避免数据共享导致的状态污染。 而根实例因为只会被创建一次(不会被复用)&#xff0c;所以可以直接用对象&#xff0c;不会有这个问题。 组件一般都会被多个实…...

网络工程师 (26)TCP/IP体系结构

一、层次 四层&#xff1a; 网络接口层&#xff1a;TCP/IP协议的最底层&#xff0c;负责网络层与硬件设备间的联系。该层协议非常多&#xff0c;包括逻辑链路和媒体访问控制&#xff0c;负责与物理传输的连接媒介打交道&#xff0c;主要功能是接收数据报&#xff0c;并把接收到…...

MySQL部署基于二进制日志文件位置的主从复制集群

MySQL主从复制介绍 MySQL 主从复制&#xff08;Master-Slave Replication&#xff09; 作为一种经典的数据库复制方案&#xff0c;被广泛应用于企业生产环境&#xff0c;尤其是在提升数据库性能、实现数据备份和分布式扩展方面具有重要作用。 官方文档&#xff1a;https://de…...

【系统设计】Spring、SpringMVC 与 Spring Boot 技术选型指南:人群、场景与实战建议

在 Java 开发领域&#xff0c;Spring 生态的技术选型直接影响项目的开发效率、维护成本和长期扩展性。然而&#xff0c;面对 Spring、SpringMVC 和 Spring Boot 这三个紧密关联的框架&#xff0c;开发者常常陷入纠结&#xff1a;该从何入手&#xff1f;如何根据团队能力和业务需…...

【CAPL实战】LIN调度表操作

文章目录 前言1、linChangeSchedTable切换调度表2、linStartScheduler开启调度表3、linStopScheduler停止调度表 前言 在LIN调度表Schedule Table文章中&#xff0c;详细介绍了LIN调度表的信息&#xff0c;那么如何在CAPL脚本测试中进行LIN调度表的操作呢&#xff1f; 1、linC…...

「vue3-element-admin」告别 vite-plugin-svg-icons!用 @unocss/preset-icons 加载本地 SVG 图标

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template &#x1f33a; 仓库主页&#xff1a; GitCode︱ Gitee ︱ Github &#x1f496; 欢迎点赞 &#x1f44d; 收藏 ⭐评论 …...

[图文]课程讲解片段-Fowler分析模式的剖析和实现01

​ 解说&#xff1a; GJJ-004-1&#xff0c;分析模式高阶Fowler分析模式的剖析和实现&#xff0c;这个课是针对Martin Fowler的《分析模式》那本书里面的模式来讲解&#xff0c;对里面的模式来剖析&#xff0c;然后用代码来实现。 做到这一步的&#xff0c;我们这个是世界上独…...

element-plus el-tree-select 修改 value 字段

element-plus el-tree-select 修改 value 字段 &#xff0c;不显示label 需要注意两个地方&#xff1a; <el-tree-select v-model"value" :data"data" multiple :render-after-expand"false" show-checkbox style"width: 240px" …...

软件测评实验室CNAS认证能力验证什么时机做?如何查询能力验证相关信息?

能力验证是软件测评实验室申请CNAS认证前必须要做的一类质量活动。CNAS软件测评实验室初次认可和扩大认可范围时&#xff0c;申请认可的每个子领域应至少参加过一次相关领域的能力验证且获得满意结果。通过认定认可后&#xff0c;只要存在可获得的能力验证&#xff0c;不同类目…...

Spring Boot 3.4 中 MockMvcTester 的新特性解析

引言 在 Spring Boot 3.4 版本中&#xff0c;引入了一个全新的 MockMvcTester 类&#xff0c;使 MockMvc 测试可以直接支持 AssertJ 断言。本文将深入探讨这一新特性&#xff0c;分析它如何优化 MockMvc 测试并提升测试的可读性。 Spring MVC 示例 为了演示 MockMvcTester 的…...

网安加·百家讲坛 | 刘志诚:以业务为中心的网络安全挑战与机遇

作者简介&#xff1a;刘志诚&#xff0c;乐信集团信息安全中心总监、OWASP广东区域负责人、网安加社区特聘专家。专注于企业数字化过程中网络空间安全风险治理&#xff0c;对大数据、人工智能、区块链等新技术在金融风险治理领域的应用&#xff0c;以及新技术带来的技术风险治理…...

配置 VS Code 调试 ROS Python 脚本:完整步骤

在 Ubuntu 系统上使用 ROS 和 VS Code 进行 Python 开发时&#xff0c;可能会遇到一些环境配置的问题&#xff0c;特别是当需要加载 ROS 环境变量以及确保正确使用 Python 3 环境时。以下是如何配置 launch.json 和 tasks.json 来确保 VS Code 调试环境能够正确加载 ROS 和 Pyt…...

HTTP4种方法(GET、POST、 PUT和DELETE)

一、GET 和 POST 1. GET方法 特点&#xff1a; 用途&#xff1a;用于从服务器获取数据。 参数传递方式&#xff1a;参数会附加在URL后面&#xff0c;以 keyvalue的形式&#xff0c;通过查询字符串传递&#xff0c;例如&#xff1a; http://example.com/page?nameJohn&…...

AnythingLLM开发者接口API测试

《Win10OllamaAnythingLLMDeepSeek构建本地多人访问知识库》见上一篇文章&#xff0c;本文在上篇基础上进行。 1.生成本地API 密钥 2.打开API测试页面&#xff08;http://localhost:3001/api/docs/&#xff09; 就可以在页面测试API了 2.测试获取用户接口(/v1/admin/users) 3…...

CSS定位简介

目录 一、静态定位&#xff08;Static Positioning&#xff09; 二、相对定位&#xff08;Relative Positioning&#xff09; 三、绝对定位&#xff08;Absolute Positioning&#xff09; 四、固定定位&#xff08;Fixed Positioning&#xff09; 五、黏性定位&#xff08;…...

CentOS服务器部署Docker+Jenkins持续集成环境

一、准备工作 一台运行 CentOS 的服务器&#xff0c;确保有足够的磁盘空间、内存资源&#xff0c;并且网络连接稳定。建议使用 CentOS 7 或更高版本&#xff0c;本文以 CentOS 7 为例进行讲解。 拥有服务器的 root 权限&#xff0c;因为后续安装软件包、配置环境等操作需要较…...

React受控组件的核心原理与实战精要

在 React 中&#xff0c;受控组件&#xff08;Controlled Component&#xff09; 是一种重要的模式&#xff0c;用于通过组件的状态来管理表单元素的值。这种模式不仅确保了数据的一致性和可预测性&#xff0c;还便于与其他功能&#xff08;如验证和格式化&#xff09;集成。本…...

基于python多线程多进程爬虫的maa作业站技能使用分析

基于python多线程多进程爬虫的maa作业站技能使用分析 技能使用分析 多线程&#xff08;8核&#xff09; import json import multiprocessing import requests from multiprocessing.dummy import Pooldef maa(st):url "https://prts.maa.plus/copilot/get/"m …...

Android studio怎么创建assets目录

在Android Studio中创建assets文件夹是一个简单的步骤&#xff0c;通常用于存储不需要编译的资源文件&#xff0c;如文本文件、图片、音频等 main文件夹&#xff0c;邮件new->folder-assets folder...

解锁 DeepSeek 模型高效部署密码:蓝耘平台全解析

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…...

【Spring相关知识】Spring应用如何优雅使用消息队列

文章目录 概述**核心概念****使用场景****快速入门**1. 添加依赖2. 配置 Binder3. 定义消息通道4. 发送和接收消息5. 运行应用 **高级特性****优点****适用场景** 概述 Spring Cloud Stream 是一个用于构建消息驱动微服务的框架&#xff0c;它基于 Spring Boot 和 Spring Inte…...

2025牛客寒假算法基础集训营4(补题)

C Tokitsukaze and Balance String (hard) 一道规律题。赛时以为是难的算法题&#xff0c;就没去碰了&#xff0c;实际上把几种情况列出来后可能就会发现&#xff0c;只有首尾相同的字符串才是平衡的。 首先我们容易发现&#xff0c;连续的1或者0是多余的&#xff0c;因为他们…...

.net一些知识点5

1.dot Net带out的参数如何使用 string name;//假设这个参数带out TestMethod(1,out name);//一定要有out 方法体中&#xff0c;一定要有out参数的赋值&#xff0c;并且能输出 2.参数的传递方式有哪些 a.值传递 b.引用传递 ref c.输出传递 out 3.设计模式知道哪些 3.us…...

基于Servlet简易学生信息管理系统

本次设计的学生信息管理系统&#xff0c;能提供以下功能&#xff1a; &#xff08;1&#xff09; 输入入学生信息并保存 &#xff08;2&#xff09; 显示所有学生信息 &#xff08;3&#xff09; 查询学生信息 &#xff08;4&#xff09; 修改学生信息并保存 &#xff08;…...

IDEA编写SpringBoot项目时使用Lombok报错“找不到符号”的原因和解决

目录 概述|背景 报错解析 解决方法 IDEA配置解决 Pom配置插件解决 概述|背景 报错发生背景&#xff1a;在SpringBoot项目中引入Lombok依赖并使用后出现"找不到符号"的问题。 本文讨论在上述背景下发生的报错原因和解决办法&#xff0c;如果仅为了解决BUG不论原…...

JVM图文入门

往期推荐 【已解决】redisCache注解失效&#xff0c;没写cacheConfig_com.howbuy.cachemanagement.client.redisclient#incr-CSDN博客 【已解决】OSS配置问题_keyuewenhua.oss-cn-beijing.aliyuncs-CSDN博客 【排坑】云服务器docker部署前后端分离项目域名解析OSS-CSDN博客 微服…...

【算法】动态规划专题⑨ —— 二维费用背包问题 python

目录 前置知识进入正题实战演练 前置知识 【算法】动态规划专题⑤ —— 0-1背包问题 滚动数组优化 python 进入正题 二维费用背包问题 方法思路 二维费用背包问题在传统背包问题的基础上增加了第二个维度的限制&#xff08;如重量&#xff09;。 每个物品具有两种费用&#x…...

链表专题-02

链表专题 /*** 链表的节点* param <E>*/ public class ListNode<E> {public E element;public ListNode<E> next;public ListNode() {}public ListNode(E element) {this.element element;}public ListNode(E element, ListNode<E> next) {this.eleme…...

亚远景-精通ASPICE:专业咨询助力汽车软件开发高效合规

在竞争日益激烈的汽车行业&#xff0c;软件开发已成为决定成败的关键因素。ASPICE&#xff08;汽车软件过程改进和能力确定&#xff09; 作为行业公认的软件开发框架&#xff0c;为汽车制造商和供应商提供了实现高效、合规开发的路线图。 然而&#xff0c;ASPICE 的实施并非易…...

HALCON 数据结构

目录 1. HALCON基本数据分类 1.1 图像相关数据 1.1.1 Image(图片) 1.1.2 Region(区域) 1.1.3 XLD(轮廓) 1.2 控制类数据 1.2.1 基本控制数据类型 1.2.2 handle(句柄) 2. 数组与字典 2.1 数组类型及特点 2.1.1 Iconic数组(Objects) 2.1.2 Control数组(Tu…...

动手写ORM框架 - GeeORM第一天 database/sql 基础

文章目录 1 初识 SQLite2 database/sql 标准库3 实现一个简单的 log 库4 核心结构 Session本文是7天用Go从零实现ORM框架GeeORM的第一篇。介绍了 SQLite 的基础操作(连接数据库,创建表、增删记录等)。使用 Go 语言标准库 database/sql 连接并操作 SQLite 数据库,并简单封装…...

ubuntu conda运行kivy时报“No matching FB config found”

错误描述&#xff1a;本人使用ubuntu自带的python环境运行kivy是没有问题的&#xff0c;就是在使用conda时发生了错误&#xff0c;去网上寻找报错原因&#xff0c;却一直没有头绪&#xff08;这个问题有诸多问题导致的&#xff0c;不敢说用我的这个方法100%能好&#xff09; 1…...

SSM开发(十一) mybatis关联关系多表查询(嵌套查询,举例说明)

目录 一、背景介绍 二、一对一查询(嵌套查询) 三、一对多查询(嵌套查询) 四、嵌套查询效率评估 注:关联查询则是指在一个查询中涉及到多个表的联合查询 一、背景介绍 当对数据库的操作涉及到多张表,这在面向对象语言如Java中就涉及到了对象与对象之间的关联关系。针对多…...

【AIGC】冷启动数据与多阶段训练在 DeepSeek 中的作用

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;冷启动数据的作用冷启动数据设计 &#x1f4af;多阶段训练的作用阶段 1&#xff1a;冷启动微调阶段 2&#xff1a;推理导向强化学习&#xff08;RL&#xff0…...

Spring(26) spring-security-oauth2 官方表结构解析

目录 一、什么是 spring-security-oauth2&#xff1f;二、spring-security-oauth2 的表结构2.1 oauth_client_details 客户端详细信息表2.2 oauth_access_token 认证授权Token记录表2.3 oauth_refresh_token 刷新授权Token记录表2.4 oauth_code 授权Code记录表 一、什么是 spri…...

WPS如何接入DeepSeek(通过JS宏调用)

WPS如何接入DeepSeek 一、文本扩写二、校对三、翻译 本文介绍如何通过 WPS JS宏调用 DeepSeek 大模型&#xff0c;实现自动化文本扩写、校对和翻译等功能。 一、文本扩写 1、随便打开一个word文档&#xff0c;点击工具栏“工具”。 2、点击“开发工具”。 3、点击“查看代码”…...

项目的虚拟环境的搭建与pytorch依赖的下载

文章目录 配置环境 pytorch的使用需要安装对应的cuda 在PyTorch中使用CUDA, pytorch与cuda不同版本对应安装指南&#xff0c;查看CUDA版本&#xff0c;安装对应版本pytorch 【超详细教程】2024最新Pytorch安装教程&#xff08;同时讲解安装CPU和GPU版本&#xff09; 配置环境…...

[每周一更]-(第133期):Go中MapReduce架构思想的使用场景

文章目录 **MapReduce 工作流程**Go 中使用 MapReduce 的实现方式&#xff1a;**Go MapReduce 的特点****哪些场景适合使用 MapReduce&#xff1f;**使用场景1. 数据聚合2. 数据过滤3. 数据排序4. 数据转换5. 数据去重6. 数据分组7. 数据统计8.**统计文本中单词出现次数****代码…...

C 移位运算符

宏定义 #define GET_BIT(n) ((1 << (n))) 用于生成一个整数&#xff0c;该整数在第 n 位上是 1&#xff0c;其余位都是 0。这个宏通常用于位操作&#xff0c;比如设置、清除或检查某个特定位置的标志位。 1 << (n)&#xff1a;这是位移操作符。它将数字 1 左移 n …...

redis高级数据结构布隆过滤器

文章目录 背景什么是布隆过滤器Redis 中的布隆过滤器布隆过滤器使用注意事项实现原理空间占用估计 背景 我们在使用新闻客户端看新闻时&#xff0c;它会给我们不停地推荐新的内容&#xff0c;它每次推荐时要去重&#xff0c;去掉那些已经看过的内容。问题来了&#xff0c;新闻…...

活动预告 |【Part1】Microsoft 安全在线技术公开课:安全性、合规性和身份基础知识

课程介绍 通过参加“Microsoft 安全在线技术公开课&#xff1a;安全性、合规性和身份基础知识”活动提升你的技能。在本次免费的介绍性活动中&#xff0c;你将获得所需的安全技能和培训&#xff0c;以创造影响力并利用机会推动职业发展。你将了解安全性、合规性和身份的基础知识…...

基于DeepSeek模型的思维导图智能系统

基于DeepSeek模型的思维导图智能系统 摘 要&#xff1a;本文研究了Prompt技术在自然语言处理&#xff08;NLP&#xff09;中的应用&#xff0c;重点探讨了其在用户输入语言转换任务中的作用。基于DeepSeek模型&#xff0c;文章通过设计不同的Prompt并结合API调用&#xff0c;…...