Студент, который находится в списке группы под четным номером, использует неименованные семафоры для координации доступа к ресурсу.
В обоих случаях студент реализует три варианта программы:
С блокирующей операцией захвата мьютекса (семафора) pthread_mutex_lock() (sem_wait());
С операцией проверки захвата мьютекса (семафора) без блокировки pthread_mutex_trylock() (sem_trywait());
С блокировкой на время операции захвата мьютекса (семафора) pthread_mutex_timedlock() (sem_timedwait()).
Шаблон программы представлен ниже:
объявить флаг завершения потока 1;
объявить флаг завершения потока 2;
объявить идентификатор мьютекса /*неименованного семафора*/;
функция потока 1()
{
пока (флаг завершения потока 1 не установлен)
{
захватить мьютекс /*неименованный семафор*/;
в цикле несколько раз выполнять
{
выводить символ ‘1’ на экран;
задержать на время;
}
освободить мьютекс /*неименованный семафор*/;
задержать на время;
}
}
функция потока 2()
{
пока (флаг завершения потока 2 не установлен)
{
захватить мьютекс /*неименованный семафор*/;
в цикле несколько раз выполнять
{
выводить символ ‘2’ на экран;
задержать на время;
}
освободить мьютекс /*неименованный семафор*/;
задержать на время;
}
}
основная программа()
{
объявить идентификатор потока 1;
объявить идентификатор потока 2;
инициализировать мьютекс /*неименованный семафор*/;
создать поток из функции потока 1;
создать поток из функции потока 2;
ждать нажатия клавиши;
установить флаг завершения потока 1;
установить флаг завершения потока 2;
ждать завершения потока 1;
ждать завершения потока 2;
удалить мьютекс /*неименованный семафор*/;
}
Какой ресурс называется критическим ресурсом?
Какой участок программы называется критическим участком?
Какой режим выполнения программ называется режимом взаимного исключения?
Перечислите способы организации режима взаимного исключения.
Опишите алгоритмы операций захвата и освобождения мьютекса.
Опишите алгоритмы операций захвата и освобождения семафора.
Какими операциями с мьютексом и с неименованным семафором можно осуществить проверку занятости ресурса без блокирования потока?
Цель работы - знакомство со средством взаимодействия потоков и процессов - неименованными каналами и с системными вызовами, обеспечивающими создание и закрытие неименованных каналов, а также передачу и прием данных через неименованные каналы.
Одним из средств взаимодействия процессов и потоков является неименованный канал. Канал не только обеспечивает передачу данных, но и поддерживает синхронизацию между потоками и процессами. Свойствами канала являются следующие положения «при попытке записать данные в полный канал процесс блокируется» и «при попытке чтения данных из пустого канала процесс блокируется».
Канал создается с помощью следующего вызова:
int pipe(int filedes[2]),
где:
filedes[2] – массив из двух файловых дескрипторов, один из которых используется для записи данных (filedes[1]), а второй (filedes[0]) – для чтения данных.
Чтение данных из канала производится следующей операцией:
ssize_t read(int fd, void *buf, size_t count),
где:
fd – файловый дескриптор для чтения;
buf – адрес буфера для чтения данных;
count – размер буфера.
Запись данных в канал производится следующей операцией:
ssize_t write(int fd, const void *buf, size_t count),
где:
fd – файловый дескриптор для записи;
buf – адрес буфера для записи данных;
count – количество байтов, предназначенных для записи.
Каждый из дескрипторов канала отдельно закрывается следующим вызовом:
int close(int fd).
Блокировки потока при чтении из пустого канала или при записи в полный канал обладают и недостатками.
В первом случае, если никакой поток не запишет данные в пустой канал, то поток, ожидающий чтение, так и останется заблокированным.
Аналогично, если никакой поток не прочитает данные из полного канала, то поток, ожидающие запись, так и останется заблокированным.
Избежать указанных недостатков позволяют неблокирующие операции чтения и записи.
Реализовать неблокирующие операции чтения и записи в неименованном канале можно следующими способами.
Использовать следующую функцию создания неименованного канала вместо ранее приведенной функции: int pipe2(int pipefd[2], int flags); где в качестве параметра int flags передать значение O_NONBLOCK, обеспечивающее неблокируемое состояние операций чтения и записи для созданных дескрипторов.
Использовать следующую функцию для установления флагов состояния дескрипторов: int fcntl(int fd, int cmd, ... /* arg */ ); где в качестве параметра int cmd можно передать команду F_SETFL установки флагов состояния дескриптора, а в списке аргументов можно передать флаг O_NONBLOCK.
Второй вариант является более предпочтительным, чем первый, поскольку является универсальным, не ориентированным исключительно на Linux.
Написать программу, содержащую два потока, обменивающихся информацией через неименованный канал (pipe).
Устранить блокировки потоков для случая чтения из пустого канала двумя способами (pipe2() и fcntl()).
объявить флаг завершения потока 1;
объявить флаг завершения потока 2;
объявить идентификатор неименованного канала;
функция потока 1()
{
объявить буфер;
пока (флаг завершения потока 1 не установлен)
{
сформировать сообщение в буфере;
записать сообщение из буфера в неименованный канал;
задержать на время;
}
}
функция потока 2()
{
объявить буфер;
пока (флаг завершения потока 2 не установлен)
{
очистить буфер;
прочитать сообщение из неименованного канала в буфер;
вывести сообщение на экран;
}
}
основная программа()
{
объявить идентификатор потока 1;
объявить идентификатор потока 2;
создать неименованный канал;
создать поток из функции потока 1;
создать поток из функции потока 2;
ждать нажатия клавиши;
установить флаг завершения потока 1;
установить флаг завершения потока 2;
ждать завершения потока 1;
ждать завершения потока 2;
закрыть неименованный канал;
}
Как обеспечивается синхронизация записи и чтения в неименованном канале?
Как осуществить использование неименованного канала для взаимодействия процессов?
Как для неименованного канала организовать чтение и запись данных «без ожидания»?
Как реализовать функциональность неименованного канала с помощью семафоров?
Как с помощью неименованных каналов организовать двунаправленное взаимодействие?
Каким отношением должны быть связаны процессы, чтобы взаимодействие между ними могло бы быть организовано через неименованные каналы?
Цель работы - знакомство с основными системными вызовами, обеспечивающими создание процессов.
Основным системным вызовом для создания нового процесса в операционных системах, поддерживающих стандарт POSIX, является следующий вызов:
pid_t fork(void).
Вызов fork(), сделанный в некотором процессе, который будем называть родительским, создает дочерний процесс, который является практически полной копией родительского процесса. При создании данные родительского процесса копируются в дочерний процесс и оба процесса начинают выполняться параллельно. Важным отличием родительского процесса от дочернего процесса является значение результата, возвращаемого функцией fork(). Дочернему процессу возвращается значение 0, а родительскому процессу возвращается идентификатор дочернего процесса, т.е.:
pid_t pid = fork();
if (pid == 0) {
//дочерний процесс
}else{
//родительский процесс;
},
где pid – возвращаемое значение, 0 – дочернему процессу, > 0 – родительскому процессу, -1 – в случае ошибки.
Другим средством, позволяющим создавать процессы, является следующий вызов:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg),
где
fn – функция, реализующая дочерний процесс,
child_stack – указатель на начало стека дочернего процесса,
flags – набор флагов, передаваемых дочернему процессу,
arg – аргументы, передаваемые функции fn.
Шаблон функции, реализующей дочерний процесс, имеет следующий вид:
static int fn(void *arg).
Набор флагов flags, передаваемых в функцию clone, позволяет управлять пространствами имен процесса-потомка, которые будут совместными или изолированными от пространств имен процесса-родителя.
Пространство имен – это средство, позволяющее изолировать некоторый вид ресурса одного процесса от доступа другого процесса. В настоящее время пространства имен являются средством организации контейнеров – механизма выполнения процессов в изолированном окружении. Целью контейнеризации является обеспечение безопасного выполнения процессов.
С помощью определенных флагов можно изолировать следующие пространства имен процесса-родителя и процесса-потомка:
Название пространства |
Значение флага |
Вид пространства |
IPC |
CLONE_NEWIPC |
очереди сообщений |
Network |
CLONE_NEWNET |
сетевые параметры |
Mount |
CLONE_NEWNS |
файловая система |
PID |
CLONE_NEWPID |
ID процессов |
User |
CLONE_NEWUSER |
ID пользователей и групп |
UTS |
CLONE_NEWUTS |
имя узла |
Параметр flags, передаваемый в функцию clone, может формироваться из перечисленных флагов путем логического сложения с базовым флагом SIGCHLD, сигналом, который посылается родителю, когда потомок завершается.
Например, если требуется в процессе-потомке создать изолированное от процесса-родителя сетевое пространство, необходимо параметр flags задать в следующем виде:
CLONE_NEWNET | SIGCHLD.
В данной работе необходимо передавать функции clone() только флаг SIGCHLD.
Наиболее распространенной схемой выполнения пары процессов (родительский – дочерний), является схема, при которой родительский процесс приостанавливает свое выполнение до завершения дочернего процесса с помощью специальной функции:
pid_t waitpid(pid_t pid, int *status, int options),
где:
pid – идентификатор дочернего процесса, завершение которого ожидается,
status – результат завершения дочернего процесса,
options – режим работы функции.
В некоторых случаях вызовы fork() и clone() используются программистом для организации параллельного выполнения процессов в рамках одной написанной программы.
В других случаях в качестве дочернего процесса необходимо выполнить внешнюю программу.
В этом случае для запуска внешней программы следует в дочернем процессе вызвать функцию семейства exec().
Существуют следующие разновидности этой функции:
int execl(const char *path, const char *arg, ...),
int execlp(const char *file, const char *arg, ...),
int execle(const char *path, const char *arg,..., char * const envp[]),
int execlpe(const char *file, const char *arg , ..., NULL, char * const envp[]),
int execv(const char *path, char *const argv[]),
int execvp(const char *file, char *const argv[]),
int execve(const char * path, char *const argv[],char *const envp[]),
int execvpe(const char *file, char *const argv[],char *const envp[]).
Написать программу 1, которая при запуске принимает несколько (3 – 5) аргументов в командной строке, а затем в цикле выводит каждый аргумент на экран с задержкой в несколько секунд.
Программа 1 должна выводить на экран свой идентификатор и идентификатор процесса-родителя.
Программа 1 должна сформировать код завершения.
Написать программу 2, которая запускает программу 1 в качестве дочернего процесса с помощью вызовов fork() и exec().
Программа 2 должна вывести на экран идентификатор процесса-родителя, свой идентификатор и идентификатор дочернего процесса.
Программа 2 должна сформировать набор параметров для передачи в дочерний процесс аргументов командной строки.
Программа 2 должна ожидать завершения дочернего процесса, проверяя событие завершения каждую половину секунды, а по завершению дочернего процесса вывести на экран код завершения.
Написать программу 3, которая запускает программу 1 в качестве дочернего процесса с помощью вызовов clone() и exec().
Программа 3 должна вывести на экран идентификатор процесса-родителя, свой идентификатор и идентификатор дочернего процесса.
Программа 3 должна сформировать набор параметров для передачи в дочерний процесс аргументов командной строки.
Программа 3 должна ожидать завершения дочернего процесса, проверяя событие завершения каждую половину секунды, а по завершению дочернего процесса вывести на экран код завершения.
Студенты с номерами 1, 9 используют 1-й вариант функции exec(). Студенты с номерами 2, 10 используют 2-й вариант функции exec(). И т.д.
Какие вызовы для создания процессов, кроме вызова fork(), существуют и в чем состоят их особенности по сравнению с вызовом fork()?
В каком случае дочерний процесс может превратиться в процесс-зомби?
Как процесс может узнать, является ли он родительским процессом или дочерним процессом?
Каким образом родительский процесс может ждать завершения дочернего процесса и находиться в незаблокированном состоянии?
Какой механизм обмена данными применяется между родительским и дочерним процессами?
Как можно показать, что изменения данных, происходящие в дочернем процессе, не затрагивают данные родительского процесса?
Цель работы - знакомство студентов со средством синхронизации процессов - именованными семафорами и с системными вызовами, обеспечивающими создание, закрытие и удаление именованных семафоров, а также захват и освобождение именованных семафоров.
Именованные семафоры позволяют организовать синхронизацию процессов в операционной системе. За счет того, что при создании и открытии именованного семафора, ему передается «имя» - цепочка символов, два процесса получают возможность получить указатель на один и тот же семафор. Т.е. в отличие от мьютексов и неименованных семафоров, именованные семафоры могут координировать доступ к критическому ресурсу не только на уровне нескольких потоков одной программы, но и а на уровне нескольких, выполняющихся программ - процессов.
В системе этот семафор реализуется в виде специального файла, время жизни которого не ограничено временем жизни процесса, его создавшего.
Наиболее распространенными программными интерфейсами для создания именованных семафоров являются:
интерфейс POSIX (Portable Operating System Interface — переносимый интерфейс операционных систем — набор стандартов, описывающих интерфейсы между операционной системой и прикладной программой (системный API Application Programming Interface) [https://ru.wikipedia.org/wiki/POSIX]);
интерфейс SVID (System V Interface Definition) стандарт, описывающий поведение ОС UNIX https://ru.wikipedia.org/wiki/System_V_Interface_Definition