第二节:系统里的各个设备文件到底是什么
| 2025-7-6
Words 4473Read Time 12 min
思考,内核模块好像都是围绕着这个设备文件和proc文件来服务的……

用户空间?内核空间?

对于一个正常的C语言程序来说,它被编译出来后应该被放到用户空间中。不同的C语言程序之间访问的内存地址是相对独立的,也就是A程序访问 0x1100000 这个地址,和B程序访问的这个地址,实际对应的物理地址并不是一致的,这也就防止程序间的数据被访问的问题。
但对于内核模块来说,内核共享其代码空间,也就是内核模块访问某个地址的时候,另一个内核模块访问的也是同一个地址。所以内核模块如果出现了 segfault 的时候,整个内核也就出现了 segfault 了。Linux 是宏内核,微内核将各个内核模块放在用户空间的做法能够很好地避免这个问题。但是微内核需要经过多次上下文切换,也就会降低其效率。

/dev 的设备文件?

可以随便 ls -lha /dev ,然后看看其输出长什么样
这里出现了两列数字,其中第一列表示系统使用的设备驱动号,而第二列则表示使用该设备驱动号的设备id。第一列相同,则表明其受到同一个驱动管理。

块设备?字符设备?

设备分为两种不同的设备,一个是块设备 block ,另一个是字符设备 character ,由ls出来的第一个属性区分,也就是b和c。
块设备在进行传入传出的时候,只能以一个块为单位来进行传输,典型的块设备有如:硬盘设备,音频设备等。
而字符设备则可以以少数的几个字符来进行传输,典型的字符设备有如 控制台,或者是单纯获取温度信息之类的,又或者是tty。

我可以手动创建一个设备吗?

当然可以,可以通过 mknod 命令来创建一个设备的映射,例如 mknod /dev/coffee c 12 2,表明使用12号驱动创建第二个设备,然后创建一个字符设备。这里的12号驱动其实是一个软盘驱动,实际上创建了也无法使用,会报 没有那个设备或地址 的错误。

对设备文件进行操作?

字面意思来说,linux的设备文件其实就是可以看成一个文件,所以你当然可以对其进行读写操作,对于各个操作,你都可以写一个函数来实现它,比如open、read、write等。当我们用不到这个操作的时候,就应该将 file_operation 对应的指针设置为NULL。文件结构大概会长下面这样,后面设置的变量就是方法名啦:
这样也就不用单独设置NULL了。
可实现的方法可以在 include/linux/fs.h 看到其方法需要怎么实现。

那设备文件是文件吗?

那其实设备文件也不是一个具体的文件,只是被linux抽象成文件了,你可以把它当成文件一样进行读写操作,实际上这个“文件”的具体操作就是在上面被定义的那样,读一下跑什么方法啊,写一下跑什么方法啊这样子。你把 /dev/null 写破头了你也不可能从里面读出一个 a,除非这个null是假的。对吧所以我们现在知道为什么可以给 /dev/null 丢垃圾了,它的读写操作压根就没干啥。
你可以在这个代码这里看到。它干啥了啊,它就是没干啥。

那好吧,我该怎么注册一个驱动号?

了解了这些,我们就得整个自己的设备文件了。在 include/linux/fs.h 这个文件这里,有一个叫 register_chrdev 的方法,它是这么写的
看起来不难,我们只需要传入一个没被占用的 major number,一个名字,以及文件操作就可以实现了。这个名字对应 /proc/devices 里的名字。那简单看看我电脑里有什么吧
这些设备名看上去都挺熟悉的。在 Documentation/admin-guide/devices.txt 可以看到大家都用了什么设备,那挑一个没用的呗,或者写一个0,也会自动给个动态生成的 major number,但那样就没法提前创建设备文件了。
实际上写代码的时候,我们要用 alloc_chrdev_region 这个方法。

模拟文件,我该怎么传递字符信息?

用 copy_from_user 这个方法即可实现,可以在 tools/virtio/linux/uaccess.hinclude/linux/uaccess.h 看到其方法应该怎么使用:
这个抽象得很简单,就是:目标,源,长度

写个 Hello World 的设备文件试试?

就着大模型来写一个试试看吧,重点就是init过程中注册class,注册dev之类的,然后写一下文件操作
完事以后可以根据 dmesg 看其驱动号,手动mknod一个对应设备号的字符设备。这个字符设备文件就好像被模拟成一个真的文件一样啦,可以被写入字符信息并暂存。
可以模拟一个设备文件被锁的情况。在一个终端执行 less < hello.dev ,然后在另一个终端尝试 rmmod hellodev,系统会报 rmmod: ERROR: Module hellodev is in use

我的设备文件只能有一个人打开!

这个可以通过设置变量位来进行操作,在原代码基础上新增以下内容:
好了,这样当你用 less < hello.dev 占用了设备文件的时候,再开就会提示设备或资源文件忙了。

诶,那能放东西在 /proc 吗

那当然也是可以的,对于 proc 里面的“文件”,其实也是可以有 ops 来设置的,在 include/linux/proc_fs.h ,我们可以看到 ops 的文件结构
要完成这一工作,可以新增这些内容,其实和设备文件也差不了多少。
单独写一个去掉了dev,只有proc的模块吧~

什么是inode?

inode是文件系统里用来存储文件的必要信息的一个元数据结构,会存储比如:文件类型、文件权限、文件大小、修改时间、创建时间、文件指针之类的信息。
比如有个文件叫 /home/guoguo/a.txt ,那么文件系统就要从 / 开始,找到 home 的inode,然后从 home 找到 guoguo 的inode,然后从 guoguo 找到 a.txt 的inode,然后就可以根据其指针读到文件内容了。
/proc目录的文件当然也有其inode,这里面的”文件“应该都有其独特的 inode_operations ,与普通的文件不太一样。inode_operations 的结构可以见 include/linux/fs.h
有一说一,那按这种说法,其实也可以在系统进行一些特殊操作,例如创建文件、创建文件夹的时候做一些内核模块自己的事情。

好麻烦啊,有没有封装好的直接输出给用户的方法?

有的兄弟,有的,这个API叫 seq_file ,我们可以在 linux/seq_file.h 找到其结构:
注意哦,这里start和next的返回类型是 void* 而不是 void,也就是它是可以返回一个地址的,如果输了 void 编辑器就会报错误。这里我稍微改了一下之前的proc的,用seqfile的API重新整了一遍
lseek是读写位置出现变更时被调用的,这里指向seq_file提供的API seq_lseek 就好
插入模块后,尝试访问 /proc/myproc_status ,得到的信息是这样的
说明这个调用过程是:
start → show(pos=1) → next →show → next (pos=3, return NULL)→ stop → start(pos=3, return NULL) → stop
也就是说,在next返回值为NULL的时候,就会调用到stop,然后再次调用start,检查确实没有可以返回的内容后再次调用stop。
Loading...