LWN: 基于io_uring的用户空间块设备驱动!

ublk是一种新的用户空间块设备驱动,利用io_uring与内核通信,旨在实现高性能。该驱动创建特殊设备/dev/ublk-control,用户空间服务器通过io_uring环来与其交互。服务器通过一系列命令如UBLK_CMD_ADD_DEV创建设备,使用mmap映射设备区域,处理I/O请求。目前提供了null和loop两种目标实现。ublk可能预示着未来内核中设备驱动的用户空间迁移趋势,但文档尚不完善。

关注了就能看到更多这么棒的文章哦~

An io_uring-based user-space block driver

By Jonathan Corbet
August 8, 2022
DeepL assisted translation
https://lwn.net/Articles/903855/

在 6.0 合并窗口期间,很容易会忽略掉新加入的 ublk 驱动;它被深埋在 io_uring 的 pull request 之中,而且完全没有任何文档能提示我们需要对它进行额外的研究。Ublk 的目标是促进在用户空间中实现高性能的 block driver。因此,它使用了 io_uring 跟内核进行通信。这个驱动目前被认为是实验性的;如果最终成功了,那么可能会是一个预兆,预示着将来内核会有巨变。

编者已经花了相当多的时间去研究 ublk 驱动的源代码,以及实现了用户空间组件的 ubdsrv server。从这个未加注释以及缺乏元音的代码中探索出来的景象,很可能在某些细节上是不正确的,不过整体理解应该相当接近现实了。

How ublk works

ublk 驱动首先创建了一个叫做 /dev/ublk-control 的特殊设备。用户空间的服务器进程(可以是很多个进程)通过打开该设备并建立一个 io_uring 的 ring 来与之通信。在这个层面的操作,基本上都是 ioctl() 命令,但是/dev/ublk-control 并没有 ioctl() 处理程序。相反,所有的操作都是通过 io_uring 的命令来发送的。既然最终目的是在 io_uring 的基础上实现一个设备,那么确实没有理由不从一开始就直接使用 io_uring 的功能。

服务器进程通常会以 UBLK_CMD_ADD_DEV 命令作为开始;正如人们所期望的,它可以将一个新的 ublk 设备添加到系统中。server 进程可以对这个设备的各个方面进行描述,包括它声称要实现的硬件队列有几个、块大小、最大传输大小以及设备可以容纳的 block 数量。在这个命令成功之后,在 ublk 驱动看来,就已经有这个设备了,并且可以通过/dev/ublkcN 来访问到,其中 N 是创建设备时所返回的设备 ID。但是,该设备此时尚未被添加到 block layer 中。

server 进程应该打开新增的 /dev/ublkcN 设备来进行如下步骤,其中第一个步骤是用 mmap()调用将设备上的一个区域映射到 server 的地址空间内。这个区域是描述 I/O 请求的 ublksrv_io_desc 结构的数组:

struct ublksrv_io_desc {/* op: bit 0-7, flags: bit 8-31 */__u32   op_flags;__u32   nr_sectors;__u64   start_sector;__u64   addr;  };

后续的 I/O 请求通知将从 io_uring 接收到。为了达到这个目的,服务器必须在新创建的设备上排队等候一组 UBLK_IO_FETCH_REQ 请求;通常情况下,为设备所声明的每个 "hardware queue"都会有一个队列,这也可能是跟 server 内运行的线程数量一一对应的。这个请求中还必须提供一个 memory buffer,用来容纳设备创建时所声明的 max request size 的数量。

在这些设置完成之后,可以使用 UBLK_CMD_START_DEV 操作来让 ublk 驱动真正创建一个对系统其他部分可见的块设备。当 block 子系统向这个设备发送一个请求时,队列中的某个 UBLK_IO_FETCH_REQ 操作就会完成。返回给用户空间 server 进程的 completion 数据中就包括了描述该请求的 ublkserv_io_desc 结构的索引,server 进程现在应该执行该请求。对于一个写请求来说,需要写入的数据就会放在 server 所提供的 buffer 中;对于读请求来说,数据应该会放在同一个 buffer 中。

在这个操作完成后,server 必须通知内核这个进展;也就是通过在 ring 中放一个 UBLK_IO_COMMIT_AND_FETCH_REQ 操作来进行通知的。它会把操作的结果反馈给 block 子系统,但同时也会把 buffer 放到队列里去准备接收下一个请求,从而避免了还需要专门进行这个操作。

还有 UBLK_CMD_STOP_DEV 和 UBLK_CMD_DEL_DEV 操作来让现有的设备消失,还有几个其他操作用来查询现有设备的信息。还有一些细节在这里并没有涉及到,这主要是为了提高性能。此外,配置 ublk 协议的目的是要实现 zero-copy I/O,但在目前的代码中没有实现。

server 代码中实现了两个 target:null 和 loop。正如人们所期望的,null 这个 target 是一个过于复杂的、针对 block 设备的/dev/null;它没有什么用,但使人们可以摒弃不相干的细节来直接看到这个驱动的工作效果。loop 这个 target 使用了一个现有的文件作为虚拟 block 设备的备份存储。根据作者 Ming Lei 的说法,使用这种循环实现,"性能甚至优于具有相同设置的内核 loop 设备"。

Implications

人们可能会问,我们为什么要做这项工作(而且显然得到了 Red Hat 的支持);如果世界一直在吵着要一个基于 io_uring 的、用户空间的、更快的 loop block 设备,其实它就已经悄悄地出现了。patch 的 cover letter 中提到的一个好处是,block 驱动代码的开发可以更容易地在用户空间完成。另一个好处是高性能的 qcow2 支持。patch cover letter 还引用了其他开发者的说法,希望有一个快速的用户空间 block 设备的机制。

不过,一个有趣的问题是,这种机制是否最终会促进一些设备驱动挪到内核之外,也许不仅仅局限在 block 设备驱动领域。将设备驱动放到用户空间的代码中,是一些 security-system 设计中的基本观点,包括 microkernel 系统。但是,这些设计方案总是有两个组件之间的通信开销的问题,尤其是它们不再在同一地址空间内运行。Io_uring 可能是解决这个问题的一个令人信服的答案。

如果今后真的如此发展了,那么未来的内核可能与我们现在的内核有很大的不同;它们可能更小,许多复杂的逻辑在独立的用户空间组件中运行。这是否是 Lei 对 ublk 的愿景的一部分?目前还不得而知,而且可能最终完全不会走到这一步。但 ublk 显然是一个有趣的实验,可能会导致下一步的重大变动。不过,在今后统治世界之前,还是需要先把自己的文档补充起来。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

int battery_type_check(int *battery_type) { int value = 0; int ret = 0; int ret_value = 0; int battery_id = 0; int battery_type_check = 0; if (batt_id == NULL) { bm_debug("[battery_type_check]: batt_id iio channel is null []\n"); *battery_type = BAT_TYPE__ATL_4400mV; battery_id = 0; return battery_id; } ret = iio_read_channel_processed(batt_id, &ret_value); if (ret < 0) bm_debug( "[battery_type_check] read channel err = %d,\n", ret); bm_debug( "[battery_type_check]: ret = %d,ret_value[%d]\n", ret, ret_value); value = ret_value; if(is_fuelgauge_apply() == true) { switch(battery_id) case 0: if (value >= BAT_TYPE_COS_4450mV_ADC_MIN && value <= BAT_TYPE_COS_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__COS_4450mV; battery_id = BAT_COS_BATT_ID; break; case 1: if (value >= BAT_TYPE_ATL_4450mV_ADC_MIN && value <= BAT_TYPE_ATL_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__ATL_4450mV; battery_id = BAT_ATL_BATT_ID; break; case 2: if (value >= BAT_TYPE_LWN_4450mV_ADC_MIN && value <= BAT_TYPE_LWN_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__LWN_4450mV; battery_id = BAT_LWN_BATT_ID; break; case 3: *battery_type = BAT_TYPE__UNKNOWN; battery_id = BAT_ENC_BATT_ID; break; default:【兼容原来的方案】 if (value >= BAT_TYPE_ATL_4450mV_ADC_MIN && value <= BAT_TYPE_ATL_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__ATL_4450mV; battery_id = BAT_ATL_BATT_ID; } else if (value >= BAT_TYPE_LWN_4450mV_ADC_MIN && value <= BAT_TYPE_LWN_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__LWN_4450mV; battery_id = BAT_LWN_BATT_ID; } else if (value >= BAT_TYPE_COS_4450mV_ADC_MIN && value <= BAT_TYPE_COS_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__COS_4450mV; battery_id = BAT_COS_BATT_ID; } else { *battery_type = BAT_TYPE__UNKNOWN; battery_id = BAT_ENC_BATT_ID; } } else { *battery_type = BAT_TYPE__UNKNOWN; battery_id = BAT_ENC_BATT_ID; } printk(KERN_ERR "[battery_type_check]: adc_value[%d], battery_type[%d], g_fg_battery_id[%d]\n", value, *battery_type, battery_id); return battery_id; }
08-21
这段代码的主要功能是检测电池类型,通过读取电池的ADC(模数转换)值来判断使用的是哪种类型的电池。以下是代码的详细解释: ### 代码功能说明: 1. **变量定义**: - `value`:存储读取到的ADC值。 - `ret`:存储读取ADC值的返回值(用于判断是否读取成功)。 - `ret_value`:从ADC通道读取的原始值。 - `battery_id`:表示电池的ID。 - `battery_type_check`:用于判断电池类型的变量。 2. **空指针检查**: 如果 `batt_id` 为 NULL,则将电池类型设置为默认值 `BAT_TYPE__ATL_4400mV`,并将 `battery_id` 设置为 0。 3. **读取ADC值**: 使用 `iio_read_channel_processed` 函数读取ADC值,并进行错误检查。 4. **电池类型判断**: 根据 `battery_id` 的值,使用 `switch-case` 结构判断电池类型。每个 `case` 对应一个电池类型,并检查ADC值是否在该类型的范围内。 5. **默认情况处理**: 如果 `battery_id` 不在预设范围内,则进入 `default` 分支,尝试匹配其他电池类型的ADC范围。 6. **日志输出**: 使用 `printk` 输出调试信息,包括ADC值、电池类型和电池ID。 ### 问题分析: 1. **语法错误**: - `switch-case` 结构的语法不正确,`case` 分支应该直接跟在 `switch` 后面,而不是嵌套在 `if` 语句中。 - `default:` 后面的冒号应该是英文冒号 `:`,而不是中文冒号 `:`。 2. **逻辑错误**: - `battery_id` 变量在 `switch-case` 结构中被用作判断条件,但它的值在代码中没有被正确更新或传递。 3. **代码结构问题**: - `switch-case` 结构应该放在 `if(is_fuelgauge_apply() == true)` 条件之外,或者需要重新设计逻辑以确保正确执行。 ### 改进后的代码: ```c int battery_type_check(int *battery_type) { int value = 0; int ret = 0; int ret_value = 0; int battery_id = 0; if (batt_id == NULL) { bm_debug("[battery_type_check]: batt_id iio channel is null []\n"); *battery_type = BAT_TYPE__ATL_4400mV; battery_id = 0; return battery_id; } ret = iio_read_channel_processed(batt_id, &ret_value); if (ret < 0) bm_debug("[battery_type_check] read channel err = %d,\n", ret); bm_debug("[battery_type_check]: ret = %d, ret_value[%d]\n", ret, ret_value); value = ret_value; if (is_fuelgauge_apply() == true) { switch (battery_id) { case 0: if (value >= BAT_TYPE_COS_4450mV_ADC_MIN && value <= BAT_TYPE_COS_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__COS_4450mV; battery_id = BAT_COS_BATT_ID; } break; case 1: if (value >= BAT_TYPE_ATL_4450mV_ADC_MIN && value <= BAT_TYPE_ATL_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__ATL_4450mV; battery_id = BAT_ATL_BATT_ID; } break; case 2: if (value >= BAT_TYPE_LWN_4450mV_ADC_MIN && value <= BAT_TYPE_LWN_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__LWN_4450mV; battery_id = BAT_LWN_BATT_ID; } break; case 3: *battery_type = BAT_TYPE__UNKNOWN; battery_id = BAT_ENC_BATT_ID; break; default: // 兼容原来的方案 if (value >= BAT_TYPE_ATL_4450mV_ADC_MIN && value <= BAT_TYPE_ATL_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__ATL_4450mV; battery_id = BAT_ATL_BATT_ID; } else if (value >= BAT_TYPE_LWN_4450mV_ADC_MIN && value <= BAT_TYPE_LWN_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__LWN_4450mV; battery_id = BAT_LWN_BATT_ID; } else if (value >= BAT_TYPE_COS_4450mV_ADC_MIN && value <= BAT_TYPE_COS_4450mV_ADC_MAX) { *battery_type = BAT_TYPE__COS_4450mV; battery_id = BAT_COS_BATT_ID; } else { *battery_type = BAT_TYPE__UNKNOWN; battery_id = BAT_ENC_BATT_ID; } break; } } else { *battery_type = BAT_TYPE__UNKNOWN; battery_id = BAT_ENC_BATT_ID; } printk(KERN_ERR "[battery_type_check]: adc_value[%d], battery_type[%d], g_fg_battery_id[%d]\n", value, *battery_type, battery_id); return battery_id; } ``` ### 代码改进说明: 1. **语法修正**: - 修复了 `switch-case` 结构的语法错误,确保每个 `case` 分支正确嵌套在 `switch` 语句中。 - 修正了 `default` 分支的冒号符号。 2. **逻辑优化**: - 确保 `battery_id` 在 `switch-case` 结构中被正确使用,并根据ADC值更新电池类型。 3. **代码结构优化**: - 保持代码结构清晰,确保 `if(is_fuelgauge_apply() == true)` 条件下的逻辑正确执行。 ### 相关问题: 1. 如何在Linux内核中实现ADC读取? 2. 如何设计电池类型的ADC阈值? 3. 如何调试和优化电池类型检测逻辑? 4. 如何处理电池类型检测中的异常情况?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值