ShuangChenYue ShuangChenYue
首页
  • Cpp之旅
  • Cpp专栏
  • Effective_CPP
  • muduo网络库
  • Unix环境高级编程
  • Cpp提高编程
  • 计算机网络
  • 操作系统
  • 数据结构
  • Linux
  • 算法
  • 基础篇
  • MySql
  • Redis
  • 电子嵌入式通信协议
  • 深入浅出SSD
  • 文件系统
  • 汇编语言
  • STM32
  • 随笔(持续更新)
  • Git知识总结
  • Git 创建删除远程分支
  • nvm使用小结
  • 虚拟机固定 IP 地址
  • Shell 脚本学习笔记
  • VScode 插件 CodeGeeX 使用教程
  • KylinV10 将项目上传至 Github教程
  • KylinV10 安装 MySQL 教程(可防踩雷)
  • kylinV10-SP1 安装 QT
  • 高并发内存池
  • USBGUARD 项目编译环境配置
  • Power_Destory 项目
  • U 盘清除工具编译教程
  • 个人博客代码推送教程
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • MFC编程随记
  • MFC实现ini配置文件的读取
  • MFC实现点击列表头排序
  • 贴图法美化Button按钮
  • 如何高效阅读嵌入式项目代码
  • NAND Flash
  • ARM 处理器
  • 嵌入式基础知识-存储器
  • 闪存存储和制造技术概述
  • 芯片IO驱动力
  • 主流先进封装技术介绍
  • 虎牙C++技术面经
  • 金山一面复习
  • 完美世界秋招 C++ 游戏开发面经(Cpp部分)
  • 博客搭建
  • 网站收藏箱
首页
  • Cpp之旅
  • Cpp专栏
  • Effective_CPP
  • muduo网络库
  • Unix环境高级编程
  • Cpp提高编程
  • 计算机网络
  • 操作系统
  • 数据结构
  • Linux
  • 算法
  • 基础篇
  • MySql
  • Redis
  • 电子嵌入式通信协议
  • 深入浅出SSD
  • 文件系统
  • 汇编语言
  • STM32
  • 随笔(持续更新)
  • Git知识总结
  • Git 创建删除远程分支
  • nvm使用小结
  • 虚拟机固定 IP 地址
  • Shell 脚本学习笔记
  • VScode 插件 CodeGeeX 使用教程
  • KylinV10 将项目上传至 Github教程
  • KylinV10 安装 MySQL 教程(可防踩雷)
  • kylinV10-SP1 安装 QT
  • 高并发内存池
  • USBGUARD 项目编译环境配置
  • Power_Destory 项目
  • U 盘清除工具编译教程
  • 个人博客代码推送教程
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • MFC编程随记
  • MFC实现ini配置文件的读取
  • MFC实现点击列表头排序
  • 贴图法美化Button按钮
  • 如何高效阅读嵌入式项目代码
  • NAND Flash
  • ARM 处理器
  • 嵌入式基础知识-存储器
  • 闪存存储和制造技术概述
  • 芯片IO驱动力
  • 主流先进封装技术介绍
  • 虎牙C++技术面经
  • 金山一面复习
  • 完美世界秋招 C++ 游戏开发面经(Cpp部分)
  • 博客搭建
  • 网站收藏箱
  • 电子嵌入式通信协议

  • 深入浅出SSD

    • 第1章 SSD综述
    • 第2章 SSD主控和全闪存阵列
    • 第3章 SSD存储介质:闪存
      • 3.1 闪存物理结构
        • 3.1.1 闪存器件原理
        • 3.1.2 SLC、MLC 和 TLC
        • 3.1.3 闪存芯片架构
        • 3.1.4 Charge Trap 型闪存
      • 3.2 闪存特性
        • 3.2.1 闪存存在的问题
        • 3.2.1.1 闪存坏块
        • 3.2.1.2 读干扰
        • 3.2.1.3 写干扰
        • 3.2.2 闪存测试
        • 3.2.3 MLC 使用特性
        • 3.2.4 读干扰
      • 3.3 闪存数据完整性
        • 3.3.1 读错误来源
        • 3.3.2 ECC 纠错码
        • 3.3.3 RAID
  • 文件系统

  • 汇编语言

  • STM32

  • 嵌入式软件开发
  • 深入浅出SSD
霜晨月
2024-06-03
目录

第3章 SSD存储介质:闪存

# 第3章 SSD 存储介质:闪存

# 3.1 闪存物理结构

# 3.1.1 闪存器件原理

固态硬盘的工作原理很多也都是基于闪存特性的。比如,闪存在写之前必须先擦除,不能覆盖写,于是固态硬盘才需要垃圾回收(Garbage Collection,或者叫Recycle);闪存每个块(Block)擦写次数达到一定值后,这个块要么变成坏块,要么存储在上面的数据不可靠,所以固态硬盘固件必须做磨损平衡,让数据平均写在所有块上,而不是盯着几个块拼命写(不然很快固态硬盘就报废了)。

闪存是一种非易失性存储器,也就是说,掉电了数据也不会丢失。闪存基本存储单元(Cell)是一种类 NMOS 的双层浮栅(Floating Gate)MOS 管,如图 3-1 所示。

图3-1

在源极(Source)和漏极(Drain)之间电流单向传导的半导体上形成存储电子的浮栅,浮栅上下被绝缘层包围,存储在里面的电子不会因为掉电而消失,所以闪存是非易失性存储器。

写操作是在控制极加正电压,使电子通过绝缘层进入浮栅极。擦除操作正好相反,是在衬底加正电压,把电子从浮栅极中吸出来,如图 3-2 所示。

图3-2

# 3.1.2 SLC、MLC 和 TLC

  • 一个存储单元存储 1bit 数据的闪存,我们叫它为 SLC(Single Level Cell)
  • 存储 2bit 数据的闪存为 MLC(Multiple Level Cell)
  • 存储 3bit 数据的闪存为 TLC(Triple Level Cell)
  • 存储 4bit 数据的闪存为 QLC

表3-1

  • 对 SLC 来说,一个存储单元存储两种状态,浮栅极里面的电子多于某个参考值的时候,我们把它采样为 0,否则就判为1。图3-3 是闪存芯片里面存储单元的阈值电压分布函数,横轴是阈值电压,纵轴是存储单元数量。读的时候采样电压值落在1范围里面,就认为是1;落在0范围里面,就认为是0。

图3-3

擦除之后,闪存读出来的值为1,充过电之后,就是0。所以,如果需要写1,就什么都不用干;写0,就需要充电到0。

  • 对 MLC 来说,如果一个存储单元存储4个状态,那么它只能存储 2bit 的数据,如图3-4所示。通俗来说就是把浮栅极里面的电子个数进行一个划分,比如低于10个电子判为0;11~20个电子判为1;21~30个电子判为2;多于30个电子判为3。

图3-4

  • 依此类推,TLC若是一个存储单元有8个状态,那么它可以存储3bit的数据,它在MLC的基础上对浮栅极里面的电子数又进一步进行了划分,如图3-5所示。

图3-5

SLC、MLC 和 TLC 分别可以存储 1bit、2bit、3bit 的数据,所以在同样面积的 DIE 上,闪存容量依次变大。

但同时,一个存储单元电子划分得越多,那么在写入的时候,控制进入浮栅极的电子个数就要越精细,所以写耗费的时间就越长;同样的,读的时候,需要尝试用不同的参考电压去读取,一定程度上加长了读取时间。所以我们会看到在性能上,TLC 不如 MLC,MLC 不如 SLC。

如表3-2 所示是 SLC、MLC 和 TLC 在性能和寿命(Endurance)上的一个直观对比

表 3-2 SLC、MLC、TLC 参数比较

闪存类型 SLC MLC TLC
每单元比特数 1 2 3
擦写次数 约10万次 约5000次 约1000次
读时间(μs) 约25 约50 约75
些时间(μs) 约300 约600 约900
擦除时间(μs) 约1500 约3000 约4500

# 3.1.3 闪存芯片架构

图3-6 所示是一个闪存块(Block)的组织架构。

图3-6

  • 一个 Wordline 对应着一个或若干个 Page,具体是多少取决于是 SLC、MLC 或者 TLC。对 SLC 来说,一个 Wordline 对应一个 Page;MLC 则对应2个 Page,这两个 Page 是一对(Lower Page和Upper Page);TLC 对应3个 Page(Lower Page、Upper Page 和 Extra Page,不同闪存厂家叫法不一样)。
  • 一个 Page 有多大,那么 Wordline 上面就有多少个存储单元,就有多少个 Bitline。
  • 一个 Block 当中的所有这些存储单元都是共用一个衬底的。
  • 这里没有考虑奇/偶Bitline,否则一个Wordline上的Page数量在此基础上要翻倍。

一个闪存内部的存储组织结构如图3-7 所示:

图3-7

一个闪存芯片有若干个 DIE(或者叫 LUN)

  • 每个 DIE 有若干个 Plane

  • 每个 Plane 有若干个 Block

  • 每个 Block 有若干个 Page

  • 每个 Page 对应着一个 Wordline

  • Wordline 由成千上万个存储单元构成

  • DIE/LUN 是接收和执行闪存命令的基本单元。如图3-7所示,LUN0 和 LUN1 可以同时接收和执行不同的命令

  • 但在一个 LUN 当中,一次只能独立执行一个命令,你不能对其中某个 Page 写的同时,又对其他 Page 进行读访问

  • 一个 LUN 又分为若干个 Plane,市面上常见的是1个或者2个Plane,现在也有4个Plane的闪存了。

  • 每个 Plane 都有自己独立的 Cache Register 和 Page Register,其大小等于一个 Page 的大小。

  • 固态硬盘主控在写某个 Page 的时候,先把数据从主控传输到该 Page 所对应 Plane 的 Cache Register 当中,然后再把整个 Cache Register 当中的数据写到闪存阵列;读的时候则相反,先把这个 Page 的数据从闪存介质读取到 Cache Register,然后再按需传给主控。这里按需是什么意思?就是我们读取数据的时候,没有必要把整个 Page 的数据都传给主控,而是按需选择数据传输。但要记住,无论是从闪存介质读数据到 Cache Register,还是把 Cache Register 的数据写入闪存介质,都以 Page 为单位,如图3-8 Page 缓存的用法所示。

图3-8

为什么需要 Cache Register 和 Page Register 两个缓存?主要目的是优化闪存的访问速度。闪存支持 Cache 读、写操作,如图3-8所示。Cache 读支持在传输前一个 Page 数据给主控的时候(Cache Register→主控),可以从闪存介质读取下一个主控需要读的Page的数据到 Page Register(闪存介质→Page Register),这样数据在闪存总线传输的时间就可以隐藏在读闪存介质的时间里(或者相反,取决于哪个时间更长);Cache Program也是如此,它支持闪存写前一个 Page 数据的同时(Page Register→闪存介质),传输下一个要写的数据到 Cache Register(主控→Cache Register),这样数据在闪存总线传输可以隐藏在前一个 Page 的写时间里。

  • 闪存写入时间是指一个 Page 的数据从 Page Register 当中写入闪存介质的时间
  • 闪存读取时间是指一个 Page 的数据从闪存介质读取到 Page Register 的时间。

闪存一般都支持Multi-Plane(或者Dual-Plane)操作。

对写来说,主控先把数据写入第一个 Plane 的 Cache Register 当中,数据保持在那里,并不立即写入闪存介质,等主控把同一个 LUN 上的另外一个或者几个 Plane 上的数据传输到相应的 Cache Register 当中,再统一写入闪存介质。

闪存的擦除是以 Block 为单位的。为什么呢?

那是因为在组织结构上,一个 Block 当中的所有存储单元是共用一个衬底的(Substrate)。当你对某衬底施加强电压,那么上面所有浮栅极的电子都会被吸出来。

# 3.1.4 Charge Trap 型闪存

闪存不只有 Floating Gate,还有 Charge Trap。中文可以翻译成电阱,CT像个陷阱一样,把电荷困在里面存起来。

图3-20

CT 与浮栅最大的不同是存储电荷的元素不同,后者是用导体存储电荷,而前者是用高电荷捕捉(Trap)密度的绝缘材料(一般为氮化硅,SI3N4)来存储电荷。浮栅就像水,电子可在里面自由移动;而 CT 就像是奶酪,电子在里面移动是非常困难的,如图 3-21 所示。

image

  • CT 的一个优势就是:对隧道氧化层不敏感,当厚度变薄或者擦除导致老化时,CT 表示压力不大。
  • CT 相比浮栅晶体管有很多优势,如上面提到的,对隧道氧化层要求不是那么苛刻;更小的存储单元间距;隧道氧化层磨损更慢;更节能;工艺实现容易;可以在更小的尺寸上实现。
  • 但是,CT 也不能完胜。在 Read Disturb 和 DataRetention 方面,CT闪存就不如浮栅极闪存。

# 3.2 闪存特性

# 3.2.1 闪存存在的问题

# 3.2.1.1 闪存坏块

闪存块(Block)具有一定的寿命,当一个闪存块接近或者超出其最大擦写次数时,可能导致存储单元永久性损伤(见图3-43)

图3-43

闪存先天存在一些不稳定或坏的存储单元,且随着使用时间增加,坏块数量也会增加。因此,用户写入的数据必须有 ECC 纠错码保护,以纠正读取时可能发生的比特错误。如果错误比特数超出纠错能力,数据将会丢失,且这些损坏的闪存块应停止使用。由于闪存在出厂时和使用过程中都会产生坏块,因此需要有坏块管理机制。

# 3.2.1.2 读干扰

在从闪存读取数据时,未选中的闪存页的控制极会被加上正电压,以确保 MOS 管导通。然而,频繁给一个 MOS 管的控制极加上正电压可能导致轻微的写入,最终导致比特翻转,即读干扰。这种状况并非永久性损坏,重新擦除闪存块后仍可正常使用。

值得注意的是,读干扰影响的是同一闪存块中的其他闪存页,而不是正在读取的闪存页本身。

图3-44

# 3.2.1.3 写干扰

除了读干扰会导致比特翻转,写干扰(Program Disturb)也会导致比特翻转。如图3-45所示。

图3-45

由于擦除过的闪存块所有的存储单元初始值是1,只有写0的时候才真正需要操作。如图3-45所示,方框里的单元是写0,即需要写的,圆圈里的单元的代表写1,并不需要写操作。我们这里把方框里的单元称为 Programmed Cells,圆圈里的单元称为 StressedCells。

与读干扰不同的是,写干扰影响的不仅是同一个闪存块当中的其他闪存页,自身闪存页也会受到影响。相同的是,都会因不期望的轻微写导致比特翻转,都会产生非永久性损伤,经擦除后,闪存块还能再次使用。

# 3.2.2 闪存测试

原因:

  • 闪存厂商卖给你的闪存不一定都是好的,总是有概率存在故障芯片;
  • 固态硬盘制造过程中有合格率问题,不能保证每个闪存芯片都焊接得完美无缺;
  • 固态硬盘制造商为了降低成本,会从各种渠道获得低价闪存芯片,这些芯片质量没有保障,需要固态硬盘制造商自己筛选。

测试方法:

  • 固态硬盘制造商为了降低成本,会从各种渠道获得低价闪存芯片,这些芯片质量没有保障,需要固态硬盘制造商自己筛选。
  • 对每个 LUN、Plane 进行读写测试,要考虑到一定的比特翻转率,看看写入的数据和读出差距有多大。写入数据要选择不同的数据类型,比如连续的0或者连续的1。存储器件测试有很多专用数据格式。

企业级固态硬盘用的是最贵的原厂闪存,消费级固态硬盘往往是便宜的,U盘就是最差的闪存了。其所用芯片来自于各种渠道,成本很低。

# 3.2.3 MLC 使用特性

对 MLC 来说,擦除一个闪存块的时间大概是几毫秒。闪存的读写则是以闪存页为基本单元的。一个闪存页大小主要有4KB、8KB、16KB 几种。对 MLC 或者 TLC 来说,写一个闪存块当中的闪存页,应该顺序写 Page0、Page1、Page2、Page3……禁止随机写入,比如 Page2、Page3、Page5、Page0……为什么?

原因主要有二:

  • 一个存储单元包含两个闪存页数据,要先写 Lower Page,再写 UpperPage。
  • 相邻单元之间有耦合电容,工艺上要求后面的闪存页写操作时前面的闪存页已经写过。

MLC有其特有的一些问题:

  • MLC 最大擦写次数会变小。这样,就更需要 WearLeveling 技术来保证整个存储介质的使用寿命。
  • 对 MLC 来说,一个存储单元存储了 2bit 的数据,对应着两个 Page:Lower Page 和 Upper Page。假设 Lower Page 先写,然后在写 Upper Page 的过程中,由于改变了整个单元的状态,如果这个时候掉电,那么之前写入的Lower Page 数据也会丢失。也就是说,写一个闪存页失败,可能会导致另外一个闪存页的数据损坏。
  • 不能随机写。不能先写 Upper Page,然后再写 LowerPage,这点就限制了我们不能随意地写。
  • 写 Lower Page 时间短,写 Upper Page 时间长,所以会看到有些闪存页写入速度快,有些闪存页写入速度慢。

首先我们来说两条存储行业的规矩:

  • 一般在没有盘内缓存的情况下,我们认为写到硬盘的数据如果已经返回写成功,那么这个数据就是安全的。数据写到物理介质上就可以放心了。
  • 如果数据在写的过程中发生了异常掉电,那么该数据即使丢了也可以接受,毕竟用户认为数据还没写完。

但是 Lower Page 数据损坏打破了这个常识:尽管已经成功写到了盘里,但是假如该数据位于 Lower Page 上,很不幸的是,恰好过了不久,后面有数据写对应的 Upper Page 时发生了异常掉电,那就会导致 Lower Page 上已经写好的数据也被破坏。也就是说,固态硬盘正在写的时候,如果发生了异常掉电,有可能会丢失之前写入的数据。

解决办法:

  • 只写 Lower Page :成本比较高,只适合关键数据和土豪。
  • Lower Page 和 Upper Page 打包写:每次数据量多凑点,争取 LowerPage 和 Upper Page 都写(需要闪存支持 One Pass Programming)。
  • 定期填充 Upper Page:消费级固态硬盘要求省电,所以会频繁进入省电模式,可能安静个几百毫秒就自动休眠了。休眠之前检查是不是有 LowerPage 写过了,有 Upper Page 还没写的情况,就把 Upper Page 也写一下。
  • 写 Lower Page 数据时,备份该数据到别的闪存块上,直到它对应的 Upper Page 数据写完。这样即使掉电导致 Lower Page 数据丢失,也可使用备份数据进行还原。
  • MLC 闪存块当 SLC 块使用,强迫用户数据写到 SLC 块,随后以垃圾回收的方式把数据从 SLC 闪存块搬到 MLC 闪存块。

# 3.2.4 读干扰

读干扰为什么会导致性能下降?

读干扰会导致浮栅极进入电子。由于有额外的电子进入,会导致晶体管阈值电压右移(Data Retention 问题导致阈值电压左移),如图 3-50 所示。

图3-50

由于晶体管阈值电压偷偷发生了变化(变大了),闪存内部逻辑如果还是按照之前的参考电压加在控制极上然后去判断数据,肯定会发生误判,也就是读到错误的数据。阈值电压右移的速度,也就是读干扰影响数据的程度,一方面与读该闪存块上数据的次数有关,读得越多,右移越多,影响越大;一方面还跟闪存块的擦除次数有关,擦写次数越多,绝缘效果越差,电子进入浮栅极就越容易,读干扰的影响也就越大。

# 3.3 闪存数据完整性

闪存的一个特性就是随着闪存的使用以及数据存储时间的变长,存储在闪存里面的数据容易发生比特翻转,出现随机性错误。因此,使用闪存作为存储介质的固态硬盘,需要采用一些数据完整性的技术来确保用户数据可靠不丢失。

常见的技术有:

  • ECC纠错。
  • RAID数据恢复。
  • 重读(Read Retry)。
  • 扫描重写技术(Read Scrub)。
  • 数据随机化。

# 3.3.1 读错误来源

闪存数据发生错误,主要有以下几个原因:

  1. 擦写次数增多

    随着闪存块擦写次数增多,氧化层逐渐老化,电子进出存储单元越来越容易,因此存储在存储单元的电荷容易发生异常,导致数据读错误。

  2. Data Retention

    随着时间的推移,存储在存储单元的电子会流失,整个阈值电压分布向左移动,导致读数据的时候发生误判

  3. 读干扰

    读一个 Wordline 数据时,需要施加 Vpass 电压在其他 Wordline 上,导致其他闪存页发生轻微写。如果读的次数过多,轻微写累积起来就会使阈值电压分布发生右移,导致读数据时候发生误判,即读数据错误。

  4. 存储单元之间干扰

    由于存储电子的浮栅极是导体,两个导体之间构成电容,一个存储单元电荷的变化会导致其他存储单元电荷变化,而受影响最大的就是与它相邻的存储单元。

  5. 写错误

    写错误一般发生在 MLC 或者 TLC 2-pass(先写 Lower Page,然后再写 UpperPage)写过程中。写 Upper Page 的时候,它是基于之前 Lower Page 的状态,然后再写每个存储单元到目标状态。如果写 Upper Page 的时候,Lower Page 数据已经出错(注意写 Upper 的时候,Lower Page 的数据是不会经过控制器 ECC 纠错的,写过程发生在闪存内部),就会导致存储单元写到一个不期望的状态,即发生写错误。

    TLC 1-pass program 则没有这个问题,因为 Lower Page 和 Upper Page 是一次性同时写入,写 Upper Page 不依赖于 Lower Page 数据。当然,如果一开始擦除状态就不对,那么还是会发生写错误。

# 3.3.2 ECC 纠错码

常用闪存 ECC 纠错算法有 BCH(Bose、Ray-Chaudhuri 与 Hocquenghem 三位大神名字的首字母)和 LDPC(Low Density Parity Check Code)等。目前市面上很多固态硬盘控制器上采用的是 BCH,但采用 LDPC 正成为一种趋势。

用户数据最终都是写在闪存页(Page)上面,闪存页空间除了用户空间,还有额外的预留空间,这部分空间可以用来写 ECC 校验数据。用户数据大小固定,需要更强的纠错能力,这就需要更多的 ECC 空间。因此,纠错强度受限于闪存页的预留空间。越多的预留空间就能提供越强的 ECC 纠错能力。

大多数固态硬盘使用静态 ECC 纠错方案,即整个生命周期内 ECC 纠错单元和校验数据大小固定。然而,由于闪存使用初期出错率低,后期出错率增加,有些固态硬盘开始采用动态 ECC 纠错方案。

动态 ECC 纠错方案的优点是:

  1. 初期使用更少的纠错码,增加用户数据存储量,减少写放大,提升带宽利用率。
  2. 随着使用时间增加,纠错能力增强,以应对出错概率增加。
  3. 根据闪存质量差异,不同的Die或闪存页可采用不同的纠错能力。质量好的 Die 和较稳定的 Lower Page 使用较弱的 ECC 保护,质量差的 Die 和不稳定的 Upper Page 使用较强的 ECC 保护。

# 3.3.3 RAID

在一些企业级和消费级固态硬盘上,为了确保数据的完整性,采用了类似磁盘阵列的 RAID (Redundant Arrays of Independent Disks)纠错技术。,通常使用 RAID 5。

RAID 5的固态硬盘内部闪存阵列由多个 Die 构成,其中一部分存储用户数据,另一部分存储校验数据(通常采用“异或”校验)。当某个 Die 上发生 ECC 无法纠正的错误时,通过读取其他 Die 上相应位置的数据,并做“异或”运算,可以恢复出该 Die 上的数据。然而,RAID 5 只能恢复单个 ECC 无法纠正的数据,如果出现多个错误,则无法恢复。

以 图3-58 所示为例,某个固态硬盘的闪存阵列由5个 Die 构成,Die 0~3 存储的是用户数据,Die P 则存储校验数据,为 Die 0、Die 1、Die 2 和 Die 3 数据之“异或”。假设 Die 1 上出现 ECC 不可纠的错误,那么可以通过读取 Die 0、Die2、Die 3 和 Die P 对应位置上的数据,然后做“异或”,就能恢复出 Die 1 上的数据。

尽管 RAID 提供了冗余纠错技术以确保数据完整性,但它需要额外的空间来存储冗余数据(校验数据),因此会牺牲一部分用户空间。与传统磁盘阵列不同,固态硬盘内部的 RAID 结构需要进行巨大的架构改变。

图3-58

上次更新: 2024/6/3 14:54:44
第2章 SSD主控和全闪存阵列
FAT16文件系统

← 第2章 SSD主控和全闪存阵列 FAT16文件系统→

Theme by Vdoing | Copyright © 2023-2024 霜晨月
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式