scull的缺陷
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
sucll代码中当有两个进程A、B正在独立尝试向同一个scull设备的相同偏移量写入数据,两个进程在同一时刻到达上述代码的第一个if处。执行接下来的kmalloc操作。当A先执行完,B后执行完时,B的结果会刷新A的结果,最终将A的结果覆盖。导致A分配的内存丢失。
这种情况就是竞态。竞态会导致对共享数据的非控制访问。
并发及其管理
内核代码是可抢占的,所以驱动程序代码可能在任何时候丢失对处理器的独占,而拥有处理器的进程可能正在调用我们的驱动程序代码。
设备中断时异步事件,也会导致代码的并发执行。
内核也会提供很多可延迟机制,workqueue、tasklet、time等,这些机制会使代码可在任何时刻执行,而不管当前进程在做什么。
竞态通常作为对资源的共享访问结果而产生。
在自己设计驱动程序时,应避免对资源的共享,尽量避免使用全局变量。
但是,资源的共享不可避免。硬件资源本质上就是共享的,而软件资源经常需要对其他执行线程可用。
全局变量并不是唯一的共享数据途径,只要我们将一个指针传递给内核的其他部分,一个新的共享就建立了。
资源共享的硬件规则:
1.在单个执行线程之外共享硬件或软件资源的时候,因为另一个线程可能对该资源的不一致观察,因此必须显式地管理对该资源的访问。
访问管理的常见技术称为”锁定“或者”互斥“——确保一次只有一个执行线程可操作共享资源。
2.当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。
信号量和互斥体
对上面提到的scull内存泄漏示例而言,我们需要对代码建立临界区:确保在任意给定时刻,代码只能被一个线程执行。
一个信号量(semaphore: 旗语,信号灯)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同时在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有时也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。
Linux内核中几乎所有的信号量均用于互斥。
Linux信号量的实现
使用信号量,内核代码必须包含<asm/semaphore.h> 。
信号量的创建和初始化:
1.创建信号量,通过sema_init完成:
void sema_init(struct semaphore *sem,int val);
val是赋给信号量一个初值。
2.信号量的声明和初始化:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
宏的作用是一个称为name的信号量变量被初始化为1(使用DECLARE_MUTEX)或者0(DECLARE_MUTEX_LOCKED)。
初始化为0 时,在允许任何线程访问之前,必须显示解锁该互斥体。
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
这两个宏表明在运行时被初始化(例如动态分配互斥体的状态下)。
P函数被称为down。
“down”减小信号量的值,也许会将调用者置于休眠状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。
down的三个版本:
void down(struct semaphore*sem);/*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore*sem);/*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore*sem);/*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*/
V函数等价于up。
void up(struct semaphore*sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/
在scull中使用信号量
正确使用锁的关键是明确指定需要保护的资源,并使用正确的锁来访问这些资源。
scull中scul.h确定了锁的范围:
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
信号量在使用前必须初始化。sucll在装载时进行初始化:
/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].