Библиотека сайта rus-linux.net
Легковесные процессы: анатомическое исследование программных потоков в Linux
Оригинал: Light-Weight Processes: Dissecting Linux ThreadsАвторы: Vishal Kanaujia, Chetan Giridhar
Дата публикации: 1 Августа 2011 г.
Перевод: А.Панин
Дата публикации перевода: 22 октября 2012 г.
В этой статье, предназначенной для разработчиков Linux и студентов факультетов компьютерных наук, рассматриваются основы работы программных потоков и их реализация на основе легковесных процессов в ОС Linux, а для лучшего понимания приводятся примеры исходного кода.
Программные потоки являются базовым элементом многозадачного программного окружения. Программный поток может быть описан как среда выполнения процесса; поэтому каждый процесс имеет как минимум один программный поток. Многопоточность предполагает наличие нескольких параллельно работающих (на многопроцессорных системах) и обычно синхронизируемых сред выполнения процесса.
Программные потоки имеют свои идентификаторы (thread ID) и могут выполняться независимо друг от друга. Они делят между собой одно адресное пространство процесса и используют эту особенность в качестве преимущества, позволяющего не использовать каналы IPC (систем межпроцессного взаимодействия - разделяемой памяти, каналов и других систем) для обмена данными. Программные потоки процесса могут взаимодействовать - например, независимые потоки могут получать/изменять значение глобальной переменной. Эта модель взаимодействия исключает лишние затраты ресурсов на вызовы IPC на уровне ядра. Поскольку потоки работают в едином адресном пространстве, переключения контекста для потока быстры и не ресурсоемки.
Каждый программный поток может обрабатываться планировщиком задач индивидуально, поэтому многопоточные приложения хорошо подходят для параллельного выполнения на многопроцессорных системах. Кроме того, создание и уничтожение потоков происходит быстро. В отличие от вызова fork(), в случае потока не создается копии адресного пространства родительского процесса, вместо этого потоки используют совместно адресное пространство вместе с другими ресурсами, включая дескрипторы файлов и обработчики сигналов.
Многопоточное приложение использует ресурсы оптимально и максимально эффективно. В таком приложении программные потоки занимаются различными задачами с учетом оптимального использования системы. Один поток может читать файл с диска, другой записывать данные в сокет. Оба потока будут работать в тандеме, будучи независимыми друг от друга. Эта модель оптимизирует использование системы, тем самым улучшая производительность.
Несколько любопытных особенностей
Наиболее известной особенностью работы с потоками является их синхронизация, особенно при наличии разделяемого ресурса, отмеченного как критический сегмент. Это сегмент кода, в котором осуществляется доступ к разделяемому ресурсу и который не должен быть доступен более чем одному потоку в любой момент времени. Поскольку каждый поток может исполняться независимо, доступ к разделяемому ресурсу не контролируется, что приводит к необходимости использования примитивов синхронизации, включающих в себя мьютексы (mutual exclusion - взаимное исключение), семафоры, блокировки чтения/записи и другие.
Эти примитивы позволяют программистам контролировать доступ к разделяемому ресурсу. В дополнение к вышесказанному, потоки, как и процессы, подвержены переходу в состояния блокировки или ожидания в случае некорректного проектирования модели синхронизации. Отладка и анализ многопоточных приложений также могут быть довольно обременительными.
Как программные потоки реализованы в Linux?
Linux позволяет разрабатывать и использовать многопоточные приложения. На пользовательском уровне реализация потоков в Linux соответствует открытому стандарту POSIX (Portable Operating System Interface for uniX - Переносимый интерфейс операционных систем Unix), обозначенному как IEEE 1003. Библиотека пользовательского уровня (glibc.so в Ubuntu) предоставляет реализацию API POSIX для потоков.
В Linux программные потоки существуют в двух отдельных пространствах - пространстве пользователя и пространстве ядра. В пространстве пользователя потоки создаются при помощи POSIX-совместимого API библиотеки pthread. Эти потоки пространства пользователя неразрывно связаны с потоками пространства ядра. В Linux потоки пространства ядра воспринимаются как "легковесные процессы". Легковесный процесс является единицей основной среды исполнения. В отличие от различных вариантов UNIX, включая такие системы, как HP-UX и SunOS, в Linux не существует отдельной системы для работы с потоками. Процесс или поток в Linux рассматривается как "задача" (task) и использует одинаковые внутренние структуры (ряд структур struct task_structs).
#include <stdio.h> #include <syscall.h> #include <pthread.h> int main() { pthread_t tid = pthread_self(); int sid = syscall(SYS_gettid); printf("LWP id is %dn", sid); printf("POSIX thread id is %dn", tid); return 0; }
kanaujia@ubuntu:~/Desktop$ ps -fL UID PID PPID LWP C NLWP STIME TTY TIME CMD kanaujia 17281 5191 17281 0 1 Jun11 pts/2 00:00:02 bash kanaujia 22838 17281 22838 0 1 08:47 pts/2 00:00:00 ps -fL kanaujia 17647 14111 17647 0 2 00:06 pts/0 00:00:00 vi clone.s
Что такое легковесные процессы?
Легковесным процессом является процесс, поддерживающий работу потока пространства пользователя. Каждый поток пространства пользователя неразрывно связан с легковесным процессом. Процедура создания легковесного процесса отличается от процедуры создания обычного процесса; у пользовательского процесса "P" может существовать ряд связанных легковесных процессов с одинаковым идентификатором группы (group ID). Группировка позволяет ядру производить разделение ресурсов (ресурсы включают в себя адресное пространство, страницы физической памяти (VM), обработчики сигналов и дескрипторы файлов). Это также позволяет ядру избежать переключений контекста при работе с этими процессами. Исчерпывающее разделение ресурсов является причиной наименования этих процессов легковесными.
Как Linux создает легковесные процессы?
В Linux создание легковесных процессов осуществляется при помощи нестандартизированного системного вызова clone(). Он похож на вызов fork(), но с более широкими возможностями. Вообще, вызов fork() реализуется при помощи вызова clone() с дополнительными параметрами, указывающими на ресурсы, которые будут разделены между процессами. Вызов clone() создает процесс, при этом дочерний процесс разделяет с родительским элементы среды исполнения, включая память, дескрипторы файлов и обработчики сигналов. Библиотека pthread также использует вызов clone() для реализации потоков. Обратитесь к файлу исходного кода ./nptl/sysdeps/pthread/createthread.c в директории исходных кодов glibc версии 2.11.2.
Создание своего легковесного процесса
Продемонстрируем пример использования вызова clone(). Посмотрите на исходный код из файла demo.c, приведенный ниже:
#include <sched.h> int clone (int (*fn) (void *), void *child_stack, int flags, void *arg);
Первым аргументом является функция потока; она вызывается во время запуска потока. После того, как вызов clone() успешно завершается, функция fn начинает исполняться одновременно с вызывающим процессом.
(char*) stack + STACK
- SIGCHLD: поток отправляет сигнал SIGCHLD родительскому процессу после завершения. Установка этого параметра позволяет родительскому процессу использовать функцию wait() для ожидания завершения всех потоков.
- CLONE_FS: Разделять информацию о файловых системах между родительским процессом и потоком. Информация включает в себя корень файловой системы, рабочую директорию и значение umask.
- CLONE_FILES: Разделять таблицу файловых дескрипторов между родительским процессом и потоком. Изменения в таблице отображаются в родительском процессе и всех потоках.
- CLOSE_SIGHAND: Разделять таблицу обработчиков сигналов между родительским процессом и потомком. И снова, если родительский процесс или один из потоков изменит обработчик сигнала, изменение будет отображено на таблицах других процессов.
- CLONE_VM: Родительский процесс и потоки работают в одном пространстве памяти. Все записи в память или отображения, сделанные одним из них, доступны другим процессам.
Последним параметром является аргумент, передаваемый функции (threadFunction), в нашем случае это файловый дескриптор.
Пожалуйста, обратитесь к примеру работы с легковесными процессами demo.c, опубликованному нами ранее.
Поток закрывает файл (/dev/null), открытый родительским процессом. Поскольку родительский процесс и поток делят таблицу файловых дескрипторов, операция закрытия файла затрагивает и родительский процесс, что приводит к неудаче при последующем вызове write(). Родительский процесс ожидает завершения работы потока (момента приема сигнала SIGCHLD). После этого он освобождает зарезервированную память и возвращает управление.
$gcc demo.c $./a.out Creating child thread child thread entering child thread exiting Parent: child closed our file descriptor $
Linux предоставляет поддержку эффективной, простой и масштабируемой инфраструктуры для работы с потоками. Это стимулирует интерес программистов к экспериментам и разработке библиотек для работы с потоками, использующих clone() в качестве основной функции.
Пожалуйста, поделитесь своими предложениями/отзывами в разделе комментариев, расположенном ниже.
- Статья о вызове clone() в Wikipedia
- Страница руководства clone()
- Статья "Using the clone() System Call" от Joey Bernard
- Реализация библиотеки для работы с программными потоками в Linux
- Стандарт IEEE Std 1003.1c-1995
- Исходные коды реализации pthread в библиотеке glibc.so
- Статья "The Fibers of Threads" от Benjamin Chelf