Linux自旋锁:探秘内核同步利器

在 Linux 操作系统那复杂而精妙的内核世界里,自旋锁宛如一颗独特而关键的 “螺丝钉”,虽看似微小却有着不可忽视的力量。它紧密地与多任务处理、并发控制以及资源共享等核心机制相互交织,深刻地影响着系统的性能、稳定性与可靠性。

当我们开启探索 Linux 自旋锁之旅时,就仿佛踏入了一个神秘而充满挑战的技术领域,在这里,我们将逐步揭开自旋锁的神秘面纱,洞察它在 Linux 内核运作中所扮演的微妙角色,以及它是如何在多线程、多进程的繁忙交互中巧妙地维持秩序,保障系统有条不紊地运行。让我们一同深入其中,领略 Linux 自旋锁的深邃魅力与强大功能吧。

一、自旋锁简介

自旋锁是为了在多处理系统(SMP)设计,实现在多处理器情况下保护临界区。在 Linux 内核中,自旋锁通常用于包含内核数据结构的操作,保证操作的原子性。

自旋锁最初是为了在多处理系统(SMP)设计,实现在多处理器情况下保护临界区。自旋锁的实现是为了保护一段短小的临界区代码,保证这个临界区的操作是原子的。在 Linux 内核中,自旋锁通常用于包含内核数据结构的操作 (如 wait_queue 等),用于保证操作内核中的数据结构的原子性。如果内核控制路径发现自旋锁可用,则 “锁定” 被设置,而代码继续进入临界区。相反,如果内核控制路径发现锁运行在另一个 CPU 的内核控制路径 “锁定”,就原地 “旋转” 并重复检查这个锁,直到这个锁可用为止。

图片

自旋锁与UP、SMP的关系:根据自旋锁的逻辑,自旋锁的临界区是不能休眠的。在UP下,只有一个CPU,如果我们执行到了临界区,此时自旋锁是不可能处于加锁状态的。因为我们正在占用CPU,又没有其它的CPU,其它的临界区要么没有到来、要么已经执行过去了。所以我们是一定能获得自旋锁的,所以自旋锁对UP来说是没有意义的。但是为了在UP和SMP下代码的一致性,UP下也有自旋锁,但是自旋锁的定义就变成了空结构体,自旋锁的加锁操作就退化成了禁用抢占,自旋锁的解锁操作也就退化成了开启抢占。所以说自旋锁只适用于SMP,但是在UP下也提供了兼容操作。

自旋锁一开始的实现是很简单的,后来随着众核时代的到来,自旋锁的公平性成了很大的问题,于是内核实现了票号自旋锁(ticket spinlock)来保证加锁的公平性。后来又发现票号自旋锁有很大的性能问题,于是又开始着力解决自旋锁的性能问题。先是开发出了MCS自旋锁,确实解决了性能问题,但是它改变了自旋锁的接口,所以没办法替代自旋锁。然后又有人对MCS自旋锁进行改造从而开发出了队列自旋锁(queue spinlock)。队列自旋锁既解决了自旋锁的性能问题,又保持了自旋锁的原有接口,非常完美。现在内核使用的自旋锁是队列自旋锁。下面我们用一张图来总结一下自旋锁的发展史(x86平台)。

二、自旋锁的特性

⑴忙等待特性

当一个线程尝试获取自旋锁,而该锁已经被其他线程占用时,这个线程不会进入阻塞状态(如挂起并让出 CPU)。相反,它会在一个循环中不断地检查(“自旋”)锁是否被释放,这个过程是忙等待(busy - waiting)的过程。例如,在简单的代码实现中可能会有如下形式:

while (test_and_set(&lock));

其中test_and_set是一个原子操作,用于检查锁是否被占用并尝试获取锁,如果锁被占用则返回真,循环就会一直执行,直到锁被释放。这种忙等待的方式可以避免线程进入睡眠和唤醒的开销,在锁被占用时间很短的情况下能快速获取锁。

⑵原子性操作保证

自旋锁的获取和释放操作必须是原子操作。原子操作是不可被中断的操作序列,在多处理器系统中,通过特殊的指令(如在 x86 架构中的LOCK指令前缀)来确保操作的原子性。以获取自旋锁为例,当一个线程执行获取锁的原子操作时,其他线程对同一把锁的获取操作必须等待这个原子操作完成。这保证了在同一时刻只有一个线程能够成功获取自旋锁,从而确保了共享资源访问的互斥性。

⑶非抢占特性(在特定实现下)

在某些自旋锁的实现中,当一个线程获取自旋锁后,它不会被强制剥夺(preemption)自旋锁,直到它主动释放锁。这与一些其他的锁机制(如某些可抢占式的互斥锁)不同,这种特性有助于简化自旋锁的实现逻辑,但是也可能导致优先级反转(priority inversion)等问题。例如,如果一个高优先级线程等待一个被低优先级线程持有的自旋锁,而低优先级线程又无法被抢占,高优先级线程就会一直处于等待状态,直到低优先级线程释放锁。

⑷轻量级同步机制

自旋锁相对于其他更复杂的同步机制(如信号量、条件变量等)来说,它的实现比较简单,并且没有复杂的等待队列管理等操作。它通常只需要使用少量的机器指令来实现获取和释放操作,所以它在一些简单的共享资源保护场景下,特别是在共享资源的临界区代码执行时间很短(通常小于两次线程上下文切换的时间)的情况下,性能比较好。例如,在对一个简单的计数器进行原子更新的场景中,使用自旋锁可以有效地保护计数器的更新操作,避免多个线程同时更新导致的数据不一致。

⑸有限的适用性

由于自旋锁在等待锁的过程中会一直占用 CPU 资源,所以如果锁被占用的时间较长,就会导致等待的线程长时间地占用 CPU 进行空转,浪费 CPU 资源。因此,自旋锁适用于保护临界区代码执行时间非常短的共享资源,如单个机器字长的数据(如整数变量)的原子更新等场景。如果临界区执行时间较长,就应该考虑使用其他更适合的同步机制,如互斥锁结合线程阻塞和唤醒机制。

三、自旋锁相关API

3.1初始化自旋锁

在 Linux 内核中,初始化自旋锁可以使用spin_lock_init函数。例如:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>

spinlock_t my_lock;

static int __init my_init(void) {
    printk(KERN_INFO "Initializing module.\n");
    spin_lock_init(&my_lock);
    return 0;
}

static void __exit my_exit(void) {
    printk(KERN_INFO "Exiting module.\n");
}

module_init(my_init);
module_exit(my_exit);

spin_lock_init函数主要用于设置锁的状态为未锁定,并可能初始化一些与锁相关的其他信息。

3.2加锁操作

①spin_lock:使用spin_lock函数可以获取指定的自旋锁,即加锁。例如:

spinlock_t lock;
void functionA() {
    spin_lock(&lock);
    // 临界区代码
    spin_unlock(&lock);
}

②spin_lock_irqsave:这个函数在获取锁之前保存中断状态并禁止本地中断。例如:

spinlock_t lock;
unsigned long flags;
void functionA() {
    spin_lock_irqsave(&lock, flags);
    // 临界区代码
    spin_unlock_irqrestore(&lock, flags);
}

③spin_lock_irq:禁止本地中断并获取自旋锁,如果确定处理器上没有禁止中断,可以使用这个函数代替spin_lock_irqsave,无需跟踪中断状态。

④spin_lock_bh:在获取锁之前禁止软件中断,但硬件中断保持打开。

3.3尝试加锁操作

spin_trylock和spin_trylock_bh函数用于尝试获取指定的自旋锁,如果没有获取到就返回 0,获取成功返回非零值。例如:

spinlock_t lock;
int result = spin_trylock(&lock);
if (result) {
    // 获取到锁,执行临界区代码
    spin_unlock(&lock);
} else {
    // 未获取到锁,执行其他操作
}

3.4解锁操作

  1. spin_unlock:释放自旋锁。

  2. spin_unlock_irqrestore:将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。传递给这个函数的flags参数必须是传递给spin_lock_irqsave的同一个变量,并且必须在同一个函数中调用spin_lock_irqsave和spin_unlock_irqrestore,否则可能会破坏某些体系。

  3. spin_unlock_irq:激活本地中断,并释放自旋锁。

  4. spin_unlock_bh:恢复软件中断状态并释放自旋锁。

四、自旋锁的使用场景

⑴多处理器系统中的短时间资源访问保护

在多处理器系统中,当多个线程需要访问共享资源,并且对共享资源的访问时间很短(例如,对一个共享的计数器进行简单的加 1 操作)时,自旋锁是一个很好的选择。

例如,在一个高性能计算的场景中,多个处理器核心可能会同时对一个全局的任务计数器进行更新。假设这个计数器用于记录已经完成的计算任务数量。每个核心在完成一个小任务后,会通过自旋锁来保护对计数器的更新操作。由于更新操作很简单,只是一个原子的加 1 操作,使用自旋锁可以快速地获取锁、更新计数器并释放锁,避免了线程阻塞和唤醒的开销。

⑵中断处理与线程同步

当一个系统既要处理中断,又要保证线程对共享资源的安全访问时,自旋锁可以用于同步。在这种情况下,中断处理程序和普通线程可能会竞争访问相同的资源。

例如,在一个网络设备驱动程序中,网络数据包的接收可能会触发中断。中断处理程序会将接收到的数据包放入一个共享的缓冲区,而用户线程可能会从这个缓冲区中读取数据包进行处理。为了防止中断处理程序和用户线程同时访问缓冲区导致数据混乱,自旋锁可以用于保护缓冲区的访问。因为中断处理通常需要快速完成,使用自旋锁可以在短时间内获取和释放锁,保证数据的完整性。

⑶实现简单的互斥访问机制

对于一些简单的多线程程序,程序员希望以一种简单的方式来实现互斥访问共享资源。自旋锁的实现相对简单,不需要复杂的线程等待队列等管理机制。

比如,在一个简单的多线程文件读取程序中,多个线程可能需要访问一个配置文件来获取读取文件的起始位置等信息。使用自旋锁可以很方便地确保每次只有一个线程访问配置文件,避免文件读取位置等信息被错误地修改。

⑷避免复杂的线程调度开销

在某些对性能要求极高的场景下,线程的阻塞和唤醒会带来较大的开销,包括保存和恢复线程上下文等操作。自旋锁通过忙等待的方式,可以避免这些开销。

例如,在一个实时性要求很高的控制系统中,多个传感器数据采集线程和控制算法执行线程可能会共享一些实时性要求极高的中间数据。如果使用阻塞式的锁,频繁的线程调度可能会导致数据更新不及时。而自旋锁可以让线程在等待锁的过程中快速地获取锁并更新数据,减少因线程调度带来的延迟。

五、自旋锁的实现原理

⑴原子操作基础

自旋锁的实现依赖于原子操作。原子操作是指在执行过程中不会被中断的操作,它可以保证在多处理器环境下操作的完整性和一致性。在现代处理器中,有专门的指令来支持原子操作。例如,在 x86 架构中,带有LOCK前缀的指令(如LOCK XCHG)可以确保操作的原子性。

原子操作主要用于实现自旋锁的两个关键部分:获取锁和释放锁。以一个简单的基于整数的自旋锁为例,通常用一个整数变量来表示锁的状态,比如0表示锁未被占用,1表示锁被占用。获取锁的过程就是通过原子操作将这个变量从0变为1,如果发现变量已经是1,则表示锁被占用,需要进行自旋等待。

⑵获取锁(lock_acquire)实现原理

最常见的获取锁的实现方式是使用test - and - set(测试并设置)原子操作。这个操作会先读取变量的值,然后设置变量的值为1,并且返回读取的值。

假设我们有一个自旋锁变量spinlock,代码实现可能如下:

int test_and_set(int *lock) {
    int old_value = *lock;
    *lock = 1;
    return old_value;
}
void lock_acquire(int *spinlock) {
    while (test_and_set(spinlock));
}

当一个线程调用lock_acquire函数时,它会进入一个循环。在每次循环中,test - and - set操作会检查锁是否被占用。如果锁未被占用(返回0),那么当前线程成功获取锁,循环结束;如果锁被占用(返回1),线程会继续循环,不断地检查锁是否被释放,这就是所谓的 “自旋” 过程。

⑶释放锁(lock_release)实现原理

释放锁的过程相对简单。通常是将表示自旋锁的变量设置为0,以表示锁已经被释放。不过,这个操作同样需要是原子操作,以确保在多处理器环境下的正确性。

代码实现可能如下:

void lock_release(int *spinlock) {
    *spinlock = 0;
}

当一个线程完成对共享资源的访问后,它会调用lock_release函数,将自旋锁的状态重置为0,这样其他正在自旋等待的线程就可以获取锁了。

⑷内存屏障(Memory Barrier)的考虑

在自旋锁的实现中,尤其是在多处理器环境下,还需要考虑内存屏障的问题。内存屏障是一种硬件或软件机制,用于确保指令执行的顺序和内存访问的顺序。

例如,在获取锁之后,需要确保在访问共享资源之前,之前的所有内存操作都已经完成,并且在释放锁之前,对共享资源的访问操作都已经完成并更新到内存中。这是因为处理器可能会对指令进行重排序,而内存屏障可以防止这种重排序对自旋锁的正确性产生影响。在一些高级编程语言或处理器特定的代码中,可能会有专门的指令(如mfence、sfence和lfence在 x86 架构中)来实现内存屏障的功能。

六、源码实现分析

以 x86 架构为例,当申请自旋锁时,arch_spin_trylock函数会判断自旋锁是否被占用,如果没被占用,尝试原子性地更新lock中的head_tail的值,将tail + 1,返回是否加锁成功。如果加锁不成功,会进入do_raw_spin_lock函数,在循环中不断读取新的head值,直到head和tail相等,表示锁可用。在释放锁时,会将tail加 1,并把之前的值记录下来,完成加锁操作。以下是对这段关于 x86 架构下自旋锁源码实现的详细分析:

⑴申请自旋锁(arch_spin_trylock函数)

初始判断:当调用arch_spin_trylock函数申请自旋锁时,首先会进行一个判断操作,即检查自旋锁是否已经被其他线程或进程占用。这一步是非常关键的,因为它确定了后续操作的走向。如果自旋锁当前未被占用,那么就有机会尝试获取该锁。

原子性更新尝试:若自旋锁未被占用,接下来会尝试原子性地更新lock中的head_tail的值。这里的head_tail应该是一个用于记录自旋锁状态相关信息的数据结构中的成员,具体可能与排队等待获取锁的线程信息等有关(虽然从给定描述中不太能明确其确切含义,但大致可推测是和锁的获取顺序、状态等相关)。

将tail的值加1,这个操作是原子性的,以确保在多处理器环境下的正确性。原子性操作能保证在执行这个更新操作时,不会被其他处理器的操作所干扰,从而准确地反映锁的获取状态。

然后根据这个原子性更新的结果返回是否加锁成功。如果更新成功,意味着当前线程成功获取了自旋锁;如果更新失败,说明在尝试更新的瞬间,可能有其他线程也在竞争获取该锁,并且已经成功获取了,所以当前线程加锁不成功。

⑵加锁不成功的处理(do_raw_spin_lock函数)

循环等待机制:当arch_spin_trylock函数加锁不成功时,会进入do_raw_spin_lock函数。在这个函数中,设置了一个循环等待的机制。

在循环中,不断地读取新的head值。这里的head同样是和head_tail相关的数据结构中的成员,推测其与锁的排队等待状态或者已经获取锁的线程信息等有关。

持续读取head值的目的是为了判断锁是否可用。通过不断对比head和tail的值,当两者相等时,表示锁可用。这是因为可能存在一种基于head和tail差值来判断是否有线程正在占用锁或者排队等待获取锁的逻辑。例如,当head和tail相等时,可能意味着当前没有线程在占用锁,或者之前排队等待的线程都已经完成了对锁的使用并释放了锁,所以此时当前线程就有机会再次尝试获取锁。

⑶释放自旋锁

更新tail值并记录:在释放自旋锁时,首先会将tail的值加1。这个操作同样应该是原子性的,以保证在多处理器环境下的正确性。

并且会把之前tail的值记录下来。虽然不太明确记录下来具体用于何种目的,但可能与后续的一些统计分析(比如查看锁的使用频率、排队情况等)或者是为了确保释放锁的操作能够完整且准确地被其他等待线程感知到有关。

通过这样的操作,完成了自旋锁的释放过程,使得其他正在等待获取自旋锁的线程能够根据新的head和tail的值来判断锁是否可用,进而有机会获取到自旋锁。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Moretl 增量文件采集工具

永久免费: <下载> <使用说明> 用途 定时全量或增量采集工控机,电脑文件或日志. 优势 开箱即用: 解压直接运行.不需额外下载.管理设备: 后台统一管理客户端.无人值守: 客户端自启动,自更新.稳定安全: 架构简单,兼容性好,通过授权控制访问. 架构 技术架构: Asp…

【Uniapp】关于实现下拉刷新的三种方式

在小程序、h5等地方中&#xff0c;常常会用到下拉刷新这个功能&#xff0c;今天来讲解实现这个功能的三种方式&#xff1a;全局下拉刷新&#xff0c;组件局部下拉刷新&#xff0c;嵌套组件下拉刷新。 全局下拉刷新 这个方式简单&#xff0c;性能佳&#xff0c;最推荐&#xf…

九.Spring Boot使用 ShardingSphere + MyBatis + Druid 进行分库分表

文章目录 前言一、引入依赖二、创建一个light-db_1备用数据库三、配置文件 application-dev.yml四、创建shardingsphere-config.yml完整项目结构 五、测试总结 前言 在现代化微服务架构中&#xff0c;随着数据量的不断增长&#xff0c;单一数据库已难以满足高可用性、扩展性和…

游戏引擎学习第101天

回顾当前情况 昨天的进度基本上完成了所有内容&#xff0c;但我们还没有进行调试。虽然我们在运行时做的事情大致上是对的&#xff0c;但还是存在一些可能或者确定的bug。正如昨天最后提到的&#xff0c;既然现在时间晚了&#xff0c;就不太适合开始调试&#xff0c;所以今天我…

鸿蒙HarmonyOS NEXT开发:横竖屏切换开发实践

文章目录 一、概述二、窗口旋转说明1、配置module.json5的orientation字段2、调用窗口的setPreferredOrientation方法 四、性能优化1、使用自定义组件冻结2、对图片使用autoResize3、排查一些耗时操作 四、常见场景示例1、视频类应用横竖屏开发2、游戏类应用横屏开发 五、其他常…

【新品解读】AI 应用场景全覆盖!解码超高端 VU+ FPGA 开发平台 AXVU13F

「AXVU13F」Virtex UltraScale XCVU13P Jetson Orin NX 继发布 AMD Virtex UltraScale FPGA PCIE3.0 开发平台 AXVU13P 后&#xff0c;ALINX 进一步研究尖端应用市场&#xff0c;面向 AI 场景进行优化设计&#xff0c;推出 AXVU13F。 AXVU13F 和 AXVU13P 采用相同的 AMD Vir…

(篇六)基于PyDracula搭建一个深度学习的软件之新版本ultralytics-8.3.28调试

ultralytics-8.3.28版本debug记录 1传入文件 代码太多不粘贴在这里了&#xff0c;完整代码写在了篇三 def open_src_file(self):config_file config/fold.jsonconfig json.load(open(config_file, r, encodingutf-8))open_fold config[open_fold]if not os.path.exists(op…

计算机毕业设计PySpark+hive招聘推荐系统 职位用户画像推荐系统 招聘数据分析 招聘爬虫 数据仓库 Django Vue.js Hadoop

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

docker配置镜像加速

1.配置方法见阿里云 图1 图2 图3 CentOS脚本 阿里云、腾讯云的镜像仓库从2022年就没有更新了&#xff0c;所以添加以下这么多仓库 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": ["https://g4f7bois.mirro…

互联网大厂中面试的高频计算机网络问题及详解

前言 哈喽各位小伙伴们,本期小梁给大家带来了互联网大厂中计算机网络部分的高频面试题,本文会以通俗易懂的语言以及图解形式描述,希望能给大家的面试带来一点帮助,祝大家offer拿到手软!!! 话不多说,我们立刻进入本期正题! 一、计算机网络基础部分 1 …

位图,晶圆MAP 边缘算法

例如这样的一张图: 如果想要求外边缘点&#xff0c;即红色区域,首先遍历所有点位&#xff0c;求出每行每列X轴和Y轴的最大值MAX和最小值MIN。然后再次遍历每个点&#xff0c;判断该点的X值&#xff0c;Y值是否是最大值或者最小值&#xff0c;如果是&#xff0c;那么它就是外边…

微信小程序医院挂号系统

第3章 系统设计 3.1系统体系结构 系统的体系结构非常重要&#xff0c;往往决定了系统的质量和生命周期。针对不同的系统可以采用不同的系统体系结构。本系统为微信小程序医院挂号系统&#xff0c;属于开放式的平台&#xff0c;所以在管理端体系结构中采用B/s。B/s结构抛弃了固…

建筑兔零基础自学python记录18|实战人脸识别项目——视频检测07

本次要学视频检测&#xff0c;我们先回顾一下图片的人脸检测建筑兔零基础自学python记录16|实战人脸识别项目——人脸检测05-CSDN博客 我们先把上文中代码复制出来&#xff0c;保留红框的部分。 ​ 然后我们来看一下源代码&#xff1a; import cv2 as cvdef face_detect_demo(…

5g基站测试要求和关键点

5G基站的测试要求涉及多个方面&#xff0c;以确保其性能、覆盖能力、稳定性和合规性。以下是5G基站测试的主要要求和关键点&#xff1a; 一、基础性能测试 射频&#xff08;RF&#xff09;性能测试 发射机性能&#xff1a;验证基站的发射功率、频率误差、调制质量&#xff08;E…

【Git版本控制器】:第一弹——Git初识,Git安装,创建本地仓库,初始化本地仓库,配置config用户名,邮箱信息

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ 相关笔记&#xff1a; https://blog.csdn.net/dj…

20250213 隨筆 雪花算法

雪花算法&#xff08;Snowflake Algorithm&#xff09; 雪花算法&#xff08;Snowflake&#xff09; 是 Twitter 在 2010 年開發的一種 分布式唯一 ID 生成算法&#xff0c;它可以在 高併發場景下快速生成全局唯一的 64-bit 長整型 ID&#xff0c;且不依賴資料庫&#xff0c;具…

22.4、Web应用漏洞分析与防护

目录 Web应用安全概述DWASP Top 10Web应用漏洞防护 - 跨站脚本攻击XSSWeb应用漏洞防护 - SQL注入Web应用漏洞防护 - 文件上传漏洞Web应用漏洞防护 - 跨站脚本攻击XSS Web应用安全概述 技术安全漏洞&#xff0c;主要是因为技术处理不当而产生的安全隐患&#xff0c;比如SQL注入…

【Vue3 入门到实战】15. 组件间通信

目录 1. Props 2. 自定义事件 3. mitt 4. v-model 4.1 v-model用在html标签上 4.2 v-model用在组件标签上 4.3 v-model 命名 4.4 总结 5. $attrs 6. $refs 和 $parent 7. provide 和 inject 8. pinia 9. slot 插槽 10. 总结 组件通信是指在不同组件之间传递数据…

云原生AI Agent应用安全防护方案最佳实践(上)

当下&#xff0c;AI Agent代理是一种全新的构建动态和复杂业务场景工作流的方式&#xff0c;利用大语言模型&#xff08;LLM&#xff09;作为推理引擎。这些Agent代理应用能够将复杂的自然语言查询任务分解为多个可执行步骤&#xff0c;并结合迭代反馈循环和自省机制&#xff0…

本地部署DeepSeek摆脱服务器繁忙

由于图片和格式解析问题&#xff0c;可前往 阅读原文 最近DeepSeek简直太火了&#xff0c;频频霸榜热搜打破春节的平静&#xff0c;大模型直接开源让全球科技圈都为之震撼&#xff01;再次证明了中国AI的换道超车与崛起 DeepSeek已经成了全民ai&#xff0c;使用量也迅速上去了…