上篇文章实现在内核模块下插入一个文件系统,实现对磁盘或者目录进行对应格式化(mount)绑定文件系统后,已经可以正常使用。
接下来了解对nvme磁盘的控制。
0:总结
1:初始化nvme磁盘,清空脏数据 dd指令
2:struct nvme_user_io结构体+ioctl实现直接控制nvme磁盘。 了解block块
3:借助内核接口插入内核模块实现对nvme磁盘的访问。 (struct block_device对象以及struct bio实现提交请求)
4:已经实现基本的对nvme磁盘的基本读和写访问,已经能访问磁盘了,基于当前已有功能,思考如何有效管理整个磁盘,如何实现文件系统的功能。
1:准备环境,了解nvme磁盘
1.1 虚拟机上新增一个nvme磁盘,格式化和对磁盘清零。
在虚拟机上新增一块nvme磁盘,查看磁盘并进行格式化,以及对磁盘进行清理,为后续测试做准备。
ubuntu@ubuntu:~$ ls /dev/nvme*
/dev/nvme0 /dev/nvme0n1
root@ubuntu:/# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 63.4M 1 loop /snap/core20/1974
loop1 7:1 0 63.5M 1 loop /snap/core20/2015
loop2 7:2 0 73.9M 1 loop /snap/core22/864
loop3 7:3 0 128.9M 1 loop /snap/docker/2893
loop4 7:4 0 111.9M 1 loop /snap/lxd/24322
loop5 7:5 0 40.8M 1 loop /snap/snapd/19993
loop6 7:6 0 40.8M 1 loop /snap/snapd/20092
sda 8:0 0 20G 0 disk
├─sda1 8:1 0 1M 0 part
├─sda2 8:2 0 1.8G 0 part /boot
└─sda3 8:3 0 18.2G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 10G 0 lvm /
sr0 11:0 1 1.8G 0 rom
nvme0n1 259:0 0 20G 0 disk
#格式化该磁盘 格式化为ext4文件系统 就可以支持文件系统相关指令 可以用cat等指令直接查看磁盘内容(发现很多乱码)
root@ubuntu:/# mkfs.ext4 /dev/nvme0n1
mke2fs 1.46.5 (30-Dec-2021)
Creating filesystem with 5242880 4k blocks and 1310720 inodes
Filesystem UUID: c4732f78-4c95-43d3-84d4-e9f36c3aec4e
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000
Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done
#已经格式化磁盘空间,可以使用dd指令对磁盘空间进行清理
#1兆字节等于1024 * 1024字节。 1G字节等于1024 * 1024 * 1024字节。 5M*4096 = 20M*1024次=20G
#dd if=/dev/zero of=/dev/nvme0n1 bs=5M count=4096
root@ubuntu:/# dd if=/dev/zero of=/dev/nvme0n1 bs=5M count=4096
4096+0 records in
4096+0 records out
21474836480 bytes (21 GB, 20 GiB) copied, 44.0386 s, 488 MB/s
#磁盘内已经无数据
1.2 nvme相关指令其实可以查看nvme设备信息
可以看到,这里的块大小是512,每次写入以512为一个块?
使用nvme相关指令可以查看相关nvme设备具体信息 这里只简单查看, (逻辑块和页的关系)
Node: 设备节点路径,即设备文件路径。
SN: 设备序列号,用于唯一标识该设备。
Model: 设备型号或制造商信息。
Namespace: 命名空间数量,表示该设备上可用的逻辑存储区域数量。
Usage: 显示当前使用容量和总容量。以"GB"为单位显示,前者表示已使用容量,后者表示总容量。
Format: 数据格式(Logical Block Format),包括块大小和元数据大小等信息。
FW Rev: 固件版本号。
请注意,实际输出结果可能
root@ubuntu:/home/ubuntu/storage/zvvfs-main# nvme list
Node SN Model Namespace Usage Format FW Rev
--------------------- -------------------- ---------------------------------------- --------- -------------------------- ---------------- --------
/dev/nvme0n1 VMware NVME_0000 VMware Virtual NVMe Disk 1 21.47 GB / 21.47 GB 512 B + 0 B 1.3
块大小是512?
2:借助struct nvme_user_io结构体+ioctl实现直接控制裸盘
借助struct nvme_user_io对读写指令,写入位置等初始化后,使用ioctl在对应块位置写入对应内容。
2.1:借助struct nvme_user_io操作nvme磁盘
2.1.1 构造写指令,在对应块位置进行写入代码。
这里结构体struct nvme_user_io很重要,需要理解各个参数,块大小,结合打印调试进行分析。
//借助nvme_ioctl.h 操作系统提供的接口与nvme设备进行交互
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> //open
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/nvme_ioctl.h> //关键头文件 必要结构体和ioctl参数宏定义
#include <sys/ioctl.h>
#define NVME_NAME "/dev/nvme0n1"
int main()
{
//虚拟机已经新增nvme设备 /dev/nvme0n1
//以打开文件的方式 直接打开
int fd = open(NVME_NAME, O_RDWR);
if(fd < 0)
{
perror("open nvme0n1 error.\n");
return -1;
}
//这里如果定义大小200
//root@ubuntu:/home/ubuntu/storage/nvme# ./nvme_ioctl_test
// *** stack smashing detected ***: terminated
// Aborted
char data[4096] = "0,hello nvme0n1.";
//借助关键结构体 进行初始化 也可以使用struct nvme_passthru_cmd
struct nvme_user_io io;
memset(&io, 0, sizeof(struct nvme_user_io));
/*
struct nvme_user_io {
__u8 opcode;
__u8 flags;
__u16 control;
__u16 nblocks;
__u16 rsvd;
__u64 metadata;
__u64 addr;
__u64 slba;
__u32 dsmgmt;
__u32 reftag;
__u16 apptag;
__u16 appmask;
};
*/
io.opcode = 0x01; // nvme_cmd_write ; //0x01
io.addr = (__u64)&data;
//注意这里起始逻辑块地址 以及逻辑块大小和块数量与读写字符串长度
io.slba = 0; // 起始逻辑块地址 逻辑块地址之间差距512 可以通过控制该位置,确定逻辑块每次大小 比如下次写入slba为8 中间间隔512*8 =4k
io.nblocks = 1; // 要写入的逻辑块数量 (n+1)*256
//借助ioctl 使用NVME_IOCTL_SUBMIT_IO 提交对nvme磁盘的io操作
if (-1 == ioctl(fd, NVME_IOCTL_SUBMIT_IO, &io)) {
perror("Failed to submit io request write.\n");
close(fd);
return 1;
}
//上面已经写入磁盘 直接从磁盘中读看看
char res[4096] = {0};
io.opcode = 0x02;// nvme_cmd_read ; //0x02
io.addr = (__u64)&res;
io.slba = 0; // 起始逻辑块地址
io.nblocks = 1; // 要读的逻辑块数量
if (-1 == ioctl(fd, NVME_IOCTL_SUBMIT_IO, &io)) {
perror("Failed to submit io request read.\n");
close(fd);
return 1;
}
printf("read buffer form nvme0n1 success[%s].\n", res);
return 0;
}
2.1.2 运行结果分析(思考struct nvme_user_io 其中slba参数的影响)
#借助mkfs.ext4 和dd指令对磁盘进行了格式化并清理动作后 使用代码进行控制
ubuntu@ubuntu:~/storage/nvme$ gcc nvme_ioctl_test.c -o nvme_ioctl_test
root@ubuntu:/home/ubuntu/storage/nvme# ./nvme_ioctl_test
read buffer form nvme0n1 success[0,hello nvme0n1.].
#修改代码 io.slba 逻辑块地址换从0换成1 可以看到磁盘写了多次
root@ubuntu:/home/ubuntu/storage/nvme# cat /dev/nvme0n1
0,hello nvme0n1.0,hello nvme0n1.
2.1.3 nvme-cli相关指令可以对nvme设备进行必要操作。
使用apt install nvme-cli安装可以使用nvme相关指令
#上面的代码中 slba =0 从逻辑地址0开始 写入512个字段
root@ubuntu:/home/ubuntu/storage/nvme# sudo nvme read /dev/nvme0n1 -s 0 -c 1 -z 512
Rounding data size to fit block count (1024 bytes)
0,hello nvme0n1.read: Success
#修改了成为slba =1 从逻辑地址1开始写入512 nblocks是写入的块大小,会覆盖后面
root@ubuntu:/home/ubuntu/storage/nvme# sudo nvme read /dev/nvme0n1 -s 1 -c 1 -z 512
Rounding data size to fit block count (1024 bytes)
0,hello nvme0n1.read: Success
root@ubuntu:/home/ubuntu/storage/nvme# sudo nvme read /dev/nvme0n1 -s 0 -c 2 -z 4096
0,hello nvme0n1.0,hello nvme0n1.read: Success
#可以查看磁盘的一些信息
root@ubuntu:/home/ubuntu/storage/nvme# nvme smart-log /dev/nvme0n1
Smart Log for NVME device:nvme0n1 namespace-id:ffffffff
critical_warning : 0
temperature : 11759 C (12032 Kelvin)
available_spare : 1%
available_spare_threshold : 100%
percentage_used : 0%
endurance group critical warning summary: 0
data_units_read : 110,680,464,442,257,309,696
data_units_written : 92,233,720,368,547,758,080
host_read_commands : 110,680,464,442,257,309,696,000
host_write_commands : 92,233,720,368,547,758,080,000
controller_busy_time : 92,233,720,368,547,758,080
power_cycles : 184,467,440,737,095,516,160
power_on_hours : 1,106,804,644,422,573,096,960
unsafe_shutdowns : 0
media_errors : 0
num_err_log_entries : 0
Warning Temperature Time : 0
Critical Composite Temperature Time : 0
Thermal Management T1 Trans Count : 0
Thermal Management T2 Trans Count : 0
Thermal Management T1 Total Time : 0
Thermal Management T2 Total Time : 0
2.2:直接读磁盘,分析struct nvme_user_io 其中slba参数的影响
基于上面向磁盘写内容的demo,直接从磁盘中读内容,思考slba参数与块大小(块大小应该是512)
2.2.1 直接读裸盘demo代码
//借助操作系统已经可以向nvme设备中写数据和读数据 这里需要注意逻辑块位置和大小
//mkfs.ext4 /dev/nvme0n1 后借助read函数直接从nvme0n1中读取练习,也可以用write写入
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> //open
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> //read close
#define NVME_NAME "/dev/nvme0n1"
int main()
{
//打开对应的设备
int fd =open(NVME_NAME, O_RDWR);
if(fd < 0) {
perror("open file error."); //stdio.h
return -1;
}
char * buffer = NULL;
buffer = malloc(8192);
if(buffer == NULL)
{
return -2;
}
memset(buffer, 0 , 8192);
//这里同样可以用write向设备中写 但是只能从头写吧或者用偏移
//一次性读取较大字节内容 分析逻辑块大小
//写入的时候 实际上按逻辑块写入的,这里从头读8192字节
int ret = read(fd, buffer, 8192);
if(ret <0)
{
perror("read buffer error.\n");
return -2;
}
printf("read size: %d \n", ret);
for(int i=0; i<8192; i+=256)
{
printf("[%d] ---> read: %s \n", i/256, buffer+i);
}
close(fd);
free(buffer);
}
2.2.2 运行结果,分析块大小实际是512
ubuntu@ubuntu:~/storage/nvme$ gcc nvme_ioctl_test.c -o nvme_ioctl_test
ubuntu@ubuntu:~/storage/nvme$ gcc nvme_read_test.c -o nvme_read_test
root@ubuntu:/home/ubuntu/storage/nvme# ./nvme_ioctl_test #向第0快逻辑地址写 改代码 写时设置 io.slba =0
read buffer form nvme0n1 success[0,hello nvme0n1.].
root@ubuntu:/home/ubuntu/storage/nvme# cat /dev/nvme0n1
0,hello nvme0n1.
root@ubuntu:/home/ubuntu/storage/nvme# ./nvme_ioctl_test #向第1快逻辑地址写 改代码 io.slba =1
read buffer form nvme0n1 success[0,hello nvme0n1.].
root@ubuntu:/home/ubuntu/storage/nvme# cat /dev/nvme0n1
0,hello nvme0n1.1,hello nvme0n1.^C
#从打印内容可以看出 0逻辑块和1逻辑块之间差256*2 = 512 字节
root@ubuntu:/home/ubuntu/storage/nvme# ./nvme_read_test
read size: 8192
[0] ---> read: 0,hello nvme0n1.
[1] ---> read:
[2] ---> read: 1,hello nvme0n1.
[3] ---> read:
...
3:借助内核提供的接口,插入内核模块实现对nvme磁盘的控制。
要用到linux内核中提供的接口。
主要借助结构体struct block_device和struct bio,实现对磁盘的读和写。
struct block_device ===》内核中标识块设备的结构体
struct bio ===》内核中表示块I/O请求的数据结构之一
3.1 插入内核模块,构造struct block_device对象,使用struct bio做io操作,向块设备中提交io请求。
struct block_device ===》内核中标识块设备的结构体
struct bio ===》内核中表示块I/O请求的数据,借助bio_add_page和submit_bio_wait函数实现(向块设备中提交io请求i并等待反馈)。
3.3.1 demo代码
插入内核模块,向nvme设备中写入内容并读取内容。
//使用内核模块 通过struct bio(块设备的基本容器)读写设备文件
#include <linux/module.h>
#include <linux/bio.h> //struct bio
#include <linux/blkdev.h> //block_device
#define DISK_NAME "/dev/nvme0n1"
struct block_device *nvme_blkdev = NULL;
int nvme_write_data(const char *buffer, int length, sector_t sector, unsigned int sectors)
{
struct page *page;
struct bio *bio;
bio = bio_alloc(GFP_KERNEL, sectors);//1
if (!bio) {
printk(KERN_ERR "Failed to allocate BIO.\n");
return -1;
}
bio->bi_opf = REQ_OP_WRITE;
bio->bi_iter.bi_sector = sector; //起始扇区号
bio->bi_bdev = nvme_blkdev; //做关联
bio_set_dev(bio, nvme_blkdev);
page = alloc_page(GFP_KERNEL);
if (!page) {
printk(KERN_ERR "alloc_page failed\n");
return -1;
}
memcpy(page_address(page), buffer, length);
printk(KERN_ERR "alloc_page buffer :%s PAGE_SIZE:%lu\n", (char*)page_address(page), PAGE_SIZE);
bio_add_page(bio, page, PAGE_SIZE, 0);
// submit_bio_write(bio);
submit_bio_wait(bio);
__free_page(page);
bio_put(bio);
return 0;
}
int nvme_read_data(char *buffer, int length, sector_t sector, unsigned int sectors)
{
struct page *page;
struct bio *bio;
bio = bio_alloc(GFP_KERNEL, sectors);//1
if (!bio) {
printk(KERN_ERR "Failed to allocate BIO.\n");
return -1;
}
bio->bi_iter.bi_sector = sector; //起始扇区号
bio->bi_bdev = nvme_blkdev; //做关联
bio_set_dev(bio, nvme_blkdev);
page = alloc_page(GFP_KERNEL);
if (!page) {
printk(KERN_ERR "alloc_page failed\n");
return -1;
}
bio_add_page(bio, page, PAGE_SIZE, 0);
submit_bio_wait(bio); //
// submit_bio_read(bio);
memcpy(buffer, page_address(page), length);
__free_page(page);
bio_put(bio);
return 0;
}
// insmod zvvfs_nvme.ko
int nvme_kernel_init(void) {
char *str = "4ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char buffer[512] = {0};
nvme_blkdev = blkdev_get_by_path(DISK_NAME, FMODE_READ | FMODE_WRITE, NULL);
if (IS_ERR(nvme_blkdev)) {
printk(KERN_ERR "blkdev_get_by_path failed\n");
return PTR_ERR(nvme_blkdev);
}
nvme_write_data(str, strlen(str), 4, 1);
nvme_read_data(buffer, 512, 4, 1);
printk(KERN_INFO "buffer: %s\n", buffer);
return 0;
}
// rmmod zvvfs_nvme.ko
void nvme_kernel_exit(void) {
if (nvme_blkdev) {
blkdev_put(nvme_blkdev, FMODE_READ | FMODE_WRITE);
nvme_blkdev = NULL;
}
printk(KERN_INFO "zvvfs_nvme_exit\n");
}
module_init(nvme_kernel_init);
module_exit(nvme_kernel_exit);
MODULE_LICENSE("GPL");
3.3.2 运行结果
#这里启始是第四个扇区
root@ubuntu:/home/ubuntu/storage/nvme_test/nvme_kernel# insmod nvme_kernel.ko
#?这里明明写入的是不同的扇区块 为什么cat打印时确是连接的?
root@ubuntu:/home/ubuntu/storage/nvme_test/nvme_kernel# cat /dev/nvme0n1
0,hello nvme0n1.1,hello nvme0n1.4ABCDEFGHIJKLMNOPQRSTUVWXYZ^C
root@ubuntu:/home/ubuntu/storage/nvme_test/nvme_kernel# rmmod nvme_kernel.ko
root@ubuntu:/home/ubuntu/storage/nvme_test/nvme_kernel#dmesg
[112485.856459] alloc_page buffer :4ABCDEFGHIJKLMNOPQRSTUVWXYZ PAGE_SIZE:4096
[112485.857883] buffer: 4ABCDEFGHIJKLMNOPQRSTUVWXYZ
[112491.774997] zvvfs_nvme_exit
root@ubuntu:/home/ubuntu/storage/nvme# ./nvme_read_test #以256做间隔打印 512一个扇区
read size: 8192
[0] ---> read: 0,hello nvme0n1.
[1] ---> read:
[2] ---> read: 1,hello nvme0n1.
[3] ---> read:
[4] ---> read:
[5] ---> read:
[6] ---> read:
[7] ---> read:
[8] ---> read: 4ABCDEFGHIJKLMNOPQRSTUVWXYZ #这里为上面写入的内容
[9] ---> read: