前言

在Linux内核中设备号的作用是用来区分不同的设备类型。

比如:

设备号23,对应的是LED

设备号17,对应的是某个存储设备

等等...

主次设备号

主设备号:对应设备的主号码

次设备号:对应设备的子号码

比如:你有两个LED,你注册了主设备号14,代表使用这个设备号的驱动都是LED设备,那么怎么区分1和2呢?就是子设备号,通过子设备号来区分是LED1还是LED2。

内核只认主设备号,最终调用时内核会通过主设备号找到索引,这个索引就是子设备号,然后去调用这个索引指向的驱动模块接口。

有很多设备号Linux内核已经自己定义了,你可以在你的发行版上输入如下命令可以查看到Linux内核定义好的设备类型,或者在Linux内核源码目录:/include/linux/major.h

cat /proc/devices

输出:

  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  5 ttyprintk
  6 lp
  7 vcs
 10 misc
 13 input
 14 sound/midi
 14 sound/dmmidi
 21 sg
 29 fb
 89 i2c
 99 ppdev

上面不是已经在当前Linux内核中注册的设备驱动,仅是Linux内核为我们开发人员定义好了的一个设备号,我们可以在后续注册时使用这个设备号,也可以自己自定义一个设备号。

已经注册好的设备都会放在/dev这个目录下。

我们可以使用ls命令查看:

ls -l /dev/*

输出:

1 root root     10, 175 Apr 21 00:00 agpgart
crw-r--r--  1 root root     10, 235 Apr 21 00:00 autofs
drwxr-xr-x  2 root root         300 Apr 21 00:00 block
drwxr-xr-x  2 root root          80 Apr 21 00:00 bsg
crw-------  1 root root     10, 234 Apr 21 00:00 btrfs-control
drwxr-xr-x  3 root root          60 Apr 21 00:00 bus
lrwxrwxrwx  1 root root           3 Apr 21 00:00 cdrom -> sr0
lrwxrwxrwx  1 root root           3 Apr 21 00:00 cdrw -> sr0
drwxr-xr-x  2 root root        3840 Apr 21 17:40 char
crw--w----  1 root tty       5,   1 Apr 21 00:01 console
lrwxrwxrwx  1 root root          11 Apr 21 00:00 core -> /proc/kcore
crw-------  1 root root     10,  59 Apr 21 00:00 cpu_dma_latency
crw-------  1 root root     10, 203 Apr 21 00:00 cuse
drwxr-xr-x  6 root root         120 Apr 21 00:00 disk
drwxr-xr-x  2 root root          60 Apr 21 00:00 dma_heap
crw-rw----+ 1 root audio    14,   9 Apr 21 00:00 dmmidi

就拿第一个来说吧

10,175 其中10代表主设备号,175代表次设备号,Apr 21 00:00代表注册日期,这个日期是内核启动加载时候的日期,agpgart设备类型

我们在注册设备时也可以使用10这个主设备号,但是不能使用175这个次设备号,因为这个次设备号已经被注册使用,Linux内核允许多个驱动共享一个主设备号但不允许共享一个次设备号。

同时也可以写一个驱动管理多个次设备。

VFS虚拟文件系统

要想了解用户态是如何在操作驱动时不关心物理媒介和不同文件系统协议规范,而使用统一接口就能抽象完成所有事情,使得用户无需关心具体底层实现,就可以使用通用接口完成不同设备的控制。

Linux内核是如何检索主次设备的?

当我们调用open函数时,会从用户态发生一个中断为10的系统中断,然后转到内核态的SYSCALL_DEFINE3函数开始执行,在经过一系列的查找调用确定最终调用的open函数。

最终的调用可能是驱动实现的open,或由VFS子系统完成的open函数,VFS子系统的open函数实现就像普通的fopen一样,会返回一个未使用的文件描述符,这个文件描述符仅对当前进程有效,不同的系统位数对大小有限制,其中当打开只有VFS会把它加入到文件监视系统中去,记录这个文件当前的状态。

open会一环接一环尝试去在对应的链表里寻找对应的文件名,查看在哪个文件设备表中注册,是普通文件还是驱动文件还是其它文件,则调用对应的open函数。

同时设备驱动是用一个表存储的:

struct kobj_map {
        struct probe {
                struct probe *next;
                dev_t dev;
                unsigned long range;
                struct module *owner;
                kobj_probe_t *get;
                int (*lock)(dev_t, void *);
                void *data;
        } *probes[255];
        struct mutex *lock;
};

这是我在Linux内核2.4里取的,最大限制为255,也就是说最多只能注册255个设备驱动,且设备驱动的主设备号不能小于0或大于255,其中0和255被Linux内核保留,不允许使用。

最新版的Linux内核已经不受限于255了,这里我以这个版本为列,让大家知道Linux是怎么来存储与找到设备模块的。

当找到是设备文件时,会去遍历kobj_map这个结构体里的probe这个表。

它是哈希表,首先内核有两种遍历方式。

因为在我们写驱动时注册主设备号时,可以手动指定设备号也可以动态分配,哈希表有一个特点就是能把一组字符算成一个固定取值范围大小的一个值,当我们是动态分配时,Linux内核会算成一个Key,然后插到对应表项里且哈希表有一个特点就是不会重复,当你的设备名字是重复的时候Linux内核也不会让你注册。

如已经有设备叫LED了,你不能在注册一个LED的设备。

当你是静态分配时Linux内核则按你给的设备号往里插,若以这个设备号为键值的表项里有数据则插入失败。

当然查找这个表项也非常简单,首先open到了__dentry_oepn这个函数,它会去遍历kobj_map->probe这个表,首先拿着用户态传进来的name,遍历probe,然后在owner取出name比对,若一致则返回当前probe的指针,后续的read,write可以根据这个指针进行调用对用的read和write。

其中dev是一个32位的变量,它按位存储,其中12位存储主设备号,20位存储次设备号,也就是说主设备号最大也不能超过12次方的大小。

这里可以看下owner这个结构体:

可以清楚的看到name这个变量。

struct module
    {
        enum module_state state;
        struct list_head list;
        char name[MODULE_NAME_LEN];
 
        struct module_kobject mkobj;
        struct module_param_attrs *param_attrs;
        const char *version;
        const char *srcversion;
 
        const struct kernel_symbol *syms;
        unsigned int num_syms;
        const unsigned long *crcs;
 
        const struct kernel_symbol *gpl_syms;
        unsigned int num_gpl_syms;
        const unsigned long *gpl_crcs;
 
        unsigned int num_exentries;
        const struct exception_table_entry *extable;
 
        int (*init)(void);
        void *module_init;
        void *module_core;
        unsigned long init_size, core_size;
        unsigned long init_text_size, core_text_size;
        struct mod_arch_specific arch;
        int unsafe;
        int license_gplok;
 
#ifdef CONFIG_MODULE_UNLOAD
        struct module_ref ref[NR_CPUS];
        struct list_head modules_which_use_me;
        struct task_struct *waiter;
        void (*exit)(void);
#endif
 
#ifdef CONFIG_KALLSYMS
        Elf_Sym *symtab;
        unsigned long num_symtab;
        char *strtab;
        struct module_sect_attrs *sect_attrs;
#endif
        void *percpu;
        char *args;
    };

总结

通俗易懂的来说,主设备号在Linux内核看来就是设备文件表的KEY,静态分配与动态分配有的差别在于是否使用哈希算法,同时非常建议大家使用动态分配。

次设备号就是用来标明是哪个设备,比如多个LED设备,次设备号就是用来区分LED1还是LED2,同时在底层内核态在检索时,是不关心次设备号的,只关心主设备号,而我们会关心次设备号。

因为内核最终会把dev传递给驱动模块,我们在代码里把dev里的次设备号取出来,判断是哪个次设备然后做对应的工作,这样就完成在一个模块里实现对多个设备的管理,或者你也可以在为每个子设备都注册一个驱动,也是可以的。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐