首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Linux是如何将硬盘展示给用户的,从物理设备到通用块层

块设备也就是存储以“块”为单位数据的设备,比较典型的如硬盘设备、光盘或者优盘。本文首先集中在硬盘设备的相关内容的分析,其它设备类型很类似,暂时不做介绍。       

在Windows操作系统下硬盘设备似乎是一个实实在在的设备,我们可以通过图形界面对硬盘设备进行管理。如下图是Windows下的硬盘管理界面,可以通过这个界面形象的看到硬盘设备,并且可以对其进行格式化等操作。

Linux操作系统的硬盘设备并不直观,在LInux系统中“一切皆文件”的理念下,硬盘设备其实也是一个文件,只不过是一个比较特殊的文件。如下图是某些硬盘和分区的文件路径,其中黄色字体部分是硬盘的路径,而前面红色方框内的b表示这个文件是硬盘设备文件,而非普通文件。  

硬盘设备文件也是位于VFS(虚拟文件系统)下面,与Ext4等文件系统类似。用户层面可以用访问普通文件的接口(API)访问硬盘。如下代码是用Python实现的一个向硬盘写入字符串的程序。代码很简单,就是打开硬盘所在的路径(path),然后调用write函数写数据。可以看出,读写硬盘其实与读写文件并没有本质的差异。

import os, sysdef write_file(filename, data): fd = open(filename, "w+") fd.write(data) fd.close() write_file("/dev/sdb", "itworld123")

Linux系统中硬盘的本质

通过上面的描述我们知道对于Linux操作系统来说,硬盘就是一个文件。而硬盘本身就是一个线性存储空间(可以理解为一个大数组),这种方式与文件也是非常类似的。鉴于上述相似性,Linux将硬盘设备抽象为一个文件并没有任何不妥之处。       

实质上,在Linux操作系统硬盘设备是基于一个称为bdev的伪文件系统来管理的,bdev文件系统是一个在内存中的伪文件系统(在内存的文件系统,无持久化的数据),位置与Ext4等文件系统相同。如下图所示,bdev文件系统的位置为图中红色区域。  

理解了块设备的管理方式,再结合我们之前对文件系统的相关介绍,这样就很容易理解后续的内容了。在文件系统相关文章介绍中我们知道,不同文件系统数据处理的关键是其提供的函数集,而这个函数集是在打开文件的时候确定的。硬盘设备也是如此,当我们打开硬盘设备时,操作系统根据硬盘设备的特性,会初始化inode中的函数集。而后续对该硬盘设备的读写操作就能通过该函数集完成。如下代码所示,块设备连同字符设备和管道都作为特殊的文件进行处理,并初始化对应的函数集。

对于块设备来说,函数集为def_blk_fops,该函数集的定义如下图。所谓的函数集,其实是一个结构体,结构体中的成员都是函数指针。对于块设备来说,通过blkdev_read_iter和blkdev_write_iter实现对块设备的读写。  

完成函数集的初始化后,当用户调用VFS层的接口时,VFS层就可以找到具体的处理函数,进而完成用户的操作。这里的函数集与本地文件系统的函数集别无二致,差异在于普通文件系统需要管理目录和文件,而这里是将硬盘看作一个大文件。因此,对于/dev/sdb等硬盘的读写操作在块设备层就是对一个线性空间的操作。当然,底层驱动会进行更多处理,只不过这些对普通用户是透明的。

有的读者可能会问,如果这个硬盘格式化后会是怎样的?我们知道,在Linux我们没法直接访问硬盘。硬盘格式化后必须挂载在某一个目录下面,我们只能通过访问这个目录来访问硬盘。

挂载文件系统的命令如下所示,可以看到挂载文件系统时非常重要的一个参数就是硬盘的路径。如果我们阅读一个文件系统挂载的相关代码就会发现,当文件系统挂载时,块设备的信息会被添加到根inode中。当文件系统最后刷写数据的时候就可以找到关联的块设备进行刷写了。

关于文件系统与块设备的关系,我们在文件系统相关章节有详细的介绍,本文就不再重复介绍了。

块设备的实现原理

前文我们从比较高层面(Hight Level)介绍了块设备的原理和块设备的特性。但是关于Linux操作系统块设备的实现原理可能还一知半解。本文将进一步深入的分析Linux的块设备,期望能让大家更加深入的理解块设备的实现细节。  

其实在Linux操作系统中可以非常方便的实现一个块设备,或者说是块设备驱动。在Linux中我们熟知的RAID、多路径和Ceph的RBD等都是这样一种块设备。其特征就是在操作系统的/dev目录下面会创建一个文件。如下图显示的不同类型的块设备,包含普通的SCSI块设备和LVM逻辑卷块设备,本质上都是块设备,差异在于在不同的业务逻辑和名称。

在Linux操作系统中,块设备的实现其实十分简单,但也十分复杂。简单的是我们可以只用2个函数就可以创建一个块设备驱动程序;复杂的地方是块设备和底层设备驱动的关系错综复杂,且块设备驱动种类繁多。

我们先看一下如何创建一个块设备,创建的方法很简单,主要是调用Linux内核的2个函数就可以创建一个块设备,这两个函数分别是alloc_disk和add_disk。alloc_disk用于分配一个gendisk结构体的实例,而后者则是将该结构体实例注册到系统中。经过上述2步的操作,我们就可以在/dev目录下看到一个块设备。另外一个比较重要的地方是初始化gendisk结构体的请求队列,这样应用层有请求的时候会调用该队列的例程进行处理。

在《Linux设备驱动程序》(第三版)第16章有一个详细的介绍了一个基于内存的块设备驱动的实现细节,并且有配套源代码。所谓基于内存的块设备是指这个块设备的数据存储在内存中,而不是真正的诸如硬盘或者光盘的物理设备中。如下是本文从该书中截取的代码片段,核心是上文提到的2个函数。  

上述块设备程序可以编译成为一个内核模块。通过insmod命令将内核模块加载后,就可以在/dev目录下看到一个名为sbulla的块设备。

基于上述实现的块设备,我们可以使用本文一开始的例子实现对这个设备的读写。此时写入的数据会被写到上图代码中1处分配的内存中,而不会被持久化。从这个层面上来看,Linux的块设备确实并不复杂。

但前文我们又说到,块设备是非常复杂的,这是为什么?其复杂性在于其底层驱动类型非常多,比如基于SATA协议的硬盘、SAS硬盘、基于IP-SAN的硬盘和网络硬盘等等,种类非常之多。虽然种类非常多,但在通用块层呈现的都是一个线性空间,复杂性在通用块层之下。  

以SCSI块设备为例,虽然都是在/dev下面呈现一个名称为sdX的块设备,但底层驱动差异却非常巨大。我们知道SCSI设备可以通过多种方式连接到主机端,如SAS、iSCSI和FCP等等。而对于iSCSI协议,底层驱动可以是网卡或者是iSCSI HBA卡。

可以看出,虽然都基于SCSI的块设备,但底层的驱动差异却是非常之大,因此初始化的流程自然也有很大的差异。这还都是SCSI块设备,如果再将基于网络的块设备(nbd或者rbd)和软盘、光盘等块设备考虑进来,那就更加复杂了。

Linux中形形色色的块设备

为了让大家更加全面的了解Linux的块设备,我们这里介绍几种比较常见的块设备,比如SCSI块设备、网络块设备和分布式存储的块设备等。接下来我们一一介绍一下上述各种不同类型的块设备。

最为典型的当然是SCSI硬盘了,SCSI硬盘通过USB、SAS或者网络HBA卡连接到服务器的主板,在操作系统内部呈现为一个硬盘设备。熟悉Linux操作系统的同学大概都清楚,对于SCSI硬盘在Linux系统内部是以sd为开头的名称。在本文图2中设备其实都是SCSI硬盘。

SCSI硬盘创建的具体实现在文件sd.c(driver/scsi/sd.c)中,在该文件中的sd_probe函数中通过调用__alloc_disk_node和device_add_disk创建了SCSI硬盘块设备。由于这个函数的实现非常长, 我们这里截取了部分重要的代码,如下图所示。

上述sd_probe函数是当一个SCSI设备连接到系统时被调用,常见的例子如登录基于iSCSI的存储系统时,或者连接基于FC-SAN的存储时,或者USB硬盘插入计算机的时候。

另外一个比较典型的块设备是网络块设备(Network Block Device,简称NBD),这种块设备通过网络将一个远程的文件或者块设备映射为本地的一个块设备。需要注意的是,这里的网络块设备并非IP-SAN或者FC-SAN,而是特指NBD。前者基于SCSI协议,而后者基于私有协议实现。

网络块设备最大的特点是建立了一个从服务端到客户端的设备映射,相对于SCSI来说NBD还是比较简单的。NBD本身是一个CS(Client-Server)架构的程序,在服务端可以将一个文件/或者硬盘映射为出来(命令为:nbd-server 12345 itworld123.txt,其中12345为端口号)。这一点其实非常类似NFS对目录的映射,差异在于NBD在客户端映射为一个硬盘,而NFS在客户端映射为一个目录树。

以Ubuntu为例,我们可以非常容易的安装NBD的客户端和服务端,然后进行基本的配置就可以建立映射。安装软件的命令如下所示。

安装成功后,在服务端执行如下命令可以创建一个文件,然后通过nbd-server建立一个NBD服务了。

在客户可以执行如下命令将服务端的资源映射过来。下面第一条命令用于加载内核模块,第二条命令用于将服务端的资源映射过来。完成映射后,大家可以验证一下,比如在客户端格式化写入一些数据,然后在服务端挂载看看数据是否存在。  

如果我们深入到NBD的内核实现,可以发现块设备主要还是通过add_disk来创建的,通过下面代码可以看到,nbd设备以字符串nbd作为前缀。如果大家注意观察会发现,在执行命令modprobe nbd之后,在/dev下面就已经有多个nbd设备了。但这些设备其实都是没有映射的设备,并不可访问,只有调用nbd-client映射后才可以访问。这部分逻辑可以通过阅读调用nbd_dev_add函数的地方代码得到印证。

最后介绍一下分布式存储系统Ceph,Ceph实现了一个内核客户端(RBD),也属于块设备。Ceph块设备的核心实现在rbd.c中,该文件中函数do_rbd_add实现了对块设备的创建。在函数中do_rbd_add, 通过调用device_add_disk创建了块设备。

本文从块设备的对用户的展现形态与在内核模块中的位置方面介绍了Linux块设备相关的内容。最后,我们通过几个实例略微了解了一下块设备是如何创建的。可能很多同学还觉得意犹未尽,想了解更多Linux块设备的细节,我们在后面会结合IO路径更加深入的介绍一下块设备是如何处理IO的。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OCm_cHrgYx1oBtYk-AfQCbHw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券