Мобильное программирование приложений реального времени в стандарте POSIX
Опрос и изменение атрибутов потоков управления
Следуя классическому принципу "познай самого себя", описание функций, обслуживающих потоки управления, мы начнем с функции pthread_self(), возвращающей в качестве результата идентификатор вызвавшего ее потока (см. листинг 1.1).
#include
Листинг 1.1. Описание функции pthread_self(). (html, txt)
Выше мы отмечали, что тип pthread_t трактуется стандартом POSIX-2001 как абстрактный. На уровне языка C он может быть представлен, например, структурой. Для работы со значениями типа pthread_t предусмотрены два метода: присваивание и сравнение на равенство, реализуемое функцией pthread_equal() (см. листинг 1.2).
#include
Листинг 1.2. Описание функции pthread_equal(). (html, txt)
Если значения аргументов t1 и t2 равны, результат функции pthread_equal() отличен от нуля.
Атрибуты потоков управления, используемые при создании последних, сгруппированы в упоминавшиеся выше атрибутные объекты. Для инициализации и разрушения атрибутных объектов служат функции pthread_attr_init() и pthread_attr_destroy() (см. листинг 1.3).
#include
int pthread_attr_init ( pthread_attr_t *attr);
int pthread_attr_destroy ( pthread_attr_t *attr);
Листинг 1.3. Описание функций pthread_attr_init() и pthread_attr_destroy(). (html, txt)
Функция pthread_attr_init() инициализирует атрибутный объект, заданный указателем attr, подразумеваемыми значениями для всех индивидуальных атрибутов потоков управления, предусмотренных реализацией.
Функция pthread_attr_destroy() разрушает заданный атрибутный объект. Впрочем, "разрушает", возможно, слишком сильный термин. Быть может, реализация просто присваивает значениям атрибутов недопустимые значения. Во всяком случае, разрушенный атрибутный объект в дальнейшем может быть вновь инициализирован.
Структура атрибутных объектов скрыта от приложений, но сам набор стандартизованных атрибутов выглядит вполне естественно.
Следуя классическому принципу "познай самого себя", описание функций, обслуживающих потоки управления, мы начнем с функции pthread_self(), возвращающей в качестве результата идентификатор вызвавшего ее потока (см. листинг 1.1).
#include
Листинг 1.1. Описание функции pthread_self().
Выше мы отмечали, что тип pthread_t трактуется стандартом POSIX-2001 как абстрактный. На уровне языка C он может быть представлен, например, структурой. Для работы со значениями типа pthread_t предусмотрены два метода: присваивание и сравнение на равенство, реализуемое функцией pthread_equal() (см. листинг 1.2).
#include
Листинг 1.2. Описание функции pthread_equal().
Если значения аргументов t1 и t2 равны, результат функции pthread_equal() отличен от нуля.
Атрибуты потоков управления, используемые при создании последних, сгруппированы в упоминавшиеся выше атрибутные объекты. Для инициализации и разрушения атрибутных объектов служат функции pthread_attr_init() и pthread_attr_destroy() (см. листинг 1.3).
#include
int pthread_attr_init ( pthread_attr_t *attr);
int pthread_attr_destroy ( pthread_attr_t *attr);
Листинг 1.3. Описание функций pthread_attr_init() и pthread_attr_destroy().
Функция pthread_attr_init() инициализирует атрибутный объект, заданный указателем attr, подразумеваемыми значениями для всех индивидуальных атрибутов потоков управления, предусмотренных реализацией.
Функция pthread_attr_destroy() разрушает заданный атрибутный объект. Впрочем, "разрушает", возможно, слишком сильный термин. Быть может, реализация просто присваивает значениям атрибутов недопустимые значения. Во всяком случае, разрушенный атрибутный объект в дальнейшем может быть вновь инициализирован.
Структура атрибутных объектов скрыта от приложений, но сам набор стандартизованных атрибутов выглядит вполне естественно. Их описание мы начнем с атрибутов стека – начального адреса и размера – и методов для их опроса и установки (см.
Их описание мы начнем с атрибутов стека – начального адреса и размера – и методов для их опроса и установки (см. листинг 1.4).
#include
int pthread_attr_getstack ( const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);
int pthread_attr_setstack ( pthread_attr_t *attr, void *stackaddr, size_t stacksize);
Листинг 1.4. Описание функций pthread_attr_getstack() и pthread_attr_setstack(). (html, txt)
Размер стека должен составлять не менее PTHREAD_STACK_MIN, начальный адрес – должным образом выровнен. Память, отведенная под стек, должна быть доступна на чтение и запись.
Функция pthread_attr_getstack() помещает атрибуты стека по указателям stackaddr и stacksize. Это – проявление единообразной для семейства функций pthread*(), обслуживающих потоки управления, дисциплины возврата результатов. Содержательные данные помещаются в выходные аргументы. При нормальном завершении результат функции равен нулю; в противном случае выдается код ошибки.
Подобная дисциплинированность является похвальной, но вынужденной. Ее причина – в разделении данных между потоками. Нельзя просто вернуть указатель на статический буфер – другой поток может в это время так или иначе работать с ним. Поэтому поток должен зарезервировать индивидуальные области памяти для размещения выходных значений (обратившись, например, к malloc()) и передать функции указатели на них.
Для опроса и изменения размера защитной области, служащей цели обнаружения переполнения стека, предназначены функции pthread_attr_getguardsize() и pthread_attr_setguardsize() (см. листинг 1.5).
#include
int pthread_attr_getguardsize ( const pthread_attr_t *restrict attr, size_t *restrict guardsize);
int pthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize);
Листинг 1.5. Описание функций pthread_attr_getguardsize() и pthread_attr_setguardsize(). (html, txt)
Если значение аргумента guardsize функции pthread_attr_setguardsize() равно нулю, при создании потоков управления с атрибутным объектом *attr защитная область отводиться не будет.
Положительные величины guardsize также становятся новыми значениями одноименного атрибута, однако являются лишь указанием операционной системе; реальный размер защитной области может быть больше заданного.
Приложение, соответствующее стандарту POSIX, должно использовать значения guardsize, кратные конфигурационной константе PAGESIZE, которая одновременно является подразумеваемым значением данного атрибута.
Если приложение посредством функции pthread_attr_setstack() взяло на себя управление стеками потоков, атрибут guardsize игнорируется, операционная система не отводит защитную область, а контроль за переполнением стека возлагается на приложение.
Отметим, что, в зависимости от ситуации, приложениям есть смысл как отказываться от защитных областей (например, если потоков управления много, памяти на защитные области уходит также много, и авторы приложения уверены, что переполнения стека быть не может), так и делать их размер больше подразумеваемого (например, если используются большие массивы и указатель стека рискует оказаться за верхней границей защитной области).
Стандартом POSIX-2001 предусмотрена группа атрибутов, обслуживающих планирование потоков управления. Соответствующие описания размещены в заголовочном файле
Реализация может поддерживать политику планирования SCHED_SPORADIC (спорадическое планирование), предусматривающую резервирование определенного количества вычислительной мощности для обработки с заданным приоритетом неких единичных, непериодических (спорадических) событий. В этом случае должны быть определены конфигурационные константы _POSIX_SPORADIC_SERVER и/или _POSIX_THREAD_SPORADIC_SERVER, а в структуре sched_param должны присутствовать следующие дополнительные поля.
int sched_ss_low_priority; /* Нижняя граница приоритета */ /* планирования сервера */ /* спорадических событий */ struct timespec sched_ss_repl_period; /* Период пополнения бюджета */ /* спорадического сервера */ struct timespec sched_ss_init_budget; /* Начальный бюджет */ /* спорадического сервера */ int sched_ss_max_repl; /* Максимальное число */ /* ждущих операций */ /* пополнений бюджета */ /* спорадического сервера */
Для опроса и установки атрибутов планирования в атрибутных объектах служат функции pthread_attr_getschedparam() и pthread_attr_setschedparam() (см. листинг 1.6).
#include
int pthread_attr_getschedparam ( const pthread_attr_t *restrict attr, struct sched_param *restrict param);
int pthread_attr_setschedparam ( pthread_attr_t *restrict attr, const struct sched_param *restrict param);
Листинг 1.6. Описание функций pthread_attr_getschedparam() и pthread_attr_setschedparam(). (html, txt)
Атрибут "политика планирования", способный принимать значения SCHED_FIFO (планирование по очереди), SCHED_RR (циклическое планирование), SCHED_OTHER ("прочее" планирование) и, возможно, SCHED_SPORADIC (спорадическое планирование), можно опросить и установить посредством функций pthread_attr_getschedpolicy() и pthread_attr_setschedpolicy() (см. листинг 1.7).
#include
int pthread_attr_getschedpolicy ( const pthread_attr_t *restrict attr, int *restrict policy);
int pthread_attr_setschedpolicy ( pthread_attr_t *attr, int policy);
Листинг 1.7. Описание функций pthread_attr_getschedpolicy() и pthread_attr_setschedpolicy(). (html, txt)
листинг 1.4).
#include
int pthread_attr_getstack ( const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);
int pthread_attr_setstack ( pthread_attr_t *attr, void *stackaddr, size_t stacksize);
Листинг 1.4. Описание функций pthread_attr_getstack() и pthread_attr_setstack().
Размер стека должен составлять не менее PTHREAD_STACK_MIN, начальный адрес – должным образом выровнен. Память, отведенная под стек, должна быть доступна на чтение и запись.
Функция pthread_attr_getstack() помещает атрибуты стека по указателям stackaddr и stacksize. Это – проявление единообразной для семейства функций pthread*(), обслуживающих потоки управления, дисциплины возврата результатов. Содержательные данные помещаются в выходные аргументы. При нормальном завершении результат функции равен нулю; в противном случае выдается код ошибки.
Подобная дисциплинированность является похвальной, но вынужденной. Ее причина – в разделении данных между потоками. Нельзя просто вернуть указатель на статический буфер – другой поток может в это время так или иначе работать с ним. Поэтому поток должен зарезервировать индивидуальные области памяти для размещения выходных значений (обратившись, например, к malloc()) и передать функции указатели на них.
Для опроса и изменения размера защитной области, служащей цели обнаружения переполнения стека, предназначены функции pthread_attr_getguardsize() и pthread_attr_setguardsize() (см. листинг 1.5).
#include
int pthread_attr_getguardsize ( const pthread_attr_t *restrict attr, size_t *restrict guardsize);
int pthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize);
Листинг 1.5. Описание функций pthread_attr_getguardsize() и pthread_attr_setguardsize().
Если значение аргумента guardsize функции pthread_attr_setguardsize() равно нулю, при создании потоков управления с атрибутным объектом *attr защитная область отводиться не будет. Положительные величины guardsize также становятся новыми значениями одноименного атрибута, однако являются лишь указанием операционной системе; реальный размер защитной области может быть больше заданного.
Приложение, соответствующее стандарту POSIX, должно использовать значения guardsize, кратные конфигурационной константе PAGESIZE, которая одновременно является подразумеваемым значением данного атрибута.
Если приложение посредством функции pthread_attr_setstack() взяло на себя управление стеками потоков, атрибут guardsize игнорируется, операционная система не отводит защитную область, а контроль за переполнением стека возлагается на приложение.
Отметим, что, в зависимости от ситуации, приложениям есть смысл как отказываться от защитных областей (например, если потоков управления много, памяти на защитные области уходит также много, и авторы приложения уверены, что переполнения стека быть не может), так и делать их размер больше подразумеваемого (например, если используются большие массивы и указатель стека рискует оказаться за верхней границей защитной области).
Стандартом POSIX-2001 предусмотрена группа атрибутов, обслуживающих планирование потоков управления. Соответствующие описания размещены в заголовочном файле
Реализация может поддерживать политику планирования SCHED_SPORADIC (спорадическое планирование), предусматривающую резервирование определенного количества вычислительной мощности для обработки с заданным приоритетом неких единичных, непериодических (спорадических) событий. В этом случае должны быть определены конфигурационные константы _POSIX_SPORADIC_SERVER и/или _POSIX_THREAD_SPORADIC_SERVER, а в структуре sched_param должны присутствовать следующие дополнительные поля.
int sched_ss_low_priority; /* Нижняя граница приоритета */ /* планирования сервера */ /* спорадических событий */ struct timespec sched_ss_repl_period; /* Период пополнения бюджета */ /* спорадического сервера */ struct timespec sched_ss_init_budget; /* Начальный бюджет */ /* спорадического сервера */ int sched_ss_max_repl; /* Максимальное число */ /* ждущих операций */ /* пополнений бюджета */ /* спорадического сервера */
Для опроса и установки атрибутов планирования в атрибутных объектах служат функции pthread_attr_getschedparam() и pthread_attr_setschedparam() (см. листинг 1.6).
#include
int pthread_attr_getschedparam ( const pthread_attr_t *restrict attr, struct sched_param *restrict param);
int pthread_attr_setschedparam ( pthread_attr_t *restrict attr, const struct sched_param *restrict param);
Листинг 1.6. Описание функций pthread_attr_getschedparam() и pthread_attr_setschedparam().
Атрибут "политика планирования", способный принимать значения SCHED_FIFO (планирование по очереди), SCHED_RR (циклическое планирование), SCHED_OTHER ("прочее" планирование) и, возможно, SCHED_SPORADIC (спорадическое планирование), можно опросить и установить посредством функций pthread_attr_getschedpolicy() и pthread_attr_setschedpolicy() (см. листинг 1.7).
#include
int pthread_attr_getschedpolicy ( const pthread_attr_t *restrict attr, int *restrict policy);
int pthread_attr_setschedpolicy ( pthread_attr_t *attr, int policy);
Листинг 1.7. Описание функций pthread_attr_getschedpolicy() и pthread_attr_setschedpolicy().
Описанный выше атрибут "область планирования конкуренции", способный принимать значения PTHREAD_SCOPE_SYSTEM и PTHREAD_SCOPE_PROCESS, обслуживают функции pthread_attr_getscope() и pthread_attr_setscope() (см. листинг 1.8).
#include
int pthread_attr_getscope ( const pthread_attr_t *restrict attr, int *restrict contentionscope);
int pthread_attr_setscope ( pthread_attr_t *attr, int contentionscope);
Листинг 1.8. Описание функций pthread_attr_getscope() и pthread_attr_setscope().
При создании потока управления все рассмотренные выше атрибуты планирования, в зависимости от значения PTHREAD_INHERIT_SCHED или PTHREAD_EXPLICIT_SCHED атрибута inheritsched, могут наследоваться у создающего потока или извлекаться из атрибутного объекта. Для опроса и изменения этого атрибута предназначены функции pthread_attr_getinheritsched() и pthread_attr_setinheritsched() (см.
листинг 1.9).
#include
int pthread_attr_getinheritsched ( const pthread_attr_t *restrict attr, int *restrict inheritsched);
int pthread_attr_setinheritsched ( pthread_attr_t *attr, int inheritsched);
Листинг 1.9. Описание функций pthread_attr_getinheritsched() и pthread_attr_setinheritsched().
Атрибут обособленности потока управления, присутствующий в атрибутном объекте, можно опросить и установить посредством функций pthread_attr_getdetachstate() и pthread_attr_setdetachstate() (см. листинг 1.10).
#include
int pthread_attr_getdetachstate ( const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstate ( pthread_attr_t *attr, int detachstate);
Листинг 1.10. Описание функций pthread_attr_getdetachstate() и pthread_attr_setdetachstate().
Напомним, что значение этого атрибута (PTHREAD_CREATE_DETACHED или PTHREAD_CREATE_JOINABLE) определяет, будет ли поток управления создан как обособленный или присоединяемый, то есть доступный другим потокам для ожидания завершения. Подразумеваемым является значение PTHREAD_CREATE_JOINABLE.
Значения атрибутов планирования могут задаваться не только при создании потока управления. Стандарт POSIX-2001 предоставляет средства для их динамического изменения и опроса (см. листинг 1.11).
#include
int pthread_getschedparam ( pthread_t thread, int *restrict policy, struct sched_param *restrict param);
int pthread_setschedparam ( pthread_t thread, int policy, const struct sched_param *param);
Листинг 1.11. Описание функций pthread_getschedparam() и pthread_setschedparam().
Отметим две тонкости, связанные с функцией pthread_setschedparam(). Во-первых, возможно, что для ее успешного вызова процесс должен обладать соответствующими привилегиями. Во-вторых, реализация не обязана поддерживать динамический переход к политике спорадического планирования (SCHED_SPORADIC) (как, впрочем, и саму эту политику).
Если требуется изменить лишь приоритет планирования, не меняя политику, проще воспользоваться функцией pthread_setschedprio() (см.
листинг 1.12), которая, правда, является новой и в исторически сложившихся реализациях может отсутствовать.
#include
Листинг 1.12. Описание функции pthread_setschedprio().
Сходную направленность, но более глобальный характер имеют функции pthread_getconcurrency() и pthread_setconcurrency() (см. листинг 1.13), позволяющие опросить и изменить уровень параллелизма выполнения потоков управления.
#include
int pthread_getconcurrency (void);
int pthread_setconcurrency (int new_level);
Листинг 1.13. Описание функций pthread_getconcurrency() и pthread_setconcurrency().
По умолчанию операционная система предоставляет возможность параллельно проявлять активность некоему "достаточному числу" потоков управления в процессе, так, чтобы это не вело к перерасходу системных ресурсов. Некоторым приложениям, однако, может требоваться более высокий уровень параллелизма; это требование они могут передать ОС в виде значения аргумента new_level функции pthread_setconcurrency(). Впрочем, с точки зрения операционной системы это всего лишь просьба или рекомендация; стандарт не специфицирует реально устанавливаемый уровень.
Нулевое значение аргумента new_level означает переход к подразумеваемому уровню параллелизма, как если бы функция pthread_setconcurrency() ранее не вызывалась.
Функция pthread_getconcurrency() в качестве результата возвращает значение уровня параллелизма, установленное предыдущим вызовом pthread_setconcurrency(). Если такового не было, выдается нуль.
Отметим, что изменение уровня параллелизма не рекомендуется использовать при реализации библиотечных функций, так как это может конфликтовать с запросами приложений.
К числу атрибутов потока управления можно отнести обслуживающие его часы процессорного времени. Для выяснения их идентификатора достаточно обратиться к функции pthread_getcpuclockid() (см. листинг 1.14).
#include
int pthread_getcpuclockid ( pthread_t thread_id, clockid_t *clock_id);
Листинг 1.14. Описание функции pthread_getcpuclockid().
Еще один атрибут потока управления – маска блокированных сигналов. Поток может опросить и/или изменить ее посредством вызова функции pthread_sigmask() (см. листинг 1.15) – аналога рассмотренной в курсе [1] функции sigprocmask().
#include
Листинг 1.15. Описание функции pthread_sigmask().
На листинге 1.16 приведен пример программы, использующей большинство описанных выше функций для опроса и изменения атрибутов потоков управления. Возможные результаты работы этой программы показаны на листинге 1.17.
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа опрашивает атрибуты потоков управления */ /* и изменяет некоторые из них */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */
#define _XOPEN_SOURCE 600
#include
int main (void) { pthread_t ct_id; /* Идентификатор текущего потока управления */ pthread_attr_t patob; /* Атрибутный объект для создания потоков управления */ int res; /* Переменная для запоминания результатов "потоковых" функций */ void *stackaddr; /* Начало стека как атрибут потока управления */ size_t atrsize; /* Размеры как атрибуты потока управления */ /* Структура с параметрами планирования */ struct sched_param shdprm; char *spname; /* Названия политики планирования, области */ /* планирования конкуренции и т.п. */
printf ("Идентификатор текущего потока управления: %lx\n", (ct_id = pthread_self ()));
if ((errno = pthread_attr_init (&patob)) != 0) { perror ("PTHREAD_ATTR_INIT"); return (errno); }
printf ("Значения, установленные системой " "в атрибутном объекте\n");
if ((errno = pthread_attr_getstack (&patob, &stackaddr, &atrsize)) != 0) { perror ("PTHREAD_ATTR_GETSTACK"); return (errno); } printf ("Адрес начала стека: %p\n", stackaddr); printf ("Размер стека: %d\n", atrsize);
assert (pthread_attr_getguardsize (&patob, &atrsize) == 0); printf ("Размер защитной области: %d\n", atrsize);
assert (pthread_attr_getschedparam (&patob, &shdprm) == 0); assert (pthread_attr_getschedpolicy (&patob, &res) == 0); switch (res) { case SCHED_FIFO: spname = "Планирование по очереди"; break; case SCHED_RR: spname = "Циклическое планирование"; break; case SCHED_OTHER: spname = "Прочее планирование"; break; default: spname = "Неизвестная политика планирования"; } printf ("Политика планирования: %s\n", spname); printf ("Приоритет планирования: %d\n", shdprm.sched_priority);
assert (pthread_attr_getscope (&patob, &res) == 0); switch (res) { case PTHREAD_SCOPE_SYSTEM: spname = "Система"; break; case PTHREAD_SCOPE_PROCESS: spname = "Процесс"; break; default: spname = "Неизвестная область планирования " "конкуренции"; } printf ("Область планирования конкуренции: %s\n", spname);
assert (pthread_attr_getinheritsched (&patob, &res) == 0); switch (res) { case PTHREAD_INHERIT_SCHED: spname = "Наследуются у родительского потока"; break; case PTHREAD_EXPLICIT_SCHED: spname = "Извлекаются из атрибутного объекта"; break; default: spname = "Устанавливаются неизвестным образом"; } printf ("Атрибуты планирования: %s\n", spname);
assert (pthread_attr_getdetachstate (&patob, &res) == 0); switch (res) { case PTHREAD_CREATE_JOINABLE: spname = "Присоединяемые"; break; case PTHREAD_CREATE_DETACHED: spname = "Обособленные"; break; default: spname = "Неизвестные"; } printf (" Потоки управления создаются как: %s\n", spname);
/* Изменим значения атрибутов планирования и уровня */ /* параллелизма */ shdprm.sched_priority = 1; if ((errno = pthread_setschedparam (ct_id, SCHED_RR, &shdprm)) != 0) { perror ("PTHREAD_SETSCHEDPARAM"); } if ((errno = pthread_setconcurrency (8192)) != 0) { perror ("PTHREAD_SETCONCURRENCY"); } printf ("\nТекущие значения атрибутов потоков управления\n");
assert (pthread_getschedparam (ct_id, &res, &shdprm) == 0); switch (res) { case SCHED_FIFO: spname = "Планирование по очереди"; break; case SCHED_RR: spname = "Циклическое планирование"; break; case SCHED_OTHER: spname = "Прочее планирование"; break; default: spname = "Неизвестная политика планирования"; } printf ("Политика планирования: %s\n", spname); printf ("Приоритет планирования: %d\n", shdprm.sched_priority); printf ("Уровень параллелизма: %d\n", pthread_getconcurrency());
return 0; }
Листинг 1.16. Пример программы, опрашивающей и изменяющей значения атрибутов потоков управления.
Идентификатор текущего потока управления: 400 Значения, установленные системой в атрибутном объекте Адрес начала стека: 0xffe01000 Размер стека: 2093056 Размер защитной области: 4096 Политика планирования: Прочее планирование Приоритет планирования: 0 Область планирования конкуренции: Система Атрибуты планирования: Извлекаются из атрибутного объекта Потоки управления создаются как: Присоединяемые
Текущие значения атрибутов потоков управления Политика планирования: Циклическое планирование Приоритет планирования: 1 Уровень параллелизма: 8192
Листинг 1.17. Возможные результаты работы программы, опрашивающей и изменяющей значения атрибутов потоков управления.
Листинг 1.18 содержит результаты выполнения упрощенного варианта этой же программы (без вызовов функций pthread_attr_getstack(), pthread_attr_getguardsize(), pthread_getconcurrency(), pthread_setconcurrency() и без соответствующих выдач) для операционной системы реального времени oc2000, соответствующей подмножеству требований стандарта POSIX-2001.
Идентификатор текущего потока управления: f31ae0 Значения, установленные системой в атрибутном объекте Политика планирования: Планирование по очереди Приоритет планирования: 100 Область планирования конкуренции: Процесс Атрибуты планирования: Извлекаются из атрибутного объекта Потоки управления создаются как: Присоединяемые
Текущие значения атрибутов потоков управления Политика планирования: Циклическое планирование Приоритет планирования: 1
Листинг 1.18. Возможные результаты работы программы, опрашивающей и изменяющей значения атрибутов потоков управления, для операционной системы реального времени oc2000.
Основные идеи, понятия и объекты
Напомним, уточним и дополним определения, которые были даны в курсе [1] применительно к потокам управления.
Процесс – это адресное пространство вместе с выполняемыми в нем потоками управления, а также системными ресурсами, которые этим потокам требуются.
После того, как процесс создан с помощью функции fork(), он считается активным. Сразу после создания в его рамках существует ровно один поток управления – копия того, что вызвал fork().
До завершения процесса в его рамках существуют по крайней мере один поток управления и адресное пространство.
Большинство атрибутов процесса разделяются существующими в его рамках потоками управления. К числу индивидуальных атрибутов относятся идентификатор, приоритет и политика планирования, значение переменной errno, ассоциированные с потоком управления пары ключ/значение (служащие для организации индивидуальных данных потока и доступа к ним), а также системные ресурсы, требующиеся для поддержки потока управления.
Идентификатор потока управления уникален в пределах процесса, но не системы в целом.
Идентификаторы потоков управления представлены значениями типа pthread_t, который трактуется в стандарте POSIX-2001 как абстрактный. В частности, для него определен метод сравнения значений на равенство.
Всем потокам управления одного процесса доступны все объекты, адреса которых могут быть определены потоком. В число таких объектов входят статические переменные, области динамической памяти, полученные от функции malloc(), прямоадресуемая память, полученная от системно-зависимых функций, автоматические переменные и т.д.
По отношению к потокам управления вводится понятие безопасных функций, которые можно вызывать параллельно в нескольких потоках без нарушения корректности их функционирования. К числу безопасных принадлежат "чистые" функции, а также функции, обеспечивающие взаимное исключение перед доступом к разделяемым объектам. Если в стандарте явно не оговорено противное, функция считается потоково-безопасной.
Выполняющимся (активным) называется поток управления, обрабатываемый в данный момент процессором.
В многопроцессорных конфигурациях может одновременно выполняться несколько потоков.
Поток управления считается готовым к выполнению, если он способен стать активным, но не может этого сделать из-за отсутствия свободного процессора.
Поток управления называется вытесненным, если его выполнение приостановлено из-за того, что другой поток с более высоким приоритетом стал готов к выполнению.
Поток управления считается блокированным, если для продолжения его выполнения должно стать истинным некоторое условие, отличное от доступности процессора.
Списком потоков управления называется упорядоченный набор равноприоритетных потоков, готовых к выполнению. Порядок потоков в списке определяется политикой планирования. Множество наборов включает все потоки в системе, готовые к выполнению.
Планированием, согласно стандарту POSIX-2001, называется применение политики выбора процесса или потока управления, готового к выполнению, для его перевода в число активных, а также политики изменения списков потоков управления.
Под политикой планирования понимается набор правил, используемых для определения порядка выполнения процессов или потоков управления для достижения некоторой цели.
Политика планирования воздействует на порядок процессов (потоков управления) по крайней мере в следующих ситуациях:
Область планирования размещения – это набор процессоров, по отношению к которым в некоторый момент времени может планироваться поток управления.
Областью планирования конкуренции называется свойство потока управления, определяющее набор потоков, с которыми он конкурирует за ресурсы, например, за процессор. В стандарте POSIX-2001 предусмотрены две подобные области – PTHREAD_SCOPE_SYSTEM (конкуренция в масштабе системы) и PTHREAD_SCOPE_PROCESS (конкуренция в масшабе процесса).
Пожалуй, общей проблемой всех приложений является контроль переполнения стека.
Для ее решения стандартом POSIX- 2001 предусмотрено существование так называемой защитной области, расположенной за верхней границей стека. При переполнении и попадания указателя стека в защитную область операционная система должна фиксировать ошибку – например, доставлять потоку управления сигнал SIGSEGV.
С каждым потоком управления ассоциирован атрибутный объект – собрание атрибутов потока с конфигурируемыми значениями, таких как адрес и размер стека, параметры планирования и т.п. В стандарте POSIX-2001 атрибутные объекты представлены как значения абстрактного типа pthread_attr_t, внутренняя структура значений которого скрыта от приложений. Смысл введения атрибутных объектов – сгруппировать немобильные параметры потоков, чтобы облегчить адаптацию приложений к новым целевым платформам. Использование идеологии абстрактных объектов позволяет безболезненно добавлять новые атрибуты, не теряя обратной совместимости.
Обратим внимание на следующее обстоятельство, важное для реализации многопотоковых приложений. Иногда потоки управления называют легковесными процессами, поскольку они требуют существенно меньшей, чем обычные процессы, аппаратно-программной поддержки и, кроме того, их функционирование сопряжено с меньшими накладными расходами. С этой точки зрения потоками можно пользоваться более свободно, чем процессами. С другой стороны, потоки одного процесса никак не защищены друг от друга, они разделяют одно адресное пространство, поэтому, в отличие от полноценных процессов, ошибки в программе одного из них могут сказаться на других, породить ситуации, которые трудно воспроизвести, что делает поиск и исправление ошибок крайне сложными. В этом смысле легковесной можно назвать аналогию с процессами, основанную только на возможности параллельной работы. Она обманчива, поскольку из вида упускается очень важный аспект разделения доменов выполнения. Вообще говоря, многопотоковые приложения существенно менее надежны, чем многопроцессные, поэтому потоками управления следует пользоваться осторожно, систематически.
Операции с потоками управления можно подразделить на две группы:
В таком порядке они и будут рассматриваться далее. Отметим, что стандарт POSIX-2001 относит их к необязательной части, именуемой, как нетрудно догадаться, "Потоки управления" ("Threads", THR).
Модель, принятая в стандарте применительно к созданию потоков управления, отличается от соответствующей модели для процессов. При создании нового потока задается функция, с вызова которой начнется его выполнение, то есть вместо пары вида fork()/exec() создающий поток должен обратиться лишь к одной функции – pthread_create(). Впрочем, как мы увидим далее, и для процессов в стандарте POSIX-2001 произошел отход от классических канонов – введены средства (функции семейства posix_spawn()) для порождения процессов "в один ход".
Потоки управления бывают обособленными (отсоединенными) и присоединяемыми; только последние доступны другим потокам для ожидания завершения и, быть может, утилизации освободившихся ресурсов. Ресурсы, освободившиеся после завершения обособленных потоков управления, утилизирует операционная система.
Поток управления можно терминировать изнутри и извне (из других потоков того же процесса). Поток может управлять состоянием восприимчивости к терминированию (разрешить/запретить собственное терминирование извне), а также специфицировать тип терминирования (отложенное или немедленное, асинхронное).
Отложенное терминирование происходит только по достижении потоком управления точек терминирования – мест в оговоренных в стандарте POSIX функциях, где поток должен отреагировать на ждущие запросы на терминирование (если оно разрешено), перед тем как его выполнение будет приостановлено на неопределенное время с сохранением состояния восприимчивости к терминированию.
Согласно стандарту POSIX-2001, точки терминирования имеются в таких функциях, как accept(), connect(), msgrcv(), msgsnd(), pause(), read(), sleep(), wait(), write() и сходных с ними по поведению.Допускается существование подобных точек и в других, также оговоренных в стандарте POSIX функциях – printf(), scanf(), semop() и т.п.
pthread_t pthread_self
|
#include |
| Листинг 1.1. Описание функции pthread_self(). |
| Закрыть окно |
|
#include |
| Листинг 1.2. Описание функции pthread_equal(). |
| Закрыть окно |
|
#include int pthread_attr_init ( pthread_attr_t *attr); int pthread_attr_destroy ( pthread_attr_t *attr); |
| Листинг 1.3. Описание функций pthread_attr_init() и pthread_attr_destroy(). |
| Закрыть окно |
|
#include int pthread_attr_getstack ( const pthread_attr_t * restrict attr, void **restrict stackaddr, size_t *restrict stacksize); int pthread_attr_setstack ( pthread_attr_t *attr, void *stackaddr, size_t stacksize); |
| Листинг 1.4. Описание функций pthread_attr_getstack() и pthread_attr_setstack(). |
| Закрыть окно |
|
#include int pthread_attr_getguardsize ( const pthread_attr_t * restrict attr, size_t *restrict guardsize); int pthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize); |
| Листинг 1.5. Описание функций pthread_attr_getguardsize() и pthread_attr_setguardsize(). |
| Закрыть окно |
|
#include int pthread_attr_getschedparam ( const pthread_attr_t *restrict attr, struct sched_param *restrict param); int pthread_attr_setschedparam ( pthread_attr_t * restrict attr, const struct sched_param *restrict param); |
| Листинг 1.6. Описание функций pthread_attr_getschedparam() и pthread_attr_setschedparam(). |
| Закрыть окно |
|
#include int pthread_attr_getschedpolicy ( const pthread_attr_t * restrict attr, int *restrict policy); int pthread_attr_setschedpolicy ( pthread_attr_t *attr, int policy); |
| Листинг 1.7. Описание функций pthread_attr_getschedpolicy() и pthread_attr_setschedpolicy(). |
| Закрыть окно |
|
#include int pthread_attr_getscope ( const pthread_attr_t * restrict attr, int *restrict contentionscope); int pthread_attr_setscope ( pthread_attr_t *attr, int contentionscope); |
| Листинг 1.8. Описание функций pthread_attr_getscope() и pthread_attr_setscope(). |
| Закрыть окно |
|
#include int pthread_attr_getinheritsched ( const pthread_attr_t * restrict attr, int *restrict inheritsched); int pthread_attr_setinheritsched ( pthread_attr_t *attr, int inheritsched); |
| Листинг 1.9. Описание функций pthread_attr_getinheritsched() и pthread_attr_setinheritsched(). |
| Закрыть окно |
|
#include int pthread_attr_getdetachstate ( const pthread_attr_t *attr, int *detachstate); int pthread_attr_setdetachstate ( pthread_attr_t *attr, int detachstate); |
| Листинг 1.10. Описание функций pthread_attr_getdetachstate() и pthread_attr_setdetachstate(). |
| Закрыть окно |
|
#include int pthread_getschedparam ( pthread_t thread, int *restrict policy, struct sched_param *restrict param); int pthread_setschedparam ( pthread_t thread, int policy, const struct sched_param *param); |
| Листинг 1.11. Описание функций pthread_getschedparam() и pthread_setschedparam(). |
| Закрыть окно |
|
#include |
| Листинг 1.12. Описание функции pthread_setschedprio(). |
| Закрыть окно |
|
#include int pthread_getconcurrency (void); int pthread_setconcurrency (int new_level); |
| Листинг 1.13. Описание функций pthread_getconcurrency() и pthread_setconcurrency(). |
| Закрыть окно |
|
#include int pthread_getcpuclockid ( pthread_t thread_id, clockid_t *clock_id); |
| Листинг 1.14. Описание функции pthread_getcpuclockid(). |
| Закрыть окно |
|
#include |
| Листинг 1.15. Описание функции pthread_sigmask(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа опрашивает атрибуты потоков управления */ /* и изменяет некоторые из них */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #define _XOPEN_SOURCE 600 #include int main (void) { pthread_t ct_id; /* Идентификатор текущего потока управления */ pthread_attr_t patob; /* Атрибутный объект для создания потоков управления */ int res; /* Переменная для запоминания результатов "потоковых" функций */ void *stackaddr; /* Начало стека как атрибут потока управления */ size_t atrsize; /* Размеры как атрибуты потока управления */ /* Структура с параметрами планирования */ struct sched_param shdprm; char *spname; /* Названия политики планирования, области */ /* планирования конкуренции и т.п. */ printf ("Идентификатор текущего потока управления: %lx\n", (ct_id = pthread_self ())); if ((errno = pthread_attr_init (&patob)) != 0) { perror ("PTHREAD_ATTR_INIT"); return (errno); } printf ("Значения, установленные системой " "в атрибутном объекте\n"); if ((errno = pthread_attr_getstack (&patob, &stackaddr, &atrsize)) != 0) { perror ("PTHREAD_ATTR_GETSTACK"); return (errno); } printf ("Адрес начала стека: %p\n", stackaddr); printf ("Размер стека: %d\n", atrsize); assert (pthread_attr_getguardsize (&patob, &atrsize) == 0); printf ("Размер защитной области: %d\n", atrsize); assert (pthread_attr_getschedparam (&patob, &shdprm) == 0); assert (pthread_attr_getschedpolicy (&patob, &res) == 0); switch (res) { case SCHED_FIFO: spname = "Планирование по очереди"; break; case SCHED_RR: spname = "Циклическое планирование"; break; case SCHED_OTHER: spname = "Прочее планирование"; break; default: spname = "Неизвестная политика планирования"; } printf ("Политика планирования: %s\n", spname); printf ("Приоритет планирования: %d\n", shdprm.sched_priority); assert (pthread_attr_getscope (&patob, &res) == 0); switch (res) { case PTHREAD_SCOPE_SYSTEM: spname = "Система"; break; case PTHREAD_SCOPE_PROCESS: spname = "Процесс"; break; default: spname = "Неизвестная область планирования " "конкуренции"; } printf ("Область планирования конкуренции: %s\n", spname); assert (pthread_attr_getinheritsched (&patob, &res) == 0); switch (res) { case PTHREAD_INHERIT_SCHED: spname = "Наследуются у родительского потока"; break; case PTHREAD_EXPLICIT_SCHED: spname = "Извлекаются из атрибутного объекта"; break; default: spname = "Устанавливаются неизвестным образом"; } printf ("Атрибуты планирования: %s\n", spname); assert (pthread_attr_getdetachstate (&patob, &res) == 0); switch (res) { case PTHREAD_CREATE_JOINABLE: spname = "Присоединяемые"; break; case PTHREAD_CREATE_DETACHED: spname = "Обособленные"; break; default: spname = "Неизвестные"; } printf ("Потоки управления создаются как: %s\n", spname); /* Изменим значения атрибутов планирования и уровня */ /* параллелизма */ shdprm.sched_priority = 1; if ((errno = pthread_setschedparam (ct_id, SCHED_RR, &shdprm)) != 0) { perror ("PTHREAD_SETSCHEDPARAM"); } if ((errno = pthread_setconcurrency (8192)) != 0) { perror ("PTHREAD_SETCONCURRENCY"); } printf ("\nТекущие значения атрибутов потоков управления\n"); assert (pthread_getschedparam (ct_id, &res, &shdprm) == 0); switch (res) { case SCHED_FIFO: spname = "Планирование по очереди"; break; case SCHED_RR: spname = "Циклическое планирование"; break; case SCHED_OTHER: spname = "Прочее планирование"; break; default: spname = "Неизвестная политика планирования"; } printf ("Политика планирования: %s\n", spname); printf ("Приоритет планирования: %d\n", shdprm.sched_priority); printf ("Уровень параллелизма: %d\n", pthread_getconcurrency()); return 0; } |
| Листинг 1.16. Пример программы, опрашивающей и изменяющей значения атрибутов потоков управления. |
| Закрыть окно |
|
Идентификатор текущего потока управления: 400 Значения, установленные системой в атрибутном объекте Адрес начала стека: 0xffe01000 Размер стека: 2093056 Размер защитной области: 4096 Политика планирования: Прочее планирование Приоритет планирования: 0 Область планирования конкуренции: Система Атрибуты планирования: Извлекаются из атрибутного объекта Потоки управления создаются как: Присоединяемые Текущие значения атрибутов потоков управления Политика планирования: Циклическое планирование Приоритет планирования: 1 Уровень параллелизма: 8192 |
| Листинг 1.17. Возможные результаты работы программы, опрашивающей и изменяющей значения атрибутов потоков управления. |
| Закрыть окно |
|
Идентификатор текущего потока управления: f31ae0 Значения, установленные системой в атрибутном объекте Политика планирования: Планирование по очереди Приоритет планирования: 100 Область планирования конкуренции: Процесс Атрибуты планирования: Извлекаются из атрибутного объекта Потоки управления создаются как: Присоединяемые Текущие значения атрибутов потоков управления Политика планирования: Циклическое планирование Приоритет планирования: 1 |
| Листинг 1.18. Возможные результаты работы программы, опрашивающей и изменяющей значения атрибутов потоков управления, для операционной системы реального времени oc2000. |
| Закрыть окно |
|
#include pthread_once_t once_control = PTHREAD_ONCE_INIT; int pthread_once ( pthread_once_t *once_control_ptr, void (*init_routine) (void)); |
| Листинг 1.19. Описание функции pthread_once(). |
| Закрыть окно |
|
#include int pthread_key_create ( pthread_key_t *key_ptr, void (*destructor) (void *)); int pthread_key_delete ( pthread_key_t key); |
| Листинг 1.20. Описание функций pthread_key_create() и pthread_key_delete(). |
| Закрыть окно |
|
#include void *pthread_getspecific ( pthread_key_t key); int pthread_setspecific ( pthread_key_t key, const void *value); |
| Листинг 1.21. Описание функций pthread_getspecific() и pthread_setspecific(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа запоминает в качестве индивидуальных данных */ /* потока управления время начала активных операций */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Деструктор индивидуальных данных, в роли которых */ /* выступает указатель на структуру типа timeval. */ /* Поскольку она не содержит указателей, достаточно */ /* освободить занимаемую ею память */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void data_destructor (void *p) { free (p); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция создания ключа индивидуальных данных, */ /* ассоциирующая с ним деструктор, освобождающий память */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void create_data_key (void) { (void) pthread_key_create (&data_key, data_destructor); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция инициализации индивидуальных данных. */ /* Запрашивает астрономическое время */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_func (void) { struct timeval *tmvl_ptr; /* Запомним астрономическое время начала операций */ /* потока управления */ if ((tmvl_ptr = (struct timeval *) malloc (sizeof (struct timeval))) == NULL) { return (NULL); } (void) gettimeofday (tmvl_ptr, NULL); /* Создадим ключ индивидуальных данных, перепоручив */ /* вызов pthread_key_create() функции pthread_once() */ (void) pthread_once (&key_once, create_data_key); (void) pthread_setspecific (data_key, tmvl_ptr); return (tmvl_ptr); } /* * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() вызывает функцию инициализации */ /* и запрашивает индивидуальные данные потока */ /* управления */ /* * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { struct timeval *tmvl_ptr; if (start_func () == NULL) { return (1); } if ((tmvl_ptr = (struct timeval *) pthread_getspecific (data_key)) != NULL) { printf ("Время начала операций потока управления: " "%ld сек, %ld мсек\n", tmvl_ptr->tv_sec, tmvl_ptr->tv_usec); } else { printf ("Отсутствуют индивидуальные данные потока " "управления.\n"); printf ("Время начала операций неизвестно\n"); return (2); } return 0; } |
| Листинг 1.22. Пример программы, формирующей и опрашивающей индивидуальные данные потоков управления. |
| Закрыть окно |
| Время начала операций потока управления: 1075707670 сек, 584737 мсек |
| Листинг 1.23. Возможные результаты работы программы, формирующей и опрашивающей индивидуальные данные потоков управления. |
| Закрыть окно |
|
#include |
| Листинг 1.24. Описание функции pthread_create(). |
| Закрыть окно |
|
#include |
| Листинг 1.25. Описание функции pthread_atfork(). |
| Закрыть окно |
|
#include |
| Листинг 1.26. Описание функции pthread_exit(). |
| Закрыть окно |
|
#include |
| Листинг 1.27. Описание функции pthread_join(). |
| Закрыть окно |
|
#include void pthread_cleanup_push ( void (*routine) (void *), void *arg); void pthread_cleanup_pop (int execute); |
| Листинг 1.28. Описание функций pthread_cleanup_push() и pthread_cleanup_pop(). |
| Закрыть окно |
|
#define pthread_cleanup_push (rtn, arg) { \ struct _pthread_handler_rec \ __cleanup_handler, \ **__head; \ __cleanup_handler.rtn = rtn; \ __cleanup_handler.arg = arg; \ (void) pthread_getspecific \ (_pthread_handler_key, &__head); \ __cleanup_handler.next = *__head; \ *__head = &__cleanup_handler; #define pthread_cleanup_pop (ex) \ *__head = __cleanup_handler.next; \ if (ex) (*__cleanup_handler.rtn) \ (__cleanup_handler.arg); \ } |
| Листинг 1.29. Возможная реализация функций pthread_cleanup_push() и pthread_cleanup_pop() как макросов. |
| Закрыть окно |
|
#include |
| Листинг 1.30. Описание функции pthread_cancel(). |
| Закрыть окно |
|
#include int pthread_setcancelstate ( int state, int *oldstate); int pthread_setcanceltype ( int type, int *oldtype); void pthread_testcancel (void); |
| Листинг 1.31. Описание функций pthread_setcancelstate(), pthread_setcanceltype(), pthread_testcancel(). |
| Закрыть окно |
|
#include |
| Листинг 1.32. Описание функции pthread_kill(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * */ /* Программа демонстрирует генерацию */ /* и доставку сигналов */ /* потокам управления */ /* * * * * * * * * * * * * * * * * */ #include /* * * * * * * * * * * * * * / /* Функция обработки сигнала */ /* * * * * * * * * * * * * * / static void signal_handler (int dummy) { printf ("Идентификатор потока, обрабатывающего сигнал: %lx\n", pthread_self ()); } /* * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока управления, */ /* которому будет направлен сигнал */ /* * * * * * * * * * * * * * * * * * * */ static void *thread_start (void *dummy) { printf ("Идентификатор нового потока управления: %lx\n", pthread_self ()); while (1) { sleep (1); } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() задает способ обработки сигнала SIGINT, */ /* создает поток управления и посылает ему сигнал */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t thread_id; struct sigaction act; /* Установим реакцию на сигнал SIGINT */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL); if ((errno = pthread_create (&thread_id, NULL, thread_start, NULL)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } printf ("Идентификатор созданного потока управления: %lx\n", thread_id); (void) pthread_kill (thread_id, SIGINT); printf ("После вызова pthread_kill()\n"); sleep (1); printf ("Выспались...\n"); return (0); } |
| Листинг 1.33. Пример использования механизма сигналов в многопотоковой программе. |
| Закрыть окно |
| Идентификатор созданного потока управления: 402 После вызова pthread_kill() Идентификатор потока, обрабатывающего сигнал: 402 Идентификатор нового потока управления: 402 Выспались... |
| Листинг 1.34. Возможные результаты работы многопотоковой программы, использующей механизм сигналов. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа демонстрирует взаимодействие сигналов */ /* и ожидания завершения потока управления */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include /* * * * * * * * * * * * * * */ /* Функция обработки сигнала */ /* * * * * * * * * * * * * * */ static void signal_handler (int dummy) { printf ("Идентификатор потока, обрабатывающего сигнал: %lx\n", pthread_self ()); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция создаваемого потока управления */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void *thread_start (void *thread_id) { printf ("Идентификатор нового потока управления: %lx\n", pthread_self ()); (void) pthread_kill ((pthread_t) thread_id, SIGINT); return ((void *) pthread_self ()); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() задает способ обработки сигнала SIGINT, */ /* создает поток управления и ожидает его завершения */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t thread_id; struct sigaction act; void *pv; /* Установим реакцию на сигнал SIGINT */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL); if ((errno = pthread_create (&thread_id, NULL, thread_start, (void *) pthread_self ())) != 0) { perror ("PTHREAD_CREATE"); return (errno); } printf ("Идентификаторы начального и созданного потоков " "управления: " "%lx %lx\n", pthread_self (), thread_id); /* Дождемся завершения созданного потока управления */ if ((errno = pthread_join (thread_id, &pv)) != 0) { perror ("PTHREAD_JOIN"); return (errno); } printf ("Статус завершения созданного потока " "управления: %p\n", pv); return (0); } |
| Листинг 1.35. Пример программы, обрабатывающей сигнал во время ожидания завершения потока управления. |
| Закрыть окно |
| Идентификаторы начального и созданного потоков управления: 400 402 Идентификатор нового потока управления: 402 Идентификатор потока, обрабатывающего сигнал: 400 Статус завершения созданного потока управления: 0x402 |
| Листинг 1.36. Возможные результаты работы программы, обрабатывающей сигнал во время ожидания завершения потока управления. |
| Закрыть окно |
|
#include |
| Листинг 1.37. Описание функции pthread_detach(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа процесса (будем называть его серверным), */ /* принимающего запросы на установления соединения и */ /* запускающего потоки управления для их обслуживания */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая (и единственная) функция потоков управления, */ /* обслуживающих запросы на копирование строк, */ /* поступающих из сокета */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *srv_thread_start (void *ad) { FILE *fpad; /* Поток данных, соответствующий */ /* дескриптору ad */ char line [LINE_MAX]; /* Буфер для принимаемых строк */ /* Структура для записи адреса */ struct sockaddr_in sai; /* Длина адреса */ socklen_t sai_len = sizeof (struct sockaddr_in); /* Опросим адрес партнера по общению (передающего сокета) */ if (getpeername ((int) ad, (struct sockaddr *) &sai, &sai_len) < 0) { perror ("GETPEERNAME"); return (NULL); } /* По файловому дескриптору ad сформируем */ /* буферизованный поток данных */ if ((fpad = fdopen ((int) ad, "r")) == NULL) { perror ("FDOPEN"); return (NULL); } /* Цикл чтения строк из сокета */ /* и выдачи их на стандартный вывод */ while (fgets (line, sizeof (line), fpad) != NULL) { printf ("Вы ввели и отправили с адреса %s, " "порт %d :", inet_ntoa (sai.sin_addr), ntohs (sai.sin_port)); fputs (line, stdout); } /* Закрытие соединения */ shutdown ((int) ad, SHUT_RD); (void) fclose (fpad); return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* В функции main() принимаются запросы на установление */ /* соединения и запускаются потоки управления для их обслуживания */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int sd; /* Дескриптор слушающего сокета */ int ad; /* Дескриптор приемного сокета */ /* Буфер для принимаемых строк */ struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL, NULL}; /* Указатель – выходной аргумент getaddrinfo */ struct addrinfo *addr_res; int res; /* Результат getaddrinfo */ pthread_attr_t patob; /* Атрибутный объект для создания */ /* потоков управления */ pthread_t adt_id; /* Идентификатор обслуживающего потока управления */ /* Создадим слушающий сокет */ if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror ("SOCKET"); return (1); } /* Привяжем этот сокет к адресу сервиса spooler */ /* на локальном хосте */ if ((res = getaddrinfo (NULL, "spooler", &hints, &addr_res)) != 0) { fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res)); return (2); } if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) { perror ("BIND"); return (3); } /* Можно освободить память, которую запрашивала */ /* функция getaddrinfo() */ freeaddrinfo (addr_res); /* Пометим сокет как слушающий */ if (listen (sd, SOMAXCONN) < 0) { perror ("LISTEN"); return (4); } /* Инициализируем атрибутный объект потоков управления */ if ((errno = pthread_attr_init (&patob)) != 0) { perror ("PTHREAD_ATTR_INIT"); return (errno); } /* Потоки управления будем создавать обособленными */ (void) pthread_attr_setdetachstate (&patob, PTHREAD_CREATE_DETACHED); /* Цикл приема соединений и запуска */ /* обслуживающих потоков управления */ while (1) { /* Примем соединение. */ /* Адрес партнера по общению нас */ /* в данном случае не интересует */ if ((ad = accept (sd, NULL, NULL)) < 0) { perror ("ACCEPT"); return (6); } /* Запустим обслуживающий поток управления */ if ((errno = pthread_create (&adt_id, &patob, srv_thread_start,(void *) ad)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } } return (0); } |
| Листинг 1.38. Пример многопотоковой программы, обслуживающей запросы на копирование строк, поступающих через сокеты. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа вызывает функции обработки в рамках */ /* порождаемых потоков управления и контролирует время */ /* их выполнения с помощью интервального таймера */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include /* Период интервального таймера (в секундах) */ #define IT_PERIOD 1 static pthread_t cthread_id; /* Идентификатор текущего */ /* потока управления, */ /* обрабатывающего данные */ static int in_proc_data = 0; /* Признак активности */ /* потока обработки данных */ static double s; /* Результат функций */ /* обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция обработки срабатывания таймера реального */ /* времени (сигнал SIGALRM) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_sigalrm (int dummy) { if (in_proc_data) { /* Не имеет значения, какой поток обрабатывает сигнал */ /* и заказывает терминирование (быть может, себя) */ (void) pthread_cancel (cthread_id); } } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Обработчик завершения потока управления. */ /* Сбрасывает признак активности потока обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_data_cleanup_handler (void *arg) { in_proc_data = (int) arg; } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока управления, обрабатывающего */ /* данные. Аргумент – указатель на функцию обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_func (void *proc_data_func) { /* Поместим в стек обработчик завершения */ pthread_cleanup_push (proc_data_cleanup_handler, 0); in_proc_data = 1; /* Время пошло ... */ /* На время выполнения функции обработки данных установим */ /* асинхронный тип терминирования, иначе оно не сработает */ (void) pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); /* Выполним функцию обработки данных */ ((void (*) (void)) (proc_data_func)) (); /* Установим отложенный тип терминирования, */ /* иначе изъятие обработчика из стека */ /* будет небезопасным действием */ (void) pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, NULL); /* Выполним обработчик завершения и удалим его из стека */ pthread_cleanup_pop (1); return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Первая функция обработки данных (вычисляет ln (2)) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_data_1 (void) { double d = 1; int i; s = 0; for (i = 1; i <= 100000000; i++) { s += d / i; d = -d; } } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Вторая функция обработки данных (вычисляет sqrt (2))*/ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_data_2 (void) { s = 1; do { s = (s + 2 / s) * 0.5; } while ((s * s – 2) > 0.000000001); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() задает способ обработки сигнала SIGALRM, */ /* взводит периодический таймер реального времени */ /* и запускает в цикле потоки обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { /* Массив указателей на функции обработки данных */ void (*fptrs []) (void) = {proc_data_1, proc_data_2, NULL}; /* Указатель на указатель на */ /* текущую функцию обработки данных */ void (**tfptr) (void); void *pstat; /* Статус завершения потока */ /* обработки данных */ struct itimerval itvl; struct sigaction sact; int i; /* Установим реакцию на сигнал SIGALRM */ sact.sa_handler = proc_sigalrm; sact.sa_flags = 0; (void) sigemptyset (&sact.sa_mask); if (sigaction (SIGALRM, &sact, NULL) < 0) { perror ("SIGACTION"); return (1); } /* Сделаем таймер реального времени периодическим */ itvl.it_interval.tv_sec = IT_PERIOD; itvl.it_interval.tv_usec = 0; /* Цикл запуска потоков обработки данных. */ /* Выполним его дважды */ for (i = 0; i < 2; i++) { for (tfptr = fptrs; *tfptr != NULL; tfptr++) { /* Взведем интервальный таймер реального времени */ itvl.it_value.tv_sec = IT_PERIOD; itvl.it_value.tv_usec = 0; if (setitimer (ITIMER_REAL, &itvl, NULL) < 0) { perror ("SETITIMER"); return (2); } /* Создадим поток обработки данных, */ /* затем дождемся его завершения */ if ((errno = pthread_create (&cthread_id, NULL, start_func, (void *) *tfptr)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } if ((errno = pthread_join (cthread_id, &pstat)) != 0) { perror ("PTHREAD_JOIN"); return (errno); } if (pstat == PTHREAD_CANCELED) { printf ("Частичный результат функции " "обработки данных: %g\n", s); } else { printf ("Полный результат функции " "обработки данных: %g\n", s); } } } return 0; } |
| Листинг 1.39. Пример многопотоковой программы, осуществляющей обработку данных с контролем времени. |
| Закрыть окно |
| Частичный результат функции обработки данных: 0.693147 Полный результат функции обработки данных: 1.41421 Частичный результат функции обработки данных: 0.693147 Полный результат функции обработки данных: 1.41421 |
| Листинг 1.40. Возможные результаты работы многопотоковой программы, осуществляющей обработку данных с контролем времени. |
| Закрыть окно |
|
#include #define N 10000 int main (void) { int i; for (i = 0; i < N; i++) { switch (fork ()) { case -1: perror ("FORK"); return (1); case 0: /* Порожденный процесс */ (void) execl ("./dummy", "dummy", (char *) 0); exit (0); default: /* Родительский процесс */ (void) wait (NULL); } } return 0; } |
| Листинг 1.41. Пример программы, порождающей в цикле практически пустые процессы. |
| Закрыть окно |
| int main (void) { return 0; } |
| Листинг 1.42. Содержимое файла dummy.c |
| Закрыть окно |
| real 34.97 user 12.36 sys 22.61 |
| Листинг 1.43. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы (вариант с вызовом execl()). |
| Закрыть окно |
| real 11.49 user 2.38 sys 9.11 |
| Листинг 1.44. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы (вариант без вызова execl()). |
| Закрыть окно |
|
#include #define N 10000 static void *thread_start (void *arg) { pthread_exit (arg); } int main (void) { pthread_t thread_id; int i; for (i = 0; i < N; i++) { if ((errno = pthread_create ( &thread_id, NULL, thread_start, NULL)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } if ((errno = pthread_join ( thread_id, NULL)) != 0) { perror ("PTHREAD_JOIN"); return (errno); } } return (0); } |
| Листинг 1.45. Пример программы, порождающей в цикле потоки управления. |
| Закрыть окно |
| real 2.08 user 0.52 sys 1.56 |
| Листинг 1.46. Возможные результаты измерения времени работы программы, порождающей в цикле потоки управления. |
| Закрыть окно |
Работа с индивидуальными данными потоков управления
Все потоки управления одного процесса разделяют общее адресное пространство и, следовательно, имеют общие данные. Чтобы сделать некоторые данные индивидуальными для потока, нужно принять специальные меры: с помощью функции pthread_key_create() создать ключ и ассоциировать с ним индивидуальные данные, воспользовавшись функцией pthread_setspecific(). В дальнейшем эти данные можно извлекать посредством функции pthread_getspecific(). Подчеркнем, что при обращении по одному (разделяемому) ключу разные потоки будут получать доступ к разным данным.
Создать один ключ, очевидно, нужно один раз. В ситуации, когда несколько потоков управления выполняют одну программу, это сопряжено с определенными проблемами. Стандартный прием, заключающийся во введении статической переменной, хранящей признак инициализированности (в данном случае – признак того, что ключ уже создан) в многопотоковой среде не работает, поскольку проверка и последующее изменение значения подобной переменной не являются атомарным действием. В принципе, манипуляции со статической переменной можно защитить каким-либо средством синхронизации, но его тоже нужно инициализировать!
Для решения проблемы однократного выполнения инициализирующих действий в многопотоковой среде стандарт POSIX-2001 предлагает функцию pthread_once() (см. листинг 1.19).
#include
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once ( pthread_once_t *once_control_ptr, void (*init_routine) (void));
Листинг 1.19. Описание функции pthread_once(). (html, txt)
При первом и только при первом обращении к функции pthread_once() с фиксированным значением аргумента once_control_ptr, вне зависимости от того, какой из потоков процесса его выполняет, будет вызвана функция (*init_routine) (), которая по идее должна осуществлять инициализирующие действия, такие как создание ключа индивидуальных данных потоков управления.
Переменная, на которую указывает аргумент once_control_ptr, должна иметь начальное значение PTHREAD_ONCE_INIT и не должна быть автоматической.
Все потоки управления одного процесса разделяют общее адресное пространство и, следовательно, имеют общие данные. Чтобы сделать некоторые данные индивидуальными для потока, нужно принять специальные меры: с помощью функции pthread_key_create() создать ключ и ассоциировать с ним индивидуальные данные, воспользовавшись функцией pthread_setspecific(). В дальнейшем эти данные можно извлекать посредством функции pthread_getspecific(). Подчеркнем, что при обращении по одному (разделяемому) ключу разные потоки будут получать доступ к разным данным.
Создать один ключ, очевидно, нужно один раз. В ситуации, когда несколько потоков управления выполняют одну программу, это сопряжено с определенными проблемами. Стандартный прием, заключающийся во введении статической переменной, хранящей признак инициализированности (в данном случае – признак того, что ключ уже создан) в многопотоковой среде не работает, поскольку проверка и последующее изменение значения подобной переменной не являются атомарным действием. В принципе, манипуляции со статической переменной можно защитить каким-либо средством синхронизации, но его тоже нужно инициализировать!
Для решения проблемы однократного выполнения инициализирующих действий в многопотоковой среде стандарт POSIX-2001 предлагает функцию pthread_once() (см. листинг 1.19).
#include
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once ( pthread_once_t *once_control_ptr, void (*init_routine) (void));
Листинг 1.19. Описание функции pthread_once().
При первом и только при первом обращении к функции pthread_once() с фиксированным значением аргумента once_control_ptr, вне зависимости от того, какой из потоков процесса его выполняет, будет вызвана функция (*init_routine) (), которая по идее должна осуществлять инициализирующие действия, такие как создание ключа индивидуальных данных потоков управления.
Переменная, на которую указывает аргумент once_control_ptr, должна иметь начальное значение PTHREAD_ONCE_INIT и не должна быть автоматической.
За создание и удаление ключа индивидуальных данных потоков управления, согласно стандарту POSIX-2001, отвечают функции pthread_key_create() и pthread_key_delete() (см. листинг 1.20).
#include
int pthread_key_create ( pthread_key_t *key_ptr, void (*destructor) (void *));
int pthread_key_delete ( pthread_key_t key);
Листинг 1.20. Описание функций pthread_key_create() и pthread_key_delete(). (html, txt)
Функция pthread_key_create() создает новый ключ, которым могут воспользоваться все входящие в процесс потоки управления, и, в соответствии со "штабной дисциплиной", помещает его по указателю key_ptr. Сразу после создания ключа для всех потоков в качестве индивидуальных данных с ним ассоциируется значение NULL. (Аналогично, после создания потока управления он не имеет индивидуальных данных, поэтому со всеми доступными ему ключами также ассоциированы пустые указатели.)
С ключом может быть ассоциирован деструктор, который вызывается при завершении потока управления с индивидуальными данными в качестве аргумента. Обычно в роли индивидуальных данных выступает указатель на динамически зарезервированную потоком область памяти, которую деструктор должен освободить.
Функция pthread_key_delete() удаляет заданный ключ, не заботясь о том, пусты ли ассоциированные значения, и не вызывая каких-либо деструкторов (напротив, ее обычно вызывают из деструктора). Вся ответственность по освобождению памяти и выполнению других необходимых зачисток возлагается на приложение.
Для выборки и изменения ассоциированных с ключом key индивидуальных данных вызывающего потока управления предназначены функции pthread_getspecific() и pthread_setspecific() (см. листинг 1.21).
#include
void *pthread_getspecific ( pthread_key_t key);
int pthread_setspecific ( pthread_key_t key, const void *value);
Листинг 1.21. Описание функций pthread_getspecific() и pthread_setspecific(). (html, txt)
Функция pthread_getspecific() возвращает индивидуальные данные в качестве результата; функция pthread_setspecific() ассоциирует с ключом key значение аргумента value.
На листинге 1.22 показана программа, использующая стандартную схему создания ключа и манипулирования индивидуальными данными потоков управления.
Листинг 1.22. Пример программы, формирующей и опрашивающей индивидуальные данные потоков управления. (html, txt)
Результат работы этой программы может выглядеть так, как показано на листинге 1.23.
Время начала операций потока управления: 1075707670 сек, 584737 мсек
Листинг 1.23. Возможные результаты работы программы, формирующей и опрашивающей индивидуальные данные потоков управления. (html, txt)
За создание и удаление ключа индивидуальных данных потоков управления, согласно стандарту POSIX-2001, отвечают функции pthread_key_create() и pthread_key_delete() (см. листинг 1.20).
#include
int pthread_key_create ( pthread_key_t *key_ptr, void (*destructor) (void *));
int pthread_key_delete ( pthread_key_t key);
Листинг 1.20. Описание функций pthread_key_create() и pthread_key_delete().
Функция pthread_key_create() создает новый ключ, которым могут воспользоваться все входящие в процесс потоки управления, и, в соответствии со "штабной дисциплиной", помещает его по указателю key_ptr. Сразу после создания ключа для всех потоков в качестве индивидуальных данных с ним ассоциируется значение NULL. (Аналогично, после создания потока управления он не имеет индивидуальных данных, поэтому со всеми доступными ему ключами также ассоциированы пустые указатели.)
С ключом может быть ассоциирован деструктор, который вызывается при завершении потока управления с индивидуальными данными в качестве аргумента. Обычно в роли индивидуальных данных выступает указатель на динамически зарезервированную потоком область памяти, которую деструктор должен освободить.
Функция pthread_key_delete() удаляет заданный ключ, не заботясь о том, пусты ли ассоциированные значения, и не вызывая каких-либо деструкторов (напротив, ее обычно вызывают из деструктора). Вся ответственность по освобождению памяти и выполнению других необходимых зачисток возлагается на приложение.
Для выборки и изменения ассоциированных с ключом key индивидуальных данных вызывающего потока управления предназначены функции pthread_getspecific() и pthread_setspecific() (см. листинг 1.21).
#include
void *pthread_getspecific ( pthread_key_t key);
int pthread_setspecific ( pthread_key_t key, const void *value);
Листинг 1.21. Описание функций pthread_getspecific() и pthread_setspecific().
Функция pthread_getspecific() возвращает индивидуальные данные в качестве результата; функция pthread_setspecific() ассоциирует с ключом key значение аргумента value.
На листинге 1. 22 показана программа, использующая стандартную схему создания ключа и манипулирования индивидуальными данными потоков управления.
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа запоминает в качестве индивидуальных данных */ /* потока управления время начала активных операций */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */
#include
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Деструктор индивидуальных данных, в роли которых */ /* выступает указатель на структуру типа timeval. */ /* Поскольку она не содержит указателей, достаточно */ /* освободить занимаемую ею память */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void data_destructor (void *p) { free (p); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция создания ключа индивидуальных данных, */ /* ассоциирующая с ним деструктор, освобождающий память */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void create_data_key (void) { (void) pthread_key_create (&data_key, data_destructor); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция инициализации индивидуальных данных. */ /* Запрашивает астрономическое время */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_func (void) { struct timeval *tmvl_ptr;
/* Запомним астрономическое время начала операций */ /* потока управления */ if ((tmvl_ptr = (struct timeval *) malloc (sizeof (struct timeval))) == NULL) { return (NULL); } (void) gettimeofday (tmvl_ptr, NULL);
/* Создадим ключ индивидуальных данных, перепоручив */ /* вызов pthread_key_create() функции pthread_once() */ (void) pthread_once (&key_once, create_data_key);
(void) pthread_setspecific (data_key, tmvl_ptr); return (tmvl_ptr); }
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() вызывает функцию инициализации */ /* и запрашивает индивидуальные данные потока */ /* управления */ /* * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { struct timeval *tmvl_ptr;
if (start_func () == NULL) { return (1); }
if ((tmvl_ptr = (struct timeval *) pthread_getspecific (data_key)) != NULL) { printf (" Время начала операций потока управления: " "%ld сек, %ld мсек\n", tmvl_ptr->tv_sec, tmvl_ptr->tv_usec); } else { printf ("Отсутствуют индивидуальные данные потока " "управления.\n"); printf ("Время начала операций неизвестно\n"); return (2); }
return 0; }
Листинг 1.22. Пример программы, формирующей и опрашивающей индивидуальные данные потоков управления.
Результат работы этой программы может выглядеть так, как показано на листинге 1.23.
Время начала операций потока управления: 1075707670 сек, 584737 мсек
Листинг 1.23. Возможные результаты работы программы, формирующей и опрашивающей индивидуальные данные потоков управления.
Создание и терминирование потоков управления
Для создания нового потока управления служит функция pthread_create() (см. листинг 1.24).
#include
Листинг 1.24. Описание функции pthread_create(). (html, txt)
Выполнение созданного потока управления начнется с вызова (*start_routine) (arg); возврат из этой функции приведет к терминированию потока с возвращаемым значением в качестве статуса завершения. Из этого правила стандартом предусмотрено одно, вполне естественное исключение: для потока, выполнение которого началось с функции main(), возврат из нее означает завершение процесса, содержащего поток, со всеми вытекающими отсюда последствиями.
Аргумент attr задает атрибуты нового потока; если значение attr равно NULL, используются зависящие от реализации подразумеваемые атрибуты.
От "родительского" вновь созданный поток управления наследует немногое: маску сигналов и характеристики вещественной арифметики.
К числу средств создания потоков можно отнести и функцию fork(). Правда, здесь нас будет интересовать не она сама, а ассоциированные с ней обработчики, зарегистрированные с помощью функции pthread_atfork() (см. листинг 1.25).
#include
Листинг 1.25. Описание функции pthread_atfork(). (html, txt)
В каждом обращении к pthread_atfork() фигурируют три обработчика (если, конечно, в качестве значения аргумента не задан пустой указатель). Первый ((*prepare)()) выполняется в контексте потока, вызвавшего fork(), до разветвления процесса; второй ((*parent)()) – в том же контексте, но после разветвления; третий ((*child)()) – в контексте единственного потока порожденного процесса.
С помощью pthread_atfork() можно зарегистрировать несколько троек обработчиков. Первые элементы троек вызываются в порядке, обратном по отношению к регистрации; вторые и третьи выполняются в прямом порядке.
Для создания нового потока управления служит функция pthread_create() (см. листинг 1.24).
#include
Листинг 1.24. Описание функции pthread_create().
Выполнение созданного потока управления начнется с вызова (*start_routine) (arg); возврат из этой функции приведет к терминированию потока с возвращаемым значением в качестве статуса завершения. Из этого правила стандартом предусмотрено одно, вполне естественное исключение: для потока, выполнение которого началось с функции main(), возврат из нее означает завершение процесса, содержащего поток, со всеми вытекающими отсюда последствиями.
Аргумент attr задает атрибуты нового потока; если значение attr равно NULL, используются зависящие от реализации подразумеваемые атрибуты.
От "родительского" вновь созданный поток управления наследует немногое: маску сигналов и характеристики вещественной арифметики.
К числу средств создания потоков можно отнести и функцию fork(). Правда, здесь нас будет интересовать не она сама, а ассоциированные с ней обработчики, зарегистрированные с помощью функции pthread_atfork() (см. листинг 1.25).
#include
Листинг 1.25. Описание функции pthread_atfork().
В каждом обращении к pthread_atfork() фигурируют три обработчика (если, конечно, в качестве значения аргумента не задан пустой указатель). Первый ((*prepare)()) выполняется в контексте потока, вызвавшего fork(), до разветвления процесса; второй ((*parent)()) – в том же контексте, но после разветвления; третий ((*child)()) – в контексте единственного потока порожденного процесса.
С помощью pthread_atfork() можно зарегистрировать несколько троек обработчиков. Первые элементы троек вызываются в порядке, обратном по отношению к регистрации; вторые и третьи выполняются в прямом порядке.
Как и процесс, поток управления можно терминировать изнутри и извне. С одним, неявным, но наиболее естественным способом "самоликвидации" – выходом из стартовой функции потока – мы уже познакомились. Тот же эффект достигается вызовом функции pthread_exit() (см. листинг 1.26).
#include
Листинг 1.26. Описание функции pthread_exit().
Терминирование потока управления в идейном плане существенно сложнее создания. Чтобы осветить все тонкости, необходимо ввести несколько новых понятий и описать целый ряд функций. Пока мы укажем лишь, что терминирование последнего потока в процессе вызывает его (процесса) завершение с нулевым кодом.
Из общих соображений (например, если исходить из аналогии между процессами и потоками управления) очевидно, что должна существовать возможность дождаться завершения заданного потока управления. Эта возможность реализуется функцией pthread_join() (см. листинг 1.27), напоминающей waitpid().
#include
Листинг 1.27. Описание функции pthread_join().
Поток управления, вызвавший функцию pthread_join(), приостанавливает выполнение до завершения потока, идентификатор которого задан аргументом thread. При успешном возврате из pthread_join() результат, как и положено, равен нулю, а по указателю value_ptr_ptr (если он не пуст) помещается значение (указатель value_ptr), переданное в качестве аргумента функции pthread_exit(). Тем самым ждуший поток получает данные о статусе завершения ожидаемого.
Отметим, что трактовка значения value_ptr возлагается на приложение. Например, оно может считать его целым числом, а не указателем; по этой причине операционная система при выполнении функции pthread_exit() не вправе выдавать ошибку типа "неверный адрес" каким бы ни был аргумент value_ptr. Если он все же является указателем, то ему нельзя присваивать адрес автоматической переменной, поскольку к моменту его использования ожидаемый поток уже завершится и состояние его автоматических переменных станет неопределенным.
Второе общее соображение касается того обстоятельства, что вызов такой функции, как ожидание завершения (pthread_join()) способен приостановить выполнение вызывающего потока управления на неопределенное время, в течение которого ему может быть доставлен обрабатываемый сигнал. Как правило, в подобных ситуациях выполнение функций (таких, например, как read()) завершается с частично достигнутым результатом (например, с числом прочитанных байт, меньшим запрошенного) и кодом ошибки EINTR, нуждающимся в нестандартной обработке, далеко не всегда реализуемой разработчиками приложений. Из-за этого в программах появляются дефекты, которые трудно воспроизвести и, соответственно, исправить.
Согласно стандарту POSIX-2001, функции, обслуживающие потоки управления, свободны от этого недостатка. Они никогда не завершаются с частичным результатом и не выдают код ошибки EINTR. Восстановление нормального состояния после того, как ожидание было прервано доставкой и обработкой сигнала, возлагается на операционную систему, а не на приложение.
Третье общее соображение состоит в том, что такое критически важное событие, как завершение потока управления, не может оставаться без функций-обработчиков. Стандартом POSIX-2001 предусмотрено существование не одного, а целого стека подобных обработчиков, ассоциированного с потоком управления. Операции над этим стеком возложены на функции pthread_cleanup_push() и pthread_cleanup_pop() (см. листинг 1.28).
#include
void pthread_cleanup_push ( void (*routine) (void *), void *arg);
void pthread_cleanup_pop (int execute);
Листинг 1.28. Описание функций pthread_cleanup_push() и pthread_cleanup_pop().
Функция pthread_cleanup_push() помещает заданный аргументами routine и arg обработчик в стек обработчиков вызывающего потока. Функция pthread_cleanup_pop() извлекает верхний обработчик из этого стека и, если значение аргумента execute отлично от нуля, вызывает его (как (*routine) (arg)).
Разумеется, все обработчики, начиная с верхнего, извлекаются из стека и вызываются при терминировании потока управления (вне зависимости от того, объясняется ли терминирование внутренними или внешними причинами). В частности, это происходит после того, как поток обратится к функции pthread_exit().
Напомним, что обработчики завершения существуют и для процессов (они регистрируются с помощью функции atexit()), однако применительно к потокам управления идея стека обработчиков оформлена в более явном и систематическом виде.
Пару функций pthread_cleanup_push() и pthread_cleanup_pop() можно представлять себе как открывающую и закрывающую скобки, оформленные в виде отдельных инструкций языка C и обрамляющие обслуживаемый обработчиком участок программы. Согласно стандарту POSIX-2001, этот участок должен представлять собой фрагмент одной лексической области видимости (блока), а pthread_cleanup_push() и pthread_cleanup_pop() могут быть реализованы как макросы (см. листинг 1.29).
#define pthread_cleanup_push (rtn, arg) { \ struct _pthread_handler_rec \ __cleanup_handler, \ **__head; \ __cleanup_handler.rtn = rtn; \ __cleanup_handler.arg = arg; \ (void) pthread_getspecific \ (_pthread_handler_key, &__head); \ __cleanup_handler.next = *__head; \ *__head = &__cleanup_handler;
#define pthread_cleanup_pop (ex) \ *__head = __cleanup_handler.next; \ if (ex) (*__cleanup_handler.rtn) \ (__cleanup_handler.arg); \ }
Листинг 1.29. Возможная реализация функций pthread_cleanup_push() и pthread_cleanup_pop() как макросов.
Обратим внимание на то, что в определении макроса pthread_cleanup_push() открывается внутренний блок, в котором декларируются два необходимых объекта – структура __cleanup_handler, описывающая обработчик, и указатель __head на вершину стека, представленного в виде односвязанного (линейного) списка. В определении pthread_cleanup_pop() этот блок закрывается. Так что даже из соображений синтаксической корректности вызовы pthread_cleanup_push() и pthread_cleanup_pop() должны быть парными и располагаться в одном блоке, но более существенной нам представляется корректность семантическая.
Если поток управления по внутренним или внешним причинам терминируется при выполнении обслуживаемого обработчиком участка, тот должен обеспечить аккуратное завершение с восстановлением (если это необходимо) целостного, корректного состояния объектов, видимых в блоке, и освобождением ресурсов, лексически доступных из текущей области видимости. Иными словами, характер обработки завершения определяется программным контекстом, в котором прервано выполнение потока управления. Отсюда и привязка стека обработчиков к блочной структуре программы.
После того, как выполнятся все обработчики завершения, в неспецифицированном порядке вызываются деструкторы индивидуальных данных (если у потока управления таковые имеются). То, что деструкторы вызываются после обработчиков завершения, делает доступными для последних индивидуальные данные потоков управления.
Отметим, что если поток – не последний в процессе, то при его завершении, разумеется, не происходит автоматического освобождения видимых приложению ресурсов процесса (таких, например, как файловые дескрипторы) и не выполняются другие общепроцессные зачистки (такие, как выполнение функций, зарегистрированных посредством atexit()).
Заказать терминирование извне потока управления с заданным идентификатором можно, воспользовавшись функцией pthread_cancel() (см. листинг 1.30).
#include
Листинг 1.30. Описание функции pthread_cancel().
Напомним, что на выполнение "заказа" влияют состояние восприимчивости и тип терминирования, установленные для потока, а также достижение точки терминирования. Эти атрибуты опрашиваются и изменяются с помощью функций pthread_setcancelstate(), pthread_setcanceltype() и pthread_testcancel() (см. листинг 1.31).
#include
int pthread_setcancelstate ( int state, int *oldstate);
int pthread_setcanceltype ( int type, int *oldtype);
void pthread_testcancel (void);
Листинг 1.31. Описание функций pthread_setcancelstate(), pthread_setcanceltype(), pthread_testcancel().
Функции pthread_setcancelstate() и pthread_setcanceltype() атомарным образом, в рамках неделимой транзакции устанавливают новые значения (state и type) для состояния восприимчивости и типа и помещают по заданным указателям (соответственно, oldstate и oldtype) старые значения. Допустимыми значениями для состояния восприимчивости к терминированию являются PTHREAD_CANCEL_ENABLE (терминирование разрешено – подразумеваемое значение для вновь созданных потоков управления) и PTHREAD_CANCEL_DISABLE, для типа – PTHREAD_CANCEL_DEFERRED (отложенное терминирование – подразумеваемое значение) и PTHREAD_CANCEL_ASYNCHRONOUS (немедленное, асинхронное терминирование).
Функция pthread_testcancel() создает в вызывающем потоке точку терминирования, то есть проверяет наличие ждущего заказа на терминирование и, при наличии такового, инициирует его обработку (если она разрешена).
Из общих соображений следует, что манипуляции с атрибутами терминирования необходимы для обеспечения атомарности транзакций и сохранения программных инвариантов. При входе в транзакцию терминирование запрещают, при выходе восстанавливают старое значение. Аналогично, если в пределах критического интервала нарушается некий программный инвариант, на этом интервале поток должен защититься от терминирования. Ресурсы, ассоциированные с потоком, обязаны оставаться в корректном состоянии, а осмысленные действия – или выполняться до конца, или не выполняться вообще.
Возвращаясь к функции pthread_cancel(), отметим, что иногда обработку заказа на терминирование сравнивают с реакцией на доставку сигнала. На наш взгляд, к подобной аналогии нужно относиться осторожно, поскольку она довольно поверхностна и основана в первую очередь на том, что и сигналы, и заказы на терминирование являются средствами асинхронного программного воздействия на потоки управления, и других механизмов подобной направленности стандарт POSIX-2001 не предусматривает. В частности, доставка как сигналов, так и заказов на терминирование способна вывести поток управления из состояния ожидания, быть может, потенциально бесконечного. Еще одна параллель – запрещение терминирования при входе в обработчик завершения и блокирование сигнала при входе в функцию его обработки.
Противоречат отмеченной аналогии следующие обстоятельства. Во-первых, обработка сигналов зачастую направлена на продолжение, а не завершение выполнения и, следовательно, носит принципиально иной, чем у обработчика завершения, характер. Во-вторых, для обработки терминирования предоставлено больше средств (не одна функция, как в случае сигналов, а целый стек обработчиков завершения плюс деструкторы индивидуальных данных) и они строже регламентированы (добавление и изъятие обработчиков стандартизованы как парные операции, выполняемые в пределах одной лексической области видимости, отложенное терминирование производится только в определенных точках программы, нелокальные переходы (siglongjmp()), являющиеся стандартным элементом обработки сигналов, применительно к обработчикам завершения допускаются с многочисленными оговорками и т.п.). Конечно, терминирование извне может быть реализовано на основе механизма сигналов, но есть и другие возможности.
Разумеется, функция pthread_cancel() не ожидает выполнения заказа на терминирование, так что вызывающий и целевой потоки управления продолжают выполняться параллельно, пока последний не осуществит всех специфицированных действий по зачистке и не завершится, выдав ждущим этого события потокам значение PTHREAD_CANCELED.
Помимо заказа на терминирование, потоку управления можно направить и "честный" сигнал, воспользовавшись средством "внутрипроцессного межпотокового" взаимодействия – функцией pthread_kill() (см. листинг 1.32).
#include
Листинг 1.32. Описание функции pthread_kill().
Как и в случае функции kill(), при нулевом значении аргумента sig проверяется корректность заданного идентификатора потока, но никакой сигнал не генерируется.
Напомним (см. курс [1]), что сигналы генерируются для конкретного потока управления (так, в частности, поступает функция pthread_kill()) или для процесса в целом (как это делает функция kill()), но доставляются они всегда одному потоку, только во втором случае его выбор определяется реализацией из соображений простоты доставки: обычно берется активный поток, если он не блокирует данный сигнал. Естественно, если для сигнала определена функция обработки, она выполняется в контексте целевого потока управления. Другие возможные действия (терминирование, остановка) всегда применяются к процессу в целом.
Для иллюстрации изложенного приведем небольшую программу (см. листинг 1.33), в которой создается поток управления с последующей доставкой ему сигнала SIGINT. Возможные результаты работы этой программы показаны на листинге 1.34.
/* * * * * * * * * * * * * * * * * */ /* Программа демонстрирует генерацию */ /* и доставку сигналов */ /* потокам управления */ /* * * * * * * * * * * * * * * * * */
#include
/* * * * * * * * * * * * * * / /* Функция обработки сигнала */ /* * * * * * * * * * * * * * / static void signal_handler (int dummy) { printf ("Идентификатор потока, обрабатывающего сигнал: %lx\n", pthread_self ()); }
/* * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока управления, */ /* которому будет направлен сигнал */ /* * * * * * * * * * * * * * * * * * * */ static void *thread_start (void *dummy) { printf ("Идентификатор нового потока управления: %lx\n", pthread_self ()); while (1) { sleep (1); }
return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() задает способ обработки сигнала SIGINT, */ /* создает поток управления и посылает ему сигнал */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t thread_id; struct sigaction act;
/* Установим реакцию на сигнал SIGINT */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL);
if ((errno = pthread_create (&thread_id, NULL, thread_start, NULL)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } printf ("Идентификатор созданного потока управления: %lx\n", thread_id);
(void) pthread_kill (thread_id, SIGINT); printf ("После вызова pthread_kill()\n");
sleep (1); printf ("Выспались...\n");
return (0); }
Листинг 1.33. Пример использования механизма сигналов в многопотоковой программе.
Идентификатор созданного потока управления: 402 После вызова pthread_kill() Идентификатор потока, обрабатывающего сигнал: 402 Идентификатор нового потока управления: 402 Выспались...
Листинг 1.34. Возможные результаты работы многопотоковой программы, использующей механизм сигналов.
Обратим внимание на два любопытных (хотя и довольно очевидных) момента. Во-первых, начальный поток управления процесса продолжает выполнение после вызова pthread_kill() и успевает сообщить об этом. Затем он на секунду засыпает, активным становится вновь созданный поток и первым делом приступает к обработке доставленного ему сигнала. Уже после возврата из функции обработки начинается выполнение инструкций стартовой функции потока управления.
Читателю предлагается самостоятельно проанализировать, как будет вести себя приведенная программа при подразумеваемом способе обработки сигнала SIGINT.
Отмеченную выше устойчивость ожидания в функциях, обслуживающих потоки управления, к доставке и обработке сигналов проиллюстрируем программой, показанной на листинге 1.35.
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа демонстрирует взаимодействие сигналов */ /* и ожидания завершения потока управления */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */
#include
/* * * * * * * * * * * * * * */ /* Функция обработки сигнала */ /* * * * * * * * * * * * * * */ static void signal_handler (int dummy) { printf ("Идентификатор потока, обрабатывающего сигнал: %lx\n", pthread_self ()); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция создаваемого потока управления */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void *thread_start (void *thread_id) { printf ("Идентификатор нового потока управления: %lx\n", pthread_self ()); (void) pthread_kill ((pthread_t) thread_id, SIGINT);
return ((void *) pthread_self ()); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() задает способ обработки сигнала SIGINT, */ /* создает поток управления и ожидает его завершения */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t thread_id; struct sigaction act; void *pv;
/* Установим реакцию на сигнал SIGINT */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL);
if ((errno = pthread_create (&thread_id, NULL, thread_start, (void *) pthread_self ())) != 0) { perror ("PTHREAD_CREATE"); return (errno); } printf ("Идентификаторы начального и созданного потоков " "управления: " "%lx %lx\n", pthread_self (), thread_id);
/* Дождемся завершения созданного потока управления */ if ((errno = pthread_join (thread_id, &pv)) != 0) { perror ("PTHREAD_JOIN"); return (errno); } printf ("Статус завершения созданного потока " "управления: %p\n", pv);
return (0); }
Листинг 1.35. Пример программы, обрабатывающей сигнал во время ожидания завершения потока управления.
Если посмотреть на возможные результаты работы этой программы (см. листинг 1.36), можно сделать вывод, что, несмотря на получение и обработку сигнала, функция pthread_join отрабатывает с нормальным (нулевым) результатом, получая статус завершения "ожидаемого" потока.
Идентификаторы начального и созданного потоков управления: 400 402 Идентификатор нового потока управления: 402 Идентификатор потока, обрабатывающего сигнал: 400 Статус завершения созданного потока управления: 0x402
Листинг 1.36. Возможные результаты работы программы, обрабатывающей сигнал во время ожидания завершения потока управления.
И здесь читателю рекомендуется самостоятельно выяснить, как поведет себя приведенная программа при подразумеваемой реакции на сигнал SIGINT.
Еще одна полезная операция, связанная с обработкой завершения потока управления, – его динамическое обособление, выполняемое функцией pthread_detach() (см. листинг 1.37).
#include
Листинг 1.37. Описание функции pthread_detach().
При завершении обособленного потока операционная система может освободить использовавшуюся им память.
Может показаться, что возможность динамического обособления является излишней (мол, достаточно соответствующего атрибута, принимаемого во внимание при создании потока функцией pthread_create()), однако это не так. Во-первых, начальный поток процесса создается нестандартным образом и обособить его статически невозможно. Во-вторых, если терминируется поток, ждущий в функции pthread_join(), обработчик его завершения должен обособить того, чье завершение является предметом ожидания, иначе с утилизацией памяти могут возникнуть проблемы.
Если приложение заботится об аккуратном освобождении памяти, то для всех потоков управления, созданных с атрибутом PTHREAD_CREATE_JOINABLE, следует предусмотреть вызов либо pthread_join(), либо pthread_detach().
В качестве примера многопотоковой программы приведем серверную часть рассматривавшегося в курсе [1] приложения, копирующего строки со стандартного ввода на стандартный вывод с "прокачиванием" их через потоковые сокеты (см. листинг 1.38).
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа процесса (будем называть его серверным), */ /* принимающего запросы на установления соединения и */ /* запускающего потоки управления для их обслуживания */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */
#include
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая (и единственная) функция потоков управления, */ /* обслуживающих запросы на копирование строк, */ /* поступающих из сокета */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *srv_thread_start (void *ad) { FILE *fpad; /* Поток данных, соответствующий */ /* дескриптору ad */ char line [LINE_MAX]; /* Буфер для принимаемых строк */ /* Структура для записи адреса */ struct sockaddr_in sai; /* Длина адреса */ socklen_t sai_len = sizeof (struct sockaddr_in);
/* Опросим адрес партнера по общению (передающего сокета) */ if (getpeername ((int) ad, (struct sockaddr *) &sai, &sai_len) < 0) { perror ("GETPEERNAME"); return (NULL); }
/* По файловому дескриптору ad сформируем */ /* буферизованный поток данных */ if ((fpad = fdopen ((int) ad, "r")) == NULL) { perror ("FDOPEN"); return (NULL); }
/* Цикл чтения строк из сокета */ /* и выдачи их на стандартный вывод */ while (fgets (line, sizeof (line), fpad) != NULL) { printf ("Вы ввели и отправили с адреса %s, " "порт %d :", inet_ntoa (sai.sin_addr), ntohs (sai.sin_port)); fputs (line, stdout); }
/* Закрытие соединения */ shutdown ((int) ad, SHUT_RD); (void) fclose (fpad);
return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* В функции main() принимаются запросы на установление */ /* соединения и запускаются потоки управления для их обслуживания */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int sd; /* Дескриптор слушающего сокета */ int ad; /* Дескриптор приемного сокета */ /* Буфер для принимаемых строк */ struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL, NULL}; /* Указатель – выходной аргумент getaddrinfo */ struct addrinfo *addr_res; int res; /* Результат getaddrinfo */ pthread_attr_t patob; /* Атрибутный объект для создания */ /* потоков управления */ pthread_t adt_id; /* Идентификатор обслуживающего потока управления */
/* Создадим слушающий сокет */ if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror ("SOCKET"); return (1); }
/* Привяжем этот сокет к адресу сервиса spooler */ /* на локальном хосте */ if ((res = getaddrinfo (NULL, "spooler", &hints, &addr_res)) != 0) { fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res)); return (2); } if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) { perror ("BIND"); return (3); }
/* Можно освободить память, которую запрашивала */ /* функция getaddrinfo() */ freeaddrinfo (addr_res);
/* Пометим сокет как слушающий */ if (listen (sd, SOMAXCONN) < 0) { perror ("LISTEN"); return (4); }
/* Инициализируем атрибутный объект потоков управления */ if ((errno = pthread_attr_init (&patob)) != 0) { perror ("PTHREAD_ATTR_INIT"); return (errno); } /* Потоки управления будем создавать обособленными */ (void) pthread_attr_setdetachstate (&patob, PTHREAD_CREATE_DETACHED);
/* Цикл приема соединений и запуска */ /* обслуживающих потоков управления */ while (1) { /* Примем соединение. */ /* Адрес партнера по общению нас */ /* в данном случае не интересует */ if ((ad = accept (sd, NULL, NULL)) < 0) { perror ("ACCEPT"); return (6); }
/* Запустим обслуживающий поток управления */ if ((errno = pthread_create (&adt_id, &patob, srv_thread_start,(void *) ad)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } }
return (0); }
Листинг 1.38. Пример многопотоковой программы, обслуживающей запросы на копирование строк, поступающих через сокеты.
Многопотоковая реализация в данном случае уместнее многопроцессной: она и выглядит проще (поскольку потоки проще создавать и не обязательно в явном виде ждать их завершения), и ресурсов потребляет меньше.
Можно надеяться, что и следующая программа (см. листинг 1.39), реализующая идею обработки данных с контролем времени, подтверждает, что применение потоков управления позволяет сделать исходный текст более простым и наглядным по сравнению с "беспотоковым" вариантом (см. курс [1]). Удалось избавиться от такого сугубо "неструктурного" средства, как нелокальные переходы, и от ассоциированных с ними тонкостей, чреватых ошибками.
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа вызывает функции обработки в рамках */ /* порождаемых потоков управления и контролирует время */ /* их выполнения с помощью интервального таймера */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */
#include
/* Период интервального таймера (в секундах) */ #define IT_PERIOD 1
static pthread_t cthread_id; /* Идентификатор текущего */ /* потока управления, */ /* обрабатывающего данные */
static int in_proc_data = 0; /* Признак активности */ /* потока обработки данных */
static double s; /* Результат функций */ /* обработки данных */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция обработки срабатывания таймера реального */ /* времени (сигнал SIGALRM) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_sigalrm (int dummy) { if (in_proc_data) { /* Не имеет значения, какой поток обрабатывает сигнал */ /* и заказывает терминирование (быть может, себя) */ (void) pthread_cancel (cthread_id); } }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Обработчик завершения потока управления. */ /* Сбрасывает признак активности потока обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_data_cleanup_handler (void *arg) { in_proc_data = (int) arg; }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока управления, обрабатывающего */ /* данные. Аргумент – указатель на функцию обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_func (void *proc_data_func) { /* Поместим в стек обработчик завершения */ pthread_cleanup_push (proc_data_cleanup_handler, 0); in_proc_data = 1; /* Время пошло ... */
/* На время выполнения функции обработки данных установим */ /* асинхронный тип терминирования, иначе оно не сработает */ (void) pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
/* Выполним функцию обработки данных */ ((void (*) (void)) (proc_data_func)) ();
/* Установим отложенный тип терминирования, */ /* иначе изъятие обработчика из стека */ /* будет небезопасным действием */ (void) pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, NULL);
/* Выполним обработчик завершения и удалим его из стека */ pthread_cleanup_pop (1); return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Первая функция обработки данных (вычисляет ln (2)) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_data_1 (void) { double d = 1; int i;
s = 0; for (i = 1; i <= 100000000; i++) { s += d / i; d = -d; } }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Вторая функция обработки данных (вычисляет sqrt (2))*/ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ static void proc_data_2 (void) { s = 1; do { s = (s + 2 / s) * 0.5; } while ((s * s – 2) > 0.000000001); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() задает способ обработки сигнала SIGALRM, */ /* взводит периодический таймер реального времени */ /* и запускает в цикле потоки обработки данных */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { /* Массив указателей на функции обработки данных */ void (*fptrs []) (void) = {proc_data_1, proc_data_2, NULL}; /* Указатель на указатель на */ /* текущую функцию обработки данных */ void (**tfptr) (void); void *pstat; /* Статус завершения потока */ /* обработки данных */ struct itimerval itvl; struct sigaction sact; int i;
/* Установим реакцию на сигнал SIGALRM */ sact.sa_handler = proc_sigalrm; sact.sa_flags = 0; (void) sigemptyset (&sact.sa_mask); if (sigaction (SIGALRM, &sact, NULL) < 0) { perror ("SIGACTION"); return (1); }
/* Сделаем таймер реального времени периодическим */ itvl.it_interval.tv_sec = IT_PERIOD; itvl.it_interval.tv_usec = 0;
/* Цикл запуска потоков обработки данных. */ /* Выполним его дважды */ for (i = 0; i < 2; i++) { for (tfptr = fptrs; *tfptr != NULL; tfptr++) { /* Взведем интервальный таймер реального времени */ itvl.it_value.tv_sec = IT_PERIOD; itvl.it_value.tv_usec = 0; if (setitimer (ITIMER_REAL, &itvl, NULL) < 0) { perror ("SETITIMER"); return (2); }
/* Создадим поток обработки данных, */ /* затем дождемся его завершения */ if ((errno = pthread_create (&cthread_id, NULL, start_func, (void *) *tfptr)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } if ((errno = pthread_join (cthread_id, &pstat)) != 0) { perror ("PTHREAD_JOIN"); return (errno); }
if (pstat == PTHREAD_CANCELED) { printf ("Частичный результат функции " "обработки данных: %g\n", s); } else { printf ("Полный результат функции " "обработки данных: %g\n", s); } } }
return 0; }
Листинг 1.39. Пример многопотоковой программы, осуществляющей обработку данных с контролем времени.
Возможные результаты работы приведенной программы показаны на листинге 1.40.
Частичный результат функции обработки данных: 0.693147 Полный результат функции обработки данных: 1.41421 Частичный результат функции обработки данных: 0.693147 Полный результат функции обработки данных: 1.41421
Листинг 1.40. Возможные результаты работы многопотоковой программы, осуществляющей обработку данных с контролем времени.
К сожалению, там, где есть недетерминированность и асинхронность, без тонкостей все равно не обойтись. Мы обратим внимание на три из них. Во-первых, возможно срабатывание таймера и доставка сигнала SIGALRM до того, как завершится (или даже начнется) первый вызов pthread_create() и будет инициализирована переменная cthread_id. Чтобы не допустить терминирования "неопределенного" потока управления в функции обработки сигнала, введен признак активности потока обработки данных. Если он установлен, переменная cthread_id заведомо инициализирована.
Во-вторых, не имеет значения, в контексте какого из потоков выполняется функция обработки сигнала – вполне допустимо, чтобы поток заказал собственное терминирование.
В-третьих, поскольку функции обработки данных не содержат точек терминирования, на время их выполнения необходимо установить асинхронный тип терминирования, иначе оно попросту не сработает. Однако использование этого типа крайне опасно, поэтому при первой возможности следует вернуться к более надежному отложенному типу. Перед манипуляциями со стеком обработчиков завершения установка данного типа или запрет терминирования являются обязательными.
Как мы уже упоминали, потоки управления иногда называют легковесными процессами. Любопытно оценить, какова степень их легковесности, насколько накладные расходы на их обслуживание меньше, чем для "настоящих" процессов.
На листингах 1.41 и 1.42 показана программа, которая в цикле порождает практически пустые процессы и дожидается их завершения; на листинге 1.43 приведены данные о времени ее работы, полученные с помощью команды time -p. Даже если сделать процессам послабление и убрать вызов execl(), времена получатся довольно большими (см. листинг 1.44) в сравнении с аналогичными данными для варианта с потоками управления (см. листинги 1.45 и 1.46).
#include
#define N 10000
int main (void) { int i;
for (i = 0; i < N; i++) { switch (fork ()) { case -1: perror ("FORK"); return (1); case 0: /* Порожденный процесс */ (void) execl ("./dummy", "dummy", (char *) 0); exit (0); default: /* Родительский процесс */ (void) wait (NULL); } }
return 0; }
Листинг 1.41. Пример программы, порождающей в цикле практически пустые процессы.
int main (void) { return 0; }
Листинг 1.42. Содержимое файла dummy.c
real 34.97 user 12.36 sys 22.61
Листинг 1.43. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы (вариант с вызовом execl()).
real 11.49 user 2.38 sys 9.11
Листинг 1.44. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы (вариант без вызова execl()).
#include
#define N 10000
static void *thread_start (void *arg) { pthread_exit (arg); }
int main (void) { pthread_t thread_id; int i;
for (i = 0; i < N; i++) { if ((errno = pthread_create ( &thread_id, NULL, thread_start, NULL)) != 0) { perror ("PTHREAD_CREATE"); return (errno); } if ((errno = pthread_join ( thread_id, NULL)) != 0) { perror ("PTHREAD_JOIN"); return (errno); } }
return (0); }
Листинг 1.45. Пример программы, порождающей в цикле потоки управления.
real 2.08 user 0.52 sys 1.56
Листинг 1.46. Возможные результаты измерения времени работы программы, порождающей в цикле потоки управления.
В первом приближении можно считать, что потоки управления на порядок дешевле процессов. На самом деле, в реальных ситуациях, когда процессы существенно превосходят по размеру приведенные выше "пустышки", различие будет еще больше.
Можно сделать вывод, что потоки управления допускают довольно свободное использование, накладные расходы на их обслуживание невелики, особенно в сравнении с обслуживанием процессов. Поэтому, проектируя приложение с параллельно выполняемыми компонентами, следует в первую очередь проанализировать возможность многопотоковой реализации. Главное препятствие в осуществлении подобной возможности – разделение потоками одного адресного пространства. Только если это препятствие окажется непреодолимым, целесообразно воспользоваться механизмом процессов.

Как и процесс, поток управления можно терминировать изнутри и извне. С одним, неявным, но наиболее естественным способом "самоликвидации" – выходом из стартовой функции потока – мы уже познакомились. Тот же эффект достигается вызовом функции pthread_exit() (см. листинг 1.26).
#include
Листинг 1.26. Описание функции pthread_exit(). (html, txt)
Терминирование потока управления в идейном плане существенно сложнее создания. Чтобы осветить все тонкости, необходимо ввести несколько новых понятий и описать целый ряд функций. Пока мы укажем лишь, что терминирование последнего потока в процессе вызывает его (процесса) завершение с нулевым кодом.
Из общих соображений (например, если исходить из аналогии между процессами и потоками управления) очевидно, что должна существовать возможность дождаться завершения заданного потока управления. Эта возможность реализуется функцией pthread_join() (см. листинг 1.27), напоминающей waitpid().
#include
Листинг 1.27. Описание функции pthread_join(). (html, txt)
Поток управления, вызвавший функцию pthread_join(), приостанавливает выполнение до завершения потока, идентификатор которого задан аргументом thread. При успешном возврате из pthread_join() результат, как и положено, равен нулю, а по указателю value_ptr_ptr (если он не пуст) помещается значение (указатель value_ptr), переданное в качестве аргумента функции pthread_exit(). Тем самым ждуший поток получает данные о статусе завершения ожидаемого.
Отметим, что трактовка значения value_ptr возлагается на приложение. Например, оно может считать его целым числом, а не указателем; по этой причине операционная система при выполнении функции pthread_exit() не вправе выдавать ошибку типа "неверный адрес" каким бы ни был аргумент value_ptr. Если он все же является указателем, то ему нельзя присваивать адрес автоматической переменной, поскольку к моменту его использования ожидаемый поток уже завершится и состояние его автоматических переменных станет неопределенным.
Второе общее соображение касается того обстоятельства, что вызов такой функции, как ожидание завершения (pthread_join()) способен приостановить выполнение вызывающего потока управления на неопределенное время, в течение которого ему может быть доставлен обрабатываемый сигнал. Как правило, в подобных ситуациях выполнение функций (таких, например, как read()) завершается с частично достигнутым результатом (например, с числом прочитанных байт, меньшим запрошенного) и кодом ошибки EINTR, нуждающимся в нестандартной обработке, далеко не всегда реализуемой разработчиками приложений. Из-за этого в программах появляются дефекты, которые трудно воспроизвести и, соответственно, исправить.
Согласно стандарту POSIX-2001, функции, обслуживающие потоки управления, свободны от этого недостатка. Они никогда не завершаются с частичным результатом и не выдают код ошибки EINTR. Восстановление нормального состояния после того, как ожидание было прервано доставкой и обработкой сигнала, возлагается на операционную систему, а не на приложение.
Третье общее соображение состоит в том, что такое критически важное событие, как завершение потока управления, не может оставаться без функций-обработчиков. Стандартом POSIX-2001 предусмотрено существование не одного, а целого стека подобных обработчиков, ассоциированного с потоком управления. Операции над этим стеком возложены на функции pthread_cleanup_push() и pthread_cleanup_pop() (см. листинг 1.28).
#include
void pthread_cleanup_push ( void (*routine) (void *), void *arg);
void pthread_cleanup_pop (int execute);
Листинг 1.28. Описание функций pthread_cleanup_push() и pthread_cleanup_pop(). (html, txt)
Функция pthread_cleanup_push() помещает заданный аргументами routine и arg обработчик в стек обработчиков вызывающего потока. Функция pthread_cleanup_pop() извлекает верхний обработчик из этого стека и, если значение аргумента execute отлично от нуля, вызывает его (как (*routine) (arg)).
Разумеется, все обработчики, начиная с верхнего, извлекаются из стека и вызываются при терминировании потока управления (вне зависимости от того, объясняется ли терминирование внутренними или внешними причинами).
В частности, это происходит после того, как поток обратится к функции pthread_exit().
Напомним, что обработчики завершения существуют и для процессов (они регистрируются с помощью функции atexit()), однако применительно к потокам управления идея стека обработчиков оформлена в более явном и систематическом виде.
Пару функций pthread_cleanup_push() и pthread_cleanup_pop() можно представлять себе как открывающую и закрывающую скобки, оформленные в виде отдельных инструкций языка C и обрамляющие обслуживаемый обработчиком участок программы. Согласно стандарту POSIX-2001, этот участок должен представлять собой фрагмент одной лексической области видимости (блока), а pthread_cleanup_push() и pthread_cleanup_pop() могут быть реализованы как макросы (см. листинг 1.29).
#define pthread_cleanup_push (rtn, arg) { \ struct _pthread_handler_rec \ __cleanup_handler, \ **__head; \ __cleanup_handler.rtn = rtn; \ __cleanup_handler.arg = arg; \ (void) pthread_getspecific \ (_pthread_handler_key, &__head); \ __cleanup_handler.next = *__head; \ *__head = &__cleanup_handler;
#define pthread_cleanup_pop (ex) \ *__head = __cleanup_handler.next; \ if (ex) (*__cleanup_handler.rtn) \ (__cleanup_handler.arg); \ }
Листинг 1.29. Возможная реализация функций pthread_cleanup_push() и pthread_cleanup_pop() как макросов. (html, txt)
Обратим внимание на то, что в определении макроса pthread_cleanup_push() открывается внутренний блок, в котором декларируются два необходимых объекта – структура __cleanup_handler, описывающая обработчик, и указатель __head на вершину стека, представленного в виде односвязанного (линейного) списка. В определении pthread_cleanup_pop() этот блок закрывается. Так что даже из соображений синтаксической корректности вызовы pthread_cleanup_push() и pthread_cleanup_pop() должны быть парными и располагаться в одном блоке, но более существенной нам представляется корректность семантическая.

![]() | © 2003-2007 INTUIT.ru. Все права защищены. |
Барьеры
Барьеры – весьма своеобразное средство синхронизации. Идея его в том, чтобы в определенной точке ожидания собралось заданное число потоков управления. Только после этого они смогут продолжить выполнение. (Поговорка "семеро одного не ждут" к барьерам не применима.)
Барьеры полезны для организации коллективных распределенных вычислений в многопроцессорной конфигурации, когда каждый участник (поток управления) выполняет часть работы, а в точке сбора частичные результаты объединяются в общий итог.
Функции, ассоциированные с барьерами, подразделяются на следующие группы.
#include
int pthread_barrier_init ( pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count);
int pthread_barrier_destroy ( pthread_barrier_t *barrier);
Листинг 2.36. Описание функций инициализации и разрушения барьеров. (html, txt)
#include
Листинг 2.37. Описание функции синхронизации на барьере. (html, txt)
#include
int pthread_barrierattr_init ( pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy ( pthread_barrierattr_t *attr);
Листинг 2.38. Описание функций инициализации и разрушения атрибутных объектов барьеров. (html, txt)
#include
int pthread_barrierattr_getpshared (const pthread_barrierattr_t *restrict attr, int *restrict pshared);
int pthread_barrierattr_setpshared (pthread_barrierattr_t *attr, int pshared);
Листинг 2.39. Описание функций опроса и установки атрибутов барьеров в атрибутных объектах. (html, txt)
Обратим внимание на аргумент count в функции инициализации барьера pthread_barrier_init(). Он задает количество синхронизируемых потоков управления. Столько потоков должны вызвать функцию pthread_barrier_wait(), прежде чем каждый из них сможет успешно завершить вызов и продолжить выполнение. (Разумеется, значение count должно быть положительным.)
Когда к функции pthread_barrier_wait() обратилось требуемое число потоков управления, одному из них (стандарт POSIX-2001 не специфицирует, какому именно) в качестве результата возвращается именованная константа PTHREAD_BARRIER_SERIAL_THREAD, а всем другим выдаются нули. После этого барьер возвращается в начальное (инициализированное) состояние, а выделенный поток может выполнить соответствующие объединительные действия.
Описанная схема работы проиллюстрирована листингом 2.40.
if ((status = pthread_barrier_wait( &barrier)) == PTHREAD_BARRIER_SERIAL_THREAD) { /* Выделенные (обычно – объединительные) */ /* действия. */ /* Выполняются каким-то одним потоком */ /* управления */
} else { /* Эта часть выполняется всеми */ /* прочими потоками */ /* управления */ if (status != 0) { /* Обработка ошибочной ситуации */ } else { /* Нормальное "невыделенное" */ /* завершение ожидания */ /* на барьере */ } }
/* Повторная синхронизация – */ /* ожидание завершения выделенных действий */ status = pthread_barrier_wait (&barrier); /* Продолжение параллельной работы */ . . .
Листинг 2.40. Типичная схема применения функции pthread_barrier_wait(). (html, txt)
Отметим, что для барьеров отсутствует вариант синхронизации с контролем времени ожидания. Это вполне понятно, поскольку в случае срабатывания контроля барьер окажется в неработоспособном состоянии (требуемое число потоков, скорее всего, уже не соберется). По той же причине функция pthread_barrier_wait() не является точкой терминирования – "оставшиеся в живых" не переживут потери товарища...
Аналогично, не являются точками терминирования функции pthread_mutex_lock() и pthread_spin_lock().Если бы они были таковыми, то точками терминирования стали бы все функции, в том числе библиотечные, которые их вызывают – malloc(), free() и т.п. Обеспечить в обработчиках завершения корректное состояние объектов, обслуживаемых подобными функциями, довольно сложно; это деятельность, чреватая ошибками, которые трудно не только найти и исправить, но даже воспроизвести. С другой стороны, мьютексы и спин-блокировки предназначены для захвата на короткое время, без длительного ожидания, так что нечувствительность к терминированию в данном случае не составляет большой проблемы.
Работу с барьерами проиллюстрируем коллективными вычислениями, производимыми двумя потоками (см. листинг 2.41).
Листинг 2.41. Пример программы, использующей барьеры. (html, txt)
В данном случае второго ожидания на барьере не понадобилось – вместо этого потоки управления просто завершаются.
Блокировки чтение-запись
Блокировки чтение-запись можно назвать интеллектуальным средством синхронизации, поскольку они делают различие между читателями и писателями. В большинстве случаев разделяемые данные чаще читают, чем изменяют (действительно, зачем писать то, что никто не будет читать?), и это делает блокировки чтение-запись весьма употребительными.
Типичный пример использования блокировок чтение-запись – синхронизация доступа к буферизованным в памяти фрагментам файловой системы, особенно к каталогам (чем ближе к корню, тем чаще их читают и реже изменяют). Важно отметить в этой связи, что, в отличие от блокировок, реализуемых функцией fcntl() (см. курс [1]), блокировки чтение-запись могут применяться и там, где файловая система отсутствует (например, в минимальных конфигурациях, функционирующих под управлением соответствующей подпрофилю стандарта POSIX операционной системы реального времени).
Применительно к блокировкам чтение-запись предоставляются следующие группы функций.
#include
int pthread_rwlock_init ( pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy ( pthread_rwlock_t *rwlock);
Листинг 2.20. Описание функций инициализации и разрушения блокировок чтение-запись. (html, txt)
#include
int pthread_rwlock_rdlock ( pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock ( pthread_rwlock_t *rwlock);
Листинг 2.21. Описание функций установки блокировки на чтение. (html, txt)
#include
Листинг 2.22. Описание функции установки блокировки на чтение с ограниченным ожиданием. (html, txt)
листинги 2.23 и 2.24);
#include
int pthread_rwlock_wrlock ( pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock ( pthread_rwlock_t *rwlock);
Листинг 2.23. Описание функций установки блокировки на запись. (html, txt)
#include
Листинг 2.24. Описание функции установки блокировки на запись с ограниченным ожиданием. (html, txt)
#include
Листинг 2.25. Описание функции снятия блокировки чтение-запись. (html, txt)
#include
int pthread_rwlockattr_init ( pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy ( pthread_rwlockattr_t *attr);
Листинг 2.26. Описание функций инициализации и разрушения атрибутных объектов блокировок. (html, txt)
#include
int pthread_rwlockattr_getpshared ( const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared ( pthread_rwlockattr_t *attr, int pshared);
Листинг 2.27. Описание функций опроса и установки атрибутов блокировок в атрибутных объектах. (html, txt)
Тонким вопросом, связанным с блокировками чтение-запись, является взаимодействие читателей и писателей. Согласно стандарту POSIX-2001, если речь не идет о приложениях реального времени, то от реализации зависит, будет ли приостановлен поток управления при попытке установить блокировку на чтение при наличии ожидающих освобождения той же блокировки писателей. (Как правило, чтобы воспрепятствовать зависанию писателей, читателей в подобной ситуации "тормозят", иначе они так и будут подхватывать блокировку друг у друга.
Для политик планирования реального времени преимущество получает более приоритетный поток.)
Естественно, если блокировку установил писатель, читателю придется подождать. Если этим писателем оказался тот же поток, ждать придется долго...
Параллельно одним или несколькими потоками управления может быть установлено несколько блокировок на чтение. Сколько раз устанавливали такую блокировку, столько же раз ее необходимо снять. (Установка блокировки писателем возможна только после последнего снятия.) Максимально допустимое число одновременно установленных блокировок на чтение зависит от реализации.
Когда снимают блокировку, на которую претендуют и читатели, и писатели, как правило, преимущество получают писатели, но, строго говоря, решение зависит от реализации.
Разумеется, снимать блокировку может только тот поток управления, который ее устанавливал; в противном случае поведение неопределено.
В качестве иллюстрации применения блокировок чтение-запись приведем набор функций для работы со списками в многопотоковой среде. На листинге 2.28 показан заголовочный файл, содержащий необходимые описания, на листинге 2.29 – C-файл с реализацией функций. Листинг 2.30 содержит текст тестовой программы, листинг 2.31 – результаты ее работы. Обратим внимание на использование блокировок без приостановки выполнения и на проверку статуса завершения попыток их установить.
Листинг 2.28. Заголовочный файл g_list.h для функций работы со списками в многопотоковой среде. (html, txt)
Листинг 2.29. Исходный текст функций для работы со списками в многопотоковой среде. (html, txt)
Листинг 2.30. Пример программы, использующей функции для работы со списками в многопотоковой среде. (html, txt)
Попыток выполнить операцию со списком: 1503 Число попыток, неудачных из-за занятости списка: 0
Листинг 2.31. Возможные результаты работы программы, использующей функции для работы со списками в многопотоковой среде. (html, txt)
Можно видеть, что в данном случае оптимистичное применение блокировок чтение-запись без приостановки выполнения в случае невозможности немедленной установки себя полностью оправдало.
/* Инициализировать список */ extern void g_list_init (g_listP);
/* Завершить работу со списком */ extern void g_list_destroy (g_listP);
/* Вставить новый элемент */ /* в конец списка */ extern void g_list_ins_last ( g_listP, void *); /* Вставить новый элемент в */ /* список перед первым, */ /* удовлетворяющим заданному */ /* свойству (или в конец) */ extern void g_list_ins_fun ( g_listP, void *, afun);
/* Удалить элемент из списка */ extern void g_list_del ( g_listP, void *);
/* Применить процедуру ко всем */ /* элементам списка */ extern void g_list_forall_x ( g_listP, aproc, void *);
/* Выбрать подходящий элемент списка */ extern void *g_list_suchas_x ( g_listP, afun, void *);
/* Выдать элемент по номеру */ extern void *g_list_get_bynum ( g_listP, int);
#endif
Листинг 2.28. Заголовочный файл g_list. h для функций работы со списками в многопотоковой среде.
/* * * * * * * * * * * * * * * * * * * * */ /* Реализация функций для многопотоковой */ /* работы со списками. */ /* Применяются блокировки чтение-запись */ /* без приостановки выполнения */ /* * * * * * * * * * * * * * * * * * * * */
#define _XOPEN_SOURCE 600
#include
/* * * * * * * * * * * * * */ /* Инициализировать список */ /* * * * * * * * * * * * * */ void g_list_init (g_listP plist) { plist->head = NULL; errno = pthread_rwlock_init ( &plist->lk, NULL); } /* * * * * * * * * * * * * * * */ /* Завершить работу со списком */ /* * * * * * * * * * * * * * * */ void g_list_destroy (g_listP plist) { errno = pthread_rwlock_destroy ( &plist->lk); }
/* * * * * * * * * * * * * * * * * * * * */ /* Вставить новый элемент в конец списка */ /* * * * * * * * * * * * * * * * * * * * */ void g_list_ins_last (g_listP plist, void *pval) { register g_linkP pt; register g_linkP *p;
if ((errno = pthread_rwlock_trywrlock ( &plist->lk)) != 0) { return; }
for (p = &plist->head; *p != NULL; p = &(*p)->pnext) ; if ((pt = (g_linkP) malloc (sizeof (*pt))) != NULL) { pt->pnext = NULL; pt->pvalue = pval; *p = pt; }
(void) pthread_rwlock_unlock ( &plist->lk); }
/* * * * * * * * * * * * * * * * * */ /* Вставить новый элемент в список */ /* перед первым, */ /* удовлетворяющим заданному свойству */ /* (или в конец) */ /* * * * * * * * * * * * * * * * */ void g_list_ins_fun (g_listP plist, void *pval, afun fun) { register g_linkP pt; register g_linkP *p;
if ((errno = pthread_rwlock_trywrlock ( &plist->lk)) != 0) { return; }
for (p = &plist->head; (*p != NULL) && (fun (pval, (*p)->pnext->pvalue) == 0); p = &(*p)->pnext) ;
if ((pt = (g_linkP) malloc ( sizeof (*pt))) != NULL) { pt->pnext = NULL; pt->pvalue = pval; *p = pt; }
(void) pthread_rwlock_unlock ( &plist->lk); }
/* * * * * * * * * * * * * * */ /* Удалить элемент из списка */ /* * * * * * * * * * * * * * */ void g_list_del (g_listP plist, void *pval) { register g_linkP pt; register g_linkP *p;
if ((errno = pthread_rwlock_trywrlock (&plist->lk)) != 0) { return; }
for (p = &plist->head; *p != NULL; p = &(*p)->pnext) { if ((*p)->pvalue == pval) { pt = *p; *p = pt->pnext; free (pt); errno = pthread_rwlock_unlock( &plist->lk); return; } }
/* Пытаемся удалить */ /* несуществующий элемент */ (void) pthread_rwlock_unlock ( &plist->lk); errno = EINVAL; }
/* * * * * * * * * * * * * */ /* Применить процедуру ко */ /* всем элементам списка */ /* * * * * * * * * * * * */ void g_list_forall_x (g_listP plist, aproc proc, void *extobj) { register g_linkP pt;
/* Устанавливаем блокировку на запись, */ /* поскольку, возможно, процедура */ /* модифицирует */ /* объекты, ссылки на которые */ /* хранятся в списке */ if ((errno = pthread_rwlock_trywrlock( &plist->lk)) != 0) { return; }
for (pt = plist->head; pt != NULL; pt = pt->pnext) { proc (extobj, pt->pvalue); }
(void) pthread_rwlock_unlock( &plist->lk); }
/* * * * * * * * * * * * * * * * * * */ /* Выбрать подходящий элемент списка */ /* * * * * * * * * * * * * * * * * * */ void *g_list_suchas_x (g_listP plist, afun fun, void *extobj) { register g_linkP pt;
/* Устанавливаем блокировку на */ /* чтение, так как */ /* считаем, что функция не */ /* модифицирует объекты, */ /* ссылки на которые хранятся */ /* в списке */ if ((errno = pthread_rwlock_tryrdlock( &plist->lk)) != 0) { return NULL; }
for (pt = plist->head; pt != NULL; pt = pt->pnext) { if (fun (extobj, pt->pvalue)) { (void) pthread_rwlock_unlock( &plist->lk); return (pt->pvalue); } }
(void) pthread_rwlock_unlock( &plist->lk); return NULL; }
/* * * * * * * * * * * * * * */ /* Выдать элемент по номеру */ /* * * * * * * * * * * * * * */ void *g_list_get_bynum (g_listP plist, int n) { register g_linkP pt;
if ((errno = pthread_rwlock_tryrdlock( &plist->lk)) != 0) { return NULL; }
for (pt = plist->head; n--, pt != NULL; pt = pt->pnext) { if (n == 0) { (void) pthread_rwlock_unlock( &plist->lk); return (pt->pvalue); } }
/* Пытаемся извлечь несуществующий */ /* компонент */ (void) pthread_rwlock_unlock( &plist->lk); errno = EINVAL;
return NULL; }
Листинг 2.29. Исходный текст функций для работы со списками в многопотоковой среде.
/* * * * * * * * * * * * * * * * * * * */ /* Тест функций многопотоковой работы */ /* со списками */ /* * * * * * * * * * * * * * * * * * * */
#define _XOPEN_SOURCE 600
#include
/* Структура для засыпания на */ /* минимальное время */ static struct timespec nslp = {0, 1};
/* Структуры для возврата результатов */ /* потоками: */ /* Сколько операций пытались сделать, */ /* сколько из них оказались неудачными */ /* из-за занятости списка */ static struct pt_res { int nops; int nbusy; } ptres [3] = {{0, 0}, {0, 0}, {0, 0}};
/* * * * * * * * * * * * * * * * * * * * */ /* Этот поток добавляет элементы к списку */ /* * * * * * * * * * * * * * * * * * * * */ static void *start_func_1 (void *plist) { int *p1;
while (1) { if ((p1 = (int *) malloc ( sizeof (int))) != NULL) { *p1 = rand (); errno = 0; g_list_ins_last (plist, p1); ptres [0].nops++; if (errno == EBUSY) { ptres [0].nbusy++; } } (void) nanosleep (&nslp, NULL); }
return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * */ /* Процедура для подсчета суммы элементов */ /* списка */ /* * * * * * * * * * * * * * * * * * * * * */ static void proc_sum (void *sum, void *pval) { *((int *) sum) += *((int *) pval); } /* * * * * * * * * * * * * * * * * * * * * */ /* Этот поток подсчитывает сумму элементов */ /* списка */ /* * * * * * * * * * * * * * * * * * * * * */ static void *start_func_2 (void *plist) { int sum;
while (1) { sum = 0; errno = 0; g_list_forall_x (plist, &proc_sum, &sum); ptres [1].nops++; if (errno == EBUSY) { ptres [1].nbusy++; } (void) nanosleep (&nslp, NULL); }
return (NULL); }
/* * * * * * * * * * * * * * * * * */ /* Этот поток удаляет из списка */ /* элементы со случайными номерами */ /* * * * * * * * * * * * * * * * * */ static void *start_func_3 (void *plist) { int *p1;
while (1) { errno = 0; p1 = (int *) g_list_get_bynum( plist, rand ()); ptres [2].nops++; if (errno == EBUSY) { ptres[2].nbusy++; }
if (p1 != NULL) { errno = 0; g_list_del (plist, p1); ptres [2].nops++; if (errno == EBUSY) { ptres [2].nbusy++; } free (p1); } (void) nanosleep (&nslp, NULL); }
return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * * */ /* Начальный поток всех запустит, поспит, */ /* потом всех терминирует и выдаст статистику */ /* * * * * * * * * * * * * * * * * * * * * * */ int main (void) { g_listP plist; pthread_t pt1, pt2, pt3;
g_list_init (plist);
pthread_create (&pt1, NULL, start_func_1, plist); pthread_create (&pt2, NULL, start_func_2, plist); pthread_create (&pt3, NULL, start_func_3, plist);
sleep (10);
pthread_cancel (pt1); pthread_cancel (pt2); pthread_cancel (pt3);
pthread_join (pt1, NULL); pthread_join (pt2, NULL); pthread_join (pt3, NULL);
g_list_destroy (plist);
printf ("Попыток выполнить " "операцию со списком: %d\n", ptres [0].nops + ptres [1].nops + ptres [2].nops); printf ("Число попыток, неудачных" " из-за занятости " "списка: %d\n", ptres [0].nbusy + ptres [1].nbusy + ptres [2].nbusy);
return (0); }
Листинг 2.30. Пример программы, использующей функции для работы со списками в многопотоковой среде.
Попыток выполнить операцию со списком: 1503 Число попыток, неудачных из-за занятости списка: 0
Листинг 2.31. Возможные результаты работы программы, использующей функции для работы со списками в многопотоковой среде.
Можно видеть, что в данном случае оптимистичное применение блокировок чтение-запись без приостановки выполнения в случае невозможности немедленной установки себя полностью оправдало.
Мьютексы
Функции, обслуживающие мьютексы, можно разбить на следующие группы:
#include
int pthread_mutex_init ( pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy ( pthread_mutex_t *mutex);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
Листинг 2.1. Описание функций инициализации и разрушения мьютексов. (html, txt)
#include
int pthread_mutex_lock ( pthread_mutex_t *mutex);
int pthread_mutex_trylock ( pthread_mutex_t *mutex);
int pthread_mutex_unlock ( pthread_mutex_t *mutex);
Листинг 2.2. Описание функций захвата и освобождения мьютексов. (html, txt)
#include
Листинг 2.3. Описание функции захвата мьютексов с ограниченным ожиданием. (html, txt)
#include
int pthread_mutex_getprioceiling ( const pthread_mutex_t *restrict mutex, int *restrict prioceiling);
int pthread_mutex_setprioceiling ( pthread_mutex_t *restrict mutex, int prioceiling, int *restrict old_ceiling);
Листинг 2.4. Описание функций опроса и установки атрибутов мьютекса. (html, txt)
#include
int pthread_mutexattr_init ( pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy ( pthread_mutexattr_t *attr);
Листинг 2.5. Описание функций инициализации и разрушения атрибутных объектов мьютексов. (html, txt)
листинг 2.6).
#include
int pthread_mutexattr_gettype ( const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype ( pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_getpshared ( const pthread_mutexattr_t *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared ( pthread_mutexattr_t *attr, int pshared);
int pthread_mutexattr_getprotocol ( const pthread_mutexattr_t *restrict attr, int *restrict protocol);
int pthread_mutexattr_setprotocol ( *attr, int protocol);
int pthread_mutexattr_getprioceiling ( const pthread_mutexattr_t *restrict attr, int *restrict prioceiling); int pthread_mutexattr_setprioceiling ( pthread_mutexattr_t *attr, int prioceiling);
Листинг 2.6. Описание функций опроса и установки атрибутов мьютекса в атрибутных объектах. (html, txt)
Мы не будем детально описывать каждую из перечисленных выше функций (хотя бы потому, что дисциплина работы с атрибутами и атрибутными объектами та же, что и для потоков управления), но коснемся лишь отдельных, специфических аспектов.
Сразу после инициализации функцией pthread_mutex_init() мьютекс, разумеется, оказывается свободным. Разрушить функцией pthread_mutex_destroy() можно только инициализированный, свободный мьютекс.
Для инициализации статически описанных мьютексов с подразумеваемыми значениями атрибутов целесообразно воспользоваться макросом PTHREAD_MUTEX_INITIALIZER. Эффект будет тем же, что и после вызова pthread_mutex_init() с пустым указателем в качестве значения аргумента attr, только без накладных расходов на проверку корректности атрибутного объекта.
В стандарте POSIX-2001 тип pthread_mutex_t трактуется как абстрактный, со скрытой структурой и даже без методов для присваивания и сравнения на равенство, а попытки обойти их отсутствие за счет применения операций с областями памяти, естественно, обречены на неудачу поскольку, согласно стандарту, для синхронизации должны использоваться сами объекты-мьютексы, а не их копии.Это "развязывает руки" операционной системе в использовании доступных аппаратных возможностей при реализации мьютексов, делая ее максимально эффективной.
У инициализированного мьютекса имеется четыре атрибута:
Мы уже отмечали важность эффективной реализации для средств синхронизации потоков управления. Вероятно, по этой причине в число атрибутов не включен идентификатор потока-владельца, поскольку его нужно устанавливать и хранить.
У инициализированного мьютекса имеется четыре атрибута:
Мы уже отмечали важность эффективной реализации для средств синхронизации потоков управления. Вероятно, по этой причине в число атрибутов не включен идентификатор потока-владельца, поскольку его нужно устанавливать и хранить.
В стандарте POSIX-2001 определены четыре типа мьютексов.
PTHREAD_MUTEX_NORMAL
Эффективный, но небезопасный тип. Не проверяется возможность возникновения тупиковой ситуации при захвате подобного мьютекса (например, когда поток управления попытается захватить уже принадлежащий ему мьютекс). Не фиксируются ошибки при освобождении чужого или свободного мьютекса.
PTHREAD_MUTEX_ERRORCHECK
Данный тип обеспечивает выявление ошибочных ситуаций. Упомянутые выше некорректные действия с мьютексами приведут к выдаче кода ошибки. Поскольку, как считается, мьютекс обычно доступен для захвата, а значения атрибутов проверяются лишь тогда, когда поток приходится блокировать, использование атрибутов по сути не влияет на эффективность. В частности, контроль ошибок несколько замедляет нормальное функционирование только при освобождении мьютекса.
PTHREAD_MUTEX_RECURSIVE
Мьютекс со счетчиком и выявлением ошибочных ситуаций. Поток управления может многократно захватить мьютекс, но затем должен столько же раз освободить его.
PTHREAD_MUTEX_DEFAULT
Подразумеваемое значение атрибута "тип". Некорректные действия приводят к неопределенному эффекту. Реализация может отождествить этот тип с одним из вышеописанных.
Атрибут "верхняя грань приоритетов выполнения" определяет минимальный приоритет, с которым будет выполняться критический интервал, охраняемый мьютексом.
Чтобы избежать инверсии приоритетов, значение этого атрибута следует сделать не меньшим, чем максимальный из приоритетов потоков, могущих захватить данный мьютекс (отсюда и название атрибута). Диапазон допустимых значений атрибута тот же, что и для приоритетов политики планирования SCHED_FIFO.
Атрибут "протокол" влияет на планирование потока управления во время владения мьютексом. Согласно стандарту, возможных протоколов три.
PTHREAD_PRIO_NONE
Владение мьютексом не влияет на приоритет и планирование потока управления.
PTHREAD_PRIO_INHERIT
Если поток управления, захватив мьютексы с данным протоколом, блокирует более приоритетные потоки, ему присваивается максимальный из приоритетов ждущих потоков. Если во время владения появляются новые ждущие более приоритетные потоки, приоритет владельца должен быть соответственно повышен. Если затем владелец будет блокирован на другом мьютексе с данным протоколом, повышение приоритетов должно быть рекурсивно распространено.
PTHREAD_PRIO_PROTECT
Если поток управления владеет мьютексами данного типа, он выполняется с приоритетом, являющимся максимумом из верхних граней приоритетов выполнения этих мьютексов, независимо от того, ждут ли какие-либо другие потоки их освобождения.
В стандарте оговаривается, что при изменении приоритета, вызванном операциями с мьютексами, подчиняющимися протоколам PTHREAD_PRIO_INHERIT или PTHREAD_PRIO_PROTECT, поток управления не должен перемещаться в хвост очереди планирования.
Если поток управления владеет несколькими мьютексами с разными протоколами, ему назначается приоритет, максимальный из предписанных каждым из протоколов.
Признак использования несколькими процессами мьютекса, в соответствии со стандартом POSIX-2001, может принимать два значения.
PTHREAD_PROCESS_PRIVATE
Мьютекс доступен только для потоков управления, выполняющихся в рамках того же процесса, что и поток, инициализировавший мьютекс. Это значение атрибута является подразумеваемым.
PTHREAD_PROCESS_SHARED
С мьютексом могут работать все потоки управления, имеющие доступ к памяти, в которой мьютекс расположен, даже если эта память разделяется несколькими процессами.
На попытки захвата мьютекса, осуществляемые с применением функций pthread_mutex_lock(), pthread_mutex_trylock() или pthread_mutex_timedlock() значения атрибутов влияют так, как описано выше.
Как правило, поток, вызвавший pthread_mutex_lock(), блокируется, если мьютекс уже захвачен, и ждет его освобождения. В аналогичной ситуации функция pthread_mutex_trylock() немедленно завершается, возвращая код ошибки, а функция pthread_mutex_timedlock() блокируется, пока либо мьютекс не освободится, либо не наступит заданный аргументом abstime момент времени, отсчитываемого по часам CLOCK_REALTIME.
Обычно функцию pthread_mutex_timedlock() вызывают лишь после того, как с помощью pthread_mutex_trylock() выяснили, что мьютекс захвачен. Ограничение времени ожидания делает программу устойчивой к ошибкам, препятствующим освобождению мьютексов, хотя, если считать, что охраняемый критический интервал невелик, данное свойство не имеет особого значения.
В соответствии с общим подходом, если во время ожидания освобождения мьютекса потоку доставляется сигнал, после выхода из функции обработки сигнала оно возобновляется, как если бы и не прерывалось.
Функция pthread_mutex_unlock() обычно освобождает мьютекс. Для мьютексов типа PTHREAD_MUTEX_RECURSIVE вызов pthread_mutex_unlock(), строго говоря, приводит лишь к уменьшению счетчика на единицу, а освобождение происходит только тогда, когда счетчик становится нулевым. Кому достанется мьютекс после освобождения, зависит от политики планирования.
В качестве примера использования мьютексов приведем упрощенную реализацию динамического выделения памяти в многопотоковой среде. На листинге 2.7 показан заголовочный файл, на листинге 2.8 – исходный текст функций выделения и освобождения памяти.
#ifndef g_MALLOC #define g_MALLOC
/* Количество размеров (в словах типа size_t), */ /* для которых поддерживаются */ /* разные списки памяти */ #define DIF_SIZES 8
/* Размер пула памяти */ #define POOL_SIZE 65536
/* Указатель на кусок памяти нулевого размера */ #define g_NULL ((void *) (-1))
/* Первое поле следующей структуры нужно /* для всех кусков памяти, а второе – */ /* только для провязки свободных.*/ /* При отведении памяти адрес второго */ */ поля выдается как результат */ typedef struct listi { size_t length; struct listi *pnext; } *list_of_mem;
extern void *g_malloc (size_t); extern void g_free (void *);
#endif
Листинг 2.7. Заголовочный файл g_malloc.h для функций выделения и освобождения памяти в многопотоковой среде.
/* * * * * * * * * * * * * * * * * * * * * */ /* Функции выделения и освобождения памяти */ /* * * * * * * * * * * * * * * * * * * * * */
#include
static char mem_pool [POOL_SIZE] = {0, }; /* Размер занятой части пула памяти */ /* Списки свободного пространства */ /* (по одному на каждый размер */ /* от 1 до DIF_SIZES) */ static size_t cur_pool_size = 0;
static list_of_mem short_lists [DIF_SIZES] = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}; /* Список больших */ /* свободных кусков (превосходящих DIF_SIZES) */ /* Разные мьютексы для разных */ /* групп списков свободного пространства */ static list_of_mem big_list = NULL;
static pthread_mutex_t short_lists_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t big_list_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t pool_mutex = PTHREAD_MUTEX_INITIALIZER;
/* * * * * * * * * * */ /* Выделение памяти */ /* * * * * * * * * * */ void *g_malloc (size_t size) { /* Указатель для хождения по списку */ /* больших свободных кусков */ list_of_mem *p; /* Указатель для хождения по спискам */ /* свободных кусков */ list_of_mem pt; /* Индекс в массиве short_lists */ size_t ls; size_t ts; /* Временная переменная */
if (size == 0) { return (g_NULL); /* Важно, чтобы результат был */ /* отличен от NULL, поскольку NULL */ /* – признак ненормального завершения */ }
/* Округлим запрошенный размер вверх */ /* до кратного размеру size_t и */ /* прибавим слово служебной информации */ size = (size – 1 + 2 * sizeof (size_t)) & ~(sizeof (size_t) – 1);
/* Вычислим индекс в массиве */ /* short_lists [], соответствующий */ /* запрошенному размеру */ ls = size / sizeof (size_t) – 2;
if (ls < DIF_SIZES) { /* Попробуем выдать кусок */ /* из списка коротких */ assert ( pthread_mutex_lock( &short_lists_mutex) == 0); if ((pt = short_lists [ls]) != NULL) { /* Есть нужный кусок */ short_lists [ls] = (short_lists [ls])->pnext; assert (pthread_mutex_unlock( &short_lists_mutex) == 0); return (&pt->pnext); } assert (pthread_mutex_unlock( &short_lists_mutex) == 0); }
/* Попробуем выдать кусок из */ /* списка больших */ assert (pthread_mutex_lock( &big_list_mutex) == 0); for (p = &big_list, pt = *p; pt != NULL; p = &pt->pnext, pt = *p) { if ((signed long) (ts = pt->length – size) >= 0) { /* Нашли подходящий кусок */ if (ts < sizeof (*pt)) { /* Придется выдать кусок целиком – */ /* в остатке не помещается */ /* служебная информация */ *p = pt->pnext; } else { /* Отрежем сколько надо и, */ /* при необходимости, */ /* перецепим остаток в */ /* список коротких */ if ((ls = (pt->length = ts) / sizeof (size_t) – 2) < DIF_SIZES) { *p = pt->pnext; assert (pthread_mutex_lock( &short_lists_mutex) == 0); pt->pnext = short_lists [ls]; short_lists [ls] = pt; assert (pthread_mutex_unlock( &short_lists_mutex) == 0); } pt = (list_of_mem) ((char *) pt + ts); pt->length = size; } assert ( pthread_mutex_unlock( &big_list_mutex) == 0); return (&pt->pnext); } } /* for */ assert (pthread_mutex_unlock ( &big_list_mutex) == 0);
/* Кусок из большого списка */ /* выдать не удалось. */ /* Попробуем взять прямо из */ /* пула памяти */ assert (pthread_mutex_lock( &pool_mutex) == 0); if (cur_pool_size + size <= POOL_SIZE) { pt = (list_of_mem) (mem_pool + cur_pool_size); pt->length = size; cur_pool_size += size; assert (pthread_mutex_unlock ( &pool_mutex) == 0); return (&pt->pnext); } assert (pthread_mutex_unlock( &pool_mutex) == 0);
/* Неудача при выделении памяти */ errno = ENOMEM; return (NULL); }
/* * * * * * * * * * * * * * * * * * */ /* Возврат ранее запрошенной памяти */ /* * * * * * * * * * * * * * * * * * */ void g_free (void *p) { list_of_mem pt; size_t size, ls;
if ((p == g_NULL) || (p == NULL)) { return; }
/* Установим указатель на */ /* служебную информацию */ pt = (list_of_mem) ((char *) p – sizeof (size_t)); size = pt->length; ls = size / sizeof (size_t) – 2; memset (p, 0, size – sizeof (size_t));
/* Не из конца ли пула этот кусок? */ assert (pthread_mutex_lock( &pool_mutex) == 0); if (((char *) pt + size) == (mem_pool + cur_pool_size)) { pt->length = 0; cur_pool_size -= size; assert (pthread_mutex_unlock( &pool_mutex) == 0); return; } assert (pthread_mutex_unlock( &pool_mutex) == 0);
/* Добавим освободившийся кусок */ /* к одному из списков */ if (ls < DIF_SIZES) { assert (pthread_mutex_lock( &short_lists_mutex) == 0); pt->pnext = short_lists [ls]; short_lists [ls] = pt; assert (pthread_mutex_unlock( &short_lists_mutex) == 0); } else { /* Добавим к большому списку */ assert (pthread_mutex_lock( &big_list_mutex) == 0); pt->pnext = big_list; big_list = pt; assert (pthread_mutex_unlock( &big_list_mutex) == 0); } }
Листинг 2.8. Исходный текст функций выделения и освобождения памяти в многопотоковой среде.
Основная идея реализации состоит в том, что, помимо начального пула, поддерживаются списки свободных кусков памяти – отдельные списки для небольших размеров (чтобы ускорить их отведение и возврат и уменьшить фрагментацию памяти) и общий список для прочих (больших) кусков. Из общего списка выбирается первый подходящий элемент.
С каждым из трех источников выделения памяти (пул, списки коротких, список больших кусков) ассоциирован свой мьютекс. Цель подобного разделения – минимизировать время владения мьютексом. Только при операциях с общим списком оно может быть большим.
Для библиотечных функций удобны предоставляемые стандартом POSIX-2001 средства инициализации статически описанных мьютексов.
На листинге 2.9 показана тестовая многопотоковая программа, а на листинге 2.10 – возможные результаты ее работы.
Видно, что выполнение потоков управления чередуется, и потоки влияют друг на друга.
#include
static void *start_func (void *dummy) { void *p1, *p2, *p3;
printf ("g_malloc (65000): %p\n", p1 = g_malloc (65000)); sleep (1); printf ("g_malloc (1): %p\n", p2 = g_malloc (1)); sleep (1); g_free (p1); sleep (1); g_free (p2); sleep (1); printf ("g_malloc (64990): %p\n", p1 = g_malloc (64990)); sleep (1); printf ("g_malloc (1): %p\n", p2 = g_malloc (1)); sleep (1); printf ("g_malloc (5): %p\n", p3 = g_malloc (5)); sleep (1); g_free (p1); sleep (1); g_free (p2); sleep (1); g_free (p3); sleep (1); printf ("g_malloc (100000): %p\n", p3 = g_malloc (100000));
return (NULL); }
int main (void) { pthread_t pt1, pt2;
pthread_create (&pt1, NULL, start_func, NULL); pthread_create (&pt2, NULL, start_func, NULL);
pthread_join (pt1, NULL); pthread_join (pt2, NULL);
return (0); }
Листинг 2.9. Пример программы, использующей функции выделения и освобождения памяти в многопотоковой среде.
g_malloc (65000): 0x8049024 g_malloc (65000): (nil) g_malloc (1): 0x8058e10 g_malloc (1): 0x8058e18 g_malloc (64990): 0x804902c g_malloc (64990): (nil) g_malloc (1): 0x8049024 g_malloc (1): 0x8058e10 g_malloc (5): 0x8058e18 g_malloc (5): 0x8058e24 g_malloc (100000): (nil) g_malloc (100000): (nil)
Листинг 2.10. Возможные результаты работы программы, использующей функции выделения и освобождения памяти в многопотоковой среде.
Мьютексы – весьма подходящее средство для реализации обеда философов. Ниже приведена программа, написанная С.В. Самборским (см. листинг 2.11).
/* Обедающие философы. Многопотоковая реализация с помощью мьютексов. Запуск: mudrecMutex [-a | -p | -I] [-t число_секунд] имя_философа ...
Опции: -t число_секунд – сколько секунд моделируется
Стратегии захвата вилок: -a – сначала захватывается вилка с меньшим номером; -p – сначала захватывается нечетная вилка; -I – некорректная (но эффективная) интеллигентная стратегия: во время ожидания уже захваченная вилка кладется.
Пример запуска: mudrecMutex -p -t 300 A B C D E F G\ H I J K L M N\ O P Q R S T U\ V W X Y Z */
static char rcsid[] __attribute__((unused)) = \ "$Id: mudrecMutex.c,v 1.14 2003/11/20 16:09:20" "sambor Exp $";
#include
#define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)>(b)?(b):(a))
struct mudrec { char *name; int left_fork, right_fork; int eat_time, wait_time, think_time, max_wait_time; int count; pthread_t thread; int private_pFdIn; } *kafedra;
/* Глобальные счетчики и логические переменные */ int Stop = 0; /* Признак конца обеда */
/* Различные дескрипторы */ int protokol [2] = {-1, -1}; #define pFdIn (protokol [1]) #define pFdOut (protokol [0])
/* Массив мьютексов для синхронизации доступа */ /* к вилкам */ pthread_mutex_t *mtxFork;
/* Разные алгоритмы захвата вилок */ static void get_forks_simple ( struct mudrec *this); static void get_forks_odd ( struct mudrec *this); static void get_forks_maybe_infinit_time( struct mudrec *this);
/* Используемый метод захвата вилок */ void (*get_forks) (struct mudrec *this) = get_forks_simple;
/* Возвращение вилок */ static void put_forks (struct mudrec *this);
/*Потоки-философы */ void *filosof (void *arg) { struct mudrec *this = arg; char buffer [LINE_MAX]; int bytes; int private_pFdIn = this->private_pFdIn;
while (!Stop) { /* Пора подкрепиться */ { int wait_time, tm = time (NULL);
bytes = write ( private_pFdIn, buffer, strlen (buffer));
(*get_forks) (this);
wait_time = time (NULL) – tm; this->wait_time += wait_time; this->max_wait_time = max (wait_time, this->max_wait_time);
sprintf( buffer, "%s: ждал вилок %d сек\n", this->name, wait_time); bytes = write (private_pFdIn, buffer, strlen (buffer)); }
/* Может, обед уже закончился? */ if (Stop) { put_forks (this); break; }
/* Ест */ { int eat_time = rand () % 20 + 1;
sleep (eat_time);
this->eat_time += eat_time; this->count++; sprintf (buffer, "%s: ел %d сек\n", this->name, eat_time); bytes = write ( private_pFdIn, buffer, strlen (buffer)); }
/* Отдает вилки */ put_forks (this);
if (Stop) break;
/* Размышляет */ { int think_time = rand () % 10 + 1;
sleep (think_time); this->think_time += think_time; } } /* while (!Stop) */
sprintf (buffer,"%s: уходит\n", this->name); bytes = write (private_pFdIn, buffer, strlen (buffer));
close (private_pFdIn);
return (NULL); } /* Поток-философ */
/* Кладет вилки одну за другой */ static void put_forks (struct mudrec *this) { pthread_mutex_unlock ( &mtxFork [this->left_fork – 1]); pthread_mutex_unlock ( &mtxFork [this->right_fork – 1]); }
/* Берет вилки по очереди в порядке номеров */ static void get_forks_simple( struct mudrec *this) { int first = min ( this->left_fork, this->right_fork); int last = max ( this->left_fork, this->right_fork); pthread_mutex_lock ( &mtxFork [first – 1]); pthread_mutex_lock ( &mtxFork [last – 1]); }
/* Берем сначала нечетную вилку */ /* (если обе нечетные – */ /* то с большим номером) */ static void get_forks_odd (struct mudrec *this) { int left = this->left_fork, right = this->right_fork;
int first; int last;
if ((left & 1) > (right & 1)) { first = left; last = right; } else if ((left & 1) < (right & 1)) { first = right; last = left; } else { first = max (left, right); last = min (left, right); }
pthread_mutex_lock ( &mtxFork [first – 1]); pthread_mutex_lock ( &mtxFork [last – 1]); }
/* Берем вилки по очереди, в */ /* произвольном порядке. */ /* Но если вторая вилка не */ /* берется сразу, то кладем */ /* первую. */ /* То есть философ не расходует */ /* вилочное время впустую. */ static void get_forks_maybe_infinit_time (struct mudrec *this) { int left = this->left_fork, right = this->right_fork;
for (;;) { pthread_mutex_lock ( &mtxFork [left – 1]); if (0 == pthread_mutex_trylock (&mtxFork [right – 1])) return; pthread_mutex_unlock ( &mtxFork [left – 1]); pthread_mutex_lock ( &mtxFork [right – 1]); if (0 == pthread_mutex_trylock (&mtxFork [left – 1])) return; pthread_mutex_unlock ( &mtxFork [right – 1]); } }
/* Мелкие служебные функции */ static void stop (int dummy) { Stop = 1; }
static void usage (char name []) { fprintf (stderr, "Использование: %s" " [-a | -p | -I] " "[-t число_секунд] " "имя_философа ...\n", name); exit (1); }
/* Точка входа демонстрационной программы */ int main (int argc, char *argv []) { char buffer [LINE_MAX], *p; int i, n, c; int open_room_time = 300; int nMudr; struct sigaction sact;
while ((c = getopt (argc, argv, "apIt:")) != -1) { (c) { case 'a': get_forks = get_forks_simple; break; case 'p': get_forks = get_forks_odd; break; case 'I': get_forks = get_forks_maybe_infinit_time; break; case 't': open_room_time = strtol( optarg, &p, 0); if (optarg [0] == 0 || *p != 0) usage (argv [0]); break; default : usage (argv [0]); } }
nMudr = argc – optind; /* Меньше двух */ /* философов неинтересно ... */ if (nMudr < 2) usage (argv [0]);
/* Создание канала для протокола */ /* обработки событий */ pipe (protokol); kafedra = calloc (sizeof (struct mudrec), nMudr);
/* Зачисление на кафедру */ for (i = 0; i < nMudr; i++, optind++) { kafedra [i].name = argv [optind]; /* Выдадим телефон */ kafedra [i].private_pFdIn = fcntl (pFdIn, F_DUPFD, 0);
/* Укажем новичку, */ /* какими вилками */ /* пользоваться */ kafedra [i].left_fork = i + 1; kafedra [i].right_fork = i + 2; } /* Последний */ /* пользуется вилкой первого */ kafedra [nMudr – 1].right_fork = 1;
/* Зададим реакцию на сигналы */ /* и установим будильник */ /* на конец обеда */ sact.sa_handler = stop; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGINT, &sact, (struct sigaction *) NULL); (void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL); alarm (open_room_time);
/* Создадим мьютексы для охраны вилок */ mtxFork = calloc ( sizeof (pthread_mutex_t), nMudr); for (i = 0; i < nMudr; i++) { pthread_mutex_init ( &mtxFork [i], NULL); }
/* Философы входят в столовую */ for (i = 0; i < nMudr; i++) pthread_create ( &kafedra [i].thread, NULL, &filosof, (void *) &kafedra [i]);
/* Выдача сообщений на стандартный */ /* вывод и выход */ /* после окончания всех задач */ close (pFdIn); while (1) { n = read (pFdOut, buffer, LINE_MAX); if (n == 0 || (n < 0 && errno != EINTR)) break; for (i = 0; i < n; i++) putchar (buffer [i]); } close (pFdOut);
/* Уничтожение мьютексов */ for (i = 0; i < nMudr; i++) { pthread_mutex_destroy ( &mtxFork [i]); }
/* Выдача сводной информации */ { int full_eating_time = 0; int full_waiting_time = 0; int full_thinking_time = 0; for (i = 1; i <= nMudr; i++) { struct mudrec *this = &kafedra [i – 1];
full_eating_time += this->eat_time; full_waiting_time += this->wait_time; full_thinking_time += this->think_time;
if (this->count > 0) { float count = this->count; float think_time = this->think_time / count; float eat_time = this->eat_time / count; float wait_time = this->wait_time / count;
printf ( "%s: ел %d раз в " "среднем: думал=%.1f " "ел=%.1f ждал=%.1f " "(максимум %d)\n", this->name, this->count, think_time,eat_time, wait_time, this->max_wait_time); } else printf ("%s: не поел\n", this->name); } /* for */
{ float total_time = ( full_eating_time + full_waiting_time + full_thinking_time) / (float) nMudr; printf("Среднее число одновременно" " едящих = %.3f\n", full_eating_time / total_time); printf("Среднее число одновременно" " ждущих = %.3f\n", full_waiting_time / total_time); } } /* Выдача сводной информации */
free (mtxFork); free (kafedra);
/* Сообщим об окончании работы. */ printf ("Конец обеда\n");
return 0; }
Листинг 2.11.Многопотоковый вариант решения задачи об обедающих философах с использованием мьютексов.
Особенности синхронизации потоков управления
По сравнению с процессами, потоки управления характеризуются двумя особенностями:
Очевидно, чтобы быть практически полезными, средства синхронизации, специально ориентированные на потоки управления, должны быть оптимизированы с учетом обеих отмеченных особенностей.
К числу таких средств, присутствующих в стандарте POSIX-2001, принадлежат мьютексы, переменные условия, блокировки чтение-запись, спин-блокировки и барьеры.
Мьютекс – это синхронизирующий объект, использование которого позволяет множеству потоков управления упорядочить доступ к разделяемым данным. Название этого средства синхронизации отражает его функциональность – взаимное исключение (mutual-exclusion). Поток захватывает мьютекс в монопольное владение и остается владельцем, пока сам же его не освободит.
Переменная условия в качестве синхронизирующего объекта дает потокам управления возможность многократно приостанавливать выполнение, пока некий ассоциированный предикат (условие) не станет истинным. Говорят, что поток, выполнение которого приостановлено на переменной условия, блокирован на этой переменной.
Блокировки чтение-запись (много читателей или один писатель) в каждый момент времени позволяют нескольким потокам управления одновременно иметь к данным доступ на чтение или только одному потоку – доступ на запись. Естественно, подобные блокировки обычно применяют для защиты данных, которые читаются чаще, чем изменяются.
Спин-блокировки представляют собой низкоуровневое средство синхронизации. Как и мьютексы, они предназначены для упорядочения доступа множества потоков управления к разделяемым данным.
Барьеры предназначены для синхронизации множества потоков управления в определенной точке их выполнения.
Средства синхронизации могут использоваться для достижения двух существенно разных целей:
Мьютексы и блокировки можно отнести к первой из выделенных категорий, переменные условия и барьеры – ко второй.
Вообще говоря, средства синхронизации потоков управления можно использовать не только в рамках одного процесса, но и среди множества процессов, расположенных в разделяемой, доступной на запись памяти, соответствующим образом инициализированной.
Со средствами синхронизации потоков управления ассоциируются атрибуты и атрибутные объекты, которые можно опрашивать и/или изменять аналогично тому, как это делается для самих потоков.
Переменные условия
Пусть имеется некоторый предикат (условие), зависящий от значений переменных, разделяемых несколькими потоками управления. Совместное использование мьютексов, переменных условия и обслуживающих их функций позволяет организовать экономное ожидание состояния истинности этого предиката.
Общая схема применения переменных условия выглядит следующим образом. С разделяемыми переменными, фигурирующими в предикате, ассоциируется мьютекс, который необходимо захватить перед началом проверок. Затем поток управления входит в цикл вида
while (! предикат) { Ожидание на переменной условия с освобождением мьютекса. После успешного завершения ожидания поток вновь оказывается владельцем мьютекса. }
После нормального выхода из цикла проверяемое условие истинно; можно выполнить требуемые действия и освободить мьютекс.
Разблокирование потоков управления, ожидающих на переменной условия, должен обеспечить другой поток, изменивший значения разделяемых переменных и отправивший ждущим соответствующее уведомление. Вообще говоря, не гарантируется, что в момент разблокирования проверяемое условие истинно, поэтому ожидание и следует обертывать в цикл, делая его потенциально многократным.
По логике применения переменные условия напоминают семафоры: одни потоки ждут их перехода в некоторое состояние, другие своими действиями должны этот переход обеспечить и, тем самым, разблокировать ждущих. Есть, однако, и принципиальное отличие. Содержательный предикат, истинность которого является целью ожидания, ассоциируется с семафором неявно и дополнительно не проверяется; считается, что прекращение ожидания само по себе является свидетельством истинности. Это значит, что ответственность за корректность синхронизирующих действий разделяется между оперирующими с семафором потоками управления со всеми вытекающими отсюда последствиями. Для переменных условия прекращение ожидания гарантий истинности предиката не дает, его приходится в явном виде записывать в заголовке цикла, что делает программу устойчивее и упрощает анализ ее корректности.
Отметим также, что все действия с разделяемыми переменными, которые производятся при поддержке переменных условия, осуществляются при захваченном мьютексе и, тем самым, оказываются потоково-безопасными. Если же попытаться дополнить содержательными проверками операции с семафорами, о синхронизации доступа к разделяемым переменным придется позаботиться отдельно.
Переходя к описанию функций, предлагаемых стандартом POSIX-2001 для обслуживания переменных условия, укажем, что их можно разделить на следующие группы:
#include
int pthread_cond_init ( pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy ( pthread_cond_t *cond);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Листинг 2.12. Описание функций инициализации и разрушения переменных условия. (html, txt)
#include
int pthread_cond_wait ( pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait ( pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, struct timespec *restrict abstime);
Листинг 2.13. Описание функций блокирования на переменной условия. (html, txt)
#include
int pthread_cond_broadcast ( pthread_cond_t *cond);
int pthread_cond_signal ( pthread_cond_t *cond);
Листинг 2.14. Описание функций разблокирования потоков управления, блокированных на переменной условия. (html, txt)
#include
int pthread_condattr_init ( pthread_condattr_t *attr);
int pthread_condattr_destroy ( pthread_condattr_t *attr);
Листинг 2.15. Описание функций инициализации и разрушения атрибутных объектов переменных условия. (html, txt)
#include
int pthread_condattr_getpshared ( const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared ( pthread_condattr_t *attr, int pshared);
int pthread_condattr_getclock ( const pthread_condattr_t *restrict attr, clockid_t *restrict clock_id);
int pthread_condattr_setclock ( pthread_condattr_t *attr, clockid_t clock_id);
Листинг 2.16. Описание функций опроса и установки атрибутов переменных условия в атрибутных объектах. (html, txt)
Для функции pthread_cond_timedwait() к приведенному перечню добавляется еще и срабатывание контроля по времени. Отметим, впрочем, что и в этом случае мьютекс передается во владение вызывающему потоку управления, а предикат может оказаться истинным (пока срабатывал контроль по времени, другой поток изменил значения разделяемых переменных).
По целому ряду причин контроль по времени целесообразно оформлять с помощью абсолютных, а не относительных значений. Вероятно, главная из них состоит в том, что целью является не успешный возврат из функции pthread_cond_timedwait(), а истинность предиката, то есть завершение цикла, показанного на листинге 2.17.
rc = 0; while (! predicate && rc == 0) { rc = pthread_cond_timedwait ( &cond, &mutex, &ts); }
Листинг 2.17. Типичный цикл ожидания на переменной условия с контролем по времени.
Если бы аргумент ts задавался как интервал, его пришлось бы перевычислять в цикле; кроме того, контроль по времени оказался бы смазанным непредсказуемыми задержками в выполнении потока управления.
Ожидание на переменной условия является точкой терминирования потока управления. Перед вызовом первого обработчика завершения поток вновь становится владельцем мьютекса, как если бы ожидание завершилось обычным образом, но вместо возврата из функции началось выполнение действий по терминированию, причем в том же состоянии, в каком выполняется код в окрестности обращения к функции. Это делает безопасным доступ обработчиков завершения к разделяемым переменным и вообще упрощает их написание, что способствует уменьшению числа возможных ошибок.
Функция pthread_cond_broadcast() разблокирует все потоки управления, ждущие на переменной условия (это полезно, например, когда писатель уступает место читателям), а функция pthread_cond_signal() (не генерирующая, вопреки названию, никаких сигналов) – по крайней мере один из них (если таковые вообще имеются). Порядок разблокирования определяется политикой планирования; борьба за владение мьютексом, заданным в начале ожидания, протекает так же, как и при одновременном вызове pthread_mutex_lock (mutex).
Вообще говоря, поток управления, вызвавший pthread_cond_broadcast() или pthread_cond_signal(), не обязан быть владельцем упомянутого мьютекса; тем не менее, если требуется предсказуемость решений планировщика, это условие должно быть выполнено.
Реализация должна гарантировать, что поток управления, ожидавший на переменной условия и разблокированный в результате получения заказа на терминирование, не "потребит" разрешение на разблокирование, сгенерированное функцией pthread_cond_signal(), если имеются другие потоки, ожидающие на той же переменной условия. Без подобной гарантии конкуренция между терминированием и разблокированием может завершиться тупиковой ситуацией.
Не рекомендуется вызывать pthread_cond_signal() (а также освобождать мьютексы) в (асинхронно выполняемых) функциях обработки сигналов.
В качестве примера использования переменных условия и мьютексов приведем прямолинейное, но вполне практичное многопотоковое решение задачи об обедающих философах (см. листинг 2.18).
/* * * * * * * * * * * * * * * * * */ /* Многопотоковый вариант обеда */ /* философов с использованием */ /* мьютексов и переменных условия */ /* * * * * * * * * * * * * * * * * */
#include
/* Число обедающих философов */ #define QPH 5
/* Время (в секундах) на обед */ #define FO 15
/* Длительность еды */ #define ernd (rand () % 3 + 1)
/* Длительность разговора */ #define trnd (rand () % 5 + 1)
/* Состояние вилок */ static int fork_busy [QPH] = {0, };
/* Синхронизирующая переменная условия */ static pthread_cond_t forks_cond = PTHREAD_COND_INITIALIZER;
/* Синхронизирующий мьютекс */ static pthread_mutex_t forks_mutex = PTHREAD_MUTEX_INITIALIZER;
/* * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока-философа. */ /* Аргумент – номер философа */ /* * * * * * * * * * * * * * * * * * */ void *start_phil (void *no) { /* Время до конца обеда */ int fo; /* Время очередного отрезка */ /* еды или беседы */ int t;
fo = FO; while (fo > 0) { /* Обед */
/* Философ говорит */ printf ("Философ %d беседует\n", (int) no); t = trnd; sleep (t); fo -= t;
/* Пытается взять вилки */ (void) pthread_mutex_lock ( &forks_mutex); while (fork_busy [(int) no – 1] || fork_busy [(int) no % QPH] ) { (void) pthread_cond_wait ( &forks_cond, &forks_mutex); } fork_busy [(int) no – 1] = fork_busy [(int) no % QPH] = 1; (void) pthread_mutex_unlock ( &forks_mutex);
/* Ест */ printf ("Философ %d ест\n", (int) no); t = ernd; sleep (t); fo -= t;
/* Отдает вилки */ (void) pthread_mutex_lock ( &forks_mutex); fork_busy [(int) no – 1] = fork_busy [(int) no % QPH] = 0; (void) pthread_cond_broadcast (&forks_cond); (void) pthread_mutex_unlock (&forks_mutex); } /* while */
printf ("Философ %d закончил обед\n", (int) no); return (NULL); }
/* * * * * * * * * * * * * * * */ /* Создание потоков-философов */ /* и ожидание их завершения */ /* * * * * * * * * * * * * * * */ int main (void) { /* Массив идентификаторов */ /* потоков-философов */ pthread_t pt_id [QPH]; /* Номер философа */ int no; /* Атрибутный объект для */ /* создания потоков */ pthread_attr_t attr_obj;
if ((errno = pthread_attr_init( &attr_obj)) != 0) { perror ("PTHREAD_ATTR_INIT"); return (errno); }
/* В очередь, господа */ /* философы, в очередь! */ if ((errno = pthread_attr_setschedpolicy( &attr_obj, SCHED_FIFO)) != 0) { perror ( "PTHREAD_ATTR_SETSCHEDPOLICY"); return (errno); }
/* Все – к столу */ for (no = 1; no <= QPH; no++) { if ((errno = pthread_create ( &pt_id [no – 1], &attr_obj, start_phil, (void *) no)) != 0) { perror ( "PTHREAD_CREATE"); return (no); } }
/* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) pthread_join ( pt_id [no – 1], NULL); }
return 0; }
Листинг 2.18. Пример многопотоковой реализации обеда философов с использованием мьютексов и переменных условия.
Результаты работы этой программы могут выглядеть так, как показано на листинге 2.19.
Философ 1 беседует Философ 2 беседует Философ 3 беседует Философ 4 беседует Философ 5 беседует Философ 4 ест Философ 2 ест Философ 4 беседует Философ 5 ест Философ 2 беседует Философ 3 ест Философ 5 беседует Философ 1 ест Философ 3 беседует Философ 4 ест Философ 1 беседует Философ 2 ест Философ 2 беседует Философ 1 ест Философ 4 беседует Философ 3 ест Философ 1 беседует Философ 5 ест Философ 5 беседует Философ 3 беседует Философ 4 ест Философ 2 ест Философ 4 беседует Философ 2 беседует Философ 1 ест Философ 3 ест Философ 1 закончил обед Философ 5 ест Философ 3 закончил обед Философ 2 ест Философ 5 закончил обед Философ 4 ест Философ 2 закончил обед Философ 4 закончил обед
Листинг 2.19. Возможные результаты работы многопотоковой программы, моделирующей обед философов с использованием мьютексов и переменных условия.
Несмотря на то, что в приведенной программе используется глобальный (общий для всех философов и вилок) мьютекс, это не приведет к лишним задержкам, так как мьютекс служит не для захвата вилок, а лишь для проверки и изменения их состояния. Практически всегда он будет свободен.
В употреблении глобальной (в том же смысле, что и мьютекс) переменной условия и всеобщем разблокировании ждущих (посредством вызова pthread_cond_broadcast()) при освобождении любой пары вилок также нет ничего плохого. Ресурсов это практически не отнимает, а пока до разбуженного философа дойдет очередь, нужные ему вилки на самом деле могут оказаться свободными, даже если ожидание на переменной условия было прервано не из-за них. В то же время, кажущееся предпочтительным создание соответствующего числа переменных условия и избирательное разблокирование соседей насытившегося философа двумя вызовами pthread_cond_signal() на самом деле не позволяет гарантировать отсутствие ложных пробуждений, так как из-за нюансов планирования нужную вилку могут выхватить буквально из-под носа (или из-под руки).
Обратим внимание на то, что цикл, в который заключено ожидание на переменной условия, в данном случае использован для захвата нескольких ресурсов.Это напоминает описанные в курсе [1] групповые операции с семафорами, если иметь в виду цикл в целом и отвлечься от ложных разблокирований.
Разумеется, приведенное решение задачи об обедающих философах является нечестным, поскольку во время ожидания на переменной условия, которое длится неопределенно долго, философ "отключается", он не беседует и не ест, что, по условию задачи, запрещено (этим недостатком страдает и программа из курса [1], основанная на групповых операциях с семафорами). Однако как иллюстрация типичных способов работы с мьютексами и переменными условия данная программа имеет право на существование.
restrict mutex, const pthread_mutexattr_t
|
#include int pthread_mutex_init ( pthread_mutex_t * restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy ( pthread_mutex_t *mutex); pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; |
| Листинг 2.1. Описание функций инициализации и разрушения мьютексов. |
| Закрыть окно |
|
#include int pthread_mutex_lock ( pthread_mutex_t *mutex); int pthread_mutex_trylock ( pthread_mutex_t *mutex); int pthread_mutex_unlock ( pthread_mutex_t *mutex); |
| Листинг 2.2. Описание функций захвата и освобождения мьютексов. |
| Закрыть окно |
|
#include |
| Листинг 2.3. Описание функции захвата мьютексов с ограниченным ожиданием. |
| Закрыть окно |
|
#include int pthread_mutex_getprioceiling ( const pthread_mutex_t *restrict mutex, int *restrict prioceiling); int pthread_mutex_setprioceiling ( pthread_mutex_t * restrict mutex, int prioceiling, int *restrict old_ceiling); |
| Листинг 2.4. Описание функций опроса и установки атрибутов мьютекса. |
| Закрыть окно |
|
#include int pthread_mutexattr_init ( pthread_mutexattr_t *attr); int pthread_mutexattr_destroy ( pthread_mutexattr_t *attr); |
| Листинг 2.5. Описание функций инициализации и разрушения атрибутных объектов мьютексов. |
| Закрыть окно |
|
#include int pthread_mutexattr_gettype ( const pthread_mutexattr_t *restrict attr, int *restrict type); int pthread_mutexattr_settype ( pthread_mutexattr_t *attr, int type); int pthread_mutexattr_getpshared ( const pthread_mutexattr_t *restrict attr, int *restrict pshared); int pthread_mutexattr_setpshared ( pthread_mutexattr_t *attr, int pshared); int pthread_mutexattr_getprotocol ( const pthread_mutexattr_t *restrict attr, int *restrict protocol); int pthread_mutexattr_setprotocol ( *attr, int protocol); int pthread_mutexattr_getprioceiling ( const pthread_mutexattr_t *restrict attr, int *restrict prioceiling); int pthread_mutexattr_setprioceiling ( pthread_mutexattr_t *attr, int prioceiling); |
| Листинг 2.6. Описание функций опроса и установки атрибутов мьютекса в атрибутных объектах. |
| Закрыть окно |
|
#ifndef g_MALLOC #define g_MALLOC /* Количество размеров (в словах типа size_t), */ /* для которых поддерживаются */ /* разные списки памяти */ #define DIF_SIZES 8 /* Размер пула памяти */ #define POOL_SIZE 65536 /* Указатель на кусок памяти нулевого размера */ #define g_NULL ((void *) (-1)) /* Первое поле следующей структуры нужно /* для всех кусков памяти, а второе – */ /* только для провязки свободных.*/ /* При отведении памяти адрес второго */ */ поля выдается как результат */ typedef struct listi { size_t length; struct listi *pnext; } *list_of_mem; extern void *g_malloc (size_t); extern void g_free (void *); #endif |
| Листинг 2.7. Заголовочный файл g_malloc.h для функций выделения и освобождения памяти в многопотоковой среде. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * */ /* Функции выделения и освобождения памяти */ /* * * * * * * * * * * * * * * * * * * * * */ #include static char mem_pool [POOL_SIZE] = {0, }; /* Размер занятой части пула памяти */ /* Списки свободного пространства */ /* (по одному на каждый размер */ /* от 1 до DIF_SIZES) */ static size_t cur_pool_size = 0; static list_of_mem short_lists [DIF_SIZES] = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}; /* Список больших */ /* свободных кусков (превосходящих DIF_SIZES) */ /* Разные мьютексы для разных */ /* групп списков свободного пространства */ static list_of_mem big_list = NULL; static pthread_mutex_t short_lists_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t big_list_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t pool_mutex = PTHREAD_MUTEX_INITIALIZER; /* * * * * * * * * * */ /* Выделение памяти */ /* * * * * * * * * * */ void *g_malloc (size_t size) { /* Указатель для хождения по списку */ /* больших свободных кусков */ list_of_mem *p; /* Указатель для хождения по спискам */ /* свободных кусков */ list_of_mem pt; /* Индекс в массиве short_lists */ size_t ls; size_t ts; /* Временная переменная */ if (size == 0) { return (g_NULL); /* Важно, чтобы результат был */ /* отличен от NULL, поскольку NULL */ /* – признак ненормального завершения */ } /* Округлим запрошенный размер вверх */ /* до кратного размеру size_t и */ /* прибавим слово служебной информации */ size = (size – 1 + 2 * sizeof (size_t)) & ~(sizeof (size_t) – 1); /* Вычислим индекс в массиве */ /* short_lists [], соответствующий */ /* запрошенному размеру */ ls = size / sizeof (size_t) – 2; if (ls < DIF_SIZES) { /* Попробуем выдать кусок */ /* из списка коротких */ assert ( pthread_mutex_lock( &short_lists_mutex) == 0); if ((pt = short_lists [ls]) != NULL) { /* Есть нужный кусок */ short_lists [ls] = (short_lists [ls])->pnext; assert (pthread_mutex_unlock( &short_lists_mutex) == 0); return (&pt->pnext); } assert (pthread_mutex_unlock( &short_lists_mutex) == 0); } /* Попробуем выдать кусок из */ /* списка больших */ assert (pthread_mutex_lock( &big_list_mutex) == 0); for (p = &big_list, pt = *p; pt != NULL; p = &pt->pnext, pt = *p) { if ((signed long) (ts = pt->length – size) >= 0) { /* Нашли подходящий кусок */ if (ts < sizeof (*pt)) { /* Придется выдать кусок целиком – */ /* в остатке не помещается */ /* служебная информация */ *p = pt->pnext; } else { /* Отрежем сколько надо и, */ /* при необходимости, */ /* перецепим остаток в */ /* список коротких */ if ((ls = (pt->length = ts) / sizeof (size_t) – 2) < DIF_SIZES) { *p = pt->pnext; assert (pthread_mutex_lock( &short_lists_mutex) == 0); pt->pnext = short_lists [ls]; short_lists [ls] = pt; assert (pthread_mutex_unlock( &short_lists_mutex) == 0); } pt = (list_of_mem) ((char *) pt + ts); pt->length = size; } assert ( pthread_mutex_unlock( &big_list_mutex) == 0); return (&pt->pnext); } } /* for */ assert (pthread_mutex_unlock ( &big_list_mutex) == 0); /* Кусок из большого списка */ /* выдать не удалось. */ /* Попробуем взять прямо из */ /* пула памяти */ assert (pthread_mutex_lock( &pool_mutex) == 0); if (cur_pool_size + size <= POOL_SIZE) { pt = (list_of_mem) (mem_pool + cur_pool_size); pt->length = size; cur_pool_size += size; assert (pthread_mutex_unlock ( &pool_mutex) == 0); return (&pt->pnext); } assert (pthread_mutex_unlock( &pool_mutex) == 0); /* Неудача при выделении памяти */ errno = ENOMEM; return (NULL); } /* * * * * * * * * * * * * * * * * * */ /* Возврат ранее запрошенной памяти */ /* * * * * * * * * * * * * * * * * * */ void g_free (void *p) { list_of_mem pt; size_t size, ls; if ((p == g_NULL) || (p == NULL)) { return; } /* Установим указатель на */ /* служебную информацию */ pt = (list_of_mem) ((char *) p – sizeof (size_t)); size = pt->length; ls = size / sizeof (size_t) – 2; memset (p, 0, size – sizeof (size_t)); /* Не из конца ли пула этот кусок? */ assert (pthread_mutex_lock( &pool_mutex) == 0); if (((char *) pt + size) == (mem_pool + cur_pool_size)) { pt->length = 0; cur_pool_size -= size; assert (pthread_mutex_unlock( &pool_mutex) == 0); return; } assert (pthread_mutex_unlock( &pool_mutex) == 0); /* Добавим освободившийся кусок */ /* к одному из списков */ if (ls < DIF_SIZES) { assert (pthread_mutex_lock( &short_lists_mutex) == 0); pt->pnext = short_lists [ls]; short_lists [ls] = pt; assert (pthread_mutex_unlock( &short_lists_mutex) == 0); } else { /* Добавим к большому списку */ assert (pthread_mutex_lock( &big_list_mutex) == 0); pt->pnext = big_list; big_list = pt; assert (pthread_mutex_unlock( &big_list_mutex) == 0); } } |
| Листинг 2.8. Исходный текст функций выделения и освобождения памяти в многопотоковой среде. |
| Закрыть окно |
|
#include static void *start_func (void *dummy) { void *p1, *p2, *p3; printf ("g_malloc (65000): %p\n", p1 = g_malloc (65000)); sleep (1); printf ("g_malloc (1): %p\n", p2 = g_malloc (1)); sleep (1); g_free (p1); sleep (1); g_free (p2); sleep (1); printf ("g_malloc (64990): %p\n", p1 = g_malloc (64990)); sleep (1); printf ("g_malloc (1): %p\n", p2 = g_malloc (1)); sleep (1); printf ("g_malloc (5): %p\n", p3 = g_malloc (5)); sleep (1); g_free (p1); sleep (1); g_free (p2); sleep (1); g_free (p3); sleep (1); printf ("g_malloc (100000): %p\n", p3 = g_malloc (100000)); return (NULL); } int main (void) { pthread_t pt1, pt2; pthread_create (&pt1, NULL, start_func, NULL); pthread_create (&pt2, NULL, start_func, NULL); pthread_join (pt1, NULL); pthread_join (pt2, NULL); return (0); } |
| Листинг 2.9. Пример программы, использующей функции выделения и освобождения памяти в многопотоковой среде. |
| Закрыть окно |
| g_malloc (65000): 0x8049024 g_malloc (65000): (nil) g_malloc (1): 0x8058e10 g_malloc (1): 0x8058e18 g_malloc (64990): 0x804902c g_malloc (64990): (nil) g_malloc (1): 0x8049024 g_malloc (1): 0x8058e10 g_malloc (5): 0x8058e18 g_malloc (5): 0x8058e24 g_malloc (100000): (nil) g_malloc (100000): (nil) |
| Листинг 2.10. Возможные результаты работы программы, использующей функции выделения и освобождения памяти в многопотоковой среде. |
| Закрыть окно |
|
/* Обедающие философы. Многопотоковая реализация с помощью мьютексов. Запуск: mudrecMutex [-a | -p | -I] [-t число_секунд] имя_философа ... Опции: -t число_секунд – сколько секунд моделируется Стратегии захвата вилок: -a – сначала захватывается вилка с меньшим номером; -p – сначала захватывается нечетная вилка; -I – некорректная (но эффективная) интеллигентная стратегия: во время ожидания уже захваченная вилка кладется. Пример запуска: mudrecMutex -p -t 300 A B C D E F G\ H I J K L M N\ O P Q R S T U\ V W X Y Z */ static char rcsid[] __attribute__((unused)) = \ "$Id: mudrecMutex.c,v 1.14 2003/11/20 16:09:20" "sambor Exp $"; #include #define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)>(b)?(b):(a)) struct mudrec { char *name; int left_fork, right_fork; int eat_time, wait_time, think_time, max_wait_time; int count; pthread_t thread; int private_pFdIn; } *kafedra; /* Глобальные счетчики и логические переменные */ int Stop = 0; /* Признак конца обеда */ /* Различные дескрипторы */ int protokol [2] = {-1, -1}; #define pFdIn (protokol [1]) #define pFdOut (protokol [0]) /* Массив мьютексов для синхронизации доступа */ /* к вилкам */ pthread_mutex_t *mtxFork; /* Разные алгоритмы захвата вилок */ static void get_forks_simple ( struct mudrec *this); static void get_forks_odd ( struct mudrec *this); static void get_forks_maybe_infinit_time( struct mudrec *this); /* Используемый метод захвата вилок */ void (*get_forks) (struct mudrec *this) = get_forks_simple; /* Возвращение вилок */ static void put_forks (struct mudrec *this); /*Потоки-философы */ void *filosof (void *arg) { struct mudrec *this = arg; char buffer [LINE_MAX]; int bytes; int private_pFdIn = this->private_pFdIn; while (!Stop) { /* Пора подкрепиться */ { int wait_time, tm = time (NULL); bytes = write ( private_pFdIn, buffer, strlen (buffer)); (*get_forks) (this); wait_time = time (NULL) – tm; this->wait_time += wait_time; this->max_wait_time = max (wait_time, this->max_wait_time); sprintf( buffer, "%s: ждал вилок %d сек\n", this->name, wait_time); bytes = write (private_pFdIn, buffer, strlen (buffer)); } /* Может, обед уже закончился? */ if (Stop) { put_forks (this); break; } /* Ест */ { int eat_time = rand () % 20 + 1; sleep (eat_time); this->eat_time += eat_time; this->count++; sprintf (buffer, "%s: ел %d сек\n", this->name, eat_time); bytes = write ( private_pFdIn, buffer, strlen (buffer)); } /* Отдает вилки */ put_forks (this); if (Stop) break; /* Размышляет */ { int think_time = rand () % 10 + 1; sleep (think_time); this->think_time += think_time; } } /* while (!Stop) */ sprintf (buffer,"%s: уходит\n", this->name); bytes = write (private_pFdIn, buffer, strlen (buffer)); close (private_pFdIn); return (NULL); } /* Поток-философ */ /* Кладет вилки одну за другой */ static void put_forks (struct mudrec *this) { pthread_mutex_unlock ( &mtxFork [this->left_fork – 1]); pthread_mutex_unlock ( &mtxFork [this->right_fork – 1]); } /* Берет вилки по очереди в порядке номеров */ static void get_forks_simple( struct mudrec *this) { int first = min ( this->left_fork, this->right_fork); int last = max ( this->left_fork, this->right_fork); pthread_mutex_lock ( &mtxFork [first – 1]); pthread_mutex_lock ( &mtxFork [last – 1]); } /* Берем сначала нечетную вилку */ /* (если обе нечетные – */ /* то с большим номером) */ static void get_forks_odd (struct mudrec *this) { int left = this->left_fork, right = this->right_fork; int first; int last; if ((left & 1) > (right & 1)) { first = left; last = right; } else if ((left & 1) < (right & 1)) { first = right; last = left; } else { first = max (left, right); last = min (left, right); } pthread_mutex_lock ( &mtxFork [first – 1]); pthread_mutex_lock ( &mtxFork [last – 1]); } /* Берем вилки по очереди, в */ /* произвольном порядке. */ /* Но если вторая вилка не */ /* берется сразу, то кладем */ /* первую. */ /* То есть философ не расходует */ /* вилочное время впустую. */ static void get_forks_maybe_infinit_time (struct mudrec *this) { int left = this->left_fork, right = this->right_fork; for (;;) { pthread_mutex_lock ( &mtxFork [left – 1]); if (0 == pthread_mutex_trylock (&mtxFork [right – 1])) return; pthread_mutex_unlock ( &mtxFork [left – 1]); pthread_mutex_lock ( &mtxFork [right – 1]); if (0 == pthread_mutex_trylock (&mtxFork [left – 1])) return; pthread_mutex_unlock ( &mtxFork [right – 1]); } } /* Мелкие служебные функции */ static void stop (int dummy) { Stop = 1; } static void usage (char name []) { fprintf (stderr, "Использование: %s" " [-a | -p | -I] " "[-t число_секунд] " "имя_философа ...\n", name); exit (1); } /* Точка входа демонстрационной программы */ int main (int argc, char *argv []) { char buffer [LINE_MAX], *p; int i, n, c; int open_room_time = 300; int nMudr; struct sigaction sact; while ((c = getopt (argc, argv, "apIt:")) != -1) { (c) { case 'a': get_forks = get_forks_simple; break; case 'p': get_forks = get_forks_odd; break; case 'I': get_forks = get_forks_maybe_infinit_time; break; case 't': open_room_time = strtol( optarg, &p, 0); if (optarg [0] == 0 || *p != 0) usage (argv [0]); break; default : usage (argv [0]); } } nMudr = argc – optind; /* Меньше двух */ /* философов неинтересно ... */ if (nMudr < 2) usage (argv [0]); /* Создание канала для протокола */ /* обработки событий */ pipe (protokol); kafedra = calloc (sizeof (struct mudrec), nMudr); /* Зачисление на кафедру */ for (i = 0; i < nMudr; i++, optind++) { kafedra [i].name = argv [optind]; /* Выдадим телефон */ kafedra [i].private_pFdIn = fcntl (pFdIn, F_DUPFD, 0); /* Укажем новичку, */ /* какими вилками */ /* пользоваться */ kafedra [i].left_fork = i + 1; kafedra [i].right_fork = i + 2; } /* Последний */ /* пользуется вилкой первого */ kafedra [nMudr – 1].right_fork = 1; /* Зададим реакцию на сигналы */ /* и установим будильник */ /* на конец обеда */ sact.sa_handler = stop; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGINT, &sact, (struct sigaction *) NULL); (void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL); alarm (open_room_time); /* Создадим мьютексы для охраны вилок */ mtxFork = calloc ( sizeof (pthread_mutex_t), nMudr); for (i = 0; i < nMudr; i++) { pthread_mutex_init ( &mtxFork [i], NULL); } /* Философы входят в столовую */ for (i = 0; i < nMudr; i++) pthread_create ( &kafedra [i].thread, NULL, &filosof, (void *) &kafedra [i]); /* Выдача сообщений на стандартный */ /* вывод и выход */ /* после окончания всех задач */ close (pFdIn); while (1) { n = read (pFdOut, buffer, LINE_MAX); if (n == 0 || (n < 0 && errno != EINTR)) break; for (i = 0; i < n; i++) putchar (buffer [i]); } close (pFdOut); /* Уничтожение мьютексов */ for (i = 0; i < nMudr; i++) { pthread_mutex_destroy ( &mtxFork [i]); } /* Выдача сводной информации */ { int full_eating_time = 0; int full_waiting_time = 0; int full_thinking_time = 0; for (i = 1; i <= nMudr; i++) { struct mudrec *this = &kafedra [i – 1]; full_eating_time += this->eat_time; full_waiting_time += this->wait_time; full_thinking_time += this->think_time; if (this->count > 0) { float count = this->count; float think_time = this->think_time / count; float eat_time = this->eat_time / count; float wait_time = this->wait_time / count; printf ( "%s: ел %d раз в " "среднем: думал=%.1f " "ел=%.1f ждал=%.1f " "(максимум %d)\n", this->name, this->count, think_time,eat_time, wait_time, this->max_wait_time); } else printf ("%s: не поел\n", this->name); } /* for */ { float total_time = ( full_eating_time + full_waiting_time + full_thinking_time) / (float) nMudr; printf("Среднее число одновременно" " едящих = %.3f\n", full_eating_time / total_time); printf("Среднее число одновременно" " ждущих = %.3f\n", full_waiting_time / total_time); } } /* Выдача сводной информации */ free (mtxFork); free (kafedra); /* Сообщим об окончании работы. */ printf ("Конец обеда\n"); return 0; } |
| Листинг 2.11. Многопотоковый вариант решения задачи об обедающих философах с использованием мьютексов. |
| Закрыть окно |
|
#include int pthread_cond_init ( pthread_cond_t * restrict cond, const pthread_condattr_t *restrict attr); int pthread_cond_destroy ( pthread_cond_t *cond); pthread_cond_t cond = PTHREAD_COND_INITIALIZER; |
| Листинг 2.12. Описание функций инициализации и разрушения переменных условия. |
| Закрыть окно |
|
#include int pthread_cond_wait ( pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_timedwait ( pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, struct timespec *restrict abstime); |
| Листинг 2.13. Описание функций блокирования на переменной условия. |
| Закрыть окно |
|
#include int pthread_cond_broadcast ( pthread_cond_t *cond); int pthread_cond_signal ( pthread_cond_t *cond); |
| Листинг 2.14. Описание функций разблокирования потоков управления, блокированных на переменной условия. |
| Закрыть окно |
|
#include int pthread_condattr_init ( pthread_condattr_t *attr); int pthread_condattr_destroy ( pthread_condattr_t *attr); |
| Листинг 2.15. Описание функций инициализации и разрушения атрибутных объектов переменных условия. |
| Закрыть окно |
|
#include int pthread_condattr_getpshared ( const pthread_condattr_t *restrict attr, int *restrict pshared); int pthread_condattr_setpshared ( pthread_condattr_t *attr, int pshared); int pthread_condattr_getclock ( const pthread_condattr_t *restrict attr, clockid_t *restrict clock_id); int pthread_condattr_setclock ( pthread_condattr_t *attr, clockid_t clock_id); |
| Листинг 2.16. Описание функций опроса и установки атрибутов переменных условия в атрибутных объектах. |
| Закрыть окно |
| rc = 0; while (! predicate && rc == 0) { rc = pthread_cond_timedwait ( &cond, &mutex, &ts); } |
| Листинг 2.17. Типичный цикл ожидания на переменной условия с контролем по времени. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * */ /* Многопотоковый вариант обеда */ /* философов с использованием */ /* мьютексов и переменных условия */ /* * * * * * * * * * * * * * * * * */ #include /* Число обедающих философов */ #define QPH 5 /* Время ( в секундах) на обед */ #define FO 15 /* Длительность еды */ #define ernd (rand () % 3 + 1) /* Длительность разговора */ #define trnd (rand () % 5 + 1) /* Состояние вилок */ static int fork_busy [QPH] = {0, }; /* Синхронизирующая переменная условия */ static pthread_cond_t forks_cond = PTHREAD_COND_INITIALIZER; /* Синхронизирующий мьютекс */ static pthread_mutex_t forks_mutex = PTHREAD_MUTEX_INITIALIZER; /* * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока-философа. */ /* Аргумент – номер философа */ /* * * * * * * * * * * * * * * * * * */ void *start_phil (void *no) { /* Время до конца обеда */ int fo; /* Время очередного отрезка */ /* еды или беседы */ int t; fo = FO; while (fo > 0) { /* Обед */ /* Философ говорит */ printf ("Философ %d беседует\n", (int) no); t = trnd; sleep (t); fo -= t; /* Пытается взять вилки */ (void) pthread_mutex_lock ( &forks_mutex); while (fork_busy [(int) no – 1] || fork_busy [(int) no % QPH] ) { (void) pthread_cond_wait ( &forks_cond, &forks_mutex); } fork_busy [(int) no – 1] = fork_busy [(int) no % QPH] = 1; (void) pthread_mutex_unlock ( &forks_mutex); /* Ест */ printf ("Философ %d ест\n", (int) no); t = ernd; sleep (t); fo -= t; /* Отдает вилки */ (void) pthread_mutex_lock ( &forks_mutex); fork_busy [(int) no – 1] = fork_busy [(int) no % QPH] = 0; (void) pthread_cond_broadcast (&forks_cond); (void) pthread_mutex_unlock (&forks_mutex); } /* while */ printf ("Философ %d закончил обед\n", (int) no); return (NULL); } /* * * * * * * * * * * * * * * */ /* Создание потоков-философов */ /* и ожидание их завершения */ /* * * * * * * * * * * * * * * */ int main (void) { /* Массив идентификаторов */ /* потоков-философов */ pthread_t pt_id [QPH]; /* Номер философа */ int no; /* Атрибутный объект для */ /* создания потоков */ pthread_attr_t attr_obj; if ((errno = pthread_attr_init( &attr_obj)) != 0) { perror ("PTHREAD_ATTR_INIT"); return (errno); } /* В очередь, господа */ /* философы, в очередь! */ if ((errno = pthread_attr_setschedpolicy( &attr_obj, SCHED_FIFO)) != 0) { perror ( "PTHREAD_ATTR_SETSCHEDPOLICY"); return (errno); } /* Все – к столу */ for (no = 1; no <= QPH; no++) { if ((errno = pthread_create ( &pt_id [no – 1], &attr_obj, start_phil, (void *) no)) != 0) { perror ( "PTHREAD_CREATE"); return (no); } } /* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) pthread_join ( pt_id [no – 1], NULL); } return 0; } |
| Листинг 2.18. Пример многопотоковой реализации обеда философов с использованием мьютексов и переменных условия. |
| Закрыть окно |
| Философ 1 беседует Философ 2 беседует Философ 3 беседует Философ 4 беседует Философ 5 беседует Философ 4 ест Философ 2 ест Философ 4 беседует Философ 5 ест Философ 2 беседует Философ 3 ест Философ 5 беседует Философ 1 ест Философ 3 беседует Философ 4 ест Философ 1 беседует Философ 2 ест Философ 2 беседует Философ 1 ест Философ 4 беседует Философ 3 ест Философ 1 беседует Философ 5 ест Философ 5 беседует Философ 3 беседует Философ 4 ест Философ 2 ест Философ 4 беседует Философ 2 беседует Философ 1 ест Философ 3 ест Философ 1 закончил обед Философ 5 ест Философ 3 закончил обед Философ 2 ест Философ 5 закончил обед Философ 4 ест Философ 2 закончил обед Философ 4 закончил обед |
| Листинг 2.19. Возможные результаты работы многопотоковой программы, моделирующей обед философов с использованием мьютексов и переменных условия. |
| Закрыть окно |
|
#include int pthread_rwlock_init ( pthread_rwlock_t * restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy ( pthread_rwlock_t *rwlock); |
| Листинг 2.20. Описание функций инициализации и разрушения блокировок чтение-запись. |
| Закрыть окно |
|
#include int pthread_rwlock_rdlock ( pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock ( pthread_rwlock_t *rwlock); |
| Листинг 2.21. Описание функций установки блокировки на чтение. |
| Закрыть окно |
|
#include |
| Листинг 2.22. Описание функции установки блокировки на чтение с ограниченным ожиданием. |
| Закрыть окно |
|
#include int pthread_rwlock_wrlock ( pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock ( pthread_rwlock_t *rwlock); |
| Листинг 2.23. Описание функций установки блокировки на запись. |
| Закрыть окно |
|
#include |
| Листинг 2.24. Описание функции установки блокировки на запись с ограниченным ожиданием. |
| Закрыть окно |
|
#include |
| Листинг 2.25. Описание функции снятия блокировки чтение-запись. |
| Закрыть окно |
|
#include int pthread_rwlockattr_init ( pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy ( pthread_rwlockattr_t *attr); |
| Листинг 2.26. Описание функций инициализации и разрушения атрибутных объектов блокировок. |
| Закрыть окно |
|
#include int pthread_rwlockattr_getpshared ( const pthread_rwlockattr_t *restrict attr, int *restrict pshared); int pthread_rwlockattr_setpshared ( pthread_rwlockattr_t *attr, int pshared); |
| Листинг 2.27. Описание функций опроса и установки атрибутов блокировок в атрибутных объектах. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * */ /* Типы, структуры и функции */ /* для многопотоковой работы */ /* со списками */ /* * * * * * * * * * * * * * */ #ifndef g_LIST #define g_LIST /* Указатель на процедуру */ typedef void (*aproc) (void *, void *); /* Указатель на целочисленную функцию */ typedef int (*afun) (void *, void *); /* Элемент списка указателей */ /* на объекты произвольного типа */ typedef struct g_link { /* Ссылка на следующий элемент списка */ struct g_link *pnext; /* Ссылка на объект */ void *pvalue; } *g_linkP; /* Список указателей на объекты */ /* произвольного типа */ typedef struct g_list { /* Голова списка */ g_linkP head; /* Блокировка, ассоциированная */ pthread_rwlock_t lk; /* со списком */ } *g_listP; /* Инициализировать список */ extern void g_list_init (g_listP); /* Завершить работу со списком */ extern void g_list_destroy (g_listP); /* Вставить новый элемент */ /* в конец списка */ extern void g_list_ins_last ( g_listP, void *); /* Вставить новый элемент в */ /* список перед первым, */ /* удовлетворяющим заданному */ /* свойству (или в конец) */ extern void g_list_ins_fun ( g_listP, void *, afun); /* Удалить элемент из списка */ extern void g_list_del ( g_listP, void *); /* Применить процедуру ко всем */ /* элементам списка */ extern void g_list_forall_x ( g_listP, aproc, void *); /* Выбрать подходящий элемент списка */ extern void *g_list_suchas_x ( g_listP, afun, void *); /* Выдать элемент по номеру */ extern void *g_list_get_bynum ( g_listP, int); #endif |
| Листинг 2.28. Заголовочный файл g_list.h для функций работы со списками в многопотоковой среде. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * */ /* Реализация функций для многопотоковой */ /* работы со списками. */ /* Применяются блокировки чтение-запись */ /* без приостановки выполнения */ /* * * * * * * * * * * * * * * * * * * * */ #define _XOPEN_SOURCE 600 #include /* * * * * * * * * * * * * */ /* Инициализировать список */ /* * * * * * * * * * * * * */ void g_list_init (g_listP plist) { plist->head = NULL; errno = pthread_rwlock_init ( &plist->lk, NULL); } /* * * * * * * * * * * * * * * */ /* Завершить работу со списком */ /* * * * * * * * * * * * * * * */ void g_list_destroy (g_listP plist) { errno = pthread_rwlock_destroy ( &plist->lk); } /* * * * * * * * * * * * * * * * * * * * */ /* Вставить новый элемент в конец списка */ /* * * * * * * * * * * * * * * * * * * * */ void g_list_ins_last (g_listP plist, void *pval) { register g_linkP pt; register g_linkP *p; if ((errno = pthread_rwlock_trywrlock ( &plist->lk)) != 0) { return; } for (p = &plist->head; *p != NULL; p = &(*p)->pnext) ; if ((pt = (g_linkP) malloc (sizeof (*pt))) != NULL) { pt->pnext = NULL; pt->pvalue = pval; *p = pt; } (void) pthread_rwlock_unlock ( &plist->lk); } /* * * * * * * * * * * * * * * * * */ /* Вставить новый элемент в список */ /* перед первым, */ /* удовлетворяющим заданному свойству */ /* (или в конец) */ /* * * * * * * * * * * * * * * * */ void g_list_ins_fun (g_listP plist, void *pval, afun fun) { register g_linkP pt; register g_linkP *p; if ((errno = pthread_rwlock_trywrlock ( &plist->lk)) != 0) { return; } for (p = &plist->head; (*p != NULL) && (fun (pval, (*p)->pnext->pvalue) == 0); p = &(*p)->pnext) ; if ((pt = (g_linkP) malloc ( sizeof (*pt))) != NULL) { pt->pnext = NULL; pt->pvalue = pval; *p = pt; } (void) pthread_rwlock_unlock ( &plist->lk); } /* * * * * * * * * * * * * * */ /* Удалить элемент из списка */ /* * * * * * * * * * * * * * */ void g_list_del (g_listP plist, void *pval) { register g_linkP pt; register g_linkP *p; if ((errno = pthread_rwlock_trywrlock (&plist->lk)) != 0) { return; } for (p = &plist->head; *p != NULL; p = &(*p)->pnext) { if ((*p)->pvalue == pval) { pt = *p; *p = pt->pnext; free (pt); errno = pthread_rwlock_unlock( &plist->lk); return; } } /* Пытаемся удалить */ /* несуществующий элемент */ (void) pthread_rwlock_unlock ( &plist->lk); errno = EINVAL; } /* * * * * * * * * * * * * */ /* Применить процедуру ко */ /* всем элементам списка */ /* * * * * * * * * * * * */ void g_list_forall_x (g_listP plist, aproc proc, void *extobj) { register g_linkP pt; /* Устанавливаем блокировку на запись, */ /* поскольку, возможно, процедура */ /* модифицирует */ /* объекты, ссылки на которые */ /* хранятся в списке */ if ((errno = pthread_rwlock_trywrlock( &plist->lk)) != 0) { return; } for (pt = plist->head; pt != NULL; pt = pt->pnext) { proc (extobj, pt->pvalue); } (void) pthread_rwlock_unlock( &plist->lk); } /* * * * * * * * * * * * * * * * * * */ /* Выбрать подходящий элемент списка */ /* * * * * * * * * * * * * * * * * * */ void *g_list_suchas_x (g_listP plist, afun fun, void *extobj) { register g_linkP pt; /* Устанавливаем блокировку на */ /* чтение, так как */ /* считаем, что функция не */ /* модифицирует объекты, */ /* ссылки на которые хранятся */ /* в списке */ if ((errno = pthread_rwlock_tryrdlock( &plist->lk)) != 0) { return NULL; } for (pt = plist->head; pt != NULL; pt = pt->pnext) { if (fun (extobj, pt->pvalue)) { (void) pthread_rwlock_unlock( &plist->lk); return (pt->pvalue); } } (void) pthread_rwlock_unlock( &plist->lk); return NULL; } /* * * * * * * * * * * * * * */ /* Выдать элемент по номеру */ /* * * * * * * * * * * * * * */ void *g_list_get_bynum (g_listP plist, int n) { register g_linkP pt; if ((errno = pthread_rwlock_tryrdlock( &plist->lk)) != 0) { return NULL; } for (pt = plist->head; n--, pt != NULL; pt = pt->pnext) { if (n == 0) { (void) pthread_rwlock_unlock( &plist->lk); return (pt->pvalue); } } /* Пытаемся извлечь несуществующий */ /* компонент */ (void) pthread_rwlock_unlock( &plist->lk); errno = EINVAL; return NULL; } |
| Листинг 2.29. Исходный текст функций для работы со списками в многопотоковой среде. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * */ /* Тест функций многопотоковой работы */ /* со списками */ /* * * * * * * * * * * * * * * * * * * */ #define _XOPEN_SOURCE 600 #include /* Структура для засыпания на */ /* минимальное время */ static struct timespec nslp = {0, 1}; /* Структуры для возврата результатов */ /* потоками: */ /* Сколько операций пытались сделать, */ /* сколько из них оказались неудачными */ /* из-за занятости списка */ static struct pt_res { int nops; int nbusy; } ptres [3] = {{0, 0}, {0, 0}, {0, 0}}; /* * * * * * * * * * * * * * * * * * * * */ /* Этот поток добавляет элементы к списку */ /* * * * * * * * * * * * * * * * * * * * */ static void *start_func_1 (void *plist) { int *p1; while (1) { if ((p1 = (int *) malloc ( sizeof (int))) != NULL) { *p1 = rand (); errno = 0; g_list_ins_last (plist, p1); ptres [0].nops++; if (errno == EBUSY) { ptres [0].nbusy++; } } (void) nanosleep (&nslp, NULL); } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * */ /* Процедура для подсчета суммы элементов */ /* списка */ /* * * * * * * * * * * * * * * * * * * * * */ static void proc_sum (void *sum, void *pval) { *((int *) sum) += *((int *) pval); } /* * * * * * * * * * * * * * * * * * * * * */ /* Этот поток подсчитывает сумму элементов */ /* списка */ /* * * * * * * * * * * * * * * * * * * * * */ static void *start_func_2 (void *plist) { int sum; while (1) { sum = 0; errno = 0; g_list_forall_x (plist, &proc_sum, &sum); ptres [1].nops++; if (errno == EBUSY) { ptres [1].nbusy++; } (void) nanosleep (&nslp, NULL); } return (NULL); } /* * * * * * * * * * * * * * * * * */ /* Этот поток удаляет из списка */ /* элементы со случайными номерами */ /* * * * * * * * * * * * * * * * * */ static void *start_func_3 (void *plist) { int *p1; while (1) { errno = 0; p1 = (int *) g_list_get_bynum( plist, rand ()); ptres [2].nops++; if (errno == EBUSY) { ptres[2].nbusy++; } if (p1 != NULL) { errno = 0; g_list_del (plist, p1); ptres [2].nops++; if (errno == EBUSY) { ptres [2].nbusy++; } free (p1); } (void) nanosleep (&nslp, NULL); } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * */ /* Начальный поток всех запустит, поспит, */ /* потом всех терминирует и выдаст статистику */ /* * * * * * * * * * * * * * * * * * * * * * */ int main (void) { g_listP plist; pthread_t pt1, pt2, pt3; g_list_init (plist); pthread_create (&pt1, NULL, start_func_1, plist); pthread_create (&pt2, NULL, start_func_2, plist); pthread_create (&pt3, NULL, start_func_3, plist); sleep (10); pthread_cancel (pt1); pthread_cancel (pt2); pthread_cancel (pt3); pthread_join (pt1, NULL); pthread_join (pt2, NULL); pthread_join (pt3, NULL); g_list_destroy (plist); printf ("Попыток выполнить " "операцию со списком: %d\n", ptres [0].nops + ptres [1].nops + ptres [2].nops); printf ("Число попыток, неудачных" " из-за занятости " "списка: %d\n", ptres [0].nbusy + ptres [1].nbusy + ptres [2].nbusy); return (0); } |
| Листинг 2.30. Пример программы, использующей функции для работы со списками в многопотоковой среде. |
| Закрыть окно |
| Попыток выполнить операцию со списком: 1503 Число попыток, неудачных из-за занятости списка: 0 |
| Листинг 2.31. Возможные результаты работы программы, использующей функции для работы со списками в многопотоковой среде. |
| Закрыть окно |
|
#include int pthread_spin_init ( pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy ( pthread_spinlock_t *lock); |
| Листинг 2.32. Описание функций инициализации и разрушения спин-блокировок. |
| Закрыть окно |
|
#include int pthread_spin_lock ( pthread_spinlock_t *lock); int pthread_spin_trylock ( pthread_spinlock_t *lock); |
| Листинг 2.33. Описание функций установки спин-блокировки. |
| Закрыть окно |
|
#include |
| Листинг 2.34. Описание функции снятия спин-блокировки. |
| Закрыть окно |
| pthread_spin_lock (&ss->lock); /* Восстановим старую маску */ ss->blocked = oldmask; /* Проверим ждущие сигналы */ pending = ss->pending & ~ss->blocked; pthread_spin_unlock (&ss->lock); |
| Листинг 2.35. Фрагмент возможной реализации функции sigsuspend(). |
| Закрыть окно |
|
#include int pthread_barrier_init ( pthread_barrier_t * restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count); int pthread_barrier_destroy ( pthread_barrier_t *barrier); |
| Листинг 2.36. Описание функций инициализации и разрушения барьеров. |
| Закрыть окно |
|
#include |
| Листинг 2.37. Описание функции синхронизации на барьере. |
| Закрыть окно |
|
#include int pthread_barrierattr_init ( pthread_barrierattr_t *attr); int pthread_barrierattr_destroy ( pthread_barrierattr_t *attr); |
| Листинг 2.38. Описание функций инициализации и разрушения атрибутных объектов барьеров. |
| Закрыть окно |
|
#include int pthread_barrierattr_getpshared (const pthread_barrierattr_t *restrict attr, int *restrict pshared); int pthread_barrierattr_setpshared (pthread_barrierattr_t *attr, int pshared); |
| Листинг 2.39. Описание функций опроса и установки атрибутов барьеров в атрибутных объектах. |
| Закрыть окно |
|
if ((status = pthread_barrier_wait( &barrier)) == PTHREAD_BARRIER_SERIAL_THREAD) { /* Выделенные (обычно – объединительные) */ /* действия. */ /* Выполняются каким-то одним потоком */ /* управления */ } else { /* Эта часть выполняется всеми */ /* прочими потоками */ /* управления */ if (status != 0) { /* Обработка ошибочной ситуации */ } else { /* Нормальное "невыделенное" */ /* завершение ожидания */ /* на барьере */ } } /* Повторная синхронизация – */ /* ожидание завершения выделенных действий */ status = pthread_barrier_wait (&barrier); /* Продолжение параллельной работы */ . . . |
| Листинг 2.40. Типичная схема применения функции pthread_barrier_wait(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * */ /* Программа использует барьеры для слияния */ /* результатов */ /* коллективных вычислений ln(2) */ /* * * * * * * * * * * * * * * * * * * * * * */ #define _XOPEN_SOURCE 600 #include static pthread_barrier_t mbrr; /* Ряд для ln(2) будут суммировать */ /* два потока. */ /* Один возьмет на себя положительные */ /* слагаемые, */ /* другой – отрицательные */ static double sums [2] = {0, 0}; /* * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потоков управления, */ /* участвующих в вычислениях ln(2). */ /* Аргумент ns на самом деле */ /* целочисленный и равен 1 или 2 */ /* * * * * * * * * * * * * * * * * * * * */ static void *start_func (void *ns) { double d = 1; double s = 0; int i; /* Вычислим свою часть ряда */ for (i = (int) ns; i <= 100000000; i += 2) { s += d / i; } /* Запомним результат в нужном месте */ sums [(int) ns – 1] = s; /* Синхронизируемся для получения */ /* общего итога */ if (pthread_barrier_wait (&mbrr) == PTHREAD_BARRIER_SERIAL_THREAD) { sums [0] -= sums [1]; } /* Указатель на итог возвращают оба потока */ return (sums); } /* * * * * * * * * * * * * * * * * */ /* Инициализация барьера, */ /* создание и ожидание завершения */ /* потоков управления */ /* * * * * * * * * * * * * * * * * */ int main (void) { pthread_t pt1, pt2; double *pd; if ((errno = pthread_barrier_init ( &mbrr, NULL, 2)) != 0) { perror ("PTHREAD_BARRIER_INIT"); return (errno); } pthread_create (&pt1, NULL, start_func, (void *) 1); pthread_create (&pt2, NULL, start_func, (void *) 2); pthread_join (pt1, (void **) &pd); pthread_join (pt2, (void **) &pd); printf ("Коллективно вычисленное" " значение ln(2): %g\n", *pd); return (pthread_barrier_destroy( &mbrr)); } |
| Листинг 2.41. Пример программы, использующей барьеры. |
| Закрыть окно |
Спин-блокировки
Спин-блокировки представляют собой чрезвычайно низкоуровневое средство синхронизации, предназначенное в первую очередь для применения в многопроцессорной конфигурации с разделяемой памятью. Они обычно реализуются как атомарно устанавливаемое булево значение (истина – блокировка установлена). Аппаратура поддерживает подобные блокировки командами вида "проверить и установить".
При попытке установить спин-блокировку, если она захвачена кем-то другим, как правило, применяется активное ожидание освобождения, с постоянным опросом в цикле состояния блокировки. Естественно, при этом занимается процессор, так что спин-блокировки следует устанавливать только на очень короткое время и их владелец не должен приостанавливать свое выполнение.
Для описываемых блокировок стандарт POSIX-2001 не предусматривает установки с ограниченным ожиданием. Это понятно, поскольку накладные расходы по времени на ограничение в типичном случае превысят само время ожидания.
По сравнению с мьютексами спин-блокировки могут иметь то преимущество, что (активное) ожидание и установка не связаны с переключением контекстов, активизацией планировщика и т.п. Если ожидание оказывается кратким, минимальными оказываются и накладные расходы. Приложение, чувствительное к подобным тонкостям, в каждой конкретной ситуации может выбрать наиболее эффективное средство синхронизации.
Согласно стандарту POSIX-2001, спин-блокировки обслуживаются следующими группами функций:
#include
int pthread_spin_init ( pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy ( pthread_spinlock_t *lock);
Листинг 2.32. Описание функций инициализации и разрушения спин-блокировок. (html, txt)
#include
int pthread_spin_lock ( pthread_spinlock_t *lock);
int pthread_spin_trylock ( pthread_spinlock_t *lock);
Листинг 2.33. Описание функций установки спин-блокировки. (html, txt)
#include
Листинг 2.34. Описание функции снятия спин-блокировки. (html, txt)
Обратим внимание на то, что применительно к спин-блокировкам было решено не возиться с атрибутными объектами, а единственный поддерживаемый атрибут – признак использования несколькими процессами – задавать при вызове функции pthread_spin_init().
Поскольку между применением мьютексов и спин-блокировок много общего, мы не будем приводить примеры программ, использующих спин-блокировки. Ограничимся маленьким, слегка модифицированным характерным фрагментом реализации функции sigsuspend() в библиотеке glibc (см. листинг 2.35).
pthread_spin_lock (&ss->lock); /* Восстановим старую маску */ ss->blocked = oldmask; /* Проверим ждущие сигналы */ pending = ss->pending & ~ss->blocked; pthread_spin_unlock (&ss->lock);
Листинг 2.35. Фрагмент возможной реализации функции sigsuspend(). (html, txt)
Спин-блокировка устанавливается на очень короткий участок кода; естественно, она должна быть реализована весьма эффективно, чтобы накладные расходы не оказались чрезмерными.
Часы и таймеры
Стандартом POSIX-2001 предусмотрены средства создания таймеров для процессов, которые будут генерировать уведомления о наступлении заданного момента в виде рассмотренных выше сигналов реального времени. При этом разрешающая способность общесистемных часов реального времени CLOCK_REALTIME и монотонных часов CLOCK_MONOTONIC, а также основанных на них сервисов времени должна быть не хуже, чем заданное конфигурационной константой _POSIX_CLOCKRES_MIN значение 20 мсек (1/50 секунды). Естественно, реализация может обеспечивать более высокую разрешающую способность.
Если определена конфигурационная константа _POSIX_CPUTIME, значит, реализация дополнительно поддерживает для процессов часы процессорного времени с идентификатором типа clockid_t и значением CLOCK_PROCESS_CPUTIME_ID.
Если определена константа _POSIX_THREAD_CPUTIME, то аналогичные часы с идентификатором CLOCK_THREAD_CPUTIME_ID поддерживаются и для потоков управления.
Функция clock_getcpuclockid() позволяет выяснить идентификатор часов процессорного времени для заданного (отличного от вызывающего) процесса, а функция pthread_getcpuclockid() – аналогичный идентификатор для заданного потока управления текущего процесса. В принципе, это позволяет мобильным образом строить системы мониторинга выполнения независимо разработанных приложений, выявляя узкие места и ситуации перерасхода процессорного времени, что очень важно по крайней мере для выполнения требований мягкого реального времени. Правда, возможность создания таймеров на основе полученных идентификаторов объявлена в стандарте POSIX-2001 как зависящая от реализации.
Для приложений реального времени важна возможность использования не только процессорных, но и монотонных часов. В основном из этих соображений в стандарте POSIX-2001 присутствует расширенный аналог рассмотренной в курсе [1] функции nanosleep() – clock_nanosleep() (см. листинг 3.19).
#include
Листинг 3.19. Описание функции clock_nanosleep(). (html, txt)
Аргумент rqtp задает момент времени (по часам с идентификатором clock_id), до наступления которого приостанавливается выполнение текущего потока управления. Если в аргументе flags установлен флаг TIMER_ABSTIME, этот момент трактуется как абсолютный, в противном случае – как относительный.
Разумеется, "наносон" может быть прерван доставкой обрабатываемого сигнала. Если при этом значение аргумента rmtp отлично от NULL, а момент возобновления выполнения задан как относительный, то в указуемую структуру типа timespec помещается "недоспанное" время.
Отметим, что функция clock_nanosleep() полностью аналогична nanosleep(), если не устанавливать флаг TIMER_ABSTIME, а в качестве часов использовать общесистемные часы реального времени CLOCK_REALTIME.
Возможность приостановки выполнения до наступления абсолютного момента времени полезна, например, для периодических процессов, когда, после завершения всех операций текущего периода, нужно заснуть до начала следующего периода. Для nanosleep() в таком случае необходимо узнать текущее время и вычесть его из расчетного времени возобновления выполнения; clock_nanosleep() с флагом TIMER_ABSTIME позволяет сразу задать время возобновления. Более сложное вычисление аргумента функции nanosleep() плохо не столько само по себе, сколько из-за возможного вытеснения потока управления с процессора перед самым вызовом nanosleep(); в результате поток позже заснет и, соответственно, проснется позже запланированного момента времени. (Нетрудно видеть, что это общая проблема относительных таймеров.)
Понятно также, почему "недоспанное" время возвращается, только если момент возобновления выполнения задан как относительный. Чтобы "доспать" после обработки сигнала, абсолютный момент времени перевычислять не нужно, а вот в качестве относительного как раз пригодится значение, на которое указывает аргумент rmtp.
Естественно, попытка использования в clock_nanosleep() часов процессорного времени вызывающего потока управления считается ошибкой.
Следующая программа (см. листинг 3.20) иллюстрирует типичную схему применения функции clock_nanosleep() для организации периодических процессов.
Листинг 3.20. Пример применения функции clock_nanosleep() для организации периодического процесса. (html, txt)
На листинге 3.21 показаны возможные результаты выполнения приведенной программы.
Листинг 3.21. Возможные результаты выполнения программы, использующей функцию clock_nanosleep() для организации периодического процесса. (html, txt)
В качестве несложного упражнения читателю предлагается объяснить, почему в функции tmspc_add() наносекунды суммируются после секунд, а также почему длительность первой итерации оказывается несколько больше, чем у последующих.
Для организации периодических процессов можно воспользоваться не только часами, но и таймерами. Чтобы создать для процесса таймер, генерирующий уведомления о наступлении заданного момента в виде сигнала реального времени, следует обратиться к функции timer_create() (см. листинг 3.22).
#include
Листинг 3.22. Описание функции timer_create(). (html, txt)
Таймер создается на основе часов с идентификатором clockid; идентификатор таймера (уникальный в пределах вызывающего процесса) записывается по указателю timerid. Разумеется, сразу после создания таймер оказывается в невзведенном состоянии.
Аргумент evp, указывающий на структуру типа sigevent, определяет характер уведомлений о срабатывании таймера. Если его значение равно NULL, то в качестве способа уведомления принимается SIGEV_SIGNAL, при срабатывании генерируется сигнал реального времени с подразумеваемым номером и значением, равным идентификатору таймера.
Если реализация поддерживает часы процессорного времени, то подобные часы для вызывающего процесса (потока управления) могут обслуживать создаваемый таймер.
Одношаговое порождение процессов
При стандартизации средств одношагового порождения процессов преследовались две основные цели:
В качестве дополнительных целей объявлены:
Еще одна дополнительная цель – простота интерфейса. Семейство exec*() насчитывает шесть членов; для posix_spawn*() хватило двух с единым списком аргументов в духе execve() и небольшими отличиями в трактовке имени файла с образом нового процесса (напоминающими разницу между execve() и execvp()).
Более точно: для одношагового порождения процессов служат функции posix_spawn() и posix_spawnp() (см. листинг 3.1).
#include
int posix_spawn (pid_t *restrict pid, const char *restrict path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv [restrict], char *const envp [restrict]); int posix_spawnp (pid_t *restrict pid, const char *restrict file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv [restrict], char *const envp [restrict]);
Листинг 3.1. Описание функций одношагового порождения процессов. (html, txt)
Аргументами, отличающими posix_spawn() и posix_spawnp() от функций семейства exec*() являются pid, file_actions, attrp. Опишем их назначение.
По указателю pid (если он отличен от NULL) возвращается идентификатор успешно порожденного процесса.
Аргументы file_actions и attrp отвечают за контроль сущностей, наследуемых при одношаговом порождении процессов.
Чтобы достичь перечисленных выше целей, функции posix_spawn() и posix_spawnp() контролируют шесть видов наследуемых сущностей:
Контроль файловых дескрипторов, в первую очередь стандартных ввода, вывода и протокола, который осуществляется с помощью аргумента file_actions, позволяет порожденному процессу получить доступ к потокам данных, открытым или даже порожденным родительским процессом, без встраивания в исходный текст или передачи в качестве аргументов main() конкретных имен файлов или номеров дескрипторов.
Как правило, все открытые дескрипторы родительского процесса остаются таковыми и в порожденном, за исключением тех, у которых установлен флаг FD_CLOEXEC. Кроме того, если значение аргумента file_actions отлично от NULL, до обработки флагов FD_CLOEXEC принимается во внимание указуемый объект типа posix_spawn_file_actions_t, который содержит действия по закрытию, открытию и/или дублированию файловых дескрипторов. Для формирования объектов типа posix_spawn_file_actions_t служат функции posix_spawn_file_actions_init(), posix_spawn_file_actions_addclose(), posix_spawn_file_actions_addopen() и posix_spawn_file_actions_adddup2(); функция posix_spawn_file_actions_destroy() ликвидирует подобный объект (см. листинг 3.2).
#include
int posix_spawn_file_actions_init (posix_spawn_file_actions_t *file_actions);
int posix_spawn_file_actions_destroy (posix_spawn_file_actions_t *file_actions);
int posix_spawn_file_actions_addclose (posix_spawn_file_actions_t *file_actions, int fildes);
int posix_spawn_file_actions_addopen ( posix_spawn_file_actions_t *restrict file_actions, int fildes, const char *restrict path, int oflag, mode_t mode);
int posix_spawn_file_actions_adddup2 (posix_spawn_file_actions_t *file_actions, int fildes, int newfildes);
Листинг 3.2.
Описание функций формирования и ликвидации объектов типа posix_spawn_file_actions_t. (html, txt)
Функция posix_spawn_file_actions_addclose() добавляет дескриптор fildes к числу закрываемых перед началом выполнения порожденного процесса. Функция posix_spawn_file_actions_addopen() предписывает открыть дескриптор fildes, как если бы был выполнен вызов open (path, oflag, mode). Наконец, функция posix_spawn_file_actions_adddup2() специфицирует дублирование дескриптора fildes в newfildes (close (newfildes); fcntl (fildes, F_DUPFD, newfildes)). Таким образом, функции posix_spawn() и posix_spawnp(), отправляясь от набора открытых дескрипторов родительского процесса, выполняют действия, заданные аргументом file_actions, и получают набор дескрипторов, открытых в порождаемом процессе, то есть родительский процесс берет на себя согласование по файловым дескрипторам с независимо созданным новым образом процесса.
Отметим, что с помощью функции posix_spawn_file_actions_addopen() удобно перенаправлять ввод/вывод порожденного процесса.
За контроль других сущностей, наследуемых при одношаговом порождении процессов, отвечает атрибутный объект , заданный аргументом attrp. Для формирования и опроса подобных объектов служат функции, показанные на листингах 3.3, 3.4 и 3.5.
#include
int posix_spawnattr_init ( posix_spawnattr_t *attr);
int posix_spawnattr_destroy ( posix_spawnattr_t *attr);
int posix_spawnattr_getflags ( const posix_spawnattr_t *restrict attr, short *restrict flags);
int posix_spawnattr_setflags ( posix_spawnattr_t *attr, short flags);
int posix_spawnattr_getpgroup ( const posix_spawnattr_t *restrict attr, pid_t *restrict pgroup);
int posix_spawnattr_setpgroup ( posix_spawnattr_t *attr, pid_t pgroup);
Листинг 3.3. Описание функций формирования и опроса атрибутных объектов порождаемых процессов. (html, txt)
#include
int posix_spawnattr_getschedparam ( const posix_spawnattr_t *restrict attr, struct sched_param *restrict schedparam);
int posix_spawnattr_setschedparam ( posix_spawnattr_t * restrict attr, const struct sched_param *restrict schedparam);
int posix_spawnattr_getschedpolicy ( const posix_spawnattr_t *restrict attr, int *restrict schedpolicy);
int posix_spawnattr_setschedpolicy ( posix_spawnattr_t *attr, int schedpolicy);
Листинг 3.4. Описание функций опроса и установки параметров и политики планирования в атрибутных объектах порождаемых процессов. (html, txt)
#include
int posix_spawnattr_setsigdefault ( posix_spawnattr_t *restrict attr, const sigset_t *restrict sigdefault);
int posix_spawnattr_getsigmask ( const posix_spawnattr_t *restrict attr, sigset_t *restrict sigmask);
int posix_spawnattr_setsigmask ( posix_spawnattr_t *restrict attr, const sigset_t *restrict sigmask);
Листинг 3.5. Описание функций опроса и установки подразумеваемой обработки и маски сигналов в атрибутных объектах порождаемых процессов. (html, txt)
POSIX_SPAWN_SETSIGMASK
Установить начальную маску сигналов по атрибутному объекту.
POSIX_SPAWN_SETSCHEDPARAM
Установить параметры планирования по атрибутному объекту.
POSIX_SPAWN_SETSCHEDULER
Установить политику и параметры планирования по атрибутному объекту (независимо от состояния флага POSIX_SPAWN_SETSCHEDPARAM).
Если значение аргумента attrp равно NULL, используются подразумеваемые значения атрибутов.
Все характеристики нового процесса, на которые не воздействуют аргументы attrp и file_actions, устанавливаются так, как если бы применялось двухшаговое порождение fork()/exec(). Будут ли при одношаговом порождении выполняться обработчики разветвления процессов, зарегистрированные с помощью функции atfork(), зависит от реализации.
При библиотечной реализации функций posix_spawn() и posix_spawnp() некоторые ошибки могут быть выявлены только после порождения процесса. В таком случае родительский процесс может узнать о них, анализируя с помощью макросов WIFEXITED, WEXITSTATUS (см. курс [1]) значение stat_val, возвращаемое функциями wait() и/или waitpid(). Предлагается, чтобы статус "аварийного завершения до начала реального выполнения" равнялся 127. Это не очень естественно и удобно, но иного выхода не видно.
В целом средства одношагового порождения процессов позволяют достичь сформулированных выше целей. Они просты, но обладают достаточной выразительной силой и, несомненно, способны заменить двухшаговое порождение по крайней мере в половине типичных случаев.
Чтобы лучше понять семантику одношагового порождения процессов, приведем фрагмент возможной библиотечной реализации функции posix_spawn(), представленной в четвертой, информационной части стандарта POSIX-2001 (см. листинги 3.6 и 3.7).
typedef struct { short posix_attr_flags; pid_t posix_attr_pgroup; sigset_t posix_attr_sigmask; sigset_t posix_attr_sigdefault; int posix_attr_schedpolicy; struct sched_param posix_attr_schedparam; } posix_spawnattr_t;
typedef char *posix_spawn_file_actions_t;
int posix_spawn (pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv [], char *const envp []);
Листинг 3.6. Фрагмент возможного содержимого файла spawn.h.
int posix_spawn (pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv [], char *const envp []) {
/* Создадим новый процесс */ if ((*pid = fork()) == (pid_t) 0) { /* Порожденный процесс */ /* Позаботимся о группе процессов */ if (attrp->posix_attr_flags & POSIX_SPAWN_SETPGROUP) { /* Изменим унаследованную группу */ if (setpgid (0, attrp->posix_attr_pgroup) != 0) { /* Неудача */ exit (127); } }
/* Позаботимся о действующих идентификаторах */ /* пользователя и группы */ if (attrp->posix_attr_flags & POSIX_SPAWN_RESETIDS) { /* В данном случае неудачи быть не может */ setuid (getuid ()); setgid (getgid ()); }
/* Позаботимся о подразумеваемом способе */ /* обработки сигналов */ if (attrp->posix_attr_flags & POSIX_SPAWN_SETSIGDEF) { struct sigaction deflt; sigset_t all_signals; int s;
deflt.sa_handler = SIG_DFL; deflt.sa_flags = 0;
sigfillset (&all_signals);
/* Цикл по всем сигналам */ for (s = 0; sigismember (&all_signals, s); s++) { if (sigismember (&attrp->posix_attr_sigdefault, s)) { if (sigaction (s, &deflt, NULL) == -1) { exit (127); } } } }
/* Проконтролируем остальные атрибуты */ /* . . . */
/* Подменим образ процесса */ execve (path, argv, envp); exit (127); } else { /* Родительский (вызывающий) процесс */ if (*pid == (pid_t) (-1)) return errno; return 0; } }
Листинг 3.7. Фрагмент возможной библиотечной реализации функции posix_spawn().
Если для порожденного процесса нужно особым образом установить характеристику, не принадлежащую к числу описанных выше контролируемых сущностей (например, значение переменной окружения), приходится перед обращением к posix_spawn() сохранить и переустановить ее, а затем восстановить прежнее значение (см. листинг 3.8). Правда, при этом необходимо, чтобы все потоки управления, выполняющиеся в рамках родительского процесса, были устойчивы к подобным манипуляциям.
/* Запуск процесса с произвольным идентификатором */ /* пользователя */
uid_t old_uid; uid_t new_uid = ...;
old_uid = getuid (); setuid (new_uid);
posix_spawn (...);
setuid (old_uid);
Листинг 3.8. Пример установки характеристики порожденного процесса, не принадлежащей к числу контролируемых стандартными средствами.
На листинге 3.9 показан пример перенаправления стандартных ввода и вывода порождаемого процесса с помощью формирования и использования объекта типа posix_spawn_file_actions_t. В данном случае стандартный вывод (дескриптор 1) направляется в файл outfile, а стандартный ввод (дескриптор 0) отождествляется с открытым ранее дескриптором socket_pair [1]. Попутно обеспечивается закрытие в новом процессе дескрипторов socket_pair [0] и socket_pair [1].
posix_spawn_file_actions_t file_actions;
posix_spawn_file_actions_init ( &file_actions); posix_spawn_file_actions_addopen ( &file_actions, 1, "outfile", ...); posix_spawn_file_actions_adddup2 ( &file_actions, socket_pair [1], 0); posix_spawn_file_actions_addclose ( &file_actions, socket_pair [0]); posix_spawn_file_actions_addclose ( &file_actions, socket_pair [1]);
posix_spawn (..., &file_actions, ...); posix_spawn_file_actions_destroy ( &file_actions);
Листинг 3.9. Пример перенаправления стандартных ввода и вывода порождаемого процесса.
Любопытно сопоставить реальные накладные расходы на одношаговое и двухшаговое порождение процессов. Программа, порождающая с помощью функции posix_spawn() практически пустые процессы, показана на листинге 3.10. На листинге 3.11 приведены данные о времени ее работы, полученные с помощью команды time -p. Полученные результаты практически не отличаются от измеренного ранее времени двухшагового порождения. Это означает, что в используемой нами версии ОС Linux функции posix_spawn() и posix_spawnp() реализованы как библиотечные, а их применение выигрыша в эффективности в данном случае не дает (но по соображениям "мобильной потенциальной эффективности" их все равно есть смысл использовать).
#include
#define N 10000
int main (void) { char *s_argv [] = {"dummy", NULL}; char *s_env [] = {NULL};
int i;
for (i = 0; i < N; i++) { if ((errno = posix_spawn ( NULL, "./dummy", NULL, NULL, s_argv, s_env)) != 0) { perror ("POSIX_SPAWN"); return (errno); } (void) wait (NULL); }
return 0; }
Листинг 3.10. Пример программы, порождающей в цикле практически пустые процессы с помощью функции posix_spawn().
real 34.37 user 12.01 sys 22.07
Листинг 3.11. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы с помощью функции posix_spawn().
Основные идеи, понятия и объекты
В рамках решаемой стандартом POSIX общей задачи обеспечения мобильности приложений на уровне исходных текстов можно выделить подзадачу мобильного программирования систем, к которым предъявляются требования работы в реальном масштабе времени. (Разумеется, стандарт не может и не должен преодолевать зависимость числовых параметров реализации от свойств аппаратной платформы.)
В качестве средств для решения этой подзадачи в стандарте POSIX-2001 присутствует целый ряд идей, понятий, объектов и функций, которые можно сгруппировать в следующие предметные области:
Одношаговое порождение процессов (в противоположность традиционной для ОС Unix двухшаговой модели fork()/exec()) основано на применении функций posix_spawn() и posix_spawnp(). Оно в любом случае менее тяжеловесно (хотя и тяжеловеснее порождения потоков управления), но особенно актуально для аппаратных конфигураций, в которых отсутствует поддержка виртуальной памяти и, как следствие, реализация функции fork() проблематична.
Под сигналами реального времени понимается не нарушающее совместимости расширение общего механизма сигналов, повышающее детерминированность за счет постановки сигналов, асинхронно доставленных приложению, в очередь.
Основные понятия, ассоциированные с часами и таймерами, были рассмотрены в курсе [1]. В настоящем курсе мы сосредоточимся на необязательных элементах стандарта POSIX-2001, специфичных для реального времени.
В качестве средств межпроцессного взаимодействия в реальном времени в стандарт POSIX-2001 включены очереди сообщений, семафоры и разделяемые сегменты памяти (см. также курс [1], где детально анализируется другой класс средств межпроцессного взаимодействия с аналогичными названиями).
Чтобы время доступа к объекту было по возможности минимальным и не превышало заданной величины, этот объект делают резидентным в физической памяти.
Применительно к процессам подобный подход реализуется посредством удержания в памяти страниц, принадлежащих прикладному процессу.
Достижению той же цели – минимизации времени (а также унификации) доступа – служит механизм объектов в памяти. Стандартом POSIX-2001 предусмотрено три вида таких объектов:
С помощью функции mmap() строится отображение объекта в памяти на группу страниц из адресного пространства вызывающего процесса, так что доступ к адресам из заданного диапазона выливается в обращение к ассоциированному объекту (например, к файлу).
Объекты в разделяемой памяти характеризуются тем, что их можно параллельно отобразить в адресное пространство нескольких процессов.
Объект в типизированной памяти представляет собой комбинацию типизированных пула и порта памяти. Типизированный пул – это область памяти с однородными операционными характеристиками. Пулы могут быть вложенными. Аппаратный доступ к содержимому пула осуществляется через порт. Объект в типизированной памяти идентифицируется именем из пространства имен типизированной памяти.
Отметим, что объект в памяти не обязательно должен быть резидентным в физической памяти.
Согласно стандарту POSIX-2001, приоритет – это неотрицательная целочисленная величина, ассоциированная с процессом или потоком управления. Допустимый диапазон приоритетов определяется применяемой политикой планирования. Большие величины представляют более высокие приоритеты.
Приоритетное планирование (или планирование, основанное на приоритетах) – это средство усиления детерминированности, позволяющее приложениям определять порядок, в соответствии с которым потоки управления, готовые к выполнению, получают доступ к процессорным ресурсам.
Средства асинхронного ввода/вывода позволяют прикладным процессам ставить в очередь команды ввода/вывода данных и получать асинхронные уведомления о завершении выполнения этих команд.
Рекомендательные интерфейсы – это средство проинформировать операционную систему о поведении мобильного приложения, чтобы ОС могла принять меры для оптимизации его обслуживания. Например, если прогнозируется последовательный доступ к файлу, ОС может осуществлять опережающее чтение, чтобы совместить во времени ввод и обработку данных.
Применительно к процессам подобный подход реализуется посредством удержания в памяти страниц, принадлежащих прикладному процессу.
Достижению той же цели – минимизации времени (а также унификации) доступа – служит механизм объектов в памяти. Стандартом POSIX-2001 предусмотрено три вида таких объектов:
С помощью функции mmap() строится отображение объекта в памяти на группу страниц из адресного пространства вызывающего процесса, так что доступ к адресам из заданного диапазона выливается в обращение к ассоциированному объекту (например, к файлу).
Объекты в разделяемой памяти характеризуются тем, что их можно параллельно отобразить в адресное пространство нескольких процессов.
Объект в типизированной памяти представляет собой комбинацию типизированных пула и порта памяти. Типизированный пул – это область памяти с однородными операционными характеристиками. Пулы могут быть вложенными. Аппаратный доступ к содержимому пула осуществляется через порт. Объект в типизированной памяти идентифицируется именем из пространства имен типизированной памяти.
Отметим, что объект в памяти не обязательно должен быть резидентным в физической памяти.
Согласно стандарту POSIX-2001, приоритет – это неотрицательная целочисленная величина, ассоциированная с процессом или потоком управления. Допустимый диапазон приоритетов определяется применяемой политикой планирования. Большие величины представляют более высокие приоритеты.
Приоритетное планирование (или планирование, основанное на приоритетах) – это средство усиления детерминированности, позволяющее приложениям определять порядок, в соответствии с которым потоки управления, готовые к выполнению, получают доступ к процессорным ресурсам.
Средства асинхронного ввода/вывода позволяют прикладным процессам ставить в очередь команды ввода/вывода данных и получать асинхронные уведомления о завершении выполнения этих команд.
Рекомендательные интерфейсы – это средство проинформировать операционную систему о поведении мобильного приложения, чтобы ОС могла принять меры для оптимизации его обслуживания. Например, если прогнозируется последовательный доступ к файлу, ОС может осуществлять опережающее чтение, чтобы совместить во времени ввод и обработку данных.
restrict pid, const char
|
#include int posix_spawn (pid_t * restrict pid, const char *restrict path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv [restrict], char *const envp [restrict]); int posix_spawnp (pid_t *restrict pid, const char *restrict file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv [restrict], char *const envp [restrict]); |
| Листинг 3.1. Описание функций одношагового порождения процессов. |
| Закрыть окно |
|
#include int posix_spawn_file_actions_init (posix_spawn_file_actions_t *file_actions); int posix_spawn_file_actions_destroy (posix_spawn_file_actions_t *file_actions); int posix_spawn_file_actions_addclose (posix_spawn_file_actions_t *file_actions, int fildes); int posix_spawn_file_actions_addopen ( posix_spawn_file_actions_t * restrict file_actions, int fildes, const char *restrict path, int oflag, mode_t mode); int posix_spawn_file_actions_adddup2 (posix_spawn_file_actions_t *file_actions, int fildes, int newfildes); |
| Листинг 3.2. Описание функций формирования и ликвидации объектов типа posix_spawn_file_actions_t. |
| Закрыть окно |
| #include int posix_spawnattr_init ( posix_spawnattr_t *attr); int posix_spawnattr_destroy ( posix_spawnattr_t *attr); int posix_spawnattr_getflags ( const posix_spawnattr_t *restrict attr, short *restrict flags); int posix_spawnattr_setflags ( posix_spawnattr_t *attr, short flags); int posix_spawnattr_getpgroup ( const posix_spawnattr_t *restrict attr, pid_t *restrict pgroup); int posix_spawnattr_setpgroup ( posix_spawnattr_t *attr, pid_t pgroup); |
| Листинг 3.3. Описание функций формирования и опроса атрибутных объектов порождаемых процессов. |
| Закрыть окно |
|
#include int posix_spawnattr_getschedparam ( const posix_spawnattr_t *restrict attr, struct sched_param *restrict schedparam); int posix_spawnattr_setschedparam ( posix_spawnattr_t * restrict attr, const struct sched_param *restrict schedparam); int posix_spawnattr_getschedpolicy ( const posix_spawnattr_t *restrict attr, int *restrict schedpolicy); int posix_spawnattr_setschedpolicy ( posix_spawnattr_t *attr, int schedpolicy); |
| Листинг 3.4. Описание функций опроса и установки параметров и политики планирования в атрибутных объектах порождаемых процессов. |
| Закрыть окно |
|
#include int posix_spawnattr_setsigdefault ( posix_spawnattr_t * restrict attr, const sigset_t *restrict sigdefault); int posix_spawnattr_getsigmask ( const posix_spawnattr_t *restrict attr, sigset_t *restrict sigmask); int posix_spawnattr_setsigmask ( posix_spawnattr_t *restrict attr, const sigset_t *restrict sigmask); |
| Листинг 3.5. Описание функций опроса и установки подразумеваемой обработки и маски сигналов в атрибутных объектах порождаемых процессов. |
| Закрыть окно |
|
typedef struct { short posix_attr_flags; pid_t posix_attr_pgroup; sigset_t posix_attr_sigmask; sigset_t posix_attr_sigdefault; int posix_attr_schedpolicy; struct sched_param posix_attr_schedparam; } posix_spawnattr_t; typedef char *posix_spawn_file_actions_t; int posix_spawn (pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv [], char *const envp []); |
| Листинг 3.6. Фрагмент возможного содержимого файла spawn.h. |
| Закрыть окно |
|
int posix_spawn (pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv [], char *const envp []) { /* Создадим новый процесс */ if ((*pid = fork()) == (pid_t) 0) { /* Порожденный процесс */ /* Позаботимся о группе процессов */ if (attrp->posix_attr_flags & POSIX_SPAWN_SETPGROUP) { /* Изменим унаследованную группу */ if (setpgid (0, attrp->posix_attr_pgroup) != 0) { /* Неудача */ exit (127); } } /* Позаботимся о действующих идентификаторах */ /* пользователя и группы */ if (attrp->posix_attr_flags & POSIX_SPAWN_RESETIDS) { /* В данном случае неудачи быть не может */ setuid (getuid ()); setgid (getgid ()); } /* Позаботимся о подразумеваемом способе */ /* обработки сигналов */ if (attrp->posix_attr_flags & POSIX_SPAWN_SETSIGDEF) { struct sigaction deflt; sigset_t all_signals; int s; deflt.sa_handler = SIG_DFL; deflt.sa_flags = 0; sigfillset (&all_signals); /* Цикл по всем сигналам */ for (s = 0; sigismember (&all_signals, s); s++) { if (sigismember (&attrp->posix_attr_sigdefault, s)) { if (sigaction (s, &deflt, NULL) == -1) { exit (127); } } } } /* Проконтролируем остальные атрибуты */ /* . . . */ /* Подменим образ процесса */ execve (path, argv, envp); exit (127); } else { /* Родительский (вызывающий) процесс */ if (*pid == (pid_t) (-1)) return errno; return 0; } } |
| Листинг 3.7. Фрагмент возможной библиотечной реализации функции posix_spawn(). |
| Закрыть окно |
|
/* Запуск процесса с произвольным идентификатором */ /* пользователя */ uid_t old_uid; uid_t new_uid = ...; old_uid = getuid (); setuid (new_uid); posix_spawn (...); setuid (old_uid); |
| Листинг 3.8. Пример установки характеристики порожденного процесса, не принадлежащей к числу контролируемых стандартными средствами. |
| Закрыть окно |
|
posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_init ( &file_actions); posix_spawn_file_actions_addopen ( & file_actions, 1, "outfile", ...); posix_spawn_file_actions_adddup2 ( &file_actions, socket_pair [1], 0); posix_spawn_file_actions_addclose ( &file_actions, socket_pair [0]); posix_spawn_file_actions_addclose ( &file_actions, socket_pair [1]); posix_spawn (..., &file_actions, ...); posix_spawn_file_actions_destroy ( &file_actions); |
| Листинг 3.9. Пример перенаправления стандартных ввода и вывода порождаемого процесса. |
| Закрыть окно |
|
#include #define N 10000 int main (void) { char *s_argv [] = {"dummy", NULL}; char *s_env [] = {NULL}; int i; for (i = 0; i < N; i++) { if ((errno = posix_spawn ( NULL, "./dummy", NULL, NULL, s_argv, s_env)) != 0) { perror ("POSIX_SPAWN"); return (errno); } (void) wait (NULL); } return 0; } |
| Листинг 3.10. Пример программы, порождающей в цикле практически пустые процессы с помощью функции posix_spawn(). |
| Закрыть окно |
| real 34.37 user 12.01 sys 22.07 |
| Листинг 3.11. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы с помощью функции posix_spawn(). |
| Закрыть окно |
|
#include |
| Листинг 3.12. Описание функции sigqueue(). |
| Закрыть окно |
|
#include int sigwaitinfo ( const sigset_t *restrict set, siginfo_t *restrict info); int sigtimedwait ( const sigset_t *restrict set, siginfo_t * restrict info, const struct timespec *restrict timeout); |
| Листинг 3.13. Описание функций sigwaitinfo() и sigtimedwait(). |
| Закрыть окно |
|
#include |
| Листинг 3.14. Описание функции sigaltstack(). |
| Закрыть окно |
|
#include if ((sighstk.ss_sp = malloc( SIGSTKSZ)) == NULL) { perror ("malloc (SIGSTKSZ)"); /* Аварийное завершение */ } sighstk.ss_size = SIGSTKSZ; sighstk.ss_flags = 0; if (sigaltstack (&sighstk, (stack_t *) NULL) != 0) { perror ("SIGALTSTACK"); . . . } . . . |
| Листинг 3.15. Типичная схема определения альтернативного стека. |
| Закрыть окно |
|
#include int sigsetjmp ( sigjmp_buf env, int savemask); void siglongjmp (sigjmp_buf env, int val); |
| Листинг 3.16. Описание функций sigsetjmp() и siglongjmp(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Многопотоковый вариант обеда философов */ /* с использованием сигналов реального времени */ /* * * * * * * * * * * * * * * * * * * * * * * */ #include /* Число обедающих философов */ #define QPH 5 /* Время (в секундах) на обед */ #define FO 15 /* Длительность еды */ #define ernd (rand () % 3 + 1) /* Длительность разговора */ #define trnd (rand () % 5 + 1) /* Номер сигнала, используемого для захвата и освобождения вилок */ #define SIG_FORK SIGRTMIN /* Номер сигнала, используемого для информирования философа */ #define SIG_PHIL SIGINT static pthread_t pt_id [QPH]; /* Массив идентификаторов */ /* потоков - философов */ static int fork_busy [QPH] = {0, }; /* Состояние вилок */ static int phil_req [QPH] = {0, }; /* Невыполненные */ /* заявки на вилки */ static sigjmp_buf phil_env [QPH]; /* Массив буферов для */ /* нелокальных переходов */ static pid_t pid_wt; /* Идентификатор процесса,*/ /* контролирующего вилки */ static pthread_key_t phil_key; /* Ключ индивидуальных */ /* данных потоков-философов */ /* * * * * * * * * * * * * * * * * * * */ /* Функция обработки сигнала SIG_PHIL */ /* * * * * * * * * * * * * * * * * * * */ static void phil_eat (int signo) { int no; /* Номер философа, которому достался сигнал */ no = (int) pthread_getspecific (phil_key); if ((no > 0) && (no <= QPH)) { siglongjmp (phil_env [no – 1], signo); } } /* * * * * * * * * * * * * * * * * * * * * * */ /* Попытка выполнить заявку на захват вилок */ /* от философа номер no, если она есть */ /* * * * * * * * * * * * * * * * * * * * * * */ static void fork_lock (int no) { if (phil_req [no – 1] != 0) { /* Заявка есть. */ /* Вилки свободны? */ if ((fork_busy [no – 1] == 0) && (fork_busy [no % QPH] == 0)) { /* Выполним заявку */ fork_busy [no – 1] = fork_busy [no % QPH] = 1; phil_req [no – 1] = 0; (void) pthread_kill (pt_id [no – 1], SIG_PHIL); } } } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, обслуживающего заявки на */ /* захват и освобождение вилок. */ /* Заявка передается в виде значения, ассоциированного */ /* с сигналом signo. */ /* Значение no > 0 запрашивает захват вилок для философа */ /* с номером no, no < 0 – освобождение вилок философа -no */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_waiter (void *signo) { siginfo_t sinfo; /* Структура для получения данных */ /* о сигнале */ int no; /* Номер философа, приславшего заявку */ sigset_t s_sgno; /* Маска ожидаемых сигналов */ pid_wt = getpid (); /* Сформируем маску ожидаемых сигналов */ if ((sigemptyset (&s_sgno) != 0) || (sigaddset (&s_sgno, (int) signo) != 0)) { perror ("SIGEMPTYSET/SIGADDSET"); return (NULL); } while (1) { if (sigwaitinfo (&s_sgno, &sinfo) != (int) signo) { return (NULL); } else { /* Поступила заявка. */ /* Посмотрим, что от нас хотят */ if ((no = sinfo.si_value.sival_int) > 0) { /* Заявка на захват вилок. */ /* Запомним ее ... */ phil_req [no – 1] = 1; /* ... и попробуем выполнить */ fork_lock (no); } else { /* Освобождение вилок */ no = -no; fork_busy [no – 1] = fork_busy [no % QPH] = 0; /* Попробуем выполнить заявки от соседей */ fork_lock (no % QPH + 1); fork_lock (no == 1 ? QPH : (no – 1)); } } /* Другие сигналы нас не интересуют */ } /* while (1) */ } /* * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока-философа. */ /* Аргумент – номер философа */ /* * * * * * * * * * * * * * * * * * */ void *start_phil (void *no) { int fo; /* Время до конца обеда */ int t; /* Время очередного отрезка еды или беседы */ time_t tbe; /* Время, когда философу понадобились вилки */ union sigval sval; /* Значение посылаемого сигнала: */ /* (int) no – заказ вилок */ /* -(int) no – освобождение вилок */ /* Запомним значение аргумента в качестве */ /* индивидуальных данных потока */ (void) pthread_setspecific (phil_key, no); /* Подготовка к обеду */ fo = FO; if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) { /* Сюда придем после нелокального перехода */ /* из обработчика сигнала SIG_PHIL. */ /* Философ просил вилки и получил их */ printf ("Философ %d ест\n", (int) no); t = ernd; sleep (t); /* Нужно вычесть времена еды и ожидания вилок */ fo -= (int) (time ((time_t *) NULL) – tbe) + t; /* Отдает вилки */ sval.sival_int = -((int) no); (void) sigqueue (pid_wt, SIG_FORK, sval); } while (fo > 0) { printf ("Философ %d беседует\n", (int) no); t = trnd; sleep (t); fo -= t; /* Пытается взять вилки */ tbe = time ((time_t *) NULL); sval.sival_int = (int) no; (void) sigqueue (pid_wt, SIG_FORK, sval); /* Пока вилки заняты, приходится беседовать... */ printf ("Философ %d беседует в ожидании вилок\n", (int) no); sleep (fo); fo = 0; } /* while */ printf ("Философ %d закончил обед\n", (int) no); return (NULL); } /* * * * * * * * * * */ /* Организация обеда */ /* * * * * * * * * * */ int main (void) { int no; /* Номер философа */ struct sigaction sact; /* Структура для обработки */ /* обычных сигналов */ pthread_t pt_wt; /* Идентификатор потока, */ /* управляющего вилками */ /* Блокируем сигнал SIG_FORK */ if ((sigemptyset (&sact.sa_mask) == 0) && (sigaddset (&sact.sa_mask, SIG_FORK) == 0)) { (void) pthread_sigmask (SIG_BLOCK, &sact.sa_mask, (sigset_t *) NULL); } /* Установим для сигнала SIG_FORK флаг SA_SIGINFO */ sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = (void (*) (int, siginfo_t *, void *)) SIG_DFL; (void) sigaction (SIG_FORK, &sact, (struct sigaction *) NULL); /* Установим реакцию на сигнал SIG_PHIL */ sact.sa_handler = phil_eat; sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIG_PHIL, &sact, (struct sigaction *) NULL); /* Создадим поток, захватывающий и освобождающий вилки */ if ((errno = pthread_create (&pt_wt, NULL, start_waiter, (void *) SIG_FORK)) != 0) { perror ("PTHREAD_CREATE-1"); return (errno); } /* Создадим ключ индивидуальных данных */ if ((errno = pthread_key_create (&phil_key, (void (*) (void *)) NULL)) != 0) { perror ("PTHREAD_KEY_CREATE"); return (errno); } /* Все – к столу */ for (no = 1; no <= QPH; no++) { if ((errno = pthread_create (&pt_id [no – 1], NULL, start_phil, (void *) no)) != 0) { perror ("PTHREAD_CREATE"); return (no); } } /* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) pthread_join (pt_id [no – 1], NULL); } (void) pthread_key_delete (phil_key); /* Завершим поток, контролирующий вилки */ (void) pthread_cancel (pt_wt); (void) pthread_join (pt_wt, NULL); return 0; } |
| Листинг 3.17. Пример реализации обеда философов с использованием сигналов реального времени. |
| Закрыть окно |
| Философ 1 беседует Философ 2 беседует Философ 3 беседует Философ 4 беседует Философ 5 беседует Философ 4 беседует в ожидании вилок Философ 4 ест Философ 2 беседует в ожидании вилок Философ 2 ест Философ 3 беседует в ожидании вилок Философ 4 беседует Философ 1 беседует в ожидании вилок Философ 5 беседует в ожидании вилок Философ 5 ест Философ 2 беседует Философ 3 ест Философ 5 беседует Философ 1 ест Философ 2 беседует в ожидании вилок Философ 4 беседует в ожидании вилок Философ 3 беседует Философ 4 ест Философ 5 беседует в ожидании вилок Философ 1 беседует Философ 2 ест Философ 2 беседует Философ 4 закончил обед Философ 1 беседует в ожидании вилок Философ 5 ест Философ 2 беседует в ожидании вилок Философ 2 ест Философ 3 беседует в ожидании вилок Философ 3 закончил обед Философ 1 закончил обед Философ 5 закончил обед Философ 2 закончил обед |
| Листинг 3.18. Возможные результаты выполнения программы, реализующей обед философов с использованием сигналов реального времени. |
| Закрыть окно |
|
#include |
| Листинг 3.19. Описание функции clock_nanosleep(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * */ /* Организация периодических процессов */ /* с помощью функции clock_nanosleep() */ /* * * * * * * * * * * * * * * * * * * */ #define _XOPEN_SOURCE 600 #include /* * * * * * * * * * * * * * * * * * * * */ /* Сложение двух структур типа timespec */ /* * * * * * * * * * * * * * * * * * * * */ static void tmspc_add (struct timespec *a1, struct timespec *a2, struct timespec *res) { res->tv_sec = a1->tv_sec + a2->tv_sec + (a1->tv_nsec + a2->tv_nsec) / 1000000000; res->tv_nsec = (a1->tv_nsec + a2->tv_nsec) % 1000000000; } /* * * * * * * * * * * * * * * * * * * */ /* Организация периодического процесса */ /* * * * * * * * * * * * * * * * * * * */ int main (void) { struct timespec t_bp; /* Время начала очередного */ / *периода выполнения */ struct timespec prd = {1, 250000000}; /* Период */ /* выполнения: 1.25 сек */ clockid_t clk_id = CLOCK_REALTIME; /* Идентификатор */ /* используемых часов */ struct timespec t_tmp; int i; /* Запомним время начала выполнения */ (void) clock_gettime (clk_id, &t_bp); printf ("Начало выполнения: %ld сек %ld нсек\n", t_bp.tv_sec, t_bp.tv_nsec); for (i = 0; i < 8; i++) { /* Содержательные действия. */ /* Предполагается, что они укладываются в период */ sleep (1); /* Доспим до конца периода */ tmspc_add (&t_bp, &prd, &t_bp); (void) clock_nanosleep (clk_id, TIMER_ABSTIME, &t_bp, NULL); (void) clock_gettime (clk_id, &t_tmp); printf ("Конец периода: %ld сек %ld нсек\n", t_tmp.tv_sec, t_tmp.tv_nsec); } return 0; } |
| Листинг 3.20. Пример применения функции clock_nanosleep() для организации периодического процесса. |
| Закрыть окно |
| Начало выполнения: 1079080828 сек 194254000 нсек Конец периода: 1079080829 сек 460021000 нсек Конец периода: 1079080830 сек 710023000 нсек Конец периода: 1079080831 сек 960020000 нсек Конец периода: 1079080833 сек 210021000 нсек Конец периода: 1079080834 сек 460023000 нсек Конец периода: 1079080835 сек 710021000 нсек Конец периода: 1079080836 сек 960094000 нсек Конец периода: 1079080838 сек 210022000 нсек |
| Листинг 3.21. Возможные результаты выполнения программы, использующей функцию clock_nanosleep() для организации периодического процесса. |
| Закрыть окно |
|
#include |
| Листинг 3.22. Описание функции timer_create(). |
| Закрыть окно |
|
#include |
| Листинг 3.23. Описание функции timer_delete(). |
| Закрыть окно |
|
#include int timer_gettime (timer_t timerid, struct itimerspec *value); int timer_settime ( timer_t timerid, int flags, const struct itimerspec *restrict value, struct itimerspec *restrict ovalue); int timer_getoverrun (timer_t timerid); |
| Листинг 3.24. Описание функций timer_gettime(), timer_settime() и timer_getoverrun(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Многопотоковый вариант обеда философов */ /* с использованием сигналов реального времени */ /* и таймера для контроля длительности обеда */ /* * * * * * * * * * * * * * * * * * * * * * * */ #include /* Число обедающих философов */ #define QPH 5 /* Время (в секундах) на обед */ #define FO 15 /* Длительность еды */ #define ernd (rand () % 3 + 1) /* Длительность разговора */ #define trnd (rand () % 5 + 1) /* Номер сигнала, используемого для захвата */ /* и освобождения вилок */ #define SIG_FORK SIGRTMIN /* Номер сигнала, используемого таймером */ /* для окончания обеда */ #define SIG_DEND (SIGRTMIN + 1) /* Номер сигнала, используемого */ /* для информирования философа */ #define SIG_PHIL SIGINT static pthread_t pt_id [QPH]; /* Массив идентификаторов */ /* потоков-философов */ static int fork_busy [QPH] = {0, }; /* Состояние вилок */ static int phil_req [QPH] = {0, }; /* Невыполненные */ /* заявки на вилки */ static sigjmp_buf phil_env [QPH]; /* Массив буферов для */ /* нелокальных переходов */ static pid_t pid_wt; /* Идентификатор процесса, */ /* контролирующего вилки */ static pthread_key_t phil_key; /* Ключ индивидуальных */ /* данных потоков-философов */ /* * * * * * * * * * * * * * * * * * */ /* Функция обработки сигнала SIG_PHIL */ /* * * * * * * * * * * * * * * * * * */ static void phil_eat (int signo) { int no; /* Номер философа, которому достался сигнал */ no = (int) pthread_getspecific (phil_key); if ((no > 0) && (no <= QPH)) { siglongjmp (phil_env [no – 1], signo); } } /* * * * * * * * * * * * * * * * * * */ /* Деструктор индивидуальных данных */ /* потоков-философов */ /* * * * * * * * * * * * * * * * * * */ static void phil_destructor (void *no) { printf ("Философ %d закончил обед\n", (int) no); } /* * * * * * * * * * * * * * * * * * * * * * * */ /* Функция, вызываемая при срабатывании таймера, */ /* контролирующего длительность обеда */ /* * * * * * * * * * * * * * * * * * * * * * ** */ static void end_ph_dinner (union sigval dummy) { int i; for (i = 0; i < QPH; i++) { (void) pthread_cancel (pt_id [i]); } } /* * * * * * * * * * * * * * * * * * * * * */ /* Попытка выполнить заявку на захват вилок */ /* от философа номер no, если она есть */ /* * * * * * * * * * * * * * * * * * * * * */ static void fork_lock (int no) { if (phil_req [no – 1] != 0) { /* Заявка есть. */ /* Вилки свободны? */ if ((fork_busy [no – 1] == 0) && (fork_busy [no % QPH] == 0)) { /* Выполним заявку */ fork_busy [no – 1] = fork_busy [no % QPH] = 1; phil_req [no – 1] = 0; (void) pthread_kill (pt_id [no – 1], SIG_PHIL); } } } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, обслуживающего заявки на */ /* захват и освобождение вилок. */ /* Заявка передается в виде значения, ассоциированного */ /* с сигналом signo. */ /* Значение no > 0 запрашивает захват вилок для философа */ /* с номером no, no < 0 – освобождение вилок философа -no */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_waiter (void *signo) { siginfo_t sinfo; /* Структура для получения данных */ /* о сигнале */ int no; /* Номер философа, приславшего */ /* заявку */ sigset_t s_sgno; /* Маска ожидаемых сигналов */ pid_wt = getpid (); /* Сформируем маску ожидаемых сигналов */ if ((sigemptyset (&s_sgno) != 0) || (sigaddset (&s_sgno, (int) signo) != 0)) { perror ("SIGEMPTYSET/SIGADDSET"); return (NULL); } while (1) { if (sigwaitinfo (&s_sgno, &sinfo) != (int) signo) { return (NULL); } else { /* Поступила заявка. */ /* Посмотрим, что от нас хотят */ if ((no = sinfo.si_value.sival_int) > 0) { /* Заявка на захват вилок. */ /* Запомним ее ... */ phil_req [no – 1] = 1; /* ... и попробуем выполнить */ fork_lock (no); } else { /* Освобождение вилок */ no = -no; fork_busy [no – 1] = fork_busy [no % QPH] = 0; /* Попробуем выполнить заявки от соседей */ fork_lock (no % QPH + 1); fork_lock (no == 1 ? QPH : (no – 1)); } } /* Другие сигналы нас не интересуют */ } /* while (1) */ } /* * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока-философа. */ /* Аргумент – номер философа */ /* * * * * * * * * * * * * * * * * * */ void *start_phil (void *no) { union sigval sval; /* Значение посылаемого сигнала: */ /* (int) no – заказ вилок */ /* -(int) no – освобождение вилок */ /* Запомним значение аргумента в качестве */ /* индивидуальных данных потока */ (void) pthread_setspecific (phil_key, no); if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) { /* Сюда придем после нелокального перехода */ /* из обработчика сигнала SIG_PHIL. */ /* Философ просил вилки и получил их */ printf ("Философ %d ест\n", (int) no); if (sleep (ernd) != 0) { return (NULL); } /* Отдает вилки */ sval.sival_int = -((int) no); (void) sigqueue (pid_wt, SIG_FORK, sval); } { /* Обед */ printf ("Философ %d беседует\n", (int) no); if (sleep (trnd) != 0) { return (NULL); } /* Пытается взять вилки */ sval.sival_int = (int) no; (void) sigqueue (pid_wt, SIG_FORK, sval); /* Пока вилки заняты, приходится беседовать... */ printf ("Философ %d беседует в ожидании вилок\n", (int) no); sleep (FO); } /* Конец обеда */ return (NULL); } /* * * * * * * * * * */ /* Организация обеда */ /* * * * * * * * * * */ int main (void) { int no; /* Номер философа */ struct sigaction sact; /* Структура для обработки */ /* обычных сигналов */ struct sigevent dend; /* Структура для генерации */ /* сигнала таймером */ timer_t dend_tmrid; /* Идентификатор таймера, */ /* контролирующего длительность */ /* обеда */ struct itimerspec dend_tmrsp = {{0, 0}, {FO, 0}}; /* Структура для взведения таймера, */ /* контролирующего длительность обеда */ pthread_t pt_wt; /* Идентификатор потока, управляющего */ /* вилками */ /* Блокируем сигнал SIG_FORK */ if ((sigemptyset (&sact.sa_mask) == 0) && (sigaddset (&sact.sa_mask, SIG_FORK) == 0)) { (void) pthread_sigmask (SIG_BLOCK, &sact.sa_mask, (sigset_t *) NULL); } /* Установим для сигнала SIG_FORK флаг SA_SIGINFO */ sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = (void (*) (int, siginfo_t *, void *)) SIG_DFL; (void) sigaction (SIG_FORK, &sact, (struct sigaction *) NULL); /* Установим реакцию на сигнал SIG_PHIL */ sact.sa_handler = phil_eat; sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIG_PHIL, &sact, (struct sigaction *) NULL); /* Создадим поток, захватывающий и освобождающий вилки */ if ((errno = pthread_create (&pt_wt, NULL, start_waiter, (void *) SIG_FORK)) != 0) { perror ("PTHREAD_CREATE-1"); return (errno); } /* Создадим ключ индивидуальных данных */ if ((errno = pthread_key_create (&phil_key, phil_destructor)) != 0) { perror ("PTHREAD_KEY_CREATE"); return (errno); } /* Создадим и взведем таймер, */ /* контролирующий длительность обеда */ dend.sigev_notify = SIGEV_THREAD; dend.sigev_signo = SIG_DEND; dend.sigev_notify_function = end_ph_dinner; dend.sigev_notify_attributes = NULL; if (timer_create (CLOCK_REALTIME, &dend, &dend_tmrid) != 0) { perror ("TIMER_CREATE"); return (-1); } if (timer_settime (dend_tmrid, 0, &dend_tmrsp, NULL) != 0) { perror ("TIMER_SETTIME"); return (-1); } /* Все – к столу */ for (no = 1; no <= QPH; no++) { if ((errno = pthread_create (&pt_id [no – 1], NULL, start_phil, (void *) no)) != 0) { perror ("PTHREAD_CREATE"); return (no); } } /* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) pthread_join (pt_id [no – 1], NULL); } (void) pthread_key_delete (phil_key); (void) timer_delete (dend_tmrid); /* Поток, контролирующий вилки, */ /* можно не терминировать и не ждать */ return 0; } |
| Листинг 3.25. Пример реализации обеда философов с использованием сигналов реального времени и таймера. |
| Закрыть окно |
| Философ 1 беседует Философ 2 беседует Философ 3 беседует Философ 4 беседует Философ 5 беседует Философ 4 беседует в ожидании вилок Философ 4 ест Философ 2 беседует в ожидании вилок Философ 2 ест Философ 3 беседует в ожидании вилок Философ 4 беседует Философ 1 беседует в ожидании вилок Философ 5 беседует в ожидании вилок Философ 5 ест Философ 2 беседует Философ 3 ест Философ 5 беседует Философ 1 ест Философ 2 беседует в ожидании вилок Философ 4 беседует в ожидании вилок Философ 3 беседует Философ 4 ест Философ 5 беседует в ожидании вилок Философ 1 беседует Философ 2 ест Философ 2 беседует Философ 1 беседует в ожидании вилок Философ 4 беседует Философ 1 ест Философ 2 беседует в ожидании вилок Философ 3 беседует в ожидании вилок Философ 3 ест Философ 1 беседует Философ 5 ест Философ 4 беседует в ожидании вилок Философ 5 беседует Философ 1 закончил обед Философ 2 закончил обед Философ 3 беседует Философ 4 закончил обед Философ 5 закончил обед Философ 3 беседует в ожидании вилок Философ 3 закончил обед |
| Листинг 3.26. Возможные результаты выполнения программы, реализующей обед философов с использованием сигналов реального времени и таймера. |
| Закрыть окно |
Сигналы реального времени
Приложениям реального времени требуются средства надежного, детерминированного, асинхронного извещения (уведомления) о событиях. Теоретически для этого можно было бы разработать совершенно новый механизм, допускающий исключительно эффективную реализацию, однако авторы стандарта POSIX-2001 не пошли по такому пути по той простой причине, что уже имеется механизм сигналов, который обладает почти всеми необходимыми свойствами. "Почти", потому что он не решает следующих проблем.
Чтобы устранить перечисленные недостатки, механизм сигналов был расширен, а в стандарт была введена необязательная часть, получившая название "сигналы реального времени" (Realtime Signals Extension, RTS).
Номера сигналов реального времени лежат в диапазоне от SIGRTMIN до SIGRTMAX. Всего таких сигналов должно быть не меньше, чем RTSIG_MAX. Обеспечивается ли поведение в реальном времени для других сигналов, зависит от реализации.
"Жизненный цикл" сигналов реального времени состоит из четырех фаз:
Сигналы реального времени могут генерироваться при срабатывании высокоточных таймеров, завершении операции асинхронного ввода/вывода, поступлении межпроцессного сообщения, выполнении функции sigqueue() и т.д.
На фазе генерации сигналов центральную роль играет определенная в заголовочном файле
int sigev_notify; /* Способ уведомления */ int sigev_signo; /* Номер сигнала */
union sigval sigev_value; /* Значение сигнала */
void (*) (union sigval) sigev_notify_function; /* Функция уведомления */
(pthread_attr_t *)sigev_notify_attributes; /* Атрибуты уведомления */
Значение поля sigev_notify определяет способ уведомления об асинхронном событии.
Константа SIGEV_NONE означает отсутствие уведомления (событие проходит незамеченным).
Константа SIGEV_SIGNAL предписывает сгенерировать сигнал с номером sigev_signo. Если для этого сигнала установлен флаг SA_SIGINFO, он (сигнал) ставится в очередь к процессу. Тем самым обеспечивается надежная доставка уведомлений о событиях.
Константа SIGEV_THREAD задает в качестве механизма уведомления вызов функции.
Чтобы обеспечить дифференциацию между сигналами одного типа, с ними ассоциируется значение, которое задается при генерации сигнала либо в явном виде как отдельный аргумент соответствующей функции, либо как значение поля sigev_value структуры-аргумента типа sigevent.
Значение сигнала реального времени может быть целым числом или указателем, поскольку объединение типа sigval должно определяться со следующими полями:
int sival_int; /* Значение сигнала – целое число */ void *sival_ptr; /* Значение сигнала – указатель */
Если для многопотоковой программы в качестве способа уведомления о наступлении события избран вызов функции (константа SIGEV_THREAD в поле sigev_notify), то указатель на эту функцию извлекается из поля sigev_notify_function, а ее аргументом служит значение сигнала. Эта функция выполняется как стартовая для вновь созданного обособленного потока управления с атрибутным объектом *sigev_notify_attributes.
При подобном способе уведомления сигналы как таковые не генерируются, а уведомляющую функцию не следует путать с функцией обработки сигнала, которая вызывается в ином контексте и с другими аргументами.
После того, как сигнал сгенерирован функцией sigqueue() или иным способом, позволяющим задать определяемое приложением значение, наступает фаза ожидания. Сигналы одного типа ставятся в очередь к процессу в порядке генерации.
Сигналы, сгенерированные с помощью вызова функции kill() или в результате наступления такого события, как аппаратное прерывание, срабатывание будильника или ввод управляющего символа с терминала, для которых реализация не обеспечивает постановку в очередь, никак на эту очередь не влияют.
При наличии нескольких неблокированных ждущих сигналов реального времени, их доставка процессу производится в порядке возрастания номеров. Тем самым поддерживается ранжированная по приоритетам доставка уведомлений, а роль приоритета играет номер сигнала.
На фазе обработки уведомления об асинхронном событии используется структура типа siginfo_t. Для сигналов реального времени она включает, помимо описанных в курсе [1], дополнительный элемент
union sigval si_value; /* Значение сигнала */
в который переносится значение из поля sigev_value структуры типа sigevent.
Подразумеваемые действия при обработке сигнала реального времени (SIG_DFL) состоят в аварийном завершении процесса.
Простейший способ сгенерировать сигнал реального времени – обратиться к функции sigqueue() (см. листинг 3.12).
#include
Листинг 3.12. Описание функции sigqueue(). (html, txt)
Функция sigqueue() посылает сигнал с номером signo и значением value процессу, идентификатор которого задан аргументом pid. Права на посылку сигнала определяются так же, как и для функции kill(); аналогично kill(), при нулевом значении signo сигнал не посылается, а проверяется существование процесса с идентификатором pid.
Функция sigqueue() завершается немедленно, без какого-либо ожидания. Если для сигнала signo установлен флаг SA_SIGINFO и в наличии достаточно ресурсов, сигнал ставится в очередь к процессу pid. Если флаг SA_SIGINFO не установлен, сигнал посылается процессу-получателю по крайней мере один раз, но, возможно, без ассоциированного с ним значения.
Если процесс посылает сигнал сам себе, он (сигнал) будет доставлен вызывающему потоку управления до выхода из функции sigqueue().
Дождаться доставки сигнала реального времени можно с помощью функций sigwaitinfo() и sigtimedwait() (см. листинг 3.13), являющихся аналогами рассмотренной в курсе [1] функции sigwait().
#include
int sigwaitinfo ( const sigset_t *restrict set, siginfo_t *restrict info);
int sigtimedwait ( const sigset_t *restrict set, siginfo_t * restrict info, const struct timespec *restrict timeout);
Листинг 3.13. Описание функций sigwaitinfo() и sigtimedwait(). (html, txt)
Данные функции возвращают в качестве нормального результата номер полученного сигнала, входящего в набор set. Кроме того, если значение аргумента info отлично от NULL, заполняются поля si_signo (номер сигнала), si_code (источник сигнала) и, возможно, si_value (значение, если оно ассоциировано с сигналом) указуемого объекта структурного типа siginfo_t. Разумеется, полученный сигнал изымается из очереди ждущих, а соответствующие ресурсы освобождаются.
Функция sigtimedwait() отличается тем, что ограничивает время ожидания сигнала заданным интервалом. Если аргумент timeout является пустым указателем, поведение функции не специфицировано. Если реализация поддерживает монотонные часы (CLOCK_MONOTONIC), они и будут использоваться для контроля времени ожидания.
Если несколько потоков управления ждут один сигнал, он достанется кому-то одному из них.
Поле ss_flags определяет состояние нового стека. Флаг SS_DISABLE в этом поле по сути означает отказ от альтернативного стека; в таком случае значения ss_sp и ss_size игнорируются.
Альтернативный стек располагается в диапазоне адресов от ss_sp до (но не включая) ss_sp + ss_size. Стандарт не специфицирует, с какого конца и в каком направлении растет стек.
Если значением аргумента oss служит непустой указатель, то после возврата из функции sigaltstack() в указуемую структуру типа stack_t будут помещены прежние значения характеристик альтернативного стека. В частности, в поле ss_flags будет отражено состояние стека, которое могут характеризовать по крайней мере следующие флаги.
SS_ONSTACK
Этот флаг означает, что в данный момент процесс выполняется на альтернативном стеке обработки сигналов. Попытки модифицировать стек в то время, как на нем происходит выполнение, трактуются как ошибочные.
SS_DISABLE
Этот флаг означает, что в данный момент альтернативный стек обработки сигналов отключен.
Константа SIGSTKSZ задает подразумеваемый, а MINSIGSTKSZ – минимально допустимый размер альтернативного стека для функции обработки сигналов. Обычно, чтобы учесть системные накладные расходы, нужно сложить потребности приложения и MINSIGSTKSZ.
Естественно, все заботы по контролю за переполнением и исчерпанием альтернативного стека возлагаются на приложение.
На листинге 3.15 показана типичная схема определения альтернативного стека.
#include
if ((sighstk.ss_sp = malloc( SIGSTKSZ)) == NULL) { perror ("malloc (SIGSTKSZ)"); /* Аварийное завершение */ } sighstk.ss_size = SIGSTKSZ; sighstk.ss_flags = 0; if (sigaltstack (&sighstk, (stack_t *) NULL) != 0) { perror ("SIGALTSTACK"); . . . } . . .
Листинг 3.15. Типичная схема определения альтернативного стека.
Обработка сигналов (не обязательно реального времени) нередко сочетается с нелокальными переходами. В таких случаях могут оказаться полезными функции sigsetjmp() и siglongjmp() (см.
листинг 3.16). Они аналогичны функциям setjmp() и longjmp() с единственным содержательным отличием: если значение аргумента savemask функции sigsetjmp() отлично от нуля, маска сигналов вызывающего потока управления сохраняется как часть его окружения и восстанавливается после выполнения siglongjmp().
#include
int sigsetjmp (sigjmp_buf env, int savemask);
void siglongjmp (sigjmp_buf env, int val);
Листинг 3.16. Описание функций sigsetjmp() и siglongjmp().
В качестве примера использования сигналов реального времени приведем еще один вариант реализации обеда философов (см. листинг 3.17).
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Многопотоковый вариант обеда философов */ /* с использованием сигналов реального времени */ /* * * * * * * * * * * * * * * * * * * * * * * */
#include
/* Число обедающих философов */ #define QPH 5
/* Время (в секундах) на обед */ #define FO 15
/* Длительность еды */ #define ernd (rand () % 3 + 1)
/* Длительность разговора */ #define trnd (rand () % 5 + 1)
/* Номер сигнала, используемого для захвата и освобождения вилок */ #define SIG_FORK SIGRTMIN
/* Номер сигнала, используемого для информирования философа */ #define SIG_PHIL SIGINT
static pthread_t pt_id [QPH]; /* Массив идентификаторов */ /* потоков - философов */ static int fork_busy [QPH] = {0, }; /* Состояние вилок */ static int phil_req [QPH] = {0, }; /* Невыполненные */ /* заявки на вилки */ static sigjmp_buf phil_env [QPH]; /* Массив буферов для */ /* нелокальных переходов */
static pid_t pid_wt; /* Идентификатор процесса,*/ /* контролирующего вилки */
static pthread_key_t phil_key; /* Ключ индивидуальных */ /* данных потоков-философов */
/* * * * * * * * * * * * * * * * * * * */ /* Функция обработки сигнала SIG_PHIL */ /* * * * * * * * * * * * * * * * * * * */ static void phil_eat (int signo) { int no; /* Номер философа, которому достался сигнал */
no = (int) pthread_getspecific (phil_key); if ((no > 0) && (no <= QPH)) { siglongjmp (phil_env [no – 1], signo); } }
/* * * * * * * * * * * * * * * * * * * * * * */ /* Попытка выполнить заявку на захват вилок */ /* от философа номер no, если она есть */ /* * * * * * * * * * * * * * * * * * * * * * */ static void fork_lock (int no) { if (phil_req [no – 1] != 0) { /* Заявка есть. */ /* Вилки свободны? */ if ((fork_busy [no – 1] == 0) && (fork_busy [no % QPH] == 0)) { /* Выполним заявку */ fork_busy [no – 1] = fork_busy [no % QPH] = 1; phil_req [no – 1] = 0; (void) pthread_kill (pt_id [no – 1], SIG_PHIL); } } }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, обслуживающего заявки на */ /* захват и освобождение вилок. */ /* Заявка передается в виде значения, ассоциированного */ /* с сигналом signo. */ /* Значение no > 0 запрашивает захват вилок для философа */ /* с номером no, no < 0 – освобождение вилок философа -no */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_waiter (void *signo) { siginfo_t sinfo; /* Структура для получения данных */ /* о сигнале */ int no; /* Номер философа, приславшего заявку */ sigset_t s_sgno; /* Маска ожидаемых сигналов */
pid_wt = getpid ();
/* Сформируем маску ожидаемых сигналов */ if ((sigemptyset (&s_sgno) != 0) || (sigaddset (&s_sgno, (int) signo) != 0)) { perror ("SIGEMPTYSET/SIGADDSET"); return (NULL); }
while (1) { if (sigwaitinfo (&s_sgno, &sinfo) != (int) signo) { return (NULL); } else { /* Поступила заявка. */ /* Посмотрим, что от нас хотят */ if ((no = sinfo.si_value.sival_int) > 0) { /* Заявка на захват вилок. */ /* Запомним ее ... */ phil_req [no – 1] = 1; /* ... и попробуем выполнить */ fork_lock (no); } else { /* Освобождение вилок */ no = -no; fork_busy [no – 1] = fork_busy [no % QPH] = 0; /* Попробуем выполнить заявки от соседей */ fork_lock (no % QPH + 1); fork_lock (no == 1 ? QPH : (no – 1)); } } /* Другие сигналы нас не интересуют */ } /* while (1) */ }
/* * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока-философа. */ /* Аргумент – номер философа */ /* * * * * * * * * * * * * * * * * * */ void *start_phil (void *no) { int fo; /* Время до конца обеда */ int t; /* Время очередного отрезка еды или беседы */ time_t tbe; /* Время, когда философу понадобились вилки */ union sigval sval; /* Значение посылаемого сигнала: */ /* (int) no – заказ вилок */ /* -(int) no – освобождение вилок */
/* Запомним значение аргумента в качестве */ /* индивидуальных данных потока */ (void) pthread_setspecific (phil_key, no);
/* Подготовка к обеду */ fo = FO;
if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) { /* Сюда придем после нелокального перехода */ /* из обработчика сигнала SIG_PHIL. */ /* Философ просил вилки и получил их */ printf ("Философ %d ест\n", (int) no); t = ernd; sleep (t); /* Нужно вычесть времена еды и ожидания вилок */ fo -= (int) (time ((time_t *) NULL) – tbe) + t; /* Отдает вилки */ sval.sival_int = -((int) no); (void) sigqueue (pid_wt, SIG_FORK, sval); }
while (fo > 0) { printf ("Философ %d беседует\n", (int) no); t = trnd; sleep (t); fo -= t;
/* Пытается взять вилки */ tbe = time ((time_t *) NULL); sval.sival_int = (int) no; (void) sigqueue (pid_wt, SIG_FORK, sval);
/* Пока вилки заняты, приходится беседовать... */ printf ("Философ %d беседует в ожидании вилок\n", (int) no); sleep (fo); fo = 0; } /* while */ printf ("Философ %d закончил обед\n", (int) no);
return (NULL); }
/* * * * * * * * * * */ /* Организация обеда */ /* * * * * * * * * * */ int main (void) { int no; /* Номер философа */ struct sigaction sact; /* Структура для обработки */ /* обычных сигналов */ pthread_t pt_wt; /* Идентификатор потока, */ /* управляющего вилками */
/* Блокируем сигнал SIG_FORK */ if ((sigemptyset (&sact.sa_mask) == 0) && (sigaddset (&sact.sa_mask, SIG_FORK) == 0)) { (void) pthread_sigmask (SIG_BLOCK, &sact.sa_mask, (sigset_t *) NULL); }
/* Установим для сигнала SIG_FORK флаг SA_SIGINFO */ sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = (void (*) (int, siginfo_t *, void *)) SIG_DFL; (void) sigaction (SIG_FORK, &sact, (struct sigaction *) NULL);
/* Установим реакцию на сигнал SIG_PHIL */ sact.sa_handler = phil_eat; sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIG_PHIL, &sact, (struct sigaction *) NULL);
/* Создадим поток, захватывающий и освобождающий вилки */ if ((errno = pthread_create (&pt_wt, NULL, start_waiter, (void *) SIG_FORK)) != 0) { perror ("PTHREAD_CREATE-1"); return (errno); }
/* Создадим ключ индивидуальных данных */ if ((errno = pthread_key_create (&phil_key, (void (*) (void *)) NULL)) != 0) { perror ("PTHREAD_KEY_CREATE"); return (errno); }
/* Все – к столу */ for (no = 1; no <= QPH; no++) { if ((errno = pthread_create (&pt_id [no – 1], NULL, start_phil, (void *) no)) != 0) { perror ("PTHREAD_CREATE"); return (no); } }
/* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) pthread_join (pt_id [no – 1], NULL); }
(void) pthread_key_delete (phil_key);
/* Завершим поток, контролирующий вилки */ (void) pthread_cancel (pt_wt); (void) pthread_join (pt_wt, NULL);
return 0; }
Листинг 3.17. Пример реализации обеда философов с использованием сигналов реального времени.
Здесь сигналы реального времени служат средством межпотокового взаимодействия. В качестве значений сигналов передаются данные для захвата и освобождения вилок. В данном случае сигналы реального времени обрабатываются синхронно, посредством функции sigwaitinfo(), в рамках специально выделенного потока управления (что, конечно, нечестно). Разумеется, ожидаемые таким способом сигналы предварительно должны быть блокированы.
Для информирования философов о том, что нужные вилки захвачены и можно приступать к еде, применяются обычные сигналы, обрабатываемые асинхронно.
В качестве небольшой тонкости обратим внимание на значение второго аргумента (оно отлично от нуля) в вызове функции sigsetjmp(). Таким образом обеспечиваетсявосстановление маски сигналов после нелокального перехода из функции обработки сигнала SIG_PHIL. Если этого не сделать, сигнал SIG_PHIL останется блокированным и второй раз философ вилок уже не получит (точнее, он не узнает о том, что вилки для него захвачены).
Возможные результаты выполнения приведенной программы показаны на листинге 3.18.
Философ 1 беседует Философ 2 беседует Философ 3 беседует Философ 4 беседует Философ 5 беседует Философ 4 беседует в ожидании вилок Философ 4 ест Философ 2 беседует в ожидании вилок Философ 2 ест Философ 3 беседует в ожидании вилок Философ 4 беседует Философ 1 беседует в ожидании вилок Философ 5 беседует в ожидании вилок Философ 5 ест Философ 2 беседует Философ 3 ест Философ 5 беседует Философ 1 ест Философ 2 беседует в ожидании вилок Философ 4 беседует в ожидании вилок Философ 3 беседует Философ 4 ест Философ 5 беседует в ожидании вилок Философ 1 беседует Философ 2 ест Философ 2 беседует Философ 4 закончил обед Философ 1 беседует в ожидании вилок Философ 5 ест Философ 2 беседует в ожидании вилок Философ 2 ест Философ 3 беседует в ожидании вилок Философ 3 закончил обед Философ 1 закончил обед Философ 5 закончил обед Философ 2 закончил обед
Листинг 3.18. Возможные результаты выполнения программы, реализующей обед философов с использованием сигналов реального времени.
Передача и прием сообщений в реальном времени
Средства локальной передачи и приема сообщений присутствуют, вероятно, во всех или почти во всех операционных системах. Во многих версиях ОС Unix представлено по нескольку разновидностей подобных средств. Следуя за исторически сложившимися реализациями, стандарт POSIX-2001 предусматривает два вида очередей сообщений и обслуживающих их функций. Один из них был рассмотрен нами в курсе [1]. Здесь мы опишем вторую разновидность, предназначенную для использования в системах реального времени.
Описываемые очереди сообщений являются общесистемными. Они доступны по именам, которые могут быть маршрутными именами файлов.
Над очередями сообщений определены следующие группы функций:
Одну очередь могут открыть несколько посылающих и/или принимающих сообщения процессов. При открытии может производиться контроль прав доступа.
Для каждой очереди задается фиксированная верхняя граница размера сообщений, которые могут быть в эту очередь отправлены.
На порядок приема влияет имеющийся механизм приоритетов сообщений.
Процесс может получать асинхронные уведомления о том, что в очереди появилось сообщение.
Для открытия очереди служит функция mq_open() (см. листинг 4.1), которая, по аналогии с файлами, создает описание открытой очереди и ссылающийся на него дескриптор типа mqd_t, возвращаемый в качестве нормального результата.
#include
Листинг 4.1. Описание функции mq_open(). (html, txt)
Трактовка имени очереди (аргумент name) зависит от реализации. Оговаривается только, что оно должна подчиняться ограничениям, налагаемым на маршрутные имена, и что если его первым символом является '/', то процессы, вызывающие mq_open() с одинаковыми значениями name, ссылаются на одну и ту же очередь (если, конечно, ее не удаляли).
При реализации дескрипторов очередей сообщений могут применяться файловые дескрипторы. В таком случае приложение может открыть не менее OPEN_MAX файлов и очередей.
Аргумент oflag специфицирует запрашиваемые виды доступа к очереди: на прием (чтение) и отправку (запись). Контроль прав доступа для очередей сообщений производится так же, как и для файлов.
В целом набор флагов и их трактовка для очередей сообщений те же, что и для файлов: O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NONBLOCK.
Если установлен флаг O_CREAT, то при вызове функции mq_open() необходимо задать два дополнительных аргумента: режим доступа (тип mode_t) и указатель на атрибутный объект (тип struct mq_attr *) создаваемой очереди сообщений.
Согласно стандарту POSIX-2001, структура типа mq_attr, описанная в заголовочном файле
long mq_flags; /* Флаги очереди сообщений */ long mq_maxmsg; /* Максимальное число сообщений в очереди */ long mq_msgsize; /* Максимальный размер сообщения в очереди */ long mq_curmsgs; /* Текущее число сообщений в очереди */
Для опроса и/или установки атрибутов очереди служат функции mq_getattr() и mq_setattr() (см. листинг 4.2). Впрочем, про установку атрибутов сказано, пожалуй слишком сильно: посредством вызова 2 можно изменить лишь состояние флага 2 (и, возможно, некоторых других флагов, зависящих от реализации).
#include
int mq_getattr ( mqd_t mqdes, struct mq_attr *mqstat);
int mq_setattr (mqd_t mqdes, const struct mq_attr *restrict mqstat, struct mq_attr *restrict omqstat);
Листинг 4.2. Описание функций mq_getattr() и mq_setattr(). (html, txt)
После того, как процесс завершил работу с очередью сообщений, соответствующий дескриптор следует закрыть, воспользовавшись функцией mq_close() (см. листинг 4.3).
#include
Листинг 4.3. Описание функции mq_close(). (html, txt)
Если очередь сообщений стала совсем ненужной, ее можно удалить с помощью функции mq_unlink() (см.листинг 4.4).
#include
Листинг 4.4. Описание функции mq_unlink(). (html, txt)
При реализации дескрипторов очередей сообщений могут применяться файловые дескрипторы. В таком случае приложение может открыть не менее OPEN_MAX файлов и очередей.
Аргумент oflag специфицирует запрашиваемые виды доступа к очереди: на прием (чтение) и отправку (запись). Контроль прав доступа для очередей сообщений производится так же, как и для файлов.
В целом набор флагов и их трактовка для очередей сообщений те же, что и для файлов: O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NONBLOCK.
Если установлен флаг O_CREAT, то при вызове функции mq_open() необходимо задать два дополнительных аргумента: режим доступа (тип mode_t) и указатель на атрибутный объект (тип struct mq_attr *) создаваемой очереди сообщений.
Согласно стандарту POSIX-2001, структура типа mq_attr, описанная в заголовочном файле
long mq_flags; /* Флаги очереди сообщений */ long mq_maxmsg; /* Максимальное число сообщений в очереди */ long mq_msgsize; /* Максимальный размер сообщения в очереди */ long mq_curmsgs; /* Текущее число сообщений в очереди */
Для опроса и/или установки атрибутов очереди служат функции mq_getattr() и mq_setattr() (см. листинг 4.2). Впрочем, про установку атрибутов сказано, пожалуй слишком сильно: посредством вызова 2 можно изменить лишь состояние флага 2 (и, возможно, некоторых других флагов, зависящих от реализации).
#include
int mq_getattr ( mqd_t mqdes, struct mq_attr *mqstat);
int mq_setattr (mqd_t mqdes, const struct mq_attr *restrict mqstat, struct mq_attr *restrict omqstat);
Листинг 4.2. Описание функций mq_getattr() и mq_setattr().
После того, как процесс завершил работу с очередью сообщений, соответствующий дескриптор следует закрыть, воспользовавшись функцией mq_close() (см. листинг 4.3).
#include
Листинг 4.3. Описание функции mq_close().
Если очередь сообщений стала совсем ненужной, ее можно удалить с помощью функции mq_unlink() (см. листинг 4.4).
#include
Листинг 4.4. Описание функции mq_unlink().
Переходя к описанию содержательных действий с очередями сообщений, укажем, что отправка осуществляется функциями mq_send() и mq_timedsend() (см. листинги 4.5 и 4.6).
#include
Листинг 4.5. Описание функции mq_send(). #include
Листинг 4.6. Описание функции mq_timedsend().
Более точно: функции mq_send() и mq_timedsend() помещают сообщение из msg_len байт, на которое указывает аргумент msg_ptr, в очередь, заданную дескриптором mqdes (если она не полна), в соответствии с приоритетом msg_prio (большим значениям msg_prio соответствует более высокий приоритет сообщения; допустимый диапазон - от 0 до MQ_PRIO_MAX - 1).
Если очередь полна, а флаг O_NONBLOCK не установлен, вызов mq_send() блокируется до появления свободного места. Функция mq_timedsend() в таких случаях контролирует длительность ожидания (до заданного аргументом abstime абсолютного момента времени по часам CLOCK_REALTIME).
Для извлечения (разумеется, с удалением) сообщений из очереди служат функции mq_receive() и mq_timedreceive() (см. листинги 4.7 и 4.8). Извлекается самое старое из сообщений с самым высоким приоритетом и помещается в буфер, на который указывает аргумент msg_ptr. Если размер буфера (значение аргумента msg_len) меньше атрибута очереди mq_msgsize, вызов завершается неудачей. Если значение msg_prio_ptr отлично от NULL, в указуемый объект помещается приоритет принятого сообщения.
#include
Листинг 4.7. Описание функции mq_receive().
#include
Листинг 4.8. Описание функции mq_timedreceive().
Если очередь пуста, а флаг O_NONBLOCK не установлен, вызов mq_receive() блокируется до появления сообщения. Функция mq_timedreceive() в таких случаях контролирует длительность ожидания.
Нормальным результатом обеих функций является размер в байтах извлеченного из очереди сообщения. Как всегда, признаком ошибки служит -1.
Посредством функции mq_notify() (см. листинг 4.9) процесс может зарегистрироваться на получение уведомления о том, что в очередь, бывшую до этого пустой, поступило сообщение.
#include
Листинг 4.9. Описание функции mq_notify().
Для уведомлений используется механизм сигналов реального времени. В каждый момент времени только один процесс может быть зарегистрированным, а сама регистрация является одноразовой: после поступления уведомления она отменяется. Процесс может добровольно отменить регистрацию, если передаст NULL в качестве значения аргумента notification.
Если, наряду с зарегистрированным процессом, имеется поток управления, ожидающий сообщения в вызове mq_receive() или mq_timedreceive(), поступившее сообщение достанется потоку, а процесс не получит никакого уведомления, как если бы очередь осталась пустой. Это очень по-человечески: живое стояние в очереди всегда ценилось выше всяких списков и уведомлений.
На наш взгляд, возможность получать уведомления о том, что очередь сообщений стала непустой, является скорее заплатой, призванной скрыть неполноту функциональности poll() и select(), чем полноценным, практически полезным средством. В стандартизованном интерфейсе отсутствует концептуальная целостность - из соображений симметрии необходимы уведомления о том, что очередь стала неполной и в нее можно отправлять сообщения. Далее, состояние очереди может меняться параллельно с регистрацией и/или получением уведомления, и нет никаких средств, чтобы сделать соответствующую транзакцию атомарной. В результате остается неясным, когда процесс получит уведомление (и получит ли он его вообще), какое сообщение он примет после получения уведомления (и будет ли что принимать) и т.п., хотя, с другой стороны, устраивать конкуренцию за сообщения тоже не обязательно.
Поучительно сопоставить два вида очередей сообщений - только что описанные и те, что были представлены в курсе [1]. Сразу видно, что первые устроены проще и, следовательно, могут быть реализованы эффективнее. Во-первых, структура типа mq_attr гораздо компактнее, чем msqid_ds. Нет и речи о хранении времени последних операций и идентификаторов процессов, их выполнивших. Во-вторых, упрощены производимые проверки. Контролируется максимальный размер одного сообщения, а не суммарный размер сообщений в очереди. Права доступа проверяются при открытии, а не при каждой операции. Только наличие приоритетов вносит элемент сложности, сопоставимый с обработкой типов сообщений, но приоритеты так или иначе должны быть реализованы - либо на уровне ОС, либо в приложении, поэтому лучше этот аспект стандартизовать и поручить операционной системе.
Примером применения очередей сообщений послужит программа, вычисляющая взвешенную сумму целых чисел, генерируемых и отправляемых множеством потоков управления (см. листинг 4.10).
/* * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа принимает сообщения */ /* и суммирует содержащиеся в них целые числа. */ /* Массивы случайных целых чисел */ /* генерируют несколько потоков управления. */ /* Каждый поток использует свою очередь сообщений.*/ /* Используется режим без блокировки, */ /* с уведомлениями о появлении сообщений в очереди*/ /* * * * * * * * * * * * * * * * * * * * * * * * */
#include
/* Число потоков управления, порождающих случайные числа */ #define PT_N MQ_OPEN_MAX
/* Число сообщений, генерируемых каждым потоком управления */ #define MSG_N 128
/* Длина имени очереди сообщений */ #define MQ_NAME_LENGTH PATH_MAX
/* Количество целых чисел в одном сообщении */ #define MSG_INT_SIZE 32
/* Максимальное число сообщений в очереди */ #define MQ_MSGS_MAX 16
/* Приоритет порождаемого сообщения */ #define prio_rnd (rand () % MQ_PRIO_MAX)
/* Номер сигнала, используемого для уведомлений */ #define SIG_MQ_NOTIFY SIGRTMIN
/* Массив идентификаторов очередей сообщений */ static mqd_t mq_des [PT_N];
/* Массив структур для задания уведомлений */ /* о поступлении сообщений в очереди */ static struct sigevent evnt_mq_notify [PT_N];
/* Мьютекс, используемый для синхронизации */ /* доступа к переменной sum */ static pthread_mutex_t sm_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Общий результат суммирования */ static int sum = 0;
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Функция, вызываемая при получении уведомления*/ /* о том, что очередь сообщений стала непустой. */ /* Номер очереди передается как аргумент */ /* * * * * * * * * * * * * * * * * * * * * * * */ static void msg_arrvd (union sigval pt_nm) { int msg_buf [MSG_INT_SIZE]; mqd_t mqdes; unsigned int msg_prio; int msg_sum = 0; ssize_t msg_size; int i;
mqdes = mq_des [pt_nm.sival_int];
/* Примем и обработаем имеющиеся сообщения, */ /* а затем снова зарегистрируемся на получение */ /* такого же уведомления */ while ((msg_size = mq_receive (mqdes, (char *) msg_buf, MSG_INT_SIZE * sizeof (int), &msg_prio)) > 0) { for (i = 0; i < (msg_size / (signed int) sizeof (int)); i++) { msg_sum += msg_buf [i]; } msg_sum *= msg_prio; }
if ((errno = pthread_mutex_lock (&sm_mutex)) != 0) { perror ("PTHREAD_MUTEX_LOCK"); } sum += msg_sum; if ((errno = pthread_mutex_unlock (&sm_mutex)) != 0) { perror ("PTHREAD_MUTEX_UNLOCK"); }
if (mq_notify (mqdes, &evnt_mq_notify [pt_nm.sival_int]) != 0) { perror ("MQ_NOTIFY"); } }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, генерирующего числа и */ /* посылающего сообщения. */ /* Аргумент - номер очереди сообщений */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_sender (void *pt_nm) {
int msg_buf [MSG_INT_SIZE]; int i, j;
/* Сформируем и пошлем заданное число сообщений */ /* (проверяя, не переполнилась ли очередь)*/ for (j = 0; j < MSG_N; j++) { for (i = 0; i < MSG_INT_SIZE; i++) { msg_buf [i] = rand (); } if (mq_send (mq_des [(int) pt_nm], (char *) msg_buf, MSG_INT_SIZE * sizeof (int), prio_rnd) != 0) { perror ("MQ_SEND"); return (NULL); } }
return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Создание очереди сообщений, */ /* регистрация на получение уведомлений, */ /* создание и ожидание завершения потоков управления */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { /* Массив идентификаторов порождаемых */ /* потоков. Эти потоки будут */ pthread_t pt_mqs [PT_N]; /* генерировать сообщения*/ /* Массив для генерации и хранения */ /* имен очередей сообщений */ char mq_name [PT_N] [MQ_NAME_LENGTH]; struct mq_attr mqattrs; /* Атрибуты создаваемых очередей */ int i;
for (i = 0; i < PT_N; i++) { /* Создадим очереди сообщений */ /* и зарегистрируемся на получение уведомлений */ sprintf (mq_name [i], "g%d", i);
mqattrs.mq_flags = O_NONBLOCK; mqattrs.mq_maxmsg = MQ_MSGS_MAX; mqattrs.mq_msgsize = MSG_INT_SIZE * sizeof (int); mqattrs.mq_curmsgs = 0; if ((mq_des [i] = mq_open (mq_name [i], O_RDWR | O_CREAT | O_NONBLOCK, 0777, &mqattrs)) == (mqd_t) (-1)) { perror ("MQ_OPEN"); return (-1); }
/* Сформируем структуру evnt_mq_notify */ evnt_mq_notify [i].sigev_notify = SIGEV_THREAD; evnt_mq_notify [i].sigev_signo = SIG_MQ_NOTIFY; evnt_mq_notify [i].sigev_value.sival_int = i; evnt_mq_notify [i].sigev_notify_function = msg_arrvd; evnt_mq_notify [i].sigev_notify_attributes = NULL; if (mq_notify (mq_des [i], &evnt_mq_notify [i]) != 0) { perror ("MQ_NOTIFY_MAIN"); return (-1); }
/* Создадим потоки управления */ if ((errno = pthread_create (&pt_mqs [i], NULL, start_sender, (void *) i)) != 0) { perror ("PTHREAD_CREATE"); return (i); } } /* for */
/* Ожидание завершения */ for (i = 0; i < PT_N; i++) { (void) pthread_join (pt_mqs [i], NULL); }
/* Закроем дескрипторы и удалим очереди */ for (i = 0; i < PT_N; i++) { (void) mq_close (mq_des [i]); (void) mq_unlink (mq_name [i]); }
printf ("Общая сумма: %d\n", sum);
if ((errno = pthread_mutex_destroy (&sm_mutex)) != 0) { perror ("PTHREAD_MUTEX_DESTROY"); return (errno); }
return 0; }
Листинг 4.10. Пример программы, использующей очереди сообщений.
Обратим внимание на то, что если в программе для отправки и приема сообщений используются буфера одного размера, он должен равняться значению атрибута mq_msgsize, которое задается при создании очереди. Отметим также применение режима без блокировки, что важно для приложений реального времени.
mqd_t mq_open
|
#include |
| Листинг 4.1. Описание функции mq_open(). |
| Закрыть окно |
|
#include int mq_getattr ( mqd_t mqdes, struct mq_attr *mqstat); int mq_setattr ( mqd_t mqdes, const struct mq_attr *restrict mqstat, struct mq_attr *restrict omqstat); |
| Листинг 4.2. Описание функций mq_getattr() и mq_setattr(). |
| Закрыть окно |
| #include |
| Листинг 4.3. Описание функции mq_close(). |
| Закрыть окно |
| #include |
| Листинг 4.4. Описание функции mq_unlink(). |
| Закрыть окно |
| #include |
| Листинг 4.5. Описание функции mq_send(). |
| Закрыть окно |
| #include |
| Листинг 4.6. Описание функции mq_timedsend(). |
| Закрыть окно |
|
#include |
| Листинг 4.7. Описание функции mq_receive(). |
| Закрыть окно |
|
#include |
| Листинг 4.8. Описание функции mq_timedreceive(). |
| Закрыть окно |
| #include |
| Листинг 4.9. Описание функции mq_notify(). |
| Закрыть окно |
| /* * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа принимает сообщения */ /* и суммирует содержащиеся в них целые числа. */ /* Массивы случайных целых чисел */ /* генерируют несколько потоков управления. */ /* Каждый поток использует свою очередь сообщений.*/ /* Используется режим без блокировки, */ /* с уведомлениями о появлении сообщений в очереди*/ /* * * * * * * * * * * * * * * * * * * * * * * * */ #include /* Число потоков управления, порождающих случайные числа */ #define PT_N MQ_OPEN_MAX /* Число сообщений, генерируемых каждым потоком управления */ #define MSG_N 128 /* Длина имени очереди сообщений */ #define MQ_NAME_LENGTH PATH_MAX /* Количество целых чисел в одном сообщении */ #define MSG_INT_SIZE 32 /* Максимальное число сообщений в очереди */ #define MQ_MSGS_MAX 16 /* Приоритет порождаемого сообщения */ #define prio_rnd (rand () % MQ_PRIO_MAX) /* Номер сигнала, используемого для уведомлений */ #define SIG_MQ_NOTIFY SIGRTMIN /* Массив идентификаторов очередей сообщений */ static mqd_t mq_des [PT_N]; /* Массив структур для задания уведомлений */ /* о поступлении сообщений в очереди */ static struct sigevent evnt_mq_notify [PT_N]; /* Мьютекс, используемый для синхронизации */ /* доступа к переменной sum */ static pthread_mutex_t sm_mutex = PTHREAD_MUTEX_INITIALIZER; /* Общий результат суммирования */ static int sum = 0; /* * * * * * * * * * * * * * * * * * * * * * * */ /* Функция, вызываемая при получении уведомления*/ /* о том, что очередь сообщений стала непустой. */ /* Номер очереди передается как аргумент */ /* * * * * * * * * * * * * * * * * * * * * * * */ static void msg_arrvd (union sigval pt_nm) { int msg_buf [MSG_INT_SIZE]; mqd_t mqdes; unsigned int msg_prio; int msg_sum = 0; ssize_t msg_size; int i; mqdes = mq_des [pt_nm.sival_int]; /* Примем и обработаем имеющиеся сообщения, */ /* а затем снова зарегистрируемся на получение */ /* такого же уведомления */ while ((msg_size = mq_receive (mqdes, (char *) msg_buf, MSG_INT_SIZE * sizeof (int), &msg_prio)) > 0) { for (i = 0; i < (msg_size / (signed int) sizeof (int)); i++) { msg_sum += msg_buf [i]; } msg_sum *= msg_prio; } if ((errno = pthread_mutex_lock (&sm_mutex)) != 0) { perror ("PTHREAD_MUTEX_LOCK"); } sum += msg_sum; if ((errno = pthread_mutex_unlock (&sm_mutex)) != 0) { perror ("PTHREAD_MUTEX_UNLOCK"); } if (mq_notify (mqdes, &evnt_mq_notify [pt_nm.sival_int]) != 0) { perror ("MQ_NOTIFY"); } } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, генерирующего числа и */ /* посылающего сообщения. */ /* Аргумент - номер очереди сообщений */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_sender (void *pt_nm) { int msg_buf [MSG_INT_SIZE]; int i, j; /* Сформируем и пошлем заданное число сообщений */ /* (проверяя, не переполнилась ли очередь)*/ for (j = 0; j < MSG_N; j++) { for (i = 0; i < MSG_INT_SIZE; i++) { msg_buf [i] = rand (); } if (mq_send (mq_des [(int) pt_nm], (char *) msg_buf, MSG_INT_SIZE * sizeof (int), prio_rnd) != 0) { perror ("MQ_SEND"); return (NULL); } } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Создание очереди сообщений, */ /* регистрация на получение уведомлений, */ /* создание и ожидание завершения потоков управления */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { /* Массив идентификаторов порождаемых */ /* потоков. Эти потоки будут */ pthread_t pt_mqs [PT_N]; /* генерировать сообщения*/ /* Массив для генерации и хранения */ /* имен очередей сообщений */ char mq_name [PT_N] [MQ_NAME_LENGTH]; struct mq_attr mqattrs; /* Атрибуты создаваемых очередей */ int i; for (i = 0; i < PT_N; i++) { /* Создадим очереди сообщений */ /* и зарегистрируемся на получение уведомлений */ sprintf (mq_name [i], "g%d", i); mqattrs.mq_flags = O_NONBLOCK; mqattrs.mq_maxmsg = MQ_MSGS_MAX; mqattrs.mq_msgsize = MSG_INT_SIZE * sizeof (int); mqattrs.mq_curmsgs = 0; if ((mq_des [i] = mq_open (mq_name [i], O_RDWR | O_CREAT | O_NONBLOCK, 0777, &mqattrs)) == (mqd_t) (-1)) { perror ("MQ_OPEN"); return (-1); } /* Сформируем структуру evnt_mq_notify */ evnt_mq_notify [i].sigev_notify = SIGEV_THREAD; evnt_mq_notify [i].sigev_signo = SIG_MQ_NOTIFY; evnt_mq_notify [i].sigev_value.sival_int = i; evnt_mq_notify [i].sigev_notify_function = msg_arrvd; evnt_mq_notify [i].sigev_notify_attributes = NULL; if (mq_notify (mq_des [i], &evnt_mq_notify [i]) != 0) { perror ("MQ_NOTIFY_MAIN"); return (-1); } /* Создадим потоки управления */ if ((errno = pthread_create (&pt_mqs [i], NULL, start_sender, (void *) i)) != 0) { perror ("PTHREAD_CREATE"); return (i); } } /* for */ /* Ожидание завершения */ for (i = 0; i < PT_N; i++) { (void) pthread_join (pt_mqs [i], NULL); } /* Закроем дескрипторы и удалим очереди */ for (i = 0; i < PT_N; i++) { (void) mq_close (mq_des [i]); (void) mq_unlink (mq_name [i]); } printf ("Общая сумма: %d\n", sum); if ((errno = pthread_mutex_destroy (&sm_mutex)) != 0) { perror ("PTHREAD_MUTEX_DESTROY"); return (errno); } return 0; } |
| Листинг 4.10. Пример программы, использующей очереди сообщений. |
| Закрыть окно |
|
#include sem_t *sem_open (const char *name, int oflag, ...); int sem_init (sem_t *sem, int pshared, unsigned value); |
| Листинг 4.11. Описание функций sem_open() и sem_init(). |
| Закрыть окно |
| #include int sem_close (sem_t *sem); int sem_unlink (const char *name); int sem_destroy (sem_t *sem); |
| Листинг 4.12. Описание функций закрытия и ликвидации семафоров. |
| Закрыть окно |
| #include int sem_wait (sem_t *sem); int sem_trywait (sem_t *sem); |
| Листинг 4.13. Описание функций захвата семафоров. |
| Закрыть окно |
| #include |
| Листинг 4.14. Описание функции захвата семафора с контролем времени ожидания. |
| Закрыть окно |
| #include |
| Листинг 4.15. Описание функции sem_post(). |
| Закрыть окно |
| #include |
| Листинг 4.16. Описание функции sem_getvalue(). |
| Закрыть окно |
| /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа реализует взаимодействие */ /* поставщик/потребитель (писатель/читатель). */ /* Поставщик генерирует случайные целые числа и помещает их буфер */ /* на один элемент, потребитель извлекает их оттуда и суммирует. */ /* Для синхронизации используются неименованные семафоры*/ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include /* Семафор, разрешающий записывать в буфер новые данные */ static sem_t w_sem; /* Семафор, разрешающий читать данные из буфера */ static sem_t r_sem; /* Общий результат суммирования */ static int sum = 0; /* * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, генерирующего числа */ /* * * * * * * * * * * * * * * * * * * * * * * */ void *start_writer (void *dummy) { while (sem_wait (&w_sem) == 0) { my_buf = rand (); if (sem_post (&r_sem) != 0) { perror ("SEM_POST-R"); return (NULL); } } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, читающего и суммирующего числа */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_reader (void *dummy) { while (sem_wait (&r_sem) == 0) { sum += my_buf; if (sem_post (&w_sem) != 0) { perror ("SEM_POST-W"); return (NULL); } } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * */ /* Инициализация семафоров, */ /* создание и терминирование потоков управления*/ /* * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t w_ptid;/* Идентификатор потока-писателя */ pthread_t r_ptid;/* Идентификатор потока-читателя */ /* Инициализируем семафоры. */ /* Семафор записи будет свободен,*/ /* семафор чтения - захвачен */ if (sem_init (&w_sem, 0, 1) == -1) { perror ("SEM_INIT-W"); return (-1); } if (sem_init (&r_sem, 0, 0) == -1) { perror ("SEM_INIT-R"); return (-1); } /* Создадим потоки управления - писателя и читателя */ if ((errno = pthread_create (&w_ptid, NULL, start_writer, NULL)) != 0) { perror ("PTHREAD_CREATE-W"); return (errno); } if ((errno = pthread_create (&r_ptid, NULL, start_reader, NULL)) != 0) { perror ("PTHREAD_CREATE-R"); return (errno); } /* Дадим потокам повыполняться */ sleep (10); /* Терминируем потоки */ (void) pthread_cancel (w_ptid); (void) pthread_cancel (r_ptid); /* Дождемся завершения потоков */ (void) pthread_join (w_ptid, NULL); (void) pthread_join (r_ptid, NULL); /* Ликвидируем семафоры */ if (sem_destroy (&w_sem) != 0) { perror ("SEM_DESTROY-W"); return (-1); } if (sem_destroy (&r_sem) != 0) { perror ("SEM_DESTROY-R"); return (-1); } printf ("Сумма сгенерированных чисел: %d\n", sum); return 0; } |
| Листинг 4.17. Пример программы, реализующей взаимодействие поставщик/потребитель. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Программа реализует взаимодействие */ /* поставщик/потребитель (писатель/читатель). */ /* Поставщик генерирует случайные целые числа */ /* и помещает их в кольцевой буфер, */ /* потребитель извлекает их оттуда и суммирует.*/ /* Для синхронизации используются */ /* неименованные семафоры */ /* * * * * * * * * * * * * * * * * * * * * * * */ #include /* Буфер для хранения генерируемых данных */ static int my_buf [BUFSIZ]; /* Индекс, по которому можно записать очередной элемент */ static int w_ind = 0; /* Индекс, по которому можно прочитать очередной элемент */ static int r_ind = 0; /* Семафор, разрешающий записывать в буфер новые данные */ static sem_t w_sem; /* Семафор, разрешающий читать данные из буфера */ static sem_t r_sem; /* Общий результат суммирования */ static int sum = 0; /* * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, генерирующего числа*/ /* * * * * * * * * * * * * * * * * * * * * * * */ void *start_writer (void *dummy) { while (sem_wait (&w_sem) == 0) { my_buf [w_ind] = rand (); w_ind = (w_ind + 1) % BUFSIZ; if (sem_post (&r_sem) != 0) { perror ("SEM_POST-R"); return (NULL); } } return (NULL); } /* * * * * * * * * * * * * * * */ /* Стартовая функция потока, */ /* читающего и суммирующего числа*/ /* * * * * * * * * * * * * * * */ void *start_reader (void *dummy) { while (sem_wait (&r_sem) == 0) { sum += my_buf [r_ind]; r_ind = (r_ind + 1) % BUFSIZ; if (sem_post (&w_sem) != 0) { perror ("SEM_POST-W"); return (NULL); } } return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * */ /* Инициализация семафоров, */ /* создание и терминирование потоков управления*/ /* * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t w_ptid;/* Идентификатор потока-писателя */ pthread_t r_ptid;/* Идентификатор потока-читателя */ /* Инициализируем семафоры. */ /* Семафор записи будет свободен,*/ /* разрешая заполнить весь буфер,*/ /* семафор чтения - захвачен */ if (sem_init (&w_sem, 0, BUFSIZ) == -1) { perror ("SEM_INIT-W"); return (-1); } if (sem_init (&r_sem, 0, 0) == -1) { perror ("SEM_INIT-R"); return (-1); } /* Создадим потоки управления - писателя и читателя */ if ((errno = pthread_create (&w_ptid, NULL, start_writer, NULL)) != 0) { perror ("PTHREAD_CREATE-W"); return (errno); } if ((errno = pthread_create (&r_ptid, NULL, start_reader, NULL)) != 0) { perror ("PTHREAD_CREATE-R"); return (errno); } /* Дадим потокам повыполняться */ sleep (10); /* Терминируем потоки */ (void) pthread_cancel (w_ptid); (void) pthread_cancel (r_ptid); /* Дождемся завершения потоков */ (void) pthread_join (w_ptid, NULL); (void) pthread_join (r_ptid, NULL); /* Ликвидируем семафоры */ if (sem_destroy (&w_sem) != 0) { perror ("SEM_DESTROY-W"); return (-1); } if (sem_destroy (&r_sem) != 0) { perror ("SEM_DESTROY-R"); return (-1); } printf ("Сумма сгенерированных чисел: %d\n", sum); return 0; } |
| Листинг 4.18. Пример программы, реализующей взаимодействие поставщик/потребитель с помощью целочисленных семафоров. |
| Закрыть окно |
|
/* Обедающие философы. Многопотоковая реализация с помощью семафоров. Запуск: mudrecSem [-a | -p | -I] [-t число_секунд] имя_философа ... Опции: -t число_секунд - сколько секунд моделируется Стратегии захвата вилок: -a - сначала захватывается вилка с меньшим номером; -p - сначала захватывается нечетная вилка; -I - некорректная (но эффективная) интеллигентная стратегия: во время ожидания уже захваченная вилка кладется. Пример запуска: mudrecSem -p -t 300 A B C D E F G H I J K L M N\ O P Q R S T U V W X Y Z */ static char rcsid[] __attribute__((unused)) = \ "$Id: mudrecSem.c,v 1.2 2004/03/18 10:28:38 sambor Exp $"; #include #define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)>(b)?(b):(a)) struct mudrec { char *name; int left_fork, right_fork; int eat_time, wait_time, think_time, max_wait_time; int count; pthread_t thread; int private_pFdIn; } *kafedra; /* Глобальные счетчики и логические переменные */ int Stop = 0; /* Признак конца обеда */ /* Различные дескрипторы */ int protokol [2] = {-1, -1}; #define pFdIn (protokol [1]) #define pFdOut (protokol [0]) /* Массив семафоров для синхронизации доступа к вилкам */ sem_t *semFork; /* Разные алгоритмы захвата вилок */ static void get_forks_simple (struct mudrec *this); static void get_forks_odd (struct mudrec *this); static void get_forks_maybe_infinit_time (struct mudrec *this); /* Используемый метод захвата вилок */ void (*get_forks) (struct mudrec *this) = get_forks_simple; /* Возвращение вилок */ static void put_forks (struct mudrec *this); /* * Потоки-философы */ void *filosof (void *arg) { struct mudrec *this = arg; char buffer [LINE_MAX]; int bytes; int private_pFdIn = this->private_pFdIn; while (!Stop) { /* Пора подкрепиться */ { int wait_time, tm = time (NULL); sprintf (buffer, "%s: хочет есть\n", this->name); bytes = write (private_pFdIn, buffer, strlen (buffer)); (*get_forks) (this); wait_time = time (NULL) - tm; this->wait_time += wait_time; this->max_wait_time = max (wait_time, this->max_wait_time); sprintf (buffer,"%s: ждал вилок %d сек\n", this->name, wait_time); bytes = write (private_pFdIn, buffer, strlen (buffer)); } /* Может, обед уже закончился? */ if (Stop) { put_forks (this); break; } /* Ест */ { int eat_time = rand () % 20 + 1; sleep (eat_time); this->eat_time += eat_time; this->count++; sprintf (buffer,"%s: ел %d сек\n", this->name, eat_time); bytes = write (private_pFdIn, buffer, strlen (buffer)); } /* Отдает вилки */ put_forks (this); if (Stop) break; /* Размышляет */ { int think_time = rand () % 10 + 1; sleep (think_time); this->think_time += think_time; } } /* while (!Stop) */ sprintf (buffer,"%s: уходит\n", this->name); bytes = write (private_pFdIn, buffer, strlen (buffer)); close (private_pFdIn); return (NULL); } /* Поток-философ */ /* Кладет вилки одну за другой */ static void put_forks (struct mudrec *this) { sem_post (&semFork [this->left_fork - 1]); sem_post (&semFork [this->right_fork - 1]); } /* Берет вилки по очереди в порядке номеров */ static void get_forks_simple (struct mudrec *this) { int first = min (this->left_fork, this->right_fork); int last = max (this->left_fork, this->right_fork); sem_wait (&semFork [first - 1]); sem_wait (&semFork [last - 1]); } /* Берем сначала нечетную вилку */ /* (если обе нечетные - то с большим номером) */ static void get_forks_odd (struct mudrec *this) { int left = this->left_fork, right = this->right_fork; int first; int last; if ((left & 1) > (right & 1)) { first = left; last = right; } else if ((left & 1) < (right & 1)) { first = right; last = left; } else { first = max (left, right); last = min (left, right); } sem_wait (&semFork [first - 1]); sem_wait (&semFork [last - 1]); } /* Берем вилки по очереди, в произвольном порядке. * Но если вторая вилка не берется сразу, то кладем первую. * То есть философ не расходует вилочное время впустую. */ static void get_forks_maybe_infinit_time (struct mudrec *this) { int left = this->left_fork, right = this->right_fork; for (;;) { sem_wait (&semFork [left - 1]); if (0 == sem_trywait (&semFork [right - 1])) return; sem_post (&semFork [left - 1]); sem_wait (&semFork [right - 1]); if (0 == sem_trywait (&semFork [left - 1])) return; sem_post (&semFork [right - 1]); } } /* Мелкие служебные функции */ static void stop (int dummy) { Stop = 1; } static void usage (char name []) { fprintf (stderr, "Использование: %s [-a | -p | -I] [-t число_секунд] " "имя_философа ...\n", name); exit (1); } /* Точка входа демонстрационной программы */ int main (int argc, char *argv []) { char buffer [LINE_MAX], *p; int i, n, c; int open_room_time = 300; int nMudr; struct sigaction sact; while ((c = getopt (argc, argv, "apIt:")) != -1) { switch (c) { case 'a': get_forks = get_forks_simple; break; case 'p': get_forks = get_forks_odd; break; case 'I': get_forks = get_forks_maybe_infinit_time; break; case 't': open_room_time = strtol (optarg, &p, 0); if (optarg [0] == 0 || *p != 0) usage (argv [0]); break; default : usage (argv [0]); } } nMudr = argc - optind; if (nMudr < 2) usage (argv [0]); /* Меньше двух */ /* философов неинтересно ... */ /* Создание канала для протокола обработки событий */ pipe (protokol); kafedra = calloc (sizeof (struct mudrec), nMudr); /* Зачисление на кафедру */ for (i = 0; i < nMudr; i++, optind++) { kafedra [i].name = argv [optind]; /* Выдадим телефон */ kafedra [i].private_pFdIn = fcntl (pFdIn, F_DUPFD, 0); /* Укажем новичку, какими вилками пользоваться */ kafedra [i].left_fork = i + 1; kafedra [i].right_fork = i + 2; } kafedra [nMudr - 1].right_fork = 1; /* Последний*/ /* пользуется вилкой первого */ /* Зададим реакцию на сигналы и установим будильник */ /* на конец обеда */ sact.sa_handler = stop; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGINT, &sact, ( struct sigaction *) NULL); (void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL); alarm (open_room_time); /* Создадим семафоры для охраны вилок */ semFork = calloc (sizeof (sem_t), nMudr); for (i = 0; i < nMudr; i++) { sem_init (&semFork [i], 0, 1 /* На каждое место */ /* по одной вилке */); } /* Философы входят в столовую */ for (i = 0; i < nMudr; i++) pthread_create (&kafedra [i].thread, NULL, &filosof, (void *) &kafedra [i]); /* Выдача сообщений на стандартный вывод и выход */ /* после окончания всех задач */ close (pFdIn); while (1) { n = read (pFdOut, buffer, LINE_MAX); if (n == 0 || (n < 0 && errno != EINTR)) break; for (i = 0; i < n; i++) putchar (buffer [i]); } close (pFdOut); /* Уничтожение семафоров */ for (i = 0; i < nMudr; i++) { sem_destroy (&semFork [i]); } /* Выдача сводной информации */ { int full_eating_time = 0; int full_waiting_time = 0; int full_thinking_time = 0; for (i = 1; i <= nMudr; i++) { struct mudrec *this = &kafedra [i - 1]; full_eating_time += this->eat_time; full_waiting_time += this->wait_time; full_thinking_time += this->think_time; if (this->count > 0) { float count = this->count; float think_time = this->think_time / count; float eat_time = this->eat_time / count; float wait_time = this->wait_time / count; printf ("%s: ел %d раз в среднем: думал=%.1f " "ел=%.1f ждал=%.1f (максимум %d)\n", this->name, this->count, think_time, eat_time, wait_time, this->max_wait_time); } else printf ("%s: не поел\n", this->name); } /* for */ { float total_time = (full_eating_time + full_waiting_time + full_thinking_time) / (float) nMudr; printf("Среднее число одновременно едящих = %.3f\n", full_eating_time / total_time); printf("Среднее число одновременно ждущих = %.3f\n", full_waiting_time / total_time); } } /* Выдача сводной информации */ free (semFork); free (kafedra); /* Сообщим об окончании работы. */ printf ("Конец обеда\n"); return 0; } |
| Листинг 4.19. Многопотоковый вариант решения задачи об обедающих философах с использованием семафоров реального времени. |
| Закрыть окно |
|
#include int shm_open (const char *name, int oflag, mode_t mode); int shm_unlink (const char *name); |
| Листинг 4.20. Описание функций shm_open() и shm_unlink(). |
| Закрыть окно |
|
#ifndef g_SHM #define g_SHM /* Имя объекта в разделяемой памяти */ #define O_SHM_NAME "/g_o.shm" /* Используемый номер сигнала реального времени */ #define SIG_SHM SIGRTMIN /* Используемые значения сигнала реального времени */ #define SIGVAL_LINE 0 #define SIGVAL_EOF EOF #endif |
| Листинг 4.21. Заголовочный файл "g_shm.h" программы, копирующей строки со стандартного ввода на стандартный вывод. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа, состоящая из двух процессов, копирует */ /* строки со стандартного ввода на стандартный вывод,*/ /* "прокачивая" их через разделяемый сегмент памяти. */ /* Для синхронизации доступа к разделяемому сегменту */ /* используются сигналы реального времени */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include #include "g_shm.h" /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Создание разделяемого сегмента памяти, */ /* чтение со стандартного ввода и запись строк в сегмент*/ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int fd_shm; /* Дескриптор объекта в разделяемой памяти*/ FILE *fp; /* Поток для записи в объект */ char line [LINE_MAX]; /* Буфер для копируемых строк */ struct sigaction sact; /* Структура для обработки сигналов */ union sigval sg_val;/* Значение сигнала */ int sg_no; /* Номер принятого сигнала */ pid_t cpid;/* Идентификатор порожденного процесса */ /* Создадим разделяемый сегмент памяти */ if ((fd_shm = shm_open (O_SHM_NAME, O_RDWR | O_CREAT, 0777)) < 0) { perror ("SHM_CREAT"); return (1); } /* Сформируем поток данных по файловому дескриптору */ /* объекта в разделяемой памяти */ assert ((fp = fdopen (fd_shm, "w")) != NULL); /* Отменим буферизацию вывода */ setbuf (fp, NULL); /* Сформируем маску сигналов (блокируем SIG_SHM) */ (void) sigemptyset (&sact.sa_mask); (void) sigaddset (&sact.sa_mask, SIG_SHM); (void) sigprocmask (SIG_BLOCK, &sact.sa_mask, (sigset_t *) NULL); /* Установим для сигнала SIG_SHM флаг SA_SIGINFO */ sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = (void (*) (int, siginfo_t *, void *)) SIG_DFL; (void) sigaction (SIG_SHM, &sact, (struct sigaction *) NULL); /* Подготовительная работа закончена */ switch (cpid = fork ()) { case -1: perror ("FORK"); return (2); case 0: /* Чтение из объекта и выдачу на стандартный */ /* вывод реализуем в порожденном процессе */ if (execl ("./g_r_shm", "g_r_shm", (char *) NULL) < 0) { perror ("EXECL"); return (3); } } /* Чтение со стандартного ввода и запись в объект */ /* возложим на родительский процесс.*/ /* В начальный момент объект в разделяемой памяти*/ /* доступен для записи */ assert (fseek (fp, 0, SEEK_SET) == 0); fputs ("Вводите строки\n", fp); /* Сообщим порожденному процессу,*/ /* что объект в разделяемой памяти заполнен*/ sg_val.sival_int = SIGVAL_LINE; assert (sigqueue (cpid, SIG_SHM, sg_val) == 0); while (fgets (line, sizeof (line), stdin) != NULL) { assert (fseek (fp, 0, SEEK_SET) == 0); /* Дождемся, когда в объект можно будет писать */ if ((sigwait (&sact.sa_mask, &sg_no) != 0) || (sg_no != SIG_SHM)) { return (4); } assert (fputs ("Вы ввели: ", fp) != EOF); assert (fputs (line, fp) != EOF); assert (sigqueue (cpid, SIG_SHM, sg_val) == 0); } /* Сообщим о конце файла */ sg_val.sival_int = SIGVAL_EOF; assert (sigqueue (cpid, SIG_SHM, sg_val) == 0); fclose (fp); (void) wait (NULL); if (shm_unlink (O_SHM_NAME) != 0) { perror ("SHM_UNLINK"); return (7); } return (0); } |
| Листинг 4.22. Исходный текст начального процесса двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Процесс читает строки из объекта в разделяемой памяти */ /* и копирует их на стандартный вывод */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include #include "g_shm.h" /* * * * * * * * * * * * * * * * * * * * */ /* Открытие разделяемого сегмента памяти,*/ /* чтение из сегмента и выдача строк */ /* на стандартный вывод */ /* * * * * * * * * * * * * * * * * * * * */ int main (void) { int fd_shm; /* Дескриптор объекта */ /* в разделяемой памяти */ FILE *fp; /* Поток для чтения из объекта */ char line [LINE_MAX];/* Буфер для копируемых строк */ sigset_t smask; /* Маска ожидаемых сигналов */ siginfo_t sinfo; /* Структура для получения */ /* данных о сигнале */ pid_t ppid; /* Идентификатор родительского */ /* процесса */ /* Откроем разделяемый сегмент памяти */ if ((fd_shm = shm_open (O_SHM_NAME, O_RDONLY, 0777)) < 0) { perror ("SHM_OPEN"); return (1); } /* Сформируем поток по файловому дескриптору объекта */ /* в разделяемой памяти */ assert ((fp = fdopen (fd_shm, "r")) != NULL); /* Отменим буферизацию ввода */ setbuf (fp, NULL); /* Запомним идентификатор родительского процесса */ ppid = getppid (); /* Сформируем маску ожидаемых сигналов (SIG_SHM) */ (void) sigemptyset (&smask); (void) sigaddset (&smask, SIG_SHM); /* Подготовительная работа закончена */ while ((fseek (fp, 0, SEEK_SET) == 0) && /* Дождемся, когда из объекта можно будет читать */ (sigwaitinfo (&smask, &sinfo) == SIG_SHM) && /* И прочитаем строку, а не конец файла */ (sinfo.si_value.sival_int == SIGVAL_LINE)) { (void) fgets (line, sizeof (line), fp); /* Сообщим родительскому процессу, */ /* что данные из объекта извлечены */ assert (kill (ppid, SIG_SHM) == 0); /* Выдадим, наконец, строку на стандартный вывод */ assert (fputs (line, stdout) != EOF); } fclose (fp); return 0; } |
| Листинг 4.23. Исходный текст порождаемого процесса (файл g_r_shm.c) двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. |
| Закрыть окно |
Разделяемые сегменты памяти
Разделяемые сегменты памяти, называемые далее объектами в разделяемой памяти, - самый эффективный способ передачи данных между процессами. Одной операцией записи можно передать данные сразу многим процессам, разделяющим тот же объект.
Объекты в разделяемой памяти полезны как для систем, поддерживающих виртуальную память и раздельные адресные пространства для процессов, так и для встроенных систем с минимальной аппаратной поддержкой.
Минимальный мобильный программный интерфейс к объектам в разделяемой памяти включает функции открытия (возможно, с созданием) подобного объекта и получения его дескриптора, а также удаления ранее созданного объекта.
Стандартный программный интерфейс состоит из двух функций: shm_open() и shm_unlink() (см. листинг 4.19).
#include
int shm_open (const char *name, int oflag, mode_t mode);
int shm_unlink (const char *name);
Листинг 4.20. Описание функций shm_open() и shm_unlink(). (html, txt)
При открытии с помощью функции shm_open() возвращается файловый дескриптор. Имя (аргумент name) трактуется стандартным для рассматриваемых средств межпроцессного взаимодействия образом. Посредством аргумента oflag могут указываться флаги O_RDONLY, O_RDWR, O_CREAT, O_EXCL и/или O_TRUNC. Если объект создается, то режим доступа к нему формируется в соответствии со значением аргумента mode и маской создания файлов процесса.
После создания объект в разделяемой памяти существует, пока не будет удален функцией shm_unlink(). Он сохраняет свое состояние после закрытия всех ссылающихся на него дескрипторов, однако эффект от перезагрузки системы стандарт POSIX-2001 не специфицирует.
Представляется естественным, что способ доступа к объектам определяется типом дескриптора, возвращаемого при их открытии. Если это адрес, то доступ сводится к операциям чтения/записи из/в память. Если это файловый дескриптор, то для доступа должны использоваться функции файлового ввода/вывода - read(), write() и т.п. Подобное естественное применение объектов в разделяемой памяти иллюстрируется двухпроцессной программой, копирующей строки со стандартного ввода на стандартный вывод (см.
листинги 4.21, 4.22, 4.23). Предполагается, что заголовочный файл называется "g_shm.h", а файл с образом процесса, запускаемого посредством execl(), - "g_r_shm".
#ifndef g_SHM #define g_SHM
/* Имя объекта в разделяемой памяти */ #define O_SHM_NAME "/g_o.shm"
/* Используемый номер сигнала реального времени */ #define SIG_SHM SIGRTMIN
/* Используемые значения сигнала реального времени */ #define SIGVAL_LINE 0 #define SIGVAL_EOF EOF
#endif
Листинг 4.21. Заголовочный файл "g_shm.h" программы, копирующей строки со стандартного ввода на стандартный вывод. (html, txt)
Листинг 4.22. Исходный текст начального процесса двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. (html, txt)
Листинг 4.23. Исходный текст порождаемого процесса (файл g_r_shm.c) двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. (html, txt)
Объект в разделяемой памяти используется в приведенной программе как буфер на один элемент (одну строку). Чтение и запись производятся с начала объекта, для чего применяется файловое позиционирование. Разумеется, в такой ситуации порожденный процесс не может обычным образом обнаружить конец передаваемого ему файла, поэтому приходится применять дополнительные средства. В данном случае это сигналы реального времени, которые несут двойную нагрузку - обеспечивают синхронизацию доступа к объекту в разделяемой памяти и передают от родительского процесса порожденному информацию о конце файла.
К сожалению, стандарт POSIX-2001 не следует приведенным выше естественным предположениям, касающимся связи между типом дескриптора и способом доступа к объекту, так что приведенная программа, строго говоря, не соответствует стандарту. Дело в том, что файловый дескриптор, возвращаемый функцией shm_open(), является дескриптором "второго сорта" (хотя и первой свежести): результат применения к нему функций fdopen(), read(), write() и т.п. не специфицирован.
Далее мы увидим, для чего этот дескриптор можно употребить и как организовать межпроцессное взаимодействие через разделяемые сегменты памяти реального времени в полном соответствии с положениями стандарта POSIX-2001.Здесь же отметим, что в большинстве реализаций для представления разделяемых сегментов применяются файлы, отображенные в память, так что их дескрипторы вполне пригодны для выполнения файловых операций, а мобильность приведенной программы с практической точки зрения можно считать удовлетворительной.
Семафоры реального времени
Семафор реального времени - это эффективный механизм синхронизации процессов, представляющий собой общесистемный разделяемый ресурс, имеющий неотрицательное целочисленное значение.
Основными операциями над семафором являются захват и освобождение. Если делается попытка захвата семафора, когда его значение равно нулю, выполнение вызывающего потока управления приостанавливается и он добавляется к множеству потоков, ждущих на семафоре; в противном случае значение уменьшается.
Если при освобождении семафора множество ждущих потоков было непусто, один из них удаляется из этого множества и его выполнение возобновляется; в противном случае значение семафора просто увеличивается.
Семафоры бывают именованными и безымянными (неименованными). Первые именуются цепочками символов и создаются функцией sem_open() с флагом O_CREAT, вторые создаются функцией sem_init(). При прочих операциях семафор идентифицируется открытым дескриптором (который может быть унаследован у родительского процесса, вызвавшего fork()). Дескриптор реализуется как указатель на объект типа sem_t.
Перед выполнением операций семафор необходимо инициализировать, задав неотрицательное значение. Отрицательные значения (точнее, их абсолютная величина) могут использоваться реализацией для указания числа ждущих потоков управления.
Семафор сохраняет свое состояние после закрытия последней ссылки на него, то есть если позднее он будет вновь открыт, его значение окажется тем же, что и перед закрытием.
Детальное описание функций, обслуживающих семафоры, мы начнем, разумеется, с sem_open() и sem_init() (см. листинг 4.11), которые обеспечивают открытие, создание и инициализацию.
#include
sem_t *sem_open (const char *name, int oflag, ...);
int sem_init (sem_t *sem, int pshared, unsigned value);
Листинг 4.11. Описание функций sem_open() и sem_init(). (html, txt)
Имена семафоров (аргумент name функции sem_open()) устроены и трактуются так же, как и описанные выше имена очередей сообщений. Флагов (в аргументе oflag) может быть установлено два: O_CREAT и/или O_EXCL.
Если установлен флаг O_CREAT, то при вызове функции sem_open() необходимо задать два дополнительных аргумента: режим доступа (тип mode_t) и значение семафора (тип unsigned int).
В случае ошибки функция sem_open() возвращает значение SEM_FAILED, отличное от любого допустимого указателя на объект типа sem_t, а sem_init() "по старинке" возвращает -1.
Отметим, что нормальный результат для функции sem_init() в POSIX-2001 не стандартизован; вероятно, в будущих версиях им станет нуль. Инициализированный объект типа sem_t помещается по указателю sem. Аргумент value задает начальное значение создаваемого неименованного семафора. Если аргумент pshared отличен от нуля, семафор разделяется между процессами; в противном случае разделение возможно только между потоками управления вызывающего процесса.
Именованный семафор можно закрыть, обратившись к функции sem_close(), и удалить с помощью функции sem_unlink(); для ликвидации неименованных семафоров служит функция sem_destroy() (см. листинг 4.12). Нормальный результат этих функций равен нулю, в случае ошибки возвращается -1.
#include
int sem_close (sem_t *sem);
int sem_unlink (const char *name);
int sem_destroy (sem_t *sem);
Листинг 4.12. Описание функций закрытия и ликвидации семафоров. (html, txt)
Отметим, что эффект от вызова sem_close() для неименованного семафора, равно как и последствия вызова sem_destroy() для семафора именованного, не определены. Также не определен результат применения функции sem_destroy() к семафору, на котором имеются ждущие потоки управления.
Для захвата семафоров служат функции sem_wait(), sem_trywait() и sem_timedwait() (см. листинги 4.13 и 4.14).
#include
int sem_wait (sem_t *sem);
int sem_trywait (sem_t *sem);
Листинг 4.13. Описание функций захвата семафоров. (html, txt) #include
Листинг 4.14. Описание функции захвата семафора с контролем времени ожидания. (html, txt)
Если значение семафора было положительным, все три перечисленные функции без каких-либо задержек завершаются успехом, уменьшая это значение до нуля и возвращая нулевой результат. В противном случае вызов sem_trywait() завершается неудачей, вызов sem_wait() блокируется до освобождения семафора, вызов sem_timedwait() также блокируется, но с контролем времени ожидания.
Освобождение семафора осуществляется функцией sem_post() (см. листинг 4.15). Для возобновления выполнения (если таковое имеет место) выбирается наиболее приоритетный поток управления, а среди потоков с равными приоритетами - тот, что ждал дольше других.
#include
Листинг 4.15. Описание функции sem_post(). (html, txt)
Функция sem_getvalue() (см. листинг 4.16) позволяет опросить значение семафора, не меняя его состояния.
#include
Листинг 4.16. Описание функции sem_getvalue(). (html, txt)
Значение семафора записывается по указателю sval. Если семафор был захвачен, это значение окажется нулевым или отрицательным.
Если сопоставить семафоры реального времени с теми, что были описаны в курсе [1], то можно сделать те же выводы, что и для очередей сообщений. Семафоры реального времени, согласно стандарту POSIX-2001, по сути являются бинарными. Они устроены проще и, следовательно, могут быть реализованы эффективнее. Главное - отсутствуют тяжеловесные, со сложной семантикой, трудные для реализации групповые операции.
Использование семафоров реального времени проиллюстрируем программой, реализующей взаимодействие поставщик/потребитель (см. листинг 4.17).
Листинг 4.17. Пример программы, реализующей взаимодействие поставщик/потребитель. (html, txt)
Применение неименованных семафоров в данном случае представляется вполне естественным. Поставщик захватывает семафор записи, заполняет буфер и освобождает семафор, разрешающий чтение. Потребитель действует симметричным образом.
Отметим, что семафоры ликвидируются после терминирования использующих их потоков управления, что, согласно стандарту POSIX-2001, является безопасным.
Поясним и обсудим сделанное выше замечание о том, что, согласно стандарту POSIX-2001, семафоры реального времени по сути являются бинарными. Такой вывод можно сделать по двум фразам из описания функции sem_trywait(). Во-первых, функция sem_trywait() захватывает семафор только в том случае, если он еще не захвачен, то есть если значение семафора положительно. Во-вторых, в случае успешного завершения семафор захватывается и должен оставаться в этом состоянии, пока не будет успешно выполнена функция sem_post().
Здесь ничего не говорится об уменьшении положительного значения без захвата семафора, зато намеки на это имеются в части, где приводятся детальное разъяснение положений стандарта и обоснование принятых решений. Смущает и то, что в описании функции sem_post() не запрещается освобождать свободный семафор, что, естественно, выливается в увеличение его значения. Далее, в разных местах семафоры называются то бинарными, то целочисленными. Наконец, автору не известно ни одной реализации, где семафоры реального времени не были бы целочисленными. В общем, сложное это дело - толкование внутренне противоречивых стандартов, вступающих в конфликт с реальной жизнью...
Если считать, что рассматриваемые семафоры являются целочисленными, приведенную выше программу можно усовершенствовать, сделав буфер кольцевым, с размером, большим единицы (см. листинг 4.18).
Листинг 4.18. Пример программы, реализующей взаимодействие поставщик/потребитель с помощью целочисленных семафоров. (html, txt)
Значение семафора w_sem равно числу элементов буфера, доступных для записи, r_sem - для чтения. Многопотоковым инвариантом программы является сумма этих величин, равная размеру буфера. В начальный момент буфер целиком доступен для записи. После этого вызовы sem_wait() и sem_post() уменьшают значение "своего" и увеличивают значение "чужого" семафора. Поток управления приостанавливается в sem_wait(), когда "свое" значение уменьшается до нуля, у нужно ждать, пока другой поток вызовом sem_post() не увеличит его, сделав положительным.
Разумеется, обсуждение темы семафоров было бы неполным без обеда философов. Мы приведем программу, написанную С.В. Самборским (см. листинг 4.19). В ней семафоры используются как бинарные, поэтому ее стандартность и мобильность не вызывают сомнений.
Листинг 4.19. Многопотоковый вариант решения задачи об обедающих философах с использованием семафоров реального времени. (html, txt)
/* Дадим потокам повыполняться */ sleep (10);
/* Терминируем потоки */ (void) pthread_cancel (w_ptid); (void) pthread_cancel (r_ptid);
/* Дождемся завершения потоков */ (void) pthread_join (w_ptid, NULL); (void) pthread_join (r_ptid, NULL); /* Ликвидируем семафоры */ if (sem_destroy (&w_sem) != 0) { perror ("SEM_DESTROY-W"); return (-1); } if (sem_destroy (&r_sem) != 0) { perror ("SEM_DESTROY-R"); return (-1); }
printf ("Сумма сгенерированных чисел: %d\n", sum);
return 0; }
Листинг 4.17. Пример программы, реализующей взаимодействие поставщик/потребитель.
Применение неименованных семафоров в данном случае представляется вполне естественным. Поставщик захватывает семафор записи, заполняет буфер и освобождает семафор, разрешающий чтение. Потребитель действует симметричным образом.
Отметим, что семафоры ликвидируются после терминирования использующих их потоков управления, что, согласно стандарту POSIX-2001, является безопасным.
Поясним и обсудим сделанное выше замечание о том, что, согласно стандарту POSIX-2001, семафоры реального времени по сути являются бинарными. Такой вывод можно сделать по двум фразам из описания функции sem_trywait(). Во-первых, функция sem_trywait() захватывает семафор только в том случае, если он еще не захвачен, то есть если значение семафора положительно. Во-вторых, в случае успешного завершения семафор захватывается и должен оставаться в этом состоянии, пока не будет успешно выполнена функция sem_post().
Здесь ничего не говорится об уменьшении положительного значения без захвата семафора, зато намеки на это имеются в части, где приводятся детальное разъяснение положений стандарта и обоснование принятых решений. Смущает и то, что в описании функции sem_post() не запрещается освобождать свободный семафор, что, естественно, выливается в увеличение его значения. Далее, в разных местах семафоры называются то бинарными, то целочисленными. Наконец, автору не известно ни одной реализации, где семафоры реального времени не были бы целочисленными.
В общем, сложное это дело - толкование внутренне противоречивых стандартов, вступающих в конфликт с реальной жизнью...
Если считать, что рассматриваемые семафоры являются целочисленными, приведенную выше программу можно усовершенствовать, сделав буфер кольцевым, с размером, большим единицы (см. листинг 4.18).
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Программа реализует взаимодействие */ /* поставщик/потребитель (писатель/читатель). */ /* Поставщик генерирует случайные целые числа */ /* и помещает их в кольцевой буфер, */ /* потребитель извлекает их оттуда и суммирует.*/ /* Для синхронизации используются */ /* неименованные семафоры */ /* * * * * * * * * * * * * * * * * * * * * * * */
#include
/* Буфер для хранения генерируемых данных */ static int my_buf [BUFSIZ];
/* Индекс, по которому можно записать очередной элемент */ static int w_ind = 0;
/* Индекс, по которому можно прочитать очередной элемент */ static int r_ind = 0;
/* Семафор, разрешающий записывать в буфер новые данные */ static sem_t w_sem;
/* Семафор, разрешающий читать данные из буфера */ static sem_t r_sem;
/* Общий результат суммирования */ static int sum = 0;
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока, генерирующего числа*/ /* * * * * * * * * * * * * * * * * * * * * * * */ void *start_writer (void *dummy) { while (sem_wait (&w_sem) == 0) { my_buf [w_ind] = rand (); w_ind = (w_ind + 1) % BUFSIZ; if (sem_post (&r_sem) != 0) { perror ("SEM_POST-R"); return (NULL); } }
return (NULL); }
/* * * * * * * * * * * * * * * */ /* Стартовая функция потока, */ /* читающего и суммирующего числа*/ /* * * * * * * * * * * * * * * */ void *start_reader (void *dummy) { while (sem_wait (&r_sem) == 0) { sum += my_buf [r_ind]; r_ind = (r_ind + 1) % BUFSIZ; if (sem_post (&w_sem) != 0) { perror ("SEM_POST-W"); return (NULL); } }
return (NULL); }
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Инициализация семафоров, */ /* создание и терминирование потоков управления*/ /* * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_t w_ptid;/* Идентификатор потока-писателя */ pthread_t r_ptid;/* Идентификатор потока-читателя */
/* Инициализируем семафоры. */ /* Семафор записи будет свободен,*/ /* разрешая заполнить весь буфер,*/ /* семафор чтения - захвачен */ if (sem_init (&w_sem, 0, BUFSIZ) == -1) { perror ("SEM_INIT-W"); return (-1); } if (sem_init (&r_sem, 0, 0) == -1) { perror ("SEM_INIT-R"); return (-1); }
/* Создадим потоки управления - писателя и читателя */ if ((errno = pthread_create (&w_ptid, NULL, start_writer, NULL)) != 0) { perror ("PTHREAD_CREATE-W"); return (errno); } if ((errno = pthread_create (&r_ptid, NULL, start_reader, NULL)) != 0) { perror ("PTHREAD_CREATE-R"); return (errno); }
/* Дадим потокам повыполняться */ sleep (10);
/* Терминируем потоки */ (void) pthread_cancel (w_ptid); (void) pthread_cancel (r_ptid);
/* Дождемся завершения потоков */ (void) pthread_join (w_ptid, NULL); (void) pthread_join (r_ptid, NULL);
/* Ликвидируем семафоры */ if (sem_destroy (&w_sem) != 0) { perror ("SEM_DESTROY-W"); return (-1); }
if (sem_destroy (&r_sem) != 0) { perror ("SEM_DESTROY-R"); return (-1); }
printf ("Сумма сгенерированных чисел: %d\n", sum);
return 0; }
Листинг 4.18. Пример программы, реализующей взаимодействие поставщик/потребитель с помощью целочисленных семафоров.
Значение семафора w_sem равно числу элементов буфера, доступных для записи, r_sem - для чтения. Многопотоковым инвариантом программы является сумма этих величин, равная размеру буфера. В начальный момент буфер целиком доступен для записи. После этого вызовы sem_wait() и sem_post() уменьшают значение "своего" и увеличивают значение "чужого" семафора. Поток управления приостанавливается в sem_wait(), когда "свое" значение уменьшается до нуля, у нужно ждать, пока другой поток вызовом sem_post() не увеличит его, сделав положительным.
Разумеется, обсуждение темы семафоров было бы неполным без обеда философов. Мы приведем программу, написанную С.В. Самборским (см. листинг 4.19). В ней семафоры используются как бинарные, поэтому ее стандартность и мобильность не вызывают сомнений.
/* Обедающие философы. Многопотоковая реализация с помощью семафоров. Запуск: mudrecSem [-a | -p | -I] [-t число_секунд] имя_философа ...
Опции: -t число_секунд - сколько секунд моделируется
Стратегии захвата вилок: -a - сначала захватывается вилка с меньшим номером; -p - сначала захватывается нечетная вилка; -I - некорректная (но эффективная) интеллигентная стратегия: во время ожидания уже захваченная вилка кладется.
Пример запуска: mudrecSem -p -t 300 A B C D E F G H I J K L M N\ O P Q R S T U V W X Y Z */
static char rcsid[] __attribute__((unused)) = \ "$Id: mudrecSem.c,v 1.2 2004/03/18 10:28:38 sambor Exp $";
#include
#define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)>(b)?(b):(a))
struct mudrec { char *name; int left_fork, right_fork; int eat_time, wait_time, think_time, max_wait_time; int count; pthread_t thread; int private_pFdIn; } *kafedra;
/* Глобальные счетчики и логические переменные */ int Stop = 0; /* Признак конца обеда */
/* Различные дескрипторы */ int protokol [2] = {-1, -1}; #define pFdIn (protokol [1]) #define pFdOut (protokol [0])
/* Массив семафоров для синхронизации доступа к вилкам */ sem_t *semFork;
/* Разные алгоритмы захвата вилок */ static void get_forks_simple (struct mudrec *this); static void get_forks_odd (struct mudrec *this); static void get_forks_maybe_infinit_time (struct mudrec *this);
/* Используемый метод захвата вилок */ void (*get_forks) (struct mudrec *this) = get_forks_simple;
/* Возвращение вилок */ static void put_forks (struct mudrec *this);
/* * Потоки-философы */ void *filosof (void *arg) { struct mudrec *this = arg; char buffer [LINE_MAX]; int bytes; int private_pFdIn = this->private_pFdIn;
while (!Stop) { /* Пора подкрепиться */ { int wait_time, tm = time (NULL);
sprintf (buffer, "%s: хочет есть\n", this->name); bytes = write (private_pFdIn, buffer, strlen (buffer));
(*get_forks) (this);
wait_time = time (NULL) - tm; this->wait_time += wait_time; this->max_wait_time = max (wait_time, this->max_wait_time);
sprintf (buffer,"%s: ждал вилок %d сек\n", this->name, wait_time); bytes = write (private_pFdIn, buffer, strlen (buffer)); }
/* Может, обед уже закончился? */ if (Stop) { put_forks (this); break; }
/* Ест */ { int eat_time = rand () % 20 + 1;
sleep (eat_time);
this->eat_time += eat_time; this->count++; sprintf (buffer,"%s: ел %d сек\n", this->name, eat_time); bytes = write (private_pFdIn, buffer, strlen (buffer)); }
/* Отдает вилки */ put_forks (this);
if (Stop) break;
/* Размышляет */ { int think_time = rand () % 10 + 1;
sleep (think_time); this->think_time += think_time; } } /* while (!Stop) */
sprintf (buffer,"%s: уходит\n", this->name); bytes = write (private_pFdIn, buffer, strlen (buffer));
close (private_pFdIn);
return (NULL); } /* Поток-философ */
/* Кладет вилки одну за другой */ static void put_forks (struct mudrec *this) { sem_post (&semFork [this->left_fork - 1]); sem_post (&semFork [this->right_fork - 1]); }
/* Берет вилки по очереди в порядке номеров */ static void get_forks_simple (struct mudrec *this) { int first = min (this->left_fork, this->right_fork); int last = max (this->left_fork, this->right_fork);
sem_wait (&semFork [first - 1]); sem_wait (&semFork [last - 1]); }
/* Берем сначала нечетную вилку */ /* (если обе нечетные - то с большим номером) */ static void get_forks_odd (struct mudrec *this) { int left = this->left_fork, right = this->right_fork;
int first; int last;
if ((left & 1) > (right & 1)) { first = left; last = right; } else if ((left & 1) < (right & 1)) { first = right; last = left; } else { first = max (left, right); last = min (left, right); }
sem_wait (&semFork [first - 1]); sem_wait (&semFork [last - 1]); }
/* Берем вилки по очереди, в произвольном порядке. * Но если вторая вилка не берется сразу, то кладем первую. * То есть философ не расходует вилочное время впустую. */ static void get_forks_maybe_infinit_time (struct mudrec *this) { int left = this->left_fork, right = this->right_fork;
for (;;) { sem_wait (&semFork [left - 1]); if (0 == sem_trywait (&semFork [right - 1])) return; sem_post (&semFork [left - 1]); sem_wait (&semFork [right - 1]); if (0 == sem_trywait (&semFork [left - 1])) return; sem_post (&semFork [right - 1]); } }
/* Мелкие служебные функции */ static void stop (int dummy) { Stop = 1; }
static void usage (char name []) { fprintf (stderr, "Использование: %s [-a | -p | -I] [-t число_секунд] " "имя_философа ...\n", name); exit (1); }
/* Точка входа демонстрационной программы */ int main (int argc, char *argv []) { char buffer [LINE_MAX], *p; int i, n, c; int open_room_time = 300; int nMudr; struct sigaction sact;
while ((c = getopt (argc, argv, "apIt:")) != -1) { switch (c) { case 'a': get_forks = get_forks_simple; break; case 'p': get_forks = get_forks_odd; break; case 'I': get_forks = get_forks_maybe_infinit_time; break; case 't': open_room_time = strtol (optarg, &p, 0); if (optarg [0] == 0 || *p != 0) usage (argv [0]); break; default : usage (argv [0]); } }
nMudr = argc - optind; if (nMudr < 2) usage (argv [0]); /* Меньше двух */ /* философов неинтересно ... */
/* Создание канала для протокола обработки событий */ pipe (protokol);
kafedra = calloc (sizeof (struct mudrec), nMudr);
/* Зачисление на кафедру */ for (i = 0; i < nMudr; i++, optind++) { kafedra [i].name = argv [optind]; /* Выдадим телефон */ kafedra [i].private_pFdIn = fcntl (pFdIn, F_DUPFD, 0); /* Укажем новичку, какими вилками пользоваться */ kafedra [i].left_fork = i + 1; kafedra [i].right_fork = i + 2; } kafedra [nMudr - 1].right_fork = 1; /* Последний*/ /* пользуется вилкой первого */
/* Зададим реакцию на сигналы и установим будильник */ /* на конец обеда */ sact.sa_handler = stop; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGINT, &sact, ( struct sigaction *) NULL); (void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL); alarm (open_room_time);
/* Создадим семафоры для охраны вилок */ semFork = calloc (sizeof (sem_t), nMudr); for (i = 0; i < nMudr; i++) { sem_init (&semFork [i], 0, 1 /* На каждое место */ /* по одной вилке */); }
/* Философы входят в столовую */ for (i = 0; i < nMudr; i++) pthread_create (&kafedra [i].thread, NULL, &filosof, (void *) &kafedra [i]);
/* Выдача сообщений на стандартный вывод и выход */ /* после окончания всех задач */ close (pFdIn); while (1) { n = read (pFdOut, buffer, LINE_MAX); if (n == 0 || (n < 0 && errno != EINTR)) break; for (i = 0; i < n; i++) putchar (buffer [i]); } close (pFdOut);
/* Уничтожение семафоров */ for (i = 0; i < nMudr; i++) { sem_destroy (&semFork [i]); }
/* Выдача сводной информации */ { int full_eating_time = 0; int full_waiting_time = 0; int full_thinking_time = 0; for (i = 1; i <= nMudr; i++) { struct mudrec *this = &kafedra [i - 1];
full_eating_time += this->eat_time; full_waiting_time += this->wait_time; full_thinking_time += this->think_time;
if (this->count > 0) { float count = this->count; float think_time = this->think_time / count; float eat_time = this->eat_time / count; float wait_time = this->wait_time / count;
printf ("%s: ел %d раз в среднем: думал=%.1f " "ел=%.1f ждал=%.1f (максимум %d)\n", this->name, this->count, think_time, eat_time, wait_time, this->max_wait_time); } else printf ("%s: не поел\n", this->name); } /* for */
{ float total_time = (full_eating_time + full_waiting_time + full_thinking_time) / (float) nMudr;
printf("Среднее число одновременно едящих = %.3f\n", full_eating_time / total_time); printf("Среднее число одновременно ждущих = %.3f\n", full_waiting_time / total_time); } } /* Выдача сводной информации */
free (semFork); free (kafedra);
/* Сообщим об окончании работы. */ printf ("Конец обеда\n");
return 0; }
Листинг 4.19. Многопотоковый вариант решения задачи об обедающих философах с использованием семафоров реального времени.
Объекты в типизированной памяти
Объекты в типизированной памяти - это конфигурируемые реализацией именованные пулы памяти, доступные одному или нескольким процессорам системы через один или несколько портов. Пример подобной конфигурации показан на рис. 5.1.

Рис. 5.1. Пример многопроцессорной системы с несколькими пулами памяти
Здесь пул памяти M4 доступен только процессору P2. Пул M2 включает в себя пулы M2.1 и M2.2, причем доступ к M2.1 возможен только через порт B1. Остальные пулы доступны обоим процессорам через указанные на рисунке порты.
Разные пулы памяти могут обладать различными операционными характеристиками. Это может быть статическая или динамическая память, ПЗУ, энергонезависимая память и т.п.
Каждая допустимая комбинация пула памяти и порта идентифицируется именем, определяемым при конфигурировании системы способом, зависящим от реализации. Используя это имя, объект в типизированной памяти можно открыть и отобразить в адресное пространство процесса. Реализация должна поддерживать как динамическое выделение (резервирование) памяти из пула (аналог malloc(), когда приложение задает только запрашиваемый объем), так и отображение части пула с заданным в приложении смещением от начала. Зарезервированный фрагмент в последующем подлежит освобождению. Наконец, при доступе через определенный порт нужно иметь возможность выяснить смещение и размер непрерывного участка типизированной памяти.
Согласно стандарту POSIX-2001, для открытия объектов в типизированной памяти служит функция posix_typed_mem_open() (см. листинг 5.11).
#include
Листинг 5.11. Описание функции posix_typed_mem_open(). (html, txt)
Нормальным результатом функции posix_typed_mem_open() является файловый дескриптор объекта в типизированной памяти с именем name.
Аргумент oflag задает один из трех разрешенных видов доступа к объекту - O_RDONLY, O_WRONLY или O_RDWR.
Значение аргумента tflag определяет поведение объекта в типизированной памяти при последующих отображениях посредством функции mmap().
Может быть установлен один из трех флагов:
POSIX_TYPED_MEM_ALLOCATE
При вызове mmap() резервировать и отображать память, возможно, состоящую из нескольких несмежных сегментов (но в любом случае отображается она в непрерывный фрагмент адресного пространства процесса).
POSIX_TYPED_MEM_ALLOCATE_CONTIG
При вызове mmap() резервировать и отображать один непрерывный сегмент памяти.
POSIX_TYPED_MEM_MAP_ALLOCATABLE
При вызове mmap() отображать объект в адресное пространство процесса, не воздействуя на возможности резервирования памяти.
Если не установлен ни один из описанных флагов, указанный фрагмент объекта отображается в адресное пространство процесса и становится недоступным для резервирования; после отмены отображения возможность резервирования восстанавливается.
Отметим, что, в отличие от объектов в разделяемой памяти, объекты в типизированной памяти нельзя создать - их можно только открыть. Не применима к ним (точнее, к их открытым дескрипторам) и функция установки размера ftruncate().
После того, как объект в типизированной памяти открыт, посредством функции posix_typed_mem_get_info() (см. листинг 5.12) можно выяснить максимальный объем памяти, доступной для резервирования. Это важно, поскольку типизированная память зачастую является дефицитным ресурсом.
#include
Листинг 5.12. Описание функции posix_typed_mem_get_info(). (html, txt)
Соответствующая информация возвращается в структуре типа posix_typed_mem_info, на которую указывает аргумент info. Согласно стандарту POSIX-2001, эта структура должна содержать поле
size_t posix_tmi_length; /* Максимальный объем памяти, */ /* доступной для резервирования*/
Функция posix_typed_mem_get_info() принимает во внимание флаг POSIX_TYPED_MEM_ALLOCATE или POSIX_TYPED_MEM_ALLOCATE_CONTIG, если он был установлен при открытии объекта в типизированной памяти. При отсутствии этих флагов результат вызова posix_typed_mem_get_info() не специфицирован.
Разумеется, возвращаемый максимальный объем памяти, доступной для резервирования, есть величина, динамически изменяющаяся. Если объект в типизированной памяти является разделяемым, то нет гарантии, что в интервале между опросом и попыткой резервирования этот объем останется прежним.
Еще одним проявлением общего принципа "если объект может измениться, должна быть возможность опросить его текущее состояние" является наличие в стандарте POSIX-2001 функции posix_mem_offset() (см. листинг 5.13).
#include
Листинг 5.13. Описание функции posix_mem_offset(). (html, txt)
Функция posix_mem_offset() позволяет выяснить адрес (смещение от начала), длину и дескриптор объекта (блока) в типизированной памяти, отображенного в адресное пространство процесса, начиная с адреса addr и имеющего длину len. Искомое смещение записывается по указателю off. По указателю contig_len помещается минимум из значения len и длины максимального непрерывного блока типизированной памяти, отображенного, начиная с addr. По указателю fildes выдается дескриптор, использованный в вызове mmap() при задании опрашиваемого отображения (если с тех пор дескриптор закрыли, выдается -1).
Если дескриптор объекта в типизированной памяти был открыт без флагов POSIX_TYPED_MEM_ALLOCATE и POSIX_TYPED_MEM_ALLOCATE_CONTIG, то возвращенные функцией posix_mem_offset() значения, будучи подставлены в вызов mmap(), зададут отображение в точности того же блока типизированной памяти, что и при первоначальном обращении к mmap().
Поведение функции posix_mem_offset() при попытке опросить характеристики отображения объекта, не являющегося объектом в типизированной памяти, зависит от реализации.
Рассмотрим возможную последовательность применения описанных функций. Пусть процессу A1, выполняющемуся на процессоре P1 (см. выше рис. 5.1), требуется зарезервировать блок памяти из пула M2, причем этот блок предполагается использовать совместно с процессом A2, выполняющемся на процессоре P2.
Поскольку P2 имеет доступ только к M2.2, процессы должны использовать именно эту часть M2.
В качестве первого шага процесс A1 вызывает функцию posix_typed_mem_open() с именем объекта "/m2.2-b1" (или каким-то еще в том же духе, включающим имена пула и порта) и установленным в аргументе tflag флагом POSIX_TYPED_MEM_ALLOCATE, получая открытый дескриптор, пригодный для резервирования памяти. Затем A1 обращается с этим дескриптором к функции mmap(), указывая в качестве длины довольно большое значение, например, 1048576. Пусть в ответ на запрос система выделяет два несмежных блока типизированной памяти длиной 524288 байт каждый, отображает их в один непрерывный фрагмент адресного пространства процесса A1 и возвращает указатель на него. Теперь процесс A1 может стандартным образом работать с полученным мегабайтным массивом.
Если процесс A1 пожелает выяснить, какие части пула M2.2 были зарезервированы, он должен обратиться к функции posix_mem_offset() с адресом первого элемента массива и полной длиной (1048576). В ответ он получит смещение и длину первого из выделенных блоков (524288). Поскольку возвращенная длина меньше запрошенной, требуется повторный вызов posix_mem_offset() со смещением в полмегабайта от начала массива и соответственно уменьшенной длиной. (В общем случае, разумеется, следует организовать цикл, вызывая posix_mem_offset() до тех пор, пока не будет исчерпан весь отображенный массив. Чтобы избежать подобных относительно сложных действий, можно при открытии объекта в типизированной памяти установить в tflag флаг POSIX_TYPED_MEM_ALLOCATE_CONTIG, но тогда, возможно, попытка резервирования непрерывного мегабайтного блока закончится неудачей.)
Чтобы обеспечить совместное использование типизированной памяти с процессом A2, A1 должен каким-то образом (очевидно, с помощью средств межпроцессного взаимодействия) передать тому смещения и длины зарезервированных блоков. Получив эти данные, A2 обращается к posix_typed_mem_open() с именем "/m2.2-b2" (доступ через порт B2) и нулевым значением tflag, а затем дважды вызывает mmap() для отображения обоих непрерывных фрагментов типизированной памяти в свое адресное пространство.Вероятно, при втором вызове целесообразно указать соответствующий адрес начала и флаг MAP_FIXED, чтобы "склеить" эти отображения и, как и в случае с A1, получить непрерывный мегабайтный массив.
Отображение объектов в адресное пространство процессов
Идея, лежащая в основе рассматриваемого класса средств, в сущности, весьма проста. Если отобразить объект в адресное пространство процесса, то доступ к объекту можно осуществлять обычными операциями чтения/записи. Если один объект отображен в адресное пространство нескольких процессов, он превращается в средство межпроцессного взаимодействия, так как данные, записанные в объект одним процессом, появляются в адресных пространствах всех участников отображения.
Отображаться могут обычные файлы, а также объекты в разделяемой и типизированной памяти.
Реализация должна обеспечивать, чтобы отображению подвергалось целое число страниц памяти из адресного пространства процесса. Размер страницы является значением конфигурационной переменной PAGESIZE. Отображаемая часть объекта должна начинаться с границы страницы. Длина отображаемой части не обязана быть кратной странице.
Попытки доступа к отображенной памяти, лежащей за текущей границей объекта, приводят к генерации сигнала SIGBUS.
К отображенным страницам могут предоставляться различные виды доступа: на чтение, запись и/или выполнение. При попытке нарушить наложенные ограничения процессу доставляется сигнал SIGSEGV.
Отображение объектов в адресное пространство процессов осуществляется функцией mmap() (см. листинг 5.1).
#include
Листинг 5.1. Описание функции mmap(). (html, txt)
Отображаемая часть объекта задается файловым дескриптором fildes, смещением off и длиной len. (Вот для чего, заметим в скобках, при открытии объекта в разделяемой памяти функцией shm_open() возвращается файловый дескриптор - чтобы передать его mmap() и отобразить объект в адресное пространство процесса, а вовсе не для того, чтобы применять к нему операции файлового ввода/вывода. В исторически сложившихся реализациях функция mmap() появилась как средство отображения в память обычных файлов, так что с расширением функциональности другие виды отображаемых объектов пришлось стричь под ту же гребенку.)
Идея, лежащая в основе рассматриваемого класса средств, в сущности, весьма проста. Если отобразить объект в адресное пространство процесса, то доступ к объекту можно осуществлять обычными операциями чтения/записи. Если один объект отображен в адресное пространство нескольких процессов, он превращается в средство межпроцессного взаимодействия, так как данные, записанные в объект одним процессом, появляются в адресных пространствах всех участников отображения.
Отображаться могут обычные файлы, а также объекты в разделяемой и типизированной памяти.
Реализация должна обеспечивать, чтобы отображению подвергалось целое число страниц памяти из адресного пространства процесса. Размер страницы является значением конфигурационной переменной PAGESIZE. Отображаемая часть объекта должна начинаться с границы страницы. Длина отображаемой части не обязана быть кратной странице.
Попытки доступа к отображенной памяти, лежащей за текущей границей объекта, приводят к генерации сигнала SIGBUS.
К отображенным страницам могут предоставляться различные виды доступа: на чтение, запись и/или выполнение. При попытке нарушить наложенные ограничения процессу доставляется сигнал SIGSEGV.
Отображение объектов в адресное пространство процессов осуществляется функцией mmap() (см. листинг 5.1).
#include
Листинг 5.1. Описание функции mmap().
Отображаемая часть объекта задается файловым дескриптором fildes, смещением off и длиной len. (Вот для чего, заметим в скобках, при открытии объекта в разделяемой памяти функцией shm_open() возвращается файловый дескриптор - чтобы передать его mmap() и отобразить объект в адресное пространство процесса, а вовсе не для того, чтобы применять к нему операции файлового ввода/вывода. В исторически сложившихся реализациях функция mmap() появилась как средство отображения в память обычных файлов, так что с расширением функциональности другие виды отображаемых объектов пришлось стричь под ту же гребенку.)
Значения аргументов addr, prot и flags задают характеристики отображения - рекомендуемый адрес в адресном пространстве процесса, запрашиваемые виды доступа к отображенным страницам и управляющие флаги.
Результатом функции mmap() служит выбранный реализацией адрес в адресном пространстве процесса, начиная с которого отображен заданный фрагмент объекта, или, в случае неудачи, значение MAP_FAILED. Если в аргументе flags установлен флаг MAP_FIXED, результирующий адрес должен совпадать со значением addr; предполагается, что приложение располагает знаниями о целевой архитектуре, достаточными для точной спецификации отображения, что, впрочем, отрицательным образом сказывается на мобильности. Если флаг MAP_FIXED не установлен, а значение addr равно NULL, реализация имеет полную свободу в выборе результирующего адреса; подобную комбинацию следует рекомендовать как максимально мобильную.
В значении аргумента flags, помимо MAP_FIXED, могут быть установлены флаги MAP_SHARED (запись в отображенную память изменяет отображаемый объект со всеми вытекающими отсюда последствиями) или MAP_PRIVATE (изменения в отображенной памяти видны только записывающему процессу и не распространяются на отображаемый объект). Стандарт POSIX-2001 не специфицирует, имеет ли место обратная зависимость, то есть видны ли процессу, установившему флаг MAP_PRIVATE, изменения в отображаемом объекте.
Аргумент prot, определяющий разрешенные виды доступа к отображенным страницам, строится как комбинация флагов PROT_READ, PROT_WRITE и PROT_EXEC (разрешен доступ, соответственно, на чтение, запись и/или выполнение). Альтернативный вариант - установить флаг PROT_NONE, запрещающий доступ к странице (из общих соображений выключатель всегда должен быть под рукой). Значение аргумента prot должно быть согласовано с флагами, указанными при открытии объекта и получении дескриптора fildes.
Стандарт POSIX-2001 предусматривает возможность динамической смены разрешенных видов доступа к отображенным страницам посредством вызова функции mprotect() (см.
листинг 5.2).
#include
Листинг 5.2. Описание функции mprotect(). (html, txt)
Виды доступа меняются в соответствии со значением аргумента prot для всех страниц, пересекающихся с частью адресного пространства процесса, начинающейся со значения addr (которое должно указывать на границу страницы) и имеющей длину len байт.
Нормальным результатом функции mprotect() (и описываемых далее функций) служит нуль; в случае ошибки возвращается -1.
Для отмены отображений в адресное пространство процессов служит функция munmap() (см. листинг 5.3).
#include
Листинг 5.3. Описание функции munmap(). (html, txt)
Отображения отменяются для страниц, пересекающихся с частью адресного пространства процесса, начинающейся со значения addr (которое должно указывать на границу страницы) и имеющей длину len байт.
Отображение объектов в адресное пространство процессов - универсальное, исключительно мощное средств. В качестве примера применим его для реализации упрощенного аналога служебной программы cat (см. листинг 5.4).
Листинг 5.4. Пример программы, использующей файлы, отображенные в память. (html, txt)
Отметим, что обрабатываемые файлы только открываются, но не читаются, а отображаются в память, откуда и выдаются на стандартный вывод. Роль буфера играют отображенные страницы.
Значения аргументов addr, prot и flags задают характеристики отображения - рекомендуемый адрес в адресном пространстве процесса, запрашиваемые виды доступа к отображенным страницам и управляющие флаги.
Результатом функции mmap() служит выбранный реализацией адрес в адресном пространстве процесса, начиная с которого отображен заданный фрагмент объекта, или, в случае неудачи, значение MAP_FAILED. Если в аргументе flags установлен флаг MAP_FIXED, результирующий адрес должен совпадать со значением addr; предполагается, что приложение располагает знаниями о целевой архитектуре, достаточными для точной спецификации отображения, что, впрочем, отрицательным образом сказывается на мобильности. Если флаг MAP_FIXED не установлен, а значение addr равно NULL, реализация имеет полную свободу в выборе результирующего адреса; подобную комбинацию следует рекомендовать как максимально мобильную.
В значении аргумента flags, помимо MAP_FIXED, могут быть установлены флаги MAP_SHARED (запись в отображенную память изменяет отображаемый объект со всеми вытекающими отсюда последствиями) или MAP_PRIVATE (изменения в отображенной памяти видны только записывающему процессу и не распространяются на отображаемый объект). Стандарт POSIX-2001 не специфицирует, имеет ли место обратная зависимость, то есть видны ли процессу, установившему флаг MAP_PRIVATE, изменения в отображаемом объекте.
Аргумент prot, определяющий разрешенные виды доступа к отображенным страницам, строится как комбинация флагов PROT_READ, PROT_WRITE и PROT_EXEC (разрешен доступ, соответственно, на чтение, запись и/или выполнение). Альтернативный вариант - установить флаг PROT_NONE, запрещающий доступ к странице (из общих соображений выключатель всегда должен быть под рукой). Значение аргумента prot должно быть согласовано с флагами, указанными при открытии объекта и получении дескриптора fildes.
Стандарт POSIX-2001 предусматривает возможность динамической смены разрешенных видов доступа к отображенным страницам посредством вызова функции mprotect() (см.
листинг 5.2).
#include
Листинг 5.2. Описание функции mprotect().
Виды доступа меняются в соответствии со значением аргумента prot для всех страниц, пересекающихся с частью адресного пространства процесса, начинающейся со значения addr (которое должно указывать на границу страницы) и имеющей длину len байт.
Нормальным результатом функции mprotect() (и описываемых далее функций) служит нуль; в случае ошибки возвращается -1.
Для отмены отображений в адресное пространство процессов служит функция munmap() (см. листинг 5.3).
#include
Листинг 5.3. Описание функции munmap().
Отображения отменяются для страниц, пересекающихся с частью адресного пространства процесса, начинающейся со значения addr (которое должно указывать на границу страницы) и имеющей длину len байт.
Отображение объектов в адресное пространство процессов - универсальное, исключительно мощное средств. В качестве примера применим его для реализации упрощенного аналога служебной программы cat (см. листинг 5.4).
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Программа выдает на стандартный вывод файлы,*/ /* имена которых заданы в командной строке */ /* * * * * * * * * * * * * * * * * * * * * * * */
#include
int main (int argc, char *argv []) { struct stat sbuf; /* Структура для получения информации */ /* о файле */ int fd; /* Дескриптор обрабатываемого файла */ void *maddr; /* Адрес отображенного в память файла */ int i;
for (i = 1; i < argc; i++) { if ((fd = open (argv [i], O_RDONLY)) < 0) { perror ("OPEN"); continue; } if (fstat (fd, &sbuf) != 0) { perror ("FSTAT"); close (fd); continue; } if ((maddr = mmap (NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED){ perror ("MMAP"); close (fd); continue; } if (write (STDOUT_FILENO, maddr, sbuf.st_size) != sbuf.st_size) { perror ("WRITE"); } if (munmap (maddr, sbuf.st_size) != 0) { perror ("MUNMAP"); } close (fd); } /* for */
return 0; }
Листинг 5.4. Пример программы, использующей файлы, отображенные в память.
Отметим, что обрабатываемые файлы только открываются, но не читаются, а отображаются в память, откуда и выдаются на стандартный вывод. Роль буфера играют отображенные страницы.
При работе с объектами в памяти полезны функции truncate() и, особенно, ftruncate() (см. листинг 5.5), позволяющие установить размер объекта равным заданной величине length. Напомним, что попытка записи за пределы отображенного объекта не только не расширит его, но, весьма вероятно, приведет к доставке процессу сигнала SIGBUS.
#include
int truncate (const char *path, off_t length);
int ftruncate (int fildes, off_t length);
Листинг 5.5. Описание функций truncate() и ftruncate.
Функции truncate() и ftruncate() применимы к обычным файлам, которые в первом случае задаются маршрутным именем, а во втором - открытым дескриптором. Кроме того, функция ftruncate() применима к объектам в разделяемой памяти (и это единственная узаконенная стандартом POSIX-2001 файловая операция над дескриптором подобного объекта). Других способов изменить размер объекта в разделяемой памяти стандарт не предусматривает.
Применение объектов в разделяемой памяти в сочетании с функциями mmap() и ftruncate() иллюстрируется модифицированным вариантом двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод (см. листинги 5.6, 5.7, 5.8). Как и для исходного варианта, предполагается, что заголовочный файл называется "g_shm.h", а файл с образом процесса, запускаемого посредством execl(), - "g_r_shm".
#ifndef g_SHM #define g_SHM
/* Имя объекта в разделяемой памяти */ #define O_SHM_NAME "/g_o.shm"
/* Номер сигнала для синхронизации /* доступа к разделяемому сегменту */ #define SIG_SHM SIGALRM
#endif
Листинг 5.6. Заголовочный файл "g_shm.h" программы, копирующей строки со стандартного ввода на стандартный вывод. /* * * * * * * * * * * * * * * * * * * * * */ /* Программа, состоящая из двух процессов, */ /* копирует строки со стандартного ввода на*/ /* стандартный вывод, используя в качестве */ /* буфера разделяемый сегмент, отображенный*/ /* в адресные пространства процессов. */ /* Для синхронизации доступа к разделяемому*/ /* сегменту используется сигнал SIGALRM */ /* * * * * * * * * * * * * * * * * * * * * */
#include
#include "g_shm.h"
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Создание разделяемого сегмента памяти, */ /* отображение его в адресное пространство процесса, */ /* чтение строк со стандартного ввода в сегмент */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int fd_shm; /* Дескриптор объекта в разделяемой памяти */ void *addr_shm; /* Адрес отображенного в память объекта */ sigset_t smask; /* Маска блокируемых сигналов */ siginfo_t sinfo;/* Структура для получения данных о сигнале */ /* Длительность ожидания прихода сигнала */ struct timespec stmspc = {10, 0}; pid_t cpid; /* Идентификатор порожденного процесса */
/* Создадим разделяемый сегмент памяти */ if ((fd_shm = shm_open (O_SHM_NAME, O_RDWR | O_CREAT, 0777)) < 0) { perror ("SHM_CREAT"); return (1); }
/* Сделаем размер созданного объекта равным LINE_MAX */ if (ftruncate (fd_shm, LINE_MAX) != 0) { perror ("FTRUNCATE"); return (2); }
/* Отобразим сегмент в память */ if ((addr_shm = mmap (NULL, LINE_MAX, PROT_READ | PROT_WRITE, MAP_SHARED, fd_shm, 0)) == MAP_FAILED) { perror ("MMAP-P"); return (3); }
/* Сформируем маску сигналов (блокируем SIG_SHM) */ (void) sigemptyset (&smask); (void) sigaddset (&smask, SIG_SHM); (void) sigprocmask (SIG_BLOCK, &smask, (sigset_t *) NULL);
/* Подготовительная работа закончена */
switch (cpid = fork ()) { case -1: perror ("FORK"); return (4); case 0: /* Чтение из объекта и выдачу на стандартный*/ /* вывод реализуем в порожденном процессе*/ if (execl ("./g_r_shm", "g_r_shm", ( char *) NULL) < 0) { perror ("EXECL"); return (5); } }
/* Чтение строк со стандартного ввода в объект */ /* возложим на родительский процесс. */ /* В начальный момент объект в разделяемой памяти */ /* доступен для записи*/ while (fgets (addr_shm, LINE_MAX, stdin) != NULL) { /* Сообщим порожденному процессу, */ /* что в объект в разделяемой памяти */ /* помещена очередная строка */ assert (kill (cpid, SIG_SHM) == 0); /* Дождемся, когда в объект можно будет прочитать*/ /* следующую строку */ if (sigtimedwait (&smask, &sinfo, &stmspc) != SIG_SHM) { break; } }
/* Порожденный процесс должен завершиться*/ /* по контролю времени ожидания*/ (void) wait (NULL);
if (shm_unlink (O_SHM_NAME) != 0) { perror ("SHM_UNLINK"); return (6); }
return (0); }
Листинг 5.7. Исходный текст начального процесса двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. /* * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Процесс берет строки из объекта в разделяемой памяти*/ /* и выдает их на стандартный вывод */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include
#include "g_shm.h"
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Открытие разделяемого сегмента памяти, */ /* отображение его в адресное пространство процесса, */ /* чтение из сегмента и выдача строк на стандартный вывод */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int fd_shm; /* Дескриптор объекта в разделяемой */ /* памяти */ void *addr_shm; /* Адрес отображенного в память */ /* объекта */ sigset_t smask; /* Маска ожидаемых сигналов */ siginfo_t sinfo; /* Структура для получения данных*/ /* о сигнале */ /* Длительность ожидания строки */ struct timespec stmspc = {10, 0}; pid_t ppid; /* Идентификатор родительского процесса */
/* Откроем разделяемый сегмент памяти */ if ((fd_shm = shm_open (O_SHM_NAME, O_RDONLY, 0777)) < 0) { perror ("SHM_OPEN"); return (1); }
/* Отобразим сегмент в память */ if ((addr_shm = mmap (NULL, LINE_MAX, PROT_READ, MAP_SHARED, fd_shm, 0)) == MAP_FAILED) { perror ("MMAP-C"); return (2); }
/* Запомним идентификатор родительского процесса */ ppid = getppid ();
/* Сформируем маску ожидаемых сигналов (SIG_SHM) */ (void) sigemptyset (&smask); (void) sigaddset (&smask, SIG_SHM);
/* Подготовительная работа закончена */ fputs ("Вводите строки\n", stdout);
while (sigtimedwait (&smask, &sinfo, &stmspc) == SIG_SHM) { /* Дождались, когда в объекте появилась строка.*/ /* Выдадим ее на стандартный вывод*/ assert (fputs ("Вы ввели: ", stdout) != EOF); assert (fputs (addr_shm, stdout) != EOF); /* Сообщим родительскому процессу, */ /* что данные из объекта извлечены */ assert (kill (ppid, SIG_SHM) == 0); }
return 0; }
Листинг 5.8. Исходный текст порождаемого процесса (файл g_r_shm.c) двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод.
В модифицированном варианте сигналы (не имеет значения - обычные или реального времени) применяются только как средство синхронизации. Для выявления конца файла задействован механизм контроля длительности ожидания. Читателю предлагается поварьировать эти длительности, немного задержаться с вводом строк с терминала и проанализировать поведение программы.
Отметим, что при использовании объектов в разделяемой памяти, отображенных в адресные пространства процессов, передачи данных как таковой не требуется. В приведенной программе начальный процесс читает строки в разделяемый объект, а порожденный берет их оттуда же и выводит без каких-либо дополнительных копирований. Флаг MAP_SHARED в вызове mmap() обеспечивает доступность данных, записанных в объект одним процессом, всем другим процессам-читателям.
Еще одна возможность, полезная в связи с отображением объектов в адресное пространство процессов, - синхронизация (согласование состояния) оперативной и долговременной памяти. Это возможность реализует функция msync() (см. листинг 5.9).
#include
Листинг 5.9. Описание функции msync().
Функция msync() записывает в долговременную память все измененные страницы объекта в памяти, пересекающиеся с фрагментом адресного пространства процесса, начинающимся с адреса addr (это должна быть граница страницы) и имеющим длину len байт.
Если объектом является файл, отображенный в память (естественно, без флага MAP_PRIVATE), то вызов msync() гарантирует завершение всех операций записи с обеспечением целостности данных синхронизированного ввода/вывода (см.
курс [1]). Эффект от применения msync() к объектам в разделяемой и типизированной памяти не специфицирован.
Аргумент flags определяет характер действий при записи измененных страниц. Флаг MS_ASYNC означает асинхронную, а MS_SYNC - синхронную запись. Кроме того, флаг MS_INVALIDATE предписывает выполнить инициализацию кэша данных (приведение его в соответствие с содержимым долговременной памяти).
Отметим, что функция msync() обязана присутствовать только на системах, поддерживающих две необязательные возможности стандарта POSIX-2001 - файлы, отображенные в память, и синхронизированный ввод/вывод. В этом смысле ее можно считать дважды необязательной.
Вообще говоря, вызов msync() - не единственная причина, способная инициировать запись в долговременную память; подобная запись может стать следствием нормальной системной активности.
Проиллюстрируем работу с файлами, отображенными в память, еще одним примером, реализующим некоторые функции систем управления базами данных в памяти (см. листинг 5.10).
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа реализует некоторые функции систем управления базами*/ /* данных в памяти, следуя принципу неуничтожения информации */ /* (изменения записываются не в сам объет, а в его новую версию) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include
/* Размер страницы */ static int pgsz;
/* Структура для получения */ /* информации об исходном файле*/ static struct stat sbuf_src;
/* Адрес отображенного в память исходного файла*/ static char *saddr;
/* Признак того, что выполнялась функция */ /* обработки сигнала SIGSEGV */ static char sigsegv_catching;
/* * * * * * * * * * * * * * * * * * * * */ /* Функция обработки сигнала SIGSEGV. */ /* Предполагается, что этот сигнал */ /* генерируется при попытке записи в */ /* страницы, доступные только на чтение. */ /* Функция добавляет разрешение на запись*/ /* * * * * * * * * * * * * * * * * * * * */ static void sigsegv_sigaction (int sig, siginfo_t *sig_info, void *addr) { void *page_addr; /* Адрес страницы, в которую*/ /*пытались писать*/
page_addr = (char *) sig_info->si_addr - (off_t) ((char *) sig_info->si_addr - saddr) % pgsz; assert (mprotect (page_addr, pgsz, PROT_READ | PROT_WRITE) == 0); sigsegv_catching = 1; }
/* * * * * * * * * * * * * * * * * * * * * * * */ /* Установка способа обработки сигнала SIGSEGV */ /* * * * * * * * * * * * * * * * * * * * * * * */ static int sigsegv_set_proc (void (*sigsegv_actn) (int, siginfo_t *, void *), struct sigaction *old_sigsegv_sact) { struct sigaction sact;
(void) sigemptyset (&sact.sa_mask); sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = sigsegv_actn; if (sigaction (SIGSEGV, &sact, old_sigsegv_sact) != 0) { perror ("SIGACTION"); return (-1); }
return 0; }
/* * * * * * * * * * * * * * * * * * * * * */ /* Функция начала работы с исходным файлом.*/ /* Нормальный результат равен нулю */ /* * * * * * * * * * * * * * * * * * * * * */ static int init_src (char *name_src) { int fd_src; /* Дескриптор исходного файла */
/* Откроем исходный файл и отобразим его в память */ if ((fd_src = open (name_src, O_RDONLY)) < 0) { perror ("OPEN-SRC"); return (-1); } if (fstat (fd_src, &sbuf_src) != 0) { perror ("FSTAT"); return (-1); } if ((saddr = (char *) mmap (NULL, sbuf_src.st_size, PROT_READ, MAP_PRIVATE, fd_src, 0)) == MAP_FAILED) { perror ("MMAP"); return (-1); }
return 0; } /* * * * * * * * * * * * * * * * * * */ /* Опрос конфигурационных параметров */ /* * * * * * * * * * * * * * * * * * */ static int init_conf_params (void) { if ((pgsz = sysconf (_SC_PAGESIZE)) == -1) { perror ("SYSCONF"); return (-1); }
return 0; }
/* * * * * * * * * * * */ /* Общая инициализация */ /* * * * * * * * * * * */ static int init_all (char *name_src) { if ((init_src (name_src) != 0) || (sigsegv_set_proc (sigsegv_sigaction, (struct sigaction *) NULL) != 0) || (init_conf_params () != 0)) { return (-1); }
return 0; }
/* * * * * * * * * * * * * * * * * */ /* Запись в файл измененных страниц*/ /* * * * * * * * * * * * * * * * * */ static int write_dlt (char *name_dlt) { int fd_dlt; /* Дескриптор файла изменений*/ volatile char tmp; /* Значение пробного байта*/ off_t pos;/* Позиция в отображенной памяти*/
if ((fd_dlt = open (name_dlt, O_CREAT | O_WRONLY | O_TRUNC, 0777)) < 0) { perror ("OPEN-DLT"); return (-1); }
/* Измененные страницы выявим путем пробных записей. */ for (pos = 0; pos < sbuf_src.st_size; pos += pgsz) { tmp = saddr [pos]; sigsegv_catching = 0; saddr [pos] = tmp; if (sigsegv_catching == 0) { /* В страницу удалось записать */ /* без генерации сигнала SIGSEGV.*/ /* Значит, она была доступна на запись.*/ /* Следовательно, ее модифицировали.*/ /* Запишем ее в файл изменений */ (void) lseek (fd_dlt, pos, SEEK_SET); assert (write (fd_dlt, saddr + pos, pgsz) == pgsz); } /* Уберем добавленное разрешение на запись */ assert (mprotect (saddr + pos, pgsz, PROT_READ) == 0); } /* for */
return (close (fd_dlt)); }
/* * * * * * * * * * * * * */ /* Функция main() вызывает */ /* описанные выше функции */ /* и обращается на запись */ /* к отображенной памяти */ /* * * * * * * * * * * * * */ int main (int argc, char *argv []) { if (argc != 3) { fprintf (stderr, "Использование: %s исходный_файл " "файл_изменений\n", argv [0]); return (-1); }
if (init_all (argv [1]) != 0) { fprintf (stderr, "Ошибка инициализации\n"); return (-1); }
printf ("Размер страницы: %d\n" "Адрес отображенного в память исходного файла: %p\n" "Длина отображенного в память исходного файла: %ld\n", pgsz, saddr, sbuf_src.st_size);
saddr [0] = '*';
return (write_dlt (argv [2])); }
Листинг 5.10. Пример программы, использующей файлы, отображенные в память.
Приведенная программа следует фундаментальному принципу неуничтожения информации. При модификациях порождаются новые версии объектов; первоначальные объекты остаются неизменными. Подобный подход особенно полезен, когда СУБД является технологическим элементом инструментальной среды разработки программ, поскольку историю создания и модификации программных систем обязательно нужно сохранять.
Обратим внимание на изменение разрешенных видов доступа к отображенным страницам, реализуемое обращениями к функции mprotect().
Первоначально отображенная память доступна только на чтение. При попытке записи в отображенную память генерируется сигнал SIGSEGV, и функция его обработки добавляет доступ на запись к соответствующей странице. В результате появляется возможность отслеживания изменений и сохранения в последующем только измененных страниц, так что дисковое пространство на версии объектов будет расходоваться экономно.
Выбранный способ выявления измененных страниц путем пробных записей может показаться несколько вычурным (в функции обработки сигнала SIGSEGV можно было бы формировать список адресов страниц, в которые производится запись, и тогда в функции фиксации изменений достаточно просто пройти по этому списку), но он вытекает из еще одного важного принципа - в максимальной степени упрощать и ускорять обычную работу, даже за счет возможного усложнения и замедления редко исполняемых фрагментов.
К сожалению, приведенная программа не вполне соответствует стандарту POSIX-2001 в части обработки сигналов. По стандарту поведение процесса после нормального выхода из функций обработки сигналов SIGBUS, SIGFPE, SIGILL и SIGSEGV не определено, если только последние не были сгенерированы вызовами kill(), sigqueue() или raise(). (Заметим попутно, что и игнорирование перечисленных сигналов приводит к неопределенному поведению.) Строго говоря, и вызов mprotect() из функций обработки сигналов не считается безопасным. В общем, функция sigsegv_sigaction() - сама нестандартность и немобильность, и это, конечно, плохо, но то, что вся нестандартность и немобильность программы сосредоточена в одной небольшой функции, безусловно, хорошо.
size_t len, int prot, int
|
#include |
| Листинг 5.1. Описание функции mmap(). |
| Закрыть окно |
| #include |
| Листинг 5.2. Описание функции mprotect(). |
| Закрыть окно |
| #include |
| Листинг 5.3. Описание функции munmap(). |
| Закрыть окно |
| /* * * * * * * * * * * * * * * * * * * * * * * */ /* Программа выдает на стандартный вывод файлы,*/ /* имена которых заданы в командной строке */ /* * * * * * * * * * * * * * * * * * * * * * * */ #include int main (int argc, char *argv []) { struct stat sbuf; /* Структура для получения информации */ /* о файле */ int fd; /* Дескриптор обрабатываемого файла */ void *maddr; /* Адрес отображенного в память файла */ int i; for (i = 1; i < argc; i++) { if ((fd = open (argv [i], O_RDONLY)) < 0) { perror ("OPEN"); continue; } if (fstat (fd, &sbuf) != 0) { perror ("FSTAT"); close (fd); continue; } if ((maddr = mmap (NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED){ perror ("MMAP"); close (fd); continue; } if (write (STDOUT_FILENO, maddr, sbuf.st_size) != sbuf.st_size) { perror ("WRITE"); } if (munmap (maddr, sbuf.st_size) != 0) { perror ("MUNMAP"); } close (fd); } /* for */ return 0; } |
| Листинг 5.4. Пример программы, использующей файлы, отображенные в память. |
| Закрыть окно |
|
#include int truncate (const char *path, off_t length); int ftruncate ( int fildes, off_t length); |
| Листинг 5.5. Описание функций truncate() и ftruncate. |
| Закрыть окно |
| #ifndef g_SHM #define g_SHM /* Имя объекта в разделяемой памяти */ #define O_SHM_NAME "/g_o.shm" /* Номер сигнала для синхронизации /* доступа к разделяемому сегменту */ #define SIG_SHM SIGALRM #endif |
| Листинг 5.6. Заголовочный файл "g_shm.h" программы, копирующей строки со стандартного ввода на стандартный вывод. |
| Закрыть окно |
| /* * * * * * * * * * * * * * * * * * * * * */ /* Программа, состоящая из двух процессов, */ /* копирует строки со стандартного ввода на*/ /* стандартный вывод, используя в качестве */ /* буфера разделяемый сегмент, отображенный*/ /* в адресные пространства процессов. */ /* Для синхронизации доступа к разделяемому*/ /* сегменту используется сигнал SIGALRM */ /* * * * * * * * * * * * * * * * * * * * * */ #include #include "g_shm.h" /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Создание разделяемого сегмента памяти, */ /* отображение его в адресное пространство процесса, */ /* чтение строк со стандартного ввода в сегмент */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int fd_shm; /* Дескриптор объекта в разделяемой памяти */ void *addr_shm; /* Адрес отображенного в память объекта */ sigset_t smask; /* Маска блокируемых сигналов */ siginfo_t sinfo;/* Структура для получения данных о сигнале */ /* Длительность ожидания прихода сигнала */ struct timespec stmspc = {10, 0}; pid_t cpid; /* Идентификатор порожденного процесса */ /* Создадим разделяемый сегмент памяти */ if ((fd_shm = shm_open (O_SHM_NAME, O_RDWR | O_CREAT, 0777)) < 0) { perror ("SHM_CREAT"); return (1); } /* Сделаем размер созданного объекта равным LINE_MAX */ if (ftruncate (fd_shm, LINE_MAX) != 0) { perror ("FTRUNCATE"); return (2); } /* Отобразим сегмент в память */ if ((addr_shm = mmap (NULL, LINE_MAX, PROT_READ | PROT_WRITE, MAP_SHARED, fd_shm, 0)) == MAP_FAILED) { perror ("MMAP-P"); return (3); } /* Сформируем маску сигналов (блокируем SIG_SHM) */ (void) sigemptyset (&smask); (void) sigaddset (&smask, SIG_SHM); (void) sigprocmask (SIG_BLOCK, &smask, (sigset_t *) NULL); /* Подготовительная работа закончена */ switch (cpid = fork ()) { case -1: perror ("FORK"); return (4); case 0: /* Чтение из объекта и выдачу на стандартный*/ /* вывод реализуем в порожденном процессе*/ if (execl ("./g_r_shm", "g_r_shm", ( char *) NULL) < 0) { perror ("EXECL"); return (5); } } /* Чтение строк со стандартного ввода в объект */ /* возложим на родительский процесс. */ /* В начальный момент объект в разделяемой памяти */ /* доступен для записи*/ while (fgets (addr_shm, LINE_MAX, stdin) != NULL) { /* Сообщим порожденному процессу, */ /* что в объект в разделяемой памяти */ /* помещена очередная строка */ assert (kill (cpid, SIG_SHM) == 0); /* Дождемся, когда в объект можно будет прочитать*/ /* следующую строку */ if (sigtimedwait (&smask, &sinfo, &stmspc) != SIG_SHM) { break; } } /* Порожденный процесс должен завершиться*/ /* по контролю времени ожидания*/ (void) wait (NULL); if (shm_unlink (O_SHM_NAME) != 0) { perror ("SHM_UNLINK"); return (6); } return (0); } |
| Листинг 5.7. Исходный текст начального процесса двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. |
| Закрыть окно |
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Процесс берет строки из объекта в разделяемой памяти*/ /* и выдает их на стандартный вывод */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include #include "g_shm.h" /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Открытие разделяемого сегмента памяти, */ /* отображение его в адресное пространство процесса, */ /* чтение из сегмента и выдача строк на стандартный вывод */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { int fd_shm; /* Дескриптор объекта в разделяемой */ /* памяти */ void *addr_shm; /* Адрес отображенного в память */ /* объекта */ sigset_t smask; /* Маска ожидаемых сигналов */ siginfo_t sinfo; /* Структура для получения данных*/ /* о сигнале */ /* Длительность ожидания строки */ struct timespec stmspc = {10, 0}; pid_t ppid; /* Идентификатор родительского процесса */ /* Откроем разделяемый сегмент памяти */ if ((fd_shm = shm_open (O_SHM_NAME, O_RDONLY, 0777)) < 0) { perror ("SHM_OPEN"); return (1); } /* Отобразим сегмент в память */ if ((addr_shm = mmap (NULL, LINE_MAX, PROT_READ, MAP_SHARED, fd_shm, 0)) == MAP_FAILED) { perror ("MMAP-C"); return (2); } /* Запомним идентификатор родительского процесса */ ppid = getppid (); /* Сформируем маску ожидаемых сигналов (SIG_SHM) */ (void) sigemptyset (&smask); (void) sigaddset (&smask, SIG_SHM); /* Подготовительная работа закончена */ fputs ("Вводите строки\n", stdout); while (sigtimedwait (&smask, &sinfo, &stmspc) == SIG_SHM) { /* Дождались, когда в объекте появилась строка.*/ /* Выдадим ее на стандартный вывод*/ assert (fputs ("Вы ввели: ", stdout) != EOF); assert (fputs (addr_shm, stdout) != EOF); /* Сообщим родительскому процессу, */ /* что данные из объекта извлечены */ assert (kill (ppid, SIG_SHM) == 0); } return 0; } |
| Листинг 5.8. Исходный текст порождаемого процесса (файл g_r_shm.c) двухпроцессной программы, копирующей строки со стандартного ввода на стандартный вывод. |
| Закрыть окно |
|
#include |
| Листинг 5.9. Описание функции msync(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа реализует некоторые функции систем управления базами*/ /* данных в памяти, следуя принципу неуничтожения информации */ /* (изменения записываются не в сам объет, а в его новую версию) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include /* Размер страницы */ static int pgsz; /* Структура для получения */ /* информации об исходном файле*/ static struct stat sbuf_src; /* Адрес отображенного в память исходного файла*/ static char *saddr; /* Признак того, что выполнялась функция */ /* обработки сигнала SIGSEGV */ static char sigsegv_catching; /* * * * * * * * * * * * * * * * * * * * */ /* Функция обработки сигнала SIGSEGV. */ /* Предполагается, что этот сигнал */ /* генерируется при попытке записи в */ /* страницы, доступные только на чтение. */ /* Функция добавляет разрешение на запись*/ /* * * * * * * * * * * * * * * * * * * * */ static void sigsegv_sigaction (int sig, siginfo_t *sig_info, void *addr) { void *page_addr; /* Адрес страницы, в которую*/ /*пытались писать*/ page_addr = (char *) sig_info->si_addr - (off_t) ((char *) sig_info->si_addr - saddr) % pgsz; assert (mprotect (page_addr, pgsz, PROT_READ | PROT_WRITE) == 0); sigsegv_catching = 1; } /* * * * * * * * * * * * * * * * * * * * * * * */ /* Установка способа обработки сигнала SIGSEGV */ /* * * * * * * * * * * * * * * * * * * * * * * */ static int sigsegv_set_proc (void (*sigsegv_actn) (int, siginfo_t *, void *), struct sigaction *old_sigsegv_sact) { struct sigaction sact; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = sigsegv_actn; if (sigaction (SIGSEGV, &sact, old_sigsegv_sact) != 0) { perror ("SIGACTION"); return (-1); } return 0; } /* * * * * * * * * * * * * * * * * * * * * */ /* Функция начала работы с исходным файлом.*/ /* Нормальный результат равен нулю */ /* * * * * * * * * * * * * * * * * * * * * */ static int init_src (char *name_src) { int fd_src; /* Дескриптор исходного файла */ /* Откроем исходный файл и отобразим его в память */ if ((fd_src = open (name_src, O_RDONLY)) < 0) { perror ("OPEN-SRC"); return (-1); } if (fstat (fd_src, &sbuf_src) != 0) { perror ("FSTAT"); return (-1); } if ((saddr = (char *) mmap (NULL, sbuf_src.st_size, PROT_READ, MAP_PRIVATE, fd_src, 0)) == MAP_FAILED) { perror ("MMAP"); return (-1); } return 0; } /* * * * * * * * * * * * * * * * * * */ /* Опрос конфигурационных параметров */ /* * * * * * * * * * * * * * * * * * */ static int init_conf_params (void) { if ((pgsz = sysconf (_SC_PAGESIZE)) == -1) { perror ("SYSCONF"); return (-1); } return 0; } /* * * * * * * * * * * */ /* Общая инициализация */ /* * * * * * * * * * * */ static int init_all (char *name_src) { if ((init_src (name_src) != 0) || (sigsegv_set_proc (sigsegv_sigaction, (struct sigaction *) NULL) != 0) || (init_conf_params () != 0)) { return (-1); } return 0; } /* * * * * * * * * * * * * * * * * */ /* Запись в файл измененных страниц*/ /* * * * * * * * * * * * * * * * * */ static int write_dlt (char *name_dlt) { int fd_dlt; /* Дескриптор файла изменений*/ volatile char tmp; /* Значение пробного байта*/ off_t pos;/* Позиция в отображенной памяти*/ if ((fd_dlt = open (name_dlt, O_CREAT | O_WRONLY | O_TRUNC, 0777)) < 0) { perror ("OPEN-DLT"); return (-1); } /* Измененные страницы выявим путем пробных записей. */ for (pos = 0; pos < sbuf_src.st_size; pos += pgsz) { tmp = saddr [pos]; sigsegv_catching = 0; saddr [pos] = tmp; if (sigsegv_catching == 0) { /* В страницу удалось записать */ /* без генерации сигнала SIGSEGV.*/ /* Значит, она была доступна на запись.*/ /* Следовательно, ее модифицировали.*/ /* Запишем ее в файл изменений */ (void) lseek (fd_dlt, pos, SEEK_SET); assert (write (fd_dlt, saddr + pos, pgsz) == pgsz); } /* Уберем добавленное разрешение на запись */ assert (mprotect (saddr + pos, pgsz, PROT_READ) == 0); } /* for */ return (close (fd_dlt)); } /* * * * * * * * * * * * * */ /* Функция main() вызывает */ /* описанные выше функции */ /* и обращается на запись */ /* к отображенной памяти */ /* * * * * * * * * * * * * */ int main (int argc, char *argv []) { if (argc != 3) { fprintf (stderr, "Использование: %s исходный_файл " "файл_изменений\n", argv [0]); return (-1); } if (init_all (argv [1]) != 0) { fprintf (stderr, "Ошибка инициализации\n"); return (-1); } printf ("Размер страницы: %d\n" "Адрес отображенного в память исходного файла: %p\n" "Длина отображенного в память исходного файла: %ld\n", pgsz, saddr, sbuf_src.st_size); saddr [0] = '*'; return (write_dlt (argv [2])); } |
| Листинг 5.10. Пример программы, использующей файлы, отображенные в память. |
| Закрыть окно |
|
#include |
| Листинг 5.11. Описание функции posix_typed_mem_open(). |
| Закрыть окно |
|
#include |
| Листинг 5.12. Описание функции posix_typed_mem_get_info(). |
| Закрыть окно |
|
#include |
| Листинг 5.13. Описание функции posix_mem_offset(). |
| Закрыть окно |
|
#include |
| Листинг 5.14. Описание функции mlock(). |
| Закрыть окно |
|
#include |
| Листинг 5.15. Описание функции munlock(). |
| Закрыть окно |
|
#include int mlockall (int flags); int munlockall (void); |
| Листинг 5.16. Описание функций mlockall() и munlockall(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * */ /* Программа демонстрирует удержание в памяти*/ /* всего адресного пространства процесса, */ /* в том числе стека с учетом возможного роста*/ /* * * * * * * * * * * * * * * * * * * * * * */ #include #define LOCKED_STACK_SIZE 048576 /* * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Следующая функция нужна, чтобы вызвать рост стека до*/ /* требуемого размера. На всякий случай примем меры для*/ /* защиты от слишком умного оптимизирующего компилятора*/ /* * * * * * * * * * * * * * * * * * * * * * * * * * * */ static int dummy_func (void) { char dummy_arr [LOCKED_STACK_SIZE]; int i; int res = 0; dummy_arr [0] = 0; for (i = 1; i < LOCKED_STACK_SIZE; i++) { dummy_arr [i] = dummy_arr [i - 1] + i; } for (i = 0; i < LOCKED_STACK_SIZE; i++) { res += dummy_arr [i]; } return (res); } /* * * * * * * * * * * * * * * * * * * * * * * * * */ /* Функция main() вызывает вспомогательную функцию */ /* * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { if (mlockall (MCL_CURRENT | MCL_FUTURE) != 0) { perror ("MLOCKALL"); return (1); } fprintf (stderr, "Результат вспомогательной функции:" "%d\n", dummy_func ()); return 0; } |
| Листинг 5.17. Пример программы, удерживающей в памяти растущий стек. |
| Закрыть окно |
Средства удержания процессов в памяти
Современные системы имеют сложную аппаратную организацию. Отметим следующие особенности таких систем, существенные для приложений реального времени.
Подкачка страниц, непопадание в кэш, обращение к более медленной памяти вносят элементы недетерминированности в работу приложений. Чтобы по возможности избавиться от них, в стандарт POSIX введены средства удержания в физической памяти страниц из адресного пространства процессов. Они не ликвидируют недетерминированность полностью (например, ничего не говорится о длительности трансляции логического адреса в физический), но позволяют обеспечить разумную верхнюю границу времени доступа к командам и данным процессов.
Если реализация не поддерживает виртуальной памяти, то описываемые далее функции могут просто ничего не делать (кроме, быть может, проверки корректности аргументов).
Для удержания в физической памяти группы страниц из адресного пространства процесса служит функция mlock() (см. листинг 5.14), воспользоваться которой может только процесс, обладающий соответствующими привилегиями (см. курс [1], где раскрывается понятие "соответствующие привилегии").
#include
Листинг 5.14. Описание функции mlock(). (html, txt)
После успешного завершения вызова mlock() резидентными в памяти станут все страницы, пересекающиеся с частью адресного пространства процесса, начинающейся со значения addr и имеющей длину len байт. Реализация может требовать, чтобы значение addr указывало на границу страницы.
Если некоторая страница несколько раз отображена в адресные пространства процессов, то для ее удержания в памяти достаточно позаботиться о каком-либо одном отображении.
Удержание отменяется после вызовов fork() и exec(), а также ликвидации по какой-либо причине соответствующей части адресного пространства процесса (отмена отображения, завершение процесса и т.п.).
Явная отмена удержания группы страниц реализуется функцией munlock() (см. листинг 5.15).
#include
Листинг 5.15. Описание функции munlock(). (html, txt)
Более точно, munlock() разрешает больше не удерживать в памяти заданные страницы из адресного пространства вызывающего процесса. Если страница была отображена несколько раз и удерживалась в памяти несколькими вызовами mlock(), то одного обращения к munlock() для отмены удержания недостаточно.
Если нужно удерживать в памяти все адресное пространство процесса (что имеет место для большинства приложений реального времени), целесообразно воспользоваться функциями mlockall() и munlockall() (см. листинг 5.16).
#include
int mlockall (int flags);
int munlockall (void);
Листинг 5.16. Описание функций mlockall() и munlockall(). (html, txt)
Поскольку адресное пространство процесса при выполнении, вообще говоря, меняется, необходимо уточнить, что именно должно удерживаться в памяти. Для этого служит аргумент flags функции mlockall(), в значении которого могут фигурировать следующие флаги.
MCL_CURRENT
Удерживать в памяти страницы, присутствующие в адресном пространстве процесса на момент вызова.
MCL_FUTURE
Удерживать в памяти страницы, которые будут отображены в адресное пространство процесса после вызова.
Очевидно, установка обоих флагов позволяет удерживать в памяти и текущие, и будущие страницы.
Если установлен флаг MCL_FUTURE, то со временем общий объем удерживаемых страниц может превысить размеры физической памяти. В таком случае поведение операционной системы зависит от реализации.
Функция munlockall() отменяет удержание для всех страниц, присутствующих в адресном пространстве процесса на момент вызова или отображенных позднее, если только при вызове mlockall() не был установлен флаг MCL_FUTURE.
Определенную проблему составляет удержание в памяти страниц стека, рост которого не всегда можно точно оценить. (А стек, конечно, нуждается в удержании, иначе, например, функция обработки сигнала может выполниться с неожиданной задержкой.) Одно из возможных решений этой проблемы состоит в установке флага MCL_FUTURE при обращении к mlockall() и последующем вызове вспомогательной функции, в которой продекларирован автоматический массив достаточного размера (см.листинг 5.17).
Листинг 5.17. Пример программы, удерживающей в памяти растущий стек. (html, txt)
При написании такого рода программ следует позаботиться о защите от слишком интеллектуального оптимизирующего компилятора, который не только способен избавиться от неиспользуемых локальных переменных, но и может не включать в выходной код ненужные функции.
Функции управления планированием
Мы приступаем к рассмотрению функций для опроса и/или установки политики и/или параметров планирования применительно к процессам. Функции аналогичной направленности для потоков управления были рассмотрены нами ранее.
Стандарт POSIX-2001 предусматривает следующие функции управления планированием: sched_getscheduler() (опрос политики планирования процесса), sched_getparam() (опрос параметров планирования процесса), sched_setscheduler() (установка политики и параметров планирования процесса) и sched_setparam() (установка параметров планирования процесса) (см. листинг 6.1).
#include
int sched_getscheduler (pid_t pid);
int sched_getparam (pid_t pid, struct sched_param *param);
int sched_setscheduler (pid_t pid, int policy, const struct sched_param *param);
int sched_setparam (pid_t pid, const struct sched_param *param);
Листинг 6.1. Описание функций управления планированием. (html, txt)
Функция sched_getscheduler() возвращает в качестве (нормального) результата политику планирования процесса с заданным идентификатором (в случае неудачи результат равен -1). Если значение аргумента pid равно нулю, имеется в виду вызывающий процесс. Вообще говоря, опрос атрибутов других процессов подвержен контролю прав доступа, зависящему от реализации.
Функция sched_getparam() записывает параметры планирования заданного процесса по указателю param, в структуру типа sched_param; ее нормальный результат равен нулю. (Напомним, что, согласно стандарту POSIX-2001, у структуры типа sched_param только одно обязательное поле – приоритет sched_priority.)
Весьма мощной является функция sched_setscheduler(). Она позволяет установить новые политику и параметры планирования заданного процесса и возвращает в качестве нормального результата прежнюю политику. Разумеется, установка атрибутов планирования подвержена контролю прав доступа в еще большей степени, чем опрос: даже в изменении собственных атрибутов процессу может быть отказано.
Вызов sched_setscheduler() не влияет впрямую на планирование потоков управления.
Косвенно могут быть затронуты лишь потоки заданного процесса, если их область планирования конкуренции есть PTHREAD_SCOPE_PROCESS (из-за того, что меняются атрибуты планирования содержащего их процесса).
Любопытно отметить, что стандарт POSIX-2001 не требует атомарности вызова sched_setscheduler() с точки зрения выполняющихся потоков управления. Хотя в итоге изменится все или ничего, эти изменения могут наблюдаться как поэтапные; соответственно они будут сказываться на поведении потоков.
Функция sched_setparam(), в отличие от sched_setscheduler(), изменяет только параметры планирования, но ее воздействие на заданный процесс стандартизовано детальнее.
В общем случае заданный процесс помещается в хвост списка, соответствующего его новому приоритету.
Если новый приоритет заданного процесса выше, чем у выполняющегося, и заданный процесс готов к выполнению, он вытесняет с процессора наименее приоритетный процесс. (Заметим в скобках, что это одно из немногих мест в стандарте, точно регламентирующих поведение приложения в рамках многопроцессорной конфигурации.)
Если вызывающий процесс уменьшает собственный приоритет, он, в свою очередь, может оказаться вытесненным с процессора.
Стандарт POSIX-2001 предоставляет средства для опроса характеристик политик планирования – минимального (sched_get_priority_min()) и максимального (sched_get_priority_max()) среди допустимых приоритетов, а также величины кванта выделяемого процессорного времени для политики циклического планирования (sched_rr_get_interval()) (см. листинг 6.2).
#include
int sched_get_priority_min (int policy);
int sched_get_priority_max (int policy);
int sched_rr_get_interval (pid_t pid, struct timespec *q_ptr);
Листинг 6.2. Описание функций опроса характеристик политик планирования. (html, txt)
Функция sched_rr_get_interval() записывает по указателю q_ptr в структуру типа timespec величину кванта процессорного времени, выделяемого заданному процессу, и возвращает нуль в качестве нормального результата.
Нормальными результатами функций sched_get_priority_min() и sched_get_priority_max(), разумеется, служат, соответственно, минимальный и максимальный приоритеты.
Несколько особняком стоит альтруистическая функция sched_yield() (см. листинг 6.3), позволяющая вызывающему потоку управления добровольно уступить процессор. Результат (нулевой) поток получит только после того, как вновь окажется в голове своего списка и будет выбран планировщиком на выполнение.
#include
Листинг 6.3. Описание функции sched_yield(). (html, txt)
Следующая программа (см. листинг 6.4) иллюстрирует применение описанных функций. Возможные результаты ее выполнения для ОС Linux и операционной системы реального времени oc2000 показаны на листингах 6.5 и 6.6.
Листинг 6.4. Пример программы, использующей функции управления планированием. (html, txt)
Листинг 6.5. Возможные результаты выполнения программы, использующей функции управления планированием, в ОС Linux. (html, txt)
Листинг 6.6. Возможные результаты выполнения программы, использующей функции управления планированием, в операционной системе реального времени oc2000. (html, txt)
Проиллюстрируем теперь ситуацию с инверсией приоритетов (см. листинг 6.7).
Листинг 6.7. Пример программы, моделирующей ситуацию инверсии приоритетов. (html, txt)
Идея приведенной программы состоит в том, что у потоков управления с низким и высоким приоритетами имеется общий семафор, охраняющий вход в критический интервал. Так получается, что этот семафор первым захватывает процесс с низким приоритетом. Его критический интервал мал, но в это время оказывается готовым к выполнению поток управления со средним приоритетом, который на заметное время занимает процессор, не давая низкоприоритетному выйти из критического интервала и освободить семафор. Из-за этого поток с высоким приоритетом оказывается блокированным.
Из технических деталей обратим внимание на использование значения PTHREAD_EXPLICIT_SCHED аргумента inheritsched функции pthread_attr_setinheritsched().Оно предписывает извлекать характеристики планирования создаваемых потоков управления из атрибутного объекта, а не наследовать их у создающего потока. В данном случае это важно, так как потоки управления необходимо создавать с разными приоритетами.
Возможные результаты выполнения приведенной программы под управлением операционной системы реального времени oc2000 показаны на листинге 6.8.
Листинг 6.8. Возможные результаты выполнения программы, моделирующей ситуацию инверсии приоритетов, под управлением операционной системы реального времени oc2000. (html, txt)
Основные идеи и понятия
Мы приступаем к рассмотрению политик и параметров планирования применительно к использованию процессоров потоками управления (процессами), готовыми к выполнению. Эта тема уже затрагивалась в курсе [1] и предыдущих разделах данного курса, посвященных потокам управления.
Приложения реального времени, чтобы удовлетворить налагаемым на них внешним ограничениям, должны располагать средствами контроля за порядком выполнения входящих в их состав параллельных процессов. При этом планирование должно быть быстрым и детерминированным, с гарантией вытеснения низкоприоритетных процессов высокоприоритетными сразу после того, как последние оказываются готовыми к выполнению.
Процессы реального времени, по сравнению с прочими, должны обладать более высокими приоритетами. В таком случае они смогут передавать друг другу вычислительные ресурсы как мобильным, так и системно-зависимым образом.
Общая идея приоритетного планирования состоит в следующем. У каждого потока управления (процесса) в каждый момент времени есть определенный приоритет. Равноприоритетные потоки управления, готовые к выполнению, объединяются в списки, так что поток попадает в список в соответствии со своим текущим приоритетом.
Списки равноприоритетных потоков упорядочены, от первого элемента (головы) к последнему (хвосту). Цель политики планирования состоит в определении допустимых операций над множеством списков (например, перемещение потоков между списками и внутри них).
С каждой политикой ассоциирован допустимый диапазон приоритетов. Диапазоны для разных политик, разумеется, могут и, более, того, должны, перекрываться, иначе, например, не удастся организовать параллельную детерминированную обработку периодических и непериодических событий.
Реализация, удовлетворяющая стандарту POSIX, должна выбирать для выполнения процесс (поток управления), находящийся в голове наиболее приоритетного непустого списка, независимо от ассоциированной политики планирования. После этого процесс (поток управления) удаляется из списка.
Другие действия (предыдущие и последующие) специфичны для установленной политики планирования. Рассмотрим их.
Политика SCHED_FIFO (планирование по очереди) предписывает упорядочивать потоки управления в пределах каждого списка по длительности ожидания. Иными словами, в голове списка располагается поток, ожидающий доступ к процессору дольше других. Операции со списками для этой политики определяются следующим образом.
Допустимый диапазон приоритетов должен включать по крайней мере 32 различных значения и ограничиваться величинами, которые возвращают функции sched_get_priority_min() и sched_get_priority_max() с аргументом SCHED_FIFO.
Политика планирования по очереди удовлетворяет сформулированным выше требованиям к обслуживанию приложений реального времени. Критически важный (наиболее приоритетный) процесс будет выполняться всегда, когда он к этому готов, занимая процессор столько времени, сколько пожелает. Иногда политику SCHED_FIFO называют "выполнением до завершения" или "выполнением до блокирования". В многопользовательских системах с разделением времени подобная политика, разумеется, выглядела бы недружественной, но для систем реального времени она хороша прежде всего своей детерминированностью.
Чтобы сделать планирование более справедливым по отношению к равноприоритетным процессам, можно воспользоваться политикой SCHED_RR (циклическое планирование ). Она эквивалентна SCHED_FIFO с одним исключением: когда время, в течение которого поток управления занимал процессор, становится больше или равным результату функции sched_rr_get_interval(), он перемещается в хвост соответствующего списка, а для выполнения выбирается головной поток. Таким образом, ни один из равноприоритетных потоков не сможет монополизировать процессор.
Если поток управления вытесняется, а потом возобновляет выполнение, он "дорабатывает" выделенный ему квант процессорного времени.
Несмотря на кажущуюся простоту, применение политики циклического планирования требует понимания некоторых тонкостей. Если предположить, что накладные и непредвиденные расходы процессорного времени малы, для политики SCHED_RR будут справедливы следующие утверждения:
Если накладными и непредвиденными расходами процессорного времени пренебречь нельзя, встает вопрос, на кого их списывать. От ответа на этот вопрос зависит, какое из сформулированных выше утверждений станет ложным. Если списывать расходы "на систему" и честно выделять каждому потоку управления Q единиц процессорного времени, поток может возобновить свое выполнение существенно позднее, чем через (N – 1) * Q единиц времени. Обычно для приложений реального времени это неприемлемо (не гарантируется время отклика на события, обслуживаемые потоками), поэтому расходы списывают на потоки, выделяя каждому из них не более Q единиц процессорного времени, что делает ложным первое утверждение, но гарантирует истинность второго. Разумеется, взрывная активность периферийных устройств, "отвлекающих" процессор на обработку прерываний, может практически целиком "съесть" выделяемый потоку квант времени; если подобная активность окажется периодической с тем же периодом, что и циклическое планирование, некоторые потоки рискуют оказаться на голодном пайке, поэтому стандарт добавляет еще одно требование к реализации политики SCHED_RR: ни один поток управления не должен ожидать доступ к процессору неопределенно долго.
Отметим, что стандарт POSIX-2001 не предоставляет средств для установки размера кванта времени, выделяемого потокам при циклическом планировании. Кроме того, не уточняется, является ли этот размер общесистемной величиной (как оно обычно бывает) или может быть разным для разных процессов.
Подчеркнем также, что политика планирования – это атрибут потока управления (процесса), а не глобальная, общесистемная характеристика. В рамках одного приложения могут сосуществовать потоки не только с разными приоритетами, но и с разными политиками планирования. Их осмысленная кооперация – забота приложения.
Период пополнения бюджета характеризуется значением элемента sched_ss_repl_period описанной ранее структуры типа sched_param. Доступный бюджет вычислительных ресурсов инициализируется значением элемента sched_ss_init_budget той же структуры.
Политика спорадического планирования предусматривает ряд дополнительных условий, при выполнении которых приоритет потока управления "переключается" между значениями элементов sched_priority и sched_ss_low_priority структуры типа sched_param.
Приоритет потока определяется следующим образом. Если доступный бюджет вычислительных ресурсов положителен и число ждущих операций пополнения бюджета меньше значения элемента sched_ss_max_repl структуры типа sched_param, потоку управления присваивается приоритет sched_priority, в противном случае - sched_ss_low_priority. Если значение sched_priorityне больше, чем sched_ss_low_priority, приоритет считается неопределенным. Изменение доступного бюджета вычислительных ресурсов и, следовательно, присваиваемого приоритета, производится по следующим правилам.
Когда поток, находящийся в голове списка для sched_priority, получает доступ к процессору, время его выполнения ограничивается доступным бюджетом вычислительных ресурсов (с поправкой на разрешающую способность используемых часов, зависящую от реализации).
выше). Операция пополнения бюджета планируется на момент времени, равный сумме времени активации и значения элемента sched_ss_repl_period структуры типа sched_param. Число операций пополнения бюджета, планируемых за один раз, не должно превышать значения sched_ss_max_repl.
Операция пополнения бюджета состоит в прибавлении в запланированное время размера пополнения к доступному бюджету вычислительных ресурсов. Если при этом бюджет станет больше начального, он уменьшается до значения sched_ss_initial_budget. Кроме того, если поток управления был готов к выполнению или выполнялся с приоритетом sched_ss_low_priority, он помещается в хвост списка sched_priority.
Приведенное описание выглядит довольно замысловатым, хотя основная идея относительно проста. Определенные вычислительные ресурсы резервируются для обработки непериодических событий с высоким приоритетом (sched_priority). Если отведенного времени не хватило, остальные события обрабатываются в фоновом режиме с (низким) приоритетом sched_ss_low_priority. Истраченное "приоритетное" время возвращается в бюджет в результате выполнения планируемых операций пополнения, по истечении периода пополнения. При этом поток может вернуться с низкого приоритета на высокий.
По стандарту реализация должна допускать не менее _POSIX_SS_REPL_MAX ждущих операций пополнения бюджета. Значение этой конфигурационной константы определено равным четырем, так что один серверный поток управления может мобильным образом обслуживать за каждый период пополнения до четырех спорадических событий.
В целом стандартизованная политика планирования SCHED_SPORADIC представляет собой разумный компромисс между эффективностью обработки непериодических событий, детерминированностью выполнения периодических процессов и сложностью реализации. Спорадическое планирование может применяться в системах авионики, управления роботами, промышленной автоматизации и т.п.
Четвертая политика планирования, которую должны поддерживать реализации, соответствующие стандарту POSIX, называется "прочей" (SCHED_OTHER).
Она необходима, чтобы мобильные приложения могли заявить, что они больше не нуждаются в политике планирования реального времени.
Используемые в приложении политики и параметры планирования выбираются с целью выполнения предъявляемых требований реального времени, которые, в свою очередь, являются отражением условий функционирования приложения. Если это условия постоянны, нет нужды динамически менять политику и/или приоритеты. Чаше всего так и бывает. Но в некоторых случаях (например, при спуске космического аппарата) условия могут измениться кардинально, что требует если и не смены приложения, то его существенной перестройки. В этой связи важно отметить, что стандарт POSIX-2001 предоставляет достаточно средств для динамического изменения значений атрибутов планирования.
Вообще говоря, установки и изменения атрибутов планирования могут производиться мелкими порциями, путем обращения к множеству функций вроде pthread_setschedprio(), или массово, в результате вызова одной функции с несколькими аргументами или одним структурным аргументом (такой, например, как pthread_setschedparam()). При начальной установке второй подход предпочтительнее. Из соображений минимальности интерфейса его следует распространить и на другие ситуации, что в общем и целом и сделано в стандарте POSIX-2001. Дополнительным доводом в пользу данного подхода является простота поддержания целостности конфигурации планирования, если значения отдельных характеристик могут оказаться некорректными для определенных реализаций: вызов "массовой" функции изменит все или ничего.
Интуитивно очевидно, что планировщик должен запускаться тогда, когда происходят события, способные вызвать передачу процессора другому потоку управления (процессу). Важно, что стандарт POSIX-2001 идет существенно дальше интуитивной очевидности, явно перечисляя подобные события:
Стандарт детально и точно формулирует правила манипулирования очередями потоков, готовых к выполнению, а также правила вытеснения выполняющегося потока с процессора. Это позволяет разработчикам приложений мобильным образом добиться детерминированного планирования.
Подчеркнем концептуальную экономность и целостность стандарта POSIX-2001 применительно к планированию. Вообще говоря, можно представить себе подход, при котором доступ к процессору и к примитивам синхронизации базируется на разных принципах (скажем, при освобождении мьютекса он мог бы доставаться первому по очереди, то есть ждущему дольше других). Разработчики стандарта POSIX-2001 сочли такой подход неприемлемым, поскольку он по меньшей мере затрудняет анализ выполнения требований реального времени. Согласно стандарту, когда примитив синхронизации освобождается, он достается потоку управления, первому с точки зрения применимой политики планирования. (Иными словами, из числа блокированных (приостановленных) в разряд готовых к выполнению переводится самый приоритетный поток.) В POSIX-2001 есть только одно планирование и принципы его едины.
Естественно, стандарт не может регламентировать все. Некоторые аспекты поведения приложений зависят от реализации. Например, в стандарте ничего не говорится о приоритете системных процессов; соответствующая информация должна быть представлена в документации по операционной системе. (Общее требование состоит в том, чтобы приоритет системных процессов был ниже, чем у процессов реального времени.)
Есть проблемы, трудные для текущего периода стандартизации. К таковым относится планирование в рамках многопроцессорных конфигураций. POSIX-2001 констатирует лишь, что для подобных конфигураций правила политик планирования интерпретируются способом, зависящим от реализации. Подлинная стандартизация здесь еще предстоит.
Наконец, у приоритетного планирования есть концептуальные проблемы, такие, как инверсия приоритетов, когда высокоприоритетный поток управления, не приостановленный по собственной воле в ожидании какого-либо события, не может продолжить выполнение, в то время как поток с более низким приоритетом успешно выполняется.Зачастую инверсия приоритетов является следствием конкуренции за разделяемые ресурсы; далее мы рассмотрим соответствующие примеры.
Несмотря на то, что в стандарте POSIX-2001 приняты определенные меры для ограничения инверсии приоритетов – при захвате мьютекса приоритет потока управления повышается в соответствии с протоколом, являющимся одним из атрибутов мьютекса (см. выше раздел, посвященный мьютексам), это конечно, нельзя считать полным решением проблемы инверсии (например, для семафоров ничего подобного не предусмотрено). Тем не менее, POSIX-2001 служит хорошей основой для разработки мобильных приложений реального времени, в том числе применительно к весьма сложным и тонким аспектам.
pid_t pid, int policy, const
|
#include int sched_getscheduler (pid_t pid); int sched_getparam (pid_t pid, struct sched_param *param); int sched_setscheduler ( pid_t pid, int policy, const struct sched_param *param); int sched_setparam (pid_t pid, const struct sched_param *param); |
| Листинг 6.1. Описание функций управления планированием. |
| Закрыть окно |
|
#include int sched_get_priority_min (int policy); int sched_get_priority_max (int policy); int sched_rr_get_interval (pid_t pid, struct timespec *q_ptr); |
| Листинг 6.2. Описание функций опроса характеристик политик планирования. |
| Закрыть окно |
|
#include |
| Листинг 6.3. Описание функции sched_yield(). |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа опрашивает характеристики политик планирования, */ /* а также атрибуты планирования текущего процесса */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include int main (void) { struct sched_param shdprm; /* Значения параметров */ /* планирования */ struct timespec qp; /* Величина кванта */ /* процессорного времени */ printf ("Допустимые диапазоны приоритетов для разных " "политик планирования\n"); printf ("SCHED_FIFO : от %d до %d\n", sched_get_priority_min (SCHED_FIFO), sched_get_priority_max (SCHED_FIFO)); printf ("SCHED_RR : от %d до %d\n", sched_get_priority_min (SCHED_RR), sched_get_priority_max (SCHED_RR)); printf ("SCHED_OTHER: от %d до %d\n", sched_get_priority_min (SCHED_OTHER), sched_get_priority_max (SCHED_OTHER)); printf ("Текущая политика планирования для текущего " "процесса: "); switch (sched_getscheduler (0)) { case SCHED_FIFO: printf ("SCHED_FIFO\n"); break; case SCHED_RR: printf ("SCHED_RR\n"); break; case SCHED_OTHER: printf ("SCHED_OTHER\n"); break; case -1: perror ("SCHED_GETSCHEDULER"); break; default: printf ("Неизвестная политика планирования\n"); } if (sched_getparam (0, &shdprm) == 0) { printf ("Текущий приоритет текущего процесса: %d\n", shdprm.sched_priority); } else { perror ("SCHED_GETPARAM"); } shdprm.sched_priority = 50; if (sched_setscheduler (0, SCHED_RR, &shdprm) == -1) { perror ("SCHED_SETSCHEDULER"); } if (sched_rr_get_interval (0, &qp) == 0) { printf ("Квант процессорного времени при " "циклическом планировании: %g сек\n", qp.tv_sec + qp.tv_nsec / 1000000000.0); } else { perror ("SCHED_RR_GET_INTERVAL"); } return 0; } |
| Листинг 6.4. Пример программы, использующей функции управления планированием. |
| Закрыть окно |
|
Допустимые диапазоны приоритетов для разных политик планирования SCHED_FIFO : от 1 до 99 SCHED_RR : от 1 до 99 SCHED_OTHER : от 0 до 0 Текущая политика планирования для текущего процесса: SCHED_OTHER Текущий приоритет текущего процесса: 0 Квант процессорного времени при циклическом планировании: 0.15 сек |
| Листинг 6.5. Возможные результаты выполнения программы, использующей функции управления планированием, в ОС Linux. |
| Закрыть окно |
| Допустимые диапазоны приоритетов для разных политик планирования SCHED_FIFO : от 1 до 255 SCHED_RR : от 1 до 255 SCHED_OTHER : от 0 до 255 Текущая политика планирования для текущего процесса: SCHED_FIFO Текущий приоритет текущего процесса: 100 Квант процессорного времени при циклическом планировании: 0.08 сек |
| Листинг 6.6. Возможные результаты выполнения программы, использующей функции управления планированием, в операционной системе реального времени oc2000. |
| Закрыть окно |
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа моделирует ситуацию инверсии приоритетов. */ /* Идея состоит в том, что имеется три потока */ /* управления с низким, средним */ /* и высоким приоритетами. */ /* Поток с низким приоритетом захватывает семафор, */ /* когда на него никто больше не претендует, */ /* но не может освободить его, */ /* потому что его вытесняет с процессора */ /* поток со средним приоритетом. */ /* В это время поток с высоким приоритетом хочет */ /* захватить тот же семафор, */ /* но он занят и неизвестно когда освободится... */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ #include static sem_t sem_mi; /* Семафор для потока со средним */ /* приоритетом */ static sem_t sem_hi; /* Семафор для потока с высоким */ /* приоритетом */ static double s = 0; /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока с высоким приоритетом */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_hi (void *dummy) { printf ("Поток с высоким приоритетом перед захватом " "семафора\n"); assert (sem_wait (&sem_hi) == 0); printf ("Поток с высоким приоритетом после захвата " "семафора\n"); assert (sem_post (&sem_hi) == 0); return (NULL); } /* * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока со средним приоритетом */ /* * * * * * * * * * * * * * * * * * * * * * * * * */ void *start_mi (void *dummy) { int i; double d = 1; printf ("Поток со средним приоритетом перед захватом " "семафора\n"); assert (sem_wait (&sem_mi) == 0); /* Займем процессор вычислениями */ for (i = 1; i < 100000000; i++) { s += d/i; d = -d; } printf ("Поток со средним приоритетом перед " "освобождением семафора\n"); assert (sem_post (&sem_mi) == 0); return (&s); } /* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Стартовая функция потока с низким приоритетом */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */ int main (void) { pthread_attr_t attrob; /* Атрибутный объект */ /* создаваемых потоков */ struct sched_param shdprm; /* Значения параметров */ /* планирования */ int shdplc; /* Политика планирования */ pthread_t pt_mi; /* Идентификатор потока */ /* со средним приоритетом */ pthread_t pt_hi; /* Идентификатор потока */ /* с высоким приоритетом */ /* Создадим два семафора в захваченном состоянии */ assert (sem_init (&sem_mi, 0, 0) == 0); assert (sem_init (&sem_hi, 0, 0) == 0); /* Установим политику планирования и сделаем */ /* текущий поток управления низкоприоритетным */ shdprm.sched_priority = sched_get_priority_max (SCHED_FIFO) – 31; assert (pthread_setschedparam (pthread_self (), SCHED_FIFO, &shdprm) == 0); /* Инициализируем атрибутный объект */ /* для создаваемых потоков управления */ assert (pthread_attr_init (&attrob) == 0); /* Установим атрибуты планирования */ assert (pthread_attr_setinheritsched (&attrob, PTHREAD_EXPLICIT_SCHED) == 0); assert (pthread_attr_setschedpolicy (&attrob, SCHED_FIFO) == 0); shdprm.sched_priority += 15; assert (pthread_attr_setschedparam (&attrob, &shdprm) == 0); /* Создадим поток управления со средним приоритетом */ assert (pthread_create (&pt_mi, &attrob, start_mi, NULL) == 0); /* Подправим атрибутный объект и создадим */ /* поток управления с высоким приоритетом */ shdprm.sched_priority += 15; assert (pthread_attr_setschedparam (&attrob, &shdprm) == 0); assert (pthread_create (&pt_hi, &attrob, start_hi, NULL) == 0); /* Опросим параметры планирования потоков управления */ assert (pthread_getschedparam (pthread_self (), &shdplc, &shdprm) == 0); assert (shdplc == SCHED_FIFO); printf ("Низкий приоритет: %d\n", shdprm.sched_priority); assert (pthread_getschedparam (pt_mi, &shdplc, &shdprm) == 0); assert (shdplc == SCHED_FIFO); printf ("Средний приоритет: %d\n", shdprm.sched_priority); assert (pthread_getschedparam (pt_hi, &shdplc, &shdprm) == 0); assert (shdplc == SCHED_FIFO); printf ("Высокий приоритет: %d\n", shdprm.sched_priority); /* Создадим ситуацию инверсии приоритетов */ printf ("Поток с низким приоритетом\n" "перед освобождением семафора для потока " "со средним приоритетом\n"); assert (sem_post (&sem_mi) == 0); printf ("Поток с низким приоритетом\n" "перед освобождением семафора для потока " "с высоким приоритетом\n"); assert (sem_post (&sem_hi) == 0); (void) pthread_join (pt_mi, NULL); (void) pthread_join (pt_hi, NULL); assert (sem_destroy (&sem_mi) == 0); assert (sem_destroy (&sem_hi) == 0); return 0; } |
| Листинг 6.7. Пример программы, моделирующей ситуацию инверсии приоритетов. |
| Закрыть окно |
| Поток со средним приоритетом перед захватом семафора Поток с высоким приоритетом перед захватом семафора Низкий приоритет: 224 Средний приоритет: 239 Высокий приоритет: 254 Поток с низким приоритетом перед освобождением семафора для потока со средним приоритетом Поток со средним приоритетом перед освобождением семафора Поток с низким приоритетом перед освобождением семафора для потока с высоким приоритетом Поток с высоким приоритетом после захвата семафора |
| Листинг 6.8. Возможные результаты выполнения программы, моделирующей ситуацию инверсии приоритетов, под управлением операционной системы реального времени oc2000. |
| Закрыть окно |
Функции асинхронного ввода/вывода
Основными операциями асинхронного ввода/вывода являются чтение и запись данных (см. листинг 7.1).
#include
Листинг 7.1. Описание функций асинхронного чтения и записи. (html, txt)
Функция aio_read() инициирует запрос на чтение aiocbp->aio_nbytes байт из файла, ассоциированного с дескриптором aiocbp->aio_fildes, начиная с абсолютной позиции aiocbp->aio_offset, в буфер с адресом aiocbp->aio_buf. Возврат из функции произойдет тогда, когда запрос будет принят к исполнению или поставлен в очередь; при этом нормальный результат равен нулю, а значение -1 (в совокупности с errno) свидетельствует об ошибке (например, исчерпаны ресурсы и запрос не удалось поставить в очередь).
Функция aio_write() осуществляет соответствующие действия для записи данных. Если для файлового дескриптора aiocbp->aio_fildes установлен флаг O_APPEND, запись будет производиться в конец файла.
Функция lio_listio() (см. листинг 7.2) позволяет за один вызов инициировать (поставить в очередь) список запросов на чтение и/или запись данных.
#include
Листинг 7.2. Описание функции lio_listio(). (html, txt)
Элементы массива listio [] специфицируют отдельные запросы. В полях aio_lio_opcode указуемых структур типа aiocb могут располагаться значения LIO_READ (запрос на чтение), LIO_WRITE (запрос на запись) или LIO_NOP (пустой запрос). Остальные элементы указуемых структур интерпретируются в соответствии со смыслом запроса (можно считать, что адреса структур передаются в качестве аргументов функциям aio_read() или aio_write()). Число элементов в массиве listio [] задается аргументом nent.
Значение аргумента mode определяет способ обработки списка запросов – синхронный (LIO_WAIT) или асинхронный (LIO_NOWAIT). В первом случае возврат из вызова lio_listio() произойдет только после завершения всех заказанных операций ввода/вывода; при этом аргумент sigev игнорируется.
Во втором случае возврат из lio_listio() произойдет немедленно, а по завершении всех операций ввода/вывода в соответствии со значением аргумента sigev будет сгенерировано асинхронное уведомление.
Нормальный результат выполнения функции lio_listio() равен нулю, но для двух описанных случаев он имеет разный смысл – успешное завершение всех операций ввода/вывода или только успешная постановка запросов в очередь соответственно. Во втором случае часть операций ввода/вывода может завершиться неудачей, и с каждой из них придется разбираться индивидуально. Подобное разбирательство позволяют осуществить функции aio_return() и aio_error() (см. листинг 7.3).
#include
ssize_t aio_return ( struct aiocb *aiocbp);
int aio_error ( const struct aiocb *aiocbp);
Листинг 7.3. Описание функций aio_return() и aio_error(). (html, txt)
Аргументом этих функций служит адрес управляющего блока, который играет в данном случае роль идентификатора запроса. Функция aio_return() выдает значение, которое вернула бы соответствующая функция обычного ввода/вывода (если операция ввода/вывода в момент опроса не была завершена, возвращаемое значение, разумеется, не определено). К идентификатору данного запроса функцию aio_return() можно применить ровно один раз; последующие вызовы aio_return() и aio_error() могут завершиться неудачей. (Конечно, если в той же указуемой структуре типа aiocb сформировать новый запрос и выполнить его, возможность корректного вызова функций aio_return() и aio_error() появляется снова.)
Результатом вызова aio_error() служит значение, которое установила бы в переменную errno соответствующая функция обычного ввода/вывода (или, если операция еще не завершена, результат равен EINPROGRESS). Если операция асинхронного ввода/вывода завершилась успешно, aio_error() вернет нулевой результат. Функцию aio_error() к идентификатору данного запроса можно применять произвольное число раз, и до завершения его выполнения, и после.
Ожидающие обработки запросы на асинхронный ввод/вывод можно аннулировать, воспользовавшись функцией aio_cancel() (см.
листинг 7.4).
#include
Листинг 7.4. Описание функции aio_cancel(). (html, txt)
Функция aio_cancel() пытается аннулировать один ( если значение аргумента aiocbp отлично от NULL) или все ожидающие обработки запросы, в которых фигурирует файловый дескриптор fildes. После аннулирования генерируется соответствующее асинхронное уведомление, статус ошибки устанавливается равным ECANCELED, а в качестве возвращаемого значения выдается -1.
Если запрос не удается аннулировать (возможные причины этого, вообще говоря, зависят от реализации), он выполняется стандартным образом.
Результат вызова aio_cancel() равен AIO_CANCELED, если все указанные запросы аннулированы. Если хотя бы один из запросов уже выполняется, он не может быть аннулирован, и тогда в качестве результата возвращается значение AIO_NOTCANCELED (а со статусом запросов, как и для функции lio_listio(), нужно разбираться индивидуально, пользуясь функцией aio_error()). Результат AIO_ALLDONE свидетельствует о том, что выполнение всех указанных запросов не только начато, но и уже завершено. Во всех остальных случаях результат вызова aio_cancel() равен -1.
Какой именно, как обычно, нужно выяснять индивидуально, пользуясь функциями aio_error() и aio_return().
Описываемая далее группа функций (см. листинг 7.6) предназначена для согласования состояния буферизованных и хранимых в долговременной памяти данных, обеспечения целостности данных и файлов. Подобные функции необходимы в системах обработки транзакций, чтобы гарантировать определенное состояние долговременной памяти, даже если система аварийно завершит работу. Один элемент из этой группы – функцию msync() – мы уже рассматривали.
#include
#include
#include
#include
Листинг 7.6. Описание функций синхронизации оперативной и долговременной памяти.
Функция sync() имеет глобальный характер – она планирует запись в файловые системы всех измененных данных.
Функция fsync() обеспечивает запись измененных данных в файл, заданный дескриптором fildes. Более того, после успешного возврата из функции (с нулевым результатом) окажутся выполненными все стоявшие в очереди к этому файлу запросы на ввод/вывод с гарантией целостности файла.
Функция fdatasync() аналогична fsync(), но гарантируется лишь целостность данных (см. курс [1]).
Функция aio_fsync() осуществляет "асинхронную синхронизацию" файла. Иными словами, порождается запрос, выполнение которого при значении аргумента op, равном O_DSYNC, эквивалентно вызову fdatasync (aiocbp->aio_fildes), а при op, равном O_SYNC, – fsync (aiocbp->aio_fildes). Как и для других операций асинхронного ввода/вывода, адрес управляющего блока может использоваться в качестве аргумента функций aio_error() и aio_return(), а после завершения генерируется асинхронное уведомление в соответствии со значением элемента aiocbp->aio_sigevent. Все остальные поля указуемой структуры типа aiocb игнорируются.
Проиллюстрируем использование функций асинхронного ввода/вывода программой, подсчитывающей суммарное число строк в совокупности файлов (см.
листинг 7.7).
/* * * * * * * * * * * * * * * * * * */ /* Программа подсчитывает суммарное */ /* число строк – в файлах аргументах */ /* командной строки. */ /* Если аргументы отсутствуют или в */ /* качестве имени задан минус, */ /* читается стандартный ввод. */ /* * * * * * * * * * * * * * * * * * */
#include
int main (int argc, char *argv []) { /* Указатель на массив */ /* указателей на управляющие */ /* блоки запросов операций */ /* асинхронного чтения */ struct aiocb **lio_ptr; // Общее число строк в файлах long nlines = 0; // Признак повторного указания // стандартного ввода int dup_stdin = 0; int i;
/* Сведем случай с отсутствием */ /* аргументов к общему, */ /* воспользовавшись одним из */ /* немногих стандартизованных */ /* файлов */ if (argc == 1) { argv [0] = "-"; } else { argv [0] = "/dev/null"; }
/* Зарезервируем память, */ /* откроем заданные файлы */ /* и сформируем начальный список */ /* запросов на чтение */ assert ((lio_ptr = (struct aiocb **) malloc (sizeof (struct aiocb *) * argc)) != NULL);
for (i = 0; i < argc; i++) { assert ((lio_ptr [i] = (struct aiocb *) malloc (sizeof (struct aiocb))) != NULL);
if (strcmp (argv [i], "-") == 0) { if (dup_stdin == 0) { lio_ptr [i]->aio_fildes = STDIN_FILENO; dup_stdin = 1; } else { lio_ptr [i]->aio_fildes = fcntl (STDIN_FILENO, F_DUPFD, 0); } } else if ((lio_ptr [i]->aio_fildes = open (argv [i], O_RDONLY)) == -1) { perror ("OPEN"); free (lio_ptr [i]); lio_ptr [i] = NULL; continue; }
lio_ptr [i]->aio_offset = 0; assert ((lio_ptr [i]->aio_buf = malloc(BUFSIZ)) != NULL); lio_ptr [i]->aio_nbytes = BUFSIZ; lio_ptr [i]->aio_reqprio = 0; lio_ptr [i]->aio_sigevent.sigev_notify = SIGEV_NONE; lio_ptr [i]->aio_lio_opcode = LIO_READ; } /* for (i < argc) */
/* Поехали ... */ assert (lio_listio (LIO_NOWAIT, lio_ptr, argc, &lio_ptr [0]->aio_sigevent) == 0);
/* Будем ждать завершения */ /* операций ввода/вывода, */ /* обрабатывать прочитанные */ /* данные и */ /* инициировать новые запросы */ /* на чтение */ while (aio_suspend (( const struct aiocb **) lio_ptr, argc, NULL) == 0) { /* Выясним, какой запрос */ /* и как выполнен */
/* Число недочитанных файлов */ int nreqs = 0;
for (i = 0; i < argc; i++) { if (lio_ptr [i] == NULL) { continue; }
/* Есть обслуживаемые */ /* запросы */ nreqs++;
if (aio_error (lio_ptr [i]) == EINPROGRESS) { continue; } { // Запрос выполнен
// Число прочитанных байт ssize_t nb;
if ((nb = aio_return (lio_ptr [i])) <= 0) { /* Дошли до конца файла*/ /* или чтение */ /* завершилось ошибкой */ (void) close(lio_ptr [i]->aio_fildes); free ((void *) lio_ptr [i]->aio_buf); free (lio_ptr [i]); lio_ptr [i] = NULL; nreqs--; } else { /* Обработаем прочитанные */ /* данные */
/* Текущее начало поиска */ /* перевода строки */ char *p; /* Позиция, где нашли */ /* перевод строки */ char *p1; p = (char *) lio_ptr [i]->aio_buf; while ((p1 = (char *) memchr (p, '\n', nb - (p – (char *) lio_ptr [i]->aio_buf))) != NULL) { nlines++; p = p1 + 1; }
/* Инициируем новый */ /* запрос на чтение */ lio_ptr [i]->aio_offset += nb; (void) aio_read (lio_ptr [i]); } } } /* for (i < argc) */
/* Остались недочитанные */ /* файлы? */ if (nreqs == 0) { break; } } /* while */
printf ("%ld\n", nlines);
return 0; }
Листинг 7.7. Пример программы, использующей функции асинхронного ввода/вывода.
Отметим списочную форму начального представления и ожидания выполнения запросов на асинхронное чтение. Из технических деталей обратим внимание на пустой указатель в качестве элемента массива указателей на структуры типа aiocb (функция lio_listio() игнорирует такие элементы) и в качестве аргумента timeout функции aio_suspend() (означает бесконечное ожидание).
Второй вариант решения той же задачи (см.
листинг 7.8) демонстрирует другой способ уведомления о завершении операции асинхронного ввода/вывода – вызов функции.
/* * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа подсчитывает суммарное число строк */ /* в файлах – аргументах командной строки. */ /* Если аргументы отсутствуют или в качестве имени */ /* задан минус, читается стандартный ввод. */ /* * * * * * * * * * * * * * * * * * * * * * * * * * */
#include