Chinaunix首页 | 论坛 | 博客

分类: LINUX

2009-11-09 10:02:41

察觉数据可访问性
一些用户应用程序非常复杂并不满足于旧有的open()/read()/close()调用, 它们希望在新数据达到时或者驱动准备接收新数据时可以同步或异步的被通知到. 本节我们考察两个察觉数据可访问的字符驱动函数: poll()和fasync(), 前一个是同步的, 后者是异步的. 察觉数据可访问不仅仅和简单的CMOS内存相关,所以我们先以流行的用户空间应用程序: X Windows Server来说明一些使用场景.

Poll

考虑下面来自X Windows源代码(可从下载)中处理鼠标事件的片段:
xc/programs/Xserver/hw/xfree86/input/mouse/mouse.c:
  case PROT_THINKING:           /* ThinkingMouse */
  /* This mouse may send a PnP ID string, ignore it. */
  usleep(200000); xf86FlushInput(pInfo->fd);
  /* Send the command to initialize the beast. */
  for (s = "E5E5"; *s; ++s) {
    xf86WriteSerial(pInfo->fd, s, 1);
    if ((xf86WaitForInput(pInfo->fd, 1000000) <= 0))
    break;
    xf86ReadSerial(pInfo->fd, &c, 1);
    if (c != *s) break;
  }
  break;
本来, 代码发送一个初始化命令给鼠标,轮询直至察觉到有输入数据,读取来自设备的响应数据. 如果深入到Xf86WaitForInput()里面,你会发现有一个select()系统调用:
xc/programs/Xserver/hw/xfree86/os-support/shared/posix_tty.c:
int
xf86WaitForInput(int fd, int timeout)
{
  fd_set readfds;
  struct timeval to;
  int r;

  FD_ZERO(&readfds);
  if (fd >= 0) {
    FD_SET(fd, &readfds);
  }

  to.tv_sec  = timeout / 1000000;
  to.tv_usec = timeout % 1000000;

  if (fd >= 0) {
    SYSCALL (r = select(FD_SETSIZE, &readfds, NULL, NULL, &to));
  } else {
    SYSCALL (r = select(FD_SETSIZE, NULL, NULL, NULL, &to));
  }

  if (xf86Verbose >= 9)
    ErrorF ("select returned %d\n", r);

  return (r);
}
你可提供大量文件描述符给select(),让它监视它们直到相关联的数据状态发生改变. 你也可以请求超时来覆盖可用数据. 如果你请求NULL超时, select()永远阻塞. 前面代码中的select()导致X服务器在超时时间内轮询得到连接的鼠标数据
Linux支持另外的系统调用poll(),它的语义和select()类似. 2.6内核支持一个新的非POSIX系统调用epoll(),它是poll()的升级超集. 所有这些系统调用都依赖于相同的底层字符驱动函数poll().

大多数I/O系统调用是兼容POSIX标准也不是特定Linux的, 但内在的驱动函数确是特定于操作系统的. 在Linux上poll()驱动函数是select()系统调用的支撑函数. 在前面的X 服务器语义中, 鼠标驱动的poll()函数大概是这样的:

static DECLARE_WAIT_QUEUE_HEAD(mouse_wait); /* Wait Queue */

static unsigned int
mouse_poll(struct file *file, poll_table *wait)
{
  poll_wait(file, &mouse_wait, wait);
  spin_lock_irq(&mouse_lock);

  /* See if data has arrived from the device or
     if the device is ready to accept more data */
  /* ... */
  spin_unlock_irq(&mouse_lock);

  /* Availability of data is detected from interrupt context */
  if (data_is_available()) return(POLLIN | POLLRDNORM);

  /* Data can be written. Not relevant for mice */
  if (data_can_be_written()) return(POLLOUT | POLLWRNORM);

  return 0;
}
当xf86waitForinput()调用select(), 通用内核轮询实现(定义在fs/select.c)调用mouse_poll(). mouse_poll()带有两个参数: 文件指针(struct file*)和指向内核数据结构poll_table的指针. poll_table是要轮询得数据的设备驱动拥有的等待队列表.

mouse_poll()使用库函数poll_wait()添加等待队列(mouse_wait)到内核poll_table并进入休眠. 设备驱动通常拥有一些等待队列,它们一直阻塞到检测到数据状况发生变化.这个状态可以是来自设备的新数据到来,驱动想传递新数据给应用程序或者设备准备就绪来接收心数据. 这些状况通常但不是总是由驱动的中断处理函数检测到. 当鼠标驱动的中断处理函数察觉到鼠标运动,就调用wake_up_interruptible(&mouse_wait)来唤醒休眠的mouse_poll().

如果数据状况没有变化, poll()函数返回0. 如果驱动做好准备发送至少一字节数据给应用程序,就返回POLLIN|POLLRDNORM.如果驱动准备从应用程序接收至少一字节数据,就返回POLLOUT|POLLWRNORM. 因此,如果没有鼠标移动,mouse_poll()返回0并且调用线程进入休眠. 当鼠标中断处理函数察觉到设备数据,内核将再次调用mouse_poll()并唤醒mouse_wait队列,这次mouse_poll()返回POLLIN|POLLRDNORM,所以select()调用以及Xf86WaitForInput()返回正数值. X服务器的鼠标处理函数(xc/programs/Xserver/hw/xfree86/input/mouse/mouse.c)继续从鼠标中读数据.

轮询驱动的应用程序通常对驱动特征而不是设备特征感兴趣. 例如,依靠缓冲区的驱动可能在设备之前做好准备接收来自应用程序的新数据.


Fasync
一些应用程序为了性能原因希望能从设备驱动中得到异步通知。假设Linux起搏器上的应用程序忙于处理复杂的计算但是希望在自动测量接口上的数据一到来时就能被通知到。select()/poll()机制在这种情况不起作用因为它阻塞了计算.应用程序需要的是一种异步事件报告. 如果自动测量驱动一旦检测到起搏器的数据就可以异步分发一个信号(通常是SIGIO)的话,应用程序就可以使用一个信号处理函数捕获到并执行相应的代码流.

作为真实的异步通知例子,我们退回到之前的当输入设备检测到数据就请求警告给X服务器. 看下X服务器代码片段:

xc/programs/Xserver/hw/xfree86/os-support/shared/sigio.c:
int xf86InstallSIGIOHandler(int fd, void (*f)(int, void *),
                            void *closure)
{
  struct sigaction sa;
  struct sigaction osa;

  if (fcntl(fd, F_SETOWN, getpid()) == -1) {
    blocked = xf86BlockSIGIO();

  /* O_ASYNC is defined as SIGIO elsewhere by the X server */
  if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_ASYNC) == -1) {
    xf86UnblockSIGIO(blocked); return 0;
  }
  sigemptyset(&sa.sa_mask);
  sigaddset(&sa.sa_mask, SIGIO);
  sa.sa_flags = 0;
  sa.sa_handler = xf86SIGIO;
  sigaction(SIGIO, &sa, &osa);
  /* ... */
  return 0;
}

static void
xf86SIGIO(int sig)
{
  /* Identify the device that triggered generation of this
     SIGIO and handle the data arriving from it */
  /* ... */
}
可以看到X服务器做了如下事情:
1. 调用fcntl(F_SETOWN). fcntl()系统调用用来操作文件描述符的行为. F_SETOWN设置描述符的归属关系给调用进程. 当内核需要知道在哪里发送异步信号时这是必须的.该步骤对设备驱动是透明的.
2. 调用fcntl(F_SETFL). F_SETFL请求驱动传递SIGIO给应用程序,无论何时读数据或者如果驱动准备完毕接收更多的应用程序数据. 对它的调用导致fasync()驱动函数的调用. 这个函数的责任就是从进程列表中添加或删除要被传递SIGIO的入口,最后fasync()使用了内核库函数faysnc_helper().
3. 实现SIGIO信号处理函数xf86SIGIO(). 并使用sigaction()安装它. 当底层的输入设备驱动检测到数据状态的改变时,就分发一个SIGIO给注册的请求接着触发xf86sigio()的执行. 如果信号处理函数服务于多个设备的异步事件, 就需要另外的机制,如处理函数内部的select()调用来识别出事件对应到的设备. 字符驱动调用kill_fasync()发送SIGIO给进程. 为通知读事件, POLLIN作为参数传给kill_fasync(),而POLLOUT作为参数时则是为了通知写事件.

为了看清驱动层如何实现异步通知链, 我们来看下一个假想的输入设备驱动的fasync()函数:
/* This is invoked by the kernel when the X server opens this
 * input device and issues fcntl(F_SETFL) on the associated file
 * descriptor. fasync_helper() ensures that if the driver issues a
 * kill_fasync(), a SIGIO is dispatched to the owning application.
 */
static int
inputdevice_fasync(int fd, struct file *filp, int on)
{
  return fasync_helper(fd, filp, on, &inputdevice_async_queue);
}
/* Interrupt Handler */
irqreturn_t
inputdevice_interrupt(int irq, void *dev_id)
{
  /* ... */
  /* Dispatch a SIGIO using kill_fasync() when input data is
     detected. Output data is not relevant since this is a read-only
     device */
  wake_up_interruptible(&inputdevice_wait);
  kill_fasync(&inputdevice_async_queue, SIGIO, POLL_IN);
  /* ... */
  return IRQ_HANDLED;
}
查看SIGIO传递太复杂,考虑下tty驱动的情形, 对它感兴趣的应用程序在不同场景下得到通知:
1. 底层驱动并没有准备好接收应用程序数据,就让调用进程进入休眠状态. 当之后的驱动中断处理函数判断出设备可以接收更多数据时,它就唤醒应用程序并调用kill_async(POLLOUT).
2. 如果接收到换行符, tty层调用kill_fasync(POLLIN);
3. 当驱动检测到设备中有足够数据字节到来,就唤醒睡眠的读线程。 接着就发送信息给进程调用kill_fasync(POLLIN).

阅读(902) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~