我可以将/ dev / sda用作普通的顺序文件吗?

时间:2019-04-16 10:35:36

标签: c++ linux

我需要获得更好的批量写入性能,这是通过将分区和格式化的SSD设备与ext4文件系统一起使用来实现的。当我使用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();

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

我认为是,因为它的行为类似于文件。但是,我想检查一下是否没有重大的隐藏问题。

1 个答案:

答案 0 :(得分:0)

/dev/sda通常代表阻止设备。与/dev/tty(一个字符设备)或/dev/zero(另一个字符设备),/proc/self/fd/0(一个< strong>伪文件),或(例如)/home/inetknght/file常规文件

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

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

您看到的性能改进可能是删除文件系统。文件系统经常有一些使用开销(有时是很大的开销),但是它们会简化所构建的较低级别的内容,例如块设备。简单的代码更容易维护。 使用不同的文件系统将为您提供不同的性能特征。因此,从较低的级别开始,您可能不需要增加的复杂性。

可能可以写块设备 ,就像您正在写流设备一样。如果底层设备确实是一个块设备,那么当您写一些不能被设备的块大小整除的字节时会发生什么?假设块大小为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

我尚未进行设置突袭的这四个设备。我在这里选择/dev/sda

file命令非常适合发现有关各种文件的常规信息。

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

...但是它告诉我有关此文件的特殊信息。

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

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可以写入。一点点谷歌搜索,我discover有一个command /sbin/blockdev让我读/写一些块设备ioctl

听起来很酷。

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仅在32emB以下 报告是否为最佳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覆盖了最初的零。回声不太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 wo rld 的一部分。没关系;我预料到了。您应该意识到块设备的行为:它可能用零覆盖了同一块中的其他所有内容。

很明显,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工作正常!为什么会有差异?!我怀疑这可以归结为我所说的阻止设备与众不同。但是我不知道答案。我怀疑获取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。不同的操作系统可能具有不同的行为。当然,不同的块设备也可能具有不同的行为(我使用的是旋转锈蚀,但您提到使用的是SSD)。