Linux CD-ROM 标准

作者:

David van Leeuwen <david@ElseWare.cistron.nl>

日期:

1999 年 3 月 12 日

更新者:

Erik Andersen (andersee@debian.org)

更新者:

Jens Axboe (axboe@image.dk)

引言

Linux 可能是支持硬件设备种类最广泛的类 Unix 操作系统。原因大概是:

  • Linux 现在支持的众多平台(即 i386-PC、Sparc Suns 等)可用的硬件设备列表很长。

  • 操作系统的开放设计,使得任何人都可以为 Linux 编写驱动程序。

  • 有大量的源代码可作为编写驱动程序的示例。

Linux 的开放性以及多种不同类型的可用硬件使得 Linux 能够支持许多不同的硬件设备。不幸的是,正是这种开放性使得 Linux 能够支持所有这些不同的设备,但也使得每个设备驱动程序的行为在设备之间差异很大。这种行为差异对于 CD-ROM 设备来说非常显著;特定的驱动器对“标准” ioctl() 调用的反应因设备驱动程序而异。为了避免使他们的驱动程序完全不一致,Linux CD-ROM 驱动程序的编写者通常通过理解、复制然后更改现有驱动程序来创建新的设备驱动程序。不幸的是,这种做法并未在所有 Linux CD-ROM 驱动程序之间保持统一的行为。

本文档旨在建立所有 Linux CD-ROM 设备驱动程序之间的统一行为。本文档还定义了各种 ioctl() 以及低级 CD-ROM 设备驱动程序应如何实现它们。目前(截至 Linux 2.1.x 开发内核),包括 IDE/ATAPI 和 SCSI 在内的几个低级 CD-ROM 设备驱动程序,现在都使用这个统一接口。

CD-ROM 开发时,CD-ROM 驱动器与计算机之间的接口并未在标准中指定。因此,开发了许多不同的 CD-ROM 接口。其中一些具有自己的专有设计(Sony、Mitsumi、Panasonic、Philips),其他制造商采用了现有的电气接口并改变了功能(CreativeLabs/SoundBlaster、Teac、Funai)或简单地使其驱动器适应一个或多个现有电气接口(Aztech、Sanyo、Funai、Vertos、Longshine、Optics Storage 和大多数“无名”制造商)。在新的驱动器确实带来自己的接口或使用自己的命令集和流控制方案的情况下,要么需要编写一个单独的驱动程序,要么需要增强现有驱动程序。历史为我们提供了对许多这些不同接口的 CD-ROM 支持。如今,几乎所有新的 CD-ROM 驱动器都是 IDE/ATAPI 或 SCSI,并且制造商创建新接口的可能性非常小。即使是寻找旧专有接口的驱动器也变得困难。

当我(在 1.3.70 年代)查看现有的软件接口时,它通过 cdrom.h 表达,它似乎是一组相当混乱的命令和数据格式 [1]。似乎软件接口的许多功能都是为了适应特定驱动器的能力而临时添加的。更重要的是,似乎“标准”命令的行为在大多数不同的驱动程序中是不同的:例如,一些驱动程序在托盘打开时发生 open() 调用时会关闭托盘,而另一些则不会。一些驱动程序在打开设备时会锁定门,以防止文件系统不一致,而另一些则不会,以允许软件弹出。毫无疑问,不同驱动器的功能各不相同,但即使两个驱动器具有相同的功能,其驱动程序的行为通常也不同。

我决定发起一场讨论,探讨如何使所有 Linux CD-ROM 驱动程序的行为更加统一。我首先联系了 Linux 内核中许多 CD-ROM 驱动程序的开发者。他们的反馈鼓励我编写统一 CD-ROM 驱动程序,本文档旨在描述该驱动程序。统一 CD-ROM 驱动程序的实现位于 cdrom.c 文件中。该驱动程序旨在成为一个额外的软件层,位于每个 CD-ROM 驱动器的低级设备驱动程序之上。通过添加这个额外的层,所有不同的 CD-ROM 设备都能够表现得**完全**相同(只要底层硬件允许)。

统一 CD-ROM 驱动程序的目标**不是**疏远尚未采取措施支持这项工作的驱动程序开发人员。统一 CD-ROM 驱动程序的目标是简单地为编写 CD-ROM 驱动器应用程序的人们提供**一个** Linux CD-ROM 接口,该接口对所有 CD-ROM 设备都具有一致的行为。此外,这也为低级设备驱动程序代码和 Linux 内核之间提供了一致的接口。我们注意确保与 cdrom.h 中定义的数据结构和程序员接口实现 100% 兼容。本指南旨在帮助 CD-ROM 驱动程序开发人员调整其代码以使用 cdrom.c 中定义的统一 CD-ROM 驱动程序代码。

我个人认为,最重要的硬件接口是 IDE/ATAPI 驱动器,当然还有 SCSI 驱动器,但随着硬件价格的不断下降,人们也可能拥有不止一个 CD-ROM 驱动器,可能是混合类型。重要的是这些驱动器以相同的方式运行。1994 年 12 月,最便宜的 CD-ROM 驱动器之一是 Philips cm206,一款双速专有驱动器。在我忙于为其编写 Linux 驱动程序的几个月里,专有驱动器变得过时,IDE/ATAPI 驱动器成为标准。截至本文档最后一次更新时(1997 年 11 月),甚至**找到**低于 16 倍速的 CD-ROM 驱动器也变得困难,而 24 倍速驱动器已很常见。

通过另一个软件层实现标准化

本文档构思时,所有驱动程序都直接通过自己的例程实现 CD-ROM ioctl() 调用。这导致了不同驱动程序可能忘记做重要事情的危险,例如检查用户是否向驱动程序提供了有效数据。更重要的是,这导致了行为上的差异,这一点已经讨论过。

为此,创建了统一 CD-ROM 驱动程序,以强制执行一致的 CD-ROM 驱动器行为,并为各种低级 CD-ROM 设备驱动程序提供一组通用的服务。统一 CD-ROM 驱动程序现在提供了一个额外的软件层,将 ioctl()open() 的实现与实际的硬件实现分开。请注意,这项工作很少进行会影响用户应用程序的更改。最大的变化是将各种低级 CD-ROM 驱动程序的头文件内容移动到内核的 cdrom 目录。这样做是为了帮助确保用户只看到一个 cdrom 接口,即 cdrom.h 中定义的接口。

CD-ROM 驱动器足够特殊(即与软盘或硬盘驱动器等其他块设备不同),可以定义一组通用的**CD-ROM 设备操作**,即 <cdrom-device>_dops。这些操作与经典的块设备文件操作 <block-device>_fops 不同。

统一 CD-ROM 驱动程序接口层的例程在文件 cdrom.c 中实现。在此文件中,统一 CD-ROM 驱动程序通过注册以下通用 struct file_operations 来与内核作为块设备进行接口

struct file_operations cdrom_fops = {
        NULL,                   /* lseek */
        block _read ,           /* read--general block-dev read */
        block _write,           /* write--general block-dev write */
        NULL,                   /* readdir */
        NULL,                   /* select */
        cdrom_ioctl,            /* ioctl */
        NULL,                   /* mmap */
        cdrom_open,             /* open */
        cdrom_release,          /* release */
        NULL,                   /* fsync */
        NULL,                   /* fasync */
        NULL                    /* revalidate */
};

每个活动的 CD-ROM 设备都共享这个 struct。上面声明的例程都在 cdrom.c 中实现,因为这个文件是定义和标准化所有 CD-ROM 设备的行为的地方。与各种类型 CD-ROM 硬件的实际接口仍然由各种低级 CD-ROM 设备驱动程序执行。这些例程只是实现了所有 CD-ROM(以及实际上所有可移动介质设备)共有的某些**功能**。

低级 CD-ROM 设备驱动程序的注册现在通过 cdrom.c 中的通用例程完成,而不再通过虚拟文件系统 (VFS)。cdrom.c 中实现的接口通过两个通用结构进行,这些结构包含有关驱动程序功能以及驱动程序操作的特定驱动器的信息。这些结构是:

cdrom_device_ops

此结构包含有关 CD-ROM 设备低级驱动程序的信息。此结构在概念上与设备的主编号相关联(尽管某些驱动程序可能具有不同的主编号,IDE 驱动程序就是这种情况)。

cdrom_device_info

此结构包含有关特定 CD-ROM 驱动器的信息,例如其设备名称、速度等。此结构在概念上与设备的次编号相关联。

通过调用以下函数,低级设备驱动程序将特定 CD-ROM 驱动器注册到统一 CD-ROM 驱动程序:

register_cdrom(struct cdrom_device_info * <device>_info)

设备信息结构 <device>_info 包含内核与低级 CD-ROM 设备驱动程序接口所需的所有信息。此结构中最重要的条目之一是指向低级驱动程序的 cdrom_device_ops 结构的指针。

设备操作结构 cdrom_device_ops 包含一个指向低级设备驱动程序中实现的函数的指针列表。当 cdrom.c 访问 CD-ROM 设备时,它通过此结构中的函数进行。不可能知道未来 CD-ROM 驱动器的所有功能,因此预计随着新技术的开发,此列表可能需要不时扩展。例如,CD-R 和 CD-R/W 驱动器正变得流行,很快就需要为它们添加支持。目前,当前的 struct 是:

struct cdrom_device_ops {
        int (*open)(struct cdrom_device_info *, int)
        void (*release)(struct cdrom_device_info *);
        int (*drive_status)(struct cdrom_device_info *, int);
        unsigned int (*check_events)(struct cdrom_device_info *,
                                     unsigned int, int);
        int (*media_changed)(struct cdrom_device_info *, int);
        int (*tray_move)(struct cdrom_device_info *, int);
        int (*lock_door)(struct cdrom_device_info *, int);
        int (*select_speed)(struct cdrom_device_info *, unsigned long);
        int (*get_last_session) (struct cdrom_device_info *,
                                 struct cdrom_multisession *);
        int (*get_mcn)(struct cdrom_device_info *, struct cdrom_mcn *);
        int (*reset)(struct cdrom_device_info *);
        int (*audio_ioctl)(struct cdrom_device_info *,
                           unsigned int, void *);
        const int capability;           /* capability flags */
        int (*generic_packet)(struct cdrom_device_info *,
                              struct packet_command *);
};

当低级设备驱动程序实现其中一项功能时,它应向此 struct 添加一个函数指针。但是,如果未实现特定函数,则此 struct 应包含一个 NULL。当 CD-ROM 驱动器注册到统一 CD-ROM 驱动程序时,capability 标志指定 CD-ROM 硬件和/或低级 CD-ROM 驱动程序的功能。

请注意,大多数函数的参数少于其 blkdev_fops 对应项。这是因为 inodefile 结构中的信息很少被使用。对于大多数驱动程序,主要参数是 struct cdrom_device_info,从中可以提取主编号和次编号。(不过,大多数低级 CD-ROM 驱动程序甚至不查看主编号和次编号,因为它们许多只支持一个设备。)这将通过下面描述的 cdrom_device_info 中的 dev 可用。

当前,已注册到 cdrom.c 的特定于驱动器、类似次设备的信息包含以下字段:

struct cdrom_device_info {
      const struct cdrom_device_ops * ops;    /* device operations for this major */
      struct list_head list;                  /* linked list of all device_info */
      struct gendisk * disk;                  /* matching block layer disk */
      void *  handle;                         /* driver-dependent data */

      int mask;                               /* mask of capability: disables them */
      int speed;                              /* maximum speed for reading data */
      int capacity;                           /* number of discs in a jukebox */

      unsigned int options:30;                /* options flags */
      unsigned mc_flags:2;                    /*  media-change buffer flags */
      unsigned int vfs_events;                /*  cached events for vfs path */
      unsigned int ioctl_events;              /*  cached events for ioctl path */
      int use_count;                          /*  number of times device is opened */
      char name[20];                          /*  name of the device type */

      __u8 sanyo_slot : 2;                    /*  Sanyo 3-CD changer support */
      __u8 keeplocked : 1;                    /*  CDROM_LOCKDOOR status */
      __u8 reserved : 5;                      /*  not used yet */
      int cdda_method;                        /*  see CDDA_* flags */
      __u8 last_sense;                        /*  saves last sense key */
      __u8 media_written;                     /*  dirty flag, DVD+RW bookkeeping */
      unsigned short mmc3_profile;            /*  current MMC3 profile */
      int for_data;                           /*  unknown:TBD */
      int (*exit)(struct cdrom_device_info *);/*  unknown:TBD */
      int mrw_mode_page;                      /*  which MRW mode page is in use */
};

使用此 struct,通过 next 字段构建已注册次设备的链表。设备号、设备操作结构和驱动器属性的规范存储在此结构中。

如果特定驱动器不支持驱动器的某个功能,则可以使用 mask 标志来屏蔽 ops->capability 中列出的一些功能。speed 值指定驱动器的最大磁头速率,以普通音频速度(176KB/秒原始数据或 150KB/秒文件系统数据)为单位测量。参数声明为 const,因为它们描述了驱动器的属性,这些属性在注册后不会改变。

少数寄存器包含 CD-ROM 驱动器本地的变量。options 标志用于指定通用 CD-ROM 例程的行为。这些各种标志寄存器应提供足够的灵活性以适应不同用户的需求(而**不是**低级设备驱动程序作者的“任意”需求,如旧方案中所示)。mc_flags 寄存器用于将 media_changed() 的信息缓冲到两个独立的队列中。特定于次要驱动器的其他数据可以通过 handle 访问,该 handle 可以指向特定于低级驱动程序的数据结构。字段 use_countnextoptionsmc_flags 无需初始化。

cdrom.c 形成的中间软件层将执行一些额外的簿记。设备的利用计数(打开设备的进程数)注册在 use_count 中。函数 cdrom_ioctl() 将验证读写操作的适当用户内存区域,并且在传输 CD 上的某个位置时,它将通过以标准格式向低级驱动程序发出请求,并在用户软件和低级驱动程序之间转换所有格式来“清理”格式。这大大减轻了驱动程序的内存检查、格式检查和转换工作。此外,必要的结构将在程序堆栈上声明。

函数的实现应按照以下部分中的定义进行。必须实现两个函数,即 open()release()。其他函数可以省略,它们对应的功能标志将在注册时被清除。通常,函数成功时返回零,错误时返回负值。函数调用应在命令完成后才返回,但当然等待设备不应占用处理器时间。

int open(struct cdrom_device_info *cdi, int purpose)

Open() 应尝试以特定**目的**打开设备,目的可以是:

  • 为读取数据而打开,如 mount() (2) 或用户命令 ddcat 所做。

  • ioctl 命令而打开,如音频 CD 播放程序所做。

请注意,任何策略性代码(在 open() 时关闭托盘等)都由 cdrom.c 中的调用例程完成,因此低级例程只需关注正确的初始化,例如启动光盘等。

void release(struct cdrom_device_info *cdi)

应执行设备特定的操作,例如使设备停转。然而,策略性的操作,例如弹出托盘或解锁舱门,应留给通用例程 cdrom_release()。这是唯一返回类型为 void 的函数。

int drive_status(struct cdrom_device_info *cdi, int slot_nr)

如果实现,函数 drive_status 应提供有关驱动器状态的信息(而不是光盘状态,光盘可能在驱动器中也可能不在)。如果驱动器不是换碟机,则应忽略 slot_nr。在 cdrom.h 中列出了可能性:

CDS_NO_INFO             /* no information available */
CDS_NO_DISC             /* no disc is inserted, tray is closed */
CDS_TRAY_OPEN           /* tray is opened */
CDS_DRIVE_NOT_READY     /* something is wrong, tray is moving? */
CDS_DISC_OK             /* a disc is loaded and everything is fine */
int tray_move(struct cdrom_device_info *cdi, int position)

此函数(没有其他代码)如果实现,应控制托盘的移动。(没有其他函数应该控制此功能。) 参数 position 控制期望的移动方向。

  • 0 关闭托盘

  • 1 打开托盘

此函数成功时返回 0,错误时返回非零值。请注意,如果托盘已处于所需位置,则无需采取任何操作,并且返回值应为 0。

int lock_door(struct cdrom_device_info *cdi, int lock)

此函数(而不是其他代码)控制舱门的锁定,如果驱动器允许的话。lock 的值控制期望的锁定状态:

  • 0 解锁舱门,允许手动打开

  • 1 锁定舱门,无法手动弹出托盘

此函数成功时返回 0,错误时返回非零值。请注意,如果门已处于请求状态,则无需采取任何操作,并且返回值应为 0。

int select_speed(struct cdrom_device_info *cdi, unsigned long speed)

某些 CD-ROM 驱动器能够改变其磁头速度。改变 CD-ROM 驱动器速度有几个原因。压制不良的 CD-ROM 可能会受益于低于最大磁头速率。现代 CD-ROM 驱动器可以获得非常高的磁头速率(高达 24x 很常见)。据报道,这些驱动器在这些高速下会产生读取错误,降低速度可以防止在这种情况下数据丢失。最后,其中一些驱动器会发出恼人的大噪音,较低的速度可以减少这种噪音。

此函数指定读取数据或播放音频的速度。speed 值指定驱动器的磁头速度,以标准 CD-ROM 速度(176kB/秒原始数据或 150kB/秒文件系统数据)为单位测量。因此,要请求 CD-ROM 驱动器以 300kB/秒的速度运行,您将调用 CDROM_SELECT_SPEED ioctl 并设置 speed=2。特殊值“0”表示“自动选择”,即最大数据速率或实时音频速率。如果驱动器没有此“自动选择”功能,则应根据当前加载的光盘做出决定,并且返回值应为正。负返回值表示错误。

int get_last_session(struct cdrom_device_info *cdi,
                     struct cdrom_multisession *ms_info)

此函数应实现旧的相应 ioctl()。对于设备 cdi->dev,当前光盘的最后一次会话的开始应在指针参数 ms_info 中返回。请注意,cdrom.c 中的例程已对该参数进行了净化:无论调用软件请求的格式如何,其请求的格式**始终**为 CDROM_LBA 类型(线性块寻址模式)。但净化甚至更进一步:如果低级实现愿意(当然,适当设置 ms_info->addr_format 字段),它可以以 CDROM_MSF 格式返回请求的信息,并且 cdrom.c 中的例程将在必要时进行转换。成功时返回值为 0。

int get_mcn(struct cdrom_device_info *cdi,
            struct cdrom_mcn *mcn)

一些光盘带有“媒体目录号”(MCN),也称为“通用产品代码”(UPC)。此编号应反映通常在产品条形码上找到的编号。不幸的是,少数光盘上带有此类编号的光盘甚至不使用相同的格式。此函数的返回参数是指向类型为 struct cdrom_mcn 的预声明内存区域的指针。MCN 预期为 13 个字符的字符串,以空字符终止。

int reset(struct cdrom_device_info *cdi)

此调用应执行对驱动器的硬复位(尽管在需要硬复位的情况下,驱动器很可能不再响应命令)。优选地,控制应在驱动器完成复位后才返回给调用者。如果驱动器不再响应,底层低级 cdrom 驱动器超时可能是明智之举。

int audio_ioctl(struct cdrom_device_info *cdi,
                unsigned int cmd, void *arg)

cdrom.h 中定义的一些 CD-ROM-ioctl() 可以通过上述例程实现,因此函数 cdrom_ioctl 将使用它们。然而,大多数 ioctl() 处理音频控制。我们已决定将这些操作通过单个函数访问,重复参数 cmdarg。请注意,后者是 void 类型,而不是 unsigned long int。然而,例程 cdrom_ioctl() 确实做了一些有用的事情。它将所有音频调用的地址格式类型标准化为 CDROM_MSF(分、秒、帧)。它还验证 arg 的内存位置,并为参数保留堆栈内存。这使得 audio_ioctl() 的实现比旧驱动程序方案简单得多。例如,您可以查看应根据本文档更新的 cm206.c 中的函数 cm206_audio_ioctl()

未实现的 ioctl 应返回 -ENOSYS,但无害的请求(例如,CDROMSTART)可以通过返回 0(成功)来忽略。其他错误应符合标准,无论它们是什么。当低级驱动程序返回错误时,统一 CD-ROM 驱动程序会尽可能地将错误代码返回给调用程序。(尽管我们可能决定在 cdrom_ioctl() 中清理返回值,以保证音频播放器软件的统一接口。)

int dev_ioctl(struct cdrom_device_info *cdi,
              unsigned int cmd, unsigned long arg)

有些 ioctl() 似乎是特定于某些 CD-ROM 驱动器的。也就是说,它们是为了服务某些驱动器的某些功能而引入的。事实上,有 6 种不同的 ioctl() 用于读取数据,无论是某种特定格式还是音频数据。支持将音频轨道作为数据读取的驱动器不多,我认为这是因为艺术家版权保护的原因。此外,我认为如果支持音频轨道,它应该通过 VFS 而不是通过 ioctl() 来完成。这里可能存在一个问题是音频帧长 2352 字节,因此音频文件系统要么一次请求 75264 字节(512 和 2352 的最小公倍数),要么驱动程序应克服这种不一致(对此我持反对态度)。此外,硬件很难找到精确的帧边界,因为音频帧中没有同步头。一旦这些问题得到解决,此代码应在 cdrom.c 中标准化。

因为有太多的 ioctl() 似乎是为了满足某些驱动程序而引入的 [2],所以任何非标准 ioctl() 都会通过 dev_ioctl() 调用进行路由。原则上,“私有” ioctl() 应该在其设备的主设备号之后编号,而不是通用 CD-ROM ioctl0x53。目前不支持的 ioctl() 包括:

CDROMREADMODE1, CDROMREADMODE2, CDROMREADAUDIO, CDROMREADRAW, CDROMREADCOOKED, CDROMSEEK, CDROMPLAY-BLK and CDROM-READALL

CD-ROM 功能

除了仅仅实现一些 ioctl 调用之外,cdrom.c 中的接口还提供了指示 CD-ROM 驱动器**能力**的可能性。这可以通过在注册阶段对 cdrom.h 中定义的任意数量的功能常量进行逻辑或运算来实现。目前,功能可以是以下任何一项:

CDC_CLOSE_TRAY          /* can close tray by software control */
CDC_OPEN_TRAY           /* can open tray */
CDC_LOCK                /* can lock and unlock the door */
CDC_SELECT_SPEED        /* can select speed, in units of * sim*150 ,kB/s */
CDC_SELECT_DISC         /* drive is juke-box */
CDC_MULTI_SESSION       /* can read sessions *> rm1* */
CDC_MCN                 /* can read Media Catalog Number */
CDC_MEDIA_CHANGED       /* can report if disc has changed */
CDC_PLAY_AUDIO          /* can perform audio-functions (play, pause, etc) */
CDC_RESET               /* hard reset device */
CDC_IOCTLS              /* driver has non-standard ioctls */
CDC_DRIVE_STATUS        /* driver implements drive status */

功能标志被声明为 const,以防止驱动程序意外篡改内容。功能标志实际上通知 cdrom.c 驱动程序能做什么。如果驱动程序找到的驱动器不具备该功能,可以通过 cdrom_device_info 变量 mask 将其屏蔽掉。例如,SCSI CD-ROM 驱动程序已经实现了加载和弹出 CD-ROM 的代码,因此其在 capability 中的相应标志将被设置。但是 SCSI CD-ROM 驱动器可能是一个光盘盒系统,无法加载托盘,因此对于该驱动器,cdrom_device_info 结构将设置 mask 中的 CDC_CLOSE_TRAY 位。

在文件 cdrom.c 中,您会遇到许多类似以下类型的结构:

if (cdo->capability & ~cdi->mask & CDC _<capability>) ...

没有 ioctl 来设置掩码... 原因是我认为控制**行为**比控制**能力**更好。

选项

最后一个标志寄存器控制 CD-ROM 驱动器的**行为**,以满足不同用户的需求,希望这能独立于为 Linux 社区提供驱动器支持的各自作者的想法。当前的控制行为选项有:

CDO_AUTO_CLOSE  /* try to close tray upon device open() */
CDO_AUTO_EJECT  /* try to open tray on last device close() */
CDO_USE_FFLAGS  /* use file_pointer->f_flags to indicate purpose for open() */
CDO_LOCK        /* try to lock door if device is opened */
CDO_CHECK_TYPE  /* ensure disc type is data if opened for data */

此寄存器的初始值为 CDO_AUTO_CLOSE | CDO_USE_FFLAGS | CDO_LOCK,反映了我对用户界面和软件标准的看法。在您抗议之前,cdrom.c 中实现了两个新的 ioctl(),允许您通过软件控制行为。它们是:

CDROM_SET_OPTIONS       /* set options specified in (int)arg */
CDROM_CLEAR_OPTIONS     /* clear options specified in (int)arg */

有一个选项需要更多解释:CDO_USE_FFLAGS。在下一个新章节中,我们将解释为什么需要此选项。

一个名为 setcd 的软件包,可从 Debian 发行版和 sunsite.unc.edu 获取,允许用户级别控制这些标志。

需要知道打开 CD-ROM 设备的目的

传统上,Unix 设备可以通过两种不同的“模式”使用,要么通过读/写设备文件,要么通过设备的 ioctl() 调用向设备发出控制命令。CD-ROM 驱动器的问题在于,它们可以用于两个完全不同的目的。一个是挂载可移动文件系统,即 CD-ROM,另一个是播放音频 CD。音频命令完全通过 ioctl() 实现,大概是因为第一个实现(SUN?)就是如此。原则上这没有错,但要很好地控制“CD 播放器”,要求设备**始终**可以打开,以便发出 ioctl 命令,无论驱动器处于什么状态。

另一方面,当用作可移动介质光盘驱动器(CD-ROM 的最初目的)时,我们希望确保光盘驱动器在打开设备时已准备好操作。在旧方案中,一些 CD-ROM 驱动器不进行任何完整性检查,导致在尝试将 CD-ROM 挂载到空驱动器时,VFS 向内核报告大量 I/O 错误。这不是一种特别优雅的方法来发现没有插入 CD-ROM;它或多或少看起来像旧的 IBM-PC 尝试从空的软盘驱动器读取几秒钟,之后系统抱怨无法从中读取。如今,我们可以**感知**驱动器中是否存在可移动介质,我们相信我们应该利用这个事实。在打开设备时进行完整性检查,验证 CD-ROM 的可用性及其正确类型(数据),将是可取的。

这两种使用 CD-ROM 驱动器的方式,主要是用于数据,其次是用于播放音频光盘,对 open() 调用的行为有不同的要求。音频使用只是想打开设备以获取文件句柄,该句柄是发出 ioctl 命令所必需的,而数据使用则希望打开以进行正确和可靠的数据传输。用户程序指示其打开设备**目的**的唯一方法是通过 flags 参数(参见 open(2))。对于 CD-ROM 设备,这些标志未实现(某些驱动程序实现了对写入相关标志的检查,但如果设备文件具有正确的权限标志,则这不是严格必要的)。大多数选项标志对 CD-ROM 设备来说根本没有意义:O_CREATO_NOCTTYO_TRUNCO_APPENDO_SYNC 对 CD-ROM 没有任何意义。

因此,我们建议使用标志 O_NONBLOCK 来表示设备仅为发出 ioctl 命令而打开。严格来说,O_NONBLOCK 的含义是打开设备和随后的调用不会导致调用进程等待。我们可以将其解释为不要等待直到有人插入了有效的资料 CD-ROM。因此,我们对 CD-ROM 的 open() 调用的实现建议如下:

  • 如果除了 O_RDONLY 之外没有设置其他标志,则设备将打开以进行数据传输,并且仅在成功初始化传输后返回值为 0。此调用甚至可能在 CD-ROM 上引发一些操作,例如关闭托盘。

  • 如果设置了 O_NONBLOCK 选项标志,则除非整个设备不存在,否则打开将始终成功。驱动器将不采取任何行动。

那标准呢?

您可能会犹豫是否接受这份提案,因为它来自 Linux 社区,而不是某个标准化机构。那么 SUN、SGI、HP 以及所有其他 Unix 和硬件厂商呢?嗯,这些公司处于幸运的境地,他们通常可以控制其支持产品的硬件和软件,并且足够大,可以制定自己的标准。他们不必处理十几种或更多不同的、相互竞争的硬件配置[3]

我们相信,在 Linux 社区中,使用 O_NONBLOCK 来指示设备仅用于 ioctl 命令可以很容易地引入。所有 CD 播放器作者都必须被告知,我们甚至可以向程序发送我们自己的补丁。使用 O_NONBLOCK 很可能不会影响 CD 播放器在 Linux 以外的其他操作系统上的行为。最后,用户总是可以通过调用 ioctl(file_descriptor, CDROM_CLEAR_OPTIONS, CDO_USE_FFLAGS) 来恢复旧行为。

open() 的首选策略

cdrom.c 中的例程设计成能够通过 CDROM_SET/CLEAR_OPTIONS ioctls 来实现 CD-ROM 设备(**任何**类型)的运行时行为配置。因此,可以设置各种操作模式:

CDO_AUTO_CLOSE | CDO_USE_FFLAGS | CDO_LOCK

这是默认设置。(将来,有了 CDO_CHECK_TYPE 会更好。)如果设备尚未被任何其他进程打开,并且设备正在为数据传输而打开(O_NONBLOCK 未设置),并且发现托盘是打开的,则会尝试关闭托盘。然后,验证驱动器中是否有光盘,并且如果设置了 CDO_CHECK_TYPE,则验证其是否包含“数据模式 1”类型的轨道。只有所有测试都通过,返回值才为零。门被锁定以防止文件系统损坏。如果驱动器是为音频而打开(O_NONBLOCK 已设置),则不采取任何操作,并返回 0 值。

CDO_AUTO_CLOSE | CDO_AUTO_EJECT | CDO_LOCK

这模仿了当前 sbpcd 驱动程序的行为。选项标志被忽略,如果需要,托盘在第一次打开时关闭。类似地,托盘在最后一次释放时打开,即,如果 CD-ROM 被卸载,它会自动弹出,以便用户可以更换它。

我们希望这些选项能说服所有人(驱动程序维护者和用户程序开发人员)采纳新的 CD-ROM 驱动程序方案和选项标志解释。

cdrom.c 中例程的描述

cdrom.c 中只有少数例程导出给驱动程序。在本节中,我们将讨论这些例程,以及接管 CD-ROM 与内核接口的函数。cdrom.c 对应的头文件名为 cdrom.h。以前,此文件的一些内容放在 ucdrom.h 文件中,但该文件现在已合并回 cdrom.h

struct file_operations cdrom_fops

此结构的内容在 cdrom_api 中描述。指向此结构的指针被分配给 struct gendiskfops 字段。

int register_cdrom(struct cdrom_device_info *cdi)

此函数的使用方式与向内核注册 cdrom_fops 大致相同,设备操作和信息结构,如 cdrom_api 中所述,应注册到统一 CD-ROM 驱动程序:

register_cdrom(&<device>_info);

此函数成功时返回零,失败时返回非零。结构 <device>_info 应包含指向驱动程序的 <device>_dops 的指针,如:

struct cdrom_device_info <device>_info = {
        <device>_dops;
        ...
}

请注意,一个驱动程序必须有一个静态结构 <device>_dops,而它可以有任意数量的 <device>_info 结构,只要有活动的次设备。Register_cdrom() 从这些结构构建一个链表。

void unregister_cdrom(struct cdrom_device_info *cdi)

取消注册次设备号为 MINOR(cdi->dev) 的设备 cdi 会将该次设备从列表中移除。如果它是低级驱动程序注册的最后一个次设备,则这将断开已注册的设备操作例程与 CD-ROM 接口的连接。此函数成功时返回零,失败时返回非零。

int cdrom_open(struct inode * ip, struct file * fp)

此函数不是由低级驱动程序直接调用,它列在标准 cdrom_fops 中。如果 VFS 打开文件,此函数将激活。此例程中实现了一种策略,处理设备中设置的所有功能和选项,这些功能和选项与 cdrom_device_ops 相关联。然后,程序流程转移到设备相关的 open() 调用。

void cdrom_release(struct inode *ip, struct file *fp)

此函数实现了 cdrom_open() 的逆向逻辑,然后调用设备相关的 release() 例程。当使用计数达到 0 时,通过调用 sync_dev(dev)invalidate_buffers(dev) 来刷新已分配的缓冲区。

int cdrom_ioctl(struct inode *ip, struct file *fp,
                unsigned int cmd, unsigned long arg)

此函数以统一的方式处理所有标准的 CD-ROM 设备 ioctl 请求。不同的调用分为三类:可由设备操作直接实现的 ioctl();通过 audio_ioctl() 调用路由的 ioctl();以及其余的(可能是设备相关的)ioctl()。通常,负返回值表示错误。

直接实现的 ioctl()

如果实现且未被屏蔽,以下“旧”CD-ROM ioctl() 通过直接调用 cdrom_device_ops 中的设备操作来实施:

CDROMMULTISESSION

请求 CD-ROM 上的最后一个会话。

CDROMEJECT

打开托盘。

CDROMCLOSETRAY

关闭托盘。

CDROMEJECT_SW

如果 argnot=0,则将行为设置为自动关闭(第一次打开时关闭托盘)和自动弹出(最后一次释放时弹出),否则将行为设置为在 open()release() 调用时不移动。

CDROM_GET_MCN

从 CD 获取媒体目录号。

Ioctl*s 通过 *audio_ioctl() 路由

以下一组 ioctl() 都通过调用 cdrom_fops 函数 audio_ioctl() 来实现。内存检查和分配在 cdrom_ioctl() 中执行,并且还对地址格式(CDROM_LBA/CDROM_MSF)进行净化。

CDROMSUBCHNL

获取类型为 struct cdrom_subchnl * 的参数 arg 中的子通道数据。

CDROMREADTOCHDR

读取目录表头,在类型为 struct cdrom_tochdr *arg 中。

CDROMREADTOCENTRY

读取目录表条目,在类型为 struct cdrom_tocentry *arg 中指定。

CDROMPLAYMSF

播放由类型为 struct cdrom_msf *arg 定界的分钟、秒、帧格式的音频片段。

CDROMPLAYTRKIND

播放由类型为 struct cdrom_ti *arg 定界的音轨-索引格式的音频片段。

CDROMVOLCTRL

设置由类型为 struct cdrom_volctrl *arg 指定的音量。

CDROMVOLREAD

读取音量,通过类型为 struct cdrom_volctrl *arg

CDROMSTART

启动光盘。

CDROMSTOP

停止播放音频片段。

CDROMPAUSE

暂停播放音频片段。

CDROMRESUME

恢复播放。

cdrom.c 中的新 ioctl()

以下 ioctl() 已被引入,以允许用户程序控制单个 CD-ROM 设备的行为。新的 ioctl 命令可以通过其名称中的下划线进行识别。

CDROM_SET_OPTIONS

设置由 arg 指定的选项。返回修改后的选项标志寄存器。使用 arg = rm0 读取当前标志。

CDROM_CLEAR_OPTIONS

清除由 arg 指定的选项。返回修改后的选项标志寄存器。

CDROM_SELECT_SPEED

选择光盘的磁头速率,由 arg 指定,以标准 cdrom 速度(176KB/秒原始数据或 150KB/秒文件系统数据)为单位。值为 0 表示“自动选择”,即实时播放音频光盘,以最大速度播放数据光盘。arg 的值会与 cdrom_dops 中找到的驱动器的最大磁头速率进行检查。

CDROM_SELECT_DISC

从自动点唱机中选择编号为 arg 的光盘。

第一张光盘编号为 0。arg 的值将与 cdrom_dops 中找到的自动点唱机中光盘的最大数量进行检查。

CDROM_MEDIA_CHANGED

如果自上次调用以来光盘已更改,则返回 1。对于自动点唱机,额外的参数 arg 指定了提供信息的插槽。特殊值 CDSL_CURRENT 请求返回有关当前选定插槽的信息。

CDROM_TIMED_MEDIA_CHANGE

检查自用户提供的时间以来光盘是否已更改,并返回上次光盘更改的时间。

arg 是指向 cdrom_timed_media_change_info 结构的指针。arg->last_media_change 可由调用代码设置,以表示上次已知媒体更改的时间戳(由调用者)。成功返回后,此 ioctl 调用将把 arg->last_media_change 设置为内核/驱动器知道的最新媒体更改时间戳(以毫秒为单位),如果该时间戳比调用者设置的时间戳更近,则将 arg->has_changed 设置为 1。

CDROM_DRIVE_STATUS

通过调用 drive_status() 返回驱动器状态。返回值在 cdrom_drive_status 中定义。请注意,此调用不返回有关驱动器当前播放活动的信息;这可以通过 CDROMSUBCHNLioctl 调用进行轮询。对于点唱机,额外的参数 arg 指定了(可能受限的)信息所对应的插槽。特殊值 CDSL_CURRENT 请求返回有关当前选定插槽的信息。

CDROM_DISC_STATUS

返回当前在驱动器中的光盘类型。它应被视为 CDROM_DRIVE_STATUS 的补充。此 ioctl 可以提供有关当前插入驱动器的光盘的**一些**信息。此功能以前在低级驱动程序中实现,但现在完全在统一 CD-ROM 驱动程序中执行。

CD 作为各种数字信息载体的开发历史导致了许多不同的光盘类型。此 ioctl 仅在 CD 仅包含**一种**数据类型时才有用。虽然这种情况经常发生,但 CD 上有数据轨道和音频轨道的情况也很常见。由于这是一个现有接口,而不是通过改变其创建时的假设来修复此接口,从而破坏所有使用此功能的用户应用程序,统一 CD-ROM 驱动程序实现了此 ioctl 如下:如果所讨论的 CD 上有音频轨道,并且绝对没有 CD-I、XA 或数据轨道,则会报告为 CDS_AUDIO。如果它同时有音频和数据轨道,则会返回 CDS_MIXED。如果光盘上没有音频轨道,并且所讨论的 CD 上有任何 CD-I 轨道,则会报告为 CDS_XA_2_2。如果失败,如果所讨论的 CD 上有任何 XA 轨道,则会报告为 CDS_XA_2_1。最后,如果所讨论的 CD 上有任何数据轨道,则会报告为数据 CD (CDS_DATA_1)。

ioctl 可以返回:

CDS_NO_INFO     /* no information available */
CDS_NO_DISC     /* no disc is inserted, or tray is opened */
CDS_AUDIO       /* Audio disc (2352 audio bytes/frame) */
CDS_DATA_1      /* data disc, mode 1 (2048 user bytes/frame) */
CDS_XA_2_1      /* mixed data (XA), mode 2, form 1 (2048 user bytes) */
CDS_XA_2_2      /* mixed data (XA), mode 2, form 1 (2324 user bytes) */
CDS_MIXED       /* mixed audio/data disc */

有关各种光盘类型的帧布局的一些信息,请参阅最新版本的 cdrom.h

CDROM_CHANGER_NSLOTS

返回自动点唱机中的插槽数。

CDROMRESET

重置驱动器。

CDROM_GET_CAPABILITY

返回驱动器的 capability 标志。有关这些标志的更多信息,请参阅 cdrom_capabilities 部分。

CDROM_LOCKDOOR

锁定驱动器舱门。arg == 0 解锁舱门,其他任何值锁定舱门。

CDROM_DEBUG

开启调试信息。只有 root 允许这样做。与 CDROM_LOCKDOOR 语义相同。

设备相关的 ioctl()

最后,所有其他 ioctl() 都(如果已实现)传递给函数 dev_ioctl()。不执行内存分配或验证。

如何更新您的驱动程序

  • 备份您当前的驱动程序。

  • 获取 cdrom.ccdrom.h 文件,它们应位于本文档随附的目录树中。

  • 确保您包含 cdrom.h

  • register_blkdev 的第三个参数从 &<your-drive>_fops 更改为 &cdrom_fops

  • 紧接着那一行,添加以下代码以注册到统一 CD-ROM 驱动程序:

    register_cdrom(&<your-drive>_info);*
    

    同样,在适当位置添加对 unregister_cdrom() 的调用。

  • 将设备操作 struct 的示例复制到您的源代码中,例如从 cm206.ccm206_dops 中复制,并将所有条目更改为与您的驱动程序对应的名称,或者您喜欢的名称。如果您的驱动程序不支持某个功能,则将该条目设置为 NULL。在 capability 条目中,您应该列出您的驱动程序当前支持的所有功能。如果您的驱动程序具有未列出的功能,请给我发送消息。

  • 从相同的示例驱动程序中复制 cdrom_device_info 声明,并根据您的需求修改条目。如果您的驱动程序动态确定硬件功能,则此结构也应动态声明。

  • 根据 cdrom.h 中列出的原型和 cdrom_api 中给出的规范,在您的 <device>_dops 结构中实现所有函数。您很可能已经实现了大部分代码,并且您几乎肯定需要调整原型和返回值。

  • 将您的 <device>_ioctl() 函数重命名为 audio_ioctl 并稍微更改原型。删除 cdrom_ioctl 第一部分中列出的条目,如果您的代码正常,这些只是对您在上一步中调整的例程的调用。

  • 您可以删除 audio_ioctl() 函数中所有剩余的处理音频命令的内存检查代码(这些在 cdrom_ioctl 的第二部分列出)。也不需要内存分配,因此 switch 语句中的大多数 case*s 看起来与以下类似:

    case CDROMREADTOCENTRY:
            get_toc_entry\bigl((struct cdrom_tocentry *) arg);
    
  • 所有剩余的 ioctl 情况必须移到一个单独的函数 <device>_ioctl 中,即设备相关的 ioctl()。请注意,内存检查和分配必须保留在此代码中!

  • 更改 <device>_open()<device>_release() 的原型,并移除任何策略性代码(即托盘移动、门锁定等)。

  • 尝试重新编译驱动程序。我们建议您同时使用 cdrom.o 和您的驱动程序的模块,因为这样调试更容易。

鸣谢

感谢所有参与人员。首先,Erik Andersen,他接过了维护 cdrom.c 并将许多 CD-ROM 相关代码集成到 2.1 内核中的火炬。感谢 Scott Snyder 和 Gerd Knorr,他们是第一个为 SCSI 和 IDE-CD 驱动程序实现此接口并为内核 2.0 相关数据结构的扩展添加了许多想法。进一步感谢 Heiko Eißfeldt、Thomas Quinot、Jon Tombs、Ken Pizzini、Eberhard Mönkeberg 和 Andrew Kroll,这些 Linux CD-ROM 设备驱动程序开发人员在编写过程中慷慨地提供了建议和批评。最后,当然,我要感谢 Linus Torvalds,他首先使这一切成为可能。