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


目录
相关文章
|
11天前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
11天前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
12天前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
12天前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
15天前
|
负载均衡 算法 Linux
深入探索Linux内核调度机制:公平与效率的平衡####
本文旨在剖析Linux操作系统内核中的进程调度机制,特别是其如何通过CFS(完全公平调度器)算法实现多任务环境下资源分配的公平性与系统响应速度之间的微妙平衡。不同于传统摘要的概览性质,本文摘要将直接聚焦于CFS的核心原理、设计目标及面临的挑战,为读者揭开Linux高效调度的秘密。 ####
32 3
|
17天前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
31 6
|
17天前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。
|
18天前
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
27 2
|
18天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
43 1
|
18天前
|
算法 前端开发 Linux
深入理解Linux内核调度器:CFS与实时性的平衡####
本文旨在探讨Linux操作系统的核心组件之一——完全公平调度器(CFS)的工作原理,分析其在多任务处理环境中如何实现进程间的公平调度,并进一步讨论Linux对于实时性需求的支持策略。不同于传统摘要仅概述内容要点,本部分将简要预览CFS的设计哲学、核心算法以及它是如何通过红黑树数据结构来维护进程执行顺序,同时触及Linux内核为满足不同应用场景下的实时性要求而做出的权衡与优化。 ####