块设备文件读取实验记录


文件: 一个linux文件就是一个字节序列,所有的IO设备(例如网络,磁盘和终端)都被模型化为文件,而所有的的输入和输出都被当做相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许linux内核引出一个简单,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统计且一致的方式来执行。(选自csapp)

文件描述符: 文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符。文件描述符屏蔽了文件(普通文件与目录文件)、管道和设备之间的差异,使它们看起来都像字节流。
Linux文件类型常见的有:普通文件、目录、字符设备文件、块设备文件、符号链接文件,当一个进程打开一个设备文件时,内核将读写系统调用转移到内核设备实现,而不是传递给文件系统。

在这里插入图片描述
一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。除了文件描述符表,系统还需要维护另外两张表:

  • 打开文件表(Open file table)
  • i-node 表(i-node table)

文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个。
文件描述符只不过是一个数组下标!通过文件描述符,可以找到文件指针,从而进入打开文件表。
参见Linux 文件描述符到底是什么?

以下研究对设备文件(“/dev/nvme0n2(设备)和/dev/nvme0n2p1(分区)”)的读写操作

添加实验设备

在虚拟机中添加10G的NVMe盘

lsblk  # 硬盘文件为/dev/nvme0n2 
fdisk /dev/nvme0n2  # 创建分区 m获取帮助 n添加分区 而后全部按默认来 w保存退出
lsblk # 验证分区结果
sudo mkfs.ext4 /dev/nvme0n2p1 # 分区格式化
sudo gedit /etc/fstab  # 记录挂载信息
# 添加一行
/dev/nvme0n2p1 /home/lt1020/Desktop/NVMeTest ext4 defaults 0 0
# 其中/dev/nvme0n2p1为分区,/home/lt1020/Desktop/NVMeTest为挂载目录
mount -a
sudo chown -R lt1020:lt1020 /home/lt1020/Desktop/NVMeTest # 更改目录拥有者,使其可以随意访问(可选)

简单测试只是预测试,偏移量测试得到了想要的结果。

简单分区测试

stat测试

#include <sys/types.h> //定义了一些常用数据类型,比如size_t
#include <fcntl.h>     //定义了open、creat等函数,以及表示文件权限的宏定义
#include <unistd.h>    //定义了read、write、close、lseek等函数
#include <errno.h>     //与全局变量errno相关的定义
#include <sys/ioctl.h> //定义了ioctl函数
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/nvme0n2p1", O_RDWR);
    if (fd < 0)
    {
        printf("/dev/nvme0n2p1  open fail,errno = %d.\r\n", errno);
        return -1;
    }
    struct stat sb;
    fstat(fd, &sb);
    printf("文件类型:                ");

    switch (sb.st_mode & S_IFMT) //st_mode进行”&”操作,从而就可以得到某些特定的信息。
    {
    case S_IFBLK:
        printf("块设备\n");
        break;
    case S_IFCHR:
        printf("字符设备\n");
        break;
    case S_IFDIR:
        printf("目录\n");
        break;
    case S_IFIFO:
        printf("先入先出/管道\n");
        break;
    case S_IFLNK:
        printf("创建符号链接\n");
        break;
    case S_IFREG:
        printf("普通文件\n");
        break;
    case S_IFSOCK:
        printf("套接字\n");
        break;
    default:
        printf("未知数?\n");
        break;
    }

    printf("索引节点号:            %ld\n", (long)sb.st_ino);

    printf("Mode:                     %lo (octal)\n", (unsigned long)sb.st_mode);

    printf("链接数:               %ld\n", (long)sb.st_nlink);
    printf("所有权:                UID=%ld   GID=%ld\n", (long)sb.st_uid, (long)sb.st_gid);

    printf("首选I/O块大小: %ld bytes\n", (long)sb.st_blksize);
    printf("文件大小:                %lld bytes\n", (long long)sb.st_size);
    printf("块分配:         %lld\n", (long long)sb.st_blocks);

    printf("最后状态更改:       %s", ctime(&sb.st_ctime));
    printf("最后的文件访问:         %s", ctime(&sb.st_atime));
    printf("最后的文件修改:   %s", ctime(&sb.st_mtime));
    close(fd);
    return 0;
}

代码来自博客

/dev/nvme0n2p1  open fail,errno = 13. # 需加上sudo

在这里插入图片描述
使用stat命令,其中21为挂载目录下的普通文件
在这里插入图片描述

File: 文件名称
Size: 文件大小
Blocks: 文件占用的块数
IO Block: 4096:
block special file:文件类型
Device: 5h/5d:文件所在设备号,分别以十六进制和十进制显示
Inode: 文件节点号
Links: 1:硬链接数
Access: 访问权限
Uid:所有者ID与名称
Gid:所有者用户组ID与名称
Access:最后访问时间
Modify:最后修改时间
Change:最后状态改变时间
Birth -:无法获知文件创建时间。注意:Linux下的文件未存储文件创建时间

read测试

#include <sys/types.h> //定义了一些常用数据类型,比如size_t
#include <fcntl.h>     //定义了open、creat等函数,以及表示文件权限的宏定义
#include <unistd.h>    //定义了read、write、close、lseek等函数
#include <errno.h>     //与全局变量errno相关的定义
#include <sys/ioctl.h> //定义了ioctl函数
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/nvme0n2p1", O_RDWR);
    if (fd < 0)
    {
        printf("/dev/nvme0n2p1  open fail,errno = %d.\r\n", errno);
        return -1;
    }
    char buf[400];
    int res = read(fd, buf, sizeof(buf));
    if (res < 0)
    {
        printf("read dat fail,errno = %d.\r\n", errno);
        return -1;
    }
    else
    {
        printf("read %d bytes:%s\r\n", res, buf);
    }
    close(fd);
    return 0;
}

在这里插入图片描述
不管buf大小为多少,什么都没输出,但read读入了相应数量的字节,表示全部为空字符(全0)。
write测试

#include <sys/types.h> //定义了一些常用数据类型,比如size_t
#include <fcntl.h>     //定义了open、creat等函数,以及表示文件权限的宏定义
#include <unistd.h>    //定义了read、write、close、lseek等函数
#include <errno.h>     //与全局变量errno相关的定义
#include <sys/ioctl.h> //定义了ioctl函数
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/nvme0n2p1", O_RDWR);
    if (fd < 0)
    {
        printf("/dev/nvme0n2p1  open fail,errno = %d.\r\n", errno);
        return -1;
    }
    char *buf = "test block device hhh";
    int res = write(fd, buf, sizeof(buf));
    if (res < 0)
    {
        printf("write dat fail,errno = %d.\r\n", errno);
        return -1;
    }
    else
    {
        printf("write %d bytes:%s\r\n", res, buf);
    }
    close(fd);
    return 0;
}

在这里插入图片描述
只能写入8字节的数据。

int res = write(fd, buf, sizeof(buf));

更新:这是因为sizeof的对象是指针而不是数组,指针的大小就为8

简单设备测试

将文件名改成/dev/nvme0n2后,除stat测试有一些不同外,其他全部与分区结果一致。
在这里插入图片描述
在这里插入图片描述

设备偏移量测试

#include <sys/types.h> //定义了一些常用数据类型,比如size_t
#include <fcntl.h>     //定义了open、creat等函数,以及表示文件权限的宏定义
#include <unistd.h>    //定义了read、write、close、lseek等函数
#include <errno.h>     //与全局变量errno相关的定义
#include <sys/ioctl.h> //定义了ioctl函数
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/nvme0n2", O_RDWR);
    if (fd < 0)
    {
        printf("/dev/nvme0n2  open fail,errno = %d.\r\n", errno);
        return -1;
    }
    long len = lseek(fd, 0, SEEK_END); // 定位到文件末尾
    printf("lseek return %ld\n", len);
    close(fd);
    return 0;
}

在这里插入图片描述
10737418240即10G!!! 第一个符合预取的测试

设置偏移量,写入数据后再读取,检验是否成功写入,随意设置偏移量为1073741824,即1G

#include <sys/types.h> //定义了一些常用数据类型,比如size_t
#include <fcntl.h>     //定义了open、creat等函数,以及表示文件权限的宏定义
#include <unistd.h>    //定义了read、write、close、lseek等函数
#include <errno.h>     //与全局变量errno相关的定义
#include <sys/ioctl.h> //定义了ioctl函数
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/nvme0n2", O_RDWR);
    if (fd < 0)
    {
        printf("/dev/nvme0n2  open fail,errno = %d.\r\n", errno);
        return -1;
    }
    long len = lseek(fd, 1073741824, SEEK_SET); // 从文件文件开头位置往后移动1073741824个字节
    printf("lseek return %ld\n", len);
    char *write_buf = "test block device hhh";
    int write_res = write(fd, write_buf, sizeof(write_buf));
    if (write_res < 0)
    {
        printf("write dat fail,errno = %d.\r\n", errno);
        return -1;
    }
    else
    {
        printf("write %d bytes:%s\r\n", write_res, write_buf);
    }
    len = lseek(fd, 1073741824, SEEK_SET); // 重置偏移量
    printf("lseek return %ld\n", len);
    char read_buf[400];
    int read_res = read(fd, read_buf, sizeof(read_buf));
    if (read_res < 0)
    {
        printf("read dat fail,errno = %d.\r\n", errno);
        return -1;
    }
    else
    {
        printf("read %d bytes:%s\r\n", read_res, read_buf);
    }

    close(fd);
    return 0;
}

在这里插入图片描述
这表示成功写入了8byte数据

更新:sizeof使用错误
循环写入数据


#include <sys/types.h> //定义了一些常用数据类型,比如size_t
#include <fcntl.h>     //定义了open、creat等函数,以及表示文件权限的宏定义
#include <unistd.h>    //定义了read、write、close、lseek等函数
#include <errno.h>     //与全局变量errno相关的定义
#include <sys/ioctl.h> //定义了ioctl函数
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/nvme0n2", O_RDWR);
    if (fd < 0)
    {
        printf("/dev/nvme0n2  open fail,errno = %d.\r\n", errno);
        return -1;
    }
    long len = lseek(fd, 7073741824, SEEK_SET);
    printf("lseek return %ld\n", len);
    char write_buf[] = "test block device hhh";
    int size = sizeof(write_buf) / sizeof(char);  // 数组长度
    printf("%d\n", size);
    for (char *p = write_buf; p < write_buf + size; p += 8)
    {
        int write_res = write(fd, p, 8);
        if (write_res < 0)
        {
            printf("write dat fail,errno = %d.\r\n", errno);
            return -1;
        }
        else
        {
            printf("write %d bytes\r\n", write_res);
        }
    }

    len = lseek(fd, 7073741824, SEEK_SET);
    printf("lseek return %ld\n", len);
    char read_buf[400];
    int read_res = read(fd, read_buf, sizeof(read_buf));
    if (read_res < 0)
    {
        printf("read dat fail,errno = %d.\r\n", errno);
        return -1;
    }
    else
    {
        printf("read %d bytes:%s\r\n", read_res, read_buf);
    }

    close(fd);
    return 0;
}

在这里插入图片描述
成功写入所有数据!
实际上不用这么麻烦,只是之前用错了sizeof而已
这也不代表成功写入硬盘了,因为会先写入到页缓存中,读取时也是先从页缓存读取

sizeof对数组与指针取值

sizeof(T) operator is used in different way according to the operand type.
sizeof(T)返回存储一个类型T的对象所需要的字节数。

sizeof(指针) 指针变量的大小,64位机器即为8
sizeof(数组) 存储数组需要的字节数
sizeof(数组) / sizeof(数组元素)为数组长度

(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;

(2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量;

(3)指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址!

(4)数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针,同时失去了常量性

参见C/C++数组名与指针区别深入探索

分区偏移量测试

将文件名改成/dev/nvme0n2p1
在这里插入图片描述
大小变成了10736369664,比设备文件小了1048576个字节

其他测试与设备测试结果一致。

结语

通过偏移量测试可以看出,访问块设备文件,就像是访问一个扁平的地址空间,其大小即为设备大小(分区略小,可能是设备需留有部分空间记录分区信息)。
奇怪的一点是:为什么write一次只能写入8个字节? 更新:sizeof使用问题

相关博客:
tips:可使用该网站查询kernel相关结构体与函数
专栏:块设备文件与文件系统的关系
概要:

关于块设备文件,可以从两方面来进行理解。从块设备文件的外部表现来看,它是属于某个外部文件系统上的一个文件。通常Linux内核将其存放在/dev目录下,用户像对常规文件一样来对其进行访问。从块设备文件的内部实现来看,它可以看作是一种特殊文件系统的所属文件,同时该块设备文件的文件逻辑编号与块设备逻辑编号一一对应。
而为了对块设备文件进行便捷的组织与管理,Linux内核创建了bdev文件系统,该文件系统的目的是为了建立块设备文件在外部表现与内部实现之间的关联性。bdev文件系统是一个“伪”文件系统,它只被内核使用,而无需挂载到全局的文件系统树上。

块设备文件inode特征:

  1. 文件模式为块设备文件

  2. 文件内容为块设备编号,保存在inode当中

  3. 文件长度为0

Linux内核利用block_inode(实际上为bdev_inode结构体)数据结构表示块设备的inode,其中包含了两个字段,分别是struct block_device,即块设备描述符。另一个是struct inode,即inode描述符。

struct bdev_inode {
	struct block_device bdev;
	struct inode vfs_inode;
};

Linux系统为了能够对整体的inode进行统一的管理,因此在宿主系统中创建了与bdev文件系统中相对应的inode(次inode)
次inode的i_bdev成员指向主inode的bdev成员


struct inode {
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
		char			*i_link;
		unsigned		i_dir_seq;
	};
// 5.xx内核版本实现不一致,没有i_bdev成员
	union {
		struct pipe_inode_info	*i_pipe;
		struct cdev		*i_cdev;
		char			*i_link;
		unsigned		i_dir_seq;
	};

linux为什么要挂载,直接访问/dev目录不行吗?
概要:

/dev/下的设备文件面向的是设备本身,你虽然可以打开、读取、写入一个存储设备,但是你面向的终究是一个存储设备,不是文件系统。存储设备提供的访问单元是块,比如你可以决定访问某一个或几个扇区的数据,但是对于一个庞大的存储设备,你很难知道哪个块里是什么数据。用户需要面向的单位不是存储块本身,用户面向的单位是文件,而文件这个概念是文件系统提供的,一个文件的数据(和元数据)可能散落在一个存储设备的各个角落,用户通过直接读取存储块的内容的方式获取文件内容是非常困难的,和大海捞针一样。

理解块设备驱动、通用块层、IO调度层的关系和试验
概要:

struct bdev_inode {
	struct block_device bdev;
	struct inode vfs_inode;
};
// block_device块设备结构体
struct block_device {
	sector_t		bd_start_sect;
	sector_t		bd_nr_sectors;
	struct disk_stats __percpu *bd_stats;
	unsigned long		bd_stamp;
	bool			bd_read_only;	/* read-only policy */
	dev_t			bd_dev;
	int			bd_openers;
	struct inode *		bd_inode;	/* will die */
	struct super_block *	bd_super;
	void *			bd_claiming;
	struct device		bd_device;
	void *			bd_holder;
	int			bd_holders;
	bool			bd_write_holder;
	struct kobject		*bd_holder_dir;
	u8			bd_partno;
	spinlock_t		bd_size_lock; /* for bd_inode->i_size updates */
	struct gendisk *	bd_disk;
	struct request_queue *	bd_queue;
}
// gendisk磁盘描述符结构体
struct gendisk {
	/*
	 * major/first_minor/minors should not be set by any new driver, the
	 * block core will take care of allocating them automatically.
	 */
	int major;
	int first_minor;
	int minors;

	char disk_name[DISK_NAME_LEN];	/* name of major driver */

	unsigned short events;		/* supported events */
	unsigned short event_flags;	/* flags related to event processing */

	struct xarray part_tbl;
	struct block_device *part0;

	const struct block_device_operations *fops;
	struct request_queue *queue;
	void *private_data;
}
// 请求队列request_queue
struct request_queue {
	struct request		*last_merge;
	struct elevator_queue	*elevator;

	struct percpu_ref	q_usage_counter;

	struct blk_queue_stats	*stats;
	struct rq_qos		*rq_qos;

	const struct blk_mq_ops	*mq_ops;

	/* sw queues */
	struct blk_mq_ctx __percpu	*queue_ctx;

	unsigned int		queue_depth;

	/* hw dispatch queues */
	struct blk_mq_hw_ctx	**queue_hw_ctx;
	unsigned int		nr_hw_queues;

	/*
	 * The queue owner gets to use this for whatever they like.
	 * ll_rw_blk doesn't touch it.
	 */
	void			*queuedata;
}
// 驱动行为函数
struct blk_mq_ops {
	/**
	 * @queue_rq: Queue a new request from block IO.
	 */
	blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
				 const struct blk_mq_queue_data *);

	/**
	 * @commit_rqs: If a driver uses bd->last to judge when to submit
	 * requests to hardware, it must define this function. In case of errors
	 * that make us stop issuing further requests, this hook serves the
	 * purpose of kicking the hardware (which the last request otherwise
	 * would have done).
	 */
	void (*commit_rqs)(struct blk_mq_hw_ctx *);

	/**
	 * @queue_rqs: Queue a list of new requests. Driver is guaranteed
	 * that each request belongs to the same queue. If the driver doesn't
	 * empty the @rqlist completely, then the rest will be queued
	 * individually by the block layer upon return.
	 */
	void (*queue_rqs)(struct request **rqlist);

	/**
	 * @get_budget: Reserve budget before queue request, once .queue_rq is
	 * run, it is driver's responsibility to release the
	 * reserved budget. Also we have to handle failure case
	 * of .get_budget for avoiding I/O deadlock.
	 */
	int (*get_budget)(struct request_queue *);

	/**
	 * @put_budget: Release the reserved budget.
	 */
	void (*put_budget)(struct request_queue *, int);

	/**
	 * @set_rq_budget_token: store rq's budget token
	 */
	void (*set_rq_budget_token)(struct request *, int);
	/**
	 * @get_rq_budget_token: retrieve rq's budget token
	 */
	int (*get_rq_budget_token)(struct request *);

	/**
	 * @timeout: Called on request timeout.
	 */
	enum blk_eh_timer_return (*timeout)(struct request *, bool);

	/**
	 * @poll: Called to poll for completion of a specific tag.
	 */
	int (*poll)(struct blk_mq_hw_ctx *, struct io_comp_batch *);

	/**
	 * @complete: Mark the request as complete.
	 */
	void (*complete)(struct request *);

	/**
	 * @init_hctx: Called when the block layer side of a hardware queue has
	 * been set up, allowing the driver to allocate/init matching
	 * structures.
	 */
	int (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
	/**
	 * @exit_hctx: Ditto for exit/teardown.
	 */
	void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);

	/**
	 * @init_request: Called for every command allocated by the block layer
	 * to allow the driver to set up driver specific data.
	 *
	 * Tag greater than or equal to queue_depth is for setting up
	 * flush request.
	 */
	int (*init_request)(struct blk_mq_tag_set *set, struct request *,
			    unsigned int, unsigned int);
	/**
	 * @exit_request: Ditto for exit/teardown.
	 */
	void (*exit_request)(struct blk_mq_tag_set *set, struct request *,
			     unsigned int);

	/**
	 * @cleanup_rq: Called before freeing one request which isn't completed
	 * yet, and usually for freeing the driver private data.
	 */
	void (*cleanup_rq)(struct request *);

	/**
	 * @busy: If set, returns whether or not this queue currently is busy.
	 */
	bool (*busy)(struct request_queue *);

	/**
	 * @map_queues: This allows drivers specify their own queue mapping by
	 * overriding the setup-time function that builds the mq_map.
	 */
	int (*map_queues)(struct blk_mq_tag_set *set);

#ifdef CONFIG_BLK_DEBUG_FS
	/**
	 * @show_rq: Used by the debugfs implementation to show driver-specific
	 * information about a request.
	 */
	void (*show_rq)(struct seq_file *m, struct request *rq);
#endif
};

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最佳损友1020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值