In the last entry I started to outline the high level components of implementing a Process Isolation framework. In this entry, I’ll dive a little deeper into the file system aspects of Process Isolation including:
- Processing file opens … or creates, or overwrites, or supersedes
- How to handle deleted files
But first let’s talk a little about how the file system isolation will be handled in general. The file system filter driver will implement what is called a shadow file system. For accesses on files or directories, a given File Object, call it FO(top), will be passed into the filter driver from the IO Manager specifying a name of the object to be accessed. Because the filter driver will be taking ownership of FO(top)–more on ownership later–it will initiate a new open to the underlying base file system name space, or BNS. This additional open will be for the corresponding object name in the BNS. It will also attempt to open the file in the shadow name space, what we previously called the SNS. For ease, let’s have the SNS be rooted in the directory C:\SNS and have per-volume GUID sub-directories for each volume in the system.
After these 2 opens have completed we will know:
- If the file exists in the BNS
- If the file exists in the SNS
Let’s call the respective file objects returned on successful opens FO(BNS) and FO(SNS). To review:
- We have determined that we are isolating some process (more on this later) that is accessing a file, let’s say C:\Foo.txt.
- The names associated with the various file objects are: FO(top) = C:\Foo.txt, FO(BNS) = C:\Foo.txt, FO(SNS) = C:\SNS\Volume{GUID}\Foo.txt
- The filter has received this open request in the pre-create handler with the corresponding file object FO(top) and performed opens on 2 other instances resulting in FO(BNS) and FO(SNS). Note that either or both of these secondary opens may have failed.
Next we need to determine how to handle the original open initiated on C:\Foo.txt, i.e. FO(top). Let’s break down the possible cases:
Case 1: Neither FO(BNS) or FO(SNS) exist
This case is where the file C:\Foo.txt does not exist in either of the underlying name spaces so we can process the initial request based on what the caller would like to do. If the caller specified any of the create options such as FILE_CREATE, FILE_OPEN_IF or FILE_OVERWRITE_IF, then we can create this file in the SNS and complete the request as if the file were created. Given our goal of isolating this process, we are not creating the file in the BNS. In fact we will never allow an operation to alter (create/change/delete) content in the BNS, only the SNS. Thus in this case we would end up creating a file called C:\SNS\Volume{Guid}\Foo.txt in the SNS. As an optimization, we could have passed down the correct CreateDisposition when we initially checked the SNS, and the file would have been created at that point instead of having to do multiple calls into the SNS.
Case 2: Both FO(BNS) and FO(SNS) exist
In this case, the file exists in both name spaces and we need to handle the result of the operation based on what the caller is requesting. If the caller requested to simply open the file, that is they specified FILE_OPEN or FILE_OPEN_IF, then we can stash away the FO(SNS) in our context structure and note that this file is a Shadow File. If the caller specified FILE_CREATE, we immediately fail the operation with FILE_OBJECT_NAME_COLLISION since we are not able to create the file. If the caller specified a destructive create, such as FILE_OVERWRITE, FILE_OVERWRITE_IF or FILE_SUPERSEDE, then we would need to perform the corresponding operation on the FO(SNS). This can be handled either by explicitly truncating the SNS file and possibly resetting attributes or, as indicated in Case 1, sending down the CreateDisposition when initially opening the file in the SNS.
Case 3: FO(BNS) exists and FO(SNS) does not exist
In this case, the file exists in the BNS but not in the SNS. Moving as we have in the previous cases we will handle this case based upon what the caller is requesting with one exception. If the file had been previously deleted, which will be discussed below, then the FO(BNS) could be closed and the operation processed as in Case 1. If the file had not been previously deleted then, based upon the caller’s request, we proceed. If the caller requested FILE_OPEN or FILE_OPEN_IF, then we stash away FO(BNS) into the context structure and note that this file is NOT a Shadow File. If the caller requested FILE_CREATE, we immediately fail this request with STATUS_OBJECT_NAME_COLLISION since the file cannot be recreated.
Now the complex cases … FILE_OVERWRITE, FILE_OVERWRITE_IF and FILE_SUPERSEDE. In each of these cases the file in the BNS must not be altered, so the file must be first migrated to the SNS. The optimization here is that the file data does not have to be migrated since all of these cases are destructive opens and result in the file being truncated to zero length. So only the file name and possibly the file attributes need to be created in the SNS. The result will be a valid FO(SNS) that will be stashed away in our context structure noting that the file is a Shadow File.
Case 4: FO(BNS) does not exist and FO(SNS) does exist
In this final case, the file exists in the SNS but not in the BNS. If the caller indicated FILE_OPEN or FILE_OPEN_IF, then the FO(SNS) would be stashed away in the context structure and the file marked as a Shadow File. If the caller requested FILE_CREATE, we can immediately fail this request with STATUS_OBJECT_NAME_COLLISION as before. If the caller requested FILE_OVERWRITE, FILE_OVERWRITE_IF or FILE_SUPERSEDE, then we would proceed as in Case 2 and perform the necessary operations to truncate the file and possibly reset attributes on the FO(SNS). In the end we would stash away the FO(SNS) in our context structure and mark the file as a Shadow File.
At this point in the processing we have established where the file exists and have a valid FO, either the FO(BNS) or FO(SNS). As noted in Part 1 of this series, for files there is no need to have both file objects open, as in Case 2 above. In this scenario we only need FO(SNS) for files so we would close off the FO(BNS). For directories we will need to maintain both file objects since we may need to query the FO(BNS) on a directory enumeration in order to merge the content with the FO(SNS) view. In all scenarios, FO(top) is what we are taking ownership of because it is the FO that the caller is actually interacting with, we are simply redirecting those accesses to the appropriate underlying FO(s) (BNS and/or SNS).
There are two items to address now that involve handling deleted files and the context structure that was alluded to in the above sections.
The first is how to handle deleted files. Since the object of Process Isolation is to keep the BNS unaltered, we need a way to maintain the fact that a file was deleted from the BNS without actually deleting the file from the BNS. The options depend on the whether the isolation allows for persistent information. If all information about a given process group being isolated is deleted when the process group terminates, then it would be sufficient to keep an in-memory structure that shows the file C:\Foo.txt has been deleted. In the above cases where we need to determine if the file had previously been deleted, we simply check this in-memory tree. Note that we only need to maintain this information for files that exist in the BNS. Even after the file is subsequently created in the SNS, we must maintain this information about the state of the file in the BNS since the file now in the SNS could later be deleted. If we did not maintain this information about the state of the file in the BNS, then when an open was processed later on the file, we would find it exists in the BNS, not knowing it had previously been deleted. If the isolation information for a process group is going to persist across a given process, terminating and restarting, then one alternative would be to create a stub file in the SNS to indicate that the file has been deleted from the BNS. You could name this file C:\SNS\Volume{GUID}\~deleted.Foo.txt for an indicator that C:\Foo.txt had been deleted, for example. Then the determination if the file had been previously deleted in the above cases would be a matter of attempting to open this mangled file name in the SNS.
Lastly, we will cover our context structure. In the preceding discussions, a lot has been assumed about the reader’s understanding of how file systems, in general, operate under the Windows platforms. This will be no exception … the context described above will be the memory block to be allocated, initialized and set as the FO(top)->FsContext and FO(top)->FsContext2 pointers. This will allow us to track per file and per open information for each access. In addition to setting up these pointers in the FO(top) file object, we will also establish the FO(top)->SectionObjectPointer control structure, which will mandate that we take over control of the cache interface for processing IO requests. More on this later. But for now, all of the initialization of the FO(top) pointers will result in us taking ownership of the FO(top) file object so it is our responsibility to handle ALL requests for this file object.
UP NEXT: I’ll discuss the details of the Copy-on-Write processing, as well as some of the more subtle details of directory merging and rename processing.
Leave a Reply
You must be logged in to post a comment.