6.4.2 Эмуляция интерфейсов задачи RTOS |
Предыдущая Содержание Следующая |
Давайте посмотрим на довольно сложную часть OSPL, связь один-ко-многим между API RTOS и API Linux. Возьмём в качестве примера API RTOS для создание и уничтожения задачи. Прототипами API в нашей RTOS для создания и уничтожения задачи являются:
rtosError_t rtosCreateTask (char *name, <-- имя задачи rtosEntry_t routine, <-- точка входа char * arg1, <-- аргумент для точки входа int arg, <-- аргумент для точки входа void * arg3, <-- аргумент для точки входа int priority, <-- приоритет новой задачи int stackSize, <-- размер стека rtosTask_t *tHandle); <-- описание задачи
Функция rtosCreateTask порождает новую задачу, которая начинает своё выполнение с функции routine. Приоритет новой задачи устанавливается с помощью аргумента priority. В случае успешного завершения функция возвращает RTOS_OK и сохраняет описание задачи для созданной задачи в аргументе tHandle.
void rtosDeleteTask(rtosTask_t tHandle <-- описание задачи );
Функция rtosDeleteTask завершает выполнение задачи tHandle и ждёт её завершения. Когда функция rtosDeleteTask возвращается, это гарантирует, что задача tHandle мертва. Сначала мы обсудим реализацию этих API в пользовательском пространстве, а затем реализацию в ядре.
Эмуляция интерфейсов задачи в пользовательском пространстве
Перед переходом к деталям реализации rtosCreateTask и rtosDeleteTask, давайте обсудим два участвующих типа данных: rtosEntry_t и rtosTask_t. rtosEntry_t определён в ospl.h и он должен быть использован без переопределения, как в нашей реализации. Изменение этого типа будет означать изменение описания всех функций, основанных на этом типе, что мы хотим избежать. rtosEntry_t определяется как
typedef void (*rtosEntry_t) (char *arg1, int arg2, void *arg3);
Внутренности rtosTask_t понятны только интерфейсам задач. Для других API это просто непрозрачный тип данных. Так как мы реализуем API задач RTOS с помощью API Linux, мы имеем свободу переопределить этот тип в соответствии с нашими потребностями. Новое определение в файле ospl.h:
typedef struct { char name[100]; <-- имя задачи pthread_t thread_id; <-- идентификатор потока }rtosTask_t;
Сначала мы обсудим rtosCreateTask.
rtosError_t rtosCreateTask(char *name, rtosEntry_t routine, char * arg1, int arg2, void *arg3, int priority, int stackSize, rtosTask_t *tHandle){ #ifndef __KERNEL__
int err; uarg_t uarg; strcpy(tHandle->name, name); uarg.entry_routine = routine; uarg.priority = priority; uarg.arg1 = arg1; uarg.arg2 = arg2; uarg.arg3 = arg3; err = pthread_create (&tHandle->thread_id, NULL, wrapper_routine, (void *)&uarg); return (err) ? RTOS_ERROR : RTOS_OK;
#else ... }
Мы определим новую структуру uarg_t, которая содержит все аргументы входной процедуры задачи RTOS, указатель на входную процедуру и приоритет задачи.
typedef struct _uarg_t { rtosEntry_t entry_routine; int priority; char *arg1; int arg2; void *arg3; }uarg_t;
Для каждого вызова rtosCreateTask новый поток выполнения создаётся путём вызова функции pthread_create. Идентификатор созданного потока хранится в аргументе tHandle->thread_id. Новый поток выполняет функцию wrapper_routine.
void wrapper_routine(void *arg){ uarg_t *uarg = (uarg_t *)arg; nice(rtos_to_nice(uarg->priority)); uarg->entry_routine(uarg->arg1, uarg->arg2, uarg->arg3); }
В wrapper_routine приоритет потока регулируется с помощью системного вызова nice. Функция rtos_to_nice выполняет преобразование параметров приоритетов RTOS в параметры Linux. Вам необходимо переписать эту функцию в зависимости от вашего RTOS. Наконец, она передаёт управление фактической входной процедуре с соответствующими аргументами. Вы можете быть удивлены тем, что мы проигнорировали в данной реализации аргумент stackSize. В Linux указывать размер стека потока или задачи нет необходимости. О выделении стека заботится ядро. Оно увеличивает стек динамически по мере необходимости. Если по каким-либо причинам вы захотите задать размер стека, вызовите pthread_attr_setstacksize до вызова pthread_create. Теперь обсудим rtosDeleteTask.
void rtosDeleteTask(rtosTask_t tHandle){ #ifndef __KERNEL__ pthread_cancel(tHandle.thread_id); pthread_join(tHandle.thread_id, NULL); #else ... }
Чтобы отправить запрос на завершение потока tHandle, функция вызывает pthread_cancel. Затем, чтобы подождать его завершения, она вызывает pthread_join. Обратите внимание в нашем предыдущем обсуждении завершения потоков, что pthread_cancel не прекращает выполнение данного потока. Она просто посылает запрос на завершение. Поток, который получает запрос, имеет возможность либо принять его, либо проигнорировать. Таким образом, чтобы успешно проэмулировать rtosTaskdelete, все потоки должны принять запрос на завершение к исполнению. Они могут сделать это добавлением в код явных точек завершения, например, так:
static void rtosTask (char * arg1, int arg2, void * arg3){ while(1){ /* * Thread body */ pthread_testcancel(); <-- Добавляем явную точку завершения } }
На самом деле, чтобы принять запрос на завершение, поток может использовать любой метод, о котором говорилось ранее. Но вы должны быть осторожны при использовании немедленного типа завершения потока (PTHREAD_CANCEL_ASYNCHRONOUS). Поток, использующий немедленное завершение, может быть прекращён, когда он держит какую-нибудь блокировку и находится в критической секции. При завершении блокировка не освобождается, и это может привести к тупиковой ситуации, если другой поток попытается получить блокировку.
Эмуляция интерфейсов задачи в ядре
Давайте обсудим реализацию интерфейсов задачи rtosCreateTask и rtosDeleteTask в ядре. Перед переходом к деталям реализации мы сначала рассмотрим две функции ядра: wait_for_completion и complete. Эти две функции используются в ядре для синхронизации выполнения кода. Они также используются для уведомления о событиях. Поток ядра может ждать пока произойдёт событие, вызывая wait_for_completion на специальной переменной. Он пробуждается другим потоком с помощью вызова complete на этой переменной, когда событие происходит. Где мы нуждаемся в функциях ядра wait_for_completion и complete? В нашем OSPL задача RTOS реализуется с использованием потоков ядра. Поток ядра создаётся с помощью функции kernel_thread. Он может быть завершён посылкой ему сигнала. Наша реализация rtosDeleteTask посылает сигнал потоку ядра и вызывает для ожидания завершения wait_for_completion. Поток ядра при получении сигнала вызывает complete, чтобы перед выходом пробудить вызывающего rtosDeleteTask. Первой является rtosCreateTask.
rtosError_t rtosCreateTask(char *name, rtosEntry_t routine, void * arg, int priority, int stackSize, rtosTask_t *tHandle){ #ifndef __KERNEL__ ... #else struct completion *complete_ptr = (struct completion *)kmalloc(sizeof(structcompletion), GFP_KERNEL); karg_t *karg = (karg_t *)kmalloc(sizeof(karg_t), GFP_KERNEL); strcpy(tHandle->name, name); init_completion(complete_ptr); <-- Initialize a completion variable tHandle->exit = complete_ptr; karg->entry_routine = routine; karg->priority = priority; karg->arg1 = arg1; karg->arg2 = arg2; karg->arg3 = arg3; karg->exit = complete_ptr; tHandle->karg = karg; tHandle->thread_pid = kernel_thread(wrapper_routine, (void *)karg, CLONE_KERNEL); return RTOS_OK;
#endif
}
Каждая задача ядра RTOS связана с переменной complete_ptr для обработки его завершения. Она инициализируется вызовом функции ядра init_completion. complete_ptr обёрнута вместе с аргументами входной процедуры arg и приоритетом priority в структуру karg типа karg_t. Наконец, для создания потока ядра, который начинает исполняться с функции wrapper_routine, вызывается функция kernel_thread. Аргументом wrapper_routine является karg. kernel_thread возвращает для созданного потока идентификатор потока, который хранится в tHandle->thread_pid. Обратите внимание, что для последующего использования в функции rtosDeleteTask структура tHandle также хранит complete_ptr и karg. wrapper_routine устанавливает приоритет используя sys_nice и вызывает фактическую процедуру входа.
void wrapper_routine(void *arg){ karg_t *karg = (karg_t *)arg; sys_nice(rtos_to_nice(karg->priority)); karg->entry_routine(karg->arg1, karg->arg2, karg->arg3,karg->exit); }
Обратите внимание, что мы внесли изменения в прототип входной функции для размещения ещё одного аргумента, указателя на переменную завершения.
typedef void (*rtosEntry_t) (char *arg1, int arg2, void *arg3, struct completion * exit);
Изменения, необходимые для обработки завершения потока ядра в rtosDeleteTask, обсуждаются далее. Наконец, мы обсуждаем rtosDeleteTask.
void rtosDeleteTask(rtosTask_t tHandle){ #ifndef __KERNEL__ ... #else kill_proc(tHandle.thread_pid, SIGTERM, 1); wait_for_completion(tHandle.exit); kfree(tHandle.exit); kfree(tHandle.karg); #endif }
Чтобы послать сигнал SIGTERM потоку tHandle.thread_pid, реализация вызывает функцию ядра kill_proc. Затем вызывающий rtosDeleteTask ожидает завершения потока вызовом wait_for_completion на переменной завершения tHandle.exit. При пробуждении он освобождает ресурсы, выделенные в rtosCreateTask, и возвращается. Отправки сигнала потоку ядра не достаточно для его прекращения. Поток, который получает сигнал, должен проверять наличие ожидающих сигналов на своём пути исполнения. Если сигнал прекращения (в нашем случае SIGTERM) находится в состоянии ожидания, то он должен перед выходом вызвать complete на переменной завершения. Поток ядра в данной реализации выглядел бы так:
static int my_kernel_thread (char *arg1, int arg2, void *arg3, struct completion * exit) { daemonize("%s", "my_thread"); allow_signal(SIGTERM);
while (1) {
/* * Тело потока */
/* Проверка на завершение */ if (signal_pending (current)) { flush_signals(current); break; }
}
/* посылаем сигнал и завершаем работу */ complete_and_exit (exit, 0); }
Чтобы подвести итог, в Таблице 6.2 перечислены соответствия между интерфейсами задачи RTOS и Linux.
Таблица 6.2 Интерфейсы задачи RTOS и Linux
| |||||||||||
Предыдущая Содержание Следующая |