Linux设备驱动程序学习笔记08:自旋锁和信号量

来源:互联网 发布:从0到简单小游戏java 编辑:程序博客网 时间:2024/06/10 16:11

并发与竞态

在写驱动程序的时候,必须要考虑的一个问题是对共享资源(硬件资源、全局变量以及静态变量等)的并发访问。而并发访问则很容易导致竞态(对资源的非控制访问)的产生。

假设驱动程序中有类似下面的一段代码:

...static char *p;...void func(void) {if (!p) {p = kmalloc(size, GPF_KERNEL);if (!p)goto err_0;}}...

现在有两个独立的进程A和B在同时调用func函数。每个进程的if(!p)都为真了,那么将调用两次kmalloc。如果进程B调用的kmalloc在进程A之后返回,那么p指向的是进程B为其分配的内存。而进程A所分配的内存则没有任何指针指向它,无法通过kfree将其返还给系统。上面的过程即是一个竞态过程。

访问共享资源的代码区称为临界区,上面func内的代码即是一个临界区。为了避免竞态的发生,应该使用某种机制对临界区加以保护。最常用的方法是自旋锁和信号量。

自旋锁

自旋锁它是为实现保护共享资源而提出一种锁机制。它的过程大致是:当程序要运行到临界区的时候先查看自旋锁的状态,如果没被锁住则它将自旋锁设定为锁定状态(此时相应的线程持有锁),并进入临界区执行代码。而没有持有锁的线程则是一直在临界区外查询锁的状态(自旋、忙等),直至持有锁的线程释放锁。

自旋锁的主要操作过程如下:

spinlock_t my_lock;  //定义自旋锁spin_lock_init(&my_lock); //初始化自旋锁spin_lock(&my_lock); //获取自旋锁…/*临界区代码*/spin_unlock(&my_lock); /*释放自旋锁*/

采用上述自旋锁保护的临界区代码可以保证不受其它CPU的影响,但是还是可能受到中断的影响。我们可以在加锁的时候关中断。内核为我们提供了一下的一些获取及释放锁的操作:

spin_lock_irq(&my_lock);  //获取自旋锁并关中断spin_unlock_irq(&my_lock); //释放自旋锁并开中断spin_lock_irqsave(&my_lock); //获取自旋锁、关中断并保存中断状态spin_unlock_irqrestore(&my_lock); //释放自旋锁开中断并恢复中断状态spin_lock_bh(&my_lock); //获取锁并关中断底半部spin_unlock_bh(&my_lock); //释放锁并开中断底半部

另外,Linux内核还提供了下面的操作接口:

spin_trylock(&my_lock);

它用于尝试获得自旋锁,如果能够立即获得,它将获得锁并返回真,否则立即返回假。它实际上不在自旋忙等了。

自旋锁会屏蔽抢占进程并可能导致系统死锁(最常见的引起死锁的原因是对自旋锁的递归调用)。在用自旋锁保护的临界区内不能调用可能引起进程调度的函数,如果进程获得自旋锁之后再阻塞,如调用copy_from_user/copy_to_user等函数,则可能导致内核崩溃。

 对共享资源的一种常用的使用情景是:允许多个线程同时对资源进行读操作,而只允许一个线程对资源进行写操作,读和写操作不能同时进行。如果使用前面的普通的自旋锁则无法区分具体的操作。内核提供了另一种形式的自旋锁---读/写自旋锁。它的主要操作过程如下:

rwlcok_t my_rwlcok = RW_LOCK_UNLOCKED; //定义并静态初始化一个读写锁rwlock_t my_rwlock; //定义一个读写锁rwlock_init(&my_rwlock); //初始化读写锁/*在读取函数中使用读取锁*/read_lock(&my_rwlock);… /*临界区代码*/read_unlock(&my_rwlock);/*写入函数中使用写入锁*/write_lock(&my_rwlock);…/*临界区代码*/write_unlock(&my_rwlock);

同样的读写自旋锁也有一些拓展的使用接口:

/*读取锁的相关接口*/read_lock_irq(&my_rwlock);read_unlock_irq(&my_rwlock);read_lock_irqsave(&my_rwlock, flag);read_unlock_irqrestore(&my_rwlock, flag);read_lock_bh(&my_rwlock);read_unlock_bh(&my_rwlock);/*写入锁的相关接口*/write_lock_irq(&my_rwlock);write_unlock_irq(&my_rwlock);write_lock_irqsave(&my_rwlock, flag);write_unlock_irqrestore(&my_rwlock, flag);write_lock_bh(&my_rwlock);write_unlock_bh(&my_rwlock);write_trylock((&my_rwlock);

这些接口的用法和含义同普通自旋锁的接口和含义类似,需注意的是只有写入锁有trylock。读取锁没有trylock。在程序中使用自旋锁应加上头文件<linux/spinlock.h>。

 

信号量

信号量是保护共享资源的另一种方式,它像是一个对共享资源具有锁定功能的计数器。它的运行过程大致是:当程序运行到临界区的时候首先查看信号量的计数值,如果值为正,则进入临界区执行代码并将信号量的值减1,等执行完临界区的代码后再将信号量的值加1;如果值为0,则相应的进程进入休眠状态,直至信号量的值变为正进程被唤醒并重新查看信号量的值。在实际使用的过程当中,可以将信号量的初始值设为1以实现对共享资源的互斥访问。注意:在上述过程中我们只是调用内核提供的接口,信号量的值是由内核维护的。

信号量的使用过程如下:

struct semaphore my_sem; //定义一个信号量sema_init(&my_sem, 1); //将信号量初始化为1down(&my_sem); //获取信号量… /*临界区代码*/up(&my_sem); //释放信号量

如果浏览内核代码的话会发现down()已经被deprecated了,内核还提供了一些其它的接口:

down_interruptible(&my_sem); //在休眠过程中可被中断down_trylock(&my_sem);

同自旋锁一样,信号量也有读写信号量,使用方法都是类似的。下面仅列出读写信号量的接口:

struct rw_semaphore my_rwsem; //定义一个读写信号量init_rwsem(&my_rwsem); //初始化读写信号量down_read(&my_rwsem); //获取读信号量down_read_trylock(&my_rwsem); //尝试获取读信号量up_read(&my_rwsem); //释放读信号量down_write(&my_rwsem); //获取写信号量down_write_trylock(&my_rwsem); //尝试获取写信号量up_write(&my_rwsem); //释放写信号量

在程序中使用信号量时需加头文件<linux/semaphore.h>,使用读写信号量时需加头文件<linux/rwsem.h>。

当将信号量的值初始化为1时,它和互斥体就是一样的了。互斥体的主要接口有:

struct mutex my_mtx; //定义一个互斥体mutex_init(&my_mtx); //初始化互斥体mutex_lock(&my_mtx); //获取互斥体mutex_ trylock(&my_mtx); //尝试获取互斥体mutex_lock_interruptible(&my_mtx); //获取互斥体并设置为可中断mutex_unlock(&my_mtx); //释放互斥体

自旋锁和信号量是两种常用的保护临界区的手段。他们的使用主要有以下的一些区别:

1、使用自旋锁的程序在无法获取自旋锁时是属于忙等状态而使用信号量的程序在无法获取信号量时事处于休眠状态的,因此自旋锁适用于持有时间很短时使用,而信号量适用于较长持有时间时使用。

2、当程序持有自旋锁时不能进行内核抢占,而持有信号量是允许进行内核抢占。

3、信号量是存在进程上下文中的,因此信号量只能在进程上下文中使用。而自旋锁可以在任何上下文中使用。

4、使用信号量时允许发生进程上下文切换,而如果在使用自旋锁时发生上下文切换则会引起死锁。












0 0
原创粉丝点击