Linux设备驱动第六天(工作队列、定时器、延时)

2019-07-13 08:47发布

回顾:
  1. linux内核混杂设备。
    特点:主设备号由内核分配好,主设备号为10;内核通过次设备来区分各个混杂设备。
    数据结构:
    struct miscdevice{
    int minor;//MISC_DYNAMIC_MINON(内核分配)
    char *name;//设备文件名
    struce file_operaionts *fops;//给混杂设备赋予相关的硬件操作接口
    }
  2. 中断相关(谈谈中断的认识?如何实现一个按键驱动?)
    2.1 为什么有中断?
    2.2 中断的硬件链接和硬件触发过程
    硬件外设
    中断控制器:中断信号首先要经过中断控制器,集成在CPU内部
    CPU
    2.3 中断的软件处理流程图?
    画图
    这里写图片描述
    异常向量
    保护现场
    恢复现场
    2.4 中断编程(不管是ARM裸板还是Linux系统,都必须做以下几步)
    建立异常向量表
    保护现场
    中断的处理
    恢复现场
    2.5 linux内核中断编程
    异常向量表的建立,保护现场,恢复现场三部份内容已经由内核来实现!开发者只需要完成中断的处理过程(即编写中断处理函数)。两个重要函数:
    request_irq(中断号,中断处理函数,中断标志,中断名称,给中断处理函数传递的参数)
    free_irq(中断号,给中断处理函数传递的参数)
    注意:注册中断处理函数传递的参数一定要与释放中断传递的参数保持一样!并且中断号和中断处理函数传递的参数具有一一对应的特性!
    中断处理函数原型:
    static irqreturn_t(中断号,给中断处理函数传递的参数)。
    网卡、串口:接收中断、发送中断
    2.6 linux内核对于中断处理函数的要求:
    linux内核要求中断处理函数的执行速度越快越好!
    硬件中断的优先级高于软中断,软中断高于进程!
    中断处理函数不隶属于任何进程,不参与进程的调试!
    中断处理函数不能直接跟用户空间进行数据交互,如果要交互必须要配合系统调用!
    硬件中断无优先级
    软件中断有优先级
    进程有优先级 ssleep():内核休眠,秒级休眠 2.7 如果在某些场合中断处理函数执行的时间比较长,比较耗时,这种情况势必会影响系统的并发能力与响应能力,linux内核要求将中断处理函数拆分为两部分:顶半部和底半部!
    顶半部:中断的处理函数,它的执行速度快(目的是立即释放CPU资源)!还要登记底半部的内容,将来CPU会执行比较耗时的内容!不可被中断。
    底半部:做原先中断处理函数中比较耗时,不紧急的内容!可被中断
    底半部的实现机制:本质就是延后执行的一种手段!
    tasklet:小任务
    struct tasklet_struct{
    void (*func)(unsigned long);//延后处理函数
    unsigned long data;//给处理函数传递的参数
    }
    分配初始化tasklet对象
    在中断处理函数中调用tasklet_schedule进行登记,CPU会在适当的时间去执行延后处理函数!
    切记:tasklet的延后处理函数运行在中断上下文中,又是基于软中断实现,所能延后处理函数不能做休眠操作!可以被打断
    tasklet也是一种延后执行的手段,比用户线程的效率高,但它不能休眠。
工作队列
工作队列也是底半部延后执行的一种手段,这种机制就是为了解决tasklet的延后处理函数不能休眠的问题。因为在某些场合可能需要在延后处理函数中做休眠动作,此时tasklet就不符合要求,可以采用工作队列。工作队列对应的延后处理函数可以进行休眠操作!说明工作队列的延后处理函数工作在进程上下文中,可以进行休眠,并且参与进程的调度! linux描述工作队列相关的数据结构:
普通工作
struct work_struct{
work_func_t func;
}; work_func_t原型:
typedef void(*work_func_t)(struct work_struct *work)
成员说明:
func:工作队列的延后处理函数,将来做原先中断处理函数比较耗时的内容,可以做休眠操作!
延后处理函数的开通work,work指向分配初始化的工作. 延时工作:
struct delay_work{
struct work_struct_work;
struct timer_list timer;
}
延时工作的特点:能够指定延时处理函数,并且能够指定登记延时工作的具体时间间隔! 如何使用工作队列?
1,分配普通工作或者延时工作
struct work_struct work;//分配普通工作对象
或者
struct delay_work dwork;//分配延时工作对象
2,初始化普通工作或者延时工作对象(目标就是指定一个延后处理函数而已)
INIT_WORK(&work,btn_work);//给普通工作对象赋予一个延后处理函数btn_work
或者
INIT_DELAY_WORK(&work,btn_dwork);//给延时工作对象赋予一个延后处理函数btn_dwork
3,要顶半部中断处理函数中进行登记普通工作或者延时工作
schedule_work(&work);//登记普通工作,一旦登记完毕,当顶半部执行完毕以后,CPU在适当的时候去执行普通工作的延后处理函数:btn_work;
或者
schedule_delayed_work(&dwork,5*HZ);//登记延时工作,注意不是立即登记,而是等待5秒以后,再去登记延时工作!一旦登记完成,CPU在适当的时间去执行延时工作的延后处理函数
4,切记:工作队列相关的延后处理函数运行在进程上下文中,可以休眠操作! vi编辑器的分屏编辑代码:
1,进入命令输入模式(shift+:)
2,vs 文件名 //进行左右分屏
3,sp 文件名 //进行上下分屏
4, ctrl+ww //进行切换
5,shift + v //行选
6, ctrl+v //列选
7,hjkl 键可以用于方向键,前提是可视模式下! 案例:工作队列的基本使用 #include #include #include #include #include <asm/gpio.h> #include //声明一个描述按键硬件的数据结构 struct btn_resource{ int req;//中断号 int gpio;//gpio编号 char *name;//名称 }; //分配初始化按键信息 static struct btn_resource btn_info[] ={ [0] = { .irq = IRQ_ENT(0), .gpio = S5PV210_GPH0(0), .name = "KEY_UP" }, [0] = { .irq = IRQ_ENT(1), .gpio = S5PV210_GPH0(1), .name = "KEY_DOWN" } }; static struct btn_resource *pdata; //分配普通工作或者延时工作 static struct work_struct work;//立马登记 static struct delay_work dwork;//可以指定一个延时登记 //普通工作的延后处理函数,work指针指向上面分配的普通工作对象的work static void btn_work(struct work_struct *work){ unsinged int pinstate ; pinstate = gpio_get_value(pdata->gpio); printk(%s:按键%s的状态为:%s ",__func__,pdata->name,pinstate?"松开":"按下"); } //延时工作的延后处理函数 //work:delay_work的第一个成员为work_struct //work指针指向上面分配的延时工作对象work static void btn_dwork(struct work_struct *work){ unsinged int pinstate ; pinstate = gpio_get_value(pdata->gpio); printk(%s:按键%s的状态为:%s ",__func__,pdata->name,pinstate?"松开":"按下"); } //中断处理函数 //这个函数相当于顶半部 static irqreturn_t button_isr(int irq,void *dev_id) { //获取按键对应的硬件信息 pdata = (struct btn_resource *)dev_id; //登记普通工作,一旦登记完成,CPU在适当的时候去执行普通工作的处理函数 // schedule_work(&work); //登记延时工作,登记的延时时间为5s,5s以后进行登记, //一旦登记完成,CPU在适当的时候执行延时工作的延后处理函数 schedule_deylay_work(&dwork,5HZ); return IRQ_HANDLED;//正常处理中断 } static int btn_init(void) { int i; for(i=0;i//申请GPIO资源 ,使用完记得释放 gpio_request(btn_info[i].gpio,btn_info[i].name); //注册外部中断XEINT0的中断处理函数 request_irq((btn_info[i].irq, button_isr, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,//对于外部中断,需要指定触发方式,这里指定双边缘触发 btn_info[i].name, &btn_info[i]); } INIT_WORK(&work,btn_work);//给普通工作对象赋值一个延后处理函数 INIT_DELAYED_WORK(&dwork,btn_dwork);//给延迟工作对象赋值一个延后处理函数 return 0; } static void btn_exit(void) { int i; for(i=0;i//释放中断 free_irq(btn_info.irq,&btn_info[i]); //释放GPIO资源 gpio_free(btn_info[i].gpio); } } module_init(btn_init); modult_exit(btn_exit); MODULE_LICENSE("GPL"); 案例:利用工作队列实现每隔2秒钟开关灯(在延后的处理函数再登记一次)
底半部的最后一个延后机制:软中断
软中断也是延后执行的一种手段,它也有对应的延后处理函数,tasklet本身也是基于软中断实现的
软中为设计的注意事项:
1,软中断对应的延后处理函数可以同时在多个CPUh,而tasklet只能在一个CPU上运行。
2,软中断对就的延后处理函数实现不能以模块的形式实现,必须静态编译到内核中
3,由于软中断的处理函数可以运行多个CPU上,要求软中断的处理函数必须具备可重入性! staitc int g_data; void swap(int *x,int *y){ //上锁 g_data = *x; *x = *y; *y = g_data; //解锁 } void swap1(int *x,int *y){ int data; data = *x; *x = *y; *y = data; } swap1函数具备可重入性,swap如果不上锁、解锁不具备可重入性(里面用到全局变量)。 函数可重入性:
  1. 尽量避免使用访问全局变量
  2. 如果要访问全局变量,一定要做好互拆访问,当然这种会千万代码的执行效率变低!

linux内核定时器:
1,系统定时器硬件
特点:
这是个硬件电路,而不是软件!
可以通过软件编程实现定时器硬件的工作输出频率!
系统定时器硬件能够周期性的,稳定的,给CPU发送一个时钟中断!
2,linux内核的系统定时器硬件的时钟中断处理函数做哪些事件?
时钟中断处理函数能够周期性的被内核调用!
更新系统的运行时间,关键更新jiffies_64,jeffies取的jiffies_64的低32位!
更新实际时间(墙上时间)
检查进程的时间片,决定是否需要进行调度
检查是否有超时的软件内核定时器,如果有超时的定时器,内核就运行定时器对应的处理函数。

3,概念
HZ:常数,对于ARM架构HZ=100,对于X86架构HZ=1000,表示1秒钟系统定时器硬件能够产生100次或者1000次的时钟中断;系统定时器的频率就是根据HZ来设置!
tick:节拍率 1tick = 1/HZ ,产生一次时钟中断的时间为10ms(HZ=1000)
jiffies:linux内核的全局的变量,一般用于记录自开机以来产生了多少次时钟中断,每发生一次,jiffies自动加1.
注意:jiffies的回绕问题,一般用jiffies来表示流失时间
在进行超时判断时,不允许出现:
if(jiffiest>timout) //这样的代码是不允许出现的
应该:
time_before(jiffies,timeout)
或者
time_after(jiffies,timout);
linux内核定时器 linux内核软件定时器的数据结构: struct timer_list { struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); }; 成员说明:
expires:超时时候jiffies的值,例如如果定时器的超时时间为10秒钟:expires = jiffies+10*HZ;
function:定时器的超时处理函数,一旦定时器到期,内核就会执行这个超时处理函数!
切记:定时器本身是基于软中断实现,那么超时处理函数运行在中断上下文中,不允许做休眠动作!
data:给超时处理函数传递的参数,一般传递的指针!
注意:检查定时器是否超时是在时钟中断处理函数中进行! 如何在驱动中实现一个定时器?
1,分配定时器对象
struct timer_list mytimer;
2,初始化定时器对象
init_timer(&mytimer);//并没有指定超时时间,超时处理函数和传递参数,需要进行额外的初始化工作
mytimer.expires = jiffies+10*HZ;//设置时间
mytimer.function = mytimer_func
mytimer.data = (unsigned long)&mydata;
3,向内核添加和启动定时器
add_timer(&mytimer);//一旦添加完毕,内核就开始对这个定时器进行倒计时!一旦定时器到期,内核执行对应的超时处理函数 ,并且删除这个定时器!
4,删除定时器
del_timer(&mytimer);//定时器没有到期,删除定时器有意义,如果定时器到期了,删除就没有意义了(定时器到期,内核会删除定时器)
5,修改定时器
mod_timer(&mytimer,jiffies+20*HZ);
注意:
mod_timer等价于:
1,先删除定时器
2,修改定时器的超时时间expires = jiffies + 20*HZ
3,再次添加启动定时器,add_timer
注意:在实际使用的的时候,千万不要单独使用以上1-3步来实现定时器的修改,因为这1,2,3这三步骤的执行路径不具备原子性!
“代码执行路径具有原子性”: 一段代码在被执行的时候,不需要别的任务打断这个执行路径
6,定时器一旦到期,内核只执行一次超时处理函数,如果要重复执行超时处理函数,只需要在超时处理函数中重新添加启动定时器即可!一般利用mod_timer进行添加即可! 案例:利用定时器,每2秒钟打印一句话
案例:利用定时吕在,第2秒开关灯
案例:利用定时器,加载驱动 模块以后,能够动态修改灯的闪动频
率:100ms,300ms,600,ms,1000ms,2000ms
案例:在定时器的处理函数 中添加:ssleep(1000);查看实现
提示:
1,内核模块参数的知识点;
static int times = 2000;
module_param(times,int,0664)
2,毫秒和jiffies之间的转换:
3,测试:
echo 100 >/sys/module/mytimer/paramters/times 利用定时器,每2秒钟打印一句话代码: #include #include #include #include #include //分配定时器对象 static struct timer_list mytimer; static int g_data = 0x55; //超时处理函数 static void mytimer_functon(unsigned long data){ printk("%s:data = %#x ",__func__,*(int*)data); //可以在这里开关灯 //每2秒钟要打印一次,重新添加定时器 mod_timer(&mytimer,jiffies+msecs_to_jiffies(2000)); } static int mytimer_init(void) { //申请GPIO资源,配置GPIO为输出,输出0 //初始化定时器对象 init_tiemr(&mytimer); //初始化定器的超时时间 mytiemr.expires = jiffies + msecs_to_jiffise(2000); //初始化定时器的超时处理函数 mytimer.function = mytimer_function; //给超时处理函数传递参数 mytimer.data = (unsigned long)&g_data; //启动定时器 add_timer(&mytimer);//定时器开始倒计时! printk("定时器初始化完毕!"); return 0; } static void mytimer_exit(void) { //删除定时器 del_timer(&mytimer); //释放GPIO资源 } module_init(mytimer_init); module_exit(mytimer_exit); MODULE_LICENSE("GPL");
linux内核延时的方法:
  1. 忙延时 :CPU原地突转!错过要延时的时间!
    忙延时的方法:
    ndelay(纳秒数);//纳秒延时
    udelay(微秒数)//微秒延时
    mdelay(毫秒数)//毫秒级的延时
    注意:但凡使用jiffies实现的延时,最小的延时时间为10ms,如果 小于这个时间的延时,必须采用以上三种方法!
    注意:mdelay这个函数的延时,如果延时时间大于10ms,一般使用休眠延时(jiffies),如果小于10ms,要使用mdelay
    以上延时函数的实现是基于BogoMIPS:
    查看S5PV210的bogomips
    cat /proc/cpuinfo
    BogoMIPS :998.15
    意思:一秒钟CPU执行998.15个百成条指令!
  2. 休眠延时:仅仅适合于进程!
    方法:
    msleep(毫秒数)//毫秒级休眠
    ssleep(秒数);//秒级休眠
    schedule();//永久休眠
    schedule_timeout(5*HZ)//指定时间的休眠 ,5秒以后,自动唤醒
    注意:以上四个方法不能用于中断上下文!

两个SecureCRT串口软件能同时打开同一个串口硬件设备吗?
案例:要求LED设备只能被一个应用程序所打开访问操作!(要么在应用程序中实现,要么在驱动中实现)
驱动实现过程:
这里写图片描述