Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра Linux
Назад Внутренние механизмы ядра Вперед

Синхронизации

Существует множество примитивов синхронизации, как теоретически проработанных, так и конкретно используемых и доступных в ядре Linux, и число их постоянно возрастает. Эта множественность связана, главным образом, с борьбой за эффективность (производительность) выполнения кода — для отдельных функциональных потребностей вводятся новые, более эффективные для этих конкретных нужд примитивы синхронизации. Тем не менее, основная сущность работы всех примитивов синхронизации остаётся одинаковой, в том виде, как она была впервые описана Э. Дейкстрой в его знаменитой работе 1968 г. «Взаимодействие последовательных процессов».

Критические секции кода и защищаемые области данных

Для решения задачи синхронизации в ядре Linux существует множество механизмов синхронизации (сами объекты синхронизации называют примитивами синхронизации) и появляются всё новые и новые... , некоторые из механизмов вводятся даже для поддержки единичных потребностей. Условно, по функциональному использованию, примитивы синхронизации можно разделить на (хотя такое разделение часто оказывается весьма условным):

  • Примитивы для защиты фрагментов исполняемого кода (критических секций) от одновременного (или псевдо-одновременного) исполнения. Классический пример: мьютекс, блокировки чтения-записи...
  • Примитивы для защиты областей данных от несанкционированного изменений: атомарные переменные и операции, счётные семафоры...

Механизмы синхронизации

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

  • переменные, локальные для каждого процессора (per-CPU variables), интерфейс которых описан в файле <linux/percpu.h>;
  • атомарные переменные (описаны в архитектурно-зависимых файлах <atomic*.h>);
  • спин-блокировки (<linux/spinlock.h>);
  • сериальные (последовательные) блокировки (<linux/seqlock.h>);

  • семафоры (<linux/semaphore.h>);

  • семафоры чтения и записи (<linux/rwsem.h>);
  • мьютексы реального времени (<linux/rtmutex.h>);
  • механизмы ожидания завершения (<linux/completion.h>);

Рассмотрение механизмов синхронизаций далее проведено как-раз в обратном порядке, с ожидания завершения, потому, что это естественным образом продолжает начатое выше рассмотрение потоков ядра.

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

Условные переменные и ожидание завершения

Естественным сценарием является запуск некоторой задачи в отдельном потоке и последующее ожидание завершения ее выполнения (см. аварийное завершение выше). В ядре нет аналога функции ожидания завершения потока, вместо нее требуется явно использовать механизмы синхронизации (аналогичные POSIX 1003.b определению барьеров pthread_barrier_t). Использование для ожидания какого-либо события обычного семафора не рекомендуется: в частности, реализация семафора оптимизирована исходя из предположения, что обычно (основную часть времени жизни) они открыты. Для этой задачи лучше использовать не семафоры, а специальный механизм ожидания выполнения - completion (в терминологии ядра Linux он называется условной переменной, но разительно отличается от условной переменной как её понимает стандарт POSIX). Этот механизм (<linux/completion.h>) позволяет одному или нескольким потокам ожидать наступления какого-то события, например, завершения другого потока, или перехода его в состояние готовности выполнять работу. Следующий пример демонстрирует запуск потока и ожидание завершения его выполнения (это минимальная модификация для сравнения примера запуска потока ранее):

mod_thr2.c :

	#include <linux/module.h>
	#include <linux/sched.h>
	#include <linux/delay.h>    
	
	static int thread( void * data ) {
	   struct completion *finished = (struct completion*)data;
	   struct task_struct *curr = current;      /* current - указатель на дескриптор текущей задачи */
	   printk( KERN_INFO "child process [%d] is running\n", curr->pid );
	   msleep( 10000 );                        /* Пауза 10 с. */
	   printk( KERN_INFO "child process [%d] is completed\n", curr->pid );
	   complete( finished );                   /* Отмечаем факт выполнения условия. */
	   return 0;
	}
	
	int test_thread( void ) {
	   DECLARE_COMPLETION( finished );
	   struct task_struct *curr = current;
	   printk( KERN_INFO "main process [%d] is running\n", curr->pid );
	   pid_t pid = kernel_thread( thread, &finished, CLONE_FS ); /* Запускаем новый поток */
	   msleep( 5000 );                                          /* Пауза 5 с. */
	   wait_for_completion( &finished );                         /* Ожидаем выполнения условия */
	   printk( KERN_INFO "main process [%d] is completed\n", curr->pid );
	   return -1;
	}
	
	module_init( test_thread );

Выполнение этого примера разительно отличается от его предыдущего прототипа (обратите внимание на временные метки сообщений!):

$ sudo insmod ./mod_thr2.ko

	insmod: error inserting './mod_thr2.ko': -1 Operation not permitted

$ sudo cat /var/log/messages | tail -n4

	Apr 17 21:20:23 notebook kernel: main process [12406] is running
	Apr 17 21:20:23 notebook kernel: child process [12407] is running
	Apr 17 21:20:33 notebook kernel: child process [12407] is completed
	Apr 17 21:20:33 notebook kernel: main process [12406] is completed

$ ps -A | grep 12406

$ ps -A | grep 12407

	$   

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

	DECLARE_COMPLETION( name );

Либо инициализироваться динамически:

	void init_completion( struct completion * );

Примечание: Всё разнообразие в Linux как потоков ядра (kernel_thread()), так и параллельных процессов (fork()) и потоков пространства пользователя (pthread_create()) обеспечивается тем, что потоки и процессы в этой системе фактически не разделены принципиально, и те и другие создаются единым системным вызовом clone() - все различия создания определяются набором флагов вида CLONE_* для создаваемой задачи (последний параметр kernel_thread() нашего примера).


Предыдущий раздел: Оглавление Следующий раздел:
Потоки ядра   Атомарные переменные и операции