| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/devices/block/drivers/core/block-device.h" |
| |
| #include <fuchsia/hardware/block/partition/cpp/banjo.h> |
| #include <lib/operation/block.h> |
| #include <lib/zbi-format/partition.h> |
| #include <lib/zx/profile.h> |
| #include <lib/zx/thread.h> |
| #include <threads.h> |
| #include <zircon/threads.h> |
| |
| #include "src/devices/block/drivers/core/server.h" |
| #include "src/storage/lib/storage-metrics/block-metrics.h" |
| |
| zx_status_t BlockDevice::DdkGetProtocol(uint32_t proto_id, void* out_protocol) { |
| switch (proto_id) { |
| case ZX_PROTOCOL_BLOCK: { |
| self_protocol_.GetProto(static_cast<block_protocol_t*>(out_protocol)); |
| return ZX_OK; |
| } |
| case ZX_PROTOCOL_BLOCK_PARTITION: { |
| if (!parent_partition_protocol_.is_valid()) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| parent_partition_protocol_.GetProto(static_cast<block_partition_protocol_t*>(out_protocol)); |
| return ZX_OK; |
| } |
| case ZX_PROTOCOL_BLOCK_VOLUME: { |
| if (!parent_volume_protocol_.is_valid()) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| parent_volume_protocol_.GetProto(static_cast<block_volume_protocol_t*>(out_protocol)); |
| return ZX_OK; |
| } |
| default: |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| } |
| |
| void BlockDevice::UpdateStats(bool success, zx::ticks start_tick, block_op_t* op) { |
| uint64_t bytes_transfered = op->rw.length * info_.block_size; |
| std::lock_guard<std::mutex> lock(stat_lock_); |
| stats_.UpdateStats(success, start_tick, op->command.opcode, bytes_transfered); |
| } |
| |
| // Define the maximum I/O possible for the midlayer; this is arbitrarily |
| // set to the size of RIO's max payload. |
| // |
| // If a smaller value of "max_transfer_size" is defined, that will |
| // be used instead. |
| constexpr uint32_t kMaxMidlayerIO = 8192; |
| |
| zx_status_t BlockDevice::DoIo(zx::vmo& vmo, size_t buf_len, zx_off_t off, zx_off_t vmo_off, |
| bool write) { |
| std::lock_guard<std::mutex> lock(io_lock_); |
| const size_t block_size = info_.block_size; |
| const size_t max_xfer = std::min(info_.max_transfer_size, kMaxMidlayerIO); |
| |
| if (buf_len == 0) { |
| return ZX_OK; |
| } |
| if ((buf_len % block_size) || (off % block_size)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| uint64_t sub_txn_offset = 0; |
| while (sub_txn_offset < buf_len) { |
| size_t sub_txn_length = std::min(buf_len - sub_txn_offset, max_xfer); |
| |
| block_op_t* op = reinterpret_cast<block_op_t*>(io_op_.get()); |
| const uint8_t opcode = write ? BLOCK_OPCODE_WRITE : BLOCK_OPCODE_READ; |
| op->command = {.opcode = opcode, .flags = 0}; |
| ZX_DEBUG_ASSERT(sub_txn_length / block_size < std::numeric_limits<uint32_t>::max()); |
| op->rw.length = static_cast<uint32_t>(sub_txn_length / block_size); |
| op->rw.vmo = vmo.get(); |
| op->rw.offset_dev = (off + sub_txn_offset) / block_size; |
| op->rw.offset_vmo = (vmo_off + sub_txn_offset) / block_size; |
| |
| sync_completion_reset(&io_signal_); |
| auto completion_cb = [](void* cookie, zx_status_t status, block_op_t* op) { |
| BlockDevice* bdev = reinterpret_cast<BlockDevice*>(cookie); |
| bdev->io_status_ = status; |
| sync_completion_signal(&bdev->io_signal_); |
| }; |
| |
| BlockQueue(op, completion_cb, this); |
| sync_completion_wait(&io_signal_, ZX_TIME_INFINITE); |
| |
| if (io_status_ != ZX_OK) { |
| return io_status_; |
| } |
| |
| sub_txn_offset += sub_txn_length; |
| } |
| |
| return io_status_; |
| } |
| |
| void BlockDevice::DdkRelease() { delete this; } |
| |
| void BlockDevice::BlockQuery(block_info_t* block_info, size_t* op_size) { |
| // It is important that all devices sitting on top of the volume protocol avoid |
| // caching a copy of block info for query. The "block_count" field is dynamic, |
| // and may change during the lifetime of the volume. |
| size_t parent_op_size; |
| parent_protocol_.Query(block_info, &parent_op_size); |
| |
| // Safety check that parent op size doesn't change dynamically. |
| ZX_DEBUG_ASSERT(parent_op_size == parent_op_size_); |
| |
| *op_size = OpSize(); |
| } |
| |
| void BlockDevice::UpdateStatsAndCallCompletion(void* cookie, zx_status_t status, block_op_t* op) { |
| BlockDevice* block_device = static_cast<BlockDevice*>(cookie); |
| block::BorrowedOperation<StatsCookie> txn(op, block_device->parent_op_size_); |
| StatsCookie* stats_cookie = txn.private_storage(); |
| |
| block_device->UpdateStats(status == ZX_OK, stats_cookie->start_tick, op); |
| txn.Complete(status); |
| } |
| |
| void BlockDevice::BlockQueue(block_op_t* op, block_impl_queue_callback completion_cb, |
| void* cookie) { |
| zx::ticks start_tick = zx::ticks::now(); |
| |
| if (completion_status_stats_) { |
| block::BorrowedOperation<StatsCookie> txn(op, completion_cb, cookie, parent_op_size_); |
| StatsCookie* stats_cookie = txn.private_storage(); |
| stats_cookie->start_tick = start_tick; |
| parent_protocol_.Queue(txn.take(), UpdateStatsAndCallCompletion, this); |
| } else { |
| // Since we don't know the return status, we assume all commands succeeded. |
| UpdateStats(true, start_tick, op); |
| parent_protocol_.Queue(op, completion_cb, cookie); |
| } |
| } |
| |
| void BlockDevice::GetInfo(GetInfoCompleter::Sync& completer) { |
| fuchsia_hardware_block::wire::BlockInfo info; |
| static_assert(sizeof(info) == sizeof(block_info_t)); |
| size_t block_op_size; |
| parent_protocol_.Query(reinterpret_cast<block_info_t*>(&info), &block_op_size); |
| // Set or clear fuchsia.hardware_block/Flag.BOOTPART appropriately. |
| if (has_bootpart_) { |
| info.flags |= fuchsia_hardware_block::wire::Flag::kBootpart; |
| } else { |
| info.flags -= fuchsia_hardware_block::wire::Flag::kBootpart; |
| } |
| completer.ReplySuccess(info); |
| } |
| |
| void BlockDevice::GetStats(GetStatsRequestView request, GetStatsCompleter::Sync& completer) { |
| std::lock_guard<std::mutex> lock(stat_lock_); |
| if (!enable_stats_) { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| |
| fuchsia_hardware_block::wire::BlockStats stats; |
| stats_.CopyToFidl(&stats); |
| if (request->clear) { |
| stats_.Reset(); |
| } |
| completer.ReplySuccess(stats); |
| } |
| |
| void BlockDevice::OpenSession(OpenSessionRequestView request, |
| OpenSessionCompleter::Sync& completer) { |
| zx::result server = Server::Create(&self_protocol_); |
| if (server.is_error()) { |
| request->session.Close(server.error_value()); |
| return; |
| } |
| |
| // TODO(https://fxbug.dev/42067206): Avoid running a thread per session; make `Server` async |
| // instead. |
| thrd_t thread; |
| if (thrd_create_with_name( |
| &thread, |
| +[](void* arg) { |
| [[maybe_unused]] zx_status_t status = reinterpret_cast<Server*>(arg)->Serve(); |
| return 0; |
| }, |
| server.value().get(), "block_server") != thrd_success) { |
| request->session.Close(ZX_ERR_NO_MEMORY); |
| return; |
| } |
| |
| // Set a scheduling role for the block_server thread. |
| // This is required in order to service the blobfs-pager-thread, which is on a deadline profile. |
| // This will no longer be needed once we have the ability to propagate deadlines. Until then, we |
| // need to set deadline profiles for all threads that the blobfs-pager-thread interacts with in |
| // order to service page requests. |
| // |
| // Also note that this will apply to block_server threads spawned to service each block client |
| // (in the typical case, we have two - blobfs and minfs). The capacity of 1ms is chosen so as to |
| // accommodate most cases without throttling the thread. The desired capacity was 50us, but some |
| // tests that use a large ramdisk require a larger capacity. In the average case though on a real |
| // device, the block_server thread runs for less than 50us. 1ms provides us with a generous |
| // leeway, without hurting performance in the typical case - a thread is not penalized for not |
| // using its full capacity. |
| { |
| const char* role_name = "fuchsia.devices.block.drivers.core.block-server"; |
| if (zx_status_t status = device_set_profile_by_role(zxdev(), thrd_get_zx_handle(thread), |
| role_name, strlen(role_name)); |
| status != ZX_OK) { |
| zxlogf(WARNING, "block: Failed to apply role to block server: %s", |
| zx_status_get_string(status)); |
| } |
| } |
| |
| fidl::BindServer( |
| fdf::Dispatcher::GetCurrent()->async_dispatcher(), std::move(request->session), |
| std::move(server.value()), |
| [thread](Server* server, fidl::UnbindInfo, fidl::ServerEnd<fuchsia_hardware_block::Session>) { |
| server->Close(); |
| thrd_join(thread, nullptr); |
| }); |
| } |
| |
| void BlockDevice::ReadBlocks(ReadBlocksRequestView request, ReadBlocksCompleter::Sync& completer) { |
| if (zx_status_t status = |
| DoIo(request->vmo, request->length, request->dev_offset, request->vmo_offset, false); |
| status != ZX_OK) { |
| completer.ReplyError(status); |
| return; |
| } |
| completer.ReplySuccess(); |
| } |
| |
| void BlockDevice::WriteBlocks(WriteBlocksRequestView request, |
| WriteBlocksCompleter::Sync& completer) { |
| if (zx_status_t status = |
| DoIo(request->vmo, request->length, request->dev_offset, request->vmo_offset, true); |
| status != ZX_OK) { |
| completer.ReplyError(status); |
| return; |
| } |
| completer.ReplySuccess(); |
| } |
| |
| void BlockDevice::GetTypeGuid(GetTypeGuidCompleter::Sync& completer) { |
| if (!parent_partition_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, {}); |
| return; |
| } |
| fuchsia_hardware_block_partition::wire::Guid guid; |
| static_assert(sizeof(guid.value) == sizeof(guid_t)); |
| guid_t* guid_ptr = reinterpret_cast<guid_t*>(guid.value.data()); |
| zx_status_t status = parent_partition_protocol_.GetGuid(GUIDTYPE_TYPE, guid_ptr); |
| completer.Reply(status, fidl::ObjectView<decltype(guid)>::FromExternal(&guid)); |
| } |
| |
| void BlockDevice::GetInstanceGuid(GetInstanceGuidCompleter::Sync& completer) { |
| if (!parent_partition_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, {}); |
| return; |
| } |
| fuchsia_hardware_block_partition::wire::Guid guid; |
| static_assert(sizeof(guid.value) == sizeof(guid_t)); |
| guid_t* guid_ptr = reinterpret_cast<guid_t*>(guid.value.data()); |
| zx_status_t status = parent_partition_protocol_.GetGuid(GUIDTYPE_INSTANCE, guid_ptr); |
| completer.Reply(status, fidl::ObjectView<decltype(guid)>::FromExternal(&guid)); |
| } |
| |
| void BlockDevice::GetName(GetNameCompleter::Sync& completer) { |
| if (!parent_partition_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, {}); |
| return; |
| } |
| char name[fuchsia_hardware_block_partition::wire::kNameLength]; |
| zx_status_t status = parent_partition_protocol_.GetName(name, sizeof(name)); |
| completer.Reply(status, |
| status == ZX_OK ? fidl::StringView::FromExternal(name) : fidl::StringView{}); |
| } |
| |
| void BlockDevice::QuerySlices(QuerySlicesRequestView request, |
| QuerySlicesCompleter::Sync& completer) { |
| if (!parent_volume_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, {}, {}); |
| return; |
| } |
| fidl::Array<fuchsia_hardware_block_volume::wire::VsliceRange, |
| fuchsia_hardware_block_volume::wire::kMaxSliceRequests> |
| ranges; |
| static_assert(sizeof(decltype(ranges)::value_type) == sizeof(slice_region_t)); |
| slice_region_t* ranges_ptr = reinterpret_cast<slice_region_t*>(ranges.data()); |
| size_t range_count; |
| zx_status_t status = parent_volume_protocol_.QuerySlices( |
| request->start_slices.data(), request->start_slices.count(), ranges_ptr, std::size(ranges), |
| &range_count); |
| completer.Reply(status, ranges, range_count); |
| } |
| |
| void BlockDevice::GetVolumeInfo(GetVolumeInfoCompleter::Sync& completer) { |
| if (!parent_volume_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED, {}, {}); |
| return; |
| } |
| fuchsia_hardware_block_volume::wire::VolumeManagerInfo manager_info; |
| static_assert(sizeof(manager_info) == sizeof(volume_manager_info_t)); |
| fuchsia_hardware_block_volume::wire::VolumeInfo volume_info; |
| static_assert(sizeof(volume_info) == sizeof(volume_info_t)); |
| zx_status_t status = |
| parent_volume_protocol_.GetInfo(reinterpret_cast<volume_manager_info_t*>(&manager_info), |
| reinterpret_cast<volume_info_t*>(&volume_info)); |
| fidl::ObjectView<decltype(manager_info)> manager_info_view; |
| fidl::ObjectView<decltype(volume_info)> volume_info_view; |
| if (status == ZX_OK) { |
| manager_info_view = decltype(manager_info_view)::FromExternal(&manager_info); |
| volume_info_view = decltype(volume_info_view)::FromExternal(&volume_info); |
| } |
| completer.Reply(status, manager_info_view, volume_info_view); |
| } |
| |
| void BlockDevice::Extend(ExtendRequestView request, ExtendCompleter::Sync& completer) { |
| if (!parent_volume_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| slice_extent_t extent = { |
| .offset = request->start_slice, |
| .length = request->slice_count, |
| }; |
| completer.Reply(parent_volume_protocol_.Extend(&extent)); |
| } |
| |
| void BlockDevice::Shrink(ShrinkRequestView request, ShrinkCompleter::Sync& completer) { |
| if (!parent_volume_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| slice_extent_t extent = { |
| .offset = request->start_slice, |
| .length = request->slice_count, |
| }; |
| completer.Reply(parent_volume_protocol_.Shrink(&extent)); |
| } |
| |
| void BlockDevice::Destroy(DestroyCompleter::Sync& completer) { |
| if (!parent_volume_protocol_.is_valid()) { |
| completer.Reply(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| completer.Reply(parent_volume_protocol_.Destroy()); |
| } |
| |
| zx_status_t BlockDevice::Bind(void* ctx, zx_device_t* dev) { |
| auto bdev = std::make_unique<BlockDevice>(dev); |
| |
| // The Block Implementation Protocol is required. |
| if (!bdev->parent_protocol_.is_valid()) { |
| zxlogf(ERROR, "block device: does not support block protocol"); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| bdev->parent_protocol_.Query(&bdev->info_, &bdev->parent_op_size_); |
| |
| if (bdev->info_.max_transfer_size < bdev->info_.block_size) { |
| zxlogf(ERROR, "block device: has smaller max xfer (0x%x) than block size (0x%x)", |
| bdev->info_.max_transfer_size, bdev->info_.block_size); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| bdev->io_op_ = std::make_unique<uint8_t[]>(bdev->OpSize()); |
| size_t block_size = bdev->info_.block_size; |
| if ((block_size < 512) || (block_size & (block_size - 1))) { |
| zxlogf(ERROR, "block device: invalid block size: %zu", block_size); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| // Check to see if we have a ZBI partition map. |
| uint8_t buffer[METADATA_PARTITION_MAP_MAX]; |
| size_t actual; |
| zx_status_t status = |
| device_get_metadata(dev, DEVICE_METADATA_PARTITION_MAP, buffer, sizeof(buffer), &actual); |
| if (status == ZX_OK && actual >= sizeof(zbi_partition_map_t)) { |
| bdev->has_bootpart_ = true; |
| } |
| |
| // We implement |ZX_PROTOCOL_BLOCK|, not |ZX_PROTOCOL_BLOCK_IMPL|. This is the |
| // "core driver" protocol for block device drivers. |
| status = |
| bdev->DdkAdd(ddk::DeviceAddArgs("block").forward_metadata(dev, DEVICE_METADATA_GPT_INFO)); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // The device has been added; we'll release it in blkdev_release. |
| [[maybe_unused]] auto r = bdev.release(); |
| return ZX_OK; |
| } |
| |
| static constexpr zx_driver_ops_t block_driver_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = &BlockDevice::Bind; |
| return ops; |
| }(); |
| |
| ZIRCON_DRIVER(block, block_driver_ops, "zircon", "0.1"); |