Реферат: Администрирование локальных сетей
Схема с синхронными событиями очень распространена. Кассир сидит у кассы и ожидает, пока к нему в окошечко не заглянет клиент. Поезд периодически проезжает мимо светофора и останавливается, если горит красный. Функция Си пассивно "спит" до тех пор, пока ее не вызовут; однако она всегда готова выполнить свою работу (обслужить клиента). Такое ожидающее заказа (события) действующее лицо называется сервер. После выполнения заказа сервер вновь переходит в состояние ожидания вызова. Итак, если событие ожидается в специальном месте и в определенные моменты времени (издается некий вызов для ОПРОСА) - это синхронные события. Канонический пример - функция gets, которая задержит выполнение программы, пока с клавиатуры не будет введена строка. Большинство ожиданий внутри системных вызовов - синхронны. Ядро ОС выступает для программ пользователей в роли сервера, выполняющего сисвызовы (хотя и не только в этой роли - ядро иногда предпринимает и активные действия: передача процессора другому процессу через определенное время (режим разделения времени), убивание процесса при ошибке, и.т.п.).
Сигналы - это асинхронные события. Они приходят неожиданно, в любой момент времени - вроде телефонного звонка. Кроме того, их не требуется заказывать - сигнал процессу может поступить совсем без повода. Аналогия из жизни такова: человек сидит и пишет письмо. Вдруг его окликают посреди фразы - он отвлекается, отвечает на вопрос, и вновь продолжает прерванное занятие. Человек не ожидал этого оклика (быть может, он готов к нему, но он не озирался по сторонам специально). Кроме того, сигнал мог поступить когда он писал 5-ое предложение, а мог - когда 34-ое. Момент времени, в который произойдет прерывание, не фиксирован.
Сигналы имеют номера, причем их количество ограничено - есть определенный список допустимых сигналов. Номера и мнемонические имена сигналов перечислены в includeфайле <signal.h> и имеют вид SIGнечто. Допустимы сигналы с номерами 1..NSIG-1, где NSIG определено в этом файле. При получении сигнала мы узнаем его номер, но не узнаем никакой иной информации: ни от кого поступил сигнал, ни что от нас хотят. Просто "звонит телефон". Чтобы получить дополнительную информацию, наш процесс должен взять ее из другого известного места; например - прочесть заказ из некоторого файла, об имени которого все наши программы заранее "договорились". Сигналы процессу могут поступать тремя путями:
- От другого процесса, который явно посылает его нам вызовом
kill(pid, sig);
где pid - идентификатор (номер) процесса-получателя, а sig - номер сигнала. Послать сигнал можно только родственному процессу - запущенному тем же пользователем.
- От операционной системы. Система может посылать процессу ряд сигналов, сигнализирующих об ошибках, например при обращении программы по несуществующему адресу или при ошибочном номере системного вызова. Такие сигналы обычно прекращают наш процесс.
- От пользователя - с клавиатуры терминала можно нажимом некоторых клавиш послать сигналы SIGINT и SIGQUIT. Собственно, сигнал посылается драйвером терминала при получении им с клавиатуры определенных символов. Так можно прервать зациклившуюся или надоевшую программу.
Процесс-получатель должен как-то отреагировать на сигнал. Программа может:
- проигнорировать сигнал (не ответить на звонок);
- перехватить сигнал (снять трубку), выполнить какие-то действия, затем продолжить прерванное занятие;
- быть убитой сигналом (звонок был подкреплен броском гранаты в окно);
В большинстве случаев сигнал по умолчанию убивает процесс-получатель. Однако процесс может изменить это умолчание и задать свою реакцию явно. Это делается вызовом signal:
#include <signal.h>
void (*signal(int sig, void (*react)() )) ();
Параметр react может иметь значение:
SIG_IGN
сигнал sig будет отныне игнорироваться. Некоторые сигналы (например SIGKILL) невозможно перехватить или проигнорировать.
SIG_DFL
восстановить реакцию по умолчанию (обычно - смерть получателя). имя_функции Например
void fr(gotsig){ ..... } /* обработчик */
... signal (sig, fr); ... /* задание реакции */
Тогда при получении сигнала sig будет вызвана функция fr, в которую в качестве аргумента системой будет передан номер сигнала, действительно вызвавшего ее gotsig==sig. Это полезно, т.к. можно задать одну и ту же функцию в качестве реакции для нескольких сигналов:
... signal (sig1, fr); signal(sig2, fr); ...
После возврата из функции fr() программа продолжится с прерванного места. Перед вызовом функции-обработчика реакция автоматически сбрасывается в реакцию по умолчанию SIG_DFL, а после выхода из обработчика снова восстанавливается в fr. Это значит, что во время работы функции-обработчика может прийти сигнал, который убьет программу.
Приведем список некоторых сигналов; полное описание посмотрите в документации. Колонки таблицы: G - может быть перехвачен; D - по умолчанию убивает процесс (k), игнорируется (i); C - образуется дамп памяти процесса: файл core, который затем может быть исследован отладчиком adb; F - реакция на сигнал сбрасывается; S - посылается обычно системой, а не явно.
сигнал G D C F S смысл
SIGTERM + k - + - завершить процесс
SIGKILL - k - + - убить процесс
SIGINT + k - + - прерывание с клавиш
SIGQUIT + k + + - прерывание с клавиш
SIGALRM + k - + + будильник
SIGILL + k + - + запрещенная команда
SIGBUS + k + + + обращение по неверному
SIGSEGV + k + + + адресу
SIGUSR1, USR2 + i - + - пользовательские
SIGCLD + i - + + смерть потомка
- Сигнал SIGILL используется иногда для эмуляции команд с плавающей точкой, что происходит примерно так: при обнаружении "запрещенной" команды для отсутствующего процессора "плавающей" арифметики аппаратура дает прерывание и система посылает процессу сигнал SIGILL. По сигналу вызывается функция-эмулятор плавающей арифметики (подключаемая к выполняемому файлу автоматически), которая и обрабатывает требуемую команду. Это может происходить много раз, именно поэтому реакция на этот сигнал не сбрасывается.
- SIGALRM посылается в результате его заказа вызовом alarm() (см. ниже).
- Сигнал SIGCLD посылается процессу-родителю при выполнении процессом-потомком сисвызова exit (или при смерти вследствие получения сигнала). Обычно процессродитель при получении такого сигнала (если он его заказывал) реагирует, выполняя в обработчике сигнала вызов wait (см. ниже). По-умолчанию этот сигнал игнорируется.
- Реакция SIG_IGN не сбрасывается в SIG_DFL при приходе сигнала, т.е. сигнал игнорируется постоянно.
- Вызов signal возвращает старое значение реакции, которое может быть запомнено в переменную вида void (*f)(); а потом восстановлено.
- Синхронное ожидание (сисвызов) может иногда быть прервано асинхронным событием (сигналом), но об этом ниже.
Деления просесса
Системный вызов fork() (вилка) создает новый процесс: копию процесса, издавшего вызов. Отличие этих процессов состоит только в возвращаемом fork-ом значении:
0 - в новом процессе.
pid нового процесса - в исходном.
Вызов fork может завершиться неудачей если таблица процессов переполнена. Простейший способ сделать это:
main(){
while(1)
if( ! fork()) pause();
}
Одно гнездо таблицы процессов зарезервировано - его может использовать только суперпользователь (в целях жизнеспособности системы: хотя бы для того, чтобы запустить программу, убивающую все эти процессы-варвары).
Пайпы и FIFO-файлы.
Процессы могут обмениваться между собой информацией через файлы. Существуют файлы с необычным поведением - так называемые FIFO-файлы (first in, first out), ведущие себя подобно очереди. У них указатели чтения и записи разделены. Работа с таким файлом напоминает проталкивание шаров через трубу - с одного конца мы вталкиваем данные, с другого конца - вынимаем их. Операция чтения из пустой "трубы" проиостановит вызов read (и издавший его процесс) до тех пор, пока кто-нибудь не запишет в FIFOфайл какие-нибудь данные. Операция позиционирования указателя - lseek() - неприме- нима к FIFO-файлам. FIFO-файл создается системным вызовом
#include <sys/types.h>
#include <sys/stat.h>
mknod( имяФайла, S_IFIFO | 0666, 0 );
где 0666 - коды доступа к файлу. При помощи FIFO-файла могут общаться даже неродственные процессы.
Разновидностью FIFO-файла является безымянный FIFO-файл, предназначенный для обмена информацией между процессом-отцом и процессом-сыном. Такой файл - канал связи как раз и называется термином "труба" или pipe. Он создается вызовом pipe:
int conn[2]; pipe(conn);
Если бы файл-труба имел имя PIPEFILE, то вызов pipe можно было бы описать как
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]); процесс B имеет право сделать wait.
Если процесс B закроет свою сторону трубы close(conn[0]) прежде, чем процесс A закончит запись в нее, то при вызове write в процессе A, система пришлет процессу A сигнал SIGPIPE - "запись в канал, из которого никто не читает".
Нелокальный переход.
Теперь поговорим про нелокальный переход. Стандартная функция setjmp позволяет установить в программе "контрольную точку"*, а функция longjmp осуществляет прыжок в эту точку, выполняя за один раз выход сразу из нескольких вызванных функций (если надо)*. Эти функции не являются системными вызовами, но поскольку они реализуются машинно-зависимым образом, а используются чаще всего как реакция на некоторый сигнал, речь о них идет в этом разделе. Вот как, например, выглядит рестарт программы по прерыванию с клавиатуры:
#include <signal.h>
#include <setjmp.h>
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 лучше делать автоматической переменной - своей для каждого уровня вызова функции).
Разделяемая память
shmget создает новый сегмент разделяемой памяти или находит существующий сегмент с тем же ключом shmat подключает сегмент с указанным дескриптором к виртуальной памяти обращающегося процесса shmdt отключает от виртуальной памяти ранее подключенный к ней сегмент с указанным виртуальным адресом начала shmctl служит для управления параметрами, связанными с существующим сегментом После подключения сегмента разделяемой памяти к виртуальной памяти процесса, он может обращаться к соответствующим элементам памяти с использованием обычных машинных команд чтения и записи
shmid = shmget(key, size, flag);
- size определяет желаемый размер сегмента в байтах
- если в таблице разделяемой памяти находится элемент, содержащий заданный ключ, и права доступа не противоречат текущим характеристикам процесса, то значением системного вызова является дескриптор существующего сегмента
- реальный размер сегмента можно узнать с помощью системного вызова shmctl
- иначе создается новый сегмент с размером не меньше установленного в системе минимального размера сегмента разделяемой памяти и не больше установленного максимального размера
- создание сегмента не означает немедленного выделения под него основной памяти
- откладывается до выполнения первого системного вызова подключения сегмента к виртуальной памяти некоторого процесса
- при выполнении последнего системного вызова отключения сегмента от виртуальной памяти соответствующая основная память освобождается
virtaddr = shmat(id, addr, flags);
Страницы: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31