一:概述
前面介绍了当内核检测到匹配的PCI设备后,会调用 qxl_pci_probe 初始化设备,其中会调用qxl_device_init 来初始化设备,为QXL设备进行内存映射,资源分配,环形缓冲区初始化,IRQ注册等操作,本文展开说说这些细节,以及介绍下QXL的显存管理。
二:QXL设备初始化细节
int qxl_device_init(struct qxl_device *qdev,
struct pci_dev *pdev)
{
int r, sb;
pci_set_drvdata(pdev, &qdev->ddev);
mutex_init(&qdev->gem.mutex);
mutex_init(&qdev->update_area_mutex);
mutex_init(&qdev->release_mutex);
mutex_init(&qdev->surf_evict_mutex);
qxl_gem_init(qdev);
qdev->rom_base = pci_resource_start(pdev, 2);
qdev->rom_size = pci_resource_len(pdev, 2);
qdev->vram_base = pci_resource_start(pdev, 0);
qdev->io_base = pci_resource_start(pdev, 3);
qdev->vram_mapping = io_mapping_create_wc(qdev->vram_base, pci_resource_len(pdev, 0));
if (!qdev->vram_mapping) {
pr_err("Unable to create vram_mapping");
return -ENOMEM;
}
if (pci_resource_len(pdev, 4) > 0) {
/* 64bit surface bar present */
sb = 4;
qdev->surfaceram_base = pci_resource_start(pdev, sb);
qdev->surfaceram_size = pci_resource_len(pdev, sb);
qdev->surface_mapping =
io_mapping_create_wc(qdev->surfaceram_base,
qdev->surfaceram_size);
}
if (qdev->surface_mapping == NULL) {
/* 64bit surface bar not present (or mapping failed) */
sb = 1;
qdev->surfaceram_base = pci_resource_start(pdev, sb);
qdev->surfaceram_size = pci_resource_len(pdev, sb);
qdev->surface_mapping =
io_mapping_create_wc(qdev->surfaceram_base,
qdev->surfaceram_size);
if (!qdev->surface_mapping) {
pr_err("Unable to create surface_mapping");
r = -ENOMEM;
goto vram_mapping_free;
}
}
DRM_DEBUG_KMS("qxl: vram %llx-%llx(%dM %dk), surface %llx-%llx(%dM %dk, %s)\n",
(unsigned long long)qdev->vram_base,
(unsigned long long)pci_resource_end(pdev, 0),
(int)pci_resource_len(pdev, 0) / 1024 / 1024,
(int)pci_resource_len(pdev, 0) / 1024,
(unsigned long long)qdev->surfaceram_base,
(unsigned long long)pci_resource_end(pdev, sb),
(int)qdev->surfaceram_size / 1024 / 1024,
(int)qdev->surfaceram_size / 1024,
(sb == 4) ? "64bit" : "32bit");
qdev->rom = ioremap_wc(qdev->rom_base, qdev->rom_size);
if (!qdev->rom) {
pr_err("Unable to ioremap ROM\n");
r = -ENOMEM;
goto surface_mapping_free;
}
if (!qxl_check_device(qdev)) {
r = -ENODEV;
goto rom_unmap;
}
r = qxl_bo_init(qdev);
if (r) {
DRM_ERROR("bo init failed %d\n", r);
goto rom_unmap;
}
qdev->ram_header = ioremap_wc(qdev->vram_base +
qdev->rom->ram_header_offset,
sizeof(*qdev->ram_header));
if (!qdev->ram_header) {
DRM_ERROR("Unable to ioremap RAM header\n");
r = -ENOMEM;
goto bo_fini;
}
qdev->command_ring = qxl_ring_create(&(qdev->ram_header->cmd_ring_hdr),
sizeof(struct qxl_command),
QXL_COMMAND_RING_SIZE,
qdev->io_base + QXL_IO_NOTIFY_CMD,
&qdev->display_event);
if (!qdev->command_ring) {
DRM_ERROR("Unable to create command ring\n");
r = -ENOMEM;
goto ram_header_unmap;
}
qdev->cursor_ring = qxl_ring_create(
&(qdev->ram_header->cursor_ring_hdr),
sizeof(struct qxl_command),
QXL_CURSOR_RING_SIZE,
qdev->io_base + QXL_IO_NOTIFY_CURSOR,
&qdev->cursor_event);
if (!qdev->cursor_ring) {
DRM_ERROR("Unable to create cursor ring\n");
r = -ENOMEM;
goto command_ring_free;
}
qdev->release_ring = qxl_ring_create(
&(qdev->ram_header->release_ring_hdr),
sizeof(uint64_t),
QXL_RELEASE_RING_SIZE, 0,
NULL);
if (!qdev->release_ring) {
DRM_ERROR("Unable to create release ring\n");
r = -ENOMEM;
goto cursor_ring_free;
}
idr_init_base(&qdev->release_idr, 1);
spin_lock_init(&qdev->release_idr_lock);
spin_lock_init(&qdev->release_lock);
idr_init_base(&qdev->surf_id_idr, 1);
spin_lock_init(&qdev->surf_id_idr_lock);
mutex_init(&qdev->async_io_mutex);
/* reset the device into a known state - no memslots, no primary
* created, no surfaces. */
qxl_io_reset(qdev);
/* must initialize irq before first async io - slot creation */
r = qxl_irq_init(qdev);
if (r) {
DRM_ERROR("Unable to init qxl irq\n");
goto release_ring_free;
}
/*
* Note that virtual is surface0. We rely on the single ioremap done
* before.
*/
setup_slot(qdev, &qdev->main_slot, 0, "main",
(unsigned long)qdev->vram_base,
(unsigned long)qdev->rom->ram_header_offset);
setup_slot(qdev, &qdev->surfaces_slot, 1, "surfaces",
(unsigned long)qdev->surfaceram_base,
(unsigned long)qdev->surfaceram_size);
INIT_WORK(&qdev->gc_work, qxl_gc_work);
return 0;
release_ring_free:
qxl_ring_free(qdev->release_ring);
cursor_ring_free:
qxl_ring_free(qdev->cursor_ring);
command_ring_free:
qxl_ring_free(qdev->command_ring);
ram_header_unmap:
iounmap(qdev->ram_header);
bo_fini:
qxl_bo_fini(qdev);
rom_unmap:
iounmap(qdev->rom);
surface_mapping_free:
io_mapping_free(qdev->surface_mapping);
vram_mapping_free:
io_mapping_free(qdev->vram_mapping);
return r;
}
这个代码做了以下几件事:
1. 将qdev绑定到PCI设备,即将设备相关的数据结构与PCI设备关联起来,供后续使用。
2. 初始化多个mutex 和 spinlock,并调用qxl_gem_init() 初始化GEM相关数据。
3. 获取QXL设备的ROM,VRAM,I/O,Surface RAM的物理地址和大小。
qdev->rom_base = pci_resource_start(pdev, 2);
qdev->rom_size = pci_resource_len(pdev, 2);
qdev->vram_base = pci_resource_start(pdev, 0);
qdev->io_base = pci_resource_start(pdev, 3);
上面这些语句是从PCI配置空间中的BAR(Base Address Register)中读取地址信息,BAR是用于映射设备资源(如 ROM、VRAM、I/O 区域)的。
那么这些BAR值是什么时候写进去的呢? 这些BAR值是在系统启动时,由BIOS/UEFI或操作系统内核写进去的。比如当系统上电时,BIOS/UEFI扫描所有PCI设备,它们声明了BAR,BIOS会为这些BAR分配内存地址,并写入BAR寄存器,系统引导后,Linux内核也会扫描PCI总线,并读取这些BAR的值,映射成资源表。虽然QXL是虚拟显卡设备,但在QEMU虚拟机中BAR的写入过程和上面的描述是一样的。
4. 将物理地址映射到内核虚拟地址空间,以确保内核能够能访问这些区域。其中 vram_base 是QXL的存放通用GPU数据的显存区域,主要用于存储命令缓冲区、缓冲区对象;surfaceram_base 是QXL的存储图层数据的显存区域, 主要存放图像数据、纹理。rom_base是显卡的只读存储器地址区域,主要存放硬件描述结构,版本信息,设备初始化配置。
5. 初始化缓冲区对象;这个后面再详细介绍。
6. 初始化命令缓冲区。
7. 设置main slot 和 surface slot 地址和大小;这个意思是说前面已经把vram显存和surface显存准备好了,现在来告诉QEMU虚拟GPU设备,你可以从这些地址读写数据了;
8. 重置设备状态和初始化中断处理。
9. 初始化异步工作项 gc_work, 它的作用是延迟执行异步任务( qxl_gc_work() ), 作用是将来某个时刻由内核调度器执行 qxl_gc_work()。 qxl_gc_work()的主要职责是回收不再使用的显存对象,释放命令缓冲区对象或surface对象,清理命令缓冲区的垃圾引用。
三:缓冲区对象初始化
下面进一步说说缓冲区对象的初始化过程 qxl_bo_init,qxl_bo_init 调用 qxl_ttm_init, qxl_ttm_init 是QXL显卡驱动中用于初始化缓冲区对象的函数,它的作用是建立QXL显卡的内存管理系统,包括VRAM 和 Surface RAM两部分。 VRAM 是用于存放显示输出帧缓冲区,即我们最终会看到的画面,是“扫描输出”区域,Surface RAM 用来存放多个图形表面,即多个图层/窗口的缓存,比如Qt、GTK的窗口、合成器的临时缓冲等。
int qxl_ttm_init(struct qxl_device *qdev)
{
int r;
int num_io_pages; /* != rom->num_io_pages, we include surface0 */
/* No others user of address space so set it to 0 */
r = ttm_device_init(&qdev->mman.bdev, &qxl_bo_driver, NULL,
qdev->ddev.anon_inode->i_mapping,
qdev->ddev.vma_offset_manager,
false, false);
if (r) {
DRM_ERROR("failed initializing buffer object driver(%d).\n", r);
return r;
}
/* NOTE: this includes the framebuffer (aka surface 0) */
num_io_pages = qdev->rom->ram_header_offset / PAGE_SIZE;
r = qxl_ttm_init_mem_type(qdev, TTM_PL_VRAM, num_io_pages);
if (r) {
DRM_ERROR("Failed initializing VRAM heap.\n");
return r;
}
r = qxl_ttm_init_mem_type(qdev, TTM_PL_PRIV,
qdev->surfaceram_size / PAGE_SIZE);
if (r) {
DRM_ERROR("Failed initializing Surfaces heap.\n");
return r;
}
DRM_INFO("qxl: %uM of VRAM memory size\n",
(unsigned int)qdev->vram_size / (1024 * 1024));
DRM_INFO("qxl: %luM of IO pages memory ready (VRAM domain)\n",
((unsigned int)num_io_pages * PAGE_SIZE) / (1024 * 1024));
DRM_INFO("qxl: %uM of Surface memory size\n",
(unsigned int)qdev->surfaceram_size / (1024 * 1024));
return 0;
}
1. 这个代码首先调用 ttm_device_init 初始化显存管理系统,让显卡设备能用TTM框架管理自己的显存,并通过一组定制函数来自定义自己的行为(qxl_bo_driver);
ttm_device_init(&qdev->mman.bdev, &qxl_bo_driver,
NULL, qdev->ddev.anon_inode->i_mapping,
qdev->ddev.vma_offset_manager,
false, false);
这个代码的意思是:我要开始用TTM管理QXL的显存了,相关操作安装qxl_bo_driver 来处理。
TTM 是Linux DRM 子系统中的一个通用显存管理框架,它的主要职责是同意管理显存系统内存,对页表进行地址映射,管理图形缓冲区对象,支持缓冲区搬迁(显存和内存之间来回迁移),通过DMA等机制与内核其他子系统协作。
static struct ttm_device_funcs qxl_bo_driver = {
.ttm_tt_create = &qxl_ttm_tt_create,
.ttm_tt_destroy = &qxl_ttm_backend_destroy,
.eviction_valuable = ttm_bo_eviction_valuable,
.evict_flags = &qxl_evict_flags,
.move = &qxl_bo_move,
.io_mem_reserve = &qxl_ttm_io_mem_reserve,
.delete_mem_notify = &qxl_bo_delete_mem_notify,
};
这个qxl_bo_driver 定义了QXL驱动如何管理TTM buffer object 的行为,起到类似“策略函数集合”的作用。其中:
ttm_tt_create 是用来创建页表结构的,用于系统内存页管理。
ttm_tt_destroy 是用来销毁页表的。
eviction_valuable 判断某BO是否值得被驱逐。
evict_flags 设置被驱逐 BO 的新内存属性,比如将其移动到系统内存
move 实现显存与系统内存之间的移动逻辑
io_mem_reserve 映射到I/O内存时调用,告诉内核如何将显存映射成可访问的地址
delete_mem_notify BO被释放前的回调,用来清理QXL资源。
static struct ttm_tt *qxl_ttm_tt_create(struct ttm_buffer_object *bo,
uint32_t page_flags)
{
struct ttm_tt *ttm;
ttm = kzalloc(sizeof(struct ttm_tt), GFP_KERNEL);
if (ttm == NULL)
return NULL;
if (ttm_tt_init(ttm, bo, page_flags, ttm_cached, 0)) {
kfree(ttm);
return NULL;
}
return ttm;
}
这个代码会创建一张页表(ttm_tt), 映射这块bo的内存区域,然后内核后面就能通过ttm_tt找到每一页。
static int qxl_bo_move(struct ttm_buffer_object *bo, bool evict,
struct ttm_operation_ctx *ctx,
struct ttm_resource *new_mem,
struct ttm_place *hop)
{
struct ttm_resource *old_mem = bo->resource;
int ret;
if (!old_mem) {
if (new_mem->mem_type != TTM_PL_SYSTEM) {
hop->mem_type = TTM_PL_SYSTEM;
hop->flags = TTM_PL_FLAG_TEMPORARY;
return -EMULTIHOP;
}
ttm_bo_move_null(bo, new_mem);
return 0;
}
qxl_bo_move_notify(bo, new_mem);
ret = ttm_bo_wait_ctx(bo, ctx);
if (ret)
return ret;
if (old_mem->mem_type == TTM_PL_SYSTEM && bo->ttm == NULL) {
ttm_bo_move_null(bo, new_mem);
return 0;
}
return ttm_bo_move_memcpy(bo, ctx, new_mem);
}
这个代码用于将图形缓冲区(bo)迁移到另一个内存位置,根据当前状态选择合适的方式来迁移,比如复制内容,置为NULL。
2. 计算VRAM的页数,以及初始化TTM的VRAM (framebuffer + command buffer)和 Surface RAM。
三:注册DRM驱动细节
回忆在之前的 qxl_pci_probe 函数里, 会注册一个DRM驱动,来定义驱动的能力,回调函数。 下面来详细说说这个DRM驱动。
static int
qxl_pci_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
....
qdev = devm_drm_dev_alloc(&pdev->dev, &qxl_driver,
struct qxl_device, ddev);
if (IS_ERR(qdev)) {
pr_err("Unable to init drm dev");
return -ENOMEM;
}
....
}
static struct drm_driver qxl_driver = {
.driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC | DRIVER_CURSOR_HOTSPOT,
.dumb_create = qxl_mode_dumb_create,
.dumb_map_offset = drm_gem_ttm_dumb_map_offset,
#if defined(CONFIG_DEBUG_FS)
.debugfs_init = qxl_debugfs_init,
#endif
.gem_prime_import_sg_table = qxl_gem_prime_import_sg_table,
DRM_FBDEV_TTM_DRIVER_OPS,
.fops = &qxl_fops,
.ioctls = qxl_ioctls,
.num_ioctls = ARRAY_SIZE(qxl_ioctls),
.name = DRIVER_NAME,
.desc = DRIVER_DESC,
.major = 0,
.minor = 1,
.patchlevel = 0,
.release = qxl_drm_release,
};
首先这个DRM驱动支持GEM,即显存管理框架,用于buffer对象(BO)的分配、映射管理。并且支持模式设置(modesetting),意味着它可以设置分辨率、刷新率,是KMS的前提。 同时驱动支持原子操作,即显示配置的更改可以打包成一个事务进行提交,不会产生中间态,确保一致性。支持硬件游标的热点位置设置,即可指定光标图像的“热点”,例如鼠标箭头的尖端,而不是总以左上角为基准。
其次这个DRM驱动支持以下回调函数:
dump_create: 创建dump buffer的函数,用于没有 GPU 加速 的 framebuffer。
dump_map_offset: 为dump buffer映射提供 offset, 通常使用通用的 drm_gem_ttm_dump_map_offset。
debugfs_init: 在debugfs中初始化调试接口,仅在内核开启CONFIG_DEBUG_FS时编译。
gem_prime_import_sg_table: 用于DMA-BUF导入,支持跨设备 buffer 共享。
DRM_FBDEV_TTM_DRIVERS_OPS: 宏定义,为使用 TTM 管理显存的 fbdev 提供默认实现。
fops: 文件操作函数合集,比如 open,release,mmap等。
ioctls / num_ioctls : 自定义 IOCTL 命令数组及数量。
release:当DRM device 被释放时调用的回调函数。
四: 缓冲区对象的创建
前面注册了DRM驱动,其中DRM驱动的一个功能是用于buffer对象(BO)的分配、映射管理。下面看看怎么做的。
int qxl_gem_object_create(struct qxl_device *qdev, int size,
int alignment, int initial_domain,
bool discardable, bool kernel,
struct qxl_surface *surf,
struct drm_gem_object **obj)
{
struct qxl_bo *qbo;
int r;
*obj = NULL;
/* At least align on page size */
if (alignment < PAGE_SIZE)
alignment = PAGE_SIZE;
r = qxl_bo_create(qdev, size, kernel, false, initial_domain, 0, surf, &qbo);
if (r) {
if (r != -ERESTARTSYS)
DRM_ERROR(
"Failed to allocate GEM object (%d, %d, %u, %d)\n",
size, initial_domain, alignment, r);
return r;
}
*obj = &qbo->tbo.base;
mutex_lock(&qdev->gem.mutex);
list_add_tail(&qbo->list, &qdev->gem.objects);
mutex_unlock(&qdev->gem.mutex);
return 0;
}
这个函数的主要功能是:在QXL显存设备上分配一块图形内存,并封装为一个GEM对象,提供用户或内核使用。其中核心实现是 qxl_bo_create 函数,下面来看一下:
int qxl_bo_create(struct qxl_device *qdev, unsigned long size,
bool kernel, bool pinned, u32 domain, u32 priority,
struct qxl_surface *surf,
struct qxl_bo **bo_ptr)
{
struct ttm_operation_ctx ctx = { !kernel, false };
struct qxl_bo *bo;
enum ttm_bo_type type;
int r;
if (kernel)
type = ttm_bo_type_kernel;
else
type = ttm_bo_type_device;
*bo_ptr = NULL;
bo = kzalloc(sizeof(struct qxl_bo), GFP_KERNEL);
if (bo == NULL)
return -ENOMEM;
size = roundup(size, PAGE_SIZE);
r = drm_gem_object_init(&qdev->ddev, &bo->tbo.base, size);
if (unlikely(r)) {
kfree(bo);
return r;
}
bo->tbo.base.funcs = &qxl_object_funcs;
bo->type = domain;
bo->surface_id = 0;
INIT_LIST_HEAD(&bo->list);
if (surf)
bo->surf = *surf;
qxl_ttm_placement_from_domain(bo, domain);
bo->tbo.priority = priority;
r = ttm_bo_init_reserved(&qdev->mman.bdev, &bo->tbo, type,
&bo->placement, 0, &ctx, NULL, NULL,
&qxl_ttm_bo_destroy);
if (unlikely(r != 0)) {
if (r != -ERESTARTSYS)
dev_err(qdev->ddev.dev,
"object_init failed for (%lu, 0x%08X)\n",
size, domain);
return r;
}
if (pinned)
ttm_bo_pin(&bo->tbo);
ttm_bo_unreserve(&bo->tbo);
*bo_ptr = bo;
return 0;
}
这个函数实现了QXL驱动中用于创建图形内存对象的核心功能, 它通过TTM内存管理框架创建一个buffer,对应一个qxl_bo 对象,同时初始化底层的drm_gem_object 和 ttm_buffer_object, 最后返回这个buffer。
这个函数首先分配 qxl_bo 结构体内存,这个结构体定义如下:
struct qxl_bo {
struct ttm_buffer_object tbo;
/* Protected by gem.mutex */
struct list_head list;
/* Protected by tbo.reserved */
struct ttm_place placements[3];
struct ttm_placement placement;
struct iosys_map map;
void *kptr;
unsigned int map_count;
int type;
/* Constant after initialization */
unsigned int is_primary:1; /* is this now a primary surface */
unsigned int is_dumb:1;
struct qxl_bo *shadow;
unsigned int hw_surf_alloc:1;
struct qxl_surface surf;
uint32_t surface_id;
struct qxl_release *surf_create;
};
这个结构体是QXL DRM 驱动中缓冲对象结构体,是显存管理的核心,其中ttm_buffer_object 是描述这个对象当前在哪个内存区。 base 是内嵌的drm_gem_object,供用户空间访问。 ttm_place 是表示当前BO可被放置的内存区域。 iosys_map 封装了内核对该BO的访问映射信息。kptr 是内核虚拟地址,用于读写显存。
下面再回到 qxl_bo_create 函数中,在初始化完qxl_bo之后,这个函数会初始化drm_gem_object, 作用是让gem对象挂到 qxl_bo->tbo.base 上。 然后设置操作这个bo对象的回调函数。 接着调用 ttm_bo_init_reserved,它是TTM框架中最核心的内存资源初始化接口,它会把bo注册进TTM管理系统,为bo分配页表,并将它安置到合适的内存区域,准备好GPU访问所需的地址映射,并确保在bo在资源紧张时能被swap-out, move等。
五:其他缓冲区对象操作
ttm_bo_pin 用于将bo固定在一个内存位置上,防止其被移动或换出,使用场景是GPU framebuffer 等必须驻留在显存/某特定区域。或者驱动不允许bo在使用过程中被搬迁(如DMA正在传输)。
ttm_bo_vmap 用与将bo映射到内核虚拟地址空间,使得内核可以通过CPU直接访问这块显存或系统内存。使用场景是CPU想读写某个BO时。
这两个函数实现细节会在后面的ttm文章中详细介绍。qxl_bo 操作目前只是在它基础上做了封装。
六:备注:
一:TTM 和 GEM 以及GTT 的区别:
1. GEM 是高层统一抽象,用户用户空间与内核之间传递 buffer。
2. TTM 更底层,用于内核内部的显存和BO管理机制。
3. GTT 是Graphics Translation Table(图形地址重映射表),它的实际数据存在系统内存中,由GPU通过地址映射机制访问,就像GPU自己的“页表”,GTT把一块系统内存“虚拟”成GPU可访问的地址。
七:总结:
以下是qxl显存管理用到的函数总结:
0. ttm_device_init 用来初始化ttm设备; ttm_range_man_init 用来初始化内存类型区域。 ttm_device_funcs 是实现显存与系统内存之间的统一管理用的,是TTM的钩子函数。
1. drm_gem_object 是用户态和内核态之间共享显存的桥梁,比如我们在用户态创建一个用于渲染的framebuffer 或 texture,它在内核中就可能对应一个 drm_gem_objet.
2. ttm_buffer_object 前面说了 drm_gem_object 是封装用户可见的抽象对象,那么对应内部就是ttm_buffer_object了,它负责处理页表映射,VRAM/GTT切换等复杂细节。
3. drm_gem_object_init 是用来初始化一个GEM对象的。
4. drm_gem_object_funcs 是在drm_gem_object_init 初始化时传进去的回调函数,用来操作一个GEM对象的(打开,释放,关闭,固定,映射等)。
5. ttm_bo_init_reserved 是用来初始化一个 ttm_buffer_object 的
6. ttm_bo_pin 是用来固定一个 ttm_buffer_object 的
7. ttm_bo_unreserve 是用来解除缓冲区的独占,可以允许其他进程或组件访问。
8. drm_gem_handle_create 是用来为GEM对象创建一个句柄,方便用户空间使用。
9. drm_gem_object_get, drm_gem_object_put 是增加/减少引用技术。
10. drm_gem_ttm_dumb_map_offset 是获取GEM对象的 mmap 偏移量。
11. ttm_bo_vmap 用于将 ttm_buffer_object 对象映射到内核虚拟地址空间。
12. ttm_bo_vunmap 用于取消之前通过 ttm_bo_vmap 建立的内核虚拟地址映射。
13. drm_gem_object_lookup 是根据句柄查找GEM对象。