Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Procesor a jádra, běh na určeném jádru v C.

29.12.2022

Ukážeme si jak v jazyce C získat informace o počtu jader procesoru a také jak omezit běh aplikace nebo jednotlivého vlákna na jedno nebo více vybraných jader. Na úvod ale nejprve stručný popis jak jako uživatel z příkazové řádky nastavit konkrétnímu programu jako celku jádra která může využívat.

Příkazem taskset můžeme spustit zvolený program s omezením na vybraná jádra nebo totéž nastavit již běžícímu programu. pro spuštění zavoláme příkaz taskset následovaný hodnotou bitové masky a jménem programu. Pro nastaveni běžícímu programu pak pužijeme parametr -p a místo jména pak PID konkrétního běžícího procesu. Bity v masce představují jednotlivá jádra počítáno zprava (jádro 0), takže napříkad takto omezíme program na jádra 0 a 3.

$ taskset 0x00000009 jmeno_programu

Pro běžící proces s PID rovným 1234 pak takto:

$ taskset =p 0x00000009 1234

Místo číselné hodnoty bitové masky můžeme také použít vyjmenování povolených jader, s oddělením čárkou a to můžeme kombinovat s rozsahem odděleným pomlčkou. Například pro jádra 0,1,2,6 můžeme použít:

$ taskset -c 0-2,6 jmeno_programu

Zjištení počtu jader procesoru

Zjistit počet jader procesoru můžeme několika způsoby. Například můžeme enumerovat podsložky ve složce /dev/cpu, které jsou nazvány od "0" do počtu jader mínus jedna.

Zde použijeme možnost využitím funkce sysconf, která nám kromě mnoha dalších parametrů systému umožňuje zjistit počet procesorů které jsou "konfigurované" systémem, tedy o kterých systém ví a může je použít pokud jsou online, tj. nejsou právě vypnuté z důvodu úspory energie nebo z jiné příčiny. A právě počet aktuálně online jader také můžeme funkcí sysconf zjistit. Napíšeme si tedy následující funkci která nám vypíše výše uvedené počty:

static void show_cpu_counts(void)
{
	size_t poc = sysconf(_SC_NPROCESSORS_CONF);
	printf("pocet procesoru konfigurovanych systemem: %lu\n", poc);
	poc = sysconf(_SC_NPROCESSORS_ONLN);
	printf("pocet dostupnych procesoru online: %lu\n", poc);
}

Pomocí funkce sched_setaffinity můžeme určit která konkrétní jádra procesoru může aplikace použít. Nastavujeme je jako bitové příznaky ve struktuře cpu_set_t, s tím že máme k disposici příslušná makra a jádra jsou číslována od nuly (jak jsme ostatně v céčku zvyklí).

Vyzkoušíme v praxi. Povolíme aplikaci využívat první a poslední jádro (s předpokladem že máme alespoň 4-jádrový procesor, aby se účinek projevil).

static void test_set_cpu(void)
{
	size_t poc = sysconf(_SC_NPROCESSORS_ONLN);
	if (poc < 2)
		exit(EXIT_FAILURE);
	cpu_set_t cpuset;
	CPU_ZERO(&cpuset);
	CPU_SET(0, &cpuset);
	CPU_SET(poc - 1, &cpuset);
	sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
}

Dále si napíšeme funkci která vypíše počet povolených jader a jako řadu jedniček a nul, která konkrétní jádra jsou povolena:

static void cpu_info(void)
{
	cpu_set_t cpuset;
	CPU_ZERO(&cpuset);
	sched_getaffinity(0, sizeof(cpu_set_t), &cpuset);
	size_t poc = CPU_COUNT(&cpuset);
	printf("poc. prirazenych jader: %lu\n", poc);
	poc = sysconf(_SC_NPROCESSORS_ONLN);
	for (size_t i = 0; i < poc; i++)
	{
		printf(CPU_ISSET(i, &cpuset) ? "1 " : "0 ");
	}
	printf("\n");
}

Pro další testování rychlosti a výkonu vláken si připravíme funkci vlákna která bude mezi náhodně generovanými čísly načítat součet všech která jsou dělitelná číslem 1234 a při každém přírůstku o 800 do terminálu aktuální součet do řádku podle indexu konkrétního vlákna, takže budeme moci za běhu sledovat rozdíl v rychlosti vláken. Konkrétní hodnoty si samozřejmě můžete upravit podle potřeby podle výkonu procesoru.

static void* thread_proc_work(void* arg)
{
	while (1)
	{
		if (0 == (rand() % 1234))
		{
			_sums[(size_t)arg]++;
		}
		if (0 == (_sums[(size_t)arg] % 800))
		{
			pthread_mutex_lock(&_mutex);
			term_goto(1 + (int)((size_t)arg), 1);
			printf("thread: %lu, suma: %lu",
				(unsigned long)arg, (unsigned long)_sums[(size_t)arg]);
			pthread_mutex_unlock(&_mutex);
		}
	}
	return NULL;
}

Pro testování vlivu na výkon si na výše uvedené funkci spustíme vlákna v počtu odpovídajícím počtu jader procesoru s tím, že první polovině vláken přiřadíme každému vlastní jádro všem vláknům z druhé poloviny povolíme běh pouze na posledním jádře. Po spuštění pak uvidíme rozdíl v rychlosti první a druhé polovuny vláken. Ten bude samozřejmě tím výraznější čím více jader bude mít procesor. Nyní se jíž podívejme na kompletní výpis testovací aplikace včetně implemantace výše uvedených funkcí.

/* gcc -O2 -pthread main.c */
#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sched.h>
#include <unistd.h>
#include <termios.h>
#include <pthread.h>
#include <cpuid.h>
#include <errno.h>

static pthread_t* _threads = NULL;
static unsigned long* _sums = NULL;
static pthread_mutex_t _mutex;

static void term_erase(void)
{
	(void)fprintf(stdout, "\033[1;1H\033[2J");
}

static void term_goto(int row, int col)
{
	(void)fprintf(stdout, "\033[%d;%df", row, col);
}

static void* thread_proc_work(void* arg)
{
	while (1)
	{
		if (0 == (rand() % 1234))
		{
			_sums[(size_t)arg]++;
		}
		if (0 == (_sums[(size_t)arg] % 800))
		{
			pthread_mutex_lock(&_mutex);
			term_goto(1 + (int)((size_t)arg), 1);
			printf("thread: %lu, suma: %lu",
				(unsigned long)arg, (unsigned long)_sums[(size_t)arg]);
			pthread_mutex_unlock(&_mutex);
		}
	}
	return NULL;
}

static void test_working(void)
{
	pthread_mutex_init(&_mutex, NULL);
	srand(time(NULL));
	memset(_sums, 0, sizeof(unsigned long));
	int iret;
	cpu_set_t cpuset;
	size_t poc = sysconf(_SC_NPROCESSORS_ONLN);
	for (size_t i = 0; i < poc; i++)
	{
		iret = pthread_create(&_threads[i], NULL, &thread_proc_work, (void*)i);
		if (0 != iret)
			exit(EXIT_FAILURE);
		if (i < poc >> 1)
		{
			CPU_ZERO(&cpuset);
			CPU_SET(i, &cpuset);
			pthread_setaffinity_np(_threads[i], sizeof(cpu_set_t), &cpuset);
		}
		else
		{
			CPU_ZERO(&cpuset);
			CPU_SET(poc - 1, &cpuset);
			pthread_setaffinity_np(_threads[i], sizeof(cpu_set_t), &cpuset);
		}
	}
	term_erase();
	int ch;
	while ((ch = getchar()) != 'q')
	{
		if ('s' == ch)
		{
			for (size_t i = 0; i < poc; i++)
			{
				pthread_cancel(_threads[i]);
			}
		}
	}
	pthread_mutex_destroy(&_mutex);
}

static void cpu_info(void)
{
	cpu_set_t cpuset;
	CPU_ZERO(&cpuset);
	sched_getaffinity(0, sizeof(cpu_set_t), &cpuset);
	size_t poc = CPU_COUNT(&cpuset);
	printf("poc. prirazenych jader: %lu\n", poc);
	poc = sysconf(_SC_NPROCESSORS_ONLN);
	for (size_t i = 0; i < poc; i++)
	{
		printf(CPU_ISSET(i, &cpuset) ? "1 " : "0 ");
	}
	printf("\n");
}

static void test_set_cpu(void)
{
	size_t poc = sysconf(_SC_NPROCESSORS_ONLN);
	if (poc < 2)
		exit(EXIT_FAILURE);
	cpu_set_t cpuset;
	CPU_ZERO(&cpuset);
	CPU_SET(0, &cpuset);
	CPU_SET(poc - 1, &cpuset);
	sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
}

static void show_cpu_counts(void)
{
	size_t poc = sysconf(_SC_NPROCESSORS_CONF);
	printf("pocet procesoru konfigurovanych systemem: %lu\n", poc);
	poc = sysconf(_SC_NPROCESSORS_ONLN);
	printf("pocet dostupnych procesoru online: %lu\n", poc);
}

int main(int argc, char** argv)
{
	struct termios _termios;
	size_t poc = sysconf(_SC_NPROCESSORS_ONLN);
	tcgetattr(STDIN_FILENO, &_termios);
	struct termios ti;
	cpu_set_t cpuset;
	if (tcgetattr(STDIN_FILENO, &ti) == -1)
		return errno;
	ti.c_lflag &= ~ECHO;
	ti.c_lflag &= ~ICANON;
	if (0 != tcsetattr(STDIN_FILENO, TCSAFLUSH, &ti))
		return errno;
	cfmakeraw(&ti);
	show_cpu_counts();
	_threads = (pthread_t*)malloc(poc * sizeof(pthread_t));
	_sums = (unsigned long*)malloc(poc * sizeof(unsigned long));
	test_set_cpu();
	cpu_info();
	for (size_t i = 0; i < poc; i++)
	{
		CPU_SET(i, &cpuset);
	}
	sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
	cpu_info();
	puts("enter pro sputeni testu");
	(void)getchar();
	test_working();
	free(_threads);
	free(_sums);
	tcsetattr(STDIN_FILENO, TCSAFLUSH, &_termios);
	term_erase();
	return EXIT_SUCCESS;
}

Aplikaci sestavíme pomocí GCC např. takto:

$ gcc -O2 -pthread main.c