Pull to refresh

Операционная система на PHP: пишем драйвер файловой системы fat32, часть 1


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

Вступление



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

image

Выходов несколько. Можно написать «киллер-апп» (http://en.wikipedia.org/wiki/Killer_application), которое будет настолько хорошим и уникальным, что только ради него пользователи будут переходить на новую ОС. Такие примеры были неоднократно, к примеру, с использованием Linux — появлялись совершенно страшные статьи из серии «сейчас мы все отформатируем, ничего из данных не останется, будем вводить команды в эту страшную консольку». Это создает скорее негативный имидж для потенциальных пользователей.

Можно попробовать сделать совместимость с имеющимися ОС, дабы была возможность запускать уже привычные приложения. Но… Так тоже уже делали, к примеру в OS/2, как результат — отсутствие нативного софта, преимуществ использования именно этой платформы небыло, использовали кривую windows, которая в те времена обладала своими особенностями. Нечто похожее происходит и сейчас в OS GNU/Linux, в которой есть «не эмулятор» wine — масса негативных отзывов и мнение новичка формируется из ошибок поддержки неродного софта.

image

Можно прикрутить к нашей платформе удобный биллинг и делиться деньгами с разработчиками. Идея не нова, софта действительно настрогают очень много, однако… Если залезть в маркеты и поискать игры… Мы найдем 100 товердефенсов и 1000 тетрисов. Да, количественно софта очень много, но на практике уникального — крупицы в море bloatware, показывающего рекламу и имеющего функционал поделок на уроках информатики. Можно ли прославиться, если под твою платформу написали кучу ненужного софта и каждая софтина клянчит деньги?

image

А может быть и не нужно ничего делать? Может быть просто сделать качественную платформу, под которую будет удобно и легко программировать? Платформу, которую сможет использовать каждый и которая будет нетребовательна к ресурсам? К примеру, к таким платформам как wordpress, joomla, phpbb написаны тысячи плагинов, как бесплатных, так и коммерческих. Да, это не ОС, но это самостоятельная платформа. А что такое программа, если не плагин к операционной системе?

Мотивация.



На сегодняшний день на php написано очень много прикладного программного обеспечения для удовлетворения практически любой потребности. Одних плагинов к вордпрессу уже почти 25000: wordpress.org/extend/plugins — удостоверьтесь сами. Ни один текстовый редактор не сможет похвалиться таким количеством функционала. На php были переписаны все загрузочные скрипты для GNU/Linux и все это использовалось в проекте РОД Linux — дистрибутив основанный на веб-технологиях: www.linux.org.ru/news/linux-general/1325513/page4 — показывает, как php может быть опорой при создании ОС.

image

А почему бы не сделать платформу, которая была бы написана полностью на php? Это нивелирует неудобства уже существующих платформ и сделает разработку новых приложений на PHP еще удобнее, еще более оптимальной. За многие годы php доказало, что является серьезным языком программирования, на котором можно решать практически любые задачи. Открытость платформы — тоже очень важный фактор, который позитивно влияет на разработку. Взлет количества приложений под GNU/Linux по сравнению с закрытыми платформами легко объясним — программистам больше не надо месяцами ждать ответов от поставщиков закрытых решений, можно сразу посмотреть в исходники и решить задачу за несколько часов. Лично я часто заглядываю в исходники платформ, под которые пишу, это позволяет мне не только решать задачи, но и решать их наиболее оптимальным способом.

Современные ОС написаны на неуправляемых языках, где возможны утечки памяти и переполнения. Это путь к уязвимостям. Если же писать ОС на управляемом языке, то проблемы с повреждением памяти уйдут в прошлое, как страшный сон. Будет не только достигнут невиданный ранее уровень безопасности, но и появлятся расширенные средства для управления всем процессом работы приложения. К примеру, открываются новые горизонты для многопользовательских приложений, событийного программирования, масштабируемости и виртуализации. Можно смело сказать, что это обеспечит качественный скачек в области программирования и компьютерных наук.

Интерфейсы:


image
Операционная система предоставляет всем своим приложениям какой-то определенный набор API для работы со всеми своими функциями. Это работа с окнами, устройствами ввода, не исключение и файловая система. Обычно, говоря о работе с файловой системой, у нас есть:
  1. Открытие файла
  2. Закрытие файла
  3. Чтение данных
  4. Запись данных
  5. Обрезание файла

К примеру, если мы хотим вывести файл на экран, то нам сначала надо открыть файл, потом его весь прочитать, потом закрыть, дабы избежать утечек памяти. Честно говоря, мне давно надоело писать море бойлерплейта для таких простых целей. Другой пример — я считал файл, нашел какие-то нужные данные внутри, изменил их… А как мне его сохранить? Максимум что я могу — сделать seek в середину, а далее я вынужден перезаписать всю оставшуюся часть файла. Если данных записал меньше чем было, то еще и хвост файла надо обрезать. А дописать в конец — всегда пожалуйста. Я уже не говорю о специальных символах, которые запрещены в именах файлов на некоторых файловых системах. Все это — издержки интерфейса. Поэтому сразу говорю — не пугайтесь, когда я буду использовать свой, упрощенный интерфейс для работы с файлами.

В этой статье мы реализуем 2 системных вызова в будущей операционной системе:
  • readFile($path) — вернет файл целиком
  • readDir($path) — вернет список файлов как массив

Да, мне надоело писать кучу ненужного бойлерплейта, да и чтение файла/директории в 99.999% случаев нужно целиком, а современные системы это вполне позволяют. Будем смотреть в будущее, а не в прошлое.

Перейдем к делу



image
Довольно слов, займемся делом. В этой части я реализую файловую систему, но файловой системе как правило требуется доступ к диску. Поэтому, в качестве вступления, я реализую «заглушку», которая будет реализовывать интерфейс по чтению секторов. В дальнейшем коде заглушка будет переписана.

<?php

namespace PHPOS;

class Device{

        // Этот класс - временная затычка
        // Пока будем использовать функции работы с файлом.
        // В будущем здесь будет работа с устройством напрямую.

        private $device;
        const sectorSize=512;

        // Открываем устройство и передаем путь к открытию устройства/образа
        // Для Linux - это /dev/sda2 к примеру
        // Для Windows - это \\.\PhysicalDrive0 или \\.\C:
        public function open($path){
                $this->device=fopen($path,"rb+");
        }

        // Просто читаем 1 сектор
        public function readSector($num){
                fseek($this->device,$num*self::sectorSize,0);
                return(fread($this->device,self::sectorSize));
        }

        // Закрываем устройство
        public function close(){
                fclose($this->device);
        }
}


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

$sector=i32v(substr($mbr,512-2-4*16+$num*16+8,4));
Аналогично и с разбиением разделов GPT, только там немного другие смещения и 8 байт на смещение.

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


<?php

namespace PHPOS;

// Утилиты для работы с числами. Это просто для удобства
class NumTools{

        // Для чтения unsigned int в 32 бита
        public static function i32v($a){
                $a=unpack("I",$a);
                return($a[1]);
        }

        // Для чтения unsigned int в 16 бит
        public static function i16v($a){
                $a=unpack("S",$a);
                return($a[1]);
        }

        // Для чтения unsigned int в 8 бит
        public static function i8v($a){
                $a=unpack("C",$a);
                return($a[1]);
        }
}


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

Итак, все готово, полетели!

image

<?php

namespace PHPOS;
use PHPOS\NumTools;

class Fat32 {

        // Здесь мы определим некоторые константы, которые будут нужны нашему драйверу

        // Определение разделителя. Лично мне не нравится разделитель \, с ним куча проблем
        // Поэтому я решил использовать разделитель ": ", который можно сразу использовать
        // в интерфейсе пользователя
        const separator=": ";

        // Просто скучные константы, которые нужны для чтения директорий
        const dirRO=1;
        const dirHidden=2;
        const dirSystem=4;
        const dirVolume=8;
        const dirDirectory=16;
        const dirArchive=32;
        const dirLFN=0x0F;

        // Параметры файловой системы, мы их будем инициализировать в конструкторе
        public $clusterSize=0;
        public $dataSector=0;
        public $fatSector=0;
        private $device;
        private $root=0;
        private $paths=array();

        // Конструтор класса, которому передаем объект устройства.
        // Здесь же мы инициализируем основные параметры раздела.
        function __construct($dev){
                $this->device=$dev;
                $data=$this->device->readSector(0);
                $this->clusterSize=NumTools::i8v(substr($data,0x0d,1));
                $this->fatSector=NumTools::i16v(substr($data,0x0e,2));
                $this->root=NumTools::i32v(substr($data,0x2c,4));
                $this->dataSector=$this->fatSector+NumTools::i32v(substr($data,0x24,4))*NumTools::i8v(substr($data,0x10,1));
        }

        // Читаем отдельный кластер
        public function readCluster($num){
                $data="";
                for($q=0;$q<$this->clusterSize;$q++){
                        $data.=$this->device->readSector($this->dataSector+($num-2)*$this->clusterSize+$q);
                }
                return($data);
        }

        // Читаем цепочку кластеров
        public function readChain($cluster){
                $data="";
                do{
                        $data.=$this->readCluster($cluster);
                        $fatSector=intval($this->fatSector+$cluster*4/512);
                        $fat=$this->device->readSector($fatSector);
                        $cluster=NumTools::i32v(substr($fat,$cluster*4%512));
                }while($cluster>0 && ($cluster & 0xFFFFFFF)<0xFFFFFF8);
                return($data);
        }

        // Ищем файл (директория - тоже файл)
        private function findFile($path){

                // Оптимизации для highload
                if(array_key_exists($path,$this->paths)){
                        return($this->paths[$path]);
                }

                $parts=explode($path,self::separator);
                $path="";

                // VFS-лайк:
                foreach($parts as $part){
                        $curPath=$this->readDir($path);
                        $nextPath=$path.(strlen($path)>0?self::separator:"").$part;
                        if(in_array($nextPath,$curPath)){
                                $path=$nextPath;
                        } else {
                                // not found
                                return(-1);
                        }
                }
                return($this->paths[$path]);
        }

        // Читаем директории
        public function readDir($parent){
                $res=array();
                if($parent===""){
                        $dir=$this->readChain($this->root);
                } else {
                        $inode=$this->findFile($parent);
                        if(is_array($inode)){
                                $dir=$this->readChain($inode[1]);
                                $parent.=self::separator;
                        } else {
                                return($res);
                        }
                }
                $LFN="";
                while(strlen($dir)>=32){
                        $flags=NumTools::i8v(substr($dir,11,1));
                        if($flags == self::dirLFN){
                                $name=substr($dir,1,10).substr($dir,0x0e,12).substr($dir,0x1c,4);
                                for($q=0;$q<strlen($name);$q+=2){
                                        if(substr($name,$q,2) === "\x00\x00"){
                                                $name=substr($name,0,$q);
                                                break;
                                        }
                                }
                                $LFN=iconv('UCS-2LE', 'UTF-8',$name).$LFN;
                        } else {
                                $name=strlen($LFN)>0?$LFN:$this->trimNull(substr($dir,0,11));
                                $cluster=NumTools::i32v(substr($dir,26,2).substr($dir,20,2));
                                $size=NumTools::i32v(substr($dir,28,4));
                                $isDir=$flags & self::dirDirectory?1:0;
                                if($flags==0 && $cluster<2){
                                        break;
                                }
                                array_push($res,$parent.$name);
                                $this->paths[$parent.$name]=array($isDir,$cluster,$size);
                                $LFN="";
                        }
                        $dir=substr($dir,32);
                }
                return($res);
        }

        // Читаем файл по координатам
        private function readFileData($cluster,$size){
                $file=$this->readChain($cluster);
                return(substr($file,0,$size));
        }

        // Читаем файл по пути
        public function readFile($path){
                $inode=$this->findFile($path);
                if(is_array($inode)){
                        return($this->readFileData($inode[1],$inode[2]));
                }
                return(null);
        }

        // Утилитка для обрезания null-символов
        private function trimNull($s){
                while(substr($s,-1)==="\x00"){
                        $s=substr($s,0,-1);
                }
                return($s);
        }
}


Вот собственно и все. Основные вещи я прокомментировал в коде, думаю все понятно. Если я что-то забыл откомментировать, то пишите в комментариях, я с большим удовольствием отвечу.

Сразу замечу, что это не весь драйвер — эта версия только читает файлы, поддержку записи и утилит для файловой системы мы реализуем позже.

Практическое применение.



image
Вам знакома ситуация, когда Firefox начинает дичайше тормозить, пытаясь реорганизовать свой кеш? А что будет, если кеш копился месяц, а потом Firefox (или вы сами) захотите его удалить? Лично у меня это выглядит как дичайший скрежет винта, который напоминает индексирование всего диска. Вообще, он меня сильно пугал: сидишь себе в интернете, никого не трогаешь, и вдруг диск начинает шуметь так, словно пробрался какой-то злой вирус и удаляет все-все файлы. Лично меня ПУГАЕТ такое поведение. И дело даже не в sqlite. Когда я открывал и закрывал Firefox несколько раз, в попытках окончательно удалить кеш, я все равно не мог избавиться от шуршания диска. Плюнув на все, я пошел в директорию с профайлом, в надежде вычистить его руками. Какого же было мое удивление, что удаление файлов через файловый менеджер заняло у меня 15 минут (!), при этом диск дичайше скрипел, имитируя работу вирусов. И это на новеньком компьютере со скоростным винтом и 16 гигабайтами оперативки. Дело не в том, что в кеше было очень много мелких файлов, а в том, что было достаточно записать несколько секторов, а остальные отметить свободными. Но драйвер файловой системы поступал последовательно, удаляя файл за файлом, тем самым получилась такая картина. Я был вынужден отключить кеш в Firefox, иначе им невозможно пользоваться.

image
Другая картина наверняка будет знакома веб-разработчикам: пользователи заливают свои картинки, от картинок мы генерируем превьюшки, иногда достаточно мелкие, дабы занимали они 1-2 кб. И такими картинками забиты целые диски. Какого же бывает удивление разработчиков, когда картинок всего 200 гигабайт, а терабайтный диск оказывается полностью заполнен. Судорожные попытки проверить целостность файловой системы него не дают. Куда подевалось все место? Может производитель диска в очередной раз всех обманул, только теперь у него мегабайт — это 200000 байт? А причина в том, что обычно (в повседневной жизни) файлы используются несколько бОльшего объема, разработчики под них и подстроили все параметры, поэтому файловая система оказывается просто неоптимизированной для мелких файлов. Знакомо, правда?

image
Еще одна неприятность, которая может ждать вас как на домашнем компьютере, так и на сервере: ограничение на размер максимального пути, проблемы доступа к большим директориям. Далеко ходить в общем-то и не надо: habrahabr.ru/post/152193 — множество файлов парализовали работу сервера. Или, вспоминается другая история: человек поднял у себя FTP с гостевым доступом, а потом обнаружил у себя на диске набор директорий вида:
▒䧋䟚憌徧綘栕䩂ჯ簟᥋鹬롂༉᧥釭뮄욣Ψ᛺፟ᚐᙺ৳ටඤݟᣀтϦ໻Ѐ▒̿ኤӢʼł*ĽɈ▒ɾĿȏȭ▒
Нет, это небыло повреждение диска или логической целостности, так как это безобразие происходило строго внутри директории FTP-сервера, однако такие директории человек ничем не мог удалить. Позже оказалось, что есть даже специальная программа, автор которой гордится тем, что она умеет создавать на FTP неудаляемые директории и лекарства от нее не существует.

Продолжать этот список можно бесконечно, но сегодня речь о моем драйвере на PHP, который может разрешить все эти проблемы. Нет, это не статья о том, как удалить миллионы файлов, такое уже есть: habrahabr.ru/post/157613 — в отличии от этой статьи, мы не будем довольствоваться крохами существующих решений, а сами возьмем управление в свои руки.

Тестирование


image
Пришло время посмотреть, чего же у нас получилось. Но для начала надо подготовить тестовое окружение. Для начала создадим файл весом в 100 мегабайт — это будет образ нашей файловой системы:
dd if=/dev/zero of=fatImage bs=1M count=100

Затем сделаем его устройством и отформатируем:
 losetup /dev/loop0 fatImage
mkfs.vfat -s1 -F 32 -f2 -I -ntest /dev/loop0

К сожалению, мне не удалось отформатировать раздел, без вывода сообщения:
Loop device does not match a floppy size, using default hd params
Но это не проблема, мы можем самостоятельно написать утилиту для форматирования раздела, правда сделаем это чуть позже, в следующих частях этой статьи, а пока будем довольствоваться тем что имеем.

Смонтируем наш раздел
mount /dev/loop0 /mnt/


Собственно, раздел присоединен к нашей основной системе.
Копируем файлы, создаем директории. В общем, заполняем раздел данными.
Я создал директорию и пару файлов с длинными кириллическими файлами (для теста)

Осталось демонтировать нашу файловую систему и отсоединить loop:
umount /dev/loop0
losetup -d /dev/loop0


Вы готовы стать маленькими детьми, которые набрасываются на вкуснятинку?
image


// Создаем устройство и подключаем наш образ
$dd=new Device();
$dd->open("fatImage");

// Создаем экземпляр драйвера с нашим устройством
$ff=new Fat32($dd);

// Читаем корень и дампим
$myroot=$ff->readDir("");
var_dump($myroot);

// Читаем файл и дампим
$txt=$ff->readFile("Новый текстовый документ.txt");
var_dump($txt);

// Читаем директорию и дампим
$mydir=$ff->readDir("Новая папка");
var_dump($mydir);

// Читаем файл в директории
$mybinary=$ff->readFile("Новая папка: captcho.gif");

// файл бинарный, поэтому записываем его
$testFile=fopen("test.gif","wb");
fwrite($testFile,$mybinary);
fclose($testFile);


Результат:
array(3) {
  [0]=>
  string(11) "test       "
  [1]=>
  string(21) "Новая папка"
  [2]=>
  string(50) "Новый текстовый документ.txt"
}
string(8) "justtext"
array(3) {
  [0]=>
  string(34) "Новая папка: .          "
  [1]=>
  string(34) "Новая папка: ..         "
  [2]=>
  string(34) "Новая папка: captcho.gif"
}


Бинарное сравнение файлов:
md5sum test.gif
76bef45f6572d2931b4792a8d24739c6  test.gif
md5sum test/Новая\ папка/captcho.gif
76bef45f6572d2931b4792a8d24739c6  test/Новая папка/captcho.gif


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

image

Приношу извинения, что все сразу не написал, но разработка требует много времени и сил, как только я допишу драйвер — сразу же будет продолжение. В следующих частях: поддержка записи, утилиты для файловой системы, драйвер для видеоадаптера, создание графического тулкита, побег из браузера — вытаскиваем приложения на десктоп, работа с системами ввода. Это краткий обзор того, что я уже запланировал. Как видите, писать операционные системы — это весело. И хоть тут и не любят попрошаек, я прошу прокомментировать проект и быть может присоединиться к нему, ведь программировать вместе всегда интереснее.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.