tty 层,第二部分
在本专栏的第一部分(LJ,2002 年 8 月),我们介绍了 tty 层的基本知识以及如何创建一个最小的 tty 驱动程序。现在我们继续深入 tty 层,尝试解释一些高级部分。
还记得第一部分中所有 tty 驱动程序都需要实现的庞大的 struct tty_driver 结构吗? 让我们关注一下上次未完全涵盖的几个函数。
当在设备节点上调用 ioctl(2) 时,tty 层会调用 struct tty_driver 中的 ioctl 函数回调。 如果您的驱动程序不知道如何处理传递给它的 ioctl 值,则应返回 -ENOIOCTLCMD,以尝试让 tty 层实现通用版本(如果可能)。 但是,您的驱动程序应该处理哪些 ioctl 值呢?
2.4.19 内核定义了大约 60 种不同的可能 tty ioctl。 您的 tty 驱动程序不必实现所有这些 ioctl,但最好尝试处理以下常见的 ioctl。
TIOCMGET: 当用户想要查找串行端口的控制线(例如 DTR 或 RTS 线)的状态时调用。 如果您可以直接读取串行端口的 MSR 或 MCR 寄存器,或者如果您在本地保留它们的副本(就像某些 USB 转串行类型的设备需要做的那样),那么这就是如何实现此 ioctl 的方法
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if (cmd == TIOCMGET) { unsigned int result = 0; unsigned int msr = tp->msr; unsigned int mcr = tp->mcr; result = ((mcr & MCR_DTR) ? TIOCM_DTR: 0) /* DTR is set */ | ((mcr & MCR_RTS) ? TIOCM_RTS: 0) /* RTS is set */ | ((msr & MSR_CTS) ? TIOCM_CTS: 0) /* CTS is set */ | ((msr & MSR_CD) ? TIOCM_CAR: 0) /* Carrier detect is set*/ | ((msr & MSR_RI) ? TIOCM_RI: 0) /* Ring Indicator is set */ | ((msr & MSR_DSR) ? TIOCM_DSR: 0); /* DSR is set */ if (copy_to_user((unsigned int *)arg, &result, sizeof(unsigned int))) return -EFAULT; return 0; } return -ENOIOCTLCMD; }
TIOCMBIS、TIOCMBIC 和 TIOCMSET: 用于设置 tty 设备上不同的调制解调器控制寄存器。 TIOCMBIS 调用可以打开 RTS、DTR 或环回寄存器,而 TIOCMBIC 调用可以关闭它们。 TIOCMSET 调用会关闭所有三个值,然后仅设置它想要的特定值。 以下是如何处理这种情况的示例
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if ((cmd == TIOCMBIS) || (cmd == TIOCMBIC) || (cmd == TIOCMSET)) { unsigned int value; unsigned int mcr = tp->mcr; if (copy_from_user(&value, (unsigned int *)arg, sizeof(unsigned int))) return -EFAULT; switch (cmd) { case TIOCMBIS: if (value & TIOCM_RTS) mcr |= MCR_RTS; if (value & TIOCM_DTR) mcr |= MCR_RTS; if (value & TIOCM_LOOP) mcr |= MCR_LOOPBACK; break; case TIOCMBIC: if (value & TIOCM_RTS) mcr &= ~MCR_RTS; if (value & TIOCM_DTR) mcr &= ~MCR_RTS; if (value & TIOCM_LOOP) mcr &= ~MCR_LOOPBACK; break; case TIOCMSET: /* turn off the RTS and DTR and * LOOPBACK, and then only turn on * what was asked for */ mcr &= ~(MCR_RTS | MCR_DTR | MCR_LOOPBACK); mcr |= ((value & TIOCM_RTS) ? MCR_RTS : 0); mcr |= ((value & TIOCM_DTR) ? MCR_DTR : 0); mcr |= ((value & TIOCM_LOOP) ? MCR_LOOPBACK : 0); break; } /* set the new MCR value in the device */ tp->mcr = mcr; return 0; } return -ENOIOCTLCMD; }请注意,环回请求 (TIOCM_LOOP) 在 2.2 内核中不存在,但在 2.4 和更新的内核中存在。
如果您的 tty 驱动程序可以处理这四个 ioctl,它将与大多数现有的用户空间程序一起工作。 但是,总有一些奇怪的程序会请求其他 ioctl 之一。 因此,您可能还需要考虑处理其他一些常见的 ioctl 函数。
TIOCSERGETLSR: 调用以检索 tty 设备的线路状态寄存器 (LSR) 值。
TIOCGSERIAL: 调用以一次性从您的设备获取大量串行线路信息。 指向 struct serial_struct 的指针会传递给此调用,您的驱动程序应使用适当的值填充该结构。 一些程序(如 setserial 和 dip)调用此函数以确保波特率已正确设置,并获取有关您的 tty 设备类型的一般信息。 以下是如何实现这种情况的示例
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if (cmd == TIOCGSERIAL) { struct serial_struct tmp; if (!arg) return -EFAULT; memset(&tmp, 0, sizeof(tmp)); tmp.type = tp->type; tmp.line = tp->line; tmp.port = tp->port; tmp.irq = tp->irq; tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ; tmp.xmit_fifo_size = tp->xmit_fifo_size; tmp.baud_base = tp->baud_base; tmp.close_delay = 5*HZ; tmp.closing_wait = 30*HZ; tmp.custom_divisor = tp->custom_divisor; tmp.hub6 = tp->hub6; tmp.io_type = tp->io_type; if (copy_to_user(arg, &tmp, sizeof(struct serial_struct))) return -EFAULT; return 0; } return -ENOIOCTLCMD; }
TIOCSSERIAL: 与 TIOCGSERIAL 相反; 通过此调用,用户可以一次性设置您设备的串行线路状态。 指向 struct serial_struct 的指针会传递给此调用,其中包含您的设备现在应设置的数据。 如果您的设备未实现此调用,几乎所有程序仍将正常工作。
TIOCMIWAIT: 一个有趣的调用。 如果用户发出此 ioctl 调用,他们希望在内核中休眠,直到 tty 设备的 MSR 寄存器发生某些事情。 “arg”参数将包含用户正在等待的事件类型。 此 ioctl 通常用于等待状态线路更改,表明更多数据已准备好发送到设备。
但是,在实现 TIOCMIWAIT ioctl 时要小心。 几乎所有使用它的现有内核驱动程序也使用 interruptible_sleep_on() 调用,这是不安全的。 (涉及许多讨厌的竞争条件。) 相反,应使用 wait_queue 以避免这些问题。 以下是实现 TIOCMIWAIT 的正确方法示例
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if (cmd == TIOCMIWAIT) { DECLARE_WAITQUEUE(wait, current); struct async_icount cnow; struct async_icount cprev; cprev = tp->icount; while (1) { add_wait_queue(&tp->wait, &wait); set_current_state(TASK_INTERRUPTIBLE); schedule(); remove_wait_queue(&tp->wait, &wait); /* see if a signal woke us up */ if (signal_pending(current)) return -ERESTARTSYS; cnow = edge_port->icount; if (cnow.rng == cprev.rng && cnow.dsr == cprev.dsr && cnow.dcd == cprev.dcd && cnow.cts == cprev.cts) return -EIO; /* no change => error */ if (((arg & TIOCM_RNG) && (cnow.rng != cprev.rng)) || ((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr)) || ((arg & TIOCM_CD) && (cnow.dcd != cprev.dcd)) || ((arg & TIOCM_CTS) && (cnow.cts != cprev.cts)) ) { return 0; } cprev = cnow; } } return -ENOIOCTLCMD; }
在代码中识别 MSR 寄存器更改的部分中,行
wake_up_interruptible(&tp->wait);必须调用才能使此代码正常工作。
TIOCGICOUNT: 当用户想要知道已发生的串行线路中断数时调用。 内核会传递一个指向 struct serial_icounter_struct 的指针,需要填充该结构。 此调用通常与之前的 TIOCMIWAIT ioctl 调用结合使用。 如果您在驱动程序运行时跟踪所有这些中断,则实现此调用的代码可能非常简单。 有关示例,请参见 drivers/usb/serial/io_edgeport.c。
您的 tty_struct 中的 write() 回调可以从中断上下文和用户上下文中调用。 了解这一点很重要,因为您不应在中断上下文中调用任何可能休眠的函数。 这包括任何可能调用 schedule() 的函数,例如常见的函数,如 copy_from_user()、kmalloc() 和 printk()。 如果您真的想休眠,请通过调用 in_interrupt() 检查您的状态。
当 tty 子系统本身需要通过 tty 设备发送一些数据时,可以调用 write() 回调。 如果您未在 tty_struct 中实现 put_char() 函数,则可能会发生这种情况。 (请记住,如果没有 put_char() 函数,tty 层将使用 write() 函数。) 当 tty 层想要将换行符转换为换行符加上换行符时,通常会发生这种情况。 这里要记住的重点是您的 write() 函数不得为此类调用返回 0。 这意味着您必须将该字节的数据写入设备,因为调用者(tty 层)将不会缓冲数据并在稍后重试。 由于 write() 调用无法确定它是否被调用来代替 put_char()——即使只发送一个字节的数据——请尝试始终实现您的 write() 调用,使其能够接受至少一个字节的数据。 许多当前的 USB 转串行 tty 驱动程序不遵循此规则,因此,当连接到某些终端类型时,它们无法正常工作。
为了正确实现 set_termios() 回调,您的驱动程序必须能够解码 termios 结构中的所有不同设置。 这是一项复杂的任务,因为所有线路设置都以各种方式打包到 termios 结构中。
清单 1 显示了 set_termios() 调用的一个简单实现,它会将用户请求的所有不同线路设置打印到内核调试日志中。
首先,保存 tty 结构中的 cflags 变量的副本,因为我们将经常访问它
unsigned int cflag; cflag = tty->termios->c_cflag;
接下来,测试看看是否有我们需要做的事情。 例如,看看用户是否尝试使用我们当前拥有的相同设置; 如果没有必要,我们不想做任何额外的工作。
/* check that they really want us to change * something */ if (old_termios) { if ((cflag == old_termios->c_cflag) && (RELEVANT_IFLAG(tty->termios->c_iflag) == RELEVANT_IFLAG(old_termios->c_iflag))) { printk (KERN_DEBUG " - nothing to change...\n"); return; } }RELEVANT_IFLAG() 宏定义为
#define RELEVANT_IFLAG(iflag) (iflag & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK))用于屏蔽 cflags 变量的重要位。 将此值与旧值进行比较,看看它们是否不同。 如果它们没有不同,则不需要更改任何内容,因此我们返回。 请注意,在尝试访问 old_termios 变量的字段之前,我们首先检查 old_termios 变量是否实际指向某些内容。 此检查是必需的,因为有时此变量将为 NULL。 尝试访问 NULL 指针的字段将在内核中导致严重的 oops。
现在我们知道我们需要更改终端设置,让我们看一下请求的字节大小
/* get the byte size */ switch (cflag & CSIZE) { case CS5: printk (KERN_DEBUG " - data bits = 5\n"); break; case CS6: printk (KERN_DEBUG " - data bits = 6\n"); break; case CS7: printk (KERN_DEBUG " - data bits = 7\n"); break; default: case CS8: printk (KERN_DEBUG " - data bits = 8\n"); break; }
我们使用 CSIZE 位字段屏蔽 cflag 并测试结果。 如果我们无法弄清楚设置了哪些位,我们可以使用默认的 8 个数据位。 然后我们检查请求的奇偶校验值
/* determine the parity */ if (cflag & PARENB) if (cflag & PARODD) printk (KERN_DEBUG " - parity odd\n"); else printk (KERN_DEBUG " - parity even\n"); else printk (KERN_DEBUG " - parity none\n");我们首先测试用户是否真的想要首先设置某种类型的奇偶校验。 如果是这样,那么我们需要测试他们想要哪种奇偶校验(奇数或偶数)。
请求的停止位也很容易测试
/* figure out the stop bits requested */ if (cflag & CSTOPB) printk (KERN_DEBUG " - stop bits = 2\n"); else printk (KERN_DEBUG " - stop bits = 1\n");
现在,我们正在确定适当的流控制设置。 确定我们是否应该使用 RTS/CTS 是一个简单的过程
/* figure out the flow control settings */ if (cflag & CRTSCTS) printk (KERN_DEBUG " - RTS/CTS is enabled\n"); else printk (KERN_DEBUG " - RTS/CTS is disabled\n");但是,确定软件流控制的不同模式以及不同的停止和起始字符有点困难
/* determine software flow control */ /* if we are implementing XON/XOFF, set the start * and stop character in the device */ if (I_IXOFF(tty) || I_IXON(tty)) { unsigned char stop_char = STOP_CHAR(tty); unsigned char start_char = START_CHAR(tty); /* if we are implementing INBOUND XON/XOFF */ if (I_IXOFF(tty)) printk (KERN_DEBUG " - INBOUND XON/XOFF is enabled, " "XON = %2x, XOFF = %2x", start_char, stop_char); else printk (KERN_DEBUG " - INBOUND XON/XOFF " "is disabled"); /* if we are implementing OUTBOUND XON/XOFF */ if (I_IXON(tty)) printk (KERN_DEBUG " - OUTBOUND XON/XOFF is enabled, " "XON = %2x, XOFF = %2x", start_char, stop_char); else printk (KERN_DEBUG " - OUTBOUND XON/XOFF " "is disabled"); }最后,我们要确定波特率。 幸运的是,tty_get_baud_rate() 函数可以从 termios 设置中提取特定的波特率,并将其作为整数返回
/* get the baud rate wanted */ printk (KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty));现在已经确定了所有不同的线路设置,您需要使用这些信息来正确设置设备。
Vern Hoxie 在 scicom.alphacdc.com/pub/linux 上提供了一套关于如何从用户空间访问串行端口的出色文档和示例程序。 大部分信息对于内核程序员来说不是很有用,但是对不同 ioctl(2) 命令的描述以及获取和设置 tty 信息的各种不同方式背后的历史记录非常好。 我强烈建议任何实现 tty 内核驱动程序的人阅读这些内容,即使只是为了确定用户将如何尝试使用您的驱动程序。
希望这两篇文章有助于揭开 tty 层的神秘面纱。 如果您在如何实现特定回调时遇到困难,内核中有很多驱动程序与 tty 层交互作为完整示例。 在 drivers/char 和 drivers/usb 目录中搜索“tty_register_driver”以查找这些文件。
我要感谢 Al Borchers,他帮助确定了 write() 回调的真正工作原理以及其中涉及的所有细微差别。 他们与 Peter Berger 一起编写了 drivers/usb/serial/digi_acceleport.c,这是一个用于 Digi AccelePort 设备的 USB 转串行驱动程序。 它是工作良好的 tty 驱动程序的一个很好的例子。
Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。 他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。