# Практическая работа 1: Подсистемы Linux - Системные вызовы, Управление памятью, Управление процессами, Сетевая подсистема.
## Цели работы:
1) Научиться просматривать и анализировать системные вызовы.
2) Научиться работать с процессами: узнавать, какие процессы запущены, как они распоряжаются памятью системы, останавливать процессы.
3) Узнать сетевые интерфейсы системы, их ip-адреса.
## 1. Теоретические материалы к работе:
### 1.1 Что такое ядро
Как известно, компьютер – не только система аппаратного обеспечения (железа), но и набор работающего на нем программного обеспечения. Чтобы второе могло эффективно работать на первом, нужна более низкоуровневая программа, скрывающая сложности работы с железом и предоставляющая обычным программам и пользователям удобный для них интерфейс.
Железо говорит на языке сигналов, регистров, секторов, переводов головок. Программам все это не надо. Они говорят на языке "записать, прочитать, сложить, вычесть ...". Специальной программой, обеспечивающей остальным простой и понятный интерфейс для работы на имеющемся оборудовании, является ядро операционной системы.
Представим себе, что ядра нет, а каждая запущенная программа сама обращается к железу и обрабатывает сигналы от него. Вроде бы ничего страшного, кроме дублирования кода в каждой такой программе. Но на компьютере одновременно работает множество программ. Как они будут "договариваться" между собой о совместном использовании общего аппаратного обеспечения?
Конечно, они могут встать в очередь, и сначала одна программа выполнится полностью, затем другая. Однако одни программы должны работать постоянно в фоновом режиме, другие – могут долго ожидать ввода или вывода, третьи – должны получать данные из другой работающей программы. Поэтому функция оптимального распределения аппаратных ресурсов возлагается на ядро. Оно организует как бы параллельную работу множества программ, играет роль менеджера.
Ядро операционной системы – это тоже программа, написанная на том или ином языке программирования и скомпилированная в исполняемый файл. Однако, в отличии от других программ, ядро всегда загружается первым и потом постоянно "сидит" в определенной области оперативной памяти. То есть это программа, которая всегда находится в запущенном состоянии и взаимодействует, с одной стороны, с железом, а с другой – с системными и пользовательскими программами.
Выделяют операционные системы на монолитном ядре и микроядре, а также разные промежуточные варианты. Монолитное ядро проще и быстрее работает, так как в памяти всегда находится почти весь код. Микроядро меньше, сложнее, работает медленнее, однако нередко считается более передовым из-за легкости подключения новых частей кода. Микроядро, находясь в памяти, организует взаимодействие между другими частями кода операционной системы, которые являются самостоятельными программами.
Ядро Unix являлось первой практической реализацией новых идей и открытий 60-70-х годов XX века в области создания операционных систем.
Unix имеет простое монолитное ядро, в котором почти все представляется в виде файлов. Настройки хранятся в текстовых файлах, оборудование также имеет файловый интерфейс. Unix была написана на языке C, и это сделало ее переносимой с одной аппаратной платформы на другую. В Unix были впервые реализованы так называемые многозадачность и многопоточность, виртуальная память и многое другое.
В 80-х годах Unix-системы начали множится и видоизменяться. Некоторые умы вовремя спохватились и создали специальные стандарты, обеспечивающие совместимость систем. Это значит, что программа, написанная для одной Unix-подобной системы, должна работать в другой. Стандарты назвали POSIX.
### 1.2 Системные вызовы
Системный вызов — это функция, которая позволяет процессу взаимодействовать с ядром Linux. Это просто программный способ для компьютерной программы заказать средство из ядра операционной системы. Системные вызовы предоставляют ресурсы операционной системы пользовательским программам через API (интерфейс прикладного программирования). Системные вызовы могут обращаться только к структуре ядра. Они необходимы для всех служб, которым требуются ресурсы.
Пользователи при повседневной работе обычно используют утилиты командной строки и графический интерфейс (`GUI`). При этом в фоне незаметно работают системные вызовы, обращаясь к ядру для выполнения работы.
Системные вызовы очень похожи на вызовы функций, в том смысле, что в них передаются аргументы и они возвращают значения. Единственное отличие состоит в том, что системные вызовы работают на уровне ядра, а функции нет. Переключение из пользовательского режима в режим ядра осуществляется с помощью специального механизма прерываний.
Большая часть этих деталей скрыта от пользователя в системных библиотеках (`glibc` в Linux-системах). Системные вызовы по своей природе являются универсальными, но несмотря на это, механика их выполнения во многом аппаратно-зависима.
Все системные вызовы можно разделить на следующие категории:
* Управление процессами
* Управление файлами
* Управление каталогами и файловой системой
* Прочие
### 1.3 Подсистема управления процессами
Управление процессами в многозадачной системе заключается в выделении ресурсов ядра для каждого запущенного процесса, осуществлении переключения контекста процессов и синхронизации выполнения многих процессов в системе даже при наличии только одного центрального процессора.
Планировщик заданий — алгоритм ядра, обслуживающий очередь процессов, выделяющий кванты времени ЦП для каждого процесса и обеспечивающий переключение контекста с учетом приоритетов процессов.
Программа или команда в период выполнения представляется в ОС как процесс. Структуры данных ядра, переменные окружения и другие параметры процесса, включая виртуальное адресное пространство называется контекстом процесса. Параметры процесса сохраняются на время жизни процесса в виртуальной файловой структуре `/proc` - создается соответствующий виртуальный каталог с именем PID процесса.
При загрузке системы инициируется процесс `init` c PID=1, который порождает другие процессы, например, логон процесс для регистрации пользователя в системе. При входе в систему инициируется процесс исполняющий интерпретатор `shell`. При запуске любой программы или команды вы порождаете процесс-потомок интерпретатора и т.д. Таким образом создается иерархия процессов в многозадачной ОС.
Особенностью систем UNIX является наследование процессом-потомком контекста процесса родителя. Применение техники «копирование-при-записи» позволяет ускорить порождение нового процесса.
### 1.4 Подсистема управления памятью
Каждый процесс выполняется в своем собственном адресном пространстве. Состояние процесса в каждый момент времени описывается его контекстом, в который включаются
* адресное пространство,
* текущее состояние регистров, в частности, счетчик команд PC,
* переменные окружения: `PATH, HOME, PWD` и др.
* маска сигналов,
* таблица открытых файлов процесса,
* `PID, UID, GID`, и некоторые другие идентификаторы и структуры данных
Адресное пространство процесса это весь набор доступных виртуальных адресов, например, для 32-разрядной архитектуры от 0 до 232-1=4294967295=4Гб. Для обслуживания адресных пространств всех процессов в системе существует специальный процесс `swap`, реализующий специальный алгоритм выгрузки данных из физической памяти в swap-область на диске и загрузки их обратно по требованию. Для оптимизации свопинга, обмен между диском и физической памятью осуществляется непрерывными фрагментами адресного пространства, обычно по 4К, называемыми страницами памяти. В процессорах i386 и выше существует аппаратная поддержка виртуальной памяти — MMU – `memory management unit`, которая интенсивно используется ядром ОС.
При выполнении приложения процесс может находиться в режиме пользователя или в режиме ядра, когда выполняется системный вызов. Поэтому, все адресное пространство любого процесса делится на две области: область пользователя — доступная в режиме пользователя, и область ядра — доступная в режиме ядра.
При загрузке исполняемого модуля формата ELF в адресное пространство процесса страницы области пользователя распределяются по четырем разделам:
`TEXT`, `DATA`, `bss` и `stack`. Причем, раздел текст доступен только для чтения, остальные разделы - для чтения и записи. Аналогичным образом ядро использует область ядра, которая является общей для всех процессов и поэтому для каждого процесса создается отдельный стек в области ядра.
## Методические указания к выполнению:
Для просмотра системных вызовов будем использовать утилиту командной строки `strace`. Установка:
```
user@debian:~$ apt install strace
```
Теперь создадим тестовую директорию и пару файлов в ней:
```
user@debian:~$ mkdir /tmp/calls
user@debian:~$ cd /tmp/calls
user@debian:/tmp/calls$ touch file1
user@debian:/tmp/calls$ touch file2
```
Теперь посмотрим содержимое папки с помощью команды `ls`:
```
user@debian:/tmp$ ls calls
file1 file2
```
Как мы и ожидали, в терминале вывелись два только что созданных файла. Но что в момент выполнения команды произошло на более глубоком уровне?
Здесь в игру вступает абстракция. Вот как работает эта команда:
*Утилита командной строки -> Функции системных библиотек (glibc) -> Системные вызовы*
Команда ls вызывает функции из системных библиотек Linux (glibc). Эти библиотеки, в свою очередь, вызывают системные вызовы, которые выполняют большую часть работы.
Если вы хотите узнать, какие функции вызывались из библиотеки glibc, то используйте команду strace со следующей за ней командой ls calls. Но в данной практике нас интересуют системные вызовы, которые используются функциями системных библиотек.
Посмотрим их в помощью следующей команды:
```
user@debian:~$ strace ls /tmp/calls
execve("/usr/bin/ls", ["ls", "/tmp/calls"], 0x7ffd637abdf8 /* 39 vars */) = 0
...
...
...
write(1, "file1 file2\n", 13file1 file2
) = 13
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
```
Вывод весьма объемный, поэтому для упрощения его анализа запишем его в файл:
```
user@debian:~$ strace -o trace.log ls /tmp/calls
file1 file2
```
Весь вывод будет теперь будет в файле trace.log. Давайте разбираться что там. Посмотрим вызовы, в которых упоминается /tmp/calls:
```
user@debian:/home/user# grep /tmp/calls trace.log
execve("/usr/bin/ls", ["ls", "/tmp/calls"], 0x7ffe873d3e28 /* 39 vars */) = 0
statx(AT_FDCWD, "/tmp/calls", AT_STATX_SYNC_AS_STAT, STATX_MODE, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFDIR|0755, stx_size=4096, ...}) = 0
openat(AT_FDCWD, "/tmp/calls", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
```
Взглянем на первую строку:
```
execve("/usr/bin/ls", ["ls", "/tmp/calls"], 0x7ffd637abdf8 /* 39 vars */) = 0
```
* В начале строки находится имя выполняемого системного вызова — это **execve**.
* Текст в круглых скобках — это аргументы, передаваемые системному вызову.
* Число после знака = (в данном случае 0) — это значение, возвращаемое системным вызовом.
Запоминать все системные вызовы не обязательно (и не очень просто, в Linux около 380 системных вызовов), так как посмотреть что делает тот или иной вызов можно с помощью man. (нужно убедиться что пакет manpages-dev установлен, либо доустановить его командой `sudo apt install manpages-dev`).
Ниже приведены номера разделов man:
```
1. Выполняемые программы или команды для командной оболочки.
2. Системные вызовы (функции, предоставляемые ядром).
3. Библиотечные вызовы (функции программных библиотек).
4. Специальные файлы (которые обычно находятся в /dev).
```
```
root@debian:/home# man 2 execve
```
Нам откроется справка, в которой, помимо прочей информации будет описание вызова:
```
DESCRIPTION
execve() executes the program referred to by pathname. This causes the program that is currently being run by the calling process to be replaced with a
new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.
```
В соответствии с документацией системный вызов execve выполняет программу, которая передается ему в параметрах (в данном случае это ls). В него также передаются дополнительные параметры для ls. В этом примере это /tmp/calls. Следовательно, этот системный вызов просто запускает ls с /tmp/calls в качестве параметра.
В следующий системный вызов stat передается параметр /tmp/calls:
```
statx(AT_FDCWD, "/tmp/calls", AT_STATX_SYNC_AS_STAT, STATX_MODE, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFDIR|0755, stx_size=4096, ...}) = 0
```
Для просмотра документации используйте man 2 statx. Системный вызов **statx** возвращает информацию об указанном файле. Помните, что все в Linux — файл, включая каталоги.
Далее системный вызов **openat** открывает /tmp/calls. Обратите внимание, что возвращается значение 3. Это дескриптор файла, который будет использоваться в последующих системных вызовах.
Посмотрим на другие системные вызовы в файле trace.log. Вы увидите системный вызов getdents, который и делает большую часть необходимой работы для выполнения команды ls /tmp/calls. Теперь выполним grep getdents для файла trace.log:
```
user@debian:/home/user# grep getdents trace.log
getdents64(3, 0x5633b130ea30 /* 4 entries */, 32768) = 112
getdents64(3, 0x5633b130ea30 /* 0 entries */, 32768) = 0
```
В документации говорится, что getdents читает записи каталога, это, собственно, нам и нужно. Обратите внимание, что аргумент для getdent равен 3 — это дескриптор файла, полученный ранее от системного вызова openat.
Теперь, когда получено содержимое каталога, нужен способ отобразить информацию в терминале. Делаем grep для другого системного вызова write, который используется для вывода на терминал:
```
user@debian:/home/user# grep write trace.log
write(1, "file1 file2\n", 13) = 13
```
В аргументах вы можете видеть имена файлов, которые будут выводится: file1 и file2. Что касается первого аргумента (1), вспомните, что в Linux для любого процесса по умолчанию открываются три файловых дескриптора:
0 — стандартный поток ввода
1 — стандартный поток вывода
2 — стандартный поток ошибок
Таким образом, системный вызов write выводит file1 и file2 на стандартный вывод, которым является терминал, обозначаемый числом 1. Возвращаемое вызовом значение (13) - это количество выведенных байт.
Теперь вы знаете, какие системные вызовы сделали большую часть работы для команды ls /tmp/calls. Но что насчет других 100+ системных вызовов в файле trace.log?
Операционная система выполняет много вспомогательных действий для запуска процесса, поэтому многое из того, что вы видите в файле trace.log — это инициализация и очистка процесса.
Теперь вы можете анализировать системные вызовы для любых программ. Утилита strace так же предоставляет множество полезных параметров командной строки, некоторые из которых описаны ниже.
По умолчанию strace отображает не всю информацию о системных вызовах. Однако у нее есть опция `-v verbose`, которая покажет дополнительную информацию о каждом системном вызове:
`root@debian:/tmp/calls# strace -v ls strace -v ls`
Хорошая практика использовать параметр -f для отслеживания дочерних процессов, созданных запущенным процессом:
`root@debian:/tmp/calls# strace -f ls`
А если вам нужны только имена системных вызовов, количество их запусков и процент времени, затраченного на выполнение? Вы можете использовать опцию -c, чтобы получить эту статистику:
`root@debian:/tmp/calls# strace -c ls`
Если вы хотите отследить определенный системный вызов, например, open, и проигнорировать другие, то можно использовать опцию -e с именем системного вызова:
```
root@debian:/tmp/calls# strace -e openat ls
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpcre2-8.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/filesystems", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
file1 file2
+++ exited with 0 +++
```
До сих пор мы отслеживали только явный запуск команд. Но как насчет команд, которые были запущены ранее? Что, если вы хотите отслеживать демонов? Для этого у strace есть специальная опция -p, которой вы можете передать идентификатор процесса.
Мы не будем запускать демона, а используем команду cat, которая отображает содержимое файла, переданного ему в качестве аргумента. Но если аргумент не указать, то команда cat будет просто ждать ввод от пользователя. После ввода текста она выведет введенный текст на экран. И так до тех пор, пока пользователь не нажмет Ctrl+C для выхода.
Запустите команду cat на одном терминале.
```
root@debian:/# cat
```
На другом терминале найдите идентификатор процесса (PID) с помощью команды ps:
```
root@debian:/# ps -ef | grep cat
```
Теперь запустите strace с опцией -p и PID'ом, который вы нашли с помощью ps. После запуска strace выведет информацию о процессе, к которому он подключился, а также его PID. Теперь strace отслеживает системные вызовы, выполняемые командой cat. Первый системный вызов, который вы увидите — это read, ожидающий ввода от потока с номером 0, то есть от стандартного ввода, который сейчас является терминалом, на котором запущена команда cat:
```
root@debian:/# strace -p 2417
strace: Process 2417 attached
read(0,
```
Теперь вернитесь к терминалу, где вы оставили запущенную команду cat, и введите какой-нибудь текст:
```
root@debian:/tmp/calls# cat
123
123 //вывод команды cat
```
Вернитесь к терминалу, где strace был подключен к процессу cat. Теперь вы видите два новых системных вызова: предыдущий read, который теперь прочитал ввод, и еще один для записи write, который записывает ввод обратно в терминал, и снова новый read, который ожидает чтения с терминала:
```
root@debian:/# strace -p 2417
strace: Process 2417 attached
read(0, "123\n", 131072) = 4
write(1, "123\n", 4) = 4
read(0,
```
В данном примере мы использовали PID процесса, чтобы получить информацию о нем. Однако зная PID, мы можем выяснить гораздо больше, чем системные вызовы, используемые процессом. К примеру, мы можем посмотреть как процесс распоряжается памятью. Для этого можно посмотреть информацию из файла /proc/<PID>/maps (или воспользоваться команда pmap PID):
```
root@debian:/# cat /proc/1/maps
55a5ce409000-55a5ce43f000 r--p 00000000 08:01 787234 /usr/lib/systemd/systemd
55a5ce43f000-55a5ce50b000 r-xp 00036000 08:01 787234 /usr/lib/systemd/systemd
55a5ce50b000-55a5ce568000 r--p 00102000 08:01 787234 /usr/lib/systemd/systemd
...
```
Поля в каждой строке имеют следующий формат:
`start-end perm offset major:minor inode image`
где:
`start-end`
Начало и окончание виртуальных адресов для этой области памяти.
`perm`
Битовая маска с разрешениями для области памяти на чтение, запись и исполнение. Это поле описывает, что процессу разрешено делать со страницами, которые принадлежат этой области. Последний символ в поле - это либо p для "закрытых" (“private”), или s для "общих" (“shared”).
`offset`
Где начинается область памяти в файле, с которым она связана. Смещение 0 означает, что начало области памяти соответствует началу файла.
`major:minor`
Старший и младший номера устройства удерживающего файл, который был на отображён. Как ни странно, при отображении устройства, старший и младший номера ссылаются на дисковый раздел, содержащий специальный файл устройства, который был открыт пользователем, а не самого устройства.
`inode`
Номер inode (структура данных в которой хранится информация о файле или директории в файловой системе.) отображённого файла.
`image`
Имя файла (обычно это исполняемый файл), который был отображён.
Теперь давайте посмотрим какие процессы сейчас выполняются в системе:
```
root@debian:/# ps
PID TTY TIME CMD
2234 pts/0 00:00:00 su
2235 pts/0 00:00:00 bash
2236 pts/0 00:00:00 ps
```
Запуская любую программу в командной строке вы порождаете новый процесс, являющийся потомком текущего shell процесса. Новый процесс получает PID, увеличивая на единицу максимальный PID всех ранее существующих процессов в системе, и остальной контекст, сохраняемый в файловой системе /proc в подкаталоге с именем равном PID.
В команде ps можно использовать три типа опций: UNIX – с одним дефисом, BSD – без дефисов и
Linux – с двойным дефисом. Комбинации различных опций позволяют получить информацию о процессах в системе различного уровня детализации и формата. Например, для вывода информации обо всех процессах в системе воспользуйтесь командами:
```
$ ps -e
$ ps -ef
$ ps -ely
```
Текущее состояние всех процессов можно также увидеть в динамически обновляемом в реальном
времени экранном представлении с помощью команды top:
```
root@debian:/# top
top - 04:32:12 up 3 min, 1 user, load average: 0.67, 0.64, 0.30
Tasks: 205 total, 1 running, 204 sleeping, 0 stopped, 0 zombie
%Cpu(s): 1.7 us, 0.0 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 1946.6 total, 193.2 free, 936.0 used, 817.4 buff/cache
MiB Swap: 975.0 total, 974.0 free, 1.0 used. 845.3 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1205 user 20 0 3416284 278124 116388 S 2.3 14.0 0:05.76 gnome-s+
513 root 20 0 0 0 0 I 0.3 0.0 0:00.44 kworker+
2065 user 20 0 400876 47636 36236 S 0.3 2.4 0:00.56 gnome-t+
...
```
Каждый процесс может выполняться в интерактивном (foreground) или фоновом (background) режиме. Процесс может быть приостановлен сигналом SIGSTOP или SIGTSTP, посылаемым стерминала интерактивному процессу нажатием клавиш Ctrl-Z. При запуске в фоновом режиме или при остановке процесса порождаются «задания» (jobs), список которых выводится по команде
`$ jobs`
Номер задания указан в квадратных скобках и может использоваться для перевода задания в
интерактивный режим (здесь N – номер задания)
`$ fg %N`
или в фоновый
`$ bg %N`
Приоритеты процессов имеют 40 градаций: от -20 - самый высокий, имеющий преимущество в
очереди планировщика, до 19 - самый низкий.. По-умолчанию, приоритет процесса обычного пользователя равен 0, и пользователь может только понизить приоритет, добавляя положительное число (по-умолчанию, N=10) в параметрах команды nice
`$ nice -n N команда`
Принудительно остановить процесс можно командой kill PID, например
`$ kill 1234`
остановит процесс с PID=1234.
Ключевым объектом сетевой подсистемы Linux является интерфейс. Сетевой интерфейс в Linux – это абстрактный именованный объект, используемый для передачи данных через некоторую линию связи.
Например, если в системе существует интерфейс eth0, то в большинстве случаев на современных компьютерах он сопоставлен Ethernet-адаптеру, встроенному в материнскую плату. Интерфейс с именем ppp0 отвечает за некоторое соединение «точка-точка» с другим компьютером. Интерфейс с именем lo является виртуальным и представляет как бы замкнутый сам на себя (вход непосредственно подключен к выходу) сетевой адаптер.
Основная задача интерфейса – абстрагироваться от физической составляющей канала. То есть программы и система будут использовать один и тот же метод «отправить пакет» для отправки данных через любой интерфейс – хоть lo, хоть ethX, хоть pppY, и точно так же использовать один и тот же метод «принять пакет» – то есть создается унифицированный API передачи данных, независимый от носителя.
Для того, чтобы ознакомиться с интерфейсами, можно воспользоваться командой ifconfig:
```
root@debian:/# sudo ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.183.221 netmask 255.255.255.0 broadcast 192.168.183.255
inet6 fe80::20c:29ff:fe9a:220c prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:9a:22:0c txqueuelen 1000 (Ethernet)
RX packets 29182 bytes 36843502 (35.1 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4305 bytes 343647 (335.5 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 103 bytes 9074 (8.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 103 bytes 9074 (8.8 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
```
Из вывода мы видим, что в системе два интерфейса: ens33 с ip-адресом 192.168.183.221 и loopback интерфейс 127.0.0.1.