目录
一、内核中的竞争状态和互斥
浅谈可重入函数与不可重入函数:https://blog.csdn.net/chenyefei/article/details/82682241
1、一些概念
(1)竞争状态(简称竟态)
并发:多CPU、多任务、多中断操作一块相同的代码,在未运行完情况下被打断。
(2)临界段、互斥锁、死锁
临界段:有可能出问题、出现并发的代码段,在这种代码段的前后加上互斥从而保护它。
互斥锁:直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
(3)同步(多CPU、多任务、中断)
2、解决竟态的方法
(1)原子操作 automic_t
操作开始无法被打断直至执行完。
(2)信号量、互斥锁
详解:https://www.cnblogs.com/alinh/p/6905221.html
https://baike.baidu.com/item/%E4%BF%A1%E5%8F%B7%E9%87%8F/9807501?fr=aladdin//百度百科
信号量:struct semaohore结构体在内核中定义如下:
struct semaphore {
raw_spinlock_t lock;
/**
* *lock变量是用来对count变量起保护作用的。当要改变count要改变时,及在down/up函数中应该会调用spin_lock/spin_unlock锁定lock锁和释放lock锁
* *如果count该值大于0,表示资源是空闲的。如果等于0,表示信号量是忙的,但是没有进程在等待这个资源。
* 如果count为负,表示资源忙,并且至少有一个进程在等待。
* 但是请注意,负值并不代表等待的进程数量。
*/
unsigned int count;
struct list_head wait_list;
};
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。
比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的,也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
(3)自旋锁
信号量支持休眠,可暂时交出CPU,提高CPU的利用率。而自旋锁则是在原地打转(类似while(1)),但时间不会很长,不会交出CPU
3、自旋锁和信号量的使用要点
(1)自旋锁不能递归,因为其不会休眠交出CPU
(2)自旋锁可以用在中断上下文(信号量不可以,因为可能睡眠),但是在中断上下文中获取自旋锁之前要先禁用本地中断(拥有自旋锁的代码必须不能睡眠,所以不能被其他中断打断)
中断程序是不参与进程调度的,一旦中断程序中进入睡眠交出cpu,是无法被调度接着执行中断处理程序的。在一个中断处理程序中,信号量不可以出现,因为中断处理程序不参与进程调度。一旦运行,必须运行完,在这个过程中是不能交出CPU的,然而信号量本身是可以睡眠的。
(3)自旋锁的核心要求是:拥有自旋锁的代码必须不能睡眠,要一直持有CPU直到释放自旋锁。
多核CPU,每个核心都可以单独打开进入中断,拿着自旋锁的代码一定不可睡,当其休眠了其拿着的锁还没交出,若此时某个进程再去申请锁,则会陷入死锁的状态。
(4)进程上下文,意思是可执行程序代码,是进程的重要组成部分。进程上下文实际上是进程执行活动全过程的静态描述。包含每个进程执行过的、执行时的以及待执行的指令和数据;在指令寄存器、堆栈、状态字寄存器等中的内容。此外, 还包括进程打开的文件描述符等.
内核同步机制-读写信号量(rw_semaphore):读/写信号量适于在读多写少的情况下使用。如果
一个任务需要读和写操作时,它将被看作写者,在不需要写操作的情况下可降级为读者。任意多
个读者可同时拥有一个读/写信号量,对临界区代码进行操作。
在没有写者操作时,任何读者都可成功获得读/写信号量进行读操作。如果有写者在操作时,读者
必须被挂起等待直到写者释放该信号量。在没有写者或读者操作时,写者必须等待前面的写者或
读者释放该信号量后,才能访问临界区。写者独占临界区,排斥其他的写者和读者,而读者只排
斥写者。
读/写信号量可通过依赖硬件架构或纯软件代码两种方式实现
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,不可用于中断上下文,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占(高优先级进程抢走CPU)失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
Linux用户抢占和内核抢占详解:https://blog.csdn.net/gatieme/article/details/51872618
二、中断的上下半部
应用程序的运行环境:用户态下的进程上下文,由内核进行调度,可以在没运行完的情况下交出CPU,然后再次被调度得到CPU。输入类设备的事件是异步事件,并无确定的时间。
1、中断处理的注意点
(1)中断上下文:处在一个中断处理程序中,一种特殊的上下文,无法被内核调度系统所调度,不能和用户空间进行数据交互(copy_to_user, copy_from_user函数)
(2)不能交出CPU(不能休眠、不能schedule,即使用调度接口)
(3)ISR运行时间应尽可能短,越长则系统响应特性越差,不能直接或间接调用sleep()函数。
2、中断下半部2种解决方案详解
详解必看:https://blog.csdn.net/godleading/article/details/52971179
linux人为的将ISR分为两部,为什么要分上半部(top half,又叫顶半部)和下半部(bottom half,又叫底半部)
上半部就是之前所说的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在下半部执行。
拿网卡来举例,在linux内核中,当网卡一旦接受到数据,网卡会通过中断告诉内核处理数据,内核会在网卡中断处理函数(上半部)执行一些网卡硬件的必要设置,因为这是在中断响应后急切要干的事情。接着,内核调用对应的下半部函数来处理网卡接收到的数据,因为数据处理没必要在中断处理函数里面马上执行,可以将中断让出来做更紧迫的事情。
(2)下半部处理策略1:tasklet(小任务),负责登记中断、做与中断标记相关的工作(即中断相关寄存器的那些标志位),时间段比较紧急的工作
(3)下半部处理策略2:workqueue(工作队列)
实质并未省去总体的时间,只不过将一个整体分为了两部分,先执行一部分等一下(处理紧急的事情)再去执行另一部分,这样就兼顾了系统的响应特性
3、tasklet使用实战
(1)tasklet相关接口介绍(内核搜索该关键字)
tasklet_schedule(&host->tasklet);//放在上半部最后激活下半部
kernel/include/linux/interrupt.h
//描述tasklet的结构体
struct tasklet_struct
{
struct tasklet_struct *next;//将多个tasklet链接成单向循环链表
unsigned long state;//TASKLET_STATE_SCHED(Tasklet is scheduled for execution) TASKLET_STATE_RUN(Tasklet is running (SMP only))
atomic_t count;//0:激活tasklet 非0:禁用tasklet
void (*func)(unsigned long); //用户自定义函数
unsigned long data; //函数入参
};
//定义名字为name的非激活tasklet
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
static inline void tasklet_disable(struct tasklet_struct *t)
//函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet被再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出
static inline void tasklet_enable(struct tasklet_struct *t)
//使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"
static inline void tasklet_schedule(struct tasklet_struct *t)
//调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己
tasklet_hi_schedule(struct tasklet_struct *t)
//和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期.
tasklet_kill(struct tasklet_struct *t)
//确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync
//tasklet调度实现
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
完整代码示例:
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/irqs.h>//arch\arm\mach-s3c2410\include\mach\irqs.h
#include <linux/interrupt.h>
#include <linux/gpio.h>
/*
* X210:
*
* POWER -> EINT1 -> GPH0_1
* LEFT -> EINT2 -> GPH0_2//使用
* DOWN -> EINT3 -> GPH0_3//使用
* UP -> KP_COL0 -> GPH2_0
* RIGHT -> KP_COL1 -> GPH2_1
* MENU -> KP_COL3 -> GPH2_3 (KEY_A)
* BACK -> KP_COL2 -> GPH2_2 (KEY_B)
*/
#define BUTTON_IRQ IRQ_EINT2
static struct input_dev *button_dev;
void func(unsigned long data)//下半部函数
{
//input_report_key(button_dev, BTN_0, inb(BUTTON_PORT) & 1);
//inb(BUTTON_PORT) & 1:读取gpio的值,但210不支持
int flag;
printk("key-s5pv210:this is bottom half.\n");
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0)); // input模式
flag = gpio_get_value(S5PV210_GPH0(2));
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f)); // eint2模式
input_report_key(button_dev, KEY_LEFT, !flag);
input_sync(button_dev);
}
DECLARE_TASKLET(mytasklet, func, 0);
static irqreturn_t button_interrupt(int irq, void *dummy) //上半部函数
{
printk("key-s5pv210:this is top half.\n");
tasklet_schedule(&mytasklet);//调用下半部函数
return IRQ_HANDLED;
}
static int __init button_init(void)
{
int error;
error = gpio_request(S5PV210_GPH0(2), "GPH0_2");
if (error)
{
printk("key_s5pvb210:request gpio GPH0(2) fail.\n");
}
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f));// eint2模式,即外部中断
//这个值应该有问题根据我的分析。应为0x0f00
//但改为这个值只可执行一次中断
if (request_irq(BUTTON_IRQ, button_interrupt, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "button-X210", NULL))//申请中断
{
printk(KERN_ERR "key_s5pvb210.c: Can't allocate irq %d\n", BUTTON_IRQ);
return -EBUSY;
}
button_dev = input_allocate_device();
if (!button_dev)
{
printk(KERN_ERR "key_s5pvb210.c: Not enough memory\n");
error = -ENOMEM;
goto err_free_irq;
}
button_dev->evbit[0] = BIT_MASK(EV_KEY);//老方法,新方法使用:set_bit
button_dev->keybit[BIT_WORD(KEY_LEFT)] = BIT_MASK(KEY_LEFT);//input.h
error = input_register_device(button_dev);
if (error)
{
printk(KERN_ERR "key_s5pvb210.c: Failed to register device\n");
goto err_free_dev;
}
return 0;
err_free_dev:
input_free_device(button_dev);
err_free_irq:
free_irq(BUTTON_IRQ, button_interrupt);
return error;
}
static void __exit button_exit(void)
{
input_unregister_device(button_dev);
gpio_free(S5PV210_GPH0(2));
free_irq(BUTTON_IRQ, button_interrupt);
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("aston <1264671872@qq.com>");
MODULE_DESCRIPTION("key driver for x210 button.");
4、workqueue实战演示
(1)workqueue的突出特点是下半部会交给worker thead,因此下半部处于进程上下文,可以被调度,因此可以睡眠。
(2)kernel/include/linux/workqueue.h
struct work_struct {
atomic_long_t data; //传递给工作函数的参数
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry; //链表结构,链接同一工作队列上的工作。
work_func_t func; //工作函数,用户自定义实现
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
//工作队列执行函数的原型:
void (*work_func_t)(struct work_struct *work);
//该函数会由一个工作者线程执行,因此其在进程上下文中,可以睡眠也可以中断。但只能在内核中运行,无法访问用户空间。
#define __WORK_INITIALIZER(n, f) { \
.data = WORK_DATA_STATIC_INIT(), \
.entry = { &(n).entry, &(n).entry }, \
.func = (f), \
__WORK_INIT_LOCKDEP_MAP(#n, &(n)) \
}
//定义正常执行的工作项
DECLARE_WORK(name,function) \
struct work_struct n = __WORK_INITIALIZER(n, f)
//调度默认工作队列
extern int schedule_work(struct work_struct *work);
完整代码示例:
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/irqs.h>//arch\arm\mach-s3c2410\include\mach\irqs.h
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/workqueue.h>
/*
* X210:
*
* POWER -> EINT1 -> GPH0_1
* LEFT -> EINT2 -> GPH0_2//使用
* DOWN -> EINT3 -> GPH0_3//使用
* UP -> KP_COL0 -> GPH2_0
* RIGHT -> KP_COL1 -> GPH2_1
* MENU -> KP_COL3 -> GPH2_3 (KEY_A)
* BACK -> KP_COL2 -> GPH2_2 (KEY_B)
*/
#define BUTTON_IRQ IRQ_EINT2
static struct input_dev *button_dev;
//void func(unsigned long data)//下半部函数
void func(struct work_struct *work)
{
//input_report_key(button_dev, BTN_0, inb(BUTTON_PORT) & 1);
//inb(BUTTON_PORT) & 1:读取gpio的值,但210不支持
int flag;
printk("key-s5pv210:this is workqueue bottom half.\n");
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0)); // input模式
flag = gpio_get_value(S5PV210_GPH0(2));
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f)); // eint2模式
input_report_key(button_dev, KEY_LEFT, !flag);
input_sync(button_dev);
}
//DECLARE_TASKLET(mytasklet, func, 0);
DECLARE_WORK(mywork, func);
static irqreturn_t button_interrupt(int irq, void *dummy)
{
printk("key-s5pv210:this is workqueue top half.\n");
//tasklet_schedule(&mytasklet);
schedule_work(&mywork);//调用下半部函数
return IRQ_HANDLED;
}
static int __init button_init(void)
{
int error;
error = gpio_request(S5PV210_GPH0(2), "GPH0_2");
if (error)
{
printk("key_s5pvb210:request gpio GPH0(2) fail.\n");
}
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f));// eint2模式,即外部中断
//这个值应该有问题根据我的分析。应为0x0f00
//但改为这个值只可执行一次中断
if (request_irq(BUTTON_IRQ, button_interrupt, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "button-X210", NULL))//申请中断
{
printk(KERN_ERR "key_s5pvb210.c: Can't allocate irq %d\n", BUTTON_IRQ);
return -EBUSY;
}
button_dev = input_allocate_device();
if (!button_dev)
{
printk(KERN_ERR "key_s5pvb210.c: Not enough memory\n");
error = -ENOMEM;
goto err_free_irq;
}
button_dev->evbit[0] = BIT_MASK(EV_KEY);//老方法,新方法使用:set_bit
button_dev->keybit[BIT_WORD(KEY_LEFT)] = BIT_MASK(KEY_LEFT);//input.h
error = input_register_device(button_dev);
if (error)
{
printk(KERN_ERR "key_s5pvb210.c: Failed to register device\n");
goto err_free_dev;
}
return 0;
err_free_dev:
input_free_device(button_dev);
err_free_irq:
free_irq(BUTTON_IRQ, button_interrupt);
return error;
}
static void __exit button_exit(void)
{
input_unregister_device(button_dev);
gpio_free(S5PV210_GPH0(2));
free_irq(BUTTON_IRQ, button_interrupt);
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("aston <1264671872@qq.com>");
MODULE_DESCRIPTION("key driver for x210 button.");
5、中断上下半部处理原则
(1)必须立即进行紧急处理的极少量任务放入在中断的顶半部中,此时屏蔽了与自己同类型的中断,由于任务量少,所以可以迅速不受打扰地处理完紧急任务。
(2)需要较少时间的中等数量的急迫任务放在tasklet中。此时不会屏蔽任何中断(包括与自己的顶半部同类型的中断),所以不影响顶半部对紧急事务的处理;同时又不会进行用户进程调度(说明tasklet下半部也是中断上下文),从而保证了自己急迫任务得以迅速完成。
(3)需要较多时间且并不急迫(允许被操作系统剥夺运行权)的大量任务放在workqueue中。此时操作系统会尽量快速处理完这个任务,但如果任务量太大,期间操作系统也会有机会调度别的用户进程运行,从而保证不会因为这个任务需要运行时间将其它用户进程无法进行。
(4)可能引起睡眠的任务放在workqueue中。因为在workqueue中睡眠是安全的。在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时(也会有睡眠),用workqueue很合适。
注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来并且引用了部分他人博客的内容,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。