Linux 设备驱动程序访问控制

Linux Device Driver Access Control

我正在为一些新颖的硬件实现设备驱动程序,并希望一次只允许一个进程访问该设备。并发 read/write 操作会使硬件混乱到很可能需要进行硬重置的程度。我还有以下问题:

  1. 在示例代码 from Linux Device Drivers 中,open() 调用使用了锁,但 close() 没有。这里是不是仍然存在竞争条件,或者 scull_s_count 的递减保证是原子的?基本上,在这个例子中,我想知道如果一个进程试图打开设备而另一个进程正在结束并关闭它会发生什么。

  2. 我假设我不需要在 read() 中检查打开标志的状态(我正在做类似于示例 scull_s_count 的事情)和 write() 调用,因为进入这些调用的唯一方法是用户空间应用程序是否已经通过成功调用 open() 接收到 fd。这个假设正确吗?

感谢 tadman 的评论,我对内核的 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() 分配的更大结构的一部分。结果,它被初始化为零。

因此,这里的代码表明锁的目的是防止多个processes/threads同时打开设备,而open_flag是一个[=20] =] 防止了我在上面的问题 1 中担心的竞争条件。这个实现是否足够?另外,我还在寻找问题2的答案。

示例代码使用自旋锁,但互斥锁是否更合适?代码部分相对较小,几乎没有争用,因此进入睡眠和唤醒的性能可能低于旋转。 lock/mutex 始终从用户上下文访问,因此您应该可以安全入睡。

你举的例子确实有问题。减量绝对不能保证是原子的,而且几乎肯定不会。

但实际上,我认为没有 compiler/CPU 组合会生成可能失败的代码。可能发生的最坏情况是一个 CPU 核心可以完成关闭,然后另一个核心可以调用打开并恢复忙碌,因为它有一个陈旧的标志缓存值。

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(),该描述符将被继承到新进程中。尽管存在上述 "single open" 限制,但多个 进程 可以同时打开设备。

因此您仍然必须在驱动程序的文件操作中保持线程安全,因为用户仍然可以有多个 threads/processes 同时打开设备并进行同时调用。如果您已经确保安全,为什么要阻止用户这样做呢?也许他们知道自己在做什么,并且会确保他们的驱动程序的多个开启者 "take turns" 而不会调用冲突?

同时考虑在 open 调用中使用 O_EXCL 标志使单个 open 可选的可能性。

你看错问题了。如果硬件无法处理并发 reads/writes,则由驱动程序强制执行。驱动程序是可以访问硬件的单个进程。驱动程序允许以线程安全的方式访问自身。来自用户空间的 read/writes 不应该直接进入硬件,它们应该由驱动程序处理,并且驱动程序根据硬件要求处理硬件。例如, write() 处理程序可以将数据转储到队列中并设置一个标志,以便您的 write_hardware 无限循环可以获取它并在可以这样做时实际将其写入硬件。

我对 linux 内核编程有点生疏,但是,同时使用原子和自旋锁对我来说看起来有点开销。

want to only allow one process to have access to the device at a time

如果这就是您所需要的,那么 scull 实现工作得很好,scull 驱动程序的开放实现中的自旋锁确保一次只有一个进程获得有效的文件句柄。

我认为我很确定(没有检查)头骨示例不在释放(关闭)中使用锁,因为只有打开它的进程可以关闭它,如果文件句柄无效,其他进程将无法进入发布代码。

The example code uses a spinlock, but would a mutex be more appropriate?

自旋锁实现速度更快,足以完成此任务。

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;
}

你和其他提供答案的人都正确地认为这个例子是有缺陷的,TrentP 是正确的,如果你使用像 test_and_set_bit() 这样的原子位操作(或者你可以使用 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() 函数不需要更改。

现在回答你的实际问题:

  1. 是的,存在竞争条件并且递减不是原子的。如果一个进程试图打开设备而另一个进程正在关闭它,则可能会发生许多不好的事情。例如,您的部分清理代码可能 运行 与来自并发打开的设备初始化代码同时出现,这可能导致几乎任何可以想象到的令人讨厌的结果。

  2. 是的,你的假设是正确的。您不需要在 read/write 方法中进行任何检查。

补充说明:

  • 问题中显示的代码是不够的,因为 close() 中没有排序;您需要在 close() 中的 atomic_set() 之前添加 smp_mb_before_atomic(),类似于上面的代码。但是,如果您简单地使用 test_and_set_bit() 和 TrentP 组合原子读取和设置,则不需要锁。

  • spinlock vs mutex:这个决定非常简单地取决于你是否需要在锁下休眠,即如果你的初始化代码包含任何可能导致进程休眠的东西,那么你应该使用一个互斥锁,而如果你的初始化只是设置一些变量然后再次释放锁,那么自旋锁是非常明智的,因为它比互斥锁更轻量级。鉴于这是你的 open/close,即没有什么性能关键,只使用互斥锁而不用担心代码是否会休眠就完全没问题了。但是,如果您完全放弃锁并仅使用所示的 test_and_set_bit(),代码会好得多。

want to only allow one process to have access to the device at a time

你不能:fork()无法阻止单个进程打开一个设备,因为在 fork() 之后,子进程从其父进程继承文件描述符,允许父进程和子进程read/write/ioctl 设备。此外 exec() 会将您设备的文件描述符传输到另一个程序。

并且不要忘记 Unix 套接字可用于将文件描述符传递给您的设备到另一个进程(参见 SCM_RIGHTS)。