logo search
HCS12 с применением языка С - royallib

8.4.2. Управление задачами

В этом разделе мы рассмотрим, как ОСРВ взаимодействует с отдельной задачей. Сначала мы рассмотрим различные состояния, в которых может находиться задача. Затем исследуем, как задачи переходят из одного состояния в другое, и рассмотрим программную функцию, позволяющую моделировать состояния задач и их связь друг с другом. Это обсуждение будет частью полного обсуждения блока управления задачами.

Состояния задачи. В любой момент времени задача может быть только в одном определенном состоянии. Различные состояния задачи показаны на рис. 8.14. ОСРВ должна отслеживать и изменять состояние каждой задачи. Не забудем при этом, что все задачи независимы, асинхронны и претендуют на процессорное время. ОСРВ должна обеспечить эффективное планирование этого дефицитного ресурса.

Рис. 8.14. Состояния задач

Задача находится в одном из следующих состояний:

• Бездействия (Dormant — D): задача не нуждается в обработке и не требует  процессорного времени. Она рассматривается как удаленная или неактивная задача, и по сигналу операционной системы переходит в состояние готовности.

• Готовности (Ready — R): задача полностью готова к переходу в активное состояние; однако, в настоящее время процессор занят другой задачей. Задача может переходить в состояние готовности из состояний бездействия или активности. Она переходит от активного состояния в состояние готовности, когда процессор обрабатывает другую более приоритетную задачу. Зарезервированная задача с более низким приоритетом (находящаяся в состоянии готовности) повторно переходит в активное состояние, как только становится доступным процессорное время и операционной системой дается разрешение на выполнение.

• Активности (Executing/active/running — A): задача управляется процессором, выполняя часть своей программы. Так как наша система содержит только один процессор, только одна задача может быть в активном состоянии в любое данное время. Задача остается в активном состоянии, пока не происходит одно из трех событий:

 1) завершаются необходимые для выполнения задачи действия;

 2) она выгружается задачей с более высоким приоритетом;

 3) она возвращает управление операционной системе.

Во всех этих случаях задача переходит от активного состояния в состояние готовности. Эти варианты станут более ясными, когда мы обсудим различные типы систем ОСРВ в разделе 8.5. Из активного состояния задача может также переходить в состояния ожидания.

• Ожидание (Wait — W): выполнение задачи было отсрочено. Она остается в ждущем состоянии на заданном отрезке времени и затем переходит в состояние готовности, ожидая обработки. Задача переводится в состояние ожидания временно, чтобы выделить время для обработки задач с более низким приоритетом.

• Приостановка (suspended — S): задача ждет некоторого ресурса. Как только ресурс становится доступным, задача переходит в состояние готовности и ждет процессорного времени.

• Восстановление (rescheduling — X): Это состояние вводится каждый раз, когда задача выполнена, но не может сразу же перейти в состояние готовности. В этом случае, задача остается в состоянии восстановления, пока не закончится необходимый интервал восстановления (RSI). Как только это происходит, задача снова переходит в состояние готовности.

Приведенная диаграмма состояний задач может быть выполнена с помощью программного кода, использующего команду switch, которая позволяет нам точно управлять переходами в зависимости от состояния.

/********************************************************************/

char task_state_diagram(char present_state, char action) {

 char next_state;

 switch (present_state) {

 case 'D': /*если состояние бездействия (D) и выбрана опция create(c), то */

           /* задача переходит в состояние готовности, в противном */

           /* случае она остается в состоянии бездействия (D) */

  if (action == 'с') next_state = 'R';

  else next_state = 'D';

  break;

 case 'R': /*если задача в состоянии готовности (R), процессор доступен */

           /* и задача имеет наивысший приоритет,то она переходит в */

           /* активное состояние (A). Это состояние обозначается */

           /*присвоением переменной action значения (g). */

  if (action == 'g') next_state = 'A';

  else next_state = 'R';

  break;

 case 'A': /*из состояния активности(A)задача переходит в состояние */

           /*определяемое значением переменной action. */

  if (action == 'p') /*время ожидания или приостановка задачи*/

   next_state = 'R'; /*возврат в состояние готовности*/

  else if (action == 'w')

   /*переход в состояние ожидания*/

   next_state = 'W'; /*состояние ожидания */

  else if (action == 'n') /*ресурс недоступен */

   next_state = 'S';

  /*задача переходит в состояние приостановки*/

  else if (action == 'd')

   /*завершается выполнение задачи*/

   next_state = "X';

  /*если требуется время для */

  /* восстановления */

  else if (action == 'x') /*задача исключается */

   next_state = 'D'; /*возврат в состояние бездействия */

  else next_state = 'A'; /*остается в состоянии активности */

  break;

 case 'X': /*из состояния восстановления (X) переход в состояние */

           /*готовности (R)по сигналу таймера восстановления (t). */

  if (action=='t') /*ожидается сигнал таймера восстановления*/

   next_state='R'; /*задача переходит в состояние готовности*/

  else next_state='X';

  break;

 case 'W': /*из состояния ожидания (W) переход в состояние готовности (R)*/

           /*по сигналу таймера ожидания (e). */

  if (action == 'e') /*ожидание сигнала таймера*/

   next_state = 'R'; /*возврат в состояние готовности*/

  else next_state = 'W';

  break;

 case 'S': /*из состояния приостановки (S) переход в состояние готовности (R)*/

           /*при появлении ожидаемого ресурса (a) */

  if (action == 'a') /*необходимый ресурс доступен*/

   next_state = 'R'; /*переход в состояние готовности*/

  else next_state = 'S';

  break;

  return next_state;

 }

}

/********************************************************************/

Блок управления задачами. Как мы уже упоминали, задачи представляют собой независимые, асинхронные и взаимодействующие процессы, для взаимосвязанного выполнения которых необходим один и тот же процессор. Давайте вспомним о нашем бесстрашном официанте из предшествующего раздела. Официант (процессор) должен удовлетворять запросы нескольких заказчиков (задач), используя при этом ограниченные ресурсы. Так как официант (процессор) переключается с обслуживания одного столика заказчиков (от одной задачи) к другому, ему, или ей необходимо помнить состояние (контекст) каждого из столиков (задач), чтобы обеспечить качественное обслуживание.

В этом разделе мы рассмотрим, как ОСРВ «запоминает» и отслеживает состояние каждой задачи, чтобы позволить каждой задаче выполнить свой процесс. Вы, наверное, думаете: «А в чем сложности? Позвольте процессу, который стал активным завершиться». Но как мы иллюстрировали в примере с роботом, это невозможно. Так, в системе с большим количеством конкурирующих задач, некоторые задачи низким приоритетом никогда не выполнялись бы процессором. Вообразите, что случилось бы, если наш официант (процессор) посвятил все свое время одному столику (процесс) до полного его обслуживания, не обращая внимания на все остальные столики (задачи).

Мы не знаем точно, как хороший официант следит за многими столиками одновременно. А вот ОСРВ для отслеживания состояния каждой задачи обычно использует блок управления задачами (TCB). Каждая задача в ОСРВ имеет собственную связь с TCB, которая обеспечивает текущую информацию о задаче. Эта информация используется и модифицируется ядром ОСРВ, чтобы эффективно отслеживать, планировать и выполнять весь набор задач данной системы. Мы должны подчеркнуть, что TCB изменяется только операционной системой. Задача не имеет прямого контакта с TCB, хотя этот блок содержит наиболее обновленную информацию о задаче.

Структура TCB показана на рис. 8.15. Когда ОСРВ переключается с одной задачи на другую, вся ключевая информация о текущей задаче должна быть надежно сохранена перед переключением к следующей задаче. Таким образом, когда задача позже получает процессорное время, она может продолжить процесс с того места, где он был остановлен, при выгрузке. Как мы говорили ранее, эта ключевая информация задачи называется контекстом.

Рис. 8.15. Блок управления задачами

Прервемся на минуту, и подумаем, какая информация была бы важна для задачи. Рассмотрим также, как мы могли бы выполнить программное обеспечение TCB. Как минимум TCB должен содержать имя задачи, текущее состояние, приоритет, текущий контекст (ключевые значения регистров), и адрес, по которому можно найти этот контекст для обработки задачи.

Сначала, попробуем справится с разработкой программы для TCB. Данные, отражающие отдельные свойства каждой задачи имеют различные типы. Ранее в этой главе мы обсуждали запись или структуру — абстрактный тип данных, хорошо подходящий для программной реализации TCB. Как было упомянуто, структура представляет собой определяемую пользователем совокупность различных, но связанных между собой типов данных. Мы можем объединить эти различные типы данных в структуру и использовать ее для создания программы TCB. В главе мы уже разработали структуру для автомобиля и следили за самой разнообразной информацией о конкретных автомобилях, заключенной в полях структуры. Мы сделаем теперь то же самое для TCB. (На самом деле, мы попросим, чтобы это сделали вы в качестве задания 4 для самостоятельной работы). Используйте автомобильную структуру в качестве примера, и разработайте подобную структуру для TCB.

Рассмотрим подробнее отдельные поля для TCB и определим типы данных для каждого поля.

• Имя задачи состоит из символьного массива. Для примера робота мы ограничим имя задачи 20 символами. Это позволит нам сохранить полное имя для каждой задачи, не пользуясь маловразумительными сокращениями. Мы позволяем себе такую расточительность в использовании памяти ради удобочитаемости.

• Текущее состояние задачи: как следует из предыдущего раздела, задача в каждый момент времени может находиться в одном из шести состояний бездействия (D), готовности (R), активности (A), ожидания (W), приостановки (S) и восстановления (X). Мы обозначили каждое состояние задачи одним символом. Мы можем, следовательно, сохранять текущее состояние задачи в символьной переменной внутри нашей структуры TCB.

• Приоритет задачи: Для относительного ранжирования задач внутри системы, разработчиком системы назначается приоритет задачи. В нашем примере с роботом, мы использует фиксированные приоритетные значения. Но при необходимости можно выполнить и ОСРВ системы, в которых приоритет задачи может изменяться в процессе выполнения программы. Приоритет задачи представляется натуральными числами. В следующем примере мы покажем, как назначать приоритет задачи.

• Контекст задачи: Содержимое всех ключевых регистров связанных с задачей, составляет контекст задачи. Система прерывания, встроенная в микропроцессор 68HC12 использует стек, чтобы сохранить весь контекст, когда прерывание происходит. Мы также используем стек, чтобы сохранить контекст задачи. Каждая задача имеет собственный стек, следовательно, ОСРВ имеет целый ряд одновременно существующих стеков. Как программист системы, вы должны гарантировать, что эти стеки не будут смешиваться друг с другом в пространстве памяти. Для простоты, мы используем фиксированную структуру стека, разработанную ранее в этой главе для контекстной памяти TCB. Так как все ключевые регистры и ячейки памяти в микроконтроллере 68HC12 имеют объем в 8 или 16 бит, мы используем фиксированный массив целых чисел для стеков TCB.

• Состояние активности задачи: состояние активности задачи представляет собой следующий шаг программы, который должен выполняться, как только задача станет активной. После инициализации, задача начинается с начала соответствующего ей программного кода. Однако до своего полного завершения задача может неоднократно выгружаться задачами с более высоким приоритетом. Следовательно, TCB должен помнить следующий шаг, с которого должно продолжиться выполнение задачи.

• Указатель задачи: Так как мы будем связывать эти структуры TCB с помощью указателей, нам необходим указатель на следующий в списке TCB.

Вы уже, наверное, усвоили понятиям задачи и блока управления задачей. Однако мы еще не касались ряда сложных проблем. Вам, вероятно, не очень ясна идея выхода из программы до ее завершения, даже если она обладает наивысшим приоритетом. Мы исследуем эту тему в следующем разделе. Вы можете воображать осложнения, которые получатся, если мы начнем ATD-преобразование, а оно выгрузится событием с более высоким приоритетным прежде, чем будет закончено. И представьте себе, какая путаница возникнет, если это событие с более высоким приоритетом также должно будет использовать ATD-преобразование. Мы видим, что разработчик операционной системы должен определить, когда можно безопасно прервать управление задачи. Перед исследованием этих проблем, возвратимся к нашему примеру робота и посмотрим, как назначаются приоритеты задач.

Сценарий: В нашем примере с роботом у нас было много задач по обеспечению его работы. Теперь мы должны назначить численное значение приоритета для каждой задачи. Мы будем использовать более низкое численное значение для задач с более высоким приоритетом. Например, задаче с самым высоким приоритетом сопоставим значение 1. Мы используем наше понимание сценария работы робота, чтобы назначить приоритеты задач. Так как наш робот дезактивируется, когда приближается к мине, мы присваиваем задаче обнаружения мин приоритет 1. Следующий самый высокий приоритет, равный 2 присвоим операции объезда мины. Задаче ATD-преобразования присвоим приоритет 3, так как она обеспечивает информацию о близости стенок лабиринта. Задаче выбора поворота присвоим следующий приоритет 4, так как она обрабатывает информацию, необходимую, чтобы избежать столкновения со стенками. Наконец, задаче модификации ЖКД назначаем приоритет 5. Это самый низкий приоритет для задач, рассмотренных к настоящему времени; однако, приоритет всех прочих задач еще ниже. Остающиеся задачи имеют еще более низкий приоритет. Они активны на начальных этапах работы нашей операционной системы, а затем входят в состояние бездействия. Мы, следовательно, назначаем им самый низкий приоритет, давая им приоритеты 6 (инициализация ЖКД), 7 (инициализация ATD), и 8 (инициализация ШИМ).

Фрагментация задач. Как мы уже выяснили в этом разделе, желательно разделить программный код задачи на непрерываемые части или фазы, определив удобные точки выхода. Это важно, поскольку в ОСРВ с несколькими задачами с одинаковым приоритетом, задача редко будет способна завершить связанные с ней действия от начала до конца. Чаще всего задача завершит некоторую часть действий и затем будет приостановлена задачей или задачами с более высоким приоритетом. Факт перехода задачи с более высоким приоритетом в состояние готовности еще не означает, что процессорное время немедленно передается ей. Мы должны организовать переход от одной задачи к другой, прервав выполнение кода в заранее определенном удобном для этого месте. Поскольку мы записываем контекст прерываемой задачи, мы должны сохранить его в памяти. Например, мы можем подразделять связанную с задачей функцию на три части. Первый раз, когда задача становится активной, процессор выполняет первую треть кода до удобной отметки прерывания (определенной вами, как программистом). Когда код достигает этой отметки прерывания, контекст сохраняется в TCB, и процессор прерывает управление задачей. Затем выполняется задача с более высоким приоритетом. Когда прерванная задача снова получает для своего выполнения драгоценное процессорное время, она начинается с того места, где была прервана и продолжает обработку второй части своего кода. Этот процесс продолжается, пока задача не завершает все предусмотренные действия.

Проиллюстрируем такую работу примером.

Пример: Предположим, что робот, имеющий пять ИК локаторов (рис. 8.16) выполняет функцию названную process_turn, которая инициализирует систему ATD контроллера 68HC12, начиная последовательность преобразований, необходимую, чтобы записать аналоговые сигналы от пяти датчиков (с номерами от 0 до 4), которые связаны с каналами ATD от 7 до 3, соответственно. Выход датчика Холла, установленного в нижней части робота, чтобы обнаруживать магнитные мины, связан с каналом 2 ATD. Обратите внимание: этот пример придуман, чтобы показать, как следует подразделять код, чтобы обеспечить удобные точки прерывания.

Рис. 8.16. Робот c пятью ИК локаторами и датчиком Холла. ИК-датчик обнаруживает присутствие стенок лабиринта, в то время как датчик Холла обнаруживает присутствие магнитных мин.

Код process_turn, обеспечивающий процесс поворота, приведен ниже.

void process_turn() {

 /*Инициализация системы ATD */

 ATDCTL2 = 0x80; /*установка флага ADPU, чтобы подать питание на систему ATD*/

 ATDCTL3 = 0x00; /*игнорировать «замораживание» системы */

 ATDCTL4 = 0x7F; /*Снижение частоты таймера P до 125 кГц */

                 /*выборка, время преобразования = 32 ATD цикла */

 /* 1 выборка за каждые 256 мкс */

 for (i=0; i<67; i++) { /* ожидание 100 мкс при 8 МГц ECLK*/

  ;

 }

 /*Инициализация ATD-преобразования */

 ATDCTL5 = 0x50; /*Начать многоканальное ATD-преобразование */

 /* для 8 каналов */

 while((ATDSTAT & 0x8000) == 0) { /* проверить окончание преобразования по*/

  /*состоянию флага SCF */

  ;

 }

 /* сохранить результаты ATD-преобразования*/

 /* в глобальном массиве char*/

 sens[0] = ADR7H; /*крайний левый датчик */

 sens[1] = ADR6H; /*средний левый датчик */

 sens[2] = ADR5H; /*центральный датчик */

 sens[3] = ADR4H; /*средний правый датчик */

 sens[4] = ADR3H; /*крайний правый датчик */

 sens[5] = ADR2H; /*Датчик Холла*/

 /*анализ информации датчиков для решения о повороте. Примечание: пороги для*/

 /*датчика Холла(hes_threshold) и для ИК-датчиков (opto_threshold)являются*/

 /* глобальными переменными и определены экспериментально*/

 if (sens[5] < hes_threshold) { /*сигнал с датчика Холла, объезд*/

  pwm_motors(back_up); /* робот дает задний ход*/

  /*действия, следующие после того */

  /* как робот отъехал назад */

  if(sens[0] > opto_threshold) pwm_motors(right_turn);

  else pwm_motors(left_turn);

  for(i=0; i<0xFFFF; i++) { /*задержка перед вращением двигателя */

   for(j=0; j<15; j++){

    ;

   }

  }

 }

 /*если обнаружен тупик - задний ход*/

 else if((sens[2]>opto_threshold) && (sens[0]>opto_threshold) && (sens[4]>opto_threshold)) {

  pwm_motors(back_up);

 }

 /*если стенки спереди и слева, */

 /*поворот робота направо */

 else if((sens[0]>opto_threshold) && (sens[2]>opto_threshold)) {

  pwm_motors(right_turn);

 }

 /*если стенки спереди и справа, */

 /*поворот робота налево */

 else if((sens[2]>opto_threshold) && (sens[4]>opto_threshold)) {

  pwm_motors(left_turn);

 }

 /*если стенка перед средним правым */

 /* датчиком, то полуповорот направо */

 else if (sens[1] > opto_threshold) {

  pwm_motors(half_right);

 }

 /*если стенка перед средним левым */

 /* датчиком, то полуповорот налево */

 else if (sens[3]>opto_threshold) {

  pwm_motors(half_left);

 }

 /*если сигналов от датчиков нет, продолжить движение вперед*/

 else {

  pwm_motors(forward);

 }

}

Если мы хотим подразделить этот код на три части обрабатываемые ОСРВ без прерывания, мы можем вставить точки прерывания после последовательности инициализации ATD и после последовательности записи данных с ATD. Это позволит функции без проблем прерывать и восстанавливать управление процессором. Чтобы выполнять эти изменения, мы должны ввести переменную, которую мы назовем code_section. Эта переменная позволит нам проследить, какая из трех частей кода должна быть выполнена при очередной активности задачи.

int process_turn(int code_section) {

 switch(code_section) {

 case 0:

  /*Инициализация системы ATD */

  ATDCTL2 = 0x80; /*включение ATD */

  ATDCTL3 = 0x00; /*игнорировать доступ при отладке системы */

  ATDCTL4 = 0x7F; /*Снижение частоты таймера P до 125 кГц */

  /*выборка, время преобразования = 32 ATD цикла */

  /* 1 выборка за каждые 256 мкс */

  for (i=0; i<67; i++) {

   /* ожидание 100 мкс при 8 МГц ECLK*/

   ;

  }

  code_section = 1; /*update code_section variable */

  break;

 case 1:

  /*Инициализация ATD-преобразования */

  ATDCTL5 = 0x50; /*Начать многоканальное ATD-преобразование*/

  /* для 8 каналов */

  while ((ATDSTAT & 0x8000) == 0) {

   /* проверить окончание преобразования по*/

   /*состоянию флага SCF */

   ;

  }

  /* сохранить результаты ATD-преобразования*/

  /* в глобальном массиве char */

  sens[0] = ADR7H; /*крайний левый датчик */

  sens[1] = ADR6H; /*средний левый датчик */

  sens[2] = ADR5H; /*центральный датчик */

  sens[3] = ADR4H; /*средний правый датчик */

  sens[4] = ADR3H; /*крайний правый датчик */

  sens[5] = ADR2H; /*Датчик Холла */

  code_section = 2; /*update code_section variable */

  break;

 case 2:

  /*анализ информации датчиков для решения о повороте. Примечание: пороги для*/

  /*датчика Холла(hes_threshold) и для ИК-датчиков (opto_threshold)являются*/

  /* глобальными переменными и определены экспериментально*/

  if (sens[5] < hes_threshold) { /*сигнал с датчика Холла, объезд*/

   pwm_motors(back_up); /* робот дает задний ход*/

   /*действия, следующие после того */

   /* как робот отъехал назад */

   if (sens[0] > opto_threshold) pwm_motors(right_turn);

   else pwm_motors(left_turn);

   for (i=0; i<0xFFFF; i++) { /*задержка перед вращением двигателя */

    for(j=0; j<15; j++) {

     ;

    }

   }

  }

  /*если обнаружен тупик - задний ход*/

  else if ((sens[2]>opto_threshold) && (sens[0]>opto_threshold) && (sens[4]>opto_threshold)) {

   pwm_motors(back_up);

  }

  /*если стенки спереди и слева, */

  /*поворот робота направо */

  else if((sens[0]>opto_threshold) && (sens[2]>opto_threshold)) {

   pwm_motors(right_turn);

  }

  /*если стенки спереди и справа, */

  /*поворот робота налево */

  else if((sens[2]>opto_threshold) && (sens[4]>opto_threshold)) {

   pwm_motors(left_turn);

  }

  /*если стенка перед средним правым */

  /* датчиком, то полуповорот направо */

  else if (sens[1] > opto_threshold) {

   pwm_motors(half_right);

  }

  /*если стенка перед средним левым */

  /* датчиком, то полуповорот налево */

  else if (sens[3]>opto_threshold) {

   pwm_motors(half_left);

  }

  /*если сигналов от датчиков нет */

  /*продолжить движение вперед */

  else {

   pwm_motors(forward);

  }

  code_section = 0; /* изменить переменную code_section */

  break;

 }/*конец switch*/

 return code_section;

}

Когда задача, связанная с функцией process_turn, переходит из состояния готовности в активное состояние, ОСРВ вызывает функцию с параметром 0. Функция process_turn затем выполняется до первой отметки прерывания в коде. Достигнув этой отметки, функция возвращает управление ОСРВ, которая модифицирует TCB, связанный с процессом и продолжает выполнение второй части кода, когда задача в очередной раз переходит в активное состояние. Затем задача снова возвращается в состояние готовности и ждет, когда ОСРВ выделит ей процессорное время. Повторим снова, что причина, по которой мы делим код на логические части, состоит в том, чтобы позволить задаче работать до завершения определенной части и затем позволить другой задаче выполнить часть связанного с ней кода, и т.д. Это дает возможность выполнять несколько появившихся задач практически одновременно, хотя в любой момент времени процессор выполняет только одну задачу.

В этом примере мы использовали глобальные переменные, чтобы сохранять информацию между последовательными обращениями к функции process_turn. Использование глобальных переменных может быть не самое лучшее использование дефицитных ресурсов памяти RAM. Далее в этой главе мы рассмотрим альтернативные методы движения информации между задачами.

Как можно предположить из этого примера, ядро ОСРВ должно быть довольно сложным, чтобы позволить следить за множеством задач, эффективно планировать использование процессорного времени и полностью выполнять функции, необходимые для конкретного применения. В следующем разделе мы исследуем составляющие части ядра ОСРВ и их взаимодействие друг с другом, позволяющие удовлетворить требования применения.