| /* |
| * HTTP block device |
| * |
| * Copyright (c) 2016-2017 Fabrice Bellard |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a copy |
| * of this software and associated documentation files (the "Software"), to deal |
| * in the Software without restriction, including without limitation the rights |
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| * copies of the Software, and to permit persons to whom the Software is |
| * furnished to do so, subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be included in |
| * all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| * THE SOFTWARE. |
| */ |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <stdarg.h> |
| #include <string.h> |
| #include <inttypes.h> |
| #include <assert.h> |
| #include <fcntl.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <time.h> |
| |
| #include "cutils.h" |
| #include "virtio.h" |
| #include "fs_wget.h" |
| #include "list.h" |
| #include "fbuf.h" |
| #include "machine.h" |
| |
| typedef enum { |
| CBLOCK_LOADING, |
| CBLOCK_LOADED, |
| } CachedBlockStateEnum; |
| |
| typedef struct CachedBlock { |
| struct list_head link; |
| struct BlockDeviceHTTP *bf; |
| unsigned int block_num; |
| CachedBlockStateEnum state; |
| FileBuffer fbuf; |
| } CachedBlock; |
| |
| #define BLK_FMT "%sblk%09u.bin" |
| #define GROUP_FMT "%sgrp%09u.bin" |
| #define PREFETCH_GROUP_LEN_MAX 32 |
| |
| typedef struct { |
| struct BlockDeviceHTTP *bf; |
| int group_num; |
| int n_block_num; |
| CachedBlock *tab_block[PREFETCH_GROUP_LEN_MAX]; |
| } PrefetchGroupRequest; |
| |
| /* modified data is stored per cluster (smaller than cached blocks to |
| avoid losing space) */ |
| typedef struct Cluster { |
| FileBuffer fbuf; |
| } Cluster; |
| |
| typedef struct BlockDeviceHTTP { |
| BlockDevice *bs; |
| int max_cache_size_kb; |
| char url[1024]; |
| int prefetch_count; |
| void (*start_cb)(void *opaque); |
| void *start_opaque; |
| |
| int64_t nb_sectors; |
| int block_size; /* in sectors, power of two */ |
| int nb_blocks; |
| struct list_head cached_blocks; /* list of CachedBlock */ |
| int n_cached_blocks; |
| int n_cached_blocks_max; |
| |
| /* write support */ |
| int sectors_per_cluster; /* power of two */ |
| Cluster **clusters; /* NULL if no written data */ |
| int n_clusters; |
| int n_allocated_clusters; |
| |
| /* statistics */ |
| int64_t n_read_sectors; |
| int64_t n_read_blocks; |
| int64_t n_write_sectors; |
| |
| /* current read request */ |
| BOOL is_write; |
| uint64_t sector_num; |
| int cur_block_num; |
| int sector_index, sector_count; |
| BlockDeviceCompletionFunc *cb; |
| void *opaque; |
| uint8_t *io_buf; |
| |
| /* prefetch */ |
| int prefetch_group_len; |
| } BlockDeviceHTTP; |
| |
| static void bf_update_block(CachedBlock *b, const uint8_t *data); |
| static void bf_read_onload(void *opaque, int err, void *data, size_t size); |
| static void bf_init_onload(void *opaque, int err, void *data, size_t size); |
| static void bf_prefetch_group_onload(void *opaque, int err, void *data, |
| size_t size); |
| |
| static CachedBlock *bf_find_block(BlockDeviceHTTP *bf, unsigned int block_num) |
| { |
| CachedBlock *b; |
| struct list_head *el; |
| |
| list_for_each(el, &bf->cached_blocks) { |
| b = list_entry(el, CachedBlock, link); |
| if (b->block_num == block_num) { |
| /* move to front */ |
| if (bf->cached_blocks.next != el) { |
| list_del(&b->link); |
| list_add(&b->link, &bf->cached_blocks); |
| } |
| return b; |
| } |
| } |
| return NULL; |
| } |
| |
| static void bf_free_block(BlockDeviceHTTP *bf, CachedBlock *b) |
| { |
| bf->n_cached_blocks--; |
| file_buffer_reset(&b->fbuf); |
| list_del(&b->link); |
| free(b); |
| } |
| |
| static CachedBlock *bf_add_block(BlockDeviceHTTP *bf, unsigned int block_num) |
| { |
| CachedBlock *b; |
| if (bf->n_cached_blocks >= bf->n_cached_blocks_max) { |
| struct list_head *el, *el1; |
| /* start by looking at the least unused blocks */ |
| list_for_each_prev_safe(el, el1, &bf->cached_blocks) { |
| b = list_entry(el, CachedBlock, link); |
| if (b->state == CBLOCK_LOADED) { |
| bf_free_block(bf, b); |
| if (bf->n_cached_blocks < bf->n_cached_blocks_max) |
| break; |
| } |
| } |
| } |
| b = mallocz(sizeof(CachedBlock)); |
| b->bf = bf; |
| b->block_num = block_num; |
| b->state = CBLOCK_LOADING; |
| file_buffer_init(&b->fbuf); |
| file_buffer_resize(&b->fbuf, bf->block_size * 512); |
| list_add(&b->link, &bf->cached_blocks); |
| bf->n_cached_blocks++; |
| return b; |
| } |
| |
| static int64_t bf_get_sector_count(BlockDevice *bs) |
| { |
| BlockDeviceHTTP *bf = bs->opaque; |
| return bf->nb_sectors; |
| } |
| |
| static void bf_start_load_block(BlockDevice *bs, int block_num) |
| { |
| BlockDeviceHTTP *bf = bs->opaque; |
| char filename[1024]; |
| CachedBlock *b; |
| b = bf_add_block(bf, block_num); |
| bf->n_read_blocks++; |
| /* make a XHR to read the block */ |
| #if 0 |
| printf("%u,\n", block_num); |
| #endif |
| #if 0 |
| printf("load_blk=%d cached=%d read=%d KB (%d KB) write=%d KB (%d KB)\n", |
| block_num, bf->n_cached_blocks, |
| (int)(bf->n_read_sectors / 2), |
| (int)(bf->n_read_blocks * bf->block_size / 2), |
| (int)(bf->n_write_sectors / 2), |
| (int)(bf->n_allocated_clusters * bf->sectors_per_cluster / 2)); |
| #endif |
| snprintf(filename, sizeof(filename), BLK_FMT, bf->url, block_num); |
| // printf("wget %s\n", filename); |
| fs_wget(filename, NULL, NULL, b, bf_read_onload, TRUE); |
| } |
| |
| static void bf_start_load_prefetch_group(BlockDevice *bs, int group_num, |
| const int *tab_block_num, |
| int n_block_num) |
| { |
| BlockDeviceHTTP *bf = bs->opaque; |
| CachedBlock *b; |
| PrefetchGroupRequest *req; |
| char filename[1024]; |
| BOOL req_flag; |
| int i; |
| |
| req_flag = FALSE; |
| req = malloc(sizeof(*req)); |
| req->bf = bf; |
| req->group_num = group_num; |
| req->n_block_num = n_block_num; |
| for(i = 0; i < n_block_num; i++) { |
| b = bf_find_block(bf, tab_block_num[i]); |
| if (!b) { |
| b = bf_add_block(bf, tab_block_num[i]); |
| req_flag = TRUE; |
| } else { |
| /* no need to read the block if it is already loading or |
| loaded */ |
| b = NULL; |
| } |
| req->tab_block[i] = b; |
| } |
| |
| if (req_flag) { |
| snprintf(filename, sizeof(filename), GROUP_FMT, bf->url, group_num); |
| // printf("wget %s\n", filename); |
| fs_wget(filename, NULL, NULL, req, bf_prefetch_group_onload, TRUE); |
| /* XXX: should add request in a list to free it for clean exit */ |
| } else { |
| free(req); |
| } |
| } |
| |
| static void bf_prefetch_group_onload(void *opaque, int err, void *data, |
| size_t size) |
| { |
| PrefetchGroupRequest *req = opaque; |
| BlockDeviceHTTP *bf = req->bf; |
| CachedBlock *b; |
| int block_bytes, i; |
| |
| if (err < 0) { |
| fprintf(stderr, "Could not load group %u\n", req->group_num); |
| exit(1); |
| } |
| block_bytes = bf->block_size * 512; |
| assert(size == block_bytes * req->n_block_num); |
| for(i = 0; i < req->n_block_num; i++) { |
| b = req->tab_block[i]; |
| if (b) { |
| bf_update_block(b, (const uint8_t *)data + block_bytes * i); |
| } |
| } |
| free(req); |
| } |
| |
| static int bf_rw_async1(BlockDevice *bs, BOOL is_sync) |
| { |
| BlockDeviceHTTP *bf = bs->opaque; |
| int offset, block_num, n, cluster_num; |
| CachedBlock *b; |
| Cluster *c; |
| |
| for(;;) { |
| n = bf->sector_count - bf->sector_index; |
| if (n == 0) |
| break; |
| cluster_num = bf->sector_num / bf->sectors_per_cluster; |
| c = bf->clusters[cluster_num]; |
| if (c) { |
| offset = bf->sector_num % bf->sectors_per_cluster; |
| n = min_int(n, bf->sectors_per_cluster - offset); |
| if (bf->is_write) { |
| file_buffer_write(&c->fbuf, offset * 512, |
| bf->io_buf + bf->sector_index * 512, n * 512); |
| } else { |
| file_buffer_read(&c->fbuf, offset * 512, |
| bf->io_buf + bf->sector_index * 512, n * 512); |
| } |
| bf->sector_index += n; |
| bf->sector_num += n; |
| } else { |
| block_num = bf->sector_num / bf->block_size; |
| offset = bf->sector_num % bf->block_size; |
| n = min_int(n, bf->block_size - offset); |
| bf->cur_block_num = block_num; |
| |
| b = bf_find_block(bf, block_num); |
| if (b) { |
| if (b->state == CBLOCK_LOADING) { |
| /* wait until the block is loaded */ |
| return 1; |
| } else { |
| if (bf->is_write) { |
| int cluster_size, cluster_offset; |
| uint8_t *buf; |
| /* allocate a new cluster */ |
| c = mallocz(sizeof(Cluster)); |
| cluster_size = bf->sectors_per_cluster * 512; |
| buf = malloc(cluster_size); |
| file_buffer_init(&c->fbuf); |
| file_buffer_resize(&c->fbuf, cluster_size); |
| bf->clusters[cluster_num] = c; |
| /* copy the cached block data to the cluster */ |
| cluster_offset = (cluster_num * bf->sectors_per_cluster) & |
| (bf->block_size - 1); |
| file_buffer_read(&b->fbuf, cluster_offset * 512, |
| buf, cluster_size); |
| file_buffer_write(&c->fbuf, 0, buf, cluster_size); |
| free(buf); |
| bf->n_allocated_clusters++; |
| continue; /* write to the allocated cluster */ |
| } else { |
| file_buffer_read(&b->fbuf, offset * 512, |
| bf->io_buf + bf->sector_index * 512, n * 512); |
| } |
| bf->sector_index += n; |
| bf->sector_num += n; |
| } |
| } else { |
| bf_start_load_block(bs, block_num); |
| return 1; |
| } |
| bf->cur_block_num = -1; |
| } |
| } |
| |
| if (!is_sync) { |
| // printf("end of request\n"); |
| /* end of request */ |
| bf->cb(bf->opaque, 0); |
| } |
| return 0; |
| } |
| |
| static void bf_update_block(CachedBlock *b, const uint8_t *data) |
| { |
| BlockDeviceHTTP *bf = b->bf; |
| BlockDevice *bs = bf->bs; |
| |
| assert(b->state == CBLOCK_LOADING); |
| file_buffer_write(&b->fbuf, 0, data, bf->block_size * 512); |
| b->state = CBLOCK_LOADED; |
| |
| /* continue I/O read/write if necessary */ |
| if (b->block_num == bf->cur_block_num) { |
| bf_rw_async1(bs, FALSE); |
| } |
| } |
| |
| static void bf_read_onload(void *opaque, int err, void *data, size_t size) |
| { |
| CachedBlock *b = opaque; |
| BlockDeviceHTTP *bf = b->bf; |
| |
| if (err < 0) { |
| fprintf(stderr, "Could not load block %u\n", b->block_num); |
| exit(1); |
| } |
| |
| assert(size == bf->block_size * 512); |
| bf_update_block(b, data); |
| } |
| |
| static int bf_read_async(BlockDevice *bs, |
| uint64_t sector_num, uint8_t *buf, int n, |
| BlockDeviceCompletionFunc *cb, void *opaque) |
| { |
| BlockDeviceHTTP *bf = bs->opaque; |
| // printf("bf_read_async: sector_num=%" PRId64 " n=%d\n", sector_num, n); |
| bf->is_write = FALSE; |
| bf->sector_num = sector_num; |
| bf->io_buf = buf; |
| bf->sector_count = n; |
| bf->sector_index = 0; |
| bf->cb = cb; |
| bf->opaque = opaque; |
| bf->n_read_sectors += n; |
| return bf_rw_async1(bs, TRUE); |
| } |
| |
| static int bf_write_async(BlockDevice *bs, |
| uint64_t sector_num, const uint8_t *buf, int n, |
| BlockDeviceCompletionFunc *cb, void *opaque) |
| { |
| BlockDeviceHTTP *bf = bs->opaque; |
| // printf("bf_write_async: sector_num=%" PRId64 " n=%d\n", sector_num, n); |
| bf->is_write = TRUE; |
| bf->sector_num = sector_num; |
| bf->io_buf = (uint8_t *)buf; |
| bf->sector_count = n; |
| bf->sector_index = 0; |
| bf->cb = cb; |
| bf->opaque = opaque; |
| bf->n_write_sectors += n; |
| return bf_rw_async1(bs, TRUE); |
| } |
| |
| BlockDevice *block_device_init_http(const char *url, |
| int max_cache_size_kb, |
| void (*start_cb)(void *opaque), |
| void *start_opaque) |
| { |
| BlockDevice *bs; |
| BlockDeviceHTTP *bf; |
| char *p; |
| |
| bs = mallocz(sizeof(*bs)); |
| bf = mallocz(sizeof(*bf)); |
| strcpy(bf->url, url); |
| /* get the path with the trailing '/' */ |
| p = strrchr(bf->url, '/'); |
| if (!p) |
| p = bf->url; |
| else |
| p++; |
| *p = '\0'; |
| |
| init_list_head(&bf->cached_blocks); |
| bf->max_cache_size_kb = max_cache_size_kb; |
| bf->start_cb = start_cb; |
| bf->start_opaque = start_opaque; |
| bf->bs = bs; |
| |
| bs->opaque = bf; |
| bs->get_sector_count = bf_get_sector_count; |
| bs->read_async = bf_read_async; |
| bs->write_async = bf_write_async; |
| |
| fs_wget(url, NULL, NULL, bs, bf_init_onload, TRUE); |
| return bs; |
| } |
| |
| static void bf_init_onload(void *opaque, int err, void *data, size_t size) |
| { |
| BlockDevice *bs = opaque; |
| BlockDeviceHTTP *bf = bs->opaque; |
| int block_size_kb, block_num; |
| JSONValue cfg, array; |
| |
| if (err < 0) { |
| fprintf(stderr, "Could not load block device file (err=%d)\n", -err); |
| exit(1); |
| } |
| |
| /* parse the disk image info */ |
| cfg = json_parse_value_len(data, size); |
| if (json_is_error(cfg)) { |
| vm_error("error: %s\n", json_get_error(cfg)); |
| config_error: |
| json_free(cfg); |
| exit(1); |
| } |
| |
| if (vm_get_int(cfg, "block_size", &block_size_kb) < 0) |
| goto config_error; |
| bf->block_size = block_size_kb * 2; |
| if (bf->block_size <= 0 || |
| (bf->block_size & (bf->block_size - 1)) != 0) { |
| vm_error("invalid block_size\n"); |
| goto config_error; |
| } |
| if (vm_get_int(cfg, "n_block", &bf->nb_blocks) < 0) |
| goto config_error; |
| if (bf->nb_blocks <= 0) { |
| vm_error("invalid n_block\n"); |
| goto config_error; |
| } |
| |
| bf->nb_sectors = bf->block_size * (uint64_t)bf->nb_blocks; |
| bf->n_cached_blocks = 0; |
| bf->n_cached_blocks_max = max_int(1, bf->max_cache_size_kb / block_size_kb); |
| bf->cur_block_num = -1; /* no request in progress */ |
| |
| bf->sectors_per_cluster = 8; /* 4 KB */ |
| bf->n_clusters = (bf->nb_sectors + bf->sectors_per_cluster - 1) / bf->sectors_per_cluster; |
| bf->clusters = mallocz(sizeof(bf->clusters[0]) * bf->n_clusters); |
| |
| if (vm_get_int_opt(cfg, "prefetch_group_len", |
| &bf->prefetch_group_len, 1) < 0) |
| goto config_error; |
| if (bf->prefetch_group_len > PREFETCH_GROUP_LEN_MAX) { |
| vm_error("prefetch_group_len is too large"); |
| goto config_error; |
| } |
| |
| array = json_object_get(cfg, "prefetch"); |
| if (!json_is_undefined(array)) { |
| int idx, prefetch_len, l, i; |
| JSONValue el; |
| int tab_block_num[PREFETCH_GROUP_LEN_MAX]; |
| |
| if (array.type != JSON_ARRAY) { |
| vm_error("expecting an array\n"); |
| goto config_error; |
| } |
| prefetch_len = array.u.array->len; |
| idx = 0; |
| while (idx < prefetch_len) { |
| l = min_int(prefetch_len - idx, bf->prefetch_group_len); |
| for(i = 0; i < l; i++) { |
| el = json_array_get(array, idx + i); |
| if (el.type != JSON_INT) { |
| vm_error("expecting an integer\n"); |
| goto config_error; |
| } |
| tab_block_num[i] = el.u.int32; |
| } |
| if (l == 1) { |
| block_num = tab_block_num[0]; |
| if (!bf_find_block(bf, block_num)) { |
| bf_start_load_block(bs, block_num); |
| } |
| } else { |
| bf_start_load_prefetch_group(bs, idx / bf->prefetch_group_len, |
| tab_block_num, l); |
| } |
| idx += l; |
| } |
| } |
| json_free(cfg); |
| |
| if (bf->start_cb) { |
| bf->start_cb(bf->start_opaque); |
| } |
| } |