当前位置: 首页 > 工具软件 > Workq > 使用案例 >

softirq/tasklet/workq

曹和正
2023-12-01

什么是上下文切换

上下文切换,有时也称为进程切换或任务切换,是CPU中央处理单元)从一个进程,或线程切换到另一个进程或线程。

进程,有时也称为任务,是程序的执行实例。在 Linux 中,线程是轻量级进程,可以并行运行,并与其父进程(即创建它们的进程)共享地址空间和其他资源。

上下文是指CPU寄存器和程序计数器在任何时间点的内容。寄存器是 CPU 内部的少量,非常快的内存,用于通过提供对常用值的快速访问,来加速计算机程序的执行,通常是计算的中间值。程序计数器是一种专用寄存器,用于指示 CPU 在其指令序列中的位置,并根据特定系统保存正在执行的指令的地址,或要执行的下一条指令的地址。

上下文切换可以更详细地描述为操作系统的核心对CPU上的进程(包括线程)执行以下活动:

  • 暂停一个进程的进程,并存储内存中某个进程的CPU状态(即上下文)
  • 从内存中检索下一个进程的上下文,并将其恢复到CPU的寄存器中
  • 返回到程序计数器指示的位置(即,返回到进程被中断的代码行)以恢复进程。

上下文切换有时被描述为内核暂停 CPU 上一个进程的执行,并恢复先前暂停的某个其他进程的执行。

上下文切换和模式切换

上下文切换只能在内核模式下发生。内核模式是CPU的一种特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序,包括应用程序,最初在用户模式下运行,但它们可以通过系统调用,运行部分内核代码。系统调用是类 Unix 操作系统中活动进程(即当前在 CPU 中运行的进程)对内核执行的服务(例如输入/输出 (I/O) 或进程创建)的请求(即,创建一个新进程)。 I/O 可以定义为进出 CPU 和主存储器(即 RAM)组合的任何信息移动,即该组合与计算机用户之间的通信(例如,通过键盘或鼠标),其存储设备(例如,磁盘或磁带驱动器)或其他计算机。

这两种模式在类 Unix 操作系统中的存在,意味着当系统调用导致 CPU 切换到内核模式时,类似但更简单的操作是必要的。这被称为模式切换,而不是上下文切换,因为它不会更改当前进程。

上下文切换是多任务操作系统的基本特征。多任务操作系统是多个进程在单个 CPU 上看似同时执行,且互不干扰的操作系统。这种并发错觉是通过快速连续(每秒数十或数百次)发生的上下文切换来实现的。这些上下文切换的发生是由于进程自愿放弃其在 CPU 中的时间,或调度程序在进程用完其CPU时间片时进行切换。

上下文切换也可以作为硬件中断的结果发生,硬件中断是从硬件设备(例如键盘、鼠标、调制解调器或系统时钟)到内核的信号,表明事件(例如,按键、鼠标移动)或来自网络连接的数据到达)发生。

Intel 80386 和更高版本的 CPU 包含对上下文切换的硬件支持。然而,大多数现代操作系统执行软件上下文切换,它可以在任何 CPU 上使用,而不是硬件上下文切换,以试图获得改进的性能。软件上下文切换首先在 Linux 中,为具有 2.4 内核的 Intel 兼容处理器实现。

软件上下文切换的一个主要优点是,虽然硬件机制保存了几乎所有的 CPU 状态,但软件可以更有选择性,只保存实际需要保存和重新加载的部分。然而,对于这在提高上下文切换效率方面究竟有多重要,存在一些问题。它的拥护者还声称,软件上下文切换允许改进切换代码的​​可能性,从而进一步提高效率,并允许更好地控制正在加载的数据的有效性。

运行上下文

在任何时候,系统中的每个 CPU 都可以:

  • 与任何进程无关,服务于硬件中断
  • 与任何进程都不相关联,服务于软中断或小任务
  • 与进程(用户上下文)相关联,在内核空间中运行

在用户空间运行进程

这之间有一个排序。最底层的两个可以互相抢占,但在其之上是一个严格的等级制度:每个只能被它上面的人抢占。
当一个软中断在 CPU 上运行时,没有其他软中断会抢占它,但硬件中断可以。但是,系统中的任何其他 CPU 都是独立执行的。

我们将看到用户上下文可以通过多种方式阻止中断,从而成为真正不可抢占的。

硬件中断(硬 IRQ)

计时器滴答声、网卡和键盘是可随时产生中断的真实硬件示例。内核运行中断处理程序,为硬件提供服务。内核保证此处理程序永远不会重新进入:如果相同的中断到达,则将其排队(或丢弃)。因为它禁用中断,所以这个处理程序必须很快:它经常简单地确认中断,标记一个“软件中断”以供执行并退出。

您可以判断您处于硬件中断状态,因为 in_irq() 返回 true。

软件中断/Tasklet上下文

  • 系统调用即将返回用户空间。
  • 硬件中断处理程序退出时,

任何标记为挂起(通常由硬件中断)的“软件中断”都会运行(kernel/softirq.c)。

许多真正的中断处理工作都在这里完成。在过渡到 SMP 的早期,只有“下半部分”(BH),它们没有利用多个CPU。

include/linux/interrupt.h 列出了不同的软中断。一个非常重要的软中断是,计时器软中断(包括/linux/timer.h)。

软中断通常很难处理,因为同一个软中断会在多个CPU上同时运行。出于这个原因,tasklet (include/linux/interrupt.h) 更常用:它们是可动态注册的(意味着您可以拥有任意数量的),并且它们还保证任何 tasklet 只能在一个 CPU 上运行任何时候,尽管不同的小任务可以同时运行。

Softirqs

SoftirqIndex (priority)Description
HI_SOFTIRQ0处理高优先级的tasklet
TIMER_SOFTIRQ1与定时器中断相关的tasklet
NET_TX_SOFTIRQ2将数据包发送到网卡
NET_RX_SOFTIRQ3从网卡接收数据包
SCSI_SOFTIRQ4SCSI 命令的中断后处理
TASKLET_SOFTIRQ5处理常规tasklet

HI_SOFTIRQ/TASKLET_SOFTIRQ

void __init softirq_init(void)
{
        int cpu;

        for_each_possible_cpu(cpu) {
                per_cpu(tasklet_vec, cpu).tail =
                        &per_cpu(tasklet_vec, cpu).head;
                per_cpu(tasklet_hi_vec, cpu).tail =
                        &per_cpu(tasklet_hi_vec, cpu).head;
        }

        open_softirq(TASKLET_SOFTIRQ, tasklet_action);
        open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

TIMER_SOFTIRQ

计时器APIs

void add_timer(struct timer_list *timer)
void add_timer_on(struct timer_list *timer, int cpu)
int mod_timer(struct timer_list *timer, unsigned long expires)
int timer_reduce(struct timer_list *timer, unsigned long expires)
int del_timer_sync(struct timer_list *timer)
int try_to_del_timer_sync(struct timer_list *timer)
int del_timer(struct timer_list *timer)

在I2C驱动器中的应用

这是PNX010x/PNX4008板上的I2C驱动器的计时器应用。

计时器超时函数

static void i2c_pnx_timeout(struct timer_list *t)
{
...
}

定义计时器

struct i2c_pnx_mif {
        int                     ret;            /* Return value */
        int                     mode;           /* Interface mode */
        struct completion       complete;       /* I/O completion */
        struct timer_list       timer;          /* Timeout */
        u8 *                    buf;            /* Data buffer */
        int                     len;            /* Length of data buffer */
        int                     order;          /* RX Bytes to order via TX */
};

struct i2c_pnx_algo_data {
        void __iomem            *ioaddr;
        struct i2c_pnx_mif      mif;
        int                     last;
        struct clk              *clk;
        struct i2c_adapter      adapter;
        int                     irq;
        u32                     timeout;
};

static int i2c_pnx_probe(struct platform_device *pdev)
{
       struct i2c_pnx_algo_data *alg_data;
...
       alg_data = devm_kzalloc(&pdev->dev, sizeof(*alg_data), GFP_KERNEL);
...
       timer_setup(&alg_data->mif.timer, i2c_pnx_timeout, 0);
...
}

登记计时器

static inline void i2c_pnx_arm_timer(struct i2c_pnx_algo_data *alg_data)
{
        struct timer_list *timer = &alg_data->mif.timer;
        unsigned long expires = msecs_to_jiffies(alg_data->timeout);

        if (expires <= 1)
                expires = 2;

        del_timer_sync(timer);

        dev_dbg(&alg_data->adapter.dev, "Timer armed at %lu plus %lu jiffies.\n",
                jiffies, expires);

        timer->expires = jiffies + expires;

        add_timer(timer);
}

NET_TX_SOFTIRQ/NET_RX_SOFTIRQ

tasklet

在一个设备驱动程序中使用一个 tasklet,首先,应该分配一个新的 tasklet_struct 数据结构,而且通过调用 tasklet_init() ,初始化它;此函数的参数包括,tasklet描述符的地址,tasklet函数的地址,以及tasklet函数可选的整数参数。

通过调用 tasklet_disable_nosync() ,或 tasklet_disable(),选择性地禁用 tasklet。这两个函数都增加 tasklet 描述符的计数字段。但tasklet_disable()在tasklet函数已经运行的实例终止之前,不会返回。使用 tasklet_enable(),重新启用tasklet。

根据tasklet的优先级,调用 tasklet_schedule() ,或tasklet_hi_schedule() 函数激活 tasklet。这两个功能非常相似;他们每个人都执行以下操作行动:

atmel serial 代码

定义tasklet

struct atmel_uart_port {
...
struct tasklet_struct   tasklet_rx;
struct tasklet_struct   tasklet_tx;
...
};

tasklet函数

static void atmel_tasklet_rx_func(unsigned long data)
{
        struct uart_port *port = (struct uart_port *)data;
        struct atmel_uart_port *atmel_port = to_atmel_uart_port(port);

        /* The interrupt handler does not take the lock */
        spin_lock(&port->lock);
        atmel_port->schedule_rx(port);
        spin_unlock(&port->lock);
}

static void atmel_tasklet_tx_func(unsigned long data)
{
        struct uart_port *port = (struct uart_port *)data;
        struct atmel_uart_port *atmel_port = to_atmel_uart_port(port);

        /* The interrupt handler does not take the lock */
        spin_lock(&port->lock);
        atmel_port->schedule_tx(port);
        spin_unlock(&port->lock);
}

初始化

static int atmel_startup(struct uart_port *port)
{
...
tasklet_init(&atmel_port->tasklet_rx, atmel_tasklet_rx_func,
                       (unsigned long)port);
tasklet_init(&atmel_port->tasklet_tx, atmel_tasklet_tx_func,
                        (unsigned long)port);
...
}

规划运行


static void atmel_tasklet_schedule(struct atmel_uart_port *atmel_port,
                                   struct tasklet_struct *t)
{
        if (!atomic_read(&atmel_port->tasklet_shutdown))
                tasklet_schedule(t);
}


static void atmel_rx_chars(struct uart_port *port)
{
...
 atmel_tasklet_schedule(atmel_port, &atmel_port->tasklet_rx);
...
}

static void atmel_complete_tx_dma(void *arg)
{
...
        if (!uart_circ_empty(xmit))
                atmel_tasklet_schedule(atmel_port, &atmel_port->tasklet_tx);
...
}

终止tasklet

static void atmel_shutdown(struct uart_port *port)
{
...
        /* Prevent spurious interrupts from scheduling the tasklet */
        atomic_inc(&atmel_port->tasklet_shutdown);
 ...       
        /*
         * Clear out any scheduled tasklets before
         * we destroy the buffers
         */
        tasklet_kill(&atmel_port->tasklet_rx);
        tasklet_kill(&atmel_port->tasklet_tx);
...
}

tasklet同步

如果软中断与用户上下文共享数据,则有两个问题。

  • 首先,当前用户上下文可以被软中断中断。
  • 其次,临界区可以从另一个CPU进入。

这是使用 spin_lock_bh() (include/linux/spinlock.h) 的地方。它禁用该CPU上的软中断,然后获取锁。 spin_unlock_bh() 反之。

也可以在此处使用 spin_lock_irq() 或 spin_lock_irqsave(),它们也可以停止硬件中断。

这对UP也很有效:自旋锁消失了,这个宏简单地变成 local_bh_disable() (include/linux/interrupt.h),它保护你免受正在运行的软中断。

工作队列workq

工作队列不是像tasklet那样提供一次性延迟方案,而是一种通用的延迟机制,其中工作队列的处理程序函数可以睡眠,在tasklet模型中不可能。工作队列的延迟可能比tasklet更高,但包含更丰富的工作延迟 API。

工作队列提供了一种将功能推迟到下半部分的通用方法。核心是工作队列(struct workqueue_struct),它是放置工作的结构。工作由 work_struct 结构表示,该结构标识要延迟的工作和要使用的延迟函数。

  • 工作队列由特殊内核线程执行,该线程称为工作线程。
  • 工作队列中的函数在进程上下文中运行。在进程上下文中运行是执行可以阻塞的函数的唯一方法,因为,在中断上下文中,不会发生进程切换。
  • 工作队列中的函数不可以访问进程的用户态地址空间。在工作队列中的函数是由内​​核线程执行的,因此没有用户模式地址空间使用权。

数据结构

struct workqueue_struct {
        struct list_head        pwqs;           /* WR: all pwqs of this wq */
        struct list_head        list;           /* PR: list of all workqueues */
        struct mutex            mutex;          /* protects this wq */
        int                     work_color;     /* WQ: current work color */
        int                     flush_color;    /* WQ: current flush color */
        atomic_t                nr_pwqs_to_flush; /* flush in progress */
        struct wq_flusher       *first_flusher; /* WQ: first flusher */
        struct list_head        flusher_queue;  /* WQ: flush waiters */
        struct list_head        flusher_overflow; /* WQ: flush overflow list */
        struct list_head        maydays;        /* MD: pwqs requesting rescue */
        struct worker           *rescuer;       /* I: rescue worker */
        int                     nr_drainers;    /* WQ: drain in progress */
        int                     saved_max_active; /* WQ: saved pwq max_active */
        struct workqueue_attrs  *unbound_attrs; /* PW: only for unbound wqs */
        struct pool_workqueue   *dfl_pwq;       /* PW: only for unbound wqs */
#ifdef CONFIG_SYSFS
        struct wq_device        *wq_dev;        /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
        struct lockdep_map      lockdep_map;
#endif
        char                    name[WQ_NAME_LEN]; /* I: workqueue name */
        /*
         * Destruction of workqueue_struct is sched-RCU protected to allow
         * walking the workqueues list without grabbing wq_pool_mutex.
         * This is used to dump all workqueues from sysrq.
         */
        struct rcu_head         rcu;
        /* hot fields used during command issue, aligned to cacheline */
        unsigned int            flags ____cacheline_aligned; /* WQ: WQ_* flags */
        struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
        struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */
};

struct pool_workqueue {
        struct worker_pool      *pool;          /* I: the associated pool */
        struct workqueue_struct *wq;            /* I: the owning workqueue */
        int                     work_color;     /* L: current color */
        int                     flush_color;    /* L: flushing color */
        int                     refcnt;         /* L: reference count */
        int                     nr_in_flight[WORK_NR_COLORS];             /* L: nr of in_flight works */
        int                     nr_active;      /* L: nr of active works */
        int                     max_active;     /* L: max active works */
        struct list_head        delayed_works;  /* L: delayed works */
        struct list_head        pwqs_node;      /* WR: node on wq->pwqs */
        struct list_head        mayday_node;    /* MD: node on wq->maydays */
        /*
         * Release of unbound pwq is punted to system_wq.  See put_pwq()
         * and pwq_unbound_release_workfn() for details.  pool_workqueue
         * itself is also sched-RCU protected so that the first pwq can be
         * determined without grabbing wq->mutex.
         */
        struct work_struct      unbound_release_work;
        struct rcu_head         rcu;
} __aligned(1 << WORK_STRUCT_FLAG_BITS);

工作队列APIs

alloc_workqueue(fmt, flags, max_active, args…)
INIT_WORK(_work, _func)
INIT_DELAYED_WORK(_work, _func)
static inline bool queue_work(struct workqueue_struct *wq, struct work_struct *work)
bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work)
bool cancel_work_sync(struct work_struct *work)
bool cancel_delayed_work(struct delayed_work *dwork)

应用

sdio 驱动程序

结构定义

struct if_sdio_card {
...
        struct workqueue_struct *workqueue;
        struct work_struct      packet_worker;
...
};

工作队列函数

static void if_sdio_host_to_card_worker(struct work_struct *work)
{
        struct if_sdio_card *card;
        struct if_sdio_packet *packet;
        int ret;
        unsigned long flags;

        card = container_of(work, struct if_sdio_card, packet_worker);

        while (1) {
                spin_lock_irqsave(&card->lock, flags);
                packet = card->packets;
                if (packet)
                        card->packets = packet->next;
                spin_unlock_irqrestore(&card->lock, flags);

                if (!packet)
                        break;

                sdio_claim_host(card->func);

                ret = if_sdio_wait_status(card, IF_SDIO_IO_RDY);
                if (ret == 0) {
                        ret = sdio_writesb(card->func, card->ioport,
                                           packet->buffer, packet->nb);
                }

                if (ret)
                        pr_err("error %d sending packet to firmware\n", ret);

                sdio_release_host(card->func);

                kfree(packet);
        }
}

初始化

static int if_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id)
{
...
    card->workqueue = alloc_workqueue("libertas_sdio", WQ_MEM_RECLAIM, 0);
    INIT_WORK(&card->packet_worker, if_sdio_host_to_card_worker);
...
}

规划工作队列运行

static int if_sdio_host_to_card(struct lbs_private *priv,
                u8 type, u8 *buf, u16 nb)
{
...
queue_work(card->workqueue, &card->packet_worker);
...
}
 类似资料: