----------------------------------------竞争和并发--------------------------------------------------------------
Linux内核中,什么情况会发生竞态?
对称多处理器(SMP)的多个CPU
单CPU内进程与抢占它的进程
中断(硬中断、软中断、Tasklet、底半部)与进程之间
如何解决竞态问题?
保证对共享资源的互斥访问,
指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
不要长时间屏蔽中断:会出现系统很卡。其他程序没法执行。
信号量( semaphore )
信号量本质上是一个整数值,
一对操作函数通常称为P和V。
进入临界区
相关信号量上调用P;
如果信号量>零,则该值会减小一,而进程可以继续。
如果信号量的值=零(或更小),进程必须等待直到其他人释放该信号量。
退出临界区
信号量的解锁通过调用V完成;
该函数增加信号量的值,
并在必要时唤醒等待的进程。
信号量的值应初始化为1。
一个自旋锁是一个互斥设备,
只有两个值:“锁定”和“解锁”。
实现为某个整数值中的单个位。希望获得某特定锁的代码测试相关的位。
锁可用,则“锁定”位被设置,而代码继续进入临界区
如果锁被其他人获得,则代码进入忙循环并重复检查这个锁,直到该锁可用为止。这个循环就是自旋锁的“自旋”
信号量和自旋锁的区别:
信号量分为P操作和V操作。即进入和退出临界区的操作。一般用于多进程 或多线程
自旋锁: 一般是忙等待。用于同一个线程操作里面(这里不一定对)。主要用于驱动层里面。
信号量本质是一个整数值,一般是加1或者减一。信号量可以休眠,自旋锁不可以休眠。
区别:
1、 自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小;
信号量允许阻塞,可以使用于临界区大的情况。
2、自旋锁实际上是忙等待,自旋锁可能导致系统死锁,自旋锁期间不能调用可能引起调度的函数。如:copy_from_user(),copy_to_user(),kamlloc().
信号量:进程获取不到信号量的时候,不会自旋而是进入睡眠的等待状态。
自旋锁
------------------------------------------------------
自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被占用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被占用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被占用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
自旋锁的基本形式如下:
spin_lock(&mr_lock);
//临界区
spin_unlock(&mr_lock);
因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处 理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。
总结:自旋锁是专为防止多处理器并发而引入的一种锁,多用于中断,想要获取它的任务忙等待,时间越短越好,只能被一个内核任务持有,只允许一个线程存在于临界区。
时间较长用信号量。
信号量:任务获取不到信号量则休眠等待;自旋锁:任务获取不到则忙等待(即自旋)。
信号量主要用于线程同步。
互斥体:也叫互斥锁。是原子操作。信号量的特殊形式。只有0、1两种状态
因此,在很多情况下,决定使用自旋锁还是互斥体相对来说很容易:
(1) 如果临界区需要睡眠,只能使用互斥体,因为在获得自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的;
(2) 由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自旋锁。(第4章将介绍更多的关于中断上下文的限制。)
如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁使用实例
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
critical section
spin_unlock(&lock);
案例:
为了论证并发保护的用法,我们首先从一个仅存在于进程上下文的临界区开始,并以下面的顺序逐步增加复杂性:
(1) 非抢占内核,单CPU情况下存在于进程上下文的临界区; 不需要加锁。 因为非抢占且单cpu不用担心竞争临界区的问题。 所谓临界区(上下文)就是指某些寄存器。
(2) 非抢占内核,单CPU情况下存在于进程和中断上下文的临界区;
假设有A B 两个进程和一个中断,同时进入临界区。怎么办?
分析:因为非抢占,不用担心A ,B进程抢夺临界区。但是中断会跟进程A或B抢夺临界区。且中断优先级高。此时只要进制中断。
(3) 可抢占内核,单CPU情况下存在于进程和中断上下文的临界区;
A B 中断三者同时竞争临界区:
临界区之前禁止内核抢占、中断,并在退出临界区的时候恢复内核抢占和中断。因此,执行单元A和B使用了自旋锁
unsigned long flags;
Point A:
/* Save interrupt state.
* Disable interrupts - this implicitly disables preemption */
spin_lock_irqsave( & mylock, flags);
/* ... Critical Section ... */
/* Restore interrupt state to what it was at Point A */
spin_unlock_irqrestore( & mylock, flags);
(4) 可抢占内核,SMP情况下存在于进程和中断上下文的临界区。
现在假设临界区执行于SMP机器上,而且你的内核配置了CONFIG_SMP和CONFIG_PREEMPT。
在SMP系统上,获取自旋锁时,仅仅本CPU上的中断被禁止。因此,一个进程上下文的执行单元(图2-4中的执行单元A)在一个CPU上运行的同时,一个中断处理函数(图2-4中的执行单元C)可能运行在另一个CPU上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区。中断上下文需要调用spin_lock()/spin_unlock():
spin_lock( & mylock);
/* ... Critical Section ... */
spin_unlock( & mylock);
除了有irq变体以外,自旋锁也有底半部(BH)变体。在锁被获取的时候,spin_lock_bh()会禁止底半部,而spin_unlock_bh()则会在锁被释放时重新使能底半部。我们将在第4章讨论底半部。
2.5.2 原子操作
原子操作用于执行轻量级的、仅执行一次的操作,例如修改计数器、有条件的增加值、设置位等。原子操作可以确保操作的串行化,不再需要锁进行并发访问保护。原子操作的具体实现取决于体系架构。
参考链接:
https://blog.csdn.net/muojie/article/details/6791069?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~aggregatepage~first_rank_ecpm_v1~rank_v31_ecpm-7-6791069.pc_agg_new_rank&utm_term=%E8%AF%B7%E7%AE%80%E8%BF%B0%E5%86%85%E6%A0%B8%E4%B8%AD%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6%E7%9A%84%E4%B8%AD%E8%87%AA%E6%97%8B%E9%94%81%E5%92%8C%E4%BF%A1%E5%8F%B7%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB&spm=1000.2123.3001.4430
原子变量操作
整型原子操作
类型: atomic_t
设置原子变量的值
void atomic_set(atomic_t *v,int i);
atomic_t v = ATOMIC_INIT(0);
获取原子变量的值
int atomic_read(atomic_t *v);
原子变量加/减
void atomic_add(int i,atomic_t *v);
void atomic_sub(int i,atomic_t *v);
原子变量自增/自减
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
死锁产生的原因
① 系统资源不足:系统中所拥有的资源其数量不足以满足线程运行的需要,使得在运行过程中,因争夺资源而陷入僵局。
② 线程间推进顺序非法:线程间在运行过程中,申请和释放的顺序不合法。
③ 资源分配不当。
解决:1. 保证系统资源 2. 按合法正确的顺序加解锁 3. 合理分配资源。
避免死锁规则:
其他规则
避免某个获得锁的函数调用其他同样试图获取这个锁的函数,否则代码就会死锁;
不论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁,如果试图这么做,系统将挂起;
锁的顺序规则
按同样的顺序获得锁
如果必须获得一个局部锁和一个属于内核更中心位置的锁,则应该首先获取自己的局部锁
如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量;在拥有自旋锁时调用down(可导致休眠)是个严重的错误的
completion :同步机制
一种轻量级的机制,它允许一个线程告诉另一线程某个工作已经完成。
等待completion void wait_for_completion(struct completion *c);
触发完成
void complete(struct completion *c);
void complete_all(struct completion *c);
必须在重复使用该结构之前重新初始化它。下面这个宏可用来快速执行重新初始化:
INIT_COMPLETION(struct completion c);
实例:
DECLARE_COMPLETION(xxx_comp);//定义
ssize_t complete_read(struct file *filp,
char __user *buf,size_t count,
loff_t *pos)
{
wait_for_completion(&xxx_comp);
return 0;
}
ssize_t complete_write(struct file *filp,
const char __user *buf,size_t count,
loff_t *pos)
{
complete(&xxx_comp);
return count;
}
位原子操作:
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr);
void test_bit(nr, void *addr);
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
总结:处理并发的方法:信号量、自旋锁、completion、中断屏蔽和seqlock等
信号量和自旋锁区别
如何避免死锁
原子操作
Linux内核中,什么情况会发生竞态?
对称多处理器(SMP)的多个CPU
单CPU内进程与抢占它的进程
中断(硬中断、软中断、Tasklet、底半部)与进程之间
如何解决竞态问题?
保证对共享资源的互斥访问,