На самом деле поведение распределения памяти, которое вы описываете, является общим для многих ядер ОС, и основная причина - это распределение физических страниц ядра.Как правило, ядро имеет один физический распределитель страниц, который используется для выделения страниц как для пространства ядра (включая страницы для DMA), так и для пространства пользователя.В пространстве ядра вам нужна непрерывная память, потому что дорого (для кода в ядре) отображать страницы каждый раз, когда они вам нужны.Например, на x86_64 это абсолютно бесполезно, потому что ядро может видеть все адресное пространство (в 32-битных системах ограничение 4G виртуального адресного пространства, поэтому обычно верхний 1G выделен для ядра, а нижний 3G для пользовательского пространства).
Ядро Linux использует buddy алгоритм для выделения страниц, так что выделение большего фрагмента занимает меньше итераций, чем выделение меньшего фрагмента (ну, меньшие фрагменты получаются путем разделения больших фрагментов).Более того, использование одного распределителя как для пространства ядра, так и для пространства пользователя позволяет ядру уменьшить фрагментацию.Представьте, что вы выделяете страницы для пользовательского пространства на 1 страницу за итерацию.Если пользовательскому пространству нужно N страниц, вы делаете N итераций.Что произойдет, если ядру понадобится непрерывная память?Как он может создать достаточно большой непрерывный блок, если вы украли 1 страницу из каждого большого блока и передали их в пространство пользователя?
[update]
На самом деле ядро выделяет континуумыблоки памяти для пользовательского пространства не так часто, как вы думаете.Конечно, он выделяет их при создании ELF-образа файла, когда он создает заголовок для чтения, когда пользовательский процесс читает файл, он создает их для операций IPC (конвейер, буферы сокетов) или когда пользователь передает флаг MAP_POPULATE в системный вызов mmap.Но обычно ядро использует «ленивую» схему загрузки страниц.Он предоставляет непрерывное пространство виртуальной памяти для пространства пользователя (когда пользователь делает malloc первый раз или делает mmap), но не заполняет пространство физическими страницами.Он распределяет страницы только тогда, когда происходит сбой страницы.То же самое верно, когда пользовательский процесс делает fork.В этом случае дочерний процесс будет иметь адресное пространство «только для чтения».Когда потомок изменяет некоторые данные, происходит сбой страницы, и ядро заменяет страницу в дочернем адресном пространстве новой (так что у родителя и потомка теперь разные страницы).Обычно в этих случаях ядро выделяет только одну страницу.
Конечно, существует большой вопрос фрагментации памяти.Пространство ядра всегда нуждается в непрерывной памяти.Если бы ядро выделяло страницы для пользовательского пространства из «случайных» физических мест, было бы гораздо сложнее получить большой кусок непрерывной памяти в ядре через некоторое время (например, после недели работы системы).В этом случае память будет слишком фрагментированной.
Для решения этой проблемы ядро использует схему readahead.Когда сбой страницы происходит в адресном пространстве какого-либо процесса, ядро выделяет и отображает более одной страницы (поскольку существует вероятность, что процесс будет считывать / записывать данные со следующей страницы).И, конечно, в этом случае он использует физически непрерывный блок памяти (если это возможно).Просто чтобы уменьшить потенциальную фрагментацию.