linux驱动(第九课,中断,定时器)

我们首先看看中断的执行路径。
linux中,首先是__vectors_start,这是异常向量表的入口。
ExceptionHandler处理后,会跳转到IRQHandler,最后跳转到Callback.
在handle_irq_event_percpu函数中,会调用Callback。

irqreturn_t 
handle_irq_event_percpu(struct irq_desc* desc, struct irqaction* action)
{
	...
	do{
		res = action->handler(irq, action->dev_id);
		
		action = action->next;
	}while(action);
}

irqreturn_t handle_irq_event(struct irq_desc* desc)
{
	struct irqaction* action = desc->action;
	...
	ret = handle_irq_event_percpu(desc, action); 
	...
}

从中可以看出,两个重要的结构体。
struct irq_desc,还有struct irqaction。
与驱动密切相关的就是struct irqaction。

struct irqaction{
	struct irqaction* next;
	unsigned int irq;
	unsigned int flags;
	irq_handler_t handler;
	void* dev_id;
	
	irq_handler_t thread_fn;
	struct task_struct* thread;
	unsigned int thread_flags;
	unsigned int thread_mask;
};

从中可以看出,IRQACTION内嵌链节,所以可以形成单链表。
其中最关键的成员就是irq和handler。
当内核得到IRQNUM后,会遍历IRQACTION链表上的所有对象实体,并调用Callback。
在irqHandler中,驱动开发者需要判断中断是否是自己的设备所产生,如果是则进行后续处理,如果不是则直接返回。自然的,dev_id就成为了判据。

为了在ISR中停留尽量短的时间,linux将中断处理分为top half 和bottom half。
top half往往只是简单的清标,登记中断。登记中断,意味着将bottom half 挂接到该设备的BH执行队列中去。
BH完成中断处理的主体任务,而且BH还可以被新的中断所打断。而TH往往是不可中断的。

通过分析我们可以看出,要在驱动模块中支持中断处理,最重要的,就是构造IRQACTION对象实体,并挂接到对应的IRQNUM上。
内核提供了IRQAPI。


typedef irqreturn_t (*irq_handler_t)(int, void*);

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char* name, void* dev_id );
void free_irq(unsigned int, void* dev_id);

request_irq用来在内核中创建并填充一个IRQACTION对象实体,然后注册到内核中,最后使能对应的IRQ。
当中断发生后,最终调用的Callback,就是handler。
dev_id,用来区分不同的设备。共享中断,必须传入一个非NULL的指针,而非共享中断,则传入NULL。

来看一个简单的例子。

static irqreturn_t vser_handler(int irq, void* dev_id)
{
	...
	return IRQ_HANDLED;
}

static int __init vser_init(void)
{
	...
	ret = request_irq(167, vser_handler, IRQF_TRIGGER_HIGH | IRQF_SHARED, "vser_interrupt", &vsdev);
	...
	return ret; 
}
module_init(vser_init);

static void __exit vser_exit(void)
{
	...
	free_irq(167, &vser_dev);
	...
	return ;
}

注意,共享中断必须设置FLAG,IRQF_SHARED。而且必须传递dev_id。通常,传递的dev_id是DEV的句柄,即,结构体对象指针。

IRQHandler作为TH,应该快速完成。绝对要遵守的规则是,在IRQHandler中,不能使用进程调度。即,绝对禁止调用可能引起进程切换的函数,因为一旦IRQHandler所在的进程被切换,将不能再次被调度。

BH使用了work deferring机制。
我们可以有多种选择,例如,softirqs, tasklets, workqueues, threaded IRQs等。
需要注意的是,softirq 和 tasklet是工作在中断上下文(interrupt context)中的,所以是不可抢占的,自然的,不允许休眠。而workqueue和threaded irq是工作在进程上下文(process context)中的,所以是可抢占的,自然的,可以休眠。

tasklet是基于softirq实现的,所以是运行于软中断上下文。
我们先来看看softirq。
类似于IRQACTION,内核中用结构体SOFTIRQACTION来作为软中断的控制块。他包含软中断的处理函数Callback,以及传递给参数,CallbackRef。
来看看具体的过程。

void handle_IRQ(unsigned int irq, struct pt_regs* regs)
{
	irq_enter();
	...
	generic_handle_irq(irq);
	...
	irq_exit();
}
void irq_exit(void)
{
	...
	invoke_softirq();
}

在invoke_softirq中,调用了挂接的软中断的处理函数。

驱动开发中,不推荐使用softirq。
如果需要在软中断上下文中执行BH,推荐使用的是tasklet。
tasklet使用了软中断的一个SOFTIRQNUM,即TASKLET_SOFTIRQ。对应的软中断处理函数是tasklet_action.

static void taslet_action(struct softirq_action* a)
{
	struct tasklet_struct * list;
	list = __this_cpu_read(tasklet_vec.head);
	...
	while(list){
		struct tasklet_struct *tp = list;
		list = list->netx;
		...
		tp->func(tp->data);
		...
	}
}

在IRQ处理BH时,处理了内核定义的SOTFIRQ之后,会处理TASKLET_SOFTIRQ。它会找到内核中的tasklet_struct的链表,并调用其中的Callback。

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

TASKLET的控制块内嵌了一个链节,从而可以形成链表。
state是tasklet被调度的状态,
最重要的是func和data,它是Callback和对应的CallbackRef。
内核提供了对应的服务函数。

void task_init(struct tasklet_struct *tp, void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(tasklet, func, data);
DECLARE_TASKLET_DISABLED(tasklet, func, data);

用来定义一个tasklet的对象实体,并填充。

void tasklet_schedule(struct tasklet_struct * tp);

用来将TASKLET挂接到内核的TASKLET RUN LIST中,当BH执行时,能够被CPU所调度执行。
注意,由于内核执行的TASKLET是ONESHOT的,就是说,当TASKLET被调度执行结束后,会从TASKLET RUN LIST中删除,所以,每一个IRQEVENT,都会尝试挂接TASKLET。
但是如果TASKLET已经被成功挂接,那么重复的挂接将会被内核忽略。
来看一个例子

DECLARE_TASKLET(vser_tasklet, vser_tasklet_func, (unsigned long*)&vsdev);

static irqreturn_t vser_handler(int irq, void* dev_id)
{
	tasklet_schedule(&vser_tasklet);
}

static void vser_tasklet_func(unsigned long arg)
{
	struct vser_dev* devp = (struct vser_dev*)arg;
	...
}

这是简单的tasklet的使用框架。

为了解决使用tasklet作为BH时,不能休眠的问题,可以使用workqueue。
内核在启动时,会创建多个内核线程,内核线程会获取workqueue中的每一个work,然后执行,当workqueue中没有work时,内核线程休眠。
因为是运行在进程上下文,所以work可以使用进程调度。
除了使用内核提供的workqueue之外,也可以创建自己的workqueue。

struct work_struct{
	struct list_head entry;
	
	work_func_t func;
	atomic_long_t data;

};

WORK内嵌了一个LIST_HEAD,它是一个链节,所以WORK可以形成链表。
最重要的成员是func和data。它是work对应的Callback和CallbackRef。
内核提供了work相关的服务函数。

DECLARE_WORK(n,f);
DECLARE_DELAYED_WORK(n, f);
INIT_WORK(_work,_func);
bool schedule_work(struct work_struct* work);

它们的使用,类似于tasklet。

schedule_work用来将WORK挂接到内核的WORK RUN LIST中,当BH执行时,能够被CPU所调度执行。
注意,由于内核执行的WORK是ONESHOT的,就是说,当WORK被调度执行结束后,会从WORK RUN LIST中删除,所以,每一个IRQEVENT,都会尝试挂接WORK。
但是如果WORK已经被成功挂接,那么重复的挂接将会被内核忽略。

来看一个具体的例子。

DECLARE_WORK(vser_work,vser_work_func);

static irqreturn_t vser_handler(int irq, void * dev_id)
{
	...
	schedule_work(&vser_work);
		
	return IRQ_HANDLED;
}

static void vser_work_func(struct work_struct* workp)
{
	...
}

这是基本的workqueue的使用框架。

内核中最重要的中断,就是TIMER_IRQ。

如果没有定时器,那么为了实现延时,就必须需要靠CPU空转来实现。
例如
udelay, mdelay,ndelay等。除非是在中断上下文中,或者在spinlock的自旋中,否则不推荐使用这种延迟服务。
如果延时较长,推荐使用休眠延时。
例如
msleep, ssleep。

首先来看一个基础概念。
全局变量jiffies和jiffies_64。系统每产生一次TIMER_IRQ,就会使jiffies加1。
所以,在系统开机之后的每个时刻,jiffies都是不同的。jiffies是系统绝对时间。
jiffies是TIMER的判据。
相关的另一个概念是宏HZ。表示每秒钟产生多少次TIMER_IRQ。

来看看内核中使用的TIMER的结构体。

struct timer_list{
	struct list_head entry;
	
	void (*func)(unsigned long);
	unsigned long data;
	
	unsigned long expires;
	struct tvec_base *base;
};

TIMER结构体中,内嵌了一个链节,所以,TIMER可以构成链表。

最重要的两个成员是func和data,他们是Callback和CallbackRef。
expires是TIMER到期时,对应的jiffies。

内核提供了针对TIMER的服务函数。

init_timer(timer);
void add_timer(struct timer_list* timer);
int mod_timer(struct timer_list* timer, unsigned long expires);
int del_timer(struct timer_list* timer);

add_timer用来将TIMER添加到内核的ACTIVE TIMER LIST中。
del_timer用来将TIMER从内核的ACTIVE TIMER LIST中删除。
mod_timer用来修改TIMER的expires。

TIMER的function是在TIMER_SOFTIRQ中被处理的,所以,TIMER运行在软中断上下文。
内核会遍历链表中的TIMER,如果jiffies和expires相等,那么会调用TIMER的function,并将data传递给function。
注意,TIMER不是oneshot的,内核不会主动从链表中删除TIMER,但是如果用户不修改TIMER的expires,其实也相当于是oneshot的,因为expires != jiffies,TIMER的Callback不会被内核调用执行。
如果要使TIMER的Callback被周期性的调用执行,那么需要在Callback中手工增加expires的值,从而让TIMER在之后的jiffies,能够再次被内核调用执行Callback。

来看一个具体的例子。

static vser_dev vsdev;

struct vser_dev {
	struct cdev cdev;
	...
	timer_list timer;
};

static int __init vser_init(void)
{
	init_timer(&vsdev.timer);
	vsdev.timer.expires = get_jiffies_64() + msecs_to_jiffies(1000);
	vsdev.timer.function = vser_timer_func;
	vsdev.timer.data = (unsigned long) &vsdev;
	
	add_timer(&vsdev.timer);
}
module_init(vser_init);

static void __exit vser_exit(void)
{
	...
	del_timer(&vsdev.timer);
	...
}
module_exit(vser_exit);

static void vser_timer_func(unsigned long arg)
{
	struct vser_dev* devp = (struct vser_dev*)arg;
	...
	mod_timer(&devp->timer,get_jiffies_64()+msecs_to_jiffies(1000));
}

这是使用TIMER的基本框架。

HRTIMER的使用,与之类似,在此不再探讨。

前面所提到的delayed_work,就是基于TIMER来实现的。

typedef void(*work_func_t)(struct work_struct *work);

struct delayed_work{
	struct work_struct work;
	struct timer_list timer;
	
	struct workqueue_struct *wq;
	int cpu;
};

bool schedule_delayed_work(struct delayed_work* work, unsigned long delay);

常用的方式是,

schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));

用来将毫秒转换成jiffies。
如果要周期性执行任务,通常在work中再次调用schedule_delayed_work。

来看一个完整的驱动例子。

#define SEC_MAJOR 248
struct sec_dev {
	struct cdev cdev;
	atomic_t counter;
	struct timer_list s_timer;
};

static int sec_major = SEC_MAJOR;
static struct sec_dev* sec_devp;

static void sec_timer_func(unsigned long arg)
{
	mod_timer(&sec_devp->s_timer, jiffies+ HZ);
	atomic_inc(&sec_devp->counter);
	return;
}

static int sec_open(struct inode *inode, struct file* filp)
{
	init_timer(&sec_devp->s_timer);
	sec_devp->s_timer.function = &sec_timer_func;
	sec_devp->s_timer.expires = jiffies + HZ;
	
	add_timer(&sec_devp->s_timer);
	atomic_set(&sec_devp->counter, 0);

	return 0;
}

static int sec_release(struct inode* inode, struct file* filp)
{
	del_timer(&sec_devp->s_timer);
	return 0;
}

static ssize_t sec_read(struct file* filp, char __user * buf, size_t count, loff_t* ppos)
{
	int cnt ;
	int ret;
	cnt = atomic_read(&sec_devp->counter);
	ret = put_user(cnt, (int*)buf);
	return ret;
}

static const struct file_operations sec_fops = {
	.owner = THIS_MODULE,
	.open = sec_open,
	.release = sec_release,
	.read = sec_read,
};

static void sec_setup_cdev(struct sec_dev* devp, int index)
{
	int ret;
	int devno = MKDEV(sec_major, index);

	cdev_init(&devp->cdev, &sec_fops);
	devp->cdev.owner = THIS_MODULE;
	ret = cdev_add(&dev->cdev, devno, 1);
	
	return;
}

static int __init sec_init(void)
{
	int ret;
	dev_t devno = MKDEV(sec_major, 0);

	ret = register_chrdev_region(devno, 1, "sec");

	sec_devp = kzalloc(sizeof(*sec_devp), GFP_KERNEL);
	
	sec_setup_cdev(sec_devp, 0);
	
	return 0;
}
module_init(sec_init);

static void __exit sec_exit(void)
{
	cdev_del(&sec_devp->cdev);
	unregister_chrdev_region(MKDEV(sec_major, 0), 1);
	kfree(sec_devp);
}
module_exit(sec_exit);
MODULE_LICENSE("GPL v2");

SEC_DEV内嵌了一个CDEV,它是对CDEV的扩展。
模块加载函数中,动态分配了一个对象实体,用一个全局指针来标记。
open函数中,向内核注册了一个TIMER,由内核控制TIMER的Callback的运行时机。
Callback中修改TIMER,并修改SEC_DEV的成员。
read函数,读取SEC_DEV的成员,并拷贝给用户buf。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值