(Извините за длинный пост, но я хотел быть максимально точным)
Я пытался распечатать маску сигналов основного потока во время написания программы на языке C, когда столкнулся с чем-то странным в sigprocmask
работе функции.
Фон[Источник: страница руководства sigprocmask(2)
]
Функция sigprocmask
используется для извлечения и/или изменения маски сигналов вызывающего потока.
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- Если
oldset
не равноNULL
, предыдущее значение маски сигнала сохраняется вoldset
. - Если
set
равноNULL
, то маска сигнала не изменяется (т.е.how
игнорируется), но текущее значение маски сигнала тем не менее возвращается вoldset
(если оно не равноNULL
). - Набор функций для изменения и проверки переменных типа
sigset_t
(«наборы сигналов») описан вsigsetops(3)
. Например:int sigemptyset(sigset_t *set);
: инициализирует набор сигналов, заданный параметром set toпустой, при этом все сигналы исключены из набора.int sigfillset(sigset_t *set);
: инициализирует наборполный, включая все сигналы.int sigismember(const sigset_t *set, int signum);
: проверяет, является ли signum членом множества.
Примечание: При создании заполненного набора сигналов sigfillset
функция glibc не включаетдвасигналы реального времени, используемые внутри реализации потоковой передачи NPTL.
Особенности системы
Дистрибутив Linux: Linux Mint 19.3
Cinnamon
Версия Glibc: 2.27
(по умолчанию)
Также проверено для версии Glibc:2.31.9
Вывод uname -a
:
Linux 5.0.0-32-generic #34~18.04.2-Ubuntu SMP Thu Oct 10 10:36:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
Проблема репликации
Программа, которая насторожила меня и заставила задуматься о том, что что-то может пойти не так, выглядит следующим образом:
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#define BUFFER_SIZE 32
#define OUTOFBOUNDS
#undef OUTOFBOUNDS
void print_set_bin(sigset_t *setp);
int main(void)
{
sigset_t set;
printf("NSIG = %d\n\n", NSIG);
printf("Empty set:\n");
if (sigemptyset(&set))
{
perror("sigemptyset");
return -1;
}
print_set_bin(&set);
printf("Filled set:\n");
if (sigfillset(&set))
{
perror("sigfillset");
return -1;
}
print_set_bin(&set);
printf("After sigprocmask():\n");
if (sigprocmask(SIG_BLOCK, NULL, &set))
{
perror("sigprocmask");
return -1;
}
print_set_bin(&set); // Why non-empty?
return 0;
}
void print_set_bin(sigset_t *setp)
{
int sig, res;
char buff[BUFFER_SIZE];
if (!setp)
{
fprintf(stderr, "print_set_bin(): NULL parameter\n");
return;
}
#ifdef OUTOFBOUNDS
for (sig = 0; sig <= NSIG; sig++)
#else
for (sig = 1; sig < NSIG; sig++)
#endif
{
res = sigismember(setp, sig);
if (res == -1)
{
snprintf(buff, BUFFER_SIZE, "sigisimember [%d]", sig);
perror(buff);
}
else
printf("%d", res);
}
printf(" [%s]\n\n", sigisemptyset(setp) ? "Empty" : "Non-empty");
}
Функция print_set_bin
выводит вывод sigismember
( 0
для не члена, 1
для члена) для всех сигналов. Макроопределение NSIG
(= 65) в signal.h
— это наибольший номер сигнала плюс один (1), как указано в /usr/include/x86_64-linux-gnu/bits/signum-generic.h
. В том же файле также упоминается, что этот наибольший номер сигнала включает сигналы реального времени (диапазон номеров [32, 64]) и что номер сигнала ноль (0) зарезервирован для целей тестирования.
В результате мой код, размещенный выше, проверяет номера сигналов, которые принадлежат диапазону [1, 64].
Ниже следуетвыходпрограммы:
NSIG = 65
Empty set:
0000000000000000000000000000000000000000000000000000000000000000 [Empty]
Filled set:
1111111111111111111111111111111001111111111111111111111111111111 [Non-empty]
After sigprocmask():
0000000000000000000000000000000000000000000000000000000000000000 [Non-empty]
Выходное объяснение
В этой программе выполняется манипуляция переменной set
типа sigset_t
. Сначала функция sigemptyset
используется для установки всех битов в ноль, затем функция sigfillset
используется для установки всех битов в единицу (кроме двух; см.ПримечаниевФонраздел) и, наконец, sigprocmask
используется для сохранения текущей маски сигнала в той же переменной. После всех операций, выполненных над набором сигналов, функция print_set_bin
используется для вывода того, какие сигналы принадлежат набору и является ли набор пустым (используя sigisemptyset()
).
Проблема, похоже, в последнем вызове print_set_bin
, где не найдено ни одного сигнала, принадлежащего набору, но sigisemptyset
функция характеризует набор как непустой. Это заставило меня задуматься, sigset_t
содержит ли более 64 бит и хотя бы один из них не равен нулю.
Исследовать
Прослеживая заголовочные файлы, включенные в signal.h
, я обнаружил, что sigset_t
есть структура, определенная в /usr/include/x86_64-linux-gnu/bits/types/__sigset_t.h
и __sigset_t.h
ed typedef
в /usr/include/x86_64-linux-gnu/bits/types/sigset_t.h
:
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
Сначала я думал, что 1024 бита — это слишком много, но потом наткнулся наэтот ответна другой вопрос unix.stackexchange.com. Затем я решил использовать детали реализации структуры, sigset_t
чтобы напечатать все 1024 бита. Я сделал это в следующем коде, заменив функцию print_set_bin
на функцию print_set_word
, которая печатает все _SIGSET_NWORDS
(= 16) unsigned long int
, выделенные для __val
массива.
void print_set_word(sigset_t *setp)
{
int i;
if (!setp)
{
fprintf(stderr, "print_set_word(): NULL parameter\n");
return;
}
for (i = 0; i < 16; i++)
{
printf("%lu\n", setp->__val[i]);
}
printf("[%s]\n\n", sigisemptyset(setp) ? "Empty" : "Non-empty");
}
Программавыход:
Empty set:
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
[Empty]
Filled set:
18446744067267100671
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
[Non-empty]
After sigprocmask():
0
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
18446744073709551615
[Non-empty]
Примечания:
18446744067267100671
==0b1111111111111111111111111111111001111111111111111111111111111111
(64 бита установлены в 1, за исключением двух; см.ПримечаниевФонраздел)18446744073709551615
==0b1111111111111111111111111111111111111111111111111111111111111111
(64 бита установлены в 1)
Пояснение вывода и вопрос
Как вы можете видеть, sigemptyset()
и sigfillset()
манипулирует всеми 1024 битами набора. Вызов sigemptyset()
вместо sigfillset()
перед вызовом sigprocmask()
показывает, что sigprocmask()
манипулирует только первыми 64 битами (один unsigned long int
), оставляя оставшиеся 1024-64=960 бит нетронутыми! И вот тут возникает долгожданный вопрос: не ошибка ли это? Не следует ли sigprocmask()
записывать во все данные структуры?
решение1
Да, sigprocmask()
работало некорректно!
11-мар-2020 я заполнил новый отчет об ошибке на баг-трекере glibc. Несколько минут назад статус ошибки был изменен на Resolved/Fixed as of glibc version 2.32
.
- Отчет об ошибке:https://sourceware.org/bugzilla/show_bug.cgi?id=25657
- Исправление ошибки (коммит):https://sourceware.org/git/?p=glibc.git;a=commit;h=566e10aa7292bacd74d229ca6f2cd9e8c8ba8748
Большое спасибо разработчикам glibc, принявшим участие в устранении этой ошибки!