这时处理器能重获自有,去执行其他代码。
当持有的信号量可用(被释放)后,处于等待队列中的进程会被唤醒,并获得该信号量。
1. 内核的信号量定义如下,与用户空间的信号量实现有所不同:
struct semaphore {
atomic_t count; //指可以同时处于信号量保护的临界区中的进程的数目, 大多数情况 count = 1
int sleepers; //指定了等待(允许进入临界区的进程的)数目,不同于自旋锁,等待的进程会进入睡眠,直到信号量释放,才会被唤醒。这意味着CPU可以在等待期间执行其他任务。
wait_queue_head_t wait; //实现一个队列,保存所有在该信号量上睡眠的进程的task_struct.
};
信号量与自旋锁的比较:
a. 与自旋锁相比,信号量适合用于保护更长的临界区,以防止并行。
b. 在进程上下文切换处,只能用信号量,这时不能用自旋锁。而中断上下文中不能使用信号量,因为中断上下文中是不能进行调度的,这时只能用自旋锁。
c. 信号量不适合用于断保护短的代码范围,因为使用信号量时需要进入睡眠和再次唤醒,代价很高。
d. 信号量会导致进程睡眠,而使用自旋锁的进程,会在临界区外面一直自旋。
e. 信号量比自旋锁提供了更好的CPU利用率,因为没把时间花费在忙等待上,但是信号量比自旋锁的开销更大, 多了睡眠,维护等待队列以及唤醒等开销。
f. 在占用信号量的同时,不能再去占用自旋锁,因为在等待信号量时可能会睡眠,而持有自旋锁时,是不允许睡眠的。
g. 在与用户空间交换数据时,因为代码需要睡眠,所以此时信号量是唯一的选择。
h. 信号量不会禁止内核抢占,所有支持信号量的代码都可以被抢占,这点不同于自旋锁。
2. 互斥量,其实就是一个二值信号量。
DECLARE_MUTEX(mutex)
...
down(&mutex)
/* 临界区 */
up(&mutex)
进入临界区时,用down对使用计数器减1。在计数器=0时,其他进程不能进入临界区。
它也会遇到2种情况:
A. 该信号量已经被分配时,在用down去获取时,当前进程进入睡眠,并添加到与该信号量相关联的等待队列上。同时,该进程被置于TASK_UNINTERRUPTIBLE状态,在等待进入临界区的过程中, 无法接受信号。
B. 如果该信号量没有被分配,则该进程可以立即获得信号量并进入到临界区,而不会进入睡眠。
在退出临界区时,必须调用up。负责唤醒在信号量上睡眠的某个进程,然后该进程进入临界区,其他等待的进程继续睡眠。
3. 信号量&互斥体的初始化:
// initialize mutex object
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 37) //判断linux版本,
init_MUTEX(&i2c.wr_sem); //linux版本低于2.6.37, 则使用init_MUTEX()初始化。
#else
sema_init(&i2c.wr_sem,1); //linux版本高于2.6.37,则使用sema_init()初始化信号量。
#endif
4.信号量 ---semaphore
与自旋锁相同,只有得到信号量的进程才能执行临界区的代码。
与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入睡眠等待状态。
信号量允许任意数量的进程同时持有某一信号量, 这个数量由 count 指定,
当count > 1时, 它允许在一个时刻,至多有count个信号量持有者,它允许这些线程同时访问临界区。但是这种信号量对代码限制较多,内核中使用它的机会不多。
count = 1时,就是互斥体 Mutex.,使用信号量时基本都是使用互斥体。
struct semaphore sem; //定义信号量
void sema_init(struct semaphore *sem, int val); //初始化信号量,并设置信号量sem的值为val
void init_MUTEX(struct semaphore *sem); //初始化一个用于互斥的信号量,它把信号量sem的值置为1。等同于
sema_init(struct semaphore *sem, 1);
void init_MUTEX_LOCKED(struct semaphore *sem); //也是初始化一个信号量,但它把信号两sem的值设为0,等同于
sema_init(struct semaphore *sem, 0);
下面两个宏是定义并初始化互斥信号量的“快捷方式”
DECLARE_MUTEX(name)
DECLARE_MUTEX_LOCKED(name)
前者定义一个名为name的信号量并初始化为1,后者初始化为0
4.2 获得信号量:
void down(struct semaphore *sem); //获得信号两sem,但会导致睡眠,所以不能在中断上下文中使用。
int down_interruptible(struct semaphore *sem);
因down()而进入睡眠状态的进程不能被信号打断,而因down_interruptible()进入睡眠状态的进程能被信号打断。信号也会导致该函数返回,这时函数的返回值是0.
down_interruptible() 使用比较普遍。他会把进程置成 TASK_INTERRUPTIBLE 状态,然后睡眠。这种状态意味着进程可被信号唤醒。
int down_trylock(struct semaphore *sem);
尝试获得信号量sem,如果能获得,就获得该信号量返回0,否则返回非0值。
释放信号量
void up(struct semaphore *sem);
4.3 使用案例:
static DECLARE_MUTEX(mr_sem); //定义并声明一个互斥信号量
if (down_interruptible(&mr_sem)) {
//信号被接收,信号量还未获取
}
//获取信号量,进入临界区
up(&mr_sem); //释放互斥信号量。
5 完成量用于同步 ---completion
类似:信号量用于同步,如果信号量被初始化为0,则可以用于同步。即一个执行单元的继续执行,需要等待另一执行单元完成某事。
完成量仅仅是提供了替代信号量的一个简单的解决办法。
struct completion my_completion; //定义
init_completion(&my_completion); //动态创建并初始化
或
DECLARE_COMPLETION(my_completion);
void wait_for_completion(struct completion *c); //用于等待一个completion被唤醒
void complete(struct completion *c); //只唤醒一个等待的执行单元
void complete_all(struct completion *c); //释放所有等待同一完成量的执行单元
完成量的用法:
a. 动态创建完成量,并初始化
b. 调用wait_for_completion() 进行等待。
c. 初始化完成后,调用 completion() 唤醒在等待的内核进行。
6. 读写信号量 ---rw_semaphore
允许多个读执行单元同时访问共享资源。而最多只能有一个写执行单元。
所有的读-写信号量都是互斥信号量, 也就是引用计数 count = 1.
static DECLARE_RWSEM(name); //静态创建,定义并声明一个新信号量name
struct rw_semaphore my_rws; //定义读写信号量
void init_rwsem(struct rw_semaphore *sem); //初始化读写信号量
读写信号量 只对写者互斥,在没有写者锁的情况下,允许并发持有读锁的读者数不限。
同一时刻,在没有读者获得读锁的情况下,只允许有唯一的写者可以获得写锁。
所有的读-写信号量的睡眠都不会被信号打断,所以他只有一个down(), 没有down_interruptible().
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem); //如果成功获得信号量,则返回非0值;如果信号量被别进程使用,则返回0。注意:这里与普通信号量相反。
void up_read(struct rw_semaphore *sem);
获取/释放写信号量
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)
downgrade_write() //这个函数可以动态的将获取的写锁 转换为读锁。
读-写信号量与读-写自旋锁一样,使用时,代码中的读和写必须明白无误的分割开来,否则最好别用。
mutex 其实就是一个二值信号量,也会使进程睡眠,所以不能在中断上下文中使用。
7.1 定义并初始化:
struct mutex my_mutex; //定义
mutex_init(&my_mutex); //初始化
或者
DEFINE_MUTEX(my_mutex); //定义并初始化互斥体 my_mutex
7.2 获取/释放互斥体:
void fastcall mutex_lock(struct mutex *lock); //睡眠不能被信号打断
int fastcall mutex_lock_interruptible(struct mutex *lock); //睡眠能被信号打断
int fastcall mutex_trylock(struct mutex *lock); //试图获得指定的mutex,如果成功返回1,否则失败返回0.
void fastcall mutex_unlock(struct mutex *lock);
mutex_is_locked(struct mutex *lock); //如果锁已被征用,则返回1,否则返回0
7.3 mutex的使用场景:
a. 给mutex上锁的进程,必须负责给他解锁,(不能在一个进程上下文中锁定一个mutex,而在另一个进程上下文中给它解锁)。
所以mutex不适合内核与用户空间复杂的同步场景, 最常用的使用方式是:在同一进程上下文中上锁和解锁。
b. 递归的上锁和解锁是不允许的,也就是不能递归的持有同一个锁。
c. 当持有一个mutex时,进城不可以退出。
d. mutex 不能再中断或者下半部中使用,即时mutex_trylock() 也不行,因为会睡眠。
7.4 互斥体与自旋锁的使用场合:
低开销加锁 --- 自旋锁
短期锁定 --- 自旋锁
长期加锁 --- 互斥体
中断上下文加锁 --- 自旋锁
持有锁需要睡眠 --- 互斥体