Главная страница
Навигация по странице:

  • Производительность системных вызовов

  • Обработка системных вызовов

  • Определение необходимого системного вызова

  • Передача параметров

  • Реализация системных вызовов

  • Проверка параметров

  • Контекст системного вызова

  • Окончательные шаги регистрации системного вызова

  • Второе издание


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница13 из 53
    1   ...   9   10   11   12   13   14   15   16   ...   53
    Номера системных вызовов
    Каждому системному вызову операционной системы 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
    Регистрации новых постоянных системных вызовов в ядре требует компиляции системного вы- зова в образ ядра. Тем не менее есть принципиальная возможность с помощью динамически за- гружаемого модуля ядра перехватить существующие системные вызовы и даже, ценой некоторых усилий, динамически зарегистрировать новые. — Примеч. персе.
    1   ...   9   10   11   12   13   14   15   16   ...   53


    написать администратору сайта