建立块设备驱动环境

    技术2024-07-26  13

    1.5.2 建立块设备驱动环境

    上一节内容反映的是系统上电后,Linux初始化块设备子系统并注册了相应的块设备驱动的内容。而当我们把一块300G的硬盘插入总线的SCSI插槽中时,SCSI设备对应的probe程序就会调用alloc_disk()函数为其分配gendisk结构。

     

    那么SCSI设备对应的probe程序到底是什么呢?设备驱动的研究是有一定套路的,比如说我们这里的scsi吧,必须在drivers/scsi目录下看它的KconfigMakefile文件。根据文件中的内容可得知,SCSI磁盘的驱动就只有一个文件,就是drivers/scsi/sd.c

     

    看到这个文件的最后两行:

    module_init(init_sd);

    module_exit(exit_sd);

     

    所以,scsi磁盘驱动的故事就从init_sd开始:

       1837 static int __init init_sd(void)

       1838 {

       1839         int majors = 0, i, err;

       1840

       1841         SCSI_LOG_HLQUEUE(3, printk("init_sd: sd driver entry point/n"));

       1842

       1843         for (i = 0; i < SD_MAJORS; i++)

       1844                 if (register_blkdev(sd_major(i), "sd") == 0)

       1845                         majors++;

       1846

       1847         if (!majors)

       1848                 return -ENODEV;

       1849

       1850         err = class_register(&sd_disk_class);

       1851         if (err)

       1852                 goto err_out;

       1853

       1854         err = scsi_register_driver(&sd_template.gendrv);

       1855         if (err)

       1856                 goto err_out_class;

       1857

       1858         return 0;

       1859

       1860 err_out_class:

       1861         class_unregister(&sd_disk_class);

       1862 err_out:

       1863         for (i = 0; i < SD_MAJORS; i++)

       1864                 unregister_blkdev(sd_major(i), "sd");

       1865         return err;

       1866 }

     

    函数很简单,包括一串的注册注销函数。

     

    其中register_blkdev我们见过了,在dev文件系统中注册每一个磁盘及其分区。这里出现的宏SD_MAJORS实际上被定义为16

    #define SD_MAJORS    16

    所以经过16次循环之后,我们看到这里叫sd的有16个。这些号码是怎么来的?Linux中所有的主设备号是被各种各样的设备所占据的。其中865-71136-143这么16个号码就被scsi disk所霸占了,sd_major()函数的返回值就是这16个数字。每个主设备号可以带256个次设备号。

     

    class_register()函数将scsi磁盘注册到sysfs体系中,让scsi_disk作为/sys/class/的一个子目录。感兴趣的可以去好好研究一下Linux的设备驱动模型,但是这里对我们分析文件读写的意义不大,所以我们略过。

     

    1854scsi_register_driver则是最重要的函数,作用是注册一个scsi设备驱动。在Linux设备模型中,对于每个设备驱动,有一个与之对应的struct device_driver结构体。而为了体现各类设备驱动自身的特点,各个子系统可以定义自己的结构体,然后把struct device_driver包含进来如C++中基类和扩展类一样。对于scsi子系统,这个基类就是struct scsi_driver,来自include/scsi/scsi_driver.h

     

    struct scsi_driver {

           struct module         *owner;

           struct device_driver       gendrv;

     

           int (*init_command)(struct scsi_cmnd *);

           void (*rescan)(struct device *);

           int (*issue_flush)(struct device *, sector_t *);

           int (*prepare_flush)(struct request_queue *, struct request *);

    };

     

    我们看到device_driver结构是嵌入在这个scsi_driver中的,来自include/linux/device.h

     

    struct device_driver {

           const char              * name;

           struct bus_type              * bus;

     

           struct completion    unloaded;

           struct kobject         kobj;

           struct klist             klist_devices;

           struct klist_node     knode_bus;

     

           struct module         * owner;

     

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

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

           void (*shutdown)   (struct device * dev);

           int   (*suspend)      (struct device * dev, pm_message_t state);

           int   (*resume)       (struct device * dev);

    };

     

    而咱们也自然定义了一个scsi_driver的结构体实例,它的名字叫做sd_template

     

    static struct scsi_driver sd_template = {

           .owner                  = THIS_MODULE,

           .gendrv = {

                  .name             = "sd",

                  .probe            = sd_probe,

                  .remove          = sd_remove,

                  .shutdown       = sd_shutdown,

           },

           .rescan                  = sd_rescan,

           .init_command              = sd_init_command,

           .issue_flush            = sd_issue_flush,

    };

     

    这其中,gendrv就是struct device_driver的结构体变量。咱们这么一注册,其直观效果就是:

    localhost:~ # ls /sys/bus/scsi/drivers/

    sd

     

    其间接效果就是,在系统的总线上,scsi接口发现了又scsi磁盘,就调用sd_probe加载scsi的驱动。而与以上三个函数相反的就是exit_sd()中的另外三个函数:scsi_unregister_driverclass_unregisterunregister_blkdev

     

    Ok了,这个初始化就这么简单地结束了。下一步我们就从sd_probe函数看起,某种意义来说,读sd_mod的代码就算是对scsi子系统的入门。

     

    static int sd_probe(struct device *dev)

    {

           struct scsi_device *sdp = to_scsi_device(dev);

           struct scsi_disk *sdkp;

           struct gendisk *gd;

           u32 index;

           int error;

     

    ……

     

           gd = alloc_disk(16);

           if (!gd)

                  goto out_free;

     

           if (!idr_pre_get(&sd_index_idr, GFP_KERNEL))

                  goto out_put;

    ……

           gd->major = sd_major((index & 0xf0) >> 4);

           gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00);

           gd->minors = 16;

           gd->fops = &sd_fops;

     

           if (index < 26) {

                  sprintf(gd->disk_name, "sd%c", 'a' + index % 26);

           } else if (index < (26 + 1) * 26) {

                  sprintf(gd->disk_name, "sd%c%c",

                         'a' + index / 26 - 1,'a' + index % 26);

           } else {

                  const unsigned int m1 = (index / 26 - 1) / 26 - 1;

                  const unsigned int m2 = (index / 26 - 1) % 26;

                  const unsigned int m3 =  index % 26;

                  sprintf(gd->disk_name, "sd%c%c%c",

                         'a' + m1, 'a' + m2, 'a' + m3);

           }

     

           gd->private_data = &sdkp->driver;

           gd->queue = sdkp->device->request_queue;

     

           sd_revalidate_disk(gd);

     

           gd->driverfs_dev = &sdp->sdev_gendev;

           gd->flags = GENHD_FL_DRIVERFS;

           if (sdp->removable)

                  gd->flags |= GENHD_FL_REMOVABLE;

     

           dev_set_drvdata(dev, sdkp);

           add_disk(gd);

     

           sdev_printk(KERN_NOTICE, sdp, "Attached scsi %sdisk %s/n",

                      sdp->removable ? "removable " : "", gd->disk_name);

     

           return 0;

     

     out_put:

           put_disk(gd);

     out_free:

           kfree(sdkp);

     out:

           return error;

    }

     

    我们来看一下alloc_disk()函数,在源代码sd.c中。咱们这里传递进来的参数是16表示次设备号范围为1~16

     

    struct gendisk *alloc_disk(int minors)

    {

             return alloc_disk_node(minors, -1);

    }

    struct gendisk *alloc_disk_node(int minors, int node_id)

    {

             struct gendisk *disk;

             disk = kmalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id);

             if (disk) {

                     memset(disk, 0, sizeof(struct gendisk));

                     if (!init_disk_stats(disk)) {

                             kfree(disk);

                             return NULL;

                     }

                     if (minors > 1) {

                             int size = (minors - 1) * sizeof(struct hd_struct *);

                             disk->part = kmalloc_node(size, GFP_KERNEL, node_id);

                             if (!disk->part) {

                                     kfree(disk);

                                     return NULL;

                             }

                             memset(disk->part, 0, size);

                     }

                     disk->minors = minors;

                     kobj_set_kset_s(disk,block_subsys);

                     kobject_init(&disk->kobj);

                     rand_initialize_disk(disk);

                     INIT_WORK(&disk->async_notify,

                             media_change_notify_thread);

             }

             return disk;

    }

     

    首先,我们做的事情就是申请了一个struct gendisk数据结构。毫无疑问,这个结构体是我们I/O调度层中最重要的结构体之一,就是在这里被分配的。

     

    因为minors我们给的是16,于是size等于15sizeof(struct hd_struct *),而part我们看到是struct hd_struct的二级指针,这里我们看到kmalloc_node(),这个函数中的node/node_id这些概念指的是NUMA技术中的节点,对于咱们这些根本就不会接触NUMA的人来说kmalloc_node()就等于kmalloc(),因此这里做的就是申请内存并且初始化为0。要说明的一点是,part就是partition的意思,日后它将扮演我们常说的分区的角色。

     

    然后,disk->minors设置为了16。随后,kobj_set_kset_s()block_subsys是我们前面注册的子系统,从数据结构来说,它的定义如下,来自block/genhd.c

    struct kset block_subsys;

     

    其实也就是一个struct kset。而这里的kobj_set_kset_s的作用就是让disk对应kobjectkset等于block_subsys。也就是说让kobject找到它的kset(如果你还记得当初我们在我是Sysfs中分析的kobjectkset的那套理论的话,你不会不明白这里的意图。)kobject_init()初始化一个kobject,这个函数通常就是出现在设置了kobjectkset之后。

     

    正是因为有了这么一个定义,正是因为这里我们把“block”给了block_subsyskobjname成员,所以当我们在block子系统初始化的时候调用subsystem_register(&block_subsys)之后,我们才会在/sys/目录下面看到“block”子目录:

    [root@localhost~]# ls /sys/

    block  bus  class  devices  firmware  fs  kernel  module  power

     

    看到rand_initialize_disk(disk)函数,初始化一个工作队列,这是一个很重要的数据结构,到时候用到了再来看。至此,alloc_disk_node就将返回,从而alloc_disk也就返回了。

     

    sd_probe程序调用alloc_disk()函数为我们的SCSI硬盘分配了gendisk结构,紧接着会调用add_disk()来安装这个块设备:

     

    void add_disk(struct gendisk *disk){

             disk->flags |= GENHD_FL_UP;

             blk_register_region(MKDEV(disk->major, disk->first_minor),

                                 disk->minors, NULL, exact_match, exact_lock, disk);

             register_disk(disk);

             blk_register_queue(disk);

    }

     

    这个函数虽然只有四行代码,可是十分复杂,旗下三个函数,一个比一个难,我们接下来详细分析。

     

    头一个,blk_register_region,来自block/genhd.c

     

    void blk_register_region(dev_t dev, unsigned long range, struct module *module,

                              struct kobject *(*probe)(dev_t, int *, void *),

                              int (*lock)(dev_t, void *), void *data){

             kobj_map(bdev_map, dev, range, module, probe, lock, data);

    }

     

    虽然在这里我们完全看不出这么做的意义,或者说blk_register_region这个函数究竟有什么价值现在完全体现不出来。但是其实这是 Linux中实现的一种管理设备号的机制,这里利用了传说中的哈希表(散列表)来管理设备号,哈希表的优点大家知道,便于查找。而我们的目的是为了通过给定的一个设备号就能迅速得到它所对应的kobject指针,而对于块设备来说,得到kobject是为了得到其对应的gendisk

     

    事实上,内核提供kobj_map_init()kobj_map()以及kobj_lookup()都是一个系列的,它们都是为Linux设备号管理服务的。首先,kobj_map_init提供的是一次性服务,它的使命是建立了bdev_map这个struct kobj_map。然后kobj_map()是每次在blk_register_region中被调用的,然而,调用blk_register_region()的地方可真不少,而我们这个在add_disk中调用只是其中之一,其它的比如RAID驱动那边,软驱驱动那边,都会有调用这个blk_register_region的需求,而kobj_lookup()发生在什么情况下呢?它提供的其实是售后服务。当块设备驱动完成了初始化工作,当它在内核中站稳了脚跟,会有一个设备文件和它相对应,这个文件会出现在/dev目录下。在不久的将来,当open系统调用试图打开块设备文件的时候就会调用它,更准确地说,sys_open经由filp_open然后是dentry_open(),最终会找到blkdev_openblkdev_open会调用do_opendo_open()会调用get_gendisk(),要想明白这个理儿,得先看一下dev_t这个结构。dev_t实际上就是u32,也即就是32bits。前面咱们看到的MKDEVMAJOR,都来自 include/linux/kdev_t.h

     

    #define MINORBITS       20

    #define MINORMASK       ((1U << MINORBITS) - 1)

     

    #define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))

    #define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))

    #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

     

    通过这几个宏,我们不难看出dev_t的意义了,32bits,其中高12位被用来记录设备的主设备号,低20位用来记录设备的次设备号。而MKDEV就是建立一个设备号。ma代表主设备号,mi代表次设备号,ma左移20位再和mi相或,反过来,MAJOR就是从dev中取主设备号,MINOR就是从dev中取次设备号。

     

    当一个设备闯入Linux的内核时,首先它会有一个居住证号,这就是dev_t,很显然,每个人的居住证号不一样,它是唯一的。(为什么不说是身份证号?因为居住证意味着当设备离开Linux系统的时候就可以销毁,所以它更能体现设备的流动性。)建立一个设备文件的时候,其设备号是确定的,而我们每次建立一个文件都会建立一个结构体变量,它就是struct inode,而struct inode拥有成员dev_t i_dev,所以日后我们从struct inode就可以得到其设备号dev_t,而这里kobj_map这一系列函数使得我们可以从dev_t找到对应的kobject,然后进一步找到磁盘驱动,我们不可避免的需要访问磁盘对应的gendisk结构体指针,而get_gendisk()就是在这时候粉墨登场的。咱们看到 get_gendisk()的两个参数,dev_t devint *part,前者就是设备号,而后者传递的是一个指针,这表示什么呢?这表示:

    1. 如果这个设备号对应的是一个分区,那么part变量就用来保存分区的编号。

    2. 如果这个设备号对应的是整个设备而不是某个分区,那么part就只要设置成0就行了。

     

    那么得到gendisk的目的又是什么呢?我们注意到struct gendisk有一个成员,struct block_device_operations *fops,而这个指针才是用来真正执行操作的,每一个块设备驱动都准备了这么一个结构体,比如咱们在sd中定义的那个:

     

    static struct block_device_operations sd_fops = {

             .owner                  = THIS_MODULE,

             .open                   = sd_open,

             .release                = sd_release,

             .ioctl                  = sd_ioctl,

             .getgeo                 = sd_getgeo,

    #ifdef CONFIG_COMPAT

             .compat_ioctl           = sd_compat_ioctl,

    #endif

             .media_changed          = sd_media_changed,

             .revalidate_disk        = sd_revalidate_disk,

    };

     

    正是因为有这种关系,我们才能一步一步从sys_open最终走到sd_open,也才能从用户层一步一步走到块设备驱动层。

     

    最新回复(0)