【驱动】块设备驱动(二)-通用块层
前言
通用块层是一个内核组件,处理来自系统其他组件发出的块设备请求。换句话说,通用块层包含了块设备操作的一些通用函数和数据结构,如通用磁盘结构gendisk
,请求队列结构request_queue
、请求结构request
、块设备I/O操作结构bio和块设备操作结构block_device_operations
等。
关键数据结构
buffer_head
内存中一个page所包含的磁盘块在物理上不一定是相邻的。那么page中不同的磁盘块怎么管理呢?这里就涉及到了buffer_head
结构。每个buffer_head
管理的单元是内存页page中对应的一个物理块(对于block大小为1k的情况下,一个page对应磁盘上的4个block,而每个buffer_head
对应1个磁盘block),也就是说buffer_head
管理单元是 “页”的一个子集。
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* circular list of page's buffers */
struct page *b_page; /* the page this bh is mapped to */
sector_t b_blocknr; /* start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */
struct block_device *b_bdev;
bh_end_io_t *b_end_io; /* I/O completion */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* associated with another mapping */
struct address_space *b_assoc_map; /* mapping this buffer is
associated with */
atomic_t b_count; /* users using this buffer_head */
};
unsigned long b_state
:位图,用于表示缓冲区的状态信息。例如是否是脏数据、是否已经写回磁盘等。
struct buffer_head *b_this_page
:用于构建一个循环链表,将同一页(page)上的多个缓冲区头连接在一起。通过该指针,可以遍历同一页上的所有缓冲区。
struct page *b_page
:指向映射到的页(page)的指针。缓冲区头与页相关联,表示该缓冲区头所属的页。
sector_t b_blocknr
:起始块号(block number),表示缓冲区映射的起始位置。
size_t b_size
:表示映射大小(mapping size)的变量,用于表示缓冲区映射的大小。
char *b_data
:指向页中数据的指针。它指向页中存储的缓冲区数据的起始位置。
struct block_device *b_bdev
:指向块设备(block device)的指针,表示缓冲区头所属的块设备。
bh_end_io_t *b_end_io
:一个指向I/O完成回调函数的指针。当与缓冲区头相关联的I/O操作完成时,内核将调用此回调函数。
void *b_private
:保留字段,用于存储与缓冲区头相关的私有数据。通常在与回调函数(b_end_io)一起使用,用于传递额外的参数或上下文信息。
struct list_head b_assoc_buffers
:链表头,当前缓冲区头与其他映射相关联的缓冲区头连接在一起。通过该链表,可以遍历与当前缓冲区头相关联的其他缓冲区头。
struct address_space *b_assoc_map
:指向地址空间的指针,表示当前缓冲区头所属的地址空间。地址空间用于管理文件系统中的缓冲区。
atomic_t b_count
:一个原子变量,表示当前正在使用该缓冲区头的用户数。在多线程环境下,通过原子操作可以对该计数进行增减操作,用于跟踪缓冲区头的使用情况。
ll_rw_block
通用块层提供了 ll_rw_block()
函数对逻辑块进行读写操作,根据给定的读写操作类型和缓冲区的状态,它会对指定的缓冲区执行相应的I/O操作,并处理相关的完成动作。
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
{
int i;
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
if (!trylock_buffer(bh))
continue;
if (rw == WRITE) {
if (test_clear_buffer_dirty(bh)) {
bh->b_end_io = end_buffer_write_sync;
get_bh(bh);
submit_bh(WRITE, bh);
continue;
}
} else {
if (!buffer_uptodate(bh)) {
bh->b_end_io = end_buffer_read_sync;
get_bh(bh);
submit_bh(rw, bh);
continue;
}
}
unlock_buffer(bh);
}
}
ll_rw_block
接受三个参数: rw
表示读写操作类型(READ、WRITE或READA),nr
表示传入的struct buffer_head
指针数组的长度,bhs
是一个指向struct buffer_head
指针数组的指针。
函数的作用是对传入的struct buffer_head
数组进行I/O操作,可以是读取(READ)或写入(WRITE)。第三个操作类型READA在ll_rw_block
函数中调用了generic_make_request()
函数。
在执行操作之前,该函数会丢弃那些无法获取锁定(BH_Lock状态位)的缓冲区,对于写请求,丢弃那些看起来已经是干净的缓冲区;对于读请求,丢弃那些看起来已经是最新的缓冲区。此外,对于被处理为写请求的缓冲区,该函数会标记为已经干净(缓冲区在解锁之前不会被缓冲区缓存认为是干净的)。
ll_rw_block
函数将b_end_io
设置为简单的完成处理程序,该处理程序会将缓冲区标记为最新(如果适用),解锁缓冲区并唤醒任何等待的进程。
struct bio
通用块层的核心数据结构是struct bio
,它描述了块设备的IO操作。
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned int bi_flags; /* status, command, etc */
unsigned short bi_write_hint;
int bi_error;
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
unsigned short bi_vcnt; /* how many bio_vec's */
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
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_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};
bi_next
: 指向下一个bio
结构体的指针,用于构建请求队列。bi_bdev
: 指向块设备的指针,表示该bio
请求所涉及的块设备。bi_flags
: 用于存储bio
的状态、命令等标志位。bi_write_hint
: 提示bio
是写操作的提示标志位。bi_error
: 存储bio
操作过程中的错误代码。bi_rw
: 存储bio
的读写操作类型,低位表示 READ/WRITE,高位表示优先级。bi_iter
: 用于迭代bio
的bio_vec
结构体的迭代器。bi_phys_segments
: 在物理地址合并后,bio
中的段(segment)数量。bi_seg_front_size
和bi_seg_back_size
: 用于跟踪合并后的第一个和最后一个可合并段的大小。__bi_remaining
: 原子计数器,表示剩余未处理的bio
数量。bi_end_io
: 指向bio
结束时调用的回调函数的指针。bi_private
: 指向bio
的私有数据的指针。bi_ioc
和bi_css
: 可选的io_context
和cgroup_subsys_state
,用于关联bio
。bi_integrity
: 如果启用了数据完整性检查,指向bio_integrity_payload
结构体的指针。bi_vcnt
:bio_vec
的数量,表示bio
中的向量个数。bi_max_vecs
:bio
可以容纳的最大bio_vec
的数量。__bi_cnt
: 原子计数器,表示对bio
的引用计数。bi_io_vec
: 实际的bio_vec
列表。bi_pool
: 指向bio_set
结构体的指针,表示与该bio
相关的资源池。bi_inline_vecs
: 在bio
的末尾可以内联一些向量,以避免为少量bio_vec
进行双重分配。
bio_vec
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
bv_page
:指向段的页框中页描述符的指针bv_len
; 段的长度,以字节为单位bv_offset
:页框中段数据的偏移量
顾名思义,bio_vec 就是一个 bio 的数据容器,专门用来保存 bio 的数据,当然他是这个 bio 大集体的一个最小项,刚刚说了 bio 是通用块层的最小集,而这个 bio_vec
则是组成 bio 数据的最小单位,他包含了一块数据所在的页,这块数据所在的页内偏移以及长度,通过这些信息就可以很清晰的描述数据具体位于什么位置,通过对这些数据的整合,可以将他们添加到 SGL(散列表) 中直接发送给后端硬件设备。
bvec_iter
寻遍整个 bio 发现居然没有携带需要下盘的扇区编号以及当前 bio 的大小,这个很尴尬,但确实如此,相当于 bio 作为一辆汽车,他携带了货物但是没告诉他目的地这不是完蛋了吗?不是,真正的目的地保存在这个 bvec_iter
中,作为一个迭代器,自然他的使命就是用来遍历 bvec,也就是 bio 数据区。那么他好比就是这辆 bio 汽车的货物分拣员,自然我的目的地不必贴到车上,直接告诉分拣员也是可以的,因为后面的事情可不是这辆汽车再做,而是分拣员需要逐个卸货的时候用。一起来看看迭代器长什么样?
struct bvec_iter {
sector_t bi_sector; /* device address in 512 byte sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};
有几个重点:
-
一个BIO所请求的数据在块设备中是连续的,对于不连续的数据块需要放到多个BIO中。
-
一个BIO所携带的数据大小是有上限的,该上限值由
bi_max_vecs
间接指定,超过上限的数据块必须放到多个BIO中。 -
使用
bio_for_each_segment
来遍历 bio_vec
BIO、bi_io_vec、page之间的关系
bio_alloc
bio_alloc
函数用于分配bio
结构,并返回指向分配的bio
结构的指针。bio_alloc
最终会调用到bio_alloc_bioset
函数。我们直接分析bio_alloc_bioset
。
struct bio *bio_alloc_bioset(gfp_t gfp_mask, int nr_iovecs, struct bio_set *bs)
{
gfp_t saved_gfp = gfp_mask;
unsigned front_pad;
unsigned inline_vecs;
unsigned long idx = BIO_POOL_NONE;
struct bio_vec *bvl = NULL;
struct bio *bio;
void *p;
if (!bs) {
if (nr_iovecs > UIO_MAXIOV)
return NULL;
p = kmalloc(sizeof(struct bio) +
nr_iovecs * sizeof(struct bio_vec),
gfp_mask);
front_pad = 0;
inline_vecs = nr_iovecs;
} else {
/* should not use nobvec bioset for nr_iovecs > 0 */
if (WARN_ON_ONCE(!bs->bvec_pool && nr_iovecs > 0))
return NULL;
/*
* generic_make_request() converts recursion to iteration; this
* means if we're running beneath it, any bios we allocate and
* submit will not be submitted (and thus freed) until after we
* return.
*
* This exposes us to a potential deadlock if we allocate
* multiple bios from the same bio_set() while running
* underneath generic_make_request(). If we were to allocate
* multiple bios (say a stacking block driver that was splitting
* bios), we would deadlock if we exhausted the mempool's
* reserve.
*
* We solve this, and guarantee forward progress, with a rescuer
* workqueue per bio_set. If we go to allocate and there are
* bios on current->bio_list, we first try the allocation
* without __GFP_DIRECT_RECLAIM; if that fails, we punt those
* bios we would be blocking to the rescuer workqueue before
* we retry with the original gfp_flags.
*/
if (current->bio_list &&
(!bio_list_empty(¤t->bio_list[0]) ||
!bio_list_empty(¤t->bio_list[1])))
gfp_mask &= ~__GFP_DIRECT_RECLAIM;
p = mempool_alloc(bs->bio_pool, gfp_mask);
if (!p && gfp_mask != saved_gfp) {
punt_bios_to_rescuer(bs);
gfp_mask = saved_gfp;
p = mempool_alloc(bs->bio_pool, gfp_mask);
}
front_pad = bs->front_pad;
inline_vecs = BIO_INLINE_VECS;
}
if (unlikely(!p))
return NULL;
bio = p + front_pad;
bio_init(bio);
if (nr_iovecs > inline_vecs) {
bvl = bvec_alloc(gfp_mask, nr_iovecs, &idx, bs->bvec_pool);
if (!bvl && gfp_mask != saved_gfp) {
punt_bios_to_rescuer(bs);
gfp_mask = saved_gfp;
bvl = bvec_alloc(gfp_mask, nr_iovecs, &idx, bs->bvec_pool);
}
if (unlikely(!bvl))
goto err_free;
bio_set_flag(bio, BIO_OWNS_VEC);
} else if (nr_iovecs) {
bvl = bio->bi_inline_vecs;
}
bio->bi_pool = bs;
bio->bi_flags |= idx << BIO_POOL_OFFSET;
bio->bi_max_vecs = nr_iovecs;
bio->bi_io_vec = bvl;
return bio;
err_free:
mempool_free(p, bs->bio_pool);
return NULL;
}
static inline struct bio *bio_alloc(gfp_t gfp_mask, unsigned int nr_iovecs)
{
return bio_alloc_bioset(gfp_mask, nr_iovecs, fs_bio_set);
}
首先,函数接受三个参数:gfp_mask
表示内存分配的标志,nr_iovecs
表示iovec
的数量,bs
是一个指向bio_set
结构的指针,用于指定分配bio
的bio_set
。
根据bs
是否为NULL
进行不同的分配方式。如果bs
为NULL
,则通过kmalloc
函数分配一块内存,大小为sizeof(struct bio) + nr_iovecs * sizeof(struct bio_vec)
。然后,设置front_pad
为0,inline_vecs
为nr_iovecs
的值。
如果bs
不为NULL
,则执行另一段代码。首先,检查bs->bvec_pool
是否为NULL
并且nr_iovecs
是否大于0。如果满足条件,则返回NULL
,表示分配失败。接下来,检查当前进程的bio_list
是否存在未完成的bio
请求,如果存在,则将gfp_mask
中的__GFP_DIRECT_RECLAIM
标志位清除,以避免在分配期间触发直接回收内存的行为。然后,使用mempool_alloc
函数从bs->bio_pool
中分配一块内存。如果分配失败,并且gfp_mask
与保存的gfp_mask
不相同,说明之前尝试分配时可能未释放内存,此时调用punt_bios_to_rescuer
函数将之前的bio
请求提交给救援工作队列,然后重新使用保存的gfp_mask
再次尝试分配内存。
接下来,根据分配的内存地址计算bio
的指针,并调用bio_init
函数对bio
进行初始化。
然后,根据nr_iovecs
的值进行不同的处理。如果nr_iovecs
大于inline_vecs
,则调用bvec_alloc
函数从bs->bvec_pool
中分配一块内存作为bio_vec
数组,并将分配的bio_vec
数组赋值给bvl
。如果分配失败,并且gfp_mask
与保存的gfp_mask
不相同,说明之前尝试分配时可能未释放内存,此时调用punt_bios_to_rescuer
函数将之前的bio
请求提交给救援工作队列,然后重新使用保存的gfp_mask
再次尝试分配内存。如果分配成功,则将bio
的BIO_OWNS_VEC
标志位置位,表示bio
拥有分配的bio_vec
数组。如果nr_iovecs
不为0,将分配的bio_vec
数组赋值给bio
的bi_io_vec
字段。
如果nr_iovecs
小于等于inline_vecs
且不为0,则将bio
的bi_inline_vecs
指针赋值给bvl
。
最后,设置bio
的一些字段,如bi_pool
、bi_flags
和bi_max_vecs
,然后返回指向分配的bio
结构的指针。
submit_bio
submit_bio
将bio请求到磁盘request
请求的转换(请求的合并和IO优化),并将request
请求挂入到磁盘请求的队列中,然后进行处理。
blk_qc_t submit_bio(int rw, struct bio *bio)
{
bio->bi_rw |= rw;
/*
* If it's a regular read/write or a barrier with data attached,
* go through the normal accounting stuff before submission.
*/
if (bio_has_data(bio)) {
unsigned int count;
if (unlikely(rw & REQ_WRITE_SAME))
count = bdev_logical_block_size(bio->bi_bdev) >> 9;
else
count = bio_sectors(bio);
if (rw & WRITE) {
count_vm_events(PGPGOUT, count);
} else {
task_io_account_read(bio->bi_iter.bi_size);
count_vm_events(PGPGIN, count);
}
if (unlikely(block_dump)) {
char b[BDEVNAME_SIZE];
printk(KERN_DEBUG "%s(%d): %s block %Lu on %s (%u sectors)\n",
current->comm, task_pid_nr(current),
(rw & WRITE) ? "WRITE" : "READ",
(unsigned long long)bio->bi_iter.bi_sector,
bdevname(bio->bi_bdev, b),
count);
}
}
return generic_make_request(bio);
}
-
bio->bi_rw |= rw;
将传入的读/写标志rw
合并到bio
结构体的bi_rw
字段中。这是为了将读写标志与bio
中已有的标志进行合并。 -
if (bio_has_data(bio))
条件判断检查bio
结构体中是否有数据。如果有数据,则执行下面的代码块。 -
根据不同的条件,计算数据块的数量
count
。-
如果
rw
参数包含REQ_WRITE_SAME
标志位,表示写入相同数据的请求,则将count
设置为块设备逻辑块大小(通过bdev_logical_block_size
函数获取)除以 512 的结果。 -
否则,将
count
设置为bio
结构体中的扇区数(通过bio_sectors
函数获取)。
-
-
根据读写类型进行统计和日志记录:
-
如果
rw
参数包含WRITE
标志位,表示写操作,则调用count_vm_events
函数统计页面输出(PGPGOUT
)事件的发生次数,并使用数据块的数量count
进行计数。 -
否则,调用
task_io_account_read
函数将读取操作的字节数记录到当前任务的 I/O 统计中,并调用count_vm_events
函数统计页面输入(PGPGIN
)事件的发生次数,并使用数据块的数量count
进行计数。
-
-
如果启用了块转储(
block_dump
)功能,则通过printk
函数在内核日志中打印有关当前进程、进程ID、读写类型、扇区号、块设备名称和数据块数量等信息。 -
最后,调用
generic_make_request
函数将bio
提交给块设备层处理,执行实际的 I/O 操作。
struct gendisk
linux 内核使用 gendisk
结构体来描述一个磁盘设备,定义在 include/linux/genhd.h
中
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
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);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
struct disk_part_tbl __rcu *part_tbl;
struct hd_struct part0;
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
/* Flag of rockchip specific disk: eMMC/eSD, NVMe, etc. */
bool is_rk_disk;
int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct kobject integrity_kobj;
#endif /* CONFIG_BLK_DEV_INTEGRITY */
int node_id;
};
major
:驱动程序的主设备号。first_minor
:磁盘的第一个次设备号。minors
:最大的次设备号数量,对于不能分区的磁盘来说,该值通常为1。disk_name
:主驱动程序的名称。devnode
:函数指针,用于生成磁盘节点的路径名。events
:支持的事件类型。async_events
:异步事件类型,是所有事件类型的子集。part_tbl
:指向分区表的指针数组,通过partno
索引访问分区。受匹配的块设备锁保护,但状态和其他非关键访问使用 RCU(Read-Copy Update)访问。应始终通过辅助函数访问。part0
:表示磁盘的第一个分区。fops
:指向block_device_operations
结构体的指针,表示与磁盘相关的块设备操作。queue
:指向request_queue
结构体的指针,表示与磁盘关联的请求队列。private_data
:指向磁盘的私有数据的指针。is_rk_disk
:表示是否为 Rockchip 特定的磁盘,如 eMMC/eSD、NVMe 等。flags
:磁盘的标志位。driverfs_dev
:指向设备的指针,用于 driverfs(一种用于设备驱动程序的虚拟文件系统)。slave_dir
:指向从设备目录的指针。random
:指向随机定时器状态的指针。sync_io
:用于 RAID(冗余磁盘阵列)的同步 I/O。ev
:指向磁盘事件的指针。integrity_kobj
:用于数据完整性的内核对象。node_id
:节点 ID。
hd_struct
如果将一个磁盘分成了几个分区,那么其分区表保存在hd_struct
结构的数组中,该数组的地址存放在gendisk
对象的part字段中。通过磁盘内分区的相对索引对该数组进行索引。
struct hd_struct {
sector_t start_sect;
/*
* nr_sects is protected by sequence counter. One might extend a
* partition while IO is happening to it and update of nr_sects
* can be non-atomic on 32bit machines with 64bit sector_t.
*/
sector_t nr_sects;
seqcount_t nr_sects_seq;
sector_t alignment_offset;
unsigned int discard_alignment;
struct device __dev;
struct kobject *holder_dir;
int policy, partno;
struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
int make_it_fail;
#endif
unsigned long stamp;
atomic_t in_flight[2];
#ifdef CONFIG_SMP
struct disk_stats __percpu *dkstats;
#else
struct disk_stats dkstats;
#endif
struct percpu_ref ref;
struct rcu_head rcu_head;
};
start_sect
:分区的起始扇区。nr_sects
:分区的扇区数。在 32 位机器上,更新nr_sects
可能不是原子操作,因此受到序列计数器的保护。nr_sects_seq
:扇区数的序列计数器。alignment_offset
:对齐偏移量,用于指示分区的对齐位置。discard_alignment
:丢弃对齐的大小。__dev
:Linux 设备结构体,表示与分区关联的设备。holder_dir
:指向持有者目录的指针。policy
:分区策略。partno
:分区号。info
:指向分区元信息的指针。make_it_fail
:用于配置失败的请求。stamp
:时间戳。in_flight
:用于跟踪处理中的请求的原子计数器数组。dkstats
:磁盘统计信息,可能是每 CPU 的结构体或单个结构体。ref
:引用计数器和相关的操作。rcu_head
:用于 RCU(Read-Copy Update)机制的头部。
alloc_disk
struct gendisk *alloc_disk(int minors)
{
return alloc_disk_node(minors, NUMA_NO_NODE);
}
EXPORT_SYMBOL(alloc_disk);
struct gendisk *alloc_disk_node(int minors, int node_id)
{
struct gendisk *disk;
disk = kzalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id);
if (disk) {
if (!init_part_stats(&disk->part0)) {
kfree(disk);
return NULL;
}
disk->node_id = node_id;
if (disk_expand_part_tbl(disk, 0)) {
free_part_stats(&disk->part0);
kfree(disk);
return NULL;
}
disk->part_tbl->part[0] = &disk->part0;
/*
* set_capacity() and get_capacity() currently don't use
* seqcounter to read/update the part0->nr_sects. Still init
* the counter as we can read the sectors in IO submission
* patch using seqence counters.
*
* TODO: Ideally set_capacity() and get_capacity() should be
* converted to make use of bd_mutex and sequence counters.
*/
seqcount_init(&disk->part0.nr_sects_seq);
if (hd_ref_init(&disk->part0)) {
hd_free_part(&disk->part0);
kfree(disk);
return NULL;
}
disk->minors = minors;
rand_initialize_disk(disk);
disk_to_dev(disk)->class = &block_class;
disk_to_dev(disk)->type = &disk_type;
device_initialize(disk_to_dev(disk));
}
return disk;
}
函数 alloc_disk()
用于分配一个通用磁盘,并设置其 minors
值为传入的 minors
参数。最终会调用函数 alloc_disk_node()
,并将 NUMA_NO_NODE
作为 node_id
参数传递给它。
函数 alloc_disk_node()
接受 minors
和 node_id
作为参数,用于在指定的 NUMA 节点上分配一个通用磁盘结构体。它首先使用 kzalloc_node()
分配了一个大小为 sizeof(struct gendisk)
的内存块,并将其初始化为零。
接下来,它通过调用 disk_expand_part_tbl()
来扩展分区表,并将 disk->part0
分区指针设置为第一个分区。然后,它使用 seqcount_init()
初始化分区的扇区数的序列计数器,并使用 hd_ref_init()
初始化分区的引用计数器。
最后,它设置了其他一些字段的值,如 disk->minors
、rand_initialize_disk()
初始化磁盘的随机定时器状态,并将 disk_to_dev(disk)->class
和 disk_to_dev(disk)->type
设置为 block_class
和 disk_type
。最后,它使用 device_initialize()
初始化磁盘对应的设备结构体。
add_disk
函数 add_disk()
用于将磁盘添加到系统中,并进行必要的注册和初始化操作,以便系统可以正确识别和使用该磁盘。
void add_disk(struct gendisk *disk)
{
struct backing_dev_info *bdi;
dev_t devt;
int retval;
/* minors == 0 indicates to use ext devt from part0 and should
* be accompanied with EXT_DEVT flag. Make sure all
* parameters make sense.
*/
WARN_ON(disk->minors && !(disk->major || disk->first_minor));
WARN_ON(!disk->minors && !(disk->flags & GENHD_FL_EXT_DEVT));
disk->flags |= GENHD_FL_UP;
retval = blk_alloc_devt(&disk->part0, &devt);
if (retval) {
WARN_ON(1);
return;
}
disk_to_dev(disk)->devt = devt;
/* ->major and ->first_minor aren't supposed to be
* dereferenced from here on, but set them just in case.
*/
disk->major = MAJOR(devt);
disk->first_minor = MINOR(devt);
disk_alloc_events(disk);
/* Register BDI before referencing it from bdev */
bdi = &disk->queue->backing_dev_info;
bdi_register_owner(bdi, disk_to_dev(disk));
blk_register_region(disk_devt(disk), disk->minors, NULL,
exact_match, exact_lock, disk);
register_disk(disk);
blk_register_queue(disk);
/*
* Take an extra ref on queue which will be put on disk_release()
* so that it sticks around as long as @disk is there.
*/
WARN_ON_ONCE(!blk_get_queue(disk->queue));
retval = sysfs_create_link(&disk_to_dev(disk)->kobj, &bdi->dev->kobj,
"bdi");
WARN_ON(retval);
disk_add_events(disk);
blk_integrity_add(disk);
}
-
函数对传入的磁盘结构体进行一些合法性检查。接下来,函数将
GENHD_FL_UP
标志设置为磁盘的flags
字段,表示该磁盘已启动。 -
然后,函数调用
blk_alloc_devt()
分配一个设备号给磁盘的第一个分区disk->part0
。函数将分配的设备号存储在disk_to_dev(disk)->devt
中,并将disk->major
和disk->first_minor
设置为设备号的主设备号和次设备号。接下来,函数调用disk_alloc_events()
为磁盘分配事件结构体。 -
函数注册磁盘的
backing_dev_info
结构体,以便可以从块设备中引用它。函数调用blk_register_region()
注册块设备的区域,以及调用register_disk()
注册磁盘。函数调用blk_register_queue()
注册磁盘的请求队列。 -
函数通过调用
blk_get_queue()
在磁盘的请求队列上获取一个额外的引用计数,该引用计数将在disk_release()
函数中释放。这样可以确保请求队列在磁盘存在期间保持有效。函数使用sysfs_create_link()
创建一个链接,将磁盘的设备对象与backing_dev_info
的设备对象进行关联。函数调用disk_add_events()
添加磁盘事件。 -
最后,函数调用
blk_integrity_add()
添加磁盘的完整性检查。
块设备磁盘、分区、块设备关系
最后再来一张全景图
本文参考
https://blog.csdn.net/weixin_43780260/article/details/88993543
https://blog.csdn.net/morecrazylove/article/details/128712522