Performing I/O


From a task's point of view, the process of I/O between a task and a device can be broken down into four stages:

If a task wants to perform a series of I/O operations on a single device, it only needs to prepare for I/O once. The task can then send and receive IOReqs to and from the device until it is finished.

Preparing for I/O

Before a task can send and receive IOReqs, it must prepare for I/O by opening a device and creating IOReq and IOInfo data structures.

To open a device, a task uses the following function call:

Item OpenNamedDevice( const char *name, void *args )
The name argument is a pointer to a null-terminated string of characters that contains the name of the device. The a argument is a pointer reserved for future use. Currently, this value must always be NULL.

When OpenNamedDevice() executes, it opens the named device and returns the item number of that device. Future I/O calls to the device use the device's item number.

Creating an IOReq

Once a task opens a device, the task must create one or more IOReq items to communicate with the device. Before creating an IOReq, it's important to consider how the task will be notified when the IOReq is returned. There are two options:

Notification by signal is the default method. When the device finishes with an I/O operation and wants to return the IOReq to the task, it sends the system signal SIGF_IODONE to the task. The task then knows there's a completed IOReq, and it can retrieve the IOReq when it wants to.

Notification by message is specified by providing a message port when an IOReq is created. When the device finishes with an I/O operation and wants to return the IOReq, it sends a message to the specified port. The task can read the message to get a pointer to the IOReq and then read or reuse the IOReq when it wants to.

Either notification method has advantages and disadvantages. Notification by signal uses fewer resources, but doesn't identify the returning IOReq. It merely says that an IOReq has returned. Notification by signal is useful for I/O operations that use a single IOReq passed back and forth between the task and a device.

Notification by message uses more resources, but because each message identifies a particular IOReq, the task knows exactly which IOReq is returned when it receives notification. Notification by message is useful for I/O operations that use more than one IOReq.

Once a task opens a device, the task creates an IOReq data structure using this call:

Item CreateIOReq( const char *name, uint8 pri, Item dev, Item mp )
The name argument is a pointer to a null-terminated character string containing the name of this IOReq. If the IOReq is unnamed, this argument should be NULL. The pri argument is a priority value from a low priority of 0 to a high priority of 255. The dev argument is the device number of the opened device to which this IOReq is to be sent.

The mp argument is the item number of a message port where the task is notified of the IOReq's completion. If the task wants to be notified of IOReq completion by message, it must create a message port and supply its item number here. If the task wants only to be notified of IOReq completion by signal, then this argument should be set 0.

When this call executes, the kernel creates an IOReq that is sent to the specified device and returns the item number of the IOReq. All future I/O calls using this IOReq specify it with its item number.

Initializing an IOInfo Structure

Although a task can create an IOReq, it can't directly set values in it because an IOReq is an item. The task instead defines an IOInfo data structure in its own memory, and then, when it requests an I/O operation, the task fills in the appropriate values and passes the IOInfo on to the kernel with the IOReq.

The definitions of an IOInfo data structure and the IOBuf data structure used within it (both defined in io.h) are as follows:

typedef struct IOBuf
{
    void        *iob_Buffer;                  /* ptr to users buffer */
    int32         iob_Len;                    /* len of this buffer, or transfer size*/
} IOBuf;

typedef struct IOInfo
{
    uint8        ioi_Command;                 /* Command to be executed */
    uint8        ioi_Flags;                   /* misc flags */
    uint8        ioi_Unit;                    /* unit of this device */
    uint8        ioi_Flags2;                  /* more flags, should be set to zero */
    uint32        ioi_CmdOptions;             /* device dependent options */
    uint32        ioi_User;                   /* for user use */
    int32        ioi_Offset;                  /* offset into device for transfer to */
                                              /*begin */
    IOBuf        ioi_Send;                    /* copy out information */
    IOBuf        ioi_Recv;                    /* copy in info, (address validated) */
} IOInfo;

The possible values for the IOInfo fields depend on the device to which the IOReq is sent. The fields include the following:

The IOInfo structure should always contain a command value in ioi_Command, whether it's one of the three standard commands-CMD_READ, CMD_WRITE, or CMD_STATUS-or a device-specific command. If the command involves writing data, ioi_Send should be filled in with a write buffer definition; if the command involves reading data, ioi_Recv should be filled in with a read buffer definition. And if the task wants the fastest possible I/O operation with a device that can respond immediately (a timer, for example), ioi_Flags should be set to IO_QUICK. Any other fields should be filled in as appropriate for the operation.

Passing IOInfo Values to the Device

To pass the IOInfo values to the system and send the IOReq, the task can use this call (found in io.h):

Err SendIO( Item ioReqItem, const IOInfo *ioiP )
The ioReqItem argument is the item number of the IOReq to be sent to the device. The ioiP, argument is a pointer to the IOInfo structure that describes the I/O operation to perform.

When the kernel carries out SendIO(), it copies the IOInfo values into the IOReq, then checks the IOReq to be sure that all values are appropriate. If they are, the kernel passes on the I/O request to the device responsible. The device then carries out the request.

SendIO() returns 1 if immediate I/O was used and the IOReq is immediately available to the task. SendIO() returns a 0 if I/O was done asynchronously, which means that the request is being serviced by the device in the background. SendIO() returns a negative error code if there was an error in sending the IOReq. This usually occurs if there were inappropriate values included in the IOInfo structure.

Asynchronous vs. Synchronous I/O

When a task sends an I/O request to a device using SendIO(), the device may or may not satisfy the request immediately. If SendIO() returns 1, it means that the operation is completed and no other actions are expected. If SendIO() returns a 0, it means that the I/O operation has been deferred and is being worked on in the background.

When an operation is deferred, your task is free to continue executing while the I/O is being satisfied. For example, if the CD-ROM device is doing a long seek operation in order to get to a block of data you have asked it to read, you can continue executing the main loop of your task while you wait for the block to be transferred.

When SendIO() returns 0, which means the I/O request is being serviced asynchronously, you must wait for a notification that the I/O operation has completed before you can assume anything about the state of the operation. As shown previously, when you create an IOReq using CreateIOReq(), you can specify one of two types of notification: signal or message. When an asynchronous I/O operation completes, the device sends your task a signal or a message to inform you of the completion.

Once you receive the notification that an I/O operation is complete, you must call WaitIO() to complete the I/O process.

Err WaitIO( Item ioreq )
WaitIO() cleans up any loose ends associated with the I/O process. WaitIO() can also be used when you wish to wait for an I/O operation to complete before proceeding any further. The function puts your task or thread on the wait queue until the specified IOReq has been serviced by the device.

The return value of WaitIO() corresponds to the result code of the whole I/O operation. If the operation fails for some reason, WaitIO() returns a negative error code describing the error.

If you have multiple I/O requests that are outstanding, and you receive a signal telling you an I/O operation is complete, you might need to determine which I/O request is complete. Use the CheckIO() function:

int32 CheckIO( Item ioreq )
CheckIO() returns 0 if the I/O operation is still in progress. It returns greater than 0 if the operation completes; it returns a negative error code if something is wrong.

Do not use CheckIO() to poll the state of an I/O request. You should use WaitIO() if you need to wait for a specific I/O operation to complete, or use WaitSignal() or WaitPort() if you must wait for one of a number of I/O operations to complete.

There are many cases where an I/O operation is very short and fast. In these cases, the overhead of notifying your task when the I/O completes becomes significant. The I/O subsystem provides a quick I/O mechanism to help remove this overhead as much as possible.

Quick I/O occurs whenever SendIO() returns 1. It tells you that the I/O operation is complete, and that no signal or message will be sent to your task. You can request quick I/O by setting the IO_QUICK bit in the ioi_Flags field of the IOInfo structure before you call SendIO(). IO_QUICK is merely a request for quick I/O. It is possible that the system cannot perform the operation immediately. Therefore, check the return value of SendIO() to make sure the I/O was done immediately. If it was not done synchronously, you have to use WaitIO() to wait for the I/O operation to complete.

The fastest and simplest way to do quick I/O is to use the DoIO() function:

Err DoIO( Item ioreq, const IOInfo *ioInfo )
DoIO() works just like SendIO(), except that it guarantees that the I/O operation is complete once it returns. You do not need to call WaitIO() or CheckIO() if you use DoIO() on an IOReq. DoIO() always requests quick I/O, and if the system is unable to do quick I/O, this function automatically waits for the I/O to complete before returning.

Completion Notification

When an I/O operation is performed asynchronously, the device handling the request always sends a notification to the client task when the I/O operation is complete. As mentioned previously, the notification can be either a signal or a message.

When you create IOReq items with signal notification, the responsible devices send your task the SIGF_IODONE signal whenever an I/O operation completes. If you have multiple I/O requests outstanding at the same time, you can use the following to wait for any of the operations to complete.

WaitSignal(SIGF_IODONE);
When WaitSignal() returns, you must call CheckIO() on all of the outstanding I/O requests you have to determine which one is complete. Once you find a completed request, call WaitIO() with that IOReq to mark the end of the I/O operation.

If you created your IOReq structures with message notification, you will receive a message whenever an I/O operation completes. The message is posted to the message port you specified when you created the IOReq. If you have multiple message-based I/O requests outstanding, you could wait for them using:

msgItem = WaitPort(replyPort,0);
WaitPort() puts your task to sleep until any of the IOReqs complete. Once WaitPort() returns, you can look at three fields of the message structure to obtain information about the IOReq that has completed:

{
Item msgItem;
Message *msg;

    msg        = (Message *) LookupItem(msgItem);
    ioreq        = (Item) msg->msg_DataPtr;
    result        = (Err) msg->msg_Result;
    user        = msg->msg_DataSize;
}

The msg_DataPtr field contains the item number of the IOReq that has completed. The msg_Result field contains the result code of the I/O operation. This is the same value that WaitIO() would return for this IOReq. Finally, msg_DataSize contains the value of the ioi_User field from the IOInfo structure used to initiate the I/O operation with SendIO().

Reading an IOReq

Once an IOReq returns to a task, the task can read the IOReq for information about the I/O operation. To read an IOReq, a task must get a pointer to the IOReq using the LookUpItem() call. For example:

ior = (IOReq *)LookupItem(IOReqItem);
Once a task has the address of an IOReq, it can read the values in the different fields of the IOReq. An IOReq data structure is defined as follows:

typedef struct IOReq
{
    ItemNode            io;
    MinNode            io_Link;
    struct            Device   *io_Dev;
    struct            IOReq    *(*io_CallBack)(struct IOReq *iorP);
                                   /* call, do not ReplyMsg */
    IOInfo            io_Info;
    int32            io_Actual;    /* actual size of request completed 
*/
    uint32            io_Flags;       /* internal to device driver */
    int32            io_Error;       /* any errors from request? */
    int32            io_Extension[2];    /* extra space if needed */
    Item            io_MsgItem;
    Item            io_SigItem;
} IOReq;

Fields that offer information to the average task are:

Continuing I/O

Tasks often have a series of I/O operations to carry out with a device. If so, a task can recycle an IOReq once it's been returned; the task supplies new values in an IOInfo data structure, and then uses SendIO() or DoIO() to request a new operation.

A task isn't restricted to a single IOReq for a single device. If it's useful, a task can create two or more IOReqs for a device, and work with one IOReq while others are sent to the device or are awaiting action by the task.

Aborting I/O

If an asynchronous I/O operation must be aborted while in process at the device, the issuing task can use this call (defined in io.h):

Err AbortIO( Item ioreq )
AbortIO() accepts the item number of the IOReq that is responsible for the operation to be aborted. When executed, it notifies the device that the operation should be aborted. When it is safe, the operation is aborted and the device sets the IOReq's Io_Error field to ABORTED. The device then returns the IOReq to the task.

A task should always follow the AbortIO() call with a WaitIO() call so the task will wait for the abort to complete and the IOReq to return.

Finishing I/O

When a task completely finishes I/O with a device, the task should clean up by deleting any IOReqs it created and by closing the device. To delete an IOReq, a task uses this call (found in io.h):

Err DeleteIOReq( Item ioreq )
This call accepts the item number of an IOReq and frees any resources the IOReq used.

To close a device, a task uses the CloseNameDevice() call:

Err CloseNamedDevice( Item device )