我正在为一些新颖的硬件实现设备驱动程序,并希望一次只允许一个进程访问该设备。并发读/写操作会使硬件混淆到可能需要发生硬复位的程度。我仍然有以下问题:
在示例代码from Linux Device Drivers中,open()
调用使用了锁,但close()
没有。这里是否还存在竞争条件,或者scull_s_count
的减量是否保证是原子的?基本上,在这个例子中,我想知道如果一个进程在另一个进程正在收尾并关闭它时正在尝试打开设备会发生什么。
我假设我不需要在scull_s_count
和{{1}中检查我的开放标志的状态(我正在做类似于示例的read()
)调用,因为进入这些调用的唯一方法是用户空间应用程序已经通过成功调用write()
已经收到fd
。这个假设是否正确?
感谢tadman的评论,我对内核的open()
机制进行了粗略的搜索。这是我现在拥有的一些伪代码:
atomic_t
int open(struct inode *inode, struct file *filp) {
spin_lock(&lock);
if (atomic_read(&open_flag)) {
spin_unlock(&lock);
return -EBUSY;
}
atomic_set(&open_flag, 1);
/* do other open() related stuff */
spin_unlock(&lock);
return 0;
}
int close(struct inode *inode, struct file *filp) {
int rc;
/* do close() stuff */
atomic_set(&open_flag, 0);
return rc;
}
是open_flag
,它是使用atomic_t
分配的更大结构的一部分。结果,它被初始化为零。
因此,此处的代码显示锁的目的是防止多个进程/线程同时打开设备,而kzalloc()
是open_flag
的事实阻止了竞争条件我在上面的问题1中担心。这种实现是否足够?此外,我仍在寻找问题2的答案。
示例代码使用自旋锁,但互斥锁会更合适吗?代码部分相对较小,几乎没有争用,所以进入睡眠和唤醒可能比仅仅旋转更少的性能。始终从用户上下文访问锁定/互斥锁,因此您应该安全地睡觉。
答案 0 :(得分:7)
你指出的例子确实存在缺陷。绝对不能保证是原子的,而且几乎肯定不是。
但实际上,我认为没有一个编译器/ CPU组合会产生可能失败的代码。可能发生的最坏情况是一个CPU核心可以完成关闭,然后另一个核心可以调用open并恢复忙,因为它有一个过时的缓存值。
Linux为此提供atomic_*
函数以及*_bit
原子位标志操作。请参阅内核文档中的core_api / atomic_ops.rst。
执行他的正确和简单模式的示例如下所示:
unsigned long mydriver_flags;
#define IN_USE_BIT 0
static int mydriver_open(struct inode *inode, struct file *filp)
{
if (test_and_set_bit(IN_USE_BIT, &mydriver_flags))
return -EBUSY;
/* continue with open */
return 0;
}
static int mydriver_close(struct inode *inode, struct file *filp)
{
/* do close stuff first */
smp_mb__before_atomic();
clear_bit(IN_USE_BIT, &mydriver_flags);
return 0;
}
真正的驱动程序应该为每个设备都有一个设备状态结构,其中包含mydriver_flags
。而不是像示例中所示为整个驱动程序使用单个全局。
那就是说,你要做的事情可能不是一个好主意。即使只有一个进程可以一次打开设备,进程中的打开文件描述符也会在进程中的所有线程之间共享。多个线程可以同时对同一个文件描述符进行read()
和write()
调用。
如果进程打开了文件描述符并调用fork()
,则该描述符将继承到新进程中。这是多种进程可以让设备一次打开的方式,尽管有上述“单一开放”限制。
因此,您仍然必须在驱动程序的文件操作中保持线程安全,因为用户仍然可以让多个线程/进程同时打开设备并同时进行调用。如果你已经安全,为什么要阻止用户这样做呢?也许他们知道自己在做什么,并确保他们的多个开场车员会“轮流”并且不会拨打电话冲突?
还要考虑使用open调用中的O_EXCL标志来使单个打开可选。
答案 1 :(得分:2)
您和其他提供答案的人都是正确的,示例存在缺陷,如果您使用像test_and_set_bit()
这样的原子位操作,TrentP是完全正确的,根本不需要锁定(或者您可以使用atomic_add_unless()
等。)
然而,他的答案也不完全正确,因为它没有处理close()
中的指令重新排序 - clear_bit()
不包括内存障碍。因此,将变量设置为零可能发生在“关闭东西”之前,如果其他人同时打开它,这可能会使驱动程序陷入困境。固定解决方案在调用clear_bit
之前添加了障碍:
static unsigned long mydriver_flags;
#define IN_USE_BIT 0
static int mydriver_open(struct inode *inode, struct file *filp)
{
if (test_and_set_bit(IN_USE_BIT, &mydriver_flags))
return -EBUSY;
/* continue with open */
return 0;
}
static int mydriver_close(struct inode *inode, struct file *filp)
{
/* do close stuff first */
smp_mb_before_atomic();
clear_bit(IN_USE_BIT, &mydriver_flags);
return 0;
}
请注意,test_and_set_bit()
已完全排序且已隐含内存障碍,因此open()
函数无需更改。
现在回答你的实际问题:
是的,有竞争条件,减少不是原子的。如果一个进程试图打开设备而另一个进程关闭它,可能会发生许多不好的事情。例如,清理代码的一部分可能与并发打开的设备初始化代码同时运行,这可能导致几乎任何可以想象的令人讨厌的结果。
是的,您的假设是正确的。您不需要对读/写方法进行任何检查。
问题中显示的代码不够,因为close()
中没有排序;您需要在smp_mb_before_atomic()
atomic_set()
之前close()
,类似于上面的代码。但如果您只是使用test_and_set_bit()
如上所示和TrentP组合原子读取和设置,则不需要锁定。
spinlock vs mutex:决定很简单,你是否需要在锁定下睡觉,即如果你的初始化代码包含任何可能导致进程休眠的内容,那么你应该使用互斥锁,同时如果你的初始化只是设置了一些变量,然后再次放下锁,那么自旋锁是完全合理的,因为它比互斥锁更轻。鉴于这是你的开/关,即没有任何性能关键,只使用互斥锁并且不担心代码是否可以睡眠是完全正常的。但是,如果您只是完全丢掉锁定而只是使用test_and_set_bit()
,则代码会好得多。
答案 2 :(得分:1)
我对Linux内核编程有点生气但是,使用原子和自旋锁对我来说看起来有些开销。
希望一次只允许一个进程访问该设备
如果这就是你需要的scull实现工作正常,scull驱动程序的open实现中的spinlock确保一次只有一个进程获得一个有效的文件句柄。
我认为我很确定(没有检查)头骨示例在发布中没有使用锁(关闭),因为只有打开的进程它可以关闭它,如果文件句柄不有效,其他进程就不会进入发布代码。
示例代码使用自旋锁,但互斥锁会更合适吗?
自旋锁实现更快,足以完成此任务。
int scull_s_release(struct inode *inode, struct file *filp)
{
scull_s_count--; /* release the device */
/* from there until the function return is the only place where a race can occur
* so I wouldn't define the scull implementation "flawed" */
MOD_DEC_USE_COUNT;
return 0;
}
答案 3 :(得分:1)
希望一次只允许一个进程访问该设备
您不能:fork()
无法阻止单个进程打开一个设备,因为在fork()之后,子进程从父进程继承文件描述符,允许父进程和子进程读取/ write / ioctl设备。此外,exec()
会将您设备的文件描述符传输到另一个程序。
不要忘记Unix套接字可用于将文件描述符传递给您的设备到另一个进程(参见SCM_RIGHTS
)。
答案 4 :(得分:-1)
您正在查看错误的问题。如果硬件无法处理并发读/写,那么由驱动程序来强制执行。驱动程序是可以访问硬件的单个进程。驱动程序允许以线程安全的方式访问自身。来自用户空间的读/写不应该直接进入硬件,它们应该由驱动程序处理,并且驱动程序处理硬件所需的硬件。例如,write()处理程序可以只将数据转储到队列中并设置一个标志,这样你的write_hardware无限循环可以接受它,并且当它可以这样做时实际将它写入硬件。