正点原子嵌入式linux驱动开发——Linux并发与竞争

news2024/10/6 22:33:57

Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源。在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问。在Linux驱动编写过程中对于并发控制的管理非常重要,本章就来学习一下如何在Linux驱动中处理并发

并发与竞争

并发与竞争简介

并发就是多个“用户”同时访问同一个共享资源。Linux系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话可能会导致系统崩溃。现在的Linux系统并发产生的原因很复杂,总结一下有下面几个主要原因:

  1. 多线程并发访问:Linux是多任务(线程)的系统,所以多线程访问是最基本的原因。
  2. 抢占式并发访问,从2.6版本内核开始,Linux内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
  3. 中断程序并发访问。
  4. SMP(多核)核间并发访问,现在ARM架构的多核SOC很常见,多核CPU存在核间并发访问。

并发访问带来的问题就是竞争,临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的,原子访问就表示这一个访问是一个步骤,不能再进行拆分。如果多个线程同时操作临界区就表示存在竞争,在编写驱动的时候一定要注意避免并发和防止竞争访问。一般在编写驱动的时候就要考虑到并发与竞争,而不是驱动都编写完了然后再处理并发与竞争。

保护内容

前面一直说要防止并发访问共享资源,换句话说就是要保护共享资源,防止进行并发访问。那么问题来了,什么是共享资源?也就是保护的内容是什么?保护的不是代码,而是数据!某个线程的局部变量不需要保护,要保护的是多个线程都会访问的共享数据。找到要保护的数据才是重点,而这个也是难点,因为驱动程序各不相同,那么数据也千变万化,一般像全局变量,设备结构体这些肯定是要保护的,至于其他的数据就要根据实际的驱动程序而定了

当发现驱动程序中存在并发和竞争的时候一定要处理掉,接下来依次来学习一下Linux内核提供的几种并发和竞争的处理方法。

原子操作

原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。假如现在要对无符号整形变量a赋值,值为3,用C语言实现很简单,但是C语言要先编译为成汇编指令,ARM架构不支持直接对寄存器(内存)进行读写操作,要借助寄存器R0、R1等来完成赋值操作。假设变量a的地址为0X3000000,“a=3”这一行C语言可能会被编译为如下所示的汇编代码:

示例代码27.2.1.1 汇编示例代码 
1 ldr r0, =0X30000000 /* 变量a地址 */ 
2 ldr r1, = 3 /* 要写入的值 */ 
3 str r1, [r0] /* 将3写入到a变量中 */

示例代码27.2.1.1只是一个简单的举例说明,实际的结果要比示例代码复杂的多。从上述代码可以看出, C语言里面简简单单的一句“a=3”,编译成汇编文件以后变成了3句,那么程序在执行的时候肯定是按照示例代码27.2.1.1中的汇编语句一条一条的执行。假设现在线程A要向a变量写入10这个值,而线程 B也要向a变量写入20这个值,理想中的执行顺序如下图所示:
理想执行情况
按照上图流程,确实可以实现线程A将a变量设置为10,线程B将a变量设置为20。但是实际上的执行流程可能如下图所示:
可能的执行流程
按照上图所示的流程,线程A最终将变量a设置为了20,而并不是要求的10!线程B没有问题。这就是一个最简单的设置变量值的并发与竞争的例子,要解决这个问题就要保证示例代码27.2.1.1中的三行汇编指令作为一个整体运行,也就是作为一个原子存在。Linux内核提供了一组原子操作API函数来完成此功能,Linux内核提供了两组原子操作API函数,一组是对整形变量进行操作的,一组是对位进行操作的

原子整形操作API函数

Linux内核定义了叫做atomic_t的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h文件中,定义如下:

示例代码27.2.2.1 atomic_t 结构体
171 typedef struct {
172     int counter;
173 } atomic_t;

如果要使用原子操作API函数,首先要先定义一个atomic_t的变量,如下所示:

atomic_t a; //定义a

也可以在定义原子变量的时候给原子变量赋初值,如下所示:

atomic_t b = ATOMIC_INIT(0); //定义原子变量b并赋初值为0

可以通过宏ATOMIC_INIT向原子变量赋初值。

原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,Linux内核提供了大量的原子操作API函数,如下图所示:
原子整形操作API函数表
Cortex-A7是32位的架构,所以就只需要用上图所示的32位原子操作函数即可。原子变量和相应API的使用可以参考下例:

示例代码27.2.2.2 原子变量和API函数使用 
atomic_t v = ATOMIC_INIT(0); /* 定义并初始化原子变零v=0 */ 
atomic_set(&v, 10); /* 设置v=10 */ 
atomic_read(&v); /* 读取v的值,肯定是10 */ 
atomic_inc(&v); /* v的值加1,v=11 */

原子位操作API函数

位操作也是很常用的操作,Linux内核也提供了一系列的原子位操作API函数,只不过原子位操作不像原子整形变量那样有个atomic_t的数据结构,原子位操作是直接对内存进行操作,API函数如下图所示:
原子位操作函数表

自旋锁

自旋锁简介

原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中不可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,对于结构体中成员变量的操作也要保证原子性,在线程A对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在Linux内核中就是自旋锁

当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程A持有,线程B想要获取自旋锁,那么线程B就会处于忙循环-旋转-等待状态,线程B不会进入休眠状态或者说去做其他的处理,而是会一直等待锁可用

自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。把自旋锁比作一个变量a,变量a=1的时候表示共享资源可用,当a=0的时候表示共享资源不可用。现在线程A要访问共享资源,发现a=0(自旋锁被其他线程持有)那么线程A就会不断的查询a的值,直到a=1。从这里可以看到自旋锁的一个缺点:等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了。

Linux内核使用了结构体spinlock_t表示自旋锁,结构体定义如下;

示例代码27.3.1.1 spinlock_t结构体 
61 typedef struct spinlock { 
62     union { 
63         struct raw_spinlock rlock; 
64 
65 #ifdef CONFIG_DEBUG_LOCK_ALLOC 
66 # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) 
67         struct { 
68             u8 __padding[LOCK_PADSIZE];
69             struct lockdep_map dep_map; 
70         }; 
71 #endif 
72     }; 
73 } spinlock_t;

在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示:

spinlock_t lock; //定义自旋锁

定义好自旋锁变量以后就可以使用相应的API函数来操作自旋锁。

自旋锁API函数

最基本的自旋锁API函数如下图所示:
自旋锁基本API函数表
上图中的自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占,也就说当线程A得到锁以后会暂时禁止内核抢占。如果线程A在持有锁期间进入了休眠状态,那么线程A会自动放弃CPU使用权。线程B开始运行,线程B也想要获取锁,但是此时锁被A线程持有,而且内核抢占还被禁止了!线程B无法被调度出去,那么线程A就无法运行,锁也就无法释放,死锁发生了!

上图中的API函数用于线程之间的并发访问,如果此时中断也要插一脚,中断也想访问共享资源,那该怎么办呢?首先可以肯定的是,中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本CPU中断,对于多核SOC来说会有多个CPU核),否则可能导致锁死现象的发生,下图所示:
中断打断线程
上图中,线程A先运行,并且获取到了lock这个锁,当线程A运行functionA函数的时候中断发生了,中断抢走了CPU使用权。右边的中断服务函数也要获取lock这个锁,但是这个锁被线程A占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程A是不可能执行的,死锁发生!

最好的解决方法就是获取锁之前关闭本地中断,Linux内核提供了相应的API函数,如下图所示:
线程与中断并发访问处理API函数
使用spin_lock_irq/spin_unlock_irq的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,运行也是“千变万化”,很难确定某个时刻的中断状态,因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用spin_lock_irqsave/ spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock,示例代码如下所示:

示例代码27.3.2.1 自旋锁使用示例 
1 DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */ 
2 
3 /* 线程A */ 
4 void functionA (){ 
5 	  unsigned long flags; /* 中断状态 */ 
6 	  spin_lock_irqsave(&lock, flags) /* 获取锁 */ 
7	   /* 临界区 */ 
8 	  spin_unlock_irqrestore(&lock, flags) /* 释放锁 */ 
9 } 
10 
11 /* 中断服务函数 */ 
12 void irq() { 
13 	  spin_lock(&lock) /* 获取锁 */ 
14 	  /* 临界区 */ 
15 	  spin_unlock(&lock) /* 释放锁 */ 
16 }

下半部(BH)也会竞争共享资源,有些资料也会将下半部叫做底半部。关于下半部后面会讲解,如果要在下半部里面使用自旋锁,可以使用下图中的API函数:
下半部竞争处理函数

其他类型的锁

在自旋锁的基础上还衍生出了其他特定场合使用的锁,这些锁在驱动中其实用的不多,更多的是在Linux内核中使用,本节简单来了解一下这些衍生出来的锁。

读写自旋锁

某个数据结构,读和写不能同时进行,但是可以多人并发的读取。当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。

读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。Linux内核使用rwlock_t结构体表示读写锁,结构体定义如下(删除了条件编译):

示例代码27.3.3.1 rwlock_t结构体 
typedef struct { 
    arch_rwlock_t raw_lock; 
} rwlock_t;

读写锁操作API函数分为两部分,一个是给读使用的,一个是给写使用的,这些API函数如下图所示:
读写锁API函数

顺序锁

顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。Linux内核使用seqlock_t结构体表示顺序锁,结构体定义如下:

示例代码27.3.3.2 seqlock_t结构体 
404 typedef struct { 
405     struct seqcount seqcount; 
406     spinlock_t lock; 
407 } seqlock_t;

关于顺序锁的API函数如下图所示;
顺序锁API函数表

自旋锁使用注意事项

综合前面关于自旋锁的信息,我们需要在使用自旋锁的时候要注意一下几点:

  1. 因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
  2. 自旋锁保护的临界区内不能调用任何可能导致线程休眠的API函数,否则的话可能导致死锁。
  3. 不能递归申请自旋锁,因为一旦通过递归的方式申请一个正在持有的锁,那么就必须“自旋”,等待锁被释放,然而此时正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
  4. 在编写驱动程序的时候必须考虑到驱动的可移植性,因此不管用的是单核的还是多核的SOC,都将其当做多核SOC来编写驱动程序

信号量

信号量简介

Linux内核也提供了信号量机制,信号量常常用于控制对共享资源的访问

相比于自旋锁,信号量可以使线程进入休眠状态,可以看出,使用信号量会提高处理器的使用效率,毕竟不用一直在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:

  1. 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合
  2. 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  3. 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

信号量有一个信号量值,可以通过信号量来控制访问共享资源的访问数量,信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于1,此时的信号量就是一个二值信号量。

信号量API函数

Linux内核使用semaphore结构体表示信号量,结构体内容如下所示:

示例代码27.4.2.1 semaphore结构体 
15 struct semaphore { 
16     raw_spinlock_t     lock; 
17     unsigned int       count; 
18     struct list_head   wait_list; 
19 };

要想使用信号量就得先定义,然后初始化信号量。有关信号量的API函数如下图所示:
信号量API函数
信号量的使用如下所示:

示例代码27.4.2.2 信号量使用示例 
struct semaphore sem; /* 定义信号量 */ 

sema_init(&sem, 1)/* 初始化信号量 */ 

down(&sem); /* 申请信号量 */ 
/* 临界区 */ 
up(&sem); /* 释放信号量 */

互斥体

互斥体简介

在FreeRTOS中也有互斥体,将信号量的值设置为1就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是Linux提供了一个比信号量更专业的机制来进行互斥,它就是互斥体mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在编写Linux驱动的时候遇到需要互斥访问的地方建议使用mutex。Linux内核使用mutex结构体表示互斥体,定义如下(省略条件编译部分):

示例代码27.5.1.1 mutex结构体 
struct mutex { 
    atomic_long_t owner; 
    spinlock_t wait_lock; 
};

在使用mutex之前要先定义一个mutex变量。在使用mutex的时候要注意如下几点:

  1. mutex可以导致休眠,因此不能在中断中使用mutex,中断中只能使用自旋锁。
  2. 和信号量一样,mutex保护的临界区可以调用引起阻塞的API函数
  3. 因为一次只有一个线程可以持有mutex,因此,必须由mutex的持有者释放mutex。并且mutex不能递归上锁和解锁

互斥体API函数

有关互斥体的API函数如下图所示:
互斥体API函数
互斥体的使用如下所示:

示例代码27.5.2.1 互斥体使用示例 
1 struct mutex lock; /* 定义一个互斥体 */ 
2 mutex_init(&lock); /* 初始化互斥体 */ 
3 
4 mutex_lock(&lock); /* 上锁 */ 
5 /* 临界区 */ 
6 mutex_unlock(&lock); /* 解锁 */

Linux内核还有很多其他的处理并发和竞争的机制,主要讲解了常用的原子操作、自旋锁、信号量和互斥体。以后在编写Linux驱动的时候就会频繁的使用到这几种机制。

原子操作实验

这里就是在之前的gpioled.c的基础上进行修改,使用原子操作来实现对LED的互斥访问,即一次只能允许一个应用程序使用LED

实验程序编写

修改设备树文件

这里就是用的LED,之前已经添加过了,所以不需要修改。

LED驱动修改

在gpioled的设备结构体gpioled_dev中,添加了atomic_t结构体变量lock,用来表示原子变量,之后使用static定义一个具象化的gpioled_dev的gpioled,表征led设备。

之后再led_open中,通过atomic_dec_and_test(&gpioled.lock)来判断原子变量的值,进而判断LED是否被别的应用使用,如果返回值<0,证明设备被占用,那就通过atomic_inc(&gpioled.lock)来使得原子变量=0;如果返回值=0为真,说明设备未被占用,设置私有数据private_data。

之后的led_release中,关闭驱动文件的时候通过atomic_inc(&dev->lock)释放原子变量,即将lock+1。

在led_init的初始化中,gpioled.lock通过(atomic_t)ATOMIC_INIT(0)来初始化原子变量,并通过atomic_set设置原子变量初始值为1,这样每次只允许用1个应用使用LED。

编写测试代码

这里与之前的区别,就是在写入数据write之后,会通过while循环模拟占用LED灯25s,这其中每间隔5s会print一次循环次数。

运行测试

编译驱动程序

就是把Makefile的obj-m的值变为atomic.o,然后“make -j8”就可以了。

编译测试APP

可以通过如下命令编译atomicApp.c:

arm-none-linux-gnueabihf-gcc atomicApp.c -o atomicApp

运行测试

将编译得到的atomic.ko和atomicApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载atomic.ko驱动模块:

depmod //第一次加载驱动的时候需要运行此命令
modprobe atomic.ko //加载驱动

加载成功后,可以通过如下命令来运行atomicApp软件,来控制LED:

./atomicApp /dev/gpioled 1 & //打开LED灯

完成后,红色LED会被点亮,通过会输出如下图所示信息:
打开LED灯
此时,可以输入如下命令关闭LED:

./atomicApp /dev/gpioled 0 //关闭LED灯

输入上述命令后,可以看到如下信息:
关闭LED灯
从上图可以看出,打开/dev/gpioled失败!原因是在上图中运行的atomicAPP软件正在占用/dev/gpioled,如果再次运行atomicApp软件去操作/dev/gpioled肯定会失败。必须等待上图中的atomicApp运行结束,也就是25s结束以后其他软件才能去操作/dev/gpioled。这个就是采用原子变量实现一次只能有一个应用程序访问LED灯

如果要卸载驱动的话输入如下命令即可:

rmmod atomic.ko

自旋锁实验

上一节使用原子变量实现了一次只能有一个应用程序访问LED灯,本节使用自旋锁来实现此功能。在使用自旋锁之前,先回顾一下自旋锁的使用注意事项:

  1. 自旋锁保护的临界区要尽可能的短,因此在open函数中申请自旋锁,然后在release函数中释放自旋锁的方法就不可取。可以使用一个变量来表示设备的使用情况,如果设备被使用了那么变量就加1,设备被释放以后变量就减1,只需要使用自旋锁保护这个变量即可
  2. 考虑驱动的兼容性,合理的选择API函数。

综上所述,在本节例程中,通过定义一个变量dev_stats表示设备的使用情况,dev_stats为0的时候表示设备没有被使用,dev_stats大于0的时候表示设备被使用。驱动open函数中先判断dev_stats是否为0也就是判断设备是否可用,如果为0的话就使用设备,并且将dev_stats加 1,表示设备被使用了。使用完以后在release函数中将dev_stats减1,表示设备没有被使用了。因此真正实现设备互斥访问的是变量dev_stats,但是要使用自旋锁对dev_stats来做保护

实验程序编写

修改设备树文件

这里不需要修改。

LED驱动修改

将原来使用atomic的地方换为spinlock即可,其他代码不需要修改。

gpioled_dev中换成spinlock_t lock来表示自旋锁,同时通过int dev_stats表示使用状态。

在led_open中,定义一个unsigned long flags,通过spin_lock_irqsave上锁,然后通过gpioled.dev_stats来判断设备使用情况,如果=1说明被使用了,就通过spin_unlock_irqrestore解锁;否则就直接gpioled.dev_stats++表示被占用,同时使用spin_unlock_irqrestore解锁。

在led_release中,关闭驱动文件的时候调用spin_lock_irqsave上锁,此时如果设备被占用,就直接dev->dev_stats–,之后通过spin_unlock_irqrestore解锁。

之后,在初始化的led_init函数中,调用spin_lock_init初始化自旋锁。

编写测试APP

与上一小节的atomicApp.c一样就可以了。

运行测试

将Makefile的obj-m的值改为spinlock.o,然后“make -j8”编译就可以了。

编译测试APP

通过如下命令编译:

arm-none-linux-gnueabihf-gcc spinlockApp.c -o spinlockApp

运行测试

将上一小节编译出来的spinlock.ko和spinlockApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载spinlock.ko驱动模块:

depmod //第一次加载驱动的时候需要运行此命令
modprobe spinlock.ko //加载驱动

可以通过如下的命令开关LED,实验现象与上一小节是一样的:

./spinlockApp /dev/gpioled 1 & //打开LED灯
./spinlockApp /dev/gpioled 0 //关闭LED灯

卸载驱动可以通过如下命令:

rmmod spinlock.ko

信号量实验

使用信号量来实现一次只能有一个应用程序访问LED灯,信号量可以导致休眠,因此信号量保护的临界区没有运行时间限制,可以在驱动的open函数申请信号量,然后在release函数中释放信号量。但是信号量不能用在中断中,本节实验不会在中断中使用信号量。

实验程序编写

修改设备树文件

这里不需要修改。

LED驱动修改

将原来使用到自旋锁的地方换为信号量即可,其他的内容基本不变。

在gpioled_dev设备结构体中,最后换成struct semaphore sem添加一个信号量。

在led_open中,先设置私有数据,然后可以通过down_interruptible(&gpioled.sem)获取信号量,这里进入休眠是可以被信号打断的,这里如果>=1就可以使用LED;如果=0就不能使用,应用程序就会进入休眠态,等到信号量值>=1才会被唤醒,申请信号量来获取LED使用权。

在led_release中,通过up(&dev->sem)释放信号量,信号量count+1。

在led_init中,通过sema_init(&gpioled.sem, 1)初始化信号量,表明是一个二值信号量。

编写测试APP

这里直接将之前的拷贝过来就可以了。

运行测试

编译驱动程序

把Makefile的obj-m改成semaphore.o然后“make”就可以了。

编译测试APP

通过下面的命令编译即可:

arm-none-linux-gnueabihf-gcc semaApp.c -o semaApp

运行测试

将上一小节编译出来的semaphore.ko和semaApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载semaphore.ko驱动模块:

depmod //第一次加载驱动的时候需要运行此命令
modprobe semaphore.ko //加载驱动

测试方法也与之前一样:

./semaApp /dev/gpioled 1 & //打开LED灯
./semaApp /dev/gpioled 0 & //关闭LED灯

这两条命令连续输入,第一条命令先获取到信号量,因此可以操作LED灯,将LED灯打开,并且占有25S。第二条命令因为获取信号量失败而进入休眠状态,等待第一条命令运行完毕并释放信号量以后才拥有LED灯使用权,将LED灯关闭,运行结果如下图所示:
命令运行过程
卸载驱动可使用如下命令:

rmmod semaphore.ko

互斥体实验

这里LED只涉及开关状态,所以最适合的其实是互斥体mutex。

实验程序编写

修改设备树文件

这里不需要修改。

LED驱动修改

将原来使用到信号量的地方换为mutex即可,其他的内容基本不变。

在gpioled_dev设备结构体中,最后换成struct mutex lock来表示互斥体。

led_open中,通过mutex_lock_interruptible(&gpioled.lock)获取互斥体mtex,如果成功那么就可以使用LED灯,摔了就进入休眠。

led_release中,通过mutex_unlock释放互斥锁。

led_init中,通过mutex_init初始化互斥锁。

编写测试APP

这里直接把之前的拷贝过来就可以了。

运行测试

编写驱动程序

将Makefile的obj-m换成mutex.o,然后直接“make”就可以了。

编译测试APP

可以通过如下命令来编译:

arm-none-linux-gnueabihf-gcc mutexApp.c -o mutexApp
### 运行测试 将上一小节编译出来的mutex.ko和mutexApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载mutex.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe mutex.ko //加载驱动

测试就跟之前是一样的,这里不赘述了。

如果要卸载驱动,可以通过如下命令:

rmmod mutex.ko

总结

这一篇笔记,内容其实跟之前学FreeRTOS很类似,除了没有自旋锁和原子操作,后面的信号量就是一样的,互斥体就是二值信号量。主要的代码驱动编写,也是掌握住几个API就可以了。

主要关注自旋锁和信号量。

自旋锁的上锁和解锁是在led_open之中就可以完成的,通过spin_lock_irqsave上锁,通过自行设定dev_stats来表示使用状态,=0就可以使用,由spin_unlock_irqrestore解锁;在led_release总,还是由dev_stats判断使用情况,一样要先spin_lock_irqsave上锁之后判断使用情况,有使用就dev_stats–然后spin_unlock_irqrestore解锁;同时在led_init之中要通过spin_lock_init初始化自旋锁。

信号量和互斥体就很类似,都是在gpioled_dev定义一个结构体来使用信号量/互斥体;然后在led_open中获取信号量,在led_release释放信号量,在led_init中初始化信号量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1120020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端TypeScript学习day05-索引签名、映射与类型声明文件

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 索引签名类型 映射类型 索引查询&#xff08;访问&#xff09;类型 基本使用 同时查询多个索引的类型…

2023天猫双十一活动时间表 天猫淘宝双11预售几号开始付定金

双十一购物节是生活不可或缺的一部分&#xff0c;不论是满足基本需求还是享受生活乐趣&#xff0c;都需要购物。因此&#xff0c;双十一绝对是一个不容错过的绝佳机会&#xff0c;希望大家能善用这个机会&#xff0c;因为错过了就得再等一整年。 每日领红包&#xff1a;红包有…

基于袋獾优化的BP神经网络(分类应用) - 附代码

基于袋獾优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于袋獾优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.袋獾优化BP神经网络3.1 BP神经网络参数设置3.2 袋獾算法应用 4.测试结果&#xff1a;5.M…

MYSQL第一章节DDL数据定义语言的操作(DDL-数据库操作,DDL-操作表-查询,DDL-操作表-修改,数据库的基本类型)

c知识点合集已经完成欢迎前往主页查看&#xff0c;点点赞点点关注不迷路哦 点我进入c第一章知识点合集 MYSQL第一章节DDL数据定义语言的操作 目录 DDL-数据库操作 查询所有数据库 查询当前数据库 创建数据库 删除数据库 DDL-操作表-查询 查询当前数据库中的所有表 查询表结构…

基于斑马优化的BP神经网络(分类应用) - 附代码

基于斑马优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于斑马优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.斑马优化BP神经网络3.1 BP神经网络参数设置3.2 斑马算法应用 4.测试结果&#xff1a;5.M…

【iOS】MVC模式

MVC&#xff08;Model-View-Controller&#xff0c;模型-视图-控制器&#xff09;模式是相当古老的设计模式之一&#xff0c;ta最早出现在SmallTalk语言中。现在&#xff0c;很多计算机语言和架构都采用了MVC模式。 MVC模式概述 MVC模式是一种设计模式&#xff0c;由3部分组成…

图论05-【无权无向】-图的广度优先遍历-路径问题/检测环/二分图/最短路径问题

文章目录 1. 代码仓库2. 单源路径2.1 思路2.2 主要代码 3. 所有点对路径3.1 思路3.2 主要代码 4. 联通分量5. 环检测5.1 思路5.2 主要代码 6. 二分图检测6.1 思路6.2 主要代码6.2.1 遍历每个联通分量6.2.2 判断相邻两点的颜色是否一致 7. 最短路径问题7.1 思路7.2 代码 1. 代码…

基于SSM的物业管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

AdaBoost:增强机器学习的力量

一、介绍 机器学习已成为现代技术的基石&#xff0c;为从推荐系统到自动驾驶汽车的一切提供动力。在众多机器学习算法中&#xff0c;AdaBoost&#xff08;自适应增强的缩写&#xff09;作为一种强大的集成方法脱颖而出&#xff0c;为该领域的成功做出了重大贡献。AdaBoost 是一…

数据分析案例-顾客购物数据可视化分析(文末送书)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

今年阿里云双十一服务器优惠价格讨论_看看大家怎么说?

2023阿里云双十一云服务器大概会降到什么区间&#xff1f;阿里云服务器网认为会在当前的优惠价格基础上&#xff0c;降价10%左右&#xff0c;可以在阿里云CLUB中心领券&#xff1a;aliyun.club 云服务器专用满减优惠券。阿里云服务器网从各个渠道了解到大家对今年阿里云双十一服…

在用visualstudio连接数据库显示已存在以及此版本的SQLServer不支持用户实例登录解决

在写.NET实验时用visualstudio连接数据库显示”此版本的 SQL Server 不支持用户实例登录标志。该连接将关闭“&#xff0c;我是开始在数据库已经导入了这个mbf文件的。然后就去网上找一堆办法。 失败经历&#xff1a; 按照教程操作后代码语句运行显示数据库已存在。按照网上的…

王道计算机考研 操作系统学习笔记 + 完整思维导图篇章四: 文件管理

目录 文件管理 文件的逻辑结构 无结构文件 有结构文件 顺序文件 索引文件 索引顺序文件 文件目录 文件控制块&#xff08;FCB&#xff09; 目录结构分类 单级目录结构 两级目录结构 多级目录结构 &#xff08;树形目录结构&#xff09; 无环图目录结构 索引节点 文件的物理结构…

【经典PageRank 】02/2 算法和线性代数

系列前文&#xff1a;【经典 PageRank 】01/2 PageRank的基本原理-CSDN博客 一、说明 并非所有连接都同样重要&#xff01; 该算法由 Sergey 和 Lawrence 开发&#xff0c;用于在 Google 搜索中对网页进行排名。基本原则是重要或值得信赖的网页更有可能链接到其他重要网页。例…

2023.10.21 关于 阻塞队列

目录 阻塞队列 优先级队列&#xff08;Priority Queue&#xff09; 阻塞队列&#xff08;Blocking Queue&#xff09; 消息队列&#xff08;Message Queue&#xff09; 生产者消费者模型 生产者消费者模型的两个好处 标准库阻塞队列使用 实现一个简单 生产者消费者模型…

【网络编程】基于epoll的ET模式下的Reactor

需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、Reactor介绍 二、基于epoll的ET模式下的Reactor计算器代码 1、Tcp…

C++之构造函数、析构函数、拷贝构造函数终极指南:玩转对象的诞生、生命周期与复制

W...Y的主页 代码片段分享 前言&#xff1a; 在上篇内容里&#xff0c;我们初识了C中的类与对象&#xff0c;了解了类的定义、类的实例化、 类的作用域等等&#xff0c;今天我们将继续深入了解类与对象的相关内容&#xff0c;学习构造函数、析构函数与拷贝构造函数&#xff…

WebGL笔记:图形转面的原理与实现

1 &#xff09;回顾 WebGL 三种面的适应场景 TRIANGLES 单独三角形TRIANGLE_STRIP 三角带TRIANGLE_FAN 三角扇备注 在实际的引擎开发中&#xff0c;TRIANGLES 是用得最多的TRIANGLES 的优势是可以绘制任意模型&#xff0c;缺点是比较费点 2 &#xff09;适合 TRIANGLES 单独…

Apache JMeter 安装教程

下载&#xff1a; 注意事项&#xff1a;使用JMeter前需要配置JDK环境 下载地址 下载安装以后&#xff0c;打开安装的bin目录 D:\software\apache-jmeter-5.4.1\apache-jmeter-5.4.1\bin&#xff0c;找到jmeter.bat&#xff0c;双击打开 打开后的样子 语言设置&#xff1a; 1…

windows模拟触摸

安装EcoTUIODriver驱动 GitHub - almighty-bungholio/EcoTUIODriver: Diver to convert tuio touch events into windows touch events. Started as GSoC 2012 project. 安装完后电脑属性显示笔和触控为为20点触摸点提供笔和触控支持。 在另一台电脑上运行tuio模块器是一个ja…