mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-30 14:33:24 -04:00
The size passed to dma_unmap_page() must match the size used for the
corresponding dma_map_page() call.
Stream RX and TX buffers are mapped with TB_MAX_FRAME_SIZE when the
buffer pools are allocated. However, tbstream_ring_free() currently uses
tb_ring_frame_size() as the unmap size.
That helper returns the current frame payload size, not the DMA mapping
size. On the TX path, tbstream_dev_alloc_tx() stores a shorter payload
length in frame.size when the payload is smaller than TB_MAX_FRAME_SIZE.
This happens for a short final DATA frame, and also for the CLOSE frame,
which is allocated with SZ_256.
In those cases the buffer was mapped with TB_MAX_FRAME_SIZE, but
tb_ring_frame_size() returns the shorter frame payload length. This makes
the dma_unmap_page() size differ from the original dma_map_page() size.
Use TB_MAX_FRAME_SIZE when unmapping stream buffers so the unmap size
matches the DMA mapping size used by the buffer allocation paths.
Signed-off-by: Xu Rao <raoxu@uniontech.com>
Fixes: 6db21d817b ("thunderbolt: Add support for USB4STREAM")
Signed-off-by: Mika Westerberg <mika.westerberg@linux.intel.com>
1699 lines
42 KiB
C
1699 lines
42 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Stream data over Thunderbolt/USB4 cable
|
|
*
|
|
* Copyright (C) 2026, Intel Corporation
|
|
* Authors: Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>
|
|
* Mika Westerberg <mika.westerberg@linux.intel.com>
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "tbstream: " fmt
|
|
|
|
#include <linux/configfs.h>
|
|
#include <linux/file.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/idr.h>
|
|
#include <linux/miscdevice.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/sizes.h>
|
|
#include <linux/thunderbolt.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/uio.h>
|
|
#include <linux/uuid.h>
|
|
#include <linux/wait.h>
|
|
|
|
/*
|
|
* USB4STREAM - Stream data directly over Thunderbolt/USB4 cable
|
|
*
|
|
* HopIDs are configured by the user. In Linux this is done through
|
|
* ConfigFS. Once that is done paths are be established the first time
|
|
* the stream is opened. Typically the read side is opened first to make
|
|
* sure all the data will be received.
|
|
*
|
|
* End-to-end flow control is mandatory on both sides.
|
|
*
|
|
* Data is sent to the other side as tunneled DATA packets. All the data
|
|
* is owned by the user and passed as-is from the writer to the reader.
|
|
*
|
|
* Once the stream device is closed, a CLOSE packet is sent to the peer
|
|
* so it can take the necessary action. On Linux this typically results
|
|
* in EOF being returned to the reader.
|
|
*
|
|
* Tunneled packet types:
|
|
*
|
|
* +-------+---------+------------------+
|
|
* | PDF | Type | Payload size |
|
|
* +-------+---------+------------------+
|
|
* | 2 | DATA | up to 4 KiB |
|
|
* | 3 | CLOSE | up to 256 bytes |
|
|
* +-------+---------+------------------+
|
|
*
|
|
* Each stream can optionally publish configuration values under its own
|
|
* XDomain property directory. The name of the directory is the name of
|
|
* the stream in question and the UUID is up to the stream. For example
|
|
* if the stream exposes video output then the directory name could be
|
|
* "video".
|
|
*
|
|
* Below values are reserved and can be used by the stream:
|
|
*
|
|
* +----------+-----------+-------------------------+
|
|
* | Key | Type | Contents |
|
|
* +----------+-----------+-------------------------+
|
|
* | inhopid | IMMEDIATE | Configured input HopID |
|
|
* | outhopid | IMMEDIATE | Configured output HopID |
|
|
* +----------+-----------+-------------------------+
|
|
*
|
|
* It is allowed to add more stream specific properties as well if the
|
|
* above are not enough.
|
|
*/
|
|
|
|
#define TBSTREAM_DEV_RING_SIZE 256
|
|
#define TBSTREAM_DEV_MIN_RING_SIZE 32
|
|
#define TBSTREAM_DEV_MAX_RING_SIZE 4096
|
|
#define TBSTREAM_DEV_THROTTLING 8192
|
|
#define TBSTREAM_DEV_MAX_THROTTLING 16776960
|
|
|
|
/**
|
|
* enum tbstream_frame_pdf - PDF numbers for tunneled frames
|
|
* @TBSTREAM_FRAME_START: PDF of the start of the frame
|
|
* @TBSTREAM_DATA: PDF of the DATA frame
|
|
* @TBSTREAM_CLOSE: PDF of the CLOSE frame
|
|
*/
|
|
enum tbstream_frame_pdf {
|
|
TBSTREAM_FRAME_START = 1,
|
|
TBSTREAM_DATA,
|
|
TBSTREAM_CLOSE,
|
|
};
|
|
|
|
/**
|
|
* struct tbstream_frame - Frame submitted to/from the rings
|
|
* @sdev: Pointer to the stream device
|
|
* @page: Page holding the packet
|
|
* @offset: Offset inside @page if partial read is done
|
|
* @completed: %true if the RX frame is completed
|
|
* @frame: Underlying frame structure
|
|
*/
|
|
struct tbstream_frame {
|
|
struct tbstream_dev *sdev;
|
|
struct page *page;
|
|
unsigned int offset;
|
|
bool completed;
|
|
struct ring_frame frame;
|
|
};
|
|
|
|
/**
|
|
* struct tbstream_ring - Stream RX/TX ring structure
|
|
* @ring: Pointer to the API ring
|
|
* @prod: Current value of producer
|
|
* @cons: Current value of consumer
|
|
* @frames: Holds the ring frames
|
|
*/
|
|
struct tbstream_ring {
|
|
struct tb_ring *ring;
|
|
unsigned long prod;
|
|
unsigned long cons;
|
|
struct tbstream_frame *frames;
|
|
};
|
|
|
|
/**
|
|
* struct tbstream_dev - Stream character device
|
|
* @group: ConfigFS group for this device
|
|
* @stream: Pointer to the stream if it is attached (%NULL otherwise)
|
|
* @misc: Character device used for tunneling
|
|
* @kref: Reference count
|
|
* @index: Unique identifier for the character device
|
|
* @in_hopid: In HopID
|
|
* @out_hopid: Out HopID
|
|
* @ring_size: Size of the rings
|
|
* @throttling: Interrupt throttling rate in ns
|
|
* @users: Number of times @cdev has been opened
|
|
* @closed: CLOSE packet was received
|
|
* @removed: Userspace removed the ConfigFS group underneath.
|
|
* @wait: Waitqueue for open, read and write
|
|
* @lock: Lock protecting this structure
|
|
* @tx_ring: Transmit ring
|
|
* @rx_ring: Receive ring
|
|
* @list: Stream devices are linked through this
|
|
*/
|
|
struct tbstream_dev {
|
|
struct config_group group;
|
|
struct tbstream *stream;
|
|
struct miscdevice misc;
|
|
struct kref kref;
|
|
int index;
|
|
int in_hopid;
|
|
int out_hopid;
|
|
unsigned int ring_size;
|
|
unsigned int throttling;
|
|
int users;
|
|
bool closed;
|
|
bool removed;
|
|
wait_queue_head_t wait;
|
|
struct mutex lock;
|
|
struct tbstream_ring tx_ring;
|
|
struct tbstream_ring rx_ring;
|
|
struct list_head list;
|
|
};
|
|
|
|
/**
|
|
* struct tbstream_group - Config group for stream
|
|
* @group: ConfigFS group for @stream
|
|
* @stream: Stream the ConfigFS group is attached to. %NULL if there is
|
|
* no stream attached.
|
|
* @lock: Lock protecting this structure
|
|
* @dev_list: List of stream devices
|
|
*
|
|
* This is the ConfigFS directory for one connection to another host.
|
|
* There can be several &struct stream_dev linked through @dev_list of
|
|
* this structure. Reference count managed through @group.
|
|
*/
|
|
struct tbstream_group {
|
|
struct config_group group;
|
|
struct tbstream *stream;
|
|
struct mutex lock;
|
|
struct list_head dev_list;
|
|
};
|
|
|
|
/**
|
|
* struct tbstream - Stream service private data
|
|
* @kref: Reference count
|
|
* @svc: Pointer to the service device
|
|
* @list: Streams are linked through this in @stream_list
|
|
*
|
|
* This represents the actual physical connection between two hosts.
|
|
*/
|
|
struct tbstream {
|
|
struct kref kref;
|
|
struct tb_service *svc;
|
|
struct list_head list;
|
|
};
|
|
|
|
static DEFINE_IDA(tbstream_indices);
|
|
|
|
/* Protects tbstream_list */
|
|
static DEFINE_MUTEX(tbstream_lock);
|
|
static LIST_HEAD(tbstream_list);
|
|
|
|
/* Serializes tbstream_get()/put() */
|
|
static DEFINE_MUTEX(tbstream_kref_lock);
|
|
|
|
/* Serializes tbstream_dev_get()/put() */
|
|
static DEFINE_MUTEX(tbstream_dev_kref_lock);
|
|
|
|
/* Stream property directory UUID: 3a1cb984-c4d9-4469-a277-ce2fdfd11f0d */
|
|
static const uuid_t tbstream_dir_uuid =
|
|
UUID_INIT(0x3a1cb984, 0xc4d9, 0x4469,
|
|
0xa2, 0x77, 0xce, 0x2f, 0xdf, 0xd1, 0x1f, 0x0d);
|
|
|
|
static struct tb_property_dir *tbstream_dir;
|
|
|
|
static void tbstream_release(struct kref *kref)
|
|
{
|
|
struct tbstream *stream = container_of(kref, typeof(*stream), kref);
|
|
|
|
tb_service_put(stream->svc);
|
|
kfree(stream);
|
|
}
|
|
|
|
static void tbstream_put(struct tbstream *stream)
|
|
{
|
|
if (stream) {
|
|
guard(mutex)(&tbstream_kref_lock);
|
|
kref_put(&stream->kref, tbstream_release);
|
|
}
|
|
}
|
|
|
|
static struct tbstream *tbstream_get(struct tbstream *stream)
|
|
{
|
|
if (stream) {
|
|
guard(mutex)(&tbstream_kref_lock);
|
|
kref_get(&stream->kref);
|
|
}
|
|
return stream;
|
|
}
|
|
|
|
static inline bool tbstream_valid(const struct tbstream *stream)
|
|
{
|
|
if (stream)
|
|
return !tb_service_parent(stream->svc)->is_unplugged;
|
|
return false;
|
|
}
|
|
|
|
static void tbstream_ring_free(struct tbstream_ring *ring)
|
|
{
|
|
struct device *dma_dev = tb_ring_dma_device(ring->ring);
|
|
enum dma_data_direction dir;
|
|
int i;
|
|
|
|
if (ring->ring->is_tx)
|
|
dir = DMA_TO_DEVICE;
|
|
else
|
|
dir = DMA_FROM_DEVICE;
|
|
|
|
for (i = 0; i < tb_ring_size(ring->ring); i++) {
|
|
struct tbstream_frame *sf = &ring->frames[i];
|
|
|
|
if (sf->frame.buffer_phy)
|
|
dma_unmap_page(dma_dev, sf->frame.buffer_phy,
|
|
TB_MAX_FRAME_SIZE, dir);
|
|
sf->frame.buffer_phy = 0;
|
|
if (sf->page)
|
|
__free_page(sf->page);
|
|
sf->page = NULL;
|
|
}
|
|
|
|
ring->prod = 0;
|
|
ring->cons = 0;
|
|
kfree(ring->frames);
|
|
}
|
|
|
|
static inline bool tbstream_ring_available(const struct tbstream_ring *ring)
|
|
{
|
|
return ring->prod > ring->cons;
|
|
}
|
|
|
|
static inline struct tb_xdomain *tbstream_dev_xdomain(struct tbstream_dev *sdev)
|
|
{
|
|
if (sdev->stream)
|
|
return tb_service_parent(sdev->stream->svc);
|
|
return NULL;
|
|
}
|
|
|
|
static void tbstream_dev_release(struct kref *kref)
|
|
{
|
|
struct tbstream_dev *sdev = container_of(kref, struct tbstream_dev, kref);
|
|
|
|
if (sdev->stream) {
|
|
struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
|
|
|
|
if (sdev->out_hopid > 0)
|
|
tb_xdomain_release_out_hopid(xd, sdev->out_hopid);
|
|
if (sdev->in_hopid > 0)
|
|
tb_xdomain_release_in_hopid(xd, sdev->in_hopid);
|
|
|
|
tbstream_put(sdev->stream);
|
|
}
|
|
ida_free(&tbstream_indices, sdev->index);
|
|
kfree(sdev->misc.name);
|
|
kfree(sdev);
|
|
}
|
|
|
|
static inline void tbstream_dev_put(struct tbstream_dev *sdev)
|
|
{
|
|
guard(mutex)(&tbstream_dev_kref_lock);
|
|
kref_put(&sdev->kref, tbstream_dev_release);
|
|
}
|
|
|
|
static inline struct tbstream_dev *tbstream_dev_get(struct tbstream_dev *sdev)
|
|
{
|
|
guard(mutex)(&tbstream_dev_kref_lock);
|
|
kref_get(&sdev->kref);
|
|
return sdev;
|
|
}
|
|
|
|
static inline struct tbstream_dev *to_tbstream_dev(struct miscdevice *misc)
|
|
{
|
|
return container_of(misc, struct tbstream_dev, misc);
|
|
}
|
|
|
|
static inline int tbstream_dev_valid(const struct tbstream_dev *sdev)
|
|
{
|
|
const struct tbstream *stream = sdev->stream;
|
|
|
|
if (!tbstream_valid(stream))
|
|
return -ENXIO;
|
|
if (sdev->in_hopid <= 0 || sdev->out_hopid <= 0)
|
|
return -EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
static inline bool tbstream_dev_removed(const struct tbstream_dev *sdev)
|
|
{
|
|
return sdev->removed;
|
|
}
|
|
|
|
static inline bool tbstream_dev_closed(const struct tbstream_dev *sdev)
|
|
{
|
|
return sdev->closed;
|
|
}
|
|
|
|
static void
|
|
tbstream_dev_rx_callback(struct tb_ring *ring, struct ring_frame *frame,
|
|
bool canceled)
|
|
{
|
|
struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame);
|
|
struct tbstream_dev *sdev = sf->sdev;
|
|
|
|
if (canceled)
|
|
return;
|
|
|
|
sf->completed = true;
|
|
sdev->rx_ring.prod++;
|
|
|
|
if (sf->frame.flags & RING_DESC_CRC_ERROR)
|
|
pr_warn("RX CRC error\n");
|
|
else if (sf->frame.flags & RING_DESC_BUFFER_OVERRUN)
|
|
pr_warn("RX buffer overrun\n");
|
|
else
|
|
wake_up_interruptible_poll(&sdev->wait, EPOLLIN | EPOLLRDNORM);
|
|
}
|
|
|
|
static struct tbstream_frame *
|
|
tbstream_dev_completed_rx(struct tbstream_dev *sdev)
|
|
{
|
|
struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring);
|
|
struct tbstream_frame *sf;
|
|
int index;
|
|
|
|
index = sdev->rx_ring.cons % tb_ring_size(sdev->rx_ring.ring);
|
|
sf = &sdev->rx_ring.frames[index];
|
|
if (!sf->completed)
|
|
return NULL;
|
|
|
|
dma_sync_single_for_cpu(dma_dev, sf->frame.buffer_phy,
|
|
tb_ring_frame_size(&sf->frame),
|
|
DMA_FROM_DEVICE);
|
|
return sf;
|
|
}
|
|
|
|
static int tbstream_dev_consume_rx(struct tbstream_dev *sdev)
|
|
{
|
|
struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring);
|
|
struct tbstream_frame *sf;
|
|
int index;
|
|
|
|
index = sdev->rx_ring.cons % tb_ring_size(sdev->rx_ring.ring);
|
|
sdev->rx_ring.cons++;
|
|
|
|
sf = &sdev->rx_ring.frames[index];
|
|
sf->completed = false;
|
|
sf->offset = 0;
|
|
sf->frame.size = 0;
|
|
|
|
dma_sync_single_for_device(dma_dev, sf->frame.buffer_phy,
|
|
tb_ring_frame_size(&sf->frame),
|
|
DMA_FROM_DEVICE);
|
|
|
|
return tb_ring_rx(sdev->rx_ring.ring, &sf->frame);
|
|
}
|
|
|
|
static int tbstream_dev_alloc_rx_buffers(struct tbstream_dev *sdev)
|
|
{
|
|
size_t ring_size = tb_ring_size(sdev->rx_ring.ring);
|
|
int i;
|
|
|
|
sdev->rx_ring.frames = kcalloc(ring_size, sizeof(struct tbstream_frame),
|
|
GFP_KERNEL);
|
|
if (!sdev->rx_ring.frames)
|
|
return -ENOMEM;
|
|
|
|
for (i = 0; i < ring_size; i++) {
|
|
struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring);
|
|
struct tbstream_frame *sf = &sdev->rx_ring.frames[i];
|
|
dma_addr_t dma_addr;
|
|
|
|
sf->page = alloc_page(GFP_KERNEL);
|
|
if (!sf->page)
|
|
return -ENOMEM;
|
|
|
|
dma_addr = dma_map_page(dma_dev, sf->page, 0, TB_MAX_FRAME_SIZE,
|
|
DMA_FROM_DEVICE);
|
|
if (dma_mapping_error(dma_dev, dma_addr)) {
|
|
__free_page(sf->page);
|
|
sf->page = NULL;
|
|
return -ENOMEM;
|
|
}
|
|
|
|
sf->sdev = sdev;
|
|
sf->frame.callback = tbstream_dev_rx_callback;
|
|
sf->frame.buffer_phy = dma_addr;
|
|
|
|
tb_ring_rx(sdev->rx_ring.ring, &sf->frame);
|
|
}
|
|
|
|
sdev->rx_ring.cons = 0;
|
|
sdev->rx_ring.prod = 0;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
tbstream_dev_tx_callback(struct tb_ring *ring, struct ring_frame *frame,
|
|
bool canceled)
|
|
{
|
|
struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame);
|
|
struct tbstream_dev *sdev = sf->sdev;
|
|
|
|
if (canceled)
|
|
return;
|
|
|
|
sdev->tx_ring.prod++;
|
|
if (sf->frame.eof == TBSTREAM_DATA)
|
|
wake_up_interruptible_poll(&sdev->wait, EPOLLOUT | EPOLLWRNORM);
|
|
}
|
|
|
|
static int tbstream_dev_alloc_tx_buffers(struct tbstream_dev *sdev)
|
|
{
|
|
struct device *dma_dev = tb_ring_dma_device(sdev->tx_ring.ring);
|
|
size_t ring_size = tb_ring_size(sdev->tx_ring.ring);
|
|
int i;
|
|
|
|
sdev->tx_ring.frames = kcalloc(ring_size, sizeof(struct tbstream_frame),
|
|
GFP_KERNEL);
|
|
if (!sdev->tx_ring.frames)
|
|
return -ENOMEM;
|
|
|
|
for (i = 0; i < ring_size; i++) {
|
|
struct tbstream_frame *sf = &sdev->tx_ring.frames[i];
|
|
dma_addr_t dma_addr;
|
|
|
|
sf->page = alloc_page(GFP_KERNEL);
|
|
if (!sf->page)
|
|
return -ENOMEM;
|
|
|
|
dma_addr = dma_map_page(dma_dev, sf->page, 0, TB_MAX_FRAME_SIZE,
|
|
DMA_TO_DEVICE);
|
|
if (dma_mapping_error(dma_dev, dma_addr)) {
|
|
__free_page(sf->page);
|
|
sf->page = NULL;
|
|
return -ENOMEM;
|
|
}
|
|
|
|
sf->sdev = sdev;
|
|
sf->frame.callback = tbstream_dev_tx_callback;
|
|
sf->frame.buffer_phy = dma_addr;
|
|
sf->frame.sof = TBSTREAM_FRAME_START;
|
|
}
|
|
|
|
sdev->tx_ring.cons = 0;
|
|
sdev->tx_ring.prod = ring_size - 1;
|
|
return 0;
|
|
}
|
|
|
|
static struct tbstream_frame *
|
|
tbstream_dev_alloc_tx(struct tbstream_dev *sdev, enum tbstream_frame_pdf pdf,
|
|
struct iov_iter *from, size_t size)
|
|
{
|
|
struct device *dma_dev = tb_ring_dma_device(sdev->tx_ring.ring);
|
|
struct tbstream_frame *sf;
|
|
int index;
|
|
|
|
if (!tbstream_ring_available(&sdev->tx_ring))
|
|
return ERR_PTR(-ENOBUFS);
|
|
|
|
index = sdev->tx_ring.cons % tb_ring_size(sdev->tx_ring.ring);
|
|
sdev->tx_ring.cons++;
|
|
|
|
sf = &sdev->tx_ring.frames[index];
|
|
sf->frame.size = size < TB_MAX_FRAME_SIZE ? size : 0;
|
|
sf->frame.eof = pdf;
|
|
|
|
dma_sync_single_for_cpu(dma_dev, sf->frame.buffer_phy, size,
|
|
DMA_TO_DEVICE);
|
|
if (pdf == TBSTREAM_DATA) {
|
|
if (copy_page_from_iter(sf->page, 0, size, from) != size)
|
|
return ERR_PTR(-EFAULT);
|
|
} else {
|
|
memset(page_address(sf->page), 0, size);
|
|
}
|
|
dma_sync_single_for_device(dma_dev, sf->frame.buffer_phy, size,
|
|
DMA_TO_DEVICE);
|
|
return sf;
|
|
}
|
|
|
|
static int
|
|
tbstream_dev_send_data(struct tbstream_dev *sdev, struct iov_iter *from,
|
|
size_t size)
|
|
{
|
|
struct tbstream_frame *sf;
|
|
|
|
sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_DATA, from, size);
|
|
if (IS_ERR(sf))
|
|
return PTR_ERR(sf);
|
|
return tb_ring_tx(sdev->tx_ring.ring, &sf->frame);
|
|
}
|
|
|
|
static int tbstream_dev_send_close(struct tbstream_dev *sdev)
|
|
{
|
|
struct tbstream_frame *sf;
|
|
|
|
sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_CLOSE, NULL, SZ_256);
|
|
if (IS_ERR(sf))
|
|
return PTR_ERR(sf);
|
|
return tb_ring_tx(sdev->tx_ring.ring, &sf->frame);
|
|
}
|
|
|
|
static int tbstream_dev_start(struct tbstream_dev *sdev)
|
|
{
|
|
struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
|
|
u16 sof_mask, eof_mask;
|
|
struct tb_ring *ring;
|
|
int ret, e2e_tx_hop;
|
|
|
|
ring = tb_ring_alloc_tx(xd->tb->nhi, -1, sdev->ring_size,
|
|
RING_FLAG_FRAME | RING_FLAG_E2E);
|
|
if (!ring)
|
|
return -ENOMEM;
|
|
sdev->tx_ring.ring = ring;
|
|
|
|
ret = tbstream_dev_alloc_tx_buffers(sdev);
|
|
if (ret)
|
|
goto err_free_tx;
|
|
|
|
e2e_tx_hop = ring->hop;
|
|
sof_mask = BIT(TBSTREAM_FRAME_START);
|
|
eof_mask = BIT(TBSTREAM_DATA) | BIT(TBSTREAM_CLOSE);
|
|
|
|
ring = tb_ring_alloc_rx(xd->tb->nhi, -1, sdev->ring_size,
|
|
RING_FLAG_FRAME | RING_FLAG_E2E, e2e_tx_hop,
|
|
sof_mask, eof_mask, NULL, NULL);
|
|
if (!ring) {
|
|
ret = -ENOMEM;
|
|
goto err_free_tx_buffers;
|
|
}
|
|
sdev->rx_ring.ring = ring;
|
|
|
|
ret = tb_xdomain_enable_paths(xd, sdev->out_hopid,
|
|
sdev->tx_ring.ring->hop,
|
|
sdev->in_hopid,
|
|
sdev->rx_ring.ring->hop);
|
|
if (ret)
|
|
goto err_free_rx;
|
|
|
|
tb_ring_throttling(sdev->tx_ring.ring, sdev->throttling);
|
|
tb_ring_throttling(sdev->rx_ring.ring, sdev->throttling);
|
|
|
|
tb_ring_start(sdev->tx_ring.ring);
|
|
tb_ring_start(sdev->rx_ring.ring);
|
|
|
|
ret = tbstream_dev_alloc_rx_buffers(sdev);
|
|
if (ret)
|
|
goto err_stop;
|
|
return 0;
|
|
|
|
err_stop:
|
|
tb_ring_stop(sdev->rx_ring.ring);
|
|
tb_ring_stop(sdev->tx_ring.ring);
|
|
err_free_rx:
|
|
tb_ring_free(sdev->rx_ring.ring);
|
|
err_free_tx_buffers:
|
|
tbstream_ring_free(&sdev->tx_ring);
|
|
err_free_tx:
|
|
tb_ring_free(sdev->tx_ring.ring);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void tbstream_dev_stop(struct tbstream_dev *sdev)
|
|
{
|
|
struct tb_xdomain *xd;
|
|
|
|
/* Wait for the ring to complete any outstanding frames */
|
|
tb_ring_flush(sdev->tx_ring.ring, 500);
|
|
tb_ring_stop(sdev->tx_ring.ring);
|
|
tb_ring_flush(sdev->rx_ring.ring, 500);
|
|
tb_ring_stop(sdev->rx_ring.ring);
|
|
|
|
xd = tbstream_dev_xdomain(sdev);
|
|
if (xd) {
|
|
tb_xdomain_disable_paths(xd, sdev->out_hopid,
|
|
sdev->tx_ring.ring->hop,
|
|
sdev->in_hopid,
|
|
sdev->rx_ring.ring->hop);
|
|
}
|
|
|
|
tbstream_ring_free(&sdev->rx_ring);
|
|
tb_ring_free(sdev->rx_ring.ring);
|
|
sdev->rx_ring.ring = NULL;
|
|
tbstream_ring_free(&sdev->tx_ring);
|
|
tb_ring_free(sdev->tx_ring.ring);
|
|
sdev->tx_ring.ring = NULL;
|
|
}
|
|
|
|
static ssize_t
|
|
tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to)
|
|
{
|
|
struct file *file = kiocb->ki_filp;
|
|
struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
|
|
size_t nbytes;
|
|
int ret;
|
|
|
|
ret = tbstream_dev_valid(sdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (mutex_lock_interruptible(&sdev->lock))
|
|
return -ERESTARTSYS;
|
|
|
|
while (!tbstream_ring_available(&sdev->rx_ring)) {
|
|
mutex_unlock(&sdev->lock);
|
|
|
|
if (file->f_flags & O_NONBLOCK)
|
|
return -EAGAIN;
|
|
ret = wait_event_interruptible(sdev->wait,
|
|
tbstream_ring_available(&sdev->rx_ring) ||
|
|
tbstream_dev_valid(sdev) != 0 ||
|
|
tbstream_dev_closed(sdev) ||
|
|
tbstream_dev_removed(sdev));
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = tbstream_dev_valid(sdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev))
|
|
return 0;
|
|
|
|
if (mutex_lock_interruptible(&sdev->lock))
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
nbytes = 0;
|
|
while (nbytes < iov_iter_count(to)) {
|
|
struct tbstream_frame *sf;
|
|
size_t size, sf_size;
|
|
|
|
sf = tbstream_dev_completed_rx(sdev);
|
|
if (!sf)
|
|
break;
|
|
/*
|
|
* CLOSE tunneled packet. If userspace already read
|
|
* something then we stop processing now and return
|
|
* those bytes. Next time the first frame will be CLOSE
|
|
* in which case we return EOF to the user.
|
|
*/
|
|
if (sf->frame.eof == TBSTREAM_CLOSE) {
|
|
if (!nbytes) {
|
|
tbstream_dev_consume_rx(sdev);
|
|
sdev->closed = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
sf_size = tb_ring_frame_size(&sf->frame);
|
|
size = min(iov_iter_count(to) - nbytes, sf_size);
|
|
|
|
if (copy_page_to_iter(sf->page, sf->offset, size, to) != size) {
|
|
ret = -EFAULT;
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* If not all data from the frame is read so leave it in
|
|
* place and update the offset accordingly so next read
|
|
* gets the rest.
|
|
*/
|
|
if (size < sf_size) {
|
|
sf->offset += size;
|
|
sf->frame.size = sf_size - size;
|
|
} else {
|
|
ret = tbstream_dev_consume_rx(sdev);
|
|
if (ret)
|
|
break;
|
|
}
|
|
|
|
nbytes += size;
|
|
}
|
|
|
|
mutex_unlock(&sdev->lock);
|
|
if (ret)
|
|
return ret;
|
|
return nbytes;
|
|
}
|
|
|
|
static ssize_t
|
|
tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from)
|
|
{
|
|
struct file *file = kiocb->ki_filp;
|
|
struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
|
|
size_t nbytes;
|
|
int ret;
|
|
|
|
ret = tbstream_dev_valid(sdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (mutex_lock_interruptible(&sdev->lock))
|
|
return -ERESTARTSYS;
|
|
|
|
while (!tbstream_ring_available(&sdev->tx_ring)) {
|
|
mutex_unlock(&sdev->lock);
|
|
|
|
if (file->f_flags & O_NONBLOCK)
|
|
return -EAGAIN;
|
|
ret = wait_event_interruptible(sdev->wait,
|
|
tbstream_ring_available(&sdev->tx_ring) ||
|
|
tbstream_dev_valid(sdev) != 0 ||
|
|
tbstream_dev_closed(sdev) ||
|
|
tbstream_dev_removed(sdev));
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = tbstream_dev_valid(sdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev))
|
|
return -ENXIO;
|
|
|
|
if (mutex_lock_interruptible(&sdev->lock))
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
nbytes = 0;
|
|
while (nbytes < iov_iter_count(from)) {
|
|
size_t size;
|
|
|
|
size = min(iov_iter_count(from) - nbytes, TB_MAX_FRAME_SIZE);
|
|
ret = tbstream_dev_send_data(sdev, from, size);
|
|
if (ret) {
|
|
/*
|
|
* If there are no more buffers we are done for
|
|
* this write.
|
|
*/
|
|
if (ret == -ENOBUFS)
|
|
ret = 0;
|
|
break;
|
|
}
|
|
|
|
nbytes += size;
|
|
}
|
|
|
|
mutex_unlock(&sdev->lock);
|
|
if (ret)
|
|
return ret;
|
|
return nbytes;
|
|
}
|
|
|
|
static __poll_t
|
|
tbstream_dev_fops_poll(struct file *file, struct poll_table_struct *wait)
|
|
{
|
|
struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
|
|
__poll_t mask = 0;
|
|
|
|
poll_wait(file, &sdev->wait, wait);
|
|
guard(mutex)(&sdev->lock);
|
|
if (tbstream_dev_valid(sdev) != 0) {
|
|
mask |= EPOLLHUP | EPOLLERR;
|
|
} else {
|
|
if (tbstream_ring_available(&sdev->tx_ring))
|
|
mask |= EPOLLOUT | EPOLLWRNORM;
|
|
if (tbstream_ring_available(&sdev->rx_ring))
|
|
mask |= EPOLLIN | EPOLLRDNORM;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
static int tbstream_dev_fops_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
|
|
int ret;
|
|
|
|
tbstream_dev_get(sdev);
|
|
|
|
if (mutex_lock_interruptible(&sdev->lock)) {
|
|
tbstream_dev_put(sdev);
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
/*
|
|
* If there is no stream attached yet, block until it appears
|
|
* unless this is opened in non-blocking mode.
|
|
*/
|
|
while ((ret = tbstream_dev_valid(sdev))) {
|
|
mutex_unlock(&sdev->lock);
|
|
|
|
if (ret != -ENXIO || (file->f_flags & O_NONBLOCK))
|
|
goto err_put;
|
|
|
|
ret = wait_event_interruptible(sdev->wait,
|
|
tbstream_dev_valid(sdev) == 0 ||
|
|
tbstream_dev_removed(sdev));
|
|
if (ret)
|
|
goto err_put;
|
|
|
|
if (tbstream_dev_removed(sdev)) {
|
|
ret = -ENXIO;
|
|
goto err_put;
|
|
}
|
|
|
|
if (mutex_lock_interruptible(&sdev->lock)) {
|
|
ret = -ERESTARTSYS;
|
|
goto err_put;
|
|
}
|
|
}
|
|
|
|
/* Only on first open we allocate rings and enable paths */
|
|
if (!sdev->users++) {
|
|
ret = tbstream_dev_start(sdev);
|
|
if (ret) {
|
|
sdev->users--;
|
|
goto err_unlock;
|
|
}
|
|
sdev->closed = false;
|
|
}
|
|
|
|
mutex_unlock(&sdev->lock);
|
|
return 0;
|
|
|
|
err_unlock:
|
|
mutex_unlock(&sdev->lock);
|
|
err_put:
|
|
tbstream_dev_put(sdev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tbstream_dev_fops_release(struct inode *inode, struct file *file)
|
|
{
|
|
struct tbstream_dev *sdev = to_tbstream_dev(file->private_data);
|
|
|
|
mutex_lock(&sdev->lock);
|
|
if (--sdev->users == 0) {
|
|
/*
|
|
* Send CLOSE tunneled packet to notify the other end
|
|
* that we are closing the file. We do this twice if the
|
|
* first one fails.
|
|
*/
|
|
tbstream_dev_send_close(sdev);
|
|
tbstream_dev_stop(sdev);
|
|
}
|
|
mutex_unlock(&sdev->lock);
|
|
|
|
tbstream_dev_put(sdev);
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations tbstream_dev_fops = {
|
|
.owner = THIS_MODULE,
|
|
.llseek = noop_llseek,
|
|
.read_iter = tbstream_dev_fops_read_iter,
|
|
.write_iter = tbstream_dev_fops_write_iter,
|
|
.poll = tbstream_dev_fops_poll,
|
|
.open = tbstream_dev_fops_open,
|
|
.release = tbstream_dev_fops_release,
|
|
};
|
|
|
|
static inline struct tbstream_dev *
|
|
tbstream_dev_from_group(struct config_group *group)
|
|
{
|
|
return container_of(group, struct tbstream_dev, group);
|
|
}
|
|
|
|
static ssize_t tbstream_dev_index_show(struct config_item *item, char *buf)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
|
|
return sysfs_emit(buf, "%d\n", sdev->index);
|
|
}
|
|
CONFIGFS_ATTR_RO(tbstream_dev_, index);
|
|
|
|
static ssize_t tbstream_dev_in_hopid_show(struct config_item *item, char *buf)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
|
|
return sysfs_emit(buf, "%d\n", sdev->in_hopid);
|
|
}
|
|
|
|
/* svc->lock must be held */
|
|
static void service_remove_properties(struct tb_service *svc, const char *name)
|
|
{
|
|
struct tb_property *p;
|
|
|
|
if (!svc->local_properties)
|
|
return;
|
|
|
|
p = tb_property_find(svc->local_properties, name,
|
|
TB_PROPERTY_TYPE_DIRECTORY);
|
|
if (p) {
|
|
tb_property_free_dir(p->value.dir);
|
|
tb_property_remove(p);
|
|
|
|
dev_dbg(&svc->dev, "removed local directory %s\n", name);
|
|
|
|
/*
|
|
* Is the service directory empty already? If it is then
|
|
* we can release it as well.
|
|
*/
|
|
tb_property_for_each(svc->local_properties, p) {
|
|
if (p->type == TB_PROPERTY_TYPE_DIRECTORY)
|
|
return;
|
|
}
|
|
|
|
tb_property_free_dir(svc->local_properties);
|
|
svc->local_properties = NULL;
|
|
}
|
|
}
|
|
|
|
static int service_update_properties(struct tb_service *svc, const char *name,
|
|
int in_hopid, int out_hopid)
|
|
{
|
|
struct tb_property_dir *dir;
|
|
struct tb_property *p;
|
|
|
|
guard(mutex)(&svc->lock);
|
|
|
|
if (in_hopid < 8 || out_hopid < 8) {
|
|
service_remove_properties(svc, name);
|
|
return 0;
|
|
}
|
|
|
|
if (!svc->local_properties) {
|
|
/*
|
|
* Add the service directory first time we
|
|
* populate the entries.
|
|
*/
|
|
svc->local_properties = tb_property_copy_dir(tbstream_dir);
|
|
if (!svc->local_properties)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
p = tb_property_find(svc->local_properties, name,
|
|
TB_PROPERTY_TYPE_DIRECTORY);
|
|
if (p) {
|
|
dir = p->value.dir;
|
|
|
|
p = tb_property_find(dir, "inhopid", TB_PROPERTY_TYPE_VALUE);
|
|
if (p && p->value.immediate != in_hopid)
|
|
p->value.immediate = in_hopid;
|
|
p = tb_property_find(dir, "outhopid", TB_PROPERTY_TYPE_VALUE);
|
|
if (p && p->value.immediate != out_hopid)
|
|
p->value.immediate = out_hopid;
|
|
|
|
dev_dbg(&svc->dev,
|
|
"updated local directory %s: in HopID %d, out HopID %d\n",
|
|
name, in_hopid, out_hopid);
|
|
} else {
|
|
uuid_t uuid;
|
|
int ret;
|
|
|
|
uuid_gen(&uuid);
|
|
dir = tb_property_create_dir(&uuid);
|
|
if (!dir)
|
|
return -ENOMEM;
|
|
|
|
tb_property_add_immediate(dir, "inhopid", in_hopid);
|
|
tb_property_add_immediate(dir, "outhopid", out_hopid);
|
|
|
|
ret = tb_property_add_dir(svc->local_properties, name, dir);
|
|
if (ret) {
|
|
tb_property_free_dir(dir);
|
|
return ret;
|
|
}
|
|
|
|
dev_dbg(&svc->dev,
|
|
"added local directory %s: in HopID %d, out HopID %d\n",
|
|
name, in_hopid, out_hopid);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tbstream_dev_update_properties(struct tbstream_dev *sdev)
|
|
{
|
|
struct tbstream *stream;
|
|
int ret;
|
|
|
|
stream = tbstream_get(sdev->stream);
|
|
if (!stream)
|
|
return 0;
|
|
|
|
ret = service_update_properties(stream->svc,
|
|
config_item_name(&sdev->group.cg_item),
|
|
sdev->in_hopid, sdev->out_hopid);
|
|
if (!ret)
|
|
tb_service_properties_changed(stream->svc);
|
|
|
|
tbstream_put(stream);
|
|
return ret;
|
|
}
|
|
|
|
static int tbstream_dev_alloc_in_hopid(struct tbstream_dev *sdev, int hopid)
|
|
{
|
|
struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
|
|
int ret;
|
|
|
|
if (sdev->in_hopid > 0 && sdev->in_hopid != hopid)
|
|
tb_xdomain_release_in_hopid(xd, sdev->in_hopid);
|
|
if (!hopid) {
|
|
sdev->in_hopid = hopid;
|
|
return 0;
|
|
}
|
|
ret = tb_xdomain_alloc_in_hopid(xd, hopid);
|
|
if (ret < 0)
|
|
return ret;
|
|
/*
|
|
* If specific HopID was asked by the user and we did not get
|
|
* that one then release and return error instead.
|
|
*/
|
|
if (hopid > 0 && hopid != ret) {
|
|
tb_xdomain_release_in_hopid(xd, ret);
|
|
return -EBUSY;
|
|
}
|
|
sdev->in_hopid = ret;
|
|
return 0;
|
|
}
|
|
|
|
static int tbstream_dev_alloc_out_hopid(struct tbstream_dev *sdev, int hopid)
|
|
{
|
|
struct tb_xdomain *xd = tbstream_dev_xdomain(sdev);
|
|
int ret;
|
|
|
|
if (sdev->out_hopid > 0 && sdev->out_hopid != hopid)
|
|
tb_xdomain_release_out_hopid(xd, sdev->out_hopid);
|
|
if (!hopid) {
|
|
sdev->out_hopid = hopid;
|
|
return 0;
|
|
}
|
|
ret = tb_xdomain_alloc_out_hopid(xd, hopid);
|
|
if (ret < 0)
|
|
return ret;
|
|
if (hopid > 0 && hopid != ret) {
|
|
tb_xdomain_release_out_hopid(xd, ret);
|
|
return -EBUSY;
|
|
}
|
|
sdev->out_hopid = ret;
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t
|
|
tbstream_dev_in_hopid_store(struct config_item *item, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
int ret, in_hopid;
|
|
|
|
ret = kstrtoint(buf, 0, &in_hopid);
|
|
if (ret)
|
|
return ret;
|
|
|
|
guard(mutex)(&sdev->lock);
|
|
if (sdev->users)
|
|
return -EBUSY;
|
|
if (sdev->stream) {
|
|
ret = tbstream_dev_alloc_in_hopid(sdev, in_hopid);
|
|
if (ret)
|
|
return ret;
|
|
ret = tbstream_dev_update_properties(sdev);
|
|
} else {
|
|
sdev->in_hopid = in_hopid;
|
|
}
|
|
return ret ? ret : count;
|
|
}
|
|
CONFIGFS_ATTR(tbstream_dev_, in_hopid);
|
|
|
|
static ssize_t tbstream_dev_out_hopid_show(struct config_item *item, char *buf)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
|
|
return sysfs_emit(buf, "%d\n", sdev->out_hopid);
|
|
}
|
|
|
|
static ssize_t
|
|
tbstream_dev_out_hopid_store(struct config_item *item, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
int ret, out_hopid;
|
|
|
|
ret = kstrtoint(buf, 0, &out_hopid);
|
|
if (ret)
|
|
return ret;
|
|
|
|
guard(mutex)(&sdev->lock);
|
|
if (sdev->users)
|
|
return -EBUSY;
|
|
if (sdev->stream) {
|
|
ret = tbstream_dev_alloc_out_hopid(sdev, out_hopid);
|
|
if (ret)
|
|
return ret;
|
|
ret = tbstream_dev_update_properties(sdev);
|
|
} else {
|
|
sdev->out_hopid = out_hopid;
|
|
}
|
|
return ret ? ret : count;
|
|
}
|
|
CONFIGFS_ATTR(tbstream_dev_, out_hopid);
|
|
|
|
static ssize_t tbstream_dev_ring_size_show(struct config_item *item, char *buf)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
|
|
return sysfs_emit(buf, "%u\n", sdev->ring_size);
|
|
}
|
|
|
|
static ssize_t
|
|
tbstream_dev_ring_size_store(struct config_item *item, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
unsigned int ring_size;
|
|
int ret;
|
|
|
|
ret = kstrtouint(buf, 0, &ring_size);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (ring_size < TBSTREAM_DEV_MIN_RING_SIZE ||
|
|
ring_size > TBSTREAM_DEV_MAX_RING_SIZE)
|
|
return -EINVAL;
|
|
|
|
guard(mutex)(&sdev->lock);
|
|
if (sdev->users)
|
|
return -EBUSY;
|
|
sdev->ring_size = ring_size;
|
|
return count;
|
|
}
|
|
CONFIGFS_ATTR(tbstream_dev_, ring_size);
|
|
|
|
static ssize_t tbstream_dev_throttling_show(struct config_item *item, char *buf)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
|
|
return sysfs_emit(buf, "%u\n", sdev->throttling);
|
|
}
|
|
|
|
static ssize_t
|
|
tbstream_dev_throttling_store(struct config_item *item, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
unsigned int throttling;
|
|
int ret;
|
|
|
|
ret = kstrtouint(buf, 0, &throttling);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (throttling > TBSTREAM_DEV_MAX_THROTTLING)
|
|
return -EINVAL;
|
|
|
|
guard(mutex)(&sdev->lock);
|
|
if (sdev->users)
|
|
return -EBUSY;
|
|
sdev->throttling = throttling;
|
|
return count;
|
|
}
|
|
CONFIGFS_ATTR(tbstream_dev_, throttling);
|
|
|
|
static struct configfs_attribute *tbstream_dev_attrs[] = {
|
|
&tbstream_dev_attr_index,
|
|
&tbstream_dev_attr_in_hopid,
|
|
&tbstream_dev_attr_out_hopid,
|
|
&tbstream_dev_attr_ring_size,
|
|
&tbstream_dev_attr_throttling,
|
|
NULL,
|
|
};
|
|
|
|
static void tbstream_dev_item_release(struct config_item *item)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(group);
|
|
|
|
misc_deregister(&sdev->misc);
|
|
tbstream_dev_put(sdev);
|
|
}
|
|
|
|
static struct configfs_item_operations tbstream_dev_item_ops = {
|
|
.release = tbstream_dev_item_release,
|
|
};
|
|
|
|
static const struct config_item_type tbstream_dev_type = {
|
|
.ct_owner = THIS_MODULE,
|
|
.ct_item_ops = &tbstream_dev_item_ops,
|
|
.ct_attrs = tbstream_dev_attrs,
|
|
};
|
|
|
|
static void service_get_hopids(struct tb_service *svc, const char *name,
|
|
int *in_hopid, int *out_hopid)
|
|
{
|
|
struct tb_property_dir *dir;
|
|
struct tb_property *p;
|
|
|
|
guard(mutex)(&svc->lock);
|
|
|
|
/* See if we have directory entry with the matching name */
|
|
p = tb_property_find(svc->remote_properties, name,
|
|
TB_PROPERTY_TYPE_DIRECTORY);
|
|
if (!p)
|
|
return;
|
|
|
|
dir = p->value.dir;
|
|
|
|
/*
|
|
* We need to reverse the HopIDs on our end so that in becomes
|
|
* out and vice versa.
|
|
*/
|
|
p = tb_property_find(dir, "inhopid", TB_PROPERTY_TYPE_VALUE);
|
|
if (p && p->value.immediate >= 8)
|
|
*out_hopid = p->value.immediate;
|
|
p = tb_property_find(dir, "outhopid", TB_PROPERTY_TYPE_VALUE);
|
|
if (p && p->value.immediate >= 8)
|
|
*in_hopid = p->value.immediate;
|
|
}
|
|
|
|
static void
|
|
tbstream_dev_attach_stream(struct tbstream_dev *sdev, struct tbstream_group *sg)
|
|
{
|
|
const char *name = config_item_name(&sdev->group.cg_item);
|
|
struct tbstream *stream;
|
|
|
|
stream = tbstream_get(sg->stream);
|
|
if (!stream)
|
|
return;
|
|
|
|
scoped_guard(mutex, &sdev->lock) {
|
|
sdev->stream = stream;
|
|
/*
|
|
* If there is no existing configuration (or automatic
|
|
* configuration is being used) check if the other side
|
|
* has configuration for this and use it.
|
|
*/
|
|
if (sdev->in_hopid <= 0 && sdev->out_hopid <= 0)
|
|
service_get_hopids(stream->svc, name, &sdev->in_hopid,
|
|
&sdev->out_hopid);
|
|
if (sdev->in_hopid)
|
|
tbstream_dev_alloc_in_hopid(sdev, sdev->in_hopid);
|
|
if (sdev->out_hopid)
|
|
tbstream_dev_alloc_out_hopid(sdev, sdev->out_hopid);
|
|
}
|
|
|
|
service_update_properties(stream->svc, name, sdev->in_hopid,
|
|
sdev->out_hopid);
|
|
tb_service_properties_changed(stream->svc);
|
|
|
|
/* Notify any openerers that the stream is now attached */
|
|
wake_up_interruptible(&sdev->wait);
|
|
}
|
|
|
|
static void tbstream_dev_detach_stream(struct tbstream_dev *sdev)
|
|
{
|
|
const char *name = config_item_name(&sdev->group.cg_item);
|
|
struct tbstream *stream;
|
|
struct tb_xdomain *xd;
|
|
|
|
scoped_guard(mutex, &sdev->lock) {
|
|
stream = sdev->stream;
|
|
if (!stream)
|
|
return;
|
|
sdev->stream = NULL;
|
|
xd = tb_service_parent(stream->svc);
|
|
if (sdev->out_hopid > 0)
|
|
tb_xdomain_release_out_hopid(xd, sdev->out_hopid);
|
|
if (sdev->in_hopid > 0)
|
|
tb_xdomain_release_in_hopid(xd, sdev->in_hopid);
|
|
}
|
|
|
|
service_update_properties(stream->svc, name, 0, 0);
|
|
tb_service_properties_changed(stream->svc);
|
|
|
|
tbstream_put(stream);
|
|
|
|
/* Notify any task that the stream is not valid anymore */
|
|
wake_up_interruptible_poll(&sdev->wait, EPOLLHUP | EPOLLERR);
|
|
}
|
|
|
|
static inline struct tbstream_group *
|
|
to_tbstream_group(struct config_group *group)
|
|
{
|
|
return container_of(group, struct tbstream_group, group);
|
|
}
|
|
|
|
static struct config_group *
|
|
tbstream_dev_make_group(struct config_group *group, const char *name)
|
|
{
|
|
struct tbstream_group *sg = to_tbstream_group(group);
|
|
struct tbstream_dev *sdev;
|
|
int ret, index;
|
|
|
|
/*
|
|
* We want the names to be suitable for passing as property
|
|
* directory names.
|
|
*/
|
|
if (strlen(name) > TB_PROPERTY_KEY_SIZE)
|
|
return ERR_PTR(-ENAMETOOLONG);
|
|
|
|
sdev = kzalloc_obj(*sdev, GFP_KERNEL);
|
|
if (!sdev)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
index = ida_alloc(&tbstream_indices, GFP_KERNEL);
|
|
if (index < 0) {
|
|
kfree(sdev);
|
|
return ERR_PTR(index);
|
|
}
|
|
|
|
sdev->index = index;
|
|
sdev->ring_size = TBSTREAM_DEV_RING_SIZE;
|
|
sdev->throttling = TBSTREAM_DEV_THROTTLING;
|
|
mutex_init(&sdev->lock);
|
|
init_waitqueue_head(&sdev->wait);
|
|
INIT_LIST_HEAD(&sdev->list);
|
|
/* This point forward tbstream_dev_put() must be used to release sdev */
|
|
kref_init(&sdev->kref);
|
|
|
|
config_group_init_type_name(&sdev->group, name, &tbstream_dev_type);
|
|
|
|
scoped_guard(mutex, &sg->lock)
|
|
list_add_tail(&sdev->list, &sg->dev_list);
|
|
|
|
tbstream_dev_attach_stream(sdev, sg);
|
|
|
|
sdev->misc.name = kasprintf(GFP_KERNEL, "tbstream%d", index);
|
|
sdev->misc.minor = MISC_DYNAMIC_MINOR;
|
|
sdev->misc.fops = &tbstream_dev_fops;
|
|
|
|
ret = misc_register(&sdev->misc);
|
|
if (ret) {
|
|
tbstream_dev_detach_stream(sdev);
|
|
scoped_guard(mutex, &sg->lock)
|
|
list_del(&sdev->list);
|
|
/* Calls tbstream_dev_put() */
|
|
config_group_put(&sdev->group);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
return &sdev->group;
|
|
}
|
|
|
|
static void
|
|
tbstream_dev_drop_item(struct config_group *group, struct config_item *item)
|
|
{
|
|
struct config_group *sdev_group = to_config_group(item);
|
|
struct tbstream_dev *sdev = tbstream_dev_from_group(sdev_group);
|
|
struct tbstream_group *sg = to_tbstream_group(group);
|
|
|
|
scoped_guard(mutex, &sg->lock)
|
|
list_del(&sdev->list);
|
|
/* Notify any task that the underlying group was removed */
|
|
sdev->removed = true;
|
|
wake_up_interruptible_poll(&sdev->wait, EPOLLHUP | EPOLLERR);
|
|
config_item_put(item);
|
|
}
|
|
|
|
static struct configfs_group_operations tbstream_dev_group_ops = {
|
|
.make_group = tbstream_dev_make_group,
|
|
.drop_item = tbstream_dev_drop_item,
|
|
};
|
|
|
|
static void tbstream_item_release(struct config_item *item)
|
|
{
|
|
struct config_group *group = to_config_group(item);
|
|
struct tbstream_group *sg = to_tbstream_group(group);
|
|
|
|
tbstream_put(sg->stream);
|
|
kfree(sg);
|
|
}
|
|
|
|
static struct configfs_item_operations tbstream_item_ops = {
|
|
.release = tbstream_item_release,
|
|
};
|
|
|
|
static const struct config_item_type tbstream_dev_group_type = {
|
|
.ct_owner = THIS_MODULE,
|
|
.ct_group_ops = &tbstream_dev_group_ops,
|
|
.ct_item_ops = &tbstream_item_ops,
|
|
};
|
|
|
|
static struct config_group *
|
|
tbstream_make_group(struct config_group *group, const char *name)
|
|
{
|
|
struct tbstream_group *sg;
|
|
struct tbstream *stream;
|
|
int domain, index;
|
|
u64 route;
|
|
|
|
/* Make sure the format is correct */
|
|
if (sscanf(name, "%u-%llx.%u", &domain, &route, &index) != 3)
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
sg = kzalloc_obj(*sg, GFP_KERNEL);
|
|
if (!sg)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
mutex_init(&sg->lock);
|
|
INIT_LIST_HEAD(&sg->dev_list);
|
|
|
|
guard(mutex)(&tbstream_lock);
|
|
list_for_each_entry(stream, &tbstream_list, list) {
|
|
tbstream_get(stream);
|
|
if (sysfs_streq(name, dev_name(&stream->svc->dev))) {
|
|
sg->stream = stream;
|
|
break;
|
|
}
|
|
tbstream_put(stream);
|
|
}
|
|
|
|
config_group_init_type_name(&sg->group, name, &tbstream_dev_group_type);
|
|
return &sg->group;
|
|
}
|
|
|
|
static struct configfs_group_operations tbstream_group_ops = {
|
|
.make_group = tbstream_make_group,
|
|
};
|
|
|
|
static const struct config_item_type tbstream_group_type = {
|
|
.ct_owner = THIS_MODULE,
|
|
.ct_group_ops = &tbstream_group_ops,
|
|
};
|
|
|
|
static struct config_group tbstream_group = {
|
|
.cg_item = {
|
|
.ci_namebuf = "stream",
|
|
.ci_type = &tbstream_group_type,
|
|
},
|
|
};
|
|
|
|
/* Returns reference count increased */
|
|
static struct tbstream_group *tbstream_group_find(struct tbstream *stream)
|
|
{
|
|
const char *name = dev_name(&stream->svc->dev);
|
|
struct config_item *item;
|
|
|
|
guard(mutex)(&tbstream_group.cg_subsys->su_mutex);
|
|
item = config_group_find_item(&tbstream_group, name);
|
|
if (!item)
|
|
return NULL;
|
|
return to_tbstream_group(to_config_group(item));
|
|
}
|
|
|
|
static void tbstream_group_attach_stream(struct tbstream *stream)
|
|
{
|
|
struct tbstream_group *sg;
|
|
struct tbstream_dev *sdev;
|
|
|
|
sg = tbstream_group_find(stream);
|
|
if (!sg)
|
|
return;
|
|
|
|
guard(mutex)(&sg->lock);
|
|
if (WARN_ON(sg->stream)) {
|
|
config_group_put(&sg->group);
|
|
return;
|
|
}
|
|
sg->stream = tbstream_get(stream);
|
|
/*
|
|
* If there are existing stream devices, attach the stream to
|
|
* them now.
|
|
*/
|
|
list_for_each_entry(sdev, &sg->dev_list, list) {
|
|
tbstream_dev_get(sdev);
|
|
tbstream_dev_attach_stream(sdev, sg);
|
|
tbstream_dev_put(sdev);
|
|
}
|
|
|
|
config_group_put(&sg->group);
|
|
}
|
|
|
|
static void tbstream_group_detach_stream(struct tbstream *stream)
|
|
{
|
|
struct tbstream_group *sg;
|
|
struct tbstream_dev *sdev;
|
|
|
|
sg = tbstream_group_find(stream);
|
|
if (!sg)
|
|
return;
|
|
|
|
guard(mutex)(&sg->lock);
|
|
if (sg->stream) {
|
|
/* Detach this stream from the stream devices */
|
|
list_for_each_entry_reverse(sdev, &sg->dev_list, list) {
|
|
tbstream_dev_get(sdev);
|
|
tbstream_dev_detach_stream(sdev);
|
|
tbstream_dev_put(sdev);
|
|
}
|
|
tbstream_put(sg->stream);
|
|
sg->stream = NULL;
|
|
}
|
|
|
|
config_group_put(&sg->group);
|
|
}
|
|
|
|
static int tbstream_probe(struct tb_service *svc, const struct tb_service_id *id)
|
|
{
|
|
struct tbstream *stream;
|
|
|
|
stream = kzalloc_obj(*stream, GFP_KERNEL);
|
|
if (!stream)
|
|
return -ENOMEM;
|
|
|
|
/* After this point, release stream by calling tbstream_put() */
|
|
kref_init(&stream->kref);
|
|
stream->svc = tb_service_get(svc);
|
|
INIT_LIST_HEAD(&stream->list);
|
|
|
|
scoped_guard(mutex, &tbstream_lock)
|
|
list_add_tail(&stream->list, &tbstream_list);
|
|
|
|
tbstream_group_attach_stream(stream);
|
|
tb_service_set_drvdata(svc, stream);
|
|
return 0;
|
|
}
|
|
|
|
static void tbstream_remove(struct tb_service *svc)
|
|
{
|
|
struct tbstream *stream = tb_service_get_drvdata(svc);
|
|
|
|
tbstream_group_detach_stream(stream);
|
|
scoped_guard(mutex, &tbstream_lock)
|
|
list_del(&stream->list);
|
|
tbstream_put(stream);
|
|
}
|
|
|
|
static int __maybe_unused tbstream_suspend(struct device *dev)
|
|
{
|
|
struct tb_service *svc = tb_to_service(dev);
|
|
struct tbstream *stream = tb_service_get_drvdata(svc);
|
|
struct tbstream_group *sg;
|
|
struct tbstream_dev *sdev;
|
|
|
|
sg = tbstream_group_find(stream);
|
|
if (!sg)
|
|
return 0;
|
|
|
|
list_for_each_entry_reverse(sdev, &sg->dev_list, list) {
|
|
tbstream_dev_get(sdev);
|
|
/* Stop the stream (if it was open) */
|
|
if (sdev->users)
|
|
tbstream_dev_stop(sdev);
|
|
tbstream_dev_put(sdev);
|
|
}
|
|
|
|
config_group_put(&sg->group);
|
|
return 0;
|
|
}
|
|
|
|
static int __maybe_unused tbstream_resume(struct device *dev)
|
|
{
|
|
struct tb_service *svc = tb_to_service(dev);
|
|
struct tbstream *stream = tb_service_get_drvdata(svc);
|
|
struct tbstream_group *sg;
|
|
struct tbstream_dev *sdev;
|
|
|
|
sg = tbstream_group_find(stream);
|
|
if (!sg)
|
|
return 0;
|
|
|
|
list_for_each_entry(sdev, &sg->dev_list, list) {
|
|
tbstream_dev_get(sdev);
|
|
if (sdev->users) {
|
|
int ret;
|
|
|
|
ret = tbstream_dev_start(sdev);
|
|
if (ret) {
|
|
tbstream_dev_put(sdev);
|
|
config_group_put(&sg->group);
|
|
return ret;
|
|
}
|
|
}
|
|
tbstream_dev_put(sdev);
|
|
}
|
|
|
|
config_group_put(&sg->group);
|
|
return 0;
|
|
}
|
|
|
|
static const struct dev_pm_ops tbstream_pm_ops = {
|
|
SET_SYSTEM_SLEEP_PM_OPS(tbstream_suspend, tbstream_resume)
|
|
};
|
|
|
|
static const struct tb_service_id tbstream_ids[] = {
|
|
{ TB_SERVICE("stream", 1) },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(tbsvc, tbstream_ids);
|
|
|
|
static struct tb_service_driver tbstream_driver = {
|
|
.driver = {
|
|
.owner = THIS_MODULE,
|
|
.name = "thunderbolt_stream",
|
|
.pm = &tbstream_pm_ops,
|
|
},
|
|
.probe = tbstream_probe,
|
|
.remove = tbstream_remove,
|
|
.id_table = tbstream_ids,
|
|
};
|
|
|
|
static int __init tbstream_init(void)
|
|
{
|
|
int ret;
|
|
|
|
tbstream_dir = tb_property_create_dir(&tbstream_dir_uuid);
|
|
if (!tbstream_dir)
|
|
return -ENOMEM;
|
|
|
|
tb_property_add_immediate(tbstream_dir, "prtcid", 1);
|
|
tb_property_add_immediate(tbstream_dir, "prtcvers", 1);
|
|
tb_property_add_immediate(tbstream_dir, "prtcrevs", 0);
|
|
tb_property_add_immediate(tbstream_dir, "prtcstns", 0);
|
|
|
|
ret = tb_register_property_dir("stream", tbstream_dir);
|
|
if (ret)
|
|
goto err_free_dir;
|
|
|
|
config_group_init(&tbstream_group);
|
|
ret = tb_configfs_register_group(&tbstream_group);
|
|
if (ret)
|
|
goto err_unregister_dir;
|
|
|
|
ret = tb_register_service_driver(&tbstream_driver);
|
|
if (ret)
|
|
goto err_unregister_group;
|
|
return 0;
|
|
|
|
err_unregister_group:
|
|
tb_configfs_unregister_group(&tbstream_group);
|
|
err_unregister_dir:
|
|
tb_unregister_property_dir("stream", tbstream_dir);
|
|
err_free_dir:
|
|
tb_property_free_dir(tbstream_dir);
|
|
return ret;
|
|
}
|
|
module_init(tbstream_init);
|
|
|
|
static void __exit tbstream_exit(void)
|
|
{
|
|
tb_unregister_service_driver(&tbstream_driver);
|
|
tb_configfs_unregister_group(&tbstream_group);
|
|
tb_unregister_property_dir("stream", tbstream_dir);
|
|
tb_property_free_dir(tbstream_dir);
|
|
ida_destroy(&tbstream_indices);
|
|
}
|
|
module_exit(tbstream_exit);
|
|
|
|
MODULE_AUTHOR("Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>");
|
|
MODULE_AUTHOR("Mika Westerberg <mika.westerberg@linux.intel.com>");
|
|
MODULE_DESCRIPTION("Stream data over Thunderbolt/USB4 cable");
|
|
MODULE_LICENSE("GPL");
|