© Copyright 2006 Грибов Игорь, e-mail: gribov@depni.sinp.msu.ru
Изменения в версиях. Свойства регистратора. Программная реализация регистратора.
Полная версия раздела задается в виде: версия.подверсия.выпуск.
Номер версии увеличивается при появлении глав, затрагивающих не рассмотренные ранее вопросы. Названия этих глав выносятся в оглавление раздела. Подверсия увеличивается, когда в существующие главы добавляются новые абзацы, рисунки, либо значительно перерабатывается имеющийся материал, существенно изменяя содержание главы. А при внесении лишь незначительных правок в существующие главы увеличивается номер выпуска.
Подсистема, осуществляющая регистрацию сообщений о различных событиях, ошибках и статусах должна стремиться поддерживать следующие свойства.
Возможность асинхронной регистрации. Предполагается, что требующие регистрации события, ошибки и статусы могут возникать в любом сегменте программы, например, в ходе обработки аппаратного прерывания или сигнала.
Буферизация зарегистрированных сообщений. Регистратор должен обладать некоторым собственным буфером типа FIFO, который используется для хранения сообщений о последних событиях. Желательно задавать размер этого буфера конфигурационными средствами.
Переправка накопленных в буфере регистратора сообщений в долговременное хранилище: систему файлов журнала, сеть и т.п.
Обработка собственных ошибок. Так, нужно обеспечить регистрацию факта невозможности записи нового сообщения. А при переполнении FIFO следует осуществлять его подчистку (удаление самых старых сообщений) с регистрацией соответствующей ошибки.
На рисунке приведена схема подсистемы регистратора, отвечающая за асинхронное занесение сообщений в кольцевой буфер (FIFO). Для ее реализации создается линейный набор (кэш) буферов, обеспечивающих оперативное хранение данных. Доступ к каждому буферу кэша защищен отдельным семафором. Первый буфер (см. рисунок) является выделенным и не используется для записи внешних сообщений. В него заносится на постоянное хранение сообщение о переполнении кэша. При возникновении такого события семафор первого буфера открывается, регистрируя факт переполнения. Непосредственно после записи сообщения в кэш предпринимается попытка пересылки всех накопленных данных в кольцевой буфер. Поскольку операция заполнения FIFO инициируется асинхронно, она защищена семафором, а значит запись данных в кольцевой буфер может оказаться безуспешной. В связи с этим осуществляется периодическое (по таймеру) сканирование кэша и до-вывод оставшихся в нем сообщений. В противном случае данные могли бы застрять в буфере на неопределенное время - как минимум до записи очередного сообщения.
Помимо подсистемы записи сообщений регистратор содержит некоторый модуль, осуществляющий извлечение и переправку накопленной в FIFO информации в журнал, сеть и т.п. В главе о программной реализации регистратора в качестве примера приведен алгоритм записи сообщений в систему файлов журнала с ежечасным созданием новых файлов.
На следующем рисунке показана схема подчистки кольцевого буфера при его заполнении. Когда голова буфера H упирается в его хвост T последний принудительно смещается, освобождая тем самым некоторое число элементов FIFO. При этом самые старые сообщения будут утеряны, но благодаря наличию кэша обеспечивается регистрация самого факта переполнения кольцевого буфера.
Приведенный в этом разделе вариант реализации регистратора требует наличия операционной системы. Регистратор обращается к ней с тремя запросами: на выделение динамический памяти, при работе с файлами и с запросом системного времени. В качестве сообщения используется структура, содержащая численный код события (статус) и некоторое текстовое сообщение. Все перечисленные особенности регистратора могут быть легко адаптированы к задачам, не предусматривающим использование операционной системы, либо требующим иного формата сообщений.
#include <string.h> #include <stdio.h> #include <stdlib.h> #include <time.h>
Программа регистратора использует функции стандартных библиотек С. Из <string.h> вызывается функция копирования строк, <stdio.h> поставляет функции ввода-вывода на терминал и в файл, <stdlib.h> содержит функции выделения динамической памяти, а для работы с функциями времени подключается <time.h>.
#define CACHE_SIZE 8 // Число буферов в кэше (не менее 2) #define PMC_STATUSBUF_MIN 10 // Min 5 #define PMC_STATUSBUF_MAX 60000 // Max 64883 #define ERROR_CONFIG -20 // Ошибка конфигурации #define ERROR_STATUSCACHE -12 // Ошибка переполнения кэша #define ERROR_STATUSFIFO -11 // Ошибка переполнения FIFO #define ERROR_MALLOC -10 // Ошибка выделения памяти для FIFO #define ERROR -1 #define OK 1 #define STR_FILE_NAME_SIZE 256 // Максимальная длина имени файла (255 и '\0') #define STR_STATMES_SIZE 60 // Максимальная длина сообщения (59 и '\0') #define STR_TS_SIZE 20 // Длина строки временной метки (19 и '\0') typedef char int8; typedef unsigned char unsigned8; typedef short int16; typedef unsigned short unsigned16; typedef int int32; typedef unsigned int unsigned32; typedef struct { time_t ts; int16 status; char message[STR_STATMES_SIZE]; } status; // Структура сообщения
Структура сообщения регистратора содержит три записи: временную метку события, его численный код (статус) и дополнительное текстовое сообщение. Данная структура может быть преобразована к любому виду, потребному конкретной реализации регистратора.
typedef struct { short busy; status st; } statuscache; // Структура данных буфера, дополнена семафором busy statuscache status_cache[CACHE_SIZE]; // Кэш-буфер status *statbase; // Указатель начала кольцевого буфера (FIFO) unsigned16 bufsize_status; // Конфигурируемый размер FIFO int16 sem_statsen, sem_cachefl; // Семафоры отправки данных из FIFO и записи из кэша в FIFO unsigned16 head_st, tail_st; // Голова и хвост кольцевого буфера int logs_hour; // Время в часах. Используется для управления файлами журнала FILE *filestatus; // Файл журнала для записи сообщений из FIFO char argv_file_name[STR_FILE_NAME_SIZE]; // Имя файла программы, полученное от операционной системы. // Используется при формировании имени файла журнала. void write_status(int16 status, char *mess); void send_status(status *st) // sn_0 { char ts[STR_TS_SIZE]; char stat[STR_STATMES_SIZE]; strftime(ts, STR_TS_SIZE, "%d-%m-%Y %H:%M:%S", localtime(&st->ts)); // sn_5 if (st->status == ERROR_MALLOC) { // sn_6 sprintf(stat, "Memory allocation error"); } else if (st->status == ERROR_STATUSFIFO) { sprintf(stat, "Status FIFO overflow"); } else if (st->status == ERROR_STATUSCACHE) { sprintf(stat, "Status cache overflow"); } else if (st->status == ERROR_CONFIG) { sprintf(stat, "Configuration error"); } else sprintf(stat, "Error/Status %4i", st->status); // sn_14 if (filestatus != NULL) { // sn_15 fprintf(filestatus, "%s %s %s\n", ts, stat, st->message); // sn_16 } else { printf("%s %s %s\n", ts, stat, st->message); // sn_18 } }
Функция send_status(st) выполняет отправку сообщения регистратора. Поскольку окончательным местом назначения является файл журнала, функция преобразует сообщение в текстовый вид. Его временная метка записывается в формате DD-MM-YYYY HH:MM:SS [sn_5]. В строках [sn_6...sn_14] раскрывается код сообщения. И, наконец, сообщение заносится в файл журнала [sn_16], если таковой был открыт [sn_15], либо – при отсутствии файла – выводится на терминал [sn_18].
void flush_status_cache(void) // fs_0 { unsigned16 head, cnt; sem_cachefl++; // fs_4 if (sem_cachefl != 0) { // fs_5 sem_cachefl--; // fs_6 return; } for (cnt = 0; cnt < CACHE_SIZE; cnt++) { // fs_9 if (status_cache[cnt].busy < 0) continue; // fs_10 head = head_st+1; // fs_11 if (head == bufsize_status) head = 0; // fs_12 sem_statsen++; // fs_13 if (head == tail_st) { // fs_14 write_status(ERROR_STATUSFIFO, "flush_status_cache()"); // fs_15 if (sem_statsen != 0) { // fs_16 sem_statsen--; // fs_17 sem_cachefl--; // fs_18 return; // fs_19 } tail_st += 4 + bufsize_status/100; // fs_21 if (tail_st >= bufsize_status) tail_st -= bufsize_status; // fs_22 } sem_statsen--; // fs_24 memcpy(statbase+head, &status_cache[cnt].st, sizeof(status)); // fs_25 head_st = head; // fs_26 status_cache[cnt].busy = -1; // fs_27 } sem_cachefl--; // fs_29 }
Функция flush_status_cache() переписывает накопленные в кэше сообщения в кольцевой буфер. Она работает с разделяемыми ресурсами (голова [fs_11] и хвост [fs_21] кольцевого буфера), повторное обращение к которым не допустимо. Безусловная сигналобезопасность функции поддерживается блокирующим семафором sem_cachefl [fs_4, fs_6, fs_18, fs_29], который также используется для закрытия доступа к кэшу на время выполнения критической секции записи данных в write_status(status, mess). При выводе сообщений в FIFO производится линейный просмотр кэша (цикл [fs_9]) с пропуском не занятых буферов [fs_10]. Для заполненных кэш-буферов осуществляется продвижение головы FIFO [fs_11, fs_12] и, если имеется свободное место, производится копирование содержимого кэша по адресу головы кольцевого буфера [fs_25]. Затем устанавливается новое значение адреса головы head_st [fs_26] и открывается семафор занятости кэш-буфера [fs_27].
Операторы [fs_13...fs_24] осуществляют подчистку кольцевого буфера в случае, если он оказался полон, то есть когда голова буфера уперлась в его хвост [fs_14]. Для осуществления этой операции нужно прежде всего заблокировать доступ к хвосту буфера, для чего используется семафор извлечения данных из FIFO sem_statsen, который запрещает вывод накопленных в буфере сообщений [fs_13]. Сам факт подчистки регистрируется путем записи статуса [fs_15], который будет занесен в кэш функцией write_status(status, mess). Здесь и далее в качестве дополнительного текстового сообщения используется название функции, в которой регистрируется статус. Оператор [fs_16] осуществляет проверку того, что подчистка кольцевого буфера не была активирована во время вывода данных из FIFO, то есть при осуществлении манипуляций с хвостом буфера. Если такая ситуация имеет место, выполнение flush_status_cache() прекращается [fs_19], предоставляя возможность функции вывода show_status() завершить свою работу. Операторы [fs_21, fs_22] выполняют собственно подчистку FIFO. Величина продвижения хвоста определяется линейной формулой: один процент от размера кольцевого буфера плюс 4 записи. Именно такая формула обуславливает предельные значения минимального и максимального размеров буфера (5 – при подчистке теряются все сообщения FIFO, 64883 – значение в [fs_21] не превышает 65535 при использовании в качестве головы и хвоста буфера переменных типа unsigned16). Отметим еще раз, что при выполнении любых манипуляций с подчисткой FIFO сообщения не теряются, а продолжают регистрироваться в кэше.
void write_status(int16 status, char *mess) // ws_0 { unsigned16 cnt; sem_cachefl++; // ws_4 for (cnt = 1; cnt < CACHE_SIZE; cnt++) { // ws_5 status_cache[cnt].busy++; // ws_6 if (status_cache[cnt].busy == 0) { // ws_7 status_cache[cnt].st.ts = time(NULL); // ws_8 status_cache[cnt].st.status = status; // ws_9 strncpy(status_cache[cnt].st.message, mess, STR_STATMES_SIZE); // ws_10 status_cache[cnt].st.message[STR_STATMES_SIZE-1] = '\0'; // ws_11 sem_cachefl--; // ws_12 if (statbase == NULL) head_st++; // ws_13 else flush_status_cache(); // ws_14 return; } status_cache[cnt].busy--; // ws_17 } status_cache[0].st.ts = time(NULL); // ws_18 status_cache[0].busy = 0; // ws_19 sem_cachefl--; // ws_20 if (statbase == NULL) head_st++; // ws_21 else flush_status_cache(); // ws_22 }
Повторно-входимая функция записи сообщений (статусов) write_status(status, *mess) заносит код события и текстовое сообщение в кэш-буфер, дополняя их временной меткой [ws_8]. При этом полагается, что допустим повторно-входимый запрос текущего времени time(NULL). Эта функция выполняет роль прикладного интерфейса (API) асинхронного регистратора.
Доступ к каждому буферу осуществляется сигналобезопасно с использованием семафора [ws_6, ws_7, ws_17]. Функция просматривает кэш, начиная с первого буфера [ws_5], поскольку нулевой используется для индикации переполнения самого кэша. При обнаружении свободного буфера [ws_7] он заполняется данными [ws_8...ws_11], причем текстовое сообщение дополняется завершающим нулем [ws_11], поскольку длина mess может превышать STR_STATMES_SIZE. После записи данных в кэш выполняемая операция зависит от того, существует ли кольцевой буфер или нет. Если он еще не определен (указатель начала FIFO равен NULL), то инкрементируется значение головы буфера [ws_13], которое в данном случае используется лишь для определения числа зарегистрированных сообщений. Этим обеспечивается возможность регистрации событий, возникающих до либо в процессе выделения памяти для FIFO. Так, если при запросе памяти для кольцевого буфера возникает ошибка, она также будет зафиксирована в кэше - см. далее функцию allocate_status_buffer() [ab_3, ab_4]. Если же FIFO уже существует, осуществляется попытка немедленного вывода данных из кэша [ws_14]. В строках [ws_18...ws_22] обрабатывается ситуация переполнения самого кэша. Для этого в нулевой буфер при его инициализации заносится на постоянное хранение статус ошибки переполнения. А при ее возникновении осуществляется лишь инициализация нулевого кэша: установ временной метки [ws_18] и семафора буфера [ws_19]. Затем, аналогично операторам [ws_13, ws_14] осуществляется либо инкремент головы буфера [ws_21], либо вывод данных из кэша [ws_22].
Обратите внимание на использование семафора блокирования вывода данных sem_cachefl [ws_4, ws_12, ws_20]. Поскольку вызов функции переноса данных из кэша в FIFO может осуществляться асинхронно по отношению к функции записи сообщений, нужно предотвратить некорректное использование разделяемых ресурсов. Так, после выполнения попытки захвата буфера [ws_6] и до фактической записи данных [ws_11] этот буфер не может считаться готовым для вывода, не взирая на значение флага busy, соответствующее заполненному буферу. Кроме того, если вызов flush_status_cache() произойдет непосредственно до семафорной операции [ws_17], значение семафора станет меньше минус 1 и данный буфер будет постоянно блокирован.
unsigned16 nof_status(void) // ns_0 { int32 ht; ht = head_st - tail_st; // ns_4 if (ht < 0) ht += bufsize_status; // ns_5 return ht; }
Функция nof_status() возвращает число заполненных элементов кольцевого буфера (зарегистрированных сообщений). Для обеспечения сигналобезопасности относительно записи и чтения FIFO разность головы и хвоста буфера присваивается локальной переменной со знаком [ns_4]. Если же использовать в операторе [ns_5] переменные head_st и tail_st непосредственно, можно получить совершенно неверный результат, если запись или чтение буфера произойдут после проверки условия в [ns_5] и совпадут с закольцовыванием FIFO.
void transform_file_name(char *filename, char *initfn) // tn_0 { unsigned16 fnp, cnt; strncpy(filename, argv_file_name, STR_FILE_NAME_SIZE); fnp = STR_FILE_NAME_SIZE-1; // tn_5 while (fnp > 0) { // tn_6 fnp--; if (filename[fnp] == '\\') { fnp++; break; } } cnt = 0; while (fnp < STR_FILE_NAME_SIZE) { // tn_14 filename[fnp] = initfn[cnt]; if (filename[fnp] == '\0') break; fnp++; cnt++; } filename[STR_FILE_NAME_SIZE-1] = '\0'; }
Вспомогательная функция transform_file_name(filename, initfn) преобразует имя файла initfn так, чтобы учесть размещение программы регистратора. Полное имя запускаемой программы – включая путь ее размещения – передается операционной системой в нулевом аргументе вектора параметров, который сохраняется в argv_file_name (см. ниже [mn_2]). Из путевого имени удаляется название самой программы (цикл [tn_6]) и дописывается имя файла initfn (цикл [tn_14]). Таким образом, полученное в результате имя filename оказывается фиксированным относительно места расположения самой программы не зависимо от директории и метода ее запуска.
void log_status_file(void) // lf_0 { time_t ts; struct tm tp; char fn[STR_TS_SIZE+10]; char file_name[STR_FILE_NAME_SIZE]; ts = time(NULL); tp = *localtime(&ts); if (logs_hour != tp.tm_hour || filestatus == NULL) { // lf_9 if (filestatus != NULL) fclose(filestatus); sprintf(fn, "Log\\status"); // lf_11 strftime(fn+10, STR_TS_SIZE, "_%Y%m%d_%H", &tp); // lf_12 transform_file_name(file_name, fn); // lf_13 filestatus = fopen(file_name, "w"); logs_hour = tp.tm_hour; } }
Вспомогательная функция log_status_file() осуществляет начальное, а затем ежечасное формирование новых файлов журнала регистратора [lf_9]. Файлы с именами status_YYYYMMDD_HH размещаются в поддиректории Log [lf_11, lf_12] относительно директории размещения программы регистратора [lf_13].
void show_cache(void) // sc_0 { unsigned16 cnt; for (cnt = 0; cnt < CACHE_SIZE; cnt++) { if (status_cache[cnt].busy >= 0) { send_status(&status_cache[cnt].st); // sc_6 status_cache[cnt].busy = -1; // sc_7 head_st--; // sc_8 } } }
Функция show_cache() выполняет отправку сообщений [sc_6], накопленных в кэше регистратора. После отправки сообщения открывается семафор занятости буфера [sc_7] и декрементируется значение головы FIFO [sc_8], используемое в данном случае только для определения числа сообщений в кэше.
void show_status(void) // ss_0 { unsigned16 tail; log_status_file(); // ss_4 if (nof_status() == 0) return; // ss_5 if (statbase == NULL) { // ss_6 show_cache(); // ss_7 return; } flush_status_cache(); // ss_10 sem_statsen++; // ss_11 if (sem_statsen == 0) { // ss_12 while (tail_st != head_st) { // ss_13 tail = tail_st+1; if (tail == bufsize_status) tail = 0; send_status(statbase+tail); // ss_16 tail_st = tail; } } sem_statsen--; // ss_20 }
Функция show_status() занимается отправкой сообщений из кэша и кольцевого буфера, если последний существует. Асинхронное обращение к этой функции не допускается, поскольку она взаимодействует с операционной системой – осуществляет открытие, закрытие и запись в файл. В нашем примере периодический вызов show_status() включен в мониторный цикл программы [mr_2]. Функция обращается с запросом формирования файлов журнала [ss_4] даже при отсутствии зарегистрированных сообщений [ss_5]. При этом появляются файлы нулевого размера, если в течение часа не фиксируется ни одного сообщения. Если операторы [ss_4] и [ss_5] поменять местами, новые файлы журнала будут формироваться лишь при наличии сообщений в FIFO. При этом, однако, следует внести изменения и в функцию log_status_file(), чтобы учитывать не только смену часа, но и дня, дабы сообщения, разделенные сутками, не попали в один и тот же файл.
Когда кольцевой буфер еще не определен и указатель начала FIFO равен NULL [ss_6], производится отправка сообщений из кэша регистратора [ss_7]. Если же FIFO существует, то, переписав в кольцевой буфер данные из кэша [ss_10], функция осуществляет вывод всех накопленных сообщений [ss_13]. Семафор извлечения данных из FIFO sem_statsen [ss_11, ss_12, ss_20] используется не для поддержки сигналобезопасности доступа к хвосту буфера, но для его запрета в момент подчистки FIFO функцией flush_status_cache() [fs_13, fs_24]. Однако, этот семафор оказался бы полезен и в случае реализации асинхронной отправки данных - доступ к хвосту буфера также должен осуществляться сигналобезопасно.
void allocate_status_buffer(void) // ab_0 { statbase = malloc(bufsize_status*sizeof(status)); // ab_2 if (statbase == NULL) { // ab_3 write_status(ERROR_MALLOC, "allocate_status_buffer()"); // ab_4 return; } head_st = 0; // ab_7 tail_st = 0; // ab_8 sem_statsen = -1; // ab_9 sem_cachefl = -1; // ab_10 }
Функция allocate_status_buffer() обеспечивает выделение памяти для размещения кольцевого буфера. Сегмент памяти нужного размера запрашивается у операционной системы [ab_2], причем сам размер буфера определяется переменной bufsize_status и может быть задан конфигурационными средствами. В случае успешного создания FIFO осуществляется его инициализация [ab_7], ([ab_8] - не обязательно) и открываются семафоры отправки данных из FIFO [ab_9] и записи из кэша в FIFO [ab_10].
void init_statusproc(void) // is_0 { unsigned16 cnt; logs_hour = 0; sem_statsen = 0; // is_5 sem_cachefl = 0; // is_6 head_st = 0; tail_st = 0; statbase = NULL; // is_9 for (cnt = 0; cnt < CACHE_SIZE; cnt++) { // is_10 memset(&status_cache[cnt], 0, sizeof(statuscache)); status_cache[cnt].busy = -1; } status_cache[0].st.status = ERROR_STATUSCACHE; // is_14 sprintf(status_cache[0].st.message, "write_status()"); // is_15 }
Функция init_statusproс() производит начальную инициализацию данных регистратора. Семафоры, используемые при работе с FIFO, устанавливаются в закрытое состояние [is_5, is_6], инициализируется указатель начала кольцевого буфера [is_9]. Таким образом, до фактического выделения памяти для FIFO возможность обращения к нему в функции flush_status_cache() [fs_25] будет невозможна. Затем осуществляется инициализация кэша (цикл [is_10]). Операторы [is_14, is_15] заносят в нулевой кэш-буфер статус ошибки переполнения. При ее возникновении будет открыт семафор нулевого кэша [ws_19], позволяя зарегистрировать саму ситуацию переполнения.
void periodic(void) // pr_0 { flush_status_cache(); }
Функция periodic() активируется периодическим таймером, инициируя пересылку накопленных в кэше сообщений в кольцевой буфер. В данном примере ее использование не обязательно, поскольку эта операция регулярно выполняется функцией отправки сообщений show_status(), вызываемой из монитора регистратора [mr_2]. Однако, следует подчеркнуть саму необходимость такого обращения к функции пересылки, причем с гарантированной периодичностью. Ведь асинхронный вызов flush_status_cache() может завершиться безуспешно, а повторное сканирование кэша в ней не предусмотрено. В результате сообщения, вновь записанные в начальные элементы кэш-буфера, не будут переданы в FIFO. Поскольку flush_status_cache() является безусловно сигналобезопасной, допустим ее асинхронный вызов, в том числе по сигналу или прерыванию таймера.
void monitor(void) // mr_0 { show_status(); // mr_2 }
Функция show_status(), отправляющая сообщения из кэша и кольцевого буфера, взаимодействует с операционной системой – осуществляет открытие, закрытие и запись в файл. Поэтому ее периодический вызов осуществляется из монитора программы [mr_2].
void read_config(void) { bufsize_status = 1234; if (bufsize_status < PMC_STATUSBUF_MIN || bufsize_status > PMC_STATUSBUF_MAX) { write_status(ERROR_CONFIG, "read_config()"); } }
Функция read_config() считывает конфигурационную информацию. Здесь задается размер кольцевого буфера bufsize_status. Конфигурационная информация обычно хранится в файлах соответствующего формата.
void init_all(void) { init_statusproc(); read_config(); if (nof_status() != 0) return; allocate_status_buffer(); }
Функция init_all() осуществляет общую инициализацию приложения регистратора. Обратите внимание на вызов nof_status(), возвращающей число зарегистрированных сообщений, в качестве индикатора возникновения ошибок.
int main(int argc, char **argv) // mn_0 { strncpy(argv_file_name, argv[0], STR_FILE_NAME_SIZE); // mn_2 filestatus = NULL; init_all(); // mn_4 if (nof_status() == 0) { // mn_5 while (1) monitor(); // mn_6 } show_status(); if (filestatus != NULL) fclose(filestatus); return 1; }
В функции main() сохраняется полное имя запускаемой программы [mn_2], включающее путь ее фактического размещения. Это дает возможность формирования имен файлов относительно директории расположения программы не зависимо от способа ее запуска. Вызов init_all() [mn_4] производит общую инициализацию приложения регистратора. В качестве индикатора ошибок здесь также используется функция nof_status() [mn_5]. При наличии таковых программа прекращает работу – вход в ее монитор [mn_6] не осуществляется.