Развиваем утилиту для работы с процессами. Разбираемся с таблицей страниц.
И немножко поддержим сигналы.
Общие моменты второй домашки остаются в силе.
Хотелось бы избежать гонок, связанных с тем, что процесс, с которым работаем внутри системного вызова, на входе в вызов существует, но где-то до выхода внезапно завершается.
Так же стоит подумать о том, что таблица страниц процесса может претерпевать изменения по ходу работы наших системных вызовов.
Документация по xv6: https://pdos.csail.mit.edu/6.828/2023/xv6/book-riscv-rev3.pdf
Кратко самое полезное про GDB: https://cs.brown.edu/courses/cs033/docs/guides/gdb.pdf
Полный GDB User Guide: https://sourceware.org/gdb/current/onlinedocs/gdb.html/
Стоимость - 1 балл
Системный вызов
int ps_pt0(int pid, uint64* table)
Возвращает 0 в случае успеха и -1 в случае любых проблем.
Получает адрес выделенной в пользовательском пространстве памяти.
Заполняет ее данными первого уровня таблицы страниц процесса с идентификатором pid
.
Используя его, реализуем подкоманду ps
:
ps pt 0 <pid> [-v]
По строчке вывода на элемент таблицы. Первое поле - номер элемента. Второе поле - hex dump элемента. Третье поле - hex dump базового адреса и строковое описание битовых флагов через запятую. Что-то типа READ,EXECUTE. Для факта валидности ничего специально не пишем.
Без опции -v выводим только валидные элементы. С опцией -v выводим все. Для невалидного элемента в третьем поле пишем INVALID и больше ничего.
Стоимость - 1 балл
Сделаем нечто аналогичное для второго уровня.
int ps_pt_1(int pid, void* addr, uint64* table)
Делаем один шаг согласно второму параметру и заполняем table
данными таблицы.
Используя этот системный вызов, реализуем подкоманду ps
:
ps pt 1 <pid> <address> [-v]
Вывод - как для pt 0
.
Стоимость - 1 балл
Разовьем эту идею на 1 шаг дальше. Новый системный вызов и новая подкоманда.
Новых параметров не появляется. Оба шага определяются адресом.
Стоимость - 2 балла
Дамп памяти. Хотим по виртуальному адресу и процессу понять содержимое памяти.
Реализуем системный вызов
int ps_copy(int pid, void* addr, int size, void* data)
Передаем область памяти data
(в процессе, вызвавшем ps_copy
) размером size
.
Возвращает 0 при успехе и меньше нуля при любых проблемах.
Нужно в адресном пространстве процесса pid
найти адрес addr
, определить его физический адрес
и скопировать size
байтов в data
.
Если процесс не живой на момент начала вызова, то это ошибка.
Если какой-либо адрес в диапазоне [addr
, addr
+ size
) не существует в адресном пространстве процесса,
то это ошибка.
Мы можем передать в том числе и собственный pid
.
На первый взгляд, в таком случае можно ограничиться простым копированием.
Но толку от такой оптимизации на самом деле мало, потому что для проверки нам надо получить pid
,
а это уже поход в ядро. А раз идем в ядро, то можно и копирование там же организовать.
Поэтому давайте в такой ситуации мы тоже будем работать обычным образом.
Но надо учесть следующее. Если вы копируете область памяти своего же процесса, то область, в которую вы ее копируете, может перекрыться с копируемой. В таком случае надо позаботиться о том, чтобы скопировалось именно исходное состояние требуемой области.
Сделаем подкоманду ps
ps dump <pid> <address> <size>
И напечатаем результат - как обычно дамп печатают. Или сообщение об ошибке.
Стоимость - 2 балла
Хотим для спящих процессов узнавать, на каком системном вызове они уснули.
Невозможно уснуть, не сделав системного вызова.
И для спящего процесса может быть интересно узнать, на каком системном вызове он уснул.
Конечно, может быть важен и стек вызовов в пользовательской части, и (хотя, и реже) в части, относящейся к ядру.
Но давайте в рамках задания ограничися хотя бы информацией о вызове.
Но кроме самого вызова (кода/имени вызова) еще могут быть интересны параметры.
Только тут есть чисто технический нюанс. У каждого системного вызова свои параметры. И чтобы обработать все системные вызовы, надо для каждого из них сделать свою обработку. И написать длинный case.
Давайте ограничимся одним вызовом - write
. Он интересный, потому что там несколько параметров.
Один из них - указатель.
Напишем системный вызов который по pid
сделает следующее:
-
проверит, находится ли процесс в спящем состоянии
-
если да, то поймет, на каком системном вызове процесс уснул
-
если он уснул на системном вызове
write
, то скопирует в переданную область памяти все параметры, использованные в данном вызовеwrite
(дескриптор, длину буфера и содержимое буфера).
Конкретная сигнатура системного вызова - на ваше усмотрение. Встраивание его в логику ps
- тоже.
Поскольку часто мы пишем с помощью write
строки, но не всегда, дамп содержимого буфера можно сделать так.
В каждой строчке выводятся 16 символов-байтов.
В левой части 16 позиций отводится на символьное представление. Если значение байта меньше, чем 32, печатаем точку.
В правой части печатаем 16-ричное представление. Две позиции на байт, одна на пробел, дополнительный пробел на каждые 4 или 8 байтов. В итоге влезем в 80 символов.
Можно символьную часть справа, а не слева напечатать.
Стоимость - 3 балла
xv6 поддерживает только сигнал SIGKILL
.
Добавим поддержку сигнала SIGPIPE
.
Поддержка будет очень простая.
Поддерживаем только "настоящий" сигнал, не сгенерированный через вызов kill
.
Это избавляет нас от необходимости думать о том, как быть со спящим процессом.
А настоящий сигнал SIGPIPE
возникает при попытке записи в pipe
, все читающие дескрипторы
которого закрыты (надо учесть dup
).
Реакцией будет только завершение процесса без дампа или игнорирование - никаких программных обработчиков.
Но нужно учесть маску. Ради маски заведем два системных вызова -
void sigsetmask(int mask)
и
int siggetmask()
Хотя у нас сигналов совсем немного, сделаем маску из рассчета на 32 сигнала.
И пусть будут константы SIGPIPE
и SIGKILL
.
И еще поддержим вызов
int signal(int sig, void (*handler)(int))
Второй параметр - для совместимости с линуксным вызовом. Нам нужно поддержать только два
макроса - SIG_IGN
и SIG_DFL
.
signal(SIGPIPE, SIG_IGN)
должен приводить к ингорированию незамаскированного сигнала SIGPIPE
.
signal(SIGPIPE, SIG_DFL)
должен приводить к завершению процесса при наличии незамаскированного сигнала SIGPIPE
.
Никакие вызовы signal(SIGKILL, ...)
не должны препятствовать завершению процесса по SIGKILL
.
Помним про отличие игнорирования от маскирования.