第 9 章. 编写 FreeBSD 设备驱动程序

9.1. 简介

本章简要介绍了为 FreeBSD 编写设备驱动程序。在此上下文中,设备主要指属于系统的硬件相关内容,例如磁盘、打印机或带有键盘的图形显示器。设备驱动程序是操作系统控制特定设备的软件组件。还有一些所谓的伪设备,其中设备驱动程序在软件中模拟设备的行为,而没有特定的底层硬件。设备驱动程序可以静态编译到系统中,也可以通过动态内核链接器设施 `kld` 按需加载。

在类 UNIX® 操作系统中,大多数设备通过设备节点访问,有时也称为特殊文件。这些文件通常位于文件系统层次结构中的 /dev 目录下。

设备驱动程序大致可以分为两类:字符设备和网络设备驱动程序。

9.2. 动态内核链接器设施 - KLD

kld 接口允许系统管理员动态地向运行的系统添加和删除功能。这允许设备驱动程序编写者将他们的新更改加载到运行的内核中,而无需不断地重启以测试更改。

kld 接口通过以下命令使用:

  • kldload - 加载新的内核模块

  • kldunload - 卸载内核模块

  • kldstat - 列出已加载的模块

内核模块的骨架布局

/*
 * KLD Skeleton
 * Inspired by Andrew Reiter's Daemonnews article
 */

#include <sys/types.h>
#include <sys/systm.h>  /* uprintf */
#include <sys/errno.h>
#include <sys/param.h>  /* defines used in kernel.h */
#include <sys/module.h>
#include <sys/kernel.h> /* types used in module initialization */

/*
 * Load handler that deals with the loading and unloading of a KLD.
 */

static int
skel_loader(struct module *m, int what, void *arg)
{
	int err = 0;

	switch (what) {
	case MOD_LOAD:                /* kldload */
		uprintf("Skeleton KLD loaded.\n");
		break;
	case MOD_UNLOAD:
		uprintf("Skeleton KLD unloaded.\n");
		break;
	default:
		err = EOPNOTSUPP;
		break;
	}
	return(err);
}

/* Declare this module to the rest of the kernel */

static moduledata_t skel_mod = {
	"skel",
	skel_loader,
	NULL
};

DECLARE_MODULE(skeleton, skel_mod, SI_SUB_KLD, SI_ORDER_ANY);

9.2.1. Makefile

FreeBSD 提供了一个系统 makefile 来简化内核模块的编译。

SRCS=skeleton.c
KMOD=skeleton

.include <bsd.kmod.mk>

使用此 makefile 运行 make 将创建一个文件 skeleton.ko,可以通过键入以下命令将其加载到内核中:

# kldload -v ./skeleton.ko

9.3. 字符设备

字符设备驱动程序是直接将数据传输到用户进程或从用户进程传输数据的驱动程序。这是最常见的设备驱动程序类型,源代码树中有很多简单的示例。

这个简单的示例伪设备会记住写入它的所有值,然后在读取时回显它们。

示例 1. FreeBSD 10.X - 12.X 的示例回显伪设备驱动程序
/*
 * Simple Echo pseudo-device KLD
 *
 * Murray Stokely
 * Søren (Xride) Straarup
 * Eitan Adler
 */

#include <sys/types.h>
#include <sys/systm.h>  /* uprintf */
#include <sys/param.h>  /* defines used in kernel.h */
#include <sys/module.h>
#include <sys/kernel.h> /* types used in module initialization */
#include <sys/conf.h>   /* cdevsw struct */
#include <sys/uio.h>    /* uio struct */
#include <sys/malloc.h>

#define BUFFERSIZE 255

/* Function prototypes */
static d_open_t      echo_open;
static d_close_t     echo_close;
static d_read_t      echo_read;
static d_write_t     echo_write;

/* Character device entry points */
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",
};

struct s_echo {
	char msg[BUFFERSIZE + 1];
	int len;
};

/* vars */
static struct cdev *echo_dev;
static struct s_echo *echomsg;

MALLOC_DECLARE(M_ECHOBUF);
MALLOC_DEFINE(M_ECHOBUF, "echobuffer", "buffer for echo module");

/*
 * This function is called by the kld[un]load(2) system calls to
 * determine what actions to take when a module is loaded or unloaded.
 */
static int
echo_loader(struct module *m __unused, int what, void *arg __unused)
{
	int error = 0;

	switch (what) {
	case MOD_LOAD:                /* kldload */
		error = make_dev_p(MAKEDEV_CHECKNAME | MAKEDEV_WAITOK,
		    &echo_dev,
		    &echo_cdevsw,
		    0,
		    UID_ROOT,
		    GID_WHEEL,
		    0600,
		    "echo");
		if (error != 0)
			break;

		echomsg = malloc(sizeof(*echomsg), M_ECHOBUF, M_WAITOK |
		    M_ZERO);
		printf("Echo device loaded.\n");
		break;
	case MOD_UNLOAD:
		destroy_dev(echo_dev);
		free(echomsg, M_ECHOBUF);
		printf("Echo device unloaded.\n");
		break;
	default:
		error = EOPNOTSUPP;
		break;
	}
	return (error);
}

static int
echo_open(struct cdev *dev __unused, int oflags __unused, int devtype __unused,
    struct thread *td __unused)
{
	int error = 0;

	uprintf("Opened device \"echo\" successfully.\n");
	return (error);
}

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

	uprintf("Closing device \"echo\".\n");
	return (0);
}

/*
 * The read function just takes the buf that was saved via
 * echo_write() and returns it to userland for accessing.
 * uio(9)
 */
static int
echo_read(struct cdev *dev __unused, struct uio *uio, int ioflag __unused)
{
	size_t amt;
	int error;

	/*
	 * How big is this read operation?  Either as big as the user wants,
	 * or as big as the remaining data.  Note that the 'len' does not
	 * include the trailing null character.
	 */
	amt = MIN(uio->uio_resid, uio->uio_offset >= echomsg->len + 1 ? 0 :
	    echomsg->len + 1 - uio->uio_offset);

	if ((error = uiomove(echomsg->msg, amt, uio)) != 0)
		uprintf("uiomove failed!\n");

	return (error);
}

/*
 * echo_write takes in a character string and saves it
 * to buf for later accessing.
 */
static int
echo_write(struct cdev *dev __unused, struct uio *uio, int ioflag __unused)
{
	size_t amt;
	int error;

	/*
	 * We either write from the beginning or are appending -- do
	 * not allow random access.
	 */
	if (uio->uio_offset != 0 && (uio->uio_offset != echomsg->len))
		return (EINVAL);

	/* This is a new message, reset length */
	if (uio->uio_offset == 0)
		echomsg->len = 0;

	/* Copy the string in from user memory to kernel memory */
	amt = MIN(uio->uio_resid, (BUFFERSIZE - echomsg->len));

	error = uiomove(echomsg->msg + uio->uio_offset, amt, uio);

	/* Now we need to null terminate and record the length */
	echomsg->len = uio->uio_offset;
	echomsg->msg[echomsg->len] = 0;

	if (error != 0)
		uprintf("Write failed: bad address!\n");
	return (error);
}

DEV_MODULE(echo, echo_loader, NULL);

加载此驱动程序后,尝试:

# echo -n "Test Data" > /dev/echo
# cat /dev/echo
Opened device "echo" successfully.
Test Data
Closing device "echo".

下一章将介绍真实的硬件设备。

9.4. 块设备 (已过时)

其他 UNIX® 系统可能支持第二种类型的磁盘设备,称为块设备。块设备是内核提供缓存的磁盘设备。这种缓存使得块设备几乎无法使用,或者至少很不稳定。缓存会重新排序写操作的顺序,剥夺应用程序了解任何时刻磁盘内容的准确性的能力。

这使得对磁盘数据结构(文件系统、数据库等)进行可预测且可靠的崩溃恢复变得不可能。由于写入可能被延迟,内核无法向应用程序报告哪个特定写入操作遇到写入错误,这进一步加剧了一致性问题。

因此,没有一个严肃的应用程序依赖于块设备,事实上,几乎所有直接访问磁盘的应用程序都非常谨慎地指定应该始终使用字符(或“原始”)设备。由于对每个磁盘(分区)到两个具有不同语义的设备的别名实现复杂化了相关的内核代码,FreeBSD 在磁盘 I/O 基础设施现代化过程中放弃了对缓存磁盘设备的支持。

9.5. 网络驱动程序

网络设备的驱动程序不使用设备节点来进行访问。它们的选择基于内核内部的其他决策,并且不需要调用 open(),而是通过使用系统调用 socket(2) 来使用网络设备。

有关更多信息,请参阅 ifnet(9),它是环回设备的来源,以及 Bill Paul 的网络驱动程序。


最后修改时间:2024 年 3 月 9 日,作者:Danilo G. Baio