43 KiB
title | lang |
---|---|
Системное программирование в Linux | 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 (больше, чем в стандарте).
Документацию не обязательно читать в терминале:
https://linux.die.net/man/,
https://www.opennet.ru/man.shtml.
Однако man
соответствует конкретному дистрибутиву Linux и версиям его пакетов,
а часть сведений с сайтов может быть неприменима к данной системе.
Документацию лучше читать в оригинале (обычно на английском), чтобы не ждать перевода и не страдать из-за ошибок переводчика. Автоматические переводчики тоже иногда ошибаются и искажают смысл.
Узнать о том, что какая-то функция вообще есть, можно из учебников и новостей. Популярные источники новостей: https://opennet.ru (на русском), https://lwn.net (на английском).
Выполнение лабораторной работы
Чтобы изучить на примере, как программировать с использованием системных API,
напишем программу example1
, которая печатает размеры файлов,
имена которых передаются ей как аргументы при запуске:
$ ./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
из <iostream>
нужно пользоваться scanf()
и printf()
из <stdio.h>
.
На практике C++ для системного программирования активно используют,
но только в пространстве пользователя.
Работа с аргументами программы
Создайте файл example1.c
, введите в него следующий код и сохраните:
#include <stdio.h>
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()
,
обратитесь к документации
и примеру в ней.
Задание. Ответьте на вопросы для самопроверки:
- Чему будет равно
argc
при запуске программы как./example1 foo bar
? - Какие значения находятся в массиве
argv
при таком запуске? - Почему в коде выше цикла начинается с 1, а не с 0?
- Как называется первый параметр функции
printf()
? - Что означают
%s
и%d
в нем? - Почему выбраны именно
%s
и%d
?
Компиляция
Соберите исполняемый файл example1
из исходного кода в example1.c
:
gcc example1.c -o example1
Запустите программу:
./example1 example1.c example1
Изучение документации
Получить сведения о файле, в частности, его размер, можно функцией stat()
.
NAME
stat, fstat, lstat, fstatat - get file status
Одна man
-страница часто содержит описание семейства функций,
в данном случае четырех функций для получения статуса файла (сведений о нем).
SYNOPSIS
#include <sys/stat.h>
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
объявленной переменной.
Задание. Модифицируйте код, соберите программу и запустите её так же, как в прошлый раз.
#include <stdio.h>
#include <sys/stat.h>
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
и аналогично с другими кодами для других причин ошибок.
Значит, при вызове функции нужно проверить возвращаемое значение, Если оно отрицательное, то обработать ошибку, например, напечатать сообщение о ней с кодом:
int ret = stat(...);
if (ret < 0) {
printf("stat(): errno=%d\n", errno);
continue;
}
Задание. Добавьте в программу обработку ошибок. Соберите программу. Проверьте, как программа реагирует на несуществующий файл.
Коды ошибок трудно и не нужно помнить наизусть. Есть функции strerror(3), perror(3) и другие, чтобы по коду ошибки получить или напечатать её текстовое описание для пользователя.
Программа errno(1) позволяет получить описание ошибки по коду (эта программа по умолчанию не установлена):
% errno 12
ENOMEM 12 Cannot allocate memory
Исправление ошибок и предупреждений компилятора
Заменим for (i = 1; i < argc…
на for (i = 1; i < argd…
(argc
→ argd
),
как будто допущена опечатка, и повторим сборку:
$ 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, но могут привести к неправильной работе программы. Показ дополнительных предупреждений включается флагами компилятора:
$ 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
со следующей программой:
#include <stdio.h>
void
print(const char* message) {
puts(message);
}
int
main(int argc, char** argv) {
print(argv[0]);
print(argv[1]);
print(argv[2]);
}
Отладчику нужны сведения о том, как машинный код в исполняемом файле
соотносится с исходным текстом, где в памяти размещены переменные и т. п.
По умолчанию эта отладочная информация не включается в исполняемый файл.
Чтобы компилятор добавил ее, нужен ключ -g
при компиляции:
$ gcc example2.c -g -o example2
Запустите программу:
$ ./example2
./example2
Segmentation fault (core dumped)
Программа аварийно завершается («падает»).
Первую строку, ./example2
, программа напечатала сама — это argv[0]
.
Вторую строку напечатала оболочка, её смысл мы разберем далее.
Запустим программу под отладчиком, чтобы найти причину аварийного завершения:
$ gdb -q --args ./example2
Reading symbols from example2...
(gdb)
Вместо приглашения командной оболочки
появилось приглашение командной строки отладчика, обозначаемое (gdb)
.
Отладчик не запускает программу сразу, это нужно сделать командой run
(GDB также воспринимает сокращения команд, например, r
вместо run
):
(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) 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) frame 2
#2 0x00005578019f114d in print (message=0x0) at example2.c:5
5 puts(message);
Можно также перемещаться вверх (up) и вниз (down) по стеку вызовов:
(gdb) up
#3 0x00005578019f1181 in main (argc=1, argv=0x7ffc86ac2d88) at example2.c:11
11 print(argv[1]);
Пока программа остановлена, можно исследовать значения переменных в ней:
(gdb) print argv[0]
$1 = 0x7ffc86ac4edc "/home/user/example2"
(gdb) print argv[1]
$2 = 0x0
Итак, проблема вызвана тем, что puts()
передан нулевой указатель как аргумент,
а взялся он из argv[1]
, который равен NULL
, потому что программа
запущена без аргументов (argc=1
, заполнен только argv[0]
).
Выполните следующую команду, её результат понадобится в дальнейшем:
(gdb) gcore coredump
warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000.
Saved corefile coredump
Отладку можно завершить:
(gdb) quit
A debugging session is active.
Inferior 1 [process 648] will be killed.
Quit anyway? (y or n)
y
Отладка post-mortem
Имея исполняемый файл с отладочной информацией, можно многое узнать об используемых программой алгоритмах. Это нежелательно, если исходный код программы закрыт.
Отладочная информация также существенно увеличивает размер исполняемого файла:
$ ./example1 example2
Size of 'example2' is 26536 bytes
По этим причинам разработчики обычно удаляют отладочную информацию, как минимум, чтобы сэкономить место на диске:
$ 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, полученный от пользователя, можно начать отладку:
$ gdb -q example2 coredump
Задание.
Какие известные вам команды GDB работают в таком режиме?
Какую информацию они выдают (с отладкой example2
и example2-stripped
)?
Почему некоторые не работают?
Иногда нужно отладить программу, установленную из пакета. В большинстве дистрибутивов отладочная информация в пакеты не входит, но есть возможность установить отдельные пакеты с ней. Например, для ALT Linux: https://www.altlinux.org/Debuginfo.
strace
Иногда программы недостаточно подробно сообщают о том,
что пытаются сделать и какие ошибки возникают.
Утилита strace
позволяет проследить все системные вызовы,
которые совершает процесс.
Например, для программы true
(ничего не делает и завершается успешно):
$ strace /bin/true
execve("/bin/true", ["/bin/true"], 0x7ffe0f8e18f8 /* 10 vars */) = 0
exit_group(0) = ?
+++ exited with 0 +++
Процесс совершил два системных вызова:
execve()
для запуска своего исполняемого файла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
позволяет аналогично отследить вызовы библиотечных функций.
Она не рассматривается в лабораторных работах.
Общее контрольное задание
-
Установить на виртуальном сервере компилятор, отладчик и программу для наблюдения системных вызовов, которые совершает процесс.
-
Узнать, какой функцией программа
rm
удаляет файлы (документировать в отчете, как это сделано). Написать программуlab01-rm
, которая удаляет все файлы, переданные ей как аргументы (обрабатывать каталоги не нужно). -
Написать программу
lab01-ls
, которая печатает список файлов в каталогах. Каталоги передаются в аргументах программы. Если их не передано, подразумевается текущий каталог. Формат списка:дата_создания_ГГГГ-ММ-ДД размер_в_байтах имя_файла
. Если файл является каталогом, в конце имени нужно приписать/
. Некоторые из нужных функций:opendir()
,strftime()
.
Контрольные вопросы
-
Чем отличаются ошибки от предупреждений?
-
Зачем включать дополнительные предупреждения компилятора?
-
Известен код ошибки, как узнать его смысл?
-
Как получить список стандартных кодов ошибок, чтобы выбрать подходящий?
-
Программа завершается сразу после запуска с ошибкой "не найден файл настроек". В документации не сказано, где и какой файл она ожидает. Как выяснить это?
-
Разработчик собрал программу и запустил под отладчиком. Однако при останове отладчик вместо имен функций показывает
??
и адреса. В чем проблема и как ее исправить? -
Разработчик собрал программу и запустил под отладчиком. Однако, когда происходит ошибка и управление передается отладчику, он показывает, что якобы выполнялся не тот код, который вызвал ошибку. В выводе отладчика есть слова "stack frame corrupted". В чем дело и как искать ошибку в этом случае?
Индивидуальные контрольные задания
Написанная программа не должна запускать другие программы,
если об этом не сказано явно.
То есть, если нужно повторить работу id
, например,
написанная программа не должна делать system("id")
или подобное.
Если команде, работу которой нужно повторить, передаются аргументы (FILE
),
они будут передаваться и написанной программе.
Опции (-a
) передаваться не будут, программа просто должна работать так,
как работает команда с указанной опцией.
Если используемая функция выделяет ресурсы (память, дескриптор), которые требуется освободить в вызывающем коде, это должно быть сделано.
Если любая из задействованных функций из системных библиотек вернет ошибку,
нужно напечатать сообщение, которое начинается с ERROR: функция:
и завершить программу с кодом 1.
Если программа вызвана неправильно,
например, предполагается вызов program ARG1 ARG2
,
а указан только один аргумент,
нужно напечатать сообщение, начинающееся на ERROR:
,
и завершить программу с кодом 2.
Варианты
-
Написать программу, которая работает как команда
id
(без параметров). -
Написать программу, которая работает как
uname -a
. -
Написать программу, которая работает как
mv FILE1 FILE2
. -
Написать программу, которая работает как
truncate FILE SIZE
. -
Написать программу, которая печатает значение переменной окружения, имя которой передается программе как параметр.
-
Написать программу, которая работает как
date %Y-%m-%d %H:%M:%S
. -
Написать программу, которая работает как
sleep SECONDS
(достаточно поддержать целоеSECONDS
). -
Написать программу, которая получает как
mkdir DIR
. В созданный каталог должна быть возможность перейти. -
Написать программу, которая выполняет команду, состоящую из аргументов (например,
program ls -l
выполняетls -l
). -
Написать программу, которая работает как
pwd
. -
Написать программу, которая принимает параметр-целое число и печатает время в наносекундах, которое занимает вычисление квадратного корня из этого числа.
-
Написать программу, которая работает как
realpath FILE
. -
Написать программу, которая работает как
env
, но печатает только переменные, начинающиеся наP
. -
Написать программу, которая работает как
ln -s FROM TO
. -
Написать программу, которая выводит максимально возможное количество процессов (аналог
prlimit -u
; «жесткий» лимит;UNLIMITED
, если нет).