Access to files is provided by a number of programs acting as components. For convenience, the component-level operations are wrapped up in a client library that aims to provide simpler, more familiar mechanisms for opening, reading, writing, and closing files, together with various other operations.
Components are provided by functionality in libfsserver used by programs found in the servers directory within the departure package. In general, each component runs in its own server thread.
Components are accessed via interfaces defined using the interface description language supported by the idl tool provided by the idl4re distribution. Interface operations in this document are described using excerpts from the appropriate interface descriptions.
Filesystems implement the Filesystem interface which provides the open_for_user operation:
open_for_user(in user_t user, out cap opener)
The operation yields a file opener appropriate for the given user credentials.
In pseudocode, the operations as conducted by the client program are as follows:
opener = filesystem.open_for_user(user)
Openers implement the Opener interface which provides the context operation:
context(out cap context)
Each client program, task or thread obtains its own context because it will need its own dedicated channel for communication with the filesystem.
In pseudocode, the operations as conducted by the client program are as follows:
context = opener.context()
An opener context acts as a dataspace, meaning that it can be attached to a task using a region manager and provide a buffer via a region of mapped memory that the task can write to. In the case of a context, the task will write a filesystem path indicating the file to be opened.
Each context allows a client program to request access to individual files via operations provided by the OpenerContext interface, of which the most pertinent is the open operation:
open(in flags_t flags, out offset_t size, out cap file, out object_flags_t object_flags)
Using the path information written to the context's memory region, the open operation will obtain a reference to a file-like object whose characteristics are described by the accompanying object_flags, these helping the client to distinguish between files that support arbitrary memory mapping operations and pipes that mandate sequential region-by-region access.
Alongside regular files, directories may also be opened. Reading from them yields a listing of directory entries.
In pseudocode, the operations as conducted by the client program are as follows:
context.write("filename") # this being a memory access operation file = context.open(flags, ...)
Filesystem objects are removed by invoking the remove operation on an opener context:
remove()
The path information identifying the object must first be written to the context's memory region.
Filesystem objects are renamed by invoking the rename operation on an opener context:
rename()
The path information of the affected object and the destination of the rename operation must first be written to the context's memory region. The destination path follows immediately after the terminating byte of the affected path.
Statistics or metadata for a filesystem object can be obtained by invoking the stat operation on an opener context:
stat()
The path information identifying the object must first be written to the context's memory region. As a result of the invocation, a stat data structure will be written to the start of the memory region.
Files themselves act as dataspaces, meaning that they can be attached to a task using a region manager and provide their content via a region of mapped memory. Files implement the MappedFile interface.
Control over the region of the file provided via mapped memory occurs using the mmap operation:
mmap(in offset_t position, in offset_t length, in offset_t start_visible, in offset_t end_visible, out offset_t start_pos, out offset_t end_pos, out offset_t size)
Files also implement the more general File interface that provides the resize operation:
resize(inout offset_t size)
This allows the portion of the memory region dedicated to the file's contents to be extended.
Directories are obtained, like files, using the open operation. They implement the Directory interface.
To read directory listings, the opendir operation is used to obtain a directory reader:
opendir(out offset_t size, out cap file, out object_flags_t object_flags)
Directory readers are meant to be accessed like files, meaning that it should be possible to attach them to a task using a region manager and access the provided content, this being a listing of directory entries, via the mapped region.
However, unlike files which may support arbitrary mapping of their contents, the provided content may be supplied by a pipe endpoint, thereby not supporting precisely the same navigation mechanisms as those supported by files.
Reading from an opened directory is achieved as shown in the following diagram.
In pseudocode, the operations as conducted by the client program are as follows:
reader = directory.opendir() reader.current_region() entries = reader.read() # this being a memory access operation
Distinct from filesystems but potentially used by them, pipe openers provide a means of obtaining pipes, which are channels that support unidirectional communication via shared memory.
Pipe openers implement the PipeOpener interface and support the following operation:
pipe(in offset_t size, out cap reader, out cap writer)
The size is indicated to request pipe regions long enough for the needs of the communicating parties, with both reader and writer endpoint capabilities being returned. Such capabilities may be propagated to the eventual parties, these typically being separate tasks.
Although not generally obtained from filesystems, pipes may be involved in providing content from some filesystem objects such as directories. However, they are also obtained directly from an appropriate pipe server providing pipe opening facilities.
Pipes expose single regions of shared memory to their endpoints, with the writing endpoint populating one region while the reading endpoint accesses the other. The reading endpoint may advance to the region being written, and this will free up a new region for the writer when it has filled its region. When the writer itself advances, it permits the reader to consume all data in the fully populated region. Naturally, the reader may not advance ahead of the writer.
Pipes implement the Pipe interface and a number of operations to support this interaction mechanism.
The details of an endpoint's current region can be queried using the following operation:
current_region(out offset_t populated_size, out offset_t size)
This provides details of the populated size (or amount of written data) in a region along with the size of the region.
Navigation to the next available region of the pipe is performed using the following operation:
next_region(inout offset_t populated_size, out offset_t size)
Here, the populated size may be specified by the writer so that the reader may query the current region's properties using the appropriate operation.
The status of the pipe can be queried using the closed operation:
closed(out int closed)
This indicates through a boolean-equivalent parameter whether one or both endpoints have been closed.