| # Zircon Device Model |
| |
| ## Introduction |
| |
| In Zircon, device drivers are implemented as ELF shared libraries (DSOs) which are |
| loaded into Device Host (devhost) processes. The Device Manager (devmgr) process, |
| contains the Device Coordinator which keeps track of drivers and devices, manages |
| the discovery of drivers, the creation and direction of Device Host processes, and |
| maintains the Device Filesystem (devfs), which is the mechanism through which userspace |
| services and applications (constrained by their namespaces) gain access to devices. |
| |
| The Device Coordinator views devices as part of a single unified tree. |
| The branches (and sub-branches) of that tree consist of some number of |
| devices within a Device Host process. The decision as to how to sub-divide |
| the overall tree among Device Hosts is based on system policy for isolating |
| drivers for security or stability reasons and colocating drivers for performance |
| reasons. |
| |
| NOTE: The current policy is simple (each device representing a physical bus-master |
| capable hardware device and its children are place into a separate devhost). It |
| will evolve to provide finer-grained partitioning. |
| |
| |
| ## Devices, Drivers, and Device Hosts |
| |
| Here's a (slightly trimmed for clarity) dump of the tree of devices in |
| Zircon running on Qemu x86-64: |
| |
| ``` |
| $ dm dump |
| [root] |
| <root> pid=1509 |
| [null] pid=1509 /boot/driver/builtin.so |
| [zero] pid=1509 /boot/driver/builtin.so |
| [misc] |
| <misc> pid=1645 |
| [console] pid=1645 /boot/driver/console.so |
| [dmctl] pid=1645 /boot/driver/dmctl.so |
| [ptmx] pid=1645 /boot/driver/pty.so |
| [i8042-keyboard] pid=1645 /boot/driver/pc-ps2.so |
| [hid-device-001] pid=1645 /boot/driver/hid.so |
| [i8042-mouse] pid=1645 /boot/driver/pc-ps2.so |
| [hid-device-002] pid=1645 /boot/driver/hid.so |
| [sys] |
| <sys> pid=1416 /boot/driver/bus-acpi.so |
| [acpi] pid=1416 /boot/driver/bus-acpi.so |
| [pci] pid=1416 /boot/driver/bus-acpi.so |
| [00:00:00] pid=1416 /boot/driver/bus-pci.so |
| [00:01:00] pid=1416 /boot/driver/bus-pci.so |
| <00:01:00> pid=2015 /boot/driver/bus-pci.proxy.so |
| [bochs_vbe] pid=2015 /boot/driver/bochs-vbe.so |
| [framebuffer] pid=2015 /boot/driver/framebuffer.so |
| [00:02:00] pid=1416 /boot/driver/bus-pci.so |
| <00:02:00> pid=2052 /boot/driver/bus-pci.proxy.so |
| [intel-ethernet] pid=2052 /boot/driver/intel-ethernet.so |
| [ethernet] pid=2052 /boot/driver/ethernet.so |
| [00:1f:00] pid=1416 /boot/driver/bus-pci.so |
| [00:1f:02] pid=1416 /boot/driver/bus-pci.so |
| <00:1f:02> pid=2156 /boot/driver/bus-pci.proxy.so |
| [ahci] pid=2156 /boot/driver/ahci.so |
| [00:1f:03] pid=1416 /boot/driver/bus-pci.so |
| ``` |
| |
| The names in square brackets are devices. The names in angle brackets are |
| proxy devices, which are instantiated in the "lower" devhost, when process |
| isolation is being provided. The pid= field indicates the process object |
| id of the devhost process that device is contained within. The path indicates |
| which driver implements that device. |
| |
| Above, for example, the pid 1416 devhost contains the pci bus driver, which has |
| created devices for each PCI device in the system. PCI device 00:02:00 happens |
| to be an intel ethernet interface, which we have a driver for (intel-ethernet.so). |
| A new devhost (pid 2052) is created, set up with a proxy device for PCI 00:02:00, |
| and the intel ethernet driver is loaded and bound to it. |
| |
| Proxy devices are invisible within the Device filesystem, so this ethernet device |
| appears as `/dev/sys/pci/00:02:00/intel-ethernet`. |
| |
| |
| ## Protocols, Interfaces, and Classes |
| |
| Devices may implement Protocols, which are C ABIs used by child devices |
| to interact with parent devices in a device-specific manner. The |
| [PCI Protocol](../../system/ulib/ddk/include/ddk/protocol/pci.h), |
| [USB Protocol](../../system/ulib/ddk/include/ddk/protocol/usb.h), |
| [Block Core Protocol](../../system/ulib/ddk/include/ddk/protocol/block.h), and |
| [Ethermac Protocol](../../system/ulib/ddk/include/ddk/protocol/ethernet.h), are |
| examples of these. Protocols are usually in-process interactions between |
| devices in the same devhost, but in cases of driver isolation, they may take |
| place via RPC to a "higher" devhost. |
| |
| Devices may implement Interfaces, which are RPC protocols that clients (services, |
| applications, etc) use. The base device interface supports posix style |
| open/close/read/write style IO. Currently, Interfaces are supported via the ioctl |
| operation in the base device interface. In the future, Fuchsia's interface definition |
| language and bindings (FIDL) will be supported. |
| |
| In many cases a Protocol is used to allow drivers to be simpler by taking advantage |
| of a common implementation of an Interface. For example, the "block" driver implements |
| the common block interface, and binds to devices implementing the Block Core Protocol, |
| and the "ethernet" driver does the same thing for the Ethernet Interface and Ethermac |
| Protocol. Some protocols, such as the two cited here, make use of shared memory, and |
| non-rpc signaling for more efficient, lower latency, and higher throughput than could |
| be achieved otherwise. |
| |
| Classes represent a promise that a device implements an Interface or Protocol. |
| Devices exist in the Device Filesystem under a topological path, like |
| `/sys/pci/00:02:00/intel-ethernet`. If they are a specific class, they also appear |
| as an alias under `/dev/class/CLASSNAME/...`. The `intel-ethernet` driver implements |
| the Ethermac interface, so it also shows up at `/dev/class/ethermac/000`. The names |
| within class directories are unique but not meaningful, assigned on demand. |
| |
| NOTE: Currently names in class directories are 3 digit decimal numbers, but they |
| are likely to change form in the future. Clients should not assume there is any |
| specific meaning to a class alias name. |
| |
| |
| ## Device Driver Lifecycle |
| |
| Device drivers are loaded into devhost processes when it is determined they are |
| needed. What determines if they are loaded or not is the Binding Program, which |
| is a description of what device a driver can bind to. The Binding Program is |
| defined using macros in [`ddk/binding.h`](../../system/ulib/ddk/include/ddk/binding.h) |
| |
| An example Binding Program from the Intel Ethernet driver: |
| ``` |
| ZIRCON_DRIVER_BEGIN(intel_ethernet, intel_ethernet_driver_ops, "zircon", "0.1", 9) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PCI), |
| BI_ABORT_IF(NE, BIND_PCI_VID, 0x8086), |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x100E), // Qemu |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x15A3), // Broadwell |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x1570), // Skylake |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x1533), // I210 standalone |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x15b7), // Skull Canyon NUC |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x15b8), // I219 |
| BI_MATCH_IF(EQ, BIND_PCI_DID, 0x15d8), // Kaby Lake NUC |
| ZIRCON_DRIVER_END(intel_ethernet) |
| ``` |
| |
| The ZIRCON_DRIVER_BEGIN and _END macros include the necessary compiler directives |
| to put the binding program into an ELF NOTE section which allows it to be inspected |
| by the Device Coordinator without needing to fully load the driver into its process. |
| The second parameter to the _BEGIN macro is a zx_driver_ops_t structure pointer (defined |
| by `[ddk/driver.h](../../system/ulib/ddk/include/ddk/driver.h)` which defines the |
| init, bind, create, and release methods. |
| |
| `init()` is invoked when a driver is loaded into a Device Host process and allows for |
| any global initialization. Typically none is required. If the `init()` method is |
| implemented and fails, the driver load will fail. |
| |
| `bind()` is invoked to offer the driver a device to bind to. The device is one that |
| has matched the bind program the driver has published. If the `bind()` method succeeds, |
| the driver **must** create a new device and add it as a child of the device passed in |
| to the `bind()` method. See Device Lifecycle for more information. |
| |
| `create()` is invoked for platform/system bus drivers or proxy drivers. For the |
| vast majority of drivers, this method is not required. |
| |
| `release()` is invoked before the driver is unloaded, after all devices it may have |
| created in `bind()` and elsewhere have been destroyed. Currently this method is |
| **never** invoked. Drivers, once loaded, remain loaded for the life of a Device Host |
| process. |
| |
| |
| ## Device Lifecycle |
| |
| Within a Device Host process, devices exist as a tree of `zx_device_t` structures |
| which are opaque to the driver. These are created with `device_add()` which the |
| driver provides a `zx_protocol_device_t` structure to. The methods defined by the |
| function pointers in this structure are the "[device ops](device-ops.md)". The |
| various structures and functions are defined in [`device.h`](../../system/ulib/ddk/include/ddk/device.h) |
| |
| The `device_add()` function creates a new device, adding it as a child to the |
| provided parent device. That parent device **must** be either the device passed |
| in to the `bind()` method of a device driver, or another device which has been |
| created by the same device driver. |
| |
| A side-effect of `device_add()` is that the newly created device will be added |
| to the global Device Filesystem maintained by the Device Coordinator. If the |
| device is created with the **DEVICE_ADD_INVISIBLE** flag, it will not be accessible |
| via opening its node in devfs until `device_make_visible()` is invoked. This |
| is useful for drivers that have to do extended initialization or probing and |
| do not want to visibly publish their device(s) until that succeeds (and quietly |
| remove them if that fails). |
| |
| Devices are reference counted. When a driver creates one with `device_add()`, |
| it then holds a reference on that device until it eventually calls `device_remove()`. |
| If a device is opened by a remote process via the Device Filesystem, a reference |
| is acquired there as well. When a device's parent is removed, its `unbind()` |
| method is invoked. This signals to the driver that it should start shutting |
| the device down and remove any child devices it has created by calling `device_remove()` |
| on them. |
| |
| Since a child device may have work in progress when its `unbind()` method is |
| called, it's possible that the parent device which just called `device_remove()` |
| on the child could continue to receive device method calls or protocol method |
| calls on behalf of that child. It is advisable that before removing its children, |
| the parent device should arrange for these methods to return errors, so that |
| calls from a child before the child removal is completed do not start more |
| work or cause unexpected interactions. |
| |
| From the moment that `device_add()` is called without the **DEVICE_ADD_INVISIBLE** |
| flag, or `device_make_visible()` is called on an invisible device, other device |
| ops may be called by the Device Host. |
| |
| The `release()` method is only called after the creating driver has called |
| `device_remove()` on the device, all open instances of that device have been |
| closed, and all children of that device have been removed and released. This |
| is the last opportunity for the driver to destroy or free any resources associated |
| with the device. It is not valid to refer to the `zx_device_t` for that device |
| after `release()` returns. Calling any device methods or protocol methods for |
| protocols obtained from the parent device past this point is illegal and will |
| likely result in a crash. |