ramfs: Overhaul block allocation to use a VMCache and physical pages.
This is a massive efficiency improvement as well as a large address space usage savings. It also paves the way for file_map() support...
This commit is contained in:
@@ -1,420 +1,359 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2007, Ingo Weinhold, [email protected].
|
* Copyright 2007, Ingo Weinhold, [email protected].
|
||||||
|
* Copyright 2019, Haiku, Inc.
|
||||||
* All rights reserved. Distributed under the terms of the MIT license.
|
* All rights reserved. Distributed under the terms of the MIT license.
|
||||||
*/
|
*/
|
||||||
|
#include "DataContainer.h"
|
||||||
|
|
||||||
|
#include <AutoDeleter.h>
|
||||||
|
#include <util/AutoLock.h>
|
||||||
|
|
||||||
|
#include <vm/VMCache.h>
|
||||||
|
#include <vm/vm_page.h>
|
||||||
|
|
||||||
#include "AllocationInfo.h"
|
#include "AllocationInfo.h"
|
||||||
#include "Attribute.h" // for debugging only
|
|
||||||
#include "Block.h"
|
|
||||||
#include "DataContainer.h"
|
|
||||||
#include "DebugSupport.h"
|
#include "DebugSupport.h"
|
||||||
#include "Misc.h"
|
#include "Misc.h"
|
||||||
#include "Node.h" // for debugging only
|
|
||||||
#include "Volume.h"
|
#include "Volume.h"
|
||||||
|
|
||||||
// constructor
|
// constructor
|
||||||
DataContainer::DataContainer(Volume *volume)
|
DataContainer::DataContainer(Volume *volume)
|
||||||
: fVolume(volume),
|
: fVolume(volume),
|
||||||
fSize(0)
|
fSize(0),
|
||||||
|
fCache(NULL)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// destructor
|
// destructor
|
||||||
DataContainer::~DataContainer()
|
DataContainer::~DataContainer()
|
||||||
{
|
{
|
||||||
Resize(0);
|
if (fCache != NULL) {
|
||||||
|
fCache->Lock();
|
||||||
|
fCache->ReleaseRefAndUnlock();
|
||||||
|
fCache = NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitCheck
|
// InitCheck
|
||||||
status_t
|
status_t
|
||||||
DataContainer::InitCheck() const
|
DataContainer::InitCheck() const
|
||||||
{
|
{
|
||||||
return (fVolume ? B_OK : B_ERROR);
|
return (fVolume != NULL ? B_OK : B_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
status_t
|
status_t
|
||||||
DataContainer::Resize(off_t newSize)
|
DataContainer::Resize(off_t newSize)
|
||||||
{
|
{
|
||||||
|
// PRINT("DataContainer::Resize(%Ld), fSize: %Ld\n", newSize, fSize);
|
||||||
|
|
||||||
status_t error = B_OK;
|
status_t error = B_OK;
|
||||||
if (newSize < 0)
|
if (newSize < fSize) {
|
||||||
newSize = 0;
|
// shrink
|
||||||
if (newSize != fSize) {
|
if (_IsCacheMode()) {
|
||||||
// Shrinking should never fail. Growing can fail, if we run out of
|
// resize the VMCache, which will automatically free pages
|
||||||
// memory. Then we try to shrink back to the original size.
|
AutoLocker<VMCache> _(fCache);
|
||||||
off_t oldSize = fSize;
|
error = fCache->Resize(newSize, VM_PRIORITY_SYSTEM);
|
||||||
error = _Resize(newSize);
|
if (error != B_OK)
|
||||||
if (error == B_NO_MEMORY && newSize > fSize)
|
return error;
|
||||||
_Resize(oldSize);
|
} else {
|
||||||
|
// small buffer mode: just set the new size (done below)
|
||||||
|
}
|
||||||
|
} else if (newSize > fSize) {
|
||||||
|
// grow
|
||||||
|
if (_RequiresCacheMode(newSize)) {
|
||||||
|
if (!_IsCacheMode())
|
||||||
|
error = _SwitchToCacheMode(fSize);
|
||||||
|
if (error != B_OK)
|
||||||
|
return error;
|
||||||
|
|
||||||
|
AutoLocker<VMCache> _(fCache);
|
||||||
|
fCache->Resize(newSize, VM_PRIORITY_SYSTEM);
|
||||||
|
|
||||||
|
// pages will be added as they are written to; so nothing else
|
||||||
|
// needs to be done here.
|
||||||
|
} else {
|
||||||
|
// no need to switch to cache mode: just set the new size
|
||||||
|
// (done below)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fSize = newSize;
|
||||||
|
|
||||||
|
// PRINT("DataContainer::Resize() done: %lx, fSize: %Ld\n", error, fSize);
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadAt
|
// ReadAt
|
||||||
status_t
|
status_t
|
||||||
DataContainer::ReadAt(off_t offset, void *_buffer, size_t size,
|
DataContainer::ReadAt(off_t offset, void *_buffer, size_t size,
|
||||||
size_t *bytesRead)
|
size_t *bytesRead)
|
||||||
{
|
{
|
||||||
uint8 *buffer = (uint8*)_buffer;
|
uint8 *buffer = (uint8*)_buffer;
|
||||||
status_t error = (buffer && offset >= 0 && bytesRead ? B_OK : B_BAD_VALUE);
|
status_t error = (buffer && offset >= 0 &&
|
||||||
if (error == B_OK) {
|
bytesRead ? B_OK : B_BAD_VALUE);
|
||||||
// read not more than we have to offer
|
if (error != B_OK)
|
||||||
offset = min(offset, fSize);
|
return error;
|
||||||
size = min(size, size_t(fSize - offset));
|
|
||||||
// iterate through the blocks, reading as long as there's something
|
// read not more than we have to offer
|
||||||
// left to read
|
offset = min(offset, fSize);
|
||||||
size_t blockSize = fVolume->GetBlockSize();
|
size = min(size, size_t(fSize - offset));
|
||||||
*bytesRead = 0;
|
|
||||||
while (size > 0) {
|
if (!_IsCacheMode()) {
|
||||||
size_t inBlockOffset = offset % blockSize;
|
// in non-cache mode, we just copy the data directly
|
||||||
size_t toRead = min(size, size_t(blockSize - inBlockOffset));
|
memcpy(buffer, fSmallBuffer + offset, size);
|
||||||
void *blockData = _GetBlockDataAt(offset / blockSize,
|
if (bytesRead != NULL)
|
||||||
inBlockOffset, toRead);
|
*bytesRead = size;
|
||||||
D(
|
return B_OK;
|
||||||
if (!blockData) {
|
|
||||||
Node *node = NULL;
|
|
||||||
if (Attribute *attribute = dynamic_cast<Attribute*>(this)) {
|
|
||||||
FATAL("attribute `%s' of\n", attribute->GetName());
|
|
||||||
node = attribute->GetNode();
|
|
||||||
} else {
|
|
||||||
node = dynamic_cast<Node*>(this);
|
|
||||||
}
|
|
||||||
if (node)
|
|
||||||
// FATAL(("node `%s'\n", node->GetName()));
|
|
||||||
FATAL("container size: %Ld, offset: %Ld, buffer size: %lu\n",
|
|
||||||
fSize, offset, size);
|
|
||||||
return B_ERROR;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
memcpy(buffer, blockData, toRead);
|
|
||||||
buffer += toRead;
|
|
||||||
size -= toRead;
|
|
||||||
offset += toRead;
|
|
||||||
*bytesRead += toRead;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cache mode
|
||||||
|
error = _DoCacheIO(offset, buffer, size, bytesRead, false);
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteAt
|
// WriteAt
|
||||||
status_t
|
status_t
|
||||||
DataContainer::WriteAt(off_t offset, const void *_buffer, size_t size,
|
DataContainer::WriteAt(off_t offset, const void *_buffer, size_t size,
|
||||||
size_t *bytesWritten)
|
size_t *bytesWritten)
|
||||||
{
|
{
|
||||||
//PRINT(("DataContainer::WriteAt(%Ld, %p, %lu, %p), fSize: %Ld\n", offset, _buffer, size, bytesWritten, fSize));
|
PRINT("DataContainer::WriteAt(%Ld, %p, %lu, %p), fSize: %Ld\n", offset, _buffer, size, bytesWritten, fSize);
|
||||||
|
|
||||||
const uint8 *buffer = (const uint8*)_buffer;
|
const uint8 *buffer = (const uint8*)_buffer;
|
||||||
status_t error = (buffer && offset >= 0 && bytesWritten
|
status_t error = (buffer && offset >= 0 && bytesWritten
|
||||||
? B_OK : B_BAD_VALUE);
|
? B_OK : B_BAD_VALUE);
|
||||||
// resize the container, if necessary
|
if (error != B_OK)
|
||||||
if (error == B_OK) {
|
return error;
|
||||||
off_t newSize = offset + size;
|
|
||||||
off_t oldSize = fSize;
|
|
||||||
if (newSize > fSize) {
|
|
||||||
error = Resize(newSize);
|
|
||||||
// pad with zero, if necessary
|
|
||||||
if (error == B_OK && offset > oldSize)
|
|
||||||
_ClearArea(offset, oldSize - offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (error == B_OK) {
|
|
||||||
// iterate through the blocks, writing as long as there's something
|
|
||||||
// left to write
|
|
||||||
size_t blockSize = fVolume->GetBlockSize();
|
|
||||||
*bytesWritten = 0;
|
|
||||||
while (size > 0) {
|
|
||||||
size_t inBlockOffset = offset % blockSize;
|
|
||||||
size_t toWrite = min(size, size_t(blockSize - inBlockOffset));
|
|
||||||
void *blockData = _GetBlockDataAt(offset / blockSize,
|
|
||||||
inBlockOffset, toWrite);
|
|
||||||
D(if (!blockData) return B_ERROR;);
|
|
||||||
memcpy(blockData, buffer, toWrite);
|
|
||||||
buffer += toWrite;
|
|
||||||
size -= toWrite;
|
|
||||||
offset += toWrite;
|
|
||||||
*bytesWritten += toWrite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//PRINT(("DataContainer::WriteAt() done: %lx, fSize: %Ld\n", error, fSize));
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFirstDataBlock
|
// resize the container, if necessary
|
||||||
void
|
if ((offset + size) > fSize)
|
||||||
DataContainer::GetFirstDataBlock(const uint8 **data, size_t *length)
|
error = Resize(offset + size);
|
||||||
{
|
if (error != B_OK)
|
||||||
if (data && length) {
|
return error;
|
||||||
if (_IsBlockMode()) {
|
|
||||||
BlockReference *block = _GetBlockList()->ItemAt(0);
|
if (!_IsCacheMode()) {
|
||||||
*data = (const uint8*)block->GetData();
|
// in non-cache mode, we just copy the data directly
|
||||||
*length = min(fSize, fVolume->GetBlockSize());
|
memcpy(fSmallBuffer + offset, buffer, size);
|
||||||
} else {
|
if (bytesWritten != NULL)
|
||||||
*data = fSmallBuffer;
|
*bytesWritten = size;
|
||||||
*length = fSize;
|
return B_OK;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cache mode
|
||||||
|
error = _DoCacheIO(offset, (uint8*)buffer, size, bytesWritten, true);
|
||||||
|
|
||||||
|
PRINT("DataContainer::WriteAt() done: %lx, fSize: %Ld\n", error, fSize);
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllocationInfo
|
// GetAllocationInfo
|
||||||
void
|
void
|
||||||
DataContainer::GetAllocationInfo(AllocationInfo &info)
|
DataContainer::GetAllocationInfo(AllocationInfo &info)
|
||||||
{
|
{
|
||||||
if (_IsBlockMode()) {
|
if (_IsCacheMode()) {
|
||||||
BlockList *blocks = _GetBlockList();
|
info.AddAreaAllocation(fCache->committed_size);
|
||||||
info.AddListAllocation(blocks->GetCapacity(), sizeof(BlockReference*));
|
|
||||||
int32 blockCount = blocks->CountItems();
|
|
||||||
for (int32 i = 0; i < blockCount; i++)
|
|
||||||
info.AddBlockAllocation(blocks->ItemAt(i)->GetBlock()->GetSize());
|
|
||||||
} else {
|
} else {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// _RequiresBlockMode
|
// _RequiresCacheMode
|
||||||
inline
|
inline bool
|
||||||
bool
|
DataContainer::_RequiresCacheMode(size_t size)
|
||||||
DataContainer::_RequiresBlockMode(size_t size)
|
|
||||||
{
|
{
|
||||||
return (size > kSmallDataContainerSize);
|
// we cannot back out of cache mode after entering it,
|
||||||
|
// as there may be other consumers of our VMCache
|
||||||
|
return _IsCacheMode() || (size > kSmallDataContainerSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// _IsBlockMode
|
// _IsCacheMode
|
||||||
inline
|
inline bool
|
||||||
bool
|
DataContainer::_IsCacheMode() const
|
||||||
DataContainer::_IsBlockMode() const
|
|
||||||
{
|
{
|
||||||
return (fSize > kSmallDataContainerSize);
|
return fCache != NULL;
|
||||||
}
|
|
||||||
|
|
||||||
// _Resize
|
|
||||||
status_t
|
|
||||||
DataContainer::_Resize(off_t newSize)
|
|
||||||
{
|
|
||||||
//PRINT(("DataContainer::_Resize(%Ld), fSize: %Ld\n", newSize, fSize));
|
|
||||||
status_t error = B_OK;
|
|
||||||
if (newSize != fSize) {
|
|
||||||
size_t blockSize = fVolume->GetBlockSize();
|
|
||||||
int32 blockCount = _CountBlocks();
|
|
||||||
int32 newBlockCount = (newSize + blockSize - 1) / blockSize;
|
|
||||||
if (newBlockCount == blockCount) {
|
|
||||||
// only the last block needs to be resized
|
|
||||||
if (_IsBlockMode() && _RequiresBlockMode(newSize)) {
|
|
||||||
// keep block mode
|
|
||||||
error = _ResizeLastBlock((newSize - 1) % blockSize + 1);
|
|
||||||
} else if (!_IsBlockMode() && !_RequiresBlockMode(newSize)) {
|
|
||||||
// keep small buffer mode
|
|
||||||
fSize = newSize;
|
|
||||||
} else if (fSize < newSize) {
|
|
||||||
// switch to block mode
|
|
||||||
_SwitchToBlockMode(newSize);
|
|
||||||
} else {
|
|
||||||
// switch to small buffer mode
|
|
||||||
_SwitchToSmallBufferMode(newSize);
|
|
||||||
}
|
|
||||||
} else if (newBlockCount < blockCount) {
|
|
||||||
// shrink
|
|
||||||
if (_IsBlockMode()) {
|
|
||||||
// remove the last blocks
|
|
||||||
BlockList *blocks = _GetBlockList();
|
|
||||||
for (int32 i = blockCount - 1; i >= newBlockCount; i--) {
|
|
||||||
BlockReference *block = blocks->ItemAt(i);
|
|
||||||
blocks->RemoveItem(i);
|
|
||||||
fVolume->FreeBlock(block);
|
|
||||||
fSize = (fSize - 1) / blockSize * blockSize;
|
|
||||||
}
|
|
||||||
// resize the last block to the correct size, respectively
|
|
||||||
// switch to small buffer mode
|
|
||||||
if (_RequiresBlockMode(newSize))
|
|
||||||
error = _ResizeLastBlock((newSize - 1) % blockSize + 1);
|
|
||||||
else
|
|
||||||
_SwitchToSmallBufferMode(newSize);
|
|
||||||
} else {
|
|
||||||
// small buffer mode: just set the new size
|
|
||||||
fSize = newSize;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// grow
|
|
||||||
if (_RequiresBlockMode(newSize)) {
|
|
||||||
// resize the first block to the correct size, respectively
|
|
||||||
// switch to block mode
|
|
||||||
if (_IsBlockMode())
|
|
||||||
error = _ResizeLastBlock(blockSize);
|
|
||||||
else {
|
|
||||||
error = _SwitchToBlockMode(min((size_t)newSize,
|
|
||||||
blockSize));
|
|
||||||
}
|
|
||||||
// add new blocks
|
|
||||||
BlockList *blocks = _GetBlockList();
|
|
||||||
while (error == B_OK && fSize < newSize) {
|
|
||||||
size_t newBlockSize = min(size_t(newSize - fSize),
|
|
||||||
blockSize);
|
|
||||||
BlockReference *block = NULL;
|
|
||||||
error = fVolume->AllocateBlock(newBlockSize, &block);
|
|
||||||
if (error == B_OK) {
|
|
||||||
if (blocks->AddItem(block))
|
|
||||||
fSize += newBlockSize;
|
|
||||||
else {
|
|
||||||
SET_ERROR(error, B_NO_MEMORY);
|
|
||||||
fVolume->FreeBlock(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no need to switch to block mode: just set the new size
|
|
||||||
fSize = newSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//PRINT(("DataContainer::_Resize() done: %lx, fSize: %Ld\n", error, fSize));
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _GetBlockList
|
|
||||||
inline
|
|
||||||
DataContainer::BlockList *
|
|
||||||
DataContainer::_GetBlockList()
|
|
||||||
{
|
|
||||||
return (BlockList*)fBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _GetBlockList
|
|
||||||
inline
|
|
||||||
const DataContainer::BlockList *
|
|
||||||
DataContainer::_GetBlockList() const
|
|
||||||
{
|
|
||||||
return (BlockList*)fBlocks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// _CountBlocks
|
// _CountBlocks
|
||||||
inline
|
inline int32
|
||||||
int32
|
|
||||||
DataContainer::_CountBlocks() const
|
DataContainer::_CountBlocks() const
|
||||||
{
|
{
|
||||||
if (_IsBlockMode())
|
if (_IsCacheMode())
|
||||||
return _GetBlockList()->CountItems();
|
return fCache->page_count;
|
||||||
else if (fSize == 0) // small buffer mode, empty buffer
|
else if (fSize == 0) // small buffer mode, empty buffer
|
||||||
return 0;
|
return 0;
|
||||||
return 1; // small buffer mode, non-empty buffer
|
return 1; // small buffer mode, non-empty buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// _GetBlockDataAt
|
// _SwitchToCacheMode
|
||||||
inline
|
|
||||||
void *
|
|
||||||
DataContainer::_GetBlockDataAt(int32 index, size_t offset, size_t DARG(size))
|
|
||||||
{
|
|
||||||
if (_IsBlockMode()) {
|
|
||||||
BlockReference *block = _GetBlockList()->ItemAt(index);
|
|
||||||
D(if (!fVolume->CheckBlock(block, offset + size)) return NULL;);
|
|
||||||
return block->GetDataAt(offset);
|
|
||||||
} else {
|
|
||||||
D(
|
|
||||||
if (offset + size > kSmallDataContainerSize) {
|
|
||||||
FATAL("DataContainer: Data access exceeds small buffer.\n");
|
|
||||||
PANIC("DataContainer: Data access exceeds small buffer.");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return fSmallBuffer + offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// _ClearArea
|
|
||||||
void
|
|
||||||
DataContainer::_ClearArea(off_t offset, off_t size)
|
|
||||||
{
|
|
||||||
// constrain the area to the data area
|
|
||||||
offset = min(offset, fSize);
|
|
||||||
size = min(size, fSize - offset);
|
|
||||||
// iterate through the blocks, clearing as long as there's something
|
|
||||||
// left to clear
|
|
||||||
size_t blockSize = fVolume->GetBlockSize();
|
|
||||||
while (size > 0) {
|
|
||||||
size_t inBlockOffset = offset % blockSize;
|
|
||||||
size_t toClear = min(size_t(size), blockSize - inBlockOffset);
|
|
||||||
void *blockData = _GetBlockDataAt(offset / blockSize, inBlockOffset,
|
|
||||||
toClear);
|
|
||||||
D(if (!blockData) return;);
|
|
||||||
memset(blockData, 0, toClear);
|
|
||||||
size -= toClear;
|
|
||||||
offset += toClear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// _ResizeLastBlock
|
|
||||||
status_t
|
status_t
|
||||||
DataContainer::_ResizeLastBlock(size_t newSize)
|
DataContainer::_SwitchToCacheMode(size_t newBlockSize)
|
||||||
{
|
{
|
||||||
//PRINT(("DataContainer::_ResizeLastBlock(%lu), fSize: %Ld\n", newSize, fSize));
|
status_t error = VMCacheFactory::CreateAnonymousCache(fCache, false, 0,
|
||||||
int32 blockCount = _CountBlocks();
|
0, false, VM_PRIORITY_SYSTEM);
|
||||||
status_t error = (fSize > 0 && blockCount > 0 && newSize > 0
|
if (error != B_OK)
|
||||||
? B_OK : B_BAD_VALUE);
|
return error;
|
||||||
D(
|
|
||||||
if (!_IsBlockMode()) {
|
fCache->temporary = 1;
|
||||||
FATAL("Call of _ResizeLastBlock() in small buffer mode.\n");
|
fCache->virtual_end = newBlockSize;
|
||||||
PANIC("Call of _ResizeLastBlock() in small buffer mode.");
|
|
||||||
return B_ERROR;
|
error = fCache->Commit(newBlockSize, VM_PRIORITY_SYSTEM);
|
||||||
}
|
if (error != B_OK)
|
||||||
);
|
return error;
|
||||||
if (error == B_OK) {
|
|
||||||
size_t blockSize = fVolume->GetBlockSize();
|
if (fSize != 0)
|
||||||
size_t oldSize = (fSize - 1) % blockSize + 1;
|
error = _DoCacheIO(0, fSmallBuffer, fSize, NULL, true);
|
||||||
if (newSize != oldSize) {
|
|
||||||
BlockList *blocks = _GetBlockList();
|
|
||||||
BlockReference *block = blocks->ItemAt(blockCount - 1);
|
|
||||||
BlockReference *newBlock = fVolume->ResizeBlock(block, newSize);
|
|
||||||
if (newBlock) {
|
|
||||||
if (newBlock != block)
|
|
||||||
blocks->ReplaceItem(blockCount - 1, newBlock);
|
|
||||||
fSize += off_t(newSize) - oldSize;
|
|
||||||
} else
|
|
||||||
SET_ERROR(error, B_NO_MEMORY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//PRINT(("DataContainer::_ResizeLastBlock() done: %lx, fSize: %Ld\n", error, fSize));
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// _SwitchToBlockMode
|
// _DoCacheIO
|
||||||
status_t
|
status_t
|
||||||
DataContainer::_SwitchToBlockMode(size_t newBlockSize)
|
DataContainer::_DoCacheIO(const off_t offset, uint8* buffer, ssize_t length,
|
||||||
|
size_t* bytesProcessed, bool isWrite)
|
||||||
{
|
{
|
||||||
// allocate a new block
|
const size_t originalLength = length;
|
||||||
BlockReference *block = NULL;
|
const bool user = IS_USER_ADDRESS(buffer);
|
||||||
status_t error = fVolume->AllocateBlock(newBlockSize, &block);
|
|
||||||
if (error == B_OK) {
|
const off_t rounded_offset = ROUNDDOWN(offset, B_PAGE_SIZE);
|
||||||
// copy the data from the small buffer into the block
|
const size_t rounded_len = ROUNDUP((length) + (offset - rounded_offset),
|
||||||
if (fSize > 0)
|
B_PAGE_SIZE);
|
||||||
memcpy(block->GetData(), fSmallBuffer, fSize);
|
vm_page** pages = new(std::nothrow) vm_page*[rounded_len / B_PAGE_SIZE];
|
||||||
// construct the block list and add the block
|
if (pages == NULL)
|
||||||
new (fBlocks) BlockList(10);
|
return B_NO_MEMORY;
|
||||||
BlockList *blocks = _GetBlockList();
|
ArrayDeleter<vm_page*> pagesDeleter(pages);
|
||||||
if (blocks->AddItem(block)) {
|
|
||||||
fSize = newBlockSize;
|
_GetPages(rounded_offset, rounded_len, isWrite, pages);
|
||||||
|
|
||||||
|
status_t error = B_OK;
|
||||||
|
size_t index = 0;
|
||||||
|
|
||||||
|
while (length > 0) {
|
||||||
|
vm_page* page = pages[index];
|
||||||
|
phys_addr_t at = (page->physical_page_number * B_PAGE_SIZE);
|
||||||
|
ssize_t bytes = B_PAGE_SIZE;
|
||||||
|
if (index == 0) {
|
||||||
|
const uint32 pageoffset = (offset % B_PAGE_SIZE);
|
||||||
|
at += pageoffset;
|
||||||
|
bytes -= pageoffset;
|
||||||
|
}
|
||||||
|
bytes = min(length, bytes);
|
||||||
|
|
||||||
|
if (isWrite) {
|
||||||
|
page->modified = true;
|
||||||
|
error = vm_memcpy_to_physical(at, buffer, bytes, user);
|
||||||
} else {
|
} else {
|
||||||
// error: destroy the block list and free the block
|
if (page != NULL) {
|
||||||
SET_ERROR(error, B_NO_MEMORY);
|
error = vm_memcpy_from_physical(buffer, at, bytes, user);
|
||||||
blocks->~BlockList();
|
} else {
|
||||||
if (fSize > 0)
|
if (user) {
|
||||||
memcpy(fSmallBuffer, block->GetData(), fSize);
|
error = user_memset(buffer, 0, bytes);
|
||||||
fVolume->FreeBlock(block);
|
} else {
|
||||||
|
memset(buffer, 0, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (error != B_OK)
|
||||||
|
break;
|
||||||
|
|
||||||
|
buffer += bytes;
|
||||||
|
length -= bytes;
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_PutPages(rounded_offset, rounded_len, pages, error == B_OK);
|
||||||
|
|
||||||
|
if (bytesProcessed != NULL)
|
||||||
|
*bytesProcessed = length > 0 ? originalLength - length : originalLength;
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// _SwitchToSmallBufferMode
|
// _GetPages
|
||||||
void
|
void
|
||||||
DataContainer::_SwitchToSmallBufferMode(size_t newSize)
|
DataContainer::_GetPages(off_t offset, off_t length, bool isWrite,
|
||||||
|
vm_page** pages)
|
||||||
{
|
{
|
||||||
// remove the first (and only) block
|
// TODO: This method is duplicated in the ram_disk. Perhaps it
|
||||||
BlockList *blocks = _GetBlockList();
|
// should be put into a common location?
|
||||||
BlockReference *block = blocks->ItemAt(0);
|
|
||||||
blocks->RemoveItem((int32)0L);
|
// get the pages, we already have
|
||||||
// destroy the block list and copy the data into the small buffer
|
AutoLocker<VMCache> locker(fCache);
|
||||||
blocks->~BlockList();
|
|
||||||
if (newSize > 0)
|
size_t pageCount = length / B_PAGE_SIZE;
|
||||||
memcpy(fSmallBuffer, block->GetData(), newSize);
|
size_t index = 0;
|
||||||
// free the block and set the new size
|
size_t missingPages = 0;
|
||||||
fVolume->FreeBlock(block);
|
|
||||||
fSize = newSize;
|
while (length > 0) {
|
||||||
|
vm_page* page = fCache->LookupPage(offset);
|
||||||
|
if (page != NULL) {
|
||||||
|
if (page->busy) {
|
||||||
|
fCache->WaitForPageEvents(page, PAGE_EVENT_NOT_BUSY, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PAGE_ACCESS_START(page);
|
||||||
|
page->busy = true;
|
||||||
|
} else
|
||||||
|
missingPages++;
|
||||||
|
|
||||||
|
pages[index++] = page;
|
||||||
|
offset += B_PAGE_SIZE;
|
||||||
|
length -= B_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
locker.Unlock();
|
||||||
|
|
||||||
|
// For a write we need to reserve the missing pages.
|
||||||
|
if (isWrite && missingPages > 0) {
|
||||||
|
vm_page_reservation reservation;
|
||||||
|
vm_page_reserve_pages(&reservation, missingPages,
|
||||||
|
VM_PRIORITY_SYSTEM);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < pageCount; i++) {
|
||||||
|
if (pages[i] != NULL)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
pages[i] = vm_page_allocate_page(&reservation,
|
||||||
|
PAGE_STATE_WIRED | VM_PAGE_ALLOC_BUSY);
|
||||||
|
|
||||||
|
if (--missingPages == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm_page_unreserve_pages(&reservation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DataContainer::_PutPages(off_t offset, off_t length, vm_page** pages,
|
||||||
|
bool success)
|
||||||
|
{
|
||||||
|
// TODO: This method is duplicated in the ram_disk. Perhaps it
|
||||||
|
// should be put into a common location?
|
||||||
|
|
||||||
|
AutoLocker<VMCache> locker(fCache);
|
||||||
|
|
||||||
|
// Mark all pages unbusy. On error free the newly allocated pages.
|
||||||
|
size_t index = 0;
|
||||||
|
|
||||||
|
while (length > 0) {
|
||||||
|
vm_page* page = pages[index++];
|
||||||
|
if (page != NULL) {
|
||||||
|
if (page->CacheRef() == NULL) {
|
||||||
|
if (success) {
|
||||||
|
fCache->InsertPage(page, offset);
|
||||||
|
fCache->MarkPageUnbusy(page);
|
||||||
|
DEBUG_PAGE_ACCESS_END(page);
|
||||||
|
} else
|
||||||
|
vm_page_free(NULL, page);
|
||||||
|
} else {
|
||||||
|
fCache->MarkPageUnbusy(page);
|
||||||
|
DEBUG_PAGE_ACCESS_END(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += B_PAGE_SIZE;
|
||||||
|
length -= B_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2007, Ingo Weinhold, [email protected].
|
* Copyright 2007, Ingo Weinhold, [email protected].
|
||||||
|
* Copyright 2019, Haiku, Inc.
|
||||||
* All rights reserved. Distributed under the terms of the MIT license.
|
* All rights reserved. Distributed under the terms of the MIT license.
|
||||||
*/
|
*/
|
||||||
#ifndef DATA_CONTAINER_H
|
#ifndef DATA_CONTAINER_H
|
||||||
#define DATA_CONTAINER_H
|
#define DATA_CONTAINER_H
|
||||||
|
|
||||||
#include "List.h"
|
#include <OS.h>
|
||||||
|
|
||||||
|
struct vm_page;
|
||||||
|
class VMCache;
|
||||||
class AllocationInfo;
|
class AllocationInfo;
|
||||||
class BlockReference;
|
|
||||||
class Volume;
|
class Volume;
|
||||||
|
|
||||||
// Size of the DataContainer's small buffer. If it contains data up to this
|
// Size of the DataContainer's small buffer. If it contains data up to this
|
||||||
@@ -49,38 +51,25 @@ public:
|
|||||||
virtual status_t WriteAt(off_t offset, const void *buffer, size_t size,
|
virtual status_t WriteAt(off_t offset, const void *buffer, size_t size,
|
||||||
size_t *bytesWritten);
|
size_t *bytesWritten);
|
||||||
|
|
||||||
void GetFirstDataBlock(const uint8 **data, size_t *length);
|
|
||||||
|
|
||||||
// debugging
|
// debugging
|
||||||
void GetAllocationInfo(AllocationInfo &info);
|
void GetAllocationInfo(AllocationInfo &info);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
typedef List<BlockReference*> BlockList;
|
inline bool _RequiresCacheMode(size_t size);
|
||||||
|
inline bool _IsCacheMode() const;
|
||||||
|
status_t _SwitchToCacheMode(size_t newBlockSize);
|
||||||
|
void _GetPages(off_t offset, off_t length, bool isWrite, vm_page** pages);
|
||||||
|
void _PutPages(off_t offset, off_t length, vm_page** pages, bool success);
|
||||||
|
status_t _DoCacheIO(const off_t offset, uint8* buffer, ssize_t length,
|
||||||
|
size_t* bytesProcessed, bool isWrite);
|
||||||
|
|
||||||
private:
|
|
||||||
static inline bool _RequiresBlockMode(size_t size);
|
|
||||||
inline bool _IsBlockMode() const;
|
|
||||||
|
|
||||||
inline BlockList *_GetBlockList();
|
|
||||||
inline const BlockList *_GetBlockList() const;
|
|
||||||
inline int32 _CountBlocks() const;
|
inline int32 _CountBlocks() const;
|
||||||
inline void *_GetBlockDataAt(int32 index, size_t offset, size_t size);
|
|
||||||
|
|
||||||
void _ClearArea(off_t offset, off_t size);
|
|
||||||
|
|
||||||
status_t _Resize(off_t newSize);
|
|
||||||
status_t _ResizeLastBlock(size_t newSize);
|
|
||||||
|
|
||||||
status_t _SwitchToBlockMode(size_t newBlockSize);
|
|
||||||
void _SwitchToSmallBufferMode(size_t newSize);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Volume *fVolume;
|
Volume *fVolume;
|
||||||
off_t fSize;
|
off_t fSize;
|
||||||
union {
|
VMCache* fCache;
|
||||||
uint8 fBlocks[sizeof(BlockList)];
|
uint8 fSmallBuffer[kSmallDataContainerSize];
|
||||||
uint8 fSmallBuffer[kSmallDataContainerSize];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DATA_CONTAINER_H
|
#endif // DATA_CONTAINER_H
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ KernelAddon ramfs
|
|||||||
AttributeIndex.cpp
|
AttributeIndex.cpp
|
||||||
AttributeIndexImpl.cpp
|
AttributeIndexImpl.cpp
|
||||||
AttributeIterator.cpp
|
AttributeIterator.cpp
|
||||||
BlockAllocator.cpp
|
|
||||||
BlockAllocatorArea.cpp
|
|
||||||
BlockAllocatorAreaBucket.cpp
|
|
||||||
BlockReferenceManager.cpp
|
|
||||||
DataContainer.cpp
|
DataContainer.cpp
|
||||||
DebugSupport.cpp
|
DebugSupport.cpp
|
||||||
Directory.cpp
|
Directory.cpp
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "Block.h"
|
#include <vm/vm_page.h>
|
||||||
#include "BlockAllocator.h"
|
|
||||||
#include "DebugSupport.h"
|
#include "DebugSupport.h"
|
||||||
#include "Directory.h"
|
#include "Directory.h"
|
||||||
#include "DirectoryEntryTable.h"
|
#include "DirectoryEntryTable.h"
|
||||||
@@ -43,11 +43,6 @@
|
|||||||
#include "TwoKeyAVLTree.h"
|
#include "TwoKeyAVLTree.h"
|
||||||
#include "Volume.h"
|
#include "Volume.h"
|
||||||
|
|
||||||
// default block size
|
|
||||||
static const off_t kDefaultBlockSize = 4096;
|
|
||||||
|
|
||||||
static const size_t kDefaultAreaSize = kDefaultBlockSize * 128;
|
|
||||||
|
|
||||||
// default volume name
|
// default volume name
|
||||||
static const char *kDefaultVolumeName = "RAM FS";
|
static const char *kDefaultVolumeName = "RAM FS";
|
||||||
|
|
||||||
@@ -143,9 +138,6 @@ Volume::Volume(fs_volume* volume)
|
|||||||
fAnyNodeListeners(),
|
fAnyNodeListeners(),
|
||||||
fEntryListeners(NULL),
|
fEntryListeners(NULL),
|
||||||
fAnyEntryListeners(),
|
fAnyEntryListeners(),
|
||||||
fBlockAllocator(NULL),
|
|
||||||
fBlockSize(kDefaultBlockSize),
|
|
||||||
fAllocatedBlocks(0),
|
|
||||||
fAccessTime(0),
|
fAccessTime(0),
|
||||||
fMounted(false)
|
fMounted(false)
|
||||||
{
|
{
|
||||||
@@ -173,14 +165,6 @@ Volume::Mount(uint32 flags)
|
|||||||
Unmount();
|
Unmount();
|
||||||
|
|
||||||
status_t error = B_OK;
|
status_t error = B_OK;
|
||||||
// create a block allocator
|
|
||||||
if (error == B_OK) {
|
|
||||||
fBlockAllocator = new(nothrow) BlockAllocator(kDefaultAreaSize);
|
|
||||||
if (fBlockAllocator)
|
|
||||||
error = fBlockAllocator->InitCheck();
|
|
||||||
else
|
|
||||||
SET_ERROR(error, B_NO_MEMORY);
|
|
||||||
}
|
|
||||||
// create the listener trees
|
// create the listener trees
|
||||||
if (error == B_OK) {
|
if (error == B_OK) {
|
||||||
fNodeListeners = new(nothrow) NodeListenerTree;
|
fNodeListeners = new(nothrow) NodeListenerTree;
|
||||||
@@ -267,41 +251,22 @@ Volume::Unmount()
|
|||||||
delete fNodeTable;
|
delete fNodeTable;
|
||||||
fNodeTable = NULL;
|
fNodeTable = NULL;
|
||||||
}
|
}
|
||||||
// delete the block allocator
|
|
||||||
if (fBlockAllocator) {
|
|
||||||
delete fBlockAllocator;
|
|
||||||
fBlockAllocator = NULL;
|
|
||||||
}
|
|
||||||
return B_OK;
|
return B_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockSize
|
|
||||||
off_t
|
|
||||||
Volume::GetBlockSize() const
|
|
||||||
{
|
|
||||||
return fBlockSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountBlocks
|
// CountBlocks
|
||||||
off_t
|
off_t
|
||||||
Volume::CountBlocks() const
|
Volume::CountBlocks() const
|
||||||
{
|
{
|
||||||
size_t bytes = 0;
|
// TODO: Compute how many pages we are using across all DataContainers.
|
||||||
system_info sysInfo;
|
return 0;
|
||||||
if (get_system_info(&sysInfo) == B_OK) {
|
|
||||||
int32 freePages = sysInfo.max_pages - sysInfo.used_pages;
|
|
||||||
bytes = (uint32)freePages * B_PAGE_SIZE
|
|
||||||
+ fBlockAllocator->GetAvailableBytes();
|
|
||||||
}
|
|
||||||
return bytes / kDefaultBlockSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountFreeBlocks
|
// CountFreeBlocks
|
||||||
off_t
|
off_t
|
||||||
Volume::CountFreeBlocks() const
|
Volume::CountFreeBlocks() const
|
||||||
{
|
{
|
||||||
// TODO:...
|
return vm_page_num_available_pages();
|
||||||
return CountBlocks() - fBlockAllocator->GetUsedBytes() / kDefaultBlockSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetName
|
// SetName
|
||||||
@@ -752,54 +717,6 @@ Volume::UpdateLiveQueries(Entry *entry, Node* node, const char *attribute,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllocateBlock
|
|
||||||
status_t
|
|
||||||
Volume::AllocateBlock(size_t size, BlockReference **block)
|
|
||||||
{
|
|
||||||
status_t error = (size > 0 && size <= fBlockSize && block
|
|
||||||
? B_OK : B_BAD_VALUE);
|
|
||||||
if (error == B_OK) {
|
|
||||||
*block = fBlockAllocator->AllocateBlock(size);
|
|
||||||
if (*block)
|
|
||||||
fAllocatedBlocks++;
|
|
||||||
else
|
|
||||||
SET_ERROR(error, B_NO_MEMORY);
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FreeBlock
|
|
||||||
void
|
|
||||||
Volume::FreeBlock(BlockReference *block)
|
|
||||||
{
|
|
||||||
if (block) {
|
|
||||||
fBlockAllocator->FreeBlock(block);
|
|
||||||
fAllocatedBlocks--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResizeBlock
|
|
||||||
BlockReference *
|
|
||||||
Volume::ResizeBlock(BlockReference *block, size_t size)
|
|
||||||
{
|
|
||||||
BlockReference *newBlock = NULL;
|
|
||||||
if (size <= fBlockSize && block) {
|
|
||||||
if (size == 0) {
|
|
||||||
fBlockAllocator->FreeBlock(block);
|
|
||||||
fAllocatedBlocks--;
|
|
||||||
} else
|
|
||||||
newBlock = fBlockAllocator->ResizeBlock(block, size);
|
|
||||||
}
|
|
||||||
return newBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckBlock
|
|
||||||
bool
|
|
||||||
Volume::CheckBlock(BlockReference *block, size_t size)
|
|
||||||
{
|
|
||||||
return fBlockAllocator->CheckBlock(block, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllocationInfo
|
// GetAllocationInfo
|
||||||
void
|
void
|
||||||
Volume::GetAllocationInfo(AllocationInfo &info)
|
Volume::GetAllocationInfo(AllocationInfo &info)
|
||||||
@@ -813,9 +730,6 @@ Volume::GetAllocationInfo(AllocationInfo &info)
|
|||||||
fRootDirectory->GetAllocationInfo(info);
|
fRootDirectory->GetAllocationInfo(info);
|
||||||
// name
|
// name
|
||||||
info.AddStringAllocation(fName.GetLength());
|
info.AddStringAllocation(fName.GetLength());
|
||||||
// block allocator
|
|
||||||
info.AddOtherAllocation(sizeof(BlockAllocator));
|
|
||||||
fBlockAllocator->GetAllocationInfo(info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadLock
|
// ReadLock
|
||||||
|
|||||||
@@ -34,9 +34,8 @@
|
|||||||
#include "String.h"
|
#include "String.h"
|
||||||
|
|
||||||
class AllocationInfo;
|
class AllocationInfo;
|
||||||
class Block;
|
class Attribute;
|
||||||
class BlockAllocator;
|
class AttributeIndex;
|
||||||
class BlockReference;
|
|
||||||
class Directory;
|
class Directory;
|
||||||
class DirectoryEntryTable;
|
class DirectoryEntryTable;
|
||||||
class Entry;
|
class Entry;
|
||||||
@@ -101,7 +100,6 @@ public:
|
|||||||
dev_t GetID() const { return fVolume != NULL ? fVolume->id : -1; }
|
dev_t GetID() const { return fVolume != NULL ? fVolume->id : -1; }
|
||||||
fs_volume* FSVolume() const { return fVolume; }
|
fs_volume* FSVolume() const { return fVolume; }
|
||||||
|
|
||||||
off_t GetBlockSize() const;
|
|
||||||
off_t CountBlocks() const;
|
off_t CountBlocks() const;
|
||||||
off_t CountFreeBlocks() const;
|
off_t CountFreeBlocks() const;
|
||||||
|
|
||||||
@@ -156,11 +154,6 @@ public:
|
|||||||
|
|
||||||
ino_t NextNodeID() { return fNextNodeID++; }
|
ino_t NextNodeID() { return fNextNodeID++; }
|
||||||
|
|
||||||
status_t AllocateBlock(size_t size, BlockReference **block);
|
|
||||||
void FreeBlock(BlockReference *block);
|
|
||||||
BlockReference *ResizeBlock(BlockReference *block, size_t size);
|
|
||||||
// debugging only
|
|
||||||
bool CheckBlock(BlockReference *block, size_t size = 0);
|
|
||||||
void GetAllocationInfo(AllocationInfo &info);
|
void GetAllocationInfo(AllocationInfo &info);
|
||||||
|
|
||||||
bigtime_t GetAccessTime() const { return fAccessTime; }
|
bigtime_t GetAccessTime() const { return fAccessTime; }
|
||||||
@@ -194,9 +187,6 @@ private:
|
|||||||
EntryListenerTree *fEntryListeners;
|
EntryListenerTree *fEntryListeners;
|
||||||
EntryListenerList fAnyEntryListeners;
|
EntryListenerList fAnyEntryListeners;
|
||||||
QueryList fQueries;
|
QueryList fQueries;
|
||||||
BlockAllocator *fBlockAllocator;
|
|
||||||
off_t fBlockSize;
|
|
||||||
off_t fAllocatedBlocks;
|
|
||||||
bigtime_t fAccessTime;
|
bigtime_t fAccessTime;
|
||||||
bool fMounted;
|
bool fMounted;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ ramfs_read_fs_info(fs_volume* _volume, struct fs_info *info)
|
|||||||
if (VolumeReadLocker locker = volume) {
|
if (VolumeReadLocker locker = volume) {
|
||||||
info->flags = B_FS_IS_PERSISTENT | B_FS_HAS_ATTR | B_FS_HAS_MIME
|
info->flags = B_FS_IS_PERSISTENT | B_FS_HAS_ATTR | B_FS_HAS_MIME
|
||||||
| B_FS_HAS_QUERY;
|
| B_FS_HAS_QUERY;
|
||||||
info->block_size = volume->GetBlockSize();
|
info->block_size = B_PAGE_SIZE;
|
||||||
info->io_size = kOptimalIOSize;
|
info->io_size = kOptimalIOSize;
|
||||||
info->total_blocks = volume->CountBlocks();
|
info->total_blocks = volume->CountBlocks();
|
||||||
info->free_blocks = volume->CountFreeBlocks();
|
info->free_blocks = volume->CountFreeBlocks();
|
||||||
|
|||||||
Reference in New Issue
Block a user