FreeBSD-设备驱动-全-

FreeBSD 设备驱动(全)

原文:FreeBSD Device Drivers

译者:飞龙

协议:CC BY-NC-SA 4.0

简介

无标题图片

欢迎来到《FreeBSD 设备驱动程序》!本书的目标是帮助您提高对 FreeBSD 下设备驱动程序的理解。完成本书后,您应该能够构建、配置和管理自己的 FreeBSD 设备驱动程序。

本书涵盖 FreeBSD 版本 8,这是截至本书撰写时推荐的用于生产使用的版本。尽管如此,您将学到的大部分内容也将适用于早期版本,并且应该适用于后续版本。

这本书是为谁写的?

我作为程序员为程序员写了这本书。因此,您会发现本书对编程的重视程度很高,而不是理论,您将检查真实的设备驱动程序(即控制硬件的驱动程序)。想象一下,如果不曾读过任何一本书就尝试写一本书。这是不可想象的!对于设备驱动程序来说,也是如此。

预备知识

为了充分利用本书,您应该熟悉 C 编程语言。您还应该了解一些操作系统设计方面的知识;例如,进程和线程之间的区别。

如果您缺乏必要的背景知识,我建议在阅读这本书之前先阅读以下三本书,或者只是将它们作为参考资料保留:

  • C 程序设计语言》,作者:Brian W. Kernighan 和 Dennis M. Ritchie(Prentice Hall PTR,1988 年)

  • 专家 C 编程》,作者:Peter van der Linden(Prentice Hall,1994 年)

  • FreeBSD 操作系统的设计和实现》,作者:Marshall Kirk McKusick 和 George V. Neville-Neil(Addison-Wesley Professional,2005 年)

概述内容

FreeBSD 设备驱动程序》包含以下章节。

第一章

提供了基本设备驱动程序编程概念和术语的概述和介绍。

第二章

描述了 FreeBSD 内核内存管理例程。

第三章

教您如何从用户空间与设备驱动程序进行通信和控制。

第四章

讨论了与多线程编程和并发执行相关的问题和解决方案。

第五章

描述了延迟代码执行和异步代码执行,并解释了为什么这些任务是必要的。

第六章

包含了几个场合中的第一个,我在这些场合中会向您介绍一个真实的设备驱动程序。

第七章

介绍了 FreeBSD 用于管理系统硬件设备的底层基础设施。从现在开始,我将专门处理真实硬件。

第八章

讨论了 FreeBSD 中的中断处理。

第九章

完整地介绍了lpt(4),并行端口打印机驱动程序。

第十章

涵盖了端口映射 I/O 和内存映射 I/O。

第十一章

审查了ipmi(4),智能平台管理接口驱动程序的部分,它使用端口映射 I/O 和内存映射 I/O。

第十二章

解释了如何在 FreeBSD 中使用直接内存访问(DMA)。

第十三章

教您如何管理存储设备,例如硬盘驱动器、闪存等。

第十四章

提供了关于常见访问方法(CAM)的概述和介绍,您将使用它来管理主机总线适配器。

第十五章

教您如何管理 USB 设备。还完整地介绍了ulpt(4),USB 打印机驱动程序。

第十六章

描述了网络驱动程序使用的数据结构。还介绍了消息信号中断(MSI)。

第十七章

分析了em(4),英特尔 PCI 千兆以太网适配器驱动程序的包接收和传输组件。

欢迎加入!

希望您觉得这本书有用且有趣。一如既往,我欢迎您通过评论或错误修复提供反馈,发送至 joe@thestackframe.org。

好的,介绍性的内容就到这里。让我们开始吧。

第一章. 构建和运行模块

无标题图片

本章介绍了 FreeBSD 设备驱动程序。我们将从描述四种不同的 UNIX 设备驱动程序及其在 FreeBSD 中的表示开始。然后,我们将描述构建和运行可加载内核模块的基本知识,并以字符驱动程序的介绍结束本章。

注意

如果你不理解上述术语中的一些,不要担心;我们将在本章中定义它们所有。

设备驱动程序类型

在 FreeBSD 中,设备 是属于系统的任何与硬件相关的项目;这包括磁盘驱动器、打印机、显卡等。设备驱动程序 是一个控制或“驱动”设备(有时是多个设备)的计算机程序。在 UNIX 和 4.0 以前的 FreeBSD 中,有四种不同类型的设备驱动程序:

  • 字符驱动程序,用于控制字符设备

  • 块驱动程序,用于控制块设备

  • 网络驱动程序,用于控制网络设备

  • 模拟设备驱动程序,用于控制模拟设备

字符设备 提供基于字符流的 I/O 接口,或者,也可以提供非结构化(原始)接口(McKusick 和 Neville-Neil,2005)。

块设备 以固定大小的块随机访问数据(Corbet 等,2005)。在 FreeBSD 4.0 及以后的版本中,块驱动程序已不存在(有关更多信息,请参阅块驱动程序已消失中的 DEV_MODULE 宏)。

网络设备 通过网络子系统传输和接收由网络子系统驱动的数据包(Corbet 等,2005)。

最后,模拟设备 是一个仅使用软件(即没有任何底层硬件)模拟设备行为的计算机程序。

可加载内核模块

设备驱动程序可以是静态编译到系统中,也可以是使用可加载内核模块(KLD)动态加载的。

注意

大多数操作系统将可加载内核模块称为 LKM——FreeBSD 只能有所不同。

KLD 是一个可以在系统启动后加载、卸载、启动和停止的内核子系统。换句话说,KLD 可以在系统运行时向内核添加功能,并在之后移除这些功能。不用说,我们的“功能”将是设备驱动程序。

通常,所有 KLD 都有两个共同组件:

  • 模块事件处理器

  • DECLARE_MODULE 宏调用

模块事件处理器

模块事件处理器 是处理 KLD 初始化和关闭的函数。当 KLD 被加载到内核或从内核卸载,或者系统关闭时,该函数会被执行。其函数原型在 <sys/module.h> 头文件中定义如下:

typedef int (*modeventhand_t)(module_t, int /* modeventtype_t */, void *);

在这里,modeventtype_t<sys/module.h> 头文件中定义如下:

typedef enum modeventtype {
        MOD_LOAD,       /* Set when module is loaded. */
        MOD_UNLOAD,     /* Set when module is unloaded. */
        MOD_SHUTDOWN,   /* Set on shutdown. */
        MOD_QUIESCE     /* Set when module is about to be unloaded. */
} modeventtype_t;

如你所见,modeventtype_t标签表示 KLD 正在被加载到内核中,或从内核卸载,或者系统即将关闭。(现在忽略该值;我们将在第四章中讨论。)

通常,你会在switch语句中使用modeventtype_t参数来为每种情况设置不同的代码块。一些示例代码可以帮助阐明我的意思:

static int
modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;

        switch (event) {
      case MOD_LOAD:
                uprintf("Hello, world!\n");
                break;
      case MOD_UNLOAD:
                uprintf("Good-bye, cruel world!\n");
                break;
      default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

注意第二个参数是如何作为switch语句的表达式。因此,当 KLD 被加载到内核中时,此模块事件处理器会打印“Hello, world!”;当 KLD 从内核卸载时,会打印“Good-bye, cruel world!”;在系统关闭之前返回EOPNOTSUPP(代表错误:不支持的操作)。

DECLARE_MODULE 宏

DECLARE_MODULE宏将 KLD 及其模块事件处理器注册到系统中。以下是它的函数原型:

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>

DECLARE_MODULE(name, moduledata_t data, sub, order);

此宏期望的参数如下。

name

name参数是模块名称,用于标识 KLD。

data

data参数期望一个填充好的moduledata_t结构,该结构在<sys/module.h>头文件中定义如下:

typedef struct moduledata {
        const char      *name;
        modeventhand_t  evhand;
        void            *priv;
} moduledata_t;

在这里,name是官方模块名称,evhand是 KLD 的模块事件处理器,priv是指向私有数据(如果存在)的指针。

sub

sub参数指定了 KLD 所属的内核子系统。此参数的有效值在sysinit_sub_id枚举中定义,该枚举位于<sys/kernel.h>头文件中。

enum sysinit_sub_id {
        SI_SUB_DUMMY            = 0x0000000,    /* Not executed.        */
        SI_SUB_DONE             = 0x0000001,    /* Processed.           */
        SI_SUB_TUNABLES         = 0x0700000,    /* Tunable values.      */
        SI_SUB_COPYRIGHT        = 0x0800001,    /* First console use.   */
        SI_SUB_SETTINGS         = 0x0880000,    /* Check settings.      */
        SI_SUB_MTX_POOL_STATIC  = 0x0900000,    /* Static mutex pool.   */
        SI_SUB_LOCKMGR          = 0x0980000,    /* Lock manager.        */
        SI_SUB_VM               = 0x1000000,    /* Virtual memory.      */
...
      SI_SUB_DRIVERS          = 0x3100000,     /* Device drivers.      */
...
};

由于显而易见的原因,我们几乎总是将sub设置为SI_SUB_DRIVERS,这是设备驱动程序子系统。

order

order参数指定了 KLD 在sub子系统中的初始化顺序。此参数的有效值在sysinit_elem_order枚举中定义,该枚举位于<sys/kernel.h>头文件中。

enum sysinit_elem_order {
        SI_ORDER_FIRST          = 0x0000000,    /* First.               */
        SI_ORDER_SECOND         = 0x0000001,    /* Second.              */
        SI_ORDER_THIRD          = 0x0000002,    /* Third.               */
        SI_ORDER_FOURTH         = 0x0000003,    /* Fourth.              */
      SI_ORDER_MIDDLE         = 0x1000000,    /* Somewhere in the middle. */
        SI_ORDER_ANY            = 0xfffffff     /* Last.                    */
};

通常情况下,我们都会将order设置为SI_ORDER_MIDDLE

Hello, world!

现在你已经足够了解如何编写你的第一个 KLD。示例 1-1 是 KLD 的完整骨架代码。

示例 1-1. hello.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  static int
 hello_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  uprintf("Hello, world!\n");
                  break;
          case MOD_UNLOAD:
                  uprintf("Good-bye, cruel world!\n");
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

 static moduledata_t hello_mod = {
          "hello",
          hello_modevent,
          NULL
  };

 DECLARE_MODULE(hello, hello_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);

这段代码包含一个模块模块事件处理程序——它与模块事件处理程序中描述的相同——以及一个填充好的结构体 moduledata_t结构,该结构作为参数第二个参数传递给宏 DECLARE_MODULE

简而言之,这个 KLD 只是一个模块事件处理程序和一个DECLARE_MODULE调用。简单,对吧?

编译和加载

要编译 KLD,你可以使用<bsd.kmod.mk> Makefile。以下是示例 1-1 的完整 Makefile:

 KMOD=   hello
 SRCS=   hello.c

  .include <bsd.kmod.mk>

在这里,模块 KMOD是 KLD 的名称,源文件 SRCS是 KLD 的源文件。顺便提一下,我将调整这个 Makefile 来编译每个 KLD。

现在,假设示例 1-1 及其 Makefile 位于同一目录中,只需输入make,编译过程(非常详细)将产生一个名为hello.ko的可执行文件,如下所示:

$ `make`
Warning: Object directory not changed from original /usr/home/ghost/hello
@ -> /usr/src/sys
machine -> /usr/src/sys/i386/include
cc -O2 -fno-strict-aliasing -pipe  -D_KERNEL -DKLD_MODULE -std=c99 -nostdinc
-I. -I@ -I@/contrib/altq -finline-limit=8000 --param inline-unit-growth=100 -
-param large-function-growth=1000 -fno-common  -mno-align-long-strings -mpref
erred-stack-boundary=2  -mno-mmx -mno-3dnow -mno-sse -mno-sse2 -mno-sse3 -ffr
eestanding -Wall -Wredundant-decls -Wnested-externs -Wstrict-prototypes  -Wmi
ssing-prototypes -Wpointer-arith -Winline -Wcast-qual  -Wundef -Wno-pointer-s
ign -fformat-extensions -c hello.c
ld  -d -warn-common -r -d -o hello.kld hello.o
:> export_syms
awk -f /sys/conf/kmod_syms.awk hello.kld  export_syms | xargs -J% objcopy % h
ello.kld
ld -Bshareable  -d -warn-common -o hello.ko hello.kld
objcopy --strip-debug hello.ko
$ `ls -F`
@@           export_syms  hello.kld    hello.o
Makefile     hello.c      hello.ko*    machine@

然后,你可以使用kldload(8)kldunload(8)分别加载和卸载hello.ko

$ `sudo kldload ./hello.ko`
Hello, world!
$ `sudo kldunload hello.ko`
Good-bye, cruel world!

作为旁白,如果你在 Makefile 中包含了<bsd.kmod.mk>,你可以使用make loadmake unload来代替kldload(8)kldunload(8),如下所示:

$ `sudo make load`
/sbin/kldload -v /usr/home/ghost/hello/hello.ko
Hello, world!
Loaded /usr/home/ghost/hello/hello.ko, id=3
$ `sudo make unload`
/sbin/kldunload -v hello.ko
Unloading hello.ko, id=3
Good-bye, cruel world!

恭喜!你现在已经成功将代码加载到运行中的内核中。在继续之前,还有一个额外的要点需要提及。你可以使用kldstat(8)动态显示任何链接到内核的文件的状态,如下所示:

$ `kldstat`
Id Refs Address    Size     Name
 1    4 0xc0400000 906518   kernel
 2    1 0xc0d07000 6a32c    acpi.ko
 3    1 0xc3301000 2000     hello.ko

如你所见,输出相当直观。现在,让我们做一些更有趣的事情。

字符驱动程序

字符驱动基本上是创建字符设备的 KLD。如前所述,字符设备提供基于字符流的 I/O 接口,或者,作为替代,提供一个非结构化(原始)接口。这些(字符设备接口建立了访问设备的规范,包括可以调用来执行 I/O 操作的程序集(McKusick 和 Neville-Neil,2005)。简而言之,字符驱动程序产生字符设备,提供设备访问。例如,lpt(4)驱动程序创建了/dev/lpt0字符设备,用于访问并行端口打印机。在 FreeBSD 4.0 及以后的版本中,大多数设备都有一个字符设备接口。

通常,所有字符驱动程序都有三个共同组件:

  • d_foo函数

  • 字符设备交换表

  • make_devdestroy_dev函数调用

d_foo 函数

d_foo 函数,其函数原型在 <sys/conf.h> 头文件中定义,是进程可以在设备上执行 I/O 操作。这些 I/O 操作大多与文件 I/O 系统调用相关联,因此命名为 d_opend_read 等。当对字符设备的“foo”操作完成时,会调用字符驱动程序的 d_foo 函数。例如,当进程从设备读取时,会调用 d_read

表 1-1 提供了每个 d_foo 函数的简要描述。

表 1-1. d_foo 函数

函数 描述
d_open 调用来打开设备以准备 I/O 操作
d_close 调用来关闭设备
d_read 调用来从设备读取数据
d_write 调用来向设备写入数据
d_ioctl 调用来执行除读取或写入之外的操作
d_poll 调用来检查设备以查看是否可读取数据或是否有空间写入数据
d_mmap 调用来将设备偏移量映射到内存地址
d_kqfilter 调用来将设备注册到内核事件列表中
d_strategy 调用来启动读取或写入操作然后立即返回
d_dump 调用来将所有物理内存写入设备

注意

如果您不理解这些操作中的某些,请不要担心;我们将在实现它们时详细描述。

字符设备切换表

字符设备切换表,struct cdevsw,指定了字符驱动程序实现了哪些 d_foo 函数。它在 <sys/conf.h> 头文件中定义如下:

struct cdevsw {
        int                     d_version;
        u_int                   d_flags;
        const char              *d_name;
        d_open_t                *d_open;
        d_fdopen_t              *d_fdopen;
        d_close_t               *d_close;
        d_read_t                *d_read;
        d_write_t               *d_write;
        d_ioctl_t               *d_ioctl;
        d_poll_t                *d_poll;
        d_mmap_t                *d_mmap;
        d_strategy_t            *d_strategy;
        dumper_t                *d_dump;
        d_kqfilter_t            *d_kqfilter;
        d_purge_t               *d_purge;
        d_spare2_t              *d_spare2;
        uid_t                   d_uid;
        gid_t                   d_gid;
        mode_t                  d_mode;
        const char              *d_kind;

        /* These fields should not be messed with by drivers. */
        LIST_ENTRY(cdevsw)      d_list;
        LIST_HEAD(, cdev)       d_devs;
        int                     d_spare3;
        struct cdevsw           *d_gianttrick;
};

下面是一个读写设备的示例字符设备切换表:

static struct cdevsw echo_cdevsw = {
        .d_version =    D_VERSION,
        .d_open =       echo_open,
        .d_close =      echo_close,
        .d_read =       echo_read,
        .d_write =      echo_write,
        .d_name =       "echo"
};

如您所见,并非每个 d_foo 函数或属性都需要定义。如果一个 d_foo 函数未定义,相应的操作将不受支持(例如,只读设备的字符设备切换表不会定义 d_write)。

不出所料,d_version(表示该驱动程序支持的 FreeBSD 版本)和 d_name(即驱动程序的名字)必须定义。通常,d_version 被设置为 D_VERSION,这是一个宏替换,用于编译时使用的 FreeBSD 版本。

make_dev 和 destroy_dev 函数

make_dev 函数接受一个字符设备切换表,并在 /dev 下创建一个字符设备节点。以下是它的函数原型:

#include <sys/param.h>
#include <sys/conf.h>

struct cdev *
make_dev(struct cdevsw *cdevsw, int minor, uid_t uid, gid_t gid,
    int perms, const char *fmt, ...);

相反,destroy_dev 函数接受由 make_dev 返回的 cdev 结构,并销毁字符设备节点。以下是它的函数原型:

#include <sys/param.h>
#include <sys/conf.h>

void
destroy_dev(struct cdev *dev);

大部分无害

示例 1-2 是一个完整的字符驱动程序(基于 Murray Stokely 和 Søren Straarup 编写的代码),它将内存区域操作得像设备一样。这个伪(或内存)设备允许您向其写入和从其读取单个字符字符串。

注意

快速看一下这段代码,并尝试辨别其结构。如果你不理解所有内容,不要担心;解释随后到来。

示例 1-2. echo.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/malloc.h>

  #define BUFFER_SIZE     256

  /* Forward declarations. */
  static d_open_t         echo_open;
  static d_close_t        echo_close;
  static d_read_t         echo_read;
  static d_write_t        echo_write;

 static struct cdevsw echo_cdevsw = {
          .d_version =    D_VERSION,
          .d_open =       echo_open,
          .d_close =      echo_close,
          .d_read =       echo_read,
          .d_write =      echo_write,
          .d_name =       "echo"
  };

  typedef struct echo {
          char buffer[BUFFER_SIZE];
          int length;
  } echo_t;

 static echo_t *echo_message;
 static struct cdev *echo_dev;

  static int
 echo_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
  {
          uprintf("Opening echo device.\n");
          return (0);
  }

  static int
 echo_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
  {
          uprintf("Closing echo device.\n");
          return (0);
  }

  static int
  echo_write(struct cdev *dev, struct uio *uio, int ioflag)
  {
          int error = 0;

          error = copyin(uio->uio_iov->iov_base, echo_message->buffer,
              MIN(uio->uio_iov->iov_len, BUFFER_SIZE - 1));
          if (error != 0) {
                  uprintf("Write failed.\n");
                  return (error);
          }

          *(echo_message->buffer +
              MIN(uio->uio_iov->iov_len, BUFFER_SIZE - 1)) = 0;

          echo_message->length = MIN(uio->uio_iov->iov_len, BUFFER_SIZE - 1);

          return (error);
  }

  static int
  echo_read(struct cdev *dev, struct uio *uio, int ioflag)
  {
          int error = 0;
          int amount;

          amount = MIN(uio->uio_resid,
              (echo_message->length - uio->uio_offset > 0) ?
               echo_message->length - uio->uio_offset : 0);

          error = uiomove(echo_message->buffer + uio->uio_offset, amount, uio);
          if (error != 0)
                  uprintf("Read failed.\n");

          return (error);
  }

  static int
  echo_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  echo_message = malloc(sizeof(echo_t), M_TEMP, M_WAITOK);
                  echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                      0600, "echo");
                  uprintf("Echo driver loaded.\n");
                  break;
          case MOD_UNLOAD:
                  destroy_dev(echo_dev);
                  free(echo_message, M_TEMP);
                  uprintf("Echo driver unloaded.\n");
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  DEV_MODULE(echo, echo_modevent, NULL);

此驱动程序首先定义一个字符设备开关表,其中包含四个名为echo_food_foo函数,其中foo等于openclosereadwrite。因此,接下来的字符设备将仅支持这四种 I/O 操作。

接下来,有两个变量声明:一个名为echo_messageecho结构指针(它将包含一个字符字符串及其长度)和一个名为echo_devcdev结构指针(它将维护由make_dev调用返回的cdev)。

然后,定义了d_foo函数echo_openecho_close——每个函数只是打印一个调试信息。通常,d_open函数为 I/O 准备设备,而d_close则取消这些准备。

注意

“为 I/O 准备设备”与“准备(或初始化)设备”之间有区别。对于像示例 1-2 这样的伪设备,设备初始化是在模块事件处理程序中完成的。

剩余的位——echo_writeecho_readecho_modeventDEV_MODULE——需要更深入的说明,因此将在各自的章节中描述。

echo_write 函数

echo_write函数从用户空间获取一个字符字符串并将其存储。以下是它的函数定义(再次):

static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        int error = 0;

        error = copyin(uio->uio_iov->iov_base,
 echo_message->buffer,
            MIN(uio->uio_iov->iov_len, BUFFER_SIZE - 1));
        if (error != 0) {
                uprintf("Write failed.\n");
                return (error);
        }

      *(echo_message->buffer +
            MIN(uio->uio_iov->iov_len, BUFFER_SIZE - 1)) = 0;

      echo_message->length = MIN(uio->uio_iov->iov_len, BUFFER_SIZE - 1);

        return (error);
}

这里,struct uio描述了一个正在运动的字符字符串——变量iov_baseiov_len分别指定字符字符串的基址和长度。

因此,这个函数首先将字符字符串从用户空间复制到内核空间。最多复制'BUFFER_SIZE - 1'个字节的数据。一旦完成,字符字符串被 null 终止,并且记录其长度(减去 null 终止符)。

注意

这不是从用户空间复制数据到内核空间的正确方法。我应该使用uiomove而不是copyin。然而,copyin更容易理解,而且到目前为止,我只是想介绍字符驱动程序的基本结构。

echo_read 函数

echo_read函数将存储的字符字符串返回到用户空间。以下是它的函数定义(再次):

static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        int error = 0;
        int amount;

        amount = MIN(uio->uio_resid,
            (echo_message->length - uio->uio_offset > 0) ?
            echo_message->length - uio->uio_offset : 0);

        error = uiomove(echo_message->buffer + uio->uio_offset,
 amount,
            uio);
        if (error != 0)
                uprintf("Read failed.\n");

        return (error);
}

在这里,变量uio_residuio_offset分别指定剩余要传输的数据量和字符字符串中的偏移量。

因此,这个函数首先确定要返回的字符数——要么是用户请求的量,要么是全部。然后echo_read将这个数量从内核空间传输到用户空间。

注意

关于在用户空间和内核空间之间复制数据的更多信息,请参阅copy(9)uio(9)手册页。我还推荐阅读 OpenBSD 的uiomove(9)手册页。

echo_modevent 函数

echo_modevent函数是此字符驱动程序的模块事件处理程序。以下是它的函数定义(再次):

static int
echo_modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;

        switch (event) {
        case MOD_LOAD:
              echo_message = malloc(sizeof(echo_t), M_TEMP, M_WAITOK);
                echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0600, "echo");
                uprintf("Echo driver loaded.\n");
                break;
        case MOD_UNLOAD:
              destroy_dev(echo_dev);
              free(echo_message, M_TEMP);
                uprintf("Echo driver unloaded.\n");
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

在模块加载时,这个函数首先调用malloc来分配sizeof(echo_t)字节的内存。然后它调用make_dev来在/dev下创建一个名为echo的字符设备节点。请注意,当make_dev返回时,字符设备是“活跃”的,并且其d_foo函数可以执行。因此,如果我在malloc之前调用make_devecho_writeecho_read可能会在echo_message指向有效内存之前执行,这将是非常危险的。重点是:除非你的驱动程序完全就绪,否则不要调用make_dev

在模块卸载时,这个函数首先调用destroy_dev来销毁echo设备节点。然后它调用free来释放分配的内存。

DEV_MODULE 宏

DEV_MODULE宏在<sys/conf.h>头文件中定义如下:

#define DEV_MODULE(name, evh, arg)                                      \
  static moduledata_t name##_mod = {                                      \
      #name,                                                              \
      evh,                                                                \
      arg                                                                 \
  };                                                                      \
 DECLARE_MODULE(name, name##_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE)

如您所见,DEV_MODULE仅仅是对DECLARE_MODULE的封装。因此,示例 1-2 可以调用DECLARE_MODULE,但DEV_MODULE更简洁(并且可以节省一些按键操作)。

不要慌张

现在我们已经走过了示例 1-2,让我们试一试:

$ `sudo kldload ./echo.ko`
Echo driver loaded.
$ `ls -l /dev/echo`
crw-------  1 root  wheel    0,  95 Jun  4 23:23 /dev/echo
$ `su`
Password:
# `echo "DON'T PANIC" > /dev/echo`
Opening echo device.
Closing echo device.
# `cat /dev/echo`
Opening echo device.
DON'T PANIC
Closing echo device.

毫不奇怪,它确实有效。在本章结束之前,有一个关键主题需要提及。

块设备驱动程序已消失

如前所述,块设备以固定大小的块传输可随机访问的数据;例如,磁盘驱动器。自然地,块设备驱动程序提供了对块设备的访问。块设备驱动程序的特点是所有 I/O 都在内核的缓冲区缓存中进行缓存,这使得块设备不可靠,有两个原因。首先,因为缓存可以重新排序写操作序列,它剥夺了写进程在任何时刻识别确切磁盘内容的能力。这使得可靠的磁盘数据结构(例如,文件系统)的崩溃恢复成为不可能。其次,缓存可以延迟写操作。所以如果发生错误,内核无法向执行写操作的过程报告哪个特定操作失败了。由于这些原因,每个访问块设备的严肃应用程序都指定始终使用字符设备接口。因此,FreeBSD 在磁盘 I/O 基础设施现代化过程中放弃了块设备驱动程序的支持。

注意

显然,FreeBSD 仍然支持块设备。有关更多信息,请参阅第十三章。

结论

本章向您介绍了 FreeBSD 设备驱动程序开发的基础。在接下来的章节中,我们将基于这里描述的概念来完善您的驱动程序工具包。顺便提一下,由于大多数 FreeBSD 设备驱动程序是字符驱动程序,不要将它们视为主要的驱动程序类——它们更像是一种用于创建字符设备节点的工具。

第二章. 分配内存

无标题图片

在上一章中,我们使用了 mallocfree 来进行内存的分配和释放。然而,FreeBSD 内核包含了一组更丰富的内存分配原语。在本章中,我们将探讨标准内核内存管理例程。这包括更详细地描述 mallocfree,并介绍 malloc_type 结构。我们将通过描述连续物理内存管理例程来结束本章。

内存管理例程

FreeBSD 内核提供了四个用于非分页内存分配和释放的函数:mallocfreereallocreallocf。这些函数可以处理任意大小或对齐的请求,并且是分配内核内存的首选方式。

#include <sys/types.h>
#include <sys/malloc.h>

void *
malloc(unsigned long size, struct malloc_type *type, int flags);

void
free(void *addr, struct malloc_type *type);

void *
realloc(void *addr, unsigned long size, struct malloc_type *type,
    int flags);

void *
reallocf(void *addr, unsigned long size, struct malloc_type *type,
    int flags);

malloc 函数在内核空间中分配 size 字节的内存。如果成功,则返回一个内核虚拟地址;否则,返回 NULL

free 函数释放 addr 处的内存——这是之前由 malloc 分配的——以供重用。请注意,free 不会清除此内存,这意味着你应该显式地将任何需要保持私有内容的内容的内存设置为 0。如果 addrNULL,则 free 不做任何操作。

注意

如果启用了 INVARIANTS,则 free 将用 0xdeadc0de 填充任何释放的内存。

因此,如果你遇到页面故障恐慌,并且故障地址在 0xdeadc0de 附近,这可能表明你正在使用已释放的内存。1]

realloc 函数将 addr 处的内存大小更改为 size 字节。如果成功,则返回一个内核虚拟地址;否则,返回 NULL,并且内存保持不变。请注意,返回的地址可能与 addr 不同,因为当大小改变时,内存可能会被重新定位以获取或提供额外的空间。有趣的是,这暗示了在调用 realloc 时不应有任何指向 addr 内存中的指针。如果 addrNULL,则 realloc 的行为与 malloc 相同。

reallocf 函数与 realloc 函数相同,只是在失败时释放 addr 处的内存。

mallocreallocreallocf 函数提供了一个 flags 参数来进一步限定它们的操作特性。此参数的有效值显示在 表 2-1 中。

表 2-1. malloc、realloc 和 reallocf 符号常量

常量 描述
M_ZERO 将分配的内存设置为 0
M_NOWAIT 如果由于资源短缺而无法立即满足分配,则 mallocreallocreallocf 将返回 NULL;在中断上下文中运行时需要 M_NOWAIT
M_WAITOK 表示等待资源是可以接受的;如果分配不能立即完成,当前进程将被挂起等待资源可用;当指定 M_WAITOK 时,mallocreallocreallocf 不能返回 NULL

flags 参数必须包含 M_NOWAITM_WAITOK


^([1]) INVARIANTS 是一个内核调试选项。有关 INVARIANTS 的更多信息,请参阅 /sys/conf/NOTES

malloc_type 结构体

mallocfreereallocreallocf 函数包含一个 type 参数,它期望是一个指向 malloc_type 结构体的指针;此结构体描述了分配内存的目的。type 参数对性能没有影响;它用于内存分析和基本健全性检查。

注意

您可以使用 vmstat -m 命令按 type 对内核动态内存使用进行性能分析。

MALLOC_DEFINE 宏

MALLOC_DEFINE 宏定义了一个新的 malloc_type 结构体。以下是它的函数原型:

#include <sys/param.h>
#include <sys/malloc.h>
#include <sys/kernel.h>

MALLOC_DEFINE(type, shortdesc, longdesc);

type 参数是新 malloc_type 结构体的名称。通常,type 应以 M_ 开头并全部大写;例如,M_FOO

shortdesc 参数期望是关于新 malloc_type 结构体的简短描述。此参数用于 vmstat -m 的输出。因此,它不应包含任何空格,以便在脚本中更容易解析 vmstat -m 的输出。

longdesc 参数期望是关于新 malloc_type 结构体的详细描述。

MALLOC_DECLARE 宏

MALLOC_DECLARE 宏使用 extern 关键字声明了一个新的 malloc_type 结构体。以下是它的函数原型:

#include <sys/types.h>
#include <sys/malloc.h>

MALLOC_DECLARE(type);

此宏定义在 <sys/malloc.h> 头文件中,如下所示:

#define MALLOC_DECLARE(type) \
        extern struct malloc_type type[1]

作为旁注,如果您需要一个私有的 malloc_type 结构体,您应该在 MALLOC_DEFINE 调用前加上 static 关键字。实际上,没有对应 MALLOC_DECLARE 调用的非静态 MALLOC_DEFINE 调用在 gcc 4.x 下实际上会导致警告。

将一切联系在一起

示例 2-1 是 示例 1-2 的修订版,它使用自己的 malloc_type 结构体而不是内核定义的 M_TEMP。示例 2-1 应该可以澄清您对 MALLOC_DEFINEMALLOC_DECLARE 可能有的任何误解。

注意

为了节省空间,echo_openecho_closeecho_writeecho_read 函数没有在此列出,因为它们没有发生变化。

示例 2-1. echo-2.0.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/malloc.h>

  #define BUFFER_SIZE     256

 MALLOC_DECLARE(M_ECHO);
 MALLOC_DEFINE(M_ECHO, "echo_buffer", "buffer for echo driver");

  static d_open_t         echo_open;
  static d_close_t        echo_close;
  static d_read_t         echo_read;
  static d_write_t        echo_write;

  static struct cdevsw echo_cdevsw = {
          .d_version =    D_VERSION,
          .d_open =       echo_open,
          .d_close =      echo_close,
          .d_read =       echo_read,
          .d_write =      echo_write,
          .d_name =       "echo"
  };

  typedef struct echo {
          char buffer[BUFFER_SIZE];
          int length;
  } echo_t;

  static echo_t *echo_message;
  static struct cdev *echo_dev;

  static int
  echo_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
  {
  ...
  }

  static int
  echo_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
  {
  ...
  }

  static int
  echo_write(struct cdev *dev, struct uio *uio, int ioflag)
  {
  ...
  }

  static int
  echo_read(struct cdev *dev, struct uio *uio, int ioflag)
  {
  ...
  }

  static int
  echo_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  echo_message = malloc(sizeof(echo_t), M_ECHO, M_WAITOK);
                  echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                      0600, "echo");
                  uprintf("Echo driver loaded.\n");
                  break;
          case MOD_UNLOAD:
                  destroy_dev(echo_dev);
                  free(echo_message, M_ECHO);
                  uprintf("Echo driver unloaded.\n");
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  DEV_MODULE(echo, echo_modevent, NULL);

此驱动程序 链接 声明并 链接 定义了一个名为 M_ECHO 的新 malloc_type 结构体。要使用此 malloc_type 结构体,需要相应地调整 mallocfree 链接 链接

注意

由于M_ECHO仅用于本地,因此不需要MALLOC_DECLARE——这里仅包括以供演示目的。

现在由于示例 2-1 使用了独特的malloc_type结构,我们可以轻松地分析其动态内存使用情况,如下所示:

$ `sudo kldload ./echo-2.0.ko`
Echo driver loaded.
$ `vmstat -m | head -n 1 && vmstat -m | grep "echo_buffer"`
         Type InUse MemUse HighUse Requests  Size(s)
  echo_buffer     1     1K       -        1  512

注意,示例 2-1 请求了 512 字节,尽管sizeof(echo_t)只有 260 字节。这是因为malloc在分配内存时向上舍入到最近的 2 的幂。此外,请注意,MALLOC_DEFINE的第二个参数(本例中的echo_buffer)用于vmstat的输出(而不是第一个参数)。


^([2]) M_TEMP/sys/kern/kern_malloc.c中定义。

连续物理内存管理例程

FreeBSD 内核提供了两个连续物理内存管理函数:contigmalloccontigfree。通常,你永远不会使用这些函数。它们主要用于处理与硬件相关的代码和偶尔的网络驱动程序。

#include <sys/types.h>
#include <sys/malloc.h>

void *
contigmalloc(unsigned long size, struct malloc_type *type, int flags,
    vm_paddr_t low, vm_paddr_t high, unsigned long alignment,
    unsigned long boundary);

void
contigfree(void *addr, unsigned long size, struct malloc_type *type);

contigmalloc函数分配size字节的连续物理内存。如果size0contigmalloc将引发恐慌。如果成功,分配将位于物理地址lowhigh之间,包括这两个地址。

alignment参数表示分配内存的物理对齐,以字节为单位。此参数必须是 2 的幂。

boundary参数指定分配内存不能跨越的物理地址边界;也就是说,它不能跨越任何boundary的倍数。此参数必须是0,表示没有边界限制,或者是一个 2 的幂。

flags参数修改contigmalloc的行为。此参数的有效值在表 2-2 中显示。

表 2-2. contigmalloc 符号常量

常量 描述
M_ZERO 导致分配的物理内存被零填充
M_NOWAIT 如果由于资源不足而无法立即满足分配,contigmalloc将返回NULL
M_WAITOK 表示可以等待资源;如果分配无法立即满足,当前进程将被挂起等待资源可用

contigfree函数释放由contigmalloc先前分配的内存addr,以便重新使用。size参数是要释放的内存量。通常,size应等于分配的内存量。

一个简单的例子

示例 2-2 修改了 示例 2-1 以使用 contigmalloccontigfree 而不是 mallocfree。示例 2-2 应该澄清您可能对 contigmalloccontigfree 的任何误解。

注意

为了节省空间,函数 echo_openecho_closeecho_writeecho_read 没有在此列出,因为它们没有发生变化。

示例 2-2. echo_contig.c

#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>

#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/malloc.h>

#define BUFFER_SIZE     256

MALLOC_DEFINE(M_ECHO, "echo_buffer", "buffer for echo driver");

static d_open_t         echo_open;
static d_close_t        echo_close;
static d_read_t         echo_read;
static d_write_t        echo_write;

static struct cdevsw echo_cdevsw = {
        .d_version =    D_VERSION,
        .d_open =       echo_open,
        .d_close =      echo_close,
        .d_read =       echo_read,
        .d_write =      echo_write,
        .d_name =       "echo"
};

typedef struct echo {
        char buffer[BUFFER_SIZE];
        int length;
} echo_t;

static echo_t *echo_message;
static struct cdev *echo_dev;

static int
echo_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
...
}

static int
echo_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
...
}

static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
echo_modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;

        switch (event) {
        case MOD_LOAD:
                echo_message = contigmalloc(sizeof(echo_t), M_ECHO,
                    M_WAITOK | M_ZERO, 0, 0xffffffff,
 PAGE_SIZE,
                    1024 * 1024);
                echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0600, "echo");
                uprintf("Echo driver loaded.\n");
                break;
        case MOD_UNLOAD:
                destroy_dev(echo_dev);
                contigfree(echo_message, sizeof(echo_t), M_ECHO);
                uprintf("Echo driver unloaded.\n");
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

DEV_MODULE(echo, echo_modevent, NULL);

在这里,图 contigmalloc 分配了 图 sizeof(echo_t) 字节的 图 零填充内存。此内存位于物理地址 图 0图 0xffffffff 之间,对齐在 图 PAGE_SIZE 边界,并且不跨越 图 1MB 地址边界。

以下输出显示了在加载示例 2-2 后的 vmstat -m 的结果:

$ `sudo kldload ./echo_contig.ko`
Echo driver loaded.
$ `vmstat -m | head -n 1 && vmstat -m | grep "echo_buffer"`
         Type InUse MemUse HighUse Requests  Size(s)
  echo_buffer     1     4K       -        1

注意到示例 2-2 使用了 4KB 的内存,尽管 sizeof(echo_t) 只有 260 字节。这是因为 contigmallocPAGE_SIZE 块中分配内存。可以预见,这个示例是在一个 i386 机器上运行的,该机器使用 4KB 的页面大小。

结论

本章详细介绍了 FreeBSD 的内存管理例程和连续物理内存管理例程。它还介绍了 malloc_type 结构体。

顺便提一下,大多数驱动程序应该定义它们自己的 malloc_type 结构体。

第三章:设备通信与控制

无标题图片

在 第一章 中,我们构建了一个可以读取和写入设备的驱动程序。除了读取和写入之外,大多数驱动程序还需要执行其他 I/O 操作,例如报告错误信息、弹出可移动媒体或激活自毁序列。本章详细介绍了如何使驱动程序执行这些操作。

我们将从描述 ioctl 接口 开始,也称为 输入/输出控制接口。该接口通常用于设备通信和控制。然后我们将描述 sysctl 接口,也称为 系统控制接口。该接口用于动态更改或检查内核的参数,包括设备驱动程序。

ioctl

ioctl 接口是 I/O 操作的万能工具(Stevens,1992)。任何不能用 d_readd_write 表达的操作(即任何不是数据传输的操作)都由 d_ioctl 支持。^([3]) 例如,CD-ROM 驱动程序的 d_ioctl 函数执行了 29 个不同的操作,例如弹出 CD、开始音频播放、停止音频播放、静音音频等。

d_ioctl 函数的原型定义在 <sys/conf.h> 头文件中,如下所示:

typedef int d_ioctl_t(struct cdev *dev, u_long cmd, caddr_t data,
                      int fflag, struct thread *td);

在这里,!cmd 是从用户空间传递的 ioctl 命令。ioctl 命令 是由驱动程序定义的数字常量,用于标识 d_ioctl 函数可以执行的不同 I/O 操作。通常,您会在 switch 语句中使用 cmd 参数来为每个 I/O 操作设置代码块。任何需要的 I/O 操作的参数都通过!data 传递。

这里是一个 d_ioctl 函数的示例:

注意

只关注这段代码的结构,忽略它的功能。

static int
echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
        int error = 0;

        switch (cmd) {
      case ECHO_CLEAR_BUFFER:
                memset(echo_message->buffer, '\0',
                    echo_message->buffer_size);
                echo_message->length = 0;
                uprintf("Buffer cleared.\n");
                break;
      case ECHO_SET_BUFFER_SIZE:
                error = echo_set_buffer_size(*(int *)data);
                if (error == 0)
                        uprintf("Buffer resized.\n");
                break;
      default:
                error = ENOTTY;
                break;
        }

        return (error);
}

注意!cmd 参数是!switch 语句的表达式。常量!ECHO_CLEAR_BUFFER 和!ECHO_SET_BUFFER_SIZE 显然是 ioctl 命令。所有 ioctl 命令都是使用四种宏定义的。我将在下一节讨论这些宏。

此外,请注意!data 参数在解引用之前是如何被!强制转换为整数指针的。这是因为 data 本质上是一个“指向 void 的指针”。

注意

void 指针可以持有任何指针类型,因此在解引用之前必须进行转换。实际上,您不能直接解引用 void 指针。

最后,根据 POSIX 标准,当收到不适当的 ioctl 命令时,应该返回错误代码 ENOTTY(Corbet 等人,2005)。因此,default 块将 error 设置为 ENOTTY

注意

在某个时间点,只有 TTY 驱动程序有 ioctl 函数,这就是为什么 ENOTTY 表示“错误:不适当的 ioctl 设备”(Corbet 等人,2005)。

现在,你已经检查了 d_ioctl 函数的结构,我将解释如何定义 ioctl 命令。


^([3]) d_ioctl 函数首次在字符设备中的 d_foo 函数中引入。

定义 ioctl 命令

要定义一个 ioctl 命令,你可以调用以下宏之一:_IO_IOR_IOW_IOWR。每个宏的解释在表 3-1 中提供。

表 3-1. ioctl 命令宏

描述
_IO 创建一个不传输数据的 ioctl 命令——换句话说,d_ioctl 中的 data 参数将被弃用——例如,弹出可移动媒体
_IOR 创建一个用于读取操作的 ioctl 命令;读取操作将数据从设备传输到用户空间;例如,检索错误信息
_IOW 创建一个用于写入操作的 ioctl 命令;写入操作将数据从用户空间传输到设备;例如,设置设备参数
_IOWR 创建一个用于双向数据传输的 I/O 操作的 ioctl 命令

_IO_IOR_IOW_IOWR<sys/ioccom.h> 头文件中定义如下:

#define _IO(g,n)        _IOC(IOC_VOID,  (g), (n), 0)
#define _IOR(g,n,t)     _IOC(IOC_OUT,   (g), (n), sizeof(t))
#define _IOW(g,n,t)     _IOC(IOC_IN,    (g), (n), sizeof(t))
#define _IOWR(g,n,t)    _IOC(IOC_INOUT, (g), (n), sizeof(t))

g 参数,代表 group,期望一个 8 位魔法数字。你可以选择任何数字——只需在你的驱动程序中一直使用它。

n 参数是序号。这个数字用于区分你的驱动程序的 ioctl 命令。

最后,t 参数是在 I/O 操作期间传输的数据类型。显然,_IO 宏没有 t 参数,因为没有数据传输发生。

通常,ioctl 命令的定义如下:

#define FOO_DO_SOMETHING        _IO('F', 1)
#define FOO_GET_SOMETHING       _IOR('F', 2, int)
#define FOO_SET_SOMETHING       _IOW('F', 3, int)
#define FOO_SWITCH_SOMETHING    _IOWR('F', 10, struct foo)

在这里,'F' 是这些 ioctl 命令的魔法数字。通常,选择你的驱动程序名称的第一个字母的大写作为魔法数字。

自然,所有的序号都是唯一的。但它们不必连续。你可以留下空隙。

最后,请注意,你可以将结构体作为 t 参数传递。使用结构体是向基于 ioctl 的操作传递多个参数的方式。

实现 ioctl

示例 3-1 是 示例 2-1 的修订版,增加了 d_ioctl 函数。正如您将看到的,这个 d_ioctl 函数处理两个 ioctl 命令。

注意

快速查看这段代码,并尝试了解其结构。如果您不理解所有内容,请不要担心;解释将随后提供。

示例 3-1. echo-3.0.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/malloc.h>
  #include <sys/ioccom.h>

  MALLOC_DEFINE(M_ECHO, "echo_buffer", "buffer for echo driver");

 #define ECHO_CLEAR_BUFFER       _IO('E', 1)
 #define ECHO_SET_BUFFER_SIZE    _IOW('E', 2, int)

  static d_open_t         echo_open;
  static d_close_t        echo_close;
  static d_read_t         echo_read;
  static d_write_t        echo_write;
  static d_ioctl_t        echo_ioctl;

  static struct cdevsw echo_cdevsw = {
          .d_version =    D_VERSION,
          .d_open =       echo_open,
          .d_close =      echo_close,
          .d_read =       echo_read,
          .d_write =      echo_write,
        .d_ioctl =      echo_ioctl,
          .d_name =       "echo"
  };

  typedef struct echo {
        int buffer_size;
          char *buffer;
          int length;
  } echo_t;

  static echo_t *echo_message;
  static struct cdev *echo_dev;

  static int
  echo_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
  {
          uprintf("Opening echo device.\n");
          return (0);
  }

  static int
  echo_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
  {
          uprintf("Closing echo device.\n");
          return (0);
  }

  static int
  echo_write(struct cdev *dev, struct uio *uio, int ioflag)
  {
          int error = 0;
          int amount;

          amount = MIN(uio->uio_resid,
              (echo_message->buffer_size - 1 - uio->uio_offset > 0) ?
               echo_message->buffer_size - 1 - uio->uio_offset : 0);
          if (amount == 0)
                  return (error);

          error = uiomove(echo_message->buffer, amount, uio);
          if (error != 0) {
                  uprintf("Write failed.\n");
                  return (error);
          }

          echo_message->buffer[amount] = '\0';
          echo_message->length = amount;

          return (error);
  }

  static int
  echo_read(struct cdev *dev, struct uio *uio, int ioflag)
  {
          int error = 0;
          int amount;

          amount = MIN(uio->uio_resid,
              (echo_message->length - uio->uio_offset > 0) ?
               echo_message->length - uio->uio_offset : 0);

          error = uiomove(echo_message->buffer + uio->uio_offset, amount, uio);
          if (error != 0)
                  uprintf("Read failed.\n");

          return (error);
  }

  static int
  echo_set_buffer_size(int size)
  {
          int error = 0;

          if (echo_message->buffer_size == size)
                  return (error);

          if (size >= 128 && size <= 512) {
                  echo_message->buffer = realloc(echo_message->buffer, size,
                      M_ECHO, M_WAITOK);
                  echo_message->buffer_size = size;

                  if (echo_message->length >= size) {
                          echo_message->length = size - 1;
                          echo_message->buffer[size - 1] = '\0';
                  }
          } else
                  error = EINVAL;

          return (error);
  }

  static int
  echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
      struct thread *td)
  {
          int error = 0;

          switch (cmd) {
          case ECHO_CLEAR_BUFFER:
                  memset(echo_message->buffer, '\0',
                      echo_message->buffer_size);
                  echo_message->length = 0;
                  uprintf("Buffer cleared.\n");
                  break;
          case ECHO_SET_BUFFER_SIZE:
                  error = echo_set_buffer_size(*(int *)data);
                  if (error == 0)
                          uprintf("Buffer resized.\n");
                  break;
          default:
                  error = ENOTTY;
                  break;
          }

          return (error);
  }

  static int
  echo_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  echo_message = malloc(sizeof(echo_t), M_ECHO, M_WAITOK);
                  echo_message->buffer_size = 256;
                  echo_message->buffer = malloc(echo_message->buffer_size,
                      M_ECHO, M_WAITOK);
                  echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                      0600, "echo");
                  uprintf("Echo driver loaded.\n");
                  break;
          case MOD_UNLOAD:
                  destroy_dev(echo_dev);
                  free(echo_message->buffer, M_ECHO);
                  free(echo_message, M_ECHO);
                  uprintf("Echo driver unloaded.\n");
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  DEV_MODULE(echo, echo_modevent, NULL);

这个驱动程序首先定义了两个 ioctl 命令:图片ECHO_CLEAR_BUFFER(用于清除内存缓冲区)和图片ECHO_SET_BUFFER_SIZE(接受一个图片整数以调整内存缓冲区大小)。

注意

通常,ioctl 命令定义在头文件中——它们在示例 3-1 中仅被定义,目的是为了简化讨论。

显然,为了适应添加 d_ioctl 函数,字符设备切换表被图片修改。此外,struct echo 被调整以包含一个变量图片 buffer_size,以保持缓冲区大小(因为现在它可以被改变)。自然地,示例 3-1 也被图片图片修改,以使用这个新变量。

注意

有趣的是,只有 echo_write 需要修改。echo_openecho_closeecho_read 函数保持不变。

echo_writeecho_set_buffer_sizeecho_ioctlecho_modevent 函数需要更深入的说明,因此它们将在各自的章节中描述。

echo_write 函数

如上所述,echo_write 函数已被从其 示例 2-1(和 示例 1-2)形式修改。以下是它的函数定义(再次):

static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        int error = 0;
        int amount;

        amount = MIN(uio->uio_resid,
          (echo_message->buffer_size - 1 - uio->uio_offset > 0) ?
            echo_message->buffer_size - 1 - uio->uio_offset : 0);
        if (amount == 0)
                return (error);

        error = uiomove(echo_message->buffer, amount,
 uio);
        if (error != 0) {
                uprintf("Write failed.\n");
                return (error);
        }

        echo_message->buffer[amount] = '\0';
        echo_message->length = amount;

        return (error);
}

这个版本的 echo_write 使用图片uiomove(如第一章所述)而不是 copyin。请注意,uiomove 对每个复制的字节减少 uio->uio_resid(一个单位)并增加 uio->uio_offset(一个单位)。这使得多次调用 uiomove 可以轻松地复制数据块。

注意

您会记得,uio->uio_residuio->uio_offset 分别表示剩余要传输的字节数和数据的偏移量(即字符字符串)。

这个函数首先图片确定要复制的字节数——要么是用户发送的数量,要么是图片缓冲区可以容纳的任何数量。然后它图片将相应数量的数据从图片用户空间传输到图片内核空间。

这个函数的其余部分应该很容易理解。

echo_set_buffer_size 函数

如其名称所示,echo_set_buffer_size函数接受一个整数来调整内存缓冲区echo_message->buffer的大小。以下是它的函数定义(再次):

static int
echo_set_buffer_size(int size)
{
        int error = 0;

      if (echo_message->buffer_size == size)
              return (error);

        if (size >= 128 && size <= 512) {
                echo_message->buffer = realloc(echo_message->buffer, size,
                    M_ECHO, M_WAITOK);
              echo_message->buffer_size = size;

              if (echo_message->length >= size) {
                        echo_message->length = size - 1;
                        echo_message->buffer[size - 1] = '\0';
                }
        } else
                error = EINVAL;

        return (error);
}

这个函数可以被分为三个部分。第一部分确认了当前和提议的缓冲区大小是不同的(否则不需要发生任何操作)图片 图片 图片 图片

第二部分图片更改内存缓冲区的大小。然后它图片记录新的缓冲区大小。请注意,如果缓冲区中存储的数据比提议的缓冲区大小长,则调整大小操作(即realloc)将截断该数据。

第三部分只有在缓冲区中存储的数据被截断的情况下才会发生。它首先图片纠正存储数据的长度。然后它图片将数据以空字符终止。

echo_ioctl 函数

echo_ioctl函数是d_ioctl函数,对应于示例 3-1。以下是它的函数定义(再次):

static int
echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
        int error = 0;

        switch (cmd) {
      case ECHO_CLEAR_BUFFER:
                memset(echo_message->buffer, '\0',
                    echo_message->buffer_size);
                echo_message->length = 0;
                uprintf("Buffer cleared.\n");
                break;
      case ECHO_SET_BUFFER_SIZE:
                error = echo_set_buffer_size(*(int *)data);
                if (error == 0)
                        uprintf("Buffer resized.\n");
                break;
        default:
                error = ENOTTY;
                break;
        }

        return (error);
}

这个函数可以执行两种基于 ioctl 的操作之一。第一种图片清除内存缓冲区。它首先图片将缓冲区清零。然后它图片将数据长度设置为0

第二种图片通过调用echo_set_buffer_size来调整内存缓冲区的大小。请注意,这个操作需要一个图片参数:提议的缓冲区大小。这个参数通过图片 data从用户空间获取。

注意

记住,在可以解引用之前,你必须对data进行类型转换。

echo_modevent 函数

如你所知,echo_modevent 函数是模块事件处理器。像 echo_write 一样,这个函数必须修改以适应添加 echo_ioctl。以下是它的函数定义(再次):

static int
echo_modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;
        switch (event) {
        case MOD_LOAD:
                echo_message = malloc(sizeof(echo_t), M_ECHO, M_WAITOK);
                echo_message->buffer_size = 256;
                echo_message->buffer = malloc(echo_message->buffer_size,
                    M_ECHO, M_WAITOK);
                echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0600, "echo");
                uprintf("Echo driver loaded.\n");
                break;
        case MOD_UNLOAD:
                destroy_dev(echo_dev);
                free(echo_message->buffer, M_ECHO);
                free(echo_message, M_ECHO);
                uprintf("Echo driver unloaded.\n");
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

这个版本的 echo_modevent 分别为 echo 结构和内存缓冲区分配内存(这就是唯一的变化)。之前,内存缓冲区不能调整大小。因此,单独的内存分配是不必要的。

不要慌张

现在我们已经走过了 示例 3-1,让我们试一试:

$ `sudo kldload ./echo-3.0.ko`
Echo driver loaded.
$ `su`
Password:
# `echo "DON'T PANIC" > /dev/echo`
Opening echo device.
Closing echo device.
# `cat /dev/echo`
Opening echo device.
DON'T PANIC
Closing echo device.

显然它有效。但我们如何调用 echo_ioctl

调用 ioctl

要调用 d_ioctl 函数,你会使用 ioctl(2) 系统调用。

#include <sys/ioctl.h>

int
ioctl(int d, unsigned long request, ...);

d 参数,代表 描述符,期望一个设备节点的文件描述符。request 参数是要发出的 ioctl 命令(例如,ECHO_CLEAR_BUFFER)。剩余的参数(...)是指向要传递给 d_ioctl 函数的数据的指针。

示例 3-2 展示了一个命令行工具,用于调用 示例 3-1 中的 echo_ioctl 函数:

示例 3-2. echo_config.c

#include <sys/types.h>
  #include <sys/ioctl.h>

  #include <err.h>
  #include <fcntl.h>
  #include <limits.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>

 #define ECHO_CLEAR_BUFFER       _IO('E', 1)
 #define ECHO_SET_BUFFER_SIZE    _IOW('E', 2, int)

  static enum {UNSET, CLEAR, SETSIZE} action = UNSET;

  /*
   * The usage statement: echo_config -c | -s size
   */

  static void
  usage()
  {
          /*
           * Arguments for this program are "either-or." That is,
           * 'echo_config -c' and 'echo_config -s size' are valid; however,
           * 'echo_config -c -s size' is invalid.
           */

          fprintf(stderr, "usage: echo_config -c | -s size\n");
          exit(1);
  }

  /*
   * This program clears or resizes the memory buffer
   * found in /dev/echo.
   */

  int
  main(int argc, char *argv[])
  {
          int ch, fd, i, size;
          char *p;

          /*
           * Parse the command-line argument list to determine
           * the correct course of action.
           *
           *    -c:      clear the memory buffer
           *    -s size: resize the memory buffer to size.
           */

          while ((ch = getopt(argc, argv, "cs:")) != −1)
                  switch (ch) {
                  case 'c':
                          if (action != UNSET)
                                  usage();
                          action = CLEAR;
                          break;
                  case 's':
                          if (action != UNSET)
                                  usage();
                          action = SETSIZE;
                          size = (int)strtol(optarg, &p, 10);
                          if (*p)
                                  errx(1, "illegal size -- %s", optarg);
                          break;
                  default:
                          usage();
                  }

          /*
           * Perform the chosen action.
           */

          if (action == CLEAR) {
                  fd = open("/dev/echo", O_RDWR);
                  if (fd < 0)
                          err(1, "open(/dev/echo)");

                  i = ioctl(fd, ECHO_CLEAR_BUFFER, NULL);
                  if (i < 0)
                          err(1, "ioctl(/dev/echo)");

                  close (fd);
          } else if (action == SETSIZE) {
                  fd = open("/dev/echo", O_RDWR);
                  if (fd < 0)
                          err(1, "open(/dev/echo)");

                  i = ioctl(fd, ECHO_SET_BUFFER_SIZE, &size);
                  if (i < 0)
                          err(1, "ioctl(/dev/echo)");

                  close (fd);
          } else
                  usage();

          return (0);
  }

注意

示例 3-2 是一个相当标准的命令行工具。因此,我不会介绍其程序结构。相反,我会集中讨论它是如何调用 echo_ioctl 的。

这个程序首先重新定义了 ECHO_CLEAR_BUFFERECHO_SET_BUFFER_SIZE。^([4]) 要发出 ioctl 命令,示例 3-2 首先打开 /dev/echo。然后它使用适当的参数调用 ioctl(2)

注意,由于 ECHO_CLEAR_BUFFER 不传输任何数据,NULL 被传递为 ioctl(2) 的第三个参数。

以下展示了执行 示例 3-2 清除内存缓冲区的结果:

$ `sudo cat /dev/echo`
Opening echo device.
DON'T PANIC
Closing echo device.
$ `sudo ./echo_config -c`
Opening echo device.
Buffer cleared.
Closing echo device.
$ `sudo cat /dev/echo`
Opening echo device.
Closing echo device.

以下展示了执行 示例 3-2 调整内存缓冲区大小的结果:

$ `sudo ./echo_config -s 128`
Opening echo device.
Buffer resized.
Closing echo device.

^([4]) 通过在头文件中定义那些 ioctl 命令,可以避免这一步骤。

sysctl

如前所述,sysctl 接口用于动态更改或检查内核参数,这包括设备驱动程序。例如,一些驱动程序允许您使用 sysctl 启用(或禁用)调试选项。

注意

本书假设您知道如何使用 sysctl;如果您不知道,请参阅 sysctl(8) 手册页。

与前一个主题不同,我将采取整体的方法来解释 sysctl。也就是说,我将首先展示一个示例,然后描述 sysctl 函数。我发现这是理解实现 sysctl 的最简单方法。

实现 sysctl,第一部分

示例 3-3 是一个完整的 KLD(基于安德烈·比亚莱茨基编写的代码)创建多个 sysctl。

示例 3-3. pointless.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>
  #include <sys/sysctl.h>

  static long  a = 100;
  static int   b = 200;
  static char *c = "Are you suggesting coconuts migrate?";

  static struct sysctl_ctx_list clist;
  static struct sysctl_oid *poid;

  static int
 sysctl_pointless_procedure(SYSCTL_HANDLER_ARGS)
  {
          char *buf = "Not at all. They could be carried.";
          return (sysctl_handle_string(oidp, buf, strlen(buf), req));
  }

  static int
  pointless_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  sysctl_ctx_init(&clist);

                  poid = SYSCTL_ADD_NODE(&clist,
                      SYSCTL_STATIC_CHILDREN(/* tree top */), OID_AUTO,
                      "example", CTLFLAG_RW, 0, "new top-level tree");
                  if (poid == NULL) {
                          uprintf("SYSCTL_ADD_NODE failed.\n");
                          return (EINVAL);
                  }
                  SYSCTL_ADD_LONG(&clist, SYSCTL_CHILDREN(poid), OID_AUTO,
                      "long", CTLFLAG_RW, &a, "new long leaf");
                  SYSCTL_ADD_INT(&clist, SYSCTL_CHILDREN(poid), OID_AUTO,
                      "int", CTLFLAG_RW, &b, 0, "new int leaf");

                  poid = SYSCTL_ADD_NODE(&clist, SYSCTL_CHILDREN(poid),
                      OID_AUTO, "node", CTLFLAG_RW, 0,
                      "new tree under example");
                  if (poid == NULL) {
                          uprintf("SYSCTL_ADD_NODE failed.\n");
                          return (EINVAL);
                  }
                  SYSCTL_ADD_PROC(&clist, SYSCTL_CHILDREN(poid), OID_AUTO,
                      "proc", CTLFLAG_RD, 0, 0, sysctl_pointless_procedure,
                      "A", "new proc leaf");

                  poid = SYSCTL_ADD_NODE(&clist,
                      SYSCTL_STATIC_CHILDREN(_debug), OID_AUTO, "example",
                      CTLFLAG_RW, 0, "new tree under debug");
                  if (poid == NULL) {
                          uprintf("SYSCTL_ADD_NODE failed.\n");
                          return (EINVAL);
                  }
                  SYSCTL_ADD_STRING(&clist, SYSCTL_CHILDREN(poid), OID_AUTO,
                      "string", CTLFLAG_RD, c, 0, "new string leaf");

                  uprintf("Pointless module loaded.\n");
                  break;
          case MOD_UNLOAD:
                  if (sysctl_ctx_free(&clist)) {
                          uprintf("sysctl_ctx_free failed.\n");
                          return (ENOTEMPTY);
                  }
                  uprintf("Pointless module unloaded.\n");
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  static moduledata_t pointless_mod = {
          "pointless",
          pointless_modevent,
          NULL
  };

  DECLARE_MODULE(pointless, pointless_mod, SI_SUB_EXEC, SI_ORDER_ANY);

在模块加载时,示例 3-3 首先通过 初始化一个名为 clist 的 sysctl 上下文。一般来说,sysctl 上下文 负责跟踪动态创建的 sysctl——这就是为什么 clist 被传递给每个 SYSCTL_ADD_* 调用的原因。

第一次调用 SYSCTL_ADD_NODE 创建了一个名为 example 的新顶级类别。SYSCTL_ADD_LONG 调用创建了一个名为 long 的新 sysctl,它处理一个长变量。请注意,SYSCTL_ADD_LONG 的第二个参数是 SYSCTL_CHILDREN(poid)^([5]),而 poid 包含 SYSCTL_ADD_NODE 的返回值。因此,long 被放置在 example 下,如下所示:

example.long

SYSCTL_ADD_INT 调用创建了一个名为 int 的新 sysctl,它处理一个整数变量。由于与 SYSCTL_ADD_LONG 的原因相同,int 被放置在 example 下:

example.long
example.int

第二次调用 SYSCTL_ADD_NODE 创建了一个名为 node 的新子类别,它被放置在 example 下,如下所示:

example.long
example.int
example.node

SYSCTL_ADD_PROC 调用创建了一个名为 proc 的新 sysctl,它使用一个 函数来处理其读写请求;在这种情况下,该函数只是打印一些文本。您会注意到 SYSCTL_ADD_PROC 的第二个参数也是 SYSCTL_CHILDREN(poid)。但 poid 现在包含第二次 SYSCTL_ADD_NODE 调用的返回值。因此,proc 被放置在 node 下:

example.long
example.int
example.node.proc

第三次调用 SYSCTL_ADD_NODE 创建了一个名为 example 的新子类别。如您所见,其第二个参数是 SYSCTL_STATIC_CHILDREN(_debug),^([6]) 这将 example 放在 debug(这是一个静态顶级类别)下。

debug.example
example.long
example.int
example.node.proc

![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137515.png) SYSCTL_ADD_STRING 调用创建了一个名为 string 的新 sysctl,用于处理字符串。由于显而易见的原因,string 被放置在 debug.example 下:

debug.example.string
example.long
example.int
example.node.proc

在模块卸载时,示例 3-3 简单地将 clist 传递给 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137517.png) sysctl_ctx_free 以销毁在模块加载期间创建的每个 sysctl。

以下展示了加载 示例 3-3 的结果:

$ `sudo kldload ./pointless.ko`
Pointless module loaded.
$ `sysctl -A | grep example`
debug.example.string: Are you suggesting coconuts migrate?
example.long: 100
example.int: 200
example.node.proc: Not at all. They could be carried.

现在,让我们详细讨论 示例 3-3 中使用的不同函数和宏。


^([5]) SYSCTL_CHILDREN 宏在 SYSCTL_STATIC_CHILDREN 宏 中描述。

^([6]) SYSCTL_STATIC_CHILDREN 宏在 SYSCTL_STATIC_CHILDREN 宏 中描述。

sysctl 上下文管理例程

如前所述,sysctl 上下文管理动态创建的 sysctl。sysctl 上下文通过 sysctl_ctx_init 函数初始化。

#include <sys/types.h>
#include <sys/sysctl.h>

int
sysctl_ctx_init(struct sysctl_ctx_list *clist);

sysctl 上下文初始化后,它可以传递给各种 SYSCTL_ADD_* 宏。这些宏将使用指向新创建的 sysctl 的指针更新 sysctl 上下文。

相反,sysctl_ctx_free 函数接受一个 sysctl 上下文并销毁它所指向的每个 sysctl。

#include <sys/types.h>
#include <sys/sysctl.h>

int
sysctl_ctx_free(struct sysctl_ctx_list *clist);

如果无法销毁 sysctl,则重新启用与 sysctl 上下文关联的所有 sysctl。

创建动态 sysctl

FreeBSD 内核提供了以下 10 个宏,用于在运行时创建 sysctl:

#include <sys/types.h>
#include <sys/sysctl.h>

struct sysctl_oid *
SYSCTL_ADD_OID(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int kind, void *arg1, int arg2, int (*handler) (SYSCTL_HANDLER_ARGS),
    const char *format, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_NODE(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, int (*handler) (SYSCTL_HANDLER_ARGS), const char *descr);

struct sysctl_oid *
SYSCTL_ADD_STRING(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, char *arg, int len, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_INT(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, int *arg, int len, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_UINT(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, unsigned int *arg, int len, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_LONG(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, long *arg, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_ULONG(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, unsigned long *arg, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_OPAQUE(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, void *arg, int len, const char *format,
    const char *descr);

struct sysctl_oid *
SYSCTL_ADD_STRUCT(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, void *arg, STRUCT_NAME, const char *descr);

struct sysctl_oid *
SYSCTL_ADD_PROC(struct sysctl_ctx_list *ctx,
    struct sysctl_oid_list *parent, int number, const char *name,
    int access, void *arg, int len,
    int (*handler) (SYSCTL_HANDLER_ARGS), const char *format,
    const char *descr);

SYSCTL_ADD_OID 宏创建一个新的 sysctl,它可以处理任何数据类型。如果成功,则返回指向 sysctl 的指针;否则,返回 NULL

其他 SYSCTL_ADD_* 宏是 SYSCTL_ADD_OID 的替代方案,可以创建一个可以处理特定数据类型的 sysctl。这些宏在 表 3-2 中解释。

表 3-2. SYSCTL_ADD_* 宏

描述
SYSCTL_ADD_NODE 创建一个新的节点(或类别),可以添加子节点
SYSCTL_ADD_STRING 创建一个新的 sysctl,用于处理以空字符终止的字符串
SYSCTL_ADD_INT 创建一个新的 sysctl,用于处理整数变量
SYSCTL_ADD_UINT 创建一个新的 sysctl,用于处理无符号整数变量
SYSCTL_ADD_LONG 创建一个新的 sysctl,用于处理长变量
SYSCTL_ADD_ULONG 创建一个新的 sysctl,用于处理无符号长变量
SYSCTL_ADD_OPAQUE 创建一个新的 sysctl,用于处理一块不透明数据;这块数据的大小由 len 参数指定
SYSCTL_ADD_STRUCT 创建一个新的 sysctl,用于处理结构体
SYSCTL_ADD_PROC 创建一个新的 sysctl,它使用一个函数来处理其读写请求;这个“处理函数”通常用于在导入或导出之前处理数据

在大多数情况下,你应该使用 SYSCTL_ADD_* 宏而不是通用的 SYSCTL_ADD_OID 宏。

SYSCTL_ADD_* 宏的参数在 表 3-3 中描述。

表 3-3. SYSCTL_ADD_* 参数

参数 描述
ctx 期望一个指向 sysctl 上下文的指针
parent 期望一个指向父 sysctl 的子节点列表的指针;关于这一点稍后会有更多说明
number 期望 sysctl 的编号;这应该始终设置为 OID_AUTO
name 期望 sysctl 的名称
access 期望一个访问标志;访问标志 指定 sysctl 是只读(CTLFLAG_RD)还是读写(CTLFLAG_RW
arg 期望一个指向 sysctl 将管理的数据的指针(或 NULL
len 如果不是调用 SYSCTL_ADD_OPAQUE,则将其设置为 0
handler 期望一个指向将处理 sysctl 的读写请求的函数的指针(或 0
format 期望一个格式名称;格式名称 识别 sysctl 将管理的类型数据;格式名称的完整列表是:"N" 表示节点,"A" 表示 char *"I" 表示 int"IU" 表示 unsigned int"L" 表示 long"LU" 表示 unsigned long,以及 "S,foo" 表示 struct foo
descr 期望 sysctl 的文本描述;此描述由 sysctl -d 打印

SYSCTL_ADD_* 宏创建的 sysctl 必须连接到父 sysctl。这是通过将 SYSCTL_STATIC_CHILDRENSYSCTL_CHILDREN 作为 parent 参数传递来完成的。

SYSCTL_STATIC_CHILDREN 宏

SYSCTL_STATIC_CHILDREN 宏在连接到静态节点时作为 parent 传递。一个 静态节点 是基本系统的一部分。

#include <sys/types.h>
#include <sys/sysctl.h>

struct sysctl_oid_list *
SYSCTL_STATIC_CHILDREN(struct sysctl_oid_list OID_NAME);

这个宏接受一个以下划线开头的父 sysctl 名称。并且所有点都必须替换为下划线。因此,要连接到 hw.usb,你会使用 _hw_usb

如果将 SYSCTL_STATIC_CHILDREN(/* 无参数 */) 作为 parent 传递给 SYSCTL_ADD_NODE,将创建一个新的顶级类别。

SYSCTL_CHILDREN 宏

SYSCTL_CHILDREN 宏在连接到动态节点时作为 parent 传递。一个 动态节点 是由 SYSCTL_ADD_NODE 调用创建的。

#include <sys/types.h>
#include <sys/sysctl.h>

struct sysctl_oid_list *
SYSCTL_CHILDREN(struct sysctl_oid *oidp);

这个宏将其唯一的参数作为 SYSCTL_ADD_NODE 调用返回的指针。

实现 sysctl,第二部分

现在你已经知道了如何在运行时创建 sysctl,让我们做一些实际的设备控制(而不是引用蒙提·派森)。

示例 3-4 是 示例 3-1 的修订版,它使用 sysctl 来调整内存缓冲区的大小。

注意

为了节省空间,函数 echo_openecho_closeecho_writeecho_read 没有在此列出,因为它们没有发生变化。

示例 3-4. echo-4.0.c

#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>

#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/malloc.h>
#include <sys/ioccom.h>
#include <sys/sysctl.h>

MALLOC_DEFINE(M_ECHO, "echo_buffer", "buffer for echo driver");

#define ECHO_CLEAR_BUFFER       _IO('E', 1)

static d_open_t         echo_open;
static d_close_t        echo_close;
static d_read_t         echo_read;
static d_write_t        echo_write;
static d_ioctl_t        echo_ioctl;

static struct cdevsw echo_cdevsw = {
        .d_version =    D_VERSION,
        .d_open =       echo_open,
        .d_close =      echo_close,
        .d_read =       echo_read,
        .d_write =      echo_write,
        .d_ioctl =      echo_ioctl,
        .d_name =       "echo"
};

typedef struct echo {
        int buffer_size;
        char *buffer;
        int length;
} echo_t;

static echo_t *echo_message;
static struct cdev *echo_dev;

static struct sysctl_ctx_list clist;
static struct sysctl_oid *poid;

static int
echo_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
...
}

static int
echo_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
...
}

static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
echo_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
        int error = 0;

        switch (cmd) {
        case ECHO_CLEAR_BUFFER:
                memset(echo_message->buffer, '\0',
                    echo_message->buffer_size);
                echo_message->length = 0;
                uprintf("Buffer cleared.\n");
                break;
        default:
                error = ENOTTY;
                break;
        }

        return (error);
}

static int
sysctl_set_buffer_size(SYSCTL_HANDLER_ARGS)
{
        int error = 0;
        int size = echo_message->buffer_size;

        error = sysctl_handle_int(oidp, &size, 0, req);
        if (error || !req->newptr || echo_message->buffer_size == size)
                return (error);

        if (size >= 128 && size <= 512) {
                echo_message->buffer = realloc(echo_message->buffer, size,
                    M_ECHO, M_WAITOK);
                echo_message->buffer_size = size;

                if (echo_message->length >= size) {
                        echo_message->length = size - 1;
                        echo_message->buffer[size - 1] = '\0';
                }
        } else
                error = EINVAL;

        return (error);
}

static int
echo_modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;

        switch (event) {
        case MOD_LOAD:
                echo_message = malloc(sizeof(echo_t), M_ECHO, M_WAITOK);
                echo_message->buffer_size = 256;
                echo_message->buffer = malloc(echo_message->buffer_size,
                    M_ECHO, M_WAITOK);
                sysctl_ctx_init(&clist);
                poid = SYSCTL_ADD_NODE(&clist,
                    SYSCTL_STATIC_CHILDREN(/* tree top */), OID_AUTO,
                    "echo", CTLFLAG_RW, 0, "echo root node");
                SYSCTL_ADD_PROC(&clist, SYSCTL_CHILDREN(poid), OID_AUTO,
                    "buffer_size", CTLTYPE_INT | CTLFLAG_RW,
                   &echo_message->buffer_size, 0,
 sysctl_set_buffer_size,
                    "I", "echo buffer size");
                echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0600, "echo");
                uprintf("Echo driver loaded.\n");
                break;
        case MOD_UNLOAD:
                destroy_dev(echo_dev);
                sysctl_ctx_free(&clist);
                free(echo_message->buffer, M_ECHO);
                free(echo_message, M_ECHO);
                uprintf("Echo driver unloaded.\n");
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

DEV_MODULE(echo, echo_modevent, NULL);

在模块加载时,示例 3-4 创建了一个名为 echo.buffer_size 的 sysctl,该 sysctl 管理内存缓冲区的大小。此外,这个 sysctl 使用一个名为 sysctl_set_buffer_size图片 处理函数来调整内存缓冲区的大小。

sysctl_set_buffer_size 函数

如上所述,sysctl_set_buffer_size 函数调整内存缓冲区的大小。在描述这个函数之前,让我们确定它的参数。

static int
sysctl_set_buffer_size(SYSCTL_HANDLER_ARGS)

常量 图片 SYSCTL_HANDLER_ARGS<sys/sysctl.h> 中定义如下:

#define SYSCTL_HANDLER_ARGS struct sysctl_oid *oidp, void *arg1, \
        int arg2, struct sysctl_req *req

在这里,图片 oidp 指向 sysctl,图片 arg1 指向 sysctl 管理的数据,图片 arg2 是数据的长度,图片 req 描述了 sysctl 请求。

现在,牢记这些论点,让我们来检查函数 sysctl_set_buffer_size

static int
sysctl_set_buffer_size(SYSCTL_HANDLER_ARGS)
{
        int error = 0;
      int size = echo_message->buffer_size;

        error = sysctl_handle_int(oidp, &size, 0, req);
      if (error ||
 !req->newptr || echo_message->buffer_size == size)
                return (error);

        if (size >= 128 && size <= 512) {
                echo_message->buffer = realloc(echo_message->buffer, size,
                    M_ECHO, M_WAITOK);
                echo_message->buffer_size = size;

                if (echo_message->length >= size) {
                        echo_message->length = size - 1;
                        echo_message->buffer[size - 1] = '\0';
                }
        } else
                error = EINVAL;

        return (error);
}

这个函数首先将 图片 size 设置为当前缓冲区大小。之后,调用 图片 sysctl_handle_int 从用户空间获取新的 sysctl 值(即建议的缓冲区大小)。

注意,sysctl_handle_int图片 第二个参数是 &size。看,这个函数接受原始 sysctl 值的指针,并用新的 sysctl 值覆盖它。

这个 图片 if 语句确保新 sysctl 值获取成功。它通过验证 sysctl_handle_int 没有错误返回,并且 图片 req->newptr 是有效的。

sysctl_set_buffer_size 的其余部分与在 echo_set_buffer_size 函数 中描述的 echo_set_buffer_size 相同。

不要慌张

现在,让我们尝试 示例 3-4:

$ `sudo kldload ./echo-4.0.ko`
Echo driver loaded.
$ `sudo sysctl echo.buffer_size=128`
echo.buffer_size: 256 -> 128

成功!

结论

本章介绍了设备通信和控制的传统方法:sysctl 和 ioctl。通常,sysctls 用于调整参数,而 ioctls 用于其他所有操作——这就是为什么 ioctls 是 I/O 操作的万能工具。注意,如果你发现自己只是为了 ioctl 请求创建设备节点,你可能应该使用 sysctls。

顺便提醒,编写与驱动程序交互的用户模式程序相对简单。因此,你的驱动程序——而不是你的用户模式程序(例如,示例 3-2)——应该始终验证用户输入。

第四章。线程同步

无标题图片

本章讨论由并发线程引起的数据和状态损坏问题。当多个线程在不同的 CPU 上同时修改相同的数据结构时,该结构可能会被损坏。同样,当一个线程被中断而另一个线程操作第一个线程正在操作的数据时,这些数据可能会被损坏(Baldwin,2002)。

幸运的是,FreeBSD 提供了一套同步原语来处理这些问题。在我描述同步原语做什么之前,您需要深入了解上述并发问题,也称为同步问题。为此,让我们分析几个。

一个简单的同步问题

考虑以下场景,其中两个线程增加相同的全局变量。在i386上,此操作可能使用以下处理器指令:

movl   count,%eax       # Move the value of count into a register (eax).
addl   $0x1,%eax        # Add 1 to the value in the register.
movl   %eax,count       # Move the value of the register into count.

假设count当前为0,并且第一个线程在第二个线程抢占它之前成功地将count的当前值加载到%eax(即,它完成了第一条指令)。作为线程切换的一部分,FreeBSD 将%eax的值(即0)保存到即将退出的线程的上下文中。现在,假设第二个线程成功完成所有三个指令,从而将count0增加到1。如果第一个线程抢占第二个线程,FreeBSD 将恢复其线程上下文,其中包括将%eax设置为0。第一个线程将在第二条指令处恢复执行,现在将向%eax添加1并将结果存储在count中。此时,count等于1,而它应该等于2。因此,由于同步问题,我们丢失了一个更新。当两个线程并发执行但略微不同步时(即,一个线程开始执行第一条指令时,另一个线程开始执行第二条指令),这也可能发生。

一个更复杂的同步问题

示例 4-1 是一个完整的字符驱动程序,允许您通过其 d_ioctl 函数操作双链表。您可以从列表中添加或删除项目,确定项目是否在列表中,或打印列表上的每个项目。示例 4-1 也包含一些同步问题。

注意

快速查看以下代码,并尝试识别同步问题。

示例 4-1. race.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/malloc.h>
  #include <sys/ioccom.h>
  #include <sys/queue.h>
  #include "race_ioctl.h"

  MALLOC_DEFINE(M_RACE, "race", "race object");

  struct race_softc {
         LIST_ENTRY(race_softc) list;
         int unit;
  };

  static LIST_HEAD(, race_softc) race_list =
      LIST_HEAD_INITIALIZER(&race_list);

  static struct race_softc *      race_new(void);
  static struct race_softc *      race_find(int unit);
  static void                     race_destroy(struct race_softc *sc);
  static d_ioctl_t                race_ioctl;

 static struct cdevsw race_cdevsw = {
          .d_version =    D_VERSION,
          .d_ioctl =      race_ioctl,
          .d_name =     RACE_NAME
  };

  static struct cdev *race_dev;

  static int
 race_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
      struct thread *td)
  {
          struct race_softc *sc;
          int error = 0;

          switch (cmd) {
          case RACE_IOC_ATTACH:
                  sc = race_new();
                  *(int *)data = sc->unit;
                  break;
          case RACE_IOC_DETACH:
                  sc = race_find(*(int *)data);
                  if (sc == NULL)
                          return (ENOENT);
                  race_destroy(sc);
                  break;
          case RACE_IOC_QUERY:
                  sc = race_find(*(int *)data);
                  if (sc == NULL)
                          return (ENOENT);
                  break;
          case RACE_IOC_LIST:
                  uprintf("  UNIT\n");
                  LIST_FOREACH(sc, &race_list, list)
                          uprintf("  %d\n", sc->unit);
                  break;
          default:
                  error = ENOTTY;
                  break;
          }

          return (error);
  }

  static struct race_softc *
  race_new(void)
  {
          struct race_softc *sc;
          int unit, max = −1;

          LIST_FOREACH(sc, &race_list, list) {
                  if (sc->unit > max)
                          max = sc->unit;
          }
          unit = max + 1;

          sc = (struct race_softc *)malloc(sizeof(struct race_softc), M_RACE,
              M_WAITOK | M_ZERO);
          sc->unit = unit;
          LIST_INSERT_HEAD(&race_list, sc, list);

          return (sc);
  }

  static struct race_softc *
  race_find(int unit)
  {
          struct race_softc *sc;

          LIST_FOREACH(sc, &race_list, list) {
                  if (sc->unit == unit)
                          break;
          }

          return (sc);
  }

  static void
  race_destroy(struct race_softc *sc)
  {
          LIST_REMOVE(sc, list);
          free(sc, M_RACE);
  }

  static int
  race_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  race_dev = make_dev(&race_cdevsw, 0, UID_ROOT, GID_WHEEL,
                      0600, RACE_NAME);
                  uprintf("Race driver loaded.\n");
                  break;
          case MOD_UNLOAD:
                  destroy_dev(race_dev);
                  uprintf("Race driver unloaded.\n");
                  break;
          case MOD_QUIESCE:
                  if (!LIST_EMPTY(&race_list))
                          error = EBUSY;
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  DEV_MODULE(race, race_modevent, NULL);

在我识别示例 4-1 的同步问题之前,让我们先了解一下。示例 4-1 首先定义并初始化了一个名为race_list的双向链表,该链表包含race_softc结构体。每个race_softc结构体包含一个(唯一的)图片单元号和一个图片结构体,该结构体维护对race_list中前一个和下一个race_softc结构体的指针。

接下来是示例 4-1 的图片字符设备切换表的定义。常量图片RACE_NAMErace_ioctl.h头文件中定义为以下内容:

#define RACE_NAME               "race"

注意示例 4-1 的字符设备切换表没有定义d_opend_close。回想一下,从第一章中,如果一个d_foo函数未定义,则相应的操作不受支持。然而,d_opend_close是独特的;当它们未定义时,内核将自动定义它们如下:

int
nullop(void)
{

        return (0);
}

这确保了每个已注册的字符设备都可以被打开和关闭。

注意

驱动程序通常在不需要为 I/O 准备设备时省略定义d_opend_close函数——就像示例 4-1 一样。

接下来是示例 4-1 的d_ioctl函数,命名为图片race_ioctl。这个函数类似于示例 4-1 的main函数。它使用三个辅助函数来完成其工作:

  • race_new

  • race_find

  • race_destroy

在我描述race_ioctl之前,我将先描述这些函数。

race_new函数

race_new函数创建一个新的race_softc结构体,然后将其插入到race_list的头部。以下是race_new函数的定义(再次):

static struct race_softc *
race_new(void)
{
        struct race_softc *sc;
        int unit, max = −1;

       LIST_FOREACH(sc, &race_list, list) {
                if (sc->unit > max)
                       max = sc->unit;
        }
        unit = max + 1;

        sc = (struct race_softc *)malloc(sizeof(struct race_softc), M_RACE,
            M_WAITOK | M_ZERO);
        sc->unit = unit;
       LIST_INSERT_HEAD(&race_list, sc, list);

       return (sc);
}

此函数首先遍历 race_list,寻找最大的单元号,并将其存储在 max 中。然后,将 unit 设置为 max 加一。然后,race_new 为新的 race_softc 结构分配内存,将其分配单元号 unit,并将其插入到 race_list 的头部。最后,race_new 返回新的 race_softc 结构的指针。

race_find 函数

race_find 函数接收一个单元号,并在 race_list 上找到相关的 race_softc 结构。

static struct race_softc *
race_find(int unit)
{
        struct race_softc *sc;

        LIST_FOREACH(sc, &race_list, list) {
                if (sc->unit == unit)
                        break;
        }

        return (sc);
}

如果 race_find 成功,则返回 race_softc 结构的指针;否则,返回 NULL

race_destroy 函数

race_destroy 函数销毁 race_list 上的 race_softc 结构。以下是其函数定义(再次):

static void
race_destroy(struct race_softc *sc)
{
       LIST_REMOVE(sc, list);
       free(sc, M_RACE);
}

此函数接收一个指向 race_softc 结构的指针,并将其从 race_list 中移除。然后,它释放该结构的分配内存。

race_ioctl 函数

在我介绍 race_ioctl 之前,需要解释其 ioctl 命令,这些命令在 race_ioctl.h 中定义。

#define RACE_IOC_ATTACH         _IOR('R', 0, int)
#define RACE_IOC_DETACH         _IOW('R', 1, int)
#define RACE_IOC_QUERY          _IOW('R', 2, int)
#define RACE_IOC_LIST           _IO('R', 3)

如您所见,race_ioctl 的三个 ioctl 命令传输一个整数值。您将看到,这个整数值是一个单元号。

这是 race_ioctl 函数定义(再次):

static int
race_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
        struct race_softc *sc;
        int error = 0;

        switch (cmd) {
      case RACE_IOC_ATTACH:
                sc = race_new();
              *(int *)data = sc->unit;
                break;
      case RACE_IOC_DETACH:
                sc = race_find(*(int *)data);
                if (sc == NULL)
                        return (ENOENT);
                race_destroy(sc);
                break;
      case RACE_IOC_QUERY:
                sc = race_find(*(int *)data);
                if (sc == NULL)
                        return (ENOENT);
                break;
      case RACE_IOC_LIST:
                uprintf("  UNIT\n");
                LIST_FOREACH(sc, &race_list, list)
                        uprintf("  %d\n", sc->unit);
                break;
        default:
                error = ENOTTY;
                break;
        }

        return (error);
}

此函数可以执行基于四个 ioctl 的操作之一。第一个操作,RACE_IOC_ATTACH,创建一个新的 race_softc 结构,并将其插入到 race_list 的头部。之后,返回新 race_softc 结构的单元号。

第二种操作,RACE_IOC_DETACH,从 race_list 中移除用户指定的 race_softc 结构。

第三种操作,RACE_IOC_QUERY,确定用户指定的 race_softc 结构是否在 race_list 上。

最后,第四种操作,RACE_IOC_LIST,打印 race_list 上每个 race_softc 结构的单元号。

race_modevent 函数

race_modevent 函数是 示例 4-1 的模块事件处理程序。以下是其函数定义(再次):

static int
race_modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;

        switch (event) {
        case MOD_LOAD:
                race_dev = make_dev(&race_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0600, RACE_NAME);
                uprintf("Race driver loaded.\n");
                break;
        case MOD_UNLOAD:
                destroy_dev(race_dev);
                uprintf("Race driver unloaded.\n");
                break;
      case MOD_QUIESCE:
              if (!LIST_EMPTY(&race_list))
                        error = EBUSY;
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

如你所见,这个函数包括一个新的情况:图片 MOD_QUIESCE

注意

由于MOD_LOADMOD_UNLOAD非常基础,而且你已经在其他地方见过类似的代码,所以我会省略对它们的讨论。

当你发出kldunload(8)命令时,MOD_QUIESCEMOD_UNLOAD之前运行。如果MOD_QUIESCE返回错误,则不会执行MOD_UNLOAD。换句话说,MOD_QUIESCE验证卸载你的模块是否安全。

注意

kldunload -f命令忽略了MOD_QUIESCE返回的每个错误。因此,你总是可以卸载一个模块,但这可能不是最好的主意。

在这里,MOD_QUIESCE 图片 确保在示例 4-1 卸载之前race_list是空的。这样做是为了防止任何可能未被声明的race_softc结构造成内存泄漏。

问题根源

现在我们已经走过了示例 4-1,让我们运行它并看看我们是否能识别其同步问题。

示例 4-2 展示了一个命令行工具,该工具用于调用示例 4-1 中的race_ioctl函数。

示例 4-2. race_config.c

#include <sys/types.h>
#include <sys/ioctl.h>

#include <err.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "../race/race_ioctl.h"

static enum {UNSET, ATTACH, DETACH, QUERY, LIST} action = UNSET;

/*
 * The usage statement: race_config -a | -d unit | -q unit | -l
 */

static void
usage()
{
        /*
         * Arguments for this program are "either-or." For example,
         * 'race_config -a' or 'race_config -d unit' are valid; however,
         * 'race_config -a -d unit' is invalid.
         */

        fprintf(stderr, "usage: race_config -a | -d unit | -q unit | -l\n");
        exit(1);
}

/*
 * This program manages the doubly linked list found in /dev/race. It
 * allows you to add or remove an item, query the existence of an item,
 * or print every item on the list.
 */

int
main(int argc, char *argv[])
{
        int ch, fd, i, unit;
        char *p;

        /*
         * Parse the command line argument list to determine
         * the correct course of action.
         *
         *    -a:      add an item.
         *    -d unit: detach an item.
         *    -q unit: query the existence of an item.
         *    -l:      list every item.
         */

        while ((ch = getopt(argc, argv, "ad:q:l")) != −1)
                switch (ch) {
                case 'a':
                        if (action != UNSET)
                                usage();
                        action = ATTACH;
                        break;
                case 'd':
                        if (action != UNSET)
                                usage();
                        action = DETACH;
                        unit = (int)strtol(optarg, &p, 10);
                        if (*p)
                                errx(1, "illegal unit -- %s", optarg);
                        break;
                case 'q':
                        if (action != UNSET)
                                usage();
                        action = QUERY;
                        unit = (int)strtol(optarg, &p, 10);
                        if (*p)
                                errx(1, "illegal unit -- %s", optarg);
                        break;
                case 'l':
                        if (action != UNSET)
                                usage();
                        action = LIST;
                        break;
                default:
                        usage();
                }

        /*
         * Perform the chosen action.
         */

        if (action == ATTACH) {
                fd = open("/dev/" RACE_NAME, O_RDWR);
                if (fd < 0)
                        err(1, "open(/dev/%s)", RACE_NAME);

                i = ioctl(fd, RACE_IOC_ATTACH, &unit);
                if (i < 0)
                        err(1, "ioctl(/dev/%s)", RACE_NAME);
                printf("unit: %d\n", unit);

                close (fd);
        } else if (action == DETACH) {
                fd = open("/dev/" RACE_NAME, O_RDWR);
                if (fd < 0)
                        err(1, "open(/dev/%s)", RACE_NAME);

                i = ioctl(fd, RACE_IOC_DETACH, &unit);
                if (i < 0)
                        err(1, "ioctl(/dev/%s)", RACE_NAME);

                close (fd);
        } else if (action == QUERY) {
                fd = open("/dev/" RACE_NAME, O_RDWR);
                if (fd < 0)
                        err(1, "open(/dev/%s)", RACE_NAME);

                i = ioctl(fd, RACE_IOC_QUERY, &unit);
                if (i < 0)
                        err(1, "ioctl(/dev/%s)", RACE_NAME);

                close (fd);
        } else if (action == LIST) {
                fd = open("/dev/" RACE_NAME, O_RDWR);
                if (fd < 0)
                        err(1, "open(/dev/%s)", RACE_NAME);

                i = ioctl(fd, RACE_IOC_LIST, NULL);
                if (i < 0)
                        err(1, "ioctl(/dev/%s)", RACE_NAME);

                close (fd);
        } else
                usage();

        return (0);
}

注意

示例 4-2 是一个标准的命令行工具。因此,我不会介绍其程序结构。

以下展示了示例 4-2 的示例执行:

$ `sudo kldload ./race.ko`
Race driver loaded.
$ `sudo ./race_config -a & sudo ./race_config -a &`
[1] 2378
[2] 2379
$ unit: 0
unit: 0

在上面,两个线程同时将一个race_softc结构添加到race_list中,导致有两个具有“唯一”单元号0race_softc结构——这是问题,对吗?

这里还有一个例子:

$ `sudo kldload ./race.ko`
Race driver loaded.
$ `sudo ./race_config -a & sudo kldunload race.ko &`
[1] 2648
[2] 2649
$ unit: 0
Race driver unloaded.

[1]-  Done                    sudo ./race_config -a
[2]+  Done                    sudo kldunload race.ko
$ `dmesg | tail -n 1`
Warning: memory type race leaked memory on destroy (1 allocations, 16 bytes
leaked).

在上面,一个线程将一个race_softc结构添加到race_list中,而另一个线程卸载race.ko,这导致内存泄漏。回想一下,MOD_QUIESCE本应防止这种情况,但它没有。为什么?

在这两个例子中,问题都是竞争条件。竞争条件是由一系列事件引起的错误。在第一个例子中,两个线程同时检查race_list,发现它是空的,并将0作为单元号分配。在第二个例子中,MOD_QUIESCE返回无错误,随后将一个race_softc结构添加到race_list中,最后MOD_UNLOAD完成。

注意

竞争条件的一个特点是它们很难重现。因此,在先前的例子中,我修改了结果。也就是说,我在特定的点上使线程进行上下文切换以达到预期的结果。在正常情况下,要使这些竞争条件发生,实际上需要数百万次尝试,我不想花那么多时间。

防止竞争条件

使用锁可以防止竞态条件。,也称为同步原语,用于序列化两个或多个线程的执行。例如,示例 4-1 中的竞态条件,是由于对race_list的并发访问引起的,可以通过使用锁来序列化对race_list的访问来防止。在线程可以访问race_list之前,它必须首先获取 foo 锁。一次只能有一个线程持有 foo。如果一个线程无法获取foo,它就不能访问race_list,必须等待当前所有者释放foo。此协议保证在任何时刻只有一个线程可以访问race_list,从而消除了示例 4-1 的竞态条件。

FreeBSD 中有几种不同的锁类型,每种锁都有其自身的特性(例如,一些锁可以被多个线程持有)。本章的剩余部分将描述 FreeBSD 中可用的不同类型的锁以及如何使用它们。

互斥锁

互斥锁(mutexes) 确保在任何时刻,只有一个线程可以访问共享对象。互斥锁是互斥和排他的结合。

注意

上一个章节中描述的foo锁是一个互斥锁。

FreeBSD 提供了两种类型的互斥锁:自旋互斥锁和睡眠互斥锁。

自旋互斥锁

自旋互斥锁 是简单的自旋锁。如果一个线程尝试获取被另一个线程持有的自旋锁,它将“自旋”并等待锁被释放。在这里,“自旋”意味着在 CPU 上无限循环。这种自旋可能导致死锁,如果持有自旋锁的线程被中断或进行上下文切换,并且所有后续线程都尝试获取该锁。因此,在持有自旋互斥锁期间,所有中断都会在本地处理器上被阻塞,并且无法执行上下文切换。

自旋互斥锁应仅用于短时间持有,并且仅用于保护与不可抢占中断和低级调度代码相关的对象(McKusick 和 Neville-Neil,2005)。通常,你永远不会使用自旋互斥锁。

睡眠互斥锁

睡眠互斥锁 是最常用的锁。如果一个线程尝试获取被另一个线程持有的睡眠互斥锁,它将进行上下文切换(即睡眠)并等待互斥锁被释放。由于这种行为,睡眠互斥锁不会受到上述死锁的影响。

睡眠互斥锁支持优先级传播。当一个线程在睡眠互斥锁上睡眠,并且其优先级高于睡眠互斥锁的当前所有者时,当前所有者将继承该线程的优先级(Baldwin,2002)。这种特性防止低优先级线程阻塞高优先级线程。

注意

在持有互斥锁的情况下睡眠(例如,调用*sleep函数,该函数将在第五章中讨论)从不安全,必须避免;否则,会有许多断言失败,内核会崩溃(McKusick 和 Neville-Neil,2005)。

Mutex 管理例程

FreeBSD 内核提供了以下七个函数用于处理互斥锁:

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/mutex.h>

void
mtx_init(struct mtx mutex, const char name, const char type,
    int opts);

void
mtx_lock(struct mtx mutex);

void
mtx_lock_spin(struct mtx mutex);

int
mtx_trylock(struct mtx mutex);

void
mtx_unlock(struct mtx mutex);

void
mtx_unlock_spin(struct mtx mutex);

void
mtx_destroy(struct mtx *mutex);

mtx_init函数初始化 mutex mutexname参数在调试期间用于识别mutextype参数在witness(4)进行锁顺序验证时使用。如果typeNULL,则使用name

注意

通常会将NULL作为type传递。

opts参数修改了mtx_init的行为。opts的有效值显示在表 4-1 中。

表 4-1. mtx_init 符号常量

常量 描述
MTX_DEF mutex初始化为睡眠锁;此位或MTX_SPIN必须存在
MTX_SPIN mutex初始化为自旋锁;此位或MTX_DEF必须存在
MTX_RECURSE 指定mutex是一个递归锁;关于递归锁的更多内容将在后面介绍
MTX_QUIET 指示系统不要对此锁的操作进行日志记录
MTX_NOWITNESS 导致witness(4)忽略此锁
MTX_DUPOK 导致witness(4)忽略此锁的重复项
MTX_NOPROFILE 指示系统不要对此锁进行性能分析

线程通过调用mtx_lock来获取睡眠锁。如果另一个线程当前持有mutex,调用者将休眠,直到mutex可用。

线程通过调用mtx_lock_spin来获取自旋锁。如果另一个线程当前持有mutex,调用者将自旋,直到mutex可用。注意,在自旋期间,所有中断都被本地处理器阻塞,并且在获取mutex后保持禁用状态。

如果在opts中传递了MTX_RECURSE,则线程可以递归地获取mutex(没有不良影响)。如果将在两个或更多级别获取,递归锁非常有用。例如:

static void
foo()
{
...
        mtx_lock(&mutex);
...
        foo();
...
        mtx_unlock(&mutex);
...
}

通过使用递归锁,低级别不需要检查是否由高级别获取了mutex。它们可以简单地根据需要获取和释放mutex(McKusick 和 Neville-Neil,2005)。

注意

我会避免递归互斥锁。您将在避免在独占锁上递归中了解到原因,在内存管理例程。

mtx_trylock函数与mtx_lock相同,不同之处在于如果另一个线程当前持有mutex,则它返回0(即调用者不会睡眠)。

线程通过调用mtx_unlock来释放睡眠互斥锁。请注意,递归锁“记住”它们被获取的次数。因此,每次成功的锁获取都必须有一个相应的锁释放。

线程通过调用mtx_unlock_spin来释放自旋互斥锁。mtx_unlock_spin函数还将中断状态恢复到获取mutex之前的状态。

mtx_destroy函数销毁互斥锁 互斥锁 mutex。请注意,在销毁时mutex可能被持有。然而,在销毁时,互斥锁不能被递归持有或让其他线程等待,否则内核会崩溃(McKusick 和 Neville-Neil,2005)。

实现互斥锁

示例 4-3 是示例 4-1 的修订版,它使用互斥锁来序列化对race_list的访问。

注意

为了节省空间,函数race_ioctlrace_newrace_findrace_destroy在此处未列出,因为它们没有发生变化。

示例 4-3. race_mtx.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/malloc.h>
  #include <sys/ioccom.h>
  #include <sys/queue.h>
  #include <sys/lock.h>
  #include <sys/mutex.h>
  #include "race_ioctl.h"

  MALLOC_DEFINE(M_RACE, "race", "race object");

  struct race_softc {
          LIST_ENTRY(race_softc) list;
          int unit;
  };

  static LIST_HEAD(, race_softc) race_list = LIST_HEAD_INITIALIZER(&race_list);
 static struct mtx race_mtx;

  static struct race_softc *      race_new(void);
  static struct race_softc *      race_find(int unit);
  static void                     race_destroy(struct race_softc *sc);
  static d_ioctl_t                race_ioctl_mtx;
  static d_ioctl_t                race_ioctl;

  static struct cdevsw race_cdevsw = {
          .d_version =    D_VERSION,
        .d_ioctl =      race_ioctl_mtx,
          .d_name =       RACE_NAME
  };

  static struct cdev *race_dev;

  static int
 race_ioctl_mtx(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
      struct thread *td)
  {
          int error;

        mtx_lock(&race_mtx);
          error = race_ioctl(dev, cmd, data, fflag, td);
        mtx_unlock(&race_mtx);

          return (error);
  }

  static int
  race_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
      struct thread *td)
  {
  ...
  }

  static struct race_softc *
  race_new(void)
  {
  ...
  }

  static struct race_softc *
  race_find(int unit)
  {
  ...
  }

  static void
  race_destroy(struct race_softc *sc)
  {
  ...
  }

  static int
  race_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;
          struct race_softc *sc, *sc_temp;

          switch (event) {
          case MOD_LOAD:
                  mtx_init(&race_mtx, "race config lock", NULL, MTX_DEF);
                  race_dev = make_dev(&race_cdevsw, 0, UID_ROOT, GID_WHEEL,
                      0600, RACE_NAME);
                  uprintf("Race driver loaded.\n");
                  break;
          case MOD_UNLOAD:
                  destroy_dev(race_dev);
                  mtx_lock(&race_mtx);
                  if (!LIST_EMPTY(&race_list)) {
                          LIST_FOREACH_SAFE(sc, &race_list, list, sc_temp) {
                                  LIST_REMOVE(sc, list);
                                  free(sc, M_RACE);
                          }
                  }

                  mtx_unlock(&race_mtx);
                  mtx_destroy(&race_mtx);
                  uprintf("Race driver unloaded.\n");
                  break;
          case MOD_QUIESCE:
                  mtx_lock(&race_mtx);
                  if (!LIST_EMPTY(&race_list))
                          error = EBUSY;
                  mtx_unlock(&race_mtx);
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  DEV_MODULE(race, race_modevent, NULL);

此驱动程序 声明了一个名为race_mtx的互斥锁,它在模块事件处理程序中被初始化为睡眠互斥锁

注意

正如您将看到的,互斥锁并不是示例 4-1 的理想解决方案。然而,目前我只想介绍如何使用互斥锁。

在示例 4-1 中,对race_list并发访问的主要来源是race_ioctl函数。这一点应该是显而易见的,因为race_ioctl管理race_list

示例 4-3 通过通过race_ioctl函数的序列化执行来修复由race_ioctl引起的竞态条件。race_ioctl_mtx定义为d_ioctl函数。它首先获取race_mtx。然后调用race_ioctl,随后释放race_mtx

如您所见,仅需要三行代码(或一个互斥锁)即可序列化race_ioctl的执行。

race_modevent 函数

race_modevent 函数是 示例 4-3 的模块事件处理程序。以下是它的函数定义(再次):

static int
race_modevent(module_t mod __unused, int event, void *arg __unused)
{
        int error = 0;
        struct race_softc *sc, *sc_temp;

        switch (event) {
        case MOD_LOAD:
              mtx_init(&race_mtx, "race config lock", NULL, MTX_DEF);
                race_dev = make_dev(&race_cdevsw, 0, UID_ROOT, GID_WHEEL,
                    0600, RACE_NAME);
                uprintf("Race driver loaded.\n");
                break;
        case MOD_UNLOAD:
              destroy_dev(race_dev);
                mtx_lock(&race_mtx);
              if (!LIST_EMPTY(&race_list)) {
                        LIST_FOREACH_SAFE(sc, &race_list, list, sc_temp) {
                                LIST_REMOVE(sc, list);
                              free(sc, M_RACE);
                        }
                }
                mtx_unlock(&race_mtx);
              mtx_destroy(&race_mtx);
                uprintf("Race driver unloaded.\n");
                break;
        case MOD_QUIESCE:
              mtx_lock(&race_mtx);
              if (!LIST_EMPTY(&race_list))
                        error = EBUSY;
              mtx_unlock(&race_mtx);
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

在模块加载时,此函数 race_mtx 初始化为一个 睡眠互斥锁。然后它 创建 示例 4-3 的设备节点:race

MOD_QUIESCE 时,此函数 获取 race_mtx 确认 race_list 为空,然后 释放 race_mtx

在模块卸载时,此函数首先调用 destroy_dev 来销毁 race 设备节点。

注意

destroy_dev 函数只有在当前正在执行的每个 d_foo 函数都完成后才返回。因此,在调用 destroy_dev 时不应持有锁;否则,您可能会使驱动程序死锁或使系统崩溃。

接下来,race_modevent 确认 race_list 仍然为空。注意,在执行 MOD_QUIESCE 之后,一个 race_softc 结构可能已被添加到 race_list 中。因此,再次检查 race_list,并释放找到的每个 race_softc 结构。一旦完成这些操作,race_mtx 就会被 销毁。

如您所见,每次访问 race_list 时,都会首先调用 mtx_lock(&race_mtx)。这在 示例 4-3 中对 race_list 的访问进行序列化时是必要的。

不要慌张

现在我们已经检查了 示例 4-3,让我们试一试:

$ `sudo kldload ./race_mtx.ko`
Race driver loaded.
$ `sudo ./race_config -a & sudo ./race_config -a &`
[1] 923
[2] 924
$ unit: 0
unit: 1

...

$ `sudo kldload ./race_mtx.ko`
Race driver loaded.
$ `sudo ./race_config -a & sudo kldunload race_mtx.ko &`
[1] 933
[2] 934
$ Race driver unloaded.
race_config: open(/dev/race): No such file or directory

[1]-  Exit 1                  sudo ./race_config -a
[2]+  Done                    sudo kldunload race_mtx.ko

毫不意外,它确实有效。然而,使用互斥锁引入了新的问题。看,race_new 函数的定义中包含这一行:

sc = (struct race_softc *)malloc(sizeof(struct race_softc), M_RACE,
           M_WAITOK | M_ZERO);

在这里, M_WAITOK 表示可以睡眠。但是,在持有互斥锁时睡眠是绝对不允许的。回想一下,在持有互斥锁时睡眠会导致内核崩溃。

解决这个问题的有两个方案:首先,将 M_WAITOK 改为 M_NOWAIT。其次,使用可以在睡眠时持有的锁。由于第一个方案会改变 示例 4-1 的功能(即,目前 race_new 从不失败),所以我们选择第二个方案。

共享/独占锁

共享/独占锁(sx 锁) 是线程在睡眠时可以持有的锁。正如其名所示,多个线程可以共享持有 sx 锁,但只有一个线程可以独占持有 sx 锁。当一个线程独占持有 sx 锁时,其他线程不能共享持有该锁。

sx 锁不支持优先级传播,与互斥锁相比效率较低。使用 sx 锁的主要原因是可以让线程在持有锁的同时睡眠。

共享/独占锁管理例程

FreeBSD 内核提供了以下 14 个函数用于处理 sx 锁:

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/sx.h>

void
sx_init(struct sx sx, const char description);

void
sx_init_flags(struct sx sx, const char description, int opts);

void
sx_slock(struct sx sx);

void
sx_xlock(struct sx sx);

int
sx_slock_sig(struct sx sx);

int
sx_xlock_sig(struct sx sx);

int
sx_try_slock(struct sx sx);

int
sx_try_xlock(struct sx sx);

void
sx_sunlock(struct sx sx);

void
sx_xunlock(struct sx sx);

void
sx_unlock(struct sx sx);

int
sx_try_upgrade(struct sx sx);

void
sx_downgrade(struct sx sx);

void
sx_destroy(struct sx sx);

sx_init 函数初始化 sx 锁 sx![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137501.png) description 参数在调试期间用于识别 sx

sx_init_flags 函数是 sx_init 的替代方案。![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137503.png) 选项参数修改了 sx_init_flags 的行为。opts 的有效值显示在 表 4-2 中。

表 4-2. sx_init_flags 符号常量

常量 描述
SX_NOADAPTIVE 如果传递了此位,并且内核未编译 NO_ADAPTIVE_SX 选项,则持有 sx 的线程将自旋而不是睡眠。
SX_RECURSE 指定 sx 是递归锁
SX_QUIET 指示系统不要对此锁的操作进行日志记录
SX_NOWITNESS 导致 witness(4) 忽略此锁
SX_DUPOK 导致 witness(4) 忽略此锁的重复项
SX_NOPROFILE 指示系统不要对此锁进行性能分析

线程通过调用 sx_slock 获取对 sx 的共享持有。如果另一个线程当前持有 sx 的独占持有,调用者将睡眠直到 sx 可用。

线程通过调用 sx_xlock 获取对 sx 的独占持有。如果有任何线程当前持有 sx 的共享或独占持有,调用者将睡眠直到 sx 可用。

sx_slock_sigsx_xlock_sig 函数与 sx_slocksx_xlock 相同,不同之处在于当调用者睡眠时,它可以被信号唤醒。如果发生这种情况,将返回非零值。

注意

通常,在锁上睡眠的线程不能被提前唤醒。

sx_try_slocksx_try_xlock 函数与 sx_slocksx_xlock 相同,不同之处在于如果无法获取 sx,它们将返回 0(即调用者不会睡眠)。

线程通过调用 sx_sunlock 释放对 sx 的共享持有,通过调用 sx_xunlock 释放独占持有。

sx_unlock 函数是 sx_sunlocksx_xunlock 的前端。当对 sx 的持有状态未知时,使用此函数。

线程可以通过调用 sx_try_upgrade 将共享持有升级为独占持有。如果持有不能立即升级,则返回 0。线程可以通过调用 sx_downgrade 将独占持有降级为共享持有。

sx_destroy 函数销毁 sx 锁 sx_lock sx。请注意,在销毁时不能持有 sx

实现共享/独占锁

示例 4-4 是 示例 4-3 的修订版,它使用 sx 锁而不是互斥锁。

注意

为了节省空间,race_ioctlrace_newrace_findrace_destroy 函数没有列出,因为它们没有改变。

示例 4-4. race_sx.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/malloc.h>
  #include <sys/ioccom.h>
  #include <sys/queue.h>
  #include <sys/lock.h>
 #include <sys/sx.h>
  #include "race_ioctl.h"

  MALLOC_DEFINE(M_RACE, "race", "race object");

  struct race_softc {
          LIST_ENTRY(race_softc) list;
          int unit;
  };

  static LIST_HEAD(, race_softc) race_list = LIST_HEAD_INITIALIZER(&race_list);
 static struct sx race_sx;

  static struct race_softc *      race_new(void);
  static struct race_softc *      race_find(int unit);
  static void                     race_destroy(struct race_softc *sc);
  static d_ioctl_t                race_ioctl_sx;
  static d_ioctl_t                race_ioctl;

  static struct cdevsw race_cdevsw = {
          .d_version =    D_VERSION,
          .d_ioctl =      race_ioctl_sx,
          .d_name =       RACE_NAME
  };

    static struct cdev *race_dev;

  static int
  race_ioctl_sx(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
      struct thread *td)
  {
          int error;

        sx_xlock(&race_sx);
          error = race_ioctl(dev, cmd, data, fflag, td);
        sx_xunlock(&race_sx);

          return (error);
  }

  static int
  race_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
      struct thread *td)
  {
  ...
  }

  static struct race_softc *
  race_new(void)
  {
  ...
  }

  static struct race_softc *
  race_find(int unit)
  {
  ...
  }

  static void
  race_destroy(struct race_softc *sc)
  {
  ...
  }

  static int
  race_modevent(module_t mod __unused, int event, void *arg __unused)
  {
          int error = 0;
          struct race_softc *sc, *sc_temp;

          switch (event) {
          case MOD_LOAD:
                sx_init(&race_sx, "race config lock");
                  race_dev = make_dev(&race_cdevsw, 0, UID_ROOT, GID_WHEEL,
                      0600, RACE_NAME);
                  uprintf("Race driver loaded.\n");
                  break;
            case MOD_UNLOAD:
                  destroy_dev(race_dev);
                sx_xlock(&race_sx);
                  if (!LIST_EMPTY(&race_list)) {
                          LIST_FOREACH_SAFE(sc, &race_list, list, sc_temp) {
                                  LIST_REMOVE(sc, list);
                                  free(sc, M_RACE);
                          }
                  }

                sx_xunlock(&race_sx);
                sx_destroy(&race_sx);
                  uprintf("Race driver unloaded.\n");
                  break;
          case MOD_QUIESCE:
                sx_xlock(&race_sx);
                  if (!LIST_EMPTY(&race_list))
                          error = EBUSY;
                sx_xunlock(&race_sx);
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  DEV_MODULE(race, race_modevent, NULL);

示例 4-4 与 示例 4-3 相同,除了每个互斥管理函数都被其 sx 锁等价物替换。

注意

示例 4-4 中的编号球突出了差异。

这里是与 示例 4-4 交互的结果:

$ `sudo kldload ./race_sx.ko`
Race driver loaded.
$ `sudo ./race_config -a & sudo ./race_config -a &`
[1] 800
[2] 801
$ unit: 0
unit: 1

...

$ `sudo kldload ./race_sx.ko`
Race driver loaded.
$ `sudo ./race_config -a & sudo kldunload race_sx.ko &`
[1] 811
[2] 812
$ unit: 0
kldunload: can't unload file: Device busy

[1]-  Done                    sudo ./race_config -a
[2]+  Exit 1                  sudo kldunload race_sx.ko

自然,一切正常,没有引入新的问题。

读写锁

读写锁(rw 锁)基本上是具有 sx 锁语义的互斥锁。像 sx 锁一样,线程可以以 读者 的身份持有 rw 锁,这等同于共享持有,或者以 写入者 的身份持有,这等同于独占持有。像互斥锁一样,rw 锁支持优先级传播,并且线程在睡眠时不能持有它们(否则内核会崩溃)。

当你需要保护一个主要将被读取而不是写入的对象时,使用 rw 锁。

读写锁管理例程

FreeBSD 内核提供了以下 11 个函数用于处理 rw 锁:

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/rwlock.h>

void
rw_init(struct rwlock *rw, const char *name);

void
rw_init_flags(struct rwlock *rw, const char *name, int opts);

void
rw_rlock(struct rwlock *rw);

void
rw_wlock(struct rwlock *rw);

int
rw_try_rlock(struct rwlock *rw);

int
rw_try_wlock(struct rwlock *rw);

void
rw_runlock(struct rwlock *rw);

void
rw_wunlock(struct rwlock *rw);

int
rw_try_upgrade(struct rwlock *rw);

void
rw_downgrade(struct rwlock *rw);

void
rw_destroy(struct rwlock *rw);

rw_init 函数初始化 rw 锁 rw_lock rw。在调试期间,name 参数用于识别 rw

rw_init_flags 函数是 rw_init 的替代方案。rw_init_flags opts 参数修改 rw_init_flags 的行为。opts 的有效值显示在 表 4-3 中。

表 4-3. rw_init_flags 符号常量

常量 描述
RW_RECURSE 指定 rw 是一个递归锁
RW_QUIET 指示系统不要记录对这个锁的操作
RW_NOWITNESS 导致 witness(4) 忽略这个锁
RW_DUPOK 导致 witness(4) 忽略这个锁的重复项
RW_NOPROFILE 指示系统不要对这个锁进行性能分析

线程通过调用 rw_rlock 获取对 rw 的共享持有。如果另一个线程当前对 rw 持有独占持有,调用者将睡眠直到 rw 可用。

线程通过调用 rw_wlock 获取对 rw 的独占持有。如果有任何线程当前对 rw 持有共享或独占持有,调用者将睡眠直到 rw 可用。

rw_try_rlockrw_try_wlock 函数与 rw_rlockrw_wlock 相同,不同之处在于如果无法获取 rw,它们将返回 0(即调用者不会睡眠)。

线程通过调用 rw_runlock 释放对 rw 的共享持有,并通过调用 rw_wunlock 释放独占持有。

线程可以通过调用 rw_try_upgrade 将共享持有升级为独占持有。如果无法立即升级,则返回 0。线程可以通过调用 rw_downgrade 将独占持有降级为共享持有。

rw_destroy 函数销毁 rw rw。请注意,在销毁时不能持有 rw

到目前为止,你应该对锁感到舒适——它们实际上没有什么。因此,我将省略使用 rw 锁的示例。

条件变量

条件变量 根据对象值同步两个或更多线程的执行。相比之下,锁通过控制对对象的访问来同步线程。

条件变量与锁结合使用以“阻塞”线程,直到条件为真。其工作方式如下:一个线程首先获取 foo 锁。然后检查条件。如果条件为假,它将在 bar 条件变量上睡眠。在 bar 上睡眠时,线程会放弃 foo。使条件为真的线程会唤醒在 bar 上睡眠的线程。以这种方式唤醒的线程在继续之前会重新获取 foo

条件变量管理例程

FreeBSD 内核提供了以下 11 个函数用于处理条件变量:

#include <sys/param.h>
#include <sys/proc.h>
#include <sys/condvar.h>

void
cv_init(struct cv *cvp, const char *d);

const char *
cv_wmesg(struct cv *cvp);

void
cv_wait(struct cv *cvp, lock);

void
cv_wait_unlock(struct cv *cvp, lock);

int
cv_wait_sig(struct cv *cvp, lock);

int
cv_timedwait(struct cv *cvp, lock, int timo);

int
cv_timedwait_sig(struct cv *cvp, lock, int timo);

void
cv_signal(struct cv *cvp);

void
cv_broadcast(struct cv *cvp);

void
cv_broadcastpri(struct cv *cvp, int pri);

void
cv_destroy(struct cv *cvp);

cv_init 函数初始化条件变量 cvp d 参数描述 cvp

cv_wmesg 函数获取 cvp 描述。此函数主要用于错误报告。

线程通过调用 cv_wait cvp 上睡眠。 lock 参数要求一个睡眠互斥锁,sx lockrw 锁。线程在调用 cv_wait 之前必须持有锁。线程不得在持有 lock 的情况下递归地睡眠在 cvp 上。

cv_wait_unlock 函数是 cv_wait 的一个变体。当线程从在 cvp 上睡眠中唤醒时,它们会放弃重新获取 lock

cv_wait_sig 函数与 cv_wait 函数相同,除了当调用者处于睡眠状态时,它可以被信号唤醒。如果发生这种情况,则返回错误代码 EINTRERESTART

注意

通常,在条件变量上睡眠的线程不能被提前唤醒。

cv_timedwait 函数与 cv_wait 函数相同,除了调用者最多睡眠 timo / hz 秒。如果睡眠超时,则返回错误代码 EWOULDBLOCK

cv_timedwait_sig 函数类似于 cv_wait_sigcv_timedwait。调用者可以通过信号唤醒,并最多睡眠 timo / hz 秒。

线程通过调用 cv_signal 唤醒在 cvp 上睡眠的一个线程,并通过调用 cv_broadcast 唤醒在 cvp 上睡眠的所有线程。

cv_broadcastpri 函数与 cv_broadcast 函数相同,除了唤醒的所有线程的优先级都提升到 pri。优先级高于 pri 的线程的优先级不会降低。

cv_destroy 函数销毁条件变量 cvp

注意

我们将在 第五章 中通过一个使用条件变量的示例进行说明。

通用指南

这里是一些关于锁使用的通用指南。请注意,这些并不是铁的规则,只是需要记住的事情。

避免在独占锁上递归

当获取独占持有或锁时,持有者通常假设它对锁保护的对象具有独占访问权。不幸的是,递归锁在某些情况下可能会破坏这个假设。例如,假设函数 F1 使用递归锁 L 来保护对象 O。如果函数 F2 获取 L,修改 O,使其处于不一致的状态,然后调用 F1F1 将递归获取 L 并错误地假设 O 处于一致状态.^([7])

解决这个问题的方法之一是使用非递归锁,并重写 F1 以确保它不会获取 L。相反,必须在调用 F1 之前获取 L

避免长时间持有独占锁

独占锁减少了并发性,应尽快释放。请注意,当不需要锁时,持有锁的时间越短越好,而不是仅仅释放它然后再重新获取(Baldwin,2002)。这是因为获取和释放锁的操作相对昂贵。


^([7]) 这段内容改编自 John H. Baldwin 的《Multithreaded FreeBSD Kernel 中的锁定》(2002 年)

结论

本章讨论了由并发线程引起的数据和状态损坏问题。简而言之,每当一个对象可以被多个线程访问时,其访问必须得到管理。

第五章。延迟执行

无标题图片

通常,驱动程序需要延迟它们的执行,以便给它们的设备、内核或用户提供时间来完成某些任务。在本章中,我将详细介绍可用于实现这些延迟的不同函数。在这个过程中,我还会描述异步代码执行。

自愿上下文切换,或休眠

自愿上下文切换,或休眠,发生在驱动线程必须等待资源可用或事件到达时;例如,驱动线程在从输入设备(如终端)请求数据后应该休眠(McKusick 和 Neville-Neil,2005)。驱动线程通过调用 *sleep 函数来休眠。

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/proc.h>

int
tsleep(void *chan, int priority, const char *wmesg, int timo);

void
wakeup(void *chan);

void
wakeup_one(void *chan);

void
pause(const char *wmesg, int timo);

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/mutex.h>

int
mtx_sleep(void *chan, struct mtx *mtx, int priority, const char *wmesg,
    int timo);

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/proc.h>

int
msleep_spin(void *chan, struct mtx *mtx, const char *wmesg, int timo);

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/sx.h>

int
sx_sleep(void *chan, struct sx *sx, int priority, const char *wmesg,
    int timo);

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/rwlock.h>

int
rw_sleep(void *chan, struct rwlock *rw, int priority, const char *wmesg,
    int timo);

通过调用 tsleep,线程可以自愿地进行上下文切换(或休眠)。tsleep 的参数与其他 *sleep 函数的参数相同,将在接下来的几段中描述。

chan 参数是通道(即,一个任意地址),它唯一标识线程正在等待的事件。

priority 参数是线程恢复时的优先级。如果 priority0,则使用当前线程的优先级。如果 PCATCHpriority 进行 OR 操作,则在睡眠前后检查信号。

wmesg 参数期望对休眠线程的简洁描述。此描述将由用户模式实用程序(如 ps(1))显示,并且对性能没有实际影响。

timo 参数指定睡眠超时。如果 timo 不为零,则线程将休眠最多 timo / hz 秒。之后,tsleep 返回错误代码 EWOULDBLOCK

wakeup 函数唤醒通道 chan 上所有休眠的线程。一般来说,从睡眠中唤醒的线程应该重新评估它们睡眠的条件。

wakeup_one 函数是 wakeup 的一个变体,它只唤醒在通道上找到的第一个休眠线程。假设当唤醒的线程完成时,它调用 wakeup_one 来唤醒另一个在 chan 上休眠的线程;这种 wakeup_one 调用的连续发生,直到 chan 上所有休眠的线程都被唤醒(McKusick 和 Neville-Neil,2005)。这减少了在 chan 上有多个线程休眠但只有一个线程可以执行有意义操作的情况下的负载。

pause 函数使调用线程休眠 timo / hz 秒。此线程不能被 wakeupwakeup_one 或信号唤醒。

剩余的 *sleep 函数——mtx_sleepmsleep_spinsx_sleeprw_sleep——是 tsleep 的变体,它们接受特定的锁。在线程休眠之前释放此锁,在线程唤醒之前重新获取此锁;如果 PDROPpriority 进行 OR 操作,则不会重新获取此锁。

注意,msleep_spin 函数没有 priority 参数。因此,它不能分配新的线程优先级,通过 PCATCH 捕获信号,或通过 PDROP 释放自旋互斥锁。

实现睡眠和条件变量

示例 5-1(基于 John Baldwin 编写的代码)是一个用于演示睡眠和条件变量的 KLD。它通过从 sysctl 获取“事件”来工作;然后,每个事件都被传递给一个线程,该线程根据接收到的事件执行特定的任务。

注意

快速浏览一下这段代码,并尝试理解其结构。如果你不理解所有内容,不要担心;解释将随后提供。

示例 5-1. sleep.c

#define INVARIANTS
  #define INVARIANT_SUPPORT

  #include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/kthread.h>
  #include <sys/proc.h>
  #include <sys/sched.h>
  #include <sys/unistd.h>
  #include <sys/lock.h>
  #include <sys/mutex.h>
  #include <sys/condvar.h>
  #include <sys/sysctl.h>

 #define MAX_EVENT 1

 static struct proc *kthread;
 static int event;
 static struct cv event_cv;
 static struct mtx event_mtx;

  static struct sysctl_ctx_list clist;
  static struct sysctl_oid *poid;

  static void
 sleep_thread(void *arg)
  {
          int ev;

          for (;;) {
                  mtx_lock(&event_mtx);
                  while ((ev = event) == 0)
                          cv_wait(&event_cv, &event_mtx);
                  event = 0;
                  mtx_unlock(&event_mtx);

                  switch (ev) {
                  case −1:
                          kproc_exit(0);
                          break;
                  case 0:
                          break;
                  case 1:
                          printf("sleep... is alive and well.\n");
                          break;
                  default:
                          panic("event %d is bogus\n", event);
                  }
          }
  }

  static int
 sysctl_debug_sleep_test(SYSCTL_HANDLER_ARGS)
  {
          int error, i = 0;

          error = sysctl_handle_int(oidp, &i, 0, req);
          if (error == 0 && req->newptr != NULL) {
                  if (i >= 1 && i <= MAX_EVENT) {
                          mtx_lock(&event_mtx);
                          KASSERT(event == 0, ("event %d was unhandled",
                              event));
                          event = i;
                          cv_signal(&event_cv);
                          mtx_unlock(&event_mtx);
                  } else
                          error = EINVAL;
          }

          return (error);
  }

  static int
 load(void *arg)
  {
          int error;
          struct proc *p;
          struct thread *td;

          error = kproc_create(sleep_thread, NULL, &p, RFSTOPPED, 0, "sleep");
          if (error)
                  return (error);

          event = 0;
          mtx_init(&event_mtx, "sleep event", NULL, MTX_DEF);
          cv_init(&event_cv, "sleep");

          td = FIRST_THREAD_IN_PROC(p);
          thread_lock(td);
          TD_SET_CAN_RUN(td);
          sched_add(td, SRQ_BORING);
          thread_unlock(td);
          kthread = p;

          sysctl_ctx_init(&clist);
          poid = SYSCTL_ADD_NODE(&clist, SYSCTL_STATIC_CHILDREN(_debug),
              OID_AUTO, "sleep", CTLFLAG_RD, 0, "sleep tree");
          SYSCTL_ADD_PROC(&clist, SYSCTL_CHILDREN(poid), OID_AUTO, "test",
              CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_debug_sleep_test, "I",
              "");

          return (0);
  }

  static int
 unload(void *arg)
  {
          sysctl_ctx_free(&clist);
          mtx_lock(&event_mtx);
          event = −1;
          cv_signal(&event_cv);
          mtx_sleep(kthread, &event_mtx, PWAIT, "sleep", 0);
          mtx_unlock(&event_mtx);
          mtx_destroy(&event_mtx);
          cv_destroy(&event_cv);

          return (0);
  }

  static int
 sleep_modevent(module_t mod __unused, int event, void *arg)
  {
          int error = 0;

          switch (event) {
          case MOD_LOAD:
                  error = load(arg);
                  break;
          case MOD_UNLOAD:
                  error = unload(arg);
                  break;
          default:
                  error = EOPNOTSUPP;
                  break;
          }

          return (error);
  }

  static moduledata_t sleep_mod = {
          "sleep",
          sleep_modevent,
          NULL
  };

  DECLARE_MODULE(sleep, sleep_mod, SI_SUB_SMP, SI_ORDER_ANY);

在 示例 5-1 的开头附近,定义了一个名为 MAX_EVENT 的常量,其值为 1,并声明了一个名为 kthreadstruct proc 指针。现在,忽略这两个对象;我稍后会讨论它们。

接下来,有两个变量声明:一个名为 event 的整数和一个名为 event_cv 的条件变量。这些变量用于同步 示例 5-1 的线程。显然, event_mtx 互斥锁用于保护 event

剩余的部分—— sleep_thread, sysctl_debug_sleep_test, load, unload, 和 sleep_modevent——需要更深入的说明,因此它们将在各自的章节中描述。

为了使事情更容易理解,我将按照它们执行的顺序而不是它们出现的顺序来描述上述部分。因此,我将从 示例 5-1 的模块事件处理器开始。

sleep_modevent 函数

sleep_modevent 函数是 示例 5-1 的模块事件处理器。以下是它的函数定义(再次):

static int
sleep_modevent(module_t mod __unused, int event, void *arg)
{

        int error = 0;

        switch (event) {
        case MOD_LOAD:
                error = load(arg);
                break;
        case MOD_UNLOAD:
                error = unload(arg);
                break;
        default:
                error = EOPNOTSUPP;
                break;
        }

        return (error);
}

在模块加载时,此函数简单地调用 加载函数。在模块卸载时,它调用 unload 函数。

加载函数

load 函数初始化这个 KLD。以下是它的函数定义(再次):

static int
load(void *arg)
{
        int error;
        struct proc *p;
        struct thread *td;

        error = kproc_create(sleep_thread, NULL, &p,
 RFSTOPPED, 0,
            "sleep");
        if (error)
                return (error);

      event = 0;
        mtx_init(&event_mtx, "sleep event", NULL, MTX_DEF);
        cv_init(&event_cv, "sleep");

        td = FIRST_THREAD_IN_PROC(p);
        thread_lock(td);
        TD_SET_CAN_RUN(td);
      sched_add(td, SRQ_BORING);
        thread_unlock(td);
      kthread = p;

        sysctl_ctx_init(&clist);
        poid = SYSCTL_ADD_NODE(&clist, SYSCTL_STATIC_CHILDREN(_debug),
            OID_AUTO, "sleep", CTLFLAG_RD, 0, "sleep tree");
        SYSCTL_ADD_PROC(&clist, SYSCTL_CHILDREN(poid), OID_AUTO, "test",
            CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_debug_sleep_test, "I",
            "");

        return (0);
}

这个函数可以分为四个部分。第一部分 创建一个内核进程来执行  函数。这个进程的句柄被保存在  中。常量 将进程置于停止状态。第二部分初始化 、 和  变量。第三部分 安排新进程执行 。它还将在  中保存进程句柄

注意

进程在线程粒度上执行,这就是为什么这段代码以线程为中心。

第四部分创建一个名为 debug.sleep.test 的 sysctl,它使用名为  的处理函数

sleep_thread 函数

sleep_thread 函数从 sysctl_debug_sleep_test 函数接收事件。然后,它根据接收到的事件执行特定的任务。以下是它的函数定义(再次):

static void
sleep_thread(void *arg)
{
        int ev;

      for (;;) {
              mtx_lock(&event_mtx);
              while ((ev = event) == 0)
                      cv_wait(&event_cv, &event_mtx);
              event = 0;
              mtx_unlock(&event_mtx);

              switch (ev) {
              case −1:
                      kproc_exit(0);
                        break;
                case 0:
                        break;
                case 1:
                        printf("sleep... is alive and well.\n");
                        break;
                default:
                        panic("event %d is bogus\n", event);
                }
        }
}

如您所见,sleep_thread 的执行被包含在一个 永远循环 中。这个循环首先 获取 。接下来,将 event 的值 保存到  中。如果 event 等于 0sleep_thread 在  上等待。注意,只有当 sleep_thread 尚未收到事件时,event 才是 0。如果已收到事件,sleep_thread 将事件设置为 0 以防止重新处理它。接下来,event_mtx 被释放。最后,接收到的事件通过一个  语句 进行处理。注意,如果接收到的事件是 sleep_thread 通过  自终止

sysctl_debug_sleep_test 函数

sysctl_debug_sleep_test 函数从 sysctl debug.sleep.test 获取事件。然后,它将这些事件传递给 sleep_thread 函数。

static int
sysctl_debug_sleep_test(SYSCTL_HANDLER_ARGS)
{
        int error, i = 0;

        error = sysctl_handle_int(oidp, &i, 0, req);
      if (error == 0 && req->newptr != NULL) {
              if (i >= 1 && i <= MAX_EVENT) {
                      mtx_lock(&event_mtx);
                      KASSERT(event == 0, ("event %d was unhandled",
                            event));
                      event = i;
                      cv_signal(&event_cv);
                        mtx_unlock(&event_mtx);
                } else
                        error = EINVAL;
        }

        return (error);
}

这个函数首先通过 debug.sleep.test 获取一个事件并将其存储在 i 中。接下来的 if 语句确保事件已成功获取。接下来,对 i 执行 范围检查。如果 i 在允许的范围内,则获取 event_mtx 并查询 event 以确保它等于 0。

注意

如果 event 不等于 0,则表示出了严重错误。如果启用了 INVARIANTS,则内核会崩溃。

最后,event 被设置为 isleep_thread 被解除阻塞以处理它。

卸载函数

unload 函数关闭这个 KLD。以下是它的函数定义(再次):

static int
unload(void *arg)
{
      sysctl_ctx_free(&clist);
        mtx_lock(&event_mtx);
      event = −1;
      cv_signal(&event_cv);
      mtx_sleep(kthread, &event_mtx, PWAIT, "sleep", 0);
        mtx_unlock(&event_mtx);
      mtx_destroy(&event_mtx);
      cv_destroy(&event_cv);

        return (0);
}

这个函数首先通过 拆除 sysctl debug.sleep.test。之后,event 被设置为 -1sleep_thread 被解除阻塞以处理它。

回想一下,如果事件是 -1,则 sleep_thread 通过 kproc_exit 自行终止。注意,kproc_exit 在返回之前在其调用者的进程句柄上执行 wakeup。这就是为什么 unload kthread 通道上睡眠的原因,因为它包含 sleep_thread 的进程句柄。

注意

回想一下,loadkthread 中保存了 sleep_thread 的进程句柄。

由于 unload 在 (at ) 睡觉直到 sleep_thread 退出,它不能在它们仍在使用时销毁 event_mtx event_cv

不要慌张

这里是加载和卸载 示例 5-1 的结果:

$ `sudo kldload ./sleep.ko`
$ `sudo sysctl debug.sleep.test=1`
debug.sleep.test: 0 -> 0
$ `dmesg | tail -n 1`
sleep... is alive and well.
$ `sudo kldunload ./sleep.ko`
$

自然,它工作得很好。现在,让我们看看其他延迟执行的方法。

内核事件处理器

事件处理器 允许驱动程序注册一个或多个函数,当事件发生时调用这些函数。例如,在停止系统之前,所有注册到事件处理器 shutdown_final 的函数都会被调用。表 5-1 描述了所有可用的事件处理器。

表 5-1. 内核事件处理器

事件处理器 描述
acpi_sleep_event 当系统被发送到睡眠状态时调用已注册的函数。
acpi_wakeup_event 当系统唤醒时调用已注册的函数。
dev_clone 当在 /dev 下的请求项不存在时,注册的函数将被调用;换句话说,这些函数将在需要时创建设备节点。
ifaddr_event 当在网络上设置地址时,注册的函数将被调用。
if_clone_event 当网络接口被克隆时,注册的函数将被调用。
ifnet_arrival_event 当出现新的网络接口时,注册的函数将被调用。
ifnet_departure_event 当网络接口被移除时,注册的函数将被调用。
power_profile_change 当系统电源配置文件更改时,注册的函数将被调用。
process_exec 当进程执行 exec 操作时,注册的函数将被调用。
process_exit 当进程退出时,注册的函数将被调用。
process_fork 当进程进行分叉时,注册的函数将被调用。
shutdown_pre_sync 在任何文件系统同步之前,系统关闭时,注册的函数将被调用。
shutdown_post_sync 在所有文件系统同步后,系统关闭时,注册的函数将被调用。
shutdown_final 在系统停止之前,注册的函数将被调用。
vm_lowmem 当虚拟内存不足时,注册的函数将被调用。
watchdog_list 当看门狗定时器被重新初始化时,注册的函数将被调用。

FreeBSD 内核提供了以下三个宏来处理事件处理器:

#include <sys/eventhandler.h>

 eventhandler_tag
EVENTHANDLER_REGISTER(name, func, arg, priority);

EVENTHANDLER_DEREGISTER(name, tag);

EVENTHANDLER_INVOKE(name, ...);

EVENTHANDLER_REGISTER 宏将函数 func 与事件处理器 name 注册。如果成功,将返回一个 eventhandler_tag。当 func 被调用时,arg 将是其第一个参数。使用 name 注册的函数将按照 priority 的顺序被调用。priority 的值可以从 0(最高优先级)到 20000(最低优先级)。

注意

通常,我使用常量 EVENTHANDLER_PRI_ANY,其值为 10000,作为 priority

EVENTHANDLER_DEREGISTER 宏用于从事件处理器 name 中删除与 tag 相关的函数(其中 tag 是一个 eventhandler_tag)。

EVENTHANDLER_INVOKE 宏执行与事件处理器 name 注册的所有函数。请注意,您永远不会调用 EVENTHANDLER_INVOKE,因为每个事件处理器都有专门的线程来执行这项任务。

注意

我们将在 第六章 中通过一个示例来讲解如何使用事件处理器。

旁注

调用 允许驱动程序在指定的时间(或定期间隔)后异步执行一个函数。这些函数被称为 调用函数

FreeBSD 内核提供了以下七个函数用于处理调用:

#include <sys/types.h>
#include <sys/systm.h>

typedef void timeout_t (void *);

void
callout_init(struct callout *c, int mpsafe);

void
callout_init_mtx(struct callout *c, struct mtx *mtx, int flags);

void
callout_init_rw(struct callout *c, struct rwlock *rw, int flags);

int
callout_stop(struct callout *c);

int
callout_drain(struct callout *c);

int
callout_reset(struct callout *c, int ticks, timeout_t *func,
    void *arg);

int
callout_schedule(struct callout *c, int ticks);

callout_init 函数初始化 callout 结构 图片 c图片 mpsafe 参数表示调用函数是否是“多处理器安全的”。此参数的有效值显示在 表 5-2 中。

表 5-2. callout_init 符号常量

常量 描述
0 调用函数不是 多处理器安全的;在执行调用函数之前获取 Giant 锁,在调用函数返回后释放。
CALLOUT_MPSAFE 调用函数是多处理器安全的;换句话说,竞态条件由调用函数本身处理。

注意

在这里,Giant 由调用子系统获取和释放。Giant 主要保护旧代码,不应由现代代码使用。

callout_init_mtx 函数是 callout_init 的替代方案。在执行调用函数之前,会获取 mutex 图片 mtx,并在调用函数返回后释放它(mtx 由调用子系统获取和释放)。在 callout_init_mtx 返回后,mtxcallout 结构 c 及其调用函数相关联。

图片 flags 参数修改 callout_init_mtx 的行为。表 5-3 显示了其唯一的有效值。

表 5-3. callout_init_mtx 符号常量

常量 描述
CALLOUT_RETURNUNLOCKED 表示调用函数将自行释放 mtx;换句话说,在调用函数返回后不会释放 mtx,而是在函数执行期间释放。

callout_init_rw 函数是 callout_init 的替代方案。在执行调用函数之前,作为写者,会获取 rw 锁 图片 rw,并在调用函数返回后释放它(rw 由调用子系统获取和释放)。在 callout_init_rw 返回后,rwcallout 结构 c 及其调用函数相关联。

图片 flags 参数修改 callout_init_rw 的行为。表 5-4 显示了其唯一的有效值。

表 5-4. callout_init_rw 符号常量

常量 描述
CALLOUT_SHAREDLOCK 导致 rw 以读取者的身份获取

callout_stop 函数取消当前挂起的调用函数。如果成功,返回非零值。如果返回 0,则调用函数目前正在执行或已经完成执行。

注意

在调用 callout_stop 之前,你必须独占持有你试图停止的调用函数相关的锁。

callout_drain 函数与 callout_stop 相同,除了如果调用函数目前正在执行,它将等待调用函数完成后再返回。如果你试图停止的调用函数需要一个锁,并且你在调用 callout_drain 时独占持有该锁,则会导致死锁。

callout_reset 函数安排函数 func ticks / hz 秒后执行一次;对于 ticks 的负值转换为 1。当 func 被调用时, arg 将是其第一个也是唯一的参数。在 callout_reset 返回后,func 是调用结构 c 的调用函数。

callout_reset 函数也可以将挂起的调用函数重新安排在新的时间执行。

注意

在调用 callout_reset 之前,你必须独占持有你试图建立或重新安排的调用或调用函数相关的锁。

callout_schedule 函数将挂起的调用函数重新安排在新的时间执行。这个函数仅仅是 callout_reset 的便利包装器。

注意

在调用 callout_schedule 之前,你必须独占持有你试图重新安排的调用函数相关的锁。

调用和竞争条件

因为调用函数是异步执行的,所以有可能在另一个线程尝试停止或重新安排它时调用调用函数;从而创建竞争条件。幸运的是,有两个简单的解决方案可以解决这个问题:

使用 callout_init_mtxcallout_init_rw callout_init(foo, 0)

与锁关联的调用函数免于上述提到的竞争条件——只要在调用调用管理函数之前持有相关的锁。

使用 callout_drain 永久取消调用函数

使用 callout_drain 而不是 callout_stop 来永久取消调用函数。注意,通过等待调用函数完成,你无法销毁它可能需要的任何对象。

注意

我们将在第六章中通过一个使用调用的示例来讲解。

任务队列

任务队列 允许驱动程序在稍后时间异步执行一个或多个函数。这些函数被称为 任务。任务队列主要用于延迟工作。

注意

任务队列类似于回调,但你不能指定执行函数的时间。

任务队列通过将任务排队在其上工作。这些任务会间歇性地执行。

全局任务队列

FreeBSD 运行并维护四个全局任务队列:

taskqueue_swi

taskqueue_swi 任务队列在中断上下文中执行其任务。中断处理程序通常将计算密集型工作推迟到这个任务队列。这个任务队列让中断处理程序更快完成,从而减少了中断禁用的时间。中断处理程序在第八章中详细讨论。

taskqueue_swi_giant

taskqueue_swi_giant 任务队列与 taskqueue_swi 相同,只是在执行任务之前会获取 Giant 锁。现代代码应避免使用此任务队列。

taskqueue_thread

taskqueue_thread 任务队列是通用任务队列。它在内核线程的上下文中执行其任务(与驱动程序执行的上下文相同)。当你有代码需要在没有线程上下文的情况下执行(例如中断处理程序)且需要执行需要线程上下文的代码时,可以使用此任务队列。

taskqueue_fast

taskqueue_fast 任务队列与 taskqueue_thread 相同,只是在执行任务之前会获取自旋锁。当你的任务不能睡眠时,使用此任务队列。

任务队列管理例程

FreeBSD 内核提供了以下宏和函数来处理任务队列:

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/queue.h>
#include <sys/taskqueue.h>

typedef void (*task_fn_t)(void *context, int pending);

struct task {
        STAILQ_ENTRY(task)      ta_link;        /* Link for queue. */
        u_short               ta_pending;     /* # of times queued. */
        u_short                 ta_priority;    /* Task priority. */
        task_fn_t               ta_func;        /* Task handler function. */
        void                    *ta_context;    /* Argument for handler. */
};

TASK_INIT(struct task *task, int priority, task_fn_t *func,
    void *context);

int
taskqueue_enqueue(struct taskqueue *queue, struct task *task);

void
taskqueue_run(struct taskqueue *queue);

void
taskqueue_drain(struct taskqueue *queue, struct task *task);

TASK_INIT 宏初始化 task 结构!taskpriority 参数是 task 在任务队列中的位置。func 参数是要执行的函数(一次)。当 func 被调用时,context 将是其第一个参数,而 ta_pending 的值将是其第二个。

taskqueue_enqueue 函数将!task 放在任务队列!queue 中,位于具有较低 priority 值的第一个 task 结构之前。如果 taskqueue_enqueue 被调用再次将 task 放在 queue 上,taskta_pending 值会增加——不会在 queue 上放置 task 的另一个副本。

taskqueue_run 函数按照任务的 优先级 值的顺序执行任务队列 队列 中的每个任务。每个任务完成后,其 task 结构从 队列 中移除。然后将其 ta_pending 值置零,并在其 task 结构上调用 wakeup。请注意,你永远不会调用 taskqueue_run,因为每个任务队列都有线程专门用于执行这项任务。

taskqueue_drain 函数等待 任务完成,该任务位于 队列 中。

注意

我们将在第六章中通过一个使用任务队列的示例进行说明。

结论

本章介绍了四种不同的延迟执行方法:

休眠 当你必须等待某些事情发生才能继续时,会进行休眠。
事件处理器 事件处理器允许你注册一个或多个函数,以便在事件发生时执行。
调用 调用允许你执行异步代码执行。调用用于在特定时间执行你的函数。
任务队列 任务队列也允许你执行异步代码。任务队列用于延迟工作。

第六章。案例研究:虚拟 Null Modem

无标题图片

本章是几个案例研究中的第一个,这些案例研究将引导您通过一个真实的设备驱动程序。这些案例研究的目的让您接触到真正的驱动程序代码——包括所有缺点——并巩固前面章节中呈现的信息。

在本章中,我们将遍历 nmdm(4),这是一个虚拟 null modem 终端驱动程序。此驱动程序创建两个通过虚拟 null modem 电缆连接的 tty(4) 设备。换句话说,一个 tty(4) 设备的输出是另一个 tty(4) 设备的输入,反之亦然。我选择分析 nmdm(4),因为它使用了事件处理器、回调和任务队列,这些都在 第五章 中描述过,但未进行演示。

先决条件

在我可以向您介绍 nmdm(4) 之前,您需要理解以下函数:

#include <sys/tty.h>

struct tty *
tty_alloc_mutex(struct ttydevsw *tsw, void *softc, struct mtx *mtx);

void
tty_makedev(struct tty *tp, struct ucred *cred, const char *fmt, ...);

void *
tty_softc(struct tty *tp);

tty_alloc_mutex 函数创建一个 TTY 设备。tsw 参数期望一个指向 TTY 设备切换表的指针,这类似于字符设备切换表,但用于 TTY 设备。softc 参数是 TTY 设备的软件上下文(或实例变量)。mtx 参数指定将保护 TTY 设备的互斥锁。

注意

在不久的将来,tty_alloc_mutex 函数将被弃用并删除。

tty_makedev 函数在 /dev 下创建一个 TTY 设备节点。tp 参数期望一个指向 TTY 设备的指针(例如,tty_alloc_mutex 的返回值)。cred 参数是设备节点的凭证。如果 credNULL,则使用 UID_ROOTGID_WHEELfmt 参数指定设备节点的名称。

tty_softc 函数返回 TTY 设备 tp 的软件上下文。

代码分析

示例 6-1 提供了 nmdm(4) 的简洁、源代码级别的概述。

示例 6-1. nmdm.c

#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>

#include <sys/tty.h>
#include <sys/conf.h>
#include <sys/eventhandler.h>
#include <sys/limits.h>
#include <sys/serial.h>
#include <sys/malloc.h>
#include <sys/queue.h>
#include <sys/taskqueue.h>
#include <sys/lock.h>
#include <sys/mutex.h>

MALLOC_DEFINE(M_NMDM, "nullmodem", "nullmodem data structures");

struct nmdm_part {
        struct tty              *np_tty;
        struct nmdm_part        *np_other;
        struct task             np_task;
        struct callout          np_callout;
        int                     np_dcd;
        int                     np_rate;
        u_long                  np_quota;
        int                     np_credits;
        u_long                  np_accumulator;

#define QS 8                    /* Quota shift. */
};

struct nmdm_softc {
        struct nmdm_part        ns_partA;
        struct nmdm_part        ns_partB;
        struct mtx              ns_mtx;
};

static tsw_outwakeup_t          nmdm_outwakeup;
static tsw_inwakeup_t           nmdm_inwakeup;
static tsw_param_t              nmdm_param;
static tsw_modem_t              nmdm_modem;

static struct ttydevsw nmdm_class = {
        .tsw_flags =            TF_NOPREFIX,
        .tsw_outwakeup =        nmdm_outwakeup,
        .tsw_inwakeup =         nmdm_inwakeup,
        .tsw_param =            nmdm_param,
        .tsw_modem =            nmdm_modem
};

static int nmdm_count = 0;

static void
nmdm_timeout(void *arg)
{
...
}

static void
nmdm_task_tty(void *arg, int pending __unused)
{
...
}

static struct nmdm_softc *
nmdm_alloc(unsigned long unit)
{
...
}

static void
nmdm_clone(void *arg, struct ucred *cred, char *name, int len,
    struct cdev **dev)
{
...
}

static void
nmdm_outwakeup(struct tty *tp)
{
...
}

static void
nmdm_inwakeup(struct tty *tp)
{
...
}

static int
bits_per_char(struct termios *t)
{
...
}

static int
nmdm_param(struct tty *tp, struct termios *t)
{
...
}

static int
nmdm_modem(struct tty *tp, int sigon, int sigoff)
{
...
}

static int
nmdm_modevent(module_t mod __unused, int event, void *arg __unused)
{
...
}

DEV_MODULE(nmdm, nmdm_modevent, NULL);

示例 6-1 提供作为方便;当我遍历 nmdm(4) 的代码时,您可以参考它以查看 nmdm(4) 的函数和结构是如何布局的。

为了使事情更容易理解,我将按照我本会编写的顺序详细说明 nmdm(4) 中的函数和结构(而不是它们出现的顺序)。为此,我们将从模块事件处理器开始。

nmdm_modevent 函数

nmdm_modevent 函数是 nmdm(4) 的模块事件处理器。以下是它的函数定义:

static int
nmdm_modevent(module_t mod __unused, int event, void *arg __unused)
{
        static eventhandler_tag tag;

        switch (event) {
        case MOD_LOAD:
                tag = EVENTHANDLER_REGISTER(dev_clone,
 nmdm_clone, 0,
                    1000);
                if (tag == NULL)
                        return (ENOMEM);
                break;
        case MOD_SHUTDOWN:
                break;
        case MOD_UNLOAD:
              if (nmdm_count != 0)
                       return (EBUSY);
              EVENTHANDLER_DEREGISTER(dev_clone, tag);
                break;
        default:
                return (EOPNOTSUPP);
        }

        return (0);

}

在模块加载时,此函数 httpatomoreillycomsourcenostarchimages1137499.png 将函数 httpatomoreillycomsourcenostarchimages1137503.png nmdm_clone 注册到事件处理器 httpatomoreillycomsourcenostarchimages1137501.png dev_clone

注意

dev_clone 事件处理程序在 表 5-1 中描述,见 Don’t Panic。

回想一下,当 /dev 下的请求项不存在时,会调用注册到 dev_clone 的函数。因此,当第一次访问 nmdm(4) 设备节点时,会调用 nmdm_clone 动态创建设备节点。有趣的是,这种动态设备创建允许创建无限数量的 nmdm(4) 设备节点。

在模块卸载时,此函数首先检查 nmdm_count 的值。

注意

变量 nmdm_count 在 示例 6-1 的开头附近声明,为一个初始化为 0 的整数。

nmdm_count 计算活动 nmdm(4) 设备节点的数量。如果它等于 0,则 nmdm_clone 从事件处理程序 dev_clone 中移除;否则,返回 EBUSY(代表 错误:设备繁忙)。

nmdm_clone 函数

如前节所述,nmdm_clone 会动态创建 nmdm(4) 设备节点。请注意,所有 nmdm(4) 设备节点都是以成对的形式创建的,名称为 nmdm%lu%c,其中 %lu 是单元号,%cAB。以下是 nmdm_clone 函数的定义:

static void
nmdm_clone(void *arg, struct ucred *cred, char *name, int len,
    struct cdev **dev)
{
        unsigned long unit;
        char *end;
        struct nmdm_softc *ns;

      if (*dev != NULL)
                return;
      if (strncmp(name, "nmdm", 4) != 0)
                return;

        /* Device name must be "nmdm%lu%c", where %c is "A" or "B". */
        name += 4;
        unit = strtoul(name, &end, 10);
      if (unit == ULONG_MAX || name == end)
                return;
      if ((end[0] != 'A' && end[0] != 'B') || end[1] != '\0')
                return;

        ns = nmdm_alloc(unit);

        if (end[0] == 'A')
              *dev = ns->ns_partA.np_tty->t_dev;
        else
              *dev = ns->ns_partB.np_tty->t_dev;
}

此函数首先检查 *dev(字符设备指针)的值。如果 *dev 不等于 NULL,这意味着已经存在设备节点,则 nmdm_clone 退出(因为没有节点需要创建)。接下来,nmdm_clone 确保 name 中的前四个字符等于 nmdm;否则退出(因为请求的设备节点是为另一个驱动程序)。然后,name 中的第五个字符,应该是单元号,被转换为无符号长整型并存储在 unit 中。接下来的 if 语句检查转换是否成功。之后,nmdm_clone 确保 name 中的单元号之后是字母 AB;否则退出。现在,确认请求的设备节点确实是为此驱动程序后,调用 nmdm_alloc 实际创建设备节点。最后,将 *dev 设置为请求的设备节点(要么是 nmdm%luA,要么是 nmdm%luB)。

注意,由于nmdm_clone已与dev_clone注册,其函数原型必须符合dev_clone所期望的类型,dev_clone<sys/conf.h>中定义。

nmdm_alloc 函数

如前所述,nmdm_alloc实际上创建了nmdm(4)的设备节点。在描述此函数之前,需要先解释nmdm_class

注意

数据结构nmdm_class在示例 6-1 的开头被声明为一个 TTY 设备切换表。

static struct ttydevsw nmdm_class = {
        .tsw_flags =          TF_NOPREFIX,
        .tsw_outwakeup =        nmdm_outwakeup,
        .tsw_inwakeup =         nmdm_inwakeup,
        .tsw_param =            nmdm_param,
        .tsw_modem =            nmdm_modem

};

标志TF_NOPREFIX表示不要在设备名称前加 tty 前缀。其他定义是nmdm_class支持的运算。这些运算将在遇到时进行描述。

既然你已经熟悉了nmdm_class,让我们来了解一下nmdm_alloc

static struct nmdm_softc *
nmdm_alloc(unsigned long unit)
{
        struct nmdm_softc *ns;

      atomic_add_int(&nmdm_count, 1);

        ns = malloc(sizeof(*ns), M_NMDM, M_WAITOK | M_ZERO);
      mtx_init(&ns->ns_mtx, "nmdm", NULL, MTX_DEF);

        /* Connect the pairs together. */
      ns->ns_partA.np_other = &ns->ns_partB;
      TASK_INIT(&ns->ns_partA.np_task, 0, nmdm_task_tty, &ns->ns_partA);
      callout_init_mtx(&ns->ns_partA.np_callout, &ns->ns_mtx, 0);

      ns->ns_partB.np_other = &ns->ns_partA;
      TASK_INIT(&ns->ns_partB.np_task, 0, nmdm_task_tty, &ns->ns_partB);
      callout_init_mtx(&ns->ns_partB.np_callout, &ns->ns_mtx, 0);

        /* Create device nodes. */
        ns->ns_partA.np_tty = tty_alloc_mutex(&nmdm_class, &ns->ns_partA,
            &ns->ns_mtx);
        tty_makedev(ns->ns_partA.np_tty, NULL, "nmdm%luA", unit);

        ns->ns_partB.np_tty = tty_alloc_mutex(&nmdm_class, &ns->ns_partB,
            &ns->ns_mtx);
        tty_makedev(ns->ns_partB.np_tty, NULL, "nmdm%luB", unit);

        return (ns);

}

此函数可以分为四个部分。第一部分通过atomic_add_int函数将nmdm_count增加一个。正如其名称所暗示的,atomic_add_int是原子的。因此,在增加它时,我们不需要锁来保护nmdm_count

第二部分为新的nmdm_softc结构分配内存。之后,它的互斥锁被初始化。除了互斥锁外,nmdm_softc还包含两个额外的成员变量:ns_partAns_partB。这些变量是nmdm_part结构,并将维护与nmdm%luAnmdm%luB相关的数据。

注意

结构nmdm_softc在示例 6-1 的开头定义。

第三部分![http://atomoreilly.com/source/nostarch/images/1137511.png]将成员变量ns_partAns_partB连接起来,以便给定ns_partA可以找到ns_partB,反之亦然。第三部分还初始化了ns_partAns_partB![http://atomoreilly.com/source/nostarch/images/1137513.png]task![http://atomoreilly.com/source/nostarch/images/1137515.png]callout结构。

最后,第四部分创建了nmdm(4)的设备节点(即nmdm%luAnmdm%luB)。

nmdm_outwakeup 函数

nmdm_outwakeup函数在nmdm_class中定义为tsw_outwakeup操作。当nmdm%luAnmdm%luB有输出时执行。以下是它的函数定义:

static void
nmdm_outwakeup(struct tty *tp)
{
        struct nmdm_part *np = tty_softc(tp);

        /* We can transmit again, so wake up our side. */
      taskqueue_enqueue(taskqueue_swi, &np->np_task);
}

此函数ns_partAns_partBtask结构排队到taskqueue_swi(也就是说,它将nmdm%luAnmdm%luB的输出处理延迟)。

nmdm_task_tty 函数

nmdm_task_tty 函数将数据从 nmdm%luA 传输到 nmdm%luB,反之亦然。此函数由 nmdm_outwakeup (验证见 nmdm_allocTASK_INIT 的第三个参数)在 taskqueue_swi 上排队。以下是它的函数定义:

static void
nmdm_task_tty(void *arg, int pending __unused)
{
        struct tty *tp, *otp;
        struct nmdm_part *np = arg;
        char c;

        tp = np->np_tty;
        tty_lock(tp);

        otp = np->np_other->np_tty;
        KASSERT(otp != NULL, ("nmdm_task_tty: null otp"));
        KASSERT(otp != tp, ("nmdm_task_tty: otp == tp"));

      if (np->np_other->np_dcd) {
              if (!tty_opened(tp)) {
                      np->np_other->np_dcd = 0;
                      ttydisc_modem(otp, 0);
                }
      } else {
              if (tty_opened(tp)) {
                        np->np_other->np_dcd = 1;
                        ttydisc_modem(otp, 1);
                }
        }

        while (ttydisc_rint_poll(otp) > 0) {
                if (np->np_rate && !np->np_quota)
                        break;
                if (ttydisc_getc(tp, &c, 1) != 1)
                        break;
                np->np_quota--;
              ttydisc_rint(otp, c, 0);
        }
        ttydisc_rint_done(otp);

        tty_unlock(tp);
}

注意

在此函数的解释中,“我们的 TTY”指的是排队在 taskqueue_swi 上的 TTY 设备(即 nmdm%luAnmdm%luB)。

此函数由两部分组成。第一部分改变两个 TTY 之间的连接状态以匹配我们的 TTY 的状态。如果我们的 TTY 图片 已关闭且其他 TTY 的数据载波检测 (DCD) 标志 图片 已开启,我们 图片 关闭该标志并 图片 关闭它们的载波信号。另一方面,如果我们的 TTY 已 图片 打开且其他 TTY 的 DCD 标志 图片 已关闭,我们打开该标志并 图片 打开它们的载波信号。简而言之,这部分确保如果我们的 TTY 已关闭(即没有数据要传输),其他 TTY 将不会有载波信号,如果我们的 TTY 已打开(即有数据要传输),其他 TTY 将会有载波信号。载波信号表示连接。换句话说,载波丢失等同于连接终止。

第二部分将数据从我们的 TTY 输出队列传输到其他 TTY 的输入队列。这部分首先 图片 轮询其他 TTY 以确定它是否可以接收数据。然后从我们的 TTY 输出队列中移除一个字符 图片 并将其放置到其他 TTY 的输入队列中。这些步骤会重复进行,直到传输完成。

nmdm_inwakeup 函数

nmdm_inwakeup 函数在 nmdm_class 中定义为 tsw_inwakeup 操作。它在 nmdm%luAnmdm%luB 可以再次接收输入时被调用。也就是说,当 nmdm%luAnmdm%luB 的输入队列已满然后空间变得可用时,此函数被执行。以下是它的函数定义:

static void
nmdm_inwakeup(struct tty *tp)
{
        struct nmdm_part *np = tty_softc(tp);

        /* We can receive again, so wake up the other side. */
      taskqueue_enqueue(taskqueue_swi,
 &np->np_other->np_task);
}

注意

在此函数的解释中,“我们的 TTY”指的是执行此函数的 TTY 设备(即 nmdm%luAnmdm%luB)。

此函数 图片taskqueue_swi 上排队其他 TTY 的 图片 任务结构。换句话说,当我们的 TTY 可以再次接收输入时,我们的 TTY 告诉其他 TTY 将数据传输给它。

nmdm_modem 函数

nmdm_modem 函数在 nmdm_class 中定义为 tsw_modem 操作。此函数设置或获取调制解调器控制线状态。以下是它的函数定义:

static int
nmdm_modem(struct tty *tp, int sigon, int sigoff)
{
        struct nmdm_part *np = tty_softc(tp);
        int i = 0;

        /* Set modem control lines. */
      if (sigon || sigoff) {
              if (sigon & SER_DTR)
                       np->np_other->np_dcd = 1;
              if (sigoff & SER_DTR)
                       np->np_other->np_dcd = 0;

              ttydisc_modem(np->np_other->np_tty, np->np_other->np_dcd);

                return (0);
        /* Get state of modem control lines. */
        } else {
              if (np->np_dcd)
                       i |= SER_DCD;
              if (np->np_other->np_dcd)
                       i |= SER_DTR;

                return (i);
        }

}

注意

在这个函数的解释中,“我们的 TTY”指的是执行此函数的 TTY 设备(即 nmdm%luAnmdm%luB)。

sigon(信号开启)或 sigoff(信号关闭)参数不为零时,此函数设置调制解调器控制线。如果 sigon 包含数据终端就绪(DTR)标志,则另一个 TTY 的 DCD 标志 被打开。如果 sigoff 包含 DTR 标志,则另一个 TTY 的 DCD 标志 被关闭。另一个 TTY 的载波信号 将与其 DCD 标志一起打开或关闭。

如果前面的讨论对你来说没有意义,这应该会帮助:一个 null 调制解调器将每个串行端口的 DTR 输出连接到另一个串行端口的 DCD 输入。DTR 输出保持关闭,直到程序访问串行端口并将其打开;另一个串行端口将此感知为其 DCD 输入打开。因此,DCD 输入用于检测另一方的就绪状态。这就是为什么当我们的 TTY 的 DTR 被 sigon’d 或 sigoff’d 时,另一个 TTY 的 DCD 标志和载波信号也会打开或关闭。

此函数在 sigon 和 sigoff 为 0 时获取调制解调器控制线状态。如果我们的 TTY 的 DCD 标志 亮起,则返回 SER_DCD。如果另一个 TTY 的 DCD 标志 亮起,表示我们的 TTY 的 DTR 标志亮起,则返回 SER_DTR

nmdm_param 函数

nmdm_param 函数在 nmdm_class 中定义为 tsw_param 操作。此函数设置 nmdm_task_tty 以定期执行。也就是说,它将 nmdm%luA 设置为定期向 nmdm%luB 传输数据,反之亦然。这种定期数据传输需要流量控制以防止一方数据溢出另一方。流量控制通过在接收方跟不上的情况下停止发送方来实现。

这里是 nmdm_param 函数的定义:

static int
nmdm_param(struct tty *tp, struct termios *t)
{
        struct nmdm_part *np = tty_softc(tp);
        struct tty *otp;
        int bpc, rate, speed, i;

        otp = np->np_other->np_tty;

      if (!((t->c_cflag | otp->t_termios.c_cflag) & CDSR_OFLOW)) {
                np->np_rate = 0;
                np->np_other->np_rate = 0;
                return (0);
        }

      bpc = imax(bits_per_char(t), bits_per_char(&otp->t_termios));

        for (i = 0; i < 2; i++) {
                /* Use the slower of their transmit or our receive rate. */
              speed = imin(otp->t_termios.c_ospeed, t->c_ispeed);
                if (speed == 0) {
                        np->np_rate = 0;
                        np->np_other->np_rate = 0;
                        return (0);
                }

                speed <<= QS;                  /* bits per second, scaled. */
                speed /= bpc;                  /* char per second, scaled. */
                rate = (hz << QS) / speed;     /* hz per callout. */
                if (rate == 0)
                        rate = 1;

                speed *= rate;
                speed /= hz;                   /* (char/sec)/tick, scaled. */

              np->np_credits = speed;
                np->np_rate = rate;
                callout_reset(&np->np_callout, rate,
 nmdm_timeout, np);

                /* Swap pointers for second pass--to update the other end. */
                np = np->np_other;
                t = &otp->t_termios;
                otp = tp;
        }

        return (0);
}

此函数可以分为三个部分。第一个部分 确定是否禁用了流量控制。如果是,则将 ns_partAns_partBnp_rate 变量置零,并退出 nmdm_paramnp_rate 变量是 nmdm_task_tty 将被执行的速率。这个速率对于 nmdm%luAnmdm%luB 可能不同。

第二部分计算 np_rate图片 值。此计算考虑了 nmdm%luAnmdm%luB图片 速度以及 图片 每个字符的位数。第二部分还确定每次执行 nmdm_task_tty 时可传输的最大字符数。

最后,第三部分导致在 rate / hz 秒后执行一次 nmdm_timeout,如 图片 所示。nmdm_timeout 函数将 nmdm_task_ttytaskqueue_swi 上排队。

第二和第三部分各执行两次,一次为 nmdm%luA,一次为 nmdm%luB

nmdm_timeout 函数

如前所述,nmdm_timeout 函数以固定间隔在 taskqueue_swi 上排队 nmdm_task_tty。以下是其函数定义:

static void
nmdm_timeout(void *arg)
{
        struct nmdm_part *np = arg;

      if (np->np_rate == 0)
                return;

        /*
         * Do a simple Floyd-Steinberg dither to avoid FP math.
         * Wipe out unused quota from last tick.
         */
        np->np_accumulator += np->np_credits;
        np->np_quota = np->np_accumulator >> QS;
        np->np_accumulator &= ((1 << QS) - 1);

      taskqueue_enqueue(taskqueue_swi, &np->np_task);
      callout_reset(&np->np_callout, np->np_rate,
 nmdm_timeout, np);
}

此函数首先 图片 检查 np_rate 的值。如果它等于 0,则 nmdm_timeout 退出。接下来,ns_partAns_partBnp_quota 变量被分配 图片 最大字符传输数(如果你回到 nmdm_task_tty 函数 在 nmdm_outwakeup 函数,应该很明显 np_quota 是如何使用的)。一旦完成,nmdm_task_tty图片图片 taskqueue_swi 上排队,图片 nmdm_timeout图片 重新安排在 图片 np_rate / hz 秒后执行。

nmdm_paramnmdm_timeout 函数用于模拟 TTY 的波特率。没有这两个函数,数据传输会变慢。

bits_per_char 函数

bits_per_char 函数返回用于表示单个字符的位数,对于给定的 TTY。此函数仅在 nmdm_param 中使用。以下是其函数定义:

static int
bits_per_char(struct termios *t)
{
        int bits;

      bits = 1;               /* start bit. */
      switch (t->c_cflag & CSIZE) {
        case CS5:
                bits += 5;
                break;
        case CS6:
                bits += 6;
                break;
        case CS7:
                bits += 7;
                break;
        case CS8:
                bits += 8;
                break;
        }
      bits++;                 /* stop bit. */
      if (t->c_cflag & PARENB)
                bits++;
      if (t->c_cflag & CSTOPB)
                bits++;

        return (bits);
}

注意,图片 返回值考虑了 图片 变量字符大小、图片 起始位、图片 停止位、图片 奇偶校验启用位和 图片 第二停止位。

不要慌张

既然我们已经走过了 nmdm(4),让我们试一试:

$ `sudo kldload ./nmdm.ko`
$ `sudo /usr/libexec/getty std.9600 nmdm0A &`
[1] 936
$ `sudo cu -l /dev/nmdm0B`
Connected

FreeBSD/i386 (wintermute.phub.net.cable.rogers.com) (nmdm0A)
login:

极好。我们能够从nmdm0B连接到正在运行getty(8)nmdm0A

结论

本章描述了虚拟空调制解调器驱动程序nmdm(4)的整个代码库。如果你注意到这个驱动程序中完全缺乏锁定并且感到担忧,请不要担心。在nmdm_alloc中初始化的ns_mtx互斥锁,在调用nmdm_outwakeupnmdm_inwakeupnmdm_modemnmdm_param之前,被 TTY 子系统隐式获取。简而言之,nmdm%luAnmdm%luB之间的每个操作都是串行化的。

第七章:新型总线与资源分配

无标题图片

到目前为止,我们只检查了伪设备,它们为编写驱动程序提供了极好的介绍。然而,大多数驱动程序需要与真实硬件交互。本章将向您展示如何编写这样的驱动程序。

我将首先介绍 新型总线,这是 FreeBSD 用于管理系统硬件设备的底层架构(McKusick 和 Neville-Neil,2005)。然后,我将描述新型总线驱动程序的基本知识,并在本章结束时讨论硬件资源分配。

自动配置与新型总线驱动程序

自动配置是 FreeBSD 执行的过程,以启用机器上的硬件设备(McKusick 和 Neville-Neil,2005)。它通过系统地探测机器的 I/O 总线以识别其子设备来实现。对于每个识别的设备,都会分配一个适当的新型总线驱动程序来配置和初始化它。请注意,设备可能无法识别或不受支持。因此,不会分配任何新型总线驱动程序。

新型总线驱动程序是 FreeBSD 中任何控制绑定到 I/O 总线的设备(即,大致上不是伪设备驱动程序的每个驱动程序)的驱动程序。

通常,所有新型总线驱动程序都包含以下三个组件:

  • device_foo 函数

  • 设备方法表

  • DRIVER_MODULE 宏调用

device_foo 函数

device_foo 函数基本上是新型总线驱动程序在自动配置期间执行的操作。表 7-1 简要介绍了每个device_foo函数。

表 7-1. device_foo 函数

函数 描述
device_identify 将新设备添加到 I/O 总线
device_probe 检测特定设备
device_attach 连接到设备
device_detach 从设备断开连接
device_shutdown 关闭设备
device_suspend 请求设备挂起
device_resume 已发生恢复

device_identify 函数向 I/O 总线添加一个新的设备(实例)。此函数仅用于无法直接识别其子设备的总线。回想一下,自动配置首先通过识别每个 I/O 总线上的子设备开始。现代总线可以直接识别连接到它们的设备。较老的总线,如 ISA,必须使用它们相关驱动程序提供的device_identify例程来识别其子设备(McKusick 和 Neville-Neil,2005)。你将很快学习如何将驱动程序与 I/O 总线关联起来。

所有识别的子设备都传递给每个新型总线驱动程序的device_probe函数。device_probe函数告诉内核其驱动程序是否可以处理识别的设备。

注意,可能有多个驱动程序可以处理已识别的子设备。因此,device_probe的返回值用于指定其驱动程序与已识别设备的匹配程度。返回最高值的device_probe函数表示最适合已识别设备的最佳 Newbus 驱动程序。以下是从<sys/bus.h>中摘录的用于表示成功(即匹配)的常量:

#define BUS_PROBE_SPECIFIC      0       /* Only I can use this device. */
#define BUS_PROBE_VENDOR        (-10)   /* Vendor-supplied driver. */
#define BUS_PROBE_DEFAULT       (-20)   /* Base OS default driver. */
#define BUS_PROBE_LOW_PRIORITY  (-40)   /* Older, less desirable driver. */
#define BUS_PROBE_GENERIC       (-100)  /* Generic driver for device. */
#define BUS_PROBE_HOOVER        (-500)  /* Driver for all devices on bus. */
#define BUS_PROBE_NOWILDCARD    (-2000000000) /* No wildcard matches. */

如您所见,成功代码是小于或等于零的值。标准的 UNIX 错误代码(即正值)用作失败代码。

一旦找到处理设备的最佳驱动程序,就会调用其device_attach函数。device_attach函数初始化设备及其任何必要软件(例如,设备节点)。

device_detach函数将驱动程序从设备断开连接。此函数应将设备设置为合理状态,并释放在device_attach期间分配的任何资源。

当系统关闭、其设备挂起或其设备从挂起状态恢复时,分别调用 Newbus 驱动的device_shutdowndevice_suspenddevice_resume函数。这些函数允许驱动程序在发生这些事件时管理其设备。

设备方法表

设备方法表,device_method_t,指定了 Newbus 驱动程序实现了哪些device_foo函数。它在<sys/bus.h>头文件中定义。

以下是一个虚构 PCI 设备的示例设备方法表:

static device_method_t foo_pci_methods[] = {
        /* Device interface. */
        DEVMETHOD(device_probe,         foo_pci_probe),
        DEVMETHOD(device_attach,        foo_pci_attach),
        DEVMETHOD(device_detach,        foo_pci_detach),
        { 0, 0 }
};

如您所见,并非每个device_foo函数都必须定义。如果一个device_foo函数未定义,则相应的操作不受支持。

毫不奇怪,每个 Newbus 驱动程序都必须定义device_probedevice_attach函数。对于旧总线的驱动程序,还必须定义device_identify函数。

DRIVER_MODULE 宏

DRIVER_MODULE宏将 Newbus 驱动程序注册到系统中。此宏在<sys/bus.h>头文件中定义。以下是它的函数原型:

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/bus.h>
#include <sys/module.h>

DRIVER_MODULE(name, busname, driver_t driver, devclass_t devclass,
    modeventhand_t evh, void *arg);

此宏期望的参数如下。

name

name参数用于识别驱动程序。

busname

busname参数指定了驱动程序的 I/O 总线(例如,isapciusb等)。

驱动程序

driver参数期望一个填充好的driver_t结构。此参数最好通过示例来理解:

static driver_t foo_pci_driver = {
        "foo_pci",
        foo_pci_methods,
        sizeof(struct foo_pci_softc)
};

在这里,foo_pci "foo_pci"是本例驱动程序的官方名称,foo_pci_methods foo_pci_methods是其设备方法表,foo_pci_softc_sizeof sizeof(struct foo_pci_softc)是其软件上下文的大小。

devclass

devclass参数期望一个未初始化的devclass_t变量,内核将使用它进行内部记账。

evh

evh参数表示一个可选的模块事件处理器。通常,我们会始终将evh设置为0,因为DRIVER_MODULE提供了自己的模块事件处理器。

arg

arg 参数是 evh 指定的事件处理程序的 void * 参数。如果 evh 设置为 0,则 arg 也必须为 0

将一切联系在一起

你现在已经足够了解如何编写你的第一个 Newbus 驱动程序。示例 7-1 是一个简单的基于 Murray Stokely 编写的代码的虚构 PCI 设备的 Newbus 驱动程序。

注意

快速看一下这段代码,并尝试理解其结构。如果你不理解其中的所有内容,不要担心;解释将随后提供。

示例 7-1. foo_pci.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/bus.h>

  #include <dev/pci/pcireg.h>
  #include <dev/pci/pcivar.h>

 struct foo_pci_softc {
         device_t        device;
         struct cdev     *cdev;
  };

  static d_open_t         foo_pci_open;
  static d_close_t        foo_pci_close;
  static d_read_t         foo_pci_read;
  static d_write_t        foo_pci_write;

 static struct cdevsw foo_pci_cdevsw = {
          .d_version =    D_VERSION,
          .d_open =       foo_pci_open,
          .d_close =      foo_pci_close,
          .d_read =       foo_pci_read,
          .d_write =      foo_pci_write,
          .d_name =       "foo_pci"
  };

 static devclass_t foo_pci_devclass;

  static int
  foo_pci_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
  {
          struct foo_pci_softc *sc;

          sc = dev->si_drv1;
          device_printf(sc->device, "opened successfully\n");
          return (0);
  }

  static int
  foo_pci_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
  {
          struct foo_pci_softc *sc;

          sc = dev->si_drv1;
          device_printf(sc->device, "closed\n");
          return (0);
  }

  static int
  foo_pci_read(struct cdev *dev, struct uio *uio, int ioflag)
  {
          struct foo_pci_softc *sc;

          sc = dev->si_drv1;
          device_printf(sc->device, "read request = %dB\n", uio->uio_resid);
          return (0);
  }

  static int
  foo_pci_write(struct cdev *dev, struct uio *uio, int ioflag)
  {
          struct foo_pci_softc *sc;

          sc = dev->si_drv1;
          device_printf(sc->device, "write request = %dB\n", uio->uio_resid);
          return (0);
  }

  static struct _pcsid {
          uint32_t        type;
          const char      *desc;
  } pci_ids[] = {
          { 0x1234abcd, "RED PCI Widget" },
          { 0x4321fedc, "BLU PCI Widget" },
          { 0x00000000, NULL }
  };

  static int
  foo_pci_probe(device_t dev)
  {
          uint32_t type = pci_get_devid(dev);
          struct _pcsid *ep = pci_ids;

          while (ep->type && ep->type != type)
                  ep++;
          if (ep->desc) {
                  device_set_desc(dev, ep->desc);
                  return (BUS_PROBE_DEFAULT);
          }

          return (ENXIO);
  }

  static int
  foo_pci_attach(device_t dev)
  {
          struct foo_pci_softc *sc = device_get_softc(dev);
          int unit = device_get_unit(dev);

          sc->device = dev;
          sc->cdev = make_dev(&foo_pci_cdevsw, unit, UID_ROOT, GID_WHEEL,
              0600, "foo_pci%d", unit);
          sc->cdev->si_drv1 = sc;

          return (0);
  }

  static int
  foo_pci_detach(device_t dev)
  {
          struct foo_pci_softc *sc = device_get_softc(dev);

          destroy_dev(sc->cdev);
          return (0);
  }

  static device_method_t foo_pci_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_probe,         foo_pci_probe),
          DEVMETHOD(device_attach,        foo_pci_attach),
          DEVMETHOD(device_detach,        foo_pci_detach),
          { 0, 0 }
  };

  static driver_t foo_pci_driver = {
          "foo_pci",
          foo_pci_methods,
          sizeof(struct foo_pci_softc)
  };
 DRIVER_MODULE(foo_pci, pci, foo_pci_driver, foo_pci_devclass, 0, 0);

此驱动程序首先定义其 软件上下文,该上下文将维护对其设备和一个 cdev 的指针,该 cdev 是由 make_dev 调用返回的。

接下来,定义了其 字符设备切换表。此表包含四个名为 foo_pci_openfoo_pci_closefoo_pci_readfoo_pci_writed_foo 函数。我将在 d_foo 函数 中描述这些函数。

然后声明了一个 devclass_t 变量。该变量作为其 devclass 参数传递给 DRIVER_MODULE 宏。

最后,定义了 d_foodevice_foo 函数。这些函数将按照它们执行的顺序进行描述。

foo_pci_probe 函数

foo_pci_probe 函数是该驱动程序的 device_probe 实现。在我详细说明此函数之前,需要描述 pci_ids 数组(位于 示例 7-1) 的描述。

static struct _pcsid {
      uint32_t        type;
      const char      *desc;
} pci_ids[] = {
        { 0x1234abcd, "RED PCI Widget" },
        { 0x4321fedc, "BLU PCI Widget" },
        { 0x00000000, NULL }
};

此数组由三个 _pcsid 结构组成。每个 _pcsid 结构包含一个 PCI ID 和一个 PCI 设备的描述。正如你可能猜到的,pci_ids 列出了该设备支持的设备。

既然我已经描述了 pci_ids,让我们来看看 foo_pci_probe

static int
foo_pci_probe(device_t dev)
{
        uint32_t type = pci_get_devid(dev);
        struct _pcsid *ep = pci_ids;

      while (ep->type && ep->type != type)
                ep++;
        if (ep->desc) {
              device_set_desc(dev, ep->desc);
              return (BUS_PROBE_DEFAULT);
        }

        return (ENXIO);
}

在这里,图片 dev 描述了在 PCI 总线上找到的已识别设备。因此,此函数首先 图片 获取 dev 的 PCI ID。然后它 图片 确定是否将 dev 的 PCI ID 列在 图片 pci_ids 中。如果是,则将 dev 的详细描述 图片 设置,并返回成功代码 BUS_PROBE_DEFAULT 图片

注意

foo_pci_attach 执行时,详细描述将打印到系统控制台。

foo_pci_attach 函数

foo_pci_attach 函数是此驱动程序的 device_attach 实现。以下是它的函数定义(再次):

static int
foo_pci_attach(device_t dev)
{
        struct foo_pci_softc *sc = device_get_softc(dev);
        int unit = device_get_unit(dev);

        sc->device = dev;
        sc->cdev = make_dev(&foo_pci_cdevsw, unit, UID_ROOT, GID_WHEEL,
            0600, "foo_pci%d", unit);
        sc->cdev->si_drv1 = sc;

        return (0);
}

在这里,图片 dev 表示该驱动程序控制下的设备。因此,此函数首先获取 dev图片 软件上下文和 图片 单元号。然后创建一个字符设备节点,并将变量 sc->devicesc->cdev->si_drv1 分别设置为 图片 dev图片 sc

注意

下面的 d_foo 函数(将在后面描述)使用 sc->devicecdev->si_drv1 来访问 devsc

d_foo 函数

因为 示例 7-1 中的每个 d_foo 函数只是打印一条调试信息(也就是说,它们基本上都是相同的),所以我只将遍历其中一个:foo_pci_open

static int
foo_pci_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct foo_pci_softc *sc;

      sc = dev->si_drv1;
      device_printf(sc->device, "opened successfully\n");
        return (0);
}

在这里,图片 devfoo_pci_attach 中的 make_dev 调用返回的 cdev。因此,此函数首先 图片 获取其软件上下文。然后它 图片 打印一条调试信息。

foo_pci_detach 函数

foo_pci_detach 函数是此驱动程序的 device_detach 实现。以下是它的函数定义(再次):

static int
foo_pci_detach(device_t dev)
{
        struct foo_pci_softc *sc = device_get_softc(dev);

      destroy_dev(sc->cdev);
        return (0);
}

在这里,图片 dev 表示该驱动程序控制下的设备。因此,此函数只是简单地获取 dev图片 软件上下文来 图片 销毁其设备节点。

别慌

既然我们已经讨论了 示例 7-1,让我们试一试:

$ `sudo kldload ./foo_pci.ko`
$ `kldstat`
Id Refs Address    Size     Name
 1    3 0xc0400000 c9f490   kernel
 2    1 0xc3af0000 2000     foo_pci.ko
$ `ls -l /dev/foo*`
ls: /dev/foo*: No such file or directory

当然,它 图片 完全失败,因为 foo_pci_probe 正在探测虚构的 PCI 设备。在结束本章之前,还有一个额外的话题需要提及。

硬件资源管理

作为配置和操作设备的一部分,驱动程序可能需要管理硬件资源,例如中断请求线(IRQs)、I/O 端口或 I/O 内存(McKusick 和 Neville-Neil,2005)。自然,Newbus 包含了执行此操作所需的一些函数。

#include <sys/param.h>
#include <sys/bus.h>

#include <machine/bus.h>
#include <sys/rman.h>
#include <machine/resource.h>

struct resource *
bus_alloc_resource(device_t dev, int type, int *rid, u_long start,
    u_long end, u_long count, u_int flags);

struct resource *
bus_alloc_resource_any(device_t dev, int type, int *rid,
    u_int flags);

int
bus_activate_resource(device_t dev, int type, int rid,
    struct resource *r);

int
bus_deactivate_resource(device_t dev, int type, int rid,
    struct resource *r);

int
bus_release_resource(device_t dev, int type, int rid,
    struct resource *r);

bus_alloc_resource 函数为特定设备分配硬件资源。如果成功,则返回一个 struct resource 指针;否则,返回 NULL。此函数通常在 device_attach 期间调用。如果在 device_probe 期间调用,则在返回之前必须释放所有分配的资源(通过 bus_release_resource)。bus_alloc_resource 的大多数参数与其他硬件资源管理函数的参数相同。这些参数将在接下来的几段中描述。

dev 参数是需要拥有硬件资源(s)的设备。在分配之前,资源归父总线所有。

type 参数表示 dev 想要分配的资源类型。此参数的有效值列在 表 7-2 中。

表 7-2. 硬件资源符号常量

常量 描述
SYS_RES_IRQ 中断请求线
SYS_RES_IOPORT I/O 端口
SYS_RES_MEMORY I/O 内存

rid 参数期望一个资源 ID(RID)。如果 bus_alloc_resource 成功,则 rid 中返回的 RID 可能与您传递的 RID 不同。您将在稍后了解更多关于 RID 的信息。

startend 参数是硬件资源(s)的起始和结束地址。要使用默认总线值,只需将 start 传递为 0ul,将 end 传递为 ˜0ul

count 参数表示硬件资源的大小。如果你使用了 startend 的默认总线值,则只有在 count 大于默认总线值时才会使用 count

flags 参数详细说明了硬件资源的特征。此参数的有效值列在 表 7-3 中。

表 7-3. bus_alloc_resource 符号常量

常量 描述
RF_ALLOCATED 分配硬件资源,但不激活它
RF_ACTIVE 分配硬件资源并自动激活资源
RF_SHAREABLE 硬件资源允许同时共享;您应该始终设置此标志,除非资源不能共享
RF_TIMESHARE 硬件资源允许时分共享

bus_alloc_resource_any 函数是 bus_alloc_resource 的便利包装器,它将 startendcount 设置为其默认总线值。

bus_activate_resource 函数激活一个之前分配的硬件资源。自然,资源必须在使用之前被激活。大多数驱动程序只是简单地将RF_ACTIVE传递给bus_alloc_resourcebus_alloc_resource_any,以避免调用bus_activate_resource

bus_deactivate_resource 函数使一个硬件资源失效。这个函数主要用于总线驱动程序(因此我们永远不会调用它)。

bus_release_resource 函数释放了一个之前分配的硬件资源。当然,在释放时资源不能处于使用状态。如果成功,返回0;否则,内核会崩溃。

注意

我们将在第八章(第八章. 中断处理)和第九章(第九章. 案例研究:并行端口打印机驱动程序)中介绍使用中断的示例,我将在第十章(第十章. 管理和使用资源)和第十一章(第十一章. 案例研究:智能平台管理接口驱动程序)中介绍需要 I/O 端口和 I/O 内存的示例。

结论

本章向您介绍了 Newbus 驱动程序开发的基础——与真实硬件一起工作。本书的其余部分将在此基础上构建,以完成您对 Newbus 的理解。

第八章. 中断处理

无标题图片

硬件设备通常必须执行(或处理)外部事件,例如旋转磁盘盘片、卷带、等待 I/O 等。这些外部事件发生的时间框架通常比处理器的慢得多——也就是说,如果处理器等待这些事件的完成(或到达),它将空闲一段时间。为了避免浪费处理器的宝贵时间,使用了中断。中断简单地说是一个硬件设备在需要处理器注意时可以发送的信号(Corbet et al., 2005)。在大多数情况下,驱动程序只需要注册一个处理程序函数来处理其设备的中断。

注册中断处理程序

以下函数,在 <sys/bus.h> 中声明,用于注册或拆除中断处理程序:

#include <sys/param.h>
#include <sys/bus.h>

int
bus_setup_intr(device_t dev, struct resource *r, int flags,
    driver_filter_t filter, driver_intr_t ithread, void *arg,
    void **cookiep);

int
bus_teardown_intr(device_t dev, struct resource *r, void *cookiep);

bus_setup_intr 函数使用中断请求(IRQ)注册一个中断处理程序。此 IRQ 必须先通过 bus_alloc_resource 分配,如《Don’t Panic》中的硬件资源管理中所述。

bus_setup_intr 函数通常在 device_attach 期间被调用。此函数的参数将在接下来的几段中描述。

dev 参数是要处理中断的设备。此设备必须有一个中断请求(IRQ)。

r 参数要求从成功调用的 bus_alloc_resource 返回值中获取返回值,该调用为 dev 分配了一个中断请求。

flags 参数对中断处理程序和/或中断进行分类。此参数的有效值在 <sys/bus.h> 中的 intr_type 枚举中定义。表 8-1 描述了更常用的值。

表 8-1. bus_setup_intr 符号常量

常量 描述
INTR_MPSAFE 表示中断处理程序是多处理器安全的,并且不需要由 Giant 保护——也就是说,任何竞争条件都应该由中断处理程序本身处理;当代代码应始终传递此标志
INTR_ENTROPY 表示中断是一个良好的熵源,并且可能被熵设备 /dev/random 使用

filterithread 参数指定中断处理程序的过滤和 ithread 例程。现在,不要担心这些参数;我将在下一节中讨论它们。

arg 参数是传递给中断处理程序的唯一参数。通常,您会始终将 arg 设置为 dev 的软件上下文。

cookiep 参数期望一个指向 void * 的指针。如果 bus_setup_intr 成功,则在 cookiep 中返回一个 cookie;此 cookie 用于销毁中断处理程序。

如您所预期的那样,bus_teardown_intr 函数会拆除一个中断处理程序。

FreeBSD 中的中断处理程序

现在你已经知道了如何注册中断处理程序,让我们讨论中断处理程序是如何实现的。

在 FreeBSD 中,中断处理程序由一个过滤例程、一个 ithread 例程或两者组成。过滤例程 在主要中断上下文中执行(即,它没有自己的上下文)。因此,它不能阻塞或进行上下文切换,并且只能使用自旋互斥锁进行同步。由于这些限制,过滤例程通常只用于需要非抢占式中断处理程序的设备。

过滤例程可以完全处理中断,或者将计算密集型的工作推迟到其关联的 ithread 例程中,前提是它有一个。 表 8-2 详细说明了过滤例程可以返回的值。

表 8-2. 过滤例程返回值

常量 描述
FILTER_STRAY 表示过滤例程无法处理此中断;此值等同于错误代码。
FILTER_HANDLED 表示中断已被完全处理;此值等同于成功代码。
FILTER_SCHEDULE_THREAD 安排 ithread 例程执行;只有当过滤例程有一个关联的 ithread 例程时,才能返回此值。

与过滤例程不同,ithread 例程 在其自己的线程上下文中执行。你可以在 ithread 例程中做任何你想做的事情,除了自愿进行上下文切换(即休眠)或等待条件变量。因为过滤例程是非抢占式的,FreeBSD 中的大多数中断处理程序只是 ithread 例程。

实现中断处理程序

示例 8-1 是一个设计用来演示中断处理程序的新旧总线驱动程序。示例 8-1 在并行端口上设置了一个中断处理程序;在读取时,它会休眠直到接收到中断。

注意

快速看一下这段代码,并尝试辨别其结构。如果你不完全理解它,不要担心;解释将随后提供。

示例 8-1. pint.c

#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>

#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/bus.h>
#include <sys/malloc.h>

#include <machine/bus.h>
#include <sys/rman.h>
#include <machine/resource.h>

#include <dev/ppbus/ppbconf.h>
#include "ppbus_if.h"
#include <dev/ppbus/ppbio.h>

#define PINT_NAME               "pint"
#define BUFFER_SIZE             256

struct pint_data {
        int                     sc_irq_rid;
        struct resource        *sc_irq_resource;
        void                   *sc_irq_cookie;
        device_t                sc_device;
        struct cdev            *sc_cdev;
        short                   sc_state;
#define PINT_OPEN               0x01
        char                   *sc_buffer;
        int                     sc_length;
};

static d_open_t                 pint_open;
static d_close_t                pint_close;
static d_read_t                 pint_read;
static d_write_t                pint_write;

static struct cdevsw pint_cdevsw = {
        .d_version =            D_VERSION,
        .d_open =               pint_open,
        .d_close =              pint_close,
        .d_read =               pint_read,
        .d_write =              pint_write,
        .d_name =               PINT_NAME
};

static devclass_t pint_devclass;

static int
pint_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int error;

        ppb_lock(ppbus);

        if (sc->sc_state) {
                ppb_unlock(ppbus);
                return (EBUSY);
        } else
                sc->sc_state |= PINT_OPEN;

        error = ppb_request_bus(ppbus, pint_device, PPB_WAIT | PPB_INTR);
        if (error) {
                sc->sc_state = 0;
                ppb_unlock(ppbus);
                return (error);
        }

        ppb_wctr(ppbus, 0);
        ppb_wctr(ppbus, IRQENABLE);

        ppb_unlock(ppbus);
        return (0);
}

static int
pint_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);

        ppb_lock(ppbus);

        ppb_wctr(ppbus, 0);
        ppb_release_bus(ppbus, pint_device);
        sc->sc_state = 0;

        ppb_unlock(ppbus);
        return (0);
}

static int
pint_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        int amount, error = 0;

        amount = MIN(uio->uio_resid,
            (BUFFER_SIZE - 1 - uio->uio_offset > 0) ?
             BUFFER_SIZE - 1 - uio->uio_offset : 0);
        if (amount == 0)
                return (error);

        error = uiomove(sc->sc_buffer, amount, uio);
        if (error) {
                device_printf(pint_device, "write failed\n");
                return (error);
        }

        sc->sc_buffer[amount] = '\0';
        sc->sc_length = amount;

        return (error);
}

static int
pint_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int amount, error = 0;

        ppb_lock(ppbus);
        error = ppb_sleep(ppbus, pint_device, PPBPRI | PCATCH, PINT_NAME, 0);
        ppb_unlock(ppbus);
        if (error)
                return (error);

        amount = MIN(uio->uio_resid,
            (sc->sc_length - uio->uio_offset > 0) ?
             sc->sc_length - uio->uio_offset : 0);

        error = uiomove(sc->sc_buffer + uio->uio_offset, amount, uio);
        if (error)
                device_printf(pint_device, "read failed\n");

        return (error);
}

static void
pint_intr(void *arg)
{
        struct pint_data *sc = arg;
        device_t pint_device = sc->sc_device;

#ifdef INVARIANTS
        device_t ppbus = device_get_parent(pint_device);
        ppb_assert_locked(ppbus);
#endif

        wakeup(pint_device);
}

static void
pint_identify(driver_t *driver, device_t parent)
{
        device_t dev;

        dev = device_find_child(parent, PINT_NAME, −1);
        if (!dev)
                BUS_ADD_CHILD(parent, 0, PINT_NAME, −1);
}

static int
pint_probe(device_t dev)
{
        /* probe() is always OK. */
        device_set_desc(dev, "Interrupt Handler Example");

        return (BUS_PROBE_SPECIFIC);
}

static int
pint_attach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);
        int error, unit = device_get_unit(dev);

        /* Declare our interrupt handler. */
        sc->sc_irq_rid = 0;
        sc->sc_irq_resource = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->sc_irq_rid, RF_ACTIVE | RF_SHAREABLE);

        /* Interrupts are mandatory. */
        if (!sc->sc_irq_resource) {
                device_printf(dev,
                    "unable to allocate interrupt resource\n");
                return (ENXIO);
        }

        /* Register our interrupt handler. */
        error = bus_setup_intr(dev, sc->sc_irq_resource,
            INTR_TYPE_TTY | INTR_MPSAFE, NULL, pint_intr,
            sc, &sc->sc_irq_cookie);
        if (error) {
                bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
                    sc->sc_irq_resource);
                device_printf(dev, "unable to register interrupt handler\n");
                return (error);
        }

        sc->sc_buffer = malloc(BUFFER_SIZE, M_DEVBUF, M_WAITOK);

        sc->sc_device = dev;
        sc->sc_cdev = make_dev(&pint_cdevsw, unit, UID_ROOT, GID_WHEEL, 0600,
            PINT_NAME "%d", unit);
        sc->sc_cdev->si_drv1 = sc;

        return (0);
}

static int
pint_detach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);

        destroy_dev(sc->sc_cdev);

        bus_teardown_intr(dev, sc->sc_irq_resource, sc->sc_irq_cookie);
        bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
            sc->sc_irq_resource);

        free(sc->sc_buffer, M_DEVBUF);

        return (0);
}

static device_method_t pint_methods[] = {
        /* Device interface. */
        DEVMETHOD(device_identify,      pint_identify),
        DEVMETHOD(device_probe,         pint_probe),
        DEVMETHOD(device_attach,        pint_attach),
        DEVMETHOD(device_detach,        pint_detach),
        { 0, 0 }
};

static driver_t pint_driver = {
        PINT_NAME,
        pint_methods,
        sizeof(struct pint_data)
};

DRIVER_MODULE(pint, ppbus, pint_driver, pint_devclass, 0, 0);
MODULE_DEPEND(pint, ppbus, 1, 1, 1);

为了使事情更容易理解,我将按照它们编写的顺序描述 示例 8-1 中的函数,而不是它们出现的顺序。为此,我将从 pint_identify 函数开始。

pint_identify 函数

pint_identify 函数是这个驱动程序的 device_identify 实现方式。从逻辑上讲,这个函数是必需的,因为并行端口无法在没有辅助的情况下识别其子设备。

这里是 pint_identify 函数的定义(再次):

static void
pint_identify(driver_t *driver, device_t parent)
{
        device_t dev;

        dev = device_find_child(parent, PINT_NAME, −1);
        if (!dev)
                BUS_ADD_CHILD(parent, 0, PINT_NAME, −1);
}

此函数首先图片确定并行端口是否曾经识别了一个名为图片PINT_NAME的子设备。如果没有,那么pint_identify图片会将PINT_NAME添加到并行端口的已识别子设备列表中。

pint_probe 函数

pint_probe函数是此驱动程序的device_probe实现。以下是它的函数定义(再次):

static int
pint_probe(device_t dev)
{
        /* probe() is always OK. */
        device_set_desc(dev, "Interrupt Handler Example");

      return (BUS_PROBE_SPECIFIC);
}

如您所见,此函数始终图片返回成功代码BUS_PROBE_SPECIFIC,因此示例 8-1 会连接到它所探测到的每个设备。这看起来可能有些错误,但实际上是正确的行为,因为通过device_identify例程、使用BUS_ADD_CHILD识别的设备,只有具有相同名称的驱动程序才会进行探测。在这种情况下,识别的设备和驱动程序名称是PINT_NAME

pint_attach 函数

pint_attach函数是此驱动程序的device_attach实现。以下是它的函数定义(再次):

static int
pint_attach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);
        int error, unit = device_get_unit(dev);

        /* Declare our interrupt handler. */
        sc->sc_irq_rid = 0;
        sc->sc_irq_resource = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->sc_irq_rid, RF_ACTIVE | RF_SHAREABLE);

        /* Interrupts are mandatory. */
        if (!sc->sc_irq_resource) {
                device_printf(dev,
                    "unable to allocate interrupt resource\n");
              return (ENXIO);
        }

        /* Register our interrupt handler. */
        error = bus_setup_intr(dev, sc->sc_irq_resource,
            INTR_TYPE_TTY | INTR_MPSAFE, NULL, pint_intr,
            sc, &sc->sc_irq_cookie);
        if (error) {
                bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
                    sc->sc_irq_resource);
                device_printf(dev, "unable to register interrupt handler\n");
                return (error);
        }

        sc->sc_buffer = malloc(BUFFER_SIZE, M_DEVBUF, M_WAITOK);

      sc->sc_device = dev;
        sc->sc_cdev = make_dev(&pint_cdevsw, unit, UID_ROOT, GID_WHEEL,
            0600, PINT_NAME "%d", unit);
      sc->sc_cdev->si_drv1 = sc;

        return (0);
}

此函数首先图片分配一个 IRQ。如果失败,则返回错误代码ENXIO(代表错误:设备未配置)。接下来,图片pint_intr函数被图片设置为dev(在这种情况下,中断处理程序只是一个 ithread 例程)的中断处理程序。之后,分配一个BUFFER_SIZE字节的缓冲区。然后sc->sc_device图片设置为dev,示例 8-1 的字符设备节点图片被创建,并且软件上下文(sc)的指针图片被保存在sc->sc_cdev->si_drv1中。

pint_detach 函数

pint_detach函数是此驱动程序的device_detach实现。以下是它的函数定义(再次):

static int
pint_detach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);

      destroy_dev(sc->sc_cdev);

      bus_teardown_intr(dev, sc->sc_irq_resource, sc->sc_irq_cookie);
      bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
            sc->sc_irq_resource);

      free(sc->sc_buffer, M_DEVBUF);

        return (0);
}

此函数首先图片销毁示例 8-1 的设备节点。一旦完成,它图片拆除了dev的中断处理程序,图片释放了dev的 IRQ,并且图片释放了分配的内存。

pint_open 函数

pint_open函数定义在pint_cdevsw(即示例 8-1 的字符设备切换表中)作为d_open操作。回想一下,d_open操作准备设备进行 I/O。

这里是pint_open函数的定义(再次):

static int
pint_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int error;

      ppb_lock(ppbus);

      if (sc->sc_state) {
                ppb_unlock(ppbus);
              return (EBUSY);
        } else
              sc->sc_state |= PINT_OPEN;

        error = ppb_request_bus(ppbus, pint_device, PPB_WAIT | PPB_INTR);
        if (error) {
                sc->sc_state = 0;
                ppb_unlock(ppbus);
                return (error);
        }

      ppb_wctr(ppbus, 0);
      ppb_wctr(ppbus, IRQENABLE);

        ppb_unlock(ppbus);
        return (0);
}

这个函数首先获取并行端口的互斥锁。然后检查sc->sc_state的值。如果它不等于 0,这表示另一个进程已经打开了设备,将返回错误代码EBUSY;否则,pint_open“打开”设备。在这种情况下,打开设备意味着将sc->sc_state设置为PINT_OPEN。之后,调用ppb_request_bus函数将pint_device标记为并行端口的拥有者。自然地,pint_device是我们的设备(即,它从pint_attach指向 dev)。

注意

拥有并行端口允许设备在它之间传输数据。

最后,在启用中断之前,pint_open清除并行端口的控制寄存器。

pint_close 函数

pint_close函数在pint_cdevsw中被定义为d_close操作。以下是它的函数定义(再次):

static int
pint_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);

      ppb_lock(ppbus);

      ppb_wctr(ppbus, 0);
      ppb_release_bus(ppbus, pint_device);
      sc->sc_state = 0;

        ppb_unlock(ppbus);
        return (0);
}

这个函数首先获取并行端口的互斥锁。然后禁用并行端口上的中断(从所有目的来看,清除控制寄存器,这就是上面代码所做的事情,禁用中断)。接下来,调用ppb_release_bus函数来放弃对并行端口的控制权。最后,将sc->sc_state清零,以便另一个进程可以打开这个设备。

pint_write 函数

pint_write函数在pint_cdevsw中被定义为d_write操作。这个函数从用户空间获取一个字符字符串并将其存储。

这里是pint_write函数的定义(再次):

static int
pint_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        int amount, error = 0;

        amount = MIN(uio->uio_resid,
            (BUFFER_SIZE - 1 - uio->uio_offset > 0) ?
             BUFFER_SIZE - 1 - uio->uio_offset : 0);
        if (amount == 0)
                return (error);

        error = uiomove(sc->sc_buffer, amount, uio);
        if (error) {
                device_printf(pint_device, "write failed\n");
                return (error);
        }

        sc->sc_buffer[amount] = '\0';
        sc->sc_length = amount;

        return (error);
}

这个函数与在 echo_write 函数中描述的echo_write函数在本质上完全相同。因此,我这里不再重复介绍。

pint_read 函数

pint_read函数在pint_cdevsw中被定义为d_read操作。这个函数在进入时会睡眠。它也会将存储的字符字符串返回到用户空间。

这里是pint_read函数的定义(再次):

static int
pint_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int amount, error = 0;

      ppb_lock(ppbus);
        error = ppb_sleep(ppbus, pint_device, PPBPRI | PCATCH,
            PINT_NAME, 0);
        ppb_unlock(ppbus);
        if (error)
                return (error);

        amount = MIN(uio->uio_resid,
            (sc->sc_length - uio->uio_offset > 0) ?
             sc->sc_length - uio->uio_offset : 0);

        error = uiomove(sc->sc_buffer + uio->uio_offset, amount, uio);
        if (error)
                device_printf(pint_device, "read failed\n");

        return (error);
}

这个函数首先获取并行端口的互斥锁。然后它在pint_device通道上睡眠。图片 然后在通道图片上睡眠。图片

注意

ppb_sleep 函数在休眠之前释放并行端口互斥锁。当然,在返回调用者之前,它也会重新获取并行端口互斥锁。

这个函数的残留部分基本上与 echo_read 函数 中描述的 echo_read 函数相同,所以我们在这里不再讨论它们。

pint_intr 函数

pint_intr 函数是 示例 8-1 的中断处理程序。以下是它的函数定义(再次):

static void
pint_intr(void *arg)
{
        struct pint_data *sc = arg;
        device_t pint_device = sc->sc_device;

#ifdef INVARIANTS
        device_t ppbus = device_get_parent(pint_device);
        ppb_assert_locked(ppbus);
#endif

      wakeup(pint_device);
}

如您所见,这个函数只是 唤醒在 pint_device 上休眠的每个线程。

注意

并行端口的中断处理程序是独特的,因为它们在获取并行端口互斥锁的情况下被调用。相反,正常的中断处理程序需要显式获取它们自己的锁。

不要慌张

现在我们已经走过了 示例 8-1,让我们试一试:

$ `sudo kldload ./pint.ko`
$ `su`
Password:
# `echo "DON'T PANIC" > /dev/pint0`
# `cat /dev/pint0 &`
[1] 954
# `ps | head -n 1 && ps | grep "cat"`
  PID  TT  STAT      TIME COMMAND
  954  v1  I      0:00.03 cat /dev/pint0

显然它工作得很好。但我们如何生成一个中断来测试我们的中断处理程序?

在并行端口上生成中断

一旦启用中断,当引脚 10 的电信号,被称为 ACK 位,从低电平变为高电平时,并行端口会生成一个中断(Corbet 等人,2005 年)。

要切换引脚 10 的电信号,我将引脚 10 连接到引脚 9(使用一个电阻),然后执行 示例 8-2 中显示的程序。

示例 8-2. tint.c

#include <sys/types.h>
  #include <machine/cpufunc.h>

  #include <err.h>
  #include <fcntl.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>

 #define BASE_ADDRESS    0x378

  int
  main(int argc, char *argv[])
  {
          int fd;

          fd = open("/dev/io", O_RDWR);
          if (fd < 0)
                  err(1, "open(/dev/io)");

          outb(BASE_ADDRESS, 0x00);
          outb(BASE_ADDRESS, 0xff);
          outb(BASE_ADDRESS, 0x00);

          close(fd);
          return (0);
  }

在这里, BASE_ADDRESS 表示并行端口的基址。在大多数现代个人计算机上,0x378 是并行端口的基址。但是,您可以通过检查机器的 BIOS 来确保这一点。

此程序将并行端口引脚 9 的电信号从 低电平变为 高电平。

注意

如果您好奇,引脚 9 是并行数据字节的最重要位(Corbet 等人,2005 年)。

下面是执行 示例 8-2 的结果:

# `echo "DON'T PANIC" > /dev/pint0`
# `cat /dev/pint0 &`
[1] 1056
# `./tint`
DON'T PANIC

结论

本章主要关注实现中断处理程序。在 第九章 中,我们将基于这里描述的概念和代码编写一个非平凡的、中断驱动的驱动程序。

第九章. 案例研究:并行端口打印机驱动程序

无标题的图片

本章是本书的第二个案例研究。在本章中,我们将分析 lpt(4),即并行端口打印机驱动程序。默认情况下,lpt(4) 被配置为中断驱动,这为我们提供了一个机会来分析一个非平凡的中断处理程序。除此之外,我选择分析 lpt(4),因为它几乎使用了前几章中描述的每个主题。它也相对较短。

注意

为了提高可读性,本章中介绍的一些变量和函数已被重命名和重构,以从 FreeBSD 源代码中的对应部分进行对比。

代码分析

示例 9-1 提供了 lpt(4) 的简洁、源代码级别的概述。

示例 9-1. lpt.c

#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>

#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/bus.h>
#include <sys/malloc.h>
#include <sys/syslog.h>

#include <machine/bus.h>
#include <sys/rman.h>
#include <machine/resource.h>

#include <dev/ppbus/ppbconf.h>
#include "ppbus_if.h"
#include <dev/ppbus/ppbio.h>
#include <dev/ppbus/ppb_1284.h>

#include <dev/ppbus/lpt.h>
#include <dev/ppbus/lptio.h>

#define LPT_NAME        "lpt"           /* official driver name.        */
#define LPT_INIT_READY  4               /* wait up to 4 seconds.        */
#define LPT_PRI         (PZERO + 8)     /* priority.                    */
#define BUF_SIZE        1024            /* sc_buf size.                 */
#define BUF_STAT_SIZE   32              /* sc_buf_stat size.            */

struct lpt_data {
        short                   sc_state;
        char                    sc_primed;
        struct callout          sc_callout;
        u_char                  sc_ticks;
        int                     sc_irq_rid;
        struct resource        *sc_irq_resource;
        void                   *sc_irq_cookie;
        u_short                 sc_irq_status;
        void                   *sc_buf;
        void                   *sc_buf_stat;
        char                   *sc_cp;
        device_t                sc_dev;
        struct cdev            *sc_cdev;
        struct cdev            *sc_cdev_bypass;
        char                    sc_flags;
        u_char                  sc_control;
        short                   sc_transfer_count;
};

/* bits for sc_state. */
#define LP_OPEN         (1 << 0)        /* device is open.              */
#define LP_ERROR        (1 << 2)        /* error received from printer. */
#define LP_BUSY         (1 << 3)        /* printer is busy writing.     */
#define LP_TIMEOUT      (1 << 5)        /* timeout enabled.             */
#define LP_INIT         (1 << 6)        /* initializing in lpt_open.    */
#define LP_INTERRUPTED  (1 << 7)        /* write call was interrupted.  */
#define LP_HAVEBUS      (1 << 8)        /* driver owns the bus.         */

/* bits for sc_ticks. */
#define LP_TOUT_INIT    10              /* initial timeout: 1/10 sec.   */
#define LP_TOUT_MAX     1               /* max timeout: 1/1 sec.        */

/* bits for sc_irq_status. */
#define LP_HAS_IRQ      0x01            /* we have an IRQ available.    */
#define LP_USE_IRQ      0x02            /* our IRQ is in use.           */
#define LP_ENABLE_IRQ   0x04            /* enable our IRQ on open.      */
#define LP_ENABLE_EXT   0x10            /* enable extended mode.        */

/* bits for sc_flags. */
#define LP_NO_PRIME     0x10            /* don't prime the printer.     */
#define LP_PRIME_OPEN   0x20            /* prime on every open.         */
#define LP_AUTO_LF      0x40            /* automatic line feed.         */
#define LP_BYPASS       0x80            /* bypass printer ready checks. */

/* masks to interrogate printer status. */
#define LP_READY_MASK   (LPS_NERR | LPS_SEL | LPS_OUT | LPS_NBSY)
#define LP_READY        (LPS_NERR | LPS_SEL |           LPS_NBSY)

/* used in polling code. */
#define LPS_INVERT      (LPS_NERR | LPS_SEL |           LPS_NACK | LPS_NBSY)
#define LPS_MASK        (LPS_NERR | LPS_SEL | LPS_OUT | LPS_NACK | LPS_NBSY)
#define NOT_READY(bus)  ((ppb_rstr(bus) ^ LPS_INVERT) & LPS_MASK)
#define MAX_SPIN        20              /* wait up to 20 usec.          */
#define MAX_SLEEP       (hz * 5)        /* timeout while waiting.       */

static d_open_t                 lpt_open;
static d_close_t                lpt_close;
static d_read_t                 lpt_read;
static d_write_t                lpt_write;
static d_ioctl_t                lpt_ioctl;

static struct cdevsw lpt_cdevsw = {
        .d_version =            D_VERSION,
        .d_open =               lpt_open,
        .d_close =              lpt_close,
        .d_read =               lpt_read,
        .d_write =              lpt_write,
        .d_ioctl =              lpt_ioctl,
        .d_name =               LPT_NAME
};

static devclass_t lpt_devclass;

static void
lpt_identify(driver_t *driver, device_t parent)
{
...
}

static int
lpt_request_ppbus(device_t dev, int how)
{
...
}

static int
lpt_release_ppbus(device_t dev)
{
...
}

static int
lpt_port_test(device_t ppbus, u_char data, u_char mask)
{
...
}

static int
lpt_detect(device_t dev)
{
...
}

static int
lpt_probe(device_t dev)
{
...
}

static void
lpt_intr(void *arg)
{
...
}

static int
lpt_attach(device_t dev)
{
...
}

static int
lpt_detach(device_t dev)
{
...
}

static void
lpt_timeout(void *arg)
{
...
}

static int
lpt_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
...
}

static int
lpt_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
...
}

static int
lpt_read(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
lpt_push_bytes(struct lpt_data *sc)
{
...
}

static int
lpt_write(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
lpt_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
...
}

static device_method_t lpt_methods[] = {
        DEVMETHOD(device_identify,      lpt_identify),
        DEVMETHOD(device_probe,         lpt_probe),
        DEVMETHOD(device_attach,        lpt_attach),
        DEVMETHOD(device_detach,        lpt_detach),
        { 0, 0 }
};

static driver_t lpt_driver = {
        LPT_NAME,
        lpt_methods,
        sizeof(struct lpt_data)
};

DRIVER_MODULE(lpt, ppbus, lpt_driver, lpt_devclass, 0, 0);
MODULE_DEPEND(lpt, ppbus, 1, 1, 1);

示例 9-1 提供了方便;当我分析 lpt(4) 的代码时,你可以参考它来查看 lpt(4) 的函数和结构是如何布局的。

为了使内容更容易理解,我将按照它们在执行时的大致顺序分析 lpt(4) 中的函数(而不是它们出现的顺序)。为此,我将从 lpt_identify 函数开始。

lpt_identify 函数

lpt_identify 函数是 lpt(4)device_identify 实现。从逻辑上讲,此函数是必需的,因为并行端口无法在没有辅助的情况下识别其子设备。

这是 lpt_identify 函数的定义:

static void
lpt_identify(driver_t *driver, device_t parent)
{
        device_t dev;

        dev = device_find_child(parent, LPT_NAME, −1);
        if (!dev)
               BUS_ADD_CHILD(parent, 0, LPT_NAME, −1);
}

此函数首先 确定并行端口是否(曾经)识别了一个名为 LPT_NAME 的子设备。如果没有,则 lpt_identify LPT_NAME 添加到并行端口的已识别子设备列表中。

lpt_probe 函数

lpt_probe 函数是 lpt(4)device_probe 实现。以下是它的函数定义:

static int
lpt_probe(device_t dev)
{
        if (!lpt_detect(dev))
                return (ENXIO);

        device_set_desc(dev, "Printer");

        return (BUS_PROBE_SPECIFIC);
}

此函数简单地调用 lpt_detect 来检测(即探测)打印机的存在。

lpt_detect 函数

如前所述,lpt_detect 检测打印机的存在。它通过写入并行端口的寄存器来实现。如果存在打印机,它可以读取刚才写入的值。

这是 lpt_detect 函数的定义:

static int
lpt_detect(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
      static u_char test[18] = {
                0x55,                   /* alternating zeros.   */
                0xaa,                   /* alternating ones.    */
                0xfe, 0xfd, 0xfb, 0xf7,
                0xef, 0xdf, 0xbf, 0x7f, /* walking zero.        */
                0x01, 0x02, 0x04, 0x08,
                0x10, 0x20, 0x40, 0x80  /* walking one.         */
        };
        int i, error, success = 1;      /* assume success.      */

      ppb_lock(ppbus);

        error = lpt_request_ppbus(dev, PPB_DONTWAIT);
        if (error) {
                ppb_unlock(ppbus);
                device_printf(dev, "cannot allocate ppbus (%d)!\n", error);
                return (0);
        }

        for (i = 0; i < 18; i++)
                if (!lpt_port_test(ppbus, test[i], 0xff)) {
                        success = 0;
                        break;
                }

      ppb_wdtr(ppbus, 0);
      ppb_wctr(ppbus, 0);

      lpt_release_ppbus(dev);
      ppb_unlock(ppbus);

        return (success);
}

此函数首先 获取并行端口的互斥锁。接下来,lpt(4) 被分配为并行端口的拥有者。然后 调用 lpt_port_test 来写入和读取并行端口的寄存器。写入此 8 位寄存器的值存储在 test[] 中,并设计为切换所有 8 位。

完成这些操作后,并行端口的 数据和控制寄存器被清除,并行端口的拥有权 被放弃,并行端口互斥锁 被释放。

lpt_port_test 函数

lpt_port_test 函数由 lpt_detect 调用,以确定是否存在打印机。以下是它的函数定义:

static int
lpt_port_test(device_t ppbus, u_char data, u_char mask)
{
        int temp, timeout = 10000;

        data &= mask;
      ppb_wdtr(ppbus, data);

        do {
                DELAY(10);
                temp = ppb_rdtr(ppbus) & mask;
        } while (temp != data && --timeout);

      return (temp == data);
}

此函数接收一个 8 位值并将其写入并行端口的数据寄存器。然后它 从该寄存器读取并 返回写入和读取的值是否匹配。

lpt_attach 函数

lpt_attach 函数是 lpt(4)device_attach 实现。以下是它的函数定义:

static int
lpt_attach(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);
        int error, unit = device_get_unit(dev);

      sc->sc_primed = 0;
      ppb_init_callout(ppbus, &sc->sc_callout, 0);

        ppb_lock(ppbus);
        error = lpt_request_ppbus(dev, PPB_DONTWAIT);
        if (error) {
                ppb_unlock(ppbus);
                device_printf(dev, "cannot allocate ppbus (%d)!\n", error);
                return (0);
        }

      ppb_wctr(ppbus, LPC_NINIT);

        lpt_release_ppbus(dev);
        ppb_unlock(ppbus);

        /* Declare our interrupt handler. */
        sc->sc_irq_rid = 0;
        sc->sc_irq_resource = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->sc_irq_rid, RF_ACTIVE | RF_SHAREABLE);

        /* Register our interrupt handler. */
        if (sc->sc_irq_resource) {
                error = bus_setup_intr(dev, sc->sc_irq_resource,
                    INTR_TYPE_TTY | INTR_MPSAFE, NULL, lpt_intr,
                    sc, &sc->sc_irq_cookie);
                if (error) {
                        bus_release_resource(dev, SYS_RES_IRQ,
                            sc->sc_irq_rid, sc->sc_irq_resource);
                        device_printf(dev,
                            "unable to register interrupt handler\n");
                        return (error);
                }

              sc->sc_irq_status = LP_HAS_IRQ | LP_USE_IRQ | LP_ENABLE_IRQ;
                device_printf(dev, "interrupt-driven port\n");
        } else {
                sc->sc_irq_status = 0;
                device_printf(dev, "polled port\n");
        }

      sc->sc_buf = malloc(BUF_SIZE, M_DEVBUF, M_WAITOK);
      sc->sc_buf_stat = malloc(BUF_STAT_SIZE, M_DEVBUF, M_WAITOK);

        sc->sc_dev = dev;

        sc->sc_cdev = make_dev(&lpt_cdevsw, unit, UID_ROOT, GID_WHEEL, 0600,
            LPT_NAME "%d", unit);
        sc->sc_cdev->si_drv1 = sc;
        sc->sc_cdev->si_drv2 = 0;

        sc->sc_cdev_bypass = make_dev(&lpt_cdevsw, unit, UID_ROOT, GID_WHEEL,
            0600, LPT_NAME "%d.ctl", unit);
        sc->sc_cdev_bypass->si_drv1 = sc;
        sc->sc_cdev_bypass->si_drv2 = (void *)LP_BYPASS;

        return (0);
}

此函数可以分为五个部分。第一部分 sc->sc_primed 设置为 0,以指示打印机需要初始化。它还 初始化 lpt(4)callout 结构。第二部分实际上 将引脚 16 的电信号,称为 nINIT,从高电平变为低电平,导致打印机启动内部复位。

注意

由于大多数信号都是高电平有效,nnINIT 中表示信号是低电平有效。

第三部分将函数 lpt_intr 注册为中断处理程序。如果成功,variable sc->sc_irq_status 被赋予 LP_HAS_IRQLP_USE_IRQLP_ENABLE_IRQ,以指示打印机是中断驱动的。第四部分为两个缓冲区分配内存: sc->sc_buf(将维护要打印的数据)和 sc->sc_buf_stat(将维护打印机的状态)。最后,第五部分创建 lpt(4) 的设备节点:lpt%dlpt%d.ctl,其中 %d 是单元号。请注意,lpt%d.ctl 包含 LP_BYPASS 标志,而 lpt%d 则没有。在 d_foo 函数中,LP_BYPASS 用于区分 lpt%d.ctllpt%d。正如您将看到的,lpt%d 设备节点代表打印机,而 lpt%d.ctl 仅用于更改打印机的操作模式(通过 lpt(4)d_ioctl 例程)。

lpt_detach 函数

lpt_detach 函数是 lpt(4)device_detach 实现。以下是它的函数定义:

static int
lpt_detach(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);

      destroy_dev(sc->sc_cdev_bypass);
      destroy_dev(sc->sc_cdev);

        ppb_lock(ppbus);
      lpt_release_ppbus(dev);
        ppb_unlock(ppbus);

      callout_drain(&sc->sc_callout);

        if (sc->sc_irq_resource) {
               bus_teardown_intr(dev, sc->sc_irq_resource,
                    sc->sc_irq_cookie);
               bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
                    sc->sc_irq_resource);
        }

      free(sc->sc_buf_stat, M_DEVBUF);
      free(sc->sc_buf, M_DEVBUF);

        return (0);
}

此函数首先销毁lpt(4)的设备节点。一旦完成,它放弃对并行端口的控制,清空lpt(4)的调用函数,拆除lpt(4)的中断处理程序,释放lpt(4)的 IRQ,并释放分配的内存。

lpt_open函数

lpt_open函数在lpt_cdevsw(即lpt(4)的字符设备切换表中)定义为d_open操作。回想一下,d_open操作是为 I/O 准备设备。

下面是lpt_open函数的定义:

static int
lpt_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int try, error;

        if (!sc)
                return (ENXIO);

        ppb_lock(ppbus);
      if (sc->sc_state) {
                ppb_unlock(ppbus);
                return (EBUSY);
        } else
                sc->sc_state |= LP_INIT;

      sc->sc_flags = (uintptr_t)dev->si_drv2;
        if (sc->sc_flags & LP_BYPASS) {
                sc->sc_state = LP_OPEN;
                ppb_unlock(ppbus);
                return (0);
        }

        error = lpt_request_ppbus(lpt_dev, PPB_WAIT | PPB_INTR);
        if (error) {
                sc->sc_state = 0;
                ppb_unlock(ppbus);
                return (error);
        }

        /* Use our IRQ? */
        if (sc->sc_irq_status & LP_ENABLE_IRQ)
                sc->sc_irq_status |= LP_USE_IRQ;
        else
                sc->sc_irq_status &= ˜LP_USE_IRQ;

        /* Reset printer. */
        if ((sc->sc_flags & LP_NO_PRIME) == 0)
                if ((sc->sc_flags & LP_PRIME_OPEN) || sc->sc_primed == 0) {
                      ppb_wctr(ppbus, 0);
                        sc->sc_primed++;
                        DELAY(500);
                }

      ppb_wctr(ppbus, LPC_SEL | LPC_NINIT);

        /* Wait until ready--printer should be running diagnostics. */
        try = 0;
      do {
                /* Give up? */
                if (try++ >= (LPT_INIT_READY * 4)) {
                        lpt_release_ppbus(lpt_dev);
                        sc->sc_state = 0;
                        ppb_unlock(ppbus);
                        return (EBUSY);
                }

                /* Wait 1/4 second. Give up if we get a signal. */
                if (ppb_sleep(ppbus, lpt_dev, LPT_PRI | PCATCH, "lpt_open",
                    hz / 4) != EWOULDBLOCK) {
                        lpt_release_ppbus(lpt_dev);
                        sc->sc_state = 0;
                        ppb_unlock(ppbus);
                        return (EBUSY);
                }
        } while ((ppb_rstr(ppbus) & LP_READY_MASK) != LP_READY);

      sc->sc_control = LPC_SEL | LPC_NINIT;
        if (sc->sc_flags & LP_AUTO_LF)
              sc->sc_control |= LPC_AUTOL;
        if (sc->sc_irq_status & LP_USE_IRQ)
              sc->sc_control |= LPC_ENA;

        ppb_wctr(ppbus, sc->sc_control);

        sc->sc_state &= ˜LP_INIT;
        sc->sc_state |= LP_OPEN;
        sc->sc_transfer_count = 0;

        if (sc->sc_irq_status & LP_USE_IRQ) {
                sc->sc_state |= LP_TIMEOUT;
                sc->sc_ticks = hz / LP_TOUT_INIT;
                callout_reset(&sc->sc_callout, sc->sc_ticks,
                   lpt_timeout, sc);
        }

        lpt_release_ppbus(lpt_dev);
        ppb_unlock(ppbus);

        return (0);
}

此函数可以分为六个部分。第一部分检查sc->sc_state的值。如果不等于0,这意味着另一个进程已经打开了打印机,返回错误代码EBUSY;否则,将sc->sc_state赋值为LP_INIT。第二部分检查dev->si_drv2的值。

如果它包含LP_BYPASS标志,这表示设备节点是lpt%d.ctl,则将sc->sc_state设置为LP_OPEN并退出lpt_open。回想一下,lpt%d.ctl仅用于更改打印机的操作模式,因此准备工作量很小。第三部分初始化打印机,然后选择并重置打印机(当 17 脚的电信号从高变低时,即被称为nSELIN,打印机准备接收数据),选择并重置打印机。第四部分等待打印机完成内部重置。第五部分选择并重置打印机,如果需要,启用自动换行,^([8])并启用中断,如果打印机是中断驱动的。第五部分还将LP_OPEN赋给sc->sc_state并将变量sc->sc_transfer_count清零。

注意

当 14 脚的电信号从高变低时,自动换行被启用,这个电信号被称为 nAUTOF。正如你所期望的,这会导致打印机在每一行后自动插入换行符。

最后,第六部分会在 sc->sc_ticks / hz 秒后执行一次 lpt_timeoutlpt_timeout 函数与中断处理程序 lpt_intr 一起使用。我将在稍后讨论这些函数。

lpt_read 函数

lpt_read 函数检索打印机的状态。用户可以通过对设备节点 lpt%d 应用 cat(1) 命令来获取打印机的状态。

下面是 lpt_read 函数的定义:

static int
lpt_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int num, error = 0;

      if (sc->sc_flags & LP_BYPASS)
                return (EPERM);

        ppb_lock(ppbus);
        error = ppb_1284_negociate(ppbus, PPB_NIBBLE, 0);
        if (error) {
                ppb_unlock(ppbus);
                return (error);
        }

        num = 0;
        while (uio->uio_resid) {
                error = ppb_1284_read(ppbus, PPB_NIBBLE,
 sc->sc_buf_stat,
                    min(BUF_STAT_SIZE, uio->uio_resid), &num);
                if (error)
                        goto end_read;

              if (!num)
                        goto end_read;

                ppb_unlock(ppbus);
                error = uiomove(sc->sc_buf_stat, num, uio);
                ppb_lock(ppbus);
                if (error)
                        goto end_read;
        }

end_read:
        ppb_1284_terminate(ppbus);
        ppb_unlock(ppbus);
        return (error);
}

此函数首先检查 sc->sc_flags 的值。如果它包含 LP_BYPASS 标志,这表示设备节点是 lpt%d.ctl,则返回错误代码 EPERM(代表 error: operation not permitted)。接下来,函数调用 ppb_1284_negociate 将并行端口接口置于 nibble 模式。

注意

Nibble 模式是检索打印机数据最常见的方式。通常,引脚 10、11、12、13 和 15 被打印机用作外部状态指示器;然而,在 nibble 模式下,这些引脚用于向主机发送数据(每次发送 4 位)。

此函数的其余部分将数据从打印机传输到用户空间。在这种情况下,数据是打印机的状态。这里,ppb_1284_read 将数据从打印机传输到内核空间。传输的字节数保存在 num 中。如果 num 等于 0,则 lpt_read 退出。然后 uiomove 函数将数据从内核空间移动到用户空间。

lpt_write 函数

lpt_write 函数从用户空间获取数据并将其存储在 sc->sc_buf 中。然后,这些数据被发送到打印机进行打印。

下面是 lpt_write 函数的定义:

static int
lpt_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        register unsigned num;
        int error;

        if (sc->sc_flags & LP_BYPASS)
                return (EPERM);

        ppb_lock(ppbus);
        error = lpt_request_ppbus(lpt_dev, PPB_WAIT | PPB_INTR);
        if (error) {
                ppb_unlock(ppbus);
                return (error);
        }

      sc->sc_state &= ˜LP_INTERRUPTED;
        while ((num = min(BUF_SIZE, uio->uio_resid))) {
                sc->sc_cp = sc->sc_buf;

                ppb_unlock(ppbus);
                error = uiomove(sc->sc_cp, num, uio);
                ppb_lock(ppbus);
                if (error)
                        break;

              sc->sc_transfer_count = num;

              if (sc->sc_irq_status & LP_ENABLE_EXT) {
                        error = ppb_write(ppbus, sc->sc_cp,
                            sc->sc_transfer_count, 0);
                        switch (error) {
                        case 0:
                                sc->sc_transfer_count = 0;
                                break;
                        case EINTR:
                                sc->sc_state |= LP_INTERRUPTED;
                                ppb_unlock(ppbus);
                                return (error);
                        case EINVAL:
                                log(LOG_NOTICE,
                                    "%s: extended mode not available\n",
                                    device_get_nameunit(lpt_dev));
                                break;
                        default:
                                ppb_unlock(ppbus);
                                return (error);
                        }
                } else while ((sc->sc_transfer_count > 0) &&
                             (sc->sc_irq_status & LP_USE_IRQ)) {
                        if (!(sc->sc_state & LP_BUSY))
                              lpt_intr(sc);

                        if (sc->sc_state & LP_BUSY) {
                                error = ppb_sleep(ppbus, lpt_dev,
                                    LPT_PRI | PCATCH, "lpt_write", 0);
                                if (error) {
                                        sc->sc_state |= LP_INTERRUPTED;
                                        ppb_unlock(ppbus);
                                        return (error);
                                }
                        }
                }

                if (!(sc->sc_irq_status & LP_USE_IRQ) &&
                     (sc->sc_transfer_count)) {
                        error = lpt_push_bytes(sc);
                        if (error) {
                                ppb_unlock(ppbus);
                                return (error);
                        }
                }
        }

        lpt_release_ppbus(lpt_dev);
        ppb_unlock(ppbus);

        return (error);
}

lpt_read 类似,这个函数首先检查 sc->sc_flags 的值。如果它包含 LP_BYPASS 标志,则返回错误代码 EPERM。接下来,从 sc->sc_state 中移除 LP_INTERRUPTED 标志(正如你将看到的,每当写操作被中断时,LP_INTERRUPTED 都会被添加到 sc->sc_state)。下面的 while 循环包含了 lpt_write 的主要部分。注意,其表达式决定了从用户空间复制到内核空间的数据量。这个量被保存在 sc->sc_transfer_count 中,每次向打印机发送一个字节时,这个值都会递减。

现在,有三种方式将数据从内核空间传输到打印机。首先,如果扩展模式被启用,lpt_write 可以直接写入打印机。

注意

扩展模式指的是增强型并行端口 (EPP) 或扩展功能端口 (ECP) 模式。EPP 和 ECP 模式旨在比普通并行端口通信更快地传输数据,并且具有更少的 CPU 开销。大多数并行端口支持这两种模式之一或两者。

第二种情况,如果打印机是中断驱动的,并且 sc->sc_state 中的 LP_BUSY 标志被清除,lpt_write 可以调用 lpt_intr 来将数据传输到打印机。在下一节中查看 lpt_intr 函数的定义,你会看到在 lpt_intr 执行期间设置了 LP_BUSY,并且 LP_BUSY 不会清除直到 sc->sc_transfer_count0。这防止了 lpt_write 在当前传输完成之前发出另一个中断驱动的传输,这就是为什么 lpt_write 会睡眠。

最后,如果前两种选项不可用,lpt_write 可以通过调用 lpt_push_bytes 来发出轮询传输,该函数在 lpt_push_bytes 函数 和 lpt_close 函数 中描述。

lpt_intr 函数

lpt_intr 函数是 lpt(4) 的中断处理程序。这个函数将 1 个字节从 sc->sc_buf 传输到打印机,然后退出。当打印机准备好接收另一个字节时,它会发送一个中断。注意,在 lpt_intr 中,sc->sc_buf 通过 sc->sc_cp 访问。

这里是 lpt_intr 函数的定义:

static void
lpt_intr(void *arg)
{
        struct lpt_data *sc = arg;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int i, status = 0;

      for (i = 0; i < 100 &&
             ((status = ppb_rstr(ppbus)) & LP_READY_MASK) != LP_READY; i++)
                ;       /* nothing. */

        if ((status & LP_READY_MASK) == LP_READY) {
              sc->sc_state = (sc->sc_state | LP_BUSY) & ˜LP_ERROR;
              sc->sc_ticks = hz / LP_TOUT_INIT;

                if (sc->sc_transfer_count) {
                      ppb_wdtr(ppbus, *sc->sc_cp++);
                      ppb_wctr(ppbus, sc->sc_control | LPC_STB);
                        ppb_wctr(ppbus, sc->sc_control);

                        if (--(sc->sc_transfer_count) > 0)
                               return;
                }

              sc->sc_state &= ˜LP_BUSY;

                if (!(sc->sc_state & LP_INTERRUPTED))
                      wakeup(lpt_dev);

                return;
        } else {
                if (((status & (LPS_NERR | LPS_OUT)) != LPS_NERR) &&
                    (sc->sc_state & LP_OPEN))
                        sc->sc_state |= LP_ERROR;
        }
}

此函数首先反复检查打印机是否在线且准备输出。如果是,将LP_BUSY标志添加到sc->sc_state,并移除表示打印机错误的LP_ERROR标志。接下来,重置sc->sc_ticks。然后从sc->sc_buf写入 1 个字节到并行端口的数据寄存器,并随后发送到打印机(当 1 号引脚的电信号,被称为nSTROBE,从高电平变为低电平时,并行端口接口上的数据被发送到打印机)。如果有更多数据要发送(即sc->sc_transfer_count大于0),lpt_intr将退出,因为等待中断再发送另一个字节是协议。如果没有更多数据要发送,将LP_BUSYsc->sc_state中清除,并唤醒lpt_write

lpt_timeout函数

lpt_timeout函数是lpt(4)的调用函数。它被设计来处理错过或未处理的中断。以下是它的函数定义:

static void
lpt_timeout(void *arg)
{
        struct lpt_data *sc = arg;
        device_t lpt_dev = sc->sc_dev;

      if (sc->sc_state & LP_OPEN) {
                sc->sc_ticks++;
                if (sc->sc_ticks > hz / LP_TOUT_MAX)
                        sc->sc_ticks = hz / LP_TOUT_MAX;
              callout_reset(&sc->sc_callout, sc->sc_ticks,
                    lpt_timeout, sc);
        } else
                sc->sc_state &= ˜LP_TIMEOUT;

        if (sc->sc_state & LP_ERROR)
              sc->sc_state &= ˜LP_ERROR;

      if (sc->sc_transfer_count)
              lpt_intr(sc);
        else {
                sc->sc_state &= ˜LP_BUSY;
                wakeup(lpt_dev);
        }
}

此函数首先检查lpt%d是否已打开。如果是,lpt_timeout将重新安排自身以执行。接下来,LP_ERRORsc->sc_state中移除。现在如果lpt(4)错过了一个中断,将调用lpt_intr以重新开始向打印机传输数据。

注意,如果没有在![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137505.png)处的if块,lpt(4)将挂起等待已发送但丢失的中断。

lpt_push_bytes函数

lpt_push_bytes函数使用轮询将数据传输到打印机。此函数仅在扩展模式禁用且打印机不是中断驱动时由lpt_write调用。

以下是lpt_push_bytes的函数定义:

static int
lpt_push_bytes(struct lpt_data *sc)
{
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int error, spin, tick;
        char ch;

      while (sc->sc_transfer_count > 0) {
                ch = *sc->sc_cp;
                sc->sc_cp++;
                sc->sc_transfer_count--;

              for (spin = 0; NOT_READY(ppbus) && spin < MAX_SPIN; spin++)
                        DELAY(1);

                if (spin >= MAX_SPIN) {
                        tick = 0;
                        while (NOT_READY(ppbus)) {
                                tick = tick + tick + 1;
                                if (tick > MAX_SLEEP)
                                        tick = MAX_SLEEP;

                                error = ppb_sleep(ppbus, lpt_dev, LPT_PRI,
                                    "lpt_poll", tick);
                                if (error != EWOULDBLOCK)
                                        return (error);
                        }
                }

              ppb_wdtr(ppbus, ch);
              ppb_wctr(ppbus, sc->sc_control | LPC_STB);
                ppb_wctr(ppbus, sc->sc_control);
        }

        return (0);
}

此函数首先 图片 验证是否有数据要传输。然后它 图片 轮询打印机以查看其是否在线且准备输出。如果打印机未准备好,lpt_push_bytes 图片 将短暂休眠,然后在唤醒时重新轮询打印机。这种休眠和轮询的循环会一直重复,直到打印机准备好。如果打印机准备好了,从 sc->sc_buf 中取出的 1 字节 图片 将写入并行端口的数据寄存器,然后 图片 发送到打印机。这个过程会一直重复,直到 sc->sc_buf 中的所有数据都传输完毕。

lpt_close 函数

lpt_close 函数在 lpt_cdevsw 中定义为 d_close 操作。以下是它的函数定义:

static int
lpt_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int error;

        ppb_lock(ppbus);

      if (sc->sc_flags & LP_BYPASS)
                goto end_close;

        error = lpt_request_ppbus(lpt_dev, PPB_WAIT | PPB_INTR);
        if (error) {
                ppb_unlock(ppbus);
                return (error);
        }

      if (!(sc->sc_state & LP_INTERRUPTED) &&
           (sc->sc_irq_status & LP_USE_IRQ))
                while ((ppb_rstr(ppbus) & LP_READY_MASK) != LP_READY ||
                   sc->sc_transfer_count)
                        if (ppb_sleep(ppbus, lpt_dev, LPT_PRI | PCATCH,
                            "lpt_close", hz) != EWOULDBLOCK)
                                break;

      sc->sc_state &= ˜LP_OPEN;
      callout_stop(&sc->sc_callout);
      ppb_wctr(ppbus, LPC_NINIT);

        lpt_release_ppbus(lpt_dev);

 end_close:
      sc->sc_state = 0;
      sc->sc_transfer_count = 0;
        ppb_unlock(ppbus);
        return (0);
}

lpt_readlpt_write 类似,此函数首先检查 sc->sc_flags 的值。如果包含 LP_BYPASS 标志,lpt_close 将跳转到 图片 end_close。接下来,lpt(4) 被分配为并行端口的拥有权。下面的 图片 if 块确保在关闭 lpt%d 之前,如果仍有数据要传输且打印机是 图片 中断驱动的,传输将完成。然后,从 sc->sc_state 中移除 LP_OPEN,停止 lpt_timeout,重置打印机,并放弃并行端口的拥有权。最后,图片 sc->sc_state图片 sc->sc_transfer_count 被置零。

lpt_ioctl 函数

lpt_ioctl 函数在 lpt_cdevsw 中定义为 d_ioctl 操作。在我描述此函数之前,需要对其 ioctl 命令 LPT_IRQ 进行解释。LPT_IRQ<dev/ppbus/lptio.h> 头文件中定义为以下内容:

#define LPT_IRQ         _IOW('p', 1, long)

如您所见,LPT_IRQ 需要一个 图片 long int 值。

static int
lpt_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        u_short old_irq_status;
        int error = 0;

        switch (cmd) {
      case LPT_IRQ:
                ppb_lock(ppbus);
                if (sc->sc_irq_status & LP_HAS_IRQ) {
                        old_irq_status = sc->sc_irq_status;
                        switch (*(int *)data) {
                        case 0:
                              sc->sc_irq_status &= ˜LP_ENABLE_IRQ;
                                break;
                        case 1:
                                sc->sc_irq_status &= ˜LP_ENABLE_EXT;
                              sc->sc_irq_status |= LP_ENABLE_IRQ;
                                break;
                        case 2:
                                sc->sc_irq_status &= ˜LP_ENABLE_IRQ;
                              sc->sc_irq_status |= LP_ENABLE_EXT;
                                break;
                        case 3:
                              sc->sc_irq_status &= ˜LP_ENABLE_EXT;
                                break;
                        default:
                                break;
                        }

                        if (old_irq_status != sc->sc_irq_status)
                                log(LOG_NOTICE,
                                    "%s: switched to %s %s mode\n",
                                    device_get_nameunit(lpt_dev),
                                    (sc->sc_irq_status & LP_ENABLE_IRQ) ?
                                    "interrupt-driven" : "polled",
                                    (sc->sc_irq_status & LP_ENABLE_EXT) ?
                                    "extended" : "standard");
                } else
                        error = EOPNOTSUPP;

                ppb_unlock(ppbus);
                break;
        default:
                error = ENODEV;
                break;
        }

        return (error);
}

根据 LPT_IRQ 给出的参数,lpt_ioctl 要么禁用中断驱动模式(这启用了轮询模式),要么启用中断驱动模式,要么启用扩展模式,要么禁用扩展模式(这启用了标准模式)。请注意,中断驱动模式和扩展模式相互冲突,因此如果启用其中一个,另一个将被禁用。

注意

要运行此函数,您将使用 lptcontrol(8) 工具,我建议您快速查看其源代码。

lpt_request_ppbus 函数

lpt_request_ppbus 函数将 lpt(4) 设置为并行端口的拥有者。回想一下,拥有并行端口允许设备(如 lpt%d)向其传输数据。

这是 lpt_request_ppbus 函数的定义:

static int
lpt_request_ppbus(device_t dev, int how)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);
        int error;

        ppb_assert_locked(ppbus);

      if (sc->sc_state & LP_HAVEBUS)
               return (0);

        error = ppb_request_bus(ppbus, dev, how);
        if (!error)
               sc->sc_state |= LP_HAVEBUS;

        return (error);
}

此函数首先检查 sc->sc_state 的值。如果它包含 LP_HAVEBUS,这表示 lpt(4) 当前拥有并行端口,则 lpt_request_ppbus 退出。否则,调用 ppb_request_buslpt(4) 设置为并行端口的拥有者,并将 LP_HAVEBUS 分配给 sc->sc_state

lpt_release_ppbus 函数

lpt_release_ppbus 函数使 lpt(4) 放弃对并行端口的拥有权。以下是其函数定义:

static int
lpt_release_ppbus(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);
        int error = 0;

        ppb_assert_locked(ppbus);

      if (sc->sc_state & LP_HAVEBUS) {
                error = ppb_release_bus(ppbus, dev);
                if (!error)
                       sc->sc_state &= ˜LP_HAVEBUS;
        }

        return (error);
}

此函数首先验证 lpt(4) 当前是否拥有并行端口。接下来,它调用 ppb_release_bus 放弃对并行端口的拥有权。然后从 sc->sc_state 中移除 LP_HAVEBUS


^([8]) 奇怪的是,目前无法请求自动换行。

结论

本章描述了 lpt(4) 并行端口打印驱动程序的整个代码库。

第十章. 管理和使用资源

无标题图片

在 第七章 中,我们讨论了如何分配中断请求、I/O 端口和 I/O 内存。第八章 专注于使用中断请求进行中断处理。本章详细介绍了如何使用 I/O 端口进行端口映射 I/O(PMIO)和 I/O 内存进行内存映射 I/O(MMIO)。在描述 PMIO 和 MMIO 之前,需要了解一些关于 I/O 端口和 I/O 内存的基础知识。

I/O 端口和 I/O 内存

每个外围设备都通过读取和写入其寄存器来控制(Corbet 等人,2005 年),这些寄存器映射到 I/O 端口或 I/O 内存。I/O 端口或 I/O 内存的使用取决于设备和架构。例如,在 i386 上,大多数 ISA 设备将它们的寄存器映射到 I/O 端口;然而,PCI 设备倾向于将它们的寄存器映射到 I/O 内存。正如你可能已经猜到的,读取和写入映射到 I/O 端口或 I/O 内存中的设备寄存器被称为 PMIO 或 MMIO。

从 I/O 端口和 I/O 内存读取

驱动程序在调用 bus_alloc_resource 分配所需的 I/O 端口或 I/O 内存范围之后,可以使用以下函数之一从这些 I/O 区域读取:

#include <sys/bus.h>
#include <machine/bus.h>

u_int8_t
bus_read_1(struct resource *r, bus_size_t offset);

u_int16_t
bus_read_2(struct resource *r, bus_size_t offset);

u_int32_t
bus_read_4(struct resource *r, bus_size_t offset);

u_int64_t
bus_read_8(struct resource *r, bus_size_t offset);

void
bus_read_multi_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_read_multi_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_read_multi_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_read_multi_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_read_region_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_read_region_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_read_region_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_read_region_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

bus_read_N 函数(其中 N1248)从 r 中的 offset 位置读取 N 字节(其中 r 是成功调用 bus_alloc_resource 分配 I/O 区域的返回值)。

bus_read_multi_N 函数从 r 中的 offset 位置读取 N 字节,共读取 count 次,并将读取的数据存储到 datap 中。简而言之,bus_read_multi_N 从同一位置多次读取。

bus_read_region_N 函数从 r 中的 offset 位置开始读取 count N-字节值,并将读取的数据存储到 datap 中。换句话说,bus_read_region_N 从 I/O 区域(即数组)中读取连续的 N-字节值。

向 I/O 端口和 I/O 内存写入

驱动程序使用以下函数之一将数据写入 I/O 区域:

#include <sys/bus.h>
#include <machine/bus.h>

void
bus_write_1(struct resource *r, bus_size_t offset,
    u_int8_t value);

void
bus_write_2(struct resource *r, bus_size_t offset,
    u_int16_t value);

void
bus_write_4(struct resource *r, bus_size_t offset,
    u_int32_t value);

void
bus_write_8(struct resource *r, bus_size_t offset,
    u_int64_t value);

void
bus_write_multi_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_write_multi_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_write_multi_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_write_multi_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_write_region_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_write_region_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_write_region_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_write_region_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_set_multi_1(struct resource *r, bus_size_t offset,
    u_int8_t value, bus_size_t count);

void
bus_set_multi_2(struct resource *r, bus_size_t offset,
    u_int16_t value, bus_size_t count);

void
bus_set_multi_4(struct resource *r, bus_size_t offset,
    u_int32_t value, bus_size_t count);

void
bus_set_multi_8(struct resource *r, bus_size_t offset,
    u_int64_t value, bus_size_t count);

void
bus_set_region_1(struct resource *r, bus_size_t offset,
    u_int8_t value, bus_size_t count);

void
bus_set_region_2(struct resource *r, bus_size_t offset,
    u_int16_t value, bus_size_t count);

void
bus_set_region_4(struct resource *r, bus_size_t offset,
    u_int32_t value, bus_size_t count);

void
bus_set_region_8(struct resource *r, bus_size_t offset,
    u_int64_t value, bus_size_t count);

bus_write_N 函数(其中 N1248)将 N-字节 value 写入 r 中的 offset 位置(其中 rbus_alloc_resource 调用分配的 I/O 区域的返回值)。

bus_write_multi_N 函数从 datap 中读取 count N-字节值,并将它们写入 r 中的 offset 位置。简而言之,bus_write_multi_N 将多个值写入同一位置。

bus_write_region_N 函数从 datap 中读取 count N-字节值,并将它们写入 r 中的某个区域,起始位置为 offset。每个后续值都写入前一个值之后 N 字节的位置。简而言之,bus_write_region_N 将连续的 N-字节值写入 I/O 区域(即数组)。

bus_set_multi_N 函数将一个 N-字节的 value 写入 r 中的 offset,重复 count 次。也就是说,bus_set_multi_N 将相同的值多次写入相同的位置。

bus_set_region_N 函数将一个 N-字节的 value,重复 count 次,写入 r 中的某个区域,从 offset 开始。换句话说,bus_set_region_N 将相同的值连续写入一个 I/O 区域(即数组)。

流操作

所有的前面函数都处理主机字节序和总线字节序之间的转换。然而,在某些情况下,您可能需要避免这种转换。幸运的是,FreeBSD 提供了以下函数来满足这种场合:

#include <sys/bus.h>
#include <machine/bus.h>

u_int8_t
bus_read_stream_1(struct resource *r, bus_size_t offset);

u_int16_t
bus_read_stream_2(struct resource *r, bus_size_t offset);

u_int32_t
bus_read_stream_4(struct resource *r, bus_size_t offset);

u_int64_t
bus_read_stream_8(struct resource *r, bus_size_t offset);

void
bus_read_multi_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_read_multi_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_read_multi_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_read_multi_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_read_region_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_read_region_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_read_region_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_read_region_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_write_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t value);

void
bus_write_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t value);

void
bus_write_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t value);

void
bus_write_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t value);

void
bus_write_multi_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_write_multi_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_write_multi_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_write_multi_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_write_region_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t *datap, bus_size_t count);

void
bus_write_region_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t *datap, bus_size_t count);

void
bus_write_region_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t *datap, bus_size_t count);

void
bus_write_region_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t *datap, bus_size_t count);

void
bus_set_multi_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t value, bus_size_t count);

void
bus_set_multi_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t value, bus_size_t count);

void
bus_set_multi_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t value, bus_size_t count);

void
bus_set_multi_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t value, bus_size_t count);

void
bus_set_region_stream_1(struct resource *r, bus_size_t offset,
    u_int8_t value, bus_size_t count);

void
bus_set_region_stream_2(struct resource *r, bus_size_t offset,
    u_int16_t value, bus_size_t count);

void
bus_set_region_stream_4(struct resource *r, bus_size_t offset,
    u_int32_t value, bus_size_t count);

void
bus_set_region_stream_8(struct resource *r, bus_size_t offset,
    u_int64_t value, bus_size_t count);

这些函数与其非流版本相同,只是它们不执行任何字节序转换。

内存屏障

如果按照与程序文本不同的顺序执行读取和写入指令序列,通常可以更快地执行(Corbet et al., 2005)。因此,现代处理器通常重新排序读取和写入指令。然而,这种优化可能会破坏执行 PMIO 和 MMIO 的驱动程序。为了防止指令重排序,使用了内存屏障。内存屏障 确保在屏障之前的所有指令都完成之前,屏障之后的任何指令都不会执行。对于 PMIO 和 MMIO 操作,bus_barrier 函数提供了这种能力:

#include <sys/bus.h>
#include <machine/bus.h>

void
bus_barrier(struct resource *r, bus_size_t offset, bus_size_t length,
    int flags);

bus_barrier 函数在 r 的某个区域中插入一个内存屏障,该区域由 offsetlength 参数描述,强制对读取或写入操作进行排序。flags 参数指定要排序的操作类型。此参数的有效值在 表 10-1 中显示。

表 10-1. bus_barrier 符号常量

常量 描述
BUS_SPACE_BARRIER_READ 同步读取操作
BUS_SPACE_BARRIER_WRITE 同步写入操作

注意,这些标志可以按位或(ORed)来强制在读取和写入操作上执行排序。bus_barrier 的一个典型用法如下:

bus_write_1(r, 0, data0);
bus_barrier(r, 0, 1, BUS_SPACE_BARRIER_WRITE);
bus_write_1(r, 0, data1);
bus_barrier(r, 0, 2, BUS_SPACE_BARRIER_READ | BUS_SPACE_BARRIER_WRITE);
data2 = bus_read_1(r, 1);
bus_barrier(r, 1, 1, BUS_SPACE_BARRIER_READ);
data3 = bus_read_1(r, 1);

在这里,对 bus_barrier 的调用保证了写入和读取的顺序与写入的顺序一致。

综合一切

示例 10-1 是一个简单的 i-Opener LED 驱动程序(基于 Warner Losh 编写的代码)。i-Opener 包含两个 LED,由位于 0x404c 的寄存器的位 0 和 1 控制。希望这个示例能澄清您可能对 PMIO(以及 MMIO)存在的任何误解。

注意

快速浏览一下这段代码,并尝试了解其结构。如果您不理解其中的所有内容,请不要担心;解释将在后面提供。

示例 10-1. led.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/bus.h>
  #include <sys/conf.h>
  #include <sys/uio.h>
  #include <sys/lock.h>
  #include <sys/mutex.h>

  #include <machine/bus.h>
  #include <sys/rman.h>
  #include <machine/resource.h>

 #define LED_IO_ADDR             0x404c
 #define LED_NUM                 2

  struct led_softc {
          int                     sc_io_rid;
          struct resource        *sc_io_resource;
          struct cdev            *sc_cdev0;
          struct cdev            *sc_cdev1;
          u_int32_t               sc_open_mask;
          u_int32_t               sc_read_mask;
          struct mtx              sc_mutex;
  };

  static devclass_t led_devclass;

  static d_open_t                 led_open;
  static d_close_t                led_close;
  static d_read_t                 led_read;
  static d_write_t                led_write;

  static struct cdevsw led_cdevsw = {
          .d_version =            D_VERSION,
          .d_open =               led_open,
          .d_close =              led_close,
          .d_read =               led_read,
          .d_write =              led_write,
          .d_name =               "led"
  };

  static int
  led_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
  {
          int led = dev2unit(dev) & 0xff;
          struct led_softc *sc = dev->si_drv1;

          if (led >= LED_NUM)
                  return (ENXIO);

          mtx_lock(&sc->sc_mutex);
          if (sc->sc_open_mask & (1 << led)) {
                  mtx_unlock(&sc->sc_mutex);
                  return (EBUSY);
          }
          sc->sc_open_mask |= 1 << led;
          sc->sc_read_mask |= 1 << led;
          mtx_unlock(&sc->sc_mutex);

          return (0);
  }

  static int
  led_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
  {
          int led = dev2unit(dev) & 0xff;
          struct led_softc *sc = dev->si_drv1;

          if (led >= LED_NUM)
                  return (ENXIO);

          mtx_lock(&sc->sc_mutex);
          sc->sc_open_mask &= ˜(1 << led);
          mtx_unlock(&sc->sc_mutex);

          return (0);
  }

  static int
  led_read(struct cdev *dev, struct uio *uio, int ioflag)
  {
          int led = dev2unit(dev) & 0xff;
          struct led_softc *sc = dev->si_drv1;
          u_int8_t ch;
          int error;

          if (led >= LED_NUM)
                  return (ENXIO);

          mtx_lock(&sc->sc_mutex);
          /* No error EOF condition. */
          if (!(sc->sc_read_mask & (1 << led))) {
                  mtx_unlock(&sc->sc_mutex);
                  return (0);
          }
          sc->sc_read_mask &= ˜(1 << led);
          mtx_unlock(&sc->sc_mutex);

          ch = bus_read_1(sc->sc_io_resource, 0);
          if (ch & (1 << led))
                  ch = '1';
          else
                  ch = '0';

          error = uiomove(&ch, 1, uio);
          return (error);
  }

  static int
  led_write(struct cdev *dev, struct uio *uio, int ioflag)
  {
          int led = dev2unit(dev) & 0xff;
          struct led_softc *sc = dev->si_drv1;
          u_int8_t ch;
          u_int8_t old;
          int error;

          if (led >= LED_NUM)
                  return (ENXIO);

          error = uiomove(&ch, 1, uio);
          if (error)
                  return (error);

          old = bus_read_1(sc->sc_io_resource, 0);
          if (ch & 1)
                  old |= (1 << led);
          else
                  old &= ˜(1 << led);

          bus_write_1(sc->sc_io_resource, 0, old);

          return (error);
  }

  static void
  led_identify(driver_t *driver, device_t parent)
  {
          device_t child;

          child = device_find_child(parent, "led", −1);
          if (!child) {
                  child = BUS_ADD_CHILD(parent, 0, "led", −1);
                  bus_set_resource(child, SYS_RES_IOPORT, 0, LED_IO_ADDR, 1);
          }
  }

  static int
  led_probe(device_t dev)
  {
          if (!bus_get_resource_start(dev, SYS_RES_IOPORT, 0))
                  return (ENXIO);

          device_set_desc(dev, "I/O Port Example");
          return (BUS_PROBE_SPECIFIC);
  }

  static int
  led_attach(device_t dev)
  {
          struct led_softc *sc = device_get_softc(dev);

          sc->sc_io_rid = 0;
          sc->sc_io_resource = bus_alloc_resource_any(dev, SYS_RES_IOPORT,
              &sc->sc_io_rid, RF_ACTIVE);
          if (!sc->sc_io_resource) {
                  device_printf(dev, "unable to allocate resource\n");
                  return (ENXIO);
          }

          sc->sc_open_mask = 0;
          sc->sc_read_mask = 0;
          mtx_init(&sc->sc_mutex, "led", NULL, MTX_DEF);

          sc->sc_cdev0 = make_dev(&led_cdevsw, 0, UID_ROOT, GID_WHEEL, 0644,
              "led0");
          sc->sc_cdev1 = make_dev(&led_cdevsw, 1, UID_ROOT, GID_WHEEL, 0644,
              "led1");
          sc->sc_cdev0->si_drv1 = sc;
          sc->sc_cdev1->si_drv1 = sc;

          return (0);
  }

  static int
  led_detach(device_t dev)
  {
          struct led_softc *sc = device_get_softc(dev);

          destroy_dev(sc->sc_cdev0);
          destroy_dev(sc->sc_cdev1);

          mtx_destroy(&sc->sc_mutex);

          bus_release_resource(dev, SYS_RES_IOPORT, sc->sc_io_rid,
              sc->sc_io_resource);

          return (0);
  }

  static device_method_t led_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_identify,      led_identify),
          DEVMETHOD(device_probe,         led_probe),
          DEVMETHOD(device_attach,        led_attach),
          DEVMETHOD(device_detach,        led_detach),
          { 0, 0 }
  };

  static driver_t led_driver = {
          "led",
          led_methods,
          sizeof(struct led_softc)
  };

  DRIVER_MODULE(led, isa, led_driver, led_devclass, 0, 0);

在我描述 示例 10-1 中定义的函数之前,请注意常量 LED_IO_ADDR 被定义为 0x404c,而常量 LED_NUM 被定义为 2

以下章节按它们大致执行的顺序描述了在 示例 10-1 中定义的函数。

led_identify 函数

led_identify 函数是本驱动程序的 device_identify 实现。这个函数是必需的,因为 ISA 总线无法在没有辅助的情况下识别其子设备。以下是 led_identify 函数的定义(再次):

static void
led_identify(driver_t *driver, device_t parent)
{
        device_t child;

        child = device_find_child(parent, "led", −1);
        if (!child) {
                child = BUS_ADD_CHILD(parent, 0, "led", −1);
              bus_set_resource(child, SYS_RES_IOPORT, 0, LED_IO_ADDR, 1);
        }
}

此函数首先确定 ISA 总线是否已识别名为 led 的子设备。如果没有,则将 "led" 添加到 ISA 总线的已识别子设备目录中。之后,调用 bus_set_resource 指定 "led" 的 I/O 端口访问从 LED_IO_ADDR 开始。

led_probe 函数

led_probe 函数是本驱动程序的 device_probe 实现。以下是它的函数定义(再次):

static int
led_probe(device_t dev)
{
        if (!bus_get_resource_start(dev, SYS_RES_IOPORT, 0))
                return (ENXIO);

        device_set_desc(dev, "I/O Port Example");
        return (BUS_PROBE_SPECIFIC);
}

此函数首先检查 "led" 是否可以获取 I/O 端口访问权限。之后,设置 "led" 的详细描述 详细描述 并返回成功代码 BUS_PROBE_SPECIFIC

led_attach 函数

led_attach 函数是本驱动程序的 device_attach 实现。以下是它的函数定义(再次):

static int
led_attach(device_t dev)
{
        struct led_softc *sc = device_get_softc(dev);

        sc->sc_io_rid = 0;
        sc->sc_io_resource = bus_alloc_resource_any(dev, SYS_RES_IOPORT,
            &sc->sc_io_rid, RF_ACTIVE);
        if (!sc->sc_io_resource) {
                device_printf(dev, "unable to allocate resource\n");
              return (ENXIO);
        }

      sc->sc_open_mask = 0;
      sc->sc_read_mask = 0;
        mtx_init(&sc->sc_mutex, "led", NULL, MTX_DEF);

        sc->sc_cdev0 = make_dev(&led_cdevsw, 0, UID_ROOT, GID_WHEEL, 0644,
            "led0");
        sc->sc_cdev1 = make_dev(&led_cdevsw, 1, UID_ROOT, GID_WHEEL, 0644,
            "led1");
        sc->sc_cdev0->si_drv1 = sc;
        sc->sc_cdev1->si_drv1 = sc;

        return (0);
}

此函数首先尝试获取一个 I/O 端口。如果失败,则返回错误代码 ENXIO。然后,成员变量 sc_open_masksc_read_mask 被置零;在 d_foo 函数中,这些变量将由 sc_mutex 保护。最后,led_attach 为每个 LED 创建一个 字符设备节点 字符设备节点

led_detach 函数

led_detach 函数是本驱动程序的 device_detach 实现。以下是它的函数定义(再次):

static int
led_detach(device_t dev)
{
        struct led_softc *sc = device_get_softc(dev);

      destroy_dev(sc->sc_cdev0);
      destroy_dev(sc->sc_cdev1);

      mtx_destroy(&sc->sc_mutex);

      bus_release_resource(dev, SYS_RES_IOPORT, sc->sc_io_rid,
            sc->sc_io_resource);

        return (0);
}

此函数首先销毁其设备节点。一旦完成,它销毁其互斥锁并释放其 I/O 端口。

led_open 函数

led_open 函数定义在 led_cdevsw(即字符设备切换表)中,作为 d_open 操作。以下是它的函数定义(再次):

static int
led_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
      int led = dev2unit(dev) & 0xff;
        struct led_softc *sc = dev->si_drv1;

      if (led >= LED_NUM)
                return (ENXIO);

        mtx_lock(&sc->sc_mutex);
      if (sc->sc_open_mask & (1 << led)) {
                mtx_unlock(&sc->sc_mutex);
                return (EBUSY);
        }
      sc->sc_open_mask |= 1 << led;
      sc->sc_read_mask |= 1 << led;
        mtx_unlock(&sc->sc_mutex);

        return (0);
}

此函数首先将打开的设备节点的单元号存储在 led 中。如果 led 大于或等于 LED_NUM,则返回 ENXIO。接下来,检查 sc_open_mask 的值。如果其 led 位不等于 0,这表示另一个进程已打开该设备,则返回 EBUSY。否则,将 sc_open_masksc_read_mask 设置为包含 1 << led。也就是说,它们的 led 位将被更改为 1

led_close 函数

led_close 函数定义在 led_cdevsw 中,作为 d_close 操作。以下是它的函数定义(再次):

static int
led_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        int led = dev2unit(dev) & 0xff;
        struct led_softc *sc = dev->si_drv1;

        if (led >= LED_NUM)
                return (ENXIO);

        mtx_lock(&sc->sc_mutex);
      sc->sc_open_mask &= ˜(1 << led);
        mtx_unlock(&sc->sc_mutex);

        return (0);
}

如您所见,此函数只是简单地清除 sc_open_maskled 位(这允许另一个进程打开此设备)。

led_read 函数

led_read 函数定义在 led_cdevsw 中,作为 d_read 操作。此函数返回一个字符,指示 LED 是开启(1)还是关闭(0)。以下是它的函数定义(再次):

static int
led_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        int led = dev2unit(dev) & 0xff;
        struct led_softc *sc = dev->si_drv1;
        u_int8_t ch;
        int error;

        if (led >= LED_NUM)
                return (ENXIO);

        mtx_lock(&sc->sc_mutex);
        /* No error EOF condition. */
      if (!(sc->sc_read_mask & (1 << led))) {
                mtx_unlock(&sc->sc_mutex);
              return (0);
        }
        sc->sc_read_mask &= ˜(1 << led);
        mtx_unlock(&sc->sc_mutex);

      ch = bus_read_1(sc->sc_io_resource, 0);
      if (ch & (1 << led))
                ch = '1';
        else
                ch = '0';

        error = uiomove(&ch, 1, uio);
        return (error);
}

此函数首先检查 sc_read_maskled 位是否已设置;否则,它退出。接下来,从 LED 的控制寄存器中读取 1 个字节到 ch。然后,将 chled 位隔离,并将其值返回到用户空间。

led_write 函数

led_write 函数定义在 led_cdevsw 中,作为 d_write 操作。此函数接收一个字符来打开(1)或关闭(0)LED。以下是它的函数定义(再次):

static int
led_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        int led = dev2unit(dev) & 0xff;
        struct led_softc *sc = dev->si_drv1;
        u_int8_t ch;
        u_int8_t old;
        int error;

        if (led >= LED_NUM)
                return (ENXIO);

        error = uiomove(&ch, 1, uio);
        if (error)
                return (error);

      old = bus_read_1(sc->sc_io_resource, 0);
      if (ch & 1)
              old |= (1 << led);
        else
              old &= ˜(1 << led);

      bus_write_1(sc->sc_io_resource, 0, old);

        return (error);
}

此函数首先从用户空间复制一个字符到ch。接下来,从 LED 的控制寄存器中读取 1 个字节到old。然后,根据用户空间中的值,将oldled位打开或关闭。之后,将old写回 LED 的控制寄存器。

结论

本章描述了 FreeBSD 提供的所有用于执行 PMIO 和 MMIO(即访问设备寄存器)的函数。下一章将讨论使用 PMIO 和 MMIO 与 PCI 设备,这比这里展示的要复杂得多。

第十一章. 案例研究:智能平台管理接口驱动程序

无标题图片

本章探讨了 ipmi(4) 的部分内容,即智能平台管理接口 (IPMI) 驱动程序。IPMI 规范定义了用于监控和管理系统硬件的标准。

注意

对于我们的目的来说,这个 IPMI 的描述就足够了,因为本章的目的是展示 PCI 驱动程序如 ipmi(4) 如何使用 PMIO 和 MMIO。

ipmi(4) 的代码库由 10 个源文件和 1 个头文件组成。在本章中,我们将遍历其中一个文件,ipmi_pci.c,其中包含与 PCI 总线相关的代码。

代码分析

示例 11-1 提供了 ipmi_pci.c 的简洁、源代码级别的概述。

示例 11-1. ipmi_pci.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/bus.h>
  #include <sys/condvar.h>
  #include <sys/eventhandler.h>
  #include <sys/selinfo.h>

  #include <machine/bus.h>
  #include <sys/rman.h>
  #include <machine/resource.h>

  #include <dev/pci/pcireg.h>
  #include <dev/pci/pcivar.h>

  #include <dev/ipmi/ipmivars.h>

  static struct ipmi_ident {
          u_int16_t       vendor;
          u_int16_t       device;
          char            *description;
  } ipmi_identifiers[] = {
          { 0x1028, 0x000d, "Dell PE2650 SMIC interface" },
          { 0, 0, 0 }
  };

  const char *
  ipmi_pci_match(uint16_t vendor, uint16_t device)
  {
  ...
  }

  static int
  ipmi_pci_probe(device_t dev)
  {
  ...
  }

  static int
  ipmi_pci_attach(device_t dev)
  {
  ...
  }

  static device_method_t ipmi_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_probe,         ipmi_pci_probe),
          DEVMETHOD(device_attach,        ipmi_pci_attach),
          DEVMETHOD(device_detach,        ipmi_detach),
          { 0, 0 }
  };

  static driver_t ipmi_pci_driver = {
          "ipmi",
          ipmi_methods,
          sizeof(struct ipmi_softc)
  };

 DRIVER_MODULE(ipmi_pci, pci, ipmi_pci_driver, ipmi_devclass, 0, 0);

  static int
  ipmi2_pci_probe(device_t dev)
  {
  ...
  }

  static int
  ipmi2_pci_attach(device_t dev)
  {
  ...
  }

  static device_method_t ipmi2_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_probe,         ipmi2_pci_probe),
          DEVMETHOD(device_attach,        ipmi2_pci_attach),
          DEVMETHOD(device_detach,        ipmi_detach),
          { 0, 0 }
  };

  static driver_t ipmi2_pci_driver = {
          "ipmi",
          ipmi2_methods,
          sizeof(struct ipmi_softc)
  };

 DRIVER_MODULE(ipmi2_pci, pci, ipmi2_pci_driver, ipmi_devclass, 0, 0);

在我描述 示例 11-1 中的函数之前,请注意它包含两个 DRIVER_MODULE 调用。换句话说,示例 11-1 声明了两个 Newbus 驱动程序;每个都设计用来处理一组不同的设备(正如你很快就会看到的)。

现在,让我们讨论 示例 11-1 中找到的函数。

ipmi_pci_probe 函数

ipmi_pci_probe 函数是 示例 11-1 中找到的第一个 Newbus 驱动程序的 device_probe 实现。以下是它的函数定义:

static int
ipmi_pci_probe(device_t dev)
{
        const char *desc;

      if (ipmi_attached)
              return (ENXIO);

        desc = ipmi_pci_match(pci_get_vendor(dev), pci_get_device(dev));
        if (desc != NULL) {
                device_set_desc(dev, desc);
                return (BUS_PROBE_DEFAULT);
        }

        return (ENXIO);
}

此函数首先检查全局变量 ipmi_attached 的值。如果它不为零,这意味着 ipmi(4) 当前正在使用中,将返回错误代码 ENXIO;否则,将调用 ipmi_pci_match 来确定此驱动程序是否可以处理 dev

ipmi_pci_match 函数

ipmi_pci_match 函数接收一个 PCI 供应商 ID/设备 ID (VID/DID) 对,并验证它是否识别这些 ID。在我定义(随后将逐步介绍)此函数之前,需要描述 ipmi_identifiers 数组。此数组在 示例 11-1 的开头附近定义,如下所示:

static struct ipmi_ident {
        u_int16_t       vendor;
        u_int16_t       device;
        char            *description;
} ipmi_identifiers[] = {
        { 0x1028, 0x000d, "Dell PE2650 SMIC interface" },
        { 0, 0, 0 }
};

如你所见,ipmi_identifiers 数组由 ipmi_ident 结构组成。每个 ipmi_ident 结构包括一个 VID/DID 对 和一个 PCI 设备的描述。正如你可能猜到的,ipmi_identifiers 列出了 示例 11-1 中第一个 Newbus 驱动支持的设备。

既然我们已经讨论了 ipmi_identifiers,让我们来了解一下 ipmi_pci_match

const char *
ipmi_pci_match(uint16_t vendor, uint16_t device)
{
        struct ipmi_ident *m;

      for (m = ipmi_identifiers; m->vendor != 0; m++)
                if (m->vendor == vendor && m->device == device)
                        return (m->description);

        return (NULL);
}

此函数确定特定的 VID/DID 对是否列在 ipmi_identifiers 中。如果是,则返回其 描述

ipmi_pci_attach 函数

ipmi_pci_attach 函数是 示例 11-1 中找到的第一个 Newbus 驱动的 device_attach 实现。以下是它的函数定义:

static int
ipmi_pci_attach(device_t dev)
{
        struct ipmi_softc *sc = device_get_softc(dev);
        struct ipmi_get_info info;
        const char *mode;
        int error, type;

      if (!ipmi_smbios_identify(&info))
                return (ENXIO);

        sc->ipmi_dev = dev;

      switch (info.iface_type) {
        case KCS_MODE:
                mode = "KCS";
                break;
        case SMIC_MODE:
                mode = "SMIC";
                break;
        case BT_MODE:
                device_printf(dev, "BT mode is unsupported\n");
                return (ENXIO);
        default:
                device_printf(dev, "No IPMI interface found\n");
                return (ENXIO);
        }

        device_printf(dev,
            "%s mode found at %s 0x%jx alignment 0x%x on %s\n",
            mode,
            info.io_mode ? "I/O port" : "I/O memory",
            (uintmax_t)info.address,
            info.offset,
            device_get_name(device_get_parent(dev)));

        if (info.io_mode)
              type = SYS_RES_IOPORT;
        else
              type = SYS_RES_MEMORY;

        sc->ipmi_io_rid = PCIR_BAR(0);
        sc->ipmi_io_res[0] = bus_alloc_resource_any(dev, type,
          &sc->ipmi_io_rid, RF_ACTIVE);
        sc->ipmi_io_type = type;
        sc->ipmi_io_spacing = info.offset;

        if (sc->ipmi_io_res[0] == NULL) {
                device_printf(dev, "could not configure PCI I/O resource\n");
                return (ENXIO);
        }

        sc->ipmi_irq_rid = 0;
        sc->ipmi_irq_res = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->ipmi_irq_rid, RF_SHAREABLE | RF_ACTIVE);

        switch (info.iface_type) {
        case KCS_MODE:
                error = ipmi_kcs_attach(sc);
                if (error)
                        goto bad;
                break;
        case SMIC_MODE:
                error = ipmi_smic_attach(sc);
                if (error)
                        goto bad;
                break;
        }

        error = ipmi_attach(dev);
        if (error)
                goto bad;

        return (0);

bad:
        ipmi_release_resources(dev);
        return (error);
}

此函数首先 检索 计算机中存储的 IPMI 数据结构,该结构位于 系统管理 BIOS (SMBIOS) 中,负责维护硬件配置信息。

根据 SMBIOS 数据,ipmi_pci_attach 确定操作模式 ipmi(4)模式 以及是否需要 I/O 端口I/O 内存访问。目前,ipmi(4) 仅支持键盘控制器样式 (KCS) 和服务器管理接口芯片 (SMIC) 模式。这些模式决定了 IPMI 消息的传输方式。就我们的目的而言,你不需要了解这两种模式的细节。

下一段代码获取 ipmi(4) 的 I/O 区域访问权限。在描述此代码之前,需要了解一些关于 PCI 设备的背景信息。启动后,PCI 设备可以将它们的设备寄存器重新映射到不同的位置,从而避免与其他设备发生地址冲突。因此,PCI 设备将它们的 I/O 映射寄存器的大小和当前位置存储在其基本地址寄存器 (BARs) 中。因此,此代码块首先调用 PCIR_BAR(0) 以获取第一个 BAR 的地址。然后,它将此地址作为 rid 参数传递给 bus_alloc_resource_any,从而获取对设备寄存器的 I/O 访问权限。

注意

为了准确起见,PCIR_BAR(x) 宏返回第 x 个 BAR 的 RID。

ipmi_pci_attach 函数的剩余部分获取一个中断请求(IRQ),启动 KCS 或 SMIC 模式,并调用 ipmi_attach 以完成设备的初始化。

ipmi2_pci_probe 函数

ipmi2_pci_probe 函数是 示例 11-1 中找到的第二个 Newbus 驱动程序的 device_probe 实现。以下是其函数定义:

static int
ipmi2_pci_probe(device_t dev)
{
        if (pci_get_class(dev) == PCIC_SERIALBUS &&
            pci_get_subclass(dev) == PCIS_SERIALBUS_IPMI) {
                device_set_desc(dev, "IPMI System Interface");
                return (BUS_PROBE_GENERIC);
        }

        return (ENXIO);
}

此函数确定 dev 是否是 PCI 总线上的通用 IPMI 设备。如果是,则设置其详细描述,并返回成功代码 BUS_PROBE_GENERIC。简而言之,此驱动程序处理 PCI 总线上的任何标准 IPMI 设备。

如你所猜,第一个 Newbus 驱动程序是为 Dell PE2650 的一种变通方法(即一种解决方案),因为它不遵循 IPMI 规范。

ipmi2_pci_attach 函数

ipmi2_pci_attach 函数是 示例 11-1 中找到的第二个 Newbus 驱动程序的 device_attach 实现。以下是其函数定义:

static int
ipmi2_pci_attach(device_t dev)
{
        struct ipmi_softc *sc = device_get_softc(dev);
        int error, iface, type;

        sc->ipmi_dev = dev;

      switch (pci_get_progif(dev)) {
        case PCIP_SERIALBUS_IPMI_SMIC:
                iface = SMIC_MODE;
                break;
        case PCIP_SERIALBUS_IPMI_KCS:
                iface = KCS_MODE;
                break;
        case PCIP_SERIALBUS_IPMI_BT:
                device_printf(dev, "BT interface is unsupported\n");
                return (ENXIO);
        default:
                device_printf(dev, "unsupported interface: %d\n",
                    pci_get_progif(dev));
                return (ENXIO);
        }

        sc->ipmi_io_rid = PCIR_BAR(0);
      if (PCI_BAR_IO(pci_read_config(dev, PCIR_BAR(0), 4)))
              type = SYS_RES_IOPORT;
        else
              type = SYS_RES_MEMORY;
        sc->ipmi_io_type = type;
        sc->ipmi_io_spacing = 1;
        sc->ipmi_io_res[0] = bus_alloc_resource_any(dev, type,
            &sc->ipmi_io_rid, RF_ACTIVE);
        if (sc->ipmi_io_res[0] == NULL) {
                device_printf(dev, "could not configure PCI I/O resource\n");
                return (ENXIO);
        }

        sc->ipmi_irq_rid = 0;
        sc->ipmi_irq_res = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->ipmi_irq_rid, RF_SHAREABLE | RF_ACTIVE);

        switch (iface) {
        case KCS_MODE:
                device_printf(dev, "using KCS interface\n");

                if (!ipmi_kcs_probe_align(sc)) {
                        device_printf(dev,
                            "unable to determine alignment\n");
                        error = ENXIO;
                        goto bad;
                }

                error = ipmi_kcs_attach(sc);
                if (error)
                        goto bad;
                break;
        case SMIC_MODE:
                device_printf(dev, "using SMIC interface\n");

                error = ipmi_smic_attach(sc);
                if (error)
                        goto bad;
                break;
        }

        error = ipmi_attach(dev);
        if (error)
                goto bad;

        return (0);

bad:
        ipmi_release_resources(dev);
        return (error);
}

此函数首先检查 dev 的编程接口以确定 ipmi(4) 的工作模式(SMIC 或 KCS)。然后调用 PCIR_BAR(0) 获取第一个 BAR 的地址。从这个 BAR 开始,ipmi2_pci_attach 确定在获取之前 ipmi(4) 是否需要 I/O 端口或 I/O 内存访问。最后,ipmi2_pci_attach 获取一个中断请求,启动 KCS 或 SMIC 模式,并调用 ipmi_attach 以完成 dev 的初始化。

结论

本章检查了 ipmi(4) 的 PCI 代码库,并介绍了两个基本概念。首先,一个源文件可以包含多个驱动程序。其次,为了获取 I/O 区域访问权限,PCI 驱动程序必须首先调用 PCIR_BAR

第十二章. 直接内存访问

无标题图片

直接内存访问(DMA)是现代处理器的一个特性,允许设备独立于 CPU 将数据传输到和从主内存。使用 DMA 时,CPU 仅初始化数据传输(也就是说,它不会完成它),然后设备(或单独的 DMA 控制器)实际移动数据。正因为如此,DMA 往往能提供更高的系统性能,因为 CPU 在数据传输期间可以自由执行其他任务。

注意

执行 DMA 操作会有一些开销。因此,只有移动大量数据(例如,存储设备)的设备才使用 DMA。你不会仅仅为了传输一两个字节的数据就使用 DMA。

实现 DMA

与之前的话题不同,我将在这里采取整体的方法。也就是说,我将首先展示一个示例,然后我将描述 DMA 函数系列。

以下伪代码是一个使用 DMA 的虚构设备的device_attach例程。

static int
foo_attach(device_t dev)
{
        struct foo_softc *sc = device_get_softc(dev);
        int error;

        bzero(sc, sizeof(*sc));

        if (bus_dma_tag_create(bus_get_dma_tag(dev),  /* parent       */
                               1,                       /* alignment    */
                               0,                       /* boundary     */
                               BUS_SPACE_MAXADDR,       /* lowaddr      */
                               BUS_SPACE_MAXADDR,       /* highaddr     */
                               NULL,                    /* filter       */
                               NULL,                    /* filterarg    */
                               BUS_SPACE_MAXSIZE_32BIT, /* maxsize      */
                               BUS_SPACE_UNRESTRICTED,  /* nsegments    */
                               BUS_SPACE_MAXSIZE_32BIT, /* maxsegsize   */
                               0,                       /* flags        */
                               NULL,                    /* lockfunc     */
                               NULL,                    /* lockfuncarg  */
                             &sc->foo_parent_dma_tag)) {
                device_printf(dev, "Cannot allocate parent DMA tag!\n");
                return (ENOMEM);
        }

        if (bus_dma_tag_create(sc->foo_parent_dma_tag,/* parent       */
                               1,                       /* alignment    */
                               0,                       /* boundary     */
                               BUS_SPACE_MAXADDR,       /* lowaddr      */
                               BUS_SPACE_MAXADDR,       /* highaddr     */
                               NULL,                    /* filter       */
                               NULL,                    /* filterarg    */
                               MAX_BAZ_SIZE,            /* maxsize      */
                               MAX_BAZ_SCATTER,         /* nsegments    */
                               BUS_SPACE_MAXSIZE_32BIT, /* maxsegsize   */
                               0,                       /* flags        */
                               NULL,                    /* lockfunc     */
                               NULL,                    /* lockfuncarg  */
                             &sc->foo_baz_dma_tag)) {
                device_printf(dev, "Cannot allocate baz DMA tag!\n");
                return (ENOMEM);
        }

        if (bus_dmamap_create(sc->foo_baz_dma_tag,      /* DMA tag      */
                              0,                        /* flags        */
                              &sc->foo_baz_dma_map)) {
                device_printf(dev, "Cannot allocate baz DMA map!\n");
                return (ENOMEM);
        }

        bzero(sc->foo_baz_buf, BAZ_BUF_SIZE);

        error = bus_dmamap_load(sc->foo_baz_dma_tag,  /* DMA tag      */
                              sc->foo_baz_dma_map,    /* DMA map      */
                              sc->foo_baz_buf,        /* buffer       */
                                BAZ_BUF_SIZE,           /* buffersize   */
                              foo_callback,           /* callback     */
                                &sc->foo_baz_busaddr,   /* callbackarg  */
                                BUS_DMA_NOWAIT);        /* flags        */
        if (error || sc->foo_baz_busaddr == 0) {
                device_printf(dev, "Cannot map baz DMA memory!\n");
                return (ENOMEM);
        }

...
}

这个伪代码首先调用bus_dma_tag_create来创建一个名为foo_parent_dma_tag的 DMA 标签。本质上,DMA 标签描述了 DMA 事务的特性与限制。

接下来,再次调用bus_dma_tag_create。注意,foo_parent_dma_tag是这个调用参数的第一个参数。看,DMA 标签可以继承其他标签的特性与限制。当然,子标签不能放宽其父标签设置的限制。因此,DMA 标签foo_baz_dma_tagfoo_parent_dma_tag的“严苛”版本。

下一个语句bus_dmamap_create创建了一个名为foo_baz_dma_map的 DMA 映射。粗略地说,DMA 映射代表根据 DMA 标签属性分配的内存区域,并且位于设备可见地址空间内。

最后,bus_dmamap_load将缓冲区foo_baz_buf加载到与 DMA 映射foo_baz_dma_map关联的设备可见地址。

注意

任何任意的缓冲区都可以用于 DMA。然而,缓冲区在加载(或映射)到设备可见地址(即 DMA 映射)之前对设备是不可访问的。

注意bus_dmamap_load需要一个回调函数,通常看起来像这样:

static void
 foo_callback(void *arg, bus_dma_segment_t *segs, int nseg, int error)
{
        if (error)
               return;

        *(bus_addr_t *)arg = segs[0].ds_addr;
}

在这里,arg解引用到传递给bus_dmamap_load的第六个参数,即foo_baz_busaddr

此回调函数在缓冲区加载操作完成后执行。如果成功,httpatomoreillycomsourcenostarchimages1137507.png 缓冲区被加载的地址将返回在 httpatomoreillycomsourcenostarchimages1137505.png arg 中。如果失败,httpatomoreillycomsourcenostarchimages1137499.png foo_callback 不会做任何事情。

启动 DMA 数据传输

假设缓冲区加载操作成功完成,可以使用类似以下方式启动 DMA 数据传输:

注意

大多数设备只需要将缓冲区的设备可见地址写入特定寄存器以启动 DMA 数据传输。

bus_write_4(sc->foo_io_resource, FOO_BAZ, sc->foo_baz_busaddr);

在这里,缓冲区的设备可见地址 httpatomoreillycomsourcenostarchimages1137503.png 被写入 httpatomoreillycomsourcenostarchimages1137499.png 设备寄存器。回想一下,前一小节中描述的 foo_callback 函数在 httpatomoreillycomsourcenostarchimages1137503.png foo_baz_busaddr 中返回了 foo_baz_buf 的设备可见地址。

拆卸 DMA

现在你已经知道了如何实现 DMA,我将演示如何拆卸它。

static int
foo_detach(device_t dev)
{
        struct foo_softc *sc = device_get_softc(dev);

        if (sc->foo_baz_busaddr != 0)
                bus_dmamap_unload(sc->foo_baz_dma_tag, sc->foo_baz_dma_map);

        if (sc->foo_baz_dma_map != NULL)
                bus_dmamap_destroy(sc->foo_baz_dma_tag, sc->foo_baz_dma_map);

        if (sc->foo_baz_dma_tag != NULL)
                bus_dma_tag_destroy(sc->foo_baz_dma_tag);

        if (sc->foo_parent_dma_tag != NULL)
                bus_dma_tag_destroy(sc->foo_parent_dma_tag);

...
}

如你所见,这段伪代码只是以相反的顺序拆除了构建时的一切。

现在,让我们详细讨论在这里和前两个部分遇到的不同函数。

创建 DMA 标签

如前所述,DMA 标签描述了 DMA 事务的特性及限制,并通过使用 bus_dma_tag_create 函数创建。

#include <machine/bus.h>

int
bus_dma_tag_create(bus_dma_tag_t parent, bus_size_t alignment,
    bus_size_t boundary, bus_addr_t lowaddr, bus_addr_t highaddr,
    bus_dma_filter_t *filtfunc, void *filtfuncarg, bus_size_t maxsize,
    int nsegments, bus_size_t maxsegsz, int flags,
    bus_dma_lock_t *lockfunc, void *lockfuncarg, bus_dma_tag_t *dmat);

在这里,parent 参数标识了父 DMA 标签。要创建顶级 DMA 标签,请将 bus_get_dma_tag(device_t dev) 作为 parent 传递。

alignment 参数表示每个 DMA 段的物理对齐,单位为字节。回想一下,DMA 映射表示根据 DMA 标签属性已分配的内存区域。这些内存区域被称为 DMA 段。如果你回到 实现 DMA 中描述的 foo_callback 函数,你会看到 arg 实际上被分配了一个 DMA 段的地址。

alignment 参数必须是 1,表示没有特定的对齐,或者是一个 2 的幂。例如,需要 DMA 缓冲区从 4KB 的倍数开始的驱动程序会将 4096 作为 alignment 传递。

boundary 参数指定了每个 DMA 段不能跨越的物理地址边界;也就是说,它们不能跨越任何 boundary 的倍数。此参数必须是 0,表示没有边界限制,或者是一个 2 的幂。

lowaddrhighaddr 参数定义了不能用于 DMA 的地址范围。例如,无法进行超过 4GB DMA 的设备将 0xFFFFFFFF 作为 lowaddr,将 BUS_SPACE_MAXADDR 作为 highaddr

注意

0xFFFFFFFF 等于 4GB,常量 BUS_SPACE_MAXADDR 表示您架构可寻址的最大内存。

filtfuncfiltfuncarg 参数分别表示一个可选的回调函数及其第一个参数。此函数在尝试在 lowaddrhighaddr 之间加载(或映射)DMA 缓冲区时执行。如果在 lowaddrhighaddr 之间存在设备可访问区域,filtfunc 应该通知系统。以下是 filtfunc 的函数原型:

int filtfunc(void *filtfuncarg, bus_addr_t addr)

如果地址 addr 可被设备访问,则此函数必须返回 0;如果它不可访问,则返回非零值。

如果 filtfuncfiltfuncargNULL,则从 lowaddrhighaddr 的整个地址范围被认为是不可访问的。

maxsize 参数表示单个 DMA 映射可能分配的最大内存量,以字节为单位。

nsegments 参数指定了在单个 DMA 映射中允许的散列/收集段的数量。一个 散列/收集段 简单地就是一个内存页面。这个名字来源于当你将一组物理上不连续的页面虚拟地组装成一个单一的连续缓冲区时,你必须“散列”你的写入和“收集”你的读取。某些设备需要连续的内存块;然而,有时可能没有足够大的块可用。因此,内核通过使用由散列/收集段组成的缓冲区来“欺骗”设备。每个 DMA 段都是一个散列/收集段。

nsegments 参数可以是 BUS_SPACE_UNRESTRICTED,这表示没有数量限制。使用 BUS_SPACE_UNRESTRICTED 创建的 DMA 标签不能创建 DMA 映射;它们只能作为父标签,因为系统无法支持由无限数量的散列/收集段组成的 DMA 映射。

maxsegsz 参数表示单个 DMA 映射中单个 DMA 段的最大大小,以字节为单位。

flags 参数修改 bus_dma_tag_create 的行为。表 12-1 显示了它的唯一有效值。

表 12-1. bus_dma_tag_create 符号常量

常量 描述
BUS_DMA_ALLOCNOW 预分配足够的资源来处理至少一个缓冲区加载操作;如果资源不足,则返回 ENOMEM

lockfunclockfuncarg 参数分别表示一个可选的回调函数及其第一个参数。还记得 bus_dmamap_load 需要一个回调函数吗?嗯,lockfunc 在该函数之前和之后执行以获取和释放任何必要的同步原语。以下是 lockfunc 的函数原型:

void lockfunc(void *lockfuncarg, bus_dma_lock_op_t op)

lockfunc 执行时, 操作包含 BUS_DMA_LOCKBUS_DMA_UNLOCK。也就是说,op 决定了要执行哪种锁定操作。

dmat 参数期望一个指向 bus_dma_tag_t 的指针;假设 bus_dma_tag_create 成功,此指针将存储生成的 DMA 标签。

拆卸 DMA 标签

DMA 标签通过 bus_dma_tag_destroy 函数被拆解。

#include <machine/bus.h>

int
bus_dma_tag_destroy(bus_dma_tag_t dmat);

如果有任何 DMA 映射仍然与 dmat 关联,此函数将返回 EBUSY

DMA 映射管理例程,第一部分

如前所述,DMA 映射代表根据 DMA 标签属性分配的内存区域(即 DMA 段),并且位于设备可见地址空间内。

DMA 映射可以使用以下功能进行管理:

#include <machine/bus.h>

int
bus_dmamap_create(bus_dma_tag_t dmat, int flags, bus_dmamap_t *mapp);

int
bus_dmamap_destroy(bus_dma_tag_t dmat, bus_dmamap_t map);

bus_dmamap_create 函数根据 DMA 标签 dmat 创建 DMA 映射,并将结果存储在 mapp 中。flags 参数修改 bus_dmamap_create 的行为。表 12-2 显示其唯一有效的值。

表 12-2. bus_dmamap_create 符号常量

常量 描述
BUS_DMA_COHERENT 使缓存同步操作尽可能便宜,适用于您的 DMA 缓冲区;此标志仅在 sparc64 上实现。

bus_dmamap_destroy 函数拆解 DMA 映射 mapdmat 参数是 map 所基于的 DMA 标签。

将 (DMA) 缓冲区加载到 DMA 映射中

FreeBSD 内核提供了四个函数,用于将缓冲区加载到与 DMA 映射关联的设备可见地址:

  • bus_dmamap_load

  • bus_dmamap_load_mbuf

  • bus_dmamap_load_mbuf_sg

  • bus_dmamap_load_uio

在描述这些函数之前,需要解释 bus_dma_segment 结构。

bus_dma_segment 结构

一个 bus_dma_segment 结构描述一个单独的 DMA 段。

typedef struct bus_dma_segment {
        bus_addr_t     ds_addr;
        bus_size_t     ds_len;
} bus_dma_segment_t;

ds_addr 字段包含其设备可见地址,ds_len 包含其长度。

bus_dmamap_load 函数

我们首先在 实现 DMA 中讨论了 bus_dmamap_load 函数。实现 DMA。

#include <machine/bus.h>

int
bus_dmamap_load(bus_dma_tag_t dmat, bus_dmamap_t map, void *buf,
    bus_size_t buflen, bus_dmamap_callback_t *callback,
    void *callbackarg, int flags);

此函数将缓冲区 buf 加载到与 DMA 映射 map 关联的设备可见地址。dmat 参数是 map 所基于的 DMA 标签。buflen 参数是要从 buf 加载的字节数。bus_dmamap_load 立即返回,并且不会因任何原因而阻塞。

callbackcallbackarg 参数分别表示回调函数及其第一个参数。callback 在缓冲区加载操作完成后执行。如果资源不足,缓冲区加载操作和 callback 将被延迟。如果 bus_dmamap_load 返回 EINPROGRESS,则已发生这种情况。以下是 callback 的函数原型:

void callback(void *callbackarg, bus_dma_segment_t *segs, int
 nseg,              int error)

当回调执行时,错误 错误显示缓冲区加载操作的成功(0)或失败(EFBIG)。错误代码 EFBIG 代表 错误:文件过大段 segs 参数是 buf 已加载到的 DMA 段的数组;段数 nseg 是此数组的大小。

以下伪代码是一个示例 callback 函数:

static void
foo_callback(void *callbackarg, bus_dma_segment_t *segs, int nseg, int error)
{
        struct foo_softc *sc = callbackarg;
        int i;

        if (error)
                return;

        sc->sg_num = nseg;
      for (i = 0; i < nseg; i++)
                sc->sg_addr[i] = segs[i].ds_addr;
}

此函数 段 遍历 segs 以返回 buf 已加载到的每个 DMA 段的设备可见地址。

注意

如果 buf 可以适应一个 DMA 段,那么可以在 实现 DMA 中描述的 foo_callback 函数用作 callback

flags 参数修改了 bus_dmamap_load 的行为。此参数的有效值显示在 表 12-3 中。

表 12-3. bus_dmamap_load 符号常量

常量 描述
BUS_DMA_NOWAIT 如果内存资源不足,缓冲区加载操作和 callback不会 被延迟。
BUS_DMA_NOCACHE 防止缓存 DMA 缓冲区,因此所有 DMA 事务都将在不重新排序的情况下执行;此标志仅在 sparc64 上实现。

bus_dmamap_load_mbuf 函数

bus_dmamap_load_mbuf 函数是 bus_dmamap_load 的一个变体,用于加载 mbuf 链(你将在 第十六章 中了解 mbuf 链)。

#include <machine/bus.h>

int
bus_dmamap_load_mbuf(bus_dma_tag_t dmat, bus_dmamap_t map,
    struct mbuf *mbuf, bus_dmamap_callback2_t *callback2,
    void *callbackarg, int flags);

这些参数中的大多数与它们的 bus_dmamap_load 对应参数相同,除了:

  • mbuf 参数,它期望一个 mbuf 链

  • callback2 参数,它需要一个不同的回调函数

  • flags 参数,它隐式设置 BUS_DMA_NOWAIT

这是 callback2 的函数原型:

void callback2(void *callbackarg, bus_dma_segment_t *segs, int nseg,
               bus_size_t mapsize, int error)

callback2callback 类似,但它返回加载的数据量。

bus_dmamap_load_mbuf_sg 函数

bus_dmamap_load_mbuf_sg 函数是 bus_dmamap_load_mbuf 的一个替代方案,它不使用 callback2

#include <machine/bus.h>

int
bus_dmamap_load_mbuf_sg(bus_dma_tag_t dmat, bus_dmamap_t map,
    struct mbuf *mbuf, bus_dma_segment_t *segs, int *nseg, int flags);

如您所见,此函数直接并立即返回 段 segs段数 nseg

bus_dmamap_load_uio 函数

bus_dmamap_load_uio 函数与 bus_dmamap_load_mbuf 相同,但它从 uio 结构内部加载缓冲区。

#include <machine/bus.h>

int
bus_dmamap_load_uio(bus_dma_tag_t dmat, bus_dmamap_t map,
    struct uio *uio, bus_dmamap_callback2_t *callback2,
    void *callbackarg, int flags);

bus_dmamap_unload 函数

bus_dmamap_unload 函数从 DMA 映射中卸载缓冲区。

#include <machine/bus.h>

void
bus_dmamap_unload(bus_dma_tag_t dmat, bus_dmamap_t map);

DMA 映射管理例程,第二部分

本节描述了一组用于管理 DMA 映射的替代函数。

#include <machine/bus.h>

int
bus_dmamem_alloc(bus_dma_tag_t dmat, void **vaddr, int flags,
    bus_dmamap_t *mapp);

void
bus_dmamem_free(bus_dma_tag_t dmat, void *vaddr, bus_dmamap_t map);

bus_dmamem_alloc 函数根据 DMA 标签 dmat 创建 DMA 映射,并将结果存储在 mapp 中。此函数还分配 maxsize 字节的连续内存(其中 maxsizedmat 定义)。此内存的地址返回在 vaddr 中。正如你很快就会看到的,这段连续内存最终将成为你的 DMA 缓冲区。flags 参数修改 bus_dmamem_alloc 的行为。此参数的有效值显示在 表 12-4 中。

表 12-4. bus_dmamem_alloc 符号常量

常量 描述
BUS_DMA_ZERO 导致分配的内存被设置为 0
BUS_DMA_NOWAIT 如果由于资源短缺无法立即满足分配,则 bus_dmamem_alloc 返回 ENOMEM
BUS_DMA_WAITOK 表示可以等待资源;如果分配不能立即满足,当前进程将被挂起以等待资源可用。
BUS_DMA_COHERENT 使 DMA 缓冲区的缓存同步操作尽可能便宜;此标志仅在 arm 和 sparc64 上实现。
BUS_DMA_NOCACHE 防止缓存 DMA 缓冲区,因此导致所有 DMA 事务都执行而不重新排序;此标志仅在 amd64i386 上实现。

注意

当你需要一个物理上连续的 DMA 缓冲区时,使用 bus_dmamem_alloc

bus_dmamem_free 函数释放由 bus_dmamem_alloc 之前分配的 vaddr 内存。然后它拆除了 DMA 映射 map

一个简单的例子

以下伪代码是一个需要 DMA 的虚构设备的 device_attach 例程。此伪代码应演示如何使用 bus_dmamem_alloc

static int
foo_attach(device_t dev)
{
        struct foo_softc *sc = device_get_softc(dev);
        int size = BAZ_SIZE;
        int error;

        bzero(sc, sizeof(*sc));

        if (bus_dma_tag_create(bus_get_dma_tag(dev),    /* parent       */
                               1,                       /* alignment    */
                               0,                       /* boundary     */
                               BUS_SPACE_MAXADDR,       /* lowaddr      */
                               BUS_SPACE_MAXADDR,       /* highaddr     */
                               NULL,                    /* filter       */
                               NULL,                    /* filterarg    */
                               BUS_SPACE_MAXSIZE_32BIT, /* maxsize      */
                               BUS_SPACE_UNRESTRICTED,  /* nsegments    */
                               BUS_SPACE_MAXSIZE_32BIT, /* maxsegsize   */
                               0,                       /* flags        */
                               NULL,                    /* lockfunc     */
                               NULL,                    /* lockfuncarg  */
                               &sc->foo_parent_dma_tag)) {
                device_printf(dev, "Cannot allocate parent DMA tag!\n");
                return (ENOMEM);
        }

        if (bus_dma_tag_create(sc->foo_parent_dma_tag,  /* parent       */
                               64,                      /* alignment    */
                               0,                       /* boundary     */
                               BUS_SPACE_MAXADDR_32BIT, /* lowaddr      */
                               BUS_SPACE_MAXADDR,       /* highaddr     */
                               NULL,                    /* filter       */
                               NULL,                    /* filterarg    */
                             size,                    /* maxsize      */
                             1,                       /* nsegments    */
                             size,                    /* maxsegsize   */
                               0,                       /* flags        */
                               NULL,                    /* lockfunc     */
                               NULL,                    /* lockfuncarg  */
                               &sc->foo_baz_dma_tag)) {
                device_printf(dev, "Cannot allocate baz DMA tag!\n");
                return (ENOMEM);
        }

        if (bus_dmamem_alloc(sc->foo_baz_dma_tag,       /* DMA tag      */
                           (void **)&sc->foo_baz_buf,   /* vaddr        */
                             BUS_DMA_NOWAIT,              /* flags        */
                           &sc->foo_baz_dma_map)) {
                device_printf(dev, "Cannot allocate baz DMA memory!\n");
                return (ENOMEM);
        }

        bzero(sc->foo_baz_buf, size);

        error = bus_dmamap_load(sc->foo_baz_dma_tag,  /* DMA tag      */
                              sc->foo_baz_dma_map,    /* DMA map      */
                              sc->foo_baz_buf,        /* buffer       */
                                size,                   /* buffersize   */
                              foo_callback,           /* callback     */
                                &sc->foo_baz_busaddr,   /* callbackarg  */
                                BUS_DMA_NOWAIT);        /* flags        */
        if (error || sc->foo_baz_busaddr == 0) {
                device_printf(dev, "Cannot map baz DMA memory!\n");
                return (ENOMEM);
        }

...
}

虽然 更多 bus_dmamem_alloc 分配 更多 内存并创建一个 更多 DMA 映射,更多 将该内存加载到更多 DMA 映射中仍然需要发生。

此外,由于 bus_dmamem_alloc 分配连续内存,nsegments 参数必须是 更多 1。同样,更多 maxsize更多 maxsegsz 参数必须相同。

最后,由于 nsegments1更多 callback 可以是 实现 DMA 中显示的 foo_callback 函数,见 实现 DMA。

同步 DMA 缓冲区

DMA 缓冲区必须在 CPU/驱动程序或设备完成每次写入操作后同步。确切的原因超出了本书的范围。但基本上是为了确保 CPU/驱动程序和设备对 DMA 缓冲区有一个一致的观点。

DMA 缓冲区与 bus_dmamap_sync 函数同步。

#include <machine/bus.h>

void
bus_dmamap_sync(bus_dma_tag_t dmat, bus_dmamap_t map, bus_dmasync_op_t op);

此函数同步当前加载在 DMA 映射 map 中的 DMA 缓冲区。dmat 参数是 map 所基于的 DMA 标签。op 参数标识要执行同步操作的类型。此参数的有效值显示在 表 12-5 中。

表 12-5. bus_dmamap_sync 符号常量

常量 描述
BUS_DMASYNC_PREWRITE 用于在 CPU/驱动程序写入 DMA 缓冲区后同步
BUS_DMASYNC_POSTREAD 用于在设备写入 DMA 缓冲区后同步

结论

本章详细介绍了 FreeBSD 的 DMA 管理例程。这些例程主要用于存储和网络驱动程序,这些内容在 第十三章、第十六章 和 第十七章 中进行了讨论。

第十三章. 存储驱动程序

无标题图片

在 FreeBSD 中,存储驱动程序提供对以块形式传输随机访问数据的设备(如磁盘驱动器、闪存等)的访问。是固定大小的数据块(Corbet 等人,2005 年)。在本章中,我将讨论如何管理采用以块为中心的 I/O 的设备。为此,需要对磁盘和 bio 结构有一定的了解,所以我们将从这里开始。

磁盘结构

disk 结构是内核对单个类似磁盘的存储设备的表示。它在 <geom/geom_disk.h> 头文件中定义如下:

struct disk {
        /* GEOM Private Data */
        struct g_geom          *d_geom;
        struct devstat         *d_devstat;
        int                     d_destroyed;

        /* Shared Objects */
        struct bio_queue_head  *d_queue;
        struct mtx             *d_lock;

        /* Descriptive Fields */
        const char             *d_name;
        u_int                   d_unit;
        u_int                   d_flags;

        /* Storage Device Methods */
        disk_open_t            *d_open;
        disk_close_t           *d_close;
        disk_strategy_t        *d_strategy;
        disk_ioctl_t           *d_ioctl;
        dumper_t               *d_dump;

        /* Mandatory Media Properties */
        u_int                   d_sectorsize;
        off_t                   d_mediasize;
        u_int                   d_maxsize;

        /* Optional Media Properties */
        u_int                   d_fwsectors;
        u_int                   d_fwheads;
        u_int                   d_stripesize;
        u_int                   d_stripeoffset;
        char                    d_ident[DISK_IDENT_SIZE];

        /* Driver Private Data */
        void                   *d_drv1;
};

struct disk 结构中的许多字段必须由存储驱动程序初始化。这些字段在以下章节中描述。

描述性字段

d_named_unit 字段分别指定存储设备的名称和单元号。这些字段必须在每个 disk 结构中定义。

d_flags 字段进一步说明了存储设备的特性。此字段的有效值显示在表 13-1 中。

表 13-1. disk 结构符号常量

常量 描述
DISKFLAG_NEEDSGIANT 表示存储设备需要由 Giant 保护
DISKFLAG_CANDELETE 表示存储设备希望在不再需要块时收到通知,以便它可以执行一些特殊处理(例如,支持 TRIM 命令的固态驱动程序的驱动程序使用此标志)
DISKFLAG_CANFLUSHCACHE 表示存储设备可以刷新其本地写入缓存

d_flags 字段是可选的,可能未定义。

存储设备方法

d_open 字段标识存储设备的打开例程。如果没有提供函数,打开将始终成功。

d_close 字段标识存储设备的关闭例程。如果没有提供函数,关闭将始终成功。d_close 例程应始终终止由 d_open 例程设置的任何内容。

d_strategy 字段标识存储设备的策略例程。策略例程用于处理以块为中心的读取、写入和其他 I/O 操作。因此,d_strategy 必须在每个 disk 结构中定义。我将在稍后更详细地讨论以块为中心的 I/O 和策略例程。

d_ioctl 字段标识存储设备的 ioctl 例程。此字段是可选的,可能未定义。

d_dump 字段标识存储设备的转储例程。转储例程在内核恐慌后调用,以将物理内存的内容记录到存储设备。请注意,d_dump 是可选的,可能未定义。

强制性媒体属性

d_sectorsized_mediasize字段分别指定存储设备的扇区和媒体大小(以字节为单位)。这些字段必须在每个磁盘结构中定义。

d_maxsize字段表示存储设备 I/O 操作的最大字节数。此字段必须在每个磁盘结构中定义。

注意,您可以在d_open例程中安全地修改d_sectorsized_mediasized_maxsize的值。

可选媒体属性

d_fwsectorsd_fwheads字段标识存储设备上的扇区和磁头数量。这些字段是可选的,可能未定义;然而,某些平台要求这些字段用于磁盘分区。

d_stripesize字段指定了存储设备的任何自然请求边界宽度(例如,RAID-5 单元上的条带大小),而d_stripeoffset字段表示第一个条带的位置或偏移。这些字段是可选的,可能未定义。有关d_stripesized_stripeoffset的更多信息,请参阅/sys/geom/notes

d_ident字段表示存储设备的序列号。此字段是可选的,可能未定义,但定义它是良好的实践。

注意,您可以在d_open例程中安全地修改上述字段。

驾驶员私有数据

d_drv1字段可能被存储驱动程序用来存放数据。通常,d_drv1将包含指向存储驱动程序softc结构的指针。

磁盘结构管理例程

FreeBSD 内核为处理磁盘结构提供了以下函数:

#include <geom/geom_disk.h>

struct disk *
disk_alloc(void);

void
disk_create(struct disk *disk, int version);

void
disk_destroy(struct disk *disk);

磁盘结构是一个由内核拥有的动态分配的结构。也就是说,您不能自己分配一个struct disk。相反,您必须调用disk_alloc

分配一个磁盘结构并不会使存储设备对系统可用。为了做到这一点,您必须初始化结构(通过定义必要的字段),然后调用disk_createversion参数必须始终为DISK_VERSION

注意,一旦disk_create返回,设备就是“活跃”的,并且可以在任何时间调用其例程。因此,您应该在您的驱动程序完全准备好处理任何操作时才调用disk_create

当不再需要磁盘结构时,应使用disk_destroy将其释放。您可以销毁一个已打开的磁盘结构。当然,之后您还需要释放在d_open期间分配的任何资源,因为d_close不能再被调用。

块 I/O 结构

一个bio结构代表一个以块为中心的 I/O 请求。粗略地说,当内核需要将一些数据传输到或从存储设备时,它会组合一个bio结构来描述该操作;然后它将这个结构传递给适当的驱动程序。

struct bio<sys/bio.h>头文件中定义如下:

struct bio {
        uint8_t bio_cmd;                /* I/O operation.               */
        uint8_t bio_flags;              /* General flags.               */
        uint8_t bio_cflags;             /* Private use by the consumer. */
        uint8_t bio_pflags;             /* Private use by the provider. */

        struct cdev *bio_dev;           /* Device to perform I/O on.    */
        struct disk *bio_disk;          /* Disk structure.              */
        off_t   bio_offset;             /* Requested position in file.  */
        long    bio_bcount;             /* Number of (valid) bytes.     */
        caddr_t bio_data;               /* Data.                        */
        int     bio_error;              /* Error number for BIO_ERROR.  */
        long    bio_resid;              /* Remaining I/O (in bytes).    */
        void (*bio_done)(struct bio *); /* biodone() handler function.  */

        void    *bio_driver1;           /* Private use by the provider. */
        void    *bio_driver2;           /* Private use by the provider. */
        void    *bio_caller1;           /* Private use by the consumer. */
        void    *bio_caller2;           /* Private use by the consumer. */

        TAILQ_ENTRY(bio) bio_queue;     /* bioq linkage.                */
        const char *bio_attribute;      /* For BIO_[GS]ETATTR.          */
        struct g_consumer *bio_from;    /* GEOM linkage.                */
        struct g_provider *bio_to;      /* GEOM linkage.                */

        off_t   bio_length;             /* Like bio_bcount.             */
        off_t   bio_completed;          /* Opposite of bio_resid.       */
        u_int   bio_children;           /* Number of spawned bios.      */
        u_int   bio_inbed;              /* Number of children home.     */
        struct bio *bio_parent;         /* Parent pointer.              */
        struct bintime bio_t0;          /* Time I/O request started.    */

        bio_task_t *bio_task;   /* bio_taskqueue() handler function.    */
        void    *bio_task_arg;          /* bio_task's argument.         */
        void    *bio_classifier1;       /* Classifier tag.              */
        void    *bio_classifier2;       /* Classifier tag.              */

        daddr_t bio_pblkno;             /* Physical block number.       */
};

/* Bits for bio_cmd.    */
#define BIO_READ        0x01
#define BIO_WRITE       0x02
#define BIO_DELETE      0x04
#define BIO_GETATTR     0x08
#define BIO_FLUSH       0x10
#define BIO_CMD0        0x20            /* For local hacks.             */
#define BIO_CMD1        0x40            /* For local hacks.             */
#define BIO_CMD2        0x80            /* For local hacks.             */

/* Bits for bio_flags.  */
#define BIO_ERROR       0x01
#define BIO_DONE        0x02
#define BIO_ONQUEUE     0x04

我们将在稍后更详细地检查 struct bio。在此期间,你只需记住,策略例程被调用以处理新接收到的 bio 结构。

块 I/O 队列

所有存储驱动程序都维护一个 块 I/O 队列 来存放任何挂起的以块为中心的 I/O 请求。一般来说,这些请求按递增或递减的设备偏移量顺序存储,以便在处理时,磁盘头将沿单一方向移动(而不是弹跳),以最大化性能。

FreeBSD 内核提供了以下函数用于处理块 I/O 队列:

#include <sys/bio.h>

void
bioq_init(struct bio_queue_head *head);

void
bioq_disksort(struct bio_queue_head *head, struct bio *bp);

struct bio *
bioq_first(struct bio_queue_head *head);

struct bio *
bioq_takefirst(struct bio_queue_head *head);

void
bioq_insert_head(struct bio_queue_head *head, struct bio *bp);

void
bioq_insert_tail(struct bio_queue_head *head, struct bio *bp);

void
bioq_remove(struct bio_queue_head *head, struct bio *bp);

void
bioq_flush(struct bio_queue_head *head, struct devstat *stp, int error);

块 I/O 队列是一个由驱动程序拥有的静态分配的结构。要初始化块 I/O 队列,你必须调用 bioq_init

要执行有序插入,请调用 bioq_disksort。要返回队列头部(即下一个要处理的请求),使用 bioq_first。最后,要返回并移除队列头部,请调用 bioq_takefirst

上文提到的函数是管理块 I/O 队列的主要方法。如果仅使用这些函数来操作队列,则队列最多包含一个逆序点(即两个排序序列)。

bioq_insert_head 函数在队列头部插入一个请求。此外,它创建一个“屏障”,使得使用 bioq_disksort 执行的所有后续插入都将在这个请求之后完成。

bioq_insert_tail 函数与 bioq_insert_head 类似,但它将请求插入到队列的末尾。请注意,bioq_insert_tail 也会创建一个屏障。

通常情况下,你会使用一个屏障来确保在继续之前所有前面的请求都已得到处理。

bioq_remove 函数从队列中移除一个请求。如果 bioq_remove 在队列头部被调用,其效果等同于 bioq_takefirst

如果使用 bioq_insert_headbioq_insert_tailbioq_remove 操作块 I/O 队列,它可能包含多个逆序点。

bioq_flush 函数清除所有队列中的请求,并使它们返回错误代码 error

注意

对于包含请求调度功能的存储设备(如 SATA 原生命令队列、SCSI 标记命令队列等),bioq_disksort 实际上是没有意义的,因为这些设备将(重新)内部排序请求。在这种情况下,使用 bioq_insert_tail 的简单先入先出 (FIFO) 块 I/O 队列就足够了。

综合一切

既然你已经对 diskbio 结构有了些了解,让我们剖析一个真实的存储驱动程序。

示例 13-1 是 Atmel 的 AT45D 系列数据闪存芯片的存储驱动程序。DataFlash 是 Atmel 的串行接口闪存,用于串行外设接口 (SPI) 总线。简而言之,示例 13-1 是 SPI 总线上的闪存存储驱动程序。

注意

快速浏览一下这段代码,并尝试识别其结构。如果你不完全理解,不要担心;接下来的解释会详细说明。

示例 13-1. at45d.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/bus.h>
  #include <sys/conf.h>
  #include <sys/bio.h>
  #include <sys/kthread.h>
  #include <sys/lock.h>
  #include <sys/mutex.h>
  #include <geom/geom_disk.h>

  #include <dev/spibus/spi.h>
  #include "spibus_if.h"

  #define MANUFACTURER_ID                 0x9f
  #define STATUS_REGISTER_READ            0xd7
  #define CONTINUOUS_ARRAY_READ_HF        0x0b
  #define PROGRAM_THROUGH_BUFFER          0x82

  struct at45d_softc {
          device_t                        at45d_dev;
          struct mtx                      at45d_mtx;
          struct intr_config_hook         at45d_ich;
          struct disk                    *at45d_disk;
          struct bio_queue_head           at45d_bioq;
          struct proc                    *at45d_proc;
  };

  static devclass_t at45d_devclass;

  static void                             at45d_delayed_attach(void *);
  static void                             at45d_task(void *);
  static void                             at45d_strategy(struct bio *);

  static int
 at45d_probe(device_t dev)
  {
          device_set_desc(dev, "AT45 flash family");
          return (BUS_PROBE_SPECIFIC);
  }

  static int
  at45d_attach(device_t dev)
  {
          struct at45d_softc *sc = device_get_softc(dev);
          int error;

          sc->at45d_dev = dev;
          mtx_init(&sc->at45d_mtx, device_get_nameunit(dev), "at45d", MTX_DEF);

          sc->at45d_ich.ich_func = at45d_delayed_attach;
          sc->at45d_ich.ich_arg = sc;
          error = config_intrhook_establish(&sc->at45d_ich);
          if (error)
                  device_printf(dev, "config_intrhook_establish() failed!\n");

          return (0);
  }

  static int
 at45d_detach(device_t dev)
  {
          return (EIO);
  }

  static int
  at45d_get_info(device_t dev, uint8_t *r)
  {
          struct spi_command cmd;
          uint8_t tx_buf[8], rx_buf[8];
          int error;

          memset(&cmd, 0, sizeof(cmd));
          memset(tx_buf, 0, sizeof(tx_buf));
          memset(rx_buf, 0, sizeof(rx_buf));

          tx_buf[0] = MANUFACTURER_ID;
          cmd.tx_cmd = &tx_buf[0];
          cmd.rx_cmd = &rx_buf[0];
          cmd.tx_cmd_sz = 5;
          cmd.rx_cmd_sz = 5;
          error = SPIBUS_TRANSFER(device_get_parent(dev), dev, &cmd);
          if (error)
                  return (error);

          memcpy(r, &rx_buf[1], 4);
          return (0);
  }

  static uint8_t
  at45d_get_status(device_t dev)
  {
          struct spi_command cmd;
          uint8_t tx_buf[8], rx_buf[8];

          memset(&cmd, 0, sizeof(cmd));
          memset(tx_buf, 0, sizeof(tx_buf));
          memset(rx_buf, 0, sizeof(rx_buf));

          tx_buf[0] = STATUS_REGISTER_READ;
          cmd.tx_cmd = &tx_buf[0];
          cmd.rx_cmd = &rx_buf[0];
          cmd.tx_cmd_sz = 2;
          cmd.rx_cmd_sz = 2;
          SPIBUS_TRANSFER(device_get_parent(dev), dev, &cmd);

          return (rx_buf[1]);
  }

  static void
  at45d_wait_for_device_ready(device_t dev)
  {
          while ((at45d_get_status(dev) & 0x80) == 0)
                  continue;
  }

  static void
  at45d_delayed_attach(void *arg)
  {
          struct at45d_softc *sc = arg;
          uint8_t buf[4];

          at45d_get_info(sc->at45d_dev, buf);
          at45d_wait_for_device_ready(sc->at45d_dev);

          sc->at45d_disk = disk_alloc();
          sc->at45d_disk->d_name = "at45d";
          sc->at45d_disk->d_unit = device_get_unit(sc->at45d_dev);
          sc->at45d_disk->d_strategy = at45d_strategy;
          sc->at45d_disk->d_sectorsize = 1056;
          sc->at45d_disk->d_mediasize = 8192 * 1056;
          sc->at45d_disk->d_maxsize = DFLTPHYS;
          sc->at45d_disk->d_drv1 = sc;

          bioq_init(&sc->at45d_bioq);
          kproc_create(&at45d_task, sc, &sc->at45d_proc, 0, 0, "at45d");

          disk_create(sc->at45d_disk, DISK_VERSION);
          config_intrhook_disestablish(&sc->at45d_ich);
  }

  static void
  at45d_strategy(struct bio *bp)
  {
          struct at45d_softc *sc = bp->bio_disk->d_drv1;

          mtx_lock(&sc->at45d_mtx);
          bioq_disksort(&sc->at45d_bioq, bp);
          wakeup(sc);
          mtx_unlock(&sc->at45d_mtx);
  }

  static void
  at45d_task(void *arg)
  {
          struct at45d_softc *sc = arg;
          struct bio *bp;
          struct spi_command cmd;
          uint8_t tx_buf[8], rx_buf[8];
          int ss = sc->at45d_disk->d_sectorsize;
          daddr_t block, end;
          char *vaddr;

          for (;;) {
                  mtx_lock(&sc->at45d_mtx);
                  do {
                          bp = bioq_first(&sc->at45d_bioq);
                          if (bp == NULL)
                                  mtx_sleep(sc, &sc->at45d_mtx, PRIBIO,
                                      "at45d", 0);
                  } while (bp == NULL);
                  bioq_remove(&sc->at45d_bioq, bp);
                  mtx_unlock(&sc->at45d_mtx);

                  end = bp->bio_pblkno + (bp->bio_bcount / ss);
                  for (block = bp->bio_pblkno; block < end; block++) {
                          vaddr = bp->bio_data + (block - bp->bio_pblkno) * ss;
                          if (bp->bio_cmd == BIO_READ) {
                                  tx_buf[0] = CONTINUOUS_ARRAY_READ_HF;
                                  cmd.tx_cmd_sz = 5;
                                  cmd.rx_cmd_sz = 5;
                          } else {
                                  tx_buf[0] = PROGRAM_THROUGH_BUFFER;
                                  cmd.tx_cmd_sz = 4;
                                  cmd.rx_cmd_sz = 4;
                          }

                          /* FIXME: This works only on certain devices. */
                          tx_buf[1] = ((block >> 5) & 0xff);
                          tx_buf[2] = ((block << 3) & 0xf8);
                          tx_buf[3] = 0;
                          tx_buf[4] = 0;
                          cmd.tx_cmd = &tx_buf[0];
                          cmd.rx_cmd = &rx_buf[0];
                          cmd.tx_data = vaddr;
                          cmd.rx_data = vaddr;
                          cmd.tx_data_sz = ss;
                          cmd.rx_data_sz = ss;
                          SPIBUS_TRANSFER(device_get_parent(sc->at45d_dev),
                              sc->at45d_dev, &cmd);
                  }
                  biodone(bp);
          }
  }

  static device_method_t at45d_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_probe,         at45d_probe),
          DEVMETHOD(device_attach,        at45d_attach),
          DEVMETHOD(device_detach,        at45d_detach),
          { 0, 0 }
  };

  static driver_t at45d_driver = {
          "at45d",
          at45d_methods,
          sizeof(struct at45d_softc)
  };

  DRIVER_MODULE(at45d, spibus, at45d_driver, at45d_devclass, 0, 0);

以下部分将按它们执行的顺序大致描述 示例 13-1 中定义的函数。

顺便提一下,因为 图片 at45d_probe图片 at45d_detach 非常基础,并且你已经在其他地方见过类似的代码,所以我会省略对它们的讨论。

at45d_attach 函数

at45d_attach 函数是这个存储驱动程序的 device_attach 实现。以下是它的函数定义(再次):

static int
at45d_attach(device_t dev)
{
        struct at45d_softc *sc = device_get_softc(dev);
        int error;

        sc->at45d_dev = dev;
      mtx_init(&sc->at45d_mtx, device_get_nameunit(dev), "at45d",
            MTX_DEF);

        sc->at45d_ich.ich_func = at45d_delayed_attach;
        sc->at45d_ich.ich_arg = sc;
        error = config_intrhook_establish(&sc->at45d_ich);
        if (error)
                device_printf(dev, "config_intrhook_establish() failed!\n");

        return (0);
}

此函数首先 图片 初始化互斥锁 at45d_mtx,它将保护 at45d 的块 I/O 队列。然后它 图片 安排 图片 at45d_delayed_attach 在中断启用时执行。

注意

在初始自动配置阶段(即系统启动后),中断被禁用。然而,一些驱动程序(如 at45d)需要中断来进行设备初始化。在这种情况下,你会使用 config_intrhook_establish,它安排一个函数在启用中断但挂载根文件系统之前执行;如果系统已经过了这个点,函数会立即被调用。

at45d_delayed_attach 函数

从广义上讲,at45d_delayed_attach 函数是 at45d_attach 的后半部分。也就是说,它完成了设备的初始化。以下是它的函数定义(再次):

static void
at45d_delayed_attach(void *arg)
{
        struct at45d_softc *sc = arg;
        uint8_t buf[4];

      at45d_get_info(sc->at45d_dev, buf);
      at45d_wait_for_device_ready(sc->at45d_dev);

        sc->at45d_disk = disk_alloc();
        sc->at45d_disk->d_name = "at45d";
        sc->at45d_disk->d_unit = device_get_unit(sc->at45d_dev);
        sc->at45d_disk->d_strategy = at45d_strategy;
        sc->at45d_disk->d_sectorsize = 1056;
        sc->at45d_disk->d_mediasize = 8192 * 1056;
        sc->at45d_disk->d_maxsize = DFLTPHYS;
        sc->at45d_disk->d_drv1 = sc;

      bioq_init(&sc->at45d_bioq);
      kproc_create(&at45d_task, sc, &sc->at45d_proc, 0, 0, "at45d");

      disk_create(sc->at45d_disk, DISK_VERSION);
      config_intrhook_disestablish(&sc->at45d_ich);
}

此函数可以分解为多个部分。首先 图片 获取设备的制造商 ID。然后 at45d_delayed_attach 图片 等待设备准备就绪。这两个动作需要启用中断。

第二部分 图片 分配并定义 at45ddisk 结构,图片 初始化 at45d 的块 I/O 队列,并 图片 创建一个新的内核进程(以执行 图片 at45d_task 函数)。

最后,创建 at45d 的设备节点,并 图片 拆卸 at45d_delayed_attach

注意

在引导过程中——在挂载根文件系统之前——系统会停滞,直到通过 config_intrhook_establish 安排的每个函数都完成并自行拆解。换句话说,如果 at45d_delayed_attach 没有调用 config_intrhook_disestablish,系统将会挂起。

at45d_get_info 函数

at45d_get_info 函数获取存储设备的制造商 ID。以下是它的函数定义(再次):

static int
at45d_get_info(device_t dev, uint8_t *r)
{
        struct spi_command cmd;
        uint8_t tx_buf[8], rx_buf[8];
        int error;

        memset(&cmd, 0, sizeof(cmd));
      memset(tx_buf, 0, sizeof(tx_buf));
      memset(rx_buf, 0, sizeof(rx_buf));

      tx_buf[0] = MANUFACTURER_ID;
      cmd.tx_cmd = &tx_buf[0];
      cmd.rx_cmd = &rx_buf[0];
      cmd.tx_cmd_sz = 5;
      cmd.rx_cmd_sz = 5;
        error = SPIBUS_TRANSFER(device_get_parent(dev), dev, &cmd);
        if (error)
                return (error);

      memcpy(r, &rx_buf[1], 4);
        return (0);
}

此函数首先将其 发送和 接收缓冲区清零。

注意

每个 SPI 数据传输都是全双工数据传输。也就是说,它始终需要一个发送和接收缓冲区,因为主设备和从设备都会发送数据——即使要发送的数据是无意义的或垃圾数据,它仍然会被传输。

此函数的其余部分 MANUFACTURER_ID 放入发送缓冲区,设置 spi_command 结构(表示 发送和 接收缓冲区及其 数据长度), 启动数据传输,并最终 将制造商 ID 返回给调用者。

at45d_wait_for_device_ready 函数

at45d_wait_for_device_ready 函数“空转”直到存储设备准备好。以下是它的函数定义(再次):

static void
at45d_wait_for_device_ready(device_t dev)
{
        while ((at45d_get_status(dev) & 0x80) == 0)
                continue;
}

此函数持续调用 at45d_get_status 直到返回 0x80,这表示设备不忙且准备好接受下一个命令。

at45d_get_status 函数

at45d_get_status 函数获取存储设备的状态。以下是它的函数定义(再次):

static uint8_t
at45d_get_status(device_t dev)
{
        struct spi_command cmd;
        uint8_t tx_buf[8], rx_buf[8];

        memset(&cmd, 0, sizeof(cmd));
        memset(tx_buf, 0, sizeof(tx_buf));
        memset(rx_buf, 0, sizeof(rx_buf));

      tx_buf[0] = STATUS_REGISTER_READ;
        cmd.tx_cmd = &tx_buf[0];
        cmd.rx_cmd = &rx_buf[0];
        cmd.tx_cmd_sz = 2;
        cmd.rx_cmd_sz = 2;
        SPIBUS_TRANSFER(device_get_parent(dev), dev, &cmd);

        return (rx_buf[1]);
}

如您所见,此函数几乎与 at45d_get_info 函数完全相同,只是它 使用了不同的命令。因此,我将省略其详细说明。

at45d_strategy 函数

at45d_strategy 函数在 at45d_delayed_attach 中定义为 d_strategy 例程;每当 at45d 接收到一个 bio 结构时就会执行它。以下是它的函数定义(再次):

static void
at45d_strategy(struct bio *bp)
{
        struct at45d_softc *sc = bp->bio_disk->d_drv1;

        mtx_lock(&sc->at45d_mtx);
      bioq_disksort(&sc->at45d_bioq, bp);
      wakeup(sc);
        mtx_unlock(&sc->at45d_mtx);
}

此函数只是接受一个 bio 结构并将其 添加到 at45d 的块 I/O 队列中。然后它 at45d_task 实际处理 bio 结构。

注意

大多数策略例程都做类似的事情。也就是说,它们实际上并不处理 bio 结构;它们只是将它们放置在块 I/O 队列中,然后另一个函数或线程来处理它们。

at45d_task 函数

如前所述,at45d_task 函数处理 at45d 的块 I/O 队列中的 bio 结构。以下是它的函数定义(再次):

static void
at45d_task(void *arg)
{
        struct at45d_softc *sc = arg;
        struct bio *bp;
        struct spi_command cmd;
        uint8_t tx_buf[8], rx_buf[8];
        int ss = sc->at45d_disk->d_sectorsize;
        daddr_t block, end;
        char *vaddr;

        for (;;) {
                mtx_lock(&sc->at45d_mtx);
                do {
                        bp = bioq_first(&sc->at45d_bioq);
                        if (bp == NULL)
                              mtx_sleep(sc, &sc->at45d_mtx, PRIBIO,
                                    "at45d", 0);
                } while (bp == NULL);
              bioq_remove(&sc->at45d_bioq, bp);
                mtx_unlock(&sc->at45d_mtx);

                end = bp->bio_pblkno + (bp->bio_bcount / ss);
                for (block = bp->bio_pblkno; block < end; block++) {
                      vaddr = bp->bio_data +
                            (block - bp->bio_pblkno) * ss;
                      if (bp->bio_cmd == BIO_READ) {
                                tx_buf[0] = CONTINUOUS_ARRAY_READ_HF;
                                cmd.tx_cmd_sz = 5;
                                cmd.rx_cmd_sz = 5;
                      } else {
                                tx_buf[0] = PROGRAM_THROUGH_BUFFER;
                                cmd.tx_cmd_sz = 4;
                                cmd.rx_cmd_sz = 4;
                        }

                        /* FIXME: This works only on certain devices. */
                        tx_buf[1] = ((block >> 5) & 0xff);
                        tx_buf[2] = ((block << 3) & 0xf8);
                        tx_buf[3] = 0;
                        tx_buf[4] = 0;
                        cmd.tx_cmd = &tx_buf[0];
                        cmd.rx_cmd = &rx_buf[0];
                        cmd.tx_data = vaddr;
                        cmd.rx_data = vaddr;
                        cmd.tx_data_sz = ss;
                        cmd.rx_data_sz = ss;
                      SPIBUS_TRANSFER(device_get_parent(sc->at45d_dev),
                            sc->at45d_dev, &cmd);
                }
              biodone(bp);
        }
}

这个函数可以分为四个部分。第一部分 图片 确定是否at45d的块 I/O 队列是空的。如果是,at45d_task 图片 睡眠;否则,它 图片 获取(并移除)队列的头部。第二部分确定块中心的 I/O 请求是 图片 读取还是 图片 写入。

注意

从驱动程序的角度来看,块中心的 I/O 请求。因此,BIO_READ表示从设备中读取。

第二部分同样计算了从bio_data(即主内存中的位置)读取或写入的偏移量 图片。这一点至关重要,因为每个 I/O 操作传输的是 1 块数据,而不是 1 个字节(即上述偏移量是 1 块数据的倍数)。

如果你在偏移量计算方面遇到困难,以下是每个涉及的变量的简要描述:ss变量是设备的扇区大小。bio_pblkno变量是读取或写入的第一个设备内存块,end是最后一个块,而blockat45d_task正在处理的当前块。

第三部分设置spi_command结构并 图片 启动数据传输。最后,第四部分 图片 告诉内核,块中心的 I/O 请求bp已成功处理。

块 I/O 完成例程

如前文所述,在处理以块为中心的 I/O 请求后,你必须通过以下方式通知内核:

#include <sys/bio.h>

void
biodone(struct bio *bp);

void
biofinish(struct bio *bp, struct devstat *stat, int error);

biodone函数告诉内核,块中心的 I/O 请求bp已成功处理。

biofinish函数与biodone相同,除了它将bp设置为返回错误代码error(也就是说,biofinish可以告诉内核bp是无效的、成功的还是不成功的)。

注意

通常,stat参数设置为NULL。有关struct devstat的更多信息,请参阅devstat(9)手册页(尽管它有些过时)。

结论

本章重点介绍了存储驱动程序的实现和理解。你学习了如何管理diskbio结构,并研究了一个实际的存储驱动程序。

第十四章。通用访问方法

无标题图片

通用访问方法 (CAM) 是一个 ANSI 标准。尽管主要用于 SCSI,但 CAM 是一种将主机总线适配器 (HBA) 驱动程序与存储驱动程序分离的方法。HBA 是连接主机与其他设备的设备(即一张卡或集成电路)。例如,USB HBA 允许主机与 USB 设备通信。

通过将 HBA 驱动程序与存储驱动程序分离,CAM 减少了单个驱动程序的复杂性。此外,这种分离使得存储驱动程序(如 CD-ROM 和磁带驱动程序)可以在任何 I/O 总线(如 IDE、SCSI 等)上控制它们的设备,只要有一个合适的 HBA 驱动程序可用。换句话说,CAM 将 HBA 和存储驱动程序模块化。

在 CAM 术语中,HBA 驱动程序被称为软件接口模块 (SIMs),而存储驱动程序被称为外围模块。顺便提一下,第十三章(ch13.html "第十三章。存储驱动程序")中讨论的存储驱动程序不在 CAM 下。为了避免混淆,从现在起我将把 CAM 下的存储驱动程序称为外围模块。

FreeBSD CAM 实现包含用于 SCSI 并行接口 (SPI)、光纤通道 (FC)、USB 大容量存储 (UMASS)、火线 (IEEE 1394) 和高级技术附件包接口 (ATAPI) 的 SIMs。它具有磁盘(da)、CD-ROM(cd)、磁带(sa)、磁带更换器(ch)、处理器类型设备(pt)和机箱服务(ses)的外围模块。此外,它还提供了一个“直通”接口,允许用户应用程序直接将 I/O 请求发送到任何 CAM 控制的设备(McKusick 和 Neville-Neil,2005)。这个接口本质上是一个 SIM(你很快就会看到)。

在本章中,你将学习如何使用 CAM 管理 HBA。当然,在你能够做到这一点之前,你需要了解 CAM 如何将外围模块与 SIMs 接口。因为外围模块只是带有一些 CAM 相关代码的存储驱动程序,所以本章只是简要地讨论了它们。

如何使用 CAM 工作

通过追踪一个 I/O 请求通过 CAM,可以最简单地理解 CAM。

在图 14-1 中,内核将一个以块为中心的 I/O 请求传递给 da(4) 外围模块。正如你所期望的,这会导致 da(4) 的策略例程(dastrategy)执行。

I/O 请求通过 CAM 子系统的路径

图 14-1. I/O 请求通过 CAM 子系统的路径

dastrategy 函数获取以块为中心的 I/O 请求,并通过 bioq_disksort 将其插入到适当的块 I/O 队列中。它通过调用 xpt_schedule 函数结束。(da(4) 外围模块支持每个 SCSI 硬盘。因此,它管理多个块 I/O 队列。)

xpt_schedule函数主要安排一个外围模块接收一个CAM 控制块(CCB)。CCB 描述了目标设备的位置(或路径)(即 I/O 请求的预期接收者)。xpt_schedule函数通过调用xpt_run_dev_allocq函数结束。(请注意,我对 CCB 的定义并不完整。我将在本章中扩展这个定义。)

xpt_run_dev_allocq函数分配并构建一个 CCB。之后,它调用外围模块的启动例程(在这个例子中是dastart)。

dastart函数从适当的块 I/O 队列中取出第一个块中心的 I/O 请求,并将其转换为 SCSI 命令。这个命令存储在由xpt_run_dev_allocq构建的 CCB 中。dastart函数通过调用xpt_action函数结束。

xpt_action函数使用存储在 CCB 中的路径信息来确定应该将 SCSI 命令发送到哪个 SIM。然后它调用该 SIM 的动作例程(在这种情况下是ahc_action)。

注意

这个示例中的 SIM 是伪随机选择的,所以它是ahc(4)的事实并不重要。

ahc_action函数获取 CCB,并将 SCSI 命令转换为特定的硬件命令。然后,这个特定的硬件命令被传递给设备执行。之后,ahc_action返回到dastrategy的调用者。

一旦设备完成特定的硬件命令(可能涉及 DMA),它就会发送一个中断,这会导致ahc(4)的完成例程(ahc_done)执行。

ahc_done函数将完成状态(即成功或失败)追加到与完成的特定硬件命令相关的 CCB。然后它调用xpt_done函数。

xpt_done函数获取完成的 CCB,并将其设置好以供camisr(CAM 中断服务例程)处理。然后它安排camisr运行。

大体上讲,camisr函数对 CCB 执行一些“家务”工作。它通过调用 CCB 指定的完成函数(在这个例子中是dadone)结束。

dadone函数,或多或少地,通过调用biodone来告诉内核块中心的 I/O 请求已经被服务。


^([9]) 图 14-1 是改编自 Marshall Kirk McKusick 和 George V. Neville-Neil 所著的《FreeBSD 操作系统的设计与实现》(Addison-Wesley,2005)。

(一个)简单示例

现在你已经熟悉了 CAM 子系统,让我们通过一些代码来操作。之后,我将详细说明不同的 CAM 相关函数。

示例 14-1 是一个伪 HBA 的 SIM(从mfi(4)代码库中提取)。

注意

快速看一下这段代码,并尝试辨别其结构。如果你不理解其中的所有内容,不要担心;解释随后到来。

示例 14-1. mfi_cam.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/selinfo.h>
  #include <sys/bus.h>
  #include <sys/conf.h>
  #include <sys/bio.h>
  #include <sys/malloc.h>
  #include <sys/uio.h>

  #include <cam/cam.h>
  #include <cam/cam_ccb.h>
  #include <cam/cam_debug.h>
  #include <cam/cam_sim.h>
  #include <cam/cam_xpt_sim.h>
  #include <cam/scsi/scsi_all.h>

  #include <machine/md_var.h>
  #include <machine/bus.h>
  #include <sys/rman.h>

  #include <dev/mfi/mfireg.h>
  #include <dev/mfi/mfi_ioctl.h>
  #include <dev/mfi/mfivar.h>

  #define ccb_mfip_ptr            sim_priv.entries[0].ptr

  struct mfip {
          device_t                dev;
          struct mfi_softc        *mfi;
          struct cam_devq         *devq;
          struct cam_sim          *sim;
          struct cam_path         *path;
  };

  static devclass_t               mfip_devclass;

  static void                     mfip_action(struct cam_sim *, union ccb *);
  static void                     mfip_poll(struct cam_sim *);
  static struct mfi_command *     mfip_start(void *);
  static void                     mfip_done(struct mfi_command *);

  static int
 mfip_probe(device_t dev)
  {
          device_set_desc(dev, "SCSI pass-through bus");
          return (BUS_PROBE_SPECIFIC);
  }

  static int
  mfip_attach(device_t dev)
  {
          struct mfip *sc;
          struct mfi_softc *mfi;

          sc = device_get_softc(dev);
          if (sc == NULL)
                  return (EINVAL);

          mfi = device_get_softc(device_get_parent(dev));
          sc->dev = dev;
          sc->mfi = mfi;
          mfi->mfi_cam_start = mfip_start;

          if ((sc->devq = cam_simq_alloc(MFI_SCSI_MAX_CMDS)) == NULL)
                  return (ENOMEM);

          sc->sim = cam_sim_alloc(mfip_action, mfip_poll, "mfi", sc,
              device_get_unit(dev), &mfi->mfi_io_lock, 1, MFI_SCSI_MAX_CMDS,
              sc->devq);
          if (sc->sim == NULL) {
                  cam_simq_free(sc->devq);
                  device_printf(dev, "cannot allocate CAM SIM\n");
                  return (EINVAL);
          }

          mtx_lock(&mfi->mfi_io_lock);
          if (xpt_bus_register(sc->sim, dev, 0) != 0) {
                  device_printf(dev,
                      "cannot register SCSI pass-through bus\n");
                  cam_sim_free(sc->sim, FALSE);
                  cam_simq_free(sc->devq);
                  mtx_unlock(&mfi->mfi_io_lock);
                  return (EINVAL);
          }
          mtx_unlock(&mfi->mfi_io_lock);

          return (0);
  }

  static int
  mfip_detach(device_t dev)
  {
          struct mfip *sc;

          sc = device_get_softc(dev);
          if (sc == NULL)
                  return (EINVAL);

          if (sc->sim != NULL) {
                  mtx_lock(&sc->mfi->mfi_io_lock);
                  xpt_bus_deregister(cam_sim_path(sc->sim));
                  cam_sim_free(sc->sim, FALSE);
                  mtx_unlock(&sc->mfi->mfi_io_lock);
          }

          if (sc->devq != NULL)
                  cam_simq_free(sc->devq);

          return (0);
  }

  static void
  mfip_action(struct cam_sim *sim, union ccb *ccb)
  {
          struct mfip *sc;
          struct mfi_softc *mfi;

          sc = cam_sim_softc(sim);
          mfi = sc->mfi;
          mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

          switch (ccb->ccb_h.func_code) {
          case XPT_PATH_INQ:
          {
                  struct ccb_pathinq *cpi;

                  cpi = &ccb->cpi;
                  cpi->version_num = 1;
                  cpi->hba_inquiry = PI_SDTR_ABLE | PI_TAG_ABLE | PI_WIDE_16;
                  cpi->target_sprt = 0;
                  cpi->hba_misc = PIM_NOBUSRESET | PIM_SEQSCAN;
                  cpi->hba_eng_cnt = 0;
                  cpi->max_target = MFI_SCSI_MAX_TARGETS;
                  cpi->max_lun = MFI_SCSI_MAX_LUNS;
                  cpi->initiator_id = MFI_SCSI_INITIATOR_ID;
                  strncpy(cpi->sim_vid, "FreeBSD", SIM_IDLEN);
                  strncpy(cpi->hba_vid, "LSI", HBA_IDLEN);
                  strncpy(cpi->dev_name, cam_sim_name(sim), DEV_IDLEN);
                  cpi->unit_number = cam_sim_unit(sim);
                  cpi->bus_id = cam_sim_bus(sim);
                  cpi->base_transfer_speed = 150000;
                  cpi->protocol = PROTO_SCSI;
                  cpi->protocol_version = SCSI_REV_2;
                  cpi->transport = XPORT_SAS;
                  cpi->transport_version = 0;

                  cpi->ccb_h.status = CAM_REQ_CMP;
                  break;
          }
          case XPT_RESET_BUS:
                  ccb->ccb_h.status = CAM_REQ_CMP;
                  break;
          case XPT_RESET_DEV:
                  ccb->ccb_h.status = CAM_REQ_CMP;
                  break;
          case XPT_GET_TRAN_SETTINGS:
          {
                  struct ccb_trans_settings_sas *sas;

                  ccb->cts.protocol = PROTO_SCSI;
                  ccb->cts.protocol_version = SCSI_REV_2;
                  ccb->cts.transport = XPORT_SAS;
                  ccb->cts.transport_version = 0;
                  sas = &ccb->cts.xport_specific.sas;
                  sas->valid &= ˜CTS_SAS_VALID_SPEED;
                  sas->bitrate = 150000;

                  ccb->ccb_h.status = CAM_REQ_CMP;
                  break;
          }
          case XPT_SET_TRAN_SETTINGS:
                  ccb->ccb_h.status = CAM_FUNC_NOTAVAIL;
                  break;
          case XPT_SCSI_IO:
          {
                  struct ccb_hdr *ccb_h = &ccb->ccb_h;
                  struct ccb_scsiio *csio = &ccb->csio;

                  ccb_h->status = CAM_REQ_INPROG;
                  if (csio->cdb_len > MFI_SCSI_MAX_CDB_LEN) {
                          ccb_h->status = CAM_REQ_INVALID;
                          break;
                  }
                  if ((ccb_h->flags & CAM_DIR_MASK) != CAM_DIR_NONE) {
                          if (ccb_h->flags & CAM_DATA_PHYS) {
                                  ccb_h->status = CAM_REQ_INVALID;
                                  break;
                          }
                          if (ccb_h->flags & CAM_SCATTER_VALID) {
                                  ccb_h->status = CAM_REQ_INVALID;
                                  break;
                          }
                  }

                  ccb_h->ccb_mfip_ptr = sc;
                  TAILQ_INSERT_TAIL(&mfi->mfi_cam_ccbq, ccb_h, sim_links.tqe);
                  mfi_startio(mfi);

                  return;
          }
          default:
                  ccb->ccb_h.status = CAM_REQ_INVALID;
                  break;
          }

          xpt_done(ccb);
          return;
  }

  static void
  mfip_poll(struct cam_sim *sim)
  {
          return;
  }

  static struct mfi_command *
  mfip_start(void *data)
  {
          union ccb *ccb = data;
          struct ccb_hdr *ccb_h = &ccb->ccb_h;
          struct ccb_scsiio *csio = &ccb->csio;
          struct mfip *sc;
          struct mfi_command *cm;
          struct mfi_pass_frame *pt;

          sc = ccb_h->ccb_mfip_ptr;

          if ((cm = mfi_dequeue_free(sc->mfi)) == NULL)
                  return (NULL);

          pt = &cm->cm_frame->pass;
          pt->header.cmd = MFI_CMD_PD_SCSI_IO;
          pt->header.cmd_status = 0;
          pt->header.scsi_status = 0;
          pt->header.target_id = ccb_h->target_id;
          pt->header.lun_id = ccb_h->target_lun;
          pt->header.flags = 0;
          pt->header.timeout = 0;
          pt->header.data_len = csio->dxfer_len;
          pt->header.sense_len = MFI_SENSE_LEN;
          pt->header.cdb_len = csio->cdb_len;
          pt->sense_addr_lo = cm->cm_sense_busaddr;
          pt->sense_addr_hi = 0;
          if (ccb_h->flags & CAM_CDB_POINTER)
                  bcopy(csio->cdb_io.cdb_ptr, &pt->cdb[0], csio->cdb_len);
          else
                  bcopy(csio->cdb_io.cdb_bytes, &pt->cdb[0], csio->cdb_len);

          cm->cm_complete = mfip_done;
          cm->cm_private = ccb;
          cm->cm_sg = &pt->sgl;
          cm->cm_total_frame_size = MFI_PASS_FRAME_SIZE;
          cm->cm_data = csio->data_ptr;
          cm->cm_len = csio->dxfer_len;
          switch (ccb_h->flags & CAM_DIR_MASK) {
          case CAM_DIR_IN:
                  cm->cm_flags = MFI_CMD_DATAIN;
                  break;
          case CAM_DIR_OUT:
                  cm->cm_flags = MFI_CMD_DATAOUT;
                  break;
          case CAM_DIR_NONE:
          default:
                  cm->cm_data = NULL;
                  cm->cm_len = 0;
                  cm->cm_flags = 0;
                  break;
          }

          TAILQ_REMOVE(&sc->mfi->mfi_cam_ccbq, ccb_h, sim_links.tqe);
          return (cm);
  }

  static void
  mfip_done(struct mfi_command *cm)
  {
          union ccb *ccb = cm->cm_private;
          struct ccb_hdr *ccb_h = &ccb->ccb_h;
          struct ccb_scsiio *csio = &ccb->csio;
          struct mfip *sc;
          struct mfi_pass_frame *pt;

          sc = ccb_h->ccb_mfip_ptr;
          pt = &cm->cm_frame->pass;

          switch (pt->header.cmd_status) {
          case MFI_STAT_OK:
          {
                  uint8_t command, device;

                  ccb_h->status = CAM_REQ_CMP;
                  csio->scsi_status = pt->header.scsi_status;

                  if (ccb_h->flags & CAM_CDB_POINTER)
                          command = ccb->csio.cdb_io.cdb_ptr[0];
                  else
                          command = ccb->csio.cdb_io.cdb_bytes[0];

                  if (command == INQUIRY) {
                          device = ccb->csio.data_ptr[0] & 0x1f;
                          if ((device == T_DIRECT) || (device == T_PROCESSOR))
                                  csio->data_ptr[0] =
                                      (device & 0xe0) | T_NODEVICE;
                  }

                  break;
          }
          case MFI_STAT_SCSI_DONE_WITH_ERROR:
          {
                  int sense_len;

                  ccb_h->status = CAM_SCSI_STATUS_ERROR | CAM_AUTOSNS_VALID;
                  csio->scsi_status = pt->header.scsi_status;

                  sense_len = min(pt->header.sense_len,
                      sizeof(struct scsi_sense_data));
                  bzero(&csio->sense_data, sizeof(struct scsi_sense_data));
                  bcopy(&cm->cm_sense->data[0], &csio->sense_data, sense_len);
                  break;
          }
          case MFI_STAT_DEVICE_NOT_FOUND:
                  ccb_h->status = CAM_SEL_TIMEOUT;
                  break;
          case MFI_STAT_SCSI_IO_FAILED:
                  ccb_h->status = CAM_REQ_CMP_ERR;
                  csio->scsi_status = pt->header.scsi_status;
                  break;
          default:
                  ccb_h->status = CAM_REQ_CMP_ERR;
                  csio->scsi_status = pt->header.scsi_status;
                  break;
          }

          mfi_release_command(cm);
          xpt_done(ccb);
  }

  static device_method_t mfip_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_probe,         mfip_probe),
          DEVMETHOD(device_attach,        mfip_attach),
          DEVMETHOD(device_detach,        mfip_detach),
          { 0, 0 }
  };

  static driver_t mfip_driver = {
          "mfip",
          mfip_methods,
          sizeof(struct mfip)
  };

  DRIVER_MODULE(mfip, mfi, mfip_driver, mfip_devclass, 0, 0);
  MODULE_DEPEND(mfip, cam, 1, 1, 1);
  MODULE_DEPEND(mfip, mfi, 1, 1, 1);

以下章节将按执行顺序大致描述 示例 14-1 中定义的函数。

作为旁白,因为 mfip_probe 非常基础,并且因为我们已经在其他地方检查了类似的代码,所以我会省略对其的讨论。

mfip_attach 函数

mfip_attach 函数是此驱动程序的 device_attach 实现。以下是其函数定义(再次):

static int
mfip_attach(device_t dev)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = device_get_softc(dev);
        if (sc == NULL)
                return (EINVAL);

        mfi = device_get_softc(device_get_parent(dev));
        sc->dev = dev;
        sc->mfi = mfi;
        mfi->mfi_cam_start = mfip_start;

        if ((sc->devq = cam_simq_alloc(MFI_SCSI_MAX_CMDS)) == NULL)
                return (ENOMEM);

        sc->sim = cam_sim_alloc(mfip_action, mfip_poll, "mfi", sc,
            device_get_unit(dev), &mfi->mfi_io_lock, 1, MFI_SCSI_MAX_CMDS,
            sc->devq);
        if (sc->sim == NULL) {
                cam_simq_free(sc->devq);
                device_printf(dev, "cannot allocate CAM SIM\n");
                return (EINVAL);
        }

        mtx_lock(&mfi->mfi_io_lock);
        if (xpt_bus_register(sc->sim, dev, 0) != 0) {
                device_printf(dev,
                    "cannot register SCSI pass-through bus\n");
                cam_sim_free(sc->sim, FALSE);
                cam_simq_free(sc->devq);
                mtx_unlock(&mfi->mfi_io_lock);
                return (EINVAL);
        }
        mtx_unlock(&mfi->mfi_io_lock);

        return (0);
}

此函数首先调用 cam_simq_alloc 以分配一个 SIM 队列。简单来说,SIM 队列 确保 HBAs 不会被 I/O 请求淹没。请看,来自外围模块的 I/O 请求位于 SIM 队列中,等待服务。当一个队列满时,任何额外的请求都会被拒绝。

接下来, 调用 cam_sim_alloc 以分配一个 SIM(或总线)描述符。请注意,如果 HBA 实现了多个总线(或通道),则每个总线都需要自己的描述符。

最后, xpt_bus_register 接收 cam_sim_alloc 返回的描述符并将其注册到 CAM 子系统。

mfip_detach 函数

mfip_detach 函数是此驱动程序的 device_detach 实现。以下是其函数定义(再次):

static int
mfip_detach(device_t dev)
{
        struct mfip *sc;

        sc = device_get_softc(dev);
        if (sc == NULL)
                return (EINVAL);

        if (sc->sim != NULL) {
                mtx_lock(&sc->mfi->mfi_io_lock);
              xpt_bus_deregister(cam_sim_path(sc->sim));
              cam_sim_free(sc->sim, FALSE);
                mtx_unlock(&sc->mfi->mfi_io_lock);
        }

        if (sc->devq != NULL)
              cam_simq_free(sc->devq);

        return (0);
}

此函数首先通过 注销并 释放其 SIM 描述符。之后,其 SIM 队列被 释放。

mfip_action 函数

mfip_action 函数在 mfip_attach 中定义为动作例程(验证见 cam_sim_alloc 的第一个参数)。动作例程 在每次 SIM 收到 CCB 时执行。

注意

回想一下,CCB 包含一个要执行 I/O 请求(或命令)以及目标设备的标识(即 I/O 请求的预期接收者)。

基本上,mfip_action 与 图 14-1 中显示的 ahc_action 函数类似。以下是其函数定义(再次):

static void
mfip_action(struct cam_sim *sim, union ccb *ccb)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = cam_sim_softc(sim);
        mfi = sc->mfi;
        mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

      switch (ccb->ccb_h.func_code) {
      case XPT_PATH_INQ:
        {
                struct ccb_pathinq *cpi;

                cpi = &ccb->cpi;
                cpi->version_num = 1;
                cpi->hba_inquiry = PI_SDTR_ABLE | PI_TAG_ABLE | PI_WIDE_16;
                cpi->target_sprt = 0;
                cpi->hba_misc = PIM_NOBUSRESET | PIM_SEQSCAN;
                cpi->hba_eng_cnt = 0;
                cpi->max_target = MFI_SCSI_MAX_TARGETS;
                cpi->max_lun = MFI_SCSI_MAX_LUNS;
                cpi->initiator_id = MFI_SCSI_INITIATOR_ID;
                strncpy(cpi->sim_vid, "FreeBSD", SIM_IDLEN);
                strncpy(cpi->hba_vid, "LSI", HBA_IDLEN);
                strncpy(cpi->dev_name, cam_sim_name(sim), DEV_IDLEN);
                cpi->unit_number = cam_sim_unit(sim);
                cpi->bus_id = cam_sim_bus(sim);
                cpi->base_transfer_speed = 150000;
                cpi->protocol = PROTO_SCSI;
                cpi->protocol_version = SCSI_REV_2;
                cpi->transport = XPORT_SAS;
                cpi->transport_version = 0;

                cpi->ccb_h.status = CAM_REQ_CMP;
                break;
        }
      case XPT_RESET_BUS:
                ccb->ccb_h.status = CAM_REQ_CMP;
                break;
      case XPT_RESET_DEV:
                ccb->ccb_h.status = CAM_REQ_CMP;
                break;
      case XPT_GET_TRAN_SETTINGS:
        {
                struct ccb_trans_settings_sas *sas;

                ccb->cts.protocol = PROTO_SCSI;
                ccb->cts.protocol_version = SCSI_REV_2;
                ccb->cts.transport = XPORT_SAS;
                ccb->cts.transport_version = 0;
                sas = &ccb->cts.xport_specific.sas;
                sas->valid &= ˜CTS_SAS_VALID_SPEED;
                sas->bitrate = 150000;

                ccb->ccb_h.status = CAM_REQ_CMP;
                break;
        }
      case XPT_SET_TRAN_SETTINGS:
                ccb->ccb_h.status = CAM_FUNC_NOTAVAIL;
                break;
      case XPT_SCSI_IO:
        {
                struct ccb_hdr *ccb_h = &ccb->ccb_h;
                struct ccb_scsiio *csio = &ccb->csio;

                ccb_h->status = CAM_REQ_INPROG;
                if (csio->cdb_len > MFI_SCSI_MAX_CDB_LEN) {
                        ccb_h->status = CAM_REQ_INVALID;
                        break;
                }
                if ((ccb_h->flags & CAM_DIR_MASK) != CAM_DIR_NONE) {
                        if (ccb_h->flags & CAM_DATA_PHYS) {
                                ccb_h->status = CAM_REQ_INVALID;
                                break;
                        }
                        if (ccb_h->flags & CAM_SCATTER_VALID) {
                                ccb_h->status = CAM_REQ_INVALID;
                                break;
                        }
                }

                ccb_h->ccb_mfip_ptr = sc;
                TAILQ_INSERT_TAIL(&mfi->mfi_cam_ccbq, ccb_h, sim_links.tqe);
                mfi_startio(mfi);

                return;
        }
        default:
                ccb->ccb_h.status = CAM_REQ_INVALID;
                break;
        }

        xpt_done(ccb);
        return;
}

大多数动作例程只是接受一个 CCB 并 根据 ccb_h.func_code 变量分支,该变量表示要执行的 I/O 操作。

目前,我将专注于 mfip_action 的结构,避免其具体细节。mfip_action 的深入解释可以在 动作例程 中找到,位于 xpt_bus_register 函数。

如您所见,这个函数可以执行六种 I/O 操作之一:它可以 图片 返回 SIM 和 HBA 属性,重置 图片 总线或 图片 设备,图片 获取或 图片 设置传输设置,或者 图片 向设备发出 SCSI 命令。

mfip_poll 函数

mfip_poll 函数定义在 mfip_attach 中作为轮询例程(验证见 cam_sim_alloc 的第二个参数)。通常,轮询例程 包装 SIM 的中断处理程序。例如,当中断不可用(例如,在内核恐慌之后)时,CAM 子系统将使用轮询例程来运行其中断处理程序。

以下是 mfip_poll 函数的定义(再次):

static void
mfip_poll(struct cam_sim *sim)
{
        return;
}

由于这个 SIM 没有实现中断处理程序,mfip_poll 只是 图片 返回。

mfip_start 函数

mfip_start 函数将 SCSI 命令转换为硬件特定命令。这个函数仅由 mfi_startio 调用。

注意

mfi_startio 函数定义在 mfi.c 中(本书未对其进行描述)。mfi_startiomfip_action 调用(在 mfip_action 函数 中描述,见 mfip_action 函数)以向设备发出 SCSI 命令。

这里是 mfip_start 函数的定义(再次):

static struct mfi_command *
mfip_start(void *data)
{
        union ccb *ccb = data;
        struct ccb_hdr *ccb_h = &ccb->ccb_h;
        struct ccb_scsiio *csio = &ccb->csio;
        struct mfip *sc;
        struct mfi_command *cm;
        struct mfi_pass_frame *pt;

        sc = ccb_h->ccb_mfip_ptr;

        if ((cm = mfi_dequeue_free(sc->mfi)) == NULL)
                return (NULL);

        pt = &cm->cm_frame->pass;
        pt->header.cmd = MFI_CMD_PD_SCSI_IO;
        pt->header.cmd_status = 0;
        pt->header.scsi_status = 0;
        pt->header.target_id = ccb_h->target_id;
        pt->header.lun_id = ccb_h->target_lun;
        pt->header.flags = 0;
        pt->header.timeout = 0;
        pt->header.data_len = csio->dxfer_len;
        pt->header.sense_len = MFI_SENSE_LEN;
        pt->header.cdb_len = csio->cdb_len;
        pt->sense_addr_lo = cm->cm_sense_busaddr;
        pt->sense_addr_hi = 0;
        if (ccb_h->flags & CAM_CDB_POINTER)
                bcopy(csio->cdb_io.cdb_ptr, &pt->cdb[0], csio->cdb_len);
        else
                bcopy(csio->cdb_io.cdb_bytes, &pt->cdb[0], csio->cdb_len);

        cm->cm_complete = mfip_done;
        cm->cm_private = ccb;
        cm->cm_sg = &pt->sgl;
        cm->cm_total_frame_size = MFI_PASS_FRAME_SIZE;
        cm->cm_data = csio->data_ptr;
        cm->cm_len = csio->dxfer_len;
        switch (ccb_h->flags & CAM_DIR_MASK) {
        case CAM_DIR_IN:
                cm->cm_flags = MFI_CMD_DATAIN;
                break;
        case CAM_DIR_OUT:
                cm->cm_flags = MFI_CMD_DATAOUT;
                break;
        case CAM_DIR_NONE:
        default:
                cm->cm_data = NULL;
                cm->cm_len = 0;
                cm->cm_flags = 0;
                break;
        }

        TAILQ_REMOVE(&sc->mfi->mfi_cam_ccbq, ccb_h, sim_links.tqe);
        return (cm);
}

如您所见,这个函数相当直接——它只是一系列赋值操作。在我们检查完 struct ccb_scsiiostruct ccb_hdr 之前,这些内容出现在 XPT_SCSI_IO 的 XPT_SCSI_IO 中,我将推迟对这个函数的详细分析。

注意到 图片 mfip_done 被设置为硬件特定命令的完成例程。

mfip_done 函数

如前所述,mfip_done 函数是这个 SIM 的完成例程。它在设备完成硬件特定命令后立即由 mfi_intr 执行。

注意

mfi_intr 函数是 mfi(4) 的中断处理程序。它在 mfi.c 中定义。

基本上,mfip_done 与 图 14-1 中显示的 ahc_done 函数类似。这是它的函数定义(再次):

static void
mfip_done(struct mfi_command *cm)
{
        union ccb *ccb = cm->cm_private;
        struct ccb_hdr *ccb_h = &ccb->ccb_h;
        struct ccb_scsiio *csio = &ccb->csio;
        struct mfip *sc;
        struct mfi_pass_frame *pt;

        sc = ccb_h->ccb_mfip_ptr;
        pt = &cm->cm_frame->pass;

        switch (pt->header.cmd_status) {
        case MFI_STAT_OK:
        {
                uint8_t command, device;

              ccb_h->status = CAM_REQ_CMP;
                csio->scsi_status = pt->header.scsi_status;

                if (ccb_h->flags & CAM_CDB_POINTER)
                        command = ccb->csio.cdb_io.cdb_ptr[0];
                else
                        command = ccb->csio.cdb_io.cdb_bytes[0];

                if (command == INQUIRY) {
                        device = ccb->csio.data_ptr[0] & 0x1f;
                        if ((device == T_DIRECT) || (device == T_PROCESSOR))
                                csio->data_ptr[0] =
                                    (device & 0xe0) | T_NODEVICE;
                }

                break;
        }
        case MFI_STAT_SCSI_DONE_WITH_ERROR:
        {
                int sense_len;

              ccb_h->status = CAM_SCSI_STATUS_ERROR | CAM_AUTOSNS_VALID;
                csio->scsi_status = pt->header.scsi_status;

                sense_len = min(pt->header.sense_len,
                    sizeof(struct scsi_sense_data));
                bzero(&csio->sense_data, sizeof(struct scsi_sense_data));
                bcopy(&cm->cm_sense->data[0], &csio->sense_data, sense_len);
                break;
        }
        case MFI_STAT_DEVICE_NOT_FOUND:
              ccb_h->status = CAM_SEL_TIMEOUT;
                break;
        case MFI_STAT_SCSI_IO_FAILED:
              ccb_h->status = CAM_REQ_CMP_ERR;
                csio->scsi_status = pt->header.scsi_status;
                break;
        default:
              ccb_h->status = CAM_REQ_CMP_ERR;
                csio->scsi_status = pt->header.scsi_status;
                break;
        }

        mfi_release_command(cm);
      xpt_done(ccb);
}

通常,执行例程会接受一个httpatomoreillycomsourcenostarchimages1137499.png硬件特定的命令,并将其完成状态(即成功或失败)附加到其关联的httpatomoreillycomsourcenostarchimages1137501.png httpatomoreillycomsourcenostarchimages1137503.png httpatomoreillycomsourcenostarchimages1137505.png httpatomoreillycomsourcenostarchimages1137507.png httpatomoreillycomsourcenostarchimages1137509.png CCB。完成此操作后,httpatomoreillycomsourcenostarchimages1137511.png xpt_done 被调用以处理完成的 CCB。

注意

mfi(4) 代码库使用 DMA 从设备获取完成状态。

现在你已经熟悉了示例 14-1,我将详细说明它所使用的不同函数、结构和构造。

SIM 注册例程

如前所述,使用 CAM 子系统注册 SIM 涉及三个功能:

  • cam_simq_alloc

  • cam_sim_alloc

  • xpt_bus_register

cam_simq_alloc 函数

cam_simq_alloc 函数分配一个 SIM 队列。

#include <cam/cam_sim.h>
#include <cam/cam_queue.h>

struct cam_devq *
cam_simq_alloc(u_int32_t max_sim_transactions);

在这里,max_sim_transactions 表示 SIM 队列的大小。通常,它计算如下:

max_sim_transactions = number_of_supported_devices *
    number_of_commands_that_can_be_concurrently_processed_per_device;

cam_sim_alloc 函数

cam_sim_alloc 函数分配一个 SIM(或总线)描述符。

注意

如果 HBA 实现了多个总线(或通道),则每个总线都需要其自己的描述符。

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/mutex.h>

#include <cam/cam_sim.h>
#include <cam/cam_queue.h>

struct cam_sim *
cam_sim_alloc(sim_action_func sim_action, sim_poll_func sim_poll,
    const char *sim_name, void *softc, u_int32_t unit, struct mtx *mtx,
    int max_dev_transactions, int max_tagged_dev_transactions,
    struct cam_devq *queue);

因为 cam_sim_alloc 的前六个参数相当明显——它们正是其名称所暗示的——所以我将省略对它们的讨论。

max_dev_transactions 参数指定每个设备并发事务的最大数量。此参数仅适用于不支持 SCSI 标记命令队列(SCSI TCQ)的设备。通常,max_dev_transactions 总是设置为 1

max_tagged_dev_transactions 参数与 max_dev_transactions 相同,但它仅适用于支持 SCSI TCQ 的设备。

queue 参数期望一个指向 SIM 队列的指针(即 cam_simq_alloc 的返回值)。

xpt_bus_register 函数

xpt_bus_register 函数将 SIM 注册到 CAM 子系统。

#include <cam/cam_sim.h>
#include <cam/cam_xpt_sim.h>

int32_t
xpt_bus_register(struct cam_sim *sim, device_t parent, u_int32_t bus)

在这里,sim 指定要注册的 SIM(即 cam_sim_alloc 的返回值),而 bus 表示其总线号。parent 参数目前未使用。

注意

如果 HBA 实现了多个总线(或通道),则每个总线都需要其唯一的总线号。

动作例程

如前所述,动作例程在 SIM 收到 CCB 时执行。你可以将动作例程视为 SIM 的“主函数”。

这里是动作例程的函数原型(来自 <cam/cam_sim.h> 头文件):

typedef void (*sim_action_func)(struct cam_sim *sim, union ccb *ccb);

回想一下,动作例程根据 ccb->ccb_h.func_code 变量进行 switch,该变量包含表示要执行 I/O 操作的常量。在本章的其余部分,我将详细介绍最常见的常量/操作。

注意

对于常量/操作的完整列表,请参阅 <cam/cam_ccb.h> 头文件中定义的 xpt_opcode 枚举。

XPT_PATH_INQ

XPT_PATH_INQ 常量指定了一个路径查询操作,它返回 SIM 和 HBA 属性。传递给 XPT_PATH_INQ 的操作例程简单地填充一个 ccb_pathinq 结构体然后返回。

struct ccb_pathinq<cam/cam_ccb.h> 头文件中定义如下:

struct ccb_pathinq {
        struct ccb_hdr ccb_h;           /* Header information fields.   */
        u_int8_t    version_num;        /* Version number.              */
        u_int8_t    hba_inquiry;        /* Imitate INQ byte 7\.          */
        u_int8_t    target_sprt;        /* Target mode support flags.   */
        u_int8_t    hba_misc;           /* Miscellaneous HBA features.  */
        u_int16_t   hba_eng_cnt;        /* HBA engine count.            */

        u_int8_t vuhba_flags[VUHBALEN]; /* Vendor unique capabilities.  */
        u_int32_t   max_target;         /* Maximum supported targets.   */
        u_int32_t   max_lun;            /* Maximum supported LUN.       */
        u_int32_t   async_flags;        /* Asynchronous handler flags.  */
        path_id_t   hpath_id;      /* Highest path ID in the subsystem. */
        target_id_t initiator_id;       /* HBA ID on the bus.           */

        char sim_vid[SIM_IDLEN];        /* SIM vendor ID.               */
        char hba_vid[HBA_IDLEN];        /* HBA vendor ID.               */
        char dev_name[DEV_IDLEN];       /* SIM device name.             */

        u_int32_t   unit_number;        /* SIM unit number.             */
        u_int32_t   bus_id;             /* SIM bus ID.                  */
        u_int32_t base_transfer_speed;  /* Base bus speed in KB/sec.    */

        cam_proto   protocol;           /* CAM protocol.                */
        u_int       protocol_version;   /* CAM protocol version.        */
        cam_xport   transport;          /* Transport (e.g., FC, USB).   */
        u_int       transport_version;  /* Transport version.           */
        union {
                struct ccb_pathinq_settings_spi spi;
                struct ccb_pathinq_settings_fc fc;
                struct ccb_pathinq_settings_sas sas;
                char ccb_pathinq_settings_opaque[PATHINQ_SETTINGS_SIZE];
        } xport_specific;

        u_int maxio;    /* Maximum supported I/O size (in bytes).       */
};

这里是一个 XPT_PATH_INQ 操作的示例(摘自 示例 14-1):

static void
mfip_action(struct cam_sim *sim, union ccb *ccb)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = cam_sim_softc(sim);
        mfi = sc->mfi;
        mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

        switch (ccb->ccb_h.func_code) {
        case XPT_PATH_INQ:
        {
                struct ccb_pathinq *cpi;

                cpi = &ccb->cpi;
                cpi->version_num = 1;
                cpi->hba_inquiry = PI_SDTR_ABLE | PI_TAG_ABLE | PI_WIDE_16;
                cpi->target_sprt = 0;
                cpi->hba_misc = PIM_NOBUSRESET | PIM_SEQSCAN;
                cpi->hba_eng_cnt = 0;
                cpi->max_target = MFI_SCSI_MAX_TARGETS;
                cpi->max_lun = MFI_SCSI_MAX_LUNS;
                cpi->initiator_id = MFI_SCSI_INITIATOR_ID;
                strncpy(cpi->sim_vid, "FreeBSD", SIM_IDLEN);
                strncpy(cpi->hba_vid, "LSI", HBA_IDLEN);
                strncpy(cpi->dev_name, cam_sim_name(sim), DEV_IDLEN);
                cpi->unit_number = cam_sim_unit(sim);
                cpi->bus_id = cam_sim_bus(sim);
                cpi->base_transfer_speed = 150000;
                cpi->protocol = PROTO_SCSI;
                cpi->protocol_version = SCSI_REV_2;
                cpi->transport = XPORT_SAS;
                cpi->transport_version = 0;

              cpi->ccb_h.status = CAM_REQ_CMP;
                break;
        }
...
        default:
              ccb->ccb_h.status = CAM_REQ_INVALID;
                break;
        }

        xpt_done(ccb);
        return;
}

注意到 ccb_pathinq 结构体是由 CCB 提供的。此外,注意到任何操作的 成功或 失败都返回在 ccb_h.status 中。

XPT_RESET_BUS

XPT_RESET_BUS 常量指定了一个总线重置操作。正如你所预期的那样,XPT_RESET_BUS 非常特定于硬件。以下是一个最小化实现(摘自 示例 14-1):

static void
mfip_action(struct cam_sim *sim, union ccb *ccb)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = cam_sim_softc(sim);
        mfi = sc->mfi;
        mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

        switch (ccb->ccb_h.func_code) {
...
        case XPT_RESET_BUS:
                ccb->ccb_h.status = CAM_REQ_CMP;
                break;
...
        default:
                ccb->ccb_h.status = CAM_REQ_INVALID;
                break;
        }

        xpt_done(ccb);
        return;
}

在这里, sim 是要重置的总线。不出所料,最小化实现放弃了任何“真实”的工作,并简单地返回 成功。

许多 SIM 使用最小化实现。一个“正确”的实现超出了本书的范围。

XPT_GET_TRAN_SETTINGS

XPT_GET_TRAN_SETTINGS 常量表示一个返回当前传输设置或用户定义的上限的 I/O 操作。传递给 XPT_GET_TRAN_SETTINGS 的操作例程简单地填充一个 ccb_trans_settings 结构体然后返回。

struct ccb_trans_settings 定义在 <cam/cam_ccb.h> 中,如下所示:

typedef enum {
        CTS_TYPE_CURRENT_SETTINGS,      /* Current transfer settings.   */
        CTS_TYPE_USER_SETTINGS          /* User-defined upper limits.   */
} cts_type;

struct ccb_trans_settings {
        struct ccb_hdr ccb_h;           /* Header information fields.   */
        cts_type  type;                 /* Current or user settings?    */
        cam_proto protocol;             /* CAM protocol.                */
        u_int     protocol_version;     /* CAM protocol version.        */
        cam_xport transport;            /* Transport (e.g., FC, USB).   */
        u_int     transport_version;    /* Transport version.           */

      union {
                u_int valid;            /* Which field(s) to honor.     */
                struct ccb_trans_settings_scsi scsi;
        } proto_specific;

      union {
                u_int valid;            /* Which field(s) to honor.     */
                struct ccb_trans_settings_spi spi;
                struct ccb_trans_settings_fc fc;
                struct ccb_trans_settings_sas sas;
                struct ccb_trans_settings_ata ata;
                struct ccb_trans_settings_sata sata;
        } xport_specific;
};

如你所见,ccb_trans_settings 将一个 协议结构体和五个 传输特定结构体打包。这些结构体在 <cam/cam_ccb.h> 中定义如下:

struct ccb_trans_settings_scsi {
        u_int           valid;          /* Which field(s) to honor.     */
#define CTS_SCSI_VALID_TQ               0x01
        u_int           flags;
#define CTS_SCSI_FLAGS_TAG_ENB          0x01
};

struct ccb_trans_settings_spi {
        u_int           valid;          /* Which field(s) to honor.     */
#define CTS_SPI_VALID_SYNC_RATE         0x01
#define CTS_SPI_VALID_SYNC_OFFSET       0x02
#define CTS_SPI_VALID_BUS_WIDTH         0x04
#define CTS_SPI_VALID_DISC              0x08
#define CTS_SPI_VALID_PPR_OPTIONS       0x10
        u_int           flags;
#define CTS_SPI_FLAGS_DISC_ENB          0x01
        u_int           sync_period;    /* Sync period.                 */
        u_int           sync_offset;    /* Sync offset.                 */
        u_int           bus_width;      /* Bus width.                   */
        u_int           ppr_options;    /* Parallel protocol request.   */
};

struct ccb_trans_settings_fc {
        u_int           valid;          /* Which field(s) to honor.     */
#define CTS_FC_VALID_WWNN               0x8000
#define CTS_FC_VALID_WWPN               0x4000
#define CTS_FC_VALID_PORT               0x2000
#define CTS_FC_VALID_SPEED              0x1000
        u_int64_t       wwnn;           /* World wide node name.        */
        u_int64_t       wwpn;           /* World wide port name.        */
        u_int32_t       port;           /* 24-bit port ID (if known).   */
        u_int32_t       bitrate;        /* Mbps.                        */
};

struct ccb_trans_settings_sas {
        u_int           valid;          /* Which field(s) to honor.     */
#define CTS_SAS_VALID_SPEED             0x1000
        u_int32_t       bitrate;        /* Mbps.                        */
};

struct ccb_trans_settings_ata {
        u_int           valid;          /* Which field(s) to honor.     */
#define CTS_ATA_VALID_MODE              0x01
#define CTS_ATA_VALID_BYTECOUNT         0x02
#define CTS_ATA_VALID_ATAPI             0x20
        int             mode;           /* Mode.                        */
        u_int           bytecount;      /* PIO transaction length.      */
        u_int           atapi;          /* ATAPI CDB length.            */
};

struct ccb_trans_settings_sata {
        u_int           valid;          /* Which field(s) to honor.     */
#define CTS_SATA_VALID_MODE             0x01
#define CTS_SATA_VALID_BYTECOUNT        0x02
#define CTS_SATA_VALID_REVISION         0x04
#define CTS_SATA_VALID_PM               0x08
#define CTS_SATA_VALID_TAGS             0x10
#define CTS_SATA_VALID_ATAPI            0x20
#define CTS_SATA_VALID_CAPS             0x40
        int             mode;           /* Legacy PATA mode.            */
        u_int           bytecount;      /* PIO transaction length.      */
        int             revision;       /* SATA revision.               */
        u_int           pm_present;     /* PM is present (XPT->SIM).    */
        u_int           tags;           /* Number of allowed tags.      */
        u_int           atapi;          /* ATAPI CDB length.            */
        u_int           caps;           /* Host and device SATA caps.   */
#define CTS_SATA_CAPS_H                 0x0000ffff
#define CTS_SATA_CAPS_H_PMREQ           0x00000001
#define CTS_SATA_CAPS_H_APST            0x00000002
#define CTS_SATA_CAPS_H_DMAAA           0x00000010
#define CTS_SATA_CAPS_D                 0xffff0000
#define CTS_SATA_CAPS_D_PMREQ           0x00010000
#define CTS_SATA_CAPS_D_APST            0x00020000
};

这里是一个 XPT_GET_TRAN_SETTINGS 操作的示例(摘自 示例 14-1):

static void
mfip_action(struct cam_sim *sim, union ccb *ccb)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = cam_sim_softc(sim);
        mfi = sc->mfi;
        mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

        switch (ccb->ccb_h.func_code) {
...
        case XPT_GET_TRAN_SETTINGS:
        {
                struct ccb_trans_settings_sas *sas;

              ccb->cts.protocol = PROTO_SCSI;
                ccb->cts.protocol_version = SCSI_REV_2;
                ccb->cts.transport = XPORT_SAS;
                ccb->cts.transport_version = 0;
                sas = &ccb->cts.xport_specific.sas;
                sas->valid &= ˜CTS_SAS_VALID_SPEED;
                sas->bitrate = 150000;

                ccb->ccb_h.status = CAM_REQ_CMP;
                break;
        }
...
        default:
                ccb->ccb_h.status = CAM_REQ_INVALID;
                break;
        }

        xpt_done(ccb);
        return;
}

注意到 ccb_trans_settings 结构体是由 CCB 提供的。自然地,只有适用于 HBA 的字段被填充。

XPT_SET_TRAN_SETTINGS

正如你所预期的那样,XPT_SET_TRAN_SETTINGSXPT_GET_TRAN_SETTINGS 的反义。也就是说,XPT_SET_TRAN_SETTINGS 根据一个 ccb_trans_settings 结构体更改当前的传输设置。不出所料,并非所有 SIM 都支持此操作。例如:

static void
mfip_action(struct cam_sim *sim, union ccb *ccb)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = cam_sim_softc(sim);
        mfi = sc->mfi;
        mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

        switch (ccb->ccb_h.func_code) {
...
        case XPT_SET_TRAN_SETTINGS:
                ccb->ccb_h.status = CAM_FUNC_NOTAVAIL;
                break;
...
        default:
                ccb->ccb_h.status = CAM_REQ_INVALID;
                break;
        }

        xpt_done(ccb);
        return;
}

此函数声明 XPT_SET_TRAN_SETTINGS 图片 不可用。请注意,“正确”的实现是特定于硬件的,且本书未涉及。

XPT_SCSI_IO

XPT_SCSI_IO 常量表示向设备发出 SCSI 命令的 I/O 操作。这个 SCSI 命令的详细信息存储在两个结构中:ccb_scsiioccb_hdr

struct ccb_scsiio<cam/cam_ccb.h> 中定义如下:

struct ccb_scsiio {
        struct ccb_hdr ccb_h;           /* Header information fields.   */
        union ccb *next_ccb;            /* Next CCB to process.         */
        u_int8_t  *req_map;             /* Mapping information.         */
        u_int8_t  *data_ptr;            /* Data buffer or S/G list.     */
        u_int32_t  dxfer_len;           /* Length of data to transfer.  */

        /* Sense information (used if the command returns an error).    */
        struct scsi_sense_data sense_data;

        u_int8_t   sense_len;           /* Sense information length.    */
        u_int8_t   cdb_len;             /* SCSI command length.         */
        u_int16_t  sglist_cnt;          /* Number of S/G segments.      */
        u_int8_t   scsi_status; /* SCSI status (returned by device).    */
        u_int8_t   sense_resid; /* Residual sense information length.   */
        u_int32_t  resid;               /* Residual data length.        */
        cdb_t      cdb_io;              /* SCSI command.                */
        u_int8_t  *msg_ptr;             /* Message.                     */
        u_int16_t  msg_len;             /* Message length.              */
        u_int8_t   tag_action;          /* Tag action?                  */
        /*
         * tag_action should be the constant below to send a non-tagged
         * transaction or one of the constants in scsi_message.h.
         */
#define CAM_TAG_ACTION_NONE             0x00
        u_int      tag_id;              /* Tag ID (from initiator).     */
        u_int      init_id;             /* Initiator ID.                */
};

struct ccb_hdr 也在 <cam/cam_ccb.h> 中定义,如下所示:

struct ccb_hdr {
        cam_pinfo       pinfo;          /* Priority scheduling.         */
        camq_entry      xpt_links;      /* Transport layer links.       */
        camq_entry      sim_links;      /* SIM layer links.             */
        camq_entry      periph_links;   /* Peripheral layer links.      */
        u_int32_t       retry_count;    /* Retry count.                 */

        /* Pointer to peripheral module done routine.                   */
        void (*cbfcnp)(struct cam_periph *, union ccb *);

        xpt_opcode      func_code;      /* I/O operation to perform.    */
        u_int32_t     status;         /* Completion status.           */
        struct cam_path *path;          /* Path for this CCB.           */
        path_id_t       path_id;        /* Path ID for the request.     */
        target_id_t     target_id;      /* Target device ID.            */
        lun_id_t        target_lun;     /* Target logical unit number.  */
        u_int32_t       flags;          /* CCB flags.                   */
        ccb_ppriv_area  periph_priv;    /* Private use by peripheral.   */
        ccb_spriv_area  sim_priv;       /* Private use by SIM.          */
        u_int32_t       timeout;        /* Timeout value.               */

        /* Deprecated. Don't use!                                       */
        struct callout_handle timeout_ch;
};

struct ccb_hdr 应该看起来很熟悉——它用于在每次 I/O 操作中返回 图片 完成状态。

以下是一个示例 XPT_SCSI_IO 操作(取自 示例 14-1):

#define ccb_mfip_ptr            sim_priv.entries[0].ptr
...
static void
mfip_action(struct cam_sim *sim, union ccb *ccb)
{
        struct mfip *sc;
        struct mfi_softc *mfi;

        sc = cam_sim_softc(sim);
        mfi = sc->mfi;
        mtx_assert(&mfi->mfi_io_lock, MA_OWNED);

        switch (ccb->ccb_h.func_code) {
...
        case XPT_SCSI_IO:
        {
                struct ccb_hdr *ccb_h = &ccb->ccb_h;
                struct ccb_scsiio *csio = &ccb->csio;

                ccb_h->status = CAM_REQ_INPROG;
              if (csio->cdb_len > MFI_SCSI_MAX_CDB_LEN) {
                        ccb_h->status = CAM_REQ_INVALID;
                        break;
                }
              if ((ccb_h->flags & CAM_DIR_MASK) != CAM_DIR_NONE) {
                      if (ccb_h->flags & CAM_DATA_PHYS) {
                                ccb_h->status = CAM_REQ_INVALID;
                              break;
                        }
                      if (ccb_h->flags & CAM_SCATTER_VALID) {
                                ccb_h->status = CAM_REQ_INVALID;
                              break;
                        }
                }

              ccb_h->ccb_mfip_ptr = sc;
                TAILQ_INSERT_TAIL(&mfi->mfi_cam_ccbq, ccb_h, sim_links.tqe);
              mfi_startio(mfi);

                return;
        }
        default:
                ccb->ccb_h.status = CAM_REQ_INVALID;
                break;
        }

        xpt_done(ccb);
        return;
}

此操作首先 图片 检查 SCSI 命令长度是否可接受。然后确定 SCSI 命令是否使用 图片 物理地址或 图片 散列/聚集段来 图片 传输数据。如果使用任一,此操作 图片 图片 将退出(因为它接收了无效的参数)。然后 ccb_h->ccb_mfip_ptr 被设置为软件上下文,并调用 mfi_startio

注意

mfi_startio 函数实际上是发出 SCSI 命令的函数。

从 mfip_start 函数 和 mfip_poll 函数 中回忆起,mfi_startio 调用 mfip_start 将 SCSI 命令转换为特定于硬件的命令。

static struct mfi_command *
mfip_start(void *data)
{
        union ccb *ccb = data;
        struct ccb_hdr *ccb_h = &ccb->ccb_h;
        struct ccb_scsiio *csio = &ccb->csio;
        struct mfip *sc;
        struct mfi_command *cm;
        struct mfi_pass_frame *pt;

        sc = ccb_h->ccb_mfip_ptr;

        if ((cm = mfi_dequeue_free(sc->mfi)) == NULL)
                return (NULL);

        pt = &cm->cm_frame->pass;
        pt->header.cmd = MFI_CMD_PD_SCSI_IO;
        pt->header.cmd_status = 0;
        pt->header.scsi_status = 0;
        pt->header.target_id = ccb_h->target_id;
        pt->header.lun_id = ccb_h->target_lun;
        pt->header.flags = 0;
        pt->header.timeout = 0;
        pt->header.data_len = csio->dxfer_len;
        pt->header.sense_len = MFI_SENSE_LEN;
        pt->header.cdb_len = csio->cdb_len;
        pt->sense_addr_lo = cm->cm_sense_busaddr;
        pt->sense_addr_hi = 0;
        if (ccb_h->flags & CAM_CDB_POINTER)
                bcopy(csio->cdb_io.cdb_ptr, &pt->cdb[0],
 csio->cdb_len);
        else
                bcopy(csio->cdb_io.cdb_bytes, &pt->cdb[0], csio->cdb_len);

        cm->cm_complete = mfip_done;
        cm->cm_private = ccb;
        cm->cm_sg = &pt->sgl;
        cm->cm_total_frame_size = MFI_PASS_FRAME_SIZE;
        cm->cm_data = csio->data_ptr;
        cm->cm_len = csio->dxfer_len;
        switch (ccb_h->flags & CAM_DIR_MASK) {
      case CAM_DIR_IN:
                cm->cm_flags = MFI_CMD_DATAIN;
                break;
      case CAM_DIR_OUT:
                cm->cm_flags = MFI_CMD_DATAOUT;
                break;
      case CAM_DIR_NONE:
        default:
                cm->cm_data = NULL;
                cm->cm_len = 0;
                cm->cm_flags = 0;
                break;
        }

        TAILQ_REMOVE(&sc->mfi->mfi_cam_ccbq, ccb_h, sim_links.tqe);
        return (cm);
}

注意到 struct ccb_hdr 列出了目标设备的 图片 设备 ID 和 图片 逻辑单元号。它还列出了 SCSI 命令是否在 图片 输入、图片 输出或 图片 无操作中传输数据。请注意,XPT_SCSI_IO 操作是从 SIM 的角度看到的。因此,“输入”意味着来自设备,“输出”意味着到设备。

ccb_scsiio结构维护要传输的数据及其长度。它还维护 SCSI 命令(通过一个图片指针或一个图片缓冲区)以及命令的图片长度。

注意

再次强调,上面构建的针对特定硬件的命令是通过mfi_startio向目标设备发出的。

回想一下,一旦设备完成针对特定硬件的命令,它会发送一个中断,这会导致完成例程(在这种情况下是mfip_done)执行。

static void
mfip_done(struct mfi_command *cm)
{
        union ccb *ccb = cm->cm_private;
        struct ccb_hdr *ccb_h = &ccb->ccb_h;
        struct ccb_scsiio *csio = &ccb->csio;
        struct mfip *sc;
        struct mfi_pass_frame *pt;

        sc = ccb_h->ccb_mfip_ptr;
        pt = &cm->cm_frame->pass;

        switch (pt->header.cmd_status) {
        case MFI_STAT_OK:
        {
                uint8_t command, device;

                ccb_h->status = CAM_REQ_CMP;
                csio->scsi_status = pt->header.scsi_status;

                if (ccb_h->flags & CAM_CDB_POINTER)
                        command = ccb->csio.cdb_io.cdb_ptr[0];
                else
                        command = ccb->csio.cdb_io.cdb_bytes[0];

                if (command == INQUIRY) {
                        device = ccb->csio.data_ptr[0] & 0x1f;
                        if ((device == T_DIRECT) || (device == T_PROCESSOR))
                                csio->data_ptr[0] =
                                    (device & 0xe0) | T_NODEVICE;
                }

                break;
        }
      case MFI_STAT_SCSI_DONE_WITH_ERROR:
        {
                int sense_len;

                ccb_h->status = CAM_SCSI_STATUS_ERROR | CAM_AUTOSNS_VALID;
                csio->scsi_status = pt->header.scsi_status;

                sense_len = min(pt->header.sense_len,
                    sizeof(struct scsi_sense_data));
                bzero(&csio->sense_data, sizeof(struct scsi_sense_data));
              bcopy(&cm->cm_sense->data[0], &csio->sense_data,
                    sense_len);
                break;
        }
        case MFI_STAT_DEVICE_NOT_FOUND:
                ccb_h->status = CAM_SEL_TIMEOUT;
                break;
        case MFI_STAT_SCSI_IO_FAILED:
                ccb_h->status = CAM_REQ_CMP_ERR;
                csio->scsi_status = pt->header.scsi_status;
                break;
        default:
                ccb_h->status = CAM_REQ_CMP_ERR;
                csio->scsi_status = pt->header.scsi_status;
                break;
        }

        mfi_release_command(cm);
        xpt_done(ccb);
}

注意,如果针对特定硬件的命令图片返回错误,图片错误信息(或感应数据)会被图片复制到ccb_scsiio结构的图片sense_data字段。

在游戏进行到这一点时,这个函数中未解释的部分应该是显而易见的。

XPT_RESET_DEV

XPT_RESET_DEV常量指定了一个设备重置操作。不出所料,XPT_RESET_DEV相当特定于硬件。以下是一个简单的XPT_RESET_DEV操作(来自bt.c):

注意

bt.c源文件是bt(4)代码库的一部分。

static void
btaction(struct cam_sim *sim, union ccb *ccb)
{
        struct bt_softc *bt;

        bt = (struct bt_softc *)cam_sim_softc(sim);

        switch (ccb->ccb_h.func_code) {
        case XPT_RESET_DEV:
                /* FALLTHROUGH */
        case XPT_SCSI_IO:
        {
...

由于必须发出一个针对特定硬件的命令来重置此设备,XPT_RESET_DEV简单地通过图片级联到XPT_SCSI_IO

虽然这里没有展示,但应强调所有操作都是通过将它们的完成状态附加到它们的 CCB(Completion Command Block)然后调用xpt_done(ccb)来结束的。

结论

本章重点介绍了 HBA 驱动程序,或 SIMs,因为它们是最常见的 CAM 相关驱动程序。当然,CAM 的内容远不止这里所展示的。你甚至可以写一本关于 CAM 的整本书!

第十五章。USB 驱动程序

无标题图片

通用串行总线(USB)是主机控制器(如个人计算机)和外围设备之间的连接协议。它被设计用来用一个所有设备都能连接的单总线来替换多种慢速总线——并行端口、串行端口和 PS/2 连接器(Corbet 等,2005)。

如官方 USB 文档所述,文档可在www.usb.org/developers/找到,USB 设备极其复杂。幸运的是,FreeBSD 提供了一个USB 模块来处理大部分复杂性。本章描述了 USB 模块和驱动程序之间的交互。但首先,需要了解一些关于 USB 设备的基础知识。

关于 USB 设备的信息

USB 主机控制器和 USB 设备之间的通信通过管道(Orwick 和 Smith,2007)进行。一个管道将主机控制器连接到设备上的一个端点。USB 设备可以有最多 32 个端点。每个端点为设备执行特定的通信相关操作,例如接收命令或传输数据。一个端点可以是四种类型之一:

  • 控制

  • 中断

  • 批量

  • 等时

控制端点用于发送和接收控制性质的信息(Oney,2003)。它们通常用于配置设备、发出设备命令、检索设备信息等。USB 协议保证控制事务能够成功执行。所有 USB 设备都有一个名为端点 0 的控制端点。

中断端点以固定速率传输少量数据。请注意,USB 设备在传统意义上不能中断其主机——它们没有异步中断。相反,USB 设备提供中断端点,这些端点被定期轮询。这些端点是 USB 键盘和鼠标(Corbet 等,2005)的主要传输方法。USB 协议保证中断事务能够成功执行。

批量端点传输大量数据。批量事务是无损的。然而,USB 协议不保证在特定时间内完成。批量端点在打印机、大容量存储设备和网络设备上很常见。

等时端点定期传输大量数据。等时事务可能会丢失数据。因此,这些端点用于可以处理数据丢失但依赖于保持数据流恒定的设备,例如音频和视频设备(Corbet 等,2005)。

更多关于 USB 设备的信息

USB 设备上的端点被分组为接口。例如,一个 USB 扬声器可能定义一组端点作为按钮的接口,另一组端点作为音频流的接口。

所有接口都有一个或多个备用设置。一个 备用设置 定义了接口的参数。例如,一个有损音频流接口可能有几个备用设置,这些设置以增加带宽为代价提供不断提高的音频质量。自然地,一次只能有一个备用设置处于活动状态。

注意

“备用设置”这个术语有点误导,因为默认接口设置是第一个备用设置。

图 15-1 展示了端点、接口和备用设置之间的关系.^([10])

一个 USB 设备布局示例

图 15-1. 一个 USB 设备布局示例

如您所见,端点不能在接口之间共享,但可以在一个接口的多个备用设置中使用。此外,每个备用设置可以有不同数量的端点。请注意,端点 0,默认的控制端点,不属于任何接口。

一组接口被称为 设备配置,或简单地称为 配置


^([10]) 图 15-1 来自彭妮·奥里克和盖·史密斯合著的《使用 Windows Driver Foundation 开发驱动程序》(Microsoft Press,2007 年)。

USB 配置结构

在 FreeBSD 中,使用 usb_config 结构来查找和与单个端点进行通信。struct usb_config<dev/usb/usbdi.h> 头文件中定义如下:

struct usb_config {
        /* USB Module Private Data */
        enum usb_hc_mode        usb_mode;

        /* Mandatory Fields */
        uint8_t                 type;
        uint8_t                 endpoint;
        uint8_t                 direction;
        usb_callback_t         *callback;
        usb_frlength_t          bufsize;

        /* Optional Fields */
        usb_timeout_t           timeout;
        usb_timeout_t           interval;
        usb_frcount_t           frames;
        uint8_t                 ep_index;
        uint8_t                 if_index;

        /* USB Transfer Flags */
        struct usb_xfer_flags   flags;
};

struct usb_config 中的许多字段必须由 USB 驱动程序初始化。这些字段将在以下章节中描述。

必需字段

type 字段指定端点类型。此字段的有效值包括 UE_CONTROLUE_BULKUE_INTERRUPTUE_ISOCHRONOUS

endpoint 字段指定端点号。UE_ADDR_ANY 的值表示端点号不重要——其他字段用于找到正确的端点。

direction 字段指定端点方向。此字段的有效值显示在 表 15-1 中。

表 15-1. USB 端点方向符号常量

常量 描述
UE_DIR_IN 指定端点为 IN 端点;即端点从设备向主机传输数据
UE_DIR_OUT 指定端点为 OUT 端点;即端点从主机向设备传输数据
UE_DIR_ANY 指定端点支持双向传输

注意

端点的方向是从主机的角度来指定的。

callback字段表示一个强制性的回调函数。该函数在由typeendpointdirection指定的端点传输数据之前和之后执行。我们将在 USB 传输(在 FreeBSD 中)")中进一步讨论此函数。

bufsize字段表示由typeendpointdirection指定的端点的缓冲区大小。正如你所期望的,bufsize用于type交易。

如本节标题所暗示的,前面的字段必须在每个usb_config结构中定义。

可选字段

timeout字段设置交易超时以毫秒为单位。如果timeout0或未定义,并且typeUE_ISOCHRONOUS,则将使用 250 毫秒的超时。

interval字段的意义基于type的值。表 15-2")详细说明了interval的用途(基于type)。

表 15-2. 间隔的目的(基于端点类型)

端点类型 间隔设置什么
--- ---
UE_CONTROL interval设置交易延迟以毫秒为单位;换句话说,在控制交易发生之前必须经过interval毫秒
UE_INTERRUPT interval设置以毫秒为单位的轮询速率;换句话说,主机控制器将每隔interval毫秒轮询中断端点;如果interval0或未定义,则使用端点的默认轮询速率
UE_BULK 对于批量端点,interval不起作用
UE_ISOCHRONOUS 对于等时端点,interval不起作用

frames字段表示由typeendpointdirection指定的端点支持的 USB 帧的最大数量。在 FreeBSD 中,USB 帧仅仅是“数据包”,它们从一个端点传送到另一个端点。USB 帧由一个或多个USB 数据包组成,这些数据包实际上包含数据。

ep_index字段要求一个非负整数。如果有多个端点通过类型、端点和方向被识别——当端点是UE_ADDR_ANY时可能会发生这种情况——则ep_index的值将用于选择一个。

if_index字段指定接口号(基于传递给usbd_transfer_setupifaces参数,该参数在 USB 配置结构管理例程中描述)。

USB 传输标志

flags字段设置由typeendpointdirection指定的端点的交易属性。该字段期望一个usb_xfer_flags结构。

struct usb_xfer_flags<dev/usb/usbdi.h>头文件中定义如下:

struct usb_xfer_flags {
        uint8_t force_short_xfer : 1;
        uint8_t short_xfer_ok    : 1;
        uint8_t short_frames_ok  : 1;
        uint8_t pipe_bof         : 1;
        uint8_t proxy_buffer     : 1;
        uint8_t ext_buffer       : 1;
        uint8_t manual_status    : 1;
        uint8_t no_pipe_ok       : 1;
        uint8_t stall_pipe       : 1;
};

struct usb_xfer_flags 中的所有字段都是可选的。这些字段是 1 位的,并作为标志使用。它们在表 15-3 中详细说明。

表 15-3. USB 传输标志

标志 描述
force_short_xfer 导致短传输;短传输基本上发送一个短的 USB 数据包,这通常表示“事务结束;”此标志可以在任何时候设置
short_xfer_ok 表示接收短传输是可以接受的;此标志可以在任何时候设置
short_frames_ok 表示接收大量短 USB 帧是可以接受的;此标志只能影响UE_INTERRUPTUE_BULK端点;它可以在任何时候设置
pipe_bof 导致任何失败的 USB 事务都保留在其队列中的第一个位置;这保证了所有事务都按 FIFO 顺序完成;此标志可以在任何时候设置
proxy_buffer bufsize向上舍入到最大的 USB 帧大小;此标志在驱动程序初始化后不能设置
ext_buffer 表示所有事务都将使用外部 DMA 缓冲区;此标志在驱动程序初始化后不能设置
manual_status 停止在控制事务中发生握手/状态阶段;此标志可以在任何时候设置
no_pipe_ok 导致忽略USB_ERR_NO_PIPE错误;此标志在驱动程序初始化后不能设置
stall_pipe 导致在每次事务之前,由typeendpointdirection指定的端点“停滞”;此标志可以在任何时候设置

注意

如果你对这些描述中的某些内容不理解,不要担心;我会在稍后详细说明。

USB 传输(在 FreeBSD 中)

记住,callback在由typeendpointdirection指定的端点传输数据之前和之后执行。下面是其函数原型:

typedef void (usb_callback_t)(struct usb_xfer *, usb_error_t);

在这里, struct usb_xfer * 包含传输状态:

struct usb_xfer {
...
        uint8_t         usb_state;
/* Set when callback is executed before a data transfer. */
#define USB_ST_SETUP            0
/* Set when callback is executed after a data transfer. */
#define USB_ST_TRANSFERRED      1
/* Set when a transfer error occurs. */
#define USB_ST_ERROR            2
...
};

通常,你会在switch语句中使用struct usb_xfer *来为每个传输状态提供一个代码块。一些示例代码可以帮助阐明我的意思。

注意

只关注代码的结构,忽略它的功能。

static void
ulpt_status_callback(struct usb_xfer *transfer, usb_error_t error)
{
        struct ulpt_softc *sc = usbd_xfer_softc(transfer);
        struct usb_device_request req;
        struct usb_page_cache *pc;
        uint8_t current_status, new_status;

        switch (USB_GET_STATE(transfer)) {
      case USB_ST_SETUP:
                req.bmRequestType = UT_READ_CLASS_INTERFACE;
                req.bRequest = UREQ_GET_PORT_STATUS;
                USETW(req.wValue, 0);
                req.wIndex[0] = sc->sc_iface_num;
                req.wIndex[1] = 0;
                USETW(req.wLength, 1);

                pc = usbd_xfer_get_frame(transfer, 0);
                usbd_copy_in(pc, 0, &req, sizeof(req));
                usbd_xfer_set_frame_len(transfer, 0, sizeof(req));
                usbd_xfer_set_frame_len(transfer, 1, 1);
                usbd_xfer_set_frames(transfer, 2);
              usbd_transfer_submit(transfer);

                break;
      case USB_ST_TRANSFERRED:
                pc = usbd_xfer_get_frame(transfer, 1);
                usbd_copy_out(pc, 0, &current_status, 1);

                current_status = (current_status ^ LPS_INVERT) & LPS_MASK;
                new_status = current_status & ˜sc->sc_previous_status;
                sc->sc_previous_status = current_status;

                if (new_status & LPS_NERR)
                       log(LOG_NOTICE, "%s: output error\n",
                            device_get_nameunit(sc->sc_dev));
                else if (new_status & LPS_SELECT)
                       log(LOG_NOTICE, "%s: offline\n",
                            device_get_nameunit(sc->sc_dev));
                else if (new_status & LPS_NOPAPER)
                       log(LOG_NOTICE, "%s: out of paper\n",
                            device_get_nameunit(sc->sc_dev));

                break;
        default:
                break;
        }
}

注意如何使用 struct usb_xfer * 作为switch语句的表达式(正如你所期望的,宏USB_GET_STATE返回传输状态)。

当在数据传输之前执行callback时,将常量 USB_ST_SETUP 设置。这种情况处理任何传输前的操作。它总是以 usbd_transfer_submit 结束,这启动数据传输。

常量 USB_ST_TRANSFERRED 在数据传输后执行 callback 时被设置。在这种情况下,执行任何传输后的操作,例如 打印日志消息。

USB 配置结构管理例程

FreeBSD 内核提供了以下函数用于处理 usb_config 结构:

#include <dev/usb/usb.h>
#include <dev/usb/usbdi.h>
#include <dev/usb/usbdi_util.h>

usb_error_t
usbd_transfer_setup(struct usb_device *udev, const uint8_t *ifaces,
    struct usb_xfer **pxfer, const struct usb_config *setup_start,
    uint16_t n_setup, void *priv_sc, struct mtx *priv_mtx);

void
usbd_transfer_unsetup(struct usb_xfer **pxfer, uint16_t n_setup);

void
usbd_transfer_start(struct usb_xfer *xfer);

void
usbd_transfer_stop(struct usb_xfer *xfer);

void
usbd_transfer_drain(struct usb_xfer *xfer);

usbd_transfer_setup 函数接受一个 usb_config 结构数组,并设置一个 usb_xfer 结构数组。![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137503.png) n_setup` 参数表示数组中的元素数量。

注意

正如你所看到的,需要一个 usb_xfer 结构来启动 USB 数据传输。

usbd_transfer_unsetup 函数销毁一个 usb_xfer 结构数组。![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/fbsd-dvc-dvr/img/httpatomoreillycomsourcenostarchimages1137507.png) n_setup` 参数表示数组中的元素数量。

usbd_transfer_start 函数接受一个 usb_xfer 结构,并开始一个 USB 传输(即,它以 USB_ST_SETUP 设置执行 callback)。

usbd_transfer_stop 函数停止与 xfer 参数相关的任何传输(即,它以 USB_ST_ERROR 设置执行 callback)。

usbd_transfer_drain 函数类似于 usbd_transfer_stop,但它等待 callback 完成后再返回。

USB 方法结构

一个 usb_fifo_methods 结构定义了 USB 驱动程序的入口点。你可以将 struct usb_fifo_methods 视为 struct cdevsw,但它是为 USB 驱动程序设计的。

struct usb_fifo_methods<dev/usb/usbdi.h> 头文件中定义如下:

struct usb_fifo_methods {
        /* Executed Unlocked */
        usb_fifo_open_t         *f_open;
        usb_fifo_close_t        *f_close;
        usb_fifo_ioctl_t        *f_ioctl;
        usb_fifo_ioctl_t        *f_ioctl_post;

        /* Executed With Mutex Locked */
        usb_fifo_cmd_t          *f_start_read;
        usb_fifo_cmd_t          *f_stop_read;
        usb_fifo_cmd_t          *f_start_write;
        usb_fifo_cmd_t          *f_stop_write;
        usb_fifo_filter_t       *f_filter_read;
        usb_fifo_filter_t       *f_filter_write;

        const char              *basename[4];
        const char              *postfix[4];
};

FreeBSD 内核提供了以下函数用于处理 usb_fifo_methods 结构:

#include <dev/usb/usb.h>
#include <dev/usb/usbdi.h>
#include <dev/usb/usbdi_util.h>

int
usb_fifo_attach(struct usb_device *udev, void *priv_sc,
    struct mtx *priv_mtx, struct usb_fifo_methods *pm,
  struct usb_fifo_sc *f_sc, uint16_t unit, uint16_t subunit,
    uint8_t iface_index, uid_t uid, gid_t gid, int mode);

void
usb_fifo_detach(struct usb_fifo_sc *f_sc);

usb_fifo_attach 函数在 /dev 下创建一个 USB 设备节点。如果成功,一个魔法饼干被保存在 f_sc 中。

usb_fifo_detach 函数接受由 usb_fifo_attach 创建的 饼干,并销毁其关联的 USB 设备节点。

将一切联系在一起

现在你已经熟悉了 usb_* 结构及其管理例程,让我们剖析一个实际的 USB 驱动程序。

示例 15-1 提供了对 ulpt(4) USB 打印机驱动程序的简洁、源代码级别的概述。

注意

为了提高可读性,本节中展示的一些变量和函数已被重命名并重构,以从 FreeBSD 源代码中的对应部分进行修改。

示例 15-1. ulpt.c

#include <sys/param.h>
  #include <sys/module.h>
  #include <sys/kernel.h>
  #include <sys/systm.h>

  #include <sys/conf.h>
  #include <sys/bus.h>
  #include <sys/lock.h>
  #include <sys/mutex.h>
  #include <sys/syslog.h>
  #include <sys/fcntl.h>

  #include <dev/usb/usb.h>
  #include <dev/usb/usbdi.h>
  #include <dev/usb/usbdi_util.h>

  #define ULPT_BUF_SIZE           (1 << 15)
  #define ULPT_IFQ_MAX_LEN        2

  #define UREQ_GET_PORT_STATUS    0x01
  #define UREQ_SOFT_RESET         0x02

  #define LPS_NERR                0x08
  #define LPS_SELECT              0x10
  #define LPS_NOPAPER             0x20
  #define LPS_INVERT              (LPS_NERR | LPS_SELECT)
  #define LPS_MASK                (LPS_NERR | LPS_SELECT | LPS_NOPAPER)

  enum {
          ULPT_BULK_DT_WR,
          ULPT_BULK_DT_RD,
          ULPT_INTR_DT_RD,
          ULPT_N_TRANSFER
  };

  struct ulpt_softc {
          device_t                sc_dev;
          struct usb_device      *sc_usb_device;
          struct mtx              sc_mutex;
          struct usb_callout      sc_watchdog;
          uint8_t                 sc_iface_num;
          struct usb_xfer        *sc_transfer[ULPT_N_TRANSFER];
          struct usb_fifo_sc      sc_fifo;
          struct usb_fifo_sc      sc_fifo_no_reset;
          int                     sc_fflags;
          struct usb_fifo        *sc_fifo_open[2];
          uint8_t                 sc_zero_length_packets;
          uint8_t                 sc_previous_status;
  };

  static device_probe_t           ulpt_probe;
  static device_attach_t          ulpt_attach;
  static device_detach_t          ulpt_detach;

  static usb_fifo_open_t          ulpt_open;
  static usb_fifo_open_t          unlpt_open;
  static usb_fifo_close_t         ulpt_close;
  static usb_fifo_ioctl_t         ulpt_ioctl;
  static usb_fifo_cmd_t           ulpt_start_read;
  static usb_fifo_cmd_t           ulpt_stop_read;
  static usb_fifo_cmd_t           ulpt_start_write;
  static usb_fifo_cmd_t           ulpt_stop_write;

  static void                     ulpt_reset(struct ulpt_softc *);
  static void                     ulpt_watchdog(void *);

  static usb_callback_t           ulpt_write_callback;
  static usb_callback_t           ulpt_read_callback;
  static usb_callback_t           ulpt_status_callback;

 static struct usb_fifo_methods ulpt_fifo_methods = {
          .f_open =               &ulpt_open,
          .f_close =              &ulpt_close,
          .f_ioctl =              &ulpt_ioctl,
          .f_start_read =         &ulpt_start_read,
          .f_stop_read =          &ulpt_stop_read,
          .f_start_write =        &ulpt_start_write,
          .f_stop_write =         &ulpt_stop_write,
          .basename[0] =        "ulpt"
  };

 static struct usb_fifo_methods unlpt_fifo_methods = {
          .f_open =               &unlpt_open,
          .f_close =              &ulpt_close,
          .f_ioctl =              &ulpt_ioctl,
          .f_start_read =         &ulpt_start_read,
          .f_stop_read =          &ulpt_stop_read,
          .f_start_write =        &ulpt_start_write,
          .f_stop_write =         &ulpt_stop_write,
          .basename[0] =        "unlpt"
  };

  static const struct usb_config ulpt_config[ULPT_N_TRANSFER] = {
        [ULPT_BULK_DT_WR] = {
                  .callback =     &ulpt_write_callback,
                  .bufsize =      ULPT_BUF_SIZE,
                  .flags =        {.pipe_bof = 1, .proxy_buffer = 1},
                  .type =         UE_BULK,
                  .endpoint =     UE_ADDR_ANY,
                  .direction =    UE_DIR_OUT
          },

        [ULPT_BULK_DT_RD] = {
                  .callback =     &ulpt_read_callback,
                  .bufsize =      ULPT_BUF_SIZE,
                  .flags =        {.short_xfer_ok = 1, .pipe_bof = 1,
                                      .proxy_buffer = 1},
                  .type =         UE_BULK,
                  .endpoint =     UE_ADDR_ANY,
                  .direction =    UE_DIR_IN
          },

        [ULPT_INTR_DT_RD] = {
                  .callback =     &ulpt_status_callback,
                  .bufsize =      sizeof(struct usb_device_request) + 1,
                  .timeout =      1000,           /* 1 second. */
                  .type =         UE_CONTROL,
                  .endpoint =     0x00,
                  .direction =    UE_DIR_ANY
          }
  };

  static int
  ulpt_open(struct usb_fifo *fifo, int fflags)
  {
  ...
  }

  static void
  ulpt_reset(struct ulpt_softc *sc)
  {
  ...
  }

  static int
  unlpt_open(struct usb_fifo *fifo, int fflags)
  {
  ...
  }

  static void
  ulpt_close(struct usb_fifo *fifo, int fflags)
  {
  ...
  }

  static int
  ulpt_ioctl(struct usb_fifo *fifo, u_long cmd, void *data, int fflags)
  {
  ...
  }

  static void
  ulpt_watchdog(void *arg)
  {
  ...
  }

  static void
  ulpt_start_read(struct usb_fifo *fifo)
  {
  ...
  }

  static void
  ulpt_stop_read(struct usb_fifo *fifo)
  {
  ...
  }

  static void
  ulpt_start_write(struct usb_fifo *fifo)
  {
  ...
  }

  static void
  ulpt_stop_write(struct usb_fifo *fifo)
  {
  ...
  }

  static void
  ulpt_write_callback(struct usb_xfer *transfer, usb_error_t error)
  {
  ...
  }

  static void
  ulpt_read_callback(struct usb_xfer *transfer, usb_error_t error)
  {
  ...
  }

  static void
  ulpt_status_callback(struct usb_xfer *transfer, usb_error_t error)
  {
  ...
  }

  static int
  ulpt_probe(device_t dev)
  {
  ...
  }

  static int
  ulpt_attach(device_t dev)
  {
  ...
  }

  static int
  ulpt_detach(device_t dev)
  {
  ...
  }

  static device_method_t ulpt_methods[] = {
          /* Device interface. */
          DEVMETHOD(device_probe,         ulpt_probe),
          DEVMETHOD(device_attach,        ulpt_attach),
          DEVMETHOD(device_detach,        ulpt_detach),
          { 0, 0 }
  };

  static driver_t ulpt_driver = {
          "ulpt",
          ulpt_methods,
          sizeof(struct ulpt_softc)
  };

  static devclass_t ulpt_devclass;

  DRIVER_MODULE(ulpt, uhub, ulpt_driver, ulpt_devclass, 0, 0);
  MODULE_DEPEND(ulpt, usb, 1, 1, 1);
  MODULE_DEPEND(ulpt, ucom, 1, 1, 1);

注意到示例 15-1 定义了三个usb_config结构。因此,ulpt(4)与三个端点通信:一个httpatomoreillycomsourcenostarchimages1137507.png批量 OUT,一个httpatomoreillycomsourcenostarchimages1137509.png批量 IN,以及httpatomoreillycomsourcenostarchimages1137511.png默认控制端点。

此外,注意示例 15-1 定义了两个httpatomoreillycomsourcenostarchimages1137499.pnghttpatomoreillycomsourcenostarchimages1137503.pngusb_fifo_methods结构。因此,ulpt(4)提供了两个设备节点:httpatomoreillycomsourcenostarchimages1137501.png``ulpt%dhttpatomoreillycomsourcenostarchimages1137505.png``unlpt%d(其中%d是单元号)。正如您将看到的,ulpt%d设备节点在打开时重置打印机,而unlpt%d则不会。

现在,让我们讨论示例 15-1 中找到的函数。

ulpt_probe 函数

ulpt_probe函数是ulpt(4)device_probe实现。以下是它的函数定义:

static int
ulpt_probe(device_t dev)
{
      struct usb_attach_arg *uaa = device_get_ivars(dev);

      if (uaa->usb_mode != USB_MODE_HOST)
                return (ENXIO);

      if ((uaa->info.bInterfaceClass == UICLASS_PRINTER) &&
            (uaa->info.bInterfaceSubClass == UISUBCLASS_PRINTER) &&
            ((uaa->info.bInterfaceProtocol == UIPROTO_PRINTER_UNI) ||
             (uaa->info.bInterfaceProtocol == UIPROTO_PRINTER_BI) ||
             (uaa->info.bInterfaceProtocol == UIPROTO_PRINTER_1284)))
                return (BUS_PROBE_SPECIFIC);

        return (ENXIO);
}

这个函数首先确保 USB 主机控制器处于主机模式,这是启动数据传输所需的。然后ulpt_probe确定dev是否是 USB 打印机。

顺便说一下,struct usb_attach_arg包含打印机的实例变量。

ulpt_attach 函数

ulpt_attach函数是ulpt(4)device_attach实现。以下是它的函数定义:

static int
ulpt_attach(device_t dev)
{
        struct usb_attach_arg *uaa = device_get_ivars(dev);
        struct ulpt_softc *sc = device_get_softc(dev);
        struct usb_interface_descriptor *idesc;
        struct usb_config_descriptor *cdesc;
        uint8_t alt_index, iface_index = uaa->info.bIfaceIndex;
        int error, unit = device_get_unit(dev);

        sc->sc_dev = dev;
        sc->sc_usb_device = uaa->device;
      device_set_usb_desc(dev);
        mtx_init(&sc->sc_mutex, "ulpt", NULL, MTX_DEF | MTX_RECURSE);
      usb_callout_init_mtx(&sc->sc_watchdog, &sc->sc_mutex, 0);

        idesc = usbd_get_interface_descriptor(uaa->iface);
        alt_index = −1;
        for (;;) {
                if (idesc == NULL)
                        break;

                if ((idesc->bDescriptorType == UDESC_INTERFACE) &&
                    (idesc->bLength >= sizeof(*idesc))) {
                        if (idesc->bInterfaceNumber != uaa->info.bIfaceNum)
                                break;
                        else {
                                alt_index++;
                                if ((idesc->bInterfaceClass ==
                                     UICLASS_PRINTER) &&
                                    (idesc->bInterfaceSubClass ==
                                     UISUBCLASS_PRINTER) &&
                                    (idesc->bInterfaceProtocol ==
                                   UIPROTO_PRINTER_BI))
                                        goto found;
                        }
                }

                cdesc = usbd_get_config_descriptor(uaa->device);
                idesc = (void *)usb_desc_foreach(cdesc, (void *)idesc);
        }
        goto detach;

found:
        if (alt_index) {
                error = usbd_set_alt_interface_index(uaa->device,
                    iface_index, alt_index);
                if (error)
                        goto detach;
        }

        sc->sc_iface_num = idesc->bInterfaceNumber;

        error = usbd_transfer_setup(uaa->device, &iface_index,
            sc->sc_transfer, ulpt_config, ULPT_N_TRANSFER, sc,
            &sc->sc_mutex);
        if (error)
                goto detach;

        device_printf(dev, "using bi-directional mode\n");

        error = usb_fifo_attach(uaa->device, sc, &sc->sc_mutex,
            &ulpt_fifo_methods, &sc->sc_fifo, unit, −1,
            iface_index, UID_ROOT, GID_OPERATOR, 0644);
        if (error)
                goto detach;

        error = usb_fifo_attach(uaa->device, sc, &sc->sc_mutex,
            &unlpt_fifo_methods, &sc->sc_fifo_no_reset, unit, −1,
            iface_index, UID_ROOT, GID_OPERATOR, 0644);
        if (error)
                goto detach;

        mtx_lock(&sc->sc_mutex);
      ulpt_watchdog(sc);
        mtx_unlock(&sc->sc_mutex);
        return (0);

detach:
        ulpt_detach(dev);
        return (ENOMEM);
}

这个函数可以分为三个部分。第一部分通过调用device_set_usb_desc(dev)来设置dev的详细描述。然后它初始化ulpt(4)callout结构。

注意

所有 USB 设备都包含对自己文本描述,这就是为什么device_set_usb_desc只接受一个device_t参数。

第二部分基本上通过httpatomoreillycomsourcenostarchimages1137507.png迭代接口号uaa->info.bIfaceNum的备用设置,直到找到支持httpatomoreillycomsourcenostarchimages1137505.png双向模式的备用设置。如果支持双向模式的备用设置不是备用设置 0,则调用usbd_set_alt_interface_index来设置这个备用设置。备用设置 0 不需要设置,因为它默认使用。

最后,第三部分 USB 转移初始化 初始化 USB 转移, 创建设备节点 ulp(4) 的设备节点 被创建,并调用 ulp_watchdog ulp_watchdog(我们将在 ulp_watchdog 函数 中进行讲解)。

ulpt_detach 函数

ulpt_detach 函数是 device_detachulpt(4) 的实现。以下是它的函数定义:

static int
ulpt_detach(device_t dev)
{
        struct ulpt_softc *sc = device_get_softc(dev);

      usb_fifo_detach(&sc->sc_fifo);
      usb_fifo_detach(&sc->sc_fifo_no_reset);

        mtx_lock(&sc->sc_mutex);
      usb_callout_stop(&sc->sc_watchdog);
        mtx_unlock(&sc->sc_mutex);

      usbd_transfer_unsetup(sc->sc_transfer, ULPT_N_TRANSFER);
      usb_callout_drain(&sc->sc_watchdog);
      mtx_destroy(&sc->sc_mutex);

        return (0);
}

此函数首先 销毁其设备节点 和 停止调用函数拆除 USB 转移清空调用函数销毁其互斥锁

ulpt_open 函数

ulp_open 函数是 ulp%d 设备节点的打开例程。以下是它的函数定义:

static int
ulpt_open(struct usb_fifo *fifo, int fflags)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);

        if (sc->sc_fflags == 0)
                ulpt_reset(sc);

        return (unlpt_open(fifo, fflags));
}

此函数首先调用 ulp_reset ulp_reset 来重置打印机。然后 unlpt_open unlpt_open 被调用以(实际上)打开打印机。

ulpt_reset 函数

如前节所述,ulp_reset 函数用于重置打印机。以下是它的函数定义:

static void
ulpt_reset(struct ulpt_softc *sc)
{
      struct usb_device_request req;
        int error;

        req.bRequest = UREQ_SOFT_RESET;
        USETW(req.wValue, 0);
        USETW(req.wIndex, sc->sc_iface_num);
        USETW(req.wLength, 0);

        mtx_lock(&sc->sc_mutex);

        req.bmRequestType = UT_WRITE_CLASS_OTHER;
        error = usbd_do_request_flags(sc->sc_usb_device, &sc->sc_mutex,
            &req, NULL, 0, NULL, 2 * USB_MS_HZ);
      if (error) {
                req.bmRequestType = UT_WRITE_CLASS_INTERFACE;
              usbd_do_request_flags(sc->sc_usb_device, &sc->sc_mutex,
                    &req, NULL, 0, NULL, 2 * USB_MS_HZ);
        }

        mtx_unlock(&sc->sc_mutex);
}

此函数首先定义一个 usb_device_request 结构重置打印机。然后 发送重置请求到打印机

注意,一些打印机将重置请求典型化为 UT_WRITE_CLASS_OTHER 和一些典型化为 UT_WRITE_CLASS_INTERFACE。因此,如果第一次请求 失败ulpt_reset 将第二次发送重置请求。

unlpt_open 函数

unlpt_open 函数是 unlpt%d 设备节点的打开例程。以下是它的函数定义:

注意

你会记得,此函数也在 ulp_open 的末尾被调用。

static int
unlpt_open(struct usb_fifo *fifo, int fflags)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);
        int error;

      if (sc->sc_fflags & fflags)
                return (EBUSY);

      if (fflags & FREAD) {
                mtx_lock(&sc->sc_mutex);
              usbd_xfer_set_stall(sc->sc_transfer[ULPT_BULK_DT_RD]);
                mtx_unlock(&sc->sc_mutex);

                error = usb_fifo_alloc_buffer(fifo,
                    usbd_xfer_max_len(sc->sc_transfer[ULPT_BULK_DT_RD]),
                    ULPT_IFQ_MAX_LEN);
                if (error)
                        return (ENOMEM);

              sc->sc_fifo_open[USB_FIFO_RX] = fifo;
        }

      if (fflags & FWRITE) {
                mtx_lock(&sc->sc_mutex);
              usbd_xfer_set_stall(sc->sc_transfer[ULPT_BULK_DT_WR]);
                mtx_unlock(&sc->sc_mutex);

                error = usb_fifo_alloc_buffer(fifo,
                    usbd_xfer_max_len(sc->sc_transfer[ULPT_BULK_DT_WR]),
                    ULPT_IFQ_MAX_LEN);
                if (error)
                        return (ENOMEM);

              sc->sc_fifo_open[USB_FIFO_TX] = fifo;
        }

      sc->sc_fflags |= fflags & (FREAD | FWRITE);
        return (0);
}

此函数首先测试 sc->sc_fflags 的值。如果不等于 0,这意味着另一个进程已经打开了打印机,将返回错误代码 EBUSY。接下来,unlpt_open 确定我们是打开打印机来读取还是写入——答案存储在 sc->sc_fflags 中。然后,向适当的端点发出清除阻塞请求。

注意

USB 设备在其自身功能中检测到的任何错误(不包括传输错误),都会导致设备“阻塞”其当前事务的端点(Oney,2003)。控制端点会自动清除其阻塞,但其他端点类型需要清除阻塞请求。自然,阻塞的端点无法执行任何事务。

接下来,为读取或写入分配内存。之后,将 fifo 参数存储在 sc->sc_fifo_open 中。

ulpt_close 函数

ulpt_close 函数是 ulpt%dunlpt%d 的关闭例程。以下是它的函数定义:

static void
ulpt_close(struct usb_fifo *fifo, int fflags)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);

      sc->sc_fflags &= ˜(fflags & (FREAD | FWRITE));

        if (fflags & (FREAD | FWRITE))
               usb_fifo_free_buffer(fifo);
}

此函数首先清除 sc->sc_fflags。然后释放 unlpt_open 中分配的内存。

ulpt_ioctl 函数

ulpt_ioctl 函数是 ulpt%dunlpt%d 的 ioctl 例程。以下是它的函数定义:

static int
ulpt_ioctl(struct usb_fifo *fifo, u_long cmd, void *data, int fflags)
{
        return (ENODEV);
}

如您所见,ulpt(4) 不支持 ioctl。

ulpt_watchdog 函数

ulpt_watchdog 函数定期检查打印机的状态。以下是它的函数定义:

注意

您会记得这个函数是在 ulpt_attach 的末尾被调用的。

static void
ulpt_watchdog(void *arg)
{
        struct ulpt_softc *sc = arg;

        mtx_assert(&sc->sc_mutex, MA_OWNED);

      if (sc->sc_fflags == 0)
               usbd_transfer_start(sc->sc_transfer[ULPT_INTR_DT_RD]);

      usb_callout_reset(&sc->sc_watchdog, hz,
 &ulpt_watchdog, sc);
}

此函数首先 图片 确保打印机未打开。然后它 图片 与默认控制端点 图片 开始一个事务(以检索打印机的状态)。回想一下 图片 usbd_transfer_start 只执行一个回调。在这种情况下,该回调是 ulpt_status_callback(为了确认,请参阅 示例 15-1 中的第三个 usb_config 结构)。最后,图片 ulpt_watchdog 被重新安排在 1 秒后执行。

ulpt_start_read 函数

当进程从 ulpt%dunlpt%d 读取时,会执行 ulpt_start_read 函数(为了验证,请参阅它们的 usb_fifo_methods 结构)。以下是它的函数定义:

static void
ulpt_start_read(struct usb_fifo *fifo)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);

       usbd_transfer_start(sc->sc_transfer[ULPT_BULK_DT_RD]);
}

此函数 图片 简单地与打印机的 图片 批量 IN 端点开始一个事务。请注意,批量 IN 端点的回调是 ulpt_read_callback(为了确认,请参阅 示例 15-1 中的第二个 usb_config 结构)。

ulpt_stop_read 函数

当进程停止从 ulpt%dunlpt%d 读取时,会调用 ulpt_stop_read 函数。以下是它的函数定义:

static void
ulpt_stop_read(struct usb_fifo *fifo)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);

      usbd_transfer_stop(sc->sc_transfer[ULPT_BULK_DT_RD]);
}

此函数 图片 停止与打印机 图片 的批量 IN 端点相关的任何事务。

ulpt_start_write 函数

当进程向 ulpt%dunlpt%d 写入时,会执行 ulpt_start_write 函数。以下是它的函数定义:

static void
ulpt_start_write(struct usb_fifo *fifo)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);

      usbd_transfer_start(sc->sc_transfer[ULPT_BULK_DT_WR]);
}

此函数 图片 简单地与打印机的 图片 批量 OUT 端点开始一个事务。请注意,批量 OUT 端点的回调是 ulpt_write_callback(为了确认,请参阅 示例 15-1 中的第一个 usb_config 结构)。

ulpt_stop_write 函数

当进程停止向 ulpt%dunlpt%d 写入时,会执行 ulpt_stop_write 函数。以下是它的函数定义:

static void
ulpt_stop_write(struct usb_fifo *fifo)
{
        struct ulpt_softc *sc = usb_fifo_softc(fifo);

      usbd_transfer_stop(sc->sc_transfer[ULPT_BULK_DT_WR]);
}

此函数 图片 停止与打印机 图片 的批量 OUT 端点相关的任何事务。

ulpt_write_callback 函数

ulpt_write_callback 函数将数据从用户空间传输到打印机(以供打印)。回想一下,此函数是批量 OUT 端点的回调,因此它在批量 OUT 传输数据之前和之后执行。

以下是对 ulpt_write_callback 的函数定义:

static void
ulpt_write_callback(struct usb_xfer *transfer, usb_error_t error)
{
        struct ulpt_softc *sc = usbd_xfer_softc(transfer);
        struct usb_fifo *fifo = sc->sc_fifo_open[USB_FIFO_TX];
        struct usb_page_cache *pc;
        int actual, max;

        usbd_xfer_status(transfer, &actual, NULL, NULL, NULL);

        if (fifo == NULL)
                return;

        switch (USB_GET_STATE(transfer)) {
      case USB_ST_SETUP:
      case USB_ST_TRANSFERRED:
setup:
                pc = usbd_xfer_get_frame(transfer, 0);
                max = usbd_xfer_max_len(transfer);
                if (usb_fifo_get_data(fifo, pc, 0,
 max,
                    &actual, 0)) {
                      usbd_xfer_set_frame_len(transfer, 0, actual);
                      usbd_transfer_submit(transfer);
                }
                break;
        default:
                if (error != USB_ERR_CANCELLED) {
                        /* Issue a clear-stall request. */
                        usbd_xfer_set_stall(transfer);
                        goto setup;
                }
                break;
        }
}

此函数首先 ![httpatomoreillycomsourcenostarchimages1137503.png] 将 foo 字节从 ![httpatomoreillycomsourcenostarchimages1137505.png] 用户空间复制到 ![httpatomoreillycomsourcenostarchimages1137507.png] 内核空间。最多复制 ![httpatomoreillycomsourcenostarchimages1137509.png] max 字节的数据。实际复制的字节数返回在 ![httpatomoreillycomsourcenostarchimages1137511.png] actual 中。接下来,设置 ![httpatomoreillycomsourcenostarchimages1137515.png] 传输长度 ![httpatomoreillycomsourcenostarchimages1137513.png]。然后,将用户空间复制的数据 ![httpatomoreillycomsourcenostarchimages1137517.png] 发送到打印机。

注意

在前面的段落中,foo 是占位符,因为我不知道 usb_fifo_get_data 返回之前复制了多少字节。

注意,![httpatomoreillycomsourcenostarchimages1137499.png] USB_ST_SETUP 情况和 ![httpatomoreillycomsourcenostarchimages1137501.png] USB_ST_TRANSFERRED 情况是相同的。这是因为你可以打印比最大传输长度更多的数据。因此,这个函数“循环”直到所有数据都发送完毕。

ulpt_read_callback 函数

ulpt_read_callback 函数从打印机获取数据。回想一下,这个函数是批量 IN 端点的回调函数,因此它在批量 IN 传输数据之前和之后执行。

以下是对 ulpt_read_callback 函数的定义:

static void
ulpt_read_callback(struct usb_xfer *transfer, usb_error_t error)
{
        struct ulpt_softc *sc = usbd_xfer_softc(transfer);
        struct usb_fifo *fifo = sc->sc_fifo_open[USB_FIFO_RX];
        struct usb_page_cache *pc;
        int actual, max;

        usbd_xfer_status(transfer, &actual, NULL, NULL, NULL);

        if (fifo == NULL)
                return;

        switch (USB_GET_STATE(transfer)) {
      case USB_ST_TRANSFERRED:
             if (actual == 0) {
                      if (sc->sc_zero_length_packets == 4)
                                /* Throttle transfers. */
                              usbd_xfer_set_interval(transfer, 500);
                        else
                                sc->sc_zero_length_packets++;
                } else {
                        /* Disable throttling. */
                        usbd_xfer_set_interval(transfer, 0);
                        sc->sc_zero_length_packets = 0;
                }

                pc = usbd_xfer_get_frame(transfer, 0);
              usb_fifo_put_data(fifo, pc, 0, actual, 1);
                /* FALLTHROUGH */
        case USB_ST_SETUP:
setup:
                if (usb_fifo_put_bytes_max(fifo) != 0) {
                        max = usbd_xfer_max_len(transfer);
                      usbd_xfer_set_frame_len(transfer, 0, max);
                      usbd_transfer_submit(transfer);
                }
                break;
        default:
                /* Disable throttling. */
                usbd_xfer_set_interval(transfer, 0);
                sc->sc_zero_length_packets = 0;

                if (error != USB_ERR_CANCELLED) {
                        /* Issue a clear-stall request. */
                        usbd_xfer_set_stall(transfer);
                        goto setup;
                }
                break;
        }
}

此函数首先 ![httpatomoreillycomsourcenostarchimages1137513.png] 确保用户空间有足够的空间来存储打印机的数据。接下来,指定最大传输长度 ![httpatomoreillycomsourcenostarchimages1137515.png]。然后从打印机检索数据。

在传输完成后 ![httpatomoreillycomsourcenostarchimages1137499.png],打印机的数据 ![httpatomoreillycomsourcenostarchimages1137507.png] 从 ![httpatomoreillycomsourcenostarchimages1137511.png] 内核空间复制到 ![httpatomoreillycomsourcenostarchimages1137509.png] 用户空间。请注意,如果 ![httpatomoreillycomsourcenostarchimages1137501.png] 连续四次没有任何返回 ![httpatomoreillycomsourcenostarchimages1137503.png],则启用传输节流 ![httpatomoreillycomsourcenostarchimages1137505.png]。

注意

一些 USB 设备无法处理多个快速传输请求,因此需要交错或节流传输。

ulpt_status_callback 函数

ulpt_status_callback 函数返回打印机的当前状态。回想一下,这个函数是默认控制端点的回调函数,因此它在与端点 0 的任何事务之前和之后执行。

以下是对 ulpt_status_callback 函数的定义:

static void
ulpt_status_callback(struct usb_xfer *transfer, usb_error_t error)
{
        struct ulpt_softc *sc = usbd_xfer_softc(transfer);
        struct usb_device_request req;
        struct usb_page_cache *pc;
        uint8_t current_status, new_status;

        switch (USB_GET_STATE(transfer)) {
        case USB_ST_SETUP:
                req.bmRequestType = UT_READ_CLASS_INTERFACE;
                req.bRequest = UREQ_GET_PORT_STATUS;
                USETW(req.wValue, 0);
                req.wIndex[0] = sc->sc_iface_num;
                req.wIndex[1] = 0;
                USETW(req.wLength, 1);

                pc = usbd_xfer_get_frame(transfer, 0);
              usbd_copy_in(pc, 0, &req, sizeof(req));
              usbd_xfer_set_frame_len(transfer, 0, sizeof(req));
              usbd_xfer_set_frame_len(transfer, 1, 1);
                usbd_xfer_set_frames(transfer, 2);
              usbd_transfer_submit(transfer);

                break;
      case USB_ST_TRANSFERRED:
                pc = usbd_xfer_get_frame(transfer, 1);
              usbd_copy_out(pc, 0, &current_status, 1);

                current_status = (current_status ^ LPS_INVERT) & LPS_MASK;
                new_status = current_status & ˜sc->sc_previous_status;
                sc->sc_previous_status = current_status;

                if (new_status & LPS_NERR)
                        log(LOG_NOTICE, "%s: output error\n",
                            device_get_nameunit(sc->sc_dev));
                else if (new_status & LPS_SELECT)
                        log(LOG_NOTICE, "%s: offline\n",
                            device_get_nameunit(sc->sc_dev));
                else if (new_status & LPS_NOPAPER)
                        log(LOG_NOTICE, "%s: out of paper\n",
                            device_get_nameunit(sc->sc_dev));

                break;
        default:
                break;
        }
}

此功能首先构建一个![httpatomoreillycomsourcenostarchimages1137499.png]获取状态请求。然后![httpatomoreillycomsourcenostarchimages1137501.png]将![httpatomoreillycomsourcenostarchimages1137505.png]请求放入![httpatomoreillycomsourcenostarchimages1137503.png]DMA 缓冲区。不久之后,请求![httpatomoreillycomsourcenostarchimages1137513.png]被发送到打印机。有趣的是,这个交易涉及![httpatomoreillycomsourcenostarchimages1137511.png]两个 USB 帧。![httpatomoreillycomsourcenostarchimages1137507.png]第一个包含获取状态请求。![httpatomoreillycomsourcenostarchimages1137509.png]第二个将保存打印机的状态。

交易完成后![httpatomoreillycomsourcenostarchimages1137515.png],打印机的状态![httpatomoreillycomsourcenostarchimages1137517.png]从 DMA 缓冲区中提取。

此函数剩余部分应不言自明。

结论

本章基本上是关于 USB 设备和驱动程序的基础教程。更多信息,请参阅官方文档,可在www.usb.org/developers/找到。

第十六章。网络驱动程序,第一部分:数据结构

image with no caption

网络设备接口 通过网络子系统(Corbet 等人,2005)发送和接收由网络子系统驱动的数据包。在本章中,我们将检查用于管理这些设备的数据结构:ifnetifmediambuf。然后,您将了解消息信号中断,它们是传统中断的替代品,通常由网络设备使用。

注意

为了保持简单,我们只检查以太网驱动程序。此外,我不会提供关于通用网络概念的讨论。

网络接口结构

ifnet 结构是内核对单个网络接口的表示。它在 <net/if_var.h> 头文件中定义如下:

struct ifnet {
        void    *if_softc;              /* Driver private data.         */
        void    *if_l2com;              /* Protocol bits.               */
        struct  vnet *if_vnet;          /* Network stack instance.      */
        TAILQ_ENTRY(ifnet) if_link;     /* ifnet linkage.               */
        char    if_xname[IFNAMSIZ];     /* External name.               */
        const char *if_dname;           /* Driver name.                 */
        int     if_dunit;       /* Unit number or IF_DUNIT_NONE.        */
        u_int   if_refcount;            /* Reference count.             */

        /*
         * Linked list containing every address associated with
         * this interface.
         */
        struct  ifaddrhead if_addrhead;

        int     if_pcount;      /* Number of promiscuous listeners.     */
        struct  carp_if *if_carp;       /* CARP interface.              */
        struct  bpf_if *if_bpf;         /* Packet filter.               */
        u_short if_index;       /* Numeric abbreviation for interface.  */
        short   if_timer;       /* Time until if_watchdog is called.    */
        struct  ifvlantrunk *if_vlantrunk; /* 802.1Q data.              */
        int     if_flags;       /* Flags (e.g., up, down, broadcast).   */
        int     if_capabilities;/* Interface features and capabilities. */
        int     if_capenable;   /* Enabled features and capabilities.   */
        void    *if_linkmib;            /* Link specific MIB data.      */
        size_t  if_linkmiblen;          /* Length of above.             */
        struct  if_data if_data;        /* Interface information.       */
        struct  ifmultihead if_multiaddrs; /* Multicast addresses.      */
        int     if_amcount;     /* Number of multicast requests.        */

        /* Interface methods.                                           */
        int     (*if_output)
                (struct ifnet *, struct mbuf *, struct sockaddr *,
                    struct route *);
        void    (*if_input)
                (struct ifnet *, struct mbuf *);
        void    (*if_start)
                (struct ifnet *);
        int     (*if_ioctl)
                (struct ifnet *, u_long, caddr_t);
        void    (*if_watchdog)
                (struct ifnet *);
        void    (*if_init)
                (void *);
        int     (*if_resolvemulti)
                (struct ifnet *, struct sockaddr **, struct sockaddr *);
        void    (*if_qflush)
                (struct ifnet *);
        int     (*if_transmit)
                (struct ifnet *, struct mbuf *);
        void    (*if_reassign)
                (struct ifnet *, struct vnet *, char *);

        struct  vnet *if_home_vnet;     /* Where we originate from.     */
        struct  ifaddr *if_addr;        /* Link level address.          */
        void    *if_llsoftc;            /* Link level softc.            */
        int     if_drv_flags;           /* Driver managed status flags. */
        struct  ifaltq if_snd;        /* Output queue, includes altq. */
        const u_int8_t *if_broadcastaddr; /* Link level broadcast addr. */
        void    *if_bridge;             /* Bridge glue.                 */
        struct  label *if_label;        /* Interface MAC label.         */

        /* Only used by IPv6\.                                           */
        struct  ifprefixhead if_prefixhead;
        void    *if_afdata[AF_MAX];
        int     if_afdata_initialized;
        struct  rwlock if_afdata_lock;
        struct  task if_linktask;
        struct  mtx if_addr_mtx;

        LIST_ENTRY(ifnet) if_clones;    /* Clone interfaces.            */
        TAILQ_HEAD(, ifg_list) if_groups; /* Linked list of groups.     */
        void    *if_pf_kif;             /* pf(4) glue.                  */
        void    *if_lagg;               /* lagg(4) glue.                */
        u_char  if_alloctype;           /* Type (e.g., Ethernet).       */

        /* Spare fields.                                                */
        char    if_cspare[3];           /* Spare characters.            */
        char    *if_description;        /* Interface description.       */
        void    *if_pspare[7];          /* Spare pointers.              */
        int     if_ispare[4];           /* Spare integers.              */
};

我将在 Hello, world! 中演示如何使用 struct ifnet,在 Hello, world! 中。现在,让我们看看它的方法字段。

if_init 字段标识了接口的初始化例程。初始化例程 被调用以初始化其接口。

if_ioctl 字段标识了接口的 ioctl 例程。典型地,ioctl 例程用于配置其接口(例如,设置最大传输单元)。

if_input 字段标识了接口的输入例程。每当接口接收到数据包时,它都会发送一个中断。其驱动程序定义的中断处理程序随后调用其 输入例程 来处理数据包。请注意,这与常规做法不同。输入例程是由驱动程序调用的,而其他例程是由网络栈调用的。if_input 字段通常指向链路层例程(例如,ether_input),而不是由驱动程序定义的例程。

注意

显然,链路层例程是由内核定义的。期望链路层例程的方法字段应由 *ifattach 函数(例如 ether_ifattach)定义,而不是直接由驱动程序定义。*ifattach 函数在 网络接口结构管理例程 中描述。

if_output 字段标识了接口的输出例程。输出例程由网络栈调用,以准备上层数据包进行传输。每个输出例程都以调用其接口的 传输例程结束。如果一个接口缺少传输例程,则调用其 启动例程。通常,当网络驱动程序定义传输例程时,其启动例程是未定义的,反之亦然。if_output 字段通常指向链路层例程(例如,ether_output),而不是由驱动程序定义的例程。

if_start 字段标识了接口的启动例程。在描述启动例程之前,讨论 发送队列是很重要的。发送队列由输出例程填充。启动例程从它们的发送队列中移除一个数据包并将其存入接口的传输环。它们重复此过程,直到发送队列为空或传输环已满。传输环是用于传输的简单环形缓冲区。网络接口使用环形缓冲区进行传输和接收。

if_transmit 字段标识了接口的传输例程。传输 例程 是启动例程的替代方案。传输例程维护自己的发送队列。也就是说,它们放弃了 预定义的发送队列,并且输出例程直接将数据包推送到它们。传输例程可以维护多个发送队列,这使得它们非常适合具有多个传输环的接口。

if_qflush 字段标识了接口的 qflush 例程。Qflush 例程被调用以清除传输例程的发送队列。每个传输例程都必须有一个相应的 qflush 例程。

if_resolvemulti 字段标识了接口的 resolvemulti 例程。Resolvemulti 例程被调用以在将多播地址注册到其接口时将网络层地址解析为链路层地址。if_resolvemulti 字段通常指向链路层例程(例如,ether_resolvemulti),而不是由驱动程序定义的例程。

if_reassign 字段标识了接口的重新分配例程。在接口移动到另一个虚拟网络栈(vnet)之前,会调用重新分配 例程。它们执行移动之前所需的任何任务。if_reassign 字段通常指向链路层例程(例如,ether_reassign),而不是由驱动程序定义的例程。

if_watchdog 字段已被弃用,并且必须定义。在 FreeBSD 版本 9 中,if_watchdog 将被移除。

网络接口结构管理例程

FreeBSD 内核提供了以下函数来处理 ifnet 结构:

#include <net/if.h>
#include <net/if_types.h>
#include <net/if_var.h>

struct ifnet *
if_alloc(u_char type);

void
if_initname(struct ifnet *ifp, const char *name, int unit);

void
if_attach(struct ifnet *ifp);

void
if_detach(struct ifnet *ifp);

void
if_free(struct ifnet *ifp);

ifnet 结构是一个由内核拥有的动态分配的结构。也就是说,你不能自己分配一个 struct ifnet。相反,你必须调用 if_alloc。类型参数是接口类型(例如,以太网设备是 IFT_ETHER)。每个接口类型的符号常量可以在 <net/if_types.h> 头文件中找到。

分配一个 ifnet 结构并不会使接口对系统可用。为了做到这一点,你必须初始化该结构(通过定义必要的字段),然后调用 if_attach

if_initname 函数是一个方便的函数,用于设置接口的名称和单元号。 (不用说,这个函数是在 if_attach 之前使用的。)

ifnet 结构不再需要时,应该使用 if_detach 来使其失效,之后可以使用 if_free 来释放它。

ether_ifattach 函数

ether_ifattach 函数是 if_attach 的一个变体,用于以太网设备。

#include <net/if.h>
#include <net/if_types.h>
#include <net/if_var.h>
#include <net/ethernet.h>

void
ether_ifattach(struct ifnet *ifp, const u_int8_t *lla);

此函数在 /sys/net/if_ethersubr.c 源文件中定义如下:

void
ether_ifattach(struct ifnet *ifp, const u_int8_t *lla)
{
        struct ifaddr *ifa;
        struct sockaddr_dl *sdl;
        int i;

        ifp->if_addrlen = ETHER_ADDR_LEN;
        ifp->if_hdrlen = ETHER_HDR_LEN;
        if_attach(ifp);
        ifp->if_mtu = ETHERMTU;
      ifp->if_output = ether_output;
      ifp->if_input = ether_input;
      ifp->if_resolvemulti = ether_resolvemulti;
#ifdef VIMAGE
      ifp->if_reassign = ether_reassign;
#endif
        if (ifp->if_baudrate == 0)
                ifp->if_baudrate = IF_Mbps(10);
        ifp->if_broadcastaddr = etherbroadcastaddr;

        ifa = ifp->if_addr;
        KASSERT(ifa != NULL, ("%s: no lladdr!\n", __func__));
        sdl = (struct sockaddr_dl *)ifa->ifa_addr;
        sdl->sdl_type = IFT_ETHER;
        sdl->sdl_alen = ifp->if_addrlen;
        bcopy(lla, LLADDR(sdl), ifp->if_addrlen);

        bpfattach(ifp, DLT_EN10MB, ETHER_HDR_LEN);
        if (ng_ether_attach_p != NULL)
                (*ng_ether_attach_p)(ifp);

        /* Print Ethernet MAC address (if lla is nonzero). */
        for (i = 0; i < ifp->if_addrlen; i++)
                if (lla[i] != 0)
                        break;
        if (i != ifp->if_addrlen)
                if_printf(ifp, "Ethernet address: %6D\n", lla, ":");
}

此函数接受一个 ifnet 结构,ifp,和一个链路层地址,lla,并为以太网设备设置 ifp

如您所见,它为 ifp 分配了某些值,包括将适当的链路层例程分配给 if_outputif_inputif_resolvemultiif_reassign

ether_ifdetach 函数

ether_ifdetach 函数是 if_detach 的一个变体,用于以太网设备。

#include <net/if.h>
#include <net/if_types.h>
#include <net/if_var.h>
#include <net/ethernet.h>

void
ether_ifdetach(struct ifnet *ifp);

此函数用于使由 ether_ifattach 设置的 ifnet 结构失效。

网络接口媒体结构

ifmedia 结构列出了网络接口支持的每种媒体类型(例如,100BASE-TX、1000BASE-SX 等)。它在 <net/if_media.h> 头文件中定义如下:

struct ifmedia {
        int     ifm_mask;               /* Mask of bits to ignore.      */
        int     ifm_media;              /* User-set media word.         */
        struct ifmedia_entry *ifm_cur;  /* Currently selected media.    */

        /*
         * Linked list containing every media type supported by
         * an interface.
         */
        LIST_HEAD(, ifmedia_entry) ifm_list;

        ifm_change_cb_t ifm_change;     /* Media change callback.       */
        ifm_stat_cb_t   ifm_status;     /* Media status callback.       */
};

网络接口媒体结构管理例程

FreeBSD 内核提供了以下函数来处理 ifmedia 结构:

#include <net/if.h>
#include <net/if_media.h>

void
ifmedia_init(struct ifmedia *ifm, int dontcare_mask,
    ifm_change_cb_t change_callback, ifm_stat_cb_t status_callback);

void
ifmedia_add(struct ifmedia *ifm, int mword, int data, void
 *aux);

void
ifmedia_set(struct ifmedia *ifm, int mword);

void
ifmedia_removeall(struct ifmedia *ifm);

ifmedia 结构是一个由网络驱动程序拥有的静态分配的结构。要初始化一个 ifmedia 结构,你必须调用 ifmedia_init

dontcare_mask 参数标记 mword 中的位,这些位可以忽略。通常,dontcare_mask 设置为 0

change_callback 参数表示一个回调函数。此函数执行以更改媒体类型或媒体选项。以下是其函数原型:

typedef int (*ifm_change_cb_t)(struct ifnet *ifp);

注意

用户可以使用 ifconfig(8) 命令更改接口的媒体类型或媒体选项。

status_callback 参数表示一个回调函数。此函数执行以返回媒体状态。以下是其函数原型:

typedef void (*ifm_stat_cb_t)(struct ifnet *ifp, struct ifmediareq *req);

注意

用户可以使用 ifconfig(8) 命令查询接口的媒体状态。

ifmedia_add 函数向 ifm 添加媒体类型。mword 参数是一个 32 位“字”,用于标识媒体类型。mword 的有效值在 <net/if_media.h> 中定义。

这里是以太网设备的 mword 值:

#define IFM_ETHER       0x00000020
#define IFM_10_T        3               /* 10BASE-T, RJ45\.              */
#define IFM_10_2        4               /* 10BASE2, thin Ethernet.      */
#define IFM_10_5        5               /* 10BASE5, thick Ethernet.     */
#define IFM_100_TX      6               /* 100BASE-TX, RJ45\.            */
#define IFM_100_FX      7               /* 100BASE-FX, fiber.           */
#define IFM_100_T4      8               /* 100BASE-T4\.                  */
#define IFM_100_VG      9               /* 100VG-AnyLAN.                */
#define IFM_100_T2      10              /* 100BASE-T2\.                  */
#define IFM_1000_SX     11      /* 1000BASE-SX, multimode fiber.        */
#define IFM_10_STP      12      /* 10BASE-T, shielded twisted-pair.     */
#define IFM_10_FL       13              /* 10BASE-FL, fiber.            */
#define IFM_1000_LX     14      /* 1000BASE-LX, single-mode fiber.      */
#define IFM_1000_CX     15      /* 1000BASE-CX, shielded twisted-pair.  */
#define IFM_1000_T      16              /* 1000BASE-T.                  */
#define IFM_HPNA_1      17              /* HomePNA 1.0 (1Mb/s).         */
#define IFM_10G_LR      18      /* 10GBASE-LR, single-mode fiber.       */
#define IFM_10G_SR      19      /* 10GBASE-SR, multimode fiber.         */
#define IFM_10G_CX4     20              /* 10GBASE-CX4\.                 */
#define IFM_2500_SX     21      /* 2500BASE-SX, multimode fiber.        */
#define IFM_10G_TWINAX  22              /* 10GBASE, Twinax.             */
#define IFM_10G_TWINAX_LONG     23      /* 10GBASE, Twinax long.        */
#define IFM_10G_LRM     24      /* 10GBASE-LRM, multimode fiber.        */
#define IFM_UNKNOWN     25              /* Undefined.                   */
#define IFM_10G_T       26              /* 10GBASE-T, RJ45\.             */

#define IFM_AUTO        0               /* Automatically select media.  */
#define IFM_MANUAL      1               /* Manually select media.       */
#define IFM_NONE        2               /* Unselect all media.          */

/* Shared options.                                                      */
#define IFM_FDX         0x00100000      /* Force full-duplex.           */
#define IFM_HDX         0x00200000      /* Force half-duplex.           */
#define IFM_FLOW        0x00400000      /* Enable hardware flow control.*/
#define IFM_FLAG0       0x01000000      /* Driver-defined flag.         */
#define IFM_FLAG1       0x02000000      /* Driver-defined flag.         */
#define IFM_FLAG2       0x04000000      /* Driver-defined flag.         */
#define IFM_LOOP        0x08000000      /* Put hardware in loopback.    */

作为一个例子,100BASE-TX 的 mword 值如下:

IFM_ETHER | IFM_100_TX

表 16-1 描述了 mword 中每个位的用途。它还显示了可以传递给 dontcare_mask 以忽略这些位的位掩码。

表 16-1. mword 的位分解

比特 比特用途 忽略比特的掩码
00–04 表示媒体类型变体(例如,100BASE-TX) IFM_TMASK
05–07 表示媒体类型(例如,以太网) IFM_NMASK
08–15 表示媒体类型特定选项 IFM_OMASK
16–18 表示媒体类型模式(仅适用于多模态媒体) IFM_MMASK
19 保留供将来使用 n/a
20–27 表示共享选项(例如,强制全双工) IFM_GMASK
28–31 表示 mword 实例 IFM_IMASK

dataaux 参数允许驱动程序提供有关 mword 的元数据。因为驱动程序通常没有元数据提供,所以 dataaux 通常设置为 0NULL

ifmedia_set 函数设置 ifm 的默认媒体类型。此函数仅在设备初始化期间使用。

ifmedia_removeall 函数接受一个 ifmedia 结构,并从其中删除每个媒体类型。

Hello, world!

现在你已经熟悉了 if* 结构及其管理例程,让我们通过一个例子来了解。以下名为 em_setup_interface 的函数,在 /sys/dev/e1000/if_em.c 中定义,用于设置 em(4)ifnetifmedia 结构。(em(4) 驱动程序是用于英特尔 PCI 千兆以太网适配器的。)

static int
em_setup_interface(device_t dev, struct adapter *adapter)
{
        struct ifnet *ifp;

        ifp = adapter->ifp = if_alloc(IFT_ETHER);
        if (ifp == NULL) {
                device_printf(dev, "cannot allocate ifnet structure\n");
                return (-1);
        }

        if_initname(ifp, device_get_name(dev), device_get_unit(dev));
        ifp->if_mtu = ETHERMTU;
        ifp->if_init = em_init;
        ifp->if_softc = adapter;
        ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST;
        ifp->if_ioctl = em_ioctl;
        ifp->if_start = em_start;
        IFQ_SET_MAXLEN(&ifp->if_snd, adapter->num_tx_desc - 1);
        ifp->if_snd.ifq_drv_maxlen = adapter->num_tx_desc - 1;
        IFQ_SET_READY(&ifp->if_snd);

      ether_ifattach(ifp, adapter->hw.mac.addr);

        ifp->if_capabilities = ifp->if_capenable = 0;

        /* Enable checksum offload. */
      ifp->if_capabilities |= IFCAP_HWCSUM | IFCAP_VLAN_HWCSUM;
      ifp->if_capenable |= IFCAP_HWCSUM | IFCAP_VLAN_HWCSUM;

        /* Enable TCP segmentation offload. */
        ifp->if_capabilities |= IFCAP_TSO4;
        ifp->if_capenable |= IFCAP_TSO4;

        /* Enable VLAN support. */
        ifp->if_data.ifi_hdrlen = sizeof(struct ether_vlan_header);
        ifp->if_capabilities |= IFCAP_VLAN_HWTAGGING | IFCAP_VLAN_MTU;
        ifp->if_capenable |= IFCAP_VLAN_HWTAGGING | IFCAP_VLAN_MTU;

        /* Interface can filter VLAN tags. */
        ifp->if_capabilities |= IFCAP_VLAN_HWFILTER;

#ifdef DEVICE_POLLING
        ifp->if_capabilities |= IFCAP_POLLING;
#endif

        /* Enable Wake-on-LAN (WOL) via magic packet? */
      if (adapter->wol) {
                ifp->if_capabilities |= IFCAP_WOL;
                ifp->if_capenable |= IFCAP_WOL_MAGIC;
        }

      ifmedia_init(&adapter->media, IFM_IMASK, em_media_change,
            em_media_status);

      if ((adapter->hw.phy.media_type == e1000_media_type_fiber) ||
            (adapter->hw.phy.media_type == e1000_media_type_internal_serdes))
        {
                u_char fiber_type = IFM_1000_SX;

                ifmedia_add(&adapter->media,
                    IFM_ETHER | fiber_type, 0, NULL);
                ifmedia_add(&adapter->media,
                    IFM_ETHER | fiber_type | IFM_FDX, 0, NULL);
        } else {
                ifmedia_add(&adapter->media,
                    IFM_ETHER | IFM_10_T, 0, NULL);
                ifmedia_add(&adapter->media,
                    IFM_ETHER | IFM_10_T | IFM_FDX, 0, NULL);
                ifmedia_add(&adapter->media,
                    IFM_ETHER | IFM_100_TX, 0, NULL);
                ifmedia_add(&adapter->media,
                    IFM_ETHER | IFM_100_TX | IFM_FDX, 0, NULL);

                if (adapter->hw.phy.type != e1000_phy_ife) {
                        ifmedia_add(&adapter->media,
                            IFM_ETHER | IFM_1000_T, 0, NULL);
                        ifmedia_add(&adapter->media,
                            IFM_ETHER | IFM_1000_T | IFM_FDX, 0, NULL);
                }
        }

        ifmedia_add(&adapter->media, IFM_ETHER | IFM_AUTO, 0, NULL);
      ifmedia_set(&adapter->media, IFM_ETHER | IFM_AUTO);

        return (0);
}

这个函数可以分为三个部分。第一部分 分配 分配一个 特定于以太网的 ifnet 结构并将其存储在 适配器 adapter->ifp 中。然后 adapter->ifp 被定义并 激活 。(在这里,适配器是 em(4) 的 softc 结构的名称。)

第二部分 概述启用 接口的特性,例如 唤醒网络 (WOL)。(WOL 是一种以太网标准,允许计算机通过网络消息开机或唤醒。)

第三部分 初始化 初始化一个 ifmedia 结构,添加 将接口支持的媒体添加到其中,并且 定义 将默认媒体类型设置为 自动选择最佳媒体

注意

当然,em_setup_interfaceem(4)device_attach 例程中被调用。

mbuf 结构

mbuf 结构是网络数据的内存缓冲区。通常,这些数据跨越多个 mbuf 结构,这些结构被组织成一个称为 mbuf 链 的链表。

struct mbuf<sys/mbuf.h> 头文件中定义如下:

struct mbuf {
      struct m_hdr m_hdr;
      union {
                struct {
                        struct pkthdr MH_pkthdr;
                        union {
                                struct m_ext MH_ext;
                                char MH_databuf[MHLEN];
                        } MH_dat;
                } MH;
                char M_databuf[MLEN];
        } M_dat;
};

每个 mbuf 结构包含一个 数据 缓冲区和 头部 ,其外观如下:

struct m_hdr {
        struct mbuf     *mh_next;         /* Next mbuf in chain.          */
        struct mbuf     *mh_nextpkt;      /* Next chain in queue/record.  */
        caddr_t          mh_data;         /* Location of data.            */
        int              mh_len;          /* Data length.                 */
        int              mh_flags;        /* Flags.                       */
        short            mh_type;         /* Data type.                   */
        uint8_t          pad[M_HDR_PAD];  /* Padding for word alignment.  */
};

我们将在 第十七章 中通过一个使用 mbuf 的例子来讲解。有关 mbuf 的更多信息,请参阅 mbuf(9) 手册页。

消息信号中断

消息信号中断(MSI)和扩展消息信号中断(MSI-X)是发送中断的替代方法。传统上,设备包含一个中断引脚,它们通过断言该引脚来生成中断,但 MSI 和 MSI-X 启用设备会将一些数据(称为MSI 消息MSI-X 消息)发送到特定的内存地址以生成中断。MSI 和 MSI-X 启用设备可以定义多个唯一消息。随后,驱动程序可以定义多个唯一的中断处理程序。换句话说,MSI 和 MSI-X 启用设备可以发出不同的中断,每个中断指定不同的条件或任务。MSI 和 MSI-X 启用设备可以分别定义多达 32 和 2,048 个唯一消息。(MSI 和 MSI-X 不仅限于网络设备。然而,它们仅限于 PCI 和 PCIe 设备。)

实现 MSI

与之前的话题不同,这里我将采取一种整体的方法。具体来说,我将首先展示一个示例,然后描述 MSI 函数族。

以下函数名为ciss_setup_msix,定义在/sys/dev/ciss/ciss.c中,用于为ciss(4)驱动程序设置 MSI。

注意

这个函数之所以被选择,仅仅是因为它很简单。它来自ciss(4)的事实并不重要。

static int
ciss_setup_msix(struct ciss_softc *sc)
{
        int i, count, error;

        i = ciss_lookup(sc->ciss_dev);
      if (ciss_vendor_data[i].flags & CISS_BOARD_NOMSI)
                return (EINVAL);

        count = pci_msix_count(sc->ciss_dev);
        if (count < CISS_MSI_COUNT) {
                count = pci_msi_count(sc->ciss_dev);
                if (count < CISS_MSI_COUNT)
                        return (EINVAL);
        }

        count = MIN(count, CISS_MSI_COUNT);
        error = pci_alloc_msix(sc->ciss_dev, &count);
        if (error) {
                error = pci_alloc_msi(sc->ciss_dev, &count);
                if (error)
                        return (EINVAL);
        }

        sc->ciss_msi = count;
        for (i = 0; i < count; i++)
              sc->ciss_irq_rid[i] = i + 1;

        return (0);
}

这个函数由四个部分组成。第一部分httpatomoreillycomsourcenostarchimages1137499.png确保设备实际上支持 MSI。

第二部分确定设备维护的唯一 MSI-X 或 MSI 消息的数量,并将答案存储在count中。

第三部分分配counthttpatomoreillycomsourcenostarchimages1137505.png MSI-Xhttpatomoreillycomsourcenostarchimages1137507.png MSI 向量,这些向量将每个消息连接到一个具有 1 到countridSYS_RES_IRQ资源。因此,为了将中断处理程序分配给第八个消息,你需要调用bus_alloc_resource_any(以分配一个SYS_RES_IRQ资源)并将 8 作为rid参数传递。然后你通常会调用bus_setup_intr

最后,第四部分httpatomoreillycomsourcenostarchimages1137509.png将每个 MSI-X 或 MSI 消息的rid存储在ciss_irq_rid数组中。

自然地,这个函数在ciss(4)device_attach例程中被调用,如下所示:

...
        /*
         * Use MSI/MSI-X?
         */
        sc->ciss_irq_rid[0] = 0;
        if (method == CISS_TRANSPORT_METHOD_PERF) {
                ciss_printf(sc, "Performant Transport\n");

                if (ciss_force_interrupt != 1 && ciss_setup_msix(sc) == 0)
                        intr = ciss_perf_msi_intr;
                else
                        intr = ciss_perf_intr;

                sc->ciss_interrupt_mask =
                    CISS_TL_PERF_INTR_OPQ | CISS_TL_PERF_INTR_MSI;
        } else {
                ciss_printf(sc, "Simple Transport\n");

                if (ciss_force_interrupt == 2)
                      ciss_setup_msix(sc);

                sc->ciss_perf = NULL;
                intr = ciss_intr;
                sc->ciss_interrupt_mask = sqmask;
        }

        /*
         * Disable interrupts.
         */
        CISS_TL_SIMPLE_DISABLE_INTERRUPTS(sc);

        /*
         * Set up the interrupt handler.
         */
        sc->ciss_irq_resource = bus_alloc_resource_any(sc->ciss_dev,
            SYS_RES_IRQ, &sc->ciss_irq_rid[0], RF_ACTIVE | RF_SHAREABLE);
        if (sc->ciss_irq_resource == NULL) {
                ciss_printf(sc, "cannot allocate interrupt resource\n");
                return (ENXIO);
        }

        error = bus_setup_intr(sc->ciss_dev, sc->ciss_irq_resource,
            INTR_TYPE_CAM | INTR_MPSAFE, NULL, intr, sc, &sc->ciss_intr);
        if (error) {
                ciss_printf(sc, "cannot set up interrupt\n");
                return (ENXIO);
        }
...

注意 MSI 是在httpatomoreillycomsourcenostarchimages1137499.pnghttpatomoreillycomsourcenostarchimages1137501.png获取中断之前httpatomoreillycomsourcenostarchimages1137503.png设置的。此外,注意rid参数是ciss_irq_rid

注意

到目前为止,ciss(4)只支持第一个 MSI-X 或 MSI 消息。

MSI 管理例程

FreeBSD 内核提供了以下函数用于处理 MSI:

#include <dev/pci/pcivar.h>

int
pci_msix_count(device_t dev);

int
pci_msi_count(device_t dev);

int
pci_alloc_msix(device_t dev, int *count);

int
pci_alloc_msi(device_t dev, int *count);

int
pci_release_msi(device_t dev);

pci_msix_countpci_msi_count 函数返回设备 dev 维护的唯一 MSI-X 或 MSI 消息的数量。

pci_alloc_msixpci_alloc_msi 函数根据 dev 分配 count 个 MSI-X 或 MSI 向量。如果可用向量不足,则分配的向量数将少于 count。成功返回后,count 将包含分配的向量数。(MSI-X 和 MSI 向量在 实现 MSI 中进行了描述,见 消息信号中断。)

pci_release_msi 函数释放由 pci_alloc_msixpci_alloc_msi 分配的 MSI-X 或 MSI 向量。

结论

本章探讨了 ifnetifmediambuf 结构,以及 MSI 和 MSI-X。在 第十七章 中,你将使用这些信息来分析一个网络驱动程序。

第十七章。网络驱动程序,第二部分:数据包接收和传输

无标题图片

本章探讨了 em(4) 的数据包接收和传输组件。不出所料,em(4) 使用 mbuf 和 MSI 进行数据包接收和传输。

数据包接收

当一个接口接收到一个数据包时,它会发送一个中断。自然地,这会导致其中断处理程序执行。例如,以下是在 em(4) 中执行的内容:

static void
em_msix_rx(void *arg)
{
        struct rx_ring *rxr = arg;
        struct adapter *adapter = rxr->adapter;
        bool more;

        ++rxr->rx_irq;

        more = em_rxeof(rxr, adapter->rx_process_limit, NULL);
        if (more)
                taskqueue_enqueue(rxr->tq, &rxr->rx_task);
        else
                E1000_WRITE_REG(&adapter->hw, E1000_IMS, rxr->ims);
}

此函数接收一个 环缓冲区指针图片 指向包含一个或多个接收到的数据包的环缓冲区的指针,并调用 em_rxeof 图片 em_rxeof 来处理这些数据包。如果有超过 rx_process_limit 图片 rx_process_limit 个数据包,则将 task 结构排队;否则,此中断 重新启用图片 重新启用。我将在 em_handle_rx 函数 中讨论 task 结构及其相关函数。

em_rxeof 函数

如前所述,em_rxeof 处理接收到的数据包。其函数定义如下,但由于此函数相当长且复杂,我将分部分介绍。以下是第一部分:

static bool
em_rxeof(struct rx_ring *rxr, int count, int *done)
{
        struct adapter *adapter = rxr->adapter;
        struct ifnet *ifp = adapter->ifp;
        struct e1000_rx_desc *cur;
        struct mbuf *mp, *sendmp;
        u8 status = 0;
        u16 len;
        int i, processed, rxdone = 0;
        bool eop;

        EM_RX_LOCK(rxr);

      for (i = rxr->next_to_check, processed = 0; count != 0; ) {
              if ((ifp->if_drv_flags & IFF_DRV_RUNNING) == 0)
                        break;

              bus_dmamap_sync(rxr->rxdma.dma_tag, rxr->rxdma.dma_map,
                    BUS_DMASYNC_POSTREAD);

                mp = sendmp = NULL;
                cur = &rxr->rx_base[i];
                status = cur->status;
                if ((status & E1000_RXD_STAT_DD) == 0)
                        break;
                len = le16toh(cur->length);
                eop = (status & E1000_RXD_STAT_EOP) != 0;

                if ((cur->errors & E1000_RXD_ERR_FRAME_ERR_MASK) ||
                    (rxr->discard == TRUE)) {
                        ++ifp->if_ierrors;
                        ++rxr->rx_discarded;
                        if (!eop)
                                rxr->discard = TRUE;
                        else
                                rxr->discard = FALSE;
                      em_rx_discard(rxr, i);
                        goto next_desc;
                }
...

此函数的执行主要在一个 for 循环图片 for 循环中进行。此循环首先 验证接口图片 验证接口是否已启动并运行。然后它 同步 DMA 缓冲区图片 同步当前加载在 DMA 映射图片 rxr->rxdma.dma_map 中的 DMA 缓冲区,该缓冲区是 rx_base 图片 rxr->rx_base

缓冲区 缓冲区图片 rxr->rx_base[i] 包含一个描述符,用于描述接收到的数据包。当一个数据包跨越多个 mbuf 时,rxr->rx_base[i] 描述链中的其中一个 mbuf。

如果 rxr->rx_base[i] 缺少 接收描述符状态图片 E1000_RXD_STAT_DD 标志,则 for 循环退出。(E1000_RXD_STAT_DD 标志代表 接收描述符状态:描述符完成。我们很快就会看到其效果。)

如果 rxr->rx_base[i] 描述链中的 最后一个 mbuf 图片 最后一个 mbuf,布尔变量 eop(代表 数据包结束)被设置为 TRUE。(不用说,当一个数据包只需要一个 mbuf 时,该 mbuf 仍然是链中的最后一个 mbuf。)

如果由rxr->rx_base[i]描述的包包含任何错误,则丢弃。请注意,在这里我使用的是单词,而不是mbuf,因为包中的每个 mbuf 都被丢弃。

现在,让我们看看em_rxeof的下一部分:

...
              mp = rxr->rx_buffers[i].m_head;
                mp->m_len = len;
                rxr->rx_buffers[i].m_head = NULL;

              if (rxr->fmp == NULL) {
                        mp->m_pkthdr.len = len;
                      rxr->fmp = rxr->lmp = mp;
                } else {
                        mp->m_flags &= ˜M_PKTHDR;
                      rxr->lmp->m_next = mp;
                      rxr->lmp = mp;
                        rxr->fmp->m_pkthdr.len += len;
                }
...

在这里,rxr->fmprxr->lmp指向链中的第一个和最后一个 mbuf,mprxr->rx_base[i]描述的 mbuf,lenmp的长度。

因此,这部分只是识别是否mp是链中的第一个 mbuf。如果不是,则mp被链接到链中

这里是em_rxeof的下一部分:

...
                 if (eop) {
                          --count;
                        sendmp = rxr->fmp;
                            sendmp->m_pkthdr.rcvif = ifp;
                          ++ifp->if_ipackets;
                        em_receive_checksum(cur, sendmp);
 #ifndef __NO_STRICT_ALIGNMENT
                        if (adapter->max_frame_size >
                              (MCLBYTES - ETHER_ALIGN) &&
                            em_fixup_rx(rxr) != 0)
                                  goto skip;
  #endif
                          if (status & E1000_RXD_STAT_VP) {
                                  sendmp->m_pkthdr.ether_vtag =
                                      le16toh(cur->special) &
                                      E1000_RXD_SPC_VLAN_MASK;
                                  sendmp->m_flags |= M_VLANTAG;
                          }
  #ifndef __NO_STRICT_ALIGNMENT
  skip:
  #endif
                        rxr->fmp = rxr->lmp = NULL;
                  }
  ...

如果mp是链中的最后一个 mbuf,则sendmp被设置为链中的第一个 mbuf,并且验证了头部校验和。

如果我们的架构需要严格对齐并且启用了巨帧,则em_rxeof对齐 mbuf 链。 (巨帧是包含超过 1500 字节数据的以太网数据包。)

这一部分通过设置rxr->fmprxr->lmpNULL来结束。以下是em_rxeof的下一部分:

...
next_desc:
                cur->status = 0;
                ++rxdone;
                ++processed;

                if (++i == adapter->num_rx_desc)
                        i = 0;

              if (sendmp != NULL) {
                        rxr->next_to_check = i;
                        EM_RX_UNLOCK(rxr);
                      (*ifp->if_input)(ifp, sendmp);
                        EM_RX_LOCK(rxr);
                        i = rxr->next_to_check;
                }

                if (processed == 8) {
                      em_refresh_mbufs(rxr, i);
                        processed = 0;
                }
        }                                      /* The end of the for loop. */
...

在这里,i递增以便em_rxeof可以到达环中的下一个 mbuf。然后,如果sendmp指向一个 mbuf 链,则执行em(4)的输入例程来发送该链到上层。之后,为em(4)分配新的 mbuf。

注意

当将 mbuf 链发送到上层时,驱动程序不得再访问这些 mbuf。从所有目的和用途来看,这些 mbuf 已经被释放。

总结来说,这个 for 循环只是将接收到的数据包中的每个 mbuf 链接起来,并将其发送到上层。这个过程会一直持续,直到处理完环中的每个数据包或达到 rx_process_limitrx_process_limit 在 数据包接收 中描述过)。

这是 em_rxeof 的最后部分:

...
        if (e1000_rx_unrefreshed(rxr))
                em_refresh_mbufs(rxr, i);

        rxr->next_to_check = i;
        if (done != NULL)
                *done = rxdone;
        EM_RX_UNLOCK(rxr);

      return ((status & E1000_RXD_STAT_DD) ? TRUE : FALSE);
}

如果还有更多数据包需要处理,em_rxeof 会返回 TRUE

em_handle_rx 函数

回想一下,当 em_rxeof 返回 TRUE 时,em_msix_rx 会将一个任务结构排队(em_msix_rx 在 数据包接收 中讨论过)。

这是那个 task 结构的函数:

static void
em_handle_rx(void *context, int pending)
{
        struct rx_ring *rxr = context;
        struct adapter *adapter = rxr->adapter;
        bool more;

        more = em_rxeof(rxr, adapter->rx_process_limit, NULL);
        if (more)
                taskqueue_enqueue(rxr->tq, &rxr->rx_task);
        else
                E1000_WRITE_REG(&adapter->hw, E1000_IMS, rxr->ims);
}

此函数几乎与 em_msix_rx 相同。当有更多数据包需要处理时,em_rxeof 会被再次调用。

数据包传输

为了传输一个数据包,网络栈会调用驱动程序的输出例程。所有输出例程都以调用其接口的传输或启动例程结束。以下是 em(4) 的启动例程:

static void
em_start(struct ifnet *ifp)
{
        struct adapter *adapter = ifp->if_softc;
        struct tx_ring *txr = adapter->tx_rings;

        if (ifp->if_drv_flags & IFF_DRV_RUNNING) {
              EM_TX_LOCK(txr);
              em_start_locked(ifp, txr);
                EM_TX_UNLOCK(txr);
        }
}

此启动例程会获取一个锁,然后调用 em_start_locked

em_start_locked 函数

em_start_locked 函数的定义如下:

static void
em_start_locked(struct ifnet *ifp, struct tx_ring *txr)
{
        struct adapter *adapter = ifp->if_softc;
        struct mbuf *m_head;

        EM_TX_LOCK_ASSERT(txr);

        if ((ifp->if_drv_flags & (IFF_DRV_RUNNING | IFF_DRV_OACTIVE)) !=
            IFF_DRV_RUNNING)
                return;

        if (!adapter->link_active)
                return;

      while (!IFQ_DRV_IS_EMPTY(&ifp->if_snd)) {
              if (txr->tx_avail <= EM_TX_CLEANUP_THRESHOLD)
                      em_txeof(txr);

              if (txr->tx_avail < EM_MAX_SCATTER) {
                      ifp->if_drv_flags |= IFF_DRV_OACTIVE;
                        break;
                }

              IFQ_DRV_DEQUEUE(&ifp->if_snd, m_head);
                if (m_head == NULL)
                        break;

                if (em_xmit(txr, &m_head)) {
                        if (m_head == NULL)
                                break;
                        ifp->if_drv_flags |= IFF_DRV_OACTIVE;
                        IFQ_DRV_PREPEND(&ifp->if_snd, m_head);
                        break;
                }

                ETHER_BPF_MTAP(ifp, m_head);

                txr->watchdog_time = ticks;
                txr->queue_status = EM_QUEUE_WORKING;
        }
}

此函数从 em(4) 的发送队列中移除一个 mbuf 并将其传输到接口。这个过程会一直重复,直到发送队列为空。(发送队列,如第十六章所述,由输出例程填充。)

注意

实际将 mbuf 传输到接口的 em_xmit 函数在本书中没有详细说明,因为其长度较长。尽管如此,它相当直接,所以你不会遇到任何麻烦。

如果可用的传输描述符数量小于或等于 EM_TX_CLEANUP_THRESHOLD,则会调用 em_txeof 来回收已使用的描述符。(传输描述符描述一个出站数据包。如果一个数据包跨越多个 mbuf,则传输描述符描述链中的一个 mbuf。)

如果可用的传输描述符数量小于 EM_MAX_SCATTER,则传输会被停止。

em_txeof 函数

em_txeof 函数遍历传输描述符,并为已传输的数据包释放 mbufs。其函数定义如下,但由于这个函数相当长且复杂,我将分部分介绍。以下是第一部分:

static bool
em_txeof(struct tx_ring *txr)
{
        struct adapter *adapter = txr->adapter;
        struct ifnet *ifp = adapter->ifp;
        struct e1000_tx_desc *tx_desc, *eop_desc;
        struct em_buffer *tx_buffer;
        int processed, first, last, done;

        EM_TX_LOCK_ASSERT(txr);

        if (txr->tx_avail == adapter->num_tx_desc) {
                txr->queue_status = EM_QUEUE_IDLE;
                return (FALSE);
        }

        processed = 0;
      first = txr->next_to_clean;
      tx_desc = &txr->tx_base[first];
      tx_buffer = &txr->tx_buffers[first];
      last = tx_buffer->next_eop;
        eop_desc = &txr->tx_base[last];

        if (++last == adapter->num_tx_desc)
                last = 0;
      done = last;
...

在这里, first 是包含传出数据包的链中的第一个 mbuf, last 是该链中的最后一个 mbuf,而 donelast 之后的 mbuf。

注意

回想一下,传输描述符以及随后的 mbufs 都存储在一个环形缓冲区中。

变量 tx_desc tx_buffer 是传输描述符及其相关 mbuf 的临时变量。

现在我们来看 em_txeof 的下一部分:

...
        bus_dmamap_sync(txr->txdma.dma_tag, txr->txdma.dma_map,
            BUS_DMASYNC_POSTREAD);

      while (eop_desc->upper.fields.status & E1000_TXD_STAT_DD) {
              while (first != done) {
                      tx_desc->upper.data = 0;
                        tx_desc->lower.data = 0;
                        tx_desc->buffer_addr = 0;
                        ++txr->tx_avail;
                        ++processed;

                        if (tx_buffer->m_head) {
                                bus_dmamap_unload(txr->txtag,
                                    tx_buffer->map);
                              m_freem(tx_buffer->m_head);
                                tx_buffer->m_head = NULL;
                        }

                        tx_buffer->next_eop = −1;
                        txr->watchdog_time = ticks;

                        if (++first == adapter->num_tx_desc)
                                first = 0;
                        tx_buffer = &txr->tx_buffers[first];
                        tx_desc = &txr->tx_base[first];
                }

                ++ifp->if_opackets;

                last = tx_buffer->next_eop;
              if (last != −1) {
                        eop_desc = &txr->tx_base[last];
                        if (++last == adapter->num_tx_desc)
                                last = 0;
                        done = last;
                } else
                        break;
        }

        bus_dmamap_sync(txr->txdma.dma_tag, txr->txdma.dma_map,
            BUS_DMASYNC_PREWRITE);
...

这个 while 循环遍历 firstlast 释放它们的 mbufs 和 将它们的传输描述符置零。(em(4) 有一个有限的传输描述符数量。将描述符置零使其再次可用。)

这个 while 循环 判断是否可以通过这个 while 循环释放另一个 mbuf 链。

这是 em_txeof 的最后一部分:

...
        txr->next_to_clean = first;

        if (!processed && ((ticks - txr->watchdog_time) > EM_WATCHDOG))
                txr->queue_status = EM_QUEUE_HUNG;

      if (txr->tx_avail > EM_MAX_SCATTER)
              ifp->if_drv_flags &= ˜IFF_DRV_OACTIVE;

        if (txr->tx_avail == adapter->num_tx_desc) {
                txr->queue_status = EM_QUEUE_IDLE;
              return (FALSE);
        }

      return (TRUE);
}

如果还有更多要回收的传输描述符,em_txeof 返回 TRUE;否则,它返回 FALSE

如果可用传输描述符的数量 大于 EM_MAX_SCATTER,则可以传输数据包。

数据包传输后

每当接口发送一个数据包时,它会发送一个中断。自然地,这会导致其中断处理程序执行。以下是 em(4) 中执行的内容:

static void
em_msix_tx(void *arg)
{
        struct tx_ring *txr = arg;
        struct adapter *adapter = txr->adapter;
        bool more;

        ++txr->tx_irq;

        EM_TX_LOCK(txr);
        more = em_txeof(txr);
        EM_TX_UNLOCK(txr);
        if (more)
              taskqueue_enqueue(txr->tq, &txr->tx_task);
        else
                E1000_WRITE_REG(&adapter->hw, E1000_IMS, txr->ims);
}

注意

由于 MSI,em(4) 可以使用不同的中断处理程序来处理数据包传输和接收。

这个函数简单地 回收已使用的传输描述符。如果有更多描述符要回收,则将一个 task 结构排队。以下是该 task 结构的函数:

static void
em_handle_tx(void *context, int pending)
{
        struct tx_ring *txr = context;
        struct adapter *adapter = txr->adapter;
        struct ifnet *ifp = adapter->ifp;

        EM_TX_LOCK(txr);

      em_txeof(txr);
      em_start_locked(ifp, txr);
        E1000_WRITE_REG(&adapter->hw, E1000_IMS, txr->ims);

        EM_TX_UNLOCK(txr);
}

这个函数首先 回收任何已使用的传输描述符,之后,任何可能由于描述符不足而暂停的包将被 传输。

结论

本章和第十六章介绍了网络设备和驱动程序的基础知识。如果你认真对待编写网络驱动程序,你应该全面复习em(4)。我建议从它的device_attach实现开始:em_attach

参考文献

Baldwin, JohnH. “Locking in the Multithreaded FreeBSD Kernel.” Proceedings of the BSDCon 2002 Conference, February 2002.

Corbet, Jonathan, AlessandroRubini, and GregKroah-Hartman. Linux Device Drivers. 3rd ed. Sebastopol, CA: O’Reilly Media, 2005.

Kernighan, BrianW. and Dennis M.Ritchie. The C Programming Language. 2nd ed. Englewood Cliffs, NJ: Prentice Hall PTR, 1988.

Kong, Joseph. Designing BSD Rootkits. San Francisco, CA: No Starch Press, 2007.

McKusick, MarshallKirk and George V.Neville-Neil. The Design and Implementation of the FreeBSD Operating System. Boston, MA: Addison-Wesley Professional, 2005.

Neville-Neil, GeorgeV. “Networking from the Bottom Up: Device Drivers.” Tutorial given at AsiaBSDCon, January 2010.

Oney, Walter. Programming the Microsoft Windows Driver Model. 2nd ed. Redmond, Washington: Microsoft Press, 2003.

Orwick, Penny and GuySmith. Developing Drivers with the Windows Driver Foundation. Redmond, Washington: Microsoft Press, 2007.

Stevens, W.Richard. Advanced Programming in the UNIX Environment. Reading, MA: Addison-Wesley Professional, 1992.

van derLinden, Peter. Expert C Programming. Englewood Cliffs, NJ: Prentice Hall, 1994.

索引

关于数字索引的说明

索引条目中的链接显示为该条目出现的部分标题。由于某些部分有多个索引标记,一个条目有多个链接到同一部分并不罕见。点击任何链接都会直接跳转到文本中标记出现的位置。

符号

%eax 值,一个简单的同步问题

*ifattach 函数,网络接口结构

*sleep 函数,自旋互斥锁

0 常量,调用

00-04 位,网络接口媒体结构管理例程

05-07 位,网络接口媒体结构管理例程

08-15 位,网络接口媒体结构管理例程

0xFFFFFFFF,创建 DMA 标签

16-18 位,网络接口媒体结构管理例程

19 位,网络接口媒体结构管理例程

20-27 位,网络接口媒体结构管理例程

28-31 位,网络接口媒体结构管理例程

<bsd.kmod.mk> Makefile,编译和加载

<sys/malloc.h> 头文件,MALLOC_DECLARE 宏

<sys/module.h> 头文件,name

_IO 宏,ioctl

_IOR 宏,ioctl

_IOW 宏,ioctl

_IOWR 宏,ioctl

_pcsid 结构体,foo_pci_probe 函数

A

访问参数,创建动态 sysctl

acpi_sleep_event 事件处理器,unload 函数

acpi_wakeup_event 事件处理器,unload 函数

动作例程,cam_sim_alloc 函数,XPT_PATH_INQ,XPT_RESET_BUS,XPT_GET_TRAN_SETTINGS,XPT_GET_TRAN_SETTINGS,XPT_SCSI_IO,XPT_SCSI_IO

XPT_GET_TRAN_SETTINGS 常量,XPT_RESET_BUS

XPT_PATH_INQ 常量,cam_sim_alloc 函数

XPT_RESET_BUS 常量,XPT_PATH_INQ

XPT_RESET_DEV 常量,XPT_SCSI_IO

XPT_SCSI_IO 常量,XPT_SCSI_IO

XPT_SET_TRAN_SETTINGS 常量,XPT_GET_TRAN_SETTINGS

高级技术附件包接口 (ATAPI),通用访问方法

ahc_action 函数,CAM 如何工作

ahc_done 函数,CAM 如何工作,mfip_start 函数

对齐参数,连续物理内存管理例程,创建 DMA 标签

交替设置,更多关于 USB 设备的信息

arg 参数,创建动态 sysctls,DRIVER_MODULE 宏

at45d_attach 函数,将一切整合在一起

at45d_delayed_attach 函数,at45d_attach 函数

at45d_get_info 函数,at45d_delayed_attach 函数

at45d_get_status 函数,at45d_get_info 函数

at45d_strategy 函数,at45d_get_status 函数

at45d_task 函数,at45d_get_status 函数

ATAPI(高级技术附件包接口),通用访问方法

atomic_add_int 函数,nmdm_alloc 函数

自动配置。参见 Newbus 驱动程序,at45d_task 函数

B

bio 结构,驱动程序私有数据

biodone 函数,at45d_task 函数

biofinish 函数,at45d_task 函数

bioq_flush 函数,块 I/O 队列

bioq_insert_head 函数,块 I/O 队列

bioq_insert_tail 函数,块 I/O 队列

bioq_remove 函数,块 I/O 队列

bio_pblkno 变量,at45d_task 函数

bits_per_char 函数,nmdm_timeout 函数

块设备,设备驱动程序类型

块驱动程序,DEV_MODULE 宏

块 I/O 队列,块 I/O 队列

块 I/O 结构,驱动程序私有数据

块,定义,存储驱动程序

以块为中心的 I/O 请求,at45d_task 函数

边界参数,连续物理内存管理例程,创建 DMA 标签

bt.c 源文件,XPT_SCSI_IO

缓冲区,DMA,实现 DMA,拆解 DMA 标签,bus_dma_segment 结构,bus_dmamap_load 函数,bus_dmamap_load 函数,bus_dmamap_load_mbuf_sg 函数,bus_dmamap_load_mbuf_sg 函数

bus_dmamap_load 函数,bus_dma_segment 结构

bus_dmamap_load_mbuf 函数,bus_dmamap_load 函数

bus_dmamap_load_mbuf_sg 函数,bus_dmamap_load 函数

bus_dmamap_load_uio 函数,bus_dmamap_load_mbuf_sg 函数

bus_dmamap_unload 函数,bus_dmamap_load_mbuf_sg 函数

bus_dma_segment 结构,拆除 DMA 标签

buflen 参数,bus_dmamap_load 函数

bufsize 字段,USB 配置结构

批量端点,关于 USB 设备

busname 参数,DRIVER_MODULE 宏

bus_alloc_resource 函数,硬件资源管理,I/O 端口和 I/O 内存

bus_deactivate_resource 函数,硬件资源管理

bus_dmamap_create 函数,实现 DMA,拆除 DMA 标签

bus_dmamap_destroy 函数,拆除 DMA 标签

bus_dmamap_load 函数,实现 DMA,bus_dma_segment 结构

bus_dmamap_load_mbuf 函数,bus_dmamap_load 函数

bus_dmamap_load_mbuf_sg 函数,bus_dmamap_load 函数

bus_dmamap_load_uio 函数,bus_dmamap_load_mbuf_sg 函数

bus_dmamap_unload 函数,bus_dmamap_load_mbuf_sg 函数

bus_dmamem_alloc 函数,bus_dmamap_load_mbuf_sg 函数,一个简单的示例

bus_dmamem_free 函数,DMA 映射管理例程,第二部分

BUS_DMASYNC_POSTREAD 常量,一个简单的示例

BUS_DMASYNC_PREWRITE 常量,一个简单的示例

BUS_DMA_ALLOCNOW 常量,创建 DMA 标签

BUS_DMA_COHERENT 常量,拆除 DMA 标签,DMA 映射管理例程,第二部分

BUS_DMA_NOCACHE 常量,内存屏障,bus_dmamap_load 函数,DMA 映射管理例程,第二部分

BUS_DMA_NOWAIT 常量,bus_dmamap_load 函数,DMA 映射管理例程,第二部分

bus_dma_segment 结构,拆除 DMA 标签

bus_dma_tag_create 函数,实现 DMA

bus_dma_tag_destroy 函数,创建 DMA 标签

BUS_DMA_WAITOK 常量,DMA 映射管理例程第二部分

BUS_DMA_ZERO 常量,DMA 映射管理例程第二部分

BUS_PROBE_SPECIFIC 成功代码,pint_probe 函数

bus_read_N 函数,从 I/O 端口和 I/O 内存读取

bus_release_resource 函数,硬件资源管理

bus_setup_intr 函数,注册中断处理程序

BUS_SPACE_BARRIER_READ 常量,内存屏障

BUS_SPACE_BARRIER_WRITE 常量,内存屏障

BUS_SPACE_MAXADDR 常量,创建 DMA 标签

bus_teardown_intr 函数,注册中断处理程序

bus_write_multi_N 函数,写入 I/O 端口和 I/O 内存

bus_write_N 函数,写入 I/O 端口和 I/O 内存

bus_write_region_N 函数,写入 I/O 端口和 I/O 内存

C

callback 参数,bus_dma_segment 结构

callback 字段,USB 配置结构

callback2 参数,bus_dmamap_load 函数

callback2 函数,bus_dmamap_load 函数

callbackarg 参数,bus_dma_segment 结构

callouts,调用

callout_drain 函数,调用

callout_init 函数,内核事件处理程序

callout_init_mtx 函数,调用

CALLOUT_MPSAFE 常量,调用

callout_reset 函数,调用

CALLOUT_RETURNUNLOCKED 常量,调用

callout_schedule 函数,调用

CALLOUT_SHAREDLOCK 常量,调用

callout_stop 函数,调用

CAM(通用访问方法)标准,通用访问方法,一个(相对)简单的示例,mfip_attach 函数,mfip_detach 函数,mfip_detach 函数,mfip_action 函数,mfip_action 函数,mfip_start 函数,SIM 注册例程,SIM 注册例程,SIM 注册例程,cam_sim_alloc 函数,cam_sim_alloc 函数,XPT_PATH_INQ,XPT_RESET_BUS,XPT_RESET_BUS,XPT_RESET_BUS,XPT_GET_TRAN_SETTINGS,XPT_SCSI_IO,XPT_SCSI_IO

动作例程,cam_sim_alloc 函数,XPT_PATH_INQ,XPT_RESET_BUS,XPT_RESET_BUS,XPT_GET_TRAN_SETTINGS,XPT_SCSI_IO,XPT_SCSI_IO

XPT_GET_TRAN_SETTINGS 常量,XPT_RESET_BUS

XPT_PATH_INQ 常量,cam_sim_alloc 函数

XPT_RESET_BUS 常量,XPT_PATH_INQ

XPT_RESET_DEV 常量,XPT_SCSI_IO

XPT_SCSI_IO 常量,XPT_SCSI_IO

XPT_SET_TRAN_SETTINGS 常量,XPT_GET_TRAN_SETTINGS

HBA(主机总线适配器)驱动程序示例,一个(相对)简单的示例,mfip_attach 函数,mfip_detach 函数,mfip_detach 函数,mfip_action 函数,mfip_action 函数,mfip_start 函数

mfip_action 函数,mfip_detach 函数

mfip_attach 函数,一个(相对)简单的示例

mfip_detach 函数,mfip_attach 函数

mfip_done 函数,mfip_start 函数

mfip_poll 函数,mfip_action 函数

mfip_start 函数,mfip_action 函数

概述,通用访问方法

SIM 注册例程,SIM 注册例程,SIM 注册例程,SIM 注册例程,cam_sim_alloc 函数

cam_simq_alloc 函数,SIM 注册例程

cam_sim_alloc 函数,SIM 注册例程

xpt_bus_register 函数,cam_sim_alloc 函数

CAM 控制块 (CCB),CAM 的工作原理

camisr 函数,CAM 的工作原理

cam_simq_alloc 函数,SIM 注册例程

cam_sim_alloc 函数,mfip_attach 函数,SIM 注册例程

CCB (CAM 控制块),CAM 的工作原理

ccb_h.func_code 变量,mfip_action 函数

ccb_pathinq 结构,cam_sim_alloc 函数,XPT_PATH_INQ

ccb_scsiio 结构,XPT_SCSI_IO

ccb_trans_settings 结构,XPT_GET_TRAN_SETTINGS

chan 参数,自愿上下文切换或睡眠

change_callback 参数,网络接口媒体结构管理例程

字符设备,设备驱动程序类型

字符设备驱动程序,编译和加载,d_foo 函数,字符设备切换表,字符设备切换表,字符设备切换表,大多无害,echo_write 函数,echo_read 函数,DEV_MODULE 宏,DEV_MODULE 宏

字符设备切换表,字符设备切换表

destroy_dev 函数,字符设备切换表

DEV_MODULE 宏,DEV_MODULE 宏

d_foo 函数,编译和加载

echo_modevent 函数,echo_read 函数

echo_read 函数,echo_write 函数

echo_write 函数,大多无害

加载,DEV_MODULE 宏

make_dev 函数,字符设备切换表

ciss_setup_msix 函数,mbuf 结构

ioctl 接口的命令,ioctl

通用访问方法 (CAM) 标准。参见 CAM 标准,编译和加载

编译 KLDs,编译和加载

条件变量,条件变量

USB 驱动程序的配置结构,USB 配置结构,USB 配置结构,可选字段,USB 传输(在 FreeBSD 中),USB 传输(在 FreeBSD 中)

管理例程,USB 传输(在 FreeBSD 中)

必需字段,USB 配置结构

可选字段,USB 配置结构

传输标志,可选字段

配置,更多关于 USB 设备

sysctl 接口的上下文,实现 sysctls,第一部分

contigfree 函数,将一切联系在一起,连续物理内存管理例程

contigmalloc 函数,将一切联系在一起

连续物理内存,连续物理内存管理例程

控制端点,关于 USB 设备

cookiep 参数,注册中断处理程序

计数参数,硬件资源管理

计数值,一个简单的同步问题

ctx 参数,创建动态 sysctls

cv_broadcastpri 函数,条件变量管理例程

cv_destroy 函数,条件变量管理例程

cv_init 函数,条件变量管理例程

cv_timedwait 函数,条件变量管理例程

cv_timedwait_sig 函数,条件变量管理例程

cv_wait_sig 函数,条件变量管理例程

cv_wait_unlock 函数,条件变量管理例程

cv_wmesg 函数,条件变量管理例程

D

d(描述符)参数,调用 ioctl

dadone 函数,CAM 如何工作

dastart 函数,CAM 如何工作

dastrategy 函数,通用访问方法

数据参数,name

数据载体检测(DCD),nmdm_task_tty 函数

USB 驱动程序的数据传输,USB 传输标志

debug.sleep.test 系统控制,load 函数

DECLARE_MODULE 宏,name,name,name,name,name

数据参数,name

name 参数,name

order 参数,name

子参数,name

延迟执行,自愿上下文切换或睡眠,自愿上下文切换或睡眠,实现睡眠和条件变量,sleep_modevent 函数,load 函数,sleep_thread 函数,sleep_thread 函数,unload 函数,内核事件处理器,Callouts,Callouts,Taskqueues,Taskqueues,全局 Taskqueues

Callouts,Callouts

事件处理器,unload 函数

load 函数,sleep_modevent 函数

睡眠,自愿上下文切换或睡眠

sleep_modevent 函数,实现睡眠和条件变量

sleep_thread 函数,load 函数

sysctl_debug_sleep_test 函数,sleep_thread 函数

taskqueues,Callouts,Taskqueues,Taskqueues,全局 Taskqueues

全局,全局 Taskqueues

管理例程,Taskqueues

概述,Callouts

unload 函数,sleep_thread 函数

自愿上下文切换,自愿上下文切换或睡眠

descr 参数,创建动态 sysctls

磁盘结构的描述字段,磁盘结构

描述符(d 参数),调用 ioctl

为 DMA 销毁标签,创建 DMA 标签

destroy_dev 函数,字符设备切换表,race_modevent 函数

devclass 参数,DRIVER_MODULE 宏

设备方法表,设备方法表

设备,构建和运行模块,构建和运行模块,更多关于 USB 设备,更多关于 USB 设备

配置,更多关于 USB 设备

定义,构建和运行模块

驱动程序类型,构建和运行模块

device_attach 函数,自动配置和新总线驱动程序

device_detach 函数,自动配置和新总线驱动程序,device_foo 函数

device_foo 函数,自动配置和新总线驱动程序

device_identify 函数,自动配置和新总线驱动程序

device_probe 函数,自动配置和新总线驱动程序

device_resume 函数,自动配置和新总线驱动程序

device_shutdown 函数,自动配置和新总线驱动程序

device_suspend 函数,自动配置和新总线驱动程序

dev_clone 事件处理器,卸载函数,nmdm_modevent 函数

DEV_MODULE 宏,DEV_MODULE 宏

直接内存访问 (DMA)。见 DMA,磁盘结构

方向字段,USB 配置结构

磁盘结构,磁盘结构,磁盘结构,描述字段,描述字段,描述字段,驱动程序私有数据,驱动程序私有数据

描述字段,磁盘结构

驱动程序私有数据,驱动程序私有数据

对其的管理例程,驱动程序私有数据

强制媒体属性,描述字段

可选媒体属性,描述字段

存储设备方法,描述字段

DISKFLAG_CANDELETE 常量,磁盘结构

DISKFLAG_CANFLUSHCACHE 常量,磁盘结构

DISKFLAG_NEEDSGIANT 常量,磁盘结构

使用 DMA 进行拆卸传输,实现 DMA

DMA (直接内存访问),直接内存访问,实现 DMA,实现 DMA,实现 DMA,创建 DMA 标签,创建 DMA 标签,创建 DMA 标签,拆除 DMA 标签,拆除 DMA 标签,bus_dma_segment 结构,bus_dmamap_load 函数,bus_dmamap_load 函数,bus_dmamap_load 函数,bus_dmamap_load 函数,bus_dmamap_load_mbuf_sg 函数,bus_dmamap_load_mbuf_sg 函数,DMA 映射管理例程第二部分,DMA 映射管理例程第二部分,一个简单的示例

缓冲区,拆除 DMA 标签,bus_dma_segment 结构,bus_dmamap_load 函数,bus_dmamap_load 函数,bus_dmamap_load 函数,bus_dmamap_load_mbuf_sg 函数,bus_dmamap_load_mbuf_sg 函数,一个简单的示例

bus_dmamap_load 函数,bus_dma_segment 结构

bus_dmamap_load_mbuf 函数,bus_dmamap_load 函数

bus_dmamap_load_mbuf_sg 函数,bus_dmamap_load 函数

bus_dmamap_load_uio 函数,bus_dmamap_load_mbuf_sg 函数

bus_dmamap_unload 函数,bus_dmamap_load_mbuf_sg 函数

bus_dmamap_segment 结构,拆除 DMA 标签

同步,一个简单的示例

示例使用,DMA 映射管理例程第二部分

映射,拆除 DMA 标签,DMA 映射管理例程第二部分

概述,直接内存访问

标签,创建 DMA 标签,创建 DMA 标签,创建 DMA 标签

创建,创建 DMA 标签

销毁,创建 DMA 标签

使用,实现 DMA,实现 DMA,实现 DMA

拆除,实现 DMA

启动,实现 DMA

dmat 参数,创建 DMA 标签,bus_dmamap_load 函数,一个简单的例子

dontcare_mask 参数,网络接口媒体结构

驱动参数,DRIVER_MODULE 宏

驱动私有数据,驱动私有数据

DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏

arg 参数,DRIVER_MODULE 宏

busname 参数,DRIVER_MODULE 宏

devclass 参数,DRIVER_MODULE 宏

驱动参数,DRIVER_MODULE 宏

evh 参数,DRIVER_MODULE 宏

name 参数,DRIVER_MODULE 宏

ds_addr 字段,bus_dma_segment 结构

导出例程,存储设备方法

动态节点,SYSCTL_CHILDREN 宏

动态 sysctl,实现 sysctl,第一部分

d_close 字段,描述性字段

d_close 函数,更复杂的同步问题

d_drv1 字段,驱动私有数据

d_flags 字段,描述性字段

d_foo 函数,编译和加载,race_modevent 函数,foo_pci_attach 函数

d_fwheads 字段,描述性字段

d_fwsectors 字段,描述性字段

d_ident 字段,描述性字段

d_ioctl 字段,描述性字段

d_ioctl 函数,ioctl,更复杂的同步问题

d_maxsize 字段,描述性字段

d_mediasize 字段,描述性字段

d_open 字段,描述性字段

d_open 函数,更复杂的同步问题

d_sectorsize 字段,描述性字段

d_strategy 字段,描述性字段

d_stripesize 字段,描述性字段

E

ECHO_CLEAR_BUFFER 命令,实现 ioctl

echo_ioctl 函数,echo_ioctl 函数

echo_modevent 函数,echo_read 函数,echo_ioctl 函数

echo_read 函数,echo_write 函数

ECHO_SET_BUFFER_SIZE 命令,实现 ioctl

echo_set_buffer_size 函数,echo_write 函数

echo_write 函数,无害的,实现 ioctl

ECP(扩展能力端口)模式,lpt_write 函数

em(4)驱动程序,网络驱动程序,第二部分:数据包接收和传输

em_handle_rx 函数,em_rxeof 函数

em_rxeof 函数,数据包接收

em_start_locked 函数,数据包传输

em_txeof 函数,em_start_locked 函数

em_xmit 函数,em_start_locked 函数

结束参数,硬件资源管理

数据包结束(eop),em_rxeof 函数

端点,关于 USB 设备

端点字段,USB 配置结构

增强型并行端口(EPP),lpt_write 函数

ENXIO 错误代码,pint_attach 函数

eop(数据包结束),em_rxeof 函数

EPP(增强型并行端口),lpt_write 函数

ep_index 字段,可选字段

ether_ifattach 函数,网络接口结构管理例程

ether_ifdetach 函数,ether_ifattach 函数

事件处理器,卸载函数,任务队列管理例程

EVENTHANDLER_DEREGISTER 宏,内核事件处理器

EVENTHANDLER_INVOKE 宏,内核事件处理器

EVENTHANDLER_PRI_ANY 常量,内核事件处理器

EVENTHANDLER_REGISTER 宏,内核事件处理器

evh 参数,DRIVER_MODULE 宏

排他性持有,不要恐慌

扩展能力端口(ECP)模式,lpt_write 函数

扩展消息信号中断(MSI-X),消息信号中断

扩展模式,lpt_write 函数

ext_buffer 标志,USB 传输标志

F

光纤通道(FC),通用访问方法

过滤参数,注册中断处理器

过滤例程,注册中断处理器

FILTER_HANDLED 常量,FreeBSD 中的中断处理器

FILTER_SCHEDULE_THREAD 常量,FreeBSD 中的中断处理器

FILTER_STRAY 常量,FreeBSD 中的中断处理器

filtfunc 参数,创建 DMA 标签

filtfunc 函数,创建 DMA 标签

filtfuncarg 参数,创建 DMA 标签

FireWire (IEEE 1394),通用访问方法

标志参数,内存管理例程,注册中断处理器,创建 DMA 标签

标志字段,可选字段

闪存驱动程序示例,将一切联系在一起,将一切联系在一起,at45d_attach 函数,at45d_delayed_attach 函数,at45d_get_info 函数,at45d_get_status 函数,at45d_get_status 函数

at45d_attach 函数,将一切联系在一起

at45d_delayed_attach 函数,at45d_attach 函数

at45d_get_info 函数,at45d_delayed_attach 函数

at45d_get_status 函数,at45d_get_info 函数

at45d_strategy 函数,at45d_get_status 函数

at45d_task 函数,at45d_get_status 函数

foo 字节,ulpt_write_callback 函数

foo 锁,防止竞态条件

foo_callback 函数,bus_dmamap_load 函数

foo_pci_attach 函数,foo_pci_probe 函数

foo_pci_detach 函数,foo_pci_attach 函数

foo_pci_probe 函数,foo_pci_probe 函数

force_short_xfer 标志,USB 传输标志

格式参数,创建动态 sysctl

帧字段,可选字段

free 函数,内存管理例程

G

g(组)参数,ioctl

全局任务队列,全局任务队列

H

处理器参数,创建动态 sysctl

使用 Newbus 驱动程序进行硬件资源管理,foo_pci_detach 函数

HBA(主机总线适配器)驱动器,一个(相对)简单的例子,mfip_attach 函数,mfip_detach 函数,mfip_detach 函数,mfip_action 函数,mfip_action 函数,mfip_start 函数

mfip_action 函数,mfip_detach 函数

mfip_attach 函数,一个(相对)简单的例子

mfip_detach 函数,mfip_attach 函数

mfip_done 函数,mfip_start 函数

mfip_poll 函数,mfip_action 函数

mfip_start 函数,mfip_action 函数

Hello, world! KLD,顺序

highaddr 参数,创建 DMA 标签

主机总线适配器(HBA)驱动器。参见 HBA 驱动器,通用访问方法

I

i-Opener LED 驱动器,将一切联系在一起,将一切联系在一起,led_probe 函数,led_probe 函数,led_probe 函数,led_detach 函数,led_close 函数,led_close 函数,led_read 函数

led_attach 函数,led_probe 函数

led_close 函数,led_close 函数

led_detach 函数,led_probe 函数

led_identify 函数,将一切联系在一起

led_open 函数,led_detach 函数

led_probe 函数,将一切联系在一起

led_read 函数,led_close 函数

led_write 函数,led_read 函数

I/O(输入/输出)操作。另见 MMIO;PMIO,ioctl,ioctl,ioctl,实现 ioctl,echo_write 函数,echo_ioctl 函数,echo_ioctl 函数,echo_modevent 函数,调用 ioctl,实现 sysctls,第一部分,实现 sysctls,第一部分,实现 sysctls,第一部分,创建动态 sysctls,创建动态 sysctls,实现 sysctls,第二部分

ioctl 接口,ioctl,ioctl,实现 ioctl,echo_write 函数,echo_ioctl 函数,echo_ioctl 函数,echo_modevent 函数

ioctl 命令,ioctl

echo_ioctl 函数,echo_ioctl 函数

echo_modevent 函数,echo_ioctl 函数

echo_set_buffer_size 函数,echo_write 函数

echo_write 函数,实现 ioctl

调用,echo_modevent 函数

sysctl 接口,调用 ioctl,实现 sysctls,第一部分,实现 sysctls,第一部分,实现 sysctls,第一部分,创建动态 sysctls,创建动态 sysctls,实现 sysctls,第二部分

上下文,实现 sysctls,第一部分

动态 sysctl,实现 sysctls,第一部分

概述,调用 ioctl

SYSCTL_CHILDREN 宏,创建动态 sysctls

sysctl_set_buffer_size 函数,实现 sysctls,第二部分

SYSCTL_STATIC_CHILDREN 宏,创建动态 sysctls

IEEE 1394(火线),通用访问方法

if*结构,你好,世界!

ifaddr_event 事件处理器,内核事件处理器

ifmedia 结构,网络接口媒体结构管理例程

ifmedia_add 函数,网络接口媒体结构管理例程

ifmedia_removeall 函数,网络接口媒体结构管理例程

ifmedia_set 函数,网络接口媒体结构管理例程

IFM_GMASK 掩码,网络接口媒体结构管理例程

IFM_IMASK 掩码,网络接口媒体结构管理例程

IFM_MMASK 掩码,网络接口媒体结构管理例程

IFM_NMASK 掩码,网络接口媒体结构管理例程

IFM_OMASK 掩码,网络接口媒体结构管理例程

IFM_TMASK 掩码,网络接口媒体结构管理例程

ifnet 结构,网络驱动程序,第一部分:数据结构,网络接口结构

ifnet_arrival_event 事件处理器,内核事件处理器

ifnet_departure_event 事件处理器,内核事件处理器

if_clone_event 事件处理器,内核事件处理器

if_index 字段,可选字段

if_init 字段,网络接口结构

if_initname 函数,网络接口结构管理例程

if_input 字段,网络接口结构

if_ioctl 字段,网络接口结构

if_output 字段,网络接口结构

if_qflush 字段,网络接口结构

if_reassign 字段,网络接口结构

if_resolvemulti 字段,网络接口结构

if_start 字段,网络接口结构

if_transmit 字段,网络接口结构

if_watchdog 字段,网络接口结构

实现 MSI,mbuf 结构

初始化例程,网络接口结构

使用 DMA 启动传输,实现 DMA

输入例程,网络接口结构

输入/输出(I/O)操作。参见 I/O 操作,网络接口结构

英特尔 PCI 千兆以太网适配器驱动程序,网络接口媒体结构管理例程

智能平台管理接口(IPMI)驱动程序。参见 IPMI 驱动程序,网络接口媒体结构管理例程

接口,字符驱动程序,更多关于 USB 设备,网络驱动程序,第一部分:数据结构

中断端点,关于 USB 设备

中断处理程序,中断处理,中断处理,注册中断处理程序,实现中断处理程序,实现中断处理程序,pint_probe 函数,pint_probe 函数,pint_probe 函数,pint_attach 函数,pint_attach 函数,pint_open 函数,pint_close 函数,pint_close 函数,pint_read 函数,pint_intr 函数

示例,实现中断处理程序,实现中断处理程序,pint_probe 函数,pint_probe 函数,pint_attach 函数,pint_attach 函数,pint_open 函数,pint_close 函数,pint_close 函数,pint_read 函数

pint_attach 函数,pint_probe 函数

pint_close 函数,pint_open 函数

pint_detach 函数,pint_attach 函数

pint_identify 函数,实现中断处理程序

pint_intr 函数,pint_read 函数

pint_open 函数,pint_attach 函数

pint_probe 函数,实现中断处理程序

pint_read 函数,pint_close 函数

pint_write 函数,pint_close 函数

在并行端口上,pint_intr 函数

概述,中断处理,注册中断处理程序

注册,中断处理

中断,定义,中断处理

中断请求线 (IRQs),硬件资源管理

间隔字段,可选字段

INTR_ENTROPY 常量,注册中断处理程序

INTR_MPSAFE 常量,注册中断处理程序

INVARIANTS 选项,内存管理例程

ioctl 命令,ioctl

ioctl 接口,ioctl,ioctl,实现 ioctl,echo_write 函数,echo_ioctl 函数,echo_ioctl 函数,echo_modevent 函数

相关命令,ioctl

echo_ioctl 函数,echo_ioctl 函数

echo_modevent 函数,echo_ioctl 函数

echo_set_buffer_size 函数,echo_write 函数

echo_write 函数,实现 ioctl

调用,echo_modevent 函数

IPMI(智能平台管理接口)驱动程序,代码分析,ipmi_pci_probe 函数,ipmi_pci_attach 函数,ipmi_pci_attach 函数,ipmi_pci_attach 函数,ipmi_pci_attach 函数

ipmi2_pci_attach 函数,ipmi_pci_attach 函数

ipmi2_pci_probe 函数,ipmi_pci_attach 函数

ipmi_pci_attach 函数,ipmi_pci_attach 函数

ipmi_pci_match 函数,ipmi_pci_probe 函数

ipmi_pci_probe 函数,代码分析

ipmi2_pci_attach 函数,ipmi_pci_attach 函数

ipmi2_pci_probe 函数,ipmi_pci_attach 函数

ipmi_attached 变量,ipmi_pci_probe 函数

ipmi_identifiers 数组,ipmi_pci_probe 函数

ipmi_pci_attach 函数,ipmi_pci_attach 函数

ipmi_pci_match 函数,ipmi_pci_probe 函数

ipmi_pci_probe 函数,代码分析

IRQs(中断请求线),foo_pci_detach 函数

同步端点,关于 USB 设备

ithread 参数,注册中断处理程序

ithread 例程,FreeBSD 中的中断处理程序

K

键盘控制器样式(KCS)模式,ipmi_pci_attach 函数

KLDs(可加载内核模块),设备驱动程序类型,name,name,name,name,name,order,编译和加载,编译和加载,d_foo 函数,字符设备切换表,字符设备切换表,字符设备切换表,无害的,echo_write 函数,echo_read 函数,DEV_MODULE 宏,DEV_MODULE 宏,DEV_MODULE 宏,DEV_MODULE 宏

块设备驱动程序,DEV_MODULE 宏

字符设备驱动程序,编译和加载,d_foo 函数,字符设备切换表,字符设备切换表,字符设备切换表,无害的,echo_write 函数,echo_read 函数,DEV_MODULE 宏,DEV_MODULE 宏

字符设备切换表,字符设备切换表

destroy_dev 函数,字符设备切换表

DEV_MODULE 宏,DEV_MODULE 宏

d_foo 函数,编译和加载

echo_modevent 函数,echo_read 函数

echo_read 函数,echo_write 函数

echo_write 函数,无害的

加载,DEV_MODULE 宏

make_dev 函数,字符设备切换表

编译和加载,编译和加载

DECLARE_MODULE 宏,name,name,name,name,name

数据参数,name

name 参数,name

order 参数,name

sub 参数,name

Hello, world! 示例,order

模块事件处理程序,设备驱动程序类型

kldunload -f 命令,race_modevent 函数

L

LED 驱动程序,将一切联系起来,将一切联系起来,led_probe 函数,led_probe 函数,led_probe 函数,led_detach 函数,led_close 函数,led_close 函数,led_read 函数

led_attach 函数,led_probe 函数

led_close 函数,led_close 函数

led_detach 函数,led_probe 函数

led_identify 函数,将一切联系起来

led_open 函数,led_detach 函数

led_probe 函数,将一切联系起来

led_read 函数,led_close 函数

led_write 函数,led_read 函数

len 参数,创建动态 sysctls

load 函数,sleep_modevent 函数

可加载内核模块 (KLDs)。见 KLDs,sleep_modevent 函数

加载,编译和加载,DEV_MODULE 宏,DEV_MODULE 宏

字符设备驱动程序,DEV_MODULE 宏

KLDs,编译和加载

lockfunc 参数,创建 DMA 标签

lockfuncarg 参数,创建 DMA 标签

锁,防止竞态条件

longdesc 参数,内存管理例程

lowaddr 参数,创建 DMA 标签

lptcontrol(8) 工具,lpt_ioctl 函数

lpt_attach 函数,lpt_detect 函数

lpt_close 函数,lpt_push_bytes 函数

lpt_detach 函数,lpt_attach 函数

lpt_detect 函数,lpt_detect 函数

lpt_identify 函数,代码分析

lpt_intr 函数,lpt_write 函数

lpt_ioctl 函数,lpt_close 函数

lpt_open 函数,lpt_open 函数

lpt_port_test 函数,lpt_detect 函数,lpt_detect 函数

lpt_probe 函数,代码分析

lpt_push_bytes 函数,lpt_timeout 函数

lpt_read 函数,lpt_open 函数

lpt_release_ppbus 函数,lpt_ioctl 函数

lpt_request_ppbus 函数,lpt_ioctl 函数

lpt_timeout 函数,lpt_timeout 函数

lpt_write 函数,lpt_read 函数

LP_BUSY 标志,lpt_intr 函数

LP_BYPASS 标志,lpt_write 函数

M

Makefiles,编译和加载

make_dev 函数,字符设备切换表

malloc 函数,内存管理例程

MALLOC_DECLARE 宏,MALLOC_DECLARE 宏

MALLOC_DEFINE 宏,内存管理例程

malloc_type 结构,内存管理例程,MALLOC_DECLARE 宏,MALLOC_DECLARE 宏

MALLOC_DECLARE 宏,MALLOC_DECLARE 宏

MALLOC_DEFINE 宏,内存管理例程

管理例程,自旋互斥锁,不要慌张,实现共享/独占锁,条件变量,条件变量,任务队列,拆除 DMA 标签,bus_dmamap_load_mbuf_sg 函数,驱动程序私有数据,网络接口结构,网络接口媒体结构,MSI 管理例程

对于条件变量,条件变量

对于磁盘结构,驱动程序私有数据

对于 DMA 映射,拆除 DMA 标签,bus_dmamap_load_mbuf_sg 函数

对于 MSI(消息信号中断),MSI 管理例程

对于互斥锁,自旋互斥锁

对于网络接口媒体结构,网络接口媒体结构

对于网络接口结构,网络接口结构

对于 rw(读取/写入)锁,实现共享/独占锁

对于 sx(共享/独占)锁,不要慌张

对于任务队列,任务队列

USB 驱动程序的强制字段,USB 配置结构

磁盘结构的强制媒体属性,描述字段

manual_status 标志,USB 传输标志

映射,DMA,拆除 DMA 标签,bus_dmamap_load_mbuf_sg 函数

掩码,用于忽略位,网络接口媒体结构管理例程

maxsegsz 参数,创建 DMA 标签

maxsize 参数,创建 DMA 标签

max_dev_transactions 参数,SIM 注册例程,cam_sim_alloc 函数

MAX_EVENT 常量,实现睡眠和条件变量

max_tagged_dev_transactions 参数,cam_sim_alloc 函数

mbuf 参数,bus_dmamap_load 函数

mbuf 链,mbuf 结构

mbuf 结构,Hello, world!

磁盘结构的媒体属性,描述字段,描述字段,描述字段

强制性的,描述字段

可选的,描述字段

内存分配,分配内存,内存管理例程,MALLOC_DECLARE 宏,MALLOC_DECLARE 宏,将一切联系起来,连续物理内存管理例程

连续物理内存,连续物理内存管理例程

malloc_type 结构,内存管理例程,MALLOC_DECLARE 宏,MALLOC_DECLARE 宏

MALLOC_DECLARE 宏,MALLOC_DECLARE 宏

MALLOC_DEFINE 宏,内存管理例程

概述,分配内存

内存屏障,内存屏障

内存映射 I/O (MMIO)。见 MMIO,mbuf 结构

消息信号中断 (MSI),mbuf 结构,消息信号中断,MSI 管理例程

实现,mbuf 结构

管理例程,MSI 管理例程

USB 驱动程序的 methods 结构,USB 配置结构管理例程

mfi(4)代码库,mfip_done 函数

mfip_action 函数,mfip_detach 函数

mfip_attach 函数,一个(相当)简单的例子

mfip_detach 函数,mfip_attach 函数

mfip_done 函数,mfip_start 函数

mfip_poll 函数,mfip_action 函数

mfip_start 函数,mfip_action 函数

mfi_intr 函数,mfip_start 函数

mfi_startio 函数,mfip_start 函数,XPT_SCSI_IO

MMIO(内存映射 I/O)。另见 I/O 操作;PMIO,I/O 端口和 I/O 内存,从 I/O 端口和 I/O 内存读取,向 I/O 端口和 I/O 内存写入,内存屏障,内存屏障

和内存屏障,内存屏障

从中读取,I/O 端口和 I/O 内存

流操作,向 I/O 端口和 I/O 内存写入

向中写入,从 I/O 端口和 I/O 内存读取

调制解调器驱动程序。另见虚拟空调制解调器,模块事件处理器

modeventtype_t 参数,模块事件处理器

模块事件处理器,设备驱动程序类型

MOD_QUIESCE 常量,race_modevent 函数

MSI(消息信号中断),mbuf 结构,mbuf 结构,MSI 管理例程

实现,mbuf 结构

管理例程,MSI 管理例程

MSI 消息,消息信号中断

MSI-X(扩展消息信号中断),mbuf 结构

MSI-X 消息,消息信号中断

msleep_spin 函数,自愿上下文切换或睡眠

MTX_DEF 常量,互斥锁管理例程

mtx_destroy 函数,互斥锁管理例程

MTX_DUPOK 常量,互斥锁管理例程

mtx_init 函数,互斥锁管理例程

MTX_NOPROFILE 常量,互斥锁管理例程

MTX_NOWITNESS 常量,互斥锁管理例程

MTX_QUIET 常量,互斥锁管理例程

MTX_RECURSE 常量,互斥锁管理例程

MTX_SPIN 常量,互斥锁管理例程

mtx_trylock 函数,互斥锁管理例程

mtx_unlock_spin 函数,互斥锁管理例程

互斥锁,互斥锁,自旋互斥锁,自旋互斥锁,睡眠互斥锁,实现互斥锁

针对自旋互斥锁的管理例程

race_modevent 函数,实现互斥锁

睡眠互斥锁,睡眠互斥锁

自旋互斥锁,互斥锁

mword 值,网络接口媒体结构管理例程

M_ECHO 结构,将一切联系在一起

M_NOWAIT 常量,内存管理例程,连续物理内存管理例程

M_WAITOK 常量,内存管理例程,连续物理内存管理例程

M_ZERO 常量,内存管理例程,连续物理内存管理例程

N

n 参数,定义 ioctl 命令

名称参数,name,name,创建动态 sysctls,DRIVER_MODULE 宏

创建动态 sysctls 的描述

对于 DECLARE_MODULE 宏,name

对于 DRIVER_MODULE 宏,DRIVER_MODULE 宏

网络设备,设备驱动程序类型

网络驱动程序,网络接口结构,网络接口结构管理例程,网络接口结构管理例程,ether_ifattach 函数,网络接口媒体结构,网络接口媒体结构管理例程,网络接口媒体结构管理例程,Hello, world!,mbuf 结构,mbuf 结构,MSI 管理例程,网络驱动程序第二部分:数据包接收和传输,数据包传输,em_txeof 函数,em_txeof 函数

网络接口媒体结构管理例程的示例

mbuf 结构,Hello, world!

MSI(消息信号中断),mbuf 结构,mbuf 结构,MSI 管理例程

实现,mbuf 结构

针对 MSI 管理例程的管理例程

网络接口媒体结构,网络接口媒体结构

网络接口结构,网络接口结构,网络接口结构管理流程,网络接口结构管理流程,ether_ifattach 函数

ether_ifattach 函数,网络接口结构管理流程

ether_ifdetach 函数,ether_ifattach 函数

管理流程,网络接口结构

数据包,网络驱动程序,第二部分:数据包接收和传输,数据包传输,em_txeof 函数,em_txeof 函数

传输后,em_txeof 函数

接收,网络驱动程序,第二部分:数据包接收和传输

传输,数据包传输

网络接口媒体结构,网络接口媒体结构

网络接口结构,网络接口结构,网络接口结构管理流程,网络接口结构管理流程,ether_ifattach 函数

ether_ifattach 函数,网络接口结构管理流程

ether_ifdetach 函数,ether_ifattach 函数

管理流程,网络接口结构

Newbus 驱动程序,Newbus 和资源分配,自动配置和新 bus 驱动程序,自动配置和新 bus 驱动程序,设备方法表,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,foo_pci_probe 函数,foo_pci_probe 函数,foo_pci_attach 函数,foo_pci_attach 函数,foo_pci_attach 函数,foo_pci_detach 函数,foo_pci_detach 函数

设备方法表,设备方法表

device_foo 函数,自动配置和新 bus 驱动程序

DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏,DRIVER_MODULE 宏

arg 参数,DRIVER_MODULE 宏

busname 参数,DRIVER_MODULE 宏

devclass 参数,DRIVER_MODULE 宏

驱动程序参数,DRIVER_MODULE 宏

evh 参数,DRIVER_MODULE 宏

name 参数,DRIVER_MODULE 宏

示例,foo_pci_probe 函数,foo_pci_probe 函数,foo_pci_attach 函数,foo_pci_attach 函数,foo_pci_attach 函数,foo_pci_detach 函数

d_foo 函数,foo_pci_attach 函数

foo_pci_attach 函数,foo_pci_probe 函数

foo_pci_detach 函数,foo_pci_attach 函数

foo_pci_probe 函数,foo_pci_probe 函数

加载,foo_pci_detach 函数

与硬件资源管理,foo_pci_detach 函数

概述,Newbus 和资源分配

nibble 模式,lpt_read 函数

nmdm(4) 驱动程序,案例研究:虚拟空调制解调器,代码分析

nmdm_alloc 函数,nmdm_alloc 函数

nmdm_clone 函数,nmdm_clone 函数

nmdm_count 变量,nmdm_modevent 函数

nmdm_inwakeup 函数,nmdm_inwakeup 函数

nmdm_modem 函数,nmdm_inwakeup 函数

nmdm_modevent 函数,nmdm_modevent 函数

nmdm_outwakeup 函数,nmdm_alloc 函数

nmdm_param 函数,nmdm_modem 函数

nmdm_task_tty 函数,nmdm_alloc 函数

nmdm_timeout 函数,nmdm_param 函数,nmdm_timeout 函数

no_pipe_ok 标志,USB 传输标志

np_rate 变量,nmdm_param 函数

nsegments 参数,创建 DMA 标签

ns_part 变量,nmdm_alloc 函数

数字参数,创建动态 sysctl

O

USB 驱动器的可选字段,USB 配置结构

磁盘结构的可选媒体属性,描述字段

排序参数,名称

输出例程,网络接口结构

P

数据包,数据包接收,em_rxeof 函数,em_rxeof 函数,数据包传输,数据包传输,em_start_locked 函数,em_txeof 函数,em_txeof 函数

发送后,em_txeof 函数

接收中,数据包接收,em_rxeof 函数,em_rxeof 函数

em_handle_rx 函数,em_rxeof 函数

em_rxeof 函数,数据包接收

发送中,数据包传输,数据包传输,em_start_locked 函数

em_start_locked 函数,数据包传输

em_txeof 函数,em_start_locked 函数

并行端口,pint_intr 函数,pint_intr 函数,代码分析,代码分析,lpt_detect 函数,lpt_detect 函数,lpt_detect 函数,lpt_detect 函数,lpt_attach 函数,lpt_open 函数,lpt_open 函数,lpt_read 函数,lpt_write 函数,lpt_timeout 函数,lpt_timeout 函数,lpt_push_bytes 函数,lpt_close 函数,lpt_ioctl 函数,lpt_ioctl 函数

中断处理程序开启,pint_intr 函数

打印机驱动示例,代码分析,代码分析,lpt_detect 函数,lpt_detect 函数,lpt_detect 函数,lpt_detect 函数,lpt_attach 函数,lpt_open 函数,lpt_open 函数,lpt_read 函数,lpt_write 函数,lpt_timeout 函数,lpt_timeout 函数,lpt_push_bytes 函数,lpt_close 函数,lpt_ioctl 函数,lpt_ioctl 函数

lpt_attach 函数,lpt_detect 函数

lpt_close 函数,lpt_push_bytes 函数

lpt_detach 函数,lpt_attach 函数

lpt_detect 函数,lpt_detect 函数

lpt_identify 函数,代码分析

lpt_intr 函数,lpt_write 函数

lpt_ioctl 函数,lpt_close 函数

lpt_open 函数,lpt_open 函数

lpt_port_test 函数,lpt_detect 函数

lpt_probe 函数,代码分析

lpt_push_bytes 函数,lpt_timeout 函数

lpt_read 函数,lpt_open 函数

lpt_release_ppbus 函数,lpt_ioctl 函数

lpt_request_ppbus 函数,lpt_ioctl 函数

lpt_timeout 函数,lpt_timeout 函数

lpt_write 函数,lpt_read 函数

父参数,创建动态 sysctl,创建 DMA 标签

pause 函数,自愿上下文切换或睡眠

PCIR_BAR(x) 宏,ipmi_pci_attach 函数

pci_alloc_msi 函数,MSI 管理例程

pci_alloc_msix 函数,MSI 管理例程

pci_msix_count 函数,MSI 管理例程

pci_msi_count 函数,MSI 管理例程

pci_release_msi 函数,MSI 管理例程

物理内存,连续的,将一切联系在一起

pint_attach 函数,pint_probe 函数

pint_close 函数,pint_open 函数

pint_detach 函数,pint_attach 函数

pint_identify 函数,实现中断处理程序

pint_intr 函数,pint_read 函数

pint_open 函数,pint_attach 函数

pint_probe 函数,实现中断处理程序

pint_read 函数,pint_close 函数

pint_write 函数,pint_close 函数

pipe,定义,USB 驱动程序

pipe_bof 标志,USB 传输标志

PMIO(端口映射 I/O)。另见 I/O 操作;MMIO,I/O 端口和 I/O 内存,从 I/O 端口和 I/O 内存读取,向 I/O 端口和 I/O 内存写入,内存屏障,将一切联系起来,将一切联系起来,led_probe 函数,led_probe 函数,led_probe 函数,led_probe 函数,led_detach 函数,led_close 函数,led_close 函数,led_read 函数

和内存屏障,内存屏障

i-Opener LED 驱动示例,将一切联系起来,将一切联系起来,led_probe 函数,led_probe 函数,led_probe 函数,led_detach 函数,led_close 函数,led_close 函数,led_read 函数

led_attach 函数,led_probe 函数

led_close 函数,led_close 函数

led_detach 函数,led_probe 函数

led_identify 函数,将一切联系起来

led_open 函数,led_detach 函数

led_probe 函数,将一切联系起来

led_read 函数,led_close 函数

led_write 函数,led_read 函数

从中读取,I/O 端口和 I/O 内存

流操作,向 I/O 端口和 I/O 内存写入

向中写入,从 I/O 端口和 I/O 内存读取

poll 例程,mfip_poll 函数

端口映射 I/O(PMIO)。另见 PMIO,内核事件处理程序

power_profile_change 事件处理器,内核事件处理器

ppb_release_bus 函数,pint_close 函数

ppb_sleep 函数,pint_read 函数

打印机驱动程序,整合一切,ulpt_attach 函数,ulpt_attach 函数,ulpt_open 函数,unlpt_open 函数,unlpt_open 函数,unlpt_open 函数,unlpt_open 函数,ulpt_watchdog 函数,ulpt_watchdog 函数,ulpt_stop_read 函数,ulpt_stop_read 函数,ulpt_stop_read 函数,ulpt_write_callback 函数,ulpt_write_callback 函数,ulpt_read_callback 函数

ulpt_close 函数,unlpt_open 函数

ulpt_detach 函数,ulpt_attach 函数

ulpt_ioctl 函数,unlpt_open 函数

ulpt_open 函数,ulpt_attach 函数

ulpt_probe 函数,整合一切

ulpt_read_callback 函数,ulpt_write_callback 函数

ulpt_reset 函数,ulpt_open 函数

ulpt_start_read 函数,ulpt_watchdog 函数

ulpt_start_write 函数,ulpt_stop_read 函数

ulpt_status_callback 函数,ulpt_read_callback 函数

ulpt_stop_read 函数,ulpt_stop_read 函数

ulpt_stop_write 函数,ulpt_stop_read 函数

ulpt_watchdog 函数,ulpt_watchdog 函数

ulpt_write_callback 函数,ulpt_write_callback 函数

unlpt_open 函数,unlpt_open 函数

优先级参数,自愿上下文切换或睡眠

process_exec 事件处理器,内核事件处理器

process_exit 事件处理器,内核事件处理器

process_fork 事件处理器,内核事件处理器

proxy_buffer 标志,USB 传输标志

伪设备,设备驱动程序类型

模拟代码,实现 DMA

Q

qflush 例程,网络接口结构

R

r 参数,注册中断处理程序

竞态条件,问题的根源

race_destroy 函数,race_find 函数

race_find 函数,一个更复杂的同步问题

race_ioctl 函数,race_find 函数,实现互斥锁

race_ioctl.h 头文件,一个更复杂的同步问题

race_ioctl_mtx 函数,实现互斥锁

RACE_IOC_ATTACH 操作,race_ioctl 函数

RACE_IOC_DETACH 操作,race_ioctl 函数

RACE_IOC_LIST 操作,race_ioctl 函数

RACE_IOC_QUERY 操作,race_ioctl 函数

race_modevent 函数,race_ioctl 函数,实现互斥锁

race_new 函数,一个更复杂的同步问题

race_softc 结构体,一个更复杂的同步问题,一个更复杂的同步问题,race_find 函数,问题的根源

读操作,定义 ioctl 命令

读写(rw)锁,实现共享/独占锁

读者,定义,实现共享/独占锁

读取,I/O 端口和 I/O 内存,I/O 端口和 I/O 内存,I/O 端口和 I/O 内存

来自 MMIO(内存映射 I/O),I/O 端口和 I/O 内存

来自 PMIO(端口映射 I/O),I/O 端口和 I/O 内存

realloc 函数,内存管理例程

reallocf 函数,内存管理例程

reassign 例程,网络接口结构

接收数据包,网络驱动程序,第二部分:数据包接收和传输,数据包接收,em_rxeof 函数,em_rxeof 函数

em_handle_rx 函数,em_rxeof 函数

em_rxeof 函数,数据包接收

概述,网络驱动程序,第二部分:数据包接收和传输

在独占锁上递归,避免,条件变量管理例程

注册中断处理程序,中断处理

resolvemulti 例程,网络接口结构

RFSTOPPED 常量,load 函数

RF_ACTIVE 常量,硬件资源管理

RF_ALLOCATED 常量,硬件资源管理

RF_SHAREABLE 常量,硬件资源管理

RF_TIMESHARE 常量,硬件资源管理

rid 参数,硬件资源管理

rw(读者/写者)锁,实现共享/独占锁

rw_destroy 函数,读者/写者锁管理例程

rw_init 函数,读者/写者锁管理例程

rw_init_flags 函数,读者/写者锁管理例程

rw_runlock 函数,读者/写者锁管理例程

rw_try_rlock 函数,读者/写者锁管理例程

rw_try_wlock 函数,读者/写者锁管理例程

rw_wunlock 函数,读者/写者锁管理例程

S

sc->sc_state 值,pint_open 函数,lpt_open 函数

散列/聚集段,创建 DMA 标签

SCSI 并行接口 (SPI),通用访问方法

sc_open_mask 值,led_detach 函数

sc_open_mask 变量,led_probe 函数

sc_read_mask 变量,led_probe 函数

服务器管理接口芯片 (SMIC) 模式,ipmi_pci_attach 函数

共享持有,不要恐慌

共享/独占 (sx) 锁。参见 sx 锁,不要恐慌

shortdesc 参数,内存管理例程

short_frames_ok 标志,USB 传输标志

short_xfer_ok 标志,USB 传输标志

shutdown_final 事件处理器,unload 函数,内核事件处理器

shutdown_post_sync 事件处理器,内核事件处理器

shutdown_pre_sync 事件处理器,内核事件处理器

sigoff 参数,nmdm_modem 函数

sigon 参数,nmdm_modem 函数

SIM 队列,mfip_attach 函数

CAM(通用访问方法)的 SIM 注册例程,SIM 注册例程,SIM 注册例程,SIM 注册例程,cam_sim_alloc 函数

cam_simq_alloc 函数,SIM 注册例程

cam_sim_alloc 函数,SIM 注册例程

xpt_bus_register 函数,cam_sim_alloc 函数

SIMs(软件接口模块),通用访问方法

大小参数,内存管理例程

睡眠互斥锁,睡眠互斥锁

睡眠状态,睡眠互斥锁,自愿上下文切换或睡眠,任务队列管理例程

sleep_modevent 函数,实现睡眠和条件变量

sleep_thread 函数,load 函数

SMBIOS(系统管理 BIOS),ipmi_pci_attach 函数

SMIC(服务器管理接口芯片)模式,ipmi_pci_attach 函数

软件接口模块(SIMs),通用访问方法

SPI(SCSI 并行接口),通用访问方法

spin 互斥锁,互斥锁

spin,定义,问题的根源

spi_command 结构,at45d_get_info 函数,at45d_task 函数

stall_pipe 标志,USB 传输标志

开始参数,硬件资源管理

启动例程,网络接口结构

静态节点,SYSCTL_STATIC_CHILDREN 宏

status_callback 参数,网络接口媒体结构管理例程

磁盘结构的存储设备方法,描述字段

存储驱动程序,磁盘结构,磁盘结构,描述字段,描述字段,描述字段,驱动程序私有数据,驱动程序私有数据,驱动程序私有数据,块 I/O 队列,块 I/O 队列,整合一切,整合一切,at45d_attach 函数,at45d_delayed_attach 函数,at45d_get_info 函数,at45d_get_status 函数,at45d_get_status 函数

块 I/O 队列,块 I/O 队列

块 I/O 结构,驱动程序私有数据

磁盘结构,磁盘结构,磁盘结构,描述字段,描述字段,描述字段,司机私有数据,司机私有数据

描述字段,磁盘结构

驱动程序私有数据,司机私有数据

针对司机私有数据的管理流程

强制媒体属性,描述字段

可选的媒体属性,描述字段

存储设备方法,描述字段

闪存驱动程序示例,将一切联系在一起,将一切联系在一起,at45d_attach 函数,at45d_delayed_attach 函数,at45d_get_info 函数,at45d_get_status 函数,at45d_get_status 函数

at45d_attach函数,将一切联系在一起

at45d_delayed_attach函数,at45d_attach 函数

at45d_get_info函数,at45d_delayed_attach 函数

at45d_get_status函数,at45d_get_info 函数

at45d_strategy函数,at45d_get_status 函数

at45d_task函数,at45d_get_status 函数

策略流程,存储设备方法

流操作,写入 I/O 端口和 I/O 内存

struct usb_xfer *参数,USB 传输标志

子参数,名称

sx(共享/独占)锁,不要慌张,共享/独占锁管理流程,条件变量管理流程,避免长时间持有独占锁,避免长时间持有独占锁

避免长时间持有独占锁,避免长时间持有独占锁

避免在独占锁上递归,条件变量管理流程

示例,共享/独占锁管理流程

针对不要慌张的管理流程

sx_destroy函数,共享/独占锁管理流程

SX_DUPOK常量,共享/独占锁管理流程

sx_init 函数,共享/独占锁管理例程

sx_init_flags 函数,共享/独占锁管理例程

SX_NOADAPTIVE 常量,共享/独占锁管理例程

SX_NOPROFILE 常量,共享/独占锁管理例程

SX_NOWITNESS 常量,共享/独占锁管理例程

SX_QUIET 常量,共享/独占锁管理例程

SX_RECURSE 常量,共享/独占锁管理例程

sx_slock_sig 函数,共享/独占锁管理例程

sx_unlock 函数,共享/独占锁管理例程

sx_xlock_sig 函数,共享/独占锁管理例程

sx_xunlock 函数,共享/独占锁管理例程

同步原语,防止竞态条件

同步 DMA 缓冲区,一个简单的例子

sysctl 上下文,实现 sysctl,第一部分

sysctl 接口,调用 ioctl,实现 sysctl,第一部分,实现 sysctl,第一部分,实现 sysctl,第一部分,创建动态 sysctl,创建动态 sysctl,实现 sysctl,第二部分

上下文,实现 sysctl,第一部分

动态 sysctl,实现 sysctl,第一部分

概述,调用 ioctl

SYSCTL_CHILDREN 宏,创建动态 sysctl

sysctl_set_buffer_size 函数,实现 sysctl,第二部分

SYSCTL_STATIC_CHILDREN 宏,创建动态 sysctl

SYSCTL_ADD_* 宏,实现 sysctl,第一部分,创建动态 sysctl

SYSCTL_ADD_INT 宏,实现 sysctl,第一部分

SYSCTL_ADD_LONG 宏,实现 sysctl,第一部分

SYSCTL_ADD_NODE 宏,实现 sysctl,第一部分,实现 sysctl,第一部分,创建动态 sysctl

SYSCTL_ADD_OID 宏,创建动态 sysctl

SYSCTL_ADD_PROC 宏,实现 sysctl,第一部分

SYSCTL_ADD_STRING 宏,实现 sysctl,第一部分

SYSCTL_CHILDREN 宏,创建动态 sysctl

sysctl_ctx_init 函数,实现 sysctl,第一部分

sysctl_debug_sleep_test 函数,load 函数,sleep_thread 函数

SYSCTL_HANDLER_ARGS 常量,sysctl_set_buffer_size 函数

sysctl_set_buffer_size 函数,实现 sysctl,第二部分

SYSCTL_STATIC_CHILDREN 宏,创建动态 sysctl

sysinit_elem_order 枚举,name

系统管理 BIOS (SMBIOS),ipmi_pci_attach 函数

SYS_RES_IOPORT 常量,硬件资源管理

SYS_RES_IRQ 常量,硬件资源管理

SYS_RES_MEMORY 常量,硬件资源管理

T

t 参数,定义 ioctl 命令

DMA 标签,创建 DMA 标签,创建 DMA 标签,创建 DMA 标签

创建,创建 DMA 标签

销毁,创建 DMA 标签

任务队列,调用,任务队列,任务队列,全局任务队列

全局,全局任务队列

管理例程,任务队列

概述,调用

taskqueue_drain 函数,任务队列管理例程

taskqueue_enqueue 函数,任务队列管理例程

taskqueue_run 函数,任务队列管理例程

任务,任务队列

TASK_INIT 宏,任务队列管理例程

TF_NOPREFIX 标志,nmdm_alloc 函数

线程同步,一个简单的同步问题,一个更复杂的同步问题,一个更复杂的同步问题,race_find 函数,race_find 函数,race_ioctl 函数,race_modevent 函数,race_modevent 函数,race_modevent 函数,防止竞态条件,互斥锁,自旋互斥锁,自旋互斥锁,睡眠互斥锁,实现互斥锁,不要慌张,共享/独占锁管理例程,实现共享/独占锁,条件变量管理例程,避免长时间持有独占锁,避免长时间持有独占锁

示例,一个更复杂的同步问题,一个更复杂的同步问题,race_find 函数,race_find 函数,race_ioctl 函数,race_modevent 函数,race_modevent 函数

问题,race_modevent 函数

race_destroy 函数,race_find 函数

race_find 函数,一个更复杂的同步问题

race_ioctl 函数,race_find 函数

race_modevent 函数,race_ioctl 函数

race_new 函数,一个更复杂的同步问题

锁,防止竞态条件

互斥锁,互斥锁,自旋互斥锁,自旋互斥锁,睡眠互斥锁,实现互斥锁

管理例程,自旋互斥锁

race_modevent 函数,实现互斥锁

睡眠互斥锁,睡眠互斥锁

自旋互斥锁,互斥锁

原因,一个简单的同步问题

rw(读取/写入)锁,实现共享/独占锁

sx(共享/独占)锁,不要慌张,共享/独占锁管理例程,条件变量管理例程,避免长时间持有独占锁,避免长时间持有独占锁

避免长时间持有独占锁,避免长时间持有独占锁

避免在独占锁上递归,条件变量管理例程

示例,共享/独占锁管理例程

管理例程,不要慌张

线程,通过它进行上下文切换,自愿上下文切换或睡眠

超时字段,USB 配置结构

timo 参数,自愿上下文切换或睡眠

USB 驱动程序的传输标志,可选字段

使用 DMA 的传输,实现 DMA

发送例程,网络接口结构

发送数据包,数据包传输,数据包传输,em_start_locked 函数,em_txeof 函数

em_start_locked 函数,数据包传输

em_txeof 函数,em_start_locked 函数

发送后,em_txeof 函数

tsleep 函数,自愿上下文切换或睡眠

TTY 设备,先决条件

tty_alloc_mutex 函数,先决条件

tty_makedev 函数,先决条件

tty_softc 函数,先决条件

tx_buffer 变量,em_txeof 函数

tx_desc 变量,em_txeof 函数

类型字段,USB 配置结构

U

UE_BULK 端点类型,可选字段

UE_CONTROL 端点类型,可选字段

UE_DIR_ANY 常量,USB 配置结构

UE_DIR_IN 常量,USB 配置结构

UE_DIR_OUT 常量,USB 配置结构

UE_INTERRUPT 端点类型,可选字段

UE_ISOCHRONOUS 端点类型,可选字段

ulpt_close 函数,unlpt_open 函数

ulpt_detach 函数,ulpt_attach 函数

ulpt_ioctl 函数,unlpt_open 函数

ulpt_open 函数,ulpt_attach 函数

ulpt_probe 函数,整合一切

ulpt_read_callback 函数,ulpt_write_callback 函数

ulpt_reset 函数,ulpt_open 函数

ulpt_start_read 函数,ulpt_watchdog 函数

ulpt_start_write 函数,ulpt_stop_read 函数

ulpt_status_callback 函数,ulpt_read_callback 函数

ulpt_stop_read 函数,ulpt_stop_read 函数

ulpt_stop_write 函数,ulpt_stop_read 函数

ulpt_watchdog 函数,ulpt_watchdog 函数

ulpt_write_callback 函数,ulpt_write_callback 函数

UMASS (USB 大容量存储),通用访问方法

通用串行总线 (USB) 驱动程序。参见 USB 驱动程序,sleep_modevent 函数,sleep_thread 函数

卸载函数,sleep_modevent 函数,sleep_thread 函数

unlpt_open 函数,unlpt_open 函数

USB (通用串行总线) 驱动程序,USB 驱动程序,USB 配置结构,USB 配置结构,可选字段,USB 传输标志,USB 传输(在 FreeBSD 中),USB 传输(在 FreeBSD 中),USB 传输(在 FreeBSD 中),USB 配置结构管理例程,整合一切,ulpt_attach 函数,ulpt_attach 函数,ulpt_open 函数,unlpt_open 函数,unlpt_open 函数,unlpt_open 函数,unlpt_open 函数,ulpt_watchdog 函数,ulpt_watchdog 函数,ulpt_stop_read 函数,ulpt_stop_read 函数,ulpt_stop_read 函数,ulpt_write_callback 函数,ulpt_write_callback 函数,ulpt_read_callback 函数

配置结构,USB 配置结构,USB 配置结构,可选字段,USB 传输(在 FreeBSD 中),USB 传输(在 FreeBSD 中)

管理例程,USB 传输(在 FreeBSD 中)

必需字段,USB 配置结构

可选字段,USB 配置结构

传输标志,可选字段

数据传输,USB 传输标志

方法结构,USB 配置结构管理例程

概述,USB 驱动程序

打印机驱动程序示例,将一切整合在一起,ulpt_attach 函数,ulpt_attach 函数,ulpt_open 函数,unlpt_open 函数,unlpt_open 函数,unlpt_open 函数,unlpt_open 函数,ulpt_watchdog 函数,ulpt_watchdog 函数,ulpt_stop_read 函数,ulpt_stop_read 函数,ulpt_stop_read 函数,ulpt_write_callback 函数,ulpt_write_callback 函数,ulpt_read_callback 函数

ulpt_close 函数,unlpt_open 函数

ulpt_detach 函数,ulpt_attach 函数

ulpt_ioctl 函数,unlpt_open 函数

ulpt_open 函数,ulpt_attach 函数

ulpt_probe 函数,将一切整合在一起

ulpt_read_callback 函数,ulpt_write_callback 函数

ulpt_reset 函数,ulpt_open 函数

ulpt_start_read 函数,ulpt_watchdog 函数

ulpt_start_write 函数,ulpt_stop_read 函数

ulpt_status_callback 函数,ulpt_read_callback 函数

ulpt_stop_read 函数,ulpt_stop_read 函数

ulpt_stop_write 函数,ulpt_stop_read 函数

ulpt_watchdog 函数,ulpt_watchdog 函数

ulpt_write_callback 函数,ulpt_write_callback 函数

unlpt_open 函数,unlpt_open 函数

USB 帧数据,可选字段

USB 大容量存储 (UMASS),通用访问方法

USB 数据包,可选字段

usbd_transfer_drain 函数,USB 配置结构管理例程

usbd_transfer_setup 函数,USB 传输(在 FreeBSD 中)

usbd_transfer_start 函数,USB 传输(在 FreeBSD 中)

usbd_transfer_stop 函数,USB 配置结构管理例程

usb_config 结构,更多关于 USB 设备的信息

usb_fifo_attach 函数,USB 配置结构管理例程

usb_fifo_detach 函数,USB 配置结构管理例程

usb_fifo_methods 结构,USB 配置结构管理例程

USB_ST_SETUP 常量,USB 传输(在 FreeBSD 中)

V

变量声明,实现睡眠和条件变量

虚拟空调制解调器,案例研究:虚拟空调制解调器,nmdm_modevent 函数,nmdm_clone 函数,nmdm_alloc 函数,nmdm_alloc 函数,nmdm_alloc 函数,nmdm_inwakeup 函数,nmdm_inwakeup 函数,nmdm_modem 函数,nmdm_timeout 函数,nmdm_timeout 函数,nmdm_timeout 函数,bits_per_char 函数

bits_per_char 函数,nmdm_timeout 函数

加载,bits_per_char 函数

nmdm_alloc 函数,nmdm_alloc 函数

nmdm_clone 函数,nmdm_clone 函数

nmdm_inwakeup 函数,nmdm_inwakeup 函数

nmdm_modem 函数,nmdm_inwakeup 函数

nmdm_modevent 函数,nmdm_modevent 函数

nmdm_outwakeup 函数,nmdm_alloc 函数

nmdm_param 函数,nmdm_modem 函数

nmdm_task_tty 函数,nmdm_alloc 函数

nmdm_timeout 函数,nmdm_timeout 函数

概述,案例研究:虚拟空调制解调器

vm_lowmem 事件处理器,内核事件处理器

自愿上下文切换,自愿上下文切换或睡眠

W

网络唤醒(WOL),Hello, world!

wakeup 函数,自愿上下文切换或睡眠

watchdog_list 事件处理器,内核事件处理器

wmesg 参数,自愿上下文切换或睡眠

WOL(网络唤醒),Hello, world!

写操作,定义 ioctl 命令

写操作,从 I/O 端口和 I/O 内存读取,从 I/O 端口和 I/O 内存读取,从 I/O 端口和 I/O 内存读取

到 MMIO(内存映射 I/O),从 I/O 端口和 I/O 内存读取

到 PMIO(端口映射 I/O),从 I/O 端口和 I/O 内存读取

X

xpt_action 函数,CAM 如何工作

xpt_bus_register 函数,cam_sim_alloc 函数

xpt_done 函数,CAM 如何工作

XPT_GET_TRAN_SETTINGS 常量,XPT_RESET_BUS

XPT_GET_TRAN_SETTINGS 操作,XPT_GET_TRAN_SETTINGS

XPT_PATH_INQ 常量,cam_sim_alloc 函数

XPT_PATH_INQ 操作,XPT_PATH_INQ

XPT_RESET_BUS 常量,XPT_PATH_INQ

XPT_RESET_DEV 常量,XPT_SCSI_IO

xpt_run_dev_allocq 函数,CAM 如何工作

xpt_schedule 函数,通用访问方法,CAM 如何工作

XPT_SCSI_IO 常量,XPT_SCSI_IO

XPT_SET_TRAN_SETTINGS 常量,XPT_GET_TRAN_SETTINGS

关于作者

《设计 BSD 根套件》(No Starch Press)的作者 Joseph Kong 从事信息安全、操作系统理论、逆向代码工程和漏洞评估工作。Kong 曾是多伦多市的系统管理员。

版权页

《FreeBSD 设备驱动程序》使用 New Baskerville、TheSansMono Condensed、Futura 和 Dogma 字体排版。

本书由伊利诺伊州马托恩的联合图形公司印刷装订。纸张为 60# Husky Offset,经森林管理委员会(FSC)认证。本书采用平装装订,打开时可以平铺。

附录 A. 更新

访问 nostarch.com/bsddrivers 获取更新、勘误以及其他信息。

posted @ 2025-11-25 17:06  绝不原创的飞龙  阅读(19)  评论(0)    收藏  举报