字符设备或字符特殊文件如何工作?

ber*_*436 28 drivers files

我正在尝试了解字符特殊文件。从wikipedia,我了解到这些文件为一次一个字符传输数据的设备“提供了一个接口”。我的理解是系统以某种方式调用字符设备而不是直接调用设备驱动程序。但是文件是如何提供这个接口的呢?它是一个翻译系统调用的可执行文件吗?有人可以解释一下是怎么回事吗。

Sha*_*dur 27

它们实际上就是这样 - 接口。由“主要”和“次要”数字编码,它们为内核提供了一个钩子。

它们有两种风格(好吧,三种,但命名管道现在不在本解释的范围内):字符设备和块设备。

块设备往往是存储设备,能够缓冲输出并存储数据以供以后检索。

字符设备是诸如音频或图形卡之类的东西,或者是键盘和鼠标之类的输入设备。

在每种情况下,当内核加载正确的驱动程序时(在启动时或通过udev 之类的程序)它会扫描各种总线以查看该驱动程序处理的任何设备是否实际存在于系统中。如果是这样,它会设置一个设备来“侦听”适当的主要/次要号码。

(例如,您的系统找到的第一张声卡的数字信号处理器获得的主/次编号对为 14/3;第二个获得 14,35,以此类推。)

由 udev 创建一个条目,/dev命名dsp为标记为major 14 minor 3的字符设备。

(在明显较旧或占用空间最小的 Linux 版本中,/dev/可能不会动态加载,而只是静态包含所有可能的设备文件。)

然后,当用户空间程序尝试访问标记为具有适当主要/次要编号的“字符特殊文件”的文件时(例如,您的音频播放器尝试将数字音频发送到/dev/dsp),内核知道该数据需要通过主/次编号附加的驱动程序传输;大概说司机知道如何处理它。

  • 1. 那么主/次编号类似于端口? (2认同)
  • 2)您在那里缺少大约三到四层抽象。您打开文本文件的程序既不知道也不关心键盘设备是什么。与底层硬件的通信通过终端仿真器(如果您处于控制台模式)或通过 X 事件层(如果您处于图形模式)发生,两者都将侦听键盘和其他驱动器并决定什么,如果有的话,传递给程序。我在这里总结了一个相当复杂的多层系统;一般来说,您可能会很好地阅读 X Window 系统。 (2认同)

Ign*_*ams 11

每个文件、设备或其他方式都支持 VFS 中的 6 种基本操作:

  1. 打开
  2. 关闭
  3. 寻找
  4. 告诉

此外,设备文件支持 I/O 控制,它允许前 6 个未涵盖的其他杂项操作。

在特殊字符的情况下,seek 和 tell 没有实现,因为它们支持流接口。也就是说,直接读取或写入,例如在 shell 中通过重定向完成:

echo 'foo' > /dev/some/char
sed ... < /dev/some/char
Run Code Online (Sandbox Code Playgroud)


Cir*_*郝海东 7

最小的可运行file_operations示例

一旦你看到一个最小的例子,一切就变得显而易见了。

关键思想是:

  • file_operations 包含每个文件相关系统调用的回调
  • mknod <path> c <major> <minor> 创建一个使用这些的字符设备 file_operations
  • 对于动态分配设备号的字符设备(避免冲突的规范),找到带有 cat /proc/devices

character_device.ko 内核模块:

#include <asm/uaccess.h> /* copy_from_user, copy_to_user */
#include <linux/errno.h> /* EFAULT */
#include <linux/fs.h> /* register_chrdev, unregister_chrdev */
#include <linux/jiffies.h>
#include <linux/module.h>
#include <linux/printk.h> /* printk */
#include <uapi/linux/stat.h> /* S_IRUSR */

#define NAME "lkmc_character_device"

MODULE_LICENSE("GPL");

static int major;

static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    size_t ret;
    char kbuf[] = {'a', 'b', 'c', 'd'};

    ret = 0;
    if (*off == 0) {
        if (copy_to_user(buf, kbuf, sizeof(kbuf))) {
            ret = -EFAULT;
        } else {
            ret = sizeof(kbuf);
            *off = 1;
        }
    }
    return ret;
}

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = read,
};

static int myinit(void)
{
    major = register_chrdev(0, NAME, &fops);
    return 0;
}

static void myexit(void)
{
    unregister_chrdev(major, NAME);
}

module_init(myinit)
module_exit(myexit)
Run Code Online (Sandbox Code Playgroud)

用户空间测试程序:

insmod /character_device.ko
dev="lkmc_character_device"
major="$(grep "$dev" /proc/devices | cut -d ' ' -f 1)"
mknod "/dev/$dev" c "$major" 0
cat /dev/lkmc_character_device
# => abcd
rm /dev/lkmc_character_device
rmmod character_device
Run Code Online (Sandbox Code Playgroud)

GitHub QEMU + Buildroot 上游带有样板以实际运行它:

更复杂的例子:


Ran*_*832 5

“一次一个字符”用词不当(字符设备必然不支持搜索和告诉的想法也是如此)。事实上,“一次块”(即严格面向记录,例如磁带驱动器*)设备必须是字符设备。因此,字符设备必须是不可搜索的——字符设备驱动程序定义了一个完整的file_operations结构,该结构可以根据设备是否支持操作来自由定义 llseek。大多数人认为的字符设备是 null、urandom、TTY 设备、声卡、鼠标等……由于这些设备的具体情况,它们都是不可查找的,但是 /dev/vcs、/dev/fb0 , 和 /dev/kmem 也是字符设备,它们都是可查找的。

正如我提到的,字符设备驱动程序定义了一个 file_operations 结构,该结构具有针对某人可能想要对文件调用的所有操作的函数指针 - 查找、读取、写入、ioctl 等 - 当相应的系统调用时,这些每个操作都会被调用一次在此设备文件打开的情况下执行。因此 read 和 write 可以用它的参数做任何它想做的事情——它可以拒绝接受过大的写入或只写入适合的;它只能读取一条记录对应的数据,而不是整个请求的字节数。

那么,什么是块设备呢?基本上,块设备是磁盘驱动器。没有其他类型的设备(除了虚拟磁盘驱动器,如 ramdisk 和环回)是块设备。它们以字符设备没有的方式集成到 I/O 请求系统、文件系统层、缓冲区/缓存系统和虚拟内存系统中,即使您从用户进程访问 /dev/sda 等. 即使是该页面作为例外提到的“原始设备”也是 character devices

*一些 UNIX 系统实现了现在所谓的“固定块模式”——它允许内核组和拆分 I/O 请求以或多或少与磁盘驱动器相同的方式来适应配置的块边界——作为一个块设备。“可变块模式”需要一个字符设备,它保留用户程序中的块边界,因为单个 write(2) 调用写入一个块,单个 read(2) 调用返回一个块。由于模式切换现在是作为 ioctl 而不是单独的设备文件实现的,因此使用了字符设备。可变记录磁带驱动器大多是“不可查找的”,因为查找涉及对记录数而不是字节数进行计数,并且本机查找操作是作为 ioctl 实现的。