/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)。