我正在尝试理解字符特殊文件。从维基百科,据我所知,这些文件为一次传输一个字符的数据的设备“提供了一个接口”。我的理解是系统以某种方式调用字符设备而不是直接调用设备驱动程序。但是文件如何提供这个接口呢?它是翻译系统调用的可执行文件吗?有人可以解释一下发生了什么事吗?
答案1
它们实际上就是接口。它们由“主要”和“次要”数字编码,为内核提供了一个钩子。
它们有两种类型(嗯,三种,但命名管道目前超出了本解释的范围):字符设备和块设备。
块设备往往是存储设备,能够缓冲输出并存储数据以供以后检索。
字符设备是指音频或显卡等设备,或者键盘和鼠标等输入设备。
在每种情况下,当内核加载正确的驱动程序时(无论是在启动时,还是通过诸如乌德夫)它扫描各种总线以查看该驱动程序处理的任何设备是否实际存在于系统上。如果是这样,它会设置一个设备来“侦听”适当的主要/次要号码。
(例如,系统找到的第一张声卡的数字信号处理器获得主/次数字对 14/3;第二个获得 14,35,等等。)
由 udev 在/dev
命名中创建一个条目dsp
,作为标记为主要 14 次要 3 的字符设备。
(在 Linux 的较旧或最小占用版本中,/dev/
可能不会动态加载,而只是静态包含所有可能的设备文件。)
然后,当用户空间程序尝试访问标记为“字符特殊文件”并具有适当的主/次编号的文件时(例如,您的音频播放器尝试将数字音频发送到/dev/dsp
),内核知道该数据需要通过主/次编号所附加的驱动程序传输;想必所说的司机知道依次如何处理它。
答案2
每个文件、设备或其他都支持 VFS 内的 6 种基本操作:
- 打开
- 关闭
- 读
- 写
- 寻找
- 告诉
此外,设备文件支持 I/O 控制,这允许前 6 个未涵盖的其他杂项操作。
在特殊字符的情况下,seek 和tell 不会被实现,因为它们支持流媒体接口。也就是说,直接读取或写入,例如在 shell 中通过重定向完成:
echo 'foo' > /dev/some/char
sed ... < /dev/some/char
答案3
最小可运行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)
用户态测试程序:
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
GitHub QEMU + Buildroot 上游,带有样板以实际运行它:
- https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/character_device.c
- https://github.com/cirosantilli/linux-kernel-module-cheat/blob/master/rootfs_overlay/character_device.sh
更复杂的例子:
read
、write
、lseek
具有固定大小的内部缓冲区并在 debugfs 上而不是字符设备上:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/fops.cpoll
:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/poll.cioctl
:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/poll.canon_inode_getfd
将 a 关联file_operations
到没有任何文件系统文件的文件描述符:https://stackoverflow.com/questions/4508998/what-is-anonymous-inode/44388030#44388030
答案4
字符设备可以由内核模块(或内核本身)创建。创建设备时,创建者会提供指向实现句柄标准调用(如 open、read 等)的函数的指针。然后,Linux 内核将这些函数与字符设备关联起来,例如,当用户模式应用程序调用 read()字符设备文件上的函数,它将导致系统调用,然后内核会将此调用路由到创建驱动程序时指定的读取函数。有关于创建字符设备的分步教程这里,您可以创建一个示例项目并使用调试器逐步执行该项目,以了解如何创建设备对象以及何时调用处理程序。