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 )
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.
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 )
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.
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;
The possible values for the IOInfo fields depend on the device to which the IOReq is sent. The fields include the following:
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;
CMD_WRITE
, CMD_READ
, and CMD_STATUS
.
IO_QUICK
, which asks that I/O take place immediately (synchronously) without notification. All other bits must be set to 0.
TIMER_UNIT_USEC
unit and the TIMER_UNIT_VBLANK
unit.
io_Info.ioi_User
field of the IOReq structure to retrieve this pointer.
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.
Err SendIO( Item ioReqItem, const IOInfo *ioiP )
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.
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);
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:
The
{
Item msgItem;
Message *msg;
msg = (Message *) LookupItem(msgItem);
ioreq = (Item) msg->msg_DataPtr;
result = (Err) msg->msg_Result;
user = msg->msg_DataSize;
}
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()
.
LookUpItem()
call. For example:
ior = (IOReq *)LookupItem(IOReqItem);
Fields that offer information to the average task are:
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;
io_Info
.ioi_User
. A copy of the ioi_User
field from the IOInfo structure supplied by the task when calling SendIO()
or DoIO()
. This is a convenient location for the task to store context information associated with the IOReq.
io_Actual
. Supplies the size in bytes of the I/O operation just completed. This is a convenient place to find out the size of the last I/O transfer.
io_Error
. Contains any errors generated by the device carrying out the I/O operation. The meaning of this value depends on the device that was used. This value is also returned by DoIO()
or WaitIO()
.
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 )
To close a device, a task uses the CloseNameDevice()
call:
Err CloseNamedDevice( Item device )