HyperNews Linux KHG 讨论页面

编写 SCSI 设备驱动程序

版权所有 (C) 1993 Rickard E. Faith (faith@cs.unc.edu)。
于 1993 年在北卡罗来纳大学为 COMP-291 编写。本文档所含信息绝不提供任何担保。
保留所有权利。在所有副本上保留版权声明和本许可声明的前提下,允许制作和分发本文档的完整副本。

这是(经作者明确许可)原始文档的修改副本。如果您希望复制本文档,建议您通过 ftp 从 ftp://ftp.cs.unc.edu/pub/users/faith/papers/scsi.paper.tar.gz 获取原始版本

[请注意,本文档自 1993 年的版权日期以来未进行修订。大多数内容仍然适用,但某些事实,例如当前支持的 SCSI 主机适配器列表,现在已经相当过时。]

您为何要编写 SCSI 驱动程序

目前,Linux 内核包含以下 SCSI 主机适配器的驱动程序:Adaptec 1542、Adaptec 1740、Future Domain TMC-1660/TMC-1680、Seagate ST-01/ST-02、UltraStor 14F 和 Western Digital WD-7000。您可能想要为您不受支持的主机适配器编写自己的驱动程序。您也可能想要重写或更新现有驱动程序之一。

什么是 SCSI?

SCSI-2 标准草案 [ANS] 的前言给出了小型计算机系统接口的简明定义,并简要解释了 SCSI-2 与 SCSI-1 和 CCS 的关系
SCSI 协议旨在提供高效的对等 I/O 总线,最多可连接 8 个设备,包括一个或多个主机。数据可以异步传输,速率仅取决于设备实现和电缆长度。同步数据传输支持高达每秒 10 兆传输的速率。借助 32 位宽数据传输选项,数据速率最高可达每秒 40 兆字节。

SCSI-2 包括用于磁盘和光盘、磁带、打印机、处理器、CD-ROM、扫描仪、介质更换器和通信设备的命令集。

1985 年,当第一个 SCSI 标准最终确定为美国国家标准时,多家制造商与 X3T9.2 任务组联系。他们希望提高 SCSI 的强制性要求,并为直接访问设备定义更多功能。为了不延误 SCSI 标准,X3T9.2 成立了一个特设小组,负责制定一份最终被称为通用命令集 (CCS) 的工作文件。许多磁盘产品在设计时都将此工作文件与 SCSI 标准结合使用。

在开发 CCS 工作文件的同时,X3T9.2 开始着手制定增强型 SCSI 标准,该标准被命名为 SCSI-2。SCSI-2 包括 CCS 工作文件的成果,并将其扩展到所有设备类型。它还添加了缓存命令、性能增强功能以及 X3T9.2 认为有价值的其他功能。虽然 SCSI-2 已经远远超出了原始 SCSI 标准(现在称为 SCSI-1),但它与 SCSI-1 设备保持了高度的兼容性。

SCSI 阶段

“SCSI 总线”在互连的 SCSI 设备之间传输数据和状态信息。“启动器”和“目标”之间的单个事务可能涉及多达 8 个不同的“阶段”。这些阶段几乎完全由目标(例如,硬盘驱动器)决定。当前阶段可以从对五个 SCSI 总线信号的检查中确定,如下表所示 [LXT91, p. 57]。
-SEL-BSY-MSG-C/D-I/O阶段
高电平高电平???总线空闲
高电平低电平???仲裁
II&T???选择
TI&T???重选
高电平低电平高电平高电平高电平数据输出
高电平低电平高电平高电平低电平数据输入
高电平低电平高电平低电平高电平命令
高电平低电平高电平低电平低电平状态
高电平低电平低电平低电平高电平消息输出
高电平低电平低电平低电平低电平消息输入
I = 启动器断言,T = 目标断言,? = 高电平或低电平

某些控制器(特别是廉价的 Seagate 控制器)需要直接操作 SCSI 总线——其他控制器则自动处理这些底层细节。八个阶段中的每一个阶段都将详细描述。

总线空闲阶段
总线空闲阶段表示 SCSI 总线处于空闲状态,当前未使用。
仲裁阶段
当 SCSI 设备尝试获得 SCSI 总线的控制权时,将进入仲裁阶段。仲裁只能在总线先前处于总线空闲阶段时开始。在仲裁期间,仲裁设备在数据总线上断言其 SCSI ID。例如,如果仲裁设备的 SCSI ID 为 2,则设备将断言0x04。如果多个设备尝试同时仲裁,则 SCSI ID 最高的设备将获胜。虽然仲裁在 SCSI-1 标准中是可选的,但在 SCSI-2 标准中是必需的阶段。
选择阶段
仲裁之后,仲裁设备(现在称为启动器)在数据总线上断言目标的 SCSI ID。目标(如果存在)将通过拉高 -BSY 线来确认选择。只要目标连接到启动器,此线就保持活动状态。
重选阶段
SCSI 协议允许设备在处理请求时从总线断开连接。当设备准备就绪时,它会重新连接到主机适配器。重选阶段与选择阶段相同,不同之处在于它由断开连接的目标用于重新连接到原始启动器。当前不支持重选的驱动程序不允许 SCSI 目标断开连接。但是,所有驱动程序都应支持重选,以便多个 SCSI 设备可以同时处理命令。这允许由于交错的 I/O 请求而显着提高吞吐量。
命令阶段
在此阶段,命令信息(6、10 或 12 字节)从启动器传输到目标。
数据输出和数据输入阶段
在这些阶段,数据在启动器和目标之间传输。例如,数据输出阶段将数据从主机适配器传输到磁盘驱动器。数据输入阶段将数据从磁盘驱动器传输到主机适配器。如果 SCSI 命令不需要数据传输,则不会进入任何一个阶段。
状态阶段
此阶段在所有命令完成后进入,并允许目标向启动器发送状态字节。有九个有效的状态字节,如下表所示 [ANS, p. 77]。请注意,由于位 1-5(位 0 是最低有效位)用于状态代码(其他位保留),因此状态字节应使用0x3e进行屏蔽,然后再进行检查。
值*状态
0x00良好
0x02检查条件
0x04条件满足
0x08
0x10中间
0x14中间-条件满足
0x18保留冲突
0x22命令终止
0x28队列已满
*使用 0x3e 屏蔽后

以下概述了三个最重要的状态代码的含义

良好
操作成功完成。
检查条件
发生错误。应使用 REQUEST SENSE 命令查找有关错误的更多信息(请参阅 SCSI 命令)。
设备无法接受命令。这可能发生在自检期间或刚开机后不久。
消息输出和消息输入阶段
附加信息在目标和启动器之间传输。此信息可能与未完成命令的状态有关,也可能是协议更改请求。在单个 SCSI 事务期间,可能会发生多个消息输入和消息输出阶段。如果支持重选,则驱动程序必须能够正确处理 SAVE DATA POINTERS、RESTORE POINTERS 和 DISCONNECT 消息。虽然 SCSI-2 标准要求这样做,但某些设备在 DISCONNECT 消息之前不会自动发送 SAVE DATA POINTERS 消息。

SCSI 命令

每个 SCSI 命令的长度为 6、10 或 12 字节。SCSI 驱动程序开发人员必须充分理解以下命令。

REQUEST SENSE
每当命令返回 CHECK CONDITION 状态时,高级 Linux SCSI 代码都会自动通过执行 REQUEST SENSE 来获取有关错误的更多信息。此命令返回一个 sense key 和一个 sense code(在 SCSI-2 标准 [ANS] 中称为“附加 sense 代码”或 ASC)。某些 SCSI 设备也可能报告“附加 sense 代码限定符”(ASCQ)。下表描述了 16 个可能的 sense key。有关 ASC 和 ASCQ 的信息,请参阅 SCSI 标准 [ANS] 或 SCSI 设备技术手册。
Sense Key描述
0x00NO SENSE
0x01RECOVERED ERROR
0x02NOT READY
0x03MEDIUM ERROR
0x04HARDWARE ERROR
0x05ILLEGAL REQUEST
0x06UNIT ATTENTION
0x07DATA PROTECT
0x08BLANK CHECK
0x09(供应商特定错误)
0x0aCOPY ABORTED
0x0bABORTED COMMAND
0x0cEQUAL
0x0dVOLUME OVERFLOW
0x0eMISCOMPARE
0x0f保留
TEST UNIT READY
此命令用于测试目标的状态。如果目标可以接受介质访问命令(例如,READ 或 WRITE),则命令返回 GOOD 状态。否则,命令返回 CHECK CONDITION 状态和 NOT READY 的 sense key。此响应通常表示目标正在完成开机自检。
INQUIRY
此命令返回目标的制造商、型号和设备类型。高级 Linux 代码使用此命令来区分磁盘、光盘和磁带驱动器(高级代码目前不支持打印机、处理器或点唱机)。
READ 和 WRITE
这些命令用于从目标传输数据和向目标传输数据。在尝试使用 READ 和 WRITE 命令之前,您应确保您的驱动程序可以支持更简单的命令,例如 TEST UNIT READY 和 INQUIRY。

入门指南

低级设备驱动程序的作者需要了解内核如何处理中断。至少,应理解禁用 (cli()) 和启用 (sti()) 中断的内核函数。某些驱动程序可能还需要调度函数(例如,schedule(), sleepon()wakeup())。有关这些函数的详细说明,请参见 支持函数

开始之前:收集工具

在您开始为 Linux 编写 SCSI 驱动程序之前,您需要获取一些资源。

最重要的是可启动的 Linux 系统——最好是从 IDE、RLL 或 MFM 硬盘启动的系统。在开发新的 SCSI 驱动程序期间,您将多次重建内核并重新启动系统。编程错误可能会导致 SCSI 驱动器非 SCSI 驱动器上的数据损坏。在开始之前备份您的系统

已安装的 Linux 系统可以非常精简:GCC 编译器发行版(包括库和二进制实用程序)、编辑器和内核源代码是您需要的全部。其他工具,如od, hexdumpless将非常有用。所有这些工具都可以安装在廉价的 20-30~MB 硬盘上。(使用过的 20 MB MFM 硬盘和控制器的价格应低于 100 美元。)

文档至关重要。至少,您需要主机适配器的技术手册。由于 Linux 是可自由分发的,并且由于您(理想情况下)希望自由分发您的源代码,请避免保密协议 (NDA)。大多数 NDA 会禁止您发布源代码——您可能被允许发布包含驱动程序的目标文件,但这在目前的 Linux 社区中是完全不可接受的。

解释 SCSI 标准的手册将很有帮助。通常,您的磁盘驱动器的技术手册就足够了,但 SCSI 标准的副本通常也会很有帮助。(1991 年 10 月 17 日 SCSI-2 标准文档草案可通过匿名 ftp 从sunsite.unc.edu/pub/Linux/development/scsi-2.tar.Z获取,也可以从 Global Engineering Documents (2805 McGaw, Irvine, CA 92714) 购买,电话 (800)-854-7179 或 (714)-261-1455。请参考文档 X3.131-199X。在 1993 年初,该手册的价格为 60-70 美元。)

在开始之前,请打印hosts.h, scsi.h以及 Linux 内核中的现有驱动程序之一的硬拷贝。在您编写驱动程序时,这些将证明是有用的参考资料。

Linux SCSI 接口

Linux 内核中的高级 SCSI 接口管理内核和低级 SCSI 设备驱动程序之间的所有交互。由于这种分层设计,低级 SCSI 驱动程序只需要向高级代码提供一些基本服务。低级驱动程序的作者不需要了解内核 I/O 系统的复杂性,因此可以在相对较短的时间内编写低级驱动程序。

两个主要结构 (Scsi_HostScsi_Cmnd) 用于在高级代码和低级代码之间进行通信。接下来的两节提供了有关这些结构和低级驱动程序要求的详细信息。

所述Scsi_Host结构

所述Scsi_Host结构用于向高级代码描述低级驱动程序。通常,此描述放在设备驱动程序的头文件中,在 C 预处理器定义中
    #define FDOMAIN_16X0  { "Future Domain TMC-16x0",          \
                             fdomain_16x0_detect,              \
                             fdomain_16x0_info,                \
                             fdomain_16x0_command,             \
                             fdomain_16x0_queue,               \
                             fdomain_16x0_abort,               \
                             fdomain_16x0_reset,               \
                             NULL,                             \
                             fdomain_16x0_biosparam,           \
                             1, 6, 64, 1 ,0, 0}
    #endif

所述Scsi_Host结构在下面给出。本节稍后将详细解释每个字段。

    typedef struct     
    {
      char               *name;
      int                (* detect)(int); 
      const char         *(* info)(void);
      int                (* queuecommand)(Scsi_Cmnd *,
                          void (*done)(Scsi_Cmnd *));
      int                (* command)(Scsi_Cmnd *);
      int                (* abort)(Scsi_Cmnd *, int);
      int                (* reset)(void);
      int                (* slave_attach)(int, int);
      int                (* bios_param)(int, int, int []);
      int                can_queue;
      int                this_id;
      short unsigned int sg_tablesize;
      short              cmd_per_lun;
      unsigned           present:1;     
      unsigned           unchecked_isa_dma:1;
    } Scsi_Host;

中的变量Scsi_Host结构

一般来说,变量在Scsi_Host结构直到detect()函数(请参阅 detect() 部分)被调用后才使用。因此,任何在主机适配器检测之前无法赋值的变量都应在检测期间赋值。例如,如果单个驱动程序为具有非常相似特征的多个主机适配器提供支持,则可能会发生这种情况。Scsi_Host结构中的某些参数可能会取决于检测到的特定主机适配器。

name

name保存指向 SCSI 主机适配器的简短描述的指针。

can_queue

can_queue保存主机适配器可以处理的未完成命令的数量。除非驱动程序支持重选并且驱动程序是中断驱动的(一些早期的 Linux 驱动程序不是中断驱动的,因此性能非常差),否则此变量应设置为 1。

this_id

大多数主机适配器都有分配给它们的特定 SCSI ID。此 SCSI ID(通常为 6 或 7)用于重选。this_id变量保存主机适配器的 SCSI ID。如果主机适配器没有分配的 SCSI ID,则此变量应设置为 -1(在这种情况下,不支持重选)。

sg_tablesize

高级代码支持“分散-收集”,这是一种通过将许多小的 SCSI 请求合并为几个大的 SCSI 请求来提高 SCSI 吞吐量的方法。由于大多数 SCSI 磁盘驱动器都采用 1:1 交错格式(“1:1 交错”意味着单个磁道中的所有扇区都连续出现在磁盘表面上),因此执行 SCSI 仲裁和选择阶段所需的时间比扇区之间的旋转延迟时间更长。(这可能是一种过度简化。在较旧的设备上,实际的命令处理可能很重要。此外,内核中存在大量的分层开销:高级 SCSI 代码、缓冲代码和文件系统代码都会导致 SCSI 性能下降。)因此,每个磁盘旋转周期只能处理一个 SCSI 请求,从而导致吞吐量约为每秒 50 千字节。但是,当支持分散-收集时,平均吞吐量通常超过每秒 500 千字节。

所述sg_tablesize变量保存分散-收集列表中允许的最大请求数。如果驱动程序不支持分散-收集,则此变量应设置为SG_NONE。如果驱动程序可以支持无限数量的分组请求,则此变量应设置为SG_ALL。某些驱动程序将使用主机适配器来管理分散-收集列表,并且可能需要限制sg_tablesize为主机适配器硬件支持的数量。例如,某些 Adaptec 主机适配器需要限制为 16。

cmd_per_lun

SCSI 标准支持“链接命令”的概念。链接命令允许将多个命令连续排队到单个 SCSI 设备。cmd_per_lun变量指定允许的链接命令的数量。如果不支持命令链接,则此变量应设置为 1。但是,目前,高级 SCSI 代码不会利用此功能。

链接命令与多个未完成命令(由can_queue变量描述)从根本上不同。链接命令始终发送到同一 SCSI 目标,并且不一定涉及重选阶段。此外,链接命令消除了集合中第一个命令之后的所有命令的仲裁、选择和消息输出阶段。相比之下,多个未完成命令可以发送到任意 SCSI 目标,并且需要仲裁、选择、消息输出和重选阶段。

present

所述present位(由高级代码)在检测到主机适配器时设置。

unchecked_isa_dma

某些主机适配器使用直接内存访问 (DMA) 来直接从计算机主内存读取和写入数据块。Linux 是一个虚拟内存操作系统,可以使用超过 16 MB 的物理内存。不幸的是,在使用 ISA 总线(所谓的“工业标准架构”总线是随着 IBM PC/XT 和 IBM PC/AT 计算机引入的)的机器上,DMA 仅限于低 16 MB 的物理内存。

如果unchecked_isa_dma位已设置,则高级代码将提供数据缓冲区,保证这些缓冲区位于物理地址空间的低 16 MB 中。为不使用 DMA 的主机适配器编写的驱动程序应将此位设置为零。专用于 EISA 总线(“扩展工业标准架构”总线是用于 386 和 i486 机器的非专有 32 位总线)机器的驱动程序也应将此位设置为零,因为 EISA 总线机器允许不受限制的 DMA 访问。

中的函数Scsi_Host结构

detect()

所述detect()函数的唯一参数是“主机号”,它是scsi_hosts变量(类型数组struct Scsi_Host)的索引。detect()函数应在检测到主机适配器时返回非零值,否则应返回零。

主机适配器检测必须谨慎进行。通常,该过程首先在 ROM 区域中查找主机适配器的“BIOS 签名”。在 PC/AT 兼容计算机上,地址空间在0xc00000xfffff0xc0000之间的使用已得到很好的定义。例如,大多数机器上的视频 BIOS 从开始,硬盘 BIOS(如果存在)从0xc80000xc0000开始。当 PC/AT 兼容计算机启动时,从0xf8000的每个 2 千字节块都会被检查 2 字节签名 (0x55aa

),这表示存在有效的 BIOS 扩展 [Nor85]。

    FUTURE DOMAIN CORP. (C) 1986-1990 1800-V2.07/28/89
BIOS 签名通常由一系列字节组成,这些字节唯一地标识 BIOS。例如,Future Domain BIOS 签名之一是字符串

,正好在 BIOS 块开头的第五个字节处找到。

找到 BIOS 签名后,可以更具体地测试是否存在功能正常的主机适配器。由于 BIOS 签名是硬编码在内核中的,因此新 BIOS 的发布可能会导致驱动程序莫名其妙地失败。此外,专门为 Linux 使用 SCSI 适配器的人可能希望禁用 BIOS 以加快启动时间。因此,如果可以在不检查 BIOS 的情况下安全地检测到适配器,则应使用该替代方法。

通常,每个主机适配器都有一系列 I/O 端口地址,用于通信。有时,这些地址会硬编码到驱动程序中,迫使所有拥有此主机适配器的 Linux 用户使用一组特定的 I/O 端口地址。其他驱动程序更灵活,并通过扫描所有可能的端口地址来查找当前的 I/O 端口地址。通常,每个主机适配器将允许 3 或 4 组地址,这些地址可以通过主机适配器卡上的硬件跳线选择。

找到 I/O 端口地址后,可以查询主机适配器以确认它确实是预期的主机适配器。这些测试是主机适配器特定的,但通常包括确定 BIOS 基地址(然后可以将其与 BIOS 签名搜索期间找到的 BIOS 地址进行比较)或验证与主板关联的唯一标识号的方法。对于 MCA 总线(“微通道架构”总线是 IBM 用于 386 和 i486 机器的专有 32 位总线)机器,每种类型的主板都给出一个唯一的标识号,其他制造商都不能使用——例如,多个 Future Domain 主机适配器也在 ISA 总线机器上使用此号码作为唯一标识符。程序员可以使用其他方法来验证主机适配器的存在和功能。

请求 IRQdetect()检测后,例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法irqaction().

所述irqaction()request_irq()函数采用两个参数,即 IRQ 号和指向处理程序例程的指针。然后,它设置一个默认的sigaction例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法结构并调用函数(Linux 0.99.7 内核源代码,irqaction()linux/kernel/irq.c例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法)的代码如下所示。我将我的讨论限制在更通用的

    int request_irq( unsigned int irq, void (*handler)( int ) )
    {
      struct sigaction sa;
    
      sa.sa_handler  = handler;
      sa.sa_flags    = 0;
      sa.sa_mask     = 0;
      sa.sa_restorer = NULL;
      return irqaction( irq, &sa );
    }

函数上。函数(Linux 0.99.7 内核源代码,例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法声明(Linux 0.99.5 内核源代码,

    int irqaction( unsigned int irq, struct sigaction *new )
函数是其中第一个参数irq是要请求的 IRQ 的编号,第二个参数new是一个具有定义(Linux 0.99.5 内核源代码,linux/include/linux/signal.h
    struct sigaction
    {
      __sighandler_t sa_handler;
      sigset_t       sa_mask;
      int            sa_flags;
      void           (*sa_restorer)(void);
    };

)的结构,如下所示在此结构中,sa_handler

    void fdomain_16x0_intr( int irq )
应指向您的中断处理程序例程,该例程应具有类似于以下内容的定义其中第一个参数其中

所述将是导致调用中断处理程序例程的 IRQ 的编号。sa_mask例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法变量用作例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法.

所述例程的内部标志。传统上,此变量在调用之前设置为零sa_flags变量可以设置为零或SA_INTERRUPT变量可以设置为零或。如果选择零,则中断处理程序将在启用其他中断的情况下运行,并将通过信号处理返回函数返回。对于相对较慢的 IRQ,例如与键盘和定时器中断相关的 IRQ,建议使用此选项。如果变量可以设置为零或被选中,则将在禁用中断的情况下调用处理程序,并且返回将避免信号处理返回函数。

所述选择“快速”IRQ 处理程序调用例程,建议用于中断驱动的硬盘例程。但是,中断处理程序应尽快打开中断,以便可以处理其他中断。sa_restorer变量当前未使用,传统上设置为.

所述irqaction()例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法NULL

函数将在成功将 IRQ 分配给指定的 中断处理程序例程时返回零。非零结果代码可以解释如下
-EINVAL变量当前未使用,传统上设置为请求的 IRQ 大于 15,或者传递了
指针而不是指向中断处理程序例程的有效指针。
-EBUSY请求的 IRQ 已分配给另一个中断处理程序。这种情况永远不应该发生,并且是调用.

panic()例程必须从内核请求任何所需的中断或 DMA 通道。有 16 个中断通道,标记为 IRQ 0 到 IRQ 15。内核提供了两种设置 IRQ 处理程序的方法的合理理由。

内核使用 Intel “中断门”来设置通过

函数请求的 IRQ 处理程序例程。Intel i486 手册 [Int90, p. 9-11] 将中断门解释如下

使用... 中断门... 的中断导致在堆栈上保存其当前值作为 EFLAGS 寄存器的已保存内容的一部分后,TF 标志 [陷阱标志] 被清除。这样做,处理器可以防止指令跟踪影响中断响应。随后的 IRET [中断返回] 指令将 TF 标志恢复为堆栈上 EFLAGS 寄存器的已保存内容中的值。

... 使用中断门的中断清除 IF 标志 [中断使能标志],这可以防止其他中断干扰当前中断处理程序。随后的 IRET 指令将 IF 标志恢复为堆栈上 EFLAGS 寄存器的已保存内容中的值。

请求 DMA 通道detect()某些 SCSI 主机适配器使用 DMA 来访问内存中的大型数据块。由于 CPU 不必处理单独的 DMA 请求,因此数据传输比 CPU 介导的传输更快,并允许 CPU 在块传输期间执行其他有用的工作(假设启用了中断)。主机适配器将使用特定的 DMA 通道。此 DMA 通道将由函数确定,并使用

函数将在成功将 IRQ 分配给指定的 中断处理程序例程时返回零。非零结果代码可以解释如下
request_dma()
指针而不是指向中断处理程序例程的有效指针。
函数从内核请求。此函数将 DMA 通道号作为其唯一参数,并在成功分配 DMA 通道时返回零。非零结果可以解释如下请求的 IRQ 已分配给另一个中断处理程序。这种情况永远不应该发生,并且是调用.
请求的 DMA 通道号大于 7。

所述请求的 DMA 通道号大于 7。请求的 DMA 通道已分配。这是一个非常严重的情况,可能会导致任何 SCSI 请求失败。它值得调用nameinfo()

函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于

所述函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于变量指向的描述,将在启动时打印。queuecommand()函数设置主机适配器以处理 SCSI 命令,然后返回。当命令完成时,将调用Scsi_Cmnddone()函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于函数,并将

  1. 结构指针作为参数。这允许以中断驱动的方式执行 SCSI 命令。在返回之前,Scsi_Cmnd函数必须执行以下几项操作
  2. 结构指针作为参数。这允许以中断驱动的方式执行 SCSI 命令。在返回之前,queuecommand()保存指向结构的指针。函数中的Scsi_Cmndscsi_done()
  3. 函数指针在Scsi_Cmnd结构中。有关更多信息,请参阅 done() 部分。Scsi_Cmnd函数必须执行以下几项操作
  4. 设置驱动程序所需的特殊

所述函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于变量。有关can_queue的详细信息,请参阅 Scsi_Cmnd 结构启动 SCSI 命令。对于高级主机适配器,这可能就像将命令发送到主机适配器“邮箱”一样简单。对于不太高级的主机适配器,仲裁阶段是手动启动的。函数函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于变量(请参阅 can_queue 部分)为非零时调用。否则,

queuecommand()

所述queuecommand()command()Scsi_Cmnd函数用于所有 SCSI 请求。函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于函数应在成功时返回零(当前高级 SCSI 代码目前忽略返回值)。queuecommand()函数在 SCSI 命令完成后调用。此命令需要的唯一参数是指向先前传递给函数的同一结构的指针。在调用函数的同一函数之前,必须正确设置

result
变量。
变量是一个 32 位整数,每个字节都有特定的含义
字节 0(LSB)
此字节包含命令的 SCSI STATUS 代码,如 SCSI 阶段 部分所述。
字节 1scsi.h此字节包含 SCSI MESSAGE,如 SCSI 阶段 部分所述。
字节 2
此字节保存主机适配器的返回代码。此字节的有效代码在
中给出,并在下面描述
DID_OK
没有错误。
DID_NO_CONNECT
SCSI 选择失败,因为在指定的地址没有设备。
由于未知原因发生超时,可能是在 SELECTION 期间或等待 RESELECTION 时。
DID_BAD_TARGET
目标的 SCSI ID 与主机适配器的 SCSI ID 相同。
DID_ABORT
高级代码调用了低级abort()函数(参见 abort() 部分)。
DID_PARITY
检测到 SCSI 奇偶校验错误。
DID_ERROR
发生了一个错误,但缺乏更合适的错误代码(例如,内部主机适配器错误)。
DID_RESET
高级代码调用了低级reset()函数(参见 reset() 部分)。
DID_BAD_INTR
发生了一个意外中断,并且没有合适的方法来处理此中断。
请注意,返回没有错误。将强制命令重试,而返回中给出,并在下面描述将中止命令。
字节 3 (MSB)
此字节用于高级返回代码,应由低级代码保留为零。

当前的低级驱动程序没有统一(或正确地)实现错误报告,因此最好查阅 scsi.c 以确定应该如何准确地报告错误,而不是研究现有的驱动程序。

启动 SCSI 命令。对于高级主机适配器,这可能就像将命令发送到主机适配器“邮箱”一样简单。对于不太高级的主机适配器,仲裁阶段是手动启动的。

所述启动 SCSI 命令。对于高级主机适配器,这可能就像将命令发送到主机适配器“邮箱”一样简单。对于不太高级的主机适配器,仲裁阶段是手动启动的。函数处理 SCSI 命令并在命令完成时返回。当最初编写 SCSI 代码时,不支持中断驱动的驱动程序。旧的驱动程序比当前的中断驱动的驱动程序效率低得多(在响应时间和延迟方面),但也更容易编写。对于新的驱动程序,此命令可以替换为对函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于函数的调用,如此处所示。(Linux 0.99.5 内核,linux/kernel/blk_drv/scsi/aha1542.c,由 Tommy Thorn 编写。)

    static volatile int internal_done_flag    = 0;
    static volatile int internal_done_errcode = 0;
    static void         internal_done( Scsi_Cmnd *SCpnt )
    {
      internal_done_errcode = SCpnt->result;
      ++internal_done_flag;
    }

    int aha1542_command( Scsi_Cmnd *SCpnt )
    {
      aha1542_queuecommand( SCpnt, internal_done );

      while (!internal_done_flag);
      internal_done_flag = 0;
      return internal_done_errcode;
    }

返回值与函数的同一结构中的Scsi_Cmnd变量相同。请参阅 done() 部分和 Scsi_Cmnd 结构 部分以获取更多详细信息。

abort()

高级 SCSI 代码处理所有超时。这使低级驱动程序无需进行计时,并允许对不同的设备使用不同的超时时间段(例如,SCSI 磁带驱动器的超时时间几乎是无限的,而 SCSI 磁盘驱动器的超时时间相对较短)。

所述abort()函数用于请求中止当前正在执行的 SCSI 命令,该命令由Scsi_Cmnd指针指示。设置函数的同一结构中的Scsi_Cmnd结构后,abort()函数返回零。如果代码,即abort()函数的第二个参数为零,则函数的同一应设置为DID_ABORT。否则,函数的同一应设置为等于代码。如果代码不为零,则通常为SCSI 选择失败,因为在指定的地址没有设备。DID_RESET.

。目前,没有一个低级驱动程序能够正确中止 SCSI 命令。发起者应请求(通过断言-ATN线路)目标进入 MESSAGE OUT 阶段。然后,发起者应向目标发送 ABORT 消息。

reset()

所述reset()函数用于重置 SCSI 总线。在 SCSI 总线重置后,任何正在执行的命令都应失败,并返回DID_RESET结果代码(参见 done() 部分)。

目前,没有一个低级驱动程序能正确处理重置。要正确重置 SCSI 命令,发起者应请求(通过断言-ATN线路)目标进入 MESSAGE OUT 阶段。然后,发起者应向目标发送 BUS DEVICE RESET 消息。也可能需要通过断言-RST线路来启动 SCSI RESET,这将导致所有目标设备被重置。重置后,可能需要与目标重新协商同步通信协议。

slave_attach()

所述slave_attach()函数目前尚未实现。此函数将用于协商主机适配器和目标驱动器之间的同步通信。此协商需要在发起者和目标之间交换一对 SYNCHRONOUS DATA TRANSFER REQUEST 消息。此交换应在以下条件下发生 [LXT91]:

支持同步数据传输的 SCSI 设备识别到自上次接收到“硬”RESET 以来,它尚未与其他 SCSI 设备通信。

支持同步数据传输的 SCSI 设备识别到自接收到 BUS DEVICE RESET 消息以来,它尚未与其他 SCSI 设备通信。

bios_param()

Linux 支持 MS-DOS(MS-DOS 是微软公司的注册商标)硬盘分区系统。每个磁盘都包含一个“分区表”,该表定义了磁盘如何划分为逻辑扇区。解释此分区表需要有关磁盘大小的信息,以柱面、磁头和每柱面扇区为单位。然而,SCSI 磁盘隐藏了它们的物理几何结构,并以逻辑方式作为连续的扇区列表访问。因此,为了与 MS-DOS 兼容,SCSI 主机适配器将“谎报”其几何结构。SCSI 磁盘的物理几何结构虽然可用,但很少用作“逻辑几何结构”。(其原因涉及 MS-DOS 施加的古老且任意的限制。)

Linux 需要确定“逻辑几何结构”,以便它可以正确地修改和解释分区表。不幸的是,没有标准的物理几何结构和逻辑几何结构之间的转换方法。因此,引入了bios_param()函数,试图提供对主机适配器几何结构信息的访问。

所述size参数是以扇区为单位的磁盘大小。一些主机适配器使用基于此数字的确定性公式来计算驱动器的逻辑几何结构。其他主机适配器将几何结构信息存储在驱动程序可以访问的表中。为了方便访问,dev参数包含驱动器的设备号。在linux/fs.h中定义了两个宏,这将有助于解释此值MAJOR(dev)是设备的主设备号,而MINOR(dev)是设备的次设备号。这些是标准 Linux mknod 命令用来在 /dev 目录中创建设备的相同主设备号和次设备号。info参数指向一个包含三个整数的数组,bios_param()函数将在返回之前填充这些整数

info[0]
磁头数
info[1]
每柱面扇区数
info[2]
柱面数

中的信息info不是驱动器的物理几何结构,而只是与 MS-DOS 用来访问驱动器的逻辑几何结构相同的逻辑几何结构。物理几何结构和逻辑几何结构之间的区别怎么强调都不过分。

所述Scsi_Cmnd结构

所述Scsi_Cmnd结构,(Linux 0.99.7 内核,linux/kernel/blk_drv/scsi/scsi.h),如下所示,由高级代码使用,以指定要由低级代码执行的 SCSI 命令。低级设备驱动程序可以忽略Scsi_Cmnd结构中的许多变量——但是,其他变量非常重要。

    typedef struct scsi_cmnd
    {
      int              host;
      unsigned char    target,
                       lun,
                       index;
      struct scsi_cmnd *next,
                       *prev;   

      unsigned char    cmnd[10];
      unsigned         request_bufflen;
      void             *request_buffer;

      unsigned char    data_cmnd[10];
      unsigned short   use_sg;
      unsigned short   sglist_len;
      unsigned         bufflen;
      void             *buffer;
        
      struct request   request;
      unsigned char    sense_buffer[16];
      int              retries;
      int              allowed;
      int              timeout_per_command,
                       timeout_total,
                       timeout;
      unsigned char    internal_timeout;
      unsigned         flags;
                
      void (*scsi_done)(struct scsi_cmnd *);  
      void (*done)(struct scsi_cmnd *);

      Scsi_Pointer     SCp;
      unsigned char    *host_scribble;
      int              result;
      
    } Scsi_Cmnd;                 

保留区域

信息性变量

hostscsi_hosts数组的索引。

target存储 SCSI 命令的目标的 SCSI ID。如果支持每个目标的多个未完成命令或多个命令,则此信息非常重要。

cmnd是一个字节数组,其中包含实际的 SCSI 命令。这些字节应在 COMMAND 阶段发送到 SCSI 目标。cmnd[0]是 SCSI 命令代码。COMMAND_SIZE宏,在scsi.h中定义,可用于确定当前 SCSI 命令的长度。

函数的同一用于存储来自 SCSI 请求的结果代码。有关此变量的更多信息,请参见 done() 部分。必须在低级例程返回之前正确设置此变量。

分散-聚集列表

use_sg包含分散-聚集链中片段的数量计数。如果use_sg为零,则request_buffer指向 SCSI 命令的数据缓冲区,并且request_bufflen是以字节为单位的此缓冲区的长度。否则,request_buffer指向一个scatterlist结构的数组,并且use_sg将指示数组中有多少个这样的结构。request_buffer的用法是非直观且令人困惑的。

数组的每个元素都包含一个scatterlist数组address和一个length组件。如果unchecked_isa_dma结构中的Scsi_Host标志设置为 1(有关 DMA 传输的更多信息,请参见 unchecked_isa_dma 部分),则保证地址在物理内存的前 16 MB 内。大量数据将由单个 SCSI 命令处理。这些数据的长度将等于scatterlist数组的索引。

指向的所有缓冲区的长度之和。

暂存区

所述结构的指针。根据主机适配器的功能和要求,可以以多种方式处理分散-聚集列表。为了支持多种方法,提供了几个暂存区供低级驱动程序独占使用。

指针queuecommand()函数中的函数仅返回指向静态区域的指针,该区域包含低级驱动程序的简要说明。此描述类似于此指针应设置为

所述函数(有关更多信息,请参见 queuecommand() 部分)。此指针没有其他用途。根据主机适配器的功能和要求,可以以多种方式处理分散-聚集列表。为了支持多种方法,提供了几个暂存区供低级驱动程序独占使用。

host_scribble高级代码提供了一对内存分配函数,scsi_malloc()scsi_free()高级代码提供了一对内存分配函数,,保证返回物理内存前 16 MB 中的内存。因此,此内存适用于 DMA。每个请求分配的内存量必须是 512 字节的倍数,并且必须小于或等于 4096 字节。通过Scsi_Host可用的内存总量是sg_tablesize, cmd_per_lununchecked_isa_dma.

所述函数(有关更多信息,请参见 queuecommand() 部分)。此指针没有其他用途。结构变量的复杂函数。高级代码提供了一对内存分配函数,指针可用于指向使用

所述分配的内存区域。低级 SCSI 驱动程序负责管理此指针及其关联的内存,并在不再需要该区域时释放它。结构
所述Scsi_PointerSCp分配的内存区域。低级 SCSI 驱动程序负责管理此指针及其关联的内存,并在不再需要该区域时释放它。变量,类型为
    typedef struct scsi_pointer
    {
      char               *ptr;             /* data pointer */
      int                this_residual;    /* left in this buffer */
      struct scatterlist *buffer;          /* which buffer */
      int                buffers_residual; /* how many buffers left */

      volatile int       Status;
      volatile int       Message;
      volatile int       have_data_in;
      volatile int       sent_command;
      volatile int       phase;
    } Scsi_Pointer;
的结构,在此处描述此结构中的变量可以以低级驱动程序中任何必要的方式使用。通常,bufferscatterlist, 指向中的当前条目scatterlist, buffers_residual计数中剩余的条目数ptr

用作缓冲区的指针,并且

this_residual

计数传输中剩余的字符数。一些主机适配器需要支持这种交互的细节——其他主机适配器可以完全忽略此结构。

第二组变量提供了方便的位置来存储 SCSI 状态信息以及各种指针和标志。

致谢
感谢 Drew Eckhardt、Michael K. Johnson、Karin Boes、Devesh Bhatnagar 和 Doug Hoffman 阅读本文的早期版本,并提供了许多有益的评论。特别感谢我的 COMP-291(计算机科学专业写作)“读者”,Peter Calingaert 教授和 Raj Kumar Singh 教授。
参考书目
[ANS]
美国国家信息系统标准草案:小型计算机系统接口-2 (SCSI-2)。 (X3T9.2/86-109,修订版 10h,1991 年 10 月 17 日)。
[Int90] 1991.
英特尔。i486 处理器程序员参考手册。 Intel/McGraw-Hill,1990。
[LXT91]


LXT SCSI 产品:规格和 OEM 技术手册,

[Nor85]