并发竞争理论与实例分析

概念

  • 并发

在这里插入图片描述
并发会造成多个程序同时访问一个共享资源,这就是竞态。

  • 并行
    在这里插入图片描述
    当任务一和任务二抢占同一个资源时,并行也可能出现竞态。

  • 并行+并发
    在这里插入图片描述

造成并发的情况

  • 多线程并发访问
    主要原因。
  • 中断程序并发访问
    中断可以随时产生,并且权力太高,一旦触发就会去切换任务(哪怕只是中断上文的简易添加任务队列,也会打算原先的工作顺序)
  • 抢占式并发访问
    Linux2.6之后支持了抢占,正在执行的进程随时可能被抢占。
  • 多处理器(SMP)并发访问
    核间并发访问

并发时应该保护什么?

  • 驱动:全局变量、设备文件(设备结构体)
  • 应用:全局变量、普通文件

一、原子操作

指该操作在处理完成之前不会被打断,一般用于整形、位的保护。

typedef struct{
    int counter;
} atomic_t;

#ifdef CONFIG_64BIT
typedef struct{
    long counter;
} atomic64_t;
#endif
ATOMIC_INIT(int i)                         // 定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v)             // 读取 v 的值,并且返回。 
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 减去 i 值。 
void atomic_inc(atomic_t *v)             // 给 v 加 1,也就是自增。
void atomic_dec(atomic_t *v)             // 从 v 减 1,也就是自减 
int atomic_dec_return(atomic_t *v)         // 从 v 减 1,并且返回 v 的值。
int atomic_inc_return(atomic_t *v)         // 给 v 加 1,并且返回 v 的值。 
int atomic_sub_and_test(int i, atomic_t *v) //从 v 减 i,如果结果为 0 就返回真,否则返回假 
int atomic_dec_and_test(atomic_t *v)         // 从 v 减 1,如果结果为 0 就返回真,否则返回假 
int atomic_inc_and_test(atomic_t *v)           // 给 v 加 1,如果结果为 0 就返回真,否则返回假 
int atomic_add_negative(int i, atomic_t *v)    // 给 v 加 i,如果结果为负就返回真,否则返回假

/* 位操作 */
void set_bit(int nr, void *p)     将 p 地址的第 nr 位置 1void clear_bit(int nr,void *p)     将 p 地址的第 nr 位清零。
void change_bit(int nr, void *p)    将 p 地址的第 nr 位进行翻转。
int test_bit(int nr, void *p)     获取 p 地址的第 nr 位的值。 
int test_and_set_bit(int nr, void *p)     将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。
int test_and_clear_bit(int nr, void *p)     将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。 
int test_and_change_bit(int nr, void *p)     将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。

案例:

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        */ 

实战:

#include <linux/atomic.h>
#include <asm/atomic.h>

static atomic64_t v=ATOMIC_INIT(1);

static int test_open(struct inode *inode, struct file *file){
    if(!atomic64_dec_and_test(&v)){
        atomic64_t(&v);
        return -EBUSY;
    }
    ......
}

static int release_test(struct inode *inode,struct file *file)
{
    atomic64_set(&v,1);//将原子类型变量 v 的值赋 1
    return 0;
}

/* 应用程序 */
int main(int argc, char *argv[])
{
    int fd;
    fd = open("/dev/test", O_RNWR);
    {
        perror("open error\n");
        return fd;    
    }
    
    sleep(5);    // 若打开设备则独占5秒
    
    close(fd);
    return 0;
}

将 main.c 编译成两个可执行文件:a.out、b.out 并执行:

./a.out &
./b.out

运行结果:
在这里插入图片描述

二、自旋锁

自旋锁(spin lock)是一种非阻塞锁,也
就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在
不断的消耗 CPU 的时间,不停的试图获取锁。
和应用层的 mutex 锁不同,mutex 锁获取不到直接就让线程睡眠了。

/* /include/linux/spinlock_types.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;
DEFINE_SPINLOCK(spinlock_t lock)         // 定义并初始化自旋锁
int spin_lock_init(spinlock_t *lock)     // 初始化自旋锁
void spin_lock(spinlock_t *lock)         // 获取指定的自旋锁,也叫做加锁
void spin_unlock(spinlock_t *lock)       // 释放指定的自旋锁
int spin_trylock(spinlock_t *lock)       // 尝试获取指定的自旋锁,如果没有获取到就返回 0
int spin_is_locked(spinlock_t *lock)     // 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0

自旋锁的使用步骤:
1 在访问临界资源的时候先申请自旋锁
2 获取到自旋锁之后就进入临界区,获取不到自旋锁就“原地等待”。
3 退出临界区的时候要释放自旋锁。
实战:

#include <linux/spinlock.h>

static spinlock_t splock;
static int flag = 1;
/* 不同的进程打开该设备都是在这个设备的进程中执行的,
   所以就算是不同的进程在使用设备,访问的都是这个设备的进程空间
*/

static int open_test(struct inode *inode,struct file *file)
{
    spin_lock(&splock);        //自旋锁加锁
    if(flag != 1){                    //判断标志位 flag 的值是否等于 1
        spin_unlock(&spinlock_test);  //有进程打开设备了就自旋锁解锁并退出
        return -EBUSY;
    }
    flag = 0;//将标志位的值设置为 0
    spin_unlock(&splock);       //自旋锁解锁
    return 0;
}

static int release_test(struct inode *inode,struct file *file)
{
    spin_lock(&splock);//自旋锁加锁
    flag = 1;
    spin_unlock(&splock);//自旋锁解锁
    return 0;
}

static int __init atomic_init(void)
{
    spin_lock_init(&splock);
    ....
}

其实也就是为了保护临界里的操作不被打断,跟独占设备没用任何关系。

自旋锁死锁

当多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进
程都将无法向前推进,这种情况就是死锁。
自旋锁死锁发生存在两种情况:
(1)第一种情况是拥有自旋锁的进程 A 在内核态阻塞了,内核调度 B 进程,碰巧 B 进程
也要获得自旋锁,此时 B 只能自旋转。而此时抢占已经关闭(在单核条件下)不会调度 A 进程了,
B 永远自旋,产生死锁,如下图(图 22-1)所示:
在这里插入图片描述
相应的解决办法是,在自旋锁的使用过程中要尽可能短的时间内拥有自旋锁,而且不能在临界区中调用导致线程休眠的函数
其实这里针对的是打开同一设备,设备A获得锁后内核态阻塞完全不影响其他设备的使用。但是设备A就用不了,这么傻逼的开发应该没人想这么做吧。所以一旦获得锁就千万不能调用 sleep() 或者其他休眠之类的函数,一旦让出了CPU时间,那么无法被唤醒了。

(2)第二种情况是进程 A 拥有自旋锁,中断到来,CPU 执行中断函数,中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,只能自旋,从而产生死锁,如下图:
在这里插入图片描述
对于中断引发的死锁,最好的解决方法就是在获取锁之前关闭本地中断,Linux 内核在
“/include/linux/spinlock.h”文件中提供了相应的 API 函数:

void spin_lock_irq(spinlock_t *lock)     // 禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock)   // 激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) // 恢复中断状态,关闭中断并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) // 将中断状态恢复到以前的状态,打开中断并释放自旋锁
void spin_lock_bh(spinlock_t *lock)      // 关闭下半部,获取自旋锁
void spin_unlock_bh(spinlock_t *lock)    // 打开下半部,获取自旋锁

由于 Linux 内核运行是非常复杂的,很难确定某个时刻的中断状态,因此建议使用
spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会
恢复中断状态。

三、信号量

信号量是操作系统中最典型的用于同步和互斥的手段,本质上是一个全局变量。
信号量的
值表示控制访问资源的线程数,可以根据实际情况来自行设置,如果在初始化的时候将信号量
量值设置为大于 1,那么这个信号量就是计数型信号量,允许多个线程同时访问共享资源。

果将信号量量值设置为 1,那么这个信号量就是二值信号量,同一时间内只允许一个线程访问
共享资源。
注意:信号量的值不能小于 0
当信号量的值为 0 时,想访问共享资源的线程必须
等待,直到信号量大于 0 时,等待的线程才可以访问。
当访问共享资源时,信号量执行“减一”
操作,访问完成后再执行“加一”操作。

信号量具有休眠特性,所以不能用在中断函数中;
如果休眠时间很短又频繁的休眠,反而带来更多资源的消耗,这时使用自旋锁效果更好。

如果需要同时使用 信号量 和 自旋锁,那么必须先 获得信号量再获得自旋锁,否则会因为信号量而永久休眠。

/* /include/linux/semaphore.h */
struct semaphore {
    raw_spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
};
DEFINE_SEAMPHORE(name)               // 定义信号量,并且设置信号量的值为 1。
void sema_init(struct semaphore *sem, int val) // 初始化信号量 sem,设置信号量值为 val。
void down(struct semaphore *sem)     // 获取信号量,不能被中断打断,如 ctrl+c
int down_interruptible(struct semaphore *sem)  // 获取信号量,可以被中断打断,如 ctrl+c
void up(struct semaphore *sem)       // 释放信号量
int down_trylock(struct semaphore *sem);       // 尝试获取信号量,如果能获取到信号量就获取,并且返回 0,不能就返回非 0

案例:

#include <linux/semaphore.h>

struct semaphore semaphore_test;    // 定义信号量

static int open_test(struct inode *inode,struct file *file)
{
printk("\nthis is open_test \n");
down(&semaphore_test);              // 信号量数量减 1
return 0;
}

static int release_test(struct inode *inode,struct file *file)
{
    up(&semaphore_test);            // 信号量数量加 1
    printk("\nthis is release_test \n");
    return 0;
}

static int __init xxx_init(void)
{
    sema_init(&semaphore_test,1);    // 安装驱动时初始化数量为 1
    ......
}

这么做之后,当一个线程打开该设备,如果未关闭该设备,另一个线程也打开该设备时就会陷入阻塞,直到第一个线程关闭了该设备,第二个线程才能打开设备。

四、互斥锁

互斥锁的实现原理与二值信号量是不同的,而且效率更高。
同一时刻只能有一个线程持有互斥锁,
并且只有持有者才可以解锁,并且不允许递归上锁和解锁。

/* /include/linux/mutex.h */
struct mutex {
    atomic_long_t owner;
    spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
    struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
    void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
};
DEFINE_MUTEX(name)               // 定义并初始化一个 mutex 变量。
void mutex_init(mutex *lock)     // 初始化 mutex。
void mutex_lock(struct mutex *lock)     // 获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。
void mutex_unlock(struct mutex *lock)   // 释放 mutex,也就给 mutex 解锁。
int mutex_is_locked(struct mutex *lock) // 判断 mutex 是否被获取,如果是的话就返回1,否则返回 0。

案例:

#include <linux/mutex.h>

struct mutex mutex_test;         // 定义互斥锁

static int open_test(struct inode *inode,struct file *file)
{
    mutex_lock(&mutex_test);//互斥锁加锁
    return 0;
}

static int release_test(struct inode *inode,struct file *file)
{
    mutex_unlock(&mutex_test);    // 互斥锁解锁
    return 0;
}

static int __init atomic_init(void)
{
    mutex_init(&mutex_test);       // 对互斥体进行初始化
    ......
}

相应的,多线程中打开同一个设备,慢打开的时候会被阻塞,等到先打开的设备的线程执行完临界操作后关闭设备时,慢打开设备的线程才能唤醒打开设备。

读写自旋锁

允许多个读者同时持有锁,但只允许一个写者同时持有锁。
读写锁适合读者多,写者少的应用场景;
实际用的不多,略过。

读写信号量

类似读写自旋锁,为了区分不同的竞争者,比如允许读者共享,而写者互斥。

struct rw_semaphore{
    long count;
    struct list_head wait_list;
    raw_spinlock_t wait_lock;
}

DECLARE_RWSEM(sem)//静态声明sem变量
init_rwsem(sem);//初始化一个sem

down_read(struct rw_semaphore *sem);    // 读者减少信号量sem计算器,类似获取锁
down_write(struct rw_semaphore *sem);   // 写者减少sem计数器
up_read(struct rw_semaphore *sem);      // 增加sem计数器
up_write(struct rw_semaphore *sem);     // 增加sem计数器
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值