Fork Me on GitHub

源码开放学ARM

LASO - Learn ARM with Source Open

首页         目录索引         资料下载         代码下载         联系作者        
下载PDF打印版本

中断的下半部处理

Tasklet (小任务)

Tasklet 机制是中断处理下半部分最常用的一种方法,其使用也是非常简单的。

一个使用tasklet的中断程序首先会通过执行中断处理程序来快速完成上半部分的工作, 接着通过调度 tasklet 使得下半部分的工作得以完成。但是下半部分何时执行则属于内核的工作。

Tasklet 定义在 linux/include/interrupt.h 实现在 kernel/softirq.c

struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};

使用举例

#include <linux/module.h>	// module_init
#include <asm/io.h>		// ioremap
#include <linux/fs.h>		// file_operations
#include <asm/uaccess.h>	// copy_from_user
#include <linux/sched.h>	// wait_queue 

#include <mach/gpio.h>		// S5PV210_GPH2
#include <mach/regs-gpio.h>
#include <linux/interrupt.h>	// requst_irq
#include <linux/irq.h>		// IRQ_TYPE_EDGE_BOTH
#include <asm/thread_info.h>		// IRQ_TYPE_EDGE_BOTH


MODULE_LICENSE("GPL");

struct tasklet_struct mytasklet;

void do_sth(unsigned long data);

irqreturn_t btn_irq_handler(int irq, void * dev_id)
{
	int local = 100;

	printk("<irq> irq = %d \n", irq);	
	
	printk("<irq> &dev = %p \n", &dev_id);	
	printk("<irq> &irq = %p \n", &irq);	
	printk("<irq> &local = %p \n", &local);	

	printk("current = %p \n", current);	
	printk("current = %s \n", current->comm);	
	printk("current pid = %d \n", current->pid);	

	tasklet_schedule(&mytasklet);
	tasklet_schedule(&mytasklet);
//	do_sth(100);

	printk("<irq> = %d \n", irq);	

	return IRQ_HANDLED;
}

void do_sth(unsigned long data)
{
	volatile int i;
	int local = 100;

	printk("\tbegin do_sth...\n");

	printk("\t<do> &data = %p \n", &data);	
	printk("\t<do> &i = %p \n", &i );	
	printk("\t<do_sth> &local = %p \n", &local);	

	printk("\t<do> current = %p \n", current);	
	printk("\t<do> current = %s \n", current->comm);	
	printk("\t<do> current pid = %d \n", current->pid);	

	for (i = 0; i < 1000000000; i++)
		;

	printk("\tend do_sth...\n");

	return;
}

static int btn_drv_init(void)
{
	int err;
	unsigned long data = 100;

	int * p = ((int)&data) & 0xFFFFE000;

	struct thread_info * pt = (struct thread_info *)p;

	printk("btn init \n");

	printk("\t<do> &data = %p \n", &data);	
	printk("p = %p\n", p);

	printk("pt -> current = %p\n", pt->task);

	printk("\t<do> current = %p \n", current);	


	tasklet_init(&mytasklet, do_sth, data);

	// see include/linux/irq.h for more types
	//err = request_irq(irq, btn_irq_handler, IRQ_TYPE_EDGE_BOTH,
	err = request_irq(160, btn_irq_handler, IRQ_TYPE_EDGE_FALLING,
			"btn0", NULL);
	if (err)
	{
		printk("request irq 0 failed!\n");
	}

	err = request_irq(161, btn_irq_handler, IRQ_TYPE_EDGE_FALLING,
			"btn1", NULL);
	if (err)
	{
		printk("request irq 1 failed!\n");
	}


	printk("request irq ok! err = %d\n", err);

	return 0;
}

static void btn_drv_exit(void)
{
	printk("btn exit \n");

	free_irq(160, NULL);
	free_irq(161, NULL);

	return;
}

module_init(btn_drv_init);
module_exit(btn_drv_exit);

工作队列

工作队列(work queue) 是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以 在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。 那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。 工作、工作队列和工作者线程 如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。

* 表示工作的数据结构

work_struct 定义在 linux/workqueue.h 中,实现在 kernel/workqueue.c 中。

work_struct 结构表示如下:

struct work_struct{
	unsigned long pending; /* 这个工作正在等待处理吗?*/
	struct list_head entry; /* 连接所有工作的链表 */
	void (*func) (void *); /* 要执行的函数 */
	void *data; /* 传递给函数的参数 */
	void *wq_data; /* 内部使用 */
	struct timer_list timer; /* 延迟的工作队列所用到的定时器 */
};

这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

* 创建推后的工作

要使用工作队列,首先要做的是创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地建该结构:

DECLARE_WORK(name, void (*func) (void *), void *data);

这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。同样,也可以在运行时通过指针创建一个工作:

INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data);

这会动态地初始化一个由work指向的工作。

* 工作队列中待执行的函数

工作队列待执行的函数原型是:

void work_handler(void *data)

这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断,并且不持有任何锁。如果需要,函数可以睡眠。需要注意的是, 尽管该函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在系统调用发生时,内核会代表用户空间的进程运 行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

* 对工作进行调度

现在工作已经被创建,我们可以调度它了。想要把给定工作的待处理函数提交给缺省的events工作线程,只需调用

schedule_work(&work);

work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。 有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度它在指定的时间执行:

schedule_delayed_work(&work, delay);

这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。

以上实现在 kernel/workqueue.c

工作队列的简单应用

#include <linux/module.h>
#include <linux/init.h>
#include <linux/workqueue.h>

static struct workqueue_struct *queue = NULL;
static struct work_struct work;

static void work_handler(struct work_struct *data)
{
        printk(KERN_ALERT "work handler function.\n");
}

static int __init test_init(void)
{
        queue = create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/
        if (!queue)
                goto err;

        INIT_WORK(&work, work_handler);
        schedule_work(&work);

        return 0;
err:
        return -1;
}

static void __exit test_exit(void)
{
        destroy_workqueue(queue);
}
MODULE_LICENSE("GPL");
module_init(test_init);
module_exit(test_exit);

内核定时器

timer_list 定义在 linux/timer.h 中,实现在 kernel/timer.c 中,如下:

12 struct timer_list {
13         /*
14          * All fields that change during normal runtime grouped to the
15          * same cacheline
16          */
17         struct list_head entry;
18         unsigned long expires;
19         struct tvec_base *base;
20 
21         void (*function)(unsigned long);
22         unsigned long data;
23 
24         int slack;
25 
26 #ifdef CONFIG_TIMER_STATS
27         int start_pid;
28         void *start_site;
29         char start_comm[16];
30 #endif
31 #ifdef CONFIG_LOCKDEP
32         struct lockdep_map lockdep_map;
33 #endif
34 };

定义在 linux/include/timer.h 实现在 kernel/timer.c

#include <linux/init.h>
#include <linux/module.h>
#include <asm/current.h> 
#include <linux/sched.h>

MODULE_LICENSE("GPL");

extern struct task_struct * current;

struct timer_list mytimer;

void timer_handler(unsigned long arg)
{
	printk("timer arg = %d\n", arg);

	// 启动下一次 timer 
	mod_timer(&mytimer, iffies + HZ * 5);
	
	return;
}

static int __init akae_init(void)
{
	int local = 0;
	printk("module name is %s\n", KBUILD_MODNAME);
	printk("akae_init at %p\n", akae_init);
	printk("local at %p\n", &local);
	printk("jiffies at %p\n", &jiffies);
	printk("jiffies is %ld\n", jiffies);
	printk("The process is \"%s\" (pid %i)\n", current->comm, current->pid);

	init_timer(&mytimer);

	printk("HZ = %d\n", HZ);
	mytimer.expires = jiffies + HZ * 5;
	mytimer.function = timer_handler;
	mytimer.data = 100;

	add_timer(&mytimer);

	printk("timer ok!\n");

	return 0;
}

static void __exit akae_exit(void)
{
	printk("module liming exit\n");
	printk("akae_exit at %p\n", akae_exit);

	del_timer_sync(&mytimer);

	return;
}

module_init(akae_init);
module_exit(akae_exit);

等待队列

声明和调用接口

在 Linux 中, 一个等待队列由一个”等待队列头”来管理, 一个 wait_queue_head_t 类型的结构, 定义在 linux/wait.h 中,实现在 kernel/wait.c .

一个等待队列头可被定义和初始化, 使用静态初始化方法:

DECLARE_WAIT_QUEUE_HEAD(name); 

或者动态地, 如下:

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

当一个进程睡眠, 它这样做以期望某些条件在以后会成真. 如我们之前注意到的, 任何睡眠的进程必须在它再次 醒来时检查来确保它在等待的条件真正为真. Linux 内核中睡眠的最简单方式是一个宏定义, 称为 wait_event(有几个变体); 它结合了处理睡眠的细节和进程在等待的条件的检查.

wait_event 的形式是:

wait_event(queue, condition)
wait_event_interruptible(queue, condition)	

基本的唤醒睡眠进程的函数称为 wake_up. 它有几个形式(但是我们现在只看其中 2 个):

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

wake_up 唤醒所有的在给定队列上等待的进程(尽管这个情形比那个要复杂一些, 如同我们之后将见到的). 其他 的形式(wake_up_interruptible)限制它自己到处理一个可中断的睡眠. 通常, 这 2 个是不用区分的(如果你使用可 中断的睡眠);

通常如果你使用 wait_event , 则用 wake_up 唤醒。

如果你使用 wait_event_interruptible,则用 wake_up_interruptible 唤醒。

范例代码

实现一个有简单行为的设备:任何试图从这个设备读取的进程都被置为睡眠. 无论何时一个进程写这个设备, 所有的睡眠进程被唤醒. 这个行为由下面的 read 和 write 方法实现:

static DECLARE_WAIT_QUEUE_HEAD(wq);
static int flag = 0;

ssize_t sleepy_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
        printk(KERN_DEBUG "process %i (%s) going to sleep\n",
               current->pid, current->comm);
	       
        wait_event_interruptible(wq, flag != 0);
        flag = 0;
        printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
	
        return 0; /* EOF */
}

ssize_t sleepy_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
        printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
               current->pid, current->comm);
	       
        flag = 1;
        wake_up_interruptible(&wq);
	
        return count; /* succeed, to avoid retrial */
}

注意这个例子里 flag 变量的使用. 因为 wait_event_interruptible 检查一个必须变为真的条件, 我们使用 flag 来创建那个条件.

上一节 | 目录索引 | 下一节

blog comments powered by Disqus