Finding the server

Updated: April 19, 2023

The first thing that a client does is call open() to get a file descriptor. Note that if the client calls the higher-level function fopen() instead, the same discussion applies—fopen() eventually calls open().

Inside the C library implementation of open(), a message is constructed, and sent to the process manager (procnto) component. The process manager is responsible for maintaining information about the pathname space. This information consists of a tree structure that contains pathnames and a node descriptor, process ID, channel ID, and handle associations:

Figure 1. QNX Neutrino's namespace.
Note: Note that in the diagram above and in the descriptions that follow, I've used the designation fs-qnx6 as the name of the resource manager that implements the Power-Safe filesystem—in reality, it's a bit more complicated, because the filesystem drivers are based on a series of DLLs that get bundled together. So, there's actually no executable called fs-qnx6; we're just using it as a placeholder for the filesystem component.

Let's say that the client calls open():

fd = open ("/dev/ser1", O_WRONLY);

In the client's C library implementation of open(), a message is constructed and sent to the process manager. This message states, “I want to open /dev/ser1; who should I talk to?”

Figure 2. First stage of name resolution.

The process manager receives the request and looks through its tree structure to see if there's a match (let's assume for now that we need an exact match). Sure enough, the pathname /dev/ser1 matches the request, and the process manager is able to reply to the client: “I found /dev/ser1. It's being handled by node descriptor 0, process ID 44, channel ID 1, handle 1. Send them your request!”

Remember, we're still in the client's open() code!

So, the open() function creates another message, and a connection to the specified node descriptor (0, meaning our node), process ID (44), channel ID (1), stuffing the handle into the message itself. This message is really the “connect” message—it's the message that the client's open() library uses to establish a connection to a resource manager (step 3 in the picture below). When the resource manager gets the connect message, it looks at it and performs validation. For example, you may have tried to open-for-write a resource manager that implements a read-only filesystem, in which case you'd get back an error (in this case, EROFS). In our example, however, the serial port resource manager looks at the request (we specified O_WRONLY; perfectly legal for a serial port) and replies with an EOK (step 4 in the picture below).

Figure 3. The _IO_CONNECT message.

Finally, the client's open() returns to the client with a valid file descriptor.

Really, this file descriptor is the connection ID we just used to send a connect message to the resource manager! Had the resource manager not given us an EOK, we would have passed this error back to the client (via errno and a -1 return from open()). (It's worthwhile to note that the process manager can return the node ID, process ID and channel ID of more than one resource manager in response to a name resolution request. In that case, the client will try each of them in turn until one succeeds, returns an error that's not ENOSYS, ENOENT, or EROFS, or the client exhausts the list, in which case the open() fails. We'll discuss this further when we look at the “before” and “after” flags, later on.)