blob: a5bf58c80dd4c69da5014e770385c61c376e0521 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#ifndef ZIRCON_KERNEL_OBJECT_INCLUDE_OBJECT_BUFFER_CHAIN_H_
#define ZIRCON_KERNEL_OBJECT_INCLUDE_OBJECT_BUFFER_CHAIN_H_
#include <lib/page_cache.h>
#include <lib/user_copy/user_ptr.h>
#include <stdint.h>
#include <string.h>
#include <zircon/types.h>
#include <cstddef>
#include <new>
#include <fbl/algorithm.h>
#include <fbl/canary.h>
#include <fbl/intrusive_single_list.h>
#include <ktl/algorithm.h>
#include <ktl/move.h>
#include <vm/page.h>
#include <vm/physmap.h>
#include <vm/pmm.h>
// BufferChain is a list of buffers allocated from the PMM.
//
// It's designed for use with channel messages. Pages backing a BufferChain are marked as
// vm_page_state::IPC.
//
// The BufferChain object itself lives *inside* its first buffer. Here's what it looks like:
//
// +--------------------------------+ +--------------------------------+
// | page | | page |
// |+------------------------------+| |+------------------------------+|
// || Buffer |+---->|| Buffer ||
// || raw_data: +---------------+|| || raw_data: +--------------+||
// || | BufferChain ||| || | message data |||
// || reserved -> | ~~~~~~~~~~~~~ ||| || reserved = 0 | continued |||
// || | message data ||| || | |||
// || +---------------+|| || +--------------+||
// |+------------------------------+| |+------------------------------+|
// +--------------------------------+ +--------------------------------+
//
// BufferChain does not dynamically allocate. An initial Alloc() call allocates a list of
// pages that will be used to build the BufferChain. This allocation can exceed the size
// actually used by the buffer chain and the excess buffers can be freed with a call to
// FreeUnusedBuffers(). The motivation for sometimes allocating more than needed and later
// freeing is that the number of needed buffers is sometimes initially unknown and it is
// presumed to be more efficient to do a single allocation than multiple allocations.
//
// BufferChain uses a private PageCache to improve performance under load by
// avoiding contention on the PMM. The page cache is tunable by the kernel
// command line parameter kernel.bufferchain.reserve-pages.
//
class BufferChain {
public:
class Buffer;
typedef fbl::SinglyLinkedList<Buffer*> BufferList;
constexpr static size_t kSizeOfBuffer = PAGE_SIZE;
constexpr static size_t kSizeOfBufferFields = 16;
// kRawDataSize is the maximum number of bytes that can fit in a single Buffer.
//
// However, not every Buffer in a BufferChain can store this many bytes. The first Buffer in a
// BufferChain is special because it also stores the chain itself. See also kContig.
constexpr static size_t kRawDataSize = kSizeOfBuffer - kSizeOfBufferFields;
// Unfortunately, we don't yet know sizeof(BufferChain) so estimate and rely on static_asserts
// further down to verify.
constexpr static size_t kSizeOfBufferChain =
sizeof(BufferList) + 2 * sizeof(list_node) + sizeof(size_t) + sizeof(BufferList::iterator);
// kContig is the number of bytes guaranteed to be stored contiguously in any buffer
constexpr static size_t kContig = kRawDataSize - kSizeOfBufferChain;
// Copies |size| bytes from this chain starting at offset |src_offset| to |dst|.
//
// |src_offset| must be in the range [0, kContig).
zx_status_t CopyOut(user_out_ptr<char> dst, size_t src_offset, size_t size) {
DEBUG_ASSERT(src_offset < buffers_.front().size());
size_t copy_offset = src_offset;
size_t rem = size;
const auto end = buffers_.end();
for (auto iter = buffers_.begin(); rem > 0 && iter != end; ++iter) {
const size_t copy_len = ktl::min(rem, iter->size() - copy_offset);
const char* src = iter->data() + copy_offset;
const zx_status_t status = dst.copy_array_to_user(src, copy_len);
if (unlikely(status != ZX_OK)) {
return status;
}
dst = dst.byte_offset(copy_len);
rem -= copy_len;
copy_offset = 0;
}
return ZX_OK;
}
// Creates a BufferChain with enough buffers to store |size| bytes.
//
// It is the caller's responsibility to free the chain with BufferChain::Free.
//
// Returns nullptr on error.
static BufferChain* Alloc(size_t size) {
size += sizeof(BufferChain);
const size_t num_buffers = (size + kRawDataSize - 1) / kRawDataSize;
// Allocate a list of pages.
zx::status<page_cache::PageCache::AllocateResult> unused_pages_result =
page_cache_.Allocate(num_buffers);
if (unused_pages_result.is_error()) {
return nullptr;
}
// We now have a list of pages. Allocate a page for the initial Buffer and construct the
// BufferChain within it.
BufferChain::BufferList temp;
list_node used_pages = LIST_INITIAL_VALUE(used_pages);
zx_status_t status =
AddNextBuffer(&unused_pages_result->page_list, &used_pages, &temp, temp.end());
if (unlikely(status != ZX_OK)) {
return nullptr;
}
BufferChain* chain =
new (temp.front().data()) BufferChain(&temp, &unused_pages_result->page_list, &used_pages);
return chain;
}
// Frees |chain| and its buffers.
static void Free(BufferChain* chain) {
// Remove the buffers and vm_page_t's from the chain *before* destroying it.
BufferChain::BufferList buffers(ktl::move(*chain->buffers()));
list_node pages = LIST_INITIAL_VALUE(pages);
list_move(&chain->used_pages_, &pages);
list_splice_after(&chain->unused_pages_, &pages);
chain->~BufferChain();
while (!buffers.is_empty()) {
BufferChain::Buffer* buf = buffers.pop_front();
buf->Buffer::~Buffer();
}
page_cache_.Free(ktl::move(pages));
}
// Free unused pages.
void FreeUnusedBuffers() {
page_cache_.Free(ktl::move(unused_pages_));
}
// Skips the specified number of bytes, so they won't be consumed by Append or AppendKernel.
//
// Assumes that it is called only at the beginning of the buffer chain.
void Skip(size_t size) {
DEBUG_ASSERT(buffer_offset_ == 0);
DEBUG_ASSERT(size <= kContig);
buffer_offset_ += size;
}
// Appends |size| bytes from |src| to this chain|.
//
// If there is insufficient remaining space in the buffer chain, ZX_ERR_OUT_OF_RANGE will be
// returned.
zx_status_t Append(user_in_ptr<const char> src, size_t size) { return AppendCommon(src, size); }
// Same as Append except |src| can be in kernel space.
//
// If there is insufficient remaining space in the buffer chain, ZX_ERR_OUT_OF_RANGE will be
// returned.
zx_status_t AppendKernel(const char* src, size_t size);
class Buffer final : public fbl::SinglyLinkedListable<Buffer*> {
public:
Buffer() = default;
~Buffer() = default;
char* data() {
canary_.Assert();
return raw_data_ + reserved_;
}
size_t size() const { return sizeof(raw_data_) - reserved_; }
void set_reserved(uint32_t reserved) {
DEBUG_ASSERT(reserved < sizeof(raw_data_));
reserved_ = reserved;
}
private:
fbl::Canary<fbl::magic("BUFC")> canary_;
uint32_t reserved_ = 0;
char raw_data_[kRawDataSize];
};
static_assert(sizeof(BufferChain::Buffer) == BufferChain::kSizeOfBuffer, "");
BufferList* buffers() { return &buffers_; }
static void InitializePageCache(uint32_t level);
private:
explicit BufferChain(BufferList* buffers, list_node* unused_pages, list_node* used_pages) {
buffer_tail_ = buffers->begin();
buffers_.swap(*buffers);
buffer_offset_ = 0;
list_move(unused_pages, &unused_pages_);
list_move(used_pages, &used_pages_);
// |this| now lives inside the first buffer.
buffers_.front().set_reserved(sizeof(BufferChain));
}
~BufferChain() {
DEBUG_ASSERT(list_is_empty(&used_pages_));
DEBUG_ASSERT(list_is_empty(&unused_pages_));
}
// Add a new Buffer to the end of |buffers| (marked by |buffer_tail|) by taking a page from the
// |unused_pages| list. In doing so, this page will be moved to the |used_pages| list.
static zx_status_t AddNextBuffer(list_node* unused_pages, list_node* used_pages,
BufferList* buffers, BufferList::iterator buffer_tail) {
if (list_is_empty(unused_pages)) {
return ZX_ERR_OUT_OF_RANGE;
}
vm_page_t* page = list_remove_head_type(unused_pages, vm_page, queue_node);
DEBUG_ASSERT(page != nullptr);
DEBUG_ASSERT(page->state() == vm_page_state::ALLOC);
page->set_state(vm_page_state::IPC);
list_add_tail(used_pages, &page->queue_node);
void* va = paddr_to_physmap(page->paddr());
if (buffers->is_empty()) {
buffers->push_front(new (va) BufferChain::Buffer);
} else {
buffers->insert_after(buffer_tail, new (va) BufferChain::Buffer);
}
return ZX_OK;
}
// |PTR_IN| is a user_in_ptr-like type.
template <typename PTR_IN>
zx_status_t AppendCommon(PTR_IN src, size_t size) {
if (size == 0) {
return ZX_OK;
}
if (buffer_tail_ == buffers_.end()) {
return ZX_ERR_OUT_OF_RANGE;
}
size_t rem = size;
while (true) {
Buffer& buf = *buffer_tail_;
const size_t copy_len = ktl::min(rem, buf.size() - buffer_offset_);
char* dst = buf.data() + buffer_offset_;
zx_status_t status = src.copy_array_from_user(dst, copy_len);
if (unlikely(status != ZX_OK)) {
buffer_tail_ = buffers_.end();
return status;
}
buffer_offset_ += copy_len;
src = src.byte_offset(copy_len);
rem -= copy_len;
if (rem == 0) {
break;
}
status = AddNextBuffer(&unused_pages_, &used_pages_, &buffers_, buffer_tail_);
if (status != ZX_OK) {
buffer_tail_ = buffers_.end();
return status;
}
buffer_offset_ = 0;
buffer_tail_++;
}
return ZX_OK;
}
// Take care when adding fields as BufferChain lives inside the first buffer of buffers_.
BufferList buffers_;
// Iterator pointing to the last valid element in the buffer.
BufferList::iterator buffer_tail_;
// Position of the next byte to write in the Buffer pointed to by |buffer_tail_|.
size_t buffer_offset_;
// used_pages_ is a list of vm_page_t descriptors for the pages that back BufferList.
list_node used_pages_ = LIST_INITIAL_VALUE(used_pages_);
// unused_pages is a list of vm_page_t descriptors for pages that have been allocated but have
// not yet been used. These pages will be migrated to used_pages_ once they are needed for
// the BufferList.
list_node unused_pages_ = LIST_INITIAL_VALUE(unused_pages_);
inline static page_cache::PageCache page_cache_;
DISALLOW_COPY_ASSIGN_AND_MOVE(BufferChain);
};
static_assert(sizeof(BufferChain) == BufferChain::kSizeOfBufferChain, "");
#endif // ZIRCON_KERNEL_OBJECT_INCLUDE_OBJECT_BUFFER_CHAIN_H_