中断机制以及中断上下部运行和内核代码分析

一、视频学习

  • 查看当前系统中的中断指令:
cat /proc/interrupts

在这里插入图片描述

第一列:IRQ序号

第二、三列:CPU0/CPU1分别是当前CPU上发生中断的次数

第四列:中断控制器名称,比如IO-APIC

最后一列:设备名称,比如timer

  • 要写一个中断程序,就必须要申请一根中断线,一个中断线又对应一个IRQ号

二、实验

Linux将中断处理分为上下两部分:

  1. 上半部分主要处理紧急的,必须马上处理的事情。(实验一)
  2. 下半部分处理不那么紧急的事情,linux内核也提供了相应的机制,这里使用tasklet机制来进行实验。(实验二)

**Tasklet机制(小任务机制)**是指对要推迟执行的函数进行组织的一种机制,也就是说,推迟处理的事情是由相关处理函数实现,何时执行,由小任务机制封装后交给内核去处理。

(一)实验一

中断程序 interrupt.c:

# include <linux/kernel.h>
# include <linux/init.h>
# include <linux/module.h>
# include <linux/interrupt.h>

static int irq;			//irq号		
static char * devname;		//设备名称	
			
//这两个是用来让我们在命令行传入参数
module_param(irq,int,0644);
module_param(devname,charp,0644);    //这里charp相当于char*,是字符指针
			
struct myirq
{
    int devid;     //这个主要用在共享irq中
};

struct myirq mydev={1119};
		
//中断处理函数
static irqreturn_t myirq_handler(int irq,void * dev)
{
    struct myirq mydev;
    static int count=1;
    mydev = *(struct myirq*)dev;		
    printk("key: %d..\n",count);
    printk("devid:%d ISR is working..\n",mydev.devid);
    printk("ISR is leaving......\n");
    count++;			//进行中断计数
    return IRQ_HANDLED;	//返回的值代表接收到了准确的中断信号并且做了相应的正确处理
}


//内核模块初始化函数
static int __init myirq_init(void)    //最重要的工作是注册中断线,并将自己写的中断服务例程注册进去,用request_irq完成
{
    printk("Module is working...\n");
    if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0)	//IRQ_SHARED,即允许共享
    {
        printk("%s request IRQ:%d failed..\n",devname,irq);
        return -1;
    }
    printk("%s request IRQ:%d success...\n",devname,irq);
    return 0;
}

//内核模块退出函数
static void __exit myirq_exit(void)
{
    printk("Module is leaving...\n");
    free_irq(irq,&mydev);            //注销函数
    printk("Free the irq:%d..\n",irq);
}

MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit);

执行指令 sudo insmod interrupt.ko irq=1 devname=myirq ,插入内核模块

执行指令cat /proc/interrupts ,查看中断情况:

在这里插入图片描述

查看一下日志信息dmesg:

在这里插入图片描述

执行指令 sudo rmmod interrupt 退出模块,dmesg查看:

在这里插入图片描述

(二)实验二

interrupt.c代码如下:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
 
static int irq;
static char * devname;
 
module_param(irq,int,0644);
module_param(devname,charp,0644);
 
struct myirq
{
        int devid;
};
 
struct myirq mydev={1119};
 
static struct tasklet_struct mytasklet;
 
static void mytasklet_handler(unsigned long data)  //下半部分处理函数
{
        printk("I am mytasklet_handler");
}
 
static irqreturn_t myirq_handler(int irq,void * dev)
{
//      struct myirq mydev;
//      static int count=1;
        static int count=0;
//      mydev = *(struct myirq*)dev;
//      printk("key: %d..\n",count);
//      printk("devid:%d ISR is working..\n",mydev.devid);
//      printk("ISR is leaving......\n");
        printk("count:%d\n",count+1);
        printk("I am myirq_handler\n");
        printk("The most of the interrupt work will be done by folling tasks\n");
       
        tasklet_init(&mytasklet,mytasklet_handler,0);  	//将下半部分函数进行注册。最主要的是要将下半部分处理函数挂载上去
        tasklet_schedule(&mytasklet);   //调度
        count++;
        return IRQ_HANDLED;
}
 
static int __init myirq_init(void)
{
        printk("Module is working...\n");
        if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0)
        {
                printk("%s request IRQ: %d failed...\n",devname,irq);
                return -1;
        }
        printk("%s request IRQ:%d success...\n",devname,irq);
        return 0;
}
 
static void __exit myirq_exit(void)
{
        printk("Module is leaving...\n");
        free_irq(irq,&mydev);
        tasklet_kill(&mytasklet);      //注销掉tasklet
        printk("Free the irq: %d..\n",irq);
}
 
MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit);

运行结果:

在这里插入图片描述

执行指令cat /proc/interrupts ,查看中断情况:

在这里插入图片描述

执行指令sudo rmmod interrupt ,退出模块:

在这里插入图片描述

三、相关源码

(一)实验一相关源码

request_irq():

/*注册中断线,并可以将自己的中断服务例程注册进去
  第一个参数irq就是中断号,对应的就是中断控制器上IRQ的编号
  第二个参数 handler,即我们要注册的中断服务例程,需要我们自己去实现,它有一个特定类型的返回值irqreturn_t
  第三参数参数flags,它指定了快速中断或者中断共享中中断的处理属性
  第四个参数name,它是设备名称,即cat /proc/interrupt命令来查看的最后一列
  第五个参数dev,指dev_id,主要用于共享中断线。注意该参数的类型是void,即可以使用强制转换为任意类型,可以作为共享中断时中断区别的一个参数。
*/
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
/*中断服务例程返回值类型*/
typedef enum irqreturn irqreturn_t;
/**
 * enum irqreturn
 * @IRQ_NONE		中断不是来自此设备或未被处理
 * @IRQ_HANDLED		此设备处理中断
 * @IRQ_WAKE_THREAD	处理程序请求来唤醒处理程序线程
 */
enum irqreturn {
	IRQ_NONE		= (0 << 0),		//0,0 << 0 是把0按2进制左移0位,结果还是0 
	IRQ_HANDLED		= (1 << 0),		//1
	IRQ_WAKE_THREAD		= (1 << 1),	//2
};
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
			 irq_handler_t thread_fn, unsigned long irqflags,
			 const char *devname, void *dev_id)
{
	struct irqaction *action;
	struct irq_desc *desc;
	int retval;

	if (irq == IRQ_NOTCONNECTED)
		return -ENOTCONN;

	/*
	 * Sanity-check: shared interrupts must pass in a real dev-ID,
	 * otherwise we'll have trouble later trying to figure out
	 * which interrupt is which (messes up the interrupt freeing
	 * logic etc).
	 *
	 * Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
	 * it cannot be set along with IRQF_NO_SUSPEND.
	 */
	if (((irqflags & IRQF_SHARED) && !dev_id) ||
	    (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
	    ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
		return -EINVAL;

	desc = irq_to_desc(irq);	//根据中断号irq,在irq_desc数组中,返回一个具体的irq_desc
	if (!desc)
		return -EINVAL;

	if (!irq_settings_can_request(desc) ||
	    WARN_ON(irq_settings_is_per_cpu_devid(desc)))
		return -EINVAL;

	if (!handler) {
		if (!thread_fn)
			return -EINVAL;
		handler = irq_default_primary_handler;
	}

	action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
	if (!action)
		return -ENOMEM;
	
	//实际上我们的注册就是要将我们传进来的参数生成一个个的action,再添加到irq_desc上,具体是通过“retval = __setup_irq(irq, desc, action);"这个函数进行的。
	action->handler = handler;
	action->thread_fn = thread_fn;
	action->flags = irqflags;
	action->name = devname;
	action->dev_id = dev_id;

	retval = irq_chip_pm_get(&desc->irq_data);
	if (retval < 0) {
		kfree(action);
		return retval;
	}

	retval = __setup_irq(irq, desc, action);

	......
	
	return retval;
}
/*  free_irq - 释放用request_irq分配的中断
 *	@irq: Interrupt line to free
 *	@dev_id: Device identity to free
*/
const void *free_irq(unsigned int irq, void *dev_id)
{
	......
	action = __free_irq(desc, dev_id);
    ......
	devname = action->name;
	kfree(action);
	return devname;
}

do_IRQ():

每次进入do_IRQ函数都是从汇编的代码中跳入,可以说发生了中断之后,do_IRQ是我们执行的第一个C语言函数,其与体系结构息息相关,我们的do_IRQ函数位于根目录下的arch/x86/kernel/irq.c

/*
 * do_IRQ处理所有普通设备的中断 
 * 这个函数最核心的部分是执行handle_irq函数,从handle_irq进入,执行完后返回到exiting_irq()
 */
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
	......
	if (likely(!IS_ERR_OR_NULL(desc))) {
		if (IS_ENABLED(CONFIG_X86_32))
			handle_irq(desc, regs);
		else
			generic_handle_irq_desc(desc);
	} else {
		ack_APIC_irq();

		if (desc == VECTOR_UNUSED) {
			pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
					     __func__, smp_processor_id(),
					     vector);
		} else {
			__this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
		}
	}

	exiting_irq();

	set_irq_regs(old_regs);
	return 1;
}
bool handle_irq(struct irq_desc *desc, struct pt_regs *regs)
{
	stack_overflow_check(regs);
	
	if (IS_ERR_OR_NULL(desc))
		return false;
	
 
	generic_handle_irq_desc(desc);
	return true;
}
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
	desc->handle_irq(desc);		//handel_irq函数,它是在irq_desc结构中
}

handel_irq函数,它是在irq_desc结构中

在这里插入图片描述

hadle_irq是一个回调函数,负责处理底层的细节,比如中断确认,边沿触发以及电平触发的处理。最后再由这个函数去调用irqaction中的handler,实际上,handler才是最后我们想要的中断服务例程。

在这里插入图片描述

handle的挂载过程:

先是注册handle,然后在 __irq_do_set_handler 函数中 desc->handle_irq = handle 完成挂载

static void mp_register_handler(unsigned int irq, unsigned long trigger)
{
	......
 	
 	//进行一个选择,是哪种方式。后面将会以handle_edge_irq的方式为例来分析,即边沿触发的方式来分析。
	hdl = fasteoi ? handle_fasteoi_irq : handle_edge_irq;
	__irq_set_handler(irq, hdl, 0, fasteoi ? "fasteoi" : "edge");
}
void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
		  const char *name)
{
	......

	__irq_do_set_handler(desc, handle, is_chained, name);
	irq_put_desc_busunlock(desc, flags);
}
static void
__irq_do_set_handler(struct irq_desc *desc, irq_flow_handler_t handle,
		     int is_chained, const char *name)
{
	......
	desc->handle_irq = handle;		//将handle挂载到了handle_irq上
	......
}

这里以handle_edge_irq为例来分析:

void handle_edge_irq(struct irq_desc *desc)
{
	......
	
	handle_irq_event(desc);
	
	......
}
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
	......

	ret = handle_irq_event_percpu(desc);

	......
}
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
	......

	retval = __handle_irq_event_percpu(desc, &flags);

	......
}
/*该函数就实现了依次执行这条中断线上的所有中断服务例程*/
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
	irqreturn_t retval = IRQ_NONE;
	unsigned int irq = desc->irq_data.irq;
	struct irqaction *action;

	record_irq_time(desc);

	for_each_action_of_desc(desc, action) {		//通过for_each_action_of_desc 这个宏来实现
		irqreturn_t res;

		trace_irq_handler_entry(irq, action);
		res = action->handler(irq, action->dev_id);		//将对应的返回值给了res,在后面可以在switch语句中可以看到,res的值是IRQ_WAKE_THREAD或IRQ_HANDLED
		trace_irq_handler_exit(irq, action, res);

		if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",
			      irq, action->handler))
			local_irq_disable();

		switch (res) {
		case IRQ_WAKE_THREAD:
			/*
			 * Catch drivers which return WAKE_THREAD but
			 * did not set up a thread function
			 */
			if (unlikely(!action->thread_fn)) {
				warn_no_thread(irq, action);
				break;
			}

			__irq_wake_thread(desc, action);

			/* Fall through - to add to randomness */
		case IRQ_HANDLED:
			*flags |= action->flags;
			break;

		default:
			break;
		}

		retval |= res;
	}

	return retval;
}
#define for_each_action_of_desc(desc, act)			\
	for (act = desc->action; act; act = act->next)

在这里插入图片描述

(二)实验二相关源码

tasklet_init():

/*内容基本上是字段的赋值,以及函数的挂载*/
void tasklet_init(struct tasklet_struct *t,
		  void (*func)(unsigned long), unsigned long data)
{
	t->next = NULL;
	t->state = 0;
	atomic_set(&t->count, 0);
	t->func = func;
	t->data = data;
}

tasklet_schedule():

static inline void tasklet_schedule(struct tasklet_struct *t)
{
	if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))		此处进行了字段的赋值
		__tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
	__tasklet_schedule_common(t, &tasklet_vec,
				  TASKLET_SOFTIRQ);
}
static void __tasklet_schedule_common(struct tasklet_struct *t,
				      struct tasklet_head __percpu *headp,
				      unsigned int softirq_nr)
{
	struct tasklet_head *head;
	unsigned long flags;

	local_irq_save(flags);
	head = this_cpu_ptr(headp);
	t->next = NULL;
	*head->tail = t;			//把传入的tasklet挂入到了链表的尾部
	head->tail = &(t->next);
	raise_softirq_irqoff(softirq_nr);	//调度不代表执行,真正的执行softirq_nr中断是tasklet_action函数,
	local_irq_restore(flags);
}
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
	tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}
static void tasklet_action_common(struct softirq_action *a,
				  struct tasklet_head *tl_head,
				  unsigned int softirq_nr)
{
	......

	while (list) {			//遍历tasklet链表
		struct tasklet_struct *t = list;

		list = list->next;

		if (tasklet_trylock(t)) {
			if (!atomic_read(&t->count)) {
				if (!test_and_clear_bit(TASKLET_STATE_SCHED,
							&t->state))
					BUG();
				t->func(t->data);	//这里就是tasklet这个结构体的最后一个字段,其实是func的一个参数
				tasklet_unlock(t);
				continue;
			}
			tasklet_unlock(t);
		}

		local_irq_disable();
		t->next = NULL;
		*tl_head->tail = t;
		tl_head->tail = &t->next;
		__raise_softirq_irqoff(softirq_nr);
		local_irq_enable();
	}
}

内核源码中驱动代码应用tasklet:

struct ieee802154_hw *
ieee802154_alloc_hw(size_t priv_data_len, const struct ieee802154_ops *ops)
{
	......
	
    //初始化
    tasklet_init(&local->tasklet,
                 ieee802154_tasklet_handler,	//中断的下部分函数
                 (unsigned long)local);
                 
	......
}
void
ieee802154_rx_irqsafe(struct ieee802154_hw *hw, struct sk_buff *skb, u8 lqi)
{
	......
	tasklet_schedule(&local->tasklet);		//调度
}
void ieee802154_unregister_hw(struct ieee802154_hw *hw)
{
	......
 
	tasklet_kill(&local->tasklet);        //注销
	......
}

在/Documentation/x86/boot.rst中可以找到关于启动时的一些说明

在/Documentation/process/changes.rst中可以找到编译内核时最少需要的工具有哪些

在/Documentation/IRQ.txt中介绍了,什么是IRQ,这里是开发人员的解释

在/Documentation/core-api/genericirq.rst中,介绍中断API,到这个文件的最后就可以看到前面涉及到的manage.c, chip.c, handle.c

在/Documentation/IRQ-affinity.txt中,有一个特殊功能。 如果一个机器中有多个CPU,我们可以指定我们产生的中断在哪个CPU上进行处理,即绑定CPU。关于这里,应该根据生成环境的特点与应用的特点来平衡IRQ中断,这有助于提高整个系统的吞吐能力与性能,不同的场景下我们需要不同的设置,不是说我们修改了某些参数就叫做性能优化,还需要通过大量的测试进行测试/观察/改进

四、问答

1.中断和异常的定义

中断也称外中断,指来自CPU执行指令以外的事件发生(这里的中断是狭义上的中断)

异常也称内中断、例外或陷入,指源自CPU执行指令内部的事件

2.Linux为什么将中断处理分为上下两部分

在这里插入图片描述
在这里插入图片描述

3.Tasklet机制是什么?

Tasklet机制(小任务机制)是指对要推迟执行的函数进行组织的一种机制,也就是说,推迟处理的事情是由相关处理函数实现,何时执行,由小任务机制封装后交给内核去处理。

小任务的数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下:

struct tasklet_struct
{
	struct tasklet_struct *next;	//指向链表中的下一个结构
	unsigned long state;			//小任务的状态
	atomic_t count;					//引用的计数器
	void (*func)(unsigned long);	//要调用的函数
	unsigned long data;				//传递给函数的参数
};

4.时钟中断的作用

在所有的外部中断中,时钟中断起着特殊的作用。因为计算机是以精确的时间进行数值运算和数据处理的,最基本的时间单元是时钟周期,例如取指令、执行指令、存取内存等,时间系统是整个操作系统活动的动力。

5.实时时钟、系统时钟和CPU时钟的区别

实时时钟:RTC(Real Time Clock)时钟,用于提供年、月、日、时、分、秒和星期等的实时时间信息,由后备电池供电,当你晚上关闭系统和早上开启系统时,RTC仍然会保持正确的时间和日期。

系统时钟:是一个存储于系统内存中的逻辑时钟。用于系统的计算,比如超时产生的中断异常,超时计算就是由系统时钟计算的。这种时钟在系统掉电或重新启动时每次会被清除。

CPU时钟:即CPU的频率,当然这里的时钟频率指的是工作频率,即外频,还有什么主频=外频×倍频,

6.为什么中断上下文不能睡眠?

(1)中断处理的时候,不应该发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断,如果在中断context中休眠,则没有办法唤醒它,因为所有的wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没有一个task_struct(这点对于softirq和tasklet一样),因此真的休眠了,比如调用了会导致block的例程,内核几乎肯定会死。

(2)schedule()在切换进程时,保存当前的进程上下文(CPU寄存器的值、进程的状态以及堆栈中的内容),以便以后恢复此进程运行。中断发生后,内核会先保存当前被中断的进程上下文(在调用中断处理程序后恢复);但在中断处理程序里,CPU寄存器的值肯定已经变化了吧(最重要的程序计数器PC、堆栈SP等),如果此时因为睡眠或阻塞操作调用了schedule(),则保存的进程上下文就不是当前的进程context了,所以不可以在中断处理程序中调用schedule()。

(3)内核中schedule()函数本身在进来的时候判断是否处于中断上下文:

if(unlikely(in_interrupt()))
BUG();

因此,强行调用schedule()的结果就是内核BUG。

(4)中断handler会使用被中断的进程内核堆栈,但不会对它有任何影响,因为handler使用完后会完全清除它使用的那部分堆栈,恢复被中断前的原貌。

(5)处于中断context时候,内核是不可抢占的。因此,如果休眠,则内核一定挂起。

猜你喜欢

转载自blog.csdn.net/qq_58538265/article/details/133916439
今日推荐