文件: 一个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
fdisk /dev/nvme0n2
lsblk
sudo mkfs.ext4 /dev/nvme0n2p1
sudo gedit /etc/fstab
/dev/nvme0n2p1 /home/lt1020/Desktop/NVMeTest ext4 defaults 0 0
sudo chown -R lt1020:lt1020 /home/lt1020/Desktop/NVMeTest
简单测试只是预测试,偏移量测试得到了想要的结果。
简单分区测试
stat测试
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#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)
{
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.
使用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>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#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>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#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字节的数据。
简单设备测试
将文件名改成/dev/nvme0n2后,除stat测试有一些不同外,其他全部与分区结果一致。
设备偏移量测试
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#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>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#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);
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数据
循环写入数据
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#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(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个字节?
相关博客: tips:可使用该网站查询kernel相关结构体与函数 专栏:块设备文件与文件系统的关系 概要:
关于块设备文件,可以从两方面来进行理解。从块设备文件的外部表现来看,它是属于某个外部文件系统上的一个文件。通常Linux内核将其存放在/dev目录下,用户像对常规文件一样来对其进行访问。从块设备文件的内部实现来看,它可以看作是一种特殊文件系统的所属文件,同时该块设备文件的文件逻辑编号与块设备逻辑编号一一对应。 而为了对块设备文件进行便捷的组织与管理,Linux内核创建了bdev文件系统,该文件系统的目的是为了建立块设备文件在外部表现与内部实现之间的关联性。bdev文件系统是一个“伪”文件系统,它只被内核使用,而无需挂载到全局的文件系统树上。
块设备文件inode特征:
-
文件模式为块设备文件 -
文件内容为块设备编号,保存在inode当中 -
文件长度为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;
};
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;
};
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;
dev_t bd_dev;
int bd_openers;
struct inode * bd_inode;
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;
struct gendisk * bd_disk;
struct request_queue * bd_queue;
}
struct gendisk {
int major;
int first_minor;
int minors;
char disk_name[DISK_NAME_LEN];
unsigned short events;
unsigned short event_flags;
struct xarray part_tbl;
struct block_device *part0;
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
}
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;
struct blk_mq_ctx __percpu *queue_ctx;
unsigned int queue_depth;
struct blk_mq_hw_ctx **queue_hw_ctx;
unsigned int nr_hw_queues;
void *queuedata;
}
struct blk_mq_ops {
blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
const struct blk_mq_queue_data *);
void (*commit_rqs)(struct blk_mq_hw_ctx *);
void (*queue_rqs)(struct request **rqlist);
int (*get_budget)(struct request_queue *);
void (*put_budget)(struct request_queue *, int);
void (*set_rq_budget_token)(struct request *, int);
int (*get_rq_budget_token)(struct request *);
enum blk_eh_timer_return (*timeout)(struct request *, bool);
int (*poll)(struct blk_mq_hw_ctx *, struct io_comp_batch *);
void (*complete)(struct request *);
int (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);
int (*init_request)(struct blk_mq_tag_set *set, struct request *,
unsigned int, unsigned int);
void (*exit_request)(struct blk_mq_tag_set *set, struct request *,
unsigned int);
void (*cleanup_rq)(struct request *);
bool (*busy)(struct request_queue *);
int (*map_queues)(struct blk_mq_tag_set *set);
#ifdef CONFIG_BLK_DEBUG_FS
void (*show_rq)(struct seq_file *m, struct request *rq);
#endif
};
|