RDM/2 The Russian Electronic Developer Magazine  
RDM/2 Русский электронный журнал разработчика  
ДомойОт редактораПишите намОбратная связьRU/2

Пpогpаммиpование в OS/2.

Дмитpий Завалишин
( Dmitry Zavalishin <dz@phantom.ru> )

Все мы время от времени сталкиваемся с чем-либо необычным, неизвестным, непонятным или просто новым для нас. Это и в обычном-то мире не редкость, а уж в компьютерном едва ли найдется год, который не ознаменовался бы глобальными переменами, и две-три революции на десятилетие у компьютерных богов наверняка припасены.
Умение быстро адаптироваться и привыкать к новым условиям - пожалуй, один из важнейших 'секретов' сегодняшнего профессионального программиста. Опытные люди знают, что ко всему в этом мире нужен правильный подход, понимание специфики, традиций, если угодно. Можно потратить массу усилий, открывая дверь автомобиля тем же методом, что и дверь кухни: сколько ни толкай - не поможет. И щеколды нет. А знай толкающий, что нужно просто повернуть одну ручку - и нет у него проблем. И наоборот: дверь кухни 'овтомобильным' способом тоже не откроешь - тут свои правила.
Статья сия имеет своей целью рассказать читатателям о 'правилах хорошего тона' при программировании в среде OS/2, о тех мелочах, понимание которых позволяет сэкономить массу усилий. Иногда для этого нужно всего лишь взглянуть на проблему с другой стороны.

Тем, кто писал только для DOS

Итак, я остановлюсь на нескольких традиционных подходах, принятых при программировании в DOS для решения некоторых типичных проблем, и постараюсь описать методы решения аналогичных проблем в OS/2.

Пpоблема пеpвая: опpос клавиатуры, мыши, часы и другие мелочи

Как правило, для написания более-менее сложных программ в DOS программисты, которым недоступна многозадачность, пользуются классическим приемом: создается 'главный цикл', который опрашивает все возможные источники событий и, при возникновении события (нажатие клавиши, изменение времени, движение мыши), вызывает подпрограмму его обработки. Если такой код использовоть без изменений в среде OS/2, то он либо поглощает массу процессорного времени (в DOS он так и делал, но вы не могли этого заметить), либо (если вставить в цикл задержку) нещадно тормозит программу. Можно сломать голову, подбирая время задержки, пытаясь изобрести задержку с изменяемым временем и прибегая к другим способам решения этой проблемы, но лучше остановиться на секунду и задуматься: почему программа была написана именно так, а не иначе ? Что породило сей 'главный цикл' ?
Очевидно: отсутствие многозадачности! Значит, в OS/2, где возможна многозадачность даже в пределах одной 'задачи' (точнее, процесса), вы можете воспользоваться ею и заставить несколько частей вашей программы работать одновременно: пусть одна подзадача (thread - 'нить') занимается часами, другая клавиатурой, третья - мышью и так далее. Необходимо просто блокировать подзадачи, ожидающие событие, для обработки которого они предназночены.
Пусть подзадача, работающая с клавиатурой, вызывает функцию чтения клавиши с ожиданием (то есть как раз ту, которую в DOS почти никто и не использовал). Подзадачу часов достаточно приостанавливать на нужное время (секунду или минуту, смотря как у вас идут часы) и так далее.

Проблема вторая и вторичная

Предположим, мы создали подзадачи, запустили их, а они, всенепременно и ничуть не дав нам порадоваться, тут же подрались внутри процесса за какой-либо ресурс: или в один файл повадились втроем записывать, или на экране кашу учинили, или просто вызвали нереентерабельную функцию библиотеки. Что делать ? Ответ удивительно стар. Не как мир, но почти. Семафоры. Семафоры -- это единственная возможность синхронизировать две задачи (одного или разных процессов) без обмена данными. Видели когда-либо табличку 'не включать, работают люди' ? Вот вам житейский пример семафора. Идея понятна: входя в опасную часть кода, задача должна повесить табличку: 'сюда нельзя, тут я сижу',- а выходя снять ее. Это позволит другой задаче дождаться перед входом в опасную зону, пока выйдет подруга, успевшая заскочить раньше. Главным полезным свойством семафора является то, что проверка его состояния и переключение в другое состояние происходит атомарно, то есть между проверкой и изменением не может произойти ничего. Почему это важно, думаю, объяснять не надо.
Ограничение нереентерабельных зон кода программы - не единственное использование семафоров. Другой вариант- передача сообщений, как правило, о том, что произошло некоторое событие. К примеру, можно организовать программу как три подзадачи: одна ожидает нажатия клавиши, вторая - щелчка мышью, и обе они при возникновении соответствующего события 'поднимают' семафор. Третья же подзадача ожидает поднятия семафора, и как только он поднимется, делает то, что требуется в соответствии с нажатой клавишей или кнопкой мыши. Ценность такой организации, в частности, в том, что программа без необходимости не загружает процессор. В многозадачной среде это немаловажно.

Проблема, вызванная отсутствием проблем с памятью

Один из вечных вопросов программирования в DOS - память, а точнее ее нехватка. Вечные свопинги на диск перед запуском другой программы, копание во временных файлах, работа с данными по маленьким кусочкам - все это отнимает массу времени и заставляет идти на ухищрения при решении, казалось бы, элементарных программистских задач. При программировании в операционных средах с виртуальной памятью (а к таковым принадлежит и OS/2) все это совершенно не имеет смысла до тех пор, пока размеры данных не приближаются к пределу адресного пространства процессов (для текущей версии OS/2 - около 512 мегабайтов, в будущих версиях предполагается увеличение). Забудьте про временные файлы, и пусть ничто не остановит вас перед, скажем, такой последовательностью операторов:
int fd = open (filename,mode);
long size_of_file = filesize(fd);
char * input = malloc(size_of_file);
read(fd, input, (unsigned) size_of_file);
( Не забудьте, что это - 32-битовый мир, и параметр sizeof третьего аргумента функции read равен 4, как и параметр long.)

Проблема с последовательными портами или 'где мой fossil' ?

DOS, что меня до сих пор удивляет, предоставлял программистам настолько убогий механизм работы с последовательными портами, что даже самые примитивные программы никогда не пользовались этим 'сервисом', а либо работали с аппаратурой портов напрямую, либо использовали так называемый fossil - 'навесной' драйвер последовательного порта. В современных ОС эта проблема уже не стоит, так как поставляемый с системой драйвер обычно отвечает основным требованиям к нему, то есть работает по прерываниям, буферизует информацию и прилично настраивается под нужды программиста. Соответственно, ни работа с портом напрямую, минуя ОС, ни fossil, уже не имеют смысла.
Последовательный порт может быть открыт в OS/2 как обычный файл, вызовом DosOpen( ..., "\\dev\\com2", ... ), и для чтения/записи используются стандартные DosRead/DosWrite. Установка параметров и управление линиями модема (DTR, например), а также получение состояния линий статуса модема осуществляются через системный вызов DosDevIOCtl. Пример 1 показывает, как установить скорость последовательного порта равной 9600 бод.
Пример 1

#define INCL_DOSFILEMGR
#define INCL_DOSDEVIOCTL
#include 

HFILE hf;              // Дескриптор файла
USHORT usBPS = 9600;   // Скорость
ULONG  ulParmLen = 2;  // Размер списка параметров
ULONG  ulAction;       // Результат работы DosOpen
APIRET rc;             // Код возврата

rc = DosOpen("\\dev\\com2", &hf, &ulAction, 0, FILE_NORMAL, FILE_OPEN,
OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYNONE, (PEAOP2) NULL);

rc = DosDevIOCtl(hf,     // Дескриптор устройства
       IOCTL_ASYNC,      // Мы будем давать команды этого класса
       ASYNC_SETBAUDRATE,// Конкретно -- установим скорость
       (PULONG) &usBPS,  // Скорость будет вот такой
       sizeof(usBPS),    // Размер предыдущего параметра -вот такой
       &ulParmLen,       // А реально -- вот столько использовано
       NULL,             // Пакет данных не используем в этот раз
       0,                // Потому он и нулевого размера
       NULL);            // И реального размера у него нет

... здесь мы будем использовать порт ...

rc = DosClose(hf);         // Закрой за собой файл

Проблема с прямым доступом к диску

Часто слышу вопрос от поднаторевших в DOS программистов: как бы это попрограммировать в OS/2 на ассемблере и 'проникнуть' с его помощью в какое-нибудь интересное место в системе: на диски там напрямую что-нибудь записать/прочитать, или в видеопамять 'слазить'... Мой ответ многих огорчит: в OS/2 использование ассемблера не дает никаких преимуществ перед Си или Паскалем. Все, что вообще возможно сделать в системе, доступно из Си. Включая прямой доступ к дискам. Обычный системный вызов DosOpen, который используется для доступа к файлам, может в OS/2 'открыть' для вас целый диск так, как будто бы это был файл. Для этого достаточно указать имя диска ('С:', например) вместо имени файла и установить флаг OPEN_FLAGS_DASD в параметре fsOpenMode системного вызова. После этого можно читать/писать/позиционироваться в файле, используя обычные команды DosRead/DosWrite/DosSetFilePtr. Этот способ не подходит, правда, в том случае, когда нужно работать с физическим диском, а не с разделом на диске, но для этого существует специальная подгруппа функций в DosDevIOCtl, о которой можно узнать из документации в .inf-файлах, кстати, весьма полной и качественно сделанной.

Проблема с прямым доступом к экрану

Вопреки популярному заблуждению, OS/2 ПО3ВОЛЯЕТ программисту добраться до экранного буфера видеоадаптера ! Поэтому вы можете перенести свою программу, работающую в DOS с видеопамятью напрямую, в OS/2 без больших мучений. Для этого нужно воспользоваться подсистемой VIO, функцией VioGetPhysBuf. Конечно, доступ не будет бесконтрольным: перед обращением к видеопамяти нужно вызвать функциЮ VioScrLock, а после - VioScrUnLock; в противном случае OS/2 остановит задачу, если в момент попытки доступа к экрану она не была задачей переднего плана.
Пример 2 показывает, как установить графический режим 320х200х256 и получить адрес видеопамяти.
Пример 2

static struct  _VIOMODEINFO orig;

#define BUFSIZE 12
#define FBTYPE 3

PCH Init320x200(void)
  {
  static stiuct  _VIOPHYSBUF phys:
  static struct  _VIOMODEINFO mode;
  unsigned rc,

  phys.pBuf=(unsigned char *) OxAOOOO;
  phys.cb =65536;
  mode.cb =BUFSIZE;
  mode.fbType=FBTYPE:
  mode.color =8;   // Число битов на точку
  mode.col =40;    // Сколько символов в строке
  mode.row =25;    // Сколько строк
  mode.hres =320;  // Точек по горизонтали
  mode.vres =200;  // Точек по вертикали

  VioGetMode(&orig, HANDLE);     // Запомним старый Видеорежим

  rc = VioSetMode(&mode, HANDLE);// Установим новый
  if(rc) return (NULL);

  rc = VioGetPhysBuf(&phys, HANDLE):
  if(rc) return (NULL):

  // А вот и адрес видеобуфера.
  return ((char *) MAKEP(phys.asel[0],0));
  }

void DeInit(void)
  {
  VioScrUnLock(HANDLE);      //  а всякий случай отопрем замок
  VioSetMode(&orig,HANDLE);  // Восстановим старый режим
  }
В заключение хочу обратить ваше внимание на несколько оговорок.

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

Если вы думаете о будущем, то учтите, что переносимые версии ОS/2, вероятно, не будут содержать подсистемы VIO.

Если вам достаточно текстового режима и хочется, чтобы программа работала не только в полноэкранных сессиях OS/2, но и в окне Presentation Manager, то лучше воспользоваться другими функциями VIO. Например, можно работать через логический видеобуфер, или просто выводить информацию через функции VIO, или даже (не смейтесь) использовать стандартный вывод и коды ANSI - быстродействие дисплейной подсистемы OS/2 достаточно велико, и эти методы дают неожиданно хорошие результаты, пригодные если не для всех, то для большинства применений.
И последнее: зачем вам тот прямой доступ к экрану ?! Ныряйте в DIVE !

DIVE: Игры по-цивилизованному, с оконным соусом

Под этой аббревиатурой (которая дословно переводится как 'нырок') упрятано одна простая идея: играм нужна работа с графикой, причем БЫСТРАЯ. Этой цели и призван служить Direct Interface to Video Extentions. Грубо говоря, это просто механизм сверхбыстрого вывода графической информации в оконной системе OS/2, но какой !
DIVE умеет на ходу выполнять преобразования палитры, масштабирование, клиппинг (обрезание перекрытых другими окнами частей изображения), а в последних бета-версиях - даже динамическое переключение из оконного в полноэкранный режим (со сменой разрешения и числа цветов) и обратно. Общий принцип действия DIVE несложен: вы создаете стандартное окно Presentation Manager, инициализируете режим DIVE и в цикле выводите подготовленные вами кадры в это окно. DIVE сам заботится о приведении вашего изображения к нужному формату (например о преобразовании 256-цветного индексированного изображения в 24-битовое изображение truecolor в формате RGB), масштабировании его до реальных размеров окна, обрезании его до видимого на экране (то есть не перекрытого другими окнами) фрагмента - все это со скоростью десятков кадров в секунду. (Тут надо отметить, что скорость тем больше, чем проще масштабирование, и если, скажем, заставить DIVE масштабировать DOOM в формате 320х200 до тощей полоски, например, в 237х548, то выглядеть он будет очень смешно, но шевелиться будет помедленнее.)
И последнее, что я хочу отметить в отношении DIVE: он применим не только в играх. Hа DIVE базируется (и по сути, дала ему жизнь) цифровая видео-подсистема OS/2 - та самая, которая умеет крутить в окошечке AVIшки, FLIки и MPEGи.
На самом деле, она вовсе не так ограничена, как может показаться: ее можно научить показывать видеофайлы других форматов (существует shareware-пакет, который добавляет в ОS/2 Warp декодеры для так называемых в народе 'виндовых' форматов AVI, а авторы обещают поддержку QuickTime), и главное: вывод изображения вовсе не обязан идти в знакомый всем пользователям 'телевизор', который выскакивает, если щелкнуть два раза по AVI-файлу. Вы вполне можете расцветить программу Presentation Manager мультяшками прямо в своих собственных окнах и диалогах; пример тому - игра Galactic Civilizations.
Но хватит игр, во всяком случае, пока. Нас ждут...

... Легко разрешимые проблемы общения

Первые шаги в ОS/2 быстро приводят на тропу активного использования многозадачности. Оказывается, поскольку нет необходимости обязательно закрыть одну программу, чтобы открыть другую, уже не привлекает идея заталкивать в один продукт все возможные и невозможные функции и возможности, как мы привыкли делать в DOS: удобнее создавать программы, выполняющие свою и только свою работу, и пользоваться ими в связках, аккордами.
Обратите внимание на слова 'Операционная система' (на второе слово). Ледяное презрение в адрес MS-DOS, которое вы наверняка замечали в глазах некоторых программистов, происходит не из отсутствия многозадачности, длинных имен файлов или еще каких-либо функций в этой 'запускалочке'. Проблема в том, что DOS не тянет именно на систему, то есть некоторую среду, объединяющую своих обитателей - программы - в единое целое, в дружно работающий коллектив задач. Конечно можно, безусловно можно заставить одну программу 'съесть' файл, приготовленный другой, но роль DOS тут, согласитесь, невелика.
В чем же прелесть этой самой 'системы', напрашивается очевидный вопрос, почему с ней лучше ? Чем десять маленьких программ, способных к сочетаниям, лучше пяти больших, делающих то же самое, но абсолютно некоммуникабельных ? Да очень просто: эти 10 программ, уже соединенные попарно, дают нам не пять, а ровно сто разных видов обработки информации. А если они могут группироваться по трое, то... правильно, тысячу. Мало того, если добавить к ним всего одну, одиннадцатую подружку, то число сочетаний резко возрастет - до 121 и 1331 соответственно. Ну как, уговорил?
Тогда к делу.

Общение: пути и методы

В общем, мне приходит в голову три пути межпрограммного взаимодействия: взаимодействие между родственными процессами (связь с привлечением родителей), взаимодействие между неродственными процессами в пределах одной машины и связь межмашинная. К первому варианту относятся традиционные (UNIX) каналы (дословно pipes - трубы, их еще называют пайпами), ко второму -- каналы межпроцессные, разделяемая память, семафоры и очереди сообщений, к третьему -- сетевые каналы и TCP/IP. Конечно, вы можете пользоваться и IPX/SPX, и NetBIOS, но вряд ли в этом есть смысл в общем случае, когда вас ничто не вынуждает к этому.
К числу довольно специфичных средств взаимодействия я бы отнес и локальные сокеты (для знакомых с UNIX это - AF_UNIX по сути и по интерфейсу), которые могут быть полезны при переносе программного обеспечения из UNIX, но доступны только когда установлена сетевая поддержка, что несколько уменьшает их полезность. Остановимся немного подробнее на каждом из механизмов.

Традиционные каналы

Канал такого типа выглядит с точки зрения программиста совсем как файл: доступ к нему осуществляется обычными функциями DosRead/DosWrite (не смущайтесь префиксом Dos в именах системных вызовов OS/2, они действительно так называются), его можно закрыть функцией DosClose, но вот создать его можно только специальной функцией DosPipe. Обычное применение канала - это передача данных на стандартный ввод (stdin) порожденного процесса или, наоборот, прием данных из stdout. Основная идея тут заключается в том, что порождаемый процесс наследует от порождающего открытые файловые дескрипторы, если не оговорено иное. Ниже (пример 3) приведен слегка подредактированный фрагмент реального кода, демонстрирующий классическое использование канала.
Пример 3

ulong ReadHandle;
ulong WriteHandle;
APIRET rc;
const PipeSize = 4096;

// Создаем канал, в переменной ReadHandle получаем дескриптор
// для чтения из  канала, WriteHandle - для записи в канал.
// Размер канала - 4 килобайта.
rc = DosCreatePipe (&ReadHandle, &WriteHandle, PipeSize);
if (rc != 0)
  {
  error(EI_Full, "DosCreatePipe error: return code = %ld", rc);
  return Err;
  }

int stdin_save = dup(0);      // припрячем наш стандартный ввод

if( 0 != dup2(ReadHandle, 0)) // подложим вместо него выход канала
  {
  error(EI_Full, "Can't dup2");
  return Err;
  }

DosClose(ReadHandle);      // старый выход закроем, чтобы не текло

// Тут хитро: мы просим систему при порождении нашего дочернего
// процесса не давать ему в наследство этот конец канала.
// Остальные дескрипторы файлов он  унаследует, а этот мы 
// припрячем - самим нужен.

DosSetFHState(WriteHandle,OPEN_FLAGS_NOINHERIT);

bool ret = Ok;

// Запустим параллельный процесс, не ожидая его завершения
if (spawnv( P_NOWAIT, "rmail.exe", argv) < 0)
  {
  error(EI_Full, "Can't execute rmail" );
  ret = Err;
  }

// Теперь все, что мы будем записывать в канал через наше
// отверстие, будет вливаться в стандартный ввод запущенного 
// нами процесса так, как если бы, например, это все 
// вводилось с клавиатуры
if (ret != Err)
  write(WriteHandle, data, strlen(data));

// Закончили? Закроем за собой наш конец канала - это приведет
// к тому, что rmail.exe при очередном считывании получит
// признак конца файла. Это, как  правило, важно.
DosClose(WriteHandle);

// Вернем на место 'заныканный' stdin - вдруг он нам 
// самим еще пригодится  ?
if (0 != dup2(stdin_save, 0))
  {
  error(EI_Full, "Can't dup2(%d, 0)", stdin_save);
  ret = Err;
  }

// И закроем его копию.
DosClose(stdin_save);

Именованные каналы

Главное отличие именованного канала в том, что до него можно добраться, и не находясь в родстве с его родителем, путем открытия по имени. Имя, конечно, нужно знать заранее. Сервер (хозяин канала) создает его с помощью системного вызова DosCreateNPipe, клиент же может открыть его совсем как файл, с помощью DosOpen или библиотечных функций open/fopen. Именованные каналы предоставляют программисту существенно большие возможности, чем обычные каналы. Вы можете выбирать при создании канала направление потока данных (прием, передача или дуплекс), байт- или блок-ориентированный режим, размеры буферов, число возможных соединений под одним именем канала и так далее. При наличии сетевой поддержки именованные каналы без дополнительных усилий со стороны программиста могут работать через сеть - достаточно при подсоединении к каналу указать перед его именем имя машины в сети (например, \\MainServer\pipe\mydata укажет на канал mydata на машине MainServer).

Именованные семафоры

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

TCP/IP

Думаю, это семейство протоколов настолько широко известно и системонезависимо, что вряд ли стоит обсуждать его в статье, ориентированной на описание свойств конкретной ОС. Отмечу только, что в OS/2 MPTS реализована полноценная подсистема TCP/IP, вполне совместимая с Berkley Sockets, поддерживающая как LAN, так и SLIP/PPP, маршрутизацию, RPC, не говоря уж о таких мелочах, как готовый FTP API и доступ из REXX.

(c)dz

---
Интересные ссылки:

---

---
Комментариев к странице: 0 | Добавить комментарий
---
Редактор: Дмитрий Бан
Оформление: Евгений Кулешов
(C) Russian Underground/2