Как работают системные вызовы?

#compiler-construction #process #operating-system #interrupt #system-calls

#построение компилятора #процесс #операционная система #прерывание #системные вызовы

Вопрос:

Я понимаю, что пользователь может владеть процессом, и у каждого процесса есть адресное пространство (которое содержит допустимые ячейки памяти, на которые может ссылаться этот процесс). Я знаю, что процесс может вызвать системный вызов и передать ему параметры, как и любой другой библиотечной функции. Это, по-видимому, предполагает, что все системные вызовы выполняются в адресном пространстве процесса путем совместного использования памяти и т.д. Но, возможно, это всего лишь иллюзия, созданная тем фактом, что на языке программирования высокого уровня системные вызовы выглядят как любая другая функция, когда ее вызывает процесс.

Но теперь позвольте мне сделать шаг глубже и более тщательно проанализировать, что происходит под капотом. Как компилятор компилирует системный вызов? Возможно, это помещает имя системного вызова и параметры, предоставленные процессом, в стек, а затем помещает в инструкцию по сборке «TRAP» или что-то в этом роде — в основном инструкцию по сборке для вызова программного прерывания.

Эта команда сборки TRAP выполняется аппаратно, сначала переключая бит mode с пользователя на ядро, а затем устанавливая указатель кода на начало процедур обслуживания прерываний. С этого момента ISR выполняется в режиме ядра, которое извлекает параметры из стека (это возможно, поскольку ядро имеет доступ к любой ячейке памяти, даже к тем, которые принадлежат пользовательским процессам) и выполняет системный вызов и, в конце концов, отключает центральный процессор, который снова переключает бит режима, и пользовательский процесс начинается с того места, где он остановился.

Правильно ли я понимаю?

Прилагается приблизительная схема моего понимания: введите описание изображения здесь

Ответ №1:

Вы довольно хорошо понимаете; фокус в том, что большинство компиляторов никогда не будут записывать системные вызовы, потому что функции, которые вызывают программы (например, getpid(2) , chdir(2) и т.д.), На самом деле предоставляются стандартной библиотекой C. Стандартная библиотека C содержит код для системного вызова, независимо от того, вызывается ли он через INT 0x80 или SYSENTER . Это была бы странная программа, которая выполняет системные вызовы без библиотеки, выполняющей эту работу. (Хотя perl предоставляет syscall() функцию, которая может напрямую выполнять системные вызовы! Безумие, не так ли?)

Далее, память. Ядро операционной системы иногда имеет простой доступ из адресного пространства к памяти пользовательского процесса. Конечно, режимы защиты различны, и предоставленные пользователем данные должны быть скопированы в защищенное адресное пространство ядра, чтобы предотвратить модификацию предоставленных пользователем данных во время выполнения системного вызова:

 static int do_getname(const char __user *filename, char *page)
{
    int retval;
    unsigned long len = PATH_MAX;

    if (!segment_eq(get_fs(), KERNEL_DS)) {
        if ((unsigned long) filename >= TASK_SIZE)
            return -EFAULT;
        if (TASK_SIZE - (unsigned long) filename < PATH_MAX)
            len = TASK_SIZE - (unsigned long) filename;
    }

    retval = strncpy_from_user(page, filename, len);
    if (retval > 0) {
        if (retval < len)
            return 0;
        return -ENAMETOOLONG;
    } else if (!retval)
        retval = -ENOENT;
    return retval;
}
  

Это, хотя и не является самим системным вызовом, является вспомогательной функцией, вызываемой функциями системного вызова, которая копирует имена файлов в адресное пространство ядра. Он проверяет, чтобы убедиться, что все имя файла находится в пределах диапазона данных пользователя, вызывает функцию, которая копирует строку из пользовательского пространства, и выполняет некоторые проверки работоспособности перед возвратом.

get_fs() и подобные функции являются остатками x86-корней Linux. Функции имеют рабочие реализации для всех архитектур, но названия остаются архаичными.

Вся дополнительная работа с сегментами связана с тем, что ядро и пользовательское пространство могут совместно использовать некоторую часть доступного адресного пространства. На 32-разрядной платформе (где цифры легко понять) ядро обычно будет иметь один гигабайт виртуального адресного пространства, а пользовательские процессы обычно будут иметь три гигабайта виртуального адресного пространства.

Когда процесс обращается к ядру, ядро «исправляет» разрешения таблицы страниц, чтобы предоставить ему доступ ко всему диапазону, и получает преимущество от предварительно заполненных записей TLB для предоставленной пользователем памяти. Большой успех. Но когда ядру необходимо контекстно переключиться обратно в пространство пользователя, оно должно очистить TLB, чтобы удалить кэшированные привилегии на страницах адресного пространства ядра.

Но хитрость в том, что одного гигабайта виртуального адресного пространства не достаточно для всех структур данных ядра на огромных машинах. Поддержание метаданных кэшированных файловых систем и драйверов блочных устройств, сетевых стеков и сопоставлений памяти для всех процессов в системе может потребовать огромного объема данных.

Доступны различные «разделения»: два концерта для пользователя, два концерта для ядра, один концерт для пользователя, три концерта для ядра и т.д. По мере увеличения пространства для ядра пространство для пользовательских процессов уменьшается. Итак, существует 4:4 разделение памяти, которое дает четыре гигабайта пользовательскому процессу, четыре гигабайта ядру, и ядро должно манипулировать дескрипторами сегментов, чтобы иметь возможность доступа к пользовательской памяти. TLB сбрасывается при входе в системные вызовы и выходе из них, что приводит к довольно значительному снижению скорости. Но это позволяет ядру поддерживать значительно большие структуры данных.

Гораздо большие таблицы страниц и диапазоны адресов 64-разрядных платформ, вероятно, делают все предыдущее странным. Во всяком случае, я очень на это надеюсь.

Комментарии:

1. На TLBS с ASID очистка не должна быть необходимой. Но в остальном, действительно исчерпывающий ответ.

2. @ninjalj, о, это бы очень помогло. Позвольте мне угадать, они в основном доступны на системах PAE или 64-разрядных платформах? 🙂

3. они доступны, по крайней мере, в MIPS.

Ответ №2:

Да, вы все поняли в значительной степени правильно. Однако есть одна деталь: когда компилятор компилирует системный вызов, он будет использовать номер системного вызова, а не имя. Например, вот список системных вызовов Linux (для старой версии, но концепция все та же).

Ответ №3:

На самом деле вы вызываете библиотеку времени выполнения C. Не компилятор вставляет TRAP, а библиотека C оборачивает TRAP в вызов библиотеки. В остальном ваше понимание правильное.

Комментарии:

1. Не могли бы вы, пожалуйста, пояснить, что такое «Библиотека времени выполнения C». Кроме того, пользовательской программе разрешено вызывать системные вызовы напрямую, не проходя через какую-либо библиотеку, это правильно?

2. @p2pnode: библиотека времени выполнения C — это библиотека, с которой обычно связаны программы на C, в Unix она обычно называется libc . И да, программы могут вызывать системные вызовы напрямую.

3. итак, если программы могут вызывать системные вызовы напрямую, как эти вызовы будут компилироваться? Здесь, похоже, библиотека времени выполнения C не будет играть никакой роли ..?

4. @p2pnode: вы бы написали встроенный asm для вызова системного вызова.

Ответ №4:

Если вы хотите выполнить системный вызов непосредственно из вашей программы, вы могли бы легко это сделать. Это зависит от платформы, но, допустим, вы хотели прочитать из файла. У каждого системного вызова есть номер. В этом случае вы помещаете номер read_from_file системного вызова в регистр EAX. Аргументы для системного вызова размещаются в разных регистрах или стеке (в зависимости от системного вызова). После того, как регистры заполнены правильными данными и вы готовы выполнить системный вызов, вы выполняете инструкцию INT 0x80 (зависит от архитектуры). Эта инструкция является прерыванием, которое приводит к передаче управления операционной системе. Затем операционная система идентифицирует номер системного вызова в регистре EAX, действует соответствующим образом и возвращает управление процессу, выполняющему системный вызов.

Способ использования системных вызовов подвержен изменениям и зависит от данной платформы. Используя библиотеки, которые предоставляют простые интерфейсы для этих системных вызовов, вы делаете свои программы более независимыми от платформы, а ваш код будет намного более читаемым и быстрее записываться. Рассмотрите возможность реализации системных вызовов непосредственно на языке высокого уровня. Вам понадобится что-то вроде встроенной сборки, чтобы гарантировать, что данные помещены в правильные регистры.

Ответ №5:

Обычные программы обычно не «компилируют системные вызовы». Для каждого системного вызова обычно используется соответствующая функция библиотеки пользовательского пространства (обычно реализованная в libc в Unix-подобных системах). Например, mkdir() функция пересылает свои аргументы mkdir системному вызову.

В системах GNU (я предполагаю, что это то же самое для других), syscall() функция используется из функции ‘mkdir()’. Функции / макросы системного вызова обычно реализуются на C. Например, взгляните на INTERNAL_SYSCALL в sysdeps/unix/sysv/linux/i386/sysdep.h или syscall в sysdeps/unix/sysv/linux/i386/sysdep.S (glibc).

Теперь, если вы посмотрите на sysdeps/unix/sysv/linux/i386/sysdep.h , вы можете увидеть, что вызов ядра выполняется с помощью, ENTER_KERNEL которая исторически должна была вызывать прерывание 0x80 в процессорах i386. Теперь он вызывает функцию (я предполагаю, что она реализована в linux-gate.so , которая представляет собой виртуальный файл SO, отображаемый ядром, он содержит наиболее эффективный способ выполнения системного вызова для вашего процессора типа a).

Комментарии:

1. Ага! Это недостающее звено, на поиски которого я потратил полчаса. : D

Ответ №6:

Да, ваше понимание абсолютно верно, программа на C может вызывать прямой системный вызов, когда этот системный вызов происходит, это может быть серия вызовов до прерывания сборки. Я думаю, что ваше понимание может очень помочь новичку.Проверьте этот код, в котором я вызываю системный вызов «system».

 #include < stdio.h  >    
#include < stdlib.h >    
int main()    
{    
    printf("Running ps with "system" system call ");    
    system("ps ax");    
    printf("Done.n");    
    exit(0);    
}