Операционная система UNIX - страница 69
. Правда, с помощью функций malloc(3C), calloc(3C) или realloc(3C) можно снова выделить и использовать эту память и снова освободить ее, но она не передается обратно ядру, а остается в пуле malloc(3C).
Для иллюстрации этого положения приведем небольшую программу, выделяющую и освобождающую память с помощью функций malloc(3C) и free(3C), соответственно. Контроль действительного значения брейк-адреса осуществляется с помощью системного вызова sbrk(2):
>#include
>#include
>main() {
> char *obrk;
> char *nbrk;
> char *naddr;
> /* Определим текущий брейк-адрес */
> obrk = sbrk(0);
> 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), таких как размер области, указатель на следующую область и т.п.
Создание и управление процессами
Работая в командной строке 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) {