Как работают системные вызовы? - PullRequest
41 голосов
/ 05 июня 2011

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

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

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

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

Ответы [ 6 ]

14 голосов
/ 05 июня 2011

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

8 голосов
/ 05 июня 2011

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

4 голосов
/ 05 июня 2011

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

3 голосов
/ 15 июня 2013

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

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

3 голосов
/ 05 июня 2011

Обычные программы обычно не "компилируют системные вызовы".Для каждого системного вызова обычно используется соответствующая функция библиотеки пользовательского пространства (обычно реализуемая в 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-файл, отображаемый ядром, он содержит наиболее эффективный способ сделать системный вызов для вашего типа CPU).

0 голосов
/ 26 июля 2012

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