Go to the previous, next section.
This chapter of the manual documents the system's filing system, and the interface it provides to other modules.
The filing system provides a means of storing data on block devices. Files are organised in a tree-like structure through the use of directories. There is no limit on the number of files or directories that a directory can contain.
All data structures and functions documented in this chapter are defined in the C header file `<vmm/fs.h>'.
The filing system is organised as a single module (see section Modules) whose name is `fs'. To get a pointer to this module something like the following code would be needed:
#include <vmm/fs.h> #include <vmm/kernel.h> ... struct fs_module fs; ... fs = (struct fs_module *)kernel->open_module("fs", SYS_VER); if(fs != NULL) { /* Use the file system module.. */ ...
All the functions documented in this chapter are actually members of
this module. For example to call the open
function:
struct file *fh; fh = fs->open("/usr/foo", F_READ);
Each file (or directory) can be located through the use of a file name. Each file name tells the system how to locate the file it specifies by listing the directories that have to be traversed from the current directory and then the name of the actual file separated by `/' characters.
Other meta characters include `:' to denote a device name (for example, the file name `hda1:foo' denotes the file `foo' on the device `hda1') or the root directory of the current device (for example `:foo', the file `foo' in the root of the current device).
Each directory contains two special entries, `.' refers to this directory and `..' refers to the directory above this one.
Finally, a `/' character at the start of a file name refers to the system's global root directory.
The following table contains several example file names together with a description of how the filing system uses them to locate the file or directory that they specify.
Our filing system has been modelled largely on the type of filing system often used by the UNIX operating system. That is, with inodes, indirect blocks, etc...
When a file system is created a certain amount of space is set aside for inodes. An inode is a data object defining a single file, it is actually defined as:
struct inode { /* Flags defining the permissions and type of the file. */ u_long attr; /* The size, in bytes, of the file. */ size_t size; /* The time that the file was last modified. */ time_t modtime; /* The number of references to this inode. */ u_long nlinks; /* Pointers to data blocks. data[9->11] are single, double and triple indirect blocks. */ u_long data[12]; };
Since a device's inodes are stored contiguously in a known location on the disk each inode has a unique (to the device) identifier; its index in the inode table. This number is called the inode's i-number.
Directories are stored in files, that is, the data defining the
contents of the directory (the name of the entry and the i-number it
points to) is contained in a file which has an entry (i.e. its
i-number) in the directory above. To the filing system the only
difference between a file and a directory is the value of the
attr
field in the object's inode.
Each entry in a directories data-file is defined as being:
#define NAME_MAX 27 struct dir_entry { /* A null name means a free slot. */ char name[NAME_MAX + 1]; u_long inum; };
Two bitmaps are used to record the allocated portions of each device, the inode bitmap codes which inodes are in use while the data bitmap records the same information for data blocks.
The main objective of the filing system is to provide tasks running in with the ability to access individual files. The functions to do this are documented in this section.
Most of the functions which perform operations on files take a parameter known as a file handle to specify which file to operate on. File handles are associated with a single file in the filing system and contain state information such as the position in the file which the next read or write will occur at.
To obtain a file handle a module must call the function open
with the name of the file to be opened (see section File Names) and an
integer defining how the handle will be used.
fs Function: struct file * open (const char *filename, u_long mode)
This function creates a new file handle and associates it with the file whose name is filename. The file position attribute is initially set to zero (the first character in the file).
The argument mode is a bit-mask created by a bitwise-OR of the necessary values. The possible values are:
F_READ
F_WRITE
F_READ
and
F_WRITE
allows both read and write access to the file.
F_CREATE
F_TRUNCATE
F_ALLOW_DIR
open
does not signal an error if the file
being opened is actually a directory.
F_DONT_LINK
If this function is successful it will return a pointer to the newly-
created file handle, otherwise if an error occurs a null pointer is
returned and the task's errno
field is set to a suitable value.
When a task has finished using a file handle it should close it (note that there is no automatic resource tracking: any open file handles will not be garbage-collected). The following function should be used for this purpose.
fs Function: void close (struct file *file)
This function deallocates the file handle file, reclaiming all the resources that it uses. It should only be called when file will not be used in the future.
It is often useful to `clone' file handles, the dup
function is
provided for this purpose.
fs Function: struct file * dup (struct file *source)
This function creates a new file handle which is associated with the same file as its sole argument, the file handle source.
The mode attribute of the new file handle is the same as that of source and its file position is set to the beginning of the file.
If this function is successful it will return a pointer to the new
file handle, if an error occurs it will return a null pointer and set
the task's errno
to an informative value.
This section of the manual documents the I/O primitives used to read and write data to a file specified by a file handle.
fs Function: long read (void *buf, size_t length, struct file *file)
This function reads as many characters as possible not exceeding the value of the parameter length from the file specified by the file handle file.
These characters are stored sequentially in the area of memory (in the kernel segment) located by the pointer buf. The value of the file handle's file position attribute defines where in the file the characters are read from.
The number of characters actually read is returned, this may be less
than the value of the length parameter, for example if the end
of the file is reached. If an error occurs this function returns a
negative error value (and errno
is also set).
The file handle's position attribute is advanced by the number of characters actually read.
fs Function: long write (void *buf, size_t length, struct file *file)
This function attempts to write length characters from the buffer pointed to by buf (in the kernel segment) to the file specified by the file handle file.
The characters are written at the position defined by the file handle's file position attribute.
The value returned is the number of characters actually written
(usually length but not always) or a negative error code. Note
that the task's errno
value is set to the same error.
The file handle's position attribute is advanced by the number of characters actually written.
fs Function: long seek (struct file *file, long arg, int type)
This function is used to adjust the value of a file handles file position attribute (which defines where in the file characters are read and written).
The type parameter defines how the value arg is used to adjust the file position of the file handle file. It can take one of the following values:
SEEK_ABS
SEEK_REL
SEEK_EOF
The value that this function returns is either the new value of
the file position (a positive value), or a negative error code
(errno
is set to reflect the same error).
As was explained in the section about the structure of a filing system (see section Filesystem Structure) each individual file has an inode to store its details. Although there is usually a one to one relation between directory entries and inodes this does not have to be the case.
Each directory entry constitutes a hard link to the file represented by the inode that it points to. Within a device there is no limit to the number of directory entries that may point to an inode; hard links may not point to an inode on a different device.
fs Function: bool make_link (const char *name, struct file *src)
This function creates a new hard link to the file pointed to by the file handle src. The new link is called name, name is interpreted relative to the task's current directory.
If the function succeeds in creating the link it returns the value
TRUE
, otherwise it sets the task's errno
to a suitable
value and returns the value FALSE
.
fs Function: bool remove_link (const char *name)
This function deletes the directory entry called name. If this is the last link to the inode that name points at the inode and its associated data will be deleted.
Note that it is a very bad idea to call this with name
referring to a directory: its likely to result in stranded data blocks.
Instead use the function remove_directory
(see section Directory Handling).
When this function succeeds it returns TRUE
, otherwise it
returns FALSE
after setting the task's errno
to an
informative value.
Although useful in many situations hard links have several defects: they can't be used across devices and they can be hard to keep track of. To solve these problems our filing system also provides symbolic links; instead of providing a link from a directory entry to an inode as hard links do, symbolic links provide a link from a directory entry to another directory entry.
Since the link is entirely contained in the inode of the directory entry initiating the link whatever happens to the object which is the target of the link, the link cannot be broken (unless the object is deleted, in which case attempting to open the link will fail until an object of the target's name is re-created).
Any attempt to open a directory entry which is a symbolic link will
result in the target of the link being opened, if the target of the
link is actually another symbolic link the process will be repeated.
Note that the F_DONT_LINK
option to the open
function
(see section File Handles) will result in the link itself being opened.
fs Function: bool make_symlink (const char *name, const char *target)
This function creates a symbolic link called name which points to the directory entry called target.
If this function succeeds in creating the new link it returns
TRUE
, otherwise it sets the task's errno
to a suitable
value and returns FALSE
.
This section of the manual documents the other operations available with files. Firstly there are a number of macros which can be used to extract the individual pieces of information held in a file handle.
Macro: u_long F_ATTR (struct file *file)
Expands to the attribute field of the inode associated with the file handle file.
Macro: u_long F_NLINKS (struct file *file)
Expands to the number of hard links pointing at the inode associated with the file handle file.
Macro: u_long F_SIZE (struct file *file)
Expands to the size of the file associated with the file handle file.
Macro: time_t F_MODTIME (struct file *file)
Expands to the date-stamp specifying the last time that this file was modified.
Macro: u_long F_INUM (struct file *file)
Expands to the i-number of the inode associated with the file handle file.
Macro: int F_IS_DIR (struct file *file)
Expands to a non-zero value if the file handle file is associated with a directory.
Macro: int F_IS_SYMLINK (struct file *file)
Expands to a non-zero value if the file handle file is associated with a symbolic link.
Macro: int F_IS_REG (struct file *file)
Expands to a non-zero value if the file handle file is associated with a regular file.
Macro: int F_READABLE (struct file *file)
Expands to a non-zero value if the file associated with the file handle file is readable.
Macro: int F_WRITEABLE (struct file *file)
Expands to a non-zero value if the file associated with the file handle file is writable.
Macro: int F_EXECABLE (struct file *file)
Expands to a non-zero value if the file associated with the file handle file is executable.
The following functions also operate on files.
fs Function: bool set_file_size (struct file *file, size_t size)
This function sets the size of the file associated with the file handle file to be size characters.
Note that no data is actually deleted from the file, even if size is smaller than the current size of the file!
If this function succeeds it will return TRUE
, otherwise it
will set errno
to a suitable value and return FALSE
.
fs Function: bool truncate_file (struct file *file)
This function deletes all data associated with the file pointed to by the file handle file and sets its size to be zero characters.
If this function succeeds it will return TRUE
, otherwise it
will set errno
to a suitable value and return FALSE
.
fs Function: bool set_file_mode (const char *name, u_long modes)
This function sets the mode bits of the inode pointed to by the directory entry called name to the bit-mask modes.
To create the modes value do a bitwise-OR of any of the following values:
ATTR_NO_READ
ATTR_NO_WRITE
ATTR_EXEC
If this function succeeds it will return TRUE
, otherwise it
will set errno
to a suitable value and return FALSE
.
Directories are a special type of file, they store a list of names together with the i-number of the inode that that directory entry refers to. Directories are organised hierarchically, the top of the tree of directories on a device is known as the device's root directory. The system root directory is the directory denoted by a leading slash in a file name, this can be any directory in the filing system and is defined when the system is compiled. It must be a directory that is available when the system is booted to allow disk-based modules to be loaded (see section Modules).
Each directory has two special entries: `.' which is a hard link to the directory itself, and `..' which is a hard link to the directory's parent directory. Whenever a directory is created these entries are automatically produced.
Each task has an attribute called its current directory, this is the point in the filing system from which all file names that the task uses are resolved. Obviously file names which begin with a device specification or a slash (i.e. `hda4:lib' or `/usr') do not actually use the current directory.
Since directories are stored as (special) files the open
function can be used to get a handle on a directory; note that when
opening a directory remember to specify the F_ALLOW_DIR
option
or an error will be signaled (see section File Handles).
fs Function: struct file * get_current_directory (void)
This function creates a new file handle which has been opened on the current task's current directory. Note that this is equivalent to the following:
fs->open(".", F_READ | F_WRITE | F_ALLOW_DIR);
If this function succeeds it will return a pointer to the new file
handle, otherwise it will set the task's errno
value
accordingly and return a null pointer.
fs Function: struct file * swap_current_directory (struct file *new-dir)
This function sets the current task's current directory to the file handle new-dir then returns the file handle which was previously the task's current directory.
The only times this function will ever fail is when new-dir is not a handle on a directory. Note that setting the current directory to a null pointer is perfectly acceptable -- it just means that the task does not have a current directory. From this it follows that if this function returns a null pointer it is not necessarily an error.
After calling this function all responsibility for the file handle new-dir passes to the filing system -- therefore it should not be closed. Also, responsibility for the file handle returned by this function (if it's not a null pointer) passes to the caller.
The next two functions allow the creation and deletion of directories.
fs Function: bool make_directory (const char *name, u_long attr)
This function creates a new directory called name, its attribute
field is set to attr bitwise-OR'd with the value
ATTR_DIRECTORY
.
This function returns TRUE
if it succeeds, FALSE
otherwise and errno
is set to a suitable error code.
fs Function: bool remove_directory (const char *name)
This function removes the directory entry called name which should be a directory, not a file.
This will only succeed if the directory to be deleted is empty except for the two special links `.' and `..'.
If it does succeed this function returns TRUE
, otherwise
errno
is set accordingly and the function returns FALSE
.
Our filing system allows the use of different types of device very easily. As long as there is some way of mapping between fixed-size blocks and the index numbers of these blocks and the device's physical media it is possible to interface the device with the filing system.
Each device in the system must be represented by an instance of the following structure (defined in `<vmm/fs.h>'):
struct fs_device { /* The name of the device, used in path names (i.e. `DEV:foo/bar') */ const char *name; /* These return >=0 on success, or an E_?? value on error. If no disk is present they should return E_NODISK. These functions may put the current task to sleep if they want to. Note that BLOCK is in FS_BLKSIZ sized blocks. */ long (*read_block)(void *user_data, blkno block, void *buf); long (*write_block)(void *user_data, blkno block, void *buf); /* Devices with removable media (i.e. floppys) should define this function, when called it returns E_NODISK if no disk is in the drive, E_DISKCHANGE if a new disk was inserted since it was last called, or 0 if nothing changed. Note that this function may *not* sleep. */ long (*test_media)(void *user_data); /* Each time one of the above functions is called the following pointer is passed to it as it's first argument. This allows device driver functions to receive a reference to some internal data structure. */ void *user_data; /* Filesystem private data follows... */
These structures are allocated dynamically, the function to call to
receive a new struct fs_device
is called alloc_device
.
After filling in the fields of the newly allocated structure to
suitable values the function add_device
should be called to add
the new device to the filing system's list of available devices.
fs Function: struct fs_device * alloc_device (void)
Finds an unused device structure, marks it as being used and returns a pointer to it. If it is unable to allocate a new structure a null pointer is returned.
fs Function: bool add_device (struct fs_device *dev)
This function adds the newly-allocate and filled in device structure dev to the list of available devices maintained by the filing system.
When this function succeeds it returns TRUE
, otherwise it
returns FALSE
.
fs Function: bool remove_device (struct fs_device *dev)
This function removes the device dev from the filing system's list of available devices. When no outstanding references to dev exist it will be deallocated.
When this function succeeds it returns TRUE
, otherwise it
returns FALSE
.
fs Function: struct fs_device * get_device (const char *name)
This function attempts to obtain a pointer to the device called
name. If such a device exists its reference counter is
incremented and a pointer to it returned. When the caller has finished
with the returned pointer they should use the release_device
function to remove themselves from the device's reference count.
If no device called name exists a null pointer is returned.
fs Function: void release_device (struct fs_device *dev)
This function is used to decrement the reference counter of the device
dev. If no other references exist the device structure will be
deallocated (this can only happen if remove_device
has been
called on dev).
The filesystem's buffer cache is a device-independent method of cacheing the most heavily used blocks in the filing system. It also makes implementing the actual file system itself easier by removing the need for any static buffers.
The filing system implementation itself makes no attempts to optimise the way it accesses blocks in the devices. For example every time a data blocks is needed up to three indirect blocks may also have to be read to find the location of the file's data block. The file-oriented parts of the filing system assume that if a block has to be read more than once in quick succession it will be in the buffer cache after the first read. This makes implementing the filing system a lot cleaner and less prone to bugs.
Currently the buffer cache uses a fixed number of static buffers to cache the blocks and no attempt is made to delay the writing of buffers back to the disk they came from (this last because the system is still being developed and it is desirable that file systems are intact after a system crash). The interface to the buffer cache has been designed with these optimisations in mind, so that it would be possible to dynamically allocate buffers (from the system's available memory) and implement a lazy-write policy with little or no change to the other parts of the filing system.
fs Function: struct buf_head * bread (struct fs_device *dev, blkno block)
This function returns a pointer to a buffer in the buffer cache containing the contents of block number block of the device dev.
If the function fails errno
will be set to a suitable value and
a null pointer will be returned.
If the returned value is not a null pointer it should be applied to
the function brelse
after the caller has finished using the buffer.
fs Function: bool brelse (struct buf_head *buf)
Signals that one of the references to the block in the buffer cache
buf has been finished with. The parameter buf should have
been obtained by previously calling bread
.
fs Function: bool bwrite (struct fs_device *dev, blkno block, const void *buf)
This function writes the block block to the device dev using the 1024 bytes stored at buf. The write will be performed through the buffer cache.
The advantage of using this function over the alternative of using
bread
to read the block, updating its data and using
bdirty
to write it back is that it removes the need to read the
block.
When this function succeeds it returns TRUE
, otherwise it sets
errno
suitably and returns FALSE
.
fs Function: bool bdirty (struct buf_head *buf, bool write-now)
This function signals to the buffer cache that the contents of the buffer in the buffer cache buf have been altered by the caller.
If the write-now parameter is non-zero the block will immediately be written back to the device it came from.
When the function succeeds it returns the value TRUE
, otherwise
errno
is set and FALSE
is returned.
As well as providing the primitive operations for file handling the file system module also exports some higher-level functions to aid in the use of files.
fs Function: int putc (u_char c, struct file *file)
This function writes one character, the character c, to the file pointed to by the file handle file.
This returns a positive value if the function succeeded, or a negative error value if it fails.
fs Function: int getc (struct file *file)
This function reads the next character from the file file and
returns it. If the end of the file is reached the value EOF
is
returned, if an error occurs a negative error code is returned.
fs Function: char * read_line (char *buf, size_t length, struct file *file)
This function tries to read a single line of text from the file handle file into the buffer buf. The parameter length defines the size of the buffer; no more than this number of characters (including the terminating zero byte) will be placed in the buffer.
If characters are read into the buffer buf will be returned, otherwise a null pointer will be returned.
fs Function: int write_string (const char *str, struct file *file)
This function writes the zero-terminated string pointed to by str to the file handle file.
Returns either the number of characters actually written or a negative error code.
fs Function: int fvprintf (struct file *file, const char *fmt, va_list args)
This function is a version of the standard C function vprintf
which writes its output to the file file and returns either the
number of characters written or a negative error code.
fs Function: int fprintf (struct file *file, const char *fmt, ...)
This function is similar to the above documented fvprintf
except it takes the arguments to the format specification on the stack.
Go to the previous, next section.