Системное программирование под linux что это

Программирование под Linux

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

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

1. На чем пишут программы?

Исторически сложилось так, что ядро Unix было написано на языке Си. Даже более того, этот язык был создан для написания ядра Unix. Поскольку ядро Linux было основано на ядре Minix (версии Unix), то оно тоже было написано на Си. Поэтому можно сказать, что основной язык программирования для Linux это Си и С++. Такая тенденция сохранялась на протяжении долгого времени.

А вообще, писать программы для Linux можно почти на любом языке начиная от Java и Python и заканчивая С# и даже Pascal. Для всех языков есть компиляторы и интерпретаторы. Писать программы на С++ сложно, а Си многими уже считается устаревшим, поэтому множество программистов используют другие языки для написания программ. Например, множество системных инструментов написаны на Python или Perl. Большинство программ от команды Linux Mint, установщик Ubuntu и некоторые скрипты apt написаны на Python. Множество скриптов, в том числе простые скрипты оптимизации написаны на Perl. Иногда для скриптов используется Ruby. Это скрипты OpenShift или, например, фреймворк Metasploit. Некоторые разработчики кроссплатформенных программ используют Java. Но основные компоненты системы написаны все же на Си.

Мы не будем рассматривать основы Си в этой статье. Си — сложный язык и вам понадобится прочитать как минимум одну книгу и много практиковаться чтобы его освоить. Мы рассмотрим как писать программы на Си в Linux, как их собирать и запускать.

2. Библиотеки

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

Библиотеки делятся на два типа:

  • Статические — они связываются с программой на этапе компиляции, они связываются и после этого все функции библиотеки доступны в программе как родные. Такие библиотеки имеют расширение .a;
  • Динамические — такие библиотеки встречаются намного чаще, они загружены в оперативную память, и связываются с программной динамически. Когда программе нужна какая-либо библиотека, она просто вызывает ее по известному адресу в оперативной памяти. Это позволяет экономить память. Расширение этих библиотек — .so, которое походит от Shared Object.

Таким образом, для любой программы на Си нужно подключать библиотеки, и все программы используют какие-либо библиотеки. Также важно заметить, на каком языке бы вы не надумали писать, в конечном итоге все будет сведено к системным библиотекам Си. Например, вы пишите программу на Python, используете стандартные возможности этого языка, а сам интерпретатор уже является программой на Си/С++, которая использует системные библиотеки для доступа к основным возможностям. Поэтому важно понимать как работают программы на Си. Конечно, есть языки, вроде Go, которые сразу переводятся на ассемблер, но там используются принципы те же, что и здесь. К тому же системное программирование linux, в основном, это Си или С++.

3. Процесс сборки программы

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

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

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

Далее к работе приступает компоновщик. Его задача связать объектный модуль со статическими библиотеками и другими объектными модулями. Для каждого исходного файла создается отдельный объектный модуль. Только теперь программа может быть запущена.

А теперь, давайте рассмотрим весь єтот процесс на практике с использованием компилятора GCC.

4. Как собрать программу

Для сборки программ в Linux используется два типа компиляторов, это Gcc и Clang. Пока что GCC более распространен, поэтому рассматривать мы будем именно его. Обычно, программа уже установлена в вашей системе, если же нет, вы можете выполнить для установки в Ubuntu:

Перед тем как мы перейдем к написанию и сборке программы, давайте рассмотрим синтаксис и опции компилятора:

$ gcc опции исходный_файл_1.с -o готовый_файл

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

  • -o — записать результат в файл для вывода;
  • -c — создать объектный файл;
  • -x — указать тип файла;
  • -l — загрузить статическую библиотеку.

Собственно, это все самое основное, что нам понадобится. Теперь создадим нашу первую программу. Она будет выводить строку текста на экран и чтобы было интереснее, считать квадратный корень из числа 9. Вот исходный код:

printf(«Корень: %fn», sqrt(9));

Я специально добавил функцию корня чтобы показать как работать с библиотеками. Сначала нужно собрать объектный файл. Перейдите в папку с исходниками и выполните:

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

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

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

Это две библиотеки загрузчика, стандартная libc и libm, которую мы подключили.

5. Автоматизация сборки

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

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

gcc -lm program.o -o program

gcc -c program.c -o program.o

Затем вам достаточно выполнить команду make для запуска компиляции, только не забудьте удалить предыдущие временные файлы и собранную программу:

Программа снова готова и вы можете ее запустить.

Выводы

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

Курс программирования на Си под Linux:

Источник

Системное программирование под linux что это

Введение и важнейшие концепции

Эта книга о системном программировании, то есть об искусстве написания системного программного обеспечения. Системное программное обеспечение живет на низком уровне, общаясь напрямую с ядром и системными библиотеками. Системное программное обеспечение включает в себя оболочку и текстовый редактор, компилятор и отладчик, утилиты ядра и системные демоны. Все эти компоненты работают с кодом ядра и библиотеками C. Большая часть остального программного обеспечения (например, высокоуровневые приложения с графическим интерфейсом пользователя) живет на высоком уровне, ныряя на низкий уровень лишь изредка или вообще не касаясь его. Одни программисты каждый день все время проводят за написанием системного программного обеспечения; другие тратят на эту задачу лишь часть своего рабочего дня. Но нет такого программиста, которому во вред были бы знания о системном программировании. Будь это raison d’кtre программиста

или просто основание для самоутверждения, системное программирование находится в сердце всего программного обеспечения, которое мы пишем.

В частности, эта книга посвящена системному программированию в Linux. Linux — это современная Unix-подобная система, написанная Линусом Торвальдсом (Linus Torvalds) и сообществом хакеров со всего света. Хотя Linux разделяет цели и идеологию Unix, Linux — это не Unix. Linux следует по собственному пути, отклоняясь где удобно и присоединяясь, когда этого требуют вопросы практичности. В целом, суть системного программирования в Linux та же, что и в любой другой Unix-системе. Но если выйти за границы основ, то Linux много делает для того, чтобы отличаться. По сравнению с традиционными Unix-системами Linux изобилует дополнительными системными вызовами, непохожим поведением и новыми возможностями.

Говоря традиционно, все программирование в Unix — это программирование системного уровня. Исторически системы Unix включали в себя совсем немного высокоуровневых абстракций. Даже программирование в среде для разработки, такой как X Window System, в полной мере обнажало корневой API (Application Programming Interface, интерфейс прикладного программирования) системы Unix. Следовательно, можно сказать, что эта книга рассказывает о программировании в Linux в целом. Но обратите внимание, что в этой книге не говорится о среде программирования Linux — вы не найдете руководства по использованию команды make на этих страницах. Речь пойдет об API системного программирования на современной машине под управлением Linux.

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

В последние годы наблюдается такая тенденция, что прикладное программирование отходит от программирования на системном уровне и превращается в высокоуровневую разработку с использованием либо веб-приложений (таких, как JavaScript и PHP), либо управляемого кода (например, на C# или Java). Такая разработка, однако, не предвещает смерть системного программирования. Действительно, кому-то же нужно писать интерпретатор JavaScript и среду исполнения C#, а это тоже программирование. Помимо этого, разработчики, пишущие на PHP или Java, также могут извлекать выгоду из знания системного программирования, так как понимание базовых элементов позволяет писать лучший код, неважно, на каком уровне он создается.

Читайте также:  Window manager для линукс

Несмотря на эту тенденцию в прикладном программировании, большая часть кода для Unix и Linux все так же пишется на системном уровне. Обычно это код на языке C, существующий в основном на базе интерфейсов, предоставляемых библиотекой C и ядром. Эти традиционные инструменты: Apache, bash, cp, Emacs, init, gcc, gdb, glibc, ls, mv, vim и X — совершенно не планируют умирать в скором времени.

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

Что такое интерфейс системного уровня, и как писать приложения системного уровня в Linux? Что именно предоставляют ядро и библиотека C? Как писать оптимальный код, и какие трюки скрываются в Linux? Какие искусные системные вызовы есть в Linux, которых нет в других вариантах Unix? Как все это работает? Вот на какие вопросы пытается ответить эта книга.

Существует три краеугольных камня, на которых держится системное программирование в Linux: системные вызовы, библиотека C и компилятор C. Каждый из них заслуживает официального представления.

Системное программирование начинается с системных вызовов. Системные вызовы — это обращения к функциям, которые делаются из пользовательского пространства — текстового редактора, вашей любимой игры и т. д. — в ядро (главный внутренний орган системы) для того, чтобы запросить у операционной системы какую-либо службу или ресурс. Системные вызовы могут быть как распространенными и часто используемыми, такими как read() и write(), так и экзотическими, как get_thread_area() или set_tid_address().

В Linux реализовано намного меньше системных вызовов, чем в ядрах большинства других операционных систем. Например, число системных вызовов в архитектуре i386 приближается к 300, а в Microsoft Windows, по слухам, их тысячи. В ядре Linux для каждой из архитектур (таких, как Alpha, i386 или PowerPC) реализован собственный список доступных системных вызовов. Следовательно, системные вызовы, имеющиеся в одной архитектуре, могут отличаться от списка системных вызовов в другой архитектуре. Тем не менее очень большой поднабор системных вызовов — более 90 % — реализован одинаково во всех архитектурах. В данной книге я рассматриваю именно этот общий поднабор — наиболее распространенные интерфейсы.

Выполнение системных вызовов

Невозможно напрямую связать приложения пользовательского пространства с пространством ядра. По причинам, относящимся к безопасности и надежности, приложения пользовательского пространства не могут непосредственно выполнять код ядра или манипулировать данными ядра. Вместо этого предоставляется механизм, при помощи которого приложение пользовательского пространства может «сигнализировать» ядру, что ему необходимо выполнить системный вызов. Приложение может отправить прерывание ядру и исполнить только тот код, к которому ему разрешает обращаться ядро. Конкретная реализация механизма варьируется в зависимости от архитектуры. В архитектуре i386, например, приложение пользовательского пространства исполняет инструкцию программного прерывания int 0x80. Эта инструкция порождает переход в пространство ядра — защищенную область ядра, где ядро исполняет обработчик программного прерывания. А что такое обработчик для прерывания 0x80? Не что иное, как обработчик системного вызова!

Приложение сообщает ядру, какой системный вызов нужно выполнить и с какими параметрами, используя аппаратные регистры (machine register). Системные вызовы обозначаются номерами начиная с нуля (0). В архитектуре i386, для того чтобы запросить системный вызов 5 (то есть вызов open()), приложение пользовательского пространства помещает значение 5 в регистр eax и только после этого исполняет инструкцию int.

Передача параметров обрабатывается схожим образом. В архитектуре i386, например, для каждого из возможных параметров используется отдельный регистр: регистры ebx, ecx, edx, esi и edi содержат, в указанном порядке, первые пять параметров. В редких случаях, когда системный вызов включает в себя более пяти параметров, используется один регистр, который указывает на буфер в пользовательском пространстве, где хранятся все параметры вызова. Конечно же, большинство системных вызовов выполняются всего лишь с парой параметров.

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

Библиотека C (libc) находится в самом сердце приложений Unix. Даже когда вы программируете на другом языке, библиотека C, вероятнее всего, все равно принимает участие в работе, обернутая более высокоуровневыми библиотеками, и предоставляет корневые службы, упрощая выполнение системных вызовов. В современных системах Linux библиотекаC—это GNU libc, сокращенно glibc, что произносится как джи-либ-си (gee-lib-see) или, реже, глиб-си (glib-see).

В библиотеке GNU C можно найти намного больше, чем подразумевает ее имя. Помимо реализации стандартной библиотеки C, glibc предоставляет обертки для системных вызовов, поддержку многопоточной обработки и базовые возможности приложений.

В Linux стандартный компилятор C предоставляется в форме коллекции компиляторов GNU (GNU Compiler Collection, gcc). Первоначально gcc был версией cc, компилятора C (C Compiler — cc), разработанной в проекте GNU. Таким образом, gcc расшифровывается как GNU C Compiler. Однако со временем была добавлена поддержка многих других языков. Поэтому сегодня gcc используется в качестве общего имени для семейства компиляторов GNU. Однако gcc — это также двоичный файл, при помощи которого вызывается компилятор C. В этой книге, говоря о gcc, обычно я имею в виду программу gcc, если контекст не подразумевает иное.

Компилятор, используемый в системах Unix, в том числе в Linux, тесно связан с системным программированием, так как компилятор помогает реализовывать стандарт C (см. раздел «Стандарты языка C») и системный интерфейс ABI (см. раздел «Интерфейсы API и ABI»), о которых рассказывается далее в этой главе.

Интерфейсы API и ABI

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

На системном уровне существует два отдельных набора определений и описаний, влияющих на переносимость программ. Первый из них — это интерфейс прикладного программирования (Application Programming Interface, API), а второй — двоичный интерфейс приложений (Application Binary Interface, ABI). Оба определяют и описывают интерфейсы между различными частями компьютерного программного обеспечения.

В интерфейсе API определяются способы, при помощи которых один фрагмент программного обеспечения общается с другим на уровне исходных текстов. Он предоставляет абстракцию в виде стандартного набора интерфейсов — обычно функций, которые какая-то часть программного обеспечения (обычно, хотя и не всегда, высокоуровневая) может вызывать из другой части программного обеспечения (обычно низкоуровневой). Например, API может абстрагировать концепцию вывода текста на экран при помощи семейства функций, обеспечивающих все необходимое для отображения текста. В API просто определяется интерфейс. Фрагмент программного обеспечения, который фактически предоставляет интерфейс API, называется реализацией API (implementation).

Очень часто API называют «контрактом». Это не совсем верно, по крайней мере в юридическом смысле термина, так как API — это не двухстороннее соглашение. Пользователь API (обычно высокоуровневое программное обеспечение) не может ничего внести в API и его реализацию. Он может использовать API в том виде, в котором интерфейс существует, или же вообще не использовать его — третьего не дано! API всего лишь гарантирует, что если обе части программного обеспечения будут удовлетворять требованиям API, то они будут совместимы на уровне исходного кода (source compatible). Это означает, что приложение-пользователь API будет успешно компилироваться с данной реализацией API.

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

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

Тогда как API определяет исходный интерфейс, ABI определяет низкоуровневый двоичный интерфейс между двумя или несколькими частями программного обеспечения в конкретной архитектуре. Интерфейс ABI формулирует, как приложение взаимодействует с самим собой, как приложение взаимодействует с ядром и как приложение взаимодействует с библиотекой. ABI обеспечивает совместимость на двоичном уровне (binary compatibility), то есть гарантирует, что фрагмент конечной программы будет функционировать в любой системе, где имеется тот же ABI, не требуя перекомпиляции.

Интерфейсы ABI связаны с такими вещами, как соглашения о вызовах, порядок следования байтов, использование регистров, выполнение системных вызовов, компоновка, поведение библиотек и формат двоичных объектов. Например, соглашения о вызовах определяют способы вызова функций, способ передачи аргументов, какие регистры сохраняются, а какие искажаются и как вызывающий код получает возвращаемое значение.

Хотя было предпринято несколько попыток определить единый ABI для одной архитектуры, охватывающей несколько операционных систем (в частности, для i386 в системах Unix), усилия не увенчались особым успехом. Наоборот, для всех операционных систем, в том числе Linux, обычно определяются собственные интерфейсы ABI. Любой ABI тесно привязан к архитектуре; большую часть ABI составляют машинно-ориентированные концепции, такие, как назначение определенных регистров или инструкции сборки. Таким образом, для каждой машинной архитектуры существует собственный интерфейс ABI для Linux. В действительности, мы обычно называем отдельные ABI по их машинным именам, например alpha или x86-64.

Системный программист должен быть знаком с ABI, но обычно необходимости запоминать подробности не возникает. ABI реализуется цепочкой инструментов (toolchain) — компилятором, компоновщиком и т. д. — и обычно не выходит на поверхность никакими другими способами. Знание ABI, однако, может помочь научиться создавать оптимальные программы и необходимо для написания кода сборки или для того, чтобы влезть в саму цепочку инструментов (что также относится к системному программированию).

Описание ABI для любой конкретной архитектуры в Linux можно найти в Интернете; этот интерфейс реализуется цепочкой инструментов соответствующей архитектуры и ядром.

Системное программирование в Unix — это древнее искусство. Основы программирования в Unix остаются неприкосновенными вот уже десятилетия. Системы Unix, однако, весьма динамичны, и их поведение меняется с добавлением новых функций. Для того чтобы внести свой вклад в процесс превращения порядка в хаос, группы стандартов объединяются в официальные стандарты, включающие в себя варианты системных интерфейсов. Существует множество подобных стандартов, но, технически говоря, Linux официально не удовлетворяет ни одному из них. Однако система Linux нацелена на соответствие двум наиболее важным и распространенным стандартам: POSIX и Single UNIX Specification (SUS — единая спецификация Unix).

Стандарты POSIX и SUS документируют, среди прочего, API языка C для интерфейса Unix-подобной операционной системы. Фактически, они определяют системное программирование или, по крайней мере, самую общеупотребительную его часть для совместимых систем Unix.

Читайте также:  Командные файлы mac os

История POSIX и SUS

В середине 80-х годов XX в. Институт инженеров по электротехнике и радиоэлектронике (Institute of Electrical and Electronics Engineers, IEEE) возглавил усилия по стандартизации интерфейсов системного уровня в системах Unix. Ричард Столлман (Richard Stallman), основатель движения Free Software (Свободное программное обеспечение), предложил для стандарта название POSIX (произносится как «пазикс» (pahzicks)), которое сегодня расшифровывается как Portable Operating System Interface (Интерфейс переносимых операционных систем).

Первым результатом этих усилий, появившимся на свет в 1988 году, стал стандарт IEEE Std 1003.1–1988 (короче — POSIX 1988). В 1990 году IEEE пересмотрел стандарт POSIX, выпустив стандарт IEEE Std 1003.1–1990 (POSIX 1990). Необязательная поддержка обработки в реальном времени и поточной обработки была документирована в стандартах IEEE Std 1003.1b–1993 (POSIX 1993 или POSIX.1b) и IEEE Std 1003.1c–1995 (POSIX 1995 или POSIX.1c) соответственно. В 2001 году необязательные стандарты были объединены с базовым стандартом POSIX 1990, благодаря чему родился единый стандарт IEEE Std 1003.1–2001 (POSIX 2001). Последняя версия, выпущенная в апреле 2004 года, носит название IEEE Std 1003.1-2004. Все ключевые стандарты POSIX обозначаются аббревиатурой POSIX.1, и последняя версия датирована 2004 годом.

В конце 80-х и начале 90-х годов производители систем Unix боролись друг с другом в так называемых «Юниксовых войнах», в которых каждый стремился к тому, чтобы его вариант Unix был признан настоящей операционной системой Unix. Несколько основных производителей Unix объединились вокруг Open Group — промышленного консорциума, который был сформирован путем объединения Open Software Foundation (OSF) и X/Open. Консорциум Open Group обеспечивал сертификацию, официальные документы и проверку соответствия. В начале 90-х годов, когда «Юниксовые войны» были в самом разгаре, Open Group выпустила единую спецификацию Unix — Single UNIX Specification. SUS быстро завоевала популярность, большей частью благодаря своей цене (она бесплатная), в противоположность высокой стоимости стандарта POSIX. Сегодня SUS включает в себя новейший стандарт POSIX.

Первая версия SUS была опубликована в 1994 году. Системы, соответствующие спецификации SUSv1, маркируются как UNIX 95. Вторая версия SUS вышла в 1997 году, и совместимые системы маркируются как UNIX 98. Третья, и последняя, версия SUS, SUSv3, была опубликована в 2002 году. Совместимые системы маркируются как UNIX 03. В SUSv3 были выполнены пересмотр и объединение стандарта IEEE Std 1003.1–2001 и нескольких других стандартов. В книге я буду указывать, стандартизированы ли системные вызовы и другие интерфейсы по POSIX. Я упоминаю POSIX, а не SUS потому, что POSIX входит в состав SUS.

Стандарты языка C

Прославленная книга Дениса Ричи и Брайана Кернигана (Dennis Ritchie, Brian Kernighan) «The C Programming Language» (издательство Prentice Hall) многие годы после публикации в 1978 году служила неформальной спецификацией языка C. Эта версия С заслужила прозвище K&R C. Язык C уже в те времена быстро отвоевывал у BASIC и других языков звание lingua franca микрокомпьютерного программирования.

Поэтому, чтобы стандартизировать уже заслуживший популярность язык, в 1983 году Американский национальный институт стандартов (American National Standards Institute, ANSI) сформировал комитет, задачей которого было разработать официальную версию C, включающую возможности и усовершенствования, предложенные разнообразными производителями, а также новый язык C++. Процесс был длинным и трудоемким, но стандарт ANSI C был завершен в 1989 году. В 1990 году Международная организация по стандартизации (International Standardization Organization, ISO) ратифицировала стандарт ISO C90, основанный на ANSI C и включающий несколько изменений.

В 1995 году ISO выпустила обновленную (хотя и редко реализуемую) версию языка C, ISO C95. В 1999 году последовало крупное обновление языка — ISO C99, в ходе которого было добавлено множество новых возможностей, включая подстановку функций, новые типы данных, массивы переменной длины, комментарии в стиле C++ и новые библиотечные функции.

Linux и стандарты

Как уже говорилось ранее, операционная система Linux нацелена на соответствие стандартам POSIX и SUS. Она предоставляет интерфейсы, документированные в SUSv3 и POSIX.1, включая необязательную поддержку обработки в реальном времени (POSIX.1b) и необязательную поддержку поточной обработки (POSIX.1c). Еще важнее то, что Linux пытается обеспечить функционирование в соответствии с требованиями POSIX и SUS. В целом, несоблюдение стандартов считается ошибкой. Считается, что Linux соответствует POSIX.1 и SUSv3, но официальная сертификация POSIX или SUS не проводилась (отдельно для каждой версии Linux), поэтому я не могу утверждать, что Linux официально отвечает требованиям POSIX или SUS.

Что касается стандартов языков, то в Linux все хорошо. Компилятор C gcc поддерживает стандарт ISO C99. Помимо этого, gcc предоставляет множество собственных расширений языка C. Эти расширения носят коллективное название GNU C и документированы в приложении к книге.

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

Среди разнообразных дистрибутивов Linux большую часть систем Linux стандартизирует Linux Standard Base (LSB). LSB — это совместный проект нескольких производителей Linux под покровительством консорциума Linux Foundation (ранее известного как Free Standards Group). Проект LSB расширяет спецификации POSIX и SUS и добавляет несколько собственных стандартов; он нацелен на стандартизацию двоичного кода, чтобы конечные программы могли выполняться на совместимых системах без необходимости вносить изменения. Большинство производителей Linux соблюдают требования LSB в той или иной степени.

Эта книга и стандарты

В этой книге я намеренно стараюсь избегать выражения преданности любым стандартам. Слишком часто встречаются книги о системном программировании в Unix, в которых в деталях рассказывается, как интерфейс ведет себя в одном стандарте по сравнению с другим, реализован ли определенный системный вызов в этой системе и в той, в общем, страницы заполнены пустой болтовней. Эта книга посвящена системному программированию в современной системе Linux, которая обеспечивается новейшими версиями ядра Linux (2.6), компилятором C gcc (4.2) и библиотекой C (2.5).

Поскольку системные интерфейсы в целом определены раз и навсегда — например, разработчики ядра Linux проходят через огромные муки, чтобы никогда не нарушать интерфейсы системных вызовов — и обеспечивают определенный уровень совместимости на уровне источников и на двоичном уровне, этот подход позволяет мне глубже погрузиться в детали системного интерфейса Linux, не ограничивая себя вопросами совместимости со множеством других систем и стандартов Unix. Благодаря такому фокусу с Linux я также могу в этой книге предложить всестороннее рассмотрение самых современных уникальных для Linux интерфейсов, которые будут оставаться актуальными еще очень долго. Эта книга основывается на близком знакомстве с Linux и, в частности, знании реализации и поведения таких компонентов, как gcc и ядро, что дает мне возможность демонстрировать вам взгляд изнутри и делиться, как опытному ветерану, лучшими практическими советами по оптимизации.

Концепции программирования в Linux

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

Я предполагаю, что вы знакомы со средой Linux. Вы должны уметь ориентироваться в оболочке, использовать базовые команды и компилировать простые программы на C. Это не обзор операционной системы Linux или ее среды программирования, а, скорее, перечисление того, что формирует основу системного программирования в Linux.

Файлы и файловая система

Файл — это самая простая и фундаментальная абстракция в Linux. Linux придерживается философии «все есть файл» (хотя и не так строго, как некоторые другие системы, такие, как Plan9).

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

Для того чтобы обратиться к файлу, его сначала нужно открыть. Файл можно открыть для чтения, записи или для чтения и записи одновременно. Обращение к открытому файлу осуществляется с использованием уникального дескриптора, обеспечивающего отображение метаданных, связанных с открытым файлом, на сам файл. Внутри ядра Linux этот дескриптор обрабатывается при помощи целочисленного значения (типа int в C), называемого дескриптором файла (file descriptor) или сокращенно fd. Файловые дескрипторы совместно используются пользовательским пространством и ядром, а пользовательские программы применяют их, чтобы напрямую обращаться к файлам. Большая доля системного программирования в Linux состоит из открытия, манипулирования, закрытия и использования дескрипторов файлов различными способами.

То, что большинство из нас называют «файлами», в Linux носит название обычного файла (regular file). Обычный файл содержит байты данных, организованные в линейный массив, который называется потоком байтов. В Linux для файла не определяется никакая дополнительная организация или формат. Байты могут иметь любые значения и следовать внутри файла в любом порядке. На системном уровне Linux не требует никакой структуризации от файлов, за исключением потока байтов. Некоторые операционные системы, например VMS, допускают только структурированные файлы, поддерживающие такие концепции, как записи (record). В Linux это не так.

Любой байт из файла можно считать и в любой байт в файле можно записать значение. Выполнение операций начинается на определенном байте, представляющем умозрительное «местоположение» внутри файла. Это местоположение называется позицией в файле (file position) или смещением в файле (file offset). Позиция в файле — это ценный фрагмент метаданных, которые ядро ассоциирует с каждым открытым файлом. Когда файл открывается впервые, значение позиции в файле равно нулю. Обычно, по мере того как файл побайтово считывается или записывается, позиция в файле, так или иначе, увеличивается. Значение позиции в файле также можно установить вручную, причем можно даже выбрать значение, находящееся дальше конца файла. Если записать байт в позицию, которая находится дальше конца файла, то промежуточные байты будут заполнены нулями. Хотя этот способ позволяет записывать байты дальше конца файла, невозможно таким же образом записывать байты в позициях, предшествующих началу файла. Это звучит бессмысленно, и, действительно, от этой практики было бы мало пользы. Значения позиции в файле начинаются с нуля, позиция не может быть отрицательной. При записи в байт в середине файла значение, которое ранее находилось по этому смещению, заменяется новым. Поэтому невозможно увеличить файл, записав что-то внутрь него. Обычно запись в файл начинают в конце файла. Максимальное значение позиции в файле ограничивается только размером типа данных C, который используется для ее хранения, и в современных системах Linux представляет 64-битное целое.

Размер файла измеряется количеством байтов и называется длиной файла (file length). Другими словами, длина — это просто число байтов в линейном массиве, составляющем файл. Длину файла можно изменить при помощи операции, называемой усечением (truncation). Файл можно усечь до размера, меньшего его первоначальной длины, причем байты удаляются в конце файла. Хотя это звучит странно, учитывая название оператора, файл также можно «усечь» до размера, большего его первоначальной длины. В этом случае новые байты (добавляемые в конце файла) заполняются нулями. Файл может быть пустым (иметь длину, равную нулю) и вообще не содержать значимых байтов. Максимальная длина файла, как и максимальное значение позиции в файле, ограничивается только пределами типов C, которые ядро Linux применяет для управления файлами. Конкретные файловые системы, однако, могут накладывать собственные ограничения, не допуская создания файлов размером больше определенного значения.

Читайте также:  Что будет если активировать windows 10 oem ключом

Один и тот же файл может быть открыт более одного раза, причем другим или тем же самым процессом. Каждому открытому экземпляру файла присваивается уникальный дескриптор файла; несколько процессов могут совместно использовать один дескриптор. Ядро не накладывает никаких ограничений на одновременный доступ к файлу. Несколько процессов могут свободно считывать и записывать один и тот же файл в одно и то же время. В результате этого при одновременном доступе процессам приходится полагаться на упорядочение отдельных операций, что обычно непредсказуемо. Программам из пользовательского пространства нужно координировать деятельность между собой, чтобы гарантировать, что одновременные обращения к файлу будут в достаточной степени синхронизированы.

Хотя доступ к файлам обычно осуществляется при помощи имен файлов (filename), в действительности файлы напрямую не связываются со своими именами. Вместо этого обращение к файлу производится через структуру inode (первоначально information node, информационный узел), которой присваивается уникальное числовое значение. Это значение называется номером inode (inode number); и чаще всего его название сокращают до inumber или ino. В inode хранятся связанные с файлом метаданные, такие, как его временная метка, указывающая момент последней модификации, его владелец, тип, длина и местоположение данных файла — но не имя файла! Inode — это и физический объект, который можно обнаружить в Unix-подобных файловых системах, и умозрительная сущность, представляемая структурой данных в ядре Linux.

Каталоги и ссылки

Обращаться к файлу при помощи его номера inode затруднительно (и это потенциальная брешь в безопасности), поэтому из пользовательского пространства файлы всегда открываются по имени, а не по номеру inode. Каталоги (directory) используются для определения имен, по которым выполняется обращение к файлам. Каталог играет роль отображения между удобными для человека именами и номерами inode. Пара из имени и номера inode называется ссылкой (link). Физическая форма этого отображения на диске — простая таблица, хэш или что-либо еще — реализуется и управляется кодом ядра, который поддерживает данную файловую систему. По существу, каталог — это обычный файл, но его отличие от обычного файла состоит в том, что он содержит только карту отображения имен в номера inode. Ядро напрямую использует эти отображения для разрешения имен в номера inode.

Когда приложение из пользовательского пространства запрашивает открытие определенного имени файла, ядро открывает каталог, в котором хранится данное имя файла, и ищет указанное имя. Исходя из имени файла, ядро получает номер inode. Inode содержит метаданные, связанные с файлом, включая местоположение данных файла на диске.

Первоначально на диске есть только один каталог, корневой каталог (root directory). Этот каталог обычно обозначается путем /. Но, как мы все знаем, чаще всего в системе хранится много каталогов. Так как же ядро узнает, в какой каталог нужно заглянуть, чтобы найти определенное имя файла?

Как я уже сказал выше, каталоги очень похожи на обычные файлы. И с ними также связаны структуры inode. Следовательно, ссылки внутри каталогов могут указывать на inode других каталогов. Это означает, что каталоги могут вкладываться друг в друга, формируя иерархию каталогов, что, в свою очередь, позволяет использовать полные пути (pathname), с которыми знакомы все пользователи Unix, например: /home/blackbeard/landscaping.txt.

Когда ядро получает запрос на открытие такого полного пути, оно проходит по всем записям каталога (directory entry; внутри ядра — dentry) в пути, каждый раз находя inode следующей записи. В предыдущем примере ядро начинает просмотр с каталога /, получает inode для каталога home, переходит туда, получает inode для blackbeard, переходит в этот каталог и, наконец, получает inode для landscaping.txt. Эта операция называется разрешением каталога, или разрешением полного пути (directory, или pathname resolution). Ядро Linux также использует кэш, называемый кэшем dentry (dentry cache), для хранения результатов разрешения каталогов, что обеспечивает более быстрый поиск в будущем, с учетом сосредоточенности во времени.

Полный путь, начинающийся с корневого каталога, является полностью уточненным (fully qualified) и называется абсолютным полным путем. Некоторые полные пути не полностью уточнены; они указываются по отношению к какому-то другому каталогу (например, todo/plunder). Такие пути называются относительными полными путями. Если ядру предоставляется относительный полный путь, то разрешение его начинается с текущего рабочего каталога (current working directory). В текущем рабочем каталоге ядро ищет каталог todo. Попав туда, ядро получает inode для каталога plunder.

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

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

По существу, ничто из вышеописанного не запрещает разрешать несколько имен в один и тот же номер inode. И это действительно разрешено. Когда несколько ссылок отображают разные имена на одну и ту же структуру inode, они называются жесткими ссылками (hard link).

Жесткие ссылки позволяют создавать в файловой системе сложные структуры, в которых несколько полных имен указывают на одни и те же данные. Жесткие ссылки могут находиться в одном каталоге или в нескольких разных каталогах. В любом случае ядро просто разрешает полный путь в правильную inode. Например, две жесткие ссылки — /home/bluebeard/map.txt и /home/black-beard/treasure.txt — могут разрешать определенный номер inode в один и тот же фрагмент данных на диске.

При удалении файла выполняется отсоединение его от структуры каталогов, и делается это всего лишь путем удаления из каталога пары из имени и inode, соответствующей этому файлу. Так как Linux поддерживает жесткие ссылки, файловая система не может разрушать inode и связанные данные при каждой операции отсоединения. Что, если где-то в файловой системе существует другая жесткая ссылка? Для того чтобы гарантировать, что файл не будет разрушен до тех пор, пока все ссылки на него не будут удалены, каждая структура inode содержит счетчик ссылок (link count), позволяющий отслеживать в системе число ссылок, указывающих на нее. Когда полное имя отсоединяется, счетчик ссылок уменьшается на единицу; только когда он достигает нуля, inode и связанные данные фактически удаляются из файловой системы.

Жесткие ссылки не могут охватывать разные файловые системы, потому что номер inode не имеет никакого смысла за пределами собственной файловой системы. В системах Unix также реализованы символические ссылки (symbolic link, название которых часто сокращается до symlink), которые могут охватывать разные файловые системы, которые проще сами по себе и менее прозрачны.

Символические ссылки выглядят как обычные файлы. У символической ссылки есть собственная структура inode и какие-то данные, содержащие полный путь к файлу, на который эта ссылка указывает. Это означает, что символические ссылки могут указывать куда угодно, в том числе на файлы и каталоги в других файловых системах, и даже на файлы и каталоги, которых вообще не существует. Символическая ссылка, указывающая на несуществующий файл, называется сломанной ссылкой (broken link).

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

Символические ссылки не так прозрачны, как жесткие ссылки. Использование жестких ссылок абсолютно прозрачно; в действительности, нужно только выяснить, что файл привязан более одного раза. Манипулирование символическими ссылками, с другой стороны, требует особых системных вызовов. Такое отсутствие прозрачности зачастую считается позитивным фактором, а символические ссылки выступают в роли скорее ярлыков (shortcut), нежели внутренних ссылок файловой системы.

Специальные файлы (special file) — это объекты ядра, которые представляются как файлы. За время своего существования системы Unix поддерживали несколько различных видов специальных файлов. Linux поддерживает четыре:

  1. файлы блочных устройств;
  2. файлы устройств посимвольного ввода-вывода;
  3. именованные конвейеры;
  4. и доменные сокеты Unix.

Специальные файлы позволяют определенным абстракциям встраиваться в файловую систему, внося вклад в парадигму «все есть файл». В Linux предусмотрен системный вызов для создания специального файла.

Доступ к устройствам в системах Unix осуществляется через файлы устройств, которые ведут себя и выглядят как обычные файлы, находящиеся в файловой системе. Файлы устройств можно открывать, считывать из них данные и записывать данные в них, и именно таким способом пользовательское пространство обращается с устройствами (физическими и виртуальными) в системе и манипулирует ими. Устройства Unix обычно подразделяют на две группы: блочные устройства (block device) и устройства посимвольного ввода-вывода (character device). У каждого типа устройств — собственный специальный файл устройства.

Доступ к устройству посимвольного ввода-вывода осуществляется как к линейной очереди байтов. Драйвер устройства помещает байты в одну очередь, один за другим, а пользовательское устройство считывает байты в том порядке, в котором они были в очередь поставлены. Пример устройства посимвольного ввода-вывода — клавиатура. Например, если пользователь вводит «peg», то приложение считает с устройства клавиатуры символ p, потом e и, наконец, g. Когда больше символов для считывания не остается, устройство возвращает код конца файла (end-of-file, EOF). Пропуск символов или считывание их в любом другом порядке не имели бы никакого смысла. Для доступа к таким устройствам вывода используются файлы устройств посимвольного ввода-вывода.

К блочному устройству, в противоположность этому, обращаются как к массиву байтов. Драйвер устройства сопоставляет байты на диске с устройством с возможностью поиска, а пользовательское пространство может обращаться к любым значимым байтам в массиве в любом порядке: оно может считать байт 12, затем байт 7, а затем снова байт 12. Блочные устройства — это обычно устройства хранения данных. Жесткие диски, дисководы гибких дисков, приводы компакт-дисков и flash-память — все это примеры блочных устройств. Доступ к ним осуществляется через файлы блочных устройств.

Именованные конвейеры (named pipe, зачастую называемые FIFO — first in, first out — первым вошел, первым вышел) — это механизм взаимодействия процессов (interprocess communication, IPC), обеспечивающий коммуникационный канал через файловый дескриптор, доступ к которому выполняется через специальный файл. Обычные конвейеры используются для «перекачки» вывода одной программы на вход другой; они создаются в памяти при помощи системного вызова и не присутствуют ни в каких файловых системах. Именованные конвейеры функционируют как обычные конвейеры, но доступ к ним осуществляется через файл, называемый специальным файлом FIFO (FIFO special file). Не связанные между собой процессы могут обращаться к этому файлу и общаться между собой.

Последний тип специальных файлов — сокеты (socket). Сокеты представляют собой расширенную форму IPC, обеспечивающую коммуникацию между двумя различными процессами, причем не только на одной машине, но и на двух разных машинах. Фактически, сокеты формируют базис сетевого программирования и программирования для Интернета. Существует множество разновидностей сокетов, включая доменные сокеты Unix — эта форма сокетов используется для коммуникации в пределах локальной машины. Сокеты, обменивающиеся данными через Интернет, могут использовать для идентификации цели пару из имени компьютера и порта, а доменные сокеты Unix используют специальный файл, находящийся в файловой системе, обычно называемый просто файлом сокета.

Источник

Оцените статью