并发是指多个执行单元同时并行被执行,而并发的执行单元对共享资源(例如硬件资源、和软件上的全局变量,静态变量等)的访问很容易导致竞态。
例如:有一块公共区域,有执行单元A和执行单元B共同访问往这块区域里面写数据,执行单元A写3000个A,执行单元B写3000个B,执行单元C来访问读取这块区域中的数据。若执行方式是ABC,那么C最终会得到3000个B;若执行方式是ACBC,那么最终的读取数据则会是AB不同类型。
是指多个处理器共同使用系统总线,因此可以访问共同的外设和寄存器
例如在两个处理器CPU0和CPU1之间,有着多个进程和中断。竞态可能会产生在不同CPU下的进程与进程之间、进程与中断之间。
由于内核版本2.6之后,在单核处理器下支持进程调度的抢占,一个进程可能会消耗完自己的时间片,也可能由于另一个高优先级的中断而打断当前进程,进程与抢占他的进程之间也存在着类似于SMP的多个CPU。
中断就包括(硬中断、软中断、Tasklet、底半部)
中断会打断当前正在执行的进程,如果中断访问被打断的进程所访问的数据时,也会产生竞态。
此外中断也会被其他高优先级的中断所中断,因此多个中断也有可能产生竞态的关系。
老版本的中断可以产生中断嵌套,及中断中继续产生中断,所以通常用标识符 IRQF_DISABLED
来避免
新版则不再提供中断嵌套的操作,从根源上直接避免。
除了上述的SMP是真正的并行之外,其余单核处理器和中断都属于“宏观并行,微观串行”
从竞态的产生来说,解决竞态问题就是解决资源被共同访问的问题,也就是保证共享资源被互斥访问。
所谓互斥访问就是在当前执行单元下访问资源的时候,其他执行单元被禁止访问。
理解锁的机制还需要从编译乱序和执行乱序上面理解
例如有下面一段代码
struct test{
int a;
int b;
int c;
};
struct test *p = NULL;
tp = kmalloc(sizeof(* tp), GFP_KERNEL);
tp->a = 1;
tp->b = 2;
tp->c = 3;
p = tp;
//如果读端要进行处理可能是
p = tp;
if(p != NULL){
do_something(p->a, p->b, p->c);
}
这一段代码如果被编译乱序,可能会出错为:
在p = tp之后才会执行tp的赋值操作,所以导致访问不到数据,这种错误在编译器的优化之下已经很少出现。
但是在高度优化的编译器下也有可能不是顺序编译,目的是为了减少不必要的访存,所以看到的汇编码并不是顺序编译的也很正常。
解决编译乱序的问题,我们也可以使用 barrier()
屏障,这个屏障可以阻挡编译器的优化,保证编译器不“乱串门”
int main()
{
int a, b, c[120], d, e=1;
d = c[120];
a = e;
b = e;
}
/*在编译器编译的情况下,有可能出现a,b的赋值在d的赋值之前*/
/*加上barrier()之后可以顺序编译了*/
int main()
{
int a, b, c[120], d, e=1;
d = c[120];
barrier();
a = e;
b = e;
}
也可以使用 volatile
关键字,但它的效果甚微,主要的目的是防止编译器优化
若编译器同时访问某个内存两次,发现这个内存中的数据并没有发生改变时,在第二次访问时就会直接使用上一次访问过后的值,是个“懒汉子”。所以添加volatile之后,告知编译器这个数据是易发生改变的,每一次存取时都直接在内存中访问,而不是cash中。
编译乱序是编译器的行为,而执行乱序是处理器的行为。
在处理器执行程序时,虽然编译是按照顺序编译,但执行时任然会将先调用连续内存的行为提前执行,因为速度快,缓存命中率高。如果前面的缓存很慢导致堵塞,后面资源可以先进行缓存,所以以此来看执行的顺序也是未知的。
例如在CPU上执行以下程序,且编译是顺序编译:
//在CPU0上执行
while(fi == 0);
print x;
//在CPU1上执行
x = 2;
fi = 1;
我们不能果断的认为打印出来的x = 2,有可能fi = 1会在x的赋值之前,所以x也有可能会为任意值。
所以这是由于一个核的内存行为对另外一个核作用引起的,Linux内核为了防止此操作,添加了一些内存屏障的指令
在ARM下的指令为
DMB
数据内存屏障
DSB
数据同步屏障
ISB
指令同步屏障
对于Linux内核的自旋锁,互斥体等互斥逻辑都会用到上述的命令:在调用锁时会使用上述指令,解锁时也会用到。
前面提到,单个CPU的执行顺序都是乱序执行,但是单个CPU也会遇到依赖点的时候会等待。
访问外设寄存器,对于寄存器的访问构不成依赖关系,各个赋值自己的。
从外设的逻辑角度而言,比如读写顺序,也需要使用CPU的屏障指令。
例如
写数据的API:
writeb()和writeb_relaxed()从I/O空间写入8位数据(1字节)
writew()和writew_relaxed()从I/O空间写入16位数据(2字节)
writel()和writel_relaxed()从I/O空间写入32位数据(4字节)
其中的区别就在于是否有__iowmb() 屏障
读数据:
readb()和readb_relaxed()从I/O空间读取8位数据(1字节)
readw()和readb_relaxew()从I/O空间读取16位数据(2字节)
readl()和readb_relaxel()从I/O空间读取32位数据(4字节)
其中的区别就在于是否有__iormb() 屏障
都是读写数据的区别就在于是否有屏障:
mb()读写屏障
rmb()读屏障
wmb()写屏障
作用于寄存器读写的 __iormb()
__iowmb()
屏障
当我们通过writel_relaxed()写完DMA的数据时,一定要用writel()来启动DMA,保证写数据不会被插队
writel_relaxed(DMA_SRC_REG, src_addr);
writel_relaxed(DMA_DST_REG, dst_addr);
wrritel_relaxed(DMA_SIZE_REG, size);
writel(DMA_ENABLE, 1);
作用域:单CPU
作用方式:在执行单元访问临界资源时屏蔽系统中的中断,所以中断屏蔽方式会使得进程与中断的并发执行不再发生
使用方法:
local_irq_disable() //屏蔽中断
。。。
critical_section //临界区
。。。
local_irq_enable() //开中断
对于ARM处理器而言,底层的中断屏蔽就是屏蔽ARM CPSR的1位;
缺点:
与local_irq_disabled不同的是, local_irq_save(flags)
除了进行禁止中断的操作之外,还会保存当前的中断位信息, local_irq_restore(flags)
则会允许中断时会恢复中断位信息,也就是在ARM下的CPSR的保存与恢复。
对于只想禁止中断的底半部,应该使用 local_bh_disable()
,使能时使用 local_bh_enable()
原子操作是指整个操作指向就像原子一样不能分割,若指向则执行到底,不会发生执行第一个命令之后中断的操作。
Linux中使用两种函数来实现原子操作:针对整型变量的原子操作,针对位的原子操作
对于ARM来说,原子操作的底层使用 LDREX
和 STREX
指令,和对于内存数据访问屏障的指令类似
对于让总线监控LDREX和STREX指令,可以让其之间没有其他实体存取该地址
问题:对于如下的线程,都是修改共享内存的数据,那么哪一个能够修改成功呢?
先看流程:
所以总结,最先执行修改内存指令STREX的操作,会最先完成。
void atomic_set(atomic_t *v, int i);//设置原子变量的值为i int count = i;
atomic_t v = ATOMIC_INIT(0);//定义原子变量v,并初始化为0 i = 0;
typedef struct {
int counter;
} atomic_t;
int atomic_read(atomic_t *v);//返回原子变量的值
void atomic_add(int i, atomic_v *v);//原子变量增加i
void atomic_sub(int i, atomic_t *v);//原子变量减少i
void atomic_inc(atomic_t *v);//原子变量自增1
void atomic_dec(atomic_t *v);//原子变量自减1
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
int atomic_add_and_test(int i, atomic_t *v);
//返回值会进行test测试,若为0则返回true,否则返回flase
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
//返回新原子变量的值
void set_bit(nr, void addr);
//操作地址addr的第nr位,设置成1
void clear_bit(nr, void addr);
//操作地址addr的第nr位,设置成0
void change_bit(nr, void addr);
//操作地址addr的第nr位,是1设置成0,是0设置成1
test_bit(nr, void addr);
//返回地址addr的第nr位
int test_and_set_bit(nr, void addr);
int test_and_clear_bit(nr, void addr);
int test_and_change_bit(nr, void addr);
使用原子变量使设备只能被一个进程打开
static atomic_t v = ATOMIC_INIT(1); //表示有这一个设备
static int globalmem_open(struct inode *inode, int index)
{
//原子变量为1则可以打开,原子变量为0则无法打开
if(!atomic_dec_test(&v))
{
atomic_inc(&v);
return -EBUSY;
}
private_data = dev_p;
return 0;
}
static int globalmem_release(struct inode *inode, struct file *filp)
{
atomic_inc(&v); //释放设备
return 0;
}
自旋(spin)锁(lock)是对临界资源访问的一种互斥机制,为了获得一个自旋锁:
自旋锁在ARM底层也使用了LDREX和STREX
自旋锁的相关操作:
spinlock_t lock;
spin_lock_init(&lock);
//该宏用于动态初始化自旋锁
spin_lock(&lock);
//该宏用于获得自旋锁,获得之后返回,否则将在那一直自旋,直到锁被释放
spin_trylock(&lock);
//该宏用于尝试获得自旋锁,如果没有获得则立刻返回flase,获得返回true
spin_unlock(&lock);
//与spin_lock spin_trylock配合使用
自旋锁作用域:
SMP对称对处理器、单CPU但内核可被抢占
在SMP下假如CPU1使用自旋锁意味着在CPU1上的抢占调度就被禁止了,但是本核的中断和底半部仍然可以抢占资源,并且其余CPU2,3等还是拥有抢占调度。
为了防止本核的中断和底半部仍然可以抢占资源,就诞生了自旋锁的衍生:
自旋锁+中断屏蔽
自旋锁+BH
spin_lock_irq() = spin_lock() + local_irq_disable();
spin_unlock_irq() = spin_unlock() + local_irq_enable();
spin_lock_irqsave() = spin_lock() + local_irq_save();
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore();
spin_lock_bh() = spin_lock() + local_bh_disabled();
spin_unlock_bh() = spin_unlock() + local_bh_enabled();
如果同一CPU下的中断和进程访问同一片共享资源,在进程中就要调用 spin_lock_irqsave()
spin_unlock_irqrestore()
,在中断中就要调用 spin_lock()
spin_unlock()
此后在同一CPU下就要争夺同一把锁
利用自选锁,使得设备只能被一个进程打开
int dev_count = 0;//定义设备被打开的次数
spinlock_t lock;
static int globalmem_open(struct inode *inode, int index)
{
spin_lock_irqsave(&lock);
if(!dev_count)
{
spin_unlock_irqrestore(&lock);
return -EBUSY;
}
dev_count++;
dev_p = private_data;
spin_unlock_irqrestore(&lock);
return 0;
}
static int globalmem_release(struct inode *inode, struct file *filp)
{
spin_lock_irqsave(&lock);
dev_count--;
spin_unlock_irqrestore(&lock);
return 0;
}
读写自旋锁方便的是读的操作,因为普通的自旋锁不关心是读还是写,一视同仁某一时刻只能允许一个执行单元对共享资源进行操作,而读写锁可以允许读的并发操作,但对写只能保持一个执行单元操作共享资源,当然读和写不能同时发生。
rwlock_t lock;
rwlock_init(&lock);//动态初始化
read_lock(rwlock_t *lock);
read_lock_irq(rwlock_t *lock);
read_lock_bh(rwlock_t *lock);
read_lock_irqsave(rwlock_t *lock, unsigned long flags);
read_unlock(rwlock_t *lock);
read_unlock_irq(rwlock_t *lock);
read_unlock_bh(rwlock_t *lock);
read_unlock_irqrestore(rwlock_t *lock, unsigned long falgs);
write_lock(rwlock_t *lock);
write_trylock(rwlock_t *lock);
write_lock_irq(rwlock_t *lock);
write_lock_irqsave(rwlock_t *lock, unsigned long flags);
write_lock_bh(rwlock_t *lock);
write_unlock(rwlock_t *lock);
write_unlock_irq(rwlock_t *lock);
write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
write_unlock_bh(rwlock_t *lock);
//定义读写锁
rwlock_t lock;
//读写锁初始化
rwlock_init(&lock);
//读锁定
read_lock(&lock);
/*临界资源*/
//读解锁
read_unlock(&lock);
//写锁定
write_lock_irqsave(&lock, flgas);
//写解锁
write_unlock_irqrestore(&lock, flags);
使用顺序锁时,读操作不会被写操作所阻塞,也就是读操作在读取共享资源时不必等待写操作执行完成,写操作也不会等待所有的读操作执行完成时才去写,但是写执行单元之间也是互斥的。
在进行读操作时,若此时写操作刚写完数据,读操作必须重新读取数据,所以在这种情况下读端可能要多次进行读操作确保数据的准确性
void write_seqlock(seqlock_t *sl);
int write_tryselock(seqlock_t *sl);
write_seqlock_irq(seqlock_t *sl);
write_seqlock_irqsave(seqlock_t *sl, unsigned long flags);
write_seqlock_bh(seqlock_t *sl);
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irq(seqlock_t *sl);
write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags);
write_sequnlock_bh(seqlock_t *sl);
对于写操作来说,顺序锁和自旋锁相同
读操作执行顺序锁如下:
//在进行读操作开始时,会对由sl保护的临界资源时会调用该函数,该函数返回顺序锁sl的当前顺序号
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags);
该操作主要在于读完成之后会检测是否在读的期间有写操作的存在,如果存在则调用
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags);
do
{
/* code */
seqnum = read_seqbegin(&sl);//返回的是顺序号
/*执行读操作*/
} while (read_seqretry(&sl, seqnum));
RCU(read-copy-update)
不同于自旋锁,对于RCU来说读操作没有锁,几乎可以认为是直接读
RCU在执行写操作时,对于共享资源会创建一个副本,然后在这个副本上进行写操作,最后使用回调机制等待一个恰当的时间再将指向原来数据的指针指向新的被修改的数据。这个恰当的时间就是,等所有引用该数据的CPU都退出访问时,再去修改。
例如:
假设有一个链表,当然其中有数据,如果要修改其中的数据,自旋锁的思路就是排他性的去修改数据,而RCU的思路是,直接创建一个新的节点,然后原来的节点的内容复制到新节点上,再在新节点上修改数据,最后用新的节点去替代原来的节点,最后等待所有的CPU读完数据之后再去释放原来的节点。
RCU的优点在于既允许了多个读执行单元同时访问数据,又允许了多个写执行单元和多个读执行单元访问数据。
rcu_read_lock();
rcu_read_lock_bh();
rcu_read_unlock();
rcu_read_unlock_bh();
synchronize_rcu();
//该函数由写操作执行,会自动检测读操作是否全部都完成,才能方便写执行
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu));
//该函数与同步rcu的synchronize_rcu()作用相似
//但是使用call_rcu可以使得写端不用等待读端操作完成之后再去释放以前的节点(写端不阻塞)
rcu_assign_pointer(p, v);
//写端指定一个节点,使得这个节点被rcu所保护,在更新节点内容时,确保旧节点的内容不会被访问
rcu_deference(p);
//读端使用这个被rcu所保护的节点,可以安全的读取被rcu保护的节点,在读时相关的数据节点不会被释放
信号量是经典的同步互斥操作,信号量的值可以是1,0也可以是n
可以用经典的PV操作对应:
P(S) :
将信号量的值减一 S=S-1;
若S≥0,则该进程继续;否则进程进入等待状态,排入等待队列
V(S) :
将信号量的值加一 S=S+1;
若S>0,则唤醒队列中等待信号量的进程
struct semaphore sem;
void sema_init(struct semaphore *sem, int val);
//初始化信号量,并将信号量的值赋为val
void down(struct semaphore *sem);
//获得信号量,但是会导致睡眠,因此不能在中断上下文中使用
void down_interruptible(struct semaphore *sem);
//该函数功能与down函数类似,但是在进入睡眠状态后可以被信号所打断
//获取到信号也会导致该函数返回,这时候返回值为0
//并且可以对返回值进行检查,若返回的非0值,则立即返回-ERESTARTSYS
例如:
if(down_interruptible(&sem)){
return -ERESTARTSYS;
}
int down_trylock(struct semphore *sem);
//该函数在尝试获得信号量sem时,若立刻获得则返回0,否则返回非0值,不会导致睡眠,可在中断中使用
void up(struct semaphore *sem);
//释放sem信号,唤醒等待者
与自旋锁一样可以对临界区进行保护,但是与自旋锁不同的是,自旋锁在获取锁的时候会进行等待,而信号量是进入睡眠状态
由于新Linux内核更倾向于mutex作为互斥手段,所以信号量不做推荐。而同步机制使用信号量的情况较多。
下面代码定义了互斥体并初始化和使用
//定义互斥体并初始化
struct mutex my_mutex;
mutex_init(&my_mutex);
//下面的函数用于获取互斥体
void mutex_lock(struct mutex *lock);
//获取不到互斥体时睡眠且不会被唤醒
int mutex_lock_interruptible(struct mutex *lock);
//获取不到互斥体时睡眠且能够被唤醒
int mutex_trylock(struct mutex *lock);
//获取不到互斥体时立刻返回
//释放互斥体
void mutex_unlock(struct mutex *lock);
互斥体和自旋锁都用作线程的同步机制里,但是不同的是:
Completion,用于一个执行单元等待执行另一个单元执行完某事
//定义完成量
struct completion my_completion;
//初始化完成量
init_completion(&my_completion);
reinit_completion(&my_completion);
//初始化或者重新初始化完成量的值为0,即没有完成的状态
//等待一个完成量被唤醒
void wait_for_completion(struct completion *c);
//唤醒完成量
void complete(struct completion *c);
//只唤醒一个等待被唤醒的执行单元
void completion_all(struct completion *c);
//释放所有等待同一完成量的执行单元
设备结构定义
模块加载函数
读/写函数
io操作函数
struct globalmem {
unsigned int size[MAX_PART];
struct cdev cdev;
struct mutex my_mutex;
};
struct globalmem *dev_p = NULL;
static int __init globalmem_init(void)
{
int ret;
//获取设备号
dev_t devno = MKDEV(globalmem_major, 0);
//注册设备
if(globalmem_major){
ret = register_chrdev_region(devno, 1, "globalmem");
}
else{
ret = alloc_chrdev_region(devno, 1, "globalmem");
}
if(ret < 0)
return ret;
//初始化锁
mutex_init(&my_mutex);
//设置设备
dev_p = kzalloc(sizeof(*dev_p), GFP_KERNEL);
if(dev_p == NULL){
goto failed;
ret = -ENOMEM;
}
globalmem_setup_cdev(dev_p, 0);
failed:
unsigned_chrdev_region(devno, 1);
return ret;
}
module_init(globalmem_init);
static int globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
struct globalmem *dev_t = filp->private->data;
//如果文件指针偏移出文件大小
if(p >= MAX_PART){
return 0;
}
//如果文件读取的内容小于规定的大小
if(count + p > MAX_PART){
count = MAX_PART - p
}
//加锁
mutex_lock(&dev_t->my_mutex);
//读取数据
ret = copy_to_user(buf, dev->mem+p, count);
if(ret < 0)
return -EFAULT;
else{
*ppos += count;
ret = count;
}
//解锁
mutex_unlock(&dev_t->my_mutex);
return ret;
}
static int globalmem_write(struct file *filp, char __usr *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
struct globalmem *dev_t = filp->private_data;
//文件指针超出文件大小
if(p >= MAX_PART){
return 0;
}
//写入的数目大于文件大小
if(p + count > MAX_PART){
count = MAX_PART - p;
}
//加锁
mutex_lock(&dev_t->my_mutex);
//写数据
ret = copy_from_user(dev_t->mem + p, buf, count);
if(ret < 0){
return -EFAULT;
}
else{
*ppos += count;
ret = count;
}
//解锁
mutex_unlock(&dev_t->my_mutex);
return ret;
}
static int globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
struct globalmem dev_t = filp->private_data:
switch(cmd){
case MEM_CLEAR:
mutex_lock(&dev_t->my_mutex);
memset(dev_t->mem, 0, MAX_PART);
mutex_unlock(&dev_t->my_mutex);
printk("the mem is to set 0\n");
break;
default:
break;
}
return 0;
}
如果你的驱动要控制多个设备,可能在驱动中就要跟踪某个设置,这时候就需要将设备放入链表中,方便管理。实际上链表分为两种:单项链表和双向链表,目前内核只实现了双向链表。
代码中添加的头文件是: #include
其中链表的核心部分数据结构是: struct list_head
struct list_head{
struct list_head *head, *prev;
};
list_head用在每个链表头和节点当中,也就是存在于所定义的设备结构体中
例如:
//我们来创建汽车链表
struct car_list{
int door_number;
char *color;
char *model;
//嵌入链表
struct list_head list;
};
struct car_list carlist;
//创建list头变量,改变量总是指向链表的头节点
static LIST_HEAD(carlist);
//内核中封装的头节点初始化
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
//创建汽车并添加到链表carlist中
struct carlist *redcar = kmalloc(sizeof(*carlist), GFP_KERNEL);
struct carlist *bulecar = kmalloc(sizeof(^carlist), GFP_KERNEL);
/*初始化每个节点的列表条目*/
INIT_LIST_HEAD(&bulecar->list);
INIT_LIST_HEAD(&redcar->list);
//添加链表
list_add(&redcar->list, &carlist);
list_add(&bulecar->list, &carlist);
//现在。链表中就包含了两个元素redcar和bluecar
总结起来就是:
struct list_head carlist_head;
INIT_LIST_HEAD(&carlist_head);
//变量的类型和函数名都不同
static LIST_HEAD(carlist_head);
void list_add(struct list_head *new, struct list_head *head)
一般是在元素节点的中间插入void list_add_tail(struct list_head *new, struct list_head *head)
是在链表的尾部插入void list_del(struct list_head *entry);
eg:
list_del(&redcar->list);
kfree(read->list)
使用宏 list_for_each_entry(pos, head, member)
或者使用 list_for_each_entry_safe(pos, head, member)
进行链表遍历
pos:用于迭代,是个循环游标
head:链表的头节点
member:数据结构(本例子中的是struct list_head的名称)
//list是数据结构中list_head定义的变量名称
list_for_each_entry(carnum, carlist, list)
{
if(acar->color == "blue"){
blue_car_num++;//返回蓝色车的数量
}
}