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

в 9:53, , рубрики: console, mac os x, OS X, security, threads, разработка, метки: , , , , ,

Потоки в OS X: как получить CPU usage всех потоков в чужой программе?Добрый день, уважаемыее-маководы!

В [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 %dn", 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 %dn", 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: Если Вы знаете способ лучше, видите явные и неявные дефекты в коде — не стесняйтесь, пишите комменты!

Автор: silvansky

Поделиться

* - обязательные к заполнению поля