由于我之前是在存储设备emmc上做的测试,实验/dev/mmcblock3设备节点的生成过程,这里实际讲解的是/dev/mmcblock3的生成过程。
1 mmcblock3块设备的注册
/dev/mmcblock3代表的是的块设备,在块设备驱动初始化时开始注册块设备,入口函数在drivers/mmc/card/block.c文件的mmc_blk_probe()函数,该函数主要分配与块设备紧密相关的struct gendisk结构,并添加到系统。
- static int mmc_blk_probe(struct mmc_card *card)
- {
- //分配struct gendisk 并初始化,初始化主次设备号、块设备操作函数mmc_bdops
- md = mmc_blk_alloc(card);}
- //添加struct gendisk *disk
- if (mmc_add_disk(md))
- goto out;
- }
- struct gendisk {
- //块设备主设备号
- int major;
- //第一个次设备号
- int first_minor;
- //该块设备的分区数
- int minors;
- //代表主块设备名字,mmcblk0
- char disk_name[DISK_NAME_LEN]; /* name of major driver */
- //disk->part_tbl_part[0]是在disk分配后赋值,在alloc_disk_node赋值,指向分区,里边的part[0]等就指向分区的struct hd_struct结构
- struct disk_part_tbl __rcu *part_tbl;
- //代表设备主分区
- struct hd_struct part0;
- //fops在alloc_disk后,在mmc_blk_alloc_req中赋值,是mmc_bdops
- const struct block_device_operations *fops;
- // mmc_blk_alloc_req中赋值
- struct request_queue *queue;
- };
一个块设备,比如mmcblk0或者sda,这代表整个块设备,一般会把这个块设备分成多个分区,比如mmcblk0p1、mmcblk0p2、mmcblk0p3或者sda1、sda2、sda3。不管有几个分区,只有一个struct gendisk disk结构,但是每个分区都有自己的block_device,即bdev,主分区也有自己的bdev。继续mmc_add_disk->add_disk:
- void add_disk(struct gendisk *disk)
- {
- //根据disk的major和first_minor,由MKDEV处理后返回devt,即主次设备号
- retval = blk_alloc_devt(&disk->part0, &devt);
- disk_to_dev(disk)->devt = devt;
- //根据主次设备号把块设备添加到系统,有点类似字符设备的cdev_add
- blk_register_region(disk_devt(disk), disk->minors, NULL, exact_match, exact_lock, disk);
- //创建mmcblk0块设备主分区节点,创建主分区的bdev
- register_disk(disk);
- }
- //创建mmcblk0块设备主分区节点,创建主分区的bdev
- static void register_disk(struct gendisk *disk)
- {
- struct device *ddev = disk_to_dev(disk);
- //主分区名字mmcblk0
- dev_set_name(ddev, "%s", disk->disk_name);
- //添加disk->part0.dev主分区的device,之后会生成/dev下块设备节点/dev/mmcblk0
- if (device_add(ddev))
- return;
- //根据分区号partno从disk->part_tbl->part[partno]得到分区结构体struct hd_struct part,根据part_devt(part)块设备主次设备号,找到或者分配对应的struct block_device *bdev、inode、bdev_inode结构
- bdev = bdget_disk(disk, 0);
- //设置1将使能扫描块设备下所有分区
- bdev->bd_invalidated = 1;
- //调用最核心的__blkdev_get函数,open块设备bdev,对bdev大部分成员赋值,扫描块设备分区,得到bdev对应的disk,对bdev的成员bd_disk、bd_queue、bd_contains、bd_part、bd_block_size等赋值,执行disk的block open函数,执行rescan_partitions扫描块设备分区
- err = blkdev_get(bdev, FMODE_READ, NULL);
- }
device_add(ddev)执行后,会通知上层有新设备注册,应用层udev会获取这些信息(设备名、设备类型、主次设备号等),在/dev目录创建mmcblk0设备节点文件,这与/dev目录下手动执行命令mknod mmcblk0 b 179 0效果一样。bdev = bdget_disk(disk, 0)函数执行后,生成主分区mmcblk0的块设备struct block_device 。blkdev_get(bdev, FMODE_READ, NULL)执行后,会扫描该主分区下的其他分区mmcblk0p1、mmcblk0p2、mmcblk0p3等,然后同样执行device_add()函数通知上层在/dev/目录生成该分区的设备节点。继续看blkdev_get-> __blkdev_get-> rescan_partitions
- int rescan_partitions(struct gendisk *disk, struct block_device *bdev)
- {
- int p
- struct parsed_partitions *state = NULL;
- / / check_partition()获取块设备的各个分区信息:分区起始地址、分区大小等等
- if (!get_capacity(disk) || !(state = check_partition(disk, bdev)))
- return 0;
- //依次循环添加注册mmcblk0p1、mmcblk0p2、mmcblk0p3等等这些分区,之后会生成/dev/mmcblk0p1、/dev/mmcblk0p2、/dev/mmcblk0p3
- for (p = 1; p < state->limit; p++) {
- sector_t size, from;
- size = state->parts[p].size;
- from = state->parts[p].from;
- part = add_partition(disk, p, from, size,state->parts[p].flags,&state->parts[p].info);
- }
- }
- 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;
- struct device *pdev;
- struct device *ddev = disk_to_dev(disk);
- p = kzalloc(sizeof(*p), GFP_KERNEL);
- init_part_stats(p)
- 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;//分区号,比如mmcblk0p3的3,以下注释都以mmcblk0p3为例
- p->policy = get_disk_ro(disk);//分区属性,是否只读
- //设置分区名字
- dname = dev_name(ddev);//主分区名字,如mmcblk0
- if (isdigit(dname[strlen(dname) - 1]))
- dev_set_name(pdev, "%sp%d", dname, partno);//组合成设备名”mmcblk0p3”
- else
- dev_set_name(pdev, "%s%d", dname, partno);
- device_initialize(pdev);
- pdev->class = &block_class;
- pdev->type = &part_type;
- pdev->parent = ddev;
- //为mmcblk0p3块设备分配主次设备号
- err = blk_alloc_devt(p, &devt);
- //注册块设备mmcblk0p3,通知上层生成/dev/mmcblk0p3设备文件
- err = device_add(pdev);
- }
以上主要完成块设备的注册,通知上层mmcblk0p1、mmcblk0p2、mmcblk0p3这些块设备注册了,真正生成还需要udev机制。udev获取注册的这些块设备信息:设备名字、设备属性、主次设备号,然后执行mknod系统调用函数真正在/dev/目录生成设备文件节点。实际是在tmpfs文件系统下生成的这些设备文件节点,因为tmpfs文件系统挂载到了/dev目录。devtmpfs文件系统与tmpfs文件系统应该类似。
2 在/dev/目录生成设备节点的过程
当块设备驱动执行device_add注册mmcblk0p3块设备,应用层udev获取该该块设备的设备名、设备属性、主次设备号,然后执行mknod()函数,在/dev目录创建mmcblk0p3设备节点文件/dev/ mmcblk0p3,其实本质就是一个文件,下面讲解这个创建过程。系统调用mknod对应的内核函数是:
- SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
- {
- return sys_mknodat(AT_FDCWD, filename, mode, dev);
- }
sys_mknodat()函数对应的函数是:
- SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode,
- unsigned, dev)
- {
- //filename是上层传入的文件名,mode包含设备属性,dev包含主次设备号
- struct dentry *dentry;
- struct path path;
- //根据/dev/mmcblk0p3搜索到父目录的dentry存于path.dentry,然后创建mmcblk0p3的dentry并返回这个dentry。由于/dev是挂载点,tmpfs文件系统挂载与/dev目录,所以这是在tmpfs文件系统上创建mmcblk0p3。
- dentry = user_path_create(dfd, filename, &path, lookup_flags);
- switch (mode & S_IFMT) {
- case 0: case S_IFREG:
- //创建一般文件,tmpfs文件系统可以创建普通文件
- error = vfs_create(path.dentry->d_inode,dentry,mode,true);
- break;
- //字符设备、块设备特殊文件的创建
- case S_IFCHR: case S_IFBLK:
- //path.dentry->d_inode 是父目录/dev 的inode,dentry是刚创建的mmcblk0p3文件的。由于/dev是挂载点,属于tmpfs文件系统,所以这是tmpfs文件系统上创建mmcblk0p3
- error = vfs_mknod(path.dentry->d_inode,dentry,mode, new_decode_dev(dev));
- break;
- case S_IFIFO: case S_IFSOCK:
- error = vfs_mknod(path.dentry->d_inode,dentry,mode,0);
- break;
- }
- }
user_path_create()函数执行流程是user_path_create-> kern_path_create-> do_path_lookup-> filename_lookup-> path_lookupat,path_lookupat()函数前文介绍过,就是open一个文件的核心函数。这里的操作是,open “/dev/mmcblk0p3”,获取/dev目录的dentry,然后创建mmcblk0p3文件dentry。需要注意的是,本次创建的是块设备这种特殊文件,并不是普通的文件,除了有文件名字mmcblk0p3即设备名外,还有属性(S_IFBLK)、主次设备号。vfs_mknod()函数中执行dir->i_op->mknod(),证实是shmem_mknod()。
- static int shmem_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
- {
- //创建/dev/mmcblk0p3文件inode,dir是父目录的inode,dentry是/dev/mmcblk0p3中mmcblk0p3文件的,dev是主次设备号
- inode = shmem_get_inode(dir->i_sb, dir, mode, dev, VM_NORESERVE);
- //建立inode和dentry的联系
- d_instantiate(dentry, inode);
- }
- static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,
- umode_t mode, dev_t dev, unsigned long flags)
- {
- struct inode *inode;
- struct shmem_inode_info *info;
- //tmpfs文件系统的超级块
- struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
- //创建/dev/mmcblk0p3文件的inode
- inode = new_inode(sb);
- if (inode) {
- //获取inode编号
- inode->i_ino = get_next_ino();
- switch (mode & S_IFMT) {
- default:
- //特殊文件,设备节点、管道、sock设备走这个分支
- //比如此时mmcblk0p3设备节点文件的创建
- inode->i_op = &shmem_special_inode_operations;
- init_special_inode(inode, mode, dev);
- break;
- case S_IFREG://tmpfs文件系统 普通文件
- inode->i_mapping->a_ops = &shmem_aops;
- inode->i_op = &shmem_inode_operations;
- inode->i_fop = &shmem_file_operations;
- mpol_shared_policy_init(&info->policy,
- shmem_get_sbmpol(sbinfo));
- break;
- case S_IFDIR://tmpfs文件系统 目录
- inc_nlink(inode);
- /* Some things misbehave if size == 0 on a directory */
- inode->i_size = 2 * BOGO_DIRENT_SIZE;
- inode->i_op = &shmem_dir_inode_operations;
- inode->i_fop = &simple_dir_operations;
- break;
- }
- }
- }
shmem_get_inode()函数主要是创建/dev/mmcblk0p3文件的inode,然后对inode进行初始化。文件的inode才真正与磁盘对应起来,包含文件的基本数据,比如修改时间、物理记录块号等元数据。不过本次情况有点特殊,是tmpfs文件系统挂载到/dev目录,是在tmpfs文件系统创建文件,tmpfs文件系统是内存型文件系统,并没有磁盘对应,所以创建inode的过程非常简单,没有对物理磁盘有什么读写操作。接着看init_special_inode()函数。
- void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
- {
- inode->i_mode = mode;
- if (S_ISCHR(mode)) {//字符设备
- inode->i_fop = &def_chr_fops;//字符设备文件操作操作结构体
- inode->i_rdev = rdev; //字符设备的主次设备号
- } else if (S_ISBLK(mode)) {//块设备
- inode->i_fop = &def_blk_fops; //块设备设备文件操作操作结构体
- inode->i_rdev = rdev; //块设备的主次设备号
- } else if (S_ISFIFO(mode))
- inode->i_fop = &pipefifo_fops;
- else if (S_ISSOCK(mode))
- inode->i_fop = &bad_sock_fops;
- ……..
- }
init_special_inode()函数中,完成对inode->i_fop = &def_blk_fops块设备文件操作函数的赋值,inode->i_rdev = rdev 是对inode主次设备号的赋值,将来在open “/dev/mmcblkop3”时用到。最后再啰嗦一点,等/dev/mmcblk0p3设备文件节点创建后,可以直接读写该块设备,比如cat /dev/mmcblk0p3。第一步肯定是open /dev/mmcblk0p3,这时走的流程与open一个普通文件是相同的,内核执行流程是sys_open-> do_sys_open-> do_filp_open-> path_openat,这里重点介绍的是open的最后一步,path_openat-> do_last-> finish_open-> do_dentry_open
- // struct file *f是/dev/mmcblk0p3 文件的struct file结构
- static int do_dentry_open(struct file *f,
- int (*open)(struct inode *, struct file *),
- const struct cred *cred)
- {
- struct inode *inode;
- // 这是/dev/mmcblk0p3 文件的inode结构
- inode = f->f_inode = f->f_path.dentry->d_inode;
- f->f_mapping = inode->i_mapping;
- //返回inode->i_fop,就是前文init_special_inode ()中赋值的def_blk_fops 结构
- f->f_op = fops_get(inode->i_fop);
- open = f->f_op->open;
- //执行blkdev_open()函数
- error = open(inode, f);
- return 0;
- }
- static int blkdev_open(struct inode * inode, struct file * filp)
- {
- //这个inode是/dev/mmcblk0p3的,inode->i_rdev是mmcblk0p3块设备的主次设备号,根据这个主次设备号,找到或者创建基于mmcblk0p3块设备的bdev
- bdev = bd_acquire(inode);
- //块设备struct address_space 页高速缓存,读写块设备用到
- filp->f_mapping = bdev->bd_inode->i_mapping;
- //完成底层mmcblk0p3块设备的open
- return blkdev_get(bdev, filp->f_mode, filp);
- }
bd_acquire()在前文介绍mount内核过程详细介绍过,它在这里的作用是根据/dev/mmcblk0p3文件的主次设备号inode->i_rdev,找到或者创建该块设备的struct block_device结构,之后才可以操作这个块设备硬件。