Linux 内核源代码情景分析(四)(下)

简介: Linux 内核源代码情景分析(四)

Linux 内核源代码情景分析(四)(中):https://developer.aliyun.com/article/1598003

(1)generic_file_write

// fs/ext2/file.c
struct file_operations ext2_file_operations = {
  llseek:   ext2_file_lseek,
  read:   generic_file_read,
  write:    generic_file_write,
  ioctl:    ext2_ioctl,
  mmap:   generic_file_mmap,
  open:   ext2_open_file,
  release:  ext2_release_file,
  fsync:    ext2_sync_file,
};

// =================================================================================
// mm/filemap.c
/*
 * Write to a file through the page cache. 
 *
 * We currently put everything into the page cache prior to writing it.
 * This is not a problem when writing full pages. With partial pages,
 * however, we first have to read the data into the cache, then
 * dirty the page, and finally schedule it for writing. Alternatively, we
 * could write-through just the portion of data that would go into that
 * page, but that would kill performance for applications that write data
 * line by line, and it's prone to race conditions.
 *
 * Note that this routine doesn't try to keep track of dirty pages. Each
 * file system has to do this all by itself, unfortunately.
 *              okir@monad.swb.de
 */
ssize_t
generic_file_write(struct file *file,const char *buf,size_t count,loff_t *ppos)
{
  struct inode  *inode = file->f_dentry->d_inode; 
  struct address_space *mapping = inode->i_mapping;
  unsigned long limit = current->rlim[RLIMIT_FSIZE].rlim_cur;
  loff_t    pos;
  struct page *page, *cached_page;
  unsigned long written;
  long    status;
  int   err;

  cached_page = NULL;

  down(&inode->i_sem);

  pos = *ppos;
  err = -EINVAL;
  if (pos < 0)
    goto out;

  err = file->f_error;
  if (err) {
    file->f_error = 0;
    goto out;
  }

  written = 0;

  if (file->f_flags & O_APPEND)
    pos = inode->i_size;

  /*
   * Check whether we've reached the file size limit.
   */
  err = -EFBIG;
  if (limit != RLIM_INFINITY) {
    if (pos >= limit) {
      send_sig(SIGXFSZ, current, 0);
      goto out;
    }
    if (count > limit - pos) {
      send_sig(SIGXFSZ, current, 0);
      count = limit - pos;
    }
  }

  status  = 0;
  if (count) {
    remove_suid(inode);
    inode->i_ctime = inode->i_mtime = CURRENT_TIME;
    mark_inode_dirty_sync(inode);
  }

  while (count) {
    unsigned long bytes, index, offset;
    char *kaddr;
    int deactivate = 1;

    /*
     * Try to find the page in the cache. If it isn't there,
     * allocate a free page.
     */
    offset = (pos & (PAGE_CACHE_SIZE -1)); /* Within page */
    index = pos >> PAGE_CACHE_SHIFT;
    bytes = PAGE_CACHE_SIZE - offset;
    if (bytes > count) {
      bytes = count;
      deactivate = 0;
    }

    /*
     * Bring in the user page that we will copy from _first_.
     * Otherwise there's a nasty deadlock on copying from the
     * same page as we're writing to, without it being marked
     * up-to-date.
     */
    { volatile unsigned char dummy;
      __get_user(dummy, buf);
      __get_user(dummy, buf+bytes-1);
    }

    status = -ENOMEM; /* we'll assign it later anyway */
    page = __grab_cache_page(mapping, index, &cached_page);
    if (!page)
      break;

    /* We have exclusive IO access to the page.. */
    if (!PageLocked(page)) {
      PAGE_BUG(page);
    }

    status = mapping->a_ops->prepare_write(file, page, offset, offset+bytes);
    if (status)
      goto unlock;
    kaddr = page_address(page);
    status = copy_from_user(kaddr+offset, buf, bytes);
    flush_dcache_page(page);
    if (status)
      goto fail_write;
    status = mapping->a_ops->commit_write(file, page, offset, offset+bytes);
    if (!status)
      status = bytes;

    if (status >= 0) {
      written += status;
      count -= status;
      pos += status;
      buf += status;
    }
unlock:
    /* Mark it unlocked again and drop the page.. */
    UnlockPage(page);
    if (deactivate)
      deactivate_page(page);
    page_cache_release(page);

    if (status < 0)
      break;
  }
  *ppos = pos;

  if (cached_page)
    page_cache_free(cached_page);

  /* For now, when the user asks for O_SYNC, we'll actually
   * provide O_DSYNC. */
  if ((status >= 0) && (file->f_flags & O_SYNC))
    status = generic_osync_inode(inode, 1); /* 1 means datasync */
  
  err = written ? written : status;
out:

  up(&inode->i_sem);
  return err;
fail_write:
  status = -EFAULT;
  ClearPageUptodate(page);
  kunmap(page);
  goto unlock;
}

(2)address_space

// include/linux/fs.h
struct address_space {
  struct list_head  clean_pages;  /* list of clean pages */
  struct list_head  dirty_pages;  /* list of dirty pages */
  struct list_head  locked_pages; /* list of locked pages */
  unsigned long   nrpages;  /* number of total pages */
  struct address_space_operations *a_ops; /* methods */
  struct inode    *host;    /* owner: inode, block_device */
  struct vm_area_struct *i_mmap;  /* list of private mappings */
  struct vm_area_struct *i_mmap_shared; /* list of shared mappings */
  spinlock_t    i_shared_lock;  /* and spinlock protecting it */
};

struct address_space_operations {
  int (*writepage)(struct page *);
  int (*readpage)(struct file *, struct page *);
  int (*sync_page)(struct page *);
  int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
  int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
  /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
  int (*bmap)(struct address_space *, long);
};

   通常这个数据结构就在 inode 结构中,成为 inode 结构的一部分,那就是 i_data (注意切莫与 ext2_inode_info 结构中的数组 i_data[]相混淆)。结构中的队列头 pages 就是用来维持缓冲页面队列的。 如果将文件映射到某些进程的用户空间,则指针 i_mmap 指向一串虚存区间,即 vm_area_struct 结构, 其中的每一个数据结构都代表着该文件在某一个进程中的空间映射。还有个指针 a_ops 也是很重要的, 它指向一个 address_space_operations 数据结构。这个结构中的函数指针给出了缓冲页面与具体文件系统的设备层之间的关系和操作,例如怎样从具体文件系统的设备上读或写一个缓冲页面等等。就 Ext2 文件系统来说,这个数据结构为 ext2_aops,是在 fs/ext2/inode.c 中定义的:

// fs/ext2/inode.c
struct address_space_operations ext2_aops = {
  readpage: ext2_readpage,
  writepage: ext2_writepage,
  sync_page: block_sync_page,
  prepare_write: ext2_prepare_write,
  commit_write: generic_commit_write,
  bmap: ext2_bmap
};

(3)ext2_prepare_write

// fs/ext2/inode.c
static int ext2_prepare_write(struct file *file, struct page *page, unsigned from, unsigned to)
{
  return block_prepare_write(page,from,to,ext2_get_block);
}

① block_prepare_write
// =================================================================================
// fs/buffer.c
int block_prepare_write(struct page *page, unsigned from, unsigned to,
      get_block_t *get_block)
{
  struct inode *inode = page->mapping->host;
  int err = __block_prepare_write(inode, page, from, to, get_block);
  if (err) {
    ClearPageUptodate(page);
    kunmap(page);
  }
  return err;
}

static int __block_prepare_write(struct inode *inode, struct page *page,
    unsigned from, unsigned to, get_block_t *get_block)
{
  unsigned block_start, block_end;
  unsigned long block;
  int err = 0;
  unsigned blocksize, bbits;
  struct buffer_head *bh, *head, *wait[2], **wait_bh=wait;
  char *kaddr = kmap(page);

  blocksize = inode->i_sb->s_blocksize;
  if (!page->buffers)
    create_empty_buffers(page, inode->i_dev, blocksize);
  head = page->buffers;

  bbits = inode->i_sb->s_blocksize_bits;
  block = page->index << (PAGE_CACHE_SHIFT - bbits);

  for(bh = head, block_start = 0; bh != head || !block_start;
      block++, block_start=block_end, bh = bh->b_this_page) {
    if (!bh)
      BUG();
    block_end = block_start+blocksize;
    if (block_end <= from)
      continue;
    if (block_start >= to)
      break;
    if (!buffer_mapped(bh)) {
      err = get_block(inode, block, bh, 1);
      if (err)
        goto out;
      if (buffer_new(bh)) {
        unmap_underlying_metadata(bh);
        if (Page_Uptodate(page)) {
          set_bit(BH_Uptodate, &bh->b_state);
          continue;
        }
        if (block_end > to)
          memset(kaddr+to, 0, block_end-to);
        if (block_start < from)
          memset(kaddr+block_start, 0, from-block_start);
        if (block_end > to || block_start < from)
          flush_dcache_page(page);
        continue;
      }
    }
    if (Page_Uptodate(page)) {
      set_bit(BH_Uptodate, &bh->b_state);
      continue; 
    }
    if (!buffer_uptodate(bh) &&
         (block_start < from || block_end > to)) {
      ll_rw_block(READ, 1, &bh);
      *wait_bh++=bh;
    }
  }
  /*
   * If we issued read requests - let them complete.
   */
  while(wait_bh > wait) {
    wait_on_buffer(*--wait_bh);
    err = -EIO;
    if (!buffer_uptodate(*wait_bh))
      goto out;
  }
  return 0;
out:
  return err;
}

   如前所述,虽然在文件系统层次上是以页面为单位缓冲的, 在设备层次上却是以记录块为单位缓冲的。所以,如果一个缓冲页面的内容是一致的,就意味着构成这个页面的所有记录块的内容都一致,反过来,如果一个缓冲页面不一致,则未必每个记录块都不一致。因此,要根据写入的位置和长度找到具体涉及的记录块,针对这些记录块做写入的准备。


   做些什么准备呢?简而言之就是使有关记录块缓冲区的内容与设备上相关记录块的内容相一致。 如果缓冲页面已经建立起对物理记录块的映射,则需要做的只是检查一下目录记录块的内容是否一致 (见第1607行利1608行),如果不一致就通过 ll_rw_block() 将设备上的记录块读到缓冲区中。由此可见,对文件的写操作实际上往往是 “写中有读” 、 “欲写先读” 。


   可是,如果缓冲页面是新的,尚未映射到物理记录块呢?那就比较复杂一些了,因为根据页面号、 页面大小、记录块大小计算所得的记录块号(见1585行)只是文件内部的逻辑块号,这是在假定文件的内容为连续的线性空间这么一个前提下计算出来的,而实际的记录块在设备上的位置则是动态地分配和回收的。另一方面,在设备层也根本没有文件的概念,而只能按设备上的记录块号读写。设备上的记录块号也是逻辑块号,与设备上的记录块位图相对应。而设备上的逻辑块号与物理记录块有着一一对应的关系,所以在文件层也可以认为是 “物理块号” 。总而言之,这里有一个从文件内的逻辑记录块号到设备上的记录块号之间的映射问题。缺少了对这种映射关系的描述,就无法根据文件内的逻辑块号在设备上找到相应的记录块。可想而知,不同的文件系统可能有不同的映射关系或过程,这就是要由作为参数传给 __block_prepare_write() 的函数指针 get_block 来完成这种映射的原因。对于 Ext2 文件系统这个函数是 ext2_get_block() ,在 fs/ext2/inode.c 中。

② ext2_get_block
// fs/ext2/inode.c
/*
 * Allocation strategy is simple: if we have to allocate something, we will
 * have to go the whole way to leaf. So let's do it before attaching anything
 * to tree, set linkage between the newborn blocks, write them if sync is
 * required, recheck the path, free and repeat if check fails, otherwise
 * set the last missing link (that will protect us from any truncate-generated
 * removals - all blocks on the path are immune now) and possibly force the
 * write on the parent block.
 * That has a nice additional property: no special recovery from the failed
 * allocations is needed - we simply release blocks and do not touch anything
 * reachable from inode.
 */

static int ext2_get_block(struct inode *inode, long iblock, struct buffer_head *bh_result, int create)
{
  int err = -EIO;
  int offsets[4];
  Indirect chain[4];
  Indirect *partial;
  unsigned long goal;
  int left;
  int depth = ext2_block_to_path(inode, iblock, offsets);

  if (depth == 0)
    goto out;

  lock_kernel();
reread:
  partial = ext2_get_branch(inode, depth, offsets, chain, &err);

  /* Simplest case - block found, no allocation needed */
  if (!partial) {
got_it:
    bh_result->b_dev = inode->i_dev;
    bh_result->b_blocknr = le32_to_cpu(chain[depth-1].key);
    bh_result->b_state |= (1UL << BH_Mapped);
    /* Clean up and exit */
    partial = chain+depth-1; /* the whole chain */
    goto cleanup;
  }

  /* Next simple case - plain lookup or failed read of indirect block */
  if (!create || err == -EIO) {
cleanup:
    while (partial > chain) {
      brelse(partial->bh);
      partial--;
    }
    unlock_kernel();
out:
    return err;
  }

  /*
   * Indirect block might be removed by truncate while we were
   * reading it. Handling of that case (forget what we've got and
   * reread) is taken out of the main path.
   */
  if (err == -EAGAIN)
    goto changed;

  if (ext2_find_goal(inode, iblock, chain, partial, &goal) < 0)
    goto changed;

  left = (chain + depth) - partial;
  err = ext2_alloc_branch(inode, left, goal,
          offsets+(partial-chain), partial);
  if (err)
    goto cleanup;

  if (ext2_splice_branch(inode, iblock, chain, partial, left) < 0)
    goto changed;

  bh_result->b_state |= (1UL << BH_New);
  goto got_it;

changed:
  while (partial > chain) {
    bforget(partial->bh);
    partial--;
  }
  goto reread;
}

   参数 iblock 表示所处理的记录块在文件中的逻辑块号,inode 则指向文件的 inode 结构;参数 create 表示是否需要创建。从 __block_prepare_write() 中传下的实际参数值为 1,所以我们在这里只关心 create 为 1 的情景。从文件内块号到设备上块号的映射,最简单最迅速的当然莫过于使用一个以文件内块号为下标的线性数组,并且将这个数组置于索引节点 inode 结构中。可是,那样就需要很大的数组,从而使索引节点和 inode 结构也变得很大,或者就得使用可变长度的索引节点而使文件系统的结构更加复杂。


   另一种方法是采用间接寻址,也就是将上述的数组分块放在设备上本来可用于存储数据的若干记录块中,而将这些记录块的块号放在索引节点和 inode 结构中。这些记录块虽然在设备上的数据区(而不是索引节点区)中,却并不构成文件本身的内容,而只是一些管理信息。由于索引节点(和 inode 结构)应该是固定大小的,所以当文件较大时还要将这种间接寻址的结构框架做成树状或链状,这样才能随着文件本身的大小而扩展其容量,显然,这种方法解决了容量的问题,但是降低了运行时的效率。


   基于这些考虑,从 Unix 早期就采用了一种折衷的方法,可以说是直接与间接相结合。其方法是把整个文件的记录块寻址分成几个部分来实现。第一部分是以文件内块号为下标的数组,这是采用直接映射的部分,对于较小的文件这一部分就够用了。由于根据文件内块号就可以在 inode 结构里的数组中直接找到相应的设备上块号,所以效率很高。至于比较大的文件,其开头那一部分记录块号也同样直接就可以找到,但是当文件的大小超出这一部分的容量时,超出的那一部分就要采用间接寻址了。 Ext2 文件系统的这一部分的大小为 12 个记录块,即数组的大小为 12。当记录块大小为 1K 字节时,相应的文件大小为 12K 字节。在 Ext2 文件系统的 ext2_inode_info 结构中,有个大小为 15 的整型数组 i_data[],其开头 12 个元素即用于此项目。当文件大小超过这一部分的容量时,该数组中的第 13 个元素指向一个记录块,这个记录块的内容也是一个整型数组,其中的每个元素都指向一个设备上记录块。 如果记录块大小为 1K 字节,则该数组的大小为 256,也就是说间接寻址的容量为 256 个记录块,即 256K 字节。这样,两个部分的总容量为 12K + 256K=268K 字节。可是,更大的文件还是容纳不下,所以超过此容量的部分要进一步采用双重(二层)间接在址。此时 inode 结构里 i_data[] 数组中的第 14 个元素指向另一个记录块,该记录块的内容也是个数组,但是每个元素都指向另一个记录块中的数组, 那才是文件内块号至设备上块号的映射表。这么一来,双重间接寻址部分的能力为 256X256=64K 个记录块,即 64M 字节。依此类推,数组 i_data[] 中的第 15 个元素用于三重(三层)间接导址,这一部分的容量可达 256X256X256=16M 个记录块,也就是 16G 字节,所以,对于 32 位结构的系统,当记录块大小为 1K 字节时,文件的最大容量为 16G+64M+256K+12K。如果设备的容量大于这个数值,就得采用更大的记录块大小了。图5.7是一个关于直接和间接映射的示意图。


   从严格意义上说,i_data[] 其实不能说是一个数组,因为它的元素并不都是同一类型的。但是,从另一个角度说,则这些元素毕竟都是长整数,都代表着设备上个记录块,只是这些记录块的用途不同而已。


   这里还要注意,在 inode 结构中有个成分名为 i_data,这是一个 address_space 数据结构。而作为 inode 结构一部分的 ext2_inode_info 结构中,也有个名为 i_data 的数组,实际上就是记录块映射表,二者毫无关系。从概念上说,inode 结构是设备上的索引节点即 ext2_inode 结构的对应物,但实际上 inode 结构中的很多内容并非来自 ext2_inode 结构。相比之下,ext2_inode_info 结构中的信息才是基本上与设备上的索引节点相对应的。例如,与ext2_inode_info 中的数组 i_data[] 相对应,在 ext2_inode 结构中也有个数组 i_block[],两个数组的大小也相同。而 ext2_inode_info 中的数组 i_data[] 之所以不能再大一些, 就是因为索引节点中的数组 i_block[] 只能这么大了。那么内存中的 inode 结构为什么与设备上的索引节点有相当大的不同呢?原因在于设备上索引节点的大小受到更多的限制,所以在索引节点中只能存储必需的信息,而且是相对静态的信息。而内存中的 inode 结构就不同了,它受的限制比较小,除了来自索引节点的必需信息外还可以用来保存一些为方便和提高运行效率所需的信息,还有一些运行时需要的更为动态的信息,如各种指针,以及为实现某些功能所需的信息,如 i_sock, i_pipe, i_wait 和 i_flock 等等。还应提醒读者,设备上的索引节点数量与设备的大小以及文件系统格式的设计有直接的关系, 设备上的每一个文件都有一个索引节点,但是内存中的 inode 结构则主要是缓冲性质的,实际上只有很小一部分文件在内存中建立并保持 inode 结构。

⑴ ext2_block_to_path
// fs/ext2/inode.c
static int ext2_block_to_path(struct inode *inode, long i_block, int offsets[4])
{
  int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb);
  int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
  const long direct_blocks = EXT2_NDIR_BLOCKS,
    indirect_blocks = ptrs,
    double_blocks = (1 << (ptrs_bits * 2));
  int n = 0;

  if (i_block < 0) {
    ext2_warning (inode->i_sb, "ext2_block_to_path", "block < 0");
  } else if (i_block < direct_blocks) {
    offsets[n++] = i_block;
  } else if ( (i_block -= direct_blocks) < indirect_blocks) {
    offsets[n++] = EXT2_IND_BLOCK;
    offsets[n++] = i_block;
  } else if ((i_block -= indirect_blocks) < double_blocks) {
    offsets[n++] = EXT2_DIND_BLOCK;
    offsets[n++] = i_block >> ptrs_bits;
    offsets[n++] = i_block & (ptrs - 1);
  } else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) {
    offsets[n++] = EXT2_TIND_BLOCK;
    offsets[n++] = i_block >> (ptrs_bits * 2);
    offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1);
    offsets[n++] = i_block & (ptrs - 1);
  } else {
    ext2_warning (inode->i_sb, "ext2_block_to_path", "block > big");
  }
  return n;
}

  在记录块大小为 1K 字节时,代码中的局部量 ptrs 赋值为 256,从向 indirect_blocks 也是 256。与 ptrs 相对应的 ptrs_bits 则为 8,因为 256 是由 1左移 8 位而成的。同样地, 二次间接的容量 double_blocks 就是由 1 左移 16 位,即 64K 。而二次间接的容量为由 1 左移 24 位,即 16M。


   除映射 “深度” 外,还要算出在每一层映射中使用的位移量,即数组中的下标,并将计算的结果放在一个数组 offset[] 中备用。例如,文件内块号 10 不需要间接映射,一步就能到位,所以返回值为 1, 并于 offsets[0] 中返回在第一个数组,即 i_data[] 中的位移 10。可是,假若文件内块号为 20,则返回值为 2,而 offset[0] 为 12, offset[1] 为 8。这样,就在数组 offset[] 中为各层映射提供了一条路线。数组的大小是 4,因为最多就是二重间接。参数 offset 实际上是一个指针,在C语言里数组名与指针是等价的。


   如果 ext2_block_to_path() 的返回值为 0 表示出了错,因为文件内块号与设备上块号之间至少也得映射一次。出错的原因可能是文件内块号太大或为负值,或是下面要讲到的冲突。否则,就进一步从磁盘上逐层读入用于间接映射的记录块,这是由 ext2_get_branch() 完成的。

⑵ ext2_get_branch
// fs/ext2/inode.c
/**
 *  ext2_get_branch - read the chain of indirect blocks leading to data
 *  @inode: inode in question
 *  @depth: depth of the chain (1 - direct pointer, etc.)
 *  @offsets: offsets of pointers in inode/indirect blocks
 *  @chain: place to store the result
 *  @err: here we store the error value
 *
 *  Function fills the array of triples <key, p, bh> and returns %NULL
 *  if everything went OK or the pointer to the last filled triple
 *  (incomplete one) otherwise. Upon the return chain[i].key contains
 *  the number of (i+1)-th block in the chain (as it is stored in memory,
 *  i.e. little-endian 32-bit), chain[i].p contains the address of that
 *  number (it points into struct inode for i==0 and into the bh->b_data
 *  for i>0) and chain[i].bh points to the buffer_head of i-th indirect
 *  block for i>0 and NULL for i==0. In other words, it holds the block
 *  numbers of the chain, addresses they were taken from (and where we can
 *  verify that chain did not change) and buffer_heads hosting these
 *  numbers.
 *
 *  Function stops when it stumbles upon zero pointer (absent block)
 *    (pointer to last triple returned, *@err == 0)
 *  or when it gets an IO error reading an indirect block
 *    (ditto, *@err == -EIO)
 *  or when it notices that chain had been changed while it was reading
 *    (ditto, *@err == -EAGAIN)
 *  or when it reads all @depth-1 indirect blocks successfully and finds
 *  the whole chain, all way to the data (returns %NULL, *err == 0).
 */
static inline Indirect *ext2_get_branch(struct inode *inode,
          int depth,
          int *offsets,
          Indirect chain[4],
          int *err)
{
  kdev_t dev = inode->i_dev;
  int size = inode->i_sb->s_blocksize;
  Indirect *p = chain;
  struct buffer_head *bh;

  *err = 0;
  /* i_data is not going away, no lock needed */
  add_chain (chain, NULL, inode->u.ext2_i.i_data + *offsets);
  if (!p->key)
    goto no_block;
  while (--depth) {
    bh = bread(dev, le32_to_cpu(p->key), size);
    if (!bh)
      goto failure;
    /* Reader: pointers */
    if (!verify_chain(chain, p))
      goto changed;
    add_chain(++p, bh, (u32*)bh->b_data + *++offsets);
    /* Reader: end */
    if (!p->key)
      goto no_block;
  }
  return NULL;

changed:
  *err = -EAGAIN;
  goto no_block;
failure:
  *err = -EIO;
no_block:
  return p;
}

   从设备上逐层读入用于间接映射的记录块时,每通过 bread() 读入一个记录块以后都要调用 verify_chain() 再检查一下映射链的有效性,实质上是检查各层映射表中有关的内容是否改变了(见代码中的条件 from->key == *from->p)。为什么有可能改变呢?这是因为从设备上读入一个记录块是费时间的操作,当前进程会进入睡眠而系统会调度其他进程运行。这样,就行可能发生冲突了。例如,被调度运行的进程可能会打开这个文件并加以截尾,即把文件原有的内容删除。所以,当因等待读入中间记录块而进入睡眠的进程恢复运行的时候,可能会发现原来有效的映射链已经变成无效了,此时 ext2_get_branch() 返回一个出错代码 -EAGAIN。当然,发生这种情况的概率是很小的,但是一个软件是否 “健壮” 就在于是否考虑到了所有的可能。至于 bread() ,那已是属于设备驱动的范畴,读者可参阅块设备驱动一章中的有关内容。


   这样,ext2_get_branch() 深化了 ext2_block_to_path() 所取得的结果,二者合在一起基本完成了从文件内块号到设备上块号的映射。


   从 ext2_get_branch() 返回的值有两种可能。首先,如果顺利完成了映射则返回值为 NULL。其次, 如果在某层上发现映射表内的相应表项为 0,则说明这个表项(记录块)原来并不存在,现在因为写操作而需要扩充文件的大小。此时返回指向该层 Indirect 结构的指针,表示映射在此 “断裂” 了。此外, 如果映射的过程中出了错,例如读记录块失败,则通过参数 err 返回一个出错代码。


   回到 ext2_get_block() 的代码中。如果顺利完成了映射,就把所得的结果填入作为参数传下来的缓冲区结构 bh_result 中,然后把映射过程中读入的缓冲区(用于间接映射)全都释放,就最后完成了记录块号的映射。


   可是,要是 ext2_get_branch() 返回了一个非 0 指针(代码中的局部量 partial ),那就说明映射在某一层上断裂了。根据映射的深度和断裂的位置(层次),这个记录块也许还只是个中间的、用于间接映射的记录块,也许就是最终的目标记录块。总之,在这种情况下,要在设备上为目标记录块以及可能 需要的中间记录块分配空间。

(4)generic_commit_write

    为写操作作好了准备以后,从缓冲区(缓冲页面)到设备上的记录块这条路就畅通了。这样才可以从用户空间把待写的内容复制过来。

// // fs/ext2/file.c
ssize_t
generic_file_write(struct file *file,const char *buf,size_t count,loff_t *ppos)
{
  // ...
    status = mapping->a_ops->prepare_write(file, page, offset, offset+bytes);
    if (status)
      goto unlock;
    kaddr = page_address(page);
    status = copy_from_user(kaddr+offset, buf, bytes);
    flush_dcache_page(page);
    if (status)
      goto fail_write;
    status = mapping->a_ops->commit_write(file, page, offset, offset+bytes);
  // ...
}

   如前所述,目标记录块的缓冲区在文件层是作为缓冲页面的部分而存在的,所以这是从用户空 间到缓冲页面的拷贝,具体通过 copy_from_user() 完成。这里 buf 指向用户空间的缓冲区,而(kaddr + offset)为缓冲页面中的起始地址,bytes 则为该页面中待拷贝的长度,这些都是在 while 循环的开头计算好了的。对于 i386 结构的处理器,flush_dcache_page() 是空操作。


   写入缓冲页面以后,还要把这些缓冲页面提交给内核线程 kflushd,这样写操作才算完成。至于 kflushd 是否来得及马上将这些记录块写回设备上,那是另一回事了。这个将缓冲页面提交给 kflushd 的操作也是因文件系统而异的,由具体文件系统通过其 address_space_operations 结构中的函数指针 commit_write 提供,对于 Ext2 文件系统,这个函数是 generic_commit_write() ,其代码在 fs/buffer.c 中。

// fs/buffer.c
int generic_commit_write(struct file *file, struct page *page,
    unsigned from, unsigned to)
{
  struct inode *inode = page->mapping->host;
  loff_t pos = ((loff_t)page->index << PAGE_CACHE_SHIFT) + to;
  __block_commit_write(inode,page,from,to);
  kunmap(page);
  if (pos > inode->i_size) {
    inode->i_size = pos;
    mark_inode_dirty(inode);
  }
  return 0;
}

static int __block_commit_write(struct inode *inode, struct page *page,
    unsigned from, unsigned to)
{
  unsigned block_start, block_end;
  int partial = 0, need_balance_dirty = 0;
  unsigned blocksize;
  struct buffer_head *bh, *head;

  blocksize = inode->i_sb->s_blocksize;

  for(bh = head = page->buffers, block_start = 0;
      bh != head || !block_start;
      block_start=block_end, bh = bh->b_this_page) {
    block_end = block_start + blocksize;
    if (block_end <= from || block_start >= to) {
      if (!buffer_uptodate(bh))
        partial = 1;
    } else {
      set_bit(BH_Uptodate, &bh->b_state);
      if (!atomic_set_buffer_dirty(bh)) {
        __mark_dirty(bh);
        buffer_insert_inode_queue(bh, inode);
        need_balance_dirty = 1;
      }
    }
  }

  if (need_balance_dirty)
    balance_dirty(bh->b_dev);
  /*
   * is this a partial write that happened to make all buffers
   * uptodate then we can optimize away a bogus readpage() for
   * the next read(). Here we 'discover' wether the page went
   * uptodate as a result of this (potentially partial) write.
   */
  if (!partial)
    SetPageUptodate(page);
  return 0;
}

    总结对一个缓冲页面的写文件操作,大致可以分为三个阶段。第一是准备阶段,第二是缓冲页面的写入阶段,最后是提交阶段。完成了对所涉及的所有页面的循环,整个写文件操作的主体 generic_file_write() 就告结束,并且 sys_write() 也随着结束了。

(5)导图

6.4 信号

To be continued

⇐ ⇒ ⇔ ⇆ ⇒ ⟺

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿

⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑿⒀⒁⒂⒃⒄⒅⒆⒇

➊➋➌➍➎➏➐➑➒➓⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴

⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵

ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ

ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ

🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩


123

image.png


目录
相关文章
|
2天前
|
存储 安全 Linux
探索Linux操作系统的心脏:内核
在这篇文章中,我们将深入探讨Linux操作系统的核心—内核。通过简单易懂的语言和比喻,我们会发现内核是如何像心脏一样为系统提供动力,处理数据,并保持一切顺畅运行。从文件系统的管理到进程调度,再到设备驱动,我们将一探究竟,看看内核是怎样支撑起整个操作系统的大厦。无论你是计算机新手还是资深用户,这篇文章都将带你领略Linux内核的魅力,让你对这台复杂机器的内部运作有一个清晰的认识。
12 3
|
12天前
|
缓存 安全 Unix
Linux 内核黑客不可靠指南【ChatGPT】
Linux 内核黑客不可靠指南【ChatGPT】
|
12天前
|
Linux 开发者
Linux内核贡献成熟度模型 【ChatGPT】
Linux内核贡献成熟度模型 【ChatGPT】
|
11天前
|
网络协议 Ubuntu Linux
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
|
11天前
|
Linux
用clang编译Linux内核
用clang编译Linux内核
|
12天前
|
Linux API C语言
Linux 内核补丁提交的清单 【ChatGPT】
Linux 内核补丁提交的清单 【ChatGPT】
|
12天前
|
安全 Linux 开发者
Linux内核管理风格 【ChatGPT】
Linux内核管理风格 【ChatGPT】
|
12天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
12天前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
11天前
|
Linux API 调度
关于在Linux内核中使用不同延迟/休眠机制 【ChatGPT】
关于在Linux内核中使用不同延迟/休眠机制 【ChatGPT】