Код
#статьи

Как появились и менялись переменные окружения

Детективное расследование о Unix.

Иллюстрация: Bell-Labs / Wikimedia commons / Colowgee для Skillbox Media

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

Иногда студенты спрашивают у меня: «Это вообще кому-то нужно сегодня?» Ещё как нужно! Окружения — это базовый механизм настройки приложений в любой Unix-подобной системе, будь это ноутбук с macOS, сервер в облаке или контейнер Kubernetes.

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

Роман Гордеев

Техлид в финтех-компании Tabby и автор курсов про тестирование на Python в Skillbox.

В далёком 2000 году впервые увидел чёрный экран FreeBSD 4.0 и полюбил Unix-консоль с первого взгляда.

Что такое переменные окружения?

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

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

Переменные окружения в их современном виде появились в 1979 году в седьмой версии Unix (Unix V7). Если посмотреть на генеалогию Unix-like операционных систем, то мы видим, что эта ОС находится у самых корней дерева, то есть в самом начале эпохи развития Linux, macOS и FreeBSD. Одним из компьютеров, работавших на ней, был PDP-11.

Авторы Unix V7 — Деннис Ритчи и Кен Томпсон — перед PDP-11
Фото: Bell Labs

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

В чём главная идея переменных окружения

Для начала разберёмся с идеей конфигурации и попробуем ответить на вопрос — почему она появилась? В этом нам поможет Джон, вымышленный сотрудник лаборатории компании DEC. Хай, Джон!

DEC — это компания, которая разрабатывала компьютеры PDP-11 и другие устройства. В распоряжении нашего Джона был терминал Teletype Model 33, вот как на фото выше. Работать было просто и удобно — Джон запускал исполняемый файл утилиты cat, набирая её имя на терминале. PDP-11 печатал результат запуска приложения со скоростью 100 слов в минуту, строка за строкой, как печатная машинка. Шумновато, но быстро.

Всё отлично работало, но прогресс не стоял на месте. Спустя небольшое время DEC разработала видеотерминал VT52 и один из них попал к Джону.

Видеотерминал VT52
Фото: Columbia University

Новый терминал отличается от старого — вместо листа бумаги у него дисплей! Из-за этого поменялось число выводимых символов: в Teletype Model 33 помещалось 72 символа в строке, а в VT52 уже 80 символов. Поменять эти значения невозможно: Teletype Model 33 — это печатная машинка, да и VT52 умеет отображать только символы ASCII и только одного размера.

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

Самый простой способ объяснить ему это — при запуске приложения указывать параметр, соответствующий терминалу. Например, так: cat TERM=dumb для Teletype Model 33 и cat TERM=vt52 для нового терминала с монитором. Джон договаривается с Деннисом Ритчи, и скоро появляется cat с поддержкой любой длины строк. Теперь можно использовать его и на новом, и на старом устройстве, указывая нужный параметр.

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

К тому моменту в Unix было написано уже несколько десятков приложений. В каждое из них пришлось добавлять поддержку параметра TERM, который подсказывал, с каким окружением приходится иметь дело. Но указывать TERM в параметрах — лишняя работа. Идеальный вариант — настроить всё окружение один раз и предоставить Unix самостоятельно подставлять параметры этих настроек для всех запускаемых приложений. Этот механизм — разделение параметров на окружение и собственно параметры приложения — и был реализован в Unix V7.

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

Идея понятна, спасибо Джону!

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

Как же выглядела настройка окружения в одной из самых первых версий Unix? Посмотрим в руководство UNIX Time Sharing System — UNIX Programmer’s Manual — Seventh Edition, Volume 1A от января 1979 года. На странице 161 в разделе про командный интерпретатор Shell есть информация и про окружение (environment).

Резюмирую для вас то, что там написано:

  • Окружение — это список пар «ключ — значение», который передаётся выполняемой программе так же, как и обычный список аргументов.
  • Для добавления переменных окружения используйте новую команду export.
  • Запускаемые из Shell команды наследуют одно и то же окружение.
  • Shell создаёт из переменных окружения параметры.

Всё работает точно так же, как и в современной Unix-подобной операционной системе. Более того, в мануале упоминаются переменные окружения PATH, HOME, TERM и другие, привычные нам по современной разработке.

С точки зрения пользователя способ работы с переменными окружения был придуман ещё на заре Unix и остался неизменным до сих пор.

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

Подкапотное пространство

Чтобы понять, что в Unix было сделано для работы окружения, опять загляну в документацию, ссылки из раздела про Shell ведут нас в ENVIRON (5) (пятёрка и другие числа в скобках далее — это номер раздела в документации).

Документация лаконична. Environ — это глобальная переменная, которая создаётся в результате вызова EXEC (2). Раз это глобальная переменная, то окружение хранится в памяти процесса. А процесс в операционной системе — это запущенный исполняемый файл в ней.

Но что такое вызов EXEC и как он создавал в памяти приложения Environ?

Вызов EXEC происходит во время работы пользователя в Shell. Когда он хочет запустить приложение, то набирает путь к его исполняемому файлу и предоставляет остальную работу Shell. Последний тоже не запускает приложение сам — он вызывает EXEC, передавая в нём параметры запуска приложения и параметры управления запуском операционной системы Unix.

Как же Unix обрабатывала EXEC? Посмотрим на документацию. Запуская программу на C, операционная система вызывает функцию main с аргументами main (argc, argv, envp). В argc, argv задаётся количество аргументов, которые были переданы, и список указателей на них. А вот envp — это указатель на окружение родительского процесса. В нашем примере это Shell, со всеми его переменными.

Но ещё до того, как выполнится main, будет создана копия того окружения, что находится в envp, и приложению она будет доступна как глобальная переменная environ. За заполнение списка переменных отвечает Shell, а за создание копии этого списка в памяти создаваемого процесса — сама операционная система.

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

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

Первое появление понятия окружения в седьмой версии Unix подтверждает то, что в мануалах шестой версии упоминаний про это нет. В документации про вызов EXEC (2) есть только два параметра — argc и argv, а envp отсутствует, то есть окружений ещё не было.

Ещё в дополнении к мануалу V7 — Annotated Excerpts from the Programmer’s Manual — можно найти замечание о том, что привычная нам Shell (Bourne shell) была написана как раз для седьмой версии, заменив предыдущую. Без поддержки в Shell окружение не было бы таким удобным для работы.

Что стало с окружением потом

Итак, у нас есть ответы на все вопросы, но я хочу посмотреть, что стало с окружением в современном Linux. Может быть, управление им изменилось?

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

Перед тем как смотреть на вызовы, сначала представлю себе, что я хочу увидеть. Отправной точкой будет вызов exec(). Почему он? Начиная с 1988 года существуют стандарты POSIX, определяющие то, как должны выглядеть системные вызовы.

Unix V7, про который мы говорили, появился раньше, но стандарты POSIX разрабатывались в основном под влиянием Unix-подобных систем. Поэтому Linux можно считать в большой степени POSIX-совместимым, а значит, мы можем искать в нём похожий вызов.

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

Запускаем в контейнере Ubuntu команду man exec и вновь открываем мануал. В Linux exec это целое семейство вызовов, но тут есть и глобальная переменная environ, и передача окружения через указатель envp. Но, чтобы нам подсмотреть в strace нужный вызов, нам надо знать его имя, судя по тому же man exec, это должен быть вызов execve. Благодаря POSIX-стандартам механизмы работы окружения в Linux очень похожи на те же механизмы в Unix V7.

Теперь запустим команду cat из Bash shell, чтобы увидеть параметры вызова:

strace -f -p <pid of bash> -e trace=execve
strace: Process 3238 attached
strace: Process 10145 attached
[pid 10145] execve("/usr/bin/cat", ["cat"], 0xaaaadacacb50 /* 12 vars */) = 0

Да, третьим аргументом передаётся список переменных, на него ссылается указатель 0xaaaadacacb50. Что там внутри у этого списка? Надо перезапустить strace с ключом -v:

strace -v -f -p <pid of bash> -e trace=execve
[pid 10149] execve("/usr/bin/cat", ["cat"], ["PID=3238","HOSTNAME=7057e22ae57d",
"PWD=/root", "HOME=/root", "LS_COLORS=rs=0:di=01;34:ln=01;36"..., "TERM=xterm", "SHLVL=2",
"PATH=/usr/local/sbin:/usr/local/"..., "OLDPWD=/", "_=/usr/bin/cat"]) = 0

Вот и наши знакомые переменные окружения в третьем параметре: PID, HOSTNAME, HOME и другие. Получается, что тут мало что отличается от вызова exec() времён Unix седьмой версии. Ну разве что не надо считать аргументы.

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

У меня есть подсказка — адрес указателя из strace — 0xaaaadacacb50. Посмотрим, что хранится по этому указателю в памяти bash:

gdb /bin/bash <pid of bash>
(gdb)x/s *(char **)0xaaaadacacb50
0xaaaadacf3870:	"PID=3238"

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

(gdb) x/s *(char **)(0xaaaadacacb50+32)
0xaaaadaccbca0:	"HOME=/root

Теперь, кажется, пришла пора остановиться.

Вместо итогов

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

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

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

Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!

Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Научитесь: Специалист по кибербезопас­ности Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована