2.3 Пользовательское пространство |
Предыдущая Содержание Следующая |
Пользовательское пространство на Linux основывается на следующих принципах.
▪Программа: это образ приложения. Он находится в файловой системе. Когда приложение необходимо запустить, образ загружается в память и начинает работать. Обратите внимание, что из-за виртуальной памяти весь образ процесса в память не загружается, а будут загружены только необходимые страницы памяти. ▪Виртуальная память: она позволяет каждому процессу иметь своё собственное адресное пространство. Виртуальная память позволяет иметь дополнительные возможности, такие как разделяемые библиотеки. Каждый процесс имеет свою собственную карту памяти в виртуальном адресном пространстве; она является уникальной для каждого процесса и совершенно не зависит от карты памяти ядра. ▪Системные вызовы: это точки входа в ядро, так что ядро может выполнять работу от имени приложения.
Чтобы понять, как в Linux работает приложение, рассмотрим небольшой пример. Допустим, что следующему фрагменту кода необходимо запускать приложение на устройстве на базе MIPS.
#include <stdio.h> char str[] = “hello world”; void myfunc() { printf(str); } main() { myfunc(); sleep(10); }
Чтобы сделать это, необходимы следующие шаги:
1. Компиляция и сборка исполняемой программы: программы для встроенной системы не собираются на целевой платформе, а требуют систему со средствами кросс-платформенной разработки. Подробнее это будет обсуждаться в Разделе 2.5; на данный момент предположим, что у нас есть базовый компьютер и инструменты для создания приложения, которое называется hello_world. 2. Загрузка исполняемой программы в файловую систему целевой платы: процесс создания корневой файловой системы и загрузку приложений на целевую платформу рассматривает Глава 8. Сейчас предположим, что этот шаг является доступным для вас; некоторым образом вы сможете загрузить hello_world в каталог /bin корневой файловой системы. 3. Выполнение программы через запуск в оболочке: оболочка является интерпретатором командного языка; она может быть использована для выполнения файлов. Не вдаваясь в подробности работы оболочки, предположим, что при вводе команды /bin/hello_world, ваша программа работает, и вы видите строку в консоли (которая, как правило, последовательный порт).
Для платформы MIPS для создания исполняемого файла используется следующая команда:
#mips_fp_le-gcc hello_world.c -o hello_world #ls -l hello_world -rwxrwxr-x 1 raghav raghav 11782 Jul 20 13:02 hello_world
Это происходит за четыре шага: сначала идёт обработка препроцессором, потом генерируется выходная сборка языка, за которой следует генерация объектного вывода, а затем последний этап - компоновка. Выходной файл hello_world является исполняемым файлом MIPS в формате, называемом ELF (Executable Linkage Format, формат исполняемой сборки). Все исполняемые файлы имеют два формата: двоичный формат и файлы сценариев (скриптов). Исполняемыми двоичными форматами, которые наиболее популярны на встраиваемых системах, являются COFF, ELF и простой (flat) формат. Формат FLAT используется в системах без MMU на uClinux и обсуждается в Главе 10. COFF первоначально был форматом по умолчанию и был заменён на более мощный и гибкий формат ELF. Формат ELF состоит из заголовка, за которым следуют многие разделы, включая текст и данные. Чтобы найти список символов в исполняемом файле, можно использовать команду nm, как показано в Распечатке 2.1. Как вы можете видеть, функциям main и myfunc, а также глобальным данным, str, были присвоены адреса, но функция printf не определена (указывается как "U") и определяется как printf@@GLIBC. Это означает, что printf - не часть образа hello_world. Тогда где определена эта функция и как определяются эти адреса? Эта функция является частью библиотеки libc (библиотеки языка Си). Libc содержит список наиболее часто используемых функций. Например, функция printf используется почти во всех приложениях. Таким образом, вместо того, чтобы находиться в каждом образе приложения, её заменителем становится библиотека. Если библиотека используется как общая библиотека, это не только оптимизирует место для хранения, но также оптимизирует расход памяти, делая так, что в памяти находится только одна копия текста. Приложение может иметь несколько библиотек, общих или статических; это может быть указано во время компоновки. Список зависимостей может быть найден с помощью следующей команды (зависимостями от общих библиотек являются динамический компоновщик ld.so и библиотека языка Си).
#mips_fp_le-ldd hello_world libc.so.6 ld-linux.so.2
Таким образом, на момент создания исполняемого файла всех перемещений и разрешений символов не произошло. Всем функциям и данным глобальных переменных, которые не являются частью общих библиотек, были присвоены адреса и их адреса разрешены так, что вызывающий знает их адреса выполнения. Тем не менее, адреса выполнения разделяемых библиотек ещё не известны, и, следовательно, их разрешение (например, из функции myfunc вызывается printf) не закончено. Всё это происходит во время выполнения, когда программа будет реально запускаться из оболочки. Отметим, что существует альтернатива использованию разделяемых библиотек, так что все ссылки компонуются статически. Например, приведённый выше код может бы быть скомпонован со статической библиотекой языка Си libc.a (которая представляет собой архивный набор объектных файлов), как показано ниже:
#mips_fp_le-gcc -static hello-world.c -o hello_world
Если вывести список символов файла, как показано выше, функции printf адрес задан. Использование статических библиотек имеет тот недостаток, что тратится место и память за счёт более быстрой скорости запуска приложения. Теперь давайте запустим программу на плате и изучить её карту памяти.
#/bin/hello_world & [1] 4479 #cat /proc/4479/maps 00400000-00401000 r-xp 00000000 00:07 4088393 /bin/hello_world 00401000-00402000 rw-p 00001000 00:07 4088393 /bin/hello_world 2aaa8000-2aac2000 r-xp 00000000 00:07 1505291 /lib/ld-2.2.5.so 2aac2000-2aac4000 rw-p 00000000 00:00 0 2ab01000-2ab02000 rw-p 00019000 00:07 1505291 /lib/ld-2.2.5.so 2ab02000-2ac5f000 r-xp 00000000 00:07 1505859 /lib/ libc-2.2.5.so 2ac5f000-2ac9e000 ---p 0015d000 00:07 1505859 /lib/ libc-2.2.5.so 2ac9e000-2aca6000 rw-p 0015c000 00:07 1505859 /lib/ libc-2.2.5.so 2aca6000-2acaa000 rw-p 00000000 00:00 0 7ffef000-7fff8000 rwxp ffff8000 00:00 0
Видно, что вместе с основной программой hello_world, выделен диапазон адресов для libc и динамического компоновщика ld.so. Во время выполнения создаётся карта памяти приложения, а затем выполняется разрешение символов (в нашем случае это printf). Это делается за несколько этапов. Загрузчик ELF, который построен как часть ядра, сканирует исполняемый файл и узнаёт, что процесс имеет зависимость от общей библиотеки; поэтому он вызывает динамический компоновщик ld.so. Ld.so, который также реализован в виде общей библиотеки, является самозагружаемой библиотекой; он загружает себя и остальные разделяемые библиотеки (libc.so) в память, фиксируя таким образом карту памяти приложения и выполняя оставшуюся часть разрешения символов. Остался последний вопрос: как на самом деле работает printf? Как уже говорилось выше, любое обслуживание, которое должно быть сделано ядром, требует, чтобы приложение сделало системный вызов. После выполнения своих внутренних дел printf тоже делает системный вызов. Поскольку фактическая реализация системных вызовов сильно зависит от аппаратного обеспечения, библиотека языка Си скрывает всё это, обеспечивая обёртки, которые на самом деле делают системный вызов.Список всех системных вызовов, которые сделаны приложением, можно узнать используя приложение под названием strace; например, запуск strace для нашего приложения даёт следующий вывод, часть которого приведена ниже:
#strace hello_world ... write(1, "hello world", 11) = 11 ...
Теперь когда стала понятна основная идея пространства ядра и пространства пользователя, перейдём к процедуре запуска системы Linux.
|
Предыдущая Содержание Следующая |