Хочу поделиться собственным опытом решения этой проблемы. Я чувствую вашу боль и замешательство, но на самом деле реализовать рабочее решение не так уж сложно, учитывая широкий спектр возможностей, которые у вас есть, если вы знаете, что делаете.
Цель
Реализуйте пул буферов, способный выполнять две операции - получение и освобождение.
Базовая стратегия пула:
- acquire withdraws a buffer from the pool effectively reducing the available number of buffers by 1;
- If there are no buffers available, two options arise:
- grow the pool and return a newly created buffer; or
- создать и вернуть фиктивный буфер (поясняется ниже).
- release возвращает буфер в пул.
Пул может быть фиксированного или переменного размера. «Переменная» означает, что изначально имеется M предварительно выделенных буферов (например, ноль), а пул может увеличиваться по запросу до N. «Фиксированный» означает, что все буферы заранее выделяются при создании пула (M = N).
Реализуйте обратный вызов, который получает буферы для libuv.
Не допускайте бесконечного роста пула при сохранении работоспособности пула ни при каких обстоятельствах, за исключением ситуаций нехватки памяти.
Выполнение
А теперь давайте подробнее рассмотрим все это.
Состав бассейна:
#define BUFPOOL_CAPACITY 100
typedef struct bufpool_s bufpool_t;
struct bufpool_s {
void *bufs[BUFPOOL_CAPACITY];
int size;
};
size
- текущий размер пула.
Сам буфер представляет собой блок памяти с префиксом следующей структуры:
#define bufbase(ptr) ((bufbase_t *)((char *)(ptr) - sizeof(bufbase_t)))
#define buflen(ptr) (bufbase(ptr)->len)
typedef struct bufbase_s bufbase_t;
struct bufbase_s {
bufpool_t *pool;
int len;
};
len
- длина буфера в байтах.
Выделение нового буфера выглядит так:
void *bufpool_alloc(bufpool_t *pool, int len) {
bufbase_t *base = malloc(sizeof(bufbase_t) + len);
if (!base) return 0;
base->pool = pool;
base->len = len;
return (char *)base + sizeof(bufbase_t);
}
Обратите внимание, что возвращенный указатель указывает на следующий после заголовка байт - область данных. Это позволяет иметь указатели буфера, как если бы они были выделены стандартным вызовом malloc
.
Распределение происходит наоборот:
void bufpool_free(void *ptr) {
if (!ptr) return;
free(bufbase(ptr));
}
Обратный вызов выделения для libuv выглядит так:
void alloc_cb(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
int len;
void *ptr = bufpool_acquire(handle->loop->data, &len);
*buf = uv_buf_init(ptr, len);
}
Вы можете видеть здесь, что alloc_cb
принимает указатель пула буферов из указателя пользовательских данных в цикле. Это означает, что буферный пул должен быть присоединен к циклу событий перед использованием. Другими словами, вы должны инициализировать пул при создании цикла и назначить его указатель на поле data
. Если вы уже храните другие пользовательские данные в этом поле, просто расширьте свою структуру.
фиктивный буфер - это фиктивный буфер, что означает, что он не создается в пуле, но по-прежнему полностью функционален. Назначение фиктивных буферов - поддерживать работу всего этого в редких ситуациях нехватки пула, то есть когда все буферы получены и есть потребность в другом. Основываясь на моем исследовании, выделение небольших блоков памяти размером около 8 Кбайт выполняется очень быстро во всех современных операционных системах, что хорошо соответствует размеру фиктивного буфера.
#define DUMMY_BUF_SIZE 8000
void *bufpool_dummy() {
return bufpool_alloc(0, DUMMY_BUF_SIZE);
}
Операция получения:
void *bufpool_acquire(bufpool_t *pool, int *len) {
void *buf = bufpool_dequeue(pool);
if (!buf) buf = bufpool_dummy();
*len = buf ? buflen(buf) : 0;
return buf;
}
Операция выпуска:
void bufpool_release(void *ptr) {
bufbase_t *base;
if (!ptr) return;
base = bufbase(ptr);
if (base->pool) bufpool_enqueue(base->pool, ptr);
else free(base);
}
Здесь есть две функции - bufpool_enqueue
и bufpool_dequeue
. В основном они выполняют всю работу бассейна.
В моем случае есть очередь буферных индексов O (1) поверх упомянутого выше, что позволяет мне более эффективно отслеживать состояние пула, очень быстро извлекая индексы буферов. Нет необходимости впадать в крайности, как я, потому что максимальный размер пула ограничен, следовательно, любой поиск по массиву также будет постоянным во времени.
В простейшем случае вы можете реализовать эти функции как чисто линейные поисковые системы по всему массиву bufs
в структуре bufpool_s
. Например, если буфер получен, вы ищите первое место, отличное от NULL, сохраняете указатель и помещаете в это место NULL. В следующий раз, когда буфер будет освобожден, вы ищите первую нулевую точку и сохраняете там указатель.
Внутреннее устройство бассейна выглядит следующим образом:
#define BUF_SIZE 64000
void *bufpool_grow(bufpool_t *pool) {
int idx = pool->size;
void *buf;
if (idx == BUFPOOL_CAPACITY) return 0;
buf = bufpool_alloc(pool, BUF_SIZE);
if (!buf) return 0;
pool->bufs[idx] = 0;
pool->size = idx + 1;
return buf;
}
void bufpool_enqueue(bufpool_t *pool, void *ptr) {
int idx;
for (idx = 0; idx < pool->size; ++idx) {
if (!pool->bufs[idx]) break;
}
assert(idx < pool->size);
pool->bufs[idx] = ptr;
}
void *bufpool_dequeue(bufpool_t *pool) {
int idx;
void *ptr;
for (idx = 0; idx < pool->size; ++idx) {
ptr = pool->bufs[idx];
if (ptr) {
pool->bufs[idx] = 0;
return ptr;
}
}
return bufpool_grow(pool);
}
Нормальный размер буфера составляет 64000 байт, потому что я хочу, чтобы он удобно помещался в блок размером 64 КБ с его заголовком.
И, наконец, процедуры инициализации и деинициализации:
void bufpool_init(bufpool_t *pool) {
pool->size = 0;
}
void bufpool_done(bufpool_t *pool) {
int idx;
for (idx = 0; idx < pool->size; ++idx) bufpool_free(pool->bufs[idx]);
}
Обратите внимание, что эта реализация упрощена для иллюстративных целей. Здесь нет политики сокращения пула, тогда как в реальном сценарии она, скорее всего, потребуется.
использование
Теперь вы должны иметь возможность писать свои обратные вызовы libuv:
void read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
/* ... */
bufpool_release(buf->base); /* Release the buffer */
}
Инициализация цикла:
uv_loop_t *loop = malloc(sizeof(*loop));
bufpool_t *pool = malloc(sizeof(*pool));
uv_loop_init(loop);
bufpool_init(pool);
loop->data = pool;
Операция:
uv_tcp_t *tcp = malloc(sizeof(*tcp));
uv_tcp_init(tcp);
/* ... */
uv_read_start((uv_handle_t *)tcp, alloc_cb, read_cb);
ОБНОВЛЕНИЕ (2 августа 2016 г.)
Также рекомендуется использовать адаптивную стратегию при выборке буфера в зависимости от запрошенного размера и возвращать объединенные буферы только тогда, когда запрашивается большой кусок данных (например, все операции чтения и длительные записи). Для других случаев (например, для большинства операций записи) возвращайте фиктивные буферы. Это поможет избежать бесполезной траты буферов пула и при этом сохранить приемлемую скорость выделения. Например:
void alloc_cb(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
int len = size; /* Requested buffer size */
void *ptr = bufpool_acquire(handle->loop->data, &len);
*buf = uv_buf_init(ptr, len);
}
void *bufpool_acquire(bufpool_t *pool, int *len) {
int size = *len;
if (size > DUMMY_BUF_SIZE) {
buf = bufpool_dequeue(pool);
if (buf) {
if (size > BUF_SIZE) *len = BUF_SIZE;
return buf;
}
size = DUMMY_BUF_SIZE;
}
buf = bufpool_alloc(0, size);
*len = buf ? size : 0;
return buf;
}
P.S. В этом фрагменте нет необходимости в buflen
и bufpool_dummy
.
person
neoxic
schedule
28.02.2015