Go to the previous, next section.

The Filing System

This chapter of the manual documents the system's filing system, and the interface it provides to other modules.

Introduction

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);

File Names

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.

`foo'
`./foo'
The file named `foo' in the current directory.

`foo/bar'
The file named `bar' in the directory called `bar' in the current directory.

`../foo'
The file called `foo' in the parent of the current directory.

`/foo'
The file named `foo' in the system's root directory.

`hda:'
The root directory of the device called `hda' (the first IDE hard disk).

`hda:foo'
The file called `foo' in the root directory of the device called `hda'.

`:foo'
The file named `foo' in the root directory of the device which the current directory is stored on.

Filesystem Structure

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.

File Handling

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.

File Handles

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
Open the file for reading.

F_WRITE
Open the file for writing. Note that combining F_READ and F_WRITE allows both read and write access to the file.

F_CREATE
Create the file if it does not already exist.

F_TRUNCATE
Delete the contents of the file as it is opened.

F_ALLOW_DIR
When this bit is set open does not signal an error if the file being opened is actually a directory.

F_DONT_LINK
Setting this bit prevents the following of symbolic links as the file is opened (see section Symbolic Links).

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.

File Input and Output

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
This causes the file position to be set arg characters from the beginning of the file.

SEEK_REL
Adds arg to the current value of the file position to obtain the new value (note that a negative arg will move backwards in the file).

SEEK_EOF
Sets the file position to be arg characters back from the last character in the file.

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).

Hard Links

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.

Symbolic Links

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.

Other File Operations

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
When this bit is set the file is unreadable.

ATTR_NO_WRITE
When this bit is set the file may not be written to.

ATTR_EXEC
When set this bit allows the file to be executed.

If this function succeeds it will return TRUE, otherwise it will set errno to a suitable value and return FALSE.

Directory Handling

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.

Device Handling

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 Buffer Cache

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.

Library Functions

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.