The Möbius Operating System: Driver Book
HOME DOWNLOAD DOCUMENTATION SCREENSHOTS  

Example: CD File System

Back: PS/2 Mouse | Next: Driver Book

DrvInit

As with any driver, execution starts at the DrvInit function:

bool DrvInit(driver_t *drv)
{
    drv->add_device = NULL;
    drv->mount_fs = CdfsMountFs;
    return true;
}

DrvInit gets called when the driver is loaded. cdfs is a file system driver, not a device driver, so it sets mount_fs to point to its CdfsMountFs function, and returns.

CdfsMountFs

All file system devices are added via a call to FsMount. In most cases, monitor can automatically mount a file system when a storage device is added. Although it tries to be intelligent about which drivers it picks (for example, it knows the difference between CD file systems and hard disk file systems), it still uses a brute force approach to choose the right driver. So your mount_fs function needs to be careful to validate the device it has been given.

The first think CdfsMountFs does is allocate its fsd_t structure. It uses the cdfs_t type for this, the first field of which is of type fsd_t:

struct cdfs_t
{
    fsd_t fsd;
    device_t *device;
    cdnode_t *nodes[128];
};

...

cdfs = malloc(sizeof(*cdfs));
if (cdfs == NULL)
    goto error0;

memset(cdfs, 0, sizeof(*cdfs));
cdfs->fsd.vtbl = &cdfs_vtbl;

Note that it assigns to the cdfs->fsd.vtbl member. This is similar to the virtual function table used for device drivers:

static const vtbl_fsd_t cdfs_vtbl =
{
    CdfsDismount,
    CdfsGetFsInfo,
    CdfsParseElement,
    CdfsCreateFile,
    CdfsLookupFile,
    CdfsGetFileInfo,
    CdfsSetFileInfo,
    CdfsFreeCookie,
    CdfsReadWriteFile,
    CdfsIoctlFile,
    CdfsPassthrough,
    CdfsMkDir,
    CdfsOpenDir,
    CdfsReadDir,
    CdfsFreeDirCookie,
    CdfsFinishIo,
    CdfsFlushCache,
};

cdfs implements all the functions, although some of the ones which don't make sense for CDs (such as CdfsSetFileInfo) return EACCESS, to denote that the file system is read-only.

The next thing CdfsMountFs does is attempt to open the device passed to it:

cdfs->device = IoOpenDevice(dest);
if (cdfs->device == NULL)
{
    wprintf(L"cdfs(%s): failed to open device\n", dest);
    goto error1;
}

The dest parameter passed by the kernel to mount_fs is an arbitrary string. cdfs passes it straight to IoOpenDevice, so it must be the name of a device relative to the /System/Devices directory; for example, Classes/cdrom0. Note the cascaded error handling using goto; each successive errorN: label performs one more level of cleanup, with error0: doing no cleanup at all, then returning NULL from the function. This is just a stylistic point, and (in my opinion) a good use of the goto keyword.

Assuming the device was opened successfully, CdfsMountFs attempts to read the Primary Volume Descriptor from the first 16 sectors of the CD:

if (IoReadSync(cdfs->device, 16 * SECTOR_SIZE, &pvd, sizeof(pvd)) < sizeof(pvd))
{
    wprintf(L"cdfs(%s): failed to read PVD\n", dest);
    goto error1;
}

Next it copies the volume ID out of the PVD and tells you about it:

for (ch = pvd.volume_id; 
    *ch != ' ' && ch < pvd.volume_id + _countof(pvd.volume_id) - 1; 
    ch++)
    ;

*ch = '\0';
wprintf(L"CdfsMountFs: mounted '%S' from %s\n", pvd.volume_id, dest);

...and allocates a vnode for the root directory. This occupies the predefined VNODE_ROOT ID, equal to 1:

root_id = CdfsAllocNode(cdfs, VNODE_NONE, 1, &pvd.root.entry, L"");
assert(root_id == VNODE_ROOT);
return &cdfs->fsd;

CdfsAllocNode

static vnode_id_t CdfsAllocNode(cdfs_t *cdfs, 
                                vnode_id_t parent, 
                                unsigned index_within_parent, 
                                const entry_t *entry,
                                const wchar_t *name)
{

CdfsAllocNode is used to alloc vnode IDs for files and directories on the CD. On a Unix-like file system, inodes in the FS can map directly to vnodes in the FSD, as long as VNODE_ROOT is made to look like the root directory. However, CDs do not have a table of inodes, so cdfs must build a table in memory as it encounters each file.

First a unique vnode ID is generated:

id = CdfsGenerateNodeId(cdfs, parent, index_within_parent);

CdfsGenerateNodeId uses a combination of the index of the file's entry within its parent's directory listing, and the number of the first sector of the parent directory, to create a unique 32-bit ID for each file.

Having got a vnode ID for the file, CdfsAllocNode checks whether it has already seen the file. If so, it uses the existing record:

cdnode = CdfsGetNode(cdfs, id);
if (cdnode != NULL)
{
    CdfsReleaseNode(cdfs, cdnode);
    return id;
}

Otherwise, it needs to allocate a new record:

cdnode = malloc(sizeof(*cdnode));
if (cdnode == NULL)
    return VNODE_NONE;

...and fill it in with the information given. The entry variable, of type entry_t*, points to the file's directory entry on the CD:

cdnode->id = id;
cdnode->locks = 0;
cdnode->parent = parent;
cdnode->entry = *entry;
cdnode->name = _wcsdup(name);
cdnode->directory = NULL;

cdfs maintains a hash table of nodes (files) for each file system:

cdnode->next = cdfs->nodes[cdnode->id % _countof(cdfs->nodes)];
cdfs->nodes[id % _countof(cdfs->nodes)] = cdnode;

Directories on the CD are treated like files, which means that their contents get cached automatically by the kernel:

if (cdnode->entry.flags & DIR_FLAG_DIRECTORY)
{
    vnode_t vnode_this = { &cdfs->fsd, id };

    cdnode->directory = FsOpen(&vnode_this, L"/", FILE_READ);
    if (cdnode->directory == NULL)
        wprintf(L"CdfsAllocVnode: failed to open %x as a directory\n", id);
}

Finally, the new vnode ID is returned to the caller:

return id;

CdfsGetNode and CdfsReleaseNode

CdfsGetNode turns a vnode ID into a more useful cdnode_t record. It performs a simple hash table lookup, and acquires a lock on the record:

static cdnode_t *CdfsGetNode(cdfs_t *cdfs, vnode_id_t id)
{
    cdnode_t *cdnode;

    cdnode = cdfs->nodes[id % _countof(cdfs->nodes)];
    while (cdnode != NULL)
    {
        if (cdnode->id == id)
        {
            KeAtomicInc(&cdnode->locks);
            return cdnode;
        }

        cdnode = cdnode->next;
    }

    return NULL;
}

CdfsReleaseNode releases the lock held by CdfsGetNode:

static void CdfsReleaseNode(cdfs_t *cdfs, cdnode_t *cdnode)
{
    KeAtomicDec(&cdnode->locks);
}

CdfsDismount

This is the first of the virtual functions referenced in the virtual function table. It should deallocate all the memory allocated to the file system.

CdfsGetFsInfo

This function reports the correct block size (the size of a CD sector) to the kernel, and reports no free space. It should probably report the total size of the CD as the total space. Note that by returning a non-zero number through cache_block_size, cdfs is requesting that the kernel caches its data automatically.

void CdfsGetFsInfo(fsd_t *fsd, fs_info_t *info)
{
    cdfs_t *cdfs;

    cdfs = (cdfs_t*) fsd;
    if (info->flags & FS_INFO_CACHE_BLOCK_SIZE)
        info->cache_block_size = SECTOR_SIZE;
    if (info->flags & FS_INFO_SPACE_TOTAL)
        info->space_total = 0;
    if (info->flags & FS_INFO_SPACE_FREE)
        info->space_free = 0;
}

CdfsParseElement

status_t CdfsParseElement(fsd_t *fsd, const wchar_t *name, 
                          wchar_t **new_path, vnode_t *node)
{
    cdfs_t *cdfs;
    cdfs = (cdfs_t*) fsd;

This is where the work begins. The first task is to look up the parent directory:

cdnode = CdfsGetNode(cdfs, node->id);
if (cdnode == NULL)
{
    wprintf(L"CdfsParseElement(%x/%s): invalid parent node\n",
        node->id, name);
    return ENOTFOUND;
}

This should only fail in the case of a bug in the kernel or in cdfs.

Next CdfsParseElement checks that the parent directory really is a directory:

if (cdnode->directory == NULL)
{
    wprintf(L"CdfsParseElement(%x/%s): parent is not a directory\n", 
        node->id, name);
    return ENOTADIR;
}

Now CdfsParseElement knows that it has a directory, and it has a pointer to its cdnode_t record. For directories, this contains a pointer to a normal file containing the directory entres. CdfsParseElement can look up the entry it needs within the directory using the normal FsRead function:

offset = 0;
sector = 0;
index = 0;
while (true)
{
    if (!FsRead(cdnode->directory, 
        &entry, 
        offset, 
        offsetof(entry_t, id), 
        &bytes_read))
        return errno;
        
    if (bytes_read < offsetof(entry_t, id) ||
        entry.struct_size == 0)
        break;
        
    assert(entry.id_length < _countof(entry_name));

Note that, where the user-mode file system functions take a handle_t parameter, the kernel-mode implementations take a file_t*. A handle_t is specific to one process, and this code could be called from any process. In any case, because drivers are trusted by the kernel, they are given direct access to kernel objects, such as files. Note also the check on entry.struct_size: each directory entry contains the size of the entry itself. An entry which reported itself as zero-length would result in an infinite loop here otherwise. The assert here is to guard against a buffer overflow.

The . and .. entries are ignored; they are synthesised higher up, by the kernel's path parsing functions:

if (index >= 2)
{

The entry name is read:

if (!FsRead(cdnode->directory, 
    entry_name,
    offset + offsetof(entry_t, id), 
    min(_countof(entry_name), entry.id_length),
    &bytes_read))
    return errno;

if (bytes_read < min(_countof(entry_name) - 1, entry.id_length))
    break;

...and each character is widened. This assumes that the character set in use is ISO-Latin-1. Unicode filenames require extensions to the CD file system format. The file name is converted to lower case. File names lookups in Möbius are case-insensitive anyway, but lower-case file names look nicer.

for (i = 0; entry_name[i] != ';' && i < entry.id_length; i++)
    entry_name_wide[i] = (wchar_t) (unsigned char) tolower(entry_name[i]);

entry_name_wide[i] = '\0';

Finally, the entry's name is compared against the one being asked for:

if (_wcsicmp(entry_name_wide, name) == 0)
{

If found, CdfsParseElement has enough information to find the entry's vnode ID, which it gets from CdfsAllocNode. Remember that CdfsAllocNode returns a cached record if available, or a new one if not:

node->id = CdfsAllocNode(cdfs, node->id, index, &entry, entry_name_wide);

As part of the cleanup, the parent directory's node is unlocked:

CdfsReleaseNode(cdfs, cdnode);

Now the search has finished:

return 0;

If this isn't the right entry, CdfsParseElement advances to the next entry, and, if necessary, the next sector:

offset += entry.struct_size;
if (offset / SECTOR_SIZE != sector)
{
    assert(offset / SECTOR_SIZE < sector + 1);
    sector++;
    offset = sector * SECTOR_SIZE;
}

index++;

If the loop comes to an end, i.e. if all the directory entries have been looked at without finding the right one, CdfsParseElement unlocks the parent directory and returns an error to the caller:

CdfsReleaseNode(cdfs, cdnode);
return ENOTFOUND;

CdfsLookupFile

status_t CdfsLookupFile(fsd_t *fsd, vnode_id_t node, uint32_t open_flags, 
                        void **cookie)
{

This function is called to turn a vnode ID into an actual file. cdfs does this using CdfsGetNode; the cdnode_t structure is used as the cookie:

cdnode = CdfsGetNode(cdfs, node);
if (cdnode == NULL)
{
    wprintf(L"CdfsLookupFile(%x): not found\n",
        node);
    return EINVALID;
}

*cookie = cdnode;
return 0;

CdfsGetFileInfo

status_t CdfsGetFileInfo(fsd_t *fsd, void *cookie, uint32_t type, void *buf)
{

This function is used in, among other things, directory listings. cdfs supports the basic information types: FILE_QUERY_NONE can be used to test whether a file exists without returning any information:

switch (type)
{
case FILE_QUERY_NONE:
    return 0;

FILE_QUERY_DIRENT returns the original directory entry:

case FILE_QUERY_DIRENT:
    di->dirent.vnode = cdnode->id;
    wcsncpy(di->dirent.name, cdnode->name, _countof(di->dirent.name));
    return 0;

FILE_QUERY_STANDARD returns some more useful information. Note the use of FsGuessMimeType to map a file extension to a MIME type. This function should be used by all file systems that don't store MIME types natively.

case FILE_QUERY_STANDARD:
    di->standard.length = cdnode->entry.length.native;

    di->standard.attributes = 0;
    if (cdnode->entry.flags & DIR_FLAG_DIRECTORY)
        di->standard.attributes |= FILE_ATTR_DIRECTORY;
    if (cdnode->entry.flags & DIR_FLAG_HIDDEN)
        di->standard.attributes |= FILE_ATTR_HIDDEN;

    FsGuessMimeType(wcsrchr(cdnode->name, '.'),
        di->standard.mimetype,
        _countof(di->standard.mimetype));
    return 0;

No other information types are supported:

return ENOTIMPL;

CdfsFreeCookie

This function releases a cookie (i.e. a cdnode_t structure) back to the file system:

void CdfsFreeCookie(fsd_t *fsd, void *cookie)
{
    cdfs_t *cdfs;
    cdnode_t *cdnode;

    cdfs = (cdfs_t*) fsd;
    cdnode = cookie;
    CdfsReleaseNode(cdfs, cdnode);
}

CdfsReadWriteFile

status_t CdfsReadWriteFile(fsd_t *fsd, const fs_request_t *req, size_t *bytes)
{

This function is called by the kernel to read from a file. Writing is not supported for CD-ROMs:

if (!req->is_reading)
{
    wprintf(L"CdfsReadWriteFile: read-only\n");
    return EACCESS;
}

Because cdfs is a cached file system, the kernel will only pass block-aligned offsets and lengths:

assert((req->pos & (SECTOR_SIZE - 1)) == 0);
assert((length & (SECTOR_SIZE - 1)) == 0);
assert(length == SECTOR_SIZE);

Now a request_dev_t structure is constructed. This will be passed to the underlying device to carry out the read. Most of the parameters from the req structure can be passed straight through. Note that req->pos, which is an offset relative to the start of the file, is converted to an offset relative to the start of the device:

dev_request = malloc(sizeof(*dev_request));
if (dev_request == NULL)
    return errno;

dev_request->header.code = DEV_READ;
dev_request->params.buffered.pages = req->pages;
dev_request->params.buffered.length = length;
dev_request->params.buffered.offset = 
    cdnode->entry.first_sector.native * SECTOR_SIZE + 
    req->pos;
dev_request->header.param = req->io;

The request is sent to the device:

cb.type = IO_CALLBACK_FSD;
cb.u.fsd = &cdfs->fsd;
if (!IoRequest(&cb, cdfs->device, &dev_request->header))
{
    status_t ret;
    wprintf(L"CdfsReadWriteFile: request failed straight away\n");
    *bytes = dev_request->params.buffered.length;
    ret = dev_request->header.result;
    free(dev_request);
    return ret;
}

...and CdfsReadWriteFile returns. It assumes that all I/O is asynchronous. If the underlying device driver handled the request synchronously, IoRequest would have called CdfsFinishIo anyway, making it look like an asynchronous request that finished really quickly.

return SIOPENDING;

CdfsFinishIo

void CdfsFinishIo(fsd_t *fsd, request_t *req)
{

This function is called when I/O initiated by CdfsReadWriteFile completes. Its only function is to inform the kernel that the read request finished, and free the request structure allocated in CdfsReadWriteFile. Note that the fs_asyncio_t pointer passed to CdfsReadWriteFile was stashed in the param field of the request structure; it is retrieved intact here.

dev_request = (request_dev_t*) req;

if (dev_request->header.result != 0)
    wprintf(L"cdfs: device failure: %d\n", dev_request->header.result);

FsNotifyCompletion(req->param, 
    dev_request->params.buffered.length, 
    dev_request->header.result);
free(req);

CdfsOpenDir

status_t CdfsOpenDir(fsd_t *fsd, vnode_id_t dir, void **dir_cookie)
{

This function works in a similar way to CdfsLookupFile. It converts a vnode ID into a cdnode_t:

cdnode = CdfsGetNode(cdfs, dir);
if (cdnode == NULL)
{
    wprintf(L"CdfsOpenDir: node %x not found\n", dir);
    return ENOTFOUND;
}

if (cdnode->directory == NULL)
{
    wprintf(L"CdfsOpenDir: node %x is not a directory\n", dir);
    CdfsReleaseNode(cdfs, cdnode);
    return ENOTADIR;
}

...then allocates a search structure, which will keep track of the next directory entry to be read by CdfsReadDir:

search = malloc(sizeof(*search));
if (search == NULL)
{
    CdfsReleaseNode(cdfs, cdnode);
    return errno;
}

search->node = cdnode;
search->offset = 0;
search->index = 0;
*dir_cookie = search;
return 0;

CdfsReadDir

status_t CdfsReadDir(fsd_t *fsd, void *dir_cookie, dirent_t *buf)
{

This function reads one entry from a directory opened by CdfsOpenDir. The main complication is converting from the on-disc directory entry into the format the kernel expects. The logic here is similar to that in CdfsLookupFile:

search = dir_cookie;

sector = search->offset / SECTOR_SIZE;
found = false;
while (!found)
{
    if (!FsRead(search->node->directory, 
        &entry, 
        search->offset, 
        offsetof(entry_t, id), 
        &bytes_read))
        return errno;

    if (bytes_read < offsetof(entry_t, id))
        return EHARDWARE;

    if (entry.struct_size == 0)
        return EEOF;

    assert(entry.id_length < _countof(entry_name));

    if (search->index >= 2 && entry.id_length > 0)
    {
        if (!FsRead(search->node->directory, 
            entry_name,
            search->offset + offsetof(entry_t, id), 
            min(_countof(entry_name), entry.id_length),
            &bytes_read))
            return errno;

        if (bytes_read < min(_countof(entry_name) - 1, entry.id_length))
            return EHARDWARE;

        for (i = 0; entry_name[i] != ';' && i < entry.id_length; i++)
            entry_name_wide[i] = (wchar_t) (unsigned char) tolower(entry_name[i]);

        entry_name_wide[i] = '\0';

        buf->vnode = CdfsGenerateNodeId(cdfs, search->node->id, search->index);
        wcsncpy(buf->name, entry_name_wide, _countof(buf->name));
        found = true;
    }

    search->offset += entry.struct_size;
    if (search->offset / SECTOR_SIZE != sector)
    {
        assert(search->offset / SECTOR_SIZE < sector + 1);
        sector++;
        search->offset = sector * SECTOR_SIZE;
    }

    search->index++;
}

return 0;

CdfsFreeDirCookie

This function is called to free a cookie allocated by CdfsOpenDir. It needs to close the directory file and deallocate the cookie:

void CdfsFreeDirCookie(fsd_t *fsd, void *dir_cookie)
{
    cdfs_t *cdfs;
    cdsearch_t *search;

    cdfs = (cdfs_t*) fsd;
    search = dir_cookie;
    CdfsReleaseNode(cdfs, search->node);
    free(search);
}

Back: File System Drivers | Next: Driver Book

Post a comment

From: