-
这是 Linux 驱动开发中利用原子变量(Atomic Variable)实现"互斥锁(设备只能被一个进程打开)"的经典逻辑。
/* 通过判断原子变量的值来检查LED 有没有被别的应用使用 */
if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock);/* 小于0 的话就加1,使其原子变量等于0 */
return -EBUSY; /* LED 被使用,返回忙 */
}
/* 执行需要保护的资源 */
/* 完成对资源操作后,把锁加1,恢复成资源可以使用的状态 */
atomic_inc(&gpioled.lock);
-
这展示了在 Linux 驱动开发中,利用自旋锁(Spinlock)+ 普通变量(
dev_stats)实现设备互斥访问(只允许一个进程打开)的另一种标准写法。spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats)
{ /* 如果设备被使用了 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
return -EBUSY;
}
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
/* 对受保护资源进行操作 */
/* 资源操作完成后,需要把普通变量(
dev_stats)变成0,以恢复资源可操作性 */spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats) {
gpioled.dev_stats--;
}
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
解释:
-
自旋锁的角色(保镖): 它的任务非常短命。它的存在仅仅是为了保护
if (gpioled.dev_stats)和dev_stats++这两行代码在多核并行时不被打扰。它就像一个保镖,护送进程安全地完成"检查并登记"这个动作。 -
dev_stats的角色(登记簿): 它的任务非常长命。一旦被写成1,它就会一直保持为1,直到用户层主动调用close()关闭设备。它负责长达几小时、甚至几天的硬件资源占有标记。
-
-
下面展示了 Linux 驱动开发中信号量(Semaphore)在实际获取锁(P 操作)时的两种不同接口:
down_interruptible和down。它们的核心区别在于:当没有信号量可用、进程被迫进入睡眠等待时,该进程是否能够响应外界的信号(如
Ctrl + C终止信号)。-
down_interruptible的逻辑/* 获取信号量,进入休眠状态的进程可以被信号打断 */
if (down_interruptible(&gpioled.sem)) {
return -ERESTARTSYS;
}
1.它的微观行为:
-
进程尝试获取信号量
gpioled.sem。 -
如果此时信号量计数
count > 0(有空闲资源),进程成功拿到锁,函数返回 0,跳过 if 语句,继续往下执行。 -
如果此时资源被别人占用了,进程就会进入可中断的睡眠状态(TASK_INTERRUPTIBLE),让出 CPU,躺在等待队列里挂起。
-
为什么要用
if判断?(信号打断机制)在睡觉期间,这个进程不仅在等锁释放,它还在睁着一只眼睛盯着系统信号 。如果用户突然觉得等得太久了,按下了
Ctrl + C,或者通过终端执行了kill命令:-
内核会捕捉到这个终止信号,并强行把这个正在睡觉的进程唤醒。
-
此时,
down_interruptible醒来后发现自己并不是因为等到了锁而醒来,而是因为被信号轰醒的。 -
于是,它会放弃继续等锁,并返回一个非 0 值(通常是
-EINTR)。 -
代码走向:
if (非0)条件成立,进入 if 内部,执行return -ERESTARTSYS;。-ERESTARTSYS的含义: 这是一个内核内部的错误码。它告诉 Linux 系统调用层:"这个进程是被信号打断的,如果可能的话,请在处理完信号后,自动重启这个系统调用;或者把错误传回给用户层(变成EINTR错误)。" 这样可以保证应用层程序不会卡死,能安全退场。
-
-
down的逻辑#if 0
down(&gpioled.sem); /* 不能被信号打断 */
#endif-
如果没有资源,进程会进入不可中断的睡眠状态(TASK_UNINTERRUPTIBLE) (也就是你在
ps命令里经常看到的D状态进程)。 -
在这个状态下,进程处于"深度昏迷"。无论你在终端里怎么按
Ctrl + C,或者执行kill -9,任何信号都会被它完全屏蔽,根本叫不醒它。 -
唯一的清醒方式: 必须等到持有锁的那个进程调用
up()释放了信号量,它才能被唤醒。 -
up(&dev->sem); /* 释放信号量,信号量值加1 */
-
-
下面展示了 Linux 内核中互斥体(Mutex)在获取锁时的两种接口:
mutex_lock_interruptible和mutex_lock。在"处理信号打断"的逻辑上,互斥体和信号量的设计哲学是完全相通的。
1.mutex_lock_interruptible的逻辑
/* 获取互斥体,可以被信号打断 */if (mutex_lock_interruptible(&gpioled.lock)) {
return -ERESTARTSYS;
}
1.它的行为机制: 1. 线程尝试获取互斥锁 `gpioled.lock`。 2. 如果锁当前是空闲的,线程**顺利拿到锁,函数返回 0** ,程序跳过 `if` 语句块继续往下走(进入临界区)。 3. 如果锁已经被其他线程占用了,当前线程就会进入**可中断的睡眠状态(TASK_INTERRUPTIBLE)**,让出 CPU,住进这个 mutex 的等待队列里。-
信号打断的处理:
在排队睡觉期间,如果用户在终端发送了信号(比如按下了
Ctrl + C或者是发送了kill信号):-
内核会立刻把这个线程从睡眠中唤醒。
-
mutex_lock_interruptible醒来后发现自己没拿到锁,而是被信号吵醒的,于是它会放弃抢锁 ,并返回一个非 0 值 (通常是-EINTR)。 -
代码进入
if分支,执行return -ERESTARTSYS;,让整个系统调用安全上浮退出,允许应用层程序响应这个Ctrl + C动作。
-
-
mutex_lock的逻辑:mutex_lock(&gpioled.lock); /* 不能被信号打断 */
-
当锁被占用时,线程会进入不可中断的睡眠状态(TASK_UNINTERRUPTIBLE) ,也就是著名的 D 状态。
-
此时,线程对外界的任何信号(包括
kill -9)完全免疫。 -
它清醒的唯一条件,就是占有锁的那个线程调用
mutex_unlock()把锁还回来。如果占有锁的线程发生死锁永远不还,这个陷入mutex_lock的线程就会变成一个永远无法杀掉的死进程,只能靠重启电脑解决。
-
-
信号量(Semaphore)和互斥体 (Mutex)对比。
#### 乐观自旋(Optimistic Spinning)
1. **信号量(Semaphore):** 只要发现没票了,老老实实立刻去睡觉(引发上下文切换,很慢)。
2. **互斥体(Mutex):** 没拿到锁时,它会先偷看一眼:"拿着这把锁的线程,目前是不是正在另一个 CPU 上跑着呢?"
* 如果对方正在跑,Mutex 认为对方很快就会用完放锁,于是它会**先原地自旋(自选锁的方式)等一会**,而不是马上睡觉。
* 如果对方已经睡觉去了,它才会跟着去睡觉。这个机制极大地减少了线程在睡眠/唤醒之间的切换开销,性能比信号量高得多。
#### 所有权限制(Ownership)
1. Mutex 严格要求"谁加锁,必须由谁解锁"。
2. 如果线程 A 调用了 `mutex_lock`,结果线程 B 试图去调用 `mutex_unlock`,内核会直接报 Bug(Kernel Panic)。而信号量(Semaphore)没有这个限制,线程 A 用 `down` 拿走票,线程 B 可以调用 `up` 还回票。