在Linux驱动开发中,“并发”和“竞争”是两个重要的概念,它们涉及到多任务环境下资源的管理和使用。
并发 (Concurrency)
并发指的是在同一时间段内,多个任务看似同时运行的现象。实际上,在单核处理器上,这通常是通过快速的任务切换来实现的,而在多核或多处理器环境中,则可以真正实现并行执行。并发可以提高系统的响应能力和资源利用率。在Linux系统中,并发不仅限于用户空间的应用程序,同样也存在于内核空间,特别是驱动程序中。驱动程序可能需要处理来自不同进程的请求,甚至是中断处理程序的并发执行。
竞争 (Contention)
竞争指的是多个任务或进程试图同时访问或修改同一个共享资源时所发生的情况。如果没有适当的同步机制来协调这些访问,则可能导致竞态条件(race condition),即最终结果依赖于相对执行顺序的不确定性。例如,如果两个线程同时读取和修改同一个变量,那么根据它们执行的相对顺序,可能会导致数据不一致或其他未定义的行为。
处理方法
处理竞争与并发的方法多种多样,这些方法旨在确保多个执行单元(如进程、线程或中断处理程序)在访问共享资源时的有序性和一致性。以下是一些主要的方法及其使用例子:
1. 原子操作(atomic operation)
定义:原子操作是指在执行过程中不会被其他线程或中断打断的操作。
使用例子:在Linux内核中,原子操作常用于对全局变量的增减操作,如计数器。假设有一个全局整型变量count
,多个线程可能同时对其进行增减操作。通过使用原子操作API,如atomic_inc(&count)
和atomic_dec(&count)
,可以确保每次增减操作都是原子的,从而避免竞争条件。
以下是一些常见的原子整形操作API函数:
- 定义原子变量初始化(ATOMIC_INIT)
- 作用:定义并初始化一个原子变量,为其赋予一个特定的初始值。
- 原型:ATOMIC_INIT
(int i);
- 参数:通常是一个整型值。
- 原子设置(atomic_set)
- 作用:设置原子变量的值。
- 原型:
void atomic_set(atomic_t *v, int i);
- 参数:
v
是指向原子变量的指针,i
是要设置的值。- 原子读取(atomic_read)
- 作用:读取原子变量的值。
- 原型:
int atomic_read(const atomic_t *v);
- 参数:
v
是指向原子变量的指针。- 返回值:返回原子变量的当前值。
- 原子增加(atomic_add)
- 作用:给原子变量增加一个值。
- 原型:
void atomic_add(int i, atomic_t *v);
- 参数:
i
是要增加的值,v
是指向原子变量的指针。- 原子减少(atomic_sub)
- 作用:从原子变量中减少一个值。
- 原型:
void atomic_sub(int i, atomic_t *v);
- 参数:
i
是要减少的值,v
是指向原子变量的指针。- 原子增加并返回(atomic_add_return / atomic_inc_return)
- 作用:给原子变量增加一个值,并返回增加后的值。
- 原型:
int atomic_add_return(int i, atomic_t *v);
- 或:
int atomic_inc_return(atomic_t *v);
(增加1并返回)- 参数:
i
是要增加的值(对于atomic_inc_return
,此参数为隐式的1),v
是指向原子变量的指针。- 返回值:返回增加后的值。
- 原子减少并返回(atomic_sub_return / atomic_dec_return)
- 作用:从原子变量中减少一个值,并返回减少后的值。
- 原型:
int atomic_sub_return(int i, atomic_t *v);
- 或:
int atomic_dec_return(atomic_t *v);
(减少1并返回)- 参数:
i
是要减少的值(对于atomic_dec_return
,此参数为隐式的1),v
是指向原子变量的指针。- 返回值:返回减少后的值。
- 原子增加如果非负(atomic_add_unless)
- 作用:如果原子变量的当前值是非负的,则给它增加一个值。
- 原型:
int atomic_add_unless(atomic_t *v, int a, int u);
- 参数:
v
是指向原子变量的指针,a
是要增加的值,u
是除非当前值等于此值时才进行操作的值(通常用于避免负值)。- 返回值:如果操作成功,则返回1;否则返回0。
以下是一些常见的原子位操作API函数:
- 原子位设置(atomic_set_bit)
- 作用:设置指定位置上的位。
- 原型:
void atomic_set_bit(int nr, void *addr);
(在某些内核版本中,可能是void set_bit(int nr, volatile unsigned long *addr);
)- 参数:
nr
是要设置的位的位置(从0开始计数),addr
是指向包含该位的内存地址的指针。- 原子位清除(atomic_clear_bit)
- 作用:清除指定位置上的位。
- 原型:
void atomic_clear_bit(int nr, void *addr);
(在某些内核版本中,可能是void clear_bit(int nr, volatile unsigned long *addr);
)- 参数:与
atomic_set_bit
相同。- 原子位翻转(atomic_flip_bit)
- 作用:翻转(即取反)指定位置上的位。
- 原型:
int atomic_flip_bit(int nr, void *addr);
(返回翻转前的位值;在某些内核版本中,可能没有直接的翻转函数,但可以通过其他方式实现)- 参数:与
atomic_set_bit
相同。- 返回值:返回翻转操作前该位的值。
- 原子位测试并设置(atomic_test_and_set_bit)
- 作用:测试指定位置上的位,如果位为0则设置为1,并返回测试结果。
- 原型:
int atomic_test_and_set_bit(int nr, void *addr);
(在某些内核版本中,可能是int test_and_set_bit(int nr, volatile unsigned long *addr);
)- 参数:与
atomic_set_bit
相同。- 返回值:如果位原来是0,则返回0,并且位被设置为1;如果位原来是1,则返回非0值。
- 原子位测试并清除(atomic_test_and_clear_bit)
- 作用:测试指定位置上的位,如果位为1则清除为0,并返回测试结果。
- 原型:
int atomic_test_and_clear_bit(int nr, void *addr);
(在某些内核版本中,可能是int test_and_clear_bit(int nr, volatile unsigned long *addr);
)- 参数:与
atomic_set_bit
相同。- 返回值:如果位原来是1,则返回非0值,并且位被清除为0;如果位原来是0,则返回0。
- 原子位测试(atomic_test_bit)
- 作用:测试指定位置上的位是否为1。
- 原型:
int atomic_test_bit(int nr, void *addr);
(在某些内核版本中,可能是通过test_bit
宏或其他方式实现)- 参数:与
atomic_set_bit
相同。- 返回值:如果位是1,则返回非0值;如果位是0,则返回0
2. 自旋锁(Spinlock)
定义:自旋锁是一种轻量级的锁,用于保护临界区资源。当一个线程获得自旋锁后,其他线程将处于忙等待状态,直到锁被释放。
使用例子:在设备驱动中,当多个线程需要访问同一个硬件设备时,可以使用自旋锁来保护对硬件寄存器的访问。例如,在访问网络设备的发送队列时,可以先加自旋锁,访问完成后释放自旋锁。这样可以确保在任一时刻只有一个线程能够访问发送队列,从而避免竞争条件。
常见的自旋锁API函数的详细介绍:
1. 初始化自旋锁
spin_lock_init(spinlock_t *lock)
- 作用:动态初始化一个自旋锁,将其设置为未锁状态。
- 参数:指向
spinlock_t
类型变量的指针,该变量表示要初始化的自旋锁。 - 注意事项:通常在动态分配的自旋锁或需要在运行时初始化的场景中使用。
DEFINE_SPINLOCK(name)
- 作用:静态初始化一个自旋锁,并为其指定一个名字。
- 参数:自旋锁的名字,该名字将用作变量名。
- 注意事项:这通常用于全局或静态自旋锁的初始化,它在编译时分配并初始化自旋锁。
2. 加锁操作
spin_lock(spinlock_t *lock)
- 作用:尝试获取自旋锁。如果锁当前未被持有,则立即获取锁;如果锁已被持有,则调用线程将忙等待直到锁被释放。
- 参数:指向要获取的自旋锁的指针。
- 注意事项:在持有锁期间,应避免调用可能导致线程休眠的函数,以免引发死锁。
spin_trylock(spinlock_t *lock)
- 作用:尝试获取自旋锁,但不会忙等待。
- 参数:指向要尝试获取的自旋锁的指针。
- 返回值:如果成功获取锁,则返回非零值(真);如果锁已被持有,则返回零值(假)。
- 注意事项:这个函数适用于那些不希望无限等待锁的场景。
3. 解锁操作
spin_unlock(spinlock_t *lock)
- 作用:释放自旋锁。
- 参数:指向要释放的自旋锁的指针。
- 注意事项:必须与
spin_lock
或spin_trylock
配对使用。
4. 其他辅助函数
spin_is_locked(spinlock_t *lock)
- 作用:检查自旋锁是否已被持有。
- 参数:指向要检查的自旋锁的指针。
- 返回值:如果锁被持有,则返回非零值(真);否则返回零值(假)。
- 注意事项:这个函数通常用于调试或状态检查。
使用自旋锁的注意事项
- 锁持有时间:自旋锁的持有时间应尽可能短,以避免CPU资源的浪费和性能下降。
- 避免死锁:在自旋锁保护的临界区内,不应调用任何可能导致线程休眠的API函数,如
malloc
、printk
(在某些情况下)、copy_from_user
等。 - 中断和抢占:
- 在中断服务例程中使用自旋锁时,应注意中断的嵌套和优先级,以避免中断上下文中的死锁。
- 在支持抢占的系统中,自旋锁会自动禁止内核抢占,但在单CPU系统中,应确保在持有锁期间不会触发抢占。
- 递归申请:应避免递归申请同一个自旋锁,因为这会导致死锁。
- 锁粒度:应合理设置锁的粒度,以平衡并发性和性能。过粗的锁粒度会导致不必要的等待和性能下降,而过细的锁粒度则可能增加锁管理的开销和复杂性。
3. 信号量(Semaphore)
定义:信号量是一种更通用的同步机制,它允许多个线程同时访问共享资源,但可以限制同时访问资源的数量。
使用例子:在数据库连接池中,可以使用信号量来限制同时访问数据库连接的线程数量。假设数据库连接池最大允许10个连接,则可以初始化一个信号量,其初始值为10。每当一个线程需要获取数据库连接时,它会尝试从信号量获取一个许可(通过sem_wait()
)。如果信号量的值大于0,则线程可以成功获取连接,并将信号量的值减1;如果信号量的值为0,则线程将被阻塞,直到有其他线程释放连接(通过sem_post()
)并增加信号量的值。
信号量API函数:
信号量的使用注意事项
- 适用场景:
- 信号量适用于那些占用资源比较久的场合,因为它可以使等待资源的线程进入休眠状态,从而避免忙等待带来的CPU资源浪费。
- 中断环境限制:
- 信号量不能用于中断中,因为中断处理例程不能休眠,而信号量的等待操作可能会导致休眠。
- 性能考虑:
- 如果共享资源的持有时间比较短,则不适合使用信号量,因为频繁的休眠和线程切换带来的开销会远大于信号量带来的同步优势。在这种情况下,应考虑使用自旋锁等轻量级的同步机制。
- 资源管理:
- 在使用信号量进行同步时,应确保在不再需要信号量时销毁它,以避免资源泄露。对于有名信号量,还需要注意信号量集的创建和删除操作,以确保资源得到正确管理。
- 原子性操作:
- 在多处理器系统中,信号量的操作通常是原子的,但访问共享资源的其他操作可能不是原子的。因此,在使用信号量进行同步时,还需要考虑其他可能的同步问题,并确保对共享资源的访问是安全的。
4. 互斥锁(Mutex)
定义:互斥锁是一种用于保护临界区资源的同步机制,确保在任何时候只有一个线程能够访问被保护的资源。
使用例子:在多线程程序中,当多个线程需要访问同一个全局变量时,可以使用互斥锁来防止竞争条件。例如,在一个银行转账系统中,当两个线程分别尝试从同一个账户中转账时,可以使用互斥锁来保护对该账户余额的访问。在访问账户余额之前,线程会先尝试获取互斥锁;如果获取成功,则进入临界区进行转账操作;操作完成后释放互斥锁。这样可以确保在任一时刻只有一个线程能够修改账户余额。
API函数:
互斥锁(mutex)的注意事项:
- 中断环境限制:
- 互斥锁可以导致休眠,因此它不能在中断处理例程中使用。中断处理例程需要快速执行完毕,并且不能进入休眠状态。在中断中应使用自旋锁等不会引起休眠的同步机制。
- 临界区调用限制:
- 和信号量类似,互斥锁保护的临界区内可以调用可能引起阻塞的API函数。这是因为互斥锁在等待资源可用时会将线程置于休眠状态,直到资源被释放并唤醒该线程。
- 互斥锁的持有与释放:
- 一次只有一个线程可以持有互斥锁,这保证了临界区内的资源访问是独占的。因此,必须由持有互斥锁的线程来释放它。如果其他线程尝试释放未持有的互斥锁,将导致未定义行为。
- 互斥锁不能递归上锁和解锁。递归上锁意味着同一个线程尝试多次获取同一个互斥锁,这将导致死锁。同样地,如果一个线程没有持有互斥锁却尝试解锁它,也会导致错误。
- 避免死锁:
- 在设计多线程程序时,应确保互斥锁的使用不会导致死锁。死锁是一种情况,其中多个线程相互等待对方释放资源,从而无法继续执行。
- 性能考虑:
- 虽然互斥锁提供了强大的同步能力,但它们也可能引入一定的性能开销。特别是在高争用情况下,互斥锁可能导致频繁的上下文切换和线程休眠。因此,在性能敏感的场景中,应谨慎使用互斥锁,并考虑使用其他轻量级的同步机制(如自旋锁、读写锁等)。
- 资源泄露避免:
- 和信号量一样,在使用互斥锁时也应确保在不再需要时正确释放它,以避免资源泄露。资源泄露可能导致系统资源耗尽,从而影响系统的稳定性和性能。