КаталогИндекс раздела
НазадОглавлениеВперед


13. Системные управляющие блоки DOS

Когда мы вели речь о загрузке системы, мы упоминали о том, что вслед за областями памяти BIOS и DOS размещаются таблицы DOS. Области, занимаемые таблицами DOS, не размещаются по фиксированным адресам, так как их объем не является строго постоянным, а определяется в основном конфигурацией системы. Хотя в процессе загрузки имеется возможность динамического формирования среды DOS, в процессе работы DOS ведет себя как система преимущественно статическая, состав и размещение в памяти ее компонент практически не меняется. Однако внутрисистемные данные в основном увязаны в списковые и табличные структуры, что, во-первых, позволяет их легко отслеживать, а во-вторых, создает потенциальную возможность для их динамической реконфигурации, правда, возможности эти недокументированные и, видимо, нелишним будет предупредить о необходимости весьма осторожного их использования. Мы здесь рассмотрим эти средства не для того, чтобы вмешиваться в работу системы (хотя такое в принципе не исключается), а для того, чтобы понять "как это устроено внутри" - рассмотреть организацию MS DOS, как пример относительно сложной программной системы.
Чтобы не обременять читателя излишним объемом справочного материала, будем рассматривать форматы управляющих блоков только для DOS версий 3.0 и выше.

13.1. Таблица векторов связи

Таблицы, используемые DOS, организованы в древовидную структуру. В корне этого дерева находится таблица, которая содержит адреса других таблиц и списков, используемых в DOS. Поскольку эта таблица является недокументированной, в различных источниках она называется по-разному: Список Списков (List of List), Блок Переменных DOS (DOS Variables Block) и т.п. Мы будем использовать название Таблица Векторов Связи и абревиатуру CVT (Connection Vectors Table) - по аналогии с подобной структурой данных в операционной системе OS/360. Через эту таблицу открывается возможность доступа ко всем внутренним данным системы. Ниже приведено описание формата CVT. Размеры полей таблицы и их назначения должны быть ясны из описания и комментариев к нему. Подробнее списки, начальные адреса которых сведены в CVT рассматриваются в следующих разделах.

/*===== Таблица Векторов Связи для DOS 3.0 и выше =====*/
struct CVT {
   word MCB_segment; /* Сегментный адрес 1-го Блока Управления Памятью */
   dword DPB_ptr; /* Адрес 1-го Блока Параметров Диска */
   dword SFT_ptr; /* Адрес 1-й Системной Таблицы Файлов */
   dword clock_ptr; /* Адрес заголовка драйвера CLOCK$ */
   dword con_ptr; /* Адрес заголовка драйвера CON */
   word maxBlock; /* Максимальный размер блока на блочном устройстве */
   dword BCB_ptr; /* Адрес 1-го буфера дисковых операций */
   dword ACD_ptr; /* Адрес Массива Текущих Каталогов */
   dword FCBtab_ptr; /* Адрес Таблицы Блоков FCB, открываемых в режиме разделения */
   word FCBtab_size; /* Размер Таблицы Блоков FCB (параметр FCBS в CONFIG.SYS) */
   byte driveCnt; /* Число блочных устройств в системе */
   byte lastdrive; /* Число идентификаторов дисков (параметр LASTDRIVE в CONFIG.SYS) */
   byte  NUL_drive[18]; /* Заголовок драйвера NUL */
   };

Недокументированная функция DOS 0x52, именуемая в нефирменных описаниях GetSysVars или GetCvt, возвращает в регистрах ES:BX адрес поля DPB_ptr таблицы векторов связи. Но перед этим полем в памяти находится 2-байтный сегментный адрес первого управляющего блока памяти, который логически также относится к векторам связи (мы уже использовали это свойство функции 0x52 в программах предыдущей главы). Поэтому мы включаем это поле в CVT и считаем, что функция 0x52 возвращает адрес, на 2 больший начального адреса таблицы.
CVT имеет и поля, расположенные перед MCB_segment, таблица продолжается и за заголовком NUL-драйвера, но содержащаяся в этих частях информация весьма специфична, существенно зависит от версии DOS (не только от старшего, но и от младшего номера версии) и в нашем пособии рассматриваться не будут.

13.2. Драйверы устройств

Управление данными в DOS происходит при посредстве драйверов устройств - системных программ, которые обеспечивают обмен данными между программами и внешними устройствами. Здесь мы, говоря о драйверах, имеем в виду не любые программы ввода-вывода, а именно программы либо входящие в состав системы, либо разработанные пользователем, но полностью отвечающие системным требованиям, поддерживаемые системой, записываемые в оперативную память при загрузке системы по команде DEVICE файла CONFIG.SYS. Последние программы называются устанавливаемыми драйверами устройств. Системные и устанавливаемые драйверы позволяют унифицировать операции, выполняемые на различных устройствах и файлах, делают прикладные программы независимыми от аппаратной части внешних устройств.
Драйвер устройства создается в виде COM-программы, отвечающей весьма жестким требованиям. Мы намереваемся посвятить разработке и применению драйверов отдельное пособие, здесь же рассмотрим только размещение драйверов в системной памяти. Драйвер обязательно должен начинаться с заголовка - области памяти, имеющей длину 18 байт и следующий формат:

   struct DR_HEAD {
       struct dr_head *next;     /* адрес следующего */
      word attr;                /* атрибуты */
      word strat_off, intr_off; /* смещения секций */
      char name[8];             /* имя */
      };

Слово атрибутов драйвера в старшем разряде содержит признак символьного (1) или блочного (0) устройства. Интерпретация остальных разрядов слова атрибутов существенно разная для символьных и блочных устройств. Для символьных устройств слово атрибутов имеет формат:
  1 e b 0 p 0 0 0  0 l 0 0 c n o i,
а для блочных устройств:
  0 e m 0 p 0 0 0  0 l 0 0 0 0 s 0,

По правилам DOS кодовая часть драйвера состоит из двух секций, называемых секцией стратегии и секцией обработки прерываний. Поля strat_off и intr_off заголовка содержат смещения этих секций от начала драйвера. Поле name для символьного устройства содержит имя устройства (возможно, дополненное пробелами), а для блочного - первый байт этого поля содержит количество устройств, поддерживаемых драйвером.
При помощи поля next, заполняемого системой при загрузке драйвера, все драйверы (системные и устанавливаемые) связываются в список. Начало этого списка - заголовок драйвера NUL в CVT. Значение 0xFFFF в части смещения поля next является признаком конца списка (такой же признак конца используется и в списках, образуемых другими управляющими блоками). Программа примера 13.1 отслеживает этот список.

/*==                      ПРИМЕР 13.1                   ==*/
/*================= Просмотр списка драйверов ============*/
#include <dos.h>
#define byte unsigned char
#define word unsigned int
#define ATR(x,z) if(drv->attr&x){printf("     %s\n",z);y++;}
#define DA(x,y) (struct DR_HEAD *)MK_FP(x,y);
struct DR_HEAD { /* заголовок драйвера */
   struct DR_HEAD *next;
   word attr, strat_addr, intr_addr;
   char name[8];
   } *drv;  /* адрес текущего драйвера */
struct DR_HEAD *clock, *con;  /* Адреса CLOCK$ и CON */
union REGS rr;
struct SREGS sr;
int i, y, y1;
main() {
  /* получение адреса CVT */
  rr.h.ah=0x52; intdosx(&rr,&rr,&sr);
  /* адрес драйвера часов */
  clock=DA(peek(sr.es,rr.x.bx+10),peek(sr.es,rr.x.bx+8));
  /* адрес драйвера консоли */
  con=DA(peek(sr.es,rr.x.bx+14),peek(sr.es,rr.x.bx+12));
  /* адрес NUL-драйвера */
  drv=DA(sr.es,rr.x.bx+34);
  printf("\nСписок драйверов устройств\n");
  while(FP_OFF(drv)!=0xffff) {
    printf("Адрес - %Fp   атрибуты - %04X   ",
      drv,drv->attr);
    if (drv->attr&0x8000)
      for (i=0;i<8; printf("%c",drv->name[i++]));
    else printf("блочный - %d",drv->name[0]);
    printf("\n"); y=0;
    if (drv==clock)
      { printf("     активный CLOCK$\n"); y++; }
    if (drv==con) { printf("     активный CON\n"); y++; }
    if (drv->attr&0x8000) {
      ATR(1,"консоль ввода")
      ATR(2,"консоль вывода")
      ATR(4,"нулевое устройство")
      ATR(8,"CLOCK$ (часы)")
      ATR(0x2000,"поддерживает OUB")
      }
    else {
      ATR(2,"32-байтный адрес сектора")
      ATR(0x2000,"читает media FAT")
      }
    ATR(0x40,"поддерживает функции DOS 3.2")
    ATR(0x800,"поддерживает Open/Close")
    ATR(0x4000,"поддерживает IOCTL")
    y=(y1=wherey())-y; getch();
    for(i=y;i<y1;gotoxy(1,i++),clreol()); gotoxy(1,y);
    drv=drv->next; /* адрес след.драйвера */
    }
  printf("Конец списка. Нажмите любую клавишу...\n");
  getch();
  }

Проанализировав результаты выполнения этой программы, читатель может прийти к следующим выводам. Пустой (NUL) драйвер, который состоит из одного заголовка выполняет единственную функцию - быть "головой" списка драйверов. Системные драйверы (драйверы, включенные в файл IO.SYS) образуют исходный список драйверов. Драйверы, устанавливаемые после них (по команде DEVICE в CONFIG.SYS), включаются в начало этого списка - сразу после NUL-драйвера. Таким образом, если устанавливаемый драйвер имеет то же имя, что и системный, то выбираться при обращении по этому имени будет устанавливаемый драйвер, так как он находится ближе к началу списка. При обращениях к драйверам CON и CLOCK$ поиск в списке не выполняется, для ускорения обращения их адреса выбираются из CVT.

13.3. Блоки параметров дисков и массив текущих каталогов

Блок Параметров Диска (DPB - Disk Parameter Block) строится DOS для каждого дискового устройства и содержит (в основном) информацию, полученную из загрузочного сектора диска. Структура блока для версий DOS 3.x следующая:

   struct DPB {
       byte dev_num;      /* # драйвера */
       byte dev_num_mod;  /* # устр-ва в драйвере */
       word SectSize;     /* размер сектора (байт) */
       byte MaxSect;      /* макс.номер сектора в кластере */
       byte Log2Sect;     /* LOG2(размера кластера) */
       word ResSect;      /* число резервных секторов */
       byte FatCnt;       /* число копий FAT */
       word RootSize;     /* размер корневого каталога */
       word Data1st;      /* # 1-го сектора области данных */
       word MaxClust;     /* максимальный номер кластера */
       byte FatSize;      /* размер FAT */
       word Root1st;      /* 1-й сектор корневого каталога */
       void *drv_ptr;     /* адрес драйвера */
       byte Media;        /* тип носителя */
       byte access;       /* доступ к носителю */
       struct DPB *next;  /* ссылка */
       word Free1st;      /* первый свободный кластер */
       word FreeCnt;      /* число свободных кластеров */
       };

Поля SectSize, ResSect, RootSize, FatCnt, FatSize, Media копируются из одноименных полей загрузочного сектора носителя (см.10.3.2). Поля MaxClust, Data1st и Root1st получются из данных загрузочного сектора путем несложных вычислений. Поле MaxSect содержит значение на 1 меньшее поля ClustSize загрузочного сектора. Поле Log2Sect дает возможность получить число секторов в кластере путем сдвига 1 на Log2Sect разрядов влево. Поле access имеет значение 0, если к носителю было обращение, или 0xFF - если не было, в последнем случае значения полей, источником которых является загрузочный сектор, могут не соответствовать действительности. Наконец, последние два поля содержат некоторую информацию о свободных кластерах, если такая информация отсутствует, то значения этих полей - 0 и 0xFFFF соответственно. При помощи поля next все DPB связываются в список (смещение 0xffff - признак конца спсика). Формат DPB для DOS 4.0 и последующих отличается от вышеприведенного тем, что размер поля FatSize составляет здесь не 1, а 2 байта и, следовательно, смещения всех последующих полей увеличиваются на 1.

В DOS имеются функции 0x1F и 0x32, возвращающие в DS:BX адрес DPB. Функция 0x1F возвращает DPB текущего диска, а 0x32 - диска, логический номер которого задается в DL (0 - текущий, 1 - A и т.д.; если задан неправильный номер диска, функция возвращает 0xFF в регистре AL). Эти функции, однако, "не удовлетворяются" значением доступа (access) 0xFF, а, если обращений к носителю не было, пытаются прочитать его загрузочную запись. В программных примерах главы 10 мы могли обращаться к функции 0x32 вместо того, чтобы читать загрузочный сектор.

В программе следующего примера, выводящей на экран содержимое всех блоков DPB, структура блока разбита на 2 части: struct DPB_1 и struct DPB_2. Поле FatSize описано как 2-байтное, но в зависимости от версии DOS адрес второй части блока (начиная с поля Root1st) назначается со смещением 16 (DOS 3.x) или 17 (DOS 4.x) относительно первой части.
Программа выводит все DPB дважды - получая их адреса как из системного списка, так и по функции DOS 0x32.

/*==                     ПРИМЕР 13.2                   ==*/
/*================= Распечатка всех DPB =================*/
#include <dos.h>
#define byte unsigned char
#define word unsigned int
struct DPB_1 {  /* 1-я часть DPB */
   byte dev_num, dev_num_mod; word SectSize;
   byte MaxSect, Log2Sect; word ResSect; byte FatCnt;
   word RootSize, Data1st, MaxClust, FatSize;
   } *dpb_1;
struct DPB_2 {  /* 2-я часть DPB */
   word Root1st; void *drv_ptr; byte Media, access;
   struct DPB_1 *next; word Free1st, FreeCnt;
   } *dpb_2;
  word dd_seg, dd_off; /* адрес 1-го DPB */
  byte ldrive;         /* номер диска */
  byte dos;            /* номер версии DOS */
  union REGS rr;
  struct SREGS sr;
main() {
  /* номер версии DOS */
  rr.h.ah=0x30; intdos(&rr,&rr); dos=rr.h.al;
  /* адрес CVT */
  rr.h.ah=0x52; intdosx(&rr,&rr,&sr);
  printf("\nТаблица DPB по спискам\n");
  /* адрес 1-го DPB */
  dd_off=peek(sr.es,rr.x.bx); dd_seg=peek(sr.es,rr.x.bx+2);
  dpb_1=(struct DPB_1 *)MK_FP(dd_seg,dd_off);
  while(FP_OFF(dpb_1)!=0xffff) {
    /* движение по списку */
    print_dpb();
    dpb_1=dpb_2->next;
    }
  printf("\nТаблица DPB по INT 32H\n");
  for(ldrive=1; ;ldrive++) {
    /* перебор всех дисков */
    rr.h.ah=0x32;    /* функция 32 */
    rr.h.dl=ldrive;  /* номер диска */
    intdosx(&rr,&rr,&sr);
    if (rr.h.al==0xff) break;
    /* адрес DPB для диска ldrive */
    dpb_1=(struct DPB_1 *)MK_FP(sr.ds,rr.x.bx);
    print_dpb();
    }
}
/*==== распечатка содержимого DPB ====*/
print_dpb() {
  /* смещение 2-й части DPB зависит от версии DOS */
  if (dos<4) dpb_2=(struct DPB_2 *)((char *)dpb_1+0x10);
  else dpb_2=(struct DPB_2 *)((char *)dpb_1+0x11);
  printf("\nАдрес DPB             - %Fp\n",dpb_1);
  printf("    устройство %u(%u)   - диск %c\n",
    dpb_1->dev_num,dpb_1->dev_num_mod,'A'+dpb_1->dev_num);
  printf("    тип носителя      - %02X\n",dpb_2->Media);
  printf("    адрес драйвера    - %Fp\n",dpb_2->drv_ptr);
  printf("    разм.сектора      - %u(байт)\n",
    dpb_1->SectSize);
  printf("    разм.кластера     - %u(сект) %u\n",
    dpb_1->MaxSect+1, 1<<dpb_1->Log2Sect);
  printf("    рез.секторов      - %u\n",dpb_1->ResSect);
  printf
    ("    всего кластеров   - %u, начиная с сектора %u\n",
    dpb_1->MaxClust,dpb_1->Data1st);
  printf("    FAT               - ");
  if (dos<4) printf("%u",(byte)dpb_1->FatSize);
  else printf("%u",dpb_1->FatSize);
  printf("(сект) * %u\n",dpb_1->FatCnt);
  printf
    ("    корневой каталог  - %u(элементов) с сектора %u\n",
    dpb_1->RootSize,dpb_2->Root1st);
  printf("    доступ            - %X\n",dpb_2->access);
  printf("    свободный кластер - %04X",dpb_2->Free1st);
  printf(" (всего - %u)\n",dpb_2->FreeCnt);
  if (getch()==27) exit();
}

Одно из полей CVT содержит указатель на начало Массива Текущих Каталогов. В версиях DOS до 3.0 такого массива не было, подобная информация содержалась в DPB. В современных версиях DOS возникла необходимость в такой структуре данных в связи с тем, что новые команды DOS SUBST и JOINT позволяют присвоить некоторому узлу дерева каталогов идентификатор диска или наоборот - описать диск как подкаталог другого диска. Общее количество возможных идентификаторов дисков задается параметром команды LASTDRIVE в CONFIG.SYS и хранится в CVT. Каждый элемент массива - Структура Текущего Каталога (CDS - Current Directory Structure) сохраняет информацию о текущем каталоге на логическом диске.
Структура CDS для версий DOS 3.x следующая:

struct CDS {
   char path[67]; /* ASCIIZ-строка, содержащая полный путь к текущему каталогу */
   word flags;   /* слово признаков, позволяющее определить, связан ли 
                    данный идентификатор с физическим диском или введен командой SUBST */
   void *dpb;    /* адрес DPB для этого диска */
   word dir_clust; /* номер 1-го кластер текущ.каталога */
   byte reserved[6];
   };
Для DOS 4.x размер последнего поля reserved - 13 байт.
Для DOS 5.0 и выше структура CDS иная:
struct CDS {
   char path[67];  /* см.выше */
   word flags;     /* см.выше */
   byte drive;     /* номер DPB, для этого диска */
   byte reserved1;
   word par_clust; /* номер 1-го кластера родительского каталога */
   word par_entry;  /* номер элемента в родительском каталоге */
   word dir_clust;  /* см.выше */
   byte reserved2[4];
   };
Массив Текущих Каталогов представляет собой именно массив, а не список - его элементы располагаются в смежных областях памяти один за другим.
В программе следующего примера, выводящей на экран содержимое Массива Текущих Каталогов, для независимости от версии DOS в описание структуры CDS включено объединение VAR, описывающее различие между версиями.

/*==                    ПРИМЕР 13.3                    ==*/
/*======= Распечатка массива текущих каталогов ==========*/
#include <dos.h>
#include <stdlib.h>
#define byte unsigned char
#define word unsigned int
struct CDS {        /* структура текущего каталога */
   char path[67];
   word flags;
   union VAR {
     struct  {   /* для DOS 3.x, 4.x */
       void *ddcb; word dir_clust; byte reserved[11];
       } dos4;
     struct {    /* для DOS 5.x и выше */
       byte drive reserved1;
       word par_clust, par_entry,dir_clust;
       byte reserved2[4];
       } dos5;
     } dosx;
   } *cds;
word a_seg, a_off;    /* нач.адрес массива */
byte lastdrive;       /* LASTDRIVE из CONFIG */
byte ndrive;          /* счетчик */
word units;           /* число блочных устр-в */
byte dos;             /* версия DOS */
int cds_size;         /* размер CDS */
union REGS rr;
struct SREGS sr;
main() {
  /* номер версии DOS */
  rr.h.ah=0x30; intdos(&rr,&rr); dos=rr.h.al;
  if (dos!=4) cds_size=81; else cds_size=88;
  /* адрес CVT */
  rr.h.ah=0x52; intdosx(&rr,&rr,&sr);
  printf("\nМассив текущих каталогов\n");
  /* выборка информации из CVT */
  a_off=peek(sr.es,rr.x.bx+22);
  a_seg=peek(sr.es,rr.x.bx+24);
  cds=(struct CDS *)MK_FP(a_seg,a_off);
  lastdrive=peekb(sr.es,rr.x.bx+33);
  printf("LASTDRIVE - %u\n",lastdrive);
  for(ndrive=0; ndrive<lastdrive; ndrive++) {
    printf("Адрес CDS - %Fp\n",cds);
    printf("         path - %s\n",cds->path);
    if (dos<5) {
      printf("         адр.DPB - ");
      if (cds->dosx.dos4.ddcb==NULL) printf("пусто\n");
      else {
        printf(" %Fp\n",cds->dosx.dos4.ddcb);
        printf("         флаги - %04X\n",cds->flags);
        printf("         1-й кластер тек.каталога - %04X\n",
          cds->dosx.dos4.dir_clust);
        }
      }
    else {
      printf("         флаги - %04X\n",cds->flags);
      printf("         1-й кластер тек.каталога - %04X\n",cds->dosx.dos5.dir_clust);
      printf("      лог.диск #%d\n",cds->dosx.dos5.drive);
      printf("      верхний каталог - кластер %04X,",cds->dosx.dos5.par_clust);
      printf(" вход - %d\n",cds->dosx.dos5.par_entry);
      }
    cds=(struct CDS *)((byte *)cds+cds_size);
    if (getch()==27) exit(0);
    }
}

13.4. Системные таблицы файлов

Один из элементов CVT указывает на первую Системную Таблицу Файлов (SFT - System File Table). Таблиц SFT может быть в системе несколько. Каждая таблица начинается с заголовка, который имеет следующий формат:

  struct SFT {
     struct SFT *next; /* адрес заголовка следующей таблицы */
     word n_files;   /* число элементов в этой таблице */
     };
Через поле next таблицы SFT увязываются в список.

Сразу вслед за заголовком таблицы в памяти следует массив элементов таблицы, которыми являются Блоки Управления Файлами DOS (DFCB - DOS File Control Block, не путать с FCB!). Формат и размер блока DFCB различен для разных версий DOS. Для версии 3.x DFCB имеет размер 53 байта, формат следующий:

 struct DFCB {
    word n_handles;       /* число дескрипторов */
    byte open_mode;       /* режим открытия */
    byte reserv1;
    byte attr;            /* атрибуты файла */
    word info;            /* состояние устройства */
    void *drv_ptr;        /* адрес драйвера/DPB */
    word First_clust;     /* номер начального кластера */
    word F_time, F_date;  /* время и дата */
    dword F_size;         /* размер файла */
    dword F_seek;         /* текущее смещение в файле */
    word lst_cl_n;        /* относит.номер текущ.кластера */
    word lst_clust;       /* абс.номер текущ.кластера */
    word dir_sect;        /* номер сектора каталога */
    byte dir_num;         /* номер элемента в секторе */
    char fname[11];       /* имя и расширение */
    byte reserved2[6];
    word owner;           /* PID хозяина */
    byte reserved4[2];
    };
Для версии 4.x размер DFCB - 59 байт и формат следующий:
  struct DFCB {
    word n_handles;       /* число дескрипторов */
    byte open_mode;       /* режим открытия */
    byte reserved;
    byte attr;            /* атрибуты файла */
    word info;            /* состояние устройства */
    char *drv_ptr;        /* адрес драйвера/DPB */
    word First_clust;     /* номер начального кластера */
    word F_time, F_date;  /* время и дата */
    dword F_size;         /* размер файла */
    dword F_seek;         /* текущее смещение в файле */
    word lst_cl_n;        /* относит.номер текущ.кластера */
    word dir_sect;        /* номер сектора каталога */
    byte reserved2[2];
    byte dir_num;         /* номер элемента в секторе */
    char fname[11];       /* имя и расширение */
    byte reserved3[6];
    word owner;            /* PID хозяина */
    word lst_clust;        /* абс.номер текущего кластера */
    byte reserved4[6];
    };
Рассмотрение полей DFCB упорядочим по их назначению, а не по порядку их расположения в памяти.
В поле fname содержится имя файла формата FCB, то есть имя дополняется до восьми, а расширение - до трех символов пробелами. Поле n_handles содержит число дескрипторов, связанных с этим файлом. Для закрытого файла (неиспользуемого DFCB) это число - 0. Поле info содержит информационное слово для файла, формируемое драйвером устройства. Интерес в этом слове для нас представляет разряд 7, являющийся индикатором того, дисковый это файл (0) или устройство (1), и разряды 0-5, которые для дисковых файлов содержат номер дисковода (0 - A, 1 - B и т.д.). Поле drv_ptr содержит адрес. Для дисковых файлов это - адрес DPB, а для файлов-устройств - адрес заголовка драйвера. Поле owner содержит PID (сегментный адрес PSP) программы, открывшей этот файл. Поле open_mode содержит режим доступа, заданный при открытии файла (1 в старшем разряде этого байта означает, что файл открыт методом FCB).
Следующая группа полей имеет смысл только для дисковых файлов и содержит данные из элемента оглавления, описывающего этот файл в каталоге. First_clust - первый кластер файла по FAT, F_time и F_date - время и дата последней модификации файла в формате каталога, F_size - размер файла в байтах, attr - байт атрибутов файла по каталогу.
Следующие поля определяют текущее состояние чтения/записи для дискового файла. F_seek - текущее смещение от начала файла в байтах, lst_cl_n - текущее смещение от начала файла в кластерах (относительный номер последнего считанного/записанного кластера), lst_clust - абсолютный номер на диске последнего считанного/записанного кластера (смещение этого поля различно для разных версий DOS).
Наконец, еще два поля используются для внесения изменений в элемент оглавления, описывающий этот файл: dir_sect - номер сектора, содержащего часть каталога, в котором файл описан, dir_num - номер элемента в этом секторе.

Программа следующего примера позволяет просмотреть таблицы DFCB. Поскольку длины блоков разные для разных версий DOS, в описании DFCB - struct DFCB - имеется первая часть, инвариантная для всех версий DOS, и вторая - union VAR - описывающая варианты структуры DFCB для DOS 3.x и 4.x.

/*==                     ПРИМЕР 13.4                    ==*/
/*================ Системные таблицы файлов ==============*/
#define byte unsigned char
#define word unsigned int
#define dword unsigned long
#define P(x) (dos>3)?dfcb->var.dos4.x:dfcb->var.dos3.x
#include <dos.h>
/* Заголовок таблицы файлов */
struct SFT {
   struct SFT *next;
   word n_files;
   } *sft;
/* Элемент таблицы файлов */
struct DFCB {
  /* инвариантная часть */
  word n_handles; byte open_mode, reserv1, attr;
  word info; char *drv_ptr;
  word First_clust, F_time, F_date;
  dword F_size, F_seek; word lst_cl_n;
  union VAR {
    struct { /* для DOS 3.x */
      word lst_clust, dir_sect; byte dir_num;
      char fname[11]; byte reserved2[6]; word owner;
      byte reserved4[2];
      } dos3;
    struct { /* для DOS 4.0 и выше */
      word dir_sect; byte reserved2[2], dir_num;
      char fname[11]; byte reserved3[6];
      word owner, lst_clust;
      byte reserved4[6];
      } dos4;
    } var;
  } *dfcb;
byte dos;              /* номер версии DOS */
int dfcb_size;         /* размер DFCB */
word sft_seg, sft_off; /* сегм.,смещ. начала */
int files;             /* счетчик файлов */
byte file;             /* признак файл/устройство */
union REGS rr;
struct SREGS sr;
word i, j, u;
char *s;
main() {
  /* получение номера версии DOS */
  rr.h.ah=0x30; intdos(&rr,&rr); dos=rr.h.al;
  /* установка размера DFCB */
  dfcb_size= (dos>3) ? 59 : 53;
  /* получение адреса системных блоков */
  rr.h.ah=0x52; intdosx(&rr,&rr,&sr);
  /* получение адреса 1-й таблицы файлов */
  sft_off=peek(sr.es,rr.x.bx+4);
  sft_seg=peek(sr.es,rr.x.bx+6);
  sft=(struct SFT *)MK_FP(sft_seg,sft_off);
  files=0;  /* счетчик файлов */
  while(FP_OFF(sft)!=0xffff) {
    /* смещение FFFF - признак конца списка */
    printf("\n\STF по адресу - %Fp (%u файлов)\n",
      sft,sft->n_files);
    dfcb=(struct DFCB *)((byte *)(sft+1)); /* 1-й DFCB */
    for (i=0; i<sft->n_files; i++) {
      /* перебор таблицы */
      printf("\nФайл %d - ",files++);
      if (dfcb->n_handles) { /* файл открыт */
        for (j=0;j<11;j++) printf("%c",P(fname[j]));
        if (file=!(dfcb->info&0x0080))
          printf(" (дисковый)");
        else printf(" (устройство)");
        printf("  \"хозяин\" - %04X ",P(owner));
        if (dos>3) {
          /* определяется для DOS-4 и выше */
          s=(char *)MK_FP(P(owner)-1,8);
          for(j=0;j<8;printf("%c",s[j++]));
          }
        printf("\n    дескрипторов  - %u;",dfcb->n_handles);
        printf(" режим доступа - %02X (",dfcb->open_mode);
        switch (dfcb->open_mode) {
          case 0: printf("Только чтение)");break;
          case 1: printf("Только запись)");break;
          case 2: printf("Чтение/Запись)");break;
          }
        if (file) /* только для дисковых файлов */
          printf(";\n    DRIVE = %c:;",
            'A'+(dfcb->info&0x007));
        if (!file) {
          printf("\n    драйвер ");
          for(s=dfcb->drv_ptr+10,j=0; j<8;
              printf("%c",s[j++]));
          }
        else printf("  адр.DPB");
        printf(" - %Fp;",dfcb->drv_ptr);
        printf("  состояние устр-ва - %04X\n",dfcb->info);
        if (file) { /* только для дисковых файлов */
          printf("    КАТАЛОГ: нач.сектор   - ");
            /* выбирается в зависимости от версии DOS */
          if (dos<4) printf("%04X",P(dir_sect));
            else printf("%04X",P(lst_clust));
          printf(", номер в секторе - %u\n",P(dir_num));
          printf("    ЭЛЕМЕНТ КАТАЛОГА: атрибут - %02X,  ",
            dfcb->attr);
          /* время и дата - упакованном формате */
          printf("время - "); u=dfcb->F_time;
          printf("%02d:%02d:%02d,  ",
            u>>11,(u>>5)&0x3f,(u&0x1f)<<1);
          printf("дата - "); u=dfcb->F_date;
          printf("%d:%02d:%02d\n",
            u&0x1f,(u>>5)&0x0f,(u>>9)+1980);
          printf("                      размер - %lu",
            dfcb->F_size);
          printf(",  нач.класт - %04X\n",dfcb->First_clust);
          printf
            ("    ТЕКУЩЕЕ СОСТОЯНИЕ:  последн.кластер - ");
          /* выбирается в зависимости от версии DOS */
          if (dos<4) printf("%04X",P(lst_clust));
          else printf("%04X",P(lst_clust));
          printf(" (%d),  ",dfcb->lst_cl_n);
          printf("смещение - %lu\n",dfcb->F_seek);
          }
        }
      else printf("не используется\n");
      if(getch()==27) exit();
      /* размер элемента зависит от версии DOS */
      dfcb=(struct DFCB *)((byte *)dfcb+dfcb_size);
      }
    sft=sft->next;
    }
}

Выполнение программы в том виде, в каком она приведена здесь, не даст сколько-нибудь интересных результатов: программа "покажет" только три постоянно открытых файла - AUX, CON, PRN. Для получения более наглядного представления о функционировании STF рекомендуем читателю оформить программу в виде функции и вставить многочисленные обращения к этой функции в какую-либо программу, выполняющую интенсивную работу с файлами (например, пример 10.7).

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

13.5. Буферизация дискового ввода-вывода

Дисковый ввод-вывод в DOS выполняется с буферизацией. Необходимость буферизации объясняется прежде всего следующими соображениями. Поскольку единицей информации при обмене с дисками является сектор, то при поступлении запроса на чтение данных с диска (пусть даже одного байта), DOS читает весь сектор - 512 байт, а затем передает затребованное число байт в программу. При следующем запросе данных из того же сектора у DOS уже нет необходимости обращаться к диску. Она передает программе данные из буфера. Аналогичным образом при записи данные накапливаются в буфере, пока весь буфер не будет запомнен, и лишь затем выводятся на диск. Кроме того, при наличии большого числа буферов DOS использует их в основном для сохранения содержимого FAT, а также каталогов, что может существенно повысить скорость поиска файлов на диске.
Буферизация (число буферов) задается командой BUFFERS файла CONFIG.SYS. В оперативной памяти можно разместить от 2 до 99 буферов. При задании слишком большого числа буферов помимо того, что расходуется оперативная память проявляется также и снижение эффективности, связанное с затратами времени на обслуживание буферного пула. Справочники рекомендуют такие значения для числа буферов:

Для AT число буферов в основной памяти может быть существенно уменьшено, если используется утилита FASTOPEN, при помощи которой можно хранить информацию о файлах и каталогах в дополнительной (expanded) памяти, или драйвер SMARTDRV.SYS, создающей в дополнительной или расширенной (extended) памяти кэш-память для дисков.
Следует иметь в виду, что буферизация применяется только при выполнении файловых операций ввода-вывода. Секторы читаемые/ записываемые по прямым обращениям из программы к прерываниям DOS 0x25/0x26 через систему буферизации не проходят.
При закрытии файла все связанные с ним буфера сбрасываются на диск. Но в DOS (начиная с 3.30) имеется функция 0x68, позволяющая принудительно записать на диск все буфера, связанные с заданным файлом - ее целесообразно применять, если есть риск потери накопленной в буферах информации при выключении или сбое питания. Кроме того, функция 0x6C (расширенное открытие файлов - начиная с DOS 4.0) позволяет открыть файл без буферизации.

Буферизация обеспечивается следующими внутренними структурами данных DOS. Каждый буфер в оперативной памяти предваряется Блоком Управления Буфера (BCB - Buffer Control Block). Нам не удалось найти в источниках удовлетворительных описаний формата и назначений полей BCB, следующие описания базируются в основном на наших собственных экспериментах в DOS 3.30 и DOS 5.0. Источники утверждают, что во всех вариантах DOS 3.x буферизация построена одинаково, и также одинакова она в DOS 4.x и DOS 5.x, однако, мы не можем ручаться за истинность этого утверждения.
Для DOS 3.30 структура BCB следующая:

  struct BCB {  /* Управляющий блок буфера DOS 3.x */
    struct BCB *next; /* адрес следующего */
    byte drive;       /* номер устройства */
    byte flag;        /* байт состояния буфера */
    word sect;        /* номер сектора */
    byte copies       /* число копий */
    byte sect_off;    /* смещение 2-й копии */
    void *dpb;        /* адрес DPB устройства */
    byte reserved[2];
    };
а для DOS 5.0 такая:
  struct BCB {  /* BCB DOS 4.0 и далее */
    word next;        /* смещение следующего */
    word prev;        /* смещение предыдущего */
    byte drive;
    byte flag;
    dword sect;
    byte copies;
    word sect_off;
    void *dpb;
    word count;       /* счетчик обращений */
    byte reserved[1];
    };
Поля drive и sect включают логический номер диска и номер сектора этого диска, содержимое которого скопировано в данный буфер (в ни разу не использованных буферах drive=0xff), поле dpb - ссылку на DPB этого устройства. Поле состояния буфера - flag - будет нами рассмотрено ниже. Поле copies содержит число копий данного сектора на диске (оно отлично от 1 только для секторов, входящих в состав FAT), а поле sect_off - смещение на диске второй копии относительно первой (в секторах).
Все BCB связаны в список, но организация списка различна для разных версий DOS. В DOS 3 начальный адрес этого списка находится в CVT со смещением 0x12 от адреса, возвращаемого функцией 0x52. Каждый элемент списка в поле next содержит адрес (сегмент и смещение) следующего элемента. Признаком конца списка является значение 0xFFFF в смещении адреса следующего элемента.
В DOS 5 все буфера расположены в одном сегменте памяти. Поля next и prev в BCB связывают их в двухсвязный список. Эти поля содержат только смещения, так как сегментный адрес у всех буферов один и тот же. Для этой версии поле CVT со смещением 0x12 включает адрес области памяти, в которой хранится адрес начала списка, для выхода на первый элемент списка необходима двойная адресация. Список - кольцевой, то есть поле next его последнего элемента указывает на первый элемент, а поле prev первого - на последний.

Теперь об интерпретации разрядов байта состояния буфера. Наши эксперименты подтверждают следующие назначения его битов:

                  0 w r 0 a d f 0,
Те разряды, в которых мы при всех экспериментах получали нули, возможно, тоже содержат какие-либо признаки, нами не расшифрованные.

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

/*==                    ПРИМЕР 13.5                     ==*/
/*======= Карта буферов дискового ввода/вывода DOS =======*/
#define byte unsigned char
#define word unsigned int
#define dword unsigned long
#include <dos.h>
#define V(x) (dos>3)?bcb->var.var5.x:bcb->var.var3.x
struct BCB {  /* Управляющий блок буфера */
  union PTR {
    struct BCB *next3;
    struct { word next, prev; } next5;
    } ptr;
  byte drive, flag;
  union VAR {
   struct VAR3 {
     word sect; byte copies, sect_off; void *dpb;
     } var3;
   struct VAR5 {
     dword sect; byte copies; word sect_off; void *dpb;
     } var5;
    }var;
   } *bcb, *bcb0;
word bbc_seg, bbc_off; /* адрес CVT */
word bseg, boff;       /* адрес 1-го буфера */
int nbuf;              /* счетчик */
byte dos;              /* версия DOS */
union REGS rr;
struct SREGS sr;
main() {
  /* получение версии */
  rr.h.ah=0x30; intdos(&rr,&rr); dos=rr.h.al;
  /* получение адреса CVT */
  rr.h.ah=0x52;
  intdosx(&rr,&rr,&sr);
  bseg=sr.es; boff=rr.x.bx;
  printf("\nТаблица буферов\n");
  printf("Pазмер буфера = %u\n",peek(bseg,boff+16));
  /* адрес 1-го буфера */
  bbc_off=peek(bseg,boff+0x12);
  bbc_seg=peek(bseg,boff+0x14);
  bcb=(struct BCB *)MK_FP(bbc_seg,bbc_off);
  if (dos>3) /* еще одна адресация */
    bcb0=bcb=bcb->ptr.next3;
  for (nbuf=1;;nbuf++) {
    printf("\nБУФЕР #%-2d",nbuf);
    printf("   Адр.BCB=%Fp",bcb);
    if (dos>3)
      printf(", след.=%04X, пред.=%04X",bcb->ptr.next5.next,bcb->ptr.next5.prev);
    printf(", адр.DPB=%Fp\n",V(dpb));
    printf("           диск=%d(%c),",
      bcb->drive,'A'+bcb->drive);
    printf(" сектор=");
    if (dos>3) printf("%lu",V(sect));
      else printf("%u",V(sect));
    printf(", копий=%d, смещение=%u\n",V(copies),V(sect_off));
    printf("           флаг состояния=%02X\n",bcb->flag);
    if (getch()==27) exit();
    /* переход к следующему BCB в списке */
    if (dos>3) {
      /* старый сегмент : новое смещение */
      bcb=(struct BCB *)MK_FP(FP_SEG(bcb),bcb->ptr.next5.next);
      /* выход из цикла, если кольцо замкнулось */
      if (bcb==bcb0) break;
      }
    else {
      /* полный адрес выбирается из next */
      bcb=bcb->ptr.next3;
      /* выход из цикла по признаку конца списка */
      if (FP_OFF(bcb)==0xffff) break;
      }
    }
}

Применение этой программы "в чистом виде", как и в предыдущем примере, не может дать интересных результатов. Гораздо интереснее оформить ее в виде функции и вызывать эту функцию из программы, работающей с файлом (или с файлами) после каждой операции чтения/записи, что мы и делали в процессе экспериментирования. Особенно показательными будут результаты ее, если включить в ее состав функцию, определяющую по номеру сектора имя файла, которому этот сектор принадлежит - все необходимые для этого средства читатель получил в главе 10, так что, надеемся, он справится с этой задачей самостоятельно.

13.6. Связи системных блоков

Итоговая схема связи системных управляющих блоков представлена на рис.13.1, на котором таблицы изображены в виде вертикальных последовательностей ячеек, списки - в виде последовательностей, "уходящих в глубину" рисунка. Большинство связей, показанных на рисунке, задается в виде прямых адресных ссылок, исключение составляют: список MCB, в котором следующий адрес вычисляется, и ссылка из JFT в SFT - по номеру элемента. Пунктиром на рисунке показана ссылка от DFCB на драйвер символьного устройства, альтернативная ссылке на DPB блочного устройства.


Рис.13.1 Связи между системными блоками DOS

На основании предложенной схемы можно попытаться строить гипотезы о выполнении системой запросов пользовательских программ на операции, например, ввода-вывода. Так, для чтения информации из файла пользовательская программа передает системе в составе запроса дескриптор обрабатываемого файла. Система при получении запроса выбирает PID активной в данный момент программы, являющийся адресом ее PSP. PSP программы содержит ссылку на ее JFT, полученный системой файловый дескриптор является номером элемента JFT. Элемент, выбранный по дескриптору из JFT, является входом в системную таблицу файлов, по которому выбирается DFCB требуемого файла. По-видимому, для символьных устройств система сразу же формирует запрос к драйверу, адрес которого содержится в DFCB. Для блочных устройств система по ссылке в DFCB выбирает DPB для блочного устройства. Используя данные о физических парамет- рах диска и системные структуры данных (FAT и каталоги, не- которые из которых, возможно, уже находятся в дисковых буфе- рах, а недостающие - считываются в них), система формирует основной параметр для обращения к драйверу блочного устройс- тва - номер сектора. Однако, прежде чем выдать драйверу зап- рос на ввод требуемого сектора система просматривает список BCB - для каких-то случаев информация может уже быть прочи- тана в дисковый буфер. Читатель сам может распространить подход, пример которого мы привели, на другие виды операций ввода-вывода.

Представляет интерес также анализ распределения памяти DOS, который можно провести, зная структуру связей между системными блоками. Анализ распределения памяти, выполняемый программой примера 12.1, во всех случаях показывает, что первый, значительный по размеру блок распределяемой памяти всегда принадлежит DOS. Что же содержится в этом блоке? Предоставим читателю самому сделать такую компиляцию из примеров этой главы, которая позволит проанализовать, какие системные структуры располагаются в ядре DOS, а какие - в распределенной DOS памяти, здесь же мы приводим (рис.13.2) результат одного из выполнений такой программы. Цифры названий областей обозначают:

*1- назначение описано в источниках, восходящих к фирмам IBM и Microsoft;
*2- назначение области определено экспериментально;
*3- назначение предположительно;
*4- назначение неизвестно.

Рис.13.2 Карта памяти DOS. (IBM PC/XT, DOS 3.30)


КаталогИндекс раздела
НазадОглавлениеВперед