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

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

Ввести массив А

Ввести массив В

Ввести массив С

Вывести массив С

При выполнении такой программы в рамках одного процесса этот процесс будет 4 раза блокироваться ожидая окончания операции ввода/вывода. Но этот алгоритм обладает внутренним параллелизмом. Вычисление суммы массивов А+В можно было бы выполнять параллельно с ожиданием окончания операции ввода массива С. Такое совмещение опереций можно реализовать использую два взаимодействующих процессов. Для простаты считаем что процессы взаимодействуют через разделяемую память.

Процесс 1 процесс 2

Создать процесс

Переключение контекста

Выделение общей памяти

Ожидание ввода А и В

Переключение контекста

Выделение общей памяти

Переключение контекста

Переключение контекста

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

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

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

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



Процесс находится в состоянии исполнения если одна из его нитей находится в состояние исполнения. Процесс находится в состояние ожидания если все его нити находятся в состоянии ожидания. Наконец, процесс находится в состоянии завершил исполнение если все его нити находятся в состоянии завершил исполнение.

Пока одна нить процесса заблокирована,

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

Различают ОС поддерживающие нити на уровне ядра и на уровне библиотеки. ОС поддерживающая нити на уровне ядра использует планирование Ц.П. в терминах нити а управление памятью и другими ресурсами остается в терминах процесса. В ОС поддерживающей нити на уровне библиотек пользователей и планирование Ц.П. и управление ресурсами осуществляется в терминах процесс.

Распределение использования Ц.П. по нитям в рамках выделенного Ц.П. временного интервала осуществляется средствами библиотеки. В подобных системах блокирование нити приводит к блокированию всего процесса. Т.к. ядро системы не имеет представления о существовании нити, по сути дела в таких ВС просто имитируются наличие нитей исполнения.

Управление памятью.

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

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

Потоки не имеют своих собственных ресурсов, они развива­ются в том же виртуальном адресном пространстве, могут пользоваться теми же файлами, виртуальными устройствами и иными ресурсами, что и данный про­цесс. Единственное, что им крайне важно иметь, - ϶ᴛᴏ процессорный ресурс. В од­нопроцессорной машинœе потоки разделяют между собой процессорное время аналогично тому, как это делают обычные процессы, а в многопроцессорной машинœе могут выполняться одновременно, в случае если не встречают конкуренции из-за об­ращения к иным ресурсам.

Многопоточность обеспечивает возможность параллельного вы­полнения нескольких видов операций в одной прикладной программе. Параллель­ные вычисления (а, следовательно, более эффективное использование ресур­сов центрального процессора и меньшее суммарное время выполнения задач) теперь уже часто реализуется на уровне потоков, и программа, оформленная в виде нескольких потоков в рамках одного процесса, должна быть выполнена быстрее за счёт параллельного выполнения ее отдельных частей.

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

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

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

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

Общее представление об этом можно получить из рис. 5.1, на котором показаны таблицы, поддерживаемые операционной системой: для памяти, устройств ввода-вывода, файлов (программ и данных) и процессов. Хотя детали таких таблиц в разных ОС могут отличаться, по сути, все они поддерживают информацию по этим четырем категориям. Располагающий одними и теми же аппаратными ресурсами, но управляемый различными ОС, компьютер может работать с разной степенью эффективности. Наибольшие сложности в управлении ресурсами компьютера возникают в мультипрограммных ОС.

Рис. 5.1. Таблицы ОС

Мультипрограммирование (многозадачность, multitasking) – это такой способ организации вычислительного процесса, при котором на одном процессоре попеременно выполняются несколько программ. Чтобы поддерживать мультипрограммирование, ОС должна определить для себя внутренние единицы работы, между которыми будут разделяться процессор и другие ресурсы компьютера. В ОС пакетной обработки, распространенных в компьютерах второго и сначала и третьего поколения, такой единицей работы было задание. В настоящее время в большинстве операционных систем определены два типа единиц работы: более крупная единица – процесс, или задача, и менее крупная – поток , или нить . Причем процесс выполняется в форме одного или нескольких потоков.

Вместе с тем, в некоторых современных ОС вновь вернулись к такой единице работы, как задание (Job), например, в Windows. Задание в Windows представляет собой набор из одного или нескольких процессов, управляемых как единое целое. В частности, с каждым заданием ассоциированы квоты и лимиты ресурсов, хранящиеся в соответствующем объекте задания. Квоты включают такие пункты, как максимальное количество процессов (это не позволяет процессам задания создавать бесконтрольное количество дочерних процессов), суммарное время центрального процессора, доступное для каждого процесса в отдельности и для всех процессов вместе, а также максимальное количество используемой памяти для процесса и всего задания. Задания также могут ограничивать свои процессы в вопросах безопасности, например, получать или запрещать права администратора (даже при наличии правильного пароля).

Процессы рассматриваются операционной системой как заявки или контейнеры для всех видов ресурсов, кроме одного – процессорного времени. Это важнейший ресурс распределяется операционной системой между другими единицами работы – потоками, которые и получили свое название благодаря тому, что они представляют собой последовательности (потоки выполнения) команд. Каждый процесс начинается с одного потока, но новые потоки могут создаваться (порождаться) процессом динамически. В простейшем случае процесс состоит из одного потока, и именно таким образом трактовалось понятие "процесс" до середины 80-х годов (например, в ранних версиях UNIX). В некоторых современных ОС такое положение сохранилось, т.е. понятие "поток" полностью поглощается понятием "процесс".

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

Взаимосвязь между заданиями, процессами и потоками показана на рис. 5.2.

Рис. 5.2. Задания, процессы, потоки

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

Таким образом, существует иерархия рабочих единиц операционной системы, которая применительно к Windows выглядит следующим образом (рис. 5.3).

Возникает вопрос: зачем нужна такая сложная организация работ, выполняемых операционной системой? Ответ нужно искать в развитии теории и практики мультипрограммирования, цель которой – в обеспечении максимально эффективного использования главного ресурса вычислительной системы – центрального процессора (нескольких центральных процессоров).

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

Рис. 5.3. Иерархия рабочих единиц ОС

Этот документ я написал когда учился на третьем курсе универа. Вещь замечательно прокатила как курсовой проект [ к ней прилагалась еще программа, в которой демонстрировались возможности многонитевого программирования - небольшой файл-сервер [возможно, будет время, поищу его в залежах архивов и напишу еще одну статейку]. Так вот, третий курс прошел, а курсовая работа осталась. Чтобы не выкидывать в архивы эту работу я решил представить ее вам. Посему убедительная просьба не пугаться чересчур официальному языку - все-таки научный доклад. Претензии по неверной пунктуации расматриваются в последнюю очередь - это все-таки научный доклад студента физтеха:]

Лев Пяхтин /Lev L. Pyakhtin/, also known as .cens


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

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

Для оперативного хранения рабочих данных существует динамическая область памяти /куча /. Эта память выделяется динамически и использование ее от процесса к процессу меняется. С помощью кучи программист может предоставить процессу дополнительную память.

Автоматически, при запуске программы, переменные размещаются в стеке [стек служит хранилищем для временного хранения переменных и адресов возврата из процедур ]. Обычно при выполнении или в режиме ожидания выполнения процессы находятся в оперативной памяти компьютера. Довольно большая ее часть резервируется ядром операционной системы, и только к оставшейся ее части могут получить доступ пользователи. Одновременно в оперативной памяти может находится несколько процессов. Память, используемая процессором, разбивается на сегменты, называемые страницами /page/ . Каждая страница имеет определенный размер, который фиксирует операционная система в зависимости от типа компьютера. Если все страницы используются и возникает потребность в новой странице, то та страница которая используется меньше остальных помещается в область подкачки /swap area/ , а на ее месте создается новая. Но если область подкачки не была определена, то с помощью специальных команд можно разместить область подкачки в файле. Но есть такие страницы которые всегда должны находится в оперативной памяти, которые называются невытесняемыми /nonpreemptable pages/ . Обычно такие страницы используются ядром, либо программами подкачки. Главная особенность в постраничном использовании памяти заключается в том, что процесс может использовать больше памяти, чем есть на самом деле.

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

Для каждого процесса создается свой блок управления, который помещается в системную таблицу процессов, находящихся в ядре. Эта таблица представляет собой массив структур блоков управления процессами. В каждом блоке содержатся данные:

  • слово состояния процесса
  • приоритет
  • величина кванта времени, выделенного системным планировщиком
  • степень использования системным процессором
  • признак диспетчеризации
  • идентификатор пользователя, которому принадлежит процесс
  • эффективный идентификатор пользователя
  • реальный и эффективный идентификаторы группы
  • группа процесса
  • идентификатор процесса и идентификатор родительского процесса
  • размер образа, размещаемого в области подкачки
  • размер сегментов кода и данных
  • массив сигналов, ожидающих обработки.
Чтобы система функционировала должным образом, ядру необходимо отслеживать все эти данные.
Создание процесса
Процесс порождается с помощью системного вызова fork() . При этом вызове происходит проверка на наличие свободной памяти, доступной для размещения нового процесса. Если требуемая память доступна, то создается процесс-потомок текущего процесса, представляющий собой точную копию вызывающего процесса. При этом в таблице процессов для нового процесса строится соответствующая структура. Новая структура создается также в таблице пользователя. При этом все ее переменные инициализируются нулями. Этому процессу присваивается новый уникальный идентификатор, а идентификатор родительского процесса запоминается в блоке управления процессом.

Вы скажете: все это замечательно, но если новый процесс - всегда копия существующего, то каким образом в системе работают разные программы? И откуда берется самая первая из них?

Процессы, выполняющие разные программы, образуются благодаря применению имеющихся в стандартной библиотеке Unix функций "семейства exec ": execl, execlp, execle, execv, execve, execvp . Эти функции отличаются форматом вызова, но в конечном итоге делают одну и ту же вещь: замещают внутри текущего процесса исполняемый код на код, содержащийся в указанном файле. Файл может быть не только двоичным исполняемым файлом Linux, но и скриптом командного интерпретатора, и двоичным файлом другого формата [например, классом java, исполняемым файлом DOS ]

Таким образом, операция запуска программы, которая в DOS и Windows выполняется как единое целое, в Linux [и в Unix вообще ] разделена на две: сначала производится запуск, а потом определяется, какая программа будет работать. Есть ли в этом смысл и не слишком ли велики накладные расходы? Ведь создание копии процесса предполагает копирование весьма значительного объема информации.

Смысл в данном подходе определенно есть. Очень часто программа должна совершить некоторые действия еще до того, как начнется собственно ее выполнение. Например, создать неименованный канал для общения с другими процессами. Такие каналы создаются системным вызовом pipe, речь о котором пойдет ниже. Реализуется это очень просто - сначала "отпочковываются" процессы, затем проделываются необходимые операции /вызов pipe() / и только после этого вызван exec.

Аналогичного результата [как показывает, в частности, пример Windows NT ] можно было бы добиться и при запуске программы за один шаг, но более сложным путем. Что же касается накладных расходов, то они чаще всего оказываются пренебрежимо малыми: при создании копии процесса его индивидуальные данные физически никуда не копируются. Вместо этого используется техника, известная под названием copy-on-write /копирование при записи /: страницы данных обоих процессов особым образом помечаются, и только тогда, когда один процесс пытается изменить содержимое какой-либо своей страницы, она дублируется.

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

Затем из памяти удаляются сегменты кода и данных, а сам процесс переходит в состояние зомби [ в поле Stat такие процессы помечаются буквой "Z ". Зомби не занимает процессорного времени, но строка в таблице процессов остается, и соответствующие структуры ядра не освобождаются. После завершения родительского процесса "осиротевший" зомби на короткое время становится потомком init , после чего уже "окончательно умирает" ]. И, наконец, родительский процесс должен очистить все ресурсы, занимаемые дочерними процессами.

Если родительский процесс по какой-то причине завершится раньше дочернего, последний становится "сиротой" /orphaned process/ . Такие "Сироты" также автоматически "усыновляются" программой init , выполняющейся в процессе с номером 1, которая и принимает сигнал об их завершении.

Также, процесс может впасть в "сон", который не удается прервать: в поле Stat это обозначается буквой "D ". Процесс, находящийся в таком состоянии, не реагирует на системные запросы и может быть уничтожен только перезагрузкой системы.

Взаимодействие процессов
Самым распространенным средством взаимодействия процессов являются сокеты /sockets/ . Программы подключаются к сокету и выдают запрос на привязку к нужному адресу. Затем данные передаются от одного сокета к другому в соответствии с указанным адресом.

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

Каналы реализованы в двух классах. Первый из них создается с помощью системного вызова pipe() . При этом для обмена информацией между процессами инициализируется специальная структура в ядре. Вызывающему процессу два дескриптора файла, один - для чтения, а другой для записи информации. Затем, когда процесс порождает новый процесс, между двумя процессами открывается коммуникационный канал. Другим типом каналов являются именованные каналы . При их использовании с управляющей структурой в ядре связывается специальный каталог, через который два автономных процесса могут обмениваться данными. При этом, каждый процесс должен открыть канал в виде обычных файлов [ один - для чтения, другой - для записи ]. Затем операции ввода/вывода выполняются обычным образом.

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

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

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

Нити
Что такое нить?
Точно также как многозадачная операционная система может делать несколько вещей одновременно при помощи разных процессов, один процесс может делать много вещей при помощи нескольких нитей. Каждая нить представляет собой независимо выполняющийся поток управления со своим счетчиком команд, регистровым контекстом и стеком. Понятия процесса и нити очень тесно связаны и поэтому трудноотличимы, нити даже часто называют легковесными процессами. Основные отличия процесса от нити заключаются в том, что, каждому процессу соответствует своя независимая от других область памяти, таблица открытых файлов, текущая директория и прочая информация уровня ядра . Нити же не связаны непосредственно с этими сущностями. У всех нитей принадлежащих данному процессу всё выше перечисленное общее, поскольку принадлежит этому процессу. Кроме того, процесс всегда является сущностью уровня ядра, то есть ядро знает о его существовании, в то время как нити зачастую является сущностями уровня пользователя и ядро может ничего не знать о ней. В подобных реализациях все данные о нити хранятся в пользовательской области памяти, и соответственно такие процедуры как порождение или переключение между нитями не требуют обращения к ядру и занимают на порядок меньше времени.
Создание нити и идеология POSIX API
При выбранном нами для изучения низкоуровневом подходе к поддержке нитей в языке все операции связанные с ними выражаются явно через вызовы функций. Соответственно теперь, когда мы получили общее представление о том, что такое нить, пора рассмотреть вопрос каким же образом мы можем создавать нити и управлять ими в наших программах. Напомню, что мы говорим о программах на языке C и интерфейсе поддержки нитей соответствующему стандарту POSIX . Согласно нему нить создается при помощи следующего вызова:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*start)(void *), void *arg)

Упрощенно вызов pthread_create[&thr,NULL,start,NULL] создаст нить которая начнет выполнять функцию start и запишет в переменную thr идентификатор созданной нити. На примере этого вызова мы подробно рассмотрим несколько вспомогательных концепций POSIX API с тем, чтобы не останавливаться на них дальше.

Первый аргумент этой функции thread - это указатель на переменную типа pthread_t , в которую будет записан идентификатор созданной нити, который в последствии можно будет передавать другим вызовам, когда мы захотим сделать что-либо с этой нитью. Здесь мы сталкиваемся с первой особенностью POSIX API, а именно с непрозрачностью базовых типов. Дело в том, что мы практически ничего не можем сказать про тип pthread_t . Мы не знаем целое ли это или указатель? Мы не можем сказать существует ли упорядоченность между значениями этого типа, то есть можно ли выстроить из них неубывающую цепочку. Единственное что сказано в стандарте, это что эти значения можно копировать, и что используя вызов int pthread_equal мы можем установить что оба идентификатора thr1 и thr2 идентифицируют одну и ту же нить [ при этом они вполне могут быть неравны в смысле оператора равенства ]. Подобными свойствами обладает большинство типов используемых в данном стандарте, более того, как правило, значения этих типов даже нельзя копировать!

Второй аргумент этой функции attr - это указатель на переменную типа pthread_attr_t , которая задает набор некоторых свойств создаваемой нити. Здесь мы сталкиваемся со второй особенностью POSIX API, а именно с концепцией атрибутов. Дело в том, что в этом API во всех случаях, когда при создании или инициализации некоторого объекта необходимо задать набор неких дополнительных его свойств, вместо указания этого набора при помощи набора параметров вызова используется передача предварительно сконструированного объекта представляющего этот набор атрибутов. Такое решение имеет, по крайней мере, два преимущества. Во-первых, мы можем зафиксировать набор параметров функции без угрозы его изменения в дальнейшем, когда у этого объекта появятся новые свойства. Во-вторых, мы можем многократно использовать один и тот же набор атрибутов для создания множества объектов.

Третий аргумент вызова pthread_create это указатель на функцию типа void* () . Именно эту функцию и начинает выполнять вновь созданная нить, при этом в качестве параметра этой функции передается четвертый аргумент вызова pthread_create . Таким образом можно с одной стороны параметризовать создаваемую нить кодом который она будет выполнять, с другой стороны параметризовать ее различными данными передаваемыми коду.

Функция pthread_create возвращает нулевое значение в случае успеха и ненулевой код ошибки в случае неудачи. Это также одна из особенностей POSIX API, вместо стандартного для Unix подхода когда функция возвращает лишь некоторый индикатор ошибки а код ошибки устанавливает в переменной errno , функции Pthreads API возвращают код ошибки в результате своего аргумента. Очевидно, это связано с тем что с появлением в программе нескольких нитей вызывающих различные функции возвращающие код ошибки в одну и ту же глобальную переменную errno , наступает полная неразбериха, а именно нет никакой гарантии что код ошибки который сейчас находится в этой переменной является результатом вызова произошедшего в этой а не другой нити. И хотя из-за огромного числа функций уже использующих errno библиотека нитей и обеспечивает по экземпляру errno для каждой нити, что в принципе можно было бы использовать и в самой библиотеке нитей, однако создатели стандарта выбрали более правильный а главное более быстрый подход при котором функции API просто возвращают коды ошибки.

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

int pthread_join(pthread_t thread, void** value_ptr)

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

В случае если нас чем-то не устраивает возврат значения через pthread_join , например, нам необходимо получить данные в нескольких нитях, то следует воспользоваться каким либо другим механизмом, например, можно организовать очередь возвращаемых значений, или возвращать значение в структуре указатель на которую передают в качестве параметра нити. То есть использование pthread_join это вопрос удобства, а не догма, в отличие от случая пары fork() - wait() . Дело тут в том, что в случае если мы хотим использовать другой механизм возврата или нас просто не интересует возвращаемое значение то мы можем отсоединить [ detach ] нить, сказав тем самым что мы хотим освободить ресурсы связанные с нитью сразу по завершению функции нити. Сделать это можно несколькими способами. Во-первых, можно сразу создать нить отсоединенной, задав соответствующий объект атрибутов при вызове pthread_create . Во-вторых, любую нить можно отсоединить вызвав в любой момент ее жизни [ то есть до вызова pthread_join() ] функцию


int pthread_detach(pthread_t thread)

И указав ей в качестве параметра идентификатор нити. При этом нить вполне может отсоединить саму себя получив свой идентификатор при помощи функции pthread_t pthread_self . Следует подчеркнуть, что отсоединение нити никоим образом не влияет на процесс ее выполнения, а просто помечает нить как готовую по своем завершении к освобождению ресурсов. Фактически тот же pthread_join , всего лишь получает возвращаемое значение и отсоединяет нить.

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

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


int pthread_exit(void *value_ptr)

Этот вызов завершает выполняемую нить, возвращая в качестве результата ее выполнения value_ptr . Реально при вызове этой функции нить из нее просто не возвращается. Надо обратить также внимание на тот факт, что функция exit() по-прежнему завершает процесс, то есть в том числе уничтожает все потоки.

Как известно, программа на Си начинается с выполнения функции main() . Нить, в которой выполняется данная функция, называется главной или начальной [ так как это первая нить в приложении ]. С одной стороны это нить обладает многими свойствами обычной нити, для нее можно получить идентификатор, она может быть отсоединена, для нее можно вызвать pthread_join из какой-либо другой нити. С другой стороны она обладает некоторыми особенностями, отличающих ее о других нитей. Во-первых, возврат из этой нити завершает весь процесс, что бывает иногда удобно, так как не надо явно заботиться о завершении остальных нитей. Если мы не хотим чтобы по завершении этой нити остальные нити были уничтожены, то следует воспользоваться функцией pthread_exit . Во-вторых, у функции этой нити не один параметр типа void* как у остальных, а пара argc-argv . Строго говоря функция main не является функцией нити так как в большинстве ОС, она сама вызывается некими функциями которые подготавливают ее выполнение автоматически формируемыми компилятором. В-третьих, многие реализации отводят на стек начальной нити гораздо больше памяти чем на стеки остальных нитей. Очевидно, это связано с тем что уже существует много однониточных приложений [ то есть традиционных приложений ] требующих значительного объема стека, а от автора нового многониточного приложения можно потребовать ограниченности аппетитов.

Жизненный цикл нити
Рассмотрим теперь жизненный цикл нити, а именно последовательность состояний в которых пребывает нить за время своего существования. В целом можно выделить четыре таких состояния:
Состояние нити Что означает
Готова /Ready/ Нить готова к выполнению, но ожидает процессора. Возможно она только что была создана, была вытеснена с процессора другой нитью, или только что была разблокирована [ вышла из соответствующего состояния ].
Выполняется /Running/ Нить сейчас выполняется. Следует заметить, что на многопроцессорной машине может быть несколько нитей в таком состоянии.
Заблокирована /Blocked/ Нить не может выполняться, так как ожидает чего-либо. Например, окончания операции ввода-вывода, сигнала от условной переменной, получения mutex и т.п.
Завершена /Terminated/ Нить была завершена, например, вследствие возврата из функции нити, вызова pthread_exit , прерывания выполнения нити /cancellation/. Нить при этом еще не была отсоединена и для нее не была вызвана функция pthread_join . Как только происходит одно из этих событий, нить перестает существовать.

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

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

Выполняющаяся нить, скорее всего, рано или поздно либо перейдет в состояние "заблокирована", вызвав операцию ожидающую чего-то, например, окончания ввода-вывода, прихода сигнала или поднятия семафора, либо перейдет в состояние "готова" будучи снята с процессора или более высокоприоритетной нитью или просто потому что исчерпала свой квант времени. Здесь надо подчеркнуть разницу между вытеснением /preemption/ то есть снятием с процессора вследствие появления готовой более приоритетной задачи, и снятием нити вследствие истечения ее кванта времени. Дело в том, что типичная ошибка предполагать что первое подразумевает второе. Существуют политики планирования которые просто не поддерживают понятие кванта времени. Такова, например политика планирования по умолчанию для нитей в ОС Solaris. Такова одна из стандартных [в смысле POSIX] политик планирования реального времени SCHED_FIFO .

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

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

Keywords: процессы, процессы в Unix, процессы в Юникс, процессы в Linux, процессы в Линукс, pthread_create, pthread_mutex, cl1mp3x, Потоки, Нити, Сокеты, Очередь, Мьютексы, Семафоры, Многозадачность

Все документы и программы на этом сайте собраны ТОЛЬКО для образовательных целей, мы не отвечаем ни за какие последствия, которые имели место как следствие использования этих материалов\программ. Вы используете все вышеперечисленное на свой страх и риск.

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

Когда говорят о процессах, то тем самым хотят отметить, что операци­онная система поддерживает их обособленность: у каждого процесса имеется свое виртуальное адресное пространство, каждому процессу назначаются свои ресур­сы – файлы, окна, семафоры и так далее. Такая обособленность нужна для того, чтобы защитить один процесс от другого, поскольку они, совместно используя все ре­сурсы ВМ, конкурируют друг с другом. В общем случае процессы могут быть никак не связаны между собой и даже могут принадлежать раз­ным пользователям, разделяющим одну вычислительную машину. Другими сло­вами, в случае процессов ОС считает их совершенно несвязанными и независимыми. При этом именно ОС берет на себя роль арбитра в конкуренции между процессами по поводу ресурсов.

Однако желательно иметь еще и возможность задействовать внутренний парал­лелизм, который может быть в самих процессах. Такой внутренний параллелизм встречается достаточно часто, а его использование позволяет ускорить реализацию процессов. Например, некоторые операции, выполняемые приложением, могут требо­вать для своего исполнения достаточно длительного использования центрального процессора. В этом случае при интерактивной работе с приложением пользова­тель вынужден долго ожидать завершения заказанной операции и не может управ­лять приложением до тех пор, пока операция не выполнится до самого конца. Такие ситуации встречаются достаточно часто, например, при обработке боль­ших изображений в графических редакторах. Если же программные модули, исполняющие такие длительные операции, оформлять в виде самостоятельных «подпроцессов» (так называемых легковесных или облегченных процессов), которые будут выполняться парал­лельно с другими подобными «подпроцессами», то у пользователя по­является возможность параллельно выполнять несколько операций в рамках од­ного приложения (процесса). Такие «подпроцессы» принято называть потоками или «нитями» (thread) . «Подпроцессы» (потоки) называют легковесными потому, что операционная система не должна для них организовывать полноценную вирту­альную машину. Потоки не имеют своих собственных ресурсов, они развива­ются в том же виртуальном адресном пространстве, могут пользоваться теми же файлами, виртуальными устройствами и иными ресурсами, что и данный про­цесс. Единственное, что им необходимо иметь, – это процессорный ресурс. В од­нопроцессорной машине потоки разделяют между собой процессорное время так же, как это делают обычные процессы, а в многопроцессорной машине могут выполняться одновременно, если не встречают конкуренции из-за об­ращения к иным ресурсам.

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

В качестве примера использования многопоточности можно привести электрон­ную таблицу или текстовый процессор. Если они были разработаны с учетом возможностей многопоточной обработки, то пользователь может запросить пересчет своего ра­бочего листа или слияние нескольких документов и одновременно продолжать заполнять таблицу или открывать для редактирования следующий документ. Особенно эффективно можно использовать многопоточ-ность для выполнения распределенных приложений: например, многопоточный сервер может парал­лельно выполнять запросы сразу нескольких клиентов.

Итак, сущность «поток» была введена для того, чтобы именно с помощью этих единиц распределять процессорное время между возможными работами. Сущ­ность «процесс» предполагает, что при диспетчеризации нужно учитывать все ресурсы, закрепленные за ним. А при манипулировании потоками можно менять только их контекст, если происходит переключение с одного потока на другой в рам­ках одного процесса. Все остальные вычислительные ресурсы при этом не затра­гиваются. Каждый процесс всегда состоит по крайней мере из одного потока, и только в случае наличия внутреннего параллелизма становится возможным «расще­пление» этого одного потока на несколько параллельных.

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

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

Все потоки имеют одно и то же виртуальное адресное про­странство своего процесса. Это означает, что они разделяют одни и те же гло­бальные переменные. Поскольку каждый поток может иметь доступ к каждому виртуальному адресу, один поток может исполь-зовать стек другого потока.

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

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

Резюме

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

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

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

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

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

Порядок взаимосвязи процессов определяется правилами синхро­низации. Часть программы, в которой осуществляется доступ к разделяемым данным, называется критической секцией (критической областью) программы. Ситуации, когда два или более процессов обрабатывают разделяемые данные, и конечный результат зависит от соотношения скоростей процессов, называются гонками. Исключение эффекта гонок по отношению к некоторому ресурсу обеспечивается различными способами взаимного исключения, такими как использование механизмов блокирующих переменных, семафоров, мьютексов.

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

Потоки («нити») представляют собой «легковесную» (или «облегченную») форму процессов. Потоки в отличие от процессов не имеют своих собственных ресурсов, они развива­ются в том же виртуальном адресном пространстве, могут пользоваться теми же файлами, виртуальными устройствами и иными ресурсами, что и данный про­цесс. Единственное, что им необходимо иметь, – это процессорный ресурс. Многопоточность обеспечивает возможность параллельного вы­полнения несколько видов операций в одной прикладной программе. Параллель­ные вычисления способствуют более эффективному использованию ресур­сов центрального процессора и существенно уменьшают суммарное время выполнения программ.

Контрольные вопросы и задания

1. Дайте определение понятиям «вычислительный процесс» и «ресурс».

2. Как классифицируются процессы и ресурсы?

3. Решение каких задач включает в себя планирование процессов?

4. Перечислите виды состояний процессов.

5. Что такое контекст и дескриптор процесса?

6. Каковы возможные причины выхода процесса из состояния выполнения?

7. Охарактеризуйте наиболее распространенные алгоритмы плани-рования процессов.

8. В чем заключается сущность алгоритмов планирования процес-сов, основанных на квантовании?

9. Опишите различия между относительными и абсолютными прио-ритетами.

10. Дайте определение понятиям вытесняющая и невытесняющая многозадачность.

11. Дайте определение понятию «прерывание».

12. Какие этапы реализуются механизмом обработки прерываний?

13. Приведите примеры внешних прерываний.

14. Какими событиями вызываются внутренние прерывания?

15. Поясните понятие программного прерывания.

16. Охарактеризуйте применяемые дисциплины обслуживания прерываний.

17. В чем заключается механизм синхронизации процессов?

18. Что такое «критическая область» программы и с какими целями она используется?

19. С помощью каких механизмов осуществляется взаимодействие процессов?

20. В чем различие понятий очередей процессов и взаимоблоки-ровок процессов?

21. Перечислите способы преодоления тупиковых ситуаций при взаимодействии процессов.

22. Опишите понятие монитора как высокоуровневого средства синхронизации процессов.

23. В чем различие понятий «процесс» и «поток»?

24. С какой целью в ОС используется механизм потоков («нитий»)?