OS/2 Sequence Driver

Introduction

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 foo and bar to use the same counter. All it means, is that when foo requests a new value, the counter may have been incremented by bar.

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.

Concept

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.

Example Application

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.

#include "devseq.h"

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

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

Given 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 sequence.

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.

The 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 0 instead.

We use 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.

We use 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.

Error Handling

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

The following 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.

devseq.h

#include <sys/types.h>

typedef uint16_t sequence;

Initialization

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.

stratini.c

    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;

Operations

strategy.c

First we define the counter.

static sequence seq = 0;

The open and 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.

Finally, the 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.

Final Words

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.

Download

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: