04_中断与时钟

1、中断与定时器

根据中断的来源, 中断可分为内部中断外部中断, 内部中断的中断源来自CPU内部(软件中断指令、 溢出、 除法错误等, 例如, 操作系统从用户态切换到内核态需借助CPU内部的软件中断) , 外部中断的中断源来自CPU外部, 由外设提出请求。

根据中断是否可以屏蔽, 中断可分为可屏蔽中断不可屏蔽中断(NMI)

根据中断入口跳转方法的不同, 中断可分为向量中断非向量中断。 采用向量中断的CPU通常为不同的中断分配不同的中断号, 当检测到某中断号的中断到来后, 就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。 非向量中断的多个中断共享一个入口地址, 进入该入口地址后, 再通过软件判断中断标志来识别具体是哪个中断。 也就是说, 向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。

非向量中断服务程序代码如下:

irq_handler()
{
	...
	int int_src = read_int_status(); /* 读硬件的中断相关寄存器 */
	switch (int_src) { /* 判断中断源 */
	case DEV_A:
		dev_a_handler();
		break;
	case DEV_B:
		dev_b_handler();
		break;
	...
	default:
		break;
	}
	...
}

定时器在硬件上也依赖中断来实现, 图1示为典型的嵌入式微处理器内可编程间隔定时器(PIT) 的工作原理, 它接收一个时钟输入, 当时钟脉冲到来时, 将目前计数值增1并与预先设置的计数值(计数目标) 比较, 若相等, 证明计数周期满, 并产生定时器中断且复位目前计数值。

在ARM多核处理器里最常用的中断控制器是GIC(Generic Interrupt Controller),它支持3种类型的中断,如图:

SGI(Software Generated Interrupt) : 软件产生的中断, 可以用于多核的核间通信, 一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。 多核调度用的IPI_WAKEUP、 IPI_TIMER、

IPI_RESCHEDULE、 IPI_CALL_FUNC、 IPI_CALL_FUNC_SINGLE、 IPI_CPU_STOP、 IPI_IRQ_WORK、IPI_COMPLETION都是由SGI产生的。

PPI(Private Peripheral Interrupt) : 某个CPU私有外设的中断, 这类外设的中断只能发给绑定的那个CPU。

SPI(Shared Peripheral Interrupt) : 共享外设的中断, 这类外设的中断可以路由到任何一个CPU。

对于SPI类型的中断, 内核可以通过如下API设定中断触发的CPU核:

extern int irq_set_affinity (unsigned int irq, const struct cpumask *m);

中断都是在CPU0上产生的, 比如, 我们可以通过如下代码把中断irq设定到CPU i上去

irq_set_affinity(irq, cpumask_of(i));

2、中断处理程序架构

为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点, Linux将中断处理程序分解为两个半部: 顶半部(Top Half) 和底半部(Bottom Half)

3、中断编程

中断申请和释放

申请

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,

const char *name, void *dev)

irq是要申请的硬件中断号。

handler是向系统登记的中断处理函数(顶半部) , 是一个回调函数, 中断发生时, 系统调用这个函数, dev参数将被传递给它。

irqflags是中断处理的属性, 可以指定中断的触发方式以及处理方式。 在触发方式方面, 可以是IRQF_TRIGGER_RISING、 IRQF_TRIGGER_FALLING、 IRQF_TRIGGER_HIGH、 IRQF_TRIGGER_LOW等。 在处理方式方面, 若设置了IRQF_SHARED, 则表示多个设备共享中断, dev是要传递给中断服务程序的私有数据, 一般设置为这个设备的设备结构体或者NULL。

request_irq() 返回0表示成功, 返回-EINVAL表示中断号无效或处理函数指针为NULL, 返回-EBUSY表示中断已经被占用且不能共享。

释放

void free_irq(unsigned int irq,void *dev_id);

tasklet

它的执行上下文是软中断, 执行时机通常是顶半部返回的时候。

使用tasklet作为底半部处理中断的设备驱动程序模板代码如下:

/* 定义tasklet和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);

/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
	...
}

/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
	...
	tasklet_schedule(&xxx_tasklet);
	...
}

/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
	...
	/* 申请中断 */
	result = request_irq(xxx_irq, xxx_interrupt,
	0, "xxx", NULL);
	...
	return IRQ_HANDLED;
}

/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

工作队列

工作队列的使用方法和tasklet非常相似, 但是工作队列的执行上下文是内核线程, 因此可以调度和睡眠。

使用工作队列处理中断底半部的设备驱动程序模板如下:

/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);

/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
	...
}

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
	...
	schedule_work(&xxx_wq);
	...
	return IRQ_HANDLED;
}

/* 设备驱动模块加载函数 */
int xxx_init(void)
{
	...
	/* 申请中断 */
	result = request_irq(xxx_irq, xxx_interrupt,
	0, "xxx", NULL);
	...
	/* 初始化工作队列 */
	INIT_WORK(&xxx_wq, xxx_do_work);
	...
}

/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

软中断

软中断(Softirq) 也是一种传统的底半部处理机制, 它的执行时机通常是顶半部返回的时候, tasklet是基于软中断实现的, 因此也运行于软中断上下文

在Linux内核中, 用softirq_action结构体表征一个软中断, 这个结构体包含软中断处理函数指针和传递给该函数的参数。 使用open_softirq() 函数可以注册软中断对应的处理函数, 而raise_softirq() 函数可以触发一个软中断。

硬中断、 软中断和信号的区别

硬中断是外部设备对CPU的中断, 软中断是中断底半部的一种处理机制, 而信号则是由内核(或其他进程) 对某个进程的中断。 在涉及系统调用的场合, 人们也常说通过软中断(例如ARM为swi) 陷入内核, 此时软中断的概念是指由软件指令引发的中断, 和我们这个地方说的softirq是两个完全不同的概念, 一个是software, 一个是soft。

实例: GPIO按键的中断

General-purpose input/output,通用型之输入输出

drivers/input/keyboard/gpio_keys.c是一个放之四海皆准的GPIO按键驱动, 为了让该驱动在特定的电路板上工作, 通常只需要修改arch/arm/mach-xxx下的板文件或者修改device tree对应的dts。 该驱动会为每个GPIO申请中断, 在gpio_keys_setup_key() 函数中进行。 注意最后一个参数bdata, 会被传入中断服务程序。

gpio_button_data结构体

struct gpio_button_data {
	const struct gpio_keys_button *button;
	struct input_dev *input;
	struct timer_list timer;
	struct work_struct work;
	unsigned int timer_debounce; /* in msecs */
	unsigned int irq;
	spinlock_t lock;
	bool disabled;
	bool key_pressed;
};

GPIO按键驱动中断申请:

static int gpio_keys_setup_key(struct platform_device *pdev,
 struct input_dev *input,
 struct gpio_button_data *bdata,
 const struct gpio_keys_button *button)
{
	 ...

	error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);
	 if (error < 0) {
		 dev_err(dev, "Unable to claim irq %d; error %d\n",
		 bdata->irq, error);
		 goto fail;
	 }
	 ...
}

request_any_context_irq() 会根据GPIO控制器本身的“上级”中断是否为threaded_irq来决定采用request_irq() 还是request_threaded_irq() 。 一组GPIO(如32个GPIO) 虽然每个都提供一个中断, 并且都有中断号, 但是在硬件上一组GPIO通常是嵌套在上一级的中断控制器上的一个中断。

GPIO按键驱动中断释放

static void gpio_remove_key(struct gpio_button_data *bdata)
{
	free_irq(bdata->irq, bdata);
	if (bdata->timer_debounce)
		del_timer_sync(&bdata->timer);
	cancel_work_sync(&bdata->work);
	if (gpio_is_valid(bdata->button->gpio))
		gpio_free(bdata->button->gpio);
}

GPIO按键驱动中断处理程序

没有明确地分为上下两个半部, 而只存在顶半部,

static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
	struct gpio_button_data *bdata = dev_id;
	BUG_ON(irq != bdata->irq);

	if (bdata->button->wakeup)
		pm_stay_awake(bdata->input->dev.parent);
	if (bdata->timer_debounce)
		mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(bdata->timer_debounce));
	else
		schedule_work(&bdata->work);

	return IRQ_HANDLED;
}

在GPIO按键驱动初始化的时候, 通过INIT_WORK(&bdata->work, gpio_keys_gpio_work_func) 初始化了bdata->work, 对应的处理函数是gpio_keys_gpio_work_func() , 代码如下

GPIO按键驱动的工作队列底半部

static void gpio_keys_gpio_work_func(struct work_struct *work)
{
	struct gpio_button_data *bdata =
	container_of(work, struct gpio_button_data, work);

	gpio_keys_gpio_report_event(bdata);

	if (bdata->button->wakeup)
		pm_relax(bdata->input->dev.parent);
}

4、中断共享

多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在, Linux支持这种中断共享。

中断共享的使用方法:

1) 共享中断的多个设备在申请中断时, 都应该使用IRQF_SHARED标志, 而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请, 或该中断虽然被申请了, 但是之前申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。

2) 尽管内核模块可访问的全局地址都可以作为request_irq(…, void*dev_id) 的最后一个参数dev_id, 但是设备结构体指针显然是可传入的最佳参数。

3) 在中断到来时, 会遍历执行共享此中断的所有中断处理程序, 直到某一个函数返回IRQ_HANDLED。 在中断处理程序顶半部中, 应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判断是否为本设备的中断, 若不是, 应迅速返回IRQ_NONE

共享中断编程模板

/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
	...
	int status = read_int_status(); /* 获知中断源 */
	if(!is_myint(dev_id,status)) /* 判断是否为本设备中断 */
		return IRQ_NONE; /* 不是本设备中断, 立即返回 */

	/* 是本设备中断, 进行处理 */
	...
	return IRQ_HANDLED; /* 返回IRQ_HANDLED表明中断已被处理 */
}

/* 设备驱动模块加载函数 */
int xxx_init(void)
{
	...
	/* 申请共享中断 */
	result = request_irq(sh_irq, xxx_interrupt,
	IRQF_SHARED, "xxx", xxx_dev);
	...
}

/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

5、内核定时器

软件意义上的定时器最终依赖硬件定时器来实现, 内核在时钟中断发生后检测各定时器是否到期, 到期后的定时器处理函数将作为软中断在底半部执行。 实质上, 时钟中断处理程序会唤起TIMER_SOFTIRQ软中断, 运行当前处理器上到期的所有定时器

1)timer_list

timer_list结构体的一个实例对应一个定时器,代码如下

struct timer_list {
	/*
	* All fields that change during normal runtime grouped to the
	* same cacheline
	*/
	struct list_head entry;
	unsigned long expires;
	struct tvec_base *base;

	void (*function)(unsigned long);
	unsigned long data;

	int slack;

#ifdef CONFIG_TIMER_STATS
	int start_pid;
	void *start_site;
	char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

当定时器期满后, 其中的function() 成员将被执行,

data成员则是传入其中的参数

expires则是定时器到期的时间(jiffies)

2)定时器

内核定时器使用模板

/* xxx设备结构体 */
struct xxx_dev {
	struct cdev cdev;
	...
	timer_list xxx_timer; /* 设备要使用的定时器 */
};

/* xxx驱动中的某函数 */
xxx_func1(…)
{
	struct xxx_dev *dev = filp->private_data;
	...
	/* 初始化定时器 */
	init_timer(&dev->xxx_timer);
	dev->xxx_timer.function = &xxx_do_timer;
	dev->xxx_timer.data = (unsigned long)dev;
	/* 设备结构体指针作为定时器处理函数参数 */
	dev->xxx_timer.expires = jiffies + delay;
	/* 添加(注册) 定时器 */
	add_timer(&dev->xxx_timer);
	...
}

/* xxx驱动中的某函数 */
xxx_func2(…)
{
	...
	/* 删除定时器 */
	del_timer (&dev->xxx_timer);
	...
}

/* 定时器处理函数 */
static void xxx_do_timer(unsigned long arg)
{
	struct xxx_device *dev = (struct xxx_device *)(arg);
	...
	/* 调度定时器再执行 */
	dev->xxx_timer.expires = jiffies + delay;
	add_timer(&dev->xxx_timer);
	...
}

内核高精度定时器(hrtimer) 使用模板

static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)
{
	...

	hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns));

	return HRTIMER_RESTART;
}

static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct imx_pcm_runtime_data *iprtd = runtime->private_data;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
	...
	hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns),
	HRTIMER_MODE_REL);
	...
}

static int snd_imx_open(struct snd_pcm_substream *substream)
{
	...
	hrtimer_init(&iprtd->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	iprtd->hrt.function = snd_hrtimer_callback;

	...
	return 0;
}
static int snd_imx_close(struct snd_pcm_substream *substream)
{
	...
	hrtimer_cancel(&iprtd->hrt);
	...
}

实例:秒字符设备

编写一个字符设备“second”(即“秒”) 的驱动, 它在被打开的时候初始化一个定时器并将其添加到内核定时器链表中, 每秒输出一次当前的jiffies(为此, 定时器处理函数中每次都要修改新的expires) , 整个程序如下

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define SECOND_MAJOR 248

static int second_major = SECOND_MAJOR;
module_param(second_major, int, S_IRUGO);

struct second_dev {
	struct cdev cdev;
	atomic_t counter;
	struct timer_list s_timer;
};

static struct second_dev *second_devp;

static void second_timer_handler(unsigned long arg)
{
	mod_timer(&second_devp->s_timer, jiffies + HZ); /* 触发下一次定时 */
	atomic_inc(&second_devp->counter); /* 增加秒计数 */

	printk(KERN_INFO "current jiffies is %ld\n", jiffies);
}

static int second_open(struct inode *inode, struct file *filp)
{
	init_timer(&second_devp->s_timer);
	second_devp->s_timer.function = &second_timer_handler;
	second_devp->s_timer.expires = jiffies + HZ;

	add_timer(&second_devp->s_timer);

	atomic_set(&second_devp->counter, 0); /* 初始化秒计数为0 */

	return 0;
}

static int second_release(struct inode *inode, struct file *filp)
{
	del_timer(&second_devp->s_timer);

	return 0;
}

static ssize_t second_read(struct file *filp, char __user * buf, size_t count, loff_t * ppos)
{
	int counter;

	counter = atomic_read(&second_devp->counter);
	if (put_user(counter, (int *)buf))/* 复制counter到userspace */
    	return -EFAULT;
	else
    	return sizeof(unsigned int);
}

static const struct file_operations second_fops = {
	.owner = THIS_MODULE,
	.open = second_open,
	.release = second_release,
	.read = second_read,
};

static void second_setup_cdev(struct second_dev *dev, int index)
{
	int err, devno = MKDEV(second_major, index);

	cdev_init(&dev->cdev, &second_fops);
	dev->cdev.owner = THIS_MODULE;
	err = cdev_add(&dev->cdev, devno, 1);
	if (err)
		printk(KERN_ERR "Failed to add second device\n");
}

static int __init second_init(void)
{
	int ret;
	dev_t devno = MKDEV(second_major, 0);

	if (second_major)
		ret = register_chrdev_region(devno, 1, "second");
	else {
		ret = alloc_chrdev_region(&devno, 0, 1, "second");
		second_major = MAJOR(devno);
	}
	if (ret < 0)
		return ret;

	second_devp = kzalloc(sizeof(*second_devp), GFP_KERNEL);
	if (!second_devp) {
		ret = -ENOMEM;
		goto fail_malloc;
	}

	second_setup_cdev(second_devp, 0);

	return 0;

fail_malloc:
	unregister_chrdev_region(devno, 1);
	return ret;
}
module_init(second_init);

static void __exit second_exit(void)
{
	cdev_del(&second_devp->cdev);
	kfree(second_devp);
	unregister_chrdev_region(MKDEV(second_major, 0), 1);
}
module_exit(second_exit);

MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
MODULE_LICENSE("GPL v2");

second设备用户空间测试程序

#include ...

main()
{
	int fd;
	int counter = 0;
	int old_counter = 0;

	/* 打开/dev/second设备文件 */
	fd = open("/dev/second", O_RDONLY);
	if (fd != - 1) {
		while (1) {
			read(fd,&counter, sizeof(unsigned int));/* 读目前经历的秒数 */
			if(counter!=old_counter) {
				printf("seconds after open /dev/second :%d\n",counter);
				old_counter = counter;
			}
		}
	} else {
		printf("Device open failure\n");
	}
}

运行second_test后, 内核将不断地输出目前的jiffies值:

应用程序将不断输出自/dec/second打开以后经历的秒数

3)延迟

短延时

void ndelay(unsigned long nsecs);

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

实现原理本质上是忙等待, 它根据CPU频率进行一定次数的循环。

长延时

睡着延迟无疑是比忙等待更好的方式, 睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用。 schedule_timeout() 可以使当前任务休眠至指定的jiffies之后再重新被调度执行,msleep() 和msleep_interruptible() 在本质上都是依靠包含了schedule_timeout() 的schedule_timeout_uninterruptible() 和schedule_timeout_interruptible() 来实现的, 如下

void msleep(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;

	while (timeout)
		timeout = schedule_timeout_uninterruptible(timeout);
}

unsigned long msleep_interruptible(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;

	while (timeout && !signal_pending(current))
		timeout = schedule_timeout_interruptible(timeout);
	return jiffies_to_msecs(timeout);
}

schedule_timeout() 的实现原理是向系统添加一个定时器, 在定时器处理函数中唤醒与参数对应的进程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值