FreeBSD, 02 лекция (от 09 октября)
Материал из eSyr's wiki.
(Новая: Сегодня попробуем создать модуль с системным вызовом. Начнем очень издалека. Что такое процессор? Эт...) |
м |
||
Строка 116: | Строка 116: | ||
vmstat -m выдает таблицу выделения памяти. | vmstat -m выдает таблицу выделения памяти. | ||
+ | |||
+ | {{FreeBSD}} | ||
+ | {{Lecture-stub}} |
Версия 14:58, 1 ноября 2013
Сегодня попробуем создать модуль с системным вызовом.
Начнем очень издалека. Что такое процессор? Это своего рода машина Тьюринга. Совокупность всех регистров есть внутреннее состояние процессора. Первые компьютеры называли вычислителями. Они шелестели перфокартами, ни на что не реагировали, и, посчитав, выдавали результат. В изначальной формулировке машины Тьюринга останов -- это очень хорошо, это значит мы получили результат. А мы хотим, чтобы машина реагировала на события. Это значит, мы хотим прерывать работу машины Тьюринга. Так возникло понятие аппаратного прерывания. Работа машины прерывается, выполняется обработчик прерывания, потом работа продолжается. Как это происходит?
Текущее значение instruction pointer кладется на стек, stack pointer увеличивается на один, в instruction pointer загружается адрес обработчика прерывания. Программа, которая выполнялась, не должна заметить что процессор отвлекся, если только она не замеряет время. Для этого interrupt handler должен не попортить ни одного регистра. Поэтому все регистры, которые он использует, он сбрасывает на стек.
Что будет, если обработчик прерывания настолько сложен, что вызывает другие функции и даже не знает, какие регистры будет менять? В таких случаях можно либо сохранить все регистры на стеке, или использовать специальные аппаратные функции сохранения контекста. Такая процедура называется context switch. Почитать user/src/sys/i386/i386/switch.c (для amd64 см. слайды).
Что будет если мы, заходя в прерывание, сохраним контекст, а выходя -- загрузим другой? Тогда у нас будет работать другой процесс. Мы сэмулировали многозадачность. Получается, если в машине есть хотя бы прерывание таймера, то мы можем реализовать многозадачную ОС.
Стало понятно, что концепция прерывания очень важна. Какие бывают прерывания?
1. аппаратные -- с клавиатуры, видеокарты, i/o ready, i/o done. timer tick . Они абсолютно асинхронны, никак не связаны с тем, что машина делала. Произошло что-то внешне и прервало процесс. Они involuntary, то есть не по желанию процесса. Процесс может их ждать, но не может их вызвать. 2. trap(sync, involuntary). Невыполнимая инструкция. Попытка выполнить привелиированную инструкцию, деление на ноль, обращение к недоступной памяти. Связано с тем, что машина только что делала. Обработчик может посмотреть в стек и понять, что вызвало проблему. Если приложение обращается к несуществующей странице, мы попадаем в ядро, и ядро может что-нибудь с этим сделать, чтобы приложению стало лучше. 3. software interrupt -- запрошенное приложением прерывание. В принципе, называть его прерыванием не совсем верно -- никто не прерывал программу, она сама попросила вызвать внешний код. Фактически это вызов привелигированной процедуры.
Режимы
Изначально было 4, но все ос используют сейчас только самый нижний и самый верхний -- ring 3 и ring 0. Когда 386 процессор загружается, он сначала эмулирует 86, переводит его в протектед мод и оказывается в ринг0. Официально шлюз в ринг0 это прерывание и возврат из прерывания. Как же нам вернуться? Подготавливается хитрый контекст, что как будто бы мы в нем выполнялись, и давайте в него вернемся. В ринг0 выполняется ядро, в ринг 3 приложение. Юзерланд может прийти в ядро через трап или инт, обратно через айрет.
Современные ос практически не пользуются софтварными прерываниями, используются 80 прерывание для входа в ядро.
Сохранение контекста и прочее в таком случчае делалось вручную, поэтому были придуманы новые способы. callgates -- из любого ринга в любогой. потом появилось syscall которая длеает львиную долю работы и вход через прерывания ушел в прошлое и современные ос инструкцию инт практически не выполняют. Сискалл можно рассматривать как софтварное прерывание но с кучей дополнительной функциональности.
Системный вызов это договоренность между приложением и ядром, причем очень строгая. Приложение кладет в регистры номер системного вызова и аргументы и вызывает сискалл.
Ядро читает это все из регистра, делает процессинг, возможно большой. Когда сискал закончен ядро кладет в регистр возвращаемое значение, в память приложения результат и возвращает выполнение приложениюю. С этого места приложение начинает выполняться в непривелигированном режиме. Для програмиста все выглядело как вызов функции. Посмотреть на это сос стороны юзерланда можно в либц, со стороны ядра sys/syscall.S
Сделаем иллюстрацию. Выполним ping www.ru. И с помощью гдб посмотрим бэктрейс. Сначала идет gethostbyname2 , потом берется dtrace и просим оттрейсить ether_output от ping. Важно здесь увидеть то, что у нас есть стек приложения и стек в ядре. ассоциированный с этим приложением. Сисколл выполняется в контексте софтварного прерывания, хотя кроме умозрительной никакой связи между этими стеками нет, потому что один в юзерланд памяти, другой в ядерной.
У каждого треда в юзерланд процессе должен быть свой ядерный стек, и эти страницы нельзя положить в своп (как вообще ядерную память), поэтому мы не можем дать треду более двух страниц, потому что иначе было бы слишком много ядерной памяти, которая фактически бы очень редко использовалась.
Есть утилита, которая позволяет трейсить системные вызовы, которые делало приложение. Мы ее запускаем для определенного приложения, в ядре на него ставится флажок и ядро начинает писать в файл всю его активность. Можно удивиться, сколько простая программа делает системных вызовов.. Поскольку много кто слинкован динамически, делается куча ммапов на страте приложения.
Иногда для того чтобы было понятно ядро умозрительно разделяют на нижнюю и верхнюю часть, но в фрибсд вы этого практически не встретите, разделение умозрительное. Есть последовательности вызова процедур от драйвера к юзерланду и пути в обратную сторону, из юзерланда вниз.
Есть некоторые структуры в ядре, разные для разных подсистем (сокетов, дисковой подсистемы, итд) и есть структуры которые доступны как тредам из софтварными интерраптов, так и из хардварных интерраптов. Типичный процесс получения данных -- приходит пакет, ядро запускает обработчик прерывания. оно может обработать его в контексте прерывания, а может пометить как требующий обработки тред и вернуться, чтобы планировщик решал когда обработать, выйдет из прерывания, пойдет по тцпип стеку, дойдет до приложения, поставит ему флажок что его надо разбудить ему пришли данные и закончитсяю. Разбуженное приложение пойдет снова вниз в ядро и достанет данные. Для оптимизации может случаться что вниз вызов сразу идет в самый низкий уровень, а обратно так невозможно. Нельзя из ядра подняться на самый верхний уровень. Мы пишем мейн и не можем допустить, чтобы он вдруг стал выполняться с совершенно другого места. Кроме случая обработки сигналов. Когда ядро посылает сигнал приложение внезапно выполняет обработчик сигнала.
Никто не говорит что обработчик прерывания должен как-тоотвечать тому, что хочет юзерланд, он может заниматься чисто ядерными вещами (например, таймер управляет шедулерами и ничего приложению не пердает).
Как добавляется сисколл во фрибсд?
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 для различных систем. Работает линуксовая совместимость -- роутинг сисколлов из одной таблицы в другую, это именно байнари совместимость, а не эмуляция.
Переходим к практике.
man SYSCALL_MODULE
man module
man modstat
man syscall
Параметры сисколла должны быть выравнены по длине машинного слова. Соответственно если я делаю сисколл в котором будет два парметра,
это значит что ему будут переданы два машинных слова и для нашего сисколла первый парметр был инт, а второй указатель на войд, и нам надо еще учесть литлендиан или бигендиан. Вэто все можно не вдаваться, потому
что все генерируется.
Сначала используем макрос SYSCALL_MODULE/. Он генерирует sysent в которой есть число агрументов и указател и на них.
Мы можем обратиться из сисколла к памяти процесса, но если окажется, что этот кусок памят с свопе, то будет очень печально. Правильный способ man 9 copy -- copy-in и copy-out.
При этом в ядре нам тоже может потребоваться память. Как она аллоциируется? Похоже на юзерлэнд но с некоторыми тонкостями. malloc(9). Принимает три аргумента, а не один, как в юзерланде. Во-первых у памяти есть задаваемый макросом тип, который нужен для дебага и ситемного администрирования. Можно у ядра запросить сколько памяти какого типа аллоцировано.
С помощью этого также можно понять какой модуль виноват в утечке памяти.
Флаги же показывают принципиальное олтчие от юзерлэнда. M_NOWAIT и M_WAITOK. в юзерлэнде маллок никогда не вовращает нулл, он блокируется и ждет, пока память появится. В ядре такой подход допстим далеко не всегда, поэтому если вы пишите новайт, надо всегда проверять, что маллок вернул не ноль.
Возьмем наш модуль и сделаем так, чтобы он аллоцировал память.
MALLOC_DEFINE создание собственного типа.
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 |