Процессы и программы
Процессом является экземпляр выполняемой программы. В данном посте мы подробно разберем это определение и вы узнаете разницу между программой и процессом.
Программа представляет собой файл, содержащий различную информацию о том, как сконструировать процесс в ходе выполнения. В эту информацию входит:
1. Идентификационный признак двоичного формата.
Каждый программный файл включает в себя метаинформацию с описанием формата исполняемого файла. Это позволяет ядру корректно интерпретировать все остальные данные внутри файла.
Изначально для исполняемых файлов UNIX было предусмотрено два широко используемых формата: исходный формат a.out (assembler output — вывод на языке ассемблера) и появившийся позже более сложный общий формат объектных файлов COFF (Common Object File Format).
В настоящее время в большинстве реализаций UNIX (включая Linux) применяется формат исполняемых и компонуемых файлов ELF (Executable and Linking Format), предоставляющий множество преимуществ по сравнению с предшественниками.
2. Машинный код.
В нем закодирован алгоритм программы.
3. Адрес входа в программу.
В нем указывается место той инструкции, с которой должно начаться выполнение программы.
4. Данные.
В программном файле содержатся значения, используемые для инициализации переменных, а также применяемые программой символьные константы (например, строки).
5. Таблицы имен и переадресации.
В них дается описание расположений имен функций и переменных внутри программы. Эти таблицы предназначены для различных целей, включая отладку и разрешение имен в ходе выполнения программы (динамическое связывание).
6. Информация о совместно используемых библиотеках и динамической компоновке.
В программный файл включаются поля, где перечисляются совместно используемые библиотеки, которые программе потребуются в ходе выполнения, а также путевое имя динамического компоновщика, который должен применяться для загрузки этих библиотек.
Одна программа может использоваться для построения множества процессов, или же, если наоборот, во множестве процессов может быть запущена одна и та же программа.
Определение процесса, которое было дано в начале, можно переформулировать следующим образом: процесс является абстрактной сущностью, которая установлена ядром и которой, для выполнения программы, выделяются системные ресурсы.
С позиции ядра процесс состоит из памяти пользовательского пространства, внутри которой содержится код программы, а также из ряда структур данных, хранящих информацию о состоянии процесса.
Некоторая информация, записанная в структурах данных ядра, включает в себя: различные идентификаторы, связанные с процессом (PID, PPID ...), таблицы виртуальной памяти и дескрипторов открытых файлов, сведения, относящиеся к текущему рабочему каталогу, доставке и обработке сигналов, использованию и ограничениям ресурсов процесса.
#linuxcore #linux
Процессом является экземпляр выполняемой программы. В данном посте мы подробно разберем это определение и вы узнаете разницу между программой и процессом.
Программа представляет собой файл, содержащий различную информацию о том, как сконструировать процесс в ходе выполнения. В эту информацию входит:
1. Идентификационный признак двоичного формата.
Каждый программный файл включает в себя метаинформацию с описанием формата исполняемого файла. Это позволяет ядру корректно интерпретировать все остальные данные внутри файла.
Изначально для исполняемых файлов UNIX было предусмотрено два широко используемых формата: исходный формат a.out (assembler output — вывод на языке ассемблера) и появившийся позже более сложный общий формат объектных файлов COFF (Common Object File Format).
В настоящее время в большинстве реализаций UNIX (включая Linux) применяется формат исполняемых и компонуемых файлов ELF (Executable and Linking Format), предоставляющий множество преимуществ по сравнению с предшественниками.
2. Машинный код.
В нем закодирован алгоритм программы.
3. Адрес входа в программу.
В нем указывается место той инструкции, с которой должно начаться выполнение программы.
4. Данные.
В программном файле содержатся значения, используемые для инициализации переменных, а также применяемые программой символьные константы (например, строки).
5. Таблицы имен и переадресации.
В них дается описание расположений имен функций и переменных внутри программы. Эти таблицы предназначены для различных целей, включая отладку и разрешение имен в ходе выполнения программы (динамическое связывание).
6. Информация о совместно используемых библиотеках и динамической компоновке.
В программный файл включаются поля, где перечисляются совместно используемые библиотеки, которые программе потребуются в ходе выполнения, а также путевое имя динамического компоновщика, который должен применяться для загрузки этих библиотек.
Одна программа может использоваться для построения множества процессов, или же, если наоборот, во множестве процессов может быть запущена одна и та же программа.
Определение процесса, которое было дано в начале, можно переформулировать следующим образом: процесс является абстрактной сущностью, которая установлена ядром и которой, для выполнения программы, выделяются системные ресурсы.
С позиции ядра процесс состоит из памяти пользовательского пространства, внутри которой содержится код программы, а также из ряда структур данных, хранящих информацию о состоянии процесса.
Некоторая информация, записанная в структурах данных ядра, включает в себя: различные идентификаторы, связанные с процессом (PID, PPID ...), таблицы виртуальной памяти и дескрипторов открытых файлов, сведения, относящиеся к текущему рабочему каталогу, доставке и обработке сигналов, использованию и ограничениям ресурсов процесса.
#linuxcore #linux
👍12🔥6⚡3❤2🥰1👾1
Структура памяти процесса
Память, выделяемая каждому процессу, состоит из нескольких частей, которые обычно называют сегментами. К числу таких сегментов относятся следующие:
1. Текстовый сегмент (.text)
Cодержит машинный код, который принадлежит программе, запущенной процессом. Текстовый сегмент создается только для чтения, чтобы процесс не мог случайно изменить свои собственные инструкции (из-за read-only статуса, некоторые компиляторы могут записывать туда константы).
Поскольку многие процессы могут выполнять одну и ту же программу, текстовый сегмент создается с возможностью совместного использования - единственная копия кода программы может быть отображена на виртуальное адресное пространство всех процессов.
2. Сегмент инициализированных данных (.data)
Хранит глобальные и статические переменные, инициализированные явным образом. Значения этих переменных считываются из исполняемого файла при загрузке программы в память.
3. Сегмент неинициализированных данных (.bss - block started by symbol)
Содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система определяет всю память в этом сегменте значением 0.
Основная причина помещения прошедших инициализацию переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные.
Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.
4. Динамически увеличивающийся и уменьшающийся сегмент стека (stack)
Содержит стековые фреймы: для каждой отдельной функции выделяется один стековый фрейм, в котором хранятся ее локальные переменные, аргументы и возвращаемое значение. По мере вызова функций и возврата из них стек расширяется и сжимается.
Текущая вершина стека отслеживается в специально предназначенном для этого регистре - указателе стека. Процесс сжатия стека называется расруткой.
5. Динамическая память (heap)
Область, которая предназначена для динамического выделения памяти в ходе выполнения программы (malloc, calloc, free, realloc - под капотом используют системные вызовы brk и sbrk).
6. Сегмент аргументов командной строки
Сегмент для хранения переменных, которые переданы программе в качестве аргументов (argc и argv[]), где argc хранит количество переданных аргументов, а argv хранит значение фактических значений вместе с именем файла:
Далее продемонстрированы различные типы переменных в коде и комментарии, указывающие на сегменты их размещения (в случае оптимизаций, часто используемые переменные могут быть помещены в регистры или вообще исключены):
В комменты я отправил несколько изображений для визуализации материала.
#linuxcore #linux #cpp #cppcore
Память, выделяемая каждому процессу, состоит из нескольких частей, которые обычно называют сегментами. К числу таких сегментов относятся следующие:
1. Текстовый сегмент (.text)
Cодержит машинный код, который принадлежит программе, запущенной процессом. Текстовый сегмент создается только для чтения, чтобы процесс не мог случайно изменить свои собственные инструкции (из-за read-only статуса, некоторые компиляторы могут записывать туда константы).
Поскольку многие процессы могут выполнять одну и ту же программу, текстовый сегмент создается с возможностью совместного использования - единственная копия кода программы может быть отображена на виртуальное адресное пространство всех процессов.
2. Сегмент инициализированных данных (.data)
Хранит глобальные и статические переменные, инициализированные явным образом. Значения этих переменных считываются из исполняемого файла при загрузке программы в память.
3. Сегмент неинициализированных данных (.bss - block started by symbol)
Содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система определяет всю память в этом сегменте значением 0.
Основная причина помещения прошедших инициализацию переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные.
Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.
4. Динамически увеличивающийся и уменьшающийся сегмент стека (stack)
Содержит стековые фреймы: для каждой отдельной функции выделяется один стековый фрейм, в котором хранятся ее локальные переменные, аргументы и возвращаемое значение. По мере вызова функций и возврата из них стек расширяется и сжимается.
Текущая вершина стека отслеживается в специально предназначенном для этого регистре - указателе стека. Процесс сжатия стека называется расруткой.
5. Динамическая память (heap)
Область, которая предназначена для динамического выделения памяти в ходе выполнения программы (malloc, calloc, free, realloc - под капотом используют системные вызовы brk и sbrk).
6. Сегмент аргументов командной строки
Сегмент для хранения переменных, которые переданы программе в качестве аргументов (argc и argv[]), где argc хранит количество переданных аргументов, а argv хранит значение фактических значений вместе с именем файла:
~$ ./prog 100 23 43 69
Далее продемонстрированы различные типы переменных в коде и комментарии, указывающие на сегменты их размещения (в случае оптимизаций, часто используемые переменные могут быть помещены в регистры или вообще исключены):
/* Сегмент неинициализированных данных */
char globBuf[65536];
/* Сегмент инициализированных данных */
int primes[] = { 2, 3, 5, 7 };
/* Размещается в фрейме для square() */
static int square(int x)
{
/* Размещается в фрейме для square() */
int result;
result = x * x;
/* Возвращаемое значение передается через регистр */
return result;
}
/* Размещается в фрейме для doCalc() */
static void doCalc(int val)
{
square(val)
if (val < 1000) {
/* Размещается в фрейме для doCalc() */
int t;
t = val * val * val;
}
}
/* Размещается в фрейме для main() */
int main(int argc, char *argv[])
{
printf("File name = %s\n", argv[0]);
printf("Number of arguments = %d\n", argc - 1);
/* Сегмент инициализированных данных */
static int key = 9973;
/* Сегмент неинициализированных данных */
static char mbuf[10240000];
/* Размещается в фрейме для main() */
char *p;
/* Указывает на память в сегменте кучи */
p = malloc(1024);
doCalc(key);
}
В комменты я отправил несколько изображений для визуализации материала.
#linuxcore #linux #cpp #cppcore
👍19🔥7❤3✍2🥰1👾1
Системные и библиотечные вызовы [1]
Ну чтож, начинаем погружение в интересную тему - рассмотрим то, без чего не обходится ни одно системное приложение - системные и библиотечные вызовы.
Тут, на самом деле, можно довольно глубоко копнуть. Если проявится ваш интерес, сделаем ряд постов, где вдоль и поперек разберем принцип работы вызовов, практики их использования и т.д.
Что такое системный вызов?
Можно считать, что все пользовательские программы работают внутри некоторой песочницы, из которой нельзя так просто выполнять ряд действий: получать доступ к файлам, передавать данные по сети, выводить текст на экран, создавать процессы и т.д. Для этого нужно обратиться к внутренним сервисам ядра и попросить его сделать что требуется через системные вызовы.
Системный вызов изменяет состояние процессора, переводя его из пользовательского режима в режим ядра, позволяя ему таким образом получить доступ к защищенной памяти ядра.
(Системные вызовы Linux перечисляются на странице руководства syscalls(2))
Это может вас немного напугать, но, обычно, системные вызовы недоступны напрямую. Все, с чем вы работаете - это тонкие функции-обертки библиотки glibc. Да-да, не удивляйтесь: read, write, fork являются всего лишь понятным пользователю интерфейсом, через который отрабатывают системные вызовы.
И да, важно помнить то, что системные вызовы - это платформозависимая история. Чтение файлов, взаимодействие с процессами, работа с сокетами, управление памятью и прочие системные операции реализованы по разному на каждой системе и могут отличаться друг от друга по API.
Как сделать прямой системный вызов?
Иногда бывает полезно явно обраться к системному вызову в случае, если для него не реализована сишная функция-обертка. Сделать это можно через библиотечный вызов syscall() из unistd.h, который первым параметром принимает номер системного вызова из "sys/syscall.h", а далее - аргументы самого вызова:
Таким образом, системный вызов write() можно инициировать по разному:
Что такое библиотечный вызов?
Библиотечным вызовом можно считать вызов функции стандартной библиотеки C, в которой не содержится прямой реализации системного вызова: системного прерывания, переноса аргументов в определенные, необходимые ядру, регистры, копирования номера системного вызова в регистр (%eax) и т.д.
Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т.д.
Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами с платформозависимой реализацией.
Например, библиотечная функция fopen() использует для открытия файла системный вызов open().
Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова.
Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а внутренний системный вызов write() просто выводит блок байтов.
Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().
#linuxcore #linux #cpp #cppcore
Ну чтож, начинаем погружение в интересную тему - рассмотрим то, без чего не обходится ни одно системное приложение - системные и библиотечные вызовы.
Тут, на самом деле, можно довольно глубоко копнуть. Если проявится ваш интерес, сделаем ряд постов, где вдоль и поперек разберем принцип работы вызовов, практики их использования и т.д.
Что такое системный вызов?
Можно считать, что все пользовательские программы работают внутри некоторой песочницы, из которой нельзя так просто выполнять ряд действий: получать доступ к файлам, передавать данные по сети, выводить текст на экран, создавать процессы и т.д. Для этого нужно обратиться к внутренним сервисам ядра и попросить его сделать что требуется через системные вызовы.
Системный вызов изменяет состояние процессора, переводя его из пользовательского режима в режим ядра, позволяя ему таким образом получить доступ к защищенной памяти ядра.
(Системные вызовы Linux перечисляются на странице руководства syscalls(2))
Это может вас немного напугать, но, обычно, системные вызовы недоступны напрямую. Все, с чем вы работаете - это тонкие функции-обертки библиотки glibc. Да-да, не удивляйтесь: read, write, fork являются всего лишь понятным пользователю интерфейсом, через который отрабатывают системные вызовы.
И да, важно помнить то, что системные вызовы - это платформозависимая история. Чтение файлов, взаимодействие с процессами, работа с сокетами, управление памятью и прочие системные операции реализованы по разному на каждой системе и могут отличаться друг от друга по API.
Как сделать прямой системный вызов?
Иногда бывает полезно явно обраться к системному вызову в случае, если для него не реализована сишная функция-обертка. Сделать это можно через библиотечный вызов syscall() из unistd.h, который первым параметром принимает номер системного вызова из "sys/syscall.h", а далее - аргументы самого вызова:
extern long int syscall (long int __sysno, ...) __THROW;
Таким образом, системный вызов write() можно инициировать по разному:
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
write(1, "hello, world!\n", 14)
syscall(SYS_write, 1, "hello, world!\n", 14);
return 0;
}
Что такое библиотечный вызов?
Библиотечным вызовом можно считать вызов функции стандартной библиотеки C, в которой не содержится прямой реализации системного вызова: системного прерывания, переноса аргументов в определенные, необходимые ядру, регистры, копирования номера системного вызова в регистр (%eax) и т.д.
Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т.д.
Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами с платформозависимой реализацией.
Например, библиотечная функция fopen() использует для открытия файла системный вызов open().
Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова.
Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а внутренний системный вызов write() просто выводит блок байтов.
Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().
#linuxcore #linux #cpp #cppcore
👍32🔥6❤🔥5❤2🖕1👾1