Учебник по сетевым системам и приложениям
Операционные системы. UNIX. Linux
Зачем нужна операционная система?
Современная информатика - это множество слоев абстракций. Абстракции позволяют создавать и поддерживать невероятно сложные системы, которые ни один человек не понимает до конца. Работа современного компьютера - это тоже большое количество абстракций. На самом нижнем слое располагаются физические устройства - центральный процессор и различные внешние устройства. На самом верхнем - прикладные программы и пользовательский интерфейс. Если бы при создании прикладных программ нужно было бы напрямую работать с аппаратным обеспечением - создание программ было бы невероятно долгим, скучным и дорогостоящим процессом. Так и было на заре компьютерной техники.
Операционные системы - это средний слой абстракции между пользователем, его программами и “железом”. Любой современный компьютер работает под управлением операционной системы. Здесь под термином “компьютер” мы понимаем и настольный ПК, и сервер и смартфон и даже некоторое носимые и встроенные устройства. Исключение составляют только самые примитивные электронные устройства и встраиваемые компьютеры.
Операционная система - это особая программа, которая загружается и начинает исполняться на компьютере при его включении и позволяет выполнять как бы внутри себя другие программы. Без операционной системы на компьютере могла бы выполняться только одна программа. Так и функционировали компьютеры до появления операционных систем. Современные операционные системы позволяют организовать многозадачность - параллельное выполнение множества программ в пределах одной пользовательской среды. Операционные системы выполняют множество разных задач, облегчающих пользование компьютером для всех категорий пользователей - обычных, работающих в браузере, офисных, работающих с документами, программистов, системных администраторов. Но две функции операционной системы являются базовыми, из которых происходят все остальные.
Операционная система выполняет две главные функции: управление ресурсами и предоставление расширенной машины. Управление ресурсами подразумевает, что именно операционная система управляет доступом всех программ к ресурсам компьютера. Под ресурсами понимается информационное и аппаратное обеспечение. Аппаратное обеспечение компьютера - это в первую очередь центральный процессор и оперативная память. Информационные - это файлы (в широком смысле), расположенные на данном компьютере. Управление доступом к процессору означает, что операционная система выполняет все программы, запущенные на компьютере, переключая их множество раз в секунду, то есть реализует многозадачность. Такая многозадачность может быть даже, если в компьютере присутствует только одно вычислительное ядро. Но чаще таких ядер несколько. И опять же именно ОС распределяет задачи по ядрам, управляет приоритетом и очередью выполнения процессов на компьютере.
Управление доступом к оперативной памяти заключается в том, что при запуске нового процесса операционная система выделяет ему определенный участок в оперативной памяти, куда имеет доступ только этот процесс. Если бы программы могли свободно обращаться к памяти друг друга это было бы очень опасно с точки зрения разделения ответственности. Мы никогда не могли бы быть уверены, что работе программы не помешают другие процессы, злонамеренно или по неосторожности программиста. Управлять таким хаосом было бы невозможно. Именно операционная система изолирует программы друг от друга и таким образом повышает надежность функционирования всей системы в целом.
Точно так же операционная система разделяет между программами доступ ко всем остальным ресурсам - файлам, внешним устройствам, сети. Когда одна программа открывает файл и начинает туда писать она может быть уверена, что посередине этого процесса в запись не вклинится другая программа. Операционная система блокирует файлы при открытии, чтобы с ними мог работать только один процесс одновременно. Если в этот момент другой процесс хочет открыть тот же файл, он должен подождать, когда первый его освободит. За все этим следит определенная подсистема операционной системы.
Для того, чтобы операционная система могла выполнять эту функцию она должна обладать эксклюзивным доступом к устройствам компьютера - от процессора до внешних устройств, таких как жесткий диск или сетевой адаптер. Иначе любая программа могла бы обойти все защитные механизмы ОС и работать с аппаратурой напрямую. Поэтому ядро операционной системы и только оно работает в определенном, привилегированном режиме центрального процессора, когда инструкциям ядра ОС можно выполнять всевозможные действия на компьютере, в том числе для работы с внешними (по отношению к ЦП) устройствами. Остальные программы работают в ограниченном, пользовательском режиме работы. В нем они не имеют доступа к аппаратному обеспечению компьютера, а могут только манипулировать данными внутри данного процесса.
Но как же в таком случае, программы могут открывать файлы (расположенные на жестком диске), посылать документы на печать, общаться по сети и выполнять другие действия? На самом деле, они не могут. Каждый раз, когда программе требуется получить доступ к внешнему ресурсу (аппаратному или информационному), она должна попросить это сделать операционную систему. Для этого в ядре ОС существует набор функций, которые может вызвать любая программа на компьютере. Такие функции называются системными вызовами. Например, существует системный вызов для открытия файла, отправки данных по сети, создания нового процесса и еще множество. Все они образуют так называемый интерфейс системных вызовов операционной системы. Интерфейс системных вызовов определяет, что программы могут делать с использованием этой конкретной операционной системы.
При выполнении системного вызова операционная система в первую очередь проверяет, имеет ли право данная программа выполнить данной действие. Потом - есть ли что-то, что может мешать выполнить его в данный момент (не заблокирован ли нужный файл, например). И только если ОС решит, что все в порядке, она выполнит просьбу программы и вернет ей результат. А если что-то пойдет не так, то ОС вместо результата вернет ошибку. Еще раз подчеркнем, пользовательские программы (а по отношению к операционной системе все программы являются ее пользователями) не могут никак получить доступ к компьютеру, кроме как через механизм системных вызовов.
У каждой операционной системы этот интерфейс может отличаться. Именно поэтому, а еще потому, что формат исполняемых файлов в разных ОС разный, программы пишутся специализированно под конкретную операционную систему. Однако, многие операционные системы следуют стандартному интерфейсу системных вызовов. Этот стандарт называется POSIX. Он регламентирует, какие вызовы должны быть у операционной системы, какие нужно передавать аргументы, как они должны выполняться. Все UNIX-подобные операционные системы, и еще некоторые другие, являются POSIX-совместимыми. Поэтому большинство программ, написанные для одной такой системы могут быть относительно легко перенесены на другую. Именно поэтому мы говорим об этих системах, как о родственных, как о семействе. Отметим, что одно из самых известных семейств несовместимых с POSIX операционных систем - это Windows.
Для своей работы операционные системы оперируют множеством понятий, которые не имеют смысла вне ОС, которые существуют только внутри системы. Мы уже упоминали системные вызовы, процессы внутри ОС. Еще операционные системы организуют информацию в виде привычных уже пользователям файлов и папок, организуют хранение файлов на диске при помощи файловых систем, отслеживают зарегистрированных и авторизованных пользователей, следят за соблюдением пользователями прав доступа. Все это и многое другое - ключевые концепции современных операционных систем.
Выводы:
- Современная компьютерная техника очень сложна и многообразна.
- Операционная система - это промежуточный слой между прикладными программами и аппаратным обеспечением.
- Операционная система - это основная программа компьютера.
- ОС позволяет запускать другие программы, в том числе - одновременно.
- ОС распределяет ресурсы - устройства компьютера между программами (процессами).
- ОС предоставляет универсальный интерфейс для программ к аппаратным устройствам.
- Только операционная система имеет полный доступ к компьютеру. Программы должны обращаться к ней.
- Ключевые понятия операционных систем - системные вызовы, процессы, файловые системы, пользователи и права.
Зачем изучать Linux?
Сейчас существует большое количество разных операционных систем. Некоторые из них более известны обычному пользователю. Некоторые - используются профессиональными разработчиками, дизайнерами или системными администраторами. Некоторые предназначены для узкоспециализированных задач.
Обычному пользователю более знакомы настольные операционные системы, такие как Windows и MacOS, а также мобильные ОС - Android и iOS. Эти системы в первую очередь предназначены для широкого круга непрофессиональных пользователей, которые выполняют разнообразные задачи - от работы в офисных программах до игр. Но кроме них существуют операционные системы, предназначенные для профессионального использования - для разработки ПО, для организации сетевых сервисов. Чаще всего все операционные системы делят на десктопные и серверные. К ним предъявляются очень разные требования. Десктопные системы предназначены для повседневного использования. Они должны быть в первую очередь, удобные в работе, обладать интуитивным графическим интерфейсом, поддерживать широкий круг пользовательских программ и пользовательских аппаратных средств (принтеров, видеокарт, Wi-Fi адаптеров, мониторов и так далее). Серверные операционные системы должны быть защищенными с точки зрения безопасности, обладать надежной внутренней архитектурой, предсказуемым поведением и интерфейсом, прекрасно работать с сетевыми протоколами, быть многопользовательскими (с сопутствующим разделением доступа). Конечно, это деление условно. Хорошая десктопная система тоже должна быть защищенной, а серверная - удобной. Но исторически такое разделение возникло очень давно и сохраняется до сих пор.
Одной из самых известных профессиональных операционных систем была и остается ОС UNIX. Она была создана одновременно с языком C. У нее было много преимуществ по отношению к тогдашним альтернативам - она была изначально многопользовательской и сетевой и обладала крайне удачной архитектурой. Она имела огромный успех и на ее основе создавались похожие и совместимые с ней другие системы, такие, как например, BSD или Linux. На сегодняшний день, Linux - это одна из самых известных и широко распространенных операционных систем семейства UNIX. Хотя изначально она создавалась для профессионального применения, сейчас ее вполне можно применять и в качестве десктопной.
Но десктопное применение Linux все-таки имеет свои минусы для обычного пользователя. Чаще всего ее применяют именно для серверных нужд. В настоящее время Linux занимает подавляющую долю на рынке серверных ОС. На основе Linux работают большинство серверов в интернете, облачных сервисов, IoT. Если вы в своей профессиональной деятельности столкнетесь с серверным окружением, скорее всего, оно будет работать под управлением какой-то из версий Linux.
Как потомок UNIX, Linux - POSIX-совместимая операционная система. Вообще, так случилось, что многие концепции, которые изначально были представлены в UNIX, а затем были унаследованы в Linux стали в профессиональной среде стандартными подходами. Например, классическая модель пользователей и разделения прав доступа, стек сетевых технологий (на его основе работает вообще весь Интернет, а для UNIX - он родной), механизм дистрибуции программ, текстовые конфигурационные файлы, стандартный командный интерпретатор и многое другое.
Поэтому почти все профессиональные ИТ-специалисты должны хотя бы в какой-то степени быть знакомыми с этим семейством операционных систем. Знание основ Linux для компьютерщика - один из базовых навыков, неважно, работаете вы программистом, сисадмином, аналитиком данных или девопс-инженером. Кроме непосредственно практической пользы, изучение непривычной операционной системы может принести и косвенные выгоды - вы невольно станете лучше понимать, как функционирует компьютер, как программы выполняются в среде ОС, как работает сама операционная система. А в случае с Linux - вы обязательно улучшите свои навыки автоматизации рутинных задач администрирования, написания скриптов, развертывания информационных окружений, эта система очень располагает к такой деятельности.
Выводы:
- Существует множество операционных систем. Каждая имеет свое предназначение.
- Различают десктопные и серверные ОС.
- Среди операционных систем есть своя генеалогия. Linux относится к UNIX-семейству.
- Linux - это изначально серверная ОС, которая может использоваться как десктопная.
- Linux - одна из самых распространенных серверных ОС.
- Многие механизмы, принятые в Linux стали стандартами в компьютерной технике и сетях.
- Изучение Linux прокачивает понимание компьютеров и навыки автоматизации.
Чем Linux отличается от Windows?
Для начала знакомства с Linux стоит подчеркнуть, в чем ее отличия от самой известной широкому кругу пользователей операционной системы - Windows. Главное отличие - Linux распространяется с открытым исходным кодом. Это, казалось бы, техническое отличие влечет за собой принципиально иную модель распространения ОС и большинства программ для нее.
Многие связывают свободное распространение программ в исходных кодах с бесплатностью, хотя это не связано напрямую. Многие бесплатные программы являются закрытыми, и существуют открытые программы, продающиеся за плату. Но открытые исходные коды подразумевают, что любой пользователь, если он обладает необходимыми навыками, может внести любые изменения в программу и распространять уже свою версию. Это приводит к тому, что большинство свободных программ создаются и распространяются сообществом энтузиастов на некоммерческих основаниях. Кроме того, практически у каждой свободной программы есть так называемые форки - альтернативные варианты, созданные когда любители программы внесли изменения независимо от изначальных разработчиков. Еще это приводит к тому, что для Linux существуют программы практически на все случаи жизни.
Обилие форков не обошло стороной и саму операционную систему. В мире Linux существует большое количество так называемых дистрибутивов. Дело в том, что для прикладного использования ядра операционной системы недостаточно. Кроме него нужны системные и прикладные программы. Каждый любитель может собрать свой собственный набор нужного ему программного обеспечения вместе с ядром ОС и распространять в пакете. Такой пакет и называется дистрибутивом. Этого понятия в принципе не существует в Windows.
Самое заметное обычному пользователю различие между разными операционными системами - несовместимость программного обеспечения. Большинство прикладных программ распространяются в скомпилированном виде - в виде бинарных исполняемых файлов. А компилировать программу нужно под конкретную операционную систему. В большинстве случаев, чтобы программ работала под другой операционной системой нужно вносить существенные изменения в код программы. Поэтому множество программ, особенно прикладных, разрабатывается только для необходимых операционных систем. Поэтому большинство программ, знакомых обычному пользователю по миру Windows в UNIX системах либо не существуют, либо есть только аналоги. В последнее время ситуация сильно исправляется, так как и сама Linux получает все большее распространение, и написание кросс-платформенных программ все больше упрощается.
Кроме того, что набор прикладного ПО существенно отличается, Linux использует централизованную модель дистрибуции программного обеспечения. Это значит, что для установки программы не обязательно искать программу на сайте производителя. В дистрибутив обязательно включается специальная программа - менеджер пакетов. Она устанавливает программы из централизованного источника - репозитория. К системе может быть подключено несколько репозиториев, каждый из которых играет роль каталога программ. Сейчас мы можем повсеместно наблюдать похожую картину в мобильных операционных системах, когда мы пользуемся магазином приложений. И неудивительно, ведь все распространенные мобильные операционные системы - UNIX-подобные.
При такой системе устанавливать распространенные прикладные программы в систему становится гораздо легче. Это происходит, по сути дела, одной командой. Поэтому многие программы установки дистрибутивов включают в себя инструкции по установке определенного количества прикладных программ. Когда вы устанавливаете Linux, вы получаете не просто “голую” систему, как в случае с Windows, а готовую к использованию, включающую большое количество необходимых каждый день программ - офисных пакетов, браузеров, программ для работы с мультимедиа, даже аудио и видео редакторы.
Но Linux отличается от Windows не только набором программ, но и внутренним устройством. Здесь мы не будем вдаваться в детали архитектуры ядра ОС, на эту тему есть большое количество специализированной литературы. Отметим лишь, что на всех уровнях изучения, устройство Linux значительно отличается, ведь эти два семейства разрабатывались полностью (или почти полностью) независимо.
Для пользователя различия во внутреннем устройстве выражаются наиболее четко в одном неожиданном аспекте. Linux - очень текстоцентрична. Она практически полностью ориентирована на работу с текстом. Большинство настроек хранятся в текстовых файлах, а не в бинарном реестре, большинство служебных команд командной строки представляют собой те ил иные манипуляции с текстовыми потоками. Поэтому при изучении Linux так важно научиться работать с текстом и обрабатывать структурированные различными способами текстовые потоки.
Выводы:
- Linux распространяется свободно, исходный код ОС открыт.
- Это более важное отличие, чем вы думаете.
- Существует множество дистрибутивов Linux.
- У Linux другой набор программного обеспечения.
- Программы обычно устанавливаются из репозиториев.
- Дистрибутивы Linux включают много прикладных программ.
- Внутреннее устройство совершенно другое.
- Linux ориентирована на работу с текстом.
Как создавалась Linux?
История Linux начинается, когда один финский студент, Линус Торвальдс, в конце 80-х годов решил создать собственное ядро операционной системы наподобие UNIX, но бесплатное. В то время система UNIX была наиболее известной и популярной операционной системой для профессионального использования. Главным ее минусом было то, что она распространялась на коммерческой основе и многие, особенно студенты, не могли себе ее позволить.
Торвальдс опубликовал первую версию ядра Linux в 1991 году. Начавшаяся как студенческий проект, Linux быстро завоевала популярность. В то время существовал еще один проект по созданию свободной альтернативы UNIX - проект GNU. Им руководил Ричард Столлман, идейный вдохновитель движения за свободное программное обеспечение. Но они концентрировались на создании набора системного программного обеспечения - интерпретатора командной строки, компилятора языка C, файловых утилит и прочего. Единственное, чего им не хватало до полноценной операционной системы - ядра. Естественно, они довольно быстро объединились с Торвальдсом. Получившаяся в результате система полностью называется GNU/Linux - набор программ GNU, работающий на основе ядра Linux. Но в широких кругах ее называют просто Linux.
С 1991 года Linux как операционная система все больше набирает популярность. Она постоянно поддерживается, до сих пор активно выходят новые версии ядра, добавляющие новые функции и поддерживающие разные компьютерные архитектуры. В разработке ядра Linux принимают участие сотни высококвалифицированных системных программистов со всего мира.
Будучи свободной альтернативой UNIX, при этом полностью с ней совместимой, Linux нашла свою популярность в качестве серверной операционной системы, практически полностью вытеснив с этого рынка саму UNIX и другие подобные системы. Но кроме этого, Linux активно развивается и как десктопная операционная система. Использование графических окружений, расширение набора доступного программного обеспечения, поддержка все большего числа аппаратных средств делают Linux вполне применимой для повседневного использования.
Отдельно отметим, что при этом Linux все еще довольно затруднительно использовать людям, абсолютно не ориентирующимся в работе компьютера. Регулярная нужда в использовании командной строки отпугивает очень многих, поэтому мы не убеждаем вас использовать Linux как замену Windows или MacOS в повседневном режиме. В этом есть как плюсы, так и минусы. И мы изучаем Linux не потому, что считаем ее лучше или хуже других операционных систем. Главная причина изучения Linux для работника сферы IT - широкое ее распространение в профессиональной инфраструктуре, в первую очередь в качестве серверной ОС.
Выводы:
- Собственно говоря, Linux - это название ядра операционной системы.
- Кроме ядра в Linux входит программное обеспечения из проекта GNU.
- Ядро Linux было разработано в 1991 году Линусом Торвальдсом.
- Linux изначально - это свободная альтернатива UNIX.
- Сейчас профессионалы используют Linux, а фанатики ждут вендекапец.
Что такое дистрибутив?
При знакомстве с Linux пользователь становится перед выбором дистрибутива. В мире Windows такого понятия как дистрибутив не существует, так как эта система разрабатывается и распространяется одной компанией.
Дистрибутив - это совокупность ядра, набора системных и прикладных программ и программы установки, которые скомпонованы, настроены и распространяются в виде, готовом для установки на компьютеры пользователя. По сути, дистрибутив - это что-то вроде версии, или конкретной реализации операционной системы.
Разные дистрибутивы выпускаются разными производителями. Среди них есть как крупные многопрофильные ИТ-компании, такие как Microsoft, IBM, Oracle, так и специализированные разработчики, такие как Canonical, RedHat, FSF, есть даже известные дистрибутивы, разработанные и поддерживаемые энтузиастами и сообществом.
В принципе, выбор дистрибутива - это вопрос удобства и легкого старта. Теоретически, установкой, удалением и настройкой соответствующих компонент можно превратить один дистрибутив в другой. НО мы не рекомендуем этого делать.
Среди множества дистрибутивов легко запутаться. Споры о том, какой дистрибутив лучше для определенных целей - это любимое занятие энтузиастов, но мы не будет об этом говорить. Для того, чтобы иметь какой-то ориентир, нужно понять какой дистрибутив выбрать и по каким критериям.
Первое, что нужно узнать про дистрибутивы - это то, что их можно объединить в семейства. Основные из них - это Debian, RedHat, Gentoo и Arch. Дело в том, что одни дистрибутивы могут разрабатываться на основе других. Это уже упоминавшаяся особенность мира свободного ПО - обилие форков. Если вам удобен какой-то дистрибутив, но хочется в нем поменять что-то, вы вполне можете это сделать и распространять получившийся дистрибутив как производную.
Основное, в чем проявляется родство дистрибутивов - это программные репозитории. Репозиторий по сути - это просто каталог программ, хранящийся на централизованном сервере. Обычно разработчики крупных и основных дистрибутивов составляют и поддерживают такие репозитории специально для своего продукта. Можно провести аналогию с мобильными операционными системами, у Android и iOS свои магазины приложений. Более того, вы не можете подключить AppStore к Android и наоборот, PlayMarket к iPhone. Потому, что у этих систем установка программ, формат пакетов тоже разный. Так и в разных семействах дистрибутивов использующиеся по умолчанию менеджеры пакетов, программы, которые устанавливают пакеты из репозиториев, тоже могут быть разными. Например, во всех дистрибутивах из семейства Debian для установки программ. используется программа apt. В семействе Red Hat используют другую программу - rpm. Поэтому и репозитории этих дистрибутивов разные, и использовать друг друга они не могут.
Но даже в пределах одного семейства многие дистрибутивы не имеют своих репозиториев (или имеют маленькие и специализированные), а используют репозитории “старших товарищей”. Так, например, дистрибутив Linux Mint использует набор репозиториев для дистрибутива Ubuntu Linux. Поэтому также можно сказать, что первый в какой-то мере основан на втором.
Но как выбрать из всего этого многообразия дистрибутив для использования так, чтобы потом не приходилось жалеть о сделанном выборе и переделывать много всего? Мы рекомендуем выбирать наиболее известные и популярные дистрибутивы. Так у вас больше шансов получить стабильно работающую систему, с большим коммьюнити, документацие и репозиториями программ. При выборе дистрибутива можно ориентироваться на актуальные рейтинги популярности, которые регулярно обновляются. Практически все популярные дистрибутивы общего назначения подойдут для первоначального освоения этой операционной системы. В дальнейшем в этом курсе мы будет ориентироваться на дистрибутивы из Debian семейства, такие как Ubuntu Linux или Linux Mint.
Выводы::
- Дистрибутив - это ядро ОС и куча программ в нагрузку.
- Выбор дистрибутива - это очень сложное, но абсолютно неважное дело.
- Основные семейства дистрибутивов - Debian, RedHat, Gentoo, Arch, есть еще куча мелких.
- Новичкам лучше выбрать распространенный и дружелюбный дистрибутив (Ubuntu, Mint).
- Всегда можно попробовать другой и перейти.
Что такое графическое окружение?
Кроме непосредственно дистрибутивов перед пользователями Linux встает проблема выбора графического окружения. Графическое окружение, оболочка рабочего стола - это программа, которая позволяет работать в операционной системе в уже привычной для большинства пользователей оконной среде при помощи мыши и, в меньшей степени, клавиатуры. В мире Windows графическая оболочка является интегрированной и неотъемлемой частью самой операционной системы. А вот для Linux это отдельная программа, которая может и отсутствовать.
Опять же из-за открытого исходного кода программисты могут создавать разные графические окружения. И за десятилетия существования Linux их было создано немало. Некоторые из них более распространены, некоторые - менее. Все дистрибутивы, ориентированные на повседневного пользователя включают в себя одну или даже несколько графических оболочек. Именно они задают вид пользовательского интерфейса - внешний вид окон, меню и панелей, эффекты и анимации, прозрачности и прочие визуальные эффекты. Исторически, наибольшую популярность приобрели две графические среды - KDE и Gnome. Выбор между ними сводится к индивидуальным эстетическим предпочтениям.
Здесь есть одна особенность. При казуальном использовании операционной системы, в отсутствие тяжелых вычислительных процессов, графическая оболочка становится наиболее ресурсоемкой программой. Использование всяческих эффектов приводит к росту требований к оперативной памяти и мощности процессора компьютера. И KDE и Gnome - это сложные полнофункциональные оболочки, которые включают в себя широкие возможности настройки и использования визуальных эффектов. Если ресурсы компьютера ограничены, следует обратить внимание на более легковесные оболочки, которые созданы специально для экономии ресурсов. Они выглядят попроще, но и требований таких не имеют.
KDE и Gnome - это довольно старые программы, которые активно развиваются и совершенствуются. Кроме непосредственно отображения окон они включают в себя большое количество вспомогательных программ - файловых менеджеров, просмотрщиков мультимедиа файлов, даже собственные офисные пакеты. Так что со временем они превращаются в такие многофункциональные программные комбайны. Другие оболочки лишены этой особенности.
Для профессионального использования вопрос выбора графической оболочки имеет второстепенное значение, так как большую часть задач необходимо будет решать в командной строке, так что как будут выглядеть окна, мелькающие на заднем плане - не очень важно и не влияет на рабочий процесс. В остальном выбор и дистрибутива и графической оболочки остается за вами.
Выводы:
- Еще одно отличие от Windows - наличие нескольких графических окружений.
- Существует множество графических оболочек.
- Самые распространенные - KDE и Gnome.
- Основное отличие - это внешний вид и наличие эффектов.
- Хотя, многие оболочки имеют свои собственные пакеты прикладных программ.
- А еще разные оболочки имеют разные требования к производительность железа.
- Профессионалы все равно работают в командной строке.
Зачем пользоваться виртуальной машиной?
Для работы с операционной системой необходимо ее установить на компьютер. Так как это основная программа компьютера, процесс ее установки сильно отличается от установки обычной прикладной программы. Он связан с переразметкой жесткого диска, освобождением места для новой операционной системы, перезаписью системного загрузчика. И в любом случае, в каждый конкретный момент на компьютере может работать только одна операционная система. Чтобы переключиться в другую, вам нужно перезагрузить компьютер и запустить другую установленную операционную систему. Если вы всего лишь хотите попробовать новую для вас среду, это может быть слишком обязывающим. Более того, в этом процессе легко сделать что-то не так, а ошибки чреваты потерей информации и повреждением существующих на компьютере программ и данных.
Но есть способ избежать лишнего риска и усилий. Существуют специальные программные средства, которые позволяют запускать одну операционную систему внутри другой, как обычную программу. Для этого они эмулируют так называемый виртуальный компьютер (виртуальную машину). А уже с этой виртуальной машиной можно делать все что угодно, в том числе, установить любую операционную систему, без риска навредить основному, физическому компьютеру. Существует несколько таких программ виртуализации одна из самых популярных - бесплатная VirtualBox от Oracle. Ее мы и будем рекомендовать использовать для установки операционной системы Linux.
Виртуальные машины - это очень полезные программы. И не только чтобы проще можно было пробовать новые операционные системы. Многим разработчикам требуется тестировать работу создаваемой программы в разных операционных системах, разных дистрибутивах или версиях. Часто разработчики создают программы для другой операционной системы, нежели установленная на их рабочем компьютере. Поэтому виртуальные машины - незаменимый инструмент разработки и тестирования программного обеспечения. Есть и еще один способ применения. Некоторые программы настолько сложны в установке, что они распространяются в виде готовой виртуальной машины, чтобы их можно было попробовать, или даже развернуть в виде виртуалки в рабочее окружение. Наконец, виртуальные машины - неотъемлемый инструмент облачных технологий. Многие облачные сервисы работают на виртуальных машинах - так гораздо проще создавать новые конфигурации, а при необходимости - удалить машину и все содержимое.
Еще одно преимущество виртуальных машин - вы можете одновременно запустить несколько и переключаться между ними без перезагрузки. Они действительно работают как обычные программы - можно запустить столько процессов, сколько помещается в оперативную память.
Одной из особенностей работы в виртуальной машине является полная изоляция. Все, что происходит в виртуальной машине никак напрямую не влияет на физическую машину (хост). Это очень полезно в плане безопасности. Например, на виртуальной машине можно проверять работу нестабильных или потенциально опасных программ. Кроме того, если вы что-то испортите в виртуальной машине, можно просто начать все заново. А это важно, ведь в процессе обучения мы не должны бояться совершать незнакомые действия.
Но что, если все-таки хочется при работе в виртуальной машине обратиться вовне? Виртуальные машины можно объединять в сеть. Можно даже создать сеть между виртуальной машиной и основной (хостом). Причем на уровне сетевых протоколов не будет видно никакой разницы между работой с виртуальной или физической машиной. Это делает виртуалки незаменимыми для тестирования сетевых приложений или тестирования настроек сети. Конечно, это подразумевает, что вы можете в виртуальной машине иметь доступ в Интернет. Причем, существует много способов подключения виртуальных машин, о которых мы скажем дальше. И можно, например, эмулировать трансляцию сетевых адресов, если вы хотите сделать такое подключение односторонним.
Еще одно преимущество работы с виртуальными машинами по сравнению с реальными - возможность в любой момент сделать резервную копию всей виртуальной машины на диск, вместе со всеми данными и установленными программами. Существуют специальные форматы файлов, в которые можно сохранить полностью виртуальную машину. Конечно, такая копия (образ) машины занимает порядочно места (это сильно зависит, сколько места занято на диске виртуальной машины), но она позволяет еще больше обезопасить себя от каких-либо сбоев. Ведь вы можете создать копию машины в момент ее стабильной работы, а если произошел сбой или ошибка, просто восстановиться из нее и продолжать работать или экспериментировать. Главное, не забывать вовремя делать бекапы.
Но, конечно, у всего есть недостатки. Главный минус виртуальных машин - большое потребление ресурсов компьютера. Чудес не бывает и здесь, и при запуске операционной системы требования по оперативной памяти, процессору и месту на жестком диске никуда не исчезают. Помните, что любая виртуальная машина потребляет ресурсы машины-хоста. Любые программы, выполняющиеся на виртуальной машине в реальности выполняются на физическом процессоре хоста. Так что обмануть процессор и запустить множество тяжелых приложений, которые в нормальном режиме он не тянет, не получится. Кстати, помимо самих программ в виртуальной машине, в этот же момент на процессоре будет выполняться сама основная операционная система, запущенные в ней программы (в том числе сам менеджер виртуальных машин), так что в целом производительность только несколько снижается, но никак не может быть повышена.
То же самое относится и к памяти. Для работы виртуалки вы должны выделить для нее в реальной оперативной памяти необходимый объем. И еще оставить место для основной системы. А если вы хотите запустить сразу несколько виртуалок - то надо, чтобы суммарный объем их памяти был меньше, чем у вас физически в компьютере. То же можно сказать и про объем жесткого диска. Виртуальная машина хранит все свои файлы на реальном жестком диске, в специальном файле (виртуальном жестком диске). Помните, что для комфортной работы операционной системы нужно как минимум несколько десятков гигабайт пространства. И столько свободного места у вас должно быть на физическом диске.
Но в реальности, многочисленные плюсы от использования виртуальных машин перевешивают недостатки. Как и любой инструмент, виртуальные машины нужно использовать для решения определенных задач. Иногда бывает такое, что они не нужны или вредны. Но для того, чтобы протестировать операционную систему без реальной установки, нет способа лучше.
Выводы:
- Установка операционной системы - это сложный и требовательный процесс.
- Виртуальные машины позволяют работать с ОС без установки на реальный компьютер.
- Это сильно упрощает тестирование разных ОС.
- Можно запускать несколько виртуалок одновременно.
- Виртуальные машины безопаснее - можно не бояться сделать что-то не так.
- Между виртуальными машинами можно создавать сети.
- Можно создавать резервные копии виртуальных машин.
- Недостаток виртуальных машин - небольшое снижение производительности.
Что необходимо для установки?
Давайте рассмотрим процесс установки ОС Linux на виртуальную машину. Для этого на понадобится, во-первых, сам менеджер виртуальных машин. Это программа, которая управляет виртуалками - позволяет создавать, запускать, удалять виртуальные машины, делать их резервные копии и многое другое. Существует несколько альтернативных программ, но мы будем использовать Oracle VirtualBox. Она бесплатная, дружественная к новичками и имеет широкие возможности. Вы ее можете скачать для своей операционной системы с официального сайта.
Во-вторых, нам понадобится дистрибутив операционной системы. В случае с Linux мы тоже можем скачать его с официального сайта. Для обучения мы выбрали дистрибутив Linux Mint. Используем дистрибутив Linux Mint, скачанный заранее, в формате iso.
Для работы в виртуальной машине необходимо:
- Физический компьютер с основной операционной системой (хост).
- Менеджер виртуальных машин. Рекомендуем использовать Oracle VirtualBox.
- Дистрибутив нужной (гостевой) операционной системы.
- Свободное место на физическом жестком диске и оперативная память для работы.
- Примерно полчаса на установку.
Как создать виртуальную машину?
Для начала работы необходимо создать виртуальную машину - то есть программный эмулятор компьютера, в который можно будет установить любую операционную систему и работать с ним изолированно, как с отдельным компьютером.
Для этого можно нажать соответствующую кнопку, или выбрать пункт меню. Указываем имя, тип и версию ОС. Имя используется для того, чтобы отличить одну виртуалку от другой, ведь мы можем создать их любое количество.
Обратите внимание на версию и разрядность создаваемой ОС. Она должна соответствовать дистрибутиву, который вы собираетесь установить на виртуальную машину. В нашем случае, выбираем Linux в типе. В поле “Версия” обратите особе внимание на разрядность операционной системы. Все современные дистрибутивы являются 64-разрядными. Бывает такое, что в списке вы видите только 32-разрядные версии. В такой виртуалке 64 дистрибутив работать не будет (наоборот, кстати, все работает - вы можете установить 32-битную ОС в 64-разрядную машину). Обычно, это свидетельствует о том, что на вашем компьютере отключена аппаратная виртуализация. Посмотрите, как включить ее в документации к BIOS вашей материнской платы.
Указываем объем памяти. Рекомендуется использовать не менее 1 ГБ памяти для современных дистрибутивов. Лучше резервировать как можно больше памяти, но не более половины от физического объема реального компьютера. Если вы планируете запускать несколько виртуалок одновременно, резервируйте еще меньше памяти, чтобы они все вместе поместились в существующую оперативку. Некоторые минималистичные дистрибутивы, или системы без графической оболочки требуют гораздо меньше оперативной памяти. Например, серверный Debian 10 можно запустить с 192 МБ памяти.
Создаем новый виртуальный жесткий диск. Физически он представляет собой большой файл на вашем жестком диске, а виртуальная машина будет видеть его как отдельный жесткий диск.
Указываем тип жесткого диска. Это определяет формат файла, который будет использоваться для хранения виртуального жесткого диска. Разные программы виртуализации используют разные форматы жестких дисков. Значение по умолчанию можно оставить, если Вы не планируете миграцию ВМ на другие программы виртуализации.
Выбираем формат хранения данных (динамический ВЖД или фиксированный). Фиксированный ВЖД сразу займет выбранный вами объем на диске. Динамический жесткий диск будет занимать столько места, сколько занимают на нем фактически записанные файлы виртуальной машины. Рекомендуется всегда выбирать динамический жесткий диск, так как это сильно экономит место на физической машине.
Указываем размер виртуального жесткого диска и путь его хранения. Если не менять его, то по умолчанию будет выбрана папка, в которой хранятся все виртуальные машины Oracle VM VirtualBox. Обратите внимание на путь к файлу ЖД, если у вас ограничен объем системного раздела. Насчет объема - для современных дистрибутивов достаточно от 20 до 50 ГБ для комфортной работы.
Теперь виртуальная машина создана. Однако, рекомендуется еще выполнить одну операцию. Выделяем созданную виртуалку и находим в интерфейсе пункт “Настройки”. Здесь можно поменять все настройки виртуального аппаратного обеспечения. Нам нужна вкладка “Дисплей”.
В настройках, в пункте «Дисплей» увеличиваем объем видеопамяти до 128 Мб и ставим галочку на пункте «Включить 3D-ускорение» для лучшей визуализации системы.
Как установить Linux на виртуальную машину?
Теперь наша виртуальная машина создана и настроена. Все готово к началу установки в нее операционной системы. Процесс установки полностью аналогичен установке на реальный компьютер, ведь используется тот же дистрибутив и та же установочная программа. Но сначала нужно подключить образ дистрибутива к виртуальной машине.
В пункте «Носители» настроек виртуальной машины выбираем образ оптического диска для подключения образа уже существующего диска. Нам нужно выбрать предварительно скачанный образ дистрибутива в формате iso.
В принципе, это можно сделать и при первом старте виртуалки.
Запускаем виртуальную машину.
Мы видим окно операционной системы, запущенной в режиме LiveCD. Все современные дистрибутивы позволяют вам опробовать работу системы и без установки, но при первой же перезагрузке все данные, настройки и программы не будут сохранены. Поэтому, на надо именно установить ОС.
Устанавливаем Linux Mint, выбрав соответствующий ярлык на рабочем столе. Мы увидим программу установки:
Выбираем язык ОС. Эта настройка влияет и на язык инсталлятора, и на язык системы после установки. Также исходя из выбранного языка будет предложена раскладка клавиатуры.
Устанавливаем стороннее ПО для видеокарты и устройств, для более комфортного использования ОС. Программы установки этой специальной галочкой удостоверяются, что вы хотите установить некоторые полезные компоненты и драйверы (например, MP3), которые не являются свободными программами.
Выбираем, как поступить с жестким диском. Если установщик найдет уже установленные ОС, то предложит установить данную систему рядом с ними. Если вы устанавливаете данный дистрибутив как единственный, можно стереть весь диск.
Подтверждаем намерение стереть весь жесткий диск. После этого уже нельзя будет вернуться назад, ведь новая таблица разделов начнет записываться на жесткий диск. Если мы работаем в виртуалке, то можем ничего не бояться, но на реальном компьютере это подразумевает очистку всех данных с этого жесткого диска.
После данного шага программа установки начнет копировать файлы операционной системы на диск. Этот процесс может занять несколько минут. Поэтому в это время программа установки предложит вам внести еще некоторые настройки.
Выбираем часовой пояс в котором мы находимся.
Выбираем необходимую раскладку клавиатуры.
Вносим данные для авторизации как суперпользователь (имя пользователя, пароль), при необходимости отмечаем пункт «Входить в систему автоматически», чтобы не вводить пароль каждый раз. Но это никак не повлияет на безопасность т.к. при выполнении действий, которые могут сильно повлиять на систему, она все равно запросит ввод пароля.
Внимание! Не забудьте пароль суперпользователя! Иначе придется переустанавливать систему с нуля.
После этого система начнет установку, а в конце попросит перезагрузить компьютер для завершения установки.
При включении появится данное окно. Установка успешно завершена!
После этого виртуальная машина перезагрузится и вы сможете начать работу в свежей операционной системе. Очень рекомендуем вам погрузиться в ОС, познакомиться с графическим интерфейсом, попользоваться прикладными программами, то есть всячески освоиться в системе. А мы проложим изучением командной строки Linux.
Основы командной строки
Зачем нужна командная строка?
Командная строка - это основной способ взаимодействия с операционными системами семейства UNIX. Все современные дистрибутивы для общего пользования включают и графическое окружение, которое позволяет обычному пользователю работать в оконном интерфейсе, без освоения командного интерпретатора. Но при необходимость выполнения служебных действий, практически всегда приходится обращаться к командной строке.
Команды позволяют выполнить практически любой действие с операционной системой. В отличие от Windows, в Linux чем более продвинутый пользователь, тем чаще ему приходится залезать в командную строку. Профессионалы работают с ней постоянно. Именно поэтому мы с вами будем изучать ее, чтобы работа в терминале не пугала нас, а наоборот, мы чувствовали себя там комфортно.
Команды значительно более гибкие и мощные, нежели графические оболочки. Графический интерфейс сильно ограничивает ваши возможности по взаимодействию с системой: вы можете выполнить только определенные действия и только в определенном порядке. Используя команды оболочки, можно выполнять любые действия, предусмотренные командной оболочкой, запускать любые программы и комбинировать их в любой последовательности
Сейчас мы будем употреблять термины “командная строка”, “оболочка”, “терминал”, “bash” и “командный интерпретатор” как синонимы. Между ними есть определенная разница, но в самом начале, чтобы не путаться, можно не обращать на нее внимания. Все эти термины относятся к способу взаимодействия пользователя с системой через ввод текстовых команд. Вообще, в Linux существует много разных командных интерпретаторов, построенных на основе разных языков программирования. Но с большим отрывом самый популярный - это bash. Именно про него мы и будем говорить дальше.
Командная строка работает по принципу REPL. То есть вы вводите команду, нажимаете <Enter>, в этот момент она исполняется, результат выводится на экран и вы можете вводить следующую. Однако, следует помнить, что некоторые команды являются интерактивными, то есть они выполняются, пока пользователь их явно не прервет.
Недостаток командной строки - отсутствие интуитивного интерфейса. Для совершения определенного действия необходимо знать команду, которая это действие совершает. В графическом интерфейсе можно сориентироваться, даже если вы не знаете точно что нужно делать, вы можете догадаться по названиям пунктов меню, пиктограмм, расположению на экране. В командной строке не так, только вы и черный экран. Поэтому сначала надо изучить хотя бы основные команды, а уже потом начинать работать. Сразу скажем, что работа в командной строке поначалу может показаться непривычной и даже неудобной. Это нормально, ощущение дискомфорта уйдет в процессе привыкания. На самом деле совершать определенные действия в командной строке гораздо удобнее, чем тыкать в кнопочки. Недаром большинство профессиональных администраторов, программистов и ученых предпочитают работать текстом, даже если есть графический инструмент для нужного действия.
Самое главное достоинство командной строки - удобство автоматизации. Командная строка - это еще и язык программирования. Если вы часто совершаете однотипные действия в командной строке вы можете просто записать их в скрипт и выполнять его. Ведь каждая команда - это по сути дела строчка в программе. В скриптах вы можете делать еще кучу всего - использовать условия, циклы, функции. Вы можете обращаться к командной строке из своих программ, на любом языке программирования. То есть при помощи командной строки вы можете производить комплексную автоматизацию. А вот автоматизировать действия в графическом интерфейсе наоборот, невероятно сложно.
Выводы:
- Для Linux командная строка - основной способ взаимодействия с пользователем.
- Самый распространенный интерпретатор командной строки в Linux - bash.
- Командная строка пугает, это нормально.
- Самое главное достоинство командной строки - удобство автоматизации.
Как туда попасть?
Изначально, терминал - это интерфейс ввода/вывода, состоящий из физических устройства ввода (клавиатура) и вывода (дисплей). Терминал предназначен исключительно для ввода информации и ее отображения на экране. Терминалы бывают физическими (реальными), виртуальными и псевдотерминалами (т.е. программами, которые “притворяются” терминалами). Не останавливаясь на подробностях работы реальных терминалов отметим, что при работе чаще всего вы будете использовать виртуальные терминалы и эмуляторы.
В системах Linux еще существуют несколько виртуальных терминалов. По умолчанию Линукс представляет доступ к шести текстовым терминалам, которые соответственно называются tty1, tty2 и т.д. Переключение между ними осуществляется сочетанием клавиш Ctrl+F1, Ctrl+F2, Ctrl+F3 и т.д. В современных дистрибутивах в одном из терминалов (обычно под номером 7) загружена графическая оболочка и этот терминал открывается по умолчанию. При загруженной графической оболочке открытие терминалов и переключение между ними производится клавишами Alt+Ctrl+Fn, (где n - номер терминала). Сама графическая оболочка будет доступна по Alt+Ctrl+F7. Для работы в каждом новом терминале вы должны прежде всего авторизоваться. Таким образом, в системе одновременно могут работать несколько различных пользователей, каждый в своем терминальном сеансе.
Если вы используете десктопный дистрибутив, то в одном из виртуальных терминалов запускается графическая оболочка. Именно он обычно и запускается по умолчанию. Поэтому при старте системы вы видите графический интерфейс. Но, кстати, для самой операционки, это просто такая специальная программа, запускаемая автоматически при загрузке в седьмом (обычно) виртуальном терминале
Самый простой способ - открыть программу-эмулятор терминала. В любой графической системе есть такая программа, которая открывает командную строку прямо в окне. Это удобно, так как не надо переключаться в другой терминал и логиниться там. Вы уже работаете в сеансе пользователя. Кроме того, такие программы-эмуляторы обычно содержат дополнительные функции, например, подсветку синтаксиса, масштабирование, прозрачность, вкладки.
Выводы:
- Терминал в старой терминологии - конечное устройство ввода-вывода пользователя.
- При запуске Linux создает несколько виртуальных терминалов.
- Для того, чтобы начать работать с терминалом, нужно, залогиниться.
- Существуют графические программы-эмуляторы терминала.
- За разными терминалами могут работать разные пользователи. А может и один.
Что вы видите на экране командной оболочки?
Когда вы открываете терминал, необходимо найти текущее положение курсора. Он обычно обозначается мигающей вертикальной чертой или знаком подчеркивания. Курсор обозначает то место, где будут появляться символы при их вводе с клавиатуры.
Все, что отображается в строке непосредственно перед курсором еще до того, как вы начали что-то вводить - это приглашение командной строки. Это строка определенного вида, которая отображается каждый раз перед вводом команды. Даже само ее название означает, что она приглашает вас ввести какую-нибудь команду. Если вы привыкли к виду приглашения командной строки, оно поможет вам ориентироваться в терминале. Кроме того, приглашение содержит некоторую полезную информацию.
Вид приглашения зависит от используемой оболочки и часто может настраиваться пользователем. Но мы пока рассмотрим стандартное приглашение оболочки по умолчанию. Оно обычно состоит из четырех частей.
Самым первым отображается имя текущего пользователя.
Далее после символа @ идет название текущего хоста (экземпляра операционной системы). Имя хоста обычно употреблаяется для обозначения текущего компьютера. Если вы работаете только за одним компьютером, вам может показаться, что это не очень полезное знание. Но в работе системного администратора часто возникает необходимость подключаться удаленно к разным хостам. И забыть, в консоли какого компьютера ты сейчас находишься - это не так странно, как может показаться.
Далее отображается имя текущей папки. Очень важный факт про консоль - в ней мы не можем находиться “просто так”, мы всегда находимся в какой-то папке на диске. Это имя “текущей директории”. Мы можем перемещаться между папками, или обращаться к файлам других папок, но в любой момент работы название текущей директории отображается в приглашении, чтобы мы всегда могли просто посмотреть, где мы сейчас.
Если вы только запустили терминал, то скорее всего находитесь в домашней папке. Она обозначается для краткости специальным символом ~. У каждого зарегистрированного пользователя есть своя домашняя папка.
Выводы:
- Курсор в терминале - это то, куда вводить буквы с клавиатуры.
- Приглашение командной строки - это полезная информация о вашем состоянии.
- В приглашении по умолчанию отображается имя пользователя, имя хоста и текущая папка.
- В терминале можно перемещаться по папкам.
- Вид терминала и приглашения можно настраивать.
Какие команды изучить в первую очередь?
Для работы в консоли необходимо знать команды. Интуитивным интерфейсом здесь не обойдешься. Изучение команд - это вопрос времени и практики. Чем больше вы работаете в системе, тем лучше ее знаете. Поэтому настоятельно рекомендуется как можно чаще пользоваться командной строкой в процессе изучения системы. Так вы сможете выработать привычку и освоить базовые команды не путем заучивания, а естественно.
90% вашего времени вы будете использовать самые базовые команды - переходы по папкам, создание и перемещение файлов и аналогичные. В этом разделе мы перечислим десять самых базовых команд, с которых удобнее всего начинать изучение командной строки и языка bash:
- pwd (print working directory) - отобразить текущую директорию.
- ls [<dirname>] (list) - вывести список файлов и папок в текущей директории. В linux директории считаются наряду с обычными файлами. ls Desktop - вывести список файлов в другой директории.
- cd [<dirname>] (change directory) - изменить текущую директорию. Для удобства, чтобы не указывать у файлов и папок постоянно путь к ним из текущей директории, можно изменить текущую директорию на ту, с которой вы работаете сейчас.
- cat <filename> (concatenate) - вывести на консоль (в стандартный вывод) содержимое текстового файла, имя которого передано как параметр. Можно передать несколько файлов, тогда они “склеятся”
- mkdir <dirname> (make a directory) - создание директории (папки). Параметром передается имя новой папки. Папка создается в текущей директории. Если здесь уже есть файл или папка с таким именем, то команда завершится с ошибкой.
- touch <filename> - вообще данная команда обновляет дату и время последнего доступа к переданному файлу. Если такого файла нет, то файл будет создан. Чаще всего эта команда используется для создания новых пустых файлов.
- _rm <filename> _(remove) - удаление файла. При помощи опций данная команда может удалять и директории. Обратите внимание, что удаление происходи насовсем. Никакая корхина в командной строке не предусмотрена. Поэтому этой командой надо пользоваться осторожно.
- cp <src> <dest> (copy) - копирование файла. Данной команде надо передать два параметра - имя копируемого файла и имя файла (возможно в другой папке), куда его необходимо скопировать. Очень часто в подобных случаях действует правило откуда - куда (сначала указывается имя исходного файла или папки, а потом - целевой папки или файла).
- mv <src> <dest> (move) - то же самое, только происходит не копирование, а перемещение файла. Если производить перемещение в ту же папку, где файл и был, он запишется с другим именем. Часто эта команда используется именно для переименования файлов.
- man <command> (manual) - получение справки по команде. Это, наверное, самая полезная команда, ей нужно пользоваться как можно чаще. Встроенная справка (мануал) - это самый главный, полный и актуальный источник информации о командах и их параметрах.
Как выполняются команды?
Каждая команда при своем выполнении (в тот момент, когда мы нажимаем Enter после самой команды) запускает определенный алгоритм, чаще всего - целую отдельную программу. Некоторые команды завершаются моментально (pwd), а другие (ping) - выполняются продолжительное время. Кроме того, некоторые команды запускают интерактивные программы (top). При работе с терминалом важно понимать, сколько будет выполняться та или иная команда.
Командная строка работает по принципу REPL (read - execute - print - loop), то есть после ввода команда тут же начинает выполняться, причем все, что эта команда выводит на печать отображается в том же терминале, в котором эта команда была введена. Поэтому для того, чтобы разные команды не смешивались, пока одна команда выполняется, она полностью блокирует собой этот терминал. То есть вы не сможете вводить другие команды, пока первая не завершится. После завершения команда освобождает занятый терминал, в нем автоматически снова выводится приглашение командной строки и цикл завершается, вы можете вводить следующую.
Большинство простых команд, таких как копирование файлов, создание директорий и другие, созданные для совершения одного определенного действия выполняются практически моментально. Большинство из этих команд - это стандартные служебные программы из проекта GNU, которые адаптировал существовавшее в UNIX рабочее окружение. Именно поэтому стандартные команды общие для всех дистрибутивов Linux, да и в других UNIX-подобных операционных системах они более-менее такие же.
Для таких простых стандартных команд часто действует определенная философия - команда должна принимать входную информацию либо через параметры, либо читать из переданного файла, и выводить информацию в консоль только чтобы сообщить об ошибке. Таким образом, чаще всего данные команды завершаются мгновенно и не выводят ничего. И именно такое поведение свидетельствует о том, что необходимые действия совершены успешно. Но новички часто думают, что если команда ничего не вывела, то она по каким-то причинам не отработала. В UNIX зачастую это не так.
Конечно, существуют команды, цель которых отобразить информацию в терминале. Типичный пример - команда ls. Она выводит список файлов в каталоге. Естественно, она должна выводить его на свой стандартный вывод. Еще, существуют команды, которые работают долгое время и выводят много информации в терминал. Например, так устроена команда find - поиск файлов по имени, команда ping вообще будет работать неопределенно долго, пока пользователь сам явне не прервет ее.
Но в любом случае, такие неинтерактивные команды стараются как можно реже запрашивать информацию у пользователя через стандартный ввод. Это сделано специально для того, чтобы команды можно было объединять в конвейеры или скрипты, то есть программы. Строить программу их интерактивных блоков довольно сложно, а из простых “кирпичиков” - очень легко. И зачастую администраторам приходится писать такие служебные скрипты, что гораздо удобнее, чем каждый раз вводить несколько команд руками.
Отдельно надо сказать о так называемых интерактивных командах. Их очень немного, но они работают в совершенно особом режиме. После запуска они полностью занимают терминал, обычно еще и полностью очищают окно. Зачастую у них присутствует псевдографический интерфейс. И в процессе своей работы пользователь может взаимодействовать с ними при помощи их собственных сочетаний клавиш, каких-то внутренних команд до тех пор, пока не выйдет из этой программы и не вернется в привычный терминал. Примерами таких интерактивных команд может служить текстовый редактор nano или программа мониторинга системы top.
Выводы:
- Команды выполняются по принципу REPL (как в IPython, например).
- В процессе выполнения команды, все что она выведет будет отображаться в терминале.
- После завершения команды вы опять увидите приглашение. Можно вводить следующую.
- Пока выполнение команды не закончено, терминал является заблокированным.
- Существуют интерактивные и неинтерактивные команды.
- Большинство команд терминала - это стандартные программы из проекта GNU.
- Если команда ничего не вывела и завершилась - значит все прошло успешно.
Какие приемы работы с командной строкой существуют?
Для успешной и быстрой работы в командной строке существует большое количество сочетаний клавиш. Ознакомьтесь с ними на практике и попробуйте запомнить наиболее полезные:
<Ctrl>+<C> - завершение текущей команды; эта комбинация клавиш используется если вам нужно аварийно прервать выполняющуюся в терминале команду.
<Ctrl>+<D> - выход из текущего сеанса (разлогин);
<Tab> - подсказки и множественное дополнение; при вводе команды, имени файла не обязательно вводить с клавиатуры название целиком. Можно ввести первые несколько символов и нажать символ табуляции. Если есть только одна команда или один файл с таким именем, оно заполнится автоматически. Если же таких несколько, то при повторном нажатии табуляции вам будет предложены все возможные варианты и вы сможете продолжить ввод с клавиатуры.
clear, <Ctrl> + <L> - очистка экрана (перемотка вперед); можно использовать команду или сочетание клавиш, они работают совершенно идентично.
<↑> - переход к предыдущей команде. Вообще с помощью стрелок вверх и вниз можно перемещаться по истории ранее введенных команд. Это очень экономит время, если вы повторяете команды или изменяете уже введенные.
<Ctrl> + <A> - переход к началу команды; аналогично клавише <Home>.
<Ctrl> + <E> - переход к концу команды; аналогично клавише <End>.
<Alt> + <F>, <Alt> + <B> - переход к следующему и предыдущему слову в команде;
history - вывод истории команд в текущем сеансе;
sudo apt update ; apt upgrade - выполнение нескольких команд из одной строки;
<Ctrl> + <R> - поиск команды в истории; это бывает полезно, если вы уже вводили похожую команду, но не можете ее найти с помощью стрелок.
Это только самые основные комбинации, работающие в любом терминале Linux. Кроме этого разные оболочки могут вводить собственные сочетания клавиш, сокращения команд и другие фишки для экономии времени. Использование этих возможностей совершенно необязательно, но делает работу в командной строке гораздо быстрее и удобнее.
Какова структура команды?
Любая команда оболочки состоит из одной или нескольких частей. Они разделяются пробелами. Основная часть, которая всегда идет первой при вводе команды - ее имя. Имя команды - обязательная часть. Нельзя выполнить команду не зная ее имени. Имя команды- чаще всего это название файла с исполняемым кодом этой команды. Например, ls - это имя команды. Точно так же имена других команд - cp. ps. top. firefox, многие другие.
Некоторые команды вполне обходятся только именем. Уже известная нам команда pwd - это команда, состоящая только из имени. Но у большинства команд кроме имени предусмотрены еще и другие элементы. Ключи или опции - это строки, которые могут быть указаны или отсутствовать и этим каким-то образом модифицировать порядок работы команды. У каждой команды свои опции, но в мире linux есть соглашения о стандартных опциях и их значениях.
Опции, они же ключи, - это необязательные строки, наличие или отсутствие которых при вызове команды как-то модифицирует ее поведение. Возьмем для примера команду ls. Будучи выполненной без опций она выводит список имен файлов, содержащихся в директории. Но у этой команды есть опция -l. Ее можно указать при вызове команды так: ls -l. То есть опции идут после имени команды через пробел. В таком виде эта команда также выведет список файлов в папке, но уже в табличном виде и с некоторой дополнительной информацией. Еще у команды ls есть опция -a - она заставит команду включить в список скрытые файлы, которые по умолчанию не отображаются.
Можно заметить, что опции состоят из одной буквы после дефиса. Это так называемый короткий формат опций. Есть еще и длинный. Например, опция -a полностью выглядит как –all. Короткие опции начинаются с одного дефиса и состоят из одной буквы. А длинные - начинаются с двух дефисов и состоят из одного или нескольких слов. Можно написать коротко, а можно и длинно, это одна и та же опция. Естественно, длинными опциями пользуются редко. Но иногда у команды может быть длинная опция, а короткого варианта нет, или наоборот.
Аргументы есть не у всех команд, указываются обычно после имени и опций. С помощью аргументов можно передать команде какую-то информацию на вход. Примерно так же, как мы передаем аргументы в функцию. Например, у той же команды ls есть необязательный аргумент - имя папки. Без аргументов эта команда отображает содержимое текущей папки, но можно заставить ее отобразить содержимое любой папки указав имя этой папки в качестве аргумента. Например, при вводе команды ls / будет отображены элементы, находящиеся в корневой папке, независимо от того, где мы сейчас находимся.
Аргументы бывают обязательные и необязательные в предыдущем примере аргумент можно и не указывать. А вот у команды cp есть целых два обязательных аргумента. Без них команда вообще не будет работать. Первый аргумент - это имя копируемого файла, а второй - имя папки, куда мы его копируем. Кроме этого у команды cp есть еще и опции.
Вообще, у каждой команды свой набор аргументов и опций и правил, в каком порядке нужно их указывать. Это называется синтаксис команды. Синтаксис команды задает разработчик команды при ее создании. Все, что мы описывали ранее - это не более чем набор общепринятых правил, традиционных для стандартных команд Linux. Но помните, что разработчики команд могут и проигнорировать эти правила. Например, в командах операционной системы BSD принято опции делать без дефиса. Поэтому у команды ps опции указываются именно так (ps a, ps aux). Самый лучший способ узнать синтаксис команды - прочитать официальную справку по ней.
Выводы:
- У любой команды есть имя - название программы - его вводить обязательно.
- У команды могут быть обязательные или необязательные аргументы и необязательные опции.
- Опции (ключи) могут модифицировать выполнение программы.
- Опции бывают длинные и короткие. Короткие начинаются с одного дефиса, длинные - с двух.
- Опции команды можно комбинировать, а короткие опции можно объединять.
- Аргументы команды - это как аргументы функции - служат для указания входных данных.
- Синтаксис команды зависит от ее разработчика и про него можно почитать в справке.
Где получить помощь?
При работе с командной строкой приходится постоянно обращаться к документации. Невозможно постоянно держать в голове все команды, нюансы их использования, синтаксис. Поэтому в саму командную строку встроена официальная документация по системным командам. Эта официальная документация называется мануалы, man. Эти мануалы пишут разработчики команд и держат в актуальном виде.
Для доступа к официальным мануалам существует команда man. Есть еще несколько информационных команд (например, info), которые могут различаться от дистрибутива к дистрибутиву. Но man до сих пор остается основным источником информации про синтаксис команд. Посмотреть справку по команде, например, pwd, можно так:
1
$ man pwd
Мы получим полную и актуальную справку (мануал) по команде pwd, включая все возможные ее ключи и опции. Команда man может показать мануал по любой команде bash.
Рекомендуется пользоваться именно этой документацией потому, что синтаксис команд может меняться. В разных дистрибутивах, например, могут быть немного различающиеся наборы стандартных команд. Также синтаксис меняется от версии к версии, с изменением и доработкой самих команд и их окружения. Поэтому именно мануалы покажут вам справку именно по вашей команде именно нужной версии.
Конечно, для того, чтобы почитать мануалы, нужно знать само имя команды. К счастью, мануалы - не единственный источник информации. По всем распространенным командам достаточно легко получить справку и примеры использования в интернете. Существует большое количество тематических справочников, форумов, сообществ пользователей и разработчиков. И чем популярнее дистрибутив вы используете, тем больше про него будет информации. Учебники, справочную литературу тоже не стоит сбрасывать со счетов.
Выводы:
- Для всех стандартных команд существует официальная справка.
- Команды и их синтаксис могут меняться в разных дистрибутивах и версиях.
- Первый источник информации - команда man.
- Также информацию можно получить в интернете, на форумах, в учебниках.
Управление файлами в Linux
Как организована структура каталогов Linux?
Одно из самых базовых понятий для любой операционной системы - это файл. Обычно, мы подразумеваем под файлом некоторое количество информации, хранящееся в постоянном хранилище (на диске), имеющее имя. Однако, Linux построена таким образом, что практически любой объект, с которым работает операционная система является файлом. Привычные нам файлы здесь называются обычными, но кроме них еще существует несколько типов, которые Linux также называет файлами, хотя с точки зрения обывателя они воспринимаются как что-то другое. Например, любое физическое устройство имеет в операционной системе особое логическое представление в виде файла в определенной папке. Вы можете зайти в эту папку и просмотреть список устройств, зайти в конкретный файл и увидеть свойства этого устройства. Однако это не просто статическая информация, хранящиеся на диске. Это динамический объект операционной системы, на диске он не занимает никакого места, его там нет. При обращении к этому файлу происходит вызов особых функция ядра ОС. Кроме этого для Linux папки, ссылки (аналог ярлыков), даже процессы самой ОС - тоже файлы.
Это сделано для унификации доступа к различным ресурсам операционной системы. Зачем придумывать особый механизм отправки документов на печать, если можно просто записать текст в файл принтера. Такой подход может немного запутывать сначала, но довольно быстро привыкаешь, что для Linux практически любой объект - это файл. Но довольно часто приходится оговариваться, имеется в виду файл в узком смысле, обычный файл, как информация на диске с именем, или в широком смысле, как концепция операционной системы.
Понятно, что файлов в каждой отдельно взятой операционной системе может быть огромное количество. И только малая часть из этого - файлы пользователей. Кроме этого существуют служебные файлы операционной системы, конфигурационные файлы, файлы системных и прикладных программ, псевдофайлы, которые мы обсуждали ранее. Если бы файлы имели только имя, ориентироваться в этой куче было бы решительно невозможно. Для этого и нужны папки. Папки, они же директории или каталоги, служат для объединения произвольного количества файлов во множество в некоторым именем. Неудивительно, что для Linux папка - это тоже файл, его особый тип. Фактически, папка - это файл, содержащий список других файлов, которые в считаются “в этой папке”. Так что сами папки тоже можно “класть” в другие папки. Таким образом, структура файлов и папок образует иерархическую структуру- дерево файловой системы.
А что происходит с файлами и папками, которые не находятся в других папках? В Linux существует специальная корневая папка. Она обозначается прямым слешем (/). Она является вершиной этой иерархии, корнем дерева, единой точкой отсчета для всех других папок на данном компьютере. Так что любой файл находится в какой-то папке, либо в в корневой директории, либо в каком-то каталоге, который может быть в другом каталоге, и так далее, но в итоге все равно в корневой папке.
Таким образом, для того чтобы указать на какой-то конкретный файл нам недостаточно его имени, еще нужен его “адрес” - путь от корневой директории по (возможно нескольким) вложенным папкам до самого файла. Такой адрес называется “путь” к файлу. У каждого файла, который находится в операционной системе существует один и только один абсолютный путь от корневой директории. Заметьте, кстати, что конкретный файл не может находится в нескольких разных папках одновременно, то есть в двух местах сразу.
Все пути в Linux отсчитываются от корня файловой системы, обозначаемого прямым слешем /. Корень всегда один, не существует никаких букв дисков, как в Windows. Корень еще называют корневой папкой или директорией, так как именно в нем содержаться все другие папки. В пути к файлу последовательно указываются папки, которые нужно пройти, чтобы найти файл, а затем - имя самого файла. Имена папок и файла разделяются прямым слешем. Так, в пути /etc/passwd первый символ / обозначает корневой каталог, etc - имя папки в корневом каталоге, passwd - имя нужного нам файла. Обратите внимание, что в Linux не принято использовать расширения в именах файлов (но, впрочем, и не запрещено, так что ими многие пользуются).
В корневой папке обычно не хранятся обычный файлы, только папки. Причем папки корневой директории несут особую смысловую нагрузку. Например, каталог /etc/ хранит текстовые конфигурационные файлы самой операционной системы, а каталог /dev/ - те самые псевдофайлы-устройства, о которых мы уже упоминали. Этот набор стандартных папок немного меняется от дистрибутива к дистрибутиву, но основные остаются общими. Существует даже специальный стандарт - Lnux FHS (filesystem hierarchy standard), который и описывает набор и предназначение стандартных папок корневой директории.
Выводы:
- Для Linux практически любой объект - файл.
- В Linux файлы организованы привычным образом в папки и подпапки, образуя иерархическую структуру - дерево файловой системы.
- Корневая папка / - это единая точка отсчета для всех папок.
- Каждый файл имеет один и только один адрес в файловой системе - абсолютный путь к нему.
- Для разделения имен папок и файлов в Linux используется только прямой слеш.
- В Linux принята стандартная структура файлов.
Что такое относительные и абсолютные пути?
Для того, чтобы операционная система, программы и пользователи могли работать с файлами и обращаться к ним у каждого файла должен быть уникальный идентификатор. Но просто имени недостаточно, потому что могут существовать несколько файлов с одинаковыми именами. Поэтому операционная система обращается к файлам используя пути.
Путь к файлу можно задавать двумя способами - абсолютным или относительным. Абсолютный путь начинается с символа / и отсчитывается от корневого каталога. Относительный путь начинается с имени папки и отсчитывается от текущего каталога. Это сделано для удобства указания пути, чтобы не повторять одно и то же, если вы работаете в данный момент преимущественно с файлами в одной папке.
При указании путей к файлам можно пользоваться некоторыми сокращениями. Вы можете использовать два специальных имени: точку (.), означающую текущую директорию, и пару точек (..), означающую родительскую директорию текущей директории. Также вы можете использовать символ тильды (~), который означает вашу домашнюю директорию, и сочетание ~username, означающее домашнюю директорию пользователя с именем username.
Помните, что нужно четко представлять себе, где лежит тот или иной файл для того, чтобы с ним работать. Часто /etc/passwd и etc/passwd - это два совершенно разных файла в разных местах. Поэтому нужно всегда четко представлять себе расположение нужного вам файла относительно текущей директории и использовать относительные или абсолютные пути правильно, не путая их.
Может показаться, что для простоты и надежности следует всегда использовать абсолютные пути. Однако относительный формат был придуман не просто так. С одной стороны, он добавляет удобства и скорости работы в командной строке, так как абсолютные пути могут быть очень длинными и в них легко ошибиться ил опечататься. Именно для этого придумано понятие текущей папки - это просто название папки, которое автоматически поставляется в относительные пути, чтобы получить абсолютный.
Но у относительных путей есть и еще одно применение. Представьте, что вы пишите скрипт или программу, которая работает с файлами в какой-то своей папке. У рабочей папки программы может быть разное расположение на разных компьютерах, или оно может поменяться если вы просто перенесете программу в другое место. Если вы используете абсолютные пути, то при каждом таком переносе программа просто сломается. То есть абсолютные пути слишком зависят от положения точки отсчета и делают программу непереносимой. Использование же относительных путей решает эту проблему - при переносе программы в другое место вся внутренняя структура папок сохранится и программа будет работать корректно без необходимости менять исходный код.
Выводы:
- У каждого файла в системе есть один уникальный адрес.
- Следует различать абсолютные и относительные пути к файлам.
- Абсолютный путь отсчитывается от корня файловой системы и не зависит от того, где находится пользователь.
- Относительный путь отсчитывается от текущей директории пользователя.
- Смысл переходить по директориям как раз в том, чтобы относительные пути были короче.
- Лучше использовать относительные пути в программах и скриптах для переносимости.
Какие виды файлов существуют?
Мы уже говорили раньше, что UNIX-подобные операционные системы представляют многие разные объекты, с которыми приходится работать в виде файлов. Это удобно и для операционной системы и для пользователей. Давайте кратко перечислим основные виды файлов и их зачем они нужны. Посмотреть тип файла всегда можно командой ls -l. В строке соответствующей файлу самый первый символ, один из атрибутов файла, указывает его тип.
Обратите внимание, что тип файла - это не расширение, как в Windows. Там расширение является обязательным элементом имени файла и показывает операционной системе, какой программой его нужно “открывать”, то есть обрабатывать его содержимое. Другими словами, расширение файла характеризует характер содержимого этого файла. В UNIX принята другая система. Изначально вы запускаете программы командами в терминале, то есть явно указываете название программы, которая должна запуститься и обрабатывать тот или иной файл. Поэтому расширение для операционной системы необязательно. Но с распространением графических интерфейсов, когда пользователи привыкают открывать файл щелчком мыши, практика использования расширений в именах файлов начинает распространяться и на Linux. Так почти все графические окружения поддерживают ассоциации файлов с программами через расширения, как в Windows. Кроме того, это удобно пользователю, так как глядя на имя файла он может предположить, что за информация там хранится.
Самый простой тип файла так и называется - обычный файл. Он помечается символом дефиса (-). Это именованная область данных на носителе, которая может содержать произвольную информацию. Часто чтобы не путаться этот тип файлов называется документом, хотя в Linux к обычным файлам относятся и мультимедийные, и офисные, и исполняемые файлы программ и файлы с исходным текстом программ и скриптов. Короче говоря, все, что в Windows называется файлом, в Linux - обычный файл.
Мы уже говорили, что папки в UNIX - это тоже файлы. Это второй по распространенности тип файлов - папки, директории или каталоги. Это все разные названия одного и того же. Они служат для организации файлов и упрощения навигации по файловой системе. В Linux папки - это по сути просто специальный файл, который содержит список других файлов, которые считаются “лежащими” в этой папке. Файл типа каталога помещается символом d.
Еще один тип файлов - это ссылки. Они помечаются символом l (link). Ссылки вместо пользовательских данных содержат путь к другому файлу. Именно этот файл будет открываться при обращении к ссылке. Этот механизм работает аналогично ярлыкам в Windows. Ссылки нужны для более удобной организации файлов, иногда бывает полезно иметь доступ к одному и тому же файлу из разных мест, то есть папок. Можно, конечно, файл просто скопировать, но копирование файлов расходует место на диске и при изменении одной копии вторая останется старой. Надо понимать, что при удалении или перемещении исходного файла все ссылки на него, если такие есть, перестанут работать. Такие ссылки называются битыми.
Надо сказать, что все это относится к так называемым символическим, или мягким ссылкам. Именно они работают подобно ярлыкам. Но в Linux есть и другой тип ссылок - жесткие ссылки. Жесткие ссылки работают совершенно по-другому и на другом уровне. По сути дела - это просто другое имя одного и того же файла. Любой файл имеет как минимум одну жесткую ссылку на него - это само имя файла. Но вы можете создать новую ссылку. И для операционной системы все жесткие ссылки на файл являются равноправными, в отличие от символических, где есть сам файл и ссылки на него. Даже если вы удалите файл по первоначальному имени, если на него есть еще жесткие ссылки, он проложит существовать. Файл удаляется тогда, когда перестает существовать последняя жесткая ссылка на него.
Еще один тип файлов в UNIX - это специальные файлы устройств. Их можно распознать по символу c или b в атрибутах.
Выводы:
- Обычные файлы содержат информацию в виде данных или программного кода.
- В Linux тип файла - это не расширение, они вообще не обязательны.
- Директории, каталоги или папки - это список файлов.
- Ссылки используются для удобства нахождения файла в нескольких папках одновременно.
- Символические ссылки - это как ярлыки в Windows.
- Жесткие ссылки - это два адреса одного и того же файла.
- Сокеты используются для взаимодействия между процессами.
Как работать со ссылками в консоли?
Самые распространенные операции с обычными файлами и каталогами мы уже изучали ранее. Сейчас поговорим о более специфичном для Linux явлении - ссылках. Для того, чтобы поглубже понять, как работают ссылки, нам надо сначала разобраться, что такое inode.
inode – это объект файловой системы, содержащий информацию о владельце/группе, которым принадлежит файл или каталог, его права доступа к нему, его размер, тип файла, timestamp-ы отражающие время модификации индексного дескриптора (ctime, changing time), время модификации содержимого файла (mtime, modification time) и время последнего доступа к файлу (atime, access time) и счётчик для учёта количества жёстких ссылок на файл. Каждый inode имеет собственный номер, который присваивается ему файловой системой в момент её создания (форматирования). По сути, inode - это уникальный численный идентификатор файла в файловой системе. Узнать inode обычному пользователю не требуется, но если интересно, можно воспользоваться опцией -i команды ls:
Иногда может возникнуть “странная” ситуация: с одной стороны – df или du будут говорить, что свободное место на диске есть, а с другой стороны операционная система будет утверждать, что “No Space Left on Device”. Одна из вероятных причин как раз явлется полное использование пула inode, выделенных для раздела на жёстком диске, т.к. кол-во inode фиксировано и задаётся во время создания таблицы раздела. Проверить общее, занятое и доступное количество inode можно с помощью df и опции -i:
Создать жесткую ссылку на файл можно командой ln. синтаксис команду такой:
1
$ ln целевой_файл файл_ссылка
при этом можно создать ссылку как на файл из этой же папки, так и на любой другой. Для этого надо воспользоваться относительными или абсолютными путями к файлу.
Кстати, вы не можете создать жесткую ссылку на файл, к которому у вас нет доступа на запись, ведь формально вы вносите изменение в inode. А для этого вы должны обладать соответствующими правами.
Обратите внимание, что у исходного файла и у ссылки - один и тот же inode. То есть по сути для операционной системы это один и тот же файл с двумя разными именами. Кстати, даже изначальный файл строго говоря - это жесткая ссылка на свой собственный inode. Поэтому жесткая ссылка - это не как ярлык к файлу, это именно два разных имени одного и того же файла.
Поэтому при удалении исходного файла жесткая ссылка на него никуда не девается и даже продолжает работать. Сам файл, то есть inode, удалится только тогда, когда на него будет удалена последняя жесткая ссылка. Именно для этого у каждого файла отслеживается количество жестких ссылок на него. Это число вы можете видеть в подробном выводе команды ls после прав доступа и перед ником владельца файла.
А вот символические ссылки, symlink’и, работают именно как ярлыки Windows, то есть как указатели на файл. Создать символическую ссылку можно командой
1
$ ln -s целевой_файл файл_ссылка
В терминале символические ссылки отображаются по -другому. Во-первых, обратите внимание на тип файла. Это уже символ ‘l’ - то есть ссылка. Во-вторых, inode изначального файла и ссылки отличаются. И в-третьих после имени ссылки указывается имя файла, на который она ссылается. Причем, если удалить исходный файл, ссылка перестанет работать. Она станет так называемой “битой ссылкой”, то есть ссылкой, которая никуда не ведет. В терминале с цветовой подсветкой такие ссылки дополнительно отображаются красным цветом для привлечения внимания.
Выводы:
- Создание, удаление, копирование, перемещение файлов изучалось ранее.
- inode - это уникальный численный идентификатор файла в файловой системе.
- inodes могут закончиться на разделе жесткого диска, тогда вы не сможете создать файл, даже если места для него хватает.
- Создать жесткую ссылку на файл можно командой ln.
- Файл существует до тех пор, пока на него есть хотя бы одна жесткая ссылка.
- Создать символическую ссылку на файл можно командой ln -s.
- Если удалить символическую ссылку исходный файл останется нетронутым.
- Если удалить сам файл, то все символические ссылки на него станут недействующими.
Что такое перенаправление ввода-вывода?
Как мы уже говорили, все консольные команды Linux являются текстоцентричными - они принимают на вход текстовый поток и выдают в качестве результата тоже какой-то текстовый поток. Текстовый поток - это по сути, одна или несколько строк текстовой информации, которая читается последовательно. Текстовые потоки очень похожи на текстовые файлы - у них есть начало, определенный конец. Но в отличие от файлов потоки читаются только последовательно, символ за символом, пока не будет достигнут специальный символ, обозначающий конец потока.
У любого процесса в операционной системе Linux существует три стандартных текстовых потока, ассоциированных с ним. Они создаются и связываются в процессом самой операционной системой. Поэтому они называются стандартными потоками. Это все справедливо, в частности, для всех команд терминала.
Стандартный поток ввода (STDIN) при работе пользователя в терминале связывается с клавиатурой. При работе команды она может считать текст их с STDIN. в таком случае выполнение команды приостанавливается и она ждет ввода пользователем текста в терминале, в котором выполняется команда. Когда пользователь введет текст и нажмет
Стандартный поток вывода (STDOUT) предназначен для отображения результата работы программы. Он по умолчанию связывается с терминалом, в котором она выполняется. Если программа посылает какой-то текст на свой STDOUT (в Python для этого существует оператор print, в bash - его аналог echo), пользователь видит его в терминале.
Стандартный поток ошибок (STDERR) работает аналогично STDOUT, но предназначен для вывода сообщений об ошибках, возникающих в процессе работы программы. По умолчанию он так же выводится в терминал, но по желанию программы или пользователя его можно перенаправить отдельно от обычного стандартного потока вывода.
В процессе своей работы команда или программа может открывать дополнительные потоки, например, открыв сетевое соединение или файл на чтение или запись. Но три стандартных потока существуют всегда. Большинство стандартных программ и команд Linux устроены таким образом, что все необходимые для своей работы данные они должны считывать с STDIN, на все результаты совей работы записывать в STDOUT. Таким образом программа представляет собой такой черный ящик в одним входом и одним выходом. Это очень полезное и распространенное соглашение по проектированию текстовых программ Linux, которое позволяет их объединять.
Потоки ввода-вывода можно перенаправлять и подключать к чему угодно: к файлам, программам или даже устройствам. Эта возможность очень мощная и полезная в определенных условиях. При помощи перенаправления потоков можно использовать программы и команды Linux гораздо более удобным и коротким образом. Перенаправления ввода-вывода осуществляются при помощи специальных обозначений, которые обычно указываются после команды, ввод или вывод которой вы хотите перенаправить. Существует несколько вариантов такого перенаправления:
- < file — использовать файл как источник данных для стандартного потока ввода.
- > file — направить стандартный поток вывода в файл. Если файл не существует, он будет создан, если существует — перезаписан сверху.
- 2> file — направить стандартный поток ошибок в файл. Если файл не существует, он будет создан, если существует — перезаписан сверху.
- >>file — направить стандартный поток вывода в файл. Если файл не существует, он будет создан, если существует — данные будут дописаны к нему в конец.
- 2>>file — направить стандартный поток ошибок в файл. Если файл не существует, он будет создан, если существует — данные будут дописаны к нему в конец.
- &>file или >&file — направить стандартный поток вывода и стандартный поток ошибок в файл. Другая форма записи: >file 2>&1.
Давайте рассмотрим наиболее распространенные сценарии такого перенаправления.
Возьмем для примера программу cat. Она считывает один ил несколько файлов, переданных ей как аргументы и выводит их содержимое (если файлов несколько, то она “склеит”, конкатенирует их содержимое, отсюда и название команды) на свой STDOUT. При помощи символа > после команды можно перенаправить ее STDOUT в другое место, например, в файл. Таким образом, если выполнить команду:
1
$ cat file1 file1 file1 > bigfile
то вместо вывода содержимого файлов на экран, в терминал, команда cat запишет этот текстовый поток в файл bigfile. То есть эта команда объединит содержимое нескольких файлов в один. Таким же способом можно записать в файл результат работы любой команды.
Обратите внимание, что при использовании символа > если целевой файл уже существует, то он будет перезаписан. Если вам нужно добавить информацию в существующий файл, то следует использовать оператор >>. Он имеет такой же эффект, но всегда добавляет новый текстовый поток в конец файла. Если же файла нет, то эти два оператора работают идентично.
Аналогично работает и перенаправление из файла. Например, существует команда sort, которая сортирует строки из текстового потока по алфавиту. По умолчанию, эта команда принимает текст для сортировки из своего STDIN, но в нее можно направить файл вот так:
1
$ sort <domains.list
Такая команда выведет в терминал отсортированный файл. Текстовый команды Linux именно поэтому и работают со стандартными потоками. Хотя в первого взгляда это может показаться нелогичным и неудобным, их можно перенаправлять как угодно и за счет этого модифицировать действие команд.
В качестве файлов при перенаправлении можно использовать и устройства. Например, в Linux существует специальное псевдоустройство /dev/null. Все, что попадает в него никуда не идет, не отображается и не сохраняется. Эта такая информационная “черная дыра”. Казалось бы, зачем? Но такое устройство может пригодиться, если нам нужно “подавить” вывод какой-то команды, то есть не отображать его на экране, не записывать в файл, просто игнорировать:
1
$ apt install -y tmux > /dev/null
Надо сказать, что операторы >, >> и < работают только, когда с одной стороны команда, а с другой - файл (или псеводустройство). но можно и объединять команды - то есть перенаправлять вывод одной команды на ввод другой. Такая операция называется конвейером (pipe) и обозначается оператором |. Например, таким образом какой-то очень длинный вывод можно подать на вход программе-пейджеру, которая отобразит его с возможностью перелистывания:
1
$ ls | less
Операции перенаправления и/или конвейеры могут комбинироваться в одной командной строке.
1
$ command <input-file >output-file
1
$ command1 | command2 | command3 >output-file
Выводы:
- Линуксовые команды работают с текстовым вводом и текстовым выводом.
- У каждого процесса есть три стандартных текстовых потока - ввод, вывод и поток ошибок.
- Можно перенаправить результат команды в файл при помощи > или >>.
- Можно направить содержимое файла на вход команде при помощи <.
- Объединять команды можно при помощи конвейера |.
Как работать с архивами?
При работе с файлами и папками часто возникает задача работы с архивами. Архив - это файл, который содержит в сжатом виде несколько файлов или папок. Архивы - это удобный способ хранения и обмена информацией. Существует большое количество алгоритмов архивации данных, множество программ-архиваторов и, соответственно, множество форматов архивных файлов. Пользователи Windows, больше всего знакомы с архивами .zip или .rar.
В Linux тоже существуют много программ-архиваторов, но самыми распространенными являются zip, tar и gzip. Самая простая и понятная программа, которую мы советуем новичкам - это zip. Она имеется в большинстве пользовательских дистрибутивов, проста по синтаксису, работает с самым распространенным форматом архивов.
Команда
1
$ zip archive.zip file1
сжимает файл в архив. При помощи команды zip archive.zip * можно сжать в архив всю папку.
Разархивация такого архива осуществляется командой unzip:
1
$ unzip archive.zip
С помощью опции -d можно указать другую папку, куда будет распаковано содержимое архива. Кроме того, вторым аргументом можно указать конкретный файл, который должен быть извлечен из архива.
Кроме формата zip, пользователи Linux могут встретиться с архивами формата .tar. Обычно они имеют двойной расширение - .tar.gz или .tar.bzip2. Это архивы, созданные программой tar - классической командой Linux. Особенностью этой программы является то, что она умеет создавать и распаковывать архивы, но не умеет их сжимать. Поэтому для сжатия используются другие инструменты - программы zip, gzip или bzip2.
Если вы встретили такой архив, то его можно распаковать командой:
1
$ tar -xvf file.tar.gz
У этой команды почти всегда много опций. x означает распаковку архива, v - вывод подробной информации о процессе (необязательно), f - указание файла для распаковки.
Выводы:
- Команда zip archive.zip file1 сжимает файл в архив.
- Команда zip archive.zip * сжимает в архив все файлы из текущей директории.
- Команда zip -r archive.zip directory1 directory2 directory3 позволяет сделать архив из папок.
- Команда unzip -l file.zip показывает содержимое архива.
- Команда unzip file1.zip распаковывает архив.
- Команда unzip file1.zip -d folder/ распаковывает архив в определенную папку.
- Команда unzip file1.zip file извлекает файл из архива.
- Команда tar -xvf archive.tar распаковывает архив в текущую директорию.
Как найти нужный файл?
При работе в командной строке очень часто возникает задача найти файл или информацию. Специально для этого существует очень мощная команда find - одна из самых часто используемых команд терминала. find - это стандартная команда, ее не надо устанавливать отдельно, она присутствует во всех дистрибутивах Linux.
Общий синтаксис команды find выглядит так:
1
$ find [папка] [критерий] [действие]
Первым параметром всегда идет папка, в которой будет производиться поиск. Если ее не указать, то команда будет искать файлы в текущей папке.
С помощью критериев можно задать особые условия поиска. Так, критерий -name осуществляет поиск по имени файла. Причем в имени можно задать шаблон. Например, такая команда будет искать все файлы с расширением txt:
1
$ find . -name"*.txt"
Критерий -type позволяет искать файлы по типу, которые бывают следующих видов:
- f – простые файлы;
- d – каталоги;
- l – символические ссылки;
- b – блочные устройства (dev);
- c – символьные устройства (dev);
- p – именованные каналы;
- s – сокеты;
Например, указав критерий -type d будут перечислены только каталоги.
Довольно полезен критерий size, который задает размер файла. Можно искать файлы больше или меньше указанного размера с помощью символов + или -, либо по точному размеру, например так:
1
$ find . size +1G
Кроме того, другие критерии этой команды позволяют искать файлы по владельцу, по времени модификации, времени доступа, задавать глубину поиска по подкаталогам, игнорировать регистр, и еще много чего. Для полного знакомства с этой командой рекомендуется прочитать мануал по ней для вашего дистрибутива.
Выводы:
- Для поиска файлов в Linux удобнее всего использовать команду find.
- Пример использования: find /home/ -size +1000000k
Как сделать резервную копию?
Одна из самых типичных задач администратора операционной системы - резервное копирование информации (бекап). Оно позволяет застраховаться на случай аппаратных или программных сбоев, ведь именно информация - это самое ценное, что существует в компьютерных системах. Но вообще, резервным копированием нужно заниматься и обычным пользователям, ведь у вас тоже может быть важная информация, которую не хотелось бы потерять. Особенно важно производить бекапы в условиях изменений информационной среды - обновлении программ и операционной системы, миграции на новые версии программ, в ходе разработки программного обеспечения, при развертывании рабочих и тестовых сред.
Rsync (Remote Sync) — это наиболее часто используемая команда для удаленного и локального копирования и синхронизации файлов и каталогов в системах Linux/Unix. С помощью команды rsync вы можете удаленно и локально копировать и синхронизировать данные между каталогами, дисками и сетевыми хранилищами, выполнять резервное копирование данных.
Программа rsync не всегда поставляется вместе с дистрибутивом, поэтому проверьте, есть ли она у вас. Если у вас команда rsync выдает сообщение о том, что программа не найдена, то вам надо установить ее командой:
1
$ sudo apt install rsync
Базовый синтаксис программы rsync выглядит так:
1
$ rsync options source destination
source и destination - это названия файлов или папок, соответственно, откуда и куда будет произведено копирование. Обычно происходит копирование из какой-то папки в папку или файл на другом разделе жесткого диска, даже на другой машине, по сети.
Как у любой мощной команды, у rsync большое количество опций. Перечисли самые полезные:
- -v: подробный вывод.
- -r: рекурсивно копирует данные (но не сохраняет временные метки и разрешения при передаче данных).
- -a: режим архива, позволяет рекурсивно копировать файлы, а также сохраняет символические ссылки, права доступа к файлам, права владения пользователей и групп и временные метки.
- -z: сжатие данных в ходе копирования.
- -h: вывод информации о ходе копирования
Основной особенностью команды rsync по сравнению с обычным копированием является то, что она копирует данные инкрементально. То есть, если в месте назначения уже была более старая резервная копия, то rsync будет копировать только разницу, то есть то, что изменилось с последнего бекапа. Это позволяет сильно экономить на времени и ширине канала копирования, особенно, если вы делаете бекапы часто.
Кроме локального копирования, rsync позволяет так же просто делать бекапы по сети с или на удаленный хост. Для этого, он должен быть доступен по сети, мы должны знать его адрес (обычно, IP-адрес или доменное имя), а так же иметь учетные данные (то есть знать имя пользователя и его пароль на удаленной машине). Указать удаленную машину можно, например, так:
1
$ rsync -avz rpmpkgs/ root@192.168.0.100:/home/
Перед началом копирования команда попросит вас ввести пароль. Подключение может проходить по протоколу защищенного соединения SSH, тогда данные еще и шифруются при передаче.
Rsync, как и многие сетевые программы является клиент-серверной программой. Это значит, что для успешного общения двух машин по протоколу rsync необходимо, чтобы эта программа была установлена на обоих машинах. Более того, на удаленном компьютере должен быть настроен сервер rsync, то есть разрешены сетевые подключения к этой программе.
Для этого нужно создать файл настроек /etc/rsyncd.conf (создание файлов в системной папке /etc/ доступно только пользователю root) с примерно таким содержанием:
1
2
3
4
5
6
7
8
9
10
11
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
log file = /var/log/rsync.log
[share]
path = /tmp/share/
hosts allow = 192.168.56.1
hosts deny = *
list = true
uid = root
gid = root
read only = false
В этом файле мы для безопасности разрешаем удаленный доступ к данному компьютеру через rsync только с одного IP-адреса - 192.168.56.1. Вам, конечно, нужно прописать адрес той машины, с которой вы будете запускать бекапы.
После создания конфигурационного файла необходимо запустит сервер и добавить его в автозагрузку.
1
2
$ sudo systemctl start rsync
$ sudo systemctl enable rsync
Если эти две команды выполнились без ошибок, это значит, что можно пользоваться службой rsync для удаленного резервного копирования.
Выводы:
- Программа Rsync используется для инкрементального резервного копирования.
- Команда rsync -avr /source/ /destination/ копирует содержимое одной папки в другую.
- Rsync позволяет копировать с или на удаленный хост используя протокол SSH.
- Синхронизация с удаленным хостом выполняется командой rsync -avz /tmp/ root@192.168.56.102:/home/
- Для работы с удаленным сервером там должен быть установлен и настроен сервер rsync.
Использование удаленного доступа по SSH
Зачем нужен протокол SSH?
Обычные пользователи почти всегда работают с компьютером локально. То есть они физически находятся у компьютера, используют имеющиеся у него устройства ввода-вывода (клавиатуру, мышь, дисплей) для выполнения необходимых операций. Однако, не всегда есть возможность физически находиться у компьютера, с которым нужно работать. Например, если вы хотите создать свой сайт, то он будет размещен на компьютере хостинг-провайдера. Этот компьютер может находиться на другом конце света. Вам нужен способ запускать команды на этом компьютере не выходя из своего дома. При профессиональной работе с компьютерами часто возникает потребность выполнять определенные операции на удаленных машинах. Например, системный администратор может обслуживать компьютерную сеть, состоящую из десятков компьютеров. Было бы очень неудобно постоянно физически перемещаться между компьютерами. Более того, удаленный компьютер может располагаться, например, в центре обработки данных у облачного провайдера, или быть вообще виртуальной машиной на кластере. Такое часто бывает при использовании облачных систем.
Источник: securityboulevard.com.
Специально для таких случаев существуют протоколы удаленного доступа. Они позволяют подключиться к удаленному компьютеру и работать на нем так же, как вы обычно работаете на локальной физической машине. Такие протоколы разделяются на графические и текстовые. Графические протоколы, например, RDP (remote desktop protocol) передают по сети изображение графического интерфейса в одну сторону, движения мыши и события клавиатуры - в другую. Текстовые протоколы, такие как Telnet или SSH, передают только текстовую информацию и позволяют работать в терминале удаленной машины. Здесь мы рассмотрим самый популярный текстовый протокол удаленного доступа - SSH.
SSH — защищенный протокол для удаленного доступа к компьютерам. Через SSH можно выполнять операции в командной строке компьютера, который физически находится в другом месте. Иными словами, SSH — это дистанционная командная строка. Визуально вы работаете на своем компьютере, но в реальности — на другом.
Протокол — это набор соглашений, правил, по которым разные программы могут обмениваться информацией. SSH — это набор правил, который известен и вашему компьютеру, и физически отдаленному компьютеру. Пример: вы вводите команду удаления файла, и эта команда передается на другой компьютер и выполняется там. Ответ (или сообщение об ошибке) возвращается и показывается на вашем компьютере.
При удаленной работе обычно используются существующие открытые каналы коммуникации, то есть компьютерные сети. Почти всегда - подключение происходит через Интернет. Поэтому очень важно обеспечить безопасность соединения. При использовании протокола SSH вся информация передается в зашифрованном виде. Подобно тому, как некоторые сайты работают по HTTPS, шифруя информацию. Например, ваш онлайн-банкинг обязательно должен работать по защищенному соединению. В таком случае даже если всю информацию перехватывает злоумышленник, он не сможет расшифровать её.
Используя SSH можно подключиться к любой удаленной машине при соблюдении некоторых условий. Ни параметры удаленной машины, ни расстояние не имеют значения. Также можно подключаться как к физическим, так и к виртуальным машинам. При подключении, вы даже можете не знать, к чему вы подключаетесь, к физическому компьютеру или к виртуальной машине в датацентре какой-то компании. При работе никакой разницы вы не заметите.
Как и большинство сетевых протоколов, SSH работает по клиент-серверному принципу. То есть на вашем компьютере, за которым вы работаете физически должна быть установлена специальная программа - ssh клиент. С ее помощью вы и будете подключаться к другим машинам. А на удаленной машине, то есть той, к которой вы подключаетесь должна работать другая программа - ssh сервер. Она будет принимать ваше подключение, выполнять ваши команды, следить за правами доступа и так далее. Как настроить и клиент и сервер мы поговорим далее.
Выводы:
- При работе часто возникает потребность выполнять операции на разных удаленных хостах.
- Обычное незащищенное соединение для этого не подходит из-за проблем с безопасностью.
- Протокол SSH позволяет выполнять команды терминала на удаленной машине.
- Протокол SSH начинает шифровать соединение еще до ввода пароля пользователя.
- Таким образом можно подключаться к любым машинам, физическим и виртуальным.
- SSH является клиент-серверным протоколом. На локальной машине работает клиент ssh, на удаленной - сервер.
Что нужно для успешного удаленного доступа?
Источник: Wikimedia.
Для того, чтобы иметь возможность подключиться к удаленной машине нужно соблюдение нескольких условий. Во-первых, как мы уже говорили, на ней должен быть установлен и работать ssh сервер. Существуют разные сервера, но самым распространенным является свободный и бесплатный сервер openssh. Он существует и для Linux и для Windows и для других операционных систем. Во многие дистрибутивы Linux, ориентированные на серверную работу он устанавливается по умолчанию. Если же его нет, то установить его очень просто командой:
1
$ sudo apt install openssh-server
Естественно, удаленная машин должна быть доступна по сети с локальной. Если удаленная машина выключена, не подключена к сети или подключена к изолированной сети, куда не пройдут пакеты от локальной машины - подключиться вы к ней не сможете. Другими словами, удаленная машина должна пинговаться с локальной.
Обратите внимание, что удаленная машина не должна быть скрыта NAT’ом или каким-либо сетевым экраном, блокирующим входящие соединения. Кстати, домание компьютеры, выходящие в интернет через обычного провайдера, как правило, как раз скрыты за NAT. Поэтому, находясь в своей домашней сети, вы сможете подключиться к другому компьютеру в ней, а извне - нет.
Протокол ssh никак не связан с интернетом. Вы можете подключаться к машинам, которые подключены в общую локальную сеть, независимо от того, есть ли выход в интернет. В любом случае, вы должны знать адрес удаленной машины, чтобы к ней подключиться. Обычно используется IP-адрес. Но если у сервера есть назначенное доменное имя - можете использовать его.
Кроме IP-адреса нужно знать номер сетевого порта, который прослушивает сервер ssh. По умолчанию любая служба ssh использует порт 22. Однако, администраторам при настройке сервера рекомендуется ради безопасности изменить порт на случайный. Поэтому для подключения надо знать, был ли переназначен стандартный порт и, если да, то на какой.
Еще одно необходимое условие - знание учетной записи пользователя на удаленной машине. Помните, что после подключения вы первым делом должны будете авторизоваться на сервере. Поэтому для этого вы должны знать имя пользователя и его пароль. Этот пользователь должен быть зарегистрирован именно на удаленной машине. Ваш локальный пользователь не подойдет. Если вы сами настраиваете сервер, то такое надо предусмотреть заранее. Например завести специального пользователя, предназначенного именно для того, чтобы под ним на эту машину заходили по протоколу ssh.
Еще одно предостережение. На реально используемых серверах обязательно должен стоять файерволл - специальная программа, которая блокирует входящие соединения на определенные порты. Она нужна для безопасности. Так вот, по умолчанию, такие программы блокируют все порты. Если вы хотите какие-то определенные порты использовать, их надо добавить в исключения файервола.
Как мы уже говорили, на локальной машине должне быть установле ssh-клиент. В *nix-подобных системах (Linux, macOS) клиент обычно установлен в системе по умолчанию, и достаточно открыть терминал и воспользоваться командой ssh. В Windows нужно скачать сторонний клиент, например, Putty. SSH-клиентов тоже существует много разных. Есть даже графические клиенты. Но даже в них придется работать в командной строке - ведь сам протокол ssh - чисто текстовый.
Выводы:
- Для подключения нужно знать адрес сервера, логин и пароль, порт подключения (по умолчанию, 22) .
- Еще нужно, чтобы сетевая инфраструктура была правильно настроена.
- Нужно, чтобы на удаленной машине работал сервер SSH.
- Самый распространенный сервер - OpenSSH - бесплатный и открытый.
- Нужно, чтобы удаленная машина была доступна по сети.
- Системные администраторы могут заблокировать доступ к порту ssh.
- По умолчанию, порты обычно закрыты, нужно их открыть.
- На локальной машине должен быть установлен клиент ssh.
- Есть консольные и графические клиенты ssh, но работать все равно придется в терминале.
Как подключиться к серверу из командной строки?
Для подключения нужно воспользоваться командой ssh. при запуске нужно указать адрес сервера, имя пользователя и, опционально, порт подключения. Вот как выглядит команда при использовании консольного клиента (в терминале):
1
$ ssh username@remote_host -p port
Например, для подключения к серверу 52.307.149.244 в аккаунт user нужно ввести:
1
$ ssh user@52.307.149.244
Если не указывать порт, то будет использован порт SSH по умолчанию — 22. Используемый порт задается при настройке SSH-сервера.
При первом подключении к конкретному серверу появится такое сообщение:
1
2
3
The authenticity of host '52.307.149.244 (52.307.149.244)' can't be established.
ECDSA key fingerprint is fd:fd:d4:f9:77:fe:73:84:e1:55:00:ad:d6:6d:22:fe.
Are you sure you want to continue connecting (yes/no)? yes
Введите yes в первый раз.
Это нужно для повышения безопасности. При настройке SSH-сервера создается уникальная комбинация символов — fingerprint («отпечатки пальцев»). Ваш компьютер запоминает эту комбинацию и сверяет ее при каждом новом соединении. Если кто-то переустановит SSH-сервер, или всю операционную систему, или вообще заменит удаленный компьютер, сохранив его адрес, то при следующем соединении вы узнаете об этом, потому что изменится fingerprint. Если fingerprint не меняется, то такое сообщение не будет появляться.
Простейший вариант — подключение по паролю. После ввода команды ssh система запросит пароль:
1
2
$ ssh ivan@52.307.149.244
password:
Пароль придется вводить каждый раз. Обратите внимание, что пароль нужно вводить уже после установки подключения. Это сделано для того, чтобы передаваемый по сети пароль уже мог шифроваться. Ведь передавать пароли в открытом виде - очень небезопасно.
После подключения вы войдете в командную строку удаленной машины. В командной строке немного чего изменится.
Обратите внимание, что изменилось приглашение командной строки - теперь там отображаются данные удаленной машины, другое имя пользователя. Другое приглашение - явный знак того, что вы работаете не на своем локальном физическом компьютере.
Теперь можно вводить любые команды и они будут выполнены на удаленном компьютере. Порядок работы такой же, как и в обычной командной строке.
Для выхода из удаленного сеанса необходимо нажать комбинацию клавиш Ctrl + D. Либо можно ввести команду exit.
Выводы:
- Команда ssh username@remote_host осуществляет подключение к удаленному терминалу.
- Если вы подключились к этому компьютеру первый раз, нужно будет подтвердить его.
- Опция -p позволяет подключиться по любому номеру порта.
- После установки соединения вам нужно будет ввести пароль от удаленной учетной записи.
- После этого вы сможете работать в командной строке так же, как и на локальной машине.
- Для выхода из удаленного сеанса есть команда exit или сочетание Ctrl + D.
Как подключиться к серверу по ключу?
Пи работе с удаленными машинами приходится при каждом подключении вводить пароль. Если вы работаете на разных машинах, приходится запоминать все пароли от всех удаленных компьютеров. Это, конечно, безопасно, но очень неудобно. К счастью, методы асимметричного шифрования позволяют избежать этого и авторизоваться на удаленных машинах без пароля, по ключу.
Асимметричные шифры устроены таким образом, что всегда используют пару ключей - один для шифрования данных, а другой, парный - для расшифровки. Эта пара ключей генерируется вместе специальным алгоритмом. И по одному ключу невозможно восстановить другой. Причем расшифровать данные можно только тем ключом, который был сгенерирован в паре с тем, которым эти данные были зашифрованы.
Поэтому пару ключей можно использовать и для авторизации. Один из ключей - тот, что используется для шифрования - назвается приватным. Его нельзя никому передавать, показывать или пересылать. Его надо хранить в секрете. Второй ключ - публичный - используется для расшифровывания. Вот его можно передавать кому угодно. Обратите внимание, что если какие-то данные удается расшифровать вашим публичным ключом, это значит, что они были зашифрованы вашим приватным ключом, и никаким другим. А так как ваш приватный ключ знаете только вы, значит и отправили эти данные именно вы. Так публичный ключ как бы подтверждает вашу личность.
Обратите внимание, что в такой схеме не очень соблюдается секретность передачи данных - ведь расшифровать их может кто угодно. Подробное рассмотрение алгоритмов шифрования и протоколов и схем их применения далеко выходит за рамки нашей темы. Заметим только, что в реальности для обеспечения секретности, целостности и идентификации в криптографических системах используются разные схемы. Мы сейчас рассматриваем только использование ключей для авторизации на сервере.
Итак, чтобы использовать авторизацию по ключу надо первым делом сгенерировать свою собственную пару ключей. Для этого используется программа ssh-keygen, которая включена в пакет ssh-клиента.
Создадим пару ключей:
1
$ ssh-keygen
Программа запустится и спросит, куда сохранять ключи:
1
2
Generating public/private rsa key pair.
Enter file in which to save the key (/home/demo/.ssh/id_rsa):
Нажмите Enter для сохранения в стандартное место — директорию .ssh/id_rsa в вашей домашней директории.
Программа запросит passphrase. Это вроде пароля для ключа. Пароль используется для дополнительной защиты - его надо будет вводить каждый раз при использовании ключа. Идея в том, что вместо разных паролей придется запомнить только один - от ключа. Можно просто нажать Enter и пропустить этот шаг.
1
2
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Ключи созданы:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Your identification has been saved in /home/demo/.ssh/id_rsa.
Your public key has been saved in /home/demo/.ssh/id_rsa.pub.
The key fingerprint is:
8c:e9:7c:fa:bf:c4:e5:9c:c9:b8:60:1f:fe:1c:d3:8a root@here
The key's randomart image is:
+--[ RSA 2048]----+
| |
| |
| |
| + |
| o S . |
| o . * + |
| o + = O . |
| + = = + |
| ....Eo+ |
+-----------------+
Теперь у вас есть два файла:
- ~/.ssh/id_rsa — приватный ключ. Никогда никому и никуда не передавайте его!
- ~/.ssh/id_rsa.pub — публичный ключ. Спокойно распространяйте его.
В Windows можно использовать ssh-gen в подсистеме Ubuntu for Windows или в командной строке Git for Windows. Или создавать ключи графической утилитой вроде PuTTYgen.
После генерации необходимо скопировать ключ на удаленный сервер. Нужно добавить публичный ключ на сервер в файл ~/.ssh/authorized_keys. Самый простой способ — запустить на локальной машине команду для копирования ключа:
1
$ ssh-copy-id -i /home/demo/.ssh/id_rsa ivan@52.307.149.244
Обратите внимание, что при этом придется обязательно ввести пароль от учетки на сервере. Таким образом скопированный ключ подтверждает, что его владелец знает пароль, поэтому в будущем можно его пускать без проверки.
Другой способ — подключиться по паролю, открыть в редакторе файл ~/.ssh/authorized_keys и добавить в конец текст из вашего файла ~/.ssh/id_rsa.pub.
Теперь при подключении пароль запрашиваться не будет. После включения соединений по ключу рекомендуется отключить подключение по паролю в настройках ssh сервера.
Один и тот же публичный ключ можно использовать для авторизации на разных серверах. Просто скопируйте его туда, куда часто заходите. Кроме обычных компьютеров, авторизация через SSH по ключу используется и на многих публичных сервисах. Например, популярный хостинг программных репозиториев GitHub заставляет использовать именно доступ по ключу для синхронизации проектов. Многие облачные сервисы так же имеют возможность авторизации по ключу.
Только заметьте, что если вы скомпрометировали ваш приватный ключ, придется генерировать новую пару ключей. А затем придется рассылать новый публичный ключ по всем свои серверам. Причем, компрометация - это не когда вы его по ошибке где-то опубликовали, это когда возникает малейшее подозрение, что третьи лица могли получить к нему доступ.
Выводы:
- Запоминать много паролей от серверов часто неудобно и небезопасно.
- Асимметричное шифрование позволяет избежать ввода пароля каждый раз.
- Команда ssh-keygen позволяет сгенерировать пару ключей - приватный и публичный.
- Приватный ключ надо хранить в секрете. Если он скомпрометирован, придется создавать новую пару.
- Публичный ключ может удостоверять подлинность отправителя.
- Можно скопировать публичный ключ на сервер командой ssh-copy-id -i.
- Ключ может быть разослан на несколько серверов, и на них можно заходить без пароля.
- Ключ тоже можно защитить паролем.
Как запомнить частые подключения?
При работе с многими удаленными серверами кроме паролей приходится запоминать еще много информации: ip-адреса, имена пользователей на каждом сервере, номера нестандартных портов, которые тоже могут отличаться, другие опции. Это не так критично, как пароли, но все равно может доставить неудобств.
В стандартном клиенте ssh предусмотрена возможность запомнить частые подключения в конфигурационном файле. Он располагается по адресу ~/.ssh/config. Если его там нет, вполне можно создать новый пустой. Вообще, папка ~/.ssh/ - это общее место, где собираются многие файлы, имеющие отношение к входящим или исходящим подключениям по ssh. Например, именно туда сохраняются файлы ключей и публичные ключи удаленных пользователей.
Этот файл имеет довольно простую структуру. В нем прописывается символьное имя подключения, которые вы будете использовать вместо строки подключения в команде ssh. После этого могут идти несколько опций, например - имя пользователя, адрес хоста, номер порта, адрес публичного ключа, который будет использоваться при подключении. Выглядит это примерно так:
1
2
3
4
5
6
7
8
9
10
11
12
Host targaryen
HostName 192.168.1.10
User daenerys
Port 7654
IdentityFile ~/.ssh/targaryen.key
Host tyrell
HostName 192.168.10.20
user oberyn
Host martell
HostName 192.168.10.50
У этого файла довольно много опций, но эти - самые полезные. После того, как в файл добавлена запись можно подключаться к хосту только по алиасу, например так:
1
$ ssh targaryen
Все остальные параметры клиент подставит сам.
Выводы:
- Файл ~/.ssh/config используется, чтобы запомнить часто использующиеся подключения.
- Можно присвоить символьное имя и связать его с адресом, именем пользователя.
- В этом файле можно прописать использование определенного файла ключа.
- Файл приватного ключа в таком случае выступает идентификацией (identity).
Зачем нужна программа Tmux?
При работе в удаленной сессии есть один подводный камень. Из-за особенности функционирования процессов в Linux, когда мы выходим из сеанса (разлогиниваемся), то все запущенные команды и программы автоматически завершатся. Следующий раз нам приходится начинать работы с самого начала. И нельзя запустить какой-то долгий процесс, чтобы он выполнялся, пока мы отключились. Такое происходит и в случае непреднамеренного отключения, например, при обрыве связи.
Чтобы обойти это ограничение, существуют специальные программы. Они не входят в стандартные дистрибутивы, так что их придется устанавливать отдельно. Такие программы работают резидентно, то есть в режиме сетевой службы и поэтому могут поддерживать сеанс даже после того, как пользователь отключился.
Tmux (terminal multiplexor) - популярная программа, значительно облегчающая работы в терминале на удаленной машине. Среди возможностей данной программы:
- разделение экрана на несколько консолей
- создание нескольких экранов
- сохранение выполняющихся процессов на удаленной машине после выхода из сессии
- расшаривание ssh сессий нескольким пользователям
То есть кроме поддержания сессии tmux сильно “прокачивает” стандартную командную строчку. Это очень упрощает и ускоряет сложную работу на сервере.
Для начала работы необходимо подключиться к удаленной машине по ssh. Конечно, можно использовать программу tmux на локальной машине для расширения возможностей терминала, но здесь мы рассмотрим возможности этой программы при удаленном доступе к терминалу.
При необходимости нужно установить tmux, введя команду
1
sudo apt install tmux
Обратите внимание, что эта программа должна быть установлена именно на той машине, на которой вы будете выполнять команды. То есть, если вы планируете использовать ее для работы на удаленном сервере, то именно в нем она и должна быть установлена. Если она есть только на вашей локальной машине, на сервере она не заработает.
Теперь можно запустить tmux, введя одноименную команду:
На экране мы видим главное окно программы tmux:
Сочетание клавиш <Ctrl>+B позволяет вводить команды tmux. Наиболее применимые команды:
- ctrl-b c - запуск нового окна;
- ctrl-b , - переименование окна;
- ctlb-b p - переключение к предыдущему окну;
- ctlb-b n - переключение к следующему окну;
- ctlb-b w - вывести список окон;
- ctlb-b % - разделение окна вертикально на две панели;
- ctlb-b “ - разделение окна горизонтально;
- ctlb-b d - отсоединиться от сессии;
Обратите внимание, сначала надо зажать <Ctrl>+B именно как комбинацию клавиш. Потом отпустить и сразу нажать клавишу, соответствующую нужной команде. Поначалу такая работа покажется очень неудобной, но со временем вы привыкните к такому оригинальному методу ввода команд.
Обратите внимание на нижнюю строчку. Здесь перечислены открытые окна терминала. Можно воспринимать их как вкладки, по которым можно перемещаться. Мы можем создать новое окно (ctrl-b c):
Теперь мы видим, что в нижней строке появилось окно 1. Звездочкой обозначено текущее окно.
Мы можем также переименовать окно (ctrl-b ,), чтобы лучше ориентироваться. Это бывает полезно для организации рабочего пространства.
Теперь мы имеем второе окно (под номером 1, так как нумерация начинается с 0) с именем window1.
Давайте для иллюстрации запустим в первом окне команду htop:
Tmux позволяет вывести список всех созданных окон (ctrl-b w), в котором можно перемещаться между ними. Также доступен предварительный просмотр содержимого каждого окна:
Работая в этой программе можно также разделять пространство в отдельно взятом окне на две панели. В каждой панели можно работать независимо, вводить команды и наблюдать результат одновременно.
Текущее окно или панель можно разбить на две панели как вертикально (ctrl-b %), так и горизонтально (ctrl-b "). Панели можно дробить неограниченное число раз:
При выполнении команды exit (или ctrl-d) панель закрывается.
Если эта панель была единственной в окне - то закроется все окно, а если окно было единственным - программа tmux завершится.
Еще одной возможностью tmux является работа с сессиями. Если использовать сессии, то вместо выхода из сессии подключения к удаленной машине можно отключиться от сессии, а при повторном подключении сессия сессия сохранит свое состояние. То есть, все процессы, запущенные на удаленной машине так и будут продолжать выполняться.
Для запуска сессии необходимо запустить tmux со специальным параметром, указав имя сессии:
Запустим в этой сессии какой-нибудь интерактивный процесс:
Нажав сочетание клавиш ctrl-b d, отсоединимся от сессии:
При помощи команд терминала можно убедиться, что программа, которую мы запустили все еще работает, несмотря на то, что мы вышли из родительского процесса.
Можно даже выйти из подключения к удаленной машине и спустя некоторое время зайти обратно.
При запуске tmux можно вывести список всех открытых сессий:
Соответственно, можно вместо запуска нового терминала подключиться к одной из существующих сессий:
Как мы можем наблюдать, терминал остался в том же состоянии, в котором мы его оставили, когда выходили из сессии:
Выводы:
- При отключении от удаленного хоста все запущенные процессы завершатся.
- Иногда нужно запустить процесс так, чтобы он продолжался на сервере после отключения.
- Для этого существуют специальные резидентные программы, например, tmux.
- Tmux организует сессии, которые продолжаются даже после отключения пользователя.
- Также вы можете продолжать работу с того места, с которого отключились.
- Tmux позволяет создавать несколько вкладок, разбивать окно терминала на области.
- Tmux сильно упрощает работу с терминалом и с удаленными серверами.
- Для работы tmux должен быть установлен на удаленном сервере.
Пользователи и их права доступа
Зачем нужно разделение доступа?
Современные операционные системы рассчитаны на использование многими пользователями одновременно. Раньше такое разделение использовалось когда компьютеры были слишком дорогими, чтобы обеспечивать ими всех работников и несколько человек могли по сети подключиться к одной рабочей станции, чтобы работать с ней.
При работе нескольких пользователей за одной машиной необходимо разделить их полномочия. Во-первых, пользователи не должны иметь право вмешиваться в работу системы в целом. Например, если один пользователь захочет удалить установленную в системе программу, это может затронуть работу других пользователей, которые эту программу используют. Во-вторых, пользователи должны сами решать, показывать ли свои файлы и папки другим пользователям.
Естественно, должны существовать пользователи с расширенным набором полномочий, которые могут осуществлять обслуживание системы в целом. Обычно это системные администраторы, которые ответственны за техническое состояние системы. Обычно они же управляют полномочиями пользователей: определяют, что могут, а чего не могут делать рядовые пользователи.
Сейчас, в эру персональных компьютеров когда за Вашим личным компьютером вряд ли работает много малознакомых человек, многопользовательская работа используется для двух основных сценариев:
- Если мы говорим о пользовательских устройствах, то часто пользователи используются для разделения доступа между приложениями. Например, в ОС Android для каждого установленного приложения регистрируется новый пользователь. Таким образом, одно приложение не может вмешаться в работу другого и в работу системы в целом.
- На серверных компьютерах все еще зачастую работают несколько человек, как системных администраторов, так и неограниченный круг пользователей, которые могут подключаться к серверу по сети и иметь доступ к некоторым данным и сервисам этого сервера. Даже на ваш персональный компьютер могут подключиться пользователи и процессы по сети и им необходимо как-то ограничить возможность действий в операционной системе.
В любом случае, речь идет о разграничении доступа пользователей к ресурсам операционной системы. Под ресурсом здесь понимается любая информация, аппаратные компоненты, программы, то есть все, чем можно воспользоваться при работе за компьютером. В операционной системе Linux любые информационные ресурсы представляются в виде файла. Это позволяет иметь один механизм разделению доступа - по файлам, вместо того, чтобы отдельно ограничивать доступ к аппаратным компонентам, сети, программам и так далее.
Любой пользователь компьютера в такой системе имеет определенные права - что-то он может делать, а что-то сама операционная система ему запретит. В Linux, так как все представляется в виде файла это выражается в том, что к каким-то файлам у пользователя есть доступ, а к каким-то нет, или он ограничен. Причем при проектировании операционных систем гораздо безопаснее и надежнее исходить их принципа минимальных прав - это когда пользователю разрешается только то, что ему необходимо делать, а все остальное - запрещается. Другими словами, все, что не разрешено явно и специально - запрещено по умолчанию. При таком подходе пользователь не сможет своими действиями нанести вред всей системе, умышленно или случайно.
Выводы:
- Современные компьютеры могут использоваться несколькими пользователями одновременно.
- Поэтому должно быть разделение и ограничение доступа к ресурсам.
- Ресурсы - это файлы, устройства, сеть. В Linux - это все файлы.
- Пользователи могут применяться для разделения доступа между приложениями.
- Каждый пользователь имеет свои полномочия в системе - права доступа.
- Необходимо придерживаться принципа минимальных прав.
Что такое пользователь?
Для того, чтобы система разделения прав доступа работала, операционная система должна знать две вещи: во-первых, какие пользователи вообще существуют и каким из них является данный пользователь. Первое - это регистрация, а второе - авторизация. В первую очередь, операционная система должна хранить информацию о том, какие пользователи ей знакомы, и какими правами обладает каждый из них. Запись о пользователе в операционной системе называется учетной записью. При установке системы создается учетная запись администратора системы, могут быть созданы запись других пользователей. Администратор может потом создавать, удалять или редактировать учетные записи пользователей.
Для начала работы с операционной системой человек должен пройти процедуру авторизации - сопоставления пользователя одной из учетных записей, хранящихся в системе.
Авторизация обычно построена на вводе в систему одного из возможных секретов:
- То, что пользователь знает - авторизация по паролю или ключевой фразе. Это технически самый простой вид авторизации до сих пор широко распространен.
- То, что пользователь имеет - физический ключ, содержащий электронный криптографический сертификат. Это развитие предыдущего способа, когда конкретный секрет скрыт для безопасности от пользователя и воплощен в какой-то физической форме.
- То, чем пользователь является - биометрическая аутентификация. Еще недавно этот способ был очень дорогим из-за нераспространенности специализированного оборудования. Сейчас он используется все чаще, так как биометрические датчики встроены во многие смартфоны. Очень часто он используется в сочетании с другими способами (двухфакторная аутентификация).
В настоящее время наиболее распространена авторизация по паролю - это самый технически простой и потенциально надежный способ. В последнее время все больше распространяются способы биометрической авторизации, более сложные способы - например, двухфакторная аутентификация которая основана на авторизации на другом доверенном устройстве. Но в любом случае, авторизация - это доказательство, что вы действительно являетесь тем пользователем, который заявляется. В случае авторизации по паролю пользователь должен ввести логин (имя пользователя) и пароль. Если пароль не соответствует тому, что содержится в учетной запись данного пользователя в конфигурации операционной системы, то она откажет пользователю в работе.
Если же авторизация проша успешно, операционная система запускает программу-оболочку. Это может быть текстовый командный интерпретатор, либо графический оконный менеджер, в зависимости от настроек и комплекта поставки системы. Но в любом случае, далее приводится в действие важный принцип, без которого невозможно надежное функционирование операционных систем и разделения доступа - все действия, которые пользователь выполняет, операционная система отслеживает и проверяет на права доступа. То есть пользователь не сможет получить доступ ни к чему, чтобы ему не было разрешено операционной системой. Более того, любая программа, команда, процесс, который запускает этот пользователь, будет работать с его правами.
Эта система работает за счет двух фактов: во-первых, пользователь не может получить доступ ни к каким ресурсам компьютера напрямую, только через операционную систему. Например, пользователь (или, что то же самое, программа, запущенная пользователем) не может напрямую открыть файл. Пользователь может лишь попросить операционную систему открыть файл и дать ему содержимое этого файла. Обычно, это происходит быстро и прозрачно, так что создается впечатление, что сам пользователь открывает файл. И во-вторых, система проверки прав доступа “зашита” в сам фундамент операционной системы, в ее ядро. Это не позволяет обойти систему прав доступа. Каждый раз когда нужно обратиться к какому-то компьютерному ресурсу, пользователь вынужден просить об этом операционную систему, а она, в свою очередь, каждый раз проверяет, что за пользователь пытается выполнить это действие и имеет ли он на это право.
Кстати, мы обычно подразумеваем, что пользователь - это человек, который работает за компьютером. Но с точки зрения операционной системы пользователь - это учетная запись, обладающая определенными правами. Человек может авторизоваться как определенный пользователь. А потом, в процессе работы, он может авторизоваться как другой пользователь, то есть сменить учетную запись. Так что лучше считать, что пользователь - это определенная роль при работе с операционной системой. Не обязательно создавать строго по одному пользователю на каждого человека, который работает за компьютером. Иногда удобнее создать пользователя в определенными правами для выполнения конкретных действий, или для работы с определенной программой. Такие пользователи иногда называются служебными. И в любой операционной системе служебных пользователей гораздо больше, чем “реальных”.
Выводы:
- Пользователь для ОС - это учетная запись с именем и способом авторизации.
- Первоначальная регистрация пользователей происходит при установке операционной системы.
- Обычно применяется авторизацию по паролю.
- Для начала работы в системе необходимо залогиниться - пройти авторизацию.
- Все процессы, которые запускает пользователь выполняются от его имени и с его правами.
- Система авторизации и разграничения пользователей встроена в само ядро операционной системы.
- Существуют служебные пользователи - они не люди, а роли при работе, которые нужны для удобства.
Где хранится информация о пользователях?
В любой операционной системе хранится информация о том, какие пользователи в ней зарегистрированы и могут авторизоваться для работы с ней. В Linux эта информация хранится в файле /etc/passwd. Это простой текстовый файл, как и большинство других конфигурационных файлов Linux. Его можно открыть и отредактировать любым текстовым редактором или командами работы с текстом терминала. Этот файл доступен для просмотра любому авторизованному пользователю, но редактировать его может только администратор системы.
Файл /etc/passwd представляет собой табличную структуру - каждая строка в нем соответствует одному пользователю, а в строке содержатся несколько полей, значения которых разделяются двоеточиями. Это типичная структура конфигурационного файла Linux. Надо отметить, что этот файл - это не просто отображение информации о пользователях, он и представляет собой настройку операционной системы. Она сама берет информацию о своих пользователях из этого файла. Поэтому, если этот файл отредактировать, например, добавить в него новую строку, это будет эквивалентно регистрации нового пользователя.
Любой пользователь Linux должен иметь имя - строку, которая однозначно характеризует пользователя (то есть не повторяется) и отображается во всех сообщениях системы. Имя пользователя состоит из строчных латинских букв и может содержать знаки подчеркивания. Имя пользователя так же указывается при авторизации пользователей в системе.
Кроме символьного имени, каждому пользователю автоматически присваивается численный идентификатор. Операционной системе проще работать с числами, чем со строками, поэтому этот идентификатор, называемый UID (user identifier), используется внутри системы. Например, именно он отслеживается у всех запущенных процессов. Обычно, в современных дистрибутивах Linux идентификаторы пользователей, которые были вручную созданы при установке или настройке системы начинаются с 1000. Номера меньше 1000 зарезервированы для служебных пользователей, которые создаются автоматически разными программами.
Кроме имени и идентификатора для авторизации пользователя нужен пароль. Раньше пароль указывался тут же, в файле /etc/passwd, вместе со всей остальной информацией о пользователе. Но по соображениям удобства, нужно, чтобы этот файл был доступен на чтение всем пользователям системы. Это нужно для организации процесса аутентификации. А если любой пользователь может прочитать этот файл, значит в нем нельзя хранить пароли. Поэтому сейчас вы не увидите паролей в файле /etc/passwd, на их месте стоит символ “х”. Сами пароли (а вернее их хеши) хранятся в другом файле - /etc/shadow, который доступен для чтения только администратору системы.
Еще в файле /etc/passwd для каждого пользователя указывается адрес домашней папки. Этот адрес подставляется вместо символа ~ в путях, а также в некоторых других сокращениях команд терминала. Домашняя папка пользователя предназначена для хранения файлов этого пользователя. У любого пользователя, естественно, есть полный доступ к этой папке. То есть он может создавать, удалять там папки, файлы, организовывать любой иерархию директорий. А вот к системным папкам у пользователя может быть только ограниченный доступ - на чтения, или вообще никакого. Так же в домашней папке пользователя могут храниться конфигурационные файлы, содержащие настройки системы или отдельных программ, относящиеся только к этому пользователю.
Обычно, при регистрации нового пользователя, его домашняя папка создается автоматически в папке /home/. И называется домашняя папка так же, как и сам пользователь. То есть у пользователя user домашняя папка по умолчанию будет /home/user. Но это можно изменить в конфигурационном файле или в команде создания пользователя. Обратите внимание, что сама папка /home/ не является домашней, и принадлежит администратору. То есть сам пользователь не сможет иметь к ней полного доступа.
Кстати, служебным пользователям, которые создаются автоматически и не соответствуют живому человеку, работающему за компьютером, домашние папки обычно не нужны. В таком случае, создание домашней папки можно пропустить, а в файле /etc/passwd будет просто пропуск.
И в последнем поле файла /etc/passwd хранится команда, которая автоматически запускается после авторизации данного пользователя в систему. Обычно, это интерпретатор командной строки /bin/bash. Именно из-за этого поведения после авторизации мы обычно видим интерфейс командной строки. Но имейте в виду, что у разных пользователей может быть установлены разные командные интерпретаторы по умолчанию. Bash - это всего лишь самый распространенный из них.
Еще при создании пользователя можно справочно установить некоторые необязательные поля - полное имя, номер кабинета, телефона. В настоящее время они практически никогда не используются и остаются пустыми.
Выводы:
- В Linux информация о всех пользователях хранится в файле /etc/passwd.
- Изначально он хранил пароли, но их давно оттуда убрали для безопасности и удобства.
- У каждого пользователя есть идентификатор - UID.
- У каждого пользователя есть символьное имя - логин.
- У пользователя может быть задан пароль на вход в систему.
- У пользователя может быть домашняя папка, интерпретатор по умолчанию.
- Еще можно задать много дополнительной информации - номер кабинета, телефона.
Зачем нужны группы?
С помощью учетных записей пользователей можно организовать простое разделение доступа, но этот механизм имеет свои ограничения. Что делать, если нужно разрешить доступ к данному файлу сразу нескольким пользователям, но не другим? Для этого в операционных системах существуют группы. Группа - это просто некоторое множество зарегистрированных пользователей, права для которых можно задавать для всей группы. Это очень удобно для более сложного управления правами. С одной стороны, у нас есть множество файлов (как мы помним, в UNIX все организовано так, что любые действия с системой - это взаимодействия с определенными файлами), с другой - множество пользователей. В таком случае взаимодействие многие-ко -многим очень неудобно. Введение групп пользователей позволяет решить эту проблему.
Читатель может спросить: а что делать, если необходимо дать определенные права нескольким группам? Не оказываемся ли мы в точно таком же затруднении, как с пользователями? На самом деле нет, потому что достаточно создать новую группу, в которую входят пользователи из всех нужных групп. Поэтому такой промежуточный механизм действительно решает проблему множественных прав.
Вся необходимая системе информация о группах хранится в файле /etc/group. Вот фрагмент содержания такого файла:
1
2
3
4
5
6
7
root:x:0:
daemon:x:1:
adm:x:4:syslog,user
user:x:1000:user
sambashare:x:133:koroteev
vboxusers:x:134:user
test:x:1001:
Структура этого файла очень проста. Каждая строка соответствует зарегистрированной группе. В строчке указывается 4 значения, разделенные символом двоеточия. После имени идет пароль группы. Этот механизм когда-то использовался для защиты групповых действий паролем, но сейчас у всех групп пароль не ставят, указывая символ x. Затем идет численный идентификатор группы. Группы нумеруются с нуля. Обычно, системные группы, которые создаются при установке ОС или системными программами и необходимы для ее функционирования имеют номера до 999. Группы, которые создаются пользователями или пользовательскими программами имеют номера с 100 и выше. После третьего двоеточия указывается список имен пользователей-членов группы через запятую.
В примере выше мы видим, например, группу root. Эта служебная группа используется как группа по умолчанию для одноименного пользователя root. Именно группу root как владельца получают файлы, созданные из-под администраторской учетки. Как видно, не во всех группах могут быть члены, некоторые группы создаются “на всякий случай”, и явлются пустыми.
Примечательная группа с идентификатором 1000 и именем user, таким же, как и имя пользователя. Дело в том, что при создании пользователя обычно автоматически создается группа с таким же именем и это пользователь туда добавляется. Это так называемая “группа по умолчанию” для каждого пользователя. Она нужна для задания группы-владельца файлов, создаваемых пользоватем.
Еще одна группа, которая фигурирует в примере - vboxusers. Ее членам разрешается доступ к механизмам виртуализации и виртуальным машинам. Поэтому добавление пользователя в эту группу эквивалентно разрешению ему производить соответствующие действия. Примерно также работает системная группа sudo или sudoers (в зависимости от дистрибутива). Членам этой группы разрешено пользоваться программой sudo, которая позволяет выполнить действия от имени пользователя root. Поэтому если вы хотите разрешить пользователю это действие, достаточно добавить его в эту группу.
На практике механизм групп часто используется самой системой и системными программами. Но иногда его заменяет другой механизм - сами пользователи. Вместо того, чтобы создавать специальную группу, которой разрешены определенные действия, можно создать одного специального пользователя, придать ему все необходимые права, и при необходимости, переключаться в учетную запись этого пользователя, либо запускать программы из-под учетки этого пользователя. Так работают, например, сетевые службы и системы управления базами данных. Но такая работа имеет свои недостатки и не всегда может заменить механизм групп. Поэтому группы все еще используются, хоть и не так часто.
Выводы:
- Группы были придуманы для удобства назначения прав доступа нескольким пользователям.
- Информация о группах хранится в файле /etc/group.
- Любой пользователь может быть членом одной или нескольких групп.
- При создании пользователя автоматически создается группа с таким же именем.
- В настоящее время группы используются не очень часто.
- Гораздо чаще создается специальный пользователь с нужными правами.
- Существует группа sudoers или sudousers - только ее члены могут использовать sudo.
Что такое суперпользователь?
Как мы говорили, пользователи в операционной системе нужны для разделения доступа - чтобы один пользователь в своей работе не мог никак нарушить работу другого пользователя. Поэтому учетные записи “обычных” пользователей в многопользовательских операционных системах всегда сильно ограничены в том, что они могут делать. Естественно, они не могут иметь доступ к файлам других пользователей (если те не разрешают это явно). Но еще они не могут выполнять действия, имеющие глобально влияние на всю операционную систему. Например, пользователи не должны менять общесистемные настройки, удалять общее программное обеспечение и так далее.
Но, конечно, иногда такие общесистемные действия приходится кому-то выполнять. Раньше, когда компьютеры были большими, корпоративными и действительно многопользовательскими, такую работу выполняли специально назначенные системные администраторы, которые по службе отвечали за настройку и поддержание работы системы. Сейчас зачастую компьютеры эксплуатируются в индивидуальном режиме, но название “администратор” для обозначения пользователя с расширенными правами осталось.
В UNIX-системах эта проблема решается простым и кардинальным способом. Существует специальный, особый пользователь root, которому в системе разрешено вообще все. В других операционных системах могут быть пользователи с разными уровнями доступа, здесь же все просто - либо обычный, ограниченный пользователь, либо root - суперпользователь. Этот особый пользователь всегда называется именно root, имеет идентификатор пользователя 0, всегда создается при установке системы. Удалить или переименовать его нельзя, это глубоко зашито в логику работы самой операционной системы.
Суперпользователь, кстати - вполне официальное название (superuser).
Пользователь root нужен именно для выполнения операций, затрагивающих систему в целом - установка и удаление программ, редактирование общесистемных настроек, управление пользователями (создание, удаление), управление системными службами и так далее. Это пользователь настолько не ограничен, что несовсем правильно даже говорить, что ему все разрешено. При совершении действий под учетной записью root права доступа даже не проверяются.
Это в частности значит, что root даже сам не может что-то себе запретить. Попробуйте на досуге создать файл (под любым пользователем, даже под рутом) и запретить всем любой доступ к нему, root все равно будет иметь к нему полный доступ. На этого пользователя просто не распространяется система прав доступа UNIX.
Есть еще одна причина, почему обычным пользователям не разрешается выполнять общесистемные действия. Такие действия несут потенциальную опасность. Обычные пользователи так ограничены, что любыми совими действиями они могут нарушить только свою собственную работу. На работу системы в целом они повлиять никак не могут. Суперпользователь root же напротив, может легко “сломать” всю систему целиком. Это особенно актуально сейчас, в эру персональных компьютеров. Не все владельцы ПК являются профессиональными системными администраторами. Поэтому лучше для обычных дел - редактирования файлов, использования браузера, и подобных - использовать учетную запись обычного пользователя.
Но выполнять настроечные, общесистемные действия все равно приходится регулярно и довольно часто. Каждый раз выходить их сеанса (то есть закрывать все запущенные процессы) и входить в сеанс рута, а потом обратно, очень неудобно. Для ускорения процесса есть две команды - sudo и sudo su. Команда sudo позволяет выполнить любую другую команду терминала с правами рута. Естественно, для этого нужно знать пароль рута. Так что система разделения прав доступа здесь не нарушается. Синтаксис команды sudo очень простой:
1
$ sudo <команда>
Команда sudo su позволяет временно (без выхода из сеанса) перети в сеанс рута. Она полезна, если нужно от имени суперпользователя выполнить сразу много команд и неудобно перед каждой писать “sudo”.
Этими командами надо пользоваться с осторожностью, так как они потенциально могут нарушить работу системы. Особенно аккуратно надо быть с командами, которые вы нашли в интернете для решения какой-то проблемы. Ведь они могут быть вредны (сознательно, а чаще несознательно). Желательно все-таки хотя бы в общих чертах понимать, что вы делаете, как работают те или иные команды и какие последствия будут от их применения. Если бездумно применять нагугленные команды рано или поздно вы обрушите все систему до состояния, когда восстановить ничего уже будет нельзя (и поверьте, любой системный администратор был в такой ситуации хотя бы однажды). Да и о работе системы ничего полезного не узнаете.
Обращаем внимание! Никогда не работайте постоянно под учетной запись root. Это очень опасно. Даже опытные администраторы предпочитают пользоваться командой sudo. От ошибок никто не застрахован, но работа в учетке обычного пользователя и регулярные бекапы могут свести последствия к минимуму.
Выводы:
- Обычные пользовательские учетные записи сильно ограничены в правах.
- Иногда необходимо произвести действия, оказывающие влияние на всю систему.
- Для этого есть специальная учетная запись суперпользователя - root.
- Во всех Linux-системах существует пользователь root с идентификатором 0.
- Именно под учетной записью суперпользователя нужно выполнять общесистемные действия: настройка, установка и удаление программ, добавление пользователей.
- Постоянно работать под учетной записью root категорически не рекомендуется, лучше иметь обычную учетку.
- Чтобы каждый раз не перелогиниваться для выполнения администраторских команд, есть специальная команда sudo.
- Командой sudo надо пользоваться с осторожностью, так как такие команды могут повредить работу операционной системы.
Какие основные действия с пользователями и группами?
Теперь, когда мы познакомились с основными механизмами учетных записей пользователей, необходимо узнать, как совершать те или иные операции с пользователями. Надо сказать, что вся информация о пользователях и группах хранится в конфигурационных файлах (/etc/passwd, /etc/shadow, /etc/group). Это не просто информационные файлы, это и есть сама конфигурация. Поэтому необходимую настройку можно делать непосредственно изменяя эти файлы. Но это неудобно и чревато ошибками. Ведь если вы нарушите синтаксис этих файлов, это может сломать всю систему.
Поэтому в составе дистрибутивов существуют служебные программы, которые автоматизируют и облегчают основные настроечные действия. В этом разделе мы рассмотрим только самые основные, которые чаще всего приходится совершать. Остальные команды и действия можно найти в соответствующих справочниках и документации.
Команда useradd позволяет добавить нового пользователя в систему. У нее есть один обязательный аргумент - имя нового пользователя.
1
sudo useradd test_user
Обратите внимание, что это действие - добавление пользователя - является общесистемным, поэтому может быть выполнено только администратором системы, то есть пользователем root. Здесь и далее мы будем обозначать это в командной строке использованием команды sudo. Если попытаться выполнить такую команду простым пользователем, вы получите ошибку Permission denied.
Команда useradd только лишь добавляет учетную запись пользователя без какой-либо другой информации. Сразу пользоваться этой новой учеткой будет нельзя - ведь у нового пользователя не установлен пароль. Установить или изменить пароль пользователя можно командой passwd:
1
passwd test_user
При выполнении этой команды вам будет предложено ввести новый пароль пользователя два раза - для надежности. А если пароль уже существовал, тогда еще придется ввести старый пароль. Обратите внимание, на этот раз команда sudo не обязательна - ведь пользователь может использовать команду passwd для изменения собственного пароля. Изменить пароль других пользователей, конечно, нельзя. А вот root может менять пароли любых пользователей.
Кроме имени пользователя и пароля у учетной записи есть еще много информации. Существует команда adduser, которая позволяет создать нового пользователя и сразу задать ему основную информацию, и парль в том числе:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo adduser test_user
Adding user `test_user' ...
Adding new group `test_user' (1001) ...
Adding new user `test_user' (1001) with group `test_user' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for test
Enter the new value, or press ENTER for the default
Full Name []:
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] Y
На примере выше видно, какую информацию можно задать. Большая часть этих полей носит справочный характер и чаще всего не используется. Поэтому ее просто можно оставить пустой.
Если хочется изменить эту информацию для уже существующего пользователя, вам понадобится команда usermod:
1
sudo usermod test_user
У этой команды вообще много опций, которые позволят отдельно задать конкретные параметры пользователя. Полный список опций доступен в man usermod.
Удалить пользователя можно командой userdel или deluser:
1
sudo userdel test_user
Существуют аналоги этих команд для создания, удаления и изменения групп - соответственно groupadd, groupdel и groupmod. Рассматривать их подробно мы не будем.
Из операций с группами чаще вего используется добавление пользователя в группу. Это происходит при помощи уже известной команды usermod с определенными опциями, которые стоит запомнить:
1
sudo usermod -a -G group_name user_name
Выводы:
- Команда useradd регистрирует нового пользователя в систему и все.
- Команда adduser более полная - создает пользователя, домашнюю папку, задает ему пароль.
- Команда userdel позволяет удалить пользователя.
- Команда passwd используется для изменения пароля пользователя.
- Пользователь может изменить свой пароль, суперпользователь может сменить пароль кому угодно.
- Команда usermod может изменить параметры пользователя.
- usermod -a -G добавляет пользователя в группу.
Что такое права доступа?
Мы рассмотрели, как устроены пользователи и группы, но еще не касались вопроса, а как именно им распределяются права на совершение тех или иных операций. И для того, чтобы приступить к рассмотрению механизма прав доступа, нужно вспомнить кое-что о UNIX-подобных системах. В них все объекты операционной системы представляются в виде файлов. Обычные файлы (документы) - это файлы, директории - это файлы, устройства - это тоже файлы.
Поэтому выполнение любых действий с операционной системой связано с доступом и выполнением определенных операций с какими-то файлами. Если пользователь хочет обменяться пакетами по сети, он должен получить доступ к файлу сетевого адаптера. Если он хочет изменить какую-то настройку, он должен изменить содержимое определенного конфигурационного файла. Если он хочет установить программу, он должен создать файлы в определенной директории (которая тоже файл). Поэтому чтобы ограничить возможные действия пользователя, достаточно ограничить ему доступ к определенным файлам.
Здесь и далее под файлом (без уточнений) мы будем понимать файлы в самом широком смысле - как объекты UNIX, включая документы, ссылки, каталоги, сокеты, устройства и так далее.
Каждый файл, существующий в UNIX-подобной операционной системе, имеет определенные внутренние атрибуты. В том числе у каждого файла есть пользователь, который считается его владельцем и группа, которая тоже считается его владельцем. Эти атрибуты устанавливаются при создании файла. Пользователем-владельцем файла является пользователь, который его создал (из-под чьей учетной записи произошло создание этого файла), а группой-владельцем записывается группа по умолчанию этого пользователя. Суперпользователь может изменить как пользователя так и группу-владельца файла, то есть “переписать” этот файл на другого пользователя и на другую группу. При этом, конечно, пользователь и группа, которые владеют файлом не обязаны соответствовать друг другу. Владелец может изменить, например, только группу-владельца файла.
Еще надо помнить, что любые действия в операционной системе совершаются только от имени какого-то зарегистрированного пользователя. Система не может что-то делать “сама”. Любой процесс инициируется каким-то пользователем и совершается от его имени. Даже если пользователь запустил программу, она тоже будет совершаться от его имени. Поэтому хотя с файлами, по сути дела, взаимодействуют именно программы, мы говорим о правах доступа пользователей. Любая программа запущена с правами того пользователя, который ее запустил. Или с правами той программы, которая запустила эту.
При этом взаимодействовать с файлом может, конечно, не только владелец но и другие пользователи. Как раз при попытке получить доступ к файлу операционная система проверяет, что за пользователь пытается это сделать. Здесь есть три варианта. Если идентификатор пользователя совпадает с идентификатором пользователя-владельца данного файла, то операционная система считает, что к файлу получает доступ владелец. Еще может быть, что пользователь не является владельцем файла, но входит в группу-владельца этого файла. Это второй случай. Если же ни то, ни то условие не выполняется, то операционная система считает пользователя “другим” (other). То есть по отношению к любому конкретному файлу данный пользователь может быть владельцем, членом группы или другим.
Но что именно значит “взаимодействие” с файлом? Ведь пользователь может совершать с файлами множество разных действий - копирование, редактирование, переименование, удаление и так далее. Всех не перечислишь и не учтешь. Операционные системы, основанные на UNIX, распознают всего три типа элементарных действий с файлами - чтение, запись и выполнение. Чтение - это когда пользователь пытается получить доступ к содержанию файла, но не изменяет его. Запись - это когда пользователь редактирует содержание файла, полностью или частично - неважно. Исполнение - это выполнение содержимого файла как программы. Это последнее действие, конечно, имеет смысл только для исполняемых файлов, но существует для всех. Как показывает практика, этих трех базовых действий вполне достаточно, они как кирпичики составляют все наше взаимодействие с операционной системой.
Таким образом, любой пользователь может иметь или не иметь право на чтение запись и выполнение определенного файла. В самом файле эти права фиксируются для трех уже знакомых категорий пользователей - владельца файла, членов группы-владельца файла и для всех остальных. Так что когда пользователь взаимодействует с файлом, система сначала устанавливает его роль по отношению к файлу - владелец, член группы или другой - а потом проверяет, какое действие пользователь пытается совершить. Если файл разрешает это действие данной категории пользователей - все хорошо, операционная система совершает это действие. Если не разрешено, то операционная система выдает ошибку Permission denied - запрещено.
Да, теоретически можно иметь доступ на запись к файлу без доступа на чтение. Тогда вы сможете перезаписать файл целиком, но не сможете увидеть, что там было или есть. На практике, конечно, такая комбинация практически не используется, но теоретически возможна.
Каждое элементарное право доступа может быть либо установлено либо нет. То есть занимает ровно один бит. Три базовых права доступа для трех категорий пользователей дает нам девять бит прав доступа - чуть больше байта. Эти девять бит вы можете увидеть в выводе команды ls -l. В первом столбце мы видим десять символов. Первый - это тип файла. А следующие девять - как раз права доступа в формате rwxrwxrwx. R (read) - это право на чтение. W (write) - запись. X (execute) - право на выполнение. Первая тройка задает права владельца файла, вторая - членов группы, а третья - для всех остальных. Если на соответствующем месте стоит буква, значит право есть, а если прочерк (дефис) - права нет. Это, кстати, значит, что владелец не обязательно имеет полный доступ к файлу, он может сам себя ограничить.
А что с другими действиями с файлами - копированием, перемещением, удалением и так далее? Разве они не учитываются операционной системой? На самом деле учитывается все, но не всегда так, как ожидается. Например, права доступа rwx имеют особое значение для каталогов. Если вспомнить, что каталог - это просто файл со списком других файлов, то все становится просто. Право на чтение каталога - это право просматривать его содержимое, то есть список файлов в нем. Право на запись - это право на изменение этого списка - создание, переименование и перемещение файлов в нем. А вот право на исполнение - особое. Это право делать этот каталог текущим, то есть переходить в него.
Получается, что операционная система учитывает любые действия пользователя. Только некоторые не являются элементарными. Например, чтобы иметь возможность скопировать файл в другую директорию, вы должны иметь право на чтения данного файла, директории, в которой он лежит и право на запись в той директории, куда вы его копируете. Чтобы переместить файл, вам нужно право на запись и исходной и в конечной директории (а вот прао на чтение файла не обязательно).
Сами права доступа, кстати, доступны всем, кто может просмотреть список файлов директории, то есть для просмотра прав доступа к файлу надо иметь право на чтение директории, в которой он лежит.
Выводы:
- Любые действия в системе совершаются от имени какого-то пользователя.
- У каждого файла есть владелец и группа-владелец, которые задаются при создании файла.
- По отношению к файлу пользователь может быть владельцем, членом группы или никем.
- Права доступа к файлу задаются для каждой из этих трех категории пользователей.
- Существует три базовых права доступа - на чтение, запись и выполнение.
- Девять базовых прав доступа отображаются в атрибутах файла командой ls -l.
- Для каталогов права доступа имеют свой смысл: чтение списка файлов, создание и удаление файлов и переход в эту папку соответственно.
- Суперпользователь всегда имеет все права - они для него просто не проверяются.
Как задавать права доступа?
Кончено, при работе с операционной системой приходится постоянно иметь в виду систему прав доступа. Мы уже знаем, как посмотреть права доступа к файлу. Теперь познакомимся с основными действиями, которые можно сделать из командной строки по отношению к файлам, его владельцам и правам доступа.
Одно из самых распространенных действий - смена владельца или группы файла. Для этого существует команда chown. Вот ее общий синтаксис:
1
sudo chown new_owner:new_group file1 file2 …
После команды указывается новый владелец (имя, не идентификатор) и новая группа владелец файла. Можно изменить владельцев сразу у нескольких файлов, можно сделать это рекурсивно по директории. Также можно отдельно задавать только владельца файла:
1
sudo chown new_owner file1
Или только группу файла:
1
sudo chown :new_group file1
Обратите внимание, что сменить владельцев файла может только суперпользователь.
Но самая популярна операция - смена самих прав доступа. При работе с операционной системой постоянно приходится кому-то что-то разрешать или запрещать делать. Для изменения прав доступа служит команда chmod. С помощью нее мы может добавить, удалить какие-то отдельные права или переписать права полностью.
Для того, чтобы научиться менять права доступа, надо вспомнить внезапно восьмеричную систему счисления. Дело в том, что права доступа могут обозначаться двумя способами. Первый - уже знакомыми символами rwx. Мы можем написать команду, например, так:
1
chmod +x file_name
Это действие разрешит выполнение данного файла всеми пользователями (то есть установит соответствующий бит для всех трех категорий пользователей). А что делать, если нужно установить только для определенных категорий? Для этого есть специальные обозначения категорий - u (user, владелец файла), g (group, члены группы-владельца), o (other, все остальные пользователи). Поэтому мы можем написать, например так:
1
chmod u+x file_name
что разрешит выполнение только владельцу. Или так:
1
chmod ug+x file_name
что разрешит выполнение владельцу и членам группы. Конечно это можно делать с любыми правами, не только x.
А вот как можно удалить какое-нибудь право:
1
chmod go-w file_name
Эта команда запретит изменение файла всем, кроме владельца.
Но иногда такая схема не очень удобна. Что если мы хотим полностью переопределить права доступа, и не хотим высчитывать, что нужно установить, а что удалить. Для этого существует вторая - восьмеричная запись прав доступа. Дело в том, что три бита прав для определенной категории пользователей представляют собой восьмеричное число - то есть цифру от 0 до 8. И каждая такая цифра определяет свой набор прав доступа. Для того, чтобы понять эту систему, достаточно познакомиться с такой таблицей:
OCT | BIN | Mask | Права на файл | Права на каталог |
---|---|---|---|---|
0 | 000 | - - - | отсутствие прав | отсутствие прав |
1 | 001 | - - x | запуск на выполнение | доступ к файлам и их атрибутам |
2 | 010 | - w - | изменение содержимого | создание и удаление файлов |
3 | 011 | - w x | права на запись и выполнение | все, кроме доступа к именам файлов |
4 | 100 | r - - | чтение содержимого | только чтение имен файлов |
5 | 101 | r - x | права на чтение и выполнение | чтение имен файлов и доступ файлам и их атрибутам |
6 | 110 | r w - | права на чтение и запись | только чтение имен файлов |
7 | 111 | r w x | полные права | все права |
Права для трех категорий задаются, соответственно, тремя восьмеричными числами. Так, например, 777 - это полные права для всех. 700 - это полные права для владельца и отсутствие прав для остальных. 640 - это права на чтение и изменение для владельца, только на чтение для группы и никаких прав для остальных.
На первый взгляд кажется сложным, но довольно быстро запоминается, а такую табличку легко нагуглить, если она забылась. Но такая форма позволяет очень быстро и компактно записать полный набор прав доступа к файлу или директории.
И эту восьмеричную запись также можно использовать в команде chmod:
1
chmod 640 file_name
Такая команда полностью переопределяет все права доступа, неважно, какие они были раньше.
Но иногда вы можете встретить запись прав доступа, состоящую не из трех, а из четырех цифр. Например, так: 0777. Эта более полная форма прав доступа. Дело в том, что помимо девяти основных прав есть еще три специальных бита прав доступа. Именно они и кодируются первой цифрой в четырехзначном обозначении.
Первый из них - Setuid или SUID бит. Если он установлен, то пари запуске данного файла на исполнение он будет запущен с правами своего владельца, а не того пользователя, который его запустил. Такой бит используется, например, в команде sudo. Подумайте, почему этой команде нужен SUID и почему без него она не будет работать. Если вы посмотрите на ее права, то увидите строку rwsr-xr-x. Вот эта s на месте права на выполнение и есть SUID-бит. Установить его можно такой командой:
1
chmod u+s file_name
Следующий специальный бит - Setgid или SGID-бит. Он также работает только с исполняемыми файлами, но файл будет запускаться с правами группы-владельца файла, а не с правами группы пользователя, который его запустил. Этот бит используется гораздо реже. Например, в программе crontab права выглядят так: rwxr-sr-x. Обратите внимание на s в правах группы. Установить SGID можно командой:
1
chmod g+s file_name
И, наконец, Sticky-bit. Это право доступа используется для директорий. Если установлен этот бит, то удалить файл из папки сможет только владелец файла, а не тот, у кого есть право на запись в этой папке. Это полезно для организации коллективных хранилищ, в которых создавать файлы может любой пользователь, а удалять - только тот, кто создал. Например, так работает системная папка /tmp/. Так выглядят ее права: rwxrwxrwt. последний символ t - это и есть индикация Sticky-bit. Установить его можно так:
1
chmod +t file_name
А откуда берутся права только что созданных файлов? Для этого в системе действует так называемая маска. Базовые права (то есть права по умолчанию) для директорий равны 0777, то есть полные права для всех, а для файлов - 0666, то есть доступ всем на чтение и запись. Но из этих базовых прав вычитается значение, заданное в конфигурации для каждого пользователя. Это значение называется umask - пользовательская маска. Она обычно равна 0002 или 0022. Узнать ее текущее значение можно командой umask без параметров:
1
2
umask
0002
В нашем примере маска равна 0002. То есть директории будут создаваться с правами 0775 (rwxrwxr-x), а файлы - 0664 (rw-rw-r–).
Маска нужна исключительно для удобства. Она задает права по умолчанию при создании новых файлов. Изменить маску можно той же командой, но с параметром:
1
umask 0022
Выводы:
- Можно изменить владельца и группу файла командой chown.
- Права доступа к файлу может изменить владелец или рут командой chmod.
- Права доступа задаются в символьном или в восьмеричном выражении.
- Можно задать специальные биты прав доступа - SUID, SGID и Sticky-bit.
- Новые файлы получают стандартные права доступа, заданные командой umask.
Управление пакетами
Как в Linux распространяются программы?
Выводы:
- Любая операционная система должна иметь возможность установки сторонних программ.
- Современные сложные программы состоят из многих файлов и требуют специальной процедуры установки.
- Поэтому они распространяются в виде программных пакетов.
- В каждой операционной системе формат пакета свой, даже в разных дистрибутивах разный.
- Программы могут зависеть друг от друга, поэтому у пакетов тоже могут быть зависимости.
- Программы также могут распространяться в виде исходного кода.
Что такое пакет?
Выводы:
- Пакет содержит всю информацию, которая необходима для установки и работы программы.
- В виде пакетов распространяются программы, прикладные и системные библиотеки.
- Кроме самого кода программы в нем содержатся ее ресурсы, а также инструкции для установки.
- Все дистрибутивы, основанные на Debian, используют deb-пакеты.
- Семейство RedHat использует пакеты RPM.
- Один пакет может зависеть от десятков других.
Что такое пакетный менеджер?
Выводы:
- Пакетный менеджер - это программа, которая автоматизирует работу с пакетами.
- В разных дистрибутивах разные пакетные менеджеры.
- Пакетный менеджер может установить программу из пакета и сам следит за зависимостями.
- Пакетный менеджер - это консольная программа, но есть графические интерфейсы.
- Существуют программные контейнеры, типа flatpack, которые не имеют зависимостей, но весят сильно больше.
Что такое репозитории пакета?
Выводы:
- Репозиторий - это каталог программных пакетов, как магазины приложений.
- Производители популярных дистрибутивов поддерживают собственные репозитории.
- Пакетный менеджер использует репозитории для поиска и установки пакетов, отображения информации.
- К пакетному менеджеру в системе можно подключить несколько репозиториев.
- Существуют сторонние репозитории от производителей программ.
- Каждый дистрибутив может пользоваться репозиториями родственных ему.
Как в Linux Mint устанавливать программы?
1
2
sudo apt update
sudo apt install tmux
Выводы:
- Команда apt install <packagename> установит программу из репозитория.
- Операции с пакетами обычно требуют привилегий суперпользователя, поэтому sudo apt…
- Для установки программы нужно знать название ее пакета.
- Программа установится только если она есть хотя бы в одном подключенном репозитории.
- Можно указать несколько программ через пробел.
- Обычно при начале работы с дистрибутивом устанавливают программы по списку.
- Опция -y подавляет запрос подтверждения, полезно в скриптах.
Какие еще операции с пакетными менеджерами нужно знать?
Выводы:
- Команда apt remove <packagename> удаляет пакет. Также можно указать несколько пакетов.
- Команда apt purge удаляет пакет и все связанные файлы, неиспользуемые зависимости.
- Команда apt update обновляет список репозиториев, надо выполнять как можно чаще.
- Команда apt install <packagename> обновляет пакет до новейшей версии.
- Команда apt upgrade обновляет все установленные пакеты. Может работать очень медленно.
- Команда apt dist-upgrade обновляет сам дистрибутив до новейшей версии.
- Команда apt search [keyword] позволяет найти нужный пакет по ключевому слову.
Управление процессами
Что такое процесс?
Выводы:
- Процесс - это программа, запущенная на выполнение в оперативной памяти.
- Одна программа может порождать несколько процессов.
- Одну программу можно запустить несколько раз, и это будут разные процессы.
- Все процессы выполняются независимо друг от друга, они изолированы операционной системой.
- У каждого процесса есть свой выделенный участок памяти, в память других процессов ему доступ запрещен.
Что такое многозадачность?
Выводы:
- В каждый момент в операционной системе выполняется множество процессов.
- На одном ядре ЦП может выполняться только одна последовательность инструкций одновременно.
- Операционная система постоянно переключает процессы на выполнение.
- Иногда переключение происходит добровольно, но чаще - нет.
- Сам процесс не имеет контроля над тем, когда его переключат.
- Большинство современных операционных систем реализуют вытесняющую многозадачность.
В каких состояниях может находиться процесс?
Выводы:
- Создание нового процесса - довольно длительный для компьютера процесс.
- После создания процесс в состоянии готовности становится в очередь.
- Когда очередь подходит, он начинает выполняться на процессоре.
- Если во время выполнения процесс завершается, ОС его уничтожает и очищает память.
- Если во время выполнения проходит квант времени, ОС опять ставит его в очередь.
- Если во время выполнения процесс блокируется, ОС его приостанавливает.
- Когда придет внешнее событие, разблокирующее процесс, он опять становится в состояние готовности.
- Существует много разных реализаций очереди процессов, с приоритетами и без.
Какую информацию ОС хранит о процессе?
Выводы:
- У каждого процесса есть численный идентификатор.
- С каждым процессом связан пользователь-владелец.
- У каждого процесса (за исключением одного) есть процесс-родитель.
- ОС хранит информацию о процессах в специальном разделе - файловой системе /proc.
- ОС запоминает статистическую информацию о потреблении процессами ресурсов.
- Большая часть информации о процессах скрыта. Например, открытые файловые дескрипторы.
Как процессы связаны с терминалами?
Выводы:
- Каждый процесс при запуске связывается с определенным терминалом.
- Это может быть виртуальный терминал, графическая программа или эмулятор.
- Это связывание используется для разделения потоков ввода-вывода.
- При запуске процесса терминал блокируется.
- Некоторые процессы не связаны с терминалами.
Как посмотреть выполняемые процессы?
Выводы:
- Самая распространенная команда - ps.
- По умолчанию она показывает только процессы текущего пользователя, связанные с текущим терминалом.
- Полный список процессов можно посмотреть командой ps aux.
- Интерактивную информацию о процессах показывает программа top.
- Существует улучшенный вариант top - htop.
- С помощью htop можно проводить полноценный мониторинг системы, выполнять операции над процессами.
Что такое процессы-демоны?
Выводы:
- Демоны не связаны с терминалами и не имеют пользовательского интерфейса.
- Они используются для выполнения фоновых операций.
- Большинство демонов - системные службы.
- Можно создать свою собственную программу-демона.
- Существует специальная программа для управления службами.
Как снять процесс?
Выводы:
- Завершить текущий процесс в терминале можно комбинацией Ctrl + C.
- Интерактивные программы нужно стараться завершать штатно.
- Снять индивидуальный процесс можно с помощью программы kill.
- Формально, это послание сигнала процессу.
- Для мягкого завершения используется сигнал 15.
- Для того, чтобы снять зависший процесс может понадобится сигнал 9.
- Убить все процессы по имени команды можно командой killall.
Как связаны родитель и потомок?
Выводы:
- Для каждого процесса, кроме самого первого существует процесс-родитель.
- Поэтому процессы в Linux образуют дерево.
- При завершении родителя, все его потомки тоже завершаются.
- Процесс наследует от родителя пользователя и права доступа.
- В редких случаях пользователь может измениться.
Что такое приоритет процесса?
Выводы:
- Приоритет процесса - это число, определяющее, как часто процесс будет получать процессорное время.
- В Linux также используется число nice value - величина, обратная приоритету.
- Nice измеряется от -20 (высший приоритет) до 19 (низший приоритет).
- Пользователь может понизить приоритет своих процессов.
- Повысить приоритет может только суперпользователь.
- Можно запустить процесс с нестандартным приоритетом командой nice -n.
- Можно изменить приоритет уже выполняемого процесса командой renice -n.
- Эти команды задают числа, которые прибавляются к nice value.
Что такое фоновые процессы?
Выводы:
- Можно запустить процесс без привязки к терминалу, в фоновом режиме с помощью &.
- Если мы хотим перевести процесс в состояние остановленный, используется сочетание клавиш «Ctrl + z».
- Можем переместить остановленный процесс на передний план командой fg.
- Можно продолжить выполнение остановленного процесса в фоновом режиме командой bg.
- Используя команду jobs мы можем получить список остановленных и фоновых процессов.
Система контроля версий Git
Основные понятия СКВ
Зачем контролировать версии?
Выводы:
- Вы разрабатываете большой и длительный проект.
- Большинство работы происходит в текстовых файлах.
- Вы часто вносите изменения и, бывает, откатываетесь назад.
- Вам нужно запоминать некоторые состояния (например, гарантированно стабильно работающие, или окончательные).
- Несколько человек работает одновременно над одним проектом.
- Вы хотите проводить ревизию изменений другого участника.
- Вы или другие работаете распределенно и вам нужно облачное хранилище
- Как Timeshift в Linux.
- Вы хотите отслеживать, кто вносит какие изменения.
- Могут происходить конфликты версий
Как происходит контроль версий в ручном режиме?
Выводы:
- История прошлых версий файла/файлов.
- В простых случаях - рабочий вариант.
- Быстро становится неуправляемой.
- Если работает несколько человек - адище.
- Конфликты версий приходится разрешать в ручном режиме.
- Занимает много места.
- Нет индексации, статистики, blame, синхронизации и прочих плюшек.
Какие есть СКВ?
Выводы:
- Subversion, Mercurial, Team Foundation Server.
- Git - сегодня самая популярная.
- Была создана Линусом Торвальдсом, когда его достали человеки.
- Создана в первую очередь для программистов.
- Знать git - сегодня это обязательно для любого айтишника.
Как СКВ меняет рабочий процесс?
Выводы:
- Ничего не удаляется. Информация не потеряется.
- Нужно решить, как часто делать коммиты.
- Позволяют создавать разные варианты одного документа, т. н. ветки, с общей историей изменений до точки ветвления и с разными — после неё.
- Дают возможность узнать, кто и когда добавил или изменил конкретный набор строк в файле.
- Ведут журнал изменений, в который пользователи могут записывать пояснения о том, что и почему они изменили в данной версии.
- Контролируют права доступа пользователей, разрешая или запрещая чтение или изменение данных, в зависимости от того, кто запрашивает это действие.
Какие основные понятия СКВ?
Выводы:
- Рабочая директория - папка, в которой хранятся все файлы проекта и в которой работает СКВ.
- Отслеживание файла - файлы в рабочей директории могут быть добавлены под СКВ или нет. Это сделано для того, чтобы не отслеживать часто меняющиеся, но неважные для рабочего процесса файлы, например: объектные файлы, настройки сборщика и IDE, временные файлы, кэш и так далее.
- **Состояние **- снимок рабочей директории в определенный момент времени.
- Репозиторий - служебное хранилище СКВ, в котором хранится информация о всех зафиксированных состояниях рабочей директории. Обычно является скрытой подпапкой в рабочей директории, но не входит в нее.
- Коммит - фиксация изменений в новое состояние.
- История - упорядоченная последовательность коммитов конкретного проекта начиная от исходного.
- Удаленный репозиторий - другой репозиторий, часто расположенный на другой машине, с которым можно наладить взаимодействие (синхронизацию). Используется для организации распределенной работы над проектом.
- Клонирование - создание локальной копии удаленного репозитория. Может происходить по протоколу ssh, HTTPS или просто копи{: .align-center style=”width: 800px;”}рованием архива.
- Апстрим - обычное название репозитория, служащего исходным для текущего. Обычно обозначается origin.
Основы работы с Git
Как установить и настроить git?
Выводы:
- Для работы нужно установить клиент git. Обычно это свободные и бесплатные программы.
- Существуют консольные и графические клиенты.
- Существуют под все стандартные платформы.
- В Linux консольный клиент - чаще всего установлен в дистрибутиве по умолчанию.
- Самая распространенная программа под Windows - git-scm.
- После установки нужно произвести первоначальную настройку - ввести имя пользователя и e-mail. Это требуется только для идентификации пользователя. Это не регистрация.
Какой простейший алгоритм использования СКВ?
1
2
3
4
5
6
cd project/
git init
git add main.py
git commit -m “Initial commit”
# . . .
git commit -m “New feature added”
Выводы:
- Установить и настроить клиент git
- Создать репозиторий в папке с проектом
- Добавить необходимые файлы под СКВ
- Сделать первоначальный коммит
- Сделать логически завершенный участок работы
- Сделать новый коммит с поясняющим сообщением
- Повторить сколько нужно
- Если что-то сломалось, вы всегда сможете вернуться на любое состояние.
Как Git хранит состояния?
Выводы:
- Связный список - у состояния есть родитель.
- Вычисляет патч между состоянием и его родителем.
- Работает строго построчно.
Как посмотреть историю коммитов?
Выводы:
- Команда git log выводит описание последних коммитов.
- Обратите внимание, что коммиты идентифицируются хешем. Вы можете использовать этот хэш как ссылку на коммит. Необязательно использовать весь хеш, только уникальную часть.
- при каждом коммите фиксируется дата, время, пользователь, совершивший коммит, сообщение, ссылка на родителя, хеш коммита.
Как перейти к другому состоянию?
Выводы:
- Команда git checkout приводит состояние рабочей директории в соответствие с выбранным коммитом.
- Если в это время какие-то файлы были изменены, изменения потеряются.
- Можно возвращать не всю рабочую директорию, а только определенные файлы.
- Всегда можно воспользоваться встроенной справкой по командам.
Как посмотреть изменения?
Выводы:
- Команда git diff показывает изменения каждого файла по сравнению с содержанием индекса или, при указании, с любым коммитом.
- Команда показывает, какой текст был удален, а какой - добавлен.
- Очень полезная команда для просмотра изменений.
- Эта команда работает построчно. Если строка изменена, то это будет отображаться как удаление старой версии и вставка новой.
- Существует множество графических реализаций данного функционала
Каков жизненный цикл файла в git?
Выводы:
- В Git различают три области: историю зафиксированных состояний, текущее состояние рабочей директории и индекс.
- Рабочая директория - это то, что мы видим в окне проводника. Мы работаем с файлами в ней, можем их редактировать любыми программами как обычно.
- Индекс - это набор файлов рабочей директории, которые отслеживаются СКВ. Когда мы делаем коммит, содержимое индекса записывается в историю состояний.
- Если мы создадим файл в рабочей директории, он не будет отслеживаться СКВ. Это сделано для того, чтобы не вычищать постоянно мусор из служебный файлов.
- Файл можно добавить под СКВ командой git add. в дальнейшем будем говорить только о файлах, отслеживаемых СКВ.
- Если мы изменили файл в рабочей директории, он становится модифицированным по сравнению с последним коммитом.
- Когда мы делаем коммит, мы записываем новое состояние и все файлы опять становятся неизмененными.
- Во многих графических средах состояние файла отображается цветом для удобства.
Как начать работу с репозиторием?
Выводы:
- Нативно, гит имеет командный интерфейс. Даже если вы планируете использовать только графические клиенты или IDE с интеграцией, полезно знать основные команды и их применение.
- Команда git init инициализирует пустой репозиторий в текущей папке.
- Команда git add добавляет файлы в индекс.
- Команда git commit фиксирует изменения. При этом в командной строке откроется редактор по умолчанию, чтобы вы могли записать сообщение коммита. Рекомендуется использовать форму git commit -m “Описание изменений”.
- Можно сделать коммит и добавить все новые файлы автоматически с помощью git commit -a
- Коммиту можно назначить метку для упрощения ориентации по истории.
Работа с ветвлением
Зачем нужны ветки?
Выводы:
- Ветвление происходит, когда мы произвели два разных изменения, основываясь на одном и том же состоянии.
- Часто происходит при командной работе.
- Кажется, что это что-то плохое.
- Git позволяет работать с ветками комфортно и, при необходимости, объединять их.
- Ветвление - это способ изолировать одни изменения от других. Например, разработка одной фичи независимо от хода работ над другой.
Как создать новую ветку?
Выводы:
- Ветки - это специальные указатели на коммиты, которые перемещаются с каждым новым коммитом.
- Создать новую ветку можно командой git branch <name>
- Команда git branch выводит все ветки.
- Ветка по умолчанию по соглашению называется master.
- Git хранит специальный указатель HEAD - он показывает, на какой ветке вы сейчас находитесь
- Чтобы перейти на существующую ветку, вам надо выполнить команду git checkout
- Ветка, на которую указывает HEAD, движется вперёд с каждым коммитом.
- Теперь коммиты, которые мы будем делать, будут относиться к новой ветке.
Как переключиться на другую ветку?
Выводы:
- В любой момент можно перейти в другую ветку и начать коммитить в нее.
- При переходе в другую ветку состояние рабочей директории приводится в соответствие с состоянием последнего коммита в этой ветке. Поэтому все изменения будут основываться на том состоянии.
- Переход между ветками откатывает изменения, которые вы делали в другой ветке, но не удаляет их. Все фиксированные изменения сохраняются в истории веток.
- При использовании веток история коммитов уже не будет линейной.
Как объединять ветки?
Выводы:
- Две ветки можно объединить (слить). Это значит, что мы применяем к файлам изменения, которые произошли и в одной ветке и во второй.
- Команда git merge <branch> сливает указанную ветку в текущую. Это значит, что создастся новый коммит в текущей ветке, учитывающий изменения в указанной.
- Коммиты слияния имеют двух родителей.
- Git автоматически определяет наилучшего общего предка для слияния веток.
- После слияния ветка не уничтожается и над ней можно продолжать работать.
Как разрешать конфликты слияния?
Выводы:
- Если вы изменили одну и ту же часть файла по-разному в двух ветках, которые собираетесь слить, Git не сможет сделать это чисто. Такая ситуация называется конфликтом слияния.
- В таком случае, git приостанавливает слияние до тех пор, пока вы не разрешите конфликт.
- Посмотреть, какие файлы не прошли слияние можно командой git status.
- Содержимое спорных файлов в рабочей директории будет отражать оба конфликтующих варианта текста.
- Чтобы разрешить конфликт, вы должны либо выбрать одну из этих частей, либо как-то объединить содержимое по своему усмотрению.
- После этого необходимо выполнить добавление этих файлов к индексу и коммит для завершения слияния веток.
Работа с удаленными репозиториями
Зачем нужны удаленные репозитории?
Выводы:
- При командной разработке приходится синхронизировать работу нескольких программистов в одном месте.
- Даже одному разработчику иногда приходится работать и вносить изменения в проект из разных мест.
- Для этого в системах контроля версий предусмотрена возможность связать локальный репозиторий с другим - удаленным.
- При связывании репозиториев возникает возможность отправить изменения, сделанные в локальном репозитории в удаленный и наоборот - скачать изменения из удаленного репозитория в локальный.
- Чтобы просмотреть, какие удалённые серверы у вас уже настроены, следует выполнить команду git remote. Она перечисляет список имён-сокращений для всех уже указанных удалённых дескрипторов. Если вы склонировали ваш репозиторий, у вас должен отобразиться, по крайней мере, origin — это имя по умолчанию, которое Git присваивает серверу, с которого вы склонировали.
Что такое DVCS?
Выводы:
- Распределенная модель git позволяет гибко взаимодействовать разработчикам в командном проекте.
- В централизованных системах все разработчики являются узлами сети, более или менее одинаково работающими на центральном хабе.
- Это означает, что если два разработчика склонируют репозиторий и каждый внесет изменения, то первый из них сможет отправить свои изменения в репозиторий без проблем. Второй разработчик должен слить изменения, сделанные первым разработчиком, чтобы избежать их перезаписи во время отправки на сервер.
- Однако, в Git’е каждый разработчик потенциально является и узлом, и хабом. То есть каждый разработчик может как вносить код в другие репозитории, так и содержать публичный репозиторий, на основе которого работают другие разработчики, и в который они вносят свои изменения.
Как связать текущий репозиторий с другим?
1
2
3
4
5
6
$ git remote
origin
$ git remote add pb git://github.com/paulboone/ticgit.git
$ git remote -v
origin git://github.com/schacon/ticgit.git
pb git://github.com/paulboone/ticgit.git
Выводы:
- Чтобы добавить новый удалённый Git-репозиторий под именем-сокращением, к которому будет проще обращаться,** выполните git remote add [сокращение] [url]**
- Теперь вы можете использовать в командной строке имя pb вместо полного URL.
- Если вы хотите извлечь (fetch) всю информацию, которая есть в удаленном репозитории, но нет в вашем, вы можете выполнить git fetch pb.
- Ветка master удаленного репозитория теперь доступна локально как pb/master. Вы можете слить (merge) её в одну из своих веток или перейти на эту ветку, если хотите её проверить.
Как склонировать репозиторий?
Выводы:
- Для получения копии существующего Git-репозитория, например, проекта, в который вы хотите внести свой вклад, необходимо использовать команду git clone.
- Вместо того, чтобы просто получить рабочую копию, Git получает копию практически всех данных, которые есть на сервере.
- Эта команда создаёт директорию “libgit2”, инициализирует в ней поддиректорию .git, скачивает все данные для этого репозитория и создаёт (checks out) рабочую копию последней версии. Если вы зайдёте в новую директорию libgit2, то увидите в ней файлы проекта, готовые для работы или использования.
- В Git реализовано несколько транспортных протоколов, которые вы можете использовать. В предыдущем примере использовался протокол https://, вы также можете встретить git:// или user@server:path/to/repo.git, использующий протокол передачи SSH.
Как получить изменения из апстрима?
Важно отметить, что команда git fetch забирает данные в ваш локальный репозиторий, но не модифицирует то, над чем вы работаете в данный момент.
Можно использовать команду git pull чтобы автоматически получить изменения из удалённой ветви и слить их со своей текущей ветвью.
Выполнение git pull, как правило, извлекает (fetch) данные с сервера, с которого вы изначально склонировали, и автоматически пытается слить (merge) их с кодом, над которым вы в данный момент работаете.
Выводы:
- Для получения изменений используется команда
git fetch
. - Чтобы автоматически слить изменения, используется команда
git pull
. - Если необходимо, при выполнении
git pull
могут создаваться коммиты слияния.
Как отправить изменения в апстрим?
1
$ git push origin master
Когда вы хотите поделиться своими наработками, вам необходимо отправить (push) их в главный репозиторий.
Команда для этого действия: git push [удал. сервер] [ветка].
Если вы и кто-то ещё одновременно клонируете, затем он выполняет команду push, а затем команду push выполняете вы, то ваш push точно будет отклонён.
Выводы:
- Для отправки своих изменений на сервер применяют команду
git push
. - Эта команда не будет производить автоматических слияний.
- Если на сервере что-то изменилось, ваш
git push
будет отклонен. - В таком случае, нужно выполнить
git pull
и затем опятьgit push
.
Работа с GitHub
Что это такое и зачем это нужно?
Выводы:
- Гитхаб это крупнейшее хранилище Git репозиториев, а также центр сотрудничества для миллионов разработчиков и проектов.
- Огромный процент репозиториев хранится на Гитхабе.
- Многие проекты с открытым исходным кодом используют его ради Git хостинга, баг-трекера, рецензирования кода и других вещей.
- Гитхаб даже используется для отслеживания динамики популярности языков программирования.
Как создать репозиторий?
Выводы:
- Первым делом нужно создать беcплатную учетную запись.
- В панели управления справа нажмите кнопку “New repository”.
- Всё, что в действительности нужно сделать, так это указать название проекта, все остальные поля опциональны.
- Теперь ваш проект хостится на GitHub и вы можете предоставить ссылку на него любому желающему.
- Все проекты на GitHub доступны как по HTTP https://github.com/<пользователь>/<имя_проекта>, так по SSH.
- Git производит контроль доступа на основании учётных данных пользователя, осуществляющего подключение.
Что такое форк?
Если вы хотите вносить свой вклад в уже существующие проекты, в которых у нас нет прав на внесения изменений путем отправки (push) изменений, вы можете создать свое собственное ответвление (“fork”) проекта.
Это означает, что GitHub создаст вашу собственную копию проекта, данная копия будет находиться в вашем пространстве имен и вы сможете легко делать изменения путем отправки (push) изменений.
Таким образом, проекты не обеспокоены тем, чтобы пользователи, которые хотели бы выступать в роли соавторов, имели право на внесение изменений путем их отправки (push). Люди просто могут создавать свои собственные ветвления (fork), вносить туда изменения, а затем отправлять свои внесенные изменения в оригинальный репозиторий проекта путем создания запроса на принятие изменений (Pull Request)
Для того, чтобы создать ответвление проекта (fork), зайдите на страницу проекта и нажмите кнопку “Cоздать ответвление” (“Fork”), которая расположена в правом верхнем углу.
Выводы:
- Форки нужны для внесения изменений в чужие репозитории.
- Форк - это ваша собственная копия проекта со всеми коммитами и историей.
- При создании форка автоматически проставляется ссылка на изначальный репозиторий.
- Автор репозитория может увидеть, кто его форкнул.
Что такое пулл реквест?
Выводы:
- При работе над форком чужого репозитория возникает необходимость отправить свои изменения в апстрим.
- Напрямую писать в репозиторий может только его автор или члены команды разработчика проекта, то есть его владельцы.
- Это сделано для разграничения прав доступа в репозитории.
- Поэтому, вы не сможете напрямую выполнить push в чужой репозиторий.
- Вместо этого вы отправляете pull request - запрос на внесение изменений.
- Этот запрос становится виден автору оригинального репозитория. Он может просмотреть вашу работу и затем либо принять его, либо отклонить.
- Пулл реквесты на гитхабе оформляются как публичные посты, где все участники могут оставлять свои комментарии.
Использование GitFlow
Для чего это нужно?
Выводы:
- Методология работы с git
- Стандартизировать рабочий процесс с использованием веток
- Вводит некоторые полезные и распространенные понятия
- Регламентирует правила слияния веток
- Сводит к минимуму риск выложить нестабильный код в мастер
- Хорошо использовать с непрерывной интеграцией
- Есть инструментальные решения.
Какие типы веток используются?
Выводы:
- У нас есть две основные ветки: master и develop.
- В ветке master содержится ровно тот же код, что и в рабочей (читай, продакт) версии проекта. А вся работа делается в ветке develop.
- Во время работы на основе develop создаются так называемые feature-ветки. Их может быть неограниченное количество.
- Ветка release используется для подготовки к новому релизу проекта.
- Ветка hotfix служит для срочного исправления багов, найденных, например, на продакте.
- Основа работы с гитфлоу - хранить в мастере только релизный код. Там не допускается рабочий процесс, потенциально приводящий к ошибкам.
Как происходит рабочий процесс?
Выводы:
- После создания репозитория создается ветка develop. Она является основной рабочей.
- Начинается работа на ветке develop.
- Возникает необходимость опробовать новую штуку – создается feature-ветка и делаются коммиты
- Закончив работу на feature-ветке, вы сливаете ее с develop.
- Если вы довольны текущей версией, но хотите продолжить работу, создается ветка release, куда перемещается текущая версия. Правка багов будет происходить на этой же ветке.
- Когда с веткой release покончено, время слить ее в master и продолжить работу с develop.
Как работать с веткой develop?
Выводы:
- Ветвь master создаётся при инициализации репозитория, что должно быть знакомо каждому пользователю Git.
- Параллельно ей также мы создаём ветку для разработки под названием develop.
- Мы считаем ветку origin/master главной. То есть, исходный код в ней должен находиться в состоянии production-ready в любой произвольный момент времени.
- Ветвь origin/develop мы считаем главной ветвью для разработки.
- Эту ветку также можно назвать «интеграционной».
- Когда исходный код в ветви develop готов к релизу, все изменения должны быть влиты в ветвь master и помечены тегом с номером релиза.
Как создавать фичи?
1
2
3
4
5
6
7
8
9
10
11
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
# . . .
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Отчёт об изменениях)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop
Ветви функциональностей (feature branches), также называемые иногда тематическими ветвями (topic branches), используются для разработки новых функций, которые должны появиться в текущем или будущем релизах.
Смысл существования ветви функциональности (feature branch) состоит в том, что она живёт так долго, сколько продолжается разработка данной функциональности (фичи).
Когда работа в ветви завершена, последняя вливается обратно в главную ветвь разработки или же удаляется (в случае неудачного эксперимента).
Выводы:
- Ветви feature используются для разработки новых функций, которые должны появиться в текущем или будущем релизах.
- Смысл существования ветви feature состоит в том, что она живёт так долго, сколько продолжается разработка данной фичи.
- Когда работа в ветви завершена, последняя вливается обратно в главную ветвь разработки или же удаляется.
Как происходит работа с релизами?
1
2
3
4
5
6
7
8
9
10
11
12
13
$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Отчёт об изменениях)
$ git tag -a 1.2
Выводы:
- Ветви release используются для подготовки к выпуску новых версий продукта.
- Они позволяют расставить финальные точки над i перед выпуском новой версии.
- Кроме того, в них можно добавлять минорные исправления, а также подготавливать метаданные для очередного релиза .
- Очередной релиз получает свой номер версии только в тот момент, когда для него создаётся новая ветвь, но ни в коем случае не раньше.
Для чего нужны ветки hotfix?
1
2
3
4
5
6
7
8
9
10
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
Выводы:
- Ветви hotfix создаются из главной (master) ветви.
- Они нужны для срочного внесения критичных исправлений в текущий релиз, если новый релиз еще не готов.
Основы сетевого программирования
Основные понятия сетевого программирования
Чем сетевые приложения отличаются от обычных?
Сетевыми приложениями мы будем называть любые приложения, которые обмениваются данными, используя компьютерную сеть. Это довольно широкое определение, и, конечно, мы не сможем рассмотреть все многообразие обширного мира сетевых технологий, который, вдобавок развивается очень быстро и новые технологии, приемы и методики появляются чуть ли не каждый день. Поэтому в данном пособии мы сконцентрируемся на освоении базовых схем обмена информацией по сети, которые лежат в основе всех более продвинутых вещей. Используя полученные знания вы сами сможете строить все более и более сложные схемы взаимодействия разных приложений и разных компонентов одного приложения по сети.
Сетевые приложения могут обмениваться информацией с другими, сторонними приложениями либо строить взаимодействие по сети между компонентами одного и того же приложения, написанного одним автором или одной командой.
Возможность обмениваться данными по сети открывает перед разработчиком широкий круг возможностей.
Вы можете обращаться к сторонним сервисам, имеющим открытое API. Сегодня существует множество сервисов, постоянно действующих в сети и предоставляющих способ обмена данными в автоматизированном формате через специальную схему взаимодействия, то есть публичный интерфейс, или API. Например, ваша программа может обратиться к погодному сервису и получить данные о погоде в определенном месте или городе.
Также вы можете сами разработать такой публичный сервис. Если ваше приложение может выдавать информацию по запросу неограниченному кругу лиц, вы можете опубликовать в Интернете приложение, которое будет обрабатывать входящие соединения и отвечать на запросы. Нужно только спроектировать API вашего сервиса, реализовать и задокументировать его.
Можно строить распределенные приложения. Сейчас довольно распространены приложения, основой функционирования которых является взаимодействие множества компонентов, запущенных на совершенно независимых компьютерах сети. Так работают, например, пиринговые сети, системы распределенных вычислений или ботнеты.
Добавьте к любой вашей программе возможность самостоятельно обновляться по сети. Почти все программы имеют подсистему автоматической и регулярной проверки сервера разработчика программы на предмет выхода новой версии. При наличии такой можно предложить пользователю скачать ее, либо обновиться самостоятельно.
Можно использовать централизованную схему клиент-сервер. В таком случае ваша программа может быть разделена на две логические части - клиентскую, которая запускается на компьютере пользователя и предоставляет ему интерфейс, и серверную, которая работает на сервере, принадлежащем разработчику, и может заниматься, например, доступом к базе данных. Логика работы программы тоже может быть разделена на две части.
Можно организовывать централизованное хранилище данных. Это удобно, если, например, вам нужно собирать данные от пользователей вашей программы в одном месте, либо предоставить пользователям возможность обмениваться сообщениями.
Если вы организуете взаимодействие с клиентами посредством нескольких каналов, можно позаботиться об омниканальности - возможности сохранять информацию о всех взаимодействиях с пользователем по любым каналам, создавая ощущение бесшовности. Такую схему активно используют, например, банки. Вы можете зайти в мобильное приложение, совершить там какую-то операцию, а затем зайти в личный кабинет на веб-сайте банка и продолжить работу с того же места.
Выводы:
- Сетевые приложения - это программы, которые обмениваются данными через сеть.
- В подавляющем большинстве случаев обмен идет на основе протоколов TCP/IP.
- В настоящее время все более или менее развитые приложения являются сетевыми в той или иной мере.
- Приложения обмениваются данными с другими либо между своими компонентами.
- Можно обращаться к сторонним сервисам
- Создание публичного сервиса - это тоже задача сетевого программирования.
- Многопользовательские приложения очень распространены в определенных предметных областях.
- Автоматические обновления - это возможность, которая есть почти во всех программных продуктах.
- Одна из частных задач сетевого программирования - удаленное хранение данных
- Для экономики и бизнеса важна омниканальность взаимодействия с клиентами и пользователями.
В чем сложности создания сетевых приложений?
Разработка сетевых приложений - отдельная дисциплина информатики, требующая от программиста некоторых дополнительных знаний и умений. Естественно, для создания приложений, использующих возможности компьютерной сети необходимо знать основы функционирования таких сетей, понимать главные сетевые протоколы, понятие маршрутизации и адресации. В подавляющем большинстве случаев на практике используются протоколы семейства TCP/IP. Это сейчас универсальный механизм передачи данных. Но сами сетевые приложения могут интенсивно использовать разные конкретные протоколы, находящиеся на разных уровнях модели OSI.
Например, в этой теме мы в основном будем говорить об использовании TCP и UDP сокетов, то есть будем работать на транспортном уровне модели OSI. Но можно работать и на других уровнях с использованием других протоколов. Так веб приложения наиболее активно полагаются на протокол HTTP и его защищенную модификацию - HTTPS. Поэтому так важно знать все многообразие существующих схем и протоколов обмена данными. Ведь при проектировании приложения нужно выбрать тот протокол, уровень, который наилучшим образом подходит для решений стоящей перед разработчиками прикладной задачи.
Кроме того, при проектировании и реализации сетевых приложений на вас, как на разработчике лежит задача продумывания конкретного обмена данными. Используемый протокол регламентирует порядок передачи данных, но конкретную схему, последовательность обмена создает разработчик конкретного приложения. Вам надо определиться, какие данные будут передаваться по сети, в каком формате, в каком порядке. Будете ли вы использовать, например, JSON или XML, может, стоит придумать свой формат данных? После соединения, какой модуль начинает передачу, сколько итераций передачи будет проходить за одно соединение, как обозначить конец передачи, нужно ли регламентировать объем передаваемой информации, нужно ли использовать шифрование данных - это все примеры вопросов, которые необходимо будет решить в ходе разработки. Фактически, каждое сетевое приложение - это по сути еще один, очень конкретный протокол передачи, которые для себя придумывают разработчики специально для этого приложения.
Еще одна область, непосредственно связанная с сетевым обменом - это параллельное программирование. Сетевые приложения обычно очень активно используют многопоточности или другие средства обеспечения многозадачности. Какие-то операции должны выполняться в фоне, пока выполняются другие. Такое приложение становится гораздо более сложным по своей структуре. И необходимо отдельно заботиться о том, чтобы оно работало корректно при всех возможных условиях. Это происходит за счет обеспечения потокобезопасности, использования правильных архитектурных и дизайнерских шаблонов, специальных алгоритмов и структур данных.
Отдельный вопрос, непосредственно связанный с сетевым программированием - обеспечение безопасности и конфиденциальности обмена данными. Любое приложение, принимающее данные по сети должно быть рассчитано на то, что его может вызвать и передать данные любое приложение или пользователь. В том числе, с неизвестными или деструктивными целями. Поэтому в сетевых приложениях необходимо применять проверки входных данных, валидацию всей принимаемой информации, возможно - механизмы аутентификации пользователей. Если же ваше приложение отправляет данные - здесь тоже возможны риски того, что они попадут не по назначению. Так можно применять те же механизмы аутентификации, шифрования трафика. Особенно аккуратным надо быть, если ваше приложение собирает, хранит или передает персональные данные пользователей. В этом случае могут применяться определенные юридические нормы и обязательства, которые разработчики обязаны соблюдать.
Надо помнить, что сетевые приложения используют не только протоколы и соглашения передачи данных, но и конкретную физическую сетевую инфраструктуру. И у нее есть определенные параметры, которые необходимо учитывать - полоса пропускания, надежность, доступность. Как будет работать приложение, если в какой-то момент перестанет быть доступна сеть? Какая скорость передачи данных нужна для бесперебойной работы серверной части приложения? На сколько одновременных подключений рассчитан каждый программный модуль? За этим всем нужно следить не только на этапе проектирования, принятия решений, но и в процессе работы приложения - организовывать постоянный мониторинг, продумывать вопросы управления нагрузкой, дублирования, репликации данных и так далее.
Выводы:
- Нужно знать основы организации компьютерной сети.
- Для некоторых приложений необходимо знать особенности конкретных сетевых протоколов, например, HTTP.
- Необходимо также отдельно заботиться о согласованности обмена информацией между компонентами приложения.
- Написание многопоточных приложений требует специальных усилий по обеспечению потокобезопасности.
- Также не нужно забывать о вопросах безопасности, конфиденциальности, валидации получаемых данных.
- Также существуют вопросы управления нагрузкой ваших сервисов и сетевой инфраструктуры.
- Необходимо также помнить о доступности сети и ограниченности полосы пропускания.
Какие основные подходы к их построению?
При создании сетевых приложений первый вопрос, который должен решить для себя разработчик - создание высокоуровневой архитектуры приложений. Из каких частей (модулей) оно будет состоять, как именно они будут обмениваться данными и с кем. Будет ли обмен происходить только между модулями самого приложения или будут предусмотрены обращения к внешним сервисам?
Не затрагивая пока вопросов использования сторонних сервисов и программ, рассмотрим два самых популярных архитектурных паттерна для сетевых приложений. В принципе, любое сетевое приложение может быть либо клиент-серверным, либо распределенным.
Самая популярная архитектура сетевых приложений - клиент серверная. Она подразумевает, что приложение состоит из серверной части и клиентской. Сервером мы будем называть именно часть программной системы, модуль, который постоянно (резидентно) выполняется и ждет запросов от клиентов. Когда запрос поступает, сервер его обрабатывает, понимает, что клиент хочет получить и выдает ему ответ.
При этом может быть одновременно запущенно несколько экземпляров клиентского модуля программы. Но как правило, они все идентичны (во всяком случае, они должны использовать идентичный протокол обмена данными), поэтому нет смысла их рассматривать отдельно, мы будем говорить об одном клиентском модуле.
Клиент в такой схеме - это модуль программы, который непосредственно взаимодействует с пользователем программы и в зависимости от его действий может инициировать соединение с сервером, чтобы произвести определенные операции.
Сервер непосредственно с клиентом не взаимодействует. Его задача - выполнять запросы клиентов. Он в такой схеме является центральным элементом. Распределение функционала между клиентом и сервером - другими словами, какие операции вашей программы должны относиться к клиентской части, а какие к серверной - тоже предмет проектирования. Определенно одно - все, что касается пользовательского интерфеса - это прерогатива клиентской части. В зависимости от задачи вы можете делать клиент более “тонким”, то есть оставить только интерфейс и больше ничего (тогда при любых действиях пользователя клиент будет запрашивать сервер и просить его выполнять операции), либо более “толстым” - то есть выносить на клиент часть непосредственного фукнционала приложения.
Достоинством клиент-серверной архитектуры является ее простота и понятность. Приложение явно делиться на четко обозначенные модули, между ними налаживается довольно типичная схема обмена данными. Но у нее есть и недостатки. Если по каким-то причинам сервер становится недоступен, то полноценно пользоваться приложением нельзя. То есть сервер - это потенциальная точка отказа.
Тем не менее, на основе клиент-серверного принципа работают большинство сетевых приложений, почи все сетевые службы, на работу с ним ориентированы большинство библиотек и фреймворков. Так что инструменты разработки обычно помогают реализовывать именно такую архитектуру приложений.
Альтернативой клиент-серверным приложениям выступают распределенные. В них программа не делится на клиент и сервер, а состоит из множества однотипных модулей, которые совмещают в себе функции и клиента и сервера. Такие модули, будучи запущенными одновременно, могут подсоединяться друг к другу и выполнять обмен данными в произвольном порядке.
Типичным примером распределенных приложений являются пиринговые сети. В них каждый экземпляр приложения может подключаться к любому другому и налаживать многосторонний обмен информацией. Такие приложения образуют сеть подключений, подобно тому, как организован сам Интернет. Такое приложение не зависит от работы центрального сервера и может продолжать функционировать даже если большая часть этой сети будет отсутствовать или будет недоступной.
Несмотря на все достоинства, проектировать, реализовывать и поддерживать распределенные приложения может быть значительно сложнее, чем клиент-серверные. Это происходит потому, что клиент-сервер, более четко определенный протокол, ему нужно лишь следовать и все будет работать так, как предполагается. А в распределенных приложениях все приходится проектировать и продумывать с нуля.
Выводы:
- Есть две главные архитектуры построения сетевых приложений - клиент-серверная и распределенная.
- Сервер - это компьютер, программа или процесс, обеспечивающий доступ к информационным ресурсам.
- Клиент обычно инициирует соединение и делает запрос к ресурсу.
- Клиент-серверная архитектура является централизованной со всеми присущими недостатками и преимуществами.
- Распределенная архитектура может обойти некоторые ограничения централизованной.
- Распределенные приложения сложнее проектировать и управлять ими.
Основы взаимодействия через сокеты
Что такое TCP-сокеты?
Со́кет (англ. socket — разъём) — название программного интерфейса для обеспечения обмена данными между процессами. Процессы при таком обмене могут исполняться как на одной ЭВМ, так и на различных ЭВМ, связанных между собой сетью.
Сокеты - это самый базовый механизм сетевого взаимодействия программ, абстрагированный от конкретной реализации сети. Сокеты работают на транспортном уровне модели OSI - там же, где и протокол TCP и UDP.
Каждый сетевой интерфейс IP-сети имеет уникальный в этой сети адрес (IP-адрес). Упрощенно можно считать, что каждый компьютер в сети Интернет имеет собственный IP-адрес. При этом в рамках одного сетевого интерфейса может быть несколько (до 65536) сетевых портов. Для установления сетевого соединения приложение клиента должно выбрать свободный порт и установить соединение с серверным приложением, которое слушает (listen) порт с определенным номером на удаленном сетевом интерфейсе. Пара IP-адрес и порт характеризуют сокет (гнездо) - начальную (конечную) точку сетевой коммуникации.
Сокеты могут применяться для связи процессов как на удаленной машине, так и на локальной. В первом случае, естественно, необходимо, чтобы удаленная машина была доступна по сети. Это можно проверить при помощи команды пинг. Во втором случае сокеты могут выступать как механизм межпроцессного взаимодействия. Или вы можете использовать одну машину для всех процессов-компонентов вашей программной системы, например, для отладки в процессе разработки.
Для создания соединения TCP/IP необходимо два сокета: один на локальной машине, а другой - на удаленной. Таким образом, каждое сетевое соединение имеет IP-адрес и порт на локальной машине, а также IP-адрес и порт на удаленной машине. Как правило, порт локальной машины (исходящий порт) не так важен и его номер не особенно используется в практике. Но порт серверного сокета - это важная информация
Сокеты могут быть клиентские и серверные. Серверный сокет - это функция в программе, которая сидит на определенном порту и “слушает” входящие соединения. Процедура создания серверного сокета аналогична вводу текста из консоли: программа блокируется до тех пор, пока пользователь не ввел что-то. Когда это случилось, программа разблокируется и может продолжать выполнение и обработку полученных данных. Также и серверный сокет: ждет, когда к нему подключится клиент и тогда продолжает выполнение программы и может считывать данные из сокета (которые послал клиент) и отправлять данные в сокет. Клиентский же сокет, наоборот, сразу пытается подключиться к определенном узлу сети (это может быть локальная машина, или, чаще, удаленный компьютер) и на определенный сетевой порт. Если на этой машине на этом порту “сидит” серверный сокет, то подключение происходит успешно. Если же данный сокет никем не прослушивается, то процедура подключения возвращает ошибку.
В языке программирования Python существует стандартный модуль socket, который реализует все необходимые функции для организации обмена сообщениями через сокеты. Для его использования его достаточно импортировать (так как это модуль стандартной библиотеки, устанавливать его не нужно, он идет в поставке с дистрибутивом Python):
1
import socket
Для начала построения сетевого взаимодействия необходимо создать сокет:
1
sock = socket.socket()
Здесь ничего особенного нет и данная часть является общей и для клиентских и для серверных сокетов. Дальше мы будем писать код отдельно для сервера и для клиента.
Существует несколько видов сокетов, которые немного различаются по сфере применения и деталях реализации. Самыми распространенными являются Интернет-сокеты. Они используются для пересылки информации между процессами. Есть еще сокеты UNIX, они не используют Интернет-протоколы для обмена сообщениями, и используются для организации межпроцессного взаимодействия.
Также среди Интернер сокетов существуют потоковые и датаграммные сокеты. Датаграммные сокеты называют “сокеты без соединения”, они используют протокол UDP вместо TCP. Потоковые сокеты обеспечивают гарантированную доставку, очередность сообщений, они более надежны. Протокол HTTP использует именно потоковые сокеты для соединения клиента с сервером. UDP обычно используется для передачи потокового медиа, когда скорость критичнее риска потери единичных пакетов.
Выводы:
- Сокеты - это базовый механизм сетевого взаимодействия программ.
- Для работы сокетов не нужно специальное программное обеспечение, они работают на уровне операционных систем.
- Сокет состоит из адреса хоста и номера сетевого порта.
- Для соединения необходимо создать два сокета - в двух модуля программы, которые нужно соединить.
- Стандартный модуль Python socket позволяет создавать сокеты и работать с ними.
- Еще отдельный вид сокетов применяется для организации межпроцессного взаимодействия в *nix системах.
- Сокеты могут использовать протокол TCP либо UDP.
Каковы правила использования номеров портов?
Для эффективного использования сетевых сокетов необходимо вспомнить концепцию сетевых портов, так как сокеты их активно используют.
IP-адрес или любой другой способ адресации хоста позволяет нам идентифицировать узел сети. Номер порта позволяет указать конкретное приложение на этом хосте, которому предназначен пакет. Номер порта нужен, так как на любом компьютере может быть одновременно запущено множество приложений, осуществляющих обмен данными по сети. Если использовать аналогию с почтовыми адресами, то IP-адрес - это номер дома, а порт - это номер квартиры в этом доме.
Номер порта - это всего лишь 16-битное число, которое указывается в пакете, передающемся по сети. Не нужно путать сетевой порт с физическими разъемами, это чисто программная концепция.
Так как на номер порта отведено 16 бит, существует всего 65536 возможных портов. Причем, номера портов отдельно считаются по протоколам TCP и UDP. Таким образом, на компьютере одновременно может существовать более 130 тысяч процессов, обменивающихся данными. На практике, свободных портов всегда в избытке и хватает даже для работы множества высоконагруженных серверов.
Но не все номера портов созданы равными. Первые 1024 являются “системными” и используются в основном стандартными приложениями. Существует общепринятое соглашение, какие сетевые службы используют системные порты. Например, служба веб-сервера по умолчанию использует 80 порт для соединений по протоколу HTTP и 443 - для протокола HTTPS. Служба SSH использует порт номер 22. И так далее. Любая стандартная сетевая служба имеет некоторый порт по умолчанию. Кстати, хорошим показателем практикующего администратора является запоминание часто используемых номеров стандартных портов. Специально это учить не нужно, только если вы не хотите блеснуть знаниями на собеседовании.
Для использования системных портов обычно требуются повышенные привилегии. Это сделано для того, чтобы обычные пользователи не “забивали” стандартные порты и не мешали работе системных сетевых служб. Мы вообще не рекомендуем использовать системные порты. Остальные могут использоваться совершенно произвольно и их более чем достаточно для повседневных нужд.
Кстати, хоть сетевые службы используют определенные стандартные порты, они вполне могут быть переназначены на свободные. Служба не “привязана” к номеру порта, это легко регулируется настройками. Например, строго рекомендуется при настройке службы SSH менять стандартный 22 порт на случайный для повышения безопасности.
Порты назначаются процессу при попытке открыть серверный сокет. В этот момент происходит связывание сокета с номером порта, который выбрал программист. Помните, что если вы пытаетесь использовать занятый порт, то это спровоцирует программную ошибку. Поэтому в реальных приложениях стоит сначала проверять, свободен ли порт или нет, либо (что гораздо проще) обрабатывать исключение при попытке связаться с занятым портом.
Сетевые администраторы могут в целях безопасности блокировать соединения на некоторые порты. Так же поступают и программы-файерволлы. Это требуется для повышения безопасности сервера. Вообще, по умолчанию, все порты “закрыты”, то есть подключения к ним блокируется файерволлом. При необходимости системный администратор может “открыть” обмен данными по определенному номеру порта в настройках файерволла. Это следует учитывать при попытках подключения к удаленным машинам.
Выводы:
- Порт - это всего лишь число в IP-пакете.
- Номер порта нужен, чтобы обратиться к определенному процессу на конкретном хосте.
- Всего существует 65536 TCP-портов и 65536 UDP-портов.
- Первые 1024 порта являются системными - их можно использовать только администраторам.
- Распространенные сетевые службы имеют стандартные номера портов, их лучше не занимать.
- Порт назначается при открытии серверного сокета. Можно занять только свободный порт.
- Системные администраторы, программы-файерволлы могут заблокировать, “закрыть” обмен данными по номерам портов.
Почему стоит начать именно с изучения сокетов?
Сокеты, которые мы рассматриваем как базовый инструмент сетевого взаимодействия, относятся к транспортному уровню модели OSI. Мы можем рассматривать механизмы передачи данных на любом уровне. Но начинаем изучение сетевых приложений именно с транспортного уровня и на это есть свои причины.
Более высокоуровневые механизмы требуют установленного и настроенного специального программного обеспечения. Чтобы написать веб-приложение, нам нужен веб-клиент и веб-сервер, настроенные и готовые к работе. Такая же ситуация с любой другой службой Интернета. Конечно, на практике большинство популярных сетевых приложений используют более высокоуровневые протоколы, например, тот же HTTP.
Использование сокетов позволяет строить приложения, обменивающиеся данными по сети, но при этом не требующие специального программного обеспечения. Даже если вы планируете использовать в профессиональной деятельности прикладные протоколы, все они в основе своей работы используют механизм сокетов, так что его понимание будет полезно всем.
С другой стороны, более низкоуровневые протоколы не абстрагируются от конкретной реализации сети. Например, на физическом уровне мы должны будем иметь дело с протоколами передачи данных по витой паре, беспроводному каналу, решать проблемы помех, интерференции сигналов и прочие технические вопросы. Это не позволит нам сконцентрироваться на сутевых вопросах создания сетевых алгоритмов.
Поэтому транспортные сокеты - это компромиссный вариант между прикладными и физическими протоколами.
Выводы:
- Сокеты не требуют специального программного обеспечения.
- Сокеты не зависят от конкретной физической реализации сети.
- Сокеты хороши для понимания основ сетевого взаимодействия.
Как организуется обмен данными через TCP-сокеты?
Для соединения двух программ необходимо создать два сокета - один серверный и один клиентский. Они обычно создаются в разных процессах. Причем эти процессы могут быть запущены как на одной машине, так и на разных, на механизм взаимодействия это не влияет.
Работа с сокетами происходит через механизм системных вызовов. Все эти вызовы аналогичны вызовам, осуществляющим операции с файлами. Список этих вызовов определен в стандарте POSIX и не зависит от используемой операционной системы или языка программирования. В большинстве языков программирования присутствуют стандартные библиотеки, которые реализуют интерфейс к этим системным вызовам. Поэтому работа с сокетами достаточно просто и бесшовно организуется на любом языке программирования и на любой операционной системе.
Давайте рассмотрим общую схему взаимодействия через сокеты и последовательность действий, которые нужно совершить, чтобы организовать соединение. Первым всегда создается серверный сокет. Он назначается на определенный порт и начинает его ожидать (прослушивать) входящие соединения. Создание сокета, связывание сокета с портом и начало прослушивания - это и есть системные вызовы.
Причем при выполнении системных вызовов могут случиться нестандартные ситуации, то есть исключения. Обычно это происходит при связывании с портом. Если вы попытаетесь занять системный порт, не обладая парами администратора, или попытаетесь занять порт, уже занятый другим процессом, возникнет программная ошибка времени выполнения. Такие ситуации надо обрабатывать в программе.
Прослушивание порта - это блокирующая операция. Это значит, что программа при начале прослушивания останавливается и ждет входящих подключений. Как только поступит подключение, программа возобновится и продолжит выполнение со следующей инструкции.
В другом процессе создается клиентский сокет. Клиент может подключиться к серверному сокету по адресу и номеру порта. Если этот порт является открытым (то есть его прослушивает какой-то процесс), то произойдет соединение и на сервере выполнится метод accept. Важно следить за тем, чтобы клиент подключился именно к нужному порту. На практике из-за этого происходит много ошибок.
Соединение с серверным сокетом - это тоже системный вызов. При этом вызове тоже часто случаются ошибки. Естественно, если не удастся установить сетевое соединение с удаленным хостом по адресу, это будет ошибка. Такое случается, если вы неправильно указали IP-адрес, либо удаленная машина недоступна по сети. Еще ошибка может возникнуть, если порт, к которому мы хотим подключиться свободен, то есть не прослушивается никаким процессом. Также не стоит забывать, что порт на удаленной машине может быть закрыт настройками файерволла.
После этого устанавливается двунаправленное соединение, которое можно использовать для чтения и записи данных. Интерфейс взаимодействия с сокетом очень похож на файловые операции, только при записи информации, она не сохраняется на диск, а посылается в сокет, откуда может быть прочитана другой стороной. Чтение информации из сокета - это также блокирующая операция.
Следует помнить, что сокеты предоставляют потоковый интерфес передачи данных. Это значит, что через сокет не получится посылать сообщения “пакетами”. По сути, сокет предоставляет два непрерывных потока - один от сервера к клиенту, другой - от клиента к серверу. Мы не можем прочитать отдельное сообщение из этого потока. При чтении мы указываем количество бит, которые хотим прочитать. И процесс будет ждать, пока другой не отправит в сокет необходимое количество (ну либо не закроет сокет). Более понятны особенности работы в потоковом режиме станут на практике.
После обмена данными, сокеты рекомендуется закрывать. Закрытие сокета - это тоже специальный системный вызов. Закрывать нужно как клиентский, так и серверный сокет. Если вы забудете закрыть сокет, то операционная система все еще будет считать соответствующий порт занятым. После завершения процесса, который открыл сокет, все открытые дескрипторы (файлы, устройства, сокеты) автоматически закрываются операционной системой. Но при этом может пройти определенной время, в течении которого сокет будет считаться занятым. Это доставляет довольно много проблем, например, при отладке программ, когда приходится часто подключаться и переподключаться к одному и тому же порту. Так что серверные сокеты надо закрывать обязательно.
Еще одно замечание. Так как сокет - это пара адрес-порт, и сокета для соединения нужно два, получается что и порта тоже нужно два? Формально, да, у нас есть клиент - его адрес и номер порта, и сервер - его адрес и номер порта. Первые называются исходящими (так как запрос на соединение происходит от них), а вторые - входящие (запрос приходит на них, на сервер). Для того, чтобы соединение прошло успешно, клиент должен знать сокет сервера. А вот сервер не должен знать сокет клиента. При установке соединения, адрес и порт клиента отправляются серверу для информации. И если адрес клиента иногда используется для его идентификации (я тебя по IP вычислю), то исходящий порт обычно не нужен. Но откуда вообще берется этот номер? Он генерируется операционной системой случайно из незанятых несистемных портов. При желании его можно посмотреть, но пользы от этого номера немного.
Выводы:
- Серверный сокет назначается на определенный свободный порт и ждет входящих соединений.
- Клиентский сокет сразу соединяется с северным. Тот должен уже существовать.
- Сокет - это двунаправленное соединение. В него можно читать и писать, как в файл.
- Сокет - это битовый потоковый протокол, строки нужно определенным образом кодировать в битовый массив пред отправкой.
- После использования сокет нужно закрыть, иначе порт будет считаться занятым.
- Существует входящий и исходящий номер порта. Но исходящий номер назначается случайно и редко используется.
Простейшие клиент и сервер
Что мы хотим сделать?
Давайте приступим к практическому знакомству с обменом информацией между процессами через сокеты. Для этого мы создадим простейшую пару клиент-сервер. Это будут две программы на Python, которые связываются друг с другом, передают друг другу какую-то информацию и закрываются.
Мы начнем с самой простой схемы передачи данных, а затем будем ее усложнять и на этих примерах будет очень легко понять, как работают сокеты, как они устроены и для чего могут быть применены.
Мы будем организовывать один набор сокетов. Так что один из них должен быть клиентским, а другой - серверным. Поэтому один процесс мы будем называть сервером, а другой - клиентом. Однако, такое разделение условно и одна и та же программа может выступать и клиентом для одного взаимодействия и сервером - для другого. Можно сказать, что клиент и сервер - это просто роли в сетевом взаимодействии. Инициирует соединение всегда клиент, это и определяет его роль в сетевом взаимодействии. То есть, то процесс, который первый начинает “стучаться” - тот и клиент.
При разработке сетевых приложений вам придется много раз запускать оба этих процесса. Вполне естественно, что часто они будут завершаться ошибками. Помните, что для соединения всегда первым должен быть запущен сервер, а затем клиент.
Как создать простой эхо-сервер?
Для начала нам нужно определиться, какой порт будет использовать наш сервер. Мы можем использовать любой несистемный номер порта. Лучше заранее убедиться, что он в данный момент не используется в вашей системе. Например, если вы используете сервер баз данных MS SQL он обычно занимает порт 5432. Мы в учебных примерах будет использовать номер 9090 - это запоминающееся, но ничем не особенное число. Вы можете использовать другой.
Для начала импортируем модуль socket:
1
import socket
Теперь можно создать сокет одноименным конструктором из этого модуля:
1
sock = socket.socket()
Теперь переменная sock хранит ссылку на объект сокета. И через эту переменную с сокетом можно производить все необходимые операции.
После этого свяжем сокет с номером порта при помощи метода bind:
1
sock.bind(('', 9090))
Этот метод принимает один аргумент - кортеж из двух элементов. Поэтому обратите внимание на двойные круглые скобки. Первый элемент кортежа - это сетевой интерфейс. Он задает имя сетевого адаптера, соединения к которому нас интересуют. Вы же знаете, что на компьютере может быть несколько адаптеров, и он может быть подключен одновременно к нескольким сетям. Вот с помощью этого параметра можно выбрать соединения из какой конкретно сети мы будем принимать. Если оставить эту строку пустой, то сервер будет доступен для всех сетевых интерфейсов. Так как мы не будем заниматься программированием межсетевых экранов, мы будем использовать именно это значение.
Второй элемент кортежа, который передается в метод bind - это, собственно, номер порта. Для примера выберем порт 9090.
Теперь у нас все готово, чтобы принимать соединения. С помощью метода listen мы запустим для данного сокета режим прослушивания. Метод принимает один аргумент — максимальное количество подключений в очереди. Установим его в единицу. Чуть позже мы узнаем, на что влияем длина очереди подключений сервера.
1
sock.listen(1)
Теперь, мы можем принять входящее подключение с помощью метода accept, который возвращает кортеж с двумя элементами: новый сокет (объект подключения) и адрес клиента. Именно этот новый сокет и будет использоваться для приема и посылке клиенту данных.
1
conn, addr = sock.accept()
Обратите внимание, что это блокирующая операция. То есть в этот момент выполнение программы приостановится и она будет ждать входящие соединения.
Объект conn мы будем использовать для общения с этим подключившимся клиентом. Переменная addr - это всего лишь кортеж их двух элементов - адреса хоста клиента и его исходящего порта. При желании можно эту информацию вывести на экран.
Так мы установили с клиентом связь и можем с ним общаться. Чтобы получить данные нужно воспользоваться методом recv, который в качестве аргумента принимает количество байт для чтения. Мы будем читать из сокета 1024 байт (или 1 кб):
1
2
data = conn.recv(1024)
conn.send(data.upper())
Обратите внимание, что прием и отправка сообщений происходит через объект conn, а не sock. Объект conn - это подключение к конкретному клиенту. Это особенность работы именно TCP-сокетов, при использовании протокола UDP все немного по-другому.
Тут есть один неудобный момент. При чтении нам нужно указать объем данных, которые мы хотим прочитать. Это обязательный параметр. Помните мы говорили, что сокеты - это потоковый механизм? Именно поэтому нельзя прочитать сообщение от клиента “целиком”. Ведь клиент может присылать в сокет информацию порциями, в произвольный момент времени, произвольной длины. В сокете, а точнее во входящем потоке, все эти сообщения “склеются” и будут представлены единым байтовым массивом.
Поэтому сервер обычно не знает, какое именно количество информации ему нужно прочитать. Но это решается довольно просто. Можно организовать бесконечный цикл, в котором читать данные какой-то определенной длины и каждый раз проверять, получили мы что-то или нет:
1
2
3
4
5
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data)
Это работает потому, что после того, как клиент отослал всю информацию, он закрывает соединение. В таком случае на сервере метод recv возвращает пустое значение. Это сигнал завершения передачи.
Метод recv(), как мы говорили, тоже является блокирующей операцией. Он разблокируется (то есть продолжает выполняться) в двух случаях: когда клиент прислал необходимый объем данных, либо когда клиент закрыл соединение.
Еще, если вы обратили внимание, после получения данных мы их отправляем обратно клиенту методом send(). Это метод посылает данные через сокет. Это уже не блокирующая операция, ведь данные посылаются тут же и не нужно ничего ждать.
Конечно, на практике такой сервер был бы бесполезным. Реальные сервера посылают клиенту какие-то другие данные, ответы на его запросы, например. Но наш простой сервер служит только для отладки и обучения. Такой сервер по понятным причинам называется “эхо-сервер”.
После получения порции данных и отсылки их обратно клиенту можно и закрыть соединение:
1
conn.close()
На этом написание сервера закончено. Он принимает соединение, принимает от клиента данные, возвращает их клиенту и закрывает соединение. Вот что у нас получилось в итоге
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
import socket
sock = socket.socket()
sock.bind(('', 9090))
sock.listen(1)
conn, addr = sock.accept()
print 'connected:', addr
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data)
conn.close()
Как создать простейший клиент?
Клиентское приложение еще короче и проще. Клиент использует точно такой же объект socket.
1
2
import socket
sock = socket.socket()
Вместо привязывания и прослушивания порта мы сразу осуществляем соединение методом connect. Для этого указывается IP-адрес хоста, на котором запущен сервер и номер порта, который он прослушивает.
1
sock.connect(('localhost', 9090))
В качестве адреса можно использовать IP-адрес, доменное имя, либо специальное имя localhost. В нашем случае, так как мы пока подключаемся к другому процессу на той же машине, будем использовать его (либо адрес 127.0.0.1, что абсолютно эквивалентно).
При неуспешном соединении метод listen выбросит исключение. Существует несколько причин - хост может быть недоступен по сети, либо порт может не прослушиваться никаким процессом.
Послание данных в сокет осуществляется методом send. Но тут есть один подводный камень. Дело в том, что сокеты - это еще и байтовый протокол. Поэтому в него не получится просто так отправить, например, строку. Ее придется преобразовать в массив байт. Для этого в Python существует специальный строковый метод - encode(). Его параметром является кодировка, которую нужно использовать для кодирования текста. Если кодировку не указывать, то будет использоваться Unicode. Рекомендуем это так и оставить. Вот что получится в итоге:
1
2
msg = 'hello, world!'
sock.send(msg.encode())
Дальше мы читаем 1024 байт данных и закрываем сокет. Для большей надежности чтение данных можно организовать так же как на сервере - в цикле.
1
2
data = sock.recv(1024)
sock.close()
Если мы обмениваемся текстовой информацией, то надо помнить, что данные полученные из сокета - это тоже байтовый массив. Чтобы преобразовать его в строку (например, для вывода на экран), нам нужно воспользоваться методом decode(), который выполняет операцию, обратную encode(). Понятно, что кодировки при этом должны совпадать. Вообще, нет ни одной причины использовать не Unicode.
1
print(data.decode())
Здесь важно помнить вот что. Этот клиент хорошо подходит к тому серверу, который мы создавали в предыдущем пункте. Ведь после установки соединения сервер ожидает приема информации (методом recv). А клиент после соединения сразу начинает отдавать информацию (методом send). Если бы же обе стороны начали ждать приема, они бы намертво заблокировали друг друга. О порядке передачи информации нужно определиться заранее. Но в общем случае, обычно именно клиент первым передает запрос, сервер его читает, передает ответ, который читает клиент.
Вот что у нас получилось в итоге:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python
import socket
sock = socket.socket()
sock.connect(('localhost', 9090))
msg = 'hello, world!'
sock.send(msg.encode())
data = sock.recv(1024)
sock.close()
print(data.decode())
Какие ограничения данного подхода?
Созданные нами клиент и сервер вполне функциональны, но у них есть ряд серьезных ограничений, которые могут затруднить более сложную работу. Например, можно заметить, что сервер выполняет только одну операцию, то есть принимает запрос, выдает ответ и на этом соединение заканчивается. Это, в принципе, не так страшно, ведь всегда можно открыть повторное соединение.
Но наш сервер может обслужить только одного клиента. Ведь после закрытия сокета программа заканчивается и сервер завершается. На практике сервера обычно работают постоянно, в бесконечном цикле, и ждут запросов от клиента. После завершения работы с одним клиентом они продолжают ожидать входящих соединений. Но это исправить можно очень просто (в следующем пункте).
Еще можно заметить, что в работе и клиента и сервера часто встречаются блокирующие операции. Само по себе это неплохо, и даже необходимо. Но представим себе, что клиент по каким-то причинам аварийно завершил свою работу. Или упала сеть. Если в это время сервер ждал сообщения от клиента (то есть заблокировался методом recv), то он останется ждать неопределенно долго. То есть упавший клиент может нарушить работу сервера. Это не очень надежно. Но это тоже легко исправить с помощью установки таймаутов.
Более сложная проблема состоит в том, что сервер способен одновременно работать только с одним клиентом. Вот это исправить уже сложнее, так как потребует многопоточного программирования. Но есть еще один вариант - использовать UDP-сокеты.
Так что давайте познакомимся с некоторыми приемами, которые позволяют обойти или исправить эти недостатки.
Как сделать сервер многоразовым?
Одно из самых заметных отличий нашего простейшего сервера от “настоящего” - его одноразовость. То есть он рассчитан только на одно подключение, после чего просто завершается. исправить эту ситуацию можно очень просто, обернув все действия после связывания сокета с портом и до закрытия сокета в вечный цикл:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python
import socket
sock = socket.socket()
sock.bind(('', 9090))
sock.listen(1)
while True:
conn, addr = sock.accept()
print 'connected:', addr
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data)
conn.close()
Теперь наш сервер после завершения соединения с одним клиентом переходит на следующую итерацию цикла и начинает снова слушать входящие соединение. И к нему может подключиться как тот же самый клиент, так и новый.
Заодно можно в коде сервера сделать небольшой рефакторинг, чтобы он был более универсальным и удобным для внесения изменений. Рефакторинг - это изменение кода без изменение функциональности. Чем понятнее, проще и системнее организация кода, тем проще его развивать, поддерживать и вносить изменения. Инструкции, отвечающие за разные вещи в хорошем коде должны быть отделены от других. Это помогает в нем ориентироваться.
Например, само собой напрашивается создание констант для параментов сокета - интерфейса и номера порта:
1
2
3
4
5
HOST = ""
PORT = 33333
TYPE = socket.AF_INET
PROTOCOL = socket.SOCK_STREAM
Кроме адреса мы еще выделили тип и протокол сокета. Это параметры конструктора socket(), которые мы раньше не использовали. Первый параметр задает тип создаваемого сокета - Интернет-сокет или UNIX-сокет. Мы будем использовать только первый тип, но сейчас мы зададим его явно. Второй параметр - это тип используемого протокола. Потоковые сокеты используют протокол TCP, а датаграммные - UDP. Скоро мы будем создавать и UDP-соединение, так что не лишним будет прописать это тоже в явном виде.
После этих изменений код создания и связывания сокета будет выглядеть следующим образом:
1
2
srv = socket.socket(TYPE, PROTOCOL)
srv.bind((HOST, PORT))
Еще одним этапом рефакторинга можно выделить функцию, обрабатывающую запрос от клиента. Сейчас у нас на сервере обработки запроса не происходит, но в будущем любой сервер каким-то образом что-то делает с запросом клиента и выдает получившийся результат. Логично сделать специальную функцию, чтобы работа с запросом была отделена в коде от механики организации соединения:
1
2
3
4
5
6
7
8
9
10
def do_something(x):
# ...
return x
# ...
srv = socket.socket(TYPE, PROTOCOL)
srv.bind((HOST, PORT))
# ...
Еще хорошей идеей, хотя бы на этапе разработки будет добавить отладочные сообщения в тем моменты, когда на сервере выполняются определенные действия. Это поможет нам визуально понимать, как выполняется код сервера, следить за ним по сообщениям в консоли и очень полезно для обучения.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
srv.listen(1)
print("Слушаю порт 33333")
sock, addr = srv.accept()
print("Подключен клиент", addr)
while 1:
pal = sock.recv(1024)
if not pal:
break
print("Получено от %s:%s:" % addr, pal)
lap = do_something(pal)
sock.send(lap)
print("Отправлено %s:%s:" % addr, lap)
sock.close()
print("Соединение закрыто")
Кроме того, такие отладочные сообщения служат заменой комментариям в коде. Сейчас комментарии особенно и не нужны, потому что их названия переменных, отладочных сообщений, имен функций понятно, что происходит в программе. Такой код называется самодокументируемым.
Конечно, код можно улучшать до бесконечности, но мы на этом остановимся. Вот что у нас получилось в итоге - многоразовый эхо-сервер:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import socket, string
HOST = ""
PORT = 33333
TYPE = socket.AF_INET
PROTOCOL = socket.SOCK_STREAM
def do_something(x):
return x
srv = socket.socket(TYPE, PROTOCOL)
srv.bind((HOST, PORT))
while 1:
srv.listen(1)
print("Слушаю порт 33333")
sock, addr = srv.accept()
print("Подключен клиент", addr)
while 1:
pal = sock.recv(1024)
if not pal:
break
print("Получено от %s:%s:" % addr, pal)
lap = do_something(pal)
sock.send(lap)
print("Отправлено %s:%s:" % addr, lap)
sock.close()
print("Соединение закрыто")
Как задать таймаут прослушивания?
Как мы говорили раньше, блокирующие операции в коде программы должны быть объектом пристального внимания программиста. Блокировки программы могут существенно замедлять ее выполнение (об этом мы поговорим потом, когда будем изучать многозадачное программирование), могут быть источником ошибок.
Модуль socket позволяет выполнять блокирующие операции в нескольких режимах. Режим по умолчанию - блокирующий. В нем при выполнении операции программа будет ждать неопределенно долго наступления внешнего события - установки соединения или отправки другой стороной данных.
Можно использовать другой режим - режим таймаута - путем вызова метода settimeout(t) перед выполнением потенциально блокирующей операции. Тогда программа тоже заблокируется, но будет ждать только определенное время - t секунд. Если по прошествии этого времени нужное событие не произойдет, то метод выбросит исключение.
Таймаут можно “вешать” как на подключение, как и на чтение данных их сокета:
1
2
3
4
5
6
7
8
9
10
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("",0))
sock.listen(1)
# accept can throw socket.timeout
sock.settimeout(5.0)
conn, addr = sock.accept()
# recv can throw socket.timeout
conn.settimeout(5.0)
conn.recv(1024)
В таком случае, лучше обрабатывать возможное исключение, чтобы оно не привело к аварийному завершения программы:
1
2
3
4
5
6
# recv can throw socket.timeout
conn.settimeout(5.0)
try:
conn.recv(1024)
except socket.timeout:
print("Клиент не отправил данные")
Как обмениваться объектами по сокетам?
В работе сетевых приложений часто возникает задача передать через сокет не просто байтовый массив или строку, а объект или файл произвольного содержания. Во многих языках программирования существует специальный механизм, который позволяет преобразовать произвольный объект в строку таким образом, чтобы потом весь объект можно было бы восстановить их этой строки.
Такой процесс называется сериализация объектов. Она применяется, например, при сохранении данных программы в файл. Но с тем же успехом ее можно использовать и для передачи объектов через сокеты.
В Python существует стандартный модуль pickle, который нужен для сериализации объектов. Из этого модуля нам понадобится две операции. Метод dumps(obj) преобразует объект в строковое представление, сериализует его. Метод loads(str) восстанавливает объект из строки - десериализует его.
Давайте рассмотрим пример, когда в сокет отправляется сериализованный словарь:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import socket
import pickle
s = socket.socket()
s.bind(("", 9090))
s.listen(1)
while True:
clientsocket, address = s.accept()
d = {1:"hi", 2: "there"}
msg = pickle.dumps(d).encode()
clientsocket.send(msg)
Имейте в виду, что на месте словаря может быть любой сложный объект. А это значит, что мы можем передавать через сокеты в сериализованном виде практически любою информацию.
На другой стороне прочитать и восстановить объект из сокета можно, например, так:
1
2
3
msg = conn.recv(1024)
d = pickle.loads(msg.decode())
В чем особенности UDP-сокетов?
До сих пор мы рассматривали только сокеты, использующие в своей работе протокол TCP. Этот протокол еще называют протоколом с установкой соединения. Но в сокетах можно использовать и протокол UDP. И такие сокеты (их еще называют датаграммными) имеют с воей реализации несколько отличий. Давайте их рассмотрим.
Создание UDP сокета очень похоже, только надо явно указывать агрументы конструктора socket() ведь по умолчанию создаются TCP сокеты:
1
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Обратите внимание на второй параметр - именно он задает протокол передачи данных. Раньше мы использовали значение socket.SOCK_STREAM - это TCP-сокет. Для UDP-сокетов нужно значение socket.SOCK_DGRAM.
Связывание серверного сокета с портом происходит точно так же:
1
s.bind(("", 9090))
Дальше пойдут различия. UDP-сокеты не требуют установки соединения. Они способны принимать данные сразу. Но для этого используется другой системный вызов (и соответствующий ему метод Python) - recvfrom. Этот метод блокирует программу и когда присоединится любой клиент, возвращает сразу и данные и сокет клиента:
1
data, addr = s.recvfrom(1024)
Этот метод также работает в потоковом режиме. То есть нам нужно указывать количество байт, которые мы хотим считать. Но при следующем вызове метода recvfrom() могут придти данные уже от другого клиента. Надо понимать, что UDP-сокеты являются “неразборчивыми”, то есть в него может писать несколько клиентов одновременно, “вперемешку”. Для этого каждый раз и возвращается исходящий сокет, чтобы мы могли понять, от какого клиента пришли эти данные.
Что касается клиентского сокета, то здесь все еще проще:
1
2
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(msg, (host, port))
То есть сразу после создания сокет готов к передаче данных. Но каждый раз нам нужно указывать, куда мы эти данные посылаем.
UDP-сокеты даже проще по своей организации, чем TCP. Меньше шагов нужно сделать, чтобы обмениваться информацией. Но UDP является протоколом без гарантированной доставки. Они ориентирован на скорость и простоту передачи данных, в то время как TCP - на надежность связи.
В конце приведем пример кода сервера и клиента, обменивающихся данными через UDP-сокеты. Код сервера совсем короткий. Но это такой же эхо-сервер, только работающий на другом протоколе:
1
2
3
4
5
6
7
8
import socket
port = 5000
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("", port))
while 1:
data, addr = s.recvfrom(1024)
print(data)
s.sendto(data, addr)
Код клиента приведем в более подробном виде. Механику работы сокетов мы уже рассмотрели, попробуйте проанализировать в этом коде обработку исключений и вывод отладочных сообщений:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket
import sys
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
except socket.error:
print('Failed to create socket')
sys.exit()
host = 'localhost';
port = 8888;
while(1) :
msg = raw_input('Enter message to send: ')
try :
s.sendto(msg, (host, port))
reply, addr = s.recvfrom(1024)
print('Server reply : ' + reply)
except socket.error, msg:
print('Error Code : ' + str(msg[0]) + ' Message ' + msg[1])
sys.exit()
Многозадачное программирование
Основы многозадачного программирования
Что такое блокировка потока?
Источник: NGINX.
Любая компьютерная программа - это последовательность инструкций, выполняемая на центральном процессоре. Инструкции идут одна за одной и выполняются последовательно на конвейере. Однако, довольно часто при работе программ центральному процессору приходится обращаться к внешним устройствам - оперативной памяти, жесткому диску, устройствам ввода-вывода. Все они работают очень медленно с точки зрения процессора.
Более того, выполнение таких операций может потребовать неопределенно долгого времени. Например, если в программе встречается инструкция чтения с клавиатуры, программа должна ждать ввода пользователя. Причем, она не может продолжиться до тех пор, пока эта инструкция не будет выполнена и не будет возвращено значение. Ведь инструкции в программе идут последовательно и выполнение следующей инструкции может зависеть от результата предыдущей. Такая ситуация называется блокировкой потока. То есть, остановка потока выполнения инструкций программы.
В операционных системах для этого существует специальный механизм - системные вызовы. Как мы знаем, программа не может напрямую обращаться к внешним устройствам, она должна вызвать соответствующую функцию операционной системы. В такой момент управление переходит к ней и программа должна ожидать окончания выполнения системного вызова.
Таким образом, при выполнении некоторых действий выполнение программы блокируется. То есть она будет ждать какое-то, заранее неопределенное время. Такие операции называются блокирующими. Примерами блокирующих операций являются:
- обращение к внешним устройствам через механизм системных вызовов;
- файловый и консольный ввод-вывод;
- выполнение сетевых запросов;
- явные инструкции ожидания (например, time.sleep в питоне);
Неблокирующими операциями являются инструкции, которые сразу же выполняются на центральном процессоре. К ним относятся математические операции, операции со строками, массивами, и другие простые манипуляции. Во время выполнения таких инструкций программа полностью загружает центральный процессор (или как минимум, одно его ядро).
На практике, большинство прикладных программ большую часть времени своего выполнения находятся именно в таком состоянии ожидания. Это приводит к нерациональному расходованию ресурсов. И именно для решения этой проблемы в операционных системах был придуман механизм многозадачности. Операционная система сама отслеживает состояние процессов и может переключить выполнение на другой, незаблокированный процесс пока этот ждет наступления внешнего события.
Выводы:
- Программы в своей работе часто обращаются к медленным внешним устройствам.
- Блокирующими называются операции, которые требуют неопределенного времени.
- Классический пример - операция чтения с консоли.
- При выполнении блокирующей операции выполнение программы приостанавливается.
- Выполнение не может быть продолжено до наступления какого-то внешнего события.
- Управление передается операционной системе посредством системного вызова.
- Большинство программ большую часть времени просто ждут.
Откуда берется прирост скорости?
В зависимости от того, насколько часто та или иная программа (а точнее ее алгоритм) выполняет блокирующие операции, все алгоритмы можно разделить на две условные категории:
Источник: Maliszewski@ResearchGate.
- задачи, ограниченные процессором (cpu-bound tasks) - это программы, большую часть которых составляют вычислительные инструкции. Такие программы редко блокируются и сильно нагружают центральный процессор. Скорость их выполнения напрямую зависит от скорости работы процессора.
- задачи, ограниченные вводом-выводом (io-bound tasks) - это такие программы, которые часто выполняют блокирующие операции, чаще всего - операции ввода-вывода. Такие программы наоборот, большую часть времени находятся в ожидании, и скорость их выполнения зависит от внешних факторов.
Надо подчеркнуть, что это характеристика самой задачи, то есть алгоритма, а не способа его решения. Если задача связана с вводом-выводом, ее нельзя сделать чисто вычислительной, и наоборот. Хотя, конечно, можно немного уменьшить или увеличить количество блокирующих операций, специальным образом спроектировав алгоритм, но в целом, именно сама решаемая задача будет определять, к какому из этих классов будет относиться программа.
Еще заметим, что данное деление, конечно, условное, и нет четкой границы между этими двумя классами. Это скорее непрерывный спектр, а эти классы - его полюса. Мы описываем его только потому, что разработчику программ полезно понимать, где в этом спектре находится его программа, так как от этого зависит, как ее выполнение можно ускорить. Ведь подходы к оптимизации таких программ существенно различаются.
Выполнение задач, ограниченных процессором, можно ускорить двумя способами. Во-первых, можно увеличить скорость процессора. Это очевидный, но не всегда возможный вариант. Во-вторых, некоторые такие задачи можно разделить на относительно независимые блоки, подзадачи, некоторые из которых можно выполнять одновременно. Такое возможно, если выполнение одного блока никак не использует результат выполнения других блоков. В таком случае, их можно выполнять одновременно, например, на разных ядрах одного процессора (или на разных процессорах, или даже машинах в кластере) и за счет этого получить прирост скорости. Такой подход называется параллелизмом или параллельным программированием.
Задачи, ограниченные вводом-выводом, сложно ускорить, используя более быстрый процессор. Это, конечно, не повредит, но такие алгоритмы только малую долю времени выполняются на нем. Здесь опять же поможет разбиение программы на независимые подзадачи. Но их уже не обязательно выполнять одновременно. Можно переключать подзадачи по мере блокировок, примерно так же как поступает операционная система с процессами. Такой подход называется асинхронное программирование. Еще вы можете встретить термин “оптимизация блокировок”.
Источник: TechRocks.ru.
Выполнение программы в таком случае, перестает быть строго последовательным. То есть мы не можем быть уверены в том, что инструкции программы выполнятся именно в той последовательности, которую мы задали при написании программы. Это немного похоже на создание программ в процедурном стиле, когда вместо написания всех инструкций в линейной последовательности, вы создаете обособленные функции, а их основного кода можете вызывать их в произвольном порядке.
Источник: ChessBase.
Еще это похоже на сеанс одновременной игры в шахматы. Гроссмейстер в этой аналогии - как процессор. Он думает быстро и его время очень ценно. А игроки - это задачи, которые часто “блокируют” его, то есть долго думают над ходом. Если бы гроссмейстер играл с каждым игроком последовательно, весь сеанс занял бы очень много времени. Поэтому на практике он не ждет окончания хода игрока, а перемещается к следующему и так далее по кругу. За исключением технических деталей, именно так и работают асинхронные программы.
Надо сказать, что в информатике существует путаница в терминах, связанных с этой темой. Не все четко понимают разницу между асинхронным и параллельным программированием. Тем более, что подзадачи тоже могут выполняться на разных ядрах. Поэтому не удивляйтесь, если в других источниках увидите другие определения асинхронности и параллелизма. Но здесь я буду использовать эти два термина для обозначения двух разных подходов к ускорению работы программ. Для общего названия практики программирования, заключающейся в разбиении алгоритма на независимые подзадачи я выбрал термин “Многозадачное программирование”. Многозадачное программирование, таким образом, включает в себя и асинхронность и параллелизм.
Выводы:
- Все алгоритмы можно поделить на две условные категории.
- Задачи, ограниченные процессором - вычислительные, постоянно выполняются.
- Задачи, ограниченные вводом-выводом - часто ждут внешних событий
- Первую категорию можно ускорить только за счет параллелизма.
- Вторую - за счет асинхронности и, в меньшей степени, за счет параллелизма.
- В момент, когда одна задача блокируется, процессор может переключиться на другую.
- Асинхронное программирование очень похоже на сеанс одновременной игры.
- В терминах существует путаница. Особенно в русскоязычных.
Каковы основные недостатки многозадачного программирования?
Многозадачное программирование помогает решить насущную проблему - оптимизацию работы программ. И поэтому оно широко используется при создании сложных информационных продуктов. В первую очередь в этом подходе нуждаются сетевые приложения, так как обмен данными по сети - это блокирующие операции. Тем не менее, надо понимать, что применение многозадачного программирования порождает определенные сложности.
Мы уже упоминали, что некоторые задачи по самой своей природе являются последовательными. Если выполнение каждой следующей инструкции требует использование результата предыдущей, то мы никак не сможет разделить программу на независимые подзадачи. Это, конечно, редкий случай, но такое может случиться. Большинство задач, все же поддаются некоторой декомпозиции.
Приведем простой пример - алгоритм решения квадратного уравнения через дискриминант. Можно разделить эту задачу на три - вычисление дискриминанта, вычисление первого корня и вычисление второго. Причем, первый этап должен быть реализован в самом начале, так как его результат используется в последующих шагах. А вот вычисления корней являются независимыми задачами, и их можно выполнять в любой последовательности и одновременно. Это, конечно, вычислительно очень простая задача, но хорошо иллюстрирует пример частично распараллеливаемого алгоритма.
Многозадачное программирование тесно связано с реализацией механизма многозадачности. Такой механизм может реализовываться либо самой операционной системой через механизм процессов, либо библиотекой языка программирования. Многозадачность - это механизм псевдоодновременного выполнения нескольких задач (алгоритмов) на компьютере, когда система многозадачности периодически переключает выполнение одного потока команд на другой. Это называется переключение контекста. В любом случае нужно помнить о двух типах многозадачности - кооперативной и вытесняющей. при кооперативной многозадачности переключение на другую задачу происходит при блокировке текущей. При вытесняющей - система сама может переключить контекст в произвольный момент, между двумя любыми инструкциями.
Если ваши задачи являются полностью изолированными, каждая задача “не замечает” переключение контекста, ведь при возврате к выполнению этой задачи она продолжится с того же места без каких-либо побочных эффектов. Проблемы возникают, если задачи взаимосвязаны и используют какие-либо общие ресурсы. Например, общие переменные. Ведь пока задача была приостановлена, значение этой общей переменной могло поменяться, а текущая задача на это не рассчитывала. А на практике большинство задач так или иначе используют общие ресурсы - переменные (то есть общие области в памяти), файлы, внешние устройства, сеть.
Поэтому если какой-то алгоритм работает корректно в обычном, однопоточном режиме, он может работать некорректно в многозадачном виде именно из-за таких общих ресурсов. Это называется потоконебезопасные алгоритмы. При написании многозадачных программ нам нужно следить за тем, как задачи в них взаимодействуют и обеспечивать потокобезопасность. Это дополнительные сложности, с которыми мы сталкиваемся при проектировании и реализации многозадачных программ.
Еще одна большая сложность при реализации многозадачных программ - сложность их отладки. Особенно при использовании вытесняющей многозадачности программист не может предсказать моменты переключения контекста. В результате инструкции выполняются в случайном порядке, что может приводить к ошибкам, которые то появляются, то исчезают. Ловить и исправлять такие ошибки довольно сложно.
Вместе с этим надо сказать, что распараллеливание алгоритма не является панацеей для быстродействия. Ведь переключение контекста - это сам по себе довольно длительный процесс. Конечно, обычно не приходится реализовывать собственный механизм многозадачности - есть уже готовые инструменты, но все равно, на создание задач и переключение между ними тратится время и ресурсы. Существует даже понятие чрезмерного распараллеливания, когда большая часть времени выполнения программы тратится не на выполнение полезной задачи, а на переключение между задачами.
Поэтому прирост скорости при использовании многозадачности никогда не бывает кратным. Даже если вы разбили программу на пять задач и запустили одновременно на разных ядрах процессора, не ожидайте пятикратного ускорения программы. Насколько в реальности может быть ускорена программа зависит от множества факторов, все, что мы можем сказать, что ускорение будет меньше, чем в пять раз.
Несмотря на все сложности, параллельное и асинхронное программирование - очень распространенные техники, которые зачастую помогают решить насущные проблемы разработки сетевых приложений. Часть только с помощью распараллеливания можно реализовать некоторую функциональность, которая в чисто последовательном режиме была бы просто невозможной. Например, создание сервера, который может одновременно обслуживать нескольких клиентов. Так что знание основ многозадачного программирования - необходимый навык для любого разработчика.
Выводы:
- Некоторые задачи являются в принципе последовательными.
- Такие программы сложнее проектировать, реализовывать и отлаживать.
- Существует понятие потокобезопасности, которую надо обеспечивать.
- Одновременное программирование связано с механизмами многозадачности.
- Вытесняющая многозадачность означает непредсказуемое переключение контекста.
- Не стоит забывать про накладные расходы - это все не бесплатно.
- Прирост скорости даже в идеальном случае не будет кратным.
Чем отличаются процессы и потоки?
Источник: D. Kurtz.
Для обеспечения многозадачности существуют известные механизмы операционной системы - процессы. Вы можете спроектировать программу так, чтобы она состояла из нескольких взаимодействующих процессов. Тогда запуском, переключением и управлением процессами займется операционная система. Здесь надо помнить два факта. Во-первых, каждый процесс является изолированным участком памяти, который имеет свой стек, собственные структуры данных, исполняемый код, и множество скрытых атрибутов. Во-вторых, все современные операционные системы реализуют именно вытесняющую многозадачность. То есть процессы могут быть приостановлены и переключены в произвольные моменты времени, вы как разработчик никак на это не можете повлиять.
Управление процессами - это прерогатива операционной системы. То есть все операции с процессами - создание, переключение, уничтожение - производится исключительно самой системой. Но на предоставляет специальные системные вызовы для того, чтобы сами процессы могли совершать необходимые действия. Например, системный вызов exec порождает новый процесс. А системный вызов exit - уничтожает текущий (этот системный вызов автоматически выполняется при достижении конца программы, фактически - это всегда последняя инструкция в любой программе). Поэтому любой процесс может запросить у операционной системы создание нового процесса. После этого в него можно загрузить определенную программу и запустить его на выполнение. Так как все эти операции осуществляются через механизм системных вызовов, они никак не зависят от языка программирования.
Но надо помнить, что все действия, связанные с переключением процессов, не имеют системных вызовов. Сами процессы никак не могут повлиять на то, когда операционная система может их приостановить. Обычные, немногопоточные программы как правило этого и не замечают, так как они возобновляют работу с того же места, для них этой приостановки как бы и не существовало. Но как только вы начинаете создавать многопоточные или многопроцессные приложения, приходится обращать внимание на этот механизм вытесняющей многозадачности. Более того, создание, переключение и уничтожение процесса - это довольно сложные и длительные операции. Кончено, с человеческой точки зрения это происходит мгновенно, много раз в секунду. Но с точки зрения компьютера это занимает довольно много времени. Поэтому операционные системы не переключают процессы очень часто.
В рамках одного процесса могут быть организованы так называемые потоки - отдельные задачи, каждая из которых имеет собственный алгоритм работы. Потоки похожи на процессы в том, что они выполняются независимо друг от другая, “параллельно”. Потоки так же могут блокироваться, ведь в ходе их выполнения могут быть выполнены блокирующие операции. Таким образом потоки тоже могут находиться либо в заблокированном состоянии, либо в состоянии готовности к выполнения, либо в состоянии выполнения в данный момент. Основная идея процессов состоит в том, что при выполнении блокирующей операции не происходит приостановки всего процесса операционной системы. Блокируется только текущий поток. Если в программе есть другие незаблокированные потоки, то есть потоки в состоянии готовности, то выполнение будет переключено на них. Это позволит не блокировать процесс целиком. Это называется оптимизация блокировок. За счет этого может произойти ускорение работы, так как программа будет меньше времени проводить в ожидании.
Любой процесс имеет как минимум один поток - главный. В процессе своего выполнения программа может создать новый поток и загрузить в него какой-то программный код. Создавать можно столько потоков, сколько необходимо. В отличие от процесса, все потоки выполняются в рамках одного процесса. Поэтому они имеют полный доступ ко всем общим ресурсам процесса - например, к переменным. Вообще, все потоки одного процесса имеют общую память - область памяти их процесса.
Вообще говоря, потоки, как и процессы - концепция операционных систем. Но на практике ни одна распространенная операционная система не поддерживает потоки на уровне ядра. Поэтому сейчас реализацией потоков занимается на ядро операционной системы (как с процессами), а внешние библиотеки. Поэтому существует множество реализаций потоков в разных библиотеках, в разных языках программирования. Мы будем изучать потоки на примере стандартной библиотеки Python, но даже в этом языке программирования существуют другие библиотеки для многопоточности. Их конкретная реализация может различаться в деталях и в механизме организации потоков.
Выводы:
- Для обеспечения многозадачности в операционной системе используются процессы.
- Операции с процессами происходят через системные вызовы и не зависят от языка.
- Создание процесса - довольно длительная процедура.
- Процессы управляются операционной системой, у программиста нет над ними контроля.
- Потоки выполняются в рамках одного процесса и имеют общую память.
- Потоки сейчас не поддерживаются на уровне ядра никакими популярными ОС.
Что может пойти не так?
Как мы уже говорили, при разработке обычных, немногозадачных программ не возникает никаких проблем при блокировке и переключении процессов. Так как процессы являются полностью изолированными единицами выполнения программного кода, когда процесс возобновляется, он продолжает свое выполнение в том же состоянии, на котором он остановился. Операционная система сама при переключении процесса переносит в нужные области оперативной памяти всю информацию, необходимую процессу для выполнения.
По-другому все обстоит в работе многозадачных приложений. Дело в том, что несколько потоков выполнения могут работать с общими ресурсами. Рассмотрим такую ситуацию. Программа состоит из двух потоков. Каждый из них работает с одним и тем же файлом - осуществляет чтение и запись в него. Допустим, посередине чтения из этого файла текущий поток прервался из-за вытесняющей многозадачности. Управление переключилось на второй поток программы, который начал запись в этот файл. После этого потоки опять переключились и управление вернулось к первому. С его точки зрения, посередине чтения содержимое файла изменилось. Это может привести к непредсказуемым последствиям, так как нарушает логику работы алгоритма.
Давайте рассмотрим классический пример. Допустим, мы пишем банковское приложение для работы со счетом клиента. И у нас есть функция списания денег со счета. Она принимает один аргумент - количество денег, которые надо списать. Допустим, так же, что счет клиента - дебетовый, то есть на нем не может быть долга. Тогда наша функция должна проверять, есть ли у клиента необходимая сумма. Если да, то списываем (и возвращаем, например, True), а если нет - отказываем. Это может выглядеть примерно так:
1
2
3
4
5
6
7
8
9
account = 100
def acquiring(amount):
global account
if account > amount:
account -= amount
return True
else:
return False
Довольно простая и абсолютно правильная логика. Наша программа будет работать без ошибок - сколько бы раз мы не вызвали функцию, счет в минус не уйдет, там стоит на это проверка.
Но допустим, для ускорения банковского обслуживания мы хотим сделать нашу программу многопоточной - чтобы функция эквайринга платежа выполнялась в новом потоке. Таким образом, можно одновременно обрабатывать несколько платежей, ускоряя работу программы.
Конечно, на практике такая простая функция и так будет работать быстро и не сильно выиграет от многопоточности. Но в реальности эта функция вполне может содержать, например, несколько запросов к удаленной базе данных, которые занимают много времени и блокируют поток. Так что в реальных приложениях есть что оптимизировать многопоточностью.
После введения многопоточности такая программа уже не будет работать гарантированно правильно. Допустим, у нас на счете 50 денег и мы списываем два раза по 30 денег. По идее, второе списание не должно пройти. Но вот, что может случиться. Если одновременно запущено два экземпляра этой функции, то первый поток может прерваться уже после проверки условия (проверка показала истину, так как 50 больше 30), но перед уменьшением баланса счета. В этот момент включается второй экземпляр функции, который тоже выполнит проверку, получит истинный результат, спишет деньги и закончится. А значит, выполнение вернется в первый поток, который продолжится с точно того же места, на котором прервался. То есть уже после проверки условия. И таким образом произойдет двойное списание.
Такая ситуация называется проблемой доступа к общим ресурсам. Общим ресурсом может быть не только файл, но и какая-то область в памяти, например, переменная, потоки ввода-вывода, любые внешние устройства. Очень часто в многопоточных программах общими ресурсами выступают переменные. Если несколько потоков работают одновременно с одними переменными, то они могут изменяться без ведома какого-либо потока.
То же самое происходит при обращении к внешним устройствам, например, к консоли. Вывод может перепутываться между потоками, нарушаться последовательность операций. В общем случае можно сказать, что проблема многопоточных программ состоит в том, что программист не может однозначно определить порядок выполнения операций в программе, состоящей из нескольких потоков.
Программы, которые корректно работают как в однопоточном, так и в многозадачном режиме называются потокобезопасными. Потокобезопасность - это свойство алгоритмов, программ, даже структур данных, которое заключается в способности правильно работать при наличии множества потоков. Обеспечение потокобезопасносности - это обязанность программиста, который разрабатывает многопоточные программы.
Одна из главных трудностей, с которыми встречается программист при написании многопоточных программ это то, что ошибки потокобезопасности очень трудно зафиксировать. В нашем примере со счетом двойное списание может произойти только если сложится очень определенная комбинация условий внешней среды. В какой момент произойдет переключение потока определяет операционная система исходя их множества факторов. Поэтому если мы запустим нашу программу вручную, скорее всего, все выполнится правильно. И тысячу раз все может выполняться правильно, а на тысяча первый - произойдет двойное списание. Поэтому ошибки потокобезопасности очень трудно заметить. Ведь такое двойное списание происходит и в реальности в настоящем банковском программном обеспечении, с его кучей степеней защиты, множеством стандартов и процедур тестирования.
Обеспечение потокобезопасности основывается на том, что ее нарушения возникают при доступе к общим ресурсам. Поэтому если ограничить возможность нескольких потоков одновременно обращаться к одному и тому же ресурсу, то программа станет работать корректно, так как избежит проблемы, описанной выше. Такой механизм называется блокировка ресурсов.
Выводы:
- Проблемы возникают, когда подзадачи обращаются к общим ресурсам.
- Если у потоков есть общая переменная, она может измениться без ведома потока.
- Если несколько потоков пишут данные в один файл, вывод может быть перепутан.
- То же самое может произойти с выводом в консоль.
- Потокобезопасность - это свойство программы работать правильно в конкуррентном режиме.
- Программист должен обеспечить потокобезопасность своей программы.
- Самый понятный способ обеспечения потокобезопасности - ввести блокировки ресурсов.
Как заблокировать доступ к ресурсу?
Источник: GeeksForGeeks.
Так как большинство проблем при работе многопоточных программ происходит при одновременном доступе нескольких потоков к одному общему ресурсу, самым решением этой проблемы будет запретить доступ нескольких потоков к одному и тому же ресурсу. Для этого существуют специальные объекты - замки, которые помогают заблокировать доступ к ресурсу.
Концептуально, замок работает очень просто. Это некоторый особый объект, у которого есть всего два действия - закрыть и открыть (обычно их называют “получить доступ”, acquire и “освободить”, release). При создании замок находится в состоянии “свободный”. Когда какой-то поток хочет поработать с определенным ресурсом (например, общей переменной), он получает досуп к замку. Получить доступ можно только к свободному замку. В этот момент замок переходит в состояние “закрытый”. Если в этот момент другой поток тоже захочет получить доступ, он не сможет, так как замок находится в состоянии “закрыт”. В этот момент второй поток блокируется и начинает ждать, когда поток, закрывший замок его освободит. Таким образом, два потока не могут работать одновременно с одним и тем же ресурсом. Поток, который первый закрыл замок заставит остальные потоки, даже если он сам прервется и передаст им управление, ждать, пока он не закончит работу с ресурсом и не высвободит замок.
Замки - это предохранительный механизм, с помощью которого программист может обеспечить потокобезопасность своих программ. Для того, чтобы эта схема сработала, разработчик должен найти все места в своей многопоточной программе, в которых идет работа с общим для нескольких потоков ресурсом, и “закрыть их на замок” - поставить перед началом работы получение доступа к замку, а в конце - освобождение замка. Не нужно думать, что использование замка в одном потоке обезопасит общий ресурс независимо от того, что происходит в других. Нет, если другие потоки не проверяют замок перед началом работы с ресурсом, то программа все разно будет потоконебезопасной.
Нужно отметить, что операции получения доступа и освобождения замка являются атомарными - потоки выполнения не могут прерваться посередине проверки. Именно поэтому, кстати, не получится обойтись “самодельными” замками. Ведь можно подумать, что такую проверку на занятость ресурса можно провести и просто условием, например так:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
locked = False
# thread 1
if not locked:
global locked
if not locked:
lock = True
do_something(resource)
lock = False
# thread 2
if not locked:
global locked
if not locked:
lock = True
do_something_else(resource)
lock = False
Этот псевдокод в принципе полностью повторяет логику работы замка. Однако, так все равно делать не нужно. Ведь мы говорим о вытесняющей многозадачности - то есть операционная система может прервать выполнение потока в произвольном месте. И вполне может случиться так, что выполнение прервется после проверки условия, но перед “закрытием” замка. Тогда второй поток подумает, что замок свободен, и также начнет работу с ресурсом. Если в этот момент управление вернется первому потоку, он продолжится с того же места, то есть уже после проверки условия, по ветке “then”. То есть тоже начнет работу с ресурсом.
Замки, которые существуют в библиотеках для многопоточного программирования реализованы специальным образом на низком уровне так, что проверка условия и изменение состояния замка - это одна атомарная операция. Так что даже если поток переключится, мы либо войдем в замок, либо нет. Поэтому одновременной работы с ресурсом не происходит.
Замок помогает заблокировать доступ к ресурсу. А что делать, если потоки в программе используют несколько общих ресурсов? Нужно создать по одному замку на каждый общий ресурс. Будь то переменные, сетевые сокеты, вывод в терминал или файлы - вы должны отследить все общие ресурсы - все “места пересечения” потоков вашей программы и работу с каждым ресурсом закрыть замком. Конечно, чем сложнее программа, чем с большим количеством объектов работают потоки, тем сложнее все отследить.
Именно поэтому, кстати, в Python существует так называемая глобальная блокировка интерпретатора. Разработчики языка в какой-то момент решили, что ставить отдельный замок на каждую блокирующую операцию - это слишком сложно и чревато ошибками. И поэтому поставили общую блокировку - одновременно только один поток может выполняться на интерпретаторе. Из-за этого многие считают реализацию многопоточности в Python неполноценной.
Но если вы работаете с несколькими ресурсами в многопоточной программе, вас поджидает еще одна потенциальная проблема - дедлоки, или взаимоблокировка. Давайте представим, что у нас есть два потока, каждый из которых работает с двумя ресурсами - А и В. Первый поток получает доступ к ресурсу А (закрывает соответствующий замок), и начинает с ним работать. Не освобождая А, он еще хочет получить доступ к ресурсу В. Тут допустим, что в это время второй поток уже получил доступ к ресурсу В (закрыл замок ресурса В) и хочет получить доступ к А. Получается неразрешимая ситуация.
Источник: Pro-Java.
Первый поток не может продолжится и блокируется потому, что он ждет, когда второй поток освободит ресурс В, а второй поток тоже заблокирован и ждет, когда первый поток освободит ресурс А. Так что ни один поток не может продолжать выполняться. И по сути вся программа намертво блокируется. Выхода их дедлока нет, можно только прервать задачу.
Но избежать такой ситуации при написании многопоточной программы довольно просто. Нужно всего лишь договориться, всегда блокировать ресурсы в строго определенном порядке. Причем это нужно только тогда, когда поток хочет одновременно поработать с двумя и более ресурсами. Хочешь работать в ресурсом А - пожалуйста. Нужен один ресурс В - тоже никаких ограничений. А вот если нужно и то и другое сразу - то всегда блокируем сначала ресурс А, а потом - В. И никогда не наоборот. Тогда дедлок никогда не сможет случиться.
Замки - это самый простой способ блокировки ресурсов. В более сложных случая могут понадобиться более продвинутые способы. Например, более сложной версией замка является семафор - замок со встроенным счетчиком подключений. Он позволяет ограничить количество одновременно работающих с ресурсом потоков. Такое полезно, например, для ограничения нагрузки на какие-либо участки программы. Например, для ограничения количества одновременных подключений к серверу или базе данных.
В заключении надо сказать, что даже самые простые замки сильно помогают обеспечить потокобезопасность программы. Но в замками не следует перебарщивать. Нужно помнить, что ожидание замка - это блокирующая операция, а смысл многопоточности - оптимизация блокировок. Может показаться соблазнительным не разбираться сильно и закрыть замком большую часть многопоточного алгоритма. Но это значит, что он сможет выполняться только в одном потоке за раз. Другими словами, если закрыть замком весь поток, то исчезнет смысл вообще использовать потоки - программа будет выполняться в синхронном режиме, как обычно. Да, он будет полностью потокобезопасно, что что толку? Поэтому замками надо закрывать минимально, только самые критические участки алгоритма программы, в которых идет именно работа с общим ресурсом.
Выводы:
- В операционных системах существуют специальные объекты - замки.
- Замок можно получить или освободить - это атомарные операции, они не могут быть прерваны.
- При попытке получить заблокированный замок поток блокируется - ждет его освобождения.
- При доступе к замку он блокируется, чтобы другой поток не мог в это время работать с ресурсом.
- На каждый ресурс нужно использовать один замок.
- Есть опасность попасть в дедлок - ресурсы нужно блокировать в определенном порядке.
- Существуют более продвинутые способы блокировки - семафоры и мьютексы.
- Чем больше блокировок использовать, тем меньше толку от многопоточности.
Какие инструменты Python надо знать?
Во всех языках программирования существуют специальные механизмы реализации как параллельного, так и асинхронного программирования. Здесь мы рассмотрим возможности языка Python для многозадачного программирования. На примере этого языка мы познакомимся с основными механизмами обеспечения многозадачности, какие они бывают, в чем их особенности и как выбрать нужный механизм для решения конкретной задачи.
Любой язык программирования позволяет получить доступ к механизму системных вызовов операционной системы. Что-то реализуется встроенными конструкциями языка, что-то - средствами стандартной библиотеки. Создание нового процесса и выполнение функции в новом процессе - это одна из стандартных возможностей операционной системы.
В языке Python для работы с многопроцессорностью существует стандартный модуль multiprocessing. Он позволяет выделить функцию или класс в новый процесс. Процессы в данном случае управляются операционной системой и, поэтому, реализуют вытесняющую многозадачность. При работе с процессами следует помнить, что создание процесса - достаточно дорогая процедура. Поэтому если распараллеливать задачу слишком сильно, можно добиться обратного эффекта снижения производительности - большая часть времени выполнения программы будет уходить на создание, уничтожение и переключение процессов.
Особенно явно это в случае с интерпретируемыми языками, как Python. Ведь программа на таких языках не выполняется непосредственно. Программу выполняет интерпретатор. А если программа состоит из нескольких процессов, то в каждый из них должен быть загружен свой экземпляр интерпретатора. А это занимает и время и оперативную память.
Потоки - более легковесная сущность. Но, как мы говорили, потоки не поддерживаются современными операционными системами. Поэтому они реализуются программно в библиотеках. Это значит, что в разных библиотеках и разных языках программирования может существовать собственна реализация многопоточности и как она работает - зависит от конкретной библиотеки. В Python существует стандартный модуль для написания многопоточных программ - threading. Потоки, которые он создает управляются не операционной системой, а этим модулем. Но он так же реализует вытесняющую многозадачность.
У модуля threading в Python есть одна особенность - глобальная блокировка интерпретатора (GIL, global interpreter lock). Для обеспечения потокобезопасности многопоточных программ, написанных и использованием этого модуля, в него встроен один глобальный замок, блокирующий доступ к интерпретатору только для одного потока за раз. Это значит, что в каждый конкретный момент времени может выполняться только один поток. Даже если компьютер имеет несколько вычислительных ядер, GIL не позволит использовать истинную параллельность, то есть одновременное выполнение нескольких потоков на процессоре.
В языке Python есть еще один механизм многозадачного программирования - модуль asyncio. Он использует еще более легковесные сущности, даже легче, чем потоки - так называемые асинхронные корутины. Из-за особенностей реализации, такие корутины практически не занимают дополнительной памяти и не тратят время на создание или удаление. За счет этого, вы можете создать просто огромное количество корутин, то есть распараллелить задачу очень сильно.
Все асинхронные корутины выполняются в одном процессе и даже, формально, в одном потоке. Поэтому они тоже не дадут возможности параллельного выполнения. Но в отличие от threading этот модуль реализует кооперативную многозадачность. То есть программист при написании программы сам решает, в какие моменты одна задача может переключится на другую. Это очень облегчает проектирование и отладку асинхронных программ. Проблема потокобезопасности практически перестает существовать.
Однако, не обходится и без недостатка. Из-за особенностей внутреннего устройства этого модуля, в нем нельзя использовать стандартные блокирующие операции. То есть, если вы в асинхронной программе используете, например, оператор input() или sock.recv(), то заблокируется весь поток, со всеми корутинами. В модуле asyncio реализованы собственные, неблокирующие версии всех популярных блокирующих операторов и методов. И если вы пишите программу заново, это не проблема. Однако это сильно затрудняет модификацию уже существующего кода, чтобы сделать его асинхронным.
Процессы | Потоки | Async | |
---|---|---|---|
Оптимизация блокировок | вытесняющая | вытесняющая | кооперативная |
Использование нескольких ядер | да | нет | нет |
Масштабируемость | низкая (десятки) | средняя (сотни) | высокая (тысячи+) |
Использование стандартных блокирующих операций | да | да | нет |
GIL | нет | да | нет |
Сравнение самых распространенных методов многозадачного программирования в Python можно видеть в таблице. Далее мы подробно поговорим про каждый из них, и будем приводить примеры кода. Сейчас же надо сказать, что не существует идеального решения, которое было бы лучше во всех условиях. Поэтому для решения конкретной задачи надо уметь выбирать нужный механизм многозадачности.
Если вам нужно ускорить задачу, ограниченную процессором, то это можно сделать только за счет параллелизма, то есть задействовав несколько вычислительных ядер или процессоров. Это умеет делать только модуль multiprocessing. Но стоит помнить, что нет смысла создавать процессов больше, чем в системе вычислительных ядер. Если вам нужно распараллелить задачу больше, нужно выбирать либо threading, либо asyncio.
Модуль threading - это классическая реализация многопоточности, она средняя по всем параметрам и подойдет в большинстве случаев. Особенно если вы хотите сделать многопоточным уже существующий код и не хотите его полностью переписывать. Если же вам нужно создать тысячи или десятки тысяч задач, то стоит обратить внимание на модуль asyncio. Надо отметить, что судя по развитию новых версий, именно этот модуль становится основным в Python, и именно на него делают ставку авторы и разработчики языка.
Выводы:
- Все языки программирования позволяют создавать процессы.
- Почти у всех языков программирования есть средства многопоточного программирования.
- В питоне три стандартные библиотеки дают возможность параллельного программирования.
- Модуль multiprocessing позволяет писать многопроцессные программы.
- Модуль threading позволяет писать многопоточные программы.
- Модуль asyncio позволяет создавать асинхронные задачи с кооперативным переключением.
- У каждого подхода свои недостатки и своя ниша.
Многопоточное программирование на Python
Как выделить функцию в поток?
Давайте начнем рассмотрение многопоточного программирования с модуля threading. Он предназначен для выполнения определенного участка кода в отдельном потоке. В поток выполнения необходимо загрузить какую-то последовательность команд. Проще всего для этих целей использовать функцию. Давайте рассмотрим простой пример создания потоков:
1
2
3
4
5
6
7
8
9
import threading
def proc():
print("Процесс")
p1 = threading.Thread(target=proc, name="t1")
p2 = threading.Thread(target=proc, name="t2")
p1.start()
p2.start()
В этой программе мы импортировали модуль, затем объявили функцию. Эта функция будет выполняться в новом потоке. Обратите внимание, что мы ни разу не вызываем эту функцию явно - тогда она выполнится по месту, в том же потоке, как обычно. Вместо этого, мы передаем эту функцию как параметр при создании объекта Thread. Этот объект будет хранить информацию о потоке и через него можно к этому потоку обращаться. Например, запустить его на выполнение. Еще одним параметром является имя. Задавать его необязательно, но через него удобно дифференцировать потоки.
После создания потока его можно запустить на выполнение, вызвав метод start(). Без этого, код потока не выполнится. Именно после команды start() произойдет выполнение кода, который был написан в целевой функции потока.
В любой программе обязательно есть один поток выполнения - главный. Он содержит основной код программы. Именно он выполняется по умолчанию. В момент запуска нового потока на выполнение программа как бы “раздваивается” - появляется новый поток, но вместе с ним продолжает существовать и основной. Выполнение потока является неблокирующей операцией. Рассмотри такой пример:
1
2
3
4
5
6
7
8
9
10
import threading
import time
def proc():
time.sleep(5)
print("Second thread")
threading.Thread(target=proc).start()
print("Main thread")
В этой программе выполнение второго потока занимает 5 секунд. Но когда мы запустим эту программу, мы увидим, что надпись “Main thread” появилась сразу же. Это потому, что программа не ждет окончания выполнения нового потока, как было бы в случае с обычной функцией, а сразу продолжает выполнение следующих инструкций. То есть основной поток продолжается несмотря на вызов второго потока.
Это тем более верно, так как метод time.sleep() является блокирующим. Поэтому даже если после выполнения сразу начал исполняться именно второй поток, после инструкции sleep() он заблокировался и управление перешло к первому (основному). Поэтому такая программа всегда покажет сначала “Main thread”, а потом, через 5 секунд - print(“Second thread”).
Это и есть оптимизация блокировок. Рассмотрим еще более явный пример:
1
2
3
4
5
6
7
8
9
10
11
import threading
import time
def proc():
time.sleep(5)
print("Second thread")
threading.Thread(target=proc).start()
threading.Thread(target=proc).start()
print("Main thread")
В этой программе мы два раза запускаем эту же функцию, каждый раз в новом потоке. Если бы мы использовали обычный, однопоточный подход, то выполнение этой программы заняло бы 10 секунд. Многопоточная программа выполняется за 5 секунд - это максимальное время выполнение одного потока. Это происходит за счет того, что программа ждем 5 секунд два раза не последовательно, один за другим, а как бы одновременно.
Давайте рассмотрим еще один пример. В нем мы создаем два потока, и в каждом выводим имя текущего потока.
1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
import time
def proc():
time.sleep(1)
print(threading.current_thread().name)
p1 = threading.Thread(target=proc, name='thread 1')
p2 = threading.Thread(target=proc, name='thread 2')
p1.start()
p2.start()
print("End")
Обратите внимание, как в threading можно получить доступ к объекту текущего потока - вызвав threading.current_thread(). В этом модуле вообще довольно много полезный функций, поэтому очень рекомендуем обратиться к документации к нему для вашей версии Python.
При выполнении данной программы вы чаще всего будете видеть такой вывод в консоли:
1
2
3
End
thread 2
thread 1
но иногда можно увидеть и так:
1
2
3
End
thread 1
thread 2
То есть не обязательно первый поток выполнится первым. Даже при условии, что они полностью идентичны по содержанию. Всегда помните, что теперь, в мире многопоточного программирования, нельзя определенно сказать, в каком порядке будут выполняться инструкции вашей программы. Это зависит от большого числа факторов, в том числе внешних по отношению к алгоритмы программы. Поэтому рассчитывать на определенный порядок выполнения потоков лучше не стоит. В этом большая гибкость, но и главная сложность многозадачного программирования.
Выводы:
- Для создания потока нужен обособленный участок кода - проще всего использовать функцию.
- Создание и запуск потока - разные операции, они не обязательно идут последовательно.
- При запуске потока программа “раздваивается” - появляются новый и основной поток.
- Основной поток продолжает выполняться и после запуска нового.
- В каком порядке будут выполнены инструкции нельзя предсказать.
Как передать параметры в поток?
Очень часто при проектировании многопоточных программ в отдельных потоках выполняются однотипные задачи. Редко приходится отдельно описывать код каждого потока индивидуально. Обычно мы пишем одну функцию, которая будет выполняться в отдельном потоке много раз, чтобы распараллелить алгоритм. Конечно, практически никогда не нужно много раз выполнять полностью идентичный код. Чаще всего он отличается какими-то небольшими параметрами. Для этого мы используем аргументы функций.
Давайте рассмотрим, как в модуле threading можно передать в функцию параметр. Как вы знаете, в многопоточной программе фукнция-поток не вызывается явно, поэтому существует отдельный синтаксис задания параметров функции при создании потока. Вот пример:
1
2
3
4
5
6
7
8
9
import threading
def proc(n):
print "Процесс", n
p1 = threading.Thread(target=proc, name="t1", kwargs={"n": "1"})
p2 = threading.Thread(target=proc, name="t2", args=[2])
p1.start()
p2.start()
Из данного кода довольно очевидно, что можно передавать аргументы двумя способами. Если вам удобнее передавать аргументы по месту (как позиционные при обычном вызове функции), то вам понадобится параметр args, в котором можно задать массив значений аргументов функции. Конечно, размер массива должен совпадать с количеством обязательных аргументов функции, указанных при ее объявлении.
Но иногда проще задавать аргументы не по месту, а по имени. Например, если у функции много необязательных аргументов и вы хотите передать только некоторые из них. Или вам удобнее передавать аргументы в другому порядке, нежели в каком они объявлялись. Наконец, задание аргументов с явным указанием имени служит для повышения читаемости кода, названия аргументов часто поясняют их смысл и служат самодокументацией.
В таком случае, следует воспользоваться параметром kwargs, в котором передаваемые аргументы указываются в виде словаря. Ключом в словаре служит имя аргумента, как оно указано в объявлении функции, а значением - собственно значение аргумента.
Обычно вместе в передачей аргумента в функцию рассматривают и возвращение из функции. Но здесь не все так просто. Дело в том, что в функциях, работающих в отдельных потоках привычный оператор return бесполезен. Вы, конечно, можете его использовать (например для управления выходом из функции как часто делают), но значение вы так не получите. Опять же, вы не вызываете свою функцию явно, поэтому не сможете написать res = func()
.
Для того, чтобы получить значение из функции, необходимо воспользоваться другими механизмами. В ряде случаев можно выводить результат работы функции в консоль. Или пользоваться общими переменными, файловым выводом. Более подробно получение результата мы рассмотрим позднее.
Выводы:
- Для дифференциации потоков в функцию-поток можно передать параметр.
- Это очень удобно для распараллеливания параметрических действий.
- Обратите внимание, что вернуть значение их потока не так просто.
- Для получения результата работы потока используется консоль, общие переменные или файлы.
Как выделить класс в поток?
Если ваш многопоточный алгоритм достаточно сложен, то иногда хочется воспользоваться объектно-ориентированным программированием. Программирование в классах предоставляет более широкие возможности. И в модуле threading есть возможность выделить в поток не просто функцию, а метод класса.
Давайте сразу рассмотрим пример кода, который иллюстрирует использование классов для создания потока:
1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
class T(threading.Thread):
def __init__(self, n):
threading.Thread.__init__(self, name="t" + n)
self.n = n
def run(self):
print "Процесс", self.n
p1 = T("1")
p2 = T("2")
p1.start()
p2.start()
Итак, вместо функции мы объявляем класс. Этот класс должен быть потомком класса Thread. Как часто бывает при наследовании от библиотечного класса, в конструкторе мы должны вызвать конструктор родительского класса. Это необходимо для реализации необходимой функциональности. Кроме этого в конструкторе мы может инициализировать какие-то необходимые поля этого класса.
Сам код, который будет выполняться в новом потоке должен содержаться в переопределенном методе run() данного класса. Обратите внимание, что они не принимает никаких аргументов (кроме обязательного self). В случае с методом класса это и необязательно, как как всю необходимую информацию можно передать в метод через поля класса.
Кроме этих двух методов в классе могут быть и другие методы или поля. Эти два - обязательные.
Обратите внимание на то, как происходит создание и запуск потока. Для создания нам нужно инстанцировать этот класс. Это, конечно, можно сделать много раз, столько, сколько нужно. Так как мы получили объект того же класса threading.Thread, как и классы, с которыми мы работали раньше, у него уже реализованы все методы для работы с потоками. Например, метод start(), с помощью которого происходит запуск потока.
Выводы:
- Часто функции недостаточно и в потоке можно запустить класс.
- При этом выполняться будет именно метод run().
- Это требуется, если в функцию придется передавать очень много значений и удобнее хранить их в полях класса.
Как завершить выполнение потока?
При выполнении многопоточной программы почти всегда возникает необходимость воспользоваться результатами работы созданных потоков. Мы уже обсуждали, что просто так из функции нельзя вернуть значение. Для того, чтобы второй поток смог передать в основной результат своего выполнения проще всего использовать общую переменную. Давайте рассмотрим простой пример:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading
import time
res = "Initial value"
def proc():
time.sleep(1)
global res
res = "The thread result"
p = threading.Thread(target=proc)
p.start()
print(res)
В этой программе мы создали переменную, в которую будем записывать результат работы потока. При старте программы поместим туда какое-то начально значение. В функции-потоке мы выполняем действия, обращаемся к переменной как к глобальной и записываем в нее результат. После этого в основной программе создаем и запускаем поток, а потом выводим результат на экран.
При запуске этой программы мы сталкиваемся с проблемой. Мы же помним, что при запуске нового потока программа раздваивается и основной поток программы продолжает исполняться. Таким образом инструкция вывода исполнится сразу же. К этому времени значение переменной не успеет измениться, так как поток еще не закончил свою работу.
Нам нужен какой-то способ выполнить определенные действия после окончания работы другого потока. Такая задача называется синхронизацией потоков. Существуют специальные механизмы, которые позволяют выполнять действия в одном потоке по событиям или условиям в другом потоке.
Самым простым механизмом синхронизации потоков является присоединение потока. У объекта Thread есть метод join(). Он работает интересным образом. Он блокирует текущий поток (тот, в котором написан join) до момент окончания того потока, у которого он вызван. Таким образом этот метод позволяет как бы “подождать” окончания другого потока. Давайте воспользуемся этим методом для того, чтобы вывести результат работы уже после того, как он вычислется:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading
import time
res = "Initial value"
def proc():
time.sleep(1)
global res
res = "The thread result"
p = threading.Thread(target=proc)
p.start()
p.join()
print(res)
Мы добавили всего одну строчку, но программа стала работать совершенно по другому. В момент выполнения join основной поток приостанавливается и начинает ждать окончания нового. Таким образом, следующие инструкции будут выполнены уже после окончания вычислений в новом потоке. Конечно, не обязательно ставить join сразу же после start. Вы можете после запуска потока еще что-то сделать в основном, а когда вам понадобится результат - подождать его.
В примере с одним новым потоком программа начинает выполняться строго последовательно. Она запускает поток, ждет его завершения и затем продолжается. Так какой же смысл вообще использовать многопоточность? Разве не тот же самый результат будет если просто вызвать функцию на выполнение? Если мы создаем один новый поток то да, никакого выигрыша мы не получаем. Но зачастую нам нужно создать множество потоков.
Давайте в нашем примере создадим не один поток, а несколько. Это проще всего сделать в цикле:
1
2
3
4
for i in range(5):
p = threading.Thread(target=proc)
p.start()
p.join()
Внимательно разберитесь, как работает эта программа. На каждой итерации цикла мы создаем поток, запускаем его, ждем завершения и переходим на следующую итерацию. Так наша программа тоже будет работать полностью синхронно - мы не продолжаем, пока не закончился очередной поток.
Для того, чтобы воспользоваться преимуществами многопоточности и выполнять потоки параллельно, нам нужно сначала их все создать, а затем их все присоединить. Но так как нам придется обращаться к каждому потоку, нам следует добавить все потоки в массив:
1
2
3
threads = []
for i in range(5):
threads.append(threading.Thread(target=proc))
Запускать потоки можно и сразу после создания, но можно и отдельно. Причем Python позволяет это сделать очень лаконично и красиво используя генераторное выражение:
1
[p.start() for p in threads]
После запуска мы можем так же и присоединить все потоки:
1
[p.join() for p in threads]
Давайте объединим все в полную программу:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading
import time
def proc():
time.sleep(1)
threads = []
for i in range(5):
threads.append(threading.Thread(target=proc))
[p.start() for p in threads]
[p.join() for p in threads]
Эта программа представляет собой довольно распространенный шаблон многопоточной программы - в основном потоке мы создаем несколько дочерних, выполняем их параллельно, затем присоединяем их все для обработки результата. Вы можете использовать этот шаблон дополнив его необходимым кодом. Но такая схема применяется почти всегда. При использовании модуля threading можно придумать много разных схем взаимодействия потоков, дочерние потоки могут сами создавать новые потоки, можно присоединять потоки не туда, где они были созданы. Но в практической деятельности сложные схемы взаимодействия потоков применяются редко. Рассмотренный шаблон прост в понимании, достаточно универсален и полезен. В будущем мы рассмотрим и другие шаблоны многозадачного программирования.
Выводы:
- Часто требуется совершить какие-либо действия после окончания работы потока.
- Для этого существует операция присоединения потока.
- Текущий поток блокируется и ждет, когда будет закончен заданный.
- Это часто требуется, если нужно воспользоваться результатом работы потока.
- Надо четко понимать, что кого будет ждать, в потоках легко запутаться.
- Поток, в котором выполняется присоединение будет ждать тот поток, у которого оно вызвано.
- Чаще всего дочерние потоки присоединяются к главному.
- Частый шаблон - массовое создание и затем массовое присоединение потока.
Как использовать замки в потоках?
Как мы уже говорили, при выполнении многопоточных программ могут возникнуть условия, когда несколько потоков одновременно работают с одним ресурсом. Такая ситуация называется “race condition” и может приводить к непредсказуемому поведению программы. Для исправления этой проблемы существуют методы блокировки ресурсов, например уже рассмотренные замки.
В модуле threading присутствует специальная реализация замков, объект Lock, применяя который можно обеспечить потокобезопасность программы. Работа с ним достаточно простая. Рассмотрим ее на примере. Допустим, у нас есть программа, которая работает в потоке с общей переменной. Одна функция в потоке ее увеличивает, а другая - уменьшает:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import threading
deposit = 100
def add_profit():
global deposit
for i in range(100000):
deposit = deposit + 10
def pay_bill():
global deposit
for i in range(100000):
deposit = deposit - 10
thread1 = threading.Thread(target = add_profit)
thread2 = threading.Thread(target = pay_bill)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(deposit)
По идее, так как мы увеличиваем и уменьшаем значение переменной одинаковое количество раз на одинаковое значение, после выполнения программы, ее значение не должно поменяться. То есть программа должна выдать “100”. Попробуйте запустить такую программу самостоятельно. Алгоритмы очень простой и, вроде бы, ничего опасного мы не делаем. Но все равно, иногда программа выдает значение “-246640”, что явно говорит об ошибке в алгоритме. Даже в таких невинных случаях, потоконебезопасный алгоритм может нас подвести.
А этот алгоритм потоконебезопасный потому, что мы работаем с общими ресурсами. В данном случае - общая переменная deposit
. Нам нужно закрыть работу с тим ресурсом под блокировку.
Как мы уже говорили, для каждого общего ресурса нужно создать свой замок. В данном случае он у нас один, поэтому и замок нужно создать один. Его нужно создать в главной программе до объявления функций-потоков. В самих потоках мы перед началом работы получаем замок, а после окончания работы с ресурсом - высвобождаем его:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import threading
lock = threading.Lock()
deposit = 100
def add_profit():
global deposit
for i in range(100000):
lock.acquire()
deposit = deposit + 10
lock.release()
def pay_bill():
global deposit
for i in range(100000):
lock.acquire()
deposit = deposit - 10
lock.release()
При использовании замков может случиться одна нестандартная ситуация. Если в процессе работы над ресурсом произошла ошибка или исключение, то поток прервется, но замок останется заблокированным. В этом случае другой поток не сможет получить доступ к этому ресурсу. Чтобы исключить такую ситуацию, надо освобождать замок даже в случае аварийного завершения потока:
1
2
3
4
5
6
7
8
def add_profit():
global deposit
for i in range(100000):
lock.acquire()
try:
deposit = deposit + 10
finally:
lock.release()
Также можно предусмотреть определенные действия в случае, если замок закрыт. По умолчанию, метод lock.acquire()
блокирует поток до тех пор, пока замок не освободиться. Но ему можно передать булев параметр - флаг ожидания. По умолчанию он равен True
. Если передать в него False
, то в случае занятости замка метод просто вернет состояние замка, без блокировки. Использовать это в программе можно, например, так:
1
2
3
4
5
6
7
8
9
def add_profit():
global deposit
for i in range(100000):
if not lock.acquire(False):
print("Resource is busy")
try:
deposit = deposit + 10
finally:
lock.release()
Для упрощения работы, замки в Python можно использовать как контекстные менеджеры. Если вы не знаете, что это - почитайте подробнее. Примерно так же мы работаем с файлами - с помощью инструкции with
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import threading
lock = threading.Lock()
deposit = 100
def add_profit():
global deposit
for i in range(100000):
with lock:
deposit = deposit + 10
def pay_bill():
global deposit
for i in range(100000):
with lock:
deposit = deposit - 10
Паттерн “Множественное создание потоков”
В многопоточном программировании существует много шаблонов, следуя которым вы сможете легко реализовать любой распространенный многопоточный алгоритм. Один из самых простых шаблонов - это распараллеливание действий по группам. Представим, что нам нужно произвести какое-то вычисление много раз, например, 1000. С другой стороны, нецелесообразно создавать 1000 потоков. Ведь 1000 действий можно разделить равными частями на два потока, на 10, на 50, как угодно.
В таких случаях удобно воспользоваться разбивкой действий на группы. Это работает примерно как пагинация при выводе большого количества объектов данных. В большинстве случаев можно передать в функцию, которая занимается обработкой какого-то объекта из множества номер этого объекта. Допустим, у нас 100 объектов которые надо обработать. И мы хотим создать, например, 5 потоков, каждый из которых будет обрабатывать по 20 объектов.
Обычно мы заранее знаем общее количество объектов, и желаемое количество групп (количество создаваемых потоков). Давайте обозначим это переменными:
1
overall, n_group = 100, 5
Из этих параметров легко вычислить, сколько объектов должен обрабатывать один поток. По определенным причинам нам нужно, чтобы эта переменная была целочисленная:
1
amount = int(overall / n_group)
Мы пока не будем задумываться над тем, что будет, если одно число не делится на другое. Тогда надо предусмотреть округление в большую сторону, проверки не превышение максимального количества объектов. Это довольно просто, но отвлечет нас от главного - смысла шаблона. Можете сами реализовать это в качестве упражнения.
Теперь мы будем создавать в цикле потоки. Каждому потоку мы передадим два параметра - начальный номер объект и количество объектов, которые он должен обработать. Начальный номер тоже достаточно легко вычислить:
1
2
3
4
threads = []
for i in range(n_group):
start = i * amount
threads.append(threading.Thread(target=proc, args=[start, amount]))
Теперь нам осталось набросать, как должна выглядеть сама функция-поток. Она должна принимать эти два параметра и в цикле обрабатывать объекты в этих пределах. Примерно так:
1
2
3
4
def proc(start, amount):
for i in range(start, start+amount):
time.sleep(1)
print(i)
Вы можете брать эту программу за основу. В мелких деталях алгоритм может отличаться, но главный смысл - разбивка множества действия на равные группы для выполнения в нескольких потоков - будет полезным во многих практических приложениях. Давайте соберем воедино всю программу:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading
import time
def proc(start, amount):
for i in range(start, start+amount):
time.sleep(1)
print(i)
overall, n_group = 100, 5
amount = int(overall / n_group)
threads = []
for i in range(n_group):
start = i * amount
print(start, amount)
threads.append(threading.Thread(target=proc, args=[start, amount]))
[p.start() for p in threads]
[p.join() for p in threads]
print("End")
Многопроцессное программирование на Python
Зачем использовать multiprocessing?
Многопоточное программирование очень полезно для оптимизации задач, ограниченных вводом-выводом. Однако на вычислительных задачах, ограниченных центральным процессором использование нескольких потоков не даст прироста скорости. В Python это происходит за счет глобальной блокировки интерпретатора. Поэтому модуль threading не даст возможности ускорять работу программ за счет распараллеливания задач на несколько вычислительных ядер. Для этого нам понадобится механизм создания нескольких процессов и распределения вычислительной нагрузки программы между ними. Именно для этого и нужен модуль multiprocessing.
Модуль multiprocessing является стандартным модулем языка Python с версии 2.6. Работа с ним очень похожа на работу с модулем threading. Вы так же выносите в отдельный объект выполнения определенную часть программы - удобнее всего работать в функциями. Но теперь эта часть загружается в новый отдельный процесс, который специально создается для этого операционной системой.
Процессы, в отличие от потоков, создаются, уничтожаются и управляются операционной системой. Все потоки одного процесса имеют общую область в памяти, где хранятся, например, переменные программы. Процессы же - полностью изолированные сущности, которые не имеют никаких общих участков. С одной стороны, это сильно облегчает обеспечение потокобезопасности многопроцессных программы - ведь большую часть работы берет на себя операционная система.
В работе многопроцессных программ на интерпретируемых языках программирования есть существенная особенность. Программы на компилируемых языках существуют в виде машинного кода и выполняются непосредственно на процессоре. Выполнение интерпретируемых программ, скриптов, устроено иначе. При “запуске” программы, которая является всего лишь текстовым файлом, на самом деле запускается интерпретатор, который читает текст скрипта, переводит в машинный код и выполняет его строчка за строчкой. Точно так же приходится поступать, если программа порождает новый процесс. В новый процесс загружается копия интерпретатора Python. Вы можете это увидеть сами в диспетчере задач или в выводе команды top. Вы увидете несколько процессов, обозначенных “python.exe” (на Windows).
Интерпретатор Python - это большая и сложная программа. И создание нового потока на Python занимает довольно много времени и памяти. По меркам компьютера, конечно, для человека все происходит мгновенно. Но именно по этой причине, процессы в Python масштабируются не так хорошо, как потоки. То есть при увеличении часла процессов, которые создаются в программе, увеличиваются накладные расходы на создание и переключение процессов. Поэтому делать очень много процессов обычно не имеет смысла. Как правило, в многопроцессных программах количество процессов, на которые распараллеливается программа, определяется количеством ядер центрального процессора компьютера. Это не жесткое требование, но некоторый ориентир. Конечно, все зависит от конкретной реализации алгоритма и архитектуры программы. Но в любом случае, речь идет о единицах, в крайнем случае - десятках процессов. В то время, как потоки могут создаваться сотнями и тысячами.
При создании процесса кроме интерпретатора в него загружается копия всех ресурсов программы. То есть, из потока вы можете иметь доступ ко всем глобальным переменным, которые были инициализированы до создания нового процесса. Но это именно копии, вы не можете, как в процессах, обмениваться информацией между процессами, изменяя значение общих переменных, ведь у каждого процесса теперь своя копия всех переменных. Это затрудняет обмен данными между процессами, но зато сильно облегчает проектирование программы, потому что меньше приходится думать о потокобезопасности. А в модуле multiprocessing есть специальные инструменты для удобного обмена данными, которых не было в модуле threading.
Но несмотря на все трудности, многопроцессный подход имеет свои ключевые преимущества, и области применения. Как мы уже говорили, только многопроцессность позволяет на Python добится ускорения вычислительных задач. Многопоточность здесь не поможет из-за глобальной блокировки интерпретатора. Но еще многопроцессность применяется для большей изоляции компонентов программы. Например, во многих современных браузерах каждая новая вкладка открывается в новом процессе. Это нужно для того, чтобы, если при отображении одной веб-страницы произошла непредвиденная ошибка, приведшая к зависанию или аварийному завершению процесса, то это не приведет к закрытию браузера в целом. Так как все потоки изолированные, когда “вылетает” один из них, остальные продолжают работать. По этой же причине внешние модули тоже часто выполняются в отдельных процессах, чтобы непроверенный сторонний код не обрушивал всю программу целиком.
Выводы:
- Модуль multiprocessing был добавлен в Python версии 2.6.
- Используя multiprocessing вы можете обойти GIL.
- Входит в стандартную библиотеку Python.
- Позволяет запускать задачи в разных процессах.
- Процессы управляются операционной системой.
- Каждый процесс имеет свою копию интерпретатора и всех ресурсов.
- Процессы питона - очень тяжеловесные, ведь у каждого свой интерпретатор.
- Позволяет получить прирост производительности на многоядерных системах.
Как выделить функцию в процесс?
Основным классом, с которым приходится работать в модуле multiprocessing является класс Process. Он очень похож на класс Thread и позволяет создать несколько процессов, которые вызывают одну и ту же функцию:
1
2
3
4
5
6
7
8
from multiprocessing import Process
def worker(name):
print('Hello from process!')
if __name__ == '__main__':
p = Process(target=worker)
p.start()
В данном примере мы объявляем функцию, которая будет выполняться в отдельном процессе. Затем мы создаем объект Process и запускаем его на выполнение точно так же, как и в модуле threading при помощи метода start()
. Но в отличии от объекта Thread, при вызове этого метода в объекте Process программа отправляет операционной системе запрос на создание нового процесса. После создания в новый процесс загружается копия интерпретатора Python со всеми ресурсами, в том числе со всеми переменными, созданными в основной программе до порождения потока.
Из-за особенностей реализации процессов, которые мы уже обсуждали, в функции, которая выполняется в процессе нельзя использовать глобальные переменные. Вернее, использовать их можно, но они у каждого процесса будут свои, а не общие, как в случае с потоками. Так же не получится просто так использовать return
для возврата результата работы функции.
Обратите внимание, что выделить в процесс можно только верхнеуровневую функцию. потоки не работают с методами или вложенными функциями. Так же обязательно использовать в основном коде программы конструкцию if __name__ == '__main__':
. Ведь во все дочерние процессы изначальная программа будет загружаться как модуль. Это особенность реализации процессов в Python. И если вы не будете использовать эту конструкцию, то в новых процессах будет выполняться основной код программы, в том числе инструкции создания новых потоков. Это очень оригинальный способ уйти в бесконечную рекурсию, который чреват зависанием всей операционной системы.
Рассмотрим уже знакомый нам пример - массовое создание процессов. В этом модуле все происходит полностью аналогично threading - работает присоединение процесса, передача параметров в функцию-процесс. Для этого примера возьмем вычислительный пример - проверка числа на простоту:
1
2
3
4
5
6
7
def worker(n):
if n < 2: pass
i = 2
while i*i <= n:
if n % i == 0:
return
print(x)
Эта функция, конечно, неоптимизирована, но нам для примера подойдет. Она печатает число только, если это число простое. Функция проверяет, делится ли заданное число на все числа от 2 до корня из заданного. Обратите внимание, как в этой функции используется return
- только для управления циклом, вместо break
. Вообще-то эта функция ничего не возвращает.
Для больших чисел эта функция будет работать довольно долго. Причем она чисто вычислительная, то есть это задача, ограниченная процессором. Этого нам и нужно. Оптимизировать ее с помощью многопоточности и модуля threading бесполезно. Давайте вызовем ее в нескольких процессах:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from multiprocessing import Process
if __name__ == '__main__':
numbers = [5, 10, 15, 20, 25]
procs = []
for number in numbers:
proc = Process(target=worker, args=(number,))
procs.append(proc)
proc.start()
for proc in procs:
proc.join()
print('Finished.')
Код этой программы уже знаком тем, кто раньше работал с потоками в Python. точно так же происходит передача аргументов в поток. Поэтому в целом данная программа повторяет шаблон “создание потоков на группы задач”.
Выводы:
- Синтаксис аналогичен созданию потока. По внутреннему устройству они сильно различаются.
- В каждый процесс загружается собственная копия интерпретатора и всех переменных.
- Поэтому глобальных переменных не будет - у каждого потока свой стек.
- Вернуть значение из процесса еще сложнее, ведь они изолированные.
- Довольно частый шаблон - использование пула потоков и функции map.
Как получить служебную информацию о процессе?
При работе с процессами иногда бывает необходимо в самой функции получить информацию о процессе, в котором она выполняется. Модуль multiprocessing предоставляет доступ к объекту current_process, через который можно получить информацию о текущем процессе. Например, имя процесса. Имя можно задать явно при создании объекта процесса, либо оно присвоится автоматически.
Еще бывает полезно проверить значение переменной __name__
, которая хранит информацию о названии текущего модуля. Но более полезная информация доступна через модуль os. Как известно, этот стандартный модуль Python позволяет получить доступ к некоторым функция операционной системы. В том числе методы получения идентификатора процесса и идентификатора родительского процесса. Рассмотрим пример:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
from multiprocessing import Process, current_process
def worker():
print('process name:'current_process().name)
print('parent process:', os.getppid())
print('process id:', os.getpid())
if __name__ == '__main__':
numbers = [5, 10, 15, 20, 25]
procs = [Process(target=worker) for number in numbers]
[proc.start() for proc in procs]
[proc.join() for proc in procs]
После запуска эта программа напечатает вывод, похожий на следующий:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
process name: Process-1
parent process: 404571
process id: 404572
process name: Process-2
parent process: 404571
process id: 404573
process name: Process-3
parent process: 404571
process id: 404574
process name: Process-4
parent process: 404571
process id: 404575
process name: Process-5
parent process: 404571
process id: 404576
Обратите внимание, что идентификаторы процессов все разные, но идентификатор родительского процесса один и тот же.
Использование замков в процессах
При использовании процессов гораздо реже возникают ситуации доступа к общим ресурсам, как в многопоточности, ведь процессы - более изолированные сущности. Но все равно такие случаи могут случаться. Например, довольно частый случай - одновременный доступ к консоли, к внешним устройствам, к сети. Все процессы, порожденные в модуле miltiprocessing будут привязаны к одному терминалу и поэтому вывод между ними может путаться.
Для решения этой проблемы в модуле так же присутствуют замки. Их назначение и применение полностью аналогично замкам их модуля threading, которые мы рассматривали ранее:
1
2
3
4
5
6
7
8
9
10
11
12
13
from multiprocessing import Process, Lock
def printer(item, lock):
with lock:
print(item)
if __name__ == '__main__':
lock = Lock()
items = ['tango', 'foxtrot', 10]
for item in items:
p = Process(target=printer, args=(item, lock))
p.start()
Зачем нужен пул процессов?
Как мы говорили ранее, процессы масштабируются гораздо хуже, чем потоки. Довольно часто нам нужно создать какое-то количество процессов, меньшее, чем количество задач. В таком случае, каждый процесс будет обрабатывать несколько задач. Мы уже рассматривали шаблон “Множественное создание потоков”. В модуле multiprocessing все гораздо проще - специально для этого существует объект Pool.
Пул процессов - это набор из определенного количества процессов, которым автоматически передаются на выполнение задачи из списка. Причем количество задач никак не связано с количеством процессов. Количество процессов задается при создании пула так:
1
pool = Pool(processes=3)
После создания, можно использовать этот пул для распараллеливания определенного количества задач. Для этого используется довольно известная функция map
. Она принимает на вход два аргумента - функцию и массив, вызывает функцию для каждого элемента массива и возвращает массив результатов:
1
pool.map(doubler, numbers)
Как бонус, пул процессов еще и заботится об обмене данными между дочерними процессами и родительским. Так что использование пула процессов сильно облегчает проектирование многопроцессных программ. Механику работы с пулом процессов можно наглядно увидеть из примера:
1
2
3
4
5
6
7
8
9
from multiprocessing import Pool
def doubler(number):
return number * 2
if __name__ == '__main__':
numbers = [5, 10, 20, 25, 30]
pool = Pool(processes=3)
print(pool.map(doubler, numbers))
Главной трудностью работы с пулом процессов является то, что он рассчитан на функцию, которая принимает один аргумент. Если функция, которую надо распараллелить, принимает несколько аргументов, существует альтернативный метод - starmap
, либо можно просто переписать ее так, чтобы она принимала кортеж.
Использование очереди
1
2
3
4
5
6
7
8
9
10
11
from multiprocessing import Process, Queue
def f(q):
q.put([42, None, 'hello'])
if __name__ == '__main__':
q = Queue()
p = Process(target=f, args=(q,))
p.start()
print(q.get()) # распечатает "[42, None, 'hello']"
p.join()
Использование конвейера
1
2
3
4
5
6
7
8
9
10
11
12
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, 'hello'])
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
p.start()
print(parent_conn.recv()) # распечатает "[42, None, 'hello']"
p.join()
Паттерн “Производитель-потребитель”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from multiprocessing import Process, Queue
sentinel = -1
def creator(data, q):
print('Creating data and putting it on the queue')
for item in data:
q.put(item)
def my_consumer(q):
while True:
data = q.get()
if data is sentinel:
break
print('data found to be processed: {}'.format(data))
processed = data * 2
print(processed)
if __name__ == '__main__':
q = Queue()
data = [5, 10, 13, -1]
process_one = Process(target=creator, args=(data, q))
process_two = Process(target=my_consumer, args=(q,))
process_one.start()
process_two.start()
q.close()
q.join_thread()
process_one.join()
process_two.join()