What is different in IRP_MJ_READ between an IRP-based and fast I/O based request?
Hmmmm… pretty much what Mr. Widdowson said so very succinctly.
Fast I/O is just an “express processing” callback that asks the file system “Can you completely handle this request right now, or do you want me to build you an IRP so you can handle this at your convenience?”
For just a bit more detail, from the ancient, and classic, article on OSR Online that first explained Fast I/O to the world (this was 1996!):
A fast I/O routine can do one of two things: it can complete the operation, set the IoStatus field to indicate the result codes for the operation and return TRUE to the I/O manager. If that is the case, the I/O manager will complete the I/O operation. Alternatively, the routine can return FALSE, in which case the I/O manager will simply create an IRP and call the standard dispatch entry point.
Note that returning TRUE doesn’t always guarantee the data has been transferred. For example, a read which starts past the end of file causes the IoStatus.Results field to be set to STATUS_END_OF_FILE, with no data copied. A read which crosses over the end of file will cause a TRUE to be returned, again with STATUS_END_OF_FILE set in the Results field, but this time with all remaining data in the file copied to the buffer.
Similarly, returning FALSE doesn’t always guarantee that some data has not been transferred. While less likely, it is possible for some data to be successfully copied but then to experience an I/O error, or to have the memory of the buffer become inaccessible.
In either case a number of secondary effects can occur. For example, while reading from the cache, it is possible that some of the data being read is not currently resident. This will result in a page fault which will result in a call back into the file system to satisfy the page fault.