/* * GRUB -- GRand Unified Bootloader * Copyright (C) 2002,2003,2004,2006,2007,2008,2009,2010 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GRUB is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GRUB. If not, see . */ #include #include #include #include #include #include #include #include #include #define GRUB_CACHE_TIMEOUT 2 /* The last time the disk was used. */ static grub_uint64_t grub_last_time = 0; struct grub_disk_cache grub_disk_cache_table[GRUB_DISK_CACHE_NUM]; void (*grub_disk_firmware_fini) (void); int grub_disk_firmware_is_tainted; #if DISK_CACHE_STATS static unsigned long grub_disk_cache_hits; static unsigned long grub_disk_cache_misses; void grub_disk_cache_get_performance (unsigned long *hits, unsigned long *misses) { *hits = grub_disk_cache_hits; *misses = grub_disk_cache_misses; } #endif grub_err_t (*grub_disk_write_weak) (grub_disk_t disk, grub_disk_addr_t sector, grub_off_t offset, grub_size_t size, const void *buf); #include "disk_common.c" void grub_disk_cache_invalidate_all (void) { unsigned i; for (i = 0; i < GRUB_DISK_CACHE_NUM; i++) { struct grub_disk_cache *cache = grub_disk_cache_table + i; if (cache->data && ! cache->lock) { grub_free (cache->data); cache->data = 0; } } } static char * grub_disk_cache_fetch (unsigned long dev_id, unsigned long disk_id, grub_disk_addr_t sector) { struct grub_disk_cache *cache; unsigned cache_index; cache_index = grub_disk_cache_get_index (dev_id, disk_id, sector); cache = grub_disk_cache_table + cache_index; if (cache->dev_id == dev_id && cache->disk_id == disk_id && cache->sector == sector) { cache->lock = 1; #if DISK_CACHE_STATS grub_disk_cache_hits++; #endif return cache->data; } #if DISK_CACHE_STATS grub_disk_cache_misses++; #endif return 0; } static void grub_disk_cache_unlock (unsigned long dev_id, unsigned long disk_id, grub_disk_addr_t sector) { struct grub_disk_cache *cache; unsigned cache_index; cache_index = grub_disk_cache_get_index (dev_id, disk_id, sector); cache = grub_disk_cache_table + cache_index; if (cache->dev_id == dev_id && cache->disk_id == disk_id && cache->sector == sector) { cache->lock = 0; } } static grub_err_t grub_disk_cache_store (unsigned long dev_id, unsigned long disk_id, grub_disk_addr_t sector, const char *data) { unsigned cache_index; struct grub_disk_cache *cache; cache_index = grub_disk_cache_get_index (dev_id, disk_id, sector); cache = grub_disk_cache_table + cache_index; cache->lock = 1; grub_free (cache->data); cache->data = 0; cache->lock = 0; cache->data = grub_malloc (GRUB_DISK_SECTOR_SIZE << GRUB_DISK_CACHE_BITS); if (! cache->data) { return grub_errno; } grub_memcpy (cache->data, data, GRUB_DISK_SECTOR_SIZE << GRUB_DISK_CACHE_BITS); cache->dev_id = dev_id; cache->disk_id = disk_id; cache->sector = sector; return GRUB_ERR_NONE; } grub_disk_dev_t grub_disk_dev_list; void grub_disk_dev_register (grub_disk_dev_t dev) { dev->next = grub_disk_dev_list; grub_disk_dev_list = dev; } void grub_disk_dev_unregister (grub_disk_dev_t dev) { grub_disk_dev_t *p, q; for (p = &grub_disk_dev_list, q = *p; q; p = &(q->next), q = q->next) { if (q == dev) { *p = q->next; break; } } } /* Return the location of the first ',', if any, which is not escaped by a '\'. */ static const char * find_part_sep (const char *name) { const char *p = name; char c; while ((c = *p++) != '\0') { if (c == '\\' && *p == ',') p++; else if (c == ',') { return p - 1; } } return NULL; } grub_disk_t grub_disk_open (const char *name) { const char *p; grub_disk_t disk; grub_disk_dev_t dev; char *raw = (char *) name; grub_uint64_t current_time; grub_dprintf ("disk", "Opening `%s'...\n", name); disk = (grub_disk_t) grub_zalloc (sizeof (*disk)); if (! disk) return 0; disk->log_sector_size = GRUB_DISK_SECTOR_BITS; /* Default 1MiB of maximum agglomerate. */ disk->max_agglomerate = 1048576 >> (GRUB_DISK_SECTOR_BITS + GRUB_DISK_CACHE_BITS); p = find_part_sep (name); if (p) { grub_size_t len = p - name; raw = grub_malloc (len + 1); if (! raw) goto fail; grub_memcpy (raw, name, len); raw[len] = '\0'; disk->name = grub_strdup (raw); } else { disk->name = grub_strdup (name); } if (! disk->name) { goto fail; } for (dev = grub_disk_dev_list; dev; dev = dev->next) { if ((dev->open) (raw, disk) == GRUB_ERR_NONE) break; else if (grub_errno == GRUB_ERR_UNKNOWN_DEVICE) grub_errno = GRUB_ERR_NONE; else goto fail; } if (! dev) { grub_error (GRUB_ERR_UNKNOWN_DEVICE, N_("disk `%s' not found"), name); goto fail; } if (disk->log_sector_size > GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS || disk->log_sector_size < GRUB_DISK_SECTOR_BITS) { grub_error (GRUB_ERR_NOT_IMPLEMENTED_YET, "sector sizes of %d bytes aren't supported yet", (1 << disk->log_sector_size)); goto fail; } disk->dev = dev; if (p) { disk->partition = grub_partition_probe (disk, p + 1); if (! disk->partition) { /* TRANSLATORS: It means that the specified partition e.g. hd0,msdos1=/dev/sda1 doesn't exist. */ grub_error (GRUB_ERR_UNKNOWN_DEVICE, N_("no such partition")); goto fail; } } /* The cache will be invalidated about 2 seconds after a device was closed. */ current_time = grub_get_time_ms (); if (current_time > (grub_last_time + GRUB_CACHE_TIMEOUT * 1000)) { grub_disk_cache_invalidate_all (); } grub_last_time = current_time; fail: if (raw && raw != name) grub_free (raw); if (grub_errno != GRUB_ERR_NONE) { grub_error_push (); grub_dprintf ("disk", "Opening `%s' failed.\n", name); grub_error_pop (); grub_disk_close (disk); return 0; } return disk; } void grub_disk_close (grub_disk_t disk) { grub_partition_t part; grub_dprintf ("disk", "Closing `%s'.\n", disk->name); if (disk->dev && disk->dev->close) (disk->dev->close) (disk); /* Reset the timer. */ grub_last_time = grub_get_time_ms (); while (disk->partition) { part = disk->partition->parent; grub_free (disk->partition); disk->partition = part; } grub_free ((void *) disk->name); grub_free (disk); } /* Small read (less than cache size and not pass across cache unit boundaries). sector is already adjusted and is divisible by cache unit size. */ static grub_err_t grub_disk_read_small_real (grub_disk_t disk, grub_disk_addr_t sector, grub_off_t offset, grub_size_t size, void *buf) { char *data; char *tmp_buf; /* Fetch the cache. */ data = grub_disk_cache_fetch (disk->dev->id, disk->id, sector); if (data) { /* Just copy it! */ grub_memcpy (buf, data + offset, size); grub_disk_cache_unlock (disk->dev->id, disk->id, sector); return GRUB_ERR_NONE; } /* Allocate a temporary buffer. */ tmp_buf = grub_malloc (GRUB_DISK_SECTOR_SIZE << GRUB_DISK_CACHE_BITS); if (! tmp_buf) return grub_errno; /* Otherwise read data from the disk actually. */ if (disk->total_sectors == GRUB_DISK_SIZE_UNKNOWN || sector + GRUB_DISK_CACHE_SIZE < (disk->total_sectors << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS))) { grub_err_t err; err = (disk->dev->read) (disk, transform_sector (disk, sector), 1U << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS - disk->log_sector_size), tmp_buf); if (!err) { /* Copy it and store it in the disk cache. */ grub_memcpy (buf, tmp_buf + offset, size); grub_disk_cache_store (disk->dev->id, disk->id, sector, tmp_buf); grub_free (tmp_buf); return GRUB_ERR_NONE; } } grub_free (tmp_buf); grub_errno = GRUB_ERR_NONE; { /* Uggh... Failed. Instead, just read necessary data. */ unsigned num; grub_disk_addr_t aligned_sector; sector += (offset >> GRUB_DISK_SECTOR_BITS); offset &= ((1 << GRUB_DISK_SECTOR_BITS) - 1); aligned_sector = (sector & ~((1ULL << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS)) - 1)); offset += ((sector - aligned_sector) << GRUB_DISK_SECTOR_BITS); num = ((size + offset + (1ULL << (disk->log_sector_size)) - 1) >> (disk->log_sector_size)); tmp_buf = grub_malloc (num << disk->log_sector_size); if (!tmp_buf) return grub_errno; if ((disk->dev->read) (disk, transform_sector (disk, aligned_sector), num, tmp_buf)) { grub_error_push (); grub_dprintf ("disk", "%s read failed\n", disk->name); grub_error_pop (); grub_free (tmp_buf); return grub_errno; } grub_memcpy (buf, tmp_buf + offset, size); grub_free (tmp_buf); return GRUB_ERR_NONE; } } static grub_err_t grub_disk_read_small (grub_disk_t disk, grub_disk_addr_t sector, grub_off_t offset, grub_size_t size, void *buf) { grub_err_t err; err = grub_disk_read_small_real (disk, sector, offset, size, buf); if (err) return err; if (disk->read_hook) (disk->read_hook) (sector + (offset >> GRUB_DISK_SECTOR_BITS), offset & (GRUB_DISK_SECTOR_SIZE - 1), size, disk->read_hook_data); return GRUB_ERR_NONE; } /* Read data from the disk. */ grub_err_t grub_disk_read (grub_disk_t disk, grub_disk_addr_t sector, grub_off_t offset, grub_size_t size, void *buf) { /* First of all, check if the region is within the disk. */ if (grub_disk_adjust_range (disk, §or, &offset, size) != GRUB_ERR_NONE) { grub_error_push (); grub_dprintf ("disk", "Read out of range: sector 0x%llx (%s).\n", (unsigned long long) sector, grub_errmsg); grub_error_pop (); return grub_errno; } /* First read until first cache boundary. */ if (offset || (sector & (GRUB_DISK_CACHE_SIZE - 1))) { grub_disk_addr_t start_sector; grub_size_t pos; grub_err_t err; grub_size_t len; start_sector = sector & ~((grub_disk_addr_t) GRUB_DISK_CACHE_SIZE - 1); pos = (sector - start_sector) << GRUB_DISK_SECTOR_BITS; len = ((GRUB_DISK_SECTOR_SIZE << GRUB_DISK_CACHE_BITS) - pos - offset); if (len > size) len = size; err = grub_disk_read_small (disk, start_sector, offset + pos, len, buf); if (err) return err; buf = (char *) buf + len; size -= len; offset += len; sector += (offset >> GRUB_DISK_SECTOR_BITS); offset &= ((1 << GRUB_DISK_SECTOR_BITS) - 1); } /* Until SIZE is zero... */ while (size >= (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS)) { char *data = NULL; grub_disk_addr_t agglomerate; grub_err_t err; /* agglomerate read until we find a first cached entry. */ for (agglomerate = 0; agglomerate < (size >> (GRUB_DISK_SECTOR_BITS + GRUB_DISK_CACHE_BITS)) && agglomerate < disk->max_agglomerate; agglomerate++) { data = grub_disk_cache_fetch (disk->dev->id, disk->id, sector + (agglomerate << GRUB_DISK_CACHE_BITS)); if (data) break; } if (data) { grub_memcpy ((char *) buf + (agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS)), data, GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS); grub_disk_cache_unlock (disk->dev->id, disk->id, sector + (agglomerate << GRUB_DISK_CACHE_BITS)); } if (agglomerate) { grub_disk_addr_t i; err = (disk->dev->read) (disk, transform_sector (disk, sector), agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS - disk->log_sector_size), buf); if (err) return err; for (i = 0; i < agglomerate; i ++) { grub_disk_cache_store (disk->dev->id, disk->id, sector + (i << GRUB_DISK_CACHE_BITS), (char *) buf + (i << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS))); } if (disk->read_hook) { (disk->read_hook) (sector, 0, agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS), disk->read_hook_data); } sector += agglomerate << GRUB_DISK_CACHE_BITS; size -= agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS); buf = (char *) buf + (agglomerate << (GRUB_DISK_CACHE_BITS + GRUB_DISK_SECTOR_BITS)); } if (data) { if (disk->read_hook) { (disk->read_hook) (sector, 0, (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS), disk->read_hook_data); } sector += GRUB_DISK_CACHE_SIZE; buf = (char *) buf + (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS); size -= (GRUB_DISK_CACHE_SIZE << GRUB_DISK_SECTOR_BITS); } } /* And now read the last part. */ if (size) { grub_err_t err; err = grub_disk_read_small (disk, sector, 0, size, buf); if (err) { return err; } } return grub_errno; } grub_uint64_t grub_disk_get_size (grub_disk_t disk) { if (disk->partition) return grub_partition_get_len (disk->partition); else if (disk->total_sectors != GRUB_DISK_SIZE_UNKNOWN) return disk->total_sectors << (disk->log_sector_size - GRUB_DISK_SECTOR_BITS); else return GRUB_DISK_SIZE_UNKNOWN; }