ГЛАВА 10 -------- Оконные меню ----------------------------------------------------------------- Итак, сейчас вы уже можете адресовать область пользовательского интерфейса интерактивной системы, ссответствующую определенным операциям. Для ввода различных полей данных - это средства поддержки формо-ориентированного ввода; для ввода текста - это оконный текстовый редактор; для обеспечения пользователей help-информацией по этим операциям - это контекстно -зависимая help-система; для поддержки каждой из этих операций - библиотека функций окна. Ранее мы с вами рассмотрели примеры адресации области пользовательского интерфейса для случая, когда прикладная программа использует только одну из операций. Однако диалоговые системы обычно обеспечивают поддержку целого ряда операций, которые может инициализировать пользователь. Выбор конкретной операции, которую необходимо выполнить в данный момент, чаще осуществляется пользователем интерактивной системы, чем самой системой. Это связано с тем, что зачастую требуется выбор из группы сложных независимых операций, а пользователь всегда лучше знает, какая именно операция должна быть выполнена в данный момент. Меню ----------------------------------------------------------------- Часто перечень возможностей системы отображается пользователю в виде списка, из которого он может выбрать нужные ему функции. Наиболее часто этот подход носит название меню. Перечень выполняемых функций является одной из основных частей пользовательского интерфейса системы и обсуждается далее. Вы уже встречались с одним из видов меню в Главе 6 при рассмотрении программы poetry.exe. Это меню содержало список - 2 - опций окна. Пользователь мог осуществлять выбор из этого меню путем нажатия клавиш, соответствующих одной из опций меню, или путем перемещения курсора по элементам меню и нажатия клавиши <ВВОД> в позиции соответствующего элемента. Такой тип меню является наиболее часто используемым в интерактивных системах, но в то же время относится к разряду наиболее эффективных и понимаемых пользователем приемов. Другим популярным типом меню, который часто используется в компьютерах, снабженных графическим пользовательским интерфейсом и манипулятором типа "мышь", является меню в виде скользящей строки (sliding bar menu). Этот тип меню представляет собой горизонтальное меню, расположенное в верхней части, с "всплывающими" вертикальными меню, раскрывающими и конкретизирующими содержание выбранных из горизонтального меню элементов. Вертикальные меню "всплывают" под соответствующими элементами горизонтального меню, и после этого пользователь получает возможность осуществлять выборку из вертикального меню, перемещаясь по нему вверх и вниз. Примером такого меню может служить система меню, принятая во Framework-II - широко распространенном продукте фирмы Ashton Tate . К преимуществам такого типа меню относится то, что оно занимает минимальную область экрана (обычно - одна строка) и наиболее полно отражает взгляд пользователя на конкретное приложение программы. Вертикальные меню реализованы в виде "всплывающих" окон. Вследствие того, что они лишь временно перекрывают изображение на экране, не уничтожая его, применение такого меню раскрывает новые перспективы в разработке диалоговых систем. На рисунке 10.1 представлен пример именно такого меню. Пользователи системы Borland's Superkey могут видоизменить это меню. +--------------------------------------------------------------+ |+------------------------------------------------------------+| || Macros Commands Functions Options Defaults Encryption || |+----------------------------+-------------------------+-----+| - 3 - | | Arrow keys OFF | | | | Bottom line OFF | | | | Command stack ON | | | | Format fields OFF | | | | Keyb. click OFF | | | | One finger OFF | | | | Playback delay 0 | | | | proTect delay OFF | | | | sUspend OFF | | | | disk Wait OFF | | | | Save optoins | | | +-------------------------+ | | | | | +--------------------------------------------------------------+ Рис. 10.1 Пример меню Процесс, образующий оконное меню ----------------------------------------------------------------- В этой главе содержится вводная информация о функциях-драйверах меню, которые используются для создания и поддержки скользящего меню-строки, которое описано выше. Такое меню создается как окно и управляется с помощью последовательности управляющих таблиц, образующихся после обращения к программе. Эти таблицы описывают последовательность выборки в скользящем меню-строке и каждом из "всплывающих" вертикальных меню. Функции-драйверы меню осуществляют обслуживание основного (целевого) процесса выполнения вашей программы. Они управляют пользовательским интерфейсом путем контроля процесса отображения меню и ввода пользователем выбранного элемента меню. Процесс ввода управляется набором таблиц меню, которые кодируются в вашей программе. Основной таблицей меню является массив MENU-структур. - 4 - Структура MENU определена в twindow.h (см. Главу 6). Этот массив является входным для каждого процесса выборки из скользящей меню- строки. Каждый ввод выбранного элемента меню содержит отображаемое пользователю имя элемента меню и ряд указателей, описывающих содержание "всплывающего" вертикального меню, соответствующего указанному пользователю элементу горизонтального меню. Эти указатели включают в себя указатель на массив имен элементов "всплывающего" меню и указатель на массив указателей функций на языке Си. Имена отображаются во "всплывающем" меню, а функции входят в состав системы, использующей это меню, и выполняются, когда пользователь отмечает в качестве выбранных соответствующие имена элементов меню. Иерархическая система меню состоит из двух уровней. Первый уровень включает в себя выборку из горизонтального меню и ограничен лишь шестью элементами вследствие того, что элементы этого уровня располагаются на одной строке. Любая выборка элемента на этом уровне приводит к появлению соответствующего "всплывающего" меню на втором уровне. Размер "всплывающего" меню ограничен 21 элементами, что соответствует максимальному числу строк, которое может быть включено в "всплывающее" меню. Каждая выборка на этом уровне приводит к вызову определенной Си-функции из закрепленных за элементами меню. Если вы хотите получить дополнительные уровни в вашем иерархическом меню, то должны описать дополнительные управляющие таблицы меню, присвоить им значения и определить рекурсивные вызовы функций поддержки системы меню. Вследствие того, что функции поддержки меню реентерабельны, выборка на втором уровне "всплывающего" меню может привести к появлению нового горизонтального меню. Описание иерархии системы меню фактически осуществляет ваша прикладная программа (или программная система), которая непосредственно генерирует массивы меню. Пример, представленный на листинге 10.3 (листинг см. ниже в этой главе), поясняет процесс формирования массивов. Обсуждение этого примера включает - 5 - обсуждение полученного изображения как результата предварительной генерации массивов меню, а также детальное описание самого процесса их генерации. Функции поддержки меню ----------------------------------------------------------------- Для использования оконных меню в этой главе вы должны вначале сгенерировать массив MENU, затем массив, на который ссылается массив MENU, а также написать и оттранслировать прикладные функции, которые будут выполняться, когда пользователь выберет конкретный элемент "всплывающего" меню. Затем вы так или иначе обращаетесь к функции menu_select, которая описана ниже. void menu_select(char *name, Menu *mn) Эта функция активизирует меню-процесс, отображая на экране дисплея горизонтальное скользящее меню-строку и переходя в состояние ожидания использования пользователем клавиатуры для выборки из меню. Указатель name является именем заголовка скользящей меню-строки. Указатель mn содержит адрес массива структур MENU в вызывающей программе. Этот массив и указатель на массив MENU определяют иерархию меню для меню-процесса. После того, как функция menu_select отобразила горизонтальное меню, пользователь, используя клавиши управления курсором вправо и влево, может перемещаться по меню. Если пользователь нажал клавишу <КЛЮЧ> (), то меню-процесс прерывается, и управление передается в точку вызова функции menu_ select. Если же пользователь нажал клавишу <ВВОД> (), то осуществляется привязка конкретного "всплывающего" вертикального меню к текущему элементу горизонтального меню (определяется текущим положением маркера меню или курсора) и отображение нужного вертикального меню. Во время отображения вертикального "всплывающего" меню - 6 - пользователь может использовать клавиши передвижения курсора вправо и влево для выбора других элементов горизонтального меню-строки. В этом режиме в соответствии с движением курсора в меню-строке осуществляется последовательное отображение вертикальных меню, причем перед отображением очередного вертикального меню предыдущее вертикальное меню уничтожается. Если пользователь нажмет клавишу <КЛЮЧ> (), то текущее "всплывающее" меню уничтожается, и процесс возвращается к обработке скользящего меню-строки аналогично описанному выше. Пользователь может использовать клавиши перемещения курсора вверх и вниз для навигации по вертикальному меню и выбора нужного элемента меню. После того, как нужный элемент выбран, нужно нажать клавишу <ВВОД> (). Нажатие этой клавиши приведет к исчезновению как вертикального, так и горизонтального меню и вызову функции, соответствующей указателю, хранимому в массиве указателей функций "всплывающего"меню и, в свою очередь, соответствующего выбранному элементу меню. Выполнение функций осуществляется путем обычного обращения к ним. После того как управление от вызванной функции передается в точку ее вызова, вся система меню восстанавливается на экране дисплея в том состоянии, которое предшествовало вызову функции, реализующей идентифицированное пользователем действие, и меню-процесс продолжается в соответствии с его описанием. Исходный листинг: tmenu.c ----------------------------------------------------------------- Листинг 10.1 содержит текст библиотечной функции tmenu.c, предназначенной для поддержки предварительного описания меню-процесса. Листинг 10.1: tmenu.c - 7 - /* ------------ tmenu.c ------------ */ #include #include #include #include "keys.h" #include "twindow.h" extern int VSG; WINDOW *open_menu(char *mnm, MENU *mn, int hsel); int gethmenu(MENU *mn, WINDOW *hmenu, int hsel); int getvmn(MENU *mn, WINDOW *hmenu, int *hsel, int vsel); int haccent(MENU *mn, WINDOW *hmenu, int hsel, int vsel); void dimension(char *sl[], int *ht, int *wd); void light(MENU *mn, WINDOW *hmenu, int hsel, int d); /* ------------ Отображение и обработка меню--------- */ void menu_select(char *name, MENU *mn) { WINDOW *open_menu(); WINDOW *hmenu; int sx, sy; int hsel = 1, vsel; curr_cursor(&sx, &sy); cursor(0, 26); hmenu = open_menu(name, mn, hsel); while (hsel = gethmenu(mn, hmenu, hsel)) { vsel = 1; while (vsel = getvmn(mn, hmenu, &hsel, vsel)) { delete_window(hmenu); set_help("", 0, 0); (*(mn+hsel-1)->func [vsel-1])(hsel, vsel); hmenu = open_menu(name, mn, hsel); } } - 8 - delete_window(hmenu); cursor(sx, sy); } /* ----Инициализация горизонтального меню-------*/ static WINDOW *open_menu(char *mnm, MENU *mn, int hsel) { int i = 0; WINDOW *hmenu; set_help("menu ", 30, 10); hmenu = establish_window(0, 0, 3, 80); set_title(hmenu, mnm); set_colors(hmenu, ALL, BLUE, AQUA, BRIGHT); set_colors(hmenu, ACCENT, WHITE, BLACK, DIM); display_window(hmenu); while ((mn+i)->mname) wprintf(hmenu, " %-10.10s ", (mn+i++)->mname); light(mn, hmenu, hsel, 1); cursor(0, 26); return hmenu; } /* ----Выборка из горизонтальногоь меню------*/ static int gethmenu(MENU *mn, WINDOW *hmenu, int hsel) { int sel; light(mn, hmenu, hsel, 1); while (TRUE) { switch (sel = get_char()) { case FWD: case BS: hsel = haccent(mn, hmenu, hsel, sel); break; case ESC: return 0; case '\r': return hsel; default: putchar(BELL); break; } } } - 9 - /* -----Всплывающее вертикальное меню--------*/ static int getvmn(MENU *mn,WINDOW *hmenu,int *hsel,int vsel) { WINDOW *vmenu; int ht = 10, wd = 20; char **mp; while (1) { dimension((mn+*hsel-1)->mselcs, &ht, &wd); vmenu = establish_window(2+(*hsel-1)*12, 2, ht, wd); set_colors(vmenu, ALL, BLUE, AQUA, BRIGHT); set_colors(vmenu, ACCENT, WHITE, BLACK, DIM); set_border(vmenu, 4); display_window(vmenu); mp = (mn+*hsel-1)->mselcs; while (*mp) wprintf(vmenu, "\n%s", *mp++); vsel = get_selection(vmenu, vsel, ""); delete_window(vmenu); if (vsel == FWD || vsel == BS) { *hsel = haccent(mn, hmenu, *hsel, vsel); vsel = 1; } else return vsel; } } /* -----Управление отображением выбранных элементов горизонтального меню -----*/ static int haccent(MENU *mn,WINDOW *hmenu,int hsel,int sel) { switch (sel) { case FWD: light(mn, hmenu, hsel ,0); if ((mn+hsel)->mname) hsel++; else hsel = 1; - 10 - light(mn, hmenu, hsel ,1); break; case BS: light(mn, hmenu, hsel ,0); if (hsel == 1) while ((mn+hsel)->mname) hsel++; else --hsel; light(mn, hmenu, hsel ,1); break; default: break; } return hsel; } /* -----Вычисление высоты и ширины меню-------*/ static void dimension(char *sl[], int *ht, int *wd) { unsigned strlen(char *); *ht = *wd = 0; while (sl [*ht]) { *wd = max(*wd, strlen(sl [*ht])); (*ht)++; } *ht += 2; *wd += 2; } /* --------Отображение в соответствии с параметром accent элемента горизонтального меню ---*/ static void light(MENU *mn, WINDOW *hmenu, int hsel, int d) { if (d) revers_video(hmenu); wcursor(hmenu, (hsel-1)*12+2, 0); wprintf(hmenu, (mn+hsel-1)->mname); normal_video(hmenu); - 11 - cursor(0, 26); } Описание программы: tmenu.c ----------------------------------------------------------------- Функция menu_select вызывается для обработки меню, описанного в массиве структур MENU. Эта функция запоминает текущее положение системы меню и позицию курсора относительно координат экрана. Затем вызывается функция open_menu, которая осуществляет инициализацию и отображение на экране (в его верхней части) горизонтального меню-строки. Цикл while осуществляет обработку горизонтального меню-строки до тех пор, пока пользователь не нажмет клавишу <КЛЮЧ> (). На каждой итерации цикла вызывается функция getmenu. Если функция getmenu возвращает значение 0, значит, пользователь нажал клавишу <КЛЮЧ> (). Если функция getmenu возвращает ненулевое значение, значит, пользователь произвел выборку одного из элементов горизонтального меню. Возвращаемое функцией значение специфицирует выбранный пользователем элемент меню. Следующий цикл while обрабатывает "всплывающее" вертикальное меню, соответствующее выбранному ранее элементу горизонтального меню-строки. На каждой операции этого цикла происходит обращение к функции getvmn, которая осуществляет обработку "всплывающего" меню. При нажатии пользователем клавиши <КЛЮЧ> () функция getvmn возвращает значение 0, в противном случае getvmn возвращает значение, специфицирующее выбранный пользователем элемент вертикального меню. После этого окно, выделенное для горизонтального и вертикальных меню, уничтожается, вызывается функция, реализующая действия, соответствующие выбранному пользователем элементу вертикального меню. По завершении работы этой функции open_menu вызывает восстановление изображения на экране горизонтального меню-строки. Функция open_menu открывает окно в верхней части экрана на всю его ширину. Это окно предназначено для горизонтального - 12 - меню-строки. Меню выбора функций вашей программы отображается в этом окне в соответствии с таблицей MENU. Первый из выбранных пользователем элементов этого меню отображается в акцентированном цвете функцией light. Функция getmenu анализирует прерывания от клавиатуры при выборе элементов меню или при передвижении курсора по меню. Если пользователь нажал клавишу перемещения курсора вправо или влево, то вызывается функция haccent для перемещения курсора по меню-строке. Если пользователь нажал клавишу <КЛЮЧ> (), то функция getmenu возвращает 0. При нажатии клавиши <ВВОД> () функция возвращает значение, соответствующее выбранному элементу меню, и передает управление в точку ее вызова. Функция getvmn предназначена для обработки вертикального меню, "всплывшего" под соответствующим элементом горизонтального меню. Вертикальное меню представляет собой окно, а его состав определяется в соответствии с таблицей MENU. Функция get_selection (см. Главу 6) вызывается для считывания пользовательского выбора. После того, как в вызывающую программу передано значение выбранного пользователем элемента меню, окно, выделяемое для "всплывающего" меню, уничтожается. При нажатии пользователем клавиш перемещения курсора вправо или влево вызывается функция haccent для перемещения вперед или назад курсора по элементам горизонтального меню-строки, а обработка вновь появляющихся (в процессе перемещения курсора по горизонтальному меню) вертикальных меню осуществляется опять функцией getvmn; в противном случае значение, возвращаемое функцией get_selection приводит к возврату управления в точку вызова функции getvmn. Пример оконного меню ----------------------------------------------------------------- Листинги 10.2, 10.3 и 10.4 содержат пример программы, - 13 - иллюстрирующей использование и обработку меню. Этот пример строит меню, позволяющее выполнять в рамках одной интегрированной системы все оконные функции, рассмотренные в качестве примеров, иллюстрирующих содержание глав с шестой по девятую. Листинг 10.2, программа menu.c, является простейшей управляющей программой, которая вызывает программу, реализующую пример (exec.c) и представленную листингом 10.3. Листинг 10.4 программы menu.prj является make-файлом Турбо Си, по которому строится выполняемая программа, реализующая пример. Листинг 10.2:menu.c /* ------------------menu.c-------------------*/ #include "twindow.h" void exec(void); char notefile [] = "note.pad"; main() { load_help("tcprogs.hlp"); exec(); } Листинг 10.3:exec.c /* ----------------exec.c------------------- */ #include #include "twindow.h" /* ----------Локальные прототипы----------- */ void testmove(void); void promote(void); void ccolor(void); - 14 - void fasttest(void); void notepad(void); void ordent(void); void poems(void); void maxims(void); /* -----------Таблицы меню------------------ */ char *dselcs[] = { " move ", " promote ", " colors ", " fast ", NULL }; char *pselcs[] = { " notepad ", " orders ", " poetry ", " sayings ", NULL }; static void (*dfuncs[])()={testmove,promote,ccolor,fasttest}; static void (*pfuncs[])()={notepad,ordent,poems,maxims}; static MENU tmn [] = { {" demos ", dselcs, dfuncs}, {" programs ", pselcs, pfuncs}, {NULL,NULL,NULL} }; void exec() { menu_select(" TC Executive ", tmn); } Листинг 10.4:menu.prj - 15 - menu.c exec.c (twindow.h, keys.h) testmove (twindow.h, keys.h) promote (twindow.h, keys.h) ccolor (twindow.h, keys.h) fasttest (twindow.h) notepad (twindow.h) ordent (twindow.h) maxims (twindow.h, keys.h) poems (twindow.h, keys.h) editor (twindow.h, keys.h) entry (twindow.h, keys.h) thelp (twindow.h, keys.h) tmenu (twindow.h) twindow (twindow.h, keys.h) ibmpc.obj Для запуска программы-примера введите следующую команду C>MENU Вид экрана дисплея, изображенный на рисунке 10.2, будет соответствовать ситуации, когда на экране отображается скользящее меню-строка. В этом примере доступны для выбора только два элемента меню, однако скользящее меню-строка может включать в себя до шести элементов, доступных для выбора пользователем. Обратимся теперь к массиву структур MENU, поименованному как tmn (см. листинг 10.3 программы exec.c). Массив содержит лишь два элемента, соответствующих элементам горизонтального меню, отображаемых пользователю. Структура MENUI определена в twindow.h (см. Главу 6) и включает три элемента: указатель на имя выбранного элемента меню, указатель на массив имен элементов "всплывающего" меню, соответствующего элементу горизонтального меню, и указатель на массив указателей функции, реализующей ту или иную операцию. - 16 - +--------------------------------------------------------------+ |+---------------------- TC Executive ------------------------+| || demos programs || |+------------------------------------------------------------+| | | | | | | | | | | | | | | | | | | | | | | | | +--------------------------------------------------------------+ Рис. 10.2 Горизонтальное скользящее меню. Для перемещения по меню с целью выборки нужного элемента из горизонтального меню используются клавиши перемещения курсора вправо и влево. Если текущим является элемент меню demos, нажмите клавишу <Ввод>. Теперь вы можете увидеть на экране картинку, аналогичную изображенной на рисунке 10.3, на котором также изображено и вертикальное меню, соответствующее элементу горизонтального меню demos. Теперь вернемся к листингу 10.3 и обратим внимание на содержимое первого элемента массива tmn, который соответствует элементу горизонтального меню demos. Здесь dselcs - адрес, указывающий размещение массива указателей типа char на имена элементов demosвертикального "всплывающего" меню, dfuncs - адрес размещения массива указателей функций. Каждый из указателей этого массива адресует одну из функций, которые рассматривались в качестве примеров в предыдущих главах. Для перемещения по вертикальному меню применяются клавиши перемещения - 17 - курсора вверх и вниз. Если вы остановились на каком-то выборе, нажмите клавишу <Ввод>. После этого будет выполнена одна из функций в зависимости от того, какой элемент меню вы выбрали. После передачи управления из этой функции в точку ее вызова (как было показано в этой главе, для функций, требующих какого-либо выбора от пользователя, это клавиша , на экране восстанавливается изображение горизонтального и вертикального меню в том состоянии, которое предшествовало вызову функции, и разрешена дальнейшая работа с ними. Для уничтожения текущего вертикального меню нажмите клавишу . Для выхода из программы и уничтожения горизонтального меню требуется повторное нажатие клавиши . +--------------------------------------------------------------+ |+---------------------- TC Executive ------------------------+| || demos programs || |++---------+-------------------------------------------------+| | | move | | | | promote | | | | colors | | | | fast | | | +---------+ | | | | | | | | | | | | | | | +--------------------------------------------------------------+ Рис. 10.3 "Всплывающее" вертикальное меню Резюме ----------------------------------------------------------------- - 18 - Итак, описание библиотеки функций оконной технологии завершено. Они позволяют поддерживать программное обеспечение на таком профессиональном уровне развития, для которого традиционно использовались возможности, предоставляемые фирмами-поставщиками библиотек оконной поддержки, изучение которых требовало много времени, да и цена их была побольше, чем стоимость этой книги. Версия описанного в этой книге пакета программ может быть использована для дальнейшего комплексного развития программных систем, после чего эти системы не требуют какой-то дополнительной перенастройки в связи с мобильностью программ, составляющих описываемый пакет. В следующей главе будут рассмотрены резидентные утилиты. Функции поддержки окон, описанные в этой книге, могут быть включены в обычные однозадачные программы, выполняемые в среде DOS, или могут быть преобразованы в программы, которые постоянно находятся в памяти, и инициализация их выполнения закрепляется за определенными клавишами. Технические приемы написания таких программ (они еще носят название резидентных программ или резидентных утилит), а также примеры их использования при разработке систем рассматриваются в следующих главах. Г Л А В А 11 Резидентные программы ----------------------------------------------------------------- Повсюду в этой книге "PC" (ПК) относится к семейству IBM PC и совместимым с ними компьютерам; однако персональные компьютеры не всегда были такими, как PC. Первые ПК создавались на знаниях и интересе любителей. ПК развивались вместе с развитием микрокомпьютерной технологии от домашних игрушек до серьезных систем. Программное обеспечение и операционные системы развивались вместе с компьютерами. Ранние операционные системы - 19 - (ОС) представляли собой немногим более, чем командные процессоры Бейсика или Паскаля, которые обеспечивали поддержку языков и простое управление файлами. Эти системы обычно ориентировались на конкретный компьютер и были несовместимы с другими. Когда компьютеры начали ориентироваться на массовое применение, операционные системы начали стабилизироваться. Среди них были Apple-DOS, NorthStar DOS, TRSDOS, и CP/M. Все они имели похожие характеристики: - однопрограммный однопользовательский режим работы - поддержка файлового каталога - поддержка интерпретаторов и компиляторов языков программирования - несовместимость программ и данных между системами С великой проницательностью Гарри Килдалл создал CP/M как открытую ОС. Изначально предназначавшаяся для поддержки создания программ для Intel MDS, эта ОС не ориентирована на какую-либо модель или марку компьютеров. CP/M состоит из базовой системы ввода-вывода (BIOS), базовой дисковой операционной системы (BDOS), и командного процессора (CCP). BIOS ориентирована на работу с аппаратурой компьютера и предназначена для управления консолью, принтером и дисковой системой. Путем написания соответствующей BIOS производители компьютеров могли использовать приемлемую и развитую операционную систему с тысячами доступных программ. Благодаря своей адаптируемости CP/M стала промышленным стандартом ОС для ПК на микропроцессорах 8080 или Z80. Микропроцессоры 8080 и Z80 могли адресовать только 64К памяти, поэтому CP/M строилась как однопользовательская однопрограммная ОС.Была построена и многопользовательская версия, названная MP/M, но она не стала промышленным фаворитом, каким была CP/M, в основном из-за медленнной работы (8080 не очень быстрый процессор), и потому, что ПК не особенно подходят для многопользовательской работы. ДОС, управляющая работой PC, является адаптацией ОС, - 20 - созданной для использования на 8086, и заметно похожей на CP/M. Microsoft купила ее у создателя - фирмы Seattle Computer Products, а IBM купила лицензию у Microsoft. ДОС до сих пор во многом похожа на CP/M. ДОС состоит из трех модулей, схожих по функциям с BDOS, CCP и BIOS в CP/M; и пользовательский интерфейс почти идентичен используемому в CP/M. У ДОС есть дополнительные возможности, такие, как каналы, фильтры, переназначение ввода-вывода, пометка файлов временем и датой, и иерархическая структура файлового каталога. ДОС по-прежнему однопользовательская однозадачная ОС, в чем совпадает с CP/M. Первые компьютеры IBM PC были очень похожи на своих предшественников с микропроцессорами 8080 и Z80; у них было 64К памяти, флоппи-диски и процессор немного побыстрее, чем Z80. Однозадачная ДОС была подходящей и адекватной этим компьютерам, но PC имели три архитектурные особенности, предназначенные для расширения. Микропроцессор 8088 адресует до 1М памяти; он имеет векторную систему прерываний; клавиатура и дисплей IBM PC являются составной частью компьютера, в отличие от видеотерминалов, присоединенных через последовательный порт. Эти характеристики легли в основу ограниченного вида мультизадачности, который развивался на PC и сейчас известен как программы, остающиеся в памяти (TSR). ДОС включает две функции, позволяющие программе объявить себя резидентной. Эти функции похожи, но имеют небольшие различия. Функция 0х31 прерывания ДОС 0х21 заканчивает выполнение программы, но оставляет ее резидентной. ДОС не будет покушаться на память, принадлежащую программе. Прерывание ДОС 0х27 делает то же самое, но ограничивает размер программы величиной 64К. Эти две функции предназначались в ДОС не для написания резидентных утилит, а для того, чтобы создатели системы могли написать программы обработки прерываний (ISR), поддерживающие дополнительные устройства ввода-вывода, такие, как мышь, графический планшет, или джойстик. Эти устройства не являются стандартной частью PC и, соответственно, не имеют стандартного - 21 - программного интерфейса с ДОС. ISR могут поддерживають и другой вид программ, не обязательно связанных с дополнительными устройствами, но расширяющих пользовательский интерфейс с компьютером. Это TSR-программы; два наиболее популярных вида таких программ - это расширители клавиатуры и программы-секретари. Расширители клавиатуры, такие, как Prokey и Superkey, позволяют пользователю присвоить значения символов функциональным клавишам, -key комбинациям, или любым другим клавишам. Программы-секретари, такие, как Sidekick и Homebase, предлагают записную книжку, калькулятор, календарь, автовызывное телефонное устройство и другие возможности рабочего стола, вызываемые нажатием клавиши. Среди других TSR-программ : программы-корректоры, системы обработки структуризованных текстов, печать со спулингом, расширения командного процессора ДОС, отладчики, часы с будильником. Эти программы и многочисленные представители других резидентных программ имеются в продаже, в источниках бесплатного программного обеспечения, или в исходных текстах, публикуемых в журналах. Эта глава представляет и объясняет класс программ для PC, известных под разными именами, среди которых всплывающие (pop-up), TSR, резидентные утилиты, и программы-секретари. Такие программы уникальны, так как после исполнения они остаются резидентными в памяти и часто ее не покидают до выключения компьютера. Будучи резидентными, они выполняются (или "всплывают") при вызове. Типичная TSR-программа вызывается внешним событием, обычно называемым "горячим ключом". Горячий ключ - это ключевая строка, формируемая при нажатии пользователем комбинации клавиш, зарезервированной для вызова утилиты. Естественно, эта комбинация не должна обычно использоваться для других целей. Активизация TSR-программы прерывает выполнение текущей программы на время работы TSR-программы. После окончания - 22 - TSR-программы прерванная программа продолжает работу. Прерванный процесс может быть нерезидентной программой, другой TSR-программой, или самой ДОС. Загрузка нескольких TSR-программ в память превращает ДОС - однозадачную по сути операционную систему - в ограниченную, в чем -то калечную мультизадачную ОС. Прерывания ----------------------------------------------------------------- Чтобы понять сущность TSR-программ, вы должны понять систему прерываний, потому что эти программы используют структуру прерываний ДОС и PC. Это обсуждение ни в коей мере не является исчерпывающим описанием прерываний, и этим вы поощряетесь к исследованию предмета с использованием материалов, посвященных архитектуре 8086/80286 и PC. Здесь объясняется, что такое прерывания и как они используются, но без детальных спецификаций. Этого вам хватит, чтобы понять TSR-программы. Прерывание - это кратковременное приостановка текущей прочедуры программы, позволяющая выполнить другую процедуру. После завершения прерывания прерванная программа продолжает выполняться так, как будто бы ничего не происходило. Эти две процедуры могут быть несвязанными - и прерывание не окажет никакого воздействия на прерванную процедуру. Они могут быть взаимозависимы - прерванная программа может быть модифицирована процедурой обработки прерывания. Прерывание может быть вызвано внешним по отношению к выполняемой программе событием или в результате действий самой программы. Прерывание может быть вызвано аппаратно или командой из программы. Векторы прерывания - 23 - ----------------------------------------------------------------- В компьютере PC имеется 256 различных прерываний, с номерами от 0 до 0хff. Некоторые из них определены для использования процессором. Например, прерывание 0 возникает при делении на 0. Другие определены для вызова функций BIOS, третьи - для использования ДОС. Напомним, что 8088/8086/80286 - это микропроцессор, PC - компьютер, построенный на его базе, а ДОС - это операционная система. Для каждого из этих трех архитектурных слоев определен свой набор прерываний. Оставшиеся прерывания доступны для использования прикладными программами и программами обслуживания устройств. Каждое прерывание представлено в памяти четырехбайтным значением адреса. Эти значения располагаются в памяти со смещениями от 0 до 0х3ff. При прерывании содержимое регистра признаков и четырехбайтный адрес выполняемой команды сохраняется в стеке.После этого прерывания запрещаются, и начинает выполняться программа с адреса, соответствующего происшедшему прерыванию. Эта программа должна сохранить используемые ей регистры, выполнить свою задачу, восстановить значения регистров, и выполнить команду возврата из прерывания, которая восстанавливает адрес прерванной программы и регистр признаков, так что прерванная программа продолжит свое выполнение с того места, где была прервана. Аппаратные прерывания ----------------------------------------------------------------- Аппаратные прерывания вызываются событиями, физически связанными в аппаратуре с соответствующими векторами прерываний. Например, клавиатура в PC связана с прерыванием 9. Нажатие клавиши вызывает прерывание выполняемой программы, как было описано выше, и переход по адресу, находящемуся в векторе прерывания, соответствующему прерыванию 9. В памяти этот вектор находится по адресу 0х24 (9*4 байт ). - 24 - Программные прерывания ----------------------------------------------------------------- Программные прерывания происходят при выполнении в текущей программе команды INT с номером прерывания в качестве операнда. В остальном нет никакой разницы между программным и аппаратным прерыванием. ДОС - однозадачная операционная система ----------------------------------------------------------------- Работу TSR-программ можно понять, изучая условия, в которых выполняются нерезидентные программы. ДОС была сделана для поддержки работы только одной задачи. ОС организует загрузку и выполнение задач и выполняет запросы на ввод-вывод. Она управляет дисковыми каталогами и файлами, работает с системными часами, выводит данные на печать, консоль, и возвращает программе символы, введенные с клавиатуры. ДОС - это в сущности сервер, обслуживающий иерархическую файловую систему и записе-ориентированные устройства, и обеспечивающий одному пользователю выполнение одной задачи. И с этой службой ДОС справляется. После первоначальной загрузки ДОС память размером в 640К, имеющаяся в PC (или в размерах памяти вашего компьютера) распределена, как показано на рис.11.1. Память с адресами от 0 до 0х400 зарезервирована для векторов прерываний.За ними следует программа ДОС. Затем идут драйверы устройств,загруженные вместе с ДОС.Например, при использовании виртуального диска или драйвера терминала ANSI, программы этих драйверов располагаются после ДОС. После драйверов идет резидентная часть командного процессора. Эта программа обрабатывает командную строку и выполняет программы, и она разделена на резидентную и нерезидентную части. Нерезидентная - 25 - область (Transient Program Area - TPA) находится после резидентной части командного процессора. Запущенная пользователем из командной строки программа загружается в TPA. В конце TPA находится область нерезидентной части командного процессора. Пользовательская программа может использовать эту область. В этом случае резидентная часть командного процессора подгружает нерезидентную после окончания программы. ______________________________________________________ | | | КОМАНДНЫЙ ПРОЦЕССОР | | (нерезидентная часть) | |______________________________________________________| | | | НЕРЕЗИДЕНТНАЯ ОБЛАСТЬ | |______________________________________________________| | | | КОМАНДНЫЙ ПРОЦЕССОР | | (резидентная часть) | |______________________________________________________| | | | ДРАЙВЕРЫ УСТРОЙСТВ | |______________________________________________________| | | | Д О С | |______________________________________________________| | | | ВЕКТОРЫ ПРЕРЫВАНИЙ | |______________________________________________________| рис.11.1. Карта памяти ДОС. После загрузки командным процессором программы она начинает выполняться. При необходимости обращения к ДОС, например, для выполнения операции с файлом, вызов ДОС осуществляется программным прерыванием с передачей параметров через регистры. В - 26 - зависимости от параметров ДОС выполняет одну из своих функций. Вызвавшая ДОС программа находится в состоянии ожидания. Результаты функций ДОС возвращаются в регистрах и с помощью установки флажка переноса. Такая последовательность событий описывает работу типичной однозадачной ОС. ДОС не обеспечивает одновременное нахождение в памяти и выполнение пользовательских программ, или поддержание информации о более, чем одной задаче в памяти. Единственный метод сделать несколько задач активными в памяти - оформить (несколько) задач как программы обработки прерываний. Для ДОС эти программы будут выглядеть как ISR-программы, поддерживающие выполнение единственной задачи. В ДОС задаче разрешается порождать подчиненную подзадачу, но только одна из них может быть активной в текущее время. Главная задача будет бездействовать, пока не завершится подзадача. Для правильной работы ISR-программ должно быть соблюдено одно правило: избегать вызова функций ДОС, если ISR-программа может быть вызвана в результате прерывания работы самой ДОС. ISR- программы, вызываемые только из нерезидентных программ с помощью программных прерываний, не имеют таких ограничений, но асинхронные по отношению к текущей задаче ISR-программы (например, обработки прерываний от таймера или клавиатуры) не должны вызывать ДОС. Программисты узнали через некоторое время, что TSR-программы, будучи вызванными, не должны использовать функции ДОС. Если резидентная программа читает с клавиатуры с помощью функций ROM-BIOS (BIOS, "зашитый" в ПЗУ) и пишет на экран теми же функциями или путем прямого доступа к буферу экрана, она выполняется без проблем; но при попытке использования такой программой функции ДОС для чего-либо, система "ломается". Функции ДОС не реентерабельны; когда резидентная программа прерывает выполнение функции ДОС и сама вызывает функцию ДОС, система превращается в мусор. Казалось бы, что нереентерабельность программ ДОС ограничивает TSR-программы функциями, позволяющими работать с памятью и использовать возможности ROM-BIOS. Это ограничение не приводит к большим жертвам. После того, как вы - 27 - узнаете, как были созданы функции "окон", вы сможете прекрасно обойтись без ДОС. К этой аномалии создатели ДОС добавили трюк, включив в состав ДОС программу печати со спулингом по имени PRINT.COM. Спулинг - это слово из прошлых времен, означающий "одновременное оперативное выполнение периферийных операций". Спулинг при печати позволяет печатать файл в то время, как пользователь выполняет на компьютере другую задачу, для которой не требуется принтер. PRINT.COM является резидентной программой, остающейся в памяти, поддерживающей очередь запросов на печать, и печатающей заданные файлы; одновременно с этим возможности компьютера и ДОС остаются доступными пользователю. Наличие программы, читающей дисковые файлы, читающей и записывающей в файл очереди на печать, и переводящей страницы на принтере, пока пользователь делает что-нибудь еще, позволяет предположить, что в ДОС реализуется некоторая мультизадачность без объяснения миру, как это делается. Это предположение и пытливая натура поколения хэккеров, конечно, подталкивали к открытию возможностей ДОС по ограниченной мультизадачности. Некоторое время ключи не удавалось подобрать; затем те, кто понял технику написания функционально полных TSR-программ, хранили секрет, так они продавали эти программы, и, наверно, хотели задушить конкурентов. Но остальные, однако, начали "взламывать" чужие программы и открыли секрет. Сегодня аккуратный автор может скомбинировать шаги, необходимые для написания такой программы, прочитав массу технических журналов и сотни выдержек из электронных бюллетеней. В то время, как вы читаете эту книгу, возможно печатаются еще книги, подобные этой, чтобы помочь объяснить суть дела. TSR-ПРОГРАММЫ ----------------------------------------------------------------- При работе ДОС и запуске из командной строки TSR-программы выполняются как нормальные нерезидентные программы. По сути, ни у - 28 - ДОС, ни у командного процессора нет способов узнать, что эти программы станут резидентными, до того, как они завершатся с помощью одной из TSR-функций ДОС. При завершении программы информируют ДОС, сколько памяти надо зарезервировать. Насколько известно, в ДОС это действие приведет лишь к установке нижнего адреса нерезидентной области на значение сразу после конца TSR-программы и уменьшению размера доступной для нерезидентных программ памяти на размер TSR-программы. На рис.11.2 показана карта памяти системы при наличии двух TSR-программ. ______________________________________________________ | | | КОМАНДНЫЙ ПРОЦЕССОР | | (нерезидентная часть) | |______________________________________________________| | | | НЕРЕЗИДЕНТНАЯ ОБЛАСТЬ | |______________________________________________________| | | | TSR-программа #2 | |______________________________________________________| | | | TSR-программа #1 | |______________________________________________________| | | | КОМАНДНЫЙ ПРОЦЕССОР | | (резидентная часть) | |______________________________________________________| | | | ДРАЙВЕРЫ УСТРОЙСТВ | |______________________________________________________| | | | Д О С | |______________________________________________________| | | | ВЕКТОРЫ ПРЕРЫВАНИЙ | |______________________________________________________| - 29 - рис.11.2. Карта памяти ДОС с двумя TSR-программами. TSR-программы существуют в двух вариантах: программы обработки прерываний и резидентные утилиты. Разница между ними незначительна, но она есть. Программы обработки прерываний. ----------------------------------------------------------------- Программы обработки прерываний реагируют на прерывания от аппаратуры или от программ и обычно предназначены для поддержки различных устройств. Примером такой программы является программа, вызываемая прерыванием от таймера. Пример в главе 12 показывает, как можно использовать прерывание от системного таймера для отображения даты и времени на экран. Программа, поставляемая вместе с "мышью" фирмы Microsoft под названием MOUSE.COM, обрабатывает аппаратные прерывания, возникающие при перемещении "мышки". MOUSE.COM также обрабатывает программные прерывания от программ, которым требуется определить местонахождение "мышки" и состояние ее кнопок. Резидентные утилиты могут включать несколько таких программ. Резидентные утилиты. ----------------------------------------------------------------- Резидентные утилиты - это программы обработки прерываний, обычно не поддерживающие какое-либо устройство, но реагирующие на нажатие определенной "горячего ключа" и запускающие процессы по запросу пользователя. Эти утилиты сохраняют состояние компьютера на момент своего вызова, и восстанавливают это состояние после окончания своей работы. Типичные резидентные утилиты используют технику "окон" для связи с пользователем. - 30 - Что может быть резидентным. ----------------------------------------------------------------- После того, как вы прочтете эту и следующую главу, вы научитесь писать TSR-программы на Турбо Cи. У вас может возникнуть тенденция писать все в виде резидентных программ и вызывать все, что возможно, по нажатию клавиши, но себя надо сдерживать. Не все может или должно быть резидентным. Помните, что после добавления резидентной программы в память ее величина уменьшается. Вы можете дойти до того, что не останется памяти для выполнения обычных программ. Можно быть богатым на "всплывающие" утилиты, но не иметь памяти, чтобы сделать маленькую табличку или составить записку на своем текстовом редакторе. Помните также, что вы увеличиваете встроенные ограничения ДОС, вводя первую резидентную программу в память. Не является сюрпризом, что TSR-программы из разных источников не уживаются. Далее, некоторые TSR-программы плохо работают с некоторыми нерезидентными программами. Некоторые TSR-программы должны быть загруженными после всех других резидентных программ; они должны быть первыми в наборе программ, обрабатывающих прерывания. Несколько таких программ не могут работать вместе, так как только одна из них может быть последней. Программа Sidekick является примером такой программы; она должна быть последней загруженной, так как она перехватывает прерывания таймера из любой программы, загруженной после нее. Такие несообразности являются результатом попыток вставить мультизадачность в ДОС. К чести создателей TSR-программ они старались прийти к соглашениям по стандартам таких программ. Они делали попытки сотрудничества и определения "хорошо ведущих" себя TSR-программ, но ни один такой стандарт до сих пор не опубликован. Также делались попытки определения такой структуры нерезидентных программ, которая позволяла бы им гармонично - 31 - сосуществовать с "правильными" TSR-программами. Создатели программ для PC привыкли работать с однозадачной ДОС и ограничениями оборудования PC определенным образом. Чтобы преодолеть ограничения ДОС и получить требуемые характеристики, многие программисты "напрямую" сканировали клавиатуру и выводили данные прямо в видеопамять. Этот метод предполагал наличие только одной программы и отсутствие соперничества из-за этих ресурсов. Такие ограничения встают на дороге TSR-программ, которым нужно быть второй задачей, не мешающей первой. Фирма Borland поставляет программу Sidekick Plus, управляющую работой TSR-программ. Она обеспечивает запуск и взаимодействие семейства TSR-программ, которые созданы с ее помощью. Преймущество такого решения - в достижении совместимости TSR-программ. Явным недостатком является необходимость иметь Sidekick Plus для запуска таких программ и то, что многие резидентные программы, созданные независимо, не будут работать в этой среде. Чтобы решить, делать свою программу резидентной или нет, мы предлагаем использовать следующие правила: - Размер программы. Если программа не может быть создана в крохотной модели памяти (64К на код, данные и стек), то вероятнее всего она не должна быть резидентной. Внушите себе это правило - и это убережет вас от засорения памяти резидентными утилитами за счет нужных вам обычных программ. Отметим, что TSR-драйвер, описанный в главе 12, одинаково хорошо работает c программами в крохотной и малой моделях. - Частота использования утилиты. Не советуем вам делать резидентной программу расчета доходов. Программы, запускаемые с частотой один раз в день должны быть нерезидентными, а один раз в час - могут стать TSR. Не перегружайте систему редко используемыми резидентными программами. - Ресурсы, необходимые утилите. Если программа должна - 32 - запрашивать дополнительную память или ее характеристики снижаются при ограничениях на память, она не должна быть резидентной; ведь она может быть вызвана во время выполнения любой другой программы, которая может занять всю или большую часть памяти. Если выполняется COM-программа, ДОС считает, что занята вся память. - "Всплывающая" природа задачи. Если программа используется в дополнение к другим - это хороший кандидат в резидентные утилиты. Например, удобно, когда можно вызвать систему компоновки текстов при работе с текстовым процессором. То же самое относится к программе-словарю. Калькулятор и записная книжка необходимы почти все время. Синтаксический анализатор программ на Си может стать полезной TSR-программой при использовании текстового редактора для подготовки программ. Но, однако, не нужно делать все программы вызываемыми по нажатию клавиши. - Время, нужное для выполнения. Главное преймущество "всплывающих" программ - это их немедленная доступность и возможность выполнения без отрыва от основной работы, выполняемой на компьютере. Если же утилита требует для выполнения целый день, то нет смысла делать ее резидентной. (Люди привыкают к удобствам. Еще несколько лет назад пользователи были довольны, если ответ на запрос к базе данных приходил из центральной машины за ночь. Сейчас же они ворчат, если им надо сохранить данные из электронной таблицы и возвратиться в ДОС, чтобы использовать программу для работы с модемом.) ПОСТРОЕНИЕ TSR-ПРОГРАММ ----------------------------------------------------------------- При написании резидентной программы вам придется решить много проблем. Некоторые из них незначительны, некоторые - 33 - разрешаются при использовании расширений стандартных библиотек Турбо Си, но некоторые являются крепким орешком. Использование ассемблера для некоторых задач является более удобным, но в этой книге везде старались максимально использовать Си, где это только предоставлялось возможным. Превращение программы в резидентную. ----------------------------------------------------------------- Чтобы стать резидентной, программа должна объявить себя соответствующим образом. Здесь программисту может помочь документация на ДОС. О двух нужных для этого функциях ДОС уже было упомянуто. Чтобы их использовать, надо знать размер программы, а для этого надо знать, как она строится в Турбо Си. Это в дальнейшем будет коротко объяснено. Если программа запущена на выполнение, присоединила себя к нужному вектору прерывания и сделала все, что требуется для превращения в резидентную, то она может вызвать одну из TSR-функций ДОС. Действие этих функций одинаково, и можно использовать функцию 0х31 прерывания 0х21 чтобы объявить программу резидентной. Далее следует фрагмент программы на Турбо Си, демонстрирующий эту возможность: #include static struct REGS rg; unsigned int sizeprogram; rg.x.ax = 0x3100; rg.x.dx = sizeprogram; intdos(&rg,&rg); Переменная sizeprogram должна содержать размер программы в шестнадцатибайтных параграфах. Резидентна ли уже программа? ----------------------------------------------------------------- - 34 - Помните, что ДОС неизвестно, что ваша программа стала резидентной и каково ее имя (эта информация доступна, но ДОС не работает с ней). После выполнения TSR-программы несколько раз несколько ее копий будет находиться в памяти, поэтому программе надо проверять, не загружена ли уже ее копия. Простейшим способом для организации такой проверки является применение одного из неиспользуемых прерываний. При первом старте TSR-программа выполняет это прерывание, проверяя возвращаемое значение. Если это значение не равно установленному в программе, то можно объявлять себя резидентной, и программа присоединяется к этому прерыванию. После этого вызов этого прерывания будет приводить к возврату определенного значения, и другие копии загруженной программы после проверки не будут объявлять себя резидентными. Векторы 0х60-0х67 всегда доступны для использования, и можно выбрать один из них. Но нет уверенности, что другая программа, взятая из другого источника, не выберет тот же вектор. Помните, что ДОС ориентирована на одну выполняемую задачу, и этой задаче дозволено использовать любое прерывание в системе. Более предпочтительным является использование вектора прерывания, указывающего на сигнатуру (уникальную запись) в памяти программы. Вместо запуска прерывания программа проверяет, не указывает ли один из векторов 0х60-0х67 на эту сигнатуру. Если таковой указатель существует, то в памяти уже есть копия программы; если же такого не находится, то программа объявляет себя резидентной и устанавливает адрес одного из свободных прерываний на сигнатуру. К несчастью, нет способов оградить этот вектор от посягательтв другой программы. Тут уж ни в чем нельзя быть уверенным. Чтобы найти неиспользуемый вектор прерывания, надо проверить их все на нулевое значение. Вектор, в который записан ноль, и есть неиспользуемый. В документации указано, что прерывания 0х60- 0х67 доступны для использования программами. - 35 - Неиспользуемые векторы прерывания можно применять для других видов связи между программами. Некоторые TSR-программы можно применять для задания параметров для своей же копии в памяти. При выполнении таких программ они передают новые параметры своим копиям через вектор прерывания. Вектор может указывать на подпрограмму обработки прерывания в TSR-программе; а сигнатура находиться с определенным смещением с том же сегменте. Найдя сигнатуру, вторая копия программы может взаимодействовать с первой через этот вектор. TSR-драйвер, описанный в главе 12, демонстрирует такой способ. Захват прерывания. ----------------------------------------------------------------- Вы можете использовать прерывания не только для связи между программами. По прерыванию может выполняться ваша программа, если на нее указывает соответствующий вектор. В Турбо Си легко можно выполнить подобный захват прерывания. С помощью функции setvect можно оперативно выполнить эту задачу. Надо объявить функцию типа interrupt, которая будет обрабатывать прерывания, и записать ее адрес в нужный вектор. Например: setvect(vno,isr); Параметр vno - это int от 0 до 255, а isr - это адрес подпрограммы обработки прерывания, которая может быть описана так: void interrupt isr(); Совместное использование прерываний. Если вы используете прерывания, нужные другим программам, то надо делать это так, чтобы другие не замечали ваших действий. - 36 - Например, если ваша программа присоединится к прерыванию от таймера и не позволит оставшейся части системы использовать это прерывание, все процессы, работающие по таймеру, будут недоступны все то время, пока ваша программа будет находиться в памяти и удерживать прерывание. В этом случае, например, остановятся системные часы. Вы можете присоединиться также к прерываниям от клавиатуры, прерываниям для вызова функций ДОС, дисковых функций BIOS и другим прерываниям поддержки TSR-программ. В любом из этих случаев надо совместно использовать прерывания с другими процессами, нуждающимися в них. Возможность работать с прерыванием другим программам выполняется следующим образом. Надо прочитать старый адрес прерывания, по которому передавалось управление до загрузки вашей программы. В библиотеке Турбо Си есть функция getvect для чтения содержимого вектора прерывания. Надо объявить interrupt указатель на функцию, и записать адрес из вектора в этот указатель: void interrupt (*oldisr)(); oldisr = getvect(vno); Затем, запишите адрес вашей подпрограммы обработки прерывания в вектор, используя функцию setvect, описанную выше. В этой подпрограмме вы можете обеспечить выполнение старой ISR, после предварительной обработки или без нее передавая управление по адресу старой ISR. Эти принципы обсуждаются в главе 12. Величина TSR-программы. ----------------------------------------------------------------- - 37 - При объявлении программы резидентной надо специфицировать ее размер, чтобы ДОС было известно, сколько памяти отвести для нее. Для безопасности можно отвести каждой программе по 64К, но при этом значительная часть занятой памяти может не использоваться. Легко определить размеры программы, написанной на ассемблере, но внутренняя структура программы на Си скрыта от программиста. Чтобы вычислить размер программы, надо знать, как компилятор Турбо Си строит программу. Рисунок 11.3 демонстрирует структуру типичной программы в крохотной модели памяти, построенную Турбо Си. Program Segment Prefix (PSP) - это конструкция ДОС, помещаемая в начале каждой программы. Она будет описана позже. Машинный код программы следует сразу за PSP. Переменные,объявленные static или external, и инициализированные при объявлении, следуют за кодом, а за ними идут неинициализированные static или external переменные. Следующая часть программы - "куча", область динамически распределяемой памяти. Ее размер зависит от того, сколько программа будет распределять памяти. Область стека располагается за "кучей", и его размеры увеличиваются в обратном направлении. Первоначально вершина стека указывает на конец области в 64К, занимаемой программой в крохотной модели. При использовании стека его указатель двигается в сторону уменьшения адресов. Величина стека зависит от глубины вложенности функций и количества локальных данных, используемых в этих функциях. При вызове функции в стек помещаются параметры и значения регистров, которые необходимо сохранить. Если функция использует локальные automatic-переменные, они тоже запоминаются в стеке. Функция, вызываемая рекурсивно и использующая большое количество параметров и automatic-переменных, будет использовать большую часть стека. Из-за динамической природы "кучи" и стека вы можете только делать некоторые предположения о размерах программы. ______________________________________________________ | | | СТЕК | |______________________________________________________| - 38 - | | | "КУЧА" | |______________________________________________________| | | | НЕИНИЦИАЛИЗИРОВАННЫЕ STATIC И EXTERNAL ДАННЫЕ | BSS |______________________________________________________|<------ | | | ИНИЦИАЛИЗИРОВАННЫЕ STATIC И EXTERNAL ДАННЫЕ | DATA |______________________________________________________|<------ | | | | | КОД ПРОГРАММЫ | | | TEXT |______________________________________________________|<------ | | | P S P | |______________________________________________________| рис.11.3. Структура программ с крохотной моделью памяти в Турбо Си. Приблизительно подсчитать размер программы можно, используя MAP-файл, генерируемый программой TLINK. В его начале находится информация, подобная следующей: Start Stop Length Name Class 00000H 010BAH 010BBH _TEXT CODE 010C0H 013D8H 00319H _DATA DATA 013DAH 013DDH 00004H _EMUSEG DATA 013DEH 013DFH 00002H _CVTSEG DATA 013E0H 013E5H 00006H _SCNSEG DATA 013E6H 014EDH 00108H _BSS BSS 014EEH 014EEH 00000H _BSSEND BSSEND Самая правая колонка, с заголовком class, показывает тип сегментов, представленных значениями в левых колонках. CODE - 39 - содержит код программы, DATA содержит инициализированные переменные, а BSS - неинициализированные. Значение в колонке Stop равно шестнадцатиричному адресу конца области неинициализированных переменных и начала "кучи". Программа, не использующая стека и "кучи", будет иметь размер, равный этому значению + 256 байт на PSP. Чтобы оценить размер "кучи", посмотрите, сколько и как вы используете функции распределения памяти. При использовании оконных функций из предыдущих глав требования к "куче" можно оценить исходя из количества и размеров одновременно существующих окон. Каждое окно требует буфера размером с удвоенное произведение ширины на высоту окна. Этот буфер удаляется из "кучи" при закрытии окна, поэтому максимальное использование "кучи" будет в момент создания максимального количества окон на экране. Надо учитывать также и использование вами функций распределения памяти. Если ваша программа распределяет память в зависимости от внешних условий, таких, как ввод пользователя или зависимости между данными, необходимы надежные обнаружение и обработка ошибок. Не взирая на ошибку, никогда не вызывайте функцию exit из резидентной программы. Вместе с определением размеров "кучи" определяется и ее верхняя граница, а, следовательно, и нижняя граница стека. Осталось теперь найти его верхнюю границу. При первом выполнении программы начало стека устанавливается на отметку 64К. При завершения и объявления себя резидентной программа сообщает ДОС свои размеры, и ДОС использует всю остальную память для загрузки других программ. При вызове TSR-программа должна установить указатель стека внутри себя, а не на другую программу. Если размер объявлен меньше 64К, то указатель стека должен быть также установлен ниже 64К. Лучший метод найти оптимальный размер стека - метод проб и ошибок. Сначала запустите программу на 64К, а затем смещайте вершину стека к меньшим адресам. При каждом таком передвижении испытывайте программу в условиях максимального использования - 40 - стека и "кучи". Продолжайте эксперименты до тех пор, пока ваша программа не будет "вешать" систему или неправильно выполняться. Затем поставьте вершину стека на безопасное смещение и интенсивно используйте программу, пока не поверите наконец, что стек безопасен для "кучи". Было бы хорошо, если бы имелся более научный и точный способ определения размеров программы, но этот подход работает, и кажется, что это единственно реальный метод. Переключение контекстов. ----------------------------------------------------------------- При первом исполнении TSR-программы она использует все ресурсы, предоставляемые ДОС нормальной задаче. После завершения и объявления себя резидентной эти ресурсы отдаются другим программам или, при отсутствии выполняемых программ, командному процессору ДОС. При выполнении TSR-программы в результате "горячего ключа" она "паразитирует" на прерванной программе. ДОС неизвестно, что начала выполняться другая задача, и все ресурсы по-прежнему принадлежат выполнявшейся раннее задаче. Поэтому системные указатели на эти ресурсы должны быть изменены так, чтобы TSR-программа стала выполняемой задачей, "известной" ДОС. Такая передача ресурсов между задачами называется переключением контекстов, и мультизадачные ДОС делают это автоматически. В однозадачной ДОС PC однако, переключения контекстов не производится, и прерывающая задача должна делать это сама. Стек. ----------------------------------------------------------------- Для всех программ нужен стек. У резидентной программы есть свой стек, но после прерывания текущей задачи указатель стека и - 41 - сегмент стека в компьютере указывают на стек прерванной задачи. Может показаться, что лучшее решение - это использовать стек прерванной программы. На деле многие ассемблерные TSR-программы так и делают, но для этого приходится ограничивать использование стека. Но, во-первых, неизвестно, какой размер стека был у прерванной программы. А во-вторых, ДОС гарантирует достаточный размер стека только для сохранения регистров. Си - язык с интенсивным использованием стека, и вам понадобится больший его размер, чем обеспечивает ДОС, и это значит, что надо переключаться на собственный стек. Переключение на собственный стек означает, что надо запомнить значение региста сегмента стека до переключения. И это значение должно быть восстановлено до передачи управления в прерванную программу. Регистры сегментов и указателей могут быть прямо адресованы в Турбо Си использованием псевдопеременных _SS и _SP. При объявлении резидентной TSR-программа запоминает собственный сегмент стека. После вызова она запоминает контекст стека прерванной программы и устанавливает свой стек. Это производится с помощью установки регистра сегмента стека на значение, запомненное при первом запуске, и указателя стека на величину, вычисленную из размеров программы. Если TSR-программа реентерабельна, то есть может прерывать сама себя, переключение стеков может привести к ошибке. При втором переключении стека вы перезапишете область сохранения стековых регистров. Чтобы избежать этого, надо писать нереентерабельные резидентные программы. Это небольшая потеря - резидентные программы не нуждаются в том, чтобы быть реентерабельными (вам не нужно прерывать свою программу-калькулятор, чтобы запустить еще одну такую же). Чтобы сделать TSR-программу нереентерабельной, устанавливайте флаг при ее вызове. Он должен оставаться установленным то окончания работы TSR-программы. При повторном вызове (например, из-за случайного нажатия "горячего ключа") проверяется установка флага и вторичного запуска не производится. - 42 - Program Segment Prefix (PSP). ----------------------------------------------------------------- PSP - это управляющая область в 256 байт, которая строится в памяти в начале каждой программы. Она содержит различные поля, используемые ДОС для управления выполнением программы. На рис. 11.4 показана ее структура. Далее будут обсуждаться все поля PSP. Заметим, что многие эти поля не были официально описаны фирмами Microsoft или IBM. Они используются так, как описано ниже, но их использование или модификация в прикладной задаче не санкционировано при продаже. Знание этих полей - подарок от хэккеров, расчленивших ДОС и напечатавших о своих находках. Эти данные верны для популярных версий ДОС - 2.0, 2.1, 3.0, 3.1, 3.2, 3.3, за исключением специально оговоренных случаев. ДОС 4.0 не публиковалась в США, и, как утверждается, будущие версии ДОС будут поддерживать мультизадачность и будут предназначены только для компьютеров с процессорами 80286/80386. Использование полей PSP описанным способом совершенно безопасно. Многие популярные коммерческие программы делают это точно так же. ______________________________________________________ | | | Вызов прерывания для завершения процесса | 0000 |______________________________________________________| | | | Сегментный адрес верхней границы памяти | 0002 |______________________________________________________| | | | 0 | 0004 |______________________________________________________| | | | Команда вызова диспетчера функций ДОС | 0005 |______________________________________________________| | | | Адрес обработчика завершения | 000A |______________________________________________________| - 43 - | | | Адрес обработчика Ctrl-Break | 000E |______________________________________________________| | | | Адрес обработчика критических ошибок | 0012 |______________________________________________________| | | | Сегментный адрес PSP родителя | 0016 |______________________________________________________| | | | Таблица указателей файлов | 0018 |______________________________________________________| | | | Сегментный адрес области системных параметров | 002C |______________________________________________________| | | | Адрес стека на время вызова функции ДОС | 002E |______________________________________________________| | | | Размеры таблицы указателей файлов | 0032 |______________________________________________________| | | | Адрес таблицы указателей файлов | 0034 |______________________________________________________| | | | Зарезервировано ДОС | 0038 |______________________________________________________| | | | Блок управления файлом #1 | 005C |______________________________________________________| | | | Блок управления файлом #2 | 006C |______________________________________________________| | | | Остаток командной строки/Дисковый буфер | 0080 |______________________________________________________| - 44 - рис.11.3. Структура PSP. Вызов прерывания для завершения процесса.(PSP:0) ------------------------------------------------ Это поле содержит команду INT 0x20; это сделано для поддержки программ, перенесенных из CP/M в ДОС. В CP/M программа завершает свое выполнение переходом на свой нулевой адрес. Сегментный адрес верхней границы памяти.(PSP:2) ----------------------------------------------- При выполнении программы ДОС выделяет ей участок памяти, в который программа загружается. Это поле содержит сегментный адрес конца этого участка памяти. Адрес обработчика завершения.(PSP:0xa) -------------------------------------- При выполнении программы ДОС запоминает текущее содержание вектора прерывания 0х22 в этом поле. После завершения программы ДОС восстанавливает значение, используя это поле. Вектор прерывания 0х22 указывает на системный обработчик завершения программ. Адрес обработчика Ctrl-Break.(PSP:0xe) -------------------------------------- При выполнении программы ДОС запоминает текущее содержание вектора прерывания 0х23 в этом поле. После завершения программы ДОС восстанавливает значение, используя это поле. Вектор прерывания 0х23 указывает на системный обработчик Ctrl-Break. - 45 - Адрес обработчика критических ошибок.(PSP:0x12) ----------------------------------------------- При выполнении программы ДОС запоминает текущее содержание вектора прерывания 0х24 в этом поле. После завершения программы ДОС восстанавливает значение, используя это поле. Вектор прерывания 0х24 указывает на системный обработчик критических ошибок. Отметим, что ДОС восстанавливает значения этих трех векторов при завершении программы и объявлении себя резидентной. Если TSR надо перехватывать эти прерывания, то придется присоединять себя к ним при каждом вызове. Сегментный адрес PSP родителя.(PSP:0x16) ---------------------------------------- Любая программа выполняется в результате обращения другой программы к ДОС. Обычно программой-отцом является командный процессор ДОС (COMMAND.COM), хотя любая программа может быть родителем любой другой. Это поле в PSP содержит сегментный адрес PSP программы-отца. У командного процессора нет "папаши", поэтому это поле в PSP командного процессора содержит сегментный адрес собственного PSP, что является указателем на самого себя. Таблица указателей файлов.(PSP:0x18) ----------------------------------------------------------------- Это поле представляет собой массив в 20 байт,каждый из которых представляет указатель файла(file handler). При открытии файла в программе ДОС возвращает в программу номер файла для использования при обращении к ДОС для записи в файл и чтения из - 46 - него. (Программы на Си, использующие потоковый ввод-вывод, прямо не обращаются к этим номерам - они используют указатель FILE, определенный в stdio; однако стандартные библиотечные функции Си, поддерживающие такой ввод-вывод, используют эти номера скрытым от вызывающей программы способом.) Эти номера файлов - номера байт в таблице указателей файлов, а элементы этой таблицы хранят значения, указывающие на соответствующую файлу структуру в системных таблицах управления файлами. Сегментный адрес области системных параметров.(PSP:0x2c) -------------------------------------------------------- Это поле содержит сегментный адрес области системных параметров, создаваемой ДОС для выполняемой программы. Область системных параметров - это выделенный задаче участок памяти, который может быть освобожден, если значения параметров не используются. Адрес стека на время вызова функции ДОС.(PSP:0x2e) -------------------------------------------------- В это поле ДОС записывает значения регистров сегмента стека и указателя стека текущей программы при вызове из программы функции ДОС. Затем ДОС переключается на свой собственный стек. Перед возвратом в вызвавшую программу ДОС использует эти значения для восстановления значения этих регистров. Размеры таблицы указателей файлов.(PSP:0x32) -------------------------------------------- Это поле содержит счетчик количества вхождений в таблицу указателей файлов. Обычно его значение равно 20. Заметим, что это поле не используется в версиях ДОС до 3.0. - 47 - Адрес таблицы указателей файлов.(PSP:0x32) ------------------------------------------ Это поле содержит полный (long) адрес таблицы указателей файлов; сегмент этого адреса равен сегменту PSP; а смещение - 0х18. Заметим, что это поле не используется в версиях ДОС до 3.0. Вероятно, предыдущие два поля добавлены, чтобы позволить программе использовать более, чем 20 одновременно открытых файлов. После выделения большей таблицы надо поместить ее адрес и количество файлов в эти поля, скопировать в нее 20 значений из PSP, и таким образом программа может увеличить количество одновременно открытых файлов по сравнению со значением из оператора FILES=файла CONFIG.SYS. Блок управления файлом #1.(PSP:0x5c) ------------------------------------ Это поле содержит блок управления файлом, который строится ДОС в случае, если в командной строке в качестве первого параметра было указано имя файла. Блок управления файлом #2.(PSP:0x6c) ------------------------------------ Это поле содержит блок управления файлом, который строится ДОС в случае, если в командной строке в качестве второго параметра было указано имя файла. Предыдущие два поля созданы для поддержки программ, конвертированных из CP/M. Остаток командной строки/Дисковый буфер.(PSP:80) - 48 - ------------------------------------------------ Последнее поле также создано под влиянием CP/M. После запуска программы все, что находилось в командной строке, начиная со второго символа после имени программы, строится в виде строки слов и записывается в это поле. Лишние пробелы удаляются. В первый байт поля записывается количество символов в этой строке. Во время выполнения программы это поле служит дисковым буфером для работы с файлами при помощи старых функций ДОС, использующих блоки управления файлами. Это поле также используется функциями ДОС, работающими с дисковыми каталогами. Контекстное переключение PSP. ----------------------------- У каждой программы есть свой PSP. Но ДОС известен только один PSP - находящийся перед программой, запущенной последней. Программа в ДОС может порождать выполнение другой программы, и программы-дети могут наследовать значения из PSP "папаши", но, так как ДОС знает только об одной активной задаче, то и только об одном PSP. PSP содержит несколько интересных полей, но самое интересное для дальнейшего обсуждения - это массив из 20 указателей файлов. Программа может открыть до 20 файлов одновременно. С каждым файлом ассоциируется указатель, являющийся элементом массива в PSP. В этом массиве есть место для 20 указателей, и первые пять из них отданы для логических устройств stdin, stdout, stderr, stdaux, stdprn. При открытии файлов новые указатели записываются в массив, неиспользуемые позиции массива имеют значение -1. К файлам программа обращается по номерам указателя в массиве, а в элементе массива с таким номером хранится ссылка на соответствующую структуру в системных таблицах управления файлами. - 49 - При открытии файла в TSR-программе его указатель записывается в PSP этой программы. При вызове TSR-программы ДОС считает, что выполняется по-прежнему прерванная программа. При обращении к ДОС для работы с файлом, открытым при первоначальном запуске TSR-программы, ДОС будет брать указатель файла с данным номером из PSP прерванной программы. Поэтому возможно или обращение к чужому файлу, если файл с таким номером открыт и в прерванной программе, или ошибка по обращению к неоткрытому файлу. Разрешить эту проблему можно следующим образом: - не открывать файлов во время инициализации TSR-программы; открывать их после вызова и прерывания другой программы. Тогда для файлов TSR-программы будут использоваться указатели в PSP прерванной программы. - не использовать работу с указателями файлов ДОС 2.0 и выше. Пользоваться FCB(File Control Block) из ДОС 1.1. Эти функции не используют PSP. Таблицы FCB строятся в области данных программы, использующих файлы. - переключить системный указатель PSP при вызове TSR-программы на ее PSP, и после отработки вернуть все на свои места. Каждое из этих решений имеет свои недостатки: - При использовании таблицы указателей файлов из PSP прерванной программы имеются два серьезных недостатка. Во-первых, может оказаться неприемлемым открывать и закрывать файлы при каждом вызове TSR-программы. Во-вторых, нельзя быть уверенным, что в таблице прерванной задачи достаточно места для указателей файлов TSR-программы. - Использование функций с FCB имеет два недостатка. Во-первых, файлы, открываемые с помощью FCB, должны - 50 - быть в текущем подкаталоге. Пути для спецификации файла указывать нельзя. Во-вторых, все стандартные библиотечные функции Турбо Си для работы с файлами предполагают использование более гибких указателей. Для использования FCB вы должны создать функции, эквивалентные стандартным open, close, read, write, fclose, fget, fput, fprintf и т.д. - Не имеется документированной функции ДОС для изменения адреса PSP. Есть документированная функция для чтения текущего PSP (INT 0x21, функция 0x62), но она доступна только в ДОС версии 3.0 и выше. Две функции ДОС (0х50 и 0х51), не описанные в документации, устанавливают и записывают адрес PSP, но они недоступны для использования резидентными программами в ДОС 2.0 и 2.1. Они разделяют стек с некоторыми функциями ДОС, которые могут быть прерваны резидентной программой. Если TSR-программа использует функции 0х50 и 0х51 в этом случае, система зависнет. Эти функции можно использовать только с ДОС версий 3.0 и выше. Адрес PSP называется идентификатором процесса (Process ID - PID). Две скрытые функции ДОС носят название GetPID (получить идентификатор процесса) и SetPID (установить идентификатор процесса). Ни одно из этих решений не является удовлетворительным. Необходимы работающие заменители функций GetPID и SetPID. Одно из решений - экспериментально определить адрес, куда ДОС записывает PID. Этот адрес, конечно, зависит от версии ДОС и может быть уникальным даже при использовании одной версии ДОС. Использование таких значений является опасным решением. Лучшим является определение адреса PID оперативно во время выполнения TSR-программы. Найти адреса, куда ДОС записывает PID, можно, используя следующую процедуру. Сначала, текущий PID восстанавливается с использованием функции GetPID. Затем по памяти, занимаемой ДОС, осуществляется поиск копии значения PID. Как только такое значение находится, его адрес запоминается и PID изменяется на какое-либо значение с помощью функции SetPID. Значение по найденному адресу анализируется на нахождение там - 51 - нового значения. Если это так, то обнаружен адрес, куда в ДОС записывается PID, и он запоминается. Первоначальное значение PID восстанавливается, и поиск продолжается. Версии ДОС до 3.0 сохраняют PID в двух местах. Версии от 3.0 и выше поддерживают одно значение PID. Адреса PID и PID самой TSR-программы записываются до завершения и обьявления себя резидентной. При вызове TSR-программы один из сохраненных адресов PID используется для чтения PID прерванной программы. Это значение запоминается, устанавливается значение PID резидентной программы. Перед завершением резидентной программы она восстанавливает в системе PID прерванной программы. Этот способ используется в функциях резидентного драйвера в главе 12. Дисковый буфер. ----------------------------------------------------------------- Вторая часть PSP - это дисковый буфер, область, через которую ДОС пишет и читает дисковые файлы, открытые для использования с FCB. ДОС также использует эту область для функций, работающих с подкаталогами. В библиотеке Турбо Си есть функции, читающие и устанавливающие адрес дискового буфера. Getdta возвращает адрес текущего дискового буфера, и setdta изменяет адрес. Эти функции имеют следующий формат: #include char far *dta; dta = getdta(); setdta(dta); Если TSR-программа будет использовать функции ДОС, которые - 52 - пишут в дисковый буфер, она должна сохранить адрес дискового буфера прерванной программы, установить свой, и перед завершением работы восстановить предыдущий адрес. Если TSR-программа будет писаться на Си, нельзя быть уверенным, что она не изменяет или не использует дисковый буфер. Действия функций из библиотеки Си достаточно не изучены, и лучшим решением будет самое осторожное. Резидентный драйвер, описанный в главе 12, сохраняет дисковый буфер прерванной программы.