Linux 设备驱动学习笔记 - 高级字符驱动操作篇 - Note.5[ poll 和 select \异步通知]

Linux 设备驱动学习笔记 - 高级字符驱动操作篇 - Note.5[ poll 和 select \异步通知]

6.3 poll 和 select

使用非阻塞 I/O 的应用程序常常使用 poll, select, 和 epoll 系统调用. poll, select 和 epoll 本质上有相同的功能:
每个允许一个进程来决定它是否可读或者写一个或多个文件而不阻塞. 这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写.
因此, 它们常常用在必须使用多输入输出流的应用程序, 而不必粘连在它们任何一个上. 相同的功能常常由多个函数提供,
因为 2 个是由不同的团队在几乎相同时间完成的: select 在 BSD Unix 中引入, 而 poll 是 System V 的解决方案. epoll 调用添加在 2.5.45,
作为使查询函数扩展到几千个文件描述符的方法.

支持任何一个这些调用都需要来自设备驱动的支持. 这个支持(对所有 3 个调用)由驱动的 poll 方法调用. 这个方法由下列的原型:

unsigned int (*poll) (struct file *filp, poll_table *wait); 

这个驱动方法被调用, 无论何时用户空间程序进行一个 poll, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符.
这个设备方法负责这 2 步:
(1)在一个或多个可指示查询状态变化的等待队列上调用 poll_wait. 如果没有文件描述符可用作 I/O,
内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符.
(2)返回一个位掩码, 描述可能不必阻塞就立刻进行的操作.

这 2 个操作常常是直接的, 并且趋向与各个驱动看起来类似. 但是, 它们依赖只能由驱动提供的信息, 因此, 必须由每个驱动单独实现.

poll_table 结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 <linux/poll.h>中声明,

// poll_table 在内核中的解释
{
	/*
 * Do not touch the structure directly, use the access functions
	 * poll_does_not_wait() and poll_requested_events() instead.
	 */
	typedef struct poll_table_struct {
			poll_queue_proc _qproc;
			unsigned long _key;
	} poll_table;

	static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
	{
		if (p && p->_qproc && wait_address)
			p->_qproc(filp, wait_address, p);
	}
}

这个文件必须被驱动源码包含. 驱动编写者不必要知道所有它内容并且必须作为一个不透明的对象使用它;
它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它, 并且可改变 poll 操作状态.
驱动增加一个等待队列到 poll_table 结构通过调用函数 poll_wait:

void poll_wait (struct file *, wait_queue_head_t *, poll_table *); 

poll 方法的第 2 个任务是返回位掩码, 它描述哪个操作可马上被实现; 这也是直接的.
如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(通过 <linux/poll.h> 定义)用来指示可能的操作:
实际上, epoll 是一组 3 个调用, 都可用来获得查询功能. 但是, 由于我们的目的, 我们可认为它是一个调用.

(1)POLLIN
如果设备可被不阻塞地读, 这个位必须设置.
(2)POLLRDNORM
这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).

(3)POLLRDBAND
这个位指示带外数据可用来从设备中读取. 当前只用在 Linux 内核的一个地方( DECnet 代码 )并且通常对设备驱动不可用.

(4)POLLPRI
高优先级数据(带外)可不阻塞地读取. 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.

(5)POLLHUP
当读这个设备的进程见到文件尾, 驱动必须设置 POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同 selcet 功能所规定的.

(6)POLLERR
一个错误情况已在设备上发生. 当调用 poll, 设备被报告位可读可写, 因为读写都返回一个错误码而不阻塞.

(7)POLLOUT
这个位在返回值中设置, 如果设备可被写入而不阻塞.

(8)POLLWRNORM
这个位和 POLLOUT 有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).

(9)POLLWRBAND
如同 POLLRDBAND , 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.

应当重复一下 POLLRDBAND 和 POLLWRBAND 仅仅对关联到 socket 的文件描述符有意义: 通常设备驱动不使用这些标志.
poll 的描述使用了大量在实际使用中相对简单的东西. 考虑 poll 方法的 scullpipe 实现:

static unsigned int scull_p_poll(struct file *filp, poll_table *wait) 
{ 
	struct scull_pipe *dev = filp->private_data; 
	unsigned int mask = 0; 
	
	/* 
	* The buffer is circular; it is considered full 
	* if "wp" is right behind "rp" and empty if the 
	* two are equal. 
	*/ 
	down(&dev->sem); 
	poll_wait(filp, &dev->inq, wait); 
	poll_wait(filp, &dev->outq, wait); 
	
	if (dev->rp != dev->wp) 
		mask |= POLLIN | POLLRDNORM; /* readable */ 
	
	if (spacefree(dev)) 
		mask |= POLLOUT | POLLWRNORM; /* writable */ 
	
	up(&dev->sem); 
	
	return mask; 
} 

这个代码简单地增加了 2 个 scullpipe 等待队列到 poll_table, 接着设置正确的掩码位, 根据数据是否可以读或写.
所示的 poll 代码缺乏文件尾支持, 因为 scullpipe 不支持文件尾情况. 对大部分真实的设备,
poll 方法应当返回 POLLHUP 如果没有更多数据(或者将)可用. 如果调用者使用 select 系统调用, 文件被报告为可读. 不管是使用 poll 还是 select, 应用程序知道它能够调用 read 而不必永远等待, 并且 read 方法返回 0 来指示文件尾.

对于 真正的 FIFO, 读者见到一个文件尾当所有的写者关闭文件, 而在 scullpipe 中读者永远见不到文件尾.
这个做法不同是因为 FIFO 是用作一个 2 个进程的通讯通道, 而 scullpipe 是一个垃圾桶, 人人都可以放数据只要至少有一个读者. 更多地,
重新实现内核中已有的东西是没有意义的, 因此我们选择在我们的例子里实现一个不同的做法.

与 FIFO 相同的方式实现文件尾就意味着检查 dev->nwwriters, 在 read 和 poll 中, 并且报告文件尾(如刚刚描述过的)如果没有进程使设备写打开.

使用这个实现方法, 如果一个读者打开 scullpipe 设备在写者之前, 它可能见到文件尾而没有机会来等待数据.
解决这个问题的最好的方式是在 open 中实现阻塞, 如同真正的 FIFO 所做的.

6.3.1 与 read 和 write 的交互

6.3.1.1 从设备中读数据

(1)如果在输入缓冲中有数据, read 调用应当立刻返回, 没有可注意到的延迟, 即便数据少于应用程序要求的, 并且驱动确保其他的数据会很快到达.你可一直返回小于你被请求的数据, 如果因为任何理由而方便这样(我们在 scull 中这样做), 如果你至少返回一个字节. 在这个情况下, poll 应当返回 POLLIN|POLLRDNORM.

(2)如果在输入缓冲中没有数据, 缺省地 read 必须阻塞直到有一个字节. 如果 O_NONBLOCK 被置位, 另一方面, read 立刻返回 -EAGIN (尽管一些老版本 SYSTEM V 返回 0 在这个情况时). 在这些情况中, poll 必须报告这个设备是不可读的直到至少一个字节到达. 一旦在缓冲中有数据, 我们就回到前面的情况.

(3)如果我们处于文件尾, read 应当立刻返回一个 0, 不管是否阻塞. 这种情况 poll 应该报告 POLLHUP.

6.3.1.2 写入设备

(1)如果在输出缓冲中有空间, write 应当不延迟返回. 它可接受小于这个调用所请求的数据, 但是它必须至少接受一个字节. 在这个情况下,poll 报告这个设备是可写的, 通过返回 POLLOUT|POLLWRNORM.

(2)如果输出缓冲是满的, 缺省地 write 阻塞直到一些空间被释放. 如果 O_NOBLOCK 被设置, write 立刻返回一个 -EAGAIN(老式的 System V Unices 返回 0). 在这些情况下, poll 应当报告文件是不可写的. 另一方面, 如果设备不能接受任何多余数据, write 返回 -ENOSPC(“设备上没有空间”), 不管是否设置了O_NONBLOCK.

(3)在返回之前不要调用 wait 来传送数据, 即便当 O_NONBLOCK 被清除. 这是因为许多应用程序选择来找出一个 write 是否会阻塞. 如果设备报告可写, 调用必须不阻塞. 如果使用设备的程序想保证它加入到输出缓冲中的数据被真正传送, 驱动必须提供一个 fsync 方法.

例如, 一个可移除的设备应当有一个 fsnyc 入口. 尽管有一套通用的规则, 还应当认识到每个设备是唯一的并且有时这些规则必须稍微弯曲一下. 例如, 面向记录的设备(例如磁带设备)无法执行部分写.

6.3.1.3 刷新挂起的输出

我们已经见到 write 方法如何自己不能解决全部的输出需要. fsync 函数, 由同名的系统调用而调用, 填补了这个空缺. 这个方法原型是:
int (*fsync) (struct file *file, struct dentry *dentry, int datasync);

如果一些应用程序需要被确保数据被发送到设备, fsync 方法必须被实现为不管 O_NONBLOCK 是否被设置.对 fsync 的调用应当只在设备被完全刷新时返回(即, 输出缓冲为空), 即便这需要一些时间. datasync 参数用来区分 fsync 和 fdatasync 系统调用; 这样, 它只对文件系统代码有用, 驱动可以忽略它.

fsync 方法没有不寻常的特性. 这个调用不是时间关键的, 因此每个设备驱动可根据作者的口味实现它. 大部分的时间, 字符驱动只有一个 NULL 指针在它们的 fops 中. 阻塞设备, 另一方面, 常常实现这个方法使用通用的 block_fsync, 它接着会刷新设备的所有的块.

6.3.2. 底层的数据结构

poll 和 select 系统调用的真正实现是相当地简单, 对那些感兴趣于它如何工作的人; epoll 更加复杂一点但是建立在同样的机制上.
无论何时用户应用程序调用 poll, select, 或者 epoll_ctl,[24]24 内核调用这个系统调用所引用的所有文件的 poll 方法,
传递相同的 poll_table 到每个. poll_table 结构只是对一个函数的封装, 这个函数建立了实际的数据结构.
那个数据结构, 对于 poll和 select, 是一个内存页的链表, 其中包含 poll_table_entry 结构.
每个 poll_table_entry 持有被传递给 poll_wait 的 struct file 和 wait_queue_head_t 指针, 以及一个关联的等待队列入口.
对 poll_wait 的调用有时还添加这个进程到给定的等待队列. 整个的结构必须由内核维护以至于这个进程可被从所有的队列中去除,
在 poll 或者 select 返回之前.

如果被轮询的驱动没有一个指示 I/O 可不阻塞地发生, poll 调用简单地睡眠直到一个它所在的等待队列(可能许多)唤醒它.

在 poll 实现中有趣的是驱动的 poll 方法可能被用一个 NULL 指针作为 poll_table 参数. 这个情况出现由于几个理由.
如果调用 poll 的应用程序已提供了一个 0 的超时值(指示不应当做等待), 没有理由来堆积等待队列, 并且系统简单地不做它.
poll_table 指针还被立刻设置为 NULL 在任何被轮询的驱动指示可以 I/O 之后. 因为内核在那一点知道不会发生等待, 它不建立等待队列链表.

当 poll 调用完成, poll_table 结构被去分配, 并且所有的之前加入到 poll 表的等待队列入口被从表和它们的等待队列中移出.

我们试图在图 poll 背后的数据结构中展示包含在轮询中的数据结构; 这个图是真实数据结构的简化地表示,
因为它忽略了一个 poll 表地多页性质并且忽略了每个 poll_table_entry 的文件指针.

在此, 可能理解在新的系统调用 epoll 后面的动机. 在一个典型的情况中, 一个对 poll 或者 select 的调用只包括一组文件描述符,
所以设置数据结构的开销是小的. 但是, 有应用程序在那里, 它们使用几千个文件描述符. 在这时,
在每次 I/O 操作之间设置和销毁这个数据结构变得非常昂贵. epoll 系统调用家族允许这类应用程序建立内部的内核数据结构只一次, 并且多次使用它们.

6.4 异步通知

尽管阻塞和非阻塞操作和 select 方法的结合对于查询设备在大部分时间是足够的, 一些情况还不能被我们迄今所见到的技术来有效地解决.
让我们想象一个进程, 在低优先级上执行一个长计算循环, 但是需要尽可能快的处理输入数据. 如果这个进程在响应新的来自某些数据获取外设的报告, 它应当立刻知道当新数据可用时. 这个应用程序可能被编写来调用 poll 有规律地检查数据, 但是, 对许多情况, 有更好的方法. 通过使能异步通知, 这个应用程序可能接受一个信号无论何时数据可用并且不需要让自己去查询.

用户程序必须执行 2 个步骤来使能来自输入文件的异步通知. 首先, 它们指定一个进程作为文件的拥有者.
当一个进程使用 fcntl 系统调用发出 F_SETOWN 命令, 这个拥有者进程的 ID 被保存在 filp->f_owner 给以后使用.
这一步对内核知道通知谁是必要的. 为了真正使能异步通知, 用户程序必须设置 FASYNC 标志在设备中, 通过 F_SETFL fcntl 命令.

在这 2 个调用已被执行后, 输入文件可请求递交一个 SIGIO 信号, 无论何时新数据到达.
信号被发送给存储于 filp->f_owner 中的进程(或者进程组, 如果值为负值).
例如, 下面的用户程序中的代码行使能了异步的通知到当前进程, 给 stdin 输入文件:

signal(SIGIO, &input_handler); /* dummy sample; sigaction() is better */ 
fcntl(STDIN_FILENO, F_SETOWN, getpid()); 
oflags = fcntl(STDIN_FILENO, F_GETFL); 
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC); 

这个在源码中名为 asynctest 的程序是一个简单的程序, 读取 stdin. 它可用来测试 scullpipe 的异步能力. 这个程序和 cat 类似但是不结束于文件尾; 它只响应输入, 而不是没有输入.

注意,不是所有的设备都支持异步通知, 并且你可选择不提供它. 应用程序常常假定异步能力只对 socket 和 tty 可用.
输入通知有一个剩下的问题. 当一个进程收到一个 SIGIO, 它不知道哪个输入文件有新数据提供. 如果多于一个文件被使能异步地通知挂起输入的进程,
应用程序必须仍然靠 poll 或者 select 来找出发生了什么.

6.4.1 驱动的观点

对我们来说一个更相关的主题是设备驱动如何实现异步信号. 下面列出了详细的操作顺序, 从内核的观点:
(1)当发出 F_SETOWN, 什么都没发生, 除了一个值被赋值给 filp->f_owner.

(2)当 F_SETFL 被执行来打开 FASYNC, 驱动的 fasync 方法被调用. 这个方法被调用无论何时 FASYNC 的值在 filp->f_flags 中被改变来通知驱动这个变化, 因此它可正确地响应. 这个标志在文件被打开时缺省地被清除. 我们将看这个驱动方法的标准实现.

(3)当数据到达, 所有的注册异步通知的进程必须被发出一个 SIGIO 信号.

虽然实现第一步是容易的–在驱动部分没有什么要做的–其他的步骤包括维护一个动态数据结构来跟踪不同的异步读者; 可能有几个.
这个动态数据结构, 但是, 不依赖特殊的设备, 并且内核提供了一个合适的通用实现这样你不必重新编写同样的代码给每个驱动.
Linux 提供的通用实现是基于一个数据结构和 2 个函数(它们在前面所说的第 2 步和第 3 步被调用). 声明相关材料的头文件是<linux/fs.h>(这里没新东西), 并且数据结构被称为 struct fasync_struct. 至于等待队列, 我们需要插入一个指针在设备特定的数据结构中.

驱动调用的 2 个函数对应下面的原型:

int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa); 
void kill_fasync(struct fasync_struct **fa, int sig, int band);

fasync_helper 被调用来从相关的进程列表中添加或去除入口项, 当 FASYNC 标志因一个打开文件而改变. 它的所有参数除了最后一个,
都被提供给 fasync 方法并且被直接传递. 当数据到达时 kill_fasync 被用来通知相关的进程. 它的参数是被传递的信号(常常是 SIGIO)和 band,
这几乎都是 POLL_IN (但是这可用来发送"紧急"或者带外数据, 在网络代码里).
这是 scullpipe 如何实现 fasync 方法的:

static int scull_p_fasync(int fd, struct file *filp, int mode) 
{ 
	struct scull_pipe *dev = filp->private_data; 
	return fasync_helper(fd, filp, mode, &dev->async_queue); 
} 

显然所有的工作都由 fasync_helper 进行. 但是, 不可能实现这个功能在没有一个方法在驱动里的情况下, 因为这个帮忙函数需要存取正确的指向 struct fasync_struct (这里是 与 dev->async_queue)的指针, 并且只有驱动可提供这个信息.
当数据到达, 下面的语句必须被执行来通知异步读者. 因为对 sucllpipe 读者的新数据通过一个发出 write 的进程被产生,
这个语句出现在 scullpipe 的 write 方法中.

if (dev->async_queue) 
	kill_fasync(&dev->async_queue, SIGIO, POLL_IN); 

注意, 一些设备还实现异步通知来指示当设备可被写入时; 在这个情况, 当然, kill_fasnyc 必须被使用一个 POLL_OUT 模式来调用.
可能会出现我们已经完成但是仍然有一件事遗漏. 我们必须调用我们的 fasync 方法, 当文件被关闭来从激活异步读者列表中去除文件.
尽管这个调用仅当 filp->f_flags 被设置为 FASYNC 时需要, 调用这个函数无论如何不会有问题并且是常见的实现.
下面的代码行, 例如, 是 scullpipe 的 release 方法的一部分:

/* remove this filp from the asynchronously notified filp's */ 
scull_p_fasync(-1, filp, 0); 

这个在异步通知之下的数据结构一直和结构 struct wait_queue 是一致的, 因为 2 种情况都涉及等待一个事件.
区别是这个 struct file 被用来替代 struct task_struct. 队列中的结构 file 接着用来存取 f_owner, 为了通知进程.

6.5 移位一个设备

本章最后一个涉及的东西是 llseek 方法, 它有用(对于某些设备)并且容易实现. POLL_IN 是一个符号, 用在异步通知代码中;
它等同于 POLLIN | POLLRDNORM.

6.5.1 llseek 实现

llseek 方法实现了 lseek 和 llseek 系统调用. 我们已经说了如果 llseek 方法从设备的操作中缺失, 内核中的缺省的实现进行移位通过修改 filp->f_pos, 这是文件中的当前读写位置. 请注意对于 lseek 系统调用要正确工作, 读和写方法必须配合, 通过使用和更新它们收到的作为的参数的 offset 项.

你可能需要提供你自己的方法, 如果移位操作对应一个在设备上的物理操作. 一个简单的例子可在 scull 驱动中找到:

loff_t scull_llseek(struct file *filp, loff_t off, int whence) 
{ 
    struct scull_dev *dev = filp->private_data; 
    loff_t newpos; 

    switch(whence) 
    { 
        case 0: /* SEEK_SET */ 
            newpos = off; 
            break; 
        case 1: /* SEEK_CUR */ 
            newpos = filp->f_pos + off; 
            break; 
        case 2: /* SEEK_END */ 
            newpos = dev->size + off; 
            break; 
        default: /* can't happen */ 
            return -EINVAL; 
    } 

    if (newpos < 0) 
        return -EINVAL; 

    filp->f_pos = newpos; 
    return newpos; 
} 

唯一设备特定的操作是从设备中获取文件长度. 在 scull 中 read 和 write 方法如需要地一样协作, 如同在第 3 章所示. 尽管刚刚展示的这个实现对 scull 有意义, 它处理一个被很好定义了的数据区, 大部分设备提供了一个数据流而不是一个数据区(想想串口或者键盘), 并且移位这些设备没有意义. 如果这就是你的设备的情况, 你不能只制止声明 llseek 操作, 因为缺省的方法允许移位. 相反, 你应当通知内核你的设备不支持 llseek , 通过调用 nonseekable_open 在你的 open 方法中.

int nonseekable_open(struct inode *inode; struct file *filp); 

这个调用标识了给定的 filp 为不可移位的; 内核从不允许一个 lseek 调用在这样一个文件上成功. 通过用这样的方式标识这个文件,
你可确定不会有通过 pread 和 pwrite 系统调用的方式来试图移位这个文件.
完整起见, 你也应该在你的 file_operations 结构中设置 llseek 方法到一个特殊的帮忙函数 no_llseek, 它定义在 <linux/fs.h>.

唯一设备特定的操作是从设备中获取文件长度. 在 scull 中 read 和 write 方法如需要地一样协作, 如同在第 3 章所示.
尽管刚刚展示的这个实现对 scull 有意义, 它处理一个被很好定义了的数据区, 大部分设备提供了一个数据流而不是一个数据区(想想串口或者键盘), 并且移位这些设备没有意义. 如果这就是你的设备的情况, 你不能只制止声明 llseek 操作, 因为缺省的方法允许移位. 相反,
你应当通知内核你的设备不支持 llseek , 通过调用 nonseekable_open 在你的 open 方法中.

int nonseekable_open(struct inode *inode; struct file *filp); 

这个调用标识了给定的 filp 为不可移位的; 内核从不允许一个 lseek 调用在这样一个文件上成功. 通过用这样的方式标识这个文件,
你可确定不会有通过 pread 和 pwrite 系统调用的方式来试图移位这个文件. 完整起见, 你也应该在你的 file_operations 结构中设置 llseek 方法到一个特殊的帮忙函数 no_llseek, 它定义在 <linux/fs.h>.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值