深入理解Linux虚拟内存管理(九)(上):https://developer.aliyun.com/article/1597903
(3)写入文件
① shmem_file_write
// mm/shmem.c // 这块是函数导引。 static ssize_t shmem_file_write(struct file *file, const char *buf, size_t count, loff_t *ppos) { // 获取描述哪些文件被写入的索引节点。 struct inode *inode = file->f_dentry->d_inode; loff_t pos; unsigned long written; int err; // 如果用户尝试写负的字节数,则返回-EINVAL。 if ((ssize_t) count < 0) return -EINVAL; // 如果用户空间缓冲区不可访问,则返回-EINVAL。 if (!access_ok(VERIFY_READ, buf, count)) return -EFAULT; // 获取保护索引节点的semaphore信号量。 down(&inode->i_sem); // 记录写入开始的位置。 pos = *ppos; // 初始化写入字节为0。 written = 0; // precheck_file_write()执行一系列的检查以确保写操作的正确进行。检查包括以 // append 模式打开文件时更新pos为文件尾部,并保证系统不会超过进程的限定。 // 文件实现在 mm/filemap.c 中 err = precheck_file_write(file, inode, &count, &pos); // 如果不能执行写操作,则跳转到out。 if (err || !count) goto out; // 如果设置了 SUID 位则清除它。 remove_suid(inode); // 更新索引节点的ctime和mtime。 inode->i_ctime = inode->i_mtime = CURRENT_TIME; // 循环直至所有的请求都进行了写操作。 do { struct page *page = NULL; unsigned long bytes, index, offset; char *kaddr; int left; // 设置offset为当前被写页面内的偏移量。 offset = (pos & (PAGE_CACHE_SIZE -1)); /* Within page */ // index是当前被写文件中页面的索引。 index = pos >> PAGE_CACHE_SHIFT; // bytes是当前页面中还需要写入的字节数。 bytes = PAGE_CACHE_SIZE - offset; // 如果bytes表明需要写入比请求(count)更多的字节数,则设置bytes为count。 if (bytes > count) bytes = count; /* * We don't hold page lock across copy from user - * what would it guard against? - so no deadlock here. */ // 定位要写入的页面。SGP_WRITE 标志位表明了如果某个页面还不存在则需 // 要分配它。如果无法找到某页面或分配页面,则跳出循环。 err = shmem_getpage(inode, index, &page, SGP_WRITE); if (err) break; // 在再次解除页面映射以前,映射被写入且从用户空间缓冲区复制数据的页面。 kaddr = kmap(page); // 从用户空间缓冲区复制数据到内核页面。定义在文件 include/asm-i386/uaccess.h 中 left = __copy_from_user(kaddr + offset, buf, bytes); kunmap(page); // 更新写入的字节数。 written += bytes; // 更新还需要写入的字节数。 count -= bytes; // 更新在文件中的位置。 pos += bytes; // 更新用户空间缓冲区中的指针。 buf += bytes; // 如果文件变大,则更新inode->i_size。 if (pos > inode->i_size) inode->i_size = pos; // 刷新dcache以避免别名混淆隐患。 flush_dcache_page(page); // 设置该页面为脏且被引用过。 SetPageDirty(page); SetPageReferenced(page); // 释放shmem_getpage()对页面使用的引用。 page_cache_release(page); // 如果所有请求的字节都没有从用户空间缓存区读出,则更新写操作统计数据和 // 文件中的位置及缓冲区中的位置。 if (left) { pos -= left; written -= left; err = -EFAULT; break; } } while (count); // 更新文件指针。 *ppos = pos; // 如果所有的字节都没有写入,则设置错误返回变量。 if (written) err = written; out: // 释放索引节点semaphore信号量。 up(&inode->i_sem); // 返回成功或者返回还需要写入的字节数。 return err; }
⑴ ⇒ shmem_getpage
⑵ ⇒ kmap
kmap 函数
⑶ ⇒ kunmap
kunmap 函数
(4)符号链接
① shmem_symlink
这个函数用于创建符号链接 symname 并决定在什么地方存储该信息。如果链接的名字足够小则存储在索引节点中,否则存储在页面帧当中。
// mm/shmem.c static int shmem_symlink(struct inode *dir, struct dentry *dentry, const char *symname) { // 这块执行基本的全部检查,并为符号链接创建一个新的索引节点。 // 参数symname是所创建链接的名字。 int error; int len; struct inode *inode; struct page *page = NULL; char *kaddr; struct shmem_inode_info *info; // 计算链接的长度(len)。 len = strlen(symname) + 1; // 如果名字比一个页面长,则返回-ENAMETOOLONG。 if (len > PAGE_CACHE_SIZE) return -ENAMETOOLONG; // 分配一个新的inodeo分配失败则返回-ENOSPC inode = shmem_get_inode(dir->i_sb, S_IFLNK|S_IRWXUGO, 0); if (!inode) return -ENOSPC; // 获取私有信息结构。 info = SHMEM_I(inode); // 索引节点的大小即链接的长度。 inode->i_size = len-1; // 这块用于存储链接信息。 // 如果名字的长度小于shmem_inode_info所使用的空间,则复制名字到保留 // 给私有结构的空间中。 if (len <= sizeof(struct shmem_inode_info)) { /* do it inline */ memcpy(info, symname, len); // 设置inode->i_op为shmem_symlink_inline_operations操作,这样可以知道 // 链接名字在索引节点中。 inode->i_op = &shmem_symlink_inline_operations; } else { // 用 shmem_getpage_locked 分配页面。 error = shmem_getpage(inode, 0, &page, SGP_WRITE); // 如果发生错误,则解除索引节点的引用并返回错误。 if (error) { iput(inode); return error; } // 使用shmem_symlink_inode_operations, 这样可以知道链接信息被包括在页面中。 inode->i_op = &shmem_symlink_inode_operations; // shmem_ilock是一个全局锁,用于保护索引节点的全局链表,这个链表是由私有的 // 信息结构info->list字段链接起来的。 spin_lock(&shmem_ilock); // 添加新的索引节点到全局链表。 list_add_tail(&info->list, &shmem_inodes); // 释放 shmem_ilock 。 spin_unlock(&shmem_ilock); // 映射页面。 kaddr = kmap(page); // 复制链接信息。 memcpy(kaddr, symname, len); // 解除页面映射。 kunmap(page); // 设置页面为脏。 SetPageDirty(page); // 释放对页面的引用。 page_cache_release(page); } // 增加目录的大小,因为添加了新的索引节点。BOGO_DIRENT_SIZE 是索引节点的 // 伪大小,这样 ls 操作看起来更好一些。 dir->i_size += BOGO_DIRENT_SIZE; // 更新 i_ctime 和 i_mtime。 dir->i_ctime = dir->i_mtime = CURRENT_TIME; // 实例化索引节点。 d_instantiate(dentry, inode); dget(dentry); // 返回成功。 return 0; }
⑴ ⇒ shmem_get_inode
⑵ ⇒ shmem_getpage
⑶ ⇒ d_instantiate
② shmem_readlink_inline
// mm/shmem.c static int shmem_readlink_inline(struct dentry *dentry, char *buffer, int buflen) { // 链接名字被包括在索引节点中,这样可以将它作为vfs_readlink()的参数传递给 VFS 层。 // 其实现在文件 fs/namei.c 中 return vfs_readlink(dentry, buffer, buflen, (const char *)SHMEM_I(dentry->d_inode)); }
③ shmem_follow_link_inline
// mm/shmem.c static int shmem_follow_link_inline(struct dentry *dentry, struct nameidata *nd) { // 链接名字被包括在索引节点中,这样可以将它作为vfs_followlink()的参数传递给 VFS 层。 // 其实现在文件 fs/namei.c 中 return vfs_follow_link(nd, (const char *)SHMEM_I(dentry->d_inode)); }
④ shmem_follow_link
// mm/shmem.c static int shmem_follow_link(struct dentry *dentry, struct nameidata *nd) { struct page *page = NULL; // 由于链接名字在页面中,因此可以调用shmem_getpage()获取页面。 int res = shmem_getpage(dentry->d_inode, 0, &page, SGP_READ); // 如果发生错误,则返回错误。 if (res) return res; // 映射页面并作为指针传递给vfs_follow_link()。 // 其实现在文件 fs/namei.c 中 res = vfs_follow_link(nd, kmap(page)); // 解除页面映射。 kunmap(page); mark_page_accessed(page); // 解除对页面的引用。 page_cache_release(page); // 返回成功。 return res; }
⑴ ⇒ shmem_getpage
⑤ shmem_readlink
// mm/shmem.c static int shmem_readlink(struct dentry *dentry, char *buffer, int buflen) { struct page *page = NULL; // 链接名字被包括在和symlink关联的页面中,它可以调用shmem_getpage()获取指针。 int res = shmem_getpage(dentry->d_inode, 0, &page, SGP_READ); // 如果发生错误,则返回NULL。 if (res) return res; // 调用kmap() 映射页面,并将它作为参数传递给vfs_readlink()。该 // 链接位于页面首部。 // 其实现在文件 fs/namei.c 中 res = vfs_readlink(dentry, buffer, buflen, kmap(page)); // 解除页面映射。 kunmap(page); // 标记页面被访问过。 mark_page_accessed(page); // 解除调用shmem_getpage()对页面的引用。 page_cache_release(page); // 返回该链接。 return res; }
⑴ ⇒ shmem_getpage
(5)同步文件
① shmem_sync_file
这个函数简单返回 0,因为文件只存在于内存而不需要同步文件到磁盘。
// mm/shmem.c static int shmem_sync_file(struct file *file, struct dentry *dentry, int datasync) { return 0; }
4、tmpfs 中的索引节点操作
(1)截取
① shmem_truncate
当调用这个函数的时候,inode->i_size 被 vmtruncate() 设置为新大小。这个函数用于创建或移除页面,以此设置文件的大小。
// mm/shmem.c static void shmem_truncate(struct inode *inode) { // 用SHMEM_I()获取索引节点的私有文件系统信息。 struct shmem_inode_info *info = SHMEM_I(inode); // 获取超级块私有信息。 struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb); unsigned long freed = 0; unsigned long index; // 更新索引节点的ctime和mtime。 inode->i_ctime = inode->i_mtime = CURRENT_TIME; // 获取文件新的尾部页面的索引。原来的大小存储在info->next_index中。 index = (inode->i_size + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; // 如果文件扩大了,只需要返回,因为全局。页面将用于表示扩大的区域。 if (index >= info->next_index) return; // 获取私有info自旋锁。 spin_lock(&info->lock); // 继续调用shmem_truncate_indirect()直至文件被截取为需要的大小。 while (index < info->next_index) freed += shmem_truncate_indirect(info, index); // 如果shmem_info_info结构表示的换出页面多于文件中的页面,这是个bug。 BUG_ON(info->swapped > info->next_index); // 释放私有info自旋锁。 spin_unlock(&info->lock); // 获取超级块私有info自旋锁。 spin_lock(&sbinfo->stat_lock); // 更新可用空闲块的数量。 sbinfo->free_blocks += freed; // 更新索引节点所使用的块数量。 inode->i_blocks -= freed*BLOCKS_PER_PAGE; // 释放超级块私有info自旋锁。 spin_unlock(&sbinfo->stat_lock); }
② shmem_truncate_indirect
这个函数定位索引节点中最后两次间接访问的块,并调用 shmem_truncate_direct() 截取它。
// mm/shmem.c /* * shmem_truncate_indirect - truncate an inode * * @info: the info structure of the inode * @index: the index to truncate * * This function locates the last doubly indirect block and calls * then shmem_truncate_direct to do the real work */ static inline unsigned long shmem_truncate_indirect(struct shmem_inode_info *info, unsigned long index) { swp_entry_t ***base; unsigned long baseidx, start; // len是当前文件中被第2次使用的最后页面。 unsigned long len = info->next_index; unsigned long freed; // 如果文件很小且所有的项都存储在直接块信息中,则调用shmem_free_swp() 并传递 // info->i_direct 中第一个交换项和项数目给这个函数,供截取之用。 if (len <= SHMEM_NR_DIRECT) { info->next_index = index; if (!info->swapped) return 0; freed = shmem_free_swp(info->i_direct + index, info->i_direct + len); info->swapped -= freed; return freed; } // 被截取的页面位于间接块的某处。这个代码部分用于计算3个变量:base, // baseidx和len。base是页面的首部,该页面包括将被截取交换项的指针。baseidx是被使用间 // 接块中第一个项的页索引而len是这里将被截取的项数目。 // // 计算两次间接块的变量。接着设置base到info->i_indirect首部的交换项。而 // info->i_indirect 首部的页面索引baseidx为SHMEM_NR_DIRECT。在这里 len 为文件中页 // 面的数量,因此减去直接块的数量就是剩下页面的数量。 if (len <= ENTRIES_PER_PAGEPAGE/2 + SHMEM_NR_DIRECT) { len -= SHMEM_NR_DIRECT; base = (swp_entry_t ***) &info->i_indirect; baseidx = SHMEM_NR_DIRECT; } else { // 如果不是这样,那么这是一个3级索引块,因此必须在计算base,baseidx和len // 以前遍历下一级。 len -= ENTRIES_PER_PAGEPAGE/2 + SHMEM_NR_DIRECT; BUG_ON(len > ENTRIES_PER_PAGEPAGE*ENTRIES_PER_PAGE/2); baseidx = len - 1; baseidx -= baseidx % ENTRIES_PER_PAGEPAGE; base = (swp_entry_t ***) info->i_indirect + ENTRIES_PER_PAGE/2 + baseidx/ENTRIES_PER_PAGEPAGE; len -= baseidx; baseidx += ENTRIES_PER_PAGEPAGE/2 + SHMEM_NR_DIRECT; } // 如果在截取后文件变大,则更新 next_index 为文件新尾部,并设置start为间接 // 块的首部。 if (index > baseidx) { info->next_index = index; start = index - baseidx; } else { // 如果在截取后文件变小,则移动当前文件尾部至将被截取的间接块的首部。 info->next_index = baseidx; start = 0; } // 如果在base处有块,则调用shmem_truncate_direct()截取其中的页面。 return *base? shmem_truncate_direct(info, base, start, len): 0; }
③ shmem_truncate_direct
这个函数用于循环遍历间接块,并对包含被截取交换向量的每个页面调用 shmem_free_swp。
// mm/shmem.c /* * shmem_truncate_direct - free the swap entries of a whole doubly * indirect block * * @info: the info structure of the inode * @dir: pointer to the pointer to the block * @start: offset to start from (in pages) * @len: how many pages are stored in this block */ static inline unsigned long shmem_truncate_direct(struct shmem_inode_info *info, swp_entry_t ***dir, unsigned long start, unsigned long len) { swp_entry_t **last, **ptr; unsigned long off, freed_swp, freed = 0; // last是被截取间接块中最后一个页面。 last = *dir + (len + ENTRIES_PER_PAGE - 1) / ENTRIES_PER_PAGE; // 如果截取是部分截取而非全页截取,则off是页面中的截取偏移量。 off = start % ENTRIES_PER_PAGE; // 从dir的startth块开始截取,直至最后一块。 for (ptr = *dir + start/ENTRIES_PER_PAGE; ptr < last; ptr++, off = 0) { // 如果这里没有页面,则继续下一个。 if (!*ptr) continue; // 如果info结构表明交换出属于这个索引节点的页面,则调用shmem_free_swp() // 释放与该页面相关联的交换槽。如果释放了一个,那么更新infoswapped并增加空闲页面的 // 计数。 if (info->swapped) { freed_swp = shmem_free_swp(*ptr + off, *ptr + ENTRIES_PER_PAGE); info->swapped -= freed_swp; freed += freed_swp; } // 如果这不是部分截取,则释放该页面。 if (!off) { freed++; free_page((unsigned long) *ptr); *ptr = 0; } } // 如果整个间接块现在已释放,则回收该页面。 if (!start) { freed++; free_page((unsigned long) *dir); *dir = 0; } // 返回被释放页面的数量。 return freed; }
④ shmem_free_swp
这个函数释放起始项位于 dir 的交换项的 count 。
// mm/shmem.c /* * shmem_free_swp - free some swap entries in a directory * * @dir: pointer to the directory * @edir: pointer after last entry of the directory */ static int shmem_free_swp(swp_entry_t *dir, swp_entry_t *edir) { swp_entry_t *ptr; int freed = 0; // 循环释放每个交换项。 for (ptr = dir; ptr < edir; ptr++) { // 如果存在交换项,则用free_swap_and_cache()释放它并设置交换项为0。它增 // 加被释放页面的数量。 if (ptr->val) { free_swap_and_cache(*ptr); *ptr = (swp_entry_t){0}; freed++; } } // 返回被释放页面的总数量。 return freed; }
(2)链接
① shmem_link
这个函数创建一个从 dentry 到 old_dentry 的硬链接。
// mm/shmem.c /* * Link a file.. */ static int shmem_link(struct dentry *old_dentry, struct inode *dir, struct dentry *dentry) { // 获取与old_dentry对应的索引节点。 struct inode *inode = old_dentry->d_inode; // 如果它链接到一个目录,则返回-EPERM。严格意义上,根目录应当允许硬 // 链接目录,虽然并不推荐这么处理,但是在文件系统中创建循环时利用诸如find的操作时可 // 能会找不到路径。所以tmpfs简单地不允许目录硬链接。 if (S_ISDIR(inode->i_mode)) return -EPERM; // 为新链接增加目录的大小。 dir->i_size += BOGO_DIRENT_SIZE; // 更新目录的mtime和ctime,并更新索引节点的ctime。 inode->i_ctime = dir->i_ctime = dir->i_mtime = CURRENT_TIME; // 增加指向索引节点的链接数量。 inode->i_nlink++; atomic_inc(&inode->i_count); /* New dentry reference */ // 调用dget()获取新dentry的额外引用。 dget(dentry); /* Extra pinning count for the created dentry */ // 实例化新目录项。 d_instantiate(dentry, inode); // 返回成功。 return 0; }
(3)解除链接
① shmem_unlink
// mm/shmem.c static int shmem_unlink(struct inode *dir, struct dentry *dentry) { // 获取解除链接的dentry的索引节点。 struct inode *inode = dentry->d_inode; // 更新目录索引节点的大小。 dir->i_size -= BOGO_DIRENT_SIZE; // 更新ctime和mtime变量。 inode->i_ctime = dir->i_ctime = dir->i_mtime = CURRENT_TIME; // 减小索引节点的链接数量。 inode->i_nlink--; // 调用dput()减小dentry的引用计数。在引用计数为0时,这个函数也调用iput()清 // 除索引节点。 dput(dentry); /* Undo the count from "create" - this does all the work */ return 0; }
(4)创建目录
① shmem_mkdir
// mm/shmem.c static int shmem_mkdir(struct inode *dir, struct dentry *dentry, int mode) { int error; // 调用shmem_mknod() (见L. 2. 2小节)创建一个特定文件。通过指定S_IFDIR标 // 志位来创建一个目录。 if ((error = shmem_mknod(dir, dentry, mode | S_IFDIR, 0))) return error; // 增加父目录的i_nlink字段。 dir->i_nlink++; return 0; }
(5)移除目录
① shmem_rmdir
// mm/shmem.c static int shmem_rmdir(struct inode *dir, struct dentry *dentry) { // 用shmem_empty()检查目录是否为空。如果不是,则返回-ENOTEMPTY。 if (!shmem_empty(dentry)) return -ENOTEMPTY; // 减小父目录的i_nlink字段。 dir->i_nlink--; // 返回 shmem_unlink()(见L. 4. 3. 1)的结果,结果一般是删除该目录。 return shmem_unlink(dir, dentry); }
② shmem_empty
这个函数检查目录是否为空。
// mm/shmem.c /* * Check that a directory is empty (this works * for regular files too, they'll just always be * considered empty..). * * Note that an empty directory can still have * children, they just all have to be negative.. */ static int shmem_empty(struct dentry *dentry) { struct list_head *list; // dcache_lock虽然保护多个事物,但主要保护dcache的查找,因为这是该函数必须做的工作。 spin_lock(&dcache_lock); list = dentry->d_subdirs.next; // 循环遍历子目录链表并查找活动目录项,而子目录链表包含了所有的子目录项。如 // 果找到,则表明目录不为空。 while (list != &dentry->d_subdirs) { // 获取子目录项。 struct dentry *de = list_entry(list, struct dentry, d_child); // 如果目录项有一个相关联的合法索引节点且目前通过hash查找到,则由 // shmem_positive() (见 L. 4. 5. 3)返回。 // 因为如果hash查找到,则意味着目录项是活动的,且目录不为空。 if (shmem_positive(de)) { // 如果目录不为空,则释放自旋锁并返回。 spin_unlock(&dcache_lock); return 0; } // 转到下一个子目录项。 list = list->next; } // 目录为空。则释放自旋锁并返回。 spin_unlock(&dcache_lock); return 1; }
③ shmem_positive
// mm/shmem.c static inline int shmem_positive(struct dentry *dentry) { // 如果目录项有一个相关联的合法索引节点且目前通过hash查找到,则返回真。 return dentry->d_inode && !d_unhashed(dentry); }
深入理解Linux虚拟内存管理(九)(下):https://developer.aliyun.com/article/1597920