Speaker 1: Welcome to the deep dive. Today, we're jumping into the
Speaker 1: Linux kernel, specifically how it handles hardware. If you've ever
Speaker 1: wanted to write a device driver, or you know, just
Speaker 1: understand how your keyboard actually talks to your OS, this
Speaker 1: is for you. We're building the conceptual map exactly.
Speaker 2: Our mission today is really about the architecture. We've looked
Speaker 2: at some pretty comprehensive guides on kernel and driver development,
Speaker 2: and we want to quickly lay out that core blueprint.
Speaker 2: You know, what do you have to know? What's different?
Speaker 2: How does the kernel see the world of devices?
Speaker 1: Okay, so starting basic the device driver, it's fundamentally a
Speaker 1: translator right sitting in the middle.
Speaker 2: It's the crucial link. Yeah, it's that specialized code connecting
Speaker 2: your apps, your user space stuff to the actual physical
Speaker 2: hardware and it all goes to the kernel. You could
Speaker 2: almost say it's the only bit of software allowed to
Speaker 2: like directly touch the hardware ports.
Speaker 1: Got it. Okay, So let's start where most drivers start,
Speaker 1: the kernel module. This is how Linux stays flexible. Right,
Speaker 1: Let's you add stuff without a.
Speaker 2: Full reboot precisely, modules like you extend the kernel dynamically
Speaker 2: at run time, and our sources. Definitely emphasize this isn't
Speaker 2: for absolute beginners. You need solid C skills and to
Speaker 2: be comfortable on the Linux command line before you really
Speaker 2: dive into writing kernel code.
Speaker 1: Right, So, if we're building one, we need the basic structure,
Speaker 1: an entry point, and ideally an exit point.
Speaker 2: That's the core. You use module in it that declares
Speaker 2: the function that runs when your module gets loaded you
Speaker 2: know withins modern mod probe. And then module exit declares
Speaker 2: the cleanup function for when you unload it with a romedet.
Speaker 1: Okay, now here's where it gets interesting. Compared to normal programming.
Speaker 1: Memory optimization. Yeah, the kernel uses these macros in and exit.
Speaker 1: Why are they so important? Ah?
Speaker 2: Yeah, they're not just comments. There are instructions for the
Speaker 2: compiler and linker really important. When you mark your in
Speaker 2: it function within it, you're telling the linker put this
Speaker 2: code in a special memory section.
Speaker 1: And what's special about that section?
Speaker 2: Well, if your module is built into the kernel statically compiled,
Speaker 2: the kernel actually freeze that memory once the init function
Speaker 2: is done, because, I mean, it's never going to call
Speaker 2: that in it function again until the next reboot, right,
Speaker 2: so why keep the code around. It's a smart optimization.
Speaker 2: Use it, lose it.
Speaker 1: That is clever proactively cleaning up its own startup code.
Speaker 1: And exit is that the flip side pretty much? Yeah.
Speaker 2: Exit tills the compiler to just leave out the exit
Speaker 2: functions code entirely if the module is built in, because
Speaker 2: if it's built in, you can unload it anyway, so
Speaker 2: save space removes code you'd never run.
Speaker 1: Okay, moving to metadata. Every module needs info like module author,
Speaker 1: But the big one seems to be module license. Why
Speaker 1: is that one so sensitive?
Speaker 2: It really is. It's about the GPL and the kernel's ecosystem.
Speaker 2: You must declare a GPL compatible license using module license,
Speaker 2: especially if you want your module to use certain kernel functions,
Speaker 2: ones that are specifically exported only for GPL modules using
Speaker 2: exports symbol GPL.
Speaker 1: And if you don't, or if you use a proprietary.
Speaker 2: License, then boot your kernel gets more distainted. The kernel
Speaker 2: sits a fly. It basically says warning, non open or
Speaker 2: untrusted code has been loaded. If you then get a
Speaker 2: crash a kernel panic, well good luck getting help from
Speaker 2: the community. They'll likely see the taint flag and you know,
Speaker 2: say they can't support it because proprietary code might be
Speaker 2: the cause. It's a serious line.
Speaker 1: Okay, that definitely sets the stage. Now let's make that
Speaker 1: mental shift. We're not writing userspace programs anymore. We're inside
Speaker 1: the kernel. The rules change, the safety nets.
Speaker 2: Gone completely gone. In user space, if something goes terribly wrong,
Speaker 2: your program crashes. The OS cleans up fine. In the kernel,
Speaker 2: no way. If you allocate memory, grab an ieoport, whatever,
Speaker 2: you must clean it up yourself before your function returns.
Speaker 2: Fail to do that and your risk leaks instability, maybe
Speaker 2: even a full system crash.
Speaker 1: So error handling is totally different. No more just returning
Speaker 1: zero for success and one for failure.
Speaker 2: Right. The standard is really strict. Kernel functions that interact
Speaker 2: with system calls. They must return errors as negative values
Speaker 2: like return error code. So you'll see you're an AIO
Speaker 2: for an IO error or an enulmum if you couldn't
Speaker 2: get memory. It's a clear convention, so errors get passed
Speaker 2: up correctly, and.
Speaker 1: To manage that cleanup, especially if you have say five
Speaker 1: steps that work then the sixth one fails. The kernel
Speaker 1: style actually recommends using go to. That sounds controversial. It does.
Speaker 2: It goes against a lot of Standard C teaching, but
Speaker 2: in the kernel it's pragmatic. You use go to strictly
Speaker 2: for error cleanup paths. You set up labels like air free, buffer,
Speaker 2: air release, lock, maybe out. When an error happens, you
Speaker 2: go to the appropriate label. This jumps you straight into
Speaker 2: the sequence of cleanup steps needed, executed in reverse order
Speaker 2: of allocation.
Speaker 1: But doesn't that make the code harder to follow all
Speaker 1: those jumps.
Speaker 2: You'd think so, but it actually tends to make it cleaner.
Speaker 2: In this context. The alternative is deeply nested eifles blocks
Speaker 2: which get really hard to read and are super easy
Speaker 2: to mess up, like forgetting a cleanup step inside one branch.
Speaker 2: The go to approach enforces a clear, linear cleanup path.
Speaker 2: It's considered the safest and most readable pattern for kernel
Speaker 2: error handling. Strange but true.
Speaker 1: Okay, I can see the logic there, given the stakes.
Speaker 1: What about functions that need to return a pointer but
Speaker 1: might also fail. You can't return both a pointer and
Speaker 1: a negative error code, ah.
Speaker 2: Classic C problem. The kernel has neat macros for this,
Speaker 2: or ptr iSER and per tier. If your function fails
Speaker 2: and needs to return say anviol invalid argument instead of
Speaker 2: a pointer, it uses airptr and ball fail. This converts
Speaker 2: the air code into a special pointer value. The code
Speaker 2: calling that function then checks the return pointer using iSER.
Speaker 2: If it returns true, it means it's an error pointer.
Speaker 2: Then if you need the actual air code back, you
Speaker 2: use ptr on that pointer and it gives you ANVOL.
Speaker 2: It avoids ambiguity with returning anal MP.
Speaker 1: That's ill again. Okay, last bit on the programming shift logging,
Speaker 1: we're supposed to move beyond just using print, right.
Speaker 2: Yeah. While print is still the underlying engine, the recommendation
Speaker 2: is strongly towards using specific elper functions. Don't just use
Speaker 2: print with log levels directly, use things like prayer or
Speaker 2: prinfo for general module messages. But and this is important,
Speaker 2: if you're in a device driver, use the dev versions
Speaker 2: UH or dev infostruct device DEV.
Speaker 1: And why the dev prefix ones specifically for drivers.
Speaker 2: Because they automatically include context about the specific device the
Speaker 2: message is coming from, its name is positioned in the system,
Speaker 2: makes debugging way easier when you have multiple identical devices.
Speaker 2: They're even netdev versions for network drivers. They all tie
Speaker 2: the message to a specific kernel object.
Speaker 1: Got it? And if we want our modules messages to
Speaker 1: stand out in the flood of kernel logs, how do
Speaker 1: we add a prefix?
Speaker 2: Simple. You use the prfmt macro, you define hashtag, define
Speaker 2: prfmt fmt KDI. You're building the devmt at the top
Speaker 2: of your source file, Katie. Build mode name gets set
Speaker 2: automatically during the build to your module's name. So now
Speaker 2: every print FO, dev etc. Automatically prints like my driver
Speaker 2: error occurred. Super helpful.
Speaker 1: Okay, we've got the rules of engagement down. Now. The
Speaker 1: big danger zone concurrency, especially on SMP systems symmetric multiprocessing,
Speaker 1: where multiple CPU use can access the same memory the
Speaker 1: same hardware simultaneously. Locks are essential.
Speaker 2: This is absolutely where things get tricky, even for experienced devs.
Speaker 2: The key is understanding the context. Are you in code
Speaker 2: that can sleep user context or code that absolutely cannot sleep,
Speaker 2: like an interrupt handler atomic context.
Speaker 1: Let's start with the fast ones. Spin locks meant for
Speaker 1: short atomic operation.
Speaker 2: Exactly very short critical sections. When a CPU grabs a
Speaker 2: standard spin lock. Using spin lock, it disables preemption on
Speaker 2: that specific CPU, so other tasks in the same core
Speaker 2: won't interrupt it. But and this is the critical part,
Speaker 2: a standard spin lock does not stop hardware interrupts from
Speaker 2: firing on that cpu.
Speaker 1: Ah, and that's the classic deadlock scenario waiting to happen sisily.
Speaker 2: Imagine task A on CPU zero called spin lock to
Speaker 2: protect some data. Then bam, hardware interrupt fires on CPU zero.
Speaker 2: The CPU jumps to the interrupt handler IRQ. Now, if
Speaker 2: that IRQ handler needs the same data and tries to
Speaker 2: acquire the same spin lock, the IRQ spins forever, waiting
Speaker 2: task A to release the lock. But Task A is
Speaker 2: preempted by the IRQ and can't run to release.
Speaker 1: It out catastrophic. So the rule is, if data protected
Speaker 1: by a spin lock might also be touched by an
Speaker 1: interrupt handler, you need something more.
Speaker 2: Yes, unless you are absolutely one hundred percent certain that
Speaker 2: no interrupt handler will ever try to access that data
Speaker 2: or acquire that lock, you must use the IRQ save variants.
Speaker 2: Those are functions like spin locker save and spin a
Speaker 2: locker store. They do two things, disable preemption and disabled
Speaker 2: hardware interrupts on the local CPU. This makes the critical
Speaker 2: section truly atomic on that core. It's a vital lesson.
Speaker 1: Okay, So spin locks are for short atomic contexts, possibly
Speaker 1: needing IRQ disabling. How do utexts differ?
Speaker 2: Mutexes are conceptually simpler but have different rules they're built
Speaker 2: using spin locks underneath, but their behavior is different. If
Speaker 2: task B tries to acquire a mutex held by task A,
Speaker 2: instead of spinning, task B is put to sleep. The
Speaker 2: kernel puts it on a weight queue and lets the
Speaker 2: CPU schedule some other.
Speaker 1: Task, so they yield the CPU much more better for
Speaker 1: potentially longer critical sections.
Speaker 2: Then, yes, much better for contention and longer holds. But
Speaker 2: and this is a huge butt because they involve sleeping.
Speaker 2: You can only use mutexes in context where sleeping is allowed.
Speaker 2: That mean primarily user context like when handling a system call.
Speaker 2: You can never acquire a mutex from an interrupt handler
Speaker 2: or any other atomic context because those contexts absolutely cannot
Speaker 2: sleep using a mutex. There boom, system panic, got it.
Speaker 1: Spin locks for atomic mutexes for sleeping contexts. Let's briefly
Speaker 1: touch on time. We hear about Jiffey's but things are
Speaker 1: more abstract now.
Speaker 2: Yeah, Chiffees was the old way. It is. The kernel
Speaker 2: counter the increments at a certain frequency defined by the
Speaker 2: h z value. It gave fairly low resolution timing. The
Speaker 2: modern kernel abstracts this. It relies on two types of
Speaker 2: hardware components. For time.
Speaker 1: The things that track time and the things that schedule
Speaker 1: events in time.
Speaker 2: You got it. First, clock source devices. These provide a
Speaker 2: high resolution, always increasing monotonic counter. Think of it as
Speaker 2: the master clock for accurate time stamps. Second clock event
Speaker 2: devices these are the programmable timers. They can be told
Speaker 2: fire and interrupt exactly n nanoseconds from now. They're crucial
Speaker 2: for things like high resolution timers or timers much more
Speaker 2: precise than Jiffy's.
Speaker 1: Okay, and before we leave this area, tasklts they were
Speaker 1: for deferring work from interrupts, right the bottom half.
Speaker 2: Yes, they were a common mechanism. Yeah, but the sources
Speaker 2: we looked at had a very very strong warning. Task
Speaker 2: lits are being actively deprecated. They're slated for removal. You
Speaker 2: should not use them in any new code. They really
Speaker 2: only exist now for historical pedagogic reasons.
Speaker 1: Okay, loud and clear. So for deferring work now, the
Speaker 1: answer is work cues.
Speaker 2: Absolutely use workques. They provide a much more flexible and
Speaker 2: robust way to q work to be executed by dedicated
Speaker 2: kernel threads. Those threads can sleep, so they can handle
Speaker 2: much longer, more complex tasks safely.
Speaker 1: Right. Let's zoom out now to the big picture, how
Speaker 1: the kernel organizes all this hardware, the Linux Device Model LDM.
Speaker 1: This is how physical stuff turns into those directories we
Speaker 1: see insists.
Speaker 2: LDM is the framework, the glue it creates that hierarchy.
Speaker 2: Buses contain devices, drivers attached to devices, and it uses
Speaker 2: three core low level structures. At the very bottom, the
Speaker 2: most fundamental piece is the object. A cobject isn't a
Speaker 2: device itself or a driver. It represents the connection or
Speaker 2: an object that can appear in sifts. Basically, every single
Speaker 2: directory you navigate in the CYS Virtual file system corresponds
Speaker 2: to a object in kernel memory. It's the basic building
Speaker 2: block of that hierarchy.
Speaker 1: Ah okay, So cobject is the foundation for creating that
Speaker 1: topology and exposing attributes out to user space through sifts.
Speaker 1: That clicks.
Speaker 2: It's fundamental. These objects are then grouped into ksets, which
Speaker 2: often correspond to directories containing multiple objects, and they have
Speaker 2: associated cobduy types which define behavior like attribute handling. Together
Speaker 2: they build the whole.
Speaker 1: Structure, and within the structure we find different kinds of buses.
Speaker 2: Broadly, yeah, you have discoverable buses. Think PCI USB. You
Speaker 2: plug something in the bus itself can figure out what
Speaker 2: it is until the kernel the hardware handles the numeration.
Speaker 2: Then you have nondiscoverable buses. The platform bus is a
Speaker 2: common example, especially in embedded systems, where peripherals are just
Speaker 2: hardwired to the CPU. The bus itself doesn't announce devices.
Speaker 2: For these, the kernel needs to be told that a
Speaker 2: device exists at a certain address or uses certain resources.
Speaker 2: This information needs to be provided somehow.
Speaker 1: Which brings us neatly to the modern way of providing
Speaker 1: that information, the device tree. This replaced the old hard
Speaker 1: coded board files exactly.
Speaker 2: Device tree or DT is a huge step for portability.
Speaker 2: Instead of c code describing the hardware layout for one
Speaker 2: specific board, you use a textual format dot DTS source files.
Speaker 2: These files describe the hardware using nodes representing devices, buses,
Speaker 2: and properties like memory addresses, interrupt numbers. This dot dts
Speaker 2: file is compiled into a binary blob, the dot DTB
Speaker 2: or FDT Flatten device tree. The bootloader loads the dot
Speaker 2: DPB into memory, and the kernel parses it at boot
Speaker 2: time to learn about the non discoverable hardware present.
Speaker 1: What about adding hardware after boot or changing configurations dynamic.
Speaker 2: That's where dtoverlays come in. These are smaller partial dot
Speaker 2: DTBO device tree blob overlay files. They act like patches.
Speaker 2: You can load it dot DTB at runtime, usually via
Speaker 2: can figs, and it modifies the live in memory device tree,
Speaker 2: maybe enabling an interface, changing a pin configuration, adding a
Speaker 2: new IT two C device. It's very powerful.
Speaker 1: So when my driver starts up and it needs to
Speaker 1: know its memory address or its interrupt number, how does
Speaker 1: it get that from the device tree? Reliably?
Speaker 2: It uses specific API calls to query the tree. Crucially,
Speaker 2: the device tree uses named properties, so instead of asking
Speaker 2: for the first interrupts, your driver asked for the interrupt
Speaker 2: named four or similar functions like platforms yourt resource by
Speaker 2: name or of property read thirty two. Let the driver
Speaker 2: request resources by name as defined in the dot dts.
Speaker 2: This decouples the driver from the exact physical wiring order.
Speaker 1: Makes sense, Okay, last piece, let's connect this architecture to
Speaker 1: a real world subsystem. The example given was industrial io
Speaker 1: iiO right io is.
Speaker 2: A great example. It's a whole kernel subsystem specifically designed
Speaker 2: for sensors. Analog to digital converters ADCs, Digital to analog
Speaker 2: converters dcs basically data acquisition hardware.
Speaker 1: It manages all the complexity of reading raw sens or values,
Speaker 1: scaling them, handling triggers.
Speaker 2: Exactly, But how does it expose that data to userspace.
Speaker 2: It doesn't require custom applications for every sensor.
Speaker 1: It uses the device model and sifts precisely.
Speaker 2: The io framework models sensors and their data points as channels.
Speaker 2: Each channel like the X axis acceleration or temperature reading,
Speaker 2: gets exposed as attributes within sifts, So reading the raw
Speaker 2: value from an accelerometer might be as simple as reading
Speaker 2: a file like cispusiodevi COO dot device zero in SLX.
Speaker 2: User space just interacts with these standard file interfaces underpinned
Speaker 2: by LDM and the cobject structure representing that channel attribute.
Speaker 1: Wow. Okay, we've covered a lot, from those tiny innit
Speaker 1: macros affecting memory all the way up to these complex
Speaker 1: frameworks like iiO built on the core architecture.
Speaker 2: Yeah, we hit the big points. The mindset shift for
Speaker 2: kernel programming error clean up with go to logging, then
Speaker 2: the concurrency mindfield spin locks versus mutex is, atomic versus
Speaker 2: sleeping contexts.
Speaker 1: And then the overall structure. How the Linux device model
Speaker 1: uses cobjects to build the hierarchy, how the device tree
Speaker 1: describes hardware, and how subsystems like iiO plug into that.
Speaker 2: So if there's one final thought to leave you with,
Speaker 2: it's this, remember that object. It seems abstract, almost trivial,
Speaker 2: but everything in the device hierarchy, from the simplest led
Speaker 2: driver to the most complex network interface or iiO sensor framework,
Speaker 2: ultimately relies on objects to establish its existence and its
Speaker 2: attributes within the kernel's world, making it visible and controllable
Speaker 2: through sifts. That tiny structure is the bedrock of Linux's
Speaker 2: entire device organization.
Speaker 1: Amazing how that one fundamental piece underpins so much complexity.
Speaker 1: That's a great takeaway. Thank you for walking us through this.
Speaker 2: My pleasure is fascinating stuff.
Speaker 1: It really is. We'll catch you next time for another
Speaker 1: deep dive.