Let's take a look at the general case for the io_open handler—it doesn't always correspond to the client's open() call!
For example, consider the stat() and access() client function calls:
For a stat() client call, we essentially perform the sequence open(), fstat(), and close(). Note that if we actually did that, three messages would be required. For performance reasons, we implement the stat() function as one single combine message:
The _IO_CONNECT_COMBINE_CLOSE message causes the io_open handler to be called. It then implicitly (at the end of processing for the combine message) causes the io_close_ocb handler to be called.
For the access() function, the client's C library will open a connection to the resource manager and perform a stat() call. Then, based on the results of the stat() call, the client's C library access() may perform an optional devctl() to get more information. In any event, because access() opened the device, it must also call close() to close it:
Notice how the access() function opened the pathname/device—it sent it an _IO_CONNECT_COMBINE message along with the _IO_STAT message. This creates an OCB (when the io_open handler is called), locks the associated attribute structure (via io_lock_ocb), performs the stat (io_stat), and then unlocks the attributes structure (io_unlock_ocb). Note that we don't implicitly close the OCB—this is left for a later, explicit, message. Contrast this handling with that of the plain stat() above.