FreeBSD, 02 лекция (от 09 октября)
Материал из eSyr's wiki.
02.entering_kernel
Сегодня попробуем создать модуль с системным вызовом.
Начнем очень издалека. Что такое процессор? Это своего рода машина Тьюринга. Совокупность всех регистров есть внутреннее состояние процессора. Первые компьютеры называли вычислителями. Они шелестели перфокартами, ни на что не реагировали, и, посчитав, выдавали результат. В изначальной формулировке машины Тьюринга останов -- это очень хорошо, это значит, мы получили результат. А мы хотим, чтобы машина реагировала на события. Это значит, мы хотим прерывать работу машины Тьюринга. Так возникло понятие аппаратного прерывания. Работа машины прерывается, выполняется обработчик прерывания, потом работа продолжается. Как это происходит?
Текущее значение instruction pointer кладется на стек, stack pointer увеличивается на один, в instruction pointer загружается адрес обработчика прерывания. Программа, которая выполнялась, не должна заметить что процессор отвлекся, если только она не замеряет время. Для этого interrupt handler должен не попортить ни одного регистра. Поэтому все регистры, которые он использует, он сбрасывает на стек.
Что будет, если обработчик прерывания настолько сложен, что вызывает другие функции и даже не знает, какие регистры будет менять? В таких случаях можно либо сохранить все регистры на стеке, или использовать специальные аппаратные функции сохранения контекста. Такая процедура называется context switch. Почитать /usr/src/sys/i386/i386/switch.c /usr/src/sys/amd64/amd64/cpu_switch.S
Что будет если мы, заходя в прерывание, сохраним контекст, а выходя -- загрузим другой? Тогда у нас будет работать другой процесс. Мы сэмулировали многозадачность. Получается, если в машине есть хотя бы прерывание таймера, то мы можем реализовать многозадачную ОС.
Стало понятно, что концепция прерывания очень важна. Какие бывают прерывания?
- аппаратные -- с клавиатуры, видеокарты, i/o ready, i/o done. timer tick . Они абсолютно асинхронны, никак не связаны с тем, что машина делала. Произошло что-то внешне и прервало процесс. Они involuntary, то есть не по желанию процесса. Процесс может их ждать, но не может их вызвать.
- trap (sync, involuntary). Невыполнимая инструкция. Попытка выполнить привелиированную инструкцию, деление на ноль, обращение к недоступной памяти. Связано с тем, что машина только что делала. Обработчик может посмотреть в стек и понять, что вызвало проблему. Если приложение обращается к несуществующей странице, мы попадаем в ядро, и ядро может что-нибудь с этим сделать, чтобы приложению стало лучше.
- software interrupt -- запрошенное приложением прерывание. В принципе, называть его прерыванием не совсем верно -- никто не прерывал программу, она сама попросила вызвать внешний код. Фактически это вызов
привелигированной процедуры.
Режимы
Изначально было 4, но все ОС используют сейчас только самый нижний и самый верхний -- ring 3 и ring 0. Когда 386 процессор загружается, он сначала эмулирует 86, переводит его в protected mode и оказывается в ring 0. Официально шлюз в ring 0 -- это прерывание и возврат из прерывания. Как же нам вернуться? Подготавливается хитрый контекст, что как будто бы мы в нем выполнялись, и давайте в него вернемся. В ring 0 выполняется ядро, в ring 3 приложение. Юзерланд может прийти в ядро через trap или int, обратно через iret.
Современные ОС практически не пользуются софтварными прерываниями, используется 0x80 прерывание (i386) / syscall (amd64) для входа в ядро.
Сохранение контекста и прочее в таком случае делалось вручную, поэтому были придуманы новые способы. callgates -- из любого ринга в любой. потом появилось syscall, которая делает львиную долю работы и вход через прерывания ушел в прошлое и современные ос инструкцию int практически не выполняют. syscall можно рассматривать как софтварное прерывание, но с кучей дополнительной функциональности.
Системный вызов это договоренность между приложением и ядром, причем очень строгая. Приложение кладет в регистры номер системного вызова и аргументы и вызывает сисколл.
Ядро читает это все из регистра, делает процессинг, возможно большой. Когда сискол закончен, ядро кладет в регистр возвращаемое значение, в память приложения результат и возвращает выполнение приложению. С этого места приложение начинает выполняться в непривелигированном режиме. Для програмиста все выглядело как вызов функции. Посмотреть на это со стороны юзерланда можно в libc, со стороны ядра - /usr/src/sys/i386/i386/trap.c , /usr/src/sys/amd64/amd64/trap.c
Сделаем иллюстрацию. Выполним ping www.ru. И с помощью gdb посмотрим бэктрейс. Сначала идет gethostbyname2 , потом берется dtrace и просим оттрейсить ether_output от ping. Важно здесь увидеть то, что у нас есть стек приложения и стек в ядре. ассоциированный с этим приложением. Сисколл выполняется в контексте софтварного прерывания, хотя кроме умозрительной никакой связи между этими стеками нет, потому что один в юзерланд памяти, другой в ядерной.
У каждого треда в юзерланд процессе должен быть свой ядерный стек, и эти страницы нельзя положить в своп (как вообще ядерную память), поэтому мы не можем дать треду более двух страниц, потому что иначе было бы слишком много ядерной памяти, которая фактически бы очень редко использовалась.
Есть утилита, которая позволяет трейсить системные вызовы, которые делало приложение. Мы ее запускаем для определенного приложения, в ядре на него ставится флажок и ядро начинает писать в файл всю его активность. Можно удивиться, сколько простая программа делает системных вызовов.. Поскольку много кто слинкован динамически, делается куча ммапов на старте приложения.
Иногда для того чтобы было понятно ядро умозрительно разделяют на нижнюю и верхнюю часть, но в фрибсд вы этого практически не встретите. Есть последовательности вызова процедур от драйвера к юзерланду и пути в обратную сторону, из юзерленда вниз.
Есть некоторые структуры в ядре, разные для разных подсистем (сокетов, дисковой подсистемы, итд) и есть структуры которые доступны как тредам из софтварными интерраптов, так и из хардварных интерраптов.
Типичный процесс получения данных -- приходит пакет, ядро запускает обработчик прерывания. Оно может обработать его в контексте прерывания, а может пометить как требующий обработки тред и вернуться, чтобы планировщик решал когда обработать, выйдет из прерывания, пойдет по tcp/ip стеку, дойдет до приложения, поставит ему флажок что его надо разбудить, ему пришли данные, и закончится. Разбуженное приложение пойдет снова вниз в ядро и достанет данные. Для оптимизации может случаться, что вниз вызов сразу идет в самый низкий уровень, а обратно так невозможно. Нельзя из ядра подняться на самый верхний уровень. Мы пишем main и не можем допустить, чтобы он вдруг стал выполняться с совершенно другого места. Кроме случая обработки сигналов. Когда ядро посылает сигнал, приложение внезапно выполняет обработчик сигнала.
Никто не говорит, что обработчик прерывания должен как-то отвечать тому, что хочет юзерланд, он может заниматься чисто ядерными вещами (например, таймер управляет шедулерами и ничего приложению не пердает).
Как добавляется сисколл во фрибсд?
cd /usr/src/sys/kern/ vi syscalls.master
std -- входит в стандарт есть с этим номером всегда nostd -- не входит в стандарт
make sysent cd ../.. make buildkernel installkernel
Что делает make sysent? Генерирует /usr/include/sys/syscall.h (для юзерленда) , /usr/include/sys/sysproto.h (для ядра), и генерирует syscall.master для различных систем. Работает линуксовая совместимость -- роутинг сисколлов из одной таблицы в другую, это именно байнари совместимость, а не эмуляция.
Переходим к практике. Динамическое добавление сисколла.
- SYSCALL_MODULE(9)
- module(9)
- modstat(2)
- syscall(2)
svn co http://svn.freebsd.org/base/user/glebius/course vi course/02.entering_kernel/syscall/module/foo_syscall.c
Параметры сисколла должны быть выравнены по длине машинного слова. Соответственно если я делаю сисколл в котором будет два парметра, это значит что ему будут переданы два машинных слова и для нашего сисколла первый парметр был инт, а второй указатель на войд, и нам надо еще учесть литлендиан или бигендиан. В это все можно не вдаваться, потому что все генерируется.
Сначала используем макрос SYSCALL_MODULE. Он генерирует struct sysent в которой есть число агрументов обрабатывающей функции и указатель на нее.
Мы можем обратиться из сисколла к памяти процесса, но если окажется, что этот кусок памяти в свопе, то будет очень печально. Правильный способ - copy(9) -- copyin и copyout.
При этом в ядре нам тоже может потребоваться память. Как она аллоциируется? Похоже на юзерлэнд, но с некоторыми тонкостями. malloc(9). Принимает три аргумента, а не один, как в юзерланде. Во-первых, у памяти есть задаваемый макросом тип, который нужен для дебага и ситемного администрирования. Можно у ядра запросить, сколько памяти какого типа аллоцировано. С помощью этого также можно понять какой модуль виноват в утечке памяти. MALLOC_DEFINE - создание собственного типа.
Флаги же показывают принципиальное отличие от юзерлэнда. M_NOWAIT и M_WAITOK. в юзерлэнде malloc никогда не вовращает NULL, он блокируется и ждет, пока память появится. В ядре такой подход допустим далеко не всегда, поэтому если вы пишите M_NOWAIT, надо всегда проверять, что маллок вернул не ноль.
vmstat -m выдает таблицу выделения памяти.
Дизайн и реализация ОС FreeBSD
01 02 03 04 05 06 07 08 09 10 11 12
Календарь
Октябрь
| 02 | 09 | 16 | 23 | 30 |
Ноябрь
| 06 | 13 | 20 | 27 | |
Декабрь
| 04 | 11 | 18 |