This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,658 @@
<audio id="audio" title="34 | 块设备(上):如何建立代理商销售模式?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f8/a1/f8c4379ed8b62ea91cb95fa48e5839a1.mp3"></audio>
上一章我们解析了文件系统最后讲文件系统读写的流程到达底层的时候没有更深入地分析下去这是因为文件系统再往下就是硬盘设备了。上两节我们解析了字符设备的mknod、打开和读写流程。那这一节我们就来讲块设备的mknod、打开流程以及文件系统和下层的硬盘设备的读写流程。
块设备一般会被格式化为文件系统但是下面的讲述中你可能会有一点困惑。你会看到各种各样的dentry和inode。块设备涉及三种文件系统所以你看到的这些dentry和inode可能都不是一回事儿请注意分辨。
块设备需要mknod吗对于启动盘你可能觉得启动了就在那里了。可是如果我们要插进一块新的USB盘还是要有这个操作的。
mknod还是会创建在/dev路径下面这一点和字符设备一样。/dev路径下面是devtmpfs文件系统。**这是块设备遇到的第一个文件系统**。我们会为这个块设备文件分配一个特殊的inode这一点和字符设备也是一样的。只不过字符设备走S_ISCHR这个分支对应inode的file_operations是def_chr_fops而块设备走S_ISBLK这个分支对应的inode的file_operations是def_blk_fops。这里要注意inode里面的i_rdev被设置成了块设备的设备号dev_t这个我们后面会用到你先记住有这么一回事儿。
```
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode-&gt;i_mode = mode;
if (S_ISCHR(mode)) {
inode-&gt;i_fop = &amp;def_chr_fops;
inode-&gt;i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode-&gt;i_fop = &amp;def_blk_fops;
inode-&gt;i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode-&gt;i_fop = &amp;pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
}
```
特殊inode的默认file_operations是def_blk_fops就像字符设备一样有打开、读写这个块设备文件但是我们常规操作不会这样做。我们会将这个块设备文件mount到一个文件夹下面。
```
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read_iter = blkdev_read_iter,
.write_iter = blkdev_write_iter,
.mmap = generic_file_mmap,
.fsync = blkdev_fsync,
.unlocked_ioctl = block_ioctl,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = blkdev_fallocate,
};
```
不过这里我们还是简单看一下打开这个块设备的操作blkdev_open。它里面调用的是blkdev_get打开这个块设备了解到这一点就可以了。
接下来我们要调用mount将这个块设备文件挂载到一个文件夹下面。如果这个块设备原来被格式化为一种文件系统的格式例如ext4那我们调用的就是ext4相应的mount操作。**这是块设备遇到的第二个文件系统**,也是向这个块设备读写文件,需要基于的主流文件系统。咱们在文件系统那一节解析的对于文件的读写流程,都是基于这个文件系统的。
还记得咱们注册ext4文件系统的时候有下面这样的结构
```
static struct file_system_type ext4_fs_type = {
.owner = THIS_MODULE,
.name = &quot;ext4&quot;,
.mount = ext4_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
```
在将一个硬盘的块设备mount成为ext4的时候我们会调用ext4_mount-&gt;mount_bdev。
```
static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data)
{
return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
}
struct dentry *mount_bdev(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data,
int (*fill_super)(struct super_block *, void *, int))
{
struct block_device *bdev;
struct super_block *s;
fmode_t mode = FMODE_READ | FMODE_EXCL;
int error = 0;
if (!(flags &amp; MS_RDONLY))
mode |= FMODE_WRITE;
bdev = blkdev_get_by_path(dev_name, mode, fs_type);
......
s = sget(fs_type, test_bdev_super, set_bdev_super, flags | MS_NOSEC, bdev);
......
return dget(s-&gt;s_root);
......
}
```
mount_bdev主要做了两件大事情。第一blkdev_get_by_path根据/dev/xxx这个名字找到相应的设备并打开它第二sget根据打开的设备文件填充ext4文件系统的super_block从而以此为基础建立一整套咱们在文件系统那一章讲的体系。
一旦这套体系建立起来以后对于文件的读写都是通过ext4文件系统这个体系进行的创建的inode结构也是指向ext4文件系统的。文件系统那一章我们只解析了这部分由于没有到达底层也就没有关注块设备相关的操作。这一章我们重新回过头来一方面看mount的时候对于块设备都做了哪些操作另一方面看读写的时候到了底层对于块设备做了哪些操作。
这里我们先来看mount_bdev做的第一件大事情通过blkdev_get_by_path根据设备名/dev/xxx得到struct block_device *bdev。
```
/**
* blkdev_get_by_path - open a block device by name
* @path: path to the block device to open
* @mode: FMODE_* mask
* @holder: exclusive holder identifier
*
* Open the blockdevice described by the device file at @path. @mode
* and @holder are identical to blkdev_get().
*
* On success, the returned block_device has reference count of one.
*/
struct block_device *blkdev_get_by_path(const char *path, fmode_t mode,
void *holder)
{
struct block_device *bdev;
int err;
bdev = lookup_bdev(path);
......
err = blkdev_get(bdev, mode, holder);
......
return bdev;
}
```
blkdev_get_by_path干了两件事情。第一个lookup_bdev根据设备路径/dev/xxx得到block_device。第二个打开这个设备调用blkdev_get。
咱们上面分析过def_blk_fops的默认打开设备函数blkdev_open它也是调用blkdev_get的。块设备的打开往往不是直接调用设备文件的打开函数而是调用mount来打开的。
```
/**
* lookup_bdev - lookup a struct block_device by name
* @pathname: special file representing the block device
*
* Get a reference to the blockdevice at @pathname in the current
* namespace if possible and return it. Return ERR_PTR(error)
* otherwise.
*/
struct block_device *lookup_bdev(const char *pathname)
{
struct block_device *bdev;
struct inode *inode;
struct path path;
int error;
if (!pathname || !*pathname)
return ERR_PTR(-EINVAL);
error = kern_path(pathname, LOOKUP_FOLLOW, &amp;path);
if (error)
return ERR_PTR(error);
inode = d_backing_inode(path.dentry);
......
bdev = bd_acquire(inode);
......
goto out;
}
```
lookup_bdev这里的pathname是设备的文件名例如/dev/xxx。这个文件是在devtmpfs文件系统中的kern_path可以在这个文件系统里面一直找到它对应的dentry。接下来d_backing_inode会获得inode。这个inode就是那个init_special_inode生成的特殊inode。
接下来bd_acquire通过这个特殊的inode找到struct block_device。
```
static struct block_device *bd_acquire(struct inode *inode)
{
struct block_device *bdev;
......
bdev = bdget(inode-&gt;i_rdev);
if (bdev) {
spin_lock(&amp;bdev_lock);
if (!inode-&gt;i_bdev) {
/*
* We take an additional reference to bd_inode,
* and it's released in clear_inode() of inode.
* So, we can access it via -&gt;i_mapping always
* without igrab().
*/
bdgrab(bdev);
inode-&gt;i_bdev = bdev;
inode-&gt;i_mapping = bdev-&gt;bd_inode-&gt;i_mapping;
}
}
return bdev;
}
```
bd_acquire中最主要的就是调用bdget它的参数是特殊inode的i_rdev。这里面在mknod的时候放的是设备号dev_t。
```
struct block_device *bdget(dev_t dev)
{
struct block_device *bdev;
struct inode *inode;
inode = iget5_locked(blockdev_superblock, hash(dev),
bdev_test, bdev_set, &amp;dev);
bdev = &amp;BDEV_I(inode)-&gt;bdev;
if (inode-&gt;i_state &amp; I_NEW) {
bdev-&gt;bd_contains = NULL;
bdev-&gt;bd_super = NULL;
bdev-&gt;bd_inode = inode;
bdev-&gt;bd_block_size = i_blocksize(inode);
bdev-&gt;bd_part_count = 0;
bdev-&gt;bd_invalidated = 0;
inode-&gt;i_mode = S_IFBLK;
inode-&gt;i_rdev = dev;
inode-&gt;i_bdev = bdev;
inode-&gt;i_data.a_ops = &amp;def_blk_aops;
mapping_set_gfp_mask(&amp;inode-&gt;i_data, GFP_USER);
spin_lock(&amp;bdev_lock);
list_add(&amp;bdev-&gt;bd_list, &amp;all_bdevs);
spin_unlock(&amp;bdev_lock);
unlock_new_inode(inode);
}
return bdev;
}
```
**在bdget中我们遇到了第三个文件系统bdev伪文件系统**。bdget函数根据传进来的dev_t在blockdev_superblock这个文件系统里面找到inode。这里注意这个inode已经不是devtmpfs文件系统的inode了。blockdev_superblock的初始化在整个系统初始化的时候会调用bdev_cache_init进行初始化。它的定义如下
```
struct super_block *blockdev_superblock __read_mostly;
static struct file_system_type bd_type = {
.name = &quot;bdev&quot;,
.mount = bd_mount,
.kill_sb = kill_anon_super,
};
void __init bdev_cache_init(void)
{
int err;
static struct vfsmount *bd_mnt;
bdev_cachep = kmem_cache_create(&quot;bdev_cache&quot;, sizeof(struct bdev_inode), 0, (SLAB_HWCACHE_ALIGN|SLAB_RECLAIM_ACCOUNT|SLAB_MEM_SPREAD|SLAB_ACCOUNT|SLAB_PANIC), init_once);
err = register_filesystem(&amp;bd_type);
if (err)
panic(&quot;Cannot register bdev pseudo-fs&quot;);
bd_mnt = kern_mount(&amp;bd_type);
if (IS_ERR(bd_mnt))
panic(&quot;Cannot create bdev pseudo-fs&quot;);
blockdev_superblock = bd_mnt-&gt;mnt_sb; /* For writeback */
}
```
所有表示块设备的inode都保存在伪文件系统 bdev中这些对用户层不可见主要为了方便块设备的管理。Linux将块设备的block_device和bdev文件系统的块设备的inode通过struct bdev_inode进行关联。所以在bdget中BDEV_I就是通过bdev文件系统的inode获得整个struct bdev_inode结构的地址然后取成员bdev得到block_device。
```
struct bdev_inode {
struct block_device bdev;
struct inode vfs_inode;
};
```
绕了一大圈,我们终于通过设备文件/dev/xxx获得了设备的结构block_device。有点儿绕我们再捋一下。设备文件/dev/xxx在devtmpfs文件系统中找到devtmpfs文件系统中的inode里面有dev_t。我们可以通过dev_t在伪文件系统 bdev中找到对应的inode然后根据struct bdev_inode找到关联的block_device。
接下来blkdev_get_by_path开始做第二件事情在找到block_device之后要调用blkdev_get打开这个设备。blkdev_get会调用__blkdev_get。
在分析打开一个设备之前我们先来看block_device这个结构是什么样的。
```
struct block_device {
dev_t bd_dev; /* not a kdev_t - it's a search key */
int bd_openers;
struct super_block * bd_super;
......
struct block_device * bd_contains;
unsigned bd_block_size;
struct hd_struct * bd_part;
unsigned bd_part_count;
int bd_invalidated;
struct gendisk * bd_disk;
struct request_queue * bd_queue;
struct backing_dev_info *bd_bdi;
struct list_head bd_list;
......
} ;
```
你应该能发现,这个结构和其他几个结构有着千丝万缕的联系,比较复杂。这是因为块设备本身就比较复杂。
比方说,我们有一个磁盘/dev/sda我们既可以把它整个格式化成一个文件系统也可以把它分成多个分区/dev/sda1、 /dev/sda2然后把每个分区格式化成不同的文件系统。如果我们访问某个分区的设备文件/dev/sda2我们应该能知道它是哪个磁盘设备的。按说它们的驱动应该是一样的。如果我们访问整个磁盘的设备文件/dev/sda我们也应该能知道它分了几个区域所以就有了下图这个复杂的关系结构。
<img src="https://static001.geekbang.org/resource/image/85/76/85f4d83e7ebf2aadf7ffcd5fd393b176.png" alt="">
struct gendisk是用来描述整个设备的因而上面的例子中gendisk只有一个实例指向/dev/sda。它的定义如下
```
struct gendisk {
int major; /* major number of driver */
int first_minor;
int minors; /* maximum number of minors, =1 for disks that can't be partitioned. */
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
......
struct disk_part_tbl __rcu *part_tbl;
struct hd_struct part0;
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
int flags;
struct kobject *slave_dir;
......
};
```
这里major是主设备号first_minor表示第一个分区的从设备号minors表示分区的数目。
disk_name给出了磁盘块设备的名称。
struct disk_part_tbl结构里是一个struct hd_struct的数组用于表示各个分区。struct block_device_operations fops指向对于这个块设备的各种操作。struct request_queue queue是表示在这个块设备上的请求队列。
struct hd_struct是用来表示某个分区的在上面的例子中有两个hd_struct的实例分别指向/dev/sda1、 /dev/sda2。它的定义如下
```
struct hd_struct {
sector_t start_sect;
sector_t nr_sects;
......
struct device __dev;
struct kobject *holder_dir;
int policy, partno;
struct partition_meta_info *info;
......
struct disk_stats dkstats;
struct percpu_ref ref;
struct rcu_head rcu_head;
};
```
在hd_struct中比较重要的成员变量保存了如下的信息从磁盘的哪个扇区开始到哪个扇区结束。
而block_device既可以表示整个块设备也可以表示某个分区所以对于上面的例子block_device有三个实例分别指向/dev/sda1、/dev/sda2、/dev/sda。
block_device的成员变量bd_disk指向的gendisk就是整个块设备。这三个实例都指向同一个gendisk。bd_part指向的某个分区的hd_structbd_contains指向的是整个块设备的block_device。
了解了这些复杂的关系,我们再来看打开设备文件的代码,就会清晰很多。
```
static int __blkdev_get(struct block_device *bdev, fmode_t mode, int for_part)
{
struct gendisk *disk;
struct module *owner;
int ret;
int partno;
int perm = 0;
if (mode &amp; FMODE_READ)
perm |= MAY_READ;
if (mode &amp; FMODE_WRITE)
perm |= MAY_WRITE;
......
disk = get_gendisk(bdev-&gt;bd_dev, &amp;partno);
......
owner = disk-&gt;fops-&gt;owner;
......
if (!bdev-&gt;bd_openers) {
bdev-&gt;bd_disk = disk;
bdev-&gt;bd_queue = disk-&gt;queue;
bdev-&gt;bd_contains = bdev;
if (!partno) {
ret = -ENXIO;
bdev-&gt;bd_part = disk_get_part(disk, partno);
......
if (disk-&gt;fops-&gt;open) {
ret = disk-&gt;fops-&gt;open(bdev, mode);
......
}
if (!ret)
bd_set_size(bdev,(loff_t)get_capacity(disk)&lt;&lt;9);
if (bdev-&gt;bd_invalidated) {
if (!ret)
rescan_partitions(disk, bdev);
......
}
......
} else {
struct block_device *whole;
whole = bdget_disk(disk, 0);
......
ret = __blkdev_get(whole, mode, 1);
......
bdev-&gt;bd_contains = whole;
bdev-&gt;bd_part = disk_get_part(disk, partno);
......
bd_set_size(bdev, (loff_t)bdev-&gt;bd_part-&gt;nr_sects &lt;&lt; 9);
}
}
......
bdev-&gt;bd_openers++;
if (for_part)
bdev-&gt;bd_part_count++;
.....
}
```
在__blkdev_get函数中我们先调用get_gendisk根据block_device获取gendisk。具体代码如下
```
/**
* get_gendisk - get partitioning information for a given device
* @devt: device to get partitioning information for
* @partno: returned partition index
*
* This function gets the structure containing partitioning
* information for the given device @devt.
*/
struct gendisk *get_gendisk(dev_t devt, int *partno)
{
struct gendisk *disk = NULL;
if (MAJOR(devt) != BLOCK_EXT_MAJOR) {
struct kobject *kobj;
kobj = kobj_lookup(bdev_map, devt, partno);
if (kobj)
disk = dev_to_disk(kobj_to_dev(kobj));
} else {
struct hd_struct *part;
part = idr_find(&amp;ext_devt_idr, blk_mangle_minor(MINOR(devt)));
if (part &amp;&amp; get_disk(part_to_disk(part))) {
*partno = part-&gt;partno;
disk = part_to_disk(part);
}
}
return disk;
}
```
我们可以想象这里面有两种情况。第一种情况是block_device是指向整个磁盘设备的。这个时候我们只需要根据dev_t在bdev_map中将对应的gendisk拿出来就好。
bdev_map是干什么的呢前面咱们学习字符设备驱动的时候讲过任何一个字符设备初始化的时候都需要调用__register_chrdev_region注册这个字符设备。对于块设备也是类似的每一个块设备驱动初始化的时候都会调用add_disk注册一个gendisk。
这里需要说明一下gen的意思是general通用的意思也就是说所有的块设备不仅仅是硬盘disk都会用一个gendisk来表示然后通过调用链add_disk-&gt;device_add_disk-&gt;blk_register_region将dev_t和一个gendisk关联起来保存在bdev_map中。
```
static struct kobj_map *bdev_map;
static inline void add_disk(struct gendisk *disk)
{
device_add_disk(NULL, disk);
}
/**
* device_add_disk - add partitioning information to kernel list
* @parent: parent device for the disk
* @disk: per-device partitioning information
*
* This function registers the partitioning information in @disk
* with the kernel.
*/
void device_add_disk(struct device *parent, struct gendisk *disk)
{
......
blk_register_region(disk_devt(disk), disk-&gt;minors, NULL,
exact_match, exact_lock, disk);
.....
}
/*
* Register device numbers dev..(dev+range-1)
* range must be nonzero
* The hash chain is sorted on range, so that subranges can override.
*/
void blk_register_region(dev_t devt, unsigned long range, struct module *module,
struct kobject *(*probe)(dev_t, int *, void *),
int (*lock)(dev_t, void *), void *data)
{
kobj_map(bdev_map, devt, range, module, probe, lock, data);
}
```
get_gendisk要处理的第二种情况是block_device是指向某个分区的。这个时候我们要先得到hd_struct然后通过hd_struct找到对应的整个设备的gendisk并且把partno设置为分区号。
我们再回到__blkdev_get函数中得到gendisk。接下来我们可以分两种情况。
如果partno为0也就是说打开的是整个设备而不是分区那我们就调用disk_get_part获取gendisk中的分区数组然后调用block_device_operations里面的open函数打开设备。
如果partno不为0也就是说打开的是分区那我们就获取整个设备的block_device赋值给变量struct block_device *whole然后调用递归__blkdev_get打开whole代表的整个设备将bd_contains设置为变量whole。
block_device_operations就是在驱动层了。例如在drivers/scsi/sd.c里面也就是MODULE_DESCRIPTION(“SCSI disk (sd) driver”)中,就有这样的定义。
```
static const struct block_device_operations sd_fops = {
.owner = THIS_MODULE,
.open = sd_open,
.release = sd_release,
.ioctl = sd_ioctl,
.getgeo = sd_getgeo,
#ifdef CONFIG_COMPAT
.compat_ioctl = sd_compat_ioctl,
#endif
.check_events = sd_check_events,
.revalidate_disk = sd_revalidate_disk,
.unlock_native_capacity = sd_unlock_native_capacity,
.pr_ops = &amp;sd_pr_ops,
};
/**
* sd_open - open a scsi disk device
* @bdev: Block device of the scsi disk to open
* @mode: FMODE_* mask
*
* Returns 0 if successful. Returns a negated errno value in case
* of error.
**/
static int sd_open(struct block_device *bdev, fmode_t mode)
{
......
}
```
在驱动层打开了磁盘设备之后我们可以看到在这个过程中block_device相应的成员变量该填的都填上了这才完成了mount_bdev的第一件大事通过blkdev_get_by_path得到block_device。
接下来就是第二件大事情我们要通过sget将block_device塞进superblock里面。注意调用sget的时候有一个参数是一个函数set_bdev_super。这里面将block_device设置进了super_block。而sget要做的就是分配一个super_block然后调用set_bdev_super这个callback函数。这里的super_block是ext4文件系统的super_block。
sget(fs_type, test_bdev_super, set_bdev_super, flags | MS_NOSEC, bdev);
```
static int set_bdev_super(struct super_block *s, void *data)
{
s-&gt;s_bdev = data;
s-&gt;s_dev = s-&gt;s_bdev-&gt;bd_dev;
s-&gt;s_bdi = bdi_get(s-&gt;s_bdev-&gt;bd_bdi);
return 0;
}
/**
* sget - find or create a superblock
* @type: filesystem type superblock should belong to
* @test: comparison callback
* @set: setup callback
* @flags: mount flags
* @data: argument to each of them
*/
struct super_block *sget(struct file_system_type *type,
int (*test)(struct super_block *,void *),
int (*set)(struct super_block *,void *),
int flags,
void *data)
{
......
return sget_userns(type, test, set, flags, user_ns, data);
}
/**
* sget_userns - find or create a superblock
* @type: filesystem type superblock should belong to
* @test: comparison callback
* @set: setup callback
* @flags: mount flags
* @user_ns: User namespace for the super_block
* @data: argument to each of them
*/
struct super_block *sget_userns(struct file_system_type *type,
int (*test)(struct super_block *,void *),
int (*set)(struct super_block *,void *),
int flags, struct user_namespace *user_ns,
void *data)
{
struct super_block *s = NULL;
struct super_block *old;
int err;
......
if (!s) {
s = alloc_super(type, (flags &amp; ~MS_SUBMOUNT), user_ns);
......
}
err = set(s, data);
......
s-&gt;s_type = type;
strlcpy(s-&gt;s_id, type-&gt;name, sizeof(s-&gt;s_id));
list_add_tail(&amp;s-&gt;s_list, &amp;super_blocks);
hlist_add_head(&amp;s-&gt;s_instances, &amp;type-&gt;fs_supers);
spin_unlock(&amp;sb_lock);
get_filesystem(type);
register_shrinker(&amp;s-&gt;s_shrink);
return s;
}
```
好了到此为止mount中一个块设备的过程就结束了。设备打开了形成了block_device结构并且塞到了super_block中。
有了ext4文件系统的super_block之后接下来对于文件的读写过程就和文件系统那一章的过程一摸一样了。只要不涉及真正写入设备的代码super_block中的这个block_device就没啥用处。这也是为什么文件系统那一章我们丝毫感觉不到它的存在但是一旦到了底层就到了block_device起作用的时候了这个我们下一节仔细分析。
## 总结时刻
从这一节我们可以看出,块设备比字符设备复杂多了,涉及三个文件系统,工作过程我用一张图总结了一下,下面带你总结一下。
1. 所有的块设备被一个map结构管理从dev_t到gendisk的映射
1. 所有的block_device表示的设备或者分区都在bdev文件系统的inode列表中
1. mknod创建出来的块设备文件在devtemfs文件系统里面特殊inode里面有块设备号
1. mount一个块设备上的文件系统调用这个文件系统的mount接口
1. 通过按照/dev/xxx在文件系统devtmpfs文件系统上搜索到特殊inode得到块设备号
1. 根据特殊inode里面的dev_t在bdev文件系统里面找到inode
1. 根据bdev文件系统上的inode找到对应的block_device根据dev_t在map中找到gendisk将两者关联起来
1. 找到block_device后打开设备调用和block_device关联的gendisk里面的block_device_operations打开设备
1. 创建被mount的文件系统的super_block。
<img src="https://static001.geekbang.org/resource/image/62/20/6290b73283063f99d6eb728c26339620.png" alt="">
## 课堂练习
到这里你是否真的体会到了Linux里面“一切皆文件”了呢那个特殊的inode除了能够表示字符设备和块设备还能表示什么呢请你看代码分析一下。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。