 | Уровень сложности: простой Авинеш Кумар, разработчик программного обеспечения, IBM
10.02.2009 В ядре Linux® 2.6.25 появилось новое состояние приостановки выполнения процесса TASK_KILLABLE, представляющее собой альтернативу эффективному, но потенциально приводящему к невозможности завершения процесса состоянию TASK_UNINTERRUPTIBLE и более безопасному, но легко прерываемому TASK_INTERRUPTIBLE. Своим появлением состояние TASK_KILLABLE обязано проблеме, возникшей в 2002 г. и связанной с драйвером файловой системы OpenAFS, который блокировал все сигналы и ожидал наступления события, находясь в состоянии, допускающем прерывания. Новое состояние приостановки аналогично TASK_UNINTERRUPTIBLE но позволяет обрабатывать фатальные сигналы. В данной статье автор освещает это нововведение и на примерах исходных текстов ядра Linux 2.6.26 и более ранней версии 2.6.18 обсуждает связанные с ним изменения и новые API.
Наряду с файлами процессы составляют основу любой операционной системы, семейства UNIX®. Процессы представляют собой "живые" сущности, выполняющие инструкции, содержащиеся в исполняемом файле. Помимо выполнения инструкций процесс может заниматься управлением открытыми файлами, контекстом процессора, адресным пространством, данными программы и другими вещами. Ядро Linux хранит полную информацию о процессе в дескрипторе процесса, объявленном в исходном тексте как struct task_struct. С различными полями struct task_struct можно ознакомиться в исходном файле ядра Linux include/linux/sched.h.
О состояниях процесса
В течение своего жизненного цикла процесс может находиться в нескольких взаимоисключающих состояниях. Ядро хранит информацию о текущем состоянии процесса в поле state структуры struct task_struct. На рисунке 1 показана диаграмма переходов между состояниями процесса.
Рисунок 1. Диаграмма переходов между состояниями процесса 
Рассмотрим состояния процесса поподробнее:
TASK_RUNNING: Процесс выполняется (использует процессор) или находится в очереди выполнения, ожидая выделения процессорного времени.
TASK_INTERRUPTIBLE: Процесс приостановлен до наступления определенного события. Это состояние может быть прервано сигналами. После получения сигнала или возобновления путем явного выполнения "пробуждающего" вызова процесс переходит в состояние TASK_RUNNING.
TASK_UNINTERRUPTIBLE: Данное состояние аналогично TASK_INTERRUPTIBLE, с той лишь разницей, что в нем не происходит обработка сигналов. Прерывание процесса, находящегося в этом состоянии, может быть нежелательным, поскольку ожидание может быть частью некоторой важной задачи. При наступлении ожидаемого события процесс возобновляется путем явного выполнения "пробуждающего" вызова.
TASK_STOPPED: Выполнение процесса остановлено, он не выполняется и не может начать выполняться. Процесс переходит в это состояние по получении таких сигналов, как SIGSTOP, SIGTSTP и т.д. Процесс сможет снова стать исполняемым после получения сигнала SIGCONT.
TASK_TRACED: Процесс находится в этом состоянии при выполнении его мониторинга такими процессами, как, например, отладчики.
EXIT_ZOMBIE: Процесс завершен. Он будет находиться в системе до момента получения родительским процессом статистической информации по его выполнению.
EXIT_DEAD: Конечное состояние (соответствующее своему названию). Процесс переходит в это состояние при его удалении из системы после того, как родительский процесс получит всю статистическую информацию, выполнив системный вызов wait4() или waitpid().
Подробную информацию о переходах между состояниями процесса можно получить, прочитав книгу The Design of the UNIX Operating System, упомянутую в разделе Ресурсы.
Как было сказано ранее, TASK_UNINTERRUPTIBLE и TASK_INTERRUPTIBLE являются состояниями приостановки процесса. Рассмотрим механизм, используемый ядром для выполнения приостановки.
Как ядро приостанавливает процессы
В ядре Linux предусмотрено два способа приостановки работы процесса.
Обычный способ приостановки процесса заключается в переводе процесса в состояние TASK_INTERRUPTIBLE или TASK_UNINTERRUPTIBLE с последующим вызовом функции планировщика schedule(). В результате приостанавливаемый процесс удаляется из очереди выполнения процессора. Если процесс приостановлен с приоритетом, допускающим прерывания (переведен в состояние TASK_INTERRUPTIBLE), его выполнение может быть возобновлено путем явного выполнения "пробуждающего" вызова (wakeup_process()) или с помощью сигналов, требующих обработки.
Однако, если процесс приостановлен с приоритетом, не допускающим прерывания (путем перевода в состояние TASK_UNINTERRUPTIBLE), его выполнение может быть возобновлено только "пробуждающим" вызовом. По этой причине рекомендуется использовать приостановку процесса с приоритетом, допускающим прерывания, вместо приоритета, не допускающего прерывания за, исключением случаев крайней необходимости (например, при выполнении операций ввода/вывода с устройством, когда обработка сигналов затруднена).
При получении сигнала процесс, приостановленный с приоритетом, допускающим прерывания, должен его обработать (если получение данного сигнала не было блокировано ранее!), предварительно прекратив выполнение текущих действий (для данной ситуации должен быть предусмотрен код очистки [cleanup code]) и возвратить -EINTR - в пользовательское пространство. При этом ответственность за обработку данного кода возврата и выполнение необходимых действий по его обработке лежит на программисте. Зная об этом, ленивый программист может предпочесть непрерываемый режим для приостановки процесса, чтобы сигналы не могли возобновить его выполнение. Но к подобному решению следует относиться с осторожностью - если "пробуждающий" вызов по какой-либо причине не сработает, процесс, имеющий приоритет, не допускающий прерывания, будет невозможно прекратить, что довольно неприятно - единственным выходом из такой ситуации является перезагрузка системы. С одной стороны, необходимо предусмотреть некоторые детали, поскольку их отсутствие повлечет возникновение ошибок как на стороне ядра, так и на стороне пользователя. С другой стороны, возможно возникновение "смертельных бессмертных" ("deadly immortals") (блокированных и незавершаемых процессов).
Но теперь появился способ приостановки процессов!
Новое состояние приостановки: TASK_KILLABLE
В ядре Linux 2.6.25 появилось новое состояние приостановки процесса TASK_KILLABLE: если процесс приостановлен с приоритетом, допускающим прерывания, он ведет себя так, как если бы находился в состоянии TASK_UNINTERRUPTIBLE, но вдобавок имеет возможность обрабатывать фатальные сигналы. Взгляните на листинг 1, содержащий сравнение состояний процесса (как объявлено в include/linux/sched.h) в версиях ядра 2.6.18 и 2.6.26:
Листинг 1. Сравнение состояний процесса в версиях ядра 2.6.18 и 2.6.26
Linux Kernel 2.6.18 Linux Kernel 2.6.26
================================= ===================================
#define TASK_RUNNING 0 #define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1 #define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2 #define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4 #define __TASK_STOPPED 4
#define TASK_TRACED 8 #define __TASK_TRACED 8
/* в tsk->exit_state */ /* в tsk->exit_state */
#define EXIT_ZOMBIE 16 #define EXIT_ZOMBIE 16
#define EXIT_DEAD 32 #define EXIT_DEAD 32
/* снова в tsk->state */ /* снова в tsk->state */
#define TASK_NONINTERACTIVE 64 #define TASK_DEAD 64
#define TASK_WAKEKILL 128 |
Обратите внимание на то, что состояния TASK_INTERRUPTIBLE и TASK_UNINTERRUPTIBLE не были изменены. TASK_WAKEKILL позволяет возобновлять выполнение процесса по получении фатальных сигналов.
В листинге 2 показаны изменения состояний TASK_STOPPED и TASK_TRACED, а также приведено определение состояния TASK_KILLABLE:
Листинг 2. Новые определения состояний в ядре 2.6.26
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) |
Другими словами, TASK_UNINTERRUPTIBLE + TASK_WAKEKILL = TASK_KILLABLE.
Новые API ядра, использующие TASK_KILLABLE
 | Несколько слов о завершении (completion) Механизм завершения является хорошим подспорьем, когда требуется приостановить процесс чтобы затем возобновить его по наступлении некоторого события. Он предоставляет простой способ синхронизации, лишенный риска возникновения "состояния гонок". Функция wait_for_completion(struct completion *comp) приостанавливает вызывающий процесс с приоритетом, не допускающим прерывания, до наступления завершения. Возобновление выполнения процесса происходит после вызова функции complete(struct completion *comp) или complete_all(struct completion *comp). Помимо wait_for_completion_killable() для реализации ожидания предусмотрены следующие функции:
-
wait_for_completion_timeout()
-
wait_for_completion_interruptible()
-
wait_for_completion_interruptible_timeout()
Структура завершения определена в исходном файле include/linux/completion.h. |
|
Рассмотрим некоторые функции, появившиеся вместе с новым состоянием.
int wait_event_killable(wait_queue_t queue, condition); Эта функция, определенная в файле include/linux/wait.h, приостанавливает процесс с возможностью обработки фатальных сигналов и помещает его в очередь ожидания queue до того момента, как condition станет равно true.
long schedule_timeout_killable(signed long timeout); Эта функция, определенная в файле kernel/timer.c, просто переводит процесс в состояние TASK_KILLABLE и вызывает функцию schedule_timeout(), приостанавливающую вызвавший ее процесс на число "мгновений" (jiffies), заданное значением переменной timeout. (В UNIX-системах "мгновение" представляет собой просто промежуток времени между двумя последовательными прерываниями системного таймера.)
int wait_for_completion_killable(struct completion *comp); Эта функция, определенная в файле kernel/sched.c, используется для приостановки процесса до наступления завершения с возможностью обработки фатальных сигналов. Функция вызывает schedule_timeout(), приостанавливая процесс на MAX_SCHEDULE_TIMEOUT (определено как равное LONG_MAX) "мгновений" при отсутствии фатальных сигналов, ожидающих обработки.
int mutex_lock_killable(struct mutex *lock); Эта функция, определенная в файле kernel/mutex.c, предназначена для блокирования мьютекса. Однако процесс, получивший фатальный сигнал во время ожидания освобождения мьютекса, заблокированного другим процессом, будет удален из очереди процессов, ожидающих освобождения мьютекса, и перейдет к обработке полученного сигнала.
int down_killable(struct semaphore *sem); Эта функция, определенная в файле kernel/semaphore.c, предназначена для блокирования семафора sem. Если семафор недоступен, выполнение процесса приостанавливается. При поступлении фатального сигнала процесс будет удален из очереди ожидания и должен будет обработать сигнал. Два других способа блокирования семафора реализуются функциями down() или down_interruptible(). Вместо устаревшей функции down() следует использовать down_killable() или down_interruptible().
 |
Изменения в клиентском коде NFS
В клиентский код NFS внесены изменения, использующие новое состояние. В листинге 3 показаны различия в коде макроса nfs_wait_event в коде ядра Linux версий 2.6.18 и 2.6.26.
Листинг 3. Изменения в коде функции макроса nfs_wait_event, использующие TASK_KILLABLE
Linux Kernel 2.6.18 Linux Kernel 2.6.26
========================================== =============================================
#define nfs_wait_event(clnt, wq, condition) #define nfs_wait_event(clnt, wq, condition)
({ ({
int __retval = 0; int __retval =
wait_event_killable(wq, condition);
if (clnt->cl_intr) { __retval;
sigset_t oldmask; })
rpc_clnt_sigmask(clnt, &oldmask);
__retval =
wait_event_interruptible(wq, condition);
rpc_clnt_sigunmask(clnt, &oldmask);
} else
wait_event(wq, condition);
__retval;
}) |
В листинге 4 сравниваются определения функции nfs_direct_wait() в коде ядра Linux версий 2.6.18 и 2.6.26.
Листинг 4. Изменения в коде функции nfs_direct_wait(), использующие TASK_KILLABLE Linux Kernel 2.6.18
=================================
static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq)
{
ssize_t result = -EIOCBQUEUED;
/* Асинхронные запросы здесь не блокируются */
if (dreq->iocb)
goto out;
result = wait_for_completion_interruptible(&dreq->completion);
if (!result)
result = dreq->error;
if (!result)
result = dreq->count;
out:
kref_put(&dreq->kref, nfs_direct_req_release);
return (ssize_t) result;
}
Linux Kernel 2.6.26
=====================
static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq)
{
ssize_t result = -EIOCBQUEUED;
/* Асинхронные запросы здесь не блокируются */
if (dreq->iocb)
goto out;
result = wait_for_completion_killable(&dreq->completion);
if (!result)
result = dreq->error;
if (!result)
result = dreq->count;
out:
return (ssize_t) result;
} |
О других изменениях клиентского кода NFS, использующих новые возможности ядра, можно узнать из сообщения списка рассылки разработчиков ядра Linux, ссылка на которое приведена в разделе Ресурсы.
Существовавший ранее параметр монтирования файловой системы NFS intr позволял прерывать процессы клиентов NFS, но прерывание происходило по получении любого, не только фатального, сигнала (например, TASK_KILLABLE).
Заключение
Несмотря на то, что новые возможности, описанные в статье, обладают преимуществами по сравнению с уже имеющимися — предлагают альтернативный способ избежания сложностей с "мертвыми" процессами, — их широкое распространение займет некоторое время. Поэтому достаточно помнить, что за исключением случаев, когда действительно важно запретить любой вид возобновления выполнения процесса, кроме явного "пробуждающего" вызова (с помощью традиционного состояния TASK_UNINTERRUPTIBLE), следует использовать новое состояние TASK_KILLABLE.
Ресурсы Научиться
- Оригинал статьи TASK_KILLABLE: New process state in Linux (EN) (developerWorks, сентябрь 2008 г.).
- Состояние процесса (EN)
TASK_KILLABLE возникло в результате рассмотрения проблемы, сформулированной Дэвидом Хауэлсом (David Howells) в 2002 г; он сообщил, что драйвер файловой системы OpenAFS блокирует все сигналы и ожидает наступления события с приоритетом, допускающим прерывания , вместо того чтобы использовать для ожидания состояние TASK_UNINTERRUPTIBLE.
- Обсуждение состояния
TASK_KILLABLE (EN) (LWN.net, июль 2008 г.) Джонатаном Корбетом (Jonathan Corbet) очень полезно для первоначального ознакомления с темой.
-
"Статья Kernel Korner: Sleeping in the Kernel" (EN) (Linux Journal, июль 2005 г.) разъясняет вопросы приостановки выполнения процесса в пространстве ядра.
- Глава 6 книги The Design of the UNIX Operating System (EN) (Prentice Hall, 1986 г., Морис Бах (Maurice J. Bach)) содержит отличное подробное описание переходов между состояниями процесса.
- Ветка "NFS Killable tasks request comments on patch" (EN) списка рассылки Linux Kernel Mailing List (EN) содержит дополнительные примеры изменений клиентского кода NFS с учетом новых функциональных возможностей ядра Linux 2.6.26, связанных с состоянием
TASK_KILLABLE.
- Для получения обзорных сведений о структуре ядра и о принципах его организации прочтите статью "Анатомия ядра Linux" (developerWorks, июнь 2007 г.).
- В разделе сайта developerWorks, посвященном Linux, можно найти другие материалы для Linux-разработчиков (включая новичков в программировании и системном администрировании Linux) и просмотреть наши самые популярные статьи и руководства (EN).
- Ознакомьтесь со всеми полезными советами и руководствами по Linux на сайте developerWorks.
- Получайте самую свежую информацию из обзоров событий мира технологий и Web-трансляций developerWorks.
Получить продукты и технологии
-
Закажите SEK для Linux (EN) - набор из двух DVD, в который включены ознакомительные версии новейших программных продуктов, разработанных IBM, в том числе, DB2®, Lotus®, Rational®, Tivoli® и WebSphere®.
- Начните ваш следующий проект для Linux с ознакомительными версиями ПО IBM, загрузить которые можно непосредственно с сайта developerWorks.
Обсудить
Об авторе  | |  | Авинеш Кумар (Avinesh Kumar) является разработчиком системного программного обеспечения в Andrew File System
Team в IBM Software Labs в Пуне (Индия). Он занимается
отладкой дампов и крахов ядра и пользовательских программ, а также анализом сообщений об ошибках в системах Linux, AIX и Solaris. Авинеш имеет MCA от
факультета вычислительной техники Пунского университета. Он энтузиаст Linux и проводит свободное время, изучая ядро Linux на своем компьютере с Fedora Core 6. |
Выскажите мнение об этой странице
|  |