Add some prose describing this functionality.

Dedicated to timo, chongo, goto and ??=

Just formatting and comments.

svn path=/trunk/; revision=56268
This commit is contained in:
Art Yerkes
2012-03-29 06:01:52 +00:00
parent 8a3e9c37be
commit a09c32720c
3 changed files with 310 additions and 28 deletions
+10
View File
@@ -28,6 +28,16 @@ ULONG CcFastReadResourceMiss;
/* FUNCTIONS ******************************************************************/
/*
CcCopyRead can be called for a region of any size and alignment, so we must
crawl the cache space, focusing one cache stripe after another and using
RtlCopyMemory to copy the input data into the cache. In constrained memory,
pages faulted into new stripes are often taken from old stripes, causing the
old stripes to be flushed right away. In the case of many short buffered in
order writes, like the ones generated by stdio, this can be really efficient.
*/
BOOLEAN
NTAPI
CcCopyRead(IN PFILE_OBJECT FileObject,
+90
View File
@@ -27,6 +27,45 @@ HANDLE CcUnmapThreadHandle, CcLazyWriteThreadHandle;
CLIENT_ID CcUnmapThreadId, CcLazyWriteThreadId;
FAST_MUTEX GlobalPageOperation;
/*
A note about private cache maps.
CcInitializeCacheMap and CcUninitializeCacheMap are not meant to be paired,
although they can work that way.
The actual operation I've gleaned from reading both jan kratchovil's writing
and real filesystems is this:
CcInitializeCacheMap means:
Make the indicated FILE_OBJECT have a private cache map if it doesn't already
and make it have a shared cache map if it doesn't already.
CcUninitializeCacheMap means:
Take away the private cache map from this FILE_OBJECT. If it's the last
private cache map corresponding to a specific shared cache map (the one that
was present in the FILE_OBJECT when it was created), then delete that too,
flusing all cached information.
Using these simple semantics, filesystems can do all the things they actually
do:
- Copy out the shared cache map pointer from a newly initialized file object
and store it in the fcb cache.
- Copy it back into any file object and call CcInitializeCacheMap to make
that file object be associated with the caching of all the other siblings.
- Call CcUninitializeCacheMap on a FILE_OBJECT many times, but have only the
first one count for each specific FILE_OBJECT.
- Have the actual last call to CcUninitializeCacheMap (that is, the one that
causes zero private cache maps to be associated with a shared cache map) to
delete the cache map and flush.
So private cache map here is a light weight structure that just remembers
what shared cache map it associates with.
*/
typedef struct _NOCC_PRIVATE_CACHE_MAP
{
LIST_ENTRY ListEntry;
@@ -98,6 +137,19 @@ CcpReleaseFileLock(PNOCC_CACHE_MAP Map)
Map->Callbacks.ReleaseFromLazyWrite(Map->LazyContext);
}
/*
Cc functions are required to treat alternate streams of a file as the same
for the purpose of caching, meaning that we must be able to find the shared
cache map associated with the ``real'' stream associated with a stream file
object, if one exists. We do that by identifying a private cache map in
our gamut that has the same volume, device and fscontext as the stream file
object we're holding. It's heavy but it does work. This can probably be
improved, although there doesn't seem to be any real association between
a stream file object and a sibling file object in the file object struct
itself.
*/
// Must have CcpLock()
PFILE_OBJECT CcpFindOtherStreamFileObject(PFILE_OBJECT FileObject)
{
@@ -141,6 +193,8 @@ CcInitializeCacheMap(IN PFILE_OBJECT FileObject,
PNOCC_PRIVATE_CACHE_MAP PrivateCacheMap = FileObject->PrivateCacheMap;
CcpLock();
/* We don't have a shared cache map. First find out if we have a sibling
stream file object we can take it from. */
if (!Map && FileObject->Flags & FO_STREAM_FILE)
{
PFILE_OBJECT IdenticalStreamFileObject =
@@ -154,6 +208,7 @@ CcInitializeCacheMap(IN PFILE_OBJECT FileObject,
FileObject, IdenticalStreamFileObject, Map);
}
}
/* We still don't have a shared cache map. We need to create one. */
if (!Map)
{
DPRINT("Initializing file object for (%p) %wZ\n", FileObject, &FileObject->FileName);
@@ -170,6 +225,9 @@ CcInitializeCacheMap(IN PFILE_OBJECT FileObject,
InsertTailList(&CcpAllSharedCacheMaps, &Map->Entry);
DPRINT("New Map %x\n", Map);
}
/* We don't have a private cache map. Link it with the shared cache map
to serve as a held reference. When the list in the shared cache map
is empty, we know we can delete it. */
if (!PrivateCacheMap)
{
PrivateCacheMap = ExAllocatePool(NonPagedPool, sizeof(*PrivateCacheMap));
@@ -184,6 +242,14 @@ CcInitializeCacheMap(IN PFILE_OBJECT FileObject,
CcpUnlock();
}
/*
This function is used by NewCC's MM to determine whether any section objects
for a given file are not cache sections. If that's true, we're not allowed
to resize the file, although nothing actually prevents us from doing ;-)
*/
ULONG
NTAPI
CcpCountCacheSections(IN PNOCC_CACHE_MAP Map)
@@ -210,18 +276,25 @@ CcUninitializeCacheMap(IN PFILE_OBJECT FileObject,
ASSERT(UninitializeEvent == NULL);
/* It may not be strictly necessary to flush here, but we do just for
kicks. */
if (Map)
CcpFlushCache(Map, NULL, 0, NULL, FALSE);
CcpLock();
/* We have a private cache map, so we've been initialized and haven't been
* uninitialized. */
if (PrivateCacheMap)
{
ASSERT(!Map || Map == PrivateCacheMap->Map);
ASSERT(PrivateCacheMap->FileObject == FileObject);
RemoveEntryList(&PrivateCacheMap->ListEntry);
/* That was the last private cache map. It's time to delete all
cache stripes and all aspects of caching on the file. */
if (IsListEmpty(&PrivateCacheMap->Map->PrivateCacheMaps))
{
/* Get rid of all the cache stripes. */
while (!IsListEmpty(&PrivateCacheMap->Map->AssociatedBcb))
{
PNOCC_BCB Bcb = CONTAINING_RECORD(PrivateCacheMap->Map->AssociatedBcb.Flink, NOCC_BCB, ThisFileList);
@@ -242,9 +315,19 @@ CcUninitializeCacheMap(IN PFILE_OBJECT FileObject,
DPRINT("Uninit complete\n");
/* The return from CcUninitializeCacheMap means that 'caching was stopped'.
*/
return LastMap;
}
/*
CcSetFileSizes is used to tell the cache manager that the file changed
size. In our case, we use the internal Mm method MmExtendCacheSection
to notify Mm that our section potentially changed size, which may mean
truncating off data.
*/
VOID
NTAPI
CcSetFileSizes(IN PFILE_OBJECT FileObject,
@@ -298,6 +381,13 @@ CcSetDirtyPageThreshold(IN PFILE_OBJECT FileObject,
while (TRUE);
}
/*
This could be implemented much more intelligently by mapping instances
of a CoW zero page into the affected regions. We just RtlZeroMemory
for now.
*/
BOOLEAN
NTAPI
CcZeroData(IN PFILE_OBJECT FileObject,
+210 -28
View File
@@ -21,6 +21,73 @@
* This helped me determine that a certain bug was not a memory overwrite. */
//#define PIN_WRITE_ONLY
/*
Pinsup implements the core of NewCC.
A couple of things about this code:
I wrote this code over the course of about 2 years, often referring to Rajeev
Nagar's Filesystem Internals, book, the msdn pages on the Cc interface, and
a few NT filesystems that are open sourced. I went to fairly great lengths to
achieve a couple of goals.
1) To make a strictly layered facility that relies entirely on Mm to provide
maps. There were many ways in which data segments in the legacy Mm were unable
to provide what I needed; page maps were only 4 gig, and all offsets were in
ULONG, so no mapping at an offset greater than 4 gig was possible. Worse than
that, due to a convoluted set of dependencies, it would have been impossible to
support any two mappings farther apart than 4 gig, even if the above was
corrected. Along with that, the cache system's ownership of some pages was
integral to the operation of legacy Mm. All of the above problems, along with
an ambiguity about when the size of a file for mapping purposes is acquired,
and its inability to allow a file to be resized when any mappings were active
led me to rewrite data sections (and all other kinds of sections in the
original version), and use that layer to implement the Cc API without regard
to any internal, undocumented parts.
2) To write the simplest possible code that implements the Cc interface as
documented. Again this is without regard to any information that might be
gained through reverse engineering the real Cc. All conclusions about workings
of Cc here are mine, any failures are mine, any differences to the documented
interface were introduced by me due to misreading, misunderstanding or mis
remembering while implementing the code. I also implemented some obvious, but
not actually specified behaviors of Cc, for example that each cache stripe is
represented by a distinct BCB that the user can make decisions about as an
opaque pointer.
3) To make real filesystems work properly.
So about how it works:
CcCacheSections is the collection of cache sections that are currently mapped.
The cache ranges which are allocated and contain pages is larger, due to the
addition of sections containing rmaps and page references, but this array
determines the actual mapped pages on behalf of all mapped files for Cc's use.
All BCB pointers yielded to a driver are a pointer to one of these cache stripe
structures. The data structure is specified as opaque and so it contains
information convenient to NEWCC's implementation here. Free entries are
summarized in CcpBitmapBuffer, for which bits are set when the entry may be
safely evicted and redirected for use by another client. Note that the
reference count for an evictable cache section will generally be 1, since
we'll keep a reference to wait for any subsequent mapping of the same stripe.
We use CcCacheClockHand as a hint to start checking free bits at a point that
walks around the cache stripe list, so that we might evict a different stripe
every time even if all are awaiting reuse. This is a way to avoid thrashing.
CcpBitmapBuffer is the RTL_BITMAP that allows us to quickly decide what buffer
to allocate from the mapped buffer set.
CcDeleteEvent is an event used to wait for a cache stripe reference count to
go to 1, thus making the stripe eligible for eviction. It's used by CcpMapData
to wait for a free map when we can't fail.
All in all, use of Mm by Cc makes this code into a simple manager that wields
sections on behalf of filesystems. As such, its code is fairly high level and
no architecture specific changes should be necessary.
*/
/* GLOBALS ********************************************************************/
#define TAG_MAP_SEC TAG('C', 'c', 'S', 'x')
@@ -55,6 +122,14 @@ PDEVICE_OBJECT
NTAPI
MmGetDeviceObjectForFile(IN PFILE_OBJECT FileObject);
/*
Allocate an almost ordinary section object for use by the cache system.
The special internal SEC_CACHE flag is used to indicate that the section
should not count when determining whether the file can be resized.
*/
NTSTATUS CcpAllocateSection
(PFILE_OBJECT FileObject,
ULONG Length,
@@ -94,6 +169,14 @@ typedef struct _WORK_QUEUE_WITH_CONTEXT
BOOLEAN Dirty;
} WORK_QUEUE_WITH_CONTEXT, *PWORK_QUEUE_WITH_CONTEXT;
/*
Unmap a cache stripe. Note that cache stripes aren't unmapped when their
last reference disappears. We enter this code only if cache for the file
is uninitialized in the last file object, or a cache stripe is evicted.
*/
VOID
CcpUnmapCache(PVOID Context)
{
@@ -105,6 +188,20 @@ CcpUnmapCache(PVOID Context)
DPRINT("Done\n");
}
/*
Somewhat deceptively named function which removes the last reference to a
cache stripe and completely removes it using CcUnmapCache. This may be
done either inline (if the Immediate BOOLEAN is set), or using a work item
at a later time. Whether this is called to unmap immeidately is mainly
determined by whether the caller is calling from a place in filesystem code
where a deadlock may occur if immediate flushing is required.
It's always safe to reuse the Bcb at CcCacheSections[Start] after calling
this.
*/
/* Must have acquired the mutex */
VOID CcpDereferenceCache(ULONG Start, BOOLEAN Immediate)
{
@@ -186,6 +283,18 @@ VOID CcpDereferenceCache(ULONG Start, BOOLEAN Immediate)
DPRINT("Done\n");
}
/*
CcpAllocateCacheSections is called by CcpMapData to obtain a cache stripe,
possibly evicting an old stripe by calling CcpDereferenceCache in order to
obtain an empty Bcb.
This function was named plural due to a question I had at the beginning of
this endeavor about whether a map may span a 256k stripe boundary. It can't
so this function can only return the index of one Bcb. Returns INVALID_CACHE
on failure.
*/
/* Needs mutex */
ULONG CcpAllocateCacheSections
(PFILE_OBJECT FileObject,
@@ -198,12 +307,12 @@ ULONG CcpAllocateCacheSections
DPRINT("AllocateCacheSections: FileObject %x\n", FileObject);
if (!FileObject->SectionObjectPointer)
return INVALID_CACHE;
return INVALID_CACHE;
Map = (PNOCC_CACHE_MAP)FileObject->SectionObjectPointer->SharedCacheMap;
if (!Map)
return INVALID_CACHE;
return INVALID_CACHE;
DPRINT("Allocating Cache Section\n");
@@ -212,34 +321,34 @@ ULONG CcpAllocateCacheSections
if (i != INVALID_CACHE)
{
DPRINT("Setting up Bcb #%x\n", i);
Bcb = &CcCacheSections[i];
DPRINT("Setting up Bcb #%x\n", i);
Bcb = &CcCacheSections[i];
ASSERT(Bcb->RefCount < 2);
if (Bcb->RefCount > 0)
{
CcpDereferenceCache(i, FALSE);
}
ASSERT(!Bcb->RefCount);
Bcb->RefCount = 1;
DPRINT("Bcb #%x RefCount %d\n", Bcb - CcCacheSections, Bcb->RefCount);
if (!RtlTestBit(CcCacheBitmap, i))
{
DPRINT1("Somebody stoeled BCB #%x\n", i);
}
ASSERT(RtlTestBit(CcCacheBitmap, i));
DPRINT("Allocated #%x\n", i);
ASSERT(CcCacheSections[i].RefCount);
ASSERT(Bcb->RefCount < 2);
if (Bcb->RefCount > 0)
{
CcpDereferenceCache(i, FALSE);
}
ASSERT(!Bcb->RefCount);
Bcb->RefCount = 1;
DPRINT("Bcb #%x RefCount %d\n", Bcb - CcCacheSections, Bcb->RefCount);
if (!RtlTestBit(CcCacheBitmap, i))
{
DPRINT1("Somebody stoeled BCB #%x\n", i);
}
ASSERT(RtlTestBit(CcCacheBitmap, i));
DPRINT("Allocated #%x\n", i);
ASSERT(CcCacheSections[i].RefCount);
}
else
{
DPRINT1("Failed to allocate cache segment\n");
DPRINT1("Failed to allocate cache segment\n");
}
return i;
}
@@ -262,6 +371,14 @@ VOID CcpMarkForExclusive(ULONG Start)
Bcb->ExclusiveWaiter++;
}
/*
Cache stripes have an idea of exclusive access, which would be hard to support
properly in the previous code. In our case, it's fairly easy, since we have
an event that indicates that the previous exclusive waiter has returned in each
Bcb.
*/
/* Must not have the mutex */
VOID CcpReferenceCacheExclusive(ULONG Start)
{
@@ -277,7 +394,19 @@ VOID CcpReferenceCacheExclusive(ULONG Start)
CcpUnlock();
}
/* Find a map that encompasses the target range */
/*
Find a map that encompasses the target range. This function does not check
whether the desired range is partly outside the stripe. This could be
implemented with a generic table, but we generally aren't carring around a lot
of segments at once for a particular file.
When this returns a map for a given file address, then that address is by
definition already mapped and can be operated on.
Returns a valid index or INVALID_CACHE.
*/
/* Must have the mutex */
ULONG CcpFindMatchingMap(PLIST_ENTRY Head, PLARGE_INTEGER FileOffset, ULONG Length)
{
@@ -302,6 +431,14 @@ ULONG CcpFindMatchingMap(PLIST_ENTRY Head, PLARGE_INTEGER FileOffset, ULONG Leng
return INVALID_CACHE;
}
/*
Internal function that's used by all pinning functions.
It causes a mapped region to exist and prefaults the pages in it if possible,
possibly evicting another stripe in order to get our stripe.
*/
BOOLEAN
NTAPI
CcpMapData
@@ -364,6 +501,11 @@ CcpMapData
DPRINT("File size %08x%08x\n", Map->FileSizes.ValidDataLength.HighPart, Map->FileSizes.ValidDataLength.LowPart);
/* Not all files have length, in fact filesystems often use stream file
objects for various internal purposes and are loose about the file
length, since the filesystem promises itself to write the right number
of bytes to the internal stream. In these cases, we just allow the file
to have the full stripe worth of space. */
if (Map->FileSizes.ValidDataLength.QuadPart)
{
SectionSize = min(CACHE_STRIPE, Map->FileSizes.ValidDataLength.QuadPart - Target.QuadPart);
@@ -378,6 +520,8 @@ CcpMapData
//ASSERT(SectionSize <= CACHE_STRIPE);
CcpUnlock();
/* CcpAllocateSection doesn't need the lock, so we'll give other action
a chance in here. */
Status = CcpAllocateSection
(FileObject,
SectionSize,
@@ -399,8 +543,9 @@ CcpMapData
retry:
/* Returns a reference */
DPRINT("Allocating cache sections: %wZ\n", &FileObject->FileName);
DPRINT("Allocating cache sections: %wZ\n", &FileObject->FileName);
BcbHead = CcpAllocateCacheSections(FileObject, SectionObject);
/* XXX todo: we should handle the immediate fail case here, but don't */
if (BcbHead == INVALID_CACHE)
{
ULONG i;
@@ -429,12 +574,18 @@ retry:
ViewSize = CACHE_STRIPE;
Bcb = &CcCacheSections[BcbHead];
/* MmMapCacheViewInSystemSpaceAtOffset is one of three methods of Mm
that are specific to NewCC. In this case, it's implementation
exactly mirrors MmMapViewInSystemSpace, but allows an offset to
be specified. */
Status = MmMapCacheViewInSystemSpaceAtOffset
(SectionObject->Segment,
&Bcb->BaseAddress,
&Target,
&ViewSize);
/* Summary: Failure. Dereference our section and tell the user we failed
*/
if (!NT_SUCCESS(Status))
{
*BcbResult = NULL;
@@ -447,6 +598,9 @@ retry:
goto cleanup;
}
/* Summary: Success. Put together a valid Bcb and link it with the others
* in the NOCC_CACHE_MAP.
*/
Success = TRUE;
//DPRINT("w1n\n");
@@ -539,6 +693,8 @@ CcMapData
return Result;
}
/* Used by functions that repin data, CcpPinMappedData does not alter the map,
but finds the appropriate stripe and update the accounting. */
BOOLEAN
NTAPI
CcpPinMappedData(IN PNOCC_CACHE_MAP Map,
@@ -708,6 +864,32 @@ CcPreparePinWrite(IN PFILE_OBJECT FileObject,
return Result;
}
/*
CcpUnpinData is the internal function that generally handles unpinning data.
It may be a little confusing, because of the way reference counts are handled.
A reference count of 2 or greater means that the stripe is still fully pinned
and can't be removed. If the owner had taken an exclusive reference, then
give one up. Note that it's an error to take more than one exclusive reference
or to take a non-exclusive reference after an exclusive reference, so detecting
or handling that case is not considered.
ReleaseBit is unset if we want to detect when a cache stripe would become
evictable without actually giving up our reference. We might want to do that
if we were going to flush before formally releasing the cache stripe, although
that facility is not used meaningfully at this time.
A reference count of exactly 1 means that the stripe could potentially be
reused, but could also be evicted for another mapping. In general, most
stripes should be in that state most of the time.
A reference count of zero means that the Bcb is completely unused. That's the
start state and the state of a Bcb formerly owned by a file that is
uninitialized.
*/
BOOLEAN
NTAPI
CcpUnpinData(IN PNOCC_BCB RealBcb, BOOLEAN ReleaseBit)