linux中并发无处不在,底层驱动需要考虑。

7.1 并发与竞争

7.1.1 概念

  • 并发:Concurrency,多个执行单元同时、并行执行
  • 竞争:Race Condistions,并发的执行单元对共享资源(硬件、软件全局变量和静态变量等)的访问很容易导致竞争

7.1.2 产生并发的情况

  • SMP下多个CPU
  • 单CPU内进程抢占
  • 中断  

  SMP是真正的并行,其他情况是“宏观并行、微观串行”引起的并发问题。解决竞争问题的办法是保证对共享资源的互斥访问,访问共享资源的代码区称为临界区,需要对临界区进行互斥保护。

7.2 编译乱序和执行乱序

7.2.1 编译乱序

  • 编译器会自动对程序进行编译优化,例如-O2,优化后的汇编可能与C语言的顺序不一样;
  • barrier()函数能禁止编译器乱序优化,能保证barrier前后不乱序。

7.2.2 执行乱序

  内核在执行代码时,也可能进行乱序执行(out-of-order execution)。例如ARM cortex-A8不支持乱序,但是A9和A15支持乱序;A53不支持乱序,A57支持。乱序执行能提高CPU的利用率,提高效率。

ldr r0, [r1]
str r2, [r3] 例如上述程序,如果ldr执行时间较长(例如cache未命中),则CPU可以不等待ldr完成,继续执行后面的str指令,也许str指令都执行完了,但是ldr还没执行完。这就导致了乱序执行。

  为什么我们感受不到乱序执行? 单核的CPU会进行“依赖点等待”,如果后面的指令依赖前面的结果,则后面指令会等待前面指令结束以后再执行,依赖点等待是硬件自动完成的,不用软件参与。

  看似完美,but问题来了,双核怎么办? “依赖点等待”的作用范围是单核内部,另一各核不知道,所以多核的竞争要格外注意乱序执行。

ARM为解决乱序执行问题,支持如下操作:
dmb,DMB前面的“显示内存访问”完成后,才能进行后面的“显示内存访问”,但不影响DMB后面的其他指令执行
dsb,DSB前面“显示内存访问”完成后才执行后面的指令
isb,flush流水线,isb后面的指令都是从缓存或内存中获取的。

7.3 中断屏蔽

  可避免中断和其他进程抢占导致的并发,对单核有效。 不太推荐单独使用。

  • 中断屏蔽,就是关闭当前core的中断,实际是屏蔽CPSR的I位。也就保证了临界区不会受中断的影响。
  • 只能停止当前core,对其他core无效
  • 调度是依赖中断的,中断屏蔽以后,整个core的任务调度都不进行了,自然就不会有进程抢占了。 所以,被包含的临界区应该尽量短。
local_irq_enable()       // 使能local_irq_disable()      // 关闭
local_irq_save(flags)   // 关闭中断,保存CPSRlocal_irq_restore(flags) // 使能中断,恢复CPSR

7.4 原子操作(对多核有作用)

  保证对一个整型数据的修改是排他的。 ARM底层有指令保证,LDREX和STREX,EX是exclusive的缩写。

/*
* ARMv6 UP and SMP safe atomic ops. We use load exclusive and
* store exclusive to ensure that these are atomic. We may loop
* to ensure that the update happens.
*/
static inline void atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result; prefetchw(&v->counter);
__asm__ __volatile__("@ atomic_add\n"
"1: ldrex %0, [%3]\n"
" add %0, %0, %4\n"
" strex %1, %0, [%3]\n"
" teq %1, #0\n"
" bne 1b"
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
: "r" (&v->counter), "Ir" (i)
: "cc");
}
   以atomic_add()为例说明:
  ldrex和strex配对使用,可以让总线监控ldrex和strex之间有无其他的实体存取该地址;多核或者同一个core都可以监测。
  如果有并发的访问,执行strex指令时,第一个寄存器的值被设置为1(Non exclusive access),并且存储的行为也不成功;
  
  下图中T3时刻,CPU0的strex不成功,会继续循环;
特别注意,T3时刻CPU0 strex不成功,没有实际访问外部地址,所以T4时刻CPU1的strex是成功的。
  如果不成功,可能循环。

  

#include <linux/atomic.h>

/*
* On ARM, ordinary assignment (str instruction) doesn't clear the local
* strex/ldrex monitor on some implementations. The reason we can use it for
* atomic_set() is the clrex or dummy strex done on every exception return.
*/
#define atomic_read(v) (*(volatile int *)&(v)->counter)
#define atomic_set(v,i) (((v)->counter) = (i)) /*
* ARMv6 UP and SMP safe atomic ops. We use load exclusive and
* store exclusive to ensure that these are atomic. We may loop
* to ensure that the update happens.
*/
static inline void atomic_add(int i, atomic_t *v)      // *v+i
static inline int atomic_add_return(int i, atomic_t *v)   // *v+i,并返回
static inline void atomic_sub(int i, atomic_t *v)      // *v-i
static inline int atomic_sub_return(int i, atomic_t *v)   // *v-i,并返回

#define atomic_inc(v) atomic_add(1, v)
#define atomic_dec(v) atomic_sub(1, v)

#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_inc_return(v) (atomic_add_return(1, v))
#define atomic_dec_return(v) (atomic_sub_return(1, v))
#define atomic_sub_and_test(i, v) (atomic_sub_return(i, v) == 0)

#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)
注:nr是第几位,p是指针

  若为单核,即使中断中不调用spin_lock也没关系,因为进程里spin_lock_irqsave已经把中断关闭了。但是考虑到双核,如果CPU0的中断不用spin_lock,中断中访问了临界区,此时CPU1也能获取自旋锁,继而访问临界区,导致临界区保护失败。

7.5 自旋锁

7.5.1 本质

  执行一个原子操作test_and_set,如果不成功,则一直循环,即“自旋”,在原地打转。

  【注意】

  1.原地打转跟阻塞不一样。原地打转一直在转,一直占用CPU;而阻塞是释放CPU的使用权,往往意味着睡眠。

  2. 持有自旋锁期间,内核抢占机制将被禁止。另一个core的抢占不受影响

7.5.2 函数

#include <linux/spinlock.h>

spinlock_t lock;       // 定义自旋锁

spin_lock_init(lock);    // 初始化自旋锁
spin_lock(lock);       // 上锁,不成功则原地打转
spin_trylock(lock);     // 上锁,不成功立即返回
spin_unlock(lock);     // 释放自旋锁

自旋锁针对SMP下的多核以及单核的进程抢占,对这两种情况是有效的。

7.5.3 进程和中断访问同一临界资源

上锁以后,能保证另一个core以及本core的其他进程不打扰临界区,但是本core中断还可能影响临界区(打断),如果想杜绝这种影响,需要使用“lock+关中断”的方式。

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

7.5.3 使用原则

  •  临界区尽量短,因为自旋锁是忙等待,临界区太长会降低系统性能
  •  防止死锁,例如一个已经拥有自旋锁的CPU想第二次获得这个自旋锁
  •  拥有自旋锁期间,不能调用引起调度的函数,例如copy_from_user(),copy_to_user(),kmalloc(),msleep等函数,可能导致内核的崩溃
  •  考虑可移植性,单核编程也要按多核考虑。例如中断中也要用spin_lock

7.6 信号量

  • 信号量是同步和互斥的手段;
  • 取值0/1/n,一般0/1二值信号量居多
  • sem>=0,进程可以继续执行
  • sem<0,则进程进入等待队列,被设置成等待状态
  • P(S),P操作,S-=1,可能导致进入等待队列
  • V(S),V操作,S+=1,唤醒等待队列中等待该信号量的进程
#include <linux/semaphore.h>

/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
}; #define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
} #define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, ) 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, );
} extern void down(struct semaphore *sem);                      // P操作,休眠的进程不能被信号唤醒
extern int __must_check down_interruptible(struct semaphore *sem);       // P操作,休眠的进程可以被信号唤醒,睡眠的进程被信号打断时,down_interruptibal()函数会返回非0值
extern int __must_check down_killable(struct semaphore *sem);
extern int __must_check down_trylock(struct semaphore *sem);          // 不成功也不进入等待队列,而是立即返回
extern int __must_check down_timeout(struct semaphore *sem, long jiffies);
extern void up(struct semaphore *sem);                       // V操作

7.6.1 使用信号量完成互斥

  一般不用了,改用linux新的互斥机制mutex。

7.6.2 使用信号量完成同步

  同步等待。

7.7 互斥体

  

#include <linux/mutex.h>

struct mutex my_mutex;

mutex_init(&my_mutex);

void mutex_lock( struct mutex * lock );
int mutex_lock_interruptibal( struct mutex * lock ); // 睡眠后可被信号打断
int mutex_trylock( struct mutex * lock );          // 不睡眠 void mutex_unlock( struct mutex * lock); 使用方法与信号量互斥的情况相同
mutex_lock( &my_mutex);
......  // 临界资源
mutex_unlock(&my_mutex);

【自旋锁与互斥体区别】

  • 级别不同,mutex是进程级,实现上依赖spin_lock;spin_lock属于更底层的手段;
  • mutex是进程级,代表进程争夺资源。如果失败,则当前进程进入睡眠,发生进程上下文切换,CPU会运行其他进程。进程切换的开销比较大,so,只有进程占用资源时间较长时,用互斥体是较好的选择;
  • spin_lock得不到时会原地打转,不会进行进程切换,so,临界区较短时,用起来比较划算;

【自旋锁与互斥体的选择】

  • 临界区大小:大选mutex,小选spin_lock
  • 临界区是否可以阻塞:mutex的临界区可以包含引起阻塞的代码;spin_lock不可以,若包含阻塞代码,则会发生进程切换,在另一个进程里如果获取本spin_lock,就会发生死锁
  • 中断中的互斥选择:mutex会引起进程切换,所以中断中不能使用,只能用spin_lock

7.8 完成量

  用于一个执行单元等待另一个执行单元执行完某事。实质就是1个执行单元主动进入等待队列,另一个执行单元从等待队列中唤醒别的执行单元。

 

#include <linux/completion.h>

struct completion my_completion;

init_completion( &my_completion );
reinit_completion( &my_completion ); void wait_for_completion( struct completion *c );  //等待一个完成量被唤醒 void completion( struct completion *c );       // 唤醒一个等待该完成量的执行单元
void completion_all( struct completion *c )      // 唤醒所有等待该完成量的执行单元

7.9 增加互斥控制后的globalmem驱动

7.10 总结

  自旋锁和互斥体相对更常用一些,注意两者的选用原则!

《linux设备驱动开发详解》笔记——7并发控制的更多相关文章

  1. linux设备驱动开发详解 笔记

      在目录的 Makefile 中关于 RTC_DRV_S3C 的编译脚本为: obj -$(CONFIG_RTC_DRV_S3C) += rtc-s3c.o 上述脚本意味着如果 RTC_DRV_S3 ...

  2. Linux设备驱动开发详解

    Linux设备驱动开发详解 http://download.csdn.net/detail/wuyouzi067/9581380

  3. 《Linux设备驱动开发详解(第2版)》配套视频登录51cto教育频道

    http://edu.51cto.com/course/course_id-379-page-1.html http://edu.51cto.com/course/course_id-379-page ...

  4. 《linux设备驱动开发详解》笔记——15 linux i2c驱动

    结合实际代码和书中描述,可能跟书上有一定出入.本文后续芯片相关代码参考ZYNQ. 15.1 总体结构 如下图,i2c驱动分为如下几个重要模块 核心层core,完成i2c总线.设备.驱动模型,对用户提供 ...

  5. 《linux设备驱动开发详解》笔记——14 linux网络设备驱动

    14.1 网络设备驱动结构 网络协议接口层:硬件无关,标准收发函数dev_queue_xmit()和netif_rx();  注意,netif_rx是将接收到的数据给上层,有时也在驱动收到数据以后调用 ...

  6. 《linux设备驱动开发详解》笔记——12linux设备驱动的软件架构思想

    本章重点讲解思想.思想.思想. 12.1 linux驱动的软件架构 下述三种思想,在linux的spi.iic.usb等复杂驱动里广泛使用.后面几节分别对这些思想进行详细说明. 思想1:驱动与设备分离 ...

  7. 《linux设备驱动开发详解》笔记——6字符设备驱动

    6.1 字符设备驱动结构 先看看字符设备驱动的架构: 6.1.1 cdev cdev结构体是字符设备的核心数据结构,用于描述一个字符设备,cdev定义如下: #include <linux/cd ...

  8. Linux设备驱动开发详解-Note(11)--- Linux 文件系统与设备文件系统(3)

    Linux 文件系统与设备文件系统(3) 成于坚持,败于止步 sysfs 文件系统与 Linux 设备模型 1.sysfs 文件系统 Linux 2.6 内核引入了 sysfs 文件系统,sysfs ...

  9. Linux设备驱动开发详解-Note(5)---Linux 内核及内核编程(1)

    Linux 内核及内核编程(1) 成于坚持,败于止步 Linux 2.6 内核的特点 Linux 2.6 相对于 Linux 2.4 有相当大的改进,主要体现在如下几个方面. 1.新的调度器 2.6 ...

  10. 《linux设备驱动开发详解》笔记——18 ARM linux设备树

    18.1 设备树的起源 linux 2.6及之前,大量板级信息被硬编码到内核里,十分庞大,大量冗余代码: linux 2.6之前,引入了设备树: 设备树源于OpenFirmware,描述硬件的数据结构 ...

随机推荐

  1. Codeforces Round #431 (Div. 2) C

    From beginning till end, this message has been waiting to be conveyed. For a given unordered multise ...

  2. 2017浙江工业大学-校赛决赛 XiaoWei的战斗力

    Description XiaoWei沉迷RPG无法自拔,但是他的战斗力只有5,所以他决定氪金提升战斗力.XiaoWei购买了n个福袋.打开1个福袋后,有以下三种情况出现:1.获得屠龙宝刀,概率为p1 ...

  3. 去掉word文档两边的空白

    1.设置-页面布局-页边距,把左边距和右边距的数据设置到最小就好,一般为0.43CM 2.把WORD页面顶部标尺,左右拉到最底,如图: 3.在打印预览里,设置页边距,操作方法同 上述 1,如图:

  4. 《java学习二》jvm性能优化-----认识jvm

    Java内存结构 Java堆(Java Heap) java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域. 在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例,这一 ...

  5. SQL Server事务的四种隔离级别

    在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些是在事务内和事务间可见的,哪些是不可见的.较低级别的隔离通常可以执行更高的并发,系统的开销也更低. 1.未提交读(Read ...

  6. c# 串口操作

    public class CommPort : IDisposable { public string Port = ""; ///<summary> ///波特率96 ...

  7. HTML5标签选择指引

  8. God made relatives.Thank God we can choose our friends.

    God made relatives.Thank God we can choose our friends. 神决定了谁是你的亲戚, 幸运的是在选择朋友方面他给了你留了余地

  9. Android 使用RecyclerView实现多行水平分页的GridView效果和ViewPager效果

    前些天看到有人在论坛上问这种效果怎么实现,没写过也没用过这个功能,网上查了一下,大多是使用ViewPager+GridView或者HorizontalScrollView+GridView实现,不过貌 ...

  10. SQL Server一个特殊的阻塞案例分析2

    最近发现一个非常奇怪的阻塞问题,如下截图所示(来自监控工具DPA),会话583被会话1036阻塞,而且阻塞发生在tempdb,被阻塞的SQL如下截图所示,会话等待类型为LCK_M_S 因为DPA工具不 ...