细节决定成败
从前两篇文章的干净代码环境出发,我们现在转向所有令人讨厌的中断内容。令人惊讶的是,Linux 为我们隐藏了大部分内容,因此我们不需要编写任何汇编代码...
现在,我们的神奇 skel-机器驱动程序可以加载甚至卸载(与 DOS 不同,它无痛),但它既没有读取也没有写入任何字符。因此,我们将开始充实前一篇文章(在fops and filp下)中介绍的 skel_read() 和 skel_write() 函数。这两个函数都接受四个参数
Static int skel_read (struct inode *inode, struct file *file, char *buf, int count) Static int skel_write (struct inode *inode, struct file *file, const char *buf, int count)
inode 结构为函数提供在 skel_open() 调用期间已经使用的信息。例如,我们从 inode->i_rdev 确定用户想要打开哪个板卡,并将此数据以及板卡的基本地址和中断传输到文件描述符的 private_data 条目。我们现在可能会忽略此信息,但如果我们不使用此技巧,inode 是我们唯一的机会来确定我们正在与哪个板卡对话。
file 结构包含更有价值的数据。您可以在 <linux/fs.h> 的定义中探索所有元素。如果您使用 private_data 条目,您可以在这里找到它,并且您还应该使用 f_flags 条目——例如,向您揭示用户是否想要阻塞或非阻塞模式。(我们将在稍后更详细地解释这个主题。)
buf 参数告诉我们读取的字节放在哪里(或写入的字节在哪里),count 指定有多少字节。但是您必须记住,每个进程都有自己的私有地址空间。在内核代码中,有一个所有进程共有的地址空间。当系统调用代表特定进程执行时,它们在内核地址空间中运行,但仍然能够访问用户空间。从历史上看,这是通过使用 fs 寄存器的汇编代码完成的;当前的 Linux 内核将特定代码隐藏在名为 get_user_byte()(用于从用户地址空间读取一个字节)、put_user_byte()(用于写入一个字节)等函数中。它们以前被称为 get_fs_byte,只有 memcpy_tofs() 和 memcpy_fromfs() 甚至在 DEC Alpha 上也揭示了这些旧时代。如果您想探索,请查看 <asm/segment.h>。
让我们想象一下理想的硬件,它总是渴望接收数据,快速读取和写入,并通过我们接口的基本地址处的简单 8 位数据端口进行访问。虽然这个例子是不现实的,但如果您不耐烦,您可以尝试以下代码
Static int skel_read (struct inode *inode,
struct file *file,
char *buf, int count) {
int n = count;
char *port = PORT0 ((struct Skel_Hw*)
(file->private_data));
while (n--) {
Wait till device is ready
put_user_byte (inb_p (port), buf);
buf++;
}
return count;
}
请注意 inb_p() 函数调用,它是来自硬件的实际 I/O 读取。您可能会决定使用其快速等效项 inb(),它省略了一些慢速硬件可能需要的最小延迟,但我更喜欢安全的方式。
等效的 skel_write() 函数几乎相同。只需将 put_user_byte() 行替换为以下内容
outb_p (get_user_byte (buf), port);
但是,这些行有很多缺点。使用它们会导致 Linux 无限循环,同时等待永远不会准备就绪的设备吗?我们的驱动程序应该将等待循环中的时间分配给其他进程,充分利用我们昂贵的 CPU 中的所有资源,并且它应该有一个输入和输出缓冲区,用于在我们不在 skel_read() 和相应的 skel_write() 调用中时到达的字节。它还应包含超时测试以防出错,并且应支持阻塞和非阻塞模式。
想象一个一次读取 256 字节的进程。不幸的是,当调用 skel_read() 时,我们的输入缓冲区是空的。那么它应该怎么做——返回并说还没有数据,还是等到至少到达一些字节?
答案是两者都是。阻塞模式意味着用户希望驱动程序等待直到读取一些字节。非阻塞模式意味着尽快返回——只需读取所有可用的字节。类似的规则适用于写入:阻塞模式意味着“在您可以接受一些数据之前不要返回”,而非阻塞模式意味着:“即使没有接受任何内容也返回。” read() 和 write() 调用通常返回成功读取或写入的数据字节数。但是,如果设备是非阻塞的并且无法传输任何字节,则通常返回 -EAGAIN(意思是:“再来一次,山姆”)。偶尔,旧代码可能会返回 -EWOULDBLOCK,它在 Linux 下与 -EAGAIN 相同。
也许现在您像我第一次听到这两种模式一样开心地笑了。如果这些概念对您来说是新的,您可能会发现以下提示很有帮助。每个设备默认以阻塞模式打开,但您可以通过在 open() 调用中设置 O_NONBLOCK 标志来选择非阻塞模式。您甚至可以使用 fcntl() 调用稍后更改文件的行为。 fcntl() 调用很简单,手册页对于任何程序员来说都足够了。
很久以前,一位美丽的公主被女巫送入漫长而沉睡的睡眠中,持续了一百年。世界几乎忘记了她和她的城堡,玫瑰缠绕着城堡,直到有一天,一位英俊的王子来了,吻了她,唤醒了她——以及你在童话故事中听到的所有其他美好的事情都发生了。
我们的驱动程序应该像公主在等待数据时所做的那样:睡觉,让世界继续运转。 Linux 提供了一种机制来实现这一点,称为 interruptible_sleep_on()。每个到达此调用的进程都将进入睡眠状态,并将其时间片贡献给世界的其余部分。它将一直在此函数中停留,直到另一个进程调用 wake_up_interruptible(),而这个“王子”通常采用成功接收或发送数据的中断处理程序的形式,或者如果发生超时情况,则采用 Linux 本身的形式。
本系列的前一篇文章展示了一个最小的中断处理程序,它被称为 skel_trial_fn(),但没有解释其工作原理。在这里,我们介绍一个“完整”的中断处理程序,它将处理到实际硬件设备的输入和来自实际硬件设备的输出。图 1 显示了其概念的简单版本:当驱动程序等待设备准备就绪(阻塞)时,它通过调用 interruptible_sleep_on() 进入睡眠状态。有效的中断结束了这种睡眠,重新启动 skel_write()。
图 1 不包括我们在使用内部输出缓冲区时需要的双重嵌套循环结构。原因是如果我们在 skel_write() 函数中只能执行写入,则不需要内部输出缓冲区。但是我们的驱动程序应该在不在 skel_read() 中时也捕获数据,并且即使不在 skel_write() 中也应该在后台写入数据。因此,我们将更改 skel_write() 中的硬件写入以写入输出缓冲区,并让中断处理程序执行到硬件的实际写入。中断和 skel_write() 现在将通过“睡美人”机制和输出缓冲区连接起来。
中断处理程序在设备的 open() 和 close() 调用期间安装和卸载,如前一篇文章中所建议的那样。此任务由以下内核调用处理
#include <linux/sched.h> int request_irq(unsigned int irq, void (*handler) (int, struct pt_regs *), unsigned long flags, const char *device); void free_irq(unsigned int irq);
handler 参数是我们希望安装的实际中断处理程序。 flags 参数的作用是设置处理程序的几个功能,最重要的是它作为快速处理程序的行为(在 flags 中设置 SA_INTERRUPT)或作为慢速处理程序的行为(未设置 SA_INTERRUPT)。快速处理程序在禁用所有中断的情况下运行,而慢速处理程序在启用除自身之外的所有中断的情况下执行。
最后,device 参数用于在查看 /proc/interrupts 时识别处理程序。
由 request_irq() 安装的处理程序函数仅传递中断号和处理器寄存器的(通常无用的)内容。
因此,我们将首先确定调用中断属于哪个板卡。如果我们找不到任何板卡,就会发生称为伪中断的情况,我们应该忽略它。通常,中断用于告知设备是否准备好进行读取或写入,因此我们必须通过一项或多项硬件测试来找出设备希望我们做什么。
当然,我们应该快速离开我们的中断处理程序。奇怪的是,即使在快速中断处理程序中也允许使用 printk()(以及 PDEBUG 行)。这是 Linux 实现的一个非常有用的功能。如果您查看 kernel/printk.c,您会发现它的实现是基于等待队列的,因为将消息实际传递到日志文件是由外部进程(通常是 klogd)处理的。
如图 图 2 所示,Linux 可以在 interruptible_sleep_on() 中处理超时。例如,如果您正在使用一个设备向其发送应答,并且期望它在有限的时间内回复,则在返回给用户进程的返回值中导致超时以发出 I/O 错误 (-EIO) 可能是一个不错的选择。
当然,用户进程也可以使用 alarm 机制来处理这个问题。但在驱动程序本身中处理这个问题肯定更容易。超时标准由 SKEL_TIMEOUT 指定,以 jiffies 为单位计数。 Jiffies 是 Linux 系统的稳定心跳,一个稳定的计时器每隔几毫秒递增一次。频率或每秒 jiffies 数在 <asm/param.h> (包含在 <linux/sched.h> 中) 中由 HZ 定义,并且在不同的架构上有所不同(100 Hz Intel,1 kHz Alpha)。您只需设置
#define SKEL_TIMEOUT timeout_seconds * HZ
/* ... */
current->timeout = jiffies + SKEL_TIMEOUT
如果 interruptible_sleep_on 超时,则在返回后将清除 current->timeout。
请注意,中断可能会发生在 skel_read() 和 skel_write() 中。可能在中断中更改的变量应声明为 volatile。它们还需要受到保护以避免竞争条件。保护关键区域的经典代码序列如下
unsigned long flags;
save_flags (flags);
cli ();
critical region
restore_flags (flags);
最后,用于“完整”错误处理程序的代码
#define SKEL_IBUFSIZ 512 #define SKEL_OBUFSIZ 512 /* for 5 seconds timeout */ #define SKEL_TIMEOUT (5*HZ) /* This should be inserted in the Skel_Hw-structure */ typedef struct Skel_Hw { /* write position in input-buffer */ volatile int ibuf_wpos; /* read position in input-buffer */ int ibuf_rpos; /* the input-buffer itself */ char *ibuf; /* write position in output-buffer */ int obuf_wpos; /* read position in output-buffer */ volatile int buf_rpos; char *obuf; struct wait_queue *skel_wait_iq; struct wait_queue *skel_wait_oq; [...] } #define SKEL_IBUF_EMPTY(b) \ ((b)->ibuf_rpos==(b)->ibuf_wpos) #define SKEL_OBUF_EMPTY(b) \ ((b)->obuf_rpos==(b)->obuf_wpos) #define SKEL_IBUF_FULL(b) \ (((b)->ibuf_wpos+1)%SKEL_IBUFSIZ==(b)->ibuf_rpos) #define SKEL_OBUF_FULL(b) \ (((b)->obuf_wpos+1)%SKEL_OBUFSIZ==(b)->obuf_rpos) Static int skel_open (struct inode *inode, struct file *filp) { /* .... */ /* First we allocate the buffers */ board->ibuf = (char*) kmalloc (SKEL_IBUFSIZ, GFP_KERNEL); if (board->ibuf == NULL) return -ENOMEM; board->obuf = (char*) kmalloc (SKEL_OBUFSIZ, GFP_KERNEL); if (board->obuf == NULL) { kfree_s (board->ibuf, SKEL_IBUFSIZ); return -ENOMEM; } /* Now we clear them */ ibuf_wpos = ibuf_rpos = 0; obuf_wpos = obuf_rpos = 0; board->irq = board->hwirq; if ((err=request_irq(board->irq> skel_interrupt, SA_INTERRUPT, "skel"))) return err; } Static void skel_interrupt(int irq, struct pt_regs *unused) { int i; Skel_Hw *board; for (i=0, board=skel_hw; i<skel_boards; board++, i++) /* spurious */ if (board->irq==irq) break; if (i==skel_boards) return; if (board_is_ready_for_input) skel_hw_write (board); if (board_is_ready_for_output) skel_hw_read (board); } Static inline void skel_hw_write (Skel_Hw *board){ int rpos; char c; while (! SKEL_OBUF_EMPTY (board) && board_ready_for_writing) { c = board->obuf [board->obuf_rpos++]; write_byte_c_to_device board->obuf_rpos %= SKEL_OBUF_SIZ; } /* Sleeping Beauty */ wake_up_interruptible (board->skel_wait_oq); } Static inline void skel_hw_read (Skel_Hw *board) { char c; /* If space left in the input buffer & device ready: */ while (! SKEL_IBUF_FULL (board) && board_ready_for_reading) { read_byte_c_from_device board->ibuf [board->ibuf_wpos++] = c; board->ibuf_wpos %= SKEL_IBUFSIZ; } wake_up_interruptible (board->skel_wait_iq); } Static int skel_write (struct inode *inode, struct file *file, char *buf, int count) { int n; int written=0; Skel_Hw board = (Skel_Hw*) (file->private_data); for (;;) { while (written<count && ! SKEL_OBUF_FULL (board)) { board->obuf [board->obuf_wpos] = get_user_byte (buf); buf++; board->obuf_wpos++; written++; board->obuf_wpos %= SKEL_OBUFSIZ; } if (written) return written; if (file->f_flags & O_NONBLOCK) return -EAGAIN; current->timeout = jiffies + SKEL_TIMEOUT; interruptible_sleep_on ( &(board->skel_wait_oq)); /* Why did we return? */ if (current->signal & ~current->blocked) /* If the signal is not not being blocked */ return -ERESTARTSYS; if (!current->timeout) /* no write till timout: i/o-error */ return -EIO; } } Static int skel_read (struct inode *inode, struct file *file, char *buf, int count) { Skel_Hw board = (Skel_Hw*) (file->private_data); int bytes_read = 0; if (!count) return 0; if (SKEL_IBUF_EMPTY (board)) { if (file->f_flags & O_NONBLOCK) /* Non-blocking */ return -EAGAIN; current->time_out = jiffies+SKEL_TIMEOUT; for (;;) { skel_tell_hw_we_ask_for_data interruptible_sleep_on ( &(board->skel_wait_iq)); if (current->signal & ~current->blocked) return -ERESTARTSYS; if (! SKEL_IBUF_EMPTY (board)) break; if (!current->timeout) /* Got timeout: return -EIO */ return -EIO; } } /* if some bytes are here, return them */ while (! SKEL_IBUF_EMPTY (board)) { put_user_byte (board->ibuf [board->ibuf_rpos], buf); buf++; board->ibuf_rpos++; bytes_read++; board->ibuf_rpos %= SKEL_IBUFSIZ; if (--count == 0) break; } if (count) /* still looking for some bytes */ skel_tell_hw_we_ask_for_data return bytes_read; }
要显示的最后一个重要的 I/O 函数是 select(),在我们看来,它是 Unix 最有趣的部分之一。
select() 调用用于等待设备准备就绪,并且是对于新手 C 程序员来说最可怕的函数之一。虽然此处未显示其在应用程序中的使用,但显示了系统调用的驱动程序特定部分,其最令人印象深刻的功能是其紧凑性。
这是完整代码
Static int skel_select(struct inode *inode,
struct file *file,
int sel_type,
select_table *wait) {
Skel_Clientdata *data=filp->private_data;
Skel_Board *board=data->board;
if (sel_type==SEL_IN) {
if (! SKEL_IBUF_EMPTY (board))
/* readable */
return 1;
skel_tell_hw_we_ask_for_data;
select_wait(&(hwp->skel_wait_iq), wait);
/* not readable */
return 0;
}
if (sel_type==SEL_OUT) {
if (! SKEL_OBUF_FULL (board))
return 1; /* writable */
/* hw knows */
select_wait (&(hwp->skel_wait_oq), wait);
return 0;
}
/* exception condition: cannot happen */
return 0;
}
如您所见,内核负责管理等待队列的麻烦,您只需检查是否准备就绪。
当我们第一次为驱动程序编写 select() 调用时,我们不理解 wait_queue 实现,您也不需要理解。您只需要知道代码有效。 wait_queue 是具有挑战性的,并且通常当您编写驱动程序时,您没有时间接受挑战。
实际上,select 在其与读取和写入的关系中更容易理解:如果 select() 说文件是可读的,则下一个读取不得阻塞(独立于 O_NONBLOCK),这意味着您必须告诉硬件返回数据。中断将收集数据,并唤醒队列。如果用户正在选择写入,情况类似:驱动程序必须告知 write() 是否会阻塞。如果缓冲区已满,它将阻塞,但您不需要告诉硬件它,因为 write() 已经告诉它了(当它填充缓冲区时)。如果缓冲区未满,则写入不会阻塞,因此您返回 1。
这种考虑选择写入的方式可能看起来很奇怪,因为有时您需要同步写入,并且您可能期望设备在已接受挂起的输入时可写入。不幸的是,这种做事方式会破坏阻塞/非阻塞机制,因此提供了一个额外的调用:如果您需要同步写入,驱动程序必须提供(在其 fops 中)fsync() 调用。应用程序通过 fsync() 系统调用调用 fops->fsync,如果驱动程序不支持它,则返回 -EINVAL。
想象一下,您想更改您构建的串行多端口卡的波特率。或者告诉您的帧捕获器更改图像的分辨率。或者其他任何东西... 您可以将这些指令包装成一系列转义序列,例如,ANSI 仿真中的屏幕定位。但是,正常的做法是进行 ioctl() 调用。
ioctl() 调用在 <sys/ioctl.h> 中定义,形式为
ioctl (int file_handle, int command, ...)
其中 ... 被认为是 char * 类型的参数(根据 ioctl 手册页)。奇怪的是,内核在 fs/ioctl.c 中以以下形式接收这些参数
int sys_ioctl (unsigned int fd, unsigned int cmd, unsigned long arg);
更令人困惑的是,<linux/ioctl.h> 给出了关于如何构建第二个参数中的命令的详细规则,但是所有驱动程序中还没有人真正遵循这些想法。
无论如何,与其清理 Linux 源代码树,不如关注 ioctl() 调用的总体思想。作为用户,您在前两个参数中传递文件句柄和命令,并将指向驱动程序应读取和/或写入的数据结构的指针作为第三个参数传递。
内核本身解释了一些命令——例如,FIONBIO,它更改文件的阻塞/非阻塞标志。其余的传递给我们自己的、驱动程序特定的 ioctl() 调用,并以以下形式到达
int skel_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
在我们展示 skel_ioctl() 实现的小例子之前,您定义的命令应遵守以下规则
从 /usr/src/linux/MAGIC 中选取一个空闲的 MAGIC 数字,并将此数字作为 16 位命令字的高八位。
在低八位中枚举命令。
为什么这样?想象一下,“傻比利”启动了他最喜欢的终端程序 minicom 来连接到他的邮箱。“傻比利”不小心将 minicom 使用的串行线路从 /dev/ttyS0 更改为 /dev/skel0(他非常傻)。 minicom 接下来要做的是使用 TCGETA 作为命令通过 ioctl() 初始化“串行线路”。不幸的是,您的设备驱动程序隐藏在 /dev/skel0 后面,使用该号码来控制实验室长期实验的电压...
如果 ioctl() 命令中的高八位因驱动程序而异,则对不适当设备的每个 ioctl() 都将导致 -EINVAL 返回,从而保护我们免受极其意外的结果。
现在,为了完成本节,我们将实现一个 ioctl() 调用,用于读取或更改驱动程序中的超时延迟。如果您想使用它,您必须引入一个新变量
unsigned long skel_timeout = SKEL_TIMEOUT;
紧跟在 SKEL_TIMEOUT 的定义之后,并将以后每次出现的 SKEL_TIMEOUT 替换为 skel_timeout。
我们选择 MAGIC '4' (ASCII 字符 4)并定义两个命令
# define SKEL_GET_TIMEOUT 0x3401 # define SKEL_SET_TIMEOUT 0x3402
在我们的用户进程中,这些行将使超时值加倍
/* ... */
unsigned long timeout;
if (ioctl (skel_hd, SKEL_GET_TIMEOUT,
&timeout) < 0) {
/* an error occurred (Silly billy?) */
/* ... */
}
timeout *= 2;
if (ioctl (skel_hd, SKEL_SET_TIMEOUT,
&timeout) < 0) {
/* another error */
/* ... */
}
在我们的驱动程序中,这些行将完成工作
int skel_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case SKEL_GET_TIMEOUT: put_user_long(skel_timeout, (long*) arg); return 0; case SKEL_SET_TIMEOUT: skel_timeout = get_user_long((long*) arg); return 0; default: return -EINVAL; /* for Silly Billy */ } }
Georg V. Zezschwitz 是一位 27 岁的 Linuxer,对计算机科学的实践方面有浓厚的兴趣,并且有避免睡眠的倾向。
XXXXXXXXXXXXXXXX (XXXXXXXXXXXXXX) 像 Georg 一样,也 27 岁,对计算机科学的实践方面有相同的兴趣,并且有相同的避免睡眠的倾向。