祝各位道友念头通达
GitHub Gitee 语雀 打赏

PCIE驱动踩坑

1. 推荐的文章和博客,文章是作者个人原创,为扫盲做准备

https://www.cnblogs.com/LoyenWang/p/14165852.html
https://www.kernel.org/doc/html/latest/driver-api/pci/p2pdma.html

另外如何在linux下查看PCIE设备是否检测到

  1. dmesg | grep [VENDOR|deviceId], 比如VENDOR为 10ee, id为 7038
[    0.246854] pci 0000:01:00.0: [10ee:7038] type 00 class 0x118000
[    0.246874] pci 0000:01:00.0: reg 0x10: [mem 0x58400000-0x587fffff]
[    0.246882] pci 0000:01:00.0: reg 0x14: [mem 0x58000000-0x583fffff]
[    0.246988] pci 0000:01:00.0: 16.000 Gb/s available PCIe bandwidth, limited by 2.5 GT/s x8 link at 0000:00:00.0 (capable of 63.008 Gb/s with 8 GT/s x8 link)
  1. lspci -tv 查看pcie总线结构和设备

  2. sudo lspci -s xxx:00.0 -vv 查看PCIE 设备详细信息, 需要用root权限才能查看的更详细的信息

  3. setpci 使用改指令可以在命令行下修改对pcie寄存器的配置和读取

sudo setpci --dumpregs # 查看可以配置寄存器
setpci -s 12:00.0 08.l #b|w|l, 1字节/2字节/4字节, 08代表的就是 下入所示 byte offset
  1. /sys/bus/pci/xxxx:00.0/ 下, 目录如图所示, 显示了pcie相关中断,bar等资源配置属性
    image
    使用如下指令对 bar 地址的读写, resource(0 ~2) 对应的是bar0 到 bar3 的地址, 可以查看MSI-X 向量表配置
    sudo ./pcimem /sys/devices/pci0000:00/0000:00:04.0/0000:12:00.0/resource2 0x0 w
pcimem源码
/*
 * pcimem.c: Simple program to read/write from/to a pci device from userspace.
 *
 *  Copyright (C) 2010, Bill Farrow (bfarrow@beyondelectronics.us)
 *
 *  Based on the devmem2.c code
 *  Copyright (C) 2000, Jan-Derk Bakker (J.D.Bakker@its.tudelft.nl)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>

#define PRINT_ERROR \
	do { \
		fprintf(stderr, "Error at line %d, file %s (%d) [%s]\n", \
		__LINE__, __FILE__, errno, strerror(errno)); exit(1); \
	} while(0)


int main(int argc, char **argv) {
	int fd;
	void *map_base, *virt_addr;
	uint64_t read_result, writeval, prev_read_result = 0;
	char *filename;
	off_t target, target_base;
	int access_type = 'w';
	int items_count = 1;
	int verbose = 0;
	int read_result_dupped = 0;
	int type_width;
	int i;
	int map_size = 4096UL;

	if(argc < 3) {
		// pcimem /sys/bus/pci/devices/0001\:00\:07.0/resource0 0x100 w 0x00
		// argv[0]  [1]                                         [2]   [3] [4]
		fprintf(stderr, "\nUsage:\t%s { sysfile } { offset } [ type*count [ data ] ]\n"
			"\tsys file: sysfs file for the pci resource to act on\n"
			"\toffset  : offset into pci memory region to act upon\n"
			"\ttype    : access operation type : [b]yte, [h]alfword, [w]ord, [d]ouble-word\n"
			"\t*count  : number of items to read:  w*100 will dump 100 words\n"
			"\tdata    : data to be written\n\n",
			argv[0]);
		exit(1);
	}
	filename = argv[1];
	target = strtoul(argv[2], 0, 0);

	if(argc > 3) {
		access_type = tolower(argv[3][0]);
		if (argv[3][1] == '*')
			items_count = strtoul(argv[3]+2, 0, 0);
	}

        switch(access_type) {
		case 'b':
			type_width = 1;
			break;
		case 'h':
			type_width = 2;
			break;
		case 'w':
			type_width = 4;
			break;
                case 'd':
			type_width = 8;
			break;
		default:
			fprintf(stderr, "Illegal data type '%c'.\n", access_type);
			exit(2);
	}

    if((fd = open(filename, O_RDWR | O_SYNC)) == -1) PRINT_ERROR;
    printf("%s opened.\n", filename);
    printf("Target offset is 0x%x, page size is %ld\n", (int) target, sysconf(_SC_PAGE_SIZE));
    fflush(stdout);

    target_base = target & ~(sysconf(_SC_PAGE_SIZE)-1);
    if (target + items_count*type_width - target_base > map_size)
	map_size = target + items_count*type_width - target_base;

    /* Map one page */
    printf("mmap(%d, %d, 0x%x, 0x%x, %d, 0x%x)\n", 0, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, (int) target);

    map_base = mmap(0, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, target_base);
    if(map_base == (void *) -1) PRINT_ERROR;
    printf("PCI Memory mapped to address 0x%08lx.\n", (unsigned long) map_base);
    fflush(stdout);

    for (i = 0; i < items_count; i++) {

        virt_addr = map_base + target + i*type_width - target_base;
        switch(access_type) {
		case 'b':
			read_result = *((uint8_t *) virt_addr);
			break;
		case 'h':
			read_result = *((uint16_t *) virt_addr);
			break;
		case 'w':
			read_result = *((uint32_t *) virt_addr);
			break;
                case 'd':
			read_result = *((uint64_t *) virt_addr);
			break;
	}

    	if (verbose)
            printf("Value at offset 0x%X (%p): 0x%0*lX\n", (int) target + i*type_width, virt_addr, type_width*2, read_result);
        else {
	    if (read_result != prev_read_result || i == 0) {
                printf("0x%04X: 0x%0*lX\n", (int)(target + i*type_width), type_width*2, read_result);
                read_result_dupped = 0;
            } else {
                if (!read_result_dupped)
                    printf("...\n");
                read_result_dupped = 1;
            }
        }
	
	prev_read_result = read_result;

    }

    fflush(stdout);

	if(argc > 4) {
		writeval = strtoull(argv[4], NULL, 0);
		switch(access_type) {
			case 'b':
				*((uint8_t *) virt_addr) = writeval;
				read_result = *((uint8_t *) virt_addr);
				break;
			case 'h':
				*((uint16_t *) virt_addr) = writeval;
				read_result = *((uint16_t *) virt_addr);
				break;
			case 'w':
				*((uint32_t *) virt_addr) = writeval;
				read_result = *((uint32_t *) virt_addr);
				break;
			case 'd':
				*((uint64_t *) virt_addr) = writeval;
				read_result = *((uint64_t *) virt_addr);
				break;
		}
		printf("Written 0x%0*lX; readback 0x%*lX\n", type_width,
		       writeval, type_width, read_result);
		fflush(stdout);
	}

	if(munmap(map_base, map_size) == -1) PRINT_ERROR;
    close(fd);
    return 0;
}

image

2. PCIE 整体如下图

PCIE设备总共有三种, 主设备(只能有一个), 从设备(多个), PCI桥(多个),
另外PCI/PCIE是接口, 而NVME是协议, 该协议可以基于PCI/PCIE接口实现, 当然也可以基于其它接口, 这个得看硬件结构, 这个类似udp和RGMII接口, UDP是协议,其接口还可以是光口等
image

3. 问题来了, PCIE驱动到底要做什么事儿,怎么去做

我们都晓得 英特尔 i40e 支持40GB 网络, 40G网络是走光纤的高速口
通过查看 i40e 驱动函数来看,40G网络也是PCIE的接口, 其实就是一个PCIE的驱动函数, 当然没有表面看起来那么简单
通过查找PCIE设备ID, 不同的设备ID代表了不同功能的网卡, 这个在官方的pdf设备当中能够看到
image

4. 深度理解, PCIE驱动

PCIE协议的传输特别类似网络传输模式,只不过PCIE相对于跟硬件关联更紧密
从 标题2 中可以看出来, PCIE可以外接很多设备

  1. 从linux系统来看: PCIE驱动就像一个应用层的程序, 这个程序可以控制你这个硬件能够做什么事,然后提供给系统或者是用户态去使用
  2. 硬件来看: PCIE驱动包含好几个方面, PCIE从设备可以用FPGA去开发调试, 也可以用arm配置专门的PCIE芯片来实现, 然后linux系统在用内核提供的PCIE API接口,开发驱动去和硬件之间沟通实现数据的高速传输
  3. 既然是高速口,那PCIE设备外接的一般是光纤, ssd存储板, dsp, ad 等高速口

5. 再深层次的理解, PCIE 数据交换的三种形式

  1. I/O 交换数据暂且不谈,效率很低,一般不会用
  2. DMA-ram, DMA到ram, cpu再处理ram的数据
  3. peer-to-peer:这个是pci设备之间的DMA, pcie专有性能

6. 疑问

  1. DMA是由pci_alloc_consistent 申请来的(暂时就了解到这一种), 那内存到硬件设备的物理地址是如何映射的,大小是怎么决定的(疑问)
  2. 数据到底是怎么从从设备传输到主设备的, DMA起到什么样的作用
  3. 多个从设备的物理内存如何和CPU内存映射的

7. 关于pci_alloc_consistent 申请流程(x86平台)linux内核版本3.10(不通内核版本会有些差别)

针对 x86 centos 系统源码解读, 不同平台可能会有所不同

  1. pci_alloc_consistent 如何申请到对应DMA内存, 他的大小由什么决定
    pci-dma-compat.h
static inline void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle) {
   return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev, size, dma_handle, GFP_ATOMIC);
}

dma-mapping.h

static inline void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag) {
   return dma_alloc_attrs(dev, size, dma_handle, flag, NULL);
}
static inline void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag, struct dma_attrs *attrs) {
   struct dma_map_ops *ops = get_dma_ops(dev);
   void *cpu_addr;

   BUG_ON(!ops);

   if (dma_alloc_from_coherent(dev, size, dma_handle, &cpu_addr))
   	return cpu_addr;

   if (!arch_dma_alloc_attrs(&dev, &flag))
   	return NULL;
   if (!ops->alloc)
   	return NULL;

   cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
   debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
   return cpu_addr;
}
extern struct dma_map_ops bad_dma_ops;
static inline struct dma_map_ops *get_dma_ops(struct device *dev)
{
   return &bad_dma_ops;
}

dma_noop.c 实现了对bad_dma_opsdma_noop_alloc定义和初始化, 根据以下代码可以看待, 在申请dma内存的时候, 设备device貌似没起到什么用处

struct dma_map_ops dma_noop_ops = {
   .alloc			= dma_noop_alloc,
   .free			= dma_noop_free,
   .map_page		= dma_noop_map_page,
   .map_sg			= dma_noop_map_sg,
   .mapping_error		= dma_noop_mapping_error,
   .dma_supported		= dma_noop_supported,
};
void *dma_noop_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp, struct dma_attrs *attrs)
{
   void *ret;
   ret = (void *)__get_free_pages(gfp, get_order(size)); //最终走向mm内存管理的函数 get_page_from_freelist
   if (ret)
   	*dma_handle = virt_to_phys(ret); //根据虚拟内存转换转换出物理地址
   return ret;
}

结论: pci_alloc_consistent 函数其实就做了两件事儿
1. 从mm内存管理中获取对应大小的页内存
2. 获取mm获取的页内存的物理地址
那问题又来了, 在centos下我尝试申请大于4MB的内存空间的时候, 系统会卡死

8.关于虚拟地址和物理地址的转换关系

./arch/arm/include/asm/memory.h

/*
 * The limitation of user task size can grow up to the end of free ram region.
 * It is difficult to define and perhaps will never meet the original meaning
 * of this define that was meant to.
 * Fortunately, there is no reference for this in noMMU mode, for now.
 */
// 上述的解释在这里的意思大概也就是说用户可以申请到所有空闲的内存地址
#define TASK_SIZE		(CONFIG_DRAM_SIZE)
#define END_MEM     		(UL(CONFIG_DRAM_BASE) + CONFIG_DRAM_SIZE)
// UL(CONFIG_DRAM_BASE) 可以理解为 数值+UL, 比如12456UL, 就是 123456为无符号长整型
#define PHYS_OFFSET 	UL(CONFIG_DRAM_BASE)
#define PAGE_OFFSET		(PHYS_OFFSET)
#define __virt_to_phys(x)	((x) - PAGE_OFFSET + PHYS_OFFSET)
#define __phys_to_virt(x)	((x) - PHYS_OFFSET + PAGE_OFFSET)
static inline phys_addr_t virt_to_phys(const volatile void *x){return __virt_to_phys((unsigned long)(x));}
static inline void *phys_to_virt(phys_addr_t x) {return (void *)(__phys_to_virt((unsigned long)(x)));}

分析: 从上述代码看, 在Centos环境下PHYS_OFFSETPAGE_OFFSET 偏移地址都是一样的, 但是实际操作并不是, 所以这里暂时没搞明白, 不过申请的大小肯定是和 CONFIG_DRAM_SIZE 有关系
/arch/arm/configs/at91x40_defconfig 中 定义了一些硬件的参数大小

CONFIG_SET_MEM_PARAM=y
CONFIG_DRAM_BASE=0x01000000
CONFIG_DRAM_SIZE=0x00400000
CONFIG_FLASH_MEM_BASE=0x01400000
CONFIG_PROCESSOR_ID=0x14000040

其中可以看到 CONFIG_DRAM_SIZE=0x00400000 也就是 4MB的大小, 这里的4MB是否就是限制我们申请最大的DMA内存大小呢?

9 关于对x86 PCIE驱动移植到arm环境下出现的问题

x86 DMA内存申请和arm下 DMA内存申请有些区别
以下是飞腾D2000,银河麒麟V10系统下对驱动的移植出现的问题

9.1 中断失去作用, 这里使用的是msi中断,

代码结构如下所示, 之前FPGA盒使用中断时INT, 但是在ARM端调试INT中断失去作用, 再查看 msi_enabled时候发现时 0,使用函数 pci_enable_msi(gDev);, 然后再查看值 msi_enabled是 1, 且 irq 中断号也能收到,也能注册成功。
依据: 在赛灵思提供的PCIE核的文档中可以看到, 如果主设备开启msi中断之后,fpga作为PCIE从设备会收到msi其中某个信号为一直高的状态,也就是说PCIE从设备是支持INTx, msi, msix 中断, 其中 msi是PCIE必须支持的, intx是主要PCI设备使用,比较旧,部分PCIE主机器可能不支持,而主设备可以选择使用从设备提供的一些中断,需要开启enable对应的中断类型。

//对中断操作相关函数
void pci_intx(struct pci_dev *dev, int enable);
int pci_enable_msi(struct pci_dev *dev);
void pci_disable_msix(struct pci_dev *dev);
void pci_disable_msi(struct pci_dev *dev);
struct pci_dev {
	...
	unsigned int	irq;
	...
	unsigned int	broken_intx_masking:1;	/* INTx masking can't be used */
	...
	unsigned int	is_busmaster:1;		/* Is busmaster */
	unsigned int	no_msi:1;		/* May not use MSI */
	...
	unsigned int	msi_enabled:1;
	unsigned int	msix_enabled:1;
	...
};

几种中断的区别:https://blog.csdn.net/greatliu2009/article/details/109377312
a. 传统的INTx中断,4个中断线,PCI设备上的所有func可任意使用其中一根或几根线通过拉低的方式向CPU发起中断信号,并把中断号放在interrupt line寄存器中,等待CPU读取。
b. msi中断,PCIE配置空间前256字节中能力描述链表中如果能找到能力ID为0x05的能力描述符,则说明该PCI设备支持MSI中断,并且软件可通过配置该能力描述符来开启、关闭MSI中断,并且在开启之前需要在该描述符内填写MSI消息在主机侧的内存空间地址,用于接收MSI中断信号;并且需要将中断起启编号写到Message Data中,并且将中断个数配置到Message Control中,后续开启中断后PCI设备则根据这两个信息生成连续的中断号。开启MSI之后,传统的INTx中断自动关闭。
c. msi-x中断,原理同MSI,PCIE配置空间前256字节中能力描述链表中如果能找到能力ID为0x11的能力描述符,则说明该PCI设备支持MSI中断。只是MSI只支持32个中断号,并且掩码长度和中断pend长度都只有32位,不能满足后续开发需求;并且MSI的中断号还必须是连续分配;msi-x则想解决上述问题。MSI-X改进了msi的能力描述符信息,弄了一个中断号与中断消息地址的对应表格,并且都可自定义;这样便解决了中断号必须连续,和中断向量比较少的问题;地址分开还有利于解决MSI可能存在中断冲突的问题。开启MSI-X之后,自动关闭传递INTx中断。

同时,同一设备MSI,MSI-X均可同时打开,也可独立打开,根据硬件设计使用。

9.2 pci_alloc_consistent 申请内存DMA返回NULL

解决方式:首先通过内核打印数据查看CMA内存为32MB, 通过查看 cat /proc/meminfo 查看 CmaFree: 31376 kB cma内存是否够用,这里都是够用的,
直接使用 dma_alloc_coherent(&hwdev->dev, size, dma_handle, GFP_KERNEL); 函数替换 pci_alloc_consistent
一定要注意, 最后一个参数是 GFP_KERNEL, 而 pci_alloc_consistent 返回为NULL的原因就是调用 dma_alloc_coherent 使用的参数为 GFP_ATOMIC.

在这里有说明使用: https://www.kernel.org/doc/html/latest/core-api/dma-api-howto.html
使用 GFP_ATOMIC 会申请和PAGESIZE大小一致的DMA内存,但是为什么在arm一直返回NULL,

流程
这里主要搞明白,在X86和在ARM下, DMA申请的内存到底是什么区域的地址,
银河麒麟使用的linux内核为5.4.18,通过源码查看函数原型pci_alloc_consistent 如下

static inline void * pci_alloc_consistent(struct pci_dev *hwdev, size_t size,
		     dma_addr_t *dma_handle)
{
	return dma_alloc_coherent(&hwdev->dev, size, dma_handle, GFP_ATOMIC);
}

函数原型是一样的,但是在arm环境下,DMA内存返回一直是NULL, 然后查看 linux-5.4.18源码, 发现最终申请DMA内存是由函数
dma_direct_alloc 去控制的, 其中 flagattrs 参数会印象申请DMA内存的走向,
函数原型调用如下:

//文件 kernel/dma/mapping.c
void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag, unsigned long attrs)
{
	const struct dma_map_ops *ops = get_dma_ops(dev);
	void *cpu_addr;
	WARN_ON_ONCE(!dev->coherent_dma_mask);
	if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
		return cpu_addr;
	/* let the implementation decide on the zone to allocate from: */
	flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);
	if (dma_is_direct(ops))
		cpu_addr = dma_direct_alloc(dev, size, dma_handle, flag, attrs);
	else if (ops->alloc)
		cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
	else
		return NULL;
	debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
	return cpu_addr;
}
//文件 kernel/dma/direct.c
void *dma_direct_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
	if (!IS_ENABLED(CONFIG_ARCH_HAS_UNCACHED_SEGMENT) &&
	    dma_alloc_need_uncached(dev, attrs))
		//经过验证,代码走得是 arch_dma_alloc 分支
		return arch_dma_alloc(dev, size, dma_handle, gfp, attrs);
	return dma_direct_alloc_pages(dev, size, dma_handle, gfp, attrs);
}
void *arch_dma_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle,
		gfp_t flags, unsigned long attrs)
{
	struct page *page = NULL;
	void *ret;
	...
	page = __dma_direct_alloc_pages(dev, size, dma_handle, flags, attrs);
	...
	*dma_handle = phys_to_dma(dev, page_to_phys(page));
	return ret;
}
void *dma_direct_alloc_pages(struct device *dev, size_t size,
		dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
	struct page *page;
	void *ret;

	page = __dma_direct_alloc_pages(dev, size, dma_handle, gfp, attrs);
	...
	return ret;
}
//文件 drivers/iommu/dma-iommu.c
struct page *__dma_direct_alloc_pages(struct device *dev, size_t size,
		dma_addr_t *dma_handle, gfp_t gfp, unsigned long attrs)
{
	size_t alloc_size = PAGE_ALIGN(size);
	int node = dev_to_node(dev);
	struct page *page = NULL;
	/* we always manually zero the memory once we are done: */
	gfp &= ~__GFP_ZERO;
	gfp |= __dma_direct_optimal_gfp_mask(dev, dev->coherent_dma_mask,
			&phys_mask);
	//可以看出第一次申请的是 IOMMU(CMA)的内存
	page = dma_alloc_contiguous(dev, alloc_size, gfp);
	if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
		dma_free_contiguous(dev, page, alloc_size);
		page = NULL;
	}
again:
	//如果申请失败的话,会从 mmu 内存中反复申请
	if (!page)	
		page = alloc_pages_node(node, gfp, get_order(alloc_size));
	if (page && !dma_coherent_ok(dev, page_to_phys(page), size)) {
		dma_free_contiguous(dev, page, size);
		page = NULL;

		if (IS_ENABLED(CONFIG_ZONE_DMA32) &&
		    phys_mask < DMA_BIT_MASK(64) &&
		    !(gfp & (GFP_DMA32 | GFP_DMA))) {
			gfp |= GFP_DMA32;
			goto again;
		}

		if (IS_ENABLED(CONFIG_ZONE_DMA) && !(gfp & GFP_DMA)) {
			gfp = (gfp & ~GFP_DMA32) | GFP_DMA;
			goto again;
		}
	}

	return page;
}
//文件 kernel/dma/contiguous.c
struct page *dma_alloc_contiguous(struct device *dev, size_t size, gfp_t gfp)
{
	size_t count = size >> PAGE_SHIFT;
	struct page *page = NULL;
	struct cma *cma = NULL;
	if (dev && dev->cma_area)
		cma = dev->cma_area;
	else if (count > 1)
		cma = dma_contiguous_default_area;

	/* CMA can be used only in the context which permits sleeping */
	if (cma && gfpflags_allow_blocking(gfp)) {
		size_t align = get_order(size);
		size_t cma_align = min_t(size_t, align, CONFIG_CMA_ALIGNMENT);
		page = cma_alloc(cma, count, cma_align, gfp & __GFP_NOWARN);
	}
	return page;
}
// 其中alloc_pages_node函数调用的层级
// mm/page_alloc.c
alloc_pages_node
 ->__alloc_pages_node
  ->__alloc_pages
   ->__alloc_pages_nodemask
    ->get_page_from_freelist
     ->rmqueue
      ->rmqueue_pcplist

注: 当我们申请的内存如果大于cma的剩余内存的时候, cma_alloc 会打印出内存不够的错误, 但是并没有直接报错,会返回一个NULL的页, 然后函数 __dma_direct_alloc_pages 会去通过其它的方式申请内存,然后会报错,内核会打印出对应的堆栈信息,但是该堆栈信息并不能说明代码正常运行时的走向,这里可能会容易误导。
image
image

可以看出来最终在 linux-5.4.18 内核源码中, 对DMA申请已经支持两种方式,第一种是 iommu, 第二种是 mmu
iommu的话最终申请的是 cma 内存
https://zhuanlan.zhihu.com/p/53455219 iommu发展现状
CMA机制
在数据方向是从外设到内存的DMA传输中,如果外设本身不支持scatter-gather形式的DMA,或者CPU不具备使用虚拟地址作为目标地址的IOMMU/SMMU机制(通常较高端的芯片才具有),那么则要求DMA的目标地址必须在物理内存上是连续的。

传统的做法是在内核启动时,通过传递"mem="的内核参数,预留一部分内存,但这种预留的内存只能作为DMA传输的“私藏”,即便之后DMA并没有真正使用这部分内存,也不能挪作他用,造成浪费。

应用对内存的需求变化是很难预测的,但是如果等到DMA设备真正产生需求时再分配,由于内存碎片等诸多原因,又可能很难申请到一块较大的连续的物理内存。

一个更灵活的做法是基于memory compaction实现的CMA(Contiguous Memory Allocator)机制(对应的内核选项为"CONFIG_CMA")。虽然名字是叫"allocator",但CMA存在的意义不光是“分配”,其目标还包括提高内存的利用率。

cam查看

通过如下指令可以查看cma申请的内存大小, 该项可以在机器启动的时候通过启动参数设置该值的大小, 在内核打印的时候会答应出 cma支持的默认大小:

[ 0.000000] cma: Reserved 32 MiB at 0x00000000f9c00000

cat /proc/meminfo
CmaTotal:          32768 kB   # 申请前
CmaFree:           31376 kB
cat /proc/meminfo
CmaTotal:          32768 kB   # 申请后,申请了4MB大小的空间
CmaFree:           27280 kB

10. 一致性DMA 映射和 流式DMA映射

10.1 一致性DMA映射 dma_alloc_coherent

一致性DMA 映射关闭了 L1/L2/L3 Cache。首先,当 CPU 写入数据时,则会直接放入内存,而不会在Cache进行缓存,所以设备可以立即DMA读取到CPU写入的数据;其次,当设备DMA写入数据到内存后,则CPU可以立即读取到该变化的数据,而不会读取Cache中的脏数据,因为Cache关闭了。

10.2 流式DMA映射 dma_map_single

因为一致性DMA关闭了Cache,虽然使用带来了方便,但是会牺牲数据读写的性能。例如Intel当前的CPU package,支持在 PCIe Endpoint DMA 访问内存时,将数据放入到L3 Cache,然后DMA 控制器从L3 Cache 读取数据(这是对我们DMA控制器直接访问DRAM内存常识的挑战)。而这些硬件的优化,在使用流式DMA影射时,可以充分发挥性能的优势

  • DMA_TO_DEVICE
    CPU将数据写入cache,然后同步cache与RAM(映射区域),同步操作完成后设备再从RAM(映射区域)获取数据
  • DMA_FROM_DEVICE
    CPU标记RAM(映射区域)对应的cache line为无效状态,以避免设备将数据写入RAM(映射区域)后,CPU从cache中获得"脏数据"
    从设备DMA -> CPU内存
struct device *dev;		/* device for DMA mapping */
struct sk_buff *skb;
dma = dma_map_single(dev, skb->data, size, DMA_TO_DEVICE);
/* tx_desc 是网卡发包时用到的描述符,
  * 硬件设备可以通过DMA 访问到描述符绑定的dma地址,然后硬件可以基于该DMA地址发起内存访问
  */
tx_desc->read.buffer_addr = cpu_to_le64(dma);

CPU 内存DMA -> 从设备

	struct page *page;
	dma_addr_t dma;
        struct device *dev;		/* device for DMA mapping */
        int page_size = PAGE_SIZE;
	/* alloc new page for storage */
	page = dev_alloc_pages(0);

	/* map page for use */
	dma = dma_map_page_attrs(dev, page, 0,
				 page_size,
				 DMA_FROM_DEVICE,
				 DMA_ATTR_SKIP_CPU_SYNC);

	/* sync the buffer for use by the device */
	dma_sync_single_range_for_device(dev, dma,
					 0, page_size,
					 DMA_FROM_DEVICE);

关于PCI的中断

INTX, MSI, MSIX
中断申请示例:

  int irqType = CUR_PCI_IRQ_MSI;
  if(CUR_PCI_IRQ_INTX == irqType) {
    if (0 > pci_enable_device(gDev)) {  //开启INTX中断
      printk(KERN_WARNING"%s: Init: Device not enabled.\n", gDrvrName);
      return (CRIT_ERR);
    }
  } else {
    if(!atomic_read(&gDev->enable_cnt))  pci_disable_device(gDev);  //禁用INTX中断, 否则MSI和MSI-X申请可能会失败
  }

  if(CUR_PCI_IRQ_MSIX == irqType) {
    if(!gDev->msix_enabled) {
      int ret3  = 0;
      ret3 = pci_alloc_irq_vectors(gDev, 1, 5, PCI_IRQ_MSIX | PCI_IRQ_AFFINITY); //开启并设置可以申请到的中断个数
      if(ret3 < 0) {
        printk("%s: #ERR pci_alloc_irq_vectors fail, ret: %d \n", gDrvrName, ret3);
        return (CRIT_ERR);
      }
    }
  } else {
    pci_disable_msix(gDev);       //禁用 MSI 中断
  }

  if(CUR_PCI_IRQ_MSI == irqType) {
    if(!gDev->msi_enabled) {
      int ret3 = 0, maxvenc = 0;
      unsigned short msgctl;
      ret3 = pci_find_capability(gDev, PCI_CAP_ID_MSI);
      printk("%s: pci_find_capability, ret: %d \n", gDrvrName, ret3);
      if(!ret3) {
        printk("%s: #ERR not support cur irq, ret: %d \n", gDrvrName, ret3);
      }

      pci_read_config_word(gDev, ret3 + PCI_MSI_FLAGS ,&msgctl);
      maxvenc = 1 << (msgctl & PCI_MSI_FLAGS_QMASK >> 1);

      printk("%s: pci_find_capability, msgctl: %d , maxvenc: %d \n", gDrvrName, msgctl, maxvenc);
      // 使用 pci_enable_msi 也是对 pci_alloc_irq_vectors 的封装, 只不过 minvenc 和 maxvenc都是 1
      ret3 = pci_alloc_irq_vectors(gDev, 1, maxvenc, PCI_IRQ_MSI | PCI_IRQ_AFFINITY);  // 该函数已经包含了 msi_capability_init->pci_msi_setup_msi_irqs(Configure MSI capability structure)
      
      if(ret3 < 0) {
        printk("%s: #ERR pci_alloc_irq_vectors fail, ret: %d \n", gDrvrName, ret3);
        return (CRIT_ERR);
      }

      printk("%s: msi_capability_init, ret3: %d \n", gDrvrName, ret3);
    }
  } else {
    pci_disable_msi(gDev);
  }
  
  gIrq = pci_irq_vector(gDev, 0);

  printk("%s:  pci_irq_vector: %d \n", gDrvrName, gIrq);

  if(gIrq < 0 || gIrq == 0xFF) {
    printk("%s: #ERR pci_irq_vector fail, gIrq: %d \n", gDrvrName, gIrq);
    return (CRIT_ERR);
  }
  
  if (0 > request_irq(gIrq, &XPCIe_IRQHandler, IRQF_SHARED, gDrvrName, gDev)) {
    printk(KERN_WARNING"%s: Init: Unable to allocate IRQ",gDrvrName);
    return (CRIT_ERR);
  }
posted @ 2022-11-10 20:27  韩若明瞳  阅读(2768)  评论(0编辑  收藏  举报