Skip to content

Latest commit

 

History

History
718 lines (512 loc) · 32.9 KB

File metadata and controls

718 lines (512 loc) · 32.9 KB

三、使用字符驱动

设备驱动是特殊代码(在内核空间中运行),它将物理设备与系统接口,并使用定义良好的应用编程接口将其导出到用户空间进程,即通过在特殊文件上实现一些系统调用**。这是因为,在类似 Unix 的操作系统中,一切都是文件,物理设备被表示为特殊文件(通常放在/dev目录中),每一个都连接到特定的设备(因此,例如,键盘可以是名为/dev/input0的文件,串口可以是名为/dev/ttyS1的文件,实时时钟可以是/dev/rtc2)。**

We can expect that network devices belong to a particular set of devices not respecting this rule because we have no /dev/eth0 file for the eth0 interface. This is true, since network devices are the only devices class that doesn't respect this rule because network-related applications don't care about individual network interfaces; they work at a higher level by referring sockets instead. That's why Linux doesn't provide direct access to network devices, as for other devices classes.

看下一张图,我们看到内核空间是用来把硬件抽象到用户空间,这样每个进程都使用同一个接口来访问外设,这个接口是由一组系统调用组成的:

该图还显示,不仅可以通过使用设备驱动,还可以通过使用另一个接口(如 sysfs 或通过实现用户空间驱动)来访问外设。

由于我们的外围设备只是(特殊的)文件,我们的驱动应该实现我们需要的系统调用来操作这些文件,尤其是那些对交换数据有用的文件。比如我们需要open()close()系统调用来启动和停止与外设的通信,需要read()write()系统调用来与之交换数据。

普通 C 函数和系统调用的主要区别只是后者主要在内核中执行,而函数只在用户空间中执行。比如printf()是函数,write()是系统调用。后者(除了 C 函数的序言和结尾部分)在内核空间中执行,而前者主要在用户空间中执行,即使在 and 处,它调用write()将其数据实际写入输出流(这是因为所有输入/输出数据流无论如何都必须通过内核)。

欲了解更多信息,请查看本书:https://prod . packtpub . com/hardware-and-creative/gnulinux-rapid-embedded-programming

嗯,本章将向我们展示如何至少实现open()close()read()write()系统调用,以便介绍设备驱动编程和字符驱动开发的第一步。

现在是时候写我们的第一个设备驱动了!在本章中,我们将从一个非常简单的字符(或字符)驱动开始,以涵盖以下食谱:

  • 创建最简单的字符驱动
  • 与字符驱动交换数据
  • 使用“一切都是文件”抽象

技术要求

在本章中,我们将需要我们在第 1 章安装开发系统第 2 章内核内部窥视中使用的任何东西,因此请参考它们进行交叉编译、内核模块加载和管理等。

有关本章的更多信息,请阅读附录

本章使用的代码和其他文件可以在https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 03下载。

创建最简单的字符驱动

在 Linux 内核中,存在三种主要的设备类型——char 设备、block 设备和 net 设备。当然,我们有三种主要的设备驱动类型;即 char、block 和 net 驱动。在本章中,我们将了解一种字符设备,它是一种可以作为字节流访问的外设,例如串行端口、音频设备等。然而,在这个食谱中,我们将展示一个真正基本的字符驱动,它只是简单地注册自己,除此之外什么也不做。即使看起来没用,我们也会发现这一步真的引入了很多新概念!

Actually, it could be possible to exchange data between peripherals and user space without a char, block, or net driver but by simply using some mechanism offered by the sysfs, but this is a special case and it is generally used only for very simple devices that have to exchange simple data types.

准备好

为了实现我们的第一个字符驱动,我们需要上一章中介绍的模块。这是因为使用内核模块是我们向内核空间注入代码的最简单方法。当然,我们可以决定将内置的驱动编译到内核中,但是,以这种方式,我们必须完全重新编译内核,并在每次修改代码时重新启动系统(这是一种可能性,但绝对不是最好的!).

Just a note before carrying on: to provide a clearer explanation regarding how a char driver works and to present a really simple example, I decided to use the legacy way to register a char driver into the kernel. There's nothing to be concerned about, since this mode of operation is perfectly legal and still supported and, in any case, in the Using a device tree to describe a character driver recipe, in Chapter 4, Using the Device Tre**e, I'm going to present the currently advised way of registering char drivers.

怎么做...

让我们看看来自 GitHub 来源的chrdev_legacy.c文件。我们有了第一个驱动,所以让我们开始详细检查它:

  1. 首先,我们来看看文件的开头:
#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>

/* Device major umber */
static int major;
  1. chrdev_legacy.c结束时,检查以下代码,其中模块的init()功能定义如下:
static int __init chrdev_init(void)
{
    int ret;

    ret = register_chrdev(0, "chrdev", &chrdev_fops);
    if (ret < 0) {
        pr_err("unable to register char device! Error %d\n", ret);
        return ret;
    }
    major = ret;
    pr_info("got major %d\n", major);

    return 0;
}

该模块的exit()功能如下:

static void __exit chrdev_exit(void)
{
    unregister_chrdev(major, "chrdev");
}

module_init(chrdev_init);
module_exit(chrdev_exit);
  1. 如果major号是从用户空间进入内核的驱动引用,文件操作结构(由chrdev_fops引用)代表我们可以在我们的驱动上执行的唯一允许的系统调用,它们的定义如下:
static struct file_operations chrdev_fops = {
    .owner    = THIS_MODULE,
    .read     = chrdev_read,
    .write    = chrdev_write,
    .open     = chrdev_open,
    .release  = chrdev_release
};
  1. 方法基本上如下实现。以下是read()write()方法:
static ssize_t chrdev_read(struct file *filp,
                           char __user *buf, size_t count,
                           loff_t *ppos)
{
    pr_info("return EOF\n");

    return 0;
}

static ssize_t chrdev_write(struct file *filp,
                            const char __user *buf, size_t count,
                            loff_t *ppos)
{
    pr_info("got %ld bytes\n", count);

    return count;
}

这里有open()release()(又名close())方法:

static int chrdev_open(struct inode *inode, struct file *filp)
{
    pr_info("chrdev opened\n");

    return 0;
}

static int chrdev_release(struct inode *inode, struct file *filp)
{
    pr_info("chrdev released\n");

    return 0;
}
  1. 要编译代码,我们可以在主机上以通常的方式进行,如下所示:
$ make KERNEL_DIR=../../../linux/
make -C ../../../linux/ \
            ARCH=arm64 \
            CROSS_COMPILE=aarch64-linux-gnu- \
            SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy modules
make[1]: Entering directory '/home/giometti/Projects/ldddc/linux'
  CC [M] /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.mod.o
  LD [M] /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.ko
make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux'
  1. 然后,为了测试我们的驱动,我们可以将其加载到我们的目标系统中(同样,我们可以使用scp命令将模块文件加载到 ESPRESSObin 中):
# insmod chrdev_legacy.ko 
chrdev_legacy: loading out-of-tree module taints kernel.
chrdev_legacy:chrdev_init: got major 239

好的。驱动已加载,我们的主要编号为239

  1. 最后,我建议你看看 ESPRESSObin 上的/proc/devices文件。这个特殊的文件是在有人读取它时动态生成的,它保存了注册到系统中的所有字符(和块)驱动;这就是为什么如果我们用grep命令过滤它,我们会发现如下内容:
# grep chrdev /proc/devices 
239 chrdev

Of course, your major number can be a different number! There's nothing strange about that; just rewrite the next commands according to the number you get.

  1. 为了在我们的驱动上有效地执行一些系统调用,我们可以使用chrdev_test.c文件中存储的程序(仍然来自 GitHub 来源);其main()功能的开始如下所示:
int main(int argc, char *argv[])
{
    int fd;
    char buf[] = "DUMMY DATA";
    int n, c;
    int ret;

    if (argc < 2) {
        fprintf(stderr, "usage: %s <dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    ret = open(argv[1], O_RDWR);
    if (ret < 0) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    printf("file %s opened\n", argv[1]);
    fd = ret;
  1. 首先,我们需要打开文件设备,然后获取文件描述符;这可以通过使用open()系统调用来完成。

  2. 然后,main()功能继续,如下所示,在设备中写入数据:

    for (c = 0; c < sizeof(buf); c += n) {
        ret = write(fd, buf + c, sizeof(buf) - c);
        if (ret < 0) {
            perror("write");
            exit(EXIT_FAILURE);
        }
        n = ret;

        printf("wrote %d bytes into file %s\n", n, argv[1]);
        dump("data written are: ", buf + c, n);
    }

通过读取刚刚写入的数据:

    for (c = 0; c < sizeof(buf); c += n) {
        ret = read(fd, buf, sizeof(buf));
        if (ret == 0) { 
            printf("read EOF\n");
            break;
        } else if (ret < 0) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        n = ret;

        printf("read %d bytes from file %s\n", n, argv[1]);
        dump("data read are: ", buf, n);
    }

设备打开后,我们的程序执行write(),然后是read()系统调用。

We should notice that I call read() and write() system calls inside a for() loop; the reason behind this implementation will be clearer in the following recipe, Exchanging data with a char driver, where we're going to see how these system calls actually work.

  1. 最后,main()可以关闭文件设备然后退出:
    close(fd);

    return 0;
}

通过这种方式,我们可以测试我们之前实现的系统调用。

它是如何工作的...

第 1 步中,可以看到,它和我们上一章介绍的内核模块非常相似,即使有一些新的include文件。然而,最重要的新条目是major变量,为了理解它有什么用,我们应该直接到文件的末尾,在那里我们找到真正的字符驱动注册。

在第 2 步中,我们再次拥有module_init()module_exit()功能和宏,如MODULE_LICENSE()(参见第 2 章内核内部的一瞥,使用内核模块的方法);然而,这里真正重要的是chrdev_init()chrdev_exit()功能的有效发挥。实际上,chrdev_init()调用register_chrdev()函数,反过来,该函数将新的字符驱动注册到系统中,将其标记为chrdev,并将提供的chrdev_fops用作文件操作,同时将返回值存储到主变量中。

我们应该考虑这个事实,因为在没有返回错误的情况下,major是我们新驱动在系统中的主要参考!事实上,内核仅通过使用其主号来区分一个字符驱动和另一个字符驱动(这就是为什么我们保存它,然后在chrdev_exit()函数中将其用作unregister_chrdev()的参数)。

第 3 步中,每个字段指向一个定义良好的函数,该函数反过来实现系统调用体。这里唯一的非功能字段是owner,只是用来指向模块的所有者,与驱动无关,只指向内核模块管理系统。

第 4 步中,通过前面代码的方式,我们的字符驱动通过使用四种方法实现了四个系统调用:open()close()(称为release())、read()write(),它们是我们可以定义到字符驱动中的非常小(且简单)的系统调用集。

注意,在这个时候,所有的方法根本什么都不做!当我们在驱动上发出read()系统调用时,chrdev_read()方法在内核空间的驱动内部被正确调用(为了理解如何与用户空间交换数据,请参见下一节)。

I use both function and method names interchangeably because all of these functions can be seen as methods in object programming, where the same function names specialize into different steps according to the object they are applied to. With drivers it is the same: for example, they all have a read() method, but this method's behavior changes according to the object (or peripheral) it is applied to.

步骤 6 中,loading out-of-tree module taints kernel消息再次只是一个警告,可以安全忽略;然而,请注意,模块文件名是chrdev_legacy.ko,而司机的名字只是chrdev

还有更多...

我们可以验证我们的新驱动是如何工作的,所以让我们编译存储在我们之前看到的chrdev_test.c文件中的程序。为此,我们可以使用 ESPRESSObin 上的下一个命令:

# make CFLAGS="-Wall -O2" chrdev_test
cc -Wall -O2 chrdev_test.c -o chrdev_test

If not yet installed, both the make and gcc commands can be easily installed into your ESPRESSObin, just using the usual apt command apt install make gcc (after the ESPRESSObin has been connected to the internet!).

现在我们可以通过执行它来尝试:

# ./chrdev_test 
usage: ./chrdev_test <dev>

没错。这是我们必须使用的文件名。我们总是说我们的设备是 Unix 操作系统中的文件,但是是哪个文件呢?要生成这个文件,也就是代表我们驱动的文件,我们必须使用mknod命令,如下所示:

# mknod chrdev c 239 0

For further information regarding the mknod command, you can take a look at its man pages by using the command line man mknod. Usually mknod created files are located in the /dev directory; however, they can be created wherever we wish and this is just an example to show how the mechanism works.

前面的命令在当前目录中创建了一个名为chrdev的文件,它是一个特殊的文件,类型为字符(或无缓冲),有一个主编号239(当然,这是我们司机的主编号,如步骤 1 中所见)和一个次编号0

At this time, we still haven't introduced minor numbers however, you should consider them as just a simple extra parameter that the kernel simply passes to the driver without changing it. It's the driver itself that knows how to manage the minor number.

事实上,如果我们使用ls命令来检查它,我们会看到以下内容:

# ls -l chrdev
crw-r--r-- 1 root root 239, 0 Feb 7 14:30 chrdev

这里,首字符c指出这个chrdev文件不是一个普通文件(由-字符表示),而是一个字符设备文件。

好的。现在我们已经将文件连接到了驱动上,让我们在上面尝试我们的测试程序。

我们在终端上获得以下输出:

# ./chrdev_test chrdev
file chrdev opened
wrote 11 bytes into file chrdev
data written are: 44 55 4d 4d 59 20 44 41 54 41 00 
read EOF

但是,在串行控制台(或通过dmesg)上,我们得到以下内容:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: got 11 bytes
chrdev_legacy:chrdev_read: return EOF
chrdev_legacy:chrdev_release: chrdev released

这正是我们所期待的!如步骤 4 所述,这里我们可以验证驱动中定义的所有系统调用open()close()(称为release())、read()write()都是通过调用相应的方法有效执行的。

Note that, if you execute the chrdev_test program directly on the serial console, all of the preceding messages will overlap each other and you may not easily recognize them! So, let me suggest you use a SSH connection to execute the test.

请参见

与字符驱动交换数据

在这个食谱中,我们将看到如何根据read()write()系统调用行为向驱动读写数据。

准备好

为了修改我们的第一个字符驱动,以允许它在用户空间之间交换数据,我们仍然可以在前面的配方中使用的模块上工作。

怎么做...

为了与我们的新驱动交换数据,我们需要根据前面所说的修改read()write()方法,并且我们必须添加一个数据缓冲区,在那里可以存储交换的数据:

  1. 因此,让我们修改我们的文件chrdev_legacy.c,如下所示,以便包含linux/uaccess.h文件并定义我们的内部缓冲区:
#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

/* Device major umber */
static int major;

/* Device data */
#define BUF_LEN 300
static char chrdev_buf[BUF_LEN];
  1. 那么chrdev_read()方法应该修改如下:
static ssize_t chrdev_read(struct file *filp,
                char __user *buf, size_t count, loff_t *ppos)
{
    int ret;

    pr_info("should read %ld bytes (*ppos=%lld)\n", 
                                     count, *ppos);

    /* Check for end-of-buffer */
    if (*ppos + count >= BUF_LEN)
        count = BUF_LEN - *ppos;

    /* Return data to the user space */
    ret = copy_to_user(buf, chrdev_buf + *ppos, count);
    if (ret < 0)
        return -EFAULT;

    *ppos += count;
    pr_info("return %ld bytes (*ppos=%lld)\n", count, *ppos);

    return count;
}

All of the preceding modifications and the next ones in this section can be easily applied by using the modify_read_write_to_chrdev_legacy.patch patch file from GitHub sources, issuing the following command line in the same directory where the chrdev_legacy.c file is located: $ patch -p3 < modify_read_write_to_chrdev_legacy.patch

  1. 我们可以对chrdev_write()方法重复这个步骤:
static ssize_t chrdev_write(struct file *filp,
             const char __user *buf, size_t count, loff_t *ppos)
{
    int ret;

    pr_info("should write %ld bytes (*ppos=%lld)\n", count, *ppos);

    /* Check for end-of-buffer */
    if (*ppos + count >= BUF_LEN)
        count = BUF_LEN - *ppos;

    /* Get data from the user space */
    ret = copy_from_user(chrdev_buf + *ppos, buf, count);
    if (ret < 0)
        return -EFAULT;

    *ppos += count;
    pr_info("got %ld bytes (*ppos=%lld)\n", count, *ppos);

    return count;
}

它是如何工作的...

步骤 2 中,通过对我们的chrdev_read()方法的上述修改,现在我们将使用驱动内部缓冲区中的copy_to_user()功能从用户空间复制提供的数据,同时相应地移动ppos指针,然后返回已经读取了多少数据(或错误)。

注意copy_from/to_user()函数在成功时返回零或者非零来表示未传输的字节数,所以,在这里,我们应该考虑这种情况(即使很少)并适当更新count,减去未传输的字节数(如果有的话),以便正确更新ppos并向用户空间返回正确的计数值。然而,为了使示例尽可能简单,我们更愿意返回一个错误条件。

还要注意的是,如果*ppos + count点超出缓冲区末端,count将被相应地重新计算,并且该函数将返回一个表示传输字节数的值,该值小于输入中提供的原始count值(该值表示所提供的目标用户缓冲区的大小,因此是允许传输的最大数据长度)。

步骤 3 中,我们可以考虑与之前相同的关于copy_to_user()返回值的注释。但是,另外在copy_from_user()上,如果某些数据无法复制,该功能将使用零字节将复制的数据填充到请求的大小。

正如我们所看到的,这个函数与前面的非常相似,即使它实现了相反的数据流。

还有更多...

修改完成后,新的驱动版本已经重新编译并正确加载到 ESPRESSObin 的内核中,我们可以再次执行我们的测试程序chrdev_test。我们应该得到以下输出:

# ./chrdev_test chrdev
file chrdev opened
wrote 11 bytes into file chrdev
data written are: 44 55 4d 4d 59 20 44 41 54 41 00 
read 11 bytes from file chrdev
data read are: 00 00 00 00 00 00 00 00 00 00 00 

从串行控制台,我们应该会看到类似如下的内容:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: should write 11 bytes (*ppos=0)
chrdev_legacy:chrdev_write: got 11 bytes (*ppos=11)
chrdev_legacy:chrdev_read: should read 11 bytes (*ppos=11)
chrdev_legacy:chrdev_read: return 11 bytes (*ppos=22)
chrdev_legacy:chrdev_release: chrdev released

好的。我们得到了我们所期望的!事实上,从内核消息中,我们可以看到chrdev_open()的调用,然后当chrdev_write()chrdev_read()被调用时会发生什么:11 个字节被传输,并且ppos指针如我们所料地移动。然后,chrdev_release()被调用,文件被关闭。

现在有一个问题:如果我们再次调用前面的命令会发生什么?

嗯,我们应该期待完全相同的输出;事实上,每次打开文件时,ppos都被重新定位在文件开头(即 0),我们在相同的位置继续读写。

以下是第二次执行的输出:

# ./chrdev_test chrdev
file chrdev opened
wrote 11 bytes into file chrdev
data written are: 44 55 4d 4d 59 20 44 41 54 41 00 
read 11 bytes from file chrdev
data read are: 00 00 00 00 00 00 00 00 00 00 00

此外,以下是相关的内核消息:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: should write 11 bytes (*ppos=0)
chrdev_legacy:chrdev_write: got 11 bytes (*ppos=11)
chrdev_legacy:chrdev_read: should read 11 bytes (*ppos=11)
chrdev_legacy:chrdev_read: return 11 bytes (*ppos=22)
chrdev_legacy:chrdev_release: chrdev released

如果我们希望读取刚刚写入的数据,我们可以修改chrdev_test程序,使其关闭,然后在调用write()后重新打开文件:

...
        printf("wrote %d bytes into file %s\n", n, argv[1]);
        dump("data written are: ", buf, n);
    }

    close(fd);

    ret = open(argv[1], O_RDWR);
    if (ret < 0) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    printf("file %s reopened\n", argv[1]);
    fd = ret;

    for (c = 0; c < sizeof(buf); c += n) {
        ret = read(fd, buf, sizeof(buf));
...

Note that all of these modifications are stored in the modify_close_open_to_chrdev_test.patch patch file from GitHub sources and it can be applied by using the following command where the chrdev_test.c file is located: $ patch -p2 < modify_close_open_to_chrdev_test.patch

现在,如果我们再次尝试执行chrdev_test,应该会得到如下输出:

# ./chrdev_test chrdev
file chrdev opened
wrote 11 bytes into file chrdev
data written are: 44 55 4d 4d 59 20 44 41 54 41 00 
file chrdev reopened
read 11 bytes from file chrdev
data read are: 44 55 4d 4d 59 20 44 41 54 41 00

完美!现在,我们准确地阅读了我们所写的内容,从内核空间中,我们得到了以下消息:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: should write 11 bytes (*ppos=0)
chrdev_legacy:chrdev_write: got 11 bytes (*ppos=11)
chrdev_legacy:chrdev_release: chrdev released
chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_read: should read 11 bytes (*ppos=0)
chrdev_legacy:chrdev_read: return 11 bytes (*ppos=11)
chrdev_legacy:chrdev_release: chrdev released

现在,我们可以完美地看到ppos发生了什么,以及chrdev_read()chrdev_write()方法是如何工作的,以便与用户空间交换数据。

请参见

  • 有关read()write()系统调用的更多信息,读者可以开始阅读相关手册页,这些手册页可以通过常用命令获得:man 2 readman 2 write

Note that, this time, we have to specify section 2 of the man pages (system-calls); otherwise, we will get information straight from section 1 (executable programs).

使用“一切都是文件”抽象

当我们介绍设备驱动时,我们说它们位于 Unix 文件抽象之下;也就是说,在类似 Unix 的操作系统中,一切都是一个文件。现在,是验证它的时候了,所以让我们看看如果我们尝试对我们的新驱动执行一些文件相关的实用程序会发生什么。

由于我们对chrdev_legacy.c文件的最新修改,我们的驱动模拟了一个 300 字节长的文件(参见BUF_LEN被设置为300chrdev_buf[BUF_LEN]缓冲区),在那里我们能够执行read()write()系统调用,就像我们对一个正常的文件所做的那样。

然而,我们可能仍然有一些疑虑,所以让我们考虑标准的catdd命令,因为我们知道它们是对操纵文件内容有用的实用程序。例如,在cat命令的手册页中,我们可以看到以下定义:

NAME
       cat - concatenate files and print on the standard output

SYNOPSIS
       cat [OPTION]... [FILE]...

DESCRIPTION
       Concatenate FILE(s) to standard output.

并且,对于dd,我们有如下定义:

NAME
       dd - convert and copy a file

SYNOPSIS
       dd [OPERAND]...
       dd OPTION

DESCRIPTION
       Copy a file, converting and formatting according to the operands.

我们没有看到任何对设备驱动的引用,只有对文件的引用,所以如果我们的驱动像文件一样工作,我们应该能够在上面使用这些命令!

准备好

为了检查“一切都是一个文件”的抽象,我们仍然可以使用我们的新的字符驱动,它可以作为一个常规文件来管理。因此,让我们确保驱动被正确地加载到内核中,并进入下一部分。

怎么做...

让我们看看如何通过以下步骤来实现:

  1. 首先,我们可以尝试用以下命令将所有0字符写入其中,以清除驱动的缓冲区:
# dd if=/dev/zero bs=100 count=3 of=chrdev
3+0 records in
3+0 records out
300 bytes copied, 0.0524863 s, 5.7 kB/s
  1. 现在,我们可以使用cat命令读取刚刚写入的数据,如下所示:
# cat chrdev | tr '\000' '0'
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

完美!如我们所见,正如预期的那样,我们删除了驱动的内部缓冲区。

The reader should notice that we use the tr command in order to translate data bytes 0 to the printable character 0; otherwise, we'll see garbage (or most probably nothing). See the tr man page with man tr for further information about its usage.

  1. 现在,我们可以尝试将一个正常的文件数据移动到我们的 char 设备中;例如,如果我们考虑/etc/passwd文件,我们应该看到如下内容:
# ls -lh /etc/passwd
-rw-r--r-- 1 root root 1.3K Jan 10 14:16 /etc/passwd

该文件大于 300 字节,但我们仍然可以尝试使用下一个命令行将其移动到字符驱动中:

# cat /etc/passwd > chrdev
cat: write error: No space left on device

正如预期的那样,我们会收到一条错误消息,因为我们的文件不能超过 300 字节。然而,真正有趣的是在内核中:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: should write 1285 bytes (*ppos=0)
chrdev_legacy:chrdev_write: got 300 bytes (*ppos=300)
chrdev_legacy:chrdev_write: should write 985 bytes (*ppos=300)
chrdev_legacy:chrdev_write: got 0 bytes (*ppos=300)
chrdev_legacy:chrdev_release: chrdev released
  1. 即使我们得到了一个错误,从前面的内核消息中,我们看到一些数据实际上已经被写入了我们的字符驱动中,所以我们可以尝试使用grep命令用下一个命令行在其中找到一个特定的行:
# grep root chrdev
root:x:0:0:root:/root:/bin/bash

For further information about grep, just see its man page with man grep.

由于引用 root 用户的那一行是/etc/passwd中的第一行之一,肯定已经复制到字符驱动中了,然后我们就如预期的那样得到了。为了完整起见,下面报告了相关的内核消息,在这些消息中,我们可以看到grep对我们的驱动进行的所有系统调用:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=0)
chrdev_legacy:chrdev_read: return 300 bytes (*ppos=300)
chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=300)
chrdev_legacy:chrdev_read: return 0 bytes (*ppos=300)
chrdev_legacy:chrdev_release: chrdev released

它是如何工作的...

使用前面的dd命令,我们生成三个 100 字节长的块,并将其传递给write()系统调用;事实上,如果我们看一下内核消息,我们会清楚地看到发生了什么:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: should write 100 bytes (*ppos=0)
chrdev_legacy:chrdev_write: got 100 bytes (*ppos=100)
chrdev_legacy:chrdev_write: should write 100 bytes (*ppos=100)
chrdev_legacy:chrdev_write: got 100 bytes (*ppos=200)
chrdev_legacy:chrdev_write: should write 100 bytes (*ppos=200)
chrdev_legacy:chrdev_write: got 100 bytes (*ppos=300)
chrdev_legacy:chrdev_release: chrdev released

第一次调用,open()后,ppos设置为0,写入数据后移动到 100。然后,在接下来的调用中,ppos增加 100 字节,直到达到 300。

第 2 步中,当我们发出cat命令时,看到内核空间中发生了什么真的很有趣,所以让我们看看与之相关的内核消息:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_read: should read 131072 bytes (*ppos=0)
chrdev_legacy:chrdev_read: return 300 bytes (*ppos=300)
chrdev_legacy:chrdev_read: should read 131072 bytes (*ppos=300)
chrdev_legacy:chrdev_read: return 0 bytes (*ppos=300)
chrdev_legacy:chrdev_release: chrdev released

如我们所见,cat要求 131,072 字节,但是,由于我们的缓冲区较短,因此只返回 300 字节;然后,cat再次执行read()请求 131,072 字节,但是现在ppos指向文件的结尾,所以返回 0 只是为了表示文件的结尾条件。

当我们试图向设备文件中写入太多数据时,我们显然会收到一条错误消息,但真正有趣的是在内核中:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_write: should write 1285 bytes (*ppos=0)
chrdev_legacy:chrdev_write: got 300 bytes (*ppos=300)
chrdev_legacy:chrdev_write: should write 985 bytes (*ppos=300)
chrdev_legacy:chrdev_write: got 0 bytes (*ppos=300)
chrdev_legacy:chrdev_release: chrdev released

首先,write()调用要求写入 1285 字节(这是/etc/passwd的真实大小),但实际只写入了 300 字节(由于缓冲区大小有限)。然后,第二个write()调用请求写入 985 字节( 1,285-300 字节),但现在ppos指向 300,这意味着缓冲区已满,然后返回 0(写入的字节),这已被 write 命令解释为设备错误情况下没有剩余空间。

步骤 4 中,与前面的grep命令相关的内核消息报告如下:

chrdev_legacy:chrdev_open: chrdev opened
chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=0)
chrdev_legacy:chrdev_read: return 300 bytes (*ppos=300)
chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=300)
chrdev_legacy:chrdev_read: return 0 bytes (*ppos=300)
chrdev_legacy:chrdev_release: chrdev released

我们可以很容易地看到grep命令首先使用open()系统调用打开我们的设备文件,然后它继续用read()读取数据,直到我们的驱动返回文件结尾(用 0 寻址),最后它执行close()系统调用释放我们的驱动。