Friday, April 26, 2019

История современных систем инициализации.

Оригинал:
https://blog.darknedgy.net/technology/2015/09/05/0/

"данный момент" - это 09.2015

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

описание Sys V и BSD-style взято из статьи Ричарда Гуча: https://blog.darknedgy.net/technology/2015/09/05/0

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

Когда речь заходит о GNU/Linux, наиболее распространено мнение, что хронологически первым был sysv init. Он был так себе, но по каким-то причинам с этим долго никто ничего не делал, пока Эппл не создали launchd, вдохновив Убунту на Upstart (или наоборот, в зависимости от того, кому вы симпатизируете), что только сделало его более тормозным. Затем в 2010 г. появилась systemd, и всё изменилось. А, да, тем временем Генту создала что-то под названием OpenRC или вроде того.

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

Цель данной статьи - внести ясность в хронологию попыток создать "современные" системы инициализации. Под современными здесь подразумеваются все попытки улучшить классические BSD и System V стили инициализационных скриптов и управления процессами.

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

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

Эта статья не касается основных супервизоров, которые не могут быть адаптированы к демонам init, таких как supervisord, monit, Circus, God, bluepill Eye. Они популярны у веб-разработчиков, потому что многие из них написаны на языках типа Питона и Руби, но они не являются системами инициализации, а также не являются оригинальными.

IBM System Resource Controller (1992)
Вероятно первая система init, написанная для AIX. В ней впервые настояли на общеизвестной семантике супервизора, когда не сервисы сами себя переводят в фоновое состояние, а этим занимается супервизор. Также в ней впервые действия производились не над отдельными демонами, а над "подсистемами", которые могли представлять из себя группы демонов и вспомогательных программ. Через 13 лет SMF в Solaris использовал этот же принцип.

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

SRC не основывалась на скриптах или даже традиционных конфигах системных служб, а имела соственную ООБД, в которой регистрировались подсистемы и сервисы с помощью специальных команд. SMF также повторил эту идею в своем хранилище конфигурационных данных сервисов, где лежат данные выполнения и постоянные конфигурации, хотя также остались видимые пользователю файлы конфигурации в формате XML.

DAEMONTOOLS (1997) + DERIVATIVES (1997-2015)
Уравнения Максвелла в управлении процессами Юникс.

Изначально созданные Даниелом Бернштейном в 1997, с тех пор они обрели большое влияние и используются по сей день. На их основе было создано несколько деривативов, а также они повлияли на системы, несколько отклонявшиеся от их модели, такие как minit, ninit and depinit. По сути все системы инициализации можно представить как расширения daemontools в той или иной форме
(например systemd => nosh).

Вот страница самого Бернштейна: http://cr.yp.to/daemontools.html.

▪ rc.d (2000)
Это модульная конструкция скриптов инициализации, основанная на зависимостях, первоначально принятая в  NetBSD примерно в 2000 г., разработанная Люком Мьюборном. Затем она проникла в другие BSD, заменив старую плоскую /etc/rc.

В rc.d демон init исполняет /etc/rc, который запускает программу rcorder для определения порядка запуска скриптов в /etc/rc.d/, основываясь на их зависимости друг от друга. Скрипты инициализации написаны в ясном, стандартном формате, основанном на файле общих подпрограмм  /etc/rc.subr. В свою очередь, поведение всей системы и запуск служб - какие запускать и какие из них отключены, названия конфигов и других модулей - конфигурируется в файле /etc/rc.conf - это шелл-скрипт, порождаемый /etc/rc и работающий с парами ключ-значение.

rc.d обладает большой гибкостью и возможностями тонкой настройки, при этом основные недостатки систем на основе System V в ней искоренены. Oднако она реализует лишь базовое управление сервисами. До перехода на systemd в 2012 Арч Линукс имел  rc.d-подобную систему инициализации.

SIMPLEINIT, JINIT AND THE NEED(8) (2001-3)
Simpleinit описана в статье Ричарда Гуча 2002 года https://blog.darknedgy.net/technology/2015/09/05/0/ как нечто среднее между SysV и BSD-стилем с некоторыми особенностями.

Вкратце, она была основана на каталоге в /sbin/init.d, содержащем короткие скрипты, запускающие службы. Порядок запуска синхронизировался с помощью двух небольших утилит: need(8) and provide(8), являющихся символическими ссылками на argv[0] of initctl(8).

Эти две утилиты должны были избавить систему от необходимости в ранлевелах и обеспечить управление зависимостями. provide(8) регистрирует службу на основе имени, а need(8) запускает и останавливает сервисы путем блокировки, если имя сервиса зарегистрировано.Была также утилита display-services(8) для отображения статуса, либо использовался стандартный файл SysV inittab(5). См. также мануал initctl(8).

Эта система добилась некоторых скромных успехов; её включили в несколько проектов встроенных систем и экспериментальных проектов. Также она служит первым примером инициализации, основанной на зависимостях. На данный (09.2015) момент она используется как минимум в одном живом дистрибутиве - Source Mage GNU/Linux.

simpleinit вдохновила Джона Фремлина написать производную систему на С++, которая называлась  jinit и необычным образом использовала очередь сообщений System V для межпроцессного взаимодействия. С 2003 г. она не развивается.

* BSD-style
Загрузка контролируется одним скриптом или небольшим набором. Обычно существует главный скрипт загрузки (как правило это  /etc/rc), организующий весь процесс загрузки. Эта схема проста для понимания, так как скриптов мало, а порядок их запуска определяется главным скриптом. Это быстрая, простая и эффективная система.
Её недостаток - проблема масштабируемости. Если какой-то сторонний пакет создает инициализационный скрипт, запускающийся во время загрузки, то ему нужно редактировать один из существующих скриптов. Это опасно, так как скрипты загрузки, мягко говоря, хрупкие. Oшибка инсталлятора может привести к тому, что система не будет загружаться.

* SysV-style
Это надежная и масштабируемая система, но слишком сложная и тяжеловесная. В основном каталоге (обычно /etc/rc.d/init.d/) находится ряд мини-скриптов, которые все вместе могут загрузить большую часть системы. Каждый из скриптов запускает и останавливает одну службу.
Управляет процессом загрузки главный скрипт, который производит действия по настройке, которые было сложно поместить в мини-скрипт, а затем начинает исполнять каждый из мини-скриптов в другом каталоге. Порядок исполнения основан на правилах раскрытия подстановочных символов оболочки командной строки.
В этом другом каталоге находятся симлинки на скрипты из /etc/rc.d/init.d/ - по две ссылки на каждый скрипт: одна имеет название, начинающееся на S, другая - на K. S - это start, эти скрипты вызываются при загрузке системы. K - kill, скрипты вызываются при остановке. Порядок зависит от цифр после букв S и К: S10 будет запущен раньше, чем S15, который будет запущен раньше, чем S20. Изначально имена ссылкам дает системный интегратор. Cторонние установщики пакетов могут просто поместить свой скрипт запуска в /etc/rc.d/init.d/, а затем создать ссылку на него в "другом каталоге". Инсталлятор должен выбрать имя, которое ещё не используется, а также определить номер, который зависит от того, насколько далеко от начала загрузочного процесса должен запускаться скрипт.
Поэтому автор системных скриптов должен оставлять пробелы между номерами, чтобы можно было добавлять числа. Обычно используются числа 10, 20, 30, 40 и т.д. К числу можно добавить произвольную строку, что значительно увеличивает количество вариантов нумерации. Обычно к числу добавляется имя  скрипта: 10inetd. Таким образом можно группировать скрипты: порядок запуска между группами будет строго пределен, а внутри группы неизвестен или неважен.
Схема загрузки SysV также поддерживает концепцию ранлевелов - уровней исполнения. Это означает, что можно загрузить систему "полностью" (с сетью, многопользовательским режимом и графикой), а можно только частично - например, в однопользовательском режиме. Ранлевелы реализованы группировкой скриптов в "другом каталоге" в ряд каталогов, каждый из которых соответствует уровню исполнения. Обычно эти каталоги называются /etc/rc.d/rc0.d/ /etc/rc.d/rc1.d/ /etc/rc.d/rc2.d/ /etc/rc.d/rc3.d/ /etc/rc.d/rc4.d/ /etc/rc.d/rc5.d/ /etc/rc.d/rc6.d/.
Главный скрипт запускает все скрипты в каталоге ранлевела, который задан в inittab, с помощью telinit или параметром загрузки. Например, можно загрузиться в однопользовательский режим, запустив скрипты из /etc/rc.d/rc1.d/. Затем, после обслуживания системы, можно загрузиться полностью - в этом случает сервисы ранлевела 1 останавливаются, а затем запускаются скрипты из /etc/rc.d/rc5.d/. Аналогично происходит переход от более высокого ранлевела к более низкому - путем остановки сервисов.
Проблема SysV init  в том, что эта хренова туча каталогов с ссылками усложняет отслеживание статуса служб: какие из них запущены и что происходит в целом.

minit (2001-2)
Разработана Fefe (Феликс фон Ляйтнер). Её можно описать как инвертированный (эндогенный) daemontools.

Там, где  daemontools порождает отдельный процесс-супервизор для каждой службы,  minit объединяет их в центральный супервизор msvc. Также minit имеет очень простую систему зависимостей: в каждом каталоге для сервиса есть файл "depends", служащий для определения порядка запуска сервисов. Службы могут запускаться как синхронно, так и асинхронно.

minit имеет минимальный размер и должен линковаться с dietlibc, созданной тем же автором. Принципы системы описаны подробно здесь: http://www.fefe.de/minit/minit-linux-kongress2004.pdf

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

▪DEPINIT (2002)
Написан Ричардом Лайтманом примерно в 2002. Он сам описал эту систему как "объединяющую идеи sysvinit, simpleinit, daemontools and make".

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

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

DAEMOND (2002-3)
Малоизвестная, но интересная с исторической точки зрения daemond имела относительно замысловатую для того времени систему разрешения зависимостей, включавшую специальные строфы для модулей ядра. У неё также был собственный синтаксис конфигов, основанный на блоках, которые могли опционально включать в себя фрагменты скриптов оболочки (аналогично заданиям Upstart)
 Например:

 service "fsck" {
    description "Check filesystems";
    require "lvm";
    setup "/sbin/fsck -C -R -A -a";
}

service "mount-local" {
    require "fsck";
    description "Mount local filesystems";
    setup "/sbin/mount -a -v -t nonfs,nosmbfs";
}

Имелся также исходный файл конфигурации /etc/daemond.rc, разработанный для быстрого параллельного запуска.

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

require "file-or-service";
    This states that the service cannot be started at all unless the
    file is present, or the service has been succesfully started.

  need "file-or-service";
        Same as require, except that if the dependency cannot be satisfied
    then the entire service is made unavaliable, as though it did not
    exist (so that services that depend on it will be able to proceed).
    This is useful when you want a service that must start when some
    condition is met, but which is optionnal otherwise.

  want "service";
    This is not a proper dependency, but a 'collaborating' service.  This
    directive states that if the service where it appears starts, then
    service must be attempted as well, but need not succeed.

  require module "module";
  need module "module";
        Same as the first two, but for kernel modules.  It is usually better
    to rely on kernel autoloading for the most part.

  group "group";
    This places the service in a group.  That group can then be refered
    to as if it was a service (starting all of the group) and will be
    deemed successful if all the members of the group are started,
    unless...

  require any "group";
    ...is used, in which case the group will be deemed succesful if /any/
    service in the group is started.

Система была написана на С++, а не на С, и у автора явно были связанные с системой амбиции: он хотел, чтобы она стала главной альтернативой "допотопным" стилям SysV и BSD. Однако она не произвела впечатления.

▪ GNU dmd (2003)
dmd (daemon managing daemons) - система, впервые выпущенная Вольфгангом Йерлингом, примечательная тем, что была полностью написана на языке  Guile Scheme и на нём же конфигурировалась.

Примерно 10 лет пребывала в коматозном состоянии, а в 2013 г. была возрождена как часть универсального пакетного менеджера Guix, также написанного на Гайле. На сегодняшний день используется для управления службами и демонами в дистрибутиве Guix.

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

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

(define (root-file-system-service)
  "Return a service whose sole purpose is to re-mount read-only the root file
system upon shutdown (aka. cleanly \"umounting\" root.)

This service must be the root of the service dependency graph so that its
'stop' action is invoked when dmd is the only process left."
  (with-monad %store-monad
    (return
     (service
      (documentation "Take care of the root file system.")
      (provision '(root-file-system))
      (start #~(const #t))
      (stop #~(lambda _
                ;; Return #f if successfully stopped.
                (sync)

                (call-with-blocked-asyncs
                 (lambda ()
                   (let ((null (%make-void-port "w")))
                     ;; Close 'dmd.log'.
                     (display "closing log\n")
                     ;; XXX: Ideally we'd use 'stop-logging', but that one
                     ;; doesn't actually close the port as of dmd 0.1.
                     (close-port (@@ (dmd comm) log-output-port))
                     (set! (@@ (dmd comm) log-output-port) null)

                     ;; Redirect the default output ports..
                     (set-current-output-port null)
                     (set-current-error-port null)

                     ;; Close /dev/console.
                     (for-each close-fdes '(0 1 2))

                     ;; At this point, there are no open files left, so the
                     ;; root file system can be re-mounted read-only.
                     (mount #f "/" #f
                            (logior MS_REMOUNT MS_RDONLY)
                            #:update-mtab? #f)

                     #f)))))
      (respawn? #f)))))


▪ PINIT (2003)
Это была малоизвестная, но на удивление значимая система инициализации, разработанная Воутером фон Клауненом где-то в 2003.
Возможно, это была первая система, где сервисы конфигурировались на XML, опередившая даже launchd и SMF. Это выглядело примерно так:

<?xml version="1.0"?>
<command provides="system.swap">
    <startup message="Activating all swap partitions...">
        /sbin/swapon -a
    </startup>
    <shutdown message="Deactivating all swap partitions...">
        /sbin/swapoff -a
    </shutdown>
    <dependency name="system.checkfs"/>
</command>

Как видно, она имела систему контроля зависимостей (основанную скорее на порядке выполнения, а не службу,  полностью отслеживающую транзакции, как в SMF или systemd). Она поддерживала параллельный запуск сервисов, а также, возможно, первая имела систему плагинов. То есть различные процедуры загрузки не прописывались в демоне init или скриптах, а подгружались и выгружались из адресного пространства pinit как динамические библиотеки, хотя формально API не существовало. От ранлевелов отказались в пользу статических профилей, содержавших список служб, которые надо было запусить/остановить для перехода в другое состояние, что напоминает настройки systemd.

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

▪ initng (2005)
Выпущенная Джимми Веннландом в марте 2005, система тестировалась в основном на Генту и была одной из самых амбициозных и завершенных систем новой школы. Помимо обычного управления процессами, группировки сервисов (группы назывались уровнями исполнения), контроля зависимостей и параллельного запуска служб, она прославилась своей обширной системой плагинов, которые могли эффективно взаимодействовать с более чем 20-ю подсистемами, закрытыми для initng. Демон init был преобразован в специальный загрузчик и обработчик модулей. Сами сервисы конфигурировались в формате блоков, называемых ifiles. initng предоставлял множество готовых файлов, например:

service service/aumix {
    use = service/alsasound;
    need = system/initial system/bootmisc;
    stdall = /dev/null;
    script start = {
        if [ -f /etc/aumixrc ]
        then
            @/usr/bin/aumix@ -f /etc/aumixrc -L
        else
            @/usr/bin/aumix@ -v75 -c75 -w75
        fi
    };
    exec stop = @/usr/bin/aumix@ -f /etc/aumixrc -S;
}

По сути это можно рассматривать как всеобъемлющий метаинит.

В какой-то момент на него смотрели разрабы Убунту, но в итоге решили создать Upstart. После этого initng тихо умер, почти ничего не достигнув.

launchd (2005)
Возможно, первая система "новой школы", характерной чертой которой был перенос основной логики в демон init и общение с ним с помощью управляющей утилиты. Конфигурировалась в формате XML, в OS X также был демон начальной загрузки (реестр обнаружения) для служб ядра. Продвигал s6 для активизации сокетов. Разделяет системных демонов и агентов - сервисы пользователя; последние группируются в типы, объединенные в специфичные для OS X подсистемы, такие как loginwindow или Aqua UI. Использует чистую загрузку сервисов, формально не имея системы контроля зависимостей, так как предполагается, что сервисы будут синхронизироваться друг с другом через  IPC в стеке  OS X. Связывает типы процессов с политиками планировщика и ограничениями ресурсов, очевидно, для сохранения отзывчивости рабочего стола.

Рассматривалось применение системы в Убунту, но было отклонено из-за лицензии Apple Public Source License, несовместимой с GPL, которая использовалась на тот момент. В данный момент исследуется в рамках NextBSD и предполагается для дальнейшего использования во  FreeBSD.

▪ Service Management Facility (SMF) (2005)
Solaris SMF - вероятно первая система со сложным отслеживанием транзакций, которое производилось во внутреннем графическом движке. Предусматривает сложные сценарии управления сервером. Глубоко интегрирована с Solaris Fault Manager для отслеживания сбоев аппаратного обеспечения. Каждый сервис идентифицируется через FMRI (Fault Management Resource Identifier). Конфиги служб в формате XML затем объединяются в базу данных, называемую репозиторием конфигурации сервисов (в ней сервисы также могут хранить данные времени выполнения). Чтение из базы происходит с помощью svcprop, а её динамическая конфигурация - с помощью svccfg. Сами экземпляры служб контролируются с помощью svcadm, информация о статусе служб извлекается с помощью svcs.

SMF имеет главную службу контроля сервисов - svc.startd, управляющий зависимостями по умолчанию, - а также делегированные службы для каждого приложения. В Солярисе делегированной службой является  inetd. Подробней - на сайте документации Oracle:
https://docs.oracle.com/cd/E23824_01/html/821-1451/hbrunlevels-25516.html.

▪ eINIT (2006)
Аналогично initng, система основывалась на плагинах, сам init был только обработчиком. Также разрабатывалась для Генту. Конфигурировалась на XML. Имел значительно более высокую мета-конфигурируемость, чем  initng. Каждый модуль настраивался с помощью XML в манифесте einit.xml. Основан на типах зависимостей provides/requires и подсистеме событий, но больше для внутренних событий типа мониторинга загрузки/выгрузки модулей, а не для регистрации сервисов.

Пример:

<einit prefix="services-virtual-module">
 <daemon id="daemon-boinc"
  name="BOINC client"
  provides="boinc"
  requires="mount-critical"
  command="cd /var/lib/boinc; boinc_client"
  restart="yes" />
</einit>

Не смог набрать обороты и был заброшен.

Upstart (2006)
Создан Скоттом Джеймсом Ремнантом для Убунту. В свое время недолго использовался в Федоре, на данный момент - в ChromeOS. Убунту переехала на systemd.

Upstart основана на идее, что действия (например, старт и остановка сервисов) должны происходить в ответ на генерируемые системой или пользовательскими программами события. Она имеет несколько модулей, называемых мостами, для трансляции событий ядра и пространства пользователся в собственную очередь Upstart. Список встроенных событий определён в  upstart-events(7).

В Upstart впервые в качестве механизма коммуникации внутри  PID 1  использован D-Bus.

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

http://upstart.ubuntu.com/cookbook/ - пособие по апстарт.


Asus eeePC fastinit + его производные (2007-2015)
Как часть линейки ноутбуков Asus’s eeePC около 2007-2008, которая имела варианты с предустановленной системой Xandros, они написали проприетарную замену init под названием  fastinit, специально созданную для...быстрой загрузки, я полагаю

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

Однако в 2014-2015 появилось её ответвление  finit, значительно дополненное разработчиком встроенных систем Хоакимом Нильсоном: https://github.com/troglobit/finit.

Во многом похоже на упомянутый  pinit, finit работает с системой плагинов, динамически включающихся в логику загрузки, но у finit есть API. Она совместима с уровнями исполнения SysV и имеет встроенный  inetd для предварительного открытия серверных сокетов. Конфиг - простой файл /etc/finit.conf, вроде этого:

user admin
host testbed

check /dev/vda1

module button
module evdev
module loop
module psmouse

runlevel 2

network service networking start

tty /dev/tty1
tty /dev/tty2
tty /dev/tty3

# Alternative method instead of runparts
#task [S] /etc/init.d/keyboard-setup start -- Setting up preliminary keymap
#task [S] /etc/init.d/acpid start          -- Starting ACPI Daemon
#task [S] /etc/init.d/kbd start            -- Preparing console
#run [2] /etc/init.d/networking start      -- Start networking

# Services to be monitored and respawned as needed
service [2345] /sbin/klogd -n             -- Kernel logging server
service [2345] /sbin/syslogd -n           -- Syslog server
service [3] /usr/sbin/gdm                 -- GNOME Display Manager

# Run start scripts from this directory
# runparts /etc/start.d

# Inetd services
inetd time/udp                wait [2345] internal          -- UNIX rdate service
inetd time/tcp              nowait [2345] internal          -- UNIX rdate service
inetd ssh@eth0:222/tcp      nowait [2345] /usr/sbin/sshd -i -- SSH service
inetd ssh/tcp               nowait [2345] /usr/sbin/sshd -i -- SSH service

# For multiple instances of the same service, add :ID somewhere between
# the service/run/task keyword and the command.
service :1 [2345] /sbin/httpd -f -h /http -p 80   -- Web server
service :2 [2345] /sbin/httpd -f -h /http -p 8080 -- Old web server

finit ориентирована на маленькие и встроенные системы, но при этом очень гибкая.

OpenRC (2007)
Используется прежде всего в Генту, но также в Alpine Linux и др. Предназначена для замены ранних скриптов базовой схемы Генту. На самом деле в OpenRC нет демона init, но есть всеобъемлющая структура управления процессами (хотя и недостаточная в смысле супервизора, так как предполагается её интеграция с внешними супервизорами, такими, как s6 daemontools-подобный супервизор), созданная под сильным влиянием rc.d из систем  BSD, отсюда и её название. Обзор можно найти в Генту-вики.

▪ Android init (2008)
Это специализированный демон инит, созданный для работы с данной платформой и обеспечивающий обычную систему ленивой загрузки, основанную на событийно-ориентированном механизма, называемом actions. Некоторые из них определяются пользователем, другие - самим демоном. Файл конфигурации - /init.rc.

Детальный обзор системы - в этой статье: https://blog.darknedgy.net/technology/2015/08/05/0-androidinit/. Система не отличается оригинальностью. Её назначение - сделать так, чтобы вендоры могли один раз настроить её в initramfs и забыть.

 systemd (2010)
Изначально планировалось название Babykit. Интервью с поттерингом: www.linuxvoice.com/interview-lennart-poettering/.
(Видать автор тоже не большой любитель systemd :D. Хотя начиналось всё неплохо, но поттеринг забыл предупредить, что собрался заменить своим поделием ядро).

procd (2012)
Демон init маленького размера с функцией супервизора, разработанный специально для OpenWrt и используемый, соответственно, на роутерах. Он использует небольшую объектно-ориентированную реализацию шины сообщений, названную ubus, и поддерживает песочницу для сервисов в пространствах имен  и через фильтрацию системных вызовов, например, seccomp-bpf.

Для скриптов инициализации используется специальная библиотека:

START=50
USE_PROCD=1

start_service() {
   procd_open_instance
   procd_set_param command /usr/bin/xupnpd
   procd_append_param command -d /usr/share/xupnpd

   procd_set_param respawn
   procd_close_instance
}
Where the procd_ routines serialize the arguments into JSON and pass them over ubus.

Epoch (2014)
Epoch - это намеренно минималистичный демон init, осуществляющий полное управление процессами во время запуска, остановки и работы системы, но выполняющий все процессы последовательно/синхронно. Для сервисов использует метафору Object и так же, как Android init, настраивается в основном файле. Пример:


Hostname=FILE /etc/hostname
DefaultRunlevel=boot
EnableLogging=true
DisableCAD=true
BlankLogOnBoot=true
MountVirtual=procfs sysfs devpts+ devshm+

ObjectID=sysclock
    ObjectDescription=Configuring system clock
    ObjectStartCommand=hwclock -s
    ObjectStopCommand=hwclock -w
    ObjectStartPriority=1
    ObjectStopPriority=2
    ObjectEnabled=true
    ObjectOptions=RAWDESCRIPTION
    ObjectRunlevels=boot core

ObjectID=mountruntmp
    ObjectDescription=Mounting /run and /tmp
    ObjectStartCommand=/etc/epoch/scripts/mountruntmp.sh
    ObjectStopCommand=NONE
    ObjectStartPriority=2
    ObjectStopPriority=0
    ObjectEnabled=true
    ObjectOptions=RAWDESCRIPTION
    ObjectRunlevels=boot core hurr

ObjectID=rwfs
    ObjectDescription=root filesystem read-write support
    ObjectStartCommand=/bin/mount -o remount,rw /
    ObjectStopCommand=/bin/mount -o remount,ro /
    ObjectStartPriority=4
    ObjectStopPriority=6
    ObjectEnabled=true

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


sinit (2014)
Über alles.

# MIT license.

#include <sys/types.h>
#include <sys/wait.h>

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

#define LEN(x) (sizeof (x) / sizeof *(x))

static void sigpoweroff(void);
static void sigreap(void);
static void sigreboot(void);
static void spawn(char *const []);

static struct {
    int sig;
    void (*handler)(void);
} sigmap[] = {
    { SIGUSR1, sigpoweroff },
    { SIGCHLD, sigreap     },
    { SIGINT,  sigreboot   },
};

static char *const rcinitcmd[]     = { "/bin/rc.init", NULL };
static char *const rcrebootcmd[]   = { "/bin/rc.shutdown", "reboot", NULL };
static char *const rcpoweroffcmd[] = { "/bin/rc.shutdown", "poweroff", NULL };

static sigset_t set;

int
main(void)
{
    int sig;
    size_t i;

    if (getpid() != 1)
        return 1;
    chdir("/");
    sigfillset(&set);
    sigprocmask(SIG_BLOCK, &set, NULL);
    spawn(rcinitcmd);
    while (1) {
        sigwait(&set, &sig);
        for (i = 0; i < LEN(sigmap); i++) {
            if (sigmap[i].sig == sig) {
                sigmap[i].handler();
                break;
            }
        }
    }
    /* not reachable */
    return 0;
}

static void
sigpoweroff(void)
{
    spawn(rcpoweroffcmd);
}

static void
sigreap(void)
{
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

static void
sigreboot(void)
{
    spawn(rcrebootcmd);
}

static void
spawn(char *const argv[])
{
    pid_t pid;

    pid = fork();
    if (pid < 0) {
        perror("fork");
    } else if (pid == 0) {
        sigprocmask(SIG_UNBLOCK, &set, NULL);
        setsid();
        execvp(argv[0], argv);
        perror("execvp");
        _exit(1);
    }
}


























Friday, April 12, 2019

Linux Swap

https://www.linux.com/news/all-about-linux-swap-space - оригинал

Свопинг - это процесс выгрузки страниц из памяти в заранее сконфигурированное место на жёстком диске. Виртуальная память - это суммарный объём физической памяти и свопа.

Своп нужен по 2-м причинам:

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

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

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

В Линуксе бывает 2 вида пространства для свопинга: раздел и файл.

Команда swapon -s покажет тип свопа, используется ли он (сколько килобайт) и приоритет - какой своп использовать в первую очередь. В Линуксе, если у вас смонтировано 2 или более свопа с одним приоритетом, система будет использовать их по очереди, что заметно увеличит производительность. От себя добавлю: если не хотите ебаться с килобайтами -

free -h - там будут меби- и гибибайты.
free -h --si степени десятки, т.е. мегабайты и гигабайты.

I CВОП-РАЗДЕЛ


Проверка, имеет ли ваш раздел для свопа тип Linux Swap:

fdisk -l от рута.

В поле Type для своп-раздела должно быть  Linux Swap, ID - 82.

Если надо изменить тип раздела, можно использовать fdisk с параметром t, но проще будет cfdisk с псевдографическим интерфейсом.

После того, как разделу присвоен тип своп, его надо подготовить: это типа как на нормальном разделе создать файловую систему.

mkswap /dev/sdx

x - номер раздела.

Теперь раздел готов к использованию, но его надо активировать:

swapon /dev/sdx

swapon -s - проверка, что он подключен и работает.

Чтобы раздел монтировался автоматически при загрузке, надо его занести в fstab, добавив туда строку:

/dev/sdx swap swap defaults 0 0

Параметры файловых систем, как правило, неприменимы к ФС типа своп. Точки монтирования нет.

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

1. swapoff -a  - отключает все свопы, присутствующие в fstab
2. swapon -as - подключить всё, что есть в fstab, и вывести резюме.


II СВОП-ФАЙЛ

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


Проверка, есть ли в системе вообще своп:

swapon -as

Создание файла для свопа:

dd if=/dev/zero of=/swapfile bs=1024 count=1048576

swapfile - имя файла.

Подготовка файла:

mkswap /swapfile

chmod 0600 /swapfile - присвоение прав доступа к файлу.

Подмонтирование:

swapon /swapfile

Строка в fstab аналогична таковой для раздела.

III РАЗМЕР СВОП-ПРОСТРАНСТВА.

Старые Юниксы типа Sun OS требовали размер свопа в 2-3 раза больше размера физической памяти. Современные реализации типа линуксов не требуют таких конских цифр, но если сконфигурировать таким образом, то они будут его использовать.

Правило "большого пальца" примерно такое:

1. Для десктопных систем своп делается примерно в 2 раза больше объема ОЗУ, чтобы можно было выкидывать туда простаивающие процессы.

2. Сервер - примерно равно объему ОЗУ.

3. Старые машины (где, например, 128 метров ОП): чем больше, тем лучше, хоть гигабайтами.


 ПОЧЕМУ ИСПОЛЬЗУЕТСЯ СВОП при том, что ОЗУ заполнено наполовину:

В линуксах за это отвечает параметр ядра swappiness. Это число от 1 до 100, по умолчанию 60. Обозначает агрессивность свопинга: чем оно больше, тем больше страниц свопится из памяти. Вот например, разработчик ядра Эндрю Мортон выставляет 100, ибо нехуй всяким раздутым приложениям засирать мою оперативную память. Недостаток такого подхода в том, что если слишком часто выгружать страницы, это снижает время отклика приложений. Потому что когда вам понадобится ткнуть в это приложение, оно будет дооолго загружаться с диска. Я вам скажу, что даже при значении 60 случается такая хуета, что файловый менеджер может открываться 3-4 секунды.

Как изменить значение параметра:

echo X > /proc/sys/vm/swappiness - под рутом.

Или
sysctl -w vm.swappiness=X.

1<=X<=100. Если надо делать это автоматически при загрузке (так сработает до перезагрузки) - добавить в /etc/sysctl.conf или какой-нибудь /etc/sysctl.d/90-swap.conf строчку:

vm.swappiness=X.

Заключение:

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

Перевод с Booleanworld: команда top

Перевод с Booleanworld: команда top.

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

Чтобы запустить интерактивный интерфейс, надо набрать в терминале top, для выхода - q.

Здесь рассматривается версия утилиты, которая поставляется с пакетом procps-ng. Чтобы выяснить, какая у вас, наберите
top -v

ИНТЕРФЕЙС КОМАНДЫ: БЛОК СТАТИСТИКИ.

Вывод команды поделен на 2 части: сверху - статистика использования системы (конкретно - cpu и ОЗУ, винты, например, он не видит); снизу - сами процессы.

Слева вверху показано системное время, за ним - uptime(up) - время непрерывной работы.

x users - это количество активных сессий. Сессии могут быть как на tty, так и на pty; как локальные, так и удаленные. Если, допустим, вы вошли в DE и запустили эмулятор терминала, то у вас будет 2 активные сессии. Для более подробной информации о сессиях есть команда who.

ИСПОЛЬЗОВАНИЕ ПАМЯТИ.

total - общее количество, free - свободная, used - использованная; avail mem - память, к-рая может быть выделена для процессов без дополнительного свопинга;

buff/cache:
disk cache - кэш дисков. Это механизм, используемый ядром Линукс для снижения обращений к диску, т.к. он медленный. Кэш диска находится в RAM, туда сохраняются наиболее часто используемые данные из областей диска. Кроме того, при записи данные сначала пишутся в буфер диска, а потом периодически всем скопом уже на диск. Buff/cache - это память, занятая буфером и кэшем в сумме. Оно считается за память, т.к. память, используемая кэшем, при необходимости будет выделена для процессов.

Tasks: статистика процессов, запущенных в системе. Total - общее количество. Для понимания остальных полей - немного информации о том, как ядро Линукс обрабатывает процессы.

Процессы могут выполнять задачи ввода/вывода информации (напр., чтение с диска), но это могут быть и вычислительные операции процессора. Во время операций I/O процессор простаивает, поэтому ОС в это время переключается на другие задачи. Кроме того, для каждого процесса ОС выделяет определенное малое кол-во времени (квант), после чего переключается на другой процесс. Так реализуется "многозадачность" (т.е. её иллюзия. Тут же отмечу, что величина кванта времени для каждой задачи определяется её приоритетом, о которых пойдёт речь позже). Всё это требует отслеживания состояния процесса. В линукс процесс может быть в одном из следующих состояний:

R - runnable: выполняющийся или стоящий в очереди на выполнение (т.е. в состоянии готовности)
S - interruptable sleep: ждёт завершения события;
D - uninterruptable sleep: ждёт завершения операции ввода/вывода;
T - stopped: остановлен с помощью сигнала управления, например, через C+z;
t - stopped: остановлен отладчиком при трассировке;
Z - zombie: для наблюдения за процессами ядро поддерживает различные структуры данных в памяти. Бывает, что процесс порождает несколько потомков, а они завершаются, хотя родитель ещё живой. При этом структуры данных потомков должны сохраняться в памяти, чтобы родитель получил информацию об их статусе. Зомби - это такие законченные процессы, чьи структуры данных ещё не выгружены из памяти.

Теперь к обозначениям top: процессы S & D обозначены sleeping, t & T - stopped, зомби и запущенные так и обозначены.

ИСПОЛЬЗОВАНИЕ ЦПУ.

В секции отражается процент процессорного времени, затраченного на различные типы задач.
us - задачи из пространства пользователя (user space);
sy - процессы ядра (system);
ni - nice, определяет приоритет процесса. Это, типа, уровень "любезности": процесс с более высоким nice более любезен к другим процессам и сам получает более низкий приоритет. Т.е., чем выше nice, чем ниже приоритет. Это значение можно поменять вручную (renice). Значение ni - это время, затраченное на процессы с измененным вручную приоритетом.
id(idle) - время простоя процессора;
wa(waiting) - время, пока ЦПУ ждёт окончания операций I/O.
hi & si (hardware interrupts, software interrupts) - это время, затраченное на обработку аппаратных и программных прерываний. Прерывание - это сигнал процессору о наступлении какого-любо события, которое требует немедленной реакции. Аппаратные прерывания (IRQ) обычно используются периферийными устройствами, чтобы сообщить, например, о нажатии клавиши на клавиатуре. Программные прерывания генерируются специфическими инструкциями процессора.
st - stolen time, украденное время. Это время, украденное виртуальной машиной. При запуске VM ей выделяется часть ресурсов, в т.ч. процессорного времени. Иногда получается, что ОС должна выполнить какую-то задачу, но не может, потому что процессор занят виртуальной машиной. st - время таких простоев.

СРЕДНЯЯ НАГРУЗКА (load average)

Секция представляет среднюю нагрузку за 1, 5 и 15 минут. Под нагрузкой подразумевается количество вычислений, произведенных процессором, т.е., в линуксе это количество процессов в статусе R или D в данный момент. Значение load average даёт относительное представление о том, как долго происходят вычисления.

Как это рассчитывается: числа делятся на количество ядер или процессоров. Например, если в системе 1 ядро, то значение 0,4 означает, что система нагружена на 40%. Значение 1 - это под завязку, 100%. 2,12 означает, что ЦПУ перегружен на 112%. Если ядер 2, то те же 0,4 надо поделить на 2 - будет 0,2, т.е. 20%.

СЕКЦИЯ ПРОЦЕССОВ.

Здесь отражены различные параметры для каждого процесса. Значения полей:

PID - process id, уникальное положительное число, идентифицирующее процесс.
USER - эффективное имя пользователя, отображающееся в эффективный UID. Эффективный - это который используется при проверке прав доступа и позволяет процессам действовать от имени пользователя, отличного от того, кто их запустил.
PR & NI: NI - значение nice для процесса, PR - приоритет с точки зрения планировщика ядра. Значение NI влияет на приоритет.
VIRT, RES, SHR and %MEM - поля, отражающие потребление памяти процессом. VIRT - это общее количество памяти, потребляемое процессом, включая код программы и данные, хранящиеся в памяти, а также страницы, выгруженные на диск в своп.  RES - потребляемая ОП. %MEM - процент от всего объема ОЗУ, занятый процессом.  SHR - количество памяти, разделяемое с другими процессами.
S(R/D/Z/T/t) - статус процесса.
TIME+ - время, потраченное ЦПУ на выполнения процесса со времени его запуска, с точностью  до сотой доли секунды.
COMMAND - имя процесса.

ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ КОМАНД top.

Интерфейс команды позволяет управлять процессами, а также различными аспектами вывода. В большинстве случаев нужно нажать клавишу во премя выполнения команды. Регистр учитывается. Клавиши:
k - kill: передать процессу сигнал. Принимает pid процесса и номер сигнала (по умолчанию 15,sigterm). Если процесс нужно завершить принудительно, то номер сигнала будет 9 - sigkill. Если не указать номер процесса, сигнал получит верхний процесс в списке.

▶  Сортировка процессов: чаще всего top применяется, чтобы выяснить, какой процесс потребляет больше ресурсов.
M - сортировать по использованию памяти.
P - по использованию ЦПУ.
N - по PID.
T - по времени выполнения.
По умолчанию порядок будет нисходящий. Чтобы изменить его на восходящий, нужно нажать R (reverse).
Можно сразу запустить команду с определенной сортировкой. За это отвечает параметр -о.
Например: top -o %CPU
Таким образом можно сортировать по любому атрибуту из секции статистики.

▶ Отображение потоков вместо процессов.
Процессы не могут попеременно использовать память или другие ресурсы, т.к. переключение между ними происходит медленно. Поэтому Линукс, как и многие другие ОС, использует потоки. Это части процессов, которые по очереди  используют ресурсы, хотя могут выполняться одновременно, как процессы. По умолчанию top  показывает процессы. Для отображения потоков - H. В секции статистики вместо Tasks будет Threads. При этом не изменяется ни один атрибут процессов, т.к. ядро обрабатывает процессы и потоки с помощью одних и тех же структур данных. Таким образом, у каждого потока будут, как и процесса, свои ID, статус и т.д. Чтобы снова переключиться на процессы, нужно снова нажать Н. Буква доступна как параметр при запуске программы: top -H.

▶ Отображение полных путей к программам и выделение системных программ.  Если это нужно - нажмите с. Названия системныx процессов будут отображаться в квадратных скобках. Также можно использовать как ключ: top -c

▶ Отображение в виде дерева - показывает иерархию родитель-потомок. Буква v.

▶ Список процессов, принадлежащих определенному пользователю - u. Принимает имя пользователя. Пустое имя - все пользователи. Или top -u <username>.

▶ Фильтры. Примеры: COMMAND=getty - ищет процесс, у которого в поле команды стоит getty. Если перед COMMAND поставить !:
!COMMAND=getty, то в поле команды не должно быть getty, чтобы имя процесса отобразилось.
%CPU>3.0 - процессы с использованием ЦПУ больше 3%. Чтобы очистить фильтр, нажмите =.

▶ Изменение вида статистики по ЦПУ и памяти. Клавиши t - cpu, m - memory. Появится ...эээ...короче, прогресс-бар. Если нажать несколько раз, то на второй изменится вид бара, потом соответствующее поле пропадёт, на четвертый - снова появится.

▶ Сохранение настроек. Если нажать W после изменения вывода команды, настройки сохранятся - он сам напишет, куда. Файл будет назваться toprc.

ЗАКЛЮЧЕНИЕ.

Мы лишь коснулись поверхности в этой статье, есть ещё много возможностей, которые не были освещены. Например, к выводу можно добавлять дополнительные поля. Чтобы узнать - RTFM.

Monday, April 1, 2019

Редактирование файлов с параметром in-place

Оригинал


РЕДАКТИРОВАНИЕ ФАЙЛОВ С ПАРАМЕТРОМ IN-PLACE.

Задавая вопросы типа "Как редактировать файлы in-place в sed/awk/perl и т.п.?" или " Я знаю, что ' >> ' добавляет текст в конец файла. Как можно добавить текст в начало файла?", люди обычно имеют в виду "Как изменить содержимое файла, не создавая временный файл?" (по какой-то неизвестной причине).

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

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

IN-PLACE?

Строго говоря, in-place на самом деле должно обозначать редактирование того же самого файла (с тем же инодом). В принципе, это возможно, но:

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

● это опасно: если во время редактирования что-то пойдет не так
(падение системы, переполнение диска и т.д.), файл останется в
недоделанном состоянии.

Ни один обычный редактор этого не делает. Даже если создается видимость
редактирования данного файла, на самом деле за кулисами создаётся
временный файл. Давайте посмотрим, что делают sed и perl, если задаётся
ключ -i.

SED

Во-первых, не все реализации редактора поддерживают эту опцию (GNU-sed поддерживает, POSIX-версия - нет). Это нестандартное расширение, поэтому оно не всегда доступно. В документации написано, что GNU-sed при задании ключа i создает временный файл (с именем исходного + расширение, к-рое надо задать) и направляет вывод в него. По завершении редактирования этот временный файл получает имя исходного ( а к исходному добавляется расширение, таким образом создается бэкап). Это можно проверить с помощью strace. Даже без strace, если посмотреть номера инодов с помощью ls -i, то можно увидеть, что иноды исходного файла и файла, модифицированного с помощью in-place, отличаются (если был задан суффикс, ессно. Если суффикс не задавать, изменяется сам исходный файл).

Используя ключ -i в  sed, убедитесь, что вы задали расширение для файла бэкапа. Только убедившись, что всё отредактировано корректно, можно удалять резервный файл. Sed в BSD и Mac OS X не принимает параметр без суффикса, но это можно обойти, задав пустой суффикс "".

PERL

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

ОЧЕРЕДНОЙ ЛОЖНЫЙ IN-PLACE.

Кстати, вот пример, который обычно приводится в качестве редактирования
in-place:

$ { rm file; command > file; } < file

command - это обычная команда редактирования файла, обычно фильтр или
потоковый редактор.
Пример: { rm file; sed '1 s/YYY/XXX/'>file; }<file

Это работает, потому что это обман. На самом деле там присутствуют 2 файла: файл, из которого берутся данные, не удаляется, так как он открыт в силу перенаправления ввода (<). Внутреннее перенаправление (>) на самом деле пишет в другой файл, хотя операционная система позволяет использовать то же имя, потому что оно больше "официально" не
принадлежит исходному файлу. По завершении процесса исходный файл (существовавший на протяжении действия анонимно, подавая данные для команды), удаляется с диска. Итак, в этом случае так же используется дополнительное пространство на диске, как если бы создавался временный файл (примерно размером с исходный файл). Это похоже на то, что делает
perl, если не задан суффикс, включая хранение исходного файла рискованном "открытом, но удалённом" состоянии. Так что если вы хотите использовать этот способ, то как минимум нужно сделать бэкап:

$ { mv file file.bak; command > file; } < file

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

ЯВНОЕ ИСПОЛЬЗОВАНИЕ ВРЕМЕННОГО ФАЙЛА.

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

Обычный метод редактирования файла выглядит примерно так (command - это команда редактирования):

$ command file > tempfile && mv tempfile file
#или, в зависимости от того, как команда считывает входные данные
$ command < file > tempfile && mv tempfile file

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

$ command file > tempfile && cat tempfile > file && rm tempfile
# or
$ cp file tempfile && command tempfile > file && rm tempfile

Эти команды несколько менее эффективны, чем предыдущие, так как происходит 2 перехода между файлами (cat в первом случае и сp во втором). В большинстве случаев обычный способ подходит, и нужды в подобных методах нет. Если вам нужны мелкие подробности этих операций, эта статья на pixelbeat http://www.pixelbeat.org/docs/unix_file_replacement.html перечисляет множество вариантов, как заменить файл, используя временный файл с сохранением или без сохранения метаданных, описывая плюсы и минусы каждого метода.

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

СПОНЖ И ДРУГИЕ ФОКУСЫ.

Существуют альтернативы явному созданию временных файлов, хотя, по мнению автора, они уступают ему. С другой стороны, у них есть преимущество - они (обычно) сохраняют инод и метаданные.
Одна из таких утилит - sponge из пакета moreutils. Её использование очень просто:

command file | sponge file

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

Итак, спонж собирает вывод команды (в памяти или, когда он становится слишком объемным, угадайте где? Во временном файле) и не открывает файл снова до тех пор, пока не получит сигнал об окончании чтения входных данных. Когда входящий поток заканчивается, утилита открывает файл на запись и пишет туда новые данные (если пришлось использовать временный файл, он просто переименовывается вызовом rename(), что более эффективно, хотя в результате меняется инод файла).

Базовая реализация программы, подобной спонжу, на perl выглядела бы примерно так:


#!/usr/bin/perl
# sponge.pl
while(<STDIN>) {
  push @a, $_;
}
# EOF here
open(OUT, ">", $ARGV[0]) or die "Error opening $ARGV[0]: $!";
print OUT @a;
close(OUT);

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

Теперь можно сделать так:

command file | sponge.pl file

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

# sponge.awk
BEGIN {
  outfile = ARGV[1]
  ARGC--
}
{ a[NR] = $0 }
END {
  for(i=1;i<=NR;i++)
    print a[i] > outfile
}

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

СТАРЫЙ ДОБРЫЙ ED.

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

printf '%s\n' ',s/^/XXX/' w q | ed -s file

(ключ -s подавляет вывод информации о том, сколько байт считано/записано, это не влияет на работу ed).

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

"Утилита ed должна работать с копией исходного файла. Изменения, сделанные в копии, не влияют на файл до тех пор, пока не дана команда w - write. Копия текста называется буфером"

Итак, теперь понятно, что sed имеет те же недостатки, что и методы спонж. В частности, когда он получает команду записи, он обрезает содержимое исходного файла и пишет в него содержимое буфера. Если данных очень много, то получается довольно длительное окно, во время которого файл находится в нецелостном состоянии, пока ed не запишет все данные (а другой копии исходных данных нет). Учтите это, если вы подозреваете, что процесс может неожиданно прерваться на середине.

"НО Я НЕ ХОЧУ ИСПОЛЬЗОВАТЬ ВРЕМЕННЫЙ ФАЙЛ!"

Ну ладно. После всего, что мы сказали, по каким-то загадочным причинам люди всё ещё пытаются избежать использования временного файла и изобретают "творческие" решения. Вот некоторые из них. ОНИ ВСЕ НЕПРАВИЛЬНЫЕ, И НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ НЕ ДОЛЖНЫ ИСПОЛЬЗОВАТЬСЯ. Дети, не повторяйте это дома!

КОНВЕЙЕР СМЕРТИ.

Иногда пытаются сделать так:

$ cat file | command > file     # не работает

или так:

$ command file | cat > file     # doesn't work

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

command file | { sleep 10; cat > file; } # НЕ ДЕЛАЙТЕ ТАК

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

Смысл здесь такой: "давайте подождём 10 сек., чтобы команда могла прочитать весь файл и выполнить операции до того, как он будет обрезан, и новые данные можно будет записать туда через конвейер." Давайте проигнорируем факт, что 10 секунд может хватить, а может и нет (как и в случае с любым заданным значением). Здесь есть гораздо более серьезная, фундаментальная проблема. Давайте посмотрим, что будет, если файл даже умеренно большого размера. Правая сторона конвейера не получает никаких данных из него в течение 10 (или скольки угодно) секунд. Это значит, что всё, что выдаёт команда, переходит в конвейер и остаётся в нём, по крайней мере до тех пор, пока не закончит выполняться sleep. Разумеется, конвейер не может хранить бесконечное количество данных, так как его размер обычно довольно ограничен (примерно несколько десятков килобайт, хотя это зависит от реализации). Что же происходит, если вывод команды полностью заполняет конвейер до окончания задержки? В какой-то момент времени вывод write(), производимый командой, блокируется. Для большинства команд это означает, что блокируется и сама команда. В частности, она больше ничего не будет читать из файла. Итак, вполне вероятно, особенно если файл с входными данными достаточно большой, и, соответственно, порождает много выходных данных, что команда будет заблокирована, не дочитав исходный файл до конца, и останется в таком состоянии до окончания задержки.

Когда, наконец, sleep заканчивается, возможны как минимум 2 варианта событий, в зависимости от того, как именно программа считывает входные и пишет выходные данные, системного механизма буферов stdio, планировщика задач, оболочки командной строки и, возможно, других факторов (подробнее о буферах stdio позже).

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

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

$ cat file | { sleep 10; command > file; }    # DO NOT DO THIS

И снова - cat будет заблокирована, если файл большой, и конвейер заполнен до того, как пройдут 10 секунд.

Вероятно, лучше сформулировать это более точно: код, упомянутый, выше, может привести к краху вашей системы. НЕ ИСПОЛЬЗУЙТЕ ЕГО ни при каких обстоятельствах. Если вы не верите и хотите убедиться в этом сами, попробуйте сделать это в файловой системе, которую можно заполнить до конца (рекомендуется зацикленная - loopback).

# dd if=/dev/zero of=loop.img bs=1M count=100 - создание файла 100Мб.
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 2.58083 s, 40.6 MB/s
# mke2fs loop.img  - создание файловой системы на loop- разделе.
mke2fs 1.41.9 (22-Aug-2009)
loop.img is not a block special device.
Proceed anyway? (y,n) y
...
# mount -o loop loop.img /mnt/temp - монтирование устройства.
# cd /mnt/temp

Создание относительно большого файла:
# seq 1 200000 > file

Приступим:

# sed 's/^/XXX/' file | { sleep 10; cat > file; }    # DO NOT DO THIS
cat: write error: No space left on device
# ls -l
total 97611
-rw-r--r-- 1 root root 99549184 2010-04-02 20:08 file
drwx------ 2 root root    12288 2010-04-02 18:48 lost+found
# tail -n 3 file
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8483
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8484
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX#
# Uh-oh...

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

БУФЕРЫ И ДЕСКРИПТОРЫ.

Ещё одно "решение", которое встречается время от времени, выглядит примерно так:

$ awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file     # DO NOT DO THIS

Это не даёт шеллу обрезать файл, так как используется '<>', что означает открытие файла на чтение и запись. Может показаться, что это просто святой грааль редактирования in-place. Так ли это?

Чтобы понять, почему это "работает" и почему (вы уже догадались) нельзя это использовать, давайте посмотрим на это с общей точки зрения.

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

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

awk '{gsub(/foo/, "bar"); print}'

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

awk '{gsub(/foo/, "bar"); print}' file 1<>file     # DO NOT DO THIS

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

Давайте посмотрим, что будет, если данных на выходе меньше, чем на входе.

$ cat > file
100
200
300
400
500
C^d
$ sed 's/00/A/' file 1<>file
$ cat file
1A
2A
3A
4A
5A

500

Это ожидаемо. После того, как были записаны изменения, остались ещё исходные данные, и sed, как и другие подобные утилиты, не вызывает truncate или ftruncate, так как он для этого не предназначен. Так что это не работает.

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

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

Первое - при использовании фильтров или потоковых редакторов чтение, очевидно, происходит раньше, чем запись. Поэтому, допустим, программа может прочитать 10 байт, а записать 20. Кроме того, операции ввода/вывода обычно буферизируются, то есть большинство программ используют буферизированные системные вызовы типа fread или fwrite. Эти вызовы не читают напрямую из файла и не пишут прямо в него, как это делают read и write. Вместо этого используются внутренние буферы (обычно реализованные с помощью библиотеки С), назначение которых - "аккумуляция" данных. Например, когда  приложение читает 10 байт с помощью fread, на самом деле читаются 4096 байт и заносятся в буфер, а 10 возвращаются приложению. Когда приложение пишет 20 байт, они заносятся в буфер вывода и только когда он будет заполнен (опять примерно 4096 байт), данные из него на самом деле пишутся в файл. Если стандартные дескрипторы ввода/вывода приложения не связаны с терминалом (если, разумеется, оно не вызывает read/write напрямую), входные/выходные данные будут буферизированы.

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

Итак, как работает буферизация. Давайте посмотрим на примере awk, с которого мы начали:

$ cat file
This is line1
This is line2
This is line3
This is line4
This is line5
$ awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file     # DO NOT DO THIS
$ cat file
This is a prepended line
This is line1
This is line2
This is line3
This is line4
This is line5

Этот явно сверхъестественный результат возможен благодаря буферизации ввода/вывода. Давайте взглянем на вывод strace:

$ strace awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file
...
open("file", O_RDONLY)              = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=70, ...}) = 0
ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff555abe30) = -1 ENOTTY (Inappropriate ioctl for device)
fstat(3, {st_mode=S_IFREG|0644, st_size=70, ...}) = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
read(3, "This is line1\nThis is line2\nThis"..., 70) = 70
read(3, "", 70)                         = 0
close(3)                                = 0
write(1, "This is a prepended line\nThis is li"..., 95) = 95
exit_group(0)

Здесь есть нечто странное: почему read вызван раньше, чем write, хотя в коде есть выражение print сразу в блоке BEGIN, которое явно должно было быть выполнено до чтения любых данных? Как мы уже сказали, I/O буферизирован, поэтому когда приложение пишет, данные не записываются в файл, пока в буфере есть свободное место или пока файл не будет закрыт или стерт. Поэтому код " print "This a prepended line" помещает строку в какой-то буфер записи библиотеки С, а не в файл. Это не очевидно из вывода strace, так как процесс полностью происходит в пространстве пользователя без системных вызовов. Затем выполнение кода продолжается, и awk входит в основной цикл, которые запрашивает чтение файла. Тогда буферизированный i/o пытается прочесть весь блок данных (read в strace), который в данном случае является всем файлом, хранящимся где-то в буфере ввода. Затем awk исполняет тело кода которое заключается в копировании входных данных в поток вывода. Обе операции буферизированы, поэтому чтение происходит из буфера ввода, а запись - из буфера вывода (которые уже содержит строку, записанную print в блоке begin, поэтому последующий вывод добавляется к ней.) Ничего этого не видно в strace, поскольку происходит в пространстве пользователя. Наконец, файл закрывается (так как завершается выполнение программы), дескриптор 1 сбрасывается, и, наконец, вызывается write. В результате всего описанного буфер вывода ко времени записи содержит как раз строку, которую мы хотели добавить в начало файла, плюс исходные данные файла, и эти данные пишутся в файл.

Давайте воспользуемся ltrace, которая показывает как системные вызовы, так и вызовы библиотек, чтобы подтвердить нашу догадку (вывод представлен не весь для большей наглядности):

$ ltrace -S -n3 awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file
...
   121    fileno(0x7f06e270b780)                        = 1
...
   213    fwrite("This is a prepended line", 1, 24, 0x7f06e270b780 <unfinished ...>
   214       SYS_fstat(1, 0x7fff663f4050)               = 0
   215       SYS_mmap(0, 4096, 3, 34, 0xffffffff)       = 0x7f06e2daa000
   216    <... fwrite resumed> )                        = 24
   217    __errno_location()                            = 0x7f06e2d9a6a8
   218    fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
...
   226    open("file", 0, 0666 <unfinished ...>
   227       SYS_open("file", 0, 0666)                  = 3
   228    <... open resumed> )                          = 3
...
   246    read(3,  <unfinished ...>
   247       SYS_read(3, "This is line1\nThis is line2\nThis"..., 70) = 70
   248    <... read resumed> "This is line1\nThis is line2\nThis"..., 70) = 70
...
   254    fwrite("This is line1", 1, 13, 0x7f06e270b780) = 13
   255    __errno_location()                            = 0x7f06e2d9a6a8
   256    fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   257    _setjmp(0x64d650, 0x1d64ceb, 0x1d635b0, 0, 0) = 0
   258    __errno_location()                            = 0x7f06e2d9a6a8
   259    fwrite("This is line2", 1, 13, 0x7f06e270b780) = 13
   260    __errno_location()                            = 0x7f06e2d9a6a8
   261    fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   262    _setjmp(0x64d650, 0x1d64cf9, 0x1d635b0, 0, 0) = 0
   263    __errno_location()                            = 0x7f06e2d9a6a8
   264    fwrite("This is line3", 1, 13, 0x7f06e270b780) = 13
   265    __errno_location()                            = 0x7f06e2d9a6a8
   266    fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   267    _setjmp(0x64d650, 0x1d64d07, 0x1d635b0, 0, 0) = 0
   268    __errno_location()                            = 0x7f06e2d9a6a8
   269    fwrite("This is line4", 1, 13, 0x7f06e270b780) = 13
   270    __errno_location()                            = 0x7f06e2d9a6a8
   271    fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   272    _setjmp(0x64d650, 0x1d64d15, 0x1d635b0, 0, 0) = 0
   273    __errno_location()                            = 0x7f06e2d9a6a8
   274    fwrite("This is line5", 1, 13, 0x7f06e270b780) = 13
   275    __errno_location()                            = 0x7f06e2d9a6a8
   276    fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   277    read(3,  <unfinished ...>
   278       SYS_read(3, "", 70)                        = 0
   279    <... read resumed> "", 70)                    = 0
   280    __errno_location()                            = 0x7f06e2d9a6a8
...
   284    close(3 <unfinished ...>
   285       SYS_close(3)                               = 0
   286    <... close resumed> )                         = 0
   287    free(0x1d64cd0)                               = <void>
   288    __errno_location()                            = 0x7f06e2d9a6a8
   289    fflush(0x7f06e270b780 <unfinished ...>
   290       SYS_write(1, "This is a prepended line\nThis is"..., 95) = 95
   291    <... fflush resumed> )                        = 0
   292    fflush(0x7f06e270b860)                        = 0
   293    exit(0 <unfinished ...>
   294       SYS_exit_group(0 <no return ...>
   295 +++ exited (status 0) +++

Awk использует буферизированный ввод/вывод (fread/fwrite), и в строке 121 файлу, соответствующему объекту по адресу  0x7f06e270b860 (предположительно указатель на объект FILE для stdout), присваивается дескриптор 1, т.е. stdout.

В строках 213-218 выполняется выражение print блока BEGIN. Заметьте, что системные вызовы записи отсутствуют, так что данные заносятся в буфер библиотеки С, а не в файл.
В строках 226-228 файл открывается на чтение, что является обычным поведением awk перед началом выполнения цикла, 246-248 - чтение содержимого файла (так как ввод данных буферизирован, вызов fread запускает системный вызов read, который считывает весь файл из буфера ввода). Начиная с 254 строки выполняется основное тело программы (т.е. {print}). Все данные поступают в буфер библиотеки С, где уже содержится вывод из блока BEGIN.
284 строка закрывает дескриптор, использованный для чтения файла. До этого момента файл всё ещё не модифицирован. В строке 289 очищается стандартный вывод, и только в 290 данные пишутся в файл.

Итак, в данном случае наши данные эффективно хранятся  в буфере вывода. Давайте запустим программу снова, но отключим буфер вывода (с помощью прекрасной утилиты stdbuf из пакета coreutils).

 stdbuf -o0 awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file
# hangs, press ctrl-C
^C
$ ls -l file
-rw-r--r-- 1 waldner waldner 10298496 Jan 28 15:04 file
$ head -n 20 file
This is a prepended line
This is a prepended line
e2
This is line3
This is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5

Это, наконец, демонстрирует, что, как и ожидалось, вывод большего количества данных, чем было введено, не работает, а если кажется, что работает, то только благодаря буферизированному выводу данных. Очевидно, что мы не знаем заранее, будет ли использоваться экранный буфер: это зависит от кода программы и других факторов. Даже если стандартом Posix предписывается, чтобы некоторые функции использовали буферизированый ввод/вывод данных, если они не обращаются к "интерактивному устройству", то нет гарантий, что приложение будет использовать эти функции ( fread/fwrite). Оно может использовать read и write напрямую, а они не имеют буферов. Даже при использовании буферизированных функций, ничто не помешает приложению вызвать fflush, когда ему заблагорассудится, или даже вообще отключить буферизацию. Если это произойдёт, начнётся ад.

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

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

# Let's prepend more than 4096 bytes to our file
$ awk 'BEGIN{for(i=1;i<=1100;i++)print i}1' file 1<>file
# after a while...
awk: write error: No space left on device
# Let's recreate the file
$ printf 'This is line 1\nThis is line 2\nThis is line 3\nThis is line 4\nThis is line 5\n' > file
# Let's try writing 5000 bytes at once now
$ awk 'BEGIN{printf "%05000d\n", 1}1' file 1<>file
$ cat file
[snip]
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
$ wc -l file
2 file

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

ЗАЛ ПОЗОРА.

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

$ sed 's/foo/longer/g' file 1<>file   # DO NOT DO THIS

# prepend data to a file. Some smart cats detect this and complain
$ { command; cat file; } 1<>file   # DO NOT DO THIS

# let's throw pipes into the mix

# prepend a file, bash
$ cat <(cat file1 file2) 1<>file2   # DO NOT DO THIS

# prepend a file, POSIX sh
$ cat file1 file2 | cat 1<>file2   # DO NOT DO THIS

# prepend text, "fooling" cat
$ { command; cat file; } | cat 1<>file   # DO NOT DO THIS

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

$ seq 100000 105000 > file
$ wc -l file
5001 file
$ { seq 1 2000; cat file; } | cat 1<>file
$ wc -l file
208229 file       # should be 7001
$ seq 100000 105000 > file
$ wc -l file
5001 file
$ { seq 1 2000; cat file; } | cat 1<>file
$ wc -l file
194630 file       # should be 7001
$ seq 100000 105000 > file
$ wc -l file
5001 file
# now let's add more data
$ { seq 1 20000; cat file; } | cat 1<>file
^C
$ ls -l file
-rw-r--r-- 1 waldner users 788046226 2010-05-09 15:26 file
# etc.

ЗАКЛЮЧЕНИЕ.

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

Update 28/12/2012:

Мне сообщили, что есть ещё способ записи в файл без создание временного файла. Прежде чем продемонстрировать его, повторю, что это плохая идея, разве что вы ДЕЙСТВИТЕЛЬНО знаете, что делаете (даже в этом случае подумайте 10 раз, прежде чем это делать).

Итак, по крайней мере в bash, подстановки, которые выполняет интерпретатор (раскрытие переменных, подстановки команд и т.п.) происходят до того, как действуют перенаправления. Поэтому можно сделать так:

mycommand > "$somefile"

Переменная $somefile должна быть раскрыта до перенаправления вывода. Как можно это использовать для редактирования in-place (в этом случае действительно без временного файла)? Просто:

printf '%s\n' "$(sed 's/foo/bar/g' file)" > file #Очередной пример для зала позора

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

$ printf '%s\n' "$(sed 's/foo/bar/g' bigfile)" > bigfile
-bash: xrealloc: ../bash/subst.c:658: cannot allocate 378889344 bytes (1308979200 bytes allocated)
Connection to piggy closed.

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

Другая, менее очевидная проблема этого подхода заключается в том, что (по крайней мере в bash) строковые литералы (такие, как второй аргумент для printf в примере) не могут содержать нули ascii, и если они появятся в выводе команды, то в результате будут проигнорированы.

Update 2 23/02/2013:

Для тех, кто хочет реального редактирования in-place, существует модуль Perl Tie::File, который позволяет модифицировать сам исходный файл с тем же инодом, а также делает всю грязную работу по расширению/обрезанию файла. Он представляет файл в виде массива, и код просто модифицирует массив, после чего изменения трансформируются в реальные изменения на диске. Разумеется, все предостережения остаются в силе: файл находится в промежуточном состоянии во время редактирования, а кроме того, по мере увеличения размера файла или количества модификаций, будет снижаться производительность. Как говорится, нельзя и рыбку съесть, и ....

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

Вот простой пример (хотя возможности этим не ограничиваются)

#!/usr/bin/perl

use Tie::File;
use warnings;
use strict;

my $filename = $ARGV[0];
my @array;

tie @array, 'Tie::File', $filename or die "Cannot tie $filename";

$array[9] = 'newline10';      # change value of line 10
splice (@array, 0, 5);        # removes first 5 lines
for (@array) {
  s/foo/longerbar/g;         # sed-like replacement
}

# etcetera; anything that can be done with an array can be done
# (but see the CAVEATS section in the documentation)

untie @array;

Тестовый прогон:

cat -n file.txt
     1 this line will be deleted 1
     2 this line will be deleted 2
     3 this line will be deleted 3
     4 this line will be deleted 4
     5 this line will be deleted 5
     6 this line will not be deleted foo
     7 foo foo abc def
     8 hello world
     9 something normal
    10 something weird
    11 something foobar
$ ls -i file.txt
11672298 file.txt
$ ./tiefile_test.pl file.txt
$ cat -n file.txt
     1 this line will not be deleted longerbar
     2 longerbar longerbar abc def
     3 hello world
     4 something normal
     5 newline10
     6 something longerbarbar
$ ls -i file.txt
11672298 file.txt

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

Update 3 17/03/2015:

Если (и только если)

・размер изменяемых и замещающих данных одинаков;
・вы точно знаете позицию в файле, в которую будут записаны изменённые данные;
・вы храбры,
ещё одной возможностью является древняя утилита dd. Фокус в том, что можно сказать программе не уничтожать файл, куда будет производиться запись, с помощью опции conv=notrunc. Таким образом, если нам известно, что текст, который мы хотим изменить, начинается с байта 200, можно сделать так:

$ printf "newtext" | dd of=myfile seek=199 bs=1 conv=notrunc
7+0 records in
7+0 records out
7 bytes (7 B) copied, 3.4565e-05 s, 86.8 kB/s

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