(4)信号量
需要包含头文件:
#include <linux/semaphore.h>
内核中的信号量本质上是睡眠锁,临界区时间可以很长,也允许睡眠。
1)信号量的使用
```c
1.分配初始化
struct semaphore sema;
sema_init(&sema,初始值);
2.获取信号量
down(&sema);//进程获取不到信号量,进入不可中断的睡眠状态
down_interruptible(&sema);//进程获取不到信号量,进入可中断的睡眠状态,可以被信号唤醒
//返回0表示获取到了信号量,返回非0表示被信号打断
down_trylock(&sema);//获取不到信号量直接返回非0,获取到返回0
//可以用于中断上下文
3.执行临界区代码
时间可以很长,可以睡眠
4.释放信号量
up(&sema);//释放信号量,唤醒等待信号量的进程
```
2)信号量的扩展
信号量也有读写信号量(struct rw_semaphore),读写信号量获取分为获取读信号量和获取写信号量,获取读信号量之后还可以继续获取读信号量,但是不允许获取写信号量。
获取写信号量之后,既不允许获取读信号量,也不允许获取写信号量。
互斥体(struct mutex)也可以用于内核竞态,相当于初始值为1的信号量。
3.内核中竞态机制的选择
八.中断
1.概念
在ARM处理器中,中断是异常的一种,处理中断按照异常的流程来处理。Linux内核中中断的机制已经实现,驱动中使用中断相当于使用资源,使用前必须先申请。
需要包含的头文件:
#include <linux/irq.h>
#include <linux/interrupt.h>
申请 ------------------- request_irq
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);//传递给中断处理函数的参数
成功返回0,失败返回非0
释放 -------------- free_irq
```c
void free_irq(unsigned int irq,//中断号
void *dev_id);//传递给中断处理函数的参数
```
如果是GPIO外部中断,可以通过GPIO端口号来获取中断号
int gpio_to_irq(unsigned gpio);
//传入gpio端口号,返回对应中断号
内核中中断处理函数编写注意事项
1.中断处理程序的执行时间不能过长
2.中断程序属于内核空间,不参与任务调度
3.中断程序不能调用引起睡眠的函数(copy_to...)
中断处理函数的返回值表示中断处理是否成功
IRQ_NONE ---------- 失败
IRQ_HANDLED ------- 成功
注:申请成功的中断的属性可以在/proc/interrupts中查询到
3.以按键为例实现内核中的中断
按键按下低电平,松开高电平
K2 K3 K4 K6分别连接到了GPIOA28 GPIOB30 GPIOB31 GPIOB9
练习:
加上其他两个按键,测试触发效果
4.中断的顶半部和底半部
中断程序的执行越快越好,但是在某些场合无法满足。比如网络设备接收数据的过程,网络设备对数据的处理比较耗时,但是网络设备接收数据必须用中断实现。长时间处于中断影响系统正常运行。
Linux内核为了解决上述问题,将中断处理分为两部分 ----------- 顶半部和底半部
顶半部就是原来的中断处理程序,用于处理比较紧急,必须立即处理的事务,比如网卡设备将数据从网卡拷贝到内存的过程。顶半部不能被打断,而且必须在顶半部中登记底半部,CPU在空闲的时候去执行底半部中的内容。
底半部执行一些不紧急,耗时长的工作,比如网卡数据交给协议层的过程。
5.如何实现底半部
软件中断(swi)
tasklet
工作队列
(1)tasklet
1)数据结构
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);//tasklet的处理函数 ----- 底半部
unsigned long data;//传递给tasklet处理函数的参数
};
2)编程使用
1.分配初始化
struct tasklet_struct mytasklet;
tasklet_init(&mytasklet,处理函数,处理函数的参数);
或者
DECLARE_TASKLET(tasklet名,处理函数,处理函数的参数);
2.在顶半部登记tasklet
tasklet_schedule(&mytasklet);
tasklet本身工作在中断上下文,处理函数中不能睡眠
(2)工作队列
需要包含的头文件:
#include <linux/workqueue.h>
taskle处理函数不能睡眠,如果底半部需要睡眠可以使用工作队列,工作队列包括工作和延时工作。
1)数据结构
//工作
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;//工作的处理函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
//延时工作
struct delayed_work {
struct work_struct work;//工作
struct timer_list timer;//内核定时器
};
2)编程使用
1.分配初始化
struct work_struct mywork;
struct delayed_work mydwork;
INIT_WORK(&mywork,工作处理函数);
INIT_DELAYED_WORK(&mydwork,延时工作处理函数);
2.在顶半部中登记工作/延时工作
shedule_work(&mywork);
shedule_delayed_work(&mydwork,延时时间);//延时时间 HZ <===> 1s
工作/延时工作处于进程上下文,使用内核线程来执行,参与任务调度,允许睡眠。
练习:
将按键判断的代码移动到工作处理函数中实现。
按键抖动:
在内核中可以使用内核定时器去除按键抖动。
九.内核中的时间流
1.概念
tick:内核心跳时钟,周期性产生时钟中断,每一次时钟中断完成系统相关的工作,HZ是tick的倒数(内核心跳频率)
HZ = 100 tick = 1/HZ = 10ms jiffies:内核中用于表示时间的全局变量,记录了开机以来发生了多少次时钟中断
2.内核中提供了jiffies和绝对时间转换的函数
3.内核中的延时函数
<100ms
ndelay
udelay
mdelay
>100ms
msleep
msleep_interruptible
ssleep
4.内核定时器
需要包含的头文件:
#include <linux/timer.h>
内核定时器属于内核提供的定时结构,属于软件定时器,用于非精准的定时功能。
(1)数据结构
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;//超时时间,超时时间点jiffies的值
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
};
(2)编程使用
(2)编程使用
1.分配内核定时器
struct timer_list mytimer;
2.初始化内核定时器
init_timer(&mytimer);
//关键的三个成员手动初始化
mytimer.expires = jiffies+超时时间;
mytimer.function = 超时处理函数;
mytimer.data = 超时处理函数的参数;
3.向内核添加启动内核定时器
add_timer(&mytimer);
4.从内核删除定时器
del_timer(&mytimer);
5.重置定时器 ---------- 修改超时时间
mod_timer(&mytimer,新的超时时间);
//mod_timer = del_timer + 修改expires + add_timer
练习:
使用内核定时器实现D7 D8 D9 D10 1s闪烁
5.使用内核定时器消除按键抖动
按键按下和松开时,会产生多个上升沿和下降沿,从而触发多次中断,实际只有一个按键事件,但是每次抖动<10ms。
如果要去掉这些抖动,10ms以内的下一次中断,要被认为是无效的按键事件。可以设置一个内核定时器的超时时间为10ms,每次触发中断都去重置内核定时器,在这种情况下,如果10ms内发生了下一次按键中断,内核定时器将在超时前被重置。之后中断后10ms内没有下一次按键中断,才会进入超时处理函数 ---------- 认为是一次真实按键事件。
十.使用中断和定时器实现按键驱动
将按键中断/内核定时器和字符设备驱动框架结合起来,但是按键事件的发生和数据上报没有直接联系。正确的操作应该是发生了按键事件才向用户上报按键事件的数据。
要实现内核中有按键事件才向用户空间上报数据的功能,需要同步操作,内核中可以使用等待队列实现同步。
等待队列的使用:
需要包含的头文件:
#include <linux/wait.h>
#include <linux/sched.h>
编程使用:
1.创建等待队列头并初始化
wait_queue_head_t wqh;
init_waitqueue_head(&wqh);
或者
DECALRE_WAIT_QUEUE_HEAD(wqh);
2.睡眠等待唤醒
wait_event_interruptible(wqh,条件);//条件不满足才进入睡眠
//被正常唤醒返回0,被信号打断返回非0错误码
3.唤醒等待的进程
wake_up_interruptible(&wqh);
作业:
修改按键驱动,将键值和按键状态分开上报 ------- 作为两个数据
1 ----- 按下 0 ----- 松开