Библиотека сайта rus-linux.net
Файловая система /proc в Linux как инструмент разработчика
Оригинал: The Linux /proc Filesystem as a Programmers' ToolАвтор: Joshua Birnbaum
Дата публикации: 17 июня 2005 г.
Перевод: А.Панин
Дата перевода: 5 декабря 2012 г.
В статье описываются приемы, которые могут применяться для манипуляции различными типами информации, связанной с работой операционной системы, при помощи системных вызовов и команд для работы с файлами.
Одним из лучших типов времяпрепровождения для человека, активно интересующегося принципами работы операционных систем, является их всестороннее изучение. Установка и настройка систем позволяют закрепить знания о них, зачастую получаемые из статей по администрированию систем и работе с сетями. Источники информации в электронном виде, такие, как руководства пользователя (man pages) и информационные руководства (Linux info (1)) также легко доступны для ознакомления. Группы Google (groops.google.com), содержащие архив сообщений Usenet за более чем двадцатилетний срок, могут помочь в поиске мнения экспертов о широком спектре задач, начиная с настройки DNS и заканчивая расслоением памяти. Тем не менее, мне кажется уникальным другой подход к изучению операционных систем - разработка программного обеспечения для них.
Мое знакомство с системным программированием явилось результатом моего желания лучше понять принципы работы операционных систем, с которыми мне приходилось иметь дело ежедневно, работая по контракту на должности администратора UNIX-, а позднее и Linux-систем. Результатом этого знакомства стала утилита ifchk, представляющая из себя детектор сниффера пакетов, разработанная на языке C и выложенная для публичного ознакомления в июне 2003 года. Изначально утилита ifchk была разработана для IRIX, но затем было осуществлено портирование под Linux в основном для версии ядра 2.4. Текущей ревизией ifchk является недавно опубликованная beta 4, а beta 5 находится в разработке.
Моя работа над утилитой ifchk позволила мне ознакомиться с рядом аспектов, связанных с функционированием операционных систем. Примерами этих аспектов являются подсистемы netlink(7) и rtnetlink(7) в Linux, системный вызов ioctl(2), применяемый для управления устройствами и сетевыми интерфейсами, система сигналов и файловая система proc, предоставляющая информацию о процессах. Файловая система proc и ее возможность предоставлять широкий спектр информации о состоянии операционной системы является ключевым вопросом, рассматриваемым в рамках данной статьи.
Подробнее о файловой системе /proc
Перед тем, как начать рассматривать файловую систему proc с точки зрения разработчика, нам необходимо определиться с вопросом о том, что это за файловая система на самом деле. Файловая система proc является виртуальной файловой системой, корневая директория которой монтируется в директорию /proc, содержащей объекты, связанные с состоянием ядра и доступные для пользователей, а также, в качестве расширения, содержащая информацию о процессах, выполняющихся в системе. Слово "виртуальная" применяется по той причине, что данная файловая система существует только с целью отражения внутренних структур данных ядра, хранящихся в оперативной памяти. По этой причине большинство файлов и директорий в /proc имеют размер 0 байт.
dr-xr-xr-x 3 noorg noorg 0 Apr 16 23:24 19636
Директория 19636 соответствует процессу с идентификатором PID 19636, являющемуся текущей сессией командной оболочки bash. Эти директории содержат в себе как поддиректории, так и обычные файлы, которые в подробностях описывают атрибуты выполнения заданного процесса. Страница руководства proc(5) содержит подробное описание этих атрибутов.
К второй категории директорий и файлов в /proc относятся директории и файлы, имена которых не представлены в числовой форме и которые описывают ряд аспектов функционирования ядра. Примером может служить файл /proc/version, содержащий информацию о версии используемого образа ядра операционной системы.
Файлы, принадлежащие файловой системе proc, могут быть открыты либо только для чтения, либо для чтения и записи. Приведенный в качестве примера файл /proc/version может быть открыт только для чтения. Его содержимое может быть просмотрено при помощи утилиты cat(1) и остается неизменным в течение периода, пока система включена и доступна для пользователей. Тем не менее, файлы, доступные для открытия в режиме чтения и записи, позволяют просматривать и изменять свое содержимое, при этом изменение содержимого отражается на режиме работы ядра операционной системы. Одним из примеров является файл /proc/sys/net/ipv4/ip_forwarding. С помощью утилиты cat(1) можно выяснить, пересылает ли система IP-дейтаграммы между сетевыми интерфейсами - файл содержит 1 в случае включенной пересылки или 0 в противном случае. При помощи утилиты echo(1) можно записать значения 1 или 0 в этот файл, тем самым включая или отключая возможность пересылки пакетов без перекомпиляции ядра и перезагрузки. Этот прием работает для множества других файлов из файловой системы proc, поддерживающих возможность открытия в режиме чтения и записи.
Предлагаю читателям самостоятельно исследовать директорию /proc доступной системы и обратиться к странице руководства proc(5) для лучшего понимания вещей, описанных выше.
Разработка программ, взаимодействующих с файловой системой /proc
Основываясь на вышесказанном, начнем рассматривать вопрос о том, как программным путем при помощи файловой системы proc получить доступ к информации о текущем состоянии ядра. В качестве замечания следует сказать том, что несмотря на то, что в статье содержатся примеры исходного кода, для лучшего понимания вы можете загрузить исходный код ifchk и скомпилировать его, после чего исследовать программу при помощи отладчика gdb(1).
Одна из функций утилиты ifchk заключается в отображении счетчиков принятых и отправленных пакетов для всех сетевых интерфейсов системы. В ifchk эта функция реализована путем исследования файла /proc/net/dev и последующей обработки данных, полученных из него. Хотя этот файл и содержит множество статистических данных, относящихся к сетевым интерфейсам, нас в первую очередь интересует получение данных о количестве пакетов, прошедших через интерфейс.
Функция ifMetric() реализует эту задачу. Код этой функции приведен в статье ниже. Также функция ifMetric() находится в файле исходного кода ~/ifchk-0.95b4/linux.c из комплекта поставки утилиты ifchk.
int ifMetric( struct ifList *list );
struct ifList { int flags; /* Флаги интерфейса. */ char data[DATASZ]; /* Имя интерфейса/номер, например, "eth0". */ struct ifList *next; /*Указатель на следующую структуру типа ifList. */ };
После начала работы нашим первым желанием могло бы быть открытие файла /proc/net/dev и поиск данных, относящихся к количеству пакетов в нем. Тем не менее, между этими двумя шагами необходимо провести мероприятия по изучению содержимого файла. Крах программ из-за некорректных исходных данных, сформированных злоумышленниками, в настоящее время является типичной проблемой безопасности. Мы не можем делать никаких предположений о данных, предоставляемых извне только на основании их источника. Как мы увидим, существует еще одна причина, связанная с безопасностью работы приложения, по которой мы выстраиваем последовательность операций по доступу к файлу таким образом, как представлено ниже. Помня обо всем вышесказанном, приступим к работе.
540 int ifMetric( struct ifList *list ) 541 { 542 struct stat fileAttrs; /* Атрибуты файла /proc/net/dev. */ 543 struct stat linkAttrs; /* Атрибуты ссылки /proc/net/dev. */ 544 char *filePath = NULL; /* Путь до файла /proc/net/dev. */ 545 FILE *file = NULL; /* Указатель на файл для вызова fopen(). */ 546 int status = 0; /* Результат закрытия файла /proc/net/dev при помощи close(). */ 547 char buf[1024] = ""; /* Буфер символов. */ 548 struct ifList *cur = NULL; /* Текущий элемент связанного списка. */ 549 int conv = 0; /* Количество совпадений при вызове sscanf(). */ 550 long long rxPackets = 0; /* Количество принятых пакетов. */ 551 long long txPackets = 0; /* Количество отправленных пакетов. */ 552 char *delim = NULL; /* Указатель на символ, найденный в строке. */ 553 unsigned int ifIndex = 0; /* Идентификатор интерфейса. */ 554 mode_t mode = 0; /* Права доступа к файлу /proc/net/dev. */ 558 cur = list; 559 if( cur -> data == NULL ) 560 { 561 fprintf( stderr, "ifchk: ERROR: interface list is empty\n" ); 562 return (-1); 563 } 564 filePath = "/proc/net/dev"; 565 if( ( lstat( filePath, &linkAttrs ) ) != 0 ) 566 { 567 perror( "lstat" ); 568 return (-1); 569 } 570 if( S_ISLNK( linkAttrs.st_mode ) ) 571 { 572 fprintf( stderr, "ifchk: ERROR: /proc/net/dev is a symbolic link\n" ); 573 return (-1); 574 }
Перед тем, как открыть файл /proc/net/dev, мы должны убедиться, что этот файл не является на самом деле символьной ссылкой (symlink). В том случае, если вместо файла программе передается символьная ссылка, которой рассматриваемый файл не должен являться, функция fopen(3) проследует по ней и откроет тот файл, на который она указывает. Следующий вызов fstat(2), использующий файловый дескриптор, возвращенный fopen(3), возвратит информацию, не относящуюся к необходимому нам файлу. Для защиты от таких ситуаций, мы используем вызов lstat(2) в отношении файла /prc/net/dev и затем проверяем, является ли файл символьной ссылкой при помощи макроса POSIX S_ISLINK. Если S_ISLINK определяет ссылку, выводим сообщение об ошибке с помощью вызова fprintf(3) и возвращаем значение -1, указывающее на ошибку. В противном случае выполнение функции продолжается.
578 if( ( file = fopen( "/proc/net/dev", "r" ) ) == NULL ) 579 { 580 perror( "fopen" ); 581 return (-1); 582 }
Предполагая, что с помощью макроса S_ISLINK не установлено наличие ссылки, мы открываем файл /proc/net/dev при помощи вызова fopen(3). Если вызов завершается успешно, fopen(3) возвращает указатель на структуру типа FILE для доступа открытому файлу /proc/net/dev. Если вызов завершается неудачно, мы вызываем perror(3) и возвращаем значение -1, указывающее на ошибку. Заметьте, что мы проверяем значение, возвращаемое функцией fopen(3). Напоследок отметим, что проверка значений, возвращаемых функциями обязательна.
586 if( ( fstat( fileno( file ), &fileAttrs ) ) != 0 ) 587 { 588 perror( "fstat" ); 589 return (-1); 590 }
Одной из первых вещей, о которых я думаю во время разработки кода, является то, какие варианты его некорректной работы возможны. Дополнительно я думаю том, как минимизировать ущерб от его некорректной работы путем использования приемов безопасного программирования. Эти действия необходимо предпринимать, особенно ввиду повышенных требований к безопасности системы и возможного ущерба от действий злоумышленников по компрометации системы.
тип файла: regular размер в байтах: 0 размер в блоках: 0 владелец: root:root права доступа: 0444 (-r--r--r--)
Если говорить о стандартной сборке ifchk, то файл /proc/net/dev должен соответствовать вышеприведенным критериям атрибутов. Тем не менее, простейшее изменение кода ifchk в случае необходимости позволяет подстроиться под изменившиеся обстоятельства.
(gdb) print fileAttrs $1 = {..., st_mode = 33060, st_uid = 0, st_gid = 0, st_size = 0, st_blocks = 0, ...}
Выше я упоминал о дополнительной причине, связанной с безопасностью, по которой была сформирована последовательность действий по доступу к файлу /proc/net/dev. Вспомним, что в ходе открытия файла мы использовали вызов lstat(2) для того, чтобы не следовать по символьным ссылкам, затем последовательность состояла из вызова fopen(3) и следующего за ним вызова fstat(2). Мы могли бы получить идентичные результаты просто использовав вызов stat(2) вместо fstat(2), после чего вызвав fopen(3), так как stat(2) возвращает информацию об атрибутах файла, идентичную той, которую возвращает вызов fstat(2). Так для чего же использовать первую последовательность вызовов - fopen(3), fstat(2) вместо второй - stat(2), fopen(3)? Это делается для того, чтобы избежать ситуации гонки (race condition). Последовательность вызовов stat(2), fopen(3) делает возможной ситуацию при которой файл, в отношении которого был использован вызов stat(2) может быть подменен на другой файл, возможно с другими атрибутами или другим содержанием до того, как будет выполнен вызов fopen(3). В этой ситуации нам кажется, что мы используем вызов fopen(3) по отношению к тому же файлу, что и вызов stat(2), но на самом деле это не соответствует действительности. Опасность такой ситуации, я думаю, очевидна.
591 if( ( ( linkAttrs.st_ino ) != ( fileAttrs.st_ino ) ) || 592 ( ( linkAttrs.st_dev ) != ( fileAttrs.st_dev ) ) ) 593 { 594 fprintf( stderr, "ifchk: ERROR: /proc/net/dev file attribute inconsistency\n" ); 595 return (-1); 596 }
В качестве дополнительной меры для проверки того, что работа происходит с одним и тем же файлом из файловой системы proc, мы сравниваем номера структур inode на основании поля st_ino и идентификаторы файловых систем на основании поля st_dev, а поля в свою очередь берутся из структур, возвращаемых вызовами lstat(2) и fstat(2), применяемыми для получения атрибутов файла /proc/net/dev. Если файл не подменен, значение linkAttrs.st_ino должно быть равно значению fileAttrs.st_ino и значение linkAttrs.st_dev должно быть равно значению fileAttrs.st_dev. В случае выполнения условия, выполнение функции продолжается. В случае различий атрибутов, выводится сообщение об ошибке при помои вызова fprintf(3) и возвращается значение -1, указывающее на ошибку.
600 if( ! ( S_ISREG( fileAttrs.st_mode ) ) ) 601 { 602 fprintf( stderr, "ifchk: ERROR: /proc/net/dev is not a regular file\n" ); 603 return (-1); 604 }
Макрос S_ISREG является стандартным макросом POSIX, предназначенным для проверки, является ли аргумент обычным файлом или не является файлом, а, например, является директорией. Если мы имеем дело с обычным файлом, выполнение функции продолжается. В противном случае выводится сообщение об ошибке при помощи вызова fprintf(3) и возвращается значение -1, указывающее на ошибку. На этом этапе может возникнуть вопрос о том, для чего нужно было использовать макрос S_ISLINK после вызова lstat(2) для установления того, что файл не является символьной ссылкой, если сейчас также проверяется тип файла. Обращение к описанию проверки на наличие символьной ссылки выше должно помочь с ответом на этот вопрос.
608 if( ( ( fileAttrs.st_size ) || ( fileAttrs.st_blocks ) ) != 0 ) 609 { 610 fprintf( stderr, "ifchk: ERROR: /proc/net/dev file size is greater than 0\n" ); 611 return (-1); 612 }
Равен ли размер файла нулю байтам и занимает ли он 0 блоков на диске? Если оба параметра равны нулю, выполнение функции продолжается. В противном случае выводится сообщение об ошибке при помощи вызова fprintf(3) и возвращается значение -1, указывающее на ошибку. Стоит отметить, что только одного невыполненного условия из двух достаточно для завершения программы с ошибкой.
616 if( ( ( fileAttrs.st_uid ) || ( fileAttrs.st_gid ) ) != 0 ) 617 { 618 fprintf( stderr, "ifchk: ERROR: /proc/net/dev is not owned by UID 0, GID 0\n" ); 619 return (-1); 620 }
Является ли владельцем файла /proc/net/dev пользователь root и принадлежит ли он группе root? В этом случае также достаточно одного невыполненного условия для возвращения значения -1, указывающего на ошибку.
624 if( ( mode = fileAttrs.st_mode & ALLPERMS ) != MODEMASK ) 625 { 626 fprintf( stderr, "ifchk: ERROR: /proc/net/dev permissions are not mode 0444\n" ); 627 return (-1); 628 }
Соответствуют ли права доступа к файлу /proc/net/dev значению 0444 - доступ только на чтение для владельца, участников группы и остальных пользователей? Макрос ALLPERMS описывается в заголовочном файле /usr/include/sys/stat.h и задает маску всех возможных прав доступа к файлу, или 07777. Макрос MODEMASK описан в файле ~/ifchk-0.95b4/linux.h и задает маску прав доступа только для чтения для владельца файла, участников группы и остальных пользователей, или 0444.
После применения битовой операции "И" к значениям fileAttrs.st_mode и ALLPERMS и последующего сравнения результата с значением MODEMASK, можно установить задаются ли права доступа к файлу /proc/net/dev значением 0444. Если это так, продолжаем выполнение функции. В противном случае, выводим сообщение об ошибке при помощи вызова fprintf(3) и возвращаем значение -1, указывающее на ошибку. На этом проверка атрибутов файла /prc/net/dev завершается. Тем не менее, перед тем, как ifchk сможет работать с содержимым этого файла, необходимо исследовать его содержимое.
Проверка соответствия содержимого файла /proc/net/dev стандартному формату необходима, поэтому сформулируем критерий, по которому ifchk принимает решение том, использовать ли файл или отклонить. В процессе развития Linux-систем, количество полей, таких, как bytes, packets, errs изменялось. Все файлы /proc/net/dev, которые я видел, имеют структуру, идентичную представленной ниже; вывод справа урезан ввиду ограниченного места на странице.
Inter-| Receive | Transmit ... face |bytes packets errs drop fifo frame compressed multicast|bytes ... lo: 34230 586 0 0 0 0 0 0 34230 ... eth0:22476180 208548 0 0 0 0 0 0 52718375 ...
После двух строк заголовка следуют данные статистики для каждого интерфейса. В более ранних версиях систем нет поля "compressed". Для простоты я решил, что файл /proc/net/dev, не содержащий этого поля, должен быть отклонен утилитой ifchk. Обладая этими знаниями, начнем рассматривать процесс программного извлечения данных из файла /proc/net/dev.
632 if( ! fgets(buf, sizeof( buf ), file) ) 633 { 634 perror( "fgets" ); 635 return (-1); 636 } 637 if( ! fgets(buf, sizeof( buf ), file) ) 638 { 639 perror( "fgets" ); 640 return (-1); 641 } 645 if( ( strstr( buf, "compressed" ) ) == NULL ) 646 { 647 fprintf( stderr, "ifchk: ERROR: /proc/net/dev header format is not supported\n" ); 648 return (-1); 649 }
Мы использовали два идентичных вызова fgets(3) для чтения первой и второй строк заголовка из файла /proc/net/dev. Каждый вызов fgets(3) приводит к перезаписи содержимого буфера buf. В результате buf будет содержать вторую строку заголовка. После этого мы проверяем, содержит ли вторая строка заголовка поле "compressed".
Если поле "compressed" обнаружено, вызов strstr(3) завершается успешно и мы имеем пригодный для получения информации файл. Если же поле "compressed" не обнаружено в буфере buf, содержащем вторую строку заголовка, выводим сообщение об ошибке при помощи вызова fprintf(3) и возвращаем значение -1, указывающее на ошибку. После этого все проверки файла, начатые с проверки атрибутов подходят к своему завершению.
Следующая часть кода представляет собой цикл while, в котором обрабатываются и выводятся данные для каждого из сетевых интерфейсов. Цикл выполняется столько раз, сколько активных интерфейсов в системе.
653 printf( "*** Network Interface Metrics ***\n" ); 654 printf( "Name Index RX-OK TX-OK\n" ); 659 while( fgets( buf, sizeof( buf ), file ) ) 660 { 664 if( ( strstr( buf, cur -> data ) ) != NULL ) 665 { 666 delim = strchr( buf, ':' ); 670 if( *( delim + 1 ) == ' ' ) 671 { 672 conv = sscanf( buf, 673 "%*s %*Lu %Lu %*lu %*lu %*lu %*lu %*lu %*lu %*Lu%Lu %*lu %*lu %*lu %*lu %*lu %*lu", 674 &rxPackets, &txPackets ); 675 } 676 else 677 { 678 conv = sscanf( buf, 679 "%*s %Lu %*lu %*lu %*lu %*lu %*lu %*lu %*Lu %Lu%*lu %*lu %*lu %*lu %*lu %*lu", 680 &rxPackets, &txPackets ); 681 } 682 }
Мы вызываем fgets(3) для чтения следующей строки из файла /proc/net/dev в буфер buf. После этого вызывается strstr(3) для проверки того, что имя интерфейса в cur->data совпадает с именем интерфейса в строке только что считанной из файла /proc/net/dev. Строки статистики для интерфейсов в файле /proc/net/dev начинаются с имени интерфейса, за которым следует двоеточие, после которого записано количество байт, принятое данным интерфейсом. В некоторых случаях между двоеточием и числом принятых байт ставится пробел, например, eth0: 6571407, в некоторых случаях пробела нет, например, eth0:12795779.
Для обработки строки в обоих случаях используется действие с указателем, целью которого является установление факта наличия или отсутствия пробела. Если пробел существует, выполняется условие оператора if () в строке 670, при этом параметр, задающий формат строки для вызова sscanf(3) учитывает наличие пробела. Если пробела не обнаружено, выполняется блок кода со строки 676. В этом случае параметр, задающий формат строки учитывает отсутствие пробела.
В обоих случаях количество принятых и отправленных пакетов копируется при помощи вызова sscanf(3) из строки файла /proc/net/dev в переменные rxPackets и txPackets для последующего вывода.
683 else 684 { 685 fprintf( stderr, "ifchk: ERROR: current metrics do not describe current interface %s\n", 686 cur -> data ); 687 return (-1); 688 }
Если при сравнении имени интерфейса в переменной cur->data и в строке файла /proc/net/dev обнаруживается несоответствие, выводится сообщение об ошибке при помощи вызова fprintf(3) и возвращается значение -1, указывающее на ошибку.
692 if( conv != 2 ) 693 { 694 fprintf( stderr, "ifchk: ERROR: /proc/net/dev parse error\n" ); 695 return (-1); 696 }
В случае успешного выполнения, вызов sscanf(3) возвращает количество найденных элементов строки. В результате значение переменной conv должно быть равно двум для rxPackets и txPackets. Если это не так, выводим сообщение с помощью fprintf(3) и возвращаем значение -1, указывающее на ошибку.
697 if( ( ifIndex = if_nametoindex( cur -> data ) ) == 0 ) 698 { 699 perror( "if_nametoindex" ); 700 return (-1); 701 }
Далее мы используем функцию if_namtoindex(), в качестве аргумента передавая ей имя интерфейса из переменной cur->data и, в случае успеха, сохраняем целочисленный идентификатор интерфейса в переменной ifIndex. Если возникает ошибка, обрабатываем ее как обычно. Идентификатор интерфейса является положительным целым числом, которое ядро ставит в соответствие каждому интерфейсу в системе.
702 printf( "%-7s %-7d %-13Lu %-13Lu\n", cur -> data, ifIndex, rxPackets, txPackets ); 703 704 conv = 0; 705 if( cur -> next != NULL ) 706 { 707 cur = cur -> next; 708 } 709 }
Собрав данные для интерфейса, выводим строку. После этого возвращаемся к началу цикла и выполнение продолжается, либо в том случае, когда условие завершения цикла выполняется, осуществляем выход из него.
713 if( ( status = fclose( file ) != 0 ) ) 714 { 715 perror( "fclose" ); 716 return (-1); 717 } 721 if( ( writeLog( LOGINFO, pw -> pw_name, NULLSTATE ) ) != 0 ) 722 { 723 fprintf( stderr, "ifchk: ERROR: could not pass logging message to syslogd\n" ); 724 return (-1); 725 } 726 return (0); 727 }
После завершения цикла вызывается fclose(3), этот вызов закрывает файл, открытый с помощью вызова fopen(3) в строке 578. Затем используется функция для записи в системный журнал информации о том, что были получены данные о состоянии сетевых интерфейсов.
После выполнения все этих действий, ifchk выводит данные о количестве переданных пакетов в системе с двумя интерфейсами в следующей форме:
*** Network Interface Metrics *** Name Index RX-OK TX-OK lo 1 104 104 eth0 3 1280903 1162571<--. ^ ^ ^ | | | |[из файла/proc/net/dev]-' | | | |[результат вызова функции if_nametoindex()] | |[из переменной cur -> data]
Заключение
Файловая система proc предоставляет огромное количество информации о системе всем ее использующим. Возможность манипулирования всеми типами информации о текущем состоянии системы с использованием системных вызовов и команд для работы с файлами, таких, как cat(1) и echo(1), делает эту файловую систему кандидатом номер один на включение в список инструментов для обслуживания Linux-систем каждого специалиста.