Setting up boot-time service for local custom CPU Frequency Governor script (cpupower)

So, for the continuing saga was began with this post ...

I am trying to create a systemd unit to create a service that will run a user script, one shot only, during the boot/startup process.

Reviewing the output from

systemd-analyze critical-chain

which I show here

The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.

graphical.target @2min 12.383s
└─multi-user.target @2min 12.383s
  └─netperf.service @2min 12.160s +222ms
    └─network-online.target @2min 12.067s
      └─NetworkManager-wait-online.service @49.403s +18.636s
        └─NetworkManager.service @35.598s +13.785s
          └─dbus.service @35.590s
            └─basic.target @35.530s
              └─sockets.target @35.530s
                └─cups.socket @48.920s
                  └─sysinit.target @31.916s
                    └─systemd-timesyncd.service @31.712s +203ms
                      └─systemd-tmpfiles-setup.service @31.504s +182ms
                        └─systemd-journal-flush.service @5.554s +25.944s
                          └─systemd-journald.service @5.250s +283ms
                            └─systemd-journald.socket @5.233s
                              └─-.mount @5.218s
                                └─-.slice @5.218s

I conclude that I want my script, to set my CPU governor and frequency, to run before "multi-user.target" (or should I specify "graphical.target" ?) .

Many examples show using the reference "network.target" for the "After=" directive, but the above report doesn't show that in the sequence. So ... my initial stab at the "service unit" definition is as follows (/etc/systemd/system/localcpufreq.service):

[Unit]
Description=Local CPU Frequency Service and Monitor
After=network-online.target
Before=multi-user.target
StartLimitIntervalSec=0


[Service]
User=root
Type=oneshot
RemainAfterExit=no
ExecStart=/bin/bash /DB001_F2/Oasis/bin/HW_Admin__Power_SetFreqCPU.sh
Restart=on-failure
RestartSec=360

[Install]
WantedBy=multi-user.target

A quick glance will reveal a possible "conflict" or "overlap" between the values of

  • Before=multi-user.target

and

  • WantedBy=multi-user.target

I am not sure if that is a conflict, or whether, to avoid not getting the login, remove the "Install" section from that.

Any opinions?

My "cpupower" script as it stands:

#!/bin/bash

####################################################################################################
###
###	Script to make it easier to learn how to control the CPU 'governor' and CPU clock frequencies
###
###	Version 5.1
###
####################################################################################################
#23456789+123456789+123456789+123456789+123456789+123456789+123456789+123456789+123456789+123456789+

dbg=0
test "${1}" = "--debug1" && { dbg=1 ; shift ; }
test "${1}" = "--debug2" && { dbg=2 ; shift ; }

###
###	Edit this script to assign preset defaults appropriate for your local Desktop CPU
###
doMode=99	# userspace
doFreq=2	# 1900MHz
doDefault=0
doStatus=0
doCron=0
doService=0

shopt -s extglob

identifyGovernors()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING identifyGovernors ..." >&2

	#governors=$(echo "userspace performance ondemand conservative powersave schedutil junker" |
	governors=$(grep 'available cpufreq governors:' ${tmp} | 
		sed 's+\:\ +\:+' |
		cut -f2 -d":"    |
		awk -v dbg="${dbg}" 'BEGIN{
			split("", plist) ;

			plist[1,1]="userspace";
			plist[2,1]="performance";
			plist[3,1]="ondemand";
			plist[4,1]="conservative";
			plist[5,1]="powersave";
			plist[6,1]="schedutil";
			c=6 ;

			for( i=1 ; i <= c ; i++ ){
				plist[i,2]=0 ;
			} ;

			if( dbg == 2 ){
				for( j=1 ; j <= c ; j++ ){
					printf("plist[%s] = %s\n", j, plist[j,1] ) | "cat 1>&2" ;
				} ;
			} ;
		}{
			for( k=1 ; k <= NF ; k++ ){
				if( dbg == 2 ){ printf("\t $%d = %s\n", k, $k ) | "cat 1>&2" ; } ; 

				valid=0 ;
				for( m=1 ; m <= c ; m++ ){
					if( dbg == 2 ){ printf("\t\t plist[%d,1] = %s\n", m, plist[m,1] ) | "cat 1>&2" ; } ; 

					if( $k == plist[m,1] ){
						plist[m,2]=1
						valid=1
						break ;
					} ;
				} ;

				if( valid == 0 ){
					printf("\n NOTE:  Reported governor mode \"%s\" is not currently handled by programmed logic.\n", $k ) | "cat 1>&2" ;
				} ;
			} ;
		}END{
			for( n=1 ; n <= c ; n++ ){
				if( plist[n,2] == 1 ){
					printf("%s\n", plist[n,1] ) ;
				} ;
			} ;
		}'
	)
}


getGov()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING getGov ..." >&2

	case ${doMode} in
		 1 ) governor="performance"	;;
		 2 ) governor="conservative"	;;
		 3 ) governor="powersave"	;;
		 4 ) governor="ondemand"	;;
		 5 ) governor="schedutil"	;;
		98 ) ;;				### governor specified on command line
		99 ) governor="userspace"	;;
	esac
}


identifyFrequencies()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING identifyFrequencies ..." >&2

	steps=$(grep 'Pstate-' ${tmp} | 
		sed 's+\:\ \ +\:+' |
		cut -f2 -d":"    |
		awk '{
			n=split($0, vals, ",") ;
			for( i=1 ; i <= n ; i++ ){
				printf("%s\n", vals[i] ) ;
			} ;
		}'
	)
}

getFreq()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING getFreq ..." >&2

	case ${doMode} in
		[1-5] ) return ;;	### No need to so set frequency for these modes
		* ) ;;
	esac

	fmax=$(echo "${steps}" | head -1 )
	fmin=$(echo "${steps}" | tail -1 )
	test -n "${fmin}" || fmin=$(echo "${steps}" | tail -2 | head -1 )

	fopt=( $(echo ${steps} ) )
	len=${#fopt[*]}
	test ${dbg} -gt 0 && echo -e "\n Number of clock speeds identified: ${len}" >&2


	test ${dbg} -gt 1 && echo "doFreq = ${doFreq}" >&2
	case ${doFreq} in
		#(!+[0-9]) ) frequency="NULL" ;;
		97 )
			case "${frequency}" in
				+([0-9])"MHz" ) ;;
				#[0-9]"."+([0-9])"GHz" | [0-9]"."[0-9][0-9]"GHz" )
				[0-9]"."+([0-9])"GHz" )
					fval=$(echo "${frequency}" | sed 's+GHz++' )
					#fval=$(echo "scale=0 ; 1000 * ${fval}" | bc )		### This form doesn't work
					#fval=$(echo "1000 * ${fval}" | bc | cut -f1 -d\. )	### Kludgy fix
					fval=$(echo "1000 * ${fval}" / 1 | bc )			### Elegant fix
					frequency="${fval}MHz"
					;;
			esac
			return ;;
		98 )	frequency="${fmin}" ; return ;;
		99 )	frequency="${fmax}" ; return ;;
		+([0-9]) )
			test ${doFreq} -gt ${len} && { printf "\n\t ERROR:  User has specified frequncy index which is out of range.  Max positional choices = 4.\n Bye!\n\n" ; exit 1 ; }
			frequency="${fopt[$(expr ${doFreq} - 1)]}" ; return ;;
		#'([:alpha:])' )
		* ) printf "\n\t ERROR:  User has specified frequency index which is invalid.  Re-run with no options to obtain report of choices available.\n Bye!\n\n" ; exit 1 ;;
	esac
}


reportOptions()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING reportOptions ..." >&2

	printf "\n Choices Available for 'CPU Frequency Governor' labels:\n\n"
	echo "${governors}" | awk '{ printf("\t %s\n", $0 ); }'

	printf "\n Choices Available for 'userspace' fixed CPU 'frequency':\n\n"
	echo "${steps}" | awk '{ printf("\t %7s\n", $0 ); }'

	printf "\n Syntax:  $(basename $0 )\n\
		\t\t[\t--list    |\n\
		\t\t\t--detail  |\n\
		\t\t\t--default |\n\
		\t\t\t[ --mode {governor_label} |\n\
		\t\t\t  --max                   |\n\
		\t\t\t  --min                   |\n\
		\t\t\t  --laptop                |\n\
		\t\t\t  --load                  |\n\
		\t\t\t  --adaptive                ]\n\
		\t\t\t[ --freq {frequency} ]\n\
		\t\t]\n\n"


	printf "\n View detailed report generated by 'cpupower' ? [y/N] => " ; read ans ; test -n "$ans" || ans="N"
	case "${ans}" in
		y* | Y* )
			printf "\n Contents of raw report from 'cpupower' (${tmp}):\n\n"
			awk '{ printf("\t|%s\n", $0 ) ; }' <"${tmp}"
			echo ""
			ls -l "${tmp}"
			echo ""
			rm -i "${tmp}"
			;;
		* ) rm -f "${tmp}" ;;
	esac
}


setGovernor()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING setGovernor ..." >&2

	test -n "${governor}" || { printf "\n ERROR:  No 'governor' label specified.  Unable to proceed.\n\n" ; exit 1 ; }

	#testor=$(grep 'available cpufreq governors:' ${tmp} | grep "${governor}" )
	testor=$(echo "${governors}" | grep "${governor}" )

	test -n "${testor}" || { printf "\n ERROR:  Governor '${governor}' is available for your hardware. Unable to proceed.\n\n" ; exit 1 ; } 

	###	Set CPU frequency under USER-defined control
	COM="${command} --cpu 'all' frequency-set --governor '${governor}'"
	printf "\n COMMAND:  ${COM}\n"
	eval ${COM}
}


setFrequency()
{
	test ${dbg} -gt 0 && echo "	>>> ENTERING setFrequency ..." >&2

	test -n "${frequency}" || { printf "\n ERROR:  No CPU frequency value specified.  Unable to proceed.\n\n" ; exit 1 ; }

	testor=$(grep 'Pstate-' ${tmp} | grep "${frequency}" )

	test -n "${testor}" || { printf "\n ERROR:  Frequency '${frequency}' is not available for your hardware. Unable to proceed.\n\n" ; exit 1 ; } 

	###	Can ONLY set frequency by itself; no other parameters allowed
	COM="${command} --cpu 'all' frequency-set --freq '${frequency}'"
	printf "\n COMMAND:  ${COM}\n"
	eval ${COM}
}


reportStatus()
{
	now=$( date '+%Y-%m-%d %H:%M:%S' )
	for cpu in 0 1 2 3
	do
		fmax=$(cat /sys/devices/system/cpu/cpu${cpu}/cpufreq/scaling_max_freq )
		fmin=$(cat /sys/devices/system/cpu/cpu${cpu}/cpufreq/scaling_min_freq )

		govr=$(cat /sys/devices/system/cpu/cpu${cpu}/cpufreq/scaling_governor )
		freq=$(cat /sys/devices/system/cpu/cpu${cpu}/cpufreq/scaling_cur_freq )

		printf "${now}|  CPU ${cpu}:  %s  %4s MHz [%s <-> %s]\n"  ${govr}  $( expr ${freq} / 1000 )  $( expr ${fmin} / 1000 )  $( expr ${fmax} / 1000 )  
	done
}



command=$(which "cpupower" )
test -n "${command}" || { printf "\n ERROR: Unable to locate command 'cpupower'.  Unable to proceed.\n\n" ; exit 1 ; } 


tmp=/tmp/$(basename "$0" ".sh" ).report


### get details report to parse for available governor labels and CPU frequencies
${command} frequency-info --debug >${tmp}

test -s "${tmp}" || { printf "\n ERROR: ${command} did not generate the required details report.  Unable to proceed.\n\n" ; exit 1 ; }


identifyGovernors

identifyFrequencies


test ${dbg} -gt 0 && echo "	>>> START parsing ..." >&2

### always report safely and informatively, if no parameters provided
if [ $# -eq 0 ]
then
	set - '--list'
fi

while [ $# -gt 0 ]
do
	case "${1}" in
		"--list" )
			reportOptions ; echo "" ; exit 0 ; ;;
		"--detail" )
			${command} frequency-info --debug ; echo "" ; exit 0 ;;
		"--mode" )
			doMode=98 ;
			governor="${2}" ; shift ; shift ;;
		"--max" )
			doMode=1 ; shift ;;
		"--min" )
			doMode=2 ; shift ;;
		"--laptop" )
			doMode=3 ; shift ;;
		"--load" )
			doMode=4 ; shift ;;
		"--adaptive" )
			doMode=5 ; shift ;;
		"--freq" )
			doMode=99 ;
			doFreq=97 ;
			frequency="${2}" ; shift ; shift ;;
		"--fmax" )
			doFreq=99 ; shift ;;
		"--fmin" )
			doFreq=98 ; shift ;;
		"--f"+([0-9]) )
			doFreq=$(echo "$1" | cut -c4- ) ; shift ;;
		"--default" )
			doDefault=1 ; shift ;;
		"--status" )
			doStatus=1 ; shift ;;
		"--cron" )
			doCron=1 ; shift ;;
		"--service" )
			doService=1 ; shift ;;
		* )
			echo "ERROR:  Invalid argument '${1}' on command line." ; exit 1 ;;
	esac
done

test ${dbg} -gt 0 && echo "	>>> END  parsing ..." >&2

if [ ${doService} -eq 1 ]
then
	rm -f /var/log/cpufreq.log
fi

if [ ${doStatus} -eq 1 ]
then
	if [ ${doCron} -eq 1 ]
	then
		{ reportStatus ; echo "" ; } >>/var/log/cpufreq.log
	else
		reportStatus
	fi
	exit
fi

getGov

getFreq
test ${dbg} -gt 1 && echo frequency = ${frequency} >&2

test ${doDefault} -eq 1 \
	&& { printf "\n Will use hard-coded presets:\n\t  governor = '${governor}'\n\t frequency = '${frequency}'\n\n" ; } \
	|| { printf "\n Will use selected governor: '${governor}' ...\n" ; }

setGovernor

if [ "${governor}" = "userspace" ]
then
	test ${doDefault} -eq 0 \
	&& { printf "\n Will use selected frequency: '${frequency}' ...\n" ; }

	setFrequency
fi

echo ""
rm -i ${tmp}

couldn't you run your script at boot time?

you mention it is a one-off thing.

just a thought.

1 Like

I have been doing that, manually after login, but I want to set it up and forget about it.

Is there another way other than a systemd unit to do that?

If I don't need to do this thru systemd, that would be good!

I am just using this as an excuse to learn about systemd for some other possible applications. :slight_smile:

Look: A starts before B and B wants that A is running when B is being started. There is no conflict at all.

WantedBy states the target or targets that the service should be started under. Think of these targets as of a replacement of the older concept of runlevels.

1 Like

A (non-systemd) try is to enable the rc.local service and create /etc/rc.local with your script. I have mine to mount volumes since /etc/fstab refuses to mount ...

perms of /etc/rc.local are 755

cat /etc/rc.local

#!/bin/bash
sleep 5
logger " ===================== mounting volumes ..."
/usr/bin/mount -a
3 Likes

Thank you, Pavlos. That worked perfectly. But doing that seems to be like creating a hybrid of the init and the new systemd systems. It just feels wrong. :frowning: ... and maybe I have the wrong impression about it.

Regarding your not able to mount your files, for what it's worth, maybe you need to add a condition for systemd to ensure those are available before proceeding, something like I did here for my own:

system/lightdm.service.d/override.conf:RequiresMountsFor=/DB001_F2
system/lightdm.service.d/override.conf.lightdm.service.OasisMega1:RequiresMountsFor=/DB001_F2

Thank you, Eugene. I believe I had scanned that reference.

I was looking for a "sanity check" on what I had understood from that and others.

I was making use of

This last was very interesting, because it exposed me to the systemd variant of those boot graphs we had way back with Ubuntu 6.04 !

When I set it up, it seemed to report failure, but the script performed its task !!!

Also, I tried to "purge" the service, but seemed to hang around, and I couldn't get it to the point that systemd would not report about it, even after entering the command

systemctl daemon-reload

Just as an aside, here is the script I am using for exploring this systemd usage:

#!/bin/bash


###
###	Create a "service" definition file for use by systemd
###

#REF:	https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6
#REF:	https://linuxhandbook.com/create-systemd-services/


prod=0
doUser=1
doServer=0

serviceUser="root"
serviceName="localcpufreq"
serviceDesc="Local CPU Frequency Service and Monitor"
serviceCommandPath="/DB001_F2/Oasis/bin/HW_Admin__Power_SetFreqCPU.sh --default --service"


prefix="SYSTEMD__"
systemdServiceFile=~/"${prefix}${serviceName}.service"

while [ $# -gt 0 ]
do
	case "${1}" in
		"--debug" )
			prefix="DEBUG__"
			systemdServiceFile="/etc/systemd/system/${prefix}${serviceName}.service"
			shift
			;;
		"--prod" )
			prod=1
			systemdServiceFile="/etc/systemd/system/${serviceName}.service"
			shift
			;;
		"--user" )
			doUser=1
			doServer=0
			shift
			;;
		"--server" )
			doServer=1
			doUser=0
			shift
			;;
		* )
			printf "\n\t Invalid option specified on command line. Abandoning process.\n Bye!\n\n" ; exit 1
			;;
	esac
done

if [ ${doUser} -eq 1 ]
then
	unitDesc="Description=${serviceDesc}"
	unitAfter="After=network-online.target"
	#unitBefore="#Before="
	unitBefore="Before=multi-user.target"
	unitStrtLimInt="StartLimitIntervalSec=0"
	unitStrtLimBrst=""

	serviceUser="User=${serviceUser}"
	serviceType="Type=oneshot"
	serviceExit="#ExitType=main"
	serviceRemain="RemainAfterExit=no"
	serviceExec="ExecStart=/bin/bash ${serviceCommandPath}"
	serviceRedo="#Restart=on-failure"
	serviceRedoDel="#RestartSec=360"

	installReqBy="#RequiredBy="
	installWantBy="WantedBy=multi-user.target"
fi

if [ ${doServer} -eq 1 ]
then
	unitDesc="Description=${serviceDesc}"
	unitAfter="After=network-online.target"
	#unitBefore="#Before="
	unitBefore="Before=multi-user.target"
	unitStrtLimInt="StartLimitIntervalSec=0"
	#unitStrtLimInt="#StartLimitIntervalSec=10"
	unitStrtLimBrst="#StartLimitBurst=5"

	serviceUser="User=${serviceUser}"
	serviceType="Type=oneshot"
	#serviceType="#Type=notify"
	#serviceType="#Type=simple"
	serviceExit="#ExitType=main"
	serviceRemain="RemainAfterExit=no"

	#ExecStartPre=
	serviceExec="ExecStart=/bin/bash ${serviceCommandPath}"
	#ExecStartPost=
	#ExecReload=/bin/bash ${serviceCommandPath}
	serviceRedo="#Restart=on-failure"
	serviceRedoDel="#RestartSec=360"
	#RestartLimitAction=reboot

	installReqBy="#RequiredBy="
	installWantBy="WantedBy=multi-user.target"
fi


createServiceFile()
{
	cat <<-EnDoFiNpUt
[Unit]
${unitDesc}
${unitAfter}
${unitBefore}
${unitStrtLimInt}
${unitStrtLimBrst}

[Service]
${serviceUser}
${serviceType}
${serviceExit}
${serviceRemain}
${serviceExec}
${serviceRedo}
${serviceRedoDel}

[Install]
${installReqBy}
${installWantBy}
EnDoFiNpUt

chmod 664 "${systemdServiceFile}"
}


createServiceFile | grep -v '^#' >"${systemdServiceFile}"


if [ ${prod} -eq 1 ]
then
	# make systemd aware of the new service definition file
	systemctl daemon-reload

	# set to start on boot:
	#systemctl enable ${serviceName}

	# run once at request
	systemctl start ${serviceName}

	# report status of service
	systemctl status ${serviceName}
else
	cat "${systemdServiceFile}"
	echo ""
	ls -l "${systemdServiceFile}"
fi

before systemd, before Linux, there was UNIX.

that was the method we used to run scripts after boot.

(yes, I'm old)

1 Like

You are not the only one Pavlos. 8 years after HS I went back to college. Though not a programming student I remember them struggling with Fotran and Cobol, and the little programming I had to learn was in qbasic. I didn't run into Unix until 1997, when it was used to control our operating system in a power plant.

2 Likes

I started with UNIX around 1985.

3 Likes

Same here. My intro was with HP-UX Admin on HP9000 series 540 with 3 of the large disk drives supporting eight 3rd-party CAD/CAM terminals. :slight_smile:

I was always impressed by the ease of adaptability of that mini-computer! Too bad the "big boys" had more money on marketing.

2 Likes

In 1985 I was still programming in pascal on a Prime 750 (Prime OS), Unix happened to me four years later.

3 Likes

Just a final update about this journey to control the CPU Frequency governor.

I have settled on the use of rc.local.

Please note that since I removed the Panel Applet for the governor 2 months ago, and using the setup described below, there have been no mysterious "interventions" changing my governor from "userspace" to "ondemand".

So ... that means that something associated with the Applet is triggering those "downgrades". I don't know what and, at this point, with nothing else having changed, it doesn't matter to me anymore.

But ... those that do use the Panel Applet for their governor use may wish to be aware of that "unexplained" behaviour. :slight_smile:

Thanks again to everyone for your help.

Here is a summary of my notes regarding the setup, in case anyone else wants to replicate. My final script can be obtained here.

####################################################################################################
[1]	Source:		cpufrequtils		[distro package]

	This files ensures the proper module is loaded into the kernel for CPU Frequency governor.

	/etc/init.d/loadcpufreq

####################################################################################################
[2]	Source:		cpufrequtils		[distro package]

	This file initializes the CPU Frequeny governor profile and settings.

	/etc/init.d/cpufrequtils

####################################################################################################
[3]	Source:		local			[ custom script ]

	This script "manages" the CPU Frequency governor according to local preferences.

	${Oasis}/bin/HW_Admin__Power_SetFreqCPU.sh

####################################################################################################
[4]	Source:		local			[ custom script ]

	This file ensures that the script 'HW_Admin__Power_SetFreqCPU.sh' is started at boot time with user specifications.

	/etc/rc.local

	||	/DB001_F2/Oasis/bin/HW_Admin__Power_SetFreqCPU.sh --default --service

####################################################################################################
[5]	Source:		local			[ custom config ]

	This crontab entry "monitors" the CPU Frequency governor, thereby making possible to confirm stability.

	||	0,10,20,30,40,50 * *	*	*	/DB001_F2/Oasis/bin/HW_Admin__Power_SetFreqCPU.sh --status --cron
2 Likes