Tasklets

Tasklets provide a simple mechanism for deferring work from interrupt context. They run in softirq context and cannot sleep.

Tasklet Basics

#include <linux/interrupt.h>

struct tasklet_struct {
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*callback)(struct tasklet_struct *t);
    /* ... */
};

Key Properties

  • Run in softirq context (interrupts enabled)
  • Cannot sleep or block
  • Same tasklet never runs concurrently on multiple CPUs
  • Different tasklets can run concurrently
  • Run on the CPU that scheduled them (usually)

Creating Tasklets

Static Declaration

/* Old API (deprecated) */
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);

/* New API (kernel 5.9+) */
static void my_tasklet_func(struct tasklet_struct *t);
DECLARE_TASKLET(my_tasklet, my_tasklet_func);

Dynamic Initialization

struct my_device {
    struct tasklet_struct tasklet;
    /* ... */
};

static void my_tasklet_func(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);
    /* Process data */
}

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    /* Initialize tasklet */
    tasklet_setup(&dev->tasklet, my_tasklet_func);

    return 0;
}

Tasklet API

Scheduling

/* Schedule tasklet to run */
tasklet_schedule(&dev->tasklet);

/* Schedule with high priority */
tasklet_hi_schedule(&dev->tasklet);

Control

/* Disable tasklet (increments count) */
tasklet_disable(&dev->tasklet);

/* Re-enable tasklet */
tasklet_enable(&dev->tasklet);

/* Kill tasklet - wait for completion and prevent rescheduling */
tasklet_kill(&dev->tasklet);

Complete Example

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/io.h>

struct my_device {
    void __iomem *regs;
    struct tasklet_struct tasklet;
    spinlock_t lock;
    u32 rx_buffer[256];
    int rx_head;
    int rx_tail;
    wait_queue_head_t rx_wait;
};

/* Tasklet function - softirq context, cannot sleep */
static void rx_tasklet_func(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);
    unsigned long flags;
    u32 data;

    spin_lock_irqsave(&dev->lock, flags);

    /* Process all available data */
    while (dev->rx_head != dev->rx_tail) {
        data = dev->rx_buffer[dev->rx_tail];
        dev->rx_tail = (dev->rx_tail + 1) % 256;

        spin_unlock_irqrestore(&dev->lock, flags);

        /* Process this item (still in softirq context) */
        process_rx_data(data);

        spin_lock_irqsave(&dev->lock, flags);
    }

    spin_unlock_irqrestore(&dev->lock, flags);

    /* Wake up readers */
    wake_up_interruptible(&dev->rx_wait);

    /* Re-enable device interrupt */
    writel(IRQ_ENABLE, dev->regs + IRQ_CTRL);
}

/* Interrupt handler - hardirq context */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    unsigned long flags;
    u32 status;

    status = readl(dev->regs + IRQ_STATUS);
    if (!(status & RX_IRQ))
        return IRQ_NONE;

    /* Disable interrupt until tasklet processes */
    writel(0, dev->regs + IRQ_CTRL);

    /* Acknowledge */
    writel(RX_IRQ, dev->regs + IRQ_CLEAR);

    /* Read all available data into buffer */
    spin_lock_irqsave(&dev->lock, flags);

    while (readl(dev->regs + RX_FIFO_COUNT)) {
        int next = (dev->rx_head + 1) % 256;

        if (next == dev->rx_tail) {
            /* Buffer full - drop data */
            readl(dev->regs + RX_DATA);  /* Discard */
            continue;
        }

        dev->rx_buffer[dev->rx_head] = readl(dev->regs + RX_DATA);
        dev->rx_head = next;
    }

    spin_unlock_irqrestore(&dev->lock, flags);

    /* Schedule tasklet */
    tasklet_schedule(&dev->tasklet);

    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    int irq, ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    spin_lock_init(&dev->lock);
    init_waitqueue_head(&dev->rx_wait);
    tasklet_setup(&dev->tasklet, rx_tasklet_func);

    dev->regs = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(dev->regs))
        return PTR_ERR(dev->regs);

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
                           0, "mydev", dev);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, dev);
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    struct my_device *dev = platform_get_drvdata(pdev);

    /* Disable hardware interrupts first */
    writel(0, dev->regs + IRQ_CTRL);

    /* Kill tasklet - waits for completion */
    tasklet_kill(&dev->tasklet);

    return 0;
}

Tasklet States

stateDiagram-v2
    [*] --> Idle: tasklet_setup()
    Idle --> Scheduled: tasklet_schedule()
    Scheduled --> Running: softirq runs
    Running --> Idle: callback returns
    Running --> Scheduled: tasklet_schedule() during callback
    Idle --> Disabled: tasklet_disable()
    Disabled --> Idle: tasklet_enable()
    Scheduled --> Dead: tasklet_kill()
    Idle --> Dead: tasklet_kill()
    Dead --> [*]

Tasklet vs Other Mechanisms

Tasklet vs Workqueue

Aspect Tasklet Workqueue
Context Softirq Process
Can sleep No Yes
Concurrency Single CPU Multi-CPU
Use case Simple, fast work Complex processing

Tasklet vs Threaded IRQ

Aspect Tasklet Threaded IRQ
Setup Manual Automatic
Context Softirq Thread
Can sleep No Yes
Preferred Legacy New drivers

Best Practices

Do

/* DO: Keep tasklet work minimal */
static void good_tasklet(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);

    /* Quick data processing */
    process_data(dev);

    /* Wake waiters */
    wake_up(&dev->waitq);
}

/* DO: Use spinlocks for shared data */
static void good_tasklet(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);
    unsigned long flags;

    spin_lock_irqsave(&dev->lock, flags);
    /* Access shared data */
    spin_unlock_irqrestore(&dev->lock, flags);
}

/* DO: Kill tasklet on removal */
static int my_remove(struct platform_device *pdev)
{
    struct my_device *dev = platform_get_drvdata(pdev);
    tasklet_kill(&dev->tasklet);
    return 0;
}

Don’t

/* DON'T: Sleep in tasklet */
static void bad_tasklet(struct tasklet_struct *t)
{
    msleep(10);                          /* BAD! */
    mutex_lock(&mutex);                  /* BAD! */
    kmalloc(size, GFP_KERNEL);           /* BAD! */
}

/* DON'T: Do heavy work in tasklet */
static void bad_tasklet(struct tasklet_struct *t)
{
    for (int i = 0; i < 1000000; i++)    /* BAD! */
        heavy_computation();
}

/* DON'T: Forget to kill on module exit */
static int bad_remove(struct platform_device *pdev)
{
    /* Tasklet might still be scheduled! */
    return 0;
}

High Priority Tasklets

For time-critical work:

/* Normal priority */
tasklet_schedule(&dev->tasklet);

/* High priority - runs before normal tasklets */
tasklet_hi_schedule(&dev->tasklet);

High-priority tasklets are used sparingly for:

  • Time-critical data (e.g., audio samples)
  • Protocol timing requirements

Disabling Tasklets

/* Disable for critical section */
tasklet_disable(&dev->tasklet);

/* Do something that conflicts with tasklet */
update_hardware_config(dev);

/* Re-enable */
tasklet_enable(&dev->tasklet);

tasklet_disable() is nestable - must call tasklet_enable() the same number of times.

Migration Note

Tasklets are being phased out in favor of threaded IRQs. For new code:

/* Old way: tasklet */
tasklet_setup(&dev->tasklet, my_tasklet_func);
request_irq(irq, handler, 0, "dev", dev);

/* Preferred: threaded IRQ */
request_threaded_irq(irq, hardirq_handler, threaded_handler,
                     IRQF_ONESHOT, "dev", dev);

Summary

  • Tasklets run in softirq context, cannot sleep
  • Use tasklet_setup() to initialize
  • Use tasklet_schedule() to queue execution
  • Always call tasklet_kill() on driver removal
  • Prefer threaded IRQs for new drivers
  • Same tasklet never runs concurrently on multiple CPUs

Next

Learn about threaded IRQs, the preferred method for modern drivers.


Back to top

Linux Driver Development Guide is a community resource for learning kernel driver development. Not affiliated with the Linux Foundation. Content provided for educational purposes.

This site uses Just the Docs, a documentation theme for Jekyll.