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

UnixForum





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

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

Параллелизм и синхронизация

«Две передние, старшие, ноги вели животное в одну сторону – за большой головой, а две задние, младшие, ноги – в противоположную, за снабжённым головой женским хвостом.»

Милорад Павич «Смерть святого Савы, или невидимая сторона Луны»

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

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

  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         6     2  0 09:52 ?       00:00:00 [migration/1]
	root         7     2  0 09:52 ?       00:00:00 [ksoftirqd/1]
	root         8     2  0 09:52 ?       00:00:00 [watchdog/1]
	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       518     1  0 09:52 ?       00:00:00 /sbin/udevd -d
	root       858     2  0 09:53 ?       00:00:00 [tifm]
	root       870     2  0 09:53 ?       00:00:00 [kmmcd]
	...
	root      1224     1  0 09:53 ?       00:00:00 /sbin/rsyslogd -c 4
	root      1245     2  0 09:53 ?       00:00:00 [kondemand/0]
	root      1246     2  0 09:53 ?       00:00:00 [kondemand/1]
	rpc       1268     1  0 09:53 ?       00:00:00 rpcbind
	...
	rpcuser   1323     1  0 09:53 ?       00:00:00 rpc.statd
	...
	root      1353     2  0 09:53 ?       00:00:00 [rpciod/0]
	root      1354     2  0 09:53 ?       00:00:00 [rpciod/1]
	root      1361     1  0 09:53 ?       00:00:00 rpc.idmapd
	...
	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      1726     2  0 09:53 ?       00:00:00 [nfsd]
	root      1727     2  0 09:53 ?       00:00:00 [nfsd]
	root      1728     2  0 09:53 ?       00:00:00 [nfsd]
	root      1729     2  0 09:53 ?       00:00:00 [nfsd]
	root      1730     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 unrunnable, 0 runnable, >0 stopped */
	   void *stack;
	...
	   int prio, static_prio, normal_prio;
	...
	   pid_t pid;
	...
	   cputime_t utime, stime, utimescaled, stimescaled;
	...
	}

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


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