Смекни!
smekni.com

Администрирование локальных сетей (стр. 16 из 39)

сигнал 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_bufjmp; /* контрольная точка */

/* прыгнуть в контрольную точку */

void onintr(nsig){ longjmp(jmp, nsig); }

main(){

int n;

n = setjmp(jmp); /* установить контрольную точку */

if( n ) printf( "Рестарт после сигнала %d&bsol;n", n);

signal (SIGINT, onintr); /* реакция на сигнал */

printf("Начали&bsol;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);

  • id - это ранее полученный дескриптор сегмента
  • addr - желаемый процессом виртуальный адрес, который должен соответствовать началу сегмента в виртуальной памяти
  • virtaddr - реальный виртуальный адрес начала сегмента не обязательно совпадает со значением прямого параметра addr
  • если addr == 0, ядро выбирает наиболее удобный виртуальный адрес начала сегмента

shmdt(addr);