Читаем Операционная система UNIX полностью

 printf("Текущий брейк-адрес= 0x%x\n", obrk);

 /* Выделим 64 байта из хипа */

 naddr = malloc(64);

 /* Определим новый брейк-адрес */

 nbrk = sbrk(0);

 printf("Новый адрес области malloc= 0x%x,"

  " брейк-адрес= 0х%x (увеличение на %d байтов)\n",

  naddr, nbrk, nbrk — obrk);

 /* "Освободим" выделенную память и проверим, что произошло

    на самом деле */

 free(naddr);

 printf("free(0x%x)\n", naddr);

 obrk = sbrk(0);

 printf("Новый брейк-адрес= 0x%x (увеличение на %d байтов)\n",

  obrk, obrk — nbrk);

}

Откомпилируем и запустим программу:

$ a.out

Текущий брейк-адрес= 0x20ac0

malloc(64)

Новый адрес области malloc = 0x20ac8, брейк-адрес = 0x22ac0

(увеличение на 8192 байтов)

free(0x20ac8)

Новый брейк-адрес = 0x22ac0 (увеличение на 0 байтов)

$

Как видно из вывода программы, несмотря на освобождение памяти функцией free(3C), значение брейк-адреса не изменилось. Также можно заметить, что функция malloc(3C) выделяет больше памяти, чем требуется. Дополнительная память выделяется для необходимого выравнивания и для хранения внутренних данных malloc(3C), таких как размер области, указатель на следующую область и т.п.

<p>Создание и управление процессами</p>

Работая в командной строке shell вы, возможно, не задумывались, каким образом запускаются программы. На самом деле каждый раз порождается новый процесс, а затем загружается программа. В UNIX эти два этапа четко разделены. Соответственно система предоставляет два различных системных вызова: один для создания процесса, а другой для запуска новой программы.

Новый процесс порождается с помощью системного вызова fork(2):

#include

#include

pid_t fork(void);

Порожденный, или дочерний процесс, хотя это кажется странным, является точной копией процесса, выполнившего этот вызов, или родительского процесса. В частности, дочерний процесс наследует такие атрибуты родителя, как:

□ идентификаторы пользователя и группы,

□ переменные окружения,

□ диспозицию сигналов и их обработчики,

□ ограничения, накладываемые на процесс,

□ текущий и корневой каталог,

□ маску создания файлов,

□ все файловые дескрипторы, включая файловые указатели,

□ управляющий терминал.

Более того, виртуальная память дочернего процесса не отличается от образа родительского: такие же сегменты кода, данных, стека, разделяемой памяти и т.д. После возврата из вызова fork(2), который происходит и в родительский и в дочерний процессы, оба начинают выполнять одну и ту же инструкцию.

Легче перечислить немногочисленные различия между этими процессами, а именно:

□ дочернему процессу присваивается уникальный идентификатор PID.

□ идентификаторы родительского процесса PPID у этих процессов различны,

□ дочерний процесс свободен от сигналов, ожидающих доставки,

□ значение, возвращаемое системным вызовом fork(2) различно для родителя и потомка.

Последнее замечание требует объяснения. Как уже говорилось, возврат из функции fork(2) происходит как в родительский, так и в дочерний процесс. При этом возвращаемое родителю значение равно PID дочернего процесса, а дочерний, в свою очередь, получает значение, равное 0. Если fork(2) возвращает -1, то это свидетельствует об ошибке (естественно, в этом случае возврат происходит только в процесс, выполнивший системный вызов).

В возвращаемом fork(2) значении заложен большой смысл, поскольку оно позволяет определить, кто является родителем, а кто — потомком, и соответственно разделить функциональность. Поясним это на примере:

main() {

 int pid;

 pid = fork();

 if (pid == -1) {

  perror("fork");

  exit(1);

 }

 if (pid == 0) {

  /* Эта часть кода выполняется дочерним процессом */

  printf("Потомок\n");

 } else {

  /* Эта часть кода выполняется родительским процессом */

  printf("Родитель\n");

 }

}

Перейти на страницу:

Похожие книги