很久之前对于虚拟文件部分的一些内容进行了整理。对于read的底层过程只是描述了一个大概,这次主要是对于每一部分的东向西进行详细的描述。go go go(方便对于面试时候的整理)。
参考 https://www.cnblogs.com/wangzahngjun/p/5553793.html
虚拟文件部分的整理参考原来博客的一片文章。具体的我就从open系统调用开始并且详细描述read的过程。
open系统调用:
对于sys_read 函数的调用过程如下
文件:
1.read()到sys_read();
2.通过task_struct->file_struct->fd[]->file*->f_op->read();
3通过目录项,找到该文件的inode;
4.在文件表中,通过文件内容偏移量计算出要读取的页;
5.通过inode找到文件对应的address_space;
6.在address_space中访问该文件的页缓存树,查找对应的页缓存结点:
(1)如果页缓存命中,那么直接返回文件内容;
(2)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取
相应的页填充该缓存页;重新进行第6步查找页缓存;
大体过程如上面所述。
具体而言的,实际的一个文件系统也是如上面所说的过程,以ext2 问价系统来描述。
对于ext2而言过程如下 sys_read->file->fop->read->do_sync_read->filp->f_op->aio_read->generic_file_aio_read.
do_sync_read 函数
ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = buf, .iov_len = len };//获取用户读字符保存buff空间的地址和所传进来的长度
struct kiocb kiocb;//用来跟踪当前同步和异步的io
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
for (;;) {
ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos); //读操作开始
if (ret != -EIOCBRETRY)
break;
wait_on_retry_sync_kiocb(&kiocb);
}
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&kiocb);
*ppos = kiocb.ki_pos;
return ret;
首先所有的参数被一层一层的传递下来到达文件系统提供的read函数。
调用的是filp->op->aio_read 同步调用这个立即返回读到的的数据。
他的底层调用的是generic_file_aio_read函数功能分析
if (filp->f_flags & O_DIRECT) {//设置直接读取不从页缓存中读。
loff_t size;
struct address_space *mapping;
struct inode *inode;
mapping = filp->f_mapping;
inode = mapping->host;
if (!count)
goto out; /* skip atime */
size = i_size_read(inode);
if (pos < size) {
retval = filemap_write_and_wait_range(mapping, pos,
pos + iov_length(iov, nr_segs) - 1);
if (!retval) {
retval = mapping->a_ops->direct_IO(READ, iocb,
iov, pos, nr_segs);
}
if (retval > 0)
*ppos = pos + retval;
if (retval) {
file_accessed(filp);
goto out;
}
}
这个参数是open函数的一个参数具体查看man 2 open 对于这个标志进行介绍具体可以总结未以下几点
O_DIRECT,绕过缓冲区高速缓存,直接IO
直接IO:Linux允许应用程序在执行磁盘IO时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备,称为直接IO(direct IO)或者裸IO(raw IO)。
应用场景:数据库系统,其高速缓存和IO优化机制均自成一体,无需内核消耗CPU时间和内存去完成相同的任务。
使用直接IO的弊端:可能会大大降低性能,内核对缓冲区告诉缓存做了不少优化,包括:按顺序预读取,在成簇磁盘块上执行IO,允许访问同一文件的多个进程共享高速缓存的缓冲区。
使用方法:在调用open函数打开文件或设备时指定O_DIRECT标志。
注意可能发生的不一致性:若一进程以O_DIRECT标志打开某文件,而另一进程以普通(即使用了高速缓存缓冲区)打开同一文件,则由直接IO所读写的数据与缓冲区高速缓存中内容之间不存在一致性,应尽量避免这一场景。
使用直接IO需要遵守的一些限制:
用于传递数据的缓冲区,其内存边界必须对齐为块大小的整数倍
数据传输的开始点,即文件和设备的偏移量,必须是块大小的整数倍
待传递数据的长度必须是块大小的整数倍。
不遵守上述任一限制均将导致EINVAL错误。
如果没有设置上面标志那么它会从页缓存中读取
for (seg = 0; seg < nr_segs; seg++) {
read_descriptor_t desc;
//将iovec结构转换成read_descriptor_t的结构
desc.written = 0;
desc.arg.buf = iov[seg].iov_base;//上面的参数重新打包一下啊
desc.count = iov[seg].iov_len;
if (desc.count == 0)
continue;
desc.error = 0;
do_generic_file_read(filp, ppos, &desc, file_read_actor);//调用当前的函数进行读取。
retval += desc.written;
if (desc.error) {
retval = retval ?: desc.error;
break;
}
if (desc.count > 0)
break;
}
out:
return retval;
do_generic_file_read()函数的主要功能是 //查找对应文件的页缓存 address_space 是根据文件的index所查找的对应的缓存页好。 find_get_page()使用地址空间的基树查找索引为index的页。尝试去找到第一个被请求的页。如果这个页不在页缓存中,就跳转到标号no_cached_page处,如果该页不是最新的,就跳转到标号page_not_up_to_date_locked处,如果在尝试去获取这个页面的独占权,即加锁的时候,没有获取成功,则跳转到page_not_up_to_date处。;
atic void do_generic_file_read(struct file *filp, loff_t *ppos,
read_descriptor_t *desc, read_actor_t actor)
{
struct address_space *mapping = filp->f_mapping; //获取页高速缓存
struct inode *inode = mapping->host;//获取inode
struct file_ra_state *ra = &filp->f_ra;
pgoff_t index;
pgoff_t last_index;
pgoff_t prev_index;
unsigned long offset; /* offset into pagecache page */
unsigned int prev_offset;
int error
. ............................
.............................
如果查到了但可能加锁的过程中出现的一下问题。
page_not_up_to_date_locked:
/* Did it get truncated before we got the lock? */
//有可能在锁页面的时候`有其它的进程将页面移除了页缓存区
//在这种情况下:将page解锁`并减少它的使用计数,重新循环```
//重新进入循环后,在页缓存区找不到对应的page.就会重新分配一个新的page
if (!page->mapping) {
unlock_page(page);
page_cache_release(page);
continue;
}
/* Did somebody else fill it already? */
//在加锁的时候,有其它的进程完成了从文件系统到具体页面的映射?
//在这种情况下,返回到page_ok.直接将页面上的内容copy到用户空间即可
if (PageUptodate(page)) {
unlock_page(page);
goto page_ok;
}
如果不是最新的页面则需要更新。更新的时候可能的问题。在这里调用实际的读page的操作mpping->a_ops->readpage()对该页进行读取。如果成功读取了一个页,检查其是否是最新的,如果是最新的,则跳转到标号page_ok处。如果发生了同步读错误,就设置其error,并跳转到readpage_error处。
readpage:
/* Start the actual read. The read will unlock the page. */
error = mapping->a_ops->readpage(filp, page); //调用具体的readpage函数,在后面会分析。
if (unlikely(error)) { //如果发生了AOP_TRUNCATED_PAGE错误,则回到find_page重新进行获取
if (error == AOP_TRUNCATED_PAGE) {
page_cache_release(page);
goto find_page;
}
goto readpage_error;
}
//如果PG_uptodata标志仍然末设置.就一直等待,一直到page不处于锁定状态
// 在将文件系统的内容读入page之前,page一直是处理Lock状态的。一直到
//读取完成后,才会将页面解锁
if (!PageUptodate(page)) {
error = lock_page_killable(page);
if (unlikely(error))
goto readpage_error;
if (!PageUptodate(page)) {
if (page->mapping == NULL) {
/*
* invalidate_inode_pages got it
*/
unlock_page(page);
page_cache_release(page);
goto find_page;
}
unlock_page(page);
shrink_readahead_size_eio(filp, ra);
error = -EIO;
goto readpage_error;
}
unlock_page(page);
}
goto page_ok; //读取成功
当该页不存在的时候:
这里主要讲述了当没有改页时时如何处理的。最后我们来看下do_generic_file_read函数的out
out:
ra->prev_pos = prev_index;
ra->prev_pos <<= PAGE_CACHE_SHIFT;
ra->prev_pos |= prev_offset;
*ppos = ((loff_t)index << PAGE_CACHE_SHIFT) + offset; //计算实际的偏移量
file_accessed(filp);//更新文件的最后一次访问时间。
}
具体上来说就这莫多东西。