A page-oriented method has not always been used in the Linux kernel to bear the main caching burden. Earlier versions included only the buffer cache to speed file operations and to enhance system performance. This was a legacy of other Unix look-alikes with the same structure. Blocks from the underlying block devices were kept in main memory buffers to make read and write operations faster. The implementation is contained in fs/buffers.c.
In contrast to pages in memory, blocks are not only (mostly) smaller but vary in size depending on the block device in use (or on the filesystem, as demonstrated in Chapter 9).
As a result of the ever increasing trend toward generic file access methods implemented by means of page-based operations, the buffer cache has lost much of its importance as a central system cache, and the main caching burden is now placed firmly on the page cache. Additionally, the standard data structure for block-based I/O is not a buffer anymore, but struct bio as discussed in Chapter 6.
Buffers are kept for small I/O transfers with block size granularity. This is often required by filesystems to handle their metadata. Transfer of raw data is done in a page-centric fashion, and the implementation of buffers is also on top of the page cache.3
The buffer cache consists of two structural units:
1. A buffer head holds all management data relating to the state of the buffer including information on block number, block size, access counter, and so on, discussed below. These data are not stored directly after the buffer head but in a separate area of RAM memory indicated by a corresponding pointer in the buffer head structure.
2. The useful data are held in specially reserved pages that may also reside in the page cache. This further subdivides the page cache as illustrated in Figure 16-2; in our example, the page is split into four identically sized parts, each of which is described by its own buffer head. The buffer heads are held in memory areas unrelated to the areas where the useful data are stored.
This enables the page to be subdivided into smaller sections because no gaps arise as a result of prefixing the buffer data with header data. As a buffer consists of at least 512 bytes, there may be up to a maximum of max_buf_per_page buffers per page; the constant is defined as a function of the page size:
#define MAX_BUF_PER_PAGE (PAGE_CACHE_SIZE / 512)
3This contrasts kernels before and including the 2.2 series that used separate caches for buffers and pages. Having two distinct caching possibilities requires enormous efforts to synchronize both, so the kernel developers chose to unify the caching scheme many years ago.
If one of the buffers is modified, this has an immediate effect on the contents of the page (and vice versa) so that there is no need for explicit synchronization of the two caches — after all, both share identical data.
Figure 16-2: Link between pages and buffers.
Figure 16-2: Link between pages and buffers.
There are, of course, applications that access block devices using blocks rather than pages — reading the superblock of a filesystem is one such example. A separate buffer cache is used to speed access of this kind. The buffer cache operates independently of the page cache, not in addition to it. To this end, buffer heads — the data structure is the same in buffer caches and page caches — are grouped together in an array of constant size whose individual entries are managed on a least recently used basis. After an entry has been used, it is placed at position 0 and the other entries are moved down accordingly; this means that the entries most frequently used are located at the beginning of the array and those less frequently used are pushed further back until they finally "drop"off the array if they have not been used for a lengthy period.
As the size of the array and therefore the number of entries in the LRU list are restricted to a fixed value that does not change during kernel run time, the kernel need not execute separate threads to trim the cache size to reasonable values. Instead, all it need do is remove the associated buffer from the cache when an entry drops off the array in order to release memory for other purposes.
Section 16.5 discusses in detail the technical details of buffer implementation. Before this, it is necessary to discuss the concept of address spaces because these are key to the implementation of cache functionality.
Was this article helpful?