Введение

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

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

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

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

Процессы в Linux

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

В Linux существует два способа создания новых процессов: через системный вызов fork() или через системный вызов exec(). Системный вызов — это специальная функция, которая позволяет процессу обратиться к ядру операционной системы за выполнением какого-то действия.

Системный вызов fork() создает новый процесс, который является точной копией родительского процесса. Новый процесс называется дочерним, а родительский — родительским. Дочерний процесс получает свой PID, свое адресное пространство и свои ресурсы, но изначально они полностью совпадают с родительскими. Дочерний процесс продолжает выполнять тот же код, что и родительский, но с другим PID. Системный вызов fork() возвращает PID дочернего процесса в родительском процессе и 0 в дочернем процессе. Это позволяет различать родительский и дочерний процесс.

Системный вызов exec() заменяет текущий процесс новым процессом, который загружает и выполняет другой исполняемый файл. Системный вызов exec() не создает нового PID, а использует существующий. Системный вызов exec() не возвращает никакого значения, если он успешно выполнен, и возвращает -1, если он не удался. Системный вызов exec() обычно используется после системного вызова fork(), чтобы дочерний процесс запустил другую программу, отличную от родительской.

Например, когда мы вводим в терминале команду ls, чтобы просмотреть список файлов в текущей директории, происходит следующее:

Терминал (shell) создает новый дочерний процесс с помощью системного вызова fork().
Дочерний процесс заменяет себя программой ls с помощью системного вызова exec().
Программа ls выполняет свою задачу и выводит список файлов на экран.
Программа ls завершается и возвращает управление родительскому процессу (терминалу).

Выполнение процессов

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

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

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

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

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

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

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

Управление процессами

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

Системный вызов wait(). Этот системный вызов позволяет родительскому процессу ждать завершения одного или нескольких дочерних процессов и получать их коды возврата. Это полезно для синхронизации процессов и предотвращения появления зомби-процессов. Зомби-процесс — это процесс, который завершился, но не был удален из таблицы процессов, потому что его родитель не вызвал wait().

Системный вызов kill(). Этот системный вызов позволяет отправлять сигналы процессам. Сигнал — это способ коммуникации между процессами или между ядром и процессами. Сигнал может быть вызван различными причинами, например, ошибкой, прерыванием, запросом пользователя и т.д. Сигнал может заставить процесс выполнить определенное действие, например, завершиться, остановиться, продолжиться или игнорировать сигнал. Существует множество видов сигналов, но самые распространенные — это SIGTERM (запрос на завершение), SIGKILL (принудительное завершение), SIGSTOP (приостановка) и SIGCONT (продолжение).

Команда ps. Эта команда позволяет просматривать информацию о текущих активных процессах в системе. Команда ps может принимать различные опции для фильтрации, сортировки и форматирования вывода. Например, команда ps -e -f --sort=-pcpu выведет информацию о всех процессах в системе в расширенном формате и отсортирует их по убыванию использования процессора.

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

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

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

Процессы в Linux могут завершаться по разным причинам: по своей инициативе, по запросу другого процесса, по сигналу от ядра или пользователя или по ошибке.
Когда процесс завершается по своей инициативе, он возвращает код возврата, который указывает на успешность или неуспешность его выполнения. Код возврата может быть получен родительским процессом с помощью системного вызова wait() или команды echo ?.Командаecho? показывает код возврата последнего выполненного процесса в терминале.

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

Когда процесс завершается по сигналу от ядра или пользователя, он также получает сигнал, который указывает на причину и способ завершения. Например, сигнал SIGSEGV означает, что процесс попытался обратиться к недопустимому адресу памяти, а сигнал SIGINT означает, что пользователь нажал клавишу прерывания (Ctrl+C). Процесс может обработать сигнал и выполнить соответствующие действия, или завершиться с ошибкой.

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

Потоки в Linux

Создание потоков

В Linux существует два способа создания новых потоков: через библиотеку POSIX Threads (Pthreads) или через системный вызов clone(). Библиотека Pthreads — это стандартный интерфейс для работы с потоками в операционных системах, основанных на UNIX. Системный вызов clone() — это расширенная версия системного вызова fork(), которая позволяет создавать новые процессы или потоки с различными уровнями разделения ресурсов.

Библиотека Pthreads предоставляет набор функций для создания, выполнения, управления и завершения потоков. Для создания нового потока нужно использовать функцию pthread_create(), которая принимает четыре аргумента: указатель на переменную типа pthread_t, которая будет хранить идентификатор потока (TID), указатель на структуру типа pthread_attr_t, которая содержит атрибуты потока (например, размер стека или приоритет), указатель на функцию, которая будет выполняться потоком (функция должна принимать один аргумент типа void* и возвращать значение типа void*), и указатель на аргумент

функции (любой тип данных). Функция pthread_create() возвращает 0 в случае успеха и код ошибки в случае неудачи.

Например, следующий код создает два потока, которые выводят свои TID на экран:

#include <stdio.h>
#include <pthread.h>

void* print_tid(void* arg) {
    printf("Hello from thread %ld\n", pthread_self());
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, print_tid, NULL);
    pthread_create(&t2, NULL, print_tid, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return 0;
}


Системный вызов clone() позволяет создавать новые процессы или потоки с различными уровнями разделения ресурсов. Для создания нового потока нужно использовать системный вызов clone() с флагом CLONE_THREAD, который указывает, что новый процесс будет иметь тот же идентификатор группы процессов (TGID), что и родительский. Системный вызов clone() принимает пять аргументов: указатель на функцию, которая будет выполняться потоком (функция должна принимать один аргумент типа void* и возвращать значение типа int), указатель на начало стека потока (стек должен быть выделен заранее), флаги, указывающие на уровень разделения ресурсов (например, CLONE_THREAD, CLONE_VM, CLONE_FS и т.д.), указатель на аргумент функции (любой тип данных) и указатель на структуру типа pt_regs, которая содержит значения регистров потока. Системный вызов clone() возвращает TID в случае успеха и -1 в случае неудачи.

Например, следующий код создает два потока, которые выводят свои TID на экран:

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/wait.h>

#define STACK_SIZE 8192
#define CLONE_THREAD 0x00010000

int print_tid(void* arg) {
    printf("Hello from thread %ld\n", syscall(SYS_gettid));
    return 0;
}

int main() {
    char* stack1 = malloc(STACK_SIZE);
    char* stack2 = malloc(STACK_SIZE);
    int t1 = clone(print_tid, stack1 + STACK_SIZE, CLONE_THREAD | CLONE_SIGHAND | CLONE_VM, NULL, NULL);
    int t2 = clone(print_tid, stack2 + STACK_SIZE, CLONE_THREAD | CLONE_SIGHAND | CLONE_VM, NULL, NULL);
    waitpid(t1, NULL, __WCLONE);
    waitpid(t2, NULL, __WCLONE);
    free(stack1);
    free(stack2);
    return 0;
}

 

Выполнение потоков

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

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

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

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

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

Управление потоками

Для управления потоками в Linux можно использовать различные функции библиотеки Pthreads, системные вызовы или команды и утилиты. Некоторые из них мы уже упомянули, а некоторые рассмотрим далее.

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

Функция pthread_cancel(). Эта функция позволяет одному потоку отправлять запрос на завершение другому потоку. Запрос на завершение не приводит к немедленному завершению потока, а лишь устанавливает флаг отмены в целевом потоке. Целевой поток может проверять флаг отмены и решать, как реагировать на него. Например, он может завершиться корректно, игнорировать запрос или отложить его до более подходящего момента.

Функция pthread_kill(). Эта функция позволяет одному потоку отправлять сигнал другому потоку. Сигнал — это способ коммуникации между процессами или между ядром и процессами. Сигнал может быть вызван различными причинами, например, ошибкой, прерыванием, запросом пользователя и т.д. Сигнал может заставить поток выполнить определенное действие, например, завершиться, остановиться, продолжиться или игнорировать сигнал. Существует множество видов сигналов, но самые распространенные — это SIGTERM (запрос на завершение), SIGKILL (принудительное завершение), SIGSTOP (приостановка) и SIGCONT (продолжение).

Команда ps. Эта команда позволяет просматривать информацию о текущих активных процессах и потоках в системе. Команда ps может принимать различные опции для фильтрации, сортировки и форматирования вывода. Например, команда ps -e -f -L выведет информацию о всех процессах и потоках в системе в расширенном формате.

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

Завершение потоков

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

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

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

Когда поток завершается по сигналу от ядра или пользователя, он также получает сигнал, который указывает на причину и способ завершения. Например, сигнал SIGSEGV означает, что поток попытался обратиться к недопустимому адресу памяти, а сигнал SIGINT означает, что пользователь нажал клавишу прерывания (Ctrl+C). Поток может обработать сигнал и выполнить соответствующие действия, или завершиться с ошибкой.

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

Заключение

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