Rio: разработка параллельной оконной системы

1. История

Тема: внешняя и, особенно, внутренняя конструкция Rio, (новой) оконной системы ОС Plan 9.

Потомок 8½:

Оконная система 8½ первоначально была написана на языке C с использованием процессов и основанных на setjmp и longjmp дополнительных сопрограмм (интересное упражнение).

В 1995 году для проекта Brazil система была переписана на языке Alef и конвертирована в настоящие потоки (threads) и процессы.

В 1999 году конвертирована для использования потоковой библиотеки.

Историческая заметка: хотя Alef был и многообещающим языком, но задача по поддержке еще одного варианта языка программирования для сонм архитектур оказалась слишком трудной, так что мы собрали во едино все наши знания и создали потоковую библиотеку для С.

2. Общая конструкция

По сути, не считая очевидных, оконная система состоит из трех составляющих:

Она должна параллельно мультиплексировать все эти части для многочисленных клиентов.

Rio выглядит довольно необычно по сравнению с другими оконными системами (но не по сравнению с сервисами Plan 9), так как она реализована как файловый сервер.

3. 9P: файл-серверный протокол Plan 9

(Почти) все «население» мира Plan 9 является файловым сервером или как минимум представителем одной из категорий:

Клиент подключается к серверу.
Клиент запрашивает — сервер отвечает.

Запросы:

Могут работать через любой надежный канал: TCP/IP, pipe,…

4. Пространство имен

Пространство имен представляет собой доступный процессу набор файлов.

В Unix-системах пространство имен одинаково для всех процессов. В Plan 9 оно управляемо, при чем у каждого процесса может быть свое частное пространство имен. На практике, пространства имен разделены между набором связанных процессов под названием группа.

Пользовательский файл профиля ($home/lib/profile) формирует пространство имен начиная с корневого каталога / и монтирует, подключая, сервисы.

«Превращает» все сервисы, включая основные устройства, в файловые серверы.

Таковы основы Plan 9.

5. Примеры устройств

Процессы:

    /proc/1/…
    …
    /proc/27/ctl
    /proc/27/mem
    /proc/27/note
    /proc/27/notepg
    /proc/27/proc
    /proc/27/status
    /proc/27/text
    …

Консоль:

    /dev/cons
    /dev/time
    /dev/cputime
    /dev/pid
(Все файлы текстовые)

6. Примеры устройств (управляемые)

Сетевые:

    /net/il/clone
    /net/il/stats
    /net/il/29/ctl
    /net/il/29/data
    /net/il/29/err
    /net/il/29/listen
    /net/il/29/local
    /net/il/29/remote
    /net/il/29/status
    …
    /net/tcp/…
    /net/udp/…
    /net/cs
    …
При чем все это без использования каких-либо специальных системных вызовов. Просто открывайте, читайте и записывайте файлы.

7. Примеры устройств (управляемые)

Мышь:

    /dev/mouse
    /dev/cursor

Растровая графика:

    /dev/screen
    /dev/window
    /dev/draw
Формат файла /dev/draw — RPC (уделенный вызов процедур), заканчивающийся еще одним файлом. (Так называемый, вложенный RPC.)
    void
    draw(Image *dst, Rectangle dr,
        Image *src, Point sp,
        Image *mask, Point mp);
    /dev/draw:
        ’d’
        2 байта для идентификатора назначения
        4x4 байтов назначения
        2 байта исходного идентификатора
        …

8. Структура оконной системы (I)

Для записи оконной системы,

Для получения символов ввода — чтение /dev/cons.
Для получения позиции мыши и нажатых кнопок — чтение /dev/mouse.
Для обновления экрана — запись /dev/draw.

Что же делает клиент?

Для получения символов ввода — чтение /dev/cons.
Для получения позиции мыши и нажатых кнопок — чтение /dev/mouse.
Для обновления экрана — запись /dev/draw.

Как вы видите, действия оконной системы и ее клиента идентичны!

9. Структура оконной системы (II)

Таким образом, Rio — это все лишь мультиплексор:

Обеспечивает одинаковый (выглядящий таковым) вид окружения для приложений.
Всем клиентам представляет эти имена файлов, но каждый получает собственный четкий набор.

Файлы /dev/cons и /dev/mouse для каждого окна отличаются. (Фактически, 8½ обеспечивала также разные файлы /dev/draw, но Rio не делает этого, что увеличивает ее эффективность.)

В отличие от Unix, где /dev/tty — это одни и те же файлы, но с различными содержимыми, в Rio /dev/cons действительно являются другими файлами, но с одинаковыми именами и различными содержимыми. (Я могу доказать, что этот вариант гораздо лучше.)

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

10. Останов: хорошие побочные эффекты

Удаление окна: контрольный счет открытых файлов.

Del (прерывание)

    /dev/cons

Удаленная прозрачность: если есть возможность экспортировать файл (напр., NFS), то есть и возможность экспортировать целую оконную систему.

Рекурсия:

11. Проблема

Rio должна управлять всеми следующими операциями:

Как же управлять всем этим параллельно, одновременные требования?

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

12. Параллельный дизайн (I): распределение процессов

Итак, теперь давайте подойдем поближе к вопросу «процессы против потоков». Давайте дадим каждому отдельному компоненту процесс, управляющий им:

  1. Окна. (Каждый запустит пример одинаковой winctl-функции с разными аргументами.)
  2. Мышь
  3. Клавиатура.
  4. Интерфейс пользователя для оконного управления.
  5. Соединение с файловым сервером, который будет получать запросы 9P.
  6. Каждый входящий клиентский запрос ввода-вывода.

Все эти процессы будут общаться лишь посредством сообщений в каналах.

Следует заметить, что большую часть времени все эти процессы будут блокированы ожиданием каких-нибудь событий. Мощь этой модели заключается в том, что мы можем разлагать сложное состояние системы на независимые исполнения — небольшие последовательности операций. Внешние события будут естественно следовать за упорядочением процессов по мере их выполнения, планируя ассоциированные связи.

13. Процессы против потоков

Выбор любого из них зависит от двух факторов: (1) системные вызовы блокировки и (2) протоколы с блокировкой.

1. При блокировке процесса с помощью системного вызова (read, write, и т.п.) будут заблокированы все потоки данного процесса. Следовательно, нам необходимо выполнять системные вызовы блокировки (прочитать мышь, прочитать 9P pipe, и т.п.) в отдельных процессах, и передавать их данные через каналы после того как они их получат.

2. Параллельное выполнение процессов не дает гарантии об одновременном доступе к памяти, так что мы должны использовать блокировки для защиты структур данных. Тем не менее, потоки в пределах одного процесса не выполняются одновременно, и планирование происходит только в местах связи. Поэтому между связями поток может получать доступ к глобальным данным без создания помех другим потокам данного процесса, и без надобности в блокировках.

Наша общая конструкция такова:

14. Картина

Прямоугольники — процессы, эллипсы — потоки.

Наличествуют и другие связи, напр., поток интерфейса пользователя также общается с потоками управления окном (window control threads — Wctl) для выполнения команды resize, и т.д.

15. Комментарии

Rio может быть представлена как основная программа (большой прямоугольник) с большим количеством потоков внутри, и несколькими процессами-серверами, соединяющими сервисы операционной системы с ней.

Никакие разделяемые данные не трогаются вне прямоугольника, так что блокировки необязательны.

Связь синхронизирует, планирует и защищает данные от модификаций.

Параллельное соединение процессов/потоков означает, что система не блокируется пока происходит ожидание чего-либо; напр., действия по управлению окна могут происходить пока клиент занят записью в окне.

Этот метод позволяет писать ПО относительно легко.

16. Xfids (1)

Xfids отвечает за состояние единого сообщения 9P. Они управляются как пул анонимных потоков, которые размещаются и передаются сообщению 9P динамически.

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

17. Xfids (2): процесс 9P

      Xfid *x;

      for(;;){
          read(fd, message);
          if(x == 0){
              sendp(cxfidalloc, nil);
              x = recvp(cxfidalloc);
          }
          преобразование message в x;
          x = (*fcall[x->type])(x);
      }
  extern void xfidread(Xfid*);

  static
  Xfid*
  fsysread(Xfid *x)
  {
      if(read of directory){
      оперирование message;
      return x;
      }
      sendp(x->c, xfidread);
      return nil;
  }

18. Xfids (3): потоки в основном процессе

Процесс-распределитель тривиален; большая часть кода в данном примере опущена. Он запускает новый Xfid схожий с этим:
    x=emalloc(sizeof(Xfid));
    x->c=chancreate(sizeof(void(*)(Xfid*)), 0);
    threadcreate(xfidctl, x, 16384);
Контроллер Xfid во многом схож с этим:
  void
  xfidctl(void *arg)
  {
      Xfid *x;
      void (*f)(Xfid*);
      x = arg;
      for(;;){
          f = recvp(x->c);
          (*f)(x);
          if(decref(x) == 0)
               sendp(cxfidfree, x);
          }
  }
  void
  xfidread(Xfid *x)
  {
      /* управление чтением как потоком главного процесса */
  }

Оригинал: /plan_9/3rd_edition/rio/
© Перевод на русский язык, Андрей С. Кухар, 2004