《linux内核中断》之 法外狂徒张三删库跑路

法外狂徒张三删库跑路

真实案例:在今年2月份,国内一个程序员删库的消息传遍it界。他的几行代码,直接让上市公司微盟的市值一天蒸发超10亿,300百万用户直接受到影响。网上是谣言四起,可谓是最牛逼的删库跑路案例。删库跑路是我们程序员可望而不可及的,是传说。最终这位技术人员,没有逃出法律的制裁,罚钱和判刑一样也没少。真是删库一时爽,身后火葬场。
删库跑路,是技术人员道路上永远无法企及的高度。但是对我们技术人员,千万不要因为一时脑热,做出不可挽回的错误i,让自己身陷牢狱之灾。除此之外,企业也应该完善安全机制和管理制度,顺便在想一下为什么员工会删库。
《linux内核中断》之 法外狂徒张三删库跑路_第1张图片

## Linux内核中断

一、简介
不管是在单片机裸机中,还是在linux中,中断都是频繁使用的对象。记得大学那会接触51单片机中断 :定时和计算器中断 ,给我折腾疯了。

  • 单片机使用中断需要设置寄存器,使能IRQ等,需要清晰的了解对应寄存器每一位的作用。在后来的蓝牙soc,nxp单片机中,就把寄存器设置部分,用api函数封装起来,提供给用户使用。
    总结一下:
    1>初始化寄存器,使能中断
    2>注册中断服务函数,也就是向irqtable数组标号写入中断服务函数
    3>中断发生,进入irq中断服务函数,在irq中断服务函数irqtable数组中查找具体的中断处理函数,执行对应的中断处理函数。

  • 在linux中,同样也进行了封装,我们只需要按照中断的使用模板,在驱动中灵活调用即可。以下就介绍内核中的中断框架。

1.中断API函数
1>中断号
每个中断都有一个中断号,可以中断号可以区分不同的中断。在内核中用一个int变量来表示中断号。中断号已经写道设备数里面,因此可以通过irq_of_parse_and_map函数从interupts属性中获取设备号,获取中断号的api:

1. 非gpio中断请求中断号
//dev : 设备节点
//index :索引号,interrput包含多条中断信息,通过index指定要获取的信息
//return : 返回的中断号
unsigned int irq_of_parse_and_map(struct device_node * dev, int  index)

2.gpui申请中断号
//gpio : 对应的gpio编号
//return:返回的中断号
int gpio_to_irq(unsigned int gpio)

2>请求中断函数(request_irq)
在内核中,使用某个中断是需要申请的,request_irq函数用于申请中断:

int request_irq(unsigned int irq,
				irq_handker_t handler,
				unsigned long flags,
				const char *name.
				void *dev)
//要申请中断的中断号
//中断处理函数,当中断发生时,进入handler
//flags:中断标志,可以在include/linux/interrupput.h中查看
常用:上升沿、下降沿、高电平、
//中断名字
//当flags设置未IRQ_SHARES(共享中断),dev用来区分不同中断;一般情况下将dev设置未设备结构体,dev会传递函数给中断处理函数irq_handler_t的第二个参数。
//return 0 成功

note:request_irq函数可能会导致睡眠,因此不能在中断上下文或者其它禁止睡眠的的代码中使用request_irq函数。

3> 释放中断函数(free_irq)
在使用request_irq申请中断,使用完成之后需要通过free_irq函数释放掉对应中断。

void free_irq(unsigned irq,
		void *dev)
//要释放的中断
//如果终端设置为共享IROQF_SHARED(共享中断)的话,此参数用来区分具体中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
//返回值:无

4>中断处理函数
使用request-irq申请中断的时候,要设置中断处理函数,中断处理函数如下:

irqreturn_t (*irq_handler_t)(int, void*)
//参数int 是中断处理函数要处理的相应中断号
//void* :指向void ,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。
//返回值irqreturn_t类型,irqretuen_t类型如下:
typedef enum irqreturn irqreturn_t; 
enum irqreturn{
	IRQ_NONE = (0<<0),
	IRQ_HANDLED = (1<<0),
	IRQ_WAKE_THREAD = (1 <<1),
};
/*return IRQ_RETVAL(IRQ_HANDLED);*/

5>中断使能与禁止函数
(1)常用api函数如下:

void enable_irq(unsigned int irq) //使能。。中断号为irq。。的中断
void disable_irq(unsigned int irq) //禁止。。中断号为irq。。的中断
/*
diable_irq 使用注意事项:
	a.保证中断函数处理完成才返回,不会产生新的中断
	b.确保已经开始执行的中断处理程序已经全部推出。
	c.在以上情况下 ,可以使用void disable_irq_nosync(unsigned int irq)
*/
void disable_irq_nosync(unsigned int irq)//函数调用之后,立即返回,不会等待当前中断函数执行完毕。
					  

(2)以上三个函数的,只用与使能或者禁止一个中断,有时候需要关闭处理 器的整个中断。可以使用如下api:

local_irq_enable()//使能当前处理器的中断系统
local_irq_disable()//禁止当前处理器的中断系统
/*有些A场景下不能使用loacal——irq——enable来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到B的感受,此时需要调用一下两个函数
*/
local_irq_save(flags)//禁止中断,并且把中断状态保存在flags中
local_irq_restore(flags)//恢复中断,将中断恢复到flags状态

2.中断上、下半部
在kernel 中,中断有上半部和下半部之分。在使用request_irq申请中断的时候,注册的中断服务函数属于:中断处理的上半部,只要中断触发,中断处理函数就会执行。
在中断中,我们希望处理函数越快处理越好,但是在现实使用中,有些中断往往耗时间。我们需要对其处理,缩小中断处理函数的执行时间。这就出现了下半部。
举个例子:tp触控屏,中断通知soc触控event发生,i2c读取坐标上报给系统。i2c最高速度有400kbit/s,所以在中断中用i2c读取数据就会浪费时间。我们可以将i2c读取坐标值暂后执行(放到下半部),中断处理函数处理相应的中断,然后清楚标志位(上半部处理)。
a.上半部:上半部是request_irq 注册的中断处理函数,处理过程比较快,部耗时的任务可以放到上半部完成。
b.下半部:如果中断函数处理任务较多,比较耗时,那么把这些耗时任务放到下半部来执行。
c.分为上、下半部的目的:实现中断函数快进快出,将对时间敏感,与硬件有关,不希望被中断的函数,希望执行速度快的放到上半部,剩下的放到下半部。

二、下半部处理机制

1>软中断

内核提供了bottom half机制实现下半部。后面引入了软中断和tasklet来代替”BH‘,完全可以使用软中断和tasklet来代替BH.随着内核版本升级,BH在2点几版本就不用了。在linux内核中,使用softirq_action表示软中断,定义在includ/linux/interrupt.h,

//a. 软中断 includ/linux/interrupt.h
struct softirq_action
{
	void (*action)(struct softirq_action *);
}

//b.中断类型 kernel/softirq.h
static struct softirq_action softirq_vec[NR_SOFTIRQS];

//NR_SOFTIRQS	在 includ/linux/interrupt.h 中
enum
{
 HI_SOFTIRQ=0, 
 TIMER_SOFTIRQ, 
 NET_TX_SOFTIRQ, 
 NET_RX_SOFTIRQ, 
 BLOCK_SOFTIRQ, 
 BLOCK_IOPOLL_SOFTIRQ, 
 TASKLET_SOFTIRQ, //tasklet 软中断 
 SCHED_SOFTIRQ, 
 HRTIMER_SOFTIRQ, 
 RCU_SOFTIRQ, 
 NR_SOFTIRQS
};

可以看到,softirq_action 结构体中的成员变量action就是软中断的服务函数,数组softirq_vec是个全局数组,因此所以cpu都可以访问到,每个cpu都有自己的触发和控制机制,并且只执行自己所触发的软中断。
如何使用软中断?
a. 先使用open_softirq函数,注册对应的软件中断处理函数,函数如下:

void open_softirq(int0nr ,void(*action)(struct softirq_action*))
//nr :要开启的软中断,如 TASKLET_SOFTIRQ
//action :软中断对应的处理函数
//void 无return

b.注册过软中断之后,需要 通过raise_softirq函数触发,函数如下:

void raise_softirq(unsigned int nr)
//nr :要触发的软中断
//void : 无return

c. 软中断必须在编译的时候:静态注册。linux内核使用softirq_irq函数初始化软件中断,
softirq_init函数在kernel/softirq.c ,函数如下

//kernel/softirq.c 
 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);
 }
 //从这里我们可以看到,默认打开TASKLET_SOFTIRQ(tasklet软中断),HI_SOFTIRQ(高优先级软中断)。

2>tasklet

tasklet是利用软中断来实现下半部的一种机制,在软中断和taskley之间,我们一般使用tasklet。linux内核使用tasklet_struct结构体来表示tasklet,如下:

struct tasklet_struct{
	struct tasklet_sturct *next;//下一个tasklet
	unsigned long state ; //tasklet 状态
	atomic_t count ;// 计数器,记录对tasklet的引用数
	void (*func)(unsigned long); //tasklet执行函数 ,可以定义函数内容,相当于中断处理函数
	unsigned long data //函数func的参数
};

如何使用tasklet?
a. 先定义一个tasklet,然后使用tasklet_init函数初始化tasklet,tasklet_init函数原型如下:

void tasklet_init(
	struct tasklet_struct *t,
	void (*func)(unsigned long),
	unsigned long data);
//t:要初始化的tasklet
//func :tasklet的处理函数
//data:传递给func函数的参数
//void 无return

/*或者通过宏 : DECCLEARE_TASKLET 来一次性完成tasklet的定义和初始化,
DECCLEARE_TASKLET 定义在 include/linux/interrupt.h 
*/
DECLARE_TASKLET(name, func, data)

b.在上半部,也就是中断处理函数中调用tasklet_schedule,就能使tasklet在合适的时间运行,tasklet_schedule 函数如下:

void tasklet_schedul(struct tasklet_struct *t)
//t:要调度的tasklet,也就是DECCLEAR_TASKLET宏里的name
//无返回值

c.参考模板

//1. 定义tasklet
struct tasklet-struct testtasklet;

void testtasklet(unsigned long data)
{
	//5.taskelet 函具体的处理内容
}

//中断处理函数test_handler
irqreturn_t test_handler(int irq ,void *dev_id)
{
.......
	//4.调度tasklet
	tasklet_schedule(&testtasklet);
.......
}

//驱动入口函数
static int __init xxx_init(coid)
{
......
	//2.初始化tasklet
	tasklet_init(&testtasklet ,testtasklet_func, data);
	//3. 注册中断处理函数
	request_irq(xxx_irq ,test_handler ,0 , "xxx" ,&xxx_dev);
......
}

3>工作队列

工作队列是另一种下半部的机制,工作队列在进程上下文执行,工作队列目的:**是将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。**如果要睡觉或者推后的工作可以选择工作队列,否则选择软中断或者tasklet。

linux 内核使用work_struct 结构体表示一个工作,如下:

	struct work_struct {
		atomic_long_t data;
		struct list_head entry;
		work_func_t func;//工作队列处理函数
	};

上面工作组织(work_struct)成工作队列,工作队列使用workqueue_struct结构体表示,如下:

struct workqueue_struct {
 struct list_head pwqs; 
 struct list_head list; 
 struct mutex mutex; 
 int work_color;
 int flush_color; 
 atomic_t nr_pwqs_to_flush;
 struct wq_flusher *first_flusher;
 struct list_head flusher_queue; 
 struct list_head flusher_overflow;
 struct list_head maydays; 
 struct worker *rescuer; 
 int nr_drainers; 
 int saved_max_active;
 struct workqueue_attrs *unbound_attrs;
 struct pool_workqueue *dfl_pwq; 
 char name[WQ_NAME_LEN];
 struct rcu_head rcu;
 unsigned int flags ____cacheline_aligned;
 struct pool_workqueue __percpu *cpu_pwqs;
 struct pool_workqueue __rcu *numa_pwq_tbl[];
 };

linux内核使用工作者线程work_thread来处理工作队列中的各个工作,linux内核使用worker结构体表示工作者线程,worker结构体如下:

struct worker {
 union {
 struct list_head entry; 
 struct hlist_node hentry;
 };
 **struct work_struct *current_work;** 
 work_func_t current_func; 
 struct pool_workqueue *current_pwq;
 bool desc_valid;
 struct list_head scheduled; 
 struct task_struct *task; 
 struct worker_pool *pool; 
 struct list_head node; 
 unsigned long last_active; 
 unsigned int flags; 
 int id; 
 char desc[WORKER_DESC_LEN];
 **struct workqueue_struct *rescue_wq;**
};

从worker结构体可以看出,每一个worker都有一个工作队列(workqueue_struct),工作者线程处理自己工作队列中的所有工作。在实际中,我们只需定义工作(work_struct)即可,关于工作队列和工作者线程基本不需要我们去管。

如何创建work_struct?
a. 直接定义一个work_truct结构体变量即可,然后用INIT_WORK宏来初始化工作,INIT_WORK 定义如下:

#define  INIT_WORK(_work,_func)
//_work表示要初始化的工作,_func是工作对应的处理函数
//or 使用如下方法,一次性完成工作的创建和初始化,宏如下:
#define DECLARE_WORK(n, f)

b. 和tasklet 一样,工作需要调度才能运行,工作的调度函数为schedule_work,函数原型如下:

bool schedule_work(struct work_struct *work);
//work :要调度的工作

c.参考模板

//1. 定义工作work
struct work_struct testwork;

void testwork_func_t(unsigned long data)
{
	//5.work函具体的处理内容
}

//中断处理函数test_handler
irqreturn_t test_handler(int irq ,void *dev_id)
{
.......
	//4.调度work
	tasklet_schedule(&testwork);
.......
}

//驱动入口函数
static int __init xxx_init(coid)
{
......
	//2.初始化work
	INIT_WORK(&testwork ,testwork_func_t);
	//3. 注册中断处理函数
	request_irq(xxx_irq ,test_handler ,0 , "xxx" ,&xxx_dev);
......
}

4>dts中断信息节点分析

如果使用设备数,需要在设备数中配置好中断属性信息,kernel 匹配设备树中的中断信息来配置中断。

	intc: interrupt-controller@17a00000 {
		compatible = "arm,gic-v3"; //匹配控制器驱动文件
		#interrupt-cells = <3>;//表示控制器下设备的cells大小
		/*
		interrupt-cells 描述了interrupts属性cells大小,也就是一条信息有几个cells。每个cells都是32bit整形值。对于arm的gic来说,一共有三个cells
		1.cells 中断类型,0表示spi中断,1表示ppi中断
		2.cells 中断号,对于spi中断来说0~987,ppi中断0~15
		3.cells bit[3:0],1 上升沿触发,2 下降沿触发,4 高电平触发,8 低电平触发
		*/
		interrupt-controller; //为空,表示当前节点是中断控制器
		#redistributor-regions = <1>;
		redistributor-stride = <0x0 0x20000>;
		reg = <0x17a00000 0x10000>,     /* GICD */
		      <0x17a60000 0x100000>;    /* GICR * 8 */
		interrupts = <1 9 4>; //指定中断号,触发方式
		interrupt-parent = <&intc>; //指定父中断,也就是中断控制器。
		ignored-save-restore-irqs = <38>;
	};

你可能感兴趣的:(linux,调试,linux,stm32)