Programmers often have a need for a unique identifier for various reasons. Sometimes people end up using databases for a simple reliable counter, when there’s no other need for a database.
This is overkill. There’s no need to depend on something like Postgres just because we need a simple counter. But implementing a reliable counter can be a daunting task. Particularly if there is more than one application using the same counter.
This is a service that could very well be implemented in the operating system. When uniqueness and perhaps order is all that’s required, it’s perfectly all right for application
bar to use the same counter. All it means, is that when
foo requests a new value, the counter may have been incremented by
We can call this a non-decreasing counter. For an individual application
foo, the results could be 1, 2, and 5; when application
bar has 3, 4 and 6.
Here we present a simple software driver that creates a device that can be opened and read like a regular file, but each read results in a new value from the counter. We shall call it
dev$seq$ so that it will be unlikely to conflict with regular file names.
As a proof of concept this driver lacks certain features that are required in a real world application. First, it’s only 16 bits so it’ll wrap around at 65,535 and become zero. Second, there’s no way to save its value to the file system; it always starts at zero upon every reboot. Third, there’s no backup procedure that can be applied.
Before we dive into the details of OS/2 device drivers, we shall look at a simple example application that retrieves the value and prints it on-screen.
We include the sequence definition.
This header file defines the type of the sequence counter to be the same size for both 16bit and 32bit applications. The type name is
APIRET rc = 0;,
HFILE devseq = 0L; and
unsigned long action = 0L; we open the device.
rc = DosOpen( "dev$seq$", &devseq, &action, 0L, FILE_NORMAL, OPEN_ACTION_OPEN_IF_EXISTS, OPEN_ACCESS_READONLY | OPEN_SHARE_DENYNONE, NULL );
sequence seq = 0; and
unsigned long size = 0L; we then read from the
devseq handle with
rc = DosRead( devseq, &seq, sizeof seq, &size );
and print the result.
printf( "Sequence: %hu\n", seq );
It is only in the
printf() function call that we explicitly need to know the size of the
More About DosOpen()
The first four parameters are basically self explanatory, the name, file handle, action and size are not covered here; see the Control Program Programming Guide, or EDM/2.
FILE_NORMAL attribute applies only if the file is created, but we have to specify something here so we use this constant. It’s entirely equivalent to just put
OPEN_ACTION_OPEN_IF_EXISTS to make sure we don’t accidentally create a new file. It’s only if the driver is loaded that we want the
DosOpen() to succeed. If someone creates a file called
dev$seq$ however, there’s a chance we might read it by mistake.
OPEN_ACCESS_READONLY to open the file handle in read only mode. This can be considered the default. We then use
OPEN_SHARE_DENYNONE so other processes can read from the sequence generator at the same time as well.
Finally, we set the extended attributes to
NULL because we’re not creating a new file.
The above code snippets have no error handling to make them easier to read and understand. However, the example code has simple error checking in place to be more like production code.
The Device Driver
sequence driver is based on the OpenWatcom physical device driver sample and is therefore a mixture of coding standards, that of the OpenWatcom and the author’s.
The Sequence Type
It is now time to introduce the type we use for the
sequence. It is a simple 16bit unsigned integer. We use the standard
uint16_t to define it. This makes sure it’s the same for 16bit code, such as the OS/2 driver, and 32bit code, for applications.
#include <sys/types.h> typedef uint16_t sequence;
At loading time, the sequence driver is initialized with a function we call
StratInit(). Its purpose is to set up the hardware, if any, and retrieve the helper routine pointer, finally we set pointers to code and data segments the operating system can discard.
There is no need to initialize anything for the sequence driver itself so the only thing the code dose is to print a message on boot time, and set up the minimum required for any device driver.
char hello = "\r\nSequence Driver 0.1 (c) 2018 Johann 'Myrkraverk' Oskarsson\r\n\n"; unsigned short written = 0; DosWrite( 1, hello, sizeof hello - 1, &written ); DevHlp = rp->in.devhlp; rp->out.finalcs = FP_OFF( &OffFinalCS ); rp->out.finalds = FP_OFF( &OffFinalDS ); rp->header.status |= RPDONE;
First we define the counter.
static sequence seq = 0;
close functions are no-ops because there’s nothing required of them for this proof of concept driver. The only thing we do is to signal the kernel that we’re done with the request.
rp->header.status |= RPDONE;
The read function is where the magic happens. First we make sure our buffer is long enough, then we convert the physical address to a
__far pointer with
DevPhysToVirt, finally we set the read size and write the counter to our buffer while incrementing it.
sequence __far *buffer; DevPhysToVirt( rp->transaddr, rp->count, &buffer ); rp->count = sizeof seq; *buffer = seq++; rp->header.status |= RPDONE;
The above snippet omits the error checking done by the example driver.
Strategy is what puts everything together. It’s the entry point for the device driver and dispatches messages to the right routine.
This is not a tutorial on how to write a device driver, so the
Strategy routine is not shown here. The interested reader is referred to the download.
This proof of concept is enough for demonstration purposes, but lacks some real world considerations. For example, the kernel context is documented not to be preempted, but it says nothing about multiple tasks running at the same time on different CPUs. And the above example does not do any locking to account for that. There may well be other considerations for a real world sequence driver we haven’t covered.
Thank you for reading.