06/04/2020

Web (2): Watcher.js — компонент для просмотра log-файлов

Задача
Иногда, чаще при разработке встраиваемых систем, возникает необходимость просматривать журнальный файл (лог) какого-либо приложения или демона, работающего на целевой системе или на «железке». Каждый раз ходить на устройство по SSH/Telnet/UART'у бывает не очень удобно. Особенно, когда система уже работает и имеются средства удалённой загрузки, допустим, конфигураций, прошивок или самого ПО, на устройство через систему управления (которая так же может быть Web-приложением).
Можно было бы просто запрашивать интересующий файл через Web-сервер. Но тут есть два нюанса. Первое — Web-сервер, в целях безопасности, не отдаст клиенту файл, расположенный вне DocumentRoot, по простому HTTP-запросу. Второе — даже если настроить сервер так, чтобы он отдавал файл (можно разрешить доступ к каталогу с логами или сделать ссылки на интересующие файлы в DocumentRoot), не будет автоматического обновления дописываемых в лог строк — придётся каждый раз обновлять страницу и ждать пока файл загрузится заново и проматывать вниз. Здесь казалось бы, можно просто написать CGI, которая будет делать 
tail -f /path/to/file
но на Web'е это работать не будет, потому что сервер отдаёт ответ от CGI только по завершению процесса, а tail -f не завершится никогда. Страница на стороне клиента зависнет в вечном состоянии загрузки.

Решение
Именно эти две задачи и решает мой компонент (Web-приложение), который я представляю в этой статье — компонент для просмотра текстовых файлов через Web-интерфейс, Watcher.
Watcher позволяет просматривать любые текстовые файлы на файловой системе, к которым у CGI-скриптов, запускаемых Web-сервером, есть доступ на чтение. Я на своей системе смог просмотреть даже /etc/fstab и /proc/cpuinfo. (А вот /var/log/messages без изменения прав доступа не получилось загрузить, он оказался под запретом даже для чтения.) Строки грузятся кусками и один раз, то есть вы можете читать информацию, не дожидаясь её полной прогрузки и без повторных загрузок файла.

Алгоритм, по которому работает Watcher
Сперва запрашивается размер файла, выраженный в количестве строк. На этом же этапе происходит определение ошибки доступа или отсутствия указанного файла. Если обнаружена ошибка, выводится сообщение и дальнейшая работа прекращается. Если ошибки не произошло, Watcher кусками подгружает строки из файла от нулевой до того размера, который был получен в начале. Затем запускается циклический таймер, который опрашивает изменение размера файла (в строках). Если файл увеличился, запрашивается (кусками) новое содержимое и отображается. Данные (количество строк и прогресс) получаемые на первом этапе отображаются в левом поле (Fetch). Данные, подгружаемые в последствии, отображаются в центральном поле (Watch).

Управление Watcher'ом
Имя файла задаётся в параметре адресной строки (?File=). То есть, вы вводите адрес (например, 127.0.0.1/Watcher.html?File=../log) и нажимаете enter. Всё, Watcher начинает работать по загрузке страницы. Если вам удобно чтобы при подгрузке новых строк, компонент автоматически прокручивался на них, есть переключатель Auto Scroll.
Иногда бывает нужно сбросить лог, очистить его содержимое. Для этого есть кнопка Clear Log. Эта функция очищает файл (при условии соответствующих прав доступа) на устройстве и перезапускает Watcher.

Ограничения и пояснения
Я долго соображал, что последняя, пустая строка, которую мы видим в текстовых редакторах, на самом деле не существует — символ новой строки добавляется в конце последней строки — той, которая нам кажется предыдущей. То есть, символ новой строки является частью последней строки, а не отдельной строкой. Поэтому, когда вы видите в редакторе или в выводе cat пустую строку снизу — это особенности вывода. Эта строка не выводится в Watcher как отдельная, пустая. Отсюда же следствие — если вы допишете строку в конец файла без символа новой строки, она не подхватится Watcher'ом.
В следствие особенностей алгоритма (работает только со строками и по строкам), который я разработал в этом компоненте есть некоторые ограничения:
  • нельзя удалять строки из файла — Watcher не отреагирует на это, более того — алгоритм вовсе собьётся
  • не имеет смысла изменять строки в файле — однажды загрузив содержимое файла, Watcher не следит за изменениями этого содержимого
  • CGI Watcher'а написан на Python 2.x — на Python 3.x пока не работает
  • у меня в проекте своя структура (расположение файлов) — вам придётся настроить свой httpd так, чтобы работал Python-скрипт из моего каталога (CGI) или переместить его в каталог с вашими CGI
Чтобы воспользоваться:
git clone https://gitlab.com/daftsoft/watcher.js.git



13/03/2020

Великолепная Java (2): сборка проекта — библиотеки, ресурсы

В предыдущей статье (Великолепная Java (1): сборка проекта — компиляция, JAR) мы рассмотрели процесс сборки Java-программ на примере очень просто организованной программы. ООП и принципы архитектуры ПО «SOLID» подразумевают разделение функционала на классы, каждый из которых обеспечивает ограниченный, минимальный с точки зрения архитектуры, функционал. Это подразумевает поставку классов в виде компонентов. Брюс Эккель в своей книге «Философия Java» разделяет разработчиков на «создателей классов» и «программистов-клиентов». Следовательно, в реальной жизни ваши проекты будут организованы несколько (скорее — значительно) сложнее, чем рассмотренный нами ранее пример.
Библиотеки (или компоненты) в Java распространяются в виде тех же .jar-файлов. Да, .jar-файл может содержать не только конечное приложение, но и библиотеки, используемые «программистами-клиентами» (и как мы увидим ниже — любые другие файлы). Библиотека отличается от приложения тем, что не содержит главного класса (того, который мы в прошлой статье указывали при помощи ключа -e). Главный класс отличается от любого класса-компонента тем, что у него есть главная функция — main (). Что, в прочем, не запрещает легко использовать функционал класса, имеющего эту функцию другими классами.
В предыдущей статье мы уже рассматривали преимущества компоновки кода в .jar-файлы, как скомпилированные классы собираются в jar и как запускать .jar-файлы как приложения.
Здесь мы рассмотрим как на Java создавать библиотеки и как создавать приложение с использованием библиотек.
Вы можете спросить — «Зачем мне вообще создавать какие-то компоненты? Я могу просто разнести код на несколько .java-файлов и компилировать их вместе или вовсе реализовать всё в одном файле». У запаковки скомпилированных классов в один компонент (.jar-файл) есть три причины:
Первая — коммерческая: дизассемблирование, хоть и выполнимо, но является задачей непростой, и компании, распространяющие компоненты на коммерческой основе, заинтересованные в сокрытии реализации своих продуктов, не поставляют библиотеки в .java- и .class-файлах. Как мы описывали в предыдущей статье, Java на каждый класс создаёт отдельный .class-файл, и чтобы «красиво» поставлять компонент в виде одного файла, удобно запаковать всю библиотеку (состоящую иногда из сотен .class-файлов) в один .jar-файл. В этой статье мы научимся, в том числе и использовать компоненты, попавшие к нам в руки в виде .jar-файла.
Вторая — архитектурная: компонент представляет (должен быть так спроектирован, чтобы представлять) собой какой-то уровень абстракции. И «программистам-клиентам» удобно использовать функционал этого компонента как отдельный .jar-файл.
Третья — производительность: так как скомпилированный код Java является кроссплатформенным, у разработчиков нет необходимости пересобирать все компоненты при сборке проекта. «Программист-клиент» может использовать скомпилированный однажды компонент в своём проекте где угодно и когда угодно (в отличие от других языков программирования), экономя время на сборку. В крупных проектах эта экономия может быть очень существенной — обратите внимание как, даже такой маленький проект как пример из этой статьи собирается по make clean all.

Начнём. Библиотеки в Java называются package. Идея компоновать классы в библиотеки так же помогает в разрешении конфликтов имён. В разных библиотеках могут находиться классы с одинаковыми именами. Обращаться к ним можно через указание имени библиотеки, в которой он реализован. Это описано в примере кода основного приложения (см. ниже).
Рассмотрим файл с кодом одного из классов, который входит в нашу библиотеку (Hello.java):
package HelloPackage;

public class Hello
{
  public void Print ()
  {
      System.out.println (getClass().getName() + ".Print ()");
  }
}
Ниже представлен пример второго класса из нашей библиотеки (Bye.java):
package HelloPackage;

// Приватный класс библиотеки, в пределах библиотеки он будет 
// доступен, а снаружи её к нему доступа не будет:
class ByeBye
{
  public void Print ()
  {
      System.out.println (getClass().getName() + ".Print ()");
  }
}

public class Bye
{
  public static void main (String args[]) throws Exception
  {
    // Демонстрация возможности упаковки исполняемых классов
    // в библиотеку, скомпилировав проект, попробуйте выполнить:
    // $JAVA_HOME/bin/java HelloPackage.Bye
    System.out.println ("Bye.main()");
  }
  public void Print ()
  {
      // Здесь класс ByeBye доступен:
      ByeBye BB = new ByeBye ();
      BB.Print();
      System.out.println (getClass().getName() + ".Print ()");
  }
}
Обратите внимание на то, что в разных файлах используется одно название package. Я мог бы объединить реализацию всех трёх классов в одном файле, но специально разнёс их для демонстрации того, что одна библиотека может быть собрана из многих файлов и, соответственно, содержать в себе много классов.
По сути дела библиотека (и её имя) это всего лишь название каталога, в который будут положены .class-файлы. Причём компиляция этих файлов в один проход необязательна — в Makefile я описал три отдельных правила для трёх классов из двух файлов. Java при компиляции любого из них, при необходимости, создаст каталог с названием библиотеки и положит в него, соответствующий .class-файл. Если в одном .java-файле реализованы несколько классов, все соответствующие им .class-файлы будут положены в каталог библиотеки. При компиляции другого файла, в котором указано такое же имя библиотеки, Java добавит новые .class-файлы в существующий каталог, не затрагивая уже находящиеся там файлы.
В коде примера так же описан приватный класс библиотеки (класс ByeBye в файле Bye.java) — это неэкспортируемый из библиотеки класс. Такие классы используются для сокрытия реализации внутреннего функционала библиотеки.
Процесс создания библиотеки заключается в следующем: компилируются все классы (javac с ключом -d — создание библиотеки), затем каталог с классами библиотеки упаковывается в .jar-файл.
Весь этот механизм описан в Makefile, здесь не буду разъяснять все ключи и параметры этого процесса. После сборки библиотеки в .jar-файл мы можем его распространять другим разработчикам единым файлом, без .java- и .class-файлов. Никаких дополнительных файлов (наподобие .h-файлов) не нужно.
Далее, имея .jar-файл библиотеки (либо только что собранный нами, либо полученный от внешнего поставщика), мы собираем основное приложение. Здесь нам нужно создать т.н. манифест — файл с указаниями параметров для Java. Эти параметры будут использоваться при запуске нашего приложения. Сам манифест представлен в проекте. Здесь ограничимся необходимыми нам параметрами. Их два, и они уже известны нам из предыдущей статьи (там мы указывали их в параметрах командной строки):
Main-Class — имя класса, который будет запускаться и имеет функцию main():
Main-Class: HelloWorld
и в Class-Path мы указываем имя .jar-файла нашей библиотеки — Hello.jar (здесь может быть любое количество библиотек, в том числе и полученных из сторонних источников)
Class-Path: Hello.jar
Код класса основного приложения:
import HelloPackage.*;
import java.io.*;

class HelloWorld
{
  public static void main (String args[]) throws Exception
  {
    // Вот так выглядело бы обращение к библиотечному классу
    // без import'а (тоже работает, но писать больше):
    // HelloPackage.Hello lHello = new HelloPackage.Hello ();
    // А так без import'а не сработает:
    Hello lHello = new Hello ();
    Bye lBye = new Bye ();
    
    // Ошибка: ByeBye является приватным классом библиотеки:
    // ByeBye lByeBye = new ByeBye ();
    
    // Получаем доступ к ресурсу - нашему текстовому файлу:
    InputStream lInputStream = 
            HelloWorld.class.getResourceAsStream("Info.txt"); 
    BufferedReader lBufferedReader = 
            new BufferedReader (new InputStreamReader(lInputStream)); 
    String lString = "";
    // Обращаемся к первому классу из нашей библиотеки:
    lHello.Print();
    // Выводим содержимое текстового файла из ресурсов:
    System.out.println ("Strings read from JAR-resource: ");
    while ((lString = lBufferedReader.readLine()) != null) 
      System.out.println(lString); 
    // Обращаемся к другому классу из нашей библиотеки:
    lBye.Print();
  }
}
Использование библиотеки сводится к простому импорту её содержания. В нашем примере мы импортируем все классы (разумеется публично экспортируемые из библиотеки). Выглядит это так:
import HelloPackage.*;

Как я уже писал, мы можем записать в .jar-файл любые файлы. В Java файлы упакованные в .jar-файл называются ресурсами. В примере выше описан механизм доступа к содержимому текстового файла.

Далее компилируем наш главный класс, собираем его .class-файл, файл манифеста, все файлы ресурсов и .jar-файл библиотеки в конечный .jar-файл и имеем приложение, включающее в себя всё необходимое. Как это делается так же описано в Makefile.

Что в этом примере сделано не совсем правильно. В демонстративно-образовательных целях я назвал .jar-файл с библиотекой именем, отличным от имени библиотеки. В целях повышения читаемости и понимания кода, а, следовательно, облегчения его дальнейшей поддержки, так делать не стоит.
Так же в этом примере я разместил код библиотеки и код исполняемого класса в один каталог и свёл всю сборку в один Makefile. С точки зрения архитектуры, это неправильно и так поступать в своей профессиональной деятельности не нужно. Обычно создают отдельный каталог, например lib, в котором лежат все библиотеки. Так как часто «программисты-клиенты» используют библиотеки, разрабатываемые не ими — либо своими коллегами, либо вовсе полученные извне компании, имеет смысл выделить библиотеки в отдельный модуль в системе контроля версий (например, submodule в git).
Если библиотека разрабатывается внутри коллектива, то иногда в принципе можно  отказаться от сборки её в .jar-файл и просто запаковать каталог (в нашем случае, HelloPackage) со всеми скомпилированными классами в .jar-файл с приложением, содержащим основную программу, не забыв указать соответствующий classpath в манифесте приложения.
Теперь по названиям компонентов — в Java принято называть компоненты по шаблону com.companyname.packagename. К слову, com здесь обозначает commercial. В примере я этому не следовал, (не только я) считаю это необязательным. Следовать этому правилу или нет — решать вам.

Чтобы посмотреть весь проект и поиграться скачайте:
git clone https://gitlab.com/daftsoft/javalibstutorial.git

05/03/2020

Великолепная Java (1): сборка проекта — компиляция, JAR

Как начинающим изучать новый язык программирования, так и опытным разработчикам иногда нужно разобраться в том как из кода получается «конечный продукт» — программа, которую можно запускать и поставлять пользователям. В этой статье мы рассмотрим все этапы, необходимые для того чтобы из Java-кода получить программу. Будем изучать Java Basic Tools или Java Development Tools, то есть, ручную, консольную сборку проекта. JDK является основным инструментом в работе с Java. Инструменты эти находятся в каталоге $JAVA_HOME/bin (например /opt/JDK-1.8.0_221/bin/).
Напишем простую программу, файл назовём Hello.java:
class HelloWorld
{
  public static void main (String args[])
  {
    System.out.println ("Hello, Java-World!");
  }
}
Для того чтобы скомпилировать Java-программу, нужно выполнить:
$JAVA_HOME/bin/javac ./Hello.java
javac — компилятор Java, транслирует .java-файлы в .class-файлы. В нашем случае, после работы javac в каталоге появится файл HelloWorld.class. Чтобы запустить нашу программу выполняем:
$JAVA_HOME/bin/java HelloWorld
Обратите внимание на три вещи. Первое — запускаем мы именно класс, то есть, нужно указывать не имя .class-файла, а имя класса (то есть имя .class-файла без расширения). Java сама найдёт (или не найдёт) в текущем каталоге файл, содержащий заданный в параметре класс. Поскольку мы указываем имя класса, а не файла, «./» перед ним ставить не нужно — это приведёт к ошибке. Второе — Java чувствительна к регистру в именах классов, файлов, переменных, поэтому все названия должны указываться с учётом регистра. Третье — каждый .class-файл соответствует имени класса, который он содержит. Имя .class-файла не связано с именем .java-файла, в котором он был реализован. Если в одном .java-файле будут реализованы несколько классов, то javac «разберёт» этот файл на соответствующее количество .class-файл'ов. Продемонстрируем это примером. Дополним файл Hello.java другим классом Hello (дополните предыдущий файл этим фрагментом кода):
class Hello
{
  public static void main(String _Args[])
  {
    try
    {
      System.out.println ("Hello, " + _Args[0] + "!");
    } catch (ArrayIndexOutOfBoundsException _E)
    {
      System.out.println ("Please pass parameter to program.");
    }
  }
}
P.S.
В Java список аргументов организован несколько иначе: нулевой элемент — это уже первый параметр, а не имя исполняемого файла.
Теперь, после компиляции (та же команда, что мы использовали в первый раз) мы увидим два .class-файла: Hello.class и HelloWorld.class. Так как оба файла имеют функцию main (), мы можем запустить оба. HelloWorld — как описано выше, и Hello (который принимает параметр командной строки):
$JAVA_HOME/bin/java Hello Daft
Отсюда мы постепенно приходим к необходимости создания какого-то единого файла для поставки пользователям. Мы же не хотим высылать пользователям море непонятных ему .class-файлов и объяснять какой именно является главным и как его запускать, так?
Для этих целей в Java реализованы Java-архивы (.jar-файлы). Чтобы создать .jar-файл и записать в него класс, нужно выполнить команду:
$JAVA_HOME/bin/jar -cvf ./Hello.jar ./Hello.class
Эта команда заархивирует указанный .class-файл в .jar-файл с заданным именем.
Чтобы запустить .jar-файл выполняем следующее:
$JAVA_HOME/bin/java -cp ./Hello.jar Hello
В предыдущей команде мы запаковывали .class-файл и указывали имя файла (с расширением), здесь мы запускаем класс — указываем имя класса (без расширения). Ключ -cp означает classpath — путь, где нужно искать заданный класс. В данной ситуации — это наш .jar-файл.
Как я уже указывал, .jar-файл является архивом и он действительно сжимается при создании, что дополнительно экономит место на носителе или время при передаче его по сети. Конкретно это zip-архив, мы можем посмотреть его содержимое:
unzip -l ./Hello.jar
Увидим:
Archive:  ./Hello.jar
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  03-04-2020 17:50   META-INF/
       69  03-04-2020 17:50   META-INF/MANIFEST.MF
      736  03-04-2020 17:36   Hello.class
---------                     -------
      805                     3 files
В вышеописанную команду запаковки можно добавить не только классы, но и любые другие файлы, которые в последствии можно будет использовать как ресурсы из ваших программ (иконки, картинки, любое иное содержание и даже другие .jar-файлы).
Но пока запуск скомпилированных и даже уже запакованных в один файл классов является всё-таки не самым «дружественным». Как упростить запуск .jar-файла? Для этого предусмотрена возможность указать главный класс, добавив ключ e (что означает указать точку входа в программу, entry point): 
$JAVA_HOME/bin/jar -cvfe ./Hello.jar Hello ./Hello.class
Теперь мы можем запустить .jar-файл самым простым для Java способом:
$JAVA_HOME/bin/java -jar ./Hello.jar
На этом на сегодня всё. В следующих статьях мы рассмотрим сборку более сложных Java-программ (да, ещё есть куда двигаться).

25/02/2020

Многопоточность в POSIX или Multithreading в ANSI C (1): поток и процесс, начало работы с libpthread

В этой статье мы научимся создавать, запускать и останавливать потоки в POSIX-совместимых системах на Plain C.
Сначала вкратце рассмотрим, что такое поток. Воспользуемся последним стандартом POSIX.1-2017, чтобы разобраться в терминологии:
Live Process
An address space with one or more threads executing within that address space, and the required system resources for those threads.
Здесь написано следующее: процесс есть адресное пространство с одним или более потоков выполняющимися в нём и необходимыми системными ресурсами для этих потоков.
Приписка «живой» нас не интересует, в стандарте используется термин Live Process для отличия его от Zombie Process. В этой статье мы будем говорить только о «живых».
Так же в стандарте написано, что многие системные ресурсы определённые стандартом являются общими для всех потоков в процессе. Эти ресурсы включают в себя ID процесса, ID родительского процесса, ID группы процессов, текущую директорию, файловые дескрипторы и пр.
Thread
A single flow of control within a process. Each thread has its own thread ID, scheduling priority and policy, errno value, floating point environment, thread-specific key/value bindings, and the required system resources to support a flow of control. Anything whose address may be determined by a thread, including but not limited to static variables, storage obtained via malloc(), directly addressable storage obtained through implementation-defined functions, and automatic variables, are accessible to all threads in the same process.
Что означает: поток есть одно «течение контроля» (нам, русскоязычным, привычнее термин «поток выполнения»). Каждый поток имеет собственный ID, приоритет планировщика и политики, значение errno и пр.. Всё, чей адрес может быть определён потоком, включая, но не ограничиваясь статическими переменными, динамически выделенная память и др. доступны всем потокам одного процесса.

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

Я считаю, что разобраться в этом было важно, так как всё (во всяком случае системное) программирование заключается в распределении ресурсов операционной системы и управлении ими, а то, что мы выше почерпнули из стандарта даёт следующие выводы:
  1. Мы можем пользоваться (читать, писать) ресурсами в своих потоках в пределах одного процесса без использования т.н. IPC (методы межпроцессного взаимодействия).
  2. Мы не можем обращаться к ресурсам других процессов без применения IPC. Даже из т.н. основного потока выполнения (main (), например), даже из под root'а. А POSIX хоть и предоставляет инструменты IPC, но это достаточно сложные механизмы.
Теперь рассмотрим технологию создания второго и последующих потоков. На всякий случай, напомню, первый у нас всегда есть — его создала ОС при запуске вашей программы (возможно, о том как это происходит я напишу). Этот поток я называю основным.
В самой по себе ANSI C нет механизмов работы с многопоточностью. Есть различные реализации работы с потоками. Например, специфичная GNU Portable Threads, Protothreads и пр. Первая преследовала цель максимальной портируемости, вторая — легковесности. Мы будем рассматривать самый распространённый (хотя и не самый оптимальный, почему я распишу в последующих статьях) стандарт, т.н. POSIX Threads (pthread). POSIX Threads API реализован в библиотеке libpthread.

Приведём пример простой многопоточной программы:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

/*
 * Здесь мы проверяем, есть ли в системе POSIX Threads
 */
#ifndef _POSIX_THREADS
#error No POSIX Threads!
#endif

/*
 * Функция, код которой будет выполняться в новом потоке.
 * Обратите внимание на то, что мы можем передать сюда
 * указатель на область памяти. Это можно использовать
 * как механизм передачи некоторых начальных данных потоку.
 */
void *ThreadRoutine (void *_Arg)
{
  printf ("String argument passed to thread routine: %s\n", 
          (char *)_Arg);
  while (0)
  {
    sleep (1);
    printf ("Thread is doing infinite work...\n");
  }
  printf ("Thread is about to exit normally...\n");
  
  /*
   * Завершить выполнение потока можно двумя способами: 
   * return и pthread_exit () с возвращаемым нами значением 
   * в параметре. Они эквивалентны за исключением некоторой
   * (незначительной) разницы в быстродействии, что, в свою
   * очередь, связано с функционалом вызова обработчиков 
   * завершения потока. Но мы их здесь не будем рассматривать.
   */
  /* return (void *)(uintptr_t)0xC0FFEE; */
  pthread_exit ((void *)(uintptr_t)0xC0FFEE);
  /*
   * А что будет, если вызвать exit () из функции потока?
   * Приложение завершится, так как exit () это не возврат
   * из функции, а более сложный системный вызов, который
   * "корректно" закрывает приложение вместе со всеми 
   * потоками. Недопонимание этой разницы может возникнуть 
   * из-за того, что мы часто завершаем наше приложение 
   * простым return'ом в main'е. На самом деле, при 
   * "возврате" из main'а компилятор подставляет функционал 
   * "корректного" закрытия программы. Поэтому завершать 
   * работу потока нужно только через return/pthread_exit (). 
   * Вы можете заменить return/pthread_exit () на exit (0) 
   * и посмотреть что произойдёт - вы не увидите вывода 
   * "Thread return".
   */
  /* exit (0); */
}

int main (int _argc, char * _argv[])
{
  /* Идентификатор потока, на Linux'е это целое число, на
   * Mac OS это структура.
   */
  pthread_t thread_id;
  /* Возвращаемое значение функции потока */
  void *ThreadRet = NULL;
  /*
   * Функция pthread_create создаёт поток. Первый параметр - 
   * идентификатор потока (для дальнейших операций с ним). 
   * Второй - аттрибуты, третий - функция потока, четвёртый - 
   * аргумент (мы передадим туда название исполняемого файла).
   */
  pthread_create (&thread_id, NULL, ThreadRoutine, _argv[0]);
  /*
   * Функция pthread_join ожидает выхода из функции потока.
   * Так же здесь мы получаем возвращаемое значение из потока.
   * Перед тем как раскомментировать этот вызов, убедитесь, что
   * цикл while в функции потока выключен (например, поставьте 
   * while (0)). Иначе программа зависнет в ожидании завершения 
   * потока. Ведь pthread_join () ждёт корректного завершения
   * работы потока.
   */
   pthread_join (thread_id, &ThreadRet);
  /*
   * Перед тем как включить этот эксперимент, поставьте в функции
   * потока while (1), чтобы включить его бесконечную работу.
   * Функция pthread_cancel () аварийно завершает поток.
   * Дадим ему 5 секунд "пожить" и завершим.
   */
  /* sleep (5); */
  /* pthread_cancel (thread_id); */
  /* Выводим значение, возвращённое из функции потока */
  printf ("Thread return: %lX\n", (uintptr_t)ThreadRet);
  return EXIT_SUCCESS;
}


NB!
Обратите внимание на то, как при возврате из функции потока, я создаю указатель на некий адрес. Это своеобразный трюк. Чтобы не выделять память для возвращаемого значения, с последующим её освобождением, но вернуть простой int, как результат работы, я вернул указатель на адрес памяти, который придумал из головы. Вы можете возвращать указатели на любые адреса, и анализировать их по возврату из pthread_join (). Но стоит помнить, что обращаться по таким, вымышленным адресам нельзя.

Забрать пример:
https://gitlab.com/daftsoft/pthread1.git

Собирать программы с POSIX Threads нужно с указанием библиотеки pthread:
gcc -std=c89 -pedantic -Wall pthread1.c -lpthread -o pthread1

К слову, в Linux'е не всегда был реализован стандарт POSIX Threads. Как же были реализованы потоки в Linux'е до «принятия» POSIX Threads? В то время (c 2.0 до 2.4 включительно) в Linux'е была своя технология, называлась она LinuxThreads. В общих чертах, она заключалась в копировании кода процесса с предоставлением доступа к памяти родительского процесса. При этом, у потоков были разные идентификаторы процессов (PID) и взаимодействие между ними осуществлялось посредством одного из механизмов IPC — сигналов (USR1 и USR2), что исключало возможность использовать этих сигналов разработчиками. То есть, по современным меркам (или по меркам стандарта), это были не совсем потоки. Всё это несовершенство продолжалось до начала 2000-х годов, когда LinuxThreads была заменена библиотекой потоков POSIX (Native POSIX Thread Library, NPTL), реализующей стандарт POSIX Threads в Linux.
Представленного примера достаточно для того, чтобы начать писать свои первые многопоточные программы.
В следующей статье мы, на примере, рассмотрим как действительно работают «общие ресурсы» в потоках в пределах одного процесса, а так же иные аспекты управления потоками.

19/02/2020

Интересно о размерах (2): модели данных

В продолжение рассуждений-исследований о соотношениях разрядности машины и размерностей типов (Интересно о размерах (1): типы и разрядные сетки машин), рассмотрим, как это выглядит формально (научно), как эти соотношения формализованы в стандартах — внесём окончательную ясность в одном из основополагающих аспектах вычислительной техники.

Соотношение разрядности машины и разрядностей типов данных (на самом деле просто отношения разрядностей типов друг к другу) называется моделью данных. И на самом деле модель данных задаётся компилятором, а не разрядностью машины, ровно, как и не операционной системой. То есть, даже на одной ОС, используя разные компиляторы, можно получить разные соотношения (которые мы косвенно рассмотрели в предыдущей статье на примере программы). Моделей данных существует много, но обычно в распространённых связках разрядность машины/ОС/компилятор используется ограниченное количество.

Принятый (сокращённый) формат названия модели данных состоит из перечисления типов, которые относятся к максимальной разрядности и значения самой разрядности.

Типы это (в порядке возрастания): S  короткое целое число (short), I  целое число (int), L  длинное целое число (long), LL  двойное длинное целое число (long long), P  указатель (void *). Разрядность указывается в битах  32, 64, 128.

char (условно) всегда 8-битный (про историю этого замечательного типа данных я напишу отдельно) и даже не указывается.

Пример, макрос которого мы использовали в предыдущей статье LP64, означает что компилятором задана следующая модель данных — 64-битными являются типы начиная с L (long), а именно — long и pointer. Указание LL излишне, так как он стоит рядом с указателем, и в таком случае считается равным ему. Если бы он был больше чем указатель, в конце было бы дописано LL128 (LP64LL128). Реальный пример ILP32LL64 (int, long и pointer — 32-битные, long long — 64-битный), эта модель использовалась в Win32 и некоторых UNIX'ах в 90-ые года. Можно задаться вопросом — как машина, разрядностью менее разрядности максимального типа вообще может оперировать такими типами? Верно — на операции с этими типами уходит более одной инструкции (такта), что крайне неэффективно с точки зрения вычислительных мощностей. 

Все типы, определённые до типов, указанных в названии модели считаются 32-битными, за исключением типа short — пока он, начиная с PDP-11 является 16-битным всегда. Это подразумевается, хотя возможна и полная запись: I32LP64 (и даже S16L32P64— эта модель данных и принята в стандартах Open System (UNIX является ОС относящейся к этому стандарту). В конце 90-х годов эта модель была согласована как основная для UNIX-систем совместным решением компаний, в том числе DEC, IBM, Sun Microsystems, HP, Intel, NCR.

Есть и отличный от предыдущих пример. В системах UNICOS (UNIX-подобная ОС для линейки суперкомпьютеров Cray) все типы являются 64-битными, и модель данных на этих машинах используется SILP64. И здесь используется символ S, так как даже short здесь 64-битный.

Ну и чтобы наше описание было полным, укажем какая модель данных была в Win16 и Win64. В Win16 использовалась LP32, что означает — 32-битные long и ponter, а int был 16-битным. А в Win64 используется LLP64, то есть, все типы до long long оставлены 32-битными. Сделано это из-за проблем с совместимостью и переписыванием WinAPI при переходе на 64-разрядные системы.

Для большинства UNIX-разработчиков ещё знакомы 32-разрядные системы, там принята модель данных ILP32, что означает что все типы, кроме short, являются 32-битными. И, собственно, единственным отличием 32-разрядных машин (компиляторов) от 64-битных является то, что pointer и long стал 64-битным. Что я и рассмотрел и доказал как один из методов определения разрядности машины в предыдущей статье. Все остальные типы остаются таких же размерностей, что и на 32-битных системах. Сделано это для совместимости кода. Но здесь стоит быть осторожным. В некоторых 32-битных программах типы int, long и pointer используются как синонимы, полагается, что их разрядности равны. Но так как размеры long и указателя в модели данных LP64 стали 64-битными мы должны помнить, что это изменение может вызвать много проблем при переносе кода с 32-разрядной платформы на 64-битную.
Поиск оптимальной модели данных, при каждом переходе на более высокую разрядность системы, является основополагающим моментом в обеспечении качественного процесса разработки ПО. Можно сказать, что модель данных является инструментом разработчика. Так как влиять мы на неё (модель) не можем, наша задача хорошо в них разбираться.

На этом пока всё.

18/02/2020

Web (1): RayCasting vs GPU Color Picking (на примере Three.js)

В определённый момент, перед разработчиком графических приложений с использованием OpenGL/WebGL встаёт задача взаимодействия пользователя с объектами. Будь то элементы двухмерного интерфейса (частный случай трёхмерного пространства) или объекты трёхмерной сцены, существует два подхода к решению этой задачи. Первый заключается в поиске объекта, на который указывает мышь математическим (тригонометрическим) расчётом.  Второй — поиск объекта по цвету пикселя, на который пользователь указал мышью. Первый кажется логичным, понятным и красивым, второй — на первый взгляд кажется странным, но у обоих методов есть свои плюсы и минусы и свои области применения.

В этой статье мы рассмотрим два метода выбора объекта на сцене (OpenGL/WebGL на примере Three.js) — RayCasting и GPU Color Pick.

RayCasting — это технология определения пересечения луча с объектом. При определении выбранного объекта — объекта, на который навели мышь, например, выбирается первый объект, с которым произошло пересечение. На самом деле собираются все объекты, с которыми пересёкся луч, а уже мы используем только первый. RayCasting работает на стороне CPU, то есть, не задействует специфические вычислительные мощности GPU. 

В Three.js для этих целей имеется уже разработанный объект THREE.Raycaster. Пользоваться им достаточно просто, в примере, которым я завершу эту статью, этот метод будет представлен. Описывать его механику смысла не вижу — это обычная тригонометрия.

У RayCasting’а есть два минуса:

  1. Он работает медленнее, так как работает на CPU, и в случае, когда у нас много объектов, это может быть очень тяжёлой задачей для центрального процессора. А в современной графике с использованием актуальных методов создания карт (т.н. сцен) количество объектов может быть очень большим.
  2. Он не учитывает факторы текстуры — прозрачность, и просто «выбирает» объекты, которые встречаются лучу (в нашем случае — выбор объекта мышью, луч рассчитывается от камеры), то есть работает исключительно геометрия (математика).

Технология GPU Color Pick заключается в следующем. При создании сцены, все объекты дублируются в другую сцену, обычно называемую PickingScene и связываются некоторыми ID реальных объектов с цветами объектов в PickingScene. Для исключения излишней нагрузки на GPU, PickingScene, как правило, делается очень маленькой (выходной буфер рендеринга очень маленькой площади, это в некоторой степени сокращает время на рендеринг). В нашем случае, по наведению мыши, вызывается функция поиска объекта в PickingScene и затем выбирается ID объекта в реальной сцене. В силу того, что технология GPU Color Pick работает по цвету, она лишена второго минуса RayCasting’а — мы можем использовать альфа-канал в текстурах объектов. Это значит, что мы можем дать пользователю возможность «выбирать» объекты стоящие «за», сквозь «прозрачные» области стоящих «перед». В силу того, что технология GPU Color Pick работает на GPU, а он, в свою очередь очень «заточен» под такие задачи, эта технология работает гораздо быстрее и почти полностью разгружает CPU (на нём остаётся лишь задача соотнести цвета и ID объектов).

У GPU Color Pick (как ни странно) есть два минуса:

  1. Мы не можем получить координаты места «встречи» указателя (здесь это не луч и не вектор) и объекта — только сам объект, так как объект выбирается не геометрией, а цветом.
  2. Подгружается GPU. Это хорошо тем, что разгружается CPU, но может быть плохо, если вычислительные мощности GPU для нас критичны. Плюс, в момент процесса самого Picking’а, нам надо ждать на CPU пока GPU выполнит задачу поиска объекта по цвету.


Все эти технологии, конечно же применяются и на Bare WebGL и на чистом OpenGL.

Чтобы поиграться (работает без Web-сервера):

git clone https://gitlab.com/daftsoft/gpu-color-pick-vs-raycasting



12/12/2019

Системы сборки (1): GNU Make - Simple Makefile


Системы сборки (1): Make - Simple Makefile  простой Makefile для несложных программ
Когда проект растёт в размерах файлов, разработчики начинают делить его на несколько файлов, потом ещё и ещё делить. Компилировать один файл командой gcc (или иной другой) не сложно. Иногда, когда проект ещё не слишком большой, пишут скрипты типа build.sh, в которых записывают все действия для сборки. Но когда проект становится очень большим, и/или усугубляется использованием сторонних библиотек, возникает необходимость автоматизировать и «обрамить» процесс сборки. Для этих целей разработана система GNU Make.
С одной стороны, в этой статье описана лишь малая доля возможностей и функционала системы GNU Make, с другой — для начала работы, для большинства простых программ и для понимания того, что написано в других Makefile'ах вам этой информации хватит.
Настройка сборки происходит через задание переменных. Все переменные могут быть пустыми (как наши LDFLAGS и INCLUDE ниже). Более того, переменные могут быть неинициализированными вовсе — не объявлены в Makefile. В таком случае, они не будут приводить к ошибкам при попытке использовать их, а будут раскрываться как пустые. Обращение к переменной осуществляется через символ $ и скобки — $(CXXFLAGS). Начнём: Имена компиляторов (CC — для C, CXX — для C++):
CC=gcc
CXX=g++
Эти переменные в make-системе есть по-умолчанию — cc и g++ (на моей системе cc — это ссылка на gcc) для компиляторов C и C++ соответственно. То есть, можно опустить эти объявления, если вы согласны собирать проекты под родную систему и стандартными компиляторами. Возможность изменять эти переменные нужна для кросс-компиляции (сборки проектов под системы, отличные от той, на которой сборка производится) или для работы с разными версиями компиляторов на одной машине. Объявления этих переменных (как и некоторых других ниже) закомментированы. Я делаю это для того чтобы, описывая значения полей и структуру Makefile'а, заодно показать насколько маленьким может быть простой Makefile. Флаги компиляции. Сюда вы можете ставить оптимизацию (например, -O3), стандарт языка (-std=c99), параметры -Werror, -Wall, -pedantic и т.п.
CFLAGS=-Wall
CXXFLAGS=-Wall
Имя исполняемого файла, который мы хотим получить на выходе:
EXECUTABLE=simple
Эта команда собирает все файлы с расширением .cpp в переменную:
SOURCES=$(wildcard *.cpp)
Эта команда создаёт переменную с именами объектных файлов. Это достигается путём замены расширения .cpp на расширение .o:
OBJECTS=$(SOURCES:.cpp=.o)
Так как нам в проекте нужен только список объектных файлов (для чего будет нужен этот список и почему список имён файлов с исходниками не будет нужен — будет видно позже), две предыдущие команды можно объединить в одну:
OBJECTS=$(patsubst %.cpp,%.o, $(wildcard *.cpp))
Здесь описано как добавить к проекту исходные файлы на C:
OBJECTS+=$(patsubst %.c,%.o, $(wildcard *.c))
LDFLAGS — флаги для линковщика (пока оставляем пустыми):
LDFLAGS=
INCLUDE — пути к заголовочным файлам, включаемым в код нашего проекта (здесь оставляем пустыми, то есть будут использованы системные):
INCLUDE=
Далее идут правила. Вся система make построена на так называемых «правилах». Правило — это основной параметр, передаваемый команде make. В описании правила, за его именем указывают зависимости, необходимые для его выполнения. Зависимости — это файлы (имена файлов). После, на новой строке, идут команды, которые будут выполняться при вызове этого правила — строки с командами. Они должны начинаться с символа табуляции. Правило может не иметь команд, правило может не иметь зависимостей. Улитка перед командой является указанием не выводить команду (в противном случае make перед выполнением команды выведет её на консоль).
all — это общепринятое название правила, выполняющего сборку проекта. В нашем случае all завязан на значение имени исполняемого файла — EXECUTABLE, а оно в свою очередь является правилом, описанным ниже. Так же можно видеть, что само по себе правило all не имеет «тела» (команд для выполнения).
all: $(EXECUTABLE)
Правило с именем исполняемого файла, получаемого из значения переменной, выглядит следующим образом:
$(EXECUTABLE): $(OBJECTS)
        $(CXX) $(CXXFLAGS) -o $@ $^ $(INCLUDE) $(LDFLAGS)
Здесь написано следующее — для создания файла, записанного в значении переменной EXECUTABLE, нужны объектные файлы (записанные в созданной нами ранее переменной-списке OBJECTS). И описано само правило, являющееся, если присмотреться, командой линковки, в нашем случае раскрывающееся в следующую команду: g++ -Wall -o simple main.o module.o extern.o Что означает: слинковать перечисленные объектные файлы (main.o module.o extern.o) в исполняемый файл simple. Откуда берутся имена объектных файлов мы помним — выше описана команда создания списка путём замены расширения .c и .cpp на .o и записи их в переменную OBJECTS. Но вы можете спросить — а где команда (или правило) для построения компиляции) исходных файлов в объектные? А его нет. Дело в том, что в системе make есть набор стандартных неявно описанных правил. Все они описаны здесь: GNU Make Catalogue of Built-In Rules
Одно из стандартных правил make выглядит примерно так:
# .o: .c
#         $(CC) $(CPPFLAGS) $(CFLAGS) -c
Что означает скомпилировать (без линковки) все .c-файлы. Компиляция без линковки выдаёт объектный файл. Вот так получаются сами объектные файлы необходимые для линковки в исполняемый файл. Автоматизация make доходит до того, что можно дать команду:
make имя файла с исходным кодом даже без расширения и даже не имея Makefile! make main По этой команде make попытается скомпилировать main.c (в случае его отсутствия make попытается сделать то же с файлом main.cpp) в объектный файл main.o, затем слинковать его в исполняемый файл main. Неявное правило можно переписать. Например, правило, описанное ниже приведёт к ошибке сборки всего проекта. Оно гласит: для сборки объектного файла extern.o нужен файл с исходным кодом extern.c. Но само по себе не создаёт объектного файла нужного для сборки нашего проекта. Что приведёт к тому, что проект никогда не будет собран. Можете раскомментировать это правило сделать make clean all и посмотреть что будет.
#extern.o: extern.c
#        echo Rule to never make this project!
clean — правило для очистки. Сюда вписываем всё, что мы хотим делать при очистке проекта. В нашем случае мы удаляем объектные файлы, исполняемый файл и временные файлы. Это правило, в том виде, в котором оно описано, удалит только те объектные файлы, которые относятся к нашему проекту — то есть имеющие соответствующие файлы с исходным кодом. Вы можете проверить это, удалив какой-нибудь .с/.cpp-файл и выполнив команду make clean — вы увидите, что объектный файл соответствующий удалённому останется. Можно поставить rm *.o, но в приличных и крупных проектах принято удалять только своё. А вдруг вы (или пользователь вашего проекта) держите ещё какие-то файлы в каталоге с вашим Makefile (например, объектные файлы, которые вы получаете без исходного кода)?
clean:
        rm -rf $(OBJECTS) $(EXECUTABLE) *~
.PHONY — Это список имён правил, которые не являются файлами. Создаётся во избежание конфликтов между названиями этих правил и именами файлов. Если не указать clean и all в списке .PHONY, make запутается, потому что по умолчанию правила clean и all будут связаны с файлами с такими именами и make будет запускать их только тогда, когда файл не будет обновлен в отношении его зависимостей. Можно создать файл clean и попробовать сделать

make clean

с закомментированной строкой .PHONY, результат будет такой:

make: 'clean' is up to date.

То есть make увидит, что файл clean уже существует и не станет выполнять это правило вообще.
Список .PHONY может быть использован для форсированной компиляции каких либо файлов, вне зависимости от их состояния. Например форсированная перестройка модуля или всего проекта. Вообще считается хорошим тоном вписывать в список .PHONY все правила, не создающие файлов — clean, all, install, test, и пр.
.PHONY: clean all
Возможно вы обратили внимание, что выше мы использовали такие переменные как $@ и $^. Это название правила и список зависимостей соответственно. Ниже написано тестовое правило, которое выводит значение этих переменных. Вы можете поэкспериментировать — поменять его название и список зависимостей. И вопрос на внимательность — как сделать так, чтобы это правило выполнялось бесконечное количество раз?
test: Makefile main.cpp
        @echo "@ = " $@
        @echo "^ = " $^
        @touch $@

Для тех, кто впервые читает или встречается с GNU Make пару слов об этой системе. GNU Make — система сборки (компиляции) программного обеспечения. Работа с ней выглядит следующим образом — (вами или не вами) пишутся правила, затем выполняется команда формата

make -f Makefile -jJobs rule_1 ... rule_n 

где:
Makefile — имя Makefile'а из которого брать правила. Эту опцию можно опустить. По-умолчанию GNU Make будет искать Makefile'ы в текущем каталоге в следующем порядке: GNUmakefile, makefile и Makefile. Рекомендуемым авторами системы является имя — Makefile.
Jobs — количество потоков выполнения. Make умеет распараллеливать работу. Обычно эту цифру ставят равной количеству процессоров либо количеству процессоров умноженному на два. Если указать ключ -j без параметра, Make сам рассчитает оптимальное количество потоков для выполнения. Здесь стоит отметить два момента. Первое — этот ключ имеет смысл только для достаточно больших проектов. Второе — задавать слишком большое значение опасно — Make никак не проверяет производительность машины, на которой запущен и её способность вытянуть заданное количество «работ». В итоге можно перегрузить машину так, что вы даже нажав Ctrl-C будете ждать пол часа. Я попадал в такую ситуацию. Ещё бывает так что большие проекты не собираются в параллельном режиме — будет ошибка компиляции, которую можно обойти запустив make ещё раз, но без многопоточной сборки. Иногда можно «пройти» это место, остановить сборку и опять запустить в многопоточном режиме. Бывает такое, как я понимаю, из-за неправильно составленных Makefile'ов — получается так, что часть кода, требующая неких объектных файлов, уже собрана, а объектные файлы ещё нет. Своеобразный race condition получается.
rule_1 ... rule_n — список правил для выполнения. Да, правила можно передавать Make по несколько штук — они будут выполняться в очерёдности, в которой были заданы пользователем. Если этот параметр опустить — Make выполнит правило, описанное первым в Makefile.
Изначально GNU Make создавался как бы «в паре» с (или «под»GCC — отсюда такая простота и прозрачность в работе Make именно с процессом сборки программ при помощи этой коллекции компиляторов. Но несмотря на это, можно написать абсолютно любые правила в Makefile'е — настройка сети, запуск/остановка сервисов и пр.

На этом о make пока всё. К более сложным аспектам работы с этой системой мы ещё вернёмся позже.
В следующей статье мы рассмотрим как смешивать C и C++ код в одном проекте (в этом Makefile мы уже затронули это с точки зрения сборки), как происходит построение программы — как компилятор и линковщик создают исполняемый файл и при чём здесь ассемблер?