Ubuntu: sem_timed, не просыпается (C)

У меня есть 3 процесса, которые нужно синхронизировать. Процесс один делает что-то, затем пробуждает процесс два и спит, который что-то делает, затем пробуждает процесс три и спит, который что-то делает, и пробуждает процесс один и спит. Весь цикл рассчитан на работу с частотой около 25 Гц (вызванная внешней синхронизацией в первом процессе, прежде чем он запустит второй процесс в моем "реальном" приложении). Я использую sem_post для запуска (пробуждения) каждого процесса и sem_timedwait() для ожидания запуска.

Это все успешно работает в течение нескольких часов. Однако в какое-то случайное время (обычно после двух-четырех часов) один из процессов начинает тайм-аут в sem_timedwait(), хотя я уверен, что семафор запускается с помощью sem_post(). Чтобы доказать это, я даже использую sem_getvalue() сразу после тайм-аута, и значение равно 1, поэтому время ожидания должно было сработать.

Пожалуйста, смотрите следующий код:

#include <stdio.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <semaphore.h>

sem_t trigger_sem1, trigger_sem2, trigger_sem3;

// The main thread process.  Called three times with a different num arg - 1, 2 or 3.
void *thread(void *arg)
{
  int num = (int) arg;
  sem_t *wait, *trigger;
  int val, retval;
  struct timespec ts;
  struct timeval tv;

  switch (num)
    {
      case 1:
        wait = &trigger_sem1;
        trigger = &trigger_sem2;
        break;
      case 2:
        wait = &trigger_sem2;
        trigger = &trigger_sem3;
        break;
      case 3:
        wait = &trigger_sem3;
        trigger = &trigger_sem1;
        break;
    }

  while (1)
    {
      // The first thread delays by 40ms to time the whole loop.  
      // This is an external sync in the real app.
      if (num == 1)   
        usleep(40000);

      // print sem value before we wait.  If this is 1, sem_timedwait() will
      // return immediately, otherwise it will block until sem_post() is called on this sem. 
      sem_getvalue(wait, &val);
      printf("sem%d wait sync sem%d. val before %d\n", num, num, val);

          // get current time and add half a second for timeout.
      gettimeofday(&tv, NULL);
      ts.tv_sec = tv.tv_sec;
      ts.tv_nsec = (tv.tv_usec + 500000);    // add half a second
      if (ts.tv_nsec > 1000000)
        {
          ts.tv_sec++;
          ts.tv_nsec -= 1000000;
        }
      ts.tv_nsec *= 1000;    /* convert to nanosecs */

      retval = sem_timedwait(wait, &ts);
      if (retval == -1)
        {
          // timed out.  Print value of sem now.  This should be 0, otherwise sem_timedwait
          // would have woken before timeout (unless the sem_post happened between the 
          // timeout and this call to sem_getvalue).
          sem_getvalue(wait, &val);
          printf("!!!!!!    sem%d sem_timedwait failed: %s, val now %d\n", 
            num, strerror(errno), val);
        }
      else
        printf("sem%d wakeup.\n", num);

        // get value of semaphore to trigger.  If it's 1, don't post as it has already been 
        // triggered and sem_timedwait on this sem *should* not block.
      sem_getvalue(trigger, &val);
      if (val <= 0)
        {
          printf("sem%d send sync sem%d. val before %d\n", num, (num == 3 ? 1 : num+1), val);
          sem_post(trigger);
        }
      else
        printf("!! sem%d not sending sync, val %d\n", num, val);
    }
}



int main(int argc, char *argv[])
{
  pthread_t t1, t2, t3;

   // create semaphores.  val of sem1 is 1 to trigger straight away and start the whole ball rolling.
  if (sem_init(&trigger_sem1, 0, 1) == -1)
    perror("Error creating trigger_listman semaphore");
  if (sem_init(&trigger_sem2, 0, 0) == -1)
    perror("Error creating trigger_comms semaphore");
  if (sem_init(&trigger_sem3, 0, 0) == -1)
    perror("Error creating trigger_vws semaphore");

  pthread_create(&t1, NULL, thread, (void *) 1);
  pthread_create(&t2, NULL, thread, (void *) 2);
  pthread_create(&t3, NULL, thread, (void *) 3);

  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_join(t3, NULL);
}

Следующий вывод выводится, когда программа работает правильно (в начале и в случайном порядке, но долгое время после). Значение sem1 всегда равно 1, прежде чем thread1 ожидает, пока он спит в течение 40 мс, к которому время sem3 его запустило, поэтому он сразу же просыпается. Два других потока ожидают получения семафора из предыдущего потока.

[...]
sem1 wait sync sem1. val before 1
sem1 wakeup.
sem1 send sync sem2. val before 0
sem2 wakeup.
sem2 send sync sem3. val before 0
sem2 wait sync sem2. val before 0
sem3 wakeup.
sem3 send sync sem1. val before 0
sem3 wait sync sem3. val before 0
sem1 wait sync sem1. val before 1
sem1 wakeup.
sem1 send sync sem2. val before 0
[...]

Однако через несколько часов один из потоков начинает время ожидания. По выводу я вижу, что семафор запускается, и когда я печатаю значение после тайм-аута, оно равно 1. Так что sem_timedwait должен был проснуться задолго до тайм-аута. Я бы никогда не ожидал, что значение семафора будет равно 1 после тайм-аута, за исключением очень редкого случая (почти наверняка никогда, но это возможно), когда триггер происходит после тайм-аута, но до того, как я вызову sem_getvalue.

Кроме того, как только он начинает терпеть неудачу, каждый sem_timedwait() на этом семафоре также терпит неудачу таким же образом. Смотрите следующий вывод, который я пронумеровал:

01  sem3 wait sync sem3. val before 0
02  sem1 wakeup.
03  sem1 send sync sem2. val before 0
04  sem2 wakeup.
05  sem2 send sync sem3. val before 0
06  sem2 wait sync sem2. val before 0
07  sem1 wait sync sem1. val before 0
08  !!!!!!    sem3 sem_timedwait failed: Connection timed out, val now 1
09  sem3 send sync sem1. val before 0
10  sem3 wait sync sem3. val before 1
11  sem3 wakeup.
12  !! sem3 not sending sync, val 1
13  sem3 wait sync sem3. val before 0
14  sem1 wakeup.
[...]

В строке 1 поток 3 (который я смущенно назвал sem3 в printf) ожидает запуска sem3. В строке 5 thread2 вызывает sem_post для sem3. Тем не менее, строка 8 показывает тайм-аут sem3, но значение семафора равно 1. thread3 затем запускает sem1 и снова ждет (10). Однако, поскольку значение уже равно 1, оно сразу просыпается. Он не отправляет sem1 снова, поскольку все это произошло до того, как управление передается thread1, однако затем он снова ждет (val теперь 0) и sem1 активируется. Теперь это повторяется навсегда, sem3 всегда отключается и показывает, что значение равно 1.

Итак, мой вопрос: почему тайм-аут sem3, несмотря на то, что семафор сработал и значение явно 1? Я бы никогда не ожидал увидеть строку 08 в выводе. Если время ожидания истекло (потому что, скажем, поток 2 вышел из строя или занимает слишком много времени), значение должно быть равно 0. И почему сначала он хорошо работает в течение 3 или 4 часов, прежде чем войти в это состояние?

Я пробовал подобный тест, используя три отдельные программы, общаясь через общую память, а не три потока в одной программе. Это больше похоже на мое приложение в реальном мире. Результаты и результаты были одинаковыми. Проблема, по-видимому, связана с семафором (особенно с вызовом sem_timedwait), а не с тем, что связано с pthreads.

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

Это использует Ubuntu 9.4 с ядром 2.6.28. Та же процедура работала правильно на Redhat и Fedora, но сейчас я пытаюсь портировать на Ubuntu. Я также пытался использовать Ubuntu 9.10, который не имел никакого значения.

Спасибо за любой совет, Джайлс

6 ответов

Решение

Кажется, проблема связана с передачей неверного аргумента тайм-аута.

По крайней мере, на моей машине первый сбой не ETIMEDOUT, но:

!!!!!! sem2 sem_timedwait не удалось: неверный аргумент, val теперь 0

Теперь, если я напишу:

  if (ts.tv_nsec >= 1000000)

(обратите внимание на добавление =), тогда он работает нормально. Другой вопрос, почему состояние семафора (предположительно) истощается, так что оно истекает при последующих попытках или просто навсегда блокирует прямой sem_wait. Похоже, ошибка в libc или ядре.

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

Ответ, я думаю, уже в оригинальном посте на вопрос.

Итак, мой вопрос: почему тайм-аут sem3, несмотря на то, что семафор сработал и значение явно 1? Я бы никогда не ожидал увидеть строку 08 в выводе. Если время ожидания истекло (например, из-за того, что поток 2 вышел из строя или занимает слишком много времени), значение должно быть равно 0. И почему сначала он работает нормально в течение 3-4 часов, прежде чем перейти в это состояние?

Итак, сценарий таков:

  1. нить 2 занимает слишком много времени
  2. Семь раз в sem_timedwait
  3. поток 3 отменен или что-то требуется, чтобы достичь sem_getvalue
  4. нить 2 просыпается и делает своеsem_post на sem3
  5. поток 3 выдает его sem_getvalueи видит 1
  6. нить 3 ветки в неправильную ветку и не делает его sem_postна sem1

Это условие гонки трудно вызвать, в основном вам нужно попасть в крошечное временное окно, в котором у одного потока возникла проблема с ожиданием семафора, а затем прочитать семафор с помощью sem_getvalue, Возникновение этого условия во многом зависит от среды (тип системы, количество ядер, нагрузка, прерывания ввода-вывода), поэтому это объясняет, почему это происходит только после нескольких часов, если не вообще.

Наличие потока управления зависит от sem_getvalue это вообще плохая идея. Единственный атомарный неблокирующий доступ к sem_t через sem_post а также sem_trywait,

Таким образом, этот пример кода из вопроса имеет это состояние гонки. Это не означает, что исходный код проблемы, который был у gillez, действительно имеет то же самое состояние гонки. Возможно, пример слишком упрощен и все еще показывает то же явление для него.

Я предполагаю, что в его первоначальной проблеме был незащищенный sem_wait, Это sem_wait это проверяется только для его возвращаемого значения, а не для errno в случае неудачи. EINTRпроисходят sem_wait вполне естественно, если процесс имеет некоторый IO. Вы только что сделали do - while с проверкой и сбросом errno если вы столкнетесь с EINTR,

Не вините в этом Ubuntu или любой другой дистрибутив:-) Что, безусловно, важнее, так это версия gcc, которую вы используете, 32 или 64 бит и т. Д., Сколько ядер у вашей системы. Поэтому, пожалуйста, дайте немного больше информации. Но, просматривая ваш код, я нашел несколько мест, которые могут привести вас к неожиданному поведению:

  • начинается со старта, кастинг intв void* назад и вперед, вы ищете проблемы. использование uintptr_tдля этого, если нужно, но здесь у вас нет оправдания, чтобы просто передавать реальные указатели на значения. &(int){ 1 } и некоторые более разумные заклинания сделали бы трюк для C99.

  • ts.tv_nsec = (tv.tv_usec + 500000) это еще одна проблемная точка. Правая сторона может иметь ширину, отличную от левой. Делать

    ts.tv_nsec = tv.tv_usec;

    ts.tv_nsec + = 500000;

  • Семейство функций sem небезопасно. Такие прерывания могут, например, вызываться IO, так как вы выполняете printf и т. Д. Проверка возвращаемого значения для -1 или так недостаточно, но в таком случае вы должны проверить errno и решите, хотите ли вы повторить попытку. Тогда вам нужно будет пересчитать оставшееся время и все в таком духе, если вы хотите быть точным. Тогда страница руководства для sem_timedwait имеет список различных кодов ошибок, которые могут возникнуть, и их причины.

  • Вы также заключаете вещи из ценностей, которые вы получаете через sem_getvalue, В многопоточной / многопроцессорной / многопроцессорной среде ваш поток мог быть незапланированным между возвратом из sem_timedwait а такжеsem_getvalue, По сути, из этого ничего нельзя вывести, переменная просто случайно соответствует значению, которое вы наблюдаете. Не делайте выводов из этого.

Я понятия не имею, что происходит не так, и код выглядит хорошо, я тоже. Вот некоторые вещи, которые вы могли бы сделать, чтобы получить дополнительную информацию.

  • используйте другой тайм-аут, как короче, так и дольше, и посмотрите, возникла ли ваша проблема
  • используйте не синхронизированную версию и посмотрите, зависает ли программа.
  • попытайтесь изменить поведение вашего планировщика ядра, например, используя параметры командной строки ядра или используя procfs или s ysfs.

Как указал Йенс, есть две гонки:

Во-первых, при оценке значения семафора после вызова sem_timedwait. Это не меняет поток управления с семафором. Вне зависимости от того, истек ли тайм-аут потока, он все равно проходит через блок "должен ли я запустить следующий поток".

Второй - в части "Должен ли я пробудить следующий поток". Мы могли бы иметь следующие события:

  1. Темы н звонки sem_getvalue(trigger) и получает 1
  2. Поток n+1 возвращается из sem_timedwait и семафор уходит в 0
  3. Поток n решает не публиковать, и семафор остается на 0

Теперь я не вижу, как это может вызвать наблюдаемое поведение. В конце концов, поскольку поток n+1 в любом случае активируется, он, в свою очередь, активирует поток n+2, который активирует поток n и т. Д.

Хотя можно получить глюки, я не понимаю, как это может привести к систематическому таймауту из потока.

Это очень интересно. Хотя я не нашел источник ошибки (все еще ищу), я проверил это на Ubuntu 9.04 под управлением Linux 2.6.34.

Я сделал снимок программы на моей машине с Ubuntu 10.04 x86_64 Core i7.

При работе с usleep(40000) программа работала нормально в течение получаса или чего-то скучного.

При работе с usleep(40) программа работала нормально еще полчаса, а может и больше, прежде чем моя машина зависла. Х умер. Контроль +alt+F1-7 умер. Я не смог войти в систему. (К сожалению, на этой глупой клавиатуре Apple нет клавиши sysrq. Мне нравится печатать на ней, но мне точно не нужны f13, f14 или f15. Я бы сделал ужасные вещи, чтобы получить правильный ключ sysrq.)

И самое лучшее: НИЧЕГО в моих журналах не говорит мне, что произошло.

$ uname -a
Linux haig 2.6.32-22-generic #36-Ubuntu SMP Thu Jun 3 19:31:57 UTC 2010 x86_64 GNU/Linux

В то же время я также играл в Java-игру в браузере (опубликованную другим пользователем stackru, ищущим обратную связь, забавное развлечение:) - так что вполне возможно, что jvm отвечает за то, что что-то щекочет, чтобы заморозить мою машину.

Другие вопросы по тегам