Процессы

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

Создание процессов fork()

Новые процессы создаются вызовом int pid=fork(), который создаёт точную копию вызвавшего его процесса. Пара процессов называются "родительский" и "дочерний" и отличаются друг от друга тремя значениями:

  • уникальный идентификатор процесса PID
  • идентификатор родительского процесса PPID
  • значение, возвращаемое вызовом fork(). В родительском это PID дочернего процесса или ошибка (-1), в дочернем fork() всегда возвращает 0.

После создания, дочерний процесс может загрузить в свою память новую программу (код и данные) из исполняемого файла вызовом execve(const char *filename, char *const argv [], char *const envp[]);

Дочерний процесс связан с родительским значением PPID. В случае завершения родительского процесса PPID меняется на особое значение 1 - PID процесса init.

Процесс init

В момент загрузки ядра создаётся особый процесс с PID=1, который должен существовать до перезагрузки ОС. Все остальные процессы в системе являются его дочерними процессами (или дочерними от дочерних и т.д.). Обычно, в первом процессе исполняется программа init поэтому в дальнейшем я буду называть его "процесс init".

В Linux процесс init защищен от вмешательства других процессов. К нему нельзя подключиться отладчиком, к его памяти нельзя получить доступ через интерфейс procfs, ему не доставляются сигналы, приводящие к завершению процесса. kill -KILL 1 - не сработает. Если же процесс init всё таки завершится, то ядро также завершает работу с соответствующим сообщением.

В современных дистрибутивах классическая программа init заменена на systemd, но сущности процесса с PID=1 это не меняет.

При загрузке Linux ядро сначала монтирует корневую файловую систему на образ диска в оперативной памяти - initrd, затем создаётся процесс с PID=1 и загружает в него программу из файла /init. В initrd из дистрибутива CentOS начальный /init - это скрипт для /bin/bash. Скрипт загружает необходимые драйверы, после чего делает две вещи, необходимые для полноценного запуска Linux:

  1. Перемонтирует корневую файловую систему на основной носитель
  2. Загружает командой exec в свою память основную программу init

Для того, чтобы выполнить эти два пункта через загрузчик в начального init два параметра:

  • основной носитель корневой ФС. Например: root=/dev/sda1
  • имя файла с программой init. Например: init=/bin/bash

Если второй параметр опущен то ищется имя зашитое в начальный init по умолчанию.

Если вы загрузите вместо init /bin/bash, как в моём примере, то сможете завершить первый и единственный процесс командой exit и пронаблюдать сообщение:

Kernel panic - not syncing: Attempted to kill init!

Этот пример так же показывает, как получить права администратора при физическом доступе к компьютеру.

PID

Каждый процесс имеет уникальный на данный момент времени идентификатор PID. Поменять PID процесса невозможно.

Значения PID 0 и 1 зарезервированы. Процесс с PID==0 не используется, PID==1 - принадлежит программе init.

Максимальное значение PID в Linux равняется PID_MAX-1. Текущее значение PID_MAX можно посмотреть командой:

cat /proc/sys/kernel/pid_max

По умолчанию это 2^16 (32768) однако в 64-разрядных Linux его можно увеличить до 2^22 (4194304):

echo 4194303 > /proc/sys/kernel/pid_max

*PID* назначаются последовательно. При создании нового процесса вызовом fork ищется *PID*, больший по значению, чем тот, который был возвращён предыдущим вызовом fork. Если при поиске достигнуто значение pid_max, то поиск продолжается с PID=2. Такое поведение выбрано потому, что некоторые программы могут проверять завершение процесса по существованию его PID. В этой ситуации желательно, чтобы PID не использовался некоторое время после завершения процесса.

UID и GID

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

Процесс может поменять своего владельца и группу в двух случаях:

  1. текущий UID равен 0 (соответствует пользователю root) и процесс обратился к системному вызову setuid(newuid). В этом случае процесс полностью меняет владельца.
  2. процесс обратился к вызову exec(file) загрузив в свою память программу из файла в атрибутах которого выставлен флаг suid или sgid. В этом случае владелец процесса сохраняется, но права доступа будут вычисляться на основе UID и GID файла.
Прикрепленный файлРазмер
Иконка изображения fork-exex-wait.png25.8 КБ

Жизненный цикл процесса

Создание процесса

Вызов newpid=fork() создает новый процесс, являющейся точной копией текущего и отличающийся лишь возвращаемым значением newpid. В родительском процессе newpid равно PID дочернего процесса, в дочернем процессе newpid равно 0. Свой PID можно узнать вызовом mypid=getpid(), родительский – вызовом parentpid=getppid().

Типичный пример

int pid, cpid;
int 
int status;
pid=fork()
if( pid > 0 ){
    cpid=waitpid(&status);
    if( cpid > 0 ){
        printf("Я старый процесс (pid=%i) создал новый (pid=%i) завершившийся с кодом %i\n",
                     getpid(),pid,WEXITSTATUS(status) );
    }
}else if( pid == 0 ){
    printf("Я новый процесс (pid=%i) создан старым (pid=%i)\n",getpid(),getppid());
}else{
    perror("Страшная ошибка:");
}

Запуск программы

В оперативной памяти процесса находятся код и данные, загруженные из файла. При запуске программы из командной строки, обычно создается новый процесс и в его память загружается файл с программой. Загрузка файла делается вызовом одной из функций семейства exec (см. man 3 exec). Функции отличаются способом передачи параметров, а также тем, используется ли переменная окружения PATH для поиска исполняемого файла. Например execl в качестве первого параметра принимает имя исполняемого файла, вторым и последующими – строки аргументы, передаваемые в argv[], и, наконец, последний параметр должен быть NULL, он дает процедуре возможность определить, что параметров больше нет.

int pid=fork();
if( pid > 0 ){
     waitpid(NULL);
}else if( pid == 0 ) {
     if(-1 == execl("/bin/ls","ls","-l",NULL) ) {
          exit(1);
      }
}

Пример exec с двумя ошибками:

if( 0 == execl("/bin/ls","-l",NULL) ){
    printf("Программа ls запущена успешно\n");
}else{
    printf("Программа ls не запущена\n");
}

Ошибка 1: Первый аргумент передаваемый программе это имя самой программы. В данном примере в списке процессов будет видна программа с именем -l, запущенная без параметров.

Ошибка 2: Поскольку код из файла /bin/ls будет загружен в текущий процесс, то старый код и данные, в том числе printf("Программа ls запущена успешно\n"), будет затерты. Первый printf не сработает никогда.

Завершение процесса

Процесс может завершиться, получив сигнал или через системный вызов _exit(int status). status может принимать значения от 0 до 255. По соглашению, status==0 означает успешное завершение программы, а ненулевое значение - означает ошибку. Некоторые программы (например kaspersky для Linux) используют статус для возврата некоторой информации о результатах работы программы.

_exit() может быть вызван несколькими путями.

  • return status; в функции main(). В этом случае _exit() выполнит некая служебная функция, вызывающая main()
  • через библиотечную функцию exit(status), которая завершает работу библиотеки libc и вызывает _exit()
  • явным вызовом _exit()

Удаление завершенного процесса из таблицы процессов

После завершения процесса его pid остается занят - это состояние процесса называется "зомби". Чтобы освободить pid родительский процесс должен дождаться завершения дочернего и очистить таблицу процессов. Это достигается вызовом:

pid_t cpid=waitpid(pid_t pid, int *status, int options)
//или
pid_t cpid=wait(int *status)

Вызов wait(&status); эквивалентен waitpid(-1, &status, 0);

waitpid ждет завершения дочернего процесса и возвращает его PID. Код завершения и обстоятельства завершения заносятся в переменную status. Дополнительно, поведением waitpid можно управлять через параметр options.

  • pid < -1 - ожидание завершения дочернего процесса из группы с pgid==-pid
  • pid == -1 - ожидание завершения любого дочернего процесса
  • pid == 0 - ожидание завершения дочернего процесса из группы, pgid которой совпадает с pgid текущего процесса
  • pid > 0 - ожидание завершения любого дочернего процесса с указанным pid

Опция WNOHANG - означает неблокирующую проверку завершившихся дочерних процессов.

Статус завершения проверяется макросами:

  • WIFEXITED(status) - истина если дочерний процесс завершился вызовом _exit(st)
  • WEXITSTATUS(status) - код завершения st переданный в _exit(st)
  • WIFSIGNALED(status) - истина если дочерний процесс завершился по сигналу
  • WTERMSIG(status) - номер завершившего сигнала
  • WCOREDUMP(status)истина если дочерний процесс завершился с дампом памяти
  • WIFSTOPPED(status) истина если дочерний процесс остановлен
  • WSTOPSIG(status) - номер остановившего сигнала
  • WIFCONTINUED(status) истина если дочерний процесс перезапущен

Основы планирования процессов

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

Прерывания по таймеру происходят в соответствии с квантом времени, выделенному процессу. В Linux квант времени по умолчанию (DEF_TIMESLICE) равен 0,1 секунды, но может быть пересчитан планировщиком процессов (sheduler).

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

В момент возврата в пользовательскую программу происходит доставка сигналов - т.е. вызов процедуры обработчика сигнала, остановка, перезапуск или завершение процесса. Некоторые сигналы (SIGSTOP) - приводят к тому, что процесс включается в список остановленных процессов, которые не поступают в очередь процессов на исполнение. Сигнал SIGCONT возвращает остановленный процесс в очередь процессов на исполнение, сигнал SIGKILL завершает остановленный процесс.

После завершения процесса вызовом _exit() или по сигналу все его ресурсы (память, открытые файлы) освобождаются, но запись в таблице процессов остаётся и занимает PID. Такой процесс называется "зомби" и должен быть явно очищен из таблицы процессов вызовом wait() в родительском процессе. Если родительский процесс завершился раньше дочерних, то всем его дочерним процессам приписывается значение PPID (parent pid) равное 1, возлагая обязательства по очистке от них таблицы процессов на особый процесс init с PID=1.

На диаграмме показаны различные состояния процесса

В Linux команда ps использует следующие обозначения состояния процесса:

  • R выполняется (в том числе в обработчике сигнала) или стоит в очереди на выполнение
  • S системный вызов ожидает ресурс, но может быть прерван
  • D системный вызов ожидает ресурс, и не может быть прерван (обычно это ввод/вывод)
  • T остановлен сигналом
  • t остановлен отладчиком
  • Z "Зомби" - завершён, но не удалён из списка процессов родительским процессом.

Планировщик процессов

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

Простейшая реализация очереди в виде FIFO очень быстра, но не поддерживает приоритеты и многопроцессорность. В Linux 2.6 воспользовались простотой FIFO, добавив к ней несколько усовершенствований:

  1. Было определено 140 приоритетов (100 реального времени + 40 назначаемых динамически), каждый из которых получил свою очередь FIFO. На запуск выбирается первый процесс в самой приоритетной очереди.
  2. В многопроцессорных системах для каждого ядра был сформирован свой набор из 140 очередей. Раз в 0,2 секунды просматриваются размеры очередей процессоров и, при необходимости балансировки, часть процессов переносится с загруженных ядер на менее загруженные
  3. Динамический приоритет назначается процессу в зависимости от отношении времени ожидания ресурсов к времени пребывания в состоянии выполнения. Чем дольше процесс ожидал ресурс, тем выше его приоритет. Таким образом, диалоговые задачи, которые 99% времени ожидают пользовательского ввода, всегда имеют наивысший приоритет.

Ссылка: Планировщик задач Linux

Прикладной программист может дополнительно понизить приоритет процесса функцией int nice(int inc); (в Linux nice() - интерфейс к вызову setpriority()). Большее значение nice означает меньший приоритет. В командной строке используется "запускалка" с таким же именем:

nice -n 50 command

Процесс Idle

Если нет процессов готовых для выполнения, то планировщик вызывает нить (процесс) Idle. В Linux 2.2 однопроцессорная кроссплатформенная версия Idle выглядела так:

int cpu_idle(void *unused) {
     for(;;)
      idle();
}

В аппаратно-зависимую реализацию idle() может быть вынесено управление энергосбережением.

В ранних версиях Linux процесс Idle имел PID=0, но, вообще говоря, Idle как самостоятельный процесс не существует.

Вычисление средней загрузки

Средняя загрузка (Load Average, LA) - усредненная мера использования ресурсов компьютера запущенными процессами. Величина LA пропорциональна числу процессоров в системе и на ненагруженной системе колеблется от нуля до значения, равного числу процессоров. Высокие значения LA (10*число ядер и более) говорят о чрезмерной нагрузке на систему и потенциальных проблемах с производительностью.

В классическом Unix LA имеет смысл среднего количества процессов в очереди на исполнение + количества выполняемых процессов за единицу времени. Т.е. LA == 1 означает, что в системе считается один процесс, LA > 1 определяет сколько процессов не смогли стартовать, поскольку им не хватило кванта времени, а LA < 1 означает, что в системе есть незагруженные ядра.

В Linux к к количеству процессов добавили ещё и процессы, ожидающих ресурсы. Теперь на рост LA значительно влияют проблемы ввода/вывода, такие как недостаточная пропускная способность сети или медленные диски.

LA усредняется по следующей формуле LAt+1=(LAcur+LAt)/2. Где LAt+1 - отображаемое значение в момент t+1, LAcur - текущее измеренное значение, LAt - значение отображавшееся в момент t. Таким образом сглаживаются пики и после резкого падения нагрузки значение LA будет медленно снижаться, а кратковременный пик нагрузки будет отображен половинной величиной LA.

Ссылка: Как считается Load Average

Выдача команды top

Выдача команды top в Linux на компьютере с 36 ядрами:

top - 19:43:53 up 4 days,  5:54,  1 user,  load average: 34.07, 33.75, 33.80
Tasks: 550 total,  12 running, 538 sleeping,   0 stopped,   0 zombie
%Cpu(s): 93.9 us,  0.5 sy,  0.0 ni,  5.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

На компьютере запущена многопоточная счётная задача, которая занимает почти все ядра и не использует ввод/вывод. LA немного меньше 36, что согласуется с распределением времени процессора: 93.9 us - пользователь, 0.5 sy - ядро, 5.5 id - Idle, 0.0 wa - ожидание устройств.

Прикрепленный файлРазмер
Иконка изображения sheduler.png58.86 КБ

Эффективные права процесса

С каждым процессом Unix связаны два атрибута uid и gid - пользователь и основная группа. В принципе они могли бы определять права доступа процесса к ФС и другим процессам, однако существует несколько ситуаций, когда права процесса отличаются от прав его владельца. Поэтому кроме uid/gid (иногда называемых реальными ruid/rgid) с процессом связаны атрибуты прав доступа - эффективные uid/gid - euid/egid, чаще всего совпадающие с ruid/rgid. Кроме того, с uid связан список вспомогательных групп, описанных в файле /etc/group . euid/egid и список групп определяют права доступа процесса к ФС. Вновь создаваемые файлы наследуют атрибуты uid/gid от euid/egid процесса. Кроме того euid определяет права доступа к другим процессам (отладка, отправка сигналов и т.п.).

euid равный нулю используется для обозначения привилегированного процесса, имеющего особые права на доступ к ФС и другим процессам, а так же на доступ к административным функциям ядра, таким как монтирование диска или использование портов TCP с номерами меньше 1024. Процесс с euid=0 всегда имеет право на чтение и запись файлов и каталогов. Право на выполнение файлов предоставляется привилегированному процессу только в том случае, когда у файла выставлен хотя бы один атрибут права на исполнение.

Примечание: в современных ОС особые привилегии процесса определяются через набор особых флагов - capabilities и не обязательно привязаны к euid=0.

(re)uid/(re)gid, а также вспомогательные группы, наследуются от родительского процесса при вызове fork(). При вызове exec() ruid/rgid сохраняются, а euid/egid могут быть изменены если у исполняемого файла выставлен флаг смены владельца. Для скриптов флаг смены владельца игнорируется т.к. фактически запускается интерпретатор, а скрипт передаётся ему в качестве параметра. В момент входа пользователя в систему программа login считывает из файлов /etc/passwd и /etc/group необходимые величины и устанавливает их перед загрузкой командного интерпретатора.

Список вспомогательных групп можно считать в массив функцией int getgroups(int size, gid_t list[]). Будет ли при этом в списке основная группа неизвестно - это зависит от реализации конкретной ОС. Максимальное число вспомогательных групп можно получить так: long ngroups_max = sysconf(_SC_NGROUPS_MAX); или из командной строки getconf NGROUPS_MAX. В моём Linux'е максимальное число групп - 65536.

Для инициализации вспомогательных групп в Linux можно воспользоваться функцией int initgroups(const char *user, gid_t group); эта функция разбирает файл /etc/group, а за тем обращается к системному вызову int setgroups(size_t size, const gid_t *list);.

Существуют несколько функций для управление атрибутами uid/gid. Для экономии места далее перечисляются только функции для работы с uid. Получить значения атрибутов можно с помощью функций getuid(), geteuid() Установить значения можно с помощью
setuid(id); - установить ruid и euid в id
seteuid(id); - установить euid в id
setreuid(rid,eid); - установить ruid и euid в rid и eid. -1 в качестве параметра означает, что значение не меняется

В Linux, HP-UX и некоторых других ОС дополнительно поддерживаются атрибут сохраненных прав процесса suid/sgid (не путать с одноименными атрибутами файла). Соответственно есть функция для установки всех трёх атрибутов setresuid(rid,eid,sid);

Если euid=0 или ruid=0 то ruid и euid могут меняться произвольно. Т.е. можно сделать euid<>0 или ruid<>0, а затем вернуться в состояние euid=ruid=0. Если оба атрибута не равны нулю, то возможно лишь изменение euid в ruid (отказ от дополнительных прав). Программа su получает euid=0 благодаря соответствующему атрибуту файла и использует возможности привилегированного процесса для запуска программ от имени произвольного пользователя (в том числе root). Веб-сервер apache, наоборот, стартует с ruid=euid=0, но затем отбирает у себя лишние права меняя ruid и euid на непривилегированные значения.