            Мобильная реализация сопроцессов
            ================================

                        Введение
                        ========

Некоторое время тому назад меня посетила идея написания реализации 
сопрограмм полностью на С, без использования кода на ассемблере даже в виде 
стандартных функций. Видимо, причиной для этого явилось наличие в языке 
Модула-2 сопрограмм как стандартного средства, меня же грызла идея доказать 
превосходство С, написав реализацию сопрограмм прямо на нем. Я почти 
реализовал эту идею для РС, но тут меня постигло разочарование - хотя 
реализация и была написана на С, она была абсолютно немобильной. Основная 
проблема была в переключении стеков сопрограмм - их структура различна на 
разных машинах. Не так давно я прочитал книгу Тони Хансена (Tony L. Hansen, 
"The C++ Answer Book"), в которой приводилась мобильная реализация 
сопрограмм на С++, однако, как сказано в книге, она неработоспособна на 
SPARC, в котором используется 2 стека. Эта реализация подсказала мне идею 
использования longjmp для переключения стеков сопрограмм. В остальном моя 
реализация устроена иначе и сейчас (на 9 марта 1993) проверена ее 
работоспособность на машинах:

i386 protected mode - Unix sVr3(SCO) - AT&T C и MS C
i80x86 real mode - MS DOS - Quick C 2.0
Sun SPARC - Unix BSD (видимо, Sun OS) - стандартный и GNU C
MC68020 (LSX 3040 - AT&T 3B like) - Unix sVr2 (X/OS) - Greenhills C

Естественно, в ней могут оставаться еще не выявленные ошибки.

Я должен поблагодарить Леонида Брухиса (Leonid A. Broukhis, leo@ipmce.su) за 
тестирование на SPARC'e, который и представлял собой основную проблему, и за 
ряд ценных замечаний, которые на данный момент в-основном остались не 
реализованными.

Вряд ли эту реализацию можно считать полноценной. Это скорее идея, которую 
каждый может развить далее в соответствии со своими потребностями. По-моему, 
хорошим образцом для развития является система Юникс. Однако и в этой 
системе существует 2 несколько различных интерфейса : один для прикладных 
программ и другой внутри ядра (причем обычно функции, называющиеся 
одинаково, выполняют глубоко различные действия). В моей реализации за 
основу взят внутренний интерфейс. Дело в том, что в ядре Юникса работают 
именно сопроцессы, а также в том, что достаточно удобно отлаживать макеты 
юниксовских драйверов в MS DOS, для чего необходима реализация сопроцессов 
под MS DOS (и данная реализация легко может быть приспособлена для этих 
целей).

                Рекомендации по компиляции
                ==========================

Скомпилируйте файл coproc.c с ключем -c (до объектного вида) без всякой 
оптимизации. Оптимизация может разрушить логику работы программы. Если вам 
попался излишне умный компилятор, убирающий неиспользуемые переменные даже 
при выключенной компиляции, необходимо в функции allocspace как-то 
использовать массив v, а, возможно, и внести другие изменения. Если вы 
компилируете в MS DOS, раскомментарьте #define DOS в начале файла. При этом 
при отсутствии активных сопрограмм вместо вызова sleep() будет выполняться 
пустой цикл. Необходимо выключить проверки на переполнение стека.

Для использования надо включить в ваш проект coproc.o (или coproc.obj) и 
подключить coproc.h (хотя последнее и не обязательно). Необходимо выключить 
проверки на переполнение стека. В MS DOS необходимо выделить достаточно 
большой стек (обычно не менее 10К).

Все функции собраны в файле coproc.c, а не разделены на отдельные файлы по 
той простой причине, что они почти все в любом случае будут подключены в 
любую программу, использующую одну из них.

                Принципы работы и описание вызовов
                ==================================

Текст разделяется всеми сопроцессами. Все статические данные разделяются 
всеми сопроцессами.  Все автоматические данные являются собственностью 
одного процесса (но если он передаст их адрес через глобальные данные 
другому процессу, то они будут доступны и ему). При переключении контекста 
запускается сопроцесс с наивысшим приоритетом из имеющихся. Более высоким 
приоритетам соответствуют чмсленно меньшие значения. 

run1coproc(func,maxfrm,varsz) 
int (*func)();
int maxfrm, varsz;
    - запустить первый сопроцесс. Для него выделяется кусок стека, 
    достаточный для запуска maxfrm вложенных функций и суммарного объема 
    локальных переменных varsz килобайт (включая и аргументы функций). 
    Видимо, в большинстве случаев вполне достаточно maxfrm=10, varsz=2. В 
    качестве функции main сопроцесса вытупает функция func. 

cofork(func, maxfrm, varsz) 
int (*func)();
int maxfrm, varsz;
    - запустить новый сопроцесс, параметры аналогичны run1coproc. Не следует 
    писать программы, зависимые от того, какой сопроцесс первым получит 
    управление после этого вызова - родительский или дочерний. Сопроцесс, не 
    получающий управление немедленно, становится готовым с приоритетом 0 
    (наивысшим).

cosleep(event, pri) 
char *event;    
int pri;
    - спать по ключу event (обычно в качестве ключа используется адрес 
    какой-нибудь переменной во избежание конфликтов), после спробуждения 
    процесс получает указанный приоритет. Специальный ключ &lbolt 
    используется для передачи управления без засыпания (процесс добавляется 
    к очереди готовых с указанным приоритетом и запускается слудующий 
    процесс из этой очереди. Если указанный приоритет наивысший, то процесс 
    просто продолжает свою работу). В Юниксе засыпание по lbolt 
    используется, для ожидания освобождения ресурса (там оно вызывает 
    засыпание на 1 секунду):

        while(ресурс недоступен)
            sleep(&lbolt, MYPRI);

    Если на данный момент нет ни одного готового процесса, планировщик ждет, 
    пока такой не появится в результате обработки сигнала (в Юниксе) или 
    прерывания (в ДОСе).

    Изначально event имел тип caddr_t, но в ДОСе в Quick C он почему-то не 
    описан в файле <sys/types.h>.

swtch(pri)
int pri;
    - макрос, расширяемый до sleep(&lbolt, pri). Вызывает переключение 
    контекста с установкой приоритета pri для вызывающего сопроцесса.

cowakeup(event)
char *event;
    - разбудить все сопроцессы, спящие по ключу event. Типичное 
    использование для синхронизации:

    static globalflag=0;

    while(globalflag)
        cosleep(&globalflag, MYPRI);
    globalflag=1;

    /* ... critical section ... */

    globalflag=0; cowakeup(&globalflag);

coexit()
    - завершить текущий процесс. Тот же эффект получается, если в главной 
    функции сопроцесса выполнить оператор return. Возврат значения сейчас не 
    поддерживается. Если больше нет ни готовых ни спящих процессов, то 
    программа завершается.

cogetpid()
    - возвращает идентификатор текущего процесса. Пока что для него нет 
    практического использования, но он будет весьма полезен, если добавить 
    работу с сигналами и т.д.

                    Тесты
                    =====

В комплекте приведены 3 иаленькие тестовые программы tst1, tst2, tst3 вместе 
с make-файлами для Quick C (*.mak). Их результаты должны быть:

tst1:
--------------------------
Trying to create the 1st process
proc1, pid=0
0' Forking...
0' sleeping
proc2, pid=1
1' Trying to fork...
1' sleeping on z
proc2, pid=2
2' waking up on z
2' exiting
1' waken up, exiting
0' proc1 waken up
0' proc1 waken up
0' proc1 waken up
0' exiting

tst2:
--------------------------
Trying to create the 1st process
proc1, pid=0
0' Forking...
0' sleeping
proc2, pid=1
1' Trying to fork...
1' sleeping on z
proc2, pid=2
2' waking up on z
2' exiting
0' proc1 waken up
0' proc1 waken up
0' proc1 waken up
0' exiting
1' waken up, exiting

tst3:
--------------------------
calling run1coproc
55
0 - sleeping
55
1 - switching
55
1 - waking up
55
1 - sleeping
55
0 - switching
55
0 - waking up
55
0 - finishing
55
1 - finishing
