[Перевод] Эффективное взаимодействие между нативными процессами Arduino и Linux
Используя скетчи Arduino в работе с платами Intel Galileo и Intel Edison, вы можете столкнуться с ситуацией, когда вам потребуется добавить дополнительную функциональность, задействовав набор Yocto для разработки встраиваемых систем на основе Linux OS. И здесь нам предстоит решить задачу, о которой мы уже упомянули в заголовке нашего поста: как наладить эффективное «общение» между этими двумя мирами.
Давайте определим некоторые критерии, которые нам при этом нужно учитывать:
Критерии
1. Отсутствие обмена данными через диск (SD-карта, eMMC) для уменьшения износа диска и повышения производительности.
2. Коммуникация вызывается исключительно какими-либо событиями (в частности, нам не нужно периодически проверять статус, но мы хотим получать уведомление о том или ином событии, в противном случае — не активировать обмен данными).
Межпроцессное взаимодействие (IPC) в Linux
Программа, созданная в среде Arduino (скетч) и запущенная на Intel Galileo или Intel Edison, — это Linux-процесс, выполняемый параллельно с другими подобными процессами. Так как мы имеем дело с полноценной системой Linux, запущенной на данных платах, возможно также использовать и стандартные средства для межпроцессное взаимодействия (IPC) между процессами Arduino и нативными процессами. Для Linux существуют различные методы IPC. Один из них — это «отображенное в память IPC». В сущности, это означает, что IPC-процессы делят между собой одну и ту же область памяти. В свою очередь, это значит, что любые модификации, сделанные посредством одного процесса, который использует определенную область памяти, сразу же становятся видимыми для всех других процессов. Подобное поведение удовлетворяет нашему первому критерию: в ходе передачи данных мы работаем с последними исключительно в памяти, они не пишутся на диск.
Мьютексы и условные переменные
Использование разделяемой памяти сразу поднимает пару вопросов, таких как:
1. Как убедиться в том, что в разделяемых данных в какое-то определенное время выполняется только один процесс (синхронизация)?
2. Как моментально оповестить другой процесс (или процессы) об изменении данных (уведомление)?
Ниже мы поочередно рассмотрим эти два вопроса. Мы сделаем возможным использование «мьютексов» и «условных переменных», которые включены в библиотеку потоков POSIX (Pthreads), доступной для системы Linux.
Синхронизация — Мьютекс
Взаимное исключение, или мьютекс (mutex) — это стандартный принцип, который реализован, пожалуй, в любой современной многозадачной ОС. В этом посте мы не будем описывать все принципы и детали, а лишь поделимся конкретной информацией относительно мьютексов из набора потоков POSIX (Pthreads). Для того, чтобы узнать больше информации, обратитесь к бумажным изданиям (например, Tanenbaum, Woodhull: Operating Systems 3rd ed. Pearson 2006) или воспользуйтесь поиском в сети Интернет.
Как следует из названия библиотеки, набор Pthreads в основном предназначен для потокового программирования. В то же время он также предлагает мощные инструменты, применимые к управлению процессами. Более того, среда разработки Arduino IDE для плат Intel Galileo и Intel Edison поддерживает библиотеку Pthreads (то есть ссылки на эту библиотеку) из коробки, таким образом предлагая легко реализуемую интеграцию. Следовательно, использование Pthreads для наших целей выглядит вполне логичным решением.
Как правило, мьютекс следит за тем, чтобы доступ к определенной критической секции кода получал лишь один поток. Так как мы здесь разбираемся с процессами, давайте используем мьютексы таким образом, чтобы только один процесс мог получить доступ к запросу pthread_mutex_lock в коде. Все остальные процессы будут переведены в спящий режим посредством самой ОС — ровно до того момента, как мьютекс получит команду pthread_mutex_unlock, после чего ОС разбудит все другие процессы, запрашивая pthread_mutex_lock.
Псевдокод:
pthread_mutex_lock(&mutex);
// read / write shared memory data here
pthread_mutex_unlock(&mutex);
Это должно быть сделано одним и тем же способом как для блокирования записи, так и блокирования доступа к чтению. В противном случае в процессе чтения может быть получен доступ к частично обновленным данным. В следующем разделе мы расскажем о принципах работы элементов Pthreads, которые позволяют получить уведомление в случае изменения данных.
Уведомление — Условная переменная
Подобно принципу, когда процесс пытается получить доступ к заблокированному мьютексу, библиотека Pthreads также включает условные переменные. Условная переменная позволяет потоку или (в нашем случае) процессу запросить разрешение заснуть до тех пор, пока не будет разбужен. Это реализуется посредством функции pthread_cond_wait.
Мьютекс и условная переменная в сочетании дают следующий псевдокод:
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond_variable, &mutex);
// read shared memory data here
pthread_mutex_unlock(&mutex);
Другому процессу необходимо разблокировать мьютекс и послать сигнал об изменении, выполнив вызов pthread_cond_signal. Это выведет процесс из спящего режима.
Больше информации вы можете найти в специализированных изданиях или на соответствующих онлайн-ресурсах. В следующем разделе представлен пример реализации кода.
Реализация
Некоторые пояснения:
Примечание:
Пример кода ниже поставляется по MIT License. Ниже вы найдете три файла:
- mmap.ino: скетч для запуска в среде разработке Arduino IDE
- mmap.cpp: нативный процесс, отвечающий за отправку данных
- mmap.h: файл заголовка — файл для использования в Arduino IDE, а также в Linux
То есть на стороне Arduino у вас должна быть папка, которая содержит файлы mmap.ino и mmap.h в директории скетчей Arduino. В Linux следует использовать папку, содержащую файлы mmap.cpp и mmap.р.
Для того чтобы запустить скетч, откройте программу mmap в среде разработки Arduino IDE и загрузите ее в соответствующую плату (Intel Galileo Gen 1, Intel Galileo Gen 2 или Intel Edison). Пакет для разработчиков Intel IoT поставляется с кросс-компилятором. Кроме того, платформа Yocto, которая идет в комплекте с Intel Edison, равно как с образом Yocto для SD-карты, или же с платой Intel Galileo, поставляется с предустановленным компилятором С++. Для запуска предустановленного компилятора вам нужно выполнить:
g++ mmap.cpp -lpthread -o mmap
В папку следует поместить файлы mmap.cpp и mmap.h. Это позволит сгенерировать бинарный файл, который можно запустить следующим образом:
./mmap {0,1}{0,1}
…где »{0,1}» выступает для 0 или 1. Например, выражение »./mmap 00» выключит оба индикатора, в то время как команда ./mmap 00 их включит. Некоторая дополнительная информация выводится в монитор последовательного интерфейса (Ctrl + Shift + M).
mmap.ino
/*
* Author: Matthias Hahn
* Copyright (C) 2014 Intel Corporation
* This file is part of mmap IPC sample provided under the MIT license
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "mmap.h"
using namespace std;
/* assume /tmp mounted on /tmpfs -> all operation in memory */
/* we can use just any file in tmpfs. assert(file size not modified && file permissions left readable) */
struct mmapData* p_mmapData; // here our mmapped data will be accessed
int led8 = 8;
int led13 = 13;
void exitError(const char* errMsg) {
/* print to the serial Arduino is attached to, i.e. /dev/ttyGS0 */
string s_cmd("echo 'error: ");
s_cmd = s_cmd + errMsg + " - exiting' > /dev/ttyGS0";
system(s_cmd.c_str());
exit(EXIT_FAILURE);
}
void setup() {
int fd_mmapFile; // file descriptor for memory mapped file
/* open file and mmap mmapData*/
fd_mmapFile = open(mmapFilePath, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd_mmapFile == -1) exitError("couldn't open mmap file");
/* make the file the right size - exit if this fails*/
if (ftruncate(fd_mmapFile, sizeof(struct mmapData)) == -1) exitError("couldn' modify mmap file");
/* memory map the file to the data */
/* assert(filesize not modified during execution) */
p_mmapData = static_cast(mmap(NULL, sizeof(struct mmapData), PROT_READ | PROT_WRITE, MAP_SHARED, fd_mmapFile, 0));
if (p_mmapData == MAP_FAILED) exitError("couldn't mmap");
/* initialize mutex */
pthread_mutexattr_t mutexattr;
if (pthread_mutexattr_init(&mutexattr) == -1) exitError("pthread_mutexattr_init");
if (pthread_mutexattr_setrobust(&mutexattr, PTHREAD_MUTEX_ROBUST) == -1) exitError("pthread_mutexattr_setrobust");
if (pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED) == -1) exitError("pthread_mutexattr_setpshared");
if (pthread_mutex_init(&(p_mmapData->mutex), &mutexattr) == -1) exitError("pthread_mutex_init");
/* initialize condition variable */
pthread_condattr_t condattr;
if (pthread_condattr_init(&condattr) == -1) exitError("pthread_condattr_init");
if (pthread_condattr_setpshared(&condattr, PTHREAD_PROCESS_SHARED) == -1) exitError("pthread_condattr_setpshared");
if (pthread_cond_init(&(p_mmapData->cond), &condattr) == -1) exitError("pthread_mutex_init");
/* for this test we just use 2 LEDs */
pinMode(led8, OUTPUT);
pinMode(led13, OUTPUT);
}
void loop() {
/* block until we are signalled from native code */
if (pthread_mutex_lock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_lock");
if (pthread_cond_wait(&(p_mmapData->cond), &(p_mmapData->mutex)) != 0) exitError("pthread_cond_wait");
if (p_mmapData->led8_on) {
system("echo 8:1 > /dev/ttyGS0");
digitalWrite(led8, HIGH);
}
else {
system("echo 8:0 > /dev/ttyGS0");
digitalWrite(led8, LOW);
}
if (p_mmapData->led13_on) {
system("echo 13:1 > /dev/ttyGS0");
digitalWrite(led13, HIGH);
}
else {
system("echo 13:0 > /dev/ttyGS0");
digitalWrite(led13, LOW);
}
if (pthread_mutex_unlock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_unlock");
}
mmap.cpp
/*
* Author: Matthias Hahn
* Copyright (C) 2014 Intel Corporation
* This file is part of mmap IPC sample provided under the MIT license
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/* mmap.cpp
Linux native program communicating via memory mapped data with Arduino sketch.
Compilation: g++ mmap.cpp -lpthread -o mmap
Run: ./mmap (e.g. ./mmap 01 -> LED 8 off, LED 13 on)
For "random" blink you may run following commands in the command line:
while [ 1 ]; do ./mmap $(($RANDOM % 2))$(($RANDOM % 2)); done
*/
#include "mmap.h"
void exitError(const char* errMsg) {
perror(errMsg);
exit(EXIT_FAILURE);
}
using namespace std;
/**
* @brief: for this example uses a binary string ""; e.g. "11": both leds on
* if no arg equals "00"
* For "random" blink you may run following commands in the command line:
* while [ 1 ]; do ./mmap $(($RANDOM % 2))$(($RANDOM % 2)); done
*/
int main(int argc, char** argv) {
struct mmapData* p_mmapData; // here our mmapped data will be accessed
int fd_mmapFile; // file descriptor for memory mapped file
/* Create shared memory object and set its size */
fd_mmapFile = open(mmapFilePath, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd_mmapFile == -1) exitError("fd error; check errno for details");
/* Map shared memory object read-writable */
p_mmapData = static_cast(mmap(NULL, sizeof(struct mmapData), PROT_READ | PROT_WRITE, MAP_SHARED, fd_mmapFile, 0));
if (p_mmapData == MAP_FAILED) exitError("mmap error");
/* the Arduino sketch might still be reading - by locking this program will be blocked until the mutex is unlocked from the reading sketch
* in order to prevent race conditions */
if (pthread_mutex_lock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_lock");
if (argc == 1) {
cout << "8:0" << endl;
cout << "13:0" << endl;
p_mmapData->led8_on = false;
p_mmapData->led13_on = false;
}
else if (argc > 1) {
// assert(correct string given)
int binNr = atol(argv[1]);
if (binNr >= 10) {
cout << "8:1" << endl;
p_mmapData->led8_on = true;
}
else {
cout << "8:0" << endl;
p_mmapData->led8_on = false;
}
binNr %= 10;
if (binNr == 1) {
cout << "13:1" << endl;
p_mmapData->led13_on = true;
}
else {
cout << "13:0" << endl;
p_mmapData->led13_on = false;
}
}
// signal to waiting thread
if (pthread_mutex_unlock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_unlock");
if (pthread_cond_signal(&(p_mmapData->cond)) != 0) exitError("pthread_cond_signal");
}
mmap.h
/*
* Author: Matthias Hahn
* Copyright (C) 2014 Intel Corporation
* This file is part of mmap IPC sample provided under the MIT license
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef MMAP_HPP
#define MMAP_HPP
#include
#include
#include
#include
#include
#include
#include
#include
/* assert(/tmp mounted to tmpfs, i.e. resides in RAM) */
/* just use any file in /tmp */
static const char* mmapFilePath = "/tmp/arduino";
struct mmapData {
bool led8_on; // led on IO8
bool led13_on; // built-in led
pthread_mutex_t mutex;
pthread_cond_t cond;
};
#endif
Спасибо за внимание!