Программирование на C

Читать Syscall Linux

Читать Syscall Linux
Значит, вам нужно читать двоичные данные? Вы можете читать из FIFO или сокета? Видите ли, вы можете использовать функцию стандартной библиотеки C, но при этом вы не получите преимуществ от специальных функций, предоставляемых ядром Linux и POSIX. Например, вы можете использовать таймауты для чтения в определенное время, не прибегая к опросу. Кроме того, вам может потребоваться что-то прочитать, не беспокоясь о том, что это специальный файл, сокет или что-то еще. Ваша единственная задача - прочитать какое-то двоичное содержимое и получить его в своем приложении. Вот где сияет системный вызов чтения.

Прочтите обычный файл с помощью системного вызова Linux

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

Во-первых, ключевое отличие от стандартной библиотеки: отсутствует буферизация. Каждый раз, когда вы вызываете функцию чтения, вы вызываете ядро ​​Linux, и это займет время - это почти мгновенно, если вы вызываете его один раз, но может замедлить вас, если вы вызываете его тысячи раз в секунду. Для сравнения, стандартная библиотека будет буферизовать ввод для вас. Поэтому всякий раз, когда вы вызываете чтение, вы должны читать не несколько байтов, а большой буфер, например несколько килобайт - за исключением случаев, когда вам действительно нужно несколько байтов, например, если вы проверяете, существует ли файл и не пуст.

Однако это имеет преимущество: каждый раз, когда вы вызываете read, вы уверены, что получаете обновленные данные, если какое-либо другое приложение изменяет в данный момент файл. Это особенно полезно для специальных файлов, таких как файлы в / proc или / sys.

Пора показать вам на реальном примере. Эта программа на C проверяет, является ли файл PNG или нет. Для этого он читает файл, указанный в пути, который вы указываете в аргументе командной строки, и проверяет, соответствуют ли первые 8 байтов заголовку PNG.

Вот код:

#включать
#включать
#включать
#включать
#включать
#включать
#включать
 
typedef enum
IS_PNG,
СЛИШКОМ КОРОТКИЙ,
INVALID_HEADER
pngStatus_t;
 
unsigned int isSyscallSuccessful (const ssize_t readStatus)
вернуть readStatus> = 0;
 

 
/ *
* checkPngHeader проверяет, соответствует ли массив pngFileHeader PNG
* заголовок файла.
*
* В настоящее время он проверяет только первые 8 байтов массива. Если массив меньше
* чем 8 байт, возвращается TOO_SHORT.
*
* pngFileHeaderLength должен содержать значение массива tye. Любое недопустимое значение
* может привести к неопределенному поведению, например, к сбою приложения.
*
* Возвращает IS_PNG, если он соответствует заголовку файла PNG. Если есть хотя бы
* 8 байт в массиве, но это не заголовок PNG, возвращается INVALID_HEADER.
*
* /
pngStatus_t checkPngHeader (const unsigned char * const pngFileHeader,
size_t pngFileHeaderLength) const unsigned char ожидаемыйPngHeader [8] =
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A;
int я = 0;
 
если (pngFileHeaderLength < sizeof(expectedPngHeader))
вернуть TOO_SHORT;
 

 
для (i = 0; i < sizeof(expectedPngHeader); i++)
если (pngFileHeader [i] != expectedPngHeader [i])
вернуть INVALID_HEADER;
 


 
/ * Если он достигает этого места, все первые 8 байтов соответствуют заголовку PNG. * /
вернуть IS_PNG;

 
int main (int argumentLength, char * argumentList [])
char * pngFileName = NULL;
беззнаковый символ pngFileHeader [8] = 0;
 
ssize_t readStatus = 0;
/ * Linux использует номер для идентификации открытого файла. * /
int pngFile = 0;
pngStatus_t pngCheckResult;
 
если (аргументДлина != 2)
fputs ("Вы должны вызывать эту программу, используя isPng ваше имя файла.\ n ", stderr);
вернуть EXIT_FAILURE;
 

 
pngFileName = список аргументов [1];
pngFile = open (pngFileName, O_RDONLY);
 
if (pngFile == -1)
perror («Не удалось открыть предоставленный файл»);
вернуть EXIT_FAILURE;
 

 
/ * Считываем несколько байтов, чтобы определить, является ли файл PNG. * /
readStatus = чтение (pngFile, pngFileHeader, sizeof (pngFileHeader));
 
if (isSyscallSuccessful (readStatus))
/ * Проверяем, является ли файл PNG, поскольку он получил данные. * /
pngCheckResult = checkPngHeader (pngFileHeader, readStatus);
 
if (pngCheckResult == TOO_SHORT)
printf ("Файл% s не в формате PNG: он слишком короткий.\ n ", pngFileName);
 
else if (pngCheckResult == IS_PNG)
printf ("Файл% s - это файл PNG!\ n ", pngFileName);
 
еще
printf ("Файл% s не в формате PNG.\ n ", pngFileName);
 

 
еще
perror («Не удалось прочитать файл»);
вернуть EXIT_FAILURE;
 

 
/ * Закрываем файл… * /
if (close (pngFile) == -1)
perror («Не удалось закрыть предоставленный файл»);
вернуть EXIT_FAILURE;
 

 
pngFile = 0;
 
вернуть EXIT_SUCCESS;
 

Смотрите, это полноценный, рабочий и компилируемый пример. Не сомневайтесь, скомпилируйте сами и протестируйте, это действительно работает. Вы должны вызвать программу из терминала следующим образом:

./ isPng ваше имя файла

Теперь давайте сосредоточимся на самом вызове чтения:

pngFile = open (pngFileName, O_RDONLY);
if (pngFile == -1)
perror («Не удалось открыть предоставленный файл»);
вернуть EXIT_FAILURE;

/ * Считываем несколько байтов, чтобы определить, является ли файл PNG. * /
readStatus = чтение (pngFile, pngFileHeader, sizeof (pngFileHeader));

Сигнатура чтения следующая (извлечена из справочных страниц Linux):

ssize_t read (int fd, void * buf, size_t count);

Во-первых, аргумент fd представляет дескриптор файла. Я немного объяснил эту концепцию в своей статье о вилке.  Дескриптор файла - это int, представляющий открытый файл, сокет, канал, FIFO, устройство, ну, это много вещей, где данные могут быть прочитаны или записаны, как правило, в виде потока. Я расскажу об этом подробнее в следующей статье.

Функция open - это один из способов сказать Linux: я хочу делать что-то с файлом по этому пути, найдите его там, где он находится, и дайте мне доступ к нему. Он вернет вам этот int, называемый дескриптором файла, и теперь, если вы хотите что-то сделать с этим файлом, используйте этот номер. Не забудьте вызвать close, когда закончите с файлом, как в примере.

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

Возвращаемое значение имеет тип ssize_t. Странный тип, не правда ли? Это означает «подписанный size_t», в основном это длинное целое число. Он возвращает количество байтов, которые он успешно прочитал, или -1, если есть проблема. Вы можете найти точную причину проблемы в глобальной переменной errno, созданной Linux, определенной в . Но для вывода сообщения об ошибке лучше использовать perror, поскольку он печатает errno от вашего имени.

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

Теперь чтение полезно не только для обычных файлов, и если вы хотите почувствовать его сверхспособности - Да, я знаю, что этого нет ни в одном комиксе Marvel, но у него есть настоящие силы - вы захотите использовать его с другими потоками, такими как трубы или сокеты. Давайте посмотрим на это:

Специальные файлы Linux и системный вызов чтения

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

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

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

Если чтение работает так же, как и с файлами, оно будет ждать, пока пользователь наберет 256 символов, прежде чем вернуться! Ваш пользователь будет ждать вечно, а затем, к сожалению, убьет ваше приложение. Это определенно не то, что вам нужно, и у вас возникнут большие проблемы.

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

Но разработчики Linux думали иначе, чтобы избежать этой проблемы:

  • Когда вы читаете обычные файлы, он пытается как можно больше прочитать количество байтов и будет активно получать байты с диска, если это необходимо.
  • Для всех других типов файлов он вернет как только есть некоторые данные и в большинстве подсчитать байты:
    1. Для терминалов это в целом когда пользователь нажимает клавишу Enter.
    2. Для сокетов TCP: как только ваш компьютер что-то получает, не имеет значения, сколько байтов он получает.
    3. Для FIFO или каналов это, как правило, такое же количество, как и в другом приложении, но ядро ​​Linux может поставлять меньше за раз, если это более удобно.

Таким образом, вы можете безопасно звонить со своим буфером 2 КБ, не оставаясь заблокированным навсегда. Обратите внимание, что он также может быть прерван, если приложение получает сигнал. Поскольку чтение из всех этих источников может занять секунды или даже часы - пока другая сторона не решит писать, в конце концов - прерывание сигналами позволяет перестать оставаться заблокированным слишком долго.

Однако у этого также есть недостаток: если вы хотите точно прочитать 2 КиБ с этими специальными файлами, вам нужно будет проверить возвращаемое значение чтения и вызвать чтение несколько раз. чтение редко заполняет весь буфер. Если ваше приложение использует сигналы, вам также необходимо проверить, не удалось ли чтение с -1, потому что оно было прервано сигналом, используя errno.

Позвольте мне показать вам, как может быть интересно использовать это особое свойство чтения:

#define _POSIX_C_SOURCE 1 / * sigaction недоступен без этого #define. * /
#включать
#включать
#включать
#включать
#включать
#включать
/ *
* isSignal сообщает, был ли системный вызов чтения прерван сигналом.
*
* Возвращает ИСТИНА, если системный вызов чтения был прерван сигналом.
*
* Глобальные переменные: читает errno, определенную в errno.час
* /
unsigned int isSignal (const ssize_t readStatus)
return (readStatus == -1 && errno == EINTR);

unsigned int isSyscallSuccessful (const ssize_t readStatus)
вернуть readStatus> = 0;

/ *
* shouldRestartRead сообщает, когда системный вызов чтения был прерван
* сигнальное событие или нет, и, учитывая, что эта причина "ошибки" временная, мы можем
* безопасно перезапустить вызов чтения.
*
* В настоящее время он проверяет только то, было ли чтение прервано сигналом, но
* можно улучшить, чтобы проверить, было ли прочитано целевое количество байтов и
* не тот случай, верните ИСТИНА для повторного чтения.
*
* /
unsigned int shouldRestartRead (const ssize_t readStatus)
return isSignal (readStatus);

/ *
* Нам нужен пустой обработчик, так как системный вызов чтения будет прерван только в том случае, если
* сигнал обрабатывается.
* /
void emptyHandler (int игнорируется)
возвращаться;

int main ()
/ * В секундах. * /
const int alarmInterval = 5;
const struct sigaction emptySigaction = emptyHandler;
char lineBuf [256] = 0;
ssize_t readStatus = 0;
беззнаковое int waitTime = 0;
/ * Не изменяйте sigaction, кроме случаев, когда вы точно знаете, что делаете. * /
sigaction (SIGALRM, & emptySigaction, NULL);
будильник (alarmInterval);
fputs ("Ваш текст: \ n", stderr);
делать
/ * Не забывайте '\ 0' * /
readStatus = чтение (STDIN_FILENO, lineBuf, sizeof (lineBuf) - 1);
if (isSignal (readStatus))
waitTime + = alarmInterval;
будильник (alarmInterval);
fprintf (stderr, «% u секунд бездействия… \ n», waitTime);

while (shouldRestartRead (readStatus));
if (isSyscallSuccessful (readStatus))
/ * Завершаем строку, чтобы избежать ошибки при ее передаче в fprintf. * /
lineBuf [readStatus] = '\ 0';
fprintf (stderr, "Вы набрали% lu chars. Вот ваша строка: \ n% s \ n ", strlen (lineBuf),
lineBuf);
еще
perror ("Не удалось прочитать со стандартного ввода");
вернуть EXIT_FAILURE;

вернуть EXIT_SUCCESS;

Еще раз, это полное приложение C, которое вы можете скомпилировать и запустить.

Он делает следующее: читает строку из стандартного ввода. Однако каждые 5 секунд он печатает строку, сообщающую пользователю, что ввод еще не был введен.

Пример, если я жду 23 секунды, прежде чем набирать «Пингвин»:

$ alarm_read
Твой текст:
5 секунд бездействия…
10 секунд бездействия…
15 секунд бездействия…
20 секунд бездействия…
Пингвин
Вы набрали 8 символов. Вот ваша строка:
Пингвин

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

Таким образом, преимущества перевешивают недостатки, описанные выше. Если вам интересно, следует ли поддерживать специальные файлы в приложении, обычно работающем с обычными файлами - и так зовут читать в петле - Я бы сказал, сделайте это, за исключением случаев, когда вы торопитесь, мой личный опыт часто доказывал, что замена файла каналом или FIFO может буквально сделать приложение намного более полезным с небольшими усилиями. В Интернете есть даже готовые функции C, которые реализуют этот цикл за вас: он называется функциями чтения.

Заключение

Как видите, fread и read могут выглядеть одинаково, но это не так. И с небольшими изменениями в том, как чтение работает для разработчика C, чтение намного интереснее для разработки новых решений проблем, с которыми вы сталкиваетесь во время разработки приложений.

В следующий раз я расскажу вам, как работает системный вызов write, так как чтение - это круто, но возможность делать и то, и другое намного лучше. А пока поэкспериментируйте с прочтением, познакомьтесь с ним и желаю вам счастливого Нового года!

Как использовать Xdotool для стимулирования щелчков мыши и нажатия клавиш в Linux
Xdotool - это бесплатный инструмент командной строки с открытым исходным кодом для имитации щелчков мыши и нажатия клавиш. В этой статье будет краткое...
5 лучших эргономичных компьютерных мышей для Linux
Вызывает ли длительное использование компьютера боль в запястье или пальцах?? Вы страдаете от скованности суставов и постоянно должны пожимать руки? В...
Как изменить настройки мыши и сенсорной панели с помощью Xinput в Linux
Большинство дистрибутивов Linux по умолчанию поставляются с библиотекой libinput для обработки событий ввода в системе. Он может обрабатывать события ...