Как конвейеры ограничивают использование памяти?

36

Брайан Керниган объясняет в этом видео, что ранние исследования Bell Labs по отношению к небольшим языкам / программам основаны на ограничении памяти

Большая машина была бы 64 кбайт - K, а не M или G - и это означало, что любая отдельная программа не могла быть очень большой, и поэтому существовала естественная тенденция писать маленькие программы, а затем конвейерный механизм, в основном перенаправление ввода-вывода, позволяющее связать одну программу с другой.

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

Из Википедии :

В большинстве Unix-подобных систем все процессы конвейера запускаются одновременно [выделено мной]с соответствующими потоками, которые управляются планировщиком вместе со всеми другими процессами, запущенными на машине. Важным аспектом этого, отделяющего каналы Unix от других реализаций канала, является концепция буферизации: например, отправляющая программа может производить 5000 байтов в секунду, а принимающая программа может быть способна принимать только 100 байтов в секунду, но не данные потеряны. Вместо этого выходные данные отправляющей программы хранятся в буфере. Когда принимающая программа готова к чтению данных, следующая программа в конвейере считывает данные из буфера. В Linux размер буфера составляет 65536 байт (64 КБ). Доступен сторонний фильтр с открытым исходным кодом, называемый bfr, для обеспечения больших буферов при необходимости.

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

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

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

Пример:

sed 'simplesubstitution' file | sort | uniq > file2

Для меня ясно, что sedчтение в файле и выкладывание его построчно. Но sort, как утверждает БК в связанном видео, это полная остановка, поэтому все данные должны быть прочитаны в память (или не так ли?), А затем переданы uniq, что (на мой взгляд) будет одним -линейная программа. Но между первым и вторым каналом все данные должны быть в памяти, нет?

Malan
источник
1
unless swap is usedсвоп всегда используется, когда не хватает оперативной памяти
edc65

Ответы:

44

Данные не должны храниться в оперативной памяти. Трубы блокируют своих писателей, если читателей нет или они не успевают; под Linux (и я думаю, что в большинстве других реализаций) есть некоторая буферизация, но это не обязательно. Как упомянуто mtraceur и JdeBP (см . Ответ последнегоранние версии Unix буферизованных каналов на диск, и вот как они помогли ограничить использование памяти: конвейер обработки можно было бы разбить на небольшие программы, каждая из которых обрабатывала бы некоторые данные в пределах дисковых буферов. Небольшие программы занимают меньше памяти, а использование каналов означает, что обработка может быть сериализована: первая программа запустится, заполнит свой выходной буфер, будет приостановлена, затем вторая программа будет запланирована, обработает буфер и т. Д. Современные системы - это заказы величины больше, чем в ранних системах Unix, и может работать параллельно с несколькими каналами; но для огромных объемов данных вы все равно увидите похожий эффект (и варианты такого рода техники используются для обработки «больших данных»).

В вашем примере

sed 'simplesubstitution' file | sort | uniq > file2

sedсчитывает данные по fileмере необходимости, затем записывает их до тех пор, sortпока они готовы их прочитать; если sortне готов, блоки записи. В конце концов, данные действительно хранятся в памяти, но они специфичны sortи sortготовы решать любые проблемы (они будут использовать временные файлы, если объем данных для сортировки слишком велик).

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

strace seq 1000000 -1 1 | (sleep 120; sort -n)

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

Стивен Китт
источник
13
Этот ответ мог бы выиграть от дополнительного объяснения того, почему разбиение программ на множество маленьких экономит память: программа должна была умещаться в оперативной памяти, но только в текущей запущенной программе. Любая другая программа была выгружена на диск в ранних версиях Unix, и только одна программа могла быть выгружена в оперативную память одновременно. Таким образом, ЦП будет запускать одну программу, которая будет записывать в канал (который тогда находился на диске ), заменять эту программу и заменять ту, которая читает из канала. Элегантный способ превратить логически параллельную сборочную линию в пошаговое последовательное выполнение.
mtraceur
6
@malan: Несколько процессов могут быть запущены и могут быть в состоянии готовности одновременно. Но не более одного процесса может выполняться на каждом физическом процессоре в любой момент времени, и задача планировщика процессов ядра состоит в том, чтобы распределять «кусочки» процессорного времени каждому выполняемому процессу. В современных системах процесс, который запускается, но в настоящее время не запланирован, временной интервал ЦП обычно остается резидентным в памяти, пока он ожидает свой следующий фрагмент, но ядру разрешено выгружать память любого процесса на диск и снова возвращать в память как это находит удобным. (Handwaving некоторые детали здесь.)
Даниэль Приден
5
Процессы на любой стороне канала могут вести себя эффективно, как сопрограммы: одна сторона записывает, пока не заполнит буфер и блоки записи, после чего процесс не может ничего сделать с остальной частью своего временного интервала, и он переходит в Режим ожидания ввода-вывода. Затем ОС передает остаток временного интервала (или другого предстоящего временного интервала) стороне чтения, которая выполняет чтение до тех пор, пока в буфере и следующих блоках чтения ничего не останется, и в этот момент процесс чтения не может ничего сделать с остальными его временная шкала и возвращается к ОС. Данные проходят через канал по одному буферу за раз.
Даниэль Приден
6
@malan Программы запускаются «одновременно» концептуально на всех Unix-системах, только на современных многопроцессорных системах с достаточным объемом ОЗУ для их хранения, что означает, что они буквально все хранятся в ОЗУ одновременно, а в системе, которая может не держите их все в ОЗУ одновременно, некоторые выгружаются на диск. Также обратите внимание, что «память» во многих контекстах означает виртуальную память, которая является суммой как пространства ОЗУ, так и пространства подкачки на диске. Википедия сосредотачивается на концепции, а не на деталях реализации, особенно потому, что то, как действительно старый Unix делал вещи, теперь менее актуально.
mtraceur
2
@malan Кроме того, вы видите противоречие в двух разных значениях «память» (RAM против RAM + swap). Я говорил только об аппаратном ОЗУ, и в этом контексте только тот код, который в данный момент выполняется ЦП, должен помещаться в ОЗУ (что и влияло на решения, о которых говорит Керниган), в то время как в контексте всех программ логически выполнялись ОС в данный момент времени (на абстрактном уровне, предоставляемом поверх среза времени), программе просто нужно вписаться во всю виртуальную память, доступную ОС, включая пространство подкачки на диске.
mtraceur
34

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

Это твоя фундаментальная ошибка. Ранние версии Unix не содержали данные канала в оперативной памяти. Они сохранили их на диске. Трубы имели i-узлы; на дисковом устройстве, которое было обозначено как устройство трубы . Системный администратор запустил программу с именем, /etc/configчтобы указать (среди прочего), какой том, на каком диске было устройство канала, какой том был корневым устройством , а какое устройство дампа .

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

Этот механизм был заменен другими в середине и конце 1980-х годов. SCO XENIX получила «Высокопроизводительную систему трубопроводов», которая заменила i-узлы буферами в ядре. 4BSD превратил безымянные трубы в пары гнезд. AT & T переопределяет трубы, используя механизм STREAMS.

И, конечно, sortпрограмма выполняла ограниченную внутреннюю сортировку 32-килобайтных порций ввода (или любого меньшего объема памяти, который мог бы выделить, если 32-килобайт не было доступно), записывая отсортированные результаты в промежуточные stmX??файлы, в /usr/tmp/которых затем внешне объединялись, сортируя для получения окончательного результата. выход.

дальнейшее чтение

  • Стив Д. Пэйт (1996). "Межпроцессного взаимодействия". UNIX Internals: практический подход . Addison-Wesley. ISBN 9780201877212.
  • Морис Дж. Бах (1987). «Системные вызовы для файловой системы». Дизайн операционной системы Unix . Prentice-Hall. ISBN 0132017571.
  • Стивен В. Эрхарт (1986). " config(1M)". Руководство программиста Unix: 3. Средства системного администрирования . Холт, Райнхарт и Уинстон. ISBN 0030093139. С. 23–28.
JdeBP
источник
1

Вы частично правы, но только случайно .

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

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

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

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

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

Damon
источник
Когда вы говорите «трубы были названы» (JdeBP, кажется, говорит, что было одно «устройство трубы»), означает ли это, что было ограничение на количество каналов, которые можно использовать в данный момент времени (т. Е. Было ограничение на сколько раз вы могли использовать |в команде)?
Малан
2
Я никогда не видел такого предела, и я не думаю, что в теории когда-либо был такой. На практике все, что имеет имя файла, нуждается в иноде, и число инодов, конечно, конечно. Как и количество физических страниц в системе, если не больше. Современные системы гарантируют атомарную запись 4k, поэтому каждый канал должен иметь хотя бы одну полную страницу 4k, что накладывает жесткое ограничение на количество каналов, которые вы можете иметь. Но подумайте о том, чтобы иметь пару гигабайт оперативной памяти ... практически, с этим пределом вы никогда не столкнетесь. Попробуйте набрать несколько миллионов труб на терминале ... :)
Деймон