// SPDX-License-Identifier: GPL-2.0
/*
* PCI Peer 2 Peer DMA support.
*
* Copyright (c) 2016-2018, Logan Gunthorpe
* Copyright (c) 2016-2017, Microsemi Corporation
* Copyright (c) 2017, Christoph Hellwig
* Copyright (c) 2018, Eideticom Inc.
*/
#define pr_fmt(fmt) "pci-p2pdma: " fmt
#include <linux/ctype.h>
#include <linux/pci-p2pdma.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/genalloc.h>
#include <linux/memremap.h>
#include <linux/percpu-refcount.h>
#include <linux/random.h>
#include <linux/seq_buf.h>
#include <linux/xarray.h>
enum pci_p2pdma_map_type {
PCI_P2PDMA_MAP_UNKNOWN = 0,
PCI_P2PDMA_MAP_NOT_SUPPORTED,
PCI_P2PDMA_MAP_BUS_ADDR,
PCI_P2PDMA_MAP_THRU_HOST_BRIDGE,
};
struct pci_p2pdma {
struct gen_pool *pool;
bool p2pmem_published;
struct xarray map_types;
};
struct pci_p2pdma_pagemap {
struct dev_pagemap pgmap;
struct pci_dev *provider;
u64 bus_offset;
};
static struct pci_p2pdma_pagemap *to_p2p_pgmap(struct dev_pagemap *pgmap)
{
return container_of(pgmap, struct pci_p2pdma_pagemap, pgmap);
}
static ssize_t size_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct pci_p2pdma *p2pdma;
size_t size = 0;
rcu_read_lock();
p2pdma = rcu_dereference(pdev->p2pdma);
if (p2pdma && p2pdma->pool)
size = gen_pool_size(p2pdma->pool);
rcu_read_unlock();
return sysfs_emit(buf, "%zd\n", size);
}
static DEVICE_ATTR_RO(size);
static ssize_t available_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct pci_p2pdma *p2pdma;
size_t avail = 0;
rcu_read_lock();
p2pdma = rcu_dereference(pdev->p2pdma);
if (p2pdma && p2pdma->pool)
avail = gen_pool_avail(p2pdma->pool);
rcu_read_unlock();
return sysfs_emit(buf, "%zd\n", avail);
}
static DEVICE_ATTR_RO(available);
static ssize_t published_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct pci_p2pdma *p2pdma;
bool published = false;
rcu_read_lock();
p2pdma = rcu_dereference(pdev->p2pdma);
if (p2pdma)
published = p2pdma->p2pmem_published;
rcu_read_unlock();
return sysfs_emit(buf, "%d\n", published);
}
static DEVICE_ATTR_RO(published);
static struct attribute *p2pmem_attrs[] = {
&dev_attr_size.attr,
&dev_attr_available.attr,
&dev_attr_published.attr,
NULL,
};
static const struct attribute_group p2pmem_group = {
.attrs = p2pmem_attrs,
.name = "p2pmem",
};
static void pci_p2pdma_release(void *data)
{
struct pci_dev *pdev = data;
struct pci_p2pdma *p2pdma;
p2pdma = rcu_dereference_protected(pdev->p2pdma, 1);
if (!p2pdma)
return;
/* Flush and disable pci_alloc_p2p_mem() */
pdev->p2pdma = NULL;
synchronize_rcu();
gen_pool_destroy(p2pdma->pool);
sysfs_remove_group(&pdev->dev.kobj, &p2pmem_group);
xa_destroy(&p2pdma->map_types);
}
static int pci_p2pdma_setup(struct pci_dev *pdev)
{
int error = -ENOMEM;
struct pci_p2pdma *p2p;
p2p = devm_kzalloc(&pdev->dev, sizeof(*p2p), GFP_KERNEL);
if (!p2p)
return -ENOMEM;
xa_init(&p2p->map_types);
p2p->pool = gen_pool_create(PAGE_SHIFT, dev_to_node(&pdev->dev));
if (!p2p->pool)
goto out;
error = devm_add_action_or_reset(&pdev->dev, pci_p2pdma_release, pdev);
if (error)
goto out_pool_destroy;
error = sysfs_create_group(&pdev->dev.kobj, &p2pmem_group);
if (error)
goto out_pool_destroy;
rcu_assign_pointer(pdev->p2pdma, p2p);
return 0;
out_pool_destroy:
gen_pool_destroy(p2p->pool);
out:
devm_kfree(&pdev->dev, p2p);
return error;
}
/**
* pci_p2pdma_add_resource - add memory for use as p2p memory
* @pdev: the device to add the memory to
* @bar: PCI BAR to add
* @size: size of the memory to add, may be zero to use the whole BAR
* @offset: offset into the PCI BAR
*
* The memory will be given ZONE_DEVICE struct pages so that it may
* be used with any DMA request.
*/
int pci_p2pdma_add_resource(struct pci_dev *pdev, int bar, size_t size,
u64 offset)
{
struct pci_p2pdma_pagemap *p2p_pgmap;
struct dev_pagemap *pgmap;
struct pci_p2pdma *p2pdma;
void *addr;
int error;
if (!(pci_resource_flags(pdev, bar) & IORESOURCE_MEM))
return -EINVAL;
if (offset >= pci_resource_len(pdev, bar))
return -EINVAL;
if (!size)
size = pci_resource_len(pdev, bar) - offset;
if (size + offset > pci_resource_len(pdev, bar))
return -EINVAL;
if (!pdev->p2pdma) {
error = pci_p2pdma_setup(pdev);
if (error)
return error;
}
p2p_pgmap