Содержание
- Разработка под KasperskyOS
- Запуск процессов
- Файловые системы и сеть
- Состав компонента VFS
- Создание IPC-канала до VFS
- Сборка исполняемого файла VFS
- Объединение клиента и VFS в один исполняемый файл
- Обзор: аргументы и переменные окружения VFS
- Монтирование файловой системы при старте
- Разделение файловых и сетевых вызовов с помощью бэкендов VFS
- Написание пользовательского бэкенда VFS
- IPC и транспорт
Обзор: Einit и init.yaml
Инициализирующая программа Einit
При старте ядро KasperskyOS находит в образе решения и запускает исполняемый файл с именем Einit
(инициализирующая программа). Запущенный процесс имеет класс Einit
и, как правило, используется для запуска остальных процессов, которые требуются в момент старта решения.
Генерация C-кода инициализирующей программы
В составе пакета инструментов KasperskyOS Community Edition поставляется утилита einit
, которая позволяет сгенерировать C-код инициализирующей программы на основе init-описания (файл с описанием обычно имеет имя init.yaml
). Полученная программа использует KasperskyOS API для выполнения следующих действий:
- статическое создание и запуск процессов;
- статическое создание IPC-каналов.
Стандартным способом использования утилиты einit
является интеграция ее вызова в один из шагов сборочного скрипта, в результате которого утилита einit
на основе файла init.yaml
сгенерирует файл einit.c
, содержащий код инициализирующей программы. На одном из следующих шагов сборочного скрипта необходимо скомпилировать файл einit.c
в исполняемый файл Einit
и включить в образ решения.
Для инициализирующей программы не требуется создавать файлы статических описаний. Эти файлы поставляются в составе пакета инструментов KasperskyOS Community Edition и автоматически подключаются при сборке решения. Однако класс процессов Einit
должен быть описан в файле security.psl
.
Синтаксис init.yaml
Init-описание содержит данные в формате YAML, которые идентифицируют:
- процессы, запускаемые при загрузке KasperskyOS;
- IPC-каналы, используемые процессами для взаимодействия между собой.
Эти данные представляют собой словарь с ключом entities
, содержащий список словарей процессов. Ключи словаря процесса приведены в таблице ниже.
Ключи словаря процесса в init-описании
Ключ |
Обязательный |
Значение |
---|---|---|
|
Да |
Класс безопасности процесса |
|
Нет |
Имя процесса. Если его не указывать, то будет взято имя класса безопасности. У каждого процесса должно быть уникальное имя. Можно запустить несколько процессов одного класса безопасности, но с разными именами. |
|
Нет |
Имя исполняемого файла в ROMFS (в образе решения), из которого будет запущен процесс. Если его не указывать, то будет взято имя класса безопасности без "префиксов" и точек. Например, процессы классов безопасности Можно запустить несколько процессов из одного исполняемого файла. |
|
Нет |
Список словарей IPC-каналов процесса. Этот список задает статически создаваемые IPC-каналы, клиентскими дескрипторами которых будет владеть процесс. По умолчанию список пуст. (Помимо статически создаваемых IPC-каналов процессы могут использовать динамически создаваемые IPC-каналы.) |
|
Нет |
Список аргументов, передаваемых процессу (функции |
|
Нет |
Словарь переменных окружения, передаваемых процессу. Ключами в этом словаре являются имена переменных, которым сопоставлены передаваемые значения. Максимальный размер значения – 1024 байта. |
Ключи словаря IPC-канала процесса приведены в таблице ниже.
Ключи словаря IPC-канала в init-описании
Ключ |
Обязательный |
Значение |
---|---|---|
|
Да |
Имя IPC-канала, которое может быть задано как конкретным значением, так и ссылкой вида
|
|
Да |
Имя процесса, который будет владеть серверным дескриптором IPC-канала. |
Примеры init-описаний
Здесь собраны init-описания, демонстрирующие различные аспекты запуска процессов.
В примерах в составе KasperskyOS Community Edition может использоваться формат init-описания с макросами (init.yaml.in
).
Файл с init-описанием обычно называется init.yaml
, хотя может иметь любое имя.
Соединение и запуск процесса-клиента и процесса-сервера
В следующем примере будут запущены два процесса: класса Client
и класса Server
. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов. Имена исполняемых файлов также не указаны, они также будут совпадать с именами классов. Процессы будут соединены IPC-каналом с именем server_connection
.
init.yaml
entities:
- name: Client
connections:
- target: Server
id: server_connection
- name: Server
Указание исполняемого файла для запуска
В следующем примере будут запущены: процесс класса Client
из исполняемого файла cl
, процесс класса ClientServer
из исполняемого файла csr
и процесс класса MainServer
из исполняемого файла msr
. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов.
init.yaml
entities:
- name: Client
path: cl
- name: ClientServer
path: csr
- name: MainServer
path: msr
Запуск двух процессов из одного исполняемого файла
В следующем примере будут запущены три процесса: процесс класса Client
из исполняемого файла по умолчанию (Client
), а также процессы классов MainServer
и BkServer
из исполняемого файла srv
. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов.
init.yaml
entities:
- name: Client
- name: MainServer
path: srv
- name: BkServer
path: srv
Запуск двух процессов одного класса
В следующем примере будут запущены: один процесс класса Client
(с именем по умолчанию – Client
) и два процесса класса Server
с именами UserServer
и PrivilegedServer
. Клиентский процесс связан с серверными процессами IPC-каналами с именами server_connection_us
и server_connection_ps
, соответственно. Имена исполняемых файлов не указаны, поэтому они будут совпадать с именами классов процессов.
init.yaml
entities:
- name: Client
connections:
- id: server_connection_us
target: UserServer
- id: server_connection_ps
target: PrivilegedServer
- task: UserServer
name: Server
- task: PrivilegedServer
name: Server
Передача переменных окружения и аргументов функции main()
В следующем примере будут запущены два процесса: один класса VfsFirst
(с именем по умолчанию – VfsFirst
) и второй класса VfsSecond
(с именем по умолчанию – VfsSecond
). Первый процесс при запуске получит аргумент -f /etc/fstab
, а также переменные окружения: ROOTFS
со значением ramdisk0,0 / ext2 0 и UNMAP_ROMFS
со значением 1. Второй процесс при запуске получит аргумент -l devfs /dev devfs 0
.
Имена исполняемых файлов не указаны, поэтому они будут совпадать с именами классов процессов.
Если в решении используется программа Env, то передаваемые через нее аргументы и переменные окружения переопределяют значения, заданные через init.yaml
.
init.yaml
entities:
- name: VfsFirst
args:
- -f
- /etc/fstab
env:
ROOTFS: ramdisk0,0 / ext2 0
UNMAP_ROMFS: 1
- name: VfsSecond
args:
- -l
- devfs /dev devfs 0
Запуск процесса с помощью KasperskyOS API
В этом примере: использование функций EntityInitEx()
и EntityRun()
для запуска исполняемого файла из образа решения.
Ниже приводится код функции GpMgrOpenSession()
, выполняющей запуск серверного процесса, соединение его с клиентским процессом и инициализацию IPC-транспорта. Исполняемый файл нового процесса должен содержаться в ROMFS-хранилище решения.
/**
* Параметр classname задает имя класса запускаемого процесса,
* параметр server задает уникальное имя процесса, а параметр service содержит имя сервиса,
* используемое при динамическом создании канала.
* Выходной параметр transport содержит инициализированный транспорт,
* если IPC-канал до клиента успешно создан.
*/
Retcode GpMgrOpenSession(const char *classname, const char *server,
const char *service, NkKosTransport *transport)
{
Retcode rc;
Entity *e;
EntityInfo tae_info;
Handle endpoint;
rtl_uint32_t riid;
int count = CONNECT_RETRY;
/* Инициализация структуры описания процесса. */
rtl_memset(&tae_info, 0, sizeof(tae_info));
tae_info.eiid = classname;
tae_info.args[0] = server;
tae_info.args[1] = service;
/* Создание процесса с описанием tae_info и именем server.
* Поскольку третий параметр равен RTL_NULL, имя запускаемого
* бинарного файла совпадает с именем класса из описания tae_info.
* Созданный процесс находится в остановленном состоянии. */
if ((e = EntityInitEx(&tae_info, server, RTL_NULL)) == NK_NULL)
{
rtl_printf("Cannot init entity '%s'\n", tae_info.eiid);
return rcFail;
}
/* Запуск процесса. */
if ((rc = EntityRun(e)) != rcOk)
{
rtl_printf("Cannot launch entity %" RTL_PRId32 "\n", rc);
EntityFree(e);
return rc;
}
/* Динамическое создание IPC-канала. */
while ((rc = KnCmConnect(server, service, INFINITE_TIMEOUT, &endpoint, &riid) ==
rcResourceNotFound && count--)
{
KnSleep(CONNECT_DELAY);
}
if (rc != rcOk)
{
rtl_printf("Cannot connect to server %" RTL_PRId32 "\n", rc);
return rc;
}
/* Инициализация IPC-транспорта. */
NkKosTransport_Init(transport, endpoint, NK_NULL, 0);
...
return rcOk;
}
Чтобы процесс мог запускать другие процессы, политика безопасности решения должна разрешать ему использование следующих служб ядра: Handle
, Task
и VMM
(их описания находятся в директории kl\core\
).
Обзор: программа Env
Служебная программа Env
предназначена для передачи аргументов и переменных окружения запускаемым процессам. При запуске каждый процесс автоматически отправляет запрос процессу Env
и получает необходимые данные.
Обращение процесса к Env
переопределяет аргументы и переменные окружения, полученные через Einit
.
Чтобы использовать программу Env
в своем решении, необходимо:
1. Разработать код программы Env
, используя макросы из env/env.h
.
2. Собрать бинарный файл программы Env
, скомпоновав ее с библиотекой env_server
.
3. В init-описании указать, что необходимо запустить процесс Env
и соединить с ней выбранные процессы (Env
при этом является сервером). Имя канала задается макросом ENV_SERVICE_NAME
, объявленным в файле env/env.h
.
4. Включить бинарный файл Env
в образ решения.
Код программы Env
В коде программы Env
используются следующие макросы и функции, объявленные в файле env/env.h
:
ENV_REGISTER_ARGS(name,argarr)
– передать процессу с именемname
аргументы из массиваargarr
(максимальный размер одного элемента – 256 байтов);ENV_REGISTER_VARS(name,envarr)
– передать процессу с именемname
переменные окружения из массиваenvarr
(максимальный размер одного элемента – 256 байтов);ENV_REGISTER_PROGRAM_ENVIRONMENT(name,argarr,envarr)
– передать процессу с именемname
как аргументы, так и переменные окружения;envServerRun()
– инициализировать серверную часть программыEnv
, чтобы она могла отвечать на запросы.
Передача переменных окружения и аргументов с помощью Env
Пример передачи аргументов при запуске процесса
Ниже приводится код программы Env
, которая при запуске процесса с именем NetVfs
передаст ему три аргумента: NetVfs
, -l devfs /dev devfs 0
и -l romfs /etc romfs 0
:
env.c
int main(int argc, char** argv)
{
const char* NetVfsArgs[] = {
"-l", "devfs /dev devfs 0",
"-l", "romfs /etc romfs 0"
};
ENV_REGISTER_ARGS("NetVfs", NetVfsArgs);
envServerRun();
return EXIT_SUCCESS;
}
Пример передачи переменных окружения при запуске процесса
Ниже приводится код программы Env
, которая при запуске процесса с именем Vfs3
передаст ему две переменных окружения: ROOTFS=ramdisk0,0 / ext2 0
и UNMAP_ROMFS=1
:
env.c
int main(int argc, char** argv)
{
const char* Vfs3Envs[] = {
"ROOTFS=ramdisk0,0 / ext2 0",
"UNMAP_ROMFS=1"
};
ENV_REGISTER_VARS("Vfs3", Vfs3Envs);
envServerRun();
return EXIT_SUCCESS;
}
Состав компонента VFS
Компонент VFS содержит набор исполняемых файлов, библиотек и файлов описаний, позволяющих использовать файловые системы и/или сетевой стек, вынесенные в отдельный процесс VFS (Virtual File System – виртуальная файловая система). При необходимости можно собрать собственные реализации VFS.
Библиотеки VFS
CMake-пакет vfs
содержит следующие библиотеки:
vfs_fs
– содержит реализации defvs, ramfs и romfs, а также позволяет добавить в VFS реализации других файловых систем;vfs_net
– содержит реализацию defvs и сетевого стека;vfs_imp
– содержит в себе сумму компонентовvfs_fs
иvfs_net
;vfs_remote
– клиентская транспортная библиотека; преобразует локальные вызовы в IPC-запросы к VFS и принимает IPC-ответы;vfs_server
– серверная транспортная библиотека VFS; принимает IPC-запросы, преобразует их в локальные вызовы и отправляет IPC-ответы;vfs_local
– используется для статической компоновки клиента с библиотеками VFS.
Исполняемые файлы VFS
CMake-пакет precompiled_vfs
содержит следующие исполняемые файлы:
VfsRamFs
VfsSdCardFs
VfsNet
Исполняемые файлы VfsRamFs
и VfsSdCardFs
включают в себя библиотеки vfs_server
, vfs_fs
, vfat
и lwext4
. Исполняемый файл VfsNet
включает в себя библиотеки vfs_server
, vfs_imp
и dnet_imp
.
Каждый из этих исполняемых файлов имеет собственные значения аргументов и переменных окружения по умолчанию.
При необходимости можно самостоятельно собрать исполняемый файл VFS с нужной функциональностью.
Файлы описаний VFS
В директории /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/kl/
находятся следующие файлы VFS:
VfsRamFs.edl
,VfsSdCardFs.edl
,VfsNet.edl
иVfsEntity.edl
и сгенерированные из них заголовочные файлы с транспортным кодом;Vfs.cdl
и сгенерированныйVfs.cdl.h
;Vfs*.idl
и сгенерированные из них заголовочные файлы с транспортным кодом.
Создание IPC-канала до VFS
Рассмотрим программу Client
, использующую файловые системы и сокеты Беркли. Для обработки ее вызовов запустим один процесс VFS (с именем VfsFsnet
). В этот процесс будут направляться как "сетевые", так и "файловые" вызовы. Такой подход используется в тех случаях, когда не требуется разделение "файловых" и "сетевых" информационных потоков.
Чтобы взаимодействие процессов Client
и VfsFsnet
было корректным, имя IPC-канала между ними должно задаваться макросом _VFS_CONNECTION_ID
, объявленным в файле vfs/defs.h
.
Ниже приводится фрагмент init‑описания для соединения процессов Client
и VfsFsnet
.
init.yaml
- name: Client
connections:
- target: VfsFsnet
id: {var: _VFS_CONNECTION_ID, include: vfs/defs.h}
- name: VfsFsnet
Сборка исполняемого файла VFS
При сборке исполняемого файла VFS можно включить в него именно ту функциональность, которая требуется, например:
- реализацию той или иной файловый системы;
- сетевой стек;
- сетевой драйвер.
Например, при разделении файловых и сетевых вызовов понадобится сборка "файловой версии" и "сетевой версии" VFS. В некоторых случаях в VFS потребуется включить и сетевой стек, и файловые системы ("полная версия" VFS).
Сборка "файловой версии" VFS
Рассмотрим программу VFS, содержащую только реализацию файловой системы lwext4 и не содержащую сетевого стека. Для сборки такого исполняемого файла необходимо файл с функцией main()
скомпоновать с библиотеками vfs_server
, vfs_fs
и lwext4
:
CMakeLists.txt
project (vfsfs)
include (platform/nk)
# Установка флагов компиляции
project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")
add_executable (VfsFs "src/vfs.c")
# Компоновка с библиотеками VFS
target_link_libraries (VfsFs
${vfs_SERVER_LIB}
${LWEXT4_LIB}
${vfs_FS_LIB})
# Подготовка VFS для соединения с процессом ramdisk-драйвера
set_target_properties (VfsFs PROPERTIES ${blkdev_ENTITY}_REPLACEMENT ${ramdisk_ENTITY})
Драйвер блочного устройства не может быть скомпонован с VFS, поэтому всегда должен быть запущен как отдельный процесс.
Взаимодействие трех процессов: клиента, "файловой версии" VFS и драйвера блочного устройства
Сборка "сетевой версии" VFS совместно с сетевым драйвером
Рассмотрим программу VFS, содержащую сетевой стек с драйвером и не содержащую реализаций файловых систем. Для сборки такого исполняемого файла необходимо файл с функцией main()
скомпоновать с библиотеками vfs_server
, vfs_implementation
и dnet_implementation
.
CMakeLists.txt
project (vfsnet)
include (platform/nk)
# Установка флагов компиляции
project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")
add_executable (VfsNet "src/vfs.c")
# Компоновка с библиотеками VFS
target_link_libraries (VfsNet
${vfs_SERVER_LIB}
${vfs_IMPLEMENTATION_LIB}
${dnet_IMPLEMENTATION_LIB})
# Отключение драйвера блочного устройства
set_target_properties (VfsNet PROPERTIES ${blkdev_ENTITY}_REPLACEMENT "")
Библиотека dnet_implementation
уже включает в себя сетевой драйвер, поэтому запуск отдельного процесса драйвера не требуется.
Взаимодействие процесса Client с процессом "сетевой версии" VFS
Сборка "сетевой версии" VFS с отдельным сетевым драйвером
Еще один вариант сборки "сетевой версии" VFS – без сетевого драйвера. Сам сетевой драйвер необходимо будет запустить как отдельный процесс. Взаимодействие с драйвером происходит через IPC с помощью библиотеки dnet_client
.
Таким образом, необходимо файл с функцией main()
скомпоновать с библиотеками vfs_server
, vfs_implementation
и dnet_client
.
CMakeLists.txt
project (vfsnet)
include (platform/nk)
# Установка флагов компиляции
project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")
add_executable (VfsNet "src/vfs.c")
# Компоновка с библиотеками VFS
target_link_libraries (VfsNet
${vfs_SERVER_LIB}
${vfs_IMPLEMENTATION_LIB}
${dnet_CLIENT_LIB})
# Отключение драйвера блочного устройства
set_target_properties (VfsNet PROPERTIES ${blkdev_ENTITY}_REPLACEMENT "")
Взаимодействие трех процессов: клиента, "сетевой версии" VFS и сетевого драйвера
Сборка "полной версии" VFS
Если в VFS требуется включить как сетевой стек, так и реализации файловых систем, то при сборке следует использовать библиотеки vfs_server
, vfs_implementation
, dnet_implementation
(или dnet_client
– в случае отдельного сетевого драйвера), а также библиотеки реализации файловых систем.
Объединение клиента и VFS в один исполняемый файл
Рассмотрим программу Client
, использующую сокеты Беркли. Вызовы, которые выполняет Client
, должны направляться в VFS. Обычный путь состоит в запуске отдельного процесса VFS и создании IPC-канала. Однако вместо этого можно интегрировать функциональность VFS в сам исполняемый файл Client
. Для этого нужно при сборке исполняемого файла Client
скомпоновать его с библиотекой vfs_local
, которая будет принимать вызовы, а также с библиотеками реализации – vfs_implementation
и dnet_implementation
.
Локальную компоновку с VFS удобно использовать при отладке. Кроме того, вызовы для работы с сетью могут обрабатываться намного быстрее за счет исключения IPC-вызовов. Тем не менее, изоляция VFS в отдельном процессе и IPC-взаимодействие с ним рекомендуется во всех случаях как более безопасный подход.
Ниже приведен сборочный скрипт исполняемого файла Client
.
CMakeLists.txt
project (client)
include (platform/nk)
# Установка флагов компиляции
project_header_default ("STANDARD_GNU_11:YES" "STRICT_WARNINGS:NO")
# Генерация файла Client.edl.h
nk_build_edl_files (client_edl_files NK_MODULE "client" EDL "${CMAKE_SOURCE_DIR}/resources/edl/Client.edl")
add_executable (Client "src/client.c")
add_dependencies (Client client_edl_files)
# Компоновка с библиотеками VFS
target_link_libraries (Client ${vfs_LOCAL_LIB} ${vfs_IMPLEMENTATION_LIB} ${dnet_IMPLEMENTATION_LIB}
Если Client
использует файловые системы, то помимо vfs_local
, его нужно скомпоновать с библиотекой vfs_fs
и реализацией используемой файловой системы. Кроме того, нужно добавить в решение драйвер блочного устройства.
Обзор: аргументы и переменные окружения VFS
Аргументы VFS
-l <запись в формате fstab>
Аргумент
-l
позволяет монтировать файловую систему.-f <путь к файлу fstab>
Аргумент
-f
позволяет передать файл с записями в формате fstab для монтирования файловых систем. Файл будет искаться в ROMFS-хранилище. Если переменнаяUMNAP_ROMFS
определена, то файл будет искаться на файловой системе, смонтированной с помощью переменнойROOTFS
.
Пример использования аргументов -l и -f
Переменные окружения VFS
UNMAP_ROMFS
Если переменная
UNMAP_ROMFS
определена, то ROMFS-хранилище будет удалено. Это позволяет сэкономить память и изменить поведение при использовании аргумента-f
.ROOTFS = <запись в формате fstab>
Переменная
ROOTFS
позволяет монтировать файловую систему в корневой каталог. В комбинации с переменнойUNMAP_ROMFS
и аргументом-f
позволяет искать fstab-файл на монтированной файловой системе, а не в ROMFS-хранилище. Пример использования ROOTFSVFS_CLIENT_MAX_THREADS
Переменная окружения
VFS_CLIENT_MAX_THREADS
позволяет в момент запуска VFS переопределить параметр конфигурирования SDKVFS_CLIENT_MAX_THREADS
.-
_VFS_NETWORK_BACKEND=<имя бэкенда>:<имя IPC-канала до VFS>
Переменная _VFS_NETWORK_BACKEND
задает используемый для "сетевых" вызовов бэкенд. Можно указать имя стандартного бэкенда: client, server или local, а также имя пользовательского бэкенда. Если используется бэкенд local, то имя IPC-канала не указывается (_VFS_NETWORK_BACKEND=local:
). Может быть указано два и больше IPC-канала через запятую.
_VFS_FILESYSTEM_BACKEND=<имя бэкенда>:<имя IPC-канала до VFS>
Переменная
_VFS_FILESYSTEM_BACKEND
задает используемый для "файловых" вызовов бэкенд. Имя бэкенда и имя IPC-канала до VFS задаются так же, как и для переменной_VFS_NETWORK_BACKEND
.
Значения по умолчанию
Для исполняемого файла VfsRamFs
:
ROOTFS = ramdisk0,0 / ext4 0
VFS_FILESYSTEM_BACKEND = server:kl.VfsRamFs
Для исполняемого файла VfsSdCardFs
:
ROOTFS = mmc0,0 / fat32 0
VFS_FILESYSTEM_BACKEND = server:kl.VfsSdCardFs
-l nodev /tmp ramfs 0
-l nodev /var ramfs 0
Для исполняемого файла VfsNet
:
VFS_NETWORK_BACKEND = server:kl.VfsNet
VFS_FILESYSTEM_BACKEND = server:kl.VfsNet
-l devfs /dev devfs 0
Монтирование файловой системы при старте
При запуске процесса VFS по умолчанию монтируется только файловая система RAMFS, в корневую директорию. Если требуется монтировать другие файловые системы, это можно сделать не только с помощью вызова mount()
после запуска VFS, но и непосредственно при запуске процесса VFS – передав ему нужные аргументы и переменные окружения.
Рассмотрим три примера монтирования файловых систем при запуске VFS. Для передачи аргументов и переменных окружения процессу VFS использована программа Env
.
Монтирование с помощью аргумента -l
Простой способ монтировать файловую систему – это передать процессу VFS аргумент -l <запись в формате fstab>
.
В этом примере при запуске процесса с именем Vfs1
будут монтированы файловые системы devfs и romfs.
env.c
int main(int argc, char** argv)
{
const char* Vfs1Args[] = {
"-l", "devfs /dev devfs 0",
"-l", "romfs /etc romfs 0"
};
ENV_REGISTER_ARGS("Vfs1", Vfs1Args);
envServerRun();
return EXIT_SUCCESS;
}
Монтирование с помощью fstab из ROMFS
Если при сборке решения добавить fstab-файл, после старта он будет доступен через ROMFS-хранилище. Его можно использовать для монтирования, передав процессу VFS аргумент -f <путь к fstab-файлу>
.
В этом примере при запуске процесса с именем Vfs2
будут монтированы файловые системы, заданные через файл fstab
, который был добавлен при сборке решения.
env.c
int main(int argc, char** argv)
{
const char* Vfs2Args[] = { "-f", "fstab" };
ENV_REGISTER_ARGS("Vfs2", Vfs2Args);
envServerRun();
return EXIT_SUCCESS;
}
Монтирование с помощью "внешнего" fstab
Пусть fstab-файл находится не в ROMFS-образе решения, а на диске. Чтобы использовать его для монтирования, необходимо передать VFS следующие аргументы и переменные окружения:
ROOTFS
. Эта переменная позволяет монтировать в корневую директорию файловую систему, в которой находится fstab-файл.UNMAP_ROMFS
. Если эта переменная определена, ROMFS-хранилище удаляется. В итоге fstab-файл будет искаться на файловой системе, смонтированной с помощью переменнойROOTFS
.-f
. Этот аргумент используется, чтобы задать путь к fstab-файлу.
В следующем примере при запуске процесса с именем Vfs3
в корневой каталог будет монтирована файловая система ext2, на которой будет найден файл /etc/fstab
для монтирования дополнительных файловых систем. ROMFS-хранилище будет удалено.
env.c
int main(int argc, char** argv)
{
const char* Vfs3Args[] = { "-f", "/etc/fstab" };
const char* Vfs3Envs[] = {
"ROOTFS=ramdisk0,0 / ext2 0",
"UNMAP_ROMFS=1"
};
ENV_REGISTER_PROGRAM_ENVIRONMENT("Vfs3", Vfs3Args, Vfs3Envs);
envServerRun();
return EXIT_SUCCESS;
}
Разделение файловых и сетевых вызовов с помощью бэкендов VFS
В этом примере: паттерн безопасной разработки, предусматривающий разделение "сетевых" и "файловых" информационных потоков.
Рассмотрим программу Client
, использующую файловые системы и сокеты Беркли. Для обработки ее вызовов мы запустим не один, а два отдельных VFS-процесса из исполняемых файлов VfsFirst
и VfsSecond
. Через переменные окружения мы зададим файловые бэкенды как работающие через канал до VfsFirst
, а сетевые бэкенды – как работающие через канал до VfsSecond
. Будем использовать стандартные бэкенды client и server. Благодаря этому мы перенаправим "файловые вызовы" Client
в VfsFirst
, а "сетевые" – в VfsSecond
. Чтобы передать процессам переменные окружения, добавим в решение программу Env
.
Init-описание решения представлено ниже. Процесс Client
будет соединен с процессами VfsFirst
и VfsSecond
, при этом каждый из трех процессов соединен с процессом Env
. Обратите внимание, что имя IPC-канала до процесса Env
задается с помощью переменной ENV_SERVICE_NAME
.
init.yaml
entities:
- name: Env
- name: Client
connections:
- target: Env
id: {var: ENV_SERVICE_NAME, include: env/env.h}
- target: VfsFirst
id: VFS1
- target: VfsSecond
id: VFS2
- name: VfsFirst
connections:
- target: Env
id: {var: ENV_SERVICE_NAME, include: env/env.h}
- name: VfsSecond
connections:
- target: Env
id: {var: ENV_SERVICE_NAME, include: env/env.h}
Чтобы направить все "файловые" вызовы в VfsFirst
, зададим значение переменной окружения _VFS_FILESYSTEM_BACKEND
следующим образом:
- для
VfsFirst
:_VFS_FILESYSTEM_BACKEND=server:<имя IPC-канала до VfsFirst>
; - для
Client
:_VFS_FILESYSTEM_BACKEND=client:<имя IPC-канала до VfsFirst>
.
Для направления "сетевых" вызовов в VfsSecond
используем аналогичную переменную окружения _VFS_NETWORK_BACKEND
:
- для
VfsSecond
зададим:_VFS_NETWORK_BACKEND=server:<имя IPC-канала до VfsSecond>
; - для
Client
:_VFS_NETWORK_BACKEND=client:<имя IPC-канала до VfsSecond>
.
Значение переменных окружения зададим через программу Env
, код которой представлен ниже.
env.c
int main(void)
{
const char* vfs_first_envs[] = { "_VFS_FILESYSTEM_BACKEND=server:VFS1" };
ENV_REGISTER_VARS("VfsFirst", vfs_first_envs);
const char* vfs_second_envs[] = { "_VFS_NETWORK_BACKEND=server:VFS2" };
ENV_REGISTER_VARS("VfsSecond", vfs_second_envs);
const char* client_envs[] = { "_VFS_FILESYSTEM_BACKEND=client:VFS1", "_VFS_NETWORK_BACKEND=client:VFS2" };
ENV_REGISTER_VARS("Client", client_envs);
envServerRun();
return EXIT_SUCCESS;
}
Написание пользовательского бэкенда VFS
В этом примере: изменение логики обработки файловых вызовов с помощью специального бэкенда VFS.
Рассмотрим решение, включающее процессы Client
, VfsFirst
и VfsSecond
. Пусть процесс Client
соединен с VfsFirst
и VfsSecond
с помощью IPC-каналов.
Задача: сделать так, чтобы обращения процесса Client
к файловой системе fat32 обрабатывались процессом VfsFirst
, а обращения к ext4 обрабатывались VfsSecond
. Для решения этой задачи можно использовать механизм бэкендов VFS, причем даже не придется менять код программы Client
.
Мы напишем пользовательский бэкенд custom_client
, который будет отправлять вызовы по каналу VFS1
или VFS2
, в зависимости от того, начинается ли путь к файлу с /mnt1. Для отправки вызовов custom_client
будет использовать стандартные бэкенды client
, то есть будет являться проксирующим бэкендом.
С помощью аргумента -l мы монтируем fat32 в директорию /mnt1 для процесса VfsFirst
и ext4 в /mnt2 для процесса VfsSecond
. (Предполагается, что VfsFirst
содержит реализацию fat32, а VfsSecond
– реализацию ext4.) С помощью переменной окружения _VFS_FILESYSTEM_BACKEND
зададим используемые процессами бэкенды (custom_client и server) и IPC-каналы (VFS1 и VFS2).
Наконец, с помощью init-описания зададим имена IPC-каналов: VFS1 и VFS2.
Ниже мы рассмотрим подробнее:
- Код бэкенда
custom_client
. - Компоновка программы
Client
и бэкендаcustom_client
. - Код программы
Env
. - Init-описание.
Написание бэкенда custom_client
Этот файл содержит реализацию проксирующего пользовательского бэкенда, передающего вызовы в один из двух стандартных бэкендов client. Логика выбора бэкенда зависит от используемого пути или от дескриптора файла и управляется дополнительными структурами данных.
backend.c
/* Код управления файловыми дескрипторами. */
struct entry
{
Handle handle;
bool is_vfat;
};
struct fd_array
{
struct entry entries[MAX_FDS];
int pos;
pthread_rwlock_t lock;
};
struct fd_array fds = { .pos = 0, .lock = PTHREAD_RWLOCK_INITIALIZER };
int insert_entry(Handle fd, bool is_vfat)
{
pthread_rwlock_wrlock(&fds.lock);
if (fds.pos == MAX_FDS)
{
pthread_rwlock_unlock(&fds.lock);
return -1;
}
fds.entries[fds.pos].handle = fd;
fds.entries[fds.pos].is_vfat = is_vfat;
fds.pos++;
pthread_rwlock_unlock(&fds.lock);
return 0;
}
struct entry *find_entry(Handle fd)
{
pthread_rwlock_rdlock(&fds.lock);
for (int i = 0; i < fds.pos; i++)
{
if (fds.entries[i].handle == fd)
{
pthread_rwlock_unlock(&fds.lock);
return &fds.entries[i];
}
}
pthread_rwlock_unlock(&fds.lock);
return NULL;
}
/* Структура пользовательского бэкенда. */
struct context
{
struct vfs wrapper;
pthread_rwlock_t lock;
struct vfs *vfs_vfat;
struct vfs *vfs_ext4;
};
struct context ctx =
{
.wrapper =
{
.dtor = _vfs_backend_dtor,
.disconnect_all_clients = _disconnect_all_clients,
.getstdin = _getstdin,
.getstdout = _getstdout,
.getstderr = _getstderr,
.open = _open,
.read = _read,
.write = _write,
.close = _close,
}
};
/* Реализация методов пользовательского бэкенда. */
static bool is_vfs_vfat_path(const char *path)
{
char vfat_path[5] = "/mnt1";
if (memcmp(vfat_path, path, sizeof(vfat_path)) != 0)
return false;
return true;
}
static void _vfs_backend_dtor(struct vfs *vfs)
{
ctx.vfs_vfat->dtor(ctx.vfs_vfat);
ctx.vfs_ext4->dtor(ctx.vfs_ext4);
}
static void _disconnect_all_clients(struct vfs *self, int *error)
{
(void)self;
(void)error;
ctx.vfs_vfat->disconnect_all_clients(ctx.vfs_vfat, error);
ctx.vfs_ext4->disconnect_all_clients(ctx.vfs_ext4, error);
}
static Handle _getstdin(struct vfs *self, int *error)
{
(void)self;
Handle handle = ctx.vfs_vfat->getstdin(ctx.vfs_vfat, error);
if (handle != INVALID_HANDLE)
{
if (insert_entry(handle, true))
{
*error = ENOMEM;
return INVALID_HANDLE;
}
}
return handle;
}
static Handle _getstdout(struct vfs *self, int *error)
{
(void)self;
Handle handle = ctx.vfs_vfat->getstdout(ctx.vfs_vfat, error);
if (handle != INVALID_HANDLE)
{
if (insert_entry(handle, true))
{
*error = ENOMEM;
return INVALID_HANDLE;
}
}
return handle;
}
static Handle _getstderr(struct vfs *self, int *error)
{
(void)self;
Handle handle = ctx.vfs_vfat->getstderr(ctx.vfs_vfat, error);
if (handle != INVALID_HANDLE)
{
if (insert_entry(handle, true))
{
*error = ENOMEM;
return INVALID_HANDLE;
}
}
return handle;
}
static Handle _open(struct vfs *self, const char *path, int oflag, mode_t mode, int *error)
{
(void)self;
Handle handle;
bool is_vfat = false;
if (is_vfs_vfat_path(path))
{
handle = ctx.vfs_vfat->open(ctx.vfs_vfat, path, oflag, mode, error);
is_vfat = true;
}
else
handle = ctx.vfs_ext4->open(ctx.vfs_ext4, path, oflag, mode, error);
if (handle == INVALID_HANDLE)
return INVALID_HANDLE;
if (insert_entry(handle, is_vfat))
{
if (is_vfat)
ctx.vfs_vfat->close(ctx.vfs_vfat, handle, error);
*error = ENOMEM;
return INVALID_HANDLE;
}
return handle;
}
static ssize_t _read(struct vfs *self, Handle fd, void *buf, size_t count, bool *nodata, int *error)
{
(void)self;
struct entry *found_entry = find_entry(fd);
if (found_entry != NULL && found_entry->is_vfat)
return ctx.vfs_vfat->read(ctx.vfs_vfat, fd, buf, count, nodata, error);
return ctx.vfs_ext4->read(ctx.vfs_ext4, fd, buf, count, nodata, error);
}
static ssize_t _write(struct vfs *self, Handle fd, const void *buf, size_t count, int *error)
{
(void)self;
struct entry *found_entry = find_entry(fd);
if (found_entry != NULL && found_entry->is_vfat)
return ctx.vfs_vfat->write(ctx.vfs_vfat, fd, buf, count, error);
return ctx.vfs_ext4->write(ctx.vfs_ext4, fd, buf, count, error);
}
static int _close(struct vfs *self, Handle fd, int *error)
{
(void)self;
struct entry *found_entry = find_entry(fd);
if (found_entry != NULL && found_entry->is_vfat)
return ctx.vfs_vfat->close(ctx.vfs_vfat, fd, error);
return ctx.vfs_ext4->close(ctx.vfs_ext4, fd, error);
}
/* Конструктор пользовательского бэкенда. ctx.vfs_vfat и ctx.vfs_ext4 инициализируются
* как стандартные бэкенды с именем "client". */
static struct vfs *_vfs_backend_create(Handle client_id, const char *config, int *error)
{
(void)config;
ctx.vfs_vfat = _vfs_init("client", client_id, "VFS1", error);
assert(ctx.vfs_vfat != NULL && "Can't initilize client backend!");
assert(ctx.vfs_vfat->dtor != NULL && "VFS FS backend has not set the destructor!");
ctx.vfs_ext4 = _vfs_init("client", client_id, "VFS2", error);
assert(ctx.vfs_ext4 != NULL && "Can't initilize client backend!");
assert(ctx.vfs_ext4->dtor != NULL && "VFS FS backend has not set the destructor!");
return &ctx.wrapper;
}
/* Регистрация пользовательского бэкенда под именем custom_client. */
static void _vfs_backend(create_vfs_backend_t *ctor, const char **name)
{
*ctor = &_vfs_backend_create;
*name = "custom_client";
}
REGISTER_VFS_BACKEND(_vfs_backend)
Компоновка программы Client и бэкенда custom_client
Написанный бэкенд скомпилируем в библиотеку:
CMakeLists.txt
add_library (backend_client STATIC "src/backend.c")
Готовую библиотеку backend_client
скомпонуем с программой Client
:
CMakeLists.txt (фрагмент)
add_dependencies (Client vfs_backend_client backend_client)
target_link_libraries (Client
pthread
${vfs_CLIENT_LIB}
"-Wl,--whole-archive" backend_client "-Wl,--no-whole-archive" backend_client
)
Написание программы Env
Чтобы передать процессам аргументы и переменные окружения, используем программу Env
.
env.c
int main(int argc, char** argv)
{
/* Монтирование fat32 в /mnt1 для процесса VfsFirst и ext4 в /mnt2 для процесса VfsSecond. */
const char* VfsFirstArgs[] = {
"-l", "ahci0 /mnt1 fat32 0"
};
ENV_REGISTER_ARGS("VfsFirst", VfsFirstArgs);
const char* VfsSecondArgs[] = {
"-l", "ahci1 /mnt2 ext4 0"
};
ENV_REGISTER_ARGS("VfsSecond", VfsSecondArgs);
/* Задание файловых бэкендов. */
const char* vfs_first_args[] = { "_VFS_FILESYSTEM_BACKEND=server:VFS1" };
ENV_REGISTER_VARS("VfsFirst", vfs_first_args);
const char* vfs_second_args[] = { "_VFS_FILESYSTEM_BACKEND=server:VFS2" };
ENV_REGISTER_VARS("VfsSecond", vfs_second_args);
const char* client_fs_envs[] = { "_VFS_FILESYSTEM_BACKEND=custom_client:VFS1,VFS2" };
ENV_REGISTER_VARS("Client", client_fs_envs);
envServerRun();
return EXIT_SUCCESS;
}
Изменение init.yaml
Для IPC-каналов, соединяющих процесс Client
с процессами VfsFirst
и VfsSecond
, необходимо задать те же имена, которые мы указали в переменной окружения _VFS_FILESYSTEM_BACKEND
: VFS1 и VFS2.
init.yaml
entities:
- name: vfs_backend.Env
- name: vfs_backend.Client
connections:
- target: vfs_backend.Env
id: {var: ENV_SERVICE_NAME, include: env/env.h}
- target: vfs_backend.VfsFirst
id: VFS1
- target: vfs_backend.VfsSecond
id: VFS2
- name: vfs_backend.VfsFirst
connections:
- target: vfs_backend.Env
id: {var: ENV_SERVICE_NAME, include: env/env.h}
- name: vfs_backend.VfsSecond
connections:
- target: vfs_backend.Env
id: {var: ENV_SERVICE_NAME, include: env/env.h}
Обзор: создание IPC-каналов
Есть два способа создания IPC-каналов: статический и динамический.
Статическое создание IPC-каналов проще в реализации, поскольку для него можно использовать init-описание.
Динамическое создание IPC-каналов позволяет изменять топологию взаимодействия процессов "на лету". Это требуется, если неизвестно, какой именно сервер содержит службу, необходимую клиенту. Например, может быть неизвестно, на какой именно накопитель нужно будет записывать данные.
Статическое создание IPC-канала
Статический способ имеет следующие особенности:
- клиент и сервер находятся в остановленном состоянии в момент создания IPC-канала;
- создание инициируются родительским процессом, запускающим клиента и сервера (обычно это Einit);
- созданный IPC-канал невозможно удалить;
- чтобы получить IPC-дескриптор и идентификатор службы (riid) после создания IPC-канала, клиент и сервер должны использовать интерфейс локатора сервисов (
coresrv/sl/sl_api.h
).
Динамическое создание IPC-канала
Динамический способ имеет следующие особенности:
- клиент и сервер уже запущены в момент создания IPC-канала;
- создание инициируются совместно клиентом и сервером;
- созданный IPC-канал может быть удален;
- клиент и сервер получают IPC-дескриптор и идентификатор службы (riid) сразу после успешного создания IPC-канала.
Создание IPC-каналов с помощью init.yaml
Здесь собраны init-описания, демонстрирующие особенности создания IPC-каналов. Примеры задания свойств и аргументов процессов через init-описания разбираются в отдельной статье.
В примерах в составе KasperskyOS Community Edition может использоваться формат init-описания с макросами (init.yaml.in
).
Файл с init-описанием обычно называется init.yaml
, хотя может иметь любое имя.
Соединение и запуск процесса-клиента и процесса-сервера
В следующем примере будут запущены два процесса: класса Client
и класса Server
. Имена процессов не указаны, поэтому они будут совпадать с именами классов процессов. Имена исполняемых файлов также не указаны, они также будут совпадать с именами классов. Процессы будут соединены IPC-каналом с именем server_connection
.
init.yaml
entities:
- name: Client
connections:
- target: Server
id: server_connection
- name: Server
Динамическое создание IPC-каналов
При динамическом создании IPC-канала используются функции:
- интерфейса сервера имен (Name Server);
- интерфейса менеджера соединений (Connection Manager).
Динамическое создание IPC-канала осуществляется по следующему сценарию:
- Запускаются процессы: клиент, сервер и сервер имен.
- Сервер подключается к серверу имен с помощью вызова
NsCreate()
и публикует имя сервера, имя интерфейса и имя службы с помощью вызоваNsPublishService()
. - Клиент подключается к серверу имен с помощью вызова
NsCreate()
и выполняет поиск имени сервера и имени службы по имени интерфейса с помощью вызоваNsEnumServices()
. - Клиент запрашивает доступ к службе с помощью вызова
KnCmConnect()
, передавая в качестве аргументов найденные имя сервера и имя службы. - Сервер вызывает функцию
KnCmListen()
для проверки наличия запросов на доступ к службе. - Сервер принимает запрос клиента на доступ к службе с помощью вызова
KnCmAccept()
, передавая в качестве аргументов имя клиента и имя службы, которые получены при вызовеKnCmListen()
.
Пункты 2 и 3 могут быть опущены, если клиент заранее знает имя сервера и имя службы.
Сервер может снимать с публикации на сервере имен ранее опубликованные службы с помощью вызова NsUnPublishService()
.
Сервер может отклонять запросы доступа к службам с помощью вызова KnCmDrop()
.
Для использования сервера имен политика безопасности решения должна разрешать взаимодействие процесса класса kl.core.NameServer
и процессами, между которыми необходимо динамически создавать IPC-каналы.
Добавление службы в решение
Чтобы программа Client
могла использовать ту или иную функциональность через механизм IPC, необходимо:
- Найти в составе KasperskyOS Community Edition исполняемый файл (условно назовем его
Server
), реализующий нужную функциональность. (Под функциональностью мы здесь понимаем одну или несколько служб, имеющих самостоятельные IPC-интерфейсы) - Подключить CMake-пакет, содержащий файл
Server
и его клиентскую библиотеку. - Добавить исполняемый файл
Server
в образ решения. - Изменить init-описание так, чтобы при старте решения программа
Einit
запускала новый серверный процесс из исполняемого файлаServer
и соединяла его IPC-каналом с процессом, запускаемым из файлаClient
.Необходимо указать корректное имя IPC-канала, чтобы транспортные библиотеки могли идентифицировать этот канал и найти его IPC-дескрипторы. Корректное имя IPC-канала, как правило, совпадает с именем класса серверного процесса. VFS при этом является исключением.
- Изменить PSL-описание так, чтобы разрешить запуск серверного процесса и IPC-взаимодействие между клиентом и сервером.
- Подключить в исходном коде программы
Client
заголовочный файл с методами сервера. - Скомпоновать программу
Client
с клиентской библиотекой.
Пример добавления GPIO-драйвера в решение
В составе KasperskyOS Community Edition есть файл gpio_hw
, реализующий функциональность GPIO-драйвера.
Следующие команды подключают CMake‑пакет gpio:
.\CMakeLists.txt
...
find_package (gpio REQUIRED COMPONENTS CLIENT_LIB ENTITY)
include_directories (${gpio_INCLUDE})
...
Добавление исполняемого файла gpio_hw
в образ решения производится с помощью переменной gpio_HW_ENTITY
, имя которой можно найти в конфигурационном файле пакета – /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/lib/cmake/gpio/gpio-config.cmake:
einit\CMakeLists.txt
...
set (ENTITIES Client ${gpio_HW_ENTITY})
...
В init-описание нужно добавить следующие строки:
init.yaml.in
...
- name: client.Client
connections:
- target: kl.drivers.GPIO
id: kl.drivers.GPIO
- name: kl.drivers.GPIO
path: gpio_hw
В PSL-описание нужно добавить следующие строки:
security.psl.in
...
execute src=Einit, dst=kl.drivers.GPIO
{
grant()
}
request src=client.Client, dst=kl.drivers.GPIO
{
grant()
}
response src=kl.drivers.GPIO, dst=client.Client
{
grant()
}
...
В коде программы Client
нужно подключить заголовочный файл, в котором объявлены методы GPIO-драйвера:
client.c
...
...
Наконец, нужно скомпоновать программу Client
с клиентской библиотекой GPIO:
client\CMakeLists.txt
...
target_link_libraries (Client ${gpio_CLIENT_LIB})
...
Для корректной работы GPIO‑драйвера может понадобиться добавить в решение компонент BSP. Чтобы не усложнять этот пример, мы не рассматриваем здесь BSP. Подробнее см. пример gpio_output: /opt/KasperskyOS-Community-Edition-<version>/examples/gpio_output
Обзор: структура IPC-сообщения
В KasperskyOS все взаимодействия между процессами статически типизированы. Допустимые структуры IPC-сообщения определяются описанием интерфейсов процесса-получателя сообщения (сервера).
Корректное IPC-сообщение (как запрос, так и ответ) содержит фиксированную часть и арену.
Фиксированная часть сообщения
Фиксированная часть сообщения содержит аргументы фиксированного размера, а также RIID и MID.
Аргументы фиксированного размера – это аргументы любых IDL-типов, кроме типа sequence
.
RIID и MID идентифицируют вызываемый интерфейс и метод:
- RIID (Runtime Implementation ID) является порядковым номером вызываемой службы процесса (начиная с нуля).
- MID (Method ID) является порядковым номером метода в содержащем его интерфейсе (начиная с нуля).
Тип фиксированной части сообщения генерируется компилятором NK на основе IDL-описания интерфейса. Для каждого метода интерфейса генерируется отдельная структура. Также генерируются типы union
для хранения любого запроса к процессу, компоненту или интерфейсу. Подробнее см. Пример генерации транспортных методов и типов.
Арена
Арена представляет собой буфер для хранения аргументов переменного размера (IDL‑тип sequence
).
Проверка структуры сообщения модулем безопасности
Перед тем как вызывать связанные с сообщением правила, подсистема Kaspersky Security Module проверяет отправляемое сообщение на корректность. Проверяются как запросы, так и ответы. Если сообщение имеет некорректную структуру, оно будет отклонено без вызова связанных с ним методов моделей безопасности.
Формирование структуры сообщения
В составе KasperskyOS Community Edition поставляются следующие инструменты, позволяющие упростить для разработчика создание и упаковку IPC-сообщения:
- Библиотека
transport-kos
для работы с транспортом NkKosTransport. - Компилятор NK, позволяющий сгенерировать специальные методы и типы.
Формирование простейшего IPC-сообщения показано в примерах echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/
).
Нахождение IPC-дескриптора
Клиентский и серверный IPC-дескрипторы требуется находить, если для используемой службы нет готовых транспортных библиотек (например, вы написали собственную службу). Для самостоятельной работы с IPC-транспортом нужно предварительно инициализировать его с помощью метода NkKosTransport_Init()
, передав в качестве второго аргумента IPC-дескриптор используемого канала.
Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/
),
Для использования служб, которые реализованы в исполняемых файлах в составе KasperskyOS Community Edition, нет необходимости находить IPC-дескриптор. Вся работа с транспортом, включая нахождение IPC-дескрипторов, выполняется поставляемыми транспортными библиотеками.
См. примеры gpio_*, net_*, net2_* и multi_vfs_* (/opt/KasperskyOS-Community-Edition-<version>/examples/
).
Нахождение IPC-дескриптора при статическом создании канала
При статическом создании IPC-канала как клиент, так и сервер могут сразу после своего запуска узнать свои IPC-дескрипторы с помощью методов ServiceLocatorRegister()
и ServiceLocatorConnect()
, указав имя созданного IPC-канала.
Например, если IPC-канал имеет имя server_connection
, то на клиентской стороне необходимо вызвать:
…
Handle handle = ServiceLocatorConnect("server_connection");
На серверной стороне необходимо вызвать:
…
nk_iid_t iid;
Handle handle = ServiceLocatorRegister("server_connection", NULL, 0, &iid);
Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/
), а так же заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/sl/sl_api.h
.
Нахождение IPC-дескриптора при динамическом создании канала
Как клиент, так и сервер получают свои IPC-дескрипторы сразу при успешном динамическом создании IPC-канала.
Клиентский IPC-дескриптор является одним из выходных (out
) аргументов метода KnCmConnect()
. Серверный IPC-дескриптор является выходным аргументом метода KnCmAccept()
. Подробнее см. заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/cm/cm_api.h
.
Нахождение идентификатора службы (riid)
Идентификатор службы (riid) требуется находить на клиентской стороне, если для используемой службы нет готовых транспортных библиотек (например, вы написали собственную службу). Для вызова методов сервера необходимо на клиентской стороне предварительно вызвать метод инициализации прокси-объекта, передав в качестве третьего аргумента идентификатор службы. Например, для интерфейса Filesystem
:
Filesystem_proxy_init(&proxy, &transport.base, riid);
Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/
),
Для использования служб, которые реализованы в исполняемых файлах в составе KasperskyOS Community Edition, нет необходимости находить идентификатор службы. Вся работа с транспортом выполняется поставляемыми транспортными библиотеками.
См. примеры gpio_*, net_*, net2_* и multi_vfs_* (/opt/KasperskyOS-Community-Edition-<version>/examples/
).
Нахождение идентификатора службы при статическом создании канала
При статическом создании IPC-канала клиент может узнать идентификатор нужной службы с помощью метода ServiceLocatorGetRiid()
, указав дескриптор IPC-канала и полное квалифицированное имя службы. Например, если экземпляр компонента OpsComp
содержит службу FS
, то на клиентской стороне необходимо вызвать:
…
nk_iid_t riid = ServiceLocatorGetRiid(handle, "OpsComp.FS");
Подробнее см. примеры echo и ping (/opt/KasperskyOS-Community-Edition-<version>/examples/
), а так же заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/sl/sl_api.h
.
Нахождение идентификатора службы при динамическом создании канала
Клиент получает идентификатор службы сразу при успешном динамическом создании IPC-канала. Клиентский IPC-дескриптор является одним из выходных (out
) аргументов метода KnCmConnect()
. Подробнее см. заголовочный файл /opt/KasperskyOS-Community-Edition-<version>/sysroot-aarch64-kos/include/coresrv/cm/cm_api.h
.
Пример генерации транспортных методов и типов
При сборке решения компилятор NK на основе EDL-, CDL- и IDL-описаний генерирует набор специальных методов и типов, упрощающих формирование, отправку, прием и обработку IPC-сообщений.
В качестве примера рассмотрим класс процессов Server
, предоставляющий службу FS
, которая содержит единственный метод Open()
:
Server.edl
entity Server
/* OpsComp - именованный экземпляр компонента Operations */
components {
OpsComp: Operations
}
Operations.cdl
component Operations
/* FS - локальное имя службы, реализующей интерфейс Filesystem */
endpoints {
FS: Filesystem
}
Filesystem.idl
package Filesystem
interface {
Open(in string<256> name, out UInt32 h);
}
На основе этих описаний будут сгенерированы файлы Server.edl.h
, Operations.cdl.h
, и Filesystem.idl.h
содержащие следующие методы и типы:
Методы и типы, общие для клиента и сервера
- Абстрактные интерфейсы, содержащие указатели на реализации входящих в них методов.
В нашем примере будет сгенерирован один абстрактный интерфейс –
Filesystem
:typedef struct Filesystem {
const struct Filesystem_ops *ops;
} Filesystem;
typedef nk_err_t
Filesystem_Open_fn(struct Filesystem *, const
struct Filesystem_Open_req *,
const struct nk_arena *,
struct Filesystem_Open_res *,
struct nk_arena *);
typedef struct Filesystem_ops {
Filesystem_Open_fn *Open;
} Filesystem_ops;
- Набор интерфейсных методов.
При вызове интерфейсного метода в запросе автоматически проставляются соответствующие значения RIID и MID.
В нашем примере будет сгенерирован единственный интерфейсный метод
Filesystem_Open
:nk_err_t Filesystem_Open(struct Filesystem *self,
struct Filesystem_Open_req *req,
const
struct nk_arena *req_arena,
struct Filesystem_Open_res *res,
struct nk_arena *res_arena)
Методы и типы, используемые только на клиенте
- Типы прокси-объектов.
Прокси-объект используется как аргумент интерфейсного метода. В нашем примере будет сгенерирован единственный тип прокси-объекта
Filesystem_proxy
:typedef struct Filesystem_proxy {
struct Filesystem base;
struct nk_transport *transport;
nk_iid_t iid;
} Filesystem_proxy;
- Функции для инициализации прокси-объектов.
В нашем примере будет сгенерирована единственная инициализирующая функция
Filesystem_proxy_init
:void Filesystem_proxy_init(struct Filesystem_proxy *self,
struct nk_transport *transport,
nk_iid_t iid)
- Типы, определяющие структуру фиксированной части сообщения для каждого конкретного метода.
В нашем примере будет сгенерировано два таких типа:
Filesystem_Open_req
(для запроса) иFilesystem_Open_res
(для ответа).typedef struct __nk_packed Filesystem_Open_req {
__nk_alignas(8)
struct nk_message base_;
__nk_alignas(4) nk_ptr_t name;
} Filesystem_Open_req;
typedef struct Filesystem_Open_res {
union {
struct {
__nk_alignas(8)
struct nk_message base_;
__nk_alignas(4) nk_uint32_t h;
};
struct {
__nk_alignas(8)
struct nk_message base_;
__nk_alignas(4) nk_uint32_t h;
} res_;
struct Filesystem_Open_err err_;
};
} Filesystem_Open_res;
Методы и типы, используемые только на сервере
- Тип, содержащий все службы компонента, а также инициализирующая функция. (Для каждого компонента сервера.)
При наличии вложенных компонентов этот тип также содержит их экземпляры, а инициализирующая функция принимает соответствующие им инициализированные структуры. Таким образом, при наличии вложенных компонентов, их инициализацию необходимо начинать с самого вложенного.
В нашем примере будет сгенерирована структура
Operations_component
и функцияOperations_component_init
:typedef struct Operations_component {
struct Filesystem *FS;
};
void Operations_component_init(struct Operations_component *self,
struct Filesystem *FS)
- Тип, содержащий все службы, предоставляемые сервером непосредственно; все экземпляры компонентов, входящие в сервер; а также инициализирующая функция.
В нашем примере будет сгенерирована структура
Server_entity
и функцияServer_entity_init
:typedef struct Server_component {
struct : Operations_component *OpsComp;
} Server_component;
void Server_entity_init(struct Server_entity *self,
struct Operations_component *OpsComp)
- Типы, определяющие структуру фиксированной части сообщения для любого метода конкретного интерфейса.
В нашем примере будет сгенерировано два таких типа:
Filesystem_req
(для запроса) иFilesystem_res
(для ответа).typedef union Filesystem_req {
struct nk_message base_;
struct Filesystem_Open_req Open;
};
typedef union Filesystem_res {
struct nk_message base_;
struct Filesystem_Open_res Open;
};
- Типы, определяющие структуру фиксированной части сообщения для любого метода любой службы конкретного компонента.
При наличии вложенных компонентов эти типы также содержат структуры фиксированной части сообщения для любых методов любых служб, включенных во все вложенные компоненты.
В нашем примере будет сгенерировано два таких типа:
Operations_component_req
(для запроса) иOperations_component_res
(для ответа).typedef union Operations_component_req {
struct nk_message base_;
Filesystem_req FS;
} Operations_component_req;
typedef union Operations_component_res {
struct nk_message base_;
Filesystem_res FS;
} Operations_component_res;
- Типы, определяющие структуру фиксированной части сообщения для любого метода любой службы конкретного компонента, экземпляр которого входит в сервер.
При наличии вложенных компонентов эти типы также содержат структуры фиксированной части сообщения для любых методов любых служб, включенных во все вложенные компоненты.
В нашем примере будет сгенерировано два таких типа:
Server_entity_req
(для запроса) иServer_entity_res
(для ответа).typedef union Server_component_req {
struct nk_message base_;
Filesystem_req OpsComp_FS;
} Server_component_req;
typedef union Server_component_res {
struct nk_message base_;
Filesystem_res OpsComp_FS;
} Server_component_res;
- Dispatch-методы (диспетчеры) для отдельного интерфейса, компонента или класса процессов.
Диспетчеры анализируют полученный запрос (значения RIID и MID), вызывают реализацию соответствующего метода, после чего сохраняют ответ в буфер. В нашем примере будут сгенерированы диспетчеры
Filesystem_interface_dispatch
,Operations_component_dispatch
иServer_entity_dispatch
.Диспетчер класса процессов обрабатывает запрос и вызывает методы, реализуемые этим классом. Если запрос содержит некорректный RIID (например, относящийся к другой службе, которой нет у этого класса процессов) или некорректный MID, диспетчер возвращает
NK_EOK
илиNK_ENOENT
.nk_err_t Server_entity_dispatch(struct Server_entity *self,
const
struct nk_message *req,
const
struct nk_arena *req_arena,
struct nk_message *res,
struct nk_arena *res_arena)
В специальных случаях можно использовать диспетчеры интерфейса и компонента. Они принимают дополнительный аргумент – ID реализации интерфейса (
nk_iid_t
). Запрос будет обработан только если переданный аргумент и RIID из запроса совпадают, а MID корректен. В противном случае диспетчеры возвращаютNK_EOK
илиNK_ENOENT
.nk_err_t Operations_component_dispatch(struct Operations_component *self,
nk_iid_t iidOffset,
const
struct nk_message *req,
const
struct nk_arena *req_arena,
struct nk_message *res,
struct nk_arena *res_arena)
nk_err_t Filesystem_interface_dispatch(struct Filesystem *impl,
nk_iid_t iid,
const
struct nk_message *req,
const
struct nk_arena *req_arena,
struct nk_message *res,
struct nk_arena *res_arena)