Kodomo

Пользователь

Загрузка ОС

Что происходит, когда вы нажимаете на кнопку питания?

В компьютере есть некоторое количество намертво зашитой памяти (а нынче и не очень намертво) – Boot ROM (read only memory). Первео, что происходит после включения питания – это эта память копируется в оперативную и начинает исполняться. Программа в этой памяти называется BIOS (basic input-output system) и занимается тем, чтобы определить подключённое оборудование и подготовить несколько стандратных функций работы с устройствами. Кроме этого, эта же программа занимается тем, что даёт пользователю некоторое количество своих свойств настроить и именно она отвечает за сообщение "Keyboard not found, press F1 to continue", если вы забыли воткнуть в компьютер клавиатуру. По завершении своей работы, эта программа по идее должна найти на диске операционную систему и передать ей управление.

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

Чтобы искать, где находится ОС, придумали весьма идиотскую вещь: в самом начале диска выделили 512 байт, которые назвали словом MBR (Master Boot Record). Эти 512 байт складываются из: магического числа (в заданном месте должна стоять пара заранее известных чисел, чтобы отличить настоящий MBR от случайно оказавшегося на его месте мусора), таблица разделов, ещё немножко лишнего (как правило оно сводится к тому, что там должно быть 6 нулей), а остальные 440 байт – это загрузочная программа. У этой программы задачи: разобраться, что ей дали в качестве оборудования (хотя бы жёсткие диски и клавиатура), найти ядро операционной системы (а оно, наверное, лежит внутри файловой системы, значит, нужно разобраться с устройством файловой системы, и ещё оно может лежать внутри lvm, значит нужно разобраться с lvm), загрузить его в память, передать ему заданным образом несколько настроек пользователя, и передать ему управление. На всё про всё 440 байт. И я должен сказать, что машинные коды несколько менее компактны, нежели, например, язык python. Типичный MBR (например, MBR Windows) вместо всего этого делает другое: ищет на диске активный размер, находит в нём запись VBR (volume boot record) примерно такой же структуры и такого же размера, и передаёт управление ей. Но у VBR задача от этого проще не становится.

Решения этой задачи у разных загрузчиков разные.

Сейчас два самых популярных загрузчика Linux зовутся LiLo (Linux Loader) и grub (Grand Unified Bootloader).

lilo решает эту проблему так: он заводит очень маленький файл – настолько маленький, чтобы он умещался в один блок файловой системы, складывает в него адреса блоков данных файлов, в которых лежат ядро, initrd (см. далее), и части загрузчика. После чего в загрузочный блок (MBR или VBR, смотря куда установили) записывает минимум информации для того, чтобы найти этот файл, догрузить дополнительные части загрузчика, а потом ядро, initrd, и предать управление в ядро. Т.е. на самом деле, lilo грязными хаками обходится без того, чтобы знать что-либо о файловой системе. Беда только в том, что если случайно мы переустановим ядро и у нас что-нибудь сломается и мы не обновим карты памяти для lilo, загрузиться в систему будет невозможно.

grub подходит по-другому, он сделан из большого количества модулей, которые несут в себе знания о том, как работать с разными типами жёстких дисков, как устроены разные файловые системы, как устроен lvm, как работать с монитором и т.п. Имея все такие модули, уже совсем нетрудно спросить у пользователя, чего именно он хочет (нарисовать ему красивую картинку с вариантами загрузки и т.п.), загрузить в память ядро, initrd, настройки, и передать ядру управление. Проблема только в том, где можно сложить эти модули так, чтобы хотя бы до нескольких (для выбранной файловой системы и типа диска) можно было дотянуться не зная устройства файловой системы. От идиотизма делать такой маленький MBR спасает в этом случае этот же самый идиотизм: как правило, для удобства адресации каждый раздел начинают с начала очередного "цилиндра", а MBR занимает не весь "цилиндр", а лишь малую его часть – таким образом, сразу после MBR зачастую оказывается достаточно свободного места, чтобы расположить драйвера файловой системы и продолжать загрузку как белые люди. Второй вариант, куда могут расположить критические части grub – в некоторых файловых системах (из значимых – reiserfs) специально для этой роли отводится в начале достаточно большой кусок свободного места подряд. grub читает настройки уже после того, как загрузил самые критичные модули, и если в настройках есть какие-нибудь проблемы, выдаёт командную строку, из которой можно (если немного помучиться и почитать документацию) сказать ему, куда нужно загружаться.

Кроме загрузки Linux, и grub, и lilo умеют делать вид, что их тут не было, и что на самом деле это был BIOS, который запустил загрузку с VBR какого-нибудь раздела – таким образом они умеют грузить, например, Windows. Этот приём называется chainloading.

Что такое initrd? Сначала объясню, зачем он нужен. Дело в том, что для ядра Linux существует дикое множество драйверов для всяческих устройств, файловых систем, сетевых протоколо и т.п. Некоторые из этих драйверов друг с другом конфликтуют (то есть ядро, имеющее в себе оба драйвера, работать не будет, или будет очень неприятно глючить), да и вообще, ядро со всеми драйверами занимает десятки мегабайт, что пока что всё ещё довольно много. Поэтому ядро порезали на части, которые можно загружать и выгружать отдельно друг от друга – эти части называются модулями ядра. Мы можем создать ядро, которое содержит в себе только нужные для нашей системы драйвера и загрузить его – но тогда, если мы заменим у компьютера, например, мышь, нам придётся делать какие-нибудь усилия после загрузки (загрузить нужный модуль), чтобы мышь заработала. Поэтому предпочитают (авторы дистрибутивов) поступать иначе: одновременно с ядром загрузить маленький образ файловой системы (init ram drive; ram = random access memory = оперативная память), в которой будут лежать все необходимые ключевые модули и программы для обнаружения оборудования и загрузки нужных модулей. Устроен же initrd очень просто – это архив cpio (cpio, tar, zip, arj, rar – это всё примерно одного семейства программы – архиваторы и упаковщики).

Когда ядро запускается (загрузчиком системы), оно делает первое знакомство с тем, что ему дали – какие у него есть процессоры, сколько их, какие у них свойства, какие у них общеизвестные глюки, какая память, – после этого подцепляет initrd, ищет в нём файл /init, и запускает его. Обычно /init в initrd – это программа, которая загружает сколько-нибудь драйверов, монтирует корневую файловую систему с жёсткого диска и запускает /sbin/init уже оттуда. Эта программа уже работае всё время, пока работает система. Она отвечает за то, какие сервисы когда работают и ещё за некоторое количество мелочей. Каждая программа, которая хочет быть запущена (или остановлена) на каком-то этапе работы системы, тащит с собой скрипт в /etc/init.d: когда программу нужно запустить, запускается /etc/init.d/скрипт start, когда её нужно остановить, /etc/init.d/скрипт stop (как правило, у этих скриптов бывают удобства в виде restart, reload, и иногда каких-нибудь ещё). Кроме того, программа должна зарегистрировать этот скрипт в init, чтобы init знал, когда его нужно запускать, а когда нет – сейчас существует довольно много разных реализаций init, и у каждой это делается немного по-разному, поэтому тут я подробнее не поясняю.

Процессы

Процесс – это исполняемый экземпляр программы. (Бывают и другие определения понятия процесса в UNIX: процесс – это запись в таблице процессов или процесс – это сущность, имеющая свой process id).

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

С процессом в UNIX связано много всяческой информации:

Вернёмся снова к рассказу про procfs. Через procfs можно вытащить много информации о процессе. Дабы не пустословить слишком много, сведу весь рассказ к одному примеру. Я запустил sleep 1h (ничего не делать 1 час), узнал, что у него PID 8242, и могу разглядывать его свойства:

$ ls -l /proc/8242/cwd
lrwxrwxrwx 1 dendik dendik 0 Mar  4 17:57 /proc/8242/cwd -> /home/dendik
$ ls -l /proc/8242/exe
lrwxrwxrwx 1 dendik dendik 0 Mar  4 17:57 /proc/8242/exe -> /bin/sleep
$ ls -l /proc/8242/fd
total 0
lrwx------ 1 dendik dendik 64 Mar  4 17:57 0 -> /dev/pts/0
lrwx------ 1 dendik dendik 64 Mar  4 17:57 1 -> /dev/pts/0
lrwx------ 1 dendik dendik 64 Mar  4 17:57 2 -> /dev/pts/0
$ ls -ld /proc/8242
dr-xr-xr-x 6 dendik dendik 0 Mar  4 17:57 /proc/8242

Свойств в procfs хранится сильно больше, и далеко не всё из них понятно. Поэтому – а ещё и для того, чтобы уметь искать процессы по их свойствам, делают утилиты. Самых важных из них две: ps и top. По каждой из них я бы в первую очередь отослал вас к документации, однако документации в UNIX много разных видов, и ей можно посвятить половину занятия, поэтому пока что я ограничусь перечислением нескольких простых случаев:

Для всех этих программ в графических средах есть довольно самоочевидные графические аналоги.

Взаимодействие процессов

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

Первый способ вы уже знаете – через fifo: два процесса открывают один fifo и один в него пишет, а другой из него читает.

Есть ещё один аналогичный и гораздо более популярный способ – через pipe. pipe – это то же самое, что fifo, только его не бывает на файловой системе. Суть такая: один процесс создаёт pipe, потом делает fork и возможно потомок делает exec, и мы получаем две разные программы, у которых есть один общий pipe, о котором никто больше в системе не знает. Теперь одна из них может туда писать, а другая может оттуда читать.

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

Чтобы снять эти ограничения придумали ещё один способ: сигналы. Идея такая: любой процесс может послать любому другому процессу сигнал (если у него достаточно для этого прав: в нормальных условиях пользователь может посылать сигналы только процессам того же пользователя). При получении сигнала процесс останавливается (немедленно) и управление в нём передаётся специальной функции – обработчику сигнала. Когда обработчик сигнала завершит работать – если он не остановит процесс – исполнение продолжится как ни в чём не бывало. Сигналы различаются по номерам (и во многих UNIX'ах не может быть больше 255 разных сигналов). Для многих номеров сигналов есть человеческие имена. Если программа не зарегистрировала обработчик сигнала самостоятельно, система будет использовать обработчик по умолчанию. Для многих сигналов обработчик по умолчанию завершает процесс – поэтому функцию (и программу) посылки сигнала называют kill.

Я приведу несколько названий сигналов с идеей их назначения:

Посылать сигналы можно командами: kill -сигнал PID или killall -сигнал имя_команды. E.g: kill -WINCH 1234. Или просто: kill 1234, чтобы послать процессу 1234 SIGSTERM.

Зомби

Когда процесс завершается (умирает), происходит это не сразу. Первым делом процесс переводится в состояние Z – zombie. Затем родителю посылается сигнал SIGCHLD. И лишь после того, как родитель скажет wait для этого процесса, процесс окончательно уйдёт из таблицы процессов и из памяти компьютера.

Сделано это для того, чтобы после смерти ребёнок мог сообщить своему родителю exit code (и вообще чтобы родитель мог посмотреть всяческую информацию о ребёнке посмертно).

Как я уже отмечал выше, если родитель умер раньше ребёнка, родителем осиротевшего ребёнка станет init – и это ещё одна задача init – ожидать умерших детей.

Абсолютный и относительный путь

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

Это понятие в равной мере применимо к процессам (у которых есть cwd), и к символьным ссылкам (у которых есть содержащая их директория).

Терминалы

Самые первые компьютеры не имели никакого интерфейса к пользователям. У них был в лучшем случае набор лампочек, отображающий состояние какого-нибудь участка памяти – и математику, пришедшему за ответом, говорили: вот твой ответ вон на тех-то лампочках лежит. Первый человеческий интерфейс компьютера выглядел как печатающая машинка – т.е. это была пара из клавиатуры и принтера (правда, принтер был именно потомком печатающей машинки, он умел набирать только ограниченное множество символов). Только где-то в начале-середине 70-х появились первые "стеклянные терминалы" – точно такое же устройство, у которого вместо принтера был монитор. Проблема была в том, что память была очень дорограя и очень медленная, а для того, чтобы несколько десятков раз в секунду обновить каждый пиксель, нужно хранить состояние этого пикселя. Памяти на это не хватало, поэтому хранили, как и у печатной машинки, какая буква лежит где на экране. В 80-е годы начали появляться первые графические станции – у них в памяти хранилось состояние каждого пикселя и монитор каждый раз, чтобы отрисовать очередной пиксель мог обратиться к этой памяти – такая память стала называться видеопамятью. Потом появились графические среды, все забыли про терминалы, но понятие терминала к тому моменту играло уже очень существенную роль в UNIX, поэтому сразу за графическими средами появились программы, которые эмулировали терминалы в графическом окне.

Примером такой программы – правда, уже слитой с ssh-клиентом – является PuTTY. (Традиционно слово терминал сокращали буквами tty – TeleTYpe).

С точки зрения UNIX терминал – это семейство устройств: /dev/tty1 – текстовая консоль в линуксе, /dev/ttyS1 – терминал, подключённый через серийный порт (понятие нынче почти умершее), /dev/pty/1 – виртуальный терминал, созданный графическим эмулятором или ssh.

Изначально терминал был очень похож на печатную машинку, и его возможности совпадали с ней: там можно было набирать буквы (им дали коды), можно было перемотать барабан на следующую строку, этой команде дали код '\n', вернуть каретку в начало строки ('\r') или на одну позицию назад ('\h'), или начать новую страницу ('\v'). Задача терминала состояла в том, чтобы интерпретировать эти коды.

Тут можно сделать лирическое отступление о переносах строк. В той среде, где разрабатывался UNIX, терминалы были умные и по переносу строки автоматически возвращали каретку в начало (всегда). Поэтому в UNIX сложилась традиция обозначать переносы строк одним символом \n. Несколько позже возник Apple, и в их системах сложилась традиция сокращать перенос строки до одного символа \r2. Когда же возникала ОС CP/M, от которой начало взял DOS, от которого начало взяла Windows, его авторы очень честно сделали переносом строки сдвиг курсора на одну позицию вниз, а возвратом каретки перенос курсора в начало строки. Поэтому в Windows принято перенос строки обозначать \r\n, и некоторые программы (например, notepad) ничего другое в качестве переноса строки и понять не могут.

Возвращаясь к терминалам – со временем у терминалов возможностей стало больше и больше: стало возможно менять цвет текста, выбирать тип шрифта (жирный, курсивный), произвольно перемещать курсор по экрану. А область кодов, отведённая под коды управления терминалами, уже давно закончилась: с очень древних времён – с 1963 года, если верить википедии – был установлен стандарт, что числа от 0 до 33 служат управлению терминалом, а все остальные обозначают буквы, цифры или какие-нибудь ещё знаки. Поэтому люди придумали обозначать сложные команды терминалу цепочками символов, начинающимися с кода "escape" (\e). Такие цепочки символов стали называть escape-последовательностями (escape-sequence).

Но была беда: производители терминалов всякий раз стремились добавлять новые возможности в свои творения, но не согласовывали друг с другом обозначения управляющих последовательностей. К нашему времени успело накопиться уже порядка тысяч разных описаний форматов того, как нужно разговаривать с терминалами. Поэтому чуть ли не с самого начала существования UNIX в нём была база данных терминалов (termcap, ныне устаревшая в пользу terminfp)-- какие команды у них есть, и как какие команды реализоваывать – и была библиотека для работы с терминалом, которая прятала всё это безобразие от программиста (libcurses, ныне libncurses). И поэтому в UNIX традиционно при начале сессии устанавливается переменная среды TERM, в которой говорится, с какого типа терминала вы работаете.

Увы, и после окончания эпохи аппаратных терминалов, с началом эпохи виртуальных терминалов, борьба разных протоколов не завершилась.

Тут маленький кусочек прикладных данных: PuTTY по умолчанию выставляет в TERM значение xterm, однако на самом деле он ведёт себя ближе к rxvt – если вы заметите когда-нибудь при работе с PuTTY какие-нибудь глюки, попробуйте сменить идентификацию терминала – может помочь.

Итак, современный терминал – который почти наверняка виртуальный – умеет воспринимать команды управления курсором (и шрифтами, цветами, и пр.) из эскейп-последовательностей. Ещё, по мере того, как на клавиатуры добавлялись новые и новые кнопки, некоторые из них стали отображать тоже на такие же эскейп-последовательности (маленькое жизненное проявление этого – а скорее, впрочем, странного устройства головы у авторов – состоит в том, что в midnight commander нужно нажимать esc дважды).

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

Сеансы

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

Набрать w гораздо удобнее, чтобы узнать, кто работает за системой, чем, например, искать по списку процессов.

Рядом со всем этим цветником есть команда write кому [куда] (кому – имя пользователя, куда – название терминала). Программа читает строки со стандартного ввода и отправляет их на чужой терминал. С этой командой связана определённая традиция: если вам что-нибудь сказали через write, вам следует (если есть время), запустить write обратно и ответить; в диалоге принято обозначать строкой -o желание услышать ответ, а строкой oo намерение завершить диалог. Используйте благоразумие вместе с этой командой – не доставайте людей. (И ещё, учитывайте, что у вашего собеседника может быть криво настроен терминал на тему отображения русского).

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

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

Упражнения


  1. Например, в случае питона, это словарь с ключами -- строками и значениями -- строками. (1)

  2. На наше счастье, самые последние версии Mac OS X -- начиная с 10 -- заимствовали много от FreeBSD, и заодно и традицию делать переносы строк символом \n (2)