Answers:
它们实际上就是接口。通过“大”和“小”数字进行编码,它们提供了与内核的挂钩。
它们有两种形式(嗯,三种,但是命名管道目前不在此说明的范围之内):字符设备和块设备。
块设备往往是存储设备,能够缓冲输出并存储数据以供以后检索。
字符设备是诸如音频或图形卡之类的东西,或诸如键盘和鼠标之类的输入设备。
在每种情况下,当内核(在引导时或通过诸如udev的程序)加载正确的驱动程序时,它都会扫描各种总线,以查看该驱动程序处理的设备是否确实存在于系统上。如果是这样,它将设置一个设备来“监听”适当的主/副号码。
(例如,系统找到的第一个声卡的数字信号处理器的主/副号码对为14/3;第二对为14,35,依此类推。)
由udev决定创建一个/dev
名称dsp
为字符设备的条目,将其标记为major 14 minor 3。
(在明显较旧或最小占用空间的Linux版本中,/dev/
可能不会动态加载,而仅静态包含所有可能的设备文件。)
然后,当用户空间程序尝试访问带有适当的主/次编号的标记为“字符特殊文件”的文件时(例如,您的音频播放器尝试将数字音频发送到/dev/dsp
),内核知道该数据需要通过附加了主要/次要号码的驾驶员进行传输;据推测,驾驶员知道该如何处理。
每个文件,设备或其他文件,都在VFS中支持6种基本操作:
此外,设备文件支持I / O控制,它允许前6个未涵盖的其他杂项操作。
对于特殊字符,由于它们支持流接口,因此未实现查找和告诉。也就是说,直接读取或写入内容(例如在外壳中使用重定向完成):
echo 'foo' > /dev/some/char
sed ... < /dev/some/char
最小的可运行file_operations
示例
一旦看到一个最小的示例,一切就变得显而易见。
关键思想是:
file_operations
包含每个与文件相关的syscall的回调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)
Userland测试程序:
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上游并带有样板以实际运行它:
更复杂的示例:
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*off = 1;
,为什么要设置为1
?
read
调用传递给同一open(
文件描述符。驾驶员可以用它做任何想做的事情。通常的语义是包含读取的字节数。但是,在此示例中,我们只有一个更简单的语义:0
对于第一次读取,1
在第一次读取之后。尝试运行它,并放置一个printk或GDB步骤对其进行调试。
“一次使用字符”是一个用词不当的说法(字符设备不一定支持搜寻和讲述的想法也是如此)。实际上,“一次阻止”(即严格面向记录的磁带,例如磁带驱动器*)设备必须是字符设备。因此,字符设备必须一定是不可搜索的想法-字符设备驱动程序定义了一个完整的file_operations
结构,该结构可以根据设备是否支持该操作自由定义。大多数人认为的字符设备是null,urandom,TTY设备,声卡,鼠标等,由于这些设备的具体特征,它们都是无法找到的,但是/ dev / vcs,/ dev / fb0和/ dev / kmem也是字符设备,它们都是可搜索的。
正如我所提到的,字符设备驱动程序定义了一个file_operations结构,该结构具有某人可能要在文件上调用的所有操作(查找,读取,写入,ioctl等)的函数指针,并且在相应的系统调用时,每个调用一次在打开该设备文件的情况下执行。因此,读写操作可以使用其参数来做任何想要的事情-它可以拒绝接受太大的写操作,或者只写适合的写操作。它只能读取对应于一条记录的数据,而不能读取整个请求的字节数。
那么,什么是块设备?基本上,块设备是磁盘驱动器。没有其他类型的设备(虚拟磁盘驱动器,如ramdisk和Loopback除外)是块设备。它们以字符设备没有的方式集成到I / O请求系统,文件系统层,缓冲区/缓存系统和虚拟内存系统中,即使您正在从用户进程访问例如/ dev / sda时。甚至页面提到的“原始设备” 也是字符设备。
*某些UNIX系统实现了现在所说的“固定块模式”-使内核组和拆分I / O请求以与磁盘驱动器大致相同的方式适应已配置的块边界-作为块设备。“可变块模式”需要一种字符设备,当单个write(2)调用写入一个块,而单个read(2)调用返回一个块时,该字符设备保留了用户程序中的块边界。由于现在将模式切换实现为ioctl而不是单独的设备文件,因此将使用字符设备。可变记录磁带驱动器大多是“不可搜索的”,因为查找涉及计数许多记录而不是字节数,并且本机查找操作被实现为一个ioctl。