Содержание


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

Comments

В предыдущей статье мы рассмотрели принятый в ядре API для создания потоков, но позже был добавлен ещё новый API более высокого уровня (в сравнении с kernel_thread()), упрощающий создание и завершение потоков, который находится в файле <linux/kthread.h>, а вызов kernel_thread() был объявлен устаревшим. В этой статье мы рассмотрим использование этого нового интерфейса для работы с потоками в Linux.

Особенности нового API для взаимодействия с потоками

Всё, сказанное в предыдущей статье относительно свойств и использования созданных потоков, остаётся в силе, так как новый интерфейс касается только аспектов создания и завершения потока. Перечислим основные вызовы, относящиеся к новому интерфейсу:

$ cat /proc/kallsyms | grep ' T ' | grep kthread
c043c7d7 T kthread_bind
c045b158 T kthread_should_stop
c045b171 T kthreadd
c045b2a2 T kthread_stop
c045b332 T kthread_create

Для создания потока в высокоуровневом интерфейсе используется следующий метод:

struct task_struct *kthread_create( int (*threadfn)(void *data),
                                    void *data, const char namefmt[], ... )

Принципиальным отличием в данном случае является то, что возвращается не PID созданного потока, а указатель структуры задачи. Такой вызов создаёт поток в блокированном (ожидающем) состоянии, и для запуска (выполнения) он должен быть разбужен вызовом wake_up_process(). Поскольку это весьма частая последовательность действий, то в файле <linux/kthread.h> (а не в /proc/kallsyms!) определён следующий макрос:

kthread_run( threadfn, data, namefmt, ... )

Который возвращает struct task_struct* или отрицательный код ошибки ERR_PTR(-ENOMEM).

Начиная с третьего параметра, функцияkthread_create() и макрос kthread_run() подобны вызовам printk() или sprintf() с переменным числом параметров: третий параметр — это форматная строка (шаблон), а все последующие параметры — это значения, заполняющие этот формат. Получившаяся в итоге строка становится идентификатором (именем) потока, под которым его далее знает ядро и которое выводится командой ps.

Гораздо интереснее обстоит дело с завершением. Созданный поток может проверять (чаще всего периодически) необходимость завершения неблокирующим вызовом:

int kthread_should_stop( void );

Если внешний по отношению к функции потока код хочет завершить поток, то он вызывает для этого потока вызов:

int kthread_stop( struct task_struct* );

Обнаружив это (по результату kthread_should_stop()) функция потока завершается. Обычно в коде потоковой функции это выглядит примерно так:

...
while( !kthread_should_stop() ) {
   // выполняемая работа потоковой функции
}
return 0;
...

Всё сказанное выше гораздо проще понять на примере, охватывающем весь жизненный цикл потока. Этот пример, полностью представленный в архиве thread.tgz в разделе "Материалы для скачивания", несколько сложнее остальных, но заслуживает детального изучения.

Листинг 3. Использование нового интерфейса потоков (файл mod_thr3.c)
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/jiffies.h>

static int N = 2;            // N - число потоков
module_param( N, int, 0 );

static char *sj( void ) {    // метка времени
   static char s[ 40 ];
   sprintf( s, "%08ld :", jiffies );
   return s;
}

static char *st( int lvl ) { // метка потока
   static char s[ 40 ];
   sprintf( s, "%skthread [%05d:%d]", sj(), current->pid, lvl );
   return s;
}

static int thread_fun1( void* data ) {
   int N = (int)data - 1;
   struct task_struct *t1 = NULL;
   printk( "%s is parent [%05d]\n", st( N ), current->parent->pid );
   if( N > 0 )
      t1 = kthread_run( thread_fun1, (void*)N, "my_thread_%d", N );
   while( !kthread_should_stop() ) {
      // выполняемая работа потоковой функции
      msleep( 100 );
   }
   printk( "%s find signal!\n", st( N ) );
   if( t1 != NULL ) kthread_stop( t1 );
   printk( "%s is completed\n", st( N ) );
   return 0;
}

static int test_thread( void ) {
   struct task_struct *t1;
   printk( "%smain process [%d] is running\n", sj(), current->pid );
   t1 = kthread_run( thread_fun1, (void*)N, "my_thread_%d", N );
   msleep( 10000 );
   kthread_stop( t1 );
   printk( "%smain process [%d] is completed\n", sj(), current->pid );
   return -1;
}

module_init( test_thread );
MODULE_LICENSE( "GPL" );

В этом коде запускается N потоков ядра (параметр N указывается при загрузке модуля), причём:

  • потоки последовательно запускают друг друга, поток i запускает поток i + 1;
  • все потоки используют одну потоковую функцию;
  • весь диагностический вывод сопровождается метками времени (jiffies), что позволяет подробно проследить хронологию событий;
  • после выдержки паузы (10с.) главный поток посылает команду завершения (kthread_stop()), а далее потоки так же по цепочке посылают такую же команду друг другу.

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

$ time sudo insmod mod_thr3.ko N=3
insmod: error inserting 'mod_thr3.ko': -1 Operation not permitted
 real    0m10.140s
user    0m0.006s
sys     0m0.010s
$ ps -ef | grep '\[' | grep 'my_'
root     14603     2  0 19:00 ?        00:00:00 [my_thread_3]
root     14604     2  0 19:00 ?        00:00:00 [my_thread_2]
root     14605     2  0 19:00 ?        00:00:00 [my_thread_1]
$ dmesg | tail -n40 | grep -v audit
34167405 : main process [14602] is running
34167410 : kthread [14603:2] is parent [00002]
34167410 : kthread [14604:1] is parent [00002]
34167410 : kthread [14605:0] is parent [00002]
34177414 : kthread [14603:2] find signal!
34177511 : kthread [14604:1] find signal!
34177516 : kthread [14605:0] find signal!
34177516 : kthread [14605:0] is completed
34177516 : kthread [14604:1] is completed
34177516 : kthread [14603:2] is completed
34177516 : main process [14602] is completed

В этом выводе хорошо видно, что:

  • порядок, в котором потоки создаются, и порядок, в котором они получают сигнал на завершение —совпадают;
  • но порядок фактического завершения — в точности обратный, потому, что при выполнении kthread_stop() поток блокируется и ожидает завершения дочернего потока, и только после этого имеет право завершиться;
  • видны задержки с малой дискретностью (100мс.) между получением команды завершения и его последующей отправкой из основного цикла потоковой функции по иерархии потоков;
  • видно, что для выполняющегося потока родительским потоком становится PID=2 (демон kthreadd), хотя всё происходящее мы и наблюдаем ещё при выполнении запускающего процессаinsmod (в функции инициализации модуля) — для созданных потоков выполняется операция демонизации.

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

Синхронизация завершения

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

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

Листинг 2. Синхронизация завершения потоков (файл mod_for.c)
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/jiffies.h>
#include <linux/semaphore.h>

#include "../prefix.c"

#define NUM 3
static struct completion finished[ NUM ];

#define DELAY 1000
static int thread_func( void* data ) {
   int num = (int)data;
   printk( "! %s is running\n", st( num ) );
   msleep( DELAY - num );
   complete( finished + num );
   printk( "! %s is finished\n", st( num ) );
   return 0;
}

#define IDENT "for_thread_%d"
static int test_mlock( void ) {
   struct task_struct *t[ NUM ];
   int i;
   for( i = 0; i < NUM; i++ )
      init_completion( finished + i );
   for( i = 0; i < NUM; i++ )
      t[ i ] = kthread_run( thread_func, (void*)i, IDENT, i );
   for( i = 0; i < NUM; i++ )
      wait_for_completion( finished + i );
   printk( "! %s is finished\n", st( NUM ) );
   return -1;
}

module_init( test_mlock );
MODULE_LICENSE( "GPL" );

Включаемый файл prefix.c содержит описания диагностических функций sj()и st(), которые были представлены в листинге 1.

Но прежде обратим внимание ещё на один приём, который очень часто применяют в потоковом программировании. Это достаточно старый приём, впервые применявшийся для потоков POSIX: когда при создании потока нужно передать ему в качестве параметра единственное скалярное значение, а потоковая функция ожидает указатель на данные потока, то к указателю приводится непосредственно это скалярное значение, никогда не бывшее указателем. В нашем случае это целочисленный индекс, 2-й параметр вызова:

kthread_run( thread_func, (void*)i, IDENT, i );

Но вернёмся к синхронизации завершения потоков. Дочерние потоки в этом примере создаются в цикле последовательно (от 0-го до 2-го). Завершаться они могут в реальной ситуации в любой момент времени и в произвольном порядке, а в примере искусственно смоделирована самая неблагоприятная ситуация, при которой потоки завершаются в порядке обратном их порождению. Но ожидается завершение потоков (в блокированном состоянии ожидающей программной единицы) опять таки всё в том же последовательном порядке от 0-го до 2-го:

for( i = 0; i < NUM; i++ ) 
      wait_for_completion( finished + i );

Это не ошибка, а стандартная практика! Нам нужно дождаться завершения всех порождённых потоков. Освободившись на очередном блокирующем ожидании завершения i-го потока, ожидающий код на последующих номерах уже ранее завершившихся потоков i>0 просто не будет блокироваться и мгновенно "перескочит" через них в цикле. Полностью такой цикл выйдет из блокированных состояний только когда завершатся все порождённые потоки, как показано ниже:

$ sudo insmod mod_for.ko
insmod: error inserting 'mod_for.ko': -1 Operation not permitted
$ dmesg | tail -n30 | grep !
! 05019276 : kthread [08114:0] is running
! 05019276 : kthread [08115:1] is running
! 05019276 : kthread [08116:2] is running
! 05020275 : kthread [08116:2] is finished
! 05020276 : kthread [08115:1] is finished
! 05020277 : kthread [08114:0] is finished
! 05020277 : kthread [08113:3] is finished

Заключение

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


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


Похожие темы


Комментарии

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

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