Profilering av skript og unngåelse av unødvendige subshell-er
Mål skripttid, og erstatt prosesskrevende mønstre som cat-grep-kjeder med innebygde alternativer.
Profilering av skript og unngåelse av unødvendige subshell-er er en gratis leksjon i Mestring av Linux-kommandolinjen og Bash-skripting på CoddyKit. Dette er leksjon 1 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Mestring av Linux-kommandolinjen og Bash-skripting, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Mestring av Linux-kommandolinjen og Bash-skripting inneholder totalt 4 leksjoner.
Hvorfor skriptytelse er viktig
Bash-skript som kjører sakte, sløser med CI-tid, blokkerer cron-jobber og frustrerer brukerne. Det meste av tregheten skyldes ikke kompleks logikk, men unødvendige prosessforgreininger: hver eksterne kommando De kaller, starter en ny underprosess.
I denne leksjonen lærer De å:
- Måle hvor tiden faktisk brukes, med
timeogbash -x - Identifisere mønstre med mange forgreininger, for eksempel unødvendig bruk av cat
- Erstatte eksterne kommandoer med raskere innebygde skallkommandoer
- Bruke subshells bevisst og unngå dem når de ikke tilfører verdi
Målet er å skrive skript som gjør det samme arbeidet med færre underprosesser og kortere klokketid.
Ta tiden på et skript med den innebygde kommandoen time
Det enkleste profileringsverktøyet er den innebygde skallkommandoen time. Sett den foran en kommando eller pipeline for å få tre målinger:
- real – medgått klokketid (det De faktisk venter på)
- user – CPU-tid brukt på kode i brukerområdet
- sys – CPU-tid brukt i kjernen (systemkall, I/O)
Et stort gap mellom real og user+sys betyr vanligvis at skriptet venter på I/O eller starter mange underprosesser. Kjør time rundt hele skriptet først for å bekrefte at det finnes et problem, før De optimaliserer noe.
#!/usr/bin/env bash
# Time a whole script block
time {
for i in $(seq 1 1000); do
echo "line $i"
done | grep -c "5"
}
# Output example:
# 271
# real 0m0.045s
# user 0m0.038s
# sys 0m0.012sSpor kjøring med bash -x og PS4
bash -x skriver ut hver kommando før den kjøres – dette er kjøringssporing. Det viser hvilke linjer som kjøres oftest, og om eksterne programmer kalles oftere enn forventet.
Som standard får hver sporede linje prefikset +. De kan gjøre prefikset mer informativt ved å bruke PS4 til å ta med tidsstempler. Da blir sporingen en lettvektsprofiler:
PS4evalueres før hver sporede kommando- Hvis De inkluderer
$EPOCHREALTIME(bash 5+) eller$(date +%s%N), får De nanosekundoppløsning - Omdiriger standardfeil til en fil og etterbehandle den for å finne langsomme deler
#!/usr/bin/env bash
# Run with: bash -x ./myscript.sh 2>trace.log
# Or embed tracing inside the script:
export PS4='+ [${EPOCHREALTIME}] ${BASH_SOURCE}:${LINENO}: '
set -x
slow_function() {
local result
result=$(cat /etc/hostname) # fork — slow
echo "host: $result"
}
slow_function
set +x
# trace.log now contains timestamps so you can diff
# adjacent lines to find which step took longest.Hva er en unødvendig subshell?
En subshell er en underordnet kopi av den gjeldende skallprosessen. Den opprettes av:
- Kommandosubstitusjon:
$(command) - Gruppering med parenteser:
( commands ) - Rørkobling til en skallkonstruksjon:
cmd | while read ...
Subshells er nødvendige når De faktisk trenger isolasjon eller en pipeline. De blir unødvendige når De bare bruker dem til å kalle et eksternt program som skallet selv kunne ha håndtert, eller når De pakker inn en innebygd kommando i et ekstra forgreiningslag uten grunn.
Hver subshell-forgreining koster omtrent 1–5 ms på et moderne Linux-system. I en løkke som kjører 10 000 ganger, legger 1000 unødvendige subshells til 1–5 sekunder med ren ekstrakostnad.
Det klassiske ant mønsteret: unødvendig bruk av cat
cat file | grep pattern er det mest kjente mønsteret med unødvendig mange forgreininger. Det starter to prosesser (cat + grep) som er koblet sammen med et rør, selv om grep alene kan lese filen direkte.
Løsningen er enkel: send filnavnet direkte til kommandoen som forstår filer. Når verktøyet ikke godtar filnavn, kalles dette inndataviderekobling; når det gjør det, utelater De ganske enkelt cat.
- Sakte:
cat file | grep pattern– 2 prosesser, 1 rør - Raskt:
grep pattern file– 1 prosess, ingen rør - Også raskt:
grep pattern < file– 1 prosess, stdin-viderekobling (ingen rørbuffer)
#!/usr/bin/env bash
# Create a sample file
seq 1 10000 > /tmp/numbers.txt
# --- Slow: useless cat ---
time cat /tmp/numbers.txt | grep -c "^5"
# --- Fast: grep reads the file directly ---
time grep -c "^5" /tmp/numbers.txt
# Both print the same count; the second is measurably faster
# because it skips the cat process and the inter-process pipe.Erstatte eksterne kommandoer med innebygde skallkommandoer
Mange transformasjoner på én linje har en innebygd ekvivalent som unngår en forgreining fullstendig. Sammenlign disse vanlige erstatningene:
echo ${#var}i stedet forecho "$var" | wc -c– strenglengde${var^^}og${var,,}i stedet forecho "$var" | tr 'a-z' 'A-Z'– konvertering av store og små bokstaver (bash 4+)${var//search/replace}i stedet forecho "$var" | sed 's/search/replace/'– enkel erstatning[[ "$var" =~ pattern ]]i stedet forecho "$var" | grep -q pattern– regulært uttrykkread -r line < filei stedet forline=$(head -n1 file)– lese første linje
Ingen av disse innebygde kommandoene starter en underprosess. Besparelsen er liten per kall, men blir betydelig i løkker.
#!/usr/bin/env bash
sentence="hello world from bash"
# --- Fork-heavy ---
upper_slow=$(echo "$sentence" | tr 'a-z' 'A-Z')
length_slow=$(echo "$sentence" | wc -c)
# --- Builtin equivalents (zero extra processes) ---
upper_fast=${sentence^^}
length_fast=${#sentence}
echo "Slow upper : $upper_slow"
echo "Fast upper : $upper_fast"
echo "Slow length: $length_slow"
echo "Fast length: $length_fast"Unngå subshells i løkker
Kommandosubstitusjon i en løkke multipliserer kostnaden ved forgreining med antallet iterasjoner. En løkke som kjører 500 ganger og inneholder ett $(date)-kall, starter 500 underprosesser bare for å hente tidsstempler.
Strategier for å redusere kostnaden i løkker:
- Flytt konstante kommandoer utenfor løkken (beregn én gang og bruk resultatet på nytt)
- Foretrekk aritmetisk ekspansjon
$(( expr ))– dette er innebygd og krever ingen forgreining - Bruk
printfi stedet for å kalledatenår De bare trenger formatering - Samle eksterne kall: hent inn data først og behandle dem én gang utenfor løkken
#!/usr/bin/env bash
# Demonstrate: compute-once vs fork-per-iteration
# Bad: $(date) forks 1000 times
time (
for i in $(seq 1 1000); do
ts=$(date +%s) # fork each iteration
echo "$i $ts" > /dev/null
done
)
# Good: capture once, reuse
time (
ts=$(date +%s) # fork exactly once
for i in $(seq 1 1000); do
echo "$i $ts" > /dev/null
done
)Subshells i rør og fallgruven med variabelomfang
I bash (i motsetning til ksh/zsh) kjører hver kommando i en pipeline i sin egen subshell. Det betyr at variabler som settes inne i et rør, går tapt når røret er ferdig.
Dette er både en feil i korrektheten og et ytelsesproblem – De kan for eksempel sende data til while read i forventning om å samle dem inn, og først etterpå oppdage at variabelen er tom.
To løsninger:
- Bruk prosessubstitusjon
while read line; do ...; done < <(command)– while-løkken kjører i det gjeldende skallet, ikke i en subshell - Bruk alternativet lastpipe (
shopt -s lastpipe) – lar det siste pipeline-segmentet kjøre i det gjeldende skallet (bash 4.2+)
#!/usr/bin/env bash
count=0
# --- Bug: count is always 0 after pipe (subshell) ---
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "After pipe : count=$count" # prints 0
# --- Fix 1: process substitution (no subshell for while) ---
count=0
while read -r n; do
(( count++ ))
done < <(seq 1 5)
echo "Process sub : count=$count" # prints 5
# --- Fix 2: lastpipe option ---
shopt -s lastpipe
count=0
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "lastpipe : count=$count" # prints 5Måle kostnaden ved subshells med en mikromåling
Det er enkelt å påvise ekstrakostnaden ved subshells med en liten måling. Sammenlign en aritmetisk operasjon utført med $(( )) (innebygd) med den samme operasjonen sendt gjennom expr (ekstern prosess).
Resultater på en typisk Linux-maskin viser at 10 000 kall til expr tar omtrent 5 sekunder, mens like mange kall til $(( )) tar under 0,1 sekunder – en 50-dobbel forskjell for identisk resultat.
Dette målemønsteret er også nyttig når De vil måle en optimalisering: kjør begge versjonene N ganger i en løkke og sammenlign med time.
#!/usr/bin/env bash
N=500
# External command (fork per call)
time (
x=0
for ((i=0; i<N; i++)); do
x=$(expr $x + 1) # forks expr each time
done
echo "expr result: $x"
)
# Arithmetic builtin (no fork)
time (
x=0
for ((i=0; i<N; i++)); do
(( x++ )) # pure builtin
done
echo "builtin result: $x"
)Bruke here-strenger for å unngå echo-rør
Et vanlig mønster er echo "$var" | command for å sende en variabel inn via stdin. Dette starter to prosesser (echo + command) og oppretter et rør. En here-string (<<<) gir samme resultat med bare én prosess – den eksterne kommandoen leser fra en midlertidig buffer som kjernen administrerer.
grep pattern <<< "$var"– én prosess, ingen rørread -r field1 field2 <<< "$line"– del en variabel uten et eksternt verktøywc -w <<< "$sentence"– tell ord fra en variabel
Here-strenger er spesielt nyttige i tette løkker der hver forgreining har betydning.
#!/usr/bin/env bash
data="The quick brown fox"
# --- Fork-heavy: echo spawns a child ---
word_count_slow=$(echo "$data" | wc -w)
echo "Slow word count: $word_count_slow"
# --- Fast: here-string, only wc spawns ---
word_count_fast=$(wc -w <<< "$data")
echo "Fast word count: $word_count_fast"
# --- Even better: use parameter expansion (zero forks) ---
# Split into array, count elements
read -ra words <<< "$data"
echo "Zero-fork count: ${#words[@]}"Praktisk refaktorering: før og etter
La oss gå gjennom et realistisk skript som behandler en loggfil, og bruke alt vi har lært. Den opprinnelige versjonen kjeder sammen cat, grep, awk og tr med rør. Den refaktorerte versjonen reduserer antallet prosesser fra 8 til 2.
Viktige endringer:
- Fjernet
cat–grepleser filen direkte - Erstattet
tr '[:lower:]' '[:upper:]'med${var^^} - Erstattet
echo "$line" | grep -qmed[[ $line =~ ]] - Brukte
read -rmed prosessubstitusjon i stedet for en while-løkke med rør
Kjør time ./script.sh på nytt etter refaktoreringen for å bekrefte forbedringen. Mål alltid – ikke anta.
#!/usr/bin/env bash
# Create a sample log
printf 'ERROR: disk full\nINFO: started\nERROR: timeout\nINFO: done\n' \
> /tmp/sample.log
# === BEFORE (fork-heavy) ===
time (
cat /tmp/sample.log \
| grep 'ERROR' \
| while read -r line; do
label=$(echo "$line" | tr '[:lower:]' '[:upper:]')
echo "[ALERT] $label"
done
)
# === AFTER (builtin-first) ===
time (
while IFS= read -r line; do
echo "[ALERT] ${line^^}"
done < <(grep 'ERROR' /tmp/sample.log)
)Kunnskapssjekk: Omfanget til underskall
Test forståelsen Deres av underskall i pipelines og hvordan De kan unngå å miste variabelendringer som gjøres inne i en pipe.
Oppsummering av leksjonen: Mål først, opprett færre prosesser
I denne leksjonen lærte De å identifisere og eliminere de vanligste kildene til unødvendig prosessoppretting i bash-skript.
Viktigste punkter:
- Bruk
timeogbash -xberiket medPS4til å måle før De optimaliserer - Unødvendig cat er det mest utbredte antimønsteret — send filnavn direkte til kommandoer som støtter dem
- Bytt ut
echo "$var" | commandmed en here-string (command <<< "$var") eller en builtin - Parameterutvidelser (
${var^^},${var//s/r},${#var}) erstatter mange kall tiltr,sedogwc - Underskall i pipelines sluker variabelendringer — bruk prosessubstitusjon eller
shopt -s lastpipe - Flytt kall til uforanderlige kommandoer utenfor løkker; foretrekk aritmetikk med
$(( ))fremforexpr
Tommelfingerregelen er: mål først, erstatt eksterne kommandoer med builtins der det er mulig, og bekreft forbedringen med en ny måling.
Lær deg Bash med en AI-veileder – gratis
Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.
- Kurs
- 22
- Leksjoner
- 88
Ofte stilte spørsmål
Er leksjonen «Profilering av skript og unngåelse av unødvendige subshell-er» gratis?
Ja – hele teksten i «Profilering av skript og unngåelse av unødvendige subshell-er» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Mestring av Linux-kommandolinjen og Bash-skripting-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Mestring av Linux-kommandolinjen og Bash-skripting inneholder totalt 4 leksjoner.
Hva lærer jeg i «Profilering av skript og unngåelse av unødvendige subshell-er»?
Mål skripttid, og erstatt prosesskrevende mønstre som cat-grep-kjeder med innebygde alternativer. Du øver på Mestring av Linux-kommandolinjen og Bash-skripting med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.
Trenger jeg erfaring for å begynne med Mestring av Linux-kommandolinjen og Bash-skripting?
Ingen tidligere erfaring er nødvendig. Mestring av Linux-kommandolinjen og Bash-skripting på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 1 av 4.
Hvor lang tid tar leksjonen «Profilering av skript og unngåelse av unødvendige subshell-er»?
De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.
Kan jeg skrive og kjøre kode i denne Mestring av Linux-kommandolinjen og Bash-skripting-leksjonen?
Ja. Alle Mestring av Linux-kommandolinjen og Bash-skripting-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.
Alle leksjonene i dette kurset
- Profilering av skript og unngåelse av unødvendige subshell-er
- Parallellitet med xargs -P og bakgrunnsjobber
- Orkestrering av arbeidsmengder med GNU parallel
- Strømmende pipelines og navngitte pipes for høy gjennomstrømning