Perl для системного администрирования

         

Прогулка по файловой системе


Наверняка вам уже не терпится посмотреть на какие-нибудь приложения, написанные на Perl. Начнем мы с «прогулки по файловой системе» - одной из наиболее часто встречающихся задач системного администрирования, связанных с файловыми системами. Обычно этот процесс состоит в поиске по всем деревьям каталогов и выполнении некоторого действия в зависимости от результатов поиска. Для этой задачи в каждой операционной системе есть свое средство. В Unix это команда find, в Windows NT/2000 - Find Files or Folders или

Search For Files or Folders, а в MacOS - Find File или Sherlock. Все эти команды полезны для поиска, но выполнять произвольные сложные действия, по мере нахождения требуемых файлов, они не способны. Мы увидим, каким образом Perl позволяет писать более изысканные обзорные программы, начав с простейших и наращивая сложность по мере продвижения.

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

Мы не можем добавить новое дисковое пространство, не имея на это денег, поэтому попытаемся рациональнее использовать существующие ресурсы. Первый наш шаг - удалить из файловой системы все файлы, которые можно удалить. В Unix первые кандидаты на удаление - это core-файлы, которые остаются после аварийных завершений программ. Большинство пользователей либо не замечают, что эти файлы создаются, либо просто игнорируют их, оставляя напрасно занятыми большие дисковые пространства. Нам нужно средство для поиска и удаления таких «пустышек» из файловой системы.

Обзор файловой системы мы начинаем с чтения содержимого какого-либо каталога и затем продолжаем обход с этой точки. Давайте немного упростим задачу и начнем с программы, которая изучает содержимое текущего каталога и сообщает, были ли найдены в нем core-файлы и другие каталоги-кандидаты для поиска.




Начнем мы с того, что откроем каталог, используя примерно тот же синтаксис, что и для открытия файла. Если попытка была неудачной, мы выходим из программы и выводим сообщение об ошибке ($! ), установленное вызовом opendir( ):

opendir(DIF), ". ") or die "He могу открыть текущий каталог: $'\л';

Мы получаем дескриптор каталога, в нашем случае DIR, который можно передать функции readdir( ), чтобы получить список файлов и каталогов в текущем каталоге. Если readdir() не может прочитать содержимое каталога, выводится сообщение об ошибке (которое объясняет, почему попытка была неудачной) и программа завершает работу:

# читаем имена файлов/каталогов данного каталога в @names

@names = readdir(DIR) or die "Невозможно прочитать текущий каталог:$! \n";

Затем закрываем открытый дескриптор каталога:

closedir(DIR);

Теперь можно работать с полученными именами:

foreach $name (@names) {

next if ($name eq "."); # пропускаем текущий каталог

next if ($name eq ".."); # пропускаем родительский каталог

if (-d $name){ # является ли каталогом?





print "найден каталог: $name\n"; next: # можно перейти к следующему имени

# из цикла for

}

if ($name eq "core") { # это файл с именем core?

print "найден! \n";

}

}

Теперь у нас есть очень простая программа, которая проверяет содержимое одного каталога. Она не только не обходит файловую систему, но даже не «проползает» по ней. Чтобы пройти по файловой системе, мы должны зайти во все каталоги, которые нам встретятся, и посмотреть на их содержимое. Если в этих подкаталогах есть еще подкаталоги, в них мы тоже должны заглянуть.

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



Именно с этим мы сталкиваемся и в нашем примере: мы собираемся просмотреть каталог, все его подкаталоги, все их подкаталоги и т. д.

Если вы никогда раньше не видели рекурсивного кода (т. е. кода, который вызывает себя), на первый взгляд он вам может показаться несколько странным. Рекурсивный код чем-то напоминает процесс раскраски матрешек. Там тоже внутри одной куклы находится другая кукла такой же формы, но меньшего размера, внутри которой есть еще одна кукла, и т. д. до самой маленькой куклы в центре.

Инструкция по раскраске матрешек могла бы выглядеть так:

  • Изучите куклу, которая находится перед вами. Внутри у нее есть кукла меньшего размера? Если да, то вытащите ее оттуда.


  • Повторяйте шаг 1 с вытащенными куклами до тех пор, пока не дойдете до центра.


  • Раскрасьте центральную куклу. Когда она высохнет, поместите ее во внешнюю по отношению к ней куклу и повторите шаг 3 со следующим контейнером.


  • Этот процесс одинаков на каждом шаге. Если у предмета, который вы держите в руках, есть «подпредметы», отложите предмет в сторону и займитесь сначала «подпредметом». Если же подпредметов у него нет, произведите с ним необходимые действия и займитесь последним отложенным предметом.

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

    Давайте рассмотрим примеры кода с рекурсией. Чтобы добавить рекурсию в наш код, мы сначала перенесем операцию сканирования каталога и действия над его содержимым в отдельную подпрограмму ScanDirect.ory(). Подпрограмма ScanDi'tctoryf) принимает единственный аргумент - каталог, который надо просканировать. Она просматривает текущий каталог, входит в нужный подкаталог и сканирует его. По завершении сканирования подпрограмма возвращается в каталог. из которого она была вызвана. Вот новый вариант программы:

    # s также считается устаревшим, многие программисты



    # предпочитают использовать отдельный модуль

    ft (из семейства модулейОет.ор1: : ) для разбора (анализа) параметров

    use Cwd: n модуль для определения текущего рабочего каталога

    # Эта подпрограмма принимает имя каталога и рекурсивно

    # сканирует файловую систему, начиная с этого места, ищет и файлы с именем "core"

    sub ScanDirectory{

    my (Sworkdir) = shift;

    my (Sstartdir) = &cwd; ft запомнить, откуда мы начали

    cndir(Jworkdir) or die "Невозможно войти в каталог $workdir:$!\n";

    opendir(DIR, ".") or die "Невозможно открыть Sworkdir:$!\n";

    my fflnames = readdir(DIR) or die "Невозможно прочитать Sworkdir:$!\n";

    closedir(DIR);

    foreach my $name (@names){ next if (Sname eq "."); next if ($name eq "..");

    if (-1 $name){ # пропускаем ссылки

    next; }

    if (-d $name){ f* это каталог7

    &ScanDirectory($name); next; }

    if (Sname eq "core") { ft имя файла "core"? и

    если в командной строке указан ключ, на самом и деле удаляем этот файл

    if (defined $r) {

    unlink($name) or die "Невозможно удалить Sname:$!\n";

    }

    else {

    print "найден в Sworkdir1\n": > } } chdir(Sstartdir) or

    die "Невозможно перейти к каталогу $startdir:$[\n"; }

    &ScanDirectcry(".");

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

    чтобы сообщать об этом, как было в предыдущем примере, наша программа рекурсивно вызывает себя, чтобы изучить сначала содержимое этого каталога. По окончании сканирования всего подкаталога (т. е. вызов ScanDirectory() возвращает значение) программа возвращается к просмотру остального содержимого текущего каталога.

    Для того чтобы сделать нашу программу полнофункциональным ликвидатором core-файлов, мы добавили в нее функцию удаления файлов. Обратите внимание на то, как это реализовано: файлы будут удалены, только если сценарий вызывается с определенным ключом -s (от «remove» - удаление) в командной строке.



    В Perl мы указываем встроенный ключ -s в строке вызова (#! /us г/с in, perl -s) для автоматического разбора параметров. Это самый простой способ разбора параметров, переданных в командной строке. Искушения ради, мы могли бы использовать какой-либо модуль из семейства Getopt. Если в командной строке присутствует ключ (например -г), то при запуске сценария устанавливается глобальная скалярная переменная с тем же именем (например $г). Если Perl вызывается без ключа -г, мы вернемся к старому поведению подпрограммы - она будет лишь сообщать, что найдены core-файлы.

    Когда вы пишете автоматические утилиты, постарайтесь сделать так, чтобы разрушительные действия были затруднены. Учтите: Perl, как и большинство серьезных языков программирования, позволяет уничтожить файловую систему без особых усилий.

    Теперь, чтобы ориентированные на NT/2000 читатели не подумали, что предыдущие примеры к ним не относятся, покажем, что эта программа может пригодиться и для них. Единственное изменение строки:

    if (Sname eq "core") {

    на:

    if (Sname eq MSCREATE.DIR") {

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

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

    Вот какую программу я написал для этого:

    use Cwd: # модуль для определения текущего рабочего каталога

    $1=1; # отключаем буферизацию ввода/вывода

    sub ScanDirectory {

    my ($workair) = shift;

    my($startdir) = &cwd;

    ft запоминаем, откуда мы начали chdir($workdir)

    or die "Невозможно зайти а каталог $workdir:$'\n":

    opendir(DIR, ".")

    or die "Невозможно открыть каталог $workdir:$!\n";

    my @names = readdir(DIR);

    closedir(DIR);

    foreach my $name (@names){ next if ($name eq "."); next if ($name eq "..");



    if (-d $name){ ft это каталог?

    &ScanDirectory($name); next; } unless (&CheckFile($name)){

    print &cwd."/".$name."\n"; # выводим имя

    ft поврежденного файла } }

    sub CheckFile<

    my($name) = shift;

    print STDERR "Проверяется ". &cwd."/'".

    $name. "\n"; # пытаемся получить состояние файла my @stat = stat($name);

    if (!$stat[4] && '$stat[5] && !$stat[6] && !$stat[7] && l$stat[8]){ return 0;

    i )

    # пытаемся открыть этот файл unless (open(T,"$name")){

    return 0: }

    tt читаем файл по байту за один раз for (my $i=0;$i< $stat[7]:$i++){

    фу $r=sys'-ead(T.$i, 1);

    if ($r '= 1) { dose(T); retu--n 0 } >

    close(T);

    return 1: >

    &ScanDirecto-y("."):

    Различия между этой программой и последним примером заключаются в наличии дополнительной подпрограммы для проверки каждого найденного файла. Для каждого файла мы вызываем функцию stat, чтобы проверить, можно ли прочитать информацию о файле (например, его размер). Если мы сделать этого не можем, значит, файл поврежден. Если же прочитать эту информацию можно, мы предпринимаем попытку открыть файл. А в качестве последней проверки пытаемся прочитать каждый байт из файла. Это не гарантирует, что файл не поврежден (его содержимое могло измениться), но это говорит о том, что файл можно прочитать.

    Вы можете удивиться, зачем применять такую странную функцию, как sysread(), для чтения файла, если можно применить о или г ead (), обычно используемые для этого в Perl. Дело в том, что sysread() позволяет читать файл побайтно, не применяя обычную буферизацию. Если файл поврежден в позиции X, нет смысла ждать, пока будут прочитаны байты в позициях Х+1, Х+2, Х+3 и т. д., как это бывает при обращении к обычным библиотечным функциям. Мы хотим, чтобы попытки читать файл в таком случае прекратились немедленно. Обычно файл читается по кускам в целях повышения производительности, но это нежелательно, т. к. в нашем случае компьютер будет издавать ужасающие звуки в течение длительного времени, когда наткнется на поврежденный файл.

    Теперь, после рассмотрения использованной мною программы, я расскажу, чем закончилась эта история. После того как рассмотренный сценарий проработал всю ночь (без преувеличений), он нашел 95 поврежденных файлов из 16000. К счастью, ни один из них не был файлом из книги, которую вы сейчас читаете; я снял копии со всех хороших файлов и перенес их в другое место. Perl просто спас положение.






    Содержание раздела