一、共享内存虚拟文件系统
1、初始化 shmfs
(1)init_tmpfs
这个函数用于注册和挂载 tmpfs 及 shmemfs 文件系统。
// mm/shmem.c // shm文件系统只有在编译时定义CONFIG_TMPFS的情况下可以挂载。即使没有 // 指定,也会因 fork() 而为匿名共享内存建立tmpfs。 #ifdef CONFIG_TMPFS /* type "shm" will be tagged obsolete in 2.5 */ // 声明在文件 <linux/fs.h> 中的 DECLARE_FSTYPE(), 声明了 tmpfs_fs_type 是 // struct file_system_type, 并填充了 4 个字段。"tmpfs" 是它的可读名字。shmem_read_super() // 函数用于为文件系统读取超级块(超级块的细节以及它们如何匹配文件系统已经超出本书范 // 畴)。FS_LITTER 是一个标志位,用于表明应该在 dcache 中维护文件系统树。最后,这个宏 // 设置文件系统的模块所有者成为载入文件系统的模块。 static DECLARE_FSTYPE(shmem_fs_type, "shm", shmem_read_super, FS_LITTER); static DECLARE_FSTYPE(tmpfs_fs_type, "tmpfs", shmem_read_super, FS_LITTER); #else static DECLARE_FSTYPE(tmpfs_fs_type, "tmpfs", shmem_read_super, FS_LITTER|FS_NOMOUNT); #endif static struct vfsmount *shm_mnt; // __init 放置该函数在初始化部分。这意味着,内核启动完毕后,这个函数的代码将被移除。 static int __init init_tmpfs(void) { int error; // 注册文件系统为tmpfs_fs_type类型,这已在第1433行进行了声明。如果失 // 败,跳转到 out3, 返回适当的错误。 error = register_filesystem(&tmpfs_fs_type); if (error) { printk(KERN_ERR "Could not register tmpfs\n"); goto out3; } // 如果在配置时指定了 tmpfs ,那么注册shmem文件系统。如果失败,跳转到 // out2, 解除tmpfs_fs_type的注册并返回错误。 #ifdef CONFIG_TMPFS error = register_filesystem(&shmem_fs_type); if (error) { printk(KERN_ERR "Could not register shm fs\n"); goto out2; } // 如果由设备文件系统(devfs)管理/dev/,则创建一个新的shm目录。如果内核没有 // 使用devfs,则系统管理员必须手工创建该目录。 devfs_mk_dir(NULL, "shm", NULL); #endif // kern_mount()在内部挂载一个文件系统。换言之,该文件系统被挂载并被激活,但 // 在VFS中不被任何用户可见。其挂载点是shm_mnt,位于shmem.c文件中,其类型是 // struct vfsmount。 在后期需要搜索文件系统并卸载这个变量。 shm_mnt = kern_mount(&tmpfs_fs_type); // 确保正确地卸载文件系统, 但是如果没有,则跳转到out1, 解除文件系统的注 // 册, 并返回错误。 if (IS_ERR(shm_mnt)) { error = PTR_ERR(shm_mnt); printk(KERN_ERR "Could not kern_mount tmpfs\n"); goto out1; } /* The internal instance should not do size checking */ // 函数shmem_set_size() (见L. 1. 3小节)用于设置文件系统中创建的块数及索引节 // 点数的最大值。 shmem_set_size(SHMEM_SB(shm_mnt->mnt_sb), ULONG_MAX, ULONG_MAX); return 0; out1: #ifdef CONFIG_TMPFS unregister_filesystem(&shmem_fs_type); out2: #endif unregister_filesystem(&tmpfs_fs_type); out3: shm_mnt = ERR_PTR(error); return error; } // 在这种情况下,module_init()表明了在载入模块上应当调用init_shmem_fs()以及 // 如何直接编译进内核,在系统启动时调用这个函数。 module_init(init_tmpfs)
① ⇐ DECLARE_FSTYPE
// include/linux/fs.h struct file_system_type { const char *name; int fs_flags; struct super_block *(*read_super) (struct super_block *, void *, int); struct module *owner; struct file_system_type * next; struct list_head fs_supers; }; #define DECLARE_FSTYPE(var,type,read,flags) \ struct file_system_type var = { \ name: type, \ read_super: read, \ fs_flags: flags, \ owner: THIS_MODULE, \ }
② ⟺ register_filesystem
// fs/super.c /** * register_filesystem - register a new filesystem * @fs: the file system structure * * Adds the file system passed to the list of file systems the kernel * is aware of for mount and other syscalls. Returns 0 on success, * or a negative errno code on an error. * * The &struct file_system_type that is passed is linked into the kernel * structures and must not be freed until the file system has been * unregistered. */ int register_filesystem(struct file_system_type * fs) { int res = 0; struct file_system_type ** p; if (!fs) return -EINVAL; if (fs->next) return -EBUSY; INIT_LIST_HEAD(&fs->fs_supers); write_lock(&file_systems_lock); p = find_filesystem(fs->name); if (*p) res = -EBUSY; else *p = fs; write_unlock(&file_systems_lock); return res; }
③ ⟺ kern_mount
// fs/super.c struct vfsmount *kern_mount(struct file_system_type *type) { return do_kern_mount(type->name, 0, (char *)type->name, NULL); }
④ ⇒ shmem_set_size
(2)shmem_read_super
这是文件系统提供的回调函数,用于读取超级块。对于普通的文件系统,这可以从磁盘读取细节信息,但是,由于这个文件系统基于 RAM ,相反它产生一个 struct super_block 。
// mm/shmem.c // 参数如下所示: // sb 是产生的超级块。 // data 包括了一些参数。 // silent 在这个函数中未使用。 static struct super_block *shmem_read_super(struct super_block *sb, void *data, int silent) { struct inode *inode; struct dentry *root; unsigned long blocks, inodes; // 设置缺省模式,uid和gid。这些可能覆盖挂载选项中的参数。 int mode = S_IRWXUGO | S_ISVTX; uid_t uid = current->fsuid; gid_t gid = current->fsgid; // 每个super_block 都允许具有一个特定文件系统的结构,该结构包括一个称为 // super_block->u 的联合结构。 宏SHMEM_SB()返回联合结构中所包含的 // struct shmem_sb_info。 struct shmem_sb_info *sbinfo = SHMEM_SB(sb); struct sysinfo si; /* * Per default we only allow half of the physical ram per * tmpfs instance */ // si_meminfo()产生struct sysinfo,包括了全部内存,可用内存和已用内存的统计数 // 据。这个函数在arch/i386/mm/init.c文件中进行了定义,它是架构相关的。 si_meminfo(&si); // 缺省情况下,只允许文件系统消耗物理内存的一半。 blocks = inodes = si.totalram / 2; // 如果tmpfs可用,这将解析挂载选项,并允许覆盖缺省值。 #ifdef CONFIG_TMPFS if (shmem_parse_options(data, &mode, &uid, &gid, &blocks, &inodes)) return NULL; #endif // 获取锁保护的 sbinfo, 它是 super_block 中的 struct shmem_sb_info。 spin_lock_init(&sbinfo->stat_lock); sbinfo->max_blocks = blocks; sbinfo->free_blocks = blocks; sbinfo->max_inodes = inodes; sbinfo->free_inodes = inodes; sb->s_maxbytes = SHMEM_MAX_BYTES; sb->s_blocksize = PAGE_CACHE_SIZE; sb->s_blocksize_bits = PAGE_CACHE_SHIFT; // 产生sb和sbinfo字段。 sb->s_magic = TMPFS_MAGIC; // shmem_ops是超级块结构的函数指针,用于重新挂载文件系统和删除索引节点。 sb->s_op = &shmem_ops; // 这个块分配特定的索引节点,用于表示文件系统的根节点。 inode = shmem_get_inode(sb, S_IFDIR | mode, 0); if (!inode) return NULL; // 设置新文件系统的根部 uid和 gid。 inode->i_uid = uid; inode->i_gid = gid; // 实现在 fs/dcache.c 中 root = d_alloc_root(inode); if (!root) { iput(inode); return NULL; } // 设置根索引节点至 super_block 中。 sb->s_root = root; // 返回产生的超级块。 return sb; }
① ⇐ SHMEM_SB
// mm/shmem.c #define SHMEM_SB(sb) (&sb->u.shmem_sb) // include/linux/fs.h struct super_block { struct list_head s_list; /* Keep this first */ kdev_t s_dev; unsigned long s_blocksize; unsigned char s_blocksize_bits; unsigned char s_dirt; unsigned long long s_maxbytes; /* Max file size */ struct file_system_type *s_type; struct super_operations *s_op; struct dquot_operations *dq_op; struct quotactl_ops *s_qcop; unsigned long s_flags; unsigned long s_magic; struct dentry *s_root; struct rw_semaphore s_umount; struct semaphore s_lock; int s_count; atomic_t s_active; struct list_head s_dirty; /* dirty inodes */ struct list_head s_locked_inodes;/* inodes being synced */ struct list_head s_files; struct block_device *s_bdev; struct list_head s_instances; struct quota_info s_dquot; /* Diskquota specific options */ union { struct minix_sb_info minix_sb; struct ext2_sb_info ext2_sb; struct ext3_sb_info ext3_sb; struct hpfs_sb_info hpfs_sb; struct ntfs_sb_info ntfs_sb; struct msdos_sb_info msdos_sb; struct isofs_sb_info isofs_sb; struct nfs_sb_info nfs_sb; struct sysv_sb_info sysv_sb; struct affs_sb_info affs_sb; struct ufs_sb_info ufs_sb; struct efs_sb_info efs_sb; struct shmem_sb_info shmem_sb; struct romfs_sb_info romfs_sb; struct smb_sb_info smbfs_sb; struct hfs_sb_info hfs_sb; struct adfs_sb_info adfs_sb; struct qnx4_sb_info qnx4_sb; struct reiserfs_sb_info reiserfs_sb; struct bfs_sb_info bfs_sb; struct udf_sb_info udf_sb; struct ncp_sb_info ncpfs_sb; struct usbdev_sb_info usbdevfs_sb; struct jffs2_sb_info jffs2_sb; struct cramfs_sb_info cramfs_sb; void *generic_sbp; } u; /* * The next field is for VFS *only*. No filesystems have any business * even looking at it. You had been warned. */ struct semaphore s_vfs_rename_sem; /* Kludge */ /* The next field is used by knfsd when converting a (inode number based) * file handle into a dentry. As it builds a path in the dcache tree from * the bottom up, there may for a time be a subpath of dentrys which is not * connected to the main tree. This semaphore ensure that there is only ever * one such free path per filesystem. Note that unconnected files (or other * non-directories) are allowed, but not unconnected diretories. */ struct semaphore s_nfsd_free_path_sem; };
② ⇒ shmem_get_inode
③ ⇔ d_alloc_root
// fs/dcache.c /** * d_alloc_root - allocate root dentry * @root_inode: inode to allocate the root for * * Allocate a root ("/") dentry for the inode given. The inode is * instantiated and returned. %NULL is returned if there is insufficient * memory or the inode passed is %NULL. */ struct dentry * d_alloc_root(struct inode * root_inode) { struct dentry *res = NULL; if (root_inode) { res = d_alloc(NULL, &(const struct qstr) { "/", 1, 0 }); if (res) { res->d_sb = root_inode->i_sb; res->d_parent = res; d_instantiate(res, root_inode); } } return res; }
(3)shmem_set_size
这个函数更新文件系统中可用块和索引节点的数量。在文件系统挂载和卸载时进行设置。
// mm/shmem.c // 这些参数描述了文件系统超级块的信息,块的最大数量(max_blocks)和索引节点的 // 最大数量(max_inodes)。 static int shmem_set_size(struct shmem_sb_info *info, unsigned long max_blocks, unsigned long max_inodes) { int error; unsigned long blocks, inodes; // 锁定超级块信息自旋锁。 spin_lock(&info->stat_lock); // 计算文件系统中当前使用的块数。在初始挂载时,这并不重要,然而,如果重新挂载 // 文件系统,该函数必须保证新的文件系统不会太小。 blocks = info->max_blocks - info->free_blocks; // 计算当前使用的索引节点数。 inodes = info->max_inodes - info->free_inodes; error = -EINVAL; // 如果重新挂载的文件系统没有足够的块存放当前信息,则跳转到out,并返 // 回-EINVAL。 if (max_blocks < blocks) goto out; // 同样地,确保是否有足够的索引节点,否则返回-EINVAL if (max_inodes < inodes) goto out; // 可以安全地挂载文件系统,因此这里设置error为0表示操作成功。 error = 0; // 设置最大索引节点数和可用节点数。 info->max_blocks = max_blocks; info->free_blocks = max_blocks - blocks; info->max_inodes = max_inodes; info->free_inodes = max_inodes - inodes; out: // 为文件系统超级块信息结构解锁。 spin_unlock(&info->stat_lock); // 成功则返回0,否则返回-EINVAL。 return error; }
2、在 tmpfs 中创建文件
(1)shmem_create
这是创建新文件时位于最顶层的函数。
// mm/shmem.c // 参数如下所示: // dir 是新文件创建时的目录索引节点。 // entry 是新文件创建时的目录节点。 // mode 是传递给开放系统调用的标志位。 static int shmem_create(struct inode *dir, struct dentry *dentry, int mode) { // 调用shmem_mknod() (见L. 2.2小节),并添加S_IFREG标志位到模式标志位,以 // 此创建一个常规文件。 return shmem_mknod(dir, dentry, mode | S_IFREG, 0); }
(2)shmem_mknod
// mm/shmem.c /* * File creation. Allocate an inode, and we're done.. */ static int shmem_mknod(struct inode *dir, struct dentry *dentry, int mode, int dev) { // 调用shmem_get_inode() (见L. 2. 3小节)创建一个新的索引节点。 struct inode *inode = shmem_get_inode(dir->i_sb, mode, dev); int error = -ENOSPC; // 如果成功创建索引节点,则更新目录统计数据并实例化新文件。 if (inode) { // 更新目录的大小。 dir->i_size += BOGO_DIRENT_SIZE; // 更新ctime和mtime字段。 dir->i_ctime = dir->i_mtime = CURRENT_TIME; // 实例化索引节点。 d_instantiate(dentry, inode); // 对目录项进行引用,以阻止在页面换出时意外地回收了目录项。 dget(dentry); /* Extra count - pin the dentry in core */ // 表明调用成功结束。 error = 0; } // 返回成功,否则返回-ENOSPC。 return error; }
① ⇒ shmem_get_inode
② ⇔ d_instantiate
// fs/dcache.c /** * d_instantiate - fill in inode information for a dentry * @entry: dentry to complete * @inode: inode to attach to this dentry * * Fill in inode information in the entry. * * This turns negative dentries into productive full members * of society. * * NOTE! This assumes that the inode count has been incremented * (or otherwise set) by the caller to indicate that it is now * in use by the dcache. */ void d_instantiate(struct dentry *entry, struct inode * inode) { if (!list_empty(&entry->d_alias)) BUG(); spin_lock(&dcache_lock); if (inode) list_add(&entry->d_alias, &inode->i_dentry); entry->d_inode = inode; spin_unlock(&dcache_lock); }
(3)shmem_get_inode
// mm/shmem.c // 这个函数用于更新空闲索引节点数,用new_inode()分配一个索引节点。 static struct inode *shmem_get_inode(struct super_block *sb, int mode, int dev) { struct inode *inode; struct shmem_inode_info *info; struct shmem_sb_info *sbinfo = SHMEM_SB(sb); // 获取sbinfo自旋锁,因为它即将被更新。 spin_lock(&sbinfo->stat_lock); // 确保有空闲的索引节点,如果没有,则返回NULL。 if (!sbinfo->free_inodes) { spin_unlock(&sbinfo->stat_lock); return NULL; } sbinfo->free_inodes--; // 更新空闲索引节点计数并释放锁。 spin_unlock(&sbinfo->stat_lock); // new_inode()处于文件系统层面,并在<linux/fs.h>中声明。它如何运作的详细情 // 况不在本文档讨论范围内,但其概要内容很简单。它从slab分配器中分配一个索引节点,并 // 将各字段赋予0,并根据超级块中的信息产生inode->i_sb,inode->i_dev和inode->i_blkbits。 // 实现在文件 fs/inode.c 中 inode = new_inode(sb); // 如果创建成功则填充索引节点各字段。 if (inode) { // 填充基本的索引节点信息。 inode->i_mode = mode; inode->i_uid = current->fsuid; inode->i_gid = current->fsgid; inode->i_blksize = PAGE_CACHE_SIZE; inode->i_blocks = 0; inode->i_rdev = NODEV; // 设置 address_space_operations 使用 shmem_aops,后者建立函数 shmem_writepage() // (见 L. 6. 1小节)用于address_space的页面回写回调函数。 inode->i_mapping->a_ops = &shmem_aops; // 填充更多的基本信息。 inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME; info = SHMEM_I(inode); info->inode = inode; // 初始化索引节点的semaphore信号量和自旋锁。 spin_lock_init(&info->lock); // 确定如何根据传入的模式信息填充剩余的字段。 switch (mode & S_IFMT) { default: // 在这种情况下,创建特定的索引节点。尤其是在挂载文件系统时和创建根索引节点时。 init_special_inode(inode, mode, dev); break; case S_IFREG: // 为常规文件创建索引节点。这里主要考虑的问题是设置 inode->i_op 和 inode->i_fop // 字段分别为 shmem_inode_operations 和 shmem_file_operations 。 inode->i_op = &shmem_inode_operations; inode->i_fop = &shmem_file_operations; spin_lock(&shmem_ilock); list_add_tail(&info->list, &shmem_inodes); spin_unlock(&shmem_ilock); break; case S_IFDIR: // 为新目录创建索引节点。更新i_nlink和i_size字段以显示增加的文件数量和 // 目录大小。这里主要考虑的问题是设置 inode->i_op 和 inode->i_fop 字段分别为 // shmem_dir_inode_operations 和 dcache_dir_ops 。 inode->i_nlink++; /* 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 = &dcache_dir_ops; break; case S_IFLNK: // 如果链接了文件, 由于它由父函数 shmem_link()操作, 因此它现在什么也不是 break; } } // 返回新索引节点,如果没有创建则返回NULL。 return inode; }
① ⇔ new_inode
// fs/inode.c /** * new_inode - obtain an inode * @sb: superblock * * Allocates a new inode for given superblock. */ struct inode * new_inode(struct super_block *sb) { static unsigned long last_ino; struct inode * inode; spin_lock_prefetch(&inode_lock); inode = alloc_inode(sb); if (inode) { spin_lock(&inode_lock); inodes_stat.nr_inodes++; list_add(&inode->i_list, &inode_in_use); inode->i_ino = ++last_ino; inode->i_state = 0; spin_unlock(&inode_lock); } return inode; }
② ⇔ init_special_inode
// fs/devices.c void init_special_inode(struct inode *inode, umode_t mode, int rdev) { inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = to_kdev_t(rdev); inode->i_cdev = cdget(rdev); } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = to_kdev_t(rdev); } else if (S_ISFIFO(mode)) inode->i_fop = &def_fifo_fops; else if (S_ISSOCK(mode)) inode->i_fop = &bad_sock_fops; else printk(KERN_DEBUG "init_special_inode: bogus imode (%o)\n", mode); }
③ ⇐ shmem_aops、shmem_file_operations
// mm/shmem.c static struct address_space_operations shmem_aops = { removepage: shmem_removepage, writepage: shmem_writepage, #ifdef CONFIG_TMPFS readpage: shmem_readpage, prepare_write: shmem_prepare_write, commit_write: shmem_commit_write, #endif }; static struct file_operations shmem_file_operations = { mmap: shmem_mmap, #ifdef CONFIG_TMPFS read: shmem_file_read, write: shmem_file_write, fsync: shmem_sync_file, #endif }; static struct inode_operations shmem_inode_operations = { truncate: shmem_truncate, setattr: shmem_notify_change, }; static struct inode_operations shmem_dir_inode_operations = { #ifdef CONFIG_TMPFS create: shmem_create, lookup: shmem_lookup, link: shmem_link, unlink: shmem_unlink, symlink: shmem_symlink, mkdir: shmem_mkdir, rmdir: shmem_rmdir, mknod: shmem_mknod, rename: shmem_rename, #endif };
④ ⇐ dcache_dir_ops
// fs/readdir.c struct file_operations dcache_dir_ops = { open: dcache_dir_open, release: dcache_dir_close, llseek: dcache_dir_lseek, read: generic_read_dir, readdir: dcache_readdir, fsync: dcache_dir_fsync, };
3、tmpfs 中的文件操作
(1)内存映射
用于映射虚拟文件到内存。惟一要做的改变是更新 VMA 的 vm_operations_struct 字段使用异常的等价 shmfs 。
① shmem_mmap
// mm/shmem.c static int shmem_mmap(struct file *file, struct vm_area_struct *vma) { struct vm_operations_struct *ops; struct inode *inode = file->f_dentry->d_inode; // 操作目前为虚拟文件系统所使用的vm_operations_struct。 ops = &shmem_vm_ops; // 确保索引节点映射的是常规文件。如果不是,则返回-EACCESS。 if (!S_ISREG(inode->i_mode)) return -EACCES; // 更新索引节点的atime,显示它是否已经被访问。 UPDATE_ATIME(inode); // 更新vma->vm_ops,这样shmem_nopage() 可以用于处理映射中的缺页中断。 vma->vm_ops = ops; return 0; }
⑴ ⇒ shmem_nopage
shmem_nopage 函数
(2)读取文件
① shmem_file_read
这是读取 tmpfs 文件时所调用的位于最顶层的函数。
// mm/shmem.c // 参数如下所示: // filp 是指向被读取文件的指针。 // buf 是应当填充的缓冲区。 // count 是应当读取的字节数。 // ppos 是当前位置。 static ssize_t shmem_file_read(struct file *filp, char *buf, size_t count, loff_t *ppos) { read_descriptor_t desc; // 计数不可能为负数。 if ((ssize_t) count < 0) return -EINVAL; // access_ok()确保安全地写count数量的字节到用户空间缓冲区。如果不能, // 则返回-EFAULT。 if (!access_ok(VERIFY_WRITE, buf, count)) return -EFAULT; if (!count) return 0; // 初始化 read_descriptor_t 结构,该结构最终传递给 file_read_actor() // (见 L.3.2.3) desc.written = 0; desc.count = count; desc.buf = buf; desc.error = 0; // 调用do_shmem_file_read()开始执行实际的读操作。 do_shmem_file_read(filp, ppos, &desc); // 返回写到用户空间缓冲区的字节数。 if (desc.written) return desc.written; // 如果没有写任何东西, 而返回错误 return desc.error; }
⑴ ⇐ read_descriptor_t
// include/linux/fs.h /* * "descriptor" for what we're up to with a read for sendfile(). * This allows us to use the same read code yet * have multiple different users of the data that * we read from a file. * * The simplest case just copies the data to user * mode. */ typedef struct { size_t written; size_t count; char * buf; int error; } read_descriptor_t;
⑵ ⇒ do_shmem_file_read
② do_shmem_file_read
这个函数通过 shmem_getpage() 找回读取文件所需的页面数,并调 file_read_actor() 复制数据到用户空间。
// mm/shmem.c static void do_shmem_file_read(struct file *filp, loff_t *ppos, read_descriptor_t *desc) { // 找回 inode 和使用 struct file 的 mapping。 struct inode *inode = filp->f_dentry->d_inode; struct address_space *mapping = inode->i_mapping; unsigned long index, offset; // index是文件中包含数据的页面的索引。 index = *ppos >> PAGE_CACHE_SHIFT; // offset是在当前被读取页面中的偏移量。 offset = *ppos & ~PAGE_CACHE_MASK; // 循环直到读取完请求的字节数。nr 是当前页面中还需要读取的字节数。desc->count // 初始为需要读取的字节数,并由file_read_actor()(见L. 3. 2. 3)减小。 for (;;) { struct page *page = NULL; unsigned long end_index, nr, ret; // end_index是文件中最后页面的索引。当到达文件尾部时停止。 end_index = inode->i_size >> PAGE_CACHE_SHIFT; // 当到达最后一个页面时,设置nr为当前页面中还需要读取的字节数。如果 // 文件指针在nr后面,则停止,因为没有更多的数据供读取。这有可能发生在文件被截断时的情况。 if (index > end_index) break; if (index == end_index) { nr = inode->i_size & ~PAGE_CACHE_MASK; if (nr <= offset) break; } // shmem_getpage() (见L. 5.1. 2)查找被请求页在页面高速缓存,交换缓存中 // 的位置。如果错误发生,则记录错误到desc->error并返回。 desc->error = shmem_getpage(inode, index, &page, SGP_READ); if (desc->error) { if (desc->error == -EINVAL) desc->error = 0; break; } /* * We must evaluate after, since reads (unlike writes) * are called without i_sem protection against truncate */ // nr是页面中必须读取的字节数,因此初始化为一个页面的大小,这样就可以读取整 // 个页面。 nr = PAGE_CACHE_SIZE; // 初始化end_index,它是文件中最后一个页面的索引 end_index = inode->i_size >> PAGE_CACHE_SHIFT; // 如果是文件的最后一个页面,则更新nr为页面的字节数。如果nr当前在文 // 件尾部之后(可能发生在文件截取的情况),则释放页面的引用(由 shmem_getpage() 使用),并 // 退出循环。 if (index == end_index) { nr = inode->i_size & ~PAGE_CACHE_MASK; if (nr <= offset) { page_cache_release(page); break; } } // 更新需要读取的字节数。请记得offset是在页面中当前的文件读取处。 nr -= offset; // 如果读取的页面不是全局零页面,则需要注意调用 flush_dcache_page() 所引 // 起的别名混淆隐患。如果是第一次读取的页面或者仅仅发生 lseek() (f_reada是0),则用 // mark_page_accessed()标记页面被访问过。 if (page != ZERO_PAGE(0)) { /* * If users can be writing to this page using arbitrary * virtual addresses, take care about potential aliasing * before reading the page on the kernel side. */ if (mapping->i_mmap_shared != NULL) flush_dcache_page(page); /* * Mark the page accessed if we read the * beginning or we just did an lseek. */ if (!offset || !filp->f_reada) mark_page_accessed(page); } /* * Ok, we have the page, and it's up-to-date, so * now we can copy it to user space... * * The actor routine returns how many bytes were actually used.. * NOTE! This may not be the same as how much of a user buffer * we filled up (we may be padding etc), so we can only update * "pos" here (the actor routine has to update the user buffer * pointers and the remaining count). */ // 调用file_read_actor()(见L. 3. 2. 3)复制数据到用户空间。它返回复制的字节数, // 并更新用户缓冲指针及剩下的计数。 ret = file_read_actor(desc, page, offset, nr); // 更新页面中读取的偏移量。 offset += ret; // 如果可能,移动索引至下一个页面。 index += offset >> PAGE_CACHE_SHIFT; // 确保offset是页面中的偏移量。 offset &= ~PAGE_CACHE_MASK; // 释放被复制页面的引用。该引用由shmem_getpage()使用。 page_cache_release(page); // 如果已经读取完请求的字节数,则返回。 if (ret != nr || !desc->count) break; } // 更新文件指针。 *ppos = ((loff_t) index << PAGE_CACHE_SHIFT) + offset; // 允许文件预读。 filp->f_reada = 1; // 更新inode的访问计数,因为它已经被读取。 UPDATE_ATIME(inode); }
⑴ ⇒ file_read_actor
⑵ ⇒ shmem_getpage
③ file_read_actor
这个函数用于从页面复制数据到用户空间缓冲区。它最终由多个函数所调用,包括 generic_file_read() 和 shmem_file_read() 。
// mm/filemap.c // 参数如下所示: // desc是个包含读信息的结构,包括缓冲区和从文件读取的总字节数。 // page是复制到用户空间且包含文件数据的页面。 // offset是页面中复制的偏移量。 // size是从页面读取的字节数。 int file_read_actor(read_descriptor_t * desc, struct page *page, unsigned long offset, unsigned long size) { char *kaddr; // count现在是从文件读取的字节数。 unsigned long left, count = desc->count; // 确保读取的字节数不会比请求的多。 if (size > count) size = count; // 调用kmap()映射页面到低端内存。见L L 1小节。 kaddr = kmap(page); // 从内核页面复制数据到用户空间缓冲区。定义在文件 include/asm-i386/uaccess.h 中 left = __copy_to_user(desc->buf, kaddr + offset, size); // 解除页面映射。见L 3.1小节。 kunmap(page); // 如果所有的字节都没有被复制,那肯定是缓冲区不能访问。这更新size,因 // 此desc->count可以反映读操作中还有多少字节待复制。返回-EFAULT到执行读操作的 // 进程。 if (left) { size -= left; desc->error = -EFAULT; } // 更新desc结构,以表明当前的读状态。 desc->count = count - size; desc->written += size; desc->buf += size; // 返回写入用户空间缓冲区的字节数。 return size; }
⑴ ⇒ kmap
kmap 函数
⑵ ⇒ kunmap
kunmap 函数
深入理解Linux虚拟内存管理(九)(中):https://developer.aliyun.com/article/1597911