CHIP8 в C - Как правильно обрабатывать таймер задержки?
TL;DR Мне нужно эмулировать таймер в C, который позволяет выполнять параллельную запись и чтение, сохраняя при этом постоянное уменьшение при 60 Гц (не точно, но приблизительно точно). Он будет частью эмулятора Linux CHIP8. Использование основанного на потоках подхода с разделяемой памятью и семафорами поднимает некоторые проблемы точности, а также условия гонки в зависимости от того, как основной поток использует таймер.
Какой лучший способ разработать и реализовать такой таймер?
Я пишу интерпретатор Linux CHIP8 на C, модуль за модулем, чтобы погрузиться в мир эмуляции.
Я хочу, чтобы моя реализация была максимально точной со спецификациями. В этом отношении таймеры оказались для меня самыми сложными модулями.
Возьмите, например, таймер задержки. В спецификациях это "специальный" регистр, изначально установленный на 0. Существуют специальные коды операций, которые устанавливают значение и получают его из регистра.
Если значение, отличное от нуля, вводится в регистр, оно автоматически начинает уменьшаться само с частотой 60 Гц, останавливаясь при достижении нуля.
Моя идея относительно его реализации состоит в следующем:
Использование вспомогательной нити, которая выполняет автоматическое уменьшение на частоте около 60 Гц с помощью
nanosleep()
, я используюfork()
создать тему на данный момент.Использование разделяемой памяти через
mmap()
для того, чтобы выделить регистр таймера и сохранить его значение на нем. Этот подход позволяет как вспомогательному, так и основному потоку считывать и записывать в регистр.Использование семафора для синхронизации доступа для обоих потоков. я использую
sem_open()
создать его иsem_wait()
а такжеsem_post()
заблокировать и разблокировать общий ресурс соответственно.
Следующий фрагмент кода иллюстрирует концепцию:
void *p = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
/* Error checking here */
sem_t *mutex = sem_open("timersem", O_CREAT, O_RDWR, 1);
/* Error checking and unlinking */
int *val = (int *) p;
*val = 120; // 2-second delay
pid_t pid = fork();
if (pid == 0) {
// Child process
while (*val > 0) { // Possible race condition
sem_wait(mutex); // Possible loss of frequency depending on main thread code
--(*val); // Safe access
sem_post(mutex);
/* Here it goes the nanosleep() */
}
} else if (pid > 0) {
// Parent process
if (*val == 10) { // Possible race condition
sem_wait(mutex);
*val = 50; // Safe access
sem_post(mutex);
}
}
Потенциальная проблема, которую я вижу с такой реализацией, основана на третьем пункте. Если программа обновляет регистр таймера, как только она достигает значения, отличного от нуля, вспомогательный поток не должен ждать, пока основной поток разблокирует ресурс, иначе задержка в 60 Гц не будет выполнена. Это подразумевает, что оба потока могут свободно обновлять и / или считывать регистр (постоянные записи в случае вспомогательного потока), что, очевидно, вводит условия гонки.
Как только я объяснил, что я делаю и чего я пытаюсь достичь, у меня возникает вопрос:
Каков наилучший способ разработать и эмулировать таймер, который позволяет выполнять одновременную запись и чтение, сохраняя при этом приемлемую фиксированную частоту?
1 ответ
Не используйте для этого потоки и примитивы синхронизации (семафоры, разделяемая память и т. Д.). На самом деле, я бы даже сказал: не используйте потоки ни для чего, если вам явно не нужен многопроцессорный параллелизм. С синхронизацией трудно разобраться, а еще сложнее отладить, если вы ошиблись.
Вместо этого найдите способ реализовать это в одном потоке. Я бы порекомендовал один из двух подходов:
Отслеживайте время, когда последнее значение было записано в регистр таймера. При чтении из регистра вычислите, как давно он был записан, и вычтите соответствующее значение из результата.
Следите за тем, сколько инструкций выполняется в целом, и вычитайте 1 из регистра таймера каждые N инструкций, где N - это большое число, такое, что N инструкций составляет около 1/60 секунды.