Copyright © 2004 by Mark Roddy. All rights reserved
The Windows PnP manager uses Device Relations information to gather information about the PnP relationships between a given device node and other device nodes in the system. (In one case the relationship query is generally concerned with only a single device node.) Device relationship queries come in four types: BusRelations, EjectionRelations, RemovalRelations, and TargetDeviceRelations. This article discusses only RemovalRelations, although it also covers general programming techniques for handling relationship queries.
A response to a RemovalRelations query is a declaration by a device node that if it is removed, some other device node will also be removed. Note that this sort of device node relationship is not the same as a bus driver parent device node/child device node relationship. The PnP Manager is informed about parent/child relationships by the BusRelations query, and needs no other information to understand that the parent device node's removal implies the removal of all of its child device nodes. As a consequence of correctly using Removal Relations, Query Remove operations will be applied to all device nodes related to a specific device node through a Removal Relations query, allowing each such device node to allow or deny the remove operations as appropriate.
The correct use of RemovalRelations allows the PnP Manager to avoid inadvertent surprise removal of devices and the associated risk of data loss to applications. When there is a non-device node stack relationship between device objects, without the correct use of Removal Relations the PnP manager cannot make intelligent decisions about the appropriateness of device removal. More explicitly, the correct use of RemovalRelations can prevent unnecessary surprise removal and "system must be rebooted" popups when new devices are added to the system or drivers are updated.
First, never use RemovalRelations to express a parent/child relationship. Instead, RemovalRelations generally are used when a Virtual Device of some sort has been created, and this virtual device has a dependency on some other (virtual or physical) device that is outside of its own PnP device node stack. There are two simple examples that help to illustrate when the use of a RemovalRelations is appropriate.
1. Muxes.
A multiplexer (mux) driver routes IO requests from the top of the mux to the bottom, typically in a 1-N relationship, although other configurations are also possible. The top of a mux driver in NT is typically some sort of virtual device, while the bottom of the mux connects to physical devices.
This can be viewed as follows:
Mux Pattern for RemovalRelations, Simple Version
Note that the IO routing from Virtual Devnode to Physical Devnode is internal to the driver for the Virtual Devnode device.
Where are such devices used in Windows? Consider the relationship between storage volume devices and their associated physical disk partitions.
In the simplest case a volume is associated with a single disk partition. In this case there could be a simple PnP managed devnode-stack that has no need for RemovalRelations. However consider mirrored or spanned/striped volumes. The association is not so simple. A volume that is striped across several disk partitions has the mux pattern of 1-N internal routing. PnP has no concept of mux relationships, and thus it is the responsibility of the storage stack to inform PnP that if it wants to remove a physical disk (and by implication its associated child volume partitions) it also has to make sure that the associated volume devices can accept the removal. Note that in the striped volume case removing any of the physical disks containing a stripe element effectively takes the entire volume offline.
If the volume is mirrored across two or more physical disks, the same need for using RemovalRelations is present, but in a somewhat more complicated fashion. In the case of mirrored volumes removing one physical disk mirror element (disk plex, for short) does not necessarily require removing the volume. It is only the removal of the last operational disk plex that requires PnP to ask the volume device if it also can be removed. Consequently a correctly implemented mirrored volume would dynamically update its RemovalRelations such that PnP would issue the QueryRemove on the volume device only if the number of operational plexes associated with the volume were reduced to one. Note also that a physical disk can contain more than one volume. This leads to further complications as if a physical disk contains the data for more than one volume the removal of that physical disk affects each of the volumes associated with that physical disk. The following diagram illustrates the more complicated picture of a Mux Pattern where the lower mux elements are in turn just 'logical elements' contained by physical devices.
In this diagram the removal of a physical disk affects more than one volume. Correctly handling RemovalRelations in this case must therefore include all associated volumes in the removal relations for each physical disk.
Notice the use of the term "operational plex" above. Consider the case where a mirrored volume is resyncing. One of the disk plexes is the source plex for the resync operation, the other plex is the target of the resync. (In fact a volume could consist of more than two plexes, but for this discussion just consider the normal two-plex case.) The source disk plex is operational, the target disk plex is not operational. The non-operational disk plex can always be removed without changing the state of its hosted volumes. The operational disk plex cannot be removed without changing the state of its hosted volumes, unless there is at least one other operational disk plex associated with all of these volumes. Operational state is not just the state of the data on the disk (valid/invalid), it is also the PnP state of the disk. A disk plex is not operational if it has not been started or is currently removed or stopped. (Complications arise if an operational plex has accepted a QueryRemove or Query Stop request, as the plex is more or less "pending non-operational", but that state can transition back to operational or to non-operational.)
2. Virtual peer device.
A virtual peer device provides a separate protocol stream for a single physical device. For example a single USB physical port could support both USB2.0 and USB1.1devices, and the implementation could construct separate device node stacks for each protocol. Removing the physical port however results in both device node stacks being removed. Consequently the peer device pattern applies, as the removal any one peer implies the removal of the other. Unlike the Mux pattern, other than the RemovalRelations there does not have to be any other relationship between the peer device nodes.
Virtual Peer Pattern for RemovalRelations
In both the mux pattern and the virtual peer pattern it is much simpler for the driver to implement removal relations correctly if the driver itself controls all related device objects. This is generally true for the peer pattern, but it need not be true for the mux pattern.
The following code segment illustrates how to use removal relations, and is followed by a brief discussion of what the code segment is illustrating. The code is intended to be an aid to understanding the concepts discussed here, and is not intended to be complete, may not be accurate, and undoubtedly won't compile. It is however loosely based on actual production code. The sample illustrates the case of simple volume mirroring, where mirrors are constructed of no more than two disk plexes. In addition the sample ignores the case where a physical disk can contain more than one volume. Also the assumption is made that a single driver controls both the physical device objects and the virtual device objects in the mux pattern.
1: NTSTATUS DispatchPnP( 2: PDEVICE_OBJECT DeviceObject, 3: PIRP Irp) 4: { 5: PAGED_CODE (); 6: NTSTATUS status = STATUS_SUCCESS; 7: PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation (Irp); 8: BOOLEAN sendDown = TRUE; 9: DEVICE_EXTENSION devExt = GETDEVICE_EXTENSION(DeviceObject); 10: 11: status = IoAcquireRemoveLock(&devExt->removeLock, Irp); 12: if (!NT_SUCCESS(Status)) { 13: 14: IoCompleteIrp(Irp, status, Irp->IoStatus.Information); 15: sendDown = FALSE; 16: 17: } else { 18: 19: switch (IrpStack->MinorFunction) { 20: 21: case IRP_MN_QUERY_DEVICE_RELATIONS: 22: status = QueryDeviceRelations(DeviceObject, Irp); 23: break; 24: 25: case IRP_MN_QUERY_REMOVE_DEVICE: 26: { 27: // 28: // removeOk decides if it should allow 29: // the query remove, based on the current state 30: // of the related devices, which state could 31: // have changed between the time the query device relations 32: // was sent and the query remove gets sent. In addition this 33: // test is a superset of okToRemove(), called from QueryDeviceRelations(), 34: // as it tests the state of the virtual device in addition to the state 35: // of the physical devices associated with the virtual device. 36: // 37: // The algorthm actually implemented in a real world example 38: // is that if there is not another operational path and 39: // the virtual device pnp state is started, stopPending, stopped, 40: // or removePending, then deny the remove. 41: // 42: // Note that removepending is conditional - the device could go operational 43: // again as somebody else in the virtual device stack could reject the 44: // remove. 45: // 46: status = removeOk(devExt); 47: Irp->IoStatus.Status = status; 48: 49: if (status == STATUS_SUCCESS) { 50: 51: SetNewPnpState(devExt, RemovePending); 52: 53: } else { 54: // 55: // we cannot send this irp down the stack 56: // 57: IoCompleteIrp(Irp, status, Irp->IoStatus.Information); 58: IoReleaseRemoveLock(&devExt->removeLock, Irp); 59: sendDown = FALSE; 60: } 61: } 62: break; 63: 64: case IRP_MN_CANCEL_REMOVE_DEVICE: 65: // 66: // First check to see whether you have received cancel-remove 67: // without first receiving a query-remove. This could happen if 68: // someone above us fails a query-remove and passes down the 69: // subsequent cancel-remove. 70: // 71: if(RemovePending == CurrentState()) 72: { 73: // 74: // We did receive a query-remove, so restore. 75: // 76: RestorePreviousPnpState(); 77: } 78: Irp->IoStatus.Status = status; 79: break; 80: 81: case IRP_MN_REMOVE_DEVICE: 82: // 83: // do whatever is appropriate for your device removal. 84: // 85: IoReleaseRemoveLockAndWait(&devExt->removeLock, Irp); 86: Irp->IoStatus.Status = status: 87: IoSkipCurrentIrpStackLocation(Irp); 88: status = IoCallDriver(devExt->nextLowerDevice, Irp); 89: sendDown = FALSE; 90: break; 91: // 92: // other pnp requests here... 93: // 94: } 95: } 96: 97: if (sendDown) { 98: IoSkipCurrentIrpStackLocation(Irp); 99: status = IoCallDriver(devExt->nextLowerDevice, Irp); 100: IoReleaseRemoveLock(&devExt->RemoveLock, Irp); 101: } 102: 103: return status; 104: } 105: 106: NTSTATUS QueryDeviceRelations( 107: PDEVICE_OBJECT DeviceObject, 108: PIRP Irp) 109: { 110: PAGED_CODE (); 111: PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp); 112: DEVICE_EXTENSION devExt = GETDEVICE_EXTENSION(DeviceObject); 113: DEVICE_RELATION_TYPE type = 114: irpSp->Parameters.QueryDeviceRelations.Type; 115: NTSTATUS Status = STATUS_SUCCESS; 116: 117: switch (type) { 118: // 119: // This example deals with the first use case, the mux pattern, 120: // where a virtual device hides one or more physical devices. 121: // 122: case RemovalRelations: 123: { 124: // 125: // You might need some locking here if you are testing the state of 126: // some other device that your driver controls 127: // 128: // 129: // removeOk decides if it is 'safe' to remove 130: // this path. A simple rule would be if there exists one 131: // other operational path then it is safe to remove 132: // this path. 133: // 134: if (NT_SUCCESS(removeOk(devExt)) { 135: // 136: // if it is not safe to remove this path then 137: // let pnp know that removal relations are important, 138: // in which case pnp will query the virtual device to see 139: // if it is ok to be removed. 140: // 141: ULONG relationsSize = sizeof(DEVICE_RELATIONS); // minimal size 142: // 143: // get the existing device relations from the irp. 144: // 145: PDEVICE_RELATIONS oldRelations = 146: (PDEVICE_RELATIONS) Irp->IoStatus.Information; 147: 148: if (oldRelations) { 149: // 150: // somebody could produce an empty relations, 151: // like that nasty driver verifier, for example. 152: // 153: if (oldRelations->Count) { 154: 155: relationsSize += (oldRelations->Count * sizeof(PDEVICE_OBJECT)); 156: 157: } else { 158: // 159: // hmmmph. They did produce an empty relations. 160: // 161: ExFreePool(oldRelations); 162: oldRelations = NULL; 163: } 164: } 165: // 166: // device relations for removal should include the 167: // virtual device above us. 168: // 169: PDEVICE_RELATIONS deviceRelations = 170: (PDEVICE_RELATIONS) ExAllocatePoolWithTag(PagedPool, relationSize, '1STH'); 171: 172: if(deviceRelations) { 173: 174: if (oldRelations) { 175: // 176: // first include what we were given. 177: // 178: RtlCopyMemory (deviceRelations->Objects, oldRelations->Objects, 179: oldRelations->Count * sizeof (PDEVICE_OBJECT)); 180: 181: deviceRelations->Count = oldRelations->Count; 182: ExFreePool(oldRelations); 183: oldRelations = NULL; 184: } 185: // 186: // point the irp at our new relations structure. 187: // 188: Irp->IoStatus.Information = (ULONG_PTR)(deviceRelations); 189: // 190: // add our virtual device to the removal relationns 191: // 192: deviceRelations->Objects[deviceRelations->Count] = 193: devExt->virtualDevice; 194: ObReferenceObject(devExt->virtualDevice)); 195: deviceRelations->Count++; 196: Status = STATUS_SUCCESS; 197: 198: } else { 199: 200: Status = STATUS_INSUFFICIENT_RESOURCES; 201: ExFreePool(oldRelations); 202: oldRelations = NULL; 203: } 204: 205: } 206: // 207: // if you acquired a lock, now would be an excellent time 208: // to release it. 209: // 210: } 211: break; 212: 213: case TargetDeviceRelation: 214: case EjectionRelations: 215: case BusRelations: 216: default: 217: break; 218: 219: } 220: // 221: // make sure we have updated the status 222: // 223: Irp->IoStatus.Status = Status; 224: return Status; 225: } 226: 227:
In the function DispatchPnP there are two PNP requests that the code must deal with: IRP_MN_QUERY_DEVICE_RELATIONS, and IRP_MN_QUERY_REMOVE_DEVICE. Other PNP requests would of course also be processed here, but most are omitted for clarity.
IRP_MN_QUERY_DEVICE_RELATIONS is received because either the PnPManager is starting its remove processing for a device node or nodes, which is the typical case, or atypically because a driver in the device node driver stack has explicitly requested this IRP by calling IoInvalidateDeviceRelations specifying type RemovalRelations. DispatchPnP calls a handler for this IRP, QueryDeviceRelations, and then finishes processing the IRP by sending it down the stack. (If this code segment was for a PDO device it would of course not forward the request, but instead complete the request.) No completion handler is required, so the IRP forwarding operation is quite simple.
After IRP_MN_QUERY_DEVICE_RELATIONS is processed by all related device nodes, (a graph traversal operation by the PnPManager) the PnPManager starts to send IRP_MN_QUERY_REMOVE_DEVICE to the related device nodes. The order in which these requests will be delivered to the set of related device nodes is not specified. In the current implementations of NT, drivers can be certain that only one such request is in progress at any time, that all IRP_MN_QUERY_DEVICE_RELATIONS for the set of device nodes have been processed before any IRP_MN_QUERY_REMOVE_DEVICE requests have been sent to this set of related device nodes, and that no IRP_MN_REMOVE_DEVICE requests will be sent to the set of related device nodes until all of the IRP_MN_QUERY_REMOVE_DEVICE requests have been processed.
DispatchPnP handles IRP_MN_QUERY_REMOVE_DEVICE operations by deciding to either allow or deny the request, using the removeOk helper function (not provided in the sample.) If our driver is going to allow query remove, it must also forward the request down the stack. If the driver is going to deny query remove, it must not forward the IRP, but instead completes it immediately.
Just to complete the picture, DispatchPnP also provides code samples for IRP_MN_CANCEL_REMOVE_DEVICE and IRP_MN_REMOVE_DEVICE.
QueryDeviceRelations implements the logic for processing an IRP_MN_QUERY_DEVICE_RELATIONS request. As stated earlier, this request always arrives before an IRP_MN_QUERY_REMOVE_DEVICE, but may also arrive due to some driver in the device node calling IoInvalidateDeviceRelations. It would be a mistake to assume that an IRP_MN_QUERY_REMOVE_DEVICE must follow. QueryDeviceRelations first decides if RemovalRelations are required by calling removeOk, the same helper function that will again be called by the DispatchPnP processing for IRP_MN_QUERY_REMOVE_DEVICE. Note that operational state can change between the time the driver processes IRP_MN_QUERY_DEVICE_RELATIONS and the time that the driver processes the IRP_MN_QUERY_REMOVE_DEVICE request. If removeOk indicates that removal is allowed, then QueryDeviceRelations performs the operations required to provide the relations information to PnP. Otherwise no processing is performed.
Processing the IRP_MN_QUERY_DEVICE_RELATIONS requires the function to create and initialize a DEVICE_RELATIONS object. This has two parts: 1) copying the existing DEVICE_RELATIONS contents (if any), and 2) adding the drivers RemovalRelations. The existing DEVICE_RELATIONS is found in the IoStatus.Information field of the IRP. The copying part has to handle the case where there is no existing DEVICE_RELATIONS structure, and the case where there is an existing DEVICE_RELATIONS structure, but it is empty. So first QueryDeviceRelations determines the data size required to perform the copy operation, then it adds to that size the additional size required to include its own removal relations. Once the size calculation is complete, QueryDeviceRelations can then allocate the appropriately sized memory block, cast that to a pointer to a DEVICE_RELATIONS structure, and initialize the object with the existing and new contents. Having done that, QueryDeviceRelations then must free the existing DEVICE_RELATIONS (if it in fact exists,) and set the Irp.IoStatus.Information field to point to the new DEVICE_RELATIONS structure it just initialized. QueryDeviceRelations has to deal with allocation failures, and does so by setting the NTSTATUS returned to the standard STATUS_INSUFFICIENT_RESOURCES.
RemovalRelations are a bit of PnP arcania. Outside of the storage stack you most
likely will never have a need for them. However, if you do implement virtual
devices that have non-pnp associated physical devices that provide services for
your virtual device, then you may need to consider if RemovalRelations might be
relevant to your implementation.
About the author:
Mark Roddy is an independent consultant specializing in Windows NT kernel software development. Mark has been working exclusively in the NT kernel since 1994, with a focus on storage subsystems and highly reliable computer platforms. In addition to software development, he has been training developers since 1996, and currently works with Azius to provide Windows NT device driver training.