LINUX.ORG.RU

Обработка сигналов в программе на C

 , , kevent, kqueue


0

2

Всем доброго времени суток.

Имеется программа на Си которая обрабатывает входящие подключения при помощи kqueue и каждое подключение отправляет на обработку в отдельный тред:

int main(int argc, char **argv) {
...

#ifdef __FreeBSD__
    // Create kqueue
    int kq;
    kq = kqueue();
    if (kq < 0) {
        perror("kqueue");
        exit(EXIT_FAILURE);
    }

    // Register server socket with kqueue for read events
    struct kevent ev;
    EV_SET(&ev, listen_sock, EVFILT_READ, EV_ADD, 0, 0, NULL);
    if (kevent(kq, &ev, 1, NULL, 0, NULL) < 0) {
        perror(«kevent»);
        exit(EXIT_FAILURE);
    }

    // Loop for handling events
    while (1) {
        struct kevent event;
        int n = kevent(kq, NULL, 0, &event, 1, NULL);

        if (n < 0) {
            perror(«kevent»);
            exit(EXIT_SUCCESS);
        }
        //int *arg;
        if ((int)event.ident == listen_sock) {
            int client_socket = accept(listen_sock, NULL, NULL);
            if (client_socket < 0) {
                perror(«accept»);
                exit(EXIT_FAILURE);
            }
            int *arg = malloc(sizeof(*arg));
            *arg = client_socket;
            pthread_t tid;
            if (pthread_create(&tid, NULL, handle_client, arg) != 0) {
                perror(«pthread_create»);
                free(arg);
                continue;
            }
            pthread_detach(tid);
        }
    }
#endif                                                                                                                         
}

Программа пишет в лог в отдельный файл. Возникла необходимость сделать ротацию логов. Ротация логов осуществляется стандартно: переименовывается текущий лог и отправляется программе SIGUSR1 чтоб она переоткрыла лог файл. Следовтельно, нужно чтоб программа перехватывала SIGUSR1 и переоткрывала лог файл.
Для этого в main() вешаю обработчик SIGUSR1:

signal(SIGUSR1, sigusr1_handler);
... и он вызывается:

void sigusr1_handler(int signal_num) {
    printf("Got SIGUSR1!\n");
}

В итоге получаю ошибку: kevent: Interrupted system call
Вот трейс:

Thread 1 received signal SIGUSR1, User defined signal 1.
Sent by kill() from pid 39787 and user 1001.
0x000000080105341a in _kevent () from /lib/libc.so.7
(gdb) bt
#0  0x000000080105341a in _kevent () from /lib/libc.so.7
#1  0x0000000800f1cb81 in ?? () from /lib/libthr.so.3
#2  0x00000000002088e9 in main (argc=3, argv=0x7fffffffe460) at main.c:387
(gdb) list main.c:387
382	   }
383	
384	   // Loop for handling events
385	   while (1) {
386	       struct kevent event;
387	       int n = kevent(kq, NULL, 0, &event, 1, NULL);
388	
389	       if (n < 0) {
390	           perror("kevent");
391	           exit(EXIT_FAILURE);
Без kqueue & kevent (если убрать все внутри цикла while(1)) все работает отлично: сигнал перехватывается, обрабатывается и программа продолжает работу.

Собственно вопрос: как корректно обработать сигнал в случае использования kqueue, выполнить нужные действия и продолжить работу программы?

★★★★★
Ответ на: комментарий от firkax

Вместо t_lock подставить мютекс или что-то подобное.

Как это запустить-то? Выложи весь код.

А ещё signalfd в BSD нет, это Linux-specific.

Да насрать, там есть EVFILT_SIGNAL для kqueue(). Это считай то же самое.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Как это запустить-то? Выложи весь код.

Не собирался я делать никаких демок к этим функциям, но да ладно, тем более что я там заметил неэффективность которую надо исправить.

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

static int log_fd;
static pthread_mutex_t log_lock;
static volatile int newlog;
static int usr1_handle(int sig) { newlog = 1; }
extern void init_usr1_handle(void) {
  struct sigaction sa;
  sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); sa.sa_handler = usr1_handle;
  sigaction(SIGUSR1, &sa, NULL);
}
static int open_newlog(void) {
  static int log_counter;
  char name[100];
  snprintf(name, sizeof(name), "logfile_%d.log", log_counter++);
  return open(name, O_WRONLY|O_CREAT|O_APPEND, 0644);
}
extern void check_newlog(void) {
  if(!newlog) return;
  pthread_mutex_lock(&log_lock);
  if(newlog) {
    fd = open_newlog();
    if(fd<0) dprintf(log_fd, "can't reopen log!\n");
    else {
      if(dup2(fd, log_fd)<0) dprintf(log_fd, "can't reopen log!\n");
      close(fd);
    }
    newlog = 0;
  }
  pthread_mutex_unlock(&log_lock);
}
extern void write_log(char const *fmt, ...) {
  va_list arg;
  int fd;
  check_newlog(); /* но лучше её делать явно в рабочем цикле */
  pthread_mutex_lock(&log_lock);
  va_start(arg, fmt);
  vdprintf(log_fd, fmt, arg);
  write(log_fd, "\n", 1);
  va_end(arg);
  pthread_mutex_unlock(&log_lock);
}
int main(int argc, char **argv) {
  int r;
  char str[100];
  if((log_fd=open_newlog())<0) { fprintf(stderr,"log open error %d (%s)\n", errno, strerror(errno)); return -1; }
  if(r=pthread_mutex_int(&log_lock,NULL)) { fprintf(stderr,"mutex init error %d (%s)\n", r, strerror(r)); return -1; }
  init_usr1_handle();
  for(r=0; ; r++) {
    write_log("Message number %d", r);
    sleep(1);
  }
}
firkax ★★★★★
()
Ответ на: комментарий от firkax
>if(newlog) {
>    fd = open_newlog();
>    if(fd<0) dprintf(log_fd, "can't reopen log!\n");
>    else {
>      if(dup2(fd, log_fd)<0) dprintf(log_fd, "can't reopen log!\n");
>      close(fd);
>    }
>    newlog = 0;
>  }

Дядя, ты сигнал просираешь вот здесь. Если у тебя SIGUSR1 придёт дважды с небольшим интервалом, это выполнится только один раз.

Конечно, в данном случае не критично, но блин, детская ошибка же.

Алсо, у тебя теперь при каждой записи в лог будет дёргаться мютекс, что весьма небыстро и не слишком приятно.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от hateyoufeel

Да, действительно. Ну значит надо newlog=0; в начало переставить.

Насчёт ошибки не совсем соглашусь. В некоторых случаях это как раз не просто «не критично» а как раз даже желательно - не обрабатывать дубликаты сигналов, пришедших пока ты прошлый не обработал, например, если сигнал запрашивает дамп какой-нить тяжёлой статистики. В данном случае это действительно может быть теоретической проблемой, если после ротации логов мы переоткроем файл, после чего сразу случится ещё одна ротация и отправка второго сигнала - писать мы будем в старый ротированный файл. Но на практике такого не случится по двум причинам: 1) ротация делается намного реже 2) пустые файлы не ротируются, даже если ротацию запустить вручную с интервалом в 10мс, и второй сигнал переоткроет тот же файл что создан пустым после первого.

Но да, всё-таки стоит поддерживать любые варианты ротации, в т.ч. кажущиеся неадекватными, поскольку у нас никаого контракта с ротацией, кроме отправки сигнала, нет.

Алсо, у тебя теперь при каждой записи в лог будет дёргаться мютекс, что весьма небыстро и не слишком приятно.

Очень даже быстро. Быстрее чем сисколл записи что в файл что в сокет. В большинстве случаев он удовлетворится вообще юзерспейсом кажется (хотя это надо смотреть конкретные реализации в разных ОС). Ну и вообще если у мютекса проблемы со скоростью можно другое что-то использовать, я не зря там абстрактный t_lock в начале поставил. Семафоры в фрибсд точно юзерспейсные до тех пор пока не приходится ждать.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 3)
Ответ на: комментарий от firkax

Да, действительно. Ну значит надоnewlog=0;в начало переставить.

Да нет, ты всё равно просрёшь доп.сигналы, на самом деле. Если у тебя до этой проверки их будет 10 подряд, ты обработаешь только один. Опять же, в данном случае, конечно, пофиг, но signalfd именно из-за этого сделали. Альтернативой signalfd/kqueue будет трюк с записью в pipe.

Сигналы в UNIX в том виде, как они изначально сделаны, это рак, жопа и сотона. Любого, кто считает, что это было хорошей идеей, надо бить по яйцам куском арматуры до полного излечения.

Но да, всё-таки стоит поддерживать любые варианты ротации, в т.ч. кажущиеся неадекватными, поскольку у нас никаого контракта с ротацией, кроме отправки сигнала, нет.

Я поэтому про syslog сразу и написал. Самому ротацией лучше не заниматься, гораздо проще писать в какой-нибудь сокет и пусть на том конце сами разбираются.

hateyoufeel ★★★★★
()
Ответ на: комментарий от firkax

Как раз надо ломать из того же - нужно чтобы сигнал поймался потоком, который прямо сейчас исполняет внутренности printf. Из другого stdio рассчитан.

не знаю. это надо смотреть как вызывается сигхандлер, если сам поток генерит сигнал. ничто не мешает(поскольку вроде ничему не противоречит) выполниться ему синхронно, прямо сразу, без задержек… поскольку непонятно, зачем его откладывать. но тогда не будет никакого реентера, поскольку в printf данный поток и не входил.

alysnix ★★★
()
Ответ на: комментарий от hateyoufeel

Да нет, ты всё равно просрёшь доп.сигналы, на самом деле. Если у тебя до этой проверки их будет 10 подряд, ты обработаешь только один. Опять же, в данном случае, конечно, пофиг, но signalfd именно из-за этого сделали. Альтернативой signalfd/kqueue будет трюк с записью в pipe.

Если их будет 10 ДО проверки, то их и надо выкинуть. Не забывай, что речь про ротацию логов, а смена названий файла и счётчик тут только для демонстрации. Если логи ротировались 10 раз и мы это заметили когда всё уже закончилось - нам однозначно НЕ надо переоткрывать файл 10 раз, надо только один. Так что да, к signalfd надо прикрутить ещё и удаление дубликатов сигналов перед обработкой.

Сигналы в UNIX в том виде, как они изначально сделаны, это рак, жопа и сотона. Любого, кто считает, что это было хорошей идеей, надо бить по яйцам куском арматуры до полного излечения.

Просто не надо сигналы считать за полноценное ipc и всё станет норм. Это только способ разового тривиального уведомления, а для отправки сложных данных надо использовать другие средства. А проблема у них есть в том что EINTR много где не обрабатывается нормально.

Я поэтому про syslog сразу и написал.

Он годится только для более-менее общесистемных логов. Хотя не знаю какие у автора. Например пытаться http-сервер логировать через syslog обернётся однозначным провалом.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Просто не надо сигналы считать за полноценное ipc и всё станет норм. Это только способ разового тривиального уведомления, а для отправки сложных данных надо использовать другие средства. А проблема у них есть в том что EINTR много где не обрабатывается нормально.

Да нет, они вообще ни для чего не годятся. SIGINT и прочие SIGTERM, разве что, кое-как полезны. Ну и SIGSEGV сотоварищи. А в остальном это всё ужасный рак.

Он годится только для более-менее общесистемных логов. Хотя не знаю какие у автора. Например пытаться http-сервер логировать через syslog обернётся однозначным провалом.

Сфига ли? Он получает логи и пишет их в файл, плюс умеет ротацию. Что ещё надо?

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от firkax

я вот не понимаю эту вашу архитектуру.

запрос на переоткрытие файла должен идти от функции записи лога, когда она обнаруживает что лог файл слишком велик. то есть и сигнала никакого не надо. если синхронное переоткрытие файла будет считаться слишком большой задержкой для функции логирования, то писать надо в очередь, и чтобы какой-то другой тред сбрасывал очередь в логфайл, и занимался этими переоткрытиями.

alysnix ★★★
()
Ответ на: комментарий от hateyoufeel

У него заранее заданный ограниченный список типов логов. которые он может писать. И этот список - один на всю систему. Представь что у тебя на http-сервере сотня доменов, каждому нужен отдельно access-лог и error-лог, а для некоторых вещей ещё какие-то доп. логи. В формат syslog такое не впихнуть по-нормальному.

firkax ★★★★★
()
Ответ на: комментарий от firkax

У него заранее заданный ограниченный список типов логов. которые он может писать. И этот список - один на всю систему.

На это вообще посрать.

Представь что у тебя на http-сервере сотня доменов, каждому нужен отдельно access-лог и error-лог, а для некоторых вещей ещё какие-то доп. логи. В формат syslog такое не впихнуть по-нормальному.

В дефолтный – нет. Но любой приличный syslog-демон умеет сравнения с образцом и без проблем по префиксу или маске тебе всё раскидает.

hateyoufeel ★★★★★
()
Ответ на: комментарий от alysnix

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

firkax ★★★★★
()
Ответ на: комментарий от alysnix

когда она обнаруживает что лог файл слишком велик

Мысли шире. Можно делать ротацию не только по размеру лога, но и по куче других условий. К примеру, по времени. Или от балды, захотелось здесь и сейчас.

По этому и есть смысл все что касается логов вынести во вне. А сервис всего лишь научить в нужный момент переоткрывать лог файл. А там уже пускай тулза ротации логов разбирается что с ними дальше делать.

iron ★★★★★
() автор топика
Ответ на: комментарий от firkax

Он годится только для более-менее общесистемных логов.

Почитай про возможности того же rsyslog. Там можно творить какую-угодно магию с логами: хранить их в какой-угодно БД, отправлять по сети, фильтровать как душе угодно и т.д. Так что syslog (на примере rsyslog) — это уже тот старый и унылый демон из 90-х, а вполне юзабельныйи фитчастый вариант для управления логами.

iron ★★★★★
() автор топика

Вот уже законченный вариант, вдруг кому-то понадобится. Оставлю, как говорится, для потомков :)

В основной программе в цикле генерим логи и записываем в pipe. В отдельном треде при помощи kqueue мониторим наличие данных в пайпе и «логируем». Если получаем SIGUSR1 — устанавливаем переменную типа sig_atomic_t в 1. В треде логгера проверяем эту переменную, если 1 то переоткрываем файл лога и сбрасываем переменную в 0.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/event.h> // kqueue
#include <pthread.h>
#include <time.h> // for rng
#include <signal.h>

static const size_t max_len = 1024;
static int pipe_fd[2];
static volatile sig_atomic_t log_rotate = 0;

static void sigusr1_handler(int sugnum) {
    log_rotate = 1;
}

static void* logger_thread(void *arg) {
    char buff[max_len];
    int kq, env;
    struct kevent change;
    struct kevent event;

    (void)arg;

    kq = kqueue();

    if (kq == -1) {
        perror("kqueue");
        exit(EXIT_FAILURE);
    }

    EV_SET(&change, pipe_fd[0], EVFILT_READ, EV_ADD, 0, 0, 0);

    while(true) {
        env = kevent(kq, &change, 1, &event, 1, NULL);
        if (env == -1)
            perror("kevent");
        else if (env > 0) {
            if (event.data > 0) {
                printf("Received %ld bytes in pipe to read\n", event.data);
                printf("Reading data from it:\n");
                if ((size_t)event.data < max_len - 1) {
                    read(pipe_fd[0], buff, (size_t)event.data);
                    buff[event.data] = '\0';
                    printf("%s\n", buff);
                } else
                    printf("Buffer overflow!\n");
            }
        }
        if (log_rotate) {
            printf("Oh shit! We got SIGUSR1, need to reopen log!\n");
            // Reopen log file here
            log_rotate = 0;
        }
    }
}

int main(void) {
    int inc = 0;
    int rng, i;
    char data[max_len];
    pthread_t thread;
    struct sigaction sig;

    memset(&sig, 0, sizeof(sig));
    sig.sa_handler = sigusr1_handler;
    sig.sa_flags = SA_RESTART;

    sigaction(SIGUSR1, &sig, NULL);

    srand(time(NULL)); // init rng generator

    // bring up logger thread
    if (pthread_create(&thread, NULL, logger_thread, NULL) < 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    if (pipe2(pipe_fd, O_NONBLOCK) == -1) {
         perror("pipe2");
         exit(EXIT_FAILURE);
    }

    char * message = "Hello, logger thread!\n";
    while(true) {
        rng = ((rand() % 3) + 1);
        for (i = 0; i < rng; i++) {
            snprintf(data, max_len, "#%d %s", inc++, message);
            write(pipe_fd[1], data, strlen(data));
        }
        sleep(1);
    }
}
Компилять так же:
cc -Wall pipe_logger.c -o pipe_logger -lpthread

iron ★★★★★
() автор топика
Ответ на: комментарий от iron

Если получаем SIGUSR1 — устанавливаем переменную типа sig_atomic_t в 1. В треде логгера проверяем эту переменную, если 1 то переоткрываем файл лога и сбрасываем переменную в 0.

нафига? У тебя сигналы через kqueue можно получать прямо в том же цикле.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

У тебя сигналы через kqueue можно получать прямо в том же цикле.

Как? Я пробовал при помощи kqueue отлавливать сингал в треде. Но основной цикл программы получает сигнал первым и до треда он не доходит. Программа пишет «Пользовательский сигнал 1» и завершается. Кажись нужно как-то блокировать сигнал в основном потоке чтоб при этом он доходил до треда.

iron ★★★★★
() автор топика
Ответ на: комментарий от iron

Как? Я пробовал при помощи kqueue отлавливать сингал в треде. Но основной цикл программы получает сигнал первым и до треда он не доходит.

Как и с signalfd, при старте программы блокируешь сигнал через SIG_BLOCK. На kqueue это не распространяется.

Вот, сходу нагуглился пример: https://gist.github.com/azat/73638b8e3b0fa563a20dadcca9e652a1

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Ух ты! Работает!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/event.h> // kqueue
#include <pthread.h>
#include <time.h> // for rng
#include <signal.h>

static const size_t max_len = 1024;
static int pipe_fd[2];

static void* logger_thread(void *arg) {
    char buff[max_len];
    int kq, result;
    struct kevent pipe_event, signal_event, events[2];

    (void)arg;

    kq = kqueue();

    if (kq == -1) {
        perror("kqueue");
        exit(EXIT_FAILURE);
    }

    EV_SET(&pipe_event, pipe_fd[0], EVFILT_READ, EV_ADD, 0, 0, 0);
    result = kevent(kq, &pipe_event, 1, NULL, 0, NULL);
    if (result == -1) {
        perror("kevent for pipe_fd");
        exit(EXIT_FAILURE);
    }
    EV_SET(&signal_event, SIGUSR1, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
    result = kevent(kq, &signal_event, 1, NULL, 0, NULL);
    if (result == -1) {
        perror("kevent for signal_event");
        exit(EXIT_FAILURE);
    }

    while(true) {
        result = kevent(kq, NULL, 0, events, 2, NULL);
        if (result == -1)
            perror("kevent");
        else if (result > 0) {
            for (int i = 0; i < result; i++) {
                if (events[i].filter == EVFILT_READ && events[i].ident == (unsigned long)pipe_fd[0]) {
                    printf("Received %ld bytes in pipe to read\n", events[i].data);
                    printf("Reading data from it:\n");
                    if ((size_t)events[i].data < max_len - 1) {
                        read(pipe_fd[0], buff, (size_t)events[i].data);
                        buff[events[i].data] = '\0';
                        printf("%s\n", buff);
                    } else
                        printf("Buffer overflow!\n");
                } else if (events[i].filter == EVFILT_SIGNAL && events[i].ident == SIGUSR1) {
                    printf("Oh shit! We got SIGUSR1, need to rotate log!\n");
                }
            }
        }
    }
}

int main(void) {
    int inc = 0, rng, i;
    char data[max_len];
    pthread_t thread;
    struct sigaction sig;

    // Disable SIGUSR1 in the main loop
    memset(&sig, 0, sizeof(sig));
    sig.sa_handler = SIG_IGN;
    sigaction(SIGUSR1, &sig, NULL);

    srand(time(NULL)); // init rng generator

    // bring up logger thread
    if (pthread_create(&thread, NULL, logger_thread, NULL) < 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    if (pipe2(pipe_fd, O_NONBLOCK) == -1) {
         perror("pipe2");
         exit(EXIT_FAILURE);
    }

    char * message = "Hello, logger thread!\n";
    while(true) {
        rng = ((rand() % 3) + 1);
        for (i = 0; i < rng; i++) {
            snprintf(data, max_len, "#%d %s", inc++, message);
            write(pipe_fd[1], data, strlen(data));
        }
        sleep(1);
    }
}

iron ★★★★★
() автор топика
Ответ на: комментарий от iron
    // bring up logger thread
    if (pthread_create(&thread, NULL, logger_thread, NULL) < 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    if (pipe2(pipe_fd, O_NONBLOCK) == -1) {
         perror("pipe2");
         exit(EXIT_FAILURE);
    }

Вот эти два вызова поменяй местами. А то у тебя тред стартует когда pipe_fd невалидны и суёт их в kqueue вместо настоящих.

hateyoufeel ★★★★★
()