#c #polymorphism #embedded #multicore
Вопрос:
Я использую двухъядерное устройство, и для ядра A требуется создать структуру данных, содержащую аргументы для списка функций, выполняемых на ядре B, периодически обновляя его и информируя ядро B. Количество аргументов и типов может изменяться во время выполнения.
Мой план состоял в следующем..
Создайте ParamsInterface
базовый класс, и каждая функция будет иметь свой собственный класс параметров, производный от базового класса. Используя полиморфизм, это позволило бы мне создать вектор ParamsInterface*
в общей памяти и заполнить его из ядра A.
Ядро A заполняет вектор new
, выбирая тот дочерний класс, который ему нужен, и перемещая возвращенный указатель на вектор.
Пример ядра A:
volatile std::vector<ParamsInterface*> sharedParams __attribute__((section(".shared_memory")))
class ParamsInterface
{
public:
virtual ~ParamsInterface(){};
};
class Function1Params: public ParamsInterface
{
public:
float mParam1;
bool mParam2;
};
void populate()
{
Function1Params *params = new Function1Params ;
params->mParam1= 1.1;
params->mParam2 = true;
sharedParams.push_back(params);
}
Затем ядро B должно иметь возможность статически приводить указатель к дочернему типу, поскольку оно знает, какого типа будет каждый элемент вектора. Конечно, доступ контролируется аппаратным семафором, и все указатели удаляются перед обновлением списка.
Пример ядра B:
void process(ParamsInterface* params)
{
Function1Params paramsCasted = static_cast<Function1Params *>(params);
processFunc1(paramsCasted->mParam1, paramsCasted->mParam2);
}
Проблема в том, что, когда ядро B считывает эти общие, некэшированные данные, адрес, на который указывает ParamsInterface*, неверен, и я получаю серьезную ошибку при попытке приведения. Я проверил указатель, созданный ядром A, и он указывает на область памяти, доступную для обоих ядер.
Мой вопрос двоякий: во — первых, делаю ли я здесь что-то неправильно-возможно, ожидаю, что сработает метод, который никогда не сработает? И во-вторых, я упускаю гораздо лучший способ сделать это? Я опустил информацию о том, как работает список функций, чтобы попытаться сделать вопрос кратким.
Большое спасибо за любую помощь.
Редактировать:
Таким образом, похоже, что проблема связана с тем, где создаются объекты ParamsInterface. Ядро A создает их, и они помещаются в кучу, которая, к сожалению, находится в области, которая не является управляемой, и ядро B, по-видимому, не может получить доступ.
Если я использую размещение new
для размещения их в управляемом разделе памяти, я должен указать точный адрес, но это будет очень сложно, так как все объекты ParamsInterface могут быть разного размера.
Итак, есть ли способ создать () «полиморфные» объекты в контейнере в определенном разделе памяти? Или мне нужно написать какой-то пользовательский менеджер пула памяти?
Спасибо
Комментарии:
1. Можете ли вы использовать потоки?
2. Я использую freertos на ядре A и голый металл на ядре B, поэтому я не использую потоки. Я ценю , что не поместил много информации о параллелизме или разделе данных, но я уверен, что это не проблема. Мне больше любопытно, является ли это правильным способом решения проблемы. Спасибо
3. Это похоже на «проблему xy». Ни одна из вещей, о которых вы упоминаете, ни в малейшей степени не связана с какой бы то ни было реальной задачей. Вы должны сосредоточиться на написании кода, который делает то, что должен, а не на мета-программировании. Кроме того, распределение кучи часто бессмысленно использовать во встроенных системах, особенно в «голом металле» /RTOS.
Ответ №1:
Похоже, вы подходите к коду на C с очень похожей на C точки зрения. Они очень разные не только в синтаксисе.
Код C интерпретируется изолированно, без аппаратного контекста, и компиляторы не обязаны уважать то, что вы пишете, кроме обеспечения сохранения побочных эффектов, создаваемых вашим кодом. Это известно как правило «как если бы».
Это распространяется на доступ к памяти в многопоточных и многопроцессорных средах. Если не указано иное, компиляторам разрешается предполагать, что память будет изменяться и считываться только тем кодом, который они компилируют, и что никакие внешние факторы не задействованы.
Вот конкретный пример:
int* sharedParams[12] __attribute__((section(".shared_memory")));
void populate()
{
auto params = new int{12};
sharedParams[4] = params;
}
int main() {
populate();
while(1) {}
}
Если я скомпилирую это с gcc -O3 -flto
помощью , я получу следующую программу:
jmp 401020 <main>
cs nop WORD PTR [rax rax*1 0x0]
nop DWORD PTR [rax 0x0]
sharedParams
Массив даже больше не существует! смотрите на godbolt
Имея это в виду, следующее:
Конечно, доступ контролируется аппаратным семафором.
этого недостаточно. Язык C не знает об этом, и синхронизация между объектами между потоками/ядрами должна выполняться как на уровне языка, так и на аппаратном уровне. Если вы этого не сделаете, компилятору будет разрешено делать различные предположения о том, как используется память, и он многое оптимизирует.
Есть два способа решить эту проблему:
- Используйте конструкции на уровне языка, такие как различные утилиты, присутствующие в стандартной библиотеке.
std::atomic_thread_fence
вероятно, это то, что вам здесь нужно. Смотрите на godbolt
void populate()
{
int* params = new int{12};
sharedParams[4] = params;
std::atomic_thread_fence( std::memory_order_release ); // std::memory_order_acquire when reading
}
N. B. std::atomic_thread_fence на самом деле не очень подходит для этого варианта использования, но поскольку вы уже работаете за пределами языка. Здесь это должно соответствовать требованиям, но формальной гарантии не будет.
- Отметьте спорную память
volatile
так, чтобы компилятор перестал делать предположения о том, когда и как она может измениться. Смотрите на godbolt
volatile int* sharedParams[12] __attribute__((section(".shared_memory")));
void populate()
{
int volatile * params = new volatile int{12};
sharedParams[4] = params;
}
Первый вариант значительно предпочтительнее. Хотя это не так жестоко, как полный барьер памяти при каждом доступе, позднее сильно свяжет руки компилятору. volatile
также не дает гарантий, что все будет происходить в правильном порядке.
дальнейшее чтение по адресу: https://en.cppreference.com/w/cpp/atomic/memory_order
Комментарии:
1. Спасибо за ответ, Фрэнк. Вы правы в том, что я был программистом на C дольше, чем на C .. На самом деле общие данные представляют собой структуру, некоторые члены которой объявлены изменчивыми, но не все. Кроме того, функция populate() на самом деле является функцией класса, и, несмотря на то, что она является одноэлементной, я не могу ссылаться в ней на изменчивый var. Поэтому я буду изучать atomic_thread_fence по этой причине. Мой вопрос также касается того, как я использовал полиморфизм и векторы для обмена данными, которые могут изменять тип и размер, могу ли я предположить, что это разумный способ сделать это после решения вышеуказанных проблем? Спасибо
2.
while(1) {}
кстати, это УБ.3. На самом деле, 1 последующий вопрос.. Будет ли простое использование release и получение atomic_thread_fence препятствовать оптимизации компилятором общих параметров? Большое спасибо
4. @mttjcksn Во-первых, оптимизация-это просто особенно сильный симптом, есть куча других возможных проблем. Что касается atomic_thred_fence, то это сложно. Если вы используете его правильно , с вами все будет в порядке. Однако теоретически, поскольку вы действуете за пределами языка, формально гарантируется, что ничего, кроме изменчивости, не будет работать.