一、临界资源:
临界区是指访问或操作共享资源的代码段,这些资源无法同时被多个执行线程访问,为了避免临界区的并发
访问,需要保证临界区的原子性,临界区不能有多个并发源同时执行,原子性保护的是资源和数据,包括静态局部
变量、全局变量、共享的数据结构、Buffer缓存等各种资源数据,产生并发访问的并发源主要有如下:
- 中断和异常:中断发生后,中断执行程序和被中断的进程之间可能产生并发访问;
- 软中断和tasklet:软中断或者tasklet随时可能会被调度执行,从而打断当前正在执行的进程上下文;
- 内核抢占:进程调度器支持抢占特性,会导致进程和进程之间的并发访问;
- 多处理器并发执行:多个处理器的上可以同时执行多个相同或者不同的进程。
二、同步机制:
上面已经介绍了临界资源的相关内容,linux内核中的同步机制就是为了保证临界资源不产生并发访问的处理
方法,当然内核中针对不同的场景有不同的同步机制,如:原子操作、自旋锁、信号量、Mutex互斥体、读写锁、
RCU锁等机制一面会一一介绍,并比较各种机制之间的差别。
1、原子操作:
(1)原子变量操作:
原子操作是指保证指令以原子的方式执行,执行的过程中不会被打断。linux内核提供了atomic_t类型的原子
变量,变量的定义如下:
typedef struct {
int counter;
} atomic_t;
原子变量的常见的操作接口和用法如下:
#define ATOMIC_INIT(i) { (i) } //定义一个原子变量并初始化为i
#define atomic_read(v) READ_ONCE((v)->counter) //读取原子变量的值
static inline void atomic_add(int i, atomic_t *v) //原子变量v增加i
static inline void atomic_sub(int i, atomic_t *v) //原子变量v减i
static inline void atomic_inc(atomic_t *v) //原子变量值加1
static inline void atomic_dec(atomic_t *v) //原子变量值减1
......
atomic_t use_cnt;
atomic_set(&use_cnt, 2);
atomic_add(4, &use_cnt);
atomic_inc(use_cnt);
(2)原子位操作:
在编写代码时,有时会使用到对某一个寄存器或者变量设置某一位的操作,可以使用如下的接口:
unsigned long word = 0;
set_bit(0, &word); /*第0位被设置*/
set_bit(1, &word); /*第1位被设置*/
clear_bit(1, &word); /*第1位被清空*/
change_bit(0, &word); /*翻转第0位*/
2、自旋锁spin_lock:
(1)自旋锁的特点如下:
a、忙等待、不允许睡眠、快速执行完成,可用于中断服务程序中;
b、自旋锁可以保证临界区不受别的CPU和本CPU的抢占进程打扰;
c、如果A执行单元首先获得锁,那么当B进入同一个例程时将获知自旋锁已被持有,需等待A释放后才能进入,
所以B只好原地打转(自旋);
d、自旋锁锁定期间不能调用可能引起进程调度的函数,如:copy_from_user(),copy_to_user(), kmalloc(),msleep();
(2)自旋锁的操作接口:
//定义于#include<linux/spinlock_types.h>
spinlock_t lock; //定义自旋锁
spin_lock_init(&lock); //初始化自旋锁
spin_lock(&lock); //如不能获得锁,原地打转。
spin_trylock(&lock);//尝试获得,如能获得锁返回真,不能获得返回假,不再原地打转。
spin_unlock(&lock); //与spin_lock()和spin_trylock()配对使用。
spin_lock_irq()
spin_unlock_irq()
spin_lock_irqsave()
spin_unlock_irqrestore()
spin_lock_bh()
spin_unlock_bh()
(3)使用举例:
spinlock_t lock; //定义自旋锁 --全局变量
spin_lock_init(&lock); //初始化自旋锁 --初始化函数中
spin_lock(&lock); // 获取自旋锁 --成对在操作前后使用
//临界区......
spin_unlock(&lock) //释放自旋锁
3、信号量:
(1)信号量的特点:
a、睡眠等待、可以睡眠、可以执行耗时长的进程;
b、共享资源允许被多个不同的进程同时访问;
c、信号量常用于暂时无法获取的共享资源,如果获取失败则进程进入不可中断的睡眠状态,只能由释放资源的进程来唤醒;
d、信号量不能在中断服务程序中使用,因为中断服务程序是不允许进程睡眠的;
(2)信号量的使用:
struct semaphore {
atomic_t count; //共享计数值
int sleepers; //等待当前信号量进入睡眠的进程个数
wait_queue_head_t wait; // wait是当前信号量的等待队列
};
//count用于判断是否可以获取该信号量:
//count大于0说明可以获取信号量;
//count小于等于0,不可以获取信号量;
操作接口如下:
static inline void down(struct semaphore * sem)
//获取信号量,获取失败则进入睡眠状态
static inline void up(struct semaphore * sem)
//释放信号量,并唤醒等待队列中的第一个进程
int down_interruptible(struct semaphore * sem);
// down_interruptible能被信号打断;
int down_trylock(struct semaphore * sem);
//该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。
如:
down(sem);
...临界区...
up(sem);
4、mutex_lock互斥锁:
互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子API之上实现的,但这对于内核用户是不可见的。
对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁
不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互
斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥
锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑。
(1)互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section),因此,在任意时刻,只有一个线程被允许
进入这样的代码保护区。mutex实际上是count=1情况下的semaphore。
struct mutex {
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
struct task_struct *owner;
......
};
结构体成员说明:
atomic_t count;指示互斥锁的状态:
1 没有上锁,可以获得;0 被锁定,不能获得,初始化为没有上锁;
spinlock_t wait_lock;
等待获取互斥锁中使用的自旋锁,在获取互斥锁的过程中,操作会在自旋锁的保护中进行,
初始化为为锁定。
struct list_head wait_list;
等待互斥锁的进程队列。
(2)mutex的使用:
a、初始化
mutex_init(&mutex); //动态初始化互斥锁
DEFINE_MUTEX(mutexname); //静态定义和初始化互斥锁
b、上锁
void mutex_lock(struct mutex *lock);
//无法获得锁时,睡眠等待,不会被信号中断。
int mutex_trylock(struct mutex *lock);
//此函数是 mutex_lock()的非阻塞版本,成功返回1,失败返回0
int mutex_lock_interruptible(struct mutex *lock);
//和mutex_lock()一样,也是获取互斥锁。在获得了互斥锁或进入睡眠直
//到获得互斥锁之后会返回0。如果在等待获取锁的时候进入睡眠状态收到一
//个信号(被信号打断睡眠),则返回_EINIR。
c、解锁
void mutex_unlock(struct mutex *lock);
5、读写锁:
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,
写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同
时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有
一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任
何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放
该读写锁。
(1)操作函数接口:
rwlock_init(x)
DEFINE_RWLOCK(x)
read_trylock(lock)
write_trylock(lock)
read_lock(lock)
write_lock(lock)
read_unlock(lock)
write_unlock(lock)
(2)总结:
A、读写锁本质上就是一个计数器,初始化值为0x01000000,表示最多可以有0x01000000个读者同时获取读锁;
B、获取读锁时,rw计数减1,判断结果的符号位是否为1,若结果符号位为0时,获取读锁成功;
C、获取读锁时,rw计数减1,判断结果的符号位是否为1。若结果符号位为1时,获取读锁失败,表示此时读写锁被写者
占有,此时调用__read_lock_failed失败处理函数,循环测试rw+1的值,直到结果的值大于等于1;
D、获取写锁时,rw计数减RW_LOCK_BIAS_STR,即rw-0x01000000,判断结果是否为0。若结果为0时,获取写锁成功;
E、获取写锁时,rw计数减RW_LOCK_BIAS_STR,即rw-0x01000000,判断结果是否为0。若结果不为0时,获取写锁失败,
表示此时有读者占有读写锁或有写着占有读写锁,此时调用__write_lock_failed失败处理函数,循环测试rw+0x01000000,
直到结果的值等于0x01000000;
F、通过对读写锁的源代码分析,可以看出读写锁其实是带计数的特殊自旋锁,能同时被多个读者占有或一个写者占有,
但不能同时被读者和写者占有。
6、读写信号量:
(1)特点:
a、同一时刻最多有一个写者(writer)获得锁;
b、同一时刻可以有多个读者(reader)获得锁;
c、同一时刻写者和读者不能同时获得锁;
(2)相关结构和函数接口:
struct rw_semaphore {
/*读/写信号量定义:
* - 如果activity为0,那么没有激活的读者或写者。
* - 如果activity为+ve,那么将有ve个激活的读者。
* - 如果activity为-1,那么将有1个激活的写者。 */
__s32 activity; /*信号量值*/
spinlock_t wait_lock; /*用于锁等待队列wait_list*/
struct list_head wait_list; /*如果非空,表示有进程等待该信号量*/
};
void init_rwsem(struct rw_semaphore* rw_sem); //初始化读写信号量
void down_read(struct rw_semaphore* rw_sem); //获取读信号量
int down_read_trylock(struct rw_semaphore* rw_sem); //尝试获取读信号量
void up_read(struct rw_semaphore* rw_sem);
void down_write(struct rw_semaphore* rw_sem); //获取写信号量
int down_write_trylock(struct rw_semaphore* rw_sem);//尝试获取写信号量
void up_write(struct rw_semaphore* rw_sem);
(3)使用方法:
rw_semaphore sem;
init_rwsem(&sem);
down_read(&sem);
...临界区...
up_read(&sem);
down_write(&sem);
...临界区...
up_write(&sem);
三、总结与拓展
1、针对上面的各种同步机制直接的差异,可以如下表格清晰的表明:
2、在解决稳定性相关的问题时,难免会出现一些死锁的问题,可以添加一些debug宏来配合调试:
CONFIG_LOCK_STAT=y
CONFIG_DEBUG_LOCKDEP=y
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_MUTEXES=y
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_RWSEM_SPIN_ON_OWNER=y
如下有一个简单的死锁的例程:
static DEFINE_SPINLOCK(hack_spinA);
static DEFINE_SPINLOCK(hack_spinB);
void hack_spinBA(void)
{
printk("%s(),hack A and B\n",__FUNCTION__);
spin_lock(&hack_spinA);
spin_lock(&hack_spinB);
}
void hack_spinAB(void)
{
printk("%s(),hack A \n",__FUNCTION__);
spin_lock(&hack_spinA);
spin_lock(&hack_spinB);
}
static ssize_t hello_test_show(struct device *dev, struct device_attribute *attr, char *buf)
{
printk("%s() \n",__FUNCTION__);
hack_spinBA();
return 0;
}
static ssize_t hello_test_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
int test;
printk("hello_test %s,%d\n",__FUNCTION__,__LINE__);
test = 4;
hack_spinAB();
return printk("%s() test = %d\n",__FUNCTION__,test);
}
static DEVICE_ATTR(hello_test, 0664, hello_test_show, hello_test_store);
static int hello_test_probe(struct platform_device *pdev)
{
printk("%s()\n",__FUNCTION__);
device_create_file(&pdev->dev, &dev_attr_hello_test);
return 0;
}
上面的例子中,先cat对应的节点会先后拿住hack_spinA和hack_spinB的两把锁,但并没有去释放这两把锁,
所以再对应的相应节点做echo操作时,会出现一直无法拿住这把锁,等待30S后会触发HWT的重启,看重启的DB
文件可以发现:
/***********在58S时通过cat 执行到了hello_test_show函数,从而拿住了锁********/
[ 58.453863] (1)[2577:cat]Dump cpuinfo
[ 58.456057] (1)[2577:cat]hello_test_show()
[ 58.456093] (1)[2577:cat]hack_spinBA(),hack A and B
[ 58.457426] (1)[2577:cat]note: cat[2577] exited with preempt_count 2
[ 58.458884] (1)[2577:cat]
[ 58.458920] (1)[2577:cat]=====================================
[ 58.458931] (1)[2577:cat][ BUG: cat/2577 still has locks held! ]
[ 58.458944] (1)[2577:cat]4.4.146+ #5 Tainted: G W O
[ 58.458955] (1)[2577:cat]-------------------------------------
[ 58.458965] (1)[2577:cat]lockdep: [Caution!] cat/2577 is runable state
[ 58.458976] (1)[2577:cat]lockdep: 2 locks held by cat/2577:
[ 58.458988] #0: (hack_spinA){......}
[ 58.459015] lockdep: , at: (1)[2577:cat][<c0845e8c>] hack_spinBA+0x24/0x3c
[ 58.459049] #1: (hack_spinB){......}
[ 58.459074] lockdep: , at: (1)[2577:cat][<c0845e94>] hack_spinBA+0x2c/0x3c
[ 58.459098] (1)[2577:cat]
/**********在64S是通过echo指令调用到hello_test_store再次去拿锁,导致死锁******/
[ 64.083878] (2)[1906:sh]hello_test hello_test_store,41
[ 64.083914] (2)[1906:sh]hack_spinAB(),hack A
/**********在94S时出现 HWT 重启***************/
[ 94.092084] -(2)[1906:sh][<c0836a40>] (aee_wdt_atf_info) from [<c0837428>] (aee_wdt_atf_entry+0x180/0x1b8)
[ 94.093571] -(2)[1906:sh][<c08372a8>] (aee_wdt_atf_entry) from [<c0152bfc>] (preempt_count_sub+0xe4/0x100)
[ 94.095055] -(2)[1906:sh][<c019694c>] (do_raw_spin_lock) from [<c0c6f5f8>] (_raw_spin_lock+0x48/0x50)
[ 94.096548] -(2)[1906:sh][<c0c6f5b0>] (_raw_spin_lock) from [<c0845ef4>] (hack_spinAB+0x24/0x3c)
[ 94.097666] -(2)[1906:sh][<c0845ed0>] (hack_spinAB) from [<c0845f30>] (hello_test_store+0x24/0x44)
[ 94.098785] -(2)[1906:sh][<c0845f0c>] (hello_test_store) from [<c04c56cc>] (dev_attr_store+0x20/0x2c)
再去看CPU的喂狗信息:kick=0x0000000b,check=0x0000000f,
可以发现是因为CPU2死锁导致没有及时喂狗,所以触发HWT重启,看CPU2的堆栈也是挂载sh的进程上:
cpu#2: Online
.nr_running : 5
.load
runnable tasks:
task PID tree-key switches prio wait-time sum-exec sum-sleep
---------------------------------------------------
DispSync 470 0.000000 1054 97 0.000000 157.709688 0.000000 /
kworker/2:2 1083 56925.416956 264 120 119.050538 21.742080 35541.090392 /
R sh 1906 84650.355638 115 120 14.202615 28671.890146 21283.409357 /
作者:frank_zyp
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文无所谓版权,欢迎转载。