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

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