Содержание


Инструменты программирования в ядре: Часть 70. Параллелизм и синхронизация. Потоки ядра

Comments

Ядро Linux является вытесняющим (преемптивным, preemptive), т.е. код ядра в состоянии вытеснить другие выполняющиеся задания, даже если они работают в режиме ядра. Только небольшое количество операционных систем имеет вытесняющее ядро, чаще всего это некоторые коммерческие реализации UNIX, например, Solaris и AIX®. Но вытеснение возможно и имеет смысл только тогда, когда имеются возможности параллелизма, т.е. когда в ядре могут существовать потоки выполнения, возможности которых мы и будем рассматривать в этой и последующей статьях.

Механизм потоков ядра

Механизм потоков ядра (kernel thread — появившийся, начиная с версии ядра 2.5) предоставляет средство для параллельного выполнения задач в ядре. Общей особенностью механизмов потоков ядра и примитивов для их синхронизации является то, что они в принципиальной основе своей единообразны, так что для пользовательского пространства и ядра различаются только тонкие нюансы и функции API, используемого для работы с потоками ядра. Поэтому рассмотрение (и тестирование на примерах) работы механизмов синхронизации можно выполнять как в пространстве ядра, так и в пространстве пользователя.

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

  1. Логический параллелизм (или квази-параллелизм), обусловленный удобством разделения разнородных сервисов ядра, но реализующие их потоки вытесняют друг друга, создавая только иллюзию параллельности. При этом синхронизация осуществляется исключительно классическими блокирующими механизмами, когда поток ожидает недоступных ему ресурсов, переводясь в блокированное состояние.
  2. Физический параллелизм (или реальный параллелизм), возникший только с широким распространением SMP (в виде многоядерности или/и гипертриэдинга), при котором разные задачи ядра выполняются одновременно на различных процессорах. В этом случае широко используются (наряду с классическими) активные примитивы синхронизации (спин-блокировки), когда один из процессоров просто ожидает требуемых ресурсов, выполняя пустые циклы ожидания. Этот второй класс (активно развивающийся примерно с 2003-2005 г.) значительно усложняет картину происходящего (существуя одновременно с предыдущим классом), и может доставить разработчику множество проблем. Но с ним придётся считаться, учитывая достаточно динамичное развитие тех направлений, которые сегодня называются массивно-параллельными системами (например, модель программирования CUDA компании NVIDIA), когда от систем с 2-4-8 процессорами SMP происходит переход к сотням и тысячам процессоров.

Механизм потоков ядра начал интенсивно использоваться, начиная с версии ядра 2.6.х, и на него даже было перенесён ряд традиционных и давно существующих демонов Linux пользовательского уровня (в протоколе команд далее специально сохранены компоненты, относящиеся к сетевой файловой подсистеме nfsd— одной из самых давних подсистем UNIX). Ниже представлен сокращённый вывод команды ps, которая выводит информацию о потоках, присутствующих в системе, где потоки ядра выделены квадратными скобками:

$ uname -r
2.6.32.9-70.fc12.i686.PAE
$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 09:52 ?        00:00:01 /sbin/init
root         2     0  0 09:52 ?        00:00:00 [kthreadd]
root         3     2  0 09:52 ?        00:00:00 [migration/0]
root         4     2  0 09:52 ?        00:00:00 [ksoftirqd/0]
root         5     2  0 09:52 ?        00:00:00 [watchdog/0]
root         9     2  0 09:52 ?        00:00:00 [events/0]
root        10     2  0 09:52 ?        00:00:00 [events/1]
...
root       438     2  0 09:52 ?        00:00:00 [kjournald]
root       458     2  0 09:52 ?        00:00:00 [kauditd]
...
root      1720     1  0 09:53 ?        00:00:00 rpc.rquotad
root      1723     2  0 09:53 ?        00:00:00 [lockd]
root      1724     2  0 09:53 ?        00:00:00 [nfsd4]
root      1725     2  0 09:53 ?        00:00:00 [nfsd]
root      1731     2  0 09:53 ?        00:00:00 [nfsd]
root      1732     2  0 09:53 ?        00:00:00 [nfsd]
root      1735     1  0 09:53 ?        00:00:00 rpc.mountd
...

Для всех показанных в выводе потоков ядра родителем (PPID) является демон kthreadd (PID=2), который, как и процесс init не имеет родителя (PPID=0) и запускается непосредственно при старте ядра. Число потоков ядра может быть весьма значительным:

$ ps -ef | grep -F '[' | wc -l 
78

Функции для работы с потоками и механизмы синхронизации для них доступны после включения заголовочного файла <linux/sched.h>. Макросcurrent возвращает указатель текущую исполняющуюся задачу в циклическом списке задач, на соответствующую ей запись struct task_struct:

struct task_struct {
   volatile long state; /* -1 не готов к запуску, 0 готов к запуску, >0 остановлен */
   void *stack;
...
   int prio, static_prio, normal_prio;
...
   pid_t pid;
...
   cputime_t utime, stime, utimescaled, stimescaled;
...
}

Это основная структура, один экземпляр которой присутствует для любой выполняющейся задачи: будь то поток ядра (созданный вызовом kernel_thread()), пользовательский процесс (главный поток этого процесса), или один из пользовательских потоков POSIX, созданных вызовом pthread_create(...) в рамках единого процесса. Linux не знает разницы (исполнительной) между потоками и процессами, и все они порождаются одним системным вызовом clone(). Только в единственном случае для кода, выполняемого в текущий момент, нет соответствующей записи struct task_struct()— это контекст прерывания (обработчик аппаратного прерывания или таймерная функция, которые мы уже рассматривали). Но и в этом случае указатель current указывает на определённую запись задачи, только это — последняя (до прерывания) выполнявшаяся задача, не имеющая никакого касательства к текущему выполняющемуся коду (текущему контексту), т.е. current в этом случае указывает на мусор. На это обстоятельство следует обратить особое внимание, так как оно может стать предметом очень серьёзных ошибок!

Потоки ядра

Как уже было сказано, ядро Linux является вытесняющим, в отличие от ядер большинства других UNIX-подобных операционных систем. Но вытеснение имеет смысл только в контексте наличия механизма параллельных ветвей выполнения. Этот механизм и предоставляется таким понятием как потоки ядра. Потоки ядра в Linux имеют много общего с потоками пользовательского пространства (pthread_*) и процессами пользовательского пространства (приложениями). Объединяет их то, что каждый из них имеет свою единственную структуру struct task_struct, содержащую всю информацию о потоке и составляющую контекст потока. Все такие структуры (контекстные структуры задач) связаны в сложную динамическую списковую структуру: начав от любой структуры можно обойти структуры всех задач, существующих в системе (что и делает команда ps -ef). Это отличает все перечисленные сущности (задачи) от кода обработчиков прерываний, которые не имеют своей структуры struct task_struct, и о которых говорят, что они выполняются в контексте прерываний. Указателем на структуру текущего контекста (если он есть), служит макрос без параметров current.

Создание потока ядра

Для создания нового потока ядра используется вызов:

int kernel_thread( int (*fn)(void *), void *arg, unsigned long flags );

Параметры такого вызова понятны: функция потока, безтиповой указатель — параметр, передаваемый этой функции, и флаги, обычные для Linux вызова clone(). Возвращаемое функцией значение — это PID вновь созданного потока (в случае успешного создания его значение будет больше нуля, а отрицательное значение — это код ошибки).

Этот же вызов можно найти и среди экспортируемых символов ядра:

$ cat /proc/kallsyms | grep kernel_thread
c0407c44 T kernel_thread
...

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

В листинге 1 представлен простейший пример создания потоков ядра. Полный код примера можно найти в модуле mod_thr1 в архиве thread.tgz в разделе "Материалы для скачивания".

Листинг 1. Создание потоков ядра (файл mod_thr1.c)
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/delay.h>

static int param = 3;
module_param( param, int, 0 );

static int thread( void * data ) {
   printk( KERN_INFO "thread: child process [%d] is running\n", current->pid );
   ssleep( param ); /* пауза на 3 с или на период, указанный в параметре */
   printk( KERN_INFO "thread: child process [%d] is completed\n", current->pid );
   return 0;
}

int test_thread( void ) {
   pid_t pid;
   printk( KERN_INFO "thread: main process [%d] is running\n", current->pid );
   pid = kernel_thread( thread, NULL, CLONE_FS ); /* запускаем новый поток */
   ssleep( 5 );                                   /* пауза 5 с. */
   printk( KERN_INFO "thread: main process [%d] is completed\n", current->pid );
   return -1;
}

module_init( test_thread );

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

$ uname -r
2.6.32.9-70.fc12.i686.PAE
$ time sudo insmod ./mod_thr1.ko
insmod: error inserting './mod_thr1.ko': -1 Operation not permitted
real	0m5.025s
user	0m0.004s
sys	0m0.012s
$ sudo cat /var/log/messages | tail -n30 | grep thread:
Jul 24 18:43:57 notebook kernel: thread: main process [12526] is running
Jul 24 18:43:57 notebook kernel: thread: child process [12527] is running
Jul 24 18:44:00 notebook kernel: thread: child process [12527] is completed
Jul 24 18:44:02 notebook kernel: thread: main process [12526] is completed

Если выполнить пример с задержкой дочернего процесса больше, чем у родительского, то после завершения запуска (при завершении созданного потока ядра!) будет выведено критическое сообщение Oops об ошибке ядра:

$ sudo insmod ./mod_thr1.ko param=7
insmod: error inserting './mod_thr1.ko': -1 Operation not permitted
$
Message from syslogd@notebook at Jul 24 18:51:00 ...
 kernel:Oops: 0002 [#1] SMP
...
$ sudo cat /var/log/messages | tail -n70 | grep thread:
Jul 24 18:50:53 notebook kernel: thread: main process [12658] is running
Jul 24 18:50:53 notebook kernel: thread: child process [12659] is running
Jul 24 18:50:58 notebook kernel: thread: main process [12658] is completed

Последний параметр flags вызова kernel_thread() определяет детальный, побитово устанавливаемый набор свойств, которыми будет обладать созданный поток ядра, так как это делается в практике Linux при вызове clone() (при создании потоков-процессов в Linux наблюдается существенное отличие от традиций UNIX/POSIX). Часто в коде модулей можно видеть создание потока с таким набором флагов:

kernel_thread( thread_function, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGHAND | SIGCHLD );

Свойства потока

Созданному потоку ядра (как и пользовательским процессам и потокам) присущ целый ряд параметров (определённый в файле <linux/sched.h>), часть которых будет иметь значения по умолчанию (например, параметры диспетчеризации), но впоследствии могут быть и изменены. Для работы с параметрами потока используются следующие API.

Взаимно однозначное соответствие PID потока и соответствующей ему основной структуры данных struct task_struct устанавливается в обоих направлениях вызовами:

static inline pid_t task_pid_nr( struct task_struct *tsk ) { 
   return tsk->pid; 
} 
struct task_struct *find_task_by_vpid( pid_t nr );

Или с помощью вызовов из <linux/pid.h>:

extern struct pid *find_vpid( int nr );
enum pid_type {
   PIDTYPE_PID,
   PIDTYPE_PGID,
   PIDTYPE_SID,
   PIDTYPE_MAX
};
struct task_struct *pid_task( struct pid *pid, enum pid_type );
struct task_struct *get_pid_task( struct pid *pid, enum pid_type );
struct pid *get_task_pid( struct task_struct *task, enum pid_type type );

В коде модуля это может выглядеть так:

struct task_struct *tsk;
tsk = find_task_by_vpid( pid );

Или так:

tsk = pid_task( find_vpid( pid ), PIDTYPE_PID );

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

struct sched_param {
   int sched_priority;
};
int sched_setscheduler( struct task_struct *task, int policy, struct sched_param *parm );
// политики планирования
#define SCHED_NORMAL  0
#define SCHED_FIFO    1
#define SCHED_RR      2
#define SCHED_BATCH   3
/* SCHED_ISO: зарезервировано, но ещё не реализовано */
#define SCHED_IDLE    5

Другие вызовы, имеющие отношение к приоритетам процесса:

void set_user_nice( struct task_struct *p, long nice );
int task_prio( const struct task_struct *p );
int task_nice( const struct task_struct *p );

Разрешения на использование выполнения на разных процессорах в SMP системах (афинити-маска потока):

extern long sched_setaffinity( pid_t pid, const struct cpumask *new_mask );
extern long sched_getaffinity( pid_t pid, struct cpumask *mask );

где структура cpumask объявлена в файле <linux/cpumask.h>, как

typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;

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

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=952100
ArticleTitle=Инструменты программирования в ядре: Часть 70. Параллелизм и синхронизация. Потоки ядра
publish-date=11072013