© 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] не осуществляется.