Ошибка сегментации (дамп ядра) у производителя и потребителя, использующего семафор

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

Код, как показано ниже:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem_consumer;
sem_t sem_producer;

typedef struct node
{   
    int data;
    struct node *next;
} Node;

Node * head = NULL;

void * producer(void *arg)
{
    while (1)
    {
        sem_wait(&sem_producer);
        Node *ptr = (Node *)malloc(sizeof(Node));
        ptr->data = rand() % 1000;
        printf("++++++ producer: %lu, %d\n", pthread_self(), ptr->data);
        ptr->next = head;
        head = ptr;
        sem_post(&sem_consumer);
    }

    return NULL;
}

void * consumer(void * arg)
{
    while (1)
    {
        sem_wait(&sem_consumer);
        Node *pdel = head;
        head = head -> next;
        printf("------ consumer: %lu, %d\n", pthread_self(), pdel->data);
        free(pdel);
        sem_post(&sem_producer);
    }

    return NULL;
}

int main(int argc, char *argv[])
{
    sem_init(&sem_consumer, 0, 0);
    sem_init(&sem_producer, 0, 3);

    pthread_t pthid[2];

    pthread_create(&pthid[0], NULL, producer, NULL);
    pthread_create(&pthid[1], NULL, consumer, NULL);

    for (int i = 0; i < 2; i++)
    {
        pthread_join(pthid[i], NULL);
    }

    sem_destroy(&sem_consumer);
    sem_destroy(&sem_producer);

    return 0;
}

запустить в gdb может быть, а затем bt

Potato 25.09.2018 16:19

Для этого типа ошибок используйте sanitzier (в данном случае ThreadSanitizer)

hellow 25.09.2018 16:22

Также обратите внимание, что такие операторы printf(), как printf("------ consumer: %lu, %d\n", pthread_self(), pdel->data);, не гарантируются атомарно, поэтому вы, вероятно, также получите чередующийся вывод, если ваш код запускает потоки одновременно. Ваш опубликованный код не должен запускать потоки производителя и потребителя одновременно, поэтому это не станет очевидным.

Andrew Henle 25.09.2018 16:28

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

Lundin 25.09.2018 16:31

@Lundin, ты прав, я позабочусь об этом в следующий раз.

Melvin Levett 25.09.2018 16:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
547
2

Ответы 2

Этот

sem_init(&sem_producer, 0, 3);

инициализирует семафор sem_producer до 3, позволяя потокам производителя и потребителя одновременный доступ к коду, например

ptr->next = head;
head = ptr;

а также

Node *pdel = head;
head = head -> next;
printf("------ consumer: %lu, %d\n", pthread_self(), pdel->data);
free(pdel);

Ваш связанный список не защищен семафором должным образом. Это состояние гонки.

Инициализация sem_producer на 1 исправит состояние гонки:

sem_init(&sem_producer, 0, 1);

Могут быть и другие ошибки, которые я не обнаружил.

@hellow Если вы собираетесь изменить смысл ответа, прокомментируйте, почему. Я недостаточно проанализировал код OP, чтобы заявить, что инициализация семафора значением 1 вместо 3 даст правильные результаты.

Andrew Henle 25.09.2018 16:30

... вы уверены? sem_init(&sem_producer, 0, 3); позволит producer() выполнить три цикла ... не проблема ли больше в защите одного ресурса (head) с помощью двух семафоров?

Attie 25.09.2018 16:30

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

Andrew Henle 25.09.2018 16:32

@ Эндрю Хенле Состояние гонки, я понял. Итак, как я могу этого избежать? Я имею в виду, что я должен сделать, чтобы максимально безопасно увеличить размер буфера для производителя, потому что я не хочу, чтобы семафор имел только один.

Melvin Levett 25.09.2018 16:33

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

Attie 25.09.2018 16:33

@AndrewHenle Честно говоря: если вы не уверены, что ваш ответ правильный, вы не должны публиковать ответ, а вместо этого краткий комментарий. Однако вполне вероятно, что это правильный ответ.

Lundin 25.09.2018 16:35

@attie Может быть, а может и нет. Я считаю, что производитель может оставаться на три итерации раньше потребителя, и оба будут изменять список одновременно, хотя я на самом деле недостаточно проанализировал код, чтобы быть уверенным.

Andrew Henle 25.09.2018 16:35

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

Andrew Henle 25.09.2018 16:36

@AndrewHenle Хорошее замечание, проблем может быть больше. Вы должны откатить правку.

Lundin 25.09.2018 16:37

@Melvin Итак, как я могу этого избежать, я имею в виду, что я должен сделать, чтобы максимально безопасно увеличить буфер для производителя Вы можете использовать мьютекс для частей потоков, которые обращаются к связанному списку, исключить семафор производителя и использовать только семафор потребителя, чтобы сообщить потоку потребителя, что в связанном списке есть хотя бы один элемент для работы.

Andrew Henle 25.09.2018 16:41

@AndrewHenle хорошее решение, я просто забыл, что я должен использовать замок для защиты критической зоны.

Melvin Levett 25.09.2018 16:44

Думаю, главная проблема:

  • Ресурс у вас один - head
  • Ваш "защита" это с двумя отдельными семафорами - sem_producer и sem_consumer

Семафоры используются для сигнализации «ты можешь взять что-нибудь» или «ты можешь положить что-нибудь», контролируя использование ресурсов - например, если вы хотите гарантировать, что список никогда не будет глубже трех объектов. Это особенно важно, когда потоки производителя / потребителя имеют разное время выполнения. Если вы не слишком озабочены производством множества товаров, за которыми потребитель, возможно, никогда не успеет, то вы можете полностью удалить sem_producer.

Мьютексы («взаимное исключение») используются для того, чтобы два потока не обрабатывали один объект одновременно. Я бы посоветовал вам использовать мьютекс вокруг манипуляций с указателем рядом с семафорами.

pthread_mutex_t mux;
void *producer(void *arg) {
    while (1) {
        sem_wait(&sem_producer);

        /* gather data */

        pthread_mutex_lock(&mux);
        p_new->next = head;
        head = p_new;
        pthread_mutex_unlock(&mux);

        sem_post(&sem_consumer);
    }

    return NULL;
}
void *consumer(void *arg) {
    while (1) {
        sem_wait(&sem_consumer);

        pthread_mutex_lock(&mux);
        p_next = head;
        if (p_next != NULL) {
            head = p_next->next;
        }
        pthread_mutex_unlock(&mux);

        /* skip if there isn't actually a new item */
        if (p_next != NULL) {
            /* do processing and discard */
        }

        sem_post(&sem_producer);
    }

    return NULL;
}

Не забудьте вызвать в pthread_mutex_init() и pthread_mutex_destroy().

Как сообщается в настоящее время, если pdel - это NULL, а поток производителя ожидает семафор sem_producer, потоки зайдут в тупик. При наличии мьютекса логически безопасно полностью исключить семафор sem_producer. В действительности, если поток-производитель заходит слишком далеко вперед, в игру может вступить максимально возможное значение семафора.

Andrew Henle 25.09.2018 16:46

@Attie Я думаю, что оператор if после метода pthread_mutex_unlock в потребителе вообще не нужен, потому что у меня есть семафор sem_consumer, чтобы гарантировать, что у меня есть элемент определенно, когда sem_wait не заблокирован. И вы пропустили освобождение pdel в потребителе.

Melvin Levett 25.09.2018 16:52

«Я думаю, что оператор if после метода pthread_mutex_unlock у потребителя вообще не нужен» - возможно, но если вы принимаете сценарий «что, если», то после тестирования он предотвратит сбой вашего приложения ... sem_wait() может вернуться по причинам, помимо «ты попал».

Attie 25.09.2018 16:55

"вы пропустили освобождение pdel в потребителе" - я удалил управление памятью из моего примера кода ... должно быть намного больше проверок возвращаемого значения! ... опущено для краткости :-)

Attie 25.09.2018 16:55

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