Kernel Thread CPU Affinity in Linux: kthread_bind Explained-Best Linux Device Drivers Training In Hyderabad

Kernel Thread CPU Affinity in Linux: kthread_bind Explained

Pin kernel threads to CPU cores the correct way on modern 6.x kernels — free Linux device drivers course, EmbeddedPathashala

Level
Intermediate
Kernel
6.x (updated)
Part
2 of 2

In Part 1 we controlled CPU placement from user space. This lecture moves inside the kernel: kernel thread CPU affinity — how a loadable kernel module can create kernel threads and bind each one to a specific CPU core. This skill shows up constantly in driver work: per-CPU data experiments, per-core polling threads, CPU-hotplug-aware workers, and performance testing. Just as importantly, we will see what changed: many older books demonstrated kernel thread CPU affinity using a kallsyms_lookup_name() hack to call unexported scheduler functions. That door was closed back in kernel 5.7, so if you copy that code today it simply will not build or run. As part of this free Linux device drivers course, you will learn the clean, supported way that works on current kernels.

What You Will Learn

  • What kernel threads are and how they differ from user threads
  • Creating kernel threads with kthread_create() / kthread_run()
  • Setting kernel thread CPU affinity with kthread_bind() and kthread_create_on_cpu()
  • Why the old kallsyms_lookup_name() approach is dead on modern kernels
  • A complete, original kernel module that pins one worker thread per core
  • How this connects to per-CPU variables and housekeeping CPUs

Prerequisites

  • Part 1 of this lecture (user-space Linux CPU affinity)
  • Ability to build and load a basic kernel module (covered in our free Linux kernel development course — the LKM series)
  • Kernel headers installed for your running kernel

Kernel Threads: A Quick Refresher

A kernel thread is a task that lives entirely in kernel space — it has no user-mode address space of its own. You have already met many of them: run ps -ef and every name in square brackets, like [ksoftirqd/0] or [kworker/2:1], is a kernel thread. Notice the naming convention: the number after the slash is the CPU core the thread is bound to. ksoftirqd/0 handles softirq work on core 0, ksoftirqd/1 on core 1, and so on. The kernel itself uses per-core bound threads everywhere, and drivers can do exactly the same.

Per-Core Kernel Threads on a 4-Core System
CPU 0
ksoftirqd/0
kworker/0:*
mydrv_worker/0
CPU 1
ksoftirqd/1
kworker/1:*
mydrv_worker/1
CPU 2
ksoftirqd/2
kworker/2:*
mydrv_worker/2
CPU 3
ksoftirqd/3
kworker/3:*
mydrv_worker/3

Each bound thread never migrates — its affinity mask contains exactly one core.

Why the Old kallsyms_lookup_name() Hack No Longer Works

Older material on kernel thread CPU affinity had a problem to work around: the scheduler function that sets a task’s allowed CPUs is not exported to modules. The workaround of that era was to ask kallsyms_lookup_name() for the address of the unexported function and call it through a function pointer. It was openly described as a hack even then.

Two changes killed it:

  • Kernel 5.7 unexported kallsyms_lookup_name() itself. The kernel developers removed the export precisely because modules were abusing it to bypass the export policy. On any kernel from 5.7 onward (i.e. everything you will realistically target in 2026), the hack fails at build/load time.
  • Supported APIs cover the real use cases. For the legitimate need — binding a kernel thread you created to a core — the kthread API has clean, exported helpers. There is simply no reason to hack.

The lesson generalizes: when a symbol is not exported, the kernel community is telling you that modules should not call it. Look for the supported interface instead of tunnelling around the wall.

The Correct APIs for Kernel Thread CPU Affinity

API What It Does Exported to Modules?
kthread_create() Create a kernel thread in a stopped state (does not run yet) Yes
kthread_bind(task, cpu) Bind a not-yet-running kthread to one core Yes
kthread_create_on_cpu() Create + bind in one step, names the thread with the CPU number Yes
wake_up_process() Start the created thread running Yes
kthread_stop() Ask the thread to exit; pairs with kthread_should_stop() in its loop Yes
set_cpus_allowed_ptr() Change the mask of an already-running task — core scheduler internal No — not for modules

The crucial ordering rule: kthread_bind() must be called before the thread has ever run. That is exactly why kthread_create() hands you a stopped thread — you bind it, then wake it. This sequence needs no special capability tricks and works on every modern kernel.

Correct Lifecycle for a Bound Kernel Thread
1. kthread_create()
thread exists, stopped
→ 2. kthread_bind(t, cpu)
affinity = that one core
→ 3. wake_up_process()
runs, pinned forever
→ 4. kthread_stop()
clean exit on rmmod

Complete Example: One Pinned Worker Thread Per Core

Here is an original, self-contained module for this free Linux device drivers course. It spawns one kernel thread per online core, binds each thread to its core, and each thread periodically logs which CPU it is on (spoiler: always the same one). Unload the module and the threads exit cleanly.

// percore_workers.c - one bound kthread per online CPU (kernel 6.x)
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/cpumask.h>

static struct task_struct *workers[NR_CPUS];

static int worker_fn(void *data)
{
    int mycpu = (long)data;

    while (!kthread_should_stop()) {
        pr_info("percore_workers: worker for cpu %d running on cpu %d\n",
                mycpu, smp_processor_id());
        ssleep(5);
    }
    pr_info("percore_workers: worker %d exiting\n", mycpu);
    return 0;
}

static int __init percore_init(void)
{
    int cpu;

    for_each_online_cpu(cpu) {
        struct task_struct *t;

        t = kthread_create(worker_fn, (void *)(long)cpu,
                           "epw_worker/%d", cpu);
        if (IS_ERR(t)) {
            pr_err("percore_workers: create failed on cpu %d\n", cpu);
            continue;
        }
        kthread_bind(t, cpu);   /* must happen BEFORE first run */
        workers[cpu] = t;
        wake_up_process(t);
    }
    pr_info("percore_workers: loaded\n");
    return 0;
}

static void __exit percore_exit(void)
{
    int cpu;

    for_each_online_cpu(cpu)
        if (workers[cpu])
            kthread_stop(workers[cpu]);
    pr_info("percore_workers: unloaded\n");
}

module_init(percore_init);
module_exit(percore_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Per-core bound kernel threads demo");

A minimal Makefile:

obj-m += percore_workers.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Build, load, and verify:

$ make
$ sudo insmod percore_workers.ko
$ sudo dmesg | tail
percore_workers: worker for cpu 0 running on cpu 0
percore_workers: worker for cpu 1 running on cpu 1
percore_workers: worker for cpu 2 running on cpu 2
percore_workers: worker for cpu 3 running on cpu 3

# Confirm the binding from user space too:
$ ps -eLo pid,psr,comm | grep epw_worker
  4711   0 epw_worker/0
  4712   1 epw_worker/1
  4713   2 epw_worker/2
  4714   3 epw_worker/3

$ sudo rmmod percore_workers

The psr column of ps shows the processor each thread last ran on — it never changes for a bound thread. You can even query a kernel thread’s mask with taskset -p <pid>, though setting it from user space on a kernel-bound thread will be refused (the kernel marks such threads with the PF_NO_SETAFFINITY flag).

Where This Pattern Is Used for Real

  • Per-CPU variables: to demonstrate or test per-CPU data safely, you want two threads guaranteed to be on two different cores — exactly what binding gives you.
  • Per-core polling: high-throughput drivers sometimes run one polling thread per core, each servicing that core’s queue.
  • CPU hotplug work: the kernel’s own smpboot per-CPU thread infrastructure (which powers ksoftirqd) creates and parks bound threads as cores go on/offline.

Common Mistakes and Troubleshooting

Mistake What Happens / Fix
Using kthread_run() then kthread_bind() kthread_run() wakes the thread immediately, so it may already be running — binding then triggers a warning. Use kthread_create() + bind + wake.
Copying the old kallsyms_lookup_name() code Fails on kernels ≥ 5.7 — the symbol is no longer exported. Use the kthread APIs in this lecture.
Forgetting kthread_stop() on module exit Orphaned threads keep running with code from an unloaded module — instant crash territory. Always stop every thread in module_exit.
Busy-looping without sleeping A bound thread that never sleeps monopolizes its core. Sleep (ssleep, wait queues) or at least cond_resched().
Ignoring CPU hotplug If the bound core goes offline, your thread is migrated off it. Production code registers CPU hotplug callbacks (cpuhp_setup_state()) or uses the smpboot thread framework.

Best Practices

  • Bind before first run — the create/bind/wake sequence is the whole trick.
  • Name threads with the core number ("myname/%d") following kernel convention, so ps output is self-explanatory.
  • Respect housekeeping. On systems using isolated cores for real-time work, don’t drop your worker threads onto the isolated cores unless that is the design.
  • Prefer workqueues when you don’t need binding. If the work has no per-core requirement, a workqueue is simpler and lets the kernel manage concurrency.
  • Stay inside exported APIs. If your design seems to require an unexported scheduler function, the design — not the export table — is what needs to change.

Performance and Security Considerations

Performance: a bound kernel thread doing heavy work steals time from everything else on that core, including user tasks pinned there — budget your per-core CPU carefully and always sleep when idle. On big.LITTLE embedded SoCs, think about which cores you bind to: a housekeeping worker belongs on a little core, a throughput worker on a big one.

Security/stability: kernel threads run with full kernel privilege; a bug is a system bug, not a process crash. The module unload path is the classic hazard — every created thread must be stopped before rmmod completes, or the thread will execute freed code. This is also why the kernel refuses user-space affinity changes on its own critical bound threads (PF_NO_SETAFFINITY): moving ksoftirqd/2 off core 2 would break softirq processing for that core.

Key Takeaways

  • Kernel thread CPU affinity is set with the supported kthread APIs: create (stopped) → kthread_bind() → wake_up_process().
  • The old kallsyms_lookup_name() hack died in kernel 5.7 — never use it in new code.
  • Per-core bound threads are a standard kernel pattern (ksoftirqd/N, kworker/N:*) and a foundation for per-CPU data work.
  • Always kthread_stop() every thread on module exit, and plan for CPU hotplug in production drivers.

Conclusion

Together with Part 1, you now control CPU placement across the whole stack: user processes and threads via sched_setaffinity() and taskset, groups of tasks via cgroup v2 cpuset, and kernel threads via kthread_bind() — all with behaviour verified on modern 6.x kernels rather than the older kernels most books describe. This pairing of “what the API is” with “why the old way stopped working” is exactly the kind of depth that separates a working kernel developer from someone who has only read tutorials. In the next lectures of this free Linux kernel development course we build on this with per-CPU variables and the concurrency machinery that depends on knowing which core you are on.

Frequently Asked Questions (FAQ)

Q1. What is kernel thread CPU affinity?

It is the restriction of a kernel thread to specific CPU cores. A bound kernel thread has an affinity mask containing exactly one core and never migrates, which is essential for per-CPU work.

Q2. Why can’t my module just call the scheduler’s internal affinity function?

Because it is not exported to modules — deliberately. Modules must use the exported kthread APIs (kthread_bind(), kthread_create_on_cpu()), which cover the legitimate use cases.

Q3. What is the difference between kthread_run() and kthread_create()?

kthread_run() creates and immediately wakes the thread. kthread_create() leaves it stopped, giving you the window to call kthread_bind() before the thread ever executes. For bound threads, always use the create/bind/wake sequence.

Q4. Can I change a kernel thread’s affinity from user space with taskset?

You can query it, but the kernel refuses changes on threads flagged PF_NO_SETAFFINITY (which includes threads bound via kthread_bind() and critical per-CPU kernel threads). That protection keeps per-core infrastructure intact.

Q5. What happens to a bound kernel thread if its CPU goes offline?

The scheduler must move it somewhere, so the binding is effectively broken. Robust drivers handle this with CPU hotplug callbacks or by using the kernel’s smpboot per-CPU thread framework, which parks/unparks threads automatically.

Q6. Is kthread_bind() still valid on the newest kernels with EEVDF?

Yes. The EEVDF scheduler (kernel 6.6+) changed how runnable normal tasks are ordered, not how affinity works. kthread_bind() and the whole kthread API remain the supported mechanism on current kernels.

Q7. Where can I learn kernel module basics needed for this lecture?

In the Linux Kernel Module (LKM) series of our free Linux kernel development course on EmbeddedPathashala — it covers building, loading, parameters, logging and more, all free, alongside our free Linux device drivers course and free embedded systems course.

Keep Building Kernel Skills — Free

Explore the full free Linux kernel development course, free Linux device drivers course and free embedded systems course on EmbeddedPathashala.

Browse All Courses

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *