显卡内存管理机制及驱动实现(Intel gma500为例)

news/2024/10/18 0:26:59/

背景

在Intel Atom芯片的环境中分析故障时,顺便看了下该环境的显卡驱动中的内存管理相关机制,显卡为Atom CPU的集成显卡,低端产品,gma500显卡。本文主要关注该显卡驱动中的内存管理相关的实现原理,其他显卡原理与之类似,有一定的参考意义。

CPU的内存管理

这个是个基础的话题,看似神秘,其实简单。简单描述如下:

CPU硬件中有个内存管理单元(MMU),内存管理依赖于页表,页表是一个逻辑概念,并不存在这样一个硬件单元,其本质就是一段物理内存(内核中分配的,地址不定),其中记录了虚拟地址物理地址的映射关系,将这段内存的起始地址告诉(其实就是将其写入指定的寄存器)CPU(MMU),然后CPU就可以利用这个页表来进行地址映射了。

页表中的虚拟地址物理地址的映射关系是动态创建的(除了内核部分),触发机制就是缺页异常(page fault),也就是说物理内存是动态的通过缺页异常分配的,当用户使用malloc或mmap之类的接口分配内存时,实际得到的虚拟地址(对用户来说,只能看见虚拟地址),实际的物理内存,是在实际访问该内存(写)时,硬件触发缺页异常,在缺页异常中分配的。

基本原理就介绍这些,关于Linux的内存管理机制足够写一本书,这里就不罗嗦了,资料也很多,以后有时间可以专门写一系列的文章来说明。

GPU的内存管理

GPU也需要管理内存?当然。而且更复杂一些。

通常来说,对于GPU,其需要管理两类内存:主存(即CPU使用的物理内存)和显存(GPU自带的内存)。

GPU还需要管理主存?

是的,其实这个说法不太准确,应该说:GPU需要使用主存(也称GTT内存,从GPU的角度),需要访问主存。实际的内存和显存的管理工作其实是内核做的,内核中有专门管理主存和显存的模块,通常为TTM或GEM(Intel),内容太多,不详细说了。

GPU管理内存的方式本质上跟CPU相似,它也有自己的MMU,也有相应的页表,这些跟CPU都是独立的。也使用页表进行地址映射,页表也使用多级方式。

不同的是,GPU需要能访问主存,也就是说GPU的页表需要能映射主存中的物理地址,基本原理其实也跟CPU处理IO内存的方式相似,也是通过一定的映射(GPU有个GART表)将主存中的物理内存和GPU自己的显存映射到统一的物理地址空间(从GPU的角度。这个地址空间对CPU是不可见的),也就是说,GPU看到的也就是自己的一个物理地址空间,GPU使用这些地址时,并不关心这些地址对应的是主存,还是显存。具体的映射工作是GPU硬件完成的,细节我们也不用关心,这里不描述。

有兴趣的可以自己搜一些资料看看。

如何使用内存?

前面说了基本的内存管理原理,这节从使用者的角度来继续说明。

大家平常都怎么使用内存?

内存使用有多种方式,通常有:

  1. malloc分配内存,这个应该是最常见的了,可能一些coder只用过这种方式~。如前面所说,其本质为分配了一段虚拟地址范围(虚拟内存),实际访问时(比如memset)通过缺页异常分配实际的物理内存。得到虚拟内存地址后,程序就可以通过这个地址来访问内存了,这个地址,在C语言中就对应指针。
  2. mmap匿名分配内存。当mmap参数的fd为空时,称为匿名映射,此时的本质操作跟malloc一样,就是分配虚拟内存。事实上,malloc实现中,也使用了mmap来分配内存,细节这里不关注了。
  3. mmap映射文件。当mmap中的fd参数不为空时,时间是将fd对应的设备空间映射到虚拟地址空间,这样,用户可以通过访问这段地址空间,而直接访问设备。当文件设备对应的是内存时(如共享内存、tmpfs),用户访问的实际为内存。

显卡如何使用内存?

回到显卡的场景,我们这里主要讨论显卡使用主存的场景,显卡为何需要使用主存呢?

通常情况下,显卡要实现图像显示,至少需要一段内存,对应于屏幕上显示的内容,这段内存就是我们平常熟知的Framebuffer,Framebuffer可以是主存或显存上分配的任意一段内存,而且还不要求物理上连续。有些显卡自带的显存很小,甚至没有,此时必须要使用主存。

不连续都可以?为什么?

是的,如之前所说,显卡(GPU)有自己的MMU和页表,会将不连续的物理内存映射到连续的虚拟地址空间中去。所以,Framebuffer对应的内存不需要物理上连续。

Framebuffer在主存或显存上都可以?为什么?

是的,如之前所说,显卡(GPU)可以范围主存中的内存,只需要将其映射到GPU的地址空间中即可,显卡可以访问的这部分主存,也叫GTT内存,具体的映射是显卡硬件完成的,我们不关心。

简单说,如果要使用主存中的内存做Framebuffer的话,只需要在主存中分配一段内存,然后告诉显卡就可以了,这个“告诉”动作,本质上就是写一些指定的寄存器,对驱动的coder来说,就是驱动提供的一些接口,通常是与硬件紧密相关的。

显卡使用内存,有两种的典型场景。

  1. 预先分配。即预先为Framebuffer分配好内存,然后直接使用。
  2. 延迟分配。不预先分配物理内存,仅在map时分配虚拟内存,并设置page fault处理接口。当实际使用这段内存时,触发page fault,在page fault处理接口分配内存,并设置为Framebuffer。

预先分配的使用流程,简单描述如下:

  1. 分配内存。使用drm提供的接口(通过ioctl方式调入内存,底层由具体的内核中的显卡驱动实现)分配内存,接口参数中提供用于选择内存分配类型的标志,用于选择分配显存或者GTT内存(也就是说选择权在用户手中)。内存分配通常依赖于内核中的TTM模块(或GEM)。
  2. 设置Framebuffer。设置分配后的内存(通常以bo方式)为Framebuffer(drm也提供了相应的接口,直接调用即可,底层也由显卡驱动实现)。
  3. map Framebuffer。步骤1中得到的内存是物理内存,还不能直接使用(使用虚拟内存的OS不能直接使用物理地址),需要将其映射到虚拟地址空间后(将物理地址转换为虚拟地址/线性地址)才能使用,此步骤即完成此操作。本质为分配相应的虚拟地址,并修改相应页表,完成映射。
  4. copy数据。将需要显示的数据拷贝到步骤上map后的虚拟地址中,该虚拟地址最终对应的就是Framebuffer,GPU会自动定期将Framebuffer中的内容显示到屏幕上。

延迟分配的使用流程,简单描述如下:

  1. 分配gtt相关管理结构(gtt_range)。gtt结构用于管理物理内存,此时并未实际分配物理内存。
  2. map内存。通常是对drm设备(/dev/dri/cardx)进行mmap操作,分配虚拟地址空间,此时也不分配物理内存,仅分配虚拟内存(vma),并设置相应的vm_ops操作,其中包括了关键的page fault处理接口。
  3. 拷贝数据,触发page fault。用户使用map后得到的虚拟地址,向其中拷贝数据,此时,由于此需要地址还没有对应的物理内存,所以,硬件会自动触发page fault,然后进入之前设置的page fault处理接口,该接口由显卡驱动实现,在其中实现物理内存的分配操作,并最终将其设置为Framebuffer。

其实过程都挺简单的,但是出于性能等因素的考虑,实际的操作可能要复杂很多,比如涉及3D加速之类的内容,这里就不详述了,后面有时间再写3D加速相关的内容。就本文而言,理解到这就可以了。

显卡驱动中的内存管理

前面说了基本的内存管理机制和使用方式,这节从驱动的角度,看看这些机制具体是如何实现的。

这里以Intel Atom集成显卡gma500为例说明。由于这个显卡的显存很小,只有8M左右,所以,很多情况下,显卡需要使用主存。本文关注的也是GPU使用主存的相关原理,对于使用显存的情况(相对简单),不做相关分析。

老版本的gma500显卡驱动中,对于Framebuffer小于8M的情况(分辨率低于1920*1080),直接在显存中分配内存并设置为Framebuffer。对于大于8M的情况,则使用vmalloc接口在主存中分配内存。其流程是典型的预先分配的场景。具体代码本文不做描述了,有兴趣的同学可以自己看看代码。

新版本的gma500显卡驱动中,对此进行改进,使用了延迟分配的方式,在后续章节中对相应代码进行分析。

## 分配Gtt Range

gtt_range可以理解为一段io mem,是显卡的pci配置空间中配置的一段IO mem资源,专门用来管理gtt内存的。

分配Gtt range的流程在显卡初始化的过程中,代码流程如下:

psbfb_createpsbfb_allocpsb_gtt_alloc_range/**	GTT resource allocator - allocate and manage GTT address space*//***	psb_gtt_alloc_range	-	allocate GTT address space*	@dev: Our DRM device*	@len: length (bytes) of address space required*	@name: resource name*	@backed: resource should be backed by stolen pages**	Ask the kernel core to find us a suitable range of addresses*	to use for a GTT mapping.**	Returns a gtt_range structure describing the object, or NULL on*	error. On successful return the resource is both allocated and marked*	as in use.*/
struct gtt_range *psb_gtt_alloc_range(struct drm_device *dev, int len,const char *name, int backed, u32 align)
{struct drm_psb_private *dev_priv = dev->dev_private;struct gtt_range *gt;struct resource *r = dev_priv->gtt_mem;int ret;unsigned long start, end;if (backed) {/* The start of the GTT is the stolen pages */start = r->start;end = r->start + dev_priv->gtt.stolen_size - 1;} else {/* The rest we will use for GEM backed objects */start = r->start + dev_priv->gtt.stolen_size;end = r->end;}gt = kzalloc(sizeof(struct gtt_range), GFP_KERNEL);if (gt == NULL)return NULL;gt->resource.name = name;gt->stolen = backed;gt->in_gart = backed;gt->roll = 0;/* Ensure this is set for non GEM objects */gt->gem.dev = dev;ret = allocate_resource(dev_priv->gtt_mem, &gt->resource,len, start, end, align, NULL, NULL);if (ret == 0) {gt->offset = gt->resource.start - r->start;return gt;}kfree(gt);return NULL;
}

从代码看,关键就是调用了allocate_resource分配了一段空闲io mem资源,这些资源本质上是BIOS中协调和预留的。

mmap实现

用户要访问为显卡分配的内存,必须将其映射到CPU的虚拟地址空间中。通常,都是通过mmap方式。

这种情况下,显卡驱动就需要实现dri设备(/dev/dri/cardx)的mmap接口,看看dri设备的mmap接口是如何实现的:

static const struct file_operations psb_gem_fops = { .owner = THIS_MODULE, .open = drm_open, .release = drm_release, .unlocked_ioctl = psb_unlocked_ioctl, .mmap = drm_gem_mmap, .poll = drm_poll, .read = drm_read, };

可以看出,其mmap接口对应为drm_gem_mmap,继续看看该接口流程:

drm_gem_mmapdrm_gem_mmap_objint drm_gem_mmap_obj(struct drm_gem_object *obj, unsigned long obj_size,struct vm_area_struct *vma)
{struct drm_device *dev = obj->dev;lockdep_assert_held(&dev->struct_mutex);/* Check for valid size. */if (obj_size < vma->vm_end - vma->vm_start)return -EINVAL;if (!dev->driver->gem_vm_ops)return -EINVAL;vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;//设置vma_ops,其中包括了page fault的处理接口vma->vm_ops = dev->driver->gem_vm_ops;vma->vm_private_data = obj;vma->vm_page_prot = pgprot_writecombine(vm_get_page_prot(vma->vm_flags));/* Take a ref for this mapping of the object, so that the fault* handler can dereference the mmap offset's pointer to the object.* This reference is cleaned up by the corresponding vm_close* (which should happen whether the vma was created by this call, or* by a vm_open due to mremap or partial unmap or whatever).*/drm_gem_object_reference(obj);drm_vm_open_locked(dev, vma);return 0;

可以看出,这个流程中设置了vma_ops,其中包括了page fault的处理接口。

接下来的流程就跟硬件紧密相关了,gma500显卡驱动的实现如下:

static struct drm_driver driver = {.driver_features = DRIVER_HAVE_IRQ | DRIVER_IRQ_SHARED | \DRIVER_MODESET | DRIVER_GEM,.load = psb_driver_load,.unload = psb_driver_unload,.lastclose = psb_driver_lastclose,.preclose = psb_driver_preclose,.set_busid = drm_pci_set_busid,.num_ioctls = ARRAY_SIZE(psb_ioctls),.device_is_agp = psb_driver_device_is_agp,.irq_preinstall = psb_irq_preinstall,.irq_postinstall = psb_irq_postinstall,.irq_uninstall = psb_irq_uninstall,.irq_handler = psb_irq_handler,.enable_vblank = psb_enable_vblank,.disable_vblank = psb_disable_vblank,.get_vblank_counter = psb_get_vblank_counter,.gem_free_object = psb_gem_free_object,.gem_vm_ops = &psb_gem_vm_ops,

gem_vm_ops设置为了psb_gem_vm_ops,看看对应的实现:

static const struct vm_operations_struct psb_gem_vm_ops = {.fault = psb_gem_fault,.open = drm_gem_vm_open,.close = drm_gem_vm_close,
};

可以看到,page fault对应的处理接口为:psb_gem_fault

page fault处理接口实现

继续看看psb_gem_fault的具体实现。

int psb_gem_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{struct drm_gem_object *obj;struct gtt_range *r;int ret;unsigned long pfn;pgoff_t page_offset;struct drm_device *dev;struct drm_psb_private *dev_priv;//获取drm_gem_object对象obj = vma->vm_private_data;	/* GEM object */dev = obj->dev;dev_priv = dev->dev_private;//获取gtt_range对象r = container_of(obj, struct gtt_range, gem);	/* Get the gtt range *//* Make sure we don't parallel update on a fault, nor move or removesomething from beneath our feet */mutex_lock(&dev->struct_mutex);/* For now the mmap pins the object and it stays pinned. As thingsstand that will do us no harm */if (r->mmapping == 0) {//为gtt_range中对应的page分配物理内存。ret = psb_gtt_pin(r);if (ret < 0) {dev_err(dev->dev, "gma500: pin failed: %d\n", ret);goto fail;}r->mmapping = 1;}/* Page relative to the VMA start - we must calculate this ourselvesbecause vmf->pgoff is the fake GEM offset */page_offset = ((unsigned long) vmf->virtual_address - vma->vm_start)>> PAGE_SHIFT;/* CPU view of the page, don't go via the GART for CPU writes */// 当使用显存时,pfn直接对应stolen_base中相应的page,这些page是从显存中映射过来的,通常由BIOS完成。if (r->stolen)pfn = (dev_priv->stolen_base + r->offset) >> PAGE_SHIFT;else// 当使用GTT内存时,从gtt_range->pages中取相应page的物理地址,这些page是在psb_gtt_pin()中分配的。pfn = page_to_pfn(r->pages[page_offset]);// 将取得的物理地址对应pfn,写入vma中,同时会修改相应页表,完成物理地址到虚拟地址的映射。ret = vm_insert_pfn(vma, (unsigned long)vmf->virtual_address, pfn);fail:mutex_unlock(&dev->struct_mutex);switch (ret) {case 0:case -ERESTARTSYS:case -EINTR:return VM_FAULT_NOPAGE;case -ENOMEM:return VM_FAULT_OOM;default:return VM_FAULT_SIGBUS;}
}

相关流程见注释,其中最关键的是psb_gtt_pin(),其中完成内存分配,实现如下:

/***	psb_gtt_pin		-	pin pages into the GTT*	@gt: range to pin**	Pin a set of pages into the GTT. The pins are refcounted so that*	multiple pins need multiple unpins to undo.**	Non GEM backed objects treat this as a no-op as they are always GTT*	backed objects.*/
int psb_gtt_pin(struct gtt_range *gt)
{int ret = 0;struct drm_device *dev = gt->gem.dev;struct drm_psb_private *dev_priv = dev->dev_private;u32 gpu_base = dev_priv->gtt.gatt_start;mutex_lock(&dev_priv->gtt_mutex);// Stolen==0表示使用gtt内存,而不使用显存if (gt->in_gart == 0 && gt->stolen == 0) {// 分配物理内存,分配后的page放在了gtt_range->pages中ret = psb_gtt_attach_pages(gt);if (ret < 0)goto out;ret = psb_gtt_insert(dev, gt, 0);if (ret < 0) {psb_gtt_detach_pages(gt);goto out;}/* 从GPU的角度,将分配的物理内存对应的物理地址写入GPU的MMU中,* 即修改GPU的页表,将主存中的page(可以不连续)映射到GPU的虚拟* 地址空间中。这样GPU就可以访问这些内存了,作为Framebuffer使用。*/psb_mmu_insert_pages(psb_mmu_get_default_pd(dev_priv->mmu),gt->pages, (gpu_base + gt->offset),gt->npage, 0, 0, PSB_MMU_CACHED_MEMORY);}gt->in_gart++;
out:mutex_unlock(&dev_priv->gtt_mutex);return ret;
}

这里面有两个关键动作:

  1. psb_gtt_attach_pages完成内存分配。
  2. psb_mmu_insert_pages完成GPU中的地址映射,使分配的内存对GPU可见。

内存分配的后续流程为:

psb_gtt_attach_pagesdrm_gem_get_pagesshmem_read_mapping_pageshmem_getpage_gfpshmem_alloc_page  alloc_page_vmaalloc_pageshmem_add_to_page_cache ///由page cache中进行管理	

至此,完成gma500显卡驱动中dri设备的mmap的实现分析,其中包括了显卡驱动中内存管理相关的关键操作,理解后,相信理解其他显卡驱动应该不会有大问题。


http://www.ppmy.cn/news/714176.html

相关文章

计算机显卡内存分布查看,Win10电脑查看显卡内存的详细教程

许多朋友买电脑时都看重电脑显卡大小&#xff0c;有时视频分辨率和显卡是有很大关系&#xff0c;那要想查看Win10电脑显卡内存应该怎么查看呢&#xff1f;详细查看方法如下所示。 方法一 1、鼠标左键点击左下方的Win10图标&#xff0c;弹出下拉菜单&#xff0c;在下拉菜单里找到…

【深度分析】如何配置深度学习用的电脑(显卡和内存的选择标准)

一、显卡指标 位宽 : 一个时钟周期内&#xff0c;显示芯片从显存中读取数据量&#xff0c;单位bit&#xff1b;显存频率&#xff1a;表示1s内显存和显示芯片之间数据传输次数&#xff0c; 单位MHZ&#xff1b;显存带宽&#xff1a;表示1s内显存与显卡芯片之间的的数据传输量&a…

CSS文本溢出显示省略号

1、单行文本溢出显示省略号 overflow: hidden;&#xff08;文字长度超出限定宽度&#xff0c;则隐藏超出的内容&#xff09; white-space: nowrap;&#xff08;设置文字在一行显示&#xff0c;不能换行&#xff09; text-overflow: ellipsis;&#xff08;规定当文本溢出时&…

s3 存储操作命令 与 GitHub CI 集成

s3 存储操作命令 与 GitHub CI 集成 下载安装 Mac本地安装方法&#xff1a; # bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"brew install s3cmds3cmd --version s3cmd version 2.2.0参考链接…

快快网络I9-13900K性能优势

快快网络最新推出的I9-13900K处理器以其出色的性能和卓越的表现在科技界引起了广泛关注。作为一款高性能处理器&#xff0c;它在多个方面都表现出了明显的优势。 高性能处理能力 I9-13900K采用了先进的7纳米制程技术&#xff0c;拥有8核16线程的配置&#xff0c;主频高达5.3G…

JavaScript(JS)的引入方法

内部脚本 JS代码必须位于<script></script>标签之间在HTML文档中&#xff0c;可以在任意地方&#xff0c;放置任意数量的<script>一般会把脚本置于<body>元素的底部&#xff0c;可以改善显示速度 外部脚本&#xff1a;将JS代码定义在外部JS文件中&…

VS Code报错 No module named ‘torch‘ (但已经安装了pytorch)

一、复现错误程序 创建一个python文件 test.py&#xff0c;其内容为&#xff1a; import torch print(torch.__version__)使用VS Code打开并运行该程序时&#xff0c;会出现以下错误&#xff1a; ModuleNotFoundError: No module named ‘torch’ 二、解决方案 选择适合的Pyt…

搜狗输入法很好用?

搜狗输入法是一款我用了很多年的输入法软件。刚开始接触电脑之前&#xff0c;那会国内普遍使用的是五笔输入法&#xff0c;所以就为了随大众去熟悉五笔口诀&#xff0c;记得死去活来。后来&#xff0c;有一款标准输入法&#xff0c;但是按照那会来说标准输入法的打字速度说是没…