1. What is VFIO?

  1. VFIO是一个可以安全的把设备I/O、中断、DMA等暴露到用户空间(userspace),从而可以在用户空间完成设备驱动的框架。

  2. 得益于vfio低开销的用户空间直接设备访问,虚拟机设备分配(device assignment)、高性能应用等可以获得更高的I/O性能。

2. IOMMU

实现用户空间设备驱动,最困难的在于如何将DMA以安全可控的方式暴露到用户空间:

  1. 提供DMA的设备通常可以写内存的任意页,因此使用户空间拥有创建DMA的能力就等同于用户空间拥有了root权限,恶意的设备可能利用此发动DMA攻击。

  2. I/O memory management unit(IOMMU)的引入对设备进行了限制,设备I/O地址需要经过IOMMU重映射为内存物理地址,如图2-1。恶意的或存在错误的设备不能读写没有被明确映射过的内存,运行在cpu上的操作系统以互斥的方式管理MMU与IOMMU,物理设备不能绕行或污染可配置的内存管理表项。

图 2-1 Comparison IOMMU and MMU

IOMMU其他好处:

  1. IOMMU可以将连续的虚拟地址映射到不连续的多个物理内存片段,从而支持vectored I/O(scatter-gather list);

  2. 对于不能寻址全部物理地址空间的设备,通过IOMMU的重映射,从而避免了将数据从设备可访问的外围地址空间拷入拷出设备无法访址的物理地址空间的额外开销(避免了bounce buffer)。

3. Device,group, container

由于device本身的特性、互连(interconnect)及IOMMU的拓扑等,IOMMU提供device 隔离(ioslation)的最小粒度是group,而不是device。如一个pci device可能包括多个function,而这些function之间数据传递可以通过专用通道(backdoor),而不经过IOMMU等等,所以device并不适合做隔离的最小单元。

container可以包含多个group,这些group共享页表信息。

4. vfio use example

详看[linux-rootdir]/Documentation/vfio.txt

与链接:vfio_device_test.c

  1. Linux kernel实现

5.1. 相关内核组件

5.1.1 内核组件概图

5-1vfio内核组件概图

图5-1 vfio内核组件概图

vfio interface:vfio通过设备文件向userspace提供统一访问接口,包括container、group、device。

vfio_iommu_driver:为vfio提供了IOMMU重映射驱动,即向用户空间暴露DMA操作,如container的ioctl选项VFIO_IOMMU_MAP_DMA即由vfio containter设备文件对应的 file_operations 的ioctl转发到vfio_iommu_driver的ioctl实现,已实现的vfio_iommu_driver包括vfio_iommu_type1、vfio_spapr_eeh等,这里重点分析vfio_iommu_type1。

vfio-pcivfio支持pci设备pass-through,vfio-pci作为pci driver挂载到pci总线,提供将pci设备io、interrupt暴露到用户空间实现。

5.1.2. 各组件之间如何关联

图3是vfio关键数据结构概图:

图5-2 vfio关键数据结构概图

注:M表示一对多

  1. 设备文件入口

a) Container

userspace通过open设备文件/dev/vfio/vfio获得container对应的文件描述符:

/* Create a new container */

int container = open("/dev/vfio/vfio", O_RDWR);

文件标识符container关联struct file_operations vfio_fops,这是在vfio/vfio.c中通过注册miscdevice实现的:

1793 static struct miscdevice vfio_dev = {

1794 .minor = VFIO_MINOR,

1795 .name = "vfio",

1796 .fops = &vfio_fops,

1797 .nodename = "vfio/vfio",

1798 .mode = S_IRUGO | S_IWUGO,

1799 };

b) Group

userspace通过open设备文件/dev/vfio/{group-num}获得group对应的文件描述符,假设

group-num为26:

/* Open the group */

group = open("/dev/vfio/26", O_RDWR);

其通过注册字符设备关联struct file_operations vfio_group_fops实现:

1567 static const struct file_operations vfio_group_fops = {

1568 .owner = THIS_MODULE,

1569 .unlocked_ioctl = vfio_group_fops_unl_ioctl,

1570 #ifdef CONFIG_COMPAT

1571 .compat_ioctl = vfio_group_fops_compat_ioctl,

1572 #endif

1573 .open = vfio_group_fops_open,

1574 .release = vfio_group_fops_release,

1575 };

接下来,userspace通过group fd关联的ioctl选项 VFIO_GROUP_SET_CONTAINER,

将group加入container:

/* Add the group to the container */

ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);

c) Device

与container和group不同,device的设备文件并不暴露在userspace,userspace通过group fd关联的ioctl选项VFIO_GROUP_GET_DEVICE_FD,得到device对应文件描述符,如:

/* Get a file descriptor for the device */

device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");

在kernel中,通过group对应的ioctl==》vfio_group_get_device_fd:

- 遍历group得到对应vfio_device后,为device分配fd及struct file,并使两者关联,其中struct file关联的是匿名的inode。

Device fd关联的struct file_operations为vfio_device_fops:

1646 static const struct file_operations vfio_device_fops = {

1647 .owner = THIS_MODULE,

1648 .release = vfio_device_fops_release,

1649 .read = vfio_device_fops_read,

1650 .write = vfio_device_fops_write,

1651 .unlocked_ioctl = vfio_device_fops_unl_ioctl,

1652 #ifdef CONFIG_COMPAT

1653 .compat_ioctl = vfio_device_fops_compat_ioctl,

1654 #endif

1655 .mmap = vfio_device_fops_mmap,

1656 };

另外,vfio_device_fops方法实现主要是封装了vfio_device对应的struct_device_ops,

该方法在创建vfio_device时赋值。

2) vfio-pci如何与vfio interface关联?

vfio-pci实际上被注册为pci driver,在文件vfio/pci/vfio_pci.c:

1206 static struct pci_driver vfio_pci_driver = {

1207 .name = "vfio-pci",

1208 .id_table = NULL, /* only dynamic ids */

1209 .probe = vfio_pci_probe,

1210 .remove = vfio_pci_remove,

1211 .err_handler = &vfio_err_handlers,

1212 };

1361 /* Register and scan for devices */

1362 ret = pci_register_driver(&vfio_pci_driver);

插入新的pci设备或驱动,并完成pci_bus_match之后,会调用pci driver对应的probe方法,在函数vfio_pci_probe中,会创建vfio_group、vfio_device并把vfio_device加入vfio_group链表,

In function vfio_pci_probe:==>vfio_add_group_dev:==>vfio_create_group分配vfio_group数据结构并完成初始化,并在初始化过程中使vfio_group指针关联idr整数(idr是内核提供的一种整数关联指针的机制,idr类似于身份证,唯一关联对应指针):

In file vfio/vfio.c:

267 * Group minor allocation/free - both called with vfio.group_lock held

268 */

269 static int vfio_alloc_group_minor(struct vfio_group *group)

270 {

271 return idr_alloc(&vfio.group_idr, group, 0, MINORMASK + 1, GFP_KERNEL);

272 }

接下来是vfio interfacevfio-pci关联的关键一步:

在打开group设备文件,调用对应file_operations的open方法时,In function vfio_group_fops_open中通过idr得到在vfio-pci初始化时创建的vfio_group:

467 static struct vfio_group *vfio_group_get_from_minor(int minor)

468 {

469 struct vfio_group *group;

470

471 mutex_lock(&vfio.group_lock);

472 group = idr_find(&vfio.group_idr, minor);

473 if (!group) {

474 mutex_unlock(&vfio.group_lock);

475 return NULL;

476 }

477 vfio_group_get(group);

478 mutex_unlock(&vfio.group_lock);

479

480 return group;

481 }

综上,vfio-pci被实现为pci driver,在初始化时创建vfio_groupvfio_device,并使vfio_group关联idr,而在打开group设备文件时,再通过idr获得已分配的vfio_group,从而将vfio interfacevfio-pci关联起来。

3) vfio_iommu_driver如何与vfio interface关联?

Userspace通过显式的ioctl调用为container关联对应的vfio_iommu_driver,如:

/* Enable the IOMMU model we want */

ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);

在内核中会遍历已注册的vfio_iommu_driver,并赋给vfio_container对应成员。

5.2. DMA暴露到userspace

实现vfio,DMA是最棘手的部分,IOMMU的引入解决了将DMA暴露到用户空间的安全性问题。

Userspace设置IOMMU的重映射的方式如下:

/* Allocate some space and setup a DMA mapping */

dma_map.vaddr = mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,

MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

dma_map.size = 1024 * 1024;

dma_map.iova = 0; /* 1MB starting at 0x0 from device view */

dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;

ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);

以上将container包含的所有设备的设备地址空间[0,1MB)映射到物理内存。

注:iova:I/O虚拟地址,用作配置DMA的地址

vaddr:用户空间虚拟地址

对于ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map),查看container设备文件对应的ioctl,该选项被转发到container关联的vfio_iommu_driver对应的ioctl,查看vfio_iommu_type1对应的ioctl实现:

In function vfio_iommu_type1_ioctl:==>vfio_dma_do_map:

- 如图5-2,在数据结构vfio_iommu中包含一个红黑树(struct rb_root dma_list),用于记录(iova, vaddr)的映射;在vfio_dma_do_map中首先检查dma_list,确保已映射的dma区域不与传入的、请求映射到iova的区域重叠;

- 接着转换得到用户空间虚拟地址vaddr对应的物理地址,并pin对应的内存区域;

- 最后,将(iova, pfn)传给iommu组件完成映射。

图5-2包含vfio_iommu_type1实现关键的数据结构:vfio_iommu, vfio_domain, vfio_group, vfio_dma等:

- iommu_domain, iommu_group是更底层iommu驱动组件抽象的数据结构;

- domain是一组资源的集合,包含了物理内存及可访问的设备, 同一个domain中的设备共享页表区域,container与domain的区别在于:

- container是vfio interface使用的概念,domain是更底层iommu_driver使用的概念;

containerdomain并不是对等的,一个container内的设备可能划分到多个domain之中由于处在同一个container中的设备共享页表,每个(iova,pfn)的映射必须同时传递到所有的domain中。

vfio_iommu_map是将(iova, pfn)传递给iommu的关键函数,其遍历所有已划分的

domain,建立(iova,pfn)映射表:

In file vfio_iommu_type1.c

534 static int vfio_iommu_map(struct vfio_iommu *iommu, dma_addr_t iova,

535 unsigned long pfn, long npage, int prot)

536 {

537 struct vfio_domain *d;

538 int ret;

539

540 list_for_each_entry(d, &iommu->domain_list, next) {

541 ret = iommu_map(d->domain, iova, (phys_addr_t)pfn << PAGE_SHIFT,

542 npage << PAGE_SHIFT, prot | d->prot);

543 if (ret) {

544 if (ret != -EBUSY ||

545 map_try_harder(d, iova, pfn, npage, prot))

546 goto unwind;

547 }

548

549 cond_resched();

550 }

551

552 return 0;

...

559 }

5.3. I/O暴露到userspace

I/O暴露到userspace比较简单,只是把I/O物理地址remap到userspace,对于pci设备包括pci config space、bar等。

在userspace可按照如下方式访问I/O区域:

/* Get a file descriptor for the device */

device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");

/* Test and setup the device */

ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);

for (i = 0; i < device_info.num_regions; i++) {

struct vfio_region_info reg = { .argsz = sizeof(reg) };

reg.index = i;

ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &reg);

/* Setup mappings... read/write offsets, mmaps

* For PCI devices, config space is a region */

}

首先分析ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &reg),经device fd对应的struct file_operations vfio_device_fops{.ioctl} ==> struct vfio_device{vfio_device_ops{.ioctl}} :

1086 static const struct vfio_device_ops vfio_pci_ops = {

1087 .name = "vfio-pci",

1088 .open = vfio_pci_open,

1089 .release = vfio_pci_release,

1090 .ioctl = vfio_pci_ioctl,

1091 .read = vfio_pci_read,

1092 .write = vfio_pci_write,

1093 .mmap = vfio_pci_mmap,

1094 .request = vfio_pci_request,

1095 };

这里分析pci bar,其他的io region重映射方式类似;

581 case VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX:

582 info.offset = VFIO_PCI_INDEX_TO_OFFSET(info.index);

583 info.size = pci_resource_len(pdev, info.index);

584 if (!info.size) {

585 info.flags = 0;

586 break;

587 }

588

589 info.flags = VFIO_REGION_INFO_FLAG_READ |

590 VFIO_REGION_INFO_FLAG_WRITE;

591 if (IS_ENABLED(CONFIG_VFIO_PCI_MMAP) &&

592 pci_resource_flags(pdev, info.index) &

593 IORESOURCE_MEM && info.size >= PAGE_SIZE) {

594 info.flags |= VFIO_REGION_INFO_FLAG_MMAP;

595 if (info.index == vdev->msix_bar) {

596 ret = msix_sparse_mmap_cap(vdev, &caps);

597 if (ret)

598 return ret;

599 }

600 }

601

602 break;

==>VFIO_PCI_INDEX_TO_OFFSET

22 #define VFIO_PCI_OFFSET_SHIFT 40

23

24 #define VFIO_PCI_OFFSET_TO_INDEX(off) (off >> VFIO_PCI_OFFSET_SHIFT)

25 #define VFIO_PCI_INDEX_TO_OFFSET(index) ((u64)(index) << VFIO_PCI_OFFSET_SHIFT)

26 #define VFIO_PCI_OFFSET_MASK (((u64)(1) << VFIO_PCI_OFFSET_SHIFT) - 1)

pci bar对应的offset只是index <<40,而当userspace通过mmap、read/write等访问对应区域时,对于传入的参数ppos,ppos低40位存储了实际的偏移量,ppos >> 40即可得到pci bar对应的index,有了这个index,再通过pci_resource_start、pci_resoucre_end、pci_resource_len等就可得到pci bar io region对应的开始地址、结束地址、长度等信息,查看mmap实现:

==>vfio_pci_mmap

1001 static int vfio_pci_mmap(void *device_data, struct vm_area_struct *vma)

1002 {

...

1009 index = vma->vm_pgoff >> (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT);

1010

...

1020 phys_len = pci_resource_len(pdev, index);

1021 req_len = vma->vm_end - vma->vm_start;

1022 pgoff = vma->vm_pgoff &

1023 ((1U << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1);

1024 req_start = pgoff << PAGE_SHIFT;

...

1058 vma->vm_private_data = vdev;

1059 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

1060 vma->vm_pgoff = (pci_resource_start(pdev, index) >> PAGE_SHIFT) + pgoff;

1061

1062 return remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,

1063 req_len, vma->vm_page_prot);

1064 }

Line 1009之所以是(VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)而不是(VFIO_PCI_OFFSET_SHIFT)是因为vma->vm_pgoff表示的是页粒度的偏移量(offset of the area in the file, in pages)。

Line 1060得到实际要访问的io区域,最后remap_pfn_range负责建立该物理区域的页表。

5.4. interrupt暴露到userspace

将interrupt暴露到userspace,利用了linux提供的系统调用eventfd。

int eventfd(unsigned int initval, int flags);

eventfd创建了一个文件描述符用于事件通知:

- 创建了一个’eventfd object’,可以作为userspace应用程序间事件wait/notify机制;

- 也可以用于内核向userspace事件通知;

- 内核维护一个64- bit的integer counter,可由’initval’赋初值;在userspace,当read时:

- 若未置EFD_SEMAPHORE位,counter为非零值,则返回8bytes的counter值,

并将该counter reset为零;

- 如counter为0, 若置EFD_NONBLOCK,则返回error EAGAIN,否则阻塞。

- 在kernel space, 维护了struct eventfd_ctx数据结构, 可以通过调用eventfd_signal()函数增加eventfd counter, 并通知用户空间;

- 在userspace, eventfd返回的文件描述符支持select/poll/epoll,从而实现kernel到userspace的异步事件通知;

- eventfd在userspace的用法可参照http://linux.die.net/man/2/eventfd

- eventfd在kernel的使用参照linux kernel源代码[linux-kernel-root]/fs/eventfd.c.

for (i = 0; i < device_info.num_irqs; i++) {

struct vfio_irq_info irq = { .argsz = sizeof(irq) };

irq.index = i;

ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &info);

/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQ */

int irqfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

....

irq_set->data = irqfd;

ioctl(device, VFIO_DEVICE_SET_IRQ, irq_set);

/* for example , create a pthread for waiting irqfd */

pthread_create(&irq_thread[i], NULL, irq_handler_func, NULL);

}

static irq_handler_func(void *arg)

{

.....

/* select/poll/epoll to wait */

...

read(irqfd,...);

}

在userspace可以按照以下方式设置与处理中断:

- 首先通过ioctl(VFIO_DEVICE_GET_IRQ_INFO)得到中断信息,

- 接着调用eventfd得到文件描述符irqfd,然后通过ioctl(VFIO_DEVICE_SET_IRQ)设置中断,使该文件描述符与中断关联。

- 接下来, userspace可以创建一个新的线程(可选的实现),在该线程中将irqfd加入select/poll/epoll的等待队列,

- 当接收到kernel space事件通知时,调用read(irqfd,...)减少eventfd counter,接着转发到Userspace设备驱动完成中断处理

在kernel space, 可以看一下ioctl(VFIO_DEVICE_SET_IRQ)实现,这里以pci msix中断为例。

vfio_pci_ioctl()==>vfio_pci_set_irqs_ioctl() ==> msix:vfio_pci_set_msi_trigger()

==>vfio_msi_set_block==>vfio_set_block_vector_signal():

308 static int vfio_msi_set_vector_signal(struct vfio_pci_device *vdev,

309 int vector, int fd, bool msix)

310 {

311 struct pci_dev *pdev = vdev->pdev;

312 struct eventfd_ctx *trigger;

313 int irq, ret;

314

315 if (vector < 0 || vector >= vdev->num_ctx)

316 return -EINVAL;

317

318 irq = msix ? vdev->msix[vector].vector : pdev->irq + vector;

...

337 trigger = eventfd_ctx_fdget(fd);

338 if (IS_ERR(trigger)) {

339 kfree(vdev->ctx[vector].name);

340 return PTR_ERR(trigger);

341 }

...

357 ret = request_irq(irq, vfio_msihandler, 0,

358 vdev->ctx[vector].name, trigger);

359 if (ret) {

360 kfree(vdev->ctx[vector].name);

361 eventfd_ctx_put(trigger);

362 return ret;

363 }

...

Line 337调用eventfd_ctx_fdget()由fd得到该文件描述符对应的struct eventfd_ctx, line 357注册中断,并将该eventfd_ctx作为参数传给中断处理函数vfio_msihandler():

239 /*

240 * MSI/MSI-X

241 */

242 static irqreturn_t vfio_msihandler(int irq, void *arg)

243 {

244 struct eventfd_ctx *trigger = arg;

245

246 eventfd_signal(trigger, 1);

247 return IRQ_HANDLED;

248 }

在中断发生时,在中断处理函数中会调用eventfd_signal(), userspace接到通知,等待的select/poll/epoll会返回,接着利用read()递减eventfd关联的counter,并调用userspace中的设备driver完成实际的中断处理任务。

6. 总结

这篇文档着重介绍了vfio和iommu的概念,并对vfio使用及linux kernel实现进行的分析。

在分析linux kernel实现时,除介绍vfio各模块组织架构及如何关联外,着重从如何将DMA暴露到userspace、如何将I/O暴露到userspace、如何将中断暴露到userspace等三个方面分析了vfio实现。

对于vfio如何使用,可以参考第4部分vfio use example,及[qemu-root]/hw/vfio/pci.c。

VFIO Introduction相关推荐

  1. ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)

    名词缩写 ASID:Address Space ID   地址空间标识符 CD:Context Descriptor:  上下文描述符: CTP:Context-table pointer   上下文 ...

  2. Blender 3.0基础入门学习教程 Introduction to Blender 3.0

    成为Blender通才,通过这个基于项目的循序渐进课程学习所有主题的基础知识. 你会学到什么 教程获取:Blender 3.0基础入门学习教程 Introduction to Blender 3.0- ...

  3. 网络增强现实开发简介 Introduction to Web AR development

    搭配webXR.mindAR.three.js和tensorflow.js 你会学到: 获得构建不同类型的网络增强现实应用程序的实践经验,包括图像效果.人脸效果和世界效果 获得关于增强现实如何在网络浏 ...

  4. ZBrush全面入门学习教程 Schoolism – Introduction to ZBrush

    ZBrush全面入门学习教程 Schoolism – Introduction to ZBrush ZBrush全面入门学习教程 Schoolism – Introduction to ZBrush ...

  5. 视频色彩校正简介 Introduction to Video Color Correction

    视频色彩校正简介 Introduction to Video Color Correction 视频色彩校正简介 Introduction to Video Color Correction MP4 ...

  6. [转]Introduction of iSCSI Target in Windows Server 2012

    Introduction of iSCSI Target in Windows Server 2012 源地址:http://blogs.technet.com/b/filecab/archive/2 ...

  7. MS UI Automation Introduction

    MS UI Automation Introduction 2014-09-17 MS UI Automation是什么 UIA架构 UI自动化模型 UI自动化树概述 UI自动化控件模式概述 UI 自 ...

  8. 音频(3):iPod Library Access Programming Guide:Introduction

    Next Introduction 介绍 iPod库访问(iPod Library Access)让应用程序可以播放用户的歌曲.有声书.和播客.这个API设计使得基本播放变得非常简单,同时也支持高级的 ...

  9. 2018-3-25论文(Whale Optimizer Algorithm)+(Gery Wolf Optimizer)笔记二---Introduction 对比

    代码以及文论的来源: Seyedali Mirjalili http://www.alimirjalili.com/Projects.html 感谢作者!!! 2014年GWO Introductio ...

最新文章

  1. SQLAlchemy的使用---外键ForeignKey数据库创建与连接
  2. x的平方根—leetcode69
  3. java信用分秒杀系统设计思路,秒杀系统设计思路
  4. yii框架phpexcel
  5. linux两台服务器传输,Linux两台服务器之间高速数据传输命令:scp应用详解
  6. 【AI视野·今日CV 计算机视觉论文速览 第242期】Mon, 14 Feb 2022
  7. php做网页的流畅,Easying轻量流畅
  8. Handbook of Constraints Programming——Chapter4 Backtracking Search Algorithms-Preliminaries
  9. 给Fedora11安装五笔
  10. 清空数据库中的某个表中数据
  11. 文件服务器minio
  12. win10下安装 迅雷精简版,提示阻止此应用
  13. Spring Boot 接入支付宝完整流程实战,看完后秒懂!
  14. R语言系统教程(六):描述统计量
  15. 浅谈智慧校园建设中存在的问题及解决方案
  16. 【图】图的一般表示法以及其他表示法转化为一般表示法
  17. 踩坑记-- UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0xa6 in position 17: illegal multibyte seque
  18. 使用普通打印机打印条码标签
  19. Linux Ubuntu 安装 Realtek 8812BU无线网卡
  20. ❤️连续面试失败后,我总结了57道面试真题❤️,如果时光可以倒流...(附答案,建议收藏)

热门文章

  1. 【转】JavaScript入门学习书籍的阶段选择——BY怿飞
  2. 新思维研究生英语第1-12单元 课文翻译习题答案
  3. 【慕课笔记】第五章 JAVA中的集合框架(中) 第1节 MapHashMap简介
  4. 南邮部分期末复习笔记汇总@tou
  5. xs128之OLED12864
  6. 假如再有三年生命,世界的教育改革家--乔布斯
  7. O - 期末考试之排名次
  8. 四旋翼无人机学习第2节--cadence工程创建与原理图的添加
  9. 大数据求签,人工智能算命,技术革新下传统行业还有灵魂吗
  10. 主窗口(08):【类】QMdiSubWindow [官翻]