linux 内核同步互斥技术之自旋锁

自旋锁

自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待(也称为忙等待)。
进程、软中断和硬中断都可以使用自旋锁。
目前内核的自旋锁是基于排队的自旋锁( queued spinlock,也称为“FIFO ticket spinlock”),算法类似于银行柜台的排队叫号。
(1)锁拥有排队号和服务号,服务号是当前占有锁的进程的排队号。
(2)每个进程申请锁的时候,首先申请一个排队号,然后轮询锁的服务号是否等于自己的排队号,如果等于,表示自己占有锁,可以进入临界区,否则继续轮询。
(3)当进程释放锁时,把服务号加 1,下一个进程看到服务号等于自己的排队号,退出自旋,进入临界区。

自旋锁的定义如下:
include/linux/spinlock_types.h
typedef struct spinlock {
    union {
        struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
        struct {
            u8 __padding[LOCK_PADSIZE];
            struct lockdep_map dep_map;
        };
#endif
    };
} spinlock_t;

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
    unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic, owner_cpu;
    void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
可以看到,数据类型 spinlock 对数据类型 raw_spinlock 做了封装, spinlock 和 raw_spinlock(原始自旋锁)有什么关系?
Linux 内核有一个实时内核分支(开启配置宏 CONFIG_PREEMPT_RT)来支持硬实时特性,内核主线只支持软实时。
对于没有打上实时内核补丁的内核, spinlock 只是封装 raw_spinlock,它们完全一样。如果打上实时内核补丁,那么 spinlock 使用实时互斥锁保护临界区,在临界区内可以被抢占和睡眠,但 raw_spinlock 还是自旋锁。
目前主线版本还没有合并实时内核补丁,说不定哪天就会合并进来,为了使代码可以兼容实时内核,最好坚持 3 个原则。
(1)尽可能使用 spinlock。
(2)绝对不允许被抢占和睡眠的地方,使用 raw_spinlock,否则使用 spinlock。
(3)如果临界区足够小,使用 raw_spinlock。

各种处理器架构需要自定义数据类型 arch_spinlock_t, ARM64 架构的定义如下:
arch/arm64/include/asm/spinlock_types.h
typedef struct {
#ifdef __AARCH64EB__
    u16 next;
    u16 owner;
#else
    u16 owner;
    u16 next;
#endif
} __aligned(4) arch_spinlock_t;
成员 next 是排队号,成员 owner 是服务号。

定义并且初始化静态自旋锁的方法如下:DEFINE_SPINLOCK(x);
在运行时动态初始化自旋锁的方法如下:spin_lock_init(x);

申请自旋锁的函数如下。
(1) void spin_lock(spinlock_t *lock);
申请自旋锁,如果锁被其他处理器占有,当前处理器自旋等待。
(2) void spin_lock_bh(spinlock_t *lock);
申请自旋锁,并且禁止当前处理器的软中断。
(3) void spin_lock_irq(spinlock_t *lock);
申请自旋锁,并且禁止当前处理器的硬中断。
(4) spin_lock_irqsave(lock, flags);
申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。
(5) int spin_trylock(spinlock_t *lock);
申请自旋锁,如果申请成功,返回 1;如果锁被其他处理器占有,当前处理器不等待,立即返回 0。
(6)int spin_is_locked(spinlock_t *lock)
判断自旋锁是否被占用(不自旋等待,占用返回非0、没占用则返回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);
释放自旋锁,并且恢复当前处理器的硬中断状态。

定义并且初始化静态原始自旋锁的方法如下:DEFINE_RAW_SPINLOCK(x);
在运行时动态初始化原始自旋锁的方法如下:raw_spin_lock_init (x);

申请原始自旋锁的函数如下。
(1) raw_spin_lock(lock)
申请原始自旋锁,如果锁被其他处理器占有,当前处理器自旋等待。
(2) raw_spin_lock_bh(lock)
申请原始自旋锁,并且禁止当前处理器的软中断。
(3) raw_spin_lock_irq(lock)
申请原始自旋锁,并且禁止当前处理器的硬中断。
(4) raw_spin_lock_irqsave(lock, flags)
申请原始自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。
(5) raw_spin_trylock(lock)
申请原始自旋锁,如果申请成功,返回 1;如果锁被其他处理器占有,当前处理器不等待,立即返回 0。

释放原始自旋锁的函数如下。
(1) raw_spin_unlock(lock)
(2) raw_spin_unlock_bh(lock)
释放原始自旋锁,并且开启当前处理器的软中断。
(3) raw_spin_unlock_irq(lock)
释放原始自旋锁,并且开启当前处理器的硬中断。
(4) raw_spin_unlock_irqrestore(lock, flags)
释放原始自旋锁,并且恢复当前处理器的硬中断状态。
在多处理器系统中,函数 spin_lock()负责申请自旋锁,其代码如下:
spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock() -> do_raw_spin_lock() -> arch_spin_lock()
arch/arm64/include/asm/spinlock.h
1 static inline void arch_spin_lock(arch_spinlock_t *lock)
2 {
3   nsigned int tmp;
4   arch_spinlock_t lockval, newval;
5   
6   sm volatile(
7       RM64_LSE_ATOMIC_INSN(
8           * LL/SC */
9            prfm pstl1strm, %3\n"
10          "1: ldaxr %w0, %3\n"
11          " add %w1, %w0, %w5\n"
12          " stxr %w2, %w1, %3\n"
13          " cbnz %w2, 1b\n",
14          /* 大系统扩展的原子指令 */
15          " mov %w2, %w5\n"
16          " ldadda %w2, %w0, %3\n"
17          __nops(3)
18      )
19      
20      /* 我们得到锁了吗? */
21      " eor %w1, %w0, %w0, ror #16\n"
22      " cbz %w1, 3f\n"
23      " sevl\n"
24      "2: wfe\n"
25      " ldaxrh %w2, %4\n"
26      " eor %w1, %w2, %w0, lsr #16\n"
27      " cbnz %w1, 2b\n"
28      /* 得到锁,临界区从这里开始*/
29      "3:"
30      : "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
31      : "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
32      : "memory");
33 }
第 7~18 行代码,申请排队号,然后把自旋锁的排队号加 1,这是一个原子操作,有两种实现方法。
1)第 9~13 行代码,使用指令 ldaxr(带有获取语义的独占加载)和 stxr(独占存储)实现,指令 ldaxr 带有获取语义,后面的加载/存储操作必须在指令 ldaxr之后被观察到。
2)第 15 行和第 16 行代码,如果处理器支持大系统扩展,那么使用带有获取语义的原子加法指令 ldadda 实现, 指令 ldadda 带有获取语义, 后面的加载/存储操作必须在指令 ldadda之后被观察到。
第 21 行和第 22 行代码,如果服务号等于当前进程的排队号,进入临界区。
第 24~27 行代码,如果服务号不等于当前进程的排队号,那么自旋等待。使用指令ldaxrh(带有获取语义的独占加载, h 表示 halfword,即 2 字节)读取服务号,指令 ldaxrh带有获取语义,后面的加载/存储操作必须在指令 ldaxrh 之后被观察到。
第 23 行代码, sevl( send event local)指令的功能是发送一个本地事件,避免错过其他处理器释放自旋锁时发送的事件。
第 24 行代码, wfe(wait for event)指令的功能是使处理器进入低功耗状态,等待事件。

函数 spin_unlock()负责释放自旋锁,其代码如下
spin_unlock() -> raw_spin_unlock() -> _raw_spin_unlock() -> __raw_spin_unlock() ->do_raw_spin_unlock() -> arch_spin_unlock()
arch/arm64/include/asm/spinlock.h
1 static inline void arch_spin_unlock(arch_spinlock_t *lock)
2 {
3   nsigned long tmp;
4   
5   sm volatile(ARM64_LSE_ATOMIC_INSN(
6           * LL/SC */
7            ldrh %w1, %0\n"
8            add %w1, %w1, #1\n"
9            stlrh %w1, %0",
10          /* 大系统扩展的原子指令 */
11          " mov %w1, #1\n"
12          " staddlh %w1, %0\n"
13          __nops(1))
14      : "=Q" (lock->owner), "=&r" (tmp)
15      :
16      : "memory");
17 }
把自旋锁的服务号加 1,有如下两种实现方法。
第 7~9 行代码,使用指令 ldrh(加载, h 表示 halfword,即 2 字节)和 stlrh(带有释放语义的存储)实现,指令 stlrh 带有释放语义,前面的加载/存储操作必须在指令 stlrh 之前被观察到。因为一次只能有一个进程进入临界区,所以只有一个进程把自旋锁的服务号加 1,不需要是原子操作。
第 11 行和第 12 行代码,如果处理器支持大系统扩展,那么使用带有释放语义的原子加法指令 staddlh 实现,指令 staddlh 带有释放语义,前面的加载/存储操作必须在指令 staddlh 之前被观察到。

在单处理器系统中,自旋锁是空的。
include/linux/spinlock_types_up.h
typedef struct { } arch_spinlock_t;
函数 spin_lock()只是禁止内核抢占。
spin_lock() -> raw_spin_lock() -> _raw_spin_lock()
include/linux/spinlock_api_up.h
#define _raw_spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); ___LOCK(lock); } while (0)
#define ___LOCK(lock) \
do { __acquire(lock); (void)(lock); } while (0)

多CPU与单CPU的spin_lock使用上的区别:
1)    如果只要和其他CPU 互斥,就要用spin_lock/spin_unlock
2)    如果要和irq及其他CPU互斥,就要用 spin_lock_irq/spin_unlock_irq
3)    如果既要和irq及其他CPU互斥,又要保存 EFLAG的状态,就要用spin_lock_irqsave/spin_unlock_irqrestore
4)    如果要和bh及其他CPU互斥,就要用spin_lock_bh/spin_unlock_bh
5)    如果不需要和其他CPU互斥,只要和irq互斥,则用local_irq_disable/local_irq_enable
6)    如果不需要和其他CPU互斥,只要和bh互斥,则用local_bh_disable/local_bh_enable

值得指出的是,对同一个数据的互斥,在不同的内核执行路径中, 所用的形式有可能不同(见下面的例子)。 
1.有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好;

2.spin_lock_irqsave保存访问共享资源前的中断标志,然后失效中断;spin_unlock_irqrestore将恢复访问共享资源前的中断标志而不是直接使能中断;

3.如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用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是用软中断实现的。

4.对tasklet和timer和互斥操作,如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此;如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。

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


实例

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

DEFINE_SPINLOCK(sp_lock);
struct task_struct * task1;
struct task_struct * task2;
struct task_struct * task3;
int i = 100000;

int thread_print_first(void *p)
{
    if(kthread_should_stop()){
        return 0;
    }
    
    printk(KERN_ALERT"Hello World:first writer cpu=%d\n",raw_smp_processor_id());
    spin_lock(&sp_lock);
    printk(KERN_ALERT"Hello World:first writting cpu=%d\n",raw_smp_processor_id());
    while(i--);
    spin_unlock(&sp_lock);
    printk(KERN_ALERT"Hello World:first written cpu=%d\n",raw_smp_processor_id());
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    return 0;
}
int thread_print_second(void *p)
{
    if(kthread_should_stop()){
        return 0;
    }
     //msleep(1000);
    printk(KERN_ALERT"Hello World:second writer cpu=%d\n",raw_smp_processor_id());
    spin_lock(&sp_lock);
    printk(KERN_ALERT"Hello World:second writting cpu=%d\n",raw_smp_processor_id());
    while(i--);
    spin_unlock(&sp_lock);
    printk(KERN_ALERT"Hello World:second written cpu=%d\n",raw_smp_processor_id());
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    return 0;
}

int thread_print_third(void *p)
{
    if(kthread_should_stop()){
        return 0;
    }
     //msleep(2000);
    printk(KERN_ALERT"Hello World:third writer cpu=%d\n",raw_smp_processor_id());
    spin_lock(&sp_lock);
    printk(KERN_ALERT"Hello World:third writting cpu=%d\n",raw_smp_processor_id());
    while(i--);
    spin_unlock(&sp_lock);
    printk(KERN_ALERT"Hello World:third written cpu=%d\n",raw_smp_processor_id());
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    return 0;
}

static int hello_init(void)
{
    printk(KERN_ALERT"Hello World enter\n");
    
    task1 = kthread_create(thread_print_first,NULL,"first");
    if(IS_ERR(task1))
    {
        printk(KERN_ALERT"kthread_create error!\n");
        return -1;
    }
    
    task2 = kthread_create(thread_print_second,NULL,"second");
    if(IS_ERR(task2))
    {
        printk(KERN_ALERT"kthread_create error!\n");
        kthread_stop(task1);
        return -1;
    }
    
    task3 = kthread_create(thread_print_third,NULL,"third");
    if(IS_ERR(task3))
    {
        printk(KERN_ALERT"kthread_create error!\n");
        kthread_stop(task1);
        kthread_stop(task2);
        return -1;
    }
    
    kthread_bind(task1,1);
    kthread_bind(task2,0);
    kthread_bind(task3,1);
    wake_up_process(task1);
    wake_up_process(task2);
    wake_up_process(task3);

    return 0;
}
static void hello_exit(void)
{
    int ret;
    if (!IS_ERR(task1)) {
        ret = kthread_stop(task1);
        printk("<<<<<<<<task1 exit, ret = %d\n", ret);
    }
        
    if (!IS_ERR(task2)) {
        ret = kthread_stop(task2);
        printk("<<<<<<<<task2 exit, ret = %d\n", ret);
    }
    
    if (!IS_ERR(task3)) {
        ret = kthread_stop(task3);
        printk("<<<<<<<<task3 exit, ret = %d\n", ret);
    }

    printk(KERN_ALERT"hello world exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt5 CMake环境配置

Qt5 CMake环境配置 设置Qt路径 有两种方法 Qt5_DIR&#xff0c;使用这个变量&#xff0c;必须把路径设置到Qt5Config.cmake所在文件夹&#xff0c;也就是安装目录下的lib/cmake/Qt5CMAKE_PREFIX_PATH&#xff0c;只需要设置到安装目录就可以了&#xff0c;这个目录就是bin、…

OpenStack和Docker结合?为何现在流行?

为何现在流行OpenStack和Docker结合&#xff1f; 结合的好处 1、资源管理与调度灵活&#xff1a; OpenStack提供了完善的虚拟机管理能力&#xff0c;而Kubernetes&#xff08;使用Docker作为容器运行环境&#xff09;在容器调度方面非常高效。将两者结合&#xff0c;可以实现…

RNN介绍及Pytorch源码解析

介绍一下RNN模型的结构以及源码&#xff0c;用作自己复习的材料。 RNN模型所对应的源码在&#xff1a;\PyTorch\Lib\site-packages\torch\nn\modules\RNN.py文件中。 RNN的模型图如下&#xff1a; 源码注释中写道&#xff0c;RNN的数学公式&#xff1a; 表示在时刻的隐藏状态…

ES6学习(三):Set和Map容器的使用

Set容器 set的结构类似于数组,但是成员是唯一且不会重复的。 创建的时候需要使用new Set([])的方法 创建Set格式数据 let set1 new Set([])console.log(set1, set1)let set2 new Set([1, 2, 3, 4, 5])console.log(set2, set2) 对比看看Set中唯一 let set3 new Set([1, 1,…

职场记5:勇闯深圳,追梦职场

上一篇《职场记4&#xff1a;我在这家公司的美好时光》 为了追求更好的发展&#xff0c;我毅然决然地离开了令我感到无比安逸的工作环境&#xff0c;去大城市深圳闯一闯。这个城市对我来说充满了未知和挑战&#xff0c;但我相信只有勇敢地面对未知&#xff0c;才能找到更好的自…

多架构容器镜像构建实战

最近在一个国产化项目中遇到了这样一个场景&#xff0c;在同一个 Kubernetes 集群中的节点是混合架构的&#xff0c;也就是说&#xff0c;其中某些节点的 CPU 架构是 x86 的&#xff0c;而另一些节点是 ARM 的。为了让我们的镜像在这样的环境下运行&#xff0c;一种最简单的做法…

Rust语言基础语法使用

1.安装开发工具: RustRover JetBrains: Essential tools for software developers and teams 下载: RustRover: Rust IDE by JetBrains 下载成功后安装并启动RustRover 安装中文语言包插件 重启RustRover生效

vue3引入echarts正确姿势

echarts文档地址&#xff1a; echarts官网地址 echarts配置手册 echarts 模板地址 1、安装 &#xff08;1&#xff09;安装echarts包 npm install echarts --save 或者 cnpm install echarts --save&#xff08;2&#xff09;安装vue echarts工具包 npm install echart…

持续集成交付CICD:Jenkins使用基于SaltStack的CD流水线下载Nexus制品

目录 一、理论 1.salt常用命令 二、实验 1.SaltStack环境检查 2.Jenkins使用基于SaltStack的CD流水线下载Nexus制品 二、问题 1.salt未找到命令 2.salt简单测试报错 3. wget输出日志过长 一、理论 1.salt常用命令 &#xff08;1&#xff09;salt 命令 该 命令执行s…

Linux-----9、echo命令

# echo命令 echo会将输入的字符串送往标准输出&#xff0c;并在最后加上换行符。 可以理解为打印字符串。 常见选项&#xff1a; -n &#xff1a;不输出最后的换行符“\n” -e&#xff1a;解释转义字符&#xff08;字符串中出现\n、\t等特殊字符&#xff0c;则特别加以处理&a…

蓝牙物联网智慧物业解决方案

蓝牙物联网智慧物业解决方案是一种利用蓝牙技术来提高物业管理和服务效率的解决方案。它通过将蓝牙技术与其他智能设备、应用程序和云服务相结合&#xff0c;为物业管理和服务提供更便捷、高效和智能化的支持。 蓝牙物联网智慧物业解决方案包括&#xff1a; 1、设备管理&#…

JupyterNotebook安装依赖 使用conda环境

怎么使用JupyterNotebook安装依赖&#xff1f;&#xff08;使用conda环境&#xff1f;&#xff09; 预装的jupyter中有conda&#xff0c;可以进入终端执行相关命令 预装的Jupyter&#xff0c;目标用户是轻度的Jupyter用户&#xff0c;如果想使用conda的多环境等高级功能&#x…

【PostgreSQL】从零开始:(五)认识数据库-数据的相关概念

【PostgreSQL】从零开始:&#xff08;五&#xff09;认识数据库-数据的相关概念 什么是数据库数据库包含哪些东西数据库技术构成数据库系统SQL语言数据库访问技术 在正式开始讲PostgreSQL内容之前&#xff0c;我们要先要了解数据的相关概念 什么是数据库 数据库由一批数据构成…

java基础知识③:反射和注解以及Java 8的新特性

目录 一、反射和注解 1、反射 2、注解 二、Java 8的新特性 1、Lambda 表达式&#xff1a; 2、Stream API&#xff1a; 3、函数式接口&#xff1a; 4、方法引用&#xff1a; 5、接口的默认方法和静态方法&#xff1a; 6、新的时间日期API&#xff08;Date/Time API&#xff09;&…

文心一言4.0使用指南

文心一言4.0使用指南 在线体验&#xff1a;体验地址 一、文心一言能力如何 不管百度公司如何&#xff0c;就AI大模型来说&#xff0c;文心一言和其他国内产品相比&#xff0c;还是具有相当大的优势的&#xff0c;可以说是在个人的使用方面&#xff0c;我认为是最顶级的。 但…

Unity中的ShaderToy

文章目录 前言一、ShaderToy网站二、ShaderToy基本框架1、我们可以在ShaderToy网站中&#xff0c;这样看用到的GLSL文档2、void mainImage 是我们的程序入口&#xff0c;类似于片断着色器3、fragColor作为输出变量&#xff0c;为屏幕每一像素的颜色&#xff0c;alpha一般赋值为…

微信小程序单图上传和多图上传

图片上传主要用到 1、wx.chooseImage(Object object) 从本地相册选择图片或使用相机拍照。 参数 Object object 属性类型默认值必填说明countnumber9否最多可以选择的图片张数sizeTypeArray.<string>[original, compressed]否所选的图片的尺寸sourceTypeArray.<s…

从开源项目中学习如何自定义 Spring Boot Starter 小组件

前言 今天参考的开源组件Graceful Response——Spring Boot接口优雅响应处理器。 具体用法可以参考github以及官方文档。 基本使用 引入Graceful Response组件 项目中直接引入如下maven依赖&#xff0c;即可使用其相关功能。 <dependency><groupId>com.feiniaoji…

Leetcode 2454. 下一个更大元素 IV

Leetcode 2454. 下一个更大元素 IV题目 给你一个下标从 0 开始的非负整数数组 nums 。对于 nums 中每一个整数&#xff0c;你必须找到对应元素的 第二大 整数。如果 nums[j] 满足以下条件&#xff0c;那么我们称它为 nums[i] 的 第二大 整数&#xff1a; j >nums[j] > nu…

螺旋矩阵算法(leetcode第59题)

题目描述&#xff1a; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。示例 1&#xff1a;输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]] 示例 2&#xff1a;输入&#…