存储课程学习笔记3_读写nvme磁盘(清除脏数据,struct nvme_user_io和ioctl进行读写,struct block_device内核提供接口读写)

上篇文章实现在内核模块下插入一个文件系统,实现对磁盘或者目录进行对应格式化(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 
#include 
#include 

#include  //open
#include 
#include 

#include 
#include  //关键头文件 必要结构体和ioctl参数宏定义
#include 

#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 
#include 
#include 

#include  //open
#include 
#include 

#include  //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 
#include  //struct bio 
#include   //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:  

你可能感兴趣的:(dpdk学习,nvme)