Linux 文件系统与设备文件

1 Linux 文件系统

1.1 Linux 文件系统与设备驱动关系

下图表明了 Linux 中虚拟文件系统、磁盘/Flash文件系统以及一般的设备文件与设备驱动程序之间的关系。

Linux 文件系统与设备文件_第1张图片
文件系统与设备驱动之间的关系

应用程序与 VFS 之间的接口是系统调用,而 VFS 与文件系统以及设备文件之间的接口是 file_operations 结构体成员函数,此结构体包含了对文件进行打开、关闭、读写、控制的一系列成员函数。由于字符设备的上层没有类似于磁盘的 ext2 等文件系统,所以字符设备的 file_operations 成员函数就直接由设备驱动提供了。

由此可以看出对于块设备的访问有两种方法:

  • 一种方法是不通过文件系统直接访问裸设备,Linux 内核实现了统一的 def_blk_fops这一个 file_operations,他的源码位于 fs/block_dev.c。所以当运行类似于 "dd if=/dev/sd1 of=sdb1.img" 的命令把整个 /dev/sdb1裸分区复制到 sdb1.img 的时候,内核走的是 def_blk_fops这个 file_operations;
  • 另一种方法是通过文件系统来访问设备,file_operations 的实现则位于文件系统内,文件系统会把正对文件的读写转换为针对设备原始扇区的读写。ext2、fat、btrfs 等文件系统中会实现针对 VFS 的file_operations 成员函数,设备驱动层将看不到 file_operations的存在。

1.2 设备驱动程序

在设备驱动程序设计中,一般而言,会关心 file 和 inode 这两个结构体。

file 结构体

file 结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个与之关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行任何操作的函数。在文件的所有实例都关闭后,内核释放此结构体的数据。结构体原型如下:

struct file {
    union{
        struct llist_node fu_llist;
        struct rcu_head fu_rcuhead;
    } f_u;

    struct path f_path;
    #define f_dentry f_path.dentry;
    struct inode *f_inode; //cached value
    const struct file_operations *f_op; //和文件关联的操作

    //Protects f_ep_links, flags.
    //must not be taken from IRQ context
    spinlock_t f_lock;
    atomic_long_t f_count;
    unsigned int f_flags; //文件标志,如 O_RDONLY O_NONBLOCK OSYNC
    fmode_t f_mode;  //文件读写模式 FMODE_READ FMODE_WRITE
    struct fown_struct f_owner;
    const struct cred *fcred;
    struct file_ra_state f_ra;

    u64 v_version;
    #ifdef CONFIG_SECURITY
    void *f_security;
    #endif

    //need for tty driver, and maybe others 
    void *private_data; //文件私有数据

    #ifdef CONFIG_EPOLL
    //used tye fs/eventpoll.c to link all the hooks to this file 
    struct list_head f_ep_links;
    struct list_head f_tfile_llink;
    #endif 

    struct address_space *f_mapping;
    
    
} __attribute__((aligned(4))); //lest something weird decides that 2 is OK

文件读/写模式 mode、标志 f_flags 都是设备驱动关心的内容,而私有数据指针 private_data 在设备驱动中被广泛应用,大多被指向设备驱动自定义以用于描述设备的结构体。

inode 结构体

VFS node 包含文件的访问权限、属性、组、大小、生成时间、访问时间、最后修改时间等信息。它是 Linux管理文件系统的最基本的单位,也是文件系统连接任何子目录、文件的桥梁。其定义如下:

struct inode{
    ...
    umode_t i_mode; //inode 的权限
    uid_t i_uid;  //inode 的拥有者id
    gid_t i_gid;  //inode 所属的群组 id
    dev_t i_rdve; //若是设备文件,此字段将记录设备的设备号
    loff_t i_size; //inode 所代表文件的大小 

    unsigned int i_blkbits;
    blkcnt_t i_blocks; //inode 所使用的block数, 一个block为 512 个字节
    union{
        struct pipe_inode_info *i_pipe;
        struct block_device  *i_bdev; //若是块设备,为其对应的block_device结构体指针
        struct cdev *i_cdev; //若是字符设备,为其对应的 cdev 的结构体指针
    };
    ...
};

对于表示设备文件的 inode 结构,i_rdev 字段包含设备编号。linux 内核设备编号分为主设备编号和次设备编号,前者为 dev_t 的高 12 位,后者位 dev_t 的低 20 位。下列操作用于从设备号中获取主设备号和次设备号:

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

查看 /proc/devices 文件可以获知系统中注册的设备,第一列位主设备号,第 2 列为设备名称。如:

Character 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
Block devices:
259 blkext
  7 loop
  8 sd
  9 md
128 sd
134 sd
135 sd
253 device-mapper
254 mdp

查看/dev 目录可以获知系统中包含的设备文件,日期的前两列分别给出了主设备号和次设备号。如:

crw--w----   1 root    tty       4,  29 4月   9 16:39 tty29
crw--w----   1 root    tty       4,   3 4月   9 16:39 tty3
crw--w----   1 root    tty       4,  30 4月   9 16:39 tty30
crw--w----   1 root    tty       4,  31 4月   9 16:39 tty31
crw--w----   1 root    tty       4,  32 4月   9 16:39 tty32

主设备号是与驱动对应的概念,同一类设备使用相同的主设备号。
同一驱动可支持多个同类设备,因此次设备号就是用来表示同一驱动下不同设备。

2 devfs

devfs(设备文件系统)是由 linux2.4 内核引入,它使得设备驱动程序能自主管理自己的设备文件。具体来说,devfs具有如下优点:

  1. 可以通过程序在设备初始化时在 /dev 目录下创建设备文件,卸载设备时将它删除。
  2. 设备驱动程序可以指定设备名、所有者和权限位,用户空间程序仍可以修改所有者和权限位。
  3. 不再需要为设备驱动程序分配主设备号以及处理次设备号,在程序中可以直接给 register_chr_dev() 传递 0 主设备号以获得可用的主设备号,并在 devfs_register() 中指定次设备号。

驱动程序应调用下面这些函数来进行设备文件的创建和撤销工作。

//创建设备目录
devfs_handle_t devfs_mk_dir(devfs_handle_t dir, const char *name, void *info);
//创建设备文件
devfs_handle_t devfs_register(devfs_handle_t dir, const char *name, unsigned int flags, \
unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info);
//撤销设备文件
void devfs_unregister(devfs_handle_t de);

在 Linux 2.4 的设备编程中,分别在模块加载、卸载函数中创建和撤销设备文件是被普遍采用并值得大力推荐的好方法。以下为示例代码:

static devfs_handle_t devfs_handle;
static int __init xxx_init(void)
{
    int ret;
    int i;
    //在内核中注册设 
    ret = register_chr_dev(XXX_MAJOR, DEVICE_NAME, &xxx_fops);
    if(ret < 0)
    {
        printk(DEVICE_NAME " can't register major number\n");
        return ret;
    }

    //创建设备文件
    devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT,
    XXX_MAJOR, 0, S_IFCHR | S_IWUSR | S_IRUSR, &xxx_fops, NULL);
    ...
    printk(DEVICE_NAME " initialized.\n");
    return 0;
}

static void __exit xxx_exit(void)
{
    devfs_unregister(devfs_handle);    //撤销设备文件
    unregister_chrdev(XXX_MAJOR, DEVICE_NAME); //注销设备
}

module_init(xxx_init);
moudule_exit(xxx_exit);

3 udev 用户空间设备管理

3.1 udev 和 devfs 的区别

在 Linux 2.6 内核中,devfs 被认为是过时的方法,并最终被淘汰了,udev 取代了它。Linux VFS 内核维护者 Al viro 指出了几点 udev 取代 devfs 的原因:

  1. devfs 所做的工作被确信可以在用户态来完成。
  2. devfs 被加入内核之时,大家期望它的质量可以迎头赶上。
  3. 发现 devfs 有一些可修复和无法修复的bug。
  4. 对于无法修复的 bug 在相当长的一段时间内没有改观。
  5. devfs 的维护者和作者对齐感到失望并停止对其维护。

Linux 设计中强调的一个基本观点是机制和策略的分离。机制是稳定的,而策略是灵活的,机制应该在内核完成,而策略最好在用户空间实现。

3.2 热插拔事件

udev 完全在用户态工作,利用设备加入或移除时内核所发送的热插拔事件(Hotplug Event)来工作。在热插拔时,设备的详细信息会由内核通过 netlink 套接字发送出来,发出来的事情叫 uevent。udev 的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用从内核收到的信息进行创建设备文件节点等工作。以下是一个获取热插拔事件 uevent的范例:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


static void die(char *s)
{
    write(2, s, strlen(s));
    exit(1);
}

int main(int argc, char* argv[])
{
    struct sockaddr_nl nls;
    struct pollfd pfd;
    char buf[512];

    //open hotplug event netlink socket 
    
    memset(&nls, 0, sizeof(struct sockaddr_nl));
    nls.nl_family = AF_NETLINK;
    nls.nl_pid = getpid();
    nls.nl_groups = -1;

    pfd.events = POLLIN;
    pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
    if(-1 == pfd.fd)
        die("Not root\n");

    //Listen to netlink socket 
    if(bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
        die("Bind failed.\n");

    while(-1 != poll(&pfd, 1, -1))
    {
        int i, len;
        len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
        if(-1 == len)
            die("recv\n");

        //print the data to stdout 
        i = 0;
        while(i < len){
            printf("%s\n", buf + i);
            i += strlen(buf + i) + 1;
        }
    }
    die("poll\n");

    return 0;
}

编译上述代码,在虚拟机上运行,插拔光盘等设备,会打印热插拔事件。

udev 就是采用这种方式接收 netlink 消息,并根据它的内容和用户设置给 udev 的规则做匹配来进行工作。

对于冷插拔的设备来说,Linux 内核提供了 sysfs 下面的一个 uevent 节点,可以往该节点写一个 “add”,导致内核重新发送 netlink,之后 udev 就可以收到冷插拔的 netlink 消息了。上述程序会dump 出如下信息:

ACTION=add
DEVPATH=/module/psmouse
SEQNUM=1682
SUBSYSTEM=module
UDEV_LOG=3
USEC_INITIALIZED=220903546792

devfs 与 udev 的一个显著区别:

  • 采用 devfs,当一个并不存在的 /dev 节点被打开的时候,devfs 能自动加载对应的驱动,而udev不这么做。
  • udev 任务驱动应该在设备被发现时,而不是设备被访问时加载。故 udev 获取设备发现时的热插拔事件,在设备发现时加载设备的驱动。

3.3 sysfs 文件系统与 Linux 设备模型

Linux 2.6 以后内核引入了 sysfs 文件系统,与 proc 类似。sysfs 把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性。sysfs 的一个目的就是展示设备驱动模型中各组件的层次关系。其顶级目录包含 block、bus、devices、class、fs、kernel、power 和 firmware 等。

block 目录包含所有的块设备;devices 目录包含系统所有的设备,并根据设备挂接的总线类型组织称层次结构;bus 目录包含系统中所有的总线类型;class 目录包含系统中的设备类型(如网卡设备、声卡设备、输入设备等)。

在 /sys/bus 的 pci 等子目录下,又会再分出 drivers 和 devices 目录中的文件是对 /sys/devices 目录中文件的符号链接。同样地,/sys/class 目录下也包含许多对 /sys/devices 下文件的链接。Linux 2.6 以后内核的设备模型。

Linux 文件系统与设备文件_第2张图片
Linux 设备模型

Linux 内核中,分别使用 bus_type、device_driver 和 device 来描述总线、驱动和设备,这 3 个结构体定义于 include/linux/device.h 头文件中,其定义如代码清单:

struct bus_type{
    const char *name;
    const char *dev_name;
    struct device *dev_root;
    
    ...
    
    int (*match)(struct device *dev, struct device_driver *drv);

    ...
};

struct device_driver{
    const char *name;
    struct bus_type *bus;

    ...

    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);

    ...
};

struct device{

    ...
    
    struct bus_type *bus; //type of bus device is on
    struct device_driver *driver; //which driver has allocated this device

    ...
    
};

device_driver 和 device 分别表示驱动和设备,而这两者都必须依附于一种总线,因此都包含 struct bus_type 指针。在 Linux 内核中,设备和驱动都是分开注册的,注册一个设备时并不需要驱动已经存在,而 1个驱动被注册的时候,也不需要对应设备已经存在,设备和驱动各自涌向内核,并寻找自己的另一半,而正是 bus_type 的match() 成员函数将两者捆绑在一起,一旦配对成功,对应总线的xxx_driver 的probe() 就被执行(xxx是总线名,如platform、pci、i2c、spi、usb等)。

总线、驱动和设备最终都会落实为 sysfs 中的 1 个目录,因为进一步追踪代码会发现,它们实际上可以认为是 kobject 的派生类,kobject 可以看作是所有总线、设备和驱动的抽象基类,1 个 kobject 对应 sysfs 中的一个目录。

总线、设备和驱动中的各个 attribute 则直接落实为 sysfs 中的 1 个文件,attribute 会伴随着 show() 和 store() 这两个函数,分别用于读写 attribute 对应的 sysfs 文件,代码给出了 attribute、bus_attribute、driver_attribute 和 device_attribute 这几个结构体的定义。

struct attribute{
    const char *name;
    umode_t mode;
   #ifdef CONFiG_DEBUG_LOCK_ALLOC
    bool ignore_lockdep:1;
    struct lock_class_key *key;
    struct lock_class_key *key;
   #endif 
};

struct bus_attribute{
    struct attribute sttr;
    ssize_t (*show)(struct bus_type *bus, char *buf);
    ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count);
};

struct driver_attribute{
    struct attribute attr;
    ssize_t (*show)(struct device_driver *driver, char *buf);
    ssize_t (*store)(struct device_driver *driver, const char *buf, size_t count);
};

struct device_attribute{
    struct attribute attr;
    ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
    ssize_t (*store)(struct device *dev, struct device_attribute *attr,
        const char *buf, size_t count);
};

事实上,sysfs 中的目录来源于 bus_type、device_driver、device,而目录中的文件则来源于 attribute。
比如,我们在 drivers/base/bus.c文件中可以找到这样的代码:

static  BUS_ATTR(drivers_probe, S_IWUSR, NULL, store_drivers_probe);
static BUS_ATTR(drivers_autoprobe, S_IWUSR | S_IRUGO, 
        show_drivers_autoprobe, store_drivers_autoprobe);
static BUS_ATTR(uevent, S_IWUSR, NULL, bus_uevent_store);

3.4 udev 的组成

udev 在用户空间执行,动态建立/删除设备文件,允许每个人都不用关心主/次设备号而提供LSB(Linux 标准规范, Linux Standard Base)名称,并可以根据需要固定名称。

udev 的工作过程如下:

  1. 当内核检测到系统中出现了新设备后,内核会通过 netlink 套接字发送uevent。
  2. udev 获取内核发送的信息,进行规则匹配。匹配的事物包括 SUBSYSTEM、ACTION、attribute、内核提供的名称(通过 KERNEL ==),以及其它环境变量。

以下是利用命令行工具 udevadm monitor --kernel --property --udev 捕获到一个虚拟机 Ubuntu 16.4.1 退出虚拟 ISO 文件的打印:

KERNEL[15730.816373] change   /devices/pci0000:00/0000:00:1f.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0 (block)
ACTION=change
DEVNAME=/dev/sr0
DEVPATH=/devices/pci0000:00/0000:00:1f.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0
DEVTYPE=disk
DISK_MEDIA_CHANGE=1
MAJOR=11
MINOR=0
SEQNUM=4324
SUBSYSTEM=block

UDEV  [15730.923482] change   /devices/pci0000:00/0000:00:1f.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0 (block)
ACTION=change
DEVLINKS=/dev/cdrom /dev/disk/by-id/ata-VBOX_CD-ROM_VB2-01700376 /dev/dvd /dev/disk/by-path/pci-0000:00:1f.1-ata-2
DEVNAME=/dev/sr0
DEVPATH=/devices/pci0000:00/0000:00:1f.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0
DEVTYPE=disk
DISK_MEDIA_CHANGE=1
ID_ATA=1
ID_BUS=ata
ID_CDROM=1
ID_CDROM_CD=1
ID_CDROM_DVD=1
ID_CDROM_MRW=1
ID_CDROM_MRW_W=1
ID_FOR_SEAT=block-pci-0000_00_1f_1-ata-2
ID_MODEL=VBOX_CD-ROM
ID_MODEL_ENC=VBOX\x20CD-ROM\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20
ID_PATH=pci-0000:00:1f.1-ata-2
ID_PATH_TAG=pci-0000_00_1f_1-ata-2
ID_REVISION=1.0
ID_SERIAL=VBOX_CD-ROM_VB2-01700376
ID_SERIAL_SHORT=VB2-01700376
ID_TYPE=cd
MAJOR=11
MINOR=0
SEQNUM=4324
SUBSYSTEM=block
SYSTEMD_READY=0
TAGS=:uaccess:systemd:seat:
USEC_INITIALIZED=1513182

可以根据以上信息,创建一个规则,以便每次插入的时候,为该盘创建一个 /dev/testISO 的符号链接。

#iso 
SUBSYSTEM=="block", ACTION=="change", KERNEL="*sr?", ENV{ID_TYPE}=="cd", SYMLINK+="testISO"

TO-DO: 实验未成功

3.5 udev 规则文件

udev 的规则文件以行为单位,以 “#” 开头的行代表注释行。其余的每一行代表一个规则。每个规则分成多个匹配部分和赋值部分。匹配部分用匹配专用的关键字来表示,匹配关键字包括:ACTION、KERNEL、BUS、SUBSYSTEM、ATTR等;赋值部分用赋值专用关键字,赋值关键字包括:NAME(创建的设备文件文件名)、SYSLINK(符号创建链接名)、OWNER(设置设备的所有者)、GROUP(设置设备的组)、IMPORT(调用外部程序)、MODE(节点访问权限)等。

由此可知,udev 可以实现设备的名称或者符号链接的自定义,通过规则文件,匹配相应设备,从而实现自定义设备名称,而devfs 则无法做到。

你可能感兴趣的:(Linux 文件系统与设备文件)