Не подходит для мест, где у файла может быть много потоков (OS X) или дырки (все Unix).
А ещё "поток" как-то не предполагает свободное позиционирование в нём, изменение размера (Unix сисколл truncate() может как урезать, так и расширять, в том числе дырками), или чтение разных данных на каждой операции чтения. И это только вспомнившееся с ходу.
То, что вы предложили, это хороший типаж (trait) для доступа, когда требуется только последовательное чтение или запись, как в моём примере с grep входа с stdin. Но не для всего, что зовётся файлом.
Не пойдёт там, где в файле может быть несколько потоков (Windows, OS X) или дырки (все Unix, OS X).
И, по-моему, именно слово "файл" вводит в заблуждение
Я бы начал с вопроса, можем ли мы разделить понятия файла на диске, файла как сущности виртуального дерева и файла как объекта, адресуемого дескриптором - разделить (и дать разные названия) без того, чтобы отправить мозговые крыши большинства ITшников в далёкие тёплые края. И вот тут у меня начинают вырастать колоссальные сомнения.
А раз реализация различна, то и должен быть способ через системные вызовы обратиться к какой-то конкретной реализации.
Он есть. socket, bind, connect, {set|get}sockopt параметризуются. Остальным - не нужно.
Наворачивание абстракций над этими двумя версиями протокола только усложняют реализацию - требуют в ядре или if городить, или вызовы через указатели, или как-то ещё.
В ядре это делают один раз и опытные разработчики. Если это делать в юзерленде, придётся делать каждому отдельно. Концентрация ошибок подымется в сотни раз.
Или же это закончится тем, что будет универсальная библиотека уже в юзерленде, которая будет делать то же самое. Что-то похожее делалось в SysV XTI (конкурент BSD sockets): в универсальный объект сокета складывались специальными ioctlʼями модули: IPv4, потом TCP, потом какой-нибудь фильтр... И что вы думаете - тоже умерло, остался BSD sockets.
Поэтому я предлагаю от них избавиться, имея различные API для различного функционала, а абстракции реализовывать уже в коде прикладной программы.
В которых не будет реализовано 99% того, что нужно. Функциональность упадёт, совместимость разрушится в обе стороны. Или опять же всё свалят на библиотеки. Вангую, что этим и закончится, после многолетнего бардака.
Почему различать IPv4 и IPv6 - перебор? Пользовательская программа использует или одно, или другое.
Потому что у них одинаково всё, кроме адресов. И то, адреса пересекаются (в Linux v6 слушающий сокет автоматом цепляется и на v4, если не отменить). Когда 99.99+% функциональности одинаково, разделение становится искусственным.
Ну а в BSD sockets это различие загнали под одно поле параметра функций bind() и connect(), избавившись от необходимости дублирования всего остального.
Условно говоря есть классы IPv4Stream и IPv6Stream, реализующие на основе разных системных API интерфейс вроде IAbstractTCPStream.
И различие исчезает с момента коннекта.
Я начинаю недоумевать, как вы программируете с таким нежеланием понимать и поддерживать абстракции.
Ваша идея понятна. Но принять я её такую не могу: просто нет смысла усложнять на ровном месте то, что делается проще и универсально - и, главное, с возможностью развития.
Сравните это с ООП. Какой бы вариант его ни был, он старается поддерживать LSP, который есть принцип совместимости интерфейсов и определённых ожиданий контракта от прочих участников. С ним я могу применять любую реализацию того, что отвечает интерфейсу и контракту. Вы же требуете определённой фиксированной реализации. Зато на шелл ложится вся ноша разбора, как писать в файл, как в сокет... а где граница? Почему в сокет, который связан с сервером в локалке, и сервером на другом континенте, пишем одинаково? Там ведь тоже различие в деталях, типа обычной round-trip задержки, которая кому-то существенна.
Вы не разрешаете абстрагироваться от излишнего. Индустрия уже показала свой выбор.
Если из него можно прочитать данные батареи - значит, он их хранит.
Нет, хранит не он. Он их просто представляет в некотором удобном для чего-то виде. Пусть рядом будут uevent_json и uevent_cbor, где то же самое, но в другом формате; они меняться будут синхронно.
И, конечно же, он расположен за пределами адресного пространства процесса.
Почему? В том же адресном пространстве - по крайней мере на x86 и ARM. Просто юзерленду недоступен именно как содержимое памяти, ещё и представлен в каких-то запутанных структурах.
Видите, вот стали чуть внимательнее смотреть в детали - и всё определение поплыло.
Да, file descriptor это больше descriptor чем file.
Но его продолжают именовать file descriptor. А не, как в Windows, "handle" неважно чего.
Меня уже подводит память, но кажется, что основная дискуссия происходила лет на 5 раньше. Это уже повторная, в сообщениях есть явное упоминание про это. Я пытался искать и не нашёл. Но гугл хранит не все сообщения, или даёт плохой поиск по ним.
Тогда это противоречит тому, что 1) "file descriptor" является стандартным понятием, даже если дескриптор ссылается на signalfd, epoll и тому подобные служебные сущности, 2) какое-нибудь /sys/class/power_supply/BAT1/uevent для файлового API это обычный файл, но он ничего не хранит и не находится за пределами адресного пространства, а просто даёт читать данные батареи.
Увы, у меня нет архивов, а там в дискуссии всё это обсосали и в итоге пришли к описанному выводу. Мы можем попытаться повторить тут, вот вам вверху первое возражение:)
Я как раз частично поддерживаю, считая, что read() для SOCK_STREAM, SOCK_DGRAM и SOCK_SEQPACKET объединён зря. Там всё равно надо обрабатывать результаты совсем по-разному. Для потока - понимать, что данные поступили по порядку, буферизация в read() может быть как угодно, хоть по байту, хоть по миллиону байт, а возврат 0 обозначает закрытие входа. Для SOCK_DGRAM надо самому контролировать повторы и потери, буфер должен позволять полную датаграмму, а возврат 0 может быть нормальной ситуацией пустой датаграммы (UDP позволяет!) Для SOCK_SEQPACKET промежуточное между ними. Ещё и ancillary data у сокетов усложняет картину. То есть это действительно разные операции, объединение их под одной крышей достаточно насильственно.
А вот различать в этом случае IPv4, IPv6, IPX, локальные сокеты, пайпы - это уже точно перебор.
Во всяких утилитах вроде 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 только как с каналом - через соответствующий интерфейс
Да. Но это уже её выбор, а не то, что ей назначил шелл.
Условно говоря, если я читаю файлы, мне API сокетов не интересны.
Совсем не условно говоря, если у вас код какого-нибудь grep, sort и ещё десятки всяких, ему нужен API ввода или вывода потока байтов. При этом ему пофиг, как выглядит источник или приёмник потока байтов, пока он поток: это файл, терминал, безымянный пайп, именованный пайп, сокет пространства файлов, сокет TCP, псевдофайл /proc или /sys - работа с ними абсолютно одинакова. И это хорошо тем, что для простых задач применяются простые обобщённые решения.
Вот когда переходим к сложным задачам - программа сама открывает объекты, поднимает соединения, делает сама восстановление после ошибок при этом - и средства усложняются.
То есть в Unix при этом оптимальна, говоря терминами лингвистов, learning curve. Простые запросы - простые методы, сложные запросы - возможность применить сложные методы.
А вот так как было в OS/360 или есть местами в Windows, что даже для простой задачи надо навернуть что-то на порядок более замороченное - приводит к опусканию рук и хлопанью дверьми, или к нелепейшим решениям для простых задач только потому, что голова пухнет от нафиг не нужных подробностей.
даже несколько проще будет, ибо не нужно будет реализовывать диспатчинг в зависимости от фактического типа объекта.
Эта разница настолько мала, что не заслуживает внимания.
Не лучше ли иметь несколько отдельных системных вызовов для чтения из различных типов источников?
Это проходили. Например, в 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, по факту, объединены в одном интерфейсе, хоть и они и разные по сути. Вот это уже та часть легаси, которую неплохо бы исправить.
Ещё в фидошные времена в ru.os.cmp пытались получить определение файла и нашли, что в разных ОС настолько отличаются критерии определения "файл", и разные псевдофайловые системы типа /proc настолько усложняют картину, что общего определения просто нет! Есть несколько вариантов, подходящих для конкретных ОС, но не для всех одновременно. Это несмотря на то, что интуитивно мы (ну кто прошёл начальный этап) всё понимаем.
Отсюда и пункт в позднее сформированном "постмодернистском" FAQ:
Процесс таки не файл. Вы не можете "открыть" процесс и иметь дескриптор на него. То, что в /proc, может исчезнуть на ходу, дав вам обгон (race) при попытке что-то сделать с этим процессом. А вот в Windows это решили: их аналог kill() это таки открыть процесс и уже по хэндлу что-то послать ему.
Интернет это не файл. Это в Plan9 таки довели идею "всё есть файл" почти во всём, дав возможность, например, open("/net/tcp/1.2.3.4:443") (или похоже), там Интернет, по факту, файловый каталог:)
Увы. То, что через дескрипторы просто передаются почти произвольные ресурсы, это ещё не всё, что нужно.
Формально - да, можно. На практике - вы при восстановлении будете должны очень много вещей вычислить и подставить самому: например, что функция должна зваться CreatePlan(), а не sub_4D6B034F, как именно формировались данные (.ascii "Hello", а не .db 0x48, 0x65, 0x6c, 0x6c, 0x6f), и так далее. Это называется реверс-инжинирингом и это отдельный серьёзный навык.
У меня на одной из прошлых работ был любитель MS Natural. Как-то он привёз новую и распаковал, я опробовал. Я не понял, как вообще на этом можно работать. Клавиши из такого свежего скрипят и заедают. У предыдущей было то же самое, я бы там понял на б/у, но не на новой же...
Не подходит для мест, где у файла может быть много потоков (OS X) или дырки (все Unix).
А ещё "поток" как-то не предполагает свободное позиционирование в нём, изменение размера (Unix сисколл truncate() может как урезать, так и расширять, в том числе дырками), или чтение разных данных на каждой операции чтения. И это только вспомнившееся с ходу.
То, что вы предложили, это хороший типаж (trait) для доступа, когда требуется только последовательное чтение или запись, как в моём примере с grep входа с stdin. Но не для всего, что зовётся файлом.
Не пойдёт там, где в файле может быть несколько потоков (Windows, OS X) или дырки (все Unix, OS X).
Я бы начал с вопроса, можем ли мы разделить понятия файла на диске, файла как сущности виртуального дерева и файла как объекта, адресуемого дескриптором - разделить (и дать разные названия) без того, чтобы отправить мозговые крыши большинства ITшников в далёкие тёплые края. И вот тут у меня начинают вырастать колоссальные сомнения.
Он есть. socket, bind, connect, {set|get}sockopt параметризуются. Остальным - не нужно.
В ядре это делают один раз и опытные разработчики. Если это делать в юзерленде, придётся делать каждому отдельно. Концентрация ошибок подымется в сотни раз.
Или же это закончится тем, что будет универсальная библиотека уже в юзерленде, которая будет делать то же самое. Что-то похожее делалось в SysV XTI (конкурент BSD sockets): в универсальный объект сокета складывались специальными ioctlʼями модули: IPv4, потом TCP, потом какой-нибудь фильтр... И что вы думаете - тоже умерло, остался BSD sockets.
В которых не будет реализовано 99% того, что нужно. Функциональность упадёт, совместимость разрушится в обе стороны. Или опять же всё свалят на библиотеки. Вангую, что этим и закончится, после многолетнего бардака.
Спасибо, не надо нам такого "счастья".
Потому что у них одинаково всё, кроме адресов. И то, адреса пересекаются (в Linux v6 слушающий сокет автоматом цепляется и на v4, если не отменить). Когда 99.99+% функциональности одинаково, разделение становится искусственным.
Ну а в BSD sockets это различие загнали под одно поле параметра функций bind() и connect(), избавившись от необходимости дублирования всего остального.
И различие исчезает с момента коннекта.
Я начинаю недоумевать, как вы программируете с таким нежеланием понимать и поддерживать абстракции.
Ваша идея понятна. Но принять я её такую не могу: просто нет смысла усложнять на ровном месте то, что делается проще и универсально - и, главное, с возможностью развития.
Сравните это с ООП. Какой бы вариант его ни был, он старается поддерживать LSP, который есть принцип совместимости интерфейсов и определённых ожиданий контракта от прочих участников. С ним я могу применять любую реализацию того, что отвечает интерфейсу и контракту. Вы же требуете определённой фиксированной реализации. Зато на шелл ложится вся ноша разбора, как писать в файл, как в сокет... а где граница? Почему в сокет, который связан с сервером в локалке, и сервером на другом континенте, пишем одинаково? Там ведь тоже различие в деталях, типа обычной round-trip задержки, которая кому-то существенна.
Вы не разрешаете абстрагироваться от излишнего. Индустрия уже показала свой выбор.
Пишется номер начального кластера. Это подходит под критерий уникальности.
Нет, хранит не он. Он их просто представляет в некотором удобном для чего-то виде. Пусть рядом будут uevent_json и uevent_cbor, где то же самое, но в другом формате; они меняться будут синхронно.
Почему? В том же адресном пространстве - по крайней мере на x86 и ARM. Просто юзерленду недоступен именно как содержимое памяти, ещё и представлен в каких-то запутанных структурах.
Видите, вот стали чуть внимательнее смотреть в детали - и всё определение поплыло.
Но его продолжают именовать file descriptor. А не, как в Windows, "handle" неважно чего.
Меня уже подводит память, но кажется, что основная дискуссия происходила лет на 5 раньше. Это уже повторная, в сообщениях есть явное упоминание про это. Я пытался искать и не нашёл. Но гугл хранит не все сообщения, или даёт плохой поиск по ним.
Но на эти тоже можно частично опереться.
Тогда это противоречит тому, что 1) "file descriptor" является стандартным понятием, даже если дескриптор ссылается на signalfd, epoll и тому подобные служебные сущности, 2) какое-нибудь /sys/class/power_supply/BAT1/uevent для файлового API это обычный файл, но он ничего не хранит и не находится за пределами адресного пространства, а просто даёт читать данные батареи.
Увы, у меня нет архивов, а там в дискуссии всё это обсосали и в итоге пришли к описанному выводу. Мы можем попытаться повторить тут, вот вам вверху первое возражение:)
Вот это уже явная тупизна с их стороны - назначение RAlt как Level3 modifier не мешало самостоятельному использованию Alt+Shift.
Я как раз частично поддерживаю, считая, что read() для SOCK_STREAM, SOCK_DGRAM и SOCK_SEQPACKET объединён зря. Там всё равно надо обрабатывать результаты совсем по-разному. Для потока - понимать, что данные поступили по порядку, буферизация в read() может быть как угодно, хоть по байту, хоть по миллиону байт, а возврат 0 обозначает закрытие входа. Для SOCK_DGRAM надо самому контролировать повторы и потери, буфер должен позволять полную датаграмму, а возврат 0 может быть нормальной ситуацией пустой датаграммы (UDP позволяет!) Для SOCK_SEQPACKET промежуточное между ними. Ещё и ancillary data у сокетов усложняет картину. То есть это действительно разные операции, объединение их под одной крышей достаточно насильственно.
А вот различать в этом случае IPv4, IPv6, IPX, локальные сокеты, пайпы - это уже точно перебор.
Нет. Это может быть, повторяю, что угодно. Их сама утилита использует как односторонние каналы, но им ничто не мешает быть произвольными ядерными сущностями, которые просто умеют в read() или write().
Сам по себе stdin может быть терминалом - для тех программ, которым это нужно. Тот же bash, если интерактивный режим, использует свои stdin и stdout именно как терминал: настраивает политику raw режима, уточняет его тип, принимает/передаёт нужные esc-последовательности. (Если неинтерактивный, скрипт - ему пофиг, работает точно так же как grep, со входом и выходом.) И тут тоже, терминал может быть настоящий железный, может быть виртуальный от юниксовой консоли, может быть псевдотерминал (сейчас в 99+% случаев), может быть что-то другое, прикидывающееся терминалом - программе пофиг, пока сущность, спрятанная за дескриптором, поддерживает tcgetattr(), tcsetattr(), tcflush() и тому подобные вызовы.
А может и не перенаправлять. При вызове без "|" он оставляет то, что ему самому поступило как дескрипторы 0 или 1. Или команду может запускать вообще не bash, а отдельная программа, давая на вход файлы или сама порождая пайпы. И снова для самой утилиты просто есть нечто на дескрипторе 0, которое как положено отрабатывает read(), и на 1 - write(). Такое себе ООП.
Кажется, вы никогда не интересовались тем, что там происходит внутри.
Да. Но это уже её выбор, а не то, что ей назначил шелл.
Совсем не условно говоря, если у вас код какого-нибудь grep, sort и ещё десятки всяких, ему нужен API ввода или вывода потока байтов. При этом ему пофиг, как выглядит источник или приёмник потока байтов, пока он поток: это файл, терминал, безымянный пайп, именованный пайп, сокет пространства файлов, сокет TCP, псевдофайл /proc или /sys - работа с ними абсолютно одинакова. И это хорошо тем, что для простых задач применяются простые обобщённые решения.
Вот когда переходим к сложным задачам - программа сама открывает объекты, поднимает соединения, делает сама восстановление после ошибок при этом - и средства усложняются.
То есть в Unix при этом оптимальна, говоря терминами лингвистов, learning curve. Простые запросы - простые методы, сложные запросы - возможность применить сложные методы.
А вот так как было в OS/360 или есть местами в Windows, что даже для простой задачи надо навернуть что-то на порядок более замороченное - приводит к опусканию рук и хлопанью дверьми, или к нелепейшим решениям для простых задач только потому, что голова пухнет от нафиг не нужных подробностей.
Эта разница настолько мала, что не заслуживает внимания.
Это проходили. Например, в 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, по факту, объединены в одном интерфейсе, хоть и они и разные по сути. Вот это уже та часть легаси, которую неплохо бы исправить.
Ещё в фидошные времена в ru.os.cmp пытались получить определение файла и нашли, что в разных ОС настолько отличаются критерии определения "файл", и разные псевдофайловые системы типа /proc настолько усложняют картину, что общего определения просто нет! Есть несколько вариантов, подходящих для конкретных ОС, но не для всех одновременно. Это несмотря на то, что интуитивно мы (ну кто прошёл начальный этап) всё понимаем.
Отсюда и пункт в позднее сформированном "постмодернистском" FAQ:
Q48: что такое файл?
A48: ОПЯТЬ?
Процесс таки не файл. Вы не можете "открыть" процесс и иметь дескриптор на него. То, что в /proc, может исчезнуть на ходу, дав вам обгон (race) при попытке что-то сделать с этим процессом.
А вот в Windows это решили: их аналог kill() это таки открыть процесс и уже по хэндлу что-то послать ему.
Интернет это не файл. Это в Plan9 таки довели идею "всё есть файл" почти во всём, дав возможность, например, open("/net/tcp/1.2.3.4:443") (или похоже), там Интернет, по факту, файловый каталог:)
Увы. То, что через дескрипторы просто передаются почти произвольные ресурсы, это ещё не всё, что нужно.
Текст и данные - об этом постоянно забывают.
Формально - да, можно. На практике - вы при восстановлении будете должны очень много вещей вычислить и подставить самому: например, что функция должна зваться CreatePlan(), а не sub_4D6B034F, как именно формировались данные (.ascii "Hello", а не .db 0x48, 0x65, 0x6c, 0x6c, 0x6f), и так далее. Это называется реверс-инжинирингом и это отдельный серьёзный навык.
Подтверждаю для Asus ROG Strix G16.
У меня на одной из прошлых работ был любитель MS Natural. Как-то он привёз новую и распаковал, я опробовал. Я не понял, как вообще на этом можно работать. Клавиши из такого свежего скрипят и заедают. У предыдущей было то же самое, я бы там понял на б/у, но не на новой же...