Модель системы процессов, работающих в разделении времени
Основная идея переключения процессов и ее связь с прерыванием были обсуждены выше. В модели используется следующий принцип: если функция обработки прерывания на Си (типа void interrupt f()) сохраняет в стеке состояние выполняемой Си-программы, то простое сохранение текущего указателя стека и переустановка сохраненного указателя внутри функции обработки прерывания приводит при выходе из прерывания к переключению от одного процесса к другому.
Основная структура данных программы - массив дескрипторов процессов. Описатель (дескриптор) процесса включает в себя динамический массив, в котором находится его стек, и текущие значения указателя стека в нем.
// Дескриптор процесса -------------------------------------
struct PROCESS
{
unsigned SS,SP; // Указатель стека процесса
char *PS; // Область стека процесса
unsigned STATUS; // Слово состояния процесса
unsigned DELAY; // Тайм - аут
}
TASK[NT], // Массив дескрипторов процессов
*PT; // Указатель на дескриптор текущего процесса
Далее в процессе определяется его текущее состояние. Согласно классической схеме он может находиться в следующих основных состояниях:
-АКТИВЕН - в данный момент выполняется (или прерван),
-ГОТОВ - может быть выполнен ;
-БЛОКИРОВАН - не может быть выполнен по каким-либо внешним причинам.
На активный процесс в модели ссылается указатель PT. В слове состояния процесса STATUS определяются биты блокировки, каждый из которых отмечает одну из причин блокировки. Их отсутствие определяет состояние готовности процесса к выполнению.
// Биты блокировки процесса ---------------------------
#define TWAIT 1 // Тайм - аут процесса
#define IO 2 // Ввод - вывод
#define MESS 4 // Ожидание сообщения
#define SEM 8 // Семафор
#define OFF 16 // Не активен
Идея переключения процессов реализована в обработчике прерываний по таймеру. В момент прерывания (которое происходит 18.2 раз в секунду) он запоминает указатель стека текущего процесса, после чего ищет, начиная с процесса, следующего за текущим, активный процесс.
Если таковой найден, то он устанавливает его указатель стека и таким образом переключается на его запомненное состояние в момент предыдущего прерывания. При выходе из прерывания процесс уже будет восстановлен "физически", то есть продолжит свое выполнение с точки прерывания.
/ / Обработчик прерывания по таймеру ------------------------------
void interrupt TIMER(bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flgs)
{
static int n;
//----- Запоминание состояния задачи ----------------------
(*OLDTIM)(); // Старый обработчик прерывания
if (DISP) return; // Диспетчер занят - переключение невозможно
DISP++; // Занять диспетчер - защита от повторного входа
PT->SP = _SP; // Сохранить текущий указатель стека
PT->SS = _SS;
//----- Поиск готового процесса --------------------------
if (RUN) {DISP--; return; }
for (CT++,n=0; n < NT; n++,CT++)
{ // Поиск " по кругу" от текущего процесса
CT %= NT; // Выбирается готовый - без битов блокировок
if (TASK[CT].STATUS ==0) break;
}
//----- Отсутствие готовых процессов ---------------------------
if (n == NT) printf("a-a-a-a-a-a-a-a");
//----- Восстановление состояния нового процесса------------
PT = TASK + CT; // Указатель на новый активный процесс
_SS = PT->SS; // Установить стек процесса
_SP = PT->SP;
DISP--;
} // Выйти из прерывания через стек нового
// процесса - переключиться на новый
Обработчик прерывания от таймера выполняет функции ДИСПЕТЧЕРА параллельных процессов. Поскольку в работе системы могут возникнуть ситуации, когда переключение процессов запрещено, и, наоборот, при работе диспетчера следует защититься от выполнения некоторых действий, которые могут быть связаны с изменением данных в дескрипторах процессов, то в программу вводятся переменные RUN , запрещающая переключение задач, и DISP , индицирующая, что в данный момент работает диспетчер.
Затем в модели определяется функция FORK , которая создает создает новый процесс. Поскольку система процессов функционирует в рамках одной Си-программы, то в качестве процесса естественным образом определяется функция, указатель на которую передается в вызове FORK .
Тогда указанная функция после выполнения FORK будет выполняться со всеми остальными уже работающими функциями-процессами в разделении времени, как асинхронный процесс.
Функция FORK ищет "выключенный" процесс в массиве дескрипторов, (блокированный в состоянии OFF ), создает динамический массив для размещения его стека, после чего устанавливает на него указатели стека и заполняет его текущее состояние. Для этого используется определение структурированной переменной STACK , последовательность элементов в которой соответствует последовательности запоминания переменных в стеке функцией обработки прерывания типа void interrupt.
// Стек функции обработки прерывания ------------------
struct STACK
{ unsigned bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flgs; };
//--------------------------------------------------------
// Запуск процесса в режиме разделения времени
// Возвращает номер процесса
int FORK(void (*pf)())
{
int nt,n;
struct STACK *sp;
RUN++; // Запретить переключение процессов
for ( nt=-1,n=0; n < NT; n++) // Искать свободный дескриптор
if (TASK[n].STATUS & OFF) break;
if (n != NT)
{
nt = n; // Резервировать память под стек
TASK[nt].PS = (char *)malloc(SIZE);
// Текущий указатель стека с конца
// выделенной памяти (стек " вверх дном" )
sp = (struct STACK *)(TASK[nt].PS + SIZE - sizeof(struct STACK));
sp->ax = sp->bx = sp->cx = sp->dx = 0;
sp->si = sp->di = 0; // Сформировать начальное состояние стека
sp->ds = sp->es = _DS;
sp->cs = _CS;
sp->ip = (unsigned)pf; // Начало процесса - адрес функции
TASK[nt].SS = _DS; // Указатель на стек процесса в дескрипторе
TASK[nt].SP = (unsigned)sp;
TASK[nt].STATUS = 0; //Процесс - ГОТОВ
}
RUN--;
return(nt);
}
Функция возвращает номер созданного процесса в массиве дескрипторов. Функция-процесс, которая вызывает FORK , называется порождающим процессом и может использовать этот номер для дальнейшего взаимодействия с порожденным процессом.
Заметим, что при порождении процесса переключения процессов запрещаются (устанавливается признак RUN ).
Если текущий процесс выполняет действия, которые запрещают его дальнейшее протекание, то есть блокируется, то для этого он должен установить один из битов (признаков) блокировки в своем слове состояния, а затем принудительно вызывать диспетчер для переключения на другой процесс. Поскольку диспетчер выполняет переключение по прерыванию от таймера, процесс должен смоделировать (эмулировать) это прерывание, что выполняется при помощи программного прерывания по тому же самому вектору при помощи функции geninterrupt. Тогда точка блокировки процесса будет естественным образом установлена сразу же после вызова этой функции в программе.
// Блокирование текущего процесса --------------------------
void BLOCK(int mask)
{
RUN++;
PT->STATUS |= mask;
RUN--;
geninterrupt(TIMVEC);
}
С использованием этого механизма процесс может уничтожить себя, или "приостановиться" на заданное число тиков таймера. В последнем случае он устанавливает в своем дескрипторе счетчик, из которого в каждом прерывании диспетчером вычитается 1. Когда счетчик обнуляется, диспетчер сбрасывает признак блокировки, и процесс снова переходит в состояние готовности.
// Уничтожение текущего процесса -------------------------
void KILL()
{
RUN++;
asm cli
PT->STATUS |= OFF;
free(PT->PS);
RUN--;
asm sti
geninterrupt(TIMVEC);
}
// Тайм - аут процесса -------------------------------------
void WAIT( unsigned tt)
{
PT->DELAY = tt;
BLOCK(TWAIT);
}
В диспетчер добавляется соответствующий цикл, который " отсчитывает" время для блокированных процессов и по завершению их блокировки - возвращает в состояние готовности.
// Вычисление тайм - аутов в диспетчере -------------------
if (!NOCLOCK)
{
for (n=0; n<NT; n++)
if (TASK[n].STATUS & TWAIT)
if (--TASK[n].DELAY ==0) TASK[n].STATUS &= ~TWAIT;
} NOCLOCK=0;
Основная функция main имеет в себе отдельные нюансы, которые могут быть поняты только в терминах процессов.
Дело в том, что она начинает выполняться в режиме обычной последовательной программы и в этом режиме готовит структуры данных для системы процессов. Затем происходит неявное переключение в режим квазипараллельных процессов, причем main становится в нем единственным или " нулевым" процессом. Для этого он настраивает структуры данных соответствующим образом - первое прерывание по таймеру осуществляется при единственно готовом " нулевом" процессе. " Нулевой" процесс не требует себе отдельного стека, а использует стек породившей его программы.
void main()
{ int n;
RUN = DISP = 0;
TASK[0].STATUS=0;
for (n=1; n < NT; n++) TASK[n].STATUS = OFF;
RUN++;
CT = 0;
PT = TASK;
OLDTIM = getvect(TIMVEC);
setvect(TIMVEC,TIMER);
// В момент первого прерывания от таймера main становится
// нулевым процессом (это можно сделать и принудительно)
geninterrupt(TIMVEC);
textbackground(BLACK);
clrscr();
FORK(consume);
FORK(produce);
RUN--;
// В данном примере " нулевой" процесс ничего не делает, хотя
// он может выполнять все функции обычного процесса
// (geninterrupt используется для принудительного переключения
// процессов, а NOCLOCK вводится в диспетчер, чтобы не учитывать
// такие принудительные прерывания для ожидающих (блокированных)
// процессов)
for(STOP=0;!STOP;)
{ NOCLOCK++; geninterrupt(TIMVEC); }
setvect(TIMVEC,OLDTIM);
clrscr();
}