Mestring av Linux-kommandolinjen og Bash-skripting · leksjon

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.

Leksjon 1 av 413 trinn

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 time og bash -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.012s

Spor 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:

  • PS4 evalueres 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 for echo "$var" | wc -c – strenglengde
  • ${var^^} og ${var,,} i stedet for echo "$var" | tr 'a-z' 'A-Z' – konvertering av store og små bokstaver (bash 4+)
  • ${var//search/replace} i stedet for echo "$var" | sed 's/search/replace/' – enkel erstatning
  • [[ "$var" =~ pattern ]] i stedet for echo "$var" | grep -q pattern – regulært uttrykk
  • read -r line < file i stedet for line=$(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 printf i stedet for å kalle date nå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 5

Må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ør
  • read -r field1 field2 <<< "$line" – del en variabel uten et eksternt verktøy
  • wc -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 – grep leser filen direkte
  • Erstattet tr '[:lower:]' '[:upper:]' med ${var^^}
  • Erstattet echo "$line" | grep -q med [[ $line =~ ]]
  • Brukte read -r med 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 time og bash -x beriket med PS4 til å 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" | command med en here-string (command <<< "$var") eller en builtin
  • Parameterutvidelser (${var^^}, ${var//s/r}, ${#var}) erstatter mange kall til tr, sed og wc
  • Underskall i pipelines sluker variabelendringer — bruk prosessubstitusjon eller shopt -s lastpipe
  • Flytt kall til uforanderlige kommandoer utenfor løkker; foretrekk aritmetikk med $(( )) fremfor expr

Tommelfingerregelen er: mål først, erstatt eksterne kommandoer med builtins der det er mulig, og bekreft forbedringen med en ny måling.

Gratis å komme i gang

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

  1. Profilering av skript og unngåelse av unødvendige subshell-er
  2. Parallellitet med xargs -P og bakgrunnsjobber
  3. Orkestrering av arbeidsmengder med GNU parallel
  4. Strømmende pipelines og navngitte pipes for høy gjennomstrømning
← Tilbake til Mestring av Linux-kommandolinjen og Bash-skripting