在Linux内核模块中实现轮询

14
我有一个简单的字符设备驱动程序,允许您从自定义硬件设备中读取数据。它使用DMA从设备内存复制数据到内核空间(然后再传输给用户)。
“read”调用非常简单。它启动DMA写入,然后等待等待队列。当DMA完成时,中断处理程序设置标志并唤醒等待队列。需要注意的重要事项是,我可以随时开始DMA,即使设备没有提供数据。DMA引擎将保持等待状态,直到有数据可复制。这很有效。我可以在用户空间实现一个简单的阻塞读取调用,并且它的行为符合我的预期。
我想要实现“poll”,以便我可以在用户空间使用“select”系统调用,同时监视此设备和套接字。
我能找到的大多数关于“poll”的资源都说要:
  1. 为可能指示状态更改的每个等待队列调用poll_wait
  2. 返回一个比特掩码,指示数据是否可用

第二部分让我感到困惑。我看到的大多数例子都有一种简单的方法(指针比较或状态位)来检查数据是否可用。在我的情况下,除非我启动DMA,否则数据将永远不可用,即使我这样做了,数据也不会立即可用(设备实际上可能需要一些时间才能获得数据并完成DMA)。

那么应该如何实现呢?poll函数是否应该启动DMA,以便数据最终变得可用?我想这会破坏我的read函数。

2个回答

10

免责声明

好的,这是一个很好的架构问题,并且它暗示了您的硬件和期望的用户空间界面的一些假设。因此,让我尝试一下猜测在您的情况下哪种解决方案最好。

设计

考虑到您没有提到write()操作,我进一步假设您的硬件一直在产生新数据。如果是这样,您提到的设计可能正是困扰您的原因:

read调用非常简单。它启动DMA写入,然后等待等待队列。

这正是阻止您以常规、常用(并且可能是您所期望的)方式使用驱动程序的原因。让我们跳出思维定势,首先想出期望的用户界面(您希望如何从用户空间使用驱动程序)。下面的情况在这里是常用的并且足够的(从我的角度来看):

  1. poll()您的设备文件以等待新数据到达
  2. read()您的设备文件以获取到达的数据
现在您可以看到,数据请求(到DMA)不应该由read()操作启动。正确的解决方案是在驱动程序中连续读取数据(没有来自用户空间的任何触发),并将其存储在内部,当用户要求您的驱动程序提供已存储在内部的数据以进行read()操作时-向用户提供数据。如果驱动程序内部没有存储数据-用户可以使用poll()操作等待新数据到达。
正如您所看到的,这是众所周知的生产者-消费者问题。您可以使用循环缓冲区将硬件中的数据存储在驱动程序中(因此当缓冲区满时有意丢失旧数据以防止缓冲区溢出情况)。因此,生产者(DMA)将写入该RX环形缓冲区的头部,而消费者(从用户空间执行read()的用户)将从该RX环形缓冲区的尾部读取。

代码参考

这整个情况让我想起了串行控制台驱动程序1 2。因此,在您的驱动程序实现中考虑使用串行API(如果您的设备确实是串行控制台)。例如,请参阅drivers/tty/serial/atmel_serial.c驱动程序。我对UART API并不是很熟悉,所以无法精确告诉您那里发生了什么,但乍一看似乎并不太难,因此您可能可以从那段代码中找到一些关于您的驱动程序设计的灵感。
如果您的驱动程序不应使用串行API,则可以使用以下驱动程序作为参考:

互补

回答您在评论中的问题:

您是否建议当没有可用数据时,read 调用 poll 并且 read 应该阻塞?

首先,您需要决定您想提供:

为了讨论,我们假设您想在驱动程序中提供这两个选项。在这种情况下,您应该在 open() 调用中检查 flags 参数是否包含 O_NONBLOCK 标志。根据 man 2 open:

O_NONBLOCKO_NDELAY

如果可能,以非阻塞模式打开文件。不会有任何对于返回的文件描述符进行的操作会导致调用进程等待。关于FIFO(命名管道)的处理,请参见fifo(7)。有关O_NONBLOCK与强制文件锁和文件租约一起使用的影响的讨论,请参见fcntl(2)

现在,当您了解用户选择的模式时,可以在驱动程序中执行以下操作:

  1. 如果open()中的flags不包含这些标志,则可以进行阻塞read()(即如果数据不可用,则等待DMA事务完成,然后返回新数据)。
  2. 但是,如果open()标志中存在O_NONBLOCK,并且循环缓冲区中没有可用数据,则应从read()调用中返回EWOULDBLOCK错误代码。

来自man 2 read:

EAGAINEWOULDBLOCK

文件描述符fd指向一个套接字,并已标记为非阻塞(O_NONBLOCK),读取将会阻塞。POSIX.1-2001允许返回这两种错误中的任何一种,并且不要求这些常量具有相同的值,因此可移植应用程序应检查这两种可能性。

您还可以阅读下一篇文章以更好地了解相应的接口:

[1] POSIX操作系统串行编程指南

[2] 串行编程HOWTO

补充2

我需要一种后台任务,不断地从设备中读取数据并填充环形缓冲区。使用poll很简单 - 只需检查该缓冲区是否有内容,但是read更加困难,因为它可能需要等待环形缓冲区中的内容被发布。

例如,请查看drivers/char/virtio_console.c驱动程序实现。

  1. poll()函数中:执行poll_wait()函数(等待新数据到达)
  2. 接收数据的中断处理程序中:执行wake_up_interruptible()函数(唤醒pollread操作)
  3. read()函数中:
    • 如果端口没有数据
      • 如果在open()操作中设置了O_NONBLOCK标志:立即返回-EAGAIN=-EWOULDBLOCK
      • 否则,如果进行阻塞读取:执行wait_event_freezable()函数等待新数据到达
    • 如果端口有数据:返回缓冲区中的数据

另请参阅相关问题: 如何将投票功能添加到内核模块代码中?


非常有帮助。我一直在考虑需要在后台始终启动DMA传输。不过,我有一个问题 - 您是在建议当没有数据可用时,read调用poll并且read应该阻塞吗? - zmb
我为你的问题添加了“补充”部分,并提供了(希望是)答案。 - Sam Protsenko
1
我想我明白了。我需要一种后台任务,它不断地从设备中读取数据并填充环形缓冲区。poll现在很简单 - 只需检查该缓冲区是否有任何内容,但是read更困难,因为它可能需要等待某些内容被发布到环形缓冲区中。 - zmb
是的,你说得对。请参见补充2,了解如何实现pollread的代码示例。 - Sam Protsenko

0

read和其他在特殊设备(字符或块设备)上的函数一样,poll函数可以以一种展示所需行为的方式实现。唯一的限制是实现必须通过等待队列来“唤醒”poll(),但这并不限制可能的行为

此外,poll行为不需要与read/write绑定![同样,这只适用于特殊设备。]

因此,将您认为对任务更有用的行为强加在poll


我的一个实践例子(可能适用于你的情况):

我的同事通过字符设备实现了消息的循环缓冲区。可以通过mmap()读取消息。当至少有一页被消息填满时,poll会“唤醒”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接