Chinaunix首页 | 论坛 | 博客
  • 博客访问: 186099
  • 博文数量: 21
  • 博客积分: 218
  • 博客等级: 二等列兵
  • 技术积分: 110
  • 用 户 组: 普通用户
  • 注册时间: 2011-05-03 14:50
个人简介

技术宅

文章存档

2020年(3)

2018年(1)

2017年(2)

2016年(1)

2015年(1)

2013年(1)

2012年(3)

2011年(9)

我的朋友

分类: 嵌入式

2012-06-27 21:49:51

网上找了半天也没找到电力版的电子档,只能用东南版的了。

 

一、           scull 中的缺陷

 

本节举例说明了scull 中的缺陷。

竞争情况是对共享数据的无控制存取的结果。 当错误的存取模式发生了, 产生了不希望的东西。对于这里讨论的竞争情况, 结果是内存泄漏. 这已经足够坏了, 但是竞争情况常常导致系统崩溃和数据损坏。程序员可能会忽视竞争情况而认为竞争情况是相当低可能性的事件。但是, 在计算机世界, 百万分之一的事件会每隔几秒发生, 并且后果会是严重的。

 

二、           并发及其管理

 

硬件资源是共享的, 软件资源也必须常常共享给多个线程。共享是生活的事实.

资源共享的硬规则: 任何时候一个硬件或软件资源被超出一个单个执行线程共享, 并且可能存在一个线程看到那个资源的不一致时, 你必须明确地管理对那个资源的存取。

另一个重要规则:当内核代码创建一个会被内核其他部分共享的对象时, 这个对象必须一直存在(并且功能正常)到它知道没有对它的外部引用存在为止。

除非它处于可以正确工作的状态, 否则对象不能对内核可用, 对这样的对象的引用必须被跟踪。

 

三、           旗标和互斥体

 

当旗标用作互斥 -- 阻止多个进程同时在同一个临界区内运行 -- 它们的值将初始化为 1(也就是只能占用一次)。这样的旗标在任何给定时间只能由一个单个进程或者线程持有。 以这种模式使用的旗标有时称为一个互斥锁。

Linux 旗标实现

为使用旗标, 内核代码必须包含 . 相关的类型是 struct semaphore

 

()、几种方法来声明和初始化:

 

1、直接创建一个旗标

void sema_init(struct semaphore *sem, int val);//val 不一定是10

 

2、旗标以互斥锁的模式使用

 

//声明加初始化,name初始化为 1或者0

DECLARE_MUTEX(name); //1

DECLARE_MUTEX_LOCKED(name);//0

 

3、动态分配

void init_MUTEX(struct semaphore *sem);//1

void init_MUTEX_LOCKED(struct semaphore *sem);//0

 

(二)、使用旗标

 

void down(struct semaphore *sem); //不可中断

int down_interruptible(struct semaphore *sem);// 操作是可中断的,这个可中断的版本几乎一直是你要的那个; 它允许一个在等待一个旗标的用户空间进程被用户中断.

int down_trylock(struct semaphore *sem);// 从不睡眠; 如果旗标在调用时不可用, down_trylock 立刻返回一个非零值.

 

(三)、释放旗标

 

void up(struct semaphore *sem);// 一旦 up 被调用, 调用者就不再拥有旗标

 

scull 中使用旗标

旗标在使用前必须初始化. scull 在加载时进行这个初始化, 在这个循环中:

 

for (i = 0; i < scull_nr_devs; i++) {

scull_devices[i].quantum = scull_quantum;

scull_devices[i].qset = scull_qset;

init_MUTEX(&scull_devices[i].sem);

scull_setup_cdev(&scull_devices[i], i);

}

旗标必须在 scull 设备对系统其他部分可用前初始化。因此, init_MUTEX

scull_setup_cdev 前被调用。 以相反的次序进行这个操作可能产生一个竞争情况, 旗标可能在它准备好之前被存取。

 

读者/写者旗标

Linux 内核为这种情况提供一个特殊的旗标类型称为 rwsem (或者" reader/writer semaphore")。允许多个并发读者常常是可能的, 只要没有人试图做任何改变. 这样做能够显著提高性能; 只读的任务可以并行进行它们的工作而不必等待其他读者退出临界区。

 

使用 rwsem 的代码必须包含 . 读者写者旗标 的相关数据类型是struct rw_semaphore

 

1、在运行时显式初始化

 

void init_rwsem(struct rw_semaphore *sem);

 

2、对需要只读存取的代码的接口是:

void down_read(struct rw_semaphore *sem);

int down_read_trylock(struct rw_semaphore *sem);

void up_read(struct rw_semaphore *sem);

 

3、写者的接口类似:

void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

void up_write(struct rw_semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);

 

rwsem 最好用在很少请求写的时候, 并且写者只占用短时间。

 

四、           Completions 机制

 

completion 是任务使用的一个轻量级机制: 允许一个线程告诉另一个线程工作已经完成。 为使用 completion, 你的代码必须包含

 

1completion 可被静态创建:

DECLARE_COMPLETION(my_completion);

 

2、动态创建和初始化:

struct completion my_completion;

init_completion(&my_completion);

 

4、等待 completion事件:

void wait_for_completion(struct completion *c);// 这个函数进行一个不可打断的等待

5、completion 事件的触发:

 

void complete(struct completion *c);// 只唤醒一个等待的线程

void complete_all(struct completion *c);// 唤醒所有等待的线程

 

如果你使用complete_all,  你必须在重新使用前重新初始化 completion 结构。

 

宏定义:

INIT_COMPLETION(struct completion c);//可用来快速进行这个初始化

 

completion 机制的典型使用是在模块退出时与内核线程的终止一起。

 

内核包含一个特殊的函数给线程使用:

void complete_and_exit(struct completion *c, long retval);

 

五、           自旋锁

 

自旋锁

 

自旋锁是内核提供的另一个用于互斥的工具。

 

旗标

拥有旗标的代码,可以睡眠

自旋锁

自旋锁可用在不能睡眠的代码中,例如中断处理。

通常自旋锁提供了比旗标更高的性能

 

一个自旋锁是一个互斥设备, 只能有 2 个值:"上锁""解锁"

 

这个"测试并置位"操作必须以原子方式进行, 以便只有一个线程能够获得锁。

 

当存在自旋锁时, 等待的处理器在一个忙循环中执行并且不作有用的工作。

 

自旋锁在没有打开抢占的单处理器系统上的操作被优化为什么不做。

 

自旋锁API介绍

 

自旋锁原语要求的包含文件是 。 一个实际的锁有类型 spinlock_t。象任何其他数据结构, 一个自旋锁必须初始化。

 

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;  //编译时初始化

void spin_lock_init(spinlock_t *lock);    //运行时初始化

 

void spin_lock(spinlock_t *lock);         //在进入一个临界区前, 你的代码必须获得需要的 lock,所有的自旋锁等待是, 由于它们的特性, 不可中断的。

void spin_unlock(spinlock_t *lock);       //释放一个你已获得的锁

 

自旋锁和原子上下文

1、应用到自旋锁的核心规则:是任何代码必须在持有自旋锁时, 是原子性的。它不能睡眠; 事实上, 它不能因为任何原因放弃处理器,除了服务中断(并且有时即便此时也不行)

 

2编写会在自旋锁下执行的代码需要注意你调用的每个函数。这些函数不能睡眠。

3在本地 CPU,需要在持有自旋锁时禁止中断。

4关于自旋锁使用的最后一个重要规则是自旋锁必须一直是尽可能短时间的持有。

自旋锁函数

实际上有 4 个函数可以加锁一个自旋锁:

void spin_lock(spinlock_t *lock);//不禁止中断

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);// 禁止中断(只在本地处理器)在获得自旋锁之前; 之前的中断状态保存在 flags .

void spin_lock_irq(spinlock_t *lock);// 你确信你应当在你释放你的自旋锁时打开中断,并且不必保持跟踪 flags

void spin_lock_bh(spinlock_t *lock);// 在获取锁之前禁止软件中断, 但是硬件中断留作打开的.

 

也有 4 个方法来释放一个自旋锁; 你用的那个必须对应你用来获取锁的函数.

void spin_unlock(spinlock_t *lock);

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

void spin_unlock_irq(spinlock_t *lock);

void spin_unlock_bh(spinlock_t *lock);

 

还有一套非阻塞的自旋锁操作:

int spin_trylock(spinlock_t *lock);

int spin_trylock_bh(spinlock_t *lock);

 

 

读者/写者自旋锁

内核提供了一个自旋锁的读者/写者形式, 直接模仿我们在本章前面见到的读者/写者旗标.这些锁允许任何数目的读者同时进入临界区, 但是写者必须是排他的存取. 读者写者锁有一个类型 rwlock_t, 中定义.

 

2 种方式被声明和被初始化:

 

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way 静态*/

 

rwlock_t my_rwlock;

rwlock_init(&my_rwlock); /* Dynamic way 动态*/

 

对于读者, 下列函数是可用的:

void read_lock(rwlock_t *lock);

void read_lock_irqsave(rwlock_t *lock, unsigned long flags);

void read_lock_irq(rwlock_t *lock);

void read_lock_bh(rwlock_t *lock);

void read_unlock(rwlock_t *lock);

void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

void read_unlock_irq(rwlock_t *lock);

void read_unlock_bh(rwlock_t *lock);

 

对于写存取的函数是类似的:

void write_lock(rwlock_t *lock);

void write_lock_irqsave(rwlock_t *lock, unsigned long flags);

void write_lock_irq(rwlock_t *lock);

void write_lock_bh(rwlock_t *lock);

int write_trylock(rwlock_t *lock);

void write_unlock(rwlock_t *lock);

void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

void write_unlock_irq(rwlock_t *lock);

void write_unlock_bh(rwlock_t *lock);

 

没有 read_trylock 函数。

 

六、           锁陷阱

不明确的规则

当你创建一个可以被并发存取的资源时, 你应当同时定义哪个锁将控制它的存取.

 

不论旗标还是自旋锁都不允许一个持锁者第 2 次请求锁; 如果你试图这样做, 系统将挂起。

 

sucll 的例子里,所作的设计决策:由系统用直接调用的那些函数均要获得信号量,以便保护要访问的设备结构。而其它的内部函数只会由其他的scull函数调用,则假定信号量已经被正确获得。

 

锁的顺序规则

在有大量锁的系统中(并且内核在成为这样一个系统), 一次需要持有多于一个锁, 对代码是不寻常的。

 

当多个锁必须获得时, 它们应当一直以同样顺序获得。

 

有帮助的两个规则:

1、如果你必须获得一个对你的代码来说的局部锁(假如, 一个设备锁), 以及一个属于内核更中心部分的锁, 先获取局部锁。

2、如果你有一个旗标和自旋锁的组合, 你必须先获得旗标; 调用 down (可能睡眠) 在持有一个自旋锁时是一个严重的错误。

 

-- 粒度加锁

作为一个通用的规则, 你应当从相对粗的加锁开始, 除非你有确实的理由相信竞争可能是一个问题. 我们需要抑制自己过早地优化; 真实地性能约束常常表现在想不到的地方。

 

七、           除了锁之外的办法

 

免锁算法

常常可以对无锁的生产者/消费者任务有用的数据结构是环形缓存. 这个算法包含一个生产者安放数据到一个数组的尾端, 而消费者从另一端移走数据. 当到达数组末端, 生产者绕回到开始. 因此一个环形缓存需要一个数组和 2 个索引值来跟踪下一个新值放到哪里,以及哪个值在下一次应当从缓存中移走.

 

对于 2.6.10, 有一个通用的环形缓存实现在内核中可用; 如何使用它的信息看 .

 

原子变量

 

但是一个完整的加锁体制对于一个简单的整数值看来有些浪费. 对于这样的情况, 内核提供了一个原子整数类型称为 atomic_t, 定义在 .

 

原子操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令.

 

void atomic_set(atomic_t *v, int i);

atomic_t v = ATOMIC_INIT(0);

//设置原子变量 v 为整数值 i. 你也可在编译时使用宏定义 ATOMIC_INIT 初始化原子值.

int atomic_read(atomic_t *v);

//返回 v 的当前值.

void atomic_add(int i, atomic_t *v);

//由 v 指向的原子变量加 i. 返回值是 void, 因为有一个额外的开销来返回新值,并且大部分时间不需要知道它.

void atomic_sub(int i, atomic_t *v);

//从 *v 减去 i.

void atomic_inc(atomic_t *v);

void atomic_dec(atomic_t *v);

//递增或递减一个原子变量.

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);

//进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.

int atomic_add_negative(int i, atomic_t *v);

//加整数变量 i v. 如果结果是负值返回值是真, 否则为假.

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);

 

位操作

atomic_t 类型的原子变量在进行整数算术时是不错的

内核提供了一套位操作函数来原子地修改或测试单个位. 因为整个操作在单步内发生, 没有中断(或者其他处理器)能干扰.

 

原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的时候不用禁止中断. 函数是体系依赖的并且在 中声明。

 

不幸的是,这些函数中的数据也是体系依赖的。

 

各种位操作是:

void set_bit(nr, void *addr);

//设置第 nr 位在 addr 指向的数据项中.

void clear_bit(nr, void *addr);

//清除指定位在 addr 处的无符号长型数据. 它的语义与 set_bit 的相反.

void change_bit(nr, void *addr);

//翻转这个位.

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);

 

但是, 最好在新代码中使用自旋锁;自旋锁很好地调试过, 它们能够处理类似中断和内核抢占这样的问题, 并且别人读你代码时不必努力理解你在做什么.

 

seqlock

2.6 内核包含了一对新机制打算来提供快速地, 无锁地存取一个共享资源。

seqlock 在这种情况下工作, 要保护的资源小, 简单, 并且常常被存取, 并且很少写存取而且必须要快。

 

seqlock 通常不能用在保护包含指针的数据结构。

 

seqlock 定义在 . 2 个通常的方法来初始化一个 seqlock( seqlock_t 类型 ):

 

seqlock_t lock1 = SEQLOCK_UNLOCKED;

seqlock_t lock2;

seqlock_init(&lock2);

 

读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作. 在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试.

读者代码象下面的形式:

unsigned int seq;

do {

seq = read_seqbegin(&the_lock);

} while read_seqretry(&the_lock, seq);

 

这个类型的锁常常用在保护某种简单计算, 需要多个一致的值. 如果这个计算最后的测试表明发生了一个并发的写, 结果被简单地丢弃并且重新计算。

 

 

seqlock 可能从一个中断处理里存取:

unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);

int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long

flags);

 

写者必须获取一个排他锁来调用:

void write_seqlock(seqlock_t *lock);

 

释放锁:

void write_sequnlock(seqlock_t *lock);

 

通常的变体:

void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);

void write_seqlock_irq(seqlock_t *lock);

void write_seqlock_bh(seqlock_t *lock);

void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);

void write_sequnlock_irq(seqlock_t *lock);

void write_sequnlock_bh(seqlock_t *lock);

还有一个 write_tryseqlock 在它能够获得锁时返回非零.

 

 

读取-拷贝-更新

它在驱动中的使用很少。

略。。。。。。

 

 

 

阅读(2589) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~