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

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

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

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

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

Прилагается грубая схема моего понимания:

6 ответов

Решение

Ваше понимание довольно близко; хитрость в том, что большинство компиляторов никогда не будут писать системные вызовы, потому что функции, которые вызывают программы (например, 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, чтобы удалить кэшированные привилегии на страницах адресного пространства ядра.

But the trick is, one gigabyte of virtual address space isnot sufficient for all kernel data structures on huge machines. Maintaining the metadata of cached filesystems and block device drivers, networking stacks, and the memory mappings for all the processes on the system, can take a huge amount of data.

So different 'splits' are available: two gigs for user, two gigs for kernel, one gig for user, three gigs for kernel, etc. As the space for the kernel goes up, the space for user processes goes down. Так что есть4:4 memory split that gives four gigabytes to the user process, four gigabytes to the kernel, and the kernel must fiddle with segment descriptors to be able to access user memory. The TLB is flushed entering and exiting system calls, which is a pretty significant speed penalty. But it lets the kernel maintain significantly larger data structures.

The much larger page tables and address ranges of 64 bit platforms probably makes all the preceding look quaint. I sure hope so, anyway.

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

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

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

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

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

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

Да, ваше понимание абсолютно верно, программа на 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);    
}
Другие вопросы по тегам