From: Felix Lee Date: Tue, 19 Sep 2023 15:10:34 +0000 (+0800) Subject: fix typo until end of character driver chapter X-Git-Url: https://www.ivnss.com/gitweb/?a=commitdiff_plain;h=38b0f5c1270ae63009d42b3f5e413022107a11e8;p=lkmpgcn fix typo until end of character driver chapter --- diff --git a/lkmpg_cn.tex b/lkmpg_cn.tex index b07d877..b32fb7c 100644 --- a/lkmpg_cn.tex +++ b/lkmpg_cn.tex @@ -1004,9 +1004,10 @@ brw-rw---- 1 root disk 8, 16 Jan 3 09:02 /dev/sdb \label{sec:chardev} \section{file\_operations 结构} \label{sec:file_operations} -数据结构 \cpp|file_operations| 代码在 \src{include/linux/fs.h} 中被定义,并且保% -存指向驱动程序所定义函数的指针,这些函数在设备上执行各种操作。该结构的每个字段% -都对应于驱动程序定义的某个函数的地址,用于处理请求的操作。 +数据结构 \cpp|file_operations| 代码在 \src{include/linux/fs.h} 中被定义,并且该% +结构体拥有指向驱动程序所定义函数的指针,这些函数在设备上执行各种操作。该结构的% +每个字段都对应于驱动程序所定义某个函数的地址,这里提到的函数,用于处理一个被请% +求的操作。 例如,每个每个字符设备驱动需要定义一个从设备读取数据的函数。数据结构 % \cpp|file_operations| 中保存模块的函数的地址,该函数执行读操作。这里代码是看起% @@ -1086,10 +1087,9 @@ struct file_operations fops = { 这意味着更清晰,并且你应当清楚,任何没有明确指定的该数据结构的成员,都将会被 gcc % 初始化为 \cpp|NULL| 值。 - 数据结构 \cpp|struct file_operations| 的一个实例包含指向不同函数的不同指针,这% -些函数被用来 \cpp|read|, \cpp|write|, \cpp|open|,\ldots{} 等等系统调用,通% -常被命名为 \cpp|fops|。 +些函数被用来执行 \cpp|read|, \cpp|write|, \cpp|open|,\ldots{} 等等系统调用,% +这些系统调用通常被命名为 \cpp|fops|。 自从 Linux v3.14 开始,read,write,与 seek 操作都被担保是线程安全的,这是通过% 使用 \cpp|f_pos| 这个特定的锁,来保证文件位置的更新成为互斥,因此,我们可安全地% @@ -1097,9 +1097,9 @@ struct file_operations fops = { 另外,自从 Linux v5.6 开始,当注册 proc 处理程序时,\cpp|proc_ops| 数据结构被引% 入,并用来替换对于数据结构 \cpp|file_operations| 的使用。 在 \ref{sec:proc_ops} % -小节以了解更信息。. +节以了解更信息。 -\section{file 数据结构} +\section{file\,数据结构} \label{sec:file_struct} 每个设备在内核中通过一个 file 数据结构来被表示,在 \src{include/linux/fs.h} 中% @@ -1108,20 +1108,20 @@ struct file_operations fops = { 核空间函数中。另外,file 数据结构的名字有些令人误解;它表示一个抽象,打开`file‘,% 而不是一个在磁盘上的文件,磁盘上的文件,通过命名为 \cpp|inode| 的数据结构表示。 -一个 file 结构的实例通常被命名为 \cpp|filp|。你还会看到它被称为结构文件对象。不% -要被迷惑。 +一个 file 结构的实例通常被命名为 \cpp|filp|。你还会看到它被称为一个结构文件对象。% +不要被迷惑。 继续看一下 file 的定义。你看到的大多数条目,例如结构 dentry 不被设备驱动程序使% -用,你可以忽略它们。这是因为驱动程序不直接填充 file;它们只使用在其它地方创建的% -file 中的结构体。 +用,你可以忽略它们。这是因为驱动程序不直接填充 file 结构;它们只使用在其它地方% +创建的 file 中的结构体。 \section{注册一个设备} \label{sec:register_device} -如同在先前所讨论的,字符设备通过设备文件来被访问,通常设备文件位于 \verb|/dev| % +如同在先前所讨论的,字符设备通过设备文件而被访问,通常设备文件位于 \verb|/dev| % 目录中。这是遵从约定。当写一个驱动时,将设备文件放到你当前目录中是可以的。对于% 产品级设备驱动,要确保设备文件放置在 \verb|/dev| 目录中。主设备号告诉你什么驱动% -程序处理什么设备文件。次设备号仅由驱动程序本身用来区分其操作的不同设备,这只在% -驱动处理多个设备时用到。 +程序处理什么设备文件。次设备号仅由驱动程序自身用来区分其操作的不同设备,此情况只% +在驱动程序处理多个设备时用到。 添加一个驱动到你的系统,意味着在内核中注册该驱动。这等同于在模块的初始化阶段分% 配一个主设备号。你通过使用 \cpp|register_chrdev| 函数来完成注册,该函数的定义% @@ -1132,29 +1132,29 @@ int register_chrdev(unsigned int major, const char *name, struct file_operations \end{code} 在这里 \verb|unsigned int major| 是你想请求的主设备号,\cpp|const char *name| % -是将在 \verb|/proc/devices| 文件中,作为设备名显示的名称,并且数据结构 % +是将在 \verb|/proc/devices| 文件中,作为设备名称来显示的名称,并且数据结构 % \cpp|file_operatins *fops| 是为你的驱动,提供的一个指向 \cpp|file_operations| % -表的指针。\cpp|register_chrdev| 函数返回一个负值,意味着注册过程的失败。注意我% +表的指针。\cpp|register_chrdev| 函数返回一个负值,意味着注册过程失败。注意我% 们没有传递主设备号到函数 \cpp|register_chrdev| 中。这是因为内核不关心主设备号;% -只有我们的驱动用它。 +只有我们的驱动程序用它。 现在的问题是,如何在不劫持已在使用的主设备号的情况下,获得主设备号?最简单的方% 法是查看 \src{Documentation/admin-guide/devices.txt} 文件,并从文档中找出一个% -未被使用的主设备号。这是做事的坏的方法,因为你不能确保被你选中的主设备号,是否% -会在之后被分配给其它设备。问题的答案是,你可以向内核提出请求,以让内核为你分配% -一个动态主设备号。 - -如果你向 \cpp|register_chrdev| 函数传递\,0\,作为主设备号,函数的返回值将是一个% -动态分配的主设备号。这样做的缺点是,你不能享受,因使用创建设备文件的软件工具而% -获得的便利,因为你不会知识为设备分配的主设备号是什么。这里有许多不同的方法来做% -这事。首先,驱动自身可以打印被新分配的数字,并且我们可手动创建该驱动的设备文件。% -其次,新注册的设备,将在文件 \verb|/proc/devices| 文件中有一个相应的记录,我们% -即可以手动来创建设备文件,或写一个 shell 脚本文件来从该文件中读取主设备号,并% -利用这个主设备号来创建设备文件。其三是,我们可以注册设备成功后,使用内核函数 % -\cpp|device_create| 创建设备文件,并且在调用 \cpp|cleanup_module| 函数时,使用% -内核函数 \cpp|device_destroy| 来移除设备文件。 - -然而,\cpp|register_chrdev()| 函数将占用一系列被分配的主设备号相关的次设备号。% +未被使用的主设备号。这是做这事的一个坏的方法,因为你不能确保被你选中的主设备号,% +是否会在之后被分配给其它设备。问题的答案,是你可以向内核提出请求,以让内核为你% +分配一个动态主设备号。 + +如果你向 \cpp|register_chrdev| 函数传递\,\verb|0|\,作为主设备号,函数的返回值将% +是一个动态分配的主设备号。这样做的缺点是,你不能享受,因使用创建设备文件的软件% +工具而获得的便利,因为你不会知道为设备分配的主设备号是什么。这里有许多不同的方% +法来做这件事。首先,驱动自身可以打印被新分配的数字,并且我们可手动创建该驱动的% +设备文件。其次,新注册的设备,将在文件 \verb|/proc/devices| 文件中有一个相应的% +记录,我们既可以手动来创建设备文件,也可以写一个 shell 脚本文件来从该文件中读% +取主设备号,并利用这个主设备号来创建设备文件。第三招,我们在可以注册设备成功后,% +使用内核函数 \cpp|device_create| 创建设备文件,并且在调用 \cpp|cleanup_module| % +函数时,使用内核函数 \cpp|device_destroy| 来移除设备文件。 + +然而,\cpp|register_chrdev()| 函数将占用与一系列被分配的主设备号相关的次设备号。% 推荐的方法是使用 cdev 接口,来减少字符设备注册时产生的浪费。 较新的接口通过两个不同的步骤完成字符设备注册。首先,我们应该注册一段设备号,% @@ -1166,13 +1166,13 @@ int register_chrdev_region(dev_t from, unsigned count, const char *name); int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); \end{code} -在两个不同函数之间选择依赖于你是否知道你设备的主设备号。如果你已经知道设备的主% -设备号时,用 \cpp|register_chrdev_region| 函数,如果你更喜欢拥有一个由内核动态% -分配的主设备号时,使用 \cpp|alloc_chrdev_region|。 +在两个不同函数之间选择,依赖于你是否知道设备的主设备号。如果你已经知道设备的主% +设备号,用 \cpp|register_chrdev_region| 函数,如果你更喜欢拥有一个由内核动态% +分配的主设备号,使用 \cpp|alloc_chrdev_region|。 再者,我们应当为我们的字符设备,初始化数据结构 \cpp|struct cdev|,并将该数据结% -构与设备号相关联。要初始化 \cpp|struct cdev|,我们可以如下一系列相似的代码来% -实现。 +构与设备号相关联。要初始化 \cpp|struct cdev|,我们可如下构建一系列相似的代码% +来实现。 \begin{code} struct cdev *my_dev = cdev_alloc(); @@ -1180,41 +1180,39 @@ my_cdev->ops = &my_fops; \end{code} 然而,常见的使用模式将嵌入 \cpp|struct cdev| 到你拥有的一个设备指定结构中。在这% -种情况下,我们将需要 \cpp|cdev_init| 来实现初始化。 +种情况下,我们将需要 \cpp|cdev_init| 函数来实现初始化。 \begin{code} void cdev_init(struct cdev *cdev, const struct file_operations *fops); \end{code} -一旦我们结束初始化过程,我们可以通过使用 \cpp|cdev_add| 函数添加 \verb|char| 设% -备到系统。 +一旦我们结束初始化过程,我们可以通过使用 \cpp|cdev_add| 函数,添加 \verb|char| % +设备到系统。 \begin{code} int cdev_add(struct cdev *p, dev_t dev, unsigned count); \end{code} -要找到一个使用该接口示例,你可以查看在 \ref{sec:device_files} 小节中被说明的 % -\verb|ioctl|。 +要找到一个使用该接口示例,你可以查看在 \ref{sec:device_files} 节中被说明的 \verb|ioctl|。 \section{注销一个设备} \label{sec:unregister_device} -每当 root 用想移除模块时,我们不能允许内核从内核中移除。如果设备文件被一个进程% -打开,并且我们移除内核模块,使用该文件将导致调用适当的函数(read/write)所在的内% -存位置。如果我们幸运,没有其它的代码被加载到内存位置,并且我们将得到奇怪的错误% -信息。如果我们不幸运,其它的内核模块被加载到相同的内存位置,这意味着跳入内核中% -其它函数的中间。这个后果将是不可能预知的,但这种影响不是很正面。 - -通常,当你不想允许一些行为,你从被提供完成任务的函数返回一个错误码(一个负数)。 - +无论 root 用户是否喜欢,我们都不允许内核模块被移除。如果设备文件被一个进程% +打开,然后我们移除该内核模块,使用该文件将导致调用适当被用函数(read/write)所在% +的内存位置。如果我们幸运,没有其它的代码被加载到该内存位置,并且我们将得到奇怪% +的错误信息。如果我们不幸运,其它的内核模块被加载到相同的内存位置,这意味着跳入% +内核中其它函数的中间。这个后果将是不可能预知的,但这种影响不是很正面。 +通常,当你不想允许一些行为,你从被提供完成任务的函数返回一个错误码(一个负数)。% 上述返回值的处理方法,对于 \cpp|cleanup_module| 函数是不可能的,因为它是一个返% -回值为空值函数。但是,有一个计数器可以跟踪有多少进程正在使用你的模块。通过查看% -使用命令 \sh|cat /proc/modules| 或 \sh|sudo lsmod| 输出结果的第三个字段的值。% -如果这个值不是零,\sh|rmmod| 将会失败。注意在 \cpp|cleanup_module| 函数中你不必% -检查记数器的值,因为相应的检查,在你通过系统调用 \cpp|sys_delete_module| 时,而% -被执行。刚刚提到的系统调用,在 \src{include/linux/syscalls.h} 中被定义。你不应% -直接使用这个记数器,但在 \src{include/linux/module.h} 中定义了相关函数,这些函% -数可以让你增加,减少,与显示这个计数器: +回值为空的函数。但是,有一个计数器可以跟踪有多少进程正在使用你的模块。通过查看% +使用命令 \sh|cat /proc/modules| 或 \sh|sudo lsmod| 输出结果的第三个字段的值,来 +得知有多少个进程目前在使用该内核模块,如果这个值不是零,\sh|rmmod| 将会失败。注% +意在 \cpp|cleanup_module| 函数中你不必检查记数器的值,因为相应的检查,你在通过% +系统调用 \cpp|sys_delete_module| 时,而被执行。刚刚提到的系统调用,在 % +\src{include/linux/syscalls.h} 中被定义。你不应直接使用这个记数器,但在 % +\src{include/linux/module.h} 中定义了相关函数,这些函数可以让你增加,减少,与显% +示这个计数器: \begin{itemize} \item \cpp|try_module_get(THIS_MODULE)|: 增加当前模块的引用计数值。 @@ -1223,19 +1221,19 @@ int cdev_add(struct cdev *p, dev_t dev, unsigned count); \end{itemize} 保持计数器精确是非常重要的;如果你确实丢失了对正确使用次数的跟踪,你将决不可能% -卸载模块;伙记们,这就是重启的时候。要模块开发的过程中,这种情况迟早会发生在你% +卸载模块;伙记们,这就是重启的时候。在模块开发的过程中,这种情况迟早会发生在你% 的身上。 \section{chardev.c} \label{sec:chardev_c} -下面的代码示例创建一个被命名为 \verb|chardev| 的字符类设备驱动。 -你可转储其设备文件的内容。 +下面的代码示例创建一个被命名为 \verb|chardev| 的字符类设备驱动。你可转储其设备% +文件的内容。 \begin{codebash} cat /proc/devices \end{codebash} -(或使用一个程序打开文件)并且驱动程序将设备文件被读取的次数放放设备文件中。我们% +(或使用一个程序打开文件)并且驱动程序将设备文件被读取的次数放在设备文件中。我们% 不支持写入文件(如 \sh|echo "hi" > /dev/hello|),但驱动程序捕获这些写入尝试,并% 告诉用户该操作是不被支持的。如果你看不到我们如何处理读入缓冲区的数据,请不要担% 心;我们对此没做太多的事情。我们只是读入数据并打印一条消息来确认我们收到了它。 @@ -1245,16 +1243,16 @@ cat /proc/devices 们用原子化 Compare-And-Swap (CAS) 来保持状态,原子化的 \cpp|CDEV_NOT_USED| 与原% 子化的 \cpp|CDEV_EXCLUSIVE_OPEN| 来决定当前文件是否被其它人打开或没有被打开。% CAS 用一个希望的值比较一个内存位置的内容,只有两者相同时,才能更改那个内存位置% -的内容到需要的值。在 \ref{sec:synchronization} 小节查看更多并发性的细节信息。 +的内容到需要的值。在 \ref{sec:synchronization} 节查看更多并发性的细节信息。 \samplec{examples/chardev.c} \section{为多个内核版本编写模块} \label{sec:modules_for_versions} 系统调用,是内核向进程展示的主要接口,各个版本之间通常保持不变。一个新的系统调% -用可以被添加,但旧的会表现得和以前一模一样。这对于向后兼容是必要的---一个新的内% -核版本不应该破坏常规进程。在大多数情况下,设备文件也将保持不变。另一方面,内核中% -的内部接口可以并且确实在版本之间发生变化。 +用可以被添加,但旧的会表现得和以前一模一样。这对于向后兼容是必要的------一个新% +的内核版本不应该破坏常规进程。在大多数情况下,设备文件也将保持不变。另一方面,% +内核中的内部接口可以,并且确实在版本之间发生变化。 不同的内核版本之间存在差异,如果你想支持多个内核版本,你会发现自己必须编写条件编% 译指令。这样做的方法是比较宏 \cpp|LINUX_VERSION_CODE| 和 \cpp|KERNEL_VERSION| %