Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Labs_2007.doc
Скачиваний:
4
Добавлен:
01.03.2025
Размер:
538 Кб
Скачать
☆

3. Оборудование

Персональный компьютер типа IBM PC Pentium, опе­рационная система Linux, каталог ovp/lab3 c файлами labos3.doc (методические указания), check-access.c, copy.c, create-file.c, hexdump.c, print-symlink.c, read-file.c, write_journal_entry.c.

4. Задание на работу

Перекомпилировать и выполнить тестирование программ на языке Си, использующих системные вызовы для доступа к файловой системе.

5. Порядок выполнения работы

5.1. Загрузить операционную систему Linux и после появления приглашения login войти в систему под именем ovp.

5.2. Если при входе пользователя в систему автоматически не загрузилась графическая система X Window и графическая оболочка (KDE или Gnome), необходимо с помощью команды startx запустить выполнение графической среды.

5.3. Запустить эмулятор консольного окна. Перейти в каталог lab3 и ознакомиться с содержимым находящихся в нем файлов check-access.c, copy.c, create-file.c, hexdump.c, print-symlink.c, read-file.c, write_journal_entry.c, содержащих программы, написанные на языке Cи.

5.4. Посредством команд gcc откомпилировать и выполнить эти программы. На различных примерах проверить правильность работы программ.

6. Оформление отчета

Отчет должен содержать:

  • тексты программ;

  • примеры тестирования программ.

7. Контрольные вопросы

7.1. Какие системные и библиотечные функции ввода-вывода Вы знаете?

7.2. Как работает системная функция readv()?

7.3. Как обрабатываются ошибки, возникающие при выполнении системных функций?

7.4. Какие коды ошибок могут возникнуть при выполнении системного вызова access()?

7.5. Как создать файл посредством системного вызова open()?

7.6. Какая маска прав доступа к файлу установлена по умолчанию?

7.7. Чем отличаются форматы текстовых файлов в Linux и в DOS/Windows?

7.8. Какие поля содержит структура stat?

7.9. Какие системные вызовы можно использовать при работе с символическими ссылками?

7.10. Как используется системная функция sendfile()?

Лабораторная работа №4

РАБОТА С ПРОЦЕССАМИ В КОМАНДНОЙ

И ПРОГРАММНОЙ СРЕДАХ СИСТЕМЫ LINUX

1. Цель и задачи работы

Ознакомление с командами и системными функциями операционной системы Linux, предназначенными для работы с процессами, а также получение практических навыков программирования процессов на низком уровне.

2. Теоретические сведения

2.1. Мониторинг процессов

Выполняющийся экземпляр программы называется процессом. Каждый процесс в Linux помечается уникальным идентификатором PID (process identifier). Идентификаторы – это 16-разрядные числа, назначаемые последовательно по мере создания процессов.

У всякого процесса имеется также родительский процесс, а идентификатор родительского процесса PPID (parent PID) относится к атрибутам процесса. Таким образом, все процессы Linux организованы в виде древовидной иерархии, на вершине которой находится процесс-демон init.

Работая с переменными-идентификаторами процессов в программах, написанных на языках С и С++, следует объявлять эти переменные как имеющие тип pid_t (определен в файле <sys/types.h>). Программа может узнать идентификатор своего собственного процесса с помощью системного вызова getpid(), а идентификатор родительского процесса – с помощью вызова getppid(). В листинге 1 показано, как это сделать.

Листинг 1. (print-pid.c) Вывод идентификатора процесса

#include <stdio.h>

#include <unistd.h>

int main ()

{

printf ("The process id is %d\n", (int) getpid ());

printf ("The parent process id is %d\n", (int) getppid ());

return 0;

}

Обратите внимание, что при повторных запусках эта программа выдает разные значения pid, поскольку всякий раз запускается новый процесс. Однако родительский идентификатор у них оказывается одинаковым.

Команда ps отображает список процессов, работающих в данный момент в системе. С помощью опций можно указывать, о каких процессах и какую именно требуется получить информацию. GNU-версия этой команды, входящая в систему Linux, поддерживает опции в стиле трех разных типов UNIX. Опции в стиле Unix98 состоят из одного или нескольких символов, перед которым(и) должен стоять дефис. Опции в стиле BSD используются без дефиса. Опции, характерные только для GNU-версии, представляют собой слово, перед которым должно стоять два дефиса.

Будучи вызванной без аргументов, команда ps выводит список тех процессов, управляющим терминалом которых является ее собственный терминал. Более полный список можно получить с помощью команд

ps –e –o pid,ppid,command|more

ps –o pid,user,start_time,command|more

Полный список опций можно узнать на man-странице команды ps. Отметим, что имеются три предопределенных формата вывода: -f (полный листинг), -l (длинный листинг) и –j (вывод заданий). В зависимости от опций в выводе команды ps могут присутствовать следующие поля: имя владельца процесса (USER), идентификаторы процессов (PID и PPID), доля времени центрального процессора и доля реальной памяти (в процентах), использованные данным процессом (CPU и MEM), виртуальный размер процесса в килобайтах (VSZ), размер резидентного набора – количество 1К-страниц в памяти (RSS), время старта процесса (STIME), управляющий терминал процесса (TTY), статус процесса (S или STAT), приоритет планирования процесса (PRI) и значение nice – приращение приоритета (NI), сколько времени центрального процессора занял данный процесс (TIME), командная строка запуска программы, выполняемой данным процессом (CMD или COMMAND).

В поле статуса процесса могут стоять следующие значения:

  • R – выполнимый процесс, ожидающий момента, когда планировщик задач выделит ему очередной квант времени;

  • S – процесс "спит";

  • D – процесс находится в состоянии подкачки на диске;

  • T – остановленный процесс;

  • Z – процесс-зомби.

Рядом с указателем статуса могут стоять дополнительные символы из следующего набора:

  • W – процесс не имеет резидентных страниц;

  • < - высокоприоритетный процесс;

  • N – низкоприоритетный процесс;

  • L – процесс имеет страницы, заблокированные в памяти.

Для того, чтобы увидеть "лес" деревьев "предок-потомок", можно воспользоваться командой pstree.

Команды ps и pstree позволяют сделать как бы "моментальный снимок" процессов, запущенных в системе. В отличие от них команда top отображает в окне состояние процессов и их активность в реальном режиме времени. Содержимое окна обновляется каждые 5 секунд. Список процессов в окне может быть отсортирован с помощью следующих команд программы top:

  • N – сортировка по PID;

  • A – сортировка процессов по возрасту;

  • P – сортировка по использованию ЦП (по умолчанию);

  • M – сортировка процессов по использованию памяти;

  • T- сортировка по времени выполнения.

Кроме команд, определяющих режим сортировки, утилита top воспринимает еще ряд команд, которые позволяют управлять процессами в интерактивном режиме. С помощью команды-клавиши <K> можно завершить некоторый процесс ( его PID будет запрошен), а с помощью клавиши <R> можно переопределить значение nice для некоторого процесса. Таким образом, эти две команды утилиты top аналогичны командам kill и renice интерпретатора команд shell.

При работе в графической оболочке KDE можно использовать утилиту KDE Task Manager, запускаемую по команде kpm. Эта программа отображает рабочие процессы и строит графики загрузки процессора и использования памяти.

Система X Window содержит еще несколько инструментальных средств, позволяющих следить за использованием ресурсов процессами. Утилита xsysinfo показывает загрузку процессора, объем используемой физической памяти и объем используемой области подкачки. Еще одной утилитой, которая позволяет получить дополнительную информацию, является xosview.

2.2. Создание процессов

Существует два способа создания процессов. Первый из них относительно прост, но применяется редко, поскольку неэффективен и связан со значительным риском для безопасности системы. Этот способ основан на использовании библиотечной функции system(), которая позволяет выполнить команду интерпретатора shell в программной среде. Например, программа, представленная в листинге 2, выполняет команду ls –l /, отображающую содержимое корневого каталога.

Листинг 2. (system.c) Использование библиотечной функции system()

#include <stdlib.h>

int main ()

{

int return_value;

return_value = system ("ls -l /");

return return_value;

}

При выполнении функции system() запускается стандартный интерпретатор Bourne shell (/bin/sh) и ему передается команда для исполнения. Если интерпретатор не может быть запущен, функция system() возвращает значение 127, а в случае возникновения других ошибок возвращает значение –1.

Поскольку функция system() запускает интерпретатор команд, могут возникнуть определенные проблемы. Так, в большинстве UNIX-систем программа /bin/sh представляет собой символическую ссылку на другой интерпретатор. В Linux – это bash. Выполнение команды может привести к различным последствиям, которые зависят от многих факторов. Поэтому более предпочтительным является создание процессов на низком уровне программирования - с помощью системных вызовов fork() и exec().

В DOS и Windows API имеется семейство функций spawn(). Они принимают в качестве аргумента имя программы, создают новый экземпляр ее процесса и запускают его. В Linux нет функции, которая делала все это за один заход. Вместо этого имеется функция fork(), создающая дочерний процесс, который является точной копией родительского процесса, и семейство функций exec(), заставляющих созданный процесс перестать быть экземпляром родительской программы и превратиться в экземпляр другой программы.

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

Программа, представленная в листинге 3, имеет оператор if, в которой первую ветвь выполняет процесс-предок, а вторую – процесс-потомок.

Листинг 3. (fork.c) Использование fork() для создания дочернего процесса

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main ()

{

pid_t child_pid;

printf("the main program process id is %d\n",(int)getpid ());

child_pid = fork ();

if (child_pid != 0) {

printf ("this is the parent process, with id %d\n",

(int) getpid ());

printf ("the child's process id is %d\n", (int) child_pid);

}

else

printf ("this is the child process, with id %d\n",

(int) getpid ());

return 0;

}

Отметим, что родительский и дочерний процессы являются равноправными в борьбе за системные ресурсы и, в частности, за использование центрального процессора. Программа, представленная в листинге 4, демонстрирует это. В файле TEST мы будем от случая к случаю получать строки вида

aABbCcDdEe или AaBbcdCDEe

что говорит о том, что первым "проснуться" после fork() может любой из двух процессов. Если же опыт дает устойчиво строки, начинающиеся с одной и той же буквы – значит в данной реализации системы один из процессов все же запускается раньше. Но не стоит использовать этот эффект в своих программах - при переносе на другую систему его может не быть.

Листинг 4. (parent-child.c) Работа процессов после завершения вызова fork()

#include <stdio.h>

int pid, i, fd; char c;

main(){

fd = creat( "TEST", 0644);

if( !(pid = fork())){ /* дочерний процесс */

c = 'a';

for(i=0; i < 5; i++){

write(fd, &c, 1); c++; sleep(1);

}

printf("child %d exit\n", getpid());

exit(0);

}

/* else родительский процесс */

c = 'A';

for(i=0; i < 5; i++){

write(fd, &c, 1); c++; sleep(1);

}

printf("Parent %d process of %d exit\n",

getpid(), pid );

}

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

Функции, входящие в семейство exec(), немного отличаются друг от друга по своим возможностям и способу вызова.

  • Функции, в названии которых присутствует суффикс 'p' (execvp() и execlp()), принимают в качестве аргумента имя программы и ищут эту программу в каталогах, определяемых переменной среды PATH. Всем остальным функциям нужно передавать полное путевое имя программы.

  • Функции, в названии которых присутствует суффикс 'v' (execv(), execvp() и execve()), принимают список аргументов программы, упакованный в массив строковых указателей, который оканчивается NULL-указателем.

  • Функции с суффиксом 'l' (execl(), execlp() и execle()) принимают список аргументов, не упакованный в массив.

  • Функции, в названии которых присутствует суффикс 'e' (execve() и execle()), в качестве дополнительного аргумента принимают массив переменных среды. Этот массив содержит строковые указатели и оканчивается пустым указателем. Каждая строка должна иметь вид "ПЕРЕМЕННАЯ=значение".

Поскольку функция exec() заменяет одну программу другой, она не возвращает значение, если завершается успешно.

Список аргументов, передаваемых программе, аналогичен аргументам командной строки, указываемых при запуске программы в интерактивном режиме. Их тоже можно получить с помощью параметров argc и argv функции main новой программы дочернего процесса.

В листинге 5 показана программа, которая отображает содержимое корневого каталога с помощью команды ls, как и программа в листинге 2. Но на этот раз команда ls запускается не из интерпретатора, а напрямую, и ей передаются аргументы -l и / .

Листинг 5. (fork-exec.c) Совместное использование функций fork() и exec()

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

int spawn (char* program, char** arg_list)

{

pid_t child_pid;

/* Создание копии текущего процесса. */

child_pid = fork ();

if (child_pid != 0)

/* Это родительский процесс. */

return child_pid;

else {

/* Выполнение указанной программы. */

execvp (program, arg_list);

/* Функция execvp() возвращает значение только в случае

ошибки. */

fprintf (stderr, "an error occurred in execvp\n");

abort ();

}

}

int main ()

{

/* Список аргументов, передаваемых команде "ls". */

char* arg_list[] = {

"ls", /* argv[0] – имя программы. */

"-l",

"/",

NULL /* Список аргументов должен заканчиваться

указателем NULL. */

};

spawn ("ls", arg_list);

printf ("done with main program\n");

return 0;

}

2.3. Сигналы

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

Сигнал представляет собой специальное сообщение, посылаемое процессу. Когда процесс принимает сигнал, он немедленно обрабатывает его, прерывая выполнение текущей функции и даже текущей строки (оператора) программы.

Сигналы распознаются по номерам, но в программах для ссылки на сигналы пользуются символическими константами. В Linux эти константы определены в файле /usr/include/bits/signum.h (его не надо включать в программу, для этого есть файл <signal.h>).

В ответ на полученный сигнал процесс выполняет ряд действий в зависимости от типа сигнала. У каждого сигнала есть стандартный обработчик, определяющий, что произойдет с процессом, если он попытается проигнорировать сигнал. Для большинства сигналов можно указать собственную функцию-обработчик сигнала. В этом случае при поступлении сигнала выполнение программы приостанавливается, выполняется обработчик, а потом программа возобновляет свою работу.

Система Linux посылает процессам сигналы в случае возникновения определенных ситуаций. Например, сигналы SIGBUS (ошибка на шине), SIGSEGV (нарушение сегментации) и SIGFPE (ошибка операции с плавающей запятой) могут быть посланы процессу, пытающемуся выполнить неправильную операцию. По умолчанию эти сигналы приводят к завершению процесса и созданию дампа оперативной памяти.

Процесс может сам послать сигнал другому процессу. Чаще всего возникает необходимость завершить требуемый процесс с помощью сигнала SIGTERM или SIGKILL. Сигнал SIGTERM является запросом на завершение; процесс может его проигнорировать и продолжить свое выполнение. Сигнал SIGKILL вызывает немедленное безусловное уничтожение процесса и не может быть проигнорирован или обработан.

С помощью сигналов можно передавать команды выполняющимся программам. Для этого существуют "пользовательские" сигналы SIGUSR1 и SIGUSR2. Иногда в аналогичных целях применяется сигнал SIGHUP, с помощью которого можно заставить программу повторно прочитать свои файлы конфигурации.

Системный вызов, находящийся в состоянии ожидания какого-либо события (read, ждущий нажатия кнопки на клавиатуре, или wait, ждущий окончания процесса-потомка), может быть прерван сигналом. При этом системный вызов вернет значение "ошибка" (-1) и errno станет равным EINTR. Это позволяет писать системные вызовы с выставлением тайм­а-ута: если событие не происходит в течение заданного времени, то завершить ожидание и прервать системный вызов. Для этой цели используется вызов alarm(sec), заказывающий посылку сигнала SIGALRM нашей программе через целое число sec секунд (sec = 0, если отменяем заказ).

В листинге 6 приведен фрагмент программы , в которой устанавливается таймаут на время выполнения системного вызова sys_call().

Листинг 6. (timeout.c) Использование сигнала SIGALRM

#include <signal.h>

void (*oldaction)();

int alarmed;

/* прозвонил будильник */

void onalarm(nsig){ alarmed++; }

...

/* установить реакцию на сигнал */

oldaction = signal (SIGALRM, onalarm);

/* заказать будильник через TIMEOUT сек. */

alarmed = 0; alarm ( TIMEOUT /* sec */ );

sys_call(...); /* ждет события */

// если вызов прервал сигнал, то по сигналу

// будет вызвана реакция на него - onalarm

if(alarmed){

// событие так и не произошло.

// вызов прерван сигналом т.к. истекло время.

}else{

alarm(0); /* отменить заказ сигнала */

// событие произошло, сисвызов успел

// завершиться до истечения интервала TIMEOUT.

}

signal (SIGALRM, oldaction); /* восстановить реакцию */

. . .

Программа, представленная в листинге 7, показывает использование сигнала SIGALRM для реализации электронных часов.

Листинг 7. (clock.c) Простейшая реализация электронных часов

#include <signal.h>

#include <time.h>

#include <stdio.h>

void tick(nsig){

time_t tim; char *s;

signal (SIGALRM, tick);

alarm(3); time(&tim);

s = ctime(&tim);

s[ strlen(s)-1 ] = '\0'; /* обрубить '\n' */

fprintf(stderr, "\r%s", s);

}

main(){ tick(0);

for(;;) pause();

}

Известно, что при получении сигнала процесс может выбрать одно из трех возможных действий: игнорировать сигнал, перехватить и самостоятельно обработать сигнал, позволить действие по умолчанию. Текущее действие при получении сигнала называется диспозицией сигнала.

Простейшим интерфейсом к сигналам UNIX является библиотечная функция signal(), использующая семантику устаревших или ненадежных сигналов. Процесс при этом имеет весьма слабые возможности управления сигналами. Во-первых, процесс не может заблокировать сигнал, т.е. отложить получение сигнала на период выполнения критического участка кода. Во-вторых, каждый раз при получении сигнала его диспозиция устанавливается на действие по умолчание. Поэтому в обработчике требуется восстанавливать диспозицию.

Библиотечная функция signal() имеет следующее определение:

#include <signal.h>

void (*signal (int sig, void (*disp)(int)))(int);

Аргумент sig определяет сигнал, диспозицию которого нужно изменить. Аргумент disp определяет новую диспозицию сигнала, в качестве которой может быть указана определенная пользователем функция-обработчик или одно из следующих значений:

  • SIG_DFL – вызвать системный обработчик для данного сигнала, т.е. выполнить действие по умолчанию;

  • SIG_IGN – сигнал следует игнорировать.

В случае успешного завершения signal() возвращает предыдущую диспозицию – это может быть функция-обработчик сигнала типа

void (*oldaction)();

или системные значения SIG_DFL или SIG_IGN. Возвращаемое значение может быть использовано для восстановления диспозиции в случае необходимости.

В программе, приведенной в листинге 8, определена диспозиция для трех сигналов: SIGINT, SIGUSR1 и SIGUSR2. При получении сигнала SIGINT вызывается обработчик sig_hndlr(), при получении сигнала SIGUSR1 производится действие по умолчанию (процесс завершает работу), а сигнал SIGUSR2 игнорируется процессом. После установки диспозиции сигналов процесс запускает бесконечный цикл, в котором вызывается системная функция pause().

Листинг 8. (old-signal.c) Установка и проверка диспозиции сигналов

#include <signal.h>

/* Функция-обработчик сигнала */

static void sig_hndlr(int signo)

{

/* Восстановим диспозицию */

signal (SIGINT, sig_hndlr);

printf("Получен сигнал SIGINT\n");

}

main()

{

/* Установим диспозицию */

signal (SIGINT, sig_hndlr);

signal (SIGUSR1, SIG_DFL);

signal (SIGUSR2, SIG_IGN);

/* Бесконечный цикл */

while(1)

pause();

}

2.4. Надежные сигналы

Стандарт POSIX.1 определил новый набор функций управления сигналами, основанный на понятии набора сигналов. Набор сигналов описывается в программе переменной типа sigset_t, в которой каждый бит представляет один сигнал.

Вместо библиотечной функции signal() стандарт POSIX.1 определяет системную функцию sigaction(), позволяющую установить диспозицию сигналов и/или узнать ее текущее значение. Программа, представленная в листинге 9, является более современной реализацией программы, представленной в листинге 8.

Обычно процесс завершается одним из двух способов: либо выполняющаяся программа вызывает функцию exit(), либо функция main() заканчивается. Возможно также аварийное завершение процесса, в ответ на получение сигнала. Таковыми могут быть, например, сигналы SIGBUS, SIGSEGV и SIGFPE. Есть сигналы, явно запрашивающие прекращение работы процесса. Например, сигнал SIGINT посылается, когда пользователь нажимает <Ctrl+C>. Сигнал SIGTERM посылается процессу командой kill без аргумента-номера сигнала. Аналогичный сигнал посылается процессу child_pid при выполнении функции kill(child_pid, SIGTERM). Если программа вызывает функцию abort(), она посылает сама себе сигнал SIGABRT. Самый "могучий" из всех сигналов – SIGKILL: он приводит к безусловному уничтожению процесса и не может быть ни блокирован, ни обработан.

Листинг 9. (new-signal.c) Использование надежных сигналов

#include <signal.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

/* Вариант "надежной" функции signal() */

void (*mysignal (int signo, void (*hndlr)(int)))(int)

{

struct sigaction act, oact;

/* Установим маску сигналов */

act.sa_handler = hndlr;

sigemptyset(&act.sa_mask);

act.sa_flags = 0;

if(signo != SIGALRM)

act.sa_flags |= SA_RESTART;

/* Установим диспозицию */

if(sigaction(signo, &act, &oact) < 0)

return (SIG_ERR);

return (oact.sa_handler);

}

/* Функция-обработчик сигнала */

static void sig_hndlr(int signo)

{

/* Эта часть кода нам уже не нужна:

mysignal (SIGINT, sig_hndlr); */

printf("Получен сигнал SIGINT\n");

}

main()

{

/* Установим диспозицию */

mysignal (SIGINT, sig_hndlr);

mysignal (SIGUSR1, SIG_DFL);

mysignal (SIGUSR2, SIG_IGN);

/* Бесконечный цикл */

while(1)

pause();

}

Здесь функция mysignal() инициализирует поля структуры данных act типа sigaction и вызывает системную функцию sigaction().

2.5. Завершение процессов

Обычно нулевой код завершения процесса говорит об успешном завершении процесса. Необходимо знать правила формирования ненулевых кодов завершения. Несмотря на то, что тип параметра функции exit(), как и тип возвращаемого значения функции main(), равен int, система Linux записывает код завершения лишь в младший из четырех байтов. Если процесс завершается без получения сигнала, его код завершения находится в диапазоне от 0 до 127. Иначе код завершения равен 128 плюс номер сигнала.

После выполнения функции fork() родительский и дочерний процессы работают параллельно. В программах, приведенных в листингах 3 и 4, родительский процесс завершался, не дожидаясь завершения порожденного им процесса. Однако в системных программах родительскому процессу часто необходимо знать, как завершился дочерний процесс. Это можно сделать с помощью функций семейства wait().

Например, в функцию main() файла fork-exec.c можно добавить следующий фрагмент программы:

/*Wait for the child process to complete.*/

wait (&child_status);

if (WIFEXITED(child_status))

printf('the child process exited normally,with exit code %d \n',

WEXITSTATUS (child_status));

else {

printf('the child process exited abnormally,with exit code %d \n',

WTERMSIG (child_status));

Здесь функция wait() блокирует вызывающий процесс до тех пор, один из его дочерних процессов не завершится. Код состояния потомка возвращается через аргумент, являющийся указателем на целое число. В этом коде зашифрована различная информация о потомке, для извлечения которой используются макросы. Например, макрос WEXITSTATUS() возвращает код завершения дочернего процесса. Макрос WIFEXITED() позволяет узнать, как именно завершился процесс: обычным образом (с помощью функции exit() или оператора return функции main()) либо аварийно вследствие получения сигнала. В последнем случае макрос WTERMSIG() извлекает из кода завершения номер сигнала.

Наряду с функцией wait() используются и другие функции этого семейства. Функция waitpid() позволяет дождаться не любого, а конкретного дочернего процесса. Функция wait3() возвращает информацию о статистике использования центрального процессора завершившимся дочерним процессом. Функция wait4() позволяет задать дополнительную информацию о том, завершения каких процессов следует дождаться.

Функция wait() не только возвращает код завершения потомка, но и удаляет информацию о потомке из системных таблиц. Если же дочерний процесс завершается в отсутствии ожидающего его системного вызова wait(), он не удаляется из системы и переходит в состояние зомби. Удаление процесса-зомби (с помощью функции wait()) возлагается на родительский процесс. Если же родительский процесс завершится, не удалив из системы своих потомков, эту функцию выполнит процесс-демон init.

Именно этот вариант работы процессов представлен в программе на листинге 10. После компиляции и запуска программы можно перейти в другое консольное окно и посмотреть список активных процессов с помощью команды

ps –e –o pid,ppid,stat,cmd

Мы обнаружим в списке активных процессов и родительский, и дочерний процессы, причем последний будет обозначен как <defunct> (несуществующий), а его код состояния равен Z, т.е. zombie.

Листинг 10. (zombie.c) Создание процесса-зомби

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

int main ()

{

pid_t child_pid;

/* Создание дочернего процесса. */

child_pid = fork ();

if (child_pid > 0) {

/* Это родительский процесс – засыпает на 1 минуту. */

sleep (60);

}

else {

/* Это дочерний процесс – немедленно завершает работу. */

exit (0);

}

return 0; /* Завершение родительского процесса */

}

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]