Потоки в OS X: как получить CPU usage всех потоков в чужой программе?

  • Tutorial
Добрый день, уважаемые хабровчане-маководы!

В [Mac] OS X имеется замечательный встроенный инструмент — Activity Monitor, который легко покажет занимаемую процессом память и процессорное время. Что ж, это очень хорошо, но иногда хочется странного. Например, посмотреть, сколько у процесса потоков (threads) и сколько CPU кушает каждый из них. Тут уже Activity Monitor нам никак не может помочь, увы, а файловой системы procfs здесь бывалый линуксоид не найдёт. Придётся решать эту проблему своими силами.

Сегодня я поведаю вам о том, как написать маленькую консольную программку, которая будет на вход принимать PID процесса и на выходе давать информацию о CPU usage каждого потока этой программы (а так же общий usage).

Писать будем на чистом C, у нас будет всего один файл исходников, и я решил не использовать Xcode для такого мелкого проекта, пусть будет обычный Makefile.

Для начала немного теории. Нам надо из нашей программы подключиться к некоей сторонней, запросить её список потоков и получить свойства каждого потока. Для этого нам надо использовать функции для работы с задачами и их потоками: task_for_pid() и task_threads().

Но не всё так просто, увы. Для использования этих функций нужны особые права для программы (назовём её threadmon, но это не принципиально). Как подсказывают компетентные источники, до версии Mac OS X 10.5 ничего не требовалось, но потом в целях безопасности были введены такие вот ограничения. А это всё значит, что нам надо будет подписать наш исполняемый файл своим сертификатом, а так же перед вызовом наших функций запросить у пользователя права на их исполнение через фреймворк Security. Что ж, начнём с начала: напишем функцию, запрашивающую права у пользователя:

#include <Security/Authorization.h>

int acquireTaskportRight()
{
	OSStatus stat;
	AuthorizationItem taskport_item[] = {{"system.privilege.taskport:"}};
	AuthorizationRights rights = {1, taskport_item}, *out_rights = NULL;
	AuthorizationRef author;

	AuthorizationFlags auth_flags = kAuthorizationFlagExtendRights | kAuthorizationFlagPreAuthorize | kAuthorizationFlagInteractionAllowed | ( 1 << 5);

	stat = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, auth_flags, &author);
	if (stat != errAuthorizationSuccess)
	{
		return 1;
	}

	stat = AuthorizationCopyRights(author, &rights, kAuthorizationEmptyEnvironment, auth_flags, &out_rights);
	if (stat != errAuthorizationSuccess)
	{
		return 1;
	}
	return 0;
}

Собственно, данная функция запросит у пользователя права на привелегию taskport, необходимую для успешного вызова task_for_pid(). Теперь нам надо в начале функции main() вызвать acquireTaskportRight() и проверить возвращаемое значение: 0 — всё ок, иначе — привелегии не получены. Что ж, пишем дальше. Пусть наша программа на вход получает pid процесса, для которого будем получать информацию. Пишем в функции main():

int main(int argc, char *argv[])
{
	if (argc != 2)
	{
		printf("Usage:\n    %s <PID>\n", argv[0]);
		return -1;
	}

	if (acquireTaskportRight())
	{
		printf("No rights granted by user or some error occured! Terminating.\n");
		return -2;
	}

	char* end;
	pid_t pid = strtol(argv[1], &end, 10);
	if (*end)
	{
		printf("Error: invalid PID given: \"%s\", terminating.\n", argv[1]);
		return -3;
	}

	printf("Starting threadmon for PID %d\n", pid);
	// TODO: the rest
}

Теперь переходим к самому интересному. Будем получать таск и все его потоки из pid'а:

	task_t port;
	kern_return_t kr = task_for_pid(mach_task_self(), pid, &port);
	if (kr != KERN_SUCCESS)
	{
		printf("task_for_pid() returned %d, terminating.\n", kr);
		return -4;
	}

	thread_array_t thread_list;
	mach_msg_type_number_t thread_count;

	thread_info_data_t thinfo;
	mach_msg_type_number_t thread_info_count;

	thread_basic_info_t basic_info_th;

	// get threads in the task
	kr = task_threads(port, &thread_list, &thread_count);
	if (kr != KERN_SUCCESS)
	{
		printf("task_threads() returned %d, terminating.\n", kr);
		return -5;
	}

Теперь дело за малым: пробежаться по всем полученным тредам и вытянуть из них нужную нам информацию:

	long tot_cpu = 0;
	int j;

	for (j = 0; j < thread_count; j++)
	{
		thread_info_count = THREAD_INFO_MAX;
		kr = thread_info(thread_list[j], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count);
		if (kr != KERN_SUCCESS)
		{
			printf("Thread %d: Error %d\n", thread_list[j], kr);
			continue;
		}
		basic_info_th = (thread_basic_info_t)thinfo;

		if (!(basic_info_th->flags & TH_FLAGS_IDLE))
		{
			tot_cpu = tot_cpu + basic_info_th->cpu_usage;
			printf("Thread %d: CPU %d%%\n", thread_list[j], basic_info_th->cpu_usage);
		}
	}
	printf("---\nTotal: CPU %ld%%\n", tot_cpu);
	return 0;

Что ж, мы получили вполне жизнеспособную программу, которую уже почти можно использовать. Маленький нюанс: прав у программы по прежнему нет. Не дали мы их. Нам нужно сделать ещё две вещи: добавить Info.plist и подписать полученный бинарник!

Создаём примерно такой плист:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleIdentifier</key>
    <string>com.silvansky.threadmon</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>threadmon</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>SecTaskAccess</key>
    <array>
      <string>allowed</string>
    </array>
  </dict>
</plist>

Внимание стоит обратить на последний ключ: SecTaskAccess, именно он нам нужен. Вот теперь нам надо в Makefile внести изменения: добавлять наш плист во время линковки:

LOPTS=-framework Security -framework CoreFoundation -sectcreate __TEXT __info_plist ./Info.plist

Ну вот, теперь почти всё, система поймёт, какие права нужны программе для успешной работы. Но не даст их программе, пока мы её не подпишем нашим сертификатом разработчика.

Тут можно долго рассуждать о сертификатах и ключах, о Developer ID и прочем, но я лишь кратко опишу ситуацию: если у Вас есть Developer ID сертификат, то подписывайте им смело. Если же его нет, можете сгенерировать самоподписаный сертификат для codesign через Keychain. Но у меня последний способ не заработал, но это, как говорят, проблема в OS X 10.8, на более ранних системах должно завестись.

Но, опять же, можно и не подписывать, если Вам не лень каждый раз набирать sudo перед запуском этой утилиты. =)

Подписываем:

codesign -s "your-certificate-name" ./threadmon

Тестируем:

$ ps -A | grep Xcode
  775 ??       617:02.82 /Applications/Xcode.app/Contents/MacOS/Xcode -psn_0_348245
73761 ttys005    0:00.00 grep Xcode
$ ./threadmon 775
Starting threadmon for PID 775
Thread 6147: CPU 55%
Thread 6403: CPU 0%
Thread 6659: CPU 0%
Thread 6915: CPU 0%
Thread 7171: CPU 0%
Thread 7427: CPU 0%
Thread 7683: CPU 0%
Thread 7939: CPU 0%
Thread 8195: CPU 0%
Thread 8451: CPU 0%
Thread 8707: CPU 0%
Thread 8963: CPU 0%
Thread 9219: CPU 0%
Thread 9475: CPU 0%
Thread 9731: CPU 0%
Thread 9987: CPU 0%
Thread 10243: CPU 0%
Thread 10499: CPU 0%
Thread 10755: CPU 0%
Thread 11011: CPU 0%
Thread 11267: CPU 0%
Thread 11523: CPU 22%
Thread 11779: CPU 7%
Thread 12035: CPU 32%
Thread 12291: CPU 46%
Thread 12547: CPU 14%
Thread 12803: CPU 0%
---
Total: CPU 176%

Ну что ж, вполне достойно! За полным исходным кодом, как обычно, отправляю Вас на гитхаб.

PS: В статье использованы фрагменты кода, взятые из ответов на StackOverflow и разнообразных личных блогов.
PPS: Если Вы знаете способ лучше, видите явные и неявные дефекты в коде — не стесняйтесь, пишите комменты!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 15

    +1
    вообще утилита Instruments умеет все это смотреть со всеми подробностями и красивым гуи :)
      +1
      Instruments тяжеловат да и не везде его можно поставить. А скомпиленную утилитку перекинуть — легко. =)
        +1
        ну Instruments тяжеловат разве что для старых конфигураций, уж что может быть быть хуже макбука с кор2дуо, но вполне нормально на нем работает.
        кстати для старых конфигурация пойдет отличный Shark, он идет в комплекте на Snow Leopard, из львов его уже выпилили. Менее жрущий.
        Но ваше решение тоже вполне, бесспорно :)
          +1
          Чей-либо рабочий бук «засорять» инструментами разработчика обычно не разрешают =)
          В этом проблема ещё.

          А моя программка маленькая и шустрая.
          +2
          Согласен. На мой взгляд отличное решение для поверхностного профилирования приложения на машинах во время тестирования.
        +1
        Есть procfs, только собрать надо :) macfuse.googlecode.com/svn/trunk/filesystems/procfs/
          +1
          Да, это можно. Но! В данном случае это весьма нестабильное решение, которое может привести к Kernel Panic. Поэтому его я так же решил не использовать.
            0
            Почему? fuse весьма стабилен, а сам «драйвер» работает в userspace.
              +1
              Именно procfs нестабилен, увы. Информация из открытых источников и личного опыта =)
          0
          176% в итоге — как-то многовато.
            +1
            Почему же? Это в расчёте на одно ядро, а у меня их 4, к примеру. Activity Monitor так же может показывать много процентов.
            А Xcode в режиме отладки может и больше съедать, чем 176 =)
              +1
              Валя, вот если бы 146 — тогда окей, максимальная загрузка.
            +1
            Баловался написанием системных утилит: github.com/astavonin/Tasks-Explorer#readme. В целом, возможностей больше чем у родного Эплового Activity Monitor. Последнюю версию лучше собрать из исходников.
              +1
              Здорово, обязательно поизучаю код на досуге!
                +1
                Ну а если будет желание под дописать — так вообще супер ;)

            Only users with full accounts can post comments. Log in, please.