Linux内核同步笔记

几个基本概念

- 临界区(critical region):访问和操作共享数据的代码段;
- 原子操作:操作在执行中不被打断,要么不执行,要么执行完;
- 竞争条件: 两个线程处于同一个临界区内执行,对数据同时访问或操作,称之为竞争;
- 同步(synchronization):避免并发和防止竞争条件成为同步。

预防死锁

- 按顺序加锁,使用嵌套锁时,必须注意按顺序加锁,可以防止拥抱类死锁。
- 防止饥饿
- 不要重复请求同一个锁
- 设计力求简单。

原子操作

原子操作可以保证执行过程不被打断,内核提供了两种原子操作接口:一组针对整数进行操作,另一组针对单独的位进行操作。

原子整数操作

针对整数的操作只能对atomic_t类型的数据操作,atomic_t定义如下所示。

typedef struct{
volatile int counter;
} atomic_t;
尽管linux支持32整数,但是atomic_t类型只能当做24位来用,这是因为在SPARC体系结构上,原子操作的实现不同于其他系统,在int中的八位引入了一个锁来避免数据并发访问。
atomic_t v;						//	定义一个原子整数 v
atomic_t u = ATOMIC_INIT(0); //定义并初始化u atomic_set(&v, 4); //设置,赋值
atomic_add(2, &v); //加
atomic_inc(&v); //自增 atomic_read(&v); //返回int型数据

原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的。在编写代码时,能使用原子操作时,就尽量不要使用复杂的加锁机制。

64位整数的原子操作

64位整数操作时,只需要讲atomic_前缀相关的类型及操作函数修改为 atomic64_前缀就可以了。

typedef struct{
volatile long counter;
} atomic64_t; atomic64_t v; // 定义一个原子整数 v
atomic64_t u = ATOMIC_INIT(0); //定义并初始化u atomic64_set(&v, 4); //设置,赋值
atomic64_add(2, &v); //加
atomic64_inc(&v); //自增 atomic64_read(&v); //返回int型数据

原子位操作

位操作函数是针对普通地址进行操作的,无需特殊的原子类型,它的参数是一个指针和一个位号。32位系统上位号是031,64位系统上位号是063。

usigned long word = 0;

set_bit(0, &word);		//第0位被设置
set_bit(1, &word); //第1位被设置 printk("%ul", &word); //此处将打印3 clear_bit(1,&word); //第1位被清除
change_bit(0, &word); //第0位被翻转

自旋锁

自旋锁最多被一个可执行线程持有,如果一个线程试图获得一个已经被持有的自旋锁(即所谓的争用),那么该线程就会一直进行忙循环——旋转——等待锁重新可用。一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(自旋特别浪费处理器时间)。

需要注意的是,自旋锁不应被长时间持有,自旋锁适用于短时间内进行轻量级加锁。

自旋锁方法

DEFINE_SPIN;OCK(mr_lock);
spin_lock(&mr_lock); /* 临界区
* 操作共享数据的代码放于此处
* 以避免对共享数据的操作并发
*/ spin_unlock(&mr_lock);
需要注意,在单处理器机器上,编译的时候并不会加入自旋锁,它仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全踢出内核。

自旋锁可以在中断中使用,在中断中使用自旋锁时,要在使用之前首先禁止本地中断。否则,中断会打断正在持有锁的内核代码,有可能回去争用这个已经被持有的自旋锁,从而造成死锁。内核提供了同时禁止中断和获取锁的函数:

DEFINE_SPIN(mr_lock);
unsigned long flags; spin_lock_irqsave(&mr_lock, flags); //临界区(critical spin_unlock_irqrestore(&mr_lock, flags);
要对数据加锁,而不是对代码加锁。加锁保护的是临界区内的数据,而非代码。

除了上述静态方法添加自旋锁,还可以动态的添加。可以用spin_lock_init()函数动态创建自旋锁。

spin_lock();			//	获取指定的自旋锁
spin_lock_irq(); // 禁止本地中断,并获取自旋锁
spin_lock_irqsave(); // 获取本地中断状态,禁止本地中断,并获取自旋锁 spin_unlock(); // 释放指定的锁
spin_unlock_irq(); // 释放指定的锁,并打开中断
spin_unlock_irqrestore();// 释放指定锁,并将中断恢复的原有状态 spin_lock_init(); // 动态初始化指定的spinlock_t
spin_trylock(); // 试图获取指定的锁,若未获取就返回非0
spin_is_locked(); // 如果指定的锁正咋被获取,则返回非0,否则返回0

读写自旋锁

读取共享数据时,不会对数据造成改变,因此可以多个线程同时对数据进行读取。但是,写数据与读数据是不能同时的。

DEFINE_RWLOCK(mr_rwlock);

read_lock(&mr_rwlock);
// 临界区(只读)
read_unlock(&mr_rwlock); write_lock(&mr_lock);
// 临界区(读写),只能被一个线程获取
write_unlock(&mr_lock);

通常情况下,读锁和写锁处于完全分割的代码分支中。

read_lock();		// 获得指定的读锁
read_lock_irq(); // 禁止本地中断,并获取读锁
read_lock_irqsave();// 保存本地中断,禁止本地中断,获取读锁 read_unlock(); // 释放指定读锁
read_unlock_irq(); // 激活本地中断,并释放读锁
read_lock_irqstore();// 恢复中断到原有状态,并释放读锁 write_lock(); // 获得指定的写锁
write_lock_irq(); // 禁止本地中断,并获取写锁
write_lock_irqsave();// 保存本地中断,禁止本地中断,获取写锁 write_unlock(); // 释放指定写锁
write_unlock_irq(); // 激活本地中断,并释放写锁
write_lock_irqstore();// 恢复中断到原有状态,并释放写锁 write_trylock(); // 试图获取写锁,写锁不成功则返回非0值
rwlock_init(); // 初始化指定的rwlock
需要注意的是,读写自旋锁照顾读锁更多一点,当读锁被占用时,写操作处于等待状态。但是,读锁却可以继续占用锁,大量的读锁被挂起,会导致写锁处于饥饿状态。

信号量

信号量是一种睡眠锁,如果有一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列。

- 由于争优信号量的进程在等待时会睡眠,所以信号量适用于锁会被长时间锁定的情况。
- 锁被短时间持有的情况不适合使用信号量,因为睡眠、维护等待队列以及唤醒所花费的时间可能比锁占用的时间还要长
- 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不可睡眠的。
- 可以在持有锁时去睡眠,其他进程试图获取该锁时,并不会死锁,而是去睡眠了。
- 占用信号量时不可以同时占用自旋锁,因为在等待信号量时有可能睡眠,而持有自旋锁时是不允许睡眠的。

计数信号量和二值信号量

信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它,通过一个计数count来表示。只允许一个持有者的信号量称为二值信号量(也成为互斥信号量),其count为1。

Linux通过down()操作来请求获得一个信号量,down()对信号量计数减1,若结果大于等于0则持有该信号量,若小于0则线程被放入等待队列。临界区内操作完成之后,通过up()来释放信号量,信号量计数加1。

创建和初始化信号量

struct semaphore name;		// 定义信号量
sema_init(&name, count); // 初始化, count表示信号量的使用数量

创建互斥信号量可以用以下更简洁的方式

static DECLEAR_MUTEX(name);

更常见的情况是,信号量作为一个大数据结构动态创建。此时,只有指向该动态创建的信号量的简介指针,可以使用如下函数来对他进行初始化:

sama_init(sem, count);

动态初始化可以通过如下函数:

init_MUTEX(sem);

使用信号量

通过函数down_interruptible()获取指定信号量,如果信号量不可用,就将调用进程设置成TASK_INTERRUPTIBLE状态进入睡眠。

使用down_trylock()函数可以尝试以堵塞的方式来获取指定的信号量。在信号已经被占领时,它返回非0值;否则返回0,并让你成功持有信号量锁。

static DECLEAR_MUTEX(mr_sem);	//定义并声明一个信号量锁

if(down_interruptible(&mr_sem)){

	//信号量未获取
} /* 临界区... */ up(&mr_sem); //释放给定的信号量
//信号量主要方法

sema_init(struct semaphore *, int);		// 以指定的计数值初始化动态创建信号量
init_MUTEX(struct semaphore *); // 以计数值1初始化动态创建信号量
down_interruptible(struct semaphore *) // 试图获取指定信号量,若信号被占用,则进入中断休眠状态
down(struct semaphore*) // 试图获取指定信号量,若信号被占用,则进入不可中断睡眠状态
down_trylock(struct semaphore*) // 试图获取指定信号量,若信号被争用,则立刻返回非0值
up(struct semaphore*) // 释放信号量,如果睡眠队列不空,则唤醒其中一个任务

读写信号量

与读写自旋锁类似,将信号量更具体为读写信号量。所有的读写信号量都是互斥信号量,他们只针对写操作互斥,不针对读者。也就是说,只要没有写锁定,并发的读锁数量不限。只要有写锁,就不可以有其他读锁或者写锁。

// 静态创建
static DECLEAR_RWSEM(name) // 动态创建
init_rwsem(struct rw_semaphore * sem) // 使用例子
static DECLEAR_RWSEM(mr_rwsem); down_read(&mr_rwsem); //获取读信号量锁 // 临界区(只读) up_read(&mr_rwsem); //释放读信号量锁 down_write(&mr_rwsem); // 获取写信号量锁 // 临界区(写) up_write(&mr_rwsem); // 释放写信号量锁

互斥体(mutex)

一种互斥的睡眠锁,其操作与计数为1的信号量相似,其接口更简单。

DEFINE_MUTEX(name);		// 静态初始化

mutex_init(&mutex);		// 动态初始化

mutex_lock(&mutex);		// 获取锁
/* 临界区 */
mutex_unlock(&mutex); // 释放锁
- 任何时刻只有一个任务可以持有mutex
- 给mutex上锁者必须负责给其解锁
- 在同一个上下文中上锁和解锁
- 当持有一个mutex时,进程不可以退出。
- mutex不能在中断或者下半部中使用,即使是mutex_trylock()也不行
- mutex只能通过官方API管理 信号量和互斥体二者很相似,在使用时要优先使用mutex,只有在很特殊的场合才会使用信号量(一般在底层)。

完成变量(completion variable)

如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,可以利用完成变量使两个任务得以同步。

DECLEAR_COMPLETION(mr_comp);	// 静态初始化
init_completion(&mr_comp); //动态创建 wait_for_completion(&mr_comp); //等待某完成变量接受信号
complete(&mr_comp); //发信号唤醒任何等待的任务

顺序锁

顺序锁的实现是通过一个序列计数器实现的,当有疑义的数据被写入之后,会得到一个锁,并且计数值增加。在读取数据前后,序列号都会被读取。如果读取的序列号值相同,说明在读操作过程中没有被写操作打断过。此外,如果序列数是偶数说明没有写操作发生,因为写锁会使值变成基数,读操作后值恢复到偶数。

seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_seq_lock);

write_seqlock(&mr_seq_lock);
//写锁被读取
write_sequnlock(&mr_seq_lock); // 写锁与自旋锁类似,差异在于读的时候
unsigned long seq; do{
seq = read_seqbegin(&mr_seq_lock);
//开始读数据。。
}while(read_seqretry(&mr_seq_lock, seq));

禁止抢占

由于内核是抢占性的,内核的进程在任何时候都可以停下来执行更改优先权的进程,这意味着一个任务与被强占的任务可能在同一个临界区内运行。为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标志。如果一个自旋锁被持有,则内核不能进行抢占。

可以通过preempt_disable()来禁止内核抢占。

preempt_disable();
//内核禁止被抢占
preempt_enable();

顺序和屏障

Linux内核设计笔记10——内核同步的更多相关文章

  1. Linux内核设计与实现——内核同步

    内核同步 同步介绍 同步的概念 临界区:也称为临界段,就是訪问和操作共享数据的代码段. 竞争条件: 2个或2个以上线程在临界区里同一时候运行的时候,就构成了竞争条件. 所谓同步.事实上防止在临界区中形 ...

  2. Linux内核设计笔记12——内存管理

    内存管理学习笔记 页 页是内核管理内存的基本单位,内存管理单元(MMU,管理内存并把虚拟地址转化为物理地址的硬件)通常以页为单位进行处理,从虚拟内存的角度看,页就是最小单位. struct page{ ...

  3. linux 驱动学习笔记01--Linux 内核的编译

    由于用的学习材料是<linux设备驱动开发详解(第二版)>,所以linux驱动学习笔记大部分文字描述来自于这本书,学习笔记系列用于自己学习理解的一种查阅和复习方式. #make confi ...

  4. Linux实战教学笔记10:正则表达式

    第十节 正则表达式 标签(空格分隔):Linux实战教学笔记 ---更多资料点我查看 第1章 什么是正则表达式 正则表达式就是为了处理大量的文本|字符串而定义的一套规则和方法 通过定义的这些特殊符号的 ...

  5. Linux内核设计笔记14——块I/O层

    块I/O层 基本概念 系统中可以随机访问固定大小数据片的硬件设备称做块设备,这些固定大小的数据片称之为块.还有一种基本的设备称之为字符设备,其需要按照顺序访问,比如键盘. 扇区:块设备中最小的寻址单元 ...

  6. Linux内核设计笔记11——定时器

    定时器与时间管理笔记 内核中的时间 时钟中断:内核中的系统定时器以某种频率触发中断,该频率可以通过编程预定. 节拍率HZ:时钟中断的频率称为节拍率. 节拍:相邻两次中断的时间间隔称为节拍,1/节拍率. ...

  7. Linux内核设计笔记8——下半部

    # 下半部笔记 1. 软中断 软中断实现 软中断是在编译期间静态分配,其结构如下所示,结构中包含一个接受该结构体指针作为参数的action函数. struct softirq_action{ void ...

  8. Linux内核设计笔记7——中断

    中断与中断处理 何为中断? 一种由设备发向处理器的电信号 中断不与处理器时钟同步,随时可以发生,内核随时可能因为中断到来而被打断. 每一个中断都有唯一一个数字标志,称之为中断线(IRQ) 异常是由软件 ...

  9. Linux内核设计笔记13——虚拟文件系统

    虚拟文件系统 内核在它的底层文件系统系统接口上建立一个抽象层,该抽象层使Linux可以支持各种文件系统,即便他们在功能和行为上存在很大差异. VFS抽象层定义了各个文件系统都支持的基本的.概念上的接口 ...

随机推荐

  1. Oracle查找lobsegment、lobindex对应的表

    在查看表空间的使用情况的时候,发现有几个LOBSEGMENT.LOBINDEX类型的对象占用了大量的空间.于是想找出那些表占用了大量的空间,以便于清理.        Oracle对BLOB类型的定义 ...

  2. js 变速动画函数

    //获取任意一个元素的任意一个属性的当前的值---当前属性的位置值 function getStyle(element, attr) { return window.getComputedStyle ...

  3. SQLMAP注入常见用法

    1.检查注入点 sqlmap -u http://www.com.tw/star_photo.php?artist_id=11 2.列数据库信息当前用户和数据库 sqlmap -u http://ww ...

  4. Shell中的${}、##和%%使用范例

    假设定义了一个变量为,代码如下: file=/dir1/dir2/dir3/my.file.txt 可以用${ }分别替换得到不同的值: ${file#*/}: 删掉第一个 / 及其左边的字符串:di ...

  5. Sass变量及嵌套

    1. 变量:SASS允许使用变量,所有变量以$开头. 变量声明:$highlight-color: #000; 注意:变量可以在css规则块定义之外存在.如下例子: $nav-color: #F90; ...

  6. Spring : JDBC模板, 事务和测试

    JDBCTemplate简单配置:-------------------------------jdbc.properties配置----------------------------------- ...

  7. 帝国cms发布信息时替换正文IMG图片标签里的ALT内容

    帝国cms发布信息时替换正文IMG图片标签里的ALT内容 在 e/class/userfun.php 里面增加 //替换正文IMG里的ALT内容 function user_imgalt($mid,$ ...

  8. openwrt从0开始-目录

    终于下定决心把近期的笔记整理一下.涉及到方方面面,记录自己的成长和沉淀自己所学. 预备知识:linux, 网络通信,待补充... 目录: 前言:openwrt简介 1. openwrt源码下载及编译环 ...

  9. 单片机-C语言-定义和申明

    以下代码是单片机程序,51单片机,编译器为HT-IDE3000, 简单来说 头文件中只能申明, 变量在头文件中申明时,要加上extern 这个关键字用来告诉编译器,变量在其它的文件中定义,为什么要在头 ...

  10. 转载:C语言指针使用的注意事项

    相信大家对指针的用法已经很熟了,这里也不多说些定义性的东西了,只说一下指针使用中的注意事项吧. 一.在定义指针的时候注意连续声明多个指针时容易犯的错误,例如int * a,b;这种声明是声明了一个指向 ...