Русский
Русский
English
Статистика
Реклама

Unix

Перевод Заметки о Unix системный вызов write(), на самом деле, не такой уж и атомарный

28.01.2021 12:17:06 | Автор: admin


Недавно я читал материал Эвана Джонса Устойчивое хранение данных и файловые API Linux. Я полагаю, что автор этой довольно хорошей статьи ошибается, говоря о том, чего можно ожидать от команды write() (и в том виде, в каком она описана в стандарте POSIX, и на практике). Начну с цитаты из статьи:

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

Означает ли это, что операция write() является атомарной? С технической точки зрения да. Операции чтения данных должны возвращать либо всё, либо ничего из того, что было записано с помощью write(). [].

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

После того как произошёл успешный возврат из операции записи (write()) в обычный файл:

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

Это не требует какого-то особого поведения от команд чтения данных из файла, запущенных другим процессом до возврата из команды записи (включая те, которые начались до начала работы write()). Если выполнить подобную команду read(), POSIX позволяет этой команде вовсе не прочитать данные, записываемые write(), прочитать лишь некоторую часть этих данных, или прочитать их все. Подобная команда read() (теоретически) является атомарной в том случае, если её вызывают из другого потока того же самого процесса. При таком подходе, определённо, не реализуется обычная, привычная всем, атомарная схема работы, когда либо видны все результаты работы некоей команды, либо результаты её работы не видны вовсе. Так как разрешено кросс-процессное выполнение команды read() во время выполнения команды write(), выполнение команды чтения может привести к возврату частичного результата работы команды записи. Мы не назвали бы атомарной SQL-базу данных, которая позволяет прочитать результаты незавершённой транзакции. Но именно это стандарт POSIX позволяет команде write(), с помощью которой выполняется запись в файлы.

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

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

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

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

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

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

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

(Большинство программ не выполняют операции read() и write() над одним и тем же файлом в одно и то же время из двух потоков.)

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

P.P.S. Я с осторожностью относился бы к принятию как данности того, что в некоем варианте Unix, в многопоточной среде, полностью реализованы атомарные операции read() и write(). Возможно, я настроен скептически, но я бы сначала это как следует проверил. Всё это похоже на излишне прихотливые требования POSIX, на которые разработчики систем закрывают глаза во имя простоты и высокой производительности своих решений.

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

Подробнее..

Перевод Заметки о Unix как команда newgrp работала в Unix V7?

13.02.2021 16:10:43 | Автор: admin
В материале, посвящённом причинам существования команды newgrp, мы узнали о том, что группам в Unix можно назначать пароли, о том, что эта команда позволяет пользователю менять свою (основную) группу. Мы выяснили, что эта команда появилась в Unix V6, что гораздо раньше, чем я ожидал. Меня тогда заинтересовал вопрос о том, как именно работала команда newgrp в Unix V7. Исходный код V7 (а так же справку и другие материалы) можно найти на tuhs.org. Поэтому ничто не мешает нам почитать код реализации этой команды, находящийся в файле newgrp.c.



(Можно посмотреть и исходный код, имеющий отношение к V6, но он гораздо сложнее, поэтому я буду рассматривать код V7. Помимо того, что V7 обычно считают системой, с которой началась современная Unix, всё, что было сделано до V7, выглядит иначе, и, как минимум, кажется немного странным.)

Я полагал, что newgrp работает с группами, защищёнными паролями, примерно так: кто угодно может выполнить команду наподобие newgrp GROUP, и если ему известен пароль, он будет помещён в группу без необходимости указания его в качестве (потенциального) члена группы в /etc/group. Возможно, именно так работает современная версия newgrp, доступная в вашем дистрибутиве. Но в V7 эта команда работала иначе. Начнём с извлечения из справки (newgrp.1):

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

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

Во-первых, если только вы не являетесь членом специальной группы other, вы должны быть записаны в /etc/group независимо от того, назначен ли группе пароль или нет. Затем, как сказано в справке, пароль группы требуется лишь в том случае, если у вашей учётной записи пароля нет. Если же у учётной записи есть пароль, то пароль группы игнорируется. Другими словами, можно не использовать пароль группы для того чтобы иметь возможность предоставить кому угодно, знающему такой пароль, потенциальную возможность доступа к группе. Пароль группы используется только для защиты группы от входа в неё пользователей с аккаунтами без паролей (если такие пользователи в системе присутствуют и если они внесены в дополнительную группу).

При работе с группами, которым назначен пароль, обычная Linux-версия newgrp ведёт себя с такими группами, членом которых вы являетесь, так же, как и V7-версия команды. Но она ещё и позволяет входить в любые группы, для которых задан пароль, в том случае, если этот пароль вам известен (так сказано в справке, я это не проверял). Во FreeBSD, к которой у меня есть доступ, пользоваться паролями групп не рекомендуется, в справке говорится, что newgrp обычно устанавливается без бита setuid. Ещё там сказано, что пароль запрашивается только тогда, когда пользователь не входит в число членов группы.

(Я полагаю, что это значит, что в стандартной установке FreeBSD пользователь не может использовать newgrp для перехода в группу, в которую он был добавлен через /etc/group.)

Возможно, вас заинтересовало последнее предложение из процитированной мной справки по V7-версии newgrp. Причина наличия этой особенности заключается в том, чтобы исключить существование дополнительных процессов командной оболочки, которые, вероятно, пользователю не нужны (это привело бы к использованию дополнительных системных ресурсов на маломощных машинах, на которых работала V7). С концептуальной точки зрения, если выполняют команду newgrp, то обычно стремятся к тому, чтобы переключить текущую сессию командной оболочки на новую группу. Если оболочка просто выполнит newgrp как обычную команду, то будет осуществлено переключение групп для её собственного процесса, а затем будет выполнена программа /bin/sh, дающая пользователю новую оболочку в новой группе. Это было бы похоже на то, как если бы пользователь выполнил бы команду sh в login-оболочке. Если при таком подходе выполнить несколько команд newgrp, то всё закончится тем, что у пользователя будет целая куча оболочек.

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

(Тут всё самое интересное кроется в комбинации msg.c и xec.c; взгляните на обработку SYSLOGIN).

Исследуете ли вы старые дистрибутивы Unix для того чтобы лучше понять современные системы?
Подробнее..

Перевод Заметки о Unix история Unix до readline

21.02.2021 12:18:25 | Автор: admin
Unix и программы, работающие в этой ОС, существуют уже очень давно. В частности, библиотека GNU Readline появилась в 1989 году (как и Bash). Времени существования этой библиотеки (и подобных проектов) вполне достаточно для того чтобы она стала бы распространённым инструментов Unix-оболочек. В наши дни совершенно естественно воспринимать readline как нечто такое, что всегда было в Unix. Но, конечно, на самом деле это не так. Unix в её современном виде ведёт историю от V7 (1979) и 4.2. BSD (1983), поэтому множество Unix-дистрибутивов было разработано до появления readline. Это, в некоторой степени, сделало их такими, какими они были.



(Нельзя сказать, что GNU Readline и Bash были первоисточниками возможностей редактирования и автодополнения команд, да и прочего подобного в стиле readline. В Unix похожие методы работы восходят, как минимум, к 1983 году, к tcsh. Но оболочка tcsh, по разным причинам, не получила широкого распространения.)

Одним из очевидных последствий отсутствия readline было появление командной оболочки csh. Эта оболочка поддерживала множество инструментов для работы с историей команд. Их работа была основана на внедрение в командную строку особых строк. Обратимся к справке по csh из 4.2. BSD:

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

Пожалуй, самой известной среди пользователей tcsh командой подстановки из истории является !!, которая повторяет предыдущую команду. В Bash есть похожий механизм, cf, и даже сегодня в руководстве по Bash есть указание на сходство cf с csh-версией соответствующего инструмента. Полагаю, что в наше время большинство тех, кто использует Bash, не применяет механизмы Bash для работы с историей команд, а просто прибегает к readline-инструментам, с которыми обычно проще работать.

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

P.S. У меня возникает такое ощущение, что широкая распространённость механизмов автодополнения команд и имён файлов понемногу влияет на то, какие имена команд и файлов применяют пользователи. Во времена, когда автодополнения не было, очень важным было то, чтобы имена команд и файлов были бы как можно короче. Неважно было то, что они могли быть очень похожи друг на друга, что могло бы привести к тому, что системе автозавершения ввода было бы сложно их различать. Как известно, короткие имена команд пользуются популярностью в Unix. Такие команды можно вводить быстрее, чем более длинные. В окружении V7 это имеет большое значение.

Пользуетесь ли вы tcsh?
Подробнее..

Перевод Заметки о Unix С-функция main() одно из мест, где видны различия между API пользовательского пространства и ядра Unix

03.03.2021 12:06:10 | Автор: admin
В современных Unix-дистрибутивах часто проводят формальную границу между API, предоставляемыми пользовательскому пространству ядром, и Unix API, которые предоставляет программам стандартная библиотека, под которой подразумевается стандартная библиотека C. Кое-кого, включая меня, это не вполне устраивает (я уже писал на эту тему). Но, независимо от того, что я об этом думаю, в Unix уже давно существует одно место, в котором видна разница между обычным API, которым пользуются все, и API, который реализован в ядре. Я говорю о традиционной точке входа в программы, написанных на языке C, о функции main(), с которой начинается выполнение таких программ.



Все знакомы с простой формой функции main(), в которой используются аргументы argc и argv. Такая функция вызывается с передачей ей количества аргументов и массива строк. При несколько более продвинутом способе работы с этой функцией применяется ещё и третий аргумент envp. Он представляет собой массив переменных окружения. Этот формат существует в Linux очень давно. Версия main() с двумя аргументами существует, как минимум, со времён exec(2) Research Unix V4. А форма этой функции с третьим аргументом, похоже, появилась в exec(2) V7.

Но это, на самом деле, не реальная точка входа в программу, которую ядро Unix V7 использует при запуске программы. Реальная точка входа имеет API, отличный от main(). Обычно C-программы в V7 начинают работу с метки, имеющей символическое имя start. Самая простая версия ассемблерного кода, в котором это используется, представлена в файле crt0.s, и тут, очевидно, выполняется некий объём подготовительной работы. Есть и другие версии подобного кода, их можно найти здесь. Тут выполняется больше вспомогательных операций, например подготовка к профилированию кода.

(В Research Unix V6 тоже был файл crt0.s, но несколько иной. Полагаю, тут, например, нет циклов. Если бы я понимал язык ассемблера PDP-11, то я лучше бы разобрался с тем, что тут, на самом деле, происходит.)

В V7 между API пользовательского пространства для main() и API ядра имеется лишь небольшая разница. В актуальных дистрибутивах Unix там часто происходит очень много всего, особенно тогда, когда пользуются динамическими загрузчиками и чем-то вроде вспомогательного вектора, который имеется в некоторых дистрибутивах. Я подозреваю, что самую простую современную версию этого механизма можно найти в musl libc для Linux, где crt1.c и функции libc для подготовки к работе main() сравнительно просты.

(Некоторый код тут присутствует из-за того, что среда выполнения C нуждается в предварительной настройке (и да, в современном C есть среда выполнения), но определённый объём этого кода предназначен для согласования того, как ядро вызывает программы, с тем, как хочет быть вызвана функция main(). Например, обратите внимание на то, что функция musl libc для запуска main() не вызывается с передачей ей argc в виде явно заданного аргумента. Она извлекает argc из памяти.)

Примечание: V7 и адрес данных 0


В конце каждой версии файла crt0.s V7 есть код, который поначалу меня озадачил:

.data.=.+2  / loc 0 for I/D; null ptr points here.

Оказалось, что он резервирует два байта в начале раздела данных. Unix V7 работает на компьютерах PDP-11, которые поддерживают разделение адресного пространства инструкций и данных. В результате раздел данных начинается с адреса (данных) 0. Резервирование двух байтов в начале адресного пространства позволяет обеспечить то, что ни переменную, ни что-то другое в разделе данных нельзя расположить по адресу 0. В результате NULL в C всегда отличается от действительных указателей.

Приходилось ли вам сталкиваться с различиями API пользовательского пространства и ядра Unix?

Подробнее..

Перевод Заметки о Unix исследование munmap() на нулевой странице и на свободном адресном пространстве

14.03.2021 12:14:15 | Автор: admin
Однажды на Fediverse мне попался интересный вопрос о munmap():

Чем именно занимается munmap() в Linux если адрес установлен в 0? В Linux подобный вызов каким-то образом срабатывает, а вот во FreeBSD нет. Полагаю, что всё дело в различной семантике команд, но не могу найти никаких пояснений по поводу такого поведения munmap().

(Там было ещё это дополнение, а тут находится краткая версия ответа)



Когда я увидел этот вопрос, то я реально удивился тому, что подобный вызов munmap() в Linux работоспособен, так как я ожидал, что он даст сбой на любом адресном пространстве, которое не входит в состав памяти, выделенной некоему процессу. Нулевая страница в Linux (да и в любой вменяемой системе), определённо, не входит в состав распределённой памяти. Поэтому предлагаю взглянуть на описание munmap() из документации SUS, выдержку из которого я привожу ниже.

Функция munmap() должна завершиться с ошибкой в следующих случаях:

[EINVAL]

Адреса в диапазоне [addr,addr+len) находятся за пределами допустимого диапазона адресов адресного пространства процесса.

(Похожую формулировку можно найти в справке по FreeBSD).

Когда я впервые это прочитал, я решил, что речь идёт о текущем адресном диапазоне процесса. Но, на практике, в Linux и во FreeBSD это не так, и я полагаю, что так же дело обстоит и в теории (в документации POSIX/SUS речь идёт о процессе вообще (of a process), а не о конкретном процессе (of this process)). В обеих этих Unix-системах можно применить munmap(), по меньшей мере, к некоему объёму неиспользуемого адресного пространства. Мы продемонстрируем это с помощью небольшой тестовой программы, которая отображает что-то в память с помощью mmap(), а потом дважды применяет к этому munmap().

Разница между Linux и FreeBSD заключается в том, что в этих системах понимается под нахождением памяти за пределами допустимого диапазона адресов адресного пространства процесса. Во FreeBSD, очевидно, за пределами находятся нулевая страница (и, возможно, в целом, нижняя область памяти). Обращение к такой памяти и вызывает сбой munmap(). А в Linux это не так. И хотя эта ОС обычно не разрешает применять mmap() к памяти в этой области, что делается не без причины, эта память, в силу своей природы, не находится за пределами адресного пространства. Если я правильно понял прочитанный мной код Linux, то нижние области памяти вообще никогда не считаются недопустимыми. Такими считаются лишь диапазоны адресов, выходящие за пределы верхней области памяти пользовательского пространства.

(Я мельком глянул на соответствующий FreeBSD-код из файла vm_mmap.c, и я думаю, что он отказывается выполнять команды munmap(), которые выходят за нижние или верхние пределы адресного пространства, отображение которого выполнил процесс. Это означает более серьёзные ограничения, чем я ожидал.)

В конечном итоге неудивительно то, что в OpenBSD принята несколько иная интерпретация munmap(), такая, которая ближе к тому, чего я ожидал от этой команды. Вот цитата из справки по munmap() из OpenBSD, в которой идёт речь о причинах возникновения ошибки:

[EINVAL]

Параметры addr и len указывают на область, которая простирается за пределы конца адресного пространства, или некоторая часть области, над которой пытаются выполнить операцию munmap(), не является частью текущего допустимого адресного пространства.

OpenBSD требует, чтобы команду munmap() применяли только к тому, память под что уже отображена, и не позволяет применять эту команду к произвольным областям потенциального адресного пространства даже в том случае, если эти области расположены между нижним и верхним пределами используемого адресного пространства (а FreeBSD это позволила бы). То, находится ли это в полном соответствии с POSIX, представляет собой интересный, но несущественный вопрос, так как я сомневаюсь в том, что разработчики OpenBSD будут это менять (и я не думаю, что они должны это менять).

Я, занимаясь всем этим, узнал одну интересную вещь. Оказывается, что в Linux, во FreeBSD и в OpenBSD используется нечто вроде различных интерпретаций стандарта POSIX (это при условии, что я правильно понял код ядра FreeBSD). Linux-интерпретация оказывается самой свободной, так как в ней разрешено применять munmap() ко всему, что может быть отображено в память в определённых обстоятельствах. OpenBSD, если нужно, вероятно решит, что допустимый диапазон для адресного пространства процесса это диапазон памяти, на который что-то уже отображено, в результате поведение системы соответствует POSIX/SUS, но такой подход, определённо, сдвигает интерпретацию стандарта в необычном направлении, уходя от чётких формулировок спецификации (хотя это именно то поведение, которого я и ожидал). А во FreeBSD имеется, так сказать, нечто среднее, возможно, связанное с деталями реализации системы.

P.S. В справке по munmap() из Linux даже не говорится о допустимом адресном пространстве всех процессов или некоего конкретного процесса как о причине ошибки munmap(); там есть лишь абстрактные рассуждения о том, что ядру могут не понравиться параметры addr или len.

Примечание: небольшая тестовая программа


Вот тестовая программа, которой я пользовался:

#include <sys/mman.h>#include <stdio.h>#include <errno.h>#include <string.h>#define MAPLEN (128*1024)int main(int argc, char **argv){void *mp;puts("Starting mmap and double munmap test.");mp = mmap(0, MAPLEN, PROT_READ, MAP_ANON|MAP_SHARED, -1, 0);if (mp == MAP_FAILED) {printf("mmap error: %s\n", strerror(errno));return 1;}if (munmap(mp, MAPLEN) < 0) {printf("munmap error on first unmap: %s\n", strerror(errno));return 1;}if (munmap(mp, MAPLEN) < 0) {printf("munmap error on second unmap: %s\n", strerror(errno));return 1;}puts("All calls succeeded without errors, can munmap() unmapped areas.");return 0;}

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

Встречались ли вы с различиями в поведении каких-нибудь системных вызовов в разных Unix-подобных ОС?

Подробнее..

Перевод Заметки о Unix работа с GNU grep и обязательное применение опции -a (--text)

19.03.2021 12:22:16 | Автор: admin


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

grep -hv 'a specific pattern' "$@" | exigrep '...' | [...]

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

Может, я неправильно понимаю суть опции -v? А, может быть, команда exitgrep вытворяет что-то такое, о чём я и не подозреваю? В итоге я решил обработать файл с использованием одной только команды grep, соединённой конвейером с командой less, и, пользуясь less, сразу открыл последние строки вывода, так как знал, что запись, которая в файле была, но о которой скрипт мне не сообщил, находилась где-то ближе к концу файла. Случилось то, чего я и ожидал. А именно, в определённый момент команда grep просто остановилась. Я, в частности, увидел следующее:

2020-04-13 16:07:06 H=(111iu.com) [223.165.241.9] [...]2020-04-13 16:07:07 unexpected disconnection [...]Binary file /var/log/exim4/mainlog matches

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

(Этот случай отличается от поведения grep, когда программа полагает, вводимая в заблуждение багом файловой системы и собственной интеллектуальностью, что некоторые текстовые файлы являются бинарными. То, что произошло в этот раз, объяснить гораздо проще.)

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

Из этого всего я вынес ценный урок. Заключается он в том, что каждый раз, когда я обрабатываю текстовые файлы с помощью GNU grep (а в моих скриптах, в общем-то, я всегда обрабатываю текстовые файлы именно так), мне нужно явным образом указывать программе на то, что она должна воспринимать эти файлы именно как текстовые. Это, к сожалению, приведёт к утяжелению кода некоторых скриптов, так как порой я пользуюсь конвейерами, в которых используются несколько команд grep, применяемых для фильтрации и обработки текста. В результате мне нужно либо снабдить все используемые команды grep опциями -a, либо выяснить, какая переменная окружения вида LC_ позволит, с минимальным воздействием на систему, это отключить, либо взяться за нечто такое, что можно сравнить со здоровенной кувалдой, использовав, как рекомендуется в справке по GNU grep, переменную LC_ALL=C.

P.S. Описанная здесь проблема касается не только Linux, так как GNU grep используется не только на компьютерах, работающих под управлением Linux. Тут всё зависит от того, что именно устанавливают, и что именно попадает в переменную PATH. Например, на FreeBSD-машине, к которой у меня есть доступ, GNU grep используется в виде /usr/bin/grep.

Приходилось ли вам сталкиваться с неожиданными эффектами, возникающими при использовании Unix-команд?

Подробнее..

Перевод Заметки о Unix проблема iowait и многопроцессорные системы

25.03.2021 12:22:32 | Автор: admin
В разных Unix-системах уже давно имеется показатель iowait. Я, правда, не могу найти систему, в которой этот показатель появился. Это не 4.x BSD, поэтому iowait, возможно, добрался до современных систем через System V и sar. Традиционным, стандартным определением iowait является время, которое система проводит в бездействии, когда в ней имеется хотя бы один процесс, ожидающий окончания операции дискового ввода-вывода. Вместо того чтобы относить это время к категории idle (простой процессора) (когда процессорное время делится на три категории user, system и idle), в некоторых Unix-системах это время стали относить к новой категории iowait.



(К моему удивлению оказалось, что понятия iowait, похоже, нет ни в одной *BSD-системе. Там используется старая схема user-system-idle и детализация системного времени. Iowait имеется в Linux и в Solaris/Illumos, этот показатель, если верить результатам беглого просмотра справки, есть ещё в HP-UX и в AIX.)

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

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

(Поиск ответа на вопрос о том, что такое iowait, усложняется в том случае, если используемая вами Unix-система при расчёте iowait ориентируется на отдельные процессоры, как часто бывает с категориями user, system и idle. Дело в том, что обычно ожидание результатов ввода-вывода не связано неким естественным образом с каким-то конкретным процессором. Похоже, что в illumos, если учесть то немногое, что об этом сказано в справке по mpstat, показатель iowait не рассматривается как нечто, относящееся к отдельным процессорам. А справка по sar(1) указывает на то, что в этой системе использован более общий подход к пониманию iowait.)

Пользуетесь ли вы показателем iowait при анализе производительности своих Unix-систем?

Подробнее..

Перевод Заметки о Unix одновременное редактирование нескольких файлов в Vim

02.04.2021 16:10:53 | Автор: admin
Недавно мы завершили перевод последней нашей машины на новый клиент для Lets Encrypt. В ходе работы нужно было поменять пути к выгружаемым TLS-сертификатам во всех конфигурационных файлах, где они использовались. На многих компьютерах был лишь один конфигурационный файл, но на некоторых из наших Apache-серверов пути к TLS-сертификатам имеются во множестве файлов. Поэтому я и заинтересовался вопросом о том, как, пользуясь Vim, одновременно вносить одни и те же изменения в несколько файлов. Оказалось, что Vim поддерживает такую возможность уже очень давно, причём сделать это можно несколькими способами. Некоторые из этих способов основаны на том, что я назвал бы странностью Vim. Кто-то, возможно, назовёт это архитектурной особенностью данного редактора.



Чаще всего, или, точнее, когда мне приходится редактировать лишь сравнительно небольшое количество файлов, мне легче всего воспользоваться очень удобной командой :all, которая позволяет открыть окно для каждого буфера. После этого я, для применения некоей команды к каждому буферу, пользуюсь командой :windo. Например так: :windo %s/.../.../. Потом я записываю все изменённые буферы командой :wa.

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

В vi уже давно имеется возможность работать с несколькими буферами, но этот редактор всегда очень внимательно относился к тому, чтобы, перед переходом к другому буферу, пользователь записывал бы изменения, внесённые в текущий буфер. Тут легко прослеживается связь такого поведения редактора с теми временами, когда им пользовались на маленьких, сравнительно сильно нагруженных машинах, для которых и создавался vi. Дело в том, что пустой буфер это буфер, который не надо хранить в памяти или в файле подкачки на диске (и при таком подходе никто ничем не рискует в том случае, если vi или сам компьютер вдруг даст сбой). Но в наши дни это не соответствует тому, как большинство других редакторов, поддерживающих работу с несколькими файлами, подходят к решению аналогичной задачи. Большинство таких редакторов позволяют пользователю держать открытыми сколько угодно много изменённых буферов. Такие редакторы при этом ни на что не жалуются, не говоря уже о том, чтобы заставлять пользователя сохранять один буфер перед переходом к другому, или не давать открывать новые буферы. То, что такие редакторы не вмешиваются в действия пользователя, немного облегчают работу с ними, и Vim в этом плане немного непоследователен, так как содержимое окна может быть изменено, но это не мешает пользователю переключиться на другое окно.

Если учесть мою точку зрения на происходящее, то я, возможно, решил бы включить опцию hidden. Ну, если только я был бы совершенно уверен в выполненных мной изменениях файлов. Мне не хочется добавлять | update к команде :bufdo для немедленной записи изменений. А включение опции hidden приближает поведение Vim к поведению других редакторов. Недостатки такого подхода, о которых идёт речь в документации Vim, к моей работе отношения не имеют. Я пользуюсь командами :q! или :qa! только тогда, когда совершенно уверен в том, что хочу отказаться от всех несохранённых изменений.

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

:hide bufdo %s/.../.../

Полагаю, нет идеального способа отмены последствий операции по изменению нескольких файлов, результат которой оказался не таким, как хотелось бы. Если все буферы пребывали в исходном состоянии до выполнения команд bufdo или windo, можно снова воспользоваться соответствующей командой для вызова undo в каждом из буферов (:bufdo u). При наличии неизменённых буферов это никак мне не повредит в том случае, если в ходе операции над несколькими файлами некий файл окажется неизменённым. Правда, если в некоторых буферах есть несохранённые данные, такая операция становится опасной, так как команда undo, выполняемая в каждом из буферов, довольно-таки ограничена. А именно, отменены будут только самые свежие изменения, вне зависимости от их связи с самой первой командой bufdo.

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

В целом же последовательность команд :all и :windo ..., в сравнении с другими, легче запомнить, при работе с ними легче использовать дополнительные параметры, они позволяют немедленно исследовать некоторые из изменений, внесённых во все редактируемые файлы. Поэтому я, вероятнее всего, буду пользоваться именно этим подходом при одновременном редактировании нескольких файлов. Тут нет той же красоты, которая видна в альтернативных методах, и я уверен, что блюстители чистоты Vim по этому поводу лишь невесело вздохнут, но я не отношу себя к тем, кого это заботит.

Пользуетесь ли вы Vim?
Подробнее..

Как придумали кодировку UTF-8 выдержки из переписки создателей

14.04.2021 12:16:12 | Автор: admin

Всем известна кодировка UTF-8, что давно доминирует в интернет пространстве, и которой пользуются много лет. Казалось бы, о ней все известно, и ничего интересного на эту тему не рассказать. Если почитать популярные ресурсы типа Википедии, то действительно там нет ничего необычного, разве что в английской версии кратко упоминается странная история о том, как ее набросали на салфетке в закусочной.

На самом деле изобретение этой кодировки не может быть настолько банальным хотя бы потому, что к ее созданию приложил руку Кен Томпсон легендарная личность. Он работал вместе с Деннисом Ритчи, был одним из создателей UNIX, внес вклад в разработку C (изобрел его предшественника B), а позднее, во время работы в Google, принял участие в создании языка Go.

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


Действующие лица:


ken (at) entrisphere.com Кен Томпсон

Кен Томпсон (слева) с Деннисом Ритчи

Rob 'Commander' Pike Роберт Пайк, канадский программист, работавший над UTF-8 вместе c Кеном Томпсоном


mkuhn (at) acm.org Маркус Кун, немецкий ученый в области информатики


henry (at) spsystems.net Генри Сперсер, автор одной из реализаций RegExp


Russ Cox <rsc@plan9.bell-labs.com> Русс Кокс, сотрудник Bell Labs, работавший над системой Plane 9


Greger Leijonhufvud <greger@friherr.com> Один из сотрудников X/Open


Plane 9 Операционная система, в которой впервые была использована кодировка UTF-8 для обеспечения ее мультиязычности.


UTF-8 Кодировка символов Юникода




Переписка 2003 года



Ниже переписка создателей кодировки, Роберта и Кена, которую Роберт Пайк начал, сетуя на то, что их вклад в создание UTF-8 незаслуженно забыли. Роберт просит одного из старых знакомых порыться в архивах почтового сервера и найти там доказательства их участия. (прим. пер.)

Subject: UTF-8 history
From: "Rob 'Commander' Pike" <r (at) google.com>
Date: Wed, 30 Apr 2003 22:32:32 -0700 (Thu 06:32 BST)
To: mkuhn (at) acm.org, henry (at) spsystems.net
Cc: ken (at) entrisphere.com


Глядя на разговоры о происхождении UTF-8, я вижу, как постоянно повторяют одну и ту же историю.
Неправильная версия:
1. UTF-8 разработала IBM.
2. Она была реализована в Plane 9 (операционная система, разработанная Bell Laboratories)
Это неправда. Я своими глазами видел, как однажды вечером в сентябре 1992 года была придумана UTF-8 на салфетке в одной закусочных Нью-Джерси.

Произошло это таким образом. Мы пользовались оригинальным UTF из стандарта ISO 10646 для поддержки 16-битных символов в Plane 9, который ненавидели, и уже были готовы к выпуску Plane 9, когда однажды поздно вечером мне позвонили одни парни, кажется они были из IBM. Я припоминаю, что встречался с ними на заседании комитета X/Open в Остине. Они хотели, чтобы мы с Кеном посмотрели их проект FSS/UTF.
В то время подавляющее большинство компьютерных программ и систем (документация, сообщения об ошибках и т.п.) было только на английском и только слева направо. Инженерам из Bell Labs показалось, что релиз Plane 9 хороший повод для того, чтобы изменить это, поскольку проще всего вводить новшества в систему на этапе ее разработки, а не исправлять уже выпущенный продукт. Потому они стали искать специалистов, которые помогут им интернационализировать их проект.

В существующей реализации Unicode было много недостатков, например, чтобы понять, где именно начинается произвольный символ, надо было разобрать всю строку с самого начала, без этого нельзя было определить границы символов.
(прим. пер.)
Мы поняли, почему они хотят изменить дизайн и решили, что это хорошая возможность использовать наш опыт, чтобы разработать новый стандарт и заставить ребят из X/Open продвинуть его. Нам пришлось рассказать им об этом, и они согласились при условии, что мы быстро с этим справимся.
Потом мы пошли перекусить, и во время ужина Кен разобрался с упаковкой битов, а когда вернулись, то позвонили ребятам из X/Open и объяснили им нашу идею. Мы отправили по почте наш набросок, и они ответили, что это лучше, чем у них (но я точно помню, что они нам свой вариант не показывали), и спросили, когда мы сможем это реализовать.
Одним из вариантов разграничения символов был слэш, но это могло запутать файловую систему, она бы могла интерпретировать его как эскейп-последовательность.
(прим. пер.)
Мне кажется, что это происходило в среду вечером. Мы пообещали, что запустим систему к понедельнику, когда у них, как мне кажется, намечалось какое-то важное совещание. В тот же вечер Кен написал код кодировщика/раскодировщика, а я начал разбираться с С и с графическими библиотеками. На следующий день код был готов, и мы начали конвертировать текстовые файлы системы. К пятнице Plane 9 уже запускался и работал на так называемом UTF-8.
А в дальнейшем история была немного переписана.

Почему мы просто не воспользовались их FSS/UTF?
Насколько я помню, в том первом телефонном звонке я напел им Дезидерату своих требований для кодировки, и в FSS/UTF не было как минимум одного возможности синхронизировать поток байтов взятых из середины потока, используя для синхронизации как можно меньше символов (см выше, про определение границ символов. прим. пер).
напел им Дезидерату
Игра слов.
Имеется в виду крылатая фраза, берущая начало из альбома Леса Крейна 1971 года, чье название и заглавная композиция: Desiderata взяты из одноименной поэмы, что переводится с латыни, как: Желаемое. То есть, напел им Дезидерату следует понимать как высказал пожелания. (прим пер.)
Поскольку нигде решения не было, мы были вольны делать это как хотели.
Я думаю, что историю придумали IBM, а реализовали в Plane 9 берет свое начало в документации по RFC 2279. Мы были так счастливы, когда UTF-8 прижился, что никому не рассказали эту историю.

Никто из нас больше не работает в Bell Labs, но я уверен, что сохранился архив электронной почты, которая может подтвердить нашу историю, и я могу попросить кого-нибудь покопаться в ней.
Итак, вся слава достается парням из X/Open и IBM за то, что они сделали это возможным и продвинули кодировку, но разработал ее Кен, и я ему помогал в этом, что бы там не говорилось в книгах по истории.

Роб


Date: Sat, 07 Jun 2003 18:44:05 -0700
From: "Rob `Commander' Pike" <r@google.com>
To: Markus Kuhn <Markus.Kuhn@cl.cam.ac.uk>
cc: henry@spsystems.net, ken@entrisphere.com,
Greger Leijonhufvud <greger@friherr.com>
Subject: Re: UTF-8 history


Я попросил Расса Кокса покопаться в архивах. Прикладываю его сообщение. Я думаю, вы согласитесь, что это подтверждает историю, которую я отправил раньше. Письмо, которое мы выслали в X/Open (думаю, что Кен редактировал и рассылал этот документ) включает новый desideratum номер 6 про обнаружение границ символов.

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

Роб


From: Russ Cox <rsc@plan9.bell-labs.com>
To: r@google.com
Subject: utf digging
Date-Sent: Saturday, June 07, 2003 7:46 PM -0400


Файл пользователя bootes /sys/src/libc/port/rune.c был изменен пользователем division-heavy 4 сентября 1992 года. Версия, попавшая в дамп имеет время 19:51:55. На другой день в него был добавлен комментарий, но в остальном он не изменялся до 14 ноября 1996 года, когда runelen была ускорена путем явной проверки значения rune вместо использования значения, возвращаемого runetochar. Последнее изменение было 26 мая 2001 года, когда была добавлена runenlen. (Rune структура, содержащая значение Юникод. Runelen и runetochar функции, работающие с этим типом данных. прим.пер)

Нашлось несколько писем из ваших ящиков, которые выдал грепинг по строке utf.

В первом идет речь про файл utf.c, который является копией wctomb и mbtowc (функции преобразования символов. прим. пер.), что обрабатывают полную 6-байтовую кодировку UTF-8, 32-битных runes. С логикой управления потоком это выглядит довольно уродливо. Я предполагаю, что этот код появился в результате того первого письма.

В /usr/ken/utf/xutf я нашел копию того, что, по видимому, является исходником того не самосинхронизирующегося способа кодирования.со схемой UTF-8, добавленной в конце письма (начинается со слов Мы определяем 7 типов byte).

Приведенная ниже версия письма, датированная 2 сентября 23:44:10, является первой. После нескольких правок, утром 8 сентября, получилась вторая версия. Логи почтового сервера показывают, как отправляется вторая версия письма и, через некоторое время, возвращается к Кену:

helix: Sep 8 03:22:13: ken: upas/sendmail: remote inet!xopen.co.uk!xojig
>From ken Tue Sep 8 03:22:07 EDT 1992 (xojig@xopen.co.uk) 6833
helix: Sep 8 03:22:13: ken: upas/sendmail: delivered rob From ken Tue Sep 8 03:22:07 EDT 1992 6833
helix: Sep 8 03:22:16: ken: upas/sendmail: remote pyxis!andrew From ken Tue Sep 8 03:22:07 EDT 1992 (andrew) 6833
helix: Sep 8 03:22:19: ken: upas/sendmail: remote coma!dmr From ken Tue Sep 8 03:22:07 EDT 1992 (dmr) 6833
helix: Sep 8 03:25:52: ken: upas/sendmail: delivered rob From ken Tue Sep 8 03:24:58 EDT 1992 141
helix: Sep 8 03:36:13: ken: upas/sendmail: delivered ken From ken Tue Sep 8 03:36:12 EDT 1992 6833


Всего хорошего.

Файлы из почтового архива


Далее файл с перепиской из дапма почтового сервера, который Расс Кокс приаттачил к своему, в ответ на просьбу Роберта покопаться в истории. Это первая версия. (прим пер.)

>From ken Fri Sep 4 03:37:39 EDT 1992

Вот наше предложение по модификации FSS-UTF. Речь идет о том же, о чем и в предыдущем. Приношу свои извинения автору.

Код был в какой-то степени протестирован и должен быть в довольно неплохой форме. Мы переделали код Plane 9 для использования с этой кодировкой, собрались выпустить дистрибутив и отобрать пользователей университета для начального тестирования.

File System Safe Universal Character Set Transformation Format (FSS-UTF)



В связи с утверждением ISO/IEC 10646 (Unicode) в качестве международного стандарта и ожиданием широкого распространения этого Универсального Набора Кодированных символов (UCS), для операционных систем, исторически основанных на формате ASCII, необходимо разработать способы представления и обработки большого количества символов, которые можно закодировать с помощью нового стандарта. У UCS есть несколько проблем, которые нужно решить в исторически сложившихся операционных системах и среде для программирования на языке C.

(Далее в тексте несколько раз упоминаются historical operating systems. Видимо в контексте исторически работающие с кодировкой ASCII. Я или опускал этот эпитет, или заменял его на существующие и т.п. прим. пер)

Самой серьезной проблемой является схема кодирования, используемая в UCS. А именно объединение стандарта UCS с существующими языками программирования, операционными системами и утилитами. Проблемы в языках программирования и операционных системах решаются в разных отраслях, тем не менее мы все еще сталкиваемся с обработкой UCS операционными системами и утилитами.

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

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

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

Цель/Задача



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

Критерии для формата преобразования



Ниже приведены рекомендации, которые должны соблюдаться, при определении формата преобразования UCS:

  1. Совместимость с существующими файловыми системами.
    Запрещено использовать нулевой байт и символ слэша как часть имени файла.
  2. Совместимость с существующими программами.
    В существующей модели многобайтовой обработки не должны использоваться коды ASCII. В формате преобразования представления символа UCS, которого нет в наборе символов ASCII, не должны использоваться коды ASCII.
  3. Простота конвертации из UCS и обратно.
  4. Первый байт содержит указание на длину многобайтовой последовательности.
  5. Формат преобразования не должен быть затратным, в смысле количества байт, используемых для кодирования.
  6. Должна быть возможность легко определять начало символа, находясь в любом месте байтового потока (строки. прим.пер.).

Предписания FSS-UTF



Предлагаемый формат преобразования UCS кодирует значения UCS в диапазоне [0,0x7fffffff] с использованием нескольких байт на один символ и длинной 1, 2, 3, 4, 5, и 6 байт. Во всех случаях кодирования более чем одним байтом начальный байт определяет количество используемых байтов, при этом в каждом байте устанавливается старший бит. Каждый байт, который не начинается с 10XXXXXX, является началом последовательности символов UCS.

Простой способ запомнить формат: количество старших единиц в первом байте означает количество байт в многобайтовом символе.

Bits  Hex Min Hex Max Byte Sequence in Binary1  7 00000000 0000007f 0vvvvvvv2  11 00000080 000007FF 110vvvvv 10vvvvvv3  16 00000800 0000FFFF 1110vvvv 10vvvvvv 10vvvvvv4  21 00010000 001FFFFF 11110vvv 10vvvvvv 10vvvvvv 10vvvvvv5  26 00200000 03FFFFFF 111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv6  31 04000000 7FFFFFFF 1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv10vvvvvv


Значение символа UCD в многобайтовой кодировке это конкатенация v-битов. Если возможно несколько способов кодирования, например UCS 0, то допустимым считается самый короткий.

Ниже приведены примеры реализаций стандартных функций C wcstombs() и mbstowcs(), которые демонстрируют алгоритмы конвертирования из UCS в формат преобразования и конвертирования из формата преобразования в UCS. Примеры реализаций включают проверку ошибок, некоторые из которых могут быть не нужны для согласования:

typedefstruct{int  cmask;int  cval;int  shift;long lmask;long lval;} Tab;staticTab    tab[] ={0x80, 0x00, 0*6,  0x7F,     0,      /* 1 byte sequence */0xE0, 0xC0, 1*6,  0x7FF,    0x80,     /* 2 byte sequence */0xF0, 0xE0, 2*6,  0xFFFF,       0x800,    /* 3 byte sequence */0xF8, 0xF0, 3*6,  0x1FFFFF,   0x10000,   /* 4 byte sequence */0xFC, 0xF8, 4*6,  0x3FFFFFF,  0x200000,   /* 5 byte sequence */0xFE, 0xFC, 5*6,  0x7FFFFFFF,  0x4000000,  /* 6 byte sequence */0,                       /* end of table */};intmbtowc(wchar_t *p, char *s, size_t n){long l;int c0, c, nc;Tab *t;if(s == 0)return 0;nc = 0;if(n <= nc)return -1;c0 = *s & 0xff;l = c0;for(t=tab; t->cmask; t++) {nc++;if((c0 & t->cmask) == t->cval) {l &= t->lmask;if(l < t->lval)return -1;*p = l;return nc;}if(n <= nc)return -1;s++;c = (*s ^ 0x80) & 0xFF;if(c & 0xC0)return -1;l = (l<<6) | c;}return -1;}intwctomb(char *s, wchar_t wc){long l;int c, nc;Tab *t;if(s == 0)return 0;l = wc;nc = 0;for(t=tab; t->cmask; t++) {nc++;if(l <= t->lmask) {c = t->shift;*s = t->cval | (l>>c);while(c > 0) {c -= 6;s++;*s = 0x80 | ((l>>c) & 0x3F);}return nc;}}return -1;}


>From ken Tue Sep 8 03:24:58 EDT 1992

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

Вторая версия письма, с правками


Далее прикладывается копия письма, которая выше описывается как: После нескольких правок, утром 8 сентября, получилась вторая версия. Повторяющаяся часть скрыта под спойлером. (прим.пер.)

>From ken Tue Sep 8 03:42:43 EDT 1992

Наконец-то я получил свою копию.
--- /usr/ken/utf/xutf from dump of Sep 2 1992 ---

Скрытый текст

File System Safe Universal Character Set Transformation Format (FSS-UTF)



В связи с утверждением ISO/IEC 10646 (Unicode) в качестве международного стандарта и ожиданием широкого распространения этого Универсального Набора Кодированных символов (UCS), для операционных систем, исторически основанных на формате ASCII, необходимо разработать способы представления и обработки большого количества символов, которые можно закодировать с помощью нового стандарта. У UCS есть несколько проблем, которые нужно решить в исторически сложившихся операционных системах и среде для программирования на языке C.

Самой серьезной проблемой является схема кодирования, используемая в UCS. А именно объединение стандарта UCS с существующими языками программирования, операционными системами и утилитами. Проблемы в языках программирования и операционных системах решаются в разных отраслях, тем не менее мы все еще сталкиваемся с обработкой UCS операционными системами и утилитами.

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

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

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

Цель/Задача



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

Критерии для формата преобразования



Ниже приведены рекомендации, которые должны соблюдаться, при определении формата преобразования UCS:

  1. Совместимость с существующими файловыми системами.
    Запрещено использовать нулевой байт и символ слэша как часть имени файла.
  2. Совместимость с существующими программами.
    В существующей модели многобайтовой обработки не должны использоваться коды ASCII. В формате преобразования представления символа UCS, которого нет в наборе символов ASCII, не должны использоваться коды ASCII.
  3. Простота конвертации из UCS и обратно.
  4. Первый байт содержит указание на длину многобайтовой последовательности.
  5. Формат преобразования не должен быть затратным, в смысле количества байт, используемых для кодирования.
  6. Должна быть возможность легко определять начало символа, находясь в любом месте байтового потока (строки. прим.пер.).

Предписания FSS-UTF



Предлагаемый формат преобразования UCS кодирует значения UCS в диапазоне [0,0x7fffffff] с использованием нескольких байт на один символ и длинной 1, 2, 3, 4, 5, и 6 байт. Во всех случаях кодирования более чем одним байтом начальный байт определяет количество используемых байтов, при этом в каждом байте устанавливается старший бит. Каждый байт, который не начинается с 10XXXXXX, является началом последовательности символов UCS.

Простой способ запомнить формат: количество старших единиц в первом байте означает количество байт в многобайтовом символе.

Bits  Hex Min Hex Max Byte Sequence in Binary1  7 00000000 0000007f 0vvvvvvv2  11 00000080 000007FF 110vvvvv 10vvvvvv3  16 00000800 0000FFFF 1110vvvv 10vvvvvv 10vvvvvv4  21 00010000 001FFFFF 11110vvv 10vvvvvv 10vvvvvv 10vvvvvv5  26 00200000 03FFFFFF 111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv6  31 04000000 7FFFFFFF 1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv10vvvvvv


Значение символа UCD в многобайтовой кодировке это конкатенация v-битов. Если возможно несколько способов кодирования, например UCS 0, то допустимым считается самый короткий.

Ниже приведены примеры реализаций стандартных функций C wcstombs() и mbstowcs(), которые демонстрируют алгоритмы конвертирования из UCS в формат преобразования и конвертирования из формата преобразования в UCS. Примеры реализаций включают проверку ошибок, некоторые из которых могут быть не нужны для согласования:

typedefstruct{int  cmask;int  cval;int  shift;long lmask;long lval;} Tab;staticTab    tab[] ={0x80, 0x00, 0*6,  0x7F,     0,      /* 1 byte sequence */0xE0, 0xC0, 1*6,  0x7FF,    0x80,     /* 2 byte sequence */0xF0, 0xE0, 2*6,  0xFFFF,       0x800,    /* 3 byte sequence */0xF8, 0xF0, 3*6,  0x1FFFFF,   0x10000,   /* 4 byte sequence */0xFC, 0xF8, 4*6,  0x3FFFFFF,  0x200000,   /* 5 byte sequence */0xFE, 0xFC, 5*6,  0x7FFFFFFF,  0x4000000,  /* 6 byte sequence */0,                       /* end of table */};intmbtowc(wchar_t *p, char *s, size_t n){long l;int c0, c, nc;Tab *t;if(s == 0)return 0;nc = 0;if(n <= nc)return -1;c0 = *s & 0xff;l = c0;for(t=tab; t->cmask; t++) {nc++;if((c0 & t->cmask) == t->cval) {l &= t->lmask;if(l < t->lval)return -1;*p = l;return nc;}if(n <= nc)return -1;s++;c = (*s ^ 0x80) & 0xFF;if(c & 0xC0)return -1;l = (l<<6) | c;}return -1;}intwctomb(char *s, wchar_t wc){long l;int c, nc;Tab *t;if(s == 0)return 0;l = wc;nc = 0;for(t=tab; t->cmask; t++) {nc++;if(l <= t->lmask) {c = t->shift;*s = t->cval | (l>>c);while(c > 0) {c -= 6;s++;*s = 0x80 | ((l>>c) & 0x3F);}return nc;}}return -1;}

int mbtowc(wchar_t *p, const char *s, size_t n){unsigned char *uc;   /* so that all bytes are nonnegative */if ((uc = (unsigned char *)s) == 0)return 0;        /* no shift states */if (n == 0)return -1;if ((*p = uc[0]) < 0x80)return uc[0] != '\0';  /* return 0 for '\0', else 1 */if (uc[0] < 0xc0){if (n < 2)return -1;if (uc[1] < 0x80)goto bad;*p &= 0x3f;*p <<= 7;*p |= uc[1] & 0x7f;*p += OFF1;return 2;}if (uc[0] < 0xe0){if (n < 3)return -1;if (uc[1] < 0x80 || uc[2] < 0x80)goto bad;*p &= 0x1f;*p <<= 14;*p |= (uc[1] & 0x7f) << 7;*p |= uc[2] & 0x7f;*p += OFF2;return 3;}if (uc[0] < 0xf0){if (n < 4)return -1;if (uc[1] < 0x80 || uc[2] < 0x80 || uc[3] < 0x80)goto bad;*p &= 0x0f;*p <<= 21;*p |= (uc[1] & 0x7f) << 14;*p |= (uc[2] & 0x7f) << 7;*p |= uc[3] & 0x7f;*p += OFF3;return 4;}if (uc[0] < 0xf8){if (n < 5)return -1;if (uc[1] < 0x80 || uc[2] < 0x80 || uc[3] < 0x80 || uc[4] < 0x80)goto bad;*p &= 0x07;*p <<= 28;*p |= (uc[1] & 0x7f) << 21;*p |= (uc[2] & 0x7f) << 14;*p |= (uc[3] & 0x7f) << 7;*p |= uc[4] & 0x7f;if (((*p += OFF4) & ~(wchar_t)0x7fffffff) == 0)return 5;}bad:;errno = EILSEQ;return -1;}

Мы определяем 7 байтовых типов:

T0 0xxxxxxx   7 free bitsTx 10xxxxxx   6 free bitsT1 110xxxxx   5 free bitsT2 1110xxxx   4 free bitsT3 11110xxx   3 free bitsT4 111110xx   2 free bitsT5 111111xx   2 free bits

Кодирование выглядит следующим образом:

>From hex Thru hex   Sequence       Bits00000000 0000007f   T0          700000080 000007FF   T1 Tx        1100000800 0000FFFF   T2 Tx Tx       1600010000 001FFFFF   T3 Tx Tx Tx     2100200000 03FFFFFF   T4 Tx Tx Tx Tx    2604000000 FFFFFFFF   T5 Tx Tx Tx Tx Tx  32

Некоторые примечания:

  1. Двумя байтами можно закодировать 2^11 степени символов, но использоваться будут только 2^112^7. Коды в диапазоне 0-7F будут считаться недопустимыми. Я думаю, что это лучше, чем добавление кучи магических констант без реальной пользы. Это замечание применимо ко всем более длинным последовательностям.
  2. Последовательности из 4, 5 и 6 байт существуют только по политическим причинам. Я бы предпочел их удалить.
  3. 6-байтовая последовательность охватывает 32 бита, предложение FSS-UTF охватывает только 31.
  4. Все последовательности синхронизируются по любому байту, не являющемуся Tx.

***


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

Давно забыта операционная система Plane 9, никто не помнит для чего ее писали и почему она номер девять, а UTF-8, спустя почти тридцать лет, все еще актуальна и не собирается уходить на покой.

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

Перевод Заметки о Unix небольшая странность семейства вызовов exec()

24.04.2021 12:23:53 | Автор: admin
Я недавно писал об опции -exec команды find и ненароком упомянул о семействе системных вызовов exec(). Это странное выражение, обычно о Unix-вызовах так не говорят, так как они, как правило, не объединяются в некие семейства. Но в данном случае речь идёт о целом наборе схожих механизмов. Перечислим их:

execv() execve() execvp() execvpe()execl() execlp() execle()

(Этот список выглядит именно так в Linux и в OpenBSD; подобный список, составленный для FreeBSD, включал бы в себя execvP(), а не execvpe(). В POSIX-версии подобного списка нет вызова execvpe(), но есть вызов fexecve(), который я не вполне готов включить в семейство вызовов exec().)



Одна из этих команд не похожа на другие. На самом деле, во всём этом списке, включающем в себя, как минимум, шесть exec()-функций, лишь execve() относится к системным вызовам; остальные exec*()-функции это просто библиотечные функции, в основе которых лежит execve(). То, что существуют удобные библиотечные функции, основанные на системном вызове (или на нескольких вызовах), не является чем-то необычным; так, например, устроено всё то, что имеет отношение к stdio. Но всё это выглядит несколько странно из-за того, что имена функций весьма близки друг к другу. У меня хорошая память на имена libc-функций Unix, но я, вероятно, в большинстве случаев, не смог бы выделить из вышеприведённого списка настоящий системный вызов exec().

(Сейчас-то я, конечно, хорошо помню о том, что execve() это системный вызов, который, в большинстве Unix-дистрибутивов, лежит в основе exec*()-функций.)

Такое множество функций со схожими именами существует со времён V7 Unix, где документация по execl(), execv(), execle() и execve() размещена в справочном файле exec(2). В V7, по состоянию на сегодняшний день, базовым системным вызовом является execve(), хотя ему назначено другое имя. Даже в V6 execl() и execv() описаны в единственном справочном файле exec(2).

(Соответствующий системный вызов V6 был назван exec и принимал лишь программу, которую нужно выполнить, и argv. Когда в V7 появилось такое понятие, как окружение, там сохранился системный вызов exec из V6, который, в качестве дополнительного аргумента, принимал envp.)

P.S. В некоторых дистрибутивах Unix есть базовые системные вызовы, которые, по сути, являются разновидностями одного и того же вызова. Такая ситуация сложилась из-за того, что API системных вызовов развивался и совершенствовался достаточно длительное время. Например, подобное могло произойти при добавлении в систему 64-битных вариантов вызовов, которые раньше были 32-битными. Правда, обычно в сознании людей присутствуют лишь самые свежие версии системных вызовов; они не представлены некими семействами разновидностей какого-то вызова и в этом смысле не похожи на семейство exec().

Встречались ли вы с какими-то странностями, касающимися именования сущностей в Unix?

Подробнее..

Перевод Заметки о Unix ограничения опции -exec команды find и стремление к удобству при реализации команд

06.05.2021 16:19:55 | Автор: admin
В материале о том, что в наши дни find, как правило, не нуждается в xargs, я отметил, что в конструкции '-exec ... {} +' скобки ('{}') (для имён файлов, генерируемых find) должны находиться в конце команды. В комментарии к той публикации анонимный читатель сказал, что это неприменимо к -exec-версии, которая запускает отдельную команду для каждого имени файла. В результате можно поместить заменяемое имя файла в любом месте команды. Это, как оказалось, относится не только к GNU Find, являясь стандартной возможностью, и я полагаю, что этого даже требует Single Unix Specification (SUS) для find.



(SUS, в отношении аргументов -exec, вводит ограничения лишь на форму '+', предписывая размещать '{}' непосредственно перед '+'. Спецификация же для формы ';' просто говорит об использовании обычного списка аргументов, после чего в описании сказано, что '{}' в списке аргументов заменяется на текущий путь. Понять это всё довольно сложно, хотя подобное в SUS и POSIX обычное дело.)

Разница между двумя формами -exec это интересный вопрос, и эта разница, вероятно, существует из-за удобства реализации формы '+'. Поэтому предлагаю начать с самого начала. Когда применяют любую из форм -exec, команда find выполняет соответствующие команды, задействуя семейство системных вызовов exec() (и библиотечные функции), которым нужно, чтобы им передавался бы массив с командами и с аргументами (то есть это argv для новой команды). Реализация этого в конструкции, где выполняется одна замена ('-exec ... ;') проста: создают и заполняют массив argv, содержащий все аргументы -exec (и команды), и запоминают индекс параметра '{}' (если такой параметр есть; он не является обязательным). Каждый раз, когда выполняют команду, текущий путь помещают в ячейку argv и дело сделано.

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

Правда, если конструкция '{}' может размещаться где угодно понадобится более сложная реализация, в которой надо будет делить фиксированные аргументы на две части, одна из которых идёт до '{}', а вторая после. Начало argv тогда заполняется предшествующими фиксированными аргументами, потом в массив попадают пути, в виде дополнительных аргументов, до достижения лимита, а затем, до exec(), присоединяются следующие фиксированные аргументы, если такие имеются. Для реализации этого всего нужно не так уж и много дополнительного труда, но над этим, всё равно, надо поработать. Поэтому я вынужден выдвинуть теорию о том, что объём этого дополнительного труда оказался именно таким, чтобы программисты, реализующие команду в System V R4 (там эта возможность появилась впервые) выбрали бы ограниченную форму ради удобства разработки и ради того, чтобы в их коде было бы меньше ошибок (ведь код, который не надо писать, определённо, совсем не содержит ошибок).

(Уверен, что это не единственная область Unix-команд, в которой можно заметить признаки стремления разработчиков к реализации того, что реализовывать удобнее. Но в случае с двумя версиями -exec команды find перед нами яркий пример такого подхода.)

Сталкивались ли вы с какими-то особенностями Unix-команд, которые можно объяснить стремлением их создателей к удобству их реализации?

Подробнее..

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

07.05.2021 16:08:17 | Автор: admin
В давние времена многоархитектурных Unix-окружений разработчики дистрибутивов не могли прийти к единому мнению о том, что должно быть в $PATH. Базовые вещи, вроде /bin и /usr/bin, были везде одинаковыми, но у каждого дистрибутива был собственный набор дополнительных директорий (у Solaris их, например, было много). Кроме того у разных локальных вычислительных групп было различное видение того, где должны размещаться локальные программы. Например в /usr/local/bin, в /local/bin, в /opt/<something>/bin, в /<group>/bin и так далее. Всё это усложняло мне жизнь, так как я занимался поддержкой общего набора dot-файлов, используемых во всех Unix-системах, за которые я отвечал, и мне не хотелось бы, чтобы моя переменная $PATH представляла бы собой огромный список, содержащий пути ко всем необходимым директориям каждой из систем. Поэтому мне нужно было убирать всё лишнее из гигантского базового списка директорий, которые могли присутствовать в $PATH, оставляя там лишь те директории, которые существовали в текущей системе. А чтобы ещё сильнее усложнить эту задачу, мне хотелось использовать для этого только команды, встроенные в оболочку, и это при работе с оболочкой, где test встроенной командой не является.



К моему счастью, есть одна команда, которая должна быть встроенной в оболочку и при этом даёт сбой в том случае, если директории не существует (или если пользователь не может с ней работать). Это команда cd. Использование cd в качестве замены 'test -d' это решение немного странное, но работоспособное. В моей оболочке были настоящие списки, поэтому я мог добиться того, что мне нужно, примерно так:

# то, что может попасть в $PATH, находится в $candidatespath=`{ tpath=()for (pe in $candidates)builtin cd $pe >[1=] >[2=] && tpath=($tpath $pe)echo $tpath }

(С относительными путями этот код не работает, но в моей переменной $PATH таких путей не было.)

Так как во всех оболочках обязательно должна быть встроенная команда cd, тот же подход можно было использовать практически во всех оболочках. Bourne-подобные оболочки, правда, усложняли задачу по сборке $PATH. Там, как минимум, нужно было добавлять :'s между элементами (cf) и, возможно, в эквиваленте $candidates для таких оболочек нужно было бы использовать :'s между записями, что привело бы к необходимости разделять записи, основываясь на этой конструкции.

(В оболочке Bourne я представил бы $candidates в виде строки, заключённой в кавычки, элементы которой разделены пробелами, так как работать с таким списком директорий гораздо проще. Правда, при таком подходе я не смог бы обрабатывать $PATH-записи, содержащие пробелы, но таких записей в $PATH обычно не бывает.)

Использование cd вышеописанным образом это, по сути, хак, но хаки это то, к чему мы вынуждены прибегать в минималистичных окружениях оболочек, когда необходимо, для решения неких задач, обойтись без внешних программ. Я же, на самом деле, написал на C маленькую программу, isdirs, которая решала вышеописанную задачу, и использовал её в тех системах, с которыми я работал достаточно часто для того, чтобы оправдать компиляцию для них этой программы. А код, в котором использовалась команда cd, был чем-то вроде запасного варианта, применяемого в системах и в ситуациях, в которых я не мог воспользоваться моей isdirs.

(Этот материал можно счесть чем-то вроде продолжения одной моей статьи про хак командной оболочки Unix, которая тоже связана с кросс-архитектурной средой и с dot-файлами.)

P.S. То, о чём я рассказал, происходило в те времена, когда системы были достаточно медленными для того чтобы тот, кто их обслуживает, стремился бы к тому, чтобы без крайней нужды не пользоваться дополнительными внешними программами в dot-файлах оболочки. Именно поэтому я и решил пользоваться только встроенными командами оболочки вместо того, чтобы выполнять множество вызовов test или чего-то подобного. А в большинстве современных оболочек test это встроенная в них команда.

Приходилось ли вам, при работе в Unix, решать какие-то задачи, необычным образом пользуясь исключительно встроенными командами оболочки?

Подробнее..

Перевод Заметки о Unix сильные и слабые стороны errno в традиционных Unix-окружениях

09.05.2021 18:11:45 | Автор: admin
Недавно я мимоходом отметил, что errno был, в целом, хорошим интерфейсом в Unix-системах до появления в них многопоточности. Кого-то подобное высказывание может удивить, поэтому сегодня предлагаю поговорить о сильных и слабых сторонах errno в традиционных Unix-окружениях, таких, как V7 Unix.



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

(Современный C способен на такие фокусы, как возврат двухэлементной структуры в паре регистров, но этого нельзя сказать о более старых и более простых версиях C, используемых, как минимум, в Research Linux V7.)

Некоторые системные вызовы C-библиотек Unix в V7 могли возвращать сведения об ошибке в виде специального значения, и, вероятно, нельзя говорить о том, что все они поддерживали подобную возможность (в V7 действовали ограничения на количество файлов, да и адресное пространство на PDP-11 тоже было достаточно ограниченным). Даже если бы это поддерживали все вызовы, это привело бы к необходимости писать больше кода в случаях, когда нужно было проверять возвращаемые значения команд вроде open() или sbrk(). В C-коде пришлось бы проверять то, в каком диапазоне значений находится возвращаемое значение, или другие характеристики этого значения.

(Реальные системные вызовы в V7 Unix и до неё использовали метод оповещения об ошибках, спроектированный для ассемблера, когда ядро было настроено на возврат в регистр r0 либо результата системного вызова, либо номера ошибки, и на выполнение установок, зависящих от того, что именно было возвращено. Почитать об этом можно в справке по dup для V4, которая написана в те времена, когда к Unix ещё готовили серьёзную ассемблерную документацию. C-библиотека V7 сделана так, что при возникновении ошибки делается запись в errno и возвращается -1. Почитайте, например, libc/sys/dup.s вместе с libc/crt/cerror.s.)

Слабая сторона errno заключается в том, что это самостоятельное глобальное значение. То есть оно может быть случайно перезаписано в том случае, если между моментом, когда в него, интересующим нас системным вызовом, были записаны сведения об ошибке, и моментом, когда мы решили воспользоваться errno, что-то ещё записало в него сведения о собственной ошибке. Подобное легко может произойти тогда, когда, после сбоя, выполняется прямое или непрямое обращение из обычного кода к какому-нибудь системному вызову, который тоже даёт сбой. Классической ошибкой такого рода была попытка сделать проверку того, является ли стандартный вывод (или стандартный вывод ошибки) терминалом. Делается это путём выполнения на нём TTY-вызова ioctl(). Когда вызов ioctl() завершится с ошибкой, исходное значение errno будет перезаписано значением ENOTTY, и причина ошибки, из-за которой завершился вызов open() или какой-то другой вызов, будет описана таинственным сообщением not a typewriter (cf).

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

(В V7 не было сигнала SIGCHLD, но он был в BSD. Это так из-за того, что в BSD появилась система управления заданиями, что и привело к необходимости наличия подобного сигнала. Но это уже совсем другая история.)

В целом же я полагаю, что errno был хорошим интерфейсом, учитывая ограничения традиционной Unix, когда не было многопоточности или нормальных способов возврата нескольких значений из вызовов C-функций. Хотя у него есть и минусы, и слабые стороны, их обычно можно было обойти, и обычно они не слишком часто давали о себе знать. API errno стал выглядеть весьма нескладно только тогда, когда в Unix появилась многопоточность, и в одном адресном пространстве могло присутствовать несколько сущностей, одновременно выполняющих системные вызовы. Как и большая часть того, что имеется в Unix (в особенности в эру Research Unix V7), это не идеальное, хотя и вполне приемлемое решение.

Сталкивались ли вы с проблемами errno?

Подробнее..

Перевод Заметки о Unix надёжная работа с API C-библиотеки Unix возможна только из программ, написанных на C

14.05.2021 20:06:19 | Автор: admin
Для того чтобы полностью реализовать требования системы верификации источника системных вызовов, разработчики OpenBSD хотят, чтобы Go выполнял бы системные вызовы через C-библиотеку, а не напрямую, из собственной среды выполнения (а у Go есть некоторые причины поступать именно так). На первый взгляд это кажется не особенно серьёзной проблемой. Это, конечно, немного неудобно, но у языка вроде Go должна быть возможность просто выполнять вызовы обычных функций из C-библиотеки, вроде open() (и использовать ABI вызова C-функций). К сожалению, не так всё просто, так как очень часто фрагменты обычного API C-библиотеки, на самом деле, реализованы в препроцессоре C. Из-за этого API C-библиотеки нельзя надёжно использовать для решения обычных задач без написания собственного связующего кода на C.



Звучит это, пожалуй, дико, поэтому позвольте мне проиллюстрировать это на примере всеми любимого значения errno, к которому обращаются для получения кода ошибки в том случае, если системный вызов даёт сбой (им же пользуются и для получения кодов ошибок от некоторых библиотечных вызовов). В этом материале я рассказывал о том, что в современных условиях механизм errno должен быть реализован так, чтобы у разных потоков были бы разные значения errno, так как они могут в одно и то же время выполнять различные системные вызовы. Это требует наличия у потоков собственных локальных хранилищ, а к такому хранилищу нельзя обратиться так же, как к простой переменной. Доступ к нему должен быть организован через специальный механизм, поддерживаемый средой выполнения C. Вот объявления errno из OpenBSD 6.6 и из текущей версии Fedora Linux с glibc:

/* OpenBSD */int *__errno(void);#define errno (*__errno())/* Fedora glibc */extern int *__errno_location (void) __THROW __attribute_const__;# define errno (*__errno_location ())

В обоих этих случаях переменная errno, на самом деле, представлена определением препроцессора. Это определение ссылается на внутренние недокументированные функции C-библиотеки (на что указывают два символа подчёркивания в их именах), которые не входят в состав общедоступного API. Если скомпилировать код, написанный на C, рассчитанный на работу с этим API errno (включив в код errno.h), то он будет работать, но это единственный официальный способ работы с errno. Нет некоей обычной переменной errno, которую можно загрузить в среде выполнения своего языка, например, после вызова функции open(). А если вызвать __errno или ____errno_location в своей среде выполнения, то это будет означать использование внутреннего API, который в будущем вполне может измениться (хотя он, вероятно, не изменится). Для того чтобы создавать надёжные среды выполнения языков программирования, которые ориентированы на общедоступный API С-библиотеки, недостаточно просто вызвать экспортированную функцию вроде open(); нужно ещё написать и скомпилировать собственную маленькую C-функцию, которая просто возвращает среде выполнения errno.

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

Это, конечно, не какая-то новая проблема Unix. С первых дней stdio в V7 некоторые из функций stdio были реализованы в stdio.h в виде макросов препроцессора. Но в течение долгого времени никто не настаивал на том, чтобы единственным официально поддерживаемым способом выполнения системных вызовов было бы их выполнение из C-библиотеки, что позволяло обойти нечто вроде того, что представляет собой современный механизм errno, в тех случаях, когда не нужна совместимость с C-кодом.

(До того, как в Unix появилась многопоточность, сущность errno была представлена простой переменной и, в целом, выглядела как хороший, хотя и не идеальный интерфейс.)

Пользовались ли вы когда-нибудь недокументированными API?


Подробнее..

Почему клавиатура всегда быстрее мыши

27.05.2021 10:19:54 | Автор: admin

Тепловая карта с клавиатуры высокоинтеллектуальных программистов, источник: r/ProgrammerHumor/

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

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

В чём же дело?

Экзотический манипулятор


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

Необычные манипуляторы с колёсиком стоили в районе 400 долларов. Затем вышел революционный компьютер Apple Lisa один из первых ПК с графическим интерфейсом. Компания Apple демпинговала она снизила стоимость манипулятора до 25 долларов и сделала сексуальный дизайн с одной кнопкой. Мышь из профессионального аксессуара превратилась в массовый гаджет.


Apple Lisa. Очень элегантный дизайн для своего времени

С тех пор мышь и GUI стали прочно ассоциироваться с компьютерами Apple и модным оконным интерфейсом.

Вскоре появилась Windows. Затем первые игры с поддержкой мыши, например, для более точного движения персонажа Возникает впечатление, что компьютерные игры постоянно стимулировали технический прогресс, в том числе производительность CPU, GPU, ёмкость накопителей, популярность мышей и других экзотических манипуляторов.

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

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

В наше время редко встретишь компьютер без мыши. А вот удовольствие от работы в консоли осталось.

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

Крутые однострочники


Вот некоторые примеры интересного использования программ Linux.

ps aux | convert label:@- process.png

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

Примечание. Утилита convert входит в пакет ImageMagick, так что нужно сначала его установить.

А вообще, текст из консоли можно быстро запостить через интернет-сервис вроде termbin.com (это как pastebin, только для консоли):

ps aux | nc termbin.com 9999

Как обычно, с алиасом для частого использования:

alias tb='nc termbin.com 9999'

Следующая:

curl ipinfo.io

Это если хотите узнать свой внешний IP-адрес через сервис ipinfo.io.

git log --format='%aN' | sort -u

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

history | awk '{print $2}' | sort | uniq -c | sort -rn | head

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

ls -d */

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

du -hs */ | sort -hr | head

Эта команда показывает только 10 крупнейших директорий в текущем каталоге.

ss -p

Просмотр, какие приложения потребляют трафик (утилиты iftop и nethogs дают более подробную информацию).

rm -f !(test.txt)

Команда удаляет из директории все файлы, кроме одного, указанного в скобках. Это работает после включения расширенной глобуляции в баше (shopt -s extglob).

python3 -m http.server

Запускает http-сервер и начинает отдавать файлы. Удобно, если хотите пошарить какой-то html-файл по сети.

screen -S the-screen-name

Создание экран-сессии.

screen -x the-screen-name

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

Утилита screen поставляется по умолчанию со многими дистрибутивами Linux, хотя не со всеми.

alias copy='xclip -i -selection clipboard'

cat file.txt | copy

Копирование файла в буфер обмена, когда первый однострочник прописан как алиас copy в баше.

sudo !!

Запустить последнюю команду под рутом, если в предыдущей команде вы забыли набрать sudo. У этой команды первое место в рейтинге однострочников.

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

Горячие клавиши как наследие консоли


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

Алиасы bash служат той же цели: выполнить команду с наименьшим количеством усилий, то есть с наименьшим количеством нажатий клавиш.

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

Если подняться на более высокий уровень абстракций, то горячие клавиши в графических интерфейсах это в каком-то смысле дальний потомок консоли, некое историческое эхо. С горячими клавишами, однострочниками и алиасами в баше человек становится быстрее компьютера приходится делать паузы, чтобы дождаться результата выполнения команд. CPU загружен практически на 100%.

Это фундаментальное преимущество клавиатуры как инструмента для ввода команд по сравнению с любыми манипуляторами. В этом же и сила консоли.

Конечно, не все команды вызываются хоткеями. Иногда требуется переместить графический объект на экране Тогда нужен курсор. Но если хоткей существует, он без вариантов быстрее, чем аналогичное действие мышью в меню. Горячие клавиши живое доказательство, что команды с клавиатуры запускаются быстрее.

В общем, из этого факта может родиться предположение, что отцы-основатели Unix всё-таки были правы, а их наследие живёт во всех операционных системах. Графическая оболочка просто тонкий слой абстракции поверх мощного фундамента, который они построили. Ведь мы помним, что macOS тоже основана на Unix и относится к семейству *nix-систем.

Ну а окошки и другие элементы графического интерфейса Windows, по мнению Apple, это вторичный продукт, скопированный с интерфейса Lisa (см. судебный процесс Apple против Microsoft c 1988 по 1994 гг).

Суд отклонил иск Apple к Microsoft. Но некоторые вещи обращают на себя внимание. Например, команда open . в консоли macOS открывает Finder в текущей директории. В Windows то же самое делает команда start . (Finder здесь называется Explorer). Окна в macOS закрываются крестиком в левом верхнем углу, а в Windows в правом углу. Возможно, на примере таких деталей Билл Гейтс доказал суду, что у него оригинальный графический интерфейс, который сильно отличается от macOS.

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



На правах рекламы


Наша компания предлагает аренду VPS для совершенно любых проектов. Создайте собственный тарифный план в пару кликов, максимальная конфигурация позволит разместить практически любой проект 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe!

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Перевод Используем черную магию для создания быстрого кольцевого буфера

13.05.2021 20:21:56 | Автор: admin

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

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

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

void put(queue_t *q, uint8_t *data, size_t size) {    for(size_t i = 0; i < size; i++){        q->buffer[(q->tail + i) % q->buffer_size] = data[i];    }    q->tail = (q->tail + size) % q->buffer_size;}

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

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

Кольцевой буфер

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

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

typedef struct {    uint8_t *buffer;    size_t   buffer_size;    size_t   head;    size_t   tail;    size_t   bytes_avail;} queue_t;

Учитывая это, мы можем написать простые методы чтения (get) и записи (put), используя побайтовый доступ:

bool put(queue_t *q, uint8_t *data, size_t size) {    if(q->buffer_size - q->bytes_avail < size){        return false;    }    for(size_t i = 0; i < size; i++){        q->buffer[(q->tail + i) % q->buffer_size] = data[i];    }    q->tail = (q->tail + size) % q->buffer_size;    q->bytes_avail += size;    return true;}bool get(queue_t *q, uint8_t *data, size_t size) {    if(q->bytes_avail < size){        return false;    }    for(size_t i = 0; i < size; i++){        data[i] = q->buffer[(q->head + i) % q->buffer_size];    }    q->head = (q->head + size) % q->buffer_size;    q->bytes_avail -= size;    return true;}

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

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

static inline off_t min(off_t a, off_t b) {    return a < b ? a : b;}bool put(queue_t *q, uint8_t *data, size_t size) {    if(q->buffer_size - q->bytes_avail < size){        return false;    }const size_t part1 = min(size, q-&gt;buffer_size - q-&gt;tail);const size_t part2 = size - part1;memcpy(q-&gt;buffer + q-&gt;tail, data,         part1);memcpy(q-&gt;buffer,           data + part1, part2);q-&gt;tail = (q-&gt;tail + size) % q-&gt;buffer_size;q-&gt;bytes_avail += size;return true;}bool get(queue_t *q, uint8_t *data, size_t size) {    if(q->bytes_avail < size){        return false;    }const size_t part1 = min(size, q-&gt;buffer_size - q-&gt;head);const size_t part2 = size - part1;memcpy(data,         q-&gt;buffer + q-&gt;head, part1);memcpy(data + part1, q-&gt;buffer,           part2);q-&gt;head = (q-&gt;head + size) % q-&gt;buffer_size;q-&gt;bytes_avail -= size;return true;}

А вот пример использования этого кольцевого буфера:

int main() {    queue_t queue;queue.buffer      = malloc(128);queue.buffer_size = 128;queue.head        = 0;queue.tail        = 0;queue.bytes_avail = 0;put(&amp;q, "hello ", 6);put(&amp;q, "world\n", 7);char s[13];get(&amp;q, (uint8_t *) s, 13);printf(s); // prints "hello world"}

Наш код прост и отлично работает. Но почему бы не усложнить его немного?

Встречайте таблицу страниц

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

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

Таблица страниц для некоторой программы А может выглядеть следующим:

Номер виртуальной страницы

Номер физической страницы

0

null

1

25

2

12

3

null

4

56

B то же время, для программы B она могла бы выглядеть следующим:

Номер виртуальной страницы

Номер физической страницы

0

11

1

null

2

92

3

21

4

null

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

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

Несколько обычных системных вызовов и один, о котором glibc не хотел бы, чтобы вы знали

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

Системный вызов

Описание

int getpagesize()

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

int ftruncate(int fd, off_t length)

Устанавливает размер файла равным length, используя его файловый дескриптор fd.

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)

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

А теперь менее распространенный системный вызов, который glibc (наш дружественный пользовательский интерфейс ядра) не предоставляет нам:

System Call

Системный вызов

Description

Описание

int memfd_create(const char *name, unsigned int flags)

Возвращает файловый дескриптор анонимного файла (anonymous file), который существует только в памяти.

Интересно! Поскольку glibc его не реализует, нам нужно будет написать небольшую обертку, если мы захотим его использовать.

int memfd_create(const char *name, unsigned int flags) {    return syscall(__NR_memfd_create, name, flags);}

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

Жуткая черная магия взлома таблицы

Ну что ж, перейдем к делу. Давайте сделаем то, что сказано в Википедии, и сделаем так, чтобы две соседние страницы указывали на одну и ту же память. Нам нужно будет изменить структуру нашей очереди и расширить ее API удобным методом инициализации.

typedef struct {    uint8_t *buffer;    size_t   buffer_size;    int      fd;    size_t   head;    size_t   tail;} queue_t;bool init(queue *q, size_t size){// Для начала убедимся, что размер кратен размеру страницыif(size % getpagesize() != 0){    return 0;}//Создаем анонимный файл и устанавливаем его размерq-&gt;fd = memfd_create("queue_buffer", 0);ftruncate(q-&gt;fd, size);// Запрашиваем у mmap адрес локации, где мы можем отобразить обе виртуальные копии буфераq-&gt;buffer = mmap(NULL, 2 * size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);// Отображаем буфер по этому адресуmmap(q-&gt;buffer, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, q-&gt;fd, 0);// Теперь снова отображаем его на следующей виртуальной страницеmmap(q-&gt;buffer + size, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, q-&gt;fd, 0);// Инициализируем индексы нашего буфераq-&gt;head = 0;q-&gt;tail = 0;}

Хорошо, что тут происходит? Что ж, сначала мы вызываем memfd_create, который делает именно то, что написано на этикетке. Мы находим место для двух копий буфера и сохраняем его как адрес нашего буфера. Затем мы отображаем виртуальный файл в память по адресу, который нам дал mmap. После этого в игру вступает наша черная магия.

Мы просим mmap снова отобразить наш файл по адресу в size байтах после его исходного местоположения. Теперь таблица страниц будет выглядеть следующим образом (предполагается, что size больше одной страницы):

Виртуальный адрес

Физический адрес

q->buffer

анонимный файл q->fd, смещение 0

q->buffer + getpagesize()

анонимным файл q->fd, смещение getpagesize()

q->buffer + size

анонимным файл q->fd, смещение 0

q->buffer + size + getpagesize()

анонимный файл q->fd, смещение getpagesize()

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

bool put(queue_t *q, uint8_t *data, size_t size) {    if(q->buffer_size - (q->tail - q->head) < size){        return false;    }    memcpy(&q->buffer[q->tail], data, size);    q->tail += size;    return true;}bool get(queue_t *q, uint8_t *data, size_t size) {    if(q->tail - q->head < size){        return false;    }    memcpy(data, &q->buffer[q->head], size);    q->head += size;    if(q->head > q->buffer_size) {       q->head -= q->buffer_size;       q->tail -= q->buffer_size;    }    return true;}

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

Обратите внимание, что нам нужно делать только один вызов memcpy вместо двух. После чтения данных мы проверяем, что указатель head не переместился во вторую виртуальную копию буфера. Если он все же переместился, мы можем уменьшить оба значения на размер буфера; они будут указывать на одну и ту же физическую память, хотя виртуальные адреса будут разными. API остался прежним.

Насколько это хорошо?

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

#include <stdio.h>#include <string.h>#include <stdint.h>#include <sys/time.h>#define BUFFER_SIZE (1024)#define MESSAGE_SIZE (32)#define NUMBER_RUNS (1000000)static inline double microtime() {    struct timeval tv;    gettimeofday(&tv, NULL);    return 1e6 * tv.tv_sec + tv.tv_usec;}static inline off_t min(off_t a, off_t b) {    return a < b ? a : b;}int main() {    uint8_t message[MESSAGE_SIZE];    uint8_t buffer[2 * BUFFER_SIZE];    size_t offset;double slow_start = microtime();offset = 0;for(int i = 0; i &lt; NUMBER_RUNS; i++){    const size_t part1 = min(MESSAGE_SIZE, BUFFER_SIZE - offset);    const size_t part2 = MESSAGE_SIZE - part1;    memcpy(buffer + offset, message, part1);    memcpy(buffer, message + part1, part2);    offset = (offset + MESSAGE_SIZE) % BUFFER_SIZE;}double slow_stop = microtime();double fast_start = microtime();offset = 0;for(int i = 0; i &lt; NUMBER_RUNS; i++){    memcpy(&amp;buffer[offset], message, MESSAGE_SIZE);    offset = (offset + MESSAGE_SIZE) % BUFFER_SIZE;}double fast_stop = microtime();printf("slow: %f microseconds per write\n", (slow_stop - slow_start) / NUMBER_RUNS);printf("fast: %f microseconds per write\n", (fast_stop - fast_start) / NUMBER_RUNS);return 0;}

На моем i5-6400 я получаю довольно единообразные результаты, выглядящие примерно так:

slow: 0.012196 microseconds per writefast: 0.004024 microseconds per write

Обратите внимание, что код с одним memcpy примерно в три раза быстрее, чем код с двумя memcpy. У нас гораздо большее преимущество по сравнению с наивным побайтовым копированием, которое составило 0,104943 микросекунды на запись. Этот микро-бенчмарк не полностью отражает всю логику очереди (на которую могут влиять такие условия, как сбои страниц и промахи TLB), однако дает нам хорошее указание на то, что наша оптимизация была успешной.

Что только что произошло?

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

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


В преддверии старта онлайн-курса Программист С приглашаем всех желающих на открытый демо-урок Жизненный цикл программы на C под UNIX. На этом вебинаре мы рассмотрим полный жизненный цикл программы на языке C под UNIX-подобной ОС на примере системы из семейства BSD. Начнём с исходного кода и закончим загрузкой готового выполняемого файла. По ходу дела посмотрим под капот различным низкоуровневым механизмам операционной системы и тулчейна компиляции и познакомимся с инструментарием UNIX для анализа программ. Присоединяйтесь!

- Узнать подробнее о курсе Программист С

- Смотреть вебинар Жизненный цикл программы на C под UNIX

Подробнее..

Отказоустойчивый кластер PostgreSQL с помощью crm

08.06.2021 14:19:38 | Автор: admin
Автор Игорь Косенков, инженер postgres Professional

Привет всем! Сегодня речь пойдет о кластере. Да, снова об отказоустойчивом кластере на базе Corosync/Pacemaker. Только настраивать мы его будем не как обычно с помощью утилиты pcs, а с помощью мало используемой утилиты crm.

С точки зрения использования этих утилит (pcs и crm) весь мир Unix-like операционок делится на два вида:
  • содержит пакеты утилиты pcs (RHEL, CentOS, Debian, Ubuntu);
  • содержит пакеты утилиты crm (SLES, Opensuse, Elbrus, Leningrad и т.д.).

crm cluster resource manager специальная утилита, которая используется для создания и управления отказоустойчивым кластером. Она включена в пакет crmsh, который обычно не входит в состав самых распространенных дистрибутивов Linux.

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

В то же время, если спросить у поисковика про утилиту настройки кластера pcs, которая является по функционалу такой же утилитой, как и crm, то информации будет много. Есть даже несколько статей на Хабре (в том числе и моя статья Кластер pacemaker/corosync без валидола).

Утилита crm такая же мощная и гибкая, как и pcs, но незаслуженно обделена вниманием.

Решено было исправить этот пробел и написать статью.

Причины, по которым те или иные разработчики дистрибутивов предпочитают кто crm, а кто pcs, мне неизвестны. Могу предположить, что все дело в зависимостях. Например, если сравнить количество зависимостей у pcs и crm, то получается такая картина:
$ sudo rpm -qpR crmsh-3.0.1-1.el7.centos.noarch.rpm | wc -l19$ sudo rpm -qpR pcs-0.9.169-3.el7.centos.x86_64.rpm | wc -l50

Сторонники минимализма, скорей всего, предпочтут crmsh. А если еще учесть, что pcs тянет за собой ruby, openssl, pam и python, а crmsh только python, то выбор в некоторых случаях будет однозначно на стороне crm. В каких случаях? Ну, например, при сертификации ОС есть некоторые трудности с пакетом ruby. Также известны случаи, когда в банковских структурах служба безопасности не разрешает установку нерегламентированного ПО.

Сходства и различия


У утилиты crm есть как сходства, так и различия с известной всем утилитой pcs.
Сходства утилит приведены в таблице 1:



На этом сходства заканчиваются и дальше идут различия, которых много, поэтому привожу лишь некоторые из них (таблица 2):



Различия начинаются с самого начала с момента инициализации кластера. Инициализировать кластер с помощью crm можно одной командой, а у pcs это происходит в два этапа авторизация и инициализация.

Удалить кластер (разобрать) у pcs можно одной командой сразу, а у crm необходимо удалять по одному узлу до тех пор, пока их не останется в кластере.

Чтобы изменить параметры ресурса, который мы уже создали в кластере, у pcs есть опция update. У crm такой опции нет, но есть команда configure edit, которая позволяет менять любые настройки кластера налету и мгновенно. Даже больше мы можем за один прием отредактировать любое количество параметров и ресурсов, и в конце редактирования применить все изменения сразу. Удобно? Думаю, да.

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

У crm в стандартной поставке нет веб-инструмента, но зато он есть в коммерческой версии SUSE HAWK.

Подготовка к настройке отказоустойчивого кластера


Лучший способ узнать и познакомиться с crm это настроить отказоустойчивый кластер.

Чем мы сейчас и займемся. Для примера возьмем ОС CentOS 7.9.

Для создания отказоустойчивого кластера PostgreSQL нам понадобится стенд, состоящий из 3-х узлов node1, node2, node3. На каждом узле установлена ОС CentOS 7.9 и пакеты corosync, pacemaker, fence-agents* (агенты фенсинга).

В качестве СУБД будем использовать Postgres Pro Standard v.11, но вы можете с таким же успехом использовать ванильную версию PostgreSQL. В нашей системе установлены необходимые пакеты postgrespro-std-11-server, postgrespro-std-11-libs, postgrespro-std-11-contrib, postgrespro-std-11-client.

Настройки СУБД (postgresql.conf) и доступа к ней (pg_hba.conf) не рассматриваются в данной статье, информации об этом достаточно в интернете. На одном из узлов (например, node1) необходимо инициализировать базу данных с помощью initdb, а на двух других узлах с помощью pg_basebackup скопировать базу данных с node1.

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

ПРИМЕЧАНИЕ:
В этом разделе все команды необходимо выполнить на всех узлах кластера.
Поскольку пакет crmsh не входит в состав дистрибутива ОС, то необходимо подключить репозиторий
Extra OKay Packages for Enterprise Linux с этим пакетом.
node1,2,3$ sudo rpm -ivh http://repo.okay.com.mx/centos/7/x86_64/release/okay-release-1-5.el7.noarch.rpm

Нам также понадобится репозитарий EPEL:
node1,2,3$ sudo yum install epel-releasenode1,2,3$ sudo yum update

Устанавливаем пакет crmsh:
node1,2,3$ sudo yum install crmsh

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

Разработчики crmsh вставили код вызова этой кластерной утилиты для удобства настройки. Можно, конечно, обойтись и без csync2, но в таком случае будет больше ручной работы с конфигурационными файлами на каждом узле.

ОТСТУПЛЕНИЕ:
Сервис csync2 может использоваться не только для создания отказоустойчивого кластера Corosync/Pacemaker. Например, если есть несколько серверов, у которых меняются конфигурационные файлы и эти файлы периодически нужно синхронизировать по критерию самый свежий файл.


Итак, устанавливаем csync2 и простейшую базу данных для хранения мета-данных (sqlite).
$ sudo yum install csync2 libsqlite3x-devel

Тут нас поджидает подводный камень.

Поскольку csync2 и crmsh не являются родными для CentOS, то без дополнительных танцев сразу после установки они не заработают. Вызов crm влечет вызов утилиты csync2, которой в свою очередь не хватает парочки systemd-юнитов. Почему этих файлов нет в пакете csync2 для CentOS мне неизвестно. Замечу, что в коммерческом дистрибутиве SLES (crmsh там родной) все необходимые файлы есть, все работает из коробки сразу после установки пакетов.
Итак, создадим и добавим недостающие systemd-юниты.
Первый называется csync2.socket и содержит:
[Socket]ListenStream=30865Accept=yes[Install]WantedBy=sockets.target

Второй называется csync2@.service с таким содержимым:
[Unit]Description=csync2 connection handlerAfter=syslog.target[Service]ExecStart=-/usr/sbin/csync2 -i -vStandardInput=socketStandardOutput=socket

Оба файла нужно разместить в стандартной папке systemd /usr/lib/systemd/system.

Юнит, относящийся к сокету, нужно активировать и установить в автозапуск при загрузке ОС:
node1,2,3$ sudo systemctl enable --now csync2.socket

Примечание. Все настройки выполнялись для ОС CentOS, но есть подозрение, что эти действия также понадобятся и для других систем, например, для Debian или Ubuntu.

Теперь у нас все готово к началу работ по настройке кластера.

Настройка кластера с помощью crm


Настройка кластера производится в 2 этапа инициализация, затем создание и добавление ресурсов. Инициализация кластера с настроенным сервисом синхронизации конфигураций csync2 производится на одном узле.

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

На всякий случай сначала удалим кластер (на всех узлах) с помощью такого набора команд:
node1,2,3$ sudo systemctl stop corosync;sudo find /var/lib/pacemaker/cib/ -type f -delete; sudo find -f /etc/corosync/ -type f -delete

Далее надо выполнить команду инициализации кластера:
node1$ sudo crm cluster init --name demo-cluster --nodes "node1 node2 node3" --yes

где demo-cluster название нашего кластера.

По этой команде создаются необходимые файлы в папке /etc/corosync: corosync.conf, ключ авторизации authkey, а также прописываются ssh-ключи для беспарольной авторизации и выполнения команд в кластере с привилегиями суперпользователя root (на всех трех узлах кластера).

По умолчанию инициализация кластера выполняется в режиме multicast. Но есть также возможность проинициализировать кластер в режиме unicast:
node1$ sudo crm cluster init --unicast --name demo-cluster --nodes "node1 node2 node3" --yes

Кластер проинициализирован и запущен.
Проверить работоспособность можно с помощью консольного монитора состояния кластера crm_mon:
node1$ sudo crm_mon -Afr

Далее можно приступать к созданию ресурсов в кластере.

Создание ресурсов в кластере


Для начала поменяем некоторые значению по умолчанию. Например, порог миграции ресурсов migration-threshold по умолчанию равен 0. Меняем на 1, чтобы после первого сбоя ресурсы мигрировали на другой узел.

node1$ sudo crm configure rsc_defaults rsc-options: migration-threshold=1 resource-stickiness=INFINITY

По умолчанию, кластер запускается в симметричном режиме.

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

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

Зачем это нужно? Ну, например, в ситуации, когда в кластере ресурсы запускаются не на всех узлах, т.е. узлы неравноценны по ресурсам и назначению.

Если, вдруг, вам когда-то понадобится изменить режим кластера с симметричного на несимметричный, то достаточно ввести команду:
node1$ sudo crm configure property symmetric-cluster=false

Мы оставим этот параметр без изменения.

Включаем механизм stonith:
node1$ sudo crm configure property stonith-enabled=yes

Создадим и добавим ресурс виртуальный IP адрес:
node1$ sudo crm configure primitive master-vip IPaddr2 op start timeout=20s interval=0 op stop timeout=20s interval=0 op monitor timeout=20s interval=10s params ip=<virtual IP> nic=eth0

где <virtual IP> виртуальный IP-адрес в кластере.

С помощью монитора состояния кластера crm_mon можно убедиться в том, что ресурс успешно создан и запущен на первом попавшемся узле:
node1$ sudo crm_mon -Afr

Создадим ресурс postgresql и назовем его pg:
node1$ sudo crm configure primitive pg pgsql op start interval=0 timeout=120s op stop interval=0 timeout=120s op monitor interval=30s timeout=30s op monitor interval=29s role=Master timeout=30s params pgctl="/opt/pgpro/std-11/bin/pg_ctl" psql="/opt/pgpro/std-11/bin/psql" pgdata="/var/lib/pgpro/std-11/data" pgport="5432" repuser=postgres master_ip=<virtual IP> rep_mode=sync node_list="node1 node2 node3"

ПРИМЕЧАНИЕ:
В данном примере пути расположения бинарников и БД указаны по умолчанию для версии Postgres Pro Std 11. Также для упрощения указан пользователь для репликации postgres. Но ничто не мешает вам изменить умолчательные пути и пользователя репликации на свои.


Хочу обратить внимание на параметр rep_mode: он задан sync. Это означает, что в отказоустойчивом кластере хотя бы одна реплика будет синхронной. Синхронность реплики в кластере обеспечивает RPO=0 (кластер без потерь данных в случае сбоя).

Зададим тип ресурса Master-Standby (ms):
node1$ sudo crm configure ms mspg pg meta target-role=Master clone-max="3"

Нам нужно, чтобы ресурсы vip-master и mspg в режиме мастер запускались на одном узле:
node1$ sudo crm configure colocation pgsql-colocation inf: master-vip:Started mspg:Master

Указываем порядок запуска ресурсов сначала СУБД в режиме мастер, потом виртуальный IP:
node1$ sudo crm configure order order-promote-pgsql Mandatory: mspg:promote master-vip:start

Таким образом, мы создали 2 необходимых ресурса виртуальный IP адрес и ресурс postgresql.

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

Фенсинг узлов


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

Для начала можно ознакомиться со списком всех агентов фенсинга:
node1$ sudo crm ra list stonith

На моем стенде node1, node2, node3 это виртуальные машины, которые запущены и управляются с помощью гипервизора KVM. Соответственно, ресурс-агент фенсинга для KVM называется fence_virsh.

Вывести полную информацию о fence_virsh:
node1$ sudo crm ra info stonith:fence_virsh

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

Проверка работоспособности фенсинга для узла node1 выглядит так:
node1$ fence_virsh -a <hypervisor IP> -l <username>-p <password> -n node1 -x --use-sudo -o status

где username & password учетная запись на хосте гипервизора.

Фенсинг для node1 настраивается так:
node1$ sudo crm configure primitive fence-node1 stonith:fence_virsh params ipaddr=<hypervisor IP> ip=<hypervisor IP> login=<username> username=<username> passwd=<password> pcmk_host_list=node1 sudo=1 op monitor interval=60s

ПРИМЕЧАНИЕ:
Ресурсы фенсинга не должны запускаться на своих узлах, иначе фенсинг может не сработать.

Следующее правило расположения запретит ресурсу фенсинга для узла node1 располагаться на этом узле:
node1$ sudo crm configure location l_fence_node1 fence-node1 -inf: node1

Для node2:
node1$ sudo crm configure primitive fence-node2 stonith:fence_virsh params ipaddr=<hypervisor IP> ip=<hypervisor IP> login=<username> username=<username> passwd=<password> pcmk_host_list=node2 sudo=1 op monitor interval=60snode1$ sudo crm configure location l_fence_node2 fence-node2 -inf: node2

Для node3:
node1$ sudo crm configure primitive fence-node3 stonith:fence_virsh params ipaddr=<hypervisor IP> ip=<hypervisor IP> login=<username> username=<username> passwd=<password> pcmk_host_list=node3 sudo=1 op monitor interval=60snode1$ sudo crm configure location l_fence_node3 fence-node3 -inf: node3

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

Инициализация кластера с помощью crm без csync2


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

Сначала вариант с использованием multicast.

Все команды выполняются на одном узле, например, на node1.
node1$ sudo crm cluster init --name demo-cluster --nodes "node1" --yes

По этой команде создаются необходимые файлы в папке /etc/corosync: corosync.conf, ключ авторизации authkey.

Далее нужно скопировать авторизационный файл authkey и corosync.conf на узлы node2 и node3:
node1$ sudo scp /etc/corosync/{authkey,corosync.conf} node2:/etc/corosync/node1$ sudo scp /etc/corosync/{authkey,corosync.conf} node3:/etc/corosync/

На остальных узлах (на node1 кластер уже запущен) запустить кластер:
node2,3$ sudo crm cluster start<source>С помощью монитора crm_mon можно убедиться, что кластер проинициализирован и запущен:<source lang="sh">node1$ sudo crm_mon -Afr


В случае настройки кластера с использованием unicast действия и команды будут отличаться.

Все команды выполняются на одном узле, например, на node1.
node1$ sudo crm cluster init --unicast --name demo-cluster --nodes "node1" --yes

Открываем файл /etc/corosync/corosync.conf и добавляем строки в секцию nodelist:
node {ring0_addr: node2nodeid: 2}node {ring0_addr: node3nodeid: 3}

В секции quorum меняем число голосов:

expected_votes: 3

Далее необходим рестарт сервиса corosync на первом узле:
node1$ sudo systemctl restart corosync

Затем нужно скопировать файл authkey и отредактированный corosync.conf на узлы node2 и node3:
node1$ sudo scp /etc/corosync/{authkey,corosync.conf} node2:/etc/corosync/node1$ sudo scp /etc/corosync/{authkey,corosync.conf} node3:/etc/corosync/

На остальных узлах (на node1 кластер уже запущен) запустить кластер:
node2,3$ sudo crm cluster start

С помощью монитора crm_mon можно убедиться, что кластер проинициализирован и запущен:
node1$ sudo crm_mon -Afr

На этом инициализация кластера без csync2 закончена.

Вспомогательные команды crm



При работе с кластером могут пригодиться некоторые crm-команды.
Для удобства команды и пояснения сведены в таблицу 3:



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

Основы Bash-скриптинга для непрограммистов. Часть 2

30.01.2021 20:16:42 | Автор: admin

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

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

Скрипты

Для выполнения нескольких команд одним вызовом удобно использовать скрипты. Скрипт это текстовый файл, содержащий команды для shell. Это могут быть как внутренние команды shell, так и вызовы внешних исполняемых файлов.

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

Перейдем в домашнюю директорию командой cd ~ и создадим в ней с помощью редактора nano (nano script.sh)файл, содержащий 2 строки:

#!/bin/bashecho Hello!

Чтобы выйти из редактора nano после набора текста скрипта, нужно нажать Ctrl+X, далее на вопрос "Save modified buffer?" нажать Y, далее на запрос "File Name to Write:" нажать Enter. При желании можно использовать любой другой текстовый редактор.

Скрипт запускается командой ./<имя_файла>, т.е. ./ перед именем файла указывает на то, что нужно выполнить скрипт или исполняемый файл, находящийся в текущей директории. Если выполнить команду script.sh, то будет выдана ошибка, т.к. оболочка будет искать файл в директориях, указанных в переменной среды PATH, а также среди встроенных команд (таких, как, например, pwd):

test@osboxes:~$ script.shscript.sh: command not found

Ошибки не будет, если выполнять скрипт с указанием абсолютного пути, но данный подход является менее универсальным: /home/user/script.sh. Однако на данном этапе при попытке выполнить созданный файл будет выдана ошибка:

test@osboxes:~$ ./script.sh-bash: ./script.sh: Permission denied

Проверим права доступа к файлу:

test@osboxes:~$ ls -l script.sh-rw-rw-r-- 1 test test 22 Nov  9 05:27 script.sh

Из вывода команды ls видно, что отсутствуют права на выполнение. Рассмотрим подробнее на картинке:

Права доступа задаются тремя наборами: для пользователя, которому принадлежит файл; для группы, в которую входит пользователь; и для всех остальных. Здесь r, w и x означают соответственно доступ на чтение, запись и выполнение.

В нашем примере пользователь (test) имеет доступ на чтение и запись, группа также имеет доступ на чтение и запись, все остальные только на чтение. Эти права выданы в соответствии с правами, заданными по умолчанию, которые можно проверить командой umask -S. Изменить права по умолчанию можно, добавив вызов команды umask с нужными параметрами в файл профиля пользователя (файл ~/.profile), либо для всех пользователей в общесистемный профиль (файл /etc/profile).

Для того, чтобы установить права, используется команда chmod <параметры> <имя_файла>. Например, чтобы выдать права на выполнение файла всем пользователям, нужно выполнить команду:

test@osboxes:~$ chmod a+x script.sh

Чтобы выдать права на чтение и выполнение пользователю и группе:

test@osboxes:~$ chmod ug+rx script.sh

Чтобы запретить доступ на запись (изменение содержимого) файла всем:

test@osboxes:~$ chmod a-w script.sh

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

test@osboxes:~$ chmod 754 script.sh

Будут выданы права -rwxr-xr--:

test@osboxes:~$ ls -la script.sh-rwxr-xr-- 1 test test 22 Nov  9 05:27 script.sh

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

Символ перед наборами прав доступа указывает на тип файла ( означает обычный файл, d директория, l ссылка, c символьное устройство, b блочное устройство, и т. д.). Соответствие числа, его двоичного представления и прав доступ можно представить в виде таблицы:

Число

Двоичный вид

Права доступа

0

000

Нет прав

1

001

Только выполнение (x)

2

010

Только запись (w)

3

011

Запись и выполнение (wx)

4

100

Только чтение (r)

5

101

Чтение и выполнение (rx)

6

110

Чтение и запись (rw)

7

111

Чтение, запись и выполнение (rwx)

Выдав права на выполнение, можно выполнить скрипт:

test@osboxes:~$ ./script.shHello!

Первая строка в скрипте содержит текст #!/bin/bash. Пара символов #! называется Шебанг (англ. shebang) и используется для указания интерпретатору, с помощью какой оболочки выполнять указанный скрипт. Это гарантирует корректность исполнения скрипта в нужной оболочке в случае, если у пользователя будет указана другая.

Также в скриптах можно встретить строку #!/bin/sh. Но, как правило, /bin/sh является ссылкой на конкретный shell, и в нашем случае /bin/sh ссылается на /bin/dash, поэтому лучше явно указывать необходимый интерпретатор. Вторая строка содержит команду echo Hello!, результат работы которой мы видим в приведенном выводе.

Параметры скриптов

Для того, чтобы обеспечить некоторую универсальность, существует возможность при вызове передавать скрипту параметры. В этом случае вызов скрипта будет выглядеть так: <имя_скрипта> <параметр1> <параметр2> , например ./script1.sh Moscow Russia.

Для того, чтобы получить значение первого параметра, необходимо в скрипте указать $1, второго - $2, и т.д. Существует также ряд других переменных, значения которых можно использовать в скрипте:
$0 имя скрипта
$# количество переданных параметров
$$ PID(идентификатор) процесса, выполняющего скрипт
$? код завершения предыдущей команды

Создадим файл script1.sh следующего содержания:

#!/bin/bashecho Hello, $USER!printf "Specified City is: %s, Country is: %s\n" $1 $2

Выдадим права на выполнение и выполним скрипт с параметрами:

test@osboxes:~$ chmod u+x script1.shtest@osboxes:~$ ./script1.sh Moscow RussiaHello, test!Specified City is: Moscow, Country is: Russia

Мы передали 2 параметра, указывающие город и страну, и использовали их в скрипте, чтобы сформировать строку, выводимую командой printf. Также для вывода в строке Hello использовали имя пользователя из переменной USER.

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

test@osboxes:~$ ./script1.sh "San Francisco" "United States"Hello, test!Specified City is: San Francisco, Country is: United States

При этом нужно доработать скрипт, чтобы в команду printf параметры также передавались в кавычках:

printf "Specified City is: %s, Country is: %s\n" "$1" "$2"

Из приведенных примеров видно, что при обращении к переменной для получения её значения используется символ $. Для того, чтобы сохранить значение переменной просто указывается её имя:

COUNTRY=RUSSIAecho $COUNTRY

Операторы условного выполнения, выбора и циклы

Так же, как и в языках программирования, в bash существуют операторы условного выполнения выполнение определенных действий при определенных условиях. Кроме того, существует возможность повторного выполнения определенного блока команд пока выполняется заданное условие операторы цикла. Рассмотрим каждый из них подробнее.

Оператор условного выполнения представляет собой конструкцию вида:

if [ <условие> ]then  <команда1>else  <команда2>fi

Создадим скрипт, проверяющий длину введенной строки (например, для проверки длины пароля), которая должна быть не меньше (т.е. больше) 8 символов:

#!/bin/bashecho Hello, $USER!echo -n "Enter string: "read strif [ ${#str} -lt 8 ]then  echo String is too shortelse  echo String is okfi

Выполним 2 теста, с длиной строки 5 и 8 символов:

test@osboxes:~$ ./script2.shHello, test!Enter string: abcdeString is too shorttest@osboxes:~$ ./script2.shHello, test!Enter string: abcdefghString is ok

Командой read str мы получаем значение, введенное пользователем и сохраняем его в переменную str. С помощью выражения ${#str} мы получаем длину строки в переменной str и сравниваем её с 8. Если длина строки меньше, чем 8 (-lt 8), то выдаем сообщение String is too short, иначе String is ok.

Условия можно комбинировать, например, чтобы указать, чтоб длина должна быть не меньше восьми 8 и не больше 16 символов, для условия некорректных строк нужно использовать выражение [ ${#str} -lt 8 ] || [ ${#str} -gt 16 ]. Здесь || означает логическое "ИЛИ", а для логического "И" в bash используется &&.

Условия также могут быть вложенными:

#!/bin/bashecho Hello, $USER!echo -n "Enter string: "read strif [ ${#str} -lt 8 ]then  echo String is too shortelse  if [ ${#str} -gt 16 ]  then    echo String is too long  else    echo String is ok  fifi

Здесь мы сначала проверяем, что строка меньше 8 символов, отсекая минимальные значения, и выводим "String is too short", если условие выполняется. Если условие не выполняется(строка не меньше 8 символов) - идем дальше(первый else) и проверяем, что строка больше 16 символов. Если условие выполняется - выводим "String is too long", если не выполняется(второй else) - выводим "String is ok".

Результат выполнения тестов:

test@osboxes:~$ ./script2.shHello, test!Enter string: abcdefString is too shorttest@osboxes:~$ ./script2.shHello, test!Enter string: abcdefghijklmnopqrstuvString is too longtest@osboxes:~$ ./script2.shHello, test!Enter string: abcdefghijklString is ok

Оператор выбора выглядит следующим образом:

case "$переменная" in "$значение1" ) <команда1>;; "$значение2" ) <команда2>;;esac

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

#!/bin/bashecho -n "Enter the name of planet: "read PLANETecho -n "The $PLANET has "case $PLANET in  Mercury | Venus ) echo -n "no";;  Earth ) echo -n "one";;  Mars ) echo -n "two";;  Jupiter ) echo -n "79";;  *) echo -n "an unknown number of";;esacecho " satellite(s)."

Тест:

test@osboxes:~$ ./script3.shEnter the name of planet: MercuryThe Mercury has no satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: VenusThe Venus has no satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: EarthThe Earth has one satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: MarsThe Mars has two satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: JupiterThe Jupiter has 79 satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: Alpha555The Alpha555 has an unknown number of satellite(s).

Здесь в зависимости от введенного названия планеты скрипт выводит количество её спутников.
В case мы использовали выражение Mercury | Venus, где | означает логическое "ИЛИ" (в отличие от if, где используется ||), чтобы выводить "no" для Меркурия и Венеры, не имеющих спутников. В case также можно указывать диапазоны с помощью []. Например, скрипт для проверки принадлежности диапазону введенного символа будет выглядеть так:

#!/bin/bashecho -n "Enter key: "read -n 1 keyechocase "$key" in  [a-z]   ) echo "Lowercase";;  [A-Z]   ) echo "Uppercase";;  [0-9]   ) echo "Digit";;  *       ) echo "Something else";;esac

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

test@osboxes:~$ ./a.shEnter key: tLowercasetest@osboxes:~$ ./a.shEnter key: PUppercasetest@osboxes:~$ ./a.shEnter key: 5Digittest@osboxes:~$ ./a.shEnter key: @Something else

Цикл может задаваться тремя разными способами:

Выполняется в интервале указанных значений (либо указанного множества):

for [ <условие> ] do <команды> done

Выполняется, пока соблюдается условие:

while [ <условие> ] do <команды> done

Выполняется, пока не перестанет соблюдаться условие:

until [ <условие> ] do <команды> done

Добавим в скрипт с планетами цикл с условием while и будем выходить из скрипта, если вместо имени планеты будет введено EXIT

#!/bin/bashPLANET="-"while [ $PLANET != "EXIT" ]do  echo -n "Enter the name of planet: "  read PLANET  if [ $PLANET != "EXIT" ]  then.    echo -n "The $PLANET has "    case $PLANET in      Mercury | Venus ) echo -n "no";;      Earth ) echo -n "one";;      Mars ) echo -n "two";;      Jupiter ) echo -n "79";;      *) echo -n "an unknown number of";;    esac  echo " satellite(s)."  fidone

Здесь мы также добавили условие, при котором оператор выбора будет выполняться только в случае, если введено не EXIT. Таким образом, мы будем запрашивать имя планеты и выводить количество её спутников до тех пор, пока не будет введено EXIT:

test@osboxes:~$ ./script4.shEnter the name of planet: EarthThe Earth has one satellite(s).Enter the name of planet: JupiterThe Jupiter has 79 satellite(s).Enter the name of planet: Planet123The Planet123 has an unknown number of satellite(s).Enter the name of planet: EXIT

Нужно отметить, что условие while [ $PLANET != "EXIT" ] можно заменить на until [ $PLANET == "EXIT" ]. == означает "равно", != означает "не равно".

Приведем пример циклов с указанием интервалов и множеств:

#!/bin/bashrm *.datecho -n "File count: "read countfor (( i=1; i<=$count; i++ ))do  head -c ${i}M </dev/urandom >myfile${i}mb.datdonels -l *.datecho -n "Delete file greater than (mb): "read maxsizefor f in *.datdo  size=$(( $(stat -c %s $f) /1024/1024))  if [ $size -gt $maxsize ]  then.    rm $f    echo Deleted file $f  fidonels -l *.datread

Сначала мы запрашиваем у пользователя количество файлов, которые необходимо сгенерировать (read count).

В первом цикле (for (( i=1; i<=$count; i++ ))) мы генерируем несколько файлов, количество которых задано в переменной count, которую введет пользователь. В команду head передаем количество мегабайт, считываемых из устройства /dev/random, чтение из которого позволяет получать случайные байты.

Символ < указывает перенаправление входного потока (/dev/urandom) для команды head.

Символ > указывает перенаправление выходного потока (вывод команды head -c ${i}M ) в файл, имя которого мы генерируем на основе постоянной строки с добавлением в неё значения переменной цикла (myfile${i}mb.dat).

Далее мы запрашиваем размер, файлы больше которого необходимо удалить.

Во втором цикле (for f in *.dat) мы перебираем все файлы .dat в текущей директории и сравниваем размер каждого файла со значением, введенным пользователем. В случае, если размер файла больше, мы удаляем этот файл.

В конце скрипта выводим список файлов .dat, чтобы отобразить список оставшихся файлов (ls -l *.dat). Результаты теста:

test@osboxes:~$ ./script5.shFile count: 10-rw-rw-r-- 1 test test 10485760 Nov  9 08:48 myfile10mb.dat-rw-rw-r-- 1 test test  1048576 Nov  9 08:48 myfile1mb.dat-rw-rw-r-- 1 test test  2097152 Nov  9 08:48 myfile2mb.dat-rw-rw-r-- 1 test test  3145728 Nov  9 08:48 myfile3mb.dat-rw-rw-r-- 1 test test  4194304 Nov  9 08:48 myfile4mb.dat-rw-rw-r-- 1 test test  5242880 Nov  9 08:48 myfile5mb.dat-rw-rw-r-- 1 test test  6291456 Nov  9 08:48 myfile6mb.dat-rw-rw-r-- 1 test test  7340032 Nov  9 08:48 myfile7mb.dat-rw-rw-r-- 1 test test  8388608 Nov  9 08:48 myfile8mb.dat-rw-rw-r-- 1 test test  9437184 Nov  9 08:48 myfile9mb.datDelete file greater than (mb): 5Deleted file myfile10mb.datDeleted file myfile6mb.datDeleted file myfile7mb.datDeleted file myfile8mb.datDeleted file myfile9mb.dat-rw-rw-r-- 1 test test 1048576 Nov  9 08:48 myfile1mb.dat-rw-rw-r-- 1 test test 2097152 Nov  9 08:48 myfile2mb.dat-rw-rw-r-- 1 test test 3145728 Nov  9 08:48 myfile3mb.dat-rw-rw-r-- 1 test test 4194304 Nov  9 08:48 myfile4mb.dat-rw-rw-r-- 1 test test 5242880 Nov  9 08:48 myfile5mb.dat

Мы создали 10 файлов (myfile1mb.dat .. myfile10mb.dat) размером от 1 до 10 мегабайт и далее удалили все файлы .dat размером больше 5 мегабайт. При этом для каждого удаляемого файла вывели сообщение о его удалении (Deleted file myfile10mb.dat). В конце вывели список оставшихся файлов (myfile1mb.dat .. myfile5mb.dat).

В следующей части мы рассмотрим функции, планировщик заданий cron, а также различные полезные команды.

Подробнее..
Категории: *nix , Linux , Unix , Ubuntu , Bash , Bash scripting , Ssh , Virtualbox , Debian , Shells

Основы Bash-скриптинга для непрограммистов. Часть 3

16.02.2021 20:15:11 | Автор: admin

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

Функции

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

Определение функции выглядит следующим образом:

<имя_функции>() {  <команды>  return <число>}funciton <имя_функции>() {  <команды>  return <число>}

Первый вариант ближе к синтаксису языка С и считается более переносимым, во втором варианте круглые скобки можно не указывать. Также может отсутствовать оператор return, если функция не возвращает значения.

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

#!/bin/bashf() {  echo Test}f

Мы объявили функцию f, которая выводит слово Test, и затем вызвали её:

test@osboxes:~$ ./script6.shTest

Так же, как и скрипт, функция может принимать параметры и использовать их, ссылаясь по номеру ($1, $2, , $N). Вызов функции с параметрами в скрипте осуществляется так:

<имя функции> <параметр1> <параметр2> <параметрN>

Функция может возвращать результат своего выполнения (код завершения) в виде числового значения в диапазоне от 0 до 255. Принято считать, что если функция возвращает 0, то она выполнилась успешно, во всех остальных случаях значение содержит код ошибки. Чтобы получить код завершения функции в скрипте, необходимо обратиться к переменной $?. Добавив параметры и возвращаемое значение, получим следующий скрипт:

#!/bin/bashsumm() {  re='^[0-9]+$'  if ! [[ $1 =~ $re ]] ; then    return 1  elif ! [[ $2 =~ $re ]] ; then    return 2  else    s=$(($1 + $2))    return 0  fi}summ $1 $2case $? in 0) echo "The sum is: $s" ;; 1) echo "var1 is not a nubmer" ;; 2) echo "var2 is not a nubmer" ;; *) echo "Unknown error" ;;esac

Здесь мы создали функцию summ, которая принимает 2 параметра и с помощью регулярного выражения ^[0-9]+$ проверяет, является ли каждый из переданных параметров числом. В случае, если первый параметр не число, то код завершения функции будет 1, если второй параметр не число, то код завершения функции будет 2. Во всех остальных случаях функция вычисляет сумму переданных параметров, сохраняя результат в глобальной переменной s.

Скрипт вызывает функцию, передавая её на вход параметры, которые были переданы ему самому при вызове. Далее проверяется код завершения функции и выдается соответствующая ошибка, если код не равен 0, иначе выдается сумма, сохраненная в переменной s. Протестируем скрипт:

test@osboxes.org:~$ ./script7.sh abc 123var1 is not a nubmertest@osboxes.org:~$ ./script7.sh 234 defvar2 is not a nubmertest@osboxes.org:~$ ./script7.sh 10 15The sum is: 25

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

Для того, чтобы объявить переменную локальной, используется слово local, например local loc_var=123. Важно отметить, все что переменные, объявляемые в теле функции, считаются необъявленными до тех пор, пока эта функция не будет вызвана.

Объединим все воедино, создав на основе рассмотренных ранее структур следующий скрипт:

#!/bin/bashclearFiles() {  rm *.dat  if [ $? -eq 0 ]  then    echo Files deleted  fi}genFiles() {  for (( i=1; i<=$1; i++ ))  do    head -c ${i}M </dev/urandom >myfile${i}mb.dat  done  ls -l *.dat}delFiles() {for f in *.dat  do    size=$(( $(stat -c %s $f) /1024/1024 ))    if [ $size -gt $1 ]    then      rm $f      echo Deleted file $f    fi  done  ls -l *.dat}showWeather() {  curl -s "https://weather-broker-cdn.api.bbci.co.uk/en/observation/rss/$1" | grep "<desc" | sed -r 's/<description>//g; s/<\/description>//g'}menu() {  clear  echo 1 - Delete all .dat files  echo 2 - Generate .dat files  echo 3 - Delete big .dat files  echo 4 - List all files  echo 5 - Planet info  echo 6 - Show weather  echo "x/q - Exit"  echo -n "Choose action: "  read -n 1 key  echo}while truedo  case "$key" in    "x" | "q" | "X" | "Q") break ;;    "1")      clearFiles      read -n 1    ;;    "2")      echo -n "File count: "      read count      genFiles $count      read -n 1    ;;    "3")      echo -n "Delete file greater than (mb): "      read maxsize      delFiles $maxsize      read -n 1    ;;    "4")      ls -la      read -n 1    ;;    "5")      ./script4.sh      read -n 1    ;;    "6")      echo -n "Enter city code: " # 524901 498817 5391959      read citycode      showWeather $citycode      read -n 1    ;;  esac  menudone

В данном скрипте мы объявили 5 функций:

  • clearFiles

  • genFiles

  • delFiles

  • showWeather

  • menu

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

  • Удалить все файлы .dat в текущей директории

  • Создать указанное количество файлов

  • Удалить файлы больше определенного размера

  • Вывести список всех файлов текущей директории

  • Запустить скрипт, выдающий информацию о планетах

  • Отобразить погоду по коду указанного города

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

test@osboxes.org:~$ ./script8.sh1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 4total 40drwxr-xr-x 2 test test 4096 Feb 16 15:56 .drwxr-xr-x 6 root root 4096 Feb 16 15:54 ..-rw------- 1 test test   42 Feb 16 15:55 .bash_history-rw-r--r-- 1 test test  220 Feb 16 15:54 .bash_logout-rw-r--r-- 1 test test 3771 Feb 16 15:54 .bashrc-rw-r--r-- 1 test test  807 Feb 16 15:54 .profile-rw-r--r-- 1 test test 1654 Feb 16 12:40 input.xml-rwxr-xr-x 1 test test  281 Feb 16 14:02 script4.sh-rwxr-xr-x 1 test test  328 Feb 16 13:40 script7.sh-rwxr-xr-x 1 test test 1410 Feb 16 15:24 script8.sh
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 2File count: 8-rw-rw-r-- 1 test test 1048576 Feb 16 16:00 myfile1mb.dat-rw-rw-r-- 1 test test 2097152 Feb 16 16:00 myfile2mb.dat-rw-rw-r-- 1 test test 3145728 Feb 16 16:00 myfile3mb.dat-rw-rw-r-- 1 test test 4194304 Feb 16 16:00 myfile4mb.dat-rw-rw-r-- 1 test test 5242880 Feb 16 16:00 myfile5mb.dat-rw-rw-r-- 1 test test 6291456 Feb 16 16:00 myfile6mb.dat-rw-rw-r-- 1 test test 7340032 Feb 16 16:00 myfile7mb.dat-rw-rw-r-- 1 test test 8388608 Feb 16 16:00 myfile8mb.dat
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 3Delete file greater than (mb): 5Deleted file myfile6mb.datDeleted file myfile7mb.datDeleted file myfile8mb.dat-rw-rw-r-- 1 test test 1048576 Feb 16 16:00 myfile1mb.dat-rw-rw-r-- 1 test test 2097152 Feb 16 16:00 myfile2mb.dat-rw-rw-r-- 1 test test 3145728 Feb 16 16:00 myfile3mb.dat-rw-rw-r-- 1 test test 4194304 Feb 16 16:00 myfile4mb.dat-rw-rw-r-- 1 test test 5242880 Feb 16 16:00 myfile5mb.dat
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 1Files deleted
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 5Enter the name of planet: MarsThe Mars has two satellite(s).
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 6Enter city code: 524901    Latest observations for Moscow from BBC Weather, including weather, temperature and wind information      Temperature: -11C (11F), Wind Direction: Northerly, Wind Speed: 0mph, Humidity: 84%, Pressure: 1018mb, , Visibility: Moderate

Примечание: для тестирования работы с данными из Интернет (пункт 6 в меню выбора скрипта) может потребоваться установка curl, это можно сделать командой sudo apt install curl.

Планировщик заданий cron

В случае, когда есть необходимость периодического запуска скриптов, полезно использовать планировщик cron, он позволяет задать расписание запуска скрипта и не требует присутствия администратора.

Просмотр заданий пользователя выполняется командой crontab l. Для редактирования и создания новых задания используется команда crontab e. Строки для запуска команд планировщика в файле конфигурации cron имеют следующий формат:

m h dom mon dow command parameters

Где m минута, h час, dom день месяца, mon месяц, dow день недели, command команда, parameters список параметров. Наглядно этот формат можно представить так:

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

10,30 * * * 1-5 command parameter1 parameter2

Более простой пример, каждые 15 минут выполнять команду:

*/15 * * * * command

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

#!/bin/bashUSER=`whoami`BACKUP_DIR=/tmp/backup_${USER}BACKUP_FILE=${USER}_$(date +%Y%m%d%M%H%S).tgzmkdir -p $BACKUP_DIRcd /tar -zcf $BACKUP_DIR/$BACKUP_FILE home/$USER

Поставим скрипт на выполнение каждый день в 22:00, выполнив команду crontab -eи добавив с помощью открывшегося редактора строку:

00 22 * * * ./backup_home.sh

Проверить, что задача добавлена в планировщик, можно командой crontab -l:

test@osboxes.org:~$ crontab -l00 22 * * * ./backup_home.sh

В результате каждый день в 22:00 будет создаваться резервная копия домашней директории пользователя (в приведенном примере для демонстрации запуск скрипта выполняется каждую минуту):

test@osboxes.org:~$ cd /tmp/backup_test/test@osboxes:/tmp/backup_test$ lltotal 80drwxrwxr-x  2 test test 4096 Feb 16 16:38 ./drwxrwxrwt 17 root root 4096 Feb 16 16:30 ../-rw-rw-r--  1 test test 4431 Feb 16 16:30 test_20210216301601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:31 test_20210216311601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:32 test_20210216321601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:33 test_20210216331601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:34 test_20210216341601.tgztest@osboxes:/tmp/backup_test$

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

Список полезных команд

Список встроенных команд интерпретатора: help
Помощь по команде: <команда> --help
Мануал по команде: man <команда>
Версия команды: <команда> --version
Список доступных оболочек: cat /etc/shells
Список пользователей и их оболочек: cat /etc/passwd
Текущая директория: pwd
Список файлов текущей директории: ls -la
Текущий пользователь: id
Переменные среды: set
Версия ОС: cat /etc/os-release
Версия ядра: uname -a
Получить привилегии суперпользователя: sudo su -
Установка программы в Debian: apt install mc
Посмотреть утилизацию(загрузку): top
Свободное место: df -h
Сколько занимает директория: du -ks /var/log
Конфигурация сетевых интерфейсов: ifconfig -a
Объем оперативной памяти: free -m
Информация о блочных устройствах(дисках): lsblk
Информация о процессорах: cat /proc/cpuinfo
Список установленных пакетов: apt list --installed
Список и статус сервисов: service --status-all
Перезапуск сервиса: service apache2 restart
Скачать файл: wget https://www.gnu.org/graphics/gplv3-with-text-136x68.png
Получить веб-страницу по URL: curl https://www.google.com
Показать задания планировщика: crontab -l
Редактировать задания планировщика: crontab -e
Вывести новые сообщения в системном логе: tail -f /var/log/syslog
Подсчитать количество строк в выводе команды: <команда> | wc -l
Изменить права доступа к файлу (разрешить выполнение всем): chmod a+x <файл>
Список процессов: ps -ef
Проверить, запущен ли процесс: ps -ef | grep <процесс>
Перейти в предыдущий каталог: cd -
Завершить процесс (сигнал kill): kill -9
Удаление файла: rm <имя файла>
Удаление директории: rm -rf <имя директории>
Редактировать файл: nano <имя_файла>
Топ 10 процессов по использованию памяти: ps aux | awk '{print $6/1024 " MB\t\t" $11}' | sort -nr | head

Полезные ссылки

Руководство по bash: GNU Bash manual
Расширенное руководство по Bash: Advanced Bash-Scripting Guide
Статья на Википедии: Bash
Описание команд и утилит оболочки bash: SS64
Часто задаваемые вопросы о Debian GNU/Linux: Debian FAQ

Заключение

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

На этом пока все, надеюсь, было интересно!
Какие другие полезные команды вы знаете и используете в работе?
Какие интересные конструкции приходилось использовать?
Какие задачи решали?
Всем удачного скриптинга, делитесь мнениями в комментариях!

Подробнее..
Категории: *nix , Linux , Unix , Ubuntu , Bash , Bash scripting , Ssh , Virtualbox , Debian , Shells

Почему работать в консоли настолько приятно? Так задумано отцами-основателями Unix

19.04.2021 12:12:03 | Автор: admin

Кен Томпсон и Деннис Ритчи

Unix. Легендарная операционная система оказала огромное влияние на разработку программного обеспечения и всю информатику в целом. Из неё выросли целые семейства Unix-подобных ОС, которыми все мы пользуемся.

Язык программирования Си, Ричард Столлман и GNU, движение Open Source, Линус Торвальдс с ядром Linux, маки, айфоны и Android. Почти всё в системном программировании 21века можно отследить до истоков до Unix.

Unix это фундаментальная база. Но что же в ней такого особенного? Есть один секрет. Точнее, два.

Философия Unix


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

Во-вторых это была правильная операционная система. Сделанная в соответствии с некими общими принципами, в соответствии с цельной философией Unix.

Брайан Керниган и Роб Пайк в книге Unix. Программное окружение формулируют основную идею этой философии:

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

Многие Unix-программы по отдельности выполняют довольно тривиальные задачи, но будучи объединёнными с другими, образуют полный и полезный инструментарий.

Другими словами, философия Unix это взаимодействие программ. Как говорил в своей презентации Брайан Керниган, программы Unix словно строительные блоки, которые можно комбинировать, складывая в эффективные конструкции. Выход одной программы является входом другой программы. Своеобразный конвейер (пайп).

Команды сочетаются в конвейер не только стандартными, очевидными способами. Бывают и весьма нетривиальные сочетания.

Волшебные конвейеры


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

Список лидеров по числу коммитов


Начнём с простого примера: отобразить список авторов репозитория git, отсортированных по количеству коммитов. Если представить задачу в терминах конвейеров, то она довольно простая. Выводим логи коммитов командой git log. Опция --format=<format> позволяет указать, в каком формате отображать коммиты. В нашем случае --format='%an' выводит только имена авторов для каждого коммита.

$ git log --format='%an'AliceBobDeniseDeniseCandiceDeniseAliceAliceAlice

Теперь утилита sort сортирует их по алфавиту.

$ git log --format='%an' | sortAliceAliceAliceAliceBobCandiceDeniseDeniseDenise

Далее используем uniq.

$ git log --format='%an' | sort | uniq -c    4 Alice    1 Bob    1 Candice    3 Denise

Согласно man-странице uniq, она учитывает только соседние совпадающие строки. Вот почему нужно было сначала отсортировать список. Флаг -c выводит перед каждой строкой количество вхождений.

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

$ git log --format='%an' | sort | uniq -c | sort -nr    4 Alice    3 Denise    1 Candice    1 Bob

Флаг -r указывает на обратный порядок сортировки. Вот и всё. Теперь у нас есть список авторов, отсортированных по количеству коммитов.

Просмотр мемов из /r/memes и установка обоев из /r/earthporn


Вы знали, что можно добавить .json к любому URL на Reddit, чтобы получить выдачу в формате JSON вместо обычного HTML? Это открывает целый мир возможностей! Например, просмотр мемов прямо из командной строки (ну, не совсем, потому что фактическое изображение будет отображаться в графической программе). Мы можем просто запросить файл напрямую через curl или wget: https://reddit.com/r/memes.json.

$ wget -O - -q 'https://reddit.com/r/memes.json''{"kind": "Listing", "data": {"modhash": "xyloiccqgm649f320569f4efb427cdcbd89e68aeceeda8fe1a", "dist": 27, "children":[{"kind": "t3", "data": {"approved_at_utc": null, "subreddit": "memes","selftext": "More info available at....'......

Wget принимает опцию -O с указанием файла для записи. Большинство программ, которые принимают такую опцию, также допускают значение -, что означает стандартный вывод stdout (или ввод, в зависимости от контекста). Опция -q просто означает работу в тихом режиме, не отображая информацию типа статуса скачивания. Теперь у нас есть большая структура JSON. Чтобы проанализировать и осмысленно использовать эти данные в командной строке, можно взять утилиту jq, аналог sed/awk для JSON. У неё простой интуитивно понятный язык.

Ответ JSON выглядит примерно так:

{    "kind": "Listing",    "data": {        "modhash": "awe40m26lde06517c260e2071117e208f8c9b5b29e1da12bf7",        "dist": 27,        "children": [],        "after": "t3_gi892x",        "before": null    }}

Итак, здесь у нас тип Listing и массив children. Каждый элемент этого массива отдельный пост.

Вот как выглядит один из элементов массива:

{    "kind": "t3",    "data": {        "subreddit": "memes",        "selftext": "",        "created": 1589309289,        "author_fullname": "t2_4amm4a5w",        "gilded": 0,        "title": "Its hard to argue with his assessment",        "subreddit_name_prefixed": "r/memes",        "downs": 0,        "hide_score": false,        "name": "t3_gi8wkj",        "quarantine": false,        "permalink": "/r/memes/comments/gi8wkj/its_hard_to_argue_with_his_assessment/",        "url": "https://i.redd.it/6vi05eobdby41.jpg",        "upvote_ratio": 0.93,        "subreddit_type": "public",        "ups": 11367,        "total_awards_received": 0,        "score": 11367,        "author_premium": false,        "thumbnail": "https://b.thumbs.redditmedia.com/QZt8_SBJDdKLVnXK8P4Wr_02ALEhGoGFEeNhpsyIfvw.jpg",        "gildings": {},        "post_hint": "image",        ".................."        "много строк скипнуто"        ".................."    }}

Здесь много интересных атрибутов, в том числе URL картинки с мемом.

Мы можем легко получить список всех URL со всех постов:

$ wget -O - -q reddit.com/r/memes.json | jq '.data.children[] |.data.url'"https://www.reddit.com/r/memes/comments/g9w9bv/join_the_unofficial_redditmc_minecraft_server_at/""https://www.reddit.com/r/memes/comments/ggsomm/10_million_subscriber_event/""https://i.imgur.com/KpwIuSO.png""https://i.redd.it/ey1f7ksrtay41.jpg""https://i.redd.it/is3cckgbeby41.png""https://i.redd.it/4pfwbtqsaby41.jpg"......

Игнорируйте первые две ссылки, это в основном закреплённые посты от модераторов.

Утилита jq считывает данные из стандартного входа и получает JSON, который мы видели ранее. Затем указан массив постов .data.children. Синтаксис .data.children[] | .data.url означает: пройти по каждому элементу массива и вывести поле url, которое находится в поле data каждого элемента.

Таким образом, мы получаем список URL всех топовых мемов в подреддите /r/memes на данный момент. Если хотите посмотреть лучшие посты недели, нужно писать https://reddit.com/r/memes/top.json?t=week. Топ всех времён: t=all, за год: t=year и так далее.

Как только у нас есть список всех URL, можно просто передать его в xargs. Это действительно полезная утилита для построения командных строк из стандартного ввода. Из описания:

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

Пустые строки игнорируются.

То есть такая команда:

$ echo "https://i.redd.it/4pfwbtqsaby41.jpg" | xargs wget -O meme.jpg -q

будет равнозначна этой:

$ wget -O meme.jpg -q "https://i.redd.it/4pfwbtqsaby41.jpg"

Теперь можно просто передать список URL'ов в просмотрщик изображений feh или eog, который принимает URL в качестве допустимого аргумента.

$ wget -O - -q reddit.com/r/memes.json | jq '.data.children[] |.data.url' | xargs feh

И у нас запускается feh с мемами, которые можно листать клавишами со стрелками, словно это картинки на локальном диске.



Или можно загрузить все изображения с помощью wget, заменив feh на wget в строке выше.

А возможности безграничны. Ещё один вариант использования картинок с Reddit через JSON установка топовой картинки подреддита /r/earthporn в качестве обоев рабочего стола:

$ wget -O - -q reddit.com/r/earthporn.json | jq '.data.children[] |.data.url' | head -1 | xargs feh --bg-fill

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


Лес в Нидерландах [20001333]


Гренландия [40323024]

Конечно, вместо /r/earthporn можно брать картинки из других подреддитов.

Вот в чём мощь конвейеров Unix. Одна-единственная строка делает всё: скачивает файл JSON, разбирает его, находит нужные данные, потом скачивает картинку по URL и устанавливает её в качестве обоев.

Это всего несколько случайных примеров. В реальности можно очень многое сделать однострочной командой в консоли, построив конвейер из простых утилит.

Работа, которая доставляет удовольствие


Особая философия Unix настолько глубока и фундаментальна, что породила целые классы Unix-подобных систем, к числу которых относятся BSD, macOS и Linux. Все они построены на этой философии. А побочный эффект работы в такой системе чувство правильности и цельности. Что так всё и должно работать: из кирпичиков, маленьких строительных блоков, которые сцепливаются в конвейеры любой сложности. Это же гениально.

Интересные факты об отцах-основателях


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

Например, в 2014 году исследователь Лея Нойкирхер нашла в дампах исходного дерева BSD 3 файл /etc/passwd с паролями всех ветеранов, таких как Деннис Ричи, Кен Томпсон, Брайан В. Керниган, Стив Борн и Билл Джой.

Для хэшей использовался алгоритм crypt(3) с максимальной длиной пароля 8 символов. Лея взломала их стандартными брутерами john и hashcat. Большинство паролей были очень слабыми. Только пароль Кена Томпсона не поддавался взлому. Даже полный перебор всех строчных букв и цифр (несколько дней) не дал результата. Неужели он использовал специальные символы? Лишь в 2017 году Найджел Уильямс в списке рассылки The Unix Heritage Society раскрыл эту тайну. Пароль со всеми возможными символами оказался q2-q4!: это первый ход пешкой на две клетки в описательной нотации и начало многих типичных дебютов, что очень хорошо вписывается в бэкграунд Кена Томпсона по компьютерным шахматам. Брутфорс на видеокарте AMD Radeon Vega64 занял более четырёх дней.

Некоторые из разработчиков Unix живы и здравствуют. Если хотите посмотреть, какие операционные системы и программы они используют на своих домашних компьютерах вот галерея 2002 года и 2015 года. Каждого в электронном письме попросили сделать скриншот текущего десктопа сразу, как только они прочитают эти слова.


Десктоп Брайана Кернигана (октябрь 2015)

Деннис Ритчи (19412011) в 2002 году использовал необычную установку: его домашний клиент на NT4 подключён по ISDN к удалённому серверу Plan9 в центральном офисе Bell Labs.



Plan9 инновационная ОС, основанная на концепции распределённых вычислений. Любая машина может использовать любые ресурсы с любой другой машины как собственные. Сейчас эту концепцию пытаются реализовать в виде контейнеров, VM и микросервисов, а там она реализована на уровне ядра ОС.

Дальше у нас Брам Моленар, автор многофункционального текстового редактора Vim:


Десктоп Брама Моленара (сентябрь 2002)


Десктоп Брама Моленара (ноябрь 2015)

Джордан Хаббард, один из сооснователей проекта FreeBSD:


Десктоп Джордана Хаббарда (ноябрь 2015)

P. S. К сожалению, Линус Торвальдс не смог сделать скриншот из своей консоли в текстовом режиме.

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



На правах рекламы


Устанавливайте любые операционные системы на наших VDS с мгновенной активацией. Сервер готов к работе через минуту после оплаты!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru