【问题标题】:Linux Device Driver Access ControlLinux 设备驱动程序访问控制
【发布时间】:2018-04-30 19:23:18
【问题描述】:

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

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

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

感谢 tadman 的 cmets,我对内核的 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_flagatomic_t 的事实防止了我的竞争条件关注上述问题1。这种实现是否足够?另外,我仍在寻找问题 2 的答案。

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

【问题讨论】:

  • 如果你有一个原子标志,你就不会有竞争条件。
  • 是否有 POSIX 原子标志?我知道 C++11 中的 std::atomic_flag,但这是一个用 C 编写的 Linux 设备驱动程序。
  • 即使粗略搜索内核中的原子操作也会得到results like this
  • 谢谢。现在将我的标志转换为 atomic_t。稍后将在此处发布示例代码。
  • @tadman,如果你有时间,看看我更新的问题。感谢您的帮助。

标签: c linux linux-kernel linux-device-driver


【解决方案1】:

你所指的例子确实有缺陷。绝对不能保证减量是原子的,几乎可以肯定不会。

但实际上,我认为编译器/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 标志以使单个打开可选的可能性。

【讨论】:

  • 所以我查看了代码,发现还有其他机制可以防止共享 fd 进行同时读/写。我想我试图解决的真正问题是多个进程,每个进程都可以使用自己的 fds 访问设备,我认为我已经实现的阻止了这种情况。
  • 这仍然可能发生,因为在您的打开处理程序中 atomic_read 和 atomic_set 之间可能存在竞争。您需要使用设置和返回值的原子操作之一。
  • @domen,对不起,我没有看到比赛,你能详细说明一下吗?
  • 没有真正的比赛,因为锁保护了 open() 中的非原子检查然后设置。但它有点违背了原子操作的目的,即非原子地使用它们,然后用自旋锁修复它。
  • 抱歉,没有种族。我同意 TrentP。
【解决方案2】:

您和提供答案的其他人都是正确的,该示例存在缺陷,而 TrentP 是正确的,如果您使用 test_and_set_bit() 之类的原子位操作(或者您可以使用 @ 987654322@等)。

但是,他的回答也不完全正确,因为它不处理 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. 是的,你的假设是正确的。您不需要对读/写方法进行任何检查。

补充说明:

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

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

【讨论】:

  • 它被复制是的,但它也被更正了,我是这么说的。请阅读代码块上方的段落,我说 TrentP 的答案不正确,并解释了原因以及“固定解决方案”,然后引用了相同的代码块但进行了更正。当然,这样做比说“请查看 TrentP 的代码块并在第 13 行(或者它是哪个行号)插入 smp_mb_before_atomic()”更好?
  • 我不认为复制别人的答案来更改一行代码是 SO 上公认的做法。在这种情况下,您对答案发表评论。
  • 这对我来说似乎有点傻,但很公平。但是,与其他人不同,我确实也回答了所有 OP 的问题,所以在评论中这样做会很奇怪。为他的问题提供答案,然后单独发表评论以纠正您的答案似乎也很愚蠢(如果没有我放入的记忆障碍,您的答案是完全错误的!)重写从头开始回答。而且我确实提到显示的答案来自您,所以我真的没有看到问题......
【解决方案3】:

希望一次只允许一个进程访问设备

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

别忘了 Unix 套接字可用于将文件描述符传递给您的设备到另一个进程(请参阅SCM_RIGHTS)。

【讨论】:

    【解决方案4】:

    我对 linux 内核编程有点生疏,但是同时使用 atomic 和 spinlock 对我来说有点开销。

    希望一次只允许一个进程访问设备

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

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

    示例代码使用自旋锁,但互斥锁是否更合适?

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

    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 问题是互斥锁或自旋锁,而不是自旋锁或原子。此外,如果只用于打开一些毫秒的性能是无关紧要的
    【解决方案5】:

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

    【讨论】:

    • 在特伦特的回答中查看我的评论。还有其他机制来处理并发读/写。目前比较麻烦的是两个进程可以同时打开设备,这是没有意义的,因为进程A可以读取一些数据,而进程B可以读取另一组数据。我需要防止这种情况发生,因为单个进程需要连续的数据流,直到设备完全完成并调用 close()。
    • 这不是你问的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-13
    • 1970-01-01
    相关资源
    最近更新 更多