Linux gcc exit function

Linux gcc exit function

Стандарт C описывает два определения EXIT_SUCCESS и EXIT_FAILURE , которые могут быть переданы exit() для обозначения соответственно успешного и неуспешного завершения.

ВОЗВРАЩАЕМЫЕ ЗНАЧЕНИЯ


СООТВЕТСТВИЕ СТАНДАРТАМ


ЗАМЕЧАНИЯ

Использование EXIT_SUCCESS и EXIT_FAILURE несколько более переносимо (на не-Unix окружения), чем использование 0 и ненулевого значения, например 1 или -1. В часности, VMS использует другое соглашение.

В BSD была произведена попытка стандартизировать коды выхода, см. файл .

После вызова exit() код выхода должен быть передан родительскому процессе. Существует три случая. Если родительский процесс установил SA_NOCLDWAIT или установил обработчик SIGCHLD в SIG_IGN, то код выход игнорируется. Если родительский процесс ожидает дочерний, то он получает уведомление о коде выхода. В обоих случаях завершаемый процесс удаляется немедленно. Если родительский процесс не указал, что он не интересуется кодом выхода дочернего процесса, и при этом он не ожидает его завершения, то завершаемый процесс превращается в «процесс-зомби» (то есть просто контейнер для единственного байта, содержащего код выхода) для того, чтобы родительский процесс мог узнать код выхода, если позднее вызовет одну из функций wait .

Если реализация поддерживает сигнал SIGCHLD, то этот сигнал поылается родительскому процессу. Не определено, посылается ли сигнал SIGCHLD родительскому процессу, установившему SA_NOCLDWAIT.

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

Если завершение процесса приводит группу процессов к потере родителя, и если любой член такой группы приостанавливается, то каждому процессу группы посылается сигнал SIGHUP, за которым следует сигнал SIGCONT.

Источник

GNU Compiler Collection, первые шаги

Эта заметка призвана на простых примерах познакомить начинающего nix-разработчика с инструментами GNU, в частности с компилятором GCC.

С его помощью мы и создадим простейшую программу. По большому счету все, как обычно. Заводим специальную папку, в которой будет размещаться проект.
Создаем в ней файл с именем: hello.c
Открываем файл в любом текстовом редакторе и пишем простейший код:

#include
int main(void)
<
printf(«Hello world!»);
return(0);
>

Сохраняем файл и выполняем команду: gcc hello.c

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

Это и есть исполняемый файл. Попробуем его запустить, для этого в консоли набираем: ./a.out

И радуемся в связи с первой написанной программой в линуксе!

Идем далее. При запуске исполняемого файла, если мы укажем только его название, система будет искать его в каталогах /usr/bin и /usr/local/bin, и, естественно, не найдет. Первый из них предназначен для размещения стабильных версий программ, как правило, входящих в дистрибутив Linux. Второй – для программ, устанавливаемых самим пользователем (за стабильность которых никто не ручается). По умолчанию, при сборке программы, устанавливаются в каталог /usr/local/bin.

Флаги используемые при компиляции

Флаг -o используем чтобы указать определенное имя получаемому исполняемому файлу: gcc hello.c -o say_hello

Флаг -E используем чтобы посмотреть, что получается после работы препроцессора. Этот флаг останавливает выполнение программы, как раз на этапе обработки препроцессором. В результате получается файл исходного кода с включённым в него содержимым заголовочных файлов.
Выподняем/смотрим: gcc -E hello.c -o hello.cpp

Флаг используем для создания объектных файлов (аналог *.obj): gcc -c kalkul.c

Название получаемого файла такое же, но компилятор изменяет расширение .c на .o (но указать можно и вручную).

Флаг -x используем, если создаётся объектный файл из исходника, уже обработанного препроцессором (например такого, какой мы получили выше), мы должны обязательно указать явно, что компилируемый файл является файлом исходного кода, обработанный препроцессором, и имеющий теги препроцессора. В противном случае он будет обрабатываться, как обычный файл C++, без учёта тегов препроцессора, а значит связь с объявленными функциями не будет устанавливаться.

Файл C++, обработанный препроцессором обозначается cpp-output:
gcc -x cpp-output -c hello.cpp

Собирается проект следующим образом: gcc hello.o -o say_hello
Запускаем: ./say_hello

Для чего нужна вся эта возня с промежуточными этапами?
Программы редко состоят из одного файла. Как правило исходных файлов несколько, и они объединены в проект. И в некоторых исключительных случаях программу приходится компоновать из нескольких частей, написанных, возможно, на разных языках. В этом случае приходится запускать компиляторы разных языков, чтобы каждый получил объектный файл из своего исходника, а затем уже эти полученные объектные файлы компоновать в исполняемую программу.

Читайте также:  Oyster планшет 10 windows

Источник

Linux gcc exit function

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux

Цилюрик О.И. Модули ядра Linux
Назад Окружение и инструменты Вперед

Компилятор GCC

Основным компилятором Linux является GCC. Но могут использоваться и другие, некоторые примеры таких иных компиляторов (используемых разными коллективами в Linux) являются: а). компилятор CC из состава IDE SolarisStudio операционной системы OpenSolaris, б). активно развивающийся в рамках проекта LLVM компилятор Clang (кандидат для замены GCC в FreeBSD, причина — лицензия), в). PCC (Portable C Compiler) — новая реализация компилятора 70-х годов, широко практикуемый в NetBSD и OpenBSD. Тем не менее, вся эта альтернативность возможна только в проектах пользовательского адресного пространства; в программировании ядра и, соответственно, модулей ядра применим исключительно компилятор GCC.

Примечание: Существуют экспериментальные проекты по сборке Linux компилятором, отличным от GCC. Есть сообщения о том, что компилятор Intel C имеет достаточную поддержку расширений GCC чтобы компилировать ядро Linux. Но при всех таких попытках пересборка может быть произведена только полностью, «с нуля»: начиная со сборки ядра и уже только потом сборка модулей. В любом случае, ядро и модули должны собираться одним компилятором.

Начало GCC было положено Ричардом Столлманом, который реализовал первый вариант GCC в 1985 на нестандартном и непереносимом диалекте языка Паскаль; позднее компилятор был переписан на языке Си Леонардом Тауэром и Ричардом Столлманом и выпущен в 1987 как компилятор для проекта GNU ( http://ru.wikipedia.org/wiki/GCC ). Компилятор GCC имеет возможность осуществлять компиляцию:

  • с нескольких языков программирования (точный перечень зависит от опций сборки самого компилятора gcc );
  • в систему команд множества (нескольких десятков) процессорных архитектур;

Достигается это 2-х уровневым процессом: а). лексический анализатор (вариант GNU утилиты bison , от общей UNIX реализации анализатора yacc ; в комплексе с лексическим анализатором flex ) и б). независимый генератор кода под архитектуру процессора.

Одно из свойств (для разработчиков модулей Linux), отличающих GCC в положительную сторону относительно других компиляторов, это расширенная многоуровневая (древовидная) система справочных подсказок, включённых в саму утилиту gcc , начиная с:

gcc (GCC) 4.4.3 20100127 (Red Hat 4.4.3-4)

Copyright (C) 2010 Free Software Foundation, Inc.

И далее . самая разная справочная информация, например, одна из полезных — опции компилятора, которые включены по умолчанию при указанном уровне оптимизации:

$ gcc -Q -O3 —help=optimizer

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

$ gcc -Q -O2 —help=optimizer > O2

$ gcc -Q -O3 —help=optimizer > O3

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

GCC имеет значительные синтаксические расширения (такие, например, как инлайновые ассемблерные вставки, или использование вложенных функций), не распознаваемые другими компиляторами языка C — ещё и поэтому альтернативные компиляторы вполне пригодны для сборки приложений, но непригодны для пересборки ядра Linux и сборки модулей ядра.

Невозможно в пару абзацев даже просто назвать то множество возможностей, которое сложилось за 25 лет развития проекта, но, к счастью, есть исчерпывающее полное руководство по GCC более чем на 600 страниц, и оно издано в русском переводе [8], которое просто рекомендуется держать под рукой на рабочем столе в качестве справочника.

Ассемблер в Linux

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

$ gcc -S -o my_file.S my_file.c

Примечание: Посмотреть результат ещё более ранней фазы препроцессирования можно, используя редко применяемый ключ -E :

$ gcc -E -o my_preprocessed.c my_file.c

Возможно использование ассемблерного кода для всех типов процессорных архитектур (x86, PPC, MIPS, AVR, ARM, . ) поддерживаемых GCC — но синтаксис записи будет отличаться.

Для генерации кода GCC вызывает as (раньше часто назывался как gas ), конфигурированный под целевой процессор:

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

Нотация AT&T

Ассемблер GCC использует синтаксическую нотацию AT&T, в отличие от нотации Intel (которую используют все инструменты Microsoft, компилятор С/С++ Intel, многоплатформенный ассемблер NASM).

Примечание: Обоснование этому простое — все названные инструменты, использующие нотацию Intel, используют её применительно к процессорам архитектуры x86. Но GCC является много-платформенным инструментом, поддерживающим не один десяток аппаратных платформ, ассемблерный код каждой из этих множественных платформ может быть записан в AT&T нотации.

В AT&T строка записанная как:

Выглядит в Intel нотации так:

Основные принципы AT&T нотации:

  1. Порядок операндов: , — в Intel нотации порядок обратный.
  2. Названия регистров имеют явный префикс % указывающий, что это регистр. То есть %eax, %dl, %esi, %xmm1 и т. д. То, что названия регистров не являются зарезервированными словами, — несомненный плюс.
  3. Явное задание размеров операндов в суффиксах команд: b-byte, w-word, l-long, q-quadword. В командах типа movl %edx, %eax это может показаться излишним, однако является весьма наглядным средством, когда речь идет о: incl (%esi) или xorw $0x7, mask
  4. Названия констант начинаются с $ и могут быть выражением. Например: movl $1,%eax
  5. Значение без префикса означает адрес. Это еще один камень преткновения новичков. Просто следует запомнить, что:
    movl $123, %eax — записать в регистр %eax число 123,
    movl 123, %eax — записать в регистр %eax содержимое ячейки памяти с адресом 123,
    movl var, %eax — записать в регистр %eax значение переменной var ,
    movl $var, %eax — загрузить адрес переменной var
  6. Для косвенной адресации необходимо использовать круглые скобки. Например: movl (%ebx), %eax — загрузить в регистр %eax значение переменной, по адресу находящемуся в регистре %ebx .
  7. SIB-адресация: смещение ( база, индекс, множитель ).
Читайте также:  Winamp плеер для windows

popw %ax /* извлечь 2 байта из стека и записать в %ax */

movl $0x12345, %eax /* записать в регистр константу 0x12345

movl %eax, %ecx /* записать в регистр %ecx операнд, который находится в регистре %eax */

movl (%ebx), %eax /* записать в регистр %eax операнд из памяти, адрес которого

находится в регистре адреса %ebx */

Пример: Вот как выглядит последовательность ассемблерных инструкций для реализации системного вызова на exit( EXIT_SUCCESS ) на x86 архитектуре:

Инлайновый ассемблер GCC

GCC Inline Assembly — встроенный ассемблер компилятора GCC, представляющий собой язык макроописания интерфейса компилируемого высокоуровнего кода с ассемблерной вставкой.

Синтаксис инлайн вставки в C-код — это оператор вида:

В простейшем случае это может быть:

asm [volatile] ( «команды ассемблера» );

1. то, как записать несколько строк инструкций ассемблера:

2. пример выполнения системного вызова write() , (показанный ранее в архиве int80.tgz ):

Для чего в случае asm служит ключевое слово volatile ? Для того чтобы указать компилятору, что вставляемый ассемблерный код может давать побочные эффекты, поэтому попытки оптимизации могут привести к логическим ошибкам.

Пример использования ассемблерного кода

Для сравнения того, как внешне выглядит функционально идентичный код, записанный на C ( gas2_0.c ), в виде ассемблерного файла ( gas2_1.c ) и инлайновой ассемблерной вставки ( gas2_2.c ), рассмотрим такой пример (архив gas-prog.tgz); прежде всего его сценарий сборки :

И далее сами файлы реализации:

Убеждаемся, что по исполнению все три варианта абсолютно идентичные:

Источник

Изучаем процессы в Linux


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

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

Всё написанное ниже справедливо к Debian Linux с ядром 4.15.0.

Содержание

Введение

Системное программное обеспечение взаимодействует с ядром системы посредством специальных функций — системных вызовов. В редких случаях существует альтернативный API, например, procfs или sysfs, выполненные в виде виртуальных файловых систем.

Атрибуты процесса

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

  • Идентификатор процесса (pid)
  • Открытые файловые дескрипторы (fd)
  • Обработчики сигналов (signal handler)
  • Текущий рабочий каталог (cwd)
  • Переменные окружения (environ)
  • Код возврата

Жизненный цикл процесса

Рождение процесса

Только один процесс в системе рождается особенным способом — init — он порождается непосредственно ядром. Все остальные процессы появляются путём дублирования текущего процесса с помощью системного вызова fork(2) . После выполнения fork(2) получаем два практически идентичных процесса за исключением следующих пунктов:

  1. fork(2) возвращает родителю PID ребёнка, ребёнку возвращается 0;
  2. У ребёнка меняется PPID (Parent Process Id) на PID родителя.

После выполнения fork(2) все ресурсы дочернего процесса — это копия ресурсов родителя. Копировать процесс со всеми выделенными страницами памяти — дело дорогое, поэтому в ядре Linux используется технология Copy-On-Write.
Все страницы памяти родителя помечаются как read-only и становятся доступны и родителю, и ребёнку. Как только один из процессов изменяет данные на определённой странице, эта страница не изменяется, а копируется и изменяется уже копия. Оригинал при этом «отвязывается» от данного процесса. Как только read-only оригинал остаётся «привязанным» к одному процессу, странице вновь назначается статус read-write.

Состояние «готов»

Сразу после выполнения fork(2) переходит в состояние «готов».
Фактически, процесс стоит в очереди и ждёт, когда планировщик (scheduler) в ядре даст процессу выполняться на процессоре.

Читайте также:  Windows file server troubleshooting

Состояние «выполняется»

Как только планировщик поставил процесс на выполнение, началось состояние «выполняется». Процесс может выполняться весь предложенный промежуток (квант) времени, а может уступить место другим процессам, воспользовавшись системным вывозом sched_yield .

Перерождение в другую программу

В некоторых программах реализована логика, в которой родительский процесс создает дочерний для решения какой-либо задачи. Ребёнок в данном случае решает какую-то конкретную проблему, а родитель лишь делегирует своим детям задачи. Например, веб-сервер при входящем подключении создаёт ребёнка и передаёт обработку подключения ему.
Однако, если нужно запустить другую программу, то необходимо прибегнуть к системному вызову execve(2) :

или библиотечным вызовам execl(3), execlp(3), execle(3), execv(3), execvp(3), execvpe(3) :

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

Как не путаться во всех этих вызовах и выбирать нужный? Достаточно постичь логику именования:

  • Все вызовы начинаются с exec
  • Пятая буква определяет вид передачи аргументов:
    • l обозначает list, все параметры передаются как arg1, arg2, . NULL
    • v обозначает vector, все параметры передаются в нуль-терминированном массиве;
  • Далее может следовать буква p, которая обозначает path. Если аргумент file начинается с символа, отличного от «/», то указанный file ищется в каталогах, перечисленных в переменной окружения PATH
  • Последней может быть буква e, обозначающая environ. В таких вызовах последним аргументом идёт нуль-терминированный массив нуль-терминированных строк вида key=value — переменные окружения, которые будут переданы новой программе.

Семейство вызовов exec* позволяет запускать скрипты с правами на исполнение и начинающиеся с последовательности шебанг (#!).

Есть соглашение, которое подразумевает, что argv[0] совпадает с нулевым аргументов для функций семейства exec*. Однако, это можно нарушить.

Любопытный читатель может заметить, что в сигнатуре функции int main(int argc, char* argv[]) есть число — количество аргументов, но в семействе функций exec* ничего такого не передаётся. Почему? Потому что при запуске программы управление передаётся не сразу в main. Перед этим выполняются некоторые действия, определённые glibc, в том числе подсчёт argc.

Состояние «ожидает»

Некоторые системные вызовы могут выполняться долго, например, ввод-вывод. В таких случаях процесс переходит в состояние «ожидает». Как только системный вызов будет выполнен, ядро переведёт процесс в состояние «готов».
В Linux так же существует состояние «ожидает», в котором процесс не реагирует на сигналы прерывания. В этом состоянии процесс становится «неубиваемым», а все пришедшие сигналы встают в очередь до тех пор, пока процесс не выйдет из этого состояния.
Ядро само выбирает, в какое из состояний перевести процесс. Чаще всего в состояние «ожидает (без прерываний)» попадают процессы, которые запрашивают ввод-вывод. Особенно заметно это при использовании удалённого диска (NFS) с не очень быстрым интернетом.

Состояние «остановлен»

В любой момент можно приостановить выполнение процесса, отправив ему сигнал SIGSTOP. Процесс перейдёт в состояние «остановлен» и будет находиться там до тех пор, пока ему не придёт сигнал продолжать работу (SIGCONT) или умереть (SIGKILL). Остальные сигналы будут поставлены в очередь.

Завершение процесса

Ни одна программа не умеет завершаться сама. Они могут лишь попросить систему об этом с помощью системного вызова _exit или быть завершенными системой из-за ошибки. Даже когда возвращаешь число из main() , всё равно неявно вызывается _exit .
Хотя аргумент системного вызова принимает значение типа int, в качестве кода возврата берется лишь младший байт числа.

Состояние «зомби»

Сразу после того, как процесс завершился (неважно, корректно или нет), ядро записывает информацию о том, как завершился процесс и переводит его в состояние «зомби». Иными словами, зомби — это завершившийся процесс, но память о нём всё ещё хранится в ядре.
Более того, это второе состояние, в котором процесс может смело игнорировать сигнал SIGKILL, ведь что мертво не может умереть ещё раз.

Забытье

Код возврата и причина завершения процесса всё ещё хранится в ядре и её нужно оттуда забрать. Для этого можно воспользоваться соответствующими системными вызовами:

Вся информация о завершении процесса влезает в тип данных int. Для получения кода возврата и причины завершения программы используются макросы, описанные в man-странице waitpid(2) .

Передача argv[0] как NULL приводит к падению.

Бывают случаи, при которых родитель завершается раньше, чем ребёнок. В таких случаях родителем ребёнка станет init и он применит вызов wait(2) , когда придёт время.

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

Благодарности

Спасибо Саше «Al» за редактуру и помощь в оформлении;

Спасибо Саше «Reisse» за понятные ответы на сложные вопросы.

Они стойко перенесли напавшее на меня вдохновение и напавший на них шквал моих вопросов.

Источник

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