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 Sun 等)可用的硬件设备种类繁多
操作系统的开放式设计,使得任何人都可以为 Linux 编写驱动程序。
周围有大量的源代码,作为如何编写驱动程序的示例。
Linux 的开放性以及可用硬件的多种类型使得 Linux 可以支持许多不同的硬件设备。不幸的是,正是这种开放性使得 Linux 能够支持所有这些不同的设备,也使得每个设备驱动程序的行为与另一个设备驱动程序显著不同。对于 CD-ROM 设备而言,这种行为的差异非常显著;特定的驱动程序对标准 ioctl() 调用的反应因驱动程序而异。为了避免驱动程序完全不一致,Linux CD-ROM 驱动程序的编写者通常通过理解、复制然后更改现有的驱动程序来创建新的设备驱动程序。不幸的是,这种做法并没有在所有 Linux CD-ROM 驱动程序中保持一致的行为。
本文档描述了一项旨在在所有不同的 Linux CD-ROM 设备驱动程序中建立统一行为的努力。本文档还定义了各种 ioctl() 及其底层 CD-ROM 设备驱动程序应如何实现它们。目前(截至 Linux 2.1.x 开发内核),一些底层 CD-ROM 设备驱动程序,包括 IDE/ATAPI 和 SCSI,现在都使用此统一接口。
在开发 CD-ROM 时,CD-ROM 驱动器和计算机之间的接口并未在标准中指定。因此,开发了许多不同的 CD-ROM 接口。其中一些具有自己的专有设计(索尼、三洋、松下、飞利浦),其他制造商采用了现有的电气接口并更改了功能(CreativeLabs/SoundBlaster、Teac、Funai),或者只是将其驱动器适配到一个或多个现有电气接口(Aztech、三洋、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 驱动器之一是飞利浦 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 对应项少。这是因为 inode 和 file 结构体中的信息使用得很少。对于大多数驱动程序,主要参数是 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_count、next、options 和 mc_flags 不需要初始化。
cdrom.c 形成的中间软件层将执行一些额外的簿记工作。设备的使用计数(打开设备的进程数)记录在 use_count 中。函数 cdrom_ioctl() 将验证用于读取和写入的适当用户内存区域,并且如果传输 CD 上的位置,它将通过以标准格式向底层驱动程序发出请求来 清理 格式,并在用户软件和底层驱动程序之间转换所有格式。这减轻了驱动程序的大部分内存检查、格式检查和转换。此外,必要的结构体将在程序堆栈上声明。
函数的实现应如下节中所定义。必须实现两个函数,即 open() 和 release()。其他函数可以省略,它们的相应功能标志将在注册时清除。通常,函数在成功时返回零,在错误时返回负数。函数调用应仅在命令完成后返回,但当然等待设备不应占用处理器时间。
int open(struct cdrom_device_info *cdi, int purpose)
Open() 应该尝试为特定 purpose 打开设备,它可以是:
打开以读取数据,如 mount() (2) 或用户命令 dd 或 cat 所做的那样。
打开以进行 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 的值指定驱动器的磁头速度,以标准 cdrom 速度(176kB/秒原始数据或 150kB/秒文件系统数据)为单位衡量。因此,要请求 CD-ROM 驱动器以 300kB/秒的速度运行,您可以使用 speed=2 调用 CDROM_SELECT_SPEED ioctl。特殊值 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() 处理音频控制。我们已决定将这些访问留给单个函数,重复参数 cmd 和 arg。请注意,后者是 void 类型,而不是 unsigned long int。例程 cdrom_ioctl() 确实会做一些有用的事情。它将所有音频调用的地址格式类型清理为 CDROM_MSF(分、秒、帧)。它还会验证 arg 的内存位置,并为参数保留堆栈内存。这使得 audio_ioctl() 的实现比旧的驱动程序方案简单得多。例如,您可以查看应该使用此文档更新的函数 cm206_audio_ioctl() cm206.c。
未实现的 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() 进行路由。原则上,private ioctl() 应该在设备的major number之后进行编号,而不是通用的 CD-ROM ioctl number,0x53。目前不支持的 ioctl() 有
CDROMREADMODE1、CDROMREADMODE2、CDROMREADAUDIO、CDROMREADRAW、CDROMREADCOOKED、CDROMSEEK、CDROMPLAY-BLK 和 CDROM-READALL
有没有软件实际使用这些?我很感兴趣!
CD-ROM 功能¶
在 cdrom.c 中的接口不仅仅是实现一些 ioctl 调用,还提供了指示 CD-ROM 驱动器功能的可能性。这可以通过在注册阶段对 cdrom.h 中定义的任意数量的功能常量进行 OR 运算来完成。目前,功能可以是以下任意一种
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_CREAT、O_NOCTTY、O_TRUNC、O_APPEND 和 O_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]。
顺便说一句,我认为 SUN 的 CD-ROM 挂载方法从根本上来说非常好:在 Solaris 下,卷守护程序会自动将新插入的 CD-ROM 挂载到 /cdrom/*<卷名>* 下。
我认为他们应该进一步推动这一点,使本地网络上的每个 CD-ROM 都挂载在类似的位置,也就是说,无论您将 CD-ROM 插入哪台特定的机器,它都会始终出现在每个系统目录树中的相同位置。当我想为 Linux 实现这样一个用户程序时,我遇到了各种驱动程序行为的差异,以及需要一个 ioctl 来通知媒体更改。
我们认为,在 Linux 社区中可以轻松引入使用 O_NONBLOCK 来指示设备仅为了 ioctl 命令而打开。所有 CD 播放器的作者都必须被告知,我们甚至可以向程序发送我们自己的补丁。使用 O_NONBLOCK 最有可能不会影响其他操作系统(而不是 Linux)上 CD 播放器的行为。最后,用户始终可以通过调用 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 gendisk 的 fops 字段。
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 获取媒体目录编号。
通过 *audio_ioctl() 路由的 Ioctl*s¶
以下一组 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 中指定且由 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
选择光盘的磁头速率,由以标准 CD-ROM 速度(176 kB/秒原始数据或 150 kB/秒文件系统数据)为单位的 arg 指定。 值 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 中定义。 请注意,此调用不返回有关驱动器当前播放活动的信息; 这可以通过对 CDROMSUBCHNL 的 ioctl 调用来轮询。 对于自动换盘机,附加参数 arg 指定提供(可能有限)信息的插槽。 特殊值 CDSL_CURRENT 请求返回有关当前选定插槽的信息。
- CDROM_DISC_STATUS
返回驱动器中当前光盘的类型。 它应视为对 CDROM_DRIVE_STATUS 的补充。 此 ioctl 可以提供有关插入驱动器的当前光盘的某些信息。 此功能以前在低级驱动程序中实现,但现在完全在统一 CD-ROM 驱动程序中执行。
CD 作为各种数字信息的载体介质的使用发展历史导致了许多不同的光盘类型。 仅当 CD 上只有一种数据类型时,此 ioctl 才有用。 虽然这种情况很常见,但 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.c 和 cdrom.h,它们应该在本文档随附的目录树中。
确保包含 cdrom.h。
将 register_blkdev 的第三个参数从 &<your-drive>_fops 更改为 &cdrom_fops。
在该行之后,添加以下内容以向统一 CD-ROM 驱动程序注册
register_cdrom(&<your-drive>_info);*
同样,在适当的位置添加对 unregister_cdrom() 的调用。
将设备操作 struct 的示例复制到您的源文件中,例如从 cm206.c 中的 cm206_dops 复制,并将所有条目更改为与您的驱动程序对应的名称,或者您喜欢的名称。 如果您的驱动程序不支持某个函数,则将条目设置为 NULL。 在条目 capability 中,您应该列出您的驱动程序当前支持的所有功能。 如果您的驱动程序具有未列出的功能,请给我发送消息。
从同一示例驱动程序复制 cdrom_device_info 声明,并根据您的需要修改条目。 如果您的驱动程序动态确定硬件的功能,则也应该动态声明此结构。
根据 cdrom.h 中列出的原型以及 cdrom_api 中给出的规范,在您的 <device>_dops 结构中实现所有函数。 您很可能已经在很大程度上实现了代码,并且几乎肯定需要调整原型和返回值。
将你的 <device>_ioctl() 函数重命名为 audio_ioctl,并稍微修改其原型。删除 cdrom_ioctl 第一部分中列出的条目,如果你的代码没问题,这些只是对你在上一步中适配的例程的调用。
你可以删除 audio_ioctl() 函数中所有处理音频命令的剩余内存检查代码(这些代码列在 cdrom_ioctl 的第二部分)。也不需要进行内存分配,因此 switch 语句中的大多数 case 看起来类似于这样:
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 让这一切成为可能。