【驱动】块设备驱动(四)-块设备驱动层

前言

块设备驱动程序是Liux块子系统中的最底层组件。它们从IO调度程序中获得请求,然后按要求处理这些请求。一个块设备驱动程序可能处理几个块设备。例如,IDE设备驱动程序可以处理几个IDE磁盘,其中的每个都是一个单独的块设备。而且,每个磁盘通常是被分区的,每个分区又可以被看作是一个逻辑块设备。

核心数据结构

block_device

block_device结构代表了内核中的一个块设备。它可以表示整个磁盘或一个特定的分区。当这个结构代表一个分区时,它的bd_contains成员指向包含这个分区的设备,bd_part成员指向设备的分区结构。当这个结构代表一个块设备时,bd_disk成员指向设备的gendisk结构。

struct block_device {
	dev_t			bd_dev;  /* not a kdev_t - it's a search key */
	int			bd_openers;
	struct inode *		bd_inode;	/* will die */
	struct super_block *	bd_super;
	struct mutex		bd_mutex;	/* open/close mutex */
	struct list_head	bd_inodes;
	void *			bd_claiming;
	void *			bd_holder;
	int			bd_holders;
	bool			bd_write_holder;
#ifdef CONFIG_SYSFS
	struct list_head	bd_holder_disks;
#endif
	struct block_device *	bd_contains;
	unsigned		bd_block_size;
	struct hd_struct *	bd_part;
	/* number of times partitions within this device have been opened. */
	unsigned		bd_part_count;
	int			bd_invalidated;
	struct gendisk *	bd_disk;
	struct request_queue *  bd_queue;
	struct list_head	bd_list;
	/*
	 * Private data.  You must have bd_claim'ed the block_device
	 * to use this.  NOTE:  bd_claim allows an owner to claim
	 * the same device multiple times, the owner must take special
	 * care to not mess up bd_private for that case.
	 */
	unsigned long		bd_private;

	/* The counter of freeze processes */
	int			bd_fsfreeze_count;
	/* Mutex for freeze */
	struct mutex		bd_fsfreeze_mutex;
};
  • bd_dev:设备号,用作搜索键而不是kdev_t类型。
  • bd_openers:设备打开者的计数器。
  • bd_inode:指向相关的inode结构体的指针(已弃用)。
  • bd_super:指向相关的super_block结构体的指针。
  • bd_mutex:用于保护设备的开启和关闭操作的互斥锁。
  • bd_inodes:一个链表头,用于保存使用此设备的inode结构体。
  • bd_claiming:指向声明该设备的指针。
  • bd_holder:指向持有该设备的指针。
  • bd_holders:持有该设备的计数器。
  • bd_write_holder:表示设备是否由写操作的持有者。
  • bd_holder_disks:一个链表头,用于保存持有该设备的磁盘。
  • bd_contains:指向包含该设备的block_device结构体的指针。
  • bd_block_size:设备的块大小。
  • bd_part:指向相关的hd_struct结构体的指针。
  • bd_part_count:该设备的分区被打开的次数。
  • bd_invalidated:表示设备是否已失效。
  • bd_disk:指向相关的gendisk结构体的指针。
  • bd_queue:指向相关的request_queue结构体的指针。
  • bd_list:一个链表头,用于将该设备添加到全局设备列表中。
  • bd_private:私有数据。使用该数据之前,必须使用bd_claim声明对设备的所有权。需要注意的是,bd_claim允许一个所有者多次声明相同的设备,所有者必须特别注意不要破坏针对该情况的bd_private
  • bd_fsfreeze_count:冻结进程的计数器。
  • bd_fsfreeze_mutex:用于冻结操作的互斥锁。

register_blkdev

register_blkdev函数用于注册块设备,并将主设备号与设备名进行映射。它使用一个链表数组major_names来存储不同主设备号的映射关系,并使用互斥锁来确保并发访问的安全性。通过动态分配内存和链表操作,函数可以有效地管理和分配主设备号,并提供适当的错误处理。

int register_blkdev(unsigned int major, const char *name)
{
	struct blk_major_name **n, *p;
	int index, ret = 0;

	mutex_lock(&block_class_lock);

	/* temporary */
	if (major == 0) {
		for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) {
			if (major_names[index] == NULL)
				break;
		}

		if (index == 0) {
			printk("register_blkdev: failed to get major for %s\n",
			       name);
			ret = -EBUSY;
			goto out;
		}
		major = index;
		ret = major;
	}

	p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);
	if (p == NULL) {
		ret = -ENOMEM;
		goto out;
	}

	p->major = major;
	strlcpy(p->name, name, sizeof(p->name));
	p->next = NULL;
	index = major_to_index(major);

	for (n = &major_names[index]; *n; n = &(*n)->next) {
		if ((*n)->major == major)
			break;
	}
	if (!*n)
		*n = p;
	else
		ret = -EBUSY;

	if (ret < 0) {
		printk("register_blkdev: cannot get major %d for %s\n",
		       major, name);
		kfree(p);
	}
out:
	mutex_unlock(&block_class_lock);
	return ret;
}
  1. 使用互斥锁block_class_lock来锁定对块设备类的访问。这是为了确保在注册块设备时不会发生并发访问的问题。
  2. 如果传入的主设备号major为0,表示请求动态分配主设备号。
  3. 在上述条件块中,通过遍历major_names数组找到一个可用的主设备号。遍历是从数组末尾开始,找到第一个为NULL的元素位置,表示该位置的主设备号可用。将其赋值给major并将其作为返回值,表示成功分配的主设备号。
  4. 使用kmalloc函数动态分配一个struct blk_major_name结构体的内存,该结构体用于存储主设备号和设备名的映射关系。
  5. 将设备名name复制到struct blk_major_name结构体的name成员中。这里使用了strlcpy函数,确保不会发生缓冲区溢出。
  6. 将主设备号major转换为索引值index,用于在major_names数组中定位对应的链表。
  7. 在对应的链表中遍历,查找是否已经存在相同的主设备号major
  8. 如果找到了相同的主设备号,表示已经被占用,将返回值ret设置为-EBUSY表示注册失败。否则,将新分配的struct blk_major_name结构体添加到链表的末尾。
  9. 如果ret小于0(即注册失败),则打印错误消息并释放分配的内存。
  10. 释放对块设备类的互斥锁,允许其他线程访问块设备类。
  11. 返回ret作为函数的结果,表示注册块设备的状态。如果成功,返回的是分配的主设备号;如果失败,返回的是相应的错误码。

blkdev_open

对于块设备文件的操作,通过block_dev伪文件系统来完成,open操作定义的函数为blkdev_open()

blkdev_open的主要任务有两个:1.获取设备的block_device信息。2.从gendisk中读取相关信息保存到block_device,同时建立数据结构之间的联系。

static int blkdev_open(struct inode * inode, struct file * filp)
{
	struct block_device *bdev;

	/*
	 * Preserve backwards compatibility and allow large file access
	 * even if userspace doesn't ask for it explicitly. Some mkfs
	 * binary needs it. We might want to drop this workaround
	 * during an unstable branch.
	 */
	filp->f_flags |= O_LARGEFILE;

	if (filp->f_flags & O_NDELAY)
		filp->f_mode |= FMODE_NDELAY;
	if (filp->f_flags & O_EXCL)
		filp->f_mode |= FMODE_EXCL;
	if ((filp->f_flags & O_ACCMODE) == 3)
		filp->f_mode |= FMODE_WRITE_IOCTL;

	bdev = bd_acquire(inode);
	if (bdev == NULL)
		return -ENOMEM;

	filp->f_mapping = bdev->bd_inode->i_mapping;

	return blkdev_get(bdev, filp->f_mode, filp);
}
  1. 设置文件标志O_LARGEFILE,以支持对大文件的访问。这是为了保持向后兼容性,在不显式要求的情况下允许对大文件的访问。某些mkfs二进制文件可能需要这个设置。
  2. 检查文件标志O_NDELAY是否被设置。如果是,则设置文件模式FMODE_NDELAY,表示以非阻塞模式打开文件。
  3. 检查文件标志O_EXCL是否被设置。如果是,则设置文件模式FMODE_EXCL,表示以独占模式打开文件。
  4. 检查文件访问模式是否为O_RDWR,即读写模式。如果是,则设置文件模式FMODE_WRITE_IOCTL,表示允许通过IOCTL进行写操作。
  5. 根据给定的inode获取对应的块设备block_devicebd_acquire函数负责获取块设备的引用计数,确保块设备在文件打开期间不会被卸载。
  6. 检查获取块设备是否失败。如果获取失败,则返回错误码-ENOMEM,表示内存不足。
  7. 将文件的映射关系设置为块设备的i_mapping。这是为了确保文件系统能够正确地将读写操作转发到块设备。
  8. 调用blkdev_get函数以确保块设备的引用计数递增,并执行必要的打开操作。函数将返回打开操作的结果。

blkdev_read_iter

blkdev_read_iter函数用于在块设备上执行读取操作。它首先获取块设备的大小和当前位置信息,然后检查是否已经达到或超出了块设备的大小。根据剩余可读取的字节数,调整目标迭代器的长度。最后,调用通用的文件读取函数generic_file_read_iter进行实际的读取操作,并返回读取操作的结果。

我们具体分析下generic_file_read_itergeneric_file_read_iter函数中的这部分代码用于执行通用文件的读取操作。它根据iocb中的标志判断是否进行直接IO读取,然后根据情况调用相应的函数进行读取操作,并更新位置信息和迭代器。在特定条件下,它会跳过剩余的读取操作,并更新文件的访问时间。最后,它返回读取操作的结果。

ssize_t blkdev_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
	struct file *file = iocb->ki_filp;
	struct inode *bd_inode = file->f_mapping->host;
	loff_t size = i_size_read(bd_inode);
	loff_t pos = iocb->ki_pos;

	if (pos >= size)
		return 0;

	size -= pos;
	iov_iter_truncate(to, size);
	return generic_file_read_iter(iocb, to);
}

ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
	struct file *file = iocb->ki_filp;
	ssize_t retval = 0;
	loff_t *ppos = &iocb->ki_pos;
	loff_t pos = *ppos;

	if (iocb->ki_flags & IOCB_DIRECT) {
		struct address_space *mapping = file->f_mapping;
		struct inode *inode = mapping->host;
		size_t count = iov_iter_count(iter);
		loff_t size;

		if (!count)
			goto out; /* skip atime */
		size = i_size_read(inode);
		retval = filemap_write_and_wait_range(mapping, pos,
					pos + count - 1);
		if (!retval) {
			struct iov_iter data = *iter;
			retval = mapping->a_ops->direct_IO(iocb, &data, pos);
		}

		if (retval > 0) {
			*ppos = pos + retval;
			iov_iter_advance(iter, retval);
		}

		/*
		 * Btrfs can have a short DIO read if we encounter
		 * compressed extents, so if there was an error, or if
		 * we've already read everything we wanted to, or if
		 * there was a short read because we hit EOF, go ahead
		 * and return.  Otherwise fallthrough to buffered io for
		 * the rest of the read.  Buffered reads will not work for
		 * DAX files, so don't bother trying.
		 */
		if (retval < 0 || !iov_iter_count(iter) || *ppos >= size ||
		    IS_DAX(inode)) {
			file_accessed(file);
			goto out;
		}
	}

	retval = do_generic_file_read(file, ppos, iter, retval);
out:
	return retval;
}
  1. 获取与iocbkiocb结构体)相关联的文件对象file
  2. loff_t *ppos = &iocb->ki_pos;:获取指向iocb中位置信息的指针ppos
  3. loff_t pos = *ppos;:将当前位置信息保存到变量pos中。
  4. if (iocb->ki_flags & IOCB_DIRECT) { ... }:检查iocb中的标志IOCB_DIRECT是否被设置。如果设置了该标志,表示执行直接IO(Direct I/O)操作。
  5. 在直接IO操作的条件块中,首先获取文件的地址空间mapping和对应的索引节点inode。然后获取读取操作的字节数count。如果字节数为0,则跳过访问时间更新(atime)的步骤。
  6. 调用filemap_write_and_wait_range函数,确保在进行直接IO读取之前,将文件中的数据写回存储设备并等待完成。该函数将返回写入操作的结果。
  7. 如果写入操作成功(retval为0),则复制iter到新的data迭代器,并调用文件地址空间的direct_IO操作进行直接IO读取。该函数将返回读取操作的结果。
  8. 如果读取操作返回的字节数retval大于0,表示读取成功,更新当前位置ppositer的偏移,并继续进行后续的读取操作。
  9. 接下来的条件块用于处理特定情况下的直接IO读取。例如,如果读取出现错误(retval小于0),或者已经读取完所有数据(iov_iter_count(iter)为0),或者已经读取到文件末尾(*ppos >= size),或者文件是DAX文件(IS_DAX(inode)),则跳过剩余的读取操作,并更新文件的访问时间。
  10. 如果上述条件均不满足,则调用do_generic_file_read函数执行通用文件读取操作。该函数将处理剩余的读取操作,并更新位置信息和迭代器。
  11. 最后,跳转到标签out处,并返回读取操作的结果retval

blkdev_write_iter

blkdev_write_iter函数用于在块设备上执行写入操作。它首先检查块设备是否为只读模式,以及输入迭代器中是否有数据可写入。然后,根据当前位置和块设备的大小调整输入迭代器的长度。接着,通过调用通用的文件写入函数进行实际的写入操作,并返回写入操作的结果。如果写入成功,还会进行同步写入操作,确保数据真正写入块设备。最后,返回写入操作的结果。

ssize_t blkdev_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *file = iocb->ki_filp;
	struct inode *bd_inode = file->f_mapping->host;
	loff_t size = i_size_read(bd_inode);
	struct blk_plug plug;
	ssize_t ret;

	if (bdev_read_only(I_BDEV(bd_inode)))
		return -EPERM;

	if (!iov_iter_count(from))
		return 0;

	if (iocb->ki_pos >= size)
		return -ENOSPC;

	iov_iter_truncate(from, size - iocb->ki_pos);

	blk_start_plug(&plug);
	ret = __generic_file_write_iter(iocb, from);
	if (ret > 0) {
		ssize_t err;
		err = generic_write_sync(file, iocb->ki_pos - ret, ret);
		if (err < 0)
			ret = err;
	}
	blk_finish_plug(&plug);
	return ret;
}
  1. struct file *file = iocb->ki_filp;:获取与iocbkiocb结构体)相关联的文件对象file

  2. struct inode *bd_inode = file->f_mapping->host;:获取文件对象对应的块设备的索引节点bd_inode

  3. loff_t size = i_size_read(bd_inode);:获取块设备的大小(文件大小)。

  4. if (bdev_read_only(I_BDEV(bd_inode))) return -EPERM;:检查块设备是否为只读模式。如果是,直接返回错误码-EPERM,表示无法进行写入操作。

  5. if (!iov_iter_count(from)) return 0;:检查输入迭代器from中的字节数是否为0。如果为0,表示没有数据可写入,直接返回0,表示写入操作已完成。

  6. if (iocb->ki_pos >= size) return -ENOSPC;:检查当前位置是否已经达到或超出了块设备的大小。如果是,返回错误码-ENOSPC,表示空间不足,无法进行写入操作。

  7. iov_iter_truncate(from, size - iocb->ki_pos);:根据当前位置和块设备的大小,调整输入迭代器from的长度,确保只写入剩余可写入的字节数。

  8. blk_start_plug(&plug);:启动块设备的批量操作,将后续的写入操作收集到一个批处理中。

  9. ret = __generic_file_write_iter(iocb, from);:调用通用的文件写入函数__generic_file_write_iter,执行写入操作。将iocb和输入迭代器from传递给该函数进行写入操作,并返回写入操作的结果。

  10. 如果写入操作返回的字节数ret大于0,表示写入成功,执行以下代码块:

    a. ssize_t err;:定义变量err用于保存同步写入操作的结果。

    b. err = generic_write_sync(file, iocb->ki_pos - ret, ret);:调用通用的写入同步函数generic_write_sync,将写入的起始位置和字节数传递给该函数进行同步写入操作,并将结果保存到err中。

    c. if (err < 0) ret = err;:如果同步写入操作返回的结果小于0,表示出现错误,将错误码保存到ret中。

  11. blk_finish_plug(&plug);:结束块设备的批量操作。

  12. 返回写入操作的结果ret

__generic_file_write_iter

__generic_file_write_iter函数用于实际执行文件写入操作。它会进行一系列的检查和操作,包括移除特权标志、更新修改时间戳和调用适当的子函数来处理直接IO或标准缓冲区写入。需要注意的是,对于O_SYNC写入,该函数不会处理数据同步的问题,需要调用者自行处理。这主要是因为希望避免在持有i_mutex时进行数据同步操作。

ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *file = iocb->ki_filp;
	struct address_space * mapping = file->f_mapping;
	struct inode 	*inode = mapping->host;
	ssize_t		written = 0;
	ssize_t		err;
	ssize_t		status;

	/* We can write back this queue in page reclaim */
	current->backing_dev_info = inode_to_bdi(inode);
	err = file_remove_privs(file);
	if (err)
		goto out;

	err = file_update_time(file);
	if (err)
		goto out;

	if (iocb->ki_flags & IOCB_DIRECT) {
		loff_t pos, endbyte;

		written = generic_file_direct_write(iocb, from, iocb->ki_pos);
		/*
		 * If the write stopped short of completing, fall back to
		 * buffered writes.  Some filesystems do this for writes to
		 * holes, for example.  For DAX files, a buffered write will
		 * not succeed (even if it did, DAX does not handle dirty
		 * page-cache pages correctly).
		 */
		if (written < 0 || !iov_iter_count(from) || IS_DAX(inode))
			goto out;

		status = generic_perform_write(file, from, pos = iocb->ki_pos);
		/*
		 * If generic_perform_write() returned a synchronous error
		 * then we want to return the number of bytes which were
		 * direct-written, or the error code if that was zero.  Note
		 * that this differs from normal direct-io semantics, which
		 * will return -EFOO even if some bytes were written.
		 */
		if (unlikely(status < 0)) {
			err = status;
			goto out;
		}
		/*
		 * We need to ensure that the page cache pages are written to
		 * disk and invalidated to preserve the expected O_DIRECT
		 * semantics.
		 */
		endbyte = pos + status - 1;
		err = filemap_write_and_wait_range(mapping, pos, endbyte);
		if (err == 0) {
			iocb->ki_pos = endbyte + 1;
			written += status;
			invalidate_mapping_pages(mapping,
						 pos >> PAGE_CACHE_SHIFT,
						 endbyte >> PAGE_CACHE_SHIFT);
		} else {
			/*
			 * We don't know how much we wrote, so just return
			 * the number of bytes which were direct-written
			 */
		}
	} else {
		written = generic_perform_write(file, from, iocb->ki_pos);
		if (likely(written > 0))
			iocb->ki_pos += written;
	}
out:
	current->backing_dev_info = NULL;
	return written ? written : err;
}
  1. struct file *file = iocb->ki_filp;:获取与iocbkiocb结构体)相关联的文件对象file

  2. struct address_space *mapping = file->f_mapping;:获取文件对象对应的地址空间mapping

  3. struct inode *inode = mapping->host;:获取地址空间对应的索引节点inode

  4. current->backing_dev_info = inode_to_bdi(inode);:将当前进程的backing_dev_info字段设置为inode对应的块设备信息。

  5. err = file_remove_privs(file);:移除文件对象的特权标志。这是为了确保写入操作不会以特权身份执行。

  6. err = file_update_time(file);:更新文件的修改时间戳。

  7. 如果iocb->ki_flags中包含IOCB_DIRECT标志,表示执行直接IO(direct I/O),则执行以下代码块:

    a. 定义变量posendbyte,用于记录写入的起始位置和结束位置。

    b. written = generic_file_direct_write(iocb, from, iocb->ki_pos);:调用通用的直接写入函数generic_file_direct_write,执行直接IO操作,并返回已写入的字节数。

    c. 如果写入操作未完成(written < 0)或输入迭代器中没有数据可写入(!iov_iter_count(from)),或者inode是DAX文件(IS_DAX(inode)为真),则跳转到out标签。

    d. status = generic_perform_write(file, from, pos = iocb->ki_pos);:调用通用的执行写入操作的函数generic_perform_write,执行标准缓冲区写入操作,并将写入的起始位置保存到pos中,返回写入的状态码。

    e. 如果status小于0,表示写入操作返回了同步错误,将错误码保存到err中,并跳转到out标签。

    f. 计算写入操作的结束位置endbyte = pos + status - 1

    g. err = filemap_write_and_wait_range(mapping, pos, endbyte);:将页高速缓存中的数据写入磁盘,并等待写入操作完成。

    h. 如果err为0,表示写入操作成功,更新iocb的位置iocb->ki_posendbyte + 1,累加已写入的字节数到written中,并使映射页无效。

    i. 如果err不为0,表示写入操作出现错误,由于无法确定实际写入了多少字节,因此不做处理。

  8. 如果不是直接IO操作,则执行以下代码块:

    a. written = generic_perform_write(file, from, iocb->ki_pos);:调用通用的执行写入操作的函数generic_perform_write,执行标准缓冲区写入操作,并返回已写入的字节数。

    b. 如果已写入的字节数大于0,则更新iocb的位置iocb->ki_pos为当前位置加上已写入的字节数。

  9. out:标签处的代码用于清理操作,将current->backing_dev_info字段重置为NULL

  10. 返回已写入的字节数written,如果没有写入任何数据,则返回错误码err

generic_write_sync

generic_write_sync函数用于根据文件的打开标志和文件的映射索引节点的同步属性,判断是否需要执行同步写入操作。如果满足同步条件,则调用vfs_fsync_range函数执行同步写入操作。

static inline int generic_write_sync(struct file *file, loff_t pos, loff_t count)
{
	if (!(file->f_flags & O_DSYNC) && !IS_SYNC(file->f_mapping->host))
		return 0;
	return vfs_fsync_range(file, pos, pos + count - 1,
			       (file->f_flags & __O_SYNC) ? 0 : 1);
}
vfs_fsync_range

vfs_fsync_range函数用于将指定文件的指定范围内的数据和元数据同步到磁盘。它会检查文件是否定义了fsync函数,并根据参数决定是否写入元数据。在写入元数据之前,它会清除索引节点状态中的相应标志位,并将索引节点标记为已修改。最后,它调用文件的fsync函数来执行实际的同步操作。

int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
{
	struct inode *inode = file->f_mapping->host;

	if (!file->f_op->fsync)
		return -EINVAL;
	if (!datasync && (inode->i_state & I_DIRTY_TIME)) {
		spin_lock(&inode->i_lock);
		inode->i_state &= ~I_DIRTY_TIME;
		spin_unlock(&inode->i_lock);
		mark_inode_dirty_sync(inode);
	}
	return file->f_op->fsync(file, start, end, datasync);
}
  1. struct inode *inode = file->f_mapping->host;:获取文件file对应的索引节点inode
  2. if (!file->f_op->fsync):检查文件的文件操作函数指针f_op中是否定义了fsync函数。如果未定义,则返回错误码-EINVAL
  3. if (!datasync && (inode->i_state & I_DIRTY_TIME)):如果不是仅执行数据同步,并且索引节点的状态中标志位I_DIRTY_TIME被设置。
  4. spin_lock(&inode->i_lock);:获取索引节点的自旋锁,用于保护对索引节点状态的修改。
  5. inode->i_state &= ~I_DIRTY_TIME;:清除索引节点状态中的I_DIRTY_TIME标志位,表示元数据已被写回。
  6. spin_unlock(&inode->i_lock);:释放索引节点的自旋锁。
  7. mark_inode_dirty_sync(inode);:将索引节点标记为已修改,需要同步到磁盘。
  8. return file->f_op->fsync(file, start, end, datasync);:调用文件的fsync函数,将数据和元数据同步到磁盘。该函数由文件系统提供,并提供了特定文件系统的实现。
posted @ 2024-02-05 13:50  学习,积累,成长  阅读(148)  评论(0编辑  收藏  举报