Skip to content

Commit 7845e1b

Browse files
Andy Rosscarlescufi
authored andcommitted
lib/mempool: Fix spurious -ENOMEM due to agressive latency control
The mempool operations need to be atomic, but because of latency concerns (the allocator is intended for use in an ISR) the locking was designed to be as minimal as possible. And it... mostly got it right. All the list handling was correctly synchronized. The merging of four child blocks into a parent block was atomic. The splitting of a block into four children was atomic. BUT: there was a moment between the allocation of a large block and the re-addition of its three unused children where the lock was being released. This meant that another context (e.g. an ISR that just fired, interrupting the existing call to k_mem_pool_alloc()) would see some memory "missing" that wasn't actually allocated. And if this happens to have been the top level block, it's entirely possible that the whole heap looks empty, even though the other allocator might have been doing only the smallest allocation! Fix that by making the "remove a block then add back the three children we don't use" into an atomic step. We can still relax the lock between levels as we split the subblocks further. (Finally, note that this trick allows a somewhat cleaner API as we can do our "retry due to race" step internally by walking back up the block size list instead of forcing our caller to do it via that weird -EAGAIN return value.) Fixes #11022 Signed-off-by: Andy Ross <[email protected]>
1 parent 2e21a95 commit 7845e1b

File tree

1 file changed

+32
-28
lines changed

1 file changed

+32
-28
lines changed

lib/mempool/mempool.c

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,11 @@ static inline void pool_irq_unlock(struct sys_mem_pool_base *p, int key)
144144
static void *block_alloc(struct sys_mem_pool_base *p, int l, size_t lsz)
145145
{
146146
sys_dnode_t *block;
147-
int key = pool_irq_lock(p);
148147

149148
block = sys_dlist_get(&p->levels[l].free_list);
150149
if (block != NULL) {
151150
clear_free_bit(p, l, block_num(p, block, lsz));
152151
}
153-
pool_irq_unlock(p, key);
154152

155153
return block;
156154
}
@@ -198,9 +196,7 @@ static void block_free(struct sys_mem_pool_base *p, int level,
198196
static void *block_break(struct sys_mem_pool_base *p, void *block, int l,
199197
size_t *lsizes)
200198
{
201-
int i, bn, key;
202-
203-
key = pool_irq_lock(p);
199+
int i, bn;
204200

205201
bn = block_num(p, block, lsizes[l]);
206202

@@ -215,17 +211,15 @@ static void *block_break(struct sys_mem_pool_base *p, void *block, int l,
215211
}
216212
}
217213

218-
pool_irq_unlock(p, key);
219-
220214
return block;
221215
}
222216

223217
int _sys_mem_pool_block_alloc(struct sys_mem_pool_base *p, size_t size,
224218
u32_t *level_p, u32_t *block_p, void **data_p)
225219
{
226-
int i, from_l;
227-
int alloc_l = -1, free_l = -1;
228-
void *data;
220+
int i, from_l, alloc_l = -1, free_l = -1;
221+
unsigned int key;
222+
void *data = NULL;
229223
size_t lsizes[p->n_levels];
230224

231225
/* Walk down through levels, finding the one from which we
@@ -255,26 +249,36 @@ int _sys_mem_pool_block_alloc(struct sys_mem_pool_base *p, size_t size,
255249
return -ENOMEM;
256250
}
257251

258-
/* Iteratively break the smallest enclosing block... */
259-
data = block_alloc(p, free_l, lsizes[free_l]);
260-
261-
if (data == NULL) {
262-
/* This can happen if we race with another allocator.
263-
* It's OK, just back out and the timeout code will
264-
* retry. Note mild overloading: -EAGAIN isn't for
265-
* propagation to the caller, it's to tell the loop in
266-
* k_mem_pool_alloc() to try again synchronously. But
267-
* it means exactly what it says.
268-
*
269-
* This doesn't happen for user mode memory pools as this
270-
* entire function runs with a semaphore held.
271-
*/
272-
return -EAGAIN;
273-
}
252+
/* Now walk back down the levels (i.e. toward bigger sizes)
253+
* looking for an available block. Start at the smallest
254+
* enclosing block found above (note that because that loop
255+
* was done without synchronization, it may no longer be
256+
* available!) as a useful optimization. Note that the
257+
* removal of the block from the list and the re-addition of
258+
* its the three unused children needs to be performed
259+
* atomically, otherwise we open up a situation where we can
260+
* "steal" the top level block of the whole heap, causing a
261+
* spurious -ENOMEM.
262+
*/
263+
key = pool_irq_lock(p);
264+
for (i = free_l; i >= 0; i--) {
265+
data = block_alloc(p, i, lsizes[i]);
274266

275-
for (from_l = free_l; from_l < alloc_l; from_l++) {
276-
data = block_break(p, data, from_l, lsizes);
267+
/* Found one. Iteratively break it down to the size
268+
* we need. Note that we relax the lock to allow a
269+
* pending interrupt to fire so we don't hurt latency
270+
* by locking the full loop.
271+
*/
272+
if (data != NULL) {
273+
for (from_l = i; from_l < alloc_l; from_l++) {
274+
data = block_break(p, data, from_l, lsizes);
275+
pool_irq_unlock(p, key);
276+
key = pool_irq_lock(p);
277+
}
278+
break;
279+
}
277280
}
281+
pool_irq_unlock(p, key);
278282

279283
*level_p = alloc_l;
280284
*block_p = block_num(p, data, lsizes[alloc_l]);

0 commit comments

Comments
 (0)