The Basics

This chapter includes:

The basics

In this chapter, just like with any good cookbook, we'll look at the basic techniques that are used in the recipes. I use a certain programming style, with conventions that I've found comfortable, and more importantly, useful and time-saving over the years. I want to share these conventions with you here so I don't have to describe them in each chapter.

If you're an experienced UNIX and C programmer, you can just skip this chapter, or come back to it if you find something confusing later.

In the beginning…

In the beginning, I'd begin each project by creating a new directory, typing e main.c, and entering the code (e is a custom version of vi that I use). Then it hit me that a lot of the stuff that occurs at the beginning of a new project is always the same. You need:

For projects dealing with a resource manager (introduced in the previous book), other common parts are required:

This resulted in two simple scripts (and associated data files) that create the project for me: mkmain and mkresmgr (see Threaded Resource Managers, below). You invoke them in an empty directory, and they un-tar template files that you can edit to add your own specific functionality.

You'll see this template approach throughout the examples in this book.

The main() function

The first thing that runs is, of course, main(). You'll notice that my main() functions tend to be very short — handle command-line processing (always with optproc()), call a few initialization functions (if required), and then start processing.

In general, I try to deallocate any memory that I've allocated throughout the course of the program. This might seem like overkill because the operating system cleans up your resources when you drop off the end of main() (or call exit()). Why bother to clean up after yourself? Some of the subsystems might find their way into other projects — and they might need to start up and shut down many times during the life of the process. Not being sloppy about the cleanup phase makes my life just that much easier when I reuse code. It also helps when using tools like the debug malloc() library.

Command-line processing — optproc()

The command-line processing function is a bit more interesting. The majority of variables derived from the command line are called opt*, where * is the option letter. You'll often see code like this:

if (optv) {
    // do something when -v is present
}

By convention, the -v option controls verbosity; each -v increments optv. Some code looks at the value of optv to determine how much “stuff” to print; other code just treats optv as a Boolean. The -d option is often used to control debug output.

Command-line processing generally follows the POSIX convention: some options are just flags (like -v) and some have values. Flags are usually declared as int. Valued options are handled in the command-line handler's switch statement, including range checking.

One of the last things in most optproc() handlers is a final sanity check on the command-line options:

The last thing in optproc() is the command-line argument handling. POSIX says that all command-line options come first, followed by the argument list. An initial pass of command-line validation is done right in the switch statement after the getopt() call. Final validation is done after all of the parameters have been scanned from the command-line.

Common globals

There are a few common global variables that I use in most of my projects:

version
This pointer to a character string contains the version string. By convention, the version is always five characters — A.BCDE, with A being the major version number, and BCDE being the build number. The version is the only thing that's stored in the version.c file.
progname
A pointer to a character string containing the name of the program. (This is an old convention of mine; Neutrino now has the __progname variable as well.)
blankname
A pointer to a character string the same length as progname, filled with blank characters (it gets used in multi-line messages).

I strive to have all messages printed from every utility include the progname variable. This is useful if you're running a bunch of utilities and redirecting their output to a common log file or the console.

You'll often see code like this in my programs:

fprintf (stderr, "%s:  error message...\n", progname, ...);

Usage messages

Both QNX 4 and Neutrino let an executable have a built-in usage message — just a short reminder of the command-line options and some notes on what the executable does.

You can try this out right now at a command-line — type use cat to get information about the cat command. You'll see the following:

cat - concatenate and print files (POSIX)

cat [-cu] [file...]
Options:
 -c      Compress, do not display empty lines.
 -u      Unbuffered, for interactive use.
 -n      Print line numbers without restarting.
 -r      Print line numbers restarting count for each file.

You can add these messages into your own executables. My standard mkmain script generates the Makefile and main.use files required for this.


Note: With QNX 4 it was possible to have the usage message embedded within a C source file, typically main.c.

This doesn't work the same way under Neutrino. For example, if the usage message has a line containing an odd number of single-quote characters ('), the compiler gives you grief, even though the offending line is walled-off in an #ifdef section. The solution was to move the usage message out of main.c and into a separate file (usually main.use). To maintain the usage message within the C code, you can get around this by putting the usage message in comments first, and then using the #ifdef for the usage message:

/*
#ifdef __USAGE
%C A utility that's using a single quote
#endif
*/

It's an ANSI C compiler thing. :-)


Threaded resource managers

The resource managers presented in this book are single-threaded. The resource manager part of the code runs with one thread — other threads, not directly related to the resource manager framework, may also be present.

This was done for simplicity, and because the examples presented here don't need multiple resource manager threads because:

  1. the QSSL-supplied resource manager library enforces singled-threaded access to any resource that shares an attributes structure,
  2. all of the I/O and Connect function outcalls are run to completion and do not block, and
  3. simplicity.

Single-threaded access to resources that share an attributes structure means that if two or more client threads attempt to access a resource, only one of the threads can make an outcall at a time. There's a lock in the attributes structure that the QSSL-supplied library acquires before making an outcall, and releases when the call returns.

The outcalls “running to completion” means that every outcall is done “as fast as possible,” without any delay or blocking calls. Even if the resource manager was multi-threaded, and different attributes structures were being accessed, things would not be any faster on a single-processor box (though they might be faster on an SMP box).

A multi-threaded resource manager may be a lot more complex than a single-threaded resource manager. It's worthwhile to consider if you really need a multi-threaded resource manager. The complexity comes from:

  1. handling unblock pulses (see Getting Started with QNX Neutrino),
  2. terminating the resource manager, and
  3. general multi-threaded complexity.

Handling unblock pulses was covered extensively in my previous book — the short story is that the thread that's actively working on the client's request probably won't be the thread that receives the unblock pulse, and the two threads may need to interact in order to abort the in-progress operation. Not impossible to handle, just complicated.

When it comes time to terminate the resource manager, all of the threads need to be synchronized, no new requests are allowed to come in, and there needs to be a graceful shutdown. In a high-availability environment, this can be further complicated by the need to maintain state with a hot standby (see the High Availability chapter).

Finally, multi-threaded designs are generally more complex than single-threaded designs. Don't get me wrong, I love to use threads when appropriate. For performing multiple, concurrent, and independent operations, threads are wonderful. For speeding up performance on an SMP box, threads are great. But having threads for the sake of having threads, and the accompanying synchronization headaches, should be avoided.