Saturday, June 13, 2009

Pedigree Progress

When I last posted I chatted a bit about CDs and the Pedigree installer. Since then, I have successfully written a driver, and that means Pedigree now installs off a CD without a hitch. Naturally, there's still bugs and minor things that need fixing (such as creating a default user!), but it's at least mostly complete.

Since then though, we've been refactoring the POSIX subsystem a fair bit. Rather than have a lot of POSIX-specific functionality at the kernel level, I've developed a Subsystem abstraction that sits between the kernel and the individual subsystems (POSIX, native, DOS, etc...). As a result of this new abstraction the POSIX subsystem has been significantly changed - it's now far neater, and a lot of sneaky bugs have been fixed.

The biggest test of Pedigree itself so far has been our brand new Apache port. Through attempting to run Apache (over and over again) I've found some pretty big bugs, as well as a fair few incorrectly implemented functions. At the moment I'm working on file locking in order to allow Apache to actually serve a document.

This file locking concept introduces some serious design choices. One of the biggest is whether to implement it generically, and have it available to the kernel, or implement it specifically for the POSIX subsystem. Both have their pros and cons.

Generic Kernel-Wide Interface
Pros:
  • Global - can be accessed and used anywhere in the kernel
  • Cleaner - no subsystem-specific code, #defines, or functions makes the final interface cleaner
  • Can wrap around already-existing objects
Cons:
  • Potentially very slow - locks on ranges of bytes need to be checked on every file operation, as the subsystem doesn't control the lock (subsystems would be able to decide better which file operations to lock)
  • May end up biased towards one subsystem rather than being truly generic

Subsystem-Specific Interface
Pros:
  • Keeps a complicated, and potentially very subsystem-specific, interface out of the kernel
  • Faster as it can use the subsystem's structures and functions when needed
Cons:
  • A real pain to implement - requires changing every file-based operation in the subsystem to use the proper locking mechanism, rather than just replacing the file object
  • Code duplication for locking in other subsystems

Two very plausible interfaces - each with equally valid pros and cons. However, the final decision is made based on the point Code duplication for locking in other subsystems. Why should such a generic concept as file locking be duplicated based on a different subsystem?

There is a compromise to be made here though. Note that I have mentioned the generic interface has a serious problem: "Potentially very slow - locks on ranges of bytes need to be checked on every file operation". This means that on every read, every write, the range of bytes being affected must be checked against every lock that the file has active. In the case of multiple shared locks, this is an O(n) search.

A simple solution exists. Whereas it is a definite deviation from the POSIX definition of a file lock, removing the ability to lock individual ranges of bytes and instead lock the entire file allows housekeeping in the kernel to be kept to a minimum - the lock can be kept as a simple Mutex rather than a list of locked ranges - and only slightly affects runtime speed.

I consider this an appropriate compromise to make, as I believe it is ineffective to lock small regions of files (and I expect that the majority of file locks in POSIX applications are made for the entire file). I also feel that implementing the ranges of bytes properly is merely an introduction of more POSIX functionality into the kernel, which is something we're trying to avoid.

EDIT:

The use of a generic interface means it's also possible to distinguish advisory and mandatory locking in the same object without requiring major changes. For instance, adding advisory locking with the generic interface to the POSIX subsystem was a 10 minute job, as it only involved editing the FileDescriptor object and (naturally) fcntl. In this implementation, mandatory locking is just an additional function call in file I/O functions, rather than multiple calls and checks per function.