Last Updated: (6-Feb-2002) for MacOS X 10.2 and later.
This document describes a new feature in Mac OS X 10.1 called USB Device Arbitration. It is aimed at developers writing user-mode USB drivers and applications for those drivers (of which Classic is a notable example). The basic principles described herein also apply to kernel drivers although some of the details of the implementation
differ (for example, use of the ::message()
method instead of General Interest Notification.)
The Mac OS 8.x/9.x USB stack was implemented with the expectation that it had exclusive access to the USB bus and the devices connected to it. As a result, any application that wanted to communicate with any USB device would have to go through that API. This same code runs inside Classic on top of Mac OS X, but that assumption is no longer true: the Mac OS X kernel now "owns" the hardware and non-Classic applications can communicate with USB devices without going through the Classic USB API.
One consequence of this is that Classic will acquire all USB devices for which it might have a driver as soon as it sees them. It acquires them by calling USBDeviceOpen()
and USBInterfaceOpen()
which in turn prevents other user-mode applications from opening them. If some other application tries to open a device/interface that Classic has
opened, it will get a kIOReturnExclusiveAccess
error and will not be allowed to communicate
with the device.
This causes problems for developers trying to develop Mac OS X versions of their drivers and applications.
What devices will be acquired by Classic? Any device that fits any of the following criteria (or has an interface that fits any of the following criteria):
Attribute | Value | Comment |
---|---|---|
deviceClass | 255 | vendor-specific |
deviceClass interfaceClass |
0 16 |
composite device with an interface of class 16 |
interfaceClass | 255 | vendor-specific |
interfaceClass | 7 | printer |
interfaceClass interfaceSubclass |
0 0 |
deprecated vendor-specific |
The only reasonable workarounds in Mac OS X 10.0.x are to not run Classic if you are attempting to access one of these kinds of devices from a non-Classic application, or to launch your non-Classic application before launching Classic. Shutting down Classic will also cause it to release its hold on these devices.
There are two mechanisms for USB device arbitration that will be available starting in the 10.1 release of Mac OS X.
The first mechanism is more static and is intended for devices where you would like to prevent Classic from ever opening the device at all, or for forcing Classic to open it even though it might not do so otherwise. This mechanism works via a "skeleton" kext that contains no code but has a personality to match each USB device you're interested in.
All it does is cause a
Boolean property ClassicMustNotSeize
or
ClassicMustSeize
to be set to True
(Yes
) on
the IOUSBDevice or IOUSBInterface node in the IORegistry inside the kernel. Classic looks for these properties when
iterating through the devices on the system.
You might be tempted to set the property programmatically, but you need to set it via a kext if you want to be sure to avoid the race condition that will occur between Classic and your code -- Classic might open the device before your code gets a chance to set the property. Another thing to keep in mind is that the IORegistry is never stored persistently and thus any properties you add to a node will not exist after a reboot. An example of this technique is included in the Mac OS X USB DDK.
You can set the property on either the device or, starting with MacOS X 10.2, one of the interfaces on the device that you are interested in. If you set the property on an interface, the entire device will be affected; there is no per-interface granularity; it is simply another way to communicate your intentions for the entire device.
Classic considers
interfaces as well as devices in order to make it easier to write your skeleton kext. If your
device is a Composite device (deviceClass == 0), Mac OS X IOUSBFamily's
Composite Driver will match against your device first and prevent
your 'Seize property from being considered, unless you also set the
OSBundleRequired
property to "Root" so that your kext
loads earlier (see the Inside Macintosh document "Loading Kernel
Extensions at Boot Time"). This is probably not a good use of the OSBundleRequired mechanism. If the property is set on an interface,
your 'Seize property will be added to the registry even if it refers to a
Composite device.
The following is an
example of the kind of Info.plist
text file you might place in your
skeleton kext. You can
edit it manually or use the Property List Editor application. The values for idProduct
, idVendor
, bConfigurationValue
and bInterfaceNumber
are used for matching against an interface; only
idProduct
and idVendor
are used for matching against a device. You may wish to use the
I/O Registry Explorer application, observing the IOService service plane with
your device connected, for help in determining these values.
To test your
skeleton kext, be sure to delete the cache file /System/Library/Extensions.mkext
and then reboot. (Items within square brackets suggest possible values.)
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"> <plist version="0.9"> <dict> <key>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleIdentifier</key> <string>com.apple.driver.test</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundlePackageType</key> <string>KEXT</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>1.5.1</string> <key>IOKitPersonalities</key> <dict> <key>TestSeizeOrUnseizeAnInterface</key> <dict> <key>CFBundleIdentifier</key> <string>com.apple.driver.AppleUSBMergeNub</string> <key>IOClass</key> <string>AppleUSBMergeNub</string> <key>IOProviderClass</key> <string>IOUSBInterface</string> <key>IOProviderMergeProperties</key> <dict> <key>[ClassicMustNotSeize or ClassicMustSeize]</key> <true/> </dict> <key>idProduct</key> <integer>[NumberInDecimal]</integer> <key>idVendor</key> <integer>[NumberInDecimal]</integer> <key>bConfigurationValue</key> <integer>[NumberInDecimal, often 1]</integer> <key>bInterfaceNumber</key> <integer>[NumberInDecimal, often 0]</integer> </dict> </dict> <key>OSBundleLibraries</key> <dict> <key>com.apple.driver.AppleUSBMergeNub</key> <string>1.8.3b1</string> <key>com.apple.iokit.IOUSBFamily</key> <string>1.8</string> </dict> </dict> </plist>
The second mechanism is more dynamic and is intended for devices where you would like Classic and non-Classic applications to be able to take turns accessing the device. This mechanism is more complex than the first mechanism but is also more flexible.
A note about devices
and interfaces: everything in this section applies equally to devices and
interfaces. Open()
stands for any of the USBDeviceOpen()
, USBInterfaceOpen()
, USBDeviceOpenSeize()
, or USBInterfaceOpenSeize()
functions. Similarly, Close()
represents functions USBDeviceClose()
or USBInterfaceClose()
.
Registering for General Interest Notifications is very similar to registering for Service Matching notifications, which you are already doing in order to find devices or interfaces of interest. Here is the prototype for the function you use to do this:
#include <IOKit/IOKitLib.h> #include <IOKit/IOKitKeys.h> kern_return_t IOServiceAddInterestNotification( IONotificationPortRef notifyPort, io_service_t service, const io_name_t interestType, IOServiceInterestCallback callback, void * refCon, io_object_t * notification );
The notifyPort
is the same one you used to register for Service Matching notifications. The service
is the io_service_t
representing the IOUSBDevice or
IOUSBInterface. We'll be using interestType
= kIOGeneralInterest
, which is defined in IOKitKeys.h
. The callback
is your callback
function whose prototype matches the one below. The refCon
will be passed to your callback function each
time it is invoked. (One use for
this, for example, might be a pointer to your record describing the
device or interface.If you do this,
be careful to validate the pointer somehow before using it!) The notification
is an output
parameter that represents the notification. If you want to dispose of the notification (because
you no longer wish to receive these notifications) then invoke IOObjectRelease()
on it.
typedef void (*IOServiceInterestCallback)( void * refcon, io_service_t service, natural_t messageType, void * messageArgument );
The refcon
and the service
are just the ones you supplied when you
registered for notifications. The messageType
and messageArgument
is one of the ones listed below, and described
in detail in what follows.
#include <IOKit/Message.h> kIOMessageServiceIsAttemptingOpen kIOMessageServiceIsRequestingClose // (kernel only) kIOMessageServiceWasClosed
USBDeviceOpen()/USBInterfaceOpen() -> AttemptingOpen(0)
When someone
(including your own code) calls Open()
, your callback will be called with messageType == kIOMessageServiceIsAttemptingOpen
and messageArgument
== 0
. Classic listens for this message in order to be a "good
citizen" but other applications may want to release devices only if the
extra argument is non-zero, in response to the more serious USBDeviceOpenSeize()
or USBInterfaceOpenSeize()
. Before releasing a device in response to this message Classic first
checks an activity timer which it keeps for each device. If no I/O has been done to or from the
device in the last five seconds, then Classic will release the device. From inside Classic it appears as
though the device had been unplugged.
USBDeviceOpenSeize()/USBInterfaceOpenSeize() -> AttemptingOpen(1)
This works just the
same as Open()
, except the kIOMessageServiceIsRequestingClose
message gets sent to whatever kernel driver is
holding the IOUSBDevice or IOUSBInterface object open instead of the kIOMessageServiceIsAttemptingOpen
message. Outside the kernel, a user-mode application which has registered for
General Interest Notifications on this IOUSBDevice or IOUSBInterface will only receive
a callback with messageType ==
kIOMessageServiceIsAttemptingOpen
and
messageArgument != 0
. The way to distinguish whether Open()
or OpenSeize()
was called is to
check the messageArgument
. We use this distinction in order to distinguish
between two levels of seriousness. A casual request to open a device or interface should be
treated differently than a seize request. You should use the ...Seize()
variation of these calls only
when you're sure you want the application that's holding the device/interface
open to release it as soon as possible, without regard for any ongoing
I/O. If your kernel driver
receives a kIOMessageServiceIsRequestingClose
message then it should close the
IOUSBDevice or IOUSBInterface as soon as possible (if possible). The same is true for a user-mode driver
that receives a General Interest Notification callback with messageType == kIOMessageServiceIsAttemptingOpen
and messageArgument
!= 0
.
USBDeviceClose()/USBInterfaceClose() -> WasClosed
When the
IOUSBDevice or IOUSBInterface is closed, your callback will be called with the kIOMessageServiceWasClosed
message. If you recently got an kIOReturnExclusiveAccess
error this is your signal to retry the Open()
that failed. For example, Classic
listens for this message after releasing a device (in respond to an kIOMessageServiceIsAttemptingOpen
message) to know when to attempt to reacquire
the device. When it reacquires the
device it simulates a hot plug so that from inside Classic it appears that the
device has been plugged in.
Warning: be careful to
ignore messages that you caused to be sent. Set a flag or increment a counter when you do open or
close operations. That way you'll know when
the next AttemptingOpen
or WasClosed
message arrives that you were the one who
caused it to be sent. Otherwise
you could go into an infinite loop arbitrating access to the device between
yourself and yourself.
Consider a vendor-specific
USB device that has been noticed by Classic. And imagine that you have a user-mode application or driver
that would like to access the device from outside Classic. The first time this entity attempts to call USBDeviceOpen
()
(similarly, USBDeviceOpenSeize()
, USBInterfaceOpen()
, or USBInterfaceOpenSeize()
) it will get a kIOReturnExclusiveAccess
error. Shortly thereafter, Classic
will call USBDeviceClose()
on the device. To applications running inside Classic, the device will
appear to have been unplugged. At
this point, anyone can attempt the USBDeviceOpen()
again and it ought to succeed. And when you are done using the device
and you USBDeviceClose()
it, Classic reopens it. It then reappears to applications
running inside Classic, just as if it had been hot-plugged.
This should provide a workaround for existing Mac OS X USB software: the user can simply retry the failed operation without needing to quit Classic.
The most complete way
to handle arbitration is to register for general interest notifications and
handle kIOMessageServiceIsAttemptingOpen
, and kIOMessageServiceWasClosed
messages as described above.
In some situations, the following simplified mechanism might suffice:
for (sleepCount = 5; sleepCount > 0; sleepCount--) { // Attempt to open the device. err = (*device)->DeviceOpen(device); if ( kIOReturnExclusiveAccess == err ) { // Someone else has the device open. // But they might release it in a few seconds. // So sleep for a second and try again. // Update user interface to warn the user that // you're waiting for the device. sleep(1); continue; } else if ( kIOReturnSuccess != err ) { // Unexpected error. Break out of the loop. break; } else { // Success. Break out of the loop. break; } } // Checkhere to see whether you got access or not.