❯ Глоссарий

  1. Программа – текстовый файл, который содержит в себе код на каком-либо языке программирования;

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

  3. Ядро – программа, лежащая в основе операционной системы, написанная на системном языке (например на C);

  4. Операционная система – ядро и стандартные пользовательские приложения;

  5. Модуль ядра – программа, которая динамически подгружается в ядро для расширения его функционала. Модуль может быть драйвером, системным вызовом или какой-либо произвольной подсистемой;

  6. Драйвер – программа, которая абстрагирует прикладного программиста/программу от низкоуровневого взаимодействия с железом и предоставляет удобный интерфейс взаимодействия с ним.

❯ Введение: что будет в статье?

Думаю, многие слышали, что в Linux «Всё есть файл». Когда я впервые услышал это от моего друга, я подумал, что он просто издевается, называя вещи не своими именами, но в реальности всё оказалось иначе. Хоть эта концепция и может показаться тривиальной для опытных людей, новичков она вводит в ступор. Да что там новичков, даже более менее опытных программистов, так как весь этот слой абстракций скрывается от них под библиотеками языков программирования, что являются ещё более высоким уровнем абстракции, и они просто о нём не задумываются.

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

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

Но всё же, так как темой данной статьи являются обширные возможности ядра и файловой системы Linux, некоторые моменты о процессах (запущенных программах) и ядре операционной системы не будут разбираться на пальцах. Но это не повод для грусти, в моей предыдущей статье мы как раз разбирали эти вещи! Если вы пока не до конца понимаете что такое процесс и каким образом операционная система осуществляет его запуск, а слово ядро наводит на вас ужас, то очень советую прочитать мою предыдущую статью.

Что будет разобрано в этой статье:

  1. Что из себя представляют современные жёсткие диски (SSD) на физическом уровне;

  2. Сколько статуй свободы понадобится, чтобы записать 1 триллион байт;

  3. Каким образом на диске хранится файловая система и как компьютер в ней разбирается;

  4. Поймём, что системные вызовы – это всё, что у нас есть (больше реально ничего нет);

  5. Разберём на пальцах модули ядра и драйверы и даже напишем простой модуль ядра Linux!;

  6. Поймём, какие фундаментальные идеи стоят за концепцией «Всё есть файл» и как это реализуются программно. Приведём простые примеры;

  7. Удивимся тому, что процессы – это файл;

  8. Удивимся тому, что мышь и тачпад – это файл;

  9. Удивимся тому, что интернет соединение – это файл.

Итак, начнём мы как всегда с основ, которые стоит понять (прочувствовать), прежде чем мы перейдём к основной теме, поехали!

❯ Основы, которые стоит понять

❯ Жёсткий диск, информация на нём

Ни для кого не секрет, что наши фото, видео и всевозможные программы хранятся на жёстком диске. Нам сейчас неважно, крутится ли там под корпусом диск под считывающей головкой (HDD) или изменяется уровень заряда транзистора c плавающим затвором (SSD). Для нас важно знать лишь одно – это чудо техники умеет записывать и хранить информацию в течение продолжительного промежутка времени. И нам стоит воспринимать диск как обычный массив байт. Для меня было настоящим открытием, с немалым удивлением, когда я осознал, что сохранив фотографию на ноутбук и положив его после этого в пыльный угол или под кровать, спустя, скажем, 10 лет, я всё ещё смогу его включить и посмотреть на эту фотографию.

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

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

PS: В списке литературы будет отличное видео на тему устройства SSD

Жёсткий диск (SSD) состоит из миллиардов или триллионов транзисторов. Например, диск на 1 Терабайт (1000 Гигабайт) содержит около 3-х триллионов транзисторов. Но не будем развивать эту тему дальше, просто нужно прочувствовать, насколько искусно человечество подчинило себе электромагнитные явления. И что пока ваш ноутбук пылится под кроватью вместе с мухами и пауками, ваши данные, будь это фотографии, видео, игры, ваш прогресс и ачивки в играх – хранятся в виде электрического заряда на триллионах транзисторов и помещаются в какой-то ничтожный по размерам кусок пространства (диск).

Приведём последний пример на эту тему. Предположим, у нас есть диск на 1 Терабайт и мы полностью забиваем его произвольным текстом. 1 Терабайт – это 1 Триллион байт. 1 символ в кодировке ASCII занимает 1 байт. Получается, у нас получится записать 1 триллион символов. На стандартной странице офисной бумаги формат A4 со стандартным шрифтом 12-14 пунктов помещается примерно от 1500 до 3000 символов. Возьмём за эталон 2000 символов (хотя это число может сильно варьироваться от шрифта и от прочих аспектов оформления текста).

Получается, если мы захотим записать триллион символов (букв) на бумагу, то нам понадобится 500 миллионов листов (1 000 000 000 000 символов / 2 000 символов = 500 000 000 листов). Стандартный лист А4 весит 5 граммов, 500 миллионов листов весят 2 500 000 000 грамм, что есть 2500000 кг, что есть 2500 тонн.

Что такое 2500 тонн? Например, это 12 c половиной статуй свободы, 166 ваших дачных домов (если принять дом за 15 тонн) или пару тысяч легковых машин. Жёсткий диск на 1 Терабайт весит примерно 100 грамм, хотя может и меньше, что уже в 25 миллионов раз меньше, чем объём бумаги, который потребовался бы для записи той же информации.

❯ Файловая система

Разумеется, мы можем просто так забить диск байтами (символами/буквам в кодировке ASCII), но в контексте современных компьютеров это не будет иметь никакого смысла. Диск должен иметь некую разметку, инструкцию/карту для компьютера, чтобы последний мог ориентироваться в данных, записанных на нём. Ровно также и с этой статьёй, если я уберу заголовки, отступы, картинки и абзацы – читать будет совершенно неинтересно и сложно. Да, аналогия не полная, но всё же.

При должном желании человек может прочитать полностью неоформленный текст, а современный компьютер не размеченный диск не сможет. Нет, разумеется, мы можем создать/запрограммировать такой компьютер, который будет просто читать наш 1 Терабайт с диска побайтово и всё, но людям, бизнесу и т.д. – нужна универсальность. Поэтому возможности чтения компьютером диска не ограничиваются на ситуации выше. Нужен способ читать совершенно разные данные и делать это универсально, поэтому «голый» диск (массив нулей, грубо говоря, если принять диск за массив байт) компьютер не распознает, ему нужна разметка, некая системная предзаписанная информация, на которую
он может опереться).

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

Наш компьютер не адресует отдельные байты накопителя информации(диска), это было бы слишком накладно и неудобно, вместо этого он адресует диск блоками, например по 512 байт для MBR разметки. В одном или нескольких блоках (на картинке обозначен как «Blocks Library») хранится информация о том, в каких блоках какие файлы лежат. Вот и всё, это и есть разметка диска. Разумеется, в реальности не всё так тривиально, существует великое множество способов организовать информацию на диске под разные потребности и задачи. Нам же удобно воспринимать диск как массив этих самых блоков, или же просто байт (ведь это абстракция, так проще) и понимать, что на диске перед началом его использования (в стандартном понимании) должна быть некая разметка.

По такой схеме и хранятся наши файлы (фотографии, игры и т.д.), разные части файлов лежат в разных блоках. Учёт обычных блоков с пользовательскими данными и доступ к специальным блокам с информацией об обычных блоках ведёт операционная система, а точнее – её ядро. Со стороны обычного пользователя, мы можем получить доступ к этим файлам через командную строку или GUI файловые менеджеры, например через nautilus в Linux или через всем знакомый проводник в Windows :) Ядро операционной системы просто прочитает свои специальные блоки, поймёт в каких блоках лежит наш файл, соберёт его по частям и «отдаст нам».

Но не все файлы в Linux, к которым мы можем получить доступ, хранятся физически (то есть в виде уровня заряда на транзисторах) на жёстком диске. Некоторые существуют только во время работы компьютера, а значит логично предположить, что хранятся они в оперативной памяти (ОЗУ), которая хранит информацию только тогда, когда компьютер включён, она энергозависима. В отличие от SSD, который может годами хранить информацию без внешнего источника питания.

Эти файлы являются просто ресурсами ядра операционной системы (не пугайтесь, мы во всём разберёмся далее), которые были «отображены» в файловую систему, дабы предоставить удобный интерфейс взаимодействия с ними. В этом мы и разберёмся далее в статье.

❯ Системные вызовы это всё, что у нас есть

Так как все пользовательские процессы выполняются в неком окружении, которое мы называем операционной системой, то и доступ ко всем физическим (реальные устройства) и логическим (ресурсы ОС: файловая система, информация о процессах и т.д.) ресурсам предоставляются процессам ядром операционной системы.

Предоставление этих ресурсов реализуется механизмом системных вызовов, вот их полный список, на момент написания статьи их 467 штук. Самые простые их них, например, sys_exit, который компилятор вставляет в конец программ, когда вы пишите return 0; в конце функции main:

#include <stdlib.h>

int main() {
	exit(0); // return 0;
}

Или же системный вызов sys_write, который вызывается внутри функции printf в С, да и вообще внутри любой функции print в языках программирования(и не только print, но об этом позже):

#include <stdio.h>
#include <stdlib.h>

int main() {
	printf("Hello, World!\n");
	exit(0); // return 0;
}

printf является частью стандартной библиотеки C - libc и нужен для форматированного вывода (сокращение от print format) для удобства программистов, но мы можем воспользоваться и более низкоуровневой функцией write, внутри printf именно она и вызывается (на эту тему есть отличная статья на Хабре):

#include <stdio.h>
#include <unistd.h>

char *string = "Hello, World!\n";

int main() {
	write(STDOUT_FILENO, string, 14);
	return 0; // exit(0);
}

Первым аргументом мы передаём дескриптор файла, в который мы хотим осуществить запись, вторым указатель на начало строки и третьим длину строки. В нашем случае мы хотим записать в терминал, поэтому дескриптор файла в первом аргументе функции write будет 1 – дескриптор стандартного потока вывода (не пугайтесь, скоро мы во всём разберёмся). Да, с записью в терминал мы взаимодействуем как и с записью в файл, к концу статьи мы постараемся понять почему это так зачем это нужно, а пока просто возьмём на веру.

STDOUT_FILENO является константой, которая определена в стандартной библиотеке в unistd.h и равна эта константа 1:

/* Standard file descriptors. */
#define STDIN_FILENO  0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */

Является ли функция write системным вызовом? Нет. Функция write, как ни странно, является просто функцией. Это обёртка над системным вызовом, написанная в стандартной библиотеке языка C.

Тут мы скажем лишь то, что системные вызовы выполняются в неком привилегированном режиме, из которого можно получить доступ к ресурсам системы. В этом режиме и работает ядро операционной системы. Когда пользовательская программа вызывает системный вызов, то говорят, что программа перешла из пользовательского пространства в пространство ядра, что и видно на картинке выше. Это значит, что теперь программа работает в адресом пространстве ядра, то есть с конкретным местом в ОЗУ (виртуальной памяти), в котором располагаются код и данные ядра. Доступ к этим данным возможен только в режиме ядра, это гарантируется аппаратно! Подробнее про системные вызовы, их использование и виртуальную память в моей предыдущей статье.

Из этого можно сделать очень важный вывод: всё, что есть в распоряжении у программ – это системные вызовы. Все программы сводятся к вызовам системных вызовов, ничего другого просто не существует. Всё остальное – это прикладные обёртки, например функции в библиотеке C libc или стандартные функции в любом другом языке программирования. Без системных вызовов программы бы не могли выполнять никакой полезной нагрузки и им было бы доступно ровным счётом ничего. Без них процессы смогли бы выполнять лишь самые тривиальные математические операции и некоторые другие вещи. Именно поэтому когда-то давно и решили написать операционную систему, для удобства!

Поэтому, как мы упоминали выше, у нас лишь 467 (на момент написания статьи количество такое) разных системных вызова и на их основе и строятся всё колоссальные множество стандартных функций, все программы, игры и т.д. Они являются строительными блоками абсолютно для всего, что вы видели, видите и сможете увидеть на экране вашего компьютера.

Всё это придумано для безопасности, так как было бы очень опрометчиво давать пользовательским программам доступ к ресурсам ядра операционной системы. Программы могли бы случайно или специально вывести систему из строя. Например, стереть весь жёсткий диск – всё, теперь у вас в руках просто кусок кремния с платковыми/железным корпусом :) Ведь в таком случае не поможет даже перезагрузка, ведь была не просто нарушена работа ОС (что-то перезаписали в ОЗУ), ОС была и вовсе стёрта, как и вся разметка на диске!

❯ Модули ядра и драйверы

И последняя вещь, которую мы разберём, прежде чем перейти к самому интересному!

Ядро Linux является монолитным, то есть все компоненты ядра (все его подсистемы) располагаются в едином пространстве ядра, в одном месте в памяти (мы уже упоминали о пространстве ядра выше). И если «умирает» одна часть ядра, то умирает и вся система, это и есть монолитное ядро. Но хоть ядро Linux и является монолитным, оно всё же поддерживает загрузку модулей. Зачем?

Ну, например, мы хотим подключить к компьютеру новую модель Raspberry Pi или проcто какую-то диковинную железку. Разумеется, драйверов (в следующем абзаце разберёмся что это) под эту железку у нас изначально не будет. И если бы ядро не поддерживало бы динамическую загрузку новых модулей, нам бы пришлось писать драйвер для этого нового устройства, вставлять его код в код ядра и перекомпилировать ВСЁ ядро заново, это очень сложно, грустно и долго!

Именно поэтому ядро Linux поддерживает динамическую загрузку модулей, чтобы можно было добавлять к ядру новый функционал без перекомпиляции всего ядра и даже без перезагрузки системы!

Так что же такое драйвер и модуль, чем они отличаются и зачем нам это нужно?

Драйвер в общем смысле – это программа, которая взаимодействует с каким-либо физическим устройством и выступает между ��елезом и прикладными программами как посредник/переводчик, который переводит с языка «железного» на язык «программный». Короче, абстрагирует нас, как программистов, от низкоуровневого взаимодействия с устройствами и предоставляется удобный программный интерфейс взаимодействия с ними. Это классический пример, но это не всегда так. Драйвер не обязательно абстрагирует какое-либо физическое устройство, он может абстрагировать и какие-либо ресурсы операционной системы (посмотрим на это позже, в следующем практическом разделе).

Модуль ядра – это просто программа, которая является частью ядра, она может использоваться во многих разных целях. Например, модули нужны для создания новых системных вызовов, драйверов, сетевых подсистем и так далее. Но можно написать модуль, который просто будет выводит «Hello, World!» в логи ядра при его загрузке в ядро и Goodbye, World!" при его выгрузке из ядра . Прошу любить и жаловать:

Мы не будем разбирать тут этот код в деталях (что за printk – print kernel, странные сигнатуры функций и прочие вещи. всё это специфика разработки на стороне ядра), но в будущем я обязательно напишу серию статей, посвящённых написанию модулей, драйверов и системных вызовов в Linux.

Просто в общих чертах разберёмся что происходит на картинке выше:

  1. Был написан некий код, который ядро воспринимает как модуль. Да, это простой код на C, но он немного специфичен;

  2. Справа с помощью команды sudo dmesg -w мы вывели логи ядра;

  3. Далее модуль надо скомпилировать, процесс компиляции тут опущен, так как статья не об этом, но тут нет ничего сложного (ls выводит нам вспомогательные файлы, которые появляются после компиляции). Среди них есть hello.ko – это и есть исполняемый файл нашего модуля. Ровно такой же, как и обычные исполняемые файлы. Но, разумеется, с некоторыми оговорками, которые выходят за рамки данной статьи;

  4. С помощью команды sudo insmod hello.ko подключаем наш модуль к ядру, выполняется функция hello_init. Результат её работы мы видим в логах ядра в первой из выделенных строчек (самые нижние строчки в правом терминале);

  5. С помощью команды lsmod | grep hello проверяем, что наш модуль реально подключён к ядру. Просто lsmod выведет все модули в системе на данный момент;

  6. С помощью команды sudo rmmod hello выгружаем модуль из ядра, выполняется функция hello_exit, результат, опять же, видим в логах ядра;

  7. Теперь lsmod | grep hello ничего не выдаёт. Это значит, что модуль мы успешно выгрузили.

Только что с высоты птичьего полёта мы разобрали «Hello, World!» из мира ядерной разработки. На основе шаблона выше и пишутся драйверы, системные вызовы и прочие вещи.

Из этого можно сделать вывод, что каждый драйвер – это модуль ядра, но не каждый модуль ядра – это драйвер.

Все эти знания понадобятся нам для понимания концепции «Всё есть файл», давайте же перейдём к ней!

❯ Всё есть файл

❯ Что это такое в общих чертах

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

Например, для приложения нет особой разницы, пишет ли оно в файл на диске или в сетевое соединение. Что то, что то – представляется файлом, который поддерживает операции read, write, close и прочее. Для клиента, обратиться к серверу на другом конце планеты не сложнее, чем записать файл на диск. Всю «грязную» работу делает ядро операционной системы.

Мы(процесс) можем обратиться через read() к файлу на жёстком диске, к интернет соединению, к какому либо внутреннему буферу произвольного устройства (на картинке выше Arduino), к логическим ресурсам системы (их на картинке обозначает Такс, так зовут пингвина Linux).

Ко всему вышеперечисленному нужен драйвер! Откуда операционная система будет знать какая на диске разметка, какие именно параметры интернет соединения и по какому сетевому протоколу происходит передача данных, какого вида буфер на каком-либо устройстве и в каком формате он хранит данные, какие именно данные и в каком формате получать от операционной системы???

А ей и не надо знать всего этого! Под каждую из этих вещей пишется отдельный драйвер, который реализует стандартный набор функций для работы с файлом(open(), read(), write(), close() и т.д.), внутри которых уже и описывается специфика взаимодействия с конкретным объектом. То есть задача драйвера – подогнать взаимодействие с тем или иным объектом(устройством/ресурсом) под файловый интерфейс.

И правда, если с чтением и записью на диск всё понятно, то, например, о сетевом соединении нужно думать так: write() – отправляем данные по сети, read() – читаем что нам по сети прислали, close() – заканчиваем передачу данных по сети и т.д. Поздравляю, теперь сетевое соединение/взаимодействие – это файл!

Надеюсь, вам уже становится понятно как это работает, но давайте взглянем на то, как это реализовано в ядре и напишем свой небольшой пример. В ядре Linux есть структура под названием file_operations, её то драйвер и заполняет! Это похоже на реализацию интерфейса в других языках, в Си же это делает с помощью указателей на функции в структуре(выглядят как обычная сигнатура функции, только название берётся в скобки и перед ним ставится *):

#include <stdio.h>

struct Animal {
	void (*speak) (char *word);
};

void speak_cat(char *word) {
	printf("%s, Meow!\n", word);
}

void speak_dog(char *word) {
	printf("%s, Bark!\n", word);
}

int main() {
	struct Animal cat;
	cat.speak = speak_cat;
	
	struct Animal dog;
	dog.speak = speak_dog;

	cat.speak("Hello");
	dog.speak("Hello");

	return 0;
}

Вывод программы:

Hello, Meow!
Hello, Bark!

Ой, ООП на C?! По сути – да, но об этом в другой статье. Нас же сейчас интересует лишь то, что данный пример с кошечкой и собачкой полностью отражает концепцию «Всё есть файл». И правда, взгляните на картинку ниже, а потом на предыдущую ещё раз.

Да, они одинаковые, надеюсь вы перешли по ссылке выше в определение структуры file_operations(даю вам второй шанс) и увидели, что это одно и тоже.

Вместо животного в примере выше, мы создаём драйвер, скажем my_driver. Далее пишем функции my_driver_open(), my_driver_read(), my_driver_write(), my_driver_close() специфичные для устройства/ресурса, которое/который абстрагирует наш драйвер. Заполняем указатели на функции open(), read(), write(), close() в экземпляре структуры file_operations нашего драйвера соответственно.

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

Теперь, когда мы на самом деле поняли природу этой абстракции, рассмотрим самые популярные лже-файлы, под маской которых скрываются совсем не файлы в стандартном понимании этого слова.

❯ Смотрим на /proc

Директория /proc или procfs(файловая система процессов) монтируется(подключается) к нашей файловой системе в момент запуска компьютера. Данные из папки /proc, как уже было сказано много раз ранее, не содержатся на жёстком диске, они содержатся в ОЗУ!

procfs содержит исчерпывающую информацию о процессах и некоторых других ресурсах системы.

Рассмотрим простую программу на C:

#include <stdio.h>
#include <unistd.h>

int main() {
	printf("My PID: %d\n", getpid());
	while(1);
	return 0;
}

Всё что она делает – это выводит свой PID(Идентификатор процесса в системе) и уходит в бесконечный цикл, чтобы процесс не завершался.

Давайте запустим:

zpnst@debian ~/D/a/pr> gcc main.c
zpnst@debian ~/D/a/pr> ./a.out
My PID: 49489

Теперь наш процесс «висит» в системе. Самое время взглянуть на папку /proc:

Все эти директории относятся к конкретным процессам, в этих папках лежит полная информация о них. Наш процесс имеет PID 49489, зайдём в его папку:

Что мы сделали?

  1. В папке /procзашли в 49489, в ней лежат файлы и папки с информацией о нашем процессе. Они не лежат на диске, это ресурсы ядра. При применении read() к этим файлам ядро просто выдаёт нам соответствующую информацию о процессе. Просто удобный интерфейс, не более!;

  2. Посмотрим на несколько простых файлов. comm показывает имя исполняемого файла, cmdline показывает полную командную, с помощью которой был запущен процесс, включая все аргументы(у нас их не было), io показывает подробную статистику операций ввода/вывода.... и так далее;

  3. В папке fd хранятся файловые дескрипторы процесса. По дефолту их три: stdin(стандартный поток ввода), stdout(стандартный поток вывода) и stderr(стандартный поток ошибок), на них мы посмотрим подробнее в следующем разделе. Когда процесс открывает какой-либо файл, ядро в рамках этого процесса назначает этому файлу дескриптор, через который процесс может взаимодействовать с ним. Да, стандартные потоки ввода/вывода это тоже файлы(посмотрим на это подробнее в следующем разделе).

Давайте откроем обычный файл и проверим папку fd этого процесса в /proc:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
	printf("My PID: %d\n", getpid());
	
	// Открываем файл и получаем его дескриптор с помощью `open()` – оболочки над системным вызовом `sys_open`
	int file_descriptor = open("file.txt", O_RDONLY);
	
	char buffer[13];
	
	// Читаем содержимое файла в буфер с помощью `read()` – оболочки над системным вызовом `sys_read`
	read(file_descriptor, buffer, 13);
	printf("%s\n", buffer);
	
	while(1);
	return 0;
}

Вывод:

zpnst@debian ~/D/a/pr> ls
file.txt  main.c
zpnst@debian ~/D/a/pr> cat file.txt
Hello, World!⏎                                                           
zpnst@debian ~/D/a/pr> gcc main.c
zpnst@debian ~/D/a/pr> ./a.out
My PID: 12927
Hello, World!

А теперь посмотрим на дескрипторы нашего процесса с PID 12927:

Появляется ещё один дескриптор, что является ссылкой на наш файл. А на что такое странное ссылаются стандартные потоки? С этим разберёмся буквально через несколько предложений.

Также, процесс может обратиться к своим данным в procfs по пути /proc/self. Для примера откроем уже знакомый нам /proc/self/cmdline. Там содержится команда, с помощью которой процесс был запущен:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
	char buffer[32];
	int file_descriptor = open("/proc/self/cmdline", O_RDONLY);
	read(file_descriptor, buffer, 32);
	printf("%s\n", buffer);
	return 0;
}

Вывод:

zpnst@debian ~/D/a/pr> ./a.out
./a.out

Всё верно, запустили мы с ./a.out, значит в cmdline - ./a.out.

То есть процессу даже не обязательно «знать» свой PID, он просто может зайти в /proc/self, а ядро уже само поймёт что это за процесс и какие данные ему выдать. Очень удобно и изящно!

❯ Смотрим на /dev

Директория /dev(devices) содержит файлы, представляющие/абстрагирующие физические устройства и логические ресурсы системы. Мы повторили предложение выше уже много раз за эту статью, давайте же притронемся к этой идее собственными руками!

❯ Терминалы, стандартные потоки ввода/вывода

Как мы говорили ранее, с каждым процессом по умолчанию ядро ассоциирует три файловых дескриптора. Стандартный потока ввода, вывода и ошибок: stdin, stdout и stderr.

Также, ранее мы уже видели, что в папке fd в procfs конкретного процесса указывают они на что-то непонятное, лежащее в папке /dev. Прошу любить и жаловать pts – это псевдотерминал.

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

Псевдотерминал выступает посредником между процессом и терминальной программой(у меня это GNOME Terminal). Псевдотерминал своего рода труба, у него есть два конца:

  1. Master конец – для терминальной программы;

  2. Slave конец – для вашей программы;

  3. Сама труба – символьное устройство /dev/pts/1.

Он нужен для того, чтобы процесс/программа могла общаться с терминальной программой, такой как GNOME Terminal.

Так как мы договорились не закапываться в подробности работы терминалов в Linux, то нам нужно знать лишь одно – для процесса записать что-то в терминал тоже самое, что и записать что-то в обычный файл.

Каждый из потоков ссылается на устройство /dev/pts/1, тогда в чём же разница? Да ни в чём! Я не шучу, это просто формальность, договорённость, что очевидно, так как все три ссылки у потоков ссылаются на один и тот же файл (устройство).

Можно было бы обойтись и одним потоком, но такое разделение оказывается весьма полезным во многих случаях, например вот:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
	const char *string = "Hello, Habr!\n";
	write(0, string, strlen(string)); // Пишем в stdin
	write(1, string, strlen(string)); // Пишем в stdout
	write(2, string, strlen(string)); // Пишем в stderr
	return 0;
}

Вывод:

zpnst@debian ~/D/a/pr> ./a.out
Hello, Habr!
Hello, Habr!
Hello, Habr!

Так как все файлы ссылаются на один и тот же псевдотерминал, на одно и то же устройство, следовательно и write() ведёт себя одинаково и при каждом из потоковых дескрипторов просто пишет в терминал.

Но благодаря разделению, вывод можно фильтровать:

zpnst@debian ~/D/a/pr> ls
a.out*  main.c
zpnst@debian ~/D/a/pr> ./a.out > output.txt 2> errors.txt
Hello, Habr!
zpnst@debian ~/D/a/pr> ls
a.out*  errors.txt  main.c  output.txt
zpnst@debian ~/D/a/pr> cat output.txt
Hello, Habr!
zpnst@debian ~/D/a/pr> cat errors.txt 
Hello, Habr!
zpnst@debian ~/D/a/pr> 

Что мы сделали?

  1. Посмотрели содержимое рабочей директории, там пока только текст программы и её исполняемый файл;

  2. Запустили программу, перенаправив стандартный поток вывода в файл output.txt с помощью >, а стандартный поток ошибок в errors.txt с помощью 2>;

  3. Теперь у нас появились файлы output.txt и errors.txt;

  4. Поток ввода мы не трогали, поэтому одна и трёх строк вывелась в терминал;

  5. Вторая строка теперь в output.txt, а третья в errors.txt.

Можем поиграть с устройством псевдотерминала напрямую:

Что мы сделали?

  1. Сначала был открыть лишь один терминал слева. Первый вывод показал нам, что есть только один псевдотерминал с индексом 0, то есть /dev/pts/0;

  2. Далее открываем второй терминал справа и повторно смотрим содержимое папки /dev/pts, появилось второе устройство /dev/pts/1!;

  3. В правом терминале вводим команду echo "Hello, World!", она просто выводит строчку на экран, вызывая write();

  4. Но мы можем вывести строчку и из левого терминала в правый, у нас же есть всё для этого! Устройство, отвечающее за правый терминал /dev/pts/1 и утилита echo. Далее просто выводим несколько раз из левого в правый, перенаправляя вывод в устройством, которое отвечает за правый терминал;

  5. А теперь пробуем вывести в левый терминал через его же устройство, такая команда немного бессмысленная, так как echo в левом терминале и без перенаправления с помощью > вывело бы в левый терминал, который закреплён за устройством /dev/pts/0.

❯ Мышь и тачпад

А теперь взглянем не на логический объект, такой как псевдотерминал, а на физическое устройство – мышь/тачпад. И в самом прямом смысле притронемся к концепции «Всё есть файл» собственными руками:

Мы вывели список всех устройств, потом зашли в устройства ввода и применили утилиту cat к файлу мыши mice. Утилита cat, применённая к обычному файлу, просто выводит его содержимое в терминал, то есть вызывает read() на этот файл(c read() мы уже знакомы).

Но применив cat к файлу мыши мы ничего не увидим... до того момента, как не подвигаем мышью! Попробуйте, это очень прикольно :)

Но что мы видим? Что за скобочки и восьмёрки и куча пустого места? Это просто управляющие коды драйвера мыши, которые терминал пытается интерпретировать как ASCII символы, ведь драйвер мыши (так как мышь – это файл), реализует функцию read() в структуре file_operationd (о ней мы говорили выше). И так как cat просто вызывает read() на файл, то никаких ошибок мы не наблюдаем. Мы просто видим, что бы получила программа, которая умеет интерпретировать данные от мыши. Например какой-нибудь графический интерфейс, которому нужно постоянно отрисовывать курсор на экране компьютера при изменении положения мыши на столе или пальца на тачпаде.

Написав простой скрипт на питоне, можно увидеть те самые байты. Запускаем скрипт и двигаем мышкой:

Почему мы читаем по три байта? Первый байт передаёт флаги и нажатую кнопку, второй байт передаёт движение по оси X (относительное перемещение по горизонтали), третий байт передаёт движение по оси Y (относительное перемещение по вертикали). На stackoverflow есть вопрос по этому поводу и код на C для просмотра этих байт. Я же для простоты решил написать на Python.

❯ Сокеты: интернет – это файл :)

Напишем простой TCP сервер на C:

Дисклеймер: этот код поистине ужасен, в нём не осуществляются проверки возвращаемых значений функций на предмет ошибки и он не следует абсолютно никаким «best practices», он нужен лишь для демонстрации! Вот такой вот одноразовый шаблон :) Примеры нормального TCP сервера на C вы можете найти в интернете по первой, а возможно и по второй, ссылке

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

Что тут произошло?

  1. В среднем терминале мы запустили сервер и он любезно сообщил на свой PID, а также, процесс сервера будет заблокирован, пока не не получит входящее соединение. Этого мы добились с помощью функции listen() и вывели информацию о том, что сервер слушает соединения в терминал;

  2. В нижнем терминале посмотрим список файловых дескрипторов сервера, теперь, кроме стандартных потоков, там появился сокет! Сокет – это ресурс ядра и управляет он ядром, дескриптор 3 хранит ссылку на этот ресурс. А ядро превращает этот ресурс в файл!

Что тут произошло?

  1. В верхнем терминале с помощью утилиты telnet подключаемся к серверу по локальному адресу и порту 4000. telnet работает по протоколу TCP, это нам и нужно;

  2. Теперь смотрим на нижний терминал, появился дескриптор под номером 4, он и олицетворяет канал связи/соединение между клиентом и сервером, через него они общаются. Если дескриптор (сокет) под номером 3 швейцар, то дескриптор (сокет) под номером 4 – официант. Думаю, аналогия ясна. В контексте сети: 3 – слушающий, 4 – принимающий и передающий.

Что тут произошло?

  1. Наконец-то отправляем сообщение Hello, Server с 41-им ! в верхнем терминале и видим, что в среднем терминале сервера мы его успешно получили!;

  2. Сервер завершил свою работу после получения перового сообщения, как мы и задумывали;

  3. Теперь в нижнем терминале при попытке вывести информацию о дескрипторах сервера у нас ничего не выходит. Разумеется, ведь файл процесса в /proc существует только во время жизни процесса, а процесс сервера уже завершился.

Вот и всё, теперь интернет соединение – это тоже файл. Сообщения от клиента сервер читает с помощью той же функции read(), а если бы он захотел отправить клиенту что-то в ответ, то воспользовался бы функцией write(), передав первым аргументом дескриптор сокета!

❯ Выводы/Заключение

Я очень рад, если вы дошли до сюда, значит я пишу не просто так. Надеюсь, вы узнали для себя что-то новое и вдохновились концепцией «Всё есть файл» в UNIX-подобных операционных системах, а в частности в Linux.

Что мы разобрали в статье?

  1. Мы поняли из чего состоит наш жёсткий диск и насколько искусно и невероятно сложно он сконструирован, а также, поняли каким образом на нём хранится файловая система и информация в целом;

  2. Узнали, что к ядру Linux можно подключать модули, на основе которых и пишутся драйверы, которые притворяются файлами. А точнее будет сказать, инкапсулируют в себе логику физического устройства или логического ресурса и выдают к ним стандартизированный файловый интерфейс взаимодействия;

  3. Рассмотрели множество примеров «необычных файлов» и поняли мощь настолько простых, но настолько изящных и фундаментальных read()/write().

Спасибо и до встречи на Хабре!

❯ Литература

Habr:

  1. Ассемблер: рассматриваем каждый байт «Hello, World!». Как на самом деле работают программы на уровне процессора и ОС

YouTube:

  1. Все ли является файлом в Linux?

  2. Как работают SSD? Как ваш смартфон хранит данные? Branch Education на русском

  3. How a Single Bit Inside Your Processor Shields Your Operating System's Integrity

  4. Первый модуль ядра на C и инструменты для его разглядывания • Live coding


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Комментарии (153)


  1. d1nckache
    08.09.2025 10:22

    Огонь! Иниересная тема


  1. sunUnderShadow
    08.09.2025 10:22

    Побольше бы таких статей на хабре


    1. Elendiar1
      08.09.2025 10:22

      Программа – текстовый файл

      Спасибо, но может не надо?


      1. Kenya-West
        08.09.2025 10:22

        Не очень шарю за столь низкие уровни, но вроде бы, по сути, любой бинарник можно обратно восстановить в Assembly-подобный язык, верно?


        1. zpnst Автор
          08.09.2025 10:22

          да, всё верно, ведь инструкции на языке ассемблера - это просто псевдонимы для машинных инструкций. поэтому что перевод из ассемблера в машинные коды, что наоборот - очень тривиален. простая замена


          1. artptr86
            08.09.2025 10:22

            А если программа самомодифицирующаяся?


            1. zpnst Автор
              08.09.2025 10:22

              А как это относится к вопросу выше? Всё равно ответ не меняется


          1. ZurgInq
            08.09.2025 10:22

            У вас, программа – текстовый файл. А процесс управляет ходом выполнения программы. То есть у вас процесс управляет ходом выполнения текстового файла.

            Это в самом начале и сразу бросается в глаза.

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


            1. zpnst Автор
              08.09.2025 10:22

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

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

              Просто лаконичное определение, в котором я не нахожу ошибок. А писать «Процесс управляет ходом выполнения исполняемого файла» - звучит не очень.

              А вообще, это не процесс управляет, а операционная система, планировщик. Процесс лишь представляет/абстрагирует, чтобы планировщику было чем оперировать.

              Так что определений можно придумать много, я же выбрал лаконичное, с пропуском очевидной части.

              Спасибо за комментарий!


              1. randomsimplenumber
                08.09.2025 10:22

                Исполняемый файл тоже текст. Только на языке процессора.


                1. netch80
                  08.09.2025 10:22

                  Текст и данные - об этом постоянно забывают.


              1. mayorovp
                08.09.2025 10:22

                Процесс не может управлять ходом выполнения программы, потому что процесс и есть программа в выполняющемся состоянии. Это во-первых.

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


        1. netch80
          08.09.2025 10:22

          Формально - да, можно. На практике - вы при восстановлении будете должны очень много вещей вычислить и подставить самому: например, что функция должна зваться CreatePlan(), а не sub_4D6B034F, как именно формировались данные (.ascii "Hello", а не .db 0x48, 0x65, 0x6c, 0x6c, 0x6f), и так далее. Это называется реверс-инжинирингом и это отдельный серьёзный навык.


      1. S-trace
        08.09.2025 10:22

        А вы никогда не видели собщение об ошибке "Text file busy" (ETXTBSY, errno 26) при попытке записать что-то в исполняющийся бинарник?

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

        Это из-за mmap'а секций бинарника в память. А скрипты не ммапятся - их читает интерпретатор (иногда целиом (python), иногда построчно (sh/bash).


        1. ProMix
          08.09.2025 10:22

          А вы никогда не видели собщение об ошибке "Text file busy" (ETXTBSY, errno 26) при попытке записать что-то в исполняющийся бинарник?

          Нет, никогда. А как у вас получилось?

          Программа при исполнении загружается в оперативную память, и больше обращений к файлу бинарника не происходит

          Наверное с mmap это будет работать именно так, как вы описали. Но это явно не частый случай, иначе любое обновление используемых библиотек или запущенных демонов заканчивалось бы ошибкой


          1. unreal_undead2
            08.09.2025 10:22

            Вообще то ELF файлы в память отображаются именно mmap'ом ;)

            иначе любое обновление используемых библиотек или запущенных демонов заканчивалось бы ошибкой

            Если бы при обновлении пакетный менеджер пытался просто открывать и писать в файл с бинарником, так бы и происходило. Но он всё таки действует умнее и сначала удаляет существующий файл. Надеюсь, не надо объяснять, как в UNIX обрабатывается удаление открытого файла.


      1. muxa_ru
        08.09.2025 10:22

        "всё есть текст"


      1. sunUnderShadow
        08.09.2025 10:22

        Не знаю, у меня в школе LISP на доске с расписанием висел. Наверное не стоит


      1. TastaBlud
        08.09.2025 10:22

        А вообще, лучше бы оперировать термином не "текстовый файл", а "байтовый поток". Почему?

        байтовый

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

        поток

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

        Для "байтового потока" же достаточно только имя (путь) (да и чаще можно вообще цифровым дескриптором обойтись, будь это inode, порт и т.д.) и 4 операции: open, close, read, write - и всё! А вот абстракции выше пусть занимаются вопросами уровня "файл" в зависимости от типа, ещё выше - дополнительными вопросами вроде кеширования, оптимального физического расположения и т.д. И такое разделение ответственности не только упрощает архитектуру и её реализацию, но и уменьшает проблему т.н. "протекающих абстракций" (leaking abstractions), когда вдруг оказывается, что более низкому уровню требуются знания более высокого уровня (опять же, чтобы прочесть текстовый файл, необходимо знать, в какой кодировке он записан, иначе проблемы неизбежны).

        Таким образом, гениальность в 3 принципах:

        1. Единство абстракций (о чем, собственно, сказано прямо в заголовке статьи), т.е. нам всё равно, кто источник/получатель, мы просто реализуем функциональность.

        2. Минимализм/простота - каждый слой (сервис, контроллер, домен...) обладает/предоставляет только самые необходимые для его цели понятия/операции, дополнительные же операции (для более удобной работы) предоставляет дополнительный или более высокоуровневый слой.

        3. Независимость - слой низкого уровня ничего не знает о существовании слоёв высокого уровня, высокоуровневые слои ничего не знают об устройстве низкоуровневых, вспомогательный слой знает только о слое кому он помогает, соседние слои вообще не знают о существовании друг друга. И только на самом высоком уровне (прикладном) есть единая точка входа (в *nix мире это корень "/").

        Всё. Всё гениальное просто. Настолько просто, что можно даже не поверить, что сложнейшая система может быть устроена настолько просто, без исключений (из правил) и оговорок. А всего-то нужно - не усложнять излишне.


        1. TastaBlud
          08.09.2025 10:22

          p.s. Ещё, требование 2ого принципа (минимализм) состоит в том, что слой реализует одну и только одну функцию, причём минималистично, но полно. (Так, например, не надо разделять этот же файловый слой на отдельные сервисы "открыватель файлов", "закрыватель файлов", "читальщик файлов" - именно потому, что они всё равно получились бы зависимыми друг от друга, что противоречит 3 принципу). Но чаще всего множество похожих операций разумно вынести во вспомогательный слой.

          p.p.s. Кажется, это и есть 3 принципа философии Unix.


        1. netch80
          08.09.2025 10:22

          не "текстовый файл", а "байтовый поток".

          Не подходит для мест, где у файла может быть много потоков (OS X) или дырки (все Unix).

          А ещё "поток" как-то не предполагает свободное позиционирование в нём, изменение размера (Unix сисколл truncate() может как урезать, так и расширять, в том числе дырками), или чтение разных данных на каждой операции чтения. И это только вспомнившееся с ходу.

          То, что вы предложили, это хороший типаж (trait) для доступа, когда требуется только последовательное чтение или запись, как в моём примере с grep входа с stdin. Но не для всего, что зовётся файлом.


          1. ProMix
            08.09.2025 10:22

            То, что вы предложили, это хороший типаж (trait) для доступа, когда требуется только последовательное чтение или запись, как в моём примере с grep входа с stdin. Но не для всего, что зовётся файлом.

            О том и речь. Статья рассказывает о работе как раз с потоками, а не с файлами. Файлам приходится поддерживать куда как больше операций


          1. TastaBlud
            08.09.2025 10:22

            А ещё "поток" как-то не предполагает свободное позиционирование в нём, изменение размера (Unix сисколл truncate() может как урезать, так и расширять, в том числе дырками), или чтение разных данных на каждой операции чтения.

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

            Именно поэтому память (RAM) абстрагирована как цельный кусок, а уж менеджер памяти сам жонглирует, как этот абстрактный кусок распределен в физической памяти, может быть он вообще на диске в файле подкачки. А прикладной софт совершенно не обязан про это знать. И только в особых случаях (например, для дефрагментаторов диска) может быть предоставлен "особый" доступ с раскрытием этих данных.

            Так что всё логично и стройно реализовано, просто не нужно смешивать разную функциональность в одну кучу.


  1. atues
    08.09.2025 10:22

    К списку литературы. Есть, хотя и старенькая, но бомбическая книга Андрея Робачевского по Unix. Из новых мне понравилась https://dmkpress.com/catalog/computer/os/978-5-97060-932-3/


    1. zpnst Автор
      08.09.2025 10:22

      Первый раз вижу, спасибо! Посмотрю


  1. unreal_undead2
    08.09.2025 10:22

    Не упомянули ioctl(), с которым юниксовый файл является по сути объектом с произвольным интерфейсом.


    1. zpnst Автор
      08.09.2025 10:22

      Да, решил не перегружать. Отличная домашняя работа для тех, кто зайдёт почитать комментарии ;)


      1. m0tral
        08.09.2025 10:22

        А это важный и ключевой аспект многих все есть файл, особенно когда это драйвера


    1. digrobot
      08.09.2025 10:22

      ioctl() портит всю красивую абстракцию, потому что в него пихают всё, что не уместилось в read() и write() =)


      1. zpnst Автор
        08.09.2025 10:22

        Ну да, его многие не любят)


        1. unreal_undead2
          08.09.2025 10:22

          Включая авторов Unix, выкинувших его в Plan 9.


          1. zpnst Автор
            08.09.2025 10:22

            Plan 9 очень интересная система, надо бы по ней тоже статью написать, спасибо за идею!


      1. mirwide
        08.09.2025 10:22

        Что не так с ioctl? Далёк от системного программирования, на неоптный глаз он выглядит удобно. Если во write перепутать fd можно сделать коллапс вселенной. В ioctl просто получим ошибку, за счёт того что в команде присутствует код устройства. Тип операции там тоже есть, те его цель не запихнуть всё что не запихнулось, а небольшая абстракция для систематизации апи.


        1. Dima_Sharihin
          08.09.2025 10:22

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

          Но с точки зрения ABI классно, да)


        1. unreal_undead2
          08.09.2025 10:22

          Тип операции там тоже есть

          Просто некий unsigned long, который в разных устройствах может означать разные вещи. Параметры операции - так и вовсе список из переменного числа произвольных аргументов.


  1. GospodinKolhoznik
    08.09.2025 10:22

    Почему в Linux «Всё есть файл»?

    Поймём, что системные вызовы – это всё, что у нас есть (больше реально ничего нет);

    А системные вызовы это не файл ))


    1. zpnst Автор
      08.09.2025 10:22

      Разумеется, думаю, это очевидно из описания в статье. А “Everything is a file” - это официальное название https://en.m.wikipedia.org/wiki/Everything_is_a_file :)


      1. GospodinKolhoznik
        08.09.2025 10:22

        Это я так душно пошутил. Меня всегда забавляли утверждения в духе "всё есть объект" или "всё есть функция" и я в таких случаях целенаправлено ищу то, что не является объектом в Java или функцией в Haskell, например.


        1. makartarentiev
          08.09.2025 10:22

          Или table в lua?)


        1. SpiderEkb
          08.09.2025 10:22

          На самом дел, последние 8 лет работаю в ОС где "все есть объект". У объекта есть имя, тип, атрибут (подтип), описание и еще ряд свойств.

          Система позволяет делать с объектом только то, что позволено для данного типа. Например, открыть программу в HEX редакторе и поправить пару байтиков вы не сможете - операция изменения для объекта типа *PGM не определена. А поменять тип существующего объекта невозможно.

          На низком уровне можно получить "системный указатель на объект" и потом уже с ним работать (в частности, получить всю информацию об объекте - "материализация системного указателя" - имя, тип и т.п.).


  1. sergeyns
    08.09.2025 10:22

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

    Я бы не был столь уверен, особенно в случае SSD дисков... И просто ноут включить не сможете (батарея сдохла), да и с SSD может много чего интересного произойти (с тем самым плавающим затвором, а точнее с зарядом..)


    1. zpnst Автор
      08.09.2025 10:22

      Согласен, 10 лет для рядового SSD - это большой срок, но всё же… это был лирический раздел статьи ;)


    1. udattsk
      08.09.2025 10:22

      В отличии от бумаги, кстати ))


      1. Sequoza
        08.09.2025 10:22

        У меня бумажные чеки через пару лет выцветают. Так, к слову.


        1. muxa_ru
          08.09.2025 10:22

          Это современные, да ещё и чеки.


        1. artptr86
          08.09.2025 10:22

          Потому что там термобумага


        1. urvanov
          08.09.2025 10:22

          Приложи их к лампе накаливания на некоторое время. Буквы снова станут читаемыми, правда, не так как в первый день, конечно.


      1. VanKrock
        08.09.2025 10:22

        Мне вот интересно, а если немного изменить условия? Допустим у нас есть листы А4 но писать и читать мы можем точками 1х1 мм, так же мы можем класть листы сбоку и снизу а так же вверх на другой лист, тем самым организуя трёхмерный массив точек, сколько бумаги потребуется чтобы сохранить 1 террабайт данных в этом случае?


        1. zuek
          08.09.2025 10:22

          Ну, элементарно же считается! Если без полей, то одна сторона А4 имеет площадь 62370 квадратных миллиметров...


          1. muxa_ru
            08.09.2025 10:22

            А4 по определению 62500 квадратных миллиметров :)


            1. Rsa97
              08.09.2025 10:22

              В стандарте ISO 216 некоторое противоречие. По нему лист A4 имеет размеры 210×297 мм, то есть площадь 62370 мм². Но по другой части стандарта его площадь 1/16 м² или 62500 мм². Проблема в округлении. Соотношение сторон должно быть 1:√2, соответственно для площади 1/16 м² стороны должны быть ~210.2241 и ~297.30178 мм.
              Но при погрешности сторон ±2 мм, определяемой тем же стандартом, мы вообще можем получить от 61360 до 63388 мм².


  1. Cubus
    08.09.2025 10:22

    А можно не городить скрипты на Питоне, а сделать true unix way:

    $ sudo cat /dev/input/mouse1|xxd -c 3 -g 1

    Получится примерно так:

    00000000: 28 01 fe  (..
    00000003: 28 02 fe  (..
    00000006: 28 02 fe  (..
    00000009: 28 00 ff  (..
    0000000c: 28 01 ff  (..
    0000000f: 28 00 ff  (..
    00000012: 28 02 fe  (..
    00000015: 28 02 fe  (..
    00000018: 28 00 ff  (..
    0000001b: 28 02 fe  (..
    0000001e: 28 01 ff  (..
    00000021: 28 01 ff  (..
    00000024: 08 02 00  ...
    00000027: 08 03 00  ...
    0000002a: 08 02 01  ...
    0000002d: 08 02 01  ...
    00000030: 08 02 00  ...
    00000033: 08 02 01  ...
    00000036: 08 01 00  ...
    00000039: 08 01 01  ...



    1. zpnst Автор
      08.09.2025 10:22

      да, очень лаконично и красиво!


    1. Wesha
      08.09.2025 10:22

      городить скрипты на Питоне

      Рождённый ползать летать не может!


    1. Finesse
      08.09.2025 10:22

      С Питоном тема раскрывается нагляднее


  1. funca
    08.09.2025 10:22

    Если сравнивать сокеты и файлы, то из общих функций у них только read, write и close - остальные все разные. Причём, read и write для сокетов это урезанные версии их же send и recv. В общем случае, подме��ить одно другим без существенного переписывания программы - не получится.

    Потоки ввода/вывода, такие как stdin, stdout и stderr, также представляют собой особый случай, поскольку набор допустимых операций, значительно различается в зависимости от направления и destination перенаправления.

    В конечном счете, утверждение "все является файловым дескриптором" было бы ближе к истине. Но файловый дескриптор - это всего лишь число. А фраза "в Unix все есть число", хоть и точна, но звучала бы довольно уныло.


    1. zpnst Автор
      08.09.2025 10:22

      Да, «Всё есть число» звучало бы скучно.

      Со всеми замечаниями согласен, но как факт, что с интернет соединением, что с потоками - можно работать через read()/write().

      Да и вообще, опять же, как я уже писал выше, «Всё есть файл» - это официальная терминология, что недалека от истины

      Спасибо за развёрнутый комментарий!


    1. TastaBlud
      08.09.2025 10:22

      "... всё есть число"

      в мире машинного кода именно так и есть, а в мире микросхем даже "всё есть бит" :)

      Ваша мысль верна, мне пришла на ум такая же, об этом я написала.


      1. netch80
        08.09.2025 10:22

        а в мире микросхем даже "всё есть бит" :)

        Это пока не начинаются проблемы типа таких. Или флэш-память с более 2 уровнями заряда (MLC, QLC и далее).

        Дырявые абстракции — они такие.


  1. lealxe
    08.09.2025 10:22

    Потому что нихрена они не гениальны, если их не хватает и приходится обходить сокетами-шмокетами, побочными интерфейсами и прочей люльней.

    Вот классическая макось была по-своему гениальна (ничего особенного, что можно записать в дзен-философию и прочие гениальности, а просто настолько хорошая инженерная работа со всех сторон), а Лисп-машины по-другому гениальны (и скромная портативная замена сему под названием Emacs по-прежнему педалит), а RSX-11 по-третьему (шутка ли - общие идеи и в НТ очень похожи до сих пор), а амиги по-четвертому.

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


    1. zpnst Автор
      08.09.2025 10:22

      Скажем так, гениально для широкой аудитории)

      Не думаю, что многие знают про Лисп-Машины, тем более моё поколение(я всего на год старше Хабра =))

      Спасибо за интересный комментарий!


  1. OlegZH
    08.09.2025 10:22

    Если бы и вправду можно было бы всё представлять себе как файл! Вопрос, только, в том, каков формат этого файла. А ещё вопрос в том, что должно быть между файлами и пользователем. Тут, понятное, дело, должны быть документы. А что такое документ? Документ — это набор связанных между собою файлов. То есть! Должно быть отображение документов на файлы.

    Если бы всё было бы файлом... Например, пользовательский интерфейс...

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


    1. Dima_Sharihin
      08.09.2025 10:22

      Например, пользовательский интерфейс...

      Открываете файл x11- или wayland- сокета, и, кхм, пишете в него...
      Вопрос удобства никто не рассматривал)


    1. TastaBlud
      08.09.2025 10:22

      Вопрос, только, в том, каков формат этого файла.

      Например, html это текст, но текст не обязательно html. Т.е. не всегда это важно, например, разметку можно поправить в Блокноте, но для полноценной работы всё потребуется браузер. Для этого и требуется иерархия слоёв, как я уже писала тут. Тот же принцип иерархии много где применяется, даже в сетевом (OSI) стеке.


  1. Error1024
    08.09.2025 10:22

    Ой, простите нет. “Все есть файл” - это самое убогое и анти-удобное что было создано, и закрепилось навсегда в виде нескон��аемого UNIX/POSIX/C legacy.


    1. randomsimplenumber
      08.09.2025 10:22

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


    1. TastaBlud
      08.09.2025 10:22

      Думаю, проблема в терминологии, потому что термин "файл" ассоциируется именно с оным на устройствах hdd/ssd и очень удивляются, что ram устроена практически одинаково, ну что ж, flash накопители почти стёрли эту грань. Я попыталась обобщить и уточнить вопрос.


  1. Anton_8266
    08.09.2025 10:22

    Хорошая статья, но как мне известно, ПАПКИ в шкафу, а в компьютере ДИРЕКТОРИИ.


    1. randomsimplenumber
      08.09.2025 10:22

      inode же ж.


      1. Rsa97
        08.09.2025 10:22

        Нет, inode - это только указатель на директорию или файл. Индексная карточка.


        1. DungeonLords
          08.09.2025 10:22

          А существуют ли inode в 2025 году?

          user@BTRFS:~$ df -i /
          Filesystem     Inodes IUsed IFree IUse% Mounted on
          /dev/nvme0n1p1      0     0     0     - /
          


          1. Belibak
            08.09.2025 10:22

            А что это у вас за бтр вместо фс в 2025 то году?


          1. Rsa97
            08.09.2025 10:22

            Зависит от файловой системы. В ext и tmpfs, например, вполне себе существуют. В fat/vfat/exfat их никогда и не было.


            1. inkelyad
              08.09.2025 10:22

              В fat/vfat/exfat их никогда и не было.

              Это зависит от интерпретации. Если сделать stat на файл на них - вполне пишет нечто, что так называется.


              1. netch80
                08.09.2025 10:22

                Пишется номер начального кластера. Это подходит под критерий уникальности.


    1. mayorovp
      08.09.2025 10:22

      Кажется, вы проспали последние 25 лет развития индустрии. Метафора "папок в шкафу" оказалась настолько удачной, что "папка" - давно уже синоним "директории".

      Я тут посмотрел, даже в 2000 году KFM (KDE file manager) использовал значки тех самых "папок в шкафу".


      1. Dima_Sharihin
        08.09.2025 10:22

        да и в 2000 это уже была устоявшаяся практика минимум лет 10 как

        win 3.1
        win 3.1


        1. mayorovp
          08.09.2025 10:22

          Ну, на винде-то понятно, они ж её и придумали. Однако, "виндузятник" никогда не станет возражать против термина "папка", это бзик пользователей других ОС. Потому я и искал именно линуксовые аналоги.


          1. dizatorr
            08.09.2025 10:22

            на винде-то понятно, они ж её и придумали.

            Разрешите подушню. Не они, ой далеко не они. Первые наработки ещё у Xerox были в уже сформировавшемся виде. А концепция ещё в 60х была разработана Дагом Энгельбартом.


  1. polar_yogi
    08.09.2025 10:22

    В глоссарии не хватает определения что такое файл. Понятно что все есть файл, осталось понять что такое файл.


    1. netch80
      08.09.2025 10:22

      Ещё в фидошные времена в ru.os.cmp пытались получить определение файла и нашли, что в разных ОС настолько отличаются критерии определения "файл", и разные псевдофайловые системы типа /proc настолько усложняют картину, что общего определения просто нет! Есть несколько вариантов, подходящих для конкретных ОС, но не для всех одновременно. Это несмотря на то, что интуитивно мы (ну кто прошёл начальный этап) всё понимаем.

      Отсюда и пункт в позднее сформированном "постмодернистском" FAQ:

      Q48: что такое файл?
      A48: ОПЯТЬ?


      1. polar_yogi
        08.09.2025 10:22

        Псевдофайловые системы типа /proc явля.тся частью файловой иерархии, и файлы там характеризуются путем, именем, и т.п. Давайте конкретно про линукс - что такое файл в linux?


      1. mayorovp
        08.09.2025 10:22

        Хм, мой вариант - "именованный контейнер для хранения данных в некотором хранилище за пределами основного адресного пространства"


        1. netch80
          08.09.2025 10:22

          Тогда это противоречит тому, что 1) "file descriptor" является стандартным понятием, даже если дескриптор ссылается на signalfd, epoll и тому подобные служебные сущности, 2) какое-нибудь /sys/class/power_supply/BAT1/uevent для файлового API это обычный файл, но он ничего не хранит и не находится за пределами адресного пространства, а просто даёт читать данные батареи.

          Увы, у меня нет архивов, а там в дискуссии всё это обсосали и в итоге пришли к описанному выводу. Мы можем попытаться повторить тут, вот вам вверху первое возражение:)


          1. mayorovp
            08.09.2025 10:22

            "file descriptor" является стандартным понятием, даже если дескриптор ссылается на signalfd, epoll и тому подобные служебные сущности

            Да, file descriptor это больше descriptor чем file. Тот же signalfd и правда файлом с моей точки зрения не является.

            какое-нибудь /sys/class/power_supply/BAT1/uevent для файлового API это обычный файл, но он ничего не хранит и не находится за пределами адресного пространства, а просто даёт читать данные батареи

            А вот тут нет никаких проблем. Если из него можно прочитать данные батареи - значит, он их хранит. И, конечно же, он расположен за пределами адресного пространства процесса.


            1. netch80
              08.09.2025 10:22

              Если из него можно прочитать данные батареи - значит, он их хранит.

              Нет, хранит не он. Он их просто представляет в некотором удобном для чего-то виде. Пусть рядом будут uevent_json и uevent_cbor, где то же самое, но в другом формате; они меняться будут синхронно.

              И, конечно же, он расположен за пределами адресного пространства процесса.

              Почему? В том же адресном пространстве - по крайней мере на x86 и ARM. Просто юзерленду недоступен именно как содержимое памяти, ещё и представлен в каких-то запутанных структурах.

              Видите, вот стали чуть внимательнее смотреть в детали - и всё определение поплыло.

              Да, file descriptor это больше descriptor чем file.

              Но его продолжают именовать file descriptor. А не, как в Windows, "handle" неважно чего.


        1. polar_yogi
          08.09.2025 10:22

          Псевдо файловые системы procfs/sysfs/.. хранятся в основном адресном пространстве. Утилита stat знает ограниченное количество типов файлов (man 2 stat / EXAMPLES). И для сетевых интерфейсов файлов нет. Это я к тому что UNIX это всё же - почти всё есть файл. План9 описывается как более экстремальная в этом плане.


      1. trinxery
        08.09.2025 10:22

        1. netch80
          08.09.2025 10:22

          Меня уже подводит память, но кажется, что основная дискуссия происходила лет на 5 раньше. Это уже повторная, в сообщениях есть явное упоминание про это. Я пытался искать и не нашёл. Но гугл хранит не все сообщения, или даёт плохой поиск по ним.

          Но на эти тоже можно частично опереться.


      1. TastaBlud
        08.09.2025 10:22

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


        1. netch80
          08.09.2025 10:22

          более абстрактный термин "байтовый поток".

          Не пойдёт там, где в файле может быть несколько потоков (Windows, OS X) или дырки (все Unix, OS X).

          И, по-моему, именно слово "файл" вводит в заблуждение

          Я бы начал с вопроса, можем ли мы разделить понятия файла на диске, файла как сущности виртуального дерева и файла как объекта, адресуемого дескриптором - разделить (и дать разные названия) без того, чтобы отправить мозговые крыши большинства ITшников в далёкие тёплые края. И вот тут у меня начинают вырастать колоссальные сомнения.


  1. Panzerschrek
    08.09.2025 10:22

    Мне вот кстати не нравится эта абстракция на уровне системных вызовов - когда есть какой-нибудь read, который хоть из файла, хоть из канала, хоть из сокета читать может. Такой API требует в ядре ветвления/непрямого вызова в реализации read (и других функций). Это создаёт немного накладных расходов, при чём часто там, где это не надо. Многие программы ведь не пользуются такой полиморфной возможностью read - они читают или всегда из файла, или всегда из канала, или всегда из сокета. Спрашивается, нужна ли такая абстракция? Не лучше ли иметь несколько отдельных системных вызовов для чтения из различных типов источников? А абстракцию можно реализовать внутри прикладной программы и только там, где это действительно нужно.

    Подобное вываливание в кучу разнородных по своей системе действий порождает некоторые трудности в использовании API. У каждой функции есть множество флагов, иногда существенно меняющих поведение. И возможных кодов ошибок сильно много, при чём для некоторых случаев использования они не возможны, но их всё равно приходится обрабатывать в пользовательском коде.


    1. S-trace
      08.09.2025 10:22

      Насколько я помню, в ядре когда создаётся файловый дескриптор - в его vtable пишутся указатели на конкретные реализации read()/write() и так далее, а дальше происходят вызовы через указатели на функции из vtable (то есть никаких явных if/case в коде ядра)


      1. Panzerschrek
        08.09.2025 10:22

        Что if/case, что вызов через таблицу виртуальных функций - так или иначе это накладные расходы на передачу потока управления, которые во многих случаях излишни.


        1. lgorSL
          08.09.2025 10:22

          Кажется, на фоне расходов на сам системный вызов это мало. Тут мои знания очень поверхностные, но кажется что для обработки системного вызова надо ещё переключиться в контекст ядра, сохранить все регистры, переключить стек на ядерный, что-то сделать, а потом вернуть регистры обратно и переключиться обрано в юзерспейс. Ещё я знаю что есть TLB, но не знаю сбрасывают ли его внутри системного вызова или как-то обходятся без этого. Т.е. системный вызов сильно сложнее внутри, чем просто вызов функции.


    1. randomsimplenumber
      08.09.2025 10:22

      Не лучше ли иметь несколько отдельных системных вызовов для чтения из различных типов источников?

      Win api, 100500 функций, в каждой 100500 параметров.. ужасно. Просто сравните open и CreateFileEx.


      1. zpnst Автор
        08.09.2025 10:22

        да, в UNIX пошли на компромисс и у них вышло неплохо. есть и специализированные функции, такие как recv под сокеты(обёрнутое read)


      1. Panzerschrek
        08.09.2025 10:22

        CreateFileEx - как раз пример той же порочной практики, когда через один интерфейс пропихивают всё что можно - и кучу флагов, и структуры для асинхронной работы с файлами и чёрт ещё знает что.
        Функция open при этом тоже не так проста, как кажется. Она на самом деле имеет переменное число аргументов и это число определяется переданными флагами.


    1. netch80
      08.09.2025 10:22

      Не лучше ли иметь несколько отдельных системных вызовов для чтения из различных типов источников?

      Это проходили. Например, в OS/360. Для каждого типа операции своё API. Посмотрите, например, макры для БТМД и ОТМД (не знаю названий на английском), они несовместимы по стилю и надо под каждый проектировать по-своему. И именно по опыту этих систем перешли к варианту, когда операция универсальна для разных типов внешних объектов.

      read(), например, именно что читает порцию данных. Ей одинаково, это был файл, псевдофайл в соответствующей FS, пайп безымянный, пайп именованный, сокет... Ей надо прочитать порцию байт, она её читает.

      Конечно, надо заметить, что Unix тут переабстрагировался в другую сторону. Например, если read() для файла получил 0, это точно конец файла, а для терминала это может быть Ctrl+D (сброс буфера) при пустом буфере (начало строки), а для UDP датаграмма размера 0 (законный вариант!) Семантика такого read() для TCP совпадает с файловой, а для UDP - нет, есть границы датаграмм, ещё и источник не однозначен. Я бы не делал read() для UDP, разрешив тут только явный recvfrom(). Но в целом вреда от переобобщения образца Unix сильно меньше, чем от необходимости делать всегда разные вызовы, как было в OS/360 (и как частично воспроизвели в Windows с сокетами).

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

      UPD: А ещё это как "трейты" ("типажи") в языках как Rust. Вот есть типаж "нечто, из чего можно read". Вот есть "нечто, поддерживающее recvmsg". Вот есть "нечто с прямым доступом", соответственно, умеет lseek(). Просто в рантайме на уровне собственно границы юзерленд - ядро тип стёрт до одного int, и поэтому вы можете попытаться вызвать lseek для сокета и recvmsg для файла, и получите ошибку в рантайме, а не при компиляции. Ещё и read потоковый, read датаграммный и read для SOCK_SEQPACKET, по факту, объединены в одном интерфейсе, хоть и они и разные по сути. Вот это уже та часть легаси, которую неплохо бы исправить.


      1. Panzerschrek
        08.09.2025 10:22

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


        1. netch80
          08.09.2025 10:22

          Условно говоря, если я читаю файлы, мне API сокетов не интересны.

          Совсем не условно говоря, если у вас код какого-нибудь grep, sort и ещё десятки всяких, ему нужен API ввода или вывода потока байтов. При этом ему пофиг, как выглядит источник или приёмник потока байтов, пока он поток: это файл, терминал, безымянный пайп, именованный пайп, сокет пространства файлов, сокет TCP, псевдофайл /proc или /sys - работа с ними абсолютно одинакова. И это хорошо тем, что для простых задач применяются простые обобщённые решения.

          Вот когда переходим к сложным задачам - программа сама открывает объекты, поднимает соединения, делает сама восстановление после ошибок при этом - и средства усложняются.

          То есть в Unix при этом оптимальна, говоря терминами лингвистов, learning curve. Простые запросы - простые методы, сложные запросы - возможность применить сложные методы.

          А вот так как было в OS/360 или есть местами в Windows, что даже для простой задачи надо навернуть что-то на порядок более замороченное - приводит к опусканию рук и хлопанью дверьми, или к нелепейшим решениям для простых задач только потому, что голова пухнет от нафиг не нужных подробностей.

          даже несколько проще будет, ибо не нужно будет реализовывать диспатчинг в зависимости от фактического типа объекта.

          Эта разница настолько мала, что не заслуживает внимания.


          1. Panzerschrek
            08.09.2025 10:22

            Во всяких утилитах вроде sort и grep вход это stdin а выход - stdout, которые ни разу не файлы, а каналы (pipe), при чём ещё односторонние. А кто в них пишет и кто из них читает, это уже отдельный вопрос. По-идее, sh/bash должен перенаправлять эти каналы куда надо - хоть в терминал, хоть в файл, хоть куда-то ещё. Сама же утилита grep должна работать с stdin/stdout только как с каналом - через соответствующий интерфейс и ни коем образом не использовать файлоспецифичный seek.


            1. netch80
              08.09.2025 10:22

              Во всяких утилитах вроде sort и grep вход это stdin а выход - stdout, которые ни разу не файлы, а каналы (pipe), при чём ещё односторонние.

              Нет. Это может быть, повторяю, что угодно. Их сама утилита использует как односторонние каналы, но им ничто не мешает быть произвольными ядерными сущностями, которые просто умеют в read() или write().

              Сам по себе stdin может быть терминалом - для тех программ, которым это нужно. Тот же bash, если интерактивный режим, использует свои stdin и stdout именно как терминал: настраивает политику raw режима, уточняет его тип, принимает/передаёт нужные esc-последовательности. (Если неинтерактивный, скрипт - ему пофиг, работает точно так же как grep, со входом и выходом.) И тут тоже, терминал может быть настоящий железный, может быть виртуальный от юниксовой консоли, может быть псевдотерминал (сейчас в 99+% случаев), может быть что-то другое, прикидывающееся терминалом - программе пофиг, пока сущность, спрятанная за дескриптором, поддерживает tcgetattr(), tcsetattr(), tcflush() и тому подобные вызовы.

              По-идее, sh/bash должен перенаправлять эти каналы куда надо - хоть в терминал, хоть в файл, хоть куда-то ещё.

              А может и не перенаправлять. При вызове без "|" он оставляет то, что ему самому поступило как дескрипторы 0 или 1. Или команду может запускать вообще не bash, а отдельная программа, давая на вход файлы или сама порождая пайпы. И снова для самой утилиты просто есть нечто на дескрипторе 0, которое как положено отрабатывает read(), и на 1 - write(). Такое себе ООП.

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

              Сама же утилита grep должна работать с stdin/stdout только как с каналом - через соответствующий интерфейс

              Да. Но это уже её выбор, а не то, что ей назначил шелл.


              1. Panzerschrek
                08.09.2025 10:22

                Вы путаете "что угодно" в обывательском смысле и "что угодно" с точки зрения конкретного типа объекта, с которым осуществляется чтение/запись. При старте программы (через sh или иным способом) вызывающая программа устанавливает каналы для stdin/stdout, соединяющие вызываемую программу с чем надо - другой программой (в случае |), или же самой вызывающей программой, которая сама может откуда ей угодно брать ввод и куда ей угодно писать вывод. Наличие полиморфных операций чтения/записи в системных вызовах для этого избыточно и как я заявляю, местами даже вредно.

                С точки зрения grep ввод и вывод должны быть только типа pipe, а не произвольным файлов (с возможностью seek). Если нужно что-то более сложное, например чтение данных из сети, то это должна делать отдельная программа. Условно говоря запускаем какой-нибудь curl/wget, который через отдельные системные вызовы для TCP потоков что-то читает из указанного адреса, пишет результат в pipe, который пользователь перенаправил в grep (через |).

                С записью вывода программы в файл аналогично - sh читает данные из pipe (и только из pipe) и пишет уже отделными системными вызовами в файл.

                При вызове без "|" он оставляет то, что ему самому поступило как дескрипторы 0 или 1.

                Это уже внутренняя логика sh - писать или в собственный выходной pipe, или же в файл, или ещё куда-то. Зачем тут нужен полиморфизм на уровне системных вызовов? Сам sh может это разрулить. А те программы, которым это не надо, не пользуются полиморфизмом системных вызовов, как это сейчас реализовано в принудительном порядке.


                1. mayorovp
                  08.09.2025 10:22

                  Погодите, вот есть команда grep, у неё stdout может быть только пайпом. Есть команда mc, у неё stdout может быть только терминалом.

                  И есть sh, для которой что grep, что mc - одинаковые внешние исполнимые файлы. Как это вообще будет работать?


                  1. Panzerschrek
                    08.09.2025 10:22

                    stdout и stdin - всегде pipe (в моей модели). Терминал - отдельная сущность. mc при запуске или открывает какой ей нужно терминал, или какой ей запускающая программа укажет.


                    1. mayorovp
                      08.09.2025 10:22

                      mc при запуске или открывает какой ей нужно терминал

                      А надо не "какой ей нужно", а тот на котором её запустили.

                      или какой ей запускающая программа укажет

                      Как?


                      1. Panzerschrek
                        08.09.2025 10:22

                        Можно передать нужный терминал через опции командной строки. Можно назначить какой-нибудь известный номер дескриптора для терминала по умолчанию, как это делается сейчас с дескрипторами 0, 1, 2, которые приняты за stdin, stdout, stderr.

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


                      1. mayorovp
                        08.09.2025 10:22

                        Можно передать нужный терминал через опции командной строки.

                        Кто его передаст? Пользователь? А он-то откуда этот номер знает?


                      1. Panzerschrek
                        08.09.2025 10:22

                        Я предлагаю не только опцию командной строки, но и стандартизированный номер дескриптора, с наследованием при старте процесса. А опция нужна для тех случаев, когда пользователь номер терминала знает. Как вариант, можно в sh сделать переменную с текущим номером терминала, раскрываемую при её использовании в качестве аргумента запускаемой программы.


                1. netch80
                  08.09.2025 10:22

                  Ваша идея понятна. Но принять я её такую не могу: просто нет смысла усложнять на ровном месте то, что делается проще и универсально - и, главное, с возможностью развития.

                  Сравните это с ООП. Какой бы вариант его ни был, он старается поддерживать LSP, который есть принцип совместимости интерфейсов и определённых ожиданий контракта от прочих участников. С ним я могу применять любую реализацию того, что отвечает интерфейсу и контракту. Вы же требуете определённой фиксированной реализации. Зато на шелл ложится вся ноша разбора, как писать в файл, как в сокет... а где граница? Почему в сокет, который связан с сервером в локалке, и сервером на другом континенте, пишем одинаково? Там ведь тоже различие в деталях, типа обычной round-trip задержки, которая кому-то существенна.

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


                  1. Panzerschrek
                    08.09.2025 10:22

                    нет смысла усложнять на ровном месте то, что делается проще и универсально

                    Как раз унификация разнородных сущностей за одним API - усложнение кода ядра ОС. За эту абстракцию приходится платить сложностью кода ядра и накладными расходами времени выполнения. Я же предлагаю вынести абстракции в код прикладных программ, и то, реализовывать их только там, где это необходимо.

                    Зато на шелл ложится вся ноша разбора, как писать в файл, как в сокет... а где граница?

                    sh в сокеты не пишет и из сокетов не читает. Это делают соответствующие программы, куда sh перенаправляет выхлоп других программ. С файлами точно так же - cat, это отдельная программа, которая читает файл и пишет его в stdout. Только вот почему-то перенаправление вывода в файл sh сам делает, хотя ничего не мешает это также на отдельную программу переложить.

                    Апелляция к индустрии - так себе аргумент. Вы просто пытаетесь рационально обосновать решен��е, которое волею случая стало широкоиспользуемым. Вполне возможно, что какие-нибудь Керниган-Ричи при написании своего Unix таки экономили место в заголовочном файле и эта вся их мотивация для унификации разнородных сущностей за одним API.


                    1. unreal_undead2
                      08.09.2025 10:22

                      Я же предлагаю вынести абстракции в код прикладных программ, и то, реализовывать их только там, где это необходимо.

                      Может ещё и явно в пользовательском коде вызывать разные read/write для разных файловых систем? Для них же разные реализации в ядре, приходится косвенный вызов делать...


                      1. Panzerschrek
                        08.09.2025 10:22

                        Как раз таки файл на файловой системе - это базовая абстракция. Для него таки нужны функции, не зависящие от конкретного типа файловой системы. Но в дополнение к этому нужны также и функции для операций с конкретной файловой системой, ибо их нюансы различаются - поддерживаются разные атрибуты файлов, некоторые операции различаются (hardlink под NTFS нету), разные файловые системы имеют различные ограничения и т. д.


                      1. unreal_undead2
                        08.09.2025 10:22

                        Для него таки нужны функции, не зависящие от конкретного типа файловой системы.

                        Ну да, те же read/write верхнего уровня, одинаковые для файла на диске, сокета или /dev/random

                        ибо их нюансы различаются

                        Дело скорее не в нюансах, а в том что только FS знает, куда конкретно на диске записать очередную порцию данных в файле.


                      1. Panzerschrek
                        08.09.2025 10:22

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

                        Но это всё детали. Вы кажется так и не уловили суть моей идеи. Я выступаю против навязывание абстракций через системные API там, где они не нужны. Если я работаю с файлами, извольте мне предоставить API именно для файлов, который не будет иметь накладных расходов на полиморфизм, свойственный API в том виде, какой он есть сейчас. Или даже глубже - если мне нужно работать с файлами именно на NTFS, я должен иметь специализированный API для этого, который напрямую работает с соответствующим кодом ядра ОС, без ненужных обходных путей.


                      1. unreal_undead2
                        08.09.2025 10:22

                        не будет иметь накладных расходов на полиморфизм

                        Так в том то и дело, что полиморфизм нужен уже для того, чтобы работать через один и тот же API с файлами на разных FS. Сокеты и т.п. оверхеда не добавляют - они просто не реализуют часть методов.


                      1. mayorovp
                        08.09.2025 10:22

                        Как раз таки файл на файловой системе - это базовая абстракция. Для него таки нужны функции, не зависящие от конкретного типа файловой системы.

                        Но ведь эта абстракция приводит к косвенным вызовам, которые вам так не нравятся из-за их производительности?


      1. Panzerschrek
        08.09.2025 10:22

        API сокетов, кстати, аналогично проблемный. Через одни и те же функции осуществляется работа и с IP пакетами, и с UDP, и с TCP, и с кучей других, ныне почти забытых протоколов. Лучше было бы иметь отдельные функции для каждого типа сокета, в том числе отдельные для сокетов TCP слушателя и TCP потока.


        1. mayorovp
          08.09.2025 10:22

          Вот не надо разного API! Это убьёт unix-сокеты.


          1. netch80
            08.09.2025 10:22

            Я как раз частично поддерживаю, считая, что read() для SOCK_STREAM, SOCK_DGRAM и SOCK_SEQPACKET объединён зря. Там всё равно надо обрабатывать результаты совсем по-разному. Для потока - понимать, что данные поступили по порядку, буферизация в read() может быть как угодно, хоть по байту, хоть по миллиону байт, а возврат 0 обозначает закрытие входа. Для SOCK_DGRAM надо самому контролировать повторы и потери, буфер должен позволять полную датаграмму, а возврат 0 может быть нормальной ситуацией пустой датаграммы (UDP позволяет!) Для SOCK_SEQPACKET промежуточное между ними. Ещё и ancillary data у сокетов усложняет картину. То есть это действительно разные операции, объединение их под одной крышей достаточно насильственно.

            А вот различать в этом случае IPv4, IPv6, IPX, локальные сокеты, пайпы - это уже точно перебор.


            1. Panzerschrek
              08.09.2025 10:22

              Почему различать IPv4 и IPv6 - перебор? Пользовательская программа использует или одно, или другое. Нюансы их использования могут различаться, и соответственно их API должен быть различным. А там, где это надо, пользовательская программа должна сама реализовывать нужный ей полиморфизм. Условно говоря есть классы IPv4Stream и IPv6Stream, реализующие на основе разных системных API интерфейс вроде IAbstractTCPStream.


              1. netch80
                08.09.2025 10:22

                Почему различать IPv4 и IPv6 - перебор? Пользовательская программа использует или одно, или другое.

                Потому что у них одинаково всё, кроме адресов. И то, адреса пересекаются (в Linux v6 слушающий сокет автоматом цепляется и на v4, если не отменить). Когда 99.99+% функциональности одинаково, разделение становится искусственным.

                Ну а в BSD sockets это различие загнали под одно поле параметра функций bind() и connect(), избавившись от необходимости дублирования всего остального.

                Условно говоря есть классы IPv4Stream и IPv6Stream, реализующие на основе разных системных API интерфейс вроде IAbstractTCPStream.

                И различие исчезает с момента коннекта.

                Я начинаю недоумевать, как вы программируете с таким нежеланием понимать и поддерживать абстракции.


                1. Panzerschrek
                  08.09.2025 10:22

                  IPv4 и IPv6 имеют различный формат заголовка пакета. Соответственно реализация в ядре ОС этих двух версий протокола различна. А раз реализация различна, то и должен быть способ через системные вызовы обратиться к какой-то конкретной реализации. Наворачивание абстракций над этими двумя версиями протокола только усложняют реализацию - требуют в ядре или if городить, или вызовы через указатели, или как-то ещё. Это накладные расходы, которые часто избыточны. Поэтому я предлагаю от них избавиться, имея различные API для различного функционала, а абстракции реализовывать уже в коде прикладной программы.


                  1. netch80
                    08.09.2025 10:22

                    А раз реализация различна, то и должен быть способ через системные вызовы обратиться к какой-то конкретной реализации.

                    Он есть. socket, bind, connect, {set|get}sockopt параметризуются. Остальным - не нужно.

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

                    В ядре это делают один раз и опытные разработчики. Если это делать в юзерленде, придётся делать каждому отдельно. Концентрация ошибок подымется в сотни раз.

                    Или же это закончится тем, что будет универсальная библиотека уже в юзерленде, которая будет делать то же самое. Что-то похожее делалось в SysV XTI (конкурент BSD sockets): в универсальный объект сокета складывались специальными ioctlʼями модули: IPv4, потом TCP, потом какой-нибудь фильтр... И что вы думаете - тоже умерло, остался BSD sockets.

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

                    В которых не будет реализовано 99% того, что нужно. Функциональность упадёт, совместимость разрушится в обе стороны. Или опять же всё свалят на библиотеки. Вангую, что этим и закончится, после многолетнего бардака.

                    Спасибо, не надо нам такого "счастья".


    1. TastaBlud
      08.09.2025 10:22

      Мне вот кстати не нравится эта абстракция на уровне системных вызовов

      Проще понять, что системный вызов - это всего лишь вызов функции (ядра), а аргументы не обязаны быть одинаковыми. Другими словами, это сигнал с данными. В остальном, действительно, унификация не нужна, поскольку требуются специфичные для каждого типа данные. Однако, схожесть сильно упрощает жизнь. Вообразите, например, что в С-функцию fread мы передаем сначала имя файла, затем режим чтения, а для записи fwrite - наоборот, сначала атрибуты, потом данные, а уж потом имя. Да Вы бы проклинали создателя такого API.

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


  1. Panzerschrek
    08.09.2025 10:22

    Абстракция "файл" на самом деле весьма дырявая. Есть каналы, разрешающие только последовательное чтение без seek. Есть нормальные файлы с произвольным доступом, которые из-за подобной абстракции несколько неудобно читать (нужно вызывать отдельно seek и read). Есть TCP сокеты, которые вроде бы как каналы, но которые имеют существенные сетевые задержки и могут отваливаться. Есть UDP сокеты, которые вообще на уровне отдельных сообщений работают, так что два последовательных write не аналогичны одному комбинированному write (с read так же). А есть ещё proc файлы, которые вовсе и не файлы, и вызов операций чтения из них это вызов к произвольному коду ядра.


    1. zpnst Автор
      08.09.2025 10:22

      Согласен с вами! Спасибо за интересный комментарий. У того же API сокетов есть более специализированная recv вместо read, но факт остаётся фактом, да


    1. akakoychenko
      08.09.2025 10:22

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

      А, с другой, как будто бы, такой подход побуждает появление неочевидных ошибок где-то в глубине стека. Например, запустили сервис, подставив адрес фтп, как путь к файлу для логирования (при том, что автор сервиса вообще держал в голове только текстовый файл и stdout при написании). Работать будет, ибо ничего ж криминального не сделано. Но однажды процесс начнёт получать абсолютно непредсказуемые ошибки, которые при этом ещё и никто никогда не сможет прочесть, потому, что они не залоггируются


    1. inkelyad
      08.09.2025 10:22

      Есть TCP сокеты, которые вроде бы как каналы, но которые имеют существенные сетевые задержки и могут отваливаться.

      "Обычные" файлы - тоже могут. Когда на сетевой файловой системе находятся, например. И так со всем остальным. Абстракция действительно дырявая, но в другую сторону. Т.е. 'обычные' файлы могут обладают особенностями, о которых никто не думает и не предполагает.


  1. netch80
    08.09.2025 10:22

    Процесс ��аки не файл. Вы не можете "открыть" процесс и иметь дескриптор на него. То, что в /proc, может исчезнуть на ходу, дав вам обгон (race) при попытке что-то сделать с этим процессом.
    А вот в Windows это решили: их аналог kill() это таки открыть процесс и уже по хэндлу что-то послать ему.

    Интернет это не файл. Это в Plan9 таки довели идею "всё есть файл" почти во всём, дав возможность, например, open("/net/tcp/1.2.3.4:443") (или похоже), там Интернет, по факту, файловый каталог:)

    Увы. То, что через дескрипторы просто передаются почти произвольные ресурсы, это ещё не всё, что нужно.


    1. zpnst Автор
      08.09.2025 10:22

      Разумеется, в случае с /proc - это прямое обращение к ядру. Но на самом тривиальном уровне мы же обращаемся функциями echo и cat с cd… да и называется /proc procFS’ом


      1. randomsimplenumber
        08.09.2025 10:22

        Если оно выглядит как файл, и читается как файл - значит файл. А файловая система необычная, да.


  1. Gbor
    08.09.2025 10:22

    Все есть данные и алгоритмы. При этом алгоритм это тоже частный случай данных.


    1. zpnst Автор
      08.09.2025 10:22

      Всё есть информация! :) Да и в контексте современной физики тоже


      1. steb
        08.09.2025 10:22

        В пределе всё-же: всё есть триединство материи-информации-меры.
        В технических примерах:

        1. памяти в компьютере не существует без железа ��� материального воплощения:
          • будь то древняя перфокарта — вещественный объект — картонка с отверстиями, у которой положение и наличие отверстий кодируют некоторую информацию;
          • либо современный SSD — сам вещественный, но хранящий информацию в виде определённых состояний электромагнитных полей в нём.

        2. хранящееся в памяти записано на каком-либо языке, в смысле способа сопоставления состояния носителя со смыслами/образами. Язык — частный случай меры.

        3. записанное может отражать:
          • либо некоторые данные о мире (информацию/образы, закодированные на некоторым языке);
          • либо выраженный на некотором языке алгоритм. При этом сам алгоритм — частный случай меры. А запись алгоритма на каком-либо языке — информация.

        Процессор компьютера опять же объект триединства… управляемо воплощающий процесс преобразования материи по [заданной] мере.


  1. nikolayErlikh
    08.09.2025 10:22

    "Программа - это текстовый файл".

    Стоит ли дальше читать!?


    1. zpnst Автор
      08.09.2025 10:22

      А что не так? Честно не понимаю. Да, программа - это текстовый файл. Просто программой можно назвать как и готовый исполняемый файл, так и исходный код. Тут просто терминология, любой читатель Хабра точно поймет о чём речь :)


    1. zpnst Автор
      08.09.2025 10:22

      Работают же «программистами», программируют они «программы», не ELF’ы же руками заполняют, а пишут исходных код программ на языках программирования. Тогда почему не «исходные кодеры» или «кодеры»?)

      Просто терминология, до которой всегда легко «докопаться»


    1. DanielJ
      08.09.2025 10:22

      Вам не стоит. А я с удовольствием почитал.


  1. Ilirium
    08.09.2025 10:22

    Ну не все файл :) Например, окно, это не файл. Символ на экране – это не файл. Точка, линия, кадр и т.п. – это не файлы. И так далее.


  1. zuek
    08.09.2025 10:22

    Дочитав до "статуй свободы весом в 1 ТБ", полез искать тэг "перевод", и с удивлением такого не обнаружил...


    1. zpnst Автор
      08.09.2025 10:22

      Родина-мать весит 8000 тонн, не подошла бы:( А я хотел!


      1. zuek
        08.09.2025 10:22

        Ну, можно было "в Шуховских телебашнях" измерить, или, на худой конец, в плавательных бассейнах ;-) Просто, вот, статуя свободы для меня - абсолютнейшая абстракция.


  1. aragont
    08.09.2025 10:22

    Про процессы написано, что это папки с кучей файлов,, многие из которых имеют нетривиальную структуру. Так про что угодно можно сказать, что оно файл. Статья про слой Virtual FS в ядре, а уж что поверх этого довольно бедного интерфейса можно наворотить - это дело фантазии программистов. Как системных, так и прикладных - через fuse.


  1. diderevyagin
    08.09.2025 10:22

    Очень хорошая статья, спасибо большое !


  1. Reternos
    08.09.2025 10:22

    Интересная статья, я узнал много нового


  1. placidity_master
    08.09.2025 10:22

    Программа – текстовый файл, который содержит в себе код на каком-либо языке программирования

    где то тихо заплакал здравый смысл.....

    как такой шлак набирает такой рейтинг?

    кто все эти люди наставившие 130 плюсов?

    Операционная система – ядро и стандартные пользовательские приложения

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

    Модуль ядра – программа, которая динамически подгружается в ядро для расширения его функционала

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

    Драйвер – программа, которая абстрагирует прикладного программиста/программу от низкоуровневого взаимодействия с железом и предоставляет удобный интерфейс взаимодействия с ним.

    а драйвер файловой системы ?....

    хоть кто то видит что статью писал двоечник, совсем оторванный от реальности и практики ?


    1. TastaBlud
      08.09.2025 10:22

      Да, необходимо в голове "переводить" с русского на русский.

      Программа – текстовый файл, который содержит в себе код на каком-либо языке программирования

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

      Операционная система – ядро и стандартные пользовательские приложения

      Я так понимаю, что автор говорит об уровне ядра и системном уровне (драйвера и т.д.), а вовсе не о приложениях типа текстового редактора, хотя таковой и нужен для сколь приемлемой работы с ОС.

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

      Видимо, бинарник также попал в категорию "текстовые файлы", ведь его в принципе можно открыть текстовым редактором.

      а драйвер файловой системы ?....

      А здесь всё правильно, по-моему. Драйвер именно абстрагирует ОС и освобождает от знания, что это, hdd, ssd, оптический диск или магнитная лента, представляя ту же абстракцию иерархии файлов с их атрибутами (естественно разными, но по стандарту POSIX)

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


  1. Zalechi
    08.09.2025 10:22

    Наконец я начинаю понимать семантику ВЯП, и синтаксис С мне зашел, аще на ура! ❤️


  1. svanichkin
    08.09.2025 10:22

    Всё есть файл, это скорее про Plan9


  1. eldog
    08.09.2025 10:22

    Видео или звуковая карта это тоже файл? Не понятно, как это всё работает, когда нужно не просто читать-писать, а делать это строго синхронизированно по времени. К мыши это, кстати, тоже относится.

    Утилита cat, применённая к обычному файлу, просто выводит его содержимое в терминал, то есть вызывает read() на этот файл...

    Но применив cat к файлу мыши мы ничего не увидим... до того момента, как не подвигаем мышью!

    То есть cat не "просто" применяет read к мыши, а получает от неё какие-то события?


    1. mayorovp
      08.09.2025 10:22

      Не понятно, как это всё работает, когда нужно не просто читать-писать, а делать это строго синхронизированно по времени

      А в чём, собственно, проблема вызывать операции чтения-записи в определённые моменты времени?

      То есть cat не "просто" применяет read к мыши, а получает от неё какие-то события?

      cat именно что просто применяет read к мыши, просто для мыши операция read как раз и является операцией получения событий.