Второе издание
Скачать 3.09 Mb.
|
Номера системных вызовов Каждому системному вызову операционной системы Linux присваивается номер системного вызова (syscall number). Этот уникальный номер используется для обращения к определенному системному вызову. Когда процесс выполняет системный вызов из пространства пользователя, процесс не обращается к системному вызову по имени. Номер системного вызова является важным атрибутом. Однажды назначенный номер не должен меняться никогда, иначе это нарушит работу уже скомпилирован- ных прикладных программ. Если системный вызов удаляется, то соответствующий номер не может использоваться повторно. В операционной системе Linux предусмо- трен так называемый "не реализованный" ("not implemented") системный вызов — функция s y s _ n i _ s y s c a l l (), которая не делает ничего, кроме того, что возвращает значение, равное -ENOSYS, — код ошибки, соответствующий неправильному систем- ному вызову. Эта функция служит для "затыкания дыр" в случае такого редкого со- бытии, как удаление системного вызова. Ядро поддерживает список зарегистрированных системных вызовов в таблице системных вызовов. Эта таблица хранится в памяти, на которую указывает перемен- ная s y s _ c a l l _ t a b l e . Данная таблица зависит от аппаратной платформы и обычно определяется в файле e n t r y . S . В таблице системных вызовов каждому уникальному номеру системного вызова назначается существующая функция s y s c a l l . 5 Может быть, интересно, почему вызов g e t p i d () возвращает поле tgid, которое является иден- тификатором группы потоков (thread group ID)? Это делается потому, что дли обычных процессов значение параметра TGID равно значению параметра PID. При наличии нескольких потоков зна- чение параметра TGID одинаково дли всек потоков одной группы. Такая реализация дает возмож- ность различным потокам вызывать функцию getpid () и получать одинаковое значение параме- тра PID. 98 Глава 5 Производительность системных вызовов Системные вызовы в операционной системе Linux работают быстрее, чем во многих других операционных системах. Это отчасти связано с невероятно малым временем переключения контекста. Переход в режим ядра и выход из него являются хорошо отлаженным процессом и простым делом. Другой фактор — это простота как механизма обработки системных вызовов, так и самих системных вызовов. Обработка системных вызовов Приложения пользователя не могут непосредственно выполнять код ядра. Они не могут просто вызвать функцию, которая существует в пространстве ядра, так как ядро находится в защищенной области памяти. Если программы смогут непосред- ственно читать и писать в адресное пространство ядра, то безопасность системы "вылетит в трубу". Пользовательские программы должны каким-либо образом сигнализировать ядру о том, что им необходимо выполнить системный вызов и что система должна пере- ключиться в режим ядра, где системный вызов должен быть выполнен с помощью ядра, работающего от имени приложения. Таким механизмом, который может подать сигнал ядру, является программное прерывание: создается исключительная ситуация (exception) и система переключа- ется в режим ядра для выполнения обработчика этой исключительной ситуации. Обработчик исключительной ситуации в данном случае и является обработчиком си- стемного вызова (system call handler). Для аппаратной платформы х8б это программ- ное прерывание определено как машинная инструкция i n t $0x80. Она приводит в действие механизм переключения в режим ядра и выполнение вектора исключи- тельной ситуации с номером 128, который является обработчиком системных вы- зовов. Обработчик системных вызовов— это функция с очень подходящим именем system_call (). Данная функция зависима от аппаратной платформы и определена в файле e n t r y . S 6 . В новых процессорах появилась такая новая функция, как sysent- er. Эта функция обеспечивает более быстрый и специализированный способ входа в ядро для выполнения системного вызова, чем использование инструкции программ- ного прерывания — i n t . Поддержка такой функции была быстро добавлена в ядро. Независимо от того, каким образом выполняется системный вызов, основным явля- ется то, что пространство пользователя вызывает исключительную ситуацию, или прерывание, чтобы вызвать переход в ядро. Определение необходимого системного вызова Простой переход в пространство ядра сам по себе не является достаточным, по- тому что существует много системных вызовов, каждый из которых осуществляет переход в режим ядра одинаковым образом. Поэтому ядру должен передаваться но- мер системного вызова. 6 Большая часть дальнейшего описания процесса обработки системных вызовов базируется на вер- сии для аппаратной платформы x86. Но не стоит волноваться, для других аппаратных платформ это выполняется аналогичным образом. Системные вызовы 99 Для аппаратной платформы х86 номер системного вызова сохраняется в регистре процессора еах перед тем, как вызывается программное прерывание. Обработчик системных вызовов после этого считывает это значение из регистра еах. Для других аппаратных платформ выполняется нечто аналогичное. Функция system_call() проверяет правильность переданного номера системно- го вызова путем сравнения его со значением постоянной NR_syscalls. Если значе- ние номера больше или равно значению NR_syscalls, то функция возвращает зна- чение -ENOSYS. В противном случае вызывается соответствующий системный вызов следующим образом: call *sys_call_table(,%eax,4) Так как каждый элемент таблицы системных вызовов имеет длину 32 бит (4 байт), то ядро умножает данный номер системного вызова на 4 для получения нужной по- зиции в таблице системных вызовов (рис. 5.2). Вызов функции read() Оболочка функции read() Приложение Оболочка функции read () в библиотеке С Пространство пользователя Вызов функции system_call() Вызов функции sys_read() Обработчик Системных вызовов Функция sys_read() Пространство ядра Рис. 5.2. Запуск обработчика системных вызовов и выполнение системного вызова Передача параметров В дополнение к номеру вызова, большинство системных вызовов требует пере- дачи им одного или нескольких параметров. Во время перехвата исключительной ситуации пространство пользователя должно каким-либо образом передать ядру эти параметры. Самый простой способ осуществить такую передачу — это сделать по аналогии с передачей номера системной функции: параметры хранятся в регистрах процессора. Для аппаратной платформы х86 регистры ebx, ecx, edx, esi, edi со- держат соответственно первые пять аргументов. В случае редких ситуаций с шестью или более аргументами, используется один регистр, который содержит указатель на память пространства пользователя, где хранятся все параметры. Возвращаемое значение также передается в пространство пользователя через ре- гистр. Для аппаратной платформа х86 оно хранится в регистре еах. 100 Глава 5 Реализация системных вызовов Реализация системного вызова в ОС Linux не связана с поведением обработчика системных вызовов. Добавление нового системного вызова в операционной системе Linux является сравнительно простым делом. Тяжелая работа связана с разработкой и реализацией самого системного вызова. Регистрация его в ядре проста. Давайте рассмотрим шаги, которые необходимо предпринять, чтобы написать новый систем- ный вызов в операционной системе Linux. Первый шаг в реализации системного вызова — это определение его назначения, т.е. что он должен делать. Каждый системный вызов должен иметь только одно на- значение. Мультиплексные системные вызовы (один системный вызов, который выполняет большой набор различных операций, в зависимости от значения флага, передаваемого в качестве аргумента) в операционной системе Linux использовать не рекомендуется. Для примера того, как не надо делать, можно обратиться к системной функции i o c t l ( ) . Какие должны быть аргументы, возвращаемые значения и коды ошибок для но- вой системной функции? Системная функция должна иметь понятный и простой ин- терфейс, по возможности с меньшим количеством аргументов. Семантика и поведе- ние системных функций — это очень важные вещи, они не должны меняться, потому что от них будет зависеть работа прикладных программ. Важным является разработка интерфейса с прицелом на будущее. Не ограниче- ны ли возможности функции без необходимости? Разрабатываемый системный вы- зов должен быть максимально общим. Не нужно полагать, что завтра он будет ис- пользоваться так же, как сегодня. Назначение системного вызова должно оставаться постоянным, но его использование может меняться. Является ли системный вызов переносимым? Не нужно делать допущений о возможном размере машинного слова или порядка следования байтов. В главе 19, "Переносимость", рассматриваются со- ответствующие вопросы. Нужно удостовериться, что никакие неверные допущения не будут мешать использованию системного вызова в будущем. Помните девиз Unix: "Обеспечивать механизм, а не стратегию". При разработке системного вызова важно помнить, что переносимость и устой- чивость необходимы не только сегодня, но и будут необходимы в будущем. Основные системные вызовы ОС Unix выдержали это испытание временем. Большинство из них такие же полезные и применимые сегодня, как и почти тридцать лет назад! Проверка параметров Системные вызовы должны тщательно проверять все свои параметры для того, чтобы убедиться, что их значения адекватны и законны. Системные вызовы выпол- няются в пространстве ядра, и если пользователь может передать неправильные зна- чения ядру, то стабильность и безопасность системы могут пострадать. Например, системные вызовы для файлового ввода-вывода данных должны про- верить, является ли значение файлового дескриптора допустимым. Функции, свя- занные с управлением процессами, должны проверить, является ли значение пере- данного идентификатора PID допустимым. Каждый параметр должен проверяться не только на предмет допустимости и законности, но и на предмет правильности значения. Системные вызовы 101 Одна из наиболее важных проверок— это проверка указателей, которые передает пользователь. Представьте, что процесс может передать любой указатель, даже тот, ко- торый указывает на область памяти, не имеющей прав чтения! Процесс может таким обманом заставить ядро скопировать данные, к которым процесс не имеет доступа, на- пример данные, принадлежащие другому процессу. Перед тем как следовать указателю, переданному из пространства пользователя, система должна убедиться в следующем. • Указатель указывает на область памяти в пространстве пользователя. Нельзя, чтобы процесс заставил ядро обратиться к памяти ядра от имени процесса. • Указатель указывает на область памяти в адресном пространстве текущего про- цесса. Нельзя позволять, чтобы процесс заставил ядро читать данные других процессов. • Для операций чтения есть права на чтение области памяти. Для операций за- писи есть права на запись области памяти. Нельзя, чтобы процессы смогли обойти ограничения на чтение и запись. Ядро предоставляет две функции для выполнения необходимых проверок при копировании данных в пространство пользователя и из него. Следует помнить, что ядро никогда не должно слепо следовать за указателем в пространстве пользователя! Одна из этих двух функций должна использоваться всегда. Для записи в пространство пользователя предоставляется функция copy_to_user (). Она принимает три параметра: адрес памяти назначения в пространстве пользовате- ля; адрес памяти источника в пространстве ядра; и размер данных, которые необхо- димо скопировать, в байтах. Для чтения из пространства пользователя используется функция copy_from_user (), которая аналогична функции copy_to_user (). Эта функция считывает данные, на которые указывает второй параметр, в область памяти, на которую указывает пер- вый параметр, количество данных — третий параметр. Обе эти функции возвращают количество байтов, которые они не смогли скопи- ровать в случае ошибки. При успешном выполнении операции возвращается нуль. В случае такой ошибки стандартным является возвращение системным вызовом зна- чения -EFAULT. Давайте рассмотрим пример системного вызова, который использует функции copy_from_user () и copy_to_user () . Системный вызов s i l l y _ c o p y () является до крайности бесполезным. Он просто копирует данные из своего первого параме- тра во второй. Это очень не эффективно, так как используется дополнительное про- межуточное копирование в пространство ядра безо всякой причины. Но зато это позволяет проиллюстрировать суть дела. /* * Системный вызов silly copy — крайне бесполезная функция, * которая копирует len байтов иэ области памяти, * на которую указывает параметр src, в область памяти, * на которую указывает параметр dst, с использованием ядра * безо всякой на то причины. Но это хороший пример! */ asmlinkage long sys_silly_copy(unsigned long *src, unsigned long *dst, unsigned long len) 102 Глава 5 } unsigned long buf; /* возвращаем ошибку, если размер машинного слова в ядре не совпадает с размером данных, переданных пользователем */ if (len != sizeof(buf)) return -EINVAL; /* копируем из src, который является адресом в пространстве пользователя, в buf */ if (copy_from_user (&buf, src, len)) return -EFAULT; /* копируем из buf в dst, который гоже является адресом в пространстве пользователя */ if (copy_to_user (dst, &buf, len) ) return -EFAULT; /* возвращаем количество скопированных данных */ return len; } Следует заметить, что обе функции, copy_from_user () и copy_to_user ( ) , мо- гут блокироваться. Это возникает, например, если страница памяти, содержащая дан- ные пользователя, не находится в физической памяти, а в данный момент вытеснена на диск. В таком случае процесс будет находиться в приостановленном состоянии до тек пор, пока обработчик прерываний из-за отсутствия страниц (page fault handler) не возвратит страницу памяти в оперативную память из файла подкачки на диске. Последняя проверка — это проверка на соответствие правам доступа. В старых версиях ядра Linux стандартом было использование функции s u s e r () для систем- ных вызовов, которые требуют прав пользователя root. Эта функция просто про- веряла, запущен ли процесс от пользователя root. Сейчас эту функцию убрали и заменили более мелко структурированным набором системных "возможностей ис- пользования" (capabilities). В новых системах предоставляется возможность прове- рять специфические права доступа к специфическим ресурсам. Функция capable () с допустимым значением флага, определяющего тип прав, возвращает ненулевое зна- чение, если пользователь обладает указанным правом, и нуль— в противном случае. Например, вызов c a p a b l e (CAP_SYS_NICE) проверяет, имеет ли вызывающий про- цесс возможность модифицировать значение параметра nice других процессов. По умолчанию суперпользователь владеет всеми правами, а пользователь, не являющий- ся пользователем root, не имеет никаких дополнительных прав. Следующий пример системного вызова, который демонстрирует использование возможностей использо- вания, тоже является практически бесполезным. asmlinkage long sys_am_i_popular (void) { /* Проверить, имеет пи право процесс использовать возможность CAP_SYS_NICE */ if (!capable(CAP_SYS_NICE)) return -EPERM; /* Возвратить нуль, чтобы обозначить успешное завершение */ return 0; } Список всех "возможностей использования" и прав, которые за ними закрепле- ны, содержится в файле Системные вызовы 103 Контекст системного вызова Как уже обсуждалось в главе 3, "Управление процессами", при выполнении си- стемного вызова ядро работает в контексте процесса. Указатель current указывает на текущее задание, которое и есть процессом, выполняющим системный вызов. В контексте процесса ядро может переходит в приостановленное состояние (на- пример, если системный вызов блокируется при вызове функции или явно вызывает функцию schedule ()), а также является полностью вытесняемым. Эти два момента важны. Возможность переходить в приостановленное состояние означает, что си- стемный вызов может использовать большую часть функциональных возможностей ядра. Как будет видно из главы 6, "Прерывания и обработка прерываний", наличие возможности переходить в приостановленное состояние значительно упрощает про- граммирование ядра 7 . Тот факт, что контекст процесса является вытесняемым, под- разумевает, что, как и в пространстве пользователя, текущее задание может быть вытеснено другим заданием. Так как новое задание может выполнить тот же систем- ный вызов, необходимо убедиться, что системные вызовы являются реентерабель- ными. Это очень похоже на требования, выдвигаемые для симметричной мультипро- цессорной обработки. Способы защиты, которые обеспечивают реентерабельность, описаны в главе 8, "Введение в синхронизацию выполнения кода ядра", и в главе 9, "Средства синхронизации в ядре". После завершение системного вызова управление передается обратно в функцию system_call (), которая в конце концов производит переключение в пространство пользователя, и далее выполнение пользовательского процесса продолжается. Окончательные шаги регистрации системного вызова После того как системный вызов написан, процедура его регистрации в качестве официального системного вызова тривиальна и состоит в следующем. • Добавляется запись в конец таблицы системных вызовов. Это необходимо сде- лать для всех аппаратных платформ, которые поддерживают этот системный вызов (для большинства системных вызовов — это все возможные платформы). Положение системного вызова в таблице — это номер системного вызова, на- чиная с нуля. Например, десятая запись таблицы соответствует системному вы- зову с номером девять. • Для всех поддерживаемых аппаратных платформ номер системной функции должен быть определен в файле include/linux/unistd.h. • Системный вызов должен быть вкомпилирован в образ ядра (в противополож- ность компиляции в качестве загружаемого модуля 8 ). Это просто соответствует размещению кода в каком-нибудь важном файле каталога kernel/. 7 Обработчики прерываний не могут переходить в приостановленное состояние и, следовательно, более ограничены в своих действиях по сравнению с системными вызовами, которые работают в контексте процесса. 8 Регистрации новых постоянных системных вызовов в ядре требует компиляции системного вы- зова в образ ядра. Тем не менее есть принципиальная возможность с помощью динамически за- гружаемого модуля ядра перехватить существующие системные вызовы и даже, ценой некоторых усилий, динамически зарегистрировать новые. — Примеч. персе. |