Семинары 6-7. Средства System V IPC. Организация работы с разделяемой памятью в UNIX. Понятие нитей исполнения (thread'ов).

(Основывается на лекции 4 и лекции 5)

Предыдущий семинар | Программа курса | Следующий семинар

Программа семинара

  1. Преимущества и недостатки потокового обмена данными.
  2. Понятие о System V IPC.
  3. Пространство имен. Адресация в System V IPC. Функция ftok().
  4. Дескрипторы System V IPC.
  5. Разделяемая память в UNIX. Системные вызовы shmget(), shmat(), shmdt().
  6. Команды ipc и ipcrm.
  7. Использование системного вызова shmctl() для освобождения ресурса.
  8. Разделяемая память и системные вызовы fork(), exec() и функция exit().
  9. Понятие о нити исполнения (thread) в UNIX. Идентификатор нити исполнения. Функция pthread_self().
  10. Создание и завершение thread'а. Функции pthread_create(), pthread_exit(), pthread_join().
  11. Необходимость синхронизации процессов и нитей исполнения, использующих общую память.

Цели занятия

  1. Дать общее представление о System V IPC.

  2. Привить навыки работы с разделяемой памятью.

  3. Дать опыт работы с командами ipcs и ipcrm.

  4. Научить создавать и завершать threads в рамках одного процесса.

  5. Студент должен осознать разницу между созданием нового процесса и созданием нового thread’а.

  6. Студент должен понять необходимость синхронизации процессов и нитей исполнения при использовании разделяемой памяти.

Практические работы

  1. Прогон программ с использованием разделяемой памяти

  2. Работа с командами ipcs и ipcrm.

  3. Самостоятельное написание, компиляция и запуск программы для организации связи двух процессов через разделяемую память.

  4. Прогон программы с использованием двух нитей исполнения.

  5. Написание, компиляция и прогон программы с использованием трех нитей исполнения.

  6. Прогон программ, иллюстрирующих необходимость синхронизации процессов и нитей исполнения, использующих общую память.

 

План занятия

  1. Преимущества и недостатки потокового обмена данными.

    На предыдущем семинаре мы познакомились с механизмами, обеспечивающими потоковую передачу данных между процессами в операционной системе UNIX, а именно с pip'ами и FIFO. Потоковые механизмы достаточно просты в реализации и удобны для использования, но обладают рядом существенных недостатков:


     
  2. Понятие о System V IPC.

    Указанные выше недостатки потоков данных привели к разработке других механизмов передачи информации между процессами. Часть этих механизмов, впервые появившихся в UNIX System V и впоследствии перекочевавших оттуда практически во все современные версии операционной системы UNIX, получила общее название System V IPC (IPC - сокращение от InterProcess Communications). В группу System V IPC входят: очереди сообщений, разделяемая память и семафоры. Эти средства организации взаимодействия процессов связаны не только общностью происхождения, но и обладают схожим интерфейсом для выполнения схожих операций, например для выделения и освобождения соответствующего ресурса в системе. Их рассмотрение мы будем проводить в порядке от менее семантически нагруженных с точки зрения операционной системы к более семантически нагруженным. Иными словами, чем позже мы будем заниматься каким-либо механизмом  из System V IPC, тем больше действий по интерпретации передаваемой информации приходится выполнять операционной системе при использовании этого механизма. Часть этого семинара мы посвятим изучению разделяемой памяти. Семафоры будут рассматриваться на семинаре 8, а очереди сообщений - на семинаре 9.
     

  3. Пространство имен. Адресация в System V IPC. Функция ftok().

    Прямая и непрямая адресация в лекцииВсе средства связи из System V IPC, как и уже рассмотренные нами pipe и FIFO, являются средствами связи с непрямой адресацией. Как мы установили  ранее, на предыдущем семинаре, для организации взаимодействия неродственных процессов с помощью средства связи с непрямой адресацией необходимо, чтобы это средство связи имело некоторое имя. Отсутствие имен у pip'ов позволяет процессам получать информацию о расположении pip'а в системе и его состоянии только через родственные связи. Наличие ассоциированного имени у FIFO - имени специализированного файла в файловой системе - позволяет неродственным процессам получать эту информацию через интерфейс файловой системы. 

    Множество всех возможных имен для объектов какого-либо вида принято называть пространством имен соответствующего вида объектов. Для FIFO пространством имен является множество всех допустимых имен файлов в файловой системе. Для всех объектов из System V IPC таким пространством имен является множество значений некоторого целочисленного типа данных - key_t. - ключа. Причем программисту не позволено напрямую присваивать значение ключа, это значение задается опосредовано: через комбинацию имени какого-либо файла, уже существующего в файловой системе, и небольшого целого числа - например, номера экземпляра средства связи.

    Такой хитрый способ получения значения ключа связан с двумя соображениями:

    Получение значения ключа из двух этих компонентов осуществляется функцией ftok().

    Nota beneНеобходимо еще раз подчеркнуть три важных момента, связанных с использованием имени файла для получения ключа. Во-первых, необходимо указывать имя файла, который уже существует в файловой системе и для которого процесс имеет право доступа на чтение (не путайте с заданием имени файла при создании FIFO, где указывалось имя для вновь создаваемого специального файла). Во-вторых, указанный файл должен сохранять свое положение на диске до тех пор, пока все процессы, участвующие во взаимодействии, не получат ключ System V IPC. В-третьих, задание имени файла, как одной из компонент для получения ключа, ни в коем случае не означает, что информация, передаваемая с помощью ассоциированного средства связи, будет располагаться в этом файле. Информация будет храниться внутри адресного пространства операционной системы, а заданное имя файла лишь позволяет различным процессам сгенерировать идентичные ключи.

     

  4. Дескрипторы System V IPC.

    Мы говорили, что информацию о потоках ввода-вывода, с которыми имеет дело текущий процесс, в частности о pip'ах и FIFO, операционная система хранит в таблице открытых файлов процесса. Системные вызовы, осуществляющие операции над потоком, используют в качестве параметра индекс элемента таблицы открытых файлов, соответствующего потоку, - файловый дескриптор. Использование файловых дескрипторов для идентификации потоков внутри процесса позволяет применять к ним уже существующий интерфейс для работы с файлами, но в то же время приводит к автоматическому закрытию потоков при завершении процесса. Этим в частности, объясняется один из недостатков потоковой передачи информации.

    При реализации компонент System V IPC была принята другая концепция. Ядро операционной системы хранит информацию обо всех средствах System V IPC, используемых в системе, вне контекста пользовательских процессов. При создании нового средства связи или получении доступа к уже существующему процесс получает неотрицательное целое число - дескриптор (идентификатор) этого средства связи, которое однозначно идентифицирует его во всей вычислительной системе. Этот дескриптор должен передаваться в качестве параметра всем системным вызовам, осуществляющим дальнейшие операции над соответствующим средством System V IPC.

    Подобная концепция позволяет устранить один из самых существенных недостатков, присущих потоковым средствам связи - требование об одновременном существовании взаимодействующих процессов, но в то же время требует повышенной осторожности для того, чтобы процесс, получающий информацию, не принял взамен новых старые, уже "протухшие" данные, по забывчивости оставленные в механизме коммуникации.
     

  5. Разделяемая память в UNIX. Системные вызовы shmget(), shmat(), shmdt().

    Разделяемая память в лекцииС точки зрения операционной системы, наименее семантически нагруженным средством System V IPC является разделяемая память (shared memory). Мы уже упоминали об этой категории средств связи на лекции. Для текущего семинара нам достаточно знать, что операционная система может позволить нескольким процессам совместно использовать некоторую область адресного пространства. Внутренние механизмы, позволяющие реализовать такое использование, будут подробно рассмотрены на лекции, посвященной сегментной, страничной и сегментно-страничной организации памяти.

    Инициализация средств связи в лекции Все средства связи System V IPC требуют предварительных инициализирующих действий (своего создания) для организации взаимодействия процессов.

    Для создания области разделяемой памяти с определенным ключом или доступа по ключу к уже существующей области применяется системный вызов shmget(). Существует два варианта его использования для создания новой области разделяемой памяти.


    Доступ к созданной области разделяемой памяти в дальнейшем обеспечивается ее дескриптором, который вернет системный вызов shmget(). Доступ к уже существующей области также может быть осуществлен двумя способами:

    • Если мы знаем ее ключ, то, использовав вызов shmget(), мы можем получить ее дескриптор. В этом случае нельзя использовать в качестве составной части флагов флаг IPC_EXCL, а значение ключа, естественно, не может быть IPC_PRIVATE. Права доступа игнорируются, а размер области должен совпадать с размером, указанным при ее создании.

    • Либо мы можем воспользоваться тем, что дескриптор System V IPC действителен в рамках всей операционной системы, и передать его значение от процесса, создавшего разделяемую память, каким-нибудь образом текущему процессу. Отметим, что при создании разделяемой памяти с помощью значения IPC_PRIVATE - это единственно возможный способ.


    После получения дескриптора необходимо включить область разделяемой памяти в адресное пространство текущего процесса. Это осуществляется с помощью системного вызова shmat(). При нормальном завершении он вернет адрес разделяемой памяти в адресном пространстве текущего процесса. Дальнейший доступ к этой памяти осуществляется с помощью обычных средств языка программирования. После окончания использования разделяемой памяти процесс может уменьшить размер своего адресного пространства, исключив из него эту область с помощью системного вызова shmdt(). Отметим, что в качестве своего параметра системный вызов shmdt() требует адрес начала области разделяемой памяти в адресном пространстве процесса, т. е. значение, которое вернул системный вызов shmat(), поэтому данное значение следует сохранять на протяжении всего времени использования разделяемой памяти.
     

  6. Прогон программ с использованием разделяемой памяти.

    Для иллюстрации использования разделяемой памяти давайте рассмотрим две взаимодействующие программы, расположенные в файлах /ftp/pub/sem6-7/stud/06-1a.c и /ftp/pub/sem6-7/stud/06-1b.c. Обе программы очень похожи друг на друга и используют разделяемую память для хранения числа запусков каждой из программ и их суммы. В разделяемой памяти размещается массив из трех целых чисел. Первый элемент массива используется как счетчик для программы 1, второй элемент - для программы 2, третий элемент - для обеих программ суммарно. Дополнительный нюанс в программах возникает из-за необходимости инициализации элементов массива при создании разделяемой памяти. Для этого нам нужно, чтобы программы могли различать случай, когда они создали ее и случай, когда она уже существовала. Мы добиваемся различия, используя вначале системный вызов shmget() с флагами IPC_CREAT и IPC_EXCL. Если вызов завершается нормально, то мы создали разделяемую память. Если вызов завершается с констатацией ошибки  и значение переменной errno равняется EEXIST, то, значит, разделяемая память уже существует, и мы можем получить ее IPC дескриптор, применяя тот же самый вызов с нулевым значением флагов. Откомпилируйте программы и запустите их несколько раз. Проанализируйте полученные результаты.
     

  7. Команды ipc и ipcrm.

    Как мы видели из предыдущего примера, созданная область разделяемой памяти сохраняется в операционной системе даже тогда, когда нет ни одного процесса,  включающего ее в свое адресное пространство. С одной стороны, это имеет определенные преимущества, не требуя одновременного существования взаимодействующих процессов, с другой стороны может причинять существенные неудобства. Допустим, что предыдущие программы мы хотим использовать таким образом, чтобы подсчитывать количество запусков в течение  одного, текущего, сеанса работы в системе. Однако в созданном сегменте разделяемой памяти остается информация от предыдущего сеанса, и программы будут выдавать общее количество запусков за все время работы с момента загрузки операционной системы. Можно было бы создавать для нового сеанса новый сегмент разделяемой памяти, но количество ресурсов в системе не безгранично. Нас спасает то, что существуют способы удалять неиспользуемые ресурсы System V IPC как с помощью команд операционной системы, так и с помощью системных вызовов. Завершение взаимодействия в лекции Все средства System V IPC требуют определенных действий для освобождения занимаемых ресурсов после окончания взаимодействия процессов. Для того, чтобы удалять ресурсы System V IPC из командной строки, нам понадобятся две команды ipcs и ipcrm. Команда ipcs выдает информацию обо всех средствах System V IPC, существующих в системе, для которых пользователь обладает правами на чтение: областях разделяемой памяти, семафорах и очередях сообщений. Из всего многообразия выводимой информации нас будут интересовать только IPC идентификаторы для средств, созданных вами. Эти идентификаторы будут использоваться в команде ipcrm, позволяющей удалить необходимый ресурс из системы. Для удаления сегмента разделяемой памяти эта команда имеет вид:

    ipcrm shm <IPC идентификатор>  

    Удалите созданный вами сегмент разделяемой памяти из операционной системы, используя эти команды.

    Nota bene Если поведение ваших программ, использующих средства System V IPC, базируется на предположении, что эти средства были созданы при их работе, не забывайте перед их запуском удалять уже существовавшие ресурсы.
     

  8. Использование системного вызова shmctl() для освобождения ресурса.

    Для той же цели - удалить область разделяемой памяти из системы - можно воспользоваться и системным вызовом shmctl(). Этот системный вызов позволяет полностью ликвидировать область разделяемой памяти в операционной системе по заданному дескриптору средства IPC, если, конечно, у вас хватает для этого полномочий. Кроме этого, системный вызов shmctl() позволяет выполнять и другие действия над сегментом разделяемой памяти, но их изучение лежит за пределами нашего курса.
     

  9. Разделяемая память и системные вызовы fork(), exec() и функция exit().

    Важным вопросом является поведение сегментов разделяемой памяти при выполнении процессом системных вызовов fork(), exec() и функции exit().

    При выполнении системного вызова fork() все области разделяемой памяти, размещенные в адресном пространстве процесса, наследуются порожденным процессом.

    При выполнении системных вызовов exec() и функции exit() все области разделяемой памяти, размещенные в адресном пространстве процесса, исключаются из его адресного пространства, но продолжают существовать в операционной системе.
     

  10. Самостоятельное написание, компиляция и запуск программы для организации связи двух процессов через разделяемую память.

    Для закрепления полученных знаний напишите две программы, осуществляющие взаимодействие через разделяемую память. Первая программа должна создавать сегмент разделяемой памяти и копировать туда свой собственный исходный текст, вторая программа должна брать оттуда этот текст, печатать его на экране и удалять сегмент разделяемой памяти из системы.
     

  11. Понятие о нити исполнения (thread) в UNIX. Идентификатор нити исполнения. Функция pthread_self().

    Нити исполнения в лекции На лекции мы говорили, что во многих современных операционных системах существует расширенная реализация понятия процесс, когда процесс представляет собой совокупность выделенных ему ресурсов и набора нитей исполнения. Нити процесса разделяют его программный код, глобальные переменные и системные ресурсы, но каждая нить имеет свой собственный программный счетчик, свое содержимое регистров и свой собственный стек. Поскольку глобальные переменные у нитей исполнения являются общими, то они могут использовать их, как элементы разделяемой памяти, не прибегая к механизму, описанному выше.

    В различных версиях операционной системы UNIX существуют различные интерфейсы, обеспечивающие работу с нитями исполнения. Мы с вами кратко ознакомимся с некоторыми функциями, позволяющими разделить процесс на thread'ы и управлять их поведением, соответствующими стандарту POSIX. Нити исполнения, удовлетворяющие стандарту POSIX, принято называть POSIX thread'ами или кратко pthread'ами.

    Реализация нитей исполнения в лекцииК сожалению, операционная система Linux не полностью поддерживает нити исполнения на уровне ядра системы. При создании нового thread'а запускается новый традиционный процесс, разделяющий с родительским традиционным процессом его ресурсы, программный код и данные, расположенные вне стека, т.е. фактически действительно создается новый thread, но ядро не умеет определять, что эти thread'ы являются составными частями одного целого. Это знает только специальный процесс-координатор, работающий на пользовательском уровне и стартующий при первом вызове функций, обеспечивающих POSIX интерфейс для нитей исполнения. Поэтому мы сможем наблюдать не все преимущества использования нитей исполнения (в частности, ускорить решение задачи с их помощью вряд ли получится), но даже в этом случае, thread'ы можно использовать как очень удобный способ для создания процессов с общими ресурсами, программным кодом и разделяемой памятью.

    Каждая нить исполнения, как и процесс, имеет в системе свой собственный уникальный номер - идентификатор thread'a. Поскольку традиционный процесс в концепции нитей исполнения трактуется как процесс, содержащий единственную нить исполнения, то мы можем узнать идентификатор этой нити и для любого обычного процесса. Для этого используется функция pthread_self(). Нить исполнения создаваемую при рождении нового процесса принято называть начальной или главной нитью исполнения этого процесса.
     

  12. Создание и завершение thread'а. Функции pthread_create(), pthread_exit(), pthread_join().

    Нити исполнения, как и традиционные процессы, могут порождать нити-потомки, правда, только внутри своего процесса. Каждый будущий thread внутри программы должен представлять собой функцию с прототипом

    void *thread(void *arg);

    Параметр arg передается этой функции при создании thread'a и может, до некоторой степени, рассматриваться как аналог параметров функции main(), о которых мы говорили на семинарах 3-4. Возвращаемое функцией значение может интерпретироваться как аналог информации, которую родительский процесс может получить после завершения процесса-ребенка. Для создания новой нити исполнения применяется функция pthread_create(). Мы не будем рассматривать ее в полном объеме, так как детальное изучение программирования с использованием thread'ов не является целью данного курса. Nota bene Важным отличием этой функции от большинства других системных вызовов и функций является то, что в случае неудачного завершения она возвращает не отрицательное, а положительное значение, которое определяет код ошибки, описанный в файле errno.h. Значение системной переменной errno при этом не устанавливается. Результатом выполнения этой функции является появление в системе новой нити исполнения, которая будет выполнять функцию, ассоциированную со thread'ом, передав ей специфицированный параметр, параллельно с уже существовавшими нитями исполнения процесса.

     Созданный thread может завершить свою деятельность тремя способами


    Одним из вариантов получения адреса, возвращаемого завершившимся thread'ом, с одновременным ожиданием его завершения является использование функции pthread_join(). Нить исполнения, вызвавшую эту функцию, переходит в состояние ожидание до завершения заданного thread'а. Функция позволяет также получить указатель, который вернул завершившийся thread в операционную систему.  

  13. Прогон программы с использованием двух нитей исполнения.

    Для иллюстрации вышесказанного давайте рассмотрим программу /ftp/pub/sem6-7/stud/06-2.c, в которой работают две нити исполнения. При работе редактора связей  необходимо явно подключить библиотеку функции для работы с pthread'ами, которая не подключается автоматически. Это делается с помощью добавления к команде компиляции и редактирования связей параметра -lpthread - подключить библиотеку pthread. Откомпилируйте эту программу и запустите на исполнение. Nota beneОбратите внимание на отличие результатов этой программы от похожей программы, иллюстрировавшей создание нового процесса, которую мы рассматривали на семинарах 3-4. Программа, создававшая новый процесс, печатала дважды одинаковые значения для переменной a, так как адресные пространства различных процессов независимы, и каждый процесс прибавлял 1 к своей собственной переменной a. Рассматриваемая программа печатает два разных значения, так как переменная a является разделяемой, и каждый thread прибавляет 1 к одной и той же переменной.

     

  14. Написание, компиляция и прогон программы с использованием трех нитей исполнения.

    Модифицируйте предыдущую программу, добавив к ней третью нить исполнения.

     

  15. Необходимость синхронизации процессов и нитей исполнения, использующих общую память.

    Критическая секция в лекции Все рассмотренные на этом семинаре примеры являются не совсем корректными. В большинстве случаев они работают правильно, однако возможны ситуации, когда совместная деятельность этих процессов или нитей исполнения приводит к неверным и неожиданным результатом. Это связано с тем, что любые неатомарные операции, связанные с изменением содержимого разделяемой памяти, представляют собой критическую секцию процесса или нити исполнения. Вернемся к рассмотрению программ /ftp/pub/sem6-7/stud/06-1a.c и /ftp/pub/sem6-7/stud/06-1b.c. При одновременном существовании 2-х процессов в операционной системе может возникнуть следующая последовательность выполнения операций во времени:

    ...
    Процесс 1: array[0] += 1;
    Процесс 2: array[1] += 1;
    Процесс 1: array[2] += 1;
    Процесс 1: printf("Program 1 was spawn %d times, program 2 - %d times, total - %d times\n", array[0], array[1], array[2]);
    ...


    Тогда печать будет давать неправильные результаты. Естественно, что воспроизвести подобную последовательность действий практически нереально. Мы не сможем подобрать необходимые времена старта процессов и степень загруженности вычислительной системы. Но мы можем смоделировать эту ситуацию, добавив в обеих программах достаточно длительные пустые циклы перед оператором array[2] += 1; Это проделано в программах /ftp/pub/sem6-7/stud/06-3a.c и /ftp/pub/sem6-7/stud/06-3b.c. Откомпилируем их, запустим любую из них один раз для создания и инициализации разделяемой памяти. Затем запустим другую и, пока она находится в цикле, запустим, например, с другого виртуального терминала, снова первую. Мы получим неожиданный результат. Алгоритмы синхронизации в лекции Как видим, для написания корректно работающих программ необходимо обеспечивать взаимоисключение при работе с разделяемой памятью и, может быть, взаимную очередность доступа к ней. Это можно сделать с помощью рассмотренных на лекции алгоритмов синхронизации, например, алгоритма Петерсона или алгоритма булочной.

    Задача повышенной сложности: модифицируйте программы /ftp/pub/sem6-7/stud/06-3a.c и /ftp/pub/sem6-7/stud/06-3b.c для корректной работы с помощью алгоритма Петерсона.

    На следующем семинаре мы рассмотрим семафоры, которые являются средством System V IPC, предназначенным для синхронизации процессов.
     

Предыдущий семинар | Программа курса | Следующий семинар