Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате |
Форум на Исходниках.RU > C/C++: Системное программирование и WinAPI > Рекурсивный поиск файлов заполняет не выгруженный пул памяти Си WINAPI |
Автор: Sherman 12.05.19, 17:14 |
Приветствую,джентельмены. Пытаюсь выполнить поиск файлов на локальном диске,включая файлы во всех каталогах. Использовал для этого примеры,которые можно найти в интернете. Файлы находит,с этим проблем нет. Но,как я заметил,сжирает постепенно память(примерно 400мб с заполненного диска в 100Гб,около 220000 файлов),заполняя не выгруженный пул памяти. Я предполагаю,проблема в инициализации переменных в начале кода и в самой рекурсии. Если найденный файл является директорией(папкой),функция вызывает сама себя,передавая в качестве аргумента найденную папку и снова,и снова. Если файл не является директорией,строка передаётся в другую функцию. Таким образом,вызывая саму себя(дубликат),функция создаются новые переменные в которые пишутся строки,а старые не выгружаются,так как функция не завершилась.И так с каждой папкой. Подскажите,пожалуйста,как можно избавится от этой проблемы. <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> DWORD WINAPI SearchFiles(LPVOID lpName) { LPTSTR lpszFileName = (LPTSTR)GlobalAlloc(GMEM_FIXED, 1024);//lpszFileName,path переменные, LPTSTR path = (LPTSTR)GlobalAlloc(GMEM_FIXED, 1024); //которые инициализируются ZeroMemory(lpszFileName, 1024); //и не выгружаются ZeroMemory(path, 1024); lstrcpy(lpszFileName, lpName); lstrcpy(path, lpszFileName); lstrcat(path, "*.*"); HANDLE hand; WIN32_FIND_DATA data_file; hand = FindFirstFile(path, &data_file); if (hand != INVALID_HANDLE_VALUE) { do { if (!strncmp(data_file.cFileName, ".", 1) || !strncmp(data_file.cFileName, "..", 2)) { continue; } if (data_file.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { lstrcpy(path, lpszFileName); lstrcat(path, data_file.cFileName); lstrcat(path, "\\"); if (!lstrcmpi(path, WinPathD) == 0) //WinPathD - папка windows { SearchFiles((LPVOID)path); //тут функция вызывает сама семя } } else { lstrcpy(file, lpszFileName); // file глобальная переменная типа LPTSTR lstrcat(file, data_file.cFileName); Thread(file); } } while (FindNextFile(hand, &data_file) != 0); } FindClose(hand); GlobalFree(lpszFileName); GlobalFree(path); return 0; } |
Автор: Олег М 13.05.19, 07:43 |
Цитата Sherman @ Но,как я заметил,сжирает постепенно память(примерно 400мб с заполненного диска в 100Гб,около 220000 файлов),заполняя не выгруженный пул памяти. 220 тыс файлов? Я правильно понимаю, что ты так и создаёшь отдельный поток на каждый файл? Если да, то они, потоки, и жрут память. |
Автор: ЫукпШ 13.05.19, 09:35 |
Цитата Sherman @ Я предполагаю,проблема в инициализации переменных в начале кода и в самой рекурсии. Если не нравится рекурсивный поиск, сделай не рекурсивный. Сравни результаты и выбери то, что больше понравилось. |
Автор: Sherman 13.05.19, 11:37 |
Не на каждый,кол-во потоков ограниченно 10,когда их становится 10,функция ждёт их завершения и снова создаёт. Добавлено К сожалению,я не знаю,как это сделать.Всё,что находил в интернете,связанно с рекурсией. Если только искать в одной конкретной папке,то без нее. |
Автор: Олег М 13.05.19, 11:39 |
Цитата Sherman @ Не на каждый,кол-во потоков ограниченно 10,когда их становится 10,функция ждёт их завершения и снова создаёт. CloseHandle() делаешь для них? Покажт полный код |
Автор: Sherman 13.05.19, 11:47 |
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> if( memory(1) < 85 && // memory(1) %загрузки памяти ff < THREADS ) // THREADS = 10 { hThreadF[ff] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, (LPVOID)rFile, 0, NULL); } else { printf("\r\n*************************************************************\r\n\r\n"); for(int i = 0; i < ff; i++) WaitForSingleObject(hThreadF[i], INFINITE); for(int i = 0; i < ff; i++) CloseHandle(hThreadF[i]); ff = 0; hThreadF[ff] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, (LPVOID)rFile, 0, NULL); } ff++; хорошо,сейчас всё переписываю,как напишу,скину. |
Автор: Sherman 13.05.19, 12:26 |
Цитата Олег М @ Раз уж речь зашла о потоках,не могли бы вы мне подсказать,если я создаю поток <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> hThreadF[ff] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, (LPVOID)rFile, 0, NULL); Он может создаться не сразу,точнее говоря,нужно время на его создание,при этом,переменная (LPVOID)rFile,которую я передаю в поток,после этой строчки,меняется. Эта переменная не является глобальной. Когда поток будет создан,функция Thread получит уже изменённую переменную или то,что именно этому потоку и передавалось? |
Автор: Олег М 13.05.19, 12:34 |
Цитата Sherman @ Когда поток будет создан,функция Thread получит уже изменённую переменную или то,что именно этому потоку и передавалось? Поток создаётся сразу, исполняется позже. Значение будет передано то, которое на момент вызова CreateThread. И если ты не удаляешь этот ресурс в вызывающем потоке, то всё будет нормально. |
Автор: Sherman 13.05.19, 12:42 |
fFile пересодаётся,не означает ли это,что прежний fFile будет удалён? <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> else { LPTSTR fFile = (LPTSTR)GlobalAlloc(GMEM_FIXED, lstrlen(lpszFileName) + lstrlen(data_file.cFileName) + 1); assert(fFile); lstrcpy(fFile, lpszFileName); lstrcat(fFile, data_file.cFileName); if( memory(1) < 85 && ff < THREADS ) { hThreadF[ff] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, (LPVOID)fFile, 0, NULL); } else { printf("\r\n*********************************************************************\r\n\r\n"); for(int i = 0; i < ff; i++) WaitForSingleObject(hThreadF[i], INFINITE); for(int i = 0; i < ff; i++) CloseHandle(hThreadF[i]); ff = 0; hThreadF[ff] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, (LPVOID)fFile, 0, NULL); } ff++; } |
Автор: Олег М 13.05.19, 12:45 |
Нет, не будет. Здесь всё более-менее правильно Добавлено Надеюсь, в потоке ты делаешь для него GlobalFree? |
Автор: Sherman 13.05.19, 12:53 |
Цитата Олег М @ Спасибо. Как можно освободить память,выделяемую этой переменной? <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> LPTSTR fFile = (LPTSTR)GlobalAlloc(GMEM_FIXED, lstrlen(lpszFileName) + lstrlen(data_file.cFileName) + 1); Я пытаюсь вызвать GlobalFree для LPVOID в функции потока Правильный ли это подход? <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> DWORD WINAPI Thread(LPVOID gfile) { // UNREFERENCED_PARAMETER(gfile); LPTSTR nfile = (LPTSTR)GlobalAlloc(GMEM_FIXED, SizeBuf); ZeroMemory(nfile, SizeBuf); lstrcpy(nfile, (TCHAR*)gfile); GlobalFree(gfile); over++; printf("%d %s\r\n", over, nfile); GlobalFree(nfile); Sleep(8000); return 0; } |
Автор: Олег М 13.05.19, 12:57 |
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> DWORD WINAPI Thread(LPVOID gfile) { // UNREFERENCED_PARAMETER(gfile); over++; printf("%d %s\r\n", over, (LPCSTR)gfile); GlobalFree(gfile); Sleep(8000); return 0; } |
Автор: ЫукпШ 13.05.19, 13:02 |
Цитата Sherman @ К сожалению,я не знаю,как это сделать.Всё,что находил в интернете,связанно с рекурсией. Если только искать в одной конкретной папке,то без нее. Алгоритм простой. Заготовим 2 списка строк - один для директорий, другой для файлов. Занесём начальную директорию (полный путь) в список директорий. Дальше начинается основной алгоритм. --- 1. Проверяем, наличие строк в списке директорий. 2. Если нет - работа окончена, в списке файлов результат поиска. 3. Если есть - извлекаем 1-ю по счёту строку из списка, будем искать в этой директории - отдельно файлы, добавляя результат в список файлов (полные пути). - отдельно директории, добавляя результат в список директорий (полные пути). 4. закончился поиск в текущей директории - переход к пункту 1. Это всё. --- Результатом работы алгоритма является список строк имен файлов (полные пути). |
Автор: Sherman 13.05.19, 13:11 |
Спасибо,большое.Я уже так сделал,вроде работает. Попробую,как память себя ведёт. |
Автор: Qraizer 13.05.19, 13:13 |
Я проверил твою функцию на своём движке в подобной утилите (поиск совпадающих под контексту файлов). Она показала те же затраты, что и мой движок. И они гораздо скромнее: 168Мб на без малого миллионе файлов. Так что если где и утечка, то явно не в SearchFiles() |
Автор: Sherman 13.05.19, 13:23 |
Согласен.fFile была объявлена глобально.Думаю,проблема была в этой переменной. Перенёс её в функцию и передаю как аргумент,там же выделяю память. Перепишу и протестирую. Спасибо,что проверили. Добавлено Цитата ЫукпШ @ Благодарю.Интересный подход.Обязательно попробую. |
Автор: JoeUser 13.05.19, 14:02 |
Интересный подход для FAT, и в тоже время - неинтересный для NTFS. Все почему-то помнят только про файлы и каталоги, но напрочь забывают по хард-линки. А ведь так можно и улететь в бесконечный скан. |
Автор: Qraizer 13.05.19, 15:00 |
Только не хард-линки, а соединения, да и то только для каталогов. Хотя для файлов тоже имеет смысл сканить, чтоб не обрабатывать дубликаты, но к ошибкам это-таки не приведёт. P.S. Если уж заводить разговор за стратегически правильный код, то там много что учитывать надо. Нужно отсекать как минимум FILE_ATTRIBUTE_TEMPORARY и FILE_ATTRIBUTE_OFFLINE. Нужно пытаться зайти в очередной каталог и проверять успех, а то вдруг прав нет туда заглядывать, иначе тоже легко можно свалиться в бесконечный скан одного и того же каталога. И если речь о каком-то аналоге бэкапа, было бы неплохо хотя бы пробовать не обновлять время доступа к очередному файлу, чтоб не ломать статистику по частоте использования. |
Автор: Sherman 13.05.19, 15:05 |
Цитата JoeUser @ Все почему-то помнят только про файлы и каталоги, но напрочь забывают по хард-линки. Что-то вообще сложное... .Попробую изучить эту тему.Спасибо. Добавлено Есть вопрос по кол-ву потоков. Пишу программку,которая будет искать все файлы на ПК,проверяя расширение файла. Если расширение файла совпадает с тем,что я укажу,mp4 например,создаёт поток передавая функции потока строку,содержащую полный путь к файлу. Функция потока,в свою очередь,копирует найденные файлы на внешний жёсткий,который я тоже укажу. Для этого я использую функции WINAPI(CreateFile,CreateFileMapping,MapViewOfFile). Какое кол=во потоков можно использовать,для этой задачи? Пока я просто ищу файлы,передаю строки в потоки и там вывожу их в консоль. Так же вывожу целое число,которое соответствует кол-ву переданных строк в функцию потока. Но я заметил,что число файлов показывает разное,при одинаковых условиях. Чем больше потоков,тем больше разница. Пробовал от 2 до 5000 потоков.(в качестве эксперимента) На 500 потоков разнице +-10,на 5000 около 100000 строк. |
Автор: ЫукпШ 13.05.19, 17:12 |
Цитата Qraizer @ P.S. Если уж заводить разговор за стратегически правильный код, то там много что учитывать надо. Нужно отсекать как минимум FILE_ATTRIBUTE_TEMPORARY и FILE_ATTRIBUTE_OFFLINE. я делал так: Для файлов : FILE_ATTRIBUTE_TEMPORARY | FILE_ATTRIBUTE_OFFLINE Для директорий : FILE_ATTRIBUTE_TEMPORARY | FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_REPARSE_POINT И конечно, проверка на ошибку "ERROR_ACCESS_DENIED". Детали я точно не помню. Но ничего страшного с запретом доступа нет. Перечислить такую директорию (убедиться в её наличии) можно, а искать файлы внутри нельзя. Произойдёт ошибка и цикл завершиться, ничего страшного. Следить за этой ошибкой полезно исключительно в информационном смысле, для выдачи сообщения, что "зайти туда было запрещено". |
Автор: Sherman 13.05.19, 17:29 |
Не подскажете,как сделать эту проверку? |
Автор: Олег М 13.05.19, 17:34 |
Цитата Sherman @ Функция потока,в свою очередь,копирует найденные файлы на внешний жёсткий,который я тоже укажу. Для этого я использую функции WINAPI(CreateFile,CreateFileMapping,MapViewOfFile). Какое кол=во потоков можно использовать,для этой задачи? Я ж тебе вроде писал, что достаточно трёх потоков. С тех пор ничего не изменилось. Не работает функция в потоке _beginthreadex (сообщение #3798301) |
Автор: JoeUser 13.05.19, 19:49 |
Ну я образно. Хотя ты прав недо-хардлинки Так это еще не все, не расслабляйся! Надо искать файлы, которых "в явном виде нет, а они есть". Я про NTFS-потоки, а-ля: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> echo file-1>file-1.txt echo secret-1>file-1.txt:secret-1.txt Файл secret-1.txt в листинге dir не виден. Но он есть, и он - в потоке файла file-1.txt, и можно убедиться: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> notepad file-1.txt notepad file-1.txt:secret-1.txt |
Автор: Sherman 13.05.19, 20:28 |
Да,я помню,но с тем способом вообще беда у меня. Я даже нигде не встречах подобие того,что вы описали. Добавлено Цитата JoeUser @ |
Автор: ЫукпШ 13.05.19, 23:01 |
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> BOOL RetC=FALSE; // ... hFind = ::FindFirstFile(pTempName, &FindFileData); if(hFind == INVALID_HANDLE_VALUE) {RetC = FALSE;} else {RetC = TRUE; } //... RetC = ::FindNextFile(...); if(RetC == FALSE) { DWORD LE = ::GetLastError(); // возможные результаты окончания if(LE == ERROR_NO_MORE_FILES) {...} // ERROR_NO_MORE_FILES = 18, больше файлов не осталось if(LE == ERROR_FILE_NOT_FOUND) {...} // ERROR_FILE_NOT_FOUND = 2, файл не найден if(LE == ERROR_PATH_NOT_FOUND) {...} // ERROR_PATH_NOT_FOUND = 3, path не найден // доступ запрещён if(LE == ERROR_ACCESS_DENIED) {...} // другое - совсем плохо } |
Автор: Sherman 14.05.19, 00:29 |
Цитата ЫукпШ @ Большое, спасибо! |
Автор: Олег М 14.05.19, 06:43 |
Цитата Sherman @ Да,я помню,но с тем способом вообще беда у меня. Я даже нигде не встречах подобие того,что вы описали. А что встречал - как на каждый чих зафигачить поток, авось система сама разберётся? Вообще - не надо решать все проблемы одновременно. Все эти хардлинки с атрибутами это вторичная проблема, не факт, что с ней столкнёшься. Основной алгоритм здесь - асинхронный поиск и копирование, у тебя он не решён. Лучше сосредоточиться на нём, а когда заработает как часы, тогда уже можно заняться и деталями. |
Автор: JoeUser 14.05.19, 12:18 |
Цитата Олег М @ Все эти хардлинки с атрибутами это вторичная проблема, не факт, что с ней столкнёшься. Не правда твоя! Берем системный диск любой современной версии винды и видим сразу же в корне соединение "Users" -> "Documents and Settings": |
Автор: Олег М 14.05.19, 12:38 |
Необязательно тестировать на Document and Settings. |
Автор: JoeUser 14.05.19, 14:14 |
Точнее - лучше не сканить систем диск с корня, иначе сбудутся пророчества тяжёлых времён :-) |
Автор: Sherman 14.05.19, 14:41 |
Цитата Олег М @ Лучше сосредоточиться на нём, а когда заработает как часы, тогда уже можно заняться и деталями. Полностью согласен.На данный момент я и копирование не делаю.Занимаюсь только настройкой поиска и потоков. Добавлено Нет,не встречал.Но встречал программку,которая делала что-то подобное и делала это очень быстро,и потоков было с лихвой. Вот только кода этой программки я не видел,да и её самой у меня уже нет. Случайно наткнулся,не думал на тот момент,что сам заинтересуюсь. Всё,что я пока делал,работает очень медленно. Всё,что я знаю о той проге, это то,что она работает с большим количеством потоков и с WINAPI. Пробовал выполнять копирование при помощи Си (FILE*) и WINAPI(CreateFile),заметил,что WINAPI работает намного быстрее. Думаю я на верном пути. Добавлено Цитата JoeUser @ Берем системный диск любой современной версии винды и видим сразу же в корне соединение "User" -> "Documents and Settings": Если я запускаю программу не от имени админа,она эти папки даже не видит,а есть и такие,для которых и вовсе прав админа не достаточно, нужно что-то вроде привилегий SE_BACKUP_NAME Но в моём случае,это не не нужно и даже вредно. |
Автор: JoeUser 14.05.19, 16:10 |
Олег М в своих предыдущих сообщения был во многом прав по поводу потоков. А мое резюме - ты люто и бешено зациклен на многопоточности. Народная мудрость гласит "все хорошо в меру". И тут это как ни кстати - актуально! Пример. Берем восми-ядерный проц. И запускаем сборку Цэ++ проекта. Сколько потоков выделить на сборку? Скрытый текст Тут сикретик. И мой ответ тамошний - в топах. Народ кагбэ одобяет. А вот теперь подумай, у тебя простой скан диска. Сколько операций твой дисковый массив сможет сделать одновременно? Универсального ответа нет. Если это честный хардварный рэйд - он закэширует твои вызовы хорошо, но все равно выполнит их последовательно или псевдо-параллельно. Если это мамковый fake-рэйд, сделает тоже самое, но плюс к тому загрузит камень, к гадалке не ходи! Единственный вменяемый вариант - когда идет одновременный скан n-независимых дисков, сидящих на разных интерфейсах. Резюме 1) Если какого-то трудного пересчета нет - не делай потоков больше, чем количество отдельных физических носителей 2) Не верю что 2-х поточная прога на 2-х винтовом рэйде сильно победит однопоточную (если вообще сможет) и не уступит 3-х поточной ЗЫ: Если захочешь узнать три Закона Будды - пиши в личку. Добавлено Тебе виднее для твоего случая. Без вопросов |
Автор: ЫукпШ 14.05.19, 19:55 |
Цитата JoeUser @ Цитата Олег М @ Все эти хардлинки с атрибутами это вторичная проблема, не факт, что с ней столкнёшься. Не правда твоя! Берем системный диск любой современной версии винды и видим сразу же в корне соединение "User" -> "Documents and Settings": Не совсем понятно, о чём дискуссия.. У этой директории просто установлены атрибуты: FILE_ATTRIBUTE_TEMPORARY | FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_REPARSE_POINT (и конечно FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM) Если захочется специально проигнорировать объект с такими атрибутами это можно сделать. И в результате игнорировать такую директорию для дальнейших действий. Если поиск не фильтровать по атрибутам, директория будет найдена. |
Автор: JoeUser 14.05.19, 20:53 |
ЫукпШ, да я к тому - что файловый носитель уже давно ушел от древовидной структуры. И только упоротый гик пытается делать обход "дерева", не взирая на все увещевания. Я кончил. |
Автор: Qraizer 14.05.19, 21:28 |
В отношении FILE_ATTRIBUTE_REPARSE_POINT мнение может быть двояким. С одной стороны, они являются ссылками на другие объекты, которые либо находятся вне рассматриваемой области тома, а то и вообще другого тома, либо и без учёта этой ссылки тоже будут обработаны. С другой же стороны, не зря же эта ссылка тут расположена, значит кому-то было нужно, чтобы та, другая, область (другого) тома вроде бы считалась как будто бы находящаяся тут. Так что этот аспект не абсолютен и вполне себе обсуждаем, и наличие этого атрибута у объекта целиком и полностью зависит от пользователя. Даже не автора программы. JoeUser же говорил о другом его аспекте: этот атрибут может иметь объект, ссылающийся на одного из своих родителей. И это уже куда серьёзней, так как его игнор ведёт к бесконечной петле. Попробуй его не игнорировать и при этом не допустить циклов. Это не так-то просто запрограммировать. С другими атрибутами другая петрушка. FILE_ATTRIBUTE_TEMPORARY выставляется приложениями для объектов, которые являются порождениями реализаций их внутренних алгоритмов. Такие объекты используются ими временно, для внутренних нужд и не содержат полезной для пользователя информации. Игнорировать такие объекты сам бог велел, ибо зачем обрабатывать мусор? FILE_ATTRIBUTE_OFFLINE говорит о том, что объект в данный момент недоступен, поэтому его обработка непосредственно сейчас может оказаться невозможной или потребовать от пользователя телодвижений, т.е. неких ручных операций, для обеспечения доступа к нему. Вряд ли пользователя это обрадует, тем более, что он всё равно, зная, что запускает приложения по обработке данных, должен был бы заранее озаботиться предоставлением доступа ко всем таким объектам, и тогда этот атрибут был бы с них уже снят. Да, он может об этом не подумать или просто забыть о парочке таких, однако останов программы, запущенной на ночь для длительной обработки в нерабочее время, в паузу поутру может обернуться неслабыми матюгами. Из-за одного объекта не были обработаны куча других, вполне нормальных. Поэтому будет хорошей практикой все такие объекты обходить, чтоб не тормозить себе работу, однако включить их отчёт о проделанной работе как о непроделанной. |
Автор: Олег М 15.05.19, 06:20 |
Цитата Sherman @ Всё,что я пока делал,работает очень медленно. Всё,что я знаю о той проге, это то,что она работает с большим количеством потоков и с WINAPI. Думаю, первое вытекает из второго. Скорее всего, она использует асинхронный ввод-вывод, виндовский, а тот, в свою очередь - пул потоков. |
Автор: Sherman 15.05.19, 15:22 |
Цитата Олег М @ Скорее всего, она использует асинхронный ввод-вывод, виндовский, а тот, в свою очередь - пул потоков. OVERLAPPED это то что мне нужно? Вчера только наткнулся на эту "фишку". Переодически веду всякие заметки по программированию в блокноте.Нашёл и о этой программке заметку. Она использовала CreateFileW,CreateFileA,CreateFileMappingA,MapViewOfFile,UnmapViewOfFile,SetFilePointerEx,WriteFile,GlobalAlloc,GlobalFree,RtlZeroMemory,CreateThread, По крайней мере,это было в листинге дис-ассамблера. Здесь не случайно нет ReadFile. По всей видимости,она проецировала файл(карту) в виртуальной памяти,читала карту блоками и писала в другой. Если это делать,как вы писали,разделить эти процедуры по разным потокам,получается,нужно в одном потоке искать файлы,во-втором проецировать файл и передавать в третий хедл CreateFileMappingA,в котором,в свою очередь,читать карту(MapViewOfFile) и писать в файл? |
Автор: Олег М 15.05.19, 17:56 |
Нет. Сначала тебе нужно сделать то что я тебе рекомендовал, это самое простое, с чего надо начинать. Проецирование и т.п. - это следующий шаг, не стоит перескакивать. |