18.9. 快速参考

最后更新于:2022-04-01 03:01:30

## 18.9. 快速参考 本节提供了对本章介绍的概念的参考. 它还解释了每个 tty 驱动需要包含的头文件的角色. 在 tty_driver 和 tty_device 结构中的成员变量的列表, 但是, 在这里不重复. ~~~ #include <linux/tty_driver.h> ~~~ 头文件, 包含 struct tty_driver 的定义和声明一些在这个结构中的不同的标志. ~~~ #include <linux/tty.h> ~~~ 头文件, 包含 tty_struct 结构的定义和几个不同的宏定义来易于存取 struct termios 的成员的单个值. 它还含有 tty 驱动核心的函数声明. ~~~ #include <linux/tty_flip.h> ~~~ 头文件, 包含几个 tty flip 缓冲内联函数, 使得易于操作 flip 缓冲结构. ~~~ #include <asm/termios.h> ~~~ 头文件, 包含 struct termio 的定义, 用于内核所建立的特定硬件平台. ~~~ struct tty_driver *alloc_tty_driver(int lines); ~~~ 函数, 创建一个 struct tty_driver, 可之后传递给 tty_register_driver 和 tty_unregister_driver 函数. ~~~ void put_tty_driver(struct tty_driver *driver); ~~~ 函数, 清理尚未成功注册到 tty 内核的 struct tty_driver 结构. ~~~ void tty_set_operations(struct tty_driver *driver, struct tty_operations *op); ~~~ 函数, 初始化 struct tty_driver 的函数回调. 有必要在 tty_register_driver 可被调用前调用. ~~~ int tty_register_driver(struct tty_driver *driver);int tty_unregister_driver(struct tty_driver *driver); ~~~ 函数, 从 tty 核心注册和注销一个 tty 驱动. ~~~ void tty_register_device(struct tty_driver *driver, unsigned minor, struct device *device); void tty_unregister_device(struct tty_driver *driver, unsigned minor); ~~~ 对 tty 核心注册和注销一个单个 tty 设备的函数. ~~~ void tty_insert_flip_char(struct tty_struct *tty, unsigned char ch, char flag); ~~~ 插入字符到 tty 设备的要被用户读的 flip 缓冲的函数. ~~~ TTY_NORMAL TTY_BREAK TTY_FRAME TTY_PARITY TTY_OVERRUN ~~~ flag 参数的不同值, 用在 tty_insert_flip_char 函数. ~~~ int tty_get_baud_rate(struct tty_struct *tty); ~~~ 函数, 获取当前为特定 tty 设备设置的波特率. ~~~ void tty_flip_buffer_push(struct tty_struct *tty); ~~~ 函数, 将当前 flip 缓冲中的数据推给用户. ~~~ tty_std_termios ~~~ 变量, 使用一套通用的缺省线路设置来初始化一个 termios 结构.
';

18.8. tty_struct 结构的细节

最后更新于:2022-04-01 03:01:27

## 18.8. tty_struct 结构的细节 tty_struct 变量被 tty 核心用来保持当前的特定 tty 端口的状态. 几乎它的所有的朋友都只被 tty 核心使用, 有几个例外. 一个 tty 驱动可以使用的成员在此描述: unsigned long flags; tty 设备的当前状态. 这是一个位段变量, 并且通过下面的宏定义存取: TTY_THROTTLED 当驱动以及有抑制函数被调用. 不应当被一个 tty 驱动设置, 只有 tty 核心. TTY_IO_ERROR 由驱动设置当它不想任何数据被读出或写入驱动. 如果一个用户程序试图做这个, 它接收一个 -EIO 错误从内核中. 这常常在设备被关闭时设置. TTY_OTHER_CLOSED 只由 pty 驱动使用来通知, 当端口已经被关闭. TTY_EXCLUSIVE 由 tty 核心设置来指示一个端口在独占模式并且只能一次由一个用户存取. TTY_DEBUG 内核中任何地方都不用. TTY_DO_WRITE_WAKEUP 如果被设置, 线路规程的 write_wakeup 函数被允许来被调用. 常常在 tty_driver 调用 wake_up_interruptible 函数的同一时间被调用. TTY_PUSH 只被缺省的 tty 线路规程内部使用. TTY_CLOSING tty 核心用来跟踪是否一个端口在那个时刻及时处于关闭过程. TTY_DONT_FLIP 被缺省的 tty 线路规程用来通知 tty 核心, 它不应当改变 flip 缓冲, 当它被置位. TTY_HW_COOK_OUT 如果被一个 tty 驱动设置, 它通知线路规程应当"烹调"发送给它的输出. 如果它没有设置, 线路规程成块拷贝驱动的输出; 否则, 它不得不为线路改变将单个发送的字节逐个求值. 这个标志应当通常不被 tty 驱动设置. TTY_HW_COOK_IN 几乎和设置在驱动中的 flag 变量中的 TTY_DRIVER_REAL_RAW 标志一致. 这个标志通常应当不被 tty 驱动设置. TTY_PTY_LOCK pty 驱动用来加锁和解锁一个端口. TTY_NO_WRITE_SPLIT 如果设置, tty 核心不将对 tty 驱动的写分成正常大小的块. 这个值不应当用来阻止对 tty 端口通过发送大量数据到端口的DoS攻击, struct tty_flip_buffer flip; 给 tty 设备的 flip 缓冲. struct tty_ldisc ldisc; 给 tty 设备的线路规程. wait_queue_head_t write_wait; 给 tty 写函数的 wait_queue. 一个 tty 驱动应当唤醒它,当它可以接收更多数据时. struct termios *termios; 指向 tty 设备的当前 termios 设置的指针. unsigned char stopped:1; 指示是否 tty 设备被停止. tty 驱动可以设置这个值. unsigned char hw_stopped:1; 指示是否 tty 设备的已经被停止. tty 驱动可以设置这个值. unsigned char low_latency:1; 指示是否 tty 设备是一个低反应周期的设备, 能够高速接收数据. tty 驱动可以设置这个值. unsigned char closing:1; 指示是否 tty 设备在关闭端口当中. tty 驱动可以设置这个值. struct tty_driver driver; 当前控制这个 tty 设备的 tty_driver 结构. void *driver_data; 指针, tty_driver 可以用来存储对于 tty 驱动本地的数据. 这个变量不被 tty 核心修改.
';

18.7. tty_operaions 结构的细节

最后更新于:2022-04-01 03:01:25

## 18.7. tty_operaions 结构的细节 tty_operations 结构包含所有的函数回调, 可以被一个 tty 驱动设置和被 tty 核心调用. 当前, 所有包含在这个结构中的的函数指针也在 tty_driver 结构中, 但是会很快被只有一个这个结构的实例来替代. int (*open)(struct tty_struct * tty, struct file * filp); open 函数. void (*close)(struct tty_struct * tty, struct file * filp); close 函数. int (*write)(struct tty_struct * tty, const unsigned char *buf, int count); write 函数. void (*put_char)(struct tty_struct *tty, unsigned char ch); 单字节写函数. 这个函数被 tty 核心调用当单个字节被写入设备. 如果一个 tty 驱动没有定义这个函数, write 函数被调用来替代, 当 tty 核心想发送一个单个字节. void (*flush_chars)(struct tty_struct *tty);void (*wait_until_sent)(struct tty_struct *tty, int timeout); 刷新数据到硬件的函数. int (*write_room)(struct tty_struct *tty); 指示多少缓冲空闲的函数. int (*chars_in_buffer)(struct tty_struct *tty); 指示多少缓冲满数据的函数. int (*ioctl)(struct tty_struct *tty, struct file * file, unsigned int cmd, unsigned long arg); ioctl 函数. 这个函数被 tty 核心调用, 当 ioctl(2)在设备节点上被调用时. void (*set_termios)(struct tty_struct *tty, struct termios * old); set_termios 函数. 这个函数被 tty 核心调用, 当设备的 termios 设置已被改变时. void (*throttle)(struct tty_struct * tty);void (*unthrottle)(struct tty_struct * tty);void (*stop)(struct tty_struct *tty);void (*start)(struct tty_struct *tty); 数据抑制函数. 这些函数用来帮助控制 tty 核心的输入缓存. 这个抑制函数被调用当 tty 核心的输入缓冲满. tty 驱动应当试图通知设备不应当发送字符给它. unthrottle 函数被调用当 tty 核心的输入缓冲已被清空, 并且它现在可以接收更多数据. tty 驱动应当接着通知设备可以接收数据. stop 和 start 函数非常象 throttle 和 unthrottle 函数, 但是它们表示 tty 驱动应当停止发送数据给设备以及以后恢复发送数据. void (*hangup)(struct tty_struct *tty); 挂起函数. 这个函数被调用当 tty 驱动应当挂起 tty 设备. 任何需要做的特殊的硬件操作应当在此时发生. void (*break_ctl)(struct tty_struct *tty, int state); 线路中断控制函数. 这个函数被调用当这个 tty 驱动要打开或关闭线路的 BREAK 状态在 RS-232 端口上. 如果状态设为 -1, BREAK 状态应当打开. 如果状态设为 0, BREAK 状态应当关闭. 如果这个函数由 tty 驱动实现, tty 核心将处理 TCSBRK, TCSBRKP, TIOCSBRK, 和 TIOCCBRK ioctl. 否则, 这些 ioctls 被发送给驱动 ioctl 函数. void (*flush_buffer)(struct tty_struct *tty); 刷新缓冲和丢失任何剩下的数据. void (*set_ldisc)(struct tty_struct *tty); 设置线路规程的函数. 这个函数被调用当 tty 核心已改变这个 tty 驱动的线路规程. 这个函数通常不用并且不应当被一个驱动定义. void (*send_xchar)(struct tty_struct *tty, char ch); 发送 X-类型 字符 的函数. 这个函数用来发送一个高优先级 XON 或者 XOFF 字符给 tty 设备. 要被发送的字符在 ch 变量中指定. int (*read_proc)(char *page, char **start, off_t off, int count, int *eof, void *data);int (*write_proc)(struct file *file, const char *buffer, unsigned long count, void *data); /proc 读和写函数. int (*tiocmget)(struct tty_struct *tty, struct file *file); 获得当前的特定 tty 设备的线路设置. 如果从 tty 设备成功获取到, 应当返回这个值给调用者. int (*tiocmset)(struct tty_struct *tty, struct file *file, unsigned int set, unsigned int clear); 设置当前的特定 tty 设备的线路设置. set 和 clear 包含了去设置或者清除的不同的线路设置.
';

18.6. tty_driver 结构的细节

最后更新于:2022-04-01 03:01:23

## 18.6. tty_driver 结构的细节 tty_driver 结构用来注册一个 tty 驱动到 tty 核心. 这是结构中所有不同的成员的列表和如何被 tty 核心使用: struct module *owner; 这个驱动的模块拥有者. int magic; 给这个结构的"魔术"值. 应当一直设为 TTY_DRIVER_MAGIC. 在 alloc_tty_driver 函数中被初始化. const char *driver_name; 驱动的名子, 用在 /proc/tty 和 sysfs. const char *name; 驱动的节点名. int name_base; 使用的起始数字, 当创建设备名子时. 当内核创建分配给这个 tty 驱动的一个特定 tty 设备的字符串表示是使用. short major; 驱动的主编号 short minor_start; 驱动的开始次编号. 这常常设为 name_base 的相同值. 典型地, 这个值设为 0. short num; 分配给这个驱动的次编号个数. 如果整个主编号范围被驱动使用了, 这个值应当设为 255. 这个变量在 alloc_tty_driver 函数中初始化. short type;short subtype; 描述什么类型的 tty 驱动在注册到 tty 核心. subtype 的值依赖于 type. type 成员可能是: TTY_DRIVER_TYPE_SYSTEM 由 tty 子系统内部使用来记住它在处理一个内部 tty 驱动. subtype 应当设为 SYSTEM_TYPE_TTY, SYSTEM_TYEP_CONSOLE, SYSTEM_TYPE_SYSCONS, 或者 SYSTEM_TYPE_SYSPTMX. 这个类型不应当被任何"正常" tty 驱动使用. TTY_DRIVER_TYPE_CONSOLE 仅被控制台驱动使用. TTY_DRIVER_TYPE_SERIAL 被任何串行类型驱动使用. subtype 应当设为 SERIAL_TYPE_NORMAL 或者 SERIAL_TYPE_CALLOUT, 根据你的驱动是什么类型. 这是 type 成员的其中一个最普遍的设置. TTY_DRIVER_TYPE_PTY 被伪控制台接口(pty)使用. subtype 需要被设置为 PTY_TYPE_MASTER 或者 PTY_TYPE_SLAVE. struct termios init_termios; 当创建设备时的初始化 struct termios 值. int flags; 驱动标志, 如同本章前面描述的. struct proc_dir_entry *proc_entry; 这个驱动的 /proc 入口结构. 它由 tty 核心创建如果驱动实现了 write_proc 或者 read_proc 函数. 这个成员不应当由 tty 驱动自己设置. struct tty_driver *other; 指向一个 tty 从驱动. 这只被 pty 驱动使用, 并且不应当被其他的 tty 驱动使用. void *driver_state; tty 驱动的内部状态. 应当只被 pty 驱动使用. struct tty_driver *next;struct tty_driver *prev; 连接变量. 这些变量被 tty 核心使用来连接所有的不同 tty 驱动, 并且不应当被任何 tty 驱动碰.
';

18.5. TTY 设备的 proc 和 sysfs 处理

最后更新于:2022-04-01 03:01:21

## 18.5. TTY 设备的 proc 和 sysfs 处理 tty 核心提供一个非常容易的方式给任何 tty 驱动来维护一个文件在 /proc/tty/driver 目录中. 如果驱动定义 read_proc 或者 write_proc 函数, 这个文件被创建. 接着, 任何在这个文件上的读或写调用被发送给这个驱动. 这些函数的格式只象标准的 /proc 文件处理函数. 作为一个例子, 由一个简单的 read_proc tty 回调实现, 只是打印出当前注册的端口号: ~~~ static int tiny_read_proc(char *page, char **start, off_t off, int count, int *eof, void *data) { struct tiny_serial *tiny; off_t begin = 0; int length = 0; int i; length += sprintf(page, "tinyserinfo:1.0 driver:%s\n", DRIVER_VERSION); for (i = 0; i < TINY_TTY_MINORS && length < PAGE_SIZE; ++i) { tiny = tiny_table[i]; if (tiny == NULL) continue; length += sprintf(page+length, "%d\n", i); if ((length + begin) > (off + count)) goto done; if ((length + begin) < off) { begin += length; length = 0; } } *eof = 1; done: if (off >= (length + begin)) return 0; *start = page + (off-begin); return (count < begin+length-off) ? count : begin + length-off; } ~~~ tty 核心处理所有的 sysfs 目录和设备创建, 当 tty 驱动被注册时, 或者当单个 tty 设备被创建时, 依赖在 struct tty_driver 中的 TTY_DRIVER_NO_DEVFS 标志. 单个目录一直包含 dev 文件, 它允许用户空间工具来决定分配给设备的主次号. 它还包含一个 device 和 driver 符号连接, 如果一个指向有效的 struct device 的指针被传递给读 tty_register_device 的调用. 除了这 3 个文件, 对单个 tty 驱动不可能在这个位置创建新的 sysfs 文件. 这个会可能在将来的内核发行中改变.
';

18.4. ioctls 函数

最后更新于:2022-04-01 03:01:18

## 18.4. ioctls 函数 在 struct tty_driver 中的 ioctl 函数被 tty 核心调用当 ioctl(2) 被在设备节点上调用. 如果这个 tty 驱动不知道如何处理传递给它的 ioctl 值, 它应当返回 -ENOIOCTLCMD 来试图让 tty 核心实现一个通用的调用版本. 2.6 内核定义了大约 70 个不同的 tty ioctls, 可被用来发送给一个 tty 驱动. 大部分的 tty 驱动不处理它们全部, 但是只有一个小的更普通的子集. 这是一个更通用的 tty ioctls 列表, 它们的含义, 以及如何实现它们: TIOCSERGETLSR 获得这个 tty 设备的线路状态寄存器( LSR )的值. TIOCGSERIAL 获得串口线信息. 调用者可以潜在地从 tty 设备获得许多串口线路信息, 在这个调用中一次全部. 一些程序( 例如 setserial 和 dip) 调用这个函数来确保波特率被正确设置, 以及来获得通常的关于驱动控制的设备类型信息. 调用者传递一个指向一个大的 serial_struct 结构的指针, 这个结构应当由 tty 驱动填充正确的值. 这是一个如何实现这个的例子: ~~~ static int tiny_ioctl(struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_serial *tiny = tty->driver_data; if (cmd == TIOCGSERIAL) { struct serial_struct tmp; if (!arg) return -EFAULT; memset(&tmp, 0, sizeof(tmp)); tmp.type = tiny->serial.type; tmp.line = tiny->serial.line; tmp.port = tiny->serial.port; tmp.irq = tiny->serial.irq; tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ; tmp.xmit_fifo_size = tiny->serial.xmit_fifo_size; tmp.baud_base = tiny->serial.baud_base; tmp.close_delay = 5*HZ; tmp.closing_wait = 30*HZ; tmp.custom_divisor = tiny->serial.custom_divisor; tmp.hub6 = tiny->serial.hub6; tmp.io_type = tiny->serial.io_type; if (copy_to_user((void __user *)arg, &tmp, sizeof(tmp))) return -EFAULT; return 0; } return -ENOIOCTLCMD; } ~~~ TIOCSSERIAL 设置串口线路信息. 这是 IOCGSERIAL 的反面, 并且允许用户一次全部设置 tty 设备的串口线状态. 一个指向 struct serial_struct 的指针被传递给这个调用, 填满这个 tty 设备应当被设置的数据. 如果这个 tty 驱动没有实现这个调用, 大部分程序仍然正确工作. TIOCMIWAIT 等待 MSR 改变. 用户在非寻常的情况下请求这个 ioctl, 它想在内核中睡眠直到这个 tty 设备的 MSR 寄存器发生某些事情. arg 参数包含用户在等待的事件类型. 这通常用来等待直到一个状态线变化, 指示有更多的数据发送给设备. 当实现这个 ioctl 时要小心, 并且不要使用 interruptible_sleep_on 调用, 因为它是不安全的(有很多不好的竞争条件涉及它). 相反, 一个 wait_queue 应当用来避免这个问题. 这是一个如何实现这个 ioctl 的例子: ~~~ static int tiny_ioctl(struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_serial *tiny = tty->driver_data; if (cmd == TIOCMIWAIT) { DECLARE_WAITQUEUE(wait, current); struct async_icount cnow; struct async_icount cprev; cprev = tiny->icount; while (1) { add_wait_queue(&tiny->wait, &wait); set_current_state(TASK_INTERRUPTIBLE); schedule(); remove_wait_queue(&tiny->wait, &wait); /* see if a signal woke us up */ if (signal_pending(current)) return -ERESTARTSYS; cnow = tiny->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; } ~~~ 在 tty 驱动的代码中能知道 MSR 寄存器改变的某些地方, 下面的代码行必须调用以便这个代码能正常工作: ~~~ wake_up_interruptible(&tp->wait); ~~~ TIOCGICOUNT 获得中断计数. 当用户要知道已经产生多少串口线中断时调用. 如果驱动有一个中断处理, 它应当定义一个内部计数器结构来跟踪这些统计和递增适当的计数器, 每次这个函数被内核运行时. 这个 ioctl 调用传递内核一个指向结构 serial_icounter_struct 的指针, 它应当被 tty 驱动填充. 这个调用常常和之前的 IOCMIWAIT ioctl 调用结合使用. 如果 tty 驱动跟踪所有的这些中断在驱动操作时, 实现这个调用的代码会非常简单: ~~~ static int tiny_ioctl(struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_serial *tiny = tty->driver_data; if (cmd == TIOCGICOUNT) { struct async_icount cnow = tiny->icount; struct serial_icounter_struct icount; icount.cts = cnow.cts; icount.dsr = cnow.dsr; icount.rng = cnow.rng; icount.dcd = cnow.dcd; icount.rx = cnow.rx; icount.tx = cnow.tx; icount.frame = cnow.frame; icount.overrun = cnow.overrun; icount.parity = cnow.parity; icount.brk = cnow.brk; icount.buf_overrun = cnow.buf_overrun; if (copy_to_user((void __user *)arg, &icount, sizeof(icount))) return -EFAULT; return 0; } return -ENOIOCTLCMD; } ~~~
';

18.3. TTY 线路设置

最后更新于:2022-04-01 03:01:16

## 18.3. TTY 线路设置 当一个用户要改变一个 tty 设备的线路设置或者获取当前线路设置, 他调用一个许多的不同 termios 用户空间库函数或者直接对这个 tty 设备的节点调用 ioctl. tty 核心转换这 2 种接口为许多不同的 tty 驱动函数回调和 ioctl 调用. ### 18.3.1. set_termios 函数 大部分 termios 用户空间函数被库转换为一个对驱动节点的 ioctl 调用. 大量的不同的 tty ioctl 调用接着被 tty 核心转换为一个对 tty 驱动的单个 set_termios 函数调用. set_termios 调用需要决定哪个线路设置它被请求来改变, 接着在 tty 设备中做这些改变. tty 驱动必须能够解码所有的在 termios 结构中的不同设置并且响应任何需要的改变. 这是一个复杂的任务, 因为所有的线路设置以很多的方式被包装进 termios 结构. 一个 set_termios 函数应当做的第一件事情是决定任何事情是否真的需要改变. 这可使用下面的代码完成: ~~~ 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 变量是第一个被检查来看是否它指向一个有效的结构, 在它被存取之前. 这是需要的, 因为有时这个变量被设为 NULL. 试图存取一个 NULL 指针成员会导致内核崩溃. 为查看需要的字节大小, CSIZE 位掩码可用来从 cflag 变量区分出正确的位. 如果这个大小无法知道, 习惯上确实是 8 个数据位. 这个可如下实现: ~~~ /* 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; } ~~~ 为决定需要的奇偶值, PARENB 位掩码可对 cflag 变量检查来告知是否任何奇偶要被设置. 如果这样, PARODD 位掩码可用来决定是否奇偶应当是奇或者偶. 这个的一个实现是: ~~~ /* 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"); ~~~ 请求的停止位也可使用 CSTOPB 位掩码从 cflag 变量中来知道. 一个实现是: ~~~ /* figure out the stop bits requested */ if (cflag & CSTOPB) printk(KERN_DEBUG " - stop bits = 2\n"); else printk(KERN_DEBUG " - stop bits = 1\n"); ~~~ 有 2 个基本的流控类型: 硬件和软件. 为确定是否用户要求硬件流控, CRTSCTS 位掩码用来对 cflag 变量检查. 它的一个例子是: ~~~ /* figure out the hardware 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 核心提供了一个函数, tty_get_baud_rate, 来帮助做这个. 这个函数返回一个整型数指示请求的波特率给特定的 tty 设备. ~~~ /* get the baud rate wanted */ printk(KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty)); ~~~ 现在 tty 驱动已经确定了所有的不同的线路设置, 它可以基于这些值正确设置硬件. ### 18.3.2. tiocmget 和 tiocmset 在 2.4 和老的内核, 常常有许多 tty ioctl 调用来获得和设置不同的控制线路设置. 这些被常量 TIOCMGET, TIOCMBIS, TIOCMBIC, 和 TIOCMSET 表示. TIOCMGET 用来获得内核的线路设置值, 并且对于 2.6 内核, 这个 ioctl 调用已经被转换为一个 tty 驱动回调函数, 称为 tiocmget. 其他的 3 个 ioctls 已经被简化并且现在用单个的 tty 驱动回调函数所代表, 称为 tiocmset. tty 驱动中的 iocmget 函数被 tty 核心所调用, 当核心需要知道当前的特定 tty 设备的控制线的物理值. 这常常用来获取一个串口的 DTR 和 RTS 线的值. 如果 tty 驱动不能直接读串口的 MSR 或者 MCR 寄存器, 因为硬件不允许这样, 一个它们的拷贝应当在本地保持. 许多 USB-到-串口 驱动必须实现这类的"影子"变量. 这是这个函数能如何被实现, 任何一个本地的这些值的拷贝被保存: ~~~ static int tiny_tiocmget(struct tty_struct *tty, struct file *file) { struct tiny_serial *tiny = tty->driver_ data; unsigned int result = 0; unsigned int msr = tiny->msr; unsigned int mcr = tiny->mcr; result = ((mcr & MCR_DTR) ? TIOCM_DTR : 0) | /* DTR is set */ ((mcr & MCR_RTS) ? TIOCM_RTS : 0) | /* RTS is set */ ((mcr & MCR_LOOP) ? TIOCM_LOOP : 0) | /* LOOP 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 */ return result; } ~~~ 在 tty 驱动中的 tiocmset 函数被 tty 核心调用, 当核心要设置一个特定 tty 设备的控制线值. tty 核心告知 tty 驱动设置什么值和清理什么, 通过传递它们用 2 个变量: set 和 clear. 这些变量包含一个应当改变的线路设置的位掩码. 一个 ioctl 调用从不请求驱动既设置又清理一个特殊的位在同一时间, 因此先发生什么操作没有关系. 这是一个例子, 关于这个函数如何能够由一个 tty 驱动实现: ~~~ static int tiny_tiocmset(struct tty_struct *tty, struct file *file, unsigned int set , unsigned int clear) { struct tiny_serial *tiny = tty->driver_data; unsigned int mcr = tiny->mcr; if (set & TIOCM_RTS) mcr |= MCR_RTS; if (set & TIOCM_DTR) mcr |= MCR_RTS; if (clear & TIOCM_RTS) mcr &= ~MCR_RTS; if (clear & TIOCM_DTR) mcr &= ~MCR_RTS; /* set the new MCR value in the device */ tiny->mcr = mcr; return 0; } ~~~
';

18.2. tty_driver 函数指针

最后更新于:2022-04-01 03:01:14

## 18.2. tty_driver 函数指针 最终, tiny_tty 驱动声明了 4 个函数指针. ### 18.2.1. open 和 close open 函数被 tty 核心调用, 当一个用户对这个 tty 驱动被分配的设备节点调用 open 时. tty 核心使用一个指向分配给这个设备的 tty_struct 结构的指针调用它, 还用一个文件指针. 这个 open 成员必须被一个 tty 驱动为它能正确工作而设置; 否则, -ENODEV 被返回给用户当调用 open 时. 当调用这个 open 函数, tty 驱动被期望或者保存一些传递给它的 tty_struct 变量中的数据, 或者保存一个可以基于端口次编号来引用的静态数组中的数据. 这是有必要的, 所以 tty 驱动知道哪个设备在被引用当以后的 close, write, 和其他函数被调用时. tiny_tty 驱动保存一个指针在 tty 结构中, 如同下面代码所见到: ~~~ static int tiny_open(struct tty_struct *tty, struct file *file) { struct tiny_serial *tiny; struct timer_list *timer; int index; /* initialize the pointer in case something fails */ tty->driver_data = NULL; /* get the serial object associated with this tty pointer */ index = tty->index; tiny = tiny_table[index]; if (tiny == NULL) { /* first time accessing this device, let's create it */ tiny = kmalloc(sizeof(*tiny), GFP_KERNEL); if (!tiny) return -ENOMEM; init_MUTEX(&tiny->sem); tiny->open_count = 0; tiny->timer = NULL; tiny_table[index] = tiny; } down(&tiny->sem); /* save our structure within the tty structure */ tty->driver_data = tiny; tiny->tty = tty; ~~~ 在这个代码中, tiny_serial 结构被保存在 tty 结构中. 这允许 tiny_write, tiny_write_room, 和 tiny_close 函数来获取 tiny_serial 结构和正确操作它. tiny_serial 结构定义为: ~~~ struct tiny_serial { struct tty_struct *tty; /* pointer to the tty for this device */ int open_count; /* number of times this port has been opened */ struct semaphore sem; /* locks this structure */ struct timer_list *timer; }; ~~~ 如同我们已见到的, open_count 变量初始化为 0 在第一次打开端口的 open 调用中. 这是一个典型的引用计数, 因为一个 tty 驱动的 open 和 close 函数可能对同一个设备多次调用以便多个进程来读写数据. 为正确处理所有的事情, 必须保持一个这个端口被打开或者关闭的次数计数; 这个驱动递增和递减这个计数在打开使用时. 当打开第一次被打开, 任何必要的硬件初始化和内存分配可以做. 当端口被最后一次关闭, 任何必要的硬件关闭和内存清理可以做. tiny_open 函数的剩下部分展示了如何跟踪设备被打开的次数: ~~~ ++tiny->open_count; if (tiny->open_count == 1) { /* this is the first time this port is opened */ /* do any hardware initialization needed here */ ~~~ open 函数必须返回或者一个负的错误号如果发生事情阻止了成功打开, 或者一个 0 来表示成功. close 函数指针被 tty 核心调用, 在用户对前面使用 open 调用而创建的文件句柄调用 close 时. 这表示设备应当在这次被关闭. 但是, 因为 open 函数可被多次调用, close函数也可多次调用. 因此这个函数应当跟踪它被调用的次数来决定是否硬件应当在此次真正被关闭. tiny_tty 驱动做这个使用下面的代码: ~~~ static void do_close(struct tiny_serial *tiny) { down(&tiny->sem); if (!tiny->open_count) { /* port was never opened */ goto exit; } --tiny->open_count; if (tiny->open_count <= 0) { /* The port is being closed by the last user. */ /* Do any hardware specific stuff here */ /* shut down our timer */ del_timer(tiny->timer); } exit: up(&tiny->sem); } static void tiny_close(struct tty_struct *tty, struct file *file) { struct tiny_serial *tiny = tty->driver_data; if (tiny) do_close(tiny); } ~~~ tiny_close 函数只是调用 do_close 函数来完成实际的关闭设备工作. 因此关闭逻辑不必在这里和驱动被卸载和端口被打开时重复. close 函数没有返回值, 因为它不被认为会失败. ### 18.2.2. 数据流 write 函数被用户在有数据发送给硬件时调用. 首先 tty 核心接收到调用, 接着它传递数据到 tty 驱动的 write 函数. tty 核心还告知 tty 驱动要发送的数据大小. 有时, 因为速度和 tty 硬件的缓冲区容量, 不是所有的写程序要求的字符可以在调用写函数时发送. 这个写函数应当返回能够发送给硬件的字符数( 或者在以后时间可排队发送 ), 因此用户程序可以检查是否所有的数据真正写入. 这种检查在用户空间非常容易完成, 比一个内核驱动站着睡眠直到所有的请求数据能够被发送. 如果任何错误发生在 wirte 调用期间, 一个负的错误值应当被返回代替被写入的字节数. write 函数可从中断上下文和用户上下文中被调用. 知道这一点是重要的, 因为 tty 驱动不应当调用任何可能当它在中断上下文中睡眠的函数. 这些包括任何可能调用调度的函数, 例如普通的函数 copy_from_user, kmalloc, 和 printk. 如果你确实想睡眠, 确信去首先检查是否驱动在中断上下文, 通过调用 calling_in_interrupt. 这个例子 tiny tty 驱动没有连接到任何真实的硬件, 因此它的写函数简单地将要写的什么数据记录到内核调试日志. 它使用下面的代码做这个: ~~~ static int tiny_write(struct tty_struct *tty, const unsigned char *buffer, int count) { struct tiny_serial *tiny = tty->driver_data; int i; int retval = -EINVAL; if (!tiny) return -ENODEV; down(&tiny->sem); if (!tiny->open_count) /* port was not opened */ goto exit; /* fake sending the data out a hardware port by * writing it to the kernel debug log. */ printk(KERN_DEBUG "%s - ", __FUNCTION__); for (i = 0; i < count; ++i) printk("%02x ", buffer[i]); printk("\n"); exit: up(&tiny->sem); return retval; } ~~~ 当 tty 子系统自己需要发送数据到 tty 设备之外, write 函数被调用. 如果 tty 驱动在 tty_struct 中没有实现 put_char 函数, 这会发生. 在这种情况下, tty 核心用一个数据大小为 1 来使用 write 函数回调. 这普遍发生在 tty 核心想转换一个新行字符为一个换行和新行字符. 这里的最大的问题是 tty 驱动的 write 函数必须不返回 0 对于这类的调用. 这意味着驱动必须写那个数据的字节到设备, 因为调用者( tty 核心 ) 不缓冲数据和在之后的时间重试. 因为 write 函数不能知道是否它在被调用来替代 put_char, 即便只有一个字节的数据被发送, 尽力实现 write 函数以至于它一直至少在返回前写一个字节. 许多当前的 USB-到-串口的 tty 驱动没有遵照这个规则, 并且因此, 一些终端类型不能正确工作当连接到它们时. write_room 函数被调用当 tty 核心想知道多少空间在写缓冲中 tty 驱动可用. 这个数字时时改变随着字符清空写缓冲以及调用写函数时, 添加字符到这个缓冲. ~~~ static int tiny_write_room(struct tty_struct *tty) { struct tiny_serial *tiny = tty->driver_data; int room = -EINVAL; if (!tiny) return -ENODEV; down(&tiny->sem); if (!tiny->open_count) { /* port was not opened */ goto exit; } /* calculate how much room is left in the device */ room = 255; exit: up(&tiny->sem); return room; } ~~~ ### 18.2.3. 其他缓冲函数 一个工作的 tty 驱动不需要在 tty_driver 结构中的 chars_in_buffer 函数, 但是它被推荐. 这个函数被调用当 tty 核心想知道多少字符仍然保留在 tty 驱动的写缓冲中要被发送. 如果驱动能够存储字符在它发送它们到硬件之前, 它应当实现这个函数为了 tty 核心能够知道是否所有的驱动中的数据已经流出. 3 个 tty_driver 结构中的函数回调可以用来刷新任何驱动保留的数据. 它们不被要求实现, 但是推荐如果 tty 驱动能够缓冲数据在它发送给硬件之前. 前 2 个函数回调称为 flush_chars 和 wait_until_sent. 这些函数被调用当 tty 核心使用 put_char 函数回调已发送了许多字符给 tty 驱动. flush_chars 函数回调被调用当 tty 核心要 tty 驱动启动发送这些字符到硬件, 如果它尚未启动. 这个函数被允许在所有的数据发送给硬件之前返回. wait_until_sent 函数回调以非常相同的发生工作; 但是它必须等待直到所有的字符在返回到 tty 核心前被发送, 或者知道超时值到时. 如果这个传递给 wait_until_sent 函数回调的超时值设为 0, 函数应当等待直到它完成这个操作. 剩下的数据刷新函数回调是 flush_buffer. 它被 tty 核心调用当 tty 驱动要刷新所有的仍然在它的写缓冲的数据. 任何保留在缓冲中的数据被丢失并且没发送给设备. ### 18.2.4. 无 read 函数? 只使用这些函数, tiny_tty 驱动可被注册, 可打开一个设备节点, 数据被写入设备, 关闭设备节点, 以驱动注销和从内核中卸载. 但是 tty 核心和 tty_driver 结构没有提供一个 read 函数; 换句话说, 没有函数调用存在来从驱动到 tty 核心获取数据. 替代一个传统的 read 函数, tty 驱动负责发送任何从硬件收到的数据到 tty 核心. tty 核心缓冲数据直到它被用户请求. 因为 tty 核心提供的缓冲逻辑, 对每个 tty 驱动不必要实现它自己的缓冲逻辑. tty 核心通知 tty 驱动当一个用户要驱动停止和开始发送数据, 但是如果内部的 tty 缓冲满, 没有这样的通知发生. tty 核心缓冲由 tty 驱动接收到的数据, 在一个称为 struct tty_flip_buffer 的结构中. 一个 flip 缓冲是一个结构包含 2 个主要数据数组. 从 tty 设备接收到的数据被存储于第一个数组. 当这个数组满, 任何等待数据的用户被通知数据可以读. 当用户从这个数组读数据, 任何新到的数据被存储在第 2 个数组. 当那个数组被读空, 数据再次刷新给用户, 并且驱动开始填充第 1 个数组. 本质上, 被接收的数据 "flips" 从一个缓冲到另一个, 期望不会溢出它们 2 个. 为试图阻止数据丢失, 一个 tty 驱动可以监视到来的数组多大, 并且, 如果它添满, 及时告知 tty 驱动在这个时刻刷新缓冲, 而不是等待下一个可用的机会. struct tty_flip_buffer 结构的细节对 tty 驱动没有关系, 只有一个例外, 可用的计数. 这个变量包含多少字节当前留在缓冲里可用来接收数据. 如果这个值等于值 TTY_FLIPBUF_SIZE, 这个 flip 缓冲需要被刷新到用户, 使用一个对 tty_flip_buffer_push 的调用. 这展示在下面的代码: ~~~ for (i = 0; i < data_size; ++i) { if (tty->flip.count >= TTY_FLIPBUF_SIZE) tty_flip_buffer_push(tty); tty_insert_flip_char(tty, data[i], TTY_NORMAL); } tty_flip_buffer_push(tty); ~~~ 从 tty 驱动接收来的要发送给用户的字符被添加到 flip 缓冲, 使用对 tty_insert_flip_char 的调用. 这个函数的第一个参数是数据应当保存入的 struct tty_struct, 第 2 个参数是要保存的字符, 第 3 个参数是任何应当为这个字符设置的标志. 这个标志值应当设为 TTY_NORMAL 如果这个是一个正常的被接收的字符. 如果这是一个特殊类型的指示错误接收数据的字符, 它应当设为 TTY_BREAK, TTY_PARITY, 或者 TTY_OVERRUN, 取决于错误. 为了"推"数据给用户, 进行一个对 tty_flip_buffer_push 的调用. 这个函数应当也被调用如果 flip 缓冲将要溢出, 如同在这个例子中展示的. 因此无论何时数据被加到 flip 缓冲, 或者当 flip 缓冲满, tty 驱动必须调用 tty_flip_buffer_push. 如果 tty 驱动可高速接收数据, tty->low_latency 标志应当设置, 它是对 tty_flip_buffer_pus 的调用被立刻执行当调用时. 否则, tty_flip_buffer_push 调用会调度它自己来将数据推出缓冲, 在之后近期的一个时间点.
';

18.1. 一个小 TTY 驱动

最后更新于:2022-04-01 03:01:11

## 18.1. 一个小 TTY 驱动 为解释 tty 核心如何工作, 我们创建一个小 tty 驱动, 可以被加载, 以及写入读出, 并且卸载. 任何一个 tty 驱动的主要数据结构是 struct tty_driver. 它用来注册和注销一个 tty 驱动到 tty 内核, 在内核头文件 <linux/tty_driver.h> 中描述. 为创建一个 struct tty_driver, 函数 alloc_tty_driver 必须用这个驱动作为参数而支持的 tty 设备号来调用. 这可使用下面的简短代码来完成: ~~~ /* allocate the tty driver */ tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS); if (!tiny_tty_driver) return -ENOMEM; ~~~ 在 alloc_tty_driver 函数被成功调用后, struct tty_driver 应当用基于 tty 驱动的需要的正确信息被初始化. 这个结构包含很多不同成员, 但不是为了有一个可工作的 tty 驱动而全部都必须被初始化. 这里有一个例子展示如何初始化这个结构并且建立足够的成员来创建一个工作的 tty 驱动. 它使用 tty_set_operations 函数来帮助拷贝驱动中定义的函数操作集合: ~~~ static struct tty_operations serial_ops = { .open = tiny_open, .close = tiny_close, .write = tiny_write, .write_room = tiny_write_room, .set_termios = tiny_set_termios, }; ... /* initialize the tty driver */ tiny_tty_driver->owner = THIS_MODULE; tiny_tty_driver->driver_name = "tiny_tty"; tiny_tty_driver->name = "ttty"; tiny_tty_driver->devfs_name = "tts/ttty%d"; tiny_tty_driver->major = TINY_TTY_MAJOR, tiny_tty_driver->type = TTY_DRIVER_TYPE_SERIAL, tiny_tty_driver->subtype = SERIAL_TYPE_NORMAL, tiny_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS, tiny_tty_driver->init_termios = tty_std_termios; tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL; tty_set_operations(tiny_tty_driver, &serial_ops); ~~~ 上面列出的变量和函数, 以及这个结构如何使用, 在本章的剩下部分讲解. 为注册这个驱动到 tty 核心, struct tty_driver 必须传递到 tty_register_driver 函数: ~~~ /* register the tty driver */ retval = tty_register_driver(tiny_tty_driver); if (retval) { printk(KERN_ERR "failed to register tiny tty driver"); put_tty_driver(tiny_tty_driver); return retval; } ~~~ 当调用 tty_register_driver, 内核创建了所有的不同 sysfs tty 文件为这个 tty 驱动可能有的整个范围的次设备. 如果你使用 devfs ( 本书不涉及 ) 并且除非指定 TTY_DRIVER_NO_DEVFS 标志, devfs 文件也被创建. 这个标志可被指定如果你只想为这个实际在系统中存在的设备调用 tty_register_device, 因此用户一直有一个内核中有的最新的设备视图, 这就是 devfs 用户期望的. 在注册它自己后, 这个驱动通过 tty_register_device 注册它控制的设备. 这个函数有 3 个参数: - 一个指针指向这个设备所属的 struct tty_driver. - 设备的次编号 - 一个指针指向这个 tty 设备所绑定的 struct device. 如果这个 tty 设备没绑定到任何一个 struct device, 这个参数可被设为 NULL. 我们的驱动一次注册所有的 tty 设备, 因为它们是虚拟的并且没有绑定到任何一个物理设备: ~~~ for (i = 0; i < TINY_TTY_MINORS; ++i) tty_register_device(tiny_tty_driver, i, NULL); ~~~ 为从 tty 核心注销这个驱动, 所有的通过调用 tty_register_device 而注册的 tty 设备需要使用对 tty_unregister_device 的调用来清理. 接着 struct tty_driver 必须使用一个 tty_unregister_driver 调用来注销. ~~~ for (i = 0; i < TINY_TTY_MINORS; ++i) tty_unregister_device(tiny_tty_driver, i); tty_unregister_driver(tiny_tty_driver); ~~~ ### 18.1.1. 结构 struct termios 在 struct tty_driver 中的 init_termios 变量是一个 struct termios. 这个变量被用来提供一个健全的线路设置集合, 如果这个端口在被用户初始化前使用. 驱动初始化这个变量使用一个标准的数值集, 它拷贝自 tty_std_termios 变量. tty_std_termos 在 tty 核心被定义为: ~~~ struct termios tty_std_termios = { .c_iflag = ICRNL | IXON, .c_oflag = OPOST | ONLCR, .c_cflag = B38400 | CS8 | CREAD | HUPCL, .c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN, .c_cc = INIT_C_CC }; ~~~ 这个 struct termios 结构用来持有所有的当前线路设置, 给这个 tty 设备的一个特定端口. 这些线路设置控制当前波特率, 数据大小, 数据流控设置, 以及许多其他值. 这个结构的不同成员是: tcflag_t c_iflag; 输入模式标志 tcflag_t c_oflag; 输出模式标志 tcflag_t c_cflag; 控制模式标志 tcflag_t c_lflag; 本地模式标志 cc_t c_line; 线路规程类型 cc_t c_cc[NCCS]; 一个控制字符数组 所有的模式标志被定义为一个大的位段. 模式的不同值, 以及它们用在哪里, 可以见在任何 Linux 发布中都有的 termios 手册页. 内核提供了一套有用的宏定义来获得不同的位. 这些宏定义在头文件 include/linux/tty.h 中定义. 所有的在 tiny_tty_driver 变量中定义的成员有必要有一个工作的 tty 驱动. owner 成员是为了防止 tty 驱动在 tty 端口打开时被卸载. 在以前的内核版本, 它由 tty 驱动自己负责处理模块引用计数逻辑. 但是内核程序员认为可能有困难来解决所有的不同的可能的竞争条件, 因此 tty 核心为 tty 驱动处理所有的这样的控制.. driver_name 和 name 成员看起来非常相似, 然而用于不同用途. driver_name 变量应当设为某个简单的, 描述性的并且和内核中所有 tty 驱动中是唯一的值. 这是因为它在 /proc/tty/drivers 文件中出现来描述这个驱动给用户, 以及在当前已加载的 tty 驱动的 sysfs tty 类目录. name 成员用来定义一个名子给单独的分配给这个 tty 驱动的 tty 节点在 /dev 树中. 这个字符串用来创建一个 tty 设备通过在这个字串的后面追加在使用的 tty 设备号. 它还用来创建一个设备名子在 sysfs /sys/class/tty 目录中. 如果 devfs 在内核中被使能, 这个名子应当包含任何这个 tty 驱动想被放入的子目录. 作为一个例子, 内核中的串口驱动设置这个 name 成员为 tts/ 如果 devfs 被使能, ttyS 如果它没有被使能. 这个字串也显示在 /proc/tty/drivers 文件中. 如同我们提及的, /proc/tty/drivers 文件展示所有的当前注册的 tty 驱动. 在内核中注册的 tiny_tty 驱动并且没有 devfs, 这个文件看来如下: ~~~ $ cat /proc/tty/drivers tiny_tty /dev/ttty 240 0-3 serial usbserial /dev/ttyUSB 188 0-254 serial serial /dev/ttyS 4 64-107 serial pty_slave /dev/pts 136 0-255 pty:slave pty_master /dev/ptm 128 0-255 pty:master pty_slave /dev/ttyp 3 0-255 pty:slave pty_master /dev/pty 2 0-255 pty:master unknown /dev/vc/ 4 1-63 console /dev/vc/0 /dev/vc/0 4 0 system:vtmaster /dev/ptmx /dev/ptmx 5 2 system /dev/console /dev/console 5 1 system:console /dev/tty /dev/tty 5 0 system:/dev/tty ~~~ 还有, 当 tny_tty driver 被注册到 tty 核心, sysfs 目录 /sys/class/tty 看来有些象下面: ~~~ $ tree /sys/class/tty/ttty* /sys/class/tty/ttty0 `-- dev /sys/class/tty/ttty1 `-- dev /sys/class/tty/ttty2 `-- dev /sys/class/tty/ttty3 `-- dev $ cat /sys/class/tty/ttty0/dev 240:0 ~~~ major 变量描述这个驱动的主编号是什么. type 和 subtype 变量声明这个驱动是什么 tty 驱动. 对于我们的例子, 我们是一个"正常"类型的串口驱动. 一个 tty 驱动的唯一的其他子类型可能是一个 "callout" 类型. callout 设备传统上用来控制一个设备的线路设置. 数据应当通过一个设备节点被发送和接收, 并且任何路线设置改变应当被发送给一个不同的设备节点, 它是这个 callout 设备. 这要求使用 2 个次编号为每个 tty 设备. 感激地, 所有的驱动既处理数据也处理线路设置在同一个设备节点, 并且这个 callout 类型很少用在新驱动中. tty 驱动和 tty 核心都使用 flags 变量来指示驱动的当前状态和它是什么类型 tty 驱动. 几个在测试或者操作 flags 时你必须使用的位掩码宏被定义了. flags 变量中的 3 个位可被驱动设置: TTY_DRIVER_RESET_TERMIOS 这个标志说明 tty 核心复位了 termios 设置, 无论何时最后一个进程已关闭这个设备. 对于控制台和 pty 驱动这是有用的. 例如, 假定用户留置一个终端在一个奇怪的状态. 在设置了这个标志时, 这个终端被复位为一个正常值当用户注销或者控制个会话的进程被"杀掉". TTY_DRIVER_REAL_RAW 这个标志说明 tty 驱动保证发送奇偶或者坏字符通知给线路规程. 这允许线路规程以一种更快的方式来处理接收到的字符, 因为它不必查看从 tty 驱动收到的每个字符. 因为速度的得益, 这个值常常为所有 tty 驱动设置. TTY_DRIVER_NO_DEVFS 这个标志说明当调用 tty_register_driver 时, tty 核心不创建任何 devfs 入口给这个 tty 设备. 这对任何动态创建和销毁次设备的驱动都是有益的. 设置这个的驱动的例子是这个 USB-到-串口 驱动, USB 猫驱动, USB 蓝牙 tty 驱动, 以及好多标准串口设备. 当 tty 驱动后来想注册一个特殊的 tty 设备到 tty 核心, 它必须调用 tty_register_device, 有一个指针到这个 tty 驱动, 并且设备的次编号已被创建. 如果这个没有完成, tty 核心仍然传递所有的调用到这个 tty 驱动, 但是一些内部的 tty 相关的功能可能不存在. 这个包括新 tty 设备的 /sbin/hotplug 通知和 tty 设备的 sysfs 表示. 当注册的 tty 设备从机器中被移出, tty 驱动必须调用 tty_unregister_device. The one remaining bit in this variable is controlled by the tty core and is called TTY_DRIVER_INSTALLED. This flag is set by the tty core after the driver has been regis-tered and should never be set by a tty driver. 这个变量中剩下的一位被 tty 核心控制, 被称为 TTY_DRIVER_INSTALLED. 这个标志被tty 核心在驱动已注册后设置并且应当从不被 tty 驱动设置.
';

第 18 章 TTY 驱动

最后更新于:2022-04-01 03:01:09

## 第 18 章 TTY 驱动 一个 tty 设备得名于电传打字机的很老的简称, 并且起初只和连接到一台 UNIX 机器的物理或者虚拟终端有关联. 长时间以来, 这个名子还逐渐表示任何串口类型的设备, 因为终端连接也能够在这样的一个连接上建立. 一些物理 tty 设备的例子是串口, USB-串口 转换器, 以及某些类型的需要特殊处理来正确工作的调制解调器(例如传统的 Win-Modem 类型设备). tty 虚拟设备支持虚拟控制台以用来登录到一台计算机, 或者从键盘, 或者从网络连接, 或者通过一个 xterm 会话. Linux tty 驱动的核心正好位于标准字符驱动级别之下, 并且提供了一些特性集中在为使用终端类型设备提供一个接口. 这个核心负责控制跨越一个 tty 设备的数据流和数据格式. 这允许 tty 驱动以一种一致的方式集中于处理到硬件和出自硬件的数据, 而不必担心如何控制对用户空间的接口. 为控制数据流, 有几个不同的线路规程可以虚拟地"插入"任何一个 tty 设备. 这由不同的 tty 线路规程驱动来完成. 如同图[tty 核心概览](# "图 18.1. tty 核心概览")所示, tty 核心从一个用户获取将要发送给一个 tty 设备的数据. 它接着传递它到一个 tty 线路规程驱动, 接着传递它到一个 tty 驱动. 这个 tty 驱动转换数据为可以发送给硬件的格式. 从 tty 硬件收到的数据向上回流通过 tty 驱动, 进入 tty 线路规程驱动, 再进入 tty 核心, 在这里它被一个用户获取. 有时 tty 驱动直接和 tty 核心通讯, 并且 tty 核心直接发送数据到 tty 驱动, 但是常常 tty 线路规程有机会修改在 2 者之间发送的数据. **图 18.1. tty 核心概览** ![tty 核心概览](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-02_55e6d9e922dcf.png) tty 驱动从未看见 tty 线路规程. 这个驱动不能直接和线路规程通讯, 它甚至也不知道它存在. 驱动的工作是以硬件能够理解的方式格式化发送给它的数据, 并且从硬件接收数据. tty 线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据. 这种格式化常常采用一个协议转换的形式, 例如 PPP 和 Bluetooth. 有 3 种不同类型 tty 驱动: 控制台, 串口, 和 pty. 控制台和 pty 驱动硬件已经被编写以及可能是唯一需要的 tty 驱动的类型. 这使得任何使用 tty 核心来与用户和系统交互的新驱动作为串口驱动. 为知道什么类型的 tty 驱动当前被加载到内核以及什么 tty 设备当前存在, 查看 /proc/tty/drivers 文件. 这个文件包括一个当前存在的不同 tty 驱动的列表, 显示驱动的名子, 缺省的节点名子, 驱动的主编号, 这个驱动使用的次编号范围, 以及 tty 驱动的类型. 下面是一个这个文件的例子: ~~~ /dev/tty /dev/tty 5 0 system:/dev/tty /dev/console /dev/console 5 1 system:console /dev/ptmx /dev/ptmx 5 2 system /dev/vc/0 /dev/vc/0 4 0 system:vtmaster usbserial /dev/ttyUSB 188 0-254 serial serial /dev/ttyS 4 64-67 serial pty_slave /dev/pts 136 0-255 pty:slave pty_master /dev/ptm 128 0-255 pty:master pty_slave /dev/ttyp 3 0-255 pty:slave pty_master /dev/pty 2 0-255 pty:master unknown /dev/tty 4 1-63 console ~~~ /proc/tty/driver/ 目录给一些 tty 驱动包含了单独的文件, 如果它们实现这个功能. 缺省的串口驱动创建一个文件在这个目录中来展示许多串口特定的硬件信息. 如何在这个目录建立一个文件的信息后面描述. 所有的当前注册的以及在内核中出现的 tty 设备有它们自己的子目录在 /sys/class/tty 下面. 在那个子目录下, 有一个 "dev" 文件包含有分配给那个 tty 设备的主次编号. 如果这个驱动告知内核物理设备和关联到这个 tty 设备的驱动的所在, 它创建符号连接到它们. 这个树的一个例子是: ~~~ /sys/class/tty/ |-- console | `-- dev |-- ptmx | `-- dev |-- tty | `-- dev |-- tty0 | `-- dev ... |-- ttyS1 | `-- dev |-- ttyS2 | `-- dev |-- ttyS3 | `-- dev ... |-- ttyUSB0 | |-- dev | |-- device -> ../../../devices/pci0000:00/0000:00:09.0/usb3/3-1/3-1:1.0/ttyUSB0 | `-- driver -> ../../../bus/usb-serial/drivers/keyspan_4 |-- ttyUSB1 | |-- dev | |-- device -> ../../../devices/pci0000:00/0000:00:09.0/usb3/3-1/3-1:1.0/ttyUSB1 | `-- driver -> ../../../bus/usb-serial/drivers/keyspan_4 |-- ttyUSB2 | |-- dev | |-- device -> ../../../devices/pci0000:00/0000:00:09.0/usb3/3-1/3-1:1.0/ttyUSB2 | `-- driver -> ../../../bus/usb-serial/drivers/keyspan_4 `-- ttyUSB3 |-- dev |-- device -> ../../../devices/pci0000:00/0000:00:09.0/usb3/3-1/3-1:1.0/ttyUSB3 `-- driver -> ../../../bus/usb-serial/drivers/keyspan_4 ~~~
';

17.16. 快速参考

最后更新于:2022-04-01 03:01:07

## 17.16. 快速参考 本节提供了本章中介绍的概念的参考. 也解释了每个驱动需要包含的头文件的角色. 在 net_device 和 sk_buff 结构中成员的列表, 但是, 这里没有重复. ~~~ #include <linux/netdevice.h> ~~~ 定义 struct net_device 和 struct net_device_stats 的头文件, 包含了几个其他网络驱动需要的头文件. ~~~ struct net_device *alloc_netdev(int sizeof_priv, char *name, void (*setup)(struct net_device *); struct net_device *alloc_etherdev(int sizeof_priv); void free_netdev(struct net_device *dev); ~~~ 分配和释放 net_device 结构的函数 ~~~ int register_netdev(struct net_device *dev); void unregister_netdev(struct net_device *dev); ~~~ 注册和注销一个网络设备. ~~~ void *netdev_priv(struct net_device *dev); ~~~ 获取网络设备结构的驱动私有区域的指针的函数. ~~~ struct net_device_stats; ~~~ 持有设备统计的结构. ~~~ netif_start_queue(struct net_device *dev); netif_stop_queue(struct net_device *dev); netif_wake_queue(struct net_device *dev); ~~~ 控制传送给驱动来发送的报文的函数. 没有报文被传送, 直到 netif_start_queue 被调用. netif_stop_queue 挂起发送, netif_wake_queue 重启队列并刺探网络层重启发送报文. ~~~ skb_shinfo(struct sk_buff *skb); ~~~ 宏定义, 提供对报文缓存的"shared info"部分的存取. ~~~ void netif_rx(struct sk_buff *skb); ~~~ 调用来通知内核一个报文已经收到并且封装到一个 socket 缓存中的函数. ~~~ void netif_rx_schedule(dev); ~~~ 来告诉内核报文可用并且应当启动查询接口; 它只是被 NAPI 兼容的驱动使用. ~~~ int netif_receive_skb(struct sk_buff *skb); void netif_rx_complete(struct net_device *dev); ~~~ 应当只被 NAPI 兼容的驱动使用. netif_receive_skb 是对于 netif_rx 的 NAPI 对等函数; 它递交一个报文给内核. 当一个 NAPI 兼容的驱动已耗尽接收报文的供应, 它应当重开中断, 并且调用 netif_rx_complete 来停止查询. ~~~ #include <linux/if.h> ~~~ 由 netdevice.h 包含, 这个文件声明接口标志( IFF_ 宏定义 )和 struct ifmap, 它在网络驱动的 ioctl 实现中有重要地位. ~~~ void netif_carrier_off(struct net_device *dev); void netif_carrier_on(struct net_device *dev); int netif_carrier_ok(struct net_device *dev); ~~~ 前 2 个函数可用来告知内核是否接口上有载波信号. netif_carrier_ok 测试载波状态, 如同在设备结构中反映的. ~~~ #include <linux/if_ether.h> ETH_ALENETH_P_IPstruct ethhdr; ~~~ 由 netdevice.h 包含, if_ether.h 定义所有的 ETH_ 宏定义, 用来代表字节长度( 例如地址长度 )以及网络协议(例如 IP). 它也定义 ethhdr 结构. ~~~ #include <linux/skbuff.h> ~~~ struct sk_buff 和相关结构的定义, 以及几个操作缓存的内联函数. 这个头文件由 netdevice.h 包含. ~~~ struct sk_buff *alloc_skb(unsigned int len, int priority); struct sk_buff *dev_alloc_skb(unsigned int len); void kfree_skb(struct sk_buff *skb); void dev_kfree_skb(struct sk_buff *skb); void dev_kfree_skb_irq(struct sk_buff *skb); void dev_kfree_skb_any(struct sk_buff *skb); ~~~ 处理 socket 缓存的分配和释放的函数. 通常驱动应当使用 dev_ 变体, 其意图就是此目的. ~~~ unsigned char *skb_put(struct sk_buff *skb, int len); unsigned char *__skb_put(struct sk_buff *skb, int len); unsigned char *skb_push(struct sk_buff *skb, int len); unsigned char *__skb_push(struct sk_buff *skb, int len); ~~~ 添加数据到一个 skb 的函数; skb_put 在 skb 的尾部放置数据, 而 skb_push 放在开始. 正常版本进行检查以确保有足够的空间; 双下划线版本不进行检查. ~~~ int skb_headroom(struct sk_buff *skb); int skb_tailroom(struct sk_buff *skb); void skb_reserve(struct sk_buff *skb, int len); ~~~ 进行 skb 中的空间管理的函数. skb_headroom 和 skb_tailroom 说明在开始和结尾分别有多少空间可用. skb_reserve 可用来保留空间, 在一个必须为空的 skb 开始. ~~~ unsigned char *skb_pull(struct sk_buff *skb, int len); ~~~ skb_pull "去除" 数据从一个 skb, 通过调整内部指针. ~~~ int skb_is_nonlinear(struct sk_buff *skb); ~~~ 如果这个 skb 是为发散/汇聚 I/O 分隔为几个片, 函数返回一个真值. ~~~ int skb_headlen(struct sk_buff *skb); ~~~ 返回 skb 的第一个片的长度, 由 skb->data 指向. ~~~ void *kmap_skb_frag(skb_frag_t *frag); void kunmap_skb_frag(void *vaddr); ~~~ 提供对非线性 skb 中的片直接存取的函数. ~~~ #include <linux/etherdevice.h> void ether_setup(struct net_device *dev); ~~~ 为以太网驱动设置大部分方法为通用实现的函数. 它还设置 dev->flags 和安排下一个可用的 ethx 给 dev->name, 如果名子的第一个字符是一个空格或者 NULL 字符. ~~~ unsigned short eth_type_trans(struct sk_buff *skb, struct net_device *dev); ~~~ 当一个以太网接口收到一个报文, 这个函数被调用来设置 skb->pkt_type. 返回值是一个协议号, 通常存储于 skb->protocol. ~~~ #include <linux/sockios.h> SIOCDEVPRIVATE ~~~ 前 16 个 ioctl 命令, 每个驱动可为它们自己的私有用途而实现. 所有的网络 ioctl 命令都在 sockios.h 中定义. ~~~ #include <linux/mii.h> struct mii_if_info; ~~~ 声明和一个结构, 支持实现 MII 标准的设备的驱动. ~~~ #include <linux/ethtool.h> struct ethtool_ops; ~~~ 声明和结构, 使得设备与 ethtool 工具一起工作.
';

17.15. 几个其他细节

最后更新于:2022-04-01 03:01:04

## 17.15. 几个其他细节 本节涵盖了几个其他主题, 对网络驱动作者感兴趣的. 在每种情况, 我们试着简单指点你正确的方向. 获取了一个主题的完整描绘可能还需要花费一些时间深入内核源码. ### 17.15.1. 独立于媒介的接口支持 媒介独立接口(或 MII) 是一个 IEEE 802.3 标准, 描述以太网收发器如何与网络控制器接口; 很多市场上的产品遵守这个接口. 如果你在编写一个驱动为一个 MII 兼容控制器, 内核输出了一个通用 MII 支持层, 可能会使你易做一些. 为使用通用 MII 层, 你应当包含 <linux/mii.h>. 你需要填充一个 mii_if_info 结构使用收发器的物理 ID 信息, 如是否全双工有效. 还要求 mii_if_info 结构的 2 个方法: ~~~ int (*mdio_read) (struct net_device *dev, int phy_id, int location); void (*mdio_write) (struct net_device *dev, int phy_id, int location, int val); ~~~ 如你可能预料的, 这些方法应当实现与你的特殊 MII 接口的通讯. 通用的 MII 代码提供一套函数, 来查询和改变收发器的操作模式; 许多设计成与 ethtool 工具一起工作( 下一节描述 ). 在 <linux/mii.h> 和 drivers/net/mii.c 中查看细节. ### 17.15.2. ethtool 支持 ethtool 是一个实用工具, 设计来给系统管理员以大量的控制网络接口的操作. 用 ethtool, 可能来控制各种接口参数, 包括速度, 介质类型, 双工模式, DMA 环设置, 硬件校验和, LAN 唤醒操作, 等等, 但是只有当 ethtool 被驱动支持. ethtool 可以从 http://sf.net/projects/gkernel/. 下载. 对 ethtool 支持的相关声明可在 <linux/ethtool.h> 中找到. 它的核心是一个 ethtool_ops 类型的结构, 里面包含一个全部 24 个不同方法来支持 ethtool. 大部分这些方法是相对直接地; 细节看 <linux/ethtool.h>. 如果你的驱动使用 MII 层, 你能使用 mii_ethtool_gset 和 mii_ethtool_sset 来实现 get_settings 和 set_settings 方法, 分别地. 对于和你的设备一起工作的 ethtool, 你必须放置一个指向你的 ethtool_ops 结构的指针在 net_devcie 结构中. 宏定义 SET_ETHTOOL_OPS( 在 <linux/netdevice.h> 中定义)应当用作这个目的. 注意你的 ethtool 方法可能会在接口关闭时被调用. Netpoll ### 17.15.3. netpoll "netpoll" 是相对迟的增加到网络协议栈中; 它的目的是使内核能够发送和接收报文, 在完整的网络和I/O子系统不可用的情况下. 它用来给如远程网络控制台和远程内核调试等特色使用的. 无论如何, 你的驱动不必支持 netpoll, 但是它可能使你的驱动在某些情况下更有用. 在大部分情况下支持 netpoll 也相对容易. 实现 netpoll 的驱动应当实现 poll_controller 方法. 它的工作是跟上控制器上可能发生的任何东西, 在缺乏设备中断时. 几乎所有的 poll_controller 方法采用下面形式: ~~~ void my_poll_controller(struct net_device *dev) { disable_device_interrupts(dev); call_interrupt_handler(dev->irq, dev, NULL); reenable_device_interrupts(dev); } ~~~ poll_controller 方法, 实际上, 是简单模拟自给定设备的中断.
';

17.14. 多播

最后更新于:2022-04-01 03:01:02

## 17.14. 多播 一个多播报文是一个会被多个主机接收的网络报文, 但不是所有主机. 这个功能通过给一组主机分配特殊的硬件地址来获得. 发向一个特殊地址的报文应当被那个组当中的所有主机接收. 在以太网的情况下, 一个多播地址在目的地址的第一个字节的最低位为 1, 而每个设备板在它自己的硬件地址的这一位上为 0. 处理主机组和硬件地址的技巧由应用程序和内核处理, 接口驱动不必处理这个问题. 多播报文的传送是一个简单问题, 因为它们看起来就如同其他的报文. 接口发送它们通过通讯媒介, 不查看目的地址. 内核必须要安排一个正确的硬件目的地址; hard_header 设备方法, 如果定义了, 不必查看它安排的数据. 内核来跟踪在任何给定时间对哪些多播地址感兴趣. 这个列表可能经常改变, 因为它是在任何给定时间和按照用户意愿运行的应用程序的功能. 驱动的工作是接收感兴趣的多播地址列表并递交给内核任何发向这些地址的报文. 驱动如何实现多播列表是依赖于底层硬件是如何工作的. 典型地, 在多播的角度上, 硬件属于 3 类中的 1 种: - 不能处理多播的接口. 这样的接口要么接收特别地发向它们的硬件地址(加上广播报文)的报文, 要么接收每一个报文. 它们只能通过接收每一个报文来接收多播报文, 因此, 潜在地压垮操作系统, 使用大量的"不感兴趣"报文. 你不经常认为这样的接口是有多播能力的, 驱动不会在 dev->flags 设置 IFF_MULTICAST. 点对点接口是特殊情况, 因为它们一直接收每个报文, 不进行任何硬件过滤. - 能够区别多播报文和其他报文(主机到主机, 或者广播). 这些接口能够被命令来接收每个多播报文, 让软件决定地址是否是主机感兴趣的. 这种情况下的开销是可接受的, 因为在一个典型网络上的多播报文的数目是少的. - 可以进行硬件检测多播地址的接口. 可以传递一个多播地址的列表给这些接口, 这些地址的报文接收, 并忽略其他多播地址的报文. 对内核这是优化的情况, 因为它不浪费处理器时间来丢弃接口收到的"不感兴趣"的报文. 内核尽力利用高级接口的能力, 通过支持第 3 种设备类型, 它是最通用的. 因此, 内核通知驱动, 在任何有效多播地址列表发生改变时, 并且它传递新的列表给驱动, 因此它能够根据新的信息来更新硬件过滤器. ### 17.14.1. 多播的内核支持 对多播报文的支持有几项组成:一个设备方法, 一个数据结构, 以及设备标识: void (*dev->set_multicast_list)(struct net_device *dev); 设备方法, 在与设备相关的机器地址改变时调用. 它也在 dev->flags 被修改时调用, 因为一些标志(例如, IFF_PROMISC) 可能也要求你重新编程硬件过滤器. 这个方法接收一个 struct net_device 指针作为一个参数, 并返回 void. 一个对实现这个方法不感兴趣的驱动可以听任它为 NULL. struct dev_mc_list *dev->mc_list; 所有设备相关的多播地址的列表. 这个结构的实际定义在本节的末尾介绍. int dev->mc_count; 链表里的项数. 这个信息有些重复, 但是用 0 来检查 mc_count 是检查这个列表的有用的方法. IFF_MULTICAST 除非驱动在 dev->flags 中设置这个标志, 接口不会被要求来处理多播报文. 然而, 内核调用驱动的 set_multicast_list 方法, 当 dev->flags 改变时, 因为多播列表可能在接口未激活时改变了. IFF_ALLMULTI 在 dev->flags 中设置的标志, 网络软件来告知驱动从网络上接收所有多播报文. 这发生在当多播路由激活时. 如果标志设置了, dev->ma_list 不该用来过滤多播报文. IFF_PROMISC 在 dev->flags 中设置的标志, 当接口在混杂模式下. 接口应当接收每个报文, 不管 dev->ma_list. 驱动开发者需要的最后一点信息是 struct dev_mc_list 的定义, 在 <linux/netdevice.h>: ~~~ struct dev_mc_list { struct dev_mc_list *next; /* Next address in list */ __u8 dmi_addr[MAX_ADDR_LEN]; /* Hardware address */ unsigned char dmi_addrlen; /* Address length */ int dmi_users; /* Number of users */ int dmi_gusers; /* Number of groups */ }; ~~~ 因为多播和硬件地址是独立于真正的报文发送, 这个结构在网络实现中是可移植的, 每个地址由一个字符串和一个长度标识, 就像 dev->dev_addr. ### 17.14.2. 典型实现 描述 set_multicast_list 的设计的最好方法是给你看一些伪码. 下面的函数是一个典型函数实现在一个全特性(ff)驱动中. 这个驱动是全模式的, 它控制的接口有一个复杂的硬件报文过滤器, 它能够持有一个主机要接收的多播地址表. 表的最大尺寸是 FF_TABLE_SIZE. 所有以 ff_ 前缀的函数是给特定硬件操作的占位者: ~~~ void ff_set_multicast_list(struct net_device *dev) { struct dev_mc_list *mcptr; if (dev->flags & IFF_PROMISC) { ff_get_all_packets(); return; } /* If there's more addresses than we handle, get all multicast packets and sort them out in software. */ if (dev->flags & IFF_ALLMULTI || dev->mc_count > FF_TABLE_SIZE) { ff_get_all_multicast_packets(); return; } /* No multicast? Just get our own stuff */ if (dev->mc_count == 0) { ff_get_only_own_packets(); return; } /* Store all of the multicast addresses in the hardware filter */ ff_clear_mc_list(); for (mc_ptr = dev->mc_list; mc_ptr; mc_ptr = mc_ptr->next) ff_store_mc_address(mc_ptr->dmi_addr); ff_get_packets_in_multicast_list(); } ~~~ 这个实现可以简化, 如果接口不能为进入报文存储多播表在硬件过滤器中. 这种情况下, FF_TABLE_SIZE 减为 0, 并且代码的最后 4 行不需要了. 如同前面提过的, 不能处理多播报文的接口不需要实现 set_multicast_list 方法来获取 dev->flags 改变的通知. 这个办法可能被称为一个"非特性的"(nf)实现. 实现非常简单, 如下面代码所示: ~~~ void nf_set_multicast_list(struct net_device *dev) { if (dev->flags & IFF_PROMISC) nf_get_all_packets(); else nf_get_only_own_packets(); } ~~~ 实现 IFF_PROMISC 是非常重要的, 因为不这样用户就不能运行 tcpdump 或任何其他网络分析器. 如果接口运行一个点对点连接, 另一方面, 根本没有必要实现 set_multicast_list, 因为用户接收每个报文.
';

17.13. 统计信息

最后更新于:2022-04-01 03:01:00

## 17.13. 统计信息 驱动需要的最后一个方法是 get_stats. 这个方法返回一个指向给设备的统计的指针. 它的实现非常简单; 展示过的这个即便在几个接口由同一个驱动管理时都好用, 因为统计量驻留于设备数据结构内部. ~~~ struct net_device_stats *snull_stats(struct net_device *dev) { struct snull_priv *priv = netdev_priv(dev); return &priv->stats; } ~~~ 需要返回有意义统计的真正工作是分布在整个驱动中的, 有各种成员量被更新. 下列列表展示了最有趣的结构 net_device_stats 中的成员: unsigned long rx_packets;unsigned long tx_packets; 接口成功传送的进入和出去报文的总和. unsigned long rx_bytes;unsigned long tx_bytes; 接口接收和发送的字节数. unsigned long rx_errors;unsigned long tx_errors; 接收和发送的错误数. 报文发送可能出错的事情是没有结束的, net_device_stats 结构包括 6 个计数器给特定的接收错误以及有 5 个给发送错误. 完整列表看 <<linux/netdevice.h>. 如果可能, 你的驱动调用维护详细的错误统计, 因为它们是对系统管理员试图追踪问题的最大帮助. unsigned long rx_dropped;unsigned long tx_dropped; 在接收和发送中丢失的报文数目. 当没有可用内存给报文数据时丢弃报文. tx_dropped 极少使用. unsigned long collisions; 由于介质拥塞引起的冲突数目. unsigned long multicast; 收到的多播报文数目. 值得重复一下, get_stats 方法可以在任何时候调用 -- 即便在接口关闭时 -- 因此只要 net_device 结构存在驱动必须保持统计信息.
';

17.12. 定制 ioctl 命令

最后更新于:2022-04-01 03:00:57

## 17.12. 定制 ioctl 命令 我们硬件看到给 socket 实现的 ioctl 系统调用; SIOCSIFADDR 和 SIOCSIFMAP 是 "socket ioctls" 的例子. 现在我们看看网络代码如何使用这个系统调用的 3 个参数. 当 ioctl 系统调用在一个 socket 上被调用, 命令号是 <linux/sockios.h>中定义的符号中的一个, 并且 sock_ioctl 函数直接调用一个协议特定的函数(这里"协议"指的是使用的主要网络协议, 例如, IP 或者 AppleTalk). 任何协议层不识别的 ioctl 命令传递给设备层. 这些设备有关的 ioctl 命令从用户空间接收一个第 3 个参数, 一个 struct ifreq*. 这个结构定义在 <linux/if.h>. SIOCSIFADDR 和 SIOCSIFMAP 命令实际上在 ifreq 结构上工作. SIOCSIFMAP 的额外参数, 定义为 ifmap, 只是 ifreq 的一个成员. 除了使用标准调用, 每个接口可以定义它自己的 ioctl 命令. plip 接口, 例如, 允许接口通过 ioctl 修改它内部的超时值. socket 的 ioctl 实现认识 16 作为接口私有的个命令: SIOCDEVPRIVATE 到 SIOCDEVPRIVATE+15.[[53](#)] 当这些命令中的一个被识别, dev->do_ioctl 在相关的接口驱动中被调用. 这个函数接收与通用 ioctl 函数使用的相同的 struct ifreq * 指针. ~~~ int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd); ~~~ ifr 指针指向一个内核空间地址, 这个地址持有用户传递的结构的一个拷贝. 在 do_ioctl 返回之后, 结构被拷贝回用户空间; 因此, 驱动可以使用这些私有命令接收和返回数据. 设备特定的命令可以选择使用结构 ifreq 中的成员, 但是它们已经传达一个标准意义, 并且不可能驱动使这个结构适应自己的需要. 成员 ifr_data 是一个 caddr_t 项( 一个指针 ), 是打算用做设备特定的需要. 驱动和用来调用它的 ioctl 命令的程序应当一致地使用 ifr_data. 例如, ppp-stats 使用设备特定的命令来从 ppp 接口驱动获取信息. 这里不值得展示一个 do_ioctl 的实现, 但是有了本章的信息和内核例子, 你应当能够在你需要时编写一个. 注意, 但是, plip 实现使用 ifr_data 不正确, 不应当作为一个 ioctl 实现的例子. [[53](#)] 注意, 根据 <linux/sockios.h>, SIOCDEVPRIVATE 命令是被不赞成的. 应当使用什么来代替它们是不明确的, 但是, 并且不少在目录树中的驱动还使用它们.
';

17.11. MAC 地址解析

最后更新于:2022-04-01 03:00:55

## 17.11. MAC 地址解析 以太网通讯的一个有趣的方面是如何将 MAC 地址( 接口的唯一硬件 ID )和 IP 编号结合起来. 大部分协议有类似的问题, 但我们这里集中于类以太网的情况. 我们试图提供这个问题的完整描述, 因此我们展示三个情形: ARP, 无 ARP 的以太网头部( 例如 plip), 以及非以太网头部. ### 17.11.1. 以太网使用 ARP 处理地址解析的通常方法是使用 Address Resolution Protocol (ARP). 幸运的是, ARP 由内核来管理, 并且一个以太网接口不需要做特别的事情来支持 ARP. 只要 dev->addr 和 dev->addr_len 在 open 时正确的赋值了, 驱动就不需要担心解决 IP 编号对应于 MAC 地址; ether_setup 安排正确的设备方法给 dev->hard_header 和 dev_rebuild_header. 尽管通常内核处理地址解析的细节(并且缓存结果), 它需要接口驱动来帮助建立报文. 毕竟, 驱动知道物理层头部细节, 然而网络代码的作者已经试图隔离内核其他部分. 为此, 内核调用驱动的 hard_header 方法使用 ARP 查询的结果来布置报文. 正常地, 以太网驱动编写者不需要知道这个过程 -- 公共的以太网代码负责了所有事情. ### 17.11.2. 不考虑 ARP 简单的点对点网络接口, 例如 plip, 可能从使用以太网头部中受益, 而避免来回发送 ARP 报文的开销. snull 中的例子代码也属于这一类的网络设备. snull 不能使用 ARP 因为驱动改变发送报文中的 IP 地址, ARP 报文也交换 IP 地址. 尽管我们可能轻易实现了一个简单 ARP 应答发生器, 更多的是演示性的来展示如何直接处理网络层头部. 如果你的设备想使用通常的硬件头而不运行 ARP, 你需要重写缺省的 dev->hard_header 方法. 这是 snull 的实现, 作为一个非常短的函数: ~~~ int snull_header(struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned int len) { struct ethhdr *eth = (struct ethhdr *)skb_push(skb,ETH_HLEN); eth->h_proto = htons(type); memcpy(eth->h_source, saddr ? saddr : dev->dev_addr, dev->addr_len); memcpy(eth->h_dest, daddr ? daddr : dev->dev_addr, dev->addr_len); eth->h_dest[ETH_ALEN-1] ^= 0x01; /* dest is us xor 1 */ return (dev->hard_header_len); } ~~~ 这个函数仅仅用内核提供的信息并把它格式成标准以太网头. 它也翻转目的以太网地址的 1 位, 理由下面叙述. 当接口收到一个报文, eth_type_trans 以几种方法来使用硬件头部. 我们已经在 snull_rx 看到这个调用. ~~~ skb->protocol = eth_type_trans(skb, dev); ~~~ 这个函数抽取协议标识( ETH_P_IP, 在这个情况下 )从以太网头; 它也赋值 skb->mac.raw, 从报文 data (使用 skb_pull)去掉硬件头部, 并且设置 skb->pkt_type. 最后一项在 skb 分配是缺省为 PACKET_HOST(指示报文是发向这个主机的), eth_type_trans 改变它来反映以太网目的地址: 如果这个地址不匹配接收它的接口地址, pkt_type 成员被设为 PACKET_OTHERHOST. 结果, 除非接口处于混杂模式或者内核打开了报文转发, netif_rx 丢弃任何类型为 PACKET_OTHERHOST 的报文. 因为这样, snull_header 小心地使目的硬件地址匹配接收接口. 如果你的接口是点对点连接, 你不会想收到不希望的多播报文. 为避免这个问题, 记住, 第一个字节的最低位(LSB)为 0 的目的地址是方向一个单个主机(即, 要么 PACKET_HOST, 要么 PACKET_OTHERHOST). plip 驱动使用 0xfc 作为它的硬件地址的第一个字节, 而 snull 使用 0x00. 两个地址都导致一个工作中的类似以太网的点对点连接. ### 17.11.3. 非以太网头部 我们刚刚看过硬件头部除目的地址外包含了一些信息, 最重要的是通讯协议. 我们现在描述硬件头部如何用来封装相关的信息. 如果你需要知道细节, 你可从内核源码里抽取它们或者从特定传送媒介的技术文档中. 大部分驱动编写者能够忽略这个讨论只是使用以太网实现. 值得一提的是不是所有信息都由每个协议提供. 一个点对点连接例如 plip 或者 snull 可能在不失去通用性的情况下避免传送这个以太网头部. hard_header 设备方法, 由 snull_header 实现所展示的, 接收自内核的递交的信息( 协议级别和硬件地址 ). 它也在 type 参数中接收 16 位协议编号; IP, 例如, 标识为 ETH_P_IP. 驱动应该正确递交报文数据和协议编号给接收主机. 一个点对点连接可能它的硬件头部的地址, 只传送协议编号, 因为保证递交是独立于源和目的地址的. 一个只有 IP 的连接甚至可能不发送任何硬件头部. 当报文在连接的另一端被收到, 接收函数应当正确设置成员 skb->protocol, skb->pkt_type, 和 skb->mac.raw. skb->mac.raw 是一个字符指针, 由在高层的网络代码(例如, net/ipv4/arp.c)所实现的地址解析机制使用. 它必须指向一个匹配 dev->type 的机器地址. 设备类型的可能的值在 <linux/if_arp.h> 中定义; 以太网接口使用 ARPHRD_ETHER. 例如, 这是 eth_type_trans 如何处理收到的报文的以太网头: ~~~ skb->mac.raw = skb->data; skb_pull(skb, dev->hard_header_len); ~~~ 在最简单的情况下( 一个没有头的点对点连接 ), skb->mac.raw 可指向一个静态缓存, 包含接口的硬件地址, protocol 可设置为 ETH_P_IP, 并且 packet_type 可让它是缺省的值 PACKET_HOST. 因为每个硬件类型是独特的, 给出超出已经讨论的特别的设备是困难的. 内核中满是例子, 但是. 例如, 可查看 AppleTalk 驱动( drivers/net/appletalk/cops.c), 红外驱动(例如, driver/net/irds/smc_ircc.c), 或者 PPP 驱动( drivers/net/ppp_generic.c).
';

17.10. Socket 缓存

最后更新于:2022-04-01 03:00:53

## 17.10. Socket 缓存 我们现在已经涵盖到了大部分关于网络接口的问题. 还缺乏的是对 sk_buff 结构的详细描述.这个结构处于 Linux 内核网络子系统的核心, 我们现在介绍这个结构的重要成员和操作它们的函数. 尽管没有严格要求去理解 sk_buff 的内部, 能够查看它的内容的能力在你追踪问题和试图优化代码时是有帮助的. 例如, 如果你看 loopback.c, 你会发现一个基于对 sk_buff 内部了解的优化. 这里适用的通常的警告是: 如果你编写利用 sk_buff 结构的知识的代码, 你应当准备好在以后内核发行中它坏掉. 仍然, 有时性能优势值得额外的维护开销. 我们这里不会描述整个结构, 只是那些在驱动里可能用到的. 如果你想看到更多, 你可以查看 <linux/skbuff.h>, 那里定义了结构和函数原型. 关于如何使用这些成员和函数的额外的细节可以通过搜索内核源码很容易获取. ### 17.10.1. 重要成员变量 这里介绍的成员是驱动可能需要存取的. 以非特别的顺序列出它们. struct net_device *dev; 接收或发送这个缓存的设备 union { /* ... */ } h;union { /* ... */ } nh;union { /*... */} mac; 指向报文中包含的各级的头的指针. union 中的某个成员都是一个不同数据结构类型的指针. h 含有传输层头部指针(例如, struct tcphdr *th); nh 包含网络层头部(例如 struct iphdr *iph); 以及 mac 包含链路层头部指针(例如 struct ethkr * ethernet). 如果你的驱动需要查看 TCP 报文的源和目的地址, 可以在 skb->h.th 中找到. 看头文件来找到全部的可以这样存取的头部类型. 注意网络驱动负责设置进入报文的 mac 指针. 这个任务正常是由 eth_type_trans 处理, 但是 非以太网驱动不得不直接设置 skb->mac.raw, 如同"非以太网头部"一节所示. unsigned char *head;unsigned char *data;unsigned char *tail;unsigned char *end; 用来寻址报文中数据的指针. head 指向分配内存的开始, data 是有效字节的开始(并且常常稍微比 head 大一些), tail 是有效字节的结尾, end 指向 tail 能够到达的最大地址. 查看它的另一个方法是可用缓存空间是 skb->end - skb->head, 当前使用的空间是 skb->tail - skb->data. unsigned int len;unsigned int data_len; len 是报文中全部数据的长度, 而 data_len 是报文存储于单个片中的部分的长度. 除非使用发散/汇聚 I/O, data_len 成员的值为 0. unsigned char ip_summed; 这个报文的校验和策略. 由驱动在进入报文上设置这个成员, 如在"报文接收"一节中描述的. unsigned char pkt_type; 在递送中使用的报文分类. 驱动负责设置它为 PACKET_HOST (报文是给自己的), PACKET_OTHERHOST (不, 这个报文不是给我的), PACKET_BROADCAST, 或者 PACKET_MULTICAST. 以太网驱动不显式修改 pkt_type, 因为 eth_type_trans 为它们做. shinfo(struct sk_buff *skb);unsigned int shinfo(skb)->nr_frags;skb_frag_t shinfo(skb)->frags; 由于性能的原因, 有些 skb 信息存储于一个分开的结构中, 它在内存中紧接着 skb. 这个"shared info"(这样命名是因为它可以在网络代码中多个 skb 拷贝中共享)必须通过 shinfo 宏定义来存取. 这个结构中有几个成员, 但是大部分超出本书的范围. 我们在"发散/汇聚 I/O"一节中见过 nr_frags 和 frags. 在结构中剩下的成员不是特别有趣. 它们用来维护缓存列表, 来统计 socket 拥有的缓存大小, 等等. ### 17.10.2. 作用于 socket 缓存的函数 使用一个 sk_buff 结构的网络驱动利用正式接口函数来操作它. 许多函数操作一个 socket 缓存; 这里是最有趣的几个: struct sk_buff *alloc_skb(unsigned int len, int priority);struct sk_buff *dev_alloc_skb(unsigned int len); 分配一个缓存区. alloc_skb 函数分配一个缓存并且将 skb->data 和 skb->tail 都初始化成 skb->head. dev_alloc_skb 函数是使用 GFP_ATOMIC 优先级调用 alloc_skb 的快捷方法, 并且在 skb->head 和 skb->data 之间保留了一些空间. 这个数据空间用在网络层之间的优化, 驱动不要动它. void kfree_skb(struct sk_buff *skb);void dev_kfree_skb(struct sk_buff *skb);void dev_kfree_skb_irq(struct sk_buff *skb);void dev_kfree_skb_any(struct sk_buff *skb); 释放缓存. kfree_skb 调用由内核在内部使用. 一个驱动应当使用一种 dev_kfree_skb 的变体: 在非中断上下文中使用 dev_kfree_skb, 在中断上下文中使用 dev_kfree_skb_irq, 或者 dev_kfree_skb_any 在任何 2 种情况下. unsigned char *skb_put(struct sk_buff *skb, int len);unsigned char *__skb_put(struct sk_buff *skb, int len); 更新 sk_buff 结构中的 tail 和 len 成员; 它们用来增加数据到缓存的结尾, 每个函数的返回值是 skb->tail 的前一个值(换句话说, 它指向刚刚创建的数据空间). 驱动可以使用返回值通过引用 memcpy(skb_put(...), data, len) 来拷贝数据或者一个等同的东东. 两个函数的区别在于 skb_put 检查以确认数据适合缓存, 而 __skb_put 省略这个检查. unsigned char *skb_push(struct sk_buff *skb, int len);unsigned char *__skb_push(struct sk_buff *skb, int len); 递减 skb->data 和递增 skb->len 的函数. 它们与 skb_put 相似, 除了数据是添加到报文的开始而不是结尾. 返回值指向刚刚创建的数据空间. 这些函数用来在发送报文之前添加一个硬件头部. 又一次, __skb_push 不同在它不检查空间是否足够. int skb_tailroom(struct sk_buff *skb); 返回可以在缓存中放置数据的可用空间数量. 如果驱动放了多于它能持有的数据到缓存中, 系统傻掉. 尽管你可能反对说一个 printk 会足够来标识出这个错误, 内存破坏对系统是非常有害的以至于开发者决定采取确定的动作. 实际中, 你不该需要检查可用空间, 如果缓存被正确地分配了. 因为驱动常常在分配缓存前获知报文的大小, 只有一个严重坏掉的驱动会在缓存中安放太多的数据, 这样出乱子就可当作一个应得的惩罚. int skb_headroom(struct sk_buff *skb); 返回 data 前面的可用空间数量, 就是, 可以 "push" 给缓存多少字节. void skb_reserve(struct sk_buff *skb, int len); 递增 data 和 tail. 这个函数可用来在填充数据前保留空间. 大部分以太网接口保留 2 个字节在报文的前面; 因此, IP 头对齐到 16 字节, 在 14 字节的以太网头后面. snull 也这样做, 尽管没有在"报文接收"一节中展现这个指令以避免在那时引入过多概念. unsigned char *skb_pull(struct sk_buff *skb, int len); 从报文的头部去除数据. 驱动不会需要使用这个函数, 但是为完整而包含在这儿. 它递减 skb->len 和递增 skb->data; 这是硬件头如何从进入报文开始被剥离. int skb_is_nonlinear(struct sk_buff *skb); 返回一个真值, 如果这个 skb 分离为多个片为发散/汇聚 I/O. int skb_headlen(struct sk_buff *skb); 返回 skb 的第一个片的长度(由 skb->data 指着). void *kmap_skb_frag(skb_frag_t *frag);void kunmap_skb_frag(void *vaddr); 如果你必须从内核中的一个非线性 skb 直接存取片, 这些函数为你映射以及去映射它们. 使用一个原子性 kmap, 因此你不能一次映射多于一个片. 内核定义了几个其他的作用于 socket 缓存的函数, 但是它们是打算用于高层网络代码, 驱动不需要它们.
';

17.9. 连接状态的改变

最后更新于:2022-04-01 03:00:50

## 17.9. 连接状态的改变 网络连接, 根据定义, 打交道本地系统之外的世界. 因此, 它们常常受外界事件的影响, 并且它们可能是短暂的东西. 网络子系统需要知道网络连接的上或下, 它提供了几个驱动可用来传达这种信息的函数. 大部分涉及实际的物理连接的网络技术提供有一个载波状态; 载波存在说明硬件存在并准备好. 以太网适配器, 例如, 在电线上感知载波信号; 当一个用户绊倒一根电缆, 载波消失, 连接断开. 缺省地, 网络设备假设有载波信号存在. 驱动可以明确改变这个状态, 但是, 使用这些函数: ~~~ void netif_carrier_off(struct net_device *dev); void netif_carrier_on(struct net_device *dev); ~~~ 如果你的驱动检测到它的一个设备载波丢失, 它应当调用 netif_carrier_off 来通知内核这个改变. 当载波回来时, 应当调用 netif_carrier_on. 一些驱动也调用 netif_carrier_off 当进行大的配置改变时(例如媒介类型); 一旦适配器已经完成复位它自身, 新载波被检测并且恢复流量. 一个整数函数也存在: ~~~ int netif_carrier_ok(struct net_device *dev); ~~~ 它可用于测试当前载波状态( 如同设备结构中所反映的 );
';

17.8. 接收中断缓解

最后更新于:2022-04-01 03:00:48

## 17.8. 接收中断缓解 当一个网络驱动如我们上面所述编写出来, 你的接口收到每个报文都中断处理器. 在许多情况下, 这是希望的操作模式, 它不是个问题. 然而, 高带宽接口能够在每秒内收到几千个报文. 这个样子的中断负载下, 系统的整体性能会受损害. 作为一个提高高端 Linux 系统性能的方法, 网络子系统开发者已创建了一种可选的基于查询的接口(称为 NAPI). [[52](#)]"查询"可能是一个不妥的字在驱动开发者看来, 他们常常看到查询是不灵巧和低效的. 查询是低效的, 但是, 仅仅在接口没有工作做的时候被查询. 当系统有一个处理大流量的高速接口时, 会一直有更多的报文来处理. 在这种情况下没有必要中断处理器; 时常从接口收集新报文是足够的. 停止接收中断能够减轻相当数量的处理器负载. 适应 NAPI 的驱动能够被告知不要输送报文给内核, 如果这些报文只是在网络代码里因拥塞而被丢弃, 这样能够在最需要的时候对性能有帮助. 由于各种理由, NAPI 驱动也比较少可能重排序报文. 不是所有的设备能够以 NAPI 模式操作, 但是. 一个 NAPI 适应的接口必须能够存储几个报文( 要么在接口卡上, 要么在内存内 DMA 环). 接口应当能够禁止中断来接收报文, 却可以继续因成功发送或其他事件而中断. 有其他微妙的事情使得编写一个适应 NAPI 的驱动更有难度; 详情见内核源码中的 Documentation/networking/NAPI_HOWTO.txt. 相对少有驱动实现 NAPI 接口. 如果你在编写一个驱动给一个可能产生大量中断的接口, 但是, 花点时间来实现 NAPI 会被证明是很值得的. snull 驱动, 当用非零的 use_napi 参数加载时, 在 NAPI 模式下操作. 在初始化时, 我们不得不建立一对格外的结构 net_device 的成员: ~~~ if (use_napi) { dev->poll = snull_poll; dev->weight = 2; } ~~~ poll 成员必须设置为你的驱动的查询函数; 我们简短看一下 snull_poll. weight 成员描述接口的相对重要性: 有多少流量可以从接口收到, 当资源紧张时. 如何设置 weight 参数没有严格的规则; 依照惯例, 10 MBps 以太网接口设置 weight 为 16, 而快一些的接口使用 64. 你不能设置 weight 为一个超过你的接口能够存储的报文数目的值. 在 snull, 我们设置 weight 为 2, 作为一个演示不同报文接收的方法. 创建适应 NAPI 的驱动的下一步是改变中断处理. 当你的接口(它应当在接收中断使能下启动)示意有报文到达, 中断处理不应当处理这个报文. 相反, 它应当禁止后面的接收中断并告知内核到时候查询接口了. 在 snull的"中断"处理里, 响应报文接收中断的代码已变为如下: ~~~ if (statusword & SNULL_RX_INTR) { snull_rx_ints(dev, 0); /* Disable further interrupts */ netif_rx_schedule(dev); } ~~~ 当接口告诉我们有报文来了, 中断处理将其留在接口中; 此时需要的所有东西就是调用 netif_rx_schedule, 它使得我们的 poll 方法在后面某个时候被调用. poll 方法有下面原型: ~~~ int (*poll)(struct net_device *dev, int *budget); ~~~ snull 的 poll 方法实现看来如此: ~~~ static int snull_poll(struct net_device *dev, int *budget) { int npackets = 0, quota = min(dev->quota, *budget); struct sk_buff *skb; struct snull_priv *priv = netdev_priv(dev); struct snull_packet *pkt; while (npackets < quota && priv->rx_queue) { pkt = snull_dequeue_buf(dev); skb = dev_alloc_skb(pkt->datalen + 2); if (! skb) { if (printk_ratelimit()) printk(KERN_NOTICE "snull: packet dropped\n"); priv->stats.rx_dropped++; snull_release_buffer(pkt); continue; } memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen); skb->dev = dev; skb->protocol = eth_type_trans(skb, dev); skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */ netif_receive_skb(skb); /* Maintain stats */ npackets++; priv->stats.rx_packets++; priv->stats.rx_bytes += pkt->datalen; snull_release_buffer(pkt); } /* If we processed all packets, we're done; tell the kernel and reenable ints */ *budget -= npackets; dev->quota -= npackets; if (! priv->rx_queue) { netif_rx_complete(dev); snull_rx_ints(dev, 1); return 0; } /* We couldn't process everything. */ return 1; } ~~~ 函数的中心部分是关于创建一个保持报文的 skb; 这部分代码和我们之前在 snull_rx 中见到的一样. 但是, 有些东西不一样: - budget 参数提供了一个我们允许传给内核的最大报文数目. 在设备结构里, quota 成员给出了另一个最大值; poll 方法必须遵守这两个限制中的较小者. 它也应当以实际收到的报文数目递减 dev->quota 和 *budget. budget 值是当前 CPU 能够从所有接口收到的最多报文数目, 而 quota 是一个每接口值, 常常在初始化时安排给接口以 weight 为起始. - 报文应当用 netif_receive_skb 递交内核, 而不是 netif_rx. - 如果 poll 方法能够在给定的限制内处理所有的报文, 它应当重新使能接收中断, 调用 netif_rx_complete 来关闭 查询, 并且返回 0. 返回值 1 指示有剩下的报文需要处理. 网络子系统保证任何给定的设备的 poll 方法不会在多于一个处理器上被同时调用. 但是, poll 调用仍然可以与你的其他设备方法的调用并发. [[52](#)] NAPI 代表"new API"; 网络黑客们精于创建接口却疏于给它们起名.
';

17.7. 中断处理

最后更新于:2022-04-01 03:00:46

## 17.7. 中断处理 大部分硬件接口通过一个中断处理来控制. 硬件中断处理器来发出 2 种可能的信号: 一个新报文到了或者一个外出报文的发送完成了. 网络接口也能够产生中断来指示错误, 例如状态改变, 等等. 通常的中断过程能够告知新报文到达中断和发送完成通知的区别, 通过检查物理设备中的状态寄存器. snull 接口类似地工作, 但是它的状态字在软件中实现, 位于 dev->priv. 网络接口的中断处理看来如此: ~~~ static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs) { int statusword; struct snull_priv *priv; struct snull_packet *pkt = NULL; /* * As usual, check the "device" pointer to be sure it is * really interrupting. * Then assign "struct device *dev" */ struct net_device *dev = (struct net_device *)dev_id; /* ... and check with hw if it's really ours */ /* paranoid */ if (!dev) return; /* Lock the device */ priv = netdev_priv(dev); spin_lock(&priv->lock); /* retrieve statusword: real netdevices use I/O instructions */ statusword = priv->status; priv->status = 0; if (statusword & SNULL_RX_INTR) { /* send it to snull_rx for handling */ pkt = priv->rx_queue; if (pkt) { priv->rx_queue = pkt->next; snull_rx(dev, pkt); } } if (statusword & SNULL_TX_INTR) { /* a transmission is over: free the skb */ priv->stats.tx_packets++; priv->stats.tx_bytes += priv->tx_packetlen; dev_kfree_skb(priv->skb); } /* Unlock the device and we are done */ spin_unlock(&priv->lock); if (pkt) snull_release_buffer(pkt); /* Do this outside the lock! */ return; } ~~~ 中断处理的第一个任务是取一个指向正确 net_device 结构的指针. 这个指针通常来自作为参数收到的 dev_id 指针. 中断处理的有趣部分处理"发送结束"的情况. 在这个情况下, 统计量被更新, 调用 dev_kfree_skb 来返回 socket 缓存给系统. 实际上, 有这个函数的 3 个变体可以调用: dev_kfree_skb(struct sk_buff *skb); 这个版本应当在你知道你的代码不会在中断上下文中运行时调用. 因为 snull 没有实际的硬件中断, 我们使用这个版本. dev_kfree_skb_irq(struct sk_buff *skb); 如果你知道会在中断处理中释放缓存, 使用这个版本, 它对这个情况做了优化. dev_kfree_skb_any(struct sk_buff *skb); 如果相关代码可能在中断或非中断上下文运行时, 使用这个版本. 最后, 如果你的驱动已暂时停止了发送队列, 这常常是用 netif_wake_queue 重启它的地方. 报文的接收, 相比于发送, 不需要特别的中断处理. 调用 snull_rx (我们已经见过)就是全部所需.
';