уникальный номер группы != группе терминала).
Если процесс еще не имеет управляющего терминала (или уже его не имеет после
setpgrp), то он может сделать любой терминал (который он имеет право открыть) управ-
ляющим для себя. Первый же файл-устройство, являющийся интерфейсом драйвера термина-
лов, который будет открыт этим процессом, станет для него управляющим терминалом. Так
процесс может иметь каналы 0, 1, 2 связанные с одним терминалом, а прерывания полу-
чать с клавиатуры другого (который он сделал управляющим для себя).
Процесс регистрации пользователя в системе - /etc/getty (название происходит от
"get tty" - получить терминал) - запускается процессом номер 1 - /etc/init-ом - на
каждом из терминалов, зарегистрированных в системе, когда
- система только что была запущена;
- либо когда пользователь на каком-то терминале вышел из системы (интерпретатор
команд завершился).
В сильном упрощении getty может быть описан так:
void main(ac, av) char *av[];
{ int f; struct termio tmodes;
for(f=0; f < NOFILE; f++) close(f);
/* Отказ от управляющего терминала,
* основание новой группы процессов.
*/
setpgrp();
/* Первоначальное явное открытие терминала */
А. Богатырев, 1992-95 - 230 - Си в UNIX
/* При этом терминал av[1] станет упр. терминалом */
open( av[1], O_RDONLY ); /* fd = 0 */
open( av[1], O_RDWR ); /* fd = 1 */
f = open( av[1], O_RDWR ); /* fd = 2 */
// ... Считывание параметров терминала из файла
// /etc/gettydefs. Тип требуемых параметров линии
// задается меткой, указываемой в av[2].
// Заполнение структуры tmodes требуемыми
// значениями ... и установка мод терминала.
ioctl (f, TCSETA, &tmodes);
// ... запрос имени и пароля ...
chdir (домашний_каталог_пользователя);
execl ("/bin/csh", "-csh", NULL);
/* Запуск интерпретатора команд. Группа процессов,
* управл. терминал, дескрипторы 0,1,2 наследуются.
*/
}
Здесь последовательные вызовы open занимают последовательные ячейки в таблице откры-
тых процессом файлов (поиск каждой новой незанятой ячейки производится с начала таб-
лицы) - в итоге по дескрипторам 0,1,2 открывается файл-терминал. После этого деск-
рипторы 0,1,2 наследуются всеми потомками интерпретатора команд. Процесс init запус-
кает по одному процессу getty на каждый терминал, как бы делая
/etc/getty /dev/tty01 m &
/etc/getty /dev/tty02 m &
...
и ожидает окончания любого из них. После входа пользователя в систему на каком-то
терминале, соответствующий getty превращается в интерпретатор команд (pid процесса
сохраняется). Как только кто-то из них умрет - init перезапустит getty на соответст-
вующем терминале (все они - его сыновья, поэтому он знает - на каком именно терми-
нале).
6.6. Трубы и FIFO-файлы.
Процессы могут обмениваться между собой информацией через файлы. Существуют
файлы с необычным поведением - так называемые FIFO-файлы (first in, first out), веду-
щие себя подобно очереди. У них указатели чтения и записи разделены. Работа с таким
файлом напоминает проталкивание шаров через трубу - с одного конца мы вталкиваем дан-
ные, с другого конца - вынимаем их. Операция чтения из пустой "трубы" проиостановит
вызов read (и издавший его процесс) до тех пор, пока кто-нибудь не запишет в FIFO-
файл какие-нибудь данные. Операция позиционирования указателя - lseek() - неприме-
нима к FIFO-файлам. FIFO-файл создается системным вызовом
#include
#include
mknod( имяФайла, S_IFIFO | 0666, 0 );
где 0666 - коды доступа к файлу. При помощи FIFO-файла могут общаться даже неродст-
венные процессы.
Разновидностью FIFO-файла является безымянный FIFO-файл, предназначенный для
обмена информацией между процессом-отцом и процессом-сыном. Такой файл - канал связи
как раз и называется термином "труба" или pipe. Он создается вызовом pipe:
int conn[2]; pipe(conn);
Если бы файл-труба имел имя PIPEFILE, то вызов pipe можно было бы описать как
А. Богатырев, 1992-95 - 231 - Си в UNIX
mknod("PIPEFILE", S_IFIFO | 0600, 0);
conn[0] = open("PIPEFILE", O_RDONLY);
conn[1] = open("PIPEFILE", O_WRONLY);
unlink("PIPEFILE");
При вызове fork каждому из двух процессов достанется в наследство пара дескрипторов:
pipe(conn);
fork();
conn[0]----<---- ----<-----conn[1]
FIFO
conn[1]---->---- ---->-----conn[0]
процесс A процесс B
Пусть процесс A будет посылать информацию в процесс B. Тогда процесс A сделает:
close(conn[0]);
// т.к. не собирается ничего читать
write(conn[1], ... );
а процесс B
close(conn[1]);
// т.к. не собирается ничего писать
read (conn[0], ... );
Получаем в итоге:
conn[1]---->----FIFO---->-----conn[0]
процесс A процесс B
Обычно поступают еще более элегантно, перенаправляя стандартный вывод A в канал
conn[1]
dup2 (conn[1], 1); close(conn[1]);
write(1, ... ); /* или printf */
а стандартный ввод B - из канала conn[0]
dup2(conn[0], 0); close(conn[0]);
read(0, ... ); /* или gets */
Это соответствует конструкции
$ A | B
записанной на языке СиШелл.
Файл, выделяемый под pipe, имеет ограниченный размер (и поэтому обычно целиком
оседает в буферах в памяти машины). Как только он заполнен целиком - процесс, пишу-
щий в трубу вызовом write, приостанавливается до появления свободного места в трубе.
Это может привести к возникновению тупиковой ситуации, если писать программу неакку-
ратно. Пусть процесс A является сыном процесса B, и пусть процесс B издает вызов
wait, не закрыв канал conn[0]. Процесс же A очень много пишет в трубу conn[1]. Мы
получаем ситуацию, когда оба процесса спят:
A потому что труба переполнена, а процесс B ничего из нее не читает, так как ждет
окончания A;
B потому что процесс-сын A не окончился, а он не может окончиться пока не допишет
свое сообщение.
Решением служит запрет процессу B делать вызов wait до тех пор, пока он не прочитает
ВСЮ информацию из трубы (не получит EOF). Только сделав после этого close(conn[0]);
А. Богатырев, 1992-95 - 232 - Си в UNIX
процесс B имеет право сделать wait.
Если процесс B закроет свою сторону трубы close(conn[0]) прежде, чем процесс A
закончит запись в нее, то при вызове write в процессе A, система пришлет процессу A
сигнал SIGPIPE - "запись в канал, из которого никто не читает".
6.6.1. Открытие FIFO файла приведет к блокированию процесса ("засыпанию"), если в
буфере FIFO файла пусто. Процесс заснет внутри вызова open до тех пор, пока в буфере
что-нибудь не появится.
Чтобы избежать такой ситуации, а, например, сделать что-нибудь иное полезное в
это время, нам надо было бы опросить файл на предмет того - можно ли его открыть?
Это делается при помощи флага O_NDELAY у вызова open.
int fd = open(filename, O_RDONLY|O_NDELAY);
Если open ведет к блокировке процесса внутри вызова, вместо этого будет возвращено
значение (-1). Если же файл может быть немедленно открыт - возвращается нормальный
дескриптор со значением >=0, и файл открыт.
O_NDELAY является зависимым от семантики того файла, который мы открываем. К
примеру, можно использовать его с файлами устройств, например именами, ведущими к
последовательным портам. Эти файлы устройств (порты) обладают тем свойством, что
одновременно их может открыть только один процесс (так устроена реализация функции
open внутри драйвера этих устройств). Поэтому, если один процесс уже работает с пор-
том, а в это время второй пытается его же открыть, второй "заснет" внутри open, и
будет дожидаться освобождения порта close первым процессом. Чтобы не ждать - следует
открывать порт с флагом O_NDELAY.
#include
#include
/* Убрать больше не нужный O_NDELAY */
void nondelay(int fd){
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NDELAY);
}
int main(int ac, char *av[]){
int fd;
char *port = ac > 1 ? "/dev/term/a" : "/dev/cua/a";
retry: if((fd = open(port, O_RDWR|O_NDELAY)) < 0){
perror(port);
sleep(10);
goto retry;
}
printf("Порт %s открыт.\n", port);
nondelay(fd);
printf("Работа с портом, вызови эту программу еще раз!\n");
sleep(60);
printf("Все.\n");
return 0;
}
Вот протокол:
А. Богатырев, 1992-95 - 233 - Си в UNIX
su# a.out & a.out xxx
[1] 22202
Порт /dev/term/a открыт.
Работа с портом, вызови эту программу еще раз!
/dev/cua/a: Device busy
/dev/cua/a: Device busy
/dev/cua/a: Device busy
/dev/cua/a: Device busy
/dev/cua/a: Device busy
/dev/cua/a: Device busy
Все.
Порт /dev/cua/a открыт.
Работа с портом, вызови эту программу еще раз!
su#
6.7. Нелокальный переход.
Теперь поговорим про нелокальный переход. Стандартная функция setjmp позволяет
установить в программе "контрольную точку"[*], а функция longjmp осуществляет прыжок в
эту точку, выполняя за один раз выход сразу из нескольких вызванных функций (если
надо)[**]. Эти функции не являются системными вызовами, но поскольку они реализуются
машинно-зависимым образом, а используются чаще всего как реакция на некоторый сигнал,
речь о них идет в этом разделе. Вот как, например, выглядит рестарт программы по
прерыванию с клавиатуры:
#include
#include
jmp_buf jmp; /* контрольная точка */
/* прыгнуть в контрольную точку */
void onintr(nsig){ longjmp(jmp, nsig); }
main(){
int n;
n = setjmp(jmp); /* установить контрольную точку */
if( n ) printf( "Рестарт после сигнала %d\n", n);
signal (SIGINT, onintr); /* реакция на сигнал */
printf("Начали\n");
...
}
setjmp возвращает 0 при запоминании контрольной точки. При прыжке в контрольную
точку при помощи longjmp, мы оказываемся снова в функции setjmp, и эта функция возв-
ращает нам значение второго аргумента longjmp, в этом примере - nsig.
Прыжок в контрольную точку очень удобно использовать в алгоритмах перебора с
возвратом (backtracking): либо - если ответ найден - прыжок на печать ответа, либо -
если ветвь перебора зашла в тупик - прыжок в точку ветвления и выбор другой альтерна-
тивы. При этом можно делать прыжки и в рекурсивных вызовах одной и той же функции: с
более высокого уровня рекурсии в вызов более низкого уровня (в этом случае jmp_buf
лучше делать автоматической переменной - своей для каждого уровня вызова функции).
____________________
[*] В некотором буфере запоминается текущее состояние процесса: положение вершины
стека вызовов функций (stack pointer); состояние всех регистров процессора, включая
регистр адреса текущей машинной команды (instruction pointer).
[**] Это достигается восстановлением состояния процесса из буфера. Изменения, проис-
шедшие за время между setjmp и longjmp в статических данных не отменяются (т.к. они
не сохранялись).
А. Богатырев, 1992-95 - 234 - Си в UNIX
6.7.1. Перепишите следующий алгоритм при помощи longjmp.