]> Git Repo - linux.git/commitdiff
Merge tag 'fscache-rewrite-20220111' of git://git.kernel.org/pub/scm/linux/kernel...
authorLinus Torvalds <[email protected]>
Wed, 12 Jan 2022 21:45:12 +0000 (13:45 -0800)
committerLinus Torvalds <[email protected]>
Wed, 12 Jan 2022 21:45:12 +0000 (13:45 -0800)
Pull fscache rewrite from David Howells:
 "This is a set of patches that rewrites the fscache driver and the
  cachefiles driver, significantly simplifying the code compared to
  what's upstream, removing the complex operation scheduling and object
  state machine in favour of something much smaller and simpler.

  The series is structured such that the first few patches disable
  fscache use by the network filesystems using it, remove the cachefiles
  driver entirely and as much of the fscache driver as can be got away
  with without causing build failures in the network filesystems.

  The patches after that recreate fscache and then cachefiles,
  attempting to add the pieces in a logical order. Finally, the
  filesystems are reenabled and then the very last patch changes the
  documentation.

  [!] Note: I have dropped the cifs patch for the moment, leaving local
      caching in cifs disabled. I've been having trouble getting that
      working. I think I have it done, but it needs more testing (there
      seem to be some test failures occurring with v5.16 also from
      xfstests), so I propose deferring that patch to the end of the
      merge window.

  WHY REWRITE?
  ============

  Fscache's operation scheduling API was intended to handle sequencing
  of cache operations, which were all required (where possible) to run
  asynchronously in parallel with the operations being done by the
  network filesystem, whilst allowing the cache to be brought online and
  offline and to interrupt service for invalidation.

  With the advent of the tmpfile capacity in the VFS, however, an
  opportunity arises to do invalidation much more simply, without having
  to wait for I/O that's actually in progress: Cachefiles can simply
  create a tmpfile, cut over the file pointer for the backing object
  attached to a cookie and abandon the in-progress I/O, dismissing it
  upon completion.

  Future work here would involve using Omar Sandoval's vfs_link() with
  AT_LINK_REPLACE[1] to allow an extant file to be displaced by a new
  hard link from a tmpfile as currently I have to unlink the old file
  first.

  These patches can also simplify the object state handling as I/O
  operations to the cache don't all have to be brought to a stop in
  order to invalidate a file. To that end, and with an eye on to writing
  a new backing cache model in the future, I've taken the opportunity to
  simplify the indexing structure.

  I've separated the index cookie concept from the file cookie concept
  by C type now. The former is now called a "volume cookie" (struct
  fscache_volume) and there is a container of file cookies. There are
  then just the two levels. All the index cookie levels are collapsed
  into a single volume cookie, and this has a single printable string as
  a key. For instance, an AFS volume would have a key of something like
  "afs,example.com,1000555", combining the filesystem name, cell name
  and volume ID. This is freeform, but must not have '/' chars in it.

  I've also eliminated all pointers back from fscache into the network
  filesystem. This required the duplication of a little bit of data in
  the cookie (cookie key, coherency data and file size), but it's not
  actually that much. This gets rid of problems with making sure we keep
  netfs data structures around so that the cache can access them.

  These patches mean that most of the code that was in the drivers
  before is simply gone and those drivers are now almost entirely new
  code. That being the case, there doesn't seem any particular reason to
  try and maintain bisectability across it. Further, there has to be a
  point in the middle where things are cut over as there's a single
  point everything has to go through (ie. /dev/cachefiles) and it can't
  be in use by two drivers at once.

  ISSUES YET OUTSTANDING
  ======================

  There are some issues still outstanding, unaddressed by this patchset,
  that will need fixing in future patchsets, but that don't stop this
  series from being usable:

  (1) The cachefiles driver needs to stop using the backing filesystem's
      metadata to store information about what parts of the cache are
      populated. This is not reliable with modern extent-based
      filesystems.

      Fixing this is deferred to a separate patchset as it involves
      negotiation with the network filesystem and the VM as to how much
      data to download to fulfil a read - which brings me on to (2)...

  (2) NFS (and CIFS with the dropped patch) do not take account of how
      the cache would like I/O to be structured to meet its granularity
      requirements. Previously, the cache used page granularity, which
      was fine as the network filesystems also dealt in page
      granularity, and the backing filesystem (ext4, xfs or whatever)
      did whatever it did out of sight. However, we now have folios to
      deal with and the cache will now have to store its own metadata to
      track its contents.

      The change I'm looking at making for cachefiles is to store
      content bitmaps in one or more xattrs and making a bit in the map
      correspond to something like a 256KiB block. However, the size of
      an xattr and the fact that they have to be read/updated in one go
      means that I'm looking at covering 1GiB of data per 512-byte map
      and storing each map in an xattr. Cachefiles has the potential to
      grow into a fully fledged filesystem of its very own if I'm not
      careful.

      However, I'm also looking at changing things even more radically
      and going to a different model of how the cache is arranged and
      managed - one that's more akin to the way, say, openafs does
      things - which brings me on to (3)...

  (3) The way cachefilesd does culling is very inefficient for large
      caches and it would be better to move it into the kernel if I can
      as cachefilesd has to keep asking the kernel if it can cull a
      file. Changing the way the backend works would allow this to be
      addressed.

  BITS THAT MAY BE CONTROVERSIAL
  ==============================

  There are some bits I've added that may be controversial:

  (1) I've provided a flag, S_KERNEL_FILE, that cachefiles uses to check
      if a files is already being used by some other kernel service
      (e.g. a duplicate cachefiles cache in the same directory) and
      reject it if it is. This isn't entirely necessary, but it helps
      prevent accidental data corruption.

      I don't want to use S_SWAPFILE as that has other effects, but
      quite possibly swapon() should set S_KERNEL_FILE too.

      Note that it doesn't prevent userspace from interfering, though
      perhaps it should. (I have made it prevent a marked directory from
      being rmdir-able).

  (2) Cachefiles wants to keep the backing file for a cookie open whilst
      we might need to write to it from network filesystem writeback.
      The problem is that the network filesystem unuses its cookie when
      its file is closed, and so we have nothing pinning the cachefiles
      file open and it will get closed automatically after a short time
      to avoid EMFILE/ENFILE problems.

      Reopening the cache file, however, is a problem if this is being
      done due to writeback triggered by exit(). Some filesystems will
      oops if we try to open a file in that context because they want to
      access current->fs or suchlike.

      To get around this, I added the following:

      (A) An inode flag, I_PINNING_FSCACHE_WB, to be set on a network
          filesystem inode to indicate that we have a usage count on the
          cookie caching that inode.

      (B) A flag in struct writeback_control, unpinned_fscache_wb, that
          is set when __writeback_single_inode() clears the last dirty
          page from i_pages - at which point it clears
          I_PINNING_FSCACHE_WB and sets this flag.

          This has to be done here so that clearing I_PINNING_FSCACHE_WB
          can be done atomically with the check of PAGECACHE_TAG_DIRTY
          that clears I_DIRTY_PAGES.

      (C) A function, fscache_set_page_dirty(), which if it is not set,
          sets I_PINNING_FSCACHE_WB and calls fscache_use_cookie() to
          pin the cache resources.

      (D) A function, fscache_unpin_writeback(), to be called by
          ->write_inode() to unuse the cookie.

      (E) A function, fscache_clear_inode_writeback(), to be called when
          the inode is evicted, before clear_inode() is called. This
          cleans up any lingering I_PINNING_FSCACHE_WB.

      The network filesystem can then use these tools to make sure that
      fscache_write_to_cache() can write locally modified data to the
      cache as well as to the server.

      For the future, I'm working on write helpers for netfs lib that
      should allow this facility to be removed by keeping track of the
      dirty regions separately - but that's incomplete at the moment and
      is also going to be affected by folios, one way or another, since
      it deals with pages"

Link: https://lore.kernel.org/all/[email protected]/
Tested-by: Dominique Martinet <[email protected]> # 9p
Tested-by: [email protected] # afs
Tested-by: Jeff Layton <[email protected]> # ceph
Tested-by: Dave Wysochanski <[email protected]> # nfs
Tested-by: Daire Byrne <[email protected]> # nfs
* tag 'fscache-rewrite-20220111' of git://git.kernel.org/pub/scm/linux/kernel/git/dhowells/linux-fs: (67 commits)
  9p, afs, ceph, nfs: Use current_is_kswapd() rather than gfpflags_allow_blocking()
  fscache: Add a tracepoint for cookie use/unuse
  fscache: Rewrite documentation
  ceph: add fscache writeback support
  ceph: conversion to new fscache API
  nfs: Implement cache I/O by accessing the cache directly
  nfs: Convert to new fscache volume/cookie API
  9p: Copy local writes to the cache when writing to the server
  9p: Use fscache indexing rewrite and reenable caching
  afs: Skip truncation on the server of data we haven't written yet
  afs: Copy local writes to the cache when writing to the server
  afs: Convert afs to use the new fscache API
  fscache, cachefiles: Display stat of culling events
  fscache, cachefiles: Display stats of no-space events
  cachefiles: Allow cachefiles to actually function
  fscache, cachefiles: Store the volume coherency data
  cachefiles: Implement the I/O routines
  cachefiles: Implement cookie resize for truncate
  cachefiles: Implement begin and end I/O operation
  cachefiles: Implement backing file wrangling
  ...

1  2 
fs/afs/file.c
fs/afs/super.c
fs/cachefiles/cache.c
fs/ceph/caps.c
fs/ceph/file.c
fs/fs-writeback.c
include/linux/fs.h

diff --combined fs/afs/file.c
index afe4b803f84b46d319e4885fdbc6ea8a1919cf40,5b98db127a1b9abf81c42f11581a3084ff38275a..720818a7c166f99e2e4a1849e90fef9b1cd67fdf
@@@ -14,6 -14,7 +14,7 @@@
  #include <linux/gfp.h>
  #include <linux/task_io_accounting_ops.h>
  #include <linux/mm.h>
+ #include <linux/swap.h>
  #include <linux/netfs.h>
  #include "internal.h"
  
@@@ -158,7 -159,9 +159,9 @@@ int afs_open(struct inode *inode, struc
  
        if (file->f_flags & O_TRUNC)
                set_bit(AFS_VNODE_NEW_CONTENT, &vnode->flags);
-       
+       fscache_use_cookie(afs_vnode_cache(vnode), file->f_mode & FMODE_WRITE);
        file->private_data = af;
        _leave(" = 0");
        return 0;
@@@ -177,8 -180,10 +180,10 @@@ error
   */
  int afs_release(struct inode *inode, struct file *file)
  {
+       struct afs_vnode_cache_aux aux;
        struct afs_vnode *vnode = AFS_FS_I(inode);
        struct afs_file *af = file->private_data;
+       loff_t i_size;
        int ret = 0;
  
        _enter("{%llx:%llu},", vnode->fid.vid, vnode->fid.vnode);
        file->private_data = NULL;
        if (af->wb)
                afs_put_wb_key(af->wb);
+       if ((file->f_mode & FMODE_WRITE)) {
+               i_size = i_size_read(&vnode->vfs_inode);
+               afs_set_cache_aux(vnode, &aux);
+               fscache_unuse_cookie(afs_vnode_cache(vnode), &aux, &i_size);
+       } else {
+               fscache_unuse_cookie(afs_vnode_cache(vnode), NULL, NULL);
+       }
        key_put(af->key);
        kfree(af);
        afs_prune_wb_keys(vnode);
@@@ -354,14 -368,19 +368,19 @@@ static bool afs_is_cache_enabled(struc
  {
        struct fscache_cookie *cookie = afs_vnode_cache(AFS_FS_I(inode));
  
-       return fscache_cookie_enabled(cookie) && !hlist_empty(&cookie->backing_objects);
+       return fscache_cookie_enabled(cookie) && cookie->cache_priv;
  }
  
  static int afs_begin_cache_operation(struct netfs_read_request *rreq)
  {
+ #ifdef CONFIG_AFS_FSCACHE
        struct afs_vnode *vnode = AFS_FS_I(rreq->inode);
  
-       return fscache_begin_read_operation(rreq, afs_vnode_cache(vnode));
+       return fscache_begin_read_operation(&rreq->cache_resources,
+                                           afs_vnode_cache(vnode));
+ #else
+       return -ENOBUFS;
+ #endif
  }
  
  static int afs_check_write_begin(struct file *file, loff_t pos, unsigned len,
@@@ -398,6 -417,12 +417,12 @@@ static void afs_readahead(struct readah
        netfs_readahead(ractl, &afs_req_ops, NULL);
  }
  
+ int afs_write_inode(struct inode *inode, struct writeback_control *wbc)
+ {
+       fscache_unpin_writeback(wbc, afs_vnode_cache(AFS_FS_I(inode)));
+       return 0;
+ }
  /*
   * Adjust the dirty region of the page on truncation or full invalidation,
   * getting rid of the markers altogether if the region is entirely invalidated.
@@@ -480,23 -505,24 +505,24 @@@ static void afs_invalidatepage(struct p
   * release a page and clean up its private state if it's not busy
   * - return true if the page can now be released, false if not
   */
- static int afs_releasepage(struct page *page, gfp_t gfp_flags)
+ static int afs_releasepage(struct page *page, gfp_t gfp)
  {
        struct folio *folio = page_folio(page);
        struct afs_vnode *vnode = AFS_FS_I(folio_inode(folio));
  
        _enter("{{%llx:%llu}[%lu],%lx},%x",
               vnode->fid.vid, vnode->fid.vnode, folio_index(folio), folio->flags,
-              gfp_flags);
+              gfp);
  
        /* deny if page is being written to the cache and the caller hasn't
         * elected to wait */
  #ifdef CONFIG_AFS_FSCACHE
        if (folio_test_fscache(folio)) {
-               if (!(gfp_flags & __GFP_DIRECT_RECLAIM) || !(gfp_flags & __GFP_FS))
+               if (current_is_kswapd() || !(gfp & __GFP_FS))
                        return false;
                folio_wait_fscache(folio);
        }
+       fscache_note_page_release(afs_vnode_cache(vnode));
  #endif
  
        if (folio_test_private(folio)) {
@@@ -514,9 -540,8 +540,9 @@@ static void afs_add_open_mmap(struct af
        if (atomic_inc_return(&vnode->cb_nr_mmap) == 1) {
                down_write(&vnode->volume->cell->fs_open_mmaps_lock);
  
 -              list_add_tail(&vnode->cb_mmap_link,
 -                            &vnode->volume->cell->fs_open_mmaps);
 +              if (list_empty(&vnode->cb_mmap_link))
 +                      list_add_tail(&vnode->cb_mmap_link,
 +                                    &vnode->volume->cell->fs_open_mmaps);
  
                up_write(&vnode->volume->cell->fs_open_mmaps_lock);
        }
diff --combined fs/afs/super.c
index 34c68724c98bec4070dd56de816c07ea78c2db99,af7cbd9949c5761440142a215403dc4e1996335b..5ec9fd97eccc8994387abba86b64bf34ba53f7eb
@@@ -55,6 -55,7 +55,7 @@@ int afs_net_id
  static const struct super_operations afs_super_ops = {
        .statfs         = afs_statfs,
        .alloc_inode    = afs_alloc_inode,
+       .write_inode    = afs_write_inode,
        .drop_inode     = afs_drop_inode,
        .destroy_inode  = afs_destroy_inode,
        .free_inode     = afs_free_inode,
@@@ -667,7 -668,6 +668,7 @@@ static void afs_i_init_once(void *_vnod
        INIT_LIST_HEAD(&vnode->pending_locks);
        INIT_LIST_HEAD(&vnode->granted_locks);
        INIT_DELAYED_WORK(&vnode->lock_work, afs_lock_work);
 +      INIT_LIST_HEAD(&vnode->cb_mmap_link);
        seqlock_init(&vnode->cb_lock);
  }
  
diff --combined fs/cachefiles/cache.c
index 0000000000000000000000000000000000000000,8095192863356b4471f6fb45df8c4fd2e763713e..ce4d4785003cb53de9b0f5aaf299a31c27b4c687
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,378 +1,378 @@@
 -      if (mnt_user_ns(path.mnt) != &init_user_ns) {
+ // SPDX-License-Identifier: GPL-2.0-or-later
+ /* Manage high-level VFS aspects of a cache.
+  *
+  * Copyright (C) 2007, 2021 Red Hat, Inc. All Rights Reserved.
+  * Written by David Howells ([email protected])
+  */
+ #include <linux/slab.h>
+ #include <linux/statfs.h>
+ #include <linux/namei.h>
+ #include "internal.h"
+ /*
+  * Bring a cache online.
+  */
+ int cachefiles_add_cache(struct cachefiles_cache *cache)
+ {
+       struct fscache_cache *cache_cookie;
+       struct path path;
+       struct kstatfs stats;
+       struct dentry *graveyard, *cachedir, *root;
+       const struct cred *saved_cred;
+       int ret;
+       _enter("");
+       cache_cookie = fscache_acquire_cache(cache->tag);
+       if (IS_ERR(cache_cookie))
+               return PTR_ERR(cache_cookie);
+       /* we want to work under the module's security ID */
+       ret = cachefiles_get_security_ID(cache);
+       if (ret < 0)
+               goto error_getsec;
+       cachefiles_begin_secure(cache, &saved_cred);
+       /* look up the directory at the root of the cache */
+       ret = kern_path(cache->rootdirname, LOOKUP_DIRECTORY, &path);
+       if (ret < 0)
+               goto error_open_root;
+       cache->mnt = path.mnt;
+       root = path.dentry;
+       ret = -EINVAL;
++      if (is_idmapped_mnt(path.mnt)) {
+               pr_warn("File cache on idmapped mounts not supported");
+               goto error_unsupported;
+       }
+       /* check parameters */
+       ret = -EOPNOTSUPP;
+       if (d_is_negative(root) ||
+           !d_backing_inode(root)->i_op->lookup ||
+           !d_backing_inode(root)->i_op->mkdir ||
+           !(d_backing_inode(root)->i_opflags & IOP_XATTR) ||
+           !root->d_sb->s_op->statfs ||
+           !root->d_sb->s_op->sync_fs ||
+           root->d_sb->s_blocksize > PAGE_SIZE)
+               goto error_unsupported;
+       ret = -EROFS;
+       if (sb_rdonly(root->d_sb))
+               goto error_unsupported;
+       /* determine the security of the on-disk cache as this governs
+        * security ID of files we create */
+       ret = cachefiles_determine_cache_security(cache, root, &saved_cred);
+       if (ret < 0)
+               goto error_unsupported;
+       /* get the cache size and blocksize */
+       ret = vfs_statfs(&path, &stats);
+       if (ret < 0)
+               goto error_unsupported;
+       ret = -ERANGE;
+       if (stats.f_bsize <= 0)
+               goto error_unsupported;
+       ret = -EOPNOTSUPP;
+       if (stats.f_bsize > PAGE_SIZE)
+               goto error_unsupported;
+       cache->bsize = stats.f_bsize;
+       cache->bshift = 0;
+       if (stats.f_bsize < PAGE_SIZE)
+               cache->bshift = PAGE_SHIFT - ilog2(stats.f_bsize);
+       _debug("blksize %u (shift %u)",
+              cache->bsize, cache->bshift);
+       _debug("size %llu, avail %llu",
+              (unsigned long long) stats.f_blocks,
+              (unsigned long long) stats.f_bavail);
+       /* set up caching limits */
+       do_div(stats.f_files, 100);
+       cache->fstop = stats.f_files * cache->fstop_percent;
+       cache->fcull = stats.f_files * cache->fcull_percent;
+       cache->frun  = stats.f_files * cache->frun_percent;
+       _debug("limits {%llu,%llu,%llu} files",
+              (unsigned long long) cache->frun,
+              (unsigned long long) cache->fcull,
+              (unsigned long long) cache->fstop);
+       stats.f_blocks >>= cache->bshift;
+       do_div(stats.f_blocks, 100);
+       cache->bstop = stats.f_blocks * cache->bstop_percent;
+       cache->bcull = stats.f_blocks * cache->bcull_percent;
+       cache->brun  = stats.f_blocks * cache->brun_percent;
+       _debug("limits {%llu,%llu,%llu} blocks",
+              (unsigned long long) cache->brun,
+              (unsigned long long) cache->bcull,
+              (unsigned long long) cache->bstop);
+       /* get the cache directory and check its type */
+       cachedir = cachefiles_get_directory(cache, root, "cache", NULL);
+       if (IS_ERR(cachedir)) {
+               ret = PTR_ERR(cachedir);
+               goto error_unsupported;
+       }
+       cache->store = cachedir;
+       /* get the graveyard directory */
+       graveyard = cachefiles_get_directory(cache, root, "graveyard", NULL);
+       if (IS_ERR(graveyard)) {
+               ret = PTR_ERR(graveyard);
+               goto error_unsupported;
+       }
+       cache->graveyard = graveyard;
+       cache->cache = cache_cookie;
+       ret = fscache_add_cache(cache_cookie, &cachefiles_cache_ops, cache);
+       if (ret < 0)
+               goto error_add_cache;
+       /* done */
+       set_bit(CACHEFILES_READY, &cache->flags);
+       dput(root);
+       pr_info("File cache on %s registered\n", cache_cookie->name);
+       /* check how much space the cache has */
+       cachefiles_has_space(cache, 0, 0, cachefiles_has_space_check);
+       cachefiles_end_secure(cache, saved_cred);
+       _leave(" = 0 [%px]", cache->cache);
+       return 0;
+ error_add_cache:
+       cachefiles_put_directory(cache->graveyard);
+       cache->graveyard = NULL;
+ error_unsupported:
+       cachefiles_put_directory(cache->store);
+       cache->store = NULL;
+       mntput(cache->mnt);
+       cache->mnt = NULL;
+       dput(root);
+ error_open_root:
+       cachefiles_end_secure(cache, saved_cred);
+ error_getsec:
+       fscache_relinquish_cache(cache_cookie);
+       cache->cache = NULL;
+       pr_err("Failed to register: %d\n", ret);
+       return ret;
+ }
+ /*
+  * See if we have space for a number of pages and/or a number of files in the
+  * cache
+  */
+ int cachefiles_has_space(struct cachefiles_cache *cache,
+                        unsigned fnr, unsigned bnr,
+                        enum cachefiles_has_space_for reason)
+ {
+       struct kstatfs stats;
+       u64 b_avail, b_writing;
+       int ret;
+       struct path path = {
+               .mnt    = cache->mnt,
+               .dentry = cache->mnt->mnt_root,
+       };
+       //_enter("{%llu,%llu,%llu,%llu,%llu,%llu},%u,%u",
+       //       (unsigned long long) cache->frun,
+       //       (unsigned long long) cache->fcull,
+       //       (unsigned long long) cache->fstop,
+       //       (unsigned long long) cache->brun,
+       //       (unsigned long long) cache->bcull,
+       //       (unsigned long long) cache->bstop,
+       //       fnr, bnr);
+       /* find out how many pages of blockdev are available */
+       memset(&stats, 0, sizeof(stats));
+       ret = vfs_statfs(&path, &stats);
+       if (ret < 0) {
+               trace_cachefiles_vfs_error(NULL, d_inode(path.dentry), ret,
+                                          cachefiles_trace_statfs_error);
+               if (ret == -EIO)
+                       cachefiles_io_error(cache, "statfs failed");
+               _leave(" = %d", ret);
+               return ret;
+       }
+       b_avail = stats.f_bavail >> cache->bshift;
+       b_writing = atomic_long_read(&cache->b_writing);
+       if (b_avail > b_writing)
+               b_avail -= b_writing;
+       else
+               b_avail = 0;
+       //_debug("avail %llu,%llu",
+       //       (unsigned long long)stats.f_ffree,
+       //       (unsigned long long)b_avail);
+       /* see if there is sufficient space */
+       if (stats.f_ffree > fnr)
+               stats.f_ffree -= fnr;
+       else
+               stats.f_ffree = 0;
+       if (b_avail > bnr)
+               b_avail -= bnr;
+       else
+               b_avail = 0;
+       ret = -ENOBUFS;
+       if (stats.f_ffree < cache->fstop ||
+           b_avail < cache->bstop)
+               goto stop_and_begin_cull;
+       ret = 0;
+       if (stats.f_ffree < cache->fcull ||
+           b_avail < cache->bcull)
+               goto begin_cull;
+       if (test_bit(CACHEFILES_CULLING, &cache->flags) &&
+           stats.f_ffree >= cache->frun &&
+           b_avail >= cache->brun &&
+           test_and_clear_bit(CACHEFILES_CULLING, &cache->flags)
+           ) {
+               _debug("cease culling");
+               cachefiles_state_changed(cache);
+       }
+       //_leave(" = 0");
+       return 0;
+ stop_and_begin_cull:
+       switch (reason) {
+       case cachefiles_has_space_for_write:
+               fscache_count_no_write_space();
+               break;
+       case cachefiles_has_space_for_create:
+               fscache_count_no_create_space();
+               break;
+       default:
+               break;
+       }
+ begin_cull:
+       if (!test_and_set_bit(CACHEFILES_CULLING, &cache->flags)) {
+               _debug("### CULL CACHE ###");
+               cachefiles_state_changed(cache);
+       }
+       _leave(" = %d", ret);
+       return ret;
+ }
+ /*
+  * Mark all the objects as being out of service and queue them all for cleanup.
+  */
+ static void cachefiles_withdraw_objects(struct cachefiles_cache *cache)
+ {
+       struct cachefiles_object *object;
+       unsigned int count = 0;
+       _enter("");
+       spin_lock(&cache->object_list_lock);
+       while (!list_empty(&cache->object_list)) {
+               object = list_first_entry(&cache->object_list,
+                                         struct cachefiles_object, cache_link);
+               cachefiles_see_object(object, cachefiles_obj_see_withdrawal);
+               list_del_init(&object->cache_link);
+               fscache_withdraw_cookie(object->cookie);
+               count++;
+               if ((count & 63) == 0) {
+                       spin_unlock(&cache->object_list_lock);
+                       cond_resched();
+                       spin_lock(&cache->object_list_lock);
+               }
+       }
+       spin_unlock(&cache->object_list_lock);
+       _leave(" [%u objs]", count);
+ }
+ /*
+  * Withdraw volumes.
+  */
+ static void cachefiles_withdraw_volumes(struct cachefiles_cache *cache)
+ {
+       _enter("");
+       for (;;) {
+               struct cachefiles_volume *volume = NULL;
+               spin_lock(&cache->object_list_lock);
+               if (!list_empty(&cache->volumes)) {
+                       volume = list_first_entry(&cache->volumes,
+                                                 struct cachefiles_volume, cache_link);
+                       list_del_init(&volume->cache_link);
+               }
+               spin_unlock(&cache->object_list_lock);
+               if (!volume)
+                       break;
+               cachefiles_withdraw_volume(volume);
+       }
+       _leave("");
+ }
+ /*
+  * Sync a cache to backing disk.
+  */
+ static void cachefiles_sync_cache(struct cachefiles_cache *cache)
+ {
+       const struct cred *saved_cred;
+       int ret;
+       _enter("%s", cache->cache->name);
+       /* make sure all pages pinned by operations on behalf of the netfs are
+        * written to disc */
+       cachefiles_begin_secure(cache, &saved_cred);
+       down_read(&cache->mnt->mnt_sb->s_umount);
+       ret = sync_filesystem(cache->mnt->mnt_sb);
+       up_read(&cache->mnt->mnt_sb->s_umount);
+       cachefiles_end_secure(cache, saved_cred);
+       if (ret == -EIO)
+               cachefiles_io_error(cache,
+                                   "Attempt to sync backing fs superblock returned error %d",
+                                   ret);
+ }
+ /*
+  * Withdraw cache objects.
+  */
+ void cachefiles_withdraw_cache(struct cachefiles_cache *cache)
+ {
+       struct fscache_cache *fscache = cache->cache;
+       pr_info("File cache on %s unregistering\n", fscache->name);
+       fscache_withdraw_cache(fscache);
+       /* we now have to destroy all the active objects pertaining to this
+        * cache - which we do by passing them off to thread pool to be
+        * disposed of */
+       cachefiles_withdraw_objects(cache);
+       fscache_wait_for_objects(fscache);
+       cachefiles_withdraw_volumes(cache);
+       cachefiles_sync_cache(cache);
+       cache->cache = NULL;
+       fscache_relinquish_cache(fscache);
+ }
diff --combined fs/ceph/caps.c
index c447fa2e2d1feb89ff9333a22203ad81fe41b436,0bc0e6c157df1b5d0b2357d400bad59a343de0f3..7d2c33cdbac677f51c1a0298875a6fa19dfc56a4
@@@ -1856,7 -1856,7 +1856,7 @@@ static int try_nonblocking_invalidate(s
        u32 invalidating_gen = ci->i_rdcache_gen;
  
        spin_unlock(&ci->i_ceph_lock);
-       ceph_fscache_invalidate(inode);
+       ceph_fscache_invalidate(inode, false);
        invalidate_mapping_pages(&inode->i_data, 0, -1);
        spin_lock(&ci->i_ceph_lock);
  
@@@ -2388,6 -2388,7 +2388,7 @@@ int ceph_write_inode(struct inode *inod
        int wait = (wbc->sync_mode == WB_SYNC_ALL && !wbc->for_sync);
  
        dout("write_inode %p wait=%d\n", inode, wait);
+       ceph_fscache_unpin_writeback(inode, wbc);
        if (wait) {
                dirty = try_flush_caps(inode, &flush_tid);
                if (dirty)
@@@ -4350,7 -4351,7 +4351,7 @@@ void ceph_get_fmode(struct ceph_inode_i
  {
        struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(ci->vfs_inode.i_sb);
        int bits = (fmode << 1) | 1;
 -      bool is_opened = false;
 +      bool already_opened = false;
        int i;
  
        if (count == 1)
  
        spin_lock(&ci->i_ceph_lock);
        for (i = 0; i < CEPH_FILE_MODE_BITS; i++) {
 -              if (bits & (1 << i))
 -                      ci->i_nr_by_mode[i] += count;
 -
                /*
 -               * If any of the mode ref is larger than 1,
 +               * If any of the mode ref is larger than 0,
                 * that means it has been already opened by
                 * others. Just skip checking the PIN ref.
                 */
 -              if (i && ci->i_nr_by_mode[i] > 1)
 -                      is_opened = true;
 +              if (i && ci->i_nr_by_mode[i])
 +                      already_opened = true;
 +
 +              if (bits & (1 << i))
 +                      ci->i_nr_by_mode[i] += count;
        }
  
 -      if (!is_opened)
 +      if (!already_opened)
                percpu_counter_inc(&mdsc->metric.opened_inodes);
        spin_unlock(&ci->i_ceph_lock);
  }
diff --combined fs/ceph/file.c
index c138e8126286cfe52996f982071003fbc95243a6,bf1017682d09d2501d25b77c745215cdf54b9678..9d9304e712d9a7bee7b1e88a0ea01a2d1c80eca7
@@@ -248,8 -248,7 +248,7 @@@ static int ceph_init_file(struct inode 
  
        switch (inode->i_mode & S_IFMT) {
        case S_IFREG:
-               ceph_fscache_register_inode_cookie(inode);
-               ceph_fscache_file_set_cookie(inode, file);
+               ceph_fscache_use_cookie(inode, file->f_mode & FMODE_WRITE);
                fallthrough;
        case S_IFDIR:
                ret = ceph_init_file_info(inode, file, fmode,
@@@ -605,25 -604,13 +604,25 @@@ static int ceph_finish_async_create(str
        in.cap.realm = cpu_to_le64(ci->i_snap_realm->ino);
        in.cap.flags = CEPH_CAP_FLAG_AUTH;
        in.ctime = in.mtime = in.atime = iinfo.btime;
 -      in.mode = cpu_to_le32((u32)mode);
        in.truncate_seq = cpu_to_le32(1);
        in.truncate_size = cpu_to_le64(-1ULL);
        in.xattr_version = cpu_to_le64(1);
        in.uid = cpu_to_le32(from_kuid(&init_user_ns, current_fsuid()));
 -      in.gid = cpu_to_le32(from_kgid(&init_user_ns, dir->i_mode & S_ISGID ?
 -                              dir->i_gid : current_fsgid()));
 +      if (dir->i_mode & S_ISGID) {
 +              in.gid = cpu_to_le32(from_kgid(&init_user_ns, dir->i_gid));
 +
 +              /* Directories always inherit the setgid bit. */
 +              if (S_ISDIR(mode))
 +                      mode |= S_ISGID;
 +              else if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP) &&
 +                       !in_group_p(dir->i_gid) &&
 +                       !capable_wrt_inode_uidgid(&init_user_ns, dir, CAP_FSETID))
 +                      mode &= ~S_ISGID;
 +      } else {
 +              in.gid = cpu_to_le32(from_kgid(&init_user_ns, current_fsgid()));
 +      }
 +      in.mode = cpu_to_le32((u32)mode);
 +
        in.nlink = cpu_to_le32(1);
        in.max_size = cpu_to_le64(lo->stripe_unit);
  
@@@ -822,6 -809,7 +821,7 @@@ int ceph_release(struct inode *inode, s
                dout("release inode %p regular file %p\n", inode, file);
                WARN_ON(!list_empty(&fi->rw_contexts));
  
+               ceph_fscache_unuse_cookie(inode, file->f_mode & FMODE_WRITE);
                ceph_put_fmode(ci, fi->fmode, 1);
  
                kmem_cache_free(ceph_file_cachep, fi);
@@@ -859,7 -847,7 +859,7 @@@ static ssize_t ceph_sync_read(struct ki
        ssize_t ret;
        u64 off = iocb->ki_pos;
        u64 len = iov_iter_count(to);
 -      u64 i_size;
 +      u64 i_size = i_size_read(inode);
  
        dout("sync_read on file %p %llu~%u %s\n", file, off, (unsigned)len,
             (file->f_flags & O_DIRECT) ? "O_DIRECT" : "");
@@@ -1218,7 -1206,11 +1218,11 @@@ ceph_direct_read_write(struct kiocb *io
             snapc, snapc ? snapc->seq : 0);
  
        if (write) {
-               int ret2 = invalidate_inode_pages2_range(inode->i_mapping,
+               int ret2;
+               ceph_fscache_invalidate(inode, true);
+               ret2 = invalidate_inode_pages2_range(inode->i_mapping,
                                        pos >> PAGE_SHIFT,
                                        (pos + count - 1) >> PAGE_SHIFT);
                if (ret2 < 0)
@@@ -1429,6 -1421,7 +1433,7 @@@ ceph_sync_write(struct kiocb *iocb, str
        if (ret < 0)
                return ret;
  
+       ceph_fscache_invalidate(inode, false);
        ret = invalidate_inode_pages2_range(inode->i_mapping,
                                            pos >> PAGE_SHIFT,
                                            (pos + count - 1) >> PAGE_SHIFT);
@@@ -2113,6 -2106,7 +2118,7 @@@ static long ceph_fallocate(struct file 
                goto unlock;
  
        filemap_invalidate_lock(inode->i_mapping);
+       ceph_fscache_invalidate(inode, false);
        ceph_zero_pagecache_range(inode, offset, length);
        ret = ceph_zero_objects(inode, offset, length);
  
@@@ -2437,6 -2431,7 +2443,7 @@@ static ssize_t __ceph_copy_file_range(s
                goto out_caps;
  
        /* Drop dst file cached pages */
+       ceph_fscache_invalidate(dst_inode, false);
        ret = invalidate_inode_pages2_range(dst_inode->i_mapping,
                                            dst_off >> PAGE_SHIFT,
                                            (dst_off + len) >> PAGE_SHIFT);
diff --combined fs/fs-writeback.c
index 4f680f848c8b5cab0d6b8f2bfcab88c23af89ddf,8294a60ce323acff44e1ea31cd83bfaccbfdaf64..f8d7fe6db989eb51c3ab481464b746c45ec1c0a9
@@@ -372,7 -372,7 +372,7 @@@ static bool inode_do_switch_wbs(struct 
  {
        struct address_space *mapping = inode->i_mapping;
        XA_STATE(xas, &mapping->i_pages, 0);
 -      struct page *page;
 +      struct folio *folio;
        bool switched = false;
  
        spin_lock(&inode->i_lock);
  
        /*
         * Count and transfer stats.  Note that PAGECACHE_TAG_DIRTY points
 -       * to possibly dirty pages while PAGECACHE_TAG_WRITEBACK points to
 -       * pages actually under writeback.
 +       * to possibly dirty folios while PAGECACHE_TAG_WRITEBACK points to
 +       * folios actually under writeback.
         */
 -      xas_for_each_marked(&xas, page, ULONG_MAX, PAGECACHE_TAG_DIRTY) {
 -              if (PageDirty(page)) {
 -                      dec_wb_stat(old_wb, WB_RECLAIMABLE);
 -                      inc_wb_stat(new_wb, WB_RECLAIMABLE);
 +      xas_for_each_marked(&xas, folio, ULONG_MAX, PAGECACHE_TAG_DIRTY) {
 +              if (folio_test_dirty(folio)) {
 +                      long nr = folio_nr_pages(folio);
 +                      wb_stat_mod(old_wb, WB_RECLAIMABLE, -nr);
 +                      wb_stat_mod(new_wb, WB_RECLAIMABLE, nr);
                }
        }
  
        xas_set(&xas, 0);
 -      xas_for_each_marked(&xas, page, ULONG_MAX, PAGECACHE_TAG_WRITEBACK) {
 -              WARN_ON_ONCE(!PageWriteback(page));
 -              dec_wb_stat(old_wb, WB_WRITEBACK);
 -              inc_wb_stat(new_wb, WB_WRITEBACK);
 +      xas_for_each_marked(&xas, folio, ULONG_MAX, PAGECACHE_TAG_WRITEBACK) {
 +              long nr = folio_nr_pages(folio);
 +              WARN_ON_ONCE(!folio_test_writeback(folio));
 +              wb_stat_mod(old_wb, WB_WRITEBACK, -nr);
 +              wb_stat_mod(new_wb, WB_WRITEBACK, nr);
        }
  
        if (mapping_tagged(mapping, PAGECACHE_TAG_WRITEBACK)) {
@@@ -1668,6 -1666,13 +1668,13 @@@ __writeback_single_inode(struct inode *
  
        if (mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
                inode->i_state |= I_DIRTY_PAGES;
+       else if (unlikely(inode->i_state & I_PINNING_FSCACHE_WB)) {
+               if (!(inode->i_state & I_DIRTY_PAGES)) {
+                       inode->i_state &= ~I_PINNING_FSCACHE_WB;
+                       wbc->unpinned_fscache_wb = true;
+                       dirty |= I_PINNING_FSCACHE_WB; /* Cause write_inode */
+               }
+       }
  
        spin_unlock(&inode->i_lock);
  
                if (ret == 0)
                        ret = err;
        }
+       wbc->unpinned_fscache_wb = false;
        trace_writeback_single_inode(inode, wbc, nr_to_write);
        return ret;
  }
diff --combined include/linux/fs.h
index 58e911cb3885c499e133cc3b76f47166b2448ade,bcf1ca43013956e100e4e0afedaf337d0a98d926..f5d3bf5b69a68b4205a0cab32a8aa4c56078c444
@@@ -41,7 -41,6 +41,7 @@@
  #include <linux/stddef.h>
  #include <linux/mount.h>
  #include <linux/cred.h>
 +#include <linux/mnt_idmapping.h>
  
  #include <asm/byteorder.h>
  #include <uapi/linux/fs.h>
@@@ -1600,11 -1599,6 +1600,11 @@@ struct super_block 
        struct list_head        s_inodes_wb;    /* writeback inodes */
  } __randomize_layout;
  
 +static inline struct user_namespace *i_user_ns(const struct inode *inode)
 +{
 +      return inode->i_sb->s_user_ns;
 +}
 +
  /* Helper functions so that in most cases filesystems will
   * not need to deal directly with kuid_t and kgid_t and can
   * instead deal with the raw numeric values that are stored
   */
  static inline uid_t i_uid_read(const struct inode *inode)
  {
 -      return from_kuid(inode->i_sb->s_user_ns, inode->i_uid);
 +      return from_kuid(i_user_ns(inode), inode->i_uid);
  }
  
  static inline gid_t i_gid_read(const struct inode *inode)
  {
 -      return from_kgid(inode->i_sb->s_user_ns, inode->i_gid);
 +      return from_kgid(i_user_ns(inode), inode->i_gid);
  }
  
  static inline void i_uid_write(struct inode *inode, uid_t uid)
  {
 -      inode->i_uid = make_kuid(inode->i_sb->s_user_ns, uid);
 +      inode->i_uid = make_kuid(i_user_ns(inode), uid);
  }
  
  static inline void i_gid_write(struct inode *inode, gid_t gid)
  {
 -      inode->i_gid = make_kgid(inode->i_sb->s_user_ns, gid);
 -}
 -
 -/**
 - * kuid_into_mnt - map a kuid down into a mnt_userns
 - * @mnt_userns: user namespace of the relevant mount
 - * @kuid: kuid to be mapped
 - *
 - * Return: @kuid mapped according to @mnt_userns.
 - * If @kuid has no mapping INVALID_UID is returned.
 - */
 -static inline kuid_t kuid_into_mnt(struct user_namespace *mnt_userns,
 -                                 kuid_t kuid)
 -{
 -      return make_kuid(mnt_userns, __kuid_val(kuid));
 -}
 -
 -/**
 - * kgid_into_mnt - map a kgid down into a mnt_userns
 - * @mnt_userns: user namespace of the relevant mount
 - * @kgid: kgid to be mapped
 - *
 - * Return: @kgid mapped according to @mnt_userns.
 - * If @kgid has no mapping INVALID_GID is returned.
 - */
 -static inline kgid_t kgid_into_mnt(struct user_namespace *mnt_userns,
 -                                 kgid_t kgid)
 -{
 -      return make_kgid(mnt_userns, __kgid_val(kgid));
 +      inode->i_gid = make_kgid(i_user_ns(inode), gid);
  }
  
  /**
  static inline kuid_t i_uid_into_mnt(struct user_namespace *mnt_userns,
                                    const struct inode *inode)
  {
 -      return kuid_into_mnt(mnt_userns, inode->i_uid);
 +      return mapped_kuid_fs(mnt_userns, i_user_ns(inode), inode->i_uid);
  }
  
  /**
  static inline kgid_t i_gid_into_mnt(struct user_namespace *mnt_userns,
                                    const struct inode *inode)
  {
 -      return kgid_into_mnt(mnt_userns, inode->i_gid);
 -}
 -
 -/**
 - * kuid_from_mnt - map a kuid up into a mnt_userns
 - * @mnt_userns: user namespace of the relevant mount
 - * @kuid: kuid to be mapped
 - *
 - * Return: @kuid mapped up according to @mnt_userns.
 - * If @kuid has no mapping INVALID_UID is returned.
 - */
 -static inline kuid_t kuid_from_mnt(struct user_namespace *mnt_userns,
 -                                 kuid_t kuid)
 -{
 -      return KUIDT_INIT(from_kuid(mnt_userns, kuid));
 -}
 -
 -/**
 - * kgid_from_mnt - map a kgid up into a mnt_userns
 - * @mnt_userns: user namespace of the relevant mount
 - * @kgid: kgid to be mapped
 - *
 - * Return: @kgid mapped up according to @mnt_userns.
 - * If @kgid has no mapping INVALID_GID is returned.
 - */
 -static inline kgid_t kgid_from_mnt(struct user_namespace *mnt_userns,
 -                                 kgid_t kgid)
 -{
 -      return KGIDT_INIT(from_kgid(mnt_userns, kgid));
 -}
 -
 -/**
 - * mapped_fsuid - return caller's fsuid mapped up into a mnt_userns
 - * @mnt_userns: user namespace of the relevant mount
 - *
 - * Use this helper to initialize a new vfs or filesystem object based on
 - * the caller's fsuid. A common example is initializing the i_uid field of
 - * a newly allocated inode triggered by a creation event such as mkdir or
 - * O_CREAT. Other examples include the allocation of quotas for a specific
 - * user.
 - *
 - * Return: the caller's current fsuid mapped up according to @mnt_userns.
 - */
 -static inline kuid_t mapped_fsuid(struct user_namespace *mnt_userns)
 -{
 -      return kuid_from_mnt(mnt_userns, current_fsuid());
 -}
 -
 -/**
 - * mapped_fsgid - return caller's fsgid mapped up into a mnt_userns
 - * @mnt_userns: user namespace of the relevant mount
 - *
 - * Use this helper to initialize a new vfs or filesystem object based on
 - * the caller's fsgid. A common example is initializing the i_gid field of
 - * a newly allocated inode triggered by a creation event such as mkdir or
 - * O_CREAT. Other examples include the allocation of quotas for a specific
 - * user.
 - *
 - * Return: the caller's current fsgid mapped up according to @mnt_userns.
 - */
 -static inline kgid_t mapped_fsgid(struct user_namespace *mnt_userns)
 -{
 -      return kgid_from_mnt(mnt_userns, current_fsgid());
 +      return mapped_kgid_fs(mnt_userns, i_user_ns(inode), inode->i_gid);
  }
  
  /**
  static inline void inode_fsuid_set(struct inode *inode,
                                   struct user_namespace *mnt_userns)
  {
 -      inode->i_uid = mapped_fsuid(mnt_userns);
 +      inode->i_uid = mapped_fsuid(mnt_userns, i_user_ns(inode));
  }
  
  /**
  static inline void inode_fsgid_set(struct inode *inode,
                                   struct user_namespace *mnt_userns)
  {
 -      inode->i_gid = mapped_fsgid(mnt_userns);
 +      inode->i_gid = mapped_fsgid(mnt_userns, i_user_ns(inode));
  }
  
  /**
  static inline bool fsuidgid_has_mapping(struct super_block *sb,
                                        struct user_namespace *mnt_userns)
  {
 -      struct user_namespace *s_user_ns = sb->s_user_ns;
 +      struct user_namespace *fs_userns = sb->s_user_ns;
 +      kuid_t kuid;
 +      kgid_t kgid;
  
 -      return kuid_has_mapping(s_user_ns, mapped_fsuid(mnt_userns)) &&
 -             kgid_has_mapping(s_user_ns, mapped_fsgid(mnt_userns));
 +      kuid = mapped_fsuid(mnt_userns, fs_userns);
 +      if (!uid_valid(kuid))
 +              return false;
 +      kgid = mapped_fsgid(mnt_userns, fs_userns);
 +      if (!gid_valid(kgid))
 +              return false;
 +      return kuid_has_mapping(fs_userns, kuid) &&
 +             kgid_has_mapping(fs_userns, kgid);
  }
  
  extern struct timespec64 current_time(struct inode *inode);
@@@ -2173,6 -2249,7 +2173,7 @@@ struct super_operations 
  #define S_ENCRYPTED   (1 << 14) /* Encrypted file (using fs/crypto/) */
  #define S_CASEFOLD    (1 << 15) /* Casefolded file */
  #define S_VERITY      (1 << 16) /* Verity file (using fs/verity/) */
+ #define S_KERNEL_FILE (1 << 17) /* File is in use by the kernel (eg. fs/cachefiles) */
  
  /*
   * Note that nosuid etc flags are inode-specific: setting some file-system
@@@ -2342,6 -2419,8 +2343,8 @@@ static inline void kiocb_clone(struct k
   *                    Used to detect that mark_inode_dirty() should not move
   *                    inode between dirty lists.
   *
+  * I_PINNING_FSCACHE_WB       Inode is pinning an fscache object for writeback.
+  *
   * Q: What is the difference between I_WILL_FREE and I_FREEING?
   */
  #define I_DIRTY_SYNC          (1 << 0)
  #define I_CREATING            (1 << 15)
  #define I_DONTCACHE           (1 << 16)
  #define I_SYNC_QUEUED         (1 << 17)
+ #define I_PINNING_FSCACHE_WB  (1 << 18)
  
  #define I_DIRTY_INODE (I_DIRTY_SYNC | I_DIRTY_DATASYNC)
  #define I_DIRTY (I_DIRTY_INODE | I_DIRTY_PAGES)
@@@ -2648,21 -2728,6 +2652,21 @@@ static inline struct user_namespace *fi
  {
        return mnt_user_ns(file->f_path.mnt);
  }
 +
 +/**
 + * is_idmapped_mnt - check whether a mount is mapped
 + * @mnt: the mount to check
 + *
 + * If @mnt has an idmapping attached different from the
 + * filesystem's idmapping then @mnt is mapped.
 + *
 + * Return: true if mount is mapped, false if not.
 + */
 +static inline bool is_idmapped_mnt(const struct vfsmount *mnt)
 +{
 +      return mnt_user_ns(mnt) != mnt->mnt_sb->s_user_ns;
 +}
 +
  extern long vfs_truncate(const struct path *, loff_t);
  int do_truncate(struct user_namespace *, struct dentry *, loff_t start,
                unsigned int time_attrs, struct file *filp);
@@@ -2786,6 -2851,8 +2790,6 @@@ static inline int filemap_fdatawait(str
  
  extern bool filemap_range_has_page(struct address_space *, loff_t lstart,
                                  loff_t lend);
 -extern bool filemap_range_needs_writeback(struct address_space *,
 -                                        loff_t lstart, loff_t lend);
  extern int filemap_write_and_wait_range(struct address_space *mapping,
                                        loff_t lstart, loff_t lend);
  extern int __filemap_fdatawrite_range(struct address_space *mapping,
This page took 0.118926 seconds and 4 git commands to generate.