Большинству программ, тесно взаимодействующих с операционной системой, в которой они работают, необходимы данные о текущем состоянии системы. Это отладчики, компиляторы, системы мониторинга, да и просто полезные утилиты. То есть информация это во всех смыслах интересная и необходимая, однако методы ее получения от системы к системе (а для разработчика смена базовой ОС разработки — привычная практика, особенно при разработке многоплатформенных решений) различаются. И ?в? QNX это особенно ярко выражено.
В первую очередь это сетевая система («Ага, удивил!», — усмехнется незнакомый с QNX читатель, но будет не прав). Архитектура QNX настолько прозрачна, а сетевая поддержка настолько глубоко интегрирована, что ни пользователю, ни разработчику, ни системе не видно никакой разницы между тем с каким устройством (файлом!) работать: с локальным или с удаленным. Любую программу можно «разделить» между узлами сети, вместо своего жесткого диска или CD-ROMа можно использовать соседский, а такие понятия, как «расшаренный ресурс» или «сетевой принтер» быстро забываются, как не очень приятный сон.
Сильно отличается архитектура диспетчера процессов. В большинстве ОС он спрятан где-то глубоко внутри ядра. В QNX же, менеджер процессов — вещь опциональная, хотя и практически всегда применяемая. А как же, спросите вы, такое чудо работает? Очень просто, как и все в QNX: системой управляет микроядро, для которого есть только несколько типовых понятий, как то: «сообщение», «поток», «барьер», «мутекс». Микроядро занимается диспетчеризацией потоков, а вся забота о процессах возложена на менеджер процессов procnto, построенный в виде менеджера ресурсов.
На менеджерах ресурсов базируется вся операционная система QNX. А обмен данными между ними и их клиентами — приложениями реализован посредством системных сообщений. За их доставку отвечает микроядро, в котором всего-то 31 килобайт кода. Отсюда кстати и работа всех процессов в полностью защищенном режиме, что так нехарактерно для операционных систем реального времени. И драйвера (тоже отдельные процессы), кстати, работают во втором, или, в крайнем случае, в первом кольце защиты. А нулевое принадлежит микроядру.
Архитектурная модель системы выполнена идеально, это, безусловно, лучшая на сегодняшний день организация ОС такого рода. Разработчики, «проникшиеся» гениальной очевидностью ее организации, часто после этого с тоской смотрят на вечнозеленый Linux и Embedded XP:
Однако надежная система требует ответственного подхода при разработке, ведь ОС — это лишь инструмент. Ответственный подход здесь — не только умение продумать и реализовать максимально эффективный алгоритм, но и вдумчиво протестировать свое детище. Об этом, собственно, и речь.
Самое для нас интересное — номер версии и тип системы, можно получить привычным для UNIX -разработчиков методом — функцией uname():
#include <sys/utsname.h>
struct utsname hostinfo;
uname(&hostinfo;);
printf( «System: \t%s \nRelease: \t%s \nVersion \t%s \nMachine \t%s \nNodename:
\t%s\n»,
hostinfo.sysname, // название системы
hostinfo.release, // номер версии.
hostinfo.version, // дата сборки
hostinfo.machine, // тип архитектуры
hostinfo.nodename // сетевое имя компьютера.
);
В результате получим:
System: QNX
Release: 6.1.0
Version: 2001/08/23-19:38:50edt
Machine: x86pc
Nodename: dmitry
Вывод этой программы аналогичен выводу программы uname с ключом ‘-a’. В QNX поддерживаются все стандартные для UNIX методы получения данных такого рода, но они не особо интересны, т.к. QNX это не совсем UNIX. Как я уже упоминал, менеджер процессов работает как драйвер псевдоустройства. За ним зарезервирован префикс «/proc». Это тоже привычная для UNIX-подобных систем практика, здесь как и везде, команда «ls /proc» выведет список идентификаторов процессов (PID) в системе. Но, при этом, есть и свои добавки, например, «размер» каталога «/proc» означает количество свободной оперативной памяти. В статье нет смысла приводить обработку ошибок, здесь и далее я ее опускаю
#include
#include <sys/stat.h>
#include
struct stat st;
int fd;
fd = open(«/proc», O_RDONLY );
fstat( fd, &st; );
printf( «Free memory: %u\n», st.st_size );
Мы узнали, что свободной памяти у нас с избытком и сразу же захотели узнать как с этим обстоят дела у соседа по сети J. Подправим программу (здесь node_name — сетевое имя):
fd = open(«/net/node_name/proc», O_RDONLY );
«Ух ты как!», — воскликнет читатель, — «а как же безопасность? Я что, могу читать любые файлы в сети?». Да. Понятия безопасности внутри QNX сети нет. Либо вы работаете совместно, в одной группе и целиком друг другу доверяете, либо выгружаете модуль QNET из памяти и отключаете его загрузку по умолчанию, оставляя только TCP/IP. Структура QNET прозрачна даже на уровне API. Рассмотрим, например, функцию SignalKill() — послать сигнал процессу
extern int SignalKill(_Uint32t __nd, pid_t __pid, int __tid, int __signo, int __code, int __value);
Первый ее параметр — дескриптор узла сети. В случае локального процесса это 0, иначе — идентификатор узла сети, получаемый из имени. В программах типа slay (послать сигнал процессу по имени, аналог скрипта killall) и pidin (получить информацию о процессах, расширенный аналог ps), принимающих имя узла сети в виде параметра после ключа ‘-n’:
#include <sys/netmgr.h> /* заголовочный файл менеджера сети */
unsigned nd; /* дескриптор узла */
c = getopt(«:n:..»); /* обработка параметров командной строки (n: значит ключ -n с параметром */
switch(c) {
…
‘n’: if (-1 == (nd = netmgr_strtond(optarg, 0))){ /* преобразуем значение параметра ‘-n’ в дескриптор узла */
exit(1); /* указано неверное имя узла. */
}
/* сравним полученное значение с именем локального узла */
if (ND_NODE_CMP(nd, ND_LOCAL_NODE)==0) {
/* это локальный узел */
}
break;
}
Самые главные сведения о системе хранятся в структуре типа procfs_sysinfo, получение параметров из которой мы и рассмотрим в качестве первого примера:
#include
#include
#include <sys/procfs.h> /* заголовочный файл менеджера процессов */
#include
#include
procfs_sysinfo *sysinfo; /* структура с данными менеджера процессов */
char buffer [100]; /* промежуточный буфер, см. ниже */
int fd; /* файловый дескриптор каталога «/proc» */
int i; /* итератор цикла */
int total; /* сколько у нас всего памяти ? */
time_t boot_time; /* время загрузки */
struct cpuinfo_entry cpu; /* структура, описывающая системный процессор (процессоры) */
int mtype; /* тип архитектуры */
int num_cpu; /* количество процессоров */
Менеджер процессов — драйвер псевдоустройства, для которого справедливы все стандартные операции для файлов, как, например, получение информации о файле (stat) или открытие его на чтение:
fd =open(«/proc», O_RDONLY);
Этим запросом мы на самом деле вовсе не открыли некий файл с диска своей (или чужой) машины, а создали программный канал, по которому наша программа будет обмениваться данными с менеджером процессов.
Структура procfs_sysinfo имеет переменную длину, которая зависит от параметров системы: типа архитектуры (поддерживаются MIPS, PPC, SH4, ARM и, конечно же, X86 ), количества процессоров и т.д. Поэтому сначала определим ее размер, который гарантированно расположен в первых ста байтах:
sysinfo = (void *) buffer;
if( -1 == devctl( fd, DCMD_PROC_SYSINFO, sysinfo, sizeof buffer, 0 ) ) {
perror («devctl»);
exit( 1 );
}
Ага, devctl() — это что-то вроде привычного ioctl(), без чего ни один драйвер не может быть полностью функционален. Фактически это тоже сообщение менеджеру процессов, с вектором ввода/вывода (IOV) установленным на sysinfo. Действительный размер структуры содержится в переменной total_size, делаем поправку и читаем данные заново:
i = sysinfo->total_size;
sysinfo=(void*)malloc(i); /* выделим необходимое количество памяти */
if( -1 == devctl( fd, DCMD_PROC_SYSINFO, sysinfo, i, 0 ) ) {
perror («devctl»);
exit( 1 );
}
Поскольку структура procfs_sysinfo разделена на несколько логических блоков (информация о процессорах, памяти, прерываниях, временных параметрах системы, аппаратуре и т.д.), для получения данных из нее реализован специальный макрос — _SYSPAGE_ENTRY, принимающий в качестве параметра имя структуры и название логического блока. Вот так выглядит код, получающий общее количество оперативной памяти в байтах:
total = _SYSPAGE_ENTRY(sysinfo, system_private )->ramsize;
Время в секундах, когда система была загружена:
boot_time = _SYSPAGE_ENTRY( sysinfo, qtime )->boot_time;
printf( «booted at %s\n», ctime(&boot;_time) );
Тип архитектуры. Для x86 — SYSPAGE_X86 и т.д.
mtype = sysinfo->type;
Количество процессоров:
num_cpu = sysinfo->num_cpu;
Пройдемся по всем процессорам и посмотрим, что они из себя представляют:
for( i=1, cpu =_SYSPAGE_ENTRY( sysinfo, cpuinfo ); i<= sysinfo->num_cpu; i++, cpu++ )
{
printf( «CPU#%d: «, i ); /* Порядковый номер процессора */
printf( «%u «, cpu->cpu ); /* Тип процессора. В моем случаем 686 */
/* Строковое описание процессора. В моем случае Intel Pentium Celeron Stepping 5 */
printf( «%s «, &_SYSPAGE_ENTRY( sysinfo, strings )->data[cpu->name] );
/* тактовая частота процессора */
printf( «%d Mhz «, cpu->speed );
/* флаги процессора */
printf( «FPU: %s MMU: %s»,
cpu->flags & CPU_FLAG_FPU ? «yes»:»no», /* FPU */
cpu->flags & CPU_FLAG_MMU ? «yes»: «no» ); /* MMU */
} //for
Распечатаем список основных блоков памяти:
struct meminfo_entry *meminfo;
meminf=(struct meminfo_entry *)SYSPAGE_ENTRY(meminfo);
while ((meminf != 0) && (meminf->type != MEMTYPE_NONE)) {
printf( «addr: %p size %x type: %d \n»,
meminf->addr, /* начальный адрес блока памяти */
meminf->size, /* размер блока */
meminf->type ); /* его тип */
meminf++; /* переходим к следующему блоку */
} //while
Вывод результатов работы этой программы вызывает недоумение, не правда ли? Сначала идет блок типа MEMTYPE_RAM(1), потом MEMTYPE_IMAGEFSYS(2), MEMTYPE_BOOTRAM(3), а потом снова MEMTYPE_RAM. Да, QNX хранит в своей памяти образ ядра (.boot или qnxbasedma.ifs), потому как драйвер файловой системы загружается не всегда. И пока я этого не увидел, я не мог понять, почему вызов mmap() в недоделанном lxrun возвращает ENOTSUP.
Кроме перечисленных типов блоков памяти есть MEMTYPE_IOMEM (блоки памяти для работы с портами ввода/вывода, это не актуально для x86) и MEMTYPE_FLASHFSYS.
Здесь описана лишь малая часть из того, что можно «выжать» из структуры procfs_sysinfo, но этого и заголовочных файлов вполне достаточно, чтобы разобраться с механизмом devctl() и спецификой procnto.
Теперь напишем программу, запрашивающую информацию о всех процессах и потоках в системе:
#include
#include
#include
#include
#include
#include <sys/procfs.h>
int main() {
DIR * dir; /* дескриптор каталога */
struct dirent dirent; /* дескриптор элемента каталога */
int fd; /* файловый дескриптор (для процесса ) */
pid_t pid; /* PID, идентификатор процесса */
char buf[100]; /* буфер для формирования строки */
procfs_info info; /* информация о процессе */
procfs_status status; /* информация о потоке */
int errcode; /* код ошибки */
if (!(dir = opendir( «/proc»))) { /* пытаемся открыть каталог */
exit(EXIT_FAILURE);
}
while (dirent = readdir(dir)) { /* ищем элемент каталога */
if(!isdigit(dirent->d_name[0])) /* это точно процесс ? */
continue;
pid = atoi(dirent->d_name); /* преобразуем в число */
if (pid <= 1 ) /* либо это чушь, либо procnto, в
любом случае опрашивать это не имеет
смысла */
continue;
sprintf( buf, «/proc/%d/as», pid ); /* …именно так */
/* пробуем его открыть и запросить информацию */
if (-1 == (fd = open(buf, O_RDONLY)) ||
EOK != devctl(fd, DCMD_PROC_INFO, &info;, sizeof info, 0 ) ) {
exit(EXIT_FAILURE);
} //while
В info находится информация об опрошенном процессе. В этой структуре 29 параметров, и я не буду описывать ее здесь. Описание можно посмотреть в заголовочном файле debug.h, структура _debug_process_t.
/* получим данные по всем потокам процесса */
for( status.tid =1 ; ; ++status.tid ) {
/* запрос на информацию по состоянию потока */
errcode = devctl(fd, DCMD_PROC_TIDSTATUS, &status;, sizeof(status), 0);
/* если потоков больше нет, то выходим из цикла
и переходим к следующему процессу */
if (errcode == ESRCH) {
break;
}
/* произошла ошибка. */
if( errcode )
exit(EXIT_FAILURE);
else
В этой структуре, как и в _debug_process_t, слишком много параметров, чтобы рассматривать их в этой статье. Так что для примера распечатаем только идентификатор потока.
printf( «tid: %d \n», status.tid );
} // for
}
close(fd);
closedir(dir);
} // main()
Это, конечно же, только малая часть. Точно таким же методом можно получить список открытых файловых дескрипторов процесса, сохраненные регистры процессора или сопроцессора, стек процесса, список переменных окружения и т.д.