[4] | 1 | /* The file system maintains a buffer cache to reduce the number of disk
|
---|
| 2 | * accesses needed. Whenever a read or write to the disk is done, a check is
|
---|
| 3 | * first made to see if the block is in the cache. This file manages the
|
---|
| 4 | * cache.
|
---|
| 5 | *
|
---|
| 6 | * The entry points into this file are:
|
---|
| 7 | * get_block: request to fetch a block for reading or writing from cache
|
---|
| 8 | * put_block: return a block previously requested with get_block
|
---|
| 9 | * alloc_zone: allocate a new zone (to increase the length of a file)
|
---|
| 10 | * free_zone: release a zone (when a file is removed)
|
---|
| 11 | * rw_block: read or write a block from the disk itself
|
---|
| 12 | * invalidate: remove all the cache blocks on some device
|
---|
| 13 | */
|
---|
| 14 |
|
---|
| 15 | #include "fs.h"
|
---|
| 16 | #include <minix/com.h>
|
---|
| 17 | #include "buf.h"
|
---|
| 18 | #include "file.h"
|
---|
| 19 | #include "fproc.h"
|
---|
| 20 | #include "super.h"
|
---|
| 21 |
|
---|
| 22 | FORWARD _PROTOTYPE( void rm_lru, (struct buf *bp) );
|
---|
| 23 |
|
---|
| 24 | /*===========================================================================*
|
---|
| 25 | * get_block *
|
---|
| 26 | *===========================================================================*/
|
---|
| 27 | PUBLIC struct buf *get_block(dev, block, only_search)
|
---|
| 28 | register dev_t dev; /* on which device is the block? */
|
---|
| 29 | register block_t block; /* which block is wanted? */
|
---|
| 30 | int only_search; /* if NO_READ, don't read, else act normal */
|
---|
| 31 | {
|
---|
| 32 | /* Check to see if the requested block is in the block cache. If so, return
|
---|
| 33 | * a pointer to it. If not, evict some other block and fetch it (unless
|
---|
| 34 | * 'only_search' is 1). All the blocks in the cache that are not in use
|
---|
| 35 | * are linked together in a chain, with 'front' pointing to the least recently
|
---|
| 36 | * used block and 'rear' to the most recently used block. If 'only_search' is
|
---|
| 37 | * 1, the block being requested will be overwritten in its entirety, so it is
|
---|
| 38 | * only necessary to see if it is in the cache; if it is not, any free buffer
|
---|
| 39 | * will do. It is not necessary to actually read the block in from disk.
|
---|
| 40 | * If 'only_search' is PREFETCH, the block need not be read from the disk,
|
---|
| 41 | * and the device is not to be marked on the block, so callers can tell if
|
---|
| 42 | * the block returned is valid.
|
---|
| 43 | * In addition to the LRU chain, there is also a hash chain to link together
|
---|
| 44 | * blocks whose block numbers end with the same bit strings, for fast lookup.
|
---|
| 45 | */
|
---|
| 46 |
|
---|
| 47 | int b;
|
---|
| 48 | register struct buf *bp, *prev_ptr;
|
---|
| 49 |
|
---|
| 50 | /* Search the hash chain for (dev, block). Do_read() can use
|
---|
| 51 | * get_block(NO_DEV ...) to get an unnamed block to fill with zeros when
|
---|
| 52 | * someone wants to read from a hole in a file, in which case this search
|
---|
| 53 | * is skipped
|
---|
| 54 | */
|
---|
| 55 | if (dev != NO_DEV) {
|
---|
| 56 | b = (int) block & HASH_MASK;
|
---|
| 57 | bp = buf_hash[b];
|
---|
| 58 | while (bp != NIL_BUF) {
|
---|
| 59 | if (bp->b_blocknr == block && bp->b_dev == dev) {
|
---|
| 60 | /* Block needed has been found. */
|
---|
| 61 | if (bp->b_count == 0) rm_lru(bp);
|
---|
| 62 | bp->b_count++; /* record that block is in use */
|
---|
| 63 |
|
---|
| 64 | return(bp);
|
---|
| 65 | } else {
|
---|
| 66 | /* This block is not the one sought. */
|
---|
| 67 | bp = bp->b_hash; /* move to next block on hash chain */
|
---|
| 68 | }
|
---|
| 69 | }
|
---|
| 70 | }
|
---|
| 71 |
|
---|
| 72 | /* Desired block is not on available chain. Take oldest block ('front'). */
|
---|
| 73 | if ((bp = front) == NIL_BUF) panic(__FILE__,"all buffers in use", NR_BUFS);
|
---|
| 74 | rm_lru(bp);
|
---|
| 75 |
|
---|
| 76 | /* Remove the block that was just taken from its hash chain. */
|
---|
| 77 | b = (int) bp->b_blocknr & HASH_MASK;
|
---|
| 78 | prev_ptr = buf_hash[b];
|
---|
| 79 | if (prev_ptr == bp) {
|
---|
| 80 | buf_hash[b] = bp->b_hash;
|
---|
| 81 | } else {
|
---|
| 82 | /* The block just taken is not on the front of its hash chain. */
|
---|
| 83 | while (prev_ptr->b_hash != NIL_BUF)
|
---|
| 84 | if (prev_ptr->b_hash == bp) {
|
---|
| 85 | prev_ptr->b_hash = bp->b_hash; /* found it */
|
---|
| 86 | break;
|
---|
| 87 | } else {
|
---|
| 88 | prev_ptr = prev_ptr->b_hash; /* keep looking */
|
---|
| 89 | }
|
---|
| 90 | }
|
---|
| 91 |
|
---|
| 92 | /* If the block taken is dirty, make it clean by writing it to the disk.
|
---|
| 93 | * Avoid hysteresis by flushing all other dirty blocks for the same device.
|
---|
| 94 | */
|
---|
| 95 | if (bp->b_dev != NO_DEV) {
|
---|
| 96 | if (bp->b_dirt == DIRTY) flushall(bp->b_dev);
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | /* Fill in block's parameters and add it to the hash chain where it goes. */
|
---|
| 100 | bp->b_dev = dev; /* fill in device number */
|
---|
| 101 | bp->b_blocknr = block; /* fill in block number */
|
---|
| 102 | bp->b_count++; /* record that block is being used */
|
---|
| 103 | b = (int) bp->b_blocknr & HASH_MASK;
|
---|
| 104 | bp->b_hash = buf_hash[b];
|
---|
| 105 | buf_hash[b] = bp; /* add to hash list */
|
---|
| 106 |
|
---|
| 107 | /* Go get the requested block unless searching or prefetching. */
|
---|
| 108 | if (dev != NO_DEV) {
|
---|
| 109 | if (only_search == PREFETCH) bp->b_dev = NO_DEV;
|
---|
| 110 | else
|
---|
| 111 | if (only_search == NORMAL) {
|
---|
| 112 | rw_block(bp, READING);
|
---|
| 113 | }
|
---|
| 114 | }
|
---|
| 115 | return(bp); /* return the newly acquired block */
|
---|
| 116 | }
|
---|
| 117 |
|
---|
| 118 | /*===========================================================================*
|
---|
| 119 | * put_block *
|
---|
| 120 | *===========================================================================*/
|
---|
| 121 | PUBLIC void put_block(bp, block_type)
|
---|
| 122 | register struct buf *bp; /* pointer to the buffer to be released */
|
---|
| 123 | int block_type; /* INODE_BLOCK, DIRECTORY_BLOCK, or whatever */
|
---|
| 124 | {
|
---|
| 125 | /* Return a block to the list of available blocks. Depending on 'block_type'
|
---|
| 126 | * it may be put on the front or rear of the LRU chain. Blocks that are
|
---|
| 127 | * expected to be needed again shortly (e.g., partially full data blocks)
|
---|
| 128 | * go on the rear; blocks that are unlikely to be needed again shortly
|
---|
| 129 | * (e.g., full data blocks) go on the front. Blocks whose loss can hurt
|
---|
| 130 | * the integrity of the file system (e.g., inode blocks) are written to
|
---|
| 131 | * disk immediately if they are dirty.
|
---|
| 132 | */
|
---|
| 133 | if (bp == NIL_BUF) return; /* it is easier to check here than in caller */
|
---|
| 134 |
|
---|
| 135 | bp->b_count--; /* there is one use fewer now */
|
---|
| 136 | if (bp->b_count != 0) return; /* block is still in use */
|
---|
| 137 |
|
---|
| 138 | bufs_in_use--; /* one fewer block buffers in use */
|
---|
| 139 |
|
---|
| 140 | /* Put this block back on the LRU chain. If the ONE_SHOT bit is set in
|
---|
| 141 | * 'block_type', the block is not likely to be needed again shortly, so put
|
---|
| 142 | * it on the front of the LRU chain where it will be the first one to be
|
---|
| 143 | * taken when a free buffer is needed later.
|
---|
| 144 | */
|
---|
| 145 | if (bp->b_dev == DEV_RAM || block_type & ONE_SHOT) {
|
---|
| 146 | /* Block probably won't be needed quickly. Put it on front of chain.
|
---|
| 147 | * It will be the next block to be evicted from the cache.
|
---|
| 148 | */
|
---|
| 149 | bp->b_prev = NIL_BUF;
|
---|
| 150 | bp->b_next = front;
|
---|
| 151 | if (front == NIL_BUF)
|
---|
| 152 | rear = bp; /* LRU chain was empty */
|
---|
| 153 | else
|
---|
| 154 | front->b_prev = bp;
|
---|
| 155 | front = bp;
|
---|
| 156 | } else {
|
---|
| 157 | /* Block probably will be needed quickly. Put it on rear of chain.
|
---|
| 158 | * It will not be evicted from the cache for a long time.
|
---|
| 159 | */
|
---|
| 160 | bp->b_prev = rear;
|
---|
| 161 | bp->b_next = NIL_BUF;
|
---|
| 162 | if (rear == NIL_BUF)
|
---|
| 163 | front = bp;
|
---|
| 164 | else
|
---|
| 165 | rear->b_next = bp;
|
---|
| 166 | rear = bp;
|
---|
| 167 | }
|
---|
| 168 |
|
---|
| 169 | /* Some blocks are so important (e.g., inodes, indirect blocks) that they
|
---|
| 170 | * should be written to the disk immediately to avoid messing up the file
|
---|
| 171 | * system in the event of a crash.
|
---|
| 172 | */
|
---|
| 173 | if ((block_type & WRITE_IMMED) && bp->b_dirt==DIRTY && bp->b_dev != NO_DEV) {
|
---|
| 174 | rw_block(bp, WRITING);
|
---|
| 175 | }
|
---|
| 176 | }
|
---|
| 177 |
|
---|
| 178 | /*===========================================================================*
|
---|
| 179 | * alloc_zone *
|
---|
| 180 | *===========================================================================*/
|
---|
| 181 | PUBLIC zone_t alloc_zone(dev, z)
|
---|
| 182 | dev_t dev; /* device where zone wanted */
|
---|
| 183 | zone_t z; /* try to allocate new zone near this one */
|
---|
| 184 | {
|
---|
| 185 | /* Allocate a new zone on the indicated device and return its number. */
|
---|
| 186 |
|
---|
| 187 | int major, minor;
|
---|
| 188 | bit_t b, bit;
|
---|
| 189 | struct super_block *sp;
|
---|
| 190 |
|
---|
| 191 | /* Note that the routine alloc_bit() returns 1 for the lowest possible
|
---|
| 192 | * zone, which corresponds to sp->s_firstdatazone. To convert a value
|
---|
| 193 | * between the bit number, 'b', used by alloc_bit() and the zone number, 'z',
|
---|
| 194 | * stored in the inode, use the formula:
|
---|
| 195 | * z = b + sp->s_firstdatazone - 1
|
---|
| 196 | * Alloc_bit() never returns 0, since this is used for NO_BIT (failure).
|
---|
| 197 | */
|
---|
| 198 | sp = get_super(dev);
|
---|
| 199 |
|
---|
| 200 | /* If z is 0, skip initial part of the map known to be fully in use. */
|
---|
| 201 | if (z == sp->s_firstdatazone) {
|
---|
| 202 | bit = sp->s_zsearch;
|
---|
| 203 | } else {
|
---|
| 204 | bit = (bit_t) z - (sp->s_firstdatazone - 1);
|
---|
| 205 | }
|
---|
| 206 | b = alloc_bit(sp, ZMAP, bit);
|
---|
| 207 | if (b == NO_BIT) {
|
---|
| 208 | err_code = ENOSPC;
|
---|
| 209 | major = (int) (sp->s_dev >> MAJOR) & BYTE;
|
---|
| 210 | minor = (int) (sp->s_dev >> MINOR) & BYTE;
|
---|
| 211 | printf("No space on %sdevice %d/%d\n",
|
---|
| 212 | sp->s_dev == root_dev ? "root " : "", major, minor);
|
---|
| 213 | return(NO_ZONE);
|
---|
| 214 | }
|
---|
| 215 | if (z == sp->s_firstdatazone) sp->s_zsearch = b; /* for next time */
|
---|
| 216 | return(sp->s_firstdatazone - 1 + (zone_t) b);
|
---|
| 217 | }
|
---|
| 218 |
|
---|
| 219 | /*===========================================================================*
|
---|
| 220 | * free_zone *
|
---|
| 221 | *===========================================================================*/
|
---|
| 222 | PUBLIC void free_zone(dev, numb)
|
---|
| 223 | dev_t dev; /* device where zone located */
|
---|
| 224 | zone_t numb; /* zone to be returned */
|
---|
| 225 | {
|
---|
| 226 | /* Return a zone. */
|
---|
| 227 |
|
---|
| 228 | register struct super_block *sp;
|
---|
| 229 | bit_t bit;
|
---|
| 230 |
|
---|
| 231 | /* Locate the appropriate super_block and return bit. */
|
---|
| 232 | sp = get_super(dev);
|
---|
| 233 | if (numb < sp->s_firstdatazone || numb >= sp->s_zones) return;
|
---|
| 234 | bit = (bit_t) (numb - (sp->s_firstdatazone - 1));
|
---|
| 235 | free_bit(sp, ZMAP, bit);
|
---|
| 236 | if (bit < sp->s_zsearch) sp->s_zsearch = bit;
|
---|
| 237 | }
|
---|
| 238 |
|
---|
| 239 | /*===========================================================================*
|
---|
| 240 | * rw_block *
|
---|
| 241 | *===========================================================================*/
|
---|
| 242 | PUBLIC void rw_block(bp, rw_flag)
|
---|
| 243 | register struct buf *bp; /* buffer pointer */
|
---|
| 244 | int rw_flag; /* READING or WRITING */
|
---|
| 245 | {
|
---|
| 246 | /* Read or write a disk block. This is the only routine in which actual disk
|
---|
| 247 | * I/O is invoked. If an error occurs, a message is printed here, but the error
|
---|
| 248 | * is not reported to the caller. If the error occurred while purging a block
|
---|
| 249 | * from the cache, it is not clear what the caller could do about it anyway.
|
---|
| 250 | */
|
---|
| 251 |
|
---|
| 252 | int r, op;
|
---|
| 253 | off_t pos;
|
---|
| 254 | dev_t dev;
|
---|
| 255 | int block_size;
|
---|
| 256 |
|
---|
| 257 | block_size = get_block_size(bp->b_dev);
|
---|
| 258 |
|
---|
| 259 | if ( (dev = bp->b_dev) != NO_DEV) {
|
---|
| 260 | pos = (off_t) bp->b_blocknr * block_size;
|
---|
| 261 | op = (rw_flag == READING ? DEV_READ : DEV_WRITE);
|
---|
| 262 | r = dev_io(op, dev, FS_PROC_NR, bp->b_data, pos, block_size, 0);
|
---|
| 263 | if (r != block_size) {
|
---|
| 264 | if (r >= 0) r = END_OF_FILE;
|
---|
| 265 | if (r != END_OF_FILE)
|
---|
| 266 | printf("Unrecoverable disk error on device %d/%d, block %ld\n",
|
---|
| 267 | (dev>>MAJOR)&BYTE, (dev>>MINOR)&BYTE, bp->b_blocknr);
|
---|
| 268 | bp->b_dev = NO_DEV; /* invalidate block */
|
---|
| 269 |
|
---|
| 270 | /* Report read errors to interested parties. */
|
---|
| 271 | if (rw_flag == READING) rdwt_err = r;
|
---|
| 272 | }
|
---|
| 273 | }
|
---|
| 274 |
|
---|
| 275 | bp->b_dirt = CLEAN;
|
---|
| 276 | }
|
---|
| 277 |
|
---|
| 278 | /*===========================================================================*
|
---|
| 279 | * invalidate *
|
---|
| 280 | *===========================================================================*/
|
---|
| 281 | PUBLIC void invalidate(device)
|
---|
| 282 | dev_t device; /* device whose blocks are to be purged */
|
---|
| 283 | {
|
---|
| 284 | /* Remove all the blocks belonging to some device from the cache. */
|
---|
| 285 |
|
---|
| 286 | register struct buf *bp;
|
---|
| 287 |
|
---|
| 288 | for (bp = &buf[0]; bp < &buf[NR_BUFS]; bp++)
|
---|
| 289 | if (bp->b_dev == device) bp->b_dev = NO_DEV;
|
---|
| 290 | }
|
---|
| 291 |
|
---|
| 292 | /*===========================================================================*
|
---|
| 293 | * flushall *
|
---|
| 294 | *===========================================================================*/
|
---|
| 295 | PUBLIC void flushall(dev)
|
---|
| 296 | dev_t dev; /* device to flush */
|
---|
| 297 | {
|
---|
| 298 | /* Flush all dirty blocks for one device. */
|
---|
| 299 |
|
---|
| 300 | register struct buf *bp;
|
---|
| 301 | static struct buf *dirty[NR_BUFS]; /* static so it isn't on stack */
|
---|
| 302 | int ndirty;
|
---|
| 303 |
|
---|
| 304 | for (bp = &buf[0], ndirty = 0; bp < &buf[NR_BUFS]; bp++)
|
---|
| 305 | if (bp->b_dirt == DIRTY && bp->b_dev == dev) dirty[ndirty++] = bp;
|
---|
| 306 | rw_scattered(dev, dirty, ndirty, WRITING);
|
---|
| 307 | }
|
---|
| 308 |
|
---|
| 309 | /*===========================================================================*
|
---|
| 310 | * rw_scattered *
|
---|
| 311 | *===========================================================================*/
|
---|
| 312 | PUBLIC void rw_scattered(dev, bufq, bufqsize, rw_flag)
|
---|
| 313 | dev_t dev; /* major-minor device number */
|
---|
| 314 | struct buf **bufq; /* pointer to array of buffers */
|
---|
| 315 | int bufqsize; /* number of buffers */
|
---|
| 316 | int rw_flag; /* READING or WRITING */
|
---|
| 317 | {
|
---|
| 318 | /* Read or write scattered data from a device. */
|
---|
| 319 |
|
---|
| 320 | register struct buf *bp;
|
---|
| 321 | int gap;
|
---|
| 322 | register int i;
|
---|
| 323 | register iovec_t *iop;
|
---|
| 324 | static iovec_t iovec[NR_IOREQS]; /* static so it isn't on stack */
|
---|
| 325 | int j, r;
|
---|
| 326 | int block_size;
|
---|
| 327 |
|
---|
| 328 | block_size = get_block_size(dev);
|
---|
| 329 |
|
---|
| 330 | /* (Shell) sort buffers on b_blocknr. */
|
---|
| 331 | gap = 1;
|
---|
| 332 | do
|
---|
| 333 | gap = 3 * gap + 1;
|
---|
| 334 | while (gap <= bufqsize);
|
---|
| 335 | while (gap != 1) {
|
---|
| 336 | gap /= 3;
|
---|
| 337 | for (j = gap; j < bufqsize; j++) {
|
---|
| 338 | for (i = j - gap;
|
---|
| 339 | i >= 0 && bufq[i]->b_blocknr > bufq[i + gap]->b_blocknr;
|
---|
| 340 | i -= gap) {
|
---|
| 341 | bp = bufq[i];
|
---|
| 342 | bufq[i] = bufq[i + gap];
|
---|
| 343 | bufq[i + gap] = bp;
|
---|
| 344 | }
|
---|
| 345 | }
|
---|
| 346 | }
|
---|
| 347 |
|
---|
| 348 | /* Set up I/O vector and do I/O. The result of dev_io is OK if everything
|
---|
| 349 | * went fine, otherwise the error code for the first failed transfer.
|
---|
| 350 | */
|
---|
| 351 | while (bufqsize > 0) {
|
---|
| 352 | for (j = 0, iop = iovec; j < NR_IOREQS && j < bufqsize; j++, iop++) {
|
---|
| 353 | bp = bufq[j];
|
---|
| 354 | if (bp->b_blocknr != bufq[0]->b_blocknr + j) break;
|
---|
| 355 | iop->iov_addr = (vir_bytes) bp->b_data;
|
---|
| 356 | iop->iov_size = block_size;
|
---|
| 357 | }
|
---|
| 358 | r = dev_io(rw_flag == WRITING ? DEV_SCATTER : DEV_GATHER,
|
---|
| 359 | dev, FS_PROC_NR, iovec,
|
---|
| 360 | (off_t) bufq[0]->b_blocknr * block_size, j, 0);
|
---|
| 361 |
|
---|
| 362 | /* Harvest the results. Dev_io reports the first error it may have
|
---|
| 363 | * encountered, but we only care if it's the first block that failed.
|
---|
| 364 | */
|
---|
| 365 | for (i = 0, iop = iovec; i < j; i++, iop++) {
|
---|
| 366 | bp = bufq[i];
|
---|
| 367 | if (iop->iov_size != 0) {
|
---|
| 368 | /* Transfer failed. An error? Do we care? */
|
---|
| 369 | if (r != OK && i == 0) {
|
---|
| 370 | printf(
|
---|
| 371 | "fs: I/O error on device %d/%d, block %lu\n",
|
---|
| 372 | (dev>>MAJOR)&BYTE, (dev>>MINOR)&BYTE,
|
---|
| 373 | bp->b_blocknr);
|
---|
| 374 | bp->b_dev = NO_DEV; /* invalidate block */
|
---|
| 375 | }
|
---|
| 376 | break;
|
---|
| 377 | }
|
---|
| 378 | if (rw_flag == READING) {
|
---|
| 379 | bp->b_dev = dev; /* validate block */
|
---|
| 380 | put_block(bp, PARTIAL_DATA_BLOCK);
|
---|
| 381 | } else {
|
---|
| 382 | bp->b_dirt = CLEAN;
|
---|
| 383 | }
|
---|
| 384 | }
|
---|
| 385 | bufq += i;
|
---|
| 386 | bufqsize -= i;
|
---|
| 387 | if (rw_flag == READING) {
|
---|
| 388 | /* Don't bother reading more than the device is willing to
|
---|
| 389 | * give at this time. Don't forget to release those extras.
|
---|
| 390 | */
|
---|
| 391 | while (bufqsize > 0) {
|
---|
| 392 | put_block(*bufq++, PARTIAL_DATA_BLOCK);
|
---|
| 393 | bufqsize--;
|
---|
| 394 | }
|
---|
| 395 | }
|
---|
| 396 | if (rw_flag == WRITING && i == 0) {
|
---|
| 397 | /* We're not making progress, this means we might keep
|
---|
| 398 | * looping. Buffers remain dirty if un-written. Buffers are
|
---|
| 399 | * lost if invalidate()d or LRU-removed while dirty. This
|
---|
| 400 | * is better than keeping unwritable blocks around forever..
|
---|
| 401 | */
|
---|
| 402 | break;
|
---|
| 403 | }
|
---|
| 404 | }
|
---|
| 405 | }
|
---|
| 406 |
|
---|
| 407 | /*===========================================================================*
|
---|
| 408 | * rm_lru *
|
---|
| 409 | *===========================================================================*/
|
---|
| 410 | PRIVATE void rm_lru(bp)
|
---|
| 411 | struct buf *bp;
|
---|
| 412 | {
|
---|
| 413 | /* Remove a block from its LRU chain. */
|
---|
| 414 | struct buf *next_ptr, *prev_ptr;
|
---|
| 415 |
|
---|
| 416 | bufs_in_use++;
|
---|
| 417 | next_ptr = bp->b_next; /* successor on LRU chain */
|
---|
| 418 | prev_ptr = bp->b_prev; /* predecessor on LRU chain */
|
---|
| 419 | if (prev_ptr != NIL_BUF)
|
---|
| 420 | prev_ptr->b_next = next_ptr;
|
---|
| 421 | else
|
---|
| 422 | front = next_ptr; /* this block was at front of chain */
|
---|
| 423 |
|
---|
| 424 | if (next_ptr != NIL_BUF)
|
---|
| 425 | next_ptr->b_prev = prev_ptr;
|
---|
| 426 | else
|
---|
| 427 | rear = prev_ptr; /* this block was at rear of chain */
|
---|
| 428 | }
|
---|