内存管理十 linux内核并发与同步机制

一、临界资源:

  临界区是指访问或操作共享资源的代码段,这些资源无法同时被多个执行线程访问,为了避免临界区的并发

访问,需要保证临界区的原子性,临界区不能有多个并发源同时执行,原子性保护的是资源和数据,包括静态局部

变量、全局变量、共享的数据结构、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
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文无所谓版权,欢迎转载。

猜你喜欢

转载自blog.csdn.net/frank_zyp/article/details/84072796