程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

linux驱动移植-linux块设备驱动基础

一、linux设备驱动

我们在linux驱动基础概念以及驱动程序框架搭建中已经介绍过,linux 将所有的外设分为 3 类:字符设备、块设备、网络设备。

1.1 字符设备

字符设备是能够像字节流(比如文件)一样被访问的设备,就是说对它的读写是以字节为单位的。 比如串口在进行收发数据时就是一个字节一个字节的进行的,我们可以在驱动程序内部使用缓冲区来存放数据以提高效率,但是串口本身对这并没有要求。字符设备的驱动程序中实现了 open、close、read、write 等系统调用,应用程序可以通过设备文件(比如/dev/ttySAC0 等)来访问字符设备。

字符设备一般包括:led、按键、usb鼠标、usb键盘等等。

1.2 块设备

块设备上的数据以块的形式存放,比如 Nand Flash上的数据就是以页为单位存放的。块设备驱动程序向用户层提供的接口与字符设备一样, 应用程序也可以通过相应的设备文件(比如/dev/mtdblock0、/dev/hda1 等)来调用 open、close、read、write 等系统调用,与块设备传送任意字节的数据。对用户而言,字符设备和块设备的访问方式没有差别。块设备驱动程序的特别之处如下。

  • 操作硬件的接口实现方式不一样:块设备驱动程序先将用户发来的数据组织成块,再写入设备;或从设备中读出若干块数据,再从中挑出用户需要的;
  • 数据块上的数据可以有一定的格式:通常在块设备中按照一定的格式存放数据,不同的文件系统类型就是用来定义这些格式的。内核中,文件系统的层次位于块设备驱动程序上面,这意味着块设备驱动程序除了向用户层提供与字符设备一样的接口外,还要向内核其他部件提供一些接口,这些接口用户是看不到的。这些接口使得可以在块设备上存放文件系统,挂载块设备。

块设备一般包括SD卡、TF卡、eMMC、Nand Flash、Nor Flash:

  • Nor Flash:可以随机读写,以“字”为基本单位、有单独的地址线和数据线,可以随机存储,可以运行程序;
  • Nand Flash:以“页面”为基本单位,通常是采用8个I/O引脚来传送控制、地址、数据信息;
  • eMMC:相当于Nand Flash + 主控IC,对外的接口协议与SD、TF卡一样,主要是针对手机或平板电脑等产品的内嵌式存储器标准规格;

针对不同的存储设备实现了不同的I/O调度算法,块设备结构的不同其I/O算法也会不用:

  • 比如对于eMMC、SD卡、Nand Flash这里没有任何机械设备的存储设备就可以任意读写任意的扇区;
  • 但是对于机械硬盘这种带有刺头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能;

1.3 网络设备

网络设备同时具有字符设备、块设备的部分特点,无法将它归入这两类中:

  • 如果说它是字符设备,它的输入/输出却是有结构的、成块的(报文、包、帧);
  • 如果说它是块设备,它的“块”又不是固定大小的,大到数百甚至数千字节,小到几字节;

UNIX 式的操作系统访问网络接口的方法是给它们分配一个唯一的名字(比如 eth0),但这个名字在文件系统中(比如/dev 目录下)不存在对应的节点项。应用程序、内核和网络驱动程序间的通信完全不同于字符设备、块设备,库、内核提供了一套和数据包传输相关的函数,而不是 open、read、write 等。

二、块设备基本知识

2.1 块设备结构

段:由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分,段在内核中由结构struct bio_vec来描述;

块:块是虚拟文件系统传输数据的基本单位,通常由1个或多个扇区组成;

扇区:扇区是硬件传输数据的基本单位,硬件一次传输一个扇区的数据到内存中。通常为512字节;

2.2 块设备流程

比如当我们要写一个很小的数据到txt文件某个位置时,由于块设备写的数据是按扇区为单位,但又不能破坏txt文件里其它位置,那么就引入了一个“缓存区”,将所有数据读到缓存区里,然后修改缓存数据,再将整个数据放入txt文件对应的某个扇区中,当我们对txt文件多次写入很小的数据的话,那么就会重复不断地对扇区读出,写入,这样会浪费很多时间在读/写硬盘上,所以内核提供了一个队列的机制,在没有关闭txt文件之前,会将读写请求进行优化,排序,合并等操作,从而提高访问硬盘的效率。

其具体流程如下:

  • 当一个用户程序要向块设备写数据时,将发出一个write()系统调用给内核;
  • 内核将调用虚拟文件系统中相应的函数,将需要写入的文件描述符和文件内容指针传递给这个函数;
  • 经过磁盘文件系统(vfat,ext2,ext3,yaffs)将把文件的读写转换为对扇区的读写,根据磁盘文件格式调用不同文件格式的写入函数,将数据发送到通用块层;
  • 数据到达通用块层(ll_rw_block是通用层的入口),就会对块设备发出写请求;
  • 在通用层发出写请求时,会先执行"I/O"调度器,调用器的作用就是把物理上相邻的读写磁盘请求合并为一个请求,提高效率;
  • 最后,块设备驱动程序向块设备发送命令和数据,将数据写入磁盘,这样一次write()操作就完成了;

我们可以使用如下流程表示:

  • 虚拟文件系统:VFS是一个抽象层,其向上提供了统一的文件访问接口,而向下则兼容了各种不同的文件系统;
  • 通用块层:提供一个统一的接口让文件系统实现者使用,而不用关心不同设备驱动程序的差异,这样实现出来的文件系统就能用于任何的块设备;

  • I/O调度层:接受通用块层发出的IO请求,缓存请求并试图合并相邻的请求,根据设置好的调度算法,回调驱动层提供的请求处理函数;

  • 块设备驱动层:具体IO交给块设备驱动层处理;

针对不同的存储设备实现了不同的I/O调度算法。块设备结构的不同其 I/O调度算法也会不同:

  • 比如对于eMMC、SD卡、Nand Flash这类没有任何机械设备的存储设备就可以任意读写任何的扇区;
  • 但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能;

2.3 块设备对象概览

我们在安装linux操作系统时,一般会将磁盘划分为多个分区,我们可以通过lsblk命令将以树状列出系统上的所有块设备信息:

root@zhengyang:/work/sambashare/linux-5.2.8# lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sr0     11:0    1  2.7G  0 rom  /media/zhengyang/Ubuntu 20.04.2.0 LTS amd642
sda      8:0    0   50G  0 disk
├─sda2   8:2    0    1K  0 part
├─sda5   8:5    0    8G  0 part [SWAP]
└─sda1   8:1    0   32G  0 part /

其意义如下:

  • NAME:块设备的名称;
  • MAJ:MIN:块设备的主设备号和次设备号;
  • RM:设备是否可移动设备。例:RM值等于1=>可移动设备;
  • SIZE :设备的容量大小信息;
  • RO :设备是否为只读;在本案例中,所有设备的RO值为0,表明他们不是只读的;
  • TYPE :显示块设备是否是磁盘或磁盘上的一个分区。在本例中,sda是磁盘,sda1、sda2、sda5是分区,而sr0是只读存储(rom)。
  • MOUNTPOINT :设备挂载的挂载点;

我们可以通过fdisk -l命令查看分区情况:

root@zhengyang:/work/sambashare/linux-5.2.8# fdisk -l
Disk /dev/sda: 50 GiB, 53687091200 bytes, 104857600 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xcde7908b

设备       启动    Start   末尾   扇区 Size Id 类型
/dev/sda1  *        2048 67108863 67106816  32G 83 Linux
/dev/sda2       67110910 83884031 16773122   8G  5 扩展
/dev/sda5       67110912 83884031 16773120   8G 82 Linux 交换 / Solaris

我们可以看到整个磁盘对应的设备文件为/dev/sda、而分区对应的设备文件为/dev/sda1、/dev/sda2、/dev/sda5,主设备号为8,磁盘对应的设备文件次设备号为0,分区对应的设备文件次设备号依次为1,2,5.

root@zhengyang:/work/sambashare/linux-5.2.8# ll /dev/sda*
brw-rw---- 1 root disk 8, 0 8月  27 10:55 /dev/sda
brw-rw---- 1 root disk 8, 1 8月  27 10:55 /dev/sda1
brw-rw---- 1 root disk 8, 2 8月  27 10:55 /dev/sda2
brw-rw---- 1 root disk 8, 5 8月  27 10:55 /dev/sda5

在linux内核中抽象出了磁盘、分区等对象:

  • 磁盘(gendisk):通用磁盘简称,抽象出以块为单位的线性排列的完整实体。一个磁盘可以创建多个分区。其中磁盘本身内嵌0号分区,它通过part_tbl指向分区表;通过queue指向了块设备驱动中分配的request_queue。另外gendisk通过全局kobj_map进行管理,每个probe管理一个磁盘设备,通过设备号可以通过bdev_map查找到probe进而通过data找到gendisk;
  • 分区表(disk_part_tbl):包含了一个数组,描述了各个分区的属性,包括起止扇区,分区名等,其中分区0描述的就是代表磁盘的分区;
  • 分区(hd_struct):是特殊的逻辑设备,每个分区覆盖了磁盘的一部分连续块,磁盘也可以理解为一个大的分区,分区编号为0,称为0号分区,其它分区从1开始编号。在对分区进行操作时,其偏移值会转换为对磁盘的偏移值,交付底层块设备执行。启动过程中,检测到磁盘会扫描分区,内存中构建磁盘和分区的关系;
  • 块设备描述符(block_device):每个分区(包括0号分区)都有一个块设备描述符,通过它可以联系上层文件系统(通过次inode,它的设备号与主inode相等)和底层的IO子系统(块设备)。每个块设备描述符通过bd_disk指向通用磁盘,通过bd_container指向了父块设备描述符,如分区的块设备描述符通过bd_container指向了磁盘的块设备描述符,所有的block_device形成一个链表;磁盘和分区都可以作为块设备独立使用,它们分别对应一个块设备描述符;
  • bdev_inode:是block_device和inode的复合体,它在添加磁盘时被创建,其中block_device就是磁盘或分区对应的设备描述符,inode为磁盘或分区对应的主inode,它属于bdev文件系统,它的设备号与从inode(块设备文件对应的inode)的设备号相同,因此通过设备文件可以找到主inode,进而找到block_device, 找到gendisk;

这些对象之间的大致关系可以使用下图表示:

三、块设备

3.1 块设备定义

linux内核中块设备的数据结构有两个:

  • 一个是struct block_device:用来描述一个块设备(比如代表磁盘的0号分区)或者块设备的一个分区;
  • 另一个是struct gendisk:用来描述整个块设备的特性;

对于一个包含多个分区的块设备,struct block_device结构有多个,而struct gendisk结构永远只有一个。

使用struct block_device定义在include/linux/fs.h文件中:

struct block_device {
        dev_t                   bd_dev;  /* not a kdev_t - it's a search key */
        int                     bd_openers;
        struct inode *          bd_inode;       /* will die */
        struct super_block *    bd_super;
        struct mutex            bd_mutex;       /* open/close mutex */
        void *                  bd_claiming;
        void *                  bd_holder;
        int                     bd_holders;
        bool                    bd_write_holder;
#ifdef CONFIG_SYSFS
        struct list_head        bd_holder_disks;
#endif
        struct block_device *   bd_contains;
        unsigned                bd_block_size;
        u8                      bd_partno;
        struct hd_struct *      bd_part;
        /* number of times partitions within this device have been opened. */
        unsigned                bd_part_count;
        int                     bd_invalidated;
        struct gendisk *        bd_disk;
        struct request_queue *  bd_queue;
        struct backing_dev_info *bd_bdi;
        struct list_head        bd_list;
        /*
         * Private data.  You must have bd_claim'ed the block_device
         * to use this.  NOTE:  bd_claim allows an owner to claim
         * the same device multiple times, the owner must take special
         * care to not mess up bd_private for that case.
         */
        unsigned long           bd_private;

        /* The counter of freeze processes */
        int                     bd_fsfreeze_count;
        /* Mutex for freeze */
        struct mutex            bd_fsfreeze_mutex;
} __randomize_layout;

其中部分成员的含义如下:

  • bd_dev:设备编号,由主设备编号和次设备编号组成;
  • bd_openers:一个引用计数,记录了该块设备打开的次数;
  • bd_inode:指向该设备文件的inode;
  • bd_super:文件系统的超级块信息;
  • bd_mutex:互斥锁;
  • bd_inodes
  • bd_contains:如果该block_device描述的是一个分区,则该变量指向描述主块设备的block_device,反之,其指向本身;
  • bd_block_size:块的大小;
  • bd_part:如果该block_device描述的是一个分区,则该变量指向分区的信息,对于gendisk,指向内置的分区0;
  • bd_part_count:如果是分区,该变量记录了分区被打开的次数,在进行分区的重新扫描前,要保证该计数值为0;
  • bd_invalidated:置1表示内存中的分区信息无效,下次打开设备时需要重新扫描分区表;
  • bd_disk:通用磁盘抽象,当该block_device作为分区抽象时,指向该分区所属的gendisk,当作为gendisk的抽象时,指向自身;
3.1.1 gendisk(通用磁盘)

linux内核使用gendisk结构体来描述一个磁盘设备,用来存储该设备的硬盘信息,包括请求队列、分区链表和块设备操作函数集等。定义在 include/linux/genhd.h中:

struct gendisk {
        /* major, first_minor and minors are input parameters only,
         * don't use directly.  Use disk_devt() and disk_max_parts().
         */
        int major;                      /* major number of driver */
        int first_minor;
        int minors;                     /* maximum number of minors, =1 for
                                         * disks that can't be partitioned. */

        char disk_name[DISK_NAME_LEN];  /* name of major driver */
        char *(*devnode)(struct gendisk *gd, umode_t *mode);

        unsigned short events;          /* supported events */
        unsigned short event_flags;     /* flags related to event processing */

        /* Array of pointers to partitions indexed by partno.
         * Protected with matching bdev lock but stat and other
         * non-critical accesses use RCU.  Always access through
         * helpers.
         */
        struct disk_part_tbl __rcu *part_tbl;
        struct hd_struct part0;

        const struct block_device_operations *fops;
        struct request_queue *queue;
        void *private_data;

        int flags;
        struct rw_semaphore lookup_sem;
        struct kobject *slave_dir;

        struct timer_rand_state *random;
        atomic_t sync_io;               /* RAID */
        struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITY
        struct kobject integrity_kobj;
#endif  /* CONFIG_BLK_DEV_INTEGRITY */
        int node_id;
        struct badblocks *bb;
        struct lockdep_map lockdep_map;
};

其中部分成员的含义如下:

  • major:通过register_blkdev函数申请的主设备号;
  • first_minor:磁盘的第一个次设备号,一般设置为0;
  • minors:磁盘的次设备号数量,也就是磁盘的分区数量,0号次设备号表示的是整个磁盘;这些分区的主设备号一样,次设备号不同;minors最小值为1,表示磁盘不分区,大于1表示磁盘分区数据量为minors-1;
  • disk_name:磁盘名,磁盘设备注册到内核后,可以在/dev下看到名为disk_name的块设备;
  • part_tbl:指向磁盘对应的分区表;
  • part0:0号分区,磁盘也可以理解为一个大的分区,分区编号为0,称为0号分区;
  • fops:块设备操作;
  • queue:磁盘对应的请求队列,针对该磁盘设备的请求都放到此队列中,可通过blk_mq_init_sq_queue函数分配;
3.1.2 block_device_operations(块设备操作)

和字符设备的file _operations一样,块设备也有操作集,其定义在include/linux/blkdev.h:

struct block_device_operations {
        int (*open) (struct block_device *, fmode_t);
        void (*release) (struct gendisk *, fmode_t);
        int (*rw_page)(struct block_device *, sector_t, struct page *, unsigned int);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        unsigned int (*check_events) (struct gendisk *disk,
                                      unsigned int clearing);
        /* ->media_changed() is DEPRECATED, use ->check_events() instead */
        int (*media_changed) (struct gendisk *);
        void (*unlock_native_capacity) (struct gendisk *);
        int (*revalidate_disk) (struct gendisk *);
        int (*getgeo)(struct block_device *, struct hd_geometry *);
        /* this callback is with swap_lock and sometimes page table lock held */
        void (*swap_slot_free_notify) (struct block_device *, unsigned long);
        int (*report_zones)(struct gendisk *, sector_t sector,
                            struct blk_zone *zones, unsigned int *nr_zones,
                            gfp_t gfp_mask);
        struct module *owner;
        const struct pr_ops *pr_ops;
};

其中部分函数指针的意义:

  • open:当打开一个块设备的时候被调用;
  • release:当关闭一个块设备的时候被调用;
  • getgeo:获取驱动器的集合信息,获取到的信息会被填充在一个hd_geometry结构中;

我们发现上面的操作函数里面并没有read、write函数,那如何对块设备进行读写操作的呢?

要想了解这个流程, 这里我们还需要了解块设备相关的另外几个数据结构request_queue、request、bio。这个我们这一节先不介绍,下一节再来介绍,

3.1.3 disk_part_tbl(分区表)

定义在include/linux/genhd.h中:

struct disk_part_tbl {
        struct rcu_head rcu_head;
        int len;
        struct hd_struct __rcu *last_lookup;
        struct hd_struct __rcu *part[];  // 指针数组,每一个成员都是 struct hd_struct*元素
};
3.1.4 hd_struct(分区)

定义在include/linux/genhd.h中:

struct hd_struct {
        sector_t start_sect;
        /*
         * nr_sects is protected by sequence counter. One might extend a
         * partition while IO is happening to it and update of nr_sects
         * can be non-atomic on 32bit machines with 64bit sector_t.
         */
        sector_t nr_sects;
        seqcount_t nr_sects_seq;
        sector_t alignment_offset;
        unsigned int discard_alignment;
        struct device __dev;
        struct kobject *holder_dir;
        int policy, partno;
        struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
        int make_it_fail;
#endif
        unsigned long stamp;
#ifdef  CONFIG_SMP
        struct disk_stats __percpu *dkstats;
#else
        struct disk_stats dkstats;
#endif
        struct percpu_ref ref;
        struct rcu_work rcu_work;
};

存放分区信息,以及该分区对应的内核设备对象device。

3.2 块设备注册

和字符设备驱动一样,我们需要向内核注册新的块设备,块设备注册函数为register_blkdev(这个函数更准确的说应该是为类似Nand Flash、Nor Flash的块设备申请一个块主设备号),定义在block/genhd.c文件中:

/**
 * register_blkdev - register a new block device
 *
 * @major: the requested major device number [1..BLKDEV_MAJOR_MAX-1]. If
 *         @major = 0, try to allocate any unused major number.
 * @name: the name of the new block device as a zero terminated string
 *
 * The @name must be unique within the system.
 *
 * The return value depends on the @major input parameter:
 *
 *  - if a major device number was requested in range [1..BLKDEV_MAJOR_MAX-1]
 *    then the function returns zero on success, or a negative error code
 *  - if any unused major number was requested with @major = 0 parameter
 *    then the return value is the allocated major number in range
 *    [1..BLKDEV_MAJOR_MAX-1] or a negative error code otherwise
 *
 * See Documentation/admin-guide/devices.txt for the list of allocated
 * major numbers.
 */
int register_blkdev(unsigned int major, const char *name)
{
        struct blk_major_name **n, *p;
        int index, ret = 0;

        mutex_lock(&block_class_lock);

        /* temporary */
        if (major == 0) {
                for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) {  // 遍历设备名称列表(索引为主设备号),如果某个元素位置未被占用,跳出
                        if (major_names[index] == NULL)
                                break;
                }

                if (index == 0) {
                        printk("%s: failed to get major for %s\n",
                               __func__, name);
                        ret = -EBUSY;
                        goto out;
                }
                major = index;  // 作为主设备号
                ret = major;
        }

        if (major >= BLKDEV_MAJOR_MAX) {
                pr_err("%s: major requested (%u) is greater than the maximum (%u) for %s\n",
                       __func__, major, BLKDEV_MAJOR_MAX-1, name);

                ret = -EINVAL;
                goto out;
        }

        p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);  // 动态申请内存空间
        if (p == NULL) {
                ret = -ENOMEM;
                goto out;
        }

        p->major = major;
        strlcpy(p->name, name, sizeof(p->name));
        p->next = NULL;
        index = major_to_index(major);

        for (n = &major_names[index]; *n; n = &(*n)->next) {  // 根据主设备号,查找当前块设备
                if ((*n)->major == major)
                        break;
        }
        if (!*n)
                *n = p;    // 保存当前块设备主设备号以及设备名称信息
        else
                ret = -EBUSY;

        if (ret < 0) {
                printk("register_blkdev: cannot get major %u for %s\n",
                       major, name);
                kfree(p);
        }
out:
        mutex_unlock(&block_class_lock);
        return ret;
}

参数含义如下:

  • major:主设备号;
  • name:块设备名称;

返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。这个函数在我看来实际上只是起到一个分配主设备号的作用,如果设备号分配成功,我们就可以将该主设备号分配给磁盘设备gendisk。

major_name数组:

#define BLKDEV_MAJOR_HASH_SIZE 255
static struct blk_major_name {
        struct blk_major_name *next;
        int major;
        char name[16];
} *major_names[BLKDEV_MAJOR_HASH_SIZE];

其中BLKDEV_MAJOR_HASH_SIZE = 255,这是一个指针数组,其中的每一个元素都指向了一个 struct blk_major_name的结构体,该结构体就是用来存放块设备的主设备号和设备名称的。

该函数总结:

  • 注册块设备时,可以把0当做主设备号传入,从而让内核自动分配一个主设备号;
  • 块设备的主设备号理论上有2^32个,而不是必须小于255;
  • 通过此函数注册后,可以在/proc/devices下看到(cat /proc/devices);
  • major_names数组(数组中的每个元素都是一个指针)可能的最终构成如图:

3.3 块设备卸载

注销块设备函数为unregister_blkdev这个函数更准确的说应该是收回为类似Nand Flash、Nor Flash的块设备申请的主设备号):

void unregister_blkdev(unsigned int major, const char *name)
{
        struct blk_major_name **n;
        struct blk_major_name *p = NULL;
        int index = major_to_index(major);

        mutex_lock(&block_class_lock);
        for (n = &major_names[index]; *n; n = &(*n)->next)
                if ((*n)->major == major)
                        break;
        if (!*n || strcmp((*n)->name, name)) {
                WARN_ON(1);
        } else {
                p = *n;
                *n = p->next;
        }
        mutex_unlock(&block_class_lock);
        kfree(p);
}

四、磁盘(gendisk)

4.1 申请gendisk

使用gendisk之前要先申请,使用alloc_disk宏进行申请,定义在include/linux/genhd.h:

#define alloc_disk_node(minors, node_id)                                \
({                                                                      \
        static struct lock_class_key __key;                             \
        const char *__name;                                             \
        struct gendisk *__disk;                                         \
                                                                        \
        __name = "(gendisk_completion)"#minors"("#node_id")";           \
                                                                        \
        __disk = __alloc_disk_node(minors, node_id);                    \
                                                                        \
        if (__disk)                                                     \
                lockdep_init_map(&__disk->lockdep_map, __name, &__key, 0); \
                                                                        \
        __disk;                                                         \
})

#define alloc_disk(minors) alloc_disk_node(minors, NUMA_NO_NODE)

第一个参数为次设备号数量,minors最小值为1,表示磁盘不分区,大于1表示磁盘分区数据量为minors-1,次设备号0表示整个磁盘,1表示第一个分区;成功返回申请成功的gendisk,失败返回NULL。

__alloc_disk_node定义在block/genhd.c文件中:

struct gendisk *__alloc_disk_node(int minors, int node_id)
{
        struct gendisk *disk;
        struct disk_part_tbl *ptbl;

        if (minors > DISK_MAX_PARTS) {
                printk(KERN_ERR
                        "block: can't allocate more than %d partitions\n",
                        DISK_MAX_PARTS);
                minors = DISK_MAX_PARTS;
        }

        disk = kzalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id); // 分配gendist对象(包含成员0号分区以及对应的device)
        if (disk) {
                if (!init_part_stats(&disk->part0)) {
                        kfree(disk);
                        return NULL;
                }
                init_rwsem(&disk->lookup_sem);
                disk->node_id = node_id;
                if (disk_expand_part_tbl(disk, 0)) { // 初始化0号分区,0号分区代表磁盘
                        free_part_stats(&disk->part0);
                        kfree(disk);
                        return NULL;
                }
                ptbl = rcu_dereference_protected(disk->part_tbl, 1);  
                rcu_assign_pointer(ptbl->part[0], &disk->part0);  // 将磁盘0号分区添加到磁盘分区表,作为分区表的第一个元素,0号分区并没有额外分配空间

                /*
                 * set_capacity() and get_capacity() currently don't use
                 * seqcounter to read/update the part0->nr_sects. Still init
                 * the counter as we can read the sectors in IO submission
                 * patch using seqence counters.
                 *
                 * TODO: Ideally set_capacity() and get_capacity() should be
                 * converted to make use of bd_mutex and sequence counters.
                 */
                seqcount_init(&disk->part0.nr_sects_seq);
                if (hd_ref_init(&disk->part0)) {
                        hd_free_part(&disk->part0);
                        kfree(disk);
                        return NULL;
                }

                disk->minors = minors;
                rand_initialize_disk(disk);  // 帮助内核产生随机数
                disk_to_dev(disk)->class = &block_class;   // 设置磁盘0号分区device->calss = &block_class,这个直观体现就是会在/sys/blocka目录下创建名为disk->name的类,
// 如/sys/block/sda
disk_to_dev(disk)->type = &disk_type; // 设置磁盘0号分区device->type = & disk_type device_initialize(disk_to_dev(disk)); // 将磁盘0号分区ddevice初始化,此为设备模型的常规做法, // 由于对应的device没有父kobject,因此父对象设置为devices_kset->kobj,这里将type设为disk_type,区别于分区为part_type } return disk; }

该函数会分配一个gendisk对象,并初始化0号分区。一般在块设备驱动的probe中调用,如:对于ufs设备来讲,在drivers/scsi/sd.c的sd_probe中调用。del_gendisk与alloc_gendisk的作用相反。

在这里我们好像并没有看到磁盘0号分区device的设备号设置,这块我们先保留着疑问,继续看代码。

class类block_class定义在block/genhd.c:

struct class block_class = {
        .name           = "block",
};

此外,我们要再讨论一下disk_expand_part_tbl函数,该函数有两个参数,第一个参数为要被扩展的磁盘、第二个参数为要扩展的分区个数。

  • 该函数先获取gendisk->part_tbl,根据part_tbl是否存在判断原始磁盘的分区个数是否为0,如果part_tbl存在则根据part_tbl->len获取原始磁盘分区个数目。;
  • 然后申请新的part_tbl和相应个数的分区结构;
  • 最后把原始磁盘的分区拷贝到新的分区中,最后使用rcu_assign_pointernew_ptbl代替old_ptbl;

由于这里分区个数传入的为0,因此只会创建一个代表磁盘的0号分区。

4.2 删除gendisk

del_gendisk用来释放内核中的gendisk对象,一般在块设备驱动函数出口函数中使用:

void del_gendisk(struct gendisk *disk)
{
        struct disk_part_iter piter;
        struct hd_struct *part;

        blk_integrity_del(disk);
        disk_del_events(disk);

        /*
         * Block lookups of the disk until all bdevs are unhashed and the
         * disk is marked as dead (GENHD_FL_UP cleared).
         */
        down_write(&disk->lookup_sem);
        /* invalidate stuff */
        disk_part_iter_init(&piter, disk,
                             DISK_PITER_INCL_EMPTY | DISK_PITER_REVERSE);
        while ((part = disk_part_iter_next(&piter))) {
                invalidate_partition(disk, part->partno);
                bdev_unhash_inode(part_devt(part));
                delete_partition(disk, part->partno);
        }
        disk_part_iter_exit(&piter);

        invalidate_partition(disk, 0);
        bdev_unhash_inode(disk_devt(disk));
        set_capacity(disk, 0);
        disk->flags &= ~GENHD_FL_UP;
        up_write(&disk->lookup_sem);

        if (!(disk->flags & GENHD_FL_HIDDEN))
                sysfs_remove_link(&disk_to_dev(disk)->kobj, "bdi");
        if (disk->queue) {
                /*
                 * Unregister bdi before releasing device numbers (as they can
                 * get reused and we'd get clashes in sysfs).
                 */
                if (!(disk->flags & GENHD_FL_HIDDEN))
                        bdi_unregister(disk->queue->backing_dev_info);
                blk_unregister_queue(disk);
        } else {
                WARN_ON(1);
        }

        if (!(disk->flags & GENHD_FL_HIDDEN))
                blk_unregister_region(disk_devt(disk), disk->minors);
        /*
         * Remove gendisk pointer from idr so that it cannot be looked up
         * while RCU period before freeing gendisk is running to prevent
         * use-after-free issues. Note that the device number stays
         * "in-use" until we really free the gendisk.
         */
        blk_invalidate_devt(disk_devt(disk));

        kobject_put(disk->part0.holder_dir);
        kobject_put(disk->slave_dir);

        part_stat_set_all(&disk->part0, 0);
        disk->part0.stamp = 0;
        if (!sysfs_deprecated)
                sysfs_remove_link(block_depr, dev_name(disk_to_dev(disk)));
        pm_runtime_set_memalloc_noio(disk_to_dev(disk), false);
        device_del(disk_to_dev(disk));
}

4.3 添加gendisk

device_add_disk将磁盘gendisk及其及分区添加到devices树及sysfs中,注意此时只会生成代表磁盘的0号分区的block_device,其它分区的block_device不会创建,其它分区的block_devcie是在代表某个分区的设备文件被打开的时候被创建。

device_add_disk一般在块设备驱动的probe中调用,如:对于ufs设备来讲,在drivers/scsi/sd.c的sd_probe中调用:

/**
 * __device_add_disk - add disk information to kernel list
 * @parent: parent device for the disk
 * @disk: per-device partitioning information
 * @groups: Additional per-device sysfs groups
 * @register_queue: register the queue if set to true
 *
 * This function registers the partitioning information in @disk
 * with the kernel.
 *
 * FIXME: error handling
 */
static void __device_add_disk(struct device *parent, struct gendisk *disk,
                              const struct attribute_group **groups,
                              bool register_queue)
{
        dev_t devt;
        int retval;

        /* minors == 0 indicates to use ext devt from part0 and should
         * be accompanied with EXT_DEVT flag.  Make sure all
         * parameters make sense.
         */
        WARN_ON(disk->minors && !(disk->major || disk->first_minor));
        WARN_ON(!disk->minors &&
                !(disk->flags & (GENHD_FL_EXT_DEVT | GENHD_FL_HIDDEN)));

        disk->flags |= GENHD_FL_UP;

        retval = blk_alloc_devt(&disk->part0, &devt);  // 为磁盘0号分区分配设备号,保存在devt中
        if (retval) {
                WARN_ON(1);
                return;
        }
        disk->major = MAJOR(devt);    // 设置主设备号
        disk->first_minor = MINOR(devt);  // 设置次设备号

        disk_alloc_events(disk);

        if (disk->flags & GENHD_FL_HIDDEN) {  // 对隐藏分区的处理
                /*
                 * Don't let hidden disks show up in /proc/partitions,
                 * and don't bother scanning for partitions either.
                 */
                disk->flags |= GENHD_FL_SUPPRESS_PARTITION_INFO;
                disk->flags |= GENHD_FL_NO_PART_SCAN;
        } else {                             // 对正常分区的处理
                int ret;

                /* Register BDI before referencing it from bdev */
                disk_to_dev(disk)->devt = devt;           // 设置磁盘0号分区device设备号
                ret = bdi_register_owner(disk->queue->backing_dev_info, 
                                                disk_to_dev(disk));
                WARN_ON(ret);
                blk_register_region(disk_devt(disk), disk->minors, NULL,
                                    exact_match, exact_lock, disk);   // 通过kobj_map建立磁盘设备号和gendisk的映射关系
        }
       register_disk(parent, disk, groups);  // 将代表整个磁盘的0号分区内嵌设备加入到sysfs中,建立磁盘与分区的关系,并将磁盘添加到系统中,期间bdget会获取整个磁盘对应的struct bdev_inode,
                                             // 它包含了struct block_device和主struct inode    
        if (register_queue)
                blk_register_queue(disk);     // 在sysfs代表磁盘分区的目录下创建queue子目录,它代表通用磁盘的请求队列,每个gendisk->queue来源于块设备驱动的request_queue

        /*
         * Take an extra ref on queue which will be put on disk_release()
         * so that it sticks around as long as @disk is there.
         */
        WARN_ON_ONCE(!blk_get_queue(disk->queue));

        disk_add_events(disk);
        blk_integrity_add(disk);
}

void device_add_disk(struct device *parent, struct gendisk *disk,
                     const struct attribute_group **groups)

{
        __device_add_disk(parent, disk, groups, true);
}

struct kobj_map内部维护了struct probe *probe[255]散列表,由数组+链表组成,数组长度最大为255:

block/genhd.c:440:static struct kobj_map *bdev_map;
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]; // 指针数组,每个元素都是 probe类型的指针
        struct mutex *lock;
};

每个probe[i]指向了一个probe链表,而每个probe内嵌了gendisk 指针(data)和磁盘设备号(dev),且每个probe按照次设备号的升序排列,这样就完成了磁盘设备号与通用磁盘描述符gendisk的映射关系。

如下图显示了两个满足主设备号major%255=2的块设备通过调用blk_register_region后的情况(即hash冲突情况下)。此函数调用完毕后形成如下图,这样通过磁盘设备号就可以直接检索到对应的gendisk磁盘描述符。

注:probe中dev为代表磁盘的设备号,range为从次设备号开始连续的设备数量,也就是说上图中的每个probe代表的是一个磁盘设备。

4.3.1 blk_register_region

blk_register_region定义在block/genhd.c文件中:

/*
 * Register device numbers dev..(dev+range-1)
 * range must be nonzero
 * The hash chain is sorted on range, so that subranges can override.
 */
void blk_register_region(dev_t devt, unsigned long range, struct module *module,
                         struct kobject *(*probe)(dev_t, int *, void *),
                         int (*lock)(dev_t, void *), void *data)
{
        kobj_map(bdev_map, devt, range, module, probe, lock, data);
}

kobj_map定义在drivers/base/map.c:

int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
             struct module *module, kobj_probe_t *probe,
             int (*lock)(dev_t, void *), void *data)
{
        unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
        unsigned index = MAJOR(dev);
        unsigned i;
        struct probe *p;

        if (n > 255)
                n = 255;

        p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
        if (p == NULL)
                return -ENOMEM;

        for (i = 0; i < n; i++, p++) {
                p->owner = module;
                p->get = probe;
                p->lock = lock;
                p->dev = dev;
                p->range = range;
                p->data = data;
        }
        mutex_lock(domain->lock);
        for (i = 0, p -= n; i < n; i++, p++, index++) {
                struct probe **s = &domain->probes[index % 255];
                while (*s && (*s)->range < range)
                        s = &(*s)->next;
                p->next = *s;
                *s = p;
        }
        mutex_unlock(domain->lock);
        return 0;
}
4.3.2 register_disk

register_disk将代表磁盘的0号分区的device注册到sysfs完成了磁盘的注册,然后:

  • 创建整个磁盘disk对应的块设备描述符block_device;
  • 扫描硬盘分区表(一般分为GPT、MBR)并解析创建各个分区对应的hd_struct,建立磁盘disk与各个分区的关系:
static void register_disk(struct device *parent, struct gendisk *disk,
                          const struct attribute_group **groups)
{
        struct device *ddev = disk_to_dev(disk);  // 代表磁盘0号分区的device
        struct block_device *bdev;
        struct disk_part_iter piter;
        struct hd_struct *part;
        int err;

        ddev->parent = parent;

        dev_set_name(ddev, "%s", disk->disk_name);  // 设置代表磁盘0号分区的device的设备名称

        /* delay uevents, until we scanned partition table */
        dev_set_uevent_suppress(ddev, 1);   // 设置扫描完分区表再发送uevent时间

        if (groups) {
                WARN_ON(ddev->groups);
                ddev->groups = groups;
        }
        if (device_add(ddev))     
                return;
        if (!sysfs_deprecated) {
                err = sysfs_create_link(block_depr, &ddev->kobj,
                                        kobject_name(&ddev->kobj));
                if (err) {
                        device_del(ddev);
                        return;
                }
        }
       /*
         * avoid probable deadlock caused by allocating memory with
         * GFP_KERNEL in runtime_resume callback of its all ancestor
         * devices
         */
        pm_runtime_set_memalloc_noio(ddev, true);

        disk->part0.holder_dir = kobject_create_and_add("holders", &ddev->kobj);
        disk->slave_dir = kobject_create_and_add("slaves", &ddev->kobj);

        if (disk->flags & GENHD_FL_HIDDEN) {
                dev_set_uevent_suppress(ddev, 0);
                return;
        }

        /* No minors to use for partitions */
        if (!disk_part_scan_enabled(disk))
                goto exit;

        /* No such device (e.g., media were just removed) */
        if (!get_capacity(disk))
                goto exit;

        bdev = bdget_disk(disk, 0);  // 获取0号分区对应的块设备描述符
        if (!bdev)
                goto exit;

        bdev->bd_invalidated = 1;
        err = blkdev_get(bdev, FMODE_READ, NULL);    // 如果首次打开代表磁盘的块设备,则扫描硬盘分区,创建各个分区hd_struct,并建立与磁盘disk的关系
        if (err < 0)
                goto exit;
        blkdev_put(bdev, FMODE_READ);

exit:
        /* announce disk after possible partitions are created */
        dev_set_uevent_suppress(ddev, 0);  // 现在磁盘和分区都已经准备好,消除延迟发送
        kobject_uevent(&ddev->kobj, KOBJ_ADD);   // 发送uevent给用户空间,告知磁盘已添加

        /* announce possible partitions */
        disk_part_iter_init(&piter, disk, 0);
        while ((part = disk_part_iter_next(&piter)))
                kobject_uevent(&part_to_dev(part)->kobj, KOBJ_ADD); // 发送uevent给用户空间,告知分区已添加
        disk_part_iter_exit(&piter);

        if (disk->queue->backing_dev_info->dev) {
                err = sysfs_create_link(&ddev->kobj,
                          &disk->queue->backing_dev_info->dev->kobj,
                          "bdi");
                WARN_ON(err);
        }
}

在alloc_disk中我们已经使用device_initialize初始化了0号分区的device设备,在这里将会执行device_add,将代表磁盘设备的device加入到linux设备模型;

bdget_disk(disk,0)函数根据磁盘设备号返回(或者创建初始化)整个磁盘(0号分区)对应的块设备描述符block_device;

blkdev_get(bdev,FMODE_READ,NULL)根据磁盘设备号查询散列表bdev_map,获取通用磁盘描述符;如果首次打开代表磁盘的块设备,则扫描分区,建立磁盘与分区的关系,并添加到系统中,期间会创建各个hd_struct;

4.3.3 bdget_disk(获取块设备)
/**
 * bdget_disk - do bdget() by gendisk and partition number
 * @disk: gendisk of interest
 * @partno: partition number
 *
 * Find partition @partno from @disk, do bdget() on it.
 *
 * CONTEXT:
 * Don't care.
 *
 * RETURNS:
 * Resulting block_device on success, NULL on failure.
 */
struct block_device *bdget_disk(struct gendisk *disk, int partno)  // 参数2为0
{
        struct hd_struct *part;
        struct block_device *bdev = NULL;

        part = disk_get_part(disk, partno);    // 获取0号分区
        if (part)
                bdev = bdget(part_devt(part)); // 传入分区设备号
        disk_put_part(part);

        return bdev;
}

bdget根据设备号返回分区对应的块设备描述符block_device。

函数disk_get_part定义如下:

struct hd_struct *__disk_get_part(struct gendisk *disk, int partno)
{
        struct disk_part_tbl *ptbl = rcu_dereference(disk->part_tbl);

        if (unlikely(partno < 0 || partno >= ptbl->len))
                return NULL;
        return rcu_dereference(ptbl->part[partno]);   
}

当我们调用bdget_disk打开0号分区时,如果此时已经建立了磁盘和0号分区之间的关系,那么part将不为NULL,此时将会执行bdget。

bdget定义在fs/block_dev.c:

struct block_device *bdget(dev_t dev)  // 设备号
{
        struct block_device *bdev;
        struct inode *inode;

        inode = iget5_locked(blockdev_superblock, hash(dev), // 根据磁盘设备号(或分区设备号)获取inode,如果不存在则创建bdev_inode,并返回inode
                        bdev_test, bdev_set, &dev);

        if (!inode)
                return NULL;

        bdev = &BDEV_I(inode)->bdev;     // 根据次inode获取bdev

        if (inode->i_state & I_NEW) {    // 如果是新创建的inode、则执行初始化
                bdev->bd_contains = NULL;
                bdev->bd_super = NULL;
                bdev->bd_inode = inode;
                bdev->bd_block_size = i_blocksize(inode);
                bdev->bd_part_count = 0;
                bdev->bd_invalidated = 0;
                inode->i_mode = S_IFBLK;
                inode->i_rdev = dev;     // 初始化设备号,与次inode一致
                inode->i_bdev = bdev;
                inode->i_data.a_ops = &def_blk_aops;
                mapping_set_gfp_mask(&inode->i_data, GFP_USER);
                spin_lock(&bdev_lock);
                list_add(&bdev->bd_list, &all_bdevs);
                spin_unlock(&bdev_lock);
                unlock_new_inode(inode);
        }
        return bdev;
}

iget5_locked函数用于获取inode,如果 inode 在内存中已经存在,则直接返回;否则创建一个新的 inode。如果是新创建的 inode,通过 super_block->s_op->read_inode() 来填充它。也就是说,如何填充一个新创建的 inode, 是由具体文件系统提供的函数实现的。具体参考基于vfs实现自己的文件系统

对上步创建的bdev_inode中的block_dev和inode执行初始化,会初始化inode内嵌的address_space->a_ops为def_blk_aops,它会在读写块设备文件时被调用。

4.3.4 blkdev_get

blkdev_get定义在fs/block_dev.c文件:

/**
 * blkdev_get - open a block device
 * @bdev: block_device to open
 * @mode: FMODE_* mask
 * @holder: exclusive holder identifier
 *
 * Open @bdev with @mode.  If @mode includes %FMODE_EXCL, @bdev is
 * open with exclusive access.  Specifying %FMODE_EXCL with %NULL
 * @holder is invalid.  Exclusive opens may nest for the same @holder.
 *
 * On success, the reference count of @bdev is unchanged.  On failure,
 * @bdev is put.
 *
 * CONTEXT:
 * Might sleep.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int blkdev_get(struct block_device *bdev, fmode_t mode, void *holder)
{
        struct block_device *whole = NULL;
        int res;

        WARN_ON_ONCE((mode & FMODE_EXCL) && !holder);

        if ((mode & FMODE_EXCL) && holder) {
                whole = bd_start_claiming(bdev, holder);
                if (IS_ERR(whole)) {
                        bdput(bdev);
                        return PTR_ERR(whole);
                }
        }

        res = __blkdev_get(bdev, mode, 0);

        if (whole) {
                struct gendisk *disk = whole->bd_disk;

                /* finish claiming */
                mutex_lock(&bdev->bd_mutex);
                bd_finish_claiming(bdev, whole, holder);
                /*
                 * Block event polling for write claims if requested.  Any
                 * write holder makes the write_holder state stick until
                 * all are released.  This is good enough and tracking
                 * individual writeable reference is too fragile given the
                 * way @mode is used in blkdev_get/put().
                 */
                if (!res && (mode & FMODE_WRITE) && !bdev->bd_write_holder &&
                    (disk->flags & GENHD_FL_BLOCK_EVENTS_ON_EXCL_WRITE)) {
                        bdev->bd_write_holder = true;
                        disk_block_events(disk);
                }

                mutex_unlock(&bdev->bd_mutex);
                bdput(whole);
        }

        return res;
}

这里调用__blkdev_get函数打开0号分区对应的块设备描述符block_device:

/*
 * bd_mutex locking:
 *
 *  mutex_lock(part->bd_mutex)
 *    mutex_lock_nested(whole->bd_mutex, 1)
 */

static int __blkdev_get(struct block_device *bdev, fmode_t mode, int for_part)
{
        struct gendisk *disk;
        int ret;
        int partno;
        int perm = 0;
        bool first_open = false;

        if (mode & FMODE_READ)
                perm |= MAY_READ;
        if (mode & FMODE_WRITE)
                perm |= MAY_WRITE;
        /*
         * hooks: /n/, see "layering violations".
         */
        if (!for_part) {
                ret = devcgroup_inode_permission(bdev->bd_inode, perm);
                if (ret != 0) {
                        bdput(bdev);
                        return ret;
                }
        }

 restart:

        ret = -ENXIO;
        //根据bdev->bd_dev(设备号)获取gendisk,并将分区号(也就是次设备号)保存在partno,主要通过查询bdev_map映射域
        disk = bdev_get_gendisk(bdev, &partno);   
        if (!disk)
                goto out;

        disk_block_events(disk);
        mutex_lock_nested(&bdev->bd_mutex, for_part);
        // 首次打开块设备
        if (!bdev->bd_openers) {            
                first_open = true;
                //建立bdev与磁盘描述符的关联
                bdev->bd_disk = disk;          // 初始化bd_disk
                bdev->bd_queue = disk->queue;  // 初始化bd_queue
                bdev->bd_contains = bdev;      // 指向自身
                bdev->bd_partno = partno;      // 分区号  
                if (!partno) {                            // 0号分区,代表磁盘自身,即打开整个磁盘块设备
                        ret = -ENXIO;
                        bdev->bd_part = disk_get_part(disk, partno);  // 初始化bd_part为0号分区对象,即disk->part_tbl->part[0]
                        if (!bdev->bd_part)
                                goto out_clear;

                        ret = 0;
                        if (disk->fops->open) {
                                ret = disk->fops->open(bdev, mode);  // 执行磁盘描述符的open回调
                                if (ret == -ERESTARTSYS) {
                                        /* Lost a race with 'disk' being
                                         * deleted, try again.
                                         * See md.c
                                         */
                                        disk_put_part(bdev->bd_part);
                                        bdev->bd_part = NULL;
                                        bdev->bd_disk = NULL;
                                        bdev->bd_queue = NULL;
                                        mutex_unlock(&bdev->bd_mutex);
                                        disk_unblock_events(disk);
                                        put_disk_and_module(disk);
                                        goto restart;
                                }
                        }

                        if (!ret) {
                                bd_set_size(bdev,(loff_t)get_capacity(disk)<<9);
                                set_init_blocksize(bdev);
                        }

                        /*
                         * If the device is invalidated, rescan partition
                         * if open succeeded or failed with -ENOMEDIUM.
                         * The latter is necessary to prevent ghost
                         * partitions on a removed medium.
                         */
                        if (bdev->bd_invalidated) {
                                if (!ret)
                                        rescan_partitions(disk, bdev);
                                else if (ret == -ENOMEDIUM)
                                        invalidate_partitions(disk, bdev);
                        }

                        if (ret)
                                goto out_clear;
                } else {    //  打开分区对应的块设备
                        struct block_device *whole;
                        // 对非0号分区,根据磁盘设备号返回整个磁盘(0号分区)对应的块设备描述符block_device
                        whole = bdget_disk(disk, 0);
                        ret = -ENOMEM;
                        if (!whole)
                                goto out_clear;
                        BUG_ON(for_part);
                        ret = __blkdev_get(whole, mode, 1);
                        if (ret)
                                goto out_clear;
                        bdev->bd_contains = whole;
                        bdev->bd_part = disk_get_part(disk, partno);
                        if (!(disk->flags & GENHD_FL_UP) ||
                            !bdev->bd_part || !bdev->bd_part->nr_sects) {
                                ret = -ENXIO;
                                goto out_clear;
                        }
                        bd_set_size(bdev, (loff_t)bdev->bd_part->nr_sects << 9);
                        set_init_blocksize(bdev);
                }

                if (bdev->bd_bdi == &noop_backing_dev_info)
                        bdev->bd_bdi = bdi_get(disk->queue->backing_dev_info);
        } else {    // 非首次打开块设备
                if (bdev->bd_contains == bdev) {
                        ret = 0;
                        if (bdev->bd_disk->fops->open)
                                ret = bdev->bd_disk->fops->open(bdev, mode);
                        /* the same as first opener case, read comment there */
                        if (bdev->bd_invalidated) {
                                if (!ret)
                                        rescan_partitions(bdev->bd_disk, bdev);   // 扫描分区表并解析,更新到磁盘的分区表中
                                else if (ret == -ENOMEDIUM)
                                        invalidate_partitions(bdev->bd_disk, bdev);
                        }
                        if (ret)
                                goto out_unlock_bdev;
                }
        }

        bdev->bd_openers++;
        if (for_part)
                bdev->bd_part_count++;
        mutex_unlock(&bdev->bd_mutex);
        disk_unblock_events(disk);
        /* only one opener holds refs to the module and disk */
        if (!first_open)
                put_disk_and_module(disk);
        return 0;

 out_clear:
        disk_put_part(bdev->bd_part);
        bdev->bd_disk = NULL;
        bdev->bd_part = NULL;
        bdev->bd_queue = NULL;
        if (bdev != bdev->bd_contains)
                __blkdev_put(bdev->bd_contains, mode, 1);
        bdev->bd_contains = NULL;
 out_unlock_bdev:
        mutex_unlock(&bdev->bd_mutex);
        disk_unblock_events(disk);
        put_disk_and_module(disk);
 out:
        bdput(bdev);

        return ret;
}

__blkdev_get主要用于建立磁盘块设备描述符block_device,分区块设备描述符block_device, 磁盘描述符gendisk,,分区hd_struct的关系,并调用磁盘描述符的open回调。

对于需要重新扫描分区的则需要解析分区表信息,并保存在磁盘的分区表(disk->part_tbl->part)。从本函数可以看出无论是磁盘还是分区都会创建相应的block_device。

1)第一次打开块设备:无论分区块设备还是磁盘块设备,都建立bdev与磁盘描述符gendisk的关联;

  • 打开整个磁盘块设备:建立磁盘块设备描述符bdev与0号分区描述符disk->part_tbl_part[0]的关联,执行磁盘描述符的open回调。由于bd_invalidated在register_disk函数中被置1,即此时的分区信息无效,因此需要重新扫描分区表;rescan_partitions主要是通过解析分区表来获取分区信息,并将各分区信息更新给磁盘的分区表(disk->part_tbl->part):

  • 打开分区对应的块设备:建立分区块设备描述符bdev与分区描述符的关联,执行磁盘描述符的open回调;

2)当非第一次打开块设备:相关对象的关系已经建立,执行磁盘描述符的open回调;

4.3.5  rescan_partitions

rescan_partitions定义在block/partition-generic.c:

int rescan_partitions(struct gendisk *disk, struct block_device *bdev)
{
        struct parsed_partitions *state = NULL;
        struct hd_struct *part;
        int p, highest, res;
rescan:
        if (state && !IS_ERR(state)) {
                free_partitions(state);
                state = NULL;
        }

        res = drop_partitions(disk, bdev);   // 删除磁盘设备的所有分区
        if (res)
                return res;

        if (disk->fops->revalidate_disk)    // 磁盘分区删除时回调
                disk->fops->revalidate_disk(disk);
        check_disk_size_change(disk, bdev, true);   // 根绝get_capacity获取的disk_size来校正bdev->bd_innode->i_size
        bdev->bd_invalidated = 0;                   // 设置标志位 
        if (!get_capacity(disk) || !(state = check_partition(disk, bdev)))
                return 0;
        if (IS_ERR(state)) {
                /*
                 * I/O error reading the partition table.  If any
                 * partition code tried to read beyond EOD, retry
                 * after unlocking native capacity.
                 */
                if (PTR_ERR(state) == -ENOSPC) {
                        printk(KERN_WARNING "%s: partition table beyond EOD, ",
                               disk->disk_name);
                        if (disk_unlock_native_capacity(disk))
                                goto rescan;
                }
                return -EIO;
        }
        /*
         * If any partition code tried to read beyond EOD, try
         * unlocking native capacity even if partition table is
         * successfully read as we could be missing some partitions.
         */
        if (state->access_beyond_eod) {
                printk(KERN_WARNING
                       "%s: partition table partially beyond EOD, ",
                       disk->disk_name);
                if (disk_unlock_native_capacity(disk))
                        goto rescan;
        }
        /* tell userspace that the media / partition table may have changed */
        kobject_uevent(&disk_to_dev(disk)->kobj, KOBJ_CHANGE);

        /* Detect the highest partition number and preallocate
         * disk->part_tbl.  This is an optimization and not strictly
         * necessary.
         */
        for (p = 1, highest = 0; p < state->limit; p++)
                if (state->parts[p].size)
                        highest = p;

        disk_expand_part_tbl(disk, highest);    // 扩展分区表,即初始化disk->part_tbl成员(动态分配struct disk_part_tbl对象)

        /* add partitions */
        for (p = 1; p < state->limit; p++) {  // 创建分区
                sector_t size, from;

                size = state->parts[p].size;  // 分区大小不为0
                if (!size)
                        continue;

                from = state->parts[p].from;     // 分区起始地址
                if (from >= get_capacity(disk)) {       // 起始地址不合法 
                        printk(KERN_WARNING
                               "%s: p%d start %llu is beyond EOD, ",
                               disk->disk_name, p, (unsigned long long) from);
                        if (disk_unlock_native_capacity(disk))
                                goto rescan;
                        continue;
                }

                if (from + size > get_capacity(disk)) {   // 分区结束地址不合法
                        printk(KERN_WARNING
                               "%s: p%d size %llu extends beyond EOD, ",
                               disk->disk_name, p, (unsigned long long) size);

                        if (disk_unlock_native_capacity(disk)) {
                                /* free state and restart */
                                goto rescan;
                        } else {
                                /*
                                 * we can not ignore partitions of broken tables
                                 * created by for example camera firmware, but
                                 * we limit them to the end of the disk to avoid
                                 * creating invalid block devices
                                 */
                                size = get_capacity(disk) - from;
                        }
                }

                /*
                 * On a zoned block device, partitions should be aligned on the
                 * device zone size (i.e. zone boundary crossing not allowed).
                 * Otherwise, resetting the write pointer of the last zone of
                 * one partition may impact the following partition.
                 */
                if (bdev_is_zoned(bdev) &&
                    !part_zone_aligned(disk, bdev, from, size)) {
                        printk(KERN_WARNING
                               "%s: p%d start %llu+%llu is not zone aligned\n",
                               disk->disk_name, p, (unsigned long long) from,
                               (unsigned long long) size);
                        continue;
                }

                part = add_partition(disk, p, from, size,    // p为分区号,form为起始地址、size为分区大小;创建分区hd_struct,并赋值给disk->part_tbl->part[p]
                                     state->parts[p].flags,
                                     &state->parts[p].info);
                if (IS_ERR(part)) {
                        printk(KERN_ERR " %s: p%d could not be added: %ld\n",
                               disk->disk_name, p, -PTR_ERR(part));
                        continue;
                }
#ifdef CONFIG_BLK_DEV_MD
                if (state->parts[p].flags & ADDPART_FLAG_RAID)
                        md_autodetect_dev(part_to_dev(part)->devt);
#endif
        }
        free_partitions(state);
        return 0;
}

check_partition函数会检测分区表格式,如果成功检测则会将分区表信息解析并保存到struct parsed_partition:

  • allocate_partitions:主要是分配struct parsed_partitions结构体,struct parsed_partitions用于保存分区表解析后的信息,它内嵌一个结构体数组,用于保存每一个解析的分区,分区的最大数目为gendisk->minors;
  • check_part:check_part为一个数组,保存着各种分区格式的check函数,
    • 循环执行check_part数组的每一个check函数,确认分区表是否符合当前check函数检查规则(这里我猜想应该就是去读取硬盘设备的分区表信息);
    • 如果符合,则将分区表解析到上一步分配的struct parsed_partitions结构体中,并返回1。从各个check函数可知它们通过读取0扇区的数据来验证分区格式;
  •  如果已经解析分区表,则打印相关信息, 否则打印无法解析分区表;

add_partition函数会根据分区信息创建一个分区hd_struct对象,并进行初始化,同时会调用device_initialize初始化partno号分区的device设备,并执行device_add,将磁盘分区设备的device加入到linux设备模型;

/*
 * Must be called either with bd_mutex held, before a disk can be opened or
 * after all disk users are gone.
 */
struct hd_struct *add_partition(struct gendisk *disk, int partno,
                                sector_t start, sector_t len, int flags,
                                struct partition_meta_info *info)
{
        struct hd_struct *p;
        dev_t devt = MKDEV(0, 0);
        struct device *ddev = disk_to_dev(disk);
        struct device *pdev;
        struct disk_part_tbl *ptbl;
        const char *dname;
        int err;

        err = disk_expand_part_tbl(disk, partno);
        if (err)
                return ERR_PTR(err);
        ptbl = rcu_dereference_protected(disk->part_tbl, 1);

        if (ptbl->part[partno])
                return ERR_PTR(-EBUSY);

        p = kzalloc(sizeof(*p), GFP_KERNEL);
        if (!p)
                return ERR_PTR(-EBUSY);

        if (!init_part_stats(p)) {
                err = -ENOMEM;
                goto out_free;
        }

        seqcount_init(&p->nr_sects_seq);
        pdev = part_to_dev(p);

        p->start_sect = start;
        p->alignment_offset =
                queue_limit_alignment_offset(&disk->queue->limits, start);
        p->discard_alignment =
                queue_limit_discard_alignment(&disk->queue->limits, start);
        p->nr_sects = len;
        p->partno = partno;
        p->policy = get_disk_ro(disk);

        if (info) {
                struct partition_meta_info *pinfo = alloc_part_info(disk);
                if (!pinfo) {
                        err = -ENOMEM;
                        goto out_free_stats;
                }
                memcpy(pinfo, info, sizeof(*info));
                p->info = pinfo;
        }

        dname = dev_name(ddev);
        if (isdigit(dname[strlen(dname) - 1]))
                dev_set_name(pdev, "%sp%d", dname, partno);
        else
                dev_set_name(pdev, "%s%d", dname, partno);

        device_initialize(pdev);
        pdev->class = &block_class;
        pdev->type = &part_type;
        pdev->parent = ddev;

        err = blk_alloc_devt(p, &devt);
        if (err)
                goto out_free_info;
        pdev->devt = devt;

        /* delay uevent until 'holders' subdir is created */
        dev_set_uevent_suppress(pdev, 1);
        err = device_add(pdev);
        if (err)
                goto out_put;

        err = -ENOMEM;
        p->holder_dir = kobject_create_and_add("holders", &pdev->kobj);
        if (!p->holder_dir)
                goto out_del;

        dev_set_uevent_suppress(pdev, 0);
        if (flags & ADDPART_FLAG_WHOLEDISK) {
                err = device_create_file(pdev, &dev_attr_whole_disk);
                if (err)
                        goto out_del;
        }

        err = hd_ref_init(p);
        if (err) {
                if (flags & ADDPART_FLAG_WHOLEDISK)
                        goto out_remove_file;
                goto out_del;
        }

        /* everything is up and running, commence */
        rcu_assign_pointer(ptbl->part[partno], p);

        /* suppress uevent if the disk suppresses it */
        if (!dev_get_uevent_suppress(ddev))
                kobject_uevent(&pdev->kobj, KOBJ_ADD);
        return p;

out_free_info:
        free_part_info(p);
out_free_stats:
        free_part_stats(p);
out_free:
        kfree(p);
        return ERR_PTR(err);
out_remove_file:
        device_remove_file(pdev, &dev_attr_whole_disk);
out_del:
        kobject_put(p->holder_dir);
        device_del(pdev);
out_put:
        put_device(pdev);
        return ERR_PTR(err);
}

4.4 put_disk

put_disk用于注销内核中的gendisk对象,在出口函数中搭配del_gendisk函数一起使用。

参考文章:

[1]linux块设备驱动简述(Linux驱动开发篇)

[2]LINUX驱动之块设备驱动

[3]22.Linux-块设备驱动之框架详细分析(详解)

[4]Linux块设备驱动详解

[5]Linux驱动开发学习笔记-块设备驱动

[6]通用block基础学习笔记 - 2. 添加磁盘到系统

posted @ 2022-09-11 11:48  大奥特曼打小怪兽  阅读(1000)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步