linux操作系统:块设备,如何建立代理商销售模式

块设备

注册管理

  • 块设备一般会被格式化为文件系统。
  • 块设备也需要mknod。
    • 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->i_mode = mode;
	if (S_ISCHR(mode)) {
		inode->i_fop = &def_chr_fops;
		inode->i_rdev = rdev;
	} else if (S_ISBLK(mode)) {
		inode->i_fop = &def_blk_fops;
		inode->i_rdev = rdev;
	} else if (S_ISFIFO(mode))
		inode->i_fop = &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,
};
  • mount可以将这个块设备文件挂载到一个文件夹下面。如果这个块设备原来被格式化为一种文件系统的格式,比如ext4,那我们调用的就是ext4相应的mount操作。这是块设备遇到的第二个文件系统,也就是向这个块设备读写文件,需要基于的主流文件系统。
  • 在将一个硬盘的块设备 mount 成为 ext4 的时候,我们会调用 ext4_mount->mount_bdev。
// 注册
static struct file_system_type ext4_fs_type = {
	.owner		= THIS_MODULE,
	.name		= "ext4",
	.mount		= ext4_mount,
	.kill_sb	= kill_block_super,
	.fs_flags	= FS_REQUIRES_DEV,
};

// 挂载
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 & 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->s_root);
......
}

mount_bdev 主要做了两件大事。

  • 第一,blkdev_get_by_path根据/dev/xxx这个名字,找到相应的设备并打开它;
  • 第二,sget根据打开的设备文件,填充ext4文件系统的super_block,从而以此为基础,建立一整套文件系统体系。一旦这套体系建立起来后,对文件系统的读写都是通过ex4文件系统这个体系进行的,创建的inode结构也是指向ext4文件系统的。

这里我们先来看 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打开这个设备

块设备的打开往往不是直接默认设备文件的打开函数,而是调用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, &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->i_rdev);
	if (bdev) {
		spin_lock(&bdev_lock);
		if (!inode->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 ->i_mapping always
			 * without igrab().
			 */
			bdgrab(bdev);
			inode->i_bdev = bdev;
			inode->i_mapping = bdev->bd_inode->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, &dev);
 
        bdev = &BDEV_I(inode)->bdev;
 
 
        if (inode->i_state & I_NEW) {
                bdev->bd_contains = NULL;
                bdev->bd_super = NULL;
                bdev->bd_inode = inode;
                bdev->bd_block_size = i_blocksize(inode);
                bdev->bd_part_count = 0;
                bdev->bd_invalidated = 0;
                inode->i_mode = S_IFBLK;
                inode->i_rdev = dev;
                inode->i_bdev = bdev;
                inode->i_data.a_ops = &def_blk_aops;
                mapping_set_gfp_mask(&inode->i_data, GFP_USER);
                spin_lock(&bdev_lock);
                list_add(&bdev->bd_list, &all_bdevs);
                spin_unlock(&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           = "bdev",
        .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("bdev_cache", sizeof(struct bdev_inode), 0, (SLAB_HWCACHE_ALIGN|SLAB_RECLAIM_ACCOUNT|SLAB_MEM_SPREAD|SLAB_ACCOUNT|SLAB_PANIC), init_once);
        err = register_filesystem(&bd_type);
        if (err)
                panic("Cannot register bdev pseudo-fs");
        bd_mnt = kern_mount(&bd_type);
        if (IS_ERR(bd_mnt))
                panic("Cannot create bdev pseudo-fs");
        blockdev_superblock = bd_mnt->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,我们也应该能知道它分了几个区域,所以就有了下图这个复杂的关系结构。

在这里插入图片描述
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是表示在这个块设备上的请求队列

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_struct
  • bd_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 & FMODE_READ)
		perm |= MAY_READ;
	if (mode & FMODE_WRITE)
		perm |= MAY_WRITE;
......
	disk = get_gendisk(bdev->bd_dev, &partno);
......
	owner = disk->fops->owner;
......
	if (!bdev->bd_openers) {
		bdev->bd_disk = disk;
		bdev->bd_queue = disk->queue;
		bdev->bd_contains = bdev;
 
 
		if (!partno) {
			ret = -ENXIO;
			bdev->bd_part = disk_get_part(disk, partno);
......
			if (disk->fops->open) {
				ret = disk->fops->open(bdev, mode);
......
			}
 
 
			if (!ret)
				bd_set_size(bdev,(loff_t)get_capacity(disk)<<9);
 
 
			if (bdev->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->bd_contains = whole;
			bdev->bd_part = disk_get_part(disk, partno);
......
			bd_set_size(bdev, (loff_t)bdev->bd_part->nr_sects << 9);
		}
	} 
......
	bdev->bd_openers++;
	if (for_part)
		bdev->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(&ext_devt_idr, blk_mangle_minor(MINOR(devt)));
		if (part && get_disk(part_to_disk(part))) {
			*partno = part->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->device_add_disk->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->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			= &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->s_bdev = data;
	s->s_dev = s->s_bdev->bd_dev;
	s->s_bdi = bdi_get(s->s_bdev->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 & ~MS_SUBMOUNT), user_ns);
......
	}
	err = set(s, data);
......
	s->s_type = type;
	strlcpy(s->s_id, type->name, sizeof(s->s_id));
	list_add_tail(&s->s_list, &super_blocks);
	hlist_add_head(&s->s_instances, &type->fs_supers);
	spin_unlock(&sb_lock);
	get_filesystem(type);
	register_shrinker(&s->s_shrink);
	return s;
}

好了,到此为止,mount 中一个块设备的过程就结束了。设备打开了,形成了 block_device 结构,并且塞到了 super_block 中。

有了 ext4 文件系统的 super_block 之后,接下来对于文件的读写过程,就和文件系统这里的过程一摸一样了。只要不涉及真正写入设备的代码,super_block 中的这个 block_device 就没啥用处。这也是为什么文件系统中,我们丝毫感觉不到它的存在,但是一旦到了底层,就到了 block_device 起作用的时候了,这个我们下一节仔细分析。

小结

  • 所有的块设备被一个 map 结构管理从 dev_t 到 gendisk 的映射;
  • 所有的 block_device 表示的设备或者分区都在 bdev 文件系统的 inode 列表中;
  • mknod 创建出来的块设备文件在 devtemfs 文件系统里面,特殊 inode 里面有块设备号;
  • mount 一个块设备上的文件系统,调用这个文件系统的 mount 接口;
  • 通过按照 /dev/xxx 在文件系统 devtmpfs 文件系统上搜索到特殊 inode,得到块设备号;
  • 根据特殊 inode 里面的 dev_t 在 bdev 文件系统里面找到 inode;
  • 根据 bdev 文件系统上的 inode 找到对应的 block_device,根据 dev_t 在 map 中找到 gendisk,将两者关联起来;
  • 找到 block_device 后打开设备,调用和 block_device 关联的 gendisk 里面的 block_device_operations 打开设备;
  • 创建被 mount 的文件系统的 super_block。

在这里插入图片描述

如何将块设备 I/O 请求送达到外部设备。

当文件系统写入时,对于ext4文件系统,将会调用ext4_file_write_iter,它将IO的调用分成两种情况:

  • 第一是直接IO。最终我们调用的是generic_file_direct_write,这里调用的是mapping->a_ops->direct_IO,实际调用的是 ext4_direct_IO,往设备层写入数据
  • 第二第缓存IO。最终我们会将数据从应用拷贝到内存缓存,但是这个时候,并不执行真正的IO操作。它们只是将整个页或者其中部分标记为脏。写操作由一个timer触发,那么时候,才调用wb_workfn往硬盘写入数据

接下来的调用链为:wb_workfn->wb_do_writeback->wb_writeback->writeback_sb_inodes->__writeback_single_inode->do_writepages。在 do_writepages 中,我们要调用 mapping->a_ops->writepages,但实际调用的是 ext4_writepages,往设备层写入数据。

这一节,我们就沿着这两种情况分析下去。

直接IO如何访问块设备

我们先来看第一种情况,直接 I/O 调用到 ext4_direct_IO。

static ssize_t ext4_direct_IO(struct kiocb *iocb, struct iov_iter *iter)
{
	struct file *file = iocb->ki_filp;
	struct inode *inode = file->f_mapping->host;
	size_t count = iov_iter_count(iter);
	loff_t offset = iocb->ki_pos;
	ssize_t ret;
......
	ret = ext4_direct_IO_write(iocb, iter);
......
}
 
 
static ssize_t ext4_direct_IO_write(struct kiocb *iocb, struct iov_iter *iter)
{
	struct file *file = iocb->ki_filp;
	struct inode *inode = file->f_mapping->host;
	struct ext4_inode_info *ei = EXT4_I(inode);
	ssize_t ret;
	loff_t offset = iocb->ki_pos;
	size_t count = iov_iter_count(iter);
......
	ret = __blockdev_direct_IO(iocb, inode, inode->i_sb->s_bdev, iter,
				   get_block_func, ext4_end_io_dio, NULL,
				   dio_flags);
 
 
……
}

在 ext4_direct_IO_write 调用 __blockdev_direct_IO,有个参数你需要特别注意一下,那就是 inode->i_sb->s_bdev。通过当前文件的 inode,我们可以得到 super_block。这个 super_block 中的 s_bdev,就是咱们上一节填进去的那个 block_device。

__blockdev_direct_IO 会调用 do_blockdev_direct_IO,在这里面我们要准备一个 struct dio 结构和 struct dio_submit 结构,用来描述将要发生的写入请求

static inline ssize_t
do_blockdev_direct_IO(struct kiocb *iocb, struct inode *inode,
		      struct block_device *bdev, struct iov_iter *iter,
		      get_block_t get_block, dio_iodone_t end_io,
		      dio_submit_t submit_io, int flags)
{
	unsigned i_blkbits = ACCESS_ONCE(inode->i_blkbits);
	unsigned blkbits = i_blkbits;
	unsigned blocksize_mask = (1 << blkbits) - 1;
	ssize_t retval = -EINVAL;
	size_t count = iov_iter_count(iter);
	loff_t offset = iocb->ki_pos;
	loff_t end = offset + count;
	struct dio *dio;
	struct dio_submit sdio = { 0, };
	struct buffer_head map_bh = { 0, };
......
	dio = kmem_cache_alloc(dio_cache, GFP_KERNEL);
	dio->flags = flags;
	dio->i_size = i_size_read(inode);
	dio->inode = inode;
	if (iov_iter_rw(iter) == WRITE) {
		dio->op = REQ_OP_WRITE;
		dio->op_flags = REQ_SYNC | REQ_IDLE;
		if (iocb->ki_flags & IOCB_NOWAIT)
			dio->op_flags |= REQ_NOWAIT;
	} else {
		dio->op = REQ_OP_READ;
	}
	sdio.blkbits = blkbits;
	sdio.blkfactor = i_blkbits - blkbits;
	sdio.block_in_file = offset >> blkbits;
 
 
	sdio.get_block = get_block;
	dio->end_io = end_io;
	sdio.submit_io = submit_io;
	sdio.final_block_in_bio = -1;
	sdio.next_block_for_io = -1;
 
 
	dio->iocb = iocb;
	dio->refcount = 1;
 
 
	sdio.iter = iter;
	sdio.final_block_in_request =
		(offset + iov_iter_count(iter)) >> blkbits;
......
	sdio.pages_in_io += iov_iter_npages(iter, INT_MAX);
 
 
	retval = do_direct_IO(dio, &sdio, &map_bh);
.....
}

do_direct_IO 里面有两层循环,第一层循环是依次处理这次要写入的所有块。对于每一块,取出对应的内存中的页 page,在这一块中,有写入的起始地址 from 和终止地址 to,所以,第二层循环就是依次处理 from 到 to 的数据,调用 submit_page_section,提交到块设备层进行写入。

static int do_direct_IO(struct dio *dio, struct dio_submit *sdio,
			struct buffer_head *map_bh)
{
	const unsigned blkbits = sdio->blkbits;
	const unsigned i_blkbits = blkbits + sdio->blkfactor;
	int ret = 0;
 
 
	while (sdio->block_in_file < sdio->final_block_in_request) {
		struct page *page;
		size_t from, to;
 
 
		page = dio_get_page(dio, sdio);
        from = sdio->head ? 0 : sdio->from;
		to = (sdio->head == sdio->tail - 1) ? sdio->to : PAGE_SIZE;
		sdio->head++;
 
 
		while (from < to) {
			unsigned this_chunk_bytes;	/* # of bytes mapped */
			unsigned this_chunk_blocks;	/* # of blocks */
......
            ret = submit_page_section(dio, sdio, page,
						  from,
						  this_chunk_bytes,
						  sdio->next_block_for_io,
						  map_bh);
......
			sdio->next_block_for_io += this_chunk_blocks;
			sdio->block_in_file += this_chunk_blocks;
			from += this_chunk_bytes;
			dio->result += this_chunk_bytes;
			sdio->blocks_available -= this_chunk_blocks;
			if (sdio->block_in_file == sdio->final_block_in_request)
				break;
......
        }
    }
}

submit_page_section 会调用 dio_bio_submit,进而调用 submit_bio 向块设备层提交数据。其中,参数 struct bio 是将数据传给块设备的通用传输对象。定义如下:

/**
 * submit_bio - submit a bio to the block device layer for I/O
 * @bio: The &struct bio which describes the I/O
 */
blk_qc_t submit_bio(struct bio *bio)
{
......
  return generic_make_request(bio);
}

缓存IO如何访问块设备

我们再来看第二种情况,缓存 I/O 调用到 ext4_writepages。这个函数比较长,我们这里只截取最重要的部分来讲解。

static int ext4_writepages(struct address_space *mapping,
			   struct writeback_control *wbc)
{
......
	struct mpage_da_data mpd;
	struct inode *inode = mapping->host;
	struct ext4_sb_info *sbi = EXT4_SB(mapping->host->i_sb);
......
	mpd.do_map = 0;
	mpd.io_submit.io_end = ext4_init_io_end(inode, GFP_KERNEL);
	ret = mpage_prepare_extent_to_map(&mpd);
	/* Submit prepared bio */
	ext4_io_submit(&mpd.io_submit);
......
}

这里比较重要的一个数据结构是 struct mpage_da_data。这里面有文件的 inode、要写入的页的偏移量,还有一个重要的 struct ext4_io_submit,里面有通用传输对象 bio。

struct mpage_da_data {
	struct inode *inode;
......
	pgoff_t first_page;	/* The first page to write */
	pgoff_t next_page;	/* Current page to examine */
	pgoff_t last_page;	/* Last page to examine */
	struct ext4_map_blocks map;
	struct ext4_io_submit io_submit;	/* IO submission data */
	unsigned int do_map:1;
};
 
 
struct ext4_io_submit {
......
	struct bio		*io_bio;
	ext4_io_end_t		*io_end;
	sector_t		io_next_block;
};

在 ext4_writepages 中,mpage_prepare_extent_to_map 用于初始化这个 struct mpage_da_data 结构。接下来的调用链为:mpage_prepare_extent_to_map->mpage_process_page_bufs->mpage_submit_page->ext4_bio_write_page->io_submit_add_bh。

在 io_submit_add_bh 中,此时的 bio 还是空的,因而我们要调用 io_submit_init_bio,初始化 bio。

static int io_submit_init_bio(struct ext4_io_submit *io,
			      struct buffer_head *bh)
{
	struct bio *bio;
 
 
	bio = bio_alloc(GFP_NOIO, BIO_MAX_PAGES);
	if (!bio)
		return -ENOMEM;
	wbc_init_bio(io->io_wbc, bio);
	bio->bi_iter.bi_sector = bh->b_blocknr * (bh->b_size >> 9);
	bio->bi_bdev = bh->b_bdev;
	bio->bi_end_io = ext4_end_bio;
	bio->bi_private = ext4_get_io_end(io->io_end);
	io->io_bio = bio;
	io->io_next_block = bh->b_blocknr;
	return 0;
}
 

我们再回到 ext4_writepages 中。在 bio 初始化完之后,我们要调用 ext4_io_submit,提交 I/O。在这里我们又是调用 submit_bio,向块设备层传输数据。ext4_io_submit 的实现如下:

void ext4_io_submit(struct ext4_io_submit *io)
{
	struct bio *bio = io->io_bio;
 
 
	if (bio) {
		int io_op_flags = io->io_wbc->sync_mode == WB_SYNC_ALL ?
				  REQ_SYNC : 0;
		io->io_bio->bi_write_hint = io->io_end->inode->i_write_hint;
		bio_set_op_attrs(io->io_bio, REQ_OP_WRITE, io_op_flags);
		submit_bio(io->io_bio);
	}
	io->io_bio = NULL;
}

如何向块设备层提交请求

既然无论是直接 I/O,还是缓存 I/O,最后都到了 submit_bio 里面,我们就来重点分析一下它。

submit_bio 会调用 generic_make_request。代码如下:

blk_qc_t generic_make_request(struct bio *bio)
{
	/*
	 * bio_list_on_stack[0] contains bios submitted by the current
	 * make_request_fn.
	 * bio_list_on_stack[1] contains bios that were submitted before
	 * the current make_request_fn, but that haven't been processed
	 * yet.
	 */
	struct bio_list bio_list_on_stack[2];
	blk_qc_t ret = BLK_QC_T_NONE;
......
	if (current->bio_list) {
		bio_list_add(&current->bio_list[0], bio);
		goto out;
	}
 
 
	bio_list_init(&bio_list_on_stack[0]);
	current->bio_list = bio_list_on_stack;
	do {
		struct request_queue *q = bdev_get_queue(bio->bi_bdev);
 
 
		if (likely(blk_queue_enter(q, bio->bi_opf & REQ_NOWAIT) == 0)) {
			struct bio_list lower, same;
 
 
			/* Create a fresh bio_list for all subordinate requests */
			bio_list_on_stack[1] = bio_list_on_stack[0];
			bio_list_init(&bio_list_on_stack[0]);
			ret = q->make_request_fn(q, bio);
 
 
			blk_queue_exit(q);
 
 
			/* sort new bios into those for a lower level
			 * and those for the same level
			 */
			bio_list_init(&lower);
			bio_list_init(&same);
			while ((bio = bio_list_pop(&bio_list_on_stack[0])) != NULL)
				if (q == bdev_get_queue(bio->bi_bdev))
					bio_list_add(&same, bio);
				else
					bio_list_add(&lower, bio);
			/* now assemble so we handle the lowest level first */
			bio_list_merge(&bio_list_on_stack[0], &lower);
			bio_list_merge(&bio_list_on_stack[0], &same);
			bio_list_merge(&bio_list_on_stack[0], &bio_list_on_stack[1]);
		} 
......
		bio = bio_list_pop(&bio_list_on_stack[0]);
	} while (bio);
	current->bio_list = NULL; /* deactivate */
out:
	return ret;
}

这里的逻辑有点复杂,我们先来看大的逻辑。在 do-while 中,我们先是获取一个请求队列 request_queue,然后调用这个队列的 make_request_fn 函数。

块设备队列结构

如果再来看 struct block_device 结构和 struct gendisk 结构,我们会发现,每个块设备都有一个请求队列 struct request_queue,用于处理上层发来的请求。

在每个块设备的驱动程序初始化的时候,会生成一个 request_queue。

struct request_queue {
	/*
	 * Together with queue_head for cacheline sharing
	 */
	struct list_head	queue_head;
	struct request		*last_merge;
	struct elevator_queue	*elevator;
......
	request_fn_proc		*request_fn;
	make_request_fn		*make_request_fn;
......
}

在请求队列 request_queue 上,首先是有一个链表 list_head,保存请求 request。

struct request {
	struct list_head queuelist;
......
	struct request_queue *q;
......
	struct bio *bio;
	struct bio *biotail;
......
}

每个 request 包括一个链表的 struct bio,有指针指向一头一尾。

struct bio {
	struct bio		*bi_next;	/* request queue link */
	struct block_device	*bi_bdev;
	blk_status_t		bi_status;
......
    struct bvec_iter	bi_iter;
	unsigned short		bi_vcnt;	/* how many bio_vec's */
	unsigned short		bi_max_vecs;	/* max bvl_vecs we can hold */
	atomic_t		__bi_cnt;	/* pin count */
	struct bio_vec		*bi_io_vec;	/* the actual vec list */
......
};
 
 
struct bio_vec {
	struct page	*bv_page;
	unsigned int	bv_len;
	unsigned int	bv_offset;
}

在 bio 中,bi_next 是链表中的下一项,struct bio_vec 指向一组页面。
在这里插入图片描述
在请求队列 request_queue 上,还有两个重要的函数,一个是 make_request_fn 函数,用于生成 request;另一个是 request_fn 函数,用于处理 request。

块设备的初始化

我们还是以 scsi 驱动为例。在初始化设备驱动的时候,我们会调用 scsi_alloc_queue,把 request_fn 设置为 scsi_request_fn。我们还会调用 blk_init_allocated_queue->blk_queue_make_request,把 make_request_fn 设置为 blk_queue_bio。

/**
 * scsi_alloc_sdev - allocate and setup a scsi_Device
 * @starget: which target to allocate a &scsi_device for
 * @lun: which lun
 * @hostdata: usually NULL and set by ->slave_alloc instead
 *
 * Description:
 *     Allocate, initialize for io, and return a pointer to a scsi_Device.
 *     Stores the @shost, @channel, @id, and @lun in the scsi_Device, and
 *     adds scsi_Device to the appropriate list.
 *
 * Return value:
 *     scsi_Device pointer, or NULL on failure.
 **/
static struct scsi_device *scsi_alloc_sdev(struct scsi_target *starget,
					   u64 lun, void *hostdata)
{
	struct scsi_device *sdev;
	sdev = kzalloc(sizeof(*sdev) + shost->transportt->device_size,
		       GFP_ATOMIC);
......
	sdev->request_queue = scsi_alloc_queue(sdev);
......
}
 
 
struct request_queue *scsi_alloc_queue(struct scsi_device *sdev)
{
	struct Scsi_Host *shost = sdev->host;
	struct request_queue *q;
 
 
	q = blk_alloc_queue_node(GFP_KERNEL, NUMA_NO_NODE);
	if (!q)
		return NULL;
	q->cmd_size = sizeof(struct scsi_cmnd) + shost->hostt->cmd_size;
	q->rq_alloc_data = shost;
	q->request_fn = scsi_request_fn;
	q->init_rq_fn = scsi_init_rq;
	q->exit_rq_fn = scsi_exit_rq;
	q->initialize_rq_fn = scsi_initialize_rq;
 
 
    // 调用 blk_queue_make_request(q, blk_queue_bio);
	if (blk_init_allocated_queue(q) < 0) {
		blk_cleanup_queue(q);
		return NULL;
	}
 
 
	__scsi_init_queue(shost, q);
......
	return q
}

在 blk_init_allocated_queue 中,除了初始化 make_request_fn 函数,我们还要做一件很重要的事情,就是初始化 I/O 的电梯算法。

int blk_init_allocated_queue(struct request_queue *q)
{
	q->fq = blk_alloc_flush_queue(q, NUMA_NO_NODE, q->cmd_size);
......
	blk_queue_make_request(q, blk_queue_bio);
......
	/* init elevator */
	if (elevator_init(q, NULL)) {
......
	}
......
}

电梯算法有很多种类型,定义为 elevator_type:

  • struct elevator_type elevator_noop:Noop 调度算法是最简单的 IO 调度算法,它将 IO 请求放入到一个 FIFO 队列中,然后逐个执行这些 IO 请求。
  • struct elevator_type iosched_deadline
    • deadline算法要保证每个IO请求在一定的时间内一定要被服务到,以此来避免某个请求饥饿。
    • 为了完成这个目标,算法中引入了两类队列,一类队列用来对请求按照起始扇区序号进行排序,通过红黑树来组织,叫做sort_list,按照此队列传输性能会比较高;另一类队列对请求按照它们的生成时间进行排序,由链表来组织,称为fifo_list,并且每一个请求都有一个期限值
  • struct elevator_type iosched_cfq
    • CFQ 完全公平调度算法。所有的请求会在多个队列中排序。同一个进程的请求,总是在同一个队列中处理。时间片会分配给每个队列,通过轮询算法,我们保证了IO带宽,以公平的方式,在不同队列之间进行共享

elevator_init 中会根据名称来指定电梯算法,如果没有选择,那就默认使用 iosched_cfq。

请求与调度

接下来,我们回到 generic_make_request 函数中。调用队列的 make_request_fn 函数,其实就是调用 blk_queue_bio。

static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
{
	struct request *req, *free;
	unsigned int request_count = 0;
......
	switch (elv_merge(q, &req, bio)) {
	case ELEVATOR_BACK_MERGE:
		if (!bio_attempt_back_merge(q, req, bio))
			break;
		elv_bio_merged(q, req, bio);
		free = attempt_back_merge(q, req);
		if (free)
			__blk_put_request(q, free);
		else
			elv_merged_request(q, req, ELEVATOR_BACK_MERGE);
		goto out_unlock;
	case ELEVATOR_FRONT_MERGE:
		if (!bio_attempt_front_merge(q, req, bio))
			break;
		elv_bio_merged(q, req, bio);
		free = attempt_front_merge(q, req);
		if (free)
			__blk_put_request(q, free);
		else
			elv_merged_request(q, req, ELEVATOR_FRONT_MERGE);
		goto out_unlock;
	default:
		break;
	}
 
 
get_rq:
	req = get_request(q, bio->bi_opf, bio, GFP_NOIO);
......
	blk_init_request_from_bio(req, bio);
......
	add_acct_request(q, req, where);
	__blk_run_queue(q);
out_unlock:
......
	return BLK_QC_T_NONE;
}

blk_queue_bio 首先要做的第一个事情就是调用elv_merge来判断,当前这个bio请求能否和目前已有的request合并起来,成为同一批IO操作,从而提高读取和缓存的性能

判断标准和 struct bio 的成员 struct bvec_iter 有关,它里面有两个变量,一个是起始磁盘簇 bi_sector,另一个是大小 bi_size

enum elv_merge elv_merge(struct request_queue *q, struct request **req,
		struct bio *bio)
{
	struct elevator_queue *e = q->elevator;
	struct request *__rq;
......
	if (q->last_merge && elv_bio_merge_ok(q->last_merge, bio)) {
		enum elv_merge ret = blk_try_merge(q->last_merge, bio);
 
 
		if (ret != ELEVATOR_NO_MERGE) {
			*req = q->last_merge;
			return ret;
		}
	}
......
	__rq = elv_rqhash_find(q, bio->bi_iter.bi_sector);
	if (__rq && elv_bio_merge_ok(__rq, bio)) {
		*req = __rq;
		return ELEVATOR_BACK_MERGE;
	}
 
 
	if (e->uses_mq && e->type->ops.mq.request_merge)
		return e->type->ops.mq.request_merge(q, req, bio);
	else if (!e->uses_mq && e->type->ops.sq.elevator_merge_fn)
		return e->type->ops.sq.elevator_merge_fn(q, req, bio);
 
 
	return ELEVATOR_NO_MERGE;
}

elv_merge 尝试了三次合并。

第一次,它先判断和上一次合并的 request 能不能再次合并,看看能不能赶上马上要走的这部电梯。在 blk_try_merge 主要做了这样的判断:如果 blk_rq_pos(rq) + blk_rq_sectors(rq) == bio->bi_iter.bi_sector,也就是说这个 request 的起始地址加上它的大小(其实是这个 request 的结束地址),如果和 bio 的起始地址能接得上,那就把 bio 放在 request 的最后,我们称为 ELEVATOR_BACK_MERGE。

如果 blk_rq_pos(rq) - bio_sectors(bio) == bio->bi_iter.bi_sector,也就是说,这个 request 的起始地址减去 bio 的大小等于 bio 的起始地址,这说明 bio 放在 request 的最前面能够接得上,那就把 bio 放在 request 的最前面,我们称为 ELEVATOR_FRONT_MERGE。否则,那就不合并,我们称为 ELEVATOR_NO_MERGE。

enum elv_merge blk_try_merge(struct request *rq, struct bio *bio)
{
......
    if (blk_rq_pos(rq) + blk_rq_sectors(rq) == bio->bi_iter.bi_sector)
		return ELEVATOR_BACK_MERGE;
	else if (blk_rq_pos(rq) - bio_sectors(bio) == bio->bi_iter.bi_sector)
		return ELEVATOR_FRONT_MERGE;
	return ELEVATOR_NO_MERGE;
}

第二次,如果和上一个合并过的 request 无法合并,那我们就调用 elv_rqhash_find。然后按照 bio 的起始地址查找 request,看有没有能够合并的。如果有的话,因为是按照起始地址找的,应该接在人家的后面,所以是 ELEVATOR_BACK_MERGE。

第三次,调用 elevator_merge_fn 试图合并。对于 iosched_cfq,调用的是 cfq_merge。在这里面,cfq_find_rq_fmerge 会调用 elv_rb_find 函数,里面的参数是 bio 的结束地址。我们还是要看,能不能找到可以合并的。如果有的话,因为是按照结束地址找的,应该接在人家前面,所以是 ELEVATOR_FRONT_MERGE。

static enum elv_merge cfq_merge(struct request_queue *q, struct request **req,
		     struct bio *bio)
{
	struct cfq_data *cfqd = q->elevator->elevator_data;
	struct request *__rq;
 
 
	__rq = cfq_find_rq_fmerge(cfqd, bio);
	if (__rq && elv_bio_merge_ok(__rq, bio)) {
		*req = __rq;
		return ELEVATOR_FRONT_MERGE;
	}
 
 
	return ELEVATOR_NO_MERGE;
}
 
 
static struct request *
cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio)
{
	struct task_struct *tsk = current;
	struct cfq_io_cq *cic;
	struct cfq_queue *cfqq;
 
 
	cic = cfq_cic_lookup(cfqd, tsk->io_context);
	if (!cic)
		return NULL;
 
 
	cfqq = cic_to_cfqq(cic, op_is_sync(bio->bi_opf));
	if (cfqq)
		return elv_rb_find(&cfqq->sort_list, bio_end_sector(bio));
 
 
	return NUL
}

等从 elv_merge 返回 blk_queue_bio 的时候,我们就知道,应该做哪种类型的合并,接着就要进行真的合并。如果没有办法合并,那就调用 get_request,创建一个新的 request,调用 blk_init_request_from_bio,将 bio 放到新的 request 里面,然后调用 add_acct_request,把新的 request 加到 request_queue 队列中。

至此,我们解析完了 generic_make_request 中最重要的两大逻辑:获取一个请求队列 request_queue 和调用这个队列的 make_request_fn 函数。

请求的处理

设备驱动程序往设备里面写,调用的是请求队列 request_queue 的另外一个函数 request_fn。对于 scsi 设备来讲,调用的是 scsi_request_fn。

static void scsi_request_fn(struct request_queue *q)
	__releases(q->queue_lock)
	__acquires(q->queue_lock)
{
	struct scsi_device *sdev = q->queuedata;
	struct Scsi_Host *shost;
	struct scsi_cmnd *cmd;
	struct request *req;
 
 
	/*
	 * To start with, we keep looping until the queue is empty, or until
	 * the host is no longer able to accept any more requests.
	 */
	shost = sdev->host;
	for (;;) {
		int rtn;
		/*
		 * get next queueable request.  We do this early to make sure
		 * that the request is fully prepared even if we cannot
		 * accept it.
		 */
		req = blk_peek_request(q);
......
		/*
		 * Remove the request from the request list.
		 */
		if (!(blk_queue_tagged(q) && !blk_queue_start_tag(q, req)))
			blk_start_request(req);
.....
		cmd = req->special;
......
		/*
		 * Dispatch the command to the low-level driver.
		 */
		cmd->scsi_done = scsi_done;
		rtn = scsi_dispatch_cmd(cmd);
......
	}
	return;
......
}

在这里面是一个 for 无限循环,从 request_queue 中读取 request,然后封装更加底层的指令,给设备控制器下指令,实施真正的 I/O 操作。

小结

对于块设备的 I/O 操作分为两种,一种是直接 I/O,另一种是缓存 I/O。无论是哪种 I/O,最终都会调用 submit_bio 提交块设备 I/O 请求。

对于每一种块设备,都有一个 gendisk 表示这个设备,它有一个请求队列,这个队列是一系列的 request 对象。每个 request 对象里面包含多个 BIO 对象,指向 page cache。所谓的写入块设备,I/O 就是将 page cache 里面的数据写入硬盘。

对于请求队列来讲,还有两个函数,一个函数叫 make_request_fn 函数,用于将请求放入队列。submit_bio 会调用 generic_make_request,然后调用这个函数。

另一个函数往往在设备驱动程序里实现,我们叫 request_fn 函数,它用于从队列里面取出请求来,写入外部设备。

在这里插入图片描述
至此,整个写入文件的过程才完整结束。这真是个复杂的过程,涉及系统调用、内存管理、文件系统和输入输出。这足以说明,操作系统真的是一个非常复杂的体系,环环相扣,需要分层次层层展开来学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值