The release Method
The role of the release method is the reverse of open. Sometimes you'll find that the method implementation is called device_close instead of device_release. Either way, the device method should perform the following tasks: 释放方法的作用与打开相反。 有时你会发现方法实现被称为 device_close 而不是 device_release。 无论哪种方式,设备方法都应该执行以下任务:
Deallocate anything that open allocated in filp->private_data解除分配在 filp->private_data 中打开的所有内容
Shut down the device on last close在最后一次关闭时关闭设备
The basic form of scull has no hardware to shut down, so the code required is minimal:[7] scull 的基本形式没有要关闭的硬件,因此所需的代码很少:[7]
int scull_release(struct inode *inode, struct file *filp) {
return 0;
}
You may be wondering what happens when a device file is closed more times than it is opened. After all, the dup and fork system calls create copies of open files without calling open; each of those copies is then closed at program termination. For example, most programs don't open their stdin file (or device), but all of them end up closing it. How does a driver know when an open device file has really been closed? 您可能想知道当设备文件关闭的次数多于打开的次数时会发生什么。 毕竟,dup 和 fork 系统调用创建打开文件的副本而不调用 open; 然后在程序终止时关闭这些副本中的每一个。 例如,大多数程序不会打开它们的标准输入文件(或设备),但最终都会关闭它。 驱动程序如何知道打开的设备文件何时真正关闭?
The answer is simple: not every close system call causes the release method to be invoked. Only the calls that actually release the device data structure invoke the method—hence its name. The kernel keeps a counter of how many times a file structure is being used. Neither fork nor dup creates a new file structure (only open does that); they just increment the counter in the existing structure. The close system call executes the release method only when the counter for the file structure drops to 0, which happens when the structure is destroyed. This relationship between the release method and the close system call guarantees that your driver sees only one release call for each open. 答案很简单:不是每个 close 系统调用都会调用 release 方法。 只有真正释放设备数据结构的调用才会调用该方法——因此得名。 内核会记录一个文件结构被使用了多少次。 fork 和 dup 都不会创建新的文件结构(只有 open 会这样做); 他们只是增加现有结构中的计数器。 只有当文件结构的计数器下降到 0 时,close 系统调用才会执行 release 方法,这发生在结构被破坏时。 释放方法和关闭系统调用之间的这种关系保证了您的驱动程序在每次打开时只看到一个释放调用。
Note that the flush method is called every time an application calls close. However, very few drivers implement flush, because usually there's nothing to perform at close time unless release is involved. 请注意,每次应用程序调用 close 时都会调用 flush 方法。 然而,很少有驱动程序实现flush,因为除非涉及release,否则通常没有什么可以在close时执行。
As you may imagine, the previous discussion applies even when the application terminates without explicitly closing its open files: the kernel automatically closes any file at process exit time by internally using the close system call. 正如您可能想象的那样,即使应用程序在没有显式关闭其打开的文件的情况下终止,前面的讨论也适用:内核通过内部使用 close 系统调用在进程退出时自动关闭任何文件。
scull's Memory Usage
Before introducing the read and write operations, we'd better look at how and why scull performs memory allocation. "How" is needed to thoroughly understand the code, and "why" demonstrates the kind of choices a driver writer needs to make, although scull is definitely not typical as a device. 在介绍读写操作之前,我们最好先看看 scull 是如何以及为什么执行内存分配的。 需要“如何”来彻底理解代码,“为什么”说明驱动程序编写者需要做出的选择,尽管 scull 绝对不是典型的设备。
This section deals only with the memory allocation policy in scull and doesn't show the hardware management skills you need to write real drivers. These skills are introduced in Chapter 9 and Chapter 10. Therefore, you can skip this section if you're not interested in understanding the inner workings of the memory-oriented scull driver. 本节仅涉及 scull 中的内存分配策略,不展示编写真正驱动程序所需的硬件管理技能。 这些技巧在第 9 章和第 10 章中介绍。因此,如果您对了解面向内存的 scull 驱动程序的内部工作不感兴趣,可以跳过本节。
The region of memory used by scull, also called a device, is variable in length. The more you write, the more it grows; trimming is performed by overwriting the device with a shorter file. scull 使用的内存区域(也称为设备)的长度是可变的。 你写得越多,它就增长得越多; 修剪是通过用较短的文件覆盖设备来执行的。
The scull driver introduces two core functions used to manage memory in the Linux kernel. These functions, defined in <linux/slab.h>, are: scull 驱动程序引入了两个用于在 Linux 内核中管理内存的核心函数。 这些在 <linux/slab.h> 中定义的函数是:
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
A call to kmalloc attempts to allocate size bytes of memory; the return value is a pointer to that memory or NULL if the allocation fails. The flags argument is used to describe how the memory should be allocated; we examine those flags in detail in Chapter 8. For now, we always use GFP_KERNEL. Allocated memory should be freed with kfree. You should never pass anything to kfree that was not obtained from kmalloc. It is, however, legal to pass a NULL pointer to kfree. 对 kmalloc 的调用尝试分配 size 字节的内存; 返回值是指向该内存的指针,如果分配失败,则返回 NULL。 flags 参数用于描述应该如何分配内存; 我们将在第 8 章详细检查这些标志。目前,我们总是使用 GFP_KERNEL。 分配的内存应该用 kfree 释放。 你不应该将任何不是从 kmalloc 获得的东西传递给 kfree。 但是,将 NULL 指针传递给 kfree 是合法的。
kmalloc is not the most efficient way to allocate large areas of memory (see Chapter 8), so the implementation chosen for scull is not a particularly smart one. The source code for a smart implementation would be more difficult to read, and the aim of this section is to show read and write, not memory management. That's why the code just uses kmalloc and kfree without resorting to allocation of whole pages, although that approach would be more efficient. kmalloc 不是分配大块内存的最有效方式(参见第 8 章),因此为 scull 选择的实现并不是特别聪明。 智能实现的源代码会更难阅读,本节的目的是展示读写,而不是内存管理。 这就是为什么代码只使用 kmalloc 和 kfree 而不诉诸整个页面的分配,尽管这种方法会更有效。
On the flip side, we didn't want to limit the size of the "device" area, for both a philosophical reason and a practical one. Philosophically, it's always a bad idea to put arbitrary limits on data items being managed. Practically, scull can be used to temporarily eat up your system's memory in order to run tests under low-memory conditions. Running such tests might help you understand the system's internals. You can use the command cp /dev/zero /dev/scull0 to eat all the real RAM with scull, and you can use the dd utility to choose how much data is copied to the scull device. 另一方面,出于哲学和实际原因,我们不想限制“设备”区域的大小。 从哲学上讲,任意限制要管理的数据项总是一个坏主意。 实际上, scull 可用于暂时消耗系统内存,以便在低内存条件下运行测试。 运行此类测试可能会帮助您了解系统的内部结构。 您可以使用命令 cp /dev/zero /dev/scull0 用 scull 吃掉所有真实 RAM,并且可以使用 dd 实用程序来选择将多少数据复制到 scull 设备。
In scull, each device is a linked list of pointers, each of which points to a scull_dev structure. Each such structure can refer, by default, to at most four million bytes, through an array of intermediate pointers. The released source uses an array of 1000 pointers to areas of 4000 bytes. We call each memory area a quantum and the array (or its length) a quantum set. A scull device and its memory areas are shown in Figure 3-1. 在 scull 中,每个设备都是一个指针链表,每个指针指向一个 scull_dev 结构。 默认情况下,每个这样的结构可以通过中间指针数组引用最多四百万字节。 发布的源代码使用一个包含 1000 个指针的数组,指向 4000 字节的区域。 我们称每个内存区域为一个量子,而数组(或其长度)为一个量子集。 scull 设备及其内存区域如图 3-1 所示。
Figure 3-1. The layout of a scull device
The chosen numbers are such that writing a single byte in scull consumes 8000 or 12,000 thousand bytes of memory: 4000 for the quantum and 4000 or 8000 for the quantum set (according to whether a pointer is represented in 32 bits or 64 bits on the target platform). If, instead, you write a huge amount of data, the overhead of the linked list is not too bad. There is only one list element for every four megabytes of data, and the maximum size of the device is limited by the computer's memory size. 选择的数字使得在 scull 中写入单个字节会消耗 8000 或 12,000,000 字节的内存:4000 用于量子,4000 或 8000 用于量子集(根据指针在目标上是用 32 位还是 64 位表示) 平台)。 相反,如果您写入大量数据,则链表的开销并不算太糟糕。 每四兆字节的数据只有一个列表元素,设备的最大大小受计算机内存大小的限制。
Choosing the appropriate values for the quantum and the quantum set is a question of policy, rather than mechanism, and the optimal sizes depend on how the device is used. Thus, the scull driver should not force the use of any particular values for the quantum and quantum set sizes. In scull, the user can change the values in charge in several ways: by changing the macros SCULL_QUANTUM and SCULL_QSET in scull.h at compile time, by setting the integer values scull_quantum and scull_qset at module load time, or by changing both the current and default values using ioctl at runtime. 为量子和量子集选择合适的值是一个策略问题,而不是机制问题,最佳尺寸取决于设备的使用方式。 因此,scull 驱动程序不应强制对量程和量程集大小使用任何特定值。 在 scull 中,用户可以通过多种方式更改负责的值:通过在编译时更改 scull.h 中的宏 SCULL_QUANTUM 和 SCULL_QSET,通过在模块加载时设置整数值 scull_quantum 和 scull_qset,或者通过同时更改当前和 在运行时使用 ioctl 的默认值。
Using a macro and an integer value to allow both compile-time and load-time configuration is reminiscent of how the major number is selected. We use this technique for whatever value in the driver is arbitrary or related to policy. 使用宏和整数值来允许编译时和加载时配置让人想起如何选择主编号。 我们将这种技术用于驱动程序中任意或与策略相关的任何值。
The only question left is how the default numbers have been chosen. In this particular case, the problem is finding the best balance between the waste of memory resulting from half-filled quanta and quantum sets and the overhead of allocation, deallocation, and pointer chaining that occurs if quanta and sets are small. Additionally, the internal design of kmalloc should be taken into account. (We won't pursue the point now, though; the innards of kmalloc are explored in Chapter 8.) The choice of default numbers comes from the assumption that massive amounts of data are likely to be written to scull while testing it, although normal use of the device will most likely transfer just a few kilobytes of data. 剩下的唯一问题是如何选择默认数字。 在这种特殊情况下,问题是在由半填充的量子和量子集导致的内存浪费与如果量子和集合很小时发生的分配、释放和指针链接的开销之间找到最佳平衡。 此外,还应考虑 kmalloc 的内部设计。 (不过,我们现在不会追究这一点;kmalloc 的内部结构将在第 8 章中进行探讨。)默认数字的选择来自这样一种假设,即在测试时可能会写入大量数据来 scull,尽管这是正常的 使用该设备很可能只传输几千字节的数据。
We have already seen the scull_dev structure that represents our device internally. That structure's quantum and qset fields hold the device's quantum and quantum set sizes, respectively. The actual data, however, is tracked by a different structure, which we call struct scull_qset : 我们已经看到了在内部代表我们设备的 scull_dev 结构。 该结构的quantum 和qset 字段分别保存设备的quantum 和quantum set 大小。 然而,实际数据由不同的结构跟踪,我们称之为 struct scull_qset :
struct scull_qset {
void **data;
struct scull_qset *next;
};
The next code fragment shows in practice how struct scull_dev and struct scull_qset are used to hold data. The function scull_trim is in charge of freeing the whole data area and is invoked by scull_open when the file is opened for writing. It simply walks through the list and frees any quantum and quantum set it finds. 下一个代码片段在实践中展示了如何使用 struct scull_dev 和 struct scull_qset 来保存数据。 函数 scull_trim 负责释放整个数据区域,并在打开文件进行写入时由 scull_open 调用。 它只是遍历列表并释放它找到的任何量子和量子集。
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next, *dptr;
int qset = dev->qset; /* "dev" is not-null */
int i;
for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
if (dptr->data) {
for (i = 0; i < qset; i++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}
scull_trim is also used in the module cleanup function to return memory used by scull to the system. scull_trim 还用于模块清理函数中,将 scull 使用的内存返回给系统。