Here's the code that addresses all the above points.
We'll go through it step-by-step in the discussion that follows:
/*
* io_read1.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/neutrino.h>
#include <sys/iofunc.h>
// our data string
char *data_string = "Hello, world!\n";
int
io_read (resmgr_context_t *ctp, io_read_t *msg, iofunc_ocb_t *ocb)
{
int sts;
int nbytes;
int nleft;
int off;
int xtype;
struct _xtype_offset *xoffset;
// 1) verify that the device is opened for read
if ((sts = iofunc_read_verify (ctp, msg, ocb, NULL)) != EOK) {
return (sts);
}
// 2) check for and handle an XTYPE override
xtype = msg -> i.xtype & _IO_XTYPE_MASK;
if (xtype == _IO_XTYPE_OFFSET) {
xoffset = (struct _xtype_offset *) (&msg -> i + 1);
off = xoffset -> offset;
} else if (xtype == _IO_XTYPE_NONE) {
off = ocb -> offset;
} else { // unknown, fail it
return (ENOSYS);
}
// 3) how many bytes are left?
nleft = ocb -> attr -> nbytes - off;
// 4) how many bytes can we return to the client?
nbytes = min (nleft, msg -> i.nbytes);
// 5) if returning data, write it to client
if (nbytes) {
MsgReply (ctp -> rcvid, nbytes, data_string + off, nbytes);
// 6) set up POSIX stat() "atime" data
ocb -> attr -> flags |= IOFUNC_ATTR_ATIME |
IOFUNC_ATTR_DIRTY_TIME;
// 7) advance the lseek() index by the number of bytes
// read if not _IO_XTYPE_OFFSET
if (xtype == _IO_XTYPE_NONE) {
ocb -> offset += nbytes;
}
} else {
// 8) not returning data, just unblock client
MsgReply (ctp -> rcvid, EOK, NULL, 0);
}
// 9) indicate we already did the MsgReply to the library
return (_RESMGR_NOREPLY);
}
- Step 1
- Here we ensured that the client's open()
call had in fact specified that the device was to be opened for reading.
If the client opened the device for writing only, and then attempted to
perform a read from it, it would be considered an error.
In that case, the helper function
iofunc_read_verify() would return EBADF,
and not EOK, so we'd return that value to the library,
which would then pass it along to the client.
- Step 2
- Here we checked to see if the client had specified an
xtype-override—a per-message override (e.g., because while
the device had been opened in non-blocking mode, this specifies for
this one request that we'd like blocking behavior).
Note that the blocking aspect of the xtype override can be noted by the
iofunc_read_verify() function's last parameter—since
we're illustrating a very simple example, we just passed in a
NULL indicating that we don't care about this aspect.
More important, however, is to see how particular xtype modifiers
are handled.
An interesting one is the _IO_XTYPE_OFFSET modifier, which,
if present, indicates that the message passed from the client contains an
offset and that the read operation should not modify the
current file position of the file descriptor (this is used by
the function
pread(),
for example).
If the _IO_XTYPE_OFFSET modifier is not present, then the
read operation can go ahead and modify the current file position.
We use the variable xtype to store the xtype that
we received in the message, and the variable off to
represent the current offset that we should be using during processing.
You'll see some additional handling of the _IO_XTYPE_OFFSET
modifier below, in step 7.
If there is a different xtype override than
_IO_XTYPE_OFFSET (and not the no-op one of
_IO_XTYPE_NONE), we fail the request with ENOSYS.
This simply means that we don't know how to handle it, and we therefore
return the error up to the client.
- Steps 3 & 4
- To calculate how many bytes we can actually return to the client,
we perform steps 3 and 4, which figure out how many bytes are available
on the device (by taking the total device size from
ocb->attr->nbytes
and subtracting the current offset into the device).
Once we know how many bytes are left, we take the smaller of that number
and the number of bytes
that the client specified that they wish to read.
For example, we may have seven bytes left, and the client wants to read
only two.
In that case, we can return only two bytes to the client.
Alternatively, if the client wanted 4096 bytes, but we had only seven left,
we could return only seven bytes.
- Step 5
- Now that we've calculated how many bytes we're going to return to the
client, we need to do different things based on whether or not we're
returning data.
If we are returning data, then after the check in step 5, we reply to
the client with the data.
Notice that we use data_string + off to return data starting
at the correct offset (the off is calculated based on
the xtype override).
Also notice the second parameter to
MsgReply()—it's documented as the
status argument, but in this case we're using it to
return the number of bytes.
This is because the implementation of the client's read()
function knows that the return value from its
MsgSendv()
(which is the status argument to
MsgReply(), by the way) is the number of bytes that
were read.
This is a common convention.
- Step 6
- Since we're returning data from the device, we know that the device has been accessed. We set
the IOFUNC_ATTR_ATIME and IOFUNC_ATTR_DIRTY_TIME
bits in the flags member of the attribute structure. This
serves as a reminder to the stat I/O function handler that the access time is
not valid and should be fetched from the system clock before replying. If we
really wanted to, we could have stuffed the current time into the
atime member of the attributes structure and cleared the
IOFUNC_ATTR_DIRTY_TIME flag. But this isn't very efficient,
since we're expecting to get a lot more read() requests from
the client than stat() requests. However, your usage patterns
may dictate otherwise.
Note: So which time does the client see when it finally
does call
stat()? The
iofunc_stat_default() function provided
by the resource manager library will look at the
flags
member of the attribute structure to see if the times are valid (the
atime,
ctime, and
mtime fields). If they are not (as will be the case
after our read I/O function handler has been called that returned data), the
iofunc_stat_default() function will update the
time(s) with the current time. The real value of the time is also updated on
a
close(), as you'd expect.
- Step 7
- Now we advance the lseek() offset by the number of bytes
that we returned to the client, only if we are not processing
the _IO_XTYPE_OFFSET override modifier.
This ensures that, in the non-_IO_XTYPE_OFFSET case, if the client calls lseek() to get the current position,
or (more importantly) when the client calls read() to get the next few
bytes, the offset into the resource is set to the correct value.
In the case of the _IO_XTYPE_OFFSET override, we leave the ocb
version of the offset alone.
- Step 8
- Contrast step 6 with this step.
Here we only unblock the client, we don't perform any other functions.
Notice also that there is no data area specified to the
MsgReply(), because we're not returning data.
- Step 9
- Finally, in step 9, we perform processing that's common regardless of
whether or not we returned data to the client.
Since we've already unblocked the client via the MsgReply(), we certainly
don't want the resource manager library doing that for us, so we tell it that we've
already done that by returning _RESMGR_NOREPLY.