A Relatively Boring Intro to Device Drivers... Still Related to Differential Amps, Don't Worry...
I decided I would take a break from differential amplifiers today to discuss something seemingly unrelated. Lots of high-speed protocols, like SATA and PCIe, use differential signaling in their physical layers. Without getting into all the details now, differential signaling has many technical benefits, such as the elimination of certain kinds of noise. Over the next couple of weeks, I think it'd be interesting to get deep into the weeds about operating systems/kernels/Linux and low-level analog circuit design. The goal is to meet at the middle-layer of abstraction and make sense of what exactly occurs when you write to a hard drive via SATA or transfer data to a GPU via PCIe.
Well, firstly, what exactly is a device driver? In a very general sense, it's a piece of software that lets you talk to a piece of hardware. But that's an utterly useless definition. Let's be more precise about what we mean. Obviously, sooner or later, someone is going to need to write a piece of software to talk to a piece of hardware if we want to do anything interesting. And it really isn't that difficult to see how.
In older x86 architectures, we might see PMIO, or port-mapped I/O. In PMIO, you'll have a bunch of peripherals, each with an address (imagine 0x4 for PCIe for instance). An IN or OUT instruction will let you write to or read from that port address. You'll have circuitry in the PCIe controller that figures out how to handle these reads and writes.
In more modern CPUs, you'll see MMIO, or memory-mapped I/O. In MMIO, main memory and device peripherals are part of the same address space. So, if I write to the right address in main memory, I'll just end up writing to the PCIe controller's memory. For now, just know that there are different ways of writing assembly to access peripherals. We can deal with the advantages and disadvantages of MMIO/PMIO in a later post.
I think device drivers in the context of the Linux kernel represent a good programming practice more than any specific kind of program. Let me unpack that statement. At the end of the day, accessing peripherals boils down to execute the right kinds of instructions and having the right kinds of address decoding circuits and buses on the die. The Linux kernel simply standardizes the software development approach to accomplishing this task.
How does this standardization work? Well, let's first ask what a "device" actually is. As far as Linux is concerned, it's just a file, typically found in the /dev/ directory. That means you can read from it and write to it (with the right permissions). However, this raises a question... what does a read or a write to a device file look like?
Well, we know that it's some combination of IO-related instructions. However, therein lies the beauty of the approach. There is now a standard abstraction for accessing devices, an API if you will. We refer to these read and write operations as system calls, and there are different syscalls that need to be implemented depending on the driver type.
For instance, character devices allow the user to access the device in increments of bytes. Block devices are accessed in increments of much larger blocks. Serial devices are typically character-based, whereas hard drives are block-based. Character device drivers require write() and read() syscalls. Block devices require a strategy() call instead. The term "strategy" refers to the fact that the block driver must strategize about how to access within the given block at the right time.
Anyhow, that's probably enough for now. This should clear up the Linux driver architecture at a high level. We can get deeper into the weeds in the coming weeks. For instance, how does the kernel know to use a particular driver when writing to a device, what instructions are used to access particular devices, what signals pass through the CPU circuitry to perform peripheral functions, etc.? 'Till next time!
Comments
Post a Comment