
В повседневной практике разработчика встраиваемых систем приходится сталкиваться с необходимостью запуска двух и более разноплановых ОС на n-ядерных системах на кристалле. Это, как правило, Linux и специализированная RTOS. На плечи Linux ложится работа с тяжеловесными стеками протоколов, а RTOS же занимается задачами реального времени.
Одна из основных задач, которая встает при такой организации системы — обеспечение механизма взаимодействия, то есть межъядерный обмен данными. Если вам интересно узнать один из вариантов решения на базе открытой библиотеки OpenMCAPI, пролистать пару десятков строк программного кода и увидеть реальные цифры пропускной способности при использовании этой библиотеки, добро пожаловать под кат.
Задача межъядерного обмена данными успешно решается за счет использования разделяемой памяти и межъядерных прерываний с написанием своей прослойки взаимодействия и портированием ее на различные ОС. Для приведения такого API к стандартизированному виду Multicore Association (MCA) разработала и выпустила в свет первую версию спецификации MCAPI (Multicore Communications API), вскоре была выпущена и вторая версия.
Рассматриваемая библиотека OpenMCAPI основана на спецификации MCAPI 2.0, разработана компанией Mentor Graphics Corporation и имеет открытый исходный код под свободной лицензией BSD/GPL. Исходные коды можно получить, воспользовавшись сайтом проекта, там же находится краткая информация по запуску и портированию.
Библиотека OpenMCAPI изначально предоставляет возможность работы под управлением ОС Linux с использованием виртуального транспорта либо разделяемой памяти (но только на платформах mpc85xx и mv78xx0).
Предлагаемая структура взаимодействия Linux и RTOS через OpenMCAPI c разделением на абстрактные уровни имеет следующий вид (см. рис 1):

Рисунок. 1. Структура взаимодействия Linux и RTOS через OpenMCAPI
Рассмотрим реализацию приведенной структуры на примерах исходного кода порта для Linux:
- MCAPI Generic — реализация внешнего MCAPI API.
- OS Layer — часть уровня MCAPI Generic, который содержит код, зависящий от операционной системы. Эта часть представлена в файле libmcapi/mcapi/linux/mcapi_os.c и содержит реализацию:
- Transport Generic — это уровень абстракции, который предоставляет механизм для работы с разделяемой памятью на уровне пользовательского пространства (userspace). Он представлен файлами libmcapi/shm/shm.c и libmcapi/shm/linux/shm_os.c и содержит реализацию:
- OS Specific Driver представлен модулем ядра Linux, обеспечивающим прямой доступ к оборудованию из пользовательского пространства. Модуль находится в папке libmcapi/shm/linux/kmod и содержит реализацию:
Для полного понимания механизма взаимодействия через транспорт, использующий разделяемую память, который реализован в библиотеке OpenMCAPI, необходимо рассмотреть механизм межъядерного сигнализирования и структуру данных в разделяемой памяти.
Дальнейшее рассмотрение будет проводиться на основе платформы mpc85xx (чип P1020 компании Freescale). Программное обеспечение: ядро Linux версии 2.6.35 с патчами, которое поставляется с комплектом средств разработки (SDK) Freescale QorIQ_SDK_V1_03 (доступен для скачивания после регистрации на их сайте), в качестве операционной системы реального времени (RTOS) использована RTEMS, исходные коды которой можно получить в git-репозиторие по ссылке git://git.rtems.org/rtems.git.
Для реализации межъядерного сигнализирования Freescale предоставляет как минимум два механизма:
- Interprocessor Interrupts (IPIs) — межъядерные прерывания, до 4 штук с поддержкой мультикастовых прерываний.
- Message Interrupts (MSGRs) — межъядерные 32-битные сообщения с генерированием прерывания при записи сообщения в регистр, до 8 штук.
В библиотеке оpenmcapi при реализации OS Specific Driver для этой платформы используется механизм MSGRs.
Рассмотрим структуру данных, содержащуюся в разделяемой памяти (см. рис. 2):

Рисунок 2. Структура данных в разделяемой памяти
Область разделяемой памяти по использованию пространству можно разбить на два блока:
- Первый блок — область SHM_MGMT_BLOCK, представлена структурой:
/* SM driver mamagement block */ struct _shm_drv_mgmt_struct_ { shm_lock shm_init_lock; mcapi_uint32_t shm_init_field; struct _shm_route_ shm_routes[CONFIG_SHM_NR_NODES]; struct _shm_buff_desc_q_ shm_queues[CONFIG_SHM_NR_NODES]; struct _shm_buff_mgmt_blk_ shm_buff_mgmt_blk; };
Структура содержит следующие элементы:
- Глобальная блокировка разделяемой памяти — shm_init_lock, используемая для разграничения доступа n-ядер к разделяемой (shared) области.
- Переменная shm_init_field содержит ключ окончания инициализации мастера, принимает значение SHM_INIT_COMPLETE_KEY по окончанию инициализации.
- Shm_routes — таблица маршрутизации со связями межъядерных сообщений, содержит CONFIG_SHM_NR_NODES связей по числу участвующих в обмене ядер (узлов). В нашем случае 2 узла.
- Shm_queues — очереди сообщений с привязкой к конкретному узлу, содержит CONFIG_SHM_NR_NODES. В нашем случае 2 очереди.
- Shm_buff_mgmt_blk — структура управления буферами (SHM_BUFFER) в области данных.
- Вторая область — область данных, содержит SHM_BUFF_COUNT (по умолчанию 128) структур SHM_BUFFER. Эта область служит непосредственно для хранения передаваемых данных. Структура SHM_BUFFER состоит из массива размером MCAPI_MAX_DATA_LEN и дополнительной структуры управления элементом.
Перед тем как рассмотреть процесс портирования необходимо привести блок-схему работы низкоуровневого механизма коммуникации через разделяемую память (механизм реализован в OpenMCAPI, см. рис. 3):

Рисунок. 3. SDL-диаграммы низкоуровневого механизма коммуникации через разделяемую память (реализован в OpenMCAPI).
Некоторые пояснения к диаграммам:
- “HW-Notification” — диаграмма описывает процесс отсылки уведомления удаленному или текущему ядру:
- Вызов функции принимает id ядра, для которого предназначено сообщение (функция openmcapi_shm_notify).
- Если уведомление предназначено для удаленного ядра “target id”, то генерируется удаленное сообщение через механизм MSGRs (рассмотрен выше) со значением в поле данных равным единице (см. блок 3), иначе происходит явный вызов обработчика прерывания interrupt_handle (диаграмма “HW-Receive”), см. блок 4.
- “HW-Receive” — диаграмма описывает процесс приема уведомления от удаленного или текущего ядра:
- Interrupt_handle является обработчиком прерывания, настроенным на срабатывание при приеме сообщения по MSGRs, используется также для явного вызова.
- В блоках 2—6 организована проверка статуса всех сообщений MSGRs, если поле данных MSGR-сообщения не равно 0, происходит разблокировка потока, который производит обработку данных в области разделяемой памяти.
- В блоках 7—8 происходит проверка места вызова обработчика прерывания (“interrupt_handler”). Если вызов был в прерывании, сбрасывается флаг присутствия сообщения MSGR.
Перед тем как перейти к описанию портирования для RTEMS, вкратце рассмотрим эту ОС.

RTEMS (Real-Time Executive for Multiprocessor Systems) — это RTOS c открытым исходным кодом, полнофункциональная операционная система реального времени с поддержкой множества открытых стандартных интерфейсов прикладного программирования (API), стандартов POSIX и BSD-сокетов. Она предназначена для использования в космических, медицинских, сетевых и многих других встраиваемых устройствах. RTEMS содержит широкий набор процессорных архитектур, таких как ARM, PowerPC, Intel, Blackfin, MIPS, Microblaze и др. Содержит большой стек реализованных сетевых протоколов, в частности tcp/ip, http, ftp, telnet. Предоставляет стандартизированный доступ к RTC, NAND, UART и другому оборудованию.
Перейдем к процессу портирования OpenMCAPI. Исходя из документа, расположенного по ссылке [1] требуется:
- Реализовать OS Layer, файлы:
- libmcapi/mcapi/rtems/mcapi_os.c;
- libmcapi /include/rtems/mgc_mcapi_impl_os.h.
- Реализовать поддержку совместимого транспорта разделяемой памяти, файл:
- libmcapi/shm/rtems/shm_os.c.
- Добавить рецепты в waf-сборщик, который используется для OpenMCAPI.
Так как целевая платформа P1020 (powerpc, 500v2) и портирование проводилось на RTOS, где допускается отсутствие разделения пространства kernel/user space, отпадает необходимость в написании:
- libmcapi/include/arch/powerpc/atomic.h;
- libmcapi/shm/rtems/kmod/.
Также отпадает необходимость в реализации OS Layer, так как RTEMS поддерживает POSIX-совместимые вызовы, файлы mcapi_os.c и mgc_mcapi_impl_os.h просто были скопированы из реализации для Linux.
Реализация транспорта разделяемой памяти полностью выполнена в файле shm_os.c и включает адаптацию вызовов из уровня абстракции Transport Generic (файл libmcapi/shm/shm.c) и реализацию механизма обмена через MSGRs.
Функции, требующие реализации:
1) mcapi_status_t openmcapi_shm_notify (mcapi_uint32_t unit_id, mcapi_uint32_t node_id) — функция отправляет нотификацию удаленному ядру(ам), реализация представлена диаграммой (см. рис 3). Исходный код приведен ниже:
/* send notify remote core */ mcapi_status_t openmcapi_shm_notify(mcapi_uint32_t unit_id, mcapi_uint32_t node_id) { mcapi_status_t mcapi_status = MCAPI_SUCCESS; int rc; rc = shm_rtems_notify(unit_id); if (rc) { mcapi_status = MGC_MCAPI_ERR_NOT_CONNECTED; } return mcapi_status; } static inline int shm_rtems_notify(const mcomm_core_t target_core) { struct mcomm_qoriq_data *const data = &mcomm_qoriq_data; /* If the target is the local core, call the interrupt handler directly. */ if (target_core == mcomm_qoriq_cpuid()) { _mcomm_interrupt_handler(NO_IRQ, data); } else { mcomm_qoriq_notify(target_core); } return 0; } /* Wake up the process(es) corresponding to the mailbox(es) which just received * packets. */ static int _mcomm_interrupt_handler(rtems_vector_number irq, struct mcomm_qoriq_data *data) { register int i; void *mbox = data->mbox_mapped; for (i = 0; i < data->nr_mboxes; i++) { int active; switch (data->mbox_size) { case 1: active = readb(mbox); break; case 4: active = readl(mbox); break; default: active = 0; } if (active) { LOG_DEBUG("%s: waking mbox %d\n", __func__, i); (void) rtems_event_send( data->rid, MMCAPI_RX_PENDING_EVENT ); } mbox += data->mbox_stride; } if (irq != NO_IRQ) { mcomm_qoriq_ack(); } return 0; }
2) mcapi_uint32_t openmcapi_shm_schedunitid(void) — функция возвращает номер текущего ядра (то есть ядра, исполняющего этот код), реализуется тривиально чтением регистра процессора. Исходный код приведен ниже:
/* Get current cpu id */ mcapi_uint32_t openmcapi_shm_schedunitid(void) { return (mcapi_uint32_t) ppc_processor_id(); }
3) mcapi_status_t openmcapi_shm_os_init(void) — функция создает и запускает низкоуровневый поток приема данных, реализуется посредством вызова функций rtems_task_create и rtems_task_start. Исходный код приведен ниже:
/* Now that SM_Mgmt_Blk has been initialized, we can start the RX thread. */ mcapi_status_t openmcapi_shm_os_init(void) { struct mcomm_qoriq_data *const data = &mcomm_qoriq_data; rtems_id id; rtems_status_code sc; if( RTEMS_SELF != data->rid ) { return MCAPI_ERR_GENERAL; } sc = rtems_task_create( rtems_build_name( 'S', 'M', 'C', 'A' ), MMCAPI_RX_TASK_PRIORITY, RTEMS_MINIMUM_STACK_SIZE, RTEMS_DEFAULT_MODES, RTEMS_DEFAULT_ATTRIBUTES, &id); if( RTEMS_SUCCESSFUL != sc ) { return MCAPI_ERR_GENERAL; } /* global save task id */ data->rid = id; sc = rtems_task_start( id, mcapi_receive_thread, 0 ); if( RTEMS_SUCCESSFUL != sc ) { perror( "rtems_task_start\n" ); return MCAPI_ERR_GENERAL; }; return MCAPI_SUCCESS; } static rtems_task mcapi_receive_thread(rtems_task_argument argument) { int rc; do { rc = shm_rtems_wait_notify(MCAPI_Node_ID); if (rc < 0) { perror("shm_rtems_wait_notify"); break; } MCAPI_Lock_RX_Queue(); /* Process the incoming data. */ shm_poll(); MCAPI_Unlock_RX_Queue(0); } while (1); printk("%s exiting!\n", __func__); } static inline int shm_rtems_wait_notify(const mcapi_uint32_t unitId) { rtems_event_set event_out; int ret = 0; while(1) { LOG_DEBUG("mcomm_mbox_pending start\n"); (void) rtems_event_receive( MMCAPI_RX_PENDING_EVENT, RTEMS_DEFAULT_OPTIONS, RTEMS_NO_TIMEOUT, &event_out ); LOG_DEBUG("rtems_event_receive\n"); ret = mcomm_mbox_pending(&mcomm_qoriq_data, (mcomm_mbox_t)unitId); LOG_DEBUG("mcomm_mbox_pending end ret=%d\n", ret); if(ret != 0) { return ret; }; } return 0; }
4) mcapi_status_t openmcapi_shm_os_finalize(void) — функция останавливает низкоуровневый поток приема данных, реализуется посредством вызова функции rtems_task_delete. Исходный код приведен ниже:
/* Finalize the SM driver OS specific layer. */ mcapi_status_t openmcapi_shm_os_finalize(void) { struct mcomm_qoriq_data *const data = &mcomm_qoriq_data; rtems_id id = data->rid; rtems_status_code sc; sc = rtems_task_delete(id); if( RTEMS_SUCCESSFUL != sc ) { return MCAPI_ERR_GENERAL; } return MCAPI_SUCCESS; }
5) void *openmcapi_shm_map(void) — функция подготовки и настройка интерфейса MSGRs, подготовка разделяемой памяти. Исходный код приведен ниже:
/* full open mcom device and get memory map addres*/ void *openmcapi_shm_map(void) { void *shm; int rc; size_t shm_bytes; // low level init // mcomm_qiroq_probe(); shm_bytes = shm_rtems_read_size(); if (shm_bytes <= 0) { perror("read shared memory size\n"); return NULL; } /* initialized device. */ rc = shm_rtems_init_device(); if (rc < 0) { perror("couldn't initialize device\n"); goto out; } shm = shm_rtems_read_addr(); if (shm == NULL) { perror("mmap shared memory"); goto out; } return shm; out: return NULL; } static size_t shm_rtems_read_size(void) { struct mcomm_qoriq_data *const data = &mcomm_qoriq_data; return (size_t) (data->mem.end - data->mem.start); } static inline int shm_rtems_init_device(void) { struct _shm_drv_mgmt_struct_ *mgmt = NULL; /* xmmm */ return mcomm_dev_initialize(&mcomm_qoriq_data, (uint32_t)&mgmt->shm_queues[0].count, CONFIG_SHM_NR_NODES, sizeof(mgmt->shm_queues[0].count), ((void *)&mgmt->shm_queues[1].count - (void *)&mgmt->shm_queues[0].count)); } static void *shm_rtems_read_addr(void) { struct mcomm_qoriq_data *const data = &mcomm_qoriq_data; return (void*)data->mem.start; }
6. void openmcapi_shm_unmap(void *shm) — функция закрывает интерфейс MSGRs, отменяет использование разделяемой памяти. Исходный код приведен ниже:
/* full close mcom device and revert memory */ void openmcapi_shm_unmap(void *shm) { /* deinitialized device. */ shm_rtems_deinit_device(); // low level deinit // mcomm_qoriq_remove(); } static inline int shm_rtems_deinit_device(void) { return mcomm_dev_finalize(&mcomm_qoriq_data); }
Отдельно следует рассмотреть реализацию функции низкоуровневого потока приема mcapi_receive_thread (исходный код см. выше). При запуске потока вызовом функции rtems_event_receive он переводится в режим ожидания события (реализуется доступным в RTEMS механизмом событий). Далее при приходе события запуска, отсылаемого в обработчике interrupt_handler (см. рис. 3, диаграмма “HW-Receive”), происходит обработка изменений в области разделяемой памяти (вызов внутренней функции openmcapi — shm_poll()), c ее предварительной блокировкой, после чего поток возвращается в состояние ожидания.
Ниже приводятся результаты, полученные при взаимодействии Linux и RTEMS через OpenMCAPI. Тестовый стенд представляет собой отладочная плата от Freescale P1020RDB-PB с установленным процессором P1020 (2 ядра). Частоты: частота ядра — 800 МГц, DDR2 — 400 МГц, CCB — 400 МГц. На ядрах 0/1 были запущены соответственно Linux/RTEMS. Обмен был двухсторонним, замерялось время, затраченное на 10000 двухсторонних посылок. Результаты тестов сведены в таблицу:
| № |
Описание теста |
Времени на одну посылку, мкс |
| 1 |
Симметричные пакеты размером 512 байт |
37,5 |
| 2 |
Симметричные пакеты размером 52430 байт |
121 |
| 3 |
Симметричные пакеты размером 100 кБайт |
346 |
| 4 |
Ассиметричные пакеты размерами 1к/100к-linux/rtems |
185 |
Из всего выше изложенного можно сделать вывод, что библиотека OpenMCAPI предоставляет собой достойный вариант реализации спецификации MCAPI, имеющей четкую структуру исходного кода, облегчающую портирование; наглядные примеры портирования (платформы powerpc и arm); свободную лицензию и производительность, достаточную для большинства применений.
[!?] Вопросы и комментарии приветствуются. На них будет отвечать автор статьи Руслан Филипович, программист дизайн-центра электроники Promwad.
