【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】

这篇具有很好参考价值的文章主要介绍了【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、上下文和并发

在讨论并发前,先要了解以下几个概念:执行流,上下文,共享与临界等。

什么叫执行流:

  • 【执行流】:有开始有结束总体顺序执行的一段代码 又称上下文

上下文分类:

  • 【任务上下文】:普通的,具有五种状态(就绪态、运行态、睡眠态、暂停态、僵死态),可被阻塞的上下文。
  • 【异常上下文】:异常时的中断处理上下文。不能进入阻塞状态。

\qquad 对于应用层编程而言,只有任务上下文。
\qquad 对于内核编程而言,即有任务上下文也有异常上下文。

什么是共享与临界:

  • 【竞态】:多任务并行执行时,如果在一个时刻同时操作同一个资源,会引起资源的错乱,这种错乱情形被称为竞态
  • 【共享资源】:可能会被多个任务同时使用的资源。
  • 【临界区】:操作共享资源的代码段。

因此为了控制竞态,就需要有相应的“并发控制机制”。

  • 【并发控制机制】:为了解决竞态,需要提供一种控制机制,来避免在同一时刻使用共享资源,这种机制被称为并发控制机制

2、并发控制机制的分类与使用场景

内核中并发控制机制分为以下几类:

  • 原子操作类 :不会被系统包括异常打断的操作。
  • 忙等待类 :通过循环轮询的方式,等待资源可用的操作。
  • 阻塞类:资源不可用时,进入睡眠态等待资源可用。

并发控制机制的使用场景:

  • 互斥场景:多个任务不能同时使用同一资源,一个在用时,其它的处于等待状态,互斥体现的是一种排它性。一般操作过程如下:

对互斥锁初始化为可用 --> 对互斥锁进行P操作锁定 --> 临界区 --> 对互斥锁进行V操作放开

  • 同步场景:多个并行任务对资源的有序访问,同步体现的是一种协作性。

对互斥锁初始化为不可用 --> “先行任务”完成所有操作后 --> “先行方”进行V操作释放资源 --> “后行方”P操作直到锁定资源 -->“后行方”执行操作。

3、并发控制详述

3.1 并发控制机制–中断屏蔽

使用原则:
\qquad 当一个中断服务程序ISR与被打断的任务可能使用相同的资源时。这时,就需要在被打断的任务在进入临界区之前先进行中断屏蔽,等执行完成后再恢复中断。

中断屏蔽相关的函数如下:

中断屏蔽相关函数 使能中断相关函数
local_irq_disable() loacal_irq_enable()
local_irq_save(flags) loacal_irq_restore(flags) 涉及cpu的中断屏蔽字相关
local_bh_disable() local_bh_enable() 与中断低半部有关,操作软中断

注意事项

  • 中断屏蔽后的临界区代码不能占用太长时间,需要尽快完成。否则中被屏蔽会引起系统调度等一系列问题。
  • Local_irq_disable()和local_irq_enable()都只能禁止和使能本CPU内的中断,因此,并不能解决SMP多CPU引发的况态。因此单独使用
  • 中断屏蔽通常不是一种值得推荐的避免况态的方法,它适合与下面要介绍的自旋锁联合使用。

适用场合:中断上下文与某任务共享资源时,或多个不同优先级的中断上下文间共享资源时。

3.1.1 函数详解

3.1.1.1 local_irq_enable 与 local_irq_disable

local_irq_enable()和local_irq_disable()函数用于在Linux内核中临时启用和禁用本地中断。
原型为:

#include <asm/irq.h>

void local_irq_enable(void);
void local_irq_disable(void); 

这两个函数的作用是:

  • local_irq_enable(): 重新启用本地中断,允许硬件在CPU上发送中断信号。
  • local_irq_disable(): 禁用本地中断,防止硬件在CPU上发送任何中断信号。
    本地中断是CPU上除NMI之外的所有外部中断,比如时钟中断、串口中断、网卡中断等。

使用示例:

void do_some_thing(void) 
{
    local_irq_disable();  // 禁用本地中断
    /*做一些需要防止中断的操作*/  
    
    local_irq_enable(); // 操作完成,重新启用中断
}

这两个函数通常用于:

  1. 临时保护一小段关键代码或共享资源,防止中断处理程序干扰。因为中断处理程序会打断正常指令流,可能访问共享资源。
  2. 在原子操作周围,确保整个操作序列不被中断打断,从而避免竞争条件。
  3. 在本地时钟节拍的开头和结尾,用于测量某段代码的执行时间等。

它们会修改CPU的状态寄存器中的IF标志位来禁用或重新启用中断。在禁用中断期间,所有的中断请求都会被屏蔽,直到再次启用中断。


3.1.1.2 local_irq_save 与 local_irq_restore

local_irq_save()和local_irq_restore()函数也用于在Linux内核中临时保存和恢复本地中断状态。
原型为:

#include <asm/irq.h>

unsigned long local_irq_save(void);
void local_irq_restore(unsigned long flags);

这两个函数的作用是:

  • local_irq_save(): 禁用本地中断,并返回此前的中断状态。
  • local_irq_restore(): 根据传入的flags恢复之前保存的本地中断状态。

与local_irq_enable()和local_irq_disable()相比,local_irq_save()可以保存当前的中断状态,并在稍后通过local_irq_restore()恢复。而local_irq_enable()和local_irq_disable()无法达到保存与恢复中断状态的效果。

使用示例:

unsigned long flags;

flags = local_irq_save();  // 禁用中断并保存状态
/*做一些需要防止中断的操作*/  

local_irq_restore(flags); // 操作完成,恢复之前的中断状态

这两个函数的用途与local_irq_enable()和local_irq_disable()类似,常用于:

  1. 临时保护关键代码或共享资源,但需要在稍后恢复原有的中断状态。
  2. 实现复杂的同步机制,根据需要禁用与恢复中断。
  3. 精确测量某段代码的执行时间,通过保存并恢复中断状态实现准确计时。
    与local_irq_enable()/disable()相比,这两个函数可以更精细地控制中断状态,实现根据条件条件性禁用与恢复中断。这在编写驱动程序和内核同步机制时非常有用。
3.1.1.3 local_bh_enable 与 local_bh_disable

local_bh_enable()和local_bh_disable()函数用于在Linux内核中临时启用和禁用**底半部(Bottom Half)**处理。
原型为:

#include <asm/irq.h>

void local_bh_enable(void);
void local_bh_disable(void);

这两个函数的作用是:

  • local_bh_enable(): 重新启用底半部处理,允许待处理的软中断和任务调度被执行。
  • local_bh_disable(): 禁用底半部处理,禁止执行任何软中断处理程序和任务调度。

底半部处理通常由两个部分组成:

  1. 软中断处理:对应内核定时器中断,用于处理超时和延迟机制。
  2. 任务调度:当中断被重新启用时,如果有更高优先级的任务就绪,则调度程序会选择它运行。

这两个函数通过修改本地的嵌套计数来禁用或重新启用底半部处理。只有当嵌套计数变为0时,底半部机制才会被重新启用。

使用示例:

void do_some_thing(void)
{
    /* 首先禁用底半部处理 */
    local_bh_disable();   
    
    /* 开始处理一个硬中断 */
    handle_hard_irq();
    
    /* 硬中断处理完成,检查是否接收到信号 */
    if (signal_pending(current)) {
        /* 收到信号,禁止底半部机制并处理信号 */ 
        local_bh_disable();  
        handle_signal();
        local_bh_enable();  // 信号处理完成,重新启用底半部
    }
    
    /* 其他与硬中断处理相关的操作 */
    do_some_task_1();
    do_some_task_2();
    
    /* 检查软中断是否到期,如果到期则立即处理 */
    if (softirq_pending(smp_processor_id()))
        invoke_softirq();
        
    /* 所有操作完成,重新启用底半部机制 */ 
    local_bh_enable();  
}

这个例子展示了如何在不同的场景下使用local_bh_enable()和local_bh_disable():

  1. 一开始禁用底半部机制,以避免硬中断处理过程中出现任务调度或软中断。
  2. 在硬中断处理完成后,如果收到信号也首先禁用底半部机制,然后处理信号,最后再重新启用。这是因为信号处理也不能进行任务调度与软中断。
  3. 处理与硬中断相关的其它操作时,底半部机制仍然被禁用。
  4. 如果在此期间软中断到期,则立即处理。因为软中断的超时时间很短,必须先处理。
  5. 所有操作完成后,重新启用底半部机制,允许任务调度与软中断处理。
    所以,这段代码展示了根据运行上下文与需求,采取启用或禁用底半部机制的方法。这可以最大限度地避免不同的处理过程之间出现干扰,实现良好的同步与时序控制。

这两个函数通常用于:

  1. 临时禁止软中断和任务调度,从而防止关键代码或资源被打乱。
  2. 处理收到的信号,避免在信号处理期间进行任务调度。
  3. 实现同步机制,根据需要选择性启用或禁用底半部处理。
    如果对底半部机制与这两个函数还不太理解,可以参考我的前面的相关介绍。理解内核的不同运行上下文、调度机制与同步手段是成为Linux内核开发高手的基础。

3.2、并发控制机制–原子变量

原子变量: 存取时不可被打断的特殊整型变量。
适用场合: 共享资源为单个整型变量的互斥场合。

3.2.1 相关函数

对原子变量的操作必须用下面这些专用宏或函数:

3.2.1.1 atomic_t 原子量类型

头文件 /arch/arm/include/asm/atomic.h

#include <linux/types.h

typedef struct {
	int counter;
} atomic_t;

该类型本质是一个数据结构。

3.2.1.2 宏ATOMIC_INIT 创建原子变量

ATOMIC_INIT(i)是一个宏,用于初始化一个原子变量为值i。
它定义在头文件中,展开后的定义为:

#include <asm/atomic.h> 

#define ATOMIC_INIT(i)    { (i) }

也就是将传入的值i放在一个括号对{}中。

这个宏用于静态初始化一个原子变量,语法为:

atomic_t my_atomic = ATOMIC_INIT(10);

这会将my_atomic初始化为10。

原子变量是Linux内核实现同步机制的基础,它可以用于实现自旋锁、计数器、标志位等。ATOMIC_INIT宏提供了一个简单的方法来初始化原子变量。
在内核开发中,我们通常会定义如下变量使用ATOMIC_INIT初始化:

atomic_t counter = ATOMIC_INIT(0);    // 计数器
atomic_t spinlock = ATOMIC_INIT(0);   // 自旋锁 
atomic_t ready = ATOMIC_INIT(0);      // 标志位

然后可以使用如下方法操作这些原子变量:

atomic_inc(&counter);    // 计数器+1
atomic_dec(&counter);    // 计数器-1

atomic_set(&spinlock, 1); // 获得自旋锁
atomic_set(&spinlock, 0); // 释放自旋锁

atomic_set(&ready, 1);   // 设置标志位 
atomic_read(&ready);     // 读取标志位

所以,ATOMIC_INIT宏提供了初始化原子变量的简单方法,配合其它原子操作宏可以实现各种同步机制与计数功能。

3.2.1.3 宏atomic_set 设置原子量的值

头文件 /arch/arm/include/asm/atomic.h
原码:

#include <asm/atomic.h> 

#define atomic_set(v,i)	(((v)->counter) = (i))

所以,可以如下定义原子量的初始值

atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
void atomic_set(atomic_t *v,int i); //设置原子量的值为i

atomic_t my_atomic;
atomic_set(&my_atomic, 10);

而像:v = 10;这样做是错误的, 因为不能对原子变量赋值操作,所以这里是错误的

3.2.1.4 宏atomic_read 获取原子量的值

这个宏用于读取一个原子变量的值
头文件 /arch/arm/include/asm/atomic.h
原码:

#include <asm/atomic.h> 

#define atomic_read(v)	(*(volatile int *)&(v)->counter)

返回值:

返回原子变量*v的counter成员的值 , int类型。

用法为:

atomic_t my_atomic;
int val = atomic_read(&my_atomic);

这会将my_atomic的值读取到val变量中。

atomic_read宏是操作原子变量的基本方法之一,它可以直接读取原子变量的值而不需要任何同步机制。这是因为原子变量在实现上包含了必要的锁或其它同步手段,可以保证各个CPU对其的操作是串行的。

在内核开发中,我们常会使用atomic_read宏:

  1. 读取自旋锁的状态,判断是否上锁:
atomic_t lock = ATOMIC_INIT(0);  
if (atomic_read(&lock)) {
    /* 上锁,需要处理 */ 
}
  1. 读取标志位的状态:
atomic_t ready = ATOMIC_INIT(0);
if (atomic_read(&ready)) {
    /* 标志位已置,进行相关处理 */ 
}
  1. 直接获取计数器的值:
atomic_t count = ATOMIC_INIT(0);
int val = atomic_read(&count);

所以,atomic_read宏提供了一种简单的方法来读取原子变量的值,配合其它原子操作宏可以实现读取各种同步机制与状态。

3.2.1.5 宏atomic_add 与 atomic_sub原子变量加减

头文件 /arch/arm/include/asm/atomic.h
原码:

#include <asm/atomic.h> 

>#define atomic_add(i, v)	(void) atomic_add_return(i, v)
>static inline int atomic_add_return(int i, atomic_t *v)
{
	unsigned long flags;
	int val;
	raw_local_irq_save(flags);
	val = v->counter;
	v->counter = val += i;
	raw_local_irq_restore(flags);
	return val;
}

atomic_add宏利用atomic_add_return()函数来实现真正的ADD操作,后者会根据体系结构采用基于锁或无锁的机制来同步该操作。对使用者来说,atomic_add提供了一个简单的接口来原子更新原子变量的值。

#include <asm/atomic.h>
>#define atomic_sub(i, v)	(void) atomic_sub_return(i, v)
>static inline int atomic_sub_return(int i, atomic_t *v)
{
	unsigned long flags;
	int val;
	raw_local_irq_save(flags);
	val = v->counter;
	v->counter = val -= i;
	raw_local_irq_restore(flags);
	return val;
}

所以可以这样用:

void atomic_add(int i,atomic_t *v);//原子变量增加I,返回V的Int类型值
void atomic_sub(int i,atomic_t *v);//原子变量减少I,返回V的Int类型值
3.2.1.6 宏 atomic_inc 和 atomic_dec 原子变量自增自减

头文件 /arch/arm/include/asm/atomic.h
原型:

#include <asm/atomic.h>
>#define atomic_inc(v)		atomic_add(1, v)
>#define atomic_dec(v)		atomic_sub(1, v)

atomic_inc和atomic_dec宏利用atomic_add()和atomic_sub()来实现加减一操作,这两个函数会根据体系结构采用基于锁或无锁的机制来同步该操作。所以,对使用者来说,atomic_inc和atomic_dec提供了简单的接口来原子更新原子变量的值。

所以可以这样用:

void atomic_inc(atomic_t *v);//原子变量增加1,返回V的Int类型值
void atomic_dec(atomic_t *v);//原子变量减少1,返回V的Int类型值
3.2.1.7 操作并测试

头文件 /arch/arm/include/asm/atomic.h

原型:

#define atomic_inc_and_test(v)	(atomic_add_return(1, v) == 0)
#define atomic_dec_and_test(v)	(atomic_sub_return(1, v) == 0)
#define atomic_sub_and_test(i, v) (atomic_sub_return(i, v) == 0)

所以可以这样使用函数:

运算后结果为0则返回真,否则返回假

int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i,atomic_t *v);
3.2.1.8 ATOMIC_BITOP原子位操作方法

ATOMIC_BITOP()宏用于在一个原子变量上执行位操作,如设置、清除和翻转指定的位。
头文件:/arch/arm/include/asm/bitops.h
原码:

#include <asm/bitops.h>
#define ATOMIC_BITOP(name,nr,p)             (__builtin_constant_p(nr) ? ____atomic_##name(nr, p) : _##name(nr,p))

这个宏定义的意思是:

如果nr(代表bit number,即位号)是一个常数,则使用____atomic_##name形式的位操作函数。
否则,使用_##name形式的位操作函数。

举个例子,如果定义如下宏:

#define SET_BIT(nr, p) ATOMIC_BITOP(set_bit, nr, p)

那么当调用SET_BIT(5, ptr)时,如果5是一个常数,会展开为:

____atomic_set_bit(5, ptr)

否则,会展开为:

_set_bit(nr, ptr)

==============================================================
补充:
在宏定义中使用“##”运算符可以将两个符号连接成一个符号。这被称为“宏连接(Macro Concatenation)”。
例如在这个宏定义中:
#define ATOMIC_BITOP(name,nr,p) (__builtin_constant_p(nr) ? ____atomic_##name(nr, p) : _##name(nr,p))
“##”运算符被用于:
____atomic_##name: 将name和____atomic_连接为一个符号,例如____atomic_set_bit
“_##name”:将_和name连接为一个符号,例如_set_bit
所以如果定义:
#define SET_BIT(nr, p) ATOMIC_BITOP(set_bit, nr, p)
那么这个宏可以展开为:
(__builtin_constant_p(nr) ? ____atomic_set_bit(nr, p) : _set_bit(nr,p))
=============================================================

3.2.1.9 用ATOMIC_BITOP构造所位操作函数

原型
头文件:/arch/arm/include/asm/bitops.h

#define set_bit(nr,p)			ATOMIC_BITOP(set_bit,nr,p)
#define clear_bit(nr,p)			ATOMIC_BITOP(clear_bit,nr,p)
#define change_bit(nr,p)		ATOMIC_BITOP(change_bit,nr,p)
#define test_and_set_bit(nr,p)		ATOMIC_BITOP(test_and_set_bit,nr,p)
#define test_and_clear_bit(nr,p)	ATOMIC_BITOP(test_and_clear_bit,nr,p)
#define test_and_change_bit(nr,p)	ATOMIC_BITOP(test_and_change_bit,nr,p)

解释:

  • set_bit(nr,p):设置p中的第nr位,等价于p |= (1 << nr)
  • clear_bit(nr,p):清除p中的第nr位,等价于p &= ~(1 << nr)
  • change_bit(nr,p):翻转p中的第nr位,等价于p ^= (1 << nr)
  • test_and_set_bit(nr,p):原子地测试第nr位是否为0,如果是则设置该位,并返回原值
  • test_and_clear_bit(nr,p):原子地测试第nr位是否为1,如果是则清除该位,并返回原值
  • test_and_change_bit(nr,p):原子地测试第nr位,并翻转该位,返回原值

所以可以这么用:

  • 设置位
    void set_bit(nr, void *addr); //设置addr所指向的数据的第nr位为1
  • 清除位
    void clear_bit(nr , void *addr);//清除addr所指向的数据的第nr位为0
  • 改变位
    void change_bit(nr , void *addr);//改变addr所指向的数据的第nr位为1
  • 测试位
    void test_bit(nr , void *addr); //测试addr所指向的数据的第nr位是否为1

3.2.2 实例

要求:字符驱动只能被一个应用进程使用。即只能被一个任务open,其它任务在同一时间要打开该设备时会出错提示。

/*************************************************************************
	> File Name: atomic-only.c
    > 作用:以原子变量做为并发控制的手段,使本驱动同时只能被一个应用层进程所open
 ************************************************************************/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/types.h>

/*1、定义重要的变量及结构体*/
struct x_dev_t {
    struct cdev  my_dev;  //cdev设备描述结构体变量
    atomic_t have_open;   //记录驱动是否被打开的原子变量,在init()时初始化,设置初值。在open()时打开
};

struct x_dev_t *pcdev;

/*所有驱动函数声明*/
int open (struct inode *, struct file *);
int close (struct inode *, struct file *);

//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
    .open = open,
    .release = close,
};


static int __init my_init(void){
    int unsucc =0;
    dev_t devno;
    int major,minor;
    pcdev = kzalloc(sizeof(struct x_dev_t), GFP_KERNEL);
    /*2、创建 devno */
    unsucc = alloc_chrdev_region(&devno , 0 , 1 , "atomic-char");
    if (unsucc){
        printk(" creating devno  faild\n");
        return -1;
    }
    major = MAJOR(devno);
    minor = MINOR(devno);
    printk("devno major = %d ; minor = %d;\n",major , minor);

    /*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
    /*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
    cdev_init(&pcdev->my_dev , &fops);
    pcdev->my_dev.owner = THIS_MODULE;
    /*4、注册cdev结构体到内核链表中*/
    unsucc = cdev_add(&pcdev->my_dev,devno,1);
    if (unsucc){
        printk("cdev add faild \n");
        return 1;
    }
    //初始化原子量have_open为1
    atomic_set(&pcdev->have_open,1);
    
    printk("the driver atomic-char initalization completed\n");


    return 0;
}


static void  __exit my_exit(void)
{
    cdev_del(&pcdev->my_dev);
    unregister_chrdev_region(pcdev->my_dev.dev , 1);
    printk("***************the driver atomic-char exit************\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = container_of(pnode->i_cdev,struct x_dev_t , my_dev);
    pf->private_data = (void *)p;
    //在open函数中对原子量have_open进行减1并检测。=0,允许打开文件,<0则不允许打开
    if (atomic_dec_and_test(&p->have_open)){
        printk("atomic-char is opened\n");
        return 0;
    }else{
        printk("device atomic-char can't be opened again\n");
        atomic_inc(&p->have_open);//原子量=-1,记得这里要把原子量加回到0
        return -1;
    }   
}
/*file_operations结构全成员函数.release的具体实现*/
int close(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = (struct x_dev_t *)pf->private_data;
    printk("atomic-char is closed \n");
    atomic_set(&p->have_open,1);
    return 0;
}


module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");

3.3、并发控制机制–自旋锁

3.3.1 概念

\qquad 自旋锁(Spin Lock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某个CPU上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量。由于它是原子操作,所以在该操作完成之前其它执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗说就是“在原地打转”。当自旋锁 持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。

\qquad 自旋锁是基于忙等待的并发控制机制,由于在获取不到资源锁时会进入忙等待,不会进入睡眠状态。忙等待是占时间片的,因此这个机制不要用于过长的临界区执行。也因此,这个互斥锁可用于异常上下文,不会使异常进入阻塞状态。

适用场合:

  1. 异常上下文之间或异常上下文与任务上下文之间共享资源时
  2. 任务上下文之间且临界区执行时间很短时
  3. 互斥问题

3.3.2 函数

头文件: /include/linux/spinlock_types.h

3.3.2.1.定义自旋锁

原码:

#include <linux/spinlock.h>

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
		struct {
			u8 __padding[LOCK_PADSIZE];
			struct lockdep_map dep_map;
		};
#endif
	};
} spinlock_t;
  1. rlock - 这个是Raw spinlock,用于实际的自旋锁定。它包含lock和unlock操作,这是真正控制互斥锁的部分。
  2. dep_map - 这个是依赖关系映射,在开启CONFIG_DEBUG_LOCK_ALLOC宏的情况下使用。它用于记录自旋锁获取和释放之间的依赖关系,以检测潜在的死锁问题。
    所以spinlock_t中嵌入了两个结构:
  • raw_spinlock - 用于实际的锁定,所有配置都会包含这个结构。
  • lockdep_map - 仅在debug配置中包含,用于记录锁依赖关系,以便检测死锁。
    所以使用前先定义自旋锁:
    spinlock_t lock;
3.3.2.2 初始化自旋锁

源码

#define <linux/spinlock.h>

#define spin_lock_init(_lock)				\
do {							\
	spinlock_check(_lock);				\
	raw_spin_lock_init(&(_lock)->rlock);		\
} while (0)
  • spinlock_check()用来检查_lock是否指向一个正确的spinlock_t对象。
  • raw_spin_lock_init()用来初始化raw_spinlock部分,将lock置0等。

用法

spin_lock_init(spinlock_t *);

3.3.2.3 获得自旋锁(即P操作,置资源为被用)

原型:

#include <linux/spinlock.h>

static inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}

static inline void spin_lock_bh(spinlock_t *lock)
{
	raw_spin_lock_bh(&lock->rlock);
}

static inline int spin_trylock(spinlock_t *lock)
{
	return raw_spin_trylock(&lock->rlock);
}

static inline void spin_lock_irq(spinlock_t *lock)
{
	raw_spin_lock_irq(&lock->rlock);
}

解释:

  • spin_lock():成功获得自旋锁立即返回,否则自旋在那里,直到该自旋锁的保持者释放。禁止中断。
  • spin_lock_bh():获取自旋锁。禁止软中断。
  • spin_trylock():成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转”
  • spin_lock_irq: 获取自旋锁lock,禁止中断,并保存中断标志位。

它们的定义均通过调用raw_spinlock部分的对应函数来实现实际的锁定操作。raw_spinlock部分实现最基本的自旋锁机制,这三个接口在它的基础上实现了Disable中断,Disable软中断等额外功能。

spin_lock(lock);        // 禁止中断,上锁
// 临界区代码  
spin_unlock(lock);      // 解锁,开中断

spin_lock_bh(lock);     // 禁止软中断,上锁 
// 临界区代码
spin_unlock_bh(lock);   // 解锁,开软中断

spin_lock_irq(lock);
// 临界区代码
spin_unlock_irq(lock);  // 释放锁,根据之前保存的状态决定是否开中断

if (spin_trylock(lock)) { // 尝试上锁,成功则进入临界区
    // 临界区代码
}  else {
    // 获取锁失败,做其他事情
}

这几个接口与自旋锁spinlock_t的初始化和销毁等一起,提供了Linux内核中的自旋锁机制。它们在许多需要互斥访问共享资源的场景中得到使用,是内核同步的基本方法之一。

重点

在中断服务程序(ISR)中选择合适的自旋锁接口,需要考虑单核与多核的差异:
在单核下:
- spin_lock()就足够,它会禁止中断,保证ISR的互斥访问。
- spin_lock_irq()和spin_lock_irqsave()则没必要,因为只有一个CPU,不会有竞争的ISR同时访问临界资源。
在多核下:
- spin_lock()不够,它仅禁止中断,无法在多个CPU之间提供互斥。
- 应选择spin_lock_irq()或spin_lock_irqsave()。
    - spin_lock_irq()会禁止本地CPU的中断,并通过自旋等待在其他CPU上执行的竞争ISR,进而实现互斥。
    - spin_lock_irqsave()同spin_lock_irq(),但可以提供更大的灵活性,因为中断状态被保存在外部变量中,可以在任意时刻进行恢复。
所以,总结如下:
在单核下,ISR中可以直接使用spin_lock()。
在多核下,ISR应使用spin_lock_irq()或spin_lock_irqsave()以实现多个CPU之间的互斥访问。
选择spin_lock_irq()还是spin_lock_irqsave()需要根据具体的使用场景而定。如果需要在锁定外实现更复杂的中断状态控制流程,应选择spin_lock_irqsave(),否则spin_lock_irq()就足够简单高效。

3.3.2.4 释放自旋锁(即V操作,置资源为可用)

原型

#include <linux/spinlock.h>


static inline void spin_unlock(spinlock_t *lock)
{
	raw_spin_unlock(&lock->rlock);
}

static inline void spin_unlock_bh(spinlock_t *lock)
{
	raw_spin_unlock_bh(&lock->rlock);
}

static inline void spin_unlock_irq(spinlock_t *lock)
{
	raw_spin_unlock_irq(&lock->rlock);
}
  • spin_unlock(): 释放自旋锁,开中断。
  • spin_unlock_bh(): 释放自旋锁,开软中断。

它们通过调用raw_spinlock部分的对应接口来实现实际的解锁操作。整个流程为:

spin_lock(lock);   // 上锁,禁止中断
// 临界区代码
spin_unlock(lock); // 解锁,开中断  

spin_lock_bh(lock);// 上锁,禁止软中断
// 临界区代码
spin_unlock_bh(lock); // 解锁,开软中断

spin_lock_irq(lock); 
// 临界区代码
spin_unlock_irq(lock);   // 释放锁,并根据spin_lock_irq()保存的状态 
             // 决定是否开中断

与获取自旋锁spin_lock()和spin_lock_bh()相对应,这两个接口实现了自旋锁的释放,并恢复中断和软中断。

3.3.3 实例

要求:用自旋锁实现上节中一个驱动同一时只能被打开一次。

/*************************************************************************
	> File Name: spinlock-only.c
    > 作用:以自旋锁做为并发控制的手段,使本驱动同时只能被一个应用层进程所open
 ************************************************************************/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/spinlock.h>

/*1、定义重要的变量及结构体*/
struct x_dev_t {
    struct cdev  my_dev;  //cdev设备描述结构体变量
    spinlock_t lock; //自旋锁
    int have_open;   //开启标志,0为已开启,1为未开启

};

struct x_dev_t *pcdev;

/*所有驱动函数声明*/
int open (struct inode *, struct file *);
int close (struct inode *, struct file *);

//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
    .open = open,
    .release = close,
};


static int __init my_init(void){
    int unsucc =0;
    dev_t devno;
    int major,minor;
    pcdev = kzalloc(sizeof(struct x_dev_t), GFP_KERNEL);
    /*2、创建 devno */
    unsucc = alloc_chrdev_region(&devno , 0 , 1 , "spinlock-char");
    if (unsucc){
        printk(" driver: creating devno  faild\n");
        return -1;
    }
    major = MAJOR(devno);
    minor = MINOR(devno);
    printk("driver : devno major = %d ; minor = %d;\n",major , minor);

    /*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
    /*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
    cdev_init(&pcdev->my_dev , &fops);
    pcdev->my_dev.owner = THIS_MODULE;
    /*4、注册cdev结构体到内核链表中*/
    unsucc = cdev_add(&pcdev->my_dev,devno,1);
    if (unsucc){
        printk("driver : cdev add faild \n");
        return 1;
    }
    //初始化自旋锁have_open 和 开启标志 
    spin_lock_init(&pcdev->lock);
    pcdev->have_open = 1;

    printk("driver : the driver spinlock-char initalization completed\n");


    return 0;
}


static void  __exit my_exit(void)
{
    cdev_del(&pcdev->my_dev);
    unregister_chrdev_region(pcdev->my_dev.dev , 1);
    printk("***************the driver spinlock-char  exit************\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = container_of(pnode->i_cdev,struct x_dev_t , my_dev);
    pf->private_data = (void *)p;
    //在open函数中对原子量have_open进行减1并检测。=0,允许打开文件,<0则不允许打开
    spin_lock(&p->lock);
    if (p->have_open == 1)
    {
        p->have_open = 0;
        printk("driver : spinlock-char  is opened\n");
        spin_unlock(&p->lock);
        return 0;
    }else{ //已被打开
        printk("driver : device spinlock-char Could not opend again\n");
        spin_unlock(&p->lock);
        return -1;
    }
}
/*file_operations结构全成员函数.release的具体实现*/
int close(struct inode *pnode , struct file *pf){
    printk("driver : spinlock-char is closed \n");
    return 0;
}
    


module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");

3.3.4 自旋锁的衍生与注意事项

【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】

【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】
【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】

【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】

3.4 、并发控制机制–信号量

信号量(Semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。

  • P(S): 将信号量S的值减1,即S=S-1; 如果S>=0,则该进程继续执行;否则该进程置为睡眠状态(阻塞),进入等待队列。
  • V(S):将信号量S的值加1,即S=S+1; 如果S>0,唤醒队列中阻塞而等待信号量的进程。

基于阻塞的并发控制机制,当要取资源时,如遇资源不足,则会使本任务进入阻塞状态。即P操作不成功时会进入阻塞睡眠状态。

适用场合:只能用于任务上下文之间且临界区执行时间较长时的互斥或同步问题

3.4.1 函数

#include <linux/semaphore.h>

3.4.1.1 定义信号量

原码:

#include <linux/semaphore.h>
struct semaphore {
	raw_spinlock_t		lock;
	unsigned int		count;
	struct list_head	wait_list;
};
  • lock:自旋锁,用于保护semaphore的访问。
  • count:资源的数量或可用数量。
  • wait_list:等待获取semaphore的进程链表。

主要接口:

  • void sema_init(struct semaphore *sem, int val); 初始化semaphore,count为val。
  • void down(struct semaphore *sem); 获取semaphore,如果sem不可用,进入睡眠。
  • int down_interruptible(struct semaphore *sem); 获取semaphore,如果 sem不可用,进入可中断睡眠。
  • int down_trylock(struct semaphore *sem); 尝试获取semaphore,如果获取不到立即返回。
  • void up(struct semaphore *sem); 释放semaphore,唤醒等待进程。
3.4.1.2 初始化信号量

原型:

#include <linux/semaphore.h>

static inline void sema_init(struct semaphore *sem, int val)
{
	static struct lock_class_key __key;
	*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
	lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

sema_init()函数用于初始化一个semaphore结构。

它主要做了两件事:

  1. 使用__SEMAPHORE_INITIALIZER宏初始化semaphore的各个成员。该宏的定义为:
#define __SEMAPHORE_INITIALIZER(name, n)                \
{                                    \
    .lock        = __RAW_SPIN_LOCK_UNLOCKED(name.lock),    \
    .count        = n,                        \
    .wait_list    = LIST_HEAD_INIT((name).wait_list)        \ 
}

它会初始化:

  • lock为解锁状态
  • count为传入的n值
  • wait_list为空
  1. 使用lockdep_init_map()为semaphore的lock成员指定依赖关系跟踪信息。这个主要用于lockdep(锁依赖检测)机制。

所以,总结来说,sema_init()的作用是对一个semaphore结构进行初始化,给其一个初始的count值,并为其lock成员lockdep设置需要的信息。这样,在后续的使用过程中,lockdep就可以根据lock的依赖关系跟踪semaphore的加锁顺序,检查是否存在潜在的死锁问题。
这个函数是使用semaphore结构的第一步,在创建semaphore实例后,必须首先调用sema_init()进行必要的初始化,否则semaphore处于未定义状态,使用结果未知。

用法

struct semaphore empty, full;
sema_init(&empty, 10);    // 初始化为空的缓冲区数为10 
sema_init(&full, 0);      // 初始化为满的缓冲区数为0
3.4.1.3 获得信号量P
#include <linux/semaphore.h>

void down(struct semaphore *sem);
int __must_check down_interruptible(struct semaphore *sem);
int __must_check down_killable(struct semaphore *sem);
int __must_check down_trylock(struct semaphore *sem);
int __must_check down_timeout(struct semaphore *sem, long jiffies);

参数:

  • sem:要获取的semaphore结构体指针。
  • jiffies:等待超时时间,以jiffies为单位。

返回值

  • 0:获取成功
    -EINTR:等待过程被致命信号中断(针对down()、down_interruptible()、down_killable())
    -EBUSY:semaphore不可用 (针对down_trylock())
    -ETIMEOUT:等待超时未能获取semaphore (针对down_timeout())

区别:

  • void down(struct semaphore *sem); 获得semaphore,如果不可用则睡眠,直到semaphore变为可用。这是最基本的down操作,会一直阻塞进程。
  • int down_interruptible(struct semaphore *sem); 获得semaphore,如果不可用则进入可中断睡眠。如果在睡眠中被信号中断,会返回-EINTR。这在一些需要对睡眠进行精细控制的场景下很有用。
  • int down_killable(struct semaphore *sem); 获得semaphore,如果不可用则进入可杀死的睡眠。如果在睡眠中被致命信号中断,会返回-EINTR。
  • int down_trylock(struct semaphore *sem); 尝试获得semaphore,如果获得成功则返回0,否则立即返回-EBUSY。这在需要尝试获取但不必等待的情况下使用。
  • int down_timeout(struct semaphore *sem, long jiffies); 尝试获得semaphore,如果在jiffies设定的超时时间内未获得,则返回-ETIMEOUT。如果在此超时时间内semaphore变为可用则返回0。这在需要等待一定时间后再使用其他手段的场景使用。
    以上接口为semaphore提供了丰富的down行为,使用它们可以实现对semaphore获取过程精细化的控制,满足各种同步场景的需要。理解这几个接口的区别有助于根据具体需求选择最适合的semaphore获取方式。

int down(struct semaphore *sem);//深度睡眠

int down_interruptible(struct semaphore *sem);//浅度睡眠

一旦信号量sem的值为0时,则该函数会进入睡眠状态。直到sem值大于0后。

3.4.1.4 释放信号量V

void up(struct semaphore *sem)函数用于释放一个semaphore信号量。它的作用是:

  • 如果有进程等待该semaphore,则唤醒最先等待的进程。
  • 如果没有等待进程,则semaphore的count值加1。

原型:

#include <linux/semaphoreh>


void up(struct semaphore *sem) 
{
    if (list_empty(&sem->wait_list))
        sem->count++;
    else
        __up(sem);
}

它首先检查sem的wait_list是否为空,如果是则直接增加sem的count值。否则就通过__up()唤醒wait_list中的最先进入睡眠的进程。

3.4.1.5 semaphore 使有和模式

1. 生产者-消费者问题:

struct semaphore empty, full;
sema_init(&empty, 10);    // 初始化为空的缓冲区数为10 
sema_init(&full, 0);      // 初始化为满的缓冲区数为0

void producer() {
    while (1) {
        // 生产一个项 
        down(&empty);     // 获取一个空缓冲区
        // 将项放入缓冲区
        up(&full);        // 发出一个满缓冲区信号
    }
}

void consumer() {
    while (1) {
        down(&full);      // 获取一个满缓冲区
        // 从缓冲区中取走一项
        up(&empty);       // 发出一个空缓冲区信号
    } 
}

empty和full两个semaphore控制了缓冲区的空满状态,实现了生产者和消费者之间的同步。

2. 读者-写者问题:

struct semaphore mutex, wr_sem;
sema_init(&mutex, 1);     // 互斥锁,初始化为1
sema_init(&wr_sem, 1);    // 写者信号量,初始化为1

void writer() {
    down(&wr_sem);        // 获取写者信号量
    down(&mutex);         // 获取互斥锁     
    // 写操作
    up(&mutex);           // 释放互斥锁
    up(&wr_sem);          // 释放写者信号量
}

void reader() {
    down(&mutex);         // 获取互斥锁
    // 读操作
    up(&mutex);           // 释放互斥锁
}

mutex实现互斥,保证同一时刻只有一个读者或写者。wr_sem实现写者优先,在进行写操作时阻塞其他读者或写者。

3.4.2 实例

要求:一个内存缓冲虚拟一个设备。该设备允许应用层多个进程同时对该设备进行读写操作。这样就需要驱动对并发场景进行控制。

/*************************************************************************
	> File Name: semaphore-memory.c
 ************************************************************************/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>


/*1、定义重要的变量及结构体*/

#define  MEM_SIZE 500

struct mem_dev_t{
    dev_t devno;
    struct cdev  my_dev;  //cdev设备描述结构体变量
    char  mem[MEM_SIZE]; //内存池,当成虚拟设备
    struct semaphore sem; //信号量
};

struct mem_dev_t *mem_dev;

/*驱动函数声明*/

ssize_t read (struct file *, char __user *, size_t, loff_t *);
ssize_t write (struct file *, const char __user *, size_t, loff_t *);
int open (struct inode *, struct file *);
int release (struct inode *, struct file *);
//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
    .open = open,
    .release = release,
    .read = read,
    .write = write,
};

/*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
/*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
static int cdev_setup(struct mem_dev_t *mem_dev  ){
    int unsucc =0;
    cdev_init(&mem_dev->my_dev , &fops);
    mem_dev->my_dev.owner = THIS_MODULE;
    /*4、注册cdev结构体到内核链表中*/
    unsucc = cdev_add(&mem_dev->my_dev,mem_dev->devno,1);
    if (unsucc){
        printk("cdev add faild \n");
        return -1;
    }
	/*********************************************************/
    sema_init( &mem_dev->sem,1); //初始化信号量,为1,意味着有资源mem
    /*********************************************************/
    return 0;

}

static int __init my_init(void){
    int major , minor;
    int unsucc =0;
    mem_dev = kzalloc(sizeof(struct mem_dev_t) , GFP_KERNEL);
    if (!mem_dev){
        printk(" allocating memory is  failed");
        return  -1;
    }

    /*2、创建 devno */
    unsucc = alloc_chrdev_region(&mem_dev->devno , 0 , 1 , "operate_memory");
    if (unsucc){
        printk(" creating devno  is failed\n");
        return -1;
    }else{

        major = MAJOR(mem_dev->devno);
        minor = MINOR(mem_dev->devno);
        printk("major = %d  ; minor = %d\n",major,minor);
    }
    /*3、 初始化cdev结构体,并联cdev结构体与file_operations.*/
    /*4、注册cdev结构体到内核链表中*/
    if (cdev_setup(mem_dev) == 0){
        printk("the driver operate_memory  initalization completed\n");
        return 0;   
    } else
        return -1;
}


static void  __exit my_exit(void)
{
    cdev_del(&mem_dev->my_dev);
    unregister_chrdev_region(mem_dev->devno , 1);
    printk("***************the driver operate_memory exit************\n");
}


/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/

int open(struct inode *pnode , struct file *pf){
    pf->private_data = (void*)mem_dev;  //把全局变量指针放入到struct file结构体里
    printk("operate_memory is opened\n");
    return 0;


}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){
    printk("operate_memory is closed \n");
    return 0;
}
    

/*file_operations结构全成员函数.read的具体实现*/
ssize_t read (struct file * pf, char __user * buf, size_t size , loff_t * ppos){
    struct mem_dev_t *pdev = pf->private_data;
    int count = 0;
    //判断偏移量的有效性
    if (*ppos >= MEM_SIZE){
        return 0;
    }
    //判断能够读到的字节数量
    if  (size > MEM_SIZE - *ppos){
        count = MEM_SIZE - *ppos;
    }else{
        count = size;
    }

    //copy_from_user返回值大于0失败
    /*********************************************************/
    down(&pdev->sem);   //信号量P操作,表示占用资源
    if ( copy_to_user(buf , &pdev->mem[*ppos] , count )){
        up(&pdev->sem);   //退出前释放信号量,V操作
        return 0;
    }else{
        *ppos += count;
        up(&pdev->sem); //退出前释放信号量,V操作
        return count;
    }
    /*********************************************************/    
}

/*file_operations结构全成员函数.write的具体实现*/
ssize_t write (struct file * pf, const char __user *buf, size_t size , loff_t *ppos){
    struct mem_dev_t *pdev = pf->private_data;
    int count = 0;
    //判断偏移量的有效性
    if (*ppos >=MEM_SIZE ){
        return 0;
    }
    //判断能够写入的字节数量
    if (size > MEM_SIZE-*ppos){
        count = MEM_SIZE-*ppos;
    }else{
        count = size;
    }
    //copy_from_user返回值大于0失败
    /*********************************************************/
    down(&pdev->sem);   //信号量P操作
    if ( copy_from_user(&pdev->mem[*ppos] , buf , count)){
        up(&pdev->sem); //退出前释放信号量,V操作
        return 0;
    }else{
        *ppos +=count;
        up(&pdev->sem); //退出前释放信号量,V操作
        return count;
    }
	/*********************************************************/
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");

3.5、互斥锁

基于阻塞的互斥机制。

=适用场合:任务上下文之间且临界区执行时间较长时的互斥问题

3.5.1 函数

#include <linux/mutex.h>

struct mutex

原型:

struct mutex {
	/* 1: unlocked, 0: locked, negative: locked, possible waiters */
	atomic_t		count;
	spinlock_t		wait_lock;
	struct list_head	wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
	struct task_struct	*owner;
#endif
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
	void			*spin_mlock;	/* Spinner MCS lock */
#endif
#ifdef CONFIG_DEBUG_MUTEXES
	const char 		*name;
	void			*magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map	dep_map;
#endif
};

参数:

  • count 来表示锁定状态,
  • wait_list保存等待的进程,
  • ,owner记录mutex的持有者。

主要接口:

  • void mutex_init(struct mutex *lock); 初始化mutex。
  • void mutex_lock(struct mutex *lock); 获得mutex,如果已加锁则睡眠。
  • int mutex_lock_interruptible(struct mutex *lock); 可中断版本的mutex_lock。
  • int mutex_trylock(struct mutex *lock); 尝试获得mutex,如果未获得立即返回。
  • void mutex_unlock(struct mutex *lock); 释放mutex,唤醒等待进程。
3.5.1.1 初始化

void mutex_init(struct mutex *lock)函数用于初始化一个mutex锁。

#include <linux/mutex.h>
# define mutex_init(mutex) \
do {							\
	static struct lock_class_key __key;		\
							\
	__mutex_init((mutex), #mutex, &__key);		\
} while (0)

使用:

struct mutex  my_mutex;
mutex_init(&my_mutex);
3.5.1.2 获取互斥体

原型:

#include <linux/mutex.h>

void mutex_lock(struct mutex *lock);

void mutex_lock(struct mutex *lock)函数用于锁住一个mutex锁。它的主要功能是:

  1. 通过atomic_dec_if_positive()尝试将mutex的count值减1,如果减1成功,则获得锁,函数返回。
  2. 如果count值减1失败,则表示锁已被其他进程持有。此时,调用会被加入到wait_list等待链表,并睡眠。
  3. 一旦锁被释放,睡眠的进程会被唤醒,再次尝试获取锁。重复该流程,直到获得锁为止。
  4. 获取锁后,lock->owner会设置为当前进程,用于调试目的。
    mutex_lock()是获取mutex锁的最基本接口,它实现了等待已持有锁的进程,唤醒并重新调度等机制。理解这个接口有助于正确使用mutex实现同步互斥。
    使用示例:
void func() 
{
    mutex_lock(&my_mutex);   // 获取锁
    // 此区域互斥
    mutex_unlock(&my_mutex); // 释放锁
}
3.5.1.3 释放互斥体

原型:

#include <linux/mutex.h>
void mutex_unlock(struct mutex *lock);

void mutex_unlock(struct mutex *lock)函数用于释放一个mutex锁。它的主要功能是:

  1. 通过atomic_inc()将mutex的count值增加1,表示释放锁。
  2. 如果wait_list不为空,则唤醒其中的第一个进程。被唤醒的进程将重新尝试获取锁。
  3. mutex的owner成员被设置为NULL。这个成员仅在开启相关配置时使用,用于调试目的。
  4. 如果mutex使用递归锁定,则调用mutex_release()释放锁。这个函数将递归锁定的count减1,如果减到0则真正释放锁。

使用步骤:

  1. 定义对应类型的变量
  2. 初始化对应变量

P/加锁
临界区
V/解锁

3.5.2 范例

这里举两个使用mutex的例子:

  1. 保护共享资源
struct mutex my_mutex;

void shared_resource_access(void) 
{
    mutex_lock(&my_mutex);
    // 操作共享资源
    mutex_unlock(&my_mutex);
}

多个进程通过mutex保护对共享资源的访问,实现同步互斥。
2. 读-写锁

struct mutex rw_mutex;

void reader(void) 
{
    mutex_lock(&rw_mutex);     // 获取读锁
    // 读操作
    mutex_unlock(&rw_mutex);   // 释放读锁
}

void writer(void) 
{
    mutex_lock(&rw_mutex);     // 获取写锁,阻塞其他读者和写者
    // 写操作
    mutex_unlock(&rw_mutex);    // 释放写锁,唤醒等待进程 
}

读锁和写锁通过同一个mutex实现,但优先级不同。写锁的请求会导致其他读锁和写锁等待,实现写优先。
3. 递归mutex

struct mutex recursive_mutex;

void recursive_func(void)
{
    mutex_lock(&recursive_mutex);
    // 递归调用自身
    recursive_func(); 
    mutex_unlock(&recursive_mutex); 
}

递归mutex允许同一个进程对其进行多次加锁。锁需与解锁次数匹配,否则会发生死锁。

4、选择并发控制机制的原则

  1. 不允许睡眠的上下文(异常上下文)需要采用忙等待类(自旋锁,原子变量),可以睡眠的上下文可以采用阻塞类(信号量,互斥锁)。在异常上下文中访问的竞争资源一定采用忙等待类。

  2. 临界区操作较长的应用建议采用阻塞类,临界区很短的操作建议采用忙等待类。

  3. 中断屏蔽仅在有与中断上下文共享资源时使用。

  4. 共享资源仅是一个简单整型量时用原子变量文章来源地址https://www.toymoban.com/news/detail-419840.html

到了这里,关于【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 嵌入式Linux驱动开发 02:将驱动程序添加到内核中

    在上一篇文章 《嵌入式Linux驱动开发 01:基础开发与使用》 中我们已经实现了最基础的驱动功能。在那篇文章中我们的驱动代码是独立于内核代码存放的,并且我们的驱动编译后也是一个独立的模块。在实际使用中将驱动代码放在内核代码中,并将驱动编译到内核中也是比较

    2023年04月09日
    浏览(61)
  • 【嵌入式Linux学习笔记】platform设备驱动和input子系统

    对于Linux这种庞大的操作系统,代码重用性非常重要,所以需要有相关的机制来提升效率,去除重复无意义的代码,尤其是对于驱动程序,所以就有了platform和INPUT子系统这两种工作机制。 学习视频地址:【正点原子】STM32MP157开发板 platform 驱动框架分为总线、设备和驱动。总

    2024年02月07日
    浏览(48)
  • 【嵌入式Linux内核驱动】05_IIC子系统 | 硬件原理与常见面试问题 | 应用编程 | 内核驱动 | 总体框架

    1.1 IIC 基础 IIC协议简介—学习笔记_iic标准协议_越吃越胖的黄的博客-CSDN博客 I2C(Inter-Integrated Circuit)是一种串行通信协议,用于连接微控制器、传感器、存储器和其他外设。 I2C使用两条线(SDA和SCL)进行通信,可以连接多个设备,每个设备都有一个唯一的地址。I2C总线上的

    2024年02月09日
    浏览(42)
  • 【嵌入式Linux内核驱动】04_Jetson nano GPIO应用 | 驱动开发 | 官方gpiolib、设备树与chip_driver

    0.暴露给应用层 应用 解决调试目录为空的问题 调试信息 1.最简读写文件(在/SYS下) 设备树 验证测试 编译文件 驱动 of_get_named_gpio_flags //获取设备树节点的属性 gpio_is_valid //判断是否合法 devm_gpio_request //申请使用gpio,并调用设置pinctrl device_create_file //根据设备树节点属性,创建

    2024年02月07日
    浏览(50)
  • 嵌入式内核及驱动开发高级

    仅devfs,导致开发不方便以及一些功能难以支持: 热插拔 不支持一些针对所有设备的统一操作(如电源管理) 不能自动mknod 用户查看不了设备信息 设备信息硬编码,导致驱动代码通用性差,即没有分离设备和驱动 uevent机制:sysfs + uevent + udevd(上层app) sysfs用途:(类似于

    2024年02月16日
    浏览(56)
  • 嵌入式开发之linux内核移植

    目录  前言 一、下载内核源码 1.1 下载linux-3.0.1 1.2 解压源码文件 二、 内核添加yaffs2文件系统支持 2.1 下载yaffs2 2.2 内核添加yaffs2文件补丁 三、配置开发板 3.1 修改机器ID 3.2 添加开发板初始化文件 3.3 配置NandFalsh 3.3.1 添加NandFlash设备 3.3.2 添加NandFlash驱动 3.3 修改Kconfig(支持

    2024年02月07日
    浏览(88)
  • 嵌入式培训机构四个月实训课程笔记(完整版)-Linux ARM驱动编程第五天-ARM Linux编程之字符设备驱动(物联技术666)

    链接:https://pan.baidu.com/s/1V0E9IHSoLbpiWJsncmFgdA?pwd=1688 提取码:1688 教学内容: 1 、内核模块的简单框架: __init __exit 执行完后就释放空间 简单框架:包含三个部分 1)模块初始化和模块退出函数 2)注册模块函数 3)模块许可 //*************************************************** #include linux

    2024年02月21日
    浏览(38)
  • 嵌入式Linux底层系统开发 +系统移植+内核文件系统(基础)

    搭建交叉编译开发环境 bootloader的选择和移植 kernel的配置、编译、移植和调试 根文件系统的制作 前两个要点通常芯片厂家提供。后边两个要点是公司的工作重点。 学习方法:先整体后局部,层层推进 如何编译—如何添加命令和功能—如何定义自己的开发板。 移植的基本步

    2024年02月03日
    浏览(56)
  • 修改嵌入式 ARM Linux 内核映像中的文件系统

    zImage 是编译内核后在 arch/arm/boot 目录下生成的一个已经压缩过的内核映像。通常我们不会使用编译生成的原始内核映像 vmlinux ,因其体积很大。因此, zImage 是我们最常见的内核二进制,可以直接嵌入到固件,也可以直接使用 qemu 进行调试。当然,在 32 位嵌入式领域还能见到

    2024年02月10日
    浏览(70)
  • 【嵌入式Linux】编译应用和ko内核模块Makefile使用记录

    在Makefile中,变量的赋值可以使用以下几种方式: = :最基本的赋值符号,表示简单的延迟展开(lazy expansion)方式。变量的值将会在使用变量的时候进行展开。 := :立即展开(immediate expansion)的赋值方式。变量的值在赋值的时候立即展开,并且在后续的使用中不再改变。

    2024年02月08日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包