Linux Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for freshers in India preparing for the Linux round of support, system admin, cloud and DevOps roles, and for developers with a few years of experience who are now asked to run and debug services on Linux servers. It has 60 questions in three levels: everyday commands and concepts, the problems you meet on real servers, and senior questions on internals, performance and safe automation. Every output shown was produced by running the commands with bash 5.2 and GNU tools on Ubuntu 24.04, so you can repeat them in a practice folder. Answer each question aloud first, then compare with ours, and take the timed mock test when you are ready.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 1,886 open Linux jobs
What Linux interviews cover
- Files and permissions: the directory layout, links, chmod numbers, umask, ownership and the special bits. Almost every interview starts here.
- Processes and services: ps and top, signals, systemd units and the journal, which are daily work on any server.
- Text processing and scripting: grep, sed, awk, sort and uniq, plus bash scripts with correct quoting, exit codes and error handling.
- Servers in practice: networking commands, disk and memory, logs, packages, users and groups, cron and SSH.
- Troubleshooting: for experienced roles, a slow server, a full disk or a failing service, explained as a safe order of checks.
How to prepare (freshers and experienced)
- Freshers: install Ubuntu in a virtual machine or WSL and do everything from the terminal for two weeks: create users, set permissions, write a backup script and schedule it with cron.
- Practise the text tools on real data, such as a web server access log, until you can find the top 10 client IPs or count errors per hour without looking anything up.
- Experienced: prepare two or three incidents you handled (a full disk, a crashing service, a slow API) and tell each as symptom, checks, cause and fix.
- Read the man page of each command you list on your CV (man 5 crontab, man systemd.exec); interviewers like to ask about one specific option.
- Learn where Ubuntu and RHEL-family systems differ: apt versus dnf, log file names such as auth.log versus secure, and the ssh versus sshd service name.
Common mistakes and interview-day tips
- Say what you would check before what you would change, and why; interviewers listen for a safe order of steps on a production server.
- Do not offer chmod 777, kill -9 or a reboot as the fix. Mention them only as a last resort, with the reason and the risk.
- If you forget an option, say how you would find it (man, --help, a quick test in a scratch folder) instead of guessing.
- In scripting questions, quote your variables and mention exit codes and error handling without being asked.
Linux interview questions: Easy questions (freshers)
20 questions1. What are the main directories in the Linux file system hierarchy, and what goes where?
Everything hangs off one root, /. /etc holds system-wide configuration, such as /etc/ssh/sshd_config. /home has the users' home folders and /root is the root user's home. /usr holds installed programs and libraries, /var holds data that changes, such as logs in /var/log, and /tmp is for temporary files that may be cleared at reboot. /dev has device files, and /proc and /sys are virtual file systems filled by the kernel. When asked where something lives, give the full path, not just the folder name.
2. What is the difference between a hard link and a soft (symbolic) link?
A hard link is a second name for the same inode, so both names share the same data and the link count goes up; the data is freed only when the last name is removed. A soft link is a small file that stores a path. In the run, after notes.txt is deleted the hard link still prints hello, but the soft link is broken because the path it points to is gone. Hard links cannot cross file systems or point to directories; soft links can do both. ls -li shows inode numbers, which is how you spot hard links.
echo hello > notes.txt
ln notes.txt hard.txt # hard link
ln -s notes.txt soft.txt # symbolic link
stat -c '%h %n' notes.txt # 2 notes.txt
rm notes.txt
cat hard.txt # hello
cat soft.txt
# cat: soft.txt: No such file or directory3. How do chmod numbers like 750 and 644 work?
Each digit is the sum of read 4, write 2 and execute 1, for owner, group and others in that order. So 750 is rwx for the owner, r-x for the group and nothing for others, which suits a script only your team should run, and 644 is the usual file mode: the owner edits, everyone reads. The symbolic form u=rw,g=r,o= sets the same bits by name. Note that chmod +x without u, g or o adds x for all three classes, as the 751 shows. Never use chmod 777 to fix a permission error; it lets any user change the file.
touch deploy.sh
chmod 750 deploy.sh
stat -c '%A %a' deploy.sh # -rwxr-x--- 750
chmod u=rw,g=r,o= deploy.sh
stat -c '%A %a' deploy.sh # -rw-r----- 640
chmod +x deploy.sh
stat -c '%A %a' deploy.sh # -rwxr-x--x 751
chown asha:devops deploy.sh # change owner and group (needs root)4. What is umask, and what permissions do new files get with umask 027?
umask lists the permission bits to remove from new files and directories. Programs normally create files with 666 and directories with 777, and the umask is taken away from that. With 027 the group loses write and others lose everything, so the new file is 640 and the new directory 750, as the run shows. The common default 022 gives 644 files and 755 directories. umask never adds the execute bit to a file, so a new script still needs chmod +x. Set it in the shell profile, or with UMask= in a systemd unit for a service.
umask 027
touch report.txt
mkdir backups
stat -c '%a %n' report.txt backups
# 640 report.txt
# 750 backups5. What do /etc/passwd and /etc/shadow contain?
/etc/passwd lists accounts, one per line, with seven colon-separated fields: user name, a placeholder x, the user ID, the primary group ID, a comment, the home directory and the login shell. Everyone can read it. The password hashes are in /etc/shadow, which only root can read, together with password ageing settings. UID 0 is root, and on Ubuntu and RHEL normal users start at 1000 by default (UID_MIN in /etc/login.defs). Use getent passwd rather than reading the file, because it also finds users that come from LDAP or another directory.
# name:x:UID:GID:comment:home:shell
asha:x:1001:1001:Asha Rao:/home/asha:/bin/bash
getent passwd asha
id asha
groups asha6. How do you find which processes are using the most CPU or memory?
For a snapshot, ps aux lists every process with its PID, user, CPU and memory share and command, and --sort=-%cpu puts the heaviest first. top, or htop where installed, shows the same view live; press P to sort by CPU and M by memory. To find a process by name use pgrep -a, which prints the PID and full command line, instead of ps aux piped to grep, which also matches the grep itself. Remember that %CPU in ps is averaged over the whole life of the process, so a sudden spike shows up better in top.
ps aux --sort=-%cpu | head -5
ps aux --sort=-%mem | head -5
pgrep -a nginx
top # P = sort by CPU, M = by memory, q = quit7. What is the difference between kill and kill -9?
kill sends SIGTERM (15) by default, which asks the process to stop. The program can catch it, finish the current request, flush data and remove its lock files. kill -9 sends SIGKILL, which the kernel applies at once: it cannot be caught or ignored, so no cleanup runs and files may be left half written. Send SIGTERM first, wait a few seconds, and use -9 only if the process will not stop. A shell reports a process killed by a signal as exit status 128 plus the signal number, so the 143 in the run means SIGTERM.
kill -l 15 # TERM
kill -l 9 # KILL
sleep 100 &
kill $! # SIGTERM by default
wait $!; echo "exit status: $?"
# exit status: 1438. How do you manage a service with systemctl?
start, stop and restart act on the service right now. reload asks it to reread its configuration without dropping connections, when the service supports it, as nginx does. enable is different: it makes the service start at boot by creating symlinks, but it does not start it now unless you add --now. status shows whether it is running, its main PID, the last log lines and the reason for a failure. A common mistake is editing a config file, running enable and expecting the change to apply; it needs a reload or restart.
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx # reread config
sudo systemctl enable --now nginx
systemctl status nginx
systemctl is-enabled nginx9. What is the difference between apt update and apt upgrade?
apt update only downloads fresh package lists from the configured repositories; nothing installed changes. apt upgrade then installs newer versions of the packages you already have. Run update first, otherwise apt works from old lists and may not see a new security fix. apt remove deletes a package but keeps its configuration files, while purge removes those as well. On RHEL, Rocky Linux and Fedora the tool is dnf, which refreshes its metadata itself when it is out of date. On a server, read the list of changes before you type y.
sudo apt update # refresh package lists
sudo apt upgrade # install newer versions
sudo apt install htop
sudo apt remove htop # keeps config files
sudo apt purge htop # removes config files too
sudo dnf install htop # RHEL, Rocky, Fedora10. Which commands show a server's IP address, routes and listening ports?
ip addr shows each network interface with its IP addresses, and ip route shows the routing table, including the default gateway. ss -tlnp lists listening TCP sockets with numeric ports and the process that owns each one; add -u for UDP, and use sudo to see process names for other users. ping checks basic reachability and curl checks that an HTTP service really answers. The older ifconfig and netstat come from the net-tools package, which many minimal images no longer install, so learn the ip and ss forms.
ip addr show # or: ip a
ip route # default gateway
ss -tlnp # listening TCP ports
ping -c 4 10.0.0.1
curl -I http://localhost:8080/11. What is the difference between df and du?
df reports space per mounted file system: size, used, available and the mount point. du walks a directory and adds up the space its files use. Use df to see that /var is 95% full and du to find which folder inside it is to blame; du -h --max-depth=1 piped to sort -h lists the folders from smallest to largest. The two can disagree: a deleted file that a process still holds open is counted by df but not by du, and du cannot count folders you have no permission to read.
df -h # space per file system
df -h /var
du -sh /var/log # total size of one folder
sudo du -h --max-depth=1 /var | sort -h12. How do you read the output of free -h?
free shows total, used and free RAM, plus buff/cache, which is memory the kernel uses to cache files, and available, its estimate of how much memory new programs can get without swapping. Linux fills spare RAM with cache on purpose, so a small free figure is normal; the cache is handed back when programs need memory. Watch available instead. Worry when available stays low and swap use keeps growing. A common fresher mistake is reporting a memory leak because the free column is close to zero.
free -h
grep -E 'MemTotal|MemAvailable' /proc/meminfo13. Where do you look for logs on a Linux server?
On systemd systems the journal collects service output and kernel messages: journalctl -u name shows one service, --since limits the time, -p err keeps errors and worse, -b limits it to the current boot and -f follows new lines. Classic text logs are in /var/log: syslog and auth.log on Ubuntu and Debian, messages and secure on RHEL-family systems, plus folders for applications such as nginx. Start from the time the problem began and read the lines just before the first error, not only the last one.
journalctl -u nginx --since "1 hour ago"
journalctl -p err -b # errors since this boot
journalctl -f # follow new entries
tail -f /var/log/nginx/error.log
sudo tail -n 50 /var/log/auth.log # Ubuntu14. Which grep options do you use most, and what do they do?
-i ignores case, -n prints line numbers, -c counts matching lines, -v inverts the match, -r searches a folder recursively, -l prints only file names and -E enables extended patterns such as ERROR|WARN. In the run, -c finds 2 ERROR lines, -in finds both timeout and Timeout with their line numbers, and -v INFO leaves 3 lines. -A 3 and -B 3 print lines after and before each match, which is how you see what led up to an error. Quote the pattern so the shell does not expand characters such as * first.
cat app.log
# 10:01:02 INFO payment started id=101
# 10:01:05 ERROR payment timeout id=101
# 10:02:11 INFO payment started id=102
# 10:02:15 WARN slow response id=102
# 10:03:40 ERROR Timeout calling bank id=103
grep -c ERROR app.log # 2
grep -in 'timeout' app.log
# 2:10:01:05 ERROR payment timeout id=101
# 5:10:03:40 ERROR Timeout calling bank id=103
grep -v INFO app.log | wc -l # 315. Why do you need sort before uniq?
uniq only removes or counts duplicates that sit on adjacent lines. In the run, uniq -c on the unsorted list counts pune in two separate groups; after sort the equal lines are together and the counts are right: delhi 2 and pune 3. The chain sort, uniq -c, sort -rn, head is the standard way to find the most frequent value, such as the client IP with the most requests in an access log. sort -u gives the unique lines in one step when you do not need the counts.
printf 'pune\ndelhi\npune\npune\ndelhi\n' > cities.txt
uniq -c cities.txt
# 1 pune
# 1 delhi
# 2 pune
# 1 delhi
sort cities.txt | uniq -c
# 2 delhi
# 3 pune
sort cities.txt | uniq -c | sort -rn | head -1
# 3 pune16. How do cut and awk pick columns out of a file?
cut is the simple tool: -d sets the delimiter and -f picks fields, so cut -d, -f2,3 prints the name and city from this CSV. awk splits each line into $1, $2 and so on, with -F setting the separator, and it can also filter and calculate. The awk command prints people in Pune with 5 or more years of experience: Asha and Meera. Use cut for quick extraction and awk when you need conditions or sums. Neither tool understands quoted CSV fields that contain commas; use a proper CSV parser for those files.
cat staff.csv
# 101,Asha,Pune,6
# 102,Ravi,Chennai,2
# 103,Meera,Pune,9
cut -d, -f2,3 staff.csv
# Asha,Pune
# Ravi,Chennai
# Meera,Pune
awk -F, '$3 == "Pune" && $4 >= 5 {print $2}' staff.csv
# Asha
# Meera17. What do $0, $1, $#, $@ and $? mean in a bash script?
$0 is the script name as it was called, $1 to $9 are the positional arguments, $# is the number of arguments, and "$@" expands to every argument as a separate word. $? is the exit status of the last command: 0 for success, anything else for failure. In the run, Ravi Kumar stays one argument because the loop uses "$@" in double quotes; unquoted, it would split into two. The script ends with exit 3, so $? prints 3. The first line, the shebang, names the interpreter, and chmod +x makes the file executable.
#!/bin/bash
# greet.sh
echo "script: $0"
echo "count: $#"
echo "first: $1"
for name in "$@"; do
echo "hello, $name"
done
exit 3
# chmod +x greet.sh; ./greet.sh Asha "Ravi Kumar"; echo "status: $?"
# script: ./greet.sh
# count: 2
# first: Asha
# hello, Asha
# hello, Ravi Kumar
# status: 318. What do >, >>, 2> and 2>&1 do?
> sends standard output (stream 1) to a file and replaces its contents, >> appends, and 2> sends standard error (stream 2) somewhere else. 2>&1 means send stream 2 to wherever stream 1 points at that moment, so the order matters. With > all.txt 2>&1 both streams land in the file, 2 lines in the run. With 2>&1 > only.txt the errors were pointed at the terminal before stdout moved, so only.txt gets 1 line and the error still shows on screen. A pipe passes only stdout to the next command.
ls notes.txt missing.txt > out.txt 2> err.txt
ls notes.txt missing.txt > all.txt 2>&1
wc -l < all.txt # 2
ls notes.txt missing.txt 2>&1 > only.txt
# ls: cannot access 'missing.txt': No such file or directory
wc -l < only.txt # 1
./job.sh >> job.log 2>&1 # append both streams19. How do you read a crontab line such as 30 2 * * *?
The five fields are minute, hour, day of month, month and day of week, followed by the command. 30 2 * * * runs at 2:30 every night, */15 in the minute field means every 15 minutes, and 0 9 * * 1-5 runs at 9:00 Monday to Friday (0 or 7 is Sunday). The time is the server's time zone, so on a server set to UTC a 2:30 job runs at 8:00 in India. Edit with crontab -e rather than touching the spool file, and use full paths in the command, because cron runs jobs with a very small environment.
# min hour day-of-month month day-of-week command
30 2 * * * /opt/scripts/backup.sh
*/15 * * * * /opt/scripts/health.sh
0 9 * * 1-5 /opt/scripts/report.sh
crontab -e # edit your jobs
crontab -l # list them20. How do you set up key-based SSH login to a server?
ssh-keygen creates a key pair: the private key stays on your laptop, protected by a passphrase, and the .pub file is the public key. ssh-copy-id appends the public key to ~/.ssh/authorized_keys on the server, and from then on you log in without the account password. sshd refuses a key when the home folder, ~/.ssh or authorized_keys can be written by other users, the usual reason key login falls back to a password prompt. Ed25519 keys are the common choice today. Never copy a private key to a server or paste it in chat.
ssh-keygen -t ed25519 -C "asha@laptop"
ssh-copy-id deploy@10.0.0.20
ssh deploy@10.0.0.20
# on the server
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keysReady to check yourself? Take the free Linux mock test
Linux interview questions: Medium questions (1-3 years)
20 questions21. How do you find and delete log files older than 30 days, or files larger than 100 MB?
-mtime +30 matches files modified more than 30 whole days ago (find drops the part of a day, so a file must be at least 31 days old), -size +100M matches files larger than 100 MiB, and -type f limits the search to regular files. Always run find without the action first and read the list, then add -exec rm {} + or -delete. The + form passes many names to one rm, which is far faster than \; that starts rm once per file. Quote the pattern, or the shell may expand *.log in the current folder before find sees it.
find logs -name '*.log' -mtime +30
# logs/app-old.log
find logs -type f -size +100M
# logs/dump.bin
find logs -name '*.log' -mtime +30 -exec rm {} +
ls logs
# app-new.log
# dump.bin22. What do read, write and execute mean on a directory?
On a directory, r lets you list the names inside, x lets you enter it and reach files through it, and w, together with x, lets you create, rename and delete entries. The run shows the split: with r but no x you see the name a.txt but cannot open it; with x but no r you cannot list the folder, yet you can open a file whose name you know. Deleting a file depends on write permission on the directory, not on the file, so the read-only a.txt in a writable folder was removed. The sticky bit changes that rule.
mkdir shared; echo data > shared/a.txt
chmod 600 shared
ls shared # a.txt
cat shared/a.txt # Permission denied
chmod 300 shared
ls shared
# ls: cannot open directory 'shared': Permission denied
cat shared/a.txt # data
chmod 755 shared; chmod 444 shared/a.txt
rm -f shared/a.txt && echo "deleted a read-only file"23. What are the setuid, setgid and sticky bits?
They are the extra leading digit in chmod. Setuid (4) on a program makes it run with the file owner's rights: passwd is setuid root so normal users can update /etc/shadow, shown as s in the owner's execute slot. Setgid (2) on a directory makes new files inherit the directory's group, which suits a shared team folder. The sticky bit (1) on a directory, as on /tmp, lets users delete only their own files although everyone can write there. Linux ignores setuid on scripts. Treat every setuid-root binary as a risk and list them with find / -perm -4000.
stat -c '%A %a %n' /tmp /usr/bin/passwd
# drwxrwxrwt 1777 /tmp
# -rwsr-xr-x 4755 /usr/bin/passwd
mkdir team drop
chmod 2775 team # drwxrwsr-x
chmod 1777 drop # drwxrwxrwt24. You added Ravi to the docker group, but he still gets permission denied. Why?
Group membership is read when a session starts, so a login that was already open keeps its old group list. id ravi reads the account database and shows the new group, while plain id in Ravi's open terminal does not. He needs to log out and back in, or run newgrp docker for one shell. Also check the command used: usermod -G without -a replaces all his supplementary groups with the new list, which can silently remove him from sudo. Remember that membership of the docker group is effectively root access, so grant it with care.
sudo usermod -aG docker ravi # -a = append
id ravi # account database: docker is listed
id # in Ravi's old session: docker missing
newgrp docker # new shell with the group, or log in again25. What are zombie and orphan processes, and how do you get rid of a zombie?
A zombie is a process that has finished but whose parent has not yet collected its exit status with wait(). It uses no CPU or memory, only a slot in the process table, and shows state Z in ps. You cannot kill a zombie, because it is already dead; you fix the parent. Send the parent SIGCHLD, restart it, or stop it, after which init (PID 1) adopts the zombies and reaps them. An orphan is a running process whose parent exited; it is adopted the same way and keeps running. A growing number of zombies means a bug in the parent.
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/'
ps -o pid,cmd -p 2140 # look at the parent (PPID)
kill -s SIGCHLD 2140 # ask it to reap its children26. Your long script stops when your SSH session drops. How do you keep it running?
When the terminal closes, the shell gets SIGHUP and passes it on to its jobs, which then exit. nohup starts a command with SIGHUP ignored; pair it with a redirect so the output goes to a file. disown -h tells bash not to send SIGHUP to a job you already started. For work you want to watch again, run it inside tmux or screen and reattach after you reconnect. Anything that must survive reboots or restart after a failure belongs in a systemd service; systemd-run gives a one-off job the same supervision without writing a unit file.
nohup ./import.sh > import.log 2>&1 &
jobs
disown -h %1
tmux new -s import # detach: Ctrl-b then d
tmux attach -t import
sudo systemd-run --unit=import /opt/scripts/import.sh27. How do you run a Node.js or Python app as a systemd service?
Write a unit file in /etc/systemd/system. User= runs the app as an unprivileged account, WorkingDirectory= sets its folder, ExecStart= gives the full command with an absolute path, and Restart=on-failure with RestartSec=5 brings it back five seconds after a crash. WantedBy=multi-user.target is what enable hooks into at boot. After creating or editing the file, run systemctl daemon-reload, then enable --now. Unit files are normally world-readable, so keep secrets in a root-only file loaded with EnvironmentFile=. systemd-analyze verify catches typos.
# /etc/systemd/system/order-api.service
[Unit]
Description=Order API (Node.js)
After=network-online.target
Wants=network-online.target
[Service]
User=orderapi
WorkingDirectory=/srv/order-api
EnvironmentFile=/etc/order-api.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
# sudo systemctl daemon-reload
# sudo systemctl enable --now order-api28. A service fails to start after a deploy. How do you debug it?
Start with systemctl status, which shows the state, the exit code and the last log lines, then read the unit's full journal for this boot. The exit status often names the cause: 203/EXEC means the ExecStart program could not be run (wrong path or not executable), 200/CHDIR means a bad WorkingDirectory, and 217/USER means the User= account could not be set up, usually because it does not exist. systemctl cat shows the unit plus its drop-in overrides. Finally run the same command by hand as the service user to see errors like a missing variable.
systemctl status order-api
journalctl -u order-api -b -n 50 --no-pager
systemctl cat order-api # unit + drop-ins in use
sudo -u orderapi env -C /srv/order-api /usr/bin/node server.js29. How do you find which package owns a file, or which version of a package is installed?
On Debian and Ubuntu, dpkg -S maps a file to its package and dpkg -L lists a package's files. apt-cache policy shows the installed version, the candidate apt would install and the repository it comes from, which is how you debug a wrong version or an extra repository taking priority. apt-mark hold stops a package from being upgraded until you unhold it. On RHEL, rpm -qf and dnf provides do the same lookups. A trap on Ubuntu 24.04: dpkg -S /bin/ls finds nothing, because /bin is a link to /usr/bin and dpkg records /usr/bin/ls.
dpkg -S /usr/bin/ssh # openssh-client: /usr/bin/ssh
dpkg -S /usr/bin/ls # coreutils: /usr/bin/ls
dpkg -S /bin/ls
# dpkg-query: no path found matching pattern /bin/ls
dpkg -L openssh-client # files in the package
apt-cache policy nginx # installed vs candidate
sudo apt-mark hold nginx # freeze the version
rpm -qf /usr/bin/ssh # RHEL family30. curl to your API says Connection refused from another server but works on the server itself. What do you check?
Connection refused means a TCP reset came back: the host was reached, but nothing accepted a connection on that address and port (a firewall rule set to reject can also do this). The usual cause is an app bound to 127.0.0.1, which only accepts local clients; ss -tlnp then shows 127.0.0.1:8080 instead of 0.0.0.0:8080 or the server's own IP, and the fix is the app's host or bind setting. A timeout is different: packets are being dropped, usually by a firewall, a cloud security group or a routing problem. curl -v shows where it fails.
ss -tlnp | grep 8080 # which address is it bound to?
curl -v http://10.0.0.15:8080/health
sudo ufw status # Ubuntu firewall
sudo nft list ruleset # nftables rules31. How do you troubleshoot a DNS problem on a Linux server?
First see what the system resolver returns with getent hosts, which follows /etc/nsswitch.conf and so includes /etc/hosts. dig asks a DNS server directly and ignores /etc/hosts, so if getent and dig disagree, look for a stale hosts entry. Compare the answer from your configured server with another server using dig @server. On Ubuntu, /etc/resolv.conf usually points at the local systemd-resolved stub, 127.0.0.53, and resolvectl status shows the real upstream servers. Answers are cached, so a changed record can take up to its TTL to show.
getent hosts api.internal.example
dig +short api.internal.example
dig @10.0.0.2 +short api.internal.example
cat /etc/resolv.conf
resolvectl status # systemd-resolved systems32. A server's disk is 100% full. How do you find and free space safely?
Confirm which file system is full with df -h, then check df -i too, because a disk can also run out of inodes when an app writes millions of tiny files, such as session or cache files. Use du -x, which stays on one file system, from the mount point down to find the largest folders. Usual culprits are logs in /var/log, the systemd journal, old release folders, core dumps and package caches. Prefer tools that clean safely, such as journalctl --vacuum-size and apt clean, over rm. Never rm a log that a process is writing; truncate it instead.
df -h /
df -i / # inodes, not bytes
sudo du -xh --max-depth=1 / | sort -h | tail
journalctl --disk-usage
sudo journalctl --vacuum-size=500M
sudo apt clean33. How does logrotate work, and what is the difference between copytruncate and a postrotate reload?
logrotate normally runs once a day from a systemd timer or cron and reads rules from /etc/logrotate.d. The example keeps 14 daily files, compresses old ones but leaves the newest rotated file plain for a day (delaycompress) and skips missing or empty logs. After a rename-style rotation the app keeps writing to the renamed file through its open handle, so it must reopen its logs, usually via a postrotate script that sends a reload. copytruncate avoids that by copying and then emptying the file, but lines written during the copy can be lost.
# /etc/logrotate.d/order-api
/var/log/order-api/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
sudo logrotate -d /etc/logrotate.d/order-api # dry run34. How do you change a value in a config file with sed, safely?
sed -i edits the file in place; giving it a suffix, as in -i.bak, keeps the original as app.conf.bak so you can roll back. Anchor the pattern (^DB_HOST=) so you change only the line you mean, not every line containing that text. When the text has slashes, such as paths, use another delimiter like | instead of escaping each slash. Run the command without -i first to preview the result. Note that -i without a suffix is GNU sed behaviour; BSD and macOS sed need an explicit, possibly empty, suffix argument, which breaks scripts moved between them.
cat app.conf
# DB_HOST=localhost
# DB_PORT=3306
# LOG_DIR=/var/www/old/logs
sed -i.bak 's/^DB_HOST=.*/DB_HOST=10.0.0.5/' app.conf
head -1 app.conf # DB_HOST=10.0.0.5
ls app.conf*
# app.conf
# app.conf.bak
sed 's|/var/www/old|/srv/app|' app.conf | tail -1
# LOG_DIR=/srv/app/logs35. How do you use awk to total a column or count errors per IP in an access log?
awk runs its action for every line, with the fields in $1, $2 and so on, and an END block runs once after the last line. The first command adds field 5, the bytes, into sum and prints 1984. The second uses a condition, status 500 or above, and an associative array keyed by IP, so it counts server errors per client: 10.1.1.7 has 2. Variables start as zero or empty, so no initialisation is needed. Note that for (k in array) returns keys in no fixed order; pipe the output to sort when order matters.
cat access.log
# 10.1.1.5 GET /api/orders 200 512
# 10.1.1.7 GET /api/users 500 128
# 10.1.1.5 POST /api/orders 200 1024
# 10.1.1.9 GET /health 200 64
# 10.1.1.7 GET /api/orders 500 256
awk '{ sum += $5 } END { print "total bytes:", sum }' access.log
# total bytes: 1984
awk '$4 >= 500 { n[$1]++ }
END { for (ip in n) print ip, n[ip] }' access.log
# 10.1.1.7 236. Why does find piped to xargs break on some file names, and how do you fix it?
By default xargs splits its input on spaces and newlines, so a file named march report.txt arrives as two arguments, ./march and report.txt, and the command fails or, worse, acts on the wrong files. The fix is to separate names with a null byte, which cannot appear in a file name: find -print0 with xargs -0. Simpler still, let find run the command itself with -exec ... {} +, which passes names safely. The same splitting problem appears in loops written as for f in $(ls), which should be replaced with a glob such as for f in *.txt.
touch 'march report.txt' april.txt
find . -name '*.txt' | xargs ls
# ls: cannot access './march': No such file or directory
# ls: cannot access 'report.txt': No such file or directory
find . -name '*.txt' -print0 | xargs -0 ls # works
find . -name '*.txt' -exec ls {} + # works37. Why should you quote variables in bash, and when do you use [[ ]] instead of [ ]?
An unquoted variable goes through word splitting and filename expansion, so a value with a space becomes two arguments and a value with * can turn into a list of files. That is why ls $file looks for my and notes.txt. Write "$file" unless you really want splitting. [[ ]] is a bash keyword: variables inside it are not split, == matches glob patterns such as A*, =~ matches regular expressions, and && and || work inside it. [ ] is the portable POSIX test command, so use it in /bin/sh scripts and quote every variable there.
file="my notes.txt"
touch "$file"
ls $file
# ls: cannot access 'my': No such file or directory
# ls: cannot access 'notes.txt': No such file or directory
ls "$file" # my notes.txt
name="Asha"
[[ $name == A* ]] && echo "starts with A" # starts with A38. What does set -euo pipefail do, and what are its limits?
-e exits the script when a command fails, -u treats an unset variable as an error instead of an empty string, and pipefail makes a pipeline fail when any part fails, not only the last one. Together they stop a script from carrying on after a silent failure. But -e has gaps: it is ignored for commands in an if or while test and in && or || lists, and that covers everything inside a function called from such a test, so check() keeps running after false. Do not rely on -e alone; check important commands explicitly.
false | true; echo "without pipefail: $?" # 0
set -o pipefail
false | true; echo "with pipefail: $?" # 1
bash -c 'set -u; echo "user is $APP_USR"'
# bash: line 1: APP_USR: unbound variable
bash -c 'set -e; check() { false; echo "still running"; }
if check; then echo "if branch"; fi'
# still running
# if branch39. A script works when you run it by hand but fails from cron. What are the usual causes?
Cron runs jobs with a minimal environment: a short PATH, no .bashrc or .profile, /bin/sh as the shell unless SHELL is set, and the home folder as the working directory. So commands your login shell finds, relative paths and bash-only syntax can all break. Set PATH at the top of the crontab or use full paths, cd to the right folder, and redirect output to a log so errors are not lost. Escape % as \%, because cron turns an unescaped % into a newline. Also check the system log for the CRON lines that show the job really ran.
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
30 2 * * * cd /opt/app && ./backup.sh >> /var/log/backup.log 2>&1
0 1 * * * tar -czf /backup/site-$(date +\%F).tgz /srv/site40. How do you use ~/.ssh/config, a jump host and SSH port forwarding?
The config file gives servers short names with their address, user, port and key, so ssh app1 replaces a long command line. ProxyJump connects through a bastion host to a private server in one step, and your private key never has to be copied to the bastion. Local forwarding with -L opens a port on your laptop that tunnels through SSH, so a MySQL client on localhost port 3307 reaches a database that is not exposed to the internet; -N skips opening a shell. Avoid agent forwarding to hosts you do not trust, because root there can use your agent.
# ~/.ssh/config
Host bastion
HostName 203.0.113.10
User asha
IdentityFile ~/.ssh/id_ed25519
Host app1
HostName 10.0.1.21
User deploy
ProxyJump bastion
# ssh app1
# ssh -N -L 3307:10.0.1.30:3306 bastionReady to check yourself? Take the free Linux mock test
Linux interview questions: Hard questions (experienced)
20 questions41. A production server is slow. How do you investigate it, step by step?
Work from the whole system down to one process, checking CPU, memory, disk and network in turn. uptime gives the load trend and top shows which processes use CPU and memory. vmstat 1 adds the run queue (r), tasks blocked on I/O (b), swap in and out (si, so) and CPU time waiting for I/O (wa). iostat -x shows per-disk latency and utilisation. ss -s and the app's own logs show connection pile-ups, and the kernel log shows OOM kills and disk errors. Note what you see before restarting anything, because a restart destroys the evidence.
uptime # load averages
top -o %CPU # or htop
vmstat 1 5 # r, b, si/so, wa
iostat -xz 1 3 # disk latency (sysstat package)
free -h
ss -s # socket summary
sudo dmesg -T | tail # OOM kills, disk errors42. What does load average really measure on Linux, and when is a high value a problem?
It is the average number of tasks that are running or waiting to run (state R) plus those in uninterruptible sleep (state D, usually waiting for disk or a network file system), over 1, 5 and 15 minutes. Read it against the CPU count from nproc: a load of 8 means fully busy on 8 cores but a long queue on 2. Because D-state tasks count, a high load with idle CPUs points at slow storage or a hung NFS mount, not at CPU. Compare the three numbers for the trend; a 1-minute value above the 15-minute value means it is getting worse.
cat /proc/loadavg # 1, 5, 15 min, running/total, last PID
nproc # number of CPUs
ps -eo stat,pid,cmd | awk '$1 ~ /^D/' # tasks in D state43. df says the disk is full but du finds far less. What is happening, and how do you fix it without a reboot?
A file deleted while a process still has it open keeps its blocks until the last handle closes, so df counts it but du, which walks file names, cannot see it. This often happens when someone deletes a large log with rm instead of rotating it. lsof +L1 lists open files whose link count is zero, with the PID and file descriptor. Restarting the process frees the space. If you cannot restart it, truncate the file through /proc/PID/fd/N; in a sandbox test a 20 MB deleted file dropped to 0 bytes. Rotate or truncate logs, never rm them.
sudo lsof -nP +L1 # open but deleted files
ls -l /proc/2140/fd | grep deleted # links ending in (deleted)
: > /proc/2140/fd/3 # truncate: space is freed
sudo systemctl restart order-api # or restart the owner44. A Java or Node process keeps disappearing at night. How do you prove the OOM killer did it, and what can you change?
When memory runs out, the kernel OOM killer picks a process with a high badness score and sends it SIGKILL, so the app logs nothing itself. The kernel log does: look for Out of memory and Killed process lines with the PID and name. Fixes in order: find the growth (a leak, an unbounded cache, a heap such as -Xmx set close to total RAM), cap the service with MemoryMax= so it cannot starve the host, and add RAM or swap if the load really needs it. oom_score_adj from -1000 to 1000 changes who is picked; -1000 exempts a process.
journalctl -k | grep -i -E 'out of memory|killed process'
cat /proc/2140/oom_score_adj
# drop-in for the service
[Service]
MemoryMax=2G
OOMScoreAdjust=-50045. What happens at the system-call level when you type ls in bash? Why is cd a builtin?
The shell finds ls in PATH, then calls fork (clone on Linux) to create a child that is a copy of the shell. The child sets up any redirections and calls execve to replace itself with /usr/bin/ls, while the parent waits for it with wait4 and stores the exit status in $?. The strace run shows exactly that sequence. cd must be a builtin because the current directory belongs to each process: a child that changed directory would change only its own, and the shell would stay where it was. export, source and exit are builtins for the same reason.
type cd ls echo
# cd is a shell builtin
# ls is /usr/bin/ls
# echo is a shell builtin
strace -f -e trace=clone,execve,wait4 \
bash -c 'ls >/dev/null; true'
# bash: clone(...), child: execve("/usr/bin/ls", ...),
# bash: wait4(...)46. How do you make a bash script shut down cleanly when systemd or Docker sends SIGTERM?
Trap TERM and do the cleanup in an EXIT trap so it runs on every exit path. Two details matter. Bash runs a trap only after the current foreground command finishes, so a script in a long foreground command ignores SIGTERM until it ends; in a test with a foreground sleep 3 the trap fired after 3 seconds. So run the work in the background and wait, because wait returns as soon as a trapped signal arrives. Then kill the child in cleanup; without that line the test left the sleep running. systemd sends SIGKILL after TimeoutStopSec.
#!/bin/bash
tmp=$(mktemp)
child=""
cleanup() {
[ -n "$child" ] && kill "$child" 2>/dev/null
rm -f "$tmp"
echo "cleaned up"
}
trap cleanup EXIT
trap 'echo "got TERM"; exit 143' TERM
sleep 30 & # stands in for the real work
child=$!
wait "$child"
# kill -TERM <pid> -> got TERM, cleaned up, status 14347. How do you limit a service's memory, CPU and number of processes with systemd?
systemd puts each service in its own cgroup, so these limits cover the whole service, children included. MemoryHigh= throttles and reclaims above a soft level, MemoryMax= is the hard cap where the OOM killer acts inside that unit only, CPUQuota=150% allows one and a half CPUs, and TasksMax= stops a fork bomb or thread leak. LimitNOFILE= is a per-process limit, not a cgroup one. Put them in a drop-in with systemctl edit so package updates do not overwrite them. The memory settings use cgroup v2; the old MemoryLimit= is deprecated.
sudo systemctl edit order-api # opens a drop-in
[Service]
MemoryHigh=1536M
MemoryMax=2G
CPUQuota=150%
TasksMax=512
LimitNOFILE=65536
systemctl show order-api -p MemoryMax -p CPUQuotaPerSecUSec
systemd-cgtop # live use per cgroup48. When would you use a systemd timer instead of cron?
A timer starts a matching .service unit, so the job gets everything a service has: logs in the journal, resource limits, a clean environment, dependencies and a failed state you can alert on. Persistent=true runs a missed job after the machine was off, RandomizedDelaySec= spreads load across a fleet, and a still-running service is not started a second time. systemd-analyze calendar checks an expression before you deploy it, as shown. Cron is still simpler for a one-line job and everyone knows it; the cost of a timer is two unit files.
# /etc/systemd/system/report.timer
[Timer]
OnCalendar=Mon..Fri 02:30
Persistent=true
RandomizedDelaySec=5m
[Install]
WantedBy=timers.target
systemd-analyze calendar 'Mon..Fri 02:30'
# Normalized form: Mon..Fri *-*-* 02:30:00
systemctl list-timers49. A file has mode 077 and you are its owner. Can you read it? How does Linux decide?
No. The kernel picks exactly one class and checks only its bits: if you own the file, the owner bits decide; otherwise, if you are in the file's group, the group bits decide; otherwise the other bits. It never falls through to a more generous class, so the owner of a 077 file is refused although everyone else may read it, as the run shows. Root bypasses read and write checks through capabilities such as CAP_DAC_OVERRIDE. The same rule lets a mode like 705 shut out one group while others can read, and it explains odd denials in audits.
echo secret > plan.txt
chmod 077 plan.txt
stat -c '%A %a' plan.txt # ----rwxrwx 77
cat plan.txt
# cat: plan.txt: Permission denied
chmod 604 plan.txt
cat plan.txt # secret50. How can a service listen on port 443 without running as root?
Ports below 1024 need only the CAP_NET_BIND_SERVICE capability, not full root. Under systemd, AmbientCapabilities= gives just that capability to a service running as a normal user, and CapabilityBoundingSet= removes the rest. setcap stores it on a single binary instead; never set it on an interpreter such as python3, because every script run with it would get the right. Other options are keeping the app on a high port behind nginx or a load balancer, or lowering net.ipv4.ip_unprivileged_port_start on Linux 4.11 and later.
# in the unit file
[Service]
User=webapp
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# or on one binary
sudo setcap 'cap_net_bind_service=+ep' /opt/webapp/server
getcap /opt/webapp/server51. ss shows thousands of connections in TIME_WAIT or CLOSE_WAIT. What does each mean, and what do you do?
TIME_WAIT is normal: the side that closes a TCP connection first keeps it for 60 seconds on Linux to absorb late packets. Large numbers appear on a client that opens a new connection per request, and they can use up local ports towards one backend; the real fix is keep-alive or connection pooling. CLOSE_WAIT is different: the other side has closed, but your application never called close() on its socket, so it is almost always an app bug, such as a leaked HTTP or database connection. ss -p shows which process owns them.
ss -s # summary by state
ss -tan state time-wait | wc -l
sudo ss -tanp state close-wait # which process?
sysctl net.ipv4.ip_local_port_range52. An app fails with Too many open files. How do you diagnose and fix it?
Every file, socket and pipe uses a file descriptor, and each process has a soft limit on how many it may hold. Compare the count in /proc/PID/fd with the Max open files line in /proc/PID/limits; grouping lsof output by type shows whether sockets or files are piling up. If the count keeps rising under steady traffic it is a leak, and raising the limit only delays the crash. For a real need, raise it where the process starts: LimitNOFILE= for a systemd service, because /etc/security/limits.conf applies only to PAM login sessions.
ls /proc/2140/fd | wc -l # descriptors in use
grep 'open files' /proc/2140/limits
sudo lsof -p 2140 | awk '{print $5}' | sort | uniq -c
# drop-in for the service
[Service]
LimitNOFILE=6553653. How do you analyse a 20 GB log file quickly without hurting the live server?
Stream the file instead of loading it: grep, awk and sort read line by line, and one awk pass that filters and counts beats a chain of cat, grep and cut. LC_ALL=C makes grep and sort compare bytes instead of applying locale rules, which is often much faster on ASCII logs. zgrep reads rotated .gz files without unpacking them. Give sort a bigger buffer with -S and a temp folder with -T on a disk with free space, since it spills to disk. Run the job under nice and ionice so the live app keeps priority, or copy the file elsewhere first.
LC_ALL=C grep -c '" 500 ' access.log
zgrep -h '" 500 ' access.log.*.gz | wc -l
# combined log format: $7 = URL, $9 = status
awk '$9 >= 500 { n[$7]++ }
END { for (u in n) print n[u], u }' access.log |
sort -rn | head
sort -S 1G --parallel=4 -T /data/tmp big.txt
nice -n 19 ionice -c3 ./analyse.sh54. How do you join two files on a key column with awk?
NR counts lines across all input files and FNR restarts at 1 for each file, so NR == FNR is true only while awk reads the first file. That block loads dept.txt into an array keyed by employee ID, and next skips the rest of the program. For the second file each line looks up its ID; the in operator tests for a key without creating it, so Ravi, who has no department, prints none. This is a left join in memory in one pass, with no sorting, unlike the join command. Trap: if the first file is empty, NR == FNR stays true for the second.
# dept.txt: 101 Payments, 103 Support
# emp.txt: 101 Asha, 102 Ravi, 103 Meera
awk 'NR == FNR { dept[$1] = $2; next }
{ print $2, ($1 in dept ? dept[$1] : "none") }' dept.txt emp.txt
# Asha Payments
# Ravi none
# Meera Support55. What makes a bash script safe to run on production servers?
Fail early and clearly: strict mode, required arguments checked with the :? expansion, and distinct exit codes (the run returned 2 for a missing folder and 1 for no arguments). Quote every variable, since paths contain spaces; the test folder was named site data. Build the output in a mktemp folder that an EXIT trap removes, then mv the finished file into place, so a crash never leaves a half-written backup. Send timestamped logs to stderr. Make re-runs harmless with mkdir -p and dated names. Run shellcheck in CI, and do not trust strict mode alone.
#!/usr/bin/env bash
set -euo pipefail
log() { printf '%s %s\n' "$(date +%F_%T)" "$*" >&2; }
src=${1:?usage: backup.sh <dir> <dest>}
dest=${2:?usage: backup.sh <dir> <dest>}
[ -d "$src" ] || { log "no such dir: $src"; exit 2; }
tmp=$(mktemp -d)
trap 'rm -rf -- "$tmp"' EXIT
name="$(basename -- "$src")-$(date +%F).tar.gz"
tar -czf "$tmp/$name" -C "$src" .
mkdir -p -- "$dest"
mv -f -- "$tmp/$name" "$dest/$name"
log "saved $dest/$name"56. Why does this counter print 0, and how do you fix it?
Each part of a pipeline runs in its own subshell, a separate process, so the while loop increments its own copy of count and the change is lost when the loop ends; the parent still has 0. Feed the loop without a pipe instead: redirect a file with done < file, or use process substitution as in the second loop, which prints 3. From bash 4.2, shopt -s lastpipe runs the last part of a pipeline in the current shell, but only when job control is off, as it is in scripts. Variables set inside $( ) or ( ) are lost the same way.
count=0
printf 'a\nb\nc\n' | while read -r line; do
count=$((count + 1))
done
echo "after pipe: $count" # after pipe: 0
count=0
while read -r line; do
count=$((count + 1))
done < <(printf 'a\nb\nc\n')
echo "after process substitution: $count" # 357. Your 5-minute cron job sometimes runs longer than 5 minutes. How do you stop overlapping runs?
flock takes a lock on a file before running the command; with -n it gives up at once if another run holds the lock, so a slow run just makes the next one skip instead of two copies fighting over the same data. The kernel releases the lock when the process exits, even after a crash, unlike a hand-made PID file that can go stale. The test shows the second run skipped with exit 1. Also log each run's start, end and exit status, and alert when a job has not succeeded recently, because a skipped or silently failing job is otherwise invisible.
*/5 * * * * flock -n /var/lock/sync.lock /opt/app/sync.sh
# test: a second run while the first holds the lock
flock -n job.lock -c 'sleep 2' &
sleep 0.5
flock -n job.lock -c 'echo second run' || echo "skipped, exit $?"
# skipped, exit 158. How do you harden SSH on a new server without locking yourself out?
Allow keys only, disable root login and passwords, and allow only named users. Before reloading, confirm your key login works, keep the current session open, and run sshd -t to check the syntax. Then read the effective value with sshd -T: sshd keeps the first value it reads for each keyword, and Ubuntu's sshd_config includes sshd_config.d/*.conf at the top, so a file that sorts before yours and sets PasswordAuthentication yes wins. Test a new login from a second terminal before closing the first, and keep OpenSSH patched.
# /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers deploy asha
sudo sshd -t # syntax check
sudo sshd -T | grep -i passwordauthentication
sudo systemctl reload ssh # sshd on RHEL59. Why is writing a temp file and then using mv the safe way to replace a config file?
mv within one file system is a rename: it only changes directory entries, and the rename system call swaps the name atomically. A reader opening app.conf sees either the whole old file or the whole new one, never a mix, and a process that already had the old file open keeps reading the old inode. Writing in place with > truncates first, so a crash or an unlucky reader sees an empty or partial file. Across file systems mv falls back to copy and delete, which is not atomic, so create the temp file in the same folder. The run confirms the inode swap.
echo v1 > app.conf
echo v2 > app.conf.tmp
t=$(stat -c %i app.conf.tmp)
mv -f app.conf.tmp app.conf
[ "$(stat -c %i app.conf)" = "$t" ] && echo "same inode"
# same inode
cat app.conf # v260. How do you give a deploy user limited sudo rights safely?
Grant exact commands with full paths instead of ALL, and edit with visudo, which checks the syntax before saving; a broken sudoers file can lock everyone out of sudo. Files in /etc/sudoers.d are skipped when their name contains a dot or ends in ~, so a file called deploy.conf would be silently ignored. Do not grant editors, pagers, find, tar or scripts the user can edit, because they can start a root shell; use sudoedit for editing files. Check the result with sudo -l -U deploy, and remember that sudo logs every command it runs.
sudo visudo -f /etc/sudoers.d/deploy
# file content:
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart order-api
sudo -l -U deploy # what may deploy run?Practice questions for November 2026
New every month3 questions from the Linux mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
Which command prints the total space used by everything under /var/log as one line?
- stat /var/log
- du -sh /var/log
- df -h /var/log
- ls -lh /var/log
Show the answer
Answer: du -sh /var/log. du -s gives one summary total for the path instead of a line per subfolder, and -h makes it readable. df /var/log reports the whole file system that holds the folder, and ls -l shows only file sizes, not a total.
-
A service has Restart=on-failure. Its process exits with status 0. What does systemd do?
- Marks it failed and disables it for the next boot
- Restarts it after RestartSec, as for any exit code
- Nothing, because a clean exit with 0 does not count as a failure
- Restarts it at once, because the process stopped
Show the answer
Answer: Nothing, because a clean exit with 0 does not count as a failure. on-failure restarts after a non-zero exit, a kill by an unclean signal, a timeout or a watchdog miss. A clean exit with 0 is treated as done. Restart=always would restart it on a clean exit too.
-
What does this print on ext4?
mkdir -p top/a top/b top/c stat -c %h top- 4
- 2
- 3
- 5
Show the answer
Answer: 5. A directory has one link from its parent entry and one from its own . entry, and each subdirectory adds one more through its .. entry. top has three subdirectories, so its link count is 2 + 3 = 5.
Ready to apply?
There are 1,886 open jobs asking for Linux on CoreHRx right now. Create a free profile once and apply with it.