DAY6-信号量,中断和内核时间流

232 阅读6分钟

(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.内核中竞态机制的选择

image-20220801235422614

八.中断

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.以按键为例实现内核中的中断

image-20220801235434664

image-20220801235454144

按键按下低电平,松开高电平

K2 K3 K4 K6分别连接到了GPIOA28 GPIOB30 GPIOB31 GPIOB9

练习:

加上其他两个按键,测试触发效果

4.中断的顶半部和底半部

中断程序的执行越快越好,但是在某些场合无法满足。比如网络设备接收数据的过程,网络设备对数据的处理比较耗时,但是网络设备接收数据必须用中断实现。长时间处于中断影响系统正常运行。

Linux内核为了解决上述问题,将中断处理分为两部分 ----------- 顶半部和底半部

顶半部就是原来的中断处理程序,用于处理比较紧急,必须立即处理的事务,比如网卡设备将数据从网卡拷贝到内存的过程。顶半部不能被打断,而且必须在顶半部中登记底半部,CPU在空闲的时候去执行底半部中的内容。

底半部执行一些不紧急,耗时长的工作,比如网卡数据交给协议层的过程。

image-20220801235443131

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    

工作/延时工作处于进程上下文,使用内核线程来执行,参与任务调度,允许睡眠。

练习:

将按键判断的代码移动到工作处理函数中实现。

按键抖动:

image-20220826171417031

在内核中可以使用内核定时器去除按键抖动。

九.内核中的时间流

1.概念

tick:内核心跳时钟,周期性产生时钟中断,每一次时钟中断完成系统相关的工作,HZ是tick的倒数(内核心跳频率)

HZ = 100 tick = 1/HZ = 10ms jiffies:内核中用于表示时间的全局变量,记录了开机以来发生了多少次时钟中断

2.内核中提供了jiffies和绝对时间转换的函数

image-20220801235743388

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内没有下一次按键中断,才会进入超时处理函数 ---------- 认为是一次真实按键事件。

image-20220801235753751

十.使用中断和定时器实现按键驱动

将按键中断/内核定时器和字符设备驱动框架结合起来,但是按键事件的发生和数据上报没有直接联系。正确的操作应该是发生了按键事件才向用户上报按键事件的数据。

要实现内核中有按键事件才向用户空间上报数据的功能,需要同步操作,内核中可以使用等待队列实现同步。

等待队列的使用:

需要包含的头文件:

#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 ----- 松开