본문 바로가기
Kernel

Page Cache: filemap_read

by hyeyoo 2022. 2. 9.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

띵언

이전 글

이전 글에서는 read() 시스템 호출에서 어떤 일이 일어나는지 알아봤다. read() 시스템 호출은 모드와 권한이 적절한지 확인한 후, 1) Direct IO를 수행하거나 2) filemap_read()로 페이지 캐시에서 데이터를 읽어온다는 것을 알아봤다.

 

VFS: read_iter() & write_iter()

원래는 페이지 캐시를 정리하려고 했는데 정리하다보니 read/write 매커니즘을 정리할 수밖에 없었다. 이 글에선 read_iter()가 어떻게 동작한는지 간단하게 알아본다. 관련 글 이 글은 가상 파일시스

hyeyoo.com

페이지 캐시란

디스크의 속도는 어마무시하게 느리다. HDD에서 지연시간이 밀리초 단위로 발생함을 생각했을 때, 중간에 캐시가 없다면 프로세서가 파일의 내용을 읽어서 처리하려면 매번 디스크에서 RAM으로 데이터를 복사하고, 그걸 CPU가 읽는걸 반복해야 한다. read()와 write()가 매번 수 밀리초씩 지연시간이 발생한다고 상상해보자. 너무나도 끔찍하다. 물론 SSD는 HDD보다 훨씬 빠르고, 4K 정도의 블록 단위로 봤을 땐 속도가 RAM과 견줄 정도긴 하지만 SSD에선 한 번에 블록 단위로만 읽을 수 있으므로 여전히 캐시가 필요하다. 아무튼 요약하면 페이지 캐시는 디스크의 내용을 메모리에 캐싱하는 역할을 한다.

캐싱은 메모리 계층구조에서 발생하는 자연법칙과도 같다. 페이지 캐시는 여러 면에서 CPU 캐시와도 닮았다. 캐싱을 하는 매커니즘이 하드웨어에 있냐 소프트웨어에 있냐 정도 차이가 아닐까.

큰 그림

페이지 캐시의 목적은 디스크의 내용을 캐시하는 것이다. 페이지 캐시의 작동 매커니즘을 단순화해서 살펴보자.

read()

읽기가 수행될 땐 매우 간단하다. 페이지 캐시 내에 파일의 내용이 존재하면 페이지 캐시로부터 데이터를 사용자 공간의 버퍼로 복사한다. 없으면 디스크에서 파일을 복사해서 페이지 캐시에 넣은 다음 페이지 캐시에서 사용자 공간의 버퍼로 복사한다.

readahead

어떤 프로세스가 큰 파일을 순차적으로 읽고 있다고 해보자. 프로세스가 요청할 때마다 새로운 페이지를 읽는다면 읽는 데에 지연 시간이 꽤 발생할 것이다. 이렇게 하는 대신 공간적 지역성을 고려해서 이번에 읽기를 요청한 부분의 뒷부분도 미리 읽기를 시작하면 순차적 읽기에서 지연시간을 줄일 수 있다.

readahead가 성능이 좋으려면 접근 패턴을 잘 알아야하는데, 접근 패턴이 무작위인지 순차적인지를 잘 구분할 수 있어야 한다. 리눅스도 이를 위한 매커니즘을 갖고있는데, readahead까지 정리하면 이 글이 너무 복잡해지므로 이 글에선 readahead 자체를 자세하게 다루지는 않는다.

자료구조

struct folio

최근 들어 커널에서 페이지로 접근하던 많은 것들이 folio로 변환되고 있다. folio는 아래 글에 간단하게 정리해놨으니 살펴보자. 이 글에선 페이지와 folio라는 용어를 혼용해서 사용한다.

 

Virtual Memory: Folio in 5.16

Introduction 저번 개발 주기때 간간이 보이길래 공부했었는데, 5.16에서 folio 패치셋이 드디어 머지되었다. 간단하게 요약해보자면 커널에서 메모리는 페이지 단위로 관리된다. 종종 여러 페이지를

hyeyoo.com

struct inode

inode는 파일시스템 내에서 파일의 정보를 나타내기 위해 사용하는 자료구조이다. 모든 종류의 파일 (regular, directory, special (device), pipe, socket, link, ... )은 고유한 inode 번호를 갖고 있으며 inode 번호로 각 파일을 식별할 수 있다.

struct address_space

디스크 상의 데이터를 메모리에 캐싱하려면 메모리 상의 페이지를 디스크 상의 섹터와 매핑할 방법이 필요하다. 물론 inode를 확장시키는 것도 방법 중에 하나지만, inode 뿐만 아니라 다양한 종류의 데이터를 캐싱할 수 있도록 하려고 struct address_space를 사용한다. 다르게 말하면 address_space는 페이지 캐시에서 하나의 파일을 나타낸다고 할 수 있다.

/**
 * struct address_space - Contents of a cacheable, mappable object.
 * @host: Owner, either the inode or the block_device.
 * @i_pages: Cached pages.
 * @invalidate_lock: Guards coherency between page cache contents and
 *   file offset->disk block mappings in the filesystem during invalidates.
 *   It is also used to block modification of page cache contents through
 *   memory mappings.
 * @gfp_mask: Memory allocation flags to use for allocating pages.
 * @i_mmap_writable: Number of VM_SHARED mappings.
 * @nr_thps: Number of THPs in the pagecache (non-shmem only).
 * @i_mmap: Tree of private and shared mappings.
 * @i_mmap_rwsem: Protects @i_mmap and @i_mmap_writable.
 * @nrpages: Number of page entries, protected by the i_pages lock.
 * @writeback_index: Writeback starts here.
 * @a_ops: Methods.
 * @flags: Error bits and flags (AS_*).
 * @wb_err: The most recent error which has occurred.
 * @private_lock: For use by the owner of the address_space.
 * @private_list: For use by the owner of the address_space.
 * @private_data: For use by the owner of the address_space.
 */

struct address_space {
        struct inode            *host;
        struct xarray           i_pages;
        struct rw_semaphore     invalidate_lock;
        gfp_t                   gfp_mask;
        atomic_t                i_mmap_writable;
#ifdef CONFIG_READ_ONLY_THP_FOR_FS
        /* number of thp, only for non-shmem files */
        atomic_t                nr_thps;
#endif
        struct rb_root_cached   i_mmap;
        struct rw_semaphore     i_mmap_rwsem;
        unsigned long           nrpages;
        pgoff_t                 writeback_index;
        const struct address_space_operations *a_ops;
        unsigned long           flags;
        errseq_t                wb_err;
        spinlock_t              private_lock;
        struct list_head        private_list;
        void                    *private_data;
} __attribute__((aligned(sizeof(long)))) __randomize_layout;

주요 필드를 일부 살펴보자.

host: 이 address_space의 소유자가 누구인지를 나타낸다. inode또는 block_device이다.

i_pages: 이 address_space에 속한 페이지들의 xarray다. xarray는 Matthew WilCox가 2018년에 추가한 radiox tree에 대한 인터페이스이다.

i_mmap: 이 address_space에 대한 private & shared 매핑을 priority search tree로 구현한 것이다. 구현은 이 논문에 기반한다고 한다. 왜 하나의 address_space에 대해서 priority search tree와 radix tree 두 가지를 모두 관리해야하는지는 모르겠다.

gfp_mask: 페이지를 할당할 때 사용할 gfp 마스크

writeback_index: writeback을 시작할 인덱스

a_ops: 이 address_space에 대한 operations을 정의한 address_space_operations에 대한 포인터이다. 이 address_space의 페이지들에 대해 읽기, 쓰기, 동기화 등등의 작업을 처리한다.

struct address_space_operations

struct address_space_operations {
        int (*writepage)(struct page *page, struct writeback_control *wbc);
        int (*readpage)(struct file *, struct page *);

        /* Write back some dirty pages from this mapping. */
        int (*writepages)(struct address_space *, struct writeback_control *);

        /* Set a page dirty.  Return true if this dirtied it */
        int (*set_page_dirty)(struct page *page);

        /*
         * Reads in the requested pages. Unlike ->readpage(), this is
         * PURELY used for read-ahead!.
         */
        int (*readpages)(struct file *filp, struct address_space *mapping,
                        struct list_head *pages, unsigned nr_pages);
        void (*readahead)(struct readahead_control *);

        int (*write_begin)(struct file *, struct address_space *mapping,
                                loff_t pos, unsigned len, unsigned flags,
                                struct page **pagep, void **fsdata);
        int (*write_end)(struct file *, struct address_space *mapping,
                                loff_t pos, unsigned len, unsigned copied,
                                struct page *page, void *fsdata);

        /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
        sector_t (*bmap)(struct address_space *, sector_t);
        void (*invalidatepage) (struct page *, unsigned int, unsigned int);
        int (*releasepage) (struct page *, gfp_t);
        void (*freepage)(struct page *);
        ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
        /*
         * migrate the contents of a page to the specified target. If
         * migrate_mode is MIGRATE_ASYNC, it must not block.
         */
        int (*migratepage) (struct address_space *,
                        struct page *, struct page *, enum migrate_mode);
        bool (*isolate_page)(struct page *, isolate_mode_t);
        void (*putback_page)(struct page *);
        int (*launder_page) (struct page *);
        int (*is_partially_uptodate) (struct page *, unsigned long,
                                        unsigned long);
        void (*is_dirty_writeback) (struct page *, bool *, bool *);
        int (*error_remove_page)(struct address_space *, struct page *);
                /* swapfile support */
        int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
                                sector_t *span);
        void (*swap_deactivate)(struct file *file);
};

struct folio_batch

얼마 전까진 mm/filemap.c에서 pagevec라는 구조체를 사용했는데... 최근에 folio로 변환하고 있는것 같다. folio_batch가 필요한 이유는, read_iter()를 수행하면서 address_space 상의 여러 페이지에 접근해야 하는데, 하나하나 접근할 때마다 락을 사용하면 lock contention이 크기 때문에 각각의 페이지(folio)의 reference count를 증가시킨 다음 folio_batch에 넣어두고 사용하기 위함이다. 이렇게 하면 folio_batch에 넣어둔 페이지에 접근하는 동안에는 누군가 해당 페이지를 캐시에서 제거할 수 없다.

/**
 * struct folio_batch - A collection of folios.
 *
 * The folio_batch is used to amortise the cost of retrieving and
 * operating on a set of folios.  The order of folios in the batch may be
 * significant (eg delete_from_page_cache_batch()).  Some users of the
 * folio_batch store "exceptional" entries in it which can be removed
 * by calling folio_batch_remove_exceptionals().
 */
struct folio_batch {
        unsigned char nr; 
        bool percpu_pvec_drained;
        struct folio *folios[PAGEVEC_SIZE];
};

코드를 읽어보자

filemap_read()

/**
 * filemap_read - Read data from the page cache.
 * @iocb: The iocb to read.
 * @iter: Destination for the data.
 * @already_read: Number of bytes already read by the caller.
 *
 * Copies data from the page cache.  If the data is not currently present,
 * uses the readahead and readpage address_space operations to fetch it.
 *
 * Return: Total number of bytes copied, including those already read by
 * the caller.  If an error happens before any bytes are copied, returns
 * a negative error number.
 */

먼저 주석을 읽어보자. filemap_read는 iocb (읽을 파일에 대한 정보), iter (복사할 버퍼에 대한 정보), already_read (함수의 호출자가 파일 상에서 얼마나 읽었는지)를 인자로 넘긴다.

filemap_read()는 페이지 캐시에서 데이터를 복사하는데 데이터가 캐시에 없다면 a_ops->readpage()로 데이터를 읽어오고, a_ops->readahead()로 readahead를 수행한다고 한다. 그리고 반환 값으로는 (already_read + filemap_read()에서 읽은 바이트 수)를 반환한다. 

ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
                ssize_t already_read)
{
        struct file *filp = iocb->ki_filp;
        struct file_ra_state *ra = &filp->f_ra;
        struct address_space *mapping = filp->f_mapping;
        struct inode *inode = mapping->host;
        struct folio_batch fbatch;
        int i, error = 0; 
        bool writably_mapped;
        loff_t isize, end_offset;

        if (unlikely(iocb->ki_pos >= inode->i_sb->s_maxbytes))
                return 0;
        if (unlikely(!iov_iter_count(iter)))
                return 0;

        iov_iter_truncate(iter, inode->i_sb->s_maxbytes);
        folio_batch_init(&fbatch);

처음엔 딱히 하는게 없는데, iov_iter_count()가 0이거나, superblock 상의 max file size보다 ki_pos가 크면 리턴해버린다. 그리고 folio_batch를 초기화하고 iter에서 아무리 크게 파일을 읽으려고 해도 max file size보다 크면 넘는 부분은 truncate한다.

        do {
                cond_resched();

                /*   
                 * If we've already successfully copied some data, then we
                 * can no longer safely return -EIOCBQUEUED. Hence mark
                 * an async read NOWAIT at that point.
                 */
                if ((iocb->ki_flags & IOCB_WAITQ) && already_read)
                        iocb->ki_flags |= IOCB_NOWAIT;
                        
                if (unlikely(iocb->ki_pos >= i_size_read(inode)))
                        break;

                error = filemap_get_pages(iocb, iter, &fbatch);
                if (error < 0) 
                        break;

아까 "페이지 캐시에서 페이지를 가져오고 없으면 디스크에서 읽어온다"고 했는데, 그 부분을 filemap_get_pages()에서 담당한다. filemap_get_pages()는 address_space->i_pages에 있는 페이지들을 folio batch에 담아준다.

                /*
                 * i_size must be checked after we know the pages are Uptodate.
                 *
                 * Checking i_size after the check allows us to calculate
                 * the correct value for "nr", which means the zero-filled
                 * part of the page is not copied back to userspace (unless
                 * another truncate extends the file - this is desired though).
                 */
                isize = i_size_read(inode);
                if (unlikely(iocb->ki_pos >= isize))
                        goto put_folios;
                end_offset = min_t(loff_t, isize, iocb->ki_pos + iter->count);

여기선 페이지를 복사할 때 사용할 end_offset을 계산한다. end_offset이 파일의 크기보다 크면 안되므로 넘는 부분은 잘잘라준다.

                /*
                 * Once we start copying data, we don't want to be touching any
                 * cachelines that might be contended:
                 */
                writably_mapped = mapping_writably_mapped(mapping);

mapping_writably_mapped()는 mapping (address_space)상에 쓰시가 가능하게 매핑된 페이지가 있는지를 확인한다. 사용자 프로세스가 페이지 캐시에 쓰기가 가능한 경우에는 D-cache aliasing 문제가 발생할 수 있어서, 복사하기 전에 flush_dcache_folio()를 호출해야 한다.

                /*
                 * When a sequential read accesses a page several times, only
                 * mark it as accessed the first time.
                 */
                if (iocb->ki_pos >> PAGE_SHIFT !=
                    ra->prev_pos >> PAGE_SHIFT)
                        folio_mark_accessed(fbatch.folios[0]);

나중에 메모리가 부족해지면 커널은 LRU (Least Recently Used) 방식으로 페이지 캐시를 포함해서 커널의 페이지들을 reclaim한다. (물론 리눅스는 classic한 LRU로 구현되어있진 않지만) 근데 어떤 페이지가 자주 접근되고, 어떤 페이지가 가끔 접근되었는지 알려면 어디선가는 페이지에 접근했다는 걸 표시해야 한다. 이때 사용하는 함수가 folio_mark_accessed()이다.

일단 위 코드를 설명해보면, ra는 이 파일에 대해서 이전에 어디까지 읽었는지에 대한 정보를 갖고 있다. (readahead에 대한 정보이다.) 그런데 예를 들어 4K 페이지를 read()로 512바이트씩 8번 읽었다면 이 페이지는 8번 접근한게 아니라 1번 접근했다고 기록하는게 맞다. (읽는 크기에 따라서 페이지의 접근 빈도를 다르게 계산하면 LRU가 부정확해진다.) 따라서 위 코드는 이전에 읽었던 페이지와 현재 페이지가 같은 경우에는 folio_mark_accessed()를 한 번만 호출하는 코드이다.

                for (i = 0; i < folio_batch_count(&fbatch); i++) {
                        struct folio *folio = fbatch.folios[i];
                        size_t fsize = folio_size(folio);
                        size_t offset = iocb->ki_pos & (fsize - 1);
                        size_t bytes = min_t(loff_t, end_offset - iocb->ki_pos,
                                             fsize - offset);
                        size_t copied;

                        if (end_offset < folio_pos(folio))
                                break;
                        if (i > 0)
                                folio_mark_accessed(folio);

이제 여기서부턴 for문으로 folio batch에 있는 folio(페이지)들을 순회하면서 folio_mark_accessed()를 호출하고 복사해주는 코드가 나온다.

                        /*
                         * If users can be writing to this folio using arbitrary
                         * virtual addresses, take care of potential aliasing
                         * before reading the folio on the kernel side.
                         */
                        if (writably_mapped)
                                flush_dcache_folio(folio);

아까 이 address_space에 writable하게 매핑된 페이지가 있는지 확인했는데, 그런 경우에는 D-cache aliasing 문제를 피하기 위해서 flush_dcache_folio()를 호출해준다.

                        copied = copy_folio_to_iter(folio, offset, bytes, iter);

                        already_read += copied;
                        iocb->ki_pos += copied;
                        ra->prev_pos = iocb->ki_pos;

                        if (copied < bytes) {
                                error = -EFAULT;
                                break;
                        }
                }

flush_dcache_folio()를 한 후에는 copy_folio_to_iter()로 페이지 캐시에서 가져온 folio를 iter가 나타내는 사용자 공간의 버퍼로 복사한다. 그 후에는 iocb->ki_pos, ra->prev_pos, already_read 등등 지금까지 읽은 오프셋과, 지금까지 얼마나 읽었는지를 갱신한다.

put_folios:
                for (i = 0; i < folio_batch_count(&fbatch); i++)
                        folio_put(fbatch.folios[i]);
                folio_batch_init(&fbatch);
        } while (iov_iter_count(iter) && iocb->ki_pos < isize && !error);

        file_accessed(filp);

        return already_read ? already_read : error;
}
EXPORT_SYMBOL_GPL(filemap_read);

이제 복사도 다 했으므로 folio_batch에 가져왔던 folio들을 모두 folio_put()으로 reference count를 줄여주고 already_read를 리턴한다.

filemap_get_pages()

위에서 살펴봤듯 이 함수의 목적은 iocb, iter를 바탕으로 페이지 캐시에서 읽을 folio (페이지)들을 fbatch에 담아오기 위함이다.

static int filemap_get_pages(struct kiocb *iocb, struct iov_iter *iter,
                struct folio_batch *fbatch)
{
        struct file *filp = iocb->ki_filp;
        struct address_space *mapping = filp->f_mapping;
        struct file_ra_state *ra = &filp->f_ra;
        pgoff_t index = iocb->ki_pos >> PAGE_SHIFT;
        pgoff_t last_index;
        struct folio *folio;
        int err = 0;
        last_index = DIV_ROUND_UP(iocb->ki_pos + iter->count, PAGE_SIZE);
retry:
        if (fatal_signal_pending(current))
                return -EINTR;
        filemap_get_read_batch(mapping, index, last_index, fbatch);
        if (!folio_batch_count(fbatch)) {
                if (iocb->ki_flags & IOCB_NOIO)
                        return -EAGAIN;
                page_cache_sync_readahead(mapping, ra, filp, index,
                                last_index - index);
                filemap_get_read_batch(mapping, index, last_index, fbatch);
        }

일단 filemap_get_pages()에서는 filemap_get_read_batch()를 호출해 mapping->i_pages를 순회해서 현재 address_space상에 존재하는 페이지들 중, 읽어야할 페이지들을 fbatch에 담아온다.

그런데 읽어야할 페이지가 하나도 mapping에 없다면 (!folio_batch_count(fbatch)) page_cache_sync_readahead()로 readahead를 수행한 후에 filemap_get_read_batch()로 다시 fbatch에 페이지들을 담는다. 여기서 하는 readahead는 읽으려고 하는 오프셋도 범위에 포함된다.

        if (!folio_batch_count(fbatch)) {
                if (iocb->ki_flags & (IOCB_NOWAIT | IOCB_WAITQ))
                        return -EAGAIN;
                err = filemap_create_folio(filp, mapping,
                                iocb->ki_pos >> PAGE_SHIFT, fbatch);
                if (err == AOP_TRUNCATED_PAGE)
                        goto retry;
                return err; 
        }

위에서 한 readahead를 했음에도 fbatch가 비어있는 경우가 있다면 (이런 경우가 왜 있는지 잘 모르겠다. 아시는 분은 댓글로 알려주세요...!) filemap_create_folio()로 페이지 캐시에 folio를 추가한다. 비슷하게 filemap_add_folio() 함수도 있는데 둘다 뒷 부분에서 살펴보자.

        folio = fbatch->folios[folio_batch_count(fbatch) - 1];
        if (folio_test_readahead(folio)) {
                err = filemap_readahead(iocb, filp, mapping, folio, last_index);
                if (err)
                        goto err; 
        }

이 함수에서 읽은 folio들 중, 마지막으로 읽은 folio에 PG_readahead 플래그가 켜져있다면, filemap_readahead()로 비동기 readahead를 수행한다.

        if (!folio_test_uptodate(folio)) {
                if ((iocb->ki_flags & IOCB_WAITQ) &&
                    folio_batch_count(fbatch) > 1) 
                        iocb->ki_flags |= IOCB_NOWAIT;
                err = filemap_update_page(iocb, mapping, iter, folio);
                if (err)
                        goto err;
        }

페이지 캐시에서 가져온 페이지가 최신 상태가 아닌 경우 (디스크가 더 최신인 경우) filemap_update_page()로 페이지 캐시에 존재하는 페이지를 디스크에서 다시 읽어와서 갱신한다. 페이지 캐시를 통해서 접근하는 파일이 어떤 경우에 디스크가 더 최신이 되는지는 잘 모르겠다. (이것도 아시는 분이 계시면 알려주세요..!)

        return 0;
err:
        if (err < 0)
                folio_put(folio);
        if (likely(--fbatch->nr))
                return 0;
        if (err == AOP_TRUNCATED_PAGE)
                goto retry;
        return err;
}

 

filemap_add_folio()

이 함수는 folio를 페이지 캐시에 추가하는 함수이다.

int filemap_add_folio(struct address_space *mapping, struct folio *folio,
                                pgoff_t index, gfp_t gfp) 
{
        void *shadow = NULL;
        int ret; 

        __folio_set_locked(folio);
        ret = __filemap_add_folio(mapping, folio, index, gfp, &shadow);
        if (unlikely(ret))
                __folio_clear_locked(folio);

우선 __folio_set_locked()로 folio를 잠근다. 그 다음 __filemap_add_folio()로 folio를 mapping->i_pages에 추가한다.

        else {
                /*
                 * The folio might have been evicted from cache only
                 * recently, in which case it should be activated like
                 * any other repeatedly accessed folio.
                 * The exception is folios getting rewritten; evicting other
                 * data from the working set, only to cache data that will
                 * get overwritten with something else, is a waste of memory.
                 */
                WARN_ON_ONCE(folio_test_active(folio));
                if (!(gfp & __GFP_WRITE) && shadow)
                        workingset_refault(folio, shadow);
                folio_add_lru(folio);
        }
        return ret; 
}
EXPORT_SYMBOL_GPL(filemap_add_folio);

__filemap_add_folio()에 성공한 경우에는 folio_add_lru()로 LRU 리스트에 추가한다. LRU 리스트는 노드별로(struct pglist_data) 관리했다가 나중에 메모리가 부족할 때 reclamation 로직에서 사용한다.

folio_add_lru()를 호출한다고 매번 노드별 LRU 리스트에 추가하는 건 아니고, per-cpu 인터페이스로 접근하는 pagevec 타입의 lru_pvecs라는 전역 변수가 있는데

/*
 * The following struct pagevec are grouped together because they are protected
 * by disabling preemption (and interrupts remain enabled).
 */
struct lru_pvecs {
        local_lock_t lock;
        struct pagevec lru_add;
        struct pagevec lru_deactivate_file;
        struct pagevec lru_deactivate;
        struct pagevec lru_lazyfree;
#ifdef CONFIG_SMP
        struct pagevec activate_page;
#endif
};
static DEFINE_PER_CPU(struct lru_pvecs, lru_pvecs) = {
        .lock = INIT_LOCAL_LOCK(lock),
};

이 lru_pvecs에서 필요한 pagevec을 선택한 후, 그 pagevec에 넣어둔 다음 필요에 따라 pagevec을 비워서 node마다 존재하는 lruvec에 넣어준다.

typedef struct pglist_data {
        [ ... ]
        /*
         * NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
         *
         * Use mem_cgroup_lruvec() to look up lruvecs.
         */
        struct lruvec           __lruvec;

        unsigned long           flags;
        [ ... ]
} pg_data_t;

filemap_create_folio()

filemap_add_folio()가 이미 만들어진 folio를 페이지 캐시에 추가하는 함수라면, 이 함수는 folio의 생성까지 해주는 함수이다.

static int filemap_create_folio(struct file *file,
                struct address_space *mapping, pgoff_t index,
                struct folio_batch *fbatch)
{
        struct folio *folio;
        int error;

        folio = filemap_alloc_folio(mapping_gfp_mask(mapping), 0);
        if (!folio)
                return -ENOMEM;

folio를 할당한다.

        /*
         * Protect against truncate / hole punch. Grabbing invalidate_lock
         * here assures we cannot instantiate and bring uptodate new
         * pagecache folios after evicting page cache during truncate
         * and before actually freeing blocks.  Note that we could
         * release invalidate_lock after inserting the folio into
         * the page cache as the locked folio would then be enough to
         * synchronize with hole punching. But there are code paths
         * such as filemap_update_page() filling in partially uptodate
         * pages or ->readpages() that need to hold invalidate_lock
         * while mapping blocks for IO so let's hold the lock here as
         * well to keep locking rules simple.
         */
        filemap_invalidate_lock_shared(mapping);

address_space마다 invalidate_lock이라는 락이 있는데, 이 락이 왜 여기서 필요한지는 모르겠다. 캐시에서 페이지를 eviction하는 로직을 분석해봐야 이해할 수 있을듯 하다.

        error = filemap_add_folio(mapping, folio, index,
                        mapping_gfp_constraint(mapping, GFP_KERNEL));
        if (error == -EEXIST)
                error = AOP_TRUNCATED_PAGE;
        if (error)
                goto error;

아까 봤던 filemap_add_folio()로 방금 생성한 folio를 페이지 캐시에 추가한다.

        error = filemap_read_folio(file, mapping, folio);
        if (error)
                goto error;

        filemap_invalidate_unlock_shared(mapping);
        folio_batch_add(fbatch, folio);
        return 0;

그 후 filemap_read_folio()로 파일의 내용을 folio로 읽어온 다음 folio_batch_add()로 fbatch에 추가한다.

error:
        filemap_invalidate_unlock_shared(mapping);
        folio_put(folio);
        return error;
}

마무리

처음 분석할 때 항상 그렇듯이 코드의 모든 부분을 이해하지는 못했다. 하지만 파일을 읽을 때 페이지 캐시에서 어떤 일이 일어나는지 큰 그림은 그릴 수 있었다.

사실 원래 페이지 캐시를 분석할 생각은 없었는데, LRU를 공부하다보니 페이지 캐시도 공부해야 했다. write()할 때의 페이지 캐시 로직도 정리하고 그 다음 LRU를 천천히 살펴봐야겠다.

참고 문서

 

Trees I: Radix trees [LWN.net]

The kernel includes a number of library routines for the implementation of useful data structures. Among those are two types of trees: radix trees and red-black trees. This article will have a look at the radix tree API, with red-black trees to follow in t

lwn.net

 

The XArray data structure [LWN.net]

LWN.net needs you!Without subscribers, LWN would simply not exist. Please consider signing up for a subscription and helping to keep LWN publishing By Jonathan Corbet January 24, 2018 linux.conf.au Sometimes, a data structure proves to be inadequate for it

lwn.net

 

Page Cache

Page Cache The page cache, as its name suggests, is a cache of pages. The pages originate from reads and writes of regular filesystem files, block device files, and memory-mapped files. In this manner, the page cache contains entire pages from recently acc

books.gigatux.nl

https://www.kernel.org/doc/Documentation/cachetlb.txt

 

flush_dcache_page와 kmap_atomic

원문 : http://barriosstory.blogspot.com/2009/01/flushdcachepage-kmapatomic.html 굉장히 소중한 자료이다. 더불어 글을 쓰는 스타일도 너무나 마음에 든다. 좋은 자료를 공유해준 분께 다시한번 감사 드립니..

decdream.tistory.com

댓글