Лекция 13 (03.12.20) - chrislvt/OS GitHub Wiki
Сигналы были описаны в методе ко второй лабе по UNIX
(5 программа).
(Внешнее по отношению к процессу асинхронное событие)
Сигналы в UNIX соответствуют каким-либо событиям. Механизм сигналов позволяет процессам реагировать на события, причем эти события могут происходить как вне процесса, так и внутри; как правило, получение процессом сигнала указывает ему на необходимость завершиться. Важнейшим событием в системе является завершение процесса. Несмотря на то, что в системе определены реакции на определённые события процессов, процесс может сам определить собственную реакцию на получаемый сигнал. Например, получив сигнал, процесс может его проигнорировать, выполнить стандартные действия, связанные с данным сигналом или может вызвать собственный обработчик сигнала, то есть по-своему отреагировать на полученный сигнал.
Сигналы описаны в библиотеке <signal.h>
В классическом UNIX определено не более 20 сигналов.
#define NSIG 20
- так описываются сигналы.
Я перечислю несколько наиболее важных сигналов. Все сигналы есть в методичке к 2 лабе по UNIX, но я хочу обратить внимание на такие важные сигналы, как:
#define SIGHUP 1 // Разрыв связи с терминалом
// В UNIX сигналы обозначаются большими буквами - это принятый способ обозначения
// предописанных макросов.
// Так же есть числовой ID.
#define SIGINT 2 // ctrl + C
#define SIGKILL 9 // (я не буду комментировать)
#define SIGSEGV 11 // нарушение сегментации
// Нужно обратить внимание, почему тут есть такая ошибка, когда мы обсуждали,
// собственно выполняется страничное преобразование.
// Умеем размечать сегмент кода, данных, стека. Эта ошибка означает,
//что в системе выполняется преобразование страницами(?) по запросу
// Мы с вами говорили, что системы для защиты адрес пространств
// проверяет выход процесса за адрессное пространство
#define SIGPIPE 13 // Запись в канал есть, чтения нет
#define SIGALRM 14 // Сигнал побудки (будильник) (лол, у нее неверно было)
#define SIGTERM 15 // программное прерывание
// Когда используется kill в терминале - возникает програмное прерывание
//(посылается сигнал номер 15).
// Есть 2 сигнала (SIGUSR1, SIGUSR2),
// которые предназначены непосредственно для пользователя.
#define SIGCLD 18 // завершился процесс потомок
#define SIGPWR 19 // авария питания
#define SIG_DFL(int((*)())) 0 // все установки будут использоваться по умолчанию
#define SIG_IGN(int((*)())) 1 // приписывает игнорирование сигнала.
В методичке дан более широкий список сигналов. Соотвественно надо понимать, что не везде существет обратная совместимость. Может быть свой расширенный набор сигналов.
Средствами посылки и обработки сигналав UNIX служат два системных вызова: kill, signal
int kill(int pid, int sig); // два формальных параметра: идентификатор процесса и сигнал.
int pid
не обязательно должен быть равен идентификатору процесса. Например, если параметр pid
<= 1, то сигнал sig
будет послан к группе процессов.
Если pid
== 0, то sig
будет послан всем процессам с id группы совпадающими с id группы процесса, который осуществил системный вызов kill
, кроме процессов с pid
0 и 1.
Показательный пример вызова kill
:
kill(getpid(), SIGALARM) // означает, что сигнал побудки будет послан самому процессу, вызвавшему kill
void (*signal(int sig, void (*handler()(int)))(int)
Для того, чтобы замаскировать сигнал, необходимо в программе вызвать signal(SIGINT, SIG_IGN)
(нажатие означает завершение процесса).
Вернуть нормальную реакцию можно следующим образом signal(SIGNINT, SIG_DFL)
Сиcтемный вызов signal
не входит в стандарт POSIX.1
.
Ситемный вызов сигнал определен в ANSI C
. Следовательно имеется во всех UNIX и Linux системах, но не рекомендуется использовать в кросс-платформенных приложениях, тк его действия будут отличаться в разных системах.
POSIX
данный стандарт разработан национальным институтом стандартов и технологий США (раньше - NIST
). Последний вариант был опубликован в 1988 году.
Полное название - POSIX.1 FIPS
(Federal Information Processing Standart)
Документ был написан для федеральных ведомств, которые покупают компьютерную технику. В любой стране основным покупателем является государство. Рыночные отношения позволяют нам выбирать, игнорировать этот документ или следовать этому документу и получить в клиенты само государство.
Возникновение POSIX
привело к появлению других стандартов. POSIX
появился вследствие сильного расхождения ОС UNIX
- в этих ОС стали использовать разные наборы системных вызовов - возникла ситуация, когда ПО на System5
нельзя было использовать на UnixBSD
и наоборот.
POSIX
- чтобы создавать переносимое ПО.
X/Open
- стандарт, созданный создан европейских компаний для создания переносимого ПО
X/Open
Portability Guide (XPG3)
1994 - создание (XPG4).
Стандарт X/Open
основан на ANSI C
, POSIX.1
, POSIX. 2
, содержит дополнительные конструкции, разработанные в рамках X/Open
(В методе ко второй лабе был пример с обработчиком сигнала catch_sig)
Если системный вызов signal
не входит в POSIX
, значит в POSIX
должен быть свой системный вызов
int sigaction(int signal_num, struct sigaction *action, struct sigaction *old_action);
struct sigaction
определена в библиотеке signal.h
В системном вызове можно определть дополнительные сигналы, которые будут, например, блокироваться при обработке signal_num
.
В лабе подчеркивалось, что с помощью сигналов, которые сопровождают события в системе (асинхронные по отношению к процессу) можно менять ход выполнения программы.
В POSIX
есть две функции для этих целей:
-
sigsetjmp
предназначен для того чтобы отметить одну или несколько позиций в программе -
siglongjmp
- для перехода в одну из этих позиций
Как мы видим, это гибкий подход к изменению поведения программы.
(43:12)
Типы семафоров:
- бинарный
- считающий
- множественный
Наборы считающих семафоров - этот тип поддерживают все современные ОС.
Доступ к отдельному семафору из набора выполняется по индексу. Важнейшим свойством набора семафоров является то, что одной неделимой операцией можно изменить все или часть набора семафоров.
Освободить семафор может любой процесс (не только тот, который захватил семафор).
Задача программиста - проверить безопасность использования семафоров в распараллеленных приложениях или при взаимодействии параллельных процессов очень непросто.
Если в программе используется большое количество семафоров, то в таких системах вероятность возникновения тупика является очень высокой. Именно поэтому было введена возможность реализованная в системе, что одной неделимой операцией можно изменить весь или часть набора семафоров.
В адресном пространстве ядра имеется таблица семафоров - системная таблица. В этой системной таблице отслеживаются все создаваемые в системе наборы семафоров. В каждом элементе такой таблицы находятся следующие данные об одном наборе семафоров.
Дескриптор набора семафоров:
- имя (идентификатор) - целое число, присваивается процессом, который создал набор семафоров. Другие процессы по этому имени могут открыть набор и получить дескриптор набора для доступа
- UID - идентификатор создателя набора семафоров (процесс, эффективный UID которого совпадает с UID создателя, может удалять набор и изменять управляющие параметры набора)
- права доступа
- количество семафоров в наборе
- время изменения (одного или нескольких значений семафоров последним процессом)
- Время последнего изменения управляющих параметров набора
- указатель на массив семафоров
Семантически наборы семафоров представляются как массивы, первый семафор имеет индекс 0.
Каждый отдельный семафор имеет набор параметров. Дескриптор отдельного семафора:
- Значение семафора
- Идентификатор процесса, который оперировал семафором в последний раз
- Число процессов, заблокированных на семафоре
Каждый элемент этой таблицы описывает структуры struct semid_ds
(<sys/sem.h>
)
Каждая строка таблицы описывает отдельный набор семафоров
Каждый семафор описан структурой struct sem
.
На семафорах определены следующие системные вызовы:
-
semget()
- создаёт набор семафоров -
semctl()
- позволяет изменять управляющие параметры набора семафоров -
semop()
- изменяет значение семафора - отдельного, набора или части набора семафоров
на семафоре определена структура
struct sembuf {
ushort sem_num; // - индекс семафора
short sem_op; // - операция на семафоре
short sem_flg; // - флаги, определенные на семафоре
};
В отличие от классических семафров Дейкстры (у него две операции), на семафоре юникс определены три операции
-
sem_op > 0
освобождение процесса, который выполнил системный вызовsemop()
- увеличение значения семафора, которое приведет к разблокировке заблокированных на данном семафоре процессов -
sem_op == 0
процесс, который выполнил такой системный вызов, будет переведен в состояние ожидания до момента освобождения ресурса, но ресурс не захватывается -
sem_op < 0
- захват ресурса - уменьшение значения семафора
Декрементировать семафор можно только если семафор имеет положительное значение.
На семафорах определны специальные флаги:
-
IPC_NOWAIT
- информирует ядро системы о нежелании процесса переходить в состояние ожидания (объясняется стремлением избежать блокировки всех процессов, которые находятся в очереди к семафору. В том случае, если захвативший ресурс процесс завершится аварийно или получит сигнал kill. Это важно в силу того, что сигнал kill невозможно перехватить. В результате убиваемый процесс не может выполнить освобождение семафора и открыть другим процессам доступ к ресурсу, которым он может управлять) -
SEM_UNDO
- указывает ядру, что оно должно отслеживать изменение значения семафора в результате системного вызоваsemop()
и при завершении процесса, вызвавшегоsemop()
, ядро ликвидирует сделанные изменения для того, чтобы процессы не были заблокированы на семафоре навечно.
75:06
Рассмотри пример создания набора семафоров и определения операций на данном наборе.
#include <sys/types.h>
#include <sys/tpc.h>
#include <sys/sem.h>
// объявляем набор из двух семафоров и задаем значения каждого из семафоров:
// первое значение - это индекс, второе - операция, третье - флаги (см объявление структуры)
struct sem_buf sbuf[2] = {{0, -1, SEM_UNDO | IDC_NOWAIT}, {1, 0, 1}};
int main()
{
// определим права доступа к каждому из семафоров
int perms = S_IRWXU | S_IRNXG | S_IRWXO;
// дескриптор набора семафоров
int fd = semget(100, 2, IPC_CREATE | perms);
// если semget выполнен удачно - нам удалось создать набор семафоров
if (fd == -1) { perror("semget"); exit(1); }
if (semop(fd, sbuf, 2) == -1) { perror("semop"); }
return 0;
}
Что делает данная программа: В программе открывается набор из двух семафоров с id 100. Этот набор будет создан, если такого набора еще не было. Для всех категорий пользователей установлены полные права доступа. Если вызов semget успешен, то выполняется semop, который декрементирует значение первого семафора, если это возможно, и выполняет проверку на 0 второго семафора.
86:06
Программные каналы - <что-то> (bassboosted, sorry), которые создаются ядром системы.
88:48
Процессы имеют защищенное адресное пространство, поэтому другой процесс не может обратиться в такое адресное пространство. Для взаимодействия процессов существует одно единственное адресное пространсто - область памяти ядра системы. В ядре создаются средства взаимодействия процессов (например, программные каналы).
Особенность разделяемых сегментов памяти: в отличие от других средств взаимодействия, разделяемые сегменты подключаются к адресному пространству процессов. Это исключает необходимость копирования данных из адресного пространства процесса в адресное пространство ядра и наоборот. Сегменты разделяемой памяти были задуманы как средство повышения производительности при передаче сообщения от одного процесса другому.
Существует таблица разделяемых сегментов.
<sys/shm.h>
- дескриптор описывается структурой struct shmid_ds
Особенностью разделяемых сегментов является то, что процесс получает указатель на разделяемый сегмент
Для разделяемой памяти определены системные вызовы:
-
shmget()
- создает разделяемый сегмент -
shmctl()
- изменяет упр параметры сегмента -
shmat()
- (attach) получение указателя на разделяемый сегмент -
shmdt()
- (detach) отделение сегмента от адресного пространства процесса
97:10
int main()
{
int perms = S_IRWXU | S_IRWXG | S_IRWXO;
// id = 100, размер 1024 байта
int fd = shmget(100, 1024, IPC_CREATE | perms);
if (fd == -1) {perror("shmget"); exit(1) }
// необходимо подключить, созданную разд память к адр пространству процесса
char *adr = (char*)shmat(fd, 0, 0);
if (addr == (char *) -1) { perror("shmat"); exit(1);}
// если сегмент подключен, то в него записывается сообщние
strcpy(addr, "hello");
// после этого разд сегмент отключается
if (shmdt(addr) == -1) perror("shmdt");
return 0;
}
(Двух приведенных примеров - создание и изменение значений семафоров и создание разделяемого сегмента - достаточно для лабы 3 UNIX)
107:26
Системное ограничение | значение |
---|---|
SHMMNI | максимальное число разделяемых сегментов |
SHMMIN | минимально возможный размер разд сегмента в байтах |
SHMMAX | максимально возможный размер разд сегмента в байтах |
Если процесс попытается создать новый разделяемый сегмент, а их число превысит максимальное (SHMMNI), то процесс будет заблокирован до тех пор, пока другой процесс не освободить какой-то разделяемый сегмент
Если процесс попытается создать разделяемый сегмент, размер которого превышает макс значения (SHMMAX), то системный вызов не будет выполнен - возвращена ошибка.
110:49
В современных ОС UNIX существуют программные каналы, которые принято называть pipe
- именованные и неименованные.
В один конец втекает, из другого вытекает - передача данных в одном направлении (потоковая передача данных). Если надо передавать данные в обе стороны, то нужна вторая труба, в которой движение информации будет противоположно первому.
Именованные программные каналы и неименованные - два типа программных каналов, которые поддерживаются современными ОС.
115:06
mknod
- создание именованного программного канала (см. лаба 2 по UNIX)
При создании именованного программного канала, это будет труба типа FIFO
(тип потоковой передачи данных)
Являются в системе специальными файлами и видны в файловой системе, более того - они имеют идентификатор в файловой системе. Любой процесс, который знает идентификатор именованного программного канала, может работать с этим программным каналом.
117:03
Не имеют идентификатора, но поддерживаются средствами файловой системы, имеют дескриптор.
В силу особенностей системного вызова fork
(системный вызов fork
создает новый процесс, процесс потомок, который является копией процесса предка - наследует код предка, дескрипторы открытых файлов, сигнальную маску и тд) неименованными программными каналами могут пользоваться процессы-родственники, тк процессы потомки наследуют от предка дескрипторы открытых файлов.
Программы не каналы имеют средства управления доступом к программному каналу
int fd[2]; // создается массив дескрипторов
pipe(fd); // передается в пайп чтобы управлять доступом
В канал нельзя писать, если из него читают, из канала нельзя читать, если в него пишут.
(Канал закрывается на чтение при записи и закрывается на запись при чтении)
Труба буферизуется на трех уровнях
Программные каналы буферизуются в системной памяти (в области данных ядра системы)
При переполнении системной памяти, буфера, имеющие наибольшее время сущствования переписываются на диск. Используются стандартные функции работы с памятью.
Если процесс записывает в пайп больше 4096 байт, то труба будет буферизоваться по времени, приостанавливая процесс, который записывает в друбу до тех пор, пока данные не будут прочитаны.
Ограничение значений канала (4096) - повысить эффективность операции обмена. При операции обмена ОС выделяет системные буферы. Если его размер не превышает 4096, то такой канал может целиком разместиться в системном буфере. (Это важная мысль)
Чтение из памяти одиночной переменной только на 30% быстрее, чем передача одной страницы. Это связано с тем, что в системе оптимизируется передача страниц (буфера соответствуют размеру страницы).