Учебное пособие по системным вызовам Linux с C

Linux System Call Tutorial With C



В нашей последней статье о Системные вызовы Linux , Я определил системный вызов, обсудил причины, по которым их можно использовать в программе, и углубился в их преимущества и недостатки. Я даже привел краткий пример сборки в C. Он проиллюстрировал суть и описал, как сделать вызов, но ничего продуктивного не сделал. Не совсем увлекательное упражнение для развития, но оно иллюстрирует суть.

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







Хотя в какой-то момент своей карьеры разработчика C вы неизбежно будете использовать системный вызов, если вы не нацеливаетесь на высокую производительность или функциональность определенного типа, библиотека glibc и другие базовые библиотеки, включенные в основные дистрибутивы Linux, позаботятся о большинстве твои нужды.



Стандартная библиотека glibc предоставляет кросс-платформенный, хорошо протестированный фреймворк для выполнения функций, которые в противном случае потребовали бы системных системных вызовов. Например, вы можете прочитать файл с помощью fscanf (), fread (), getc () и т.д., или вы можете использовать системный вызов Linux read (). Функции glibc предоставляют больше возможностей (например, лучшую обработку ошибок, форматированный ввод-вывод и т. Д.) И будут работать на любой системе, поддерживаемой glibc.



С другой стороны, бывают случаи, когда бескомпромиссная производительность и точное исполнение имеют решающее значение. Обертка, которую предоставляет fread (), добавляет накладные расходы, и хотя она незначительна, она не полностью прозрачна. Кроме того, вам могут не понадобиться или не потребоваться дополнительные функции, которые предоставляет оболочка. В этом случае лучше всего использовать системный вызов.





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

Теперь, когда вы прочитали заявления об отказе от ответственности, предупреждения и возможные обходные пути, давайте рассмотрим несколько практических примеров.



Какой у нас процессор?

Вопрос, который большинство программ, вероятно, не задумывается, но, тем не менее, действительный. Это пример системного вызова, который не может быть продублирован с помощью glibc и не покрыт оболочкой glibc. В этом коде мы вызовем вызов getcpu () напрямую через функцию syscall (). Функция системного вызова работает следующим образом:

системный вызов(SYS_call,arg1,arg2,...);

Первый аргумент SYS_call - это определение, представляющее номер системного вызова. Когда вы включаете sys / syscall.h, они включаются. Первая часть - это SYS_, а вторая - имя системного вызова.

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

example1.c

#включают
#включают
#включают
#включают

intглавный() {

беззнаковыйПроцессор,узел;

// Получить текущее ядро ​​ЦП и узел NUMA через системный вызов
// Обратите внимание, что здесь нет оболочки glibc, поэтому мы должны вызывать ее напрямую
системный вызов(SYS_getcpu, &Процессор, &узел,НУЛЕВОЙ);

// Отображаем информацию
printf ('Эта программа выполняется на ядре ЦП% u и узле NUMA% u. п п',Процессор,узел);

возвращение 0;

}

Скомпилировать и запустить:

gcc example1.c -o example1
./example1

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

Sendfile: превосходная производительность

Sendfile представляет собой отличный пример повышения производительности с помощью системных вызовов. Функция sendfile () копирует данные из одного файлового дескриптора в другой. Вместо использования нескольких функций fread () и fwrite () sendfile выполняет передачу в пространстве ядра, уменьшая накладные расходы и тем самым повышая производительность.

В этом примере мы собираемся скопировать 64 МБ данных из одного файла в другой. В одном из тестов мы собираемся использовать стандартные методы чтения / записи из стандартной библиотеки. Во втором случае мы будем использовать системные вызовы и вызов sendfile () для переноса этих данных из одного места в другое.

test1.c (glibc)

#включают
#включают
#включают
#включают

#define BUFFER_SIZE 67108864
#define BUFFER_1 'buffer1'
#define BUFFER_2 'buffer2'

intглавный() {

ФАЙЛ*неправильный, *конец;

printf (' пТест ввода-вывода с традиционными функциями glibc. п п');

// Захватываем буфер BUFFER_SIZE.
// В буфере будут случайные данные, но нас это не волнует.
printf ('Выделение буфера 64 МБ:');
char *буферзнак равно (char *) маллок (РАЗМЕР БУФЕРА);
printf ('СДЕЛАНО п');

// Записываем буфер в fOut
printf ('Запись данных в первый буфер:');
неправильныйзнак равно fopen (BUFFER_1, 'wb');
fwrite (буфер, размер(char),РАЗМЕР БУФЕРА,неправильный);
fclose (неправильный);
printf ('СДЕЛАНО п');

printf ('Копирование данных из первого файла во второй:');
конецзнак равно fopen (BUFFER_1, 'rb');
неправильныйзнак равно fopen (БУФЕР_2, 'wb');
бояться (буфер, размер(char),РАЗМЕР БУФЕРА,конец);
fwrite (буфер, размер(char),РАЗМЕР БУФЕРА,неправильный);
fclose (конец);
fclose (неправильный);
printf ('СДЕЛАНО п');

printf ('Освобождение буфера:');
бесплатно (буфер);
printf ('СДЕЛАНО п');

printf ('Удаление файлов:');
Удалить (BUFFER_1);
Удалить (БУФЕР_2);
printf ('СДЕЛАНО п');

возвращение 0;

}

test2.c (системные вызовы)

#включают
#включают
#включают
#включают
#включают
#включают
#включают
#включают
#включают

#define BUFFER_SIZE 67108864

intглавный() {

intнеправильный,конец;

printf (' пТест ввода-вывода с помощью sendfile () и связанных системных вызовов. п п');

// Захватываем буфер BUFFER_SIZE.
// В буфере будут случайные данные, но нас это не волнует.
printf ('Выделение буфера 64 МБ:');
char *буферзнак равно (char *) маллок (РАЗМЕР БУФЕРА);
printf ('СДЕЛАНО п');


// Записываем буфер в fOut
printf ('Запись данных в первый буфер:');
неправильныйзнак равнооткрытым('buffer1',O_RDONLY);
записывать(неправильный, &буфер,РАЗМЕР БУФЕРА);
близко(неправильный);
printf ('СДЕЛАНО п');

printf ('Копирование данных из первого файла во второй:');
конецзнак равнооткрытым('buffer1',O_RDONLY);
неправильныйзнак равнооткрытым('buffer2',O_RDONLY);
послать файл(неправильный,конец, 0,РАЗМЕР БУФЕРА);
близко(конец);
близко(неправильный);
printf ('СДЕЛАНО п');

printf ('Освобождение буфера:');
бесплатно (буфер);
printf ('СДЕЛАНО п');

printf ('Удаление файлов:');
разорвать связь('buffer1');
разорвать связь('buffer2');
printf ('СДЕЛАНО п');

возвращение 0;

}

Компиляция и запуск тестов 1 и 2

Для создания этих примеров вам потребуются инструменты разработки, установленные в вашем дистрибутиве. В Debian и Ubuntu это можно установить с помощью:

подходящийустановитьнеобходимое для сборки

Затем скомпилируйте с помощью:

gcctest1.c-илиtest1&& gcctest2.c-илиtest2

Чтобы запустить оба и проверить производительность, запустите:

время./test1&& время./test2

Вы должны получить такие результаты:

Тест ввода-вывода с традиционными функциями glibc.

Выделение буфера 64 МБ: ГОТОВО
Запись данных в первый буфер: ГОТОВО
Копирование данных из первого файла во второй: ГОТОВО
Освобождение буфера: ВЫПОЛНЕНО
Удаление файлов: ГОТОВО
реальный 0m0.397s
пользователь 0m0.000s
sys 0m0.203s
Тест ввода-вывода с помощью sendfile () и связанных системных вызовов.
Выделение буфера 64 МБ: ГОТОВО
Запись данных в первый буфер: ГОТОВО
Копирование данных из первого файла во второй: ГОТОВО
Освобождение буфера: ВЫПОЛНЕНО
Удаление файлов: ГОТОВО
реальный 0m0.019s
пользователь 0m0.000s
sys 0m0.016s

Как видите, код, использующий системные вызовы, работает намного быстрее, чем его эквивалент в glibc.

То, что нужно запомнить

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

При использовании некоторых системных вызовов вы должны позаботиться об использовании ресурсов, возвращаемых системными вызовами, а не библиотечными функциями. Например, структура FILE, используемая для функций glibc fopen (), fread (), fwrite () и fclose (), не совпадает с номером дескриптора файла из системного вызова open () (возвращается как целое число). Их смешивание может привести к проблемам.

В общем, системные вызовы Linux имеют меньше полос-заставок, чем функции glibc. Хотя верно, что системные вызовы содержат некоторую обработку ошибок и создание отчетов, вы получите более подробную функциональность от функции glibc.

И, наконец, несколько слов о безопасности. Системные вызовы напрямую взаимодействуют с ядром. Ядро Linux имеет обширную защиту от махинаций со стороны пользователя, но существуют неоткрытые ошибки. Не верьте, что системный вызов подтвердит ваш ввод или изолирует вас от проблем с безопасностью. Целесообразно обеспечить дезинфекцию данных, которые вы передаете системному вызову. Естественно, это хороший совет для любого вызова API, но нельзя быть осторожным при работе с ядром.

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