Олег Цилюрик - QNX/UNIX: Анатомия параллелизма
// таймера, после чего он фактически и запускается
struct itimerspec itime;
nsec2timespec(&itime.it_value, millisec * 1000000ull);
itime it_interval = itime.it_value;
if (!(ok = (timer_settime(timer, 0, &itime, NULL) == 0))) return;
}
// признак того, что объект создан успешно и его поток запущен:
bool OK(void) { return ok; }
bool statistic(void) { return st; }
};
int thrblock.code = _PULSE_CODE_MINAVAIL;
// функция потока объекта
void* syncthread(void *block) {
thrblock *p = (thrblock*)block;
struct _pulse buf;
pthread_attr_t attr;
while(true) {
// ожидание пульса от периодического таймера объекта
MsgReceivePulse(p->chid, &buf, sizeof(struct _pulse), NULL);
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// восстановить приоритет целевой функции до уровня того,
// кто ее устанавливал, вызывая конструктор
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedparam(&attr, &p->param);
// запуск целевой функции в отдельном "отсоединенном" потоке
pthread_create(NULL, &attr, p->func, NULL);
if (p->statistic()) ++p->sync;
}
}
// 'пустой' обработчик сигнала SIGINT (реакция на ^С)
inline static void empty(int signo) {}
int main(int argc, char **argv) {
// с этой точки стандартная реакция на ^С отменяется...
signal(SIGINT, empty);
// массив целевых функций
void(*funcs[])(void) = { &mon1, &mon2, &mon3 };
// периоды их синхросерий запуска
int period[] = { 317, 171, 77 };
// приоритеты, на которых отрабатывается реакция
// синхросерий на каждый из таймеров синхросерий
int priority[] = { 15, 5, 25 };
int num = sizeof(funcs) / sizeof(*funcs);
// запуск 3-х синхронизированных последовательностей
// выполнения (созданием объектов)
thrblock** tb = new (thrblock*)[num];
for (int i = 0; i < num; i++) {
tb[i] = new thrblock(funcs[i], period[i],
priority[i], true);
if (!tb[i]->OK())
perror("synchro thread create"), exit(EXIT_FAILURE);
}
// ... а теперь ожидаем ^С.
pause();
// подсчет статистики и завершение программы
cout << endl << "Monitoring finalisation!" << endl;
// вывод временных интервалов будем делать в миллисекундах:
const double n2m = 1000000.;
for (int i = 0; i < num, i++) {
timestat *p = &tb[i]->sync;
!(*p); // подсчет статистики по объекту
cout << i << 't' << p->num << "t=> " << p->mean / n2m << " [" <<
p->tmin / n2m << "..." << p->tmax / n2m << "]t~" << p->disp / n2m <<
" (" << p->disp / p->mean * 100 << "%)" << endl;
}
return EXIT_SUCCESS;
}
Вся функциональность программы сосредоточена в одном классе — thrblock, который может в неизменном виде использоваться для разных приложений. Необычной особенностью объекта этого класса является то, что он выполнен в технике «активных объектов», навеянной поверхностным знакомством с языками программирования школы Н. Вирта — ActiveOberon и Zormon. В ней говорится, что конструктор такого объекта не только создает объект данных, но и запускает (как вариант) отдельный поток выполнения для каждого создаваемого объекта. В нашем случае задача потоковой функции состоит в вызове целевой функции, адрес которой был передан конструктору объекта в качестве одного из параметров.[26]
Ниже представлены отличия нашей реализации от простого цикла с задержкой, обсуждавшейся выше (помимо исправлений очевидных недостатков):
• Для каждого синхронизирующего таймера установлен свой приоритет «пробуждения», и он может быть достаточно высоким, для того чтобы предотвратить вытеснение этого синхронизирующего потока.
• После «пробуждения» по таймеру запускается целевая функция, но выполняется это отдельным потоком, причем потоком «отсоединенным». Другими словами, процесс выполнения целевой функции никак не влияет на общую схему синхронизации.
• Перед запуском целевой функции выполняющему ее потоку восстанавливается приоритет породившего потока (но не потока обслуживания таймера!), ведь нам не нужно, чтобы целевая функция, тем более, возможно и не очень значимая, как в нашем примере, могла влиять вытеснением на процессы синхронизации.
Запустим наше тестовое приложение:
# t3
+10+10*10+10+10.10*10+10+10*10+10+10.10*10+10+10+10*10+10.10+10*10+10+10*10+10.10+10*10+10+10*10+10.10+10+10*10+10+10+10.10+10+10*10+10+10.10*10+10+10+10*10+10.10+10*10+10+10*10+10+10.10*10+10+10*10+10+10.10+10*10+10+10*10+10.10+10*10+10+10*10+10.10+10+10*10+10+10*10^C
Monitoring finalisation!
0 32 => 316.919 [316.867...317.895] ~0.178511 (0.056327%)
1 59 => 170.955 [168.583...173.296] ~0.92472 (0.540914%)
2 132 => 76.9796 [76.942...77.9524] ~0.085977 (0.111688%)
Первое, что мы должны отметить, — это очень приличную точность выдержки периода синхронизации (последняя колонка вывода). Для того чтобы убедиться в том, что целевая функция при этом выполняется под приоритетом породившего ее потока, закомментируем строки, выделенные жирным шрифтом в коде программы:
# t3
+25+25*5+25+25.15*5+25+25*5+25+25.15*5+25+25+25*5+25.15+25*5+25+25*5+25.15+25*5+25+25*5*5+25.15+25+25*5+25+25*5.15+25+25*5+25+25.15*5+25+25+25*5+25.15+25*5+25+25*5+25+25.15*5+25+25*5+25+25^C
Monitoring finalisation!
0 32 => 316.919 [316.797...317.915] ~0.185331 (0.0584792%)
1 60 => 170.955 [168.964...173.925] ~0.47915 (0.280279%)
2 34 => 76.9796 [76.8895...77.9694] ~0.0937379 (0.12177%)
В этом варианте (и диагностический вывод это подтверждает) мы искусственно ликвидировали наследование приоритета по цепочке порождения: сработавший таймер — функция потока — целевая функция объекта. Это не совсем соответствует цели, намеченной в начале этого раздела, но все же этот вариант иллюстрирует, что именно наш предыдущий вариант удовлетворял всем поставленным целям.
3. Сигналы
Сигналы инициируются некоторыми событиями в системе и посылаются процессу для его уведомления о том, что произошло нечто неординарное, требующее определенной реакции. Порождающее сигнал событие может быть действием пользователя или может быть вызвано другим процессом или ядром операционной системы. Сигналы являются одним из самых старых и традиционных механизмов UNIX.[27]
Уже из этого краткого описания можно заключить, что:
• действия, вызываемые для обработки сигнала, являются принципиально асинхронными;
• сигналы могут быть использованы как простейшее, но мощное средство межпроцессного взаимодействия.
Все сигналы определяются целочисленными константами, но для программиста это в принципе не так важно, поскольку каждому сигналу приписано символическое наименование вида SIG*. Все относящиеся к сигналам определения находятся в заголовочном файле <signal.h>, который должен быть включен в любой код, работающий с сигналами. В последующих примерах мы не будем показывать эту включающую директиву, чтобы не загромождать текст.
Посланный сигнал обрабатывается получившим его потоком или процессом (как уже обсуждалось ранее, собственно процесс ничего не может обрабатывать: это пассивная субстанция; понятно, что речь в таком контексте идет об обработке сигнала главным потоком процесса). Поток может отреагировать на полученный сигнал следующими способами (иногда действие, установленное для конкретного сигнала, называют диспозицией сигнала):
• Стандартное действие — выполнение действия, предписанного для обработки этого сигнала по умолчанию. Для многих сигналов действием по умолчанию является завершение, но это необязательно; есть сигналы, которые по умолчанию игнорируются. Для большей части сигналов, чьим действием по умолчанию является принудительное завершение процесса, предписано действие создания дампа памяти при завершении (core dump), но для некоторых, например SYSINT (завершение по [Ctrl+C]), определено такой дамп при завершении не создавать.
• Игнорирование сигнала — сигнал не оказывает никакого воздействия на ход выполнения потока получателя.
• Вызов обработчика — по поступлению сигнала вызывается функция реакции, определенная пользователем. Если для сигнала устанавливается функция-обработчик, то говорят, что сигнал перехватывается (относительно стандартного действия).
Для различных сигналов программа может установить различные механизмы обработки. Более того, в ходе выполнения программа может динамически переопределять реакции, установленные для того или иного сигнала.
Для большинства сигналов их воздействие можно перехватить из программного кода или игнорировать, но не для всех. Например, сигналы SIGKILL и SIGSTOP не могут быть ни перехвачены, ни проигнорированы. Другой пример — описываемые далее специальные сигналы QNX, начиная с SIGSELECT и далее.