zur Übersicht: Linux: meine Software und Konfigurationstipps

automatische Reduktion der I/O-Priorität von Systemprozessen

06.12.2005

Ich habe mich lange geärgert, dass bestimmte, nachrangige Prozesse mit hoher I/O-Last mein KDE komplett zum Stillstand bringen, obwohl die CPU-Last irgendwo bei 2% herumdüpelt. Seit dem Kernel 2.6.13 hat Linux einen neuen Scheduler (CFQ – Complete Fair Queueing), der es erlaubt, dieses Problem zu lösen. Es gibt drei Prioritätsklassen: real time, best effort und idle. Prozesse in der Gruppe idle kommen nur dann zum Zug, wenn kein Prozess einer höheren Gruppe noch I/O-Zugriffe ausstehen hat.

Die Herausforderung liegt also darin, die I/O-Priorität aller "unwichtigen" Prozesse auf idle zu reduzieren. Da es schwierig wäre, alle fraglichen Prozesse so starten zu lassen, habe ich einen zentralen Ansatz gewählt: Ein Script prüft alle paar Sekunden, ob neue Prozesse erzeugt worden sind, und manipuliert sie gegebenenfalls.

Das Script setzt die Priorität auf idle, wenn folgende Bedingungen erfüllt sind:

Das Script schreibt bei jedem Durchlauf seine Aktivitäten auf die Standardausgabe, also die (Nicht-)Behandlung neuer Prozesse und das Löschen nicht mehr vorhandener aus der Liste. Da das Script sinnigerweise als Hintergrundprozess gestartet wird, kann man sich den Hash der aktuell bekannten Prozesse ausgeben lassen, indem man dem Script das Signal -USR1 schickt. Es wird dann eine Datei (siehe Script, momentan /tmp/ioniced.dump) erzeugt, die zeilenweise (unsortiert) die PIDs und durch Leerzeichen getrennt den I/O-Status enthält. Wurde die I/O-Prioritätsklasse gesetzt, wird der entsprechende Wert ausgegeben. Wurde der Wert unverändert gelassen, wird statt dessen 0 ausgegeben.

Perl-Warnung

Dies ist mein erstes Perl-Script, entsprechend grauenvoll mag es für Experten aussehen. Aber es scheint korrekt zu funktionieren.

modifiziertes ps

Ich habe ein kleines, sehr einfaches bash-Script geschrieben, das ich iops nenne, das die Ausgabe von ps um die ionice-Ausgabe ergänzt. Leider kann man mit ionice immer nur die Angabe für einen Prozess abfragen, weshalb die Ausgabe nicht sonderlich schnell ist, da ionice für jeden Prozess einmal aufgerufen werden muss.

Dieses Script wird hoffentlich irgendwann mal überflüssig, wenn ps um um eine entsprechende Option ergänzt wird. Das Script erlaubt keine Modifikation der ps-Ausgabe, ließe sich aber leicht anpassen.

#!/bin/bash
version=1.0
# This script shows the ionice value for all processes by adding this value
# to the output of ps.
type ionice &>/dev/null || { echo "ionice not available" >&2; exit 1; }
export i=0
/bin/ps -e -o pid,nice,args |
while read pid nice rest
do
if [ $i -eq 0 ]
then
#printf %5s'  ' "$pid"
#printf %13s'  ' IONICE
#echo "$rest"
echo "  PID         IONICE   NI COMMAND"
else
ionice_value=$(ionice -p "$pid" 2>/dev/null | sed -n 's/:.* ///p')
if [ -z "$ionice_value" ]
then
ionice_value=exited
fi
printf %5i'  ' $pid
printf %13s'  ' $ionice_value
printf %3i' ' $nice
echo "$rest"
fi
((i++))
done

Das Script zum Download – und die Signatur dazu.

Download


#!/usr/bin/perl -w

# version 1.1, 05.12.2005, Hauke Laging, http://www.hauke-laging.de/software/
# This software is licensed under the Gnu GPL, see http://www.fsf.org/

# This script has the task to prevent I/O intensive system processes from 
# annoying the user by slowing the system down. These typical administrative 
# processes are not required to complete within a short time so it makes sense 
# to reduce the I/O priority of these tasks (which usually impacts the performance 
# of user processes much more than the CPU proirity).
#
# This script does not affect processes which have been niced to a value below zero 
# (higher priority) or already ioniced (to any value).
# This is the most simple way to prevent a process from being affected by this script.
# You can configure if a white list or black list approach is used: Either only the 
# configured programs are affected or only those not.
#
# The script runs as a daemon and reads the current PID list from ps every few 
# seconds. It cannot (or at least does not) prevent tasks from starting with 
# high I/O priority (high := high enough to annoy) but this problem es solved 
# after a few seconds.
#
# Thus this script should be started in the background. It writes to STDOUT what 
# it does/doesn't do.
#
# This script requires kernel 2.6.13 or above and the availability of ionice.

# definitions
# signals:
# USR1: The current list of known processes and their I/O priotity change status 
#	is dumped into the file $dump_file. Values 1,2 and 3 represent set priorities 
#	whereas 0 represents an unchanged priority.

@args = ("/bin/bash","-c","type ionice");
system(@args);
if ($? != 0)
{
	print "The program 'ionice' is not available; aborting.\n";
	exit 1;
}

use integer;

$min_user_id = 1000;
$max_user_id = 2000;
$syslog_tag="ioniced.pl";
$0=$syslog_tag;
$sleep = 5;
$leave_inid_pids_untouched=1;
$whitelist=1;
$dump_file="/tmp/ioniced.dump";
# gegen Ueberlaeufe: gelegentlich alle bekannten PIDs auf Vorhandensein pruefen
%known_pids = (); # Werte: 1,2,3 fuer die gesetzten, 0 fuer die unveraenderten
%skip_these = ("init"=>0,"ps"=>0,"sshd"=>0,"syslog-ng"=>0,"httpd2-prefork"=>0,"pdflush"=>0); # Werte: Dummys
%ionice_these = (find=>3,tar=>3,updatedb=>3,mandb=>3,cp=>3,mv=>3,rpm=>3,gzip=>3); # values: new I/O priority level

sub dump_ioprios {
	if (not open(FH,"> $dump_file"))
	{
		return;
	}
	foreach (keys %known_pids)
	{
		print FH $_," ",$known_pids{$_},"\n";
	}
	close(FH);
}

$SIG{USR1}="dump_ioprios";

# BEGIN: initialize the hash with the present pids
@pslines = qx/ps -eo pid,ruid,nice,comm/;
$i=0;
foreach (@pslines)
{
	$i++;
	next if $i == 1; # header line pf ps output
	$_ =~ s/^\s+//;
	$_ =~ s/\s+$//;
	@elements = split(" +",$_);
	$pid=$elements[0];
	$ruid=$elements[1];
	$nice=$elements[2];
	$command=$elements[3];
	$change_it=1;
	$change_it=0 if ($leave_inid_pids_untouched);
	$change_it=0 if ($ruid>=$min_user_id && $ruid<=$max_user_id);
	$change_it=0 if ($nice<0);
	$change_it=0 if (exists($skip_these{$command}));
	if ($change_it) # execute this one conditionally only because it takes time
	{
		$ionice_status=qx/ionice -p $pid/;
		$ionice_status =~ s/:.*$//;
		$change_it=0 if ($ionice_status != "none");
	}
	if ($whitelist)
	{
		$change_it=0 unless (exists($ionice_these{$command}));
	}
	if ($change_it)
	{
		$io_class=3;
		$explicit_io_class=$ionice_these{$command};
		if ($whitelist && defined($explicit_io_class) && ($explicit_io_class eq 1 || $explicit_io_class eq 2 || $explicit_io_class eq 3))
		{
			$io_class=$explicit_io_class;
		}
		$known_pids{$pid}=$io_class;
		system("ionice -c${io_class} -p${pid}");
		print "PID ${pid} ($command) has been changed to I/O class ${io_class}.\n";
		if ($whitelist)
		{
			system("logger -t \"${syslog_tag}\" \"PID ${pid} ($command) has been changed to I/O class ${io_class}.\"");
		}
	}
	else
	{
		$known_pids{$pid}=0;
		print "PID ${pid}'s ($command) I/O class has not been changed.\n"
	}
}
print "Initialization finished.\n";
# END: initialize the hash with the present pids

while (1)
{
	sleep $sleep;
	print "\n\nChecking for new PIDs:\n";
	@pslines = qx/ps -eo pid,ruid,nice,comm/;
	$i=0;
	%current_pids=(); # Werte: Dummys
	foreach (@pslines)
	{
		$i++;
		next if $i == 1; # header line pf ps output
		$_ =~ s/^\s+//;
		$_ =~ s/\s+$//;
		@elements = split(" +",$_);
		$pid=$elements[0];
		$ruid=$elements[1];
		$nice=$elements[2];
		$command=$elements[3];
		$current_pids{$pid}=0;
		next if (exists($known_pids{$pid}));
		next if ($command eq "ps");
		$change_it=1;
		$change_it=0 if ($ruid>=$min_user_id && $ruid<=$max_user_id);
		$change_it=0 if ($nice<0);
		$change_it=0 if (exists($skip_these{$command}));
		if ($change_it) # execute this one conditionally only because it takes time
		{
			$ionice_status=qx/ionice -p $pid/;
			$ionice_status =~ s/:.*$//;
			chomp($ionice_status);
			$change_it=0 if ($ionice_status ne "none");
		}
		if ($whitelist)
		{
			$change_it=0 unless (exists($ionice_these{$command}));
		}
		if ($change_it)
		{
			$io_class=3;
			$explicit_io_class=$ionice_these{$command};
			if ($whitelist && defined($explicit_io_class) && ($explicit_io_class eq 1 || $explicit_io_class eq 2 || $explicit_io_class eq 3))
			{
				$io_class=$explicit_io_class;
			}
			$known_pids{$pid}=$io_class;
			system("ionice -c${io_class} -p${pid}");
			print "PID ${pid} ($command) has been changed to I/O class ${io_class}.\n";
			if ($whitelist)
			{
				system("logger -t \"${syslog_tag}\" \"PID ${pid} ($command) has been changed to I/O class ${io_class}.\"");
			}
		}
		else
		{
			$known_pids{$pid}=0;
			print "PID ${pid}'s ($command) I/O class has not been changed.\n"
		}
	}
	print "\n\nPID check\n";
	foreach (keys %known_pids)
	{
		unless (exists($current_pids{$_}))
		{
			delete($known_pids{$_});
			print "PID ",$_," does not exist any more and has been deleted from the hash.\n";
		}
	}
}