\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| 中保存模块的函数的地址,该函数执行读操作。这里代码是看起%
这意味着更清晰,并且你应当清楚,任何没有明确指定的该数据结构的成员,都将会被 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| 这个特定的锁,来保证文件位置的更新成为互斥,因此,我们可安全地%
另外,自从 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} 中%
核空间函数中。另外,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| 函数来完成注册,该函数的定义%
\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| 中。这是因为内核不关心主设备号;%
-只有我们的驱动用它。
+å\8fªæ\9c\89æ\88\91们ç\9a\84驱å\8a¨ç¨\8båº\8fç\94¨å®\83ã\80\82
现在的问题是,如何在不劫持已在使用的主设备号的情况下,获得主设备号?最简单的方%
法是查看 \src{Documentation/admin-guide/devices.txt} 文件,并从文档中找出一个%
-未被使用的主设备号。这是做事的坏的方法,因为你不能确保被你选中的主设备号,是否%
-会在之后被分配给其它设备。问题的答案是,你可以向内核提出请求,以让内核为你分配%
-一个动态主设备号。
-
-如果你向 \cpp|register_chrdev| 函数传递\,0\,作为主设备号,函数的返回值将是一个%
-动态分配的主设备号。这样做的缺点是,你不能享受,因使用创建设备文件的软件工具而%
-获得的便利,因为你不会知识为设备分配的主设备号是什么。这里有许多不同的方法来做%
-这事。首先,驱动自身可以打印被新分配的数字,并且我们可手动创建该驱动的设备文件。%
-其次,新注册的设备,将在文件 \verb|/proc/devices| 文件中有一个相应的记录,我们%
-即可以手动来创建设备文件,或写一个 shell 脚本文件来从该文件中读取主设备号,并%
-å\88©ç\94¨è¿\99个主设å¤\87å\8f·æ\9d¥å\88\9b建设å¤\87æ\96\87ä»¶ã\80\82å\85¶ä¸\89æ\98¯ï¼\8cæ\88\91们å\8f¯ä»¥æ³¨å\86\8c设å¤\87æ\88\90å\8a\9få\90\8eï¼\8c使ç\94¨å\86\85æ ¸å\87½æ\95° %
-\cpp|device_create| 创建设备文件,并且在调用 \cpp|cleanup_module| 函数时,使用%
-内核函数 \cpp|device_destroy| 来移除设备文件。
-
-然而,\cpp|register_chrdev()| 函数将占用一系列被分配的主设备号相关的次设备号。%
+未被使用的主设备号。这是做这事的一个坏的方法,因为你不能确保被你选中的主设备号,%
+是否会在之后被分配给其它设备。问题的答案,是你可以向内核提出请求,以让内核为你%
+分配一个动态主设备号。
+
+如果你向 \cpp|register_chrdev| 函数传递\,\verb|0|\,作为主设备号,函数的返回值将%
+是一个动态分配的主设备号。这样做的缺点是,你不能享受,因使用创建设备文件的软件%
+工具而获得的便利,因为你不会知道为设备分配的主设备号是什么。这里有许多不同的方%
+法来做这件事。首先,驱动自身可以打印被新分配的数字,并且我们可手动创建该驱动的%
+设备文件。其次,新注册的设备,将在文件 \verb|/proc/devices| 文件中有一个相应的%
+记录,我们既可以手动来创建设备文件,也可以写一个 shell 脚本文件来从该文件中读%
+å\8f\96主设å¤\87å\8f·ï¼\8cå¹¶å\88©ç\94¨è¿\99个主设å¤\87å\8f·æ\9d¥å\88\9b建设å¤\87æ\96\87ä»¶ã\80\82第ä¸\89æ\8b\9bï¼\8cæ\88\91们å\9c¨å\8f¯ä»¥æ³¨å\86\8c设å¤\87æ\88\90å\8a\9få\90\8eï¼\8c%
+使用内核函数 \cpp|device_create| 创建设备文件,并且在调用 \cpp|cleanup_module| %
+å\87½æ\95°æ\97¶ï¼\8c使ç\94¨å\86\85æ ¸å\87½æ\95° \cpp|device_destroy| æ\9d¥ç§»é\99¤è®¾å¤\87æ\96\87ä»¶ã\80\82
+
+ç\84¶è\80\8cï¼\8c\cpp|register_chrdev()| å\87½æ\95°å°\86å\8d ç\94¨ä¸\8eä¸\80ç³»å\88\97被å\88\86é\85\8dç\9a\84主设å¤\87å\8f·ç\9b¸å\85³ç\9a\84次设å¤\87å\8f·ã\80\82%
推荐的方法是使用 cdev 接口,来减少字符设备注册时产生的浪费。
较新的接口通过两个不同的步骤完成字符设备注册。首先,我们应该注册一段设备号,%
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();
\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)|: 增加当前模块的引用计数值。
\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|),但驱动程序捕获这些写入尝试,并%
告诉用户该操作是不被支持的。如果你看不到我们如何处理读入缓冲区的数据,请不要担%
心;我们对此没做太多的事情。我们只是读入数据并打印一条消息来确认我们收到了它。
们用原子化 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| %