【问题标题】:Can I use /dev/sda just as an ordinary sequential file?我可以将 /dev/sda 用作普通的顺序文件吗?
【发布时间】:2019-09-06 10:30:03
【问题描述】:

我需要通过使用带有 ext4 文件系统的分区和格式化 SSD 设备来获得更好的批量写入性能。当我使用 dd 命令进行基准测试时,我得到了 20% 的改进

time dd if=/dev/zero of=/dev/sdb count=1024 bs=1048576

对比一下

time dd if=/dev/zero of=/mnt/test.img count=1024 bs=1048576 && sync

/mnt 是我挂载的 /dev/sda1。

假设硬盘专用于我的应用程序并且我可以为其设置权限,我可以简单地从我的 C++ 应用程序中打开 /dev/sda 并将其用作普通文件吗?我的意思是,从头开始写入数据,然后再次打开并读取:

  ofstream myfile;
  myfile.open ("/dev/sda");
  myfile << "Writing this to a file.\n";
  myfile.close();

然后以同样的精神重新打开和阅读。如果不清楚我的写作结束在哪里,我可以自己写数据结束标记。

我会假设是的,因为它的行为应该像一个文件。但是我想检查一下它是否没有明显的隐藏问题。

【问题讨论】:

  • 两个dd 命令指向不同的东西。一个直接写入设备,另一个在(不同的)设备上创建文件(因此必须通过更高级别的内核)。
  • 另外,您展示的 C++ 代码不会写入设备上的文件,它会写入原始磁盘,覆盖引导块的一部分。
  • 两个 dds 都写入块缓存。您需要在两者上都包含同步时间。
  • "我可以像普通的顺序文件一样使用 /dev/sda 吗?" - 一般来说,是的。
  • 好的,谢谢。我只想知道这种方法是否普遍允许。

标签: c++ linux


【解决方案1】:

/dev/sda 通常代表一个块设备。与之对比,例如/dev/tty(一个字符设备)或/dev/zero(另一个字符设备),/proc/self/fd/0(一个伪文件),或(例如)/home/inetknght/file常规文件

不同的设备有不同的特性。块设备以块为单位进行读写。块的大小取决于设备本身。不过,这可能会被模仿;例如,您可能通过管理程序添加了一个磁盘映像文件,并且管理程序模拟了它的块可访问性。许多块设备公开了 512 字节或 4K 字节的块大小。一些块设备是包装器;像管理程序,或者也像 RAID 设置。两者通常都会配置一个更适合控制器性能的单独块大小。

相比之下,普通文件通常是具有相关大小的简单数据流。在块设备上写入的文件流有很多幕后活动要在两者之间进行转换:大小为n 的数据需要多少块b这就是文件系统的作用:通常通过分配来转换数据块,但是对于文件大小而言,可能需要通过过度分配来分配许多块。有关它的其他元数据存储在文件系统数据树中,该数据树填充设备上的单独块。

您看到的性能改进很可能是删除了文件系统。文件系统通常有一些(有时是显着的)使用开销,但它们简化它们所构建的较低级别的东西,例如块设备。简单的代码更容易维护。 使用不同的文件系统会给您带来不同的性能特征。因此您可能不需要增加复杂性来降低级别。

可能能够写入块设备 就像您正在写入流设备一样。如果底层设备确实是块设备,那么当您写入不能被设备的块大小整除的字节数时会发生什么?假设块大小为 512 字节(相当典型,4K 也是如此),然后您写入 500 个字节。设备将如何处理其他 12 个字节? 这取决于设备:它可能会用零覆盖,它可能不理会,它可能实际上已将您的数据写入块大小的缓存位置,然后这 12 个字节从缓存中获取任何内容相同缓存位置中的前一个块。这只是文件系统提供的一个示例

所以:您提出了一个关于原始设备文件如何工作的问题。您还说过您拥有对机器的完全访问权限。我认为你学习的最好方法就是玩它,看看你发现了什么。

我碰巧在业余时间使用 USB 机箱中的一些驱动器设置 RAID。不完全理想,但我认为这很有趣。我将演示一些基本功能。如果我损坏了某些东西,我稍后会擦掉它。 ;)

firefly@firefly:~$ ls -lah /dev/sd*
brw-rw---- 1 root disk 8,  0 Apr 16 11:53 /dev/sda
brw-rw---- 1 root disk 8, 16 Apr 16 11:53 /dev/sdb
brw-rw---- 1 root disk 8, 32 Apr 16 11:54 /dev/sdc
brw-rw---- 1 root disk 8, 48 Apr 16 11:54 /dev/sdd

我尚未设置的 raid 中的四个设备。我会在这里选择/dev/sda

file 命令对于发现各种文件的一般信息非常方便。

firefly@firefly:~$ file /dev/sda
/dev/sda: block special (8/0)

...但它告诉我这个文件没有什么特别之处。

触摸会告诉我是否可以写入文件。

firefly@firefly:~$ touch /dev/sda
touch: cannot touch '/dev/sda': Permission denied

您已经知道您需要特殊权限才能对其进行写入。很高兴我不关心这台机器,所以我会直接进入 root 并重试。以 root 身份运行通常是不好的做法,但我在一个我根本不关心的系统上,无论如何都会在空闲时间擦除。

firefly@firefly:~$ sudo su -
root@firefly:~# touch /dev/sda
root@firefly:~# echo $?
0
root@firefly:~# ls -lah /dev/sd*
brw-rw---- 1 root disk 8,  0 Apr 18 04:45 /dev/sda
brw-rw---- 1 root disk 8, 16 Apr 16 11:53 /dev/sdb
brw-rw---- 1 root disk 8, 32 Apr 16 11:54 /dev/sdc
brw-rw---- 1 root disk 8, 48 Apr 16 11:54 /dev/sdd

更新了时间戳,当然 root 可以写入它。稍微google一下,我discover有一个command /sbin/blockdev让我读/写一些块设备ioctls。

听起来很酷。

root@firefly:~# blockdev --getiomin /dev/sda
4096
root@firefly:~# blockdev --getioopt /dev/sda
33553920
root@firefly:~# blockdev --getbsz /dev/sda
4096

不错!所以我发现我的块设备的块大小为 4K(由blockdev --getbsz 表示并由blockdev --getiomin 支持)。我不确定 --getioopt 报告的 32MiB 是否是最佳 IO 大小。这有点奇怪。我不会担心的。

好的,让我们退后一步。

dd 另一方面复制信息块。这对于块设备来说是完美的!但是您关于将块设备视为文件的问题会更适合实际上将其视为文件。所以停止使用dd

如果我从设备读取原始数据,我会得到什么?请记住,文本控制台上的原始数据会出现乱码,因此我将通过xxd 将其通过管道传输以提供十六进制转储。

root@firefly:~# head -c 100 /dev/sda | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000                                ....

所以这里有一些秘诀:head 通常会读取前 10 行 。我将其更改为读取前 100 个字节。由于驱动器刚刚格式化为零,head 本身会读取整个设备,因为它不包含单个换行符。这需要几个小时(这是一个 8TB 的旋转磁盘)。

让我们来玩一下这个超大“文件”

root@firefly:~# echo "hello world" > /dev/sda && head -c 16 /dev/sda | xxd
00000000: 6865 6c6c 6f20 776f 726c 640a 0000 0000  hello world.....

整洁。回显到设备用 hello world 覆盖了第一个零。 Echo 不完全是dd,所以听起来很有趣。

root@firefly:~# echo "goodbye" > /dev/sda && head -c 16 /dev/sda | xxd
00000000: 676f 6f64 6279 650a 726c 640a 0000 0000  goodbye.rld.....

您可以看到写“再见”只覆盖了 hello world 的一部分。没关系;我预料到了。您应该注意块设备的行为:它可能已经用零覆盖了同一块中的所有其他内容。

显然 bash 和 echo 似乎与设备文件配合得很好。我想知道其他语言?您的问题被标记为 [C++],所以让我们尝试一下:

root@firefly:~# g++ -x c++ -std=c++17 - <<EOF
> #include <cerrno>
> #include <cstdlib>
> #include <cstring>
> #include <fstream>
> #include <iostream>
> 
> int main(){
>     std::fstream f{"/dev/sda", std::ios_base::binary};
>     if ( false == f.good() ){
>         // C++ standard library does not let you inspect _why_ a failure occurred
>         // to get that we would have to use ::open() and check errno.
>         auto err = errno;
>         std::cerr << "unable to open /dev/sda: " << err << ": " << strerror(err) << std::endl;
>         std::cerr << f.good() << f.bad() << f.eof() << f.fail() << std::endl;
>         return EXIT_FAILURE;
>     }
>     std::cout << "opened!" << std::endl;
>     return EXIT_SUCCESS;
> }
> EOF
root@firefly:~# ./a.out 
unable to open /dev/sda: 0: Success
0001

这里有一些信息。首先:编译应用程序,使用bash heredoc 提供源代码。这对于 Linux 用户和开发人员来说是一件好事。如果你不熟悉它,那么你可以取消引用 EOF 之间的所有内容,保存到一个文件,然后编译它。

但是,重要的是使用std::fstream 打开文件失败。现在哇! 我们看到echo 工作得很好!为什么会有差异?!我怀疑这可以追溯到我所说的block devices不同。但是我不知道答案。我怀疑获得errno 会告诉我更多信息。让我们试试吧:

root@firefly:~# g++ -x c++ -std=c++17 - <<EOF
> #include <cerrno>
> #include <cstdio>
> #include <cstdlib>
> #include <cstring>
> #include <fstream>
> #include <functional>
> #include <iostream>
> #include <memory>
> 
> using FILEPTR = std::unique_ptr<std::FILE, decltype(&::std::fclose)>;
> 
> int main(){
>     FILEPTR f{nullptr, &::std::fclose};
>     // Remember, C-style has no concept of text mode vs binary mode.
>     f.reset(std::fopen("/dev/sda", "w+"));
>     if ( nullptr == f ){
>         auto err = errno;
>         std::cerr << "unable to open /dev/sda: " << err << ": " << strerror(err) << std::endl;
>         return EXIT_FAILURE;
>     }
>     std::cout << "opened!" << std::endl;
>     return EXIT_SUCCESS;
> }
> EOF
root@firefly:~# ./a.out 
opened!

哇,等一下:成功了。所以std::fstream无法打开块设备,而std::fopen()可以?!老实说,这对我来说没有多大意义。希望其他人可以在这里提供帮助。但我想这应该为您指明正确的方向。我会给你一个快速读/写的例子:

root@firefly:~# g++ -x c++ -std=c++17 - <<EOF
> extern "C" {
> #include <unistd.h>
> } // extern "C"
> 
> #include <algorithm>
> #include <array>
> #include <cerrno>
> #include <cstdio>
> #include <cstdlib>
> #include <cstring>
> #include <fstream>
> #include <functional>
> #include <iostream>
> #include <memory>
> #include <string_view>
> 
> using FILEPTR = std::unique_ptr<std::FILE, decltype(&::std::fclose)>;
> 
> int main(){
>     FILEPTR f{nullptr, &::std::fclose};
>     // Remember, C-style has no concept of text mode vs binary mode.
>     f.reset(std::fopen("/dev/sda", "w+"));
>     if ( nullptr == f ){
>         auto err = errno;
>         std::cerr << "unable to open /dev/sda: " << err << ": " << strerror(err) << std::endl;
>         return EXIT_FAILURE;
>     }
>     std::cout << "opened!" << std::endl;
> 
>     std::cout << "ftell(): " << std::ftell(f.get()) << '\n';
>     if ( 0 != std::fseek(f.get(), 0, SEEK_END) ) {
>         auto err = errno;
>         std::cerr << "unable to fseek(): " << err << ": " << std::strerror(err) << std::endl;
>         return EXIT_FAILURE;
>     }
>     std::cout << "ftell(SEEK_END): " << std::ftell(f.get()) << '\n';
>     std::rewind(f.get());
> 
>     // I thought about putting it on the stack, but it might exceed stack
>     // size on some platforms.
>     using buffer_type = std::array<char, 4096>;
>     using bufferptr = std::unique_ptr<buffer_type>;
>     bufferptr buffer = std::make_unique<buffer_type>();
>     if (gethostname(buffer->data(), buffer->size()) < 0) {
>         // using string_view to ensure the null byte gets written
>         auto s = std::string_view{"unable to get hostname\0"};
>         std::fwrite(s.data(), 1u, s.size(), f.get());
>     } else {
>         // ugh. boost::asio makes this simpler but I'll leave it to you to figure out.
>         if ( buffer->end() == std::find(buffer->begin(), buffer->end(), '\0') ){
>             std::cout << "buffer truncated" << std::endl;
>             buffer->back() = '\0';
>         }
>         std::fwrite(buffer->data(), 1u, buffer->size(), f.get());
>     }
>     if ( 0 != std::fflush(f.get()) ) {
>         int err = errno;
>         std::cerr << "fflush() failed: " << err << ": " << std::strerror(err) << std::endl;
>         return EXIT_FAILURE;
>     }
>     std::rewind(f.get());
> 
>     // reset our local internal buffer
>     std::fill(buffer->begin(), buffer->end(), '\0');
> 
>     // read into it
>     std::fread(buffer->data(), 1u, buffer->size(), f.get());
> 
>     // find where the disk's zeroes start. if we truncated, then it should start
>     // literally on the last byte in teh buffer, since we set that manually.
>     std::string_view read_message{buffer->data(), (std::size_t)std::distance(buffer->begin(), std::find(buffer->begin(), buffer->end(), '\0'))};
>     std::cout << read_message << std::endl;
> 
>     return EXIT_SUCCESS;
> }
> EOF
root@firefly:~# ./a.out 
opened!
ftell(): 0
ftell(SEEK_END): 8001563222016
firefly

完美。因此它能够发现该驱动器宣传 8TB 但更接近 7.2TiB(这就是营销部门喜欢Terabyte and Tebibyte 之间的区别)。我能够使用 C++ 成功写入和回读系统主机名。我已经(简要地)介绍了一些信息,供您了解有关性能调整块设备的信息。我很好奇你从std::FILE* 中得到了什么样的表现,或者你是否发现了一些不同的东西。

您将进入一个足够低的级别,可能会更难找到简单问题的答案。直接使用块设备时有哪些其他限制?我非常确定(虽然不是 100%)C++ 标准库正在处理我的读/写与磁盘的块大小不对齐(通过std::FILE*)。这很酷。但这让我想知道:我怎样才能关闭它以尝试获得更高的性能?我的第一个猜测是使用带有本机文件描述符的::open()::read()::write() 等。这会扔掉很多已经经过充分测试的语法糖。我不确定我想在这里重新发明轮子。事实上,::open() 的手册页特别提到了一些与处理块设备相关的信息,例如缓冲(这也可能是处理块对齐问题的原因,但我不确定)。

所以 tl;dr 是 这很复杂。是的,你可以读/写它(给定足够的权限)。不,如果您希望它像普通文件一样工作,并非一切都“正确”。具体来说,似乎std::fstream 可能不适用于块设备,但std::FILE* 可以。具体来说,您将需要手动处理数据框架。如果你使用 C 级别的 IO 函数,它无疑会起作用,但会有更多的限制或性能复杂性。整个回复假设您使用的是 Linux;不同的操作系统可能有不同的行为。当然,不同的块设备也可能有不同的行为(我使用的是 spin rust,但你提到了使用 SSD)。

【讨论】:

    猜你喜欢
    • 2020-08-25
    • 2013-08-30
    • 2012-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-27
    • 2020-04-08
    • 1970-01-01
    相关资源
    最近更新 更多