10.4.2 Анализ потоков

   Ричи упоминает о том, что им была предпринята попытка создания потоков только с процедурами "вывода" или только с процедурами обслуживания. Однако, процедура обслуживания необходима для управления потоками данных, так как модули должны иногда ставить данные в очередь, если соседние модули на время закрыты для приема данных. Процедура "вывода" так же необходима, поскольку данные должны иногда доставляться в соседние модули незамедлительно. Например, строковому интерфейсу терминала нужно вести эхо-сопровождение ввода данных на терминале в темпе с процессом. Системная функция write могла бы запускать процедуру "вывода" для следующей очереди непосредственно, та, в свою очередь, вызывала бы процедуру "вывода" для следующей очереди и так далее, не нуждаясь в механизме диспетчеризации. Процесс приостановился бы в случае переполнения очередей для вывода. Однако, со стороны ввода модули не могут приостанавливаться, поскольку их выполнение вызывается программой обработки прерываний, иначе был бы приостановлен совершенно безобидный процесс. Связь между модулями не должна быть симметричной в направлениях ввода и вывода, хотя это и делает схему менее изящной.
   Также было бы желательно реализовать каждый модуль в виде отдельного процесса, но использование большого количества модулей привело бы к переполнению таблицы процессов. Модули наделяются специальным механизмом диспетчеризации — программным прерыванием, независимым от обычного планировщика процессов. По этой причине модули не могут приостанавливать свое выполнение, так как они приостанавливали бы тем самым произвольный процесс (тот, который прерван). Модули должны хранить внутри себя информацию о своем состоянии, что делает лежащие в их основе программы более громоздкими, чем если бы приостановка выполнения была разрешена.
   В реализации потоков можно выделить несколько отклонений или несоответствий:
   • Учет ресурсов процесса в потоках затрудняется, поскольку модулям необязательно выполняться в контексте процесса, использующего поток. Ошибочно предполагать, что все процессы одинаково используют модули потоков, поскольку одним процессам может потребоваться использование сложных сетевых протоколов, тогда как другие могут использовать простые строковые интерфейсы.
   • Пользователи имеют возможность переводить терминальный драйвер в режим без обработки, в котором функция read возвращает управление через короткий промежуток времени в случае отсутствия данных (например, если newtty.c_cc[VMIN] = 0 на Рисунке 10.17). Эту особенность сложно реализовать в потоковой среде без подключения специальной программы на уровне заголовка потока.
   • Потоки выступают средствами линейной связи и не могут позволить производить с легкостью мультиплексирование на уровне ядра. В примере использования окон, рассмотренном в предыдущем разделе, выполнялось мультиплексирование на уровне пользовательского процесса.
   Несмотря на эти несоответствия, с потоками связываются большие надежды в совершенствовании разработки модулей драйвера.

10.5 ВЫВОДЫ

   Данная глава представляет собой обзор драйверов устройств в системе UNIX. Устройства могут быть либо блочного, либо символьного типа; интерфейс между устройствами и остальной частью ядра определяется типом устройств. Интерфейсом для устройств блочного типа выступает таблица ключей устройств ввода-вывода блоками, состоящая из точек входа, соответствующих процедурам открытия и закрытия устройств и стратегической процедуре. Стратегическая процедура управляет передачей данных от и к устройству блочного типа. Интерфейсом для устройств символьного типа выступает таблица ключей устройств посимвольного ввода-вывода, которая состоит из точек входа, соответствующих процедурам открытия и закрытия устройства, чтения, записи и процедуре ioctl. Системная функция ioctl использует при обращении к устройствам символьного типа свой собственный интерфейс, который позволяет осуществлять передачу управляющей информации между процессами и устройствами. По получении прерывания от устройства ядро вызывает программу обработки соответствующего прерывания, опираясь на информацию, хранящуюся в таблице векторов прерываний, и на параметры, сообщенные устройством, от которого поступило прерывание.
   Дисковые драйверы превращают номера логических блоков, используемые файловой системой, в физические адреса на диске. Блочный интерфейс дает возможность ядру буферизовать данные. Взаимодействие без обработки ускоряет ввод-вывод на диск, но игнорирует буферный кеш, увеличивая тем самым шансы разрушить файловую систему.
   Терминальные драйверы осуществляют непосредственное взаимодействие с пользователями. Ядро связывает с каждым терминалом три символьных списка, один для неструктурированного ввода с клавиатуры, один для ввода с обработкой символов стирания, удаления и возврата каретки и один для вывода. Системная функция ioctl дает процессам возможность следить за тем, как ядро обрабатывает вводимые данные, переводя терминал в канонический режим или устанавливая значения различных параметров для режима без обработки символов. Getty-процесс открывает терминальные линии и ждет связи: он формирует группу процессов во главе с регистрационным shell'ом, инициализирует с помощью функции ioctl параметры терминала и обращается к пользователю с предложением зарегистрироваться. Установленный таким образом операторский терминал посылает процессам в группе сигналы в ответ на возникновение таких событий, как "зависание" пользователя или нажатие им клавиши прерывания.
   Потоки выступают средством повышения модульности построения драйверов устройств и протоколов. Поток — это полнодуплексная связь между процессами и драйверами устройств, которая может включать в себя строковые интерфейсы и протоколы для промежуточной обработки данных. Модули потоков характеризуются четко определенным взаимодействием и гибкостью, позволяющей использовать их в сочетании с другими модулями. Эта гибкость имеет особое значение для сетевых протоколов и драйверов.

10.6 УПРАЖНЕНИЯ

   1. * Предположим, что в системе имеются два файла устройств с одними и теми же старшим и младшим номерами, при том, что оба устройства — символьного типа. Если два процесса желают одновременно открыть физическое устройство, не будет никакой разницы, открывают ли они один и тот же файл устройства или же разные файлы. Что произойдет, когда они станут закрывать устройство?
   2. * Вспомним из главы 5, что системной функции mknod требуется разрешение суперпользователя на создание нового специального файла устройства. Если доступ к устройству управляется правами доступа к файлу, почему функции mknod нужно разрешение суперпользователя?
   3. Напишите программу, которая проверяет, что файловые системы на диске не перекрываются. Этой программе потребовались бы два аргумента: файл устройства, представляющий дисковый том, и дескриптор файла, откуда берутся номера секторов и их размер для диска данного типа. Для проверки отсутствия перекрытий этой программе понадобилась бы информация из суперблоков. Будет ли такая программа всегда правильной?
   4. Программа mkfs инициализирует файловую систему на диске путем создания суперблока, выделения места для списка индексов, включения всех информационных блоков в связанный список и создания корневого каталога. Как бы вы написали программу mkfs? Как изменится эта программа при наличии таблицы содержимого тома? Каким образом следует инициализировать таблицу содержимого тома?
   5. Программы mkfs и fsck (глава 5) являются программами пользовательского уровня, а не частью ядра. Прокомментируйте это.
   6. Предположим, что программисту нужно разработать базу данных, работающую в среде ОС UNIX. Программы базы данных выполняются на пользовательском уровне, а не в составе ядра. Как система управления базой данных будет взаимодействовать с диском? Подумайте над следующими вопросами:
   • Использование стандартного интерфейса файловой системы вместо непосредственной работы с неструктурированными данными на диске,
   • Потребность в быстродействии,
   • Необходимость знать, когда фактически данные располагаются на диске,
   • Размер базы данных: должна ли она помещаться в одной файловой системе, занимать собой весь дисковый том или же располагаться на нескольких дисковых томах?
   7. Ядро системы UNIX по умолчанию предполагает, что файловая система располагается на идеальных дисках. Однако, диски могут содержать ошибки, которые делают непригодными и выводят из строя определенные сектора, несмотря на то, что остальная часть диска осталась "пригодной". Как дисковому драйверу (или интеллектуальному контроллеру диска) следует учитывать небольшое количество плохих секторов. Как это отразилось бы на производительности системы?
   8. При монтировании файловой системы ядро запускает процедуру открытия для данного драйвера, но позже освобождает индекс специального файла устройства по завершении выполнения вызова системной функции mount. При демонтировании файловой системы ядро обращается к индексу специального файла устройства, запускает процедуру закрытия для данного драйвера и вновь освобождает индекс. Сравните эту последовательность операций над индексом, а также обращений к процедурам открытия и закрытия драйвера, с последовательностью действий, совершаемых при открывании и закрывании устройства блочного типа. Прокомментируйте результаты сравнения.
   9. Выполните программу, приведенную на Рисунке 10.14, но направьте вывод данных в файл. Сравните содержимое файла с содержимым выводного потока, когда вывод идет на терминал. Вам придется прервать процессы, чтобы остановить их; только прежде пусть они получат достаточно большое количество данных. Что произойдет, если вызов функции write в программе заменить на printf(output);
   10. Что произойдет, если пользователь попытается выполнить редактирование текста на фоне программы:
 
   ed file&
 
   Обоснуйте ответ.
   11. К файлам терминалов обычно устанавливаются следующие права доступа
 
   crw-w-w- 2 mjb lus 33,11 Oct 25 20:27 tty61
 
   при входе пользователя в систему. То есть, чтение и запись разрешаются пользователю с именем "mjb", а остальным пользователям разрешена только запись. Почему?
   12. Предположим, что вам известно имя файла терминала вашего товарища. Напишите программу записи сообщений с вашего терминала на терминал вашего товарища. Какая еще информация вам нужна, чтобы закодировать приемлемое воспроизведение обычной команды write?
   13. Выполните команду stty: если параметры не указаны, она выбирает значения установок терминала и сообщает их пользователю. В противном случае пользователь может в интерактивном режиме сделать различные установки сам.
   14. Напишите элементарный строковый интерфейс, записывающий идентификатор машины в начале каждой строки выводного потока.
   15. В каноническом режиме пользователь может на время приостановить вывод данных на терминал, нажав последовательность клавиш ‹Ctrl-s›, и продолжить вывод, нажав ‹Ctrl-q›. Как в стандартном строковом интерфейсе реализуется эта особенность?
   16. *Процесс начальной загрузки порождает getty-процесс для каждой терминальной линии в системе. Что произошло бы, если бы для одного и того же терминала существовали бы одновременно два getty-процесса, ожидающие регистрации пользователя? Может ли ядро помешать этому?
   17. Пусть командный процессор shell реализован таким образом, что он "игнорирует" конец файла и продолжает считывать данные из стандартного ввода. Что произошло бы, если бы пользователь (в регистрационном shell'е) угадал конец файла и продолжил ввод с клавиатуры?
   18. *Предположим, что процесс считывает данные с операторского терминала, но игнорирует или улавливает сигналы о "зависании". Что произойдет, когда процесс продолжит считывать данные с операторского терминала после зависания?
   19. Программа getty-процесса несет ответственность за открытие терминальной линии, а программа login — за проверку регистрационных имен и паролей. Какие преимущества в том, что эти функции выполняются отдельными программами?
   20. Рассмотрим два метода реализации драйвера косвенного терминала ("/dev/tty"), описанные в разделе 10.3.6. Какие различия между ними чувствует пользователь? (Совет: подумайте о системных функциях stat и fstat).
   21. Разработайте метод планирования выполнения модулей потока, в соответствии с которым ядро имеет в своем составе специальный процесс, выполняющий процедуры обслуживания модулей тогда, когда выполнение этих процедур запланировано.
   22. * Разработайте схему построения виртуальных терминалов (окон) с использованием традиционных (не потоковых) драйверов.
   23. * Разработайте метод реализации виртуальных терминалов с использованием потоков, в котором мультиплексированием ввода-вывода между виртуальным и физическим терминалами занимался бы один из модулей ядра, а не пользовательский процесс. Опишите механизм соединения потоков со сверткой и разверткой. Что лучше: включить модуль, осуществляющий мультиплексирование, в состав ядра или построить его как пользовательский процесс?
   24. Команда ps сообщает интересную информацию об активности процессов в работающей системе. В традиционных реализациях ps считывает информацию из таблицы процессов, прямо из памяти ядра. Такой метод не совсем удобен в среде разработки, когда размер записей таблицы процессов меняется и команде ps становится нелегко обнаружить в таблице соответствующие поля. Разработайте драйвер, нечувствительный к изменениям среды.

ГЛАВА 11. ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ

   Наличие механизмов взаимодействия дает произвольным процессам возможность осуществлять обмен данными и синхронизировать свое выполнение с другими процессами. Мы уже рассмотрели несколько форм взаимодействия процессов, такие как канальная связь, использование поименованных каналов и посылка сигналов. Каналы (непоименованные) имеют недостаток, связанный с тем, что они известны только потомкам процесса, вызвавшего системную функцию pipe: не имеющие родственных связей процессы не могут взаимодействовать между собой с помощью непоименованных каналов. Несмотря на то, что поименованные каналы позволяют взаимодействовать между собой процессам, не имеющим родственных связей, они не могут использоваться ни в сети (см. главу 13), ни в организации множественных связей между различными группами взаимодействующих процессов: поименованный канал не поддается такому мультиплексированию, при котором у каждой пары взаимодействующих процессов имелся бы свой выделенный канал. Произвольные процессы могут также связываться между собой благодаря посылке сигналов с помощью системной функции kill, однако такое "сообщение" состоит из одного только номера сигнала.
   В данной главе описываются другие формы взаимодействия процессов. В начале речь идет о трассировке процессов, о том, каким образом один процесс следит за ходом выполнения другого процесса, затем рассматривается пакет IPC: сообщения, разделяемая память и семафоры. Делается обзор традиционных методов сетевого взаимодействия процессов, выполняющихся на разных машинах, и, наконец, дается представление о "гнездах", применяющихся в системе BSD. Вопросы сетевого взаимодействия, имеющие специальный характер, такие как протоколы, адресация и др., не рассматриваются, поскольку они выходят за рамки настоящей работы.

11.1 ТРАССИРОВКА ПРОЦЕССОВ

   В системе UNIX имеется простейшая форма взаимодействия процессов, используемая в целях отладки, — трассировка процессов. Процесс-отладчик, например sdb, порождает трассируемый процесс и управляет его выполнением с помощью системной функции ptrace, расставляя и сбрасывая контрольные точки, считывая и записывая данные в его виртуальное адресное пространство. Трассировка процессов, таким образом, включает в себя синхронизацию выполнения процесса-отладчика и трассируемого процесса и управление выполнением последнего.
 
    if ((pid = fork()) == 0) {
     /* потомок — трассируемый процесс */
     ptrace(0, 0, 0, 0);
     exec("имя трассируемого процесса");
    }
    /* продолжение выполнения процесса-отладчика */
    for (;;) {
     wait((int *) 0);
     read(входная информация для трассировки команд);
     ptrace(cmd, pid, …);
     if (условие завершения трассировки) break;
    }
    Рисунок 11.1. Структура процесса отладки
 
   Псевдопрограмма, представленная на Рисунке 11.1, имеет типичную структуру отладочной программы. Отладчик порождает новый процесс, запускающий системную функцию ptrace, в результате чего в соответствующей процессу-потомку записи таблицы процессов ядро устанавливает бит трассировки. Процесс-потомок предназначен для запуска (exec) трассируемой программы. Например, если пользователь ведет отладку программы a.out, процесс-потомок запускает файл с тем же именем. Ядро отрабатывает функцию exec обычным порядком, но в финале замечает, что бит трассировки установлен, и посылает процессу-потомку сигнал прерывания. На выходе из функции exec, как и на выходе из любой другой функции, ядро проверяет наличие сигналов, обнаруживает только что посланный сигнал прерывания и исполняет программу трассировки процесса как особый случай обработки сигналов. Заметив установку бита трассировки, процесс-потомок выводит своего родителя из состояния приостанова, в котором последний находится вследствие исполнения функции wait, сам переходит в состояние трассировки, подобное состоянию приостанова (но не показанное на диаграмме состояний процесса, см. Рисунок 6.1), и выполняет переключение контекста.
   Тем временем в обычной ситуации процесс-родитель (отладчик) переходит на пользовательский уровень, ожидая получения известия от трассируемого процесса. Когда соответствующее известие процессом-родителем будет получено, он выйдет из состояния ожидания (wait), прочитает (read) введенные пользователем команды и превратит их в серию обращений к функции ptrace, управляющих трассировкой процесса-потомка. Синтаксис вызова системной функции ptrace:
 
   ptrace(cmd, pid, addr, data);
 
   где в качестве cmd указываются различные команды, например, чтения данных, записи данных, возобновления выполнения и т. п., pid — идентификатор трассируемого процесса, addr — виртуальный адрес ячейки в трассируемом процессе, где будет производиться чтение или запись, data — целое значение, предназначенное для записи. Во время исполнения системной функции ptrace ядро проверяет, имеется ли у отладчика потомок с идентификатором pid и находится ли этот потомок в состоянии трассировки, после чего заводит глобальную структуру данных, предназначенную для передачи данных между двумя процессами. Чтобы другие процессы, выполняющие трассировку, не могли затереть содержимое этой структуры, она блокируется ядром, ядро записывает в нее параметры cmd, addr и data, возобновляет процесс-потомок, переводит его в состояние "готовности к выполнению" и приостанавливается до получения от него ответа. Когда процесс-потомок продолжит свое выполнение (в режиме ядра), он исполнит соответствующую (трассируемую) команду, запишет результат в глобальную структуру и "разбудит" отладчика. В зависимости от типа команды потомок может вновь перейти в состояние трассировки и ожидать поступления новой команды или же выйти из цикла обработки сигналов и продолжить свое выполнение. При возобновлении работы отладчика ядро запоминает значение, возвращенное трассируемым процессом, снимает с глобальной структуры блокировку и возвращает управление пользователю.
   Если в момент перехода процесса-потомка в состояние трассировки отладчик не находится в состоянии приостанова (wait), он не обнаружит потомка, пока не обратится к функции wait, после чего немедленно выйдет из функции и продолжит работу по вышеописанному плану.
 
    int data[32];
    main() {
     int i;
     for (i = 0; i ‹ 32; i++) printf("data[%d] = %d\n", i, data[i]);
     printf("ptrace data addr 0x%x\n", data);
    }
    Рисунок 11.2. Программа trace (трассируемый процесс)
 
    #define TR_SETUP 0
    #define TR_WRITE 5
    #define TR_RESUME 7
    int addr;
    main(argc, argv)
    int argc;
    char *argv[];
    {
     int i, pid;
     sscanf(argv[1], "%x", &addr);
     if ((pid = fork() == 0) {
      ptrace(TR_SETUP, 0, 0, 0);
      execl("trace", "trace", 0);
      exit();
     }
     for (i = 0; i ‹ 32, i++) {
      wait((int *) 0);
      /* записать значение i в пространство процесса с идентификатором pid по адресу, содержащемуся в переменной addr */
      if (ptrace(TR_WRITE, pid, addr, i) == -1) exit();
      addr += sizeof(int);
     }
     /* трассируемый процесс возобновляет выполнение */
     ptrace(TR_RESUME, pid, 1, 0);
    }
    Рисунок 11.3. Программа debug (трассирующий процесс)
 
   Рассмотрим две программы, приведенные на Рисунках 11.2 и 11.3 и именуемые trace и debug, соответственно. При запуске программы trace с терминала массив data будет содержать нулевые значения; процесс выводит адрес массива и завершает работу. При запуске программы debug с передачей ей в качестве параметра значения, выведенного программой trace, происходит следующее: программа запоминает значение параметра в переменной addr, создает новый процесс, с помощью функции ptrace подготавливающий себя к трассировке, и запускает программу trace. На выходе из функции exec ядро посылает процессу-потомку (назовем его тоже trace) сигнал SIGTRAP (сигнал прерывания), процесс trace переходит в состояние трассировки, ожидая поступления команды от программы debug. Если процесс, реализующий программу debug, находился в состоянии приостанова, связанного с выполнением функции wait, он "пробуждается", обнаруживает наличие порожденного трассируемого процесса и выходит из функции wait. Затем процесс debug вызывает функцию ptrace, записывает значение переменной цикла i в пространство данных процесса trace по адресу, содержащемуся в переменной addr, и увеличивает значение переменной addr; в программе trace переменная addr хранит адрес точки входа в массив data. Последнее обращение процесса debug к функции ptrace вызывает запуск программы trace, и в этот момент массив data содержит значения от 0 до 31. Отладчики, подобные sdb, имеют доступ к таблице идентификаторов трассируемого процесса, из которой они получают информацию об адресах данных, используемых в качестве параметров функции ptrace.
   Использование функции ptrace для трассировки процессов является обычным делом, но оно имеет ряд недостатков.
   • Для того, чтобы произвести передачу порции данных длиною в слово между процессом-отладчиком и трассируемым процессом, ядро должно выполнить четыре переключения контекста: оно переключает контекст во время вызова отладчиком функции ptrace, загружает и выгружает контекст трассируемого процесса и переключает контекст вновь на процесс-отладчик по получении ответа от трассируемого процесса. Все вышеуказанное необходимо, поскольку у отладчика нет иного способа получить доступ к виртуальному адресному пространству трассируемого процесса, отсюда замедленность протекания процедуры трассировки.
   • Процесс-отладчик может вести одновременную трассировку нескольких процессов-потомков, хотя на практике эта возможность используется редко. Если быть более критичным, следует отметить, что отладчик может трассировать только своих ближайших потомков: если трассируемый процесс-потомок вызовет функцию fork, отладчик не будет иметь контроля над порождаемым, внучатым для него, процессом, что является серьезным препятствием в отладке многоуровневых программ. Если трассируемый процесс вызывает функцию exec, запускаемые образы задач тоже подвергаются трассировке под управлением ранее вызванной функции ptrace, однако отладчик может не знать имени исполняемого образа, что затрудняет проведение символьной отладки.
   • Отладчик не может вести трассировку уже выполняющегося процесса, если отлаживаемый процесс не вызвал предварительно функцию ptrace, дав тем самым ядру свое согласие на трассировку. Это неудобно, так как в указанном случае выполняющийся процесс придется удалить из системы и перезапустить в режиме трассировки.
   • Не разрешается трассировать setuid-программы, поскольку это может привести к нарушению защиты данных (ибо в результате выполнения функции ptrace в их адресное пространство производилась бы запись данных) и к выполнению недопустимых действий. Предположим, например, что setuid-программа запускает файл с именем "privatefile". Умелый пользователь с помощью функции ptrace мог бы заменить имя файла на "/bin/sh", запустив на выполнение командный процессор shell (и все программы, исполняемые shell'ом), не имея на то соответствующих полномочий. Функция exec игнорирует бит setuid, если процесс подвергается трассировке, тем самым адресное пространство setuid-программ защищается от пользовательской записи.
   Киллиан [Killian 84] описывает другую схему трассировки процессов, основанную на переключении файловых систем (см. главу 5). Администратор монтирует файловую систему под именем "/proc"; пользователи идентифицируют процессы с помощью кодов идентификации и трактуют их как файлы, принадлежащие каталогу "/proc". Ядро дает разрешение на открытие файлов, исходя из кода идентификации пользователя процесса и кода идентификации группы. Пользователи могут обращаться к адресному пространству процесса путем чтения (read) файла и устанавливать точки прерываний путем записи (write) в файл. Функция stat сообщает различную статистическую информацию, касающуюся процесса. В данном подходе устранены три недостатка, присущие функции ptrace. Во-первых, эта схема работает быстрее, поскольку процесс-отладчик за одно обращение к указанным системным функциям может передавать больше информации, чем при работе с ptrace. Во-вторых, отладчик здесь может вести трассировку совершенно произвольных процессов, а не только своих потомков. Наконец, трассируемый процесс не должен предпринимать предварительно никаких действий по подготовке к трассировке; отладчик может трассировать и существующие процессы. Возможность вести отладку setuid-программ, предоставляемая только суперпользователю, реализуется как составная часть традиционного механизма защиты файлов.