Hệ điều hànhOperating Systems
Thuộc bộ kiến thức DevOps Roadmap.
Tổng quan
Hệ điều hành là tầng nền mà mọi DevOps engineer đứng trên đó. Container thực chất là process Linux với namespace và cgroup; node Kubernetes là máy Linux; CI runner, database, load balancer đều sống hay chết theo hành vi ở tầng OS — áp lực bộ nhớ, giới hạn file descriptor, quyết định của scheduler, disk I/O. Khi một ứng dụng chết “một cách ngẫu nhiên”, câu trả lời thường nằm ở OS: OOM killer, ổ đĩa đầy, hết inode, hoặc một systemd unit cấu hình sai.
Linux thống trị server và cloud, nên nó xứng đáng nhận phần lớn sự chú ý của bạn. Nhưng môi trường thực tế luôn hỗn hợp: bạn sẽ gặp Windows Server trong doanh nghiệp (Active Directory, IIS, workload .NET), và các hệ BSD trong thiết bị mạng và lưu trữ (pfSense, FreeNAS/TrueNAS). Hiểu khái niệm OS — process, memory, file system, I/O, socket — có thể chuyển giao giữa tất cả các hệ, và bền vững hơn nhiều so với việc thuộc lòng đặc thù của một distro.
Với DevOps, kiến thức OS xuất hiện hằng ngày: chọn kích cỡ instance, tune kernel parameter, viết systemd unit, đọc output top/vmstat giữa sự cố, hiểu vì sao container bị OOM-kill, và phân biệt “connection refused” với “connection timed out” khi debug. Chủ đề lặp lại là: các abstraction đều “rò rỉ”: container không phải một VM nhẹ, nó là một process Linux bị rào lại, và khi nó lỗi thì cái rào (namespace, cgroup, kernel dùng chung) là nơi bạn nhìn vào. Mọi tầng phía trên — Docker, Kubernetes, cloud console — rốt cuộc đều đang phát system call xuống kernel, và khi mọi thứ vỡ ở quy mô lớn, góc nhìn của kernel mới là sự thật.
Kiến thức nền tảng
Bức tranh các bản phân phối Linux
| Nhánh | Distro | Trình quản lý gói | Thường gặp ở đâu |
|---|---|---|---|
| Gốc Debian | Debian, Ubuntu | apt / .deb | Cloud VM, container, server đa dụng |
| Gốc Red Hat | RHEL, CentOS Stream, Rocky, AlmaLinux, Fedora | dnf/yum / .rpm | Doanh nghiệp, môi trường cần hỗ trợ dài hạn |
| Tối giản / container | Alpine, Flatcar, Bottlerocket | apk, dạng image | Base image container, node Kubernetes |
| SUSE | openSUSE, SLES | zypper / .rpm | Doanh nghiệp châu Âu, workload SAP |
| Khác | Arch, Amazon Linux, Gentoo | pacman, dnf | Workstation, AWS, build từ source |
Khác biệt thực dụng cần nhớ: trình quản lý gói và nhịp phát hành (Ubuntu LTS mỗi 2 năm, hỗ trợ 5 năm; RHEL vòng đời ~10 năm), module bảo mật (SELinux trên nhánh RHEL vs AppArmor trên Ubuntu/SUSE), vị trí file mặc định, và stack init/network mặc định. Alpine dùng musl libc thay vì glibc và BusyBox thay vì GNU coreutils — nguồn gốc của nhiều bug container khó hiểu (khác biệt về phân giải DNS, thiếu bash, hành vi date/getopt khác).
Một cách hình dung về distro “immutable” và tối ưu cho container (Flatcar, Bottlerocket, Talos): OS ở dạng read-only và được cập nhật như một image nguyên khối thay vì từng gói. Điều này loại bỏ configuration drift và ngày càng là cách các image node Kubernetes được dựng.
Unix (BSD) và Windows
- FreeBSD: nổi tiếng với network stack và hỗ trợ ZFS; nền tảng của firewall pfSense/OPNsense và (trước đây) các CDN appliance của Netflix. Dùng
pkgvà ports tree; init làrc.d, không phải systemd. Jail đi trước container Linux về mặt khái niệm. - OpenBSD: đặt bảo mật lên hàng đầu (“secure by default”); nơi sinh ra OpenSSH,
pf, vàLibreSSLmà bạn dùng mỗi ngày kể cả trên Linux. - macOS (Darwin): một UNIX được chứng nhận, xây trên kernel XNU; là workstation của phần lớn ngành. Userland kiểu BSD, đó là lý do một số flag GNU (
sed -i,date) khác với Linux — một cái bẫy portability thường gặp khi viết script. - Windows Server: quan trọng ở nơi có Active Directory, IIS, MSSQL hay .NET Framework. Nên học PowerShell, Windows service (
services.msc,sc.exe), Event Viewer, Task Scheduler, và WSL2 để có kernel Linux thật ngay trên Windows. Windows container có tồn tại nhưng rất nhỏ so với Linux container.
Kernel vs. user space và ranh giới system call
Kernel quản lý phần cứng, bộ nhớ, scheduling, và cưỡng chế cô lập; ứng dụng chạy ở user space và yêu cầu các dịch vụ đặc quyền qua system call (open, read, write, fork, execve, mmap, socket). Ranh giới này là mental model quan trọng bậc nhất trong công việc OS: một chương trình không thể chạm trực tiếp vào phần cứng hay bộ nhớ của process khác — nó phải hỏi kernel, và kernel quyết định. strace cho bạn nhìn ranh giới này theo thời gian thực và thường là cách nhanh nhất để trả lời “process này thực sự đang làm gì / chờ gì?”
strace -f -e trace=openat,connect -p <pid> # đang chạm vào file/socket nào?
strace -c ./program # tóm tắt số lần + thời gian mỗi syscall (tìm syscall nóng)
Process, thread và scheduling
- Process: một chương trình đang chạy với address space ảo riêng, PID, biến môi trường, file descriptor đang mở, credential và exit code. Được tạo bởi
fork()(nhân bản process cha) rồiexec()(thay thế image); process chawait()process con. Con đã thoát nhưng chưa được “reap” thành zombie (chỉ giữ PID + exit status); con có cha đã chết được nhận nuôi bởi PID 1 (orphan). - Thread: đơn vị thực thi chia sẻ address space và file descriptor của process, nên rẻ để tạo và để chuyển đổi, nhưng dễ dính data race; đồng bộ hóa bằng mutex, semaphore, condition variable. Trên Linux, thread chỉ là process chia sẻ bộ nhớ (với scheduler cả hai đều là “task”).
- Scheduling: scheduler CFS chia thời gian cho các task sẵn sàng chạy trên các CPU.
nice/renice(–20 đến +19) chỉnh độ ưu tiên;tasksetgim vào CPU; cgroup giới hạn CPU share. Độ dài run queue so với số CPU là tín hiệu bão hòa CPU hàng đầu. - POSIX: chuẩn về tính khả chuyển, định nghĩa shell, các tiện ích cơ bản và C API (signal, thao tác file,
pthreads). Đây là lý do một shell script hay chương trình C chạy được trên Linux, macOS lẫn FreeBSD, và vì sao “POSIX-compliant” là một lời hứa có ý nghĩa.
Khái niệm chính
Quản lý bộ nhớ và virtual memory
Mỗi process nhìn thấy một address space ảo riêng; kernel ánh xạ các page (thường 4 KB, tùy chọn “huge page” 2 MB/1 GB) vào RAM vật lý khi cần qua MMU, và một page chưa được chạm tới không tốn gì cho tới lần truy cập đầu tiên (demand paging). Những hệ quả bạn sẽ gặp trên production:
- Page cache: bộ nhớ “free” được tận dụng để cache dữ liệu đĩa —
free -hhiển thị “free” thấp nhưng “available” cao là bình thường và lành mạnh, không phải leak. Kernel sẽ thu hồi cache ngay lập tức khi có áp lực. - RSS vs. VSZ: Resident Set Size là RAM vật lý thực sự dùng; Virtual Size bao gồm mọi thứ được map (shared library, phần đặt trước nhưng chưa dùng, file map vào bộ nhớ) và gần như luôn lớn đến giật mình mà phần lớn vô nghĩa. Cảnh báo theo RSS/working set, không phải VSZ.
- Swap: phần bộ nhớ anonymous tràn ra đĩa. Có một ít swap là chấp nhận được; swap nặng (xem
si/sotrongvmstat) hủy hoại latency vì đĩa chậm hơn RAM hàng bậc độ lớn. Kubernetes trước đây tắt hẳn swap để giữ tính dự đoán được. - OOM killer: khi cạn bộ nhớ, kernel kill process có “badness score” cao nhất (
oom_score, tinh chỉnh quaoom_score_adj). Kiểm tra bằngdmesg | grep -i oom. Trong Kubernetes, vượt memory limit của container sẽ bị cgroup OOM kill (exit code 137 = 128 + SIGKILL/9). - Overcommit: Linux cấp phát bộ nhớ ảo nhiều hơn RAM vật lý thực có (
vm.overcommit_memory), nênmallocthành công nhưng khi thực sự ghi các page thì có thể bị OOM-kill — cấp phát và cam kết bị tách rời.
free -h # tổng quan bộ nhớ (chú ý "available", không phải "free")
vmstat 1 # cột si/so = hoạt động swap-in/swap-out
cat /proc/<pid>/status | grep -E 'VmRSS|VmSwap|VmHWM'
cat /proc/<pid>/oom_score # pid này "hấp dẫn" với OOM killer tới mức nào
dmesg -T | grep -i 'killed process' # nạn nhân của OOM killer kèm timestamp
Quản lý process và signal
ps aux --sort=-%mem | head # process ngốn RAM nhất
ps -ef --forest # cây process (quan hệ cha/con)
pgrep -af nginx # tìm PID theo tên, in cả command đầy đủ
kill -TERM <pid> # lịch sự: yêu cầu tự tắt (signal mặc định)
kill -KILL <pid> # phương án cuối: không thể bị bắt hay bỏ qua
kill -HUP <pid> # quy ước: reload cấu hình
nice -n 10 ./batch-job # khởi động với độ ưu tiên CPU thấp
renice -n 5 -p <pid> # đổi độ ưu tiên của process đang chạy
Cần thuộc các signal thông dụng và, quan trọng nhất, cái nào bắt được: SIGTERM (15, tắt êm — thứ Kubernetes và systemctl stop gửi trước, bắt được), SIGKILL (9, tức thì, không thể bắt hay chặn), SIGHUP (1, ngắt terminal, quy ước “reload config”), SIGINT (2, Ctrl-C), SIGSTOP/SIGCONT (tạm dừng/tiếp tục, cũng không bắt được), SIGCHLD (một process con đổi trạng thái). Việc shutdown “êm” — hoàn tất request đang xử lý, flush buffer, đóng kết nối — phụ thuộc hoàn toàn vào việc ứng dụng cài handler cho SIGTERM. Kubernetes gửi SIGTERM, chờ terminationGracePeriodSeconds (mặc định 30), rồi SIGKILL.
File system và I/O
- File system Linux phổ biến: ext4 (mặc định, ổn định, cân bằng tốt), XFS (mặc định trên RHEL, tuyệt vời với file lớn và I/O song song), Btrfs/ZFS (snapshot, checksum, copy-on-write, RAID tích hợp), overlayfs (union FS phân tầng mà container image được dựng lên từ đó), tmpfs (chạy trên RAM, ví dụ
/dev/shmvà nhiều mount/run). - Mọi thứ là file: file thường, thư mục, thiết bị (
/dev), trạng thái kernel/process (/proc,/sys), socket và pipe đều lộ ra qua cùng một file descriptor API (open/read/write/close). Sự đồng nhất này là một lựa chọn thiết kế đặc trưng của Unix. - Inode lưu metadata của file (quyền, chủ sở hữu, timestamp, con trỏ block) nhưng không lưu tên — tên nằm trong directory entry. Có thể hết inode (nhiều file tí hon) trong khi
df -hvẫn còn chỗ trống; kiểm tradf -i. - Hard link vs. symlink: hard link là một directory entry khác trỏ tới cùng inode (cùng file, nhiều tên, một bản trên đĩa); symlink là một file nhỏ chứa một đường dẫn (hỏng nếu target di chuyển).
- Quyền: bộ ba
rwxcho user/group/other, cộng các bit đặc biệt — setuid/setgid (chạy dưới quyền chủ file/nhóm), và sticky bit trên/tmp(chỉ chủ sở hữu mới xóa được file của mình). ACL (getfacl/setfacl) mở rộng khi mô hình cổ điển quá thô. - File descriptor là handle số nguyên của từng process tới file/socket đang mở; cạn fd gây lỗi “too many open files” — kiểm tra giới hạn bằng
ulimit -nvà mức dùng bằngls /proc/<pid>/fd | wc -l. - Đường đi của I/O: buffered I/O đi qua page cache và trả về nhanh;
fsync/fdatasyncép dữ liệu xuống lưu trữ bền vững, đó là lý do database cực kỳ quan tâm hành vi flush của đĩa và vì sao một đĩa “nói dối” về cache sẽ làm hỏng database khi mất điện.
df -h && df -i # dung lượng VÀ inode — kiểm tra cả hai
lsof +D /var/log | head # ai đang giữ file mở trong một đường dẫn
lsof -p <pid> # mọi thứ một process đang mở (file, socket, pipe)
iostat -xz 1 # latency (await) và mức sử dụng theo từng thiết bị
mount | column -t | head # cái gì được mount ở đâu, với option nào
du -sh /var/* | sort -h # tìm cái gì đang ngốn đĩa
Pseudo-filesystem /proc và /sys
/proc và /sys là kernel được phơi ra dưới dạng file — không cần công cụ đặc biệt, chỉ cần cat. Đây là nơi diễn ra phần lớn việc introspection sâu: /proc/<pid>/ chứa status, cmdline, environ, fd/, limits, cgroup của process; /proc/meminfo, /proc/loadavg, /proc/mounts, /proc/net/ phơi ra trạng thái hệ thống. /sys phơi cây thiết bị và kernel-object. Kernel tunable nằm dưới /proc/sys và được đặt bằng sysctl (lưu lâu dài trong /etc/sysctl.d/):
sysctl vm.swappiness # đọc một tunable
sysctl -w net.core.somaxconn=1024 # đặt lúc runtime (mất khi reboot)
cat /proc/1/cgroup # PID 1 thuộc cgroup nào
Socket và kiến thức mạng cơ bản
Socket là một endpoint truyền thông xác định bởi protocol + IP + port. TCP hướng kết nối (bắt tay ba bước, retransmission, đảm bảo thứ tự, điều khiển luồng/tắc nghẽn); UDP phi kết nối “bắn rồi quên” (không đảm bảo giao, ít overhead — dùng bởi DNS, QUIC, VXLAN). Hệ quả thực tế bạn sẽ chẩn đoán liên tục:
- “Connection refused” = host tới được và chủ động từ chối bạn — không có gì listen trên port đó (hoặc một gói
RSTtrả về). “Connection timed out” = gói tin bị drop âm thầm (firewall/security group, routing “hố đen”, hoặc host chết). Hai lỗi này chỉ về hai tầng hoàn toàn khác nhau. - Trạng thái kết nối TCP rất quan trọng:
LISTEN,ESTABLISHED,TIME_WAIT(bên đóng trước giữ socket ~60s để hấp thụ gói lạc). Ứ đọngTIME_WAITcộng cạn ephemeral port (dải/proc/sys/net/ipv4/ip_local_port_range) là lỗi kinh điển trên proxy chiều ra tải cao và máy tạo tải. - Unix domain socket (ví dụ
/var/run/docker.sock) là IPC chỉ cục bộ dùng đường dẫn và quyền file thay vì port — nhanh hơn loopback TCP và được bảo vệ bằng ACL của filesystem.
ss -tlnp # socket TCP đang listen + process sở hữu
ss -tan state established # các kết nối đã established
ss -s # thống kê số lượng theo trạng thái socket
Khởi động hệ thống và quản lý service: init và systemd
PID 1 là process user-space đầu tiên kernel khởi động, và là tổ tiên của mọi process (nó cũng reap các zombie mồ côi). Trước đây SysV init chạy các shell script theo thứ tự trong /etc/init.d với runlevel dạng số. Linux hiện đại dùng systemd, quản lý các unit — service, timer, mount, socket, target — với dependency khai báo, khởi động song song, giám sát process dựa trên cgroup, socket activation, và log tập trung qua journald.
# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network-online.target
Wants=network-online.target
[Service]
User=myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp.yaml
Restart=on-failure
RestartSec=5
# giới hạn tài nguyên (cùng cơ chế cgroup mà container dùng)
MemoryMax=512M
CPUQuota=50%
LimitNOFILE=65536
# hardening dạng sandbox
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload # đọc lại unit file sau khi sửa
sudo systemctl enable --now myapp # chạy ngay VÀ tự chạy khi boot
systemctl status myapp # trạng thái, log gần đây, PID chính, cgroup
systemctl list-units --failed # cái gì đang hỏng ngay lúc này
journalctl -u myapp -f --since "10 min ago" # theo dõi log của unit này
journalctl -p err -b # thông điệp mức error, trong lần boot này
systemd-analyze blame # các unit chậm nhất khi boot
systemctl mask <unit> # cưỡng chế ngăn một unit khởi động
Khái niệm liên quan: target (multi-user.target, graphical.target) thay thế runlevel; systemd timer là lựa chọn quan sát được và hiểu dependency hơn cron (chúng log vào journal và có thể “bắt” các lần chạy bị lỡ với Persistent=true); socket activation khởi động service khi có kết nối đầu tiên; và policy Restart= thay thế các script supervisor tự chế. Giới hạn MemoryMax/CPUQuota dùng chính cơ chế cgroups v2 mà container dùng — ở tầng này, systemd và Kubernetes đang làm cùng một việc.
Container là tính năng của OS, không phải phép màu
Đáng nói thẳng vì nó giải ảo rất nhiều: một container Linux là một process bình thường được kernel cô lập bằng namespace (tách biệt view về PID, mount, network interface, user, hostname, IPC) và ràng buộc bằng cgroup (giới hạn và tính toán CPU, memory, I/O, số PID). Không có đối tượng “container” nào trong kernel — Docker và containerd chỉ điều phối clone() với các cờ namespace, dựng root overlayfs, áp giới hạn cgroup, và (thường) một profile seccomp/AppArmor. Đó là lý do mọi khái niệm OS ở trên áp dụng trực tiếp bên trong container, và vì sao nsenter, /proc/<pid>/ns, và đọc file cgroup của một container là kỹ năng debug cốt lõi.
Best Practices
- Học một distro của mỗi nhánh lớn (Ubuntu + một bản clone RHEL như Rocky/Alma). Quản lý gói, công cụ bảo mật (SELinux vs AppArmor), và giá trị mặc định khác nhau; khái niệm nền tảng thì chuyển giao hoàn toàn.
- Chẩn đoán từ OS đi lên, không phải từ app đi xuống. Khi có sự cố, kiểm tra bốn thứ cơ bản — CPU, memory, disk, network (
top,free -h,df -h/df -i,ss) — trước khi lao vào log ứng dụng. OS thường đã biết câu trả lời. - Coi bộ nhớ “available”, không phải “free”, là headroom thật. Page cache có thể thu hồi khi cần; cảnh báo dựa trên “free” tạo báo động giả và che giấu leak thật.
- Luôn gửi SIGTERM trước SIGKILL, và xây ứng dụng biết shutdown êm khi nhận SIGTERM — Kubernetes, systemd và
docker stopđều dựa vào giao ước này. Thiếu handler SIGTERM nghĩa là mất request và hỏng state mỗi lần deploy. - Chạy service dưới systemd (hoặc supervisor của platform), đừng bao giờ
nohup ... &. Bạn có sẵn restart tự động, thu log, sắp thứ tự dependency, giới hạn tài nguyên, và sandbox — cùng một process sống sót sau khi session SSH của bạn kết thúc. - Đặt giới hạn tài nguyên tường minh (
ulimit/LimitNOFILE,MemoryMax, request/limit của container) để một process mất kiểm soát không làm đói hay kéo sập cả host. “Không giới hạn” là một quyết định, thường là quyết định sai. - Giám sát inode và file descriptor, không chỉ dung lượng đĩa. Hết inode (
df -i) và cạn fd làm hệ thống chết trong khidf -hvà dashboard vẫn xanh. - Vá hệ thống theo lịch cố định — bật cập nhật bảo mật tự động khi an toàn, theo dõi chiến lược reboot kernel (hoặc dùng livepatch/kexec), và biết mức phơi nhiễm CVE. Kernel chưa vá là con đường leo thang đặc quyền phổ biến nhất.
- Đừng tắt SELinux/AppArmor theo phản xạ. Học
audit2allow/aa-logprofđể sửa đúng cái bị chặn. Tắt LSM là một bước lùi bảo mật vĩnh viễn đổi lấy một phiền toái nhất thời. - Dùng
journalctlvàdmesgnhư công cụ xử lý sự cố hạng nhất. OOM kill, lỗi đĩa, segfault, và cảnh báo kernel nằm ở đó, không nằm trong log ứng dụng. Dùngdmesg -Tđể có timestamp dễ đọc. - Hiểu file system trước khi chọn nó. Snapshot và checksum (ZFS/Btrfs), hành vi với file lớn và I/O song song (XFS), và ngữ nghĩa bền vững của
fsyncquyết định trực tiếp độ an toàn của database và backup. - Tune kernel parameter có chủ đích và lưu lâu dài. Giới hạn file descriptor,
vm.swappiness,net.core.somaxconn,net.ipv4.tcp_tw_reuse, dải ephemeral port — đặt trong/etc/sysctl.d/(không chỉ lúc runtime) kèm comment giải thích vì sao. - Right-size trước khi scale-out. Đọc
vmstat/iostat/sarđể biết workload nghẽn CPU, memory hay I/O; nút thắt quyết định loại instance và liệu thêm replica có giúp ích không. - Ưu tiên node immutable, dạng image (Bottlerocket/Flatcar/Talos, hoặc golden AMI) thay vì các “pet” sống lâu vá tại chỗ. Hạ tầng thay thế được sẽ loại bỏ configuration drift.
- Hiểu nền tảng namespace/cgroup của container. Khi một pod bị OOM-kill, bị throttle, hay “không thấy” mạng, câu trả lời nằm ở primitive của OS, không phải ở abstraction của Kubernetes.
- Luyện tập trên VM dùng xong vứt. Chủ động phá hỏng — làm đầy đĩa, kích hoạt OOM killer, làm cạn file descriptor, mask một unit, làm bão hòa CPU — để lần đầu bạn thấy “chữ ký” của lỗi không phải trên production.
Tài liệu tham khảo
- DevOps Roadmap — roadmap.sh/devops
- Tài liệu Linux Kernel
- The Linux man-pages project
- Tài liệu Ubuntu Server
- Tài liệu Red Hat Enterprise Linux
- Arch Linux Wiki — trung lập với distro, chất lượng cao
- Tài liệu systemd và man page systemd.service
- FreeBSD Handbook
- OpenBSD FAQ
- Tài liệu Microsoft Windows Server
- The Linux Documentation Project
- Brendan Gregg — Linux Performance
- Julia Evans — zine & blog về nội thất Linux
- Sách: Operating Systems: Three Easy Pieces — Arpaci-Dusseau (đọc miễn phí)
- Sách: How Linux Works — Brian Ward (No Starch Press)
- Sách: The Linux Programming Interface — Michael Kerrisk (No Starch Press)
- Sách: Systems Performance — Brendan Gregg (trang tác giả)
Part of the DevOps Roadmap knowledge base.
Overview
The operating system is the layer every DevOps engineer ultimately stands on. Containers are Linux processes with namespaces and cgroups; Kubernetes nodes are Linux machines; CI runners, databases, and load balancers all live or die by OS-level behavior — memory pressure, file descriptor limits, scheduler decisions, disk I/O. When an application “randomly” dies, the answer is usually in the OS: the OOM killer, a full disk, an exhausted inode table, or a misconfigured systemd unit.
Linux dominates servers and the cloud, so it deserves most of your attention. But real environments are heterogeneous: you will meet Windows Server in enterprise shops (Active Directory, IIS, .NET workloads), and BSD systems in networking appliances and storage (pfSense, FreeNAS/TrueNAS). Understanding OS concepts — processes, memory, file systems, I/O, sockets — transfers across all of them and is far more durable than memorizing any single distro’s quirks.
For DevOps specifically, OS knowledge shows up daily: sizing instances, tuning kernel parameters, writing systemd units, reading top/vmstat output during incidents, understanding why a container was OOM-killed, and debugging “connection refused” versus “connection timed out.” The recurring theme is that abstractions leak: a container is not a lightweight VM, it is a fenced-off Linux process, and when it misbehaves the fence (namespaces, cgroups, the shared kernel) is where you look. Every layer above — Docker, Kubernetes, the cloud console — is ultimately issuing system calls to a kernel, and when things break at scale, the kernel’s view is the truth.
Fundamentals
The Linux distribution landscape
| Family | Distros | Package manager | Where you’ll see it |
|---|---|---|---|
| Debian-based | Debian, Ubuntu | apt / .deb | Cloud VMs, containers, general servers |
| Red Hat-based | RHEL, CentOS Stream, Rocky, AlmaLinux, Fedora | dnf/yum / .rpm | Enterprise, long-term-support environments |
| Minimal / container | Alpine, Flatcar, Bottlerocket | apk, image-based | Container base images, Kubernetes nodes |
| SUSE | openSUSE, SLES | zypper / .rpm | European enterprises, SAP workloads |
| Others | Arch, Amazon Linux, Gentoo | pacman, dnf | Workstations, AWS, source-based |
Key practical differences: package manager and release cadence (Ubuntu LTS every 2 years with 5 years support, RHEL ~10-year lifecycle), security module (SELinux on the RHEL family vs AppArmor on Ubuntu/SUSE), default file locations, and default init/network stacks. Alpine uses musl libc instead of glibc and BusyBox instead of GNU coreutils — a common source of subtle container bugs (DNS resolution differences, missing bash, different date/getopt behavior).
A practical framing of “immutable” and container-optimized distros (Flatcar, Bottlerocket, Talos): the OS is read-only and updated as an atomic image rather than package-by-package. This eliminates configuration drift and is increasingly how Kubernetes node images are built.
Unix (BSD) and Windows
- FreeBSD: renowned networking stack and ZFS support; basis of pfSense/OPNsense firewalls and (historically) Netflix’s CDN appliances. Uses
pkgand the ports tree; init isrc.d, not systemd. Jails predate Linux containers conceptually. - OpenBSD: security-first (“secure by default”); origin of OpenSSH,
pf, andLibreSSL, which you use every day even on Linux. - macOS (Darwin): a certified UNIX built on the XNU kernel; the developer workstation of much of the industry. Behaves like BSD userland, which is why some GNU flags (
sed -i,date) differ from Linux — a frequent scripting portability trap. - Windows Server: matters where Active Directory, IIS, MSSQL, or .NET Framework live. Learn PowerShell, Windows services (
services.msc,sc.exe), Event Viewer, Task Scheduler, and WSL2 for a real Linux kernel on Windows. Windows containers exist but are niche compared to Linux containers.
Kernel vs. user space and the system call boundary
The kernel manages hardware, memory, scheduling, and enforces isolation; applications run in user space and request privileged services via system calls (open, read, write, fork, execve, mmap, socket). This boundary is the single most important mental model in OS work: a program cannot touch hardware or another process’s memory directly — it asks the kernel, which decides. strace shows you this boundary live and is often the fastest way to answer “what is this process actually doing / waiting on?”
strace -f -e trace=openat,connect -p <pid> # what files/sockets is it touching?
strace -c ./program # syscall count + time summary (find the hot syscall)
Processes, threads, and scheduling
- Process: a running program with its own virtual address space, PID, environment, open file descriptors, credentials, and exit code. Created by
fork()(clone the parent) thenexec()(replace the image); parentswait()on children. A child that has exited but not been reaped becomes a zombie (holds only a PID + exit status); a child whose parent died is re-parented to PID 1 (orphan). - Threads: units of execution that share their process’s address space and file descriptors, making them cheap to create and to switch between, but prone to data races; synchronization uses mutexes, semaphores, and condition variables. On Linux, threads are just processes that share memory (both are “tasks” to the scheduler).
- Scheduling: the CFS scheduler time-slices runnable tasks across CPUs.
nice/renice(–20 to +19) adjusts priority;tasksetpins to CPUs; cgroups cap CPU shares. The run queue length relative to CPU count is your headline CPU-saturation signal. - POSIX: the portability standard defining the shell, core utilities, and the C API (signals, file operations,
pthreads). It is why a shell script or C program can run on Linux, macOS, and FreeBSD alike, and why “POSIX-compliant” is a meaningful promise.
Key Concepts
Memory management and virtual memory
Each process sees a private virtual address space; the kernel maps pages (typically 4 KB, optionally 2 MB/1 GB “huge pages”) to physical RAM on demand via the MMU, and a page not yet touched costs nothing until first access (demand paging). Consequences you will meet in production:
- Page cache: “free” memory is used to cache disk data —
free -hshowing low “free” but high “available” is healthy, not a leak. The kernel will evict cache instantly under pressure. - RSS vs. VSZ: Resident Set Size is physical RAM actually used; Virtual Size includes everything mapped (shared libraries, reserved-but-unused, memory-mapped files) and is almost always alarmingly large and mostly meaningless. Alert on RSS/working set, not VSZ.
- Swap: overflow of anonymous memory to disk. Some swap is fine; heavy swapping (see
si/soinvmstat) destroys latency because disk is orders of magnitude slower than RAM. Kubernetes historically disabled swap entirely for predictability. - OOM killer: under memory exhaustion the kernel kills the process with the highest badness score (
oom_score, tunable viaoom_score_adj). Check withdmesg | grep -i oom. In Kubernetes, exceeding a container memory limit triggers a cgroup OOM kill (exit code 137 = 128 + SIGKILL/9). - Overcommit: Linux grants more virtual memory than physical RAM exists (
vm.overcommit_memory), which is whymallocsucceeds but actually writing the pages later can get you OOM-killed — allocation and commitment are decoupled.
free -h # memory overview (watch "available", not "free")
vmstat 1 # si/so columns = swap-in/swap-out activity
cat /proc/<pid>/status | grep -E 'VmRSS|VmSwap|VmHWM'
cat /proc/<pid>/oom_score # how attractive this pid is to the OOM killer
dmesg -T | grep -i 'killed process' # OOM killer victims with timestamps
Process management and signals
ps aux --sort=-%mem | head # top memory consumers
ps -ef --forest # process tree (parent/child relationships)
pgrep -af nginx # find PIDs by name, show the full command
kill -TERM <pid> # polite: ask to shut down (default signal)
kill -KILL <pid> # last resort: cannot be caught or ignored
kill -HUP <pid> # convention: reload configuration
nice -n 10 ./batch-job # start with lower CPU priority
renice -n 5 -p <pid> # change priority of a running process
Know the common signals and, critically, which can be caught: SIGTERM (15, graceful stop — what Kubernetes and systemctl stop send first, catchable), SIGKILL (9, immediate, uncatchable and unblockable), SIGHUP (1, terminal hangup, conventionally “reload config”), SIGINT (2, Ctrl-C), SIGSTOP/SIGCONT (pause/resume, also uncatchable), SIGCHLD (a child changed state). Graceful shutdown — finishing in-flight requests, flushing buffers, closing connections — depends entirely on your application installing a SIGTERM handler. Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds (default 30), then SIGKILL.
File systems and I/O
- Common Linux file systems: ext4 (default, reliable, good all-rounder), XFS (RHEL default, excellent for large files and parallel I/O), Btrfs/ZFS (snapshots, checksums, copy-on-write, built-in RAID), overlayfs (the layered union FS that container images are built from), tmpfs (RAM-backed, e.g.
/dev/shmand many/runmounts). - Everything is a file: regular files, directories, devices (
/dev), kernel/process state (/proc,/sys), sockets, and pipes all present through the same file descriptor API (open/read/write/close). This uniformity is a defining Unix design choice. - Inodes store file metadata (permissions, owner, timestamps, block pointers) but not the name — names live in directory entries. You can run out of inodes (many tiny files) while
df -hstill shows free space; checkdf -i. - Hard links vs. symlinks: a hard link is another directory entry pointing at the same inode (same file, multiple names, one on-disk copy); a symlink is a small file containing a path (breaks if the target moves).
- Permissions: the
rwxtriad for user/group/other, plus special bits — setuid/setgid (run as file owner/group), and the sticky bit on/tmp(only the owner can delete their files). ACLs (getfacl/setfacl) extend this when the classic model is too coarse. - File descriptors are per-process integer handles to open files/sockets; exhaustion causes “too many open files” — check limits with
ulimit -nand usage withls /proc/<pid>/fd | wc -l. - I/O paths: buffered I/O goes through the page cache and returns fast;
fsync/fdatasyncforces data to durable storage, which is why databases care so deeply about disk flush behavior and why a lying disk cache corrupts databases on power loss.
df -h && df -i # space AND inode usage — check both
lsof +D /var/log | head # who holds files open under a path
lsof -p <pid> # everything a process has open (files, sockets, pipes)
iostat -xz 1 # per-device I/O latency (await) and utilization
mount | column -t | head # what is mounted where, with which options
du -sh /var/* | sort -h # find what is eating disk space
The /proc and /sys pseudo-filesystems
/proc and /sys are the kernel exposed as files — no special tools required, just cat. This is where much of the deep introspection happens: /proc/<pid>/ holds a process’s status, cmdline, environ, fd/, limits, and cgroup; /proc/meminfo, /proc/loadavg, /proc/mounts, and /proc/net/ expose system state. /sys exposes device and kernel-object trees. Kernel tunables live under /proc/sys and are set with sysctl (persisted in /etc/sysctl.d/):
sysctl vm.swappiness # read a tunable
sysctl -w net.core.somaxconn=1024 # set at runtime (lost on reboot)
cat /proc/1/cgroup # which cgroups PID 1 belongs to
Sockets and networking basics
A socket is a communication endpoint identified by protocol + IP + port. TCP is connection-oriented (three-way handshake, retransmission, ordering, flow/congestion control); UDP is connectionless “fire-and-forget” (no delivery guarantee, lower overhead — used by DNS, QUIC, VXLAN). Practical implications you will diagnose constantly:
- “Connection refused” = the host is reachable and actively rejected you — nothing is listening on that port (or a
RSTcame back). “Connection timed out” = packets are being silently dropped (firewall/security group, routing black hole, or a dead host). These two errors point at completely different layers. - TCP connection states matter:
LISTEN,ESTABLISHED,TIME_WAIT(the side that closed first holds the socket ~60s to absorb stray packets). Piles ofTIME_WAITplus ephemeral port exhaustion (the/proc/sys/net/ipv4/ip_local_port_rangepool) is a classic failure on busy outbound proxies and load generators. - Unix domain sockets (e.g.
/var/run/docker.sock) are local-only IPC using file-system paths and permissions instead of ports — faster than loopback TCP and secured by filesystem ACLs.
ss -tlnp # listening TCP sockets + owning process
ss -tan state established # established connections
ss -s # socket state summary counts
Startup and service management: init and systemd
PID 1 is the first user-space process the kernel starts, and the ancestor of everything (it also reaps orphaned zombies). Historically SysV init ran ordered shell scripts in /etc/init.d with numeric runlevels. Modern Linux uses systemd, which manages units — services, timers, mounts, sockets, targets — with declarative dependencies, parallel startup, cgroup-based process supervision, socket activation, and centralized logging via journald.
# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network-online.target
Wants=network-online.target
[Service]
User=myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp.yaml
Restart=on-failure
RestartSec=5
# resource limits (same cgroup mechanism containers use)
MemoryMax=512M
CPUQuota=50%
LimitNOFILE=65536
# sandboxing hardening
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload # re-read unit files after editing
sudo systemctl enable --now myapp # start now AND on boot
systemctl status myapp # state, recent logs, main PID, cgroup
systemctl list-units --failed # what is broken right now
journalctl -u myapp -f --since "10 min ago" # follow this unit's logs
journalctl -p err -b # error-priority messages, this boot
systemd-analyze blame # slowest units at boot
systemctl mask <unit> # forcibly prevent a unit from starting
Related concepts: targets (multi-user.target, graphical.target) replace runlevels; systemd timers are a more observable, dependency-aware alternative to cron (they log to the journal and can catch missed runs with Persistent=true); socket activation starts a service on first connection; and Restart= policies replace hand-rolled supervisor scripts. The MemoryMax/CPUQuota limits use the same cgroups v2 mechanism that containers use — systemd and Kubernetes are, at this layer, doing the same thing.
Containers are OS features, not magic
It is worth stating explicitly because it demystifies so much: a Linux container is a normal process that the kernel has isolated using namespaces (separate views of PIDs, mounts, network interfaces, users, hostnames, IPC) and constrained using cgroups (limits and accounting for CPU, memory, I/O, PIDs). There is no “container” object in the kernel — Docker and containerd just orchestrate clone() with namespace flags, set up an overlayfs root, apply cgroup limits, and (usually) a seccomp/AppArmor profile. This is why every OS concept above applies directly inside containers, and why nsenter, /proc/<pid>/ns, and reading a container’s cgroup files are core debugging skills.
Best Practices
- Learn one distro from each major family (Ubuntu + a RHEL clone like Rocky/Alma). Package management, security tooling (SELinux vs AppArmor), and defaults differ; the underlying concepts transfer completely.
- Diagnose from the OS up, not the app down. During incidents check the four fundamentals — CPU, memory, disk, network (
top,free -h,df -h/df -i,ss) — before diving into application logs. The OS often already knows the answer. - Treat “available” memory, not “free”, as your headroom. Page cache is reclaimable on demand; alarms on “free” memory create false pages and hide real leaks.
- Always send SIGTERM before SIGKILL, and build applications that shut down gracefully on SIGTERM — Kubernetes, systemd, and
docker stopall rely on this contract. A missing SIGTERM handler means dropped requests and corrupt state on every deploy. - Run services under systemd (or the platform’s supervisor), never
nohup ... &. You get automatic restarts, log capture, dependency ordering, resource limits, and sandboxing for free — and a process that survives your SSH session ending. - Set explicit resource limits (
ulimit/LimitNOFILE,MemoryMax, container requests/limits) so one runaway process cannot starve or crash the whole host. Unlimited is a decision, usually the wrong one. - Monitor inodes and file descriptors, not just disk bytes.
df -irunning out and fd exhaustion take systems down whiledf -hand dashboards still look green. - Keep systems patched on a schedule — enable unattended security updates where safe, track your kernel-reboot strategy (or use livepatch/kexec), and know your CVE exposure. Unpatched kernels are the most common privilege-escalation vector.
- Don’t disable SELinux/AppArmor reflexively. Learn
audit2allow/aa-logprofto fix the actual denial. A disabled LSM is a permanent security regression traded for a one-time inconvenience. - Use
journalctlanddmesgas first-class incident tools. OOM kills, disk errors, segfaults, and kernel warnings land there, not in application logs.dmesg -Tfor human timestamps. - Understand your file system before you choose it. Snapshots and checksums (ZFS/Btrfs), large-file and parallel-I/O behavior (XFS), and
fsyncdurability semantics directly determine database and backup safety. - Tune kernel parameters deliberately and persist them. File descriptor limits,
vm.swappiness,net.core.somaxconn,net.ipv4.tcp_tw_reuse, ephemeral port range — set them in/etc/sysctl.d/(not just at runtime) with a comment explaining why. - Right-size before you scale out. Read
vmstat/iostat/sarto learn whether a workload is CPU-, memory-, or I/O-bound; the bottleneck dictates the instance type and whether more replicas even help. - Prefer immutable, image-based nodes (Bottlerocket/Flatcar/Talos, or golden AMIs) over long-lived pets you patch in place. Replaceable infrastructure eliminates configuration drift.
- Understand the namespace/cgroup basis of containers. When a pod is OOM-killed, throttled, or “cannot see” the network, the answer is in the OS primitives, not in Kubernetes abstractions.
- Practice on disposable VMs. Break things deliberately — fill a disk, trigger the OOM killer, exhaust file descriptors, mask a unit, saturate CPU — so the first time you see the failure signature is not in production.
References
- DevOps Roadmap — roadmap.sh/devops
- The Linux Kernel documentation
- The Linux man-pages project
- Ubuntu Server documentation
- Red Hat Enterprise Linux documentation
- Arch Linux Wiki — distro-neutral, high quality
- systemd documentation and systemd.service man page
- FreeBSD Handbook
- OpenBSD FAQ
- Microsoft Windows Server documentation
- The Linux Documentation Project
- Brendan Gregg — Linux Performance
- Julia Evans — zines & blog on Linux internals
- Book: Operating Systems: Three Easy Pieces — Arpaci-Dusseau (free online)
- Book: How Linux Works — Brian Ward (No Starch Press)
- Book: The Linux Programming Interface — Michael Kerrisk (No Starch Press)
- Book: Systems Performance — Brendan Gregg (author site)