Linux内核中有许多不同类型的锁,它们都可以用来保护关键资源,以避免多个线程或进程之间发生竞争条件,从而保护系统的稳定性和可靠性。这些锁的类型包括:互斥锁(mutex)、读写锁(rwlock)、自旋锁(spinlock)和信号量(semaphore)。今天就给大家介绍一下Linux内核中的各种锁,以及我们在实际项目中该如何选择使用哪个锁。
二、几种锁的介绍互斥锁(mutex) 是最常用的锁,它可以保护共享资源,使得在某个时刻只有一个线程或进程可以访问它。读写锁(rwlock)则可以同时允许多个线程或进程读取共享资源,但只允许一个线程或进程写入它。自旋锁(spinlock)可以用来保护共享资源,使得在某个时刻只有一个线程或进程可以访问它,但它会使线程或进程“自旋”,直到获得锁为止。最后,信号量(semaphore)可以用来控制对共享资源的访问,以保证其他线程或进程可以安全地访问它们。
读写锁(rwlock) 是一种用于控制多线程访问共享资源的同步机制。当一个线程需要读取共享资源时,可以获取读取锁,这样其他线程就可以同时读取该资源,而不会引发冲突。当一个线程需要写入共享资源时,可以获取写入锁,这样其他线程就不能访问该资源,从而保证数据的完整性和一致性。
自旋锁(spinlock) 是一种简单而有效的用于解决多线程同步问题的锁。它是一种排他锁,可以在多线程环境下保护共享资源,以防止多个线程同时对该资源进行访问。自旋锁的基本原理是,当一个线程试图获取锁时,它会不断尝试获取锁,直到成功为止。在这期间,线程不会进入休眠状态,而是一直处于忙等待(busy-waiting)状态,这也就是自旋锁的由来。
信号量(semaphore) 是一种常用的同步机制,它可以用来控制多个线程对共享资源的访问。它有助于确保同一时间只有一个线程能够访问共享资源,从而避免资源冲突和竞争。信号量是一种整数计数器,用于跟踪可用资源的数量。当一个线程需要访问共享资源时,它首先必须获取信号量,这会将信号量的计数器减少1,而当它完成访问共享资源后,它必须释放信号量,以便其他线程也可以访问共享资源。
四、互斥锁(Mutex)互斥锁是最基本的锁类型,在内核中使用较为广泛。它是一种二元锁,只能同时有一个线程持有该锁。当一个线程请求该锁时,如果锁已被占用,则线程会被阻塞直到锁被释放。互斥锁的实现使用了原子操作,因此它的性能比较高,但也容易出现死锁情况。
在内核中,互斥锁的定义如下:
struct mutex { raw_spinlock_t wait_lock; struct list_head wait_list; struct task_struct *owner; int recursion; #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif };
互斥锁的使用非常简单,通常只需要调用两个函数即可完成:
void mutex_init(struct mutex *lock):函数用于初始化互斥锁 void mutex_lock(struct mutex *lock):函数用于获取互斥锁 void mutex_unlock(struct mutex *lock):函数用于释放互斥锁五、读写锁(Reader-Writer Lock)
读写锁是一种特殊的锁类型,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁的实现使用了两个计数器,分别记录当前持有锁的读线程数和写线程数。
在内核中,读写锁的定义如下:
struct rw_semaphore { long count; struct list_head wait_list; #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif };
读写锁的使用也比较简单,通常只需要调用三个函数即可完成:
init_rwsem(struct rw_semaphore *sem):函数用于初始化读写锁 down_read(struct rw_semaphore *sem):函数用于获取读锁 up_read(struct rw_semaphore *sem):函数用于释放读锁 down_write(struct rw_semaphore *sem):函数用于获取写锁 up_write(struct rw_semaphore *sem):函数用于释放写锁六、自旋锁(spinlock)
自旋锁是一种保护共享资源的锁,它会在等待期间一直占用CPU。自旋锁适用于代码临界区比较小的情况,且共享资源的独占时间比较短,这样就可以避免上下文切换的开销。自旋锁不能用于需要睡眠的代码临界区,因为在睡眠期间自旋锁会一直占用CPU。
在Linux内核中,自旋锁使用spinlock_t类型表示,可以通过spin_lock()和spin_unlock()函数对其进行操作。
spin_lock_init(spinlock_t *lock):用于初始化自旋锁,\ 将自旋锁的初始状态设置为未加锁状态。 spin_lock(spinlock_t *lock):用于获得自旋锁,\ 如果自旋锁已经被占用,则当前进程会自旋等待,直到自旋锁可用。 spin_trylock(spinlock_t *lock):用于尝试获取自旋锁,\ 如果自旋锁当前被占用,则返回0,否则返回1。 spin_unlock(spinlock_t *lock):用于释放自旋锁。
在使用自旋锁时,需要注意以下几点:
自旋锁只适用于临界区代码比较短的情况,因为自旋等待的过程会占用CPU资源。
自旋锁不可重入,也就是说,如果一个进程已经持有了自旋锁,那么它不能再次获取该自旋锁。
在持有自旋锁的情况下,应该尽量避免调用可能会导致调度的内核函数,比如睡眠函数,因为这可能会导致死锁的发生。
在使用自旋锁的时候,应该尽量避免嵌套使用不同类型的锁,比如自旋锁和读写锁,因为这可能会导致死锁的发生。
当临界区代码较长或者需要睡眠时,应该使用信号量或者读写锁来代替自旋锁。
信号量是一种更高级的锁机制,它可以控制对共享资源的访问次数。信号量可分为二元信号量和计数信号量。二元信号量只有0和1两种状态,常用于互斥锁的实现;计数信号量则可以允许多个进程同时访问同一共享资源,只要它们申请信号量的数量不超过该资源所允许的最大数量。
在Linux内核中,信号量使用struct semaphore结构表示,可以通过down()和up()函数对其进行操作。
void sema_init(struct semaphore *sem, int val):初始化一个信号量,\ val参数表示初始值。 void down(struct semaphore *sem):尝试获取信号量,如果信号量值为 0,\ 调用进程将被阻塞。 int down_interruptible(struct semaphore *sem):尝试获取信号量,如果信号量值为0,\ 调用进程将被阻塞,并可以被中断。 int down_trylock(struct semaphore *sem):尝试获取信号量,如果信号量值为 0,\ 则立即返回,否则返回错误。 void up(struct semaphore *sem):释放信号量,将信号量的值加 1,\ 并唤醒可能正在等待信号量的进程。八、该如何选择正确的锁
当需要对共享资源进行访问和修改时,我们通常需要采用同步机制来保证数据的一致性和正确性,其中锁是最基本的同步机制之一。不同类型的锁适用于不同的场景。
互斥锁适用于需要保护共享资源,只允许一个线程或进程访问共享资源的场景。例如,当一个线程正在修改一个数据结构时,其他线程必须等待该线程释放锁后才能修改该数据结构。
读写锁适用于共享资源的读写操作频繁且读操作远大于写操作的场景。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。例如,在一个数据库管理系统中,读取操作比写入操作频繁,使用读写锁可以提高系统的并发性能。
自旋锁适用于保护共享资源的访问时间很短的场景,当线程需要等待的时间很短时,自旋锁比互斥锁的性能更好。例如,在访问共享资源时需要进行一些简单的操作,如对共享资源进行递增或递减等操作。
信号量适用于需要协调多个线程或进程对共享资源的访问的场景,允许多个线程或进程同时访问共享资源,但同时访问的线程或进程数量有限。例如,在一个并发下载系统中,可以使用信号量来限制同时下载的文件数量。
举个生活中的例子:当我们在买咖啡的时候,柜台前可能会有一个小桶,上面写着“请取走您需要的糖果,每人一颗”这样的字样。这个小桶就是一个信号量,它限制了每个人能够取走的糖果的数量,从而保证了公平性。
如果我们把这个小桶换成互斥锁,那么就可以只允许一个人在柜台前取走糖果。如果使用读写锁,那么在非高峰期的时候,多个人可以同时取走糖果,但在高峰期时只允许一个人取走。
而如果我们把这个小桶换成自旋锁,那么当有人在取走糖果时,其他人就需要一直在那里等待,直到糖果被取走为止。这样可能会造成浪费时间的情况,因为其他人可能有更紧急的事情需要处理。
九、总结在Linux内核中,有四种常见的锁:互斥锁、读写锁、自旋锁和信号量。这些锁适用于不同的场景,开发者需要根据实际情况选择适当的锁来确保并发访问的正确性和性能。