---
title: "Сегментация и фрагментация"
description: "В главе про base-and-bounds у процесса была одна пара регистров: base (где в RAM начинается его память) и bounds (её размер). Физический адрес = base + виртуальный. Схема быстрая…"
canonical: https://graphlms.ru/os/book/segmentation
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Сегментация и фрагментация

> Источник: https://graphlms.ru/os/book/segmentation — курс «Операционные системы», GraphLMS.

## Проблема base-and-bounds

В главе про [base-and-bounds](/os/book/address-translation) у процесса была одна
пара регистров: `base` (где в RAM начинается его память) и `bounds` (её размер).
Физический адрес = `base + виртуальный`. Схема быстрая, но она кладёт в RAM всё
адресное пространство целиком, включая пустоту между кучей и стеком.

Возьмём маленькое адресное пространство на 16 KB:

```text
  Виртуальное адресное пространство (16 KB)

   0 KB ┌──────────────┐
        │ код (1 KB)   │
   1 KB ├──────────────┤
        │  (пусто)     │
   4 KB ├──────────────┤
        │ куча (2 KB)  │  растёт к большим адресам
   6 KB ├──────────────┤
        │              │
        │   пусто      │
        │   (8 KB)     │
        │              │
  14 KB ├──────────────┤
        │ стек (2 KB)  │  растёт к меньшим адресам
  16 KB └──────────────┘
```

Используется 5 KB, но base-and-bounds займёт в RAM все 16 KB. Для реального
64-битного процесса пропорция ещё хуже: используется несколько мегабайт из
терабайтов адресов. Нужен способ не класть пустоту в физическую память.

## Идея: пара base/bounds на каждую область

Адресное пространство состоит из нескольких логически разных кусков: код, куча,
стек. Дадим **каждому** свою пару base/bounds. Такой кусок называется **сегментом**
(segment): непрерывная область виртуальной памяти с одним назначением.

Теперь в RAM кладутся только сами сегменты, каждый в любое свободное место:

```text
  Физическая память

   0 KB ┌──────────────┐
        │     ОС       │
  16 KB ├──────────────┤
        │ стек         │  2 KB
  18 KB ├──────────────┤
        │ свободно     │
  28 KB ├──────────────┤
        │ код          │  1 KB
  29 KB ├──────────────┤
        │ свободно     │
  34 KB ├──────────────┤
        │ куча         │  2 KB
  36 KB ├──────────────┤
        │ свободно     │
        └──────────────┘
```

Процесс занимает в RAM 1 + 2 + 2 = 5 KB вместо 16. Таблица сегментов, которую
держит процессор (в регистрах) для текущего процесса:

| Сегмент | Номер | base | Размер | Растёт |
|---|---|---|---|---|
| Код | 00 | 28 KB | 1 KB | к большим адресам |
| Куча | 01 | 34 KB | 2 KB | к большим адресам |
| Стек | 11 | 18 KB (верхний край) | 2 KB | к меньшим адресам |

У стека в качестве base указан **верхний** край, потому что стек растёт вниз; ниже
разберём, как это считается.

## Как по адресу понять сегмент

Процессор получил виртуальный адрес. Как узнать, к какому сегменту он относится?
Простой приём: старшие биты адреса — номер сегмента, остальные — **смещение**
(offset) внутри сегмента.

Адрес у нас 14-битный (2¹⁴ = 16 KB). Отдадим 2 старших бита под номер сегмента и
12 младших под смещение. Тогда один сегмент может быть не больше 2¹² = 4 KB, а
адресное пространство делится на четыре четверти: 0–4 KB (сегмент 00), 4–8 KB (01),
8–12 KB (10, не используется) и 12–16 KB (11).

```text
  13 12 │ 11 10  9  8  7  6  5  4  3  2  1  0
 ┌──────┼───────────────────────────────────┐
 │ SEG  │            OFFSET                 │
 └──────┴───────────────────────────────────┘
```

### Пример: адрес в куче

Программа читает виртуальный адрес 4200.

```text
4200 = 4096 + 104 = 01 0000 0110 1000
                    ── ──────────────
                    SEG=01  OFFSET=104

1. SEG = 01 → куча: base = 34 KB, размер 2 KB
2. проверка: 104 < 2048? да
3. физический адрес = 34 KB + 104 = 34816 + 104 = 34920
```

А адрес 7000 = 4096 + 2904 даст смещение 2904, больше размера кучи 2048.
Процессор не будет считать физический адрес, а сгенерирует исключение, и ядро ОС
пошлёт процессу `SIGSEGV`. Название segmentation fault пришло именно отсюда:
«нарушение границ сегмента». Слово осталось, хотя сегментацию в этом виде давно не
используют.

### Пример: адрес в стеке

Стек растёт вниз: его «начало» — верхний край, виртуальный адрес 16 KB, которому
соответствует физический 18 KB. Поэтому для стека смещение считают от верхнего края
сегмента, со знаком минус.

Программа читает виртуальный адрес 15 KB = 15360:

```text
15360 = 11 1100 0000 0000
        ── ──────────────
        SEG=11  OFFSET=3072

1. SEG = 11 → стек: верхний край 18 KB, размер 2 KB, растёт вниз
2. отрицательное смещение = OFFSET − максимальный размер сегмента
                          = 3072 − 4096 = −1024
3. проверка: |−1024| ≤ 2048? да
4. физический адрес = 18 KB − 1024 = 18432 − 1024 = 17408 (17 KB)
```

Проверка здравым смыслом: адрес 15 KB на 1 KB ниже верхнего края стека (16 KB), и
физический результат на 1 KB ниже верхнего края сегмента в RAM (18 KB).

А адрес 13 KB = 13312 тоже попадает в сегмент 11, но со смещением 1024, то есть
отрицательным −3072. Это за пределами 2 KB стека: `SIGSEGV`. Для этого в таблице
сегментов и хранят бит направления роста.

## Права доступа и общие сегменты

Раз у каждого сегмента своя строка в таблице, туда легко добавить **биты прав**:
чтение, запись, исполнение. Код помечают «чтение и исполнение, без записи», и
программа не может случайно переписать свои инструкции.

Права открывают и **совместное использование** (sharing). Если десять процессов
запускают один бинарник, их сегменты кода одинаковы. Можно указать у всех base на
одну физическую копию кода с правами «только чтение». Так же одна копия
разделяемой библиотеки в RAM обслуживает много процессов; в современных системах
это делается страницами, но идея та же.

## Что делает ОС

Железо переводит адреса, а ОС отвечает за остальное:

- при переключении контекста сохраняет и загружает регистры сегментов процесса;
- когда куча растёт (`malloc` попросил ещё памяти через `brk`, см.
  [API памяти](/os/book/memory-api)), увеличивает размер сегмента кучи, если рядом в
  RAM есть место;
- ищет в физической памяти свободное место под новые сегменты.

Последний пункт и оказывается главной бедой.

## Внешняя фрагментация

Сегменты разного размера. Когда в памяти постоянно размещают и освобождают куски
разного размера, свободное место дробится на дыры. **Внешняя фрагментация**
(external fragmentation): свободной памяти в сумме достаточно, но она разбита на
несмежные куски, и непрерывный большой запрос не помещается.

```text
ДО: память плотно занята             ПОСЛЕ: A и C освободились

 0 KB ┌──────────────┐                0 KB ┌──────────────┐
      │ сегмент A 8K │                     │ свободно 8K  │
 8 KB ├──────────────┤                8 KB ├──────────────┤
      │ сегмент B 8K │                     │ сегмент B    │
16 KB ├──────────────┤               16 KB ├──────────────┤
      │ сегмент C 8K │                     │ свободно 8K  │
24 KB ├──────────────┤               24 KB ├──────────────┤
      │ сегмент D 8K │                     │ сегмент D    │
32 KB └──────────────┘               32 KB └──────────────┘

Свободно всего 16 KB, но запрос на сегмент 12 KB не помещается:
ни одна дыра не даёт 12 KB подряд.
```

Аналогия — парковка: свободно пять мест, но все по одному между машинами, и
автобусу, которому нужны три места подряд, встать некуда.

## Компактизация и её цена

Прямое лечение — **компактизация** (compaction): остановить процессы, сдвинуть
живые сегменты вплотную друг к другу, собрать свободное место в одну большую дыру и
обновить регистры base.

```text
 0 KB ┌──────────────┐
      │ сегмент B    │  переехал, base обновлён
 8 KB ├──────────────┤
      │ сегмент D    │  переехал, base обновлён
16 KB ├──────────────┤
      │ свободно 16K │  теперь одна дыра, запрос 12 KB влезает
32 KB └──────────────┘
```

Цена высокая: нужно физически скопировать мегабайты или гигабайты памяти, и пока
идёт копирование, процессы стоят. Делать это часто нельзя. Можно аккуратнее выбирать
дыру под новый сегмент (first fit, best fit и другие стратегии из главы
[управление свободной памятью](/os/book/free-space)), но при кусках произвольного
размера фрагментация всё равно накапливается.

## Почему перешли к страницам

Корень проблемы — **переменный размер** кусков. Если резать память на куски
**одинакового** размера, любая свободная клетка подойдёт под любой запрос, и
внешней фрагментации не будет. Этот подход называется **страничной организацией**
(paging), ей посвящена [следующая глава](/os/book/paging).

Сегментация в x86-64 почти выключена: в 64-битном режиме процессор игнорирует base
и размеры основных сегментов и считает память одним плоским пространством. Остались
два регистра, FS и GS, у которых работает только base. Ими пользуются для данных,
локальных для потока: например, glibc хранит через FS адрес области thread-local
storage текущего потока.

| Свойство | base-and-bounds | Сегментация | Страницы (дальше) |
|---|---|---|---|
| Что хранит процессор | 1 пара регистров | пара на сегмент | указатель на таблицу страниц |
| Пустота адресного пространства в RAM | да | нет | нет |
| Размер кусков | весь процесс | переменный | фиксированный |
| Внешняя фрагментация | есть | есть | нет |
| Права на области | нет | да | да |
| Общая память | трудно | легко | легко |

> Квиз по разделу: см. вопросы в конце документа или на https://graphlms.ru/os/book/segmentation

## Частые ошибки

- Путать внешнюю и внутреннюю фрагментацию. Внешняя — дыры **между** выделенными
  кусками; внутренняя — неиспользуемый остаток **внутри** выделенного куска.
- Думать, что сегменты в RAM должны лежать в том же порядке, что и в виртуальном
  пространстве. Каждый лежит где угодно, порядок не важен.
- Считать смещение для стека так же, как для кучи. Стек растёт вниз, смещение
  отсчитывается от верхнего края.

## Проверьте себя

1. Переведите виртуальный адрес 100 по таблице сегментов из главы.
2. Переведите адрес 6000. Что произойдёт?
3. Переведите адрес 15872 (15,5 KB).
4. Почему компактизацию нельзя делать при каждом освобождении памяти?

Ответы. (1) SEG = 00 (код), смещение 100 < 1024, физический адрес 28 KB + 100 =
28772. (2) 6000 = 4096 + 1904: SEG = 01 (куча), смещение 1904 < 2048, физический
34816 + 1904 = 36720. Обращение законно. (3) 15872 = 12288 + 3584: SEG = 11,
отрицательное смещение 3584 − 4096 = −512, |−512| ≤ 2048, физический 18432 − 512 =
17920. (4) Она копирует память и останавливает процессы; это слишком дорого.

## Что спрашивают на собесе

- Какую проблему base-and-bounds решает сегментация.
- Как из виртуального адреса достаётся номер сегмента и смещение; умейте разобрать
  конкретный адрес на биты.
- Что такое внешняя фрагментация и чем она отличается от внутренней.
- Откуда название segmentation fault.
- Почему отказались от сегментации в пользу страниц.
- Чем стек особенный при сегментации.

## Вопросы для самопроверки

**Квиз «Сегментация и фрагментация»**

1. Зачем сегментация даёт каждому куску (код/куча/стек) свою пару base/bounds вместо одной на процесс?
   - Чтобы не класть в физическую память пустую дыру между кучей и стеком (верно)
   - Чтобы процессор работал быстрее
   - Чтобы процессы могли видеть память друг друга
   - Чтобы убрать внешнюю фрагментацию
   - Пояснение: Главный выигрыш сегментации: дыра между кучей и стеком из виртуального пространства больше не занимает физическую RAM. Кладём только реальные сегменты. Внешнюю фрагментацию сегментация, наоборот, создаёт, а не убирает.
2. Виртуальный адрес 14-битный: 2 старших бита — номер сегмента, 12 младших — смещение. Адрес 4200 = 01 0000 0110 1000. Какое смещение внутри сегмента?
   - Ответ: 104
   - Пояснение: Младшие 12 бит 0000 0110 1000 в десятичном виде = 104. Старшие биты 01 указывают на сегмент (куча). Физический адрес был бы base_кучи + 104.
3. Что такое внешняя фрагментация?
   - Внутри выделенного блока остаётся неиспользуемый хвост
   - Память закончилась полностью
   - Сегмент вышел за свою границу bounds
   - Свободная память раздроблена на мелкие несмежные куски, и большой запрос не влезает, хотя суммарно места хватает (верно)
   - Пояснение: Внешняя фрагментация: суммарно свободного места хватает, но оно раскидано мелкими дырами, и непрерывный большой кусок выделить негде. Аналогия — парковка с одиночными свободными местами, куда не встанет автобус.
4. Почему от сегментации в итоге перешли к страницам?
   - Страницы не требуют таблиц
   - Сегменты переменного размера дают внешнюю фрагментацию, а компактизация дорогая; страницы фиксированного размера убирают фрагментацию (верно)
   - Страницы быстрее считают адрес
   - Сегментация не поддерживает защиту памяти
   - Пояснение: Корень проблемы — куски переменного размера: дыры разной величины, большой запрос не влезает. Компактизация требует останова и копирования RAM. Страницы одинакового размера делают дыры взаимозаменяемыми и убирают внешнюю фрагментацию.
5. Какие из утверждений про сегментацию верны?
   - Выход смещения за bounds порождает segmentation fault (верно)
   - К каждому сегменту можно добавить биты прав (r/w/x) и разделять его между процессами (верно)
   - Сегменты в физической памяти обязаны лежать подряд в том же порядке, что и в виртуальном
   - Номер сегмента берётся из старших бит виртуального адреса (верно)
   - Пояснение: Старшие биты — номер сегмента; выход за bounds — это сегфолт; права доступа и разделяемая память — естественные бонусы. Неверно про порядок: сегменты лежат вразнобой в любых свободных местах RAM — в этом и суть.
