perror("exec не удался"); exit(1);
}
/* иначе это породивший процесс */
while((pid = wait(&status)) > 0 )
printf("Окончился сын pid=%d с кодом %d\n",
pid, status >> 8);
printf( "Больше нет сыновей\n");
____________________
[**] _cleanup() закрывает файлы, открытые fopen()ом, "вытряхая" при этом данные, на-
копленные в буферах, в файл. При аварийном завершении программы файлы все равно зак-
рываются, но уже не явно, а операционной системой (в вызове _exit). При этом содер-
жимое недосброшенных буферов будет утеряно.
____________________
[*][*] GNU - программы, распространяемые в исходных текстах из Free Software Founda-
А. Богатырев, 1992-95 - 226 - Си в UNIX
wait приостанавливает[*] выполнение вызвавшего процесса до момента окончания любого из
порожденных им процессов (ведь можно было запустить и нескольких сыновей!). Как
только какой-то потомок окончится - wait проснется и выдаст номер (pid) этого
потомка. Когда никого из живых "сыновей" не осталось - он выдаст (-1). Ясно, что
процессы могут оканчиваться не в том порядке, в котором их порождали. В переменную
status заносится в специальном виде код ответа окончившегося процесса, либо номер
сигнала, которым он был убит.
#include
#include
...
int status, pid;
...
while((pid = wait(&status)) > 0){
if( WIFEXITED(status)){
printf( "Процесс %d умер с кодом %d\n",
pid, WEXITSTATUS(status));
} else if( WIFSIGNALED(status)){
printf( "Процесс %d убит сигналом %d\n",
pid, WTERMSIG(status));
if(WCOREDUMP(status)) printf( "Образовался core\n" );
/* core - образ памяти процесса для отладчика adb */
} else if( WIFSTOPPED(status)){
printf( "Процесс %d остановлен сигналом %d\n",
pid, WSTOPSIG(status));
} else if( WIFCONTINUED(status)){
printf( "Процесс %d продолжен\n",
pid);
}
}
...
Если код ответа нас не интересует, мы можем писать wait(NULL).
Если у нашего процесса не было или больше нет живых сыновей - вызов wait ничего
не ждет, а возвращает значение (-1). В написанном примере цикл while позволяет дож-
даться окончания всех потомков.
В тот момент, когда процесс-отец получает информацию о причине смерти потомка,
паспорт умершего процесса наконец вычеркивается из таблицы процессов и может быть
переиспользован новым процессом. До того, он хранится в таблице процессов в состоя-
нии "zombie" - "живой мертвец". Только для того, чтобы кто-нибудь мог узать статус
его завершения.
Если процесс-отец завершился раньше своих сыновей, то кто же сделает wait и
вычеркнет паспорт? Это сделает процесс номер 1: /etc/init. Если отец умер раньше
процессов-сыновей, то система заставляет процесс номер 1 "усыновить" эти процессы.
init обычно находится в цикле, содержащем в начале вызов wait(), то есть ожидает
____________________
tion (FSF). Среди них - C++ компилятор g++ и редактор emacs. Смысл слов GNU - "gen-
erally not UNIX" - проект был основан как противодействие начавшейся коммерциализации
UNIX и закрытию его исходных текстов. "Сделать как в UNIX, но лучше".
[*] "Живой" процесс может пребывать в одном из нескольких состояний: процесс ожидает
наступления какого-то события ("спит"), при этом ему не выделяется время процессора,
т.к. он не готов к выполнению; процесс готов к выполнению и стоит в очереди к процес-
сору (поскольку процессор выполняет другой процесс); процесс готов и выполняется про-
цессором в данный момент. Последнее состояние может происходить в двух режимах -
пользовательском (выполняются команды сегмента text) и системном (процессом был издан
системный вызов, и сейчас выполняется функция в ядре). Ожидание события бывает только
в системной фазе - внутри системного вызова (т.е. это "синхронное" ожидание). Неак-
тивные процессы ("спящие" или ждущие ресурса процессора) могут быть временно откачаны
на диск.
А. Богатырев, 1992-95 - 227 - Си в UNIX
окончания любого из своих сыновей (а они у него всегда есть, о чем мы поговорим под-
робнее чуть погодя). Таким образом init занимается чисткой таблицы процессов, хотя
это не единственная его функция.
Вот схема, поясняющая жизненный цикл любого процесса:
|pid=719,csh
|
if(!fork())------->--------* pid=723,csh
| | загрузить
wait(&status) exec("a.out",...) <-- a.out
: main(...){ с диска
: |
:pid=719,csh | pid=723,a.out
спит(ждет) работает
: |
: exit(status) умер
: }
проснулся <---проснись!--RIP
|
|pid=719,csh
Заметьте, что номер порожденного процесса не обязан быть следующим за номером роди-
теля, а только больше него. Это связано с тем, что другие процессы могли создать в
системе новые процессы до того, как наш процесс издал свой вызов fork.
6.5.7. Кроме того, wait позволяет отслеживать остановку процесса. Процесс может
быть приостановлен при помощи посылки ему сигналов SIGSTOP, SIGTTIN, SIGTTOU,
SIGTSTP. Последние три сигнала посылает при определенных обстоятельствах драйвер
терминала, к примеру SIGTSTP - при нажатии клавиши CTRL/Z. Продолжается процесс
посылкой ему сигнала SIGCONT.
В данном контексте, однако, нас интересуют не сами эти сигналы, а другая схема
манипуляции с отслеживанием статуса порожденных процессов. Если указано явно, сис-
тема может посылать процессу-родителю сигнал SIGCLD в момент изменения статуса любого
из его потомков. Это позволит процессу-родителю немедленно сделать wait и немедленно
отразить изменение состояние процесса-потомка в своих внутренних списках. Данная
схема программируется так:
void pchild(){
int pid, status;
sighold(SIGCLD);
while((pid = waitpid((pid_t) -1, &status, WNOHANG|WUNTRACED)) > 0){
dorecord:
записать_информацию_об_изменениях;
}
sigrelse(SIGCLD);
/* Reset */
signal(SIGCLD, pchild);
}
...
main(){
...
/* По сигналу SIGCLD вызывать функцию pchild */
signal(SIGCLD, pchild);
...
главный_цикл;
}
Секция с вызовом waitpid (разновидность вызова wait), прикрыта парой функций
sighold-sigrelse, запрещающих приход сигнала SIGCLD внутри этой критической секции.
А. Богатырев, 1992-95 - 228 - Си в UNIX
Сделано это вот для чего: если процесс начнет модифицировать таблицы или списки в
районе метки dorecord:, а в этот момент придет еще один сигнал, то функция pchild
будет вызвана рекурсивно и тоже попытается модифицировать таблицы и списки, в которых
еще остались незавершенными перестановки ссылок, элементов, счетчиков. Это приведет к
разрушению данных.
Поэтому сигналы должны приходить последовательно, и функции pchild вызываться
также последовательно, а не рекурсивно. Функция sighold откладывает доставку сигнала
(если он случится), а sigrelse - разрешает доставить накопившиеся сигналы (но если их
пришло несколько одного типа - все они доставляются как один такой сигнал. Отсюда -
цикл вокруг waitpid).
Флаг WNOHANG - означает "не ждать внутри вызова wait", если ни один из потомков
не изменил своего состояния; а просто вернуть код (-1)". Это позволяет вызывать
pchild даже без получения сигнала: ничего не произойдет. Флаг WUNTRACED - означает
"выдавать информацию также об остановленных процессах".
6.5.8. Как уже было сказано, при exec все открытые файлы достаются в наследство
новой программе (в частности, если между fork и exec были перенаправлены вызовом dup2
стандартные ввод и вывод, то они останутся перенаправленными и у новой программы).
Что делать, если мы не хотим, чтобы наследовались все открытые файлы? (Хотя бы
потому, что большинством из них новая программа пользоваться не будет - в основном
она будет использовать лишь fd 0, 1 и 2; а ячейки в таблице открытых файлов процесса
они занимают). Во-первых, ненужные дескрипторы можно явно закрыть close в промежутке
между fork-ом и exec-ом. Однако не всегда мы помним номера дескрипторов для этой
операции. Более радикальной мерой является тотальная чистка:
for(f = 3; f < NOFILE; f++)
close(f);
Есть более элегантный путь. Можно пометить дескриптор файла специальным флагом,
означающим, что во время вызова exec этот дескриптор должен быть автоматически закрыт
(режим file-close-on-exec - fclex):
#include
int fd = open(.....);
fcntl (fd, F_SETFD, 1);
Отменить этот режим можно так:
fcntl (fd, F_SETFD, 0);
Здесь есть одна тонкость: этот флаг устанавливается не для структуры file - "открытый
файл", а непосредственно для дескриптора в таблице открытых процессом файлов (массив
флагов: char u_pofile[NOFILE]). Он не сбрасывается при закрытии файла, поэтому нас
может ожидать сюрприз:
... fcntl (fd, F_SETFD, 1); ... close(fd);
...
int fd1 = open( ... );
Если fd1 окажется равным fd, то дескриптор fd1 будет при exec-е закрыт, чего мы явно
не ожидали! Поэтому перед close(fd) полезно было бы отменить режим fclex.
6.5.9. Каждый процесс имеет управляющий терминал (short *u_ttyp). Он достается про-
цессу в наследство от родителя (при fork и exec) и обычно совпадает с терминалом, с
на котором работает данный пользователь.
Каждый процесс относится к некоторой группе процессов (int p_pgrp), которая
также наследуется. Можно послать сигнал всем процессам указанной группы pgrp:
kill( -pgrp, sig );
Вызов
kill( 0, sig );
посылает сигнал sig всем процессам, чья группа совпадает с группой посылающего
А. Богатырев, 1992-95 - 229 - Си в UNIX
процесса. Процесс может узнать свою группу:
int pgrp = getpgrp();
а может стать "лидером" новой группы. Вызов
setpgrp();
делает следующие операции:
/* У процесса больше нет управл. терминала: */
if(p_pgrp != p_pid) u_ttyp = NULL;
/* Группа процесса полагается равной его ид-у: */
p_pgrp = p_pid; /* new group */
В свою очередь, управляющий терминал тоже имеет некоторую группу (t_pgrp). Это значе-
ние устанавливается равным группе процесса, первым открывшего этот терминал:
/* часть процедуры открытия терминала */
if( p_pid == p_pgrp // лидер группы
&& u_ttyp == NULL // еще нет упр.терм.
&& t_pgrp == 0 ){ // у терминала нет группы
u_ttyp = &t_pgrp;
t_pgrp = p_pgrp;
}
Таким процессом обычно является процесс регистрации пользователя в системе (который
спрашивает у вас имя и пароль). При закрытии терминала всеми процессами (что бывает
при выходе пользователя из системы) терминал теряет группу: t_pgrp=0;
При нажатии на клавиатуре терминала некоторых клавиш:
c_cc[ VINTR ] обычно DEL или CTRL/C
c_cc[ VQUIT ] обычно CTRL/\
драйвер терминала посылает соответственно сигналы SIGINT и SIGQUIT всем процессам
группы терминала, т.е. как бы делает
kill( -t_pgrp, sig );
Именно поэтому мы можем прервать процесс нажатием клавиши DEL. Поэтому, если процесс
сделал setpgrp(), то сигнал с клавиатуры ему послать невозможно (т.к. он имеет свой