标签:内存管理 linux kernel
接着上篇写,继续介绍zone allocator。上一篇介绍了周边,现在来看看它的全貌 --- 函数__alloc_pages()。
Kernel源代码里是这样注释函数__alloc_pages()的。其重要地位可见一斑。
1451 /* 1452 * This is the ‘heart‘ of the zoned buddy allocator. 1453 */
__alloc_pages()的工作模式很清晰:利用函数get_page_from_freelist()多次遍历zonelist中所有的zones。遍历时把关条件会逐渐放宽,其中还可能会启动内存回收等机制。
1454 struct page * fastcall 1455 __alloc_pages(gfp_t gfp_mask, unsigned int order, 1456 struct zonelist *zonelist) 1457 { 1458 const gfp_t wait = gfp_mask & __GFP_WAIT; 1459 struct zone **z; 1460 struct page *page; 1461 struct reclaim_state reclaim_state; 1462 struct task_struct *p = current; 1463 int do_retry; 1464 int alloc_flags; 1465 int did_some_progress; 1466 1467 might_sleep_if(wait); 1468 1469 if (should_fail_alloc_page(gfp_mask, order)) 1470 return NULL; 1471 1472 restart: 1473 z = zonelist->zones; /* the list of zones suitable for gfp_mask */ 1474 1475 if (unlikely(*z == NULL)) { 1476 /* 1477 * Happens if we have an empty zonelist as a result of 1478 * GFP_THISNODE being used on a memoryless node 1479 */ 1480 return NULL; 1481 } 1482 1483 page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order, 1484 zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET); 1485 if (page) 1486 goto got_pg;
第一次遍历,把关时要求相对较高:watermark选择了ALLOC_WMARK_LOW。这样可以尽量保护各个zone预留的空闲内存。
如果第一次遍历没能申请到内存,说明系统中空闲内存不多了。放宽一下把关条件,进行第二次遍历。
1499 for (z = zonelist->zones; *z; z++) 1500 wakeup_kswapd(*z, order); 1501 1512 alloc_flags = ALLOC_WMARK_MIN; 1513 if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait) 1514 alloc_flags |= ALLOC_HARDER; 1515 if (gfp_mask & __GFP_HIGH) 1516 alloc_flags |= ALLOC_HIGH; 1517 if (wait) 1518 alloc_flags |= ALLOC_CPUSET; 1519 1528 page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags); 1529 if (page) 1530 goto got_pg;
在进行第二次遍历之前,Kernel做了两件事:
1) 异步启动内存回收机制。内存回收机制是个大的topic,这里我们只需知道,该机制会释放出一些内存页面到buddy system中。
2) 调整分配标志 alloc_flags,放宽把关条件:
选择ALLOC_WMARK_MIN作为watermark。ALLOC_WMARK_MIN的watermark值(pages_min)比ALLOC_WMARK_LOW的watermark值(pages_low)数值小,选用更小的watermark可以更多的动用预留的空闲内存。
如果当前进程是实时优先级(real-time)进程且不是在中断上下文,或是这次内存申请不能被中断(__GFP_WAIT没有置位),则设置标志ALLOC_HARDER。
如果gfp_mask中__GFP_HIGH置位,则设置标志ALLOC_HIGH。
在上一篇博文里讲到,如果设置了ALLOC_HIGH 或 ALLOC_HARDER,zone_watermark_ok()中使用的阈值会进一步减少,这也就意味着把关条件放松,分配会更加aggressive。
这里有一点需要注意:GFP_ATOMIC的内存申请,会同时设置ALLOC_HARDER和ALLOC_HIGH。因为GFP_ATOMIC定义如下:
60 #define GFP_ATOMIC (__GFP_HIGH)
如果还是没能申请到内存,说明内存非常吃紧。此时,对于下面这种特殊情况,Kernel会特殊对待一下。
1534 rebalance: 1535 if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) 1536 && !in_interrupt()) { 1537 if (!(gfp_mask & __GFP_NOMEMALLOC)) { 1538 nofail_alloc: 1539 /* go through the zonelist yet again, ignoring mins */ 1540 page = get_page_from_freelist(gfp_mask, order, 1541 zonelist, ALLOC_NO_WATERMARKS); 1542 if (page) 1543 goto got_pg; 1544 if (gfp_mask & __GFP_NOFAIL) { 1545 congestion_wait(WRITE, HZ/50); 1546 goto nofail_alloc; 1547 } 1548 } 1549 goto nopage; 1550 }
如果当前环境不是中断上下文,并且当前进程设置了PF_MEMALLOC或TIF_MEMDIE,则会进行特殊对待。
如果是__GFP_NOMEMALLOC的请求,表示禁止使用为紧急情况预留的内存,这种情况下Kernel再无它法,只能返回NULL。
否则,Kernel会拿出家底,放手一搏,进行第三次遍历。这次遍历,Kernel使用了ALLOC_NO_WATERMARKS,这就意味着跳过把关函数zone_watermark_ok(),完全忽略watermark和lowmem_reserve的限制。这也是唯一能够动用全部的预留内存的地方。如果第三次遍历还是失败,苍天啊:
如果不是__GFP_NOFAIL的请求,则只能返回NULL。
如果是__GFP_NOFAIL的请求,想死又不让死,只能死抗着了。Kernel会进入一个死循环,不过每次循环之前会先等块设备层的写拥塞结束。
话说PF_MEMALLOC和TIF_MEMDIE到底表示啥东东呢?简单地讲,它们的出现一般表示当前的上下文是在回收内存。回收内存本身也是需要内存的,你先给我一点点空闲内存,我将回报给你更多的空闲内存。给我一滴水,我将还你一片海。所以它才有资格动用全部的预留内存。
如果不是上面这种特殊情况,Kernel还有一些手段可用,不过这些手段需要当前进程能够进入睡眠状态。
1552 /* Atomic allocations - we can‘t balance anything */ 1553 if (!wait) 1554 goto nopage; 1555 1556 cond_resched();
如果__GFP_WAIT没有置位,说明这次请求不允许被中断,那Kernel的那些手段就不能用了。此时只能返回NULL。
在拿出这些手段之前,Kernel先看看有没有其他人需要CPU。毕竟咱不能太自私,占着CPU太久。
1558 /* We now go into synchronous reclaim */ 1559 cpuset_memory_pressure_bump(); 1560 p->flags |= PF_MEMALLOC; 1561 reclaim_state.reclaimed_slab = 0; 1562 p->reclaim_state = &reclaim_state; 1563 1564 did_some_progress = try_to_free_pages(zonelist->zones, order, gfp_mask); 1565 1566 p->reclaim_state = NULL; 1567 p->flags &= ~PF_MEMALLOC; 1568 1569 cond_resched();
手段一:利用函数try_to_free_pages() 进行同步的内存回收。这个函数很耗时,而且可能会睡眠。
注意在调用函数try_to_free_pages()之前,Kernel设置了PF_MEMALLOC。一是表示接下来要进行内存回收操作了;二是防止函数try_to_free_pages()被递归调用,因为PF_MEMALLOC的设置会让Kernel特殊对待。
1571 if (order != 0) 1572 drain_all_local_pages();
手段二:如果申请的是多个内存页,则把未雨绸缪准备的per-cpu page frame cache中的内存页面还给buddy system。哎,不要怪Kernel抠门啊,实在是资源太紧张。
如果手段一成功释放了一些内存页面,则再来一次遍历(第三次)。
1574 if (likely(did_some_progress)) { 1575 page = get_page_from_freelist(gfp_mask, order, 1576 zonelist, alloc_flags); 1577 if (page) 1578 goto got_pg;
这次遍历使用了与第二次遍历相同的条件。
如果还是没能申请到内存,Kernel就要做个决定了,是放弃?是坚持?
1616 do_retry = 0; 1617 if (!(gfp_mask & __GFP_NORETRY)) { 1618 if ((order <= PAGE_ALLOC_COSTLY_ORDER) || 1619 (gfp_mask & __GFP_REPEAT)) 1620 do_retry = 1; 1621 if (gfp_mask & __GFP_NOFAIL) 1622 do_retry = 1; 1623 } 1624 if (do_retry) { 1625 congestion_wait(WRITE, HZ/50); 1626 goto rebalance; 1627 }
1)设置了__GFP_NORETRY,放弃,返回NULL。
2)没有设置__GFP_NORETRY,并且要分配的内存页块小于等于8页,或是设置了__GFP_REPEAT或__GFP_NOFAIL, 坚持!
选择坚持的方法就是,先等一下块设备层的写拥塞结束,然后从第二次遍历结束的地方重新开始。
如果手段一没能释放出任何页面,Kernel遇到big trouble了。这时会拿出手段三:大义灭亲,选择一个进程,杀掉其人,霸占其内存资源。
在杀人之前,先看两个标志:__GFP_FS和__GFP_NORETRY。如果__GFP_FS没有置位(不允许执行依赖于文件系统的操作),或是__GFP_NORETRY置位了(不允许重试),Kernel会立即放下屠刀,然后去思考前面“放弃还是坚持”的问题。
否则,Kernel就真要杀人了!
1579 } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) { 1580 if (!try_set_zone_oom(zonelist)) { 1581 schedule_timeout_uninterruptible(1); 1582 goto restart; 1583 } 1584 1585 /* 1586 * Go through the zonelist yet one more time, keep 1587 * very high watermark here, this is only to catch 1588 * a parallel oom killing, we must fail if we‘re still 1589 * under heavy pressure. 1590 */ 1591 page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order, 1592 zonelist, ALLOC_WMARK_HIGH|ALLOC_CPUSET); 1593 if (page) { 1594 clear_zonelist_oom(zonelist); 1595 goto got_pg; 1596 } 1597 1598 /* The OOM killer will not help higher order allocs so fail */ 1599 if (order > PAGE_ALLOC_COSTLY_ORDER) { 1600 clear_zonelist_oom(zonelist); 1601 goto nopage; 1602 } 1603 1604 out_of_memory(zonelist, gfp_mask, order); 1605 clear_zonelist_oom(zonelist); 1606 goto restart; 1607 }
在真的动刀杀人之前,Kernel再进行一次遍历(第三次)。不过这次遍历,Kernel选择了很严格的把关条件: ALLOC_WMARK_HIGH。所以这次遍历失败的可能性很大。
有人会说,这不是假仁慈吗?内存已经这么紧张了,你还定这么高的把关条件,注定要失败啊。要杀就杀,直接来吧。。。
其实这是Kernel的真仁慈。如果一个进程已经被其他人杀掉了,那么这次遍历就会成功,这样就能让一个无辜的生命幸免遇难。
Kernel还有另一个仁慈的表现。如果发现申请的内存页面大于8页,则直接返回NULL。因为这时即使杀掉一个进程,也不大可能会满足要求,何必要多牺牲一个生命呢。
好了,到了这个时候,命运是逃不过的了。Kernel召唤出杀手OOM,调用函数out_of_memory(),来杀掉一个进程,释放出其内存资源。然后回到第一次遍历之前,重新开始。
以上,就是函数__alloc_pages()的全貌。一次次的遍历,屡败屡战,衣带渐宽终不悔,为伊消得人憔悴。
本文出自 “内核部落格” 博客,请务必保留此出处http://richardguo.blog.51cto.com/9343720/1670665
Kernel那些事儿之内存管理(6) --- 衣带渐宽终不悔(下)
标签:内存管理 linux kernel
原文地址:http://richardguo.blog.51cto.com/9343720/1670665