Лекция 2. Процессы и нити: механизмы обмена информацией
Дополнительная литература
http://www.vlrscc.ru/uch/os_new/zan/z22/lek_4_3.pdf и https://rus-linux.net/lib.php?name=/denis/art/process.html и https://it.wikireading.ru/1731 и https://www.parallel.uran.ru/book/export/html/524, https://citforum.ru/operating_systems/unix/contents.shtml
Понятие процесса
Раньше мы использовали термины «программа» и «задание» для описания способов построения ОС. Но теперь мы начинаем знакомиться с деталями функционирования современных компьютерных систем, и нам придется уточнить терминологию.
Рассмотрим следующий пример. Два студента запускают программу извлечения квадратного корня. Один хочет вычислить квадратный корень из 4, а второй – из 1. С точки зрения студентов, запущена одна и та же программа; с точки зрения компьютерной системы, ей приходится заниматься двумя различными вычислительными процессами, так как разные исходные данные приводят к разному набору вычислений. Следовательно, на уровне происходящего внутри вычислительной системы мы не можем использовать термин "программа" в пользовательском смысле слова.
Рассматривая системы пакетной обработки, мы ввели понятие "задание" как совокупность программы, набора команд языка управления заданиями, необходимых для ее выполнения, и входных данных. С точки зрения студентов, они, подставив разные исходные данные, сформировали два различных задания.
Может быть, термин "задание" подойдет нам для описания внутреннего функционирования компьютерных систем? Пусть оба студента пытаются извлечь квадратный корень из 1, то есть пусть они сформировали идентичные задания, но загрузили их в вычислительную систему со сдвигом по времени. В то время как одно из выполняемых заданий приступило к печати полученного значения и ждет окончания операции ввода-вывода, второе только начинает исполняться. Задания не идентичны, так как состояние процесса их выполнения различно. Следовательно, и слово "задание" в пользовательском смысле не может применяться для описания происходящего в вычислительной системе.
Это происходит потому, что термины "программа" и "задание" предназначены для описания статических, неактивных объектов. Программа же в процессе исполнения является динамическим, активным объектом. По ходу ее работы компьютер обрабатывает различные команды и преобразует значения переменных. Для выполнения программы операционная система должна выделить определенное количество оперативной памяти, закрепить за ней определенные устройства ввода-вывода или файлы (откуда должны поступать входные данные и куда нужно доставить полученные результаты), то есть зарезервировать определенные ресурсы из общего числа ресурсов всей вычислительной системы. Их количество и конфигурация с течением времени могут изменяться. Для описания таких активных объектов внутри компьютерной системы вместо терминов "программа" и "задание" мы будем использовать новый термин – "процесс".
Определение
Понятие процесса характеризует некоторую совокупность набора исполняющихся команд, ассоциированных с ним ресурсов (выделенная для исполнения память или адресное пространство, используемые файлы и устройства ввода-вывода и т. д.) и текущего момента его выполнения (значения регистров, программного счетчика, состояние стека и значения переменных), находящуюся под управлением операционной системы. Не существует взаимно-однозначного соответствия между процессами и программами, обрабатываемыми вычислительными системами.
В некоторых операционных системах для работы определенных программ может организовываться более одного процесса или один и тот же процесс может исполнять последовательно несколько различных программ. Более того, даже в случае обработки только одной программы в рамках одного процесса нельзя считать, что процесс представляет собой просто динамическое описание кода исполняемого файла, данных и выделенных для них ресурсов. Процесс находится под управлением операционной системы, поэтому в нем может выполняться часть кода ее ядра (не находящегося в исполняемом файле!), как в случаях, специально запланированных авторами программы (например, при использовании системных вызовов), так и в непредусмотренных ситуациях (например, при обработке внешних прерываний).
Состояния процесса
Усредненная структура
Понятно, что реально на однопроцессорной компьютерной системе в каждый момент времени может исполняться только один процесс. Для мультипрограммных вычислительных систем псевдопараллельная обработка нескольких процессов достигается с помощью переключения процессора с одного процесса на другой. Пока один процесс выполняется, остальные ждут своей очереди.
Как видим, каждый процесс может находиться в двух состояниях: процесс исполняется и процесс не исполняется.
Процесс, находящийся в состоянии "процесс исполняется", через некоторое время может быть завершен операционной системой или приостановлен и снова переведен в состояние "процесс не исполняется". Приостановка процесса происходит по двум причинам: для его дальнейшей работы потребовалось какое-либо событие (например, завершение операции ввода-вывода) или истек временной интервал, отведенный операционной системе для работы данного процесса. После этого операционная система по определенному алгоритму выбирает для исполнения один из процессов, находящихся в состоянии "процесс не исполняется", и переводит его в состояние "процесс исполняется". Новый процесс, появляющийся в системе, первоначально помещается в состояние "процесс не исполняется".
Составим диаграмму состояния, принятую в курсе:
При рождении процесс получает в свое распоряжение адресное пространство, в которое загружается программный код процесса; ему выделяются стек и системные ресурсы; устанавливается начальное значение программного счетчика этого процесса и т. д. Родившийся процесс переводится в состояние "готовность".
Операционная система, пользуясь каким-либо алгоритмом планирования, выбирает один из готовых процессов и переводит его в состояние "исполнение". В состоянии "исполнение" происходит непосредственное выполнение программного код процесса. Выйти из этого состояния процесс может по трем причинам:
- операционная система прекращает его деятельность;
- он не может продолжать свою работу, пока не произойдет некоторое событие, и операционная система переводит его в состояние ожидание;
- в результате возникновения прерывания в вычислительной системе (например, прерывания от таймера по истечении предусмотренного времени выполнения) его возвращают в состояние готовность.
Из состояния "ожидание" процесс попадает в состояние "готовность" после того, как ожидаемое событие произошло, и он снова может быть выбран для исполнения.
При завершении своей деятельности процесс из состояния "исполнение" попадает в состояние "закончил исполнение".
В конкретных операционных системах состояния процесса могут быть еще более детализированы. Так, например, модель состояний процессов для операционной системы Windows NT содержит 7 различных состояний, а для операционной системы Unix – 9.
Высокая сложность
Диаграмма состояний Unix
Полный набор состояний процесса содержится в следующем перечне:
- Процесс выполняется в режиме задачи.
- Процесс выполняется в режиме ядра.
- Процесс не выполняется, но готов к запуску под управлением ядра.
- Процесс приостановлен и находится в оперативной памяти.
- Процесс готов к запуску, но программа подкачки (нулевой процесс) должна еще загрузить процесс в оперативную память, прежде чем он будет запущен под управлением ядра. Это состояние будет предметом обсуждения в главе 9 при рассмотрении системы подкачки.
- Процесс приостановлен и программа подкачки выгрузила его во внешнюю память, чтобы в оперативной памяти освободить место для других процессов.
- Процесс возвращен из привилегированного режима (режима ядра) в непривилегированный (режим задачи), ядро резервирует его и переключает контекст на другой процесс. Об отличии этого состояния от состояния 3 (готовность к запуску) пойдет речь ниже.
- Процесс вновь создан и находится в переходном состоянии; процесс существует, но не готов к выполнению, хотя и не приостановлен. Это состояние является начальным состоянием всех процессов, кроме нулевого.
- Процесс вызывает системную функцию exit и прекращает существование. Однако, после него осталась запись, содержащая код выхода, и некоторая хронометрическая статистика, собираемая родительским процессом. Это состояние является последним состоянием процесса.
Рисунок представляет собой полную диаграмму переходов процесса из состояния в состояние. Рассмотрим с помощью модели переходов типичное поведение процесса. Ситуации, которые будут обсуждаться, несколько искусственны и процессы не всегда имеют дело с ними, но эти ситуации вполне применимы для иллюстрации различных переходов. Начальным состоянием модели является создание процесса родительским процессом с помощью системной функции fork(); из этого состояния процесс неминуемо переходит в состояние готовности к запуску (3 или 5). Для простоты предположим, что процесс перешел в состояние "готовности к запуску в памяти" (3). Планировщик процессов в конечном счете выберет процесс для выполнения и процесс перейдет в состояние "выполнения в режиме ядра", где доиграет до конца роль, отведенную ему функцией fork().
После всего этого процесс может перейти в состояние "выполнения в режиме задачи". По прохождении определенного периода времени может произойти прерывание работы процессора по таймеру и процесс снова перейдет в состояние "выполнения в режиме ядра". Как только программа обработки прерывания закончит работу, ядру может понадобиться подготовить к запуску другой процесс, поэтому первый процесс перейдет в состояние "резервирования", уступив дорогу второму процессу. Состояние "резервирования" в действительности не отличается от состояния "готовности к запуску в памяти" (пунктирная линия на рисунке, соединяющая между собой оба состояния, подчеркивает их эквивалентность), но они выделяются в отдельные состояния, чтобы подчеркнуть, что процесс, выполняющийся в режиме ядра, может быть зарезервирован только в том случае, если он собирается вернуться в режим задачи. Следовательно, ядро может при необходимости подкачивать процесс из состояния "резервирования". При известных условиях планировщик выберет процесс для исполнения и тот снова вернется в состояние "выполнения в режиме задачи".
Когда процесс выполняет вызов системной функции, он из состояния "выполнения в режиме задачи" переходит в состояние "выполнения в режиме ядра". Предположим, что системной функции требуется ввод-вывод с диска и поэтому процесс вынужден дожидаться завершения ввода-вывода. Он переходит в состояние "приостанова в памяти", в котором будет находиться до тех пор, пока не получит извещения об окончании ввода-вывода. Когда ввод-вывод завершится, произойдет аппаратное прерывание работы центрального процессора и программа обработки прерывания возобновит выполнение процесса, в результате чего он перейдет в состояние "готовности к запуску в памяти".
Предположим, что система выполняет множество процессов, которые одновременно никак не могут поместиться в оперативной памяти, и программа подкачки (нулевой процесс) выгружает один процесс, чтобы освободить место для другого процесса, находящегося в состоянии "готов к запуску, но выгружен". Первый процесс, выгруженный из оперативной памяти, переходит в то же состояние. Когда программа подкачки выбирает наиболее подходящий процесс для загрузки в оперативную память, этот процесс переходит в состояние "готовности к запуску в памяти". Планировщик выбирает процесс для исполнения и он переходит в состояние "выполнения в режиме ядра". Когда процесс завершается, он исполняет системную функцию exit, последовательно переходя в состояния "выполнения в режиме ядра" и, наконец, в состояние "прекращения существования".
Процесс может управлять некоторыми из переходов на уровне задачи. Во-первых, один процесс может создать другой процесс. Тем не менее, в какое из состояний процесс перейдет после создания (т.е. в состояние "готов к выполнению, находясь в памяти" или в состояние "готов к выполнению, но выгружен") зависит уже от ядра. Процессу эти состояния не подконтрольны. Во-вторых, процесс может обратиться к различным системным функциям, чтобы перейти из состояния "выполнения в режиме задачи" в состояние "выполнения в режиме ядра", а также перейти в режим ядра по своей собственной воле. Тем не менее, момент возвращения из режима ядра от процесса уже не зависит; в результате каких-то событий он может никогда не вернуться из этого режима и из него перейдет в состояние "прекращения существования" (см. раздел 7.2, где говорится о сигналах). Наконец, процесс может завершиться с помощью функции exit по своей собственной воле, но как указывалось ранее, внешние события могут потребовать завершения процесса без явного обращения к функции exit. Все остальные переходы относятся к жестко закрепленной части модели, закодированной в ядре, и являются результатом определенных событий, реагируя на них в соответствии с правилами, сформулированными в этой и последующих главах. Некоторые из правил уже упоминались: например, то, что процесс может выгрузить другой процесс, выполняющийся в ядре.
Операции над процессами и связанные с ними понятия
Набор операций
Процесс не может перейти из одного состояния в другое самостоятельно. Изменением состояния процессов занимается операционная система. Все операции можно разбить на несколько групп:
- создание процесса – завершение процесса;
- приостановка процесса (перевод из состояния "исполнение" в состояние "готовность") – запуск процесса (перевод из состояния "готовность" в состояние "исполнение");
- блокирование процесса (перевод из состояния "исполнение" в состояние "ожидание") – разблокирование процесса (перевод из состояния "ожидание" в состояние "готовность").
Операции создания и завершения процесса являются одноразовыми, так как применяются к процессу не более одного раза (некоторые системные процессы не завершаются никогда). Все остальные операции, связанные с изменением состояния процессов, будь то запуск или блокировка, как правило, являются многоразовыми. Рассмотрим подробнее, как операционная система выполняет операции над процессами.
Process Control Block и контекст процесса
Каждый процесс представляется в системе некоторой структурой данных. Эта структура содержит информацию, специфичную для данного процесса:
- состояние, в котором находится процесс;
- программный счетчик процесса или, другими словами, адрес команды, которая должна быть выполнена для него следующей;
- содержимое регистров процессора;
- данные, необходимые для планирования использования процессора и управления памятью (приоритет процесса, размер и расположение адресного пространства и т. д.);
- учетные данные (идентификационный номер процесса, какой пользователь инициировал его работу, общее время использования процессора данным процессом и т. д.);
- сведения об устройствах ввода-вывода, связанных с процессом (например, какие устройства закреплены за процессом, таблицу открытых файлов).
Для нас важно лишь то, что для любого процесса вся информация, необходимая для совершения операций над ним, доступна операционной системе. Для простоты изложения будем считать, что она хранится в одной структуре данных. Мы будем называть ее PCB (Process Control Block) или блоком управления процессом, он является моделью процесса для операционной системы. Любая операция, производимая операционной системой над процессом, вызывает определенные изменения в PCB. В рамках принятой модели состояний процессов содержимое PCB между операциями остается постоянным.
Информацию, для хранения которой предназначен блок управления процессом, удобно для дальнейшего изложения разделить на две части. Содержимое всех регистров процессора (включая значение программного счетчика) будем называть регистровым контекстом процесса, а все остальное – системным контекстом процесса. Знания регистрового и системного контекстов процесса достаточно для того, чтобы управлять его работой в операционной системе, совершая над ним операции, но недостаточно, чтобы полностью охарактеризовать процесс. Операционную систему не интересует, какой код и какие данные находятся в адресном пространстве процесса. С точки зрения пользователя, наоборот, наибольший интерес представляет содержимое адресного пространства процесса, возможно, наряду с регистровым контекстом определяющее последовательность преобразования данных и полученные результаты. Код и данные, находящиеся в адресном пространстве процесса, будем называть его пользовательским контекстом. Совокупность регистрового, системного и пользовательского контекстов процесса для краткости принято называть просто контекстом процесса. В любой момент времени процесс полностью характеризуется своим контекстом.
INFO
В Linux нельзя напрямую посмотреть структуру Process Control Block (PCB) так, как это показано в учебниках по операционным системам, потому что PCB — это внутренняя структура ядра, недоступная напрямую пользователю. Однако информация, хранящаяся в PCB, доступна косвенно через различные системные интерфейсы и файлы в специальных каталогах.
В Linux PCB реализован как структура task_struct
в исходном коде ядра.
Как получить доступ к информации из PCB?
Хотя сам task_struct
недоступен, ядро предоставляет виртуальную файловую систему /proc
, в которой для каждого процесса есть каталог с его PID, содержащий почти всю информацию из PCB.
1. Просмотр информации о процессе через /proc/<PID>/
Замените <PID>
на реальный идентификатор процесса.
Пример:
ls /proc/1234
В этом каталоге вы найдёте файлы, соответствующие полям PCB:
Файл | Что содержит |
---|---|
/proc/<PID>/status | Основная информация: PID, PPID, UID, состояние, память и т.д. |
/proc/<PID>/stat | Статистика процесса в одной строке (технический формат) |
/proc/<PID>/cmdline | Команда, которой запущен процесс |
/proc/<PID>/exe | Символическая ссылка на исполняемый файл |
/proc/<PID>/cwd | Символическая ссылка на текущую директорию процесса |
/proc/<PID>/fd/ | Каталог с файловыми дескрипторами процесса |
/proc/<PID>/mem | Образ памяти процесса (только через gdb или ptrace ) |
/proc/<PID>/limits | Лимиты ресурсов (например, max open files) |
/proc/<PID>/sched | Информация о планировщике (приоритет, политика) |
/proc/<PID>/maps | Карта памяти процесса (сегменты кода, стека, кучи) |
2. Практические примеры
✅ Посмотреть основную информацию о процессе:
cat /proc/1234/status
Пример вывода:
Name: bash
State: S (sleeping)
Tgid: 1234
Pid: 1234
PPid: 1000
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
VmSize: 1234 kB
VmRSS: 567 kB
...
✅ Узнать, в каком состоянии процесс:
grep "State:" /proc/1234/status
✅ Посмотреть команду запуска:
cat /proc/1234/cmdline
⚠️ Иногда выводится без пробелов — можно использовать tr '\0' ' '
:
cat /proc/1234/cmdline | tr '\0' ' '
✅ Посмотреть дерево памяти:
cat /proc/1234/maps
✅ Посмотреть открытые файлы:
ls -l /proc/1234/fd/
3. Использование команд для получения данных PCB
Многие утилиты читают /proc
и отображают данные из PCB в удобной форме:
Команда | Что показывает | Источник данных |
---|---|---|
ps aux | PID, TTY, %CPU, %MEM, COMMAND | /proc/<PID>/stat , /status |
top | Реальное время мониторинга | /proc/<PID>/stat , /stat |
pstree | Иерархия процессов | /proc/<PID>/stat , PPID |
lsof -p <PID> | Открытые файлы | /proc/<PID>/fd/ |
pmap <PID> | Карта памяти | /proc/<PID>/maps |
cat /proc/<PID>/environ | Переменные окружения | /proc/<PID>/environ |
4. Особенности и ограничения
- Обычные пользователи могут читать только свои
/proc/<PID>/...
(кроме некоторых системных процессов). - Некоторые файлы (например,
/proc/<PID>/mem
) доступны только черезptrace
(например, с помощьюgdb
), и только если процесс не запущен в другом пространстве (например, в контейнере). - Информация обновляется динамически —
/proc
— это виртуальная файловая система.
Одноразовые операции
Сложный жизненный путь процесса начинается с его рождения. В очень простых системах (например, в системах, спроектированных для работы только одного конкретного приложения) все процессы могут быть порождены на этапе старта системы. Более сложные операционные системы создают процессы динамически, по мере необходимости. Инициатором рождения нового процесса после старта операционной системы может выступить либо процесс пользователя, совершивший специальный системный вызов, либо сама операционная система. Процесс, инициировавший создание нового процесса, принято называть процессом-родителем (parent process), а вновь созданный процесс – процессом-ребенком (child process). Процессы-дети могут в свою очередь порождать новых детей и т. д., образуя, в общем случае, внутри системы набор генеалогических деревьев процессов – генеалогический лес.
init (PID 1)
/ \
(PID 2) (PID 3)
/ \ \
(PID 4)(PID 5) (PID 6)
Следует отметить, что все пользовательские процессы вместе с некоторыми процессами операционной системы принадлежат одному и тому же дереву леса. Стрелочка означает отношение родитель–ребенок.
В операционной системе GNU/Linux присвоение идентификационных номеров процессов начинается с номера 0, который получает процесс kernel
при старте операционной системы. Этот номер впоследствии не может быть присвоен никакому другому процессу. Максимально возможное значение для номера процесса в Linux на базе 32-разрядных процессоров Intel составляет init (PID=1)
.
INFO
В Linux существует несколько способов вывести дерево процессов — то есть иерархию родительских и дочерних процессов, где наглядно видно, какой процесс кем был порождён. Ниже приведены основные команды для отображения дерева процессов, от простых к более продвинутым.
pstree
— отображение процессов в виде дерева
Самый простой и наглядный способ — команда pstree
.
Выводит дерево всех процессов в виде иерархии, начиная с init
или systemd
.
pstree
Показывает PID каждого процесса в скобках:
pstree -p
Вывод:
systemd(1)─┬─sshd(500)───sshd(1234)───bash(1235)───pstree(1236)
├─cron(400)
└─apache2(600)───apache2(601)
Отображает имя пользователя, запустившего процесс.
pstree -u
Показывает дерево процессов только указанного пользователя:
pstree <имя_пользователя>
Показывает путь от корня до указанного процесса (включая всех родителей):
pstree -p -s <PID>
Вывод:
systemd(1)───sshd(500)───sshd(1234)───bash(1235)
ps
— команда для детального просмотра процессов
Команда ps
может выводить процессы в древовидном формате с помощью опций.
📌 Примеры:
ps aux --forest
Выводит все процессы (a
— по всем пользователям, u
— в формате пользователя, x
— включая процессы без терминала) с визуальным отображением иерархии («лесенкой»).
Пример вывода:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 123456 7890 ? Ss 10:00 0:01 /sbin/init
root 2 0.0 0.0 0 0 ? S 10:00 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 10:00 0:00 \_ [ksoftirqd/0]
root 500 0.1 0.2 234567 12345 ? Ss 10:00 0:05 \_ /usr/sbin/sshd
den 1234 0.0 0.1 111111 9876 ? Ss 10:05 0:00 \_ sshd: den@pts/0
den 1235 0.1 0.3 222222 20000 pts/0 Ss 10:05 0:01 \_ -bash
den 1236 0.0 0.0 100000 3000 pts/0 R+ 10:06 0:00 \_ ps aux --forest
Флаг --forest
рисует графические линии, показывающие родство процессов.
При рождении процесса система заводит новый PCB с состоянием процесса "рождение" и начинает его заполнять, получает собственный уникальный идентификационный номер.
Для хранения идентификационного номера процесса в операционной системе отводится ограниченное количество битов, для соблюдения уникальности номеров количество одновременно присутствующих в ней процессов должно быть ограничено. После завершения какого-либо процесса его освободившийся идентификационный номер может быть повторно использован для другого процесса.
Обычно для выполнения своих функций процесс-ребенок требует определенных ресурсов: памяти, файлов, устройств ввода-вывода и т. д. Способы: Новый процесс может получить в свое распоряжение некоторую часть родительских ресурсов, или может получить свои ресурсы непосредственно от операционной системы. Информация о выделенных ресурсах заносится в PCB.
После наделения процесса-ребенка ресурсами необходимо занести в его адресное пространство программный код, значения данных, установить программный счетчик.
В первом случае для запуска новой программы необходимо сначала создать копию процесса-родителя, а затем процесс-ребенок должен заменить свой пользовательский контекст с помощью специального системного вызова. Во втором случае процесс-ребенок загружается новой программой из какого-либо файла. Операционная система Unix разрешает порождение процесса только первым способом;
Порождение нового процесса как дубликата процесса-родителя приводит к возможности существования программ (т. е. исполняемых файлов), для работы которых организуется более одного процесса. Возможность замены пользовательского контекста процесса по ходу его работы (т. е. загрузки для исполнения новой программы) приводит к тому, что в рамках одного и того же процесса может последовательно выполняться несколько различных программ.
После того как процесс наделен содержанием, в PCB дописывается оставшаяся информация, и состояние нового процесса изменяется на "готовность".
Процесс-родитель может продолжать свое выполнение одновременно с выполнением процесса-ребенка, а может ожидать завершения работы некоторых или всех своих "детей".
После завершения работы процесса, освобождает все ассоциированные с ним ресурсы, делая соответствующие записи в PCB. При этом сам PCB не уничтожается, а остается в системе еще некоторое время. Это связано с тем, что процесс-родитель после завершения процесса-ребенка может запросить операционную систему о причине "смерти" порожденного им процесса и/или статистическую информацию о его работе. Подобная информация сохраняется в PCB отработавшего процесса до запроса процесса-родителя или до конца его деятельности, после чего все следы завершившегося процесса окончательно исчезают из системы. В операционной системе Unix процессы, находящиеся в состоянии "закончил исполнение", принято называть процессами-зомби.
Следует заметить, что в ряде операционных систем гибель процесса-родителя приводит к завершению работы всех его "детей". В других операционных системах процессы-дети продолжают свое существование и после окончания работы процесса-родителя. При этом возникает необходимость изменения информации в PCB процессов-детей о породившем их процессе для того, чтобы генеалогический лес процессов оставался целостным.
Как правило, "осиротевшие" процессы "усыновляются" одним из системных процессов, который порождается при старте операционной системы и функционирует все время, пока она работает.
INFO
Практическая демонстрация: наблюдение за состояниями процессов с помощью top
Для изучения состояний процессов в операционной системе Linux мы будем использовать утилиту top
— мощный инструмент мониторинга, позволяющий в реальном времени наблюдать за работой процессов, загрузкой процессора, использованием памяти и многим другим.
Рассмотрим типичный вывод программы top
:
15:03:11 up 58 min, 4 users, load average: 0,02, 0,01, 0,00
52 processes: 51 sleeping, 1 running, 0 zombie, 0 stopped
CPU states: 0,8% user, 0,6% system, 0,0% nice, 0,0% iowait, 98,3% idle
Mem: 127560k av, 124696k used, 2864k free, 0k shrd, 660k buff
13460k active, 17580k inactive
Swap: 152576k av, 8952k used, 143624k free 28892k cached
PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME COMMAND
3097 den 15 0 1128 1128 832 R 2,8 0,8 0:00 top
1 root 8 0 120 84 60 S 0,0 0,0 0:04 init
2 root 12 0 0 0 0 SW 0,0 0,0 0:00 keventd
3 root 19 19 0 0 0 SWN 0,0 0,0 0:00 ksoftirqd_CPU0
...
1. Общая информация (верхние строки)
- Первая строка:
15:03:11
— текущее системное время.up 58 min
— время, в течение которого система работает без перезагрузки.4 users
— количество пользователей, вошедших в систему.load average: 0,02, 0,01, 0,00
— средняя нагрузка на систему за последние 1, 5 и 15 минут.
Примечание: Средняя нагрузка (load average) — это среднее количество процессов, находящихся в состоянии выполнения (R) или ожидания ресурсов (D). Значение меньше 1 на одноядерной системе говорит о низкой нагрузке.
Вторая строка:
- Общее количество процессов: 52.
51 sleeping
— ожидают событий (например, ввода/вывода).1 running
— активно выполняется (или готов к выполнению).0 zombie, 0 stopped
— нет "зомби" и остановленных процессов.
Следующие строки содержат информацию о загрузке CPU и использовании памяти (RAM и swap). Эта информация полезна для диагностики, но сейчас мы сосредоточимся на состояниях процессов.
2. Таблица процессов: ключевые колонки
В таблице top
отображаются важные атрибуты каждого процесса. Нас в первую очередь интересуют:
- PID — уникальный идентификатор процесса.
- USER — пользователь, запустивший процесс.
- STAT — состояние процесса.
- COMMAND — команда, запустившая процесс.
Коды состояний (STAT):
Символ | Значение |
---|---|
R | Running — процесс выполняется или готов к выполнению |
S | Sleeping — процесс ожидает события (например, ввода) |
D | Uninterruptible sleep — ожидание ввода/вывода (нельзя прервать) |
T | Stopped — процесс приостановлен (например, сигналом SIGSTOP) |
Z | Zombie — процесс завершился, но остался в таблице процессов |
< | Процесс с повышенным приоритетом (отрицательное значение nice ) |
N | Процесс с пониженным приоритетом (положительное значение nice ) |
3. Эксперимент 1: процесс в состоянии R (Running)
Создадим простой bash-скрипт process
:
#!/bin/bash
x=1
while [ $x -lt 10 ]
do
x=2
done
Сделаем его исполняемым и запустим:
chmod +x process
./process
В другом терминале выполним:
ps -a | grep process
Увидим PID процесса (например, 4035). Затем отслеживаем его через top
:
top -p 4035
Наблюдение: в колонке STAT
будет стоять R
— процесс находится в состоянии выполнения, так как он постоянно выполняет цикл.
4. Эксперимент 2: переход в состояние T (Stopped)
Вернёмся к терминалу, где работает ./process
, и нажмём Ctrl+Z. Процесс будет приостановлен.
Теперь снова запустим top -p 4035
:
PID STAT COMMAND
4035 T process
Вывод: процесс перешёл в состояние T
— он остановлен и не потребляет CPU.
5. Эксперимент 3: процесс в состоянии S (Sleeping)
Остановим процесс (если он ещё запущен) командой kill 4035
, затем модифицируем скрипт:
#!/bin/bash
sleep 10m # спим 10 минут
x=1
while [ $x -lt 10 ]
do
x=2
done
Запустим его снова и через ps
найдём PID, затем — top -p <PID>
.
Наблюдение: в колонке STAT
будет S
— процесс "спит", ожидая окончания команды sleep
.
6. Эксперимент 4: создание процесса-зомби (Zombie)
Процесс-зомби — это завершившийся дочерний процесс, чья запись осталась в таблице процессов, потому что родитель ещё не считал его статус завершения.
Создадим программу на C:
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
int pid;
int status, died;
pid = fork();
switch(pid) {
case -1:
printf("can't fork\n");
exit(-1);
case 0:
printf(" I'm the child of PID %d\n", getppid());
printf(" My PID is %d\n", getpid());
exit(0); // завершаемся сразу
default:
printf("I'm the parent.\n");
printf(" My PID is %d\n", getpid());
sleep(10); // ждём 10 секунд
died = wait(&status); // только теперь забираем статус
}
}
Компилируем:
gcc -o zombie zombie.c
Запускаем:
./zombie
Программа выведет PID дочернего процесса (например, 1148). Быстро переключаемся на другой терминал и выполняем:
top -p 1148
Наблюдение:
PID STAT COMMAND
1148 Z zombie <defunct>
Процесс стал зомби (Z
) — он уже завершился, но запись о нём осталась, пока родитель не вызвал wait()
.
Важно: зомби не потребляют память, но занимают слот в таблице процессов. Если их слишком много — это может привести к исчерпанию PID.
Сигналы в UNIX-системах: управление процессами на уровне ОС
Каждый процесс в UNIX-подобных операционных системах может получать и реагировать на сигналы — специальные программные прерывания, используемые для уведомления процесса о различных событиях.
Что такое сигнал?
Сигнал — это асинхронное уведомление, отправляемое процессу операционной системой или другим процессом. Сигналы используются для:
- Завершения процесса (например, при нажатии Ctrl+C).
- Приостановки или возобновления выполнения.
- Обработки ошибок (например, сегментация памяти).
- Взаимодействия между процессами.
Всего существует 64 сигнала, пронумерованных от 1 до 64:
- 1–32 — стандартные сигналы (например,
SIGTERM
,SIGKILL
,SIGSTOP
). - 33–64 — сигналы реального времени (real-time signals), которые используются для специализированных задач и не рассматриваются в рамках базового курса.
Сигналы можно вызывать по номеру (например, 9
) или по имени (например, SIGKILL
).
Чтобы увидеть список всех доступных сигналов, выполните:
kill -l
Вывод будет примерно таким:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
...
9) SIGKILL 15) SIGTERM 19) SIGSTOP
INFO
Как процессы реагируют на сигналы?
Процесс может:
- Игнорировать сигнал (кроме двух исключений).
- Выполнить действие по умолчанию (например, завершиться).
- Обработать сигнал с помощью пользовательской функции (в программах на C и других языках).
Исключения: два неуловимых сигнала
Существуют два сигнала, которые нельзя игнорировать и нельзя перехватить:
Сигнал | Номер | Действие |
---|---|---|
SIGKILL | 9 | Принудительное завершение процесса. Немедленно останавливает процесс без возможности завершить работу корректно. |
SIGSTOP | 19 | Принудительная остановка процесса. Процесс нельзя продолжить, пока не получит SIGCONT . |
⚠️ Эти сигналы всегда действуют независимо от кода процесса. Их используют, когда процесс не отвечает на другие команды.
Отправка сигналов: команды kill
и killall
Для отправки сигналов процессам используются две основные команды:
Команда | Назначение |
---|---|
kill | Отправляет сигнал процессу по его PID (идентификатору). |
killall | Отправляет сигнал всем процессам с указанным именем. |
Синтаксис:
kill [опция] <PID>
killall [опция] <имя_процесса>
Примеры:
- Завершить процесс с PID 785 (по умолчанию отправляется
SIGTERM
, номер 15):
kill 785
- Принудительно убить процесс (сигнал
SIGKILL
):
kill -9 785
# или
kill -KILL 785
- Остановить процесс (без завершения):
kill -19 785
# или
kill -STOP 785
- Возобновить остановленный процесс:
kill -18 785
# или
kill -CONT 785
- Завершить все процессы с именем
firefox
:
killall firefox
- Принудительно завершить все процессы
chrome
:
killall -9 chrome
🔐 Важно: обычный пользователь может отправлять сигналы только своим процессам. Для управления чужими процессами требуется права суперпользователя (root
).
Интерактивное управление процессами: top
как инструмент администрирования
Утилита top
— не только средство наблюдения, но и интерактивный инструмент управления процессами. Она объединяет функции ps
(просмотр) и kill
(управление), позволяя в реальном времени:
- Отслеживать нагрузку на CPU и память.
- Управлять процессами.
- Изменять приоритеты.
После запуска top
вы можете управлять им с помощью клавиатуры. Вот основные команды:
Клавиша | Действие |
---|---|
k | Отправить сигнал процессу. top запросит PID и номер/имя сигнала (по умолчанию SIGTERM ). |
r | Изменить приоритет (nice value) процесса. top запросит PID и новое значение nice . |
P | Сортировка по загрузке CPU (%CPU ) — режим по умолчанию. |
M | Сортировка по использованию памяти (%MEM ). |
u | Показать процессы только указанного пользователя. |
i | Переключение: показывать все процессы или только активные (R — running). |
h | Показать справку по командам. |
q | Выйти из top . |
Практический пример: завершение зависшего процесса через top
- Запустите
top
. - Найдите PID проблемного процесса (например,
firefox
, который "завис"). - Нажмите
k
. - Введите PID процесса.
- Введите номер сигнала:
9
(дляSIGKILL
). - Процесс будет немедленно завершён.
💡 Это удобно, когда вы не хотите запоминать PID — вы видите его прямо в интерфейсе.
Многоразовые операции
Одноразовые операции приводят к изменению количества процессов, находящихся под управлением операционной системы, и всегда связаны с выделением или освобождением определенных ресурсов.
Многоразовые операции, напротив, не приводят к изменению количества процессов в операционной системе и не обязаны быть связанными с выделением или освобождением ресурсов.
Запуск процесса. Из числа процессов, находящихся в состоянии "готовность", операционная система выбирает один процесс для последующего исполнения.
Для избранного процесса операционная система обеспечивает наличие в оперативной памяти информации, необходимой для его дальнейшего выполнения. Далее состояние процесса изменяется на "исполнение", восстанавливаются значения регистров для данного процесса и управление передается команде, на которую указывает счетчик команд процесса. Все данные, необходимые для восстановления контекста, извлекаются из PCB процесса, над которым совершается операция.
Приостановка процесса. Работа процесса приостанавливается в результате какого-либо прерывания. Процессор автоматически сохраняет счетчик команд и, возможно, один или несколько регистров в стеке исполняемого процесса, а затем передает управление по специальному адресу обработки данного прерывания. На этом деятельность hardware по обработке прерывания завершается. По указанному адресу обычно располагается одна из частей операционной системы. Она сохраняет динамическую часть системного и регистрового контекстов процесса в его PCB, переводит процесс в состояние "готовность" и приступает к обработке прерывания, то есть к выполнению определенных действий, связанных с возникшим прерыванием.
Блокирование процесса. Процесс блокируется, когда он не может продолжать работу, не дождавшись возникновения какого-либо события в вычислительной системе. Для этого он обращается к операционной системе с помощью определенного системного вызова. Операционная система обрабатывает системный вызов (инициализирует операцию ввода-вывода, добавляет процесс в очередь процессов, дожидающихся освобождения устройства или возникновения события, и т. д.) и, при необходимости сохранив нужную часть контекста процесса в его PCB, переводит процесс из состояния "исполнение" в состояние "ожидание".
Разблокирование процесса. После возникновения в системе какого-либо события операционной системе нужно точно определить, какое именно событие произошло. Затем операционная система проверяет, находился ли некоторый процесс в состоянии "ожидание" для данного события, и если находился, переводит его в состояние "готовность", выполняя необходимые действия, связанные с наступлением события (инициализация операции ввода-вывода для очередного ожидающего процесса и т. п.).
Переключение контекста
Деятельность мультипрограммной операционной системы состоит из цепочек операций, выполняемых над различными процессами, и сопровождается переключением процессора с одного процесса на другой.
Давайте для примера упрощенно рассмотрим, как в реальности может протекать операция разблокирования процесса, ожидающего ввода-вывода. При исполнении процессором некоторого процесса (на рисунке – процесс 1) возникает прерывание от устройства ввода-вывода, сигнализирующее об окончании операций на устройстве. Над выполняющимся процессом производится операция приостановки. Далее операционная система разблокирует процесс, инициировавший запрос на ввод-вывод (на рисунке – процесс 2) и осуществляет запуск приостановленного или нового процесса, выбранного при выполнении планирования (на рисунке был выбран разблокированный процесс). Как мы видим, в результате обработки информации об окончании операции ввода-вывода возможна смена процесса, находящегося в состоянии "исполнение".
Для корректного переключения процессора с одного процесса на другой необходимо сохранить контекст исполнявшегося процесса и восстановить контекст процесса, на который будет переключен процессор.
Такая процедура сохранения/восстановления работоспособности процессов называется переключением контекста. Время, затраченное на переключение контекста, не используется вычислительной системой для совершения полезной работы и представляет собой накладные расходы, снижающие производительность системы. Оно меняется от машины к машине и обычно колеблется в диапазоне от 1 до 1000 микросекунд. Существенно сократить накладные расходы в современных операционных системах позволяет расширенная модель процессов, включающая в себя понятие threads (нити исполнения или просто нити).
Взаимодействующие процессы
Для достижения поставленной цели различные процессы (возможно, даже принадлежащие разным пользователям) могут исполняться псевдопараллельно на одной вычислительной системе или параллельно на разных вычислительных системах, взаимодействуя между собой.
Процессы не могут взаимодействовать, не общаясь, то есть не обмениваясь информацией. "Общение" процессов обычно приводит к изменению их поведения в зависимости от полученной информации. Если деятельность процессов остается неизменной при любой принятой ими информации, то это означает, что они на самом деле в "общении" не нуждаются. Процессы, которые влияют на поведение друг друга путем обмена информацией, принято называть кооперативными или взаимодействующими процессами, в отличие от независимых процессов, не оказывающих друг на друга никакого воздействия.
Различные процессы в вычислительной системе изначально представляют собой обособленные сущности. Работа одного процесса не должна приводить к нарушению работы другого процесса. Для этого, в частности, разделены их адресные пространства и системные ресурсы, и для обеспечения корректного взаимодействия процессов требуются специальные средства и действия операционной системы. Нельзя просто поместить значение, вычисленное в одном процессе, в область памяти, соответствующую переменной в другом процессе, не предприняв каких-либо дополнительных усилий. Давайте рассмотрим основные аспекты организации совместной работы процессов.
Категории средств обмена информацией
Процессы могут взаимодействовать друг с другом, только обмениваясь информацией. По объему передаваемой информации и степени возможного воздействия на поведение другого процесса все средства такого обмена можно разделить на три категории.
Сигнальные Передается минимальное количество информации – один бит, "да" или "нет". Используются, как правило, для извещения процесса о наступлении какого-либо события. Степень воздействия на поведение процесса, получившего информацию, минимальна. Все зависит от того, знает ли он, что означает полученный сигнал, надо ли на него реагировать и каким образом. Неправильная реакция на сигнал или его игнорирование могут привести к трагическим последстви- ям. Вспомним профессора Плейшнера из кинофильма "Семнадцать мгновений весны". Сигнал тревоги – цветочный горшок на подоконнике – был ему передан, но профессор проигнорировал его. И к чему это привело?
Канальные "Общение" процессов происходит через линии связи, предоставленные операционной системой, и напоминает общение людей по телефону, с помощью записок, писем или объявлений. Объем передаваемой информации в единицу времени ограничен пропускной способностью линий связи. С увеличением количества информации возрастает и возможность влияния на поведение другого процесса.
Разделяемая память Два или более процессов могут совместно использовать некоторую область адресного пространства. Созданием разделяемой памяти занимается операционная система (если, конечно, ее об этом попросят). "Общение" процессов напоминает совместное проживание студентов в одной комнате общежития. Возможность обмена информацией максимальна, как, впрочем, и влияние на поведение другого процесса, но требует повышенной осторожности (если вы переложили на другое место вещи вашего соседа по комнате, а часть из них еще и выбросили). Использование разделяемой памяти для передачи/получения информации осуществляется с помощью средств обычных языков программирования, в то время как сигнальным и канальным средствам коммуникации для этого необходимы специальные системные вызовы. Разделяемая память представляет собой наиболее быстрый способ взаимодействия процессов в одной вычислительной системе.
Логическая организация механизма передачи информации
При рассмотрении любого из средств коммуникации нас будет интересовать не их физическая реализация (общая шина данных, прерывания, аппаратно разделяемая память и т. д.), а логическая, определяющая в конечном счете механизм их использования. Некоторые важные аспекты логической реализации являются общими для всех категорий средств связи, некоторые относятся к отдельным категориям. Давайте кратко охарактеризуем основные вопросы, требующие разъяснения при изучении того или иного способа обмена информацией.
Как устанавливается связь?
Могу ли я использовать средство связи непосредственно для обмена информацией сразу после создания процесса или первоначально необходимо предпринять определенные действия для инициализации обмена? Например, для использования общей памяти различными процессами потребуется специальное обращение к операционной системе, которая выделит необходимую область адресного пространства. Но для передачи сигнала от одного процесса к другому никакая инициализация не нужна. В то же время передача информации по линияи связи может потребовать первоначального резервирования такой линии для процессов, желающих обменяться информацией.
К этому же вопросу тесно примыкает вопрос о способе адресации при использовании средства связи. Если я передаю некоторую информацию, я должен указать, куда я ее передаю. Если я желаю получить некоторую информацию, то мне нужно знать, откуда я могу ее получить.
Различают два способа адресации: прямую и непрямую. В случае прямой адресации взаимодействующие процессы непосредственно общаются друг с другом, при каждой операции обмена данными явно указывая имя или номер процесса, которому информация предназначена или от которого она должна быть получена. Если и процесс, от которого данные исходят, и процесс, принимающий данные, указывают имена своих партнеров по взаимодействию, то такая схема адресации называется симметричной прямой адресацией. Ни один другой процесс не может вмешаться в процедуру симметричного прямого общения двух процессов, перехватить посланные или подменить ожидаемые данные. Если только один из взаимодействующих процессов, например передающий, указывает имя своего партнера по кооперации, а второй процесс в качестве возможного партнера рассматривает любой процесс в системе, например ожидает получения информации от произвольного источника, то такая схема адресации называется асимметричной прямой адресацией.
При непрямой адресации данные помещаются передающим процессом в некоторый промежуточный объект для хранения данных, имеющий свой адрес, откуда они могут быть затем изъяты каким-либо другим процессом. Примером такого объекта может служить обычная доска объявлений или рекламная газета. При этом передающий процесс не знает, как именно идентифицируется процесс, который получит информацию, а принимающий процесс не имеет представления об идентификаторе процесса, от которого он должен ее получить.
При использовании прямой адресации связь между процессами в классической операционной системе устанавливается автоматически, без дополнительных инициализирующих действий. Единственное, что нужно для использования средства связи, -- это знать, как идентифицируются процессы, участвующие в обмене данными.
При использовании непрямой адресации инициализация средства связи может и не требоваться. Информация, которой должен обладать процесс для взаимодействия с другими процессами, - это некий идентификатор промежуточного объекта для хранения данных, если он, конечно, не является единственным и неповторимым в вычислительной системе для всех процессов.
Особенности передачи информации с помощью линий связи (IPC)
IPC (Inter-Process Communication) — Межпроцессное взаимодействие
Как уже говорилось выше, передача информации между процессами посредством линий связи является достаточно безопасной по сравнению с использованием разделяемой памяти и более информативной по сравнению с сигнальными средствами коммуникации. Кроме того, разделяемая память не может быть использована для связи процессов, функционирующих на различных вычислительных системах. Возможно, именно поэтому каналы связи из средств коммуникации процессов получили наибольшее распространение. Коснемся некоторых вопросов, связанных с логической реализацией канальных средств коммуникации.
Буферизация
Может ли линия связи сохранять информацию, переданную одним процессом, до ее получения другим процессом или помещения в промежуточный объект? Каков объем этой информации? Иными словами, речь идет о том, обладает ли канал связи буфером и каков объем этого буфера. Здесь можно выделить три принципиальных варианта.
- Буфер нулевой емкости. или отсутствует. Никакая информация не может сохраняться на линии связи. В этом случае процесс, посылающий информацию, должен ожидать, пока процесс, принимающий информацию, не соблаговолит ее получить, прежде чем заниматься своими дальнейшими делами (в реальности этот случай никогда не реализуется).
- Буфер ограниченной емкости. Размер буфера равен n, то есть линия связи не может хранить до момента получения более чем n единиц информации. Если в момент передачи данных в буфере хватает места, то передающий процесс не должен ничего ожидать. Информация просто копируется в буфер. Если же в момент передачи данных буфер заполнен или места недостаточно, то необходимо задержать работу процесса отправителя до появления в буфере свободного пространства.
- Буфер неограниченной емкости. Теоретически это возможно, но практически вряд ли реализуемо. Процесс, посылающий информацию, никогда не ждет окончания ее передачи и приема другим процессом.
При использовании канального средства связи с непрямой адресацией под емкостью буфера обычно понимается количество информации, которое может быть помещено в промежуточный объект для хранения данных.
Поток ввода/вывода и сообщения
Существует две модели передачи данных по каналам связи - поток ввода-вывода и сообщения. При передаче данных с помощью потоковой модели операции передачи/приема информации вообще не интересуются содержимым данных. Процесс, прочитавший 100 байт из линии связи, не знает и не может знать, были ли они переданы одновременно, т. е. одним куском или порциями по 20 байт, пришли они от одного процесса или от разных. Данные представляют собой простой поток байтов, без какой-либо их интерпретации со стороны системы. Примерами потоковых каналов связи могут служить pipe и FIFO, описанные ниже.
Одним из наиболее простых способов передачи информации между процессами по линиям связи является передача данных через pipe (канал, трубу или, как его еще называют в литературе, конвейер). Представим себе, что у нас есть некоторая труба в вычислительной системе, в один из концов которой процессы могут "сливать" информацию, а из другого конца принимать полученный поток. Такой способ реализует потоковую модель ввода/вывода. Информацией о расположении трубы в операционной системе обладает только процесс, создавший ее. Этой информацией он может поделиться исключительно со своими наследниками - процессами-детьми и их потомками. Поэтому использовать pipe для связи между собой могут только родственные процессы, имеющие общего предка, создавшего данный канал связи.
INFO
В Linux каждая программа работает с тремя стандартными потоками ввода-вывода:
- stdin (0) — стандартный ввод (по умолчанию: клавиатура)
- stdout (1) — стандартный вывод (по умолчанию: терминал)
- stderr (2) — стандартный поток ошибок (вывод ошибок)
Примеры:
# Вывод команды в файл
ls > files.txt
# Перенаправление ошибок в файл
ls /noexist 2> error.log
# Использование pipe для передачи данных
ps aux | grep firefox
ps aux
— выводит список всех запущенных процессов.|
— символ pipe, передаёт вывод первой команды на вход второй.grep firefox
— фильтрует строки, содержащие словоfirefox
.
Если разрешить процессу, создавшему трубу, сообщать о ее местонахождении в системе другим процессам, сделав вход и выход трубы каким-либо образом видимыми для всех остальных, например, зарегистрировав ее в операционной системе под определенным именем, мы получим объект, который принято называть FIFO или именованный pipe. Именованный pipe может использоваться для организации связи между любыми процессами в системе.
INFO
🔧 Создание FIFO:
mkfifo /tmp/mypipe
1. В одном терминале — пишем:
echo "Привет из FIFO" > /tmp/mypipe
(команда блокируется, пока нет получателя)
2. В другом терминале — читаем:
cat /tmp/mypipe
После этого данные передаются, и echo
завершается.
✅ Особенности:
- FIFO — это специальный файл (
ls -l /tmp/mypipe
покажетp------
). - Данные передаются по принципу очереди: первым вошёл — первым вышел.
- Поддерживает только одностороннюю передачу.
- Используется для межпроцессного взаимодействия (IPC) в сценариях, логировании, сервисах.
📌 Удаление:
rm mypipe
В модели сообщений процессы налагают на передаваемые данные некоторую структуру. Весь поток информации они разделяют на отдельные сообщения, вводя между данными, по крайней мере, границы сообщений. Примером границ сообщений являются точки между предложениями в сплошном тексте или границы абзаца. Кроме того, к передаваемой информации могут быть присоединены указания на то, кем конкретное сообщение было послано и для кого оно предназначено. Примером указания отправителя могут служить подписи под эпиграфами в книге. Все сообщения могут иметь одинаковый фиксированный размер или могут быть переменной длины.
INFO
В Linux передача сообщений реализуется через очереди сообщений POSIX (mqueue
), но для простоты продемонстрируем более простой вариант: DBus
+ systemd
.
К сожалению, systemd напрямую не использует и не заменяет традиционные IPC-механизмы, такие как очереди сообщений System V или POSIX. Однако systemd активно применяет собственную модель обмена сообщениями через D-Bus — шину межпроцессного взаимодействия, которая лежит в основе современных Linux-систем.
✅ Практика: Отправка уведомления через D-Bus с помощью systemd-сервиса
В этом примере мы создадим простой systemd-сервис, который при запуске отправляет уведомление на рабочий стол через D-Bus — демонстрируя, как systemd взаимодействует с другими компонентами системы по модели сообщений.
🧰 Что используется:
systemd
— для управления сервисомD-Bus
— как система обмена сообщениямиnotify-send
— утилита для отправки уведомлений через D-Bus- Переменная окружения
DBUS_SESSION_BUS_ADDRESS
— для доступа к пользовательской шине
🔧 Шаг 1: Создайте скрипт уведомления
Создайте файл:
nano /home/user/notify.sh
Содержимое:
#!/bin/bash
export DISPLAY=:0
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"
/usr/bin/notify-send "Системное уведомление" "Сервис ExampleNotifier запущен!"
Замените user
на ваше имя пользователя.
Путь /run/user/<UID>/bus
— стандартный путь к пользовательской D-Bus шине.
Сделайте скрипт исполняемым:
chmod +x /home/user/notify.sh
🔧 Шаг 2: Создайте systemd-сервис
Создайте файл сервиса:
sudo nano /etc/systemd/system/example-notifier.service
Содержимое:
[Unit]
Description=Пример сервиса с обменом сообщениями через D-Bus
After=graphical-session.target
[Service]
Type=oneshot
User=your_username
ExecStart=/home/your_username/notify.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
🔹 Замените
your_username
на имя вашего пользователя.
🔧 Шаг 3: Включите и запустите сервис
# Перезагрузите конфигурацию systemd
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
# Включить сервис (чтобы запускался при старте)
sudo systemctl enable example-notifier.service
# Запустить вручную
sudo systemctl start example-notifier.service
✅ Результат
На экране пользователя появится уведомление: Системное уведомление
Сервис ExampleNotifier запущен!
💬 Что произошло (модель сообщений):
- Сервис (systemd) — инициировал действие.
- Скрипт — подготовил сообщение.
- notify-send — отправил сообщение через D-Bus (систему обмена сообщениями).
- Демон уведомлений (например,
xfce4-notifyd
илиgnome-shell
) — получил сообщение и отобразил его.
Это классический пример модели обмена сообщениями: отправитель не знает получателя, а шина (D-Bus) обеспечивает доставку.
И потоковые линии связи, и каналы сообщений всегда имеют буфер конечной длины. Когда мы будем говорить о емкости буфера для потоков данных, мы будем измерять ее в байтах. Когда мы будем говорить о емкости буфера для сообщений, мы будем измерять ее в сообщениях.
Другие варианты
Сокеты, разделяемая память, семафоры и т.д. будут обсуждаться на других лекциях.
Надежность средств связи
Dependence
- Дисциплина: Основы теории информации
- Дисциплина: Технологии физического уровня передачи данных
Данный вопрос обширен. Смотри зависимости.
Как завершается связь?
Наконец, важным вопросом при изучении средств обмена данными является вопрос прекращения обмена. Здесь нужно выделить два аспекта: требуются ли от процесса какие-либо специальные действия по прекращению использования средства коммуникации и влияет ли такое прекращение на поведение других процессов. Для способов связи, которые не подразумевали никаких инициализирующих действий, обычно ничего специального для окончания взаимодействия предпринимать не надо. Если же установление связи требовало некоторой инициализации, то, как правило, при ее завершении бывает необходимо выполнить ряд операций, например сообщить операционной системе об освобождении выделенного связного ресурса.
Если кооперативные процессы прекращают взаимодействие согласованно, то такое прекращение не влияет на их дальнейшее поведение. Иная картина наблюдается при несогласованном окончании связи одним из процессов. Если какой-либо из взаимодействующих процессов, не завершивших общение, находится в этот момент в состоянии ожидания получения данных либо попадает в такое состояние позже, то операционная система обязана предпринять некоторые действия для того, чтобы исключить вечное блокирование этого процесса. Обычно это либо прекращение работы ожидающего процесса, либо его извещение о том, что связи больше нет (например, с помощью передачи заранее определенного сигнала).
Нити исполнения
Рассмотренные выше аспекты логической реализации относятся к средствам связи, ориентированным на организацию взаимодействия различных процессов (IPC). Однако усилия, направленные на ускорение решения задач в рамках классических операционных систем, привели к появлению совершенно иных механизмов, к изменению самого понятия "процесс".
В свое время внедрение идеи мультипрограммирования позволило повысить пропускную способность компьютерных систем, т. е. уменьшить среднее время ожидания результатов работы процессов. Но любой отдельно взятый процесс в мультипрограммной системе никогда не может быть выполнен быстрее, чем при работе в однопрограммном режиме на том же вычислительном комплексе. Тем не менее, если алгоритм решения задачи обладает определенным внутренним параллелизмом, мы могли бы ускорить его работу, организовав взаимодействие нескольких процессов.
Для того чтобы реализовать нашу идею, введем новую абстракцию внутри понятия "процесс" - нить исполнения или просто нить (в англоязычной литературе используется термин thread (Поток)). Нити процесса разделяют его программный код, глобальные переменные и системные ресурсы, но каждая нить имеет собственный программный счетчик, свое содержимое регистров и свой стек. Теперь процесс представляется как совокупность взаимодействующих нитей и выделенных ему ресурсов. Процесс, содержащий всего одну нить исполнения, идентичен процессу в том смысле, который мы употребляли ранее. Для таких процессов мы в дальнейшем будем использовать термин "традиционный процесс". Иногда нити называют облегченными процессами или мини-процессами, так как во многих отношениях они подобны традиционным процессом. Нити, как и процессы, могут порождать нити-потомки, правда, только внутри своего процесса, и переходить из одного состояния в другое. Состояния нитей аналогичны состояниям традиционных процессов. Из состояния рождение процесс приходит содержащим всего одну нить исполнения. Другие нити процесса будут являться потомками этой нити-прародительницы.
Мы можем считать, что процесс находится в состоянии готовность, если хотя бы одна из его нитей находится в состоянии готовность и ни одна из нитей не находится в состоянии исполнение.
Мы можем считать, что процесс находится в состоянии исполнение, если одна из его нитей находится в состоянии исполнение.
Процесс будет находиться в состоянии ожидание, если все его нити находятся в состоянии ожидание.
Наконец, процесс находится в состояние закончил исполнение, если все его нити находятся в состоянии закончила исполнение. Пока одна нить процесса заблокирована, другая нить того же процесса может выполняться. Нити разделяют процессор так же, как это делали традиционные процессы, в соответствии с рассмотренными алгоритмами планирования.
Поскольку нити одного процесса разделяют существенно больше ресурсов, чем различные процессы, то операции создания новой нити и переключения контекста между нитями одного процесса занимают значительно меньше времени, чем аналогичные операции для процессов в целом.
Различают операционные системы, поддерживающие нити на уровне ядра и на уровне библиотек. Все сказанное выше справедливо для операционных систем, поддерживающих нити на уровне ядра. В них планирование использования процессора происходит в терминах нитей, а управление памятью и другими системными ресурсами остается в терминах процессов. В операционных системах, поддерживающих нити на уровне библиотек пользователей, и планирование процессора, и управление системными ресурсами осуществляются в терминах процессов. Распределение использования процессора по нитям в рамках выделенного процессу временного интервала осуществляется средствами библиотеки. В подобных системах блокирование одной нити приводит к блокированию всего процесса, ибо ядро операционной системы не имеет представления о существовании нитей. По сути дела, в таких вычислительных системах просто имитируется наличие нитей исполнения.
INFO
Практическая реализация в Linux (POSIX Threads)
В Linux работа с потоками реализована через стандарт POSIX Threads (pthreads). Для использования необходимо подключить заголовочный файл pthread.h
и компилировать программу с флагом -pthread
.
Пример: Параллельное вычисление и вывод
Следующая программа создает массив чисел в главном потоке, а затем порождает дополнительный поток для вычисления суммы его элементов. Главный поток ожидает завершения вычислений и выводит результат.
Код программы (simple_thread.c
):
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // для sleep
// Структура для передачи данных в поток
typedef struct {
int *array;
int size;
int sum;
} thread_data_t;
// Функция, которую будет выполнять поток
void* calculate_sum(void *arg) {
thread_data_t *data = (thread_data_t*) arg;
data->sum = 0;
for (int i = 0; i < data->size; i++) {
data->sum += data->array[i];
}
printf("Дочерний поток: сумма вычислена = %d\n", data->sum);
sleep(5); // Задержка 5 секунд
pthread_exit(NULL);
}
int main() {
// Создаем и заполняем массив
int array_size = 100;
int *numbers = malloc(array_size * sizeof(int));
for (int i = 0; i < array_size; i++) {
numbers[i] = i + 1;
}
// Подготавливаем структуру с данными для потока
thread_data_t thread_data;
thread_data.array = numbers;
thread_data.size = array_size;
pthread_t thread_id; // Идентификатор потока
sleep(5); // Задержка 5 секунд
// Создаем поток
int result = pthread_create(&thread_id, NULL, calculate_sum, (void*) &thread_data);
if (result != 0) {
perror("Ошибка при создании потока");
free(numbers);
return 1;
}
printf("Главный поток: ожидание завершения дочернего потока...\n");
// Ожидаем завершения дочернего потока
result = pthread_join(thread_id, NULL);
if (result != 0) {
perror("Ошибка при join потока");
free(numbers);
return 1;
}
// Результат уже лежит в thread_data.sum
printf("Главный поток: общая сумма элементов массива = %d\n", thread_data.sum);
sleep(5); // Задержка 5 секунд в главном потоке
free(numbers);
return 0;
}
Инструкция по компиляции и запуску:
- Сохраните код в файл
simple_thread.c
. - Откройте терминал и выполните команду компиляции:
gcc -o simple_thread simple_thread.c -pthread
- Запустите полученный исполняемый файл в фоновом режиме:
./simple_thread &
- Сразу же запустим небольшой мониторинг, будет видно как появился и исчез поток:
watch -n 1 ps -T
Что произойдет при выполнении:
- Главный поток (main) создаст и заполнит массив чисел от 1 до 100.
- Главный поток создаст дочерний поток, передав ему указатель на структуру с данными массива.
- Оба потока начинают выполняться конкурентно. Главный поток доходит до
pthread_join
и блокируется, ожидая завершения дочернего. - Дочерний поток вычисляет сумму элементов массива, сохраняет её в переданную структуру и завершается.
- Главный поток продолжает работу, выводит результат, полученный из структуры, и завершает программу.
Этот пример демонстрирует главный принцип: потоки работают с общими данными (массив numbers
и структура thread_data
) без необходимости сложных механизмов IPC, что делает их идеальным инструментом для параллелизации задач внутри одного приложения.
Поддержка нитей в Linux
В Linux нет принципиального различия между процессом и потоком на уровне ядра. Ядро оперирует концепцией "задача" (task). И процесс, и нить — это просто задачи, которые могут иметь общие или различные атрибуты.
- Процесс — это задача, которая имеет свое собственное уникальное виртуальное адресное пространство и другие ресурсы (например, таблицу файлов).
- Нить — это задача, которая разделяет виртуальное адресное пространство и другие ресурсы с другой задачей (или задачами).
Когда вы создаете новый нить с помощью pthread_create()
, системный вызов в глубине (clone()
) передает ядру специальные флаги (например, CLONE_VM
, CLONE_FS
, CLONE_FILES
), которые указывают: "создай новую задачу, но заставь ее разделить память, информацию о файловой системе и таблицу открытых файлов с родительской задачей".
Таким образом, ядро знает о существовании каждого потока и планирует их выполнение независимо друг от друга.