linux并发与竞争

并发与竞争简介

多任务同时访问同一片内存区域,这些任务可能会相会覆盖内存中的数据,造成内存读写混乱
linux系统产生并发的原因:

  1. 多线程并发访问
  2. 抢占式并发访问
  3. 中断程序并发访问
  4. SMP(多核)核间并发访问

临界区 :共享数据段
保证同一时刻只能有一个任务访问临界区资源

原子操作(一般用于整型变量或者位操作)

  1. 原子变量定义,在使用原子操作时,用原子变量代替整型变量

    1
    2
    3
    4
    typedef struct
    {
    int counter;
    }atomic_t;

    atomic_t a; //定义原子变量
    atomic_t b = ATOMIC_INIT(0) //定义原子变量b的同时将其初始化为0

  2. 整型原子操作API函数

    函数描述 函数
    定义时赋初值i atomic_t a = ATOMIC_INIT(i)
    读取原子变量v的值 int atomic_read(atomic_t *v)
    向原子变量v写入数据i void atomic_set(atomic_t *v, int i)
    原子变量v加上i void atomic_add(int i, atomic_t *v)
    原子变量v减去i void atomic_sub(int i, atomic_t *v)
    原子变量v自增 void atomic_inc(atomic_t *v)
    原子变量v自减 void atomic_dec(atomic_t *v)
    从v减1并返回v的值 int atomic_dec_return(atomic_t *v)
    从v加1并返回v的值 int atomic_inc_return(atomic_t *v)
    从v减i,结果为0返回真,否则返回假 int atomic_sub_and_test(int i, atomic_t *v)
    从v加1,结果为0返回真,否则返回假 int atomic_inc_and_test(int i, atomic_t *v)
    从v减1,结果为0返回真,否则返回假 int atomic_dec_and_test(int i, atomic_t *v)

    上述API函数针对32位系统,若在64位系统中使用原子变量,上述函数前缀由atomic_改为atomic64_,返回值由int改为long long

  3. 原子位操作API函数(直接对内存操作)

    函数描述 函数
    将p地址的第nr位置1 void set_bit(int nr, void *p)
    将p地址的第nr位置0 void clear_bit(int nr, void *p)
    将p地址的第nr位翻转 void chang_bit(int nr, void *p)
    获取p地址第nr位的值 int test_bit(int nr, void *p)

自旋锁

保证同一资源同时只能被一个任务访问,若其他线程获取锁失败,原地自旋(不断查询锁状态),等待锁可用。其缺点在于自旋状态会浪费处理器资源,降低系统性能,因此自旋锁持有时间不能太长。
linux内核使用spinlock_t表示自旋锁类型 spinlock_t lock;

  • 多线程中自旋锁API函数
    函数描述 函数
    定义并初始化自旋锁变量(宏) DEFINE_SPINLOCK(spinlock_t lock)
    初始化自旋锁 int spin_lock_init(spinlock_t *lock)
    获取指定的自旋锁(上锁,不推荐使用) void spin_lock(spinlock_t *lock)
    释放指定的自旋锁(开锁,不推荐使用) void spin_unlock(spinlock_t *lock)
    尝试获取指定自旋锁,获取失败返回0 int spin_trylock(spinlock_t *lock)
    检查指定自旋锁是否被获取,已获取返回0 int spin_is_locked(spinlock_t *lock)

    以上API函数用于线程与线程之间的并发访问,由自旋锁保护的临界区之中一定调用任何能够引起系统阻塞或者睡眠的函数,否则可能发生死锁现象。

  • 涉及到中断的自旋锁API函数

    在中断中可以使用自旋锁,但是在获取锁之前必须关闭本地中断,否则可能出现死锁现象。

    函数描述 函数
    禁止本地中断并获取自旋锁 void spin_lock_irq(spinlock_t *lock)
    激活本地中断并释放自旋锁 void spin_unlock_irq(spinlock_t *lock)
    保存中断状态,禁止本地中断,获取自旋锁 void spin_lock_irqsave(spinlock_t *lock, unsigned long flas)
    恢复中断状态,激活本地中断,释放自旋锁 void spin_unlock_irqstore(spinlock_t *lock, unsigned long flags)

    在涉及到中断的程序中,使用自旋锁时,建议在线程中使用spin_lock_irqsave和spin_unlock_irqstore函数,在中断服务函数中使用spin_lock_irq和spin_unlock_irq函数。

  • 使用自旋锁的注意事项
    • 自旋锁自旋时间要短,否则会降低系统性能。如果临界区资源较大,运行时间较长,要考虑其他的并发处理方式。
    • 自旋锁保护的临界区不能使用任何可能导致系统休眠的函数,负责可能产生死锁现象
    • 不能递归申请自旋
    • 考虑驱动程序的可移植性,无论多核还是单核,都将其视作多核CPU编写驱动代码

其他内核中常用的锁

  1. 读写自旋锁

    读写自旋锁为读操作和写操作提供不同的锁。一次只允许一个写操作,也就是同一时间只允许一个线程持有写锁,且不能进行读操作;但是当没有写操作时,允许一个或多个线程持有读锁,允许并发执行读操作。

  • linux内核使用rwlock_t表示读写锁类型

    函数描述 函数
    定义并初始化读写锁 DEFINE_RWLOCK(rwlock_t lock)
    初始化读写锁 void rwlock_init(rwlock_t *lock)
    获取读锁 void read_lock(rwlock_t *lock)
    释放读锁 void read_unlock(rwlock_t *lock)
    禁止本地中断并获取读锁 void read_lock_irq(rwlock_t *lock)
    打开本地中断并释放读锁 void read_unlock_irq(rwlock_t *lock)
    获取写锁 void write_lock(rwlock_t *lock)
    释放写锁 void write_unlock(rwlock_t *lock)
    禁止本地中断获取写锁 void write_lock_irq(rwlock_t *lock)
    打开本地中断释放写锁 void write_unlock_irq(rwlock_t *lock)

信号量

获取资源信号量加一 释放资源信号量减一
与自旋锁不同 信号量使线程休眠 且进入休眠状态后会切换线程

  • 使用信号量的注意事项
    • 信号量使线程进入休眠状态,因此适合占用临界区资源较久的情景
    • 信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠
    • 临界区资源持有时间较短不适合使用信号量,因为频繁进入休眠切换线程造成的资源开销远大于休眠节省的资源
  • 信号量原型
    • Linux中使用semaphore结构体表示信号量
      1
      2
      3
      4
      5
      struct semaphore{
      raw_spinlock_t lock;
      unsigned int count;
      struct list_head wait_list;
      };
  • 信号量API函数
函数描述 函数
定义一个信号量并设置其值为1 DEFINE_SEMAPHORE(name)
初始化信号量并设置信号量的值 void sema_init(struct semaphore *sem, int val)
获取信号量,获取失败进入休眠,不能在中断中使用,且使用此函数进入休眠后不能被信号打断 void down(struct semaphore *sem)
尝试获取信号量,获取成功返回0;获取失败不会进入休眠 int down_trylock(struct semaphore *sem)
获取信号量,与down类似,此函数进入休眠后可以被信号打断 void down_interruptible(struct semaphore *sem)
释放信号量 值加一 void up(struct semaphore *sem)
  • 示例代码:
    1
    2
    3
    4
    5
    struct semaphore sem;   //定义信号量
    sem_init(&sem, 1); //初始化信号量
    down(&sem); //获取信号量
    /* 临界区 */
    up(&sem); //释放信号量

互斥体

驱动中使用互斥的地方建议使用互斥体而非二值信号量

  • 互斥体原型
    struct mutex{
    atomic_t count;
    spinlock_t wait_lock;
    };

  • 互斥体注意事项

    • mutex同样导致系统休眠,不能在中断中使用
    • 临界区可以调用引起阻塞的API函数
    • 一次只能有一个线程持有mutex,必须由mutex的持有者释放,不能递归上锁和释放
  • 互斥体API函数

    函数描述 函数
    定义并初始化mutex变量 DEFINE_MUTEX(name)
    初始化mutex void mutex_init(struct mutex *lock)
    获取mutex,上锁,获取失败进入休眠 void mutex_lock(struct mutex *lock)
    释放mutex void mutex_ulock(struct mutex *lock)
    尝试获取Mutex,成功返回1 失败返回0 int mutex_try_lock(struct mutex *lock)
    判断mutex是否被获取,是-1 否-0 int mutex_is_locked(struct mutex *lock)
    mutex上锁,使用此函数进入休眠后可以被信号打断 void mutex_lock_interruptible(struct mutex *lock)
    • 示例代码
      1
      2
      3
      4
      5
      struct mutex lock;   //定义一个互斥体
      mutex_init(&lock); //初始化互斥体
      mutex_lock(&lock); //上锁
      /* 临界区 */
      mutex_unlock(&lock);

linux并发与竞争
http://example.com/2022/10/02/linux并发与竞争/
作者
Hector
发布于
2022年10月2日
许可协议