Linux & Shell

Linux and Shell Interview Questions With Model Answers

Permissions, processes, pipes, scripting and troubleshooting questions with model answers.

Lesson 0814 min read#linux#interview

On this page

  1. Words used in this lesson
  2. How to answer: say the idea, then the proof
  3. Proof 1: permissions
  4. Proof 2: processes, signals and exit codes
  5. Proof 3: redirection, tee and xargs
  6. Proof 4: grep, sed, awk and find recipes
  7. Proof 5: scripting traps
  8. Proof 6: SSH, systemd and cron
  9. Proof 7: disk, memory and load
  10. Proof 8: the "server is slow" walk-through
  11. Try it
  12. Interview questions
  13. Key takeaways
  14. What's next

In one line: Linux interviews come back to the same dozen ideas: who is allowed to do what (permissions), what is running (processes and signals), how data flows (pipes and redirection), how to reshape text (grep, sed, awk, find), how scripts fail (quoting and set -e), how to get onto a server (SSH), how services start (systemd and cron), and why a server is slow or full. This lesson gives 34 questions with model answers, a quick-fire table and a mock round, and every answer that can be proven with a small command was proven in a lab.

Interviewers rarely ask "list all flags of ls". They ask small questions that show whether you have really used a server: "Why does df say 100% but du finds half?" "What does 2>&1 do and does the order matter?" "A script works in my terminal but not in cron. Why?" If you have run the commands, the answers come easily. If you have only read about them, the answers sound thin.

This is the last lesson of nine. Each question points back to the lesson that teaches it: lesson 0 (shell and file system), lesson 1 (permissions), lesson 2 (processes), lesson 3 (pipes and text tools), lesson 4 (scripting), lesson 5 (SSH), lesson 6 (systemd and cron) and lesson 7 (troubleshooting). If an answer feels new, go back to that lesson first.

What I ran / checked. Everything ran in a scratch folder, /tmp/lab-linux-c, on Ubuntu 24.04.5 (kernel 6.18), bash 5.2.21, GNU sed 4.9, GNU grep 3.11, GNU find and xargs 4.9.0, mawk 1.3.4 (the default awk on Ubuntu), OpenSSH 9.6p1, shellcheck 0.9.0, Python 3.13 and curl. I was root in a sandbox VM, and root skips most permission checks, so for the permission demos I used setpriv --reuid=65534 to run commands as the unprivileged user nobody. I never touched real system files: the only real files I read were /etc/passwd, and I only ran stat on /usr/bin/passwd, /usr/bin/sudo, /tmp and /bin/ls. The access log is a made-up 8-line file I wrote by hand, so the numbers show the technique, not a real site. The "full disk" demos used a tiny RAM-backed tmpfs mount (4 MB, 30 inodes). Not run: systemctl and journalctl (this VM's PID 1 is not systemd, so I only ran systemd-analyze verify on a unit file), crontab scheduling, a real OOM kill, a stuck D-state process, container signal handling, and GNU-versus-BSD differences on macOS. Each of those is labelled "not run" where it appears. Real port-forward and authorized_keys runs are in lesson 5; I did not repeat them here. Sources: my own earlier lessons in this series, and the messages printed by the tools themselves (shellcheck, ssh-keygen, systemd-analyze). I read no web source for this lesson, and I could not open the man pages on this VM, so facts that I did not run (signals on PID 1, nohup, D state, the sticky noexec mount, macOS sed) come from memory and my earlier lessons, and are labelled. Timings and PIDs vary between runs.

Words used in this lesson

  • Must know means almost every interviewer asks it. Senior signal means it separates people with real experience. Trap means the obvious answer is wrong or incomplete.
  • Octal mode is a permission written as digits like 755 (r=4, w=2, x=1 per group).
  • Exit status is the number a command returns. 0 is success.
  • Signal is a small message sent to a process, such as SIGTERM (15) or SIGKILL (9).
  • Zombie is a finished process whose parent has not collected its exit code yet.
  • File descriptor is a number for an open stream: 0 is stdin, 1 is stdout, 2 is stderr.
  • Word splitting is the shell cutting an unquoted value into pieces at spaces.
  • Inode is the record that holds a file's details. A file system has a fixed number of them.
  • Load average is the average count of tasks running or waiting (including waiting on disk), over 1, 5 and 15 minutes.
  • Available memory is the part of RAM that programs can get without swapping, counting cache that can be given back.
  • Idempotent means doing it twice gives the same result as doing it once.

How to answer: say the idea, then the proof

A good short answer has three parts: the rule in one sentence, a tiny example or the command you would run, and the trap you have seen. For example, for 2>&1:

  1. Rule: "Redirections are applied left to right."
  2. Example: cmd > out.txt 2>&1 sends both to the file; cmd 2>&1 > out.txt does not.
  3. Trap: "I once lost an error message in a cron job because of the wrong order."

Interviewers like honesty. If you do not know, say what you would check: "I would run man, or test it on a throwaway folder." Never say you ran something you did not. In this lesson I mark what I ran and what I did not, and you should do the same in your own words.

Proof 1: permissions

Permissions are the most common topic. Here are the five facts interviewers test, each with real output.

Reading the mode. stat shows the number and the letters together:

Shell
$ stat -c '%a %A %n' /usr/bin/passwd /tmp /usr/bin/sudo /bin/ls4755 -rwsr-xr-x /usr/bin/passwd1777 drwxrwxrwt /tmp4755 -rwsr-xr-x /usr/bin/sudo755 -rwxr-xr-x /bin/ls

The extra leading digit is the special bits: 4 is setuid (the s in the owner part), 1 is the sticky bit (the t at the end). passwd and sudo are setuid-root so ordinary users can do a small privileged job. /tmp is writable by all with the sticky bit.

umask removes bits from new files. Files start from 666, directories from 777:

Shell
$ umask0022$ umask 027; touch f1; mkdir d1; ls -ld f1 d1drwxr-x--- 2 root root 4096 Oct 17 12:51 d1-rw-r----- 1 root root    0 Oct 17 12:51 f1$ (umask 077; touch u1; mkdir u2; stat -c '%a %n' u1 u2)600 u1700 u2

666 - 027 = 640 for the file and 777 - 027 = 750 for the directory. (The subtraction is a good way to remember it; the real rule is "remove those bits", which gives the same answer for these cases.) The umask 077 ran in a subshell with ( ) so it did not change my main shell.

A directory needs x to enter it. I made directory pd containing a file a, then ran as nobody:

Shell
$ chmod 644 pd        # r but no x$ ... ls pd; cat pd/aacat: pd/a: Permission denied$ chmod 711 pd        # x but no r$ ... ls pd; cat pd/als: cannot open directory 'pd': Permission deniedx

With 644 (read, no execute) you can list names but not open anything inside. With 711 (execute, no read) you cannot list, but you can open a file if you already know its name. This is a favourite question because people think "read" is enough for a directory.

Sticky bit. In a directory with mode 1777, nobody could not remove a file owned by root:

Shell
$ chmod 1777 stk; touch stk/rootfile$ ... rm -f stk/rootfilerm: cannot remove 'stk/rootfile': Operation not permitted$ chmod 777 stk       # same directory, sticky bit removed$ ... rm -f stk/rootfileexit=0

Without the sticky bit, anyone with write permission on the directory can delete any name in it, even files they do not own. Deleting is a change to the directory, not to the file.

Running a script needs x, reading it with bash does not.

Shell
$ chmod 644 h.sh$ ... ./h.sh; echo "exit=$?"       # as nobodybash: line 1: ./h.sh: Permission deniedexit=126$ ... bash h.sh                      # as nobodyhi

Exit code 126 means "found, but cannot be run". Exit code 127 means "command not found". I will use this in a question below.

Proof 2: processes, signals and exit codes

Shell
$ true; echo $?0$ ls /nonexistent 2>/dev/null; echo $?2$ nosuchcmd 2>/dev/null; echo $?127$ bash -c 'exit 300'; echo $?44

300 became 44 because exit codes are one byte: 300 mod 256 = 44. Next, how a process that is killed reports it:

Shell
$ sleep 100 & p=$!; kill -TERM $p; wait $p; echo "TERM -> $?"TERM -> 143$ sleep 100 & p=$!; kill -9 $p; wait $p; echo "KILL -> $?"KILL -> 137

The rule is 128 + signal number: SIGTERM is 15, so 143; SIGKILL is 9, so 137. When a container "exited with code 137", someone or something sent SIGKILL (often the out-of-memory killer). A script can catch SIGTERM but nobody can catch SIGKILL:

Shell
$ cat trap.sh#!/bin/bashtrap 'echo "got TERM, cleaning"; exit 0' TERMsleep 30 & wait$ ./trap.sh & tp=$!; sleep 0.5; kill -TERM $tp; wait $tp; echo "exit=$?"got TERM, cleaningexit=0

A zombie is easy to make. A parent that does not call wait leaves its finished child in the table:

Shell
$ bash -c 'sleep 0.1 & exec sleep 3' & zp=$!$ ps -o pid,ppid,stat,comm --ppid $zp  PID  PPID STAT COMMAND 2530  2528 Z    sleep

Z is the zombie state. The child (sleep 0.1) is finished, but its parent became a plain sleep 3 that never reads the child's exit code. A zombie uses no CPU and no memory, only a slot in the process table. You cannot kill it (it is already dead); you make its parent collect it or you stop the parent.

Pipelines hide failures unless you ask:

Shell
$ false | true; echo "no pipefail: $?"no pipefail: 0$ set -o pipefail; false | true; echo "pipefail: $?"pipefail: 1$ false | true | false; echo "${PIPESTATUS[@]}"1 0 1

Proof 3: redirection, tee and xargs

The order of 2>&1 matters, because the shell reads redirections left to right. 2>&1 means "make stderr point where stdout points right now".

Shell
$ ls /nonexistent > out.txt 2>&1$ cat out.txtls: cannot access '/nonexistent': No such file or directory$ ls /nonexistent 2>&1 > out2.txtls: cannot access '/nonexistent': No such file or directory$ cat out2.txt$                                   (empty)

In the first line, stdout goes to the file, then stderr copies stdout, so both are in the file. In the second, stderr copies stdout while stdout still points at the terminal, and only afterwards is stdout moved to the file. The error stays on the screen and the file is empty. The short form &> file (bash) does both in the right order; I checked that it caught both lines.

tee copies its input to a file and to its own stdout, so you can see it and keep it: echo one | tee t1.txt | tr a-z A-Z printed ONE and left one in t1.txt. This is also why echo x | sudo tee /etc/file works while sudo echo x > /etc/file does not: the > is done by your shell, before sudo starts. I ran the tee form as root, so it proves only that tee writes; the permission failure of the > form is not run here because root can write everywhere.

xargs turns lines into arguments, but it splits on spaces by default:

Shell
$ touch "a b.txt" c.txt$ ls *.txt | xargs rmrm: cannot remove 'a': No such file or directoryrm: cannot remove 'b.txt': No such file or directory$ find . -maxdepth 1 -name '*.txt' -print0 | xargs -0 ls -1./a b.txt$ printf '1\n2\n3\n4\n5\n' | xargs -n2 echo1 23 45$ echo -n | xargs echo runrun$ echo -n | xargs -r echo run$                                   (nothing printed)

c.txt was removed, but the file with a space was cut into a and b.txt. -print0 with -0 uses a NUL byte as the separator so names with spaces are safe. And with empty input, GNU xargs still runs the command once unless you add -r.

Proof 4: grep, sed, awk and find recipes

I wrote this tiny log (fields: IP, date, request, status, bytes, seconds):

TEXT
10.0.0.1 - - [17/Oct/2026:10:00:01 +0000] "GET /api/users HTTP/1.1" 200 512 0.12010.0.0.3 - - [17/Oct/2026:10:00:04 +0000] "GET /api/orders HTTP/1.1" 500 128 2.400... (8 lines in total)
Shell
$ awk '{print $9}' access.log | sort | uniq -c | sort -rn      4 200      2 500      1 404      1 401$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -2      4 10.0.0.1      2 10.0.0.2$ awk '$11 > 1 {print $7, $11}' access.log/api/orders 2.400/api/orders 2.100$ awk '{t[$7]+=$11; n[$7]++} END {for (p in t) printf "%s %.3f\n", p, t[p]/n[p]}' access.log | sort/api/login 0.030/api/orders 2.250/api/users 0.108/health 0.001/missing 0.002$ grep -E '" (4|5)[0-9]{2} ' access.log | wc -l4$ grep -o '/api/[a-z]*' access.log | sort -u/api/login/api/orders/api/users

Note: uniq -c counts only neighbouring equal lines, which is why sort goes before it. I checked: printf 'b\na\nb\n' | uniq -c gave three lines of count 1, but with sort first it gave 1 a and 2 b. The awk fields are numbered from $1 on whitespace. In this log $9 is the status and $11 the time because the date and the request contain spaces. That is why you always head -1 a log and count the fields before you trust a number.

sed for edits:

Shell
$ echo "foo foo" | sed 's/foo/bar/'bar foo$ echo "foo foo" | sed 's/foo/bar/g'bar bar$ sed -i.bak 's/^port=.*/port=8080/' conf.ini; cat conf.ini; ls conf.ini*port=8080host=aconf.iniconf.ini.bak$ echo "/usr/local/bin" | sed 's|/usr|/opt|'/opt/local/bin

Without g, only the first match on each line changes. -i.bak edits in place and keeps a backup. When the text contains /, use another delimiter such as |.

find:

Shell
$ find tree -name '*.log'tree/b/y.logtree/a/x.log$ find tree -type f -exec echo {} \; | wc -l     # one process per file4$ find tree -type f -exec echo {} + | wc -l      # files batched into one call1$ find tree -name '*.log' -delete; find tree -type ftree/b/z.txt

-exec cmd {} \; starts the command once per file. -exec cmd {} + packs many files into one call, which is much faster for big trees.

Proof 5: scripting traps

Unquoted variables split.

Shell
$ f="my file.txt"; touch "$f"$ ls -l $fls: cannot access 'my': No such file or directoryls: cannot access 'file.txt': No such file or directory$ pat="*.dat"; echo $pat; echo "$pat"g1.dat g2.dat*.dat$ dir=""; echo rm -rf "$dir/"rm -rf /

The first shows word splitting. The second shows that an unquoted value is also expanded as a glob. The third (I only echoed the command, never ran it) shows why an empty variable inside rm -rf "$dir/" becomes the root directory. Safe forms are "${dir:?}" or checking first. ${NAME:?must set} makes the script stop with an error when the variable is empty:

Shell
$ bash -c 'echo "${NAME:?must set}"'; echo "exit=$?"bash: line 1: NAME: must setexit=127

[ versus [[. With an empty, unquoted variable:

Shell
$ v=""; [ $v = "x" ]; echo "exit=$?"bash: [: =: unary operator expectedexit=2$ [[ $v = "x" ]]; echo "exit=$?"exit=1$ [[ "abc" == a* ]] && echo glob-matchglob-match

[ is an ordinary command, so $v disappears and it sees [ = x ], a broken test. [[ ]] is shell syntax; it does not split words, it supports pattern matching with == and =~ for regular expressions, and it allows && and || inside.

set -e surprises. Each line below was a separate bash -c 'set -e; ...':

TEXT
false && echo x; echo "after && list still runs"   -> prints "after && list still runs"f(){ false; echo "inside func continues?"; }; if f; then echo ok; fi                                                   -> prints "inside func continues?" then "ok"x=$(false); echo reached                           -> exits 1, "reached" is NOT printedlocal y=$(false) inside a function                 -> "reached after local y=$(false)" IS printed((i=0)); echo reached                              -> exits 1 (the arithmetic result 0 counts as "false")false | true; echo ok                              -> prints "ok" (no pipefail)set -eo pipefail; false | true                     -> exits 1

The rules to remember: set -e does not stop on a failure inside an if condition, in a &&/|| list (except the last command), or in a function called from an if. local y=$(false) hides the failure because local itself succeeds; declare first (local y; y=$(false)). ((i=0)) returns status 1 because the value is zero. And set -u catches typos: echo "$UNDEFINED" stopped with UNDEFINED: unbound variable.

while read in a pipe loses variables.

Shell
$ n=0; printf '1\n2\n' | while read -r x; do n=$((n+1)); done; echo "n=$n"n=0$ n=0; while read -r x; do n=$((n+1)); done < <(printf '1\n2\n'); echo "n=$n"n=2

Each side of a pipe runs in a subshell, so n changed in a copy. Feeding the loop with < <(...) (process substitution) keeps the loop in the main shell. Also: read without -r eats backslashes, and without IFS= it trims leading spaces (I saw [c] instead of [ c]).

ShellCheck finds this for you. On a 3-line bad script it printed SC2086 (info): Double quote to prevent globbing and word splitting and SC2045 (error): Iterating over ls output is fragile. Use globs.

Proof 6: SSH, systemd and cron

I generated a key and read its facts:

Shell
$ ssh-keygen -t ed25519 -N "" -C lab-key -f ssh/id_lab -q$ stat -c '%a %n' ssh/id_lab ssh/id_lab.pub600 ssh/id_lab644 ssh/id_lab.pub$ ssh-keygen -lf ssh/id_lab.pub256 SHA256:6zPewwbWJbrUqkZ5IlNATU8vjYb0Lr0vG0TOAmR4TrM lab-key (ED25519)$ chmod 644 ssh/bad; ssh-keygen -y -f ssh/bad@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@Permissions 0644 for 'ssh/bad' are too open.

The private key is 600, the public key may be 644, and SSH refuses a private key that others can read. ssh -G web with a config block printed the settings it would use (user deploy, hostname 203.0.113.10, port 2222), which is a safe way to check a ~/.ssh/config without connecting. Port forwarding with -L was run for real in lesson 5.

Services: I checked a unit file with systemd-analyze verify ./demo.service. It only warned Special user nobody configured, this is not safe!, so the syntax was fine. I could not start it: PID 1 here is not systemd (ps -p 1 -o comm= printed process_api). So systemctl, journalctl and timers are not run in this lesson. For cron, the key fact I can prove is the environment:

Shell
$ ./cronenv.sh                       # my normal shellPATH=/nix/var/nix/profiles/default/bin:/root/.local/bin:...:/usr/local/bin:/usr/bin:/bin$ env -i ./cronenv.sh                # an empty environmentPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin$ env -i /bin/bash -c 'which node'/usr/local/bin/node

With an empty environment the PATH is short. Cron starts jobs with a small environment too (I believe the usual cron default PATH is just /usr/bin:/bin, but I could not open the crontab manual page on this VM to confirm it, and I did not run a real cron daemon). Tools installed under your home directory, like nvm's node, are not found.

Proof 7: disk, memory and load

A tiny tmpfs with only 30 inodes shows both kinds of "full":

Shell
$ mount -t tmpfs -o size=4m,nr_inodes=30 tmpfs mnt$ i=0; while touch mnt/f$i 2>/dev/null; do i=$((i+1)); done; echo "created $i files"created 29 files$ touch mnt/ztouch: cannot touch 'mnt/z': No space left on device$ df -h mnt | tail -1tmpfs           4.0M     0  4.0M   0% /tmp/lab-linux-c/mnt$ df -i mnt | tail -1tmpfs              30    30     0  100% /tmp/lab-linux-c/mnt

df -h says 0% used, yet the file system is full: the inodes are gone. (One inode was already used by the mount root, so 29 files fit.) Then the deleted-but-open file:

Shell
$ dd if=/dev/zero of=mnt/big bs=1M count=3 status=none$ (sleep 30 < mnt/big &); rm mnt/big$ df -h mnt | tail -1tmpfs           4.0M  3.0M  1.0M  75% /tmp/lab-linux-c/mnt$ du -sh mnt0	mnt$ lsof +L1 mntCOMMAND  PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAMEsleep   2825 root  0r  REG  0,41  3145728    0   31 /tmp/lab-linux-c/mnt/big (deleted)

df says 3 MB used; du says 0. lsof +L1 lists files with zero links that are still open. I stopped sleep and df went back to 0 used. Memory and load (idle VM, so small numbers):

Shell
$ uptime 12:52:48 up 15 min,  0 user,  load average: 0.14, 0.24, 0.21$ nproc4$ free -h | head -2               total        used        free      shared  buff/cache   availableMem:            15Gi       606Mi        13Gi        14Mi       1.7Gi        15Gi$ df -h / | tail -1/dev/vda        252G   38G  1.3G  97% /

Notice the last line: the root file system of this VM is 97% full with 1.3 GB left. That is exactly the thing a calm engineer would flag first, even though CPU and memory look perfect.

Proof 8: the "server is slow" walk-through

Interviewers love this because there is no single right command. They want to hear a method (see lesson 7). Here is the version I say out loud, with what each step would tell me:

  1. Clarify. "Slow how? For everyone or one user? Since when? Did anything change (deploy, cron job, traffic)?" This costs nothing and often finds the cause.
  2. Is it the machine or the app? uptime for load against nproc; top or ps -eo pid,stat,%cpu,%mem,etime,comm --sort=-%cpu | head for the biggest users.
  3. CPU. Load above the core count means queueing. High us/sy in vmstat 1 and a big r column means CPU-bound. High wa means waiting for disk.
  4. Memory. free -h, read available. vmstat si/so columns above zero mean swapping. dmesg -T | grep -i killed for OOM kills.
  5. Disk space. df -h, then df -i, then du -xh --max-depth=1 / | sort -h top-down. If df and du disagree, lsof +L1.
  6. Disk speed. iostat -dx 1 (from the sysstat package): high %util and long await mean the disk is the bottleneck.
  7. Network. ss -s, ss -ltnp, and curl -w to split the time into connect, first byte and total. In my lab, curl -s -o /dev/null -w "code=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" http://127.0.0.1:8766/ printed code=200 connect=0.000536 ttfb=0.003506 total=0.003564.
  8. Logs. The application log around the time it started, the status-code counts with the awk | sort | uniq -c recipe above, then the system log (journalctl -u name --since "10 min ago", not run here).
  9. Fix and verify, then write it down. Change one thing at a time, confirm the symptom is gone, and note the cause for the post-mortem.

On the lab VM, that routine would have said: load 0.14 on 4 cores (fine), 15 GiB available (fine), / is 97% used (watch this), so the next questions would be "what is growing?" and "is there log rotation?".

Try it

Use a throwaway VM or container, never production.

  1. As a normal user, make a script with echo hi, without chmod +x. Run ./script, then bash script. What are the two outputs and the exit code of the first?
  2. Create a directory with mode 644, put a file in it, and compare ls dir with cat dir/file. Repeat with 711. (If you are root, use sudo -u nobody or setpriv, because root ignores the mode.)
  3. Run ls /nonexistent > o.txt 2>&1 and then ls /nonexistent 2>&1 > o2.txt. Which file is empty and why?
  4. Start sleep 100 &, kill it with kill, run wait $!; echo $?. Repeat with kill -9. What two numbers do you get?
  5. Make a file called a b.txt and delete it with ls *.txt | xargs rm. What happens? Fix it with find ... -print0 | xargs -0.
  6. Run v=""; [ $v = x ] and [[ $v = x ]]. Compare the output.
  7. Run bash -c 'set -e; ((i=0)); echo reached'. Does it print? Why?
  8. Mount a small tmpfs with -o size=4m,nr_inodes=30, create files until touch fails, and compare df -h with df -i.
  9. Pick three questions below and answer them out loud in under 60 seconds each.

Answers

  1. ./script prints Permission denied, exit code 126. bash script prints hi.
  2. With 644: ls dir lists the name, cat dir/file says Permission denied. With 711: ls dir is denied, but cat dir/file works.
  3. The second one (o2.txt) is empty: stderr was copied to the terminal before stdout moved to the file.
  4. 143 for the plain kill (SIGTERM) and 137 for kill -9 (SIGKILL).
  5. rm is called with a and b.txt and complains that they do not exist, and any other .txt files are removed. The fix is find . -maxdepth 1 -name '*.txt' -print0 | xargs -0 rm (check with ls first).
  6. [ ] prints unary operator expected with exit 2; [[ ]] quietly returns 1.
  7. It does not print reached: ((i=0)) evaluates to 0, which counts as failure (status 1), and set -e stops the script.
  8. df -h shows 0% used while df -i shows 100% inodes used, and touch says No space left on device.
  9. There is no single answer. Check yourself against the model answers.

Interview questions

Read this mode aloud: -rwxr-x--- and 750. What can each class do? (Must know)

I split it into three groups of three: owner rwx (7), group r-x (5), others --- (0). So the owner can read, write and run it, the group can read and run it, and everyone else has no access. The number is the sum per group: read is 4, write is 2, execute is 1. For a directory, x means "can enter and reach files inside" and r means "can list names". I always check a mode with stat -c '%a %A', because it shows both forms.

What does the execute bit mean on a directory? Can you have read without execute? (Must know)

Execute on a directory is the "search" permission: you can reach things inside by name. Read lets you list the names. In my lab, as an unprivileged user, a 644 directory let me ls the names but cat dir/file failed with Permission denied. A 711 directory was the opposite: I could not list it, but I could read a file when I knew its name. The usual good setup is 755 for shared directories and 700 for private ones.

What is umask, and what will a new file get with umask 027? (Must know)

umask is a set of bits that are removed from the default mode of new files (666) and directories (777). With 027, new files get 640 and new directories get 750; I verified that: -rw-r----- and drwxr-x---. It is set per process and inherited by children, so a service's umask comes from its parent or its unit file. A default of 022 gives 644 and 755. I use 077 when a script creates secrets, and I put it in a subshell so it does not leak.

What are setuid and setgid, and why are they a security risk? (Senior signal)

Setuid on an executable makes it run with the file owner's user id instead of the caller's. passwd is 4755 and owned by root, so you can change your own password, which needs a write to a root-owned file. The danger is that any bug in a setuid-root program becomes a root exploit, and a setuid shell script is even worse, so Linux ignores setuid on scripts. I keep the list of setuid files small: I would audit with find / -xdev -perm -4000 -type f (I did not run that whole-disk search in the lab). Setgid on a directory is different and useful: new files inherit the directory's group, which helps shared project folders. I did show the s in drwxr-sr-x after chmod g+s.

What is the sticky bit and where do you see it? (Senior signal)

On a directory, the sticky bit means you can delete or rename only the files you own, even if the directory is writable by everyone. /tmp is 1777 (drwxrwxrwt). I tested it: an unprivileged user got Operation not permitted removing a root-owned file from a 1777 directory, and the same rm worked after I removed the sticky bit. The reason is that deleting a name is a write to the directory, not to the file, so the extra rule is needed.

A script says Permission denied, but the file is there. Name three causes. (Trap)

First, the file is not executable: chmod +x, or run it as bash script. I saw exit status 126 for this, as opposed to 127 for "command not found". Second, a directory in the path has no x bit for me. Third, the file system is mounted noexec (common on /tmp in hardened systems; I did not test it). A fourth: the shebang points to an interpreter that is missing or not executable, which gives a confusing bad interpreter message. I start with ls -l, then namei -l path, which prints the mode of every directory in the path.

Someone says "just chmod 777 it". What do you say? (Trap)

That 777 gives everyone, including every service on the machine, the right to change the file, so it hides the real problem. The real question is who needs access. If it is one service user, I use chown service:service and 750 or 640. If a team needs it, I create a group, set chown :team, use g+rwX, and set setgid on the directory so new files get the group. I use X (capital) in chmod -R so directories get execute but plain files do not; I checked that chmod -R a+X left a plain file at 644.

Hard link versus symbolic link? (Must know)

A hard link is a second name for the same inode, so the data stays until the last name is removed. A symbolic link is a small file that stores a path. In my lab, ln orig.txt hard.txt gave both names the same inode number (3006507) with link count 2, and ln -s made a separate file. After rm orig.txt, cat hard.txt still printed data, and cat soft.txt failed with No such file or directory (a dangling link). Hard links cannot cross file systems or point to directories; symlinks can do both.

SIGTERM versus SIGKILL? When do you use each? (Must know)

SIGTERM (15) is a polite request. The process can catch it, finish its work, close files and exit; my test script printed got TERM, cleaning and exited 0. SIGKILL (9) cannot be caught or ignored, the kernel just removes the process, so there is no cleanup: temp files stay, locks stay, child processes may be orphaned. I always send SIGTERM first, wait a few seconds, and use kill -9 only if it does not exit. systemd and Kubernetes do the same: term, grace period, kill.

What do exit codes 0, 1, 2, 126, 127, 137 and 143 mean? (Must know)

0 is success; 1 is a general failure; 2 usually means wrong usage (ls /nonexistent gave 2 in my lab, and grep uses 2 for errors, with 1 meaning "no match"). 126 means the file was found but cannot be executed; 127 means command not found. Anything above 128 is 128 plus a signal number: 137 is SIGKILL and 143 is SIGTERM. I reproduced 137 and 143 with kill -9 and kill. Exit codes are one byte, so exit 300 returns 44.

What is a zombie process? Can you kill it? (Must know)

A zombie is a process that has finished but whose parent has not yet called wait to read its exit status, so a small entry stays in the process table. It shows Z in ps. I made one in the lab with a parent that did not wait: STAT was Z. It uses no CPU or memory, only a PID slot, so a few are harmless but thousands can exhaust PIDs. You cannot kill it, because it is already dead. The fix is to make the parent reap it (fix the bug, or send SIGCHLD), or stop the parent, after which init adopts and reaps it.

Why does PID 1 matter inside a container? (Senior signal)

The process with PID 1 has two special jobs: it adopts orphans and must reap them, and the kernel does not apply default signal actions to it, so if it has no handler, SIGTERM is ignored. A Node or Python app run as PID 1 may not reap zombies or react to docker stop, which then waits 10 seconds and sends SIGKILL. The fixes are a tiny init (docker run --init, tini) and handling SIGTERM in the app. I did not run containers for this lesson; this comes from memory and experience.

How do you keep a command running after you log out? (Must know)

When the terminal closes, the shell sends SIGHUP to its jobs and they usually die. Options: nohup cmd > out.log 2>&1 &, which makes the process ignore SIGHUP and sends output to a file; disown to remove a running job from the shell's list; tmux or screen to keep a whole session alive; or, properly, a systemd service. For anything that should survive reboots and restart on failure, I use a service, not nohup. I did not test logout behaviour in the sandbox (no real terminal), so this answer is from memory and experience.

Does the order in cmd > file 2>&1 matter? (Trap)

Yes, redirections are processed left to right. 2>&1 means "point stderr at where stdout points right now". In cmd > out.txt 2>&1 stdout goes to the file first, so stderr follows it. In cmd 2>&1 > out.txt stderr copies the terminal first, so errors still print on screen while the file stays empty. I ran both with ls /nonexistent: the first file held the error, the second was empty. In bash, &> file is the short form. In a cron job this matters because the mail or log you expect will not contain the errors.

Why does sudo echo x > /etc/file fail, and how do you fix it? (Trap)

The > is performed by my shell before sudo is even started, so the file is opened with my permissions. sudo only elevates echo. The fix is to elevate the writer: echo x | sudo tee /etc/file >/dev/null (add -a to append), or sudo sh -c 'echo x > /etc/file'. I ran the tee form in the lab and it wrote the file, but I was root, so the failing case is not run here.

What goes wrong with ls | xargs rm, and what is the safe version? (Senior signal)

xargs splits on whitespace and treats quotes and backslashes specially, so a file named a b.txt becomes two arguments. In my lab it printed rm: cannot remove 'a' and rm: cannot remove 'b.txt'. The safe form is NUL-separated: find . -maxdepth 1 -name '*.txt' -print0 | xargs -0 rm. I also add -r (GNU) so xargs does nothing when the input is empty; without it echo -n | xargs echo run still printed run. -n2 groups arguments in twos, -P4 runs four at a time. For simple deletes, find ... -delete has no pipe at all.

When do you reach for grep, sed or awk? (Must know)

grep to find lines: grep -E '" (4|5)[0-9]{2} ' access.log found the 4 error lines. sed to edit text as a stream: sed -i.bak 's/^port=.*/port=8080/' conf.ini. awk to work with columns and totals: awk '$11 > 1 {print $7, $11}' printed the slow requests. A practical rule is that if I can say it as "lines that match", it is grep; "change this text", sed; "for each line, do something with field N", awk. For big jobs or nested data (JSON), I use jq or a real language instead.

From an access log, how do you get the most frequent IPs or the count of each status code? (Must know)

Extract the field, sort so equal values are neighbours, count, and sort the counts: awk '{print $9}' access.log | sort | uniq -c | sort -rn. On my sample log it printed 4 200, 2 500, 1 404, 1 401. For IPs I replace $9 with $1 and add | head. The sort before uniq -c is needed because uniq only collapses adjacent lines. I always check which field is which with head -1 first, because quoted request strings contain spaces.

find -exec {} \; versus {} +, and what is dangerous about -delete? (Senior signal)

\; runs the command once per file; + batches many files into one call. On my four-file tree, \; produced 4 lines of output and + produced 1, which means one process instead of four. For thousands of files + is far faster. -delete is handy, but the order of arguments matters: find . -delete -name '*.log' would delete everything, because tests are evaluated left to right. I put -delete last and run the same command with -print first to see what it would remove.

sed -i works on Linux, but the same script fails on a Mac. Why? (Trap)

GNU sed (Linux) accepts -i with an optional suffix attached (-i.bak) or none. BSD sed (macOS) requires a suffix argument after -i, so sed -i 's/a/b/' file fails or misreads the next argument; the portable form is sed -i.bak 's/a/b/' file && rm file.bak. I only ran GNU sed 4.9, so the macOS behaviour is not run, and I would test on a Mac. The same kind of trap exists for date, xargs -r and readlink -f. For portability I can use perl -pi -e or write the edit in Python.

Why must you quote variables? Show a bug. (Must know)

Unquoted $var is split on spaces and expanded as a glob. With f="my file.txt", ls -l $f looks for two files, my and file.txt; I ran it and got two errors. With pat="*.dat", echo $pat printed the matching file names and echo "$pat" printed the star. The worst case is rm -rf $dir/ with an empty dir, which becomes rm -rf /; I only echoed that command, never ran it. So I quote every expansion, use "${dir:?}" to stop on empty values, and run shellcheck, which flags it as SC2086.

What is the difference between [ ] and [[ ]]? (Must know)

[ is a normal command (an alias of test), so the shell splits and expands its arguments first. [[ ]] is bash syntax, so it parses its own words. The practical differences, all shown in my lab: with an empty $v, [ $v = x ] fails with unary operator expected (exit 2) while [[ $v = x ]] returns 1 quietly; [[ abc == a* ]] does pattern matching; [[ ]] supports &&, || and =~ regex. The cost is portability: [[ ]] is not POSIX, so in a #!/bin/sh script on Debian (where sh is dash) I use [ ] with quotes.

What does set -euo pipefail do and where does it fail you? (Senior signal)

-e exits on a failed command, -u treats unset variables as errors, and -o pipefail makes a pipeline fail if any stage fails. The surprises I proved: -e is ignored in if conditions and &&/|| lists (false && echo x did not stop the script); local y=$(false) does not fail because local returns 0; ((i=0)) returns status 1 and stops the script; and pipefail can fail a harmless pipe, for example grep nomatch file | wc -l exits 1 because grep found nothing, which printed 0 and then stopped my script. I use it as a safety net but still check important commands explicitly, and I use trap ... ERR or EXIT for cleanup.

A counter in a while read loop is still 0 after the loop. Why? (Trap)

The loop was on the right side of a pipe, and each part of a pipeline runs in a subshell, so the increments happened in a copy. My test printed n=0 with printf ... | while read, and n=2 when I wrote while read ...; done < <(printf ...). Other fixes: redirect from a file (done < file) or use shopt -s lastpipe (non-interactive bash). I also use read -r so backslashes are kept and IFS= so leading spaces are kept.

How does SSH key login work, and what permissions must the files have? (Must know)

I make a key pair; the private key stays with me, and the public key goes into ~/.ssh/authorized_keys on the server. At login the server sends a challenge, and my client proves it holds the private key by signing it, without sending the key. Permissions matter: the private key must be 600 (my lab test printed WARNING: UNPROTECTED PRIVATE KEY FILE! for 644), ~/.ssh should be 700, and authorized_keys not writable by others, otherwise sshd with StrictModes refuses it (lesson 5 showed that login failing and then working after chmod 700). I also check the server host key fingerprint the first time, and I protect the key with a passphrase plus ssh-agent.

Explain ssh -L 9000:127.0.0.1:8081 host. (Must know)

It opens port 9000 on my machine. Anything I send to localhost:9000 travels through the encrypted SSH connection and is delivered to 127.0.0.1:8081 as seen from the server, which is the server's own localhost. It is how I reach a database or admin page that listens only on the server's loopback address. -R does the reverse (the server listens and sends traffic back to me) and -D makes a SOCKS proxy. I ran -L end to end in lesson 5 and reviewed, but did not run, -R and -D. Add -N if you only want the tunnel and no shell.

systemd or cron for a scheduled job? Why does a cron job fail when the same command works in my terminal? (Senior signal)

Cron is simple and everywhere; a systemd timer gives logs in the journal, Persistent=true catch-up after downtime, dependencies, and resource limits. For the cron failure, the usual cause is the environment: cron runs with a tiny PATH and none of my shell setup. My lab showed it: a normal shell had a long PATH including /opt/node22/bin, while env -i gave only system directories. Fixes: absolute paths, set PATH= at the top of the crontab, redirect output (>> /var/log/job.log 2>&1) so errors are not lost, and use flock to stop overlapping runs. I verified the environment effect, but I did not run a real cron daemon or a timer here.

df says the disk is full but du shows much less. Why? (Must know)

A process still holds a deleted file open. rm removes the name, but the space is freed only when the last open handle closes. du adds up names; df asks the file system. In my lab: df showed 3.0M used and du -sh showed 0. lsof +L1 listed /tmp/lab-linux-c/mnt/big (deleted). To fix it I restart the process (or truncate the file with : > file before deleting in future). Other causes: files hidden under a mount point, and reserved blocks on ext4.

"No space left on device", but df -h shows free space. What now? (Trap)

Check inodes with df -i. In my lab df -h said 0% used while df -i said 100%, and touch failed with exactly that message. Millions of small files (sessions, caches, mail queues) can use up all inodes. I find the directory with the most entries, delete or archive them, and fix the producer. Other causes: disk quotas, or I am looking at a different mount than the one that is full (df -h /path tells me).

free shows only 100 MB free. Is the server out of memory? (Trap)

Not necessarily. Linux uses spare RAM as page cache to speed up file reads and gives it back when programs need it. I read the available column, not free. On my lab VM, free was 13 GiB and available 15 GiB with 1.7 GiB in cache. Real memory pressure looks like: available near zero, swapping (si/so in vmstat), and OOM-kill lines in dmesg. I did not trigger an OOM kill in the lab, so that part comes from memory and the kernel docs I read for lesson 7, not from my own output.

What does load average mean? Is a load of 8 bad? (Must know)

It is the average number of tasks that are running, waiting for a CPU, or (on Linux) blocked on disk, over 1, 5 and 15 minutes. I compare it with the core count (nproc). On 4 cores, a load of 8 means about twice as much work as the CPUs can do, so tasks queue; on 16 cores it is half idle. I also read the trend: 1 min < 15 min means it is recovering. Since disk waits count, high load with low CPU use points to an I/O problem, so I look at wa in top and at iostat.

Walk me through "the server is slow". (Must know)

I ask what changed and when it started. Then I go resource by resource, checking utilisation, saturation and errors: uptime and top for CPU and load, free -h (available) and vmstat for memory and swapping, df -h, df -i and du for disk space, iostat -dx 1 for disk speed, ss and curl -w for the network, and finally the application logs with an awk | sort | uniq -c count of status codes. I narrow to machine versus application first, because a slow database query will not show up in any OS tool. I change one thing at a time, confirm the fix, and write down the cause. In my lab VM this routine would have flagged / at 97% with 1.3 GB left, before anything else.

A process ignores kill -9. How is that possible? (Senior signal)

The kernel can only remove a process when it is back in user mode. If the process is in uninterruptible sleep (D in ps), usually waiting for a disk or a hung network file system (NFS), the signal stays pending until the I/O returns. A zombie also cannot be killed because it is already dead. I confirm with ps -o pid,stat,wchan:20,cmd -p PID, then fix the cause: the storage or mount, not the process. I did not reproduce a D-state process in the sandbox; this is from memory and experience. Hung NFS mounts are the classic case, and umount -l or a reboot is sometimes the only way out.

How do you check for a script's mistakes before running it, and how do you debug one? (Must know)

Before running: bash -n script checks syntax, and shellcheck script finds quoting and logic traps (it flagged rm $1 and a for f in $(ls) loop on my sample file). While debugging: bash -x script prints each command after expansion, or set -x around the part I care about, with PS4='+ ${LINENO}: ' for line numbers. I also make scripts safe to re-run (idempotent), use trap cleanup EXIT, and test destructive steps with echo in front first, as I did for the rm -rf "$dir/" example.

Rapid-fire table

Short questions, short answers. Practise saying each in one breath.

QuestionOne-breath answer
Show hidden filesls -a (names starting with .)
Current directorypwd
Absolute vs relative pathStarts with / vs counted from the working directory
Find a command's locationcommand -v name or type name (cd is a builtin; ls is /usr/bin/ls)
What does $? holdExit status of the last command
What do $$ and $! holdPID of the current shell; PID of the last background job
> vs >>Overwrite vs append
/dev/nullA file that throws away what you write
grep -v / -c / -r / -lInvert / count / recurse / list names only
grep -qSilent; use the exit status (0 found, 1 not found)
sort vs sort -nText order puts 100 before 9; -n is numeric
kill default signalSIGTERM (15)
Ctrl-C, Ctrl-Z, Ctrl-DSIGINT; suspend the job (SIGTSTP); end of input
& at end of commandRun in the background
chmod u+x fAdd execute for the owner
Default file mode with umask 022644; directories 755
What does chown -R doChanges owner recursively; test the path first
Where do logs usually live/var/log and the systemd journal
kill -0 PIDChecks that the process exists, sends no signal
du -sh * vs df -hSize of names vs free space of the file system
Why tail -FFollows by name, survives log rotation
Check open portsss -ltnp
Make a script run on a schedulecron line or systemd timer
What is nprocNumber of CPU cores, to compare with load
Mock round: 15 minutes, six questions

Ask a friend to read these, or time yourself. For each, the "good answer includes" list is what a strong candidate says in about two minutes. Score yourself: 2 points if you hit most items, 1 if half, 0 if you freeze.

1. "I deleted a 5 GB log but the disk is still 100%. What happened?" Good answer includes: a process holds the deleted file; df versus du; lsof +L1; restart or truncate; log rotation as the long-term fix.

2. "A deploy script works on my laptop but fails in cron." Good answer includes: tiny environment and PATH; absolute paths; redirecting stderr to a log; the working directory; flock; checking with env -i.

3. "Explain what this does: find /var/log -name '*.gz' -mtime +30 -print0 | xargs -0 rm." Good answer includes: finds compressed logs older than 30 days; NUL separators keep odd names safe; rm is destructive so run with -print or ls first; or use -delete; and be careful with the path.

4. "Why did this script keep running after cd failed?" Good answer includes: cd returns non-zero but the script ignored it (I saw still going in /tmp/lab-linux-c); use cd dir || exit 1 or set -e; quote the path; shellcheck warns (SC2164).

5. "Production server, load is 25 on 8 cores, CPU is only 10% busy." Good answer includes: Linux load counts tasks in D state; look at wa and iostat; check a hung NFS mount or a slow disk; ps for D processes; not a CPU problem.

6. "How do you reach a database that listens only on localhost of a remote server?" Good answer includes: ssh -L local:127.0.0.1:5432 host; the second address is from the server's point of view; -N for no shell; bastion with -J; never open the database to the internet.

Scoring: 10 to 12 is strong. 6 to 9 means re-read the lessons for the questions you missed. Below 6, do the Try-it lab above with a real shell instead of reading again.

Key takeaways

  • Learn the one-sentence rule, one command, one trap for each topic. Interviewers want to hear that you have run it.
  • Permissions: read 750 and rwxr-x--- both ways, know what x means on a directory, and that umask removes bits (027 gives files 640).
  • Special bits: setuid runs as the owner (risky), setgid on a directory shares a group, the sticky bit protects files in /tmp.
  • Exit codes: 126 cannot execute, 127 not found, 128 + n means killed by signal n (137 is SIGKILL, 143 is SIGTERM). Always SIGTERM first.
  • A zombie is a finished child that nobody collected. You fix the parent, not the zombie.
  • cmd > file 2>&1 and cmd 2>&1 > file are different. sudo does not elevate your redirect, tee does.
  • Use -print0 with xargs -0 for odd file names. uniq -c needs sort first. find -exec {} + is faster than \;.
  • Quote every variable. [[ ]] is safer than [ ] in bash. set -e has holes, and a while read after a pipe runs in a subshell.
  • SSH: the private key is 600, -L forwards to an address seen from the server, and cron runs with a small environment.
  • Disk full has three faces: space (df -h), inodes (df -i) and deleted-but-open files (lsof +L1). Read available in free. Compare load with nproc.
  • For "the server is slow", state a method: clarify, CPU, memory, disk, network, logs, fix, verify.
  • Be honest: say what you ran and what you only read. Not run here: systemctl/journalctl, a real cron daemon, an OOM kill, a D-state process, container signals, macOS sed.

What's next

This is the last lesson of the series. You now have the shell, permissions, processes, text tools, scripting, SSH, services and troubleshooting in one place. Where to go next:

  • DevOps to turn these commands into pipelines, infrastructure and deployments.
  • Backend to see how the servers you now understand run APIs and databases.
  • Node.js for running and debugging a real service on Linux, including signals and graceful shutdown.
  • Security for hardening the things you touched here: SSH, permissions and least privilege.
  • Git and GitHub because every script and config in this series belongs in version control.

Pick one of your own servers or a throwaway VM and run the Try-it exercises with the tools open. Then say three of the model answers out loud, without looking. That is the best preparation.

Published by Jainik · Last updated · How these lessons are made

Share this lessonWhatsAppLinkedInX

Keep learning

Part of the study guide

Related series