使用 Linux 对 SD 卡进行压力测试

gol*_*cks 19 linux hardware sd-card

昨天我与某人就我在这里回答的逻辑和/或真实性进行了一场小辩论,vis.,在一个像样的 (GB+) 大小的 SD 卡上记录和维护 fs 元数据永远不会足够重要到佩戴该卡在合理的时间内(年和年)。反驳的要点似乎是我一定是错的,因为网上有很多关于人们磨损 SD 卡的故事。

由于我确实有带有 SD 卡的设备,其中包含 24/7 保留的 rw 根文件系统,因此我之前已经测试过这个前提,令我自己满意。我稍微调整了这个测试,重复了它(实际上是使用同一张卡)并在这里展示它。我的两个核心问题是:

  1. 是我曾经试图破坏该卡可行的,记住它的目的是不断重现的影响,重新编写方法数据量?
  2. 我用来验证卡的方法仍然可行吗?

我把问题放在这里而不是 SO 或 SuperUser 因为对第一部分的反对可能不得不断言我的测试并没有像我确定的那样真正写入卡,并且断言这需要一些linux 的特殊知识。

[也可能是 SD 卡使用某种智能缓冲或缓存,这样对同一位置的重复写入将被缓冲/缓存在不易磨损的地方。我在任何地方都没有发现任何迹象,但我在 SU 上询问了这一点]

测试背后的想法是向卡上的同一个小块写入数百万次。这远远超出了此类设备可以承受多少写入周期的任何说法,但假设磨损均衡是有效的,如果卡的大小合适,那么数百万次此类写入仍然无关紧要,因为“同一个块”会不是字面上的相同物理块。为此,我需要确保每次写入都真正刷新到硬件,并刷新到相同的明显位置。

为了刷新到硬件,我依赖于 POSIX 库调用fdatasync()

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

// Compile std=gnu99

#define BLOCK 1 << 16

int main (void) {
    int in = open ("/dev/urandom", O_RDONLY);
    if (in < 0) {
        fprintf(stderr,"open in %s", strerror(errno));
        exit(0);
    }

    int out = open("/dev/sdb1", O_WRONLY);
    if (out < 0) {
        fprintf(stderr,"open out %s", strerror(errno));
        exit(0);
    }

    fprintf(stderr,"BEGIN\n");

    char buffer[BLOCK];
    unsigned int count = 0;
    int thousands = 0;
    for (unsigned int i = 1; i !=0; i++) {
        ssize_t r = read(in, buffer, BLOCK);
        ssize_t w = write(out, buffer, BLOCK);
        if (r != w) {
            fprintf(stderr, "r %d w %d\n", r, w);
            if (errno) {
                fprintf(stderr,"%s\n", strerror(errno));
                break;
            }
        }
        if (fdatasync(out) != 0) {
            fprintf(stderr,"Sync failed: %s\n", strerror(errno));
            break;
        }
        count++;
        if (!(count % 1000)) {
            thousands++;
            fprintf(stderr,"%d000...\n", thousands);
        }
        lseek(out, 0, SEEK_SET);
    }
    fprintf(stderr,"TOTAL %lu\n", count);
    close(in);
    close(out);

    return 0;
}                                 
Run Code Online (Sandbox Code Playgroud)

我运行了大约 8 个小时,直到我在分区的开头累积了200 万次以上的写入/dev/sdb11 我可以很容易地使用/dev/sdb(原始设备而不是分区),但我看不出这会有什么不同。

然后我通过尝试在/dev/sdb1. 这行得通,表明我整晚都在写的特定块是可行的。然而,这并不意味着卡的某些区域没有因磨损平衡而磨损和移位,而是可以访问。

为了测试,我badblocks -v -w在分区上使用。这是一个破坏性的读写测试,但是磨损均衡与否,它应该是该卡可行性的有力指示,因为它仍然必须为每次滚动写入提供空间。换句话说,这相当于完​​全填满卡片,然后检查所有这些都没有问题。好几次,因为我让坏块通过几种模式工作。

[与下面的 Jason C 的评论相反,以这种方式使用坏块没有任何错误或错误。虽然由于 SD 卡的性质,它对于实际识别坏块没有用,但使用-b-c开关进行任意大小的破坏性读写测试是很好的,这是修改后的测试进行的地方(请参阅我自己的答案)。卡控制器的任何魔法或缓存都无法欺骗测试,即可以将几兆字节的数据写入硬件并再次正确读回。Jason 的其他评论似乎基于误读——IMO 是故意的,这就是为什么我没有费心去争论。抬起头来,我把它留给读者来决定什么是有意义的,什么是没有意义的。]

1这张卡是一张旧的 4 GB Sandisk 卡(上面没有“等级”编号),我几乎没用过。再次记住,这不是 200 万次写入字面上同一个物理位置;由于磨损平衡,“第一个块”将在测试期间由控制器不断移动,以平衡磨损。

slm*_*slm 12

我认为压力测试 SD 卡通常存在以下两方面的问题:

  1. 磨损均衡 无法保证对下一个写入实际上是在 SD 上执行相同的物理位置。请记住,大多数现有的 SD 系统都在积极地采取我们所知道的块,并根据每个位置所受到的感知“磨损”移动支持它的物理位置。

  2. 不同的技术(MLC 与 SLC) 我看到的另一个问题是技术的差异。SLC 类型的 SSD 与 MLC 类型相比,我希望其使用寿命更长。此外,MLC 的容差要严格得多,您不必在 SLC 上处理这些问题,或者至少他们对以这种方式失败的容忍度要高得多。

    • MLC - 多级单元
    • SLC - 单层单元

MLC 的问题在于给定的单元可以存储多个值,这些位本质上是使用电压堆叠的,而不仅仅是物理的 +5V 或 0V,例如,因此这可能导致比 SLC 更高的潜在故障率相等的。

预期寿命

我发现这个链接讨论了硬件可以持续多长时间。它的标题是:了解您的 SSD - SLC 与 MLC

SLC

根据最佳估计,SLC SSD 在大多数情况下可以计算出平均寿命在 49 到 149 岁之间。Memoright 测试可以验证 128Gb SSD 的写入寿命超过 200 年,平均每天写入 100Gb。

MLC

这就是 mlc 设计的不足之处。目前还没有发布。没有人真正研究过使用 mlc 保证什么样的预期寿命,除了它会低得多。我收到了几种不同的信念,它们的平均寿命为 10 比 1,支持 slc 设计。保守的猜测是,大多数寿命估计将在 7 到 10 年之间,具体取决于每个制造商控制器中“磨损均衡算法”的进步。

比较

为了通过写入周期进行比较,与具有 10,000 个写入周期的生命周期的 mlc 相比,slc 将具有 100,000 个完整写入周期的生命周期。这可能会显着增加,具体取决于所使用的“磨损均衡”设计。


Jas*_*n C 6

你的测试有很多问题,有些模糊,有些没有。这也取决于你的目标。两个微妙的、模糊的问题是:

  • 您不是从您正在写入的同一区域读取,您的读取测试有效,然后,什么也不做(除非控制器已读取干扰校正,在这种情况下,它可能偶尔将正在读取的页面移动到其他地方,但这仍然不影响您的测试)。
  • 您假设(并且很可能,但不能保证)控制器检测到并报告了对坏块的读/写操作——您希望写入数据、读回数据并进行比较以进行有保证的检查。

然而,这些可以说是迂腐。更严重的是:

  • 你不能用来badblocks在闪存上显示失败的页面;所有故障检测和后续页面映射均由控制器完成,并且对操作系统透明。如果驱动器支持它,您可以从 SMART 获得一些信息(我知道没有支持它的 SD 卡,也许有更高端的拇指驱动器可以)。
  • 磨损均衡,由于您的测试不考虑先前的 TRIM 命令、测试期间驱动器的空闲/使用状态以及保留空间而变得复杂。

磨损均衡:主要问题是磨损均衡是测试中的一个主要变量。它发生在控制器上(通常),在任何情况下,它甚至对直接设备搜索 + 读/写都是透明的。在您的示例中,您实际上并不知道磨损均衡状态(特别是,最近是否向空闲块发出了 TRIM 命令?)...

对于设备上的动态磨损均衡(几乎所有消费级存储设备中都存在),那么它可以处于任何状态:在一个极端情况下,没有任何页面被标记为空闲,因此控制器必须工作的唯一页面with 是保留空间中的那些(如果有)。请注意,如果在设备上预留空间,这让你开始得到保证之前,完全失败的页面写入失败(假设有标记为空闲,没有剩余的其他网页)。在另一个极端,每个页面都标记为空闲,在这种情况下,理论上您必须让设备上的每个页面都失败,然后才能开始看到写入失败。

对于静态磨损均衡(SSD 往往有,SD 卡往往没有,拇指驱动器各不相同):除了反复写入设备上的每一页之外,真的没有其他办法。

... 换句话说,有些磨损均衡的细节你无法知道,当然也无法控制——特别是是否使用动态磨损均衡,是否使用静态磨损均衡,以及设备上为磨损均衡保留的空间量(在控制器 [或某些情况下的驱动程序,如 M-Systems 旧 DiskOnChip] 之后是不可见的)。

SLC/MLC:至于 SLC 与 MLC,这对您期望看到的限制有非常直接的影响,但两者的一般磨损平衡程序和测试程序是相同的。许多供应商没有公布他们的设备是 SLC 还是 MLC 用于更便宜的消费产品,尽管任何声称每页 100k+ 周期限制的闪存驱动器都可能是 SLC(简化的权衡是 SLC = 耐用性,MLC = 密度)。

缓存:至于缓存,它有点不确定。在OS层面,一般情况下,当然fsync/fdatasync并不能保证数据真的被写入。但是,我认为在这种情况下可以安全地假设它是(或至少控制器已承诺这样做,即写入不会被缓存吞没),因为可移动驱动器通常是为以下常见使用模式设计的“弹出”(卸载 > 同步)然后移除(断电)。虽然我们不确定,但有根据的猜测表明,可以安全地假设同步保证写入绝对发生,特别是在写入 -> 同步 -> 回读(如果不是,驱动器将不可靠弹出后)。除了“同步”之外,没有其他命令可以在弹出时发出。

在控制器上,一切皆有可能,但上面的假设还包括这样一种假设,即控制器至少没有做任何“复杂”的事情来冒同步后数据丢失的风险。可以想象,如果相同的数据正在被重写(在有限的范围内),控制器可以缓冲和分组写入,或者不写入数据。在下面的程序中,我们在两个不同的数据块之间交替,并在回读之前执行同步,专门用来破坏合理的控制器缓存机制。当然,仍然没有保证也没有办法知道,但我们可以根据这些设备的正常使用和正常/通用缓存机制做出合理的假设。

测试:

不幸的是,事实是,除非您知道设备没有预留空间并且不进行静态调平,否则无法明确测试特定页面的循环限制。但是,您可以获得的最接近的结果如下(假设没有静态磨损均衡):

您需要做的第一件事是用数据填充整个卡片。这很重要,并且是原始测试中留下的主要变量。除了任何保留空间(您无法访问)之外,这标记了尽可能多的块。请注意,我们正在使用整个设备(这将破坏其上的所有数据),因为使用单个分区只会影响设备上的一个特定区域:

dd if=/dev/urandom bs=512k of=/dev/sdb conv=fsync oflag=sync
Run Code Online (Sandbox Code Playgroud)

如果您是进度条类型:

pv -pterb -s <device_size> /dev/urandom | dd bs=512k of=/dev/sdb conv=fsync oflag=sync
Run Code Online (Sandbox Code Playgroud)

编辑:对于具有 4MB 擦除块的卡,试试这个以获得更快的写入:

dd if=/dev/urandom bs=4M of=/dev/sdb conv=fsync oflag=direct,sync iflag=fullblock
Run Code Online (Sandbox Code Playgroud)

接下来,您可以编写如下循环测试程序,利用O_DIRECTO_SYNC(可能是偏执的,冗余使用fsync())尽可能多地减少操作系统缓冲和缓存,理论上可以直接写入控制器并等到它报告操作已完成:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <cstring>

using namespace std;

static const int BLOCK_SIZE = 512;
static const int ALIGNMENT = 512;
static const int OFFSET = 1024 * ALIGNMENT; // 1024 is arbitrary


int main (int argc, char **argv) {

    if (argc != 2) {
        fprintf(stderr, "usage: %s device\n", argv[0]);
        return 1;
    }

    int d = open(argv[1], O_RDWR | O_DIRECT | O_SYNC);
    if (d == -1) {
        perror(argv[1]);
        return 1;
    }

    char *block[2], *buffer;
    int index = 0, count = -1;

    // buffers must be aligned for O_DIRECT.
    posix_memalign((void **)&(block[0]), ALIGNMENT, BLOCK_SIZE);
    posix_memalign((void **)&(block[1]), ALIGNMENT, BLOCK_SIZE);
    posix_memalign((void **)&buffer, ALIGNMENT, BLOCK_SIZE);

    // different contents in each buffer
    memset(block[0], 0x55, BLOCK_SIZE);
    memset(block[1], 0xAA, BLOCK_SIZE);

    while (true) {

        // alternate buffers
        index = 1 - index;

        if (!((++ count) % 100)) {
            printf("%i\n", count);
            fflush(stdout);
        }

        // write -> sync -> read back -> compare
        if (lseek(d, OFFSET, SEEK_SET) == (off_t)-1)
            perror("lseek(w)");
        else if (write(d, block[index], BLOCK_SIZE) != BLOCK_SIZE)
            perror("write");
        else if (fsync(d))
            perror("fsync");
        else if (lseek(d, OFFSET, SEEK_SET) == (off_t)-1)
            perror("lseek(r)");
        else if (read(d, buffer, BLOCK_SIZE) != BLOCK_SIZE)
            perror("read");
        else if (memcmp(block[index], buffer, BLOCK_SIZE))
            fprintf(stderr, "memcmp: test failed\n");
        else
            continue;

        printf("failed after %i successful cycles.\n", count);
        break;

    }

}
Run Code Online (Sandbox Code Playgroud)

请注意,对于O_DIRECT,缓冲区必须适当对齐。512 字节边界通常就足够了。你可以编译:

g++ -O0 test.cpp -o test
Run Code Online (Sandbox Code Playgroud)

-D_POSIX_C_SOURCE=200112L必要时添加。

然后,按照上述方法完全填充设备后,让它运行一夜:

./test /dev/sdb
Run Code Online (Sandbox Code Playgroud)

512 字节,对齐写入很好,这将使您擦除和重写整个页面。您可以通过使用更大的块大小来显着加快测试速度,但随后获得具体结果就变得很复杂。

我目前正在测试我昨天在人行道上发现的一个相当破旧的 4GB PNY 拇指驱动器(似乎是http://www3.pny.com/4GB-Micro-Sleek-Attach留下的东西-- -紫色-P2990C418.aspx)。

上面的程序本质上是一个有限版本,badblocks在所有保留空间都用完之前你不会看到失败。因此,期望(每次迭代写入 1 页)是上述过程平均应该在reserved_pa​​ge_count * write_cycle_limit迭代中失败(同样,磨损均衡是一个主要变量)。太糟糕了,拇指驱动器和 SD 卡通常不支持 SMART,它具有报告保留空间大小的能力。

顺便说一下,出于此测试的目的,fsyncvsfdatasync对您正在执行的块设备写入没有影响。你的open()模式很重要。

如果您对技术细节感到好奇;以下是您可能想知道(以及更多)有关 SD 卡内部工作原理的所有信息:https : //www.sdcard.org/downloads/pls/simplified_specs/part1_410.pdf

编辑:字节 vs 页面:在这些类型的测试的上下文中,重要的是要根据页面而不是字节来考虑事情。反其道而行之可能会产生很大的误导。例如,在 SanDisk 8GB SD 上,根据控制器(可通过 访问/sys/classes/mmc_host/mmc?/mmc?:????/preferred_erase_size)的页面大小是完整的 4MB。写入 16MB(与 4MB 边界对齐),然后擦除/写入 4 页。但是,以彼此 4MB 的偏移量写入四个单个字节也会擦除/写入 4 个页面。

然后说“我用 16MB 写入测试”是不准确的,因为它与“我用 4 字节写入测试”的磨损量相同。更准确地说,“我测试了 4 页写入”。


pet*_*rph 5

只是在 slm 的答案中添加一些要点 - 请注意,这些更适用于 SSD,而不是“哑” SD 卡,因为 SSD对您的数据(例如重复数据删除)使用更肮脏的技巧:

  • 您正在将 64KB 写入设备的开头 - 这本身有两个问题:

    1. 闪存单元通常具有大小为 16KB 以上的擦除块(尽管更可能在 128-512KB 范围内)。这意味着它至少需要这个大小的缓存。因此,写 64KB 对我来说似乎还不够。

    2. 对于低端(阅读“非企业”)解决方案(我希望 SD/CF 卡比 SSD 更是如此)制造商可能会选择使设备的开头部分比其他部分更耐磨损,因为重要的结构 - 设备上单个分区上的分区表和 FAT(大多数存储卡都使用此设置) - 位于那里。因此,测试卡的开头可能会有偏差。

  • fdatasync() 并不能真正保证将数据写入物理介质(尽管它可能在操作系统的控制下做得最好) - 请参阅手册页:

    调用阻塞,直到设备报告传输已完成

    如果事实证明有一个小电容器,我不会感到过分惊讶,它能够为在失去外部电源的情况下将缓存数据写入闪存提供能量。

    在任何情况下,假设卡上存在缓存(请参阅我对 SU 问题的回答),写入 64KB 并同步(与fdatasync())似乎不足以令人信服。即使没有任何“电源备份”,固件可能仍然不安全地播放它并使数据未写入的时间比预期的要长一些(因为在典型的用例中它不应该产生任何问题)。

  • 您可能希望在写入新块并进行比较之前读取数据,以确保它确实有效(如果您足够偏执,请使用已清除的缓冲区进行读取)。


gol*_*cks 3

Peterph的回答确实让我进一步考虑可能的缓存问题。在深入研究之后,我仍然不能确定是否有任何、部分或所有 SD 卡都这样做,但我确实认为这是可能的。

但是,我不认为缓存会涉及大于擦除块的数据。为了确实确定,我使用 16 MB 块而不是 64 kB 重复了测试。这是 4 GB 卡总体积的 1/250。执行此操作 10,000 次大约需要 8 个小时。如果磨损均衡尽力分散负载,这意味着每个物理块将被使用 40 次。

这并不多,但测试的初衷是通过向同一(明显)位置重复写入适量数据来证明我不会轻易损坏卡,从而证明磨损均衡的功效。IMO 之前的 64 kB 测试可能是真实的,但 16 MB 的测试一定是真实的。系统已将数据刷新到硬件,并且硬件已报告写入且没有错误。如果这是一个骗局,那么该卡就没有任何用处,并且除了主存储之外,它不能在任何地方缓存 16 MB,这正是测试的目的。

希望 10,000 次写入(每次写入 16 MB)足以证明,即使在低端名牌卡(价值:5 CDN)上,运行每天 24/7 写入适量数据的 rw 根文件系统也不会磨损该卡。一段合理的时间。 一万天就是27年……卡还好好的……

如果我得到报酬来开发比这更繁重的工作的系统,我会想做至少一些测试来确定卡可以持续多长时间。我的预感是,像这样的写入速度较低,可能需要数周、数月或数年的时间以最大速度连续写入(事实上,网上没有大量此类比较测试,这说明了事实上,这将是一个非常漫长的事情)。

关于确认该卡仍然没问题,我不再认为使用badblocks它的默认配置是合适的。相反,我这样做了:

badblocks -v -w -b 524288 -c 8
Run Code Online (Sandbox Code Playgroud)

这意味着使用 512 kB 块重复 8 次 (= 4 MB) 进行测试。由于这是一种破坏性的读写测试,如果连续循环使用,它可能会像我的自制测试一样对设备施加压力。

我还在其上创建了一个文件系统,复制到一个 2 GB 文件中,diff将该文件与原始文件进行比较,然后(因为该文件是 .iso)将其安装为映像并浏览其中的文件系统。

卡还是没问题的 毕竟,这可能是预料之中的......

;) ;)