--- title: Системное программирование в Linux lang: ru --- Вы должны: * Уметь выполнять команды в командной строке Linux. * Владеть языком C. Вы научитесь: * Находить и читать документацию по системным функциям. * Использовать системные функции в программах на C. * Собирать программы из командной строки Linux. * Пользоваться отладчиком GDB. * Пользоваться программой `strace` для диагностики системных вызовов. # Системное программирование Системное программирование связано с взаимодействием с ядром ОС. Это может быть использование системных вызовов в пространстве пользователя или написание кода, который исполняется в ядре (само ядро и драйверы). Первое значительно проще и чаще встречается. Технически работа с системными вызовами мало отличается от работы с любыми другими библиотеками и их API. Однако нужно понимать концепции и возможности данной ОС. Например, для Linux это иерархия файловой системы (ФС) и специальные ФС (`procfs`, `sysfs`) для доступа к объектам ядра, разграничение прав доступа к файлам, конкретные возможности, которые ОС предоставляет для ряда задач. Большая часть системного программирования — это не написание кода, а изучение документации на системные вызовы ОС и на её возможности. # Чтение документации Из `man man` можно узнать, что документация состоит из разделов, в частности: * 1 — исполняемые программы и команды оболочки; * 2 — системные вызовы (специфичные для Linux); * 3 — функции библиотек (не специфичные для Linux); * 7 — общая информация, не связанная с конкретной функцией. Страница с одним и тем же названием может быть в разных разделах. Например, `man stat` открывает страницу раздела 1 и описывает программу `stat`. Номер раздела виден в левом верхнем углу страницы: `STAT(1)`. Функция же `stat()` описана в разделе 3, открывается как `man 3 stat`. В разделе 2 тоже есть страница про `stat`, причем она больше, чем в разделе 3. Разница в том, что *stat(3)* описывает стандартные функции для всех POSIX-систем, а *stat(2)* описывает все, что поддерживает Linux (больше, чем в стандарте). Документацию не обязательно читать в терминале: , . Однако `man` соответствует конкретному дистрибутиву Linux и версиям его пакетов, а часть сведений с сайтов может быть неприменима к данной системе. Документацию лучше читать в оригинале (обычно на английском), чтобы не ждать перевода и не страдать из-за ошибок переводчика. Автоматические переводчики тоже иногда ошибаются и искажают смысл. Узнать о том, что какая-то функция вообще есть, можно из учебников и новостей. Популярные источники новостей: (на русском), (на английском). # Выполнение лабораторной работы Чтобы изучить на примере, как программировать с использованием системных API, напишем программу `example1`, которая печатает размеры файлов, имена которых передаются ей как аргументы при запуске: ```sh $ ./example1 example1.c example1 Size of 'example1.c' is 259 bytes Size of 'example1' is 87340 bytes ``` ## Написание кода В ходе ЛР предлагается писать код в `vim` или в Midnight Commander (`mc`) по клавише `F4`. Преимущество в том, что ими можно пользоваться, просто подключившись к серверу. Это простые редакторы, но для маленьких лабораторных программ их достаточно. Вообще говоря, работа с Linux не ограничивается терминалом. Под Linux есть графические окружения, в том числе редакторы кода и интегрированные среды разработки (IDE). Популярные: Visual Studio Code (не путать с Visual Studio), Eclipse, QtCreator, CLion (платная), CodeBlocks и много других. На виртуальные серверы их ставить не нужно, так как они потребляют много ресурсов. Однако, если лабораторная работа выполняется на локальной машине, можно установить, настроить и использовать то, что удобнее. Предлагается писать код на C, а не на C++. Это связано с тем, что системные API описаны в терминах C, примеры в документации на языке C, а средства C++ в задачах этого курса менее востребованы, чем в прикладных программах. Фактически это значит, что `vector` и `string` недоступны, а вместо `cin` и `cout` из `` нужно пользоваться `scanf()` и `printf()` из ``. На практике C++ для системного программирования активно используют, но только в пространстве пользователя. ## Работа с аргументами программы Создайте файл `example1.c`, введите в него следующий код и сохраните: ```c #include int main(int argc, char** argv) { int i; for (i = 1; i < argc; i++) { printf("Size of '%s' is %d bytes\n", argv[i], 0); } return 0; } ``` Заготовка делает почти то же самое, что должна делать программа в итоге, но вместо размеров печатаются нули. Если вы забыли, как работать с аргументами программы и функцией `printf()`, обратитесь к [документации](https://en.cppreference.com/w/c/language/main_function) и примеру в ней. **Задание.** Ответьте на вопросы для самопроверки: * Чему будет равно `argc` при запуске программы как `./example1 foo bar`? * Какие значения находятся в массиве `argv` при таком запуске? * Почему в коде выше цикла начинается с 1, а не с 0? * Как называется первый параметр функции `printf()`? * Что означают `%s` и `%d` в нем? * Почему выбраны именно `%s` и `%d`? ## Компиляция Соберите исполняемый файл `example1` из исходного кода в `example1.c`: ```sh gcc example1.c -o example1 ``` Запустите программу: ```sh ./example1 example1.c example1 ``` ## Изучение документации Получить сведения о файле, в частности, его размер, можно функцией `stat()`. ``` NAME stat, fstat, lstat, fstatat - get file status ``` Одна `man`-страница часто содержит описание семейства функций, в данном случае четырех функций для получения статуса файла (сведений о нем). ``` SYNOPSIS #include int stat(const char *restrict pathname, struct stat *restrict statbuf); ... ``` Раздел-сводка описывает, как объявлена функция и какие заголовочные файлы нужно подключить, чтобы использовать её. В объявлениях часто встречается ключевое слово `restrict` с указателями. Оно означает, что если есть два параметра-указателя, то они содержат адреса разных, не пересекающихся блоков памяти. В случае `stat()` это значит, что нельзя записать в массив символов имя файла и передать указатель на этот массив и как `pathname`, и как `statbuf`, ожидая, что функция сначала прочитает из массива имя файла, а потом перезапишет содержимое массива сведениями о файле. На практике так в любом случае стараются не делать, чтобы не запутывать код. Первый параметр, `pathname`, имеет тип `const char*`, то есть это строка. Второй параметр, `statbuf`, имеет тип `struct stat*`, указатель на структуру. В отличие от C++, в C, если объявлена структура `struct Example { ... }`, то экземпляр `ex` этой структуры надо объявлять как `struct Example ex`, а не `Example ex`. ``` DESCRIPTION These functions return information about a file, in the buffer pointed to by statbuf... The stat structure All of these system calls return a stat structure, which contains the following fields: struct stat { dev_t st_dev; /* ID of device containing file */ ... ``` Раздел с описанием раскрывает, что именно делает функция, что передается во входных параметрах и что возвращается в выходных. Функция `stat()` записывает информацию о файле в поля структуры, адрес которой передан в параметре-указателе `statbuf`. Далее описывается, что будет записано в разные поля этой структуры. Нас интересует поле с размером файла: ``` st_size This field gives the size of the file (if it is a regular file or a symbolic link) in bytes. The size of a symbolic link is the length of the pathname it contains, without a terminating null byte. ``` У многих функций после описания идет раздел `EXAMPLES` (примеры), где приводятся небольшие программы, демонстрирующие, как пользоваться функцией. Большая часть `man`-страниц заканчивается разделом, где перечислены связанные страницы. Например, к теме получения сведений о файле относятся команды `ls` и `stat`, функции `access()`, `readlink()` и другие: ``` SEE ALSO ls(1), stat(1), access(2), chmod(2), chown(2), readlink(2), statx(2), utime(2), capabilities(7), inode(7), symlink(7) ``` Итак, необходимо: * Объявить переменную типа `struct stat`. * Передать функции `stat()` имя файла и адрес объявленной переменной. * Напечатать размер из поля `st_size` объявленной переменной. **Задание.** Модифицируйте код, соберите программу и запустите её так же, как в прошлый раз. ```c #include #include int main(int argc, char** argv) { struct stat st; int i; for (i = 1; i < argc; i++) { stat(argv[i], &st); printf("Size of '%s' is %d bytes\n", argv[i], st.st_size); } return 0; } ``` **Задание.** Попробуйте вызвать программу без аргументов, с несколькими аргументами-именами существующих файлов и с аргументом-именем файла, которого не существует. Объясните результаты. ## Обработка ошибок при вызове функций Как понять, что функция не смогла выполнить свою работу, например, если файла не существует? Продолжим чтение документации: ``` RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set to indicate the error. ERRORS EACCES Search permission is denied for one of the directories in the path prefix of pathname. (See also path_resolution(7).) ... ENOENT A component of pathname does not exist or is a dangling symbolic link. ... ``` Функция `stat()` возвращает целое число: 0 при успехе, (-1) при ошибках. Числовой код ошибки, по которому можно понять причину, записывается в глобальную переменную `errno`. В частности, если файла не существует, будет `errno == ENOENT` и аналогично с другими кодами для других причин ошибок. Значит, при вызове функции нужно проверить возвращаемое значение, Если оно отрицательное, то обработать ошибку, например, напечатать сообщение о ней с кодом: ```c int ret = stat(...); if (ret < 0) { printf("stat(): errno=%d\n", errno); continue; } ``` **Задание.** Добавьте в программу обработку ошибок. Соберите программу. Проверьте, как программа реагирует на несуществующий файл. Коды ошибок трудно и не нужно помнить наизусть. Есть функции *strerror(3)*, *perror(3)* и другие, чтобы по коду ошибки получить или напечатать её текстовое описание для пользователя. Программа *errno(1)* позволяет получить описание ошибки по коду (эта программа по умолчанию не установлена): ```sh % errno 12 ENOMEM 12 Cannot allocate memory ``` ## Исправление ошибок и предупреждений компилятора Заменим `for (i = 1; i < argc…` на `for (i = 1; i < argd…` (`argc` → `argd`), как будто допущена опечатка, и повторим сборку: ```sh $ gcc example1.c -o example1 ``` Возникнет ошибка, GCC даже подсказывает, в чем она может заключаться: ``` example1.c: In function ‘main’: example1.c:9:21: error: ‘argd’ undeclared (first use in this function); did you mean ‘argv’? 9 | for (i = 1; i < argd; i++) { | ^~~~ | argv example1.c:9:21: note: each undeclared identifier is reported only once for each function it appears in ``` В начале сообщения указана строка с ошибкой (9). VIM позволяет перейти к строке с заданным номером так: `:9`, Enter. **Задание.** Исправьте ошибку и убедитесь, что программа снова собирается успешно. По умолчанию компилятор обнаруживает только ошибки. Современные компиляторы могут также обнаруживать потенциальные проблемы, которые не являются ошибками с точки зрения языка C, но могут привести к неправильной работе программы. Показ дополнительных предупреждений включается флагами компилятора: ```sh $ gcc example1.c -Wall -Wextra -o example1 ``` ``` example1.c: In function ‘main’: example1.c:11:34: warning: format ‘%d’ expects argument of type ‘int’, but argument 3 has type ‘__off_t’ {aka ‘long int’} [-Wformat=] 11 | printf("Size of '%s' is %d bytes\n", argv[i], st.st_size); | ~^ ~~~~~~~~~~ | | | | unsigned int __off_t {aka long int} | %ld ``` Действительно, `st.st_size` имеет тип `long int` (обычно 64 бита), поэтому для него нужен спецификатор `%ld`. Спецификатор `%d` — для `int` (обычно 32 бита, значения до ~2 млрд.). **Задание.** Исправьте ошибку и убедитесь, что предупреждения больше не возникает. **Задание.** В каком случае проблема проявилась бы? **Внимание.** В этой и последующих работах все программы нужно собирать с такими же или более строгими флагами, исправляя все возникающие предупреждения. ## Отладка программ *Отладкой (debugging)* называется поиск и устранение ошибок в программе. Так как системные программы обычно пишутся на C, и при ошибке не показывается место в коде, где она возникла, полезна *пошаговая отладка (step-by-step debugging)* — выполнение программы «строка за строкой» с возможностью остановиться и просмотреть значения переменных. *Отладчик (debugger)* — это специальная программа, которая запускает отлаживаемую и позволяет управлять ходом её выполнения. Отладчиком часто пользуются из удобного графического интерфейса IDE. Технически на том же сервере, что и отлаживаемая программа, достаточно установить `gdbserver`, чтобы подключаться к нему и отлаживать в IDE. Зачем тогда знать командную строку GDB? * Установить на удаленный сервер GDB может быть намного проще, чем согласовать подключение к нему при работе в корпоративных сетях. Текстовые команды можно продиктовать или переслать тому, кто может подключиться к серверу. * Текст сеанса работы с GDB (все команды и их вывод) можно сохранить и поделиться им с другими специалистами, в отличие от интерактивного GUI. * Язык команд GDB позволяет широкую автоматизацию. Как минимум, команды можно записать и затем быстро повторить действия. ### Работа с GNU Debugger Создайте файл `example2.c` со следующей программой: ```c #include void print(const char* message) { puts(message); } int main(int argc, char** argv) { print(argv[0]); print(argv[1]); print(argv[2]); } ``` Отладчику нужны сведения о том, как машинный код в исполняемом файле соотносится с исходным текстом, где в памяти размещены переменные и т. п. По умолчанию эта отладочная информация не включается в исполняемый файл. Чтобы компилятор добавил ее, нужен ключ `-g` при компиляции: ```sh $ gcc example2.c -g -o example2 ``` Запустите программу: ```sh $ ./example2 ``` ``` ./example2 Segmentation fault (core dumped) ``` Программа аварийно завершается («падает»). Первую строку, `./example2`, программа напечатала сама — это `argv[0]`. Вторую строку напечатала оболочка, её смысл мы разберем далее. Запустим программу под отладчиком, чтобы найти причину аварийного завершения: ```sh $ gdb -q --args ./example2 ``` ``` Reading symbols from example2... (gdb) ``` Вместо приглашения командной оболочки появилось приглашение командной строки отладчика, обозначаемое `(gdb)`. Отладчик не запускает программу сразу, это нужно сделать командой `run` (GDB также воспринимает сокращения команд, например, `r` вместо `run`): ```gdb (gdb) run ``` ``` Starting program: /home/user/example2 Missing separate debuginfo for /lib64/ld-linux-x86-64.so.2 Try to install the hash file /usr/lib/debug/.build-id/89/c19f7bc64e27bdd0c57a4b08e124f8c95799ee.debug Missing separate debuginfo for /lib64/libc.so.6 Try to install the hash file /usr/lib/debug/.build-id/41/259207a6a4da6595a09db78fa98a4118ae0d0e.debug /home/user/example2 Program received signal SIGSEGV, Segmentation fault. 0x00007f4ef9e7ec59 in ?? () from /lib64/libc.so.6 ``` Строки в начале (`Missing separate debuginfo...`) говорят о том, что отладочной информации для системных библиотек недоступно. По этой причине в последней строке написано, что аварийный останов случился, когда выполнялся машинный код в библиотеке `/lib64/libc.so.6`, но соотнести его с местом в исходном коде библиотеки невозможно (`in ?? ()`). Запросим у отладчика *стек вызовов (stack trace),* то есть из какой функции был вызван код, приведший к аварийному останову, из какой функции была вызвана эта функция и так далее вплоть до `main()`. Это делается командой `backtrace` (`bt`): ```gdb (gdb) bt ``` ``` #0 0x00007f58f4c25c59 in ?? () from /lib64/libc.so.6 #1 0x00007f58f4b38e80 in puts () from /lib64/libc.so.6 #2 0x00005578019f114d in print (message=0x0) at example2.c:5 #3 0x00005578019f1181 in main (argc=1, argv=0x7ffc86ac2d88) at example2.c:11 ``` У стека вызовов в данном случае четыре уровня, или *фрейма (frame).* Нижние два (0 и 1) находятся в библиотеке `/lib64/libc.so.6`. Для фрейма 1 известно, что это функция `puts()`, то есть аварийный останов вызван каким-то неизвестным кодом (фрейм 0), который был вызван функцией `puts()`. Больше информации есть о фреймах 2 и 3, относящихся к отлаживаемой программе. Фрейм 2 находится в функции `print()`, конкретно это вызов `puts()` на строке 5 файла `example2.c`. Также показано, что функция вызвана с параметром `message`, который равен 0. Аналогично для фрейма 3 показано, с какими аргументами вызвана `main()`, и откуда конкретно в ней сделан вызов `print()`. Можно переместиться к нужному фрейму, указав его номер: ```gdb (gdb) frame 2 ``` ``` #2 0x00005578019f114d in print (message=0x0) at example2.c:5 5 puts(message); ``` Можно также перемещаться вверх (up) и вниз (down) по стеку вызовов: ```gdb (gdb) up ``` ``` #3 0x00005578019f1181 in main (argc=1, argv=0x7ffc86ac2d88) at example2.c:11 11 print(argv[1]); ``` Пока программа остановлена, можно исследовать значения переменных в ней: ```gdb (gdb) print argv[0] ``` ``` $1 = 0x7ffc86ac4edc "/home/user/example2" ``` ```gdb (gdb) print argv[1] ``` ``` $2 = 0x0 ``` Итак, проблема вызвана тем, что `puts()` передан нулевой указатель как аргумент, а взялся он из `argv[1]`, который равен `NULL`, потому что программа запущена без аргументов (`argc=1`, заполнен только `argv[0]`). Выполните следующую команду, её результат понадобится в дальнейшем: ```gdb (gdb) gcore coredump ``` ``` warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000. Saved corefile coredump ``` Отладку можно завершить: ```gdb (gdb) quit ``` ``` A debugging session is active. Inferior 1 [process 648] will be killed. ``` `Quit anyway? (y or n)` **`y`** ### Отладка post-mortem Имея исполняемый файл с отладочной информацией, можно многое узнать об используемых программой алгоритмах. Это нежелательно, если исходный код программы закрыт. Отладочная информация также существенно увеличивает размер исполняемого файла: ```sh $ ./example1 example2 ``` ``` Size of 'example2' is 26536 bytes ``` По этим причинам разработчики обычно удаляют отладочную информацию, как минимум, чтобы сэкономить место на диске: ```sh $ gcc example2.c -s -o example2-stripped $ ./example1 example2-stripped ``` ``` Size of 'example2-stripped' is 17184 bytes ``` **Задание.** Запустите исполняемый файл `example2-stripped` под отладчиком. Что изменилось в тексте, который GDB выводит при старте? Начините выполнение, и когда управление будет возвращено отладчику, выполните команду `bt`. Что изменилось в её выводе? Можно заметить, что отладить `example2-stripped` фактически невозможно: отладчик работает, но не получится узнать, к какому месту в исходном коде относится фрейм, чему равны локальные переменные и параметры функций. Как отлаживать программу, которая аварийно завершилась у конечного пользователя? Вернемся к тому, что печатала оболочка, когда программа запускалась без GDB: ``` Segmentation fault (core dumped) ``` Слова «segmentation fault» означают, что аварийное завершение случилось по причине некорректного доступа к памяти. Могут быть и другие причины. Слова «core dumped» означают, что система записала слепок памяти процесса на момент сбоя (core dump, «корку»). Будет ли слепок записан и куда именно, зависит от настроек системы и процесса (`man core`). Так или иначе, этот файл можно получить от пользователя, который столкнулся с проблемой. На учебных стендах получить файл, записанный системой, не получится. Однако точно такой же файл был создан ранее командой `gcore` в GDB. Он сохранен в текущей директории в файл `coredump` (аргумент команды). Имея исполняемый файл *с отладочной информацией* и файл core dump, полученный от пользователя, можно начать отладку: ```sh $ gdb -q example2 coredump ``` **Задание.** Какие известные вам команды GDB работают в таком режиме? Какую информацию они выдают (с отладкой `example2` и `example2-stripped`)? Почему некоторые не работают? Иногда нужно отладить программу, установленную из пакета. В большинстве дистрибутивов отладочная информация в пакеты не входит, но есть возможность установить отдельные пакеты с ней. Например, для ALT Linux: . ## strace Иногда программы недостаточно подробно сообщают о том, что пытаются сделать и какие ошибки возникают. Утилита `strace` позволяет проследить все системные вызовы, которые совершает процесс. Например, для программы `true` (ничего не делает и завершается успешно): ```sh $ strace /bin/true ``` ``` execve("/bin/true", ["/bin/true"], 0x7ffe0f8e18f8 /* 10 vars */) = 0 exit_group(0) = ? +++ exited with 0 +++ ``` Процесс совершил два системных вызова: 1) `execve()` для запуска своего исполняемого файла 2) `exit_group()` для своего завершения с заданным кодом (0 — успех) **Задание.** По документации `execve()` выясните, что означают первые два аргумента. Почему второй показан в квадратных скобках? Большинство программ в начале делают много системных вызовов, чтобы загрузить и инициализировать используемые библиотеки, поэтому вывод `strace` от одного их старта занимает десятки строк. На самом деле, вызов `execve()` — это не прямой интерфейс системных вызовов, а вызов функции из библиотеки `libc`, которая делает системный вызов. Часто на один системный вызов приходится несколько функций `libc` схожего назначения. Например, помимо `execve()` для запуска процессов могут использоваться упрощенные формы: `exec()`, `execv()` и другие. Так как `execve()` наиболее общая и имеет в точности те же параметры, что и системный вызов, `exec()` и прочие реализованы через нее, и `strace` не может различить, вызвана `execve()` напрямую или изнутри `exec()`. Получив имя системного вызова с помощью `strace`, имеет смысл сразу уточнить в документации к нему, нет ли более простой функции. Почему нужны обертки из `libc`? Системный вызов — это не просто вызов функции. Вызов функции представляет собой передачу управления коду функции в памяти. Но код функции, которая реализует системный вызов, находится в ядре, а пользовательские процессы не имеют доступа к памяти ядра. Системные вызовы реализуются специальной командой процессора, которая принимает не адрес перехода, а номер системного вызова; параметры передаются особым образом; есть нюансы обработки ошибок. Обертки скрывают эти сложности. Для некоторых новых функций оберток нет. Их требуется вызывать с помощью специальной функции `syscall()`, как описано на соответствующих `man`-страницах. **Задание.** Получите справку по `strace` и попробуйте использовать флаги `-v` и `-o`. **Задание.** Попробуйте использовать `-e`, чтобы скрыть `execve()`. Программа `ltrace` позволяет аналогично отследить вызовы библиотечных функций. Она не рассматривается в лабораторных работах. # Общее контрольное задание 1. Установить на виртуальном сервере компилятор, отладчик и программу для наблюдения системных вызовов, которые совершает процесс. 1. Узнать, какой функцией программа `rm` удаляет файлы (документировать в отчете, как это сделано). Написать программу `lab01-rm`, которая удаляет все файлы, переданные ей как аргументы (обрабатывать каталоги не нужно). 1. Написать программу `lab01-ls`, которая печатает список файлов в каталогах. Каталоги передаются в аргументах программы. Если их не передано, подразумевается текущий каталог. Формат списка: `дата_создания_ГГГГ-ММ-ДД размер_в_байтах имя_файла`. Если файл является каталогом, в конце имени нужно приписать `/`. Некоторые из нужных функций: `opendir()`, `strftime()`. # Контрольные вопросы 1. Чем отличаются ошибки от предупреждений? 1. Зачем включать дополнительные предупреждения компилятора? 1. Известен код ошибки, как узнать его смысл? 1. Как получить список стандартных кодов ошибок, чтобы выбрать подходящий? 1. Программа завершается сразу после запуска с ошибкой "не найден файл настроек". В документации не сказано, где и какой файл она ожидает. Как выяснить это? 1. Разработчик собрал программу и запустил под отладчиком. Однако при останове отладчик вместо имен функций показывает `??` и адреса. В чем проблема и как ее исправить? 1. Разработчик собрал программу и запустил под отладчиком. Однако, когда происходит ошибка и управление передается отладчику, он показывает, что якобы выполнялся не тот код, который вызвал ошибку. В выводе отладчика есть слова "stack frame corrupted". В чем дело и как искать ошибку в этом случае? # Индивидуальные контрольные задания Написанная программа не должна запускать другие программы, если об этом не сказано явно. То есть, если нужно повторить работу `id`, например, написанная программа не должна делать `system("id")` или подобное. Если команде, работу которой нужно повторить, передаются аргументы (`FILE`), они будут передаваться и написанной программе. Опции (`-a`) передаваться не будут, программа просто должна работать так, как работает команда с указанной опцией. Если используемая функция выделяет ресурсы (память, дескриптор), которые требуется освободить в вызывающем коде, это должно быть сделано. Если любая из задействованных функций из системных библиотек вернет ошибку, нужно напечатать сообщение, которое начинается с `ERROR: функция:` и завершить программу с кодом 1. Если программа вызвана неправильно, например, предполагается вызов `program ARG1 ARG2`, а указан только один аргумент, нужно напечатать сообщение, начинающееся на `ERROR:`, и завершить программу с кодом 2. ## Варианты 1. Написать программу, которая работает как команда `id` (без параметров). 1. Написать программу, которая работает как `uname -a`. 1. Написать программу, которая работает как `mv FILE1 FILE2`. 1. Написать программу, которая работает как `truncate FILE SIZE`. 1. Написать программу, которая печатает значение переменной окружения, имя которой передается программе как параметр. 1. Написать программу, которая работает как `date %Y-%m-%d %H:%M:%S`. 1. Написать программу, которая работает как `sleep SECONDS` (достаточно поддержать целое `SECONDS`). 1. Написать программу, которая получает как `mkdir DIR`. В созданный каталог должна быть возможность перейти. 1. Написать программу, которая выполняет команду, состоящую из аргументов (например, `program ls -l` выполняет `ls -l`). 1. Написать программу, которая работает как `pwd`. 1. Написать программу, которая принимает параметр-целое число и печатает время в наносекундах, которое занимает вычисление квадратного корня из этого числа. 1. Написать программу, которая работает как `realpath FILE`. 1. Написать программу, которая работает как `env`, но печатает только переменные, начинающиеся на `P`. 1. Написать программу, которая работает как `ln -s FROM TO`. 1. Написать программу, которая выводит максимально возможное количество процессов (аналог `prlimit -u`; «жесткий» лимит; `UNLIMITED`, если нет).