Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра Linux
Назад Архитектура и вокруг... Вперед

Траектория системного вызова

Основным предназначением ядра всякой операционной системы, вне всякого сомнения, является обслуживание системных вызовов из выполняющихся в системе процессов (операционная система занимается, скажем 99.999% своего времени жизни, и только на оставшуюся часть приходится вся остальная экзотика, которой и посвящена эта книга: обработка прерываний, обслуживание таймеров, диспетчеризация потоков и подобные «мелочи»). Поэтому вопросы взаимосвязей и взаимодействий в операционной системе всегда нужно начинать с рассмотрения той цепочки, по которой проходит системный вызов.

В любой операционной системе системный вызов (запрос обслуживания со стороны системы) выполняется некоторой процессорной инструкцией, прерывающей последовательное выполнение команд и передающий управление коду режима супервизора. Это обычно некоторая команда программного прерывания, в зависимости от архитектуры процессора исторически это были команды с мнемониками подобными: svc, emt, trap, int и подобными. Если для конкретики проследить архитектуру Intel x86, то это традиционно команда программного прерывания с различным вектором, интересно сравнить, как это делают самые разнородные системы:


Операционная система

MS-DOS

Windows

Linux

QNX

MINIX 3

Дескриптор прерывания для системного вызова


21h


2Eh


80h


21h


21h

Я специально добавил в таблицу две микроядерные операционные системы, которые принципиально по-другому строят обработку системных запросов: основной тип запроса обслуживания здесь требование отправки синхронного сообщения микроядра другому компоненту пользовательского пространства (драйверу, серверу). Но даже эта отличная модель только скрывает за фасадом то, что выполнение системных запросов, например, в QNX: MsgSend() или MsgReply() - ничего более на «аппаратном языке», в конечном итоге, чем процессорная команда int 21h с соответственно заполненными регистрами-параметрами.

Примечание: Начиная с некоторого времени (утверждается, что это примерно относится к началу 2008 года, или к времени версии Windows XP Service Pack 2) многие операционные системы (Windows, Linux) перешли от использования программного прерывания int к реализации системного вызова (возврата) через новые команды процессора sysenter (sysexit). Это было связано с заметной потерей производительности Pentium IV при классическом способе системного вызова. Но принципиально нового ничего не произошло: ключевые параметры перехода (CS, SP, IP) теперь загружаются не из памяти, а из специальных внутренних регистров MSR (Model Specific Registers) с предопределёнными (0х174, 0х175, 0х176) номерами (из большого общего числа), куда предварительно эти значения записываются, опять же, специальной новой командой wmsr... В деталях это громоздко, реализационно — производительно, а по сущности происходит то, что назвали: «вектор прерывания теперь забит в железо и процессор помогает нам быстрее перейти с одного уровня привилегий на другой».

Библиотечный и системный вызов из процесса

Теперь мы готовы перейти к более детальному рассмотрению прохождения системного вызова в Linux (будем основываться на классической реализации через команды int 80h / iret, потому что реализация через sysenter / sysexit ничего принципиально нового не вносит).

= = = = = = = = = =

здесь Рис.2 : системный вызов Linux

= = = = = = = = =

Прикладной процесс вызывает требуемые ему услуги посредством библиотечного вызова ко множеству библиотек а). *.so — динамического связывания, или б). *.a — статического связывания. Примером такой библиотеки является стандартная С-библиотека:

$ ls -l /lib/libc.*

	lrwxrwxrwx 1 root root 14 Мар 13 2010 /lib/libc.so.6 -> libc-2.11.1.so 

$ ls -l /lib/libc-*.*

	-rwxr-xr-x 1 root root 2403884 Янв 4  2010 /lib/libc-2.11.1.so 

Часть (значительная) вызовов обслуживается непосредственно внутри библиотеки, не требуя никакого вмешательства ядра, пример тому: sprintf() (или все строковые POSIX функции вида str*()). Другая часть потребует дальнейшего обслуживания со стороны ядра системы, например, вызов printf() (предельно близкий синтаксически к sprintf()). Тем не менее, все такие вызовы API классифицируются как библиотечные вызовы. Linux чётко регламентирует группы вызовов, относя библиотечные API к секции 2 руководств man. Хорошим примером тому есть целая группа функций для запуска дочернего процесса execl(), execlp(), execle(), execv(), execvp():

$ man 3 exec

	NAME 
	       execl, execlp, execle, execv, execvp - execute a file 
	SYNOPSIS 
	       #include <unistd.h> 
	...

Хотя ни один из всех этих библиотечных вызовов не запускает никаким образом дочерний процесс, а ретранслируют вызов к единственному системному вызову execve() :

$ man 2 execve

...

Описания системных вызовов (в отличие от библиотечных) отнесены к секции 3 руководств man. Системные вызовы далее преобразовываются в вызов ядра функцией syscall(), 1-м параметром которого будет номер требуемого системного вызова, например NR_execve. Для конкретности, ещё один пример: вызов printf(string), где: char *string — будет трансфороваться в write(1,string,strlen(string)), который далее в sys_call(__NR_write,...) и далее в int 0x80 (полный код такого примера показан страницей ниже).

В этом смысле очень показательно наблюдаемое разделение (упорядочение) справочных страниц системы Linux по секциям:

$ man man

...

The standard sections of the manual include:

1 User Commands

2 System Calls

3 C Library Functions

4 Devices and Special Files

5 File Formats and Conventions

6 Games et. Al.

7 Miscellanea

8 System Administration tools and Deamons

Таким образом, в подтверждение выше сказанного, справочную информацию по библиотечным функциям мы должны искать в секции 3, а по системным вызовам — в секции 2:

$ man 3 printf

...

$ man 2 write

...

Детально о самом syscall() можно посмотреть :

	$ man syscall
	 ИМЯ
	     syscall - непрямой системный вызов
	     ОБЗОР
	          #include <sys/syscall.h>
	          #include <unistd.h>
	          int syscall(int number, ...)
	     ОПИСАНИЕ
          syscall() выполняет системный вызов, номер которого задаётся значением number и с
          заданными аргументами. Символьные константы для системных вызовов можно найти в
          заголовочном файле ⟨sys/syscall.h⟩.
	...

Образцы констант некоторых хорошо известных системных вызовов (начало таблицы, в качестве примера):

$ head -n20 /usr/include/asm/unistd_32.h

	...
	#define __NR_exit                1
	#define __NR_fork                2
	#define __NR_read                3
	#define __NR_write               4
	#define __NR_open                5
	#define __NR_close               6
	...

Кроме syscall() Linux поддерживает и другой механизм системного вызова — lcall7(), устанавливая шлюз системного вызова, так, чтобы поддерживать стандарт iBCS2 (Intel Binary Compatibility Specification), благодаря чему на x86 Linux может выполняться бинарный код, подготовленный для операционных систем FreeBSD, Solaris/86, SCO Unix. Больше мы этот механизм упоминать не будем.

Системные вызовы syscall() в Linux на процессоре x86 выполняются через прерывание int 0x80. Соглашение о системных вызовах в Linux отличается от общепринятого в Unix и соответствует соглашению «fastcall». Согласно ему, программа помещает в регистр eax номер системного вызова, входные аргументы размещаются в других регистрах процессора (таким образом, системному вызову может быть передано до 6 аргументов последовательно через регистры ebx, ecx, edx, esi, edi и ebp), после чего вызывается инструкция int 0x80. Если системному вызову необходимо передать большее количество аргументов, то они размещаются в структуре, адрес на которую передается в качестве первого аргумента (ebx). Результат возвращается в регистре eax, а стек вообще не используется. Системный вызов syscall(), попав в ядро, всегда попадает в таблицу sys_call_table, и далее переадресовывается по индексу (смещению) в этой таблице на величину 1-го параметра вызова syscall() - номера требуемого системного вызова.

В любой другой поддерживаемой Linux/GCC аппаратной платформе (из многих) результат будет аналогичный: системные вызов syscall() будет «доведен» до команды программного прерывания (вызова ядра), применяемой на данной платформе, команд: EMT, TRAP или нечто подобное.

Пример прямой реализации системного вызова из пользовательского процесса на архитектуре x86 (архив int80.tgz) может выглядеть так:

mp.c :

	#include <string.h>
	#include <sys/stat.h> 
	#include <linux/kdev_t.h> 
	#include <sys/syscall.h> 

	int mknod_call( const char *pathname, mode_t mode, dev_t dev ) { 
	   long __res; 
	   __asm__ volatile ( "int $0x80": 
	      "=a" (__res): 
	     "a"(__NR_mknod),"b"((long)(pathname)),"c"((long)(mode)),"d"((long)(dev))
	   ); 
	   return (int) __res; 
	}; 

	void do_mknod( void ) { 
	   char *nam = "ZZZ"; 
	   int n = mknod_call( nam, S_IFCHR | S_IRUSR | S_IWUSR, MKDEV( 247, 0 ) ); 
	   printf( "mknod return : %d\n", n ); 
	} 
	
	int write_call( int fd, const char* str, int len ) { 
	   long __res; 
	   __asm__ volatile ( "int $0x80": 
	      "=a" (__res):"0"(__NR_write),"b"((long)(fd)),"c"((long)(str)),"d"((long)(len))); 
	   return (int) __res; 
	} 
	
	void do_write( void ) { 
	   char *str = "write syscall string!\n"; 
	   int len = strlen( str ) + 1, n;
	   printf( "string for write length = %d\n", len ); 
	   n = write_call( 1, str, len ); 
	   printf( "write return :%d\n", n ); 
	} 
	
	int getpid_call( void ) { 
	   long __res; 
	   __asm__ volatile ( "int $0x80":"=a" (__res):"a"(__NR_getpid) ); 
	   return (int) __res; 
	}; 
	
	void do_getpid( void ) { 
	   int n = getpid_call(); 
	   printf( "getpid return : %d\n", n ); 
	} 
	
	int main( int argc, char *argv[] ) { 
	   do_getpid(); 
	   do_write(); 
	   do_mknod(); 
	   return EXIT_SUCCESS; 
	}; 

А вот как происходит выполнение этого примера:

$ ./mp

	getpid return : 18753 
	string for write length = 23 
	write syscall string! 
	write return : 23 
	mknod return : -1 

$ sudo ./mp

	getpid return : 18767 
	string for write length = 23 
	write syscall string! 
	write return : 23 
	mknod return : 0 

$ ls ./Z*

	./ZZZ

- почему в первом запуске последний вызов завершился ошибкой? Да только потому, что системный вызов mknod() (в точности как и консольная команда mknod требует прав root)! А во всём остальном наша «ручная имитация» системных вызовов завершается успешно (что и подтверждает следующий запуск с правами root).

Примечание: Этот пример, помимо прочего, наглядно показывает замечательным образом как обеспечивается единообразная работа операционной системы Linux на десятке самых разнородных аппаратных платформ — то «узкое горлышко» передачи системного вызова ядру, которое будет принципиально меняться от платформы к платформе.

Возможен ли системный вызов из модуля?

Такой вопрос мне нередко задавали мне в обсуждениях. Оформим абсолютно тот же код предыдущего примера, но в формате модуля ядра (всё тот же архив int80.tgz).

md.c :

	#include <linux/init.h> 
	#include <linux/module.h> 
	#include <linux/unistd.h> 
	#include <linux/string.h> 
	#include <linux/cdev.h> 
	#include <linux/fs.h> 
	
	int write_call( int fd, const char* str, int len ) { 
	   long __res; 
	   __asm__ volatile ( "int $0x80": 
	      "=a" (__res):"0"(__NR_write),"b"((long)(fd)),"c"((long)(str)),"d"((long)(len)) ); 
	   return (int) __res; 
	} 
	
	void do_write( void ) { 
	   char *str = "write-call string!"; 
	   int len = strlen( str ) + 1, n;
	   printk( KERN_INFO "string for write length = %d\n", len ); 
	   n = write_call( 1, str, len ); 
	   printk( KERN_INFO "write return : %d\n", n ); 
	} 
	
	int mknod_call( const char *pathname, mode_t mode, dev_t dev ) { 
	   long __res; 
	   __asm__ volatile ( "int $0x80": 
	      "=a" (__res): 
	      "a"(__NR_mknod),"b"((long)(pathname)),"c"((long)(mode)),"d"((long)(dev))
	   ); 
	   return (int) __res; 
	}; 
	
	void do_mknod( void ) { 
	   char *nam = "ZZZ"; 
	   int n = mknod_call( nam, S_IFCHR | S_IRUGO, MKDEV( 247, 0 ) ); 
	   printk( KERN_INFO "mknod return : %d\n", n ); 
	} 
	
	int getpid_call( void ) { 
	   long __res; 
	   __asm__ volatile ( "int $0x80":"=a" (__res):"a"(__NR_getpid) ); 
	   return (int) __res; 
	}; 
	
	void do_getpid( void ) { 
	   int n = getpid_call(); 
	   printk( KERN_INFO "grtpid return : %d\n", n ); 
	} 
	
	static int __init hello_init( void ) { 
	   printk( KERN_INFO "======== start module ==========\n" ); 
	   do_write(); 
	   do_mknod(); 
	   do_getpid(); 
	   return -1;   // конструкция только для тестирования
	} 
	
	module_init( hello_init ); 

Примечание: Функция инициализации модуля hello_init() сознательно возвращает ненулевой код возврата — такой модуль заведомо никогда не может быть установлен, но его функция инициализации будет выполнена, и будет выполнена в пространстве ядра. Это эквивалентно обычному выполнению пользовательской программы (от main() и далее...), но в адресном пространстве ядра. Фактически, можно с некоторой условностью считать, что таким образом мы не устанавливаем модуль в ядро, а просто выполняем некоторый процесс (свой код), но уже в адресном пространстве ядра. Такой трюк будет неоднократно использоваться далее, и там же, в вопросах отладки ядерного кода, будет обсуждаться подробнее.

Выполняем аналогично тому, как мы это проделывали для процесса пользовательского пространства:

$ sudo insmod ./md.ko

insmod: error inserting './md.ko': -1 Operation not permitted

$ sudo cat /var/log/messages | tail -n50

	... 
	Jun 13 14:17:19 notebook kernel: ======== start module ========== 
	Jun 13 14:17:19 notebook kernel: string for write length = 19 
	Jun 13 14:17:19 notebook kernel: write return : -14 
	Jun 13 14:17:19 notebook kernel: mknod return : -14 
	Jun 13 14:17:19 notebook kernel: grtpid return : 9401 
	... 

Вызовы, соответствующие write() и mknod() завершились ошибкой, но getpid() выполнился нормально (сейчас мы не будем останавливаться на вопросе: что за значение он возвратил?). Что произошло? Каждый системный вызов выполняет последовательность действий :

  • копирование значений параметров вызова из пространства пользователя в пространство ядра;
  • выполнение операции в пространстве ядра;
  • копирование значений модифицируемых параметров (передаваемых по ссылке) из пространства ядра в пространство пользователя;

Вот на первом и/или последнем шагах попытки выполнить системный вызов из модуля и происходит аварийное завершение: в этом случае нет пространства пользователя в которое (или из которого) следует производить копирование данных. Но ничто более не препятствует собственно выполнению кода системного вызова из пространства ядра. Но именно по этой причине (наличие в системном вызове копирования между адресными пространствами, разноадресности параметров и результатов), для работы в пространстве ядра (и из модуля как динамической части ядра) было необходимо создать свой набор API, дублирующий по функциональности большинство библиотечных вызовов и системных вызовов. Например, должно существовать две полностью эквивалентных реализации элементарной и совершенно безобидной (не требующей вмешательства ядра) функции strlen(): одна из них будет размещена в теле разделяемой библиотеки libc.so, а другая — я коде ядра системы.

Примечание: Возьмите на заметку, что у этих двух обсуждаемых эквивалентных реализаций будет и различная авторская (если можно так сказать) принадлежность, и время обновления. Реализация в составе библиотеки libc.so, изготавливается сообществом GNU/FSF в комплексе проекта компилятора GCC, и новая версия библиотеки (и, возможно, её хэдер файлы в /usr/include) устанавливается, когда вы обновляете версию компилятора. А реализация версии той же функции в ядре принадлежит разработчикам ядра Linux, и будет обновляться когда вы, например, обновляете ядро из репозитария используемого дистрибутива, или самостоятельно собираете ядро из исходных кодов. А эти обновления (компилятора и ядра), как понятно, являются не коррелированными и не синхронизированными. Это не очевидная и часто опускаемая особенность!


Предыдущий раздел: Оглавление Следующий раздел:
Архитектура и вокруг...   Интерфейсы модуля