Подтвердить что ты не робот

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

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

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

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

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

Прикрепленная грубая диаграмма моего понимания: enter image description here

4b9b3361

Ответ 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(), а аналогичные функции - это остатки корней Linux x86. Функции имеют рабочие реализации для всех архитектур, но имена остаются архаичными.

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

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

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

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

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

Ответ 2

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

Ответ 3

Фактически вы вызываете библиотеку времени выполнения C. Это не компилятор, который вставляет TRAP, это библиотека C, которая обертывает TRAP в вызов библиотеки. Остальное ваше понимание правильное.

Ответ 4

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

В системах GNU (я думаю, это то же самое для других), функция syscall() используется из функции "mkdir()". Функция syscall/macro обычно выполняется в 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 CPU. Теперь он вызывает функцию (я предполагаю, что она реализована в linux-gate.so, который является виртуальным SO файлом, отображаемым ядром, он содержит наиболее эффективный способ сделать syscall для вашего типа процессором).

Ответ 5

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

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

Ответ 6

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

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